Spring BeanPostProcessor

1. Übersicht

In einer Reihe anderer Tutorials haben wir über BeanPostProcessor gesprochen . In diesem Tutorial werden sie mit Guavas EventBus in einem realen Beispiel verwendet .

Mit dem BeanPostProcessor von Spring können wir uns in den Lebenszyklus von Spring Bean einbinden, um dessen Konfiguration zu ändern.

BeanPostProcessor ermöglicht die direkte Änderung der Beans selbst.

In diesem Tutorial sehen wir uns ein konkretes Beispiel für diese Klassen an, in die Guavas EventBus integriert ist .

2. Setup

Zuerst müssen wir unsere Umgebung einrichten. Fügen wir unserer pom.xml die Abhängigkeiten Spring Context, Spring Expression und Guava hinzu :

 org.springframework spring-context 5.2.6.RELEASE   org.springframework spring-expression 5.2.6.RELEASE   com.google.guava guava 29.0-jre 

Als nächstes besprechen wir unsere Ziele.

3. Ziele und Umsetzung

Für unser erstes Ziel wollen wir Guava die nutzen EventBus passieren Nachrichten über verschiedene Aspekte des Systems asynchron .

Als Nächstes möchten wir Objekte für Ereignisse beim Erstellen / Zerstören von Beans automatisch registrieren und die Registrierung aufheben, anstatt die von EventBus bereitgestellte manuelle Methode zu verwenden .

Jetzt können wir mit dem Codieren beginnen!

Unsere Implementierung besteht aus einer Wrapper-Klasse für Guavas EventBus , einer benutzerdefinierten Markierungsanmerkung, einem BeanPostProcessor , einem Modellobjekt und einer Bean zum Empfangen von Aktienhandelsereignissen vom EventBus . Darüber hinaus erstellen wir einen Testfall, um die gewünschte Funktionalität zu überprüfen.

3.1. EventBus Wrapper

Zu diesem Zweck definieren wir einen EventBus- Wrapper, der einige statische Methoden zum einfachen Registrieren und Aufheben der Registrierung von Beans für Ereignisse bereitstellt , die vom BeanPostProcessor verwendet werden :

public final class GlobalEventBus { public static final String GLOBAL_EVENT_BUS_EXPRESSION = "T(com.baeldung.postprocessor.GlobalEventBus).getEventBus()"; private static final String IDENTIFIER = "global-event-bus"; private static final GlobalEventBus GLOBAL_EVENT_BUS = new GlobalEventBus(); private final EventBus eventBus = new AsyncEventBus(IDENTIFIER, Executors.newCachedThreadPool()); private GlobalEventBus() {} public static GlobalEventBus getInstance() { return GlobalEventBus.GLOBAL_EVENT_BUS; } public static EventBus getEventBus() { return GlobalEventBus.GLOBAL_EVENT_BUS.eventBus; } public static void subscribe(Object obj) { getEventBus().register(obj); } public static void unsubscribe(Object obj) { getEventBus().unregister(obj); } public static void post(Object event) { getEventBus().post(event); } }

Dieser Code bietet statische Methoden für den Zugriff auf den GlobalEventBus und den zugrunde liegenden EventBus sowie für das Registrieren und Aufheben der Registrierung für Ereignisse und das Posten von Ereignissen. Es gibt auch einen SpEL-Ausdruck, der als Standardausdruck in unserer benutzerdefinierten Anmerkung verwendet wird, um zu definieren, welchen EventBus wir verwenden möchten.

3.2. Benutzerdefinierte Markierungsanmerkung

Als Nächstes definieren wir eine benutzerdefinierte Markierungsanmerkung, die vom BeanPostProcessor verwendet wird , um Beans zu identifizieren, die für Ereignisse automatisch registriert / abgemeldet werden sollen :

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface Subscriber { String value() default GlobalEventBus.GLOBAL_EVENT_BUS_EXPRESSION; }

3.3. BeanPostProcessor

Jetzt definieren wir den BeanPostProcessor, der jede Bean auf die Subscriber- Annotation überprüft . Diese Klasse ist auch ein DestructionAwareBeanPostProcessor, eine Spring-Schnittstelle, die BeanPostProcessor einen Rückruf vor der Zerstörung hinzufügt . Wenn die Annotation vorhanden ist, registrieren wir sie bei dem EventBus, der durch den SpEL-Ausdruck der Annotation bei der Bean-Initialisierung identifiziert wird, und heben die Registrierung bei der Bean-Zerstörung auf:

