Spring BeanPostProcessor

1. Oversikt

Så, i en rekke andre opplæringsprogrammer, har vi snakket om BeanPostProcessor. I denne opplæringen vil vi sette dem i bruk i et ekte eksempel ved hjelp av Guava EventBus.

Vårens BeanPostProcessor gir oss kroker inn i vårbønns livssyklus for å endre konfigurasjonen.

BeanPostProcessor tillater direkte modifisering av selve bønnene.

I denne opplæringen skal vi se på et konkret eksempel på at disse klassene integrerer Guava EventBus.

2. Oppsett

Først må vi sette opp miljøet vårt. La oss legge til avhengighet av vårkontekst, våruttrykk og guava 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 

La oss deretter diskutere våre mål.

3. Mål og gjennomføring

For vårt første mål, vil vi bruke Guava EventBus å sende meldinger på tvers av ulike aspekter av systemet asynkront.

Deretter vil vi registrere og avregistrere objekter for hendelser automatisk ved oppretting / ødeleggelse av bønner i stedet for å bruke den manuelle metoden som tilbys av EventBus.

Så vi er nå klare til å begynne å kode!

Implementeringen vår vil bestå av en wrapper-klasse for Guava EventBus, en egendefinert markørkommentar, a BeanPostProcessor, et modellobjekt og en bønne for å motta aksjehandelshendelser fra EventBus. I tillegg oppretter vi en testsak for å verifisere ønsket funksjonalitet.

3.1. EventBus Innpakning

For å være med vil vi definere en EventBus innpakning for å gi noen statiske metoder for enkelt å registrere og avregistrere bønner for hendelser som skal brukes av 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"; privat statisk finale GlobalEventBus GLOBAL_EVENT_BUS = ny GlobalEventBus (); privat final EventBus eventBus = ny AsyncEventBus (IDENTIFIER, Executors.newCachedThreadPool ()); privat GlobalEventBus () {} offentlig statisk GlobalEventBus getInstance () {return GlobalEventBus.GLOBAL_EVENT_BUS; } offentlig statisk 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); }}

Denne koden gir statiske metoder for tilgang til GlobalEventBus og underliggende EventBus samt registrere og avregistrere for arrangementer og legge ut hendelser. Den har også et SpEL-uttrykk som brukes som standarduttrykk i vår egendefinerte kommentar for å definere hvilket EventBus vi vil bruke.

3.2. Merknad for egendefinert markør

La oss deretter definere en egendefinert markørkommentar som skal brukes av BeanPostProcessor å identifisere bønner for automatisk å registrere / avregistrere for hendelser:

@Retention (RetentionPolicy.RUNTIME) @Target (ElementType.TYPE) @ Arvet offentlig @interface Abonnent {Strengverdi () standard GlobalEventBus.GLOBAL_EVENT_BUS_EXPRESSION; }

3.3. BeanPostProcessor

Nå skal vi definere BeanPostProcessor som vil sjekke hver bønne for Abonnent kommentar. Denne klassen er også en DestructionAwareBeanPostProsessor, som er et vårgrensesnitt som legger til en tilbakeringing før ødeleggelse BeanPostProcessor. Hvis merknaden er til stede, registrerer vi den med EventBus identifisert av kommentarens SpEL-uttrykk om initialisering av bønner og avregistrerer det om bønnedestruksjon:

offentlig klasse GuavaEventBusBeanPostProcessor implementerer DestructionAwareBeanPostProcessor {Logger logger = LoggerFactory.getLogger (this.getClass ()); SpelExpressionParser expressionParser = ny SpelExpressionParser (); @Override public void postProcessBeforeDestruction (Object bean, String beanName) kaster BeansException {this.process (bean, EventBus :: avregistrer, "ødeleggelse"); } @Override public boolean requiresDestruction (Object bean) {return true; } @ Override public Object postProcessBeforeInitialization (Object bean, String beanName) kaster BeansException {return bean; } @Override public Object postProcessAfterInitialization (Object bean, String beanName) kaster BeansException {this.process (bean, EventBus :: register, "initialization"); retur bønne; } privat ugyldig prosess (Object bean, BiConsumer consumer, String action) {// Se implementering nedenfor}}

Koden ovenfor tar hver bønne og kjører den gjennom prosess metode, definert nedenfor. Den behandler den etter at bønnen er initialisert og før den blir ødelagt. De krever ødeleggelse metoden returnerer som standard, og vi holder den oppførselen her mens vi sjekker for eksistensen av @Abonnent kommentar i postProcessBeforeDestruction Ring tilbake.

La oss nå se på prosess metode:

privat ugyldig prosess (Object bean, BiConsumer consumer, String action) {Object proxy = this.getTargetObject (bean); Abonnentanotering = AnnotationUtils.getAnnotation (proxy.getClass (), Subscriber.class); hvis (kommentar == null) returnerer; this.logger.info ("{}: behandler bønne av typen {} under {}", this.getClass (). getSimpleName (), proxy.getClass (). getName (), handling); String annotationValue = annotation.value (); prøv {Expression expression = this.expressionParser.parseExpression (annotationValue); Objektverdi = expression.getValue (); hvis (! (verdi forekomst av EventBus)) {this.logger.error ("{}: uttrykk {} ikke ble evaluert til en forekomst av EventBus for bønne av typen {}", this.getClass (). getSimpleName (), annotationValue , proxy.getClass (). getSimpleName ()); komme tilbake; } EventBus eventBus = (EventBus) verdi; consumer.accept (eventBus, proxy); } catch (ExpressionException ex) {this.logger.error ("{}: ikke i stand til å analysere / evaluere uttrykk {} for bønne av typen {}", this.getClass (). getSimpleName (), annotationValue, proxy.getClass () .getName ()); }}

Denne koden kontrollerer for eksistensen av vår tilpassede markørkommentar Abonnent og hvis den er til stede, leser du SpEL-uttrykket fra det verdi eiendom. Deretter blir uttrykket evaluert til et objekt. Hvis det er en forekomst av EventBus, vi bruker BiConsumer funksjonsparameter til bønnen. De BiConsumer brukes til å registrere og avregistrere bønnen fra EventBus.

Implementeringen av metoden getTargetObject er som følgende:

private Object getTargetObject (Object proxy) kaster BeansException {if (AopUtils.isJdkDynamicProxy (proxy)) {try {return ((Advised) proxy) .getTargetSource (). getTarget (); } fange (Unntak e) {kast ny FatalBeanException ("Feil ved å få mål for JDK-proxy", e); }} returner proxy; }

3.4. StockTrade Modellobjekt

La oss deretter definere vår StockTrade modellobjekt:

offentlig klasse StockTrade {private streng symbol; privat int mengde; privat dobbel pris; privat dato handel dato; // konstruktør}

3.5. StockTradePublisher Mottaker av hendelsen

La oss så definere en lytterklasse for å varsle oss om at en handel ble mottatt, slik at vi kan skrive testen vår:

@FunctionalInterface offentlig grensesnitt StockTradeListener {ugyldig stockTradePublished (StockTrade handel); }

Til slutt definerer vi en mottaker for ny StockTrade arrangementer:

@Subscriber public class StockTradePublisher {Set stockTradeListeners = new HashSet (); offentlig ugyldig addStockTradeListener (StockTradeListener lytter) {synkronisert (this.stockTradeListeners) {this.stockTradeListeners.add (lytter); }} offentlig tomrom fjerneStockTradeListener (StockTradeListener lytter) {synkronisert (this.stockTradeListeners) {this.stockTradeListeners.remove (lytter); }} @ Abonner @ AllowConcurrentEvents ugyldig handleNewStockTradeEvent (StockTrade handel) {// publiser til DB, send til PubNub, ... Sett lyttere; synkronisert (this.stockTradeListeners) {lyttere = ny HashSet (this.stockTradeListeners); } lyttere.forEach (li -> li.stockTradePublished (handel)); }}

Koden ovenfor markerer denne klassen som en Abonnent av Guava EventBus hendelser og Guava @Abonnere merknader markerer metoden handleNewStockTradeEvent som mottaker av hendelser. Type hendelser den mottar er basert på klassen til den enkelte parameteren til metoden; i dette tilfellet mottar vi hendelser av typen StockTrade.

De @AllowConcurrentEvents kommentar tillater samtidig påkallelse av denne metoden. Når vi har mottatt en handel, gjør vi den behandlingen vi ønsker, og varsler alle lyttere.

3.6. Testing

La oss nå avslutte kodingen vår med en integrasjonstest for å bekrefte BeanPostProcessor fungerer riktig. For det første trenger vi en vårkontekst:

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

Nå kan vi implementere testen vår:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (klasser = PostProcessorConfiguration.class) offentlig klasse StockTradeIntegrationTest {@Autowired StockTradePublisher stockTradePublisher; @Test offentlig ugyldig givenValidConfig_whenTradePublished_thenTradeReceived () {Date tradeDate = new Date (); StockTrade stockTrade = ny StockTrade ("AMZN", 100, 2483.52d, tradeDate); AtomicBoolean assertionsPassed = new AtomicBoolean (false); StockTradeListener lytter = handel -> assertionsPassed .set (this.verifyExact (stockTrade, handel)); this.stockTradePublisher.addStockTradeListener (lytter); prøv {GlobalEventBus.post (stockTrade); avvente (). atMost (Duration.ofSeconds (2L)) .tiltilAsserted (() -> assertThat (assertionsPassed.get ()). isTrue ()); } til slutt {this.stockTradePublisher.remstockStockTradeListener (lytter); }} boolsk verifisere eksakt (StockTrade stockTrade, StockTrade handel) {return Objects.equals (stockTrade.getSymbol (), trade.getSymbol ()) && Objects.equals (stockTrade.getTradeDate (), trade.getTradeDate ()) && stockTrade.getQuantity () == trade.getQuantity () && stockTrade.getPrice () == trade.getPrice (); }}

Testkoden ovenfor genererer en aksjehandel og legger den ut til GlobalEventBus. Vi venter maksimalt to sekunder på at handlingen skal fullføres og å bli varslet om at handelen ble mottatt av stockTradePublisher. Videre validerer vi at mottatt handel ikke ble endret under transitt.

4. Konklusjon

Avslutningsvis vårens BeanPostProcessor tillater oss å tilpasse bønnene selv, som gir oss et middel til å automatisere bønnehandlinger vi ellers måtte gjøre manuelt.

Som alltid er kildekoden tilgjengelig på GitHub.