一、WebMvcConfigurer接口
Spring的WebMvcConfigurer接口提供了很多方法让我们来定制SpringMVC的配置。Spring提供了WebMvcConfigurerAdapter让我们更加优化的去进行配置,不过,Spring 5后WebMvcConfigurerAdapter已过期,不建议使用,我们可以直接实现WebMvcConfigurer接口进行定制化(可以参考WebMvcConfigurationSupport)。
configurePathMatch:匹配路由请求规则
- setUseSuffixPatternMatch : 设置是否是后缀模式匹配,如“/user”是否匹配/user.*,默认为true
- setUseTrailingSlashMatch : 设置是否自动后缀路径模式匹配,如“/user”是否匹配“/user/”,默认为true
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
super.configurePathMatch(configurer);
/*
* 1.ServletMappings 设置的是 "/" 2.setUseSuffixPatternMatch默认设置为true,
* 那么,"/user" 就会匹配 "/user.*",也就是说,"/user.html" 的请求会被 "/user" 的 Controller所拦截.
* 3.如果该值为false,则不匹配
*/
configurer.setUseSuffixPatternMatch(false);
/*
* setUseTrailingSlashMatch的默认值为true
* 也就是说, "/user" 和 "/user/" 都会匹配到 "/user"的Controller
*/
configurer.setUseTrailingSlashMatch(true);
}
addFormatters:注册自定义的Formatter和Convert
@Bean
public EnumConverterFactory enumConverterFactory() {
return new EnumConverterFactory();
}
/**
* 注册自定义的Formatter和Convert,例如, 对于日期类型,枚举类型的转化.
* 不过对于日期类型,使用更多的是使用
*
* @DateTimeFormat(pattern = "yyyy-MM-dd")
* private Date createTime;
*/
@Override
public void addFormatters(FormatterRegistry registry) {
super.addFormatters(registry);
registry.addConverterFactory(enumConverterFactory());
}
/**
* SpringMVC支持绑定枚举值参数。
* 匹配规则 :
* 字符串则尝试根据Enum#name()转换。
* 如果找不到匹配的则返回null
*/
public class EnumConverterFactory implements ConverterFactory<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new String2EnumConverter(targetType);
}
class String2EnumConverter<T extends Enum<T>> implements Converter<String, T> {
private Class<T> enumType;
private String2EnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(String source) {
if (source != null && !source.isEmpty()) {
try {
return Enum.valueOf(enumType, source);
} catch (Exception e) {
}
}
return null;
}
}
}
addArgumentResolvers:添加自定义方法参数处理器
addViewControllers:添加自定义视图控制器
@Override
public void addViewControllers(ViewControllerRegistry registry) {
super.addViewControllers(registry);
// 对 "/hello" 的 请求 redirect 到 "/home"
registry.addRedirectViewController("/hello", "/home");
// 对 "/admin/**" 的请求 返回 404 的 http 状态
registry.addStatusController("/admin/**", HttpStatus.NOT_FOUND);
// 将 "/home" 的 请求响应为返回 "home" 的视图
registry.addViewController("/home").setViewName("home");
}
addResourceHandlers:添加静态资源处理器
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
String os = System.getProperty("os.name");
//如果是Windows系统
if (os.toLowerCase().startsWith("win")) {
registry.addResourceHandler("/app_file/**")
// /app_file/**表示在磁盘filePathWindow目录下的所有资源会被解析为以下的路径
.addResourceLocations("file:" + filePathWindow);
} else { //linux 和mac
registry.addResourceHandler("/app_file/**")
.addResourceLocations("file:" + filePathLinux) ;
}
super.addResourceHandlers(registry);
}
addInterceptors(InterceptorRegistry registry):添加拦截器配置
Spring MVC的拦截器(Interceptor)不是Filter,同样可以实现请求的预处理、后处理。使用拦截器仅需要两个步骤:
- 实现拦截器
- 注册拦截器
实现拦截器可以自定义实现HandlerInterceptor接口,也可以通过继承HandlerInterceptorAdapter类,后者是前者的实现类。
HandlerInterceptor接口中定义了三个方法:
- boolean preHandle() 该方法在处理请求之前进行调用,就是在执行Controller的任务之前。如果返回true就继续往下执行,返回false就放弃执行。
- void postHandle()该方法将在请求处理之后,DispatcherServlet进行视图返回渲染之前进行调用,可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。
- void afterCompletion()该方法也是需要当前对应的Interceptor的preHandle方法的返回值为true时才会执行,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。用于进行资源清理。
为了使自定义的拦截器生效,需要注册拦截器到spring容器中,具体的做法是实现WebMvcConfigurer类,重写其addInterceptors(InterceptorRegistry registry)方法。最后别忘了把Bean注册到Spring容器中,可以选择@Component 或者 @Configuration。
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*调用我们创建的SessionInterceptor。
* addPathPatterns("/api/**)的意思是这个链接下的都要进入到SessionInterceptor里面去执行
* excludePathPatterns("/login")的意思是login的url可以不用进入到SessionInterceptor中,直接
* 放过执行。
*
* 注意:如果像注释那样写是不可以的。这样等于是创建了多个Interceptor。而不是只有一个Interceptor
* 所以这里有个大坑,搞了很久才发现问题。
*
* */
SessionInterceptor sessionInterceptor=new SessionInterceptor();
registry.addInterceptor(sessionInterceptor).addPathPatterns("/api/**")
.excludePathPatterns("/login","/verify");
// registry.addInterceptor(sessionInterceptor).excludePathPatterns("/login");
// registry.addInterceptor(sessionInterceptor).excludePathPatterns("/verify");
super.addInterceptors(registry);
}
configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer):配置默认静态资源处理器
/**
* 使用默认servlet处理静态资源
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
//启用默认servlet支持
configurer.enable();
}
configureViewResolvers(ViewResolverRegistry registry):配置视图解析器
从方法名称我们就能看出这个方法是用来配置视图解析器的,该方法的参数ViewResolverRegistry 是一个注册器,用来注册你想自定义的视图解析器等。ViewResolverRegistry 常用的几个方法:
① enableContentNegotiation:会创建一个内容裁决解析器ContentNegotiatingViewResolver ,该解析器不进行具体视图的解析,而是管理你注册的所有视图解析器,所有的视图会先经过它进行解析,然后由它来决定具体使用哪个解析器进行解析。具体的映射规则是根据请求的media types来决定的。
② jsp:该方法会注册一个内部资源视图解析器InternalResourceViewResolver 显然访问的所有jsp都是它进行解析的。该方法参数用来指定路径的前缀和文件后缀,如:registry.jsp("/WEB-INF/jsp/", ".jsp");
对于以上配置,假如返回的视图名称是example,它会返回/WEB-INF/jsp/example.jsp
给前端,找不到则报404。
③ beanName:该方法会注册一个BeanNameViewResolver视图解析器,这个解析器是干嘛的呢?它主要是将视图名称解析成对应的bean。什么意思呢?假如返回的视图名称是example,它会到spring容器中找有没有一个叫example的bean,并且这个bean是View.class类型的?如果有,返回这个bean。
④ viewResolver:用来注册各种各样的视图解析器的,包括自己定义的。
configureContentNegotiation(ContentNegotiationConfigurer configurer):添加内容协商配置
内容协商机制这个太专业的名称,说下来,头一次听的话,估计是无法理解他其中的含义的。
用大白话讲就是:客户端向服务端发送一个请求,然后服务端给客户端返回什么格式的数据的,是需要两端进行协商的,既然是协商,那么他们有什么协议或者规则呢?
一般现在服务端返回的数据基本都是json格式的数据,以前返回的是xml,那么现在如果要返回xml格式的数据,springmvc也是提供得有方法的。
@RequestMapping系列注解中produces可以指定返回得格式,(@GetMapping是@RequestMapping得一种变形),写法如下:
@GetMapping(value = “xxx”,produces = MediaType.APPLICATION_XML_VALUE)
注意:在返回的实体中指定**@XmlRootElement(name = “xxx”)**,指定xml的根元素。
@GetMapping(value = "getUser",produces = MediaType.APPLICATION_XML_VALUE)
public User getUser(){
User user = new User();
user.setName("我是admin用户");
return user;
}
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "user")
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
最后得到的结果就是:
<user>
<name>我是admin用户</name>
</user>
但是这只是服务端指定的,客户端不能够决定,如果客户端请求这个路径需要换成json格式的数据,那么是做不到的,所以就有了内容协商机制,也就是可以相互协商,客户端需要什么杨的格式,给个参数说明,服务端就可以返回客户端需要的数据了。
在springmvc中,我们需要达到内容协商机制,那么就需要覆盖WebMvcConfigurer中的configureContentNegotiation方法:
package com.osy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfiguration {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 是否通过请求参数来决定返回数据,默认为false
configurer.favorParameter(true)
.ignoreAcceptHeader(true) // 不检查Accept请求头
.parameterName("zyMediaType")// 参数名称,就是通过什么样的参数来获取返回值
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json",MediaType.APPLICATION_JSON)
.mediaType("xml",MediaType.APPLICATION_XML);
// mediaType此方法是从请求参数扩展名(也就是最后一个.后面的值),
// 然后绑定在parameterName上面,比如/admin/getUser.xml 等同于/admin/getUser?zyMediaType=xml
// 如果不需要这种后缀的,那么就是全部通过参数的方式传递到后台
}
};
}
}
然后我们启动项目,随意创建一个控制器:(注意,返回的实体中,在类上面必须打上@XmlRootElement注解,不然在请求xml格式的时候会报错):
@GetMapping(value = "testMediaType")
public User testMediaType(){
User user = new User();
user.setName("testMediaType");
return user;
}
实体还是上面的实体。
① 先通过url扩展名的方式访问
http://localhost:8080/admin/testMediaType.json
{"name":"testMediaType"}
② http://localhost:8080/admin/testMediaType.xml
<user>
<name>testMediaType</name>
</user>
虽然xml格式的数据现在很少用了,但是有些特殊的场景还是需要的,get这么一个技能,就可以写一个接口符合两种场景了,就无需为某一种格式的数据单独的写接口了。
configureAsyncSupport(AsyncSupportConfigurer configurer):处理异步请求
只能设置两个值,一个超时时间(毫秒,Tomcat下默认是10000毫秒,即10秒),还有一个是AsyncTaskExecutor,异步任务执行器。
addCorsMappings(CorsRegistry registry)
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowCredentials(true)
.allowedHeaders("*")
.maxAge(3600);
}
但是使用此方法配置之后再使用自定义拦截器时跨域相关配置就会失效。
原因是请求经过的先后顺序问题,当请求到来时会先进入拦截器中,而不是进入 Mapping 映射中,所以返回的头信息中并没有配置的跨域信息。浏览器就会报跨域异常。
正确的解决跨域问题的方法时使用 CorsFilter 过滤器。代码如下:
private CorsConfiguration corsConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 请求常用的三种配置,*代表允许所有,也可以自定义属性(比如 header 只能带什么,只能是 post 方式等)
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig());
return new CorsFilter(source);
}
addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers):配置统一返回值的处理器
configureMessageConverters(List<HttpMessageConverter<?>> converters) :添加消息转换器
消息转换器的目标是:HTTP输入请求格式向Java对象的转换;Java对象向HTTP输出请求的转换。有的消息转换器只支持多个数据类型,有的只支持多个输出格式,还有的两者兼备。例如:MappingJackson2HttpMessageConverter可以将Java对象转换为application/json,而ProtobufHttpMessageConverter仅支持com.google.protobuf.Message类型的输入,但是可以输出application/json、application/xml、text/plain和application/x-protobuf这么多格式
在项目中有三种办法配置消息转换器,主要区别是可定制性和易用度的衡量。
① 在Configuration类中加入@Bean定义
@Bean
public ByteArrayHttpMessageConverter byteArrayHttpMessageConverter() {
return new ByteArrayHttpMessageConverter();
}
② 重写configureMessageConverters方法,扩展现有的消息转换器链表;
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ByteArrayHttpMessageConverter());
}
③ 更多的控制,可以重写extendMessageConverters方法,首先清空转换器列表,再加入自定义的转换器。
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.clear();
converters.add(new ByteArrayHttpMessageConverter());
}
springMVC为我们提供了一系列默认的消息转换器:
对于消息转换器的调用,都是在RequestResponseBodyMethodProcessor类中完成的。它实现了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler两个接口,分别实现了处理参数和处理返回值的方法。
而要动用这些消息转换器,需要在特定的位置加上@RequestBody和@ResponseBody。
除了spring提供的9个默认的消息转换器,还可以添加自定义的消息转换器,或者更换消息转换器的实现。
一个自定义消息转换器例子:将json转换器替换为fastjson实现,xml转换器替换为jackson-dataformat-xml实现。
添加依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.8.7</version>
</dependency>
@Bean
public HttpMessageConverters messageConverters(){
//json
FastJsonHttpMessageConverter jsonMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.forName("utf-8"));
jsonMessageConverter.setFastJsonConfig(fastJsonConfig);
List<MediaType> jsonMediaTypes = new ArrayList<>();
jsonMediaTypes.add(MediaType.APPLICATION_JSON);
jsonMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
jsonMessageConverter.setSupportedMediaTypes(jsonMediaTypes);
//xml
MappingJackson2XmlHttpMessageConverter xmlMessageConverter = new MappingJackson2XmlHttpMessageConverter();
xmlMessageConverter.setObjectMapper(new XmlMapper());
xmlMessageConverter.setDefaultCharset(Charset.forName("utf-8"));
List<MediaType> xmlMediaTypes = new ArrayList<>();
xmlMediaTypes.add(MediaType.APPLICATION_XML);
xmlMediaTypes.add(MediaType.TEXT_XML);
xmlMessageConverter.setSupportedMediaTypes(xmlMediaTypes);
return new HttpMessageConverters(Arrays.asList(jsonMessageConverter, xmlMessageConverter));
}
fastJson配置实体调用setSerializerFeatures方法可以配置多个过滤方式,常用的如下:
- WriteNullListAsEmpty :List字段如果为null,输出为[],而非null
- WriteNullStringAsEmpty : 字符类型字段如果为null,输出为"",而非null
- DisableCircularReferenceDetect :消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环)
- WriteNullBooleanAsFalse:Boolean字段如果为null,输出为false,而非null
- WriteMapNullValue:是否输出值为null的字段,默认为false。
extendMessageConverters(List<HttpMessageConverter<?>> converters)
configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers):配置异常解析器
HandlerExceptionResolver顾名思义,就是处理异常的类,接口就一个方法,出现异常之后的回调,四个参数中还携带了异常堆栈信息。
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
我们自定义异常处理类就比较简单了,实现上面的接口,然后将完整的堆栈返回给调用方:
public class SelfExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
String msg = GlobalExceptionHandler.getThrowableStackInfo(ex);
try {
response.addHeader("Content-Type", "text/html; charset=UTF-8");
response.getWriter().append("自定义异常处理!!! \n").append(msg).flush();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public final class GlobalExceptionHandler{
private GlobalExceptionHandler(){}
// 堆栈信息打印方法如下
public static String getThrowableStackInfo(Throwable e) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
e.printStackTrace(new java.io.PrintWriter(buf, true));
String msg = buf.toString();
try {
buf.close();
} catch (Exception t) {
return e.getMessage();
}
return msg;
}
}
仔细观察上面的代码实现,有下面几个点需要注意:
- 为了确保中文不会乱码,我们设置了返回头 response.addHeader("Content-Type", "text/html; charset=UTF-8"); 如果没有这一行,会出现中文乱码的情况
- 我们纯后端应用,不想返回视图,直接想Response的输出流中写入数据返回 response.getWriter().append("自定义异常处理!!! \n").append(msg).flush();; 如果项目中有自定义的错误页面,可以通过返回ModelAndView来确定最终返回的错误页面
- 上面一个代码并不会直接生效,需要注册,可以在WebMvcConfigurer的子类中实现注册,实例如下:
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(0, new SelfExceptionHandler());
}
extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers)