Pemprosesan Spring BeanPost

1. Gambaran keseluruhan

Oleh itu, dalam beberapa tutorial lain, kami telah membincangkan mengenai BeanPostProcessor . Dalam tutorial ini, kami akan menggunakannya untuk digunakan dalam contoh dunia nyata menggunakan Guava's EventBus .

Spring's BeanPostProcessor memberi kita kaitan dengan kitaran hidup Spring bean untuk mengubah konfigurasi.

BeanPostProcessor memungkinkan pengubahsuaian langsung kacang itu sendiri.

Dalam tutorial ini, kita akan melihat contoh konkrit dari kelas-kelas ini yang menggabungkan Guava's EventBus .

2. Persediaan

Pertama, kita perlu mengatur persekitaran kita. Mari tambahkan pergantungan Konteks Musim Semi, Ekspresi Musim Semi, dan Jambu Batu ke pom.xml kami :

 org.springframework spring-context 5.2.6.RELEASE   org.springframework spring-expression 5.2.6.RELEASE   com.google.guava guava 29.0-jre 

Seterusnya, mari kita bincangkan tujuan kita.

3. Matlamat dan Pelaksanaan

Untuk tujuan pertama kami, kami ingin memanfaatkan EventBus Guava untuk menyampaikan mesej ke pelbagai aspek sistem secara serentak .

Seterusnya, kami ingin mendaftarkan dan membatalkan pendaftaran objek untuk acara secara automatik pada pembuatan / pemusnahan kacang dan bukannya menggunakan kaedah manual yang disediakan oleh EventBus .

Jadi, kami kini sudah bersedia untuk memulakan pengekodan!

Pelaksanaan kami akan terdiri dari kelas pembungkus untuk EventBus Guava , anotasi penanda khusus, BeanPostProcessor , objek model, dan kacang untuk menerima acara perdagangan saham dari EventBus . Sebagai tambahan, kami akan membuat kes ujian untuk mengesahkan fungsi yang diinginkan.

3.1. Pembungkus EventBus

Untuk bersama, kami akan menentukan pembungkus EventBus untuk menyediakan beberapa kaedah statik untuk mendaftar dan membatalkan pendaftaran kacang dengan mudah untuk acara yang akan digunakan oleh 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); } }

Kod ini menyediakan kaedah statik untuk mengakses GlobalEventBus dan EventBus yang mendasari serta mendaftarkan dan membatalkan pendaftaran acara dan menghantar acara. Ia juga memiliki ekspresi SpEL yang digunakan sebagai ungkapan lalai dalam anotasi khusus kami untuk menentukan EventBus mana yang ingin kami gunakan.

3.2. Anotasi Penanda Tersuai

Seterusnya, mari tentukan anotasi penanda khas yang akan digunakan oleh BeanPostProcessor untuk mengenal pasti kacang untuk secara automatik mendaftar / membatalkan pendaftaran acara:

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

3.3. Pemprosesan BeanPost

Sekarang, kami akan menentukan BeanPostProcessor yang akan memeriksa setiap kacang untuk anotasi Pelanggan . Kelas ini juga merupakan DestructionAwareBeanPostProcessor, yang merupakan antara muka Spring yang menambah panggilan balik sebelum pemusnahan ke BeanPostProcessor . Sekiranya anotasi ada, kami akan mendaftarkannya dengan EventBus yang dikenal pasti oleh ungkapan SpEL anotasi pada permulaan kacang dan membatalkan pendaftarannya pada pemusnahan kacang:

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

Kod di atas mengambil setiap biji dan menjalankannya melalui kaedah proses , yang ditentukan di bawah. Ia memprosesnya setelah kacang diinisialisasi dan sebelum dimusnahkan. The requiresDestruction kaedah pulangan benar secara lalai dan kami menjaga tingkah laku yang di sini kerana kita menyemak kewujudan @Subscriber anotasi dalam postProcessBeforeDestruction pang.

Sekarang mari kita lihat kaedah proses :

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

Kod ini memeriksa keberadaan anotasi penanda khas kami yang bernama Pelanggan dan, jika ada, membaca ungkapan SpEL dari sifat nilainya . Kemudian, ekspresi dinilai menjadi objek. Sekiranya ini adalah contoh EventBus, kami menerapkan parameter fungsi BiConsumer pada kacang. The BiConsumer digunakan untuk mendaftar dan nyahdaftarkan kacang dari EventBus .

Pelaksanaan kaedah getTargetObject adalah seperti berikut:

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. Objek Model StockTrade

Seterusnya, mari tentukan objek model StockTrade kami :

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

3.5. Penerima Acara StockTradePublisher

Then, let's define a listener class to notify us a trade was received so that we can write our test:

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

Finally, we'll define a receiver for new StockTrade events:

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

The code above marks this class as a Subscriber of Guava EventBus events and Guava's @Subscribe annotation marks the method handleNewStockTradeEvent as a receiver of events. The type of events it'll receive is based on the class of the single parameter to the method; in this case, we'll receive events of type StockTrade.

The @AllowConcurrentEvents annotation allows the concurrent invocation of this method. Once we receive a trade we do any processing we wish then notify any listeners.

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.