本周(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 入门
芋道 Spring Boot SpringMVC 入门
-
@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
路径参数,主要原因如下:- 1、封装的权限框架,基于 URL 作为权限标识,暂时是不支持带有路径参数的 URL 。
- 2、基于 URL 进行告警,而带有路径参数的 URL ,“相同” URL 实际对应的是不同的 URL ,导致无法很方便的实现按照单位时间请求错误次数告警。
- 3、
@PathVariable
路径参数的 URL ,会带来一定的 SpringMVC 的性能下滑。具体可以看看 《SpringMVC RESTful 性能优化》 文章。
-
SpringMVC 提供了测试框架 MockMvc ,方便快速测试接口,MockMvc 提供了集成测试和单元测试的能力。
- 在类上添加
@WebMvcTest
注解,并且传入的是 UserController 类,表示要对 UserController 进行单元测试。 - 同时,
@WebMvcTest
注解,是包含了@AutoConfigureMockMvc
的组合注解,所以它会自动化配置我们稍后注入的 MockMvc Bean 对象mvc
。在后续的测试中,我们会看到都是通过mvc
调用后端 API 接口。但是!每一次调用后端 API 接口,并不会执行真正的后端逻辑,而是走的 Mock 逻辑。也就是说,整个逻辑,走的是单元测试,只会启动一个 Mock 的 Spring 环境。
- 在类上添加
-
全局统一返回
- 在 SpringMVC 中,可以使用通过实现 ResponseBodyAdvice 接口,并添加
@ControllerAdvice
接口,拦截 Controller 的返回结果。 - 实现
#supports(MethodParameter returnType, Class converterType)
方法,返回true
。表示拦截 Controller 所有 API 接口的返回结果。 - 实现
#beforeBodyWrite(...)
方法,将返回结果包装成自定义的返回对象。
- 在 SpringMVC 中,可以使用通过实现 ResponseBodyAdvice 接口,并添加
-
全局异常返回
- 实现自定义的
ServiceExceptionEnum
类,定义一系列的业务异常常量,比如code固定为1000,message为登录账号不存在等 - 实现自定义的
ServiceException
异常类,并提供传入serviceExceptionEnum
参数的构造方法。 - 实现自定义的
GlobalExceptionHandler
全局异常处理,主要针对不同业务异常做不同的处理。通过@ExceptionHandler(value = 自定义的异常类.class)
来区分处理不同异常。
- 实现自定义的
-
在使用 SpringMVC 的时候,可以使用 HandlerInterceptor ,拦截 SpringMVC 处理请求的过程,自定义前置和处理的逻辑。例如说:
- 日志拦截器,记录请求与响应。这样可以知道每一次请求的参数,响应的结果,执行的时长等等信息。
- 认证拦截器,可以解析前端传入的用户标识,例如说
access_token
访问令牌,获得当前用户的信息,记录到 ThreadLocal 中。这样,后续的逻辑,只需要通过 ThreadLocal 就可以获取到用户信息。 - 授权拦截器,可以通过每个 API 接口需要的授权信息,进行判断,当前请求是否允许访问。例如说,用户是否登录,是否有该 API 操作的权限等等。
- 限流拦截器,可以通过每个 API 接口的限流配置,进行判断,当前请求是否超过允许的请求频率,避免恶意的请求,打爆整个系统
-
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. 全局异常处理」 来处理,所以很少这么做了。再例如说,记录请求结束时间,这样我们就可以计算出整个请求的耗时。
-
自定义拦截器的使用
- 首先自定义的拦截器要实现HandlerInterceptor类,并重载preHandle、postHandle、afterCompletion方法
// 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); } }
- 其次在自定义的mvc配置类(实现WebMvcConfigurer)中注入该拦截器,并在重载的addInterceptors方法中,添加自定义的 HandlerInterceptor 拦截器,到 InterceptorRegistry 拦截器注册表中。使用该拦截器对特定url进行拦截
// 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 类型时,就会导致报错此时,有两种解决方案:- 1)检查每个拦截器的实现,是不是依赖于
handler
是 HandlerMethod 的逻辑,进行修复。 - 2)不使用该方案,而是采用 「8.3 CorsFilter」 过滤器,避免
OPTIONS
预检查走到拦截器里。
- 1)检查每个拦截器的实现,是不是依赖于
-
显然,在每个 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 配置。 - 如果想要配置单个路径的 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;
}
- 在 Spring MVC 中,可以使用
@RequestBody
和@ResponseBody
两个注解,分别完成请求报(内容)到对象和**对象到响应报文(内容)**的转换,底层这种灵活的消息转换机制,就是 Spring 3.x 中新引入的 HttpMessageConverter ,即消息转换器机制。 - 使用 Fastjson 作为 JSON 默认的工具类,以提升 JSON 的序列化和反序列化性能。
- 在 Fastjson 中,已经内置提供 FastJsonHttpMessageConverter 消息转换器,方便我们替换 SpringMVC 默认的 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 前面
}