Comparing the definition of beans in the Dependency Injection mechanisms of Spring and Jakarta EE.

Photo by Markus Spiske, https://unsplash.com/photos/ZGCnQJRxh4w

Dependency Injection is one of the core features in the heart of modern application development frameworks. In a nutshell, the process of dependency injection is composed of

  1. the definition of injectable beans
  2. the injection of beans (dependencies) into those places where they are required

This blog post compares the mechanisms available in the Spring Framework and Jakarta EE for the first part of this process, which can be further broken down into the following aspects:

  • Scopes: Which bean lifecycle scopes exist, and which one is used by default?
  • Proxying: Are the injectable beans provided with a proxy?
  • Class-Level Bean Definition: How can a whole class be defined as injectable bean?
  • Method-Level Bean Definition: How can a bean be defined via a single method?
  • Field-Level Bean Definition: How can a bean be defined via a single field?
  • Bean Scanning: What has to be done to enable scanning for available beans?

For the impatient, here is a summary table for these topics, with more details following below:

Topic Spring Jakarta EE
scopes singleton, prototype, application, request, session, websocket dependent, application, request, session, conversation, EJB scopes, singleton
default scope singleton dependent
proxying configurable available, but not configurable
class-level definition @Component, @Controller, @Service, @Repository @Dependent, @ApplicationScoped, @RequestScoped, @SessionScoped, @ConversationScoped, @Stereotype, EJB annotations
method-level definition @Bean @Produces
field-level definition no @Produces
annotation scanning @ComponentScan on by default
scanning for beans without annotations? no off by default, but configurable via beans.xml

Note that this blog post does not cover Spring’s XML-based configuration.

Blog content:

Spring: Scopes

The default scope in Spring is the singleton scope, which can be expressed explicitly with the Scope annotation:

@Scope("singleton")

To prevent typos, using the following constant from the interface ConfigurableBeanFactory is recommended:

@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)

With the prototype scope, a new bean instance is returned each time that this bean is requested at an injection point:

@Scope("prototype")
// or better:
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

Web applications have access to the following additional scopes:

@ApplicationScope

@RequestScope

@SessionScope

@Scope("websocket")

One of the main differences between @ApplicationScope and the singleton scope is that for a single web application, there can be

  • only one application-scoped instance of a bean
  • multiple singleton-scoped instances of a bean (since there can be multiple Spring ApplicationContexts for a single ServletContext).

Spring: Proxying

The shorthand annotations mentioned above for ApplicationScope, RequestScope and SessionScope also set the proxyMode to TARGET_CLASS. This means that Spring will not directly inject the bean itself, but a proxy that intercepts all invocations to this bean. This is required for scenarios where a bean with a short-lived scope is injected into another bean with a more long-lived scope. For instance, when injecting a request-scoped bean into an application-scoped bean, the proxy takes care of correctly retrieving the bean for the current request. Without this proxy, the application-scoped bean would be directly populated with a request-scoped bean during the first invocation. All subsequent invocations would (wrongly) still use exactly this “stale” request-scoped bean, even if in the context of a subsequent HTTP request. When using a singleton-scoped bean instead of an application-scoped bean in this proxy-less setup, Spring will even report an error during application startup: Singleton beans are constructed eagerly, but there is no active request context during the application’s initialization phase.

Spring: Bean Definitions

Classes marked with the following stereotype annotations are picked up for dependency injection (if it has been activated with @ComponentScan):

Moreover, injectable beans can be defined by annotating a method inside a @Configuration class with @Bean, and returning the desired bean from this method:

@Bean
MyBean mb() {
  return new MyBean();
}

This allows for custom or conditional initialization of beans for more sophisticated use cases. For instance, you could return a different bean depending on the value of a certain environment variable. Note that some of these use cases can also be addressed by using Spring profiles.

Spring: Bean Scanning

To activate the scanning process for classes with stereotype annotations, the @Configuration class has to be annotated with @ComponentScan, which provides optional attributes for fine-tuning (such as specifying the packages that shall be scanned).

