Dieser Artikel ist auch auf Deutsch verfügbar
Two projects are celebrating a milestone birthday in the Spring universe this year. The Spring Framework is 20 years old and Spring Boot is 10 years old. In addition to several companies as employers for the core team, the Spring Framework in particular has seen, contributed, and influenced a lot during this time.
For example, the very long (and precise, for people who are familiar with the concepts) class names, such as AbstractAnnotationConfigDispatcherServletInitializer, are so well known that everyone has probably heard a joke about them at one point or another. There are even entire websites dedicated to this topic, such as https://projects.haykranen.nl/java/.
The second thing that everyone dealing with Spring thinks about is annotations. Nowadays, they are not limited to Spring but can be found virtually everywhere in the Java universe. Since they were introduced with JDK 5.0 19 years ago, they have increasingly become the tool of choice for many things.
But if annotations have been around for 19 years and the Spring Framework for 20 years, and they are a central part of Spring, how did everything work in the beginning when they did not yet exist? This is what we want to examine below.
Dependency Injection
The key functionality that the Spring Framework has always offered is to implement the Dependency Injection pattern. The IoC container (Inversion of Control) is used for this purpose. Classes can be registered in this container, for which the container can then create instances called Spring Beans. When such a bean is created, its dependencies are also created and set in the instance, nowadays usually via the constructor.
Before the Spring Framework, there were two options for dependency injection. The first option was, and still is, implementing it manually. To do this (see Listing 1), we create all instances in the appropriate places ourselves and thus ensure that our entire object graph is correctly initialized.
As long as all these instances are singletons (i.e. it is sufficient to have a single instance for the entire application), this works very well. To accomplish this, we create all instances once at the beginning in the correct order, and we’re done. It becomes more complicated when we need a different scope for individual instances. For example, the current transaction or the logged-in user is often attached to the thread that is currently processing the request. Accordingly, we cannot even instantiate such a “dependency” at the beginning. However, we can inject an “auxiliary class” for this purpose, which we can then use to activate a method to get to the required instance without knowing the details.
However, since it was already common in the early days of Jakarta EE (which was still called J2EE at that time) that the provided infrastructure had to create instances of our classes and therefore only the public, argumentless constructor was activated, the Service Locator pattern became widespread (see Listing 2). In this scenario, there is a class that can be used to access all other relevant objects at any time. As this requires global access, there is either a static instance of this class or the methods for obtaining other objects are static.
With the spread of Enterprise Java Beans or Jakarta Enterprise Beans, it became common to use a container that manages the instances of our classes at runtime. In order for this container to recognize our classes, the declaration via XML (see Listing 3) was used. At startup, the EJB container reads this declaration, allowing it to create instances of our classes. The Java Naming and Directory Interface was typically used to access such an instance, and this code was generally also encapsulated in a service locator (see Listing 4).
Since the declaration in XML is very repetitive and error-prone, a simplification was quickly wanted. The first solution was the use of special Doclets. Doclets allow JavaDoc comments to be parsed and processed. The “standard” doclet generates the Java Doc API documentation that we are all familiar with. With XDoclet, a tool was created that makes it possible to generate the XML declaration (see Listing 5). For this purpose, we use specific JavaDoc tags, such as @ejb.bean
, which can be used to parameterize the generation of the XML file. This also made it possible to generate the local and home interfaces belonging to the EJB. Consistent with other developments at the time, the Spring Framework also started with a declaration of its Spring Beans in XML (see Listing 6).
In contrast to the more common EJB containers at the time, however, it was possible to directly inject other objects. This meant that it was no longer necessary to use the Service Locator pattern. However, writing XML files was also repetitive and error-prone. As a result, XDoclet support for the Spring Framework was quickly available.
Only the annotations introduced in JDK 5.0 slowly but surely shifted the declaration of separate XML files directly into our code (see Listing 7).
Routing of HTTP requests
Web applications use HTTP as a protocol and therefore offer different functionalities under different paths, depending on the applied HTTP method and sometimes also based on different HTTP headers. For example, a GET /pets
can return a list of cuddly toys, while a POST /pets
creates a new one. To respond to these requests, we need to have a piece of code somewhere in our application that decides which code to execute based on the specific request. This is generally referred to as routing.
Traditionally, in Java, the Servlet API is used to respond to HTTP requests on the server side. Since this programming interface also comes from an annotation-free era, XML was also used here via a web.xml file (see Listing 8). This configuration consists of two parts. First, we must register our servlets with a name, similar to the Spring IoC container. Then we can map one or more URIs to a previously registered servlet. Similar to dependency injection, the path here also went via XDoclets (see Listing 9) to annotations (see Listing 10).
And Spring MVC, the web part of the Spring Framework, also began with a servlet-like API (see Listing 11) and mapping that needed to be explicitly defined via XML (see Listing 12). It was not until Spring Framework 2.5 that the annotations we use today were added. Besides saving us from having to explicitly enter mappings in an XML file, switching to these annotations also expanded the mapping options. Whereas previously it was only possible to map URI patterns to a single class, we can now map HTTP requests to specific methods within our class using other properties such as the HTTP method or specific HTTP headers (see Listing 13).
However, annotations are not necessarily required for routing either. Spring now has an option for this task, if we use Spring WebFlux (see Listing 14). The routes are registered at a router via builder methods. Each route receives a method reference as the target, which must correspond to a specific signature, namely an argument of the type ServerRequest
and Mono<ServerResponse>
as the return type. Unfortunately, there is no equivalent for the traditional servlet-based method yet. Nonetheless, just like with the first two areas, we can see that a world without annotations is also possible for the routing of HTTP requests.
Mapping
Nowadays, we also often use annotations for mapping data to instances of a class. Although many of the libraries or frameworks today use sensible defaults, which significantly reduce the number of annotations, we still have to use an annotation here and there.
Examples for this type of mapping include Jakarta Persistence (JPA), Jackson, and MapStruct. For example, JPA (see Listing 15) requires at least the information that the class should be a JPA entity and which field the ID of this entity corresponds to. Here, too, it is possible to define this information via XML (see Listing 16) rather than via annotation.
The alternative, which is then no longer compatible with JPA, would be to implement the mapping between our tables and our classes ourselves, for example with Java Database Connectivity (JDBC).
In addition to mapping classes to relational tables, the use of annotations is also common for serializing or deserializing classes to data formats such as JSON or XML. Libraries such as Jackson can ideally manage completely without them, but as soon as we have more specific requirements, we still need them to express these requirements (see Listing 17).
Here, too, the alternative is to write the mapping manually and use one of several libraries with which we can parse or generate JSON.