Spring ApplicationListener for ContextRefreshEvent. How to invoke only once per Hierarchy?

回眸只為那壹抹淺笑 提交于 2019-12-04 11:38:54

Yes, there is a way but it can be a bit tricky. The child contexts you are talking about are probably contexts started for a DispatcherServlet. If you have more than one of those, you'll get one context per dispatcher servlet.

Spring delegates to the container for these so there is no single point of management with regards to initialization. First, the root application context is initialized and then the various servlets are initialized by the container. For each of those, another context might kick in.

Fortunately, the servlet spec comes to the rescue with the load-on-startup parameter

The load-on-startup element indicates that this servlet should be loaded (instantiated and have its init() called) on the startup of the web application. The optional contents of these element must be an integer indicating the order in which the servlet should be loaded. If the value is a negative integer, or the element is not present, the container is free to load the servlet whenever it chooses. If the value is a positive integer or 0, the container must load and initialize the servlet as the application is deployed. The container must guarantee that servlets marked with lower integers are loaded before servlets marked with higher integers. The container may choose the order of loading of servlets with the same load-on-start-up value.

So you should do two things basically:

  1. Specify a load-on-startup element on each servlet and make sure that one has a distinctive, higher number
  2. Make sure that your listener catch the right event

Example

Consider the following (simplified) web.xml definition

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>  
    <servlet>
        <servlet-name>anotherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/first/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>anotherServlet</servlet-name>
        <url-pattern>/second/*</url-pattern>
    </servlet-mapping>

</web-app>

Detecting the right context

This setup will lead to 3 calls to the listener. In this case, anotherServlet is the last in your chain, so you can identify this as follows:

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    ApplicationContext context = event.getApplicationContext();
    if (context instanceof ConfigurableWebApplicationContext) { // sanity check
        final ConfigurableWebApplicationContext ctx =
                (ConfigurableWebApplicationContext) event.getApplicationContext();
        if ("anotherServlet-servlet".equals(ctx.getNamespace())) {
            // Run your initialization business here
        }
    }
}

If you're interested to understand where this is coming from, have a look to FrameworkServlet#initServletBean.

Not that you can still throw an exception at this point and this will still prevents the application to deploy properly.

Ordering

Finally, you can also make sure that your event is processed last in case there are multiple listeners registered for that particular event. To do this, you need to implement the Ordered interface:

public class YourListener implements ApplicationListener<ContextRefreshedEvent>, Ordered {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) { }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    } 
}

You'll have to be a little more clear about your context hierarchy, but if you have the typical setup of root context and servlet context, declare the ApplicationListener bean in the servlet context.

The root context will be refreshed by the ContextLoaderListener. The servlet context will be refreshed by the DispatcherServlet using the root context as its parent. When this refresh is done, your ApplicationListener will receive the event.

Hmm, in such cases I always use javax.annotation.PostConstruct annotation on the public method of my Spring component.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!