Java EE Observer Design Pattern Example
This article is about a Java EE Observer Design Pattern Example. The observer pattern is one of the most widely used design patterns in programming. It is implemented in the java.util
package of Java SE 8 as Observer
and Observable
.
1. Introduction
By extending these classes, we can easily implement the observer pattern. But this article is not about that. We will focus on the Java EE implementation of the observer pattern.
In the observer pattern, an object that changes its state can inform other objects that a change in state has happened. The object that changes its state is the subject. The objects that receive the notification of the change of state are the observers. The observer pattern does this in a decoupled manner so that the subject does not know about the observers.
2. Tools and Requirements
The source in this is example is based on the Java EE Kickoff App. We will not go through the details of setting up the project, so it is recommended that Eclipse with WildFly and JBoss Tools Example be read before trying out the example. We’ll be using WildFly 14.x because it is a Java EE 8 full platform compatible implementation.
3. Java EE Observer Design Pattern Implementaiton
In Java EE, the subject and observer are decoupled. Many observers can receive notification about a change in the state of the subject. It relies on the Observes
annotation. This annotation marks the observers and the subject uses the Event
class to create and fire events that observers listen for.
4. @Observes and Event
After downloading WildFly 14.x, we add it as one of our servers in Eclipse. We should have something like below:
Import the java-ee-observer-pattern project in Eclipse. We will add some code in the AuthBacking
class. AuthBacking.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package org.example.kickoff.view.auth; ...snipped... public abstract class AuthBacking { ...snipped... @Inject private Event<User> userLoginEvent; ...snipped... protected void authenticate(AuthenticationParameters parameters) throws IOException { AuthenticationStatus status = securityContext.authenticate(getRequest(), getResponse(), parameters); if (status == SEND_FAILURE) { addGlobalError( "auth.message.error.failure" ); validationFailed(); } else if (status == SEND_CONTINUE) { responseComplete(); // Prevent JSF from rendering a response so authentication mechanism can continue. } else if (activeUser.hasGroup(ADMIN)) { userLoginEvent.fire(user); redirect( "admin/users" ); } else if (activeUser.hasGroup(USER)) { userLoginEvent.fire(user); redirect( "user/profile" ); } else { redirect( "" ); } } ...snipped... } |
Parts of the code have been snipped. Download the project to get the full contents of the code. We will only be discussing what is relevant to the observer pattern. We will not discuss the other parts of the Java EE Kickoff App.
An Event
instance of type User
is created by the container and is injected into the subject class. When the fire method is invoked, a notification is fired and any observer listening for User
events will receive the user instance. Any method parameter annotated with @Observes
and of type User
will receive this instance of the user object. This is the observer pattern in its simplest form.
Create the observer classes.
LoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.Observes; import org.example.kickoff.model.User; public class LoginService { public void saveLoginAttempt( @Observes User user) { System.out.println( "Logged in: " + user.getEmail()); } } |
EmailLoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.Observes; import org.example.kickoff.model.User; public class EmailLoginService { public void sendLoginAttemptEmail( @Observes User user) { System.out.println(user.getEmail() + " has been used to log into Java EE Observer Design Pattern Example." ); } } |
The above methods with parameters annotated with @Observes
will receive a notification when a user logs in. When we log in, we should have the statements above printed.
Try logging as an admin and then as a user. We should see the output below.
Console Output
1 2 3 4 5 6 | 11:18:31,908 INFO [org.omnifaces.eventlistener.FacesRequestLogger] (default task-1) GET={url= /login , user={ip=127.0.0.1, login=null, session=zMpJHJSEodlYr0tY8-giDAciJIDcMdEStJT_6lcQ, viewState=null}, action={ source =null, event=null, methods=[], validationFailed= false }, params={}, messages={}, timer={0=220ms, 1=3ms, 2=-1ms, 3=-1ms, 4=-1ms, 5=-1ms, 6=217ms}} 11:18:49,943 INFO [stdout] (default task-1) Logged in : admin@kickoff.example.org 11:18:49,943 INFO [stdout] (default task-1) admin@kickoff.example.org has been used to log into Java EE Observer Design Pattern Example. 11:18:49,946 INFO [org.omnifaces.eventlistener.FacesRequestLogger] (default task-1) POST={url= /login , user={ip=127.0.0.1, login=admin@kickoff.example.org, session=pnrQwJj3ao-mJoPd3RmEc_I-ompITHeimrs8XvDw, viewState=stateless}, action={ source =loginForm:login, |
Those were the basics related to the J2EE observer pattern.
5. @Priority
The order in which observers are invoked is not specified. We can use the Priority
annotation to specify the invocation order of the observers. This is a new feature in Java EE 8. The lowest value priority is called first. If the priorities are the same then they are invoked in an uncertain order.
LoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.Observes; import org.example.kickoff.model.User; public class LoginService { public void saveLoginAttempt( @Observes @Priority ( 10 ) User user) { System.out.println( "Logged in: " + user.getEmail()); } } |
EmailLoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.Observes; import org.example.kickoff.model.User; public class EmailLoginService { public void sendLoginAttemptEmail( @Observes @Priority ( 100 ) User user) { System.out.println(user.getEmail() + " has been used to log into Java EE Observer Design Pattern Example." ); } } |
With the above priority configuration, the “Logging in…” is printed first followed by “Java EE Observer Design Pattern…”. Try swapping the number so that “Java EE Observer Design Pattern…” is printed first.
6. @Qualifier
So far our observers are listening for User
type events. What if we want to distinguish between different types of users? The admin and the user type? At the moment both methods get invoked when a user logs in. What if we only want to save a login attempt if it’s of type user? What if we only want to send an email if the login attempt is of type admin? In order for the observers to distinguish between events, we will use a Qualifier
.
UserEvent.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | package org.example.kickoff.business.service; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention (RetentionPolicy.RUNTIME) @Target ({ElementType.FIELD, ElementType.PARAMETER}) public @interface UserEvent { Type value(); enum Type {ADMIN, USER} } |
We will then use the UserEvent
annotation to distinguish which observer to invoke like so.
LoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.Observes; import org.example.kickoff.model.User; public class LoginService { public void saveLoginAttempt( @Observes @Priority ( 10 ) @UserEvent (UserEvent.Type.USER) User user) { System.out.println( "Logged in: " + user.getEmail()); } } |
EmailLoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.Observes; import org.example.kickoff.model.User; public class EmailLoginService { public void sendLoginAttemptEmail( @Observes @Priority ( 100 ) @UserEvent (UserEvent.Type.ADMIN) User user) { System.out.println(user.getEmail() + " has been used to log into Java EE Observer Design Pattern Example." ); } } |
And then edit AuthBacking
, adding a new event like so.
AuthBacking.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package org.example.kickoff.view.auth; ...snipped... import org.example.kickoff.business.service.UserEvent; ...snipped... public abstract class AuthBacking { ...snipped... @Inject @UserEvent (UserEvent.Type.USER) private Event<User> userLoginEvent; @Inject @UserEvent (UserEvent.Type.ADMIN) private Event<User> adminLoginEvent; ...snipped... protected void authenticate(AuthenticationParameters parameters) throws IOException { AuthenticationStatus status = securityContext.authenticate(getRequest(), getResponse(), parameters); if (status == SEND_FAILURE) { addGlobalError( "auth.message.error.failure" ); validationFailed(); } else if (status == SEND_CONTINUE) { responseComplete(); // Prevent JSF from rendering a response so authentication mechanism can continue. } else if (activeUser.hasGroup(ADMIN)) { adminLoginEvent.fire(user); redirect( "admin/users" ); } else if (activeUser.hasGroup(USER)) { userLoginEvent.fire(user); redirect( "user/profile" ); } else { redirect( "" ); } } ...snipped... } |
With the above code, whenever an admin logs in, an email is sent. Whenever a user logs in, it is saved. Our observers can now distinguish which subject has changed its states.
7. Asynchronous Observer
By default events are synchronous. In CDI 2.0, a new firing method called fireAsync
and a corresponding observer annotation ObservesAsync
handles asynchronous processing of events. We cannot set a priority because events are observed asynchronously in separate threads. Asynchronous and synchronous observers operate independently from each other. This means the synchronous firing of events are not observed by asynchronous observers and vice versa. We’ll need to change LoginService, EmailLoginService,
and AuthBacking
like so.
LoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.ObservesAsync; import org.example.kickoff.model.User; public class LoginService { public void saveLoginAttempt( @ObservesAsync @UserEvent (UserEvent.Type.USER) User user) { System.out.println( "Logged in: " + user.getEmail()); } } |
EmailLoginService.java
01 02 03 04 05 06 07 08 09 10 | package org.example.kickoff.business.service; import javax.enterprise.event.ObservesAsync; import org.example.kickoff.model.User; public class EmailLoginService { public void sendLoginAttemptEmail( @ObservesAsync @UserEvent (UserEvent.Type.ADMIN) User user) { System.out.println(user.getEmail() + " has been used to log into Java EE Observer Design Pattern Example." ); } } |
AuthBacking.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package org.example.kickoff.view.auth; ...snipped... import java.util.concurrent.CompletionStage; import org.example.kickoff.business.service.UserEvent; ...snipped... public abstract class AuthBacking { ...snipped... @Inject @UserEvent (UserEvent.Type.USER) private Event<User> userLoginEvent; @Inject @UserEvent (UserEvent.Type.ADMIN) private Event<User> adminLoginEvent; ...snipped... protected void authenticate(AuthenticationParameters parameters) throws IOException { AuthenticationStatus status = securityContext.authenticate(getRequest(), getResponse(), parameters); if (status == SEND_FAILURE) { addGlobalError( "auth.message.error.failure" ); validationFailed(); } else if (status == SEND_CONTINUE) { responseComplete(); // Prevent JSF from rendering a response so authentication mechanism can continue. } else if (activeUser.hasGroup(ADMIN)) { CompletionStage stage = adminLoginEvent.fireAsync(user); stage.handle((User event, Throwable e) -> { for (Throwable t : e.getSuppressed()) { System.out.println(t.getMessage()); } return event; }); redirect( "admin/users" ); } else if (activeUser.hasGroup(USER)) { userLoginEvent.fireAsync(user, NotificationOptions.ofExecutor( new ForkJoinPool( 10 ))); redirect( "user/profile" ); } else { redirect( "" ); } } ...snipped... } |
We have changed the method fire
to fireAsync
. We have added notification options to our firing event and specified a thread pool. Our ForkJoinPool
allows 10 threads. This means if there are 10 or fewer observers, they will get executed asynchronously. If there are more, the other observers must wait until a thread becomes available.
The fireAsync
the method returns an instance of CompletionStage
. This instance holds a reference to all the exceptions thrown during observer invocation and can be handled in the same way that you would handle a completion state instance.
8. Java EE Observer Design Pattern Summary
That’s all there is to it. We started with a simple implementation of the observer pattern then moved on to use many of the Java EE features. Reaching to the advanced topic of asynchronous observers. Let us now look at the pros and cons of the J2EE Observer Design pattern.
8.1 Pros and cons of the Observer Design Pattern
In Java, the Observer pattern was introduced in Java 1.0. J2EE Observer design helps in development by using annotations and conventions over configuration. Resources also can be injected by type and using annotations @Inject
and @Producer
. This design pattern helps in startup behavioral characteristics. The developer has control over concurrency and access timeout. This design results in lesser boilerplate code. Any java object can be injected easily. Loosely coupled design and dependency injection can be achieved easily. Business logic is decoupled from the observer through events.
The disadvantage of this pattern is performance issues related to lazy loading. Eager loading creates memory issues. The overuse of this design pattern results in problems. An observer design pattern is suggested to be used in special cases. Type safety is an issue with named annotations. The creation of objects is not transparent. The execution flow is not visible to the developer due to events triggered by other events.
9. Download the Source Code
This is an example about Java EE Observer Design Pattern.
You can download the source code of this example here: Java EE Observer Design Pattern Example
Last updated on Jun. 22nd, 2020
Observer and Observable are actually deprecated in Java 9. Check out: https://stackoverflow.com/questions/46380073/observer-is-deprecated-in-java-9-what-should-we-use-instead-of-it