Spring BeanPostProcessor

1. Общ преглед

И така, в редица други уроци говорихме за BeanPostProcessor . В този урок ще ги използваме в реалния пример, използвайки EventBus на Guava .

Spring's BeanPostProcessor ни дава куки в жизнения цикъл на Spring bean, за да модифицираме конфигурацията му.

BeanPostProcessor позволява директна модификация на самите зърна.

В този урок ще разгледаме конкретен пример за тези класове, интегриращи EventBus на Guava .

2. Настройка

Първо, трябва да създадем нашата среда. Нека добавим зависимостите Spring Spring, Spring Expression и 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 

След това нека обсъдим нашите цели.

3. Цели и изпълнение

За първата ни цел искаме да използваме EventBus на Guava, за да предаваме асинхронно съобщения в различни аспекти на системата .

След това искаме да регистрираме и отписваме обекти за събития автоматично при създаване / унищожаване на боб, вместо да използваме ръчния метод, предоставен от EventBus .

И така, вече сме готови да започнем да кодираме!

Нашето изпълнение ще се състои от клас обвивка за EventBus на Guava, анотация на персонализиран маркер, BeanPostProcessor , модел на обект и боб за получаване на събития за търговия на акции от EventBus . Освен това ще създадем тестов случай, за да проверим желаната функционалност.

3.1. EventBus Wrapper

За да бъдем с, ще дефинираме обвивката на EventBus, за да предоставим някои статични методи за лесно регистриране и отписване на боб за събития, които ще бъдат използвани от 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); } }

Този код предоставя статични методи за достъп до GlobalEventBus и основните EventBus, както и регистриране и отписване за събития и публикуване на събития. Той също така има израз SpEL, използван като израз по подразбиране в нашата персонализирана анотация, за да дефинира коя EventBus искаме да използваме.

3.2. Анотация на персонализиран маркер

След това нека дефинираме персонализирана анотация на маркер, която ще се използва от BeanPostProcessor за идентифициране на зърна за автоматично регистриране / отписване за събития:

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

3.3. BeanPostProcessor

Сега ще дефинираме BeanPostProcessor, който ще проверява всеки компонент за анотацията на абоната . Този клас е и DestructionAwareBeanPostProcessor, който е интерфейс Spring, добавящ обратно извикване преди унищожаване към BeanPostProcessor . Ако анотацията е налице, ще я регистрираме в EventBus, идентифицирана от израза SpEL на анотацията при инициализация на боб и ще я отпишем при унищожаване на боб:

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

Кодът по-горе взема всеки боб и го изпълнява чрез метода на процеса , дефиниран по-долу. Той го обработва, след като бобът е инициализиран и преди да бъде унищожен. На requiresDestruction връща метод истинските по подразбиране и пазим това поведение, когато сме се провери за наличието на @Subscriber анотация в postProcessBeforeDestruction обратно повикване.

Нека сега разгледаме метода на процеса :

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

Този код проверява за съществуването на нашата анотация на персонализиран маркер с име Subscriber и, ако е налице, чете израза SpEL от неговото свойство стойност . След това изразът се оценява в обект. Ако това е екземпляр на EventBus, ние прилагаме параметъра на функцията BiConsumer към компонента . В BiConsumer се използва да се регистрирате и отпишете боб от EventBus .

Изпълнението на метода getTargetObject е както следва:

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

След това нека дефинираме нашия обект на модел StockTrade :

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

3.5. StockTradePublisher приемник на събития

След това нека дефинираме клас на слушател, който да ни уведоми, че е получена сделка, за да можем да напишем нашия тест:

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

Накрая ще дефинираме приемник за нови събития в 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)); } }

Кодът по-горе маркира този клас като абонат на събития на Guava EventBus , а анотацията @Subscribe на Guava маркира метода handleNewStockTradeEvent като приемник на събития. Типът събития, които ще получи, се основава на класа на единичния параметър към метода; в този случай ще получим събития от тип StockTrade .

В @AllowConcurrentEvents анотацията позволява едновременното позоваването на този метод. След като получим сделка, правим каквато и да е обработка, която желаем, след това уведомяваме всички слушатели.

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.