1. Overview

Over the years, the Java ecosystem has evolved and grown tremendously. During this time, Enterprise Java Beans and Spring are two technologies that not only have competed but learned from each other symbiotically.

In this tutorial, we’ll take a look at their history and differences. Of course, we’ll see some code examples of EJB and their equivalents in the Spring world.

2. A Brief History of the Technologies

To start with, let’s take a quick peek at the history of these two technologies and how they’ve steadily developed over the years.

2.1. Enterprise Java Beans

The EJB specification is a subset of the Java EE (or J2EE, now known as Jakarta EE) specification. Its first version came out in 1999, and it was one of the first technologies designed to make server-side enterprise application development easier in Java.

It shouldered the Java developers’ burden of concurrency, security, persistence, transaction processing, and more. The specification handed these and other common enterprise concerns over to the implementing application servers’ containers, which handled them seamlessly. However, using EJBs as they were was a bit cumbersome due to the amount of configuration required. Moreover, it was proving to be a performance bottleneck.

But now, with the invention of annotations, and stiff competition from Spring, EJBs in their latest 3.2 version are much simpler to use than their debut version. The Enterprise Java Beans of today borrow heavily from Spring’s dependency injection and use of POJOs.

2.2. Spring

While EJBs (and Java EE in general) were struggling to satisfy the Java community, Spring Framework arrived like a breath of fresh air. Its first milestone release came out in the year 2004 and offered an alternative to the EJB model and its heavyweight containers.

Thanks to Spring, Java enterprise applications could now be run on lighter-weight IOC containers. Moreover, it also offered dependency inversion, AOP, and Hibernate support among myriad other useful features. With tremendous support from the Java community, Spring has now grown exponentially and can be termed as a full Java/JEE application framework.

In its latest avatar, Spring 5.0 even supports the reactive programming model. Another offshoot, Spring Boot, is a complete game-changer with it’s embedded servers and automatic configurations.

3. Prelude to the Feature Comparison

Before jumping to the feature comparison with code samples, let’s establish a few basics.

3.1. Basic Difference Between the Two

First, the fundamental and apparent difference is that EJB is a specification, whereas Spring is an entire framework.

The specification is implemented by many application servers such as GlassFish, IBM WebSphere and JBoss/WildFly. This means that our choice to use the EJB model for our application’s backend development is not enough. We also need to choose which application server to use.

Theoretically, Enterprise Java Beans are portable across app servers, though there is always the prerequisite that we shouldn’t use any vendor-specific extensions if interoperability is to be kept as an option.

Second, Spring as technology is closer to Java EE than EJB in terms of its broad portfolio of offerings. While EJBs only specify backend operations, Spring, like Java EE, also has support for UI development, RESTful APIs, and Reactive programming to name a few.

3.2. Useful Information

In the sections that follow, we’ll see the comparison of the two technologies with some practical examples. Since EJB features are a subset of the much larger Spring ecosystem, we’ll go by their types and see their corresponding Spring equivalents.

To best understand the examples, consider reading up on Java EE Session Beans, Message Driven Beans, Spring Bean, and Spring Bean Annotations first.

We’ll be using OpenJB as our embedded container to run the EJB samples. For running most of the Spring examples, its IOC container will suffice; for Spring JMS, we’ll need an embedded ApacheMQ broker.

To test all our samples, we’ll use JUnit.

4. Singleton EJB == Spring Component

Sometimes we need the container to create only a single instance of a bean. For example, let’s say we need a bean to count the number of visitors to our web application. This bean needs to be created only once during application startup.

Let’s see how to achieve this using a Singleton Session EJB and a Spring Component.

4.1. Singleton EJB Example

We’ll first need an interface to specify that our EJB has the capability to be handled remotely:

@Remote
public interface CounterEJBRemote {    
    int count();
    String getName();
    void setName(String name);
}

The next step is to define an implementation class with the annotation javax.ejb.Singleton, and viola! Our singleton is ready:

@Singleton
public class CounterEJB implements CounterEJBRemote {
    private int count = 1;
    private String name;

    public int count() {
        return count++;
    }
    
    // getter and setter for name
}

But before we can test the singleton (or any other EJB code sample), we need to initialize the ejbContainer and get the context:

@BeforeClass
public void initializeContext() throws NamingException {
    ejbContainer = EJBContainer.createEJBContainer();
    context = ejbContainer.getContext();
    context.bind("inject", this);
}

Now let’s look at the test:

@Test
public void givenSingletonBean_whenCounterInvoked_thenCountIsIncremented() throws NamingException {

    int count = 0;
    CounterEJBRemote firstCounter = (CounterEJBRemote) context.lookup("java:global/ejb-beans/CounterEJB");
    firstCounter.setName("first");
        
    for (int i = 0; i < 10; i++) {
        count = firstCounter.count();
    }
        
    assertEquals(10, count);
    assertEquals("first", firstCounter.getName());

    CounterEJBRemote secondCounter = (CounterEJBRemote) context.lookup("java:global/ejb-beans/CounterEJB");

    int count2 = 0;
    for (int i = 0; i < 10; i++) {
        count2 = secondCounter.count();
    }

    assertEquals(20, count2);
    assertEquals("first", secondCounter.getName());
}

A few things to note in the above example:

  • We are using the JNDI lookup to get counterEJB from the container
  • count2 picks up from the point count left the singleton at, and adds up to 20
  • secondCounter retains the name we set for firstCounter

The last two points demonstrate the significance of a singleton. Since the same bean instance is used each time it’s looked up, the total count is 20 and the value set for one remains the same for the other.

4.2. Singleton Spring Bean Example

The same functionality can be obtained using Spring components.

We don’t need to implement any interface here. Instead, we’ll add the @Component annotation:

@Component
public class CounterBean {
    // same content as in the EJB
}

In fact, components are singletons by default in Spring.

We also need to configure Spring to scan for components:

@Configuration
@ComponentScan(basePackages = "com.baeldung.ejbspringcomparison.spring")
public class ApplicationConfig {}

Similar to how we initialized the EJB context, we’ll now set the Spring context:

@BeforeClass
public static void init() {
    context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
}

Now let’s see our Component in action:

@Test
public void whenCounterInvoked_thenCountIsIncremented() throws NamingException {    
    CounterBean firstCounter = context.getBean(CounterBean.class);
    firstCounter.setName("first");
    int count = 0;
    for (int i = 0; i < 10; i++) {
        count = firstCounter.count();
    }

    assertEquals(10, count);
    assertEquals("first", firstCounter.getName());

    CounterBean secondCounter = context.getBean(CounterBean.class);
    int count2 = 0;
    for (int i = 0; i < 10; i++) {
        count2 = secondCounter.count();
    }

    assertEquals(20, count2);
    assertEquals("first", secondCounter.getName());
}

As we can see, the only difference with respect to EJBs is how we are getting the bean from the Spring container’s context, instead of JNDI lookup.

5. Stateful EJB == Spring Component with prototype Scope

At times, say when we are building a shopping cart, we need our bean to remember its state while going back and forth between method calls.

In this case, we need our container to generate a separate bean for each invocation and save the state. Let’s see how this can be achieved with our technologies in question.

5.1. Stateful EJB Example

Similar to our singleton EJB sample, we need a javax.ejb.Remote interface and its implementation. Only this time, its annotated with javax.ejb.Stateful:

@Stateful
public class ShoppingCartEJB implements ShoppingCartEJBRemote {
    private String name;
    private List<String> shoppingCart;

    public void addItem(String item) {
        shoppingCart.add(item);
    }
    // constructor, getters and setters
}

Let’s write a simple test to set a name and add items to a bathingCart. We’ll check its size and verify the name:

@Test
public void givenStatefulBean_whenBathingCartWithThreeItemsAdded_thenItemsSizeIsThree()
  throws NamingException {
    ShoppingCartEJBRemote bathingCart = (ShoppingCartEJBRemote) context.lookup(
      "java:global/ejb-beans/ShoppingCartEJB");

    bathingCart.setName("bathingCart");
    bathingCart.addItem("soap");
    bathingCart.addItem("shampoo");
    bathingCart.addItem("oil");

    assertEquals(3, bathingCart.getItems().size());
    assertEquals("bathingCart", bathingCart.getName());
}

Now, to demonstrate that the bean really maintains state across instances, let’s add another shoppingCartEJB to this test:

ShoppingCartEJBRemote fruitCart = 
  (ShoppingCartEJBRemote) context.lookup("java:global/ejb-beans/ShoppingCartEJB");

fruitCart.addItem("apples");
fruitCart.addItem("oranges");

assertEquals(2, fruitCart.getItems().size());
assertNull(fruitCart.getName());

Here we did not set the name and hence its value was null. Recall from the singleton test, that the name set in one instance was retained in another. This demonstrates that we got separate ShoppingCartEJB instances from the bean pool with different instance states.

5.2. Stateful Spring Bean Example

To get the same effect with Spring, we need a Component with a prototype scope:

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCartBean {
   // same contents as in the EJB
}

That’s it, just the annotations differ – the rest of the code remains the same.

To test our Stateful bean, we can use the same test as described for EJBs. The only difference is again how we get the bean from the container:

ShoppingCartBean bathingCart = context.getBean(ShoppingCartBean.class);

6. Stateless EJB != Anything in Spring

Sometimes, for example in a search API, we neither care about the instance state of a bean nor if it is a singleton. We just need the results of our search, which might be coming from any bean instance for all we care about.

6.1. Stateless EJB Example

For such scenarios, EJB has a stateless variant. The container maintains an instance pool of beans, and any of them is returned to the calling method.

The way we define it is the same as other EJB types, with a remote interface, and implementation with javax.ejb.Stateless annotation:

@Stateless
public class FinderEJB implements FinderEJBRemote {

    private Map<String, String> alphabet;

    public FinderEJB() {
        alphabet = new HashMap<String, String>();
        alphabet.put("A", "Apple");
        // add more values in map here
    }

    public String search(String keyword) {
        return alphabet.get(keyword);
    }
}

Let’s add another simple test to see this in action:

@Test
public void givenStatelessBean_whenSearchForA_thenApple() throws NamingException {
    assertEquals("Apple", alphabetFinder.search("A"));        
}

In the above example, alphabetFinder is injected as a field in the test class using the annotation javax.ejb.EJB:

@EJB
private FinderEJBRemote alphabetFinder;

The central idea behind Stateless EJBs is to enhance performance by having an instance pool of similar beans.

However, Spring does not subscribe to this philosophy and only offers singletons as stateless.

7. Message Driven Beans == Spring JMS

All EJBs discussed so far were session beans. Another kind is the message-driven one. As the name suggests, they are typically used for asynchronous communication between two systems.

7.1. MDB Example

To create a message-driven Enterprise Java Bean, we need to implement the javax.jms.MessageListener interface defining its onMessage method, and annotate the class as javax.ejb.MessageDriven:

@MessageDriven(activationConfig = { 
  @ActivationConfigProperty(propertyName = "destination", propertyValue = "myQueue"), 
  @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") 
})
public class RecieverMDB implements MessageListener {

    @Resource
    private ConnectionFactory connectionFactory;

    @Resource(name = "ackQueue")
    private Queue ackQueue;

    public void onMessage(Message message) {
        try {
            TextMessage textMessage = (TextMessage) message;
            String producerPing = textMessage.getText();

            if (producerPing.equals("marco")) {
                acknowledge("polo");
            }
        } catch (JMSException e) {
            throw new IllegalStateException(e);
        }
    }
}

Notice that we are also providing a couple of configurations for our MDB:

      • destinationType as Queue
        • myQueue as the destination queue name, to which our bean is listening

In this example, our receiver also produces an acknowledgment, and in that sense is a sender in itself. It sends a message to another queue called ackQueue.

Now let’s see this in action with a test:

@Test
public void givenMDB_whenMessageSent_thenAcknowledgementReceived()
  throws InterruptedException, JMSException, NamingException {
    Connection connection = connectionFactory.createConnection();
    connection.start();
    Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
    MessageProducer producer = session.createProducer(myQueue);
    producer.send(session.createTextMessage("marco"));
    MessageConsumer response = session.createConsumer(ackQueue);

    assertEquals("polo", ((TextMessage) response.receive(1000)).getText());
}

Here we sent a message to myQueue, which was received by our @MessageDriven annotated POJO. This POJO then sent an acknowledgment and our test received the response as a MessageConsumer.

7.2. Spring JMS Example

Well, now it’s time to do the same thing using Spring!

First, we’ll need to add a bit of configuration for this purpose. We need to annotate our ApplicationConfig class from before with @EnableJms and add a few beans to setup JmsListenerContainerFactory and JmsTemplate:

@EnableJms
public class ApplicationConfig {

    @Bean
    public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        return factory;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        return new ActiveMQConnectionFactory("tcp://localhost:61616");
    }

    @Bean
    public JmsTemplate jmsTemplate() {
        JmsTemplate template = new JmsTemplate(connectionFactory());
        template.setConnectionFactory(connectionFactory());
        return template;
    }
}

Next, we need a Producer – a simple Spring Component – that will send messages to myQueue and receive an acknowledgment from ackQueue:

@Component
public class Producer {
    @Autowired
    private JmsTemplate jmsTemplate;

    public void sendMessageToDefaultDestination(final String message) {
        jmsTemplate.convertAndSend("myQueue", message);
    }

    public String receiveAck() {
        return (String) jmsTemplate.receiveAndConvert("ackQueue");
    }
}

Then, we have a Receiver Component with a method annotated as @JmsListener to receive messages asynchronously from myQueue:

@Component
public class Receiver {
    @Autowired
    private JmsTemplate jmsTemplate;

    @JmsListener(destination = "myQueue")
    public void receiveMessage(String msg) {
        sendAck();
    }

    private void sendAck() {
        jmsTemplate.convertAndSend("ackQueue", "polo");
    }
}

It also acts as a sender for acknowledging message receipt at ackQueue.

As is our practice, let’s verify this with a test:

@Test
public void givenJMSBean_whenMessageSent_thenAcknowledgementReceived() throws NamingException {
    Producer producer = context.getBean(Producer.class);
    producer.sendMessageToDefaultDestination("marco");

    assertEquals("polo", producer.receiveAck());
}

In this test, we sent marco to myQueue and received polo as an acknowledgment from ackQueue, the same as what we did with the EJB.

One thing of note here is that Spring JMS can send/receive messages both synchronously and asynchronously.

8. Conclusion

In this tutorial, we saw a one-on-one comparison of Spring and Enterprise Java Beans. We understood their history and basic differences.

Then we dealt with simple examples to demonstrate the comparison of Spring Beans and EJBs. Needless to say, it is merely scratching the surface of what the technologies are capable of, and there is a lot more to be explored further.

Furthermore, these might be competing technologies, but that doesn’t mean they can’t co-exist. We can easily integrate EJBs in the Spring framework.

As always, source code is available over on GitHub.