Рубрики
Без рубрики

Весенний бобовый постпроцессор

Узнайте, как мы можем использовать BeanPostProcessor Spring для настройки самих бобов.

Автор оригинала: Mark Thomas.

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, 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 из его свойства 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 .