学习第六篇:【SpringBoot-Labs】 SpringMVC 入门

史意致
2023-12-01

本周(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 ,可以填写 GETPOSTPOSTDELETE 等等。[] 数组,可以填写多个请求方法。如果为空,表示匹配所有请求方法。

    @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(...) 方法,将返回结果包装成自定义的返回对象。
  • 全局异常返回

    • 实现自定义的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 注解,添加在类或方法上,标记该类/方法对应接口的 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 预检查走到拦截器里。
  • 显然,在每个 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 前面
}

 类似资料: