为什么有些Spring项目中,没有web.xml?

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 {
// Initialization logic here
}
}

2.2 使用 @HandlesTypes 注解(可选)

@HandlesTypes 是 Servlet 3.0 规范中引入的一个注解,用于与 ServletContainerInitializer 一起使用。这个注解的作用是指示哪些类应该被传递给 ServletContainerInitializeronStartup 方法。具体来说,@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注册到容器中去。

2.3 在 META-INF/services 目录下创建文件

文件名必须是 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)) {

... ...

//ReflectionUtils.accessibleConstructor--这是 Spring Framework 中的一个工具方法,用于获取给定类的可访问构造函数

//newInstance():这是 Java 反射机制中的方法,用于创建给定类的新实例。
//整个表达式的意思是,通过反射创建了一个 WebApplicationInitializer 接口的实现类的新实例。

initializers.add( (WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass).newInstance());

... ...

}
}
}

... ...


//循环遍历,调用各个实现类的onStartup方法。
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}

}

//上面的就是扫描所有WebApplicationInitializer,将其所有的实现都传递给onStartup,然后再通过反射实例化,放入initializers集合里。然后遍历依次执行子类覆写的onStartup方法。下面我们具体分析。
@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 {

/**
* The default servlet name. Can be customized by overriding {@link #getServletName}.
*/
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");

//创建Spring容器
WebApplicationContext servletAppContext = createServletApplicationContext();
... ...

//创建SpringMVC入口-DispatcherServlet
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

//addServlet-将SpringMVC中的servlet注册到web容器,从此就可以通过web服务器访问Spring中的controller
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, DispatcherServlet);
... ...

//相当于配置web.xml:表明服务启动的时候就加载Servlet
registration.setLoadOnStartup(1);

//添加URLMapping
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());

//配置filter
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;
// 使用SPI加载ServletContainerInitializer实现
WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
detectedScis = loader.load(ServletContainerInitializer.class);
... ...
}

然后在StandardContext的startInternal获取到所有的ServletContainerInitializer并调用onStartup方法:

1
2
3
4
// Call ServletContainerInitializers
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 接口的类。将他们作为参数传递给在 SpringServletContainerInitializeronStartup 方法的第一个参数,循环并调用它们的 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 文件中进行配置。


博客说明

文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,不用于任何的商业用途。如有侵权,请联系本人删除。谢谢!


为什么有些Spring项目中,没有web.xml?
https://nanchengjiumeng123.top/2024/02/11/framework/spring/Spring MVC/4.为什么有些Spring项目中,没有web.xml?/
作者
Yang Xin
发布于
2024年2月11日
许可协议