public class GuavaEventBusBeanPostProcessor implements DestructionAwareBeanPostProcessor { Logger logger = LoggerFactory.getLogger(this.getClass()); SpelExpressionParser expressionParser = new SpelExpressionParser(); @Override public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { this.process(bean, EventBus::unregister, "destruction"); } @Override public boolean requiresDestruction(Object bean) { return true; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { this.process(bean, EventBus::register, "initialization"); return bean; } private void process(Object bean, BiConsumer consumer, String action) { // See implementation below } }

Der obige Code nimmt jede Bean und führt sie durch die unten definierte Prozessmethode . Es verarbeitet es, nachdem die Bohne initialisiert wurde und bevor sie zerstört wird. Die requiresDestruction Methode gibt true zurück , standardmäßig und wir halten dieses Verhalten hier , da wir für die Existenz des überprüfen @subscriber Anmerkung im postProcessBeforeDestruction Rückruf.

Schauen wir uns nun die Prozessmethode an :

private void process(Object bean, BiConsumer consumer, String action) { Object proxy = this.getTargetObject(bean); Subscriber annotation = AnnotationUtils.getAnnotation(proxy.getClass(), Subscriber.class); if (annotation == null) return; this.logger.info("{}: processing bean of type {} during {}", this.getClass().getSimpleName(), proxy.getClass().getName(), action); String annotationValue = annotation.value(); try { Expression expression = this.expressionParser.parseExpression(annotationValue); Object value = expression.getValue(); if (!(value instanceof EventBus)) { this.logger.error( "{}: expression {} did not evaluate to an instance of EventBus for bean of type {}", this.getClass().getSimpleName(), annotationValue, proxy.getClass().getSimpleName()); return; } EventBus eventBus = (EventBus)value; consumer.accept(eventBus, proxy); } catch (ExpressionException ex) { this.logger.error("{}: unable to parse/evaluate expression {} for bean of type {}", this.getClass().getSimpleName(), annotationValue, proxy.getClass().getName()); } }

Dieser Code prüft , ob unsere benutzerdefinierte Markierungsanmerkung mit dem Namen Subscriber vorhanden ist, und liest, falls vorhanden, den SpEL-Ausdruck aus seiner value- Eigenschaft. Dann wird der Ausdruck in ein Objekt ausgewertet. Wenn es sich um eine Instanz von EventBus handelt, wenden wir den BiConsumer- Funktionsparameter auf die Bean an. Der BiConsumer wird verwendet, um die Bean im EventBus zu registrieren und die Registrierung aufzuheben .

Die Implementierung der Methode getTargetObject lautet wie folgt:

private Object getTargetObject(Object proxy) throws BeansException { if (AopUtils.isJdkDynamicProxy(proxy)) { try { return ((Advised)proxy).getTargetSource().getTarget(); } catch (Exception e) { throw new FatalBeanException("Error getting target of JDK proxy", e); } } return proxy; }

3.4. StockTrade Modellobjekt

Als nächstes definieren wir unser StockTrade-Modellobjekt :

public class StockTrade { private String symbol; private int quantity; private double price; private Date tradeDate; // constructor }

3.5. StockTradePublisher- Ereignisempfänger

Definieren wir dann eine Listener-Klasse, um uns mitzuteilen, dass ein Trade eingegangen ist, damit wir unseren Test schreiben können:

@FunctionalInterface public interface StockTradeListener { void stockTradePublished(StockTrade trade); }

Schließlich definieren wir einen Empfänger für neue StockTrade- Ereignisse:

@Subscriber public class StockTradePublisher { Set stockTradeListeners = new HashSet(); public void addStockTradeListener(StockTradeListener listener) { synchronized (this.stockTradeListeners) { this.stockTradeListeners.add(listener); } } public void removeStockTradeListener(StockTradeListener listener) { synchronized (this.stockTradeListeners) { this.stockTradeListeners.remove(listener); } } @Subscribe @AllowConcurrentEvents void handleNewStockTradeEvent(StockTrade trade) { // publish to DB, send to PubNub, ... Set listeners; synchronized (this.stockTradeListeners) { listeners = new HashSet(this.stockTradeListeners); } listeners.forEach(li -> li.stockTradePublished(trade)); } }

Der obige Code kennzeichnet diese Klasse als Abonnenten von Guava EventBus- Ereignissen, und die @ Subscribe- Annotation von Guava kennzeichnet die Methode handleNewStockTradeEvent als Empfänger von Ereignissen. Die Art der Ereignisse, die empfangen werden, basiert auf der Klasse des einzelnen Parameters für die Methode. In diesem Fall erhalten wir Ereignisse vom Typ StockTrade .

Die Annotation @AllowConcurrentEvents ermöglicht den gleichzeitigen Aufruf dieser Methode. Sobald wir einen Trade erhalten haben, führen wir jede gewünschte Verarbeitung durch und benachrichtigen dann alle Zuhörer.

3.6. Testing

Now let's wrap up our coding with an integration test to verify the BeanPostProcessor works correctly. Firstly, we'll need a Spring context:

@Configuration public class PostProcessorConfiguration { @Bean public GlobalEventBus eventBus() { return GlobalEventBus.getInstance(); } @Bean public GuavaEventBusBeanPostProcessor eventBusBeanPostProcessor() { return new GuavaEventBusBeanPostProcessor(); } @Bean public StockTradePublisher stockTradePublisher() { return new StockTradePublisher(); } }

Now we can implement our test:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = PostProcessorConfiguration.class) public class StockTradeIntegrationTest { @Autowired StockTradePublisher stockTradePublisher; @Test public void givenValidConfig_whenTradePublished_thenTradeReceived() { Date tradeDate = new Date(); StockTrade stockTrade = new StockTrade("AMZN", 100, 2483.52d, tradeDate); AtomicBoolean assertionsPassed = new AtomicBoolean(false); StockTradeListener listener = trade -> assertionsPassed .set(this.verifyExact(stockTrade, trade)); this.stockTradePublisher.addStockTradeListener(listener); try { GlobalEventBus.post(stockTrade); await().atMost(Duration.ofSeconds(2L)) .untilAsserted(() -> assertThat(assertionsPassed.get()).isTrue()); } finally { this.stockTradePublisher.removeStockTradeListener(listener); } } boolean verifyExact(StockTrade stockTrade, StockTrade trade) { return Objects.equals(stockTrade.getSymbol(), trade.getSymbol()) && Objects.equals(stockTrade.getTradeDate(), trade.getTradeDate()) && stockTrade.getQuantity() == trade.getQuantity() && stockTrade.getPrice() == trade.getPrice(); } }

The test code above generates a stock trade and posts it to the GlobalEventBus. We wait at most two seconds for the action to complete and to be notified the trade was received by the stockTradePublisher. Furthermore, we validate the received trade was not modified in transit.

4. Conclusion

In conclusion, Spring's BeanPostProcessor allows us to customize the beans themselves, providing us with a means to automate bean actions we would otherwise have to do manually.

As always, source code is available over on GitHub.