文章目录
相关文章:
Spring MVC的web.xml配置详解(ContextLoaderListener创建容器监听器、DispatcherServlet) web.xml配置
系列文章:
【springboot 】web.xml去哪了 1 (SpringServletContainerInitializer接口、WebApplicationInitializer接口)
【springboot 】web.xml去哪了2 (ServletWebServerApplicationContext容器)
前言
Servlet3.0之后,可以通过编码方式替换web.xml
-
普通的servlet工程 ,通过
用户自己
去实现ServletContainerInitializer
,在onStartup()方法中 创建servlet和filter。当然,需要依赖SPI机制进行注入 -
springmvc工程(对普通servlet进行了封装,本质也是servlet)
springmvc框架已经
内置
了SpringServletContainerInitializer
,它也是ServletContainerInitializer
的子类,在onStartup()方法中他负责读取WebApplicationInitializer
接口,因此,用户需要自己去实现WebApplicationInitializer
接口接口即可
1:web.xml是怎么没的?
1.1:Servlet3.0之前
在servlet3.0以前,编写web程序的流程大概如下:
1:编写servlet,并通过<servlet>标签注册到web.xml文件中
2:编写filter,并通过<filter>标签注册到web.xml文件中
3:编写lisner,并通过<listener>标签注册到web.xml文件中
参见 Spring MVC的web.xml配置详解(ContextLoaderListener创建容器监听器、DispatcherServlet)
用的比较多,这里就不再提供具体实例了。
web.xml文件是web容器和我们应用程序的纽带,相关的信息都需要注册在这里。此时这些信息的注册是通过web容器读取web.xml来完成的。但是这个过程比较繁琐,作为开发的我们,如果能通过编程的方式来完成这个过程的话就能实现不依赖于web.xml了。所幸,我们最终迎来了servlet3.0时代,让我们这个需求能够得到满足。
1.2:Servlet3.0
既然是通过编程的方式来完成servlet,filter,listener等web容器组件的注册,那么肯定就要提供对应的方式了,目前有两种方式,第一种是使用@WebServlet,@WebFilter
等注解,第二种方式使用使用javax.servlet.ServletContext
(这是web容器封装servlet,filter,listener等组件的上下文对象),主要API如下:
-
添加servlet相关API
public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass); public ServletRegistration.Dynamic addServlet(String servletName, String className); public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet);
调用以上方法,相当于在web.xml中配置
<servlet>
标签。 -
添加filter相关API
public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass); public FilterRegistration.Dynamic addFilter(String filterName, Filter filter); public FilterRegistration.Dynamic addFilter(String filterName, String className);
调用以上方法相当于在web.xml中配置标签。
-
添加listener相关API
public void addListener(String className); public <T extends EventListener> void addListener(T t); public void addListener(Class<? extends EventListener> listenerClass);
调用以上方法相当于在web.xml中配置
<listener>
标签。
现在,向web容器注册各种组件的API有了,想要完成注册,该怎么做呢?此时,就需要接口来提供规范了,这个接口是javax.servlet.ServletContainerInitializer
,源码如下:
// 通过在META-INF/services/文件夹下创建javax.servlet.ServletContainerInitializer文件
// 然后将自己提供的实现类按照一行一个的格式配置到文件中,web容器在启动的时候会通过SPI来加载,并执行
// 对应的onStartup方法,执行自定义的注册逻辑,完成相关组件的注册
public interface ServletContainerInitializer {
// 参数c:期望处理的class类型集合
// 参数ctx:servlet上线文对象,我们通过该对象中来完成各种web组件注册工作
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
ServletContainerInitializer
接口内含有一个onStartup()
,顾名思义,启动的时候触发,那么我们可以在这个回调函数中注入我们自定义的servlet
和filter
2. 项目实战
新建一个servlet项目,当然也可以直接下载源码(参见原文)。来测试一下。
servlet
public class MyServletContainerInitializerServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello from servlet register by ServerContainerInitializer!!!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// post也走get
this.doGet(req, resp);
}
}
filter:
public class MyServletContainerInitializerFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("MyServletContainerInitializerFilter.init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("MyServletContainerInitializerFilter.doFilter");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("MyServletContainerInitializerFilter.destroy");
}
}
ServletContainerInitializer实现类:
public class MyServletContainerInitializer implements ServletContainerInitializer {
private final static String JAR_HELLO_URL = "/hello";
//启动启动时会触发,spi机制
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("注册servlet到web容器开始!");
//注入Servlet
ServletRegistration.Dynamic dynamicServlet = servletContext.addServlet(MyServletContainerInitializerServlet.class.getSimpleName(), MyServletContainerInitializerServlet.class);
// 添加映射
dynamicServlet.addMapping(JAR_HELLO_URL);
System.out.println("注册servlet到web容器结束!");
System.out.println("注册filter到web容器开始!");
//注入filter
FilterRegistration.Dynamic dynamicFilter = servletContext.addFilter(MyServletContainerInitializerFilter.class.getSimpleName(), MyServletContainerInitializerFilter.class);
EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
dispatcherTypes.add(DispatcherType.REQUEST);
dispatcherTypes.add(DispatcherType.FORWARD);
dynamicFilter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
System.out.println("注册filter到web容器结束!");
}
}
现在准备工作都完成了,但是,web容器此时还无法识别到我们的ServletContainerInitializer
,根据规范,还需要配置SPI
,因此我们在META-INF/services
下创建文件javax.servlet.ServletContainerInitializer
,并填入实现类内容,如下:
接下来我们启动查看日志:
...snip...
注册servlet到web容器开始!
注册servlet到web容器结束!
注册filter到web容器开始!
注册filter到web容器结束!
...snip...
访问测试:
C:\Users\Administrator>curl http://192.168.10.119:10080/test_servlet3_without_webxml/hello
hello from servlet register by ServerContainerInitializer!!!
3. springMVC如何集成servlet3.0
我们自己开发程序来集成servlet3.0是通过提供ServletContainerInitializer
的实现类,并通过SPI来配置,供web服务器读取,springMVC也是如此SPI机制。
先从servlet3.0之后的启动规则说起
servlet3.0规则:
1)、服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer
实例:
2)、ServletContainerInitializer的实现放在jar包的META-INF/services
文件夹下,有一个名为javax.servlet.ServletContainerInitializer
的文件,内容就是ServletContainerInitializer
的实现类的全类名
spring-web-xxx.RELEASE.jar已经内置一个ServletContainerInitializer
的子类,子类类型为SpringServletContainerInitializer :
其提供的实现类是org.springframework.web.SpringServletContainerInitializer
(也实现了ServletContainerInitializer
),源码如下详细看注释!!!
org.springframework.web.SpringServletContainerInitializer
// 该类设计的目的是让开发人员基于编码的方式来支持servlet容器,看到@HandlesTypes(WebApplicationInitializer.class),代表该类设置web容器在回调时,将classpath下的WebApplicationInitializer的实现类作为参数传递到方法onStartup的参数webAppInitializerClasses
// 中,这种是和web.xml对立的方式(也可能和web.xml方式混合使用)
// 操作机制:当支持servlet3的web容器启动的时候,会通过jar servicec API(ServiceLoader.load(xxx))从classpath下的spring-web.jar包中读取META-INF/services/javax.servlet.ServletContainerInitializer文件,在该文件中配置的实现类正是该类,
// 然后就会调用该类的onStartup方法,并将classpath下WebApplicationInitializer的实现类作为参数传递到webAppInitializerClasses参数中
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
// 参数webAppInitializerClasses:classpath下WebApplicationInitializer的实现类
// 参数servletContext:web容器servlet上下文对象
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
// web容器传进来的webAppInitializerClasses不为空
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// 因为一些web容器会将@HandlesTypes指定的类型外的一些类传进来,所以再进一步做个判断,可以说是因为web容器的bug而不得不写的代码
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 添加到initializers集合中
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
// initializers为空,即在classpath下没有WebApplicationInitializer的子类,简单的给出日志提示,并return
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
// 日志记录在classpath下发现了多少个WebApplicationInitializer的子类
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
// 排序
AnnotationAwareOrderComparator.sort(initializers);
//**** 循环调用onoStartup方法,注册web组件到ServletContext中
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
上面代码的onStartup()
干了什么?只作一件事情,就是提取WebApplicationInitializer
的实现类,然后调用他们的onStartup()
(参见最后代码****处的for循环)。也就是说上面代码的onStartup()是个入口,实际上是通过用户自定义的WebApplicationInitializer实现类间接实现注入的。
到此处,我们就知道,想要在无web.xml文件的情况下使用springMVC,只需要提供一个org.springframework.web.WebApplicationInitializer
的实现类,然后在其onStartup方法中注册web容器相关组件就可以了
3.1 WebApplicationInitializer接口源码
WebApplicationInitializer接口源码如下,而且注释中包含了示例,教你如何注入一个自定义的spring mvc 元素!!!:
// 该接口用来在servlet3.0环境中通过编程的方式配置ServletContext(注册Servlet,Filter,Listener等),与之对应的是基于web.xml的配置方式(二者有时候也可以混用)。具体的实现类会通过SpringServletContainerInitializer(web容器的构子类)类加载并调用。
// 一般开发人员会通过web.xml方式来注册DispatcherServlet(当然还有其他组件)到web容器的。可能如下:
/*
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
*/
// 如果是使用WebApplicationInitializer的等价编程配置方式的话,可能如下:
/*
public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(appContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
*/
// 例子中是直接实现WebApplicationInitializer接口,实际中可以通过继承AbstractDispatcherServletInitializer类来完成操作,
// 该类已经完成了注册DispatcherServlet等基础工作,我们只需要实现其抽象方法提供IOC容器就可以了
// 程序中“appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");”还是使用了xml文件,我们可以使用基于注解的spring IOC容器来改造代码,实现零xml配置,代码可能如下:
/*
public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
// Create the 'root' Spring application context
AnnotationConfigWebApplicationContext rootContext =
new AnnotationConfigWebApplicationContext();
rootContext.register(AppConfig.class);
// Manage the lifecycle of the root application context
container.addListener(new ContextLoaderListener(rootContext));
// Create the dispatcher servlet's Spring application context
AnnotationConfigWebApplicationContext dispatcherContext =
new AnnotationConfigWebApplicationContext();
dispatcherContext.register(DispatcherConfig.class);
// Register and map the dispatcher servlet
ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
*/
public interface WebApplicationInitializer {
// 在该方法中可以对servletContext进行servlets,fitlers,listeners,context-params以及属性信息的配置。
void onStartup(ServletContext servletContext) throws ServletException;
}
3.2 WebApplicationInitializer接口实战
既然是springMVC程序,首先当然得定义一个处理请求的handler了,如下:
@Controller
@RequestMapping("/testinterceptor")
public class HelloController {
@PostConstruct
public void xxx() {
System.out.println("HelloController.xxx");
}
@RequestMapping("/hi")
@ResponseBody
public String sayHi(HttpServletResponse response) {
System.out.println("HelloController.sayHi");
String msg = "testinterceptor hi";
System.out.println(msg);
return msg;
}
}
然后我们定义WebApplicationInitializer的子类,如下:
public class MyWebXml implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("MyWebXml 加载开始!");
// new springmvc的容器对象
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
// 注册控制器
ctx.register(HelloController.class);
// 设置servlet上线文对象
ctx.setServletContext(servletContext);
String dispatcherServletName = DispatcherServlet.class.getSimpleName();
// 注册springmvc分发请求的DispatcherServlet
/*
相当于在web.xml中配置代码:
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
*/
ServletRegistration.Dynamic dynamicDispatcherServlet = servletContext.addServlet(dispatcherServletName, new DispatcherServlet(ctx));
dynamicDispatcherServlet.addMapping("/");
dynamicDispatcherServlet.setLoadOnStartup(1);
System.out.println("MyWebXml 加载结束!");
}
}
此时我们就可以启动程序来访问我们的接口了,如下:
C:\Users\Administrator>curl http://localhost:10080/springmvc_without_webxml_war_exploded/testinterceptor/hi
testinterceptor hi
当然我们也可以通过实现WebApplicationInitializer
的抽象子类AbstractDispatcherServletInitializer
,该类已经完成了DispatcherServlet
的注册工作,可以修改MyWebXml如下:
public class MyWebXml extends AbstractDispatcherServletInitializer {
private static AnnotationConfigWebApplicationContext servletAc = new AnnotationConfigWebApplicationContext();
private static AnnotationConfigWebApplicationContext rootAc = new AnnotationConfigWebApplicationContext();
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
// 注册handler
servletAc.register(HelloController.class);
}
/*
创建 dispatcherservlet的Spring IOC容器
*/
@Override
protected WebApplicationContext createServletApplicationContext() {
return servletAc;
}
/*
返回DispatcherServlet的servletmapping信息
*/
@Override
protected String[] getServletMappings() {
List<String> servletMappingList = new ArrayList<>();
servletMappingList.add("/");
return StringUtils.toStringArray(servletMappingList);
}
// 创建root 的spring IOC容器
@Override
protected WebApplicationContext createRootApplicationContext() {
return rootAc;
}
}
效果是完全一样的。测试源码从这里下载(参见原文)
4. springboot如何集成servlet3.0
注意,这里是集成原生的servlet,而不是springmvc中的controller
4.1:通过servlet3注解+@ServletComponentScan
通过servlet3.0中定义的@WebXxx
相关注解,然后通过@ServletComponentScan
配置要扫描的包路径,如下测试:
springboot如何集成spring的? @ComponentScan,参见 【spring】JavaConfig、@Configuration、@ComponentScan入门例子
-
定义servlet ,通过@WebServlet声明一个servlet:
@WebServlet("/myservlet") public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("res from spring boot servlet!!!"); } }
-
启动类,通过@ServletComponentScan,定义了扫描的路径,如果不填,则默认取@SpringBootApplication类所在的包作为路径
@SpringBootApplication @ServletComponentScan(basePackages = { "com.example.springbootintegarationspringmvc" }) public class SpringbootIntegrationSpringmvcApplication { public static void main(String[] args) { SpringApplication.run(SpringbootIntegrationSpringmvcApplication.class, args); } }
启动测试
C:\Users\Administrator>curl http://localhost:8080/myservlet
res from spring boot servlet!!!
4.2:通过RegistrationBean
rg.springframework.boot.web.servlet.RegistrationBean
是springboot提供的抽象类,实现了ServletContextInitializer
接口,负责将servlet,filter,listener等注册到spring IOC容器中。其源码如下:
RegistrationBean 是个内置的间接类,实现了
ServletContextInitializer
接口,我们可以扩展它,不用使用最原始的ServletContextInitializer
接口
// 用于在servlet3.0+环境中程序化方式配置servletContext的接口。与实现了WebApplicationInitializer接口
// 的类不同,不会被SpringServletContainerInitializer自动获取,因此不会被web容器自动加载。
// 但是其扮演的角色和ServletContainerInitializer类似,不同之处在于通过spring来完成生命周期
// 的管理,而非像ServletContainerInitializer是通过web容器管理生命周期。
public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
private static final Log logger = LogFactory.getLog(RegistrationBean.class);
private int order = Ordered.LOWEST_PRECEDENCE;
private boolean enabled = true;
// 这里实现具体的逻辑,完成web组件,如servlet,filter,listener等向servletcontext中
// 注册的工作,但是注意这个过程是spring在启动web容器的过程中完成的,因为此时web容器只是
// springboot的一个组件而已。
// 启动过程是"springboot main->启动web容器->调用该方法配置servletContext"
@Override
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);
}
protected abstract String getDescription();
protected abstract void register(String description, ServletContext servletContext);
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return this.enabled;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
}
4.2.1 RegistrationBean实战
首先来定义一个servlet,注意,没有@WebServlet修饰:
public class MyServlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello from my servlet 1");
}
}
定义Java config类,注意,在 @Bean中注入一个ServletRegistrationBean 实例,对应一个Servlet:
@Configuration
public class WebComponentConfiguration {
@Bean
public ServletRegistrationBean myServlet1() {
//使用ServletRegistrationBean ,映射一个Servlet
ServletRegistrationBean myServlet1 = new ServletRegistrationBean();
myServlet1.addUrlMappings("/myservlet1");
myServlet1.setServlet(new MyServlet1());
return myServlet1;
}
}
启动类:
@SpringBootApplication
@ServletComponentScan(basePackages = { "com.example.springbootintegarationspringmvc" })
public class SpringbootIntegrationSpringmvcApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootIntegrationSpringmvcApplication.class, args);
}
}
启动测试:
$ curl.exe --silent http://192.168.10.119:8080/myservlet1
hello from my servlet 1
接下来,我们看下springboot是如何最终通过调用我们的RegistrationBean来完成ServletContext的设置的。
4.3:调用过程分析
基于springboot V1.5.4RELEASE版本分析。