Skip to content
Permalink
Browse files

#10 - CompletionRegisteringBeanPostProcessor now only handles after c…

…ommit event listeners.
  • Loading branch information
odrotbohm committed Oct 25, 2019
1 parent 521d084 commit d485795ffa66327efe21c750584addcee4e2397f
@@ -104,6 +104,12 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,8 +15,6 @@
*/
package org.springframework.events.config;

import java.util.function.Supplier;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
@@ -27,22 +25,20 @@
import org.springframework.events.support.PersistentApplicationEventMulticaster;

/**
* @author Oliver Gierke
* @author Oliver Drotbohm
*/
@Configuration(proxyBeanMethods = false)
class EventPublicationConfiguration {

@Bean
PersistentApplicationEventMulticaster applicationEventMulticaster(ObjectProvider<EventPublicationRegistry> registry) {

Supplier<EventPublicationRegistry> supplier = () -> registry
.getIfAvailable(() -> new MapEventPublicationRegistry());

return new PersistentApplicationEventMulticaster(supplier);
return new PersistentApplicationEventMulticaster(
() -> registry.getIfAvailable(() -> new MapEventPublicationRegistry()));
}

@Bean
static CompletionRegisteringBeanPostProcessor bpp(ObjectFactory<EventPublicationRegistry> store) {
return new CompletionRegisteringBeanPostProcessor(store);
return new CompletionRegisteringBeanPostProcessor(() -> store.getObject());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,32 +20,41 @@
import lombok.RequiredArgsConstructor;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.function.Supplier;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.events.EventPublicationRegistry;
import org.springframework.events.PublicationTargetIdentifier;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;

/**
* @author Oliver Gierke
* {@link BeanPostProcessor} that will add a
* {@link org.springframework.events.support.CompletionRegisteringBeanPostProcessor.ProxyCreatingMethodCallback.CompletionRegisteringMethodInterceptor}
* to the bean in case it carries a {@link TransactionalEventListener} annotation so that the successful invocation of
* those methods mark the event publication to those listeners as completed.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor {

private final @NonNull ObjectFactory<EventPublicationRegistry> store;
private final @NonNull Supplier<EventPublicationRegistry> store;

/*
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String)
*/
@@ -54,7 +63,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro
return bean;
}

/*
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
*/
@@ -63,38 +72,49 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw

ProxyCreatingMethodCallback callback = new ProxyCreatingMethodCallback(store, bean);

ReflectionUtils.doWithMethods(bean.getClass(), callback);
ReflectionUtils.doWithMethods(AopProxyUtils.ultimateTargetClass(bean), callback);

return callback.getBean() == null ? bean : callback.getBean();
return callback.methodFound ? callback.getBean() : bean;

}

