Kenny MacLeod avatar Kenny MacLeod committed a50284a

EVENT-17 - Port EventListenerRegistrar from Stash, removing plugin-specific logic

Comments (0)

Files changed (8)

-aopalliance:aopalliance:1.0:jar:-:compile|org.springframework:spring-context:jar:2.0.8|{sha1}0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8
+aopalliance:aopalliance:1.0:jar:-:compile|org.springframework:spring-context:jar:2.5.6|{sha1}0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8
 com.atlassian.inject:atlassian-inject:1.0.0:jar:-:compile||{sha1}1cdcbe5f00d7dc638671a229c24f1e085dbe6858
 com.atlassian.util.concurrent:atlassian-util-concurrent:0.0.12:jar:-:compile||{sha1}d04c553c71fcd9a72439a085cb91207e52c2da05
-com.google.guava:guava:10.0.1:jar:-:compile||{sha1}292c96f9cb18231528cac4b0bf17d28149d14809
 com.google.code.findbugs:jsr305:1.3.9:jar:-:compile|com.google.guava:guava:jar:10.0.1|{sha1}40719ea6961c0cb6afaeb6a921eaa1f6afd4cfdf
+com.google.guava:guava:10.0.1:jar:-:compile||{sha1}292c96f9cb18231528cac4b0bf17d28149d14809
 commons-lang:commons-lang:2.4:jar:-:compile||{sha1}16313e02a793435009f1e458fa4af5d879f6fb11
-junit:junit:4.7:jar:-:test||{sha1}d9444742a5b897c6280724a49f57a8155517d21f
+commons-logging:commons-logging:1.1.1:jar:-:test|org.springframework:spring-test:jar:2.5.6|{sha1}5043bfebc3db072ed80fbd362e7caf00e885d8ae
+junit:junit:4.4:jar:-:test||{sha1}8f35ee1f35d2dadbb5029991449ee90c1bab4d4a
 org.mockito:mockito-all:1.8.0:jar:-:test||{sha1}f2877727c0eaef7456e830feb51bb643be670547
 org.slf4j:jcl-over-slf4j:1.5.8:jar:-:runtime||{sha1}ab78d75b7ae41770779502cc81ee4bf9c8fac6aa
 org.slf4j:slf4j-api:1.5.8:jar:-:compile||{sha1}2aaf496dfd551adb9b079f58a7e2d55eb383fc82
 org.slf4j:slf4j-nop:1.5.8:jar:-:test||{sha1}2919ed254f2280716f8a5ef66b07f305b65bc3ad
-org.springframework:spring-beans:2.0.8:jar:-:compile|org.springframework:spring-context:jar:2.0.8|{sha1}bb8c708326c5e75ba8e1460dd578273670bdce1a
-org.springframework:spring-context:2.0.8:jar:-:compile||{sha1}2a970102ddebbd40b9dbc5e3abea3131110486d8
-org.springframework:spring-core:2.0.8:jar:-:compile|org.springframework:spring-context:jar:2.0.8|{sha1}c165bab1feab6feeb51ad38872338f72f9c069b8
+org.springframework:spring-beans:2.5.6:jar:-:compile|org.springframework:spring-context:jar:2.5.6|{sha1}449ea46b27426eb846611a90b2fb8b4dcf271191
+org.springframework:spring-context:2.5.6:jar:-:compile||{sha1}983416e612875bdcf877dad4c9d5d77ae37e06ee
+org.springframework:spring-core:2.5.6:jar:-:compile|org.springframework:spring-context:jar:2.5.6|{sha1}c450bc49099430e13d21548d1e3d1a564b7e35e9
+org.springframework:spring-test:2.5.6:jar:-:test||{sha1}b3748144d370fab7ccd530e41dca328f13e85a37
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
-            <version>2.0.8</version>
+            <version>${spring.version}</version>
             <optional>true</optional>
             <exclusions>
                 <exclusion>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
-            <version>4.7</version>
+            <!-- JUnit versions 4.5 and higher don't play nice with Spring 2.5 (see https://jira.springsource.org/browse/SPR-5145)-->
+            <version>4.4</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <version>1.8.0</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <version>${spring.version}</version>
+            <scope>test</scope>
+        </dependency>
         <!-- this for the tests to run with an implementation of slf4j-api, this will > /dev/null all logs -->
         <dependency>
             <groupId>org.slf4j</groupId>
 
     <properties>
         <sl4j.version>1.5.8</sl4j.version>
+        <spring.version>2.5.6</spring.version>
     </properties>
 
     <build>
         <plugins>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <version>2.12</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>integration-test</goal>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
                 <groupId>com.atlassian.maven.plugins</groupId>
                 <artifactId>maven-dependency-tracker-plugin</artifactId>
                 <version>1.0.rc2</version>

src/main/java/com/atlassian/event/spring/EventListenerRegistrar.java

