前言
在学习springMVC注解启动启动之前,我们先了解下ServletContainerInitializer
。
在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes等,servlet3.0规范中通过ServletContainerInitializer
的onStartup(Set<Class<?>> var1, ServletContext var2)
实现此功能。就是容器启动是会调用这个方法。
每个框架要使用ServletContainerInitializer
就必须在对应的jar包的META-INF/services
目录创建一个名为javax.servlet.ServletContainerInitializer
的文件,文件内容指定具体的ServletContainerInitializer
实现类。
在spring中,其实现类是org.springframework.web.SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
/**
* 容器启动时就会分别调用每个ServletContainerInitializer的onStartup方法,并将感兴趣的类作为参数传入。
* @param webAppInitializerClasses tomcat会扫描项目中所有的WebApplicationInitializer,通过@HandlesTypes注解,注入WebApplicationInitializer的子类
* @param servletContext servlet上下文
*/
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
//注入的class必须是WebApplicationInitializer的子类,且不是接口和抽象类
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
//实例话符合要求的class,并将其对象放到initializers集合中
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);
//调用initializers集合中的WebApplicationInitializer对象的onStartup方法。
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
简单来说:web容器启动时,会调用ServletContainerInitializer子类的onStartup方法,对于SpringMvc来说,容器启动时会调用其SpringServletContainerInitializer的onStartup方法,进而调用项目中每个WebApplicationInitializer子类的的onStartup方法。
web.xml代码化
说到web.xml代码化,那就不得不要将WebApplicationInitializer,它是web.xml代码化的核心接口
补充:WebApplicationInitializer还有一个直系抽象类AbstractReactiveWebInitializer
AbstractContextLoaderInitializer
public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
registerContextLoaderListener(servletContext);
}
protected void registerContextLoaderListener(ServletContext servletContext) {
//创建SpringIoc容器
//createRootApplicationContext是模版方法
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
//注册ContextLoaderListener监听器,并监听SpingIOC容器是否创建
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
//如果SpingIOC容器创建了,那么就创建根容器
//getRootApplicationContextInitializers是模版方法
listener.setContextInitializers(getRootApplicationContextInitializers());
//把这个监听器放到Servlet上下文中
servletContext.addListener(listener);
}
else {
logger.debug("No ContextLoaderListener registered, as " +
"createRootApplicationContext() did not return an application context");
}
}
//创建SpingIOC容器
protected abstract WebApplicationContext createRootApplicationContext();
@Nullable
protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
return null;
}
这段代码的作用是,创建根容器(一般是springIOC容器),创建监听器到ContextLoaderListener监听根容器,并将其注册到ServletContext中,相当于xml如下
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/自己设置根容器路径.xml</param-value>
</context-param>
....
</web-app>
AbstractDispatcherServletInitializer
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {
//定义servlet名字 <servlet-name>springmvc</servlet-name>
public static final String DEFAULT_SERVLET_NAME = "dispatcher";
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");
//创建web容器
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());
//把dispatcherServlet作为Servlet注册到上下文中
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.");
}
//容器在启动的时候加载这个servlet,其优先级为1(正数的值越小,该servlet的优先级越高,应用启动时就越先加载)
registration.setLoadOnStartup(1);
//设置Servlet映射mapping路径
//getServletMappings()是模版方法,需要我们自己配置
registration.addMapping(getServletMappings());
//设置是否支持异步请求
//isAsyncSupported默认是true
registration.setAsyncSupported(isAsyncSupported());
//处理自定义的Filter进来,一般我们Filter不这么加进来,而是自己@WebFilter,或者借助Spring,
//备注:这里添加进来的Filter都仅仅只拦截过滤上面注册的dispatchServlet
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
//这个很清楚:调用者若相对dispatcherServlet有自己更个性化的参数设置,复写此方法即可
customizeRegistration(registration);
}
protected void customizeRegistration(ServletRegistration.Dynamic registration) {}
//设置过滤器规则
protected Filter[] getServletFilters() {return null;}
//是否支持异步请求
protected boolean isAsyncSupported() {return true;}
//创建web容器
protected abstract WebApplicationContext createServletApplicationContext();
//设置Servlet到mapping的映射
protected abstract String[] getServletMappings();
@Nullable
protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
return null;
}
}
这段代码的作用是,创建web容器,在web容器中创建DispatcherServlet对象对象,并把dispatcherServlet作为Servlet注册到ServletContext中。这段代码作用
<web-app>
....
<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/自己设置web容器路径.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>
..可以设置filter
</web-app>
AbstractAnnotationConfigDispatcherServletInitializer
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;
}
}
//创建web容器
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();
}
案例
SpringIOC容器
//扫描所有类,除了标注注解@Controller,@ControllerAdvice,@RestControllerAdvice的类
@ComponentScan(value = "clyu", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, ControllerAdvice.class, RestControllerAdvice.class})
})
@Configuration
@Configuration
public class SpringIocConfig {}
WEB容器
//只扫描标注注解@Controller,@ControllerAdvice,@RestControllerAdvice的类
@ComponentScan(value = "clyu", useDefaultFilters = false,
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, ControllerAdvice.class, RestControllerAdvice.class})}
)
@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {}
启动类配置
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringIocConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{MyWebMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
useDefaultFilters默认值为true,表示默认情况下@Component、@Repository、@Service、@Controller都会扫描
useDefaultFilters=false加上includeFilters我们就可以只扫描指定的组件了,比如Spring MVC的web子容器只扫描Controller组件
excludeFilters的时候,就不需要去设置useDefaultFilters=false,这样子我们直接排除掉即可~
特别注意
:useDefaultFilters的正确使用,不要造成重复扫描。否则很有可能造成事务不生效,并且你还非常不好定位这个错误
其相当于xml
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/SpringIocConfig.xml</param-value>
</context-param>
<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/MyWebMvcConfig.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>
</web-app>
测试1
@RestController
@RequestMapping("person")
public class PersonController {
@GetMapping("getList")
public String getPerson(){
return "1";
}
}
运行程序,调用路径http://localhost:8080/clyu/person/getList
输出 1
特别注意的是
按照上面的配置,我偶然的发现了,SpringIocConfig仍然还是去扫描了我的controller,导致我的controller被扫描了两次,怎么回事呢???
找了好久,终于找到原因了,并不是@ComponentScan或者excludeFilters的问题,而是因为咱们在执行SpringIOCConfig的时候,虽然不去扫描Controller注解了,但是它会扫描MyWebMvcConfig.java这个配置类,从而间接的又去扫描了@Controller了,因此最正确的做法应该
@ComponentScan(value = "clyu", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, ControllerAdvice.class, RestControllerAdvice.class}),@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {MyWebMvcConfig.class})
})
@Configuration
public class SpringIocConfig {
}
这样子,我们的Controller就只会被扫描一次了,容器也就非常的干净了
测试2
@RestController
@RequestMapping("person")
public class PersonController {
@Autowired
private PersonService personService;
@GetMapping("getList")
public Person getPerson(){
return personService.getPerson();
}
}
运行程序,调用路径http://localhost:8080/clyu/person/getList
输出 , 发现接口报错。当我们在web配置类添加@EnableWebMvc。接口正常输出。
原因浅析:
不开启注解@EnableWebMvc。springMVC项目会默认注册4个消息转换器,他们是
ByteArrayHttpMessageConverter StringHttpMessageConverter,
SourceHttpMessageConverter AllEncompassingFormHttpMessageConverter
开启注解@EnableWebMvc,springMVC项目会默认注册7个消息转换器,他们是
AllEncompassingFormHttpMessageConverter StringHttpMessageConverter,
ByteArrayHttpMessageConverter ResourceHttpMessageConverter,
ResourceRegionHttpMessageConverter SourceHttpMessageConverter
Jaxb2RootElementHttpMessageConverter