SpringBoot—统一功能处理
作者:mmseoamin日期:2023-12-18

SpringBoot—统一功能处理

  • 🔎小插曲(通过一级路由调用多种方法)
  • 🔎使用拦截器实现用户登录权限的统一校验
    • 自定义拦截器
    • 将自定义拦截器添加至配置文件中
    • 拦截器的实现原理
    • 统⼀访问前缀添加
    • 🔎统一异常的处理
    • 🔎统一数据格式的返回
      • 统一数据格式返回的优点
      • 统一数据格式返回的实现
      • 🔎总结

        利用 AOP 的思想对一些特定的功能进行统一的处理, 包括

        • 使用拦截器实现用户登录权限的统一校验
        • 统一异常的处理
        • 统一数据格式的返回

          🔎小插曲(通过一级路由调用多种方法)

          通过一级路由调用多种方法, 需要保证这些方法的请求类型各不相同(GET, POST, PUT…)

          SpringBoot—统一功能处理,在这里插入图片描述,第1张

          SpringBoot—统一功能处理,在这里插入图片描述,第2张

          SpringBoot—统一功能处理,在这里插入图片描述,第3张

          🔎使用拦截器实现用户登录权限的统一校验

          使用 Spring AOP 可以实现统一拦截, 但 Spring AOP 的使用较为复杂, 包括

          1. 定义拦截的规则(切点表达式)较为复杂
          2. 在切面类中拿到 HttpSession 较为复杂

          于是 Pivotal 公司针对上述情况开发出 Spring 拦截器

          Spring 拦截器的使用🍂

          1. 自定义拦截器
            • 实现 HandlerInterceptor 接口
            • 重写 preHandler 方法, 在方法中编写业务代码
            • 将自定义拦截器添加至配置文件中, 并设置拦截的规则

          自定义拦截器

          SpringBoot—统一功能处理,在这里插入图片描述,第4张

          将自定义拦截器添加至配置文件中

          • addPathPatterns, 表示需要拦截的 URL

            (/*表示一级路由, /**表示所有的请求)

          • excludePathPatterns, 表示不需要拦截的 URL

            SpringBoot—统一功能处理,在这里插入图片描述,第5张

            拦截器的实现原理

            调用方法时, 发现 DispatcherServlet

            Dispatcher → 调度器

            SpringBoot—统一功能处理,在这里插入图片描述,第6张

            所有方法都会执行 DispatcherServlet 中的 doDispatch—调度方法

            拦截器(doDispatch)的实现源码🌰

            protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
                 HttpServletRequest processedRequest = request;
                 HandlerExecutionChain mappedHandler = null;
                 boolean multipartRequestParsed = false;
                 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
                 try {
                     try {
                         ModelAndView mv = null;
                         Object dispatchException = null;
                         try {
                             processedRequest = this.checkMultipart(request);
                             multipartRequestParsed = processedRequest != request;
                             mappedHandler = this.getHandler(processedRequest);
                             if (mappedHandler == null) {
                                 this.noHandlerFound(processedRequest, response);
                                 return;
                             }
                             HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                             String method = request.getMethod();
                             boolean isGet = HttpMethod.GET.matches(method);
                             if (isGet || HttpMethod.HEAD.matches(method)) {
                                 long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                                 if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                                     return;
                                 }
                             }
                             if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                                 return;
                             }
                             mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                             if (asyncManager.isConcurrentHandlingStarted()) {
                                 return;
                             }
                             this.applyDefaultViewName(processedRequest, mv);
                             mappedHandler.applyPostHandle(processedRequest, response, mv);
                         } catch (Exception var20) {
                             dispatchException = var20;
                         } catch (Throwable var21) {
                             dispatchException = new NestedServletException("Handler dispatch failed", var21);
                         }
                         this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
                     } catch (Exception var22) {
                         this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
                     } catch (Throwable var23) {
                         this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
                     }
                 } finally {
                     if (asyncManager.isConcurrentHandlingStarted()) {
                         if (mappedHandler != null) {
                             mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                         }
                     } else if (multipartRequestParsed) {
                         this.cleanupMultipart(processedRequest);
                     }
                 }
            }
            

            SpringBoot—统一功能处理,在这里插入图片描述,第7张

            当返回结果为 false 时, 拦截器将不会进行后续操作

            SpringBoot—统一功能处理,在这里插入图片描述,第8张

            applyPreHandle 的源码🌰

            boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
                for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
                    HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
                    if (!interceptor.preHandle(request, response, this.handler)) {
                        this.triggerAfterCompletion(request, response, (Exception)null);
                        return false;
                    }
                }
                return true;
            }
            

            分析源码🍂

            在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor, 并执行 HandlerInterceptor 中的 preHandle 方法

            即自定义拦截器中重写的 preHandle 方法

            SpringBoot—统一功能处理,在这里插入图片描述,第9张

            统⼀访问前缀添加

            统⼀访问前缀的添加有 2 种方式

            1. 重写 configurePathMatch( )
            2. 在配置文件中添加

            重写 configurePathMatch( ) 🍂

            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                configurer.addPathPrefix("/bibubibu", c -> true);
            }
            
            • /bibubibu, 要添加的统一前缀
            • c -> true, 所有请求均添加统一前缀

              SpringBoot—统一功能处理,在这里插入图片描述,第10张

              在配置文件中添加🍂

              server:
                servlet:
                  context-path: /bibubibu
              

              SpringBoot—统一功能处理,在这里插入图片描述,第11张

              🔎统一异常的处理

              统一异常的处理, 利用 2 个注解

              1. @ControllerAdvice → 感知异常
              2. @ExceptionHandler → 处理异常

              未设置异常处理🍂

              SpringBoot—统一功能处理,在这里插入图片描述,第12张

              NullPointerException

              SpringBoot—统一功能处理,在这里插入图片描述,第13张

              ArithmeticException

              SpringBoot—统一功能处理,在这里插入图片描述,第14张

              设置异常处理🍂

              @ControllerAdvice
              @ResponseBody
              public class MyExceptionHandler {
                  /**
                  * 拦截所有空指针异常, 进行统一数据格式的返回
                  * @author bibubibu
                  * @date 2023/7/9
                  */
                  @ExceptionHandler(NullPointerException.class)
                  public HashMap nullPointerException(NullPointerException e) {
                      HashMap map = new HashMap<>();
                      map.put("status", -1);
                      map.put("data", null);
                      map.put("msg", "NullPointerException" + e.getMessage()); // 错误码的描述信息
                      return map;
                  }
                  /**
                  * 拦截所有算数异常, 进行统一数据格式的返回
                  * @author bibubibu
                  * @date 2023/7/9
                  */
                  @ExceptionHandler(ArithmeticException.class)
                  public HashMap arithmeticException(ArithmeticException e) {
                      HashMap map = new HashMap<>();
                      map.put("status", -1);
                      map.put("data", null);
                      map.put("msg", "ArithmeticException" + e.getMessage()); // 错误码的描述信息
                      return map;
                  }
                  /**
                  * 拦截所有异常, 进行统一数据格式的返回
                  * @author bibubibu
                  * @date 2023/7/9
                  */
                  @ExceptionHandler(Exception.class)
                  public HashMap exception(Exception e) {
                      HashMap map = new HashMap<>();
                      map.put("status", -1);
                      map.put("data", null);
                      map.put("msg", "Exception" + e.getMessage());
                      return map;
                  }
              }
              

              SpringBoot—统一功能处理,在这里插入图片描述,第15张

              NullPointerException

              SpringBoot—统一功能处理,在这里插入图片描述,第16张

              ArithmeticException

              SpringBoot—统一功能处理,在这里插入图片描述,第17张

              🔎统一数据格式的返回

              统一数据格式返回的优点

              1. 方便前端程序员更好的接收和解析后端数据接口返回的数据
              2. 降低前端程序员和后端程序员的沟通成本, 按照某个格式实现即可, 因为所有接口都是这样返回的
              3. 有利于项目统一数据的维护和修改
              4. 有利于后端技术部门统一规范的标准制定, 不会出现奇怪的返回内容

              统一数据格式返回的实现

              统一数据格式的返回, 利用注解 @ControllerAdvice + ResponseBodyAdvice(接口) 实现

              未设置统一数据格式的返回🍂

              @RestController
              @RequestMapping("/user")
              public class UserController {
                  @RequestMapping("get-num")
                  public Integer getNumber() {
                      return (int) (Math.random() * 10 + 1);
                  }
              }
              

              SpringBoot—统一功能处理,在这里插入图片描述,第18张

              设置统一数据格式的返回🍂

              1. 自定义类(ResponseAdvice), 添加 @ControllerAdvice 注解
              2. 实现 ResponseBodyAdvice 接口, 重写 supports() 与 beforeBodyWrite()

              supports() 类似于一个开关

              当返回值为 true 时, 开启 beforeBodyWrite() 中编写的相关功能

              当返回值为 false 时, 关闭 beforeBodyWrite() 中编写的相关功能

              @ControllerAdvice
              public class ResponseAdvice implements ResponseBodyAdvice {
                  @Override
                  public boolean supports(MethodParameter returnType, Class converterType) {
                      return true;
                  }
                  @Override
                  public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
                      HashMap map = new HashMap<>();
                      map.put("status", 200);
                      map.put("data", body); // 此处的 Body 是 String 类型会出错
                      map.put("msg", "");
                      return map;
                  }
              }
              

              SpringBoot—统一功能处理,在这里插入图片描述,第19张

              当方法的返回值类型为 String 时

              @RestController
              @RequestMapping("/user")
              public class UserController {
                  @RequestMapping("/get-user")
                  public String getUser() {
                      System.out.println("执行 getUser()");
                      return "getUser~~";
                  }
              }
              

              当调用方法的返回值类型为 String 时, 设置统一数据格式的返回🍂

              SpringBoot—统一功能处理,在这里插入图片描述,第20张

              类型转换异常

              SpringBoot—统一功能处理,在这里插入图片描述,第21张

              解决方法

              当调用方法的返回值类型为 String 时, 利用 jackson 完成类型转换

              @ControllerAdvice
              public class ResponseAdvice implements ResponseBodyAdvice {
                  // 利用 jackson 转换 String
                  @Autowired
                  private ObjectMapper objectMapper;
                  @Override
                  public boolean supports(MethodParameter returnType, Class converterType) {
                      return true;
                  }
                  @Override
                  public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
                      HashMap map = new HashMap<>();
                      map.put("status", 200);
                      map.put("data", body); // 此处的 Body 是 String 类型会出错
                      map.put("msg", "");
                      // 判断 Body 是否为 String 类型
                      if(body instanceof String) {
                          // 是 String 类型, 将 map 转换为 Json 格式
                          try {
                              return objectMapper.writeValueAsString(map);
                          } catch (JsonProcessingException e) {
                              e.printStackTrace();
                          }
                      }
                      return map;
                  }
              }
              

              SpringBoot—统一功能处理,在这里插入图片描述,第22张

              SpringBoot—统一功能处理,在这里插入图片描述,第23张

              🔎总结

              1. 用户登录权限的统一校验 → 实现 HandlerInterceptor 接口 + 重写 preHandler 方法 + 将自定义拦截器添加至配置文件中(实现 WebMvcConfigurer 接口)
              2. 统一访问前缀的添加 → 重写 configurePathMatch( ) / 在配置文件中添加
              3. 统一异常的处理 → 利用注解 @ControllerAdvice + @ExceptionHandler
              4. 统一数据格式的返回 → 利用注解 @ControllerAdvice + 实现接口 ResponseBodyAdvice

              🌸🌸🌸完结撒花🌸🌸🌸

              SpringBoot—统一功能处理,在这里插入图片描述,第24张