1.前言
上文我们一直都有操作web.xml。我们总是能在传统的Spring项目看到如下Web容器配置文件。
为什么有些Spring/Spring boot中,没有web.xml?
我们都知道Spring 框架是一个功能强大且广泛使用的 Java
应用程序框架,旨在简化企业级 Java
开发的复杂性。它提供了全面的基础架构支持。其实是Spring通过一系列操作将web.xml去除掉而已。
2.Servlet-ServletContainerInitializer
`ServletContainerInitializer
是 Java EE 和 Jakarta EE
规范中的一个接口,允许开发人员在应用程序启动时对 Servlet
容器进行编程配置。
它是 Servlet 3.0
规范中引入的,旨在简化在容器启动时进行注册的工作。位于
javax.servlet
包中。其主要作用是允许开发人员在 Servlet
容器启动时执行一些初始化代码。
1 2 3 4 5 6 7 8 9 10 package javax.servlet;import java.util.Set;public interface ServletContainerInitializer { void onStartup (Set<Class<?>> c, ServletContext ctx) throws ServletException; } Set<Class<?>> c:容器扫描到的与当前 ServletContainerInitializer 相关的类。 ServletContext ctx:当前 Web 应用的 ServletContext,通过它可以注册 Servlets、Filters 和 Listeners。
2.1
实现ServletContainerInitializer接口
如下:实现了ServletContainerInitializer
1 2 3 4 5 6 7 8 9 10 11 12 import javax.servlet.ServletContainerInitializer;import javax.servlet.ServletContext;import javax.servlet.ServletException;import java.util.Set;public class MyAppInitializer implements ServletContainerInitializer { @Override public void onStartup (Set<Class<?>> c, ServletContext ctx) throws ServletException { } }
2.2 使用 @HandlesTypes
注解(可选)
@HandlesTypes
是 Servlet 3.0
规范中引入的一个注解,用于与 ServletContainerInitializer
一起使用。这个注解的作用是指示哪些类应该被传递给
ServletContainerInitializer
的 onStartup
方法。具体来说,@HandlesTypes
可以用来标记需要在容器启动时进行特定处理的一组类、接口或注解。
@HandlesTypes
注解可以用来指定容器在启动时感兴趣的类或接口。容器将扫描这些类或接口的实现,并将其传递给
onStartup
方法的第一个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import javax.servlet.annotation.HandlesTypes;@HandlesTypes(MyApp.class) public class MyAppInitializer implements ServletContainerInitializer { @Override public void onStartup (Set<Class<?>> c, ServletContext ctx) throws ServletException { for (Class<?> clazz : c) { ServletRegistration.Dynamic servlet = ctx.addServlet("myServlet" , MyServlet.class); servlet.addMapping("/myServlet" ); } } } 我们发现可以注册自定义的Servlet注册到容器中去。
文件名必须是
javax.servlet.ServletContainerInitializer
,文件内容为实现类的全限定名。
1 com.example.MyAppInitializer
这是SPI机制。Java SPI(Service Provider Interface)是 Java
平台中的一种设计模式和机制,它允许服务的实现类在运行时动态地被发现和使用。SPI
是 Java 平台模块系统的一部分,特别适用于设计可扩展和可插拔的应用程序
ServletContainerInitializer
提供了一种灵活的方式,在
Servlet
容器启动时进行编程配置。通过实现该接口,你可以在应用启动时注册自定义的
Servlets、Filters 和 Listeners,而不需要依赖于传统的 web.xml
配置文件。这使得应用程序更具可配置性和可扩展性。
@HandlesTypes
注解配合
ServletContainerInitializer
使用,可以在 Servlet
容器启动时容器会自动发现并调用 ServletContainerInitializer
实现类的 onStartup
方法。容器会将所有符合
@HandlesTypes
注解中指定的类型的类传递给该方法。这种机制对于构建复杂的、可插拔的 Java
Web 应用程序非常有用。
通过@HandlesTypes与ServletContainerInitializer,我们可以去除web.xml配置。
3.Spring-SpringServletContainerInitializer
3.1
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 43 @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) { if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { ... ... initializers.add( (WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass).newInstance()); ... ... } } } ... ... for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } } } }@HandlesTypes(WebApplicationInitializer.class) ---->Set<Class<?>> webAppInitializerClasses
还记得@HandlesTypes
注解吗。在上文我们介绍了@HandlesTypes注解可以用来指定容器在启动时设置类或接口。容器将扫描这些类或接口的实现,并将其传递给 onStartup
方法的第一个参数。其实见名知意我们也知道是这么回事:
然后我们再看Spring下有以下文件。
这不就是上面所说的SPI机制吗。spring通过SPI机制将实现ServletContainerInitializer的类在容器启动的时候对
Servlet 容器进行编程配置。
3.2 WebApplicationInitializer
`WebApplicationInitializer
是 Spring Framework
提供的一个接口,用于在 Servlet 3.0+ 环境中配置 Servlet
容器。它提供了一种替代传统 web.xml
文件的方法,通过代码配置
Web 应用程序。
WebApplicationInitializer
是 Spring 3.1
引入的,它通过实现这个接口可以在 Servlet 容器启动时动态注册和配置
Servlet、Filter 和 ServletContextListener
等组件。
1 2 3 4 5 public interface WebApplicationInitializer { void onStartup (ServletContext servletContext) throws ServletException; }
总结
WebApplicationInitializer
:用于配置
Spring 应用上下文的接口,替代 web.xml
文件,通过实现该接口,可以用 Java 代码配置 Servlet 容器。可以在不使用
web.xml
文件的情况下配置 Web 应用程序。这种方法在现代
Spring 应用中非常常见
特别是与 Spring Boot
一起使用时。WebApplicationInitializer
提供了一种更灵活和可编程的方式来配置 Servlet
容器,使得应用程序的配置更加直观和易于维护。
3.3
AbstractDispatcherServletInitializer
AbstractDispatcherServletInitializer作为WebApplicationInitializer的实现类。
3.3.1 onStartup(ServletContext
sc)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); } ... ... }
3.3.2
registerDispatcherServlet(ServletContext sc)
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 protected void registerDispatcherServlet (ServletContext servletContext) { String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return empty or null" ); WebApplicationContext servletAppContext = createServletApplicationContext(); ... ... FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, DispatcherServlet); ... ... 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); }
注意:如果是SpringBoot项目这里就是唯一容器上下文。如果项目是SpringMVC+Spring,这里创建的上下文是Spring容器的子上下文,组成父子上下文。父子上下文特点是子容器(controller层)可访问父容器(service层,dao层)里的Bean,父容器不能访问子容器里的Bean,优点是层次分明。像是这种父子结构的容器,在@Service层是不能注入@Controller
Bean的,原因如上。
4.Tomcat启动流程
4.1 SPI加载
当 Servlet 容器(如
Tomcat)启动时,Tomcat通过读取web.xml,在启动时根据SPI机制的ServiceLoader#load方法
加载所有实现了 ServletContainerInitializer
接口的实现类。加载SPI的代码在org.apache.catalina.startup.ContextConfig的processServletContainerInitializers
方法中:
1 2 3 4 5 6 7 protected void processServletContainerInitializers () { List<ServletContainerInitializer> detectedScis; WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader <>(context); detectedScis = loader.load(ServletContainerInitializer.class); ... ... }
然后在StandardContext的startInternal获取到所有的ServletContainerInitializer并调用onStartup方法:
1 2 3 4 for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) { entry.getKey().onStartup(entry.getValue(), getServletContext()); }
4.2
加载SpringServletContainerInitializer
Spring 的实现类为
SpringServletContainerInitializer
,因此Tomcat会调用其onStartup
方法:
1 2 3 4 5 6 @HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup (@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { } }
其类上标注了@HandlesTypes({WebApplicationInitializer.class})
。tomcat会找到所有的WebApplicationInitializer实现类,将所有的实现类传入SpringServletContainerInitializer#onStartup方法的第一个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 SpringServletContainerInitializer----> initializer.onStartup(servletContext); AbstractDispatcherServletInitializer---->@Override public void onStartup (ServletContext servletContext) throws ServletException { super .onStartup(servletContext); registerDispatcherServlet(servletContext); }protected void registerDispatcherServlet (ServletContext servletContext) { ... ... FrameworkServlet DispatcherServlet = createDispatcherServlet(servletAppContext); ... ... }protected FrameworkServlet createDispatcherServlet (WebApplicationContext servletAppContext) { return new DispatcherServlet (servletAppContext); } 此处就通过new DispatcherServlet 的方式创建了DispatcherServlet。
回到SpringServletContainerInitializer#onStartup方法中的逻辑,将所有的WebApplicationInitializer实现类的onStartup方法一一调用。并注册DispatcherServlet
。
4.3 启动流程总结
当 Servlet 容器(如
Tomcat)启动时,Tomcat通过读取web.xml,在启动时根据SPI机制的ServiceLoader#load方法
加载所有实现了 ServletContainerInitializer
接口的实现类。
Spring 提供了一个实现类
SpringServletContainerInitializer
,并且通过@HandlesTypes
查找并通过反射机制实例化所有实现了
WebApplicationInitializer
接口的类。将他们作为参数传递给在
SpringServletContainerInitializer
的 onStartup
方法的第一个参数,循环并调用它们的 onStartup
方法来完成
DispatcherServlet
的创建和注册。最终通过new DispatcherServlet(WebApplicationContext webApplicationContext)
的方式创建了DispatcherServlet。
5.总结
传统的 Java Web 应用程序中,web.xml
文件是必须的。它是
Java EE Web 应用的部署描述符,用于配置 Servlet、Filter、Listener
等组件,以及其他与部署相关的配置。
但是在使用 Spring Framework 开发的现代化 Web
应用程序中,web.xml
文件不是必需的。Spring
提供了一种基于代码的配置方式,使得可以完全通过 Java 代码来配置
Servlet、Filter、Listener 等组件,而不需要 web.xml
文件。就比如可以创建一个实现了 WebApplicationInitializer
接口的类,并在其中编写 Servlet、Filter、Listener 的配置代码,而不是在
web.xml
文件中进行配置。
博客说明
文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,不用于任何的商业用途。如有侵权,请联系本人删除。谢谢!