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.

var environment = Environment.from(args);
var datasource = dataSourceFor(environment);
var greetingRepository = new GreetingRepository(datasource);
var helloWorldController =
    new HelloWorldController(greetingRepository);
Listing 1: Dependency Injection by hand

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.

private GreetingRepository repository;

public HelloWorldController() {
    this.repository =
        ServiceLocator.INSTANCE.getGreetingRepository();
}
Listing 2: Retrieve dependencies via Service Locator

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).

<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar ...>
    <enterprise-beans>
        <session>
            <ejb-name>HelloSessionBean</ejb-name>
            <mapped-name>ejb/HelloSessionBean</mapped-name>
            <business-local>...HelloSessionBeanLocal</business-local>
            <business-remote>...HelloSessionBeanRemote</business-remote>
            <ejb-class>...HelloSessionBean</ejb-class>
            <session-type>Stateless</session-type>
            <transaction-type>Container</transaction-type>
        </session>
    </enterprise-beans>
</ejb-jar>
Listing 3: Stateless EJB declaration in XML
public class ServiceLocator {

    static final ServiceLocator INSTANCE =
        new ServiceLocator();

    private final InitialContext context;

    private ServiceLocator() {
        try {
            var properties = new Properties();
            // ...
            this.context = new InitialContext(properties);
        } catch (NamingException e) {
            throw new IllegalStateException("Unable to create", e);
        }
    }

    public GreetingRepository getGreetingRepository() {
        return (GreetingRepository) context.lookup("JNDI_NAME");
    }
}
Listing 4: Service Locator using JNDI Lookup

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).

/**
 * @ejb.bean name="HelloSessionBean" type="Stateless"
 */
public class HelloSessionBean implements SessionBean {
    // ...
}
Listing 5: EJB with XDoclet-Tag
<beans>
    <bean id="greetingRepository" class="...GreetingRepository">
        <property name="datasource" ref="datasource" />
    </bean>

    <bean id="helloWorldController" class= "...HelloWorldController">
        <property name="repository" ref="greetingRepository" />
    </bean>
</beans>
Listing 6: Spring-IoC-Container configuration in XML

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).

@Repository
public class GreetingRepository {
    // behaves like it was annotated with @Autowired
    public GreetingRepository(DataSource datasource) {
        // ...
    }
}
Listing 7: Spring-Annotations

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).

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
        xmlns="http://java.sun.com/xml/ns/javaee" ...>
    <servlet>
        <servlet-name>HelloWorldServlet</servlet-name>
        <servlet-class>com.innoq.HelloWorldServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HelloWorldServlet</servlet-name>
        <url-pattern>/greeting</url-pattern>
    </servlet-mapping>
</web-app>
Listing 8: Servlet-Mapping in XML
/**
 * @web.servlet name="HelloWorldServlet"
 * @web.servlet-mapping url-pattern="/greeting"
 */
public class HelloWorldServlet extends HttpServlet {
    // ...
}
Listing 9: XDoclet-Tags for Servlet-Mapping
@WebServlet(name = "HelloWorldServlet", urlPatterns = "/greeting")
public class HelloWorldServlet extends HttpServlet {
    // ...
}
Listing 10: Annotations for Servlet-Mapping

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).

public class HelloWorldController implements Controller {
    @Override
    public ModelAndView handleRequest(
            HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        // ...
    }
}
Listing 11: Old Spring-Controller
<beans>
    <bean id="handlerMapping" class="...SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/greeting*">helloWorldController</prop>
            </props>
        </property>
    </bean>

    <bean id="helloWorldController" class="com.innoq.HelloWorldController" />
</beans>
Listing 12: XML-Mapping for Spring-Controller
@Controller
public class HelloWorldController {
    @GetMapping("/greeting")
    public ModelAndView greet() {
        // ...
    }
}
Listing 13: Spring-Controller with Annotations

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.

var handler = new HelloWorldController();
var router = route()
    .GET("/greeting", handler::greet)
    .build();

public class HelloWorldController {
    public Mono<ServerResponse> greet(ServerRequest request) {
        // ...
    }
}
Listing 14: Functional routing with Spring WebFlux

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.

@Entity
public class Article {
    @Id
    private Long id;

    // ...
}
Listing 15: JPA-Mapping with Annotations
<entity-mappings>
    <entity class="com.innoq.Article" name="Article">
        <attributes>
            <id name="id"></id>
        </attributes>
    </entity>
</entity-mappings>
Listing 16: JPA-Mapping with XML

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).

@JsonInclude(Include.NON_NULL)
public class Article {
    @JsonIgnore
    public long id;

    @JsonFormat(
        shape = JsonFormat.Shape.STRING,
        pattern = "dd-MM-yyyy hh:mm:ss")
    public Date createdAt;

    @JsonProperty("name")
    public String fullName;

    // ...
}
Listing 17: Customizing Jackson with Annotations

Here, too, the alternative is to write the mapping manually and use one of several libraries with which we can parse or generate JSON.

Conclusion

We have looked at three use cases here, in which we generally rely on annotations today: Dependency injection, routing HTTP requests, and mapping.

In addition to some historical background, we have seen that before annotations, these cases were mainly handled using a declarative approach via an external format such as XML. This method was then slightly adapted to the code because it is repetitive and error-prone. As an intermediate step, XDoclet was initially used to generate the declaration and ultimately switched to annotations and their evaluation at runtime.

Alternatively, these use cases can also be solved purely via code. For dependency injection, we can build the graph of our objects ourselves. Today, HTTP requests can also be routed via an object and the registration of method references. And the mapping of data to instances (or vice versa) could also be accomplished with a little bit of coding.

Other use cases in which we often use annotations today are cross-cutting aspects such as controlling transactions, generating code, for example using Lombok, or for lifecycle methods such as @PostConstruct or @PreDestroy. But even for these cases, there is a way to solve the problem without annotation by explicitly using templates, such as the TransactionTemplate, writing our own code, or by implementing suitable interfaces.

There is no simple answer as to which approach is better. Annotations that are evaluated at runtime allow us to express what is needed in a declarative way with very little code. However, the disadvantage of this approach is that we cannot debug what happens there, which is why we must truly grasp it to avoid introducing errors. Too much annotation can also negatively impact comprehension. Writing your own code, on the other hand, is very easy to debug. Mind you, coding can also be error-prone, and usually more code needs to be read to understand it. In these cases, the distance between different parts of the code often increases. For example, I can no longer see directly from a class which JSON fields were used to fill it.

As always, there is no magic bullet to solve all issues. The best approach is to evaluate the options in the specific context and then select the most suitable solution. That’s why I think it’s important not to rule out any of the options outright for dogmatic reasons. But I must admit that even I don’t always follow this advice.