Java Observer Pattern With Spring AOP and a Custom Annotation
How to use a specific implementation of the Java Observer Pattern to inject new functionalities into an existing system based on Spring.
Join the DZone community and get the full member experience.
Join For FreeThe Java Observer Pattern is among the most interesting patterns. It allows the injection of some functionality into a system, or part of a system, with minimum impact on the existing code. In this article, we present a specific solution that implements this pattern, using features like Spring Aspect Oriented Programming (AOP), Spring BeanPostProcessor, and custom annotations.
This solution explains how to extend a system by catching its main events and attaching the wanted functionality. We already explained a strategy to extend an existing system in this article: Decorator Pattern to Solve Integration Scenarios in Existing Systems. There we talked about the Java Decorator Pattern. The main difference compared to that design is that here we do not replace an object with a decorated version, but we catch events with specific payloads and trigger new features on those events' occurrences.
An example of the proposed solution has been implemented by the author as a Spring Boot project available at the following GitHub address: Java Observer Pattern with AOP and custom annotations.
A Very Quick Overview of the Java Observer Pattern
The Java Observer Pattern is a very well-known solution in the Java world. Its main goal is to implement some form of object interaction. In particular, it designs the interaction such that an object can notify other dependent objects about its state changes. The following diagram shows how it works.
A UML class diagram depicts the Java Observer pattern in its simplest form.
A class that implements the Observable interface allows to register observers, that is to say, classes that implement an Observer interface with a method that the Observable can call. The Observable notifies the observers whenever its state changes, passing a parameter representing the state change event. In a less basic form, the responsibility of registering and notifying observers could be delegated to a separate class (not shown in the picture).
A Specific Implementation Inspired by the Java Observer Pattern
A possible scenario in which the Java Observer Pattern may show its full potential could be a system with some form of workflow with specific well,-defined phases. We could imagine, for instance, an old system based on Spring that we want to extend with new functionalities leaving the existing code intact.
The solution proposed in this article allows the injection of new functionalities without even touching the existing code. It does that by taking advantage of some features of the Spring framework, such as Spring AOP, Spring BeanPostProcessor, and custom annotations. For simplicity, we will represent the target system as a single class with a method that we suppose to implement some important phase change in the software lifecycle.
What We Want to Achieve
Before describing the whole thing, let's start with the final result we want to achieve:
- We want to 'transform' a regular Spring bean into an observer of a target system using a simple custom annotation and an interface that the bean must implement. We want our observer to trigger some new functionality when a particular system state is reached.
- The annotation must have one or more parameters that represent some identification information of the system state (in our solution, we will define a single parameter for simplicity).
- The interface must define a method that is meant to be executed in the context of a state change, with a parameter representing the occurred event containing a payload with some information about the current system state, with details depending on the specific implementation.
- We want our observers to be registered when our application starts.
General Schema of Our Solution
Our Observer pattern inspires our solution aimed in the previous section with some slight differences. The following class diagram shows the general idea.
A Class diagram that shows a solution inspired by the Java Observer Pattern.
In the diagram above, two central classes are shown:
1. ObserverPostProcessor: it's a Spring bean that implements the String BeanPostProcessor interface. Spring will call the methods of BeanPostProcessor during startup and let us handle the beans being loaded into the Spring context. In our case, the responsibility of ObserverPostProcessor is to register observers in an ObserverMap object by a static Map attribute. It does so by checking if a bean is annotated with @observe, and, in that case, it takes the parameter value from the annotation and puts the observer in the ObserverMap, with that parameter value as key. Note that we have a slight difference here as compared to the general Observer design previously explained: we use a separate class to store the observer instances.
2. ObserverAspect: it's a Spring AOP aspect configured to intercept a call to a method of the target system (represented by SystemComponent class in our example). In the context of such interception, it extracts the parameter passed to the SystemComponent method and uses it to get the corresponding observer. It creates an instance of the Event class with that parameter and calls the notify method of the observer passing that instance. Here we have another difference as compared to the Observer pattern schema of the previous section: it's the ObserverAspect, and not the target system (the "Observable" in the classic pattern), that has the responsibility of sending events to the registered observers.
Basic Interactions
Hereafter is a picture with two sequence diagrams that show the basic interactions, respectively, in the startup and running phases of the application:
UML sequence diagram that shows the basic interactions related to ObserverPostProcessor and ObserverAspect classes.
Coding the Whole Design
Now that we have shown a high-level description of the solution, let's describe in detail each piece of code of the provided example, followed by the related code snippet.
Observer Interface
This interface has a single method named notify with a parameter of type Event. The notify method will be called when the target system enters some specific phase. The mechanism by which it is called is based on the Spring AOP technology described in the previous section.
package com.example.observerdemo.core;
public interface Observer {
void notify(Event event);
}
Event Class
We can see the Event class as a wrapper of the system information we want to pass to the observers. It contains an eventPayload generic attribute of type Object that will be cast to the actual object type in the observer implementation of the notify method.
package com.example.observerdemo.core;
public class Event {
private Object eventPayload;
public Event(Object eventPayload) {
super();
this.eventPayload = eventPayload;
}
public Object getEventPayload() {
return eventPayload;
}
}
Observe Custom Annotation
Our custom annotation named Observe allows a Spring bean annotated with it to be detected by the BeanPostProcessor component and registered as an observer. It defines a parameter named appPhaseContextId, which represents some identification information of a specific state of the target system, and is also used as a key to store the observer in the ObserverMap object. The ObserverAspect will use that ID to match a particular observer to the execution of the target system's "phase-changing" method (in our example, the enterSomeApplicationPhase method of the SystemComponent class).
package com.example.observerdemo.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Observe {
public String appPhaseContextId() default "";
}
ObserverMap Class
The ObserverMap class is used to store the observers. It does so by a static Map attribute.
package com.example.observerdemo.core;
import java.util.HashMap;
import java.util.Map;
public class ObserverMap {
private static Map<String, Observer> observerMap;
public static Map<String, Observer> getObserverMap() {
if (observerMap == null) {
observerMap = new HashMap<String, Observer>();
}
return observerMap;
}
}
ObserverPostProcessor Class
The ObserverPostProcessor component implements the BeanPostProcessor interface and allows us to hook into the Spring lifecycle. The postProcessAfterInitialization method is executed by Spring during its startup phase. In our implementation, it scans the beans annotated with @observe and adds them to the ObserverMap as observers, using the annotation parameter as the key.
package com.example.observerdemo.core;
import java.lang.annotation.Annotation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class ObserverPostProcessor implements BeanPostProcessor {
@SuppressWarnings("unused")
private static final long serialVersionUID = 1L;
Logger log = LoggerFactory.getLogger(ObserverPostProcessor.class);
// @Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
try {
Class<?> clazz = bean.getClass();
if (clazz.isAnnotationPresent(Observe.class)) {
Annotation annotation = clazz.getAnnotation(Observe.class);
if (annotation instanceof Observe) {
Observe observerAnnotation = (Observe) annotation;
String appPhaseContextId = observerAnnotation.appPhaseContextId();
if (!ObserverMap.getObserverMap().containsKey(appPhaseContextId)) {
ObserverMap.getObserverMap().put(appPhaseContextId, (Observer) bean);
} else {
log.error("Cannot register bean " + bean.getClass().getName()
+ " as observer. Another observer already configured with id: "
+ appPhaseContextId);
}
}
}
} catch (Throwable e) {
log.error("Unexpected error: " + e.getMessage());
}
return bean;
}
}
ObserverAspect Class
The ObserverAspect component intercepts the call to the enterSomeApplicationPhase method of the SystemComponent class, gets the ID attribute from the SystemInfo parameter, and, if an observer exists with that ID, gets it from the map, creates an Event object with SystemInfo as payload, and executes the notify method of the observer passing that object.
package com.example.observerdemo.core;
import java.io.Serializable;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.example.observerdemo.SystemInfo;
@Component
@Aspect
public class ObserverAspect {
private Logger log = LoggerFactory.getLogger(ObserverAspect.class);
@After("execution(* com.example.observerdemo.SystemComponent.enterSomeApplicationPhase(..)))")
public void afterPhaseChange(JoinPoint joinPoint) {
try {
if(joinPoint.getArgs() != null) {
SystemInfo applicationInfo = (SystemInfo) joinPoint.getArgs()[0];
Observer observer = ObserverMap.getObserverMap().get(applicationInfo.getSomeId());
if (observer != null) {
Event event = new Event(applicationInfo);
observer.notify(event);
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
SystemComponent and SystemInfo Classes
In our example, the SystemComponent serves the sole purpose of representing a generic example of some external system that we want to "observe". It defines the enterSomeApplicationPhase method that is used to represent a phase change in the target system. The method takes a parameter SystemInfo that, in a real scenario, is supposed to contain some system context and, inside it, some identification information. In our example, we have defined it with the single attribute someId, just for simplicity.
package com.example.observerdemo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class SystemComponent {
private Logger log = LoggerFactory.getLogger(SystemComponent.class);
public void enterSomeApplicationPhase(SystemInfo systemInfo) {
log.info("Entered system phase: " + systemInfo.getSomeId());
}
}
package com.example.observerdemo;
public class SystemInfo {
String someId;
public String getSomeId() {
return someId;
}
public void setSomeId(String someId) {
this.someId = someId;
}
}
Let's Make it Run
To conclude the above discussion, we show how the whole solution can be used in practice. First, we define an Observer implementation named ObserverImpl that implements the Observer interface and is annotated with Observe, taking an appPhaseContextId parameter with a value equal to "someId". The notify method will just print the value of the event payload, i.e., the SystemInfo's someId attribute. You can see below the related code snippet:
@Component
@Observe(appPhaseContextId = "someId")
public class ObserverImpl implements Observer {
private Logger log = LoggerFactory.getLogger(SystemComponent.class);
@Override
public void notify(Event event) {
log.info("Catched event related to applicationInfo id: " + ((SystemInfo) event.getEventPayload()).getSomeId());
}
}
Then we can use our SystemComponent example class to verify the whole behavior. SystemComponent is a dummy representation of a hypothetic target system. We create an instance of it and execute the method enterSomeApplicationPhase. We could run it by a @SpringBootTest annotated class, for instance, as shown below:
@SpringBootTest
class DemoApplicationTests {
@Autowired
private SystemComponent systemComponent;
@Test
void contextLoads() {
SystemInfo systemInfo = new SystemInfo();
systemInfo.setSomeId("someId");
systemComponent.enterSomeApplicationPhase(systemInfo);
}
}
When the enterSomeApplicationPhase is executed, we will see the following message in the log: "Catched event related to applicationInfo id: ...". The above code, as well as that shown in the previous section, is available on GitHub maven project, as we already said in the introduction of this article.
A Review of the Basic Steps Involved in Running the Proposed Solution
We list below the steps involved in using to use an implementation of the above design:
- We define our observer by marking a regular Java class with the Observe annotation and the related appPhaseContextId parameter. We also make it implement the interface Observer with its sole method notify, which has a single parameter of type Event.
- We start the application.
- During Spring Boot startup, the ObserverPostProcessor will check all the beans that are being loaded in the Spring context, and if a bean happens to have the Observe annotation, it will store it into the ObserverMap static map attribute using the annotation parameter appPhaseContextId as the key.
- While the application is up, if the enterSomeApplicationPhase of the SystemComponent class is called, then an observer will be notified, marked with an annotation ID matching the enterSomeApplicationPhase parameter, if a matching exists.
- Then, the observer implementation of the notify method will be executed, containing the required functionality.
Conclusion
The Java Observer Pattern is a graceful design to handle the propagation of state change events from a class to a number of other classes. It could also be seen as a base to devise slightly different patterns based on the specific requirements. In the solution proposed in this article, we have shown how we can exploit this design to extend a system with new functionalities in an elegant and effective way.
Published at DZone with permission of Mario Casari. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments