Spring BeanPostProcessor

1. Oversigt

Så i en række andre tutorials har vi talt om BeanPostProcessor. I denne vejledning vil vi sætte dem i brug i et virkeligt eksempel ved hjælp af Guava EventBus.

Forårets BeanPostProcessor giver os kroge ind i Spring Bean's livscyklus for at ændre dens konfiguration.

BeanPostProcessor muliggør direkte modifikation af selve bønnerne.

I denne vejledning skal vi se på et konkret eksempel på, at disse klasser integrerer guavas EventBus.

2. Opsætning

Først skal vi oprette vores miljø. Lad os tilføje Spring Context, Spring Expression og Guava afhængigheder til vores 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 

Lad os derefter diskutere vores mål.

3. Mål og implementering

For vores første mål ønsker vi det bruge guavas EventBus at sende meddelelser på tværs af forskellige aspekter af systemet asynkront.

Dernæst vil vi registrere og afregistrere objekter til begivenheder automatisk ved oprettelse / destruktion af bønner i stedet for at bruge den manuelle metode, der leveres af EventBus.

Så vi er nu klar til at starte kodning!

Vores implementering vil bestå af en indpakningsklasse til Guava EventBus, en brugerdefineret markørnotering, a BeanPostProcessor, et modelobjekt og en bønne til at modtage aktiehandelsbegivenheder fra EventBus. Derudover opretter vi en test sag for at kontrollere den ønskede funktionalitet.

3.1. EventBus Indpakning

For at være sammen definerer vi en EventBus indpakning for at give nogle statiske metoder til let at registrere og afregistrere bønner til begivenheder, som vil blive brugt af BeanPostProcessor:

public final class GlobalEventBus {public static final String GLOBAL_EVENT_BUS_EXPRESSION = "T (com.baeldung.postprocessor.GlobalEventBus) .getEventBus ()"; privat statisk endelig String IDENTIFIER = "global-event-bus"; privat statisk endelig GlobalEventBus GLOBAL_EVENT_BUS = ny GlobalEventBus (); privat endelig EventBus eventBus = ny AsyncEventBus (IDENTIFIER, Executors.newCachedThreadPool ()); privat GlobalEventBus () {} offentlig statisk GlobalEventBus getInstance () {returner GlobalEventBus.GLOBAL_EVENT_BUS; } offentlig statisk EventBus getEventBus () {returner GlobalEventBus.GLOBAL_EVENT_BUS.eventBus; } public static void subscribe (Object obj) {getEventBus (). register (obj); } offentlig statisk ugyldig afmelding (Object obj) {getEventBus (). unregister (obj); } offentlig statisk ugyldig post (Objektbegivenhed) {getEventBus (). post (begivenhed); }}

Denne kode giver statiske metoder til at få adgang til GlobalEventBus og underliggende EventBus samt tilmelding og afmelding af begivenheder og udstationering af begivenheder. Det har også et SpEL-udtryk, der bruges som standardudtryk i vores brugerdefinerede kommentar til at definere hvilket EventBus vi ønsker at bruge.

3.2. Kommentar til brugerdefineret markør

Lad os derefter definere en brugerdefineret markørkommentar, som vil blive brugt af BeanPostProcessor at identificere bønner til automatisk registrering / afregistrering af begivenheder:

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

3.3. BeanPostProcessor

Nu definerer vi BeanPostProcessor som vil kontrollere hver bønne for Abonnent kommentar. Denne klasse er også en DestructionAwareBeanPostProcessor, som er en Spring-grænseflade, der tilføjer et tilbagekald før ødelæggelse BeanPostProcessor. Hvis kommentaren er til stede, registrerer vi den med EventBus identificeret ved kommentarens SpEL-udtryk om initialisering af bønner og afregistrere det ved bønnedestruktion:

offentlig klasse GuavaEventBusBeanPostProcessor implementerer DestructionAwareBeanPostProcessor {Logger logger = LoggerFactory.getLogger (this.getClass ()); SpelExpressionParser expressionParser = ny SpelExpressionParser (); @Override offentligt ugyldigt postProcessBeforeDestruction (Object bean, String beanName) kaster BeansException {this.process (bean, EventBus :: unregister, "ødelæggelse"); } @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 ugyldighedsproces (Objektbønne, BiConsumer-forbruger, String-handling) {// Se implementering nedenfor}}

Koden ovenfor tager hver bønne og kører den gennem behandle metode, defineret nedenfor. Den behandler den, efter at bønnen er initialiseret, og før den ødelægges. Det kræver ødelæggelse metoden returneres som standard, og vi opbevarer den adfærd her, når vi kontrollerer for eksistensen af @Abonnent kommentar i postProcessBeforeDestruction ring tilbage.

Lad os nu se på behandle metode:

privat ugyldig proces (Objektbønne, BiConsumer forbruger, String-handling) {Objekt proxy = this.getTargetObject (bønne); Abonnentannotation = AnnotationUtils.getAnnotation (proxy.getClass (), Subscriber.class); hvis (annotation == null) returnerer; this.logger.info ("{}: behandler bønner af typen {} under {}", this.getClass (). getSimpleName (), proxy.getClass (). getName (), handling); String annotationValue = annotation.value (); prøv {Expression expression = this.expressionParser.parseExpression (annotationValue); Objektværdi = expression.getValue (); hvis (! (værdiinstans af EventBus)) {this.logger.error ("{}: udtryk {} ikke blev evalueret til en forekomst af EventBus for bønne af typen {}", this.getClass (). getSimpleName (), annotationValue , proxy.getClass (). getSimpleName ()); Vend tilbage; } EventBus eventBus = (EventBus) værdi; forbruger.accept (eventBus, proxy); } fange (ExpressionException ex) {this.logger.error ("{}: ude af stand til at analysere / evaluere udtryk {} for bønner af typen {}", this.getClass (). getSimpleName (), annotationValue, proxy.getClass () .getnavn ()); }}

Denne kode kontrollerer for eksistensen af ​​vores navngivne brugerdefinerede markørkommentar Abonnent og hvis den er til stede, læser den SpEL-udtrykket fra dets værdi ejendom. Derefter evalueres udtrykket til et objekt. Hvis det er en forekomst af EventBus, vi anvender BiConsumer funktionsparameter til bønnen. Det BiConsumer bruges til at registrere og afregistrere bønnen fra EventBus.

Implementeringen af ​​metoden getTargetObject er som følgende:

privat objekt getTargetObject (objekt proxy) kaster BeansException {hvis (AopUtils.isJdkDynamicProxy (proxy)) {prøv {return ((rådgivet) proxy) .getTargetSource (). getTarget (); } fange (Undtagelse e) {smid ny FatalBeanException ("Fejl ved at få mål for JDK-proxy", e); }} returner proxy; }

3.4. StockTrade Modelobjekt

Lad os derefter definere vores StockTrade model objekt:

offentlig klasse StockTrade {privat streng symbol; privat int mængde; privat dobbelt pris; privat Date tradeDate; // konstruktør}

3.5. StockTradePublisher Begivenhedsmodtager

Lad os derefter definere en lytterklasse, der giver os besked om, at en handel blev modtaget, så vi kan skrive vores test:

@FunctionalInterface offentlig grænseflade StockTradeListener {ugyldig stockTradePublished (StockTrade-handel); }

Endelig definerer vi en modtager til ny StockTrade begivenheder:

@Subscriber public class StockTradePublisher {Set stockTradeListeners = new HashSet (); offentlig ugyldighed addStockTradeListener (StockTradeListener lytter) {synkroniseret (this.stockTradeListeners) {this.stockTradeListeners.add (lytter); }} offentlig ugyldighed fjerneStockTradeListener (StockTradeListener lytter) {synkroniseret (this.stockTradeListeners) {this.stockTradeListeners.remove (lytter); }} @Tilmeld @AllowConcurrentEvents ugyldig handleNewStockTradeEvent (StockTrade handel) {// udgiv til DB, send til PubNub, ... Sæt lyttere; synkroniseret (this.stockTradeListeners) {lyttere = ny HashSet (this.stockTradeListeners); } lyttere.forEach (li -> li.stockTradePublished (handel)); }}

Koden ovenfor markerer denne klasse som en Abonnent af Guava EventBus begivenheder og Guava @Tilmeld annotation markerer metoden handleNewStockTradeEvent som modtager af begivenheder. Den type begivenheder, den modtager, er baseret på klassen for den enkelte parameter til metoden; i dette tilfælde modtager vi begivenheder af typen StockTrade.

Det @AllowConcurrentEvents annotation tillader samtidig påkaldelse af denne metode. Når vi har modtaget en handel, foretager vi enhver behandling, vi ønsker, og underretter alle lyttere.

3.6. Testning

Lad os nu afslutte vores kodning med en integrationstest for at bekræfte BeanPostProcessor fungerer korrekt. For det første har vi brug for en forårssammenhæng:

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

Nu kan vi implementere vores test:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (klasser = PostProcessorConfiguration.class) offentlig klasse StockTradeIntegrationTest {@Autowired StockTradePublisher stockTradePublisher; @Test offentlig ugyldighed givenValidConfig_whenTradePublished_thenTradeReceived () {Date tradeDate = new Date (); StockTrade stockTrade = ny StockTrade ("AMZN", 100, 2483.52d, tradeDate); AtomicBoolean assertionsPassed = nye AtomicBoolean (falsk); StockTradeListener-lytter = handel -> assertionsPassed .set (this.verifyExact (stockTrade, handel)); this.stockTradePublisher.addStockTradeListener (lytter); prøv {GlobalEventBus.post (stockTrade); afventer (). atMost (Duration.ofSeconds (2L)). untilAsserted (() -> assertThat (assertionsPassed.get ()). isTrue ()); } endelig {this.stockTradePublisher.remstockStockTradeListener (lytter); }} boolsk verifikation (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 aktiehandel og sender den til GlobalEventBus. Vi venter højst to sekunder på, at handlingen er afsluttet, og at vi får besked om, at handlen blev modtaget af stockTradePublisher. Desuden validerer vi, at den modtagne handel ikke blev ændret under transit.

4. Konklusion

Afslutningsvis forårets BeanPostProcessor tillader os at tilpasse bønnerne selv, der giver os et middel til at automatisere bønnehandlinger, som vi ellers skulle gøre manuelt.

Som altid er kildekoden tilgængelig på GitHub.