Javaweb 从 web.xml 到 springboot 前言 这篇文章依然是参考了这位大佬的文章 ,讲得已经非常细了,真的非常建议去看下大佬的文章。
几乎所有人都是从 servlet,jsp,filter 开始编写自己的第一个 hello world 工程。那时,还离不开 web.xml 的配置,在 xml 文件中编写繁琐的 servlet 和 filter 的配置。随着 spring 的普及,配置逐渐演变成了两种方式—java configuration 和 xml 配置共存。现如今,springboot 的普及,java configuration 成了主流,xml 配置似乎已经“灭绝”了。不知道你有没有好奇过,这中间都发生了哪些改变,web.xml 中的配置项又是被什么替代项取代了?
servlet3.0 以前的时代(使用 web.xml 配置) 项目的结构和部分代码
1 2 3 4 5 6 7 8 9 public class HelloWorldServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/plain" ); PrintWriter out = resp.getWriter(); out.println("hello world" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HelloWorldFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("触发 hello world 过滤器..." ); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { } }
在 servlet3.0 之前需要将需要上面的 HelloWorldServlet 和 HelloWorldFilter 配置在WEB-INF/web.xml
文件里面才会生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="UTF-8" ?> <web-app > <display-name > Archetype Created Web Application</display-name > <servlet > <servlet-name > HelloWorldServlet</servlet-name > <servlet-class > top.ouzhanbo.webxml.servlet.HelloWorldServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > HelloWorldServlet</servlet-name > <url-pattern > /hello</url-pattern > </servlet-mapping > <filter > <filter-name > HelloWorldFilter</filter-name > <filter-class > top.ouzhanbo.webxml.filter.HelloWorldFilter</filter-class > </filter > <filter-mapping > <filter-name > HelloWorldFilter</filter-name > <url-pattern > /hello</url-pattern > </filter-mapping > </web-app >
servlet3.0 新特性 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中一项新特性便是提供了无 xml 配置的特性。
servlet3.0 首先提供了 @WebServlet,@WebFilter 等注解,这样便有了抛弃 web.xml 的第一个途径,凭借注解声明 servlet 和 filter 来做到这一点。
除了这种方式,servlet3.0 规范还提供了更强大的功能,可以在运行时动态注册 servlet ,filter,listener。以 servlet 为例,过滤器与监听器与之类似。ServletContext 为动态配置 Servlet 增加了如下方法:
ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass) ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) ServletRegistration.Dynamic addServlet(String servletName, String className) T createServlet(Class clazz) ServletRegistration getServletRegistration(String servletName) Map getServletRegistrations() 其中前三个方法的作用是相同的,只是参数类型不同而已;通过 createServlet()方法创建的 Servlet,通常需要做一些自定义的配置,然后使用 addServlet() 方法来将其动态注册为一个可以用于服务的 Servlet。两个 getServletRegistration() 方法主要用于动态为 Servlet 增加映射信息,这等价于在 web.xml 中使用 标签为存在的 Servlet 增加映射信息。上面的 ServletRegistration.Dynamic 看源码发现其实他是继承了 ServletRegistration(FilterRegistration 也是类似)
1 2 3 4 5 6 7 8 9 10 public interface ServletRegistration extends Registration { public static interface Dynamic extends ServletRegistration , Registration.Dynamic { public void setLoadOnStartup (int loadOnStartup) ; public Set<String> setServletSecurity (ServletSecurityElement constraint) ; public void setMultipartConfig (MultipartConfigElement multipartConfig) ; public void setRunAsRole (String roleName) ; } }
以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contexInitialized 方法中调用,要么是在 ServletContainerInitializer 的 onStartup() 方法中调用。
ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup()方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给 onStartup() 处理的类。
servlet3.0+的项目结构和部分代码
我并未对 HelloWorldServlet 和 HelloWorldFilter 做任何改动,而是新增了一个 CustomServletContainerInitializer , 它实现了 javax.servlet.ServletContainerInitializer
接口,用来在 web 容器启动时加载指定的 servlet 和 filter,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class CustomServletContainerInitializer implements ServletContainerInitializer { private final static String JAR_HELLO_URL = "/hello" ; @Override public void onStartup (Set<Class<?>> c, ServletContext servletContext) throws ServletException{ System.out.println("创建 helloWorldServlet..." ); ServletRegistration.Dynamic servlet = servletContext.addServlet( HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class); servlet.addMapping(JAR_HELLO_URL); System.out.println("创建 helloWorldFilter..." ); FilterRegistration.Dynamic filter = servletContext.addFilter( HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class); EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class); dispatcherTypes.add(DispatcherType.REQUEST); dispatcherTypes.add(DispatcherType.FORWARD); filter.addMappingForUrlPatterns(dispatcherTypes, true , JAR_HELLO_URL); } }
ServletContext 我们称之为 servlet 上下文,它维护了整个 web 容器中注册的 servlet,filter,listener,以 servlet 为例,可以使用 servletContext.addServlet 等方法来添加 servlet。在上面没用到方法入参中Set<Class<?>> c
和注解@HandlesTypes
,这里说下这两个东西的作用,@HandlesTypes
注解上可以指定需要处理的类,onStartup 方法被调用时将@HandlesTypes
中指定类的子类(一定记住是子类,该类的本身不算)放入Set<Class<?>> c
这个集合中,如果没有使用@HandlesTypes
注解指定处理的类,那Set<Class<?>> c
就为null
这么声明一个 ServletContainerInitializer 的实现类,web 容器并不会识别它,所以,需要借助 SPI 机制(如果不懂 SPI 的可以看看这篇文章 )来指定该初始化类,这一步骤是通过在项目路径下创建 META-INF/services/javax.servlet.ServletContainerInitializer
来做到的,它只包含一行内容:
1 top.ouzhanbo.websci.CustomServletContainerInitializer
使用 ServletContainerInitializer 和 SPI 机制,我们的 web 应用便可以彻底摆脱 web.xml 了。
在 ContextConfig 类中可以看到通过 SPI 加载 ServletContainerInitializer 的实现类的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 List<ServletContainerInitializer> detectedScis; try { WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader <>(context); detectedScis = loader.load(ServletContainerInitializer.class); } catch (IOException e) { log.error(sm.getString( "contextConfig.servletContainerInitializerFail" , context.getName()), e); ok = false ; return ; }
在 StandardContext 类中可以看到 ServletContainerInitializer 的 onStartup 方法被调用的代码
1 2 3 4 5 6 7 8 9 10 11 12 for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) { try { entry.getKey().onStartup(entry.getValue(), getServletContext()); } catch (ServletException e) { log.error(sm.getString("standardContext.sciFail" ), e); ok = false ; break ; } }
Spring 是如何支持 servlet3.0 的? 在我最初接触 spring 的时候虽然已经有 servlet3.0 但是那时候都是在网上找一些视频来学习的,基本都是学到能用就觉得可以了,当时学的 springmvc+spring+mybatis 里面的 spirngmvc 和 spring 的配置都是用 xml 的,但是当时其实不是很懂 web.xml 中配的那个 listener 和 servlet 标签里面的那个初始化参数标签 init-param 里面那个东西有什么用,后来 springboot 出来后基本就用 springboot 了基本没在用过 ssm 那一套了,所以 spring 搭配 servlet3.0 那套无 xml 配置的方式我完全没用过,看了这位大佬的文章 加上百度再搜索了一波资料加上自己调试,现在大概弄懂了用 xml 和用 javaConfig 这两种方式浅显的原理。
使用 xml 配置的方式 先看看项目的结构和部分的代码
代码和配置如下
web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns ="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation ="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id ="WebApp_ID" version ="3.0" > <display-name > Archetype Created Web Application</display-name > <context-param > <param-name > contextConfigLocation</param-name > <param-value > classpath*:/application.xml</param-value > </context-param > <listener > <listener-class > org.springframework.web.context.ContextLoaderListener</listener-class > </listener > <servlet > <servlet-name > dispatcherServlet</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath*:/application-mvc.xml</param-value > </init-param > <load-on-startup > 1</load-on-startup > </servlet > <servlet-mapping > <servlet-name > dispatcherServlet</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > </web-app >
Spring 的配置 application.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd" > <context:component-scan base-package ="top.ouzhanbo.webspring" > <context:exclude-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan > </beans >
SpringMvc 的配置 application-mvc.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd" > <context:component-scan base-package ="top.ouzhanbo.webspring" > <context:include-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> <context:exclude-filter type ="annotation" expression ="org.springframework.stereotype.Service" /> </context:component-scan > </beans >
MyController 的代码
1 2 3 4 5 6 7 8 9 10 11 12 @Controller public class MyController { @Resource ApplicationContext applicationContext; @RequestMapping(value="/hello") @ResponseBody public String hello () { return "hello" ; } }
我们在 web.xml 上面配置的监听器 ContextLoaderListener 继承 ContextLoader 类并实现了 ServletContextListener 接口,实现了 ServletContextListener 接口的监听器会监听 servelet 容器,在 servelet 容器启动和关闭时会触发该监听器
ServletContextListener
1 2 3 4 5 6 7 8 public interface ServletContextListener extends EventListener { default public void contextInitialized (ServletContextEvent sce) {} default public void contextDestroyed (ServletContextEvent sce) {} }
ContextLoaderListener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ContextLoaderListener extends ContextLoader implements ServletContextListener { public ContextLoaderListener () { } public ContextLoaderListener (WebApplicationContext context) { super (context); } @Override public void contextInitialized (ServletContextEvent event) { initWebApplicationContext(event.getServletContext()); } @Override public void contextDestroyed (ServletContextEvent event) { closeWebApplicationContext(event.getServletContext()); ContextCleanupListener.cleanupAttributes(event.getServletContext()); } }
查看 Tomcat 源码,在 StandardContext 里面的 listenerStart 方法里面有段代码就是获取 ServletContextListener 接口的实现类并且调用它的 contextInitialized 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Object instances[] = getApplicationLifecycleListeners(); if (instances == null || instances.length == 0 ) { return ok; } ServletContextEvent event = new ServletContextEvent (getServletContext()); ServletContextEvent tldEvent = null ; if (noPluggabilityListeners.size() > 0 ) { noPluggabilityServletContext = new NoPluggabilityServletContext (getServletContext()); tldEvent = new ServletContextEvent (noPluggabilityServletContext); } for (Object instance : instances) { if (!(instance instanceof ServletContextListener)) { continue ; } ServletContextListener listener = (ServletContextListener) instance; try { fireContainerEvent("beforeContextInitialized" , listener); if (noPluggabilityListeners.contains(listener)) { listener.contextInitialized(tldEvent); } else { listener.contextInitialized(event); } fireContainerEvent("afterContextInitialized" , listener); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); fireContainerEvent("afterContextInitialized" , listener); getLogger().error(sm.getString("standardContext.listenerStart" , instance.getClass().getName()), t); ok = false ; } }
之后就是调用 ContextLoader 的 initWebApplicationContext 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public WebApplicationContext initWebApplicationContext (ServletContext servletContext) { if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null ) { throw new IllegalStateException ( "Cannot initialize context because there is already a root application context present - " + "check whether you have multiple ContextLoader* definitions in your web.xml!" ); } servletContext.log("Initializing Spring root WebApplicationContext" ); Log logger = LogFactory.getLog(ContextLoader.class); if (logger.isInfoEnabled()) { logger.info("Root WebApplicationContext: initialization started" ); } long startTime = System.currentTimeMillis(); try { if (this .context == null ) { this .context = createWebApplicationContext(servletContext); } if (this .context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this .context; if (!cwac.isActive()) { if (cwac.getParent() == null ) { 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); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms" ); } return this .context; } catch (RuntimeException | Error ex) { logger.error("Context initialization failed" , ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } }
configureAndRefreshWebApplicationContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class ContextLoader { public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation" ; protected void configureAndRefreshWebApplicationContext (ConfigurableWebApplicationContext wac, ServletContext sc) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null ) { wac.setId(idParam); } else { wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } wac.setServletContext(sc); String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (configLocationParam != null ) { wac.setConfigLocation(configLocationParam); } ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(sc, null ); } customizeContext(sc, wac); wac.refresh(); }
在 refresh 方法里面的层层深入后 XmlWebApplicationContext 这个类的 loadBeanDefinitions 方法里面看到之前保存的 spring 容器的配置文件路径
之后就是 spring 的加载配置文件的源码了,这里就不展开了(我也没怎么看过),讲那么多其实最关键的就是知道 Spring 容器是通过在 Servlet 容器初始化的时候调用监听器 ContextLoaderListener 的 contextInitialized 方法加载到 Servlet 容器中的。
那么 springmvc 的容器是在哪里加载进来的呢?其实不难猜到就在 DispatcherServlet 初始化的时候加载进来的,在 StandardWrapper 里的 loadServlet 方法里通过反射的方式创建了 servlet 实例并且将其返回给了上层的 load 方法,并且在上层的 load 方法中通过调用 initServlet 方法最终调用到 servlet 的 init 方法。
如果 servlet 是 FrameworkServlet 类或者其子类(DispatcherServlet 继承 FrameworkServlet)的实例那么层层深入后最终会调用到 FrameworkServlet 的 initWebApplicationContext 方法
initWebApplicationContext 方法的的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 protected WebApplicationContext initWebApplicationContext () { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null ; if (this .webApplicationContext != null ) { wac = this .webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { if (cwac.getParent() == null ) { cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null ) { wac = findWebApplicationContext(); } if (wac == null ) { wac = createWebApplicationContext(rootContext); } if (!this .refreshEventReceived) { synchronized (this .onRefreshMonitor) { onRefresh(wac); } } if (this .publishContext) { String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); } return wac; }
initWebApplicationContext 会调用到 createWebApplicationContext,最会调用到 createWebApplicationContext(@Nullable ApplicationContext parent) 方法
createWebApplicationContext(@Nullable ApplicationContext parent) 方法的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 protected WebApplicationContext createWebApplicationContext (@Nullable ApplicationContext parent) { Class<?> contextClass = getContextClass(); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException ( "Fatal initialization error in servlet with name '" + getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext" ); } ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); wac.setParent(parent); String configLocation = getContextConfigLocation(); if (configLocation != null ) { wac.setConfigLocation(configLocation); } configureAndRefreshWebApplicationContext(wac); return wac; }
上面 contextClass 和 contextConfigLocation 的值都是在 HttpServletBean(FrameworkServlet 的父类)的 init 方法中被赋予
最终将 springmvc 的容器保存在 servlet 容器中,这样 springmvc 就成功的加载进来了
完全无 xml 的配置方式 项目的结构和部分的代码
MyConfig 配置类的效果和上面用 xml 的方式时的 application.xml 类似,主要的针对 Spring 容器的,上面的@ComponentScan 注解的内容是告诉 Spring 容器扫描并且加载 top.ouzhanbo.webspring 下的所有被@Service 注解标注的类,useDefaultFilters = false 是为了防止 spring 默认会自动发现被 @Component、@Repository、@Service 和 @Controller 标注的类,并注册进容器中这行为。
1 2 3 4 5 @Configuration @ComponentScan(value = "top.ouzhanbo.webspring",includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Service.class})},useDefaultFilters = false) public class MyConfig {}
MyMvcConfig 配置类和上面的 MyConfig 配置类差不多,只是这个是针对 SpringMvc 容器的
1 2 3 4 5 @Configuration @ComponentScan(value = "top.ouzhanbo.webspring",includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})},useDefaultFilters = false) public class MyMvcConfig {}
MyController 的代码很简单,就是返回一个 hello 字符串
1 2 3 4 5 6 7 8 9 10 @Controller public class MyController { @RequestMapping(value="/hello") @ResponseBody public String hello () { return "hello" ; } }
WebInitializer 这个类这里先不介绍后面再说
前面介绍的 servlet3.0 下是如何通过 SPI 的方式注册 Servlet、Filter、Listener 的,其实在无 xml 配置的情况下 Spring 也是通过这种方式来注册 Servlet、Filter、Listener,找到 ServletContainerInitializer 的实现类 SpringServletContainerInitializer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup (@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List<WebApplicationInitializer> initializers = Collections.emptyList(); if (webAppInitializerClasses != null ) { initializers = new ArrayList <>(webAppInitializerClasses.size()); for (Class<?> waiClass : webAppInitializerClasses) { 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); } } }
SpringServletContainerInitializer 的 onStartup 方法的作用就是找到所有 WebApplicationInitializer 类的子类(除了接口和抽象类)并且调用它们的 onStartup 方法(套娃吖),而我们上面的 WebInitializer 就是 WebApplicationInitializer 的子类
WebInitializer 的继承关系
WebInitializer 的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class []{MyConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class []{MyMvcConfig.class}; } @Override protected String[] getServletMappings() { return new String []{"/" }; } }
WebInitializer 的 onStartup 的方法在它的父类 AbstractDispatcherServletInitializer 中实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer { public static final String DEFAULT_SERVLET_NAME = "dispatcher" ; @Override public void onStartup (ServletContext servletContext) throws ServletException { super .onStartup(servletContext); registerDispatcherServlet(servletContext); } protected void registerDispatcherServlet (ServletContext servletContext) { String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return null or empty" ); WebApplicationContext servletAppContext = createServletApplicationContext(); Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null" ); 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." ); } 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 String getServletName () { return DEFAULT_SERVLET_NAME; } protected abstract WebApplicationContext createServletApplicationContext () ; protected FrameworkServlet createDispatcherServlet (WebApplicationContext servletAppContext) { return new DispatcherServlet (servletAppContext); } @Nullable protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() { return null ; } protected abstract String[] getServletMappings(); @Nullable protected Filter[] getServletFilters() { return null ; } protected FilterRegistration.Dynamic registerServletFilter (ServletContext servletContext, Filter filter) { String filterName = Conventions.getVariableName(filter); Dynamic registration = servletContext.addFilter(filterName, filter); if (registration == null ) { int counter = 0 ; while (registration == null ) { if (counter == 100 ) { throw new IllegalStateException ("Failed to register filter with name '" + filterName + "'. " + "Check if there is another filter registered under the same name." ); } registration = servletContext.addFilter(filterName + "#" + counter, filter); counter++; } } registration.setAsyncSupported(isAsyncSupported()); registration.addMappingForServletNames(getDispatcherTypes(), false , getServletName()); return registration; } private EnumSet<DispatcherType> getDispatcherTypes () { return (isAsyncSupported() ? EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ASYNC) : EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE)); } protected boolean isAsyncSupported () { return true ; } protected void customizeRegistration (ServletRegistration.Dynamic registration) { } }
AbstractDispatcherServletInitializer 的父类 AbstractContextLoaderInitializer 的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer { protected final Log logger = LogFactory.getLog(getClass()); @Override public void onStartup (ServletContext servletContext) throws ServletException { registerContextLoaderListener(servletContext); } protected void registerContextLoaderListener (ServletContext servletContext) { WebApplicationContext rootAppContext = createRootApplicationContext(); if (rootAppContext != null ) { ContextLoaderListener listener = new ContextLoaderListener (rootAppContext); listener.setContextInitializers(getRootApplicationContextInitializers()); servletContext.addListener(listener); } else { logger.debug("No ContextLoaderListener registered, as " + "createRootApplicationContext() did not return an application context" ); } } @Nullable protected abstract WebApplicationContext createRootApplicationContext () ; @Nullable protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() { return null ; } }
AbstractDispatcherServletInitializer 的 createServletApplicationContext 方法和 AbstractContextLoaderInitializer 的 createRootApplicationContext 方法都是在子类 AbstractAnnotationConfigDispatcherServletInitializer 中实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public abstract class AbstractAnnotationConfigDispatcherServletInitializer extends AbstractDispatcherServletInitializer { @Override @Nullable protected WebApplicationContext createRootApplicationContext () { Class<?>[] configClasses = getRootConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext (); context.register(configClasses); return context; } else { return null ; } } @Override protected WebApplicationContext createServletApplicationContext () { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext (); Class<?>[] configClasses = getServletConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { context.register(configClasses); } return context; } @Nullable protected abstract Class<?>[] getRootConfigClasses(); @Nullable protected abstract Class<?>[] getServletConfigClasses(); }
看完上面代码和注释可以看出 WebInitializer 的 onStartup 方法其实主要做了几件事:
创建 Spring 容器,获取 Spring 容器对应的配置类并且把配置类注册到 Spring 容器,将 Spring 容器作为参数传入创建 Servlet 容器的监听器(ContextLoaderListener 实现了 ServletContextListener),将监听器注册到 servlet 容器中 创建 SpringMvc 容器,获取 SpringMvc 容器对应的配置类并且把配置类注册到 SpringMvc 容器,将 SpringMvc 容器作为参数传入创建 DispatcherServlet 实例,并且将 DispatcherServlet 实例注册 Servlet 容器中 之后的流程就和上面使用 xml 的方式差不多,在 Servelt 容器监听器 ContextLoaderListener 里的 contextInitialized 方法里面对 Spring 容器进初始化等一些操作,并且将 Spring 容器保存到 servlet 容器中,SpringMvc 容器也是在 Servlet 的 init 方法里面对 SpringMvc 容器进行初始化等一些操作,最终将 SpringMvc 容保存到 servlet 容器中。这里只是简单的描述一下,具体的流程可以按照上面 xml 里面分析的流程去调试代码,这样会比较清晰而且会有不错的收获。
SpringBoot 是如何加载 Servlet 的 SpringBoot 加载 servlet 和 filter 的方式和前面说的使用 web,xml 和使用 SPI 来加载 ServletContainerInitializer 实现类的方法都不同。下面先来看看在 SpringBoot 中 servlet 和 filter 的几种注册方式
注册方式一:servlet3.0 注解 +@ServletComponentScan SpringBoot 兼容 servlet3.0,可以使用一系列以 Web*开头的注解:@Servlet、@Filter、@WebListener
使用 Servlet3.0 提供的@Servlet 和@WebFilter 注解+@ServletComponentScan 注解
MyFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @WebFilter(value = "/myServlet",initParams = {@WebInitParam(name = "filterName",value = "myFilter"),@WebInitParam(name = "msg",value = "被我过滤了")}) public class MyFilter extends HttpFilter { @Override protected void doFilter (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { response.setContentType("text/html;charset=utf-8" ); String msg = getInitParameter("filterName" ) + ":" + getInitParameter("msg" ); response.getOutputStream().write(msg.getBytes(StandardCharsets.UTF_8)); response.getOutputStream().write("<br>" .getBytes(StandardCharsets.UTF_8)); chain.doFilter(request,response); } }
MyServlet
1 2 3 4 5 6 7 @WebServlet(value = "/myServlet") public class MyServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getOutputStream().write("myServlet:哈喽啊!!!" .getBytes(StandardCharsets.UTF_8)); } }
这是几种方式中最为简洁的方式,如果真的有特殊需求,需要在 springboot 下注册 servlet,filter,可以采用这样的方式,比较直观。
注册方式二:RegistrationBean MyConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration public class MyConfig { @Bean public ServletRegistrationBean<HelloWorldServlet> helloWorldServlet () { ServletRegistrationBean<HelloWorldServlet> helloWorldServlet = new ServletRegistrationBean <>(); helloWorldServlet.addUrlMappings("/hello" ); helloWorldServlet.setServlet(new HelloWorldServlet ()); return helloWorldServlet; } @Bean public FilterRegistrationBean<HelloWorldFilter> helloWorldFilter () { FilterRegistrationBean<HelloWorldFilter> helloWorldFilter = new FilterRegistrationBean <>(); helloWorldFilter.addUrlPatterns("/hello/*" ); helloWorldFilter.setFilter(new HelloWorldFilter ()); return helloWorldFilter; } }
HelloWorldFilter
1 2 3 4 5 6 7 public class HelloWorldFilter extends HttpFilter { @Override protected void doFilter (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("helloWorldFilter:被我拦截了" ); chain.doFilter(request,response); } }
HelloWorldServlet
1 2 3 4 5 6 7 public class HelloWorldServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8" ); resp.getOutputStream().write("helloWorldServlet:被我的处理了" .getBytes(StandardCharsets.UTF_8)); } }
ServletRegistrationBean 和 FilterRegistrationBean 都继承自 RegistrationBean ,RegistrationBean 是 springboot 中广泛应用的一个注册类,负责把 servlet,filter,listener 给容器化,使他们被 spring 托管,并且完成自身对 web 容器的注册。这种注册方式也值得推崇。
RegistrationBean 这个类的几个子类 ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean 的作用分别是帮助容器注册 servlet、filter、listener,DelegatingFilterProxyRegistrationBean 这个是用在 SpringSecurity 里的,这笔记力线不管,之后学习 SpringSecurity 的时候才会用到。在这里还需要留意 RegistrationBean 这个类实现了 ServletContextInitializer 接口(注意他和上面说的 ServletContainerInitializer 不是同一个东西),这个接口是一个核心接口,后面还会提到,这里先混个眼熟。
SpringBoot 内置 tomcat 容器加载 servlet 源码分析
SpringBoot 使用内置 tomcat 的情况下没有使用前面说到的 SpringServletContainerInitializer,而是使用 ServletContainerInitializer 的另一个实现类 TomcatStarter,但是这个 TomcatStarter 的调用并依赖不依赖于 servlet3.0 规范和 SPI,它走的是另外一套新的逻辑,在 Spring github 的 issue 中作者说了:https://github.com/spring-projects/spring-boot/issues/321
This was actually an intentional design decision. The search algorithm used by the containers was problematic. It also causes problems when you want to develop an executable WAR as you often want a javax.servlet.ServletContainerInitializer
for the WAR that is not executed when you run java -jar
.
See the org.springframework.boot.context.embedded.ServletContextInitializer
for an option that works with Spring Beans.
springboot 这么做是有意而为之。springboot 考虑到了如下的问题,我们在使用 springboot 时,开发阶段一般都是使用内嵌 tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 servlet3.0 的策略去加载 ServletContainerInitializer!最后作者还提供了一个替代选项:ServletContextInitializer,前文还提到 RegistrationBean 实现了 ServletContextInitializer 接口。
在 TomcatStarter 中的 ServletContextInitializer[] initializers 是 springboot 注册 servlet,filter,listener 的关键,先看看 TomcatStarter 的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class TomcatStarter implements ServletContainerInitializer { private static final Log logger = LogFactory.getLog(TomcatStarter.class); private final ServletContextInitializer[] initializers; private volatile Exception startUpException; TomcatStarter(ServletContextInitializer[] initializers) { this .initializers = initializers; } @Override public void onStartup (Set<Class<?>> classes, ServletContext servletContext) throws ServletException { try { for (ServletContextInitializer initializer : this .initializers) { initializer.onStartup(servletContext); } } catch (Exception ex) { this .startUpException = ex; if (logger.isErrorEnabled()) { logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: " + ex.getMessage()); } } } }
我们可以打断点调试一下,会发现在 TomcatStarter 里面的 ServletContextInitializer[] initializers 里存放的并不是我们上面说的 RegistrationBean 的子类的实例,而是三个用 lambda 表达式实现的匿名内部类的实例,其中在 ServletWebServerApplicationContext 中实现的匿名内部类是是获取 RegistrationBean 子类的实例的核心
想知道这个 ServletWebServerApplicationContext 的实例是在哪被放入 TomcatStarter 中的就要从 main 方法开始一路深入,到 ServletWebServerApplicationContext.createWebServer 前的调用逻辑大概是下面这样,具体自己调试看下比较清楚。
SpringApplication.run => SpringApplication.refreshContext => SpringApplication.refresh => AbstractApplicationContext.refresh
=> ServletWebServerApplicationContext.onRefresh => ServletWebServerApplicationContext.createWebServer
ServletWebServerApplicationContext 中 createWebServer 的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private void createWebServer () { WebServer webServer = this .webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null ) { ServletWebServerFactory factory = getWebServerFactory(); this .webServer = factory.getWebServer(getSelfInitializer()); getBeanFactory().registerSingleton("webServerGracefulShutdown" , new WebServerGracefulShutdownLifecycle (this .webServer)); getBeanFactory().registerSingleton("webServerStartStop" , new WebServerStartStopLifecycle (this , this .webServer)); } else if (servletContext != null ) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException ("Cannot initialize servlet context" , ex); } } initPropertySources(); }
getWebServer 方法的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public WebServer getWebServer (ServletContextInitializer... initializers) { if (this .disableMBeanRegistry) { Registry.disableRegistry(); } Tomcat tomcat = new Tomcat (); File baseDir = (this .baseDirectory != null ) ? this .baseDirectory : createTempDir("tomcat" ); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector (this .protocol); connector.setThrowOnFailure(true ); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false ); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this .additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); }
prepareContext 的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 protected void prepareContext (Host host, ServletContextInitializer[] initializers) { File documentRoot = getValidDocumentRoot(); TomcatEmbeddedContext context = new TomcatEmbeddedContext (); if (documentRoot != null ) { context.setResources(new LoaderHidingResourceRoot (context)); } context.setName(getContextPath()); context.setDisplayName(getDisplayName()); context.setPath(getContextPath()); File docBase = (documentRoot != null ) ? documentRoot : createTempDir("tomcat-docbase" ); context.setDocBase(docBase.getAbsolutePath()); context.addLifecycleListener(new FixContextListener ()); context.setParentClassLoader((this .resourceLoader != null ) ? this .resourceLoader.getClassLoader() : ClassUtils.getDefaultClassLoader()); resetDefaultLocaleMapping(context); addLocaleMappings(context); try { context.setCreateUploadTargets(true ); } catch (NoSuchMethodError ex) { } configureTldPatterns(context); WebappLoader loader = new WebappLoader (); loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName()); loader.setDelegate(true ); context.setLoader(loader); if (isRegisterDefaultServlet()) { addDefaultServlet(context); } if (shouldRegisterJspServlet()) { addJspServlet(context); addJasperInitializer(context); } context.addLifecycleListener(new StaticResourceConfigurer (context)); ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); host.addChild(context); configureContext(context, initializersToUse); postProcessContext(context); }
configureContext 的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 protected void configureContext (Context context, ServletContextInitializer[] initializers) { TomcatStarter starter = new TomcatStarter (initializers); if (context instanceof TomcatEmbeddedContext) { TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context; embeddedContext.setStarter(starter); embeddedContext.setFailCtxIfServletStartFails(true ); } context.addServletContainerInitializer(starter, NO_CLASSES); for (LifecycleListener lifecycleListener : this .contextLifecycleListeners) { context.addLifecycleListener(lifecycleListener); } for (Valve valve : this .contextValves) { context.getPipeline().addValve(valve); } for (ErrorPage errorPage : getErrorPages()) { org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org .apache.tomcat.util.descriptor.web.ErrorPage(); tomcatErrorPage.setLocation(errorPage.getPath()); tomcatErrorPage.setErrorCode(errorPage.getStatusCode()); tomcatErrorPage.setExceptionType(errorPage.getExceptionName()); context.addErrorPage(tomcatErrorPage); } for (MimeMappings.Mapping mapping : getMimeMappings()) { context.addMimeMapping(mapping.getExtension(), mapping.getMimeType()); } configureSession(context); new DisableReferenceClearingContextCustomizer ().customize(context); for (TomcatContextCustomizer customizer : this .tomcatContextCustomizers) { customizer.customize(context); } }
再看下 getSelfInitializer 方法的代码
1 2 3 4 private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer () { return this ::selfInitialize; }
selfInitialize 方法的代码如下:
1 2 3 4 5 6 7 8 9 private void selfInitialize (ServletContext servletContext) throws ServletException { prepareWebApplicationContext(servletContext); registerApplicationScope(servletContext); WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext); for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } }
getServletContextInitializerBeans 方法如下:
1 2 3 4 protected Collection<ServletContextInitializer> getServletContextInitializerBeans () { return new ServletContextInitializerBeans (getBeanFactory()); }
ServletContextInitializerBeans 的 iterator()方法
1 2 3 4 public Iterator<ServletContextInitializer> iterator () { return this .sortedList.iterator(); }
ServletContextInitializerBeans 的构造方法代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public ServletContextInitializerBeans (ListableBeanFactory beanFactory, Class<? extends ServletContextInitializer>... initializerTypes) { this .initializers = new LinkedMultiValueMap <>(); this .initializerTypes = (initializerTypes.length != 0 ) ? Arrays.asList(initializerTypes) : Collections.singletonList(ServletContextInitializer.class); addServletContextInitializerBeans(beanFactory); addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = this .initializers.values().stream() .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE)) .collect(Collectors.toList()); this .sortedList = Collections.unmodifiableList(sortedInitializers); logMappings(this .initializers); }
addServletContextInitializerBeans 方法的代码:
1 2 3 4 5 6 7 8 9 10 private void addServletContextInitializerBeans (ListableBeanFactory beanFactory) { for (Class<? extends ServletContextInitializder > initializerType : this .initializerTypes) { for (Entry<String, ? extends ServletContextInitializer > initializerBean : getOrderedBeansOfType(beanFactory, initializerType)) { addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory); } } }
addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,ListableBeanFactory beanFactory)的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void addServletContextInitializerBean (String beanName, ServletContextInitializer initializer, ListableBeanFactory beanFactory) { if (initializer instanceof ServletRegistrationBean) { Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet(); addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source); } else if (initializer instanceof FilterRegistrationBean) { Filter source = ((FilterRegistrationBean<?>) initializer).getFilter(); addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source); } else if (initializer instanceof DelegatingFilterProxyRegistrationBean) { String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName(); addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source); } else if (initializer instanceof ServletListenerRegistrationBean) { EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener(); addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source); } else { addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory, initializer); } }
addServletContextInitializerBean(Class<?> type, String beanName, ServletContextInitializer initializer,ListableBeanFactory beanFactory, Object source)的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void addServletContextInitializerBean (Class<?> type, String beanName, ServletContextInitializer initializer, ListableBeanFactory beanFactory, Object source) { this .initializers.add(type, initializer); if (source != null ) { this .seen.add(source); } if (logger.isTraceEnabled()) { String resourceDescription = getResourceDescription(beanName, beanFactory); int order = getOrder(initializer); logger.trace("Added existing " + type.getSimpleName() + " initializer bean '" + beanName + "'; order=" + order + ", resource=" + resourceDescription); } }
所以最后在 ServletWebServerApplicationContext 中的 selfInitialize 方法里面通过 getServletContextInitializerBeans 获取到的是 ServletContextInitializerBeans 中保存了我们注册到 Spring 容器的 RegistrationBean 的实例的 sortedList,然后遍历 sortedList 从而调用这些 RegistrationBean 的实例的 onStartup 方法。(如果上面的流程解析看不懂,可以配合源码自己走一遍会更加清晰)
看下 RegistrationBean 的 onStartup 方法
1 2 3 4 5 6 7 8 9 public final void onStartup (ServletContext servletContext) throws ServletException { String description = getDescription(); if (!isEnabled()) { logger.info(StringUtils.capitalize(description) + " was not registered (disabled)" ); return ; } register(description, servletContext); }
DynamicRegistrationBean(FilterRegistrationBean 和 ServletRegistrationBean 的父类)实现了 register 方法
1 2 3 4 5 6 7 8 9 protected final void register (String description, ServletContext servletContext) { D registration = addRegistration(description, servletContext); if (registration == null ) { logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)" ); return ; } configure(registration); }
ServletRegistrationBean 的 addRegistration 方法
1 2 3 4 5 6 @Override protected ServletRegistration.Dynamic addRegistration (String description, ServletContext servletContext) { String name = getServletName(); return servletContext.addServlet(name, this .servlet); }
看到这就可以大概知道 SpringBoot 是如何加载 servlet 的了,filter 和 listener 也是如此。
注册方式三:继承 ServletContextInitializer 实现 onStartup 方法 看完上面的分析我们还可以用第三种注册 servlet 的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component public class CustomServletContextInitializer implements ServletContextInitializer { private final static String JAR_HELLO_URL = "/custom" ; @Override public void onStartup (ServletContext servletContext) throws ServletException { System.out.println("创建 customServlet..." ); ServletRegistration.Dynamic servlet = servletContext.addServlet( CustomServlet.class.getSimpleName(), CustomServlet.class); servlet.addMapping(JAR_HELLO_URL); System.out.println("创建 customFilter..." ); FilterRegistration.Dynamic filter = servletContext.addFilter( CustomFilter.class.getSimpleName(), CustomFilter.class); EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class); dispatcherTypes.add(DispatcherType.REQUEST); dispatcherTypes.add(DispatcherType.FORWARD); filter.addMappingForUrlPatterns(dispatcherTypes, true , JAR_HELLO_URL); } }
最后这里提一下上面 ServletWebServerApplicationContext 中 createWebServer 的代码中的 getWebServerFactory()方法,它的作用是从 spring 容器中获取一个类型为 ServletWebServerFactory(TomcatServletWebServerFactory 是其子类)的工厂实例,而这个实例是通过 SpringBoot 的自动装配完成的(SpringBoot 的自动装配可以了解一下)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected ServletWebServerFactory getWebServerFactory () { String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class); if (beanNames.length == 0 ) { throw new ApplicationContextException ("Unable to start ServletWebServerApplicationContext due to missing " + "ServletWebServerFactory bean." ); } if (beanNames.length > 1 ) { throw new ApplicationContextException ("Unable to start ServletWebServerApplicationContext due to multiple " + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames)); } return getBeanFactory().getBean(beanNames[0 ], ServletWebServerFactory.class); }
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration(proxyBeanMethods = false) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(ServletRequest.class) @ConditionalOnWebApplication(type = Type.SERVLET) @EnableConfigurationProperties(ServerProperties.class) @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, //留意这个 ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class }) public class ServletWebServerFactoryAutoConfiguration { ..... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Configuration(proxyBeanMethods = false) class ServletWebServerFactoryConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) static class EmbeddedTomcat { @Bean TomcatServletWebServerFactory tomcatServletWebServerFactory ( ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory (); factory.getTomcatConnectorCustomizers() .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatContextCustomizers() .addAll(contextCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatProtocolHandlerCustomizers() .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList())); return factory; } } }
总结 存在 web.xml 配置的 java web 项目,servlet3.0 的 java web 项目,springboot 内嵌容器的 java web 项目加载 servlet,filter,listener 的流程都是有所差异的,理解清楚这其中的原来,其实并不容易,至少得搞懂 servlet3.0 的规范,springboot 内嵌容器的加载流程等等前置逻辑。
最后还是要感谢这位大佬的文章 ,我做的只是在他的基础上针对我自己再补充的笔记,文笔没有大佬的简洁,所以如果觉得说得太啰嗦看不懂建议去看这个大佬的文章,你会发觉你发现了新大陆。