本周(8.21-8.27)将学习芋道 Spring Boot的以下文章:
8.21: 快速入门
8.22:Spring Boot 自动配置原理 、Jar 启动原理
8.23:调试环境、 热部署入门、消除冗余代码 Lombok 入门
**8.24:**对象转换 MapStruct 入门、SpringMVC 入门
8.25: WebFlux 入门、 分布式 Session 入门
8.26:API 接口文档 Swagger 入门、API 接口文档 Swagger Starter 入门
8.27:参数校验 Validation 入门、WebSocket 入门
@Controller
注解,添加在类上,表示这是控制器 Controller 对象
name
属性:该 Controller 对象的 Bean 名字。允许空。@RestController
注解,添加在类上,是 @Controller
和 @ResponseBody
的组合注解,直接使用接口方法的返回结果,经过 JSON/XML 等序列化方式,最终返回。
@RequestMapping
注解,添加在类或方法上,标记该类/方法对应接口的配置信息。
path
属性:接口路径。[]
数组,可以填写多个接口路径。values
属性:和 path
属性相同,是它的别名。method
属性:请求方法 RequestMethod ,可以填写 GET
、POST
、POST
、DELETE
等等。[]
数组,可以填写多个请求方法。如果为空,表示匹配所有请求方法。@RequestMapping
注解的不常用属性,如下:
name
属性:接口名。一般情况下,我们不填写。params
属性:请求参数需要包含值的参数名。可以填写多个参数名。如果为空,表示匹配所有请你求方法。headers
属性:和 params
类似,只是从参数名变成请求头。consumes
属性:和 params
类似,只是从参数名变成请求头的提交内容类型( Content-Type )produces
属性:和 params
类似,只是从参数名变成请求头的( Accept )可接受类型。Spring 给每种请求方法提供了对应的注解:
@GetMapping
注解:对应 @GET
请求方法的 @RequestMapping
注解。@PostMapping
注解:对应 @POST
请求方法的 @RequestMapping
注解。@PutMapping
注解:对应 @PUT
请求方法的 @RequestMapping
注解。@DeleteMapping
注解:对应 @DELETE
请求方法的 @RequestMapping
注解。@RequestParam
注解,添加在方法参数上,标记该方法参数对应的请求参数的信息。属性如下:
name
属性:对应的请求参数名。如果为空,则直接使用方法上的参数变量名。value
属性:和 name
属性相同,是它的别名。required
属性:参数是否必须传。默认为 true
,表示必传。defaultValue
属性:参数默认值。@PathVariable
注解,添加在方法参数上,标记接口路径和方法参数的映射关系。具体的,我们在示例中来看。相比 @RequestParam
注解,少一个 defaultValue
属性。
不建议使用@PathVariable
路径参数,主要原因如下:
@PathVariable
路径参数的 URL ,会带来一定的 SpringMVC 的性能下滑。具体可以看看 《SpringMVC RESTful 性能优化》 文章。SpringMVC 提供了测试框架 MockMvc ,方便快速测试接口,MockMvc 提供了集成测试和单元测试的能力。
@WebMvcTest
注解,并且传入的是 UserController 类,表示要对 UserController 进行单元测试。@WebMvcTest
注解,是包含了 @AutoConfigureMockMvc
的组合注解,所以它会自动化配置我们稍后注入的 MockMvc Bean 对象 mvc
。在后续的测试中,我们会看到都是通过 mvc
调用后端 API 接口。但是!每一次调用后端 API 接口,并不会执行真正的后端逻辑,而是走的 Mock 逻辑。也就是说,整个逻辑,走的是单元测试,只会启动一个 Mock 的 Spring 环境。全局统一返回
@ControllerAdvice
接口,拦截 Controller 的返回结果。#supports(MethodParameter returnType, Class converterType)
方法,返回 true
。表示拦截 Controller 所有 API 接口的返回结果。#beforeBodyWrite(...)
方法,将返回结果包装成自定义的返回对象。全局异常返回
ServiceExceptionEnum
类,定义一系列的业务异常常量,比如code固定为1000,message为登录账号不存在等ServiceException
异常类,并提供传入 serviceExceptionEnum
参数的构造方法。GlobalExceptionHandler
全局异常处理,主要针对不同业务异常做不同的处理。通过@ExceptionHandler(value = 自定义的异常类.class)
来区分处理不同异常。在使用 SpringMVC 的时候,可以使用 HandlerInterceptor ,拦截 SpringMVC 处理请求的过程,自定义前置和处理的逻辑。例如说:
access_token
访问令牌,获得当前用户的信息,记录到 ThreadLocal 中。这样,后续的逻辑,只需要通过 ThreadLocal 就可以获取到用户信息。HandlerInterceptor 接口,定义了三个拦截点。代码如下:
// HandlerInterceptor.java
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
#preHandle(...)
方法,实现 handler
的前置处理逻辑。当返回 true
时,继续后续 handler
的执行;当返回 false
时,不进行后续 handler
的执行。
例如说,判断用户是否已经登录,如果未登录,返回
false
,不进行后续handler
的执行。
#postHandle(...)
方法,实现 handler
的后置处理逻辑。
例如说,在视图 View 在渲染之前,做一些处理。不过因为目前都前后端分离,所以这个后置拦截点,使用的就已经比较少了。
#afterCompletion(...)
方法,整个 handler
执行完成,并且拦截器链都执行完前置和后置的拦截逻辑,实现请求完成后的处理逻辑。注意,只有 #preHandle(...)
方法返回 true
的 HandlerInterceptor 拦截器,才能执行 #afterCompletion(...)
方法,因为这样要算 HandlerInterceptor 执行完成才有效。
例如说,释放资源。比如,清理认证拦截器产生的 ThreadLocal 线程变量,避免“污染”下一个使用到该线程的请求。
又例如说,处理
handler
执行过程中发生的异常,并记录异常日志。不过因为现在一般通过 「5. 全局异常处理」 来处理,所以很少这么做了。再例如说,记录请求结束时间,这样我们就可以计算出整个请求的耗时。
自定义拦截器的使用
// FirstInterceptor.java
public class FirstInterceptor implements HandlerInterceptor {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
logger.info("[preHandle][handler({})]", handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info("[postHandle][handler({})]", handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.info("[afterCompletion][handler({})]", handler, ex);
}
}
// SpringMVCConfiguration.java
@Configuration
public class SpringMVCConfiguration implements WebMvcConfigurer {
@Bean
public FirstInterceptor firstInterceptor() {
return new FirstInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截器一
registry.addInterceptor(this.firstInterceptor()).addPathPatterns("/**");
// 拦截器二
// registry.addInterceptor(this.secondInterceptor()).addPathPatterns("/users/current_user");
// 拦截器三
// registry.addInterceptor(this.thirdInterceptor()).addPathPatterns("/**");
}
}
使用 SpringMVC 来解决跨域。目前一共有三种方案:
@CrossCors
注解,配置每个 API 接口。CorsRegistry.java
注册表,配置每个 API 接口。CorsFilter.java
过滤器,处理跨域请求。@CrossCors
注解,添加在类或方法上,标记该类/方法对应接口的 Cors 信息。
@CrossCors
注解的常用属性,如下:
origins
属性,设置允许的请求来源。[]
数组,可以填写多个请求来源。默认值为 *
。value
属性,和 origins
属性相同,是它的别名。allowCredentials
属性,是否允许客户端请求发送 Cookie 。默认为 false
,不允许请求发送 Cookie 。maxAge
属性,本次预检请求的有效期,单位为秒。默认值为 1800 秒。@CrossCors
注解的不常用属性,如下:
methods
属性,设置允许的请求方法。[]
数组,可以填写多个请求方法。默认值为 GET
+ POST
。allowedHeaders
属性,允许的请求头 Header 。[]
数组,可以填写多个请求来源。默认值为 *
。exposedHeaders
属性,允许的响应头 Header 。[]
数组,可以填写多个请求来源。默认值为 *
。一般情况下,我们在每个 Controller 上,添加 @CrossCors
注解即可。
Vue 常用的网络库 axios ,在发起非简单请求时,会自动先先发起 OPTIONS
“预检”请求,要求服务器确认是否能够这样请求。这样,这个请求就会被 SpringMVC 的拦截器所处理。此时,如果我们的拦截器认为 handler
一定是 HandlerMethod 类型时,就会导致报错此时,有两种解决方案:
handler
是 HandlerMethod 的逻辑,进行修复。OPTIONS
预检查走到拦截器里。显然,在每个 Controller 上配置 @CrossOrigin
注解,是挺麻烦一事。所以更多的情况下,我们会选择配置 CorsRegistry 注册表。
修改 SpringMVCConfiguration 配置类,增加 CorsRegistry 相关的配置。代码如下:
// SpringMVCConfiguration.java
@Override
public void addCorsMappings(CorsRegistry registry) {
// 添加全局的 CORS 配置
registry.addMapping("/**") // 匹配所有 URL ,相当于全局配置
.allowedOrigins("*") // 允许所有请求来源
.allowCredentials(true) // 允许发送 Cookie
.allowedMethods("*") // 允许所有请求 Method
.allowedHeaders("*") // 允许所有请求 Header
// .exposedHeaders("*") // 允许所有响应 Header
.maxAge(1800L); // 有效期 1800 秒,2 小时
}
/**
,从而实现全局 CORS 配置。CorsRegistry#addMapping(String pathPattern)
方法,继续往其中添加 CORS 配置。originns
属性,只填写允许的前端域名地址。在 Spring Web 中,内置提供 CorsFilter 过滤器,实现对 CORS 的处理。其配置方式很简单,既然是 Filter 过滤器,就可以采用 「7通过 Bean 的方式 ,进行配置。所以修改 SpringMVCConfiguration 配置类,增加 CorsFilter 相关的配置。代码如下:
// SpringMVCConfiguration.java
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
// 创建 UrlBasedCorsConfigurationSource 配置源,类似 CorsRegistry 注册表
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 创建 CorsConfiguration 配置,相当于 CorsRegistration 注册信息
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*")); // 允许所有请求来源
config.setAllowCredentials(true); // 允许发送 Cookie
config.addAllowedMethod("*"); // 允许所有请求 Method
config.setAllowedHeaders(Collections.singletonList("*")); // 允许所有请求 Header
// config.setExposedHeaders(Collections.singletonList("*")); // 允许所有响应 Header
config.setMaxAge(1800L); // 有效期 1800 秒,2 小时
source.registerCorsConfiguration("/**", config);
// 创建 FilterRegistrationBean 对象
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(
new CorsFilter(source)); // 创建 CorsFilter 过滤器
bean.setOrder(0); // 设置 order 排序。这个顺序很重要哦,为避免麻烦请设置在最前
return bean;
}
@RequestBody
和 @ResponseBody
两个注解,分别完成请求报(内容)到对象和**对象到响应报文(内容)**的转换,底层这种灵活的消息转换机制,就是 Spring 3.x 中新引入的 HttpMessageConverter ,即消息转换器机制。// SpringMVCConfiguration.java
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建 FastJsonHttpMessageConverter 对象
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
// 自定义 FastJson 配置
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.defaultCharset()); // 设置字符集
fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect); // 剔除循环引用
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
// 设置支持的 MediaType
fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON,
MediaType.APPLICATION_JSON_UTF8));
// 添加到 converters 中
converters.add(0, fastJsonHttpMessageConverter); // 注意,添加到最开头,放在 MappingJackson2XmlHttpMessageConverter 前面
}