By 1996, developers had already made Java popular for its friendly APIs and automated Garbage Collection, and they started using it widely in back-end systems. One problem, however, was that most of these systems needed the same set of standard capabilities – such as persistence, transaction integrity, and concurrency control – which the JDK lacked at that time. That, naturally, led to many home-grown, closed implementations.
IBM stepped forward and released the Enterprise Java Bean (EJB) specification in 1997, with the promise that developers could write code in a standard way, with many of the common concerns automatically handled.
That’s how the first Java framework for the enterprise was born; the specification was later adopted by Sun in 1999 as EJB 1.0.
Fast forward twenty years and EJB 4.0 is now part of the Jakarta EE 9 specification. Jakarta EE represents a rebranding of Java EE under the Eclipse Foundation, continuing to evolve enterprise Java solutions for modern architectures.
Simply put, an Enterprise Java Bean is a Java class with one or more annotations from the EJB spec which grant the class special powers when running inside of an EJB container. In the following sections, we’ll discuss what these powers are and how to leverage them in your programs.
A side note – annotations in EJB are relatively new and are available since EJB 3.0. Previous versions of EJB used to have interfaces which classes had to implement. I’m not going to cover that in this article.
JNDI or Java Naming Directory Interface is a directory service which allows lookup of resources. Every resource, such as an EJB, Datasource, or JMS Queue, runs on an application server and gets a JNDI name. This JNDI name is then used to locate the resource.
All servers have a default scheme of assigning JNDI names but it can be overridden to provide custom names. The general convention is {resourceType}/{resourceName}.
For example, a DataSource’s JNDI name can be jdbc/TestDatabase and a JMS queue can have jms/TestQueue as JNDI name.
Jakarta EE Update: Jakarta EE servers like WildFly and Payara follow updated JNDI naming conventions to better align with modern cloud-native architectures.
For example, Payara might use names like java:/jms/MyQueue
, while WildFly may use java:/jboss/ea/JMS/MyQueue
Let’s now go a bit deeper into the specifics of Enterprise beans:
A session bean encapsulates business logic that can be invoked programmatically by a client. The invocation can be done locally by another class in the same JVM or remotely over the network from another JVM. The bean performs the task for the client, abstracting its complexity similar to a web service, for example.
The lifecycle of a session bean instance is, naturally, managed by the EJB container. Depending on how they’re managed, sessions beans can be in either of the following states:
As the name suggests, Stateless beans don’t have any state. As such, they are shared by multiple clients. They can be singletons but, in most implementations, containers create an instance pool of stateless EJB. And, since there is no state to maintain, they’re fast and easily managed by the container.
As a downside, owing to the shared nature of the bean, developers are responsible for ensuring that they are thread-safe.
Stateful beans are unique to each client, they represent a client’s state. Because the client interacts (“talks”) with its bean, this state is often called the conversational state. Just like stateless beans, instance lifecycle is managed by the container; they’re also destroyed when the client terminates.
A Singleton session bean is instantiated once per application and exists for the lifecycle of the application. Singleton session beans are designed for circumstances in which state must be shared across all clients. Similar to Stateless beans, developers must ensure that singletons thread safe. However, concurrency control is different between these different types of beans, as we’ll discuss further.
Now, let’s get practical and write some code. Here, we’re going to create a Maven project with a packaging type of ejb, with a dependency on javaee-api:
<project ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.stackify</groupId>
<artifactId>ejb-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ejb</packaging>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Alternatively, we could include the target server runtime dependency instead of the JavaEE APIs, but that does reduce portability between different containers.
Modern-day EJB is easy to configure, hence writing an EJB class is just a matter of adding annotations i.e. @Stateless, @Stateful or @Singleton. These annotations come from the jakarta.ejb package:
@Stateless
import jakarta.ejb.Stateless;
public class TestStatelessEjb {
public String sayHello(String name) {
return "Hello, " + name + "!";
}
}
Or:
@Stateful public class TestStatefulEjb { }
Finally:
@Singleton
public class TestSingletonEjb {
}
There’s also a javax.inject.Singleton annotation, but that’s a part of the CDI spec, so we need to be aware of that if we’re going to use it.
To address some of the complexities of state management in modern distributed systems, MicroProfile Context Propagation offers a solution that helps maintain context across service calls, improving state management for session beans. This is particularly useful in microservices architectures, where tracking client-specific or conversational states becomes crucial.
A message-driven bean or MDB is an enterprise bean that allows you to process messages asynchronously. This type of bean normally acts as a JMS message listener, which is similar to an event listener but receives JMS messages instead of events.
They resemble a Stateless session bean in many ways, but clients do not invoke them. Instead, events drive their invocation.
@MessageDriven(mappedName = "jms/TestQueue")
public class TestMessageDrivenBean implements MessageListener {
@Resource
MessageDrivenContext messageDrivenContext;
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
TextMessage msg = (TextMessage) message;
msg.getText();
}
} catch (JMSException e) {
messageDrivenContext.setRollbackOnly();
}
}
}
Here, the mapped name is the JNDI name of the JMS queue that this MDB is listening to. When a message arrives, the container calls the message-driven bean’s onMessage method to process the message. The onMessage method normally casts the message to one of the five JMS message types and handles it in accordance with the application’s business logic. The onMessage method can call helper methods or can invoke a session bean to process the information in the message.
A message can be delivered to a message-driven bean within a transaction context, so all operations within the onMessage method are part of a single transaction. If message processing is rolled back, the message will be delivered.
Modern Integrations: In addition to traditional JMS, developers can integrate MDBs with modern messaging systems like Apache Kafka or RabbitMQ. These systems offer more robust support for event-driven architectures, providing scalable, high-throughput messaging. Developers can achieve integration via additional frameworks or libraries that connect MDBs to these message brokers, enabling seamless asynchronous processing across microservices.
As discussed before, MDBs are event-driven, so in this section, we’ll talk about how to access and invoke methods of session beans.
To invoke the methods of an EJB locally, the bean can be injected in any managed class running in the container – say a Servlet:
public class TestServlet extends HttpServlet {
@EJB
TestStatelessEjb testStatelessEjb;
public void doGet(HttpServletRequest request,
HttpServletResponse response) {
testStatelessEjb.sayHello("Stackify Reader");
}
}
Invoking the method from a remote JVM is trickier and requires a bit more code. As a prerequisite, EJB must implement a remote interface to enable remoting capabilities. You will need to write an EJB client which will perform a lookup over the network.
The interface is annotated with @Remote:
@Remote
public interface TestStatelessEjbRemote {
String sayHello(String name);
}
Make sure that the TestStatelessEjb implements this interface.
Now let’s write the client which in this case would just be a simple Java SE application with the main method:
public class TestEjbClient {
public static void main(String[] args) throws NamingException {
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY,
"org.apache.openejb.client.LocalInitialContextFactory");
properties.setProperty(Context.PROVIDER_URL, "ejbd://host:4201");
Context context = new InitialContext(properties);
TestStatelessEjbRemote testStatelessEjbRemote
= (TestStatelessEjbRemote) context.lookup("ejb/TestStatelessEjbRemote");
testStatelessEjbRemote.sayHello("Stackify");
}
}
First, we created a Context with properties referring to the remote JVM. The initial context factory name and the provider URL used here are defaults for Open EJB and will vary from server to server.
Then we performed a lookup of the EJB by using the JNDI name of the bean and then typecast it to the desired remote type. Once we get the remote EJB instance, we were able to invoke the method.
Note that you’ll need two jar files in the classpath of your client:
As it happens, the Maven EJB plugin will generate a client jar file which will only have all the remote interfaces. You just need to configure the plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-ejb-plugin</artifactId> <version>3.0.0</version> <configuration> <!-- this is false by default --> <generateClient>true</generateClient> </configuration> </plugin>
In the case of Stateful beans, the container returns a new instance of the bean each time a client performs a lookup. In the case of Stateless beans, the container returns one bean from the pool.
Both Stateless and Stateful enterprise beans allow multiple clients or multiple threads from the same client to concurrently invoke methods. However, in the case of Singleton enterprise beans, the default mode is LockType.WRITE. This means that only one thread is allowed to invoke the method at once.
You can change this by adding the @Lock annotation over a method and setting to LockType.READ:
@Singleton
public class TestSingletonEjb {
@Lock(LockType.READ)
public String sayHello(String name) {
return "Hello, " + name + "!";
}
}
This fine-grained concurrency management over method level allows developers to build robust multi-threaded applications without having to deal with actual threads.
Say we have a Map instance variable in a Singleton EJB. Most clients read from the Map but a few do put elements into it. Marking the get method as lock type read and put method as lock type write would make up for a perfect implementation:
@Singleton
public class TestSingletonEjb {
private Map<String, String> elements;
public TestSingletonEjb() {
this.elements = new HashMap<>();
}
@Lock(LockType.READ)
public String getElement(String key) {
return elements.get(key);
}
@Lock(LockType.WRITE)
public void addElement(String key, String value) {
elements.put(key, value);
}
}
A write-lock locks the entire class, so the method addElement
updates the map, blocking all threads that try to access getElement
.
Improved Thread Safety with Jakarta Concurrency Utilities: To better manage thread safety and concurrency control in Singleton beans, we can leverage Jakarta Concurrency Utilities (part of Jakarta EE). These utilities provide advanced tools like @ManagedExecutorService
and @Asynchronous
. Thereby, enabling developers to offload tasks to managed threads more efficiently and manage thread execution in a more scalable way.
By utilizing these utilities, applications can handle concurrency more effectively, especially when scaling to meet the needs of modern microservices architectures.
EJB can play a valuable role in modern microservices architecture by leveraging Jakarta EE technologies. For example. Jakarta RESTful Web services (JAX-RS). We can use EJBs like Singleton or Stateless. These can be exposed as RESTful endpoints. Thus, allowing them to seamlessly integrate with other microservices.
By combining concurrency management with JAX-RS and EJB’s transactions, we can implement scalable, robust, and easy-to-maintain services.
For instance, we can invoke a Stateless EJB via a REST endpoint:
@Path("/hello")
@Stateless
public class HelloService {
@GET
public String sayHello() {
return "Hello, Microservices!";
}
}
This approach enables microservices to use the powerful EJB model while embracing the flexibility and scalability of modern distributed systems.
In Jakarta EE, the @Schedule
annotation allows easy scheduling of tasks within Enterprise JavaBeans (EJB). This annotation provides a way to define when we should execute a method, for example, at specific hours or minutes. Let’s discuss a typical example of scheduling a job at 11:55 PM in a Singleton bean:
The Singleton annotation ensures that only one instance of the bean exists, preventing multiple invocations of the scheduled job from different bean instances.
@Singleton
public class TestScheduleBean {
@Schedule(hour = "12", minute = "0", timezone = "UTC")
void executeTask() {
System.out.println("Task executed at noon UTC.");
}
}
Note here that the EJB is a Singelton. The Singleton annotation ensures that only one instance of the bean exists, preventing multiple invocations of the scheduled job from different bean instances.
Although Spring has gained a lot of traction in the enterprise development world, EJB is still very relevant and quite powerful. Out-of-the-box remoting capabilities and concurrency management are still exclusive to Enterprise Beans; JMS and JPA are a part of the JavaEE spec as well and hence treated as first-class citizens in EJB.
EJB has certainly evolved beyond its previous limitations and has re-invented itself into a modern and powerful tool in the rich Java ecosystem.
With APM, server health metrics, and error log integration, improve your application performance with Stackify Retrace. Try your free two week trial today
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]