+package com.atlassian.event.spring;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.PreDestroy;
+
+import com.atlassian.event.api.EventPublisher;
+import com.atlassian.event.config.ListenerHandlersConfiguration;
+import com.atlassian.event.spi.ListenerHandler;
+
+import com.google.common.collect.Maps;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.Ordered;
+
+/**
+ * Convenience class that registers/unregisters beans that implement the EventListener interfaces, or has method(s)
+ * annotated with the @EventListener annotation with the EventPublisher on bean creation and bean destruction.
+ * <p/>
+ * EventListenerRegistrar is implemented as a Spring BeanPostProcessor, which means that it gets called whenever a bean
+ * is created or destroyed in the application context. Because we need to get callbacks for all beans that get created,
+ * it is important to inject the minimum of dependencies through CI, as the EventListenerRegistrar can only be created
+ * AFTER all of its dependencies have been created.
+ * <p/>
+ * For this reason, the <em>name</em> of the EventPublisher bean is injected, <em>not</em> the EventPublisher instance
+ * itself. While the EventListenerRegistrar does not have the EventPublisher yet, all beans that should be registered are
+ * stored in the {@code listenersToBeRegistered} map. When the EventListenerRegistrar gets hold of the {@code EventListener},
+ * all beans in {@code listenersToBeRegistered} are registered and the map is cleared.
+ *
+ * @since 2.3.0
+ */
+public class EventListenerRegistrar implements DestructionAwareBeanPostProcessor, BeanFactoryAware, Ordered, ApplicationListener
+{
+
+    private static final Logger LOG = LoggerFactory.getLogger(EventListenerRegistrar.class);
+
+    private final String eventPublisherName;
+    private final ListenerHandlersConfiguration listenerHandlersConfiguration;
+
+    private final Map<String, Object> listenersToBeRegistered = Maps.newHashMap();
+    private ConfigurableBeanFactory beanFactory;
+    private EventPublisher eventPublisher;
+    private boolean ignoreFurtherBeanProcessing;
+
+    public EventListenerRegistrar(String eventPublisherName, ListenerHandlersConfiguration listenerHandlersConfiguration)
+    {
+        this.eventPublisherName = eventPublisherName;
+        this.listenerHandlersConfiguration = listenerHandlersConfiguration;
+    }
+
+    public int getOrder()
+    {
+        // process EventListenerRegistrar as early as possible to guarantee that we don't get passed any AOP-ed proxies.
+        return 1;
+    }
+
+    public void onApplicationEvent(ApplicationEvent applicationEvent)
+    {
+        if (applicationEvent instanceof ContextRefreshedEvent)
+        {
+            ignoreFurtherBeanProcessing = true;
+        }
+    }
+
+    @PreDestroy
+    public void onShutdown()
+    {
+        if (eventPublisher != null)
+        {
+            eventPublisher.unregister(this);
+        }
+    }
+
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
+    {
+        if (beanName.equals(eventPublisherName))
+        {
+            eventPublisher = (EventPublisher) bean;
+
+            for (Object object : listenersToBeRegistered.values())
+            {
+                eventPublisher.register(object);
+            }
+
+            listenersToBeRegistered.clear();
+        }
+        return bean;
+    }
+
+    public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException
+    {
+        if (eventPublisher != null)
+        {
+            eventPublisher.unregister(bean);
+        } else
+        {
+            listenersToBeRegistered.remove(beanName);
+        }
+    }
+
+    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
+    {
+        // Once the spring context has been refreshed the only beans to be processed from that point onwards will be prototypes.
+        // These should not be listening for Events in the first place, and so we can safely ignore them.
+        // The cost of merging is relatively high, which can have a _huge_ impact for large numbers of prototype beans. eg Hibernate Validation
+        if (ignoreFurtherBeanProcessing)
+        {
+            return bean;
+        }
+        BeanDefinition beanDefinition = null;
+        try
+        {
+            if (beanFactory != null)
+            {
+                beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
+            }
+        } catch (NoSuchBeanDefinitionException e)
+        {
+            // no bean with that name; must be an anonymous bean.
+        }
+        if (beanDefinition == null || beanDefinition.isSingleton())
+        {
+            // we only care about singleton beans; the prototype beans typically have a short lifespan
+            registerIfEventListener(beanName, bean);
+        }
+        return bean;
+    }
+
+    public void setBeanFactory(BeanFactory beanFactory) throws BeansException
+    {
+        if (beanFactory instanceof ConfigurableBeanFactory)
+        {
+            this.beanFactory = (ConfigurableBeanFactory) beanFactory;
+        }
+    }
+
+    private void registerIfEventListener(String beanName, Object bean)
+    {
+        for (ListenerHandler handler : listenerHandlersConfiguration.getListenerHandlers())
+        {
+            Collection<?> potentialListenerInvokersForBean = handler.getInvokers(bean);
+            if (!potentialListenerInvokersForBean.isEmpty())
+            {
+                LOG.debug("Registering " + beanName + " instance as an eventlistener");
+                if (eventPublisher != null)
+                {
+                    eventPublisher.register(bean);
+                } else
+                {
+                    listenersToBeRegistered.put(beanName, bean);
+                }
+                break;
+            }
+        }
+    }
+}

src/test/java/it/com/atlassian/event/spring/EventListenerRegistrationIT.java

+package it.com.atlassian.event.spring;
+
+import javax.annotation.Resource;
+
+import com.atlassian.event.api.EventPublisher;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.junit.Assert.assertThat;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class EventListenerRegistrationIT
+{
+    @Resource
+    private ExampleAnnotationBasedEventListener singletonScopedListener;
+
+    @Resource
+    private EventPublisher eventPublisher;
+
+    @After
+    public void resetListeners()
+    {
+        singletonScopedListener.reset();
+    }
+
+    @Test
+    public void testThatEventsArePublishedToListener()
+    {
+        ExampleEvent event = new ExampleEvent();
+        eventPublisher.publish(event);
+        assertThat(event, is(sameInstance(event)));
+    }
+}

src/test/java/it/com/atlassian/event/spring/ExampleAnnotationBasedEventListener.java

+package it.com.atlassian.event.spring;
+
+import com.atlassian.event.api.EventListener;
+
+/**
+ * Example event listener that uses the @EventListener annotation.
+ */
+public class ExampleAnnotationBasedEventListener
+{
+    public ExampleEvent event;
+
+    @EventListener
+    public void onEvent(ExampleEvent event) {
+        this.event = event;
+    }
+
+    public void reset() {
+        this.event = null;
+    }
+
+}

src/test/java/it/com/atlassian/event/spring/ExampleEvent.java

+package it.com.atlassian.event.spring;
+
+public class ExampleEvent
+{
+}

src/test/java/it/com/atlassian/event/spring/ListenerHandlerConfigurationFactoryBean.java

+package it.com.atlassian.event.spring;
+
+import java.util.List;
+
+import com.atlassian.event.config.ListenerHandlersConfiguration;
+import com.atlassian.event.spi.ListenerHandler;
+
+import org.springframework.beans.factory.config.AbstractFactoryBean;
+
+public class ListenerHandlerConfigurationFactoryBean extends AbstractFactoryBean {
+
+    private final List<ListenerHandler> listenerHandlers;
+
+    public ListenerHandlerConfigurationFactoryBean(List<ListenerHandler> listenerHandlers) {
+        this.listenerHandlers = listenerHandlers;
+    }
+
+    @Override
+    public Class<?> getObjectType() {
+        return ListenerHandlersConfiguration.class;
+    }
+
+    @Override
+    protected ListenerHandlersConfiguration createInstance() throws Exception {
+        return new ListenerHandlersConfiguration() {
+            public List<ListenerHandler> getListenerHandlers() {
+                return listenerHandlers;
+            }
+        };
+    }
+}

src/test/resources/it/com/atlassian/event/spring/EventListenerRegistrationIT-context.xml

+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+                           http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+    <bean id="eventExecutorFactory" class="com.atlassian.event.internal.DirectEventExecutorFactory">
+        <constructor-arg>
+            <bean class="com.atlassian.event.internal.EventThreadPoolConfigurationImpl"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="eventDispatcher" class="com.atlassian.event.internal.AsynchronousAbleEventDispatcher">
+        <constructor-arg ref="eventExecutorFactory"/>
+    </bean>
+
+    <bean id="eventListenerHandlersConfiguration" class="it.com.atlassian.event.spring.ListenerHandlerConfigurationFactoryBean">
+        <constructor-arg>
+            <list>
+                <bean class="com.atlassian.event.internal.AnnotatedMethodsListenerHandler"/>
+            </list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="eventPublisher" class="com.atlassian.event.internal.LockFreeEventPublisher">
+        <constructor-arg index="0" ref="eventDispatcher"/>
+        <constructor-arg index="1" ref="eventListenerHandlersConfiguration"/>
+    </bean>
+
+    <bean id="eventListenerRegistrar" class="com.atlassian.event.spring.EventListenerRegistrar">
+        <constructor-arg index="0"><idref bean="eventPublisher"/></constructor-arg>
+        <constructor-arg index="1" ref="eventListenerHandlersConfiguration"/>
+    </bean>
+
+    <bean id="singletonScopedListener" scope="singleton" class="it.com.atlassian.event.spring.ExampleAnnotationBasedEventListener"/>
+
+    <bean id="prototypeScopedListener" scope="prototype" class="it.com.atlassian.event.spring.ExampleAnnotationBasedEventListener"/>
+
+</beans>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.