In Spring Boot applications, @ComponentScan is implicitly provided by @SpringBootApplication and @SpringBootConfiguration.

CDI: Scopes

The default scope in CDI is the @Dependent scope, which means that the bean receives the same lifecycle as the bean that it is injected into.

CDI also provides the following annotations for further built-in scopes:

@ApplicationScoped

@RequestScoped

@SessionScoped

@ConversationScoped

Additional, non-CDI lifecycles are defined by the Jakarta Enterprise Beans (formerly known as EJB) specification, most notably

  • @Singleton: A singleton EJB’s lifecycle is similar to that of an application-scoped bean.
  • @Stateless: Stateless EJBs are kept in internal pool by the application server, which enables efficient re-use of EJBs. Moreover, the server guarantees that only one thread executes a stateless EJB at any time, meaning that they are relatively thread-safe (unless, for instance, they modify static variables).

The most important difference between Jakarta Enterprise Beans and CDI beans is that EJBs provide additional features such as transaction management and concurrency management.

And finally, there is also the @Singleton scope with the same name as the EJB scope mentioned above, but from another package (javax.inject.Singleton instead of javax.ejb.Singleton). In contrast to @ApplicationScoped and javax.ejb.Singleton, these beans are not injected with a proxy.

CDI: Proxying

In CDI, there is no configuration option for changing the proxy mode. Instead, proxies are used automatically for normal scopes (application, request, session, conversation), whereas there is no proxying for pseudo-scopes (dependent, javax.inject.Singleton). On the annotation level, normal scopes are marked with @NormalScope, and pseudo-scopes with @Scope. This means that @Dependent CDI beans exhibit similar behavior as Spring “prototype” beans with proxyMode set to NO (which is the default).

The lack of proxy-related configuration options is not a problem in practice, since CDI provides reasonable defaults for its annotations.

CDI: Bean Definitions

Classes marked with the following bean-defining annotations are picked up for dependency injection:

  • @Dependent
  • @NormalScope
    • @ApplicationScoped
    • @RequestScoped
    • @SessionScoped
    • @ConversationScoped
  • @Stereotype

With the @Stereotype annotation, you can define your own custom annotations:

@Stereotype
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomAnnotation {
  // ...
}

The default scope for these custom annotations is @Dependent, which can be adjusted by applying another scope annotation to the custom annotation’s definition.

Note that normal-scoped annotations are sufficient for a bean to become injectable in CDI, which is not the case in Spring (where you have to use annotations such as @Component).

Jakarta Enterprise Beans are also eligible for injection (most notably: @Stateless and @Singleton).

Moreover, injectable beans can be defined by annotating a method with @Produces, and returning the desired bean from this method:

@Produces
MyBean mb() {
  return new MyBean();
}

For simpler use cases, you can also annotate a field instead:

@Produces
MyBean mb = new MyBean();

Note that these producer methods and fields are only picked up if the containing class is marked with one of the bean-defining annotations mentioned above (unless you have changed the default bean-discovery-mode in beans.xml).

The default scope for beans produced by methods is @Dependent, whereas the scope of beans from producer fields defaults to that of the containing class. In both cases, this can be adjusted by accompanying the @Produces annotation with the desired scope annotation.

CDI: Bean Scanning

By default, CDI picks up annotated beans with the aforementioned bean-defining annotations. This behavior can be changed as to

  • pick up all beans
  • pick up no beans at all (none)

by providing the corresponding value to the attribute bean-discovery-mode in the beans.xml file, which is located either in /WEB-INF/, or in /WEB-INF/classes/META-INF/:

<beans 

  xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
  
  version="2.0"
  bean-discovery-mode="all">
  
</beans>

Providing an empty beans.xml file is equivalent to explicitly using the value all:

beans.xml present? bean-discovery-mode in beans.xml effective bean-discovery-mode
no - annotated
yes (empty file) - all
yes annotated annotated
yes all all
yes none none

References

The examples in this blog post were tested with