从Servlet3.0开始就可以不需要web.xml了,而Spring MVC中也很好的支持了这一个特性。
简单使用
gradle配置
description = "Spring MVC Demo"
apply plugin: "groovy"
apply plugin: "kotlin"
dependencies {
compile(project(":spring-webmvc"))
compileOnly 'javax.servlet:javax.servlet-api:3.1.0'
compile 'org.apache.tomcat.embed:tomcat-embed-core:8.5.23'
compile 'org.apache.tomcat:tomcat-jasper:8.5.16'
compile("org.slf4j:slf4j-api:1.7.30")
compile("org.slf4j:slf4j-log4j12:1.7.30")
}
实现WebApplicationInitializer接口创建一个容器
实现WebApplicationInitializer接口,只创建一个Spring Web容器。
package com.morris.spring.mvc.config;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
/**
* 实现WebApplicationInitializer接口,只创建一个Spring Web容器
*/
public class MyAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletCxt) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
ac.register(WebConfig.class);
ac.refresh();
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/"); // 不拦截jsp等静态资源
}
}
配置类
package com.morris.spring.mvc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
// 只扫描@Controller注解
@Configuration
@ComponentScan(value = "com.morris.spring.mvc.controller", useDefaultFilters = false, includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
})
public class WebConfig {
}
Controller类
package com.morris.spring.mvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("hello")
public class HelloController {
@RequestMapping("index")
@ResponseBody
public String hello() {
return "hello morris";
}
}
启动类
package com.morris.spring.mvc;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;
import javax.servlet.ServletException;
import java.io.File;
public class Application {
public static void main(String[] args) throws LifecycleException, ServletException {
Tomcat tomcat = new Tomcat(); // 创建Tomcat容器
tomcat.setPort(8080); // 端口号设置
String basePath = System.getProperty("user.dir");
tomcat.setBaseDir(basePath);
//改变文件读取路径,从resources目录下去取文件
StandardContext ctx = (StandardContext) tomcat.addWebapp("/", basePath + File.separator + "spring-mvc-demo/src" + File.separator + "main" + File.separator + "resources");
// 禁止重新载入
ctx.setReloadable(false);
// class文件读取地址
File additionWebInfClasses = new File(basePath, "spring-mvc-demo/build/classes/java/main");
// 创建WebRoot
WebResourceRoot resources = new StandardRoot(ctx);
// tomcat内部读取Class执行
resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", additionWebInfClasses.getAbsolutePath(), "/"));
ctx.setResources(resources);
tomcat.start();
// 异步等待请求执行
tomcat.getServer().await();
}
}
浏览器输入http://localhost:8080/hello/index即可看到效果。
实现AbstractAnnotationConfigDispatcherServletInitializer接口创建父子容器
AbstractAnnotationConfigDispatcherServletInitializer是WebApplicationInitializer的子类。
AbstractAnnotationConfigDispatcherServletInitializer会创建出两个容器出来:
- ServletApplicationContext(子容器):负责管理跟Web相关的Bean,如Controller
- RootApplicationContext(父容器):负责管理跟Service、Dao层相关的Bean
MyWebAppInitializer
package com.morris.spring.mvc.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* 实现AbstractAnnotationConfigDispatcherServletInitializer接口实现父子容器
*/
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
MyWebAppInitializer中需要配置/
,而不是/\*
。/
和/\*
到底有啥区别?
-
/
:拦截所有请求(包括静态资源(xx.js,xx.png)),但是不包括*.jsp -
/\*
:拦截所有请求;连*.jsp页面都拦截;jsp页面是tomcat的jsp引擎解析的(所以使用这个jsp就无法访问了)
RootConfig
package com.morris.spring.mvc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
// 扫描除@Controller注解的其他@Component注解
scan which class annotated by @Component and exculde @Controller
@Configuration
@ComponentScan(basePackages = "com.morris.spring.mvc", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
})
public class RootConfig {
}
无xml的原理分析
从servlet3.0开始,web项目中可以不需要配置web.xml,WEB容器在启动时会实例化jar中所有的/META-INF/services/javax.servlet.ServletContainerInitializer
文件中的类,然后调用这些类的onStartup()方法。
SpringServletContainerInitializer
spring-web项目中实现了上面的spi机制,它在其中配置的类为org.springframework.web.SpringServletContainerInitializer:
// 加载WebApplicationInitializer所有的实现(包括子接口,抽象类,类)
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
// webAppInitializerClasses有如下几个:
// AbstractAnnotationConfigDispatcherServletInitializer
// AbstractDispatcherServletInitializer
// AbstractContextLoaderInitializer
// AbstractReactiveWebInitializer
// MyWebAppInitializer
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 {
// 其他的都是抽象的,只有MyWebAppInitializer才能进入这里,反射实例化
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) {
/**
* @see org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#onStartup(ServletContext)
*/
// 调用WebApplicationInitializer.onStartup
initializer.onStartup(servletContext);
}
}
}
从SpringServletContainerInitializer的onStartup()可以发现,springmvc启动时会查找所有实现了WebApplicationInitializer的类,并实例化,最后调用其onStartup()方法。
WebApplicationInitializer
第一个例子中实现的就是WebApplicationInitializer接口。
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
WebApplicationInitializer只有一个方法onStartup()。
下面再来看看WebApplicationInitializer的实现类:
- AbstractContextLoaderInitializer
- AbstractDispatcherServletInitializer
- AbstractAnnotationConfigDispatcherServletInitializer
AbstractContextLoaderInitializer
从类名可以看出AbstractContextLoaderInitializer是一个ContextLoader初始化器,那么它是怎么初始化ContextLoader的呢?
public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {
public void onStartup(ServletContext servletContext) throws ServletException {
registerContextLoaderListener(servletContext);
}
protected void registerContextLoaderListener(ServletContext servletContext) {
// 创建父容器
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
// 注入ContextLoaderListener
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");
}
}
}
AbstractContextLoaderInitializer使用ServletContext向Web容器中注入了一个Listener(ContextLoaderListener),相当于以前在web.xml中这样配置:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
AbstractDispatcherServletInitializer
从类名可以看出AbstractDispatcherServletInitializer是一个DispatcherServlet初始化器,那么它是怎么初始化DispatcherServletInitializer的呢?
// DispatcherServlet初始化器
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext); // 创建ContextLoaderListener
registerDispatcherServlet(servletContext); // 创建DispatcherServlet
}
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");
/**
* @see AbstractAnnotationConfigDispatcherServletInitializer#createServletApplicationContext()
*/
WebApplicationContext servletAppContext = createServletApplicationContext(); // 创建子容器,抽象方法
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
// 创建DispatcherServlet
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()); // 默认为true,支持异步
Filter[] filters = getServletFilters(); // 过滤器,钩子方法,默认为null
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
// 添加filter到servlet容器中
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
}
AbstractDispatcherServletInitializer使用ServletContext向Web容器中注入了一个Servlet(DispatcherServlet),如果子类实现了getServletFilters()并添加了filter,也会将这些filter注入到Web容器中。
相当于web.xml这样配置:
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
AbstractAnnotationConfigDispatcherServletInitializer
AbstractAnnotationConfigDispatcherServletInitializer基于模板方法模式提供了两个钩子方法供子类通过注解配置就能实现父子容器。
// 基于注解配置的DispatcherServlet初始化器
public abstract class AbstractAnnotationConfigDispatcherServletInitializer
extends AbstractDispatcherServletInitializer {
protected WebApplicationContext createRootApplicationContext() {
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
// 根据配置类创建父容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(configClasses);
return context;
}
else {
return null;
}
}
protected WebApplicationContext createServletApplicationContext() {
// 根据配置类创建子容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
context.register(configClasses);
}
return context;
}
protected abstract Class<?>[] getRootConfigClasses();
protected abstract Class<?>[] getServletConfigClasses();
}
总结:只需要继承抽象类AbstractAnnotationConfigDispatcherServletInitializer实现其抽象方法就可以构建一个父子容器,也就是我们上面实现的MyWebAppInitializer类。