1. Обзор
Итак, в ряде других учебных пособий мы говорили о BeanPostProcessor . В этом уроке мы применим их в реальном примере, используя Guava EventBus .
Пружины BeanPostProcessor дает нам крючки в жизненном цикле Spring bean для изменения его конфигурации.
BeanPostProcessor позволяет напрямую модифицировать сами бобы.
В этом уроке мы рассмотрим конкретный пример этих классов, интегрирующих Guava EventBus .
2. Настройка
Во-первых, нам нужно настроить нашу среду. Давайте добавим Spring Контекст , Spring Выражение и 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 Гуавы для асинхронной передачи сообщений по различным аспектам системы .
Затем мы хотим автоматически регистрировать и отменять регистрацию объектов для событий при создании/уничтожении бобов вместо использования ручного метода, предоставляемого EventBus .
Итак, теперь мы готовы приступить к кодированию!
Наша реализация будет состоять из класса-оболочки для Guava EventBus , пользовательской аннотации маркера, BeanPostProcessor , объекта модели и компонента для получения событий торговли акциями из EventBus . Кроме того, мы создадим тестовый случай для проверки желаемой функциональности.
3.1. Оболочка EventBus
Для начала мы определим оболочку 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
}
} Приведенный выше код берет каждый компонент и запускает его через метод process , определенный ниже. Он обрабатывает его после инициализации компонента и до его уничтожения. Метод required Destruction по умолчанию возвращает true, и мы сохраняем это поведение здесь, когда проверяем наличие аннотации @Subscriber в postProcessBeforeDestruction обратный вызов.
Давайте теперь рассмотрим метод process :
private void process(Object bean, BiConsumerconsumer, 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 из его свойства value . Затем выражение вычисляется в объект. Если это экземпляр EventBus, мы применяем параметр BiConsumer function к компоненту. Потребитель используется для регистрации и отмены регистрации компонента из 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. Объект модели Биржевой торговли
Далее, давайте определим наш объект Торговля акциями модель:
public class StockTrade {
private String symbol;
private int quantity;
private double price;
private Date tradeDate;
// constructor
}3.5. Приемник событий Издателя Биржевой торговли
Затем давайте определим класс слушателя, который уведомит нас о получении сделки, чтобы мы могли написать наш тест:
@FunctionalInterface
public interface StockTradeListener {
void stockTradePublished(StockTrade trade);
}Наконец, мы определим получателя для новых Биржевых торгов событий:
@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 events и @Subscribe аннотация Guava помечает метод handleNewStockTradeEvent как приемник событий. Тип событий, которые он будет получать, зависит от класса одного параметра метода; в этом случае мы будем получать события типа StockTrade .
Аннотация @AllowConcurrentEvents позволяет одновременно вызывать этот метод. Как только мы получим сделку, мы сделаем любую обработку, которую пожелаем, а затем уведомим всех слушателей.
3.6. Тестирование
Теперь давайте завершим наше кодирование интеграционным тестом, чтобы проверить правильность работы BeanPostProcessor . Во-первых, нам понадобится весенний контекст:
@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();
}
}Теперь мы можем реализовать наш тест:
@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();
}
}Приведенный выше тестовый код генерирует биржевую сделку и отправляет ее в GlobalEventBus . Мы ждем не более двух секунд для завершения действия и получения уведомления о том, что сделка была получена stockTradePublisher . Кроме того, мы подтверждаем, что полученная торговля не была изменена в пути.
4. Заключение
В заключение, Spring BeanPostProcessor позволяет нам настраивать сами бобы , предоставляя нам средства для автоматизации действий бобов, которые в противном случае нам пришлось бы выполнять вручную.
Как всегда, исходный код доступен на GitHub .