Understand the Spring Web zero xml configuration principle and parent-child container relationship this time

Keywords: Spring xml Tomcat Java

preface

When using the old versions of Spring and Spring MVC for development, we need to configure a lot of XML files, which is very cumbersome. It is also very bad to let users choose their own configuration. Based on the stipulation that the Convention is greater than the configuration, Spring provides many annotations to help us simplify a large number of XML configurations; however, when using Spring MVC, we will also use WEB-INF/web.xml But in fact, we can use Java classes instead of XML configuration, which is the implementation principle of SpringBoott later. This article will take a look at how Spring implements a complete zero XML configuration.

text

Let's take a look at the original web.xml to configure:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      <!--load spring to configure-->
      classpath:spring.xml
    </param-value>
  </context-param>
  <context-param>
    <param-name>webAppRootKey</param-name>
    <param-value>ServicePlatform.root</param-value>
  </context-param>

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

  <servlet>
    <servlet-name>spring-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <!--springmvc Profile for-->
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-dispatcher.xml</param-value>
    </init-param>
    <load-on-startup>0</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring-dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

The function of each configuration here is simply to say that context param is to load our main sping.xml Configuration, such as the configuration of some bean s and the opening of annotation scanning, etc.; listener is to configure the listener, which will trigger the listener call when Tomcat is started; servlet is to configure our customized servlet implementation, such as dispatcher servlet. There are many other configurations that will not be explained one by one. Here, we mainly see context param and servlet configuration, which is the embodiment of spring IOC parent-child container. In the previous I article, I said that IOC containers are organized by parent-child relationship, but it is estimated that most people can't understand it. Except for the complex inheritance system, we didn't see the role of parent containers. Later, we will analyze it.
Knowing the configuration, we need to think about how to replace these cumbersome configurations. In fact, Tomcat provides a specification with a ServletContainerInitializer interface:

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
}

When Tomcat starts, it will call the onStartup method of the interface implementation class. This method has two parameters. Needless to say, what is the first parameter? Where do you come from? In addition, how can our custom implementation class make Tomcat call?
First of all, we answer the last question. Here we also use SPI to implement this interface. Therefore, after we implement this interface, we need to use META-INF.services Lower configuration. Secondly, the first parameter passed in here is also the implementation class of our custom extension interface. We can implement many things that need to be done at startup through our custom interface, such as loading the Servlet. But how does Tomcat know which interface our custom interface is? This requires @ HandlesTypes annotation, which is annotated on the implementation class of ServletContainerInitializer. Its value is our extended interface, so Tomcat will know which interface implementation class to pass to the onStartup method. Let's look at a simple implementation:

@HandlesTypes(LoadServlet.class)
public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        Iterator var4;
        if (set != null) {
            var4 = set.iterator();
            while (var4.hasNext()) {
                Class<?> clazz = (Class<?>) var4.next();
                if (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && LoadServlet.class.isAssignableFrom(clazz)) {
                    try {
                        ((LoadServlet) clazz.newInstance()).loadOnstarp(servletContext);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public interface LoadServlet {

    void loadOnstarp(ServletContext servletContext);
}

public class LoadServletImpl implements LoadServlet {
    @Override
    public void loadOnstarp(ServletContext servletContext) {
        ServletRegistration.Dynamic initServlet = servletContext.addServlet("initServlet", "org.springframework.web.servlet.DispatcherServlet");
        initServlet.setLoadOnStartup(1);
        initServlet.addMapping("/init");
	}
}

This is the specification provided by Tomcat. Through this specification, we can realize Spring's zero xml configuration startup and directly see how Spring does it.
According to the above, we can find meta-inf / services under spring web project/ javax.servlet.ServletContainerInitializer to configure:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

The core implementation is WebApplicationInitializer. Let's take a look at its inheritance system

AbstractReactiveWebInitializer doesn't matter. It mainly looks at the other side, but it's all abstract classes. That is to say, real instances are also implemented by ourselves, but what do we need to implement? Generally, we directly inherit the AbstractAnnotationConfigDispatcherServletInitializer class. There are four abstract methods that we need to implement:

    //Parent container
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{SpringContainer.class};
    }

    //Spring MVC configuration sub container
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{MvcContainer.class};
    }

    //Get the mapping information of dispatcher Servlet
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

	// filter configuration
    @Override
    protected Filter[] getServletFilters() {
        MyFilter myFilter = new MyFilter();
        CorsFilter corsFilter = new CorsFilter();
        return new Filter[]{myFilter,corsFilter};
    }

Here, we mainly pay attention to the getRootConfigClasses and getServletConfigClasses methods to load the parent and child containers respectively:

@ComponentScan(value = "com.dark",excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
})
public class SpringContainer {
}

@ComponentScan(value = "com.dark",includeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
},useDefaultFilters = false)
public class MvcContainer {
}

It's not strange to see the annotation on these two classes. The parent container scans and loads all classes without @ Controller annotation, while the child container does the opposite. But when you need an object, first find it from the current container. If not, get it from the parent container. Why do you design this way? Can't you just put it in a container? Think about it first and answer later.
Back in the onStartup method, call the AbstractDispatcherServletInitializer class directly:

	public void onStartup(ServletContext servletContext) throws ServletException {
		super.onStartup(servletContext);
		//Register dispatcher Servlet
		registerDispatcherServlet(servletContext);
	}

First, call the parent class:

	public void onStartup(ServletContext servletContext) throws ServletException {
		registerContextLoaderListener(servletContext);
	}

	protected void registerContextLoaderListener(ServletContext servletContext) {

		//Create the spring context and register the spring container
		WebApplicationContext rootAppContext = createRootApplicationContext();
		if (rootAppContext != null) {
			//Create listener
			ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
			listener.setContextInitializers(getRootApplicationContextInitializers());
			servletContext.addListener(listener);
		}
	}

Then call createRootApplicationContext to create the parent container:

	protected WebApplicationContext createRootApplicationContext() {
		Class<?>[] configClasses = getRootConfigClasses();
		if (!ObjectUtils.isEmpty(configClasses)) {
			AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
			context.register(configClasses);
			return context;
		}
		else {
			return null;
		}
	}

As you can see, we created an AnnotationConfigWebApplicationContext object and registered our configuration class SpringContainer. Then create Tomcat to start the load listener ContextLoaderListener, which has a contextInitialized method that will be called when Tomcat starts.

	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

	 */
	public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
		long startTime = System.currentTimeMillis();
		try {
			// Store context in local instance variable, to guarantee that
			// it is available on ServletContext shutdown.
			if (this.context == null) {
				this.context = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
				if (!cwac.isActive()) {
					// The context has not yet been refreshed -> provide services such as
					// setting the parent context, setting the application context id, etc
					if (cwac.getParent() == null) {
						// The context instance was injected without an explicit parent ->
						// determine parent for root web application context, if any.
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

			ClassLoader ccl = Thread.currentThread().getContextClassLoader();
			if (ccl == ContextLoader.class.getClassLoader()) {
				currentContext = this.context;
			}
			else if (ccl != null) {
				currentContextPerThread.put(ccl, this.context);
			}

			return this.context;
		}
	}

You can see that it is to initialize the container, which is the same as the previous analysis of xml parsing. The main attention here is to encapsulate the ServletContext object and set the parent container to the object.
After the parent container is created, it is naturally the creation of the child container. Go to the registerDispatcherServlet method:

	protected void registerDispatcherServlet(ServletContext servletContext) {
		String servletName = getServletName();
		Assert.hasLength(servletName, "getServletName() must not return null or empty");

		//Create the context of springmvc and register the MvcContainer class
		WebApplicationContext servletAppContext = createServletApplicationContext();
		Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

		//Create dispatcher Servlet
		FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
		Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
		dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

		ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
		if (registration == null) {
			throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
					"Check if there is another servlet registered under the same name.");
		}

		/*
		* If the value of the element is negative or not set, the container will reload when the Servlet is requested.
			If the value is a positive integer or 0, the servlet will be loaded and initialized when the application starts,
			The smaller the value, the higher the priority of the servlet, the earlier it is loaded
		* */
		registration.setLoadOnStartup(1);
		registration.addMapping(getServletMappings());
		registration.setAsyncSupported(isAsyncSupported());

		Filter[] filters = getServletFilters();
		if (!ObjectUtils.isEmpty(filters)) {
			for (Filter filter : filters) {
				registerServletFilter(servletContext, filter);
			}
		}

		customizeRegistration(registration);
	}

	protected WebApplicationContext createServletApplicationContext() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		Class<?>[] configClasses = getServletConfigClasses();
		if (!ObjectUtils.isEmpty(configClasses)) {
			context.register(configClasses);
		}
		return context;
	}

An AnnotationConfigWebApplicationContext object is also created here. The difference is that the configuration class registered here is our Servlet configuration. The dispatcher Servlet object is then created and the context object is set in. Seeing this, you may wonder, since the parent-child container creates objects of the same class, what's the theory of the parent-child container? Don't worry, this will be clear when initializing the above. But where is the initialization entry here? You don't see the creation and invocation of any listeners. In fact, the context object initialization here is implemented when the Servlet is initialized, that is, the init method. Go directly to the init method of HttpServletBean (mentioned when analyzing the spring MVC source code):

	public final void init() throws ServletException {
		...ellipsis
		
		// Let subclasses do whatever initialization they like.
		initServletBean();
	}

	protected final void initServletBean() throws ServletException {
		try {
			this.webApplicationContext = initWebApplicationContext();
			initFrameworkServlet();
		}
	}

	protected WebApplicationContext initWebApplicationContext() {
		//The parent container will be obtained from the servletContext, that is, the container loaded through the listener
		WebApplicationContext rootContext =
				WebApplicationContextUtils.getWebApplicationContext(getServletContext());
		WebApplicationContext wac = null;

		if (this.webApplicationContext != null) {
			// A context instance was injected at construction time -> use it
			wac = this.webApplicationContext;
			if (wac instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
				if (!cwac.isActive()) {
					if (cwac.getParent() == null) {
						cwac.setParent(rootContext);
					}
					//Container loading
					configureAndRefreshWebApplicationContext(cwac);
				}
			}
		}
		if (wac == null) {
			wac = findWebApplicationContext();
		}
		if (wac == null) {
			wac = createWebApplicationContext(rootContext);
		}

		if (!this.refreshEventReceived) {
			synchronized (this.onRefreshMonitor) {
				onRefresh(wac);
			}
		}

		if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
		}

		return wac;
	}

You should also understand here. First, get the parent container from the ServletContext, and then set it to the parent of the current container to realize the organization of the parent-child container. I think the benefits of this design are very clear. At present, the child containers are all loaded with MVC configuration and beans. Simply speaking, the Controller, the parent container is all Service, and the Controller depends on Service If you don't build such a hierarchical relationship and instantiate the parent container first, how can you realize the successful dependency injection of the Controller layer?

summary

This article analyzes the implementation principle of spring MVC zero XML configuration in combination with the previous articles, and also supplements the parent-child container relationship that has not been analyzed before, so that we can understand the implementation principle of spring IOC in a more comprehensive way in detail. I believe you will have your own ideas after reading the implementation of spring boot.

Posted by croix on Fri, 19 Jun 2020 23:54:06 -0700