如果不使用@EnableWebMvc注解,web项目同样能正确接收请求,只不过使用的是默认配置的一些组件,这些组件都配置在DispatcherServlet.properties文件,如果想修改这些默认的组件或者添加一些新的组件,就需要使用@EnableWebMvc注解开启自定义配置,并注入一个实现了WebMvcConfigurer接口的Bean。
自定义配置的使用
开启MVC配置
要实现SpringMVC自定义配置,需要使用@EnableWebMvc开启MVC配置,并实现WebMvcConfigurer接口重写其中的方法。
package com.morris.spring.mvc.config;
import com.morris.spring.mvc.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import java.util.List;
import java.util.Properties;
@EnableWebMvc // 开启自定义spring mvc配置
@Configuration
public class MVCConfig implements WebMvcConfigurer {
... ...
相当于在spring-mvc.xml中这么配置:
<mvc:annotation-driven/>
配置视图解析器
重写WebMvcConfigurer接口的configureViewResolvers()方法:
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/jsp/", ".jsp");
}
这样配置后所有Controller返回的视图都会加上前缀/jsp/
和后缀.jsp
。
例如Controller如下配置:
package com.morris.spring.mvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/index")
public String index() {
return "index";
}
}
浏览器中请求/index
,spring mvc就会controller返回的视图名称的基础上拼接上前缀和后缀,最后变成/jsp/index.jsp
,然后返回给tomcat进行解析。
相当于以前在spring-mvc.xml中这样配置:
<mvc:view-resolvers>
<mvc:jsp/>
</mvc:view-resolvers>
配置视图控制器
重写WebMvcConfigurer接口的addViewControllers()方法。
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index2").setViewName("index");
}
适用于只返回视图,没有任何业务逻辑。
等价于在Controller中这么配置:
@RequestMapping("/index2")
public String index() {
return "index";
}
静态资源映射器
重写WebMvcConfigurer接口的addResourceHandlers()方法。
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/image/**")
.addResourceLocations("classpath:/static/image/");
}
这样在浏览器输入http://localhost:8080/image/1.png,会去/static/image目录下寻找1.png文件。
默认的Servlet
重写WebMvcConfigurer接口的configureDefaultServletHandling()方法。
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
所有没法处理的URL最后都会被这个默认的Servlet处理,这样配置后,所有的静态资源如css、js等静态资源都能正常返回了。
例如在浏览器输入http://localhost:8080/static/image/1.png,会去/static/image
目录下寻找1.png文件。
相当于以前在spring-mvc.xml中这样配置:
<mvc:default-servlet-handler/>
添加拦截器
重写WebMvcConfigurer接口的addInterceptors方法:
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor());
}
MyInterceptor需要实现HandlerInterceptor接口:
package com.morris.spring.mvc.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
// 目标方法运行之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle" + request.getRequestURI());
return true;
}
// 目标方法运行之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
// 页面响应后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
注意: postHandle对@ResponseBody这种方法没啥用,因为在执行postHandle之前请求就已经返回给浏览器了,如果想在响应中追加一些参数,可以使用ResponseBodyAdvice。
相当于以前在spring-mvc.xml中这样配置:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.morris.spring.mvc.interceptor.MyInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
源码分析
MVCConfig中的配置是怎么加载到Spring MVC中的呢?
@EnableWebMvc
@EnableWebMvc导入了DelegatingWebMvcConfiguration类。
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
DelegatingWebMvcConfiguration
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
// 注入所有的WebMvcConfigurer,这里是MVCConfig类
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
... ...
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}
DelegatingWebMvcConfiguration持有我们实现了WebMvcConfigurer接口的类MVCConfig的引用,DelegatingWebMvcConfiguration中的所有方法的调用都会委托给MVCConfig,再来看一下DelegatingWebMvcConfiguration的方法什么时候被调用。
这里以DelegatingWebMvcConfiguration#addViewControllers为例:
看父类WebMvcConfigurationSupport:
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#viewControllerHandlerMapping
@Bean
@Nullable
public HandlerMapping viewControllerHandlerMapping(
@Qualifier("mvcPathMatcher") PathMatcher pathMatcher,
@Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext);
/**
* @see DelegatingWebMvcConfiguration#addViewControllers
*/
// 模板方法模式
addViewControllers(registry);
// SimpleUrlHandlerMapping
AbstractHandlerMapping handlerMapping = registry.buildHandlerMapping();
if (handlerMapping == null) {
return null;
}
handlerMapping.setPathMatcher(pathMatcher);
handlerMapping.setUrlPathHelper(urlPathHelper);
handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
viewControllerHandlerMapping()方法上面有@Bean注解,就会将方法的返回值注入到springmvc容器中,注入HandlerMapping时会调用子类的addViewControllers(),进而调用WebMvcConfigurer.addViewControllers()来实现自定义配置。
这样在DispatcherServlet#onRefresh初始化九大组件时从容器中取HandlerMapping,不会再读取DispatcherServlet.properties文件。
org.springframework.web.servlet.DispatcherServlet#initHandlerMappings
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) { // 默认为true
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
// 开启@EnableWebmvc后直接从容器中取,不会再读取DispatcherServlet.properties
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
// 读取DispatcherServlet.properties
// 默认BeanNameUrlHandlerMapping、RequestMappingHandlerMapping、RouterFunctionMapping
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
也正因为WebMvcConfigurationSupport中注入了大量的组件,所以有些组件不会再使用DispatcherServlet.properties文件中的默认值了。