Spring BeanPostProcessor

1. Vue d'ensemble

Ainsi, dans un certain nombre d'autres didacticiels, nous avons parlé de BeanPostProcessor . Dans ce didacticiel, nous les utiliserons dans un exemple réel à l'aide de l' EventBus de Guava .

Spring's BeanPostProcessor nous donne des hooks dans le cycle de vie du Spring Bean pour modifier sa configuration.

BeanPostProcessor permet la modification directe des beans eux-mêmes.

Dans ce tutoriel, nous allons regarder un exemple concret de ces classes intégrant l' EventBus de Guava .

2. Configuration

Premièrement, nous devons mettre en place notre environnement. Ajoutons les dépendances Spring Context, Spring Expression et Guava à notre pom.xml :

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

Ensuite, discutons de nos objectifs.

3. Objectifs et mise en œuvre

Pour notre premier objectif, nous voulons utiliser l' EventBus de Guava pour transmettre des messages à travers divers aspects du système de manière asynchrone .

Ensuite, nous voulons enregistrer et désenregistrer automatiquement les objets pour les événements lors de la création / destruction du bean au lieu d'utiliser la méthode manuelle fournie par EventBus .

Nous sommes donc maintenant prêts à commencer à coder!

Notre implémentation consistera en une classe wrapper pour l' EventBus de Guava , une annotation de marqueur personnalisée, un BeanPostProcessor , un objet modèle et un bean pour recevoir les événements boursiers de l' EventBus . De plus, nous allons créer un cas de test pour vérifier la fonctionnalité souhaitée.

3.1. Wrapper EventBus

Pour être avec, nous allons définir un wrapper EventBus pour fournir des méthodes statiques pour enregistrer et désinscrire facilement les beans pour les événements qui seront utilisés par le BeanPostProcessor :

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); } }

Ce code fournit des méthodes statiques pour accéder au GlobalEventBus et sous - jacente EventBus ainsi que l' enregistrement et la désinscription pour des événements et des événements affichage. Il a également une expression SpEL utilisée comme expression par défaut dans notre annotation personnalisée pour définir quel EventBus nous voulons utiliser.

3.2. Annotation de marqueur personnalisé

Ensuite, définissons une annotation de marqueur personnalisée qui sera utilisée par le BeanPostProcessor pour identifier les beans afin d'enregistrer / désenregistrer automatiquement les événements:

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

3.3. BeanPostProcessor

Maintenant, nous allons définir le BeanPostProcessor qui vérifiera chaque bean pour l' annotation Subscriber . Cette classe est également un DestructionAwareBeanPostProcessor, qui est une interface Spring ajoutant un rappel avant destruction à BeanPostProcessor . Si l'annotation est présente, nous l'enregistrerons avec l' EventBus identifié par l'expression SpEL de l'annotation lors de l'initialisation du bean et l'annulerons lors de la destruction du bean:

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 } }

Le code ci-dessus prend chaque bean et l'exécute à travers la méthode process , définie ci-dessous. Il le traite après l'initialisation du bean et avant sa destruction. La méthode requiresDestruction retourne true par défaut et nous conservons ce comportement ici lorsque nous vérifions l'existence de l' annotation @Subscriber dans le rappel postProcessBeforeDestruction .

Regardons maintenant la méthode de processus :

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()); } }

Ce code vérifie l'existence de notre annotation de marqueur personnalisée nommée Subscriber et, si elle est présente, lit l'expression SpEL à partir de sa propriété value . Ensuite, l'expression est évaluée dans un objet. S'il s'agit d'une instance d' EventBus, nous appliquons le paramètre de fonction BiConsumer au bean. Le BiConsumer est utilisé pour enregistrer et désenregistrer le bean de l' EventBus .

L'implémentation de la méthode getTargetObject est la suivante:

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. Objet de modèle StockTrade

Ensuite, définissons notre objet de modèle StockTrade :

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

3.5. Récepteur d' événements StockTradePublisher

Ensuite, définissons une classe d'auditeur pour nous notifier qu'un échange a été reçu afin que nous puissions écrire notre test:

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

Enfin, nous définirons un récepteur pour les nouveaux événements StockTrade :

@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)); } }

Le code ci-dessus marque cette classe comme un abonné des événements Guava EventBus et l' annotation @Subscribe de Guava marque la méthode handleNewStockTradeEvent comme un récepteur d'événements. Le type d'événements qu'il recevra est basé sur la classe du paramètre unique de la méthode; dans ce cas, nous recevrons des événements de type StockTrade .

L' annotation @AllowConcurrentEvents permet l'invocation simultanée de cette méthode. Une fois que nous recevons un échange, nous effectuons le traitement que nous souhaitons, puis en informons tous les auditeurs.

3.6. Essai

Maintenant, terminons notre codage avec un test d'intégration pour vérifier que le BeanPostProcessor fonctionne correctement. Tout d'abord, nous aurons besoin d'un contexte Spring:

@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(); } }

Nous pouvons maintenant implémenter notre 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(); } }

Le code de test ci-dessus génère une transaction boursière et la publie sur GlobalEventBus . Nous attendons au plus deux secondes pour que l'action se termine et pour être notifié que la transaction a été reçue par le stockTradePublisher . De plus, nous validons que l'échange reçu n'a pas été modifié en transit.

4. Conclusion

En conclusion, le BeanPostProcessor de Spring nous permet de personnaliser les beans eux - mêmes , nous fournissant ainsi un moyen d'automatiser les actions des beans que nous aurions autrement à faire manuellement.

Comme toujours, le code source est disponible sur sur GitHub.