Spring BeanPostProcessor

1. Overzicht

Dus in een aantal andere tutorials hebben we het gehad over BeanPostProcessor. In deze tutorial zullen we ze in een realistisch voorbeeld gebruiken met Guava's EventBus.

Lente BeanPostProcessor geeft ons haken in de levenscyclus van Spring bean om de configuratie ervan aan te passen.

BeanPostProcessor maakt directe modificatie van de bonen zelf mogelijk.

In deze tutorial gaan we kijken naar een concreet voorbeeld van deze klassen waarin Guava's zijn geïntegreerd EventBus.

2. Installatie

Eerst moeten we onze omgeving opzetten. Laten we de afhankelijkheden Spring Context, Spring Expression en Guava toevoegen aan onze 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 

Laten we vervolgens onze doelen bespreken.

3. Doelen en implementatie

Voor ons eerste doel willen we gebruik Guava's EventBus om berichten asynchroon over verschillende aspecten van het systeem te sturen.

Vervolgens willen we objecten automatisch registreren en uitschrijven voor gebeurtenissen bij het maken / vernietigen van bonen in plaats van de handmatige methode te gebruiken die wordt geboden door EventBus.

Dus we zijn nu klaar om te beginnen met coderen!

Onze implementatie zal bestaan ​​uit een wikkelklasse voor Guava's EventBus, een aangepaste markeringsannotatie, een BeanPostProcessor, een modelobject en een boon om beursevenementen van de EventBus. Daarnaast maken we een testcase om de gewenste functionaliteit te verifiëren.

3.1. EventBus Wikkel

Om bij te zijn, definiëren we een EventBus wrapper om een ​​aantal statische methoden te bieden om eenvoudig bonen te registreren en uit te schrijven voor evenementen die door de BeanPostProcessor:

openbare laatste klasse GlobalEventBus {openbare statische laatste String GLOBAL_EVENT_BUS_EXPRESSION = "T (com.baeldung.postprocessor.GlobalEventBus) .getEventBus ()"; private static final String IDENTIFIER = "global-event-bus"; privé statische finale GlobalEventBus GLOBAL_EVENT_BUS = nieuwe GlobalEventBus (); private final EventBus eventBus = nieuwe AsyncEventBus (IDENTIFIER, Executors.newCachedThreadPool ()); private GlobalEventBus () {} openbare statische GlobalEventBus getInstance () {return GlobalEventBus.GLOBAL_EVENT_BUS; } openbare statische EventBus getEventBus () {terugkeer GlobalEventBus.GLOBAL_EVENT_BUS.eventBus; } openbare statische leegte abonneren (Object obj) {getEventBus (). register (obj); } openbare statische leegte uitschrijven (Object obj) {getEventBus (). unregister (obj); } openbare statische ongeldige post (objectgebeurtenis) {getEventBus (). post (gebeurtenis); }}

Deze code biedt statische methoden om toegang te krijgen tot het GlobalEventBus en onderliggend EventBus evenals het in- en uitschrijven voor evenementen en het plaatsen van evenementen. Het heeft ook een SpEL-uitdrukking die wordt gebruikt als de standaarduitdrukking in onze aangepaste annotatie om te definiëren welke EventBus we willen gebruiken.

3.2. Aangepaste markeringsannotatie

Laten we vervolgens een aangepaste markeringsannotatie definiëren die zal worden gebruikt door de BeanPostProcessor bonen identificeren om zich automatisch aan / uit te schrijven voor evenementen:

@Retention (RetentionPolicy.RUNTIME) @Target (ElementType.TYPE) @Inherited public @interface Subscriber {Stringwaarde () standaard GlobalEventBus.GLOBAL_EVENT_BUS_EXPRESSION; }

3.3. BeanPostProcessor

Nu gaan we de BeanPostProcessor die elke boon controleert op de Abonnee annotatie. Deze klasse is ook een VernietigingAwareBeanPostProcessor, wat een Spring-interface is die een callback voor vernietiging toevoegt aan BeanPostProcessor. Als de annotatie aanwezig is, registreren we deze bij de EventBus geïdentificeerd door de SpEL-expressie van de annotatie bij beaninitialisatie en de registratie ongedaan maken bij het vernietigen van bonen:

openbare klasse GuavaEventBusBeanPostProcessor implementeert DestructionAwareBeanPostProcessor {Logger-logger = LoggerFactory.getLogger (this.getClass ()); SpelExpressionParser expressionParser = nieuwe SpelExpressionParser (); @Override public void postProcessBeforeDestruction (Object bean, String beanName) gooit BeansException {this.process (bean, EventBus :: unregister, "vernietiging"); } @Override public boolean vereistDestruction (Object bean) {return true; } @Override openbaar object postProcessBeforeInitialization (Object bean, String beanName) gooit BeansException {return bean; } @Override openbaar object postProcessAfterInitialization (Object bean, String beanName) gooit BeansException {this.process (bean, EventBus :: register, "initialisatie"); terugkeer boon; } privé ongeldig proces (Object bean, BiConsumer consumer, String action) {// Zie implementatie hieronder}}

De bovenstaande code neemt elke bean en voert deze door het werkwijze methode, hieronder gedefinieerd. Het verwerkt het nadat de boon is geïnitialiseerd en voordat deze wordt vernietigd. De vereist Vernietiging methode retourneert standaard true en we behouden dat gedrag hier terwijl we controleren op het bestaan ​​van de @Abonnee annotatie in het postProcessBeforeDestruction Bel terug.

Laten we nu eens kijken naar de werkwijze methode:

privé ongeldig proces (Object-bean, BiConsumer-consument, String-actie) {Object proxy = this.getTargetObject (bean); Annotatie van de abonnee = AnnotationUtils.getAnnotation (proxy.getClass (), Subscriber.class); if (annotatie == null) retourneren; this.logger.info ("{}: processing bean van het type {} tijdens {}", this.getClass (). getSimpleName (), proxy.getClass (). getName (), actie); String annotationValue = annotation.value (); probeer {Expression expression = this.expressionParser.parseExpression (annotationValue); Objectwaarde = expression.getValue (); if (! (waarde instantie van EventBus)) {this.logger.error ("{}: expressie {} heeft niet geëvalueerd naar een instantie van EventBus voor bean van het type {}", this.getClass (). getSimpleName (), annotationValue , proxy.getClass (). getSimpleName ()); terugkeren; } EventBus eventBus = (EventBus) waarde; consumer.accept (eventBus, proxy); } catch (ExpressionException ex) {this.logger.error ("{}: kan expressie niet parseren / evalueren {} voor bean van het type {}", this.getClass (). getSimpleName (), annotationValue, proxy.getClass () .getName ()); }}

Deze code controleert op het bestaan ​​van onze aangepaste markeringsannotatie met de naam Abonnee en leest, indien aanwezig, de SpEL-uitdrukking uit zijn waarde eigendom. Vervolgens wordt de uitdrukking geëvalueerd in een object. Als het een instantie is van EventBus, we passen de BiConsumer functieparameter aan de boon. De BiConsumer wordt gebruikt om de boon aan en af ​​te melden van het EventBus.

De implementatie van de methode getTargetObject is als volgt:

private Object getTargetObject (Object proxy) gooit BeansException {if (AopUtils.isJdkDynamicProxy (proxy)) {probeer {return ((Advised) proxy) .getTargetSource (). getTarget (); } catch (uitzondering e) {throw nieuwe FatalBeanException ("Fout bij ophalen doel van JDK-proxy", e); }} retourneer proxy; }

3.4. StockTrade Modelobject

Laten we vervolgens onze definiëren StockTrade model object:

openbare klasse StockTrade {privé String-symbool; private int hoeveelheid; particuliere dubbele prijs; privé Date tradeDate; // constructor}

3.5. StockTradePublisher Gebeurtenis-ontvanger

Laten we vervolgens een listenerklasse definiëren om ons te laten weten dat een transactie is ontvangen, zodat we onze test kunnen schrijven:

@FunctionalInterface openbare interface StockTradeListener {void stockTradePublished (StockTrade-handel); }

Ten slotte definiëren we een ontvanger voor nieuw StockTrade evenementen:

@Subscriber openbare klasse StockTradePublisher {Set stockTradeListeners = nieuwe HashSet (); public void addStockTradeListener (StockTradeListener luisteraar) {gesynchroniseerd (this.stockTradeListeners) {this.stockTradeListeners.add (luisteraar); }} public void removeStockTradeListener (StockTradeListener luisteraar) {gesynchroniseerd (this.stockTradeListeners) {this.stockTradeListeners.remove (luisteraar); }} @Subscribe @AllowConcurrentEvents ongeldig handleNewStockTradeEvent (StockTrade-handel) {// publiceer naar DB, stuur naar PubNub, ... Stel luisteraars in; gesynchroniseerd (this.stockTradeListeners) {listeners = nieuwe HashSet (this.stockTradeListeners); } listeners.forEach (li -> li.stockTradePublished (handel)); }}

De bovenstaande code markeert deze klasse als een Abonnee van Guava EventBus evenementen en Guava's @Abonneren annotatie markeert de methode handleNewStockTradeEvent als ontvanger van evenementen. Het type gebeurtenissen dat het zal ontvangen, is gebaseerd op de klasse van de enkele parameter van de methode; in dit geval ontvangen we evenementen van het type StockTrade.

De @AllowConcurrentEvents annotatie maakt de gelijktijdige aanroep van deze methode mogelijk. Zodra we een transactie hebben ontvangen, voeren we elke gewenste verwerking uit en stellen we eventuele luisteraars op de hoogte.

3.6. Testen

Laten we nu onze codering afronden met een integratietest om het BeanPostProcessor correct werkt. Ten eerste hebben we een lente-context nodig:

@Configuration openbare klasse PostProcessorConfiguration {@Bean openbare GlobalEventBus eventBus () {terugkeer GlobalEventBus.getInstance (); } @Bean openbare GuavaEventBusBeanPostProcessor eventBusBeanPostProcessor () {retourneer nieuwe GuavaEventBusBeanPostProcessor (); } @Bean public StockTradePublisher stockTradePublisher () {retourneer nieuwe StockTradePublisher (); }}

Nu kunnen we onze test uitvoeren:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = PostProcessorConfiguration.class) openbare klasse StockTradeIntegrationTest {@Autowired StockTradePublisher stockTradePublisher; @Test openbare ongeldig gegevenValidConfig_whenTradePublished_thenTradeReceived () {Date tradeDate = new Date (); StockTrade stockTrade = nieuwe StockTrade ("AMZN", 100, 2483.52d, tradeDate); AtomicBoolean assertionsPassed = nieuwe AtomicBoolean (false); StockTradeListener listener = trade -> assertionsPassed .set (this.verifyExact (stockTrade, trade)); this.stockTradePublisher.addStockTradeListener (luisteraar); probeer {GlobalEventBus.post (stockTrade); await (). atMost (Duration.ofSeconds (2L)) .untilAsserted (() -> assertThat (assertionsPassed.get ()). isTrue ()); } eindelijk {this.stockTradePublisher.removeStockTradeListener (luisteraar); }} 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 (); }}

De bovenstaande testcode genereert een aandelentransactie en plaatst deze op de GlobalEventBus. We wachten maximaal twee seconden totdat de actie is voltooid en om op de hoogte te worden gesteld dat de transactie is ontvangen door de stockTradePublisher. Bovendien valideren we dat de ontvangen transactie tijdens het transport niet is gewijzigd.

4. Conclusie

Tot slot, Spring's BeanPostProcessor sta ons toe om pas de bonen zelf aan, wat ons een middel biedt om bean-acties te automatiseren die we anders handmatig zouden moeten uitvoeren.

Zoals altijd is de broncode beschikbaar op GitHub.