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, BiConsumerconsumer, 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 { SetstockTradeListeners = 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 .