/**
* Method callback to find a {@link TransactionalEventListener} method and creating a proxy including an
* {@link CompletionRegisteringBeanPostProcessor} for it or adding the latter to the already existing advisor chain.
*
* @author Oliver Drotbohm
*/
private static class ProxyCreatingMethodCallback implements MethodCallback {

private final CompletionRegisteringMethodInterceptor interceptor;

private @Getter Object bean;
private boolean methodFound = false;

public ProxyCreatingMethodCallback(ObjectFactory<EventPublicationRegistry> registry, Object bean) {
/**
* Creates a new {@link ProxyCreatingMethodCallback} for the given {@link EventPublicationRegistry} and bean
* instance.
*
* @param registry must not be {@literal null}.
* @param bean must not be {@literal null}.
*/
public ProxyCreatingMethodCallback(Supplier<EventPublicationRegistry> registry, Object bean) {

Assert.notNull(registry, "EventPublicationRegistry must not be null!");
Assert.notNull(bean, "Bean must not be null!");

this.bean = bean;
this.interceptor = new CompletionRegisteringMethodInterceptor(registry);
}

/*
* (non-Javadoc)
* @see org.springframework.util.ReflectionUtils.MethodCallback#doWith(java.lang.reflect.Method)
*/
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {

if (methodFound) {
return;
}

TransactionalEventListener listener = AnnotatedElementUtils.findMergedAnnotation(method,
TransactionalEventListener.class);

if (listener == null) {
if (methodFound || !CompletionRegisteringMethodInterceptor.isCompletingMethod(method)) {
return;
}

@@ -120,34 +140,68 @@ private Object createCompletionRegisteringProxy(Object bean) {
return factory.getProxy();
}

/**
* {@link MethodInterceptor} to trigger the completion of an event publication after a transaction event listener
* method has been completed successfully.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
private static class CompletionRegisteringMethodInterceptor implements MethodInterceptor, Ordered {

private final @NonNull ObjectFactory<EventPublicationRegistry> registry;
private static final Map<Method, Boolean> COMPLETING_METHOD = new ConcurrentReferenceHashMap<>();

/*
private final @NonNull Supplier<EventPublicationRegistry> registry;

/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {

Object result = invocation.proceed();
Method method = invocation.getMethod();

// Mark publication complete if the method is a transactional event listener.
if (!isCompletingMethod(method)) {
return result;
}

registry.getObject().markCompleted(invocation.getArguments()[0],
PublicationTargetIdentifier.forMethod(invocation.getMethod()));
PublicationTargetIdentifier identifier = PublicationTargetIdentifier.forMethod(method);
registry.get().markCompleted(invocation.getArguments()[0], identifier);

return result;
}

/*
/*
* (non-Javadoc)
* @see org.springframework.core.Ordered#getOrder()
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE - 10;
}

/**
* Returns whether the given method is one that requires publication completion.
*
* @param method must not be {@literal null}.
* @return
*/
static boolean isCompletingMethod(Method method) {

Assert.notNull(method, "Method must not be null!");

return COMPLETING_METHOD.computeIfAbsent(method, it -> {

TransactionalEventListener annotation = AnnotatedElementUtils.getMergedAnnotation(method,
TransactionalEventListener.class);

return annotation == null ? false : annotation.phase().equals(TransactionPhase.AFTER_COMMIT);
});
}
}

}
}
@@ -0,0 +1,105 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.events.support;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.util.function.BiConsumer;

import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.event.EventListener;
import org.springframework.events.EventPublicationRegistry;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

/**
* Unit tests for {@link CompletionRegisteringBeanPostProcessor}.
*
* @author Oliver Drotbohm
*/
class CompletionRegisteringBeanPostProcessorUnitTest {

EventPublicationRegistry registry = mock(EventPublicationRegistry.class);
BeanPostProcessor processor = new CompletionRegisteringBeanPostProcessor(() -> registry);
SomeEventListener bean = new SomeEventListener();

@Test // #10
void doesNotProxyNonTransactionalEventListenerClass() {

NoEventListener bean = new NoEventListener();

assertThat(bean).isSameAs(processor.postProcessBeforeInitialization(bean, "bean"));
}

@Test // #10
void triggersCompletionForAfterCommitEventListener() throws Exception {
assertCompletion(SomeEventListener::onAfterCommit);
}

@Test // #10
void doesNotTriggerCompletionForNonAfterCommitPhase() throws Exception {
assertNonCompletion(SomeEventListener::onAfterRollback);
}

@Test // #10
void doesNotTriggerCompletionForPlainEventListener() {
assertNonCompletion(SomeEventListener::simpleEventListener);
}

@Test // #10
void doesNotTriggerCompletionForNonEventListener() {
assertNonCompletion(SomeEventListener::nonEventListener);
}

private void assertCompletion(BiConsumer<SomeEventListener, Object> consumer) {
assertCompletion(consumer, true);
}

private void assertNonCompletion(BiConsumer<SomeEventListener, Object> consumer) {
assertCompletion(consumer, false);
}

private void assertCompletion(BiConsumer<SomeEventListener, Object> consumer, boolean expected) {

Object processed = processor.postProcessAfterInitialization(bean, "listener");

assertThat(processed).isInstanceOf(Advised.class);
assertThat(processed).isInstanceOfSatisfying(SomeEventListener.class, //
it -> consumer.accept(it, new Object()));

verify(registry, times(expected ? 1 : 0)).markCompleted(any(), any());
}

static class SomeEventListener {

@TransactionalEventListener
void onAfterCommit(Object event) {}

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
void onAfterRollback(Object object) {}

@EventListener
void simpleEventListener(Object object) {}

void nonEventListener(Object object) {}
}

static class NoEventListener {}
}

0 comments on commit d485795

Please sign in to comment.
You can’t perform that action at this time.