• SpringBoot 统一功能处理


    目录

    一、用户登录权限验证

    1.1 SpringAOP可以进行处理吗?

    1.2 创建自定义拦截器

     1.3 将自定义拦截器配置到系统配置项中

    1.4 拦截器的实现原理

    1.4.1 实现原理源码分析

    1.5 统一访问前缀添加

    二、统一异常处理

    2.1 为什么需要使用统一异常处理?

    2.2 统一异常处理的实现

    三、统一数据返回格式

    3.1 为什么需要统一数据返回格式?

    3.2 统一数据返回格式的实现

     3.3 返回值为String类型时,应该如何处理?

    3.3.1 将 StringHttpMessageConverter 去掉。

    3.3.2 在统一数据返回的时候,单独处理String类型,让其返回一个String字符串,而非 HashMap

     总结:


    前言:

    一般Spring Boot统一功能处理模块,也是AOP的实战环节,要实现课程目标有以下3个:

    • 统一用户登录权限验证
    • 统一数据格式
    • 统一异常处理

    一、用户登录权限验证

    1.1 SpringAOP可以进行处理吗?

    1. import org.aspectj.lang.ProceedingJoinPoint;
    2. import org.aspectj.lang.annotation.*;
    3. import org.springframework.stereotype.Component;
    4. @Aspect
    5. @Component
    6. public class UserAspect {
    7. // 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法
    8. @Pointcut("execution(* com.example.demo.controller..*.*(..))")
    9. public void pointcut(){ }
    10. // 前置⽅法
    11. @Before("pointcut()")
    12. public void doBefore(){
    13. }
    14. // 环绕⽅法
    15. @Around("pointcut()")
    16. public Object doAround(ProceedingJoinPoint joinPoint){
    17. Object obj = null;
    18. System.out.println("Around ⽅法开始执⾏");
    19. try {
    20. // 执⾏拦截⽅法
    21. obj = joinPoint.proceed();
    22. } catch (Throwable throwable) {
    23. throwable.printStackTrace();
    24. }
    25. System.out.println("Around ⽅法结束执⾏");
    26. return obj;
    27. }
    28. }

    我们知道SpringAOP虽然就提供了对用户登录的处理逻辑,但是存在一些问题:

    • 没有办法获取HttpSession对象
    • 如果要对一部分方法拦截,一部分方法不拦截,这种情况很难处理。(比如注册和登录方法在用户登录权限验证中是不能进行拦截的)

    拦截器和SpringAOP虽然都是AOP的实现方式,但是这两个其实是完全不同的技术体系。

    Spring提供了具体的实现拦截器:HandlerInterceptor,该SpringBoot 拦截器实现分为以下两个步骤:

    1. 自定义拦截器
    2. 将自定义拦截器配置到系统配置项,并且设置合理的拦截规则

    1.2 创建自定义拦截器

    自定义拦截器继承HandlerInterceptor后,需要重写相对应的方法,这里我们重写 preHandle方法:

     代码如下:

    1. package com.example.demo.common;
    2. import org.springframework.web.servlet.HandlerInterceptor;
    3. import javax.servlet.http.HttpServletRequest;
    4. import javax.servlet.http.HttpServletResponse;
    5. import javax.servlet.http.HttpSession;
    6. /**
    7. * 自定义拦截器
    8. */
    9. public class LoginInterceptor implements HandlerInterceptor {
    10. /**
    11. * 以下方法是调用目标方法之前执行的方法。此方法返回boolean类型的值
    12. * 返回true标识验证成功,程序会继续执行后续流程
    13. * 返回false, 表示拦截器拦截失败, 验证未通过, 后续的流程和目标方法不再执行。
    14. * @param request
    15. * @param response
    16. * @param handler
    17. * @return
    18. * @throws Exception
    19. */
    20. @Override
    21. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    22. // 用户登录判断业务
    23. HttpSession session = request.getSession(false);
    24. if (session != null && session.getAttribute("session_userinfo") != null) {
    25. // 用户已经登录
    26. return true;
    27. }
    28. return false;
    29. }
    30. }

     1.3 将自定义拦截器配置到系统配置项中

     重写addInterceptors方法:

     设置拦截规则,代码如下

    1. package com.example.demo.config;
    2. import com.example.demo.common.LoginInterceptor;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    6. public class MyConfig implements WebMvcConfigurer {
    7. @Autowired
    8. LoginInterceptor loginInterceptor;
    9. @Override
    10. public void addInterceptors(InterceptorRegistry registry) {
    11. registry.addInterceptor(loginInterceptor)
    12. .addPathPatterns("/**") //拦截所有的url
    13. .excludePathPatterns("user/login") // url为:user/login 不进行拦截 以下同理
    14. .excludePathPatterns("user/reg")
    15. .excludePathPatterns("image/**"); // image夹目录下的所有url都不进行拦截
    16. }
    17. }

    或者使用Spring方法,通过DI注入的方式,这样可以实现不用new一个实例:

    首先需要将拦截器添加到spring中,也就是给其添加一个五大类注解,这里我们就使用@Component。接着就可以使用@Autowired来得到实例。

     其中:

    • addPathPatterns: 表示需要拦截的URL,“**”表示拦截任意方法(也就是所有方法)。
    • excludePathPatterns: 表示需要排除的URL。

     说明:以上拦截规则可以拦截此项目中的URL,包括静态文件(图片文件,JS和CSS等文件)。

    1.4 拦截器的实现原理

    下面我们先来看一组正常情况下的调用顺序:

    然而有了拦截器之后,会在调用Controller之前进行相应的业务处理,执行的流程如下图所示:

    1.4.1 实现原理源码分析

    所有的Controller执行都会通过一个调度器DispatcherServlet来实现,这一点可以从Spring Boot控制台的打印信息看出,如下图所示:

    在IDEA中,通过全局搜索doDispatch,方法代码如下:

    1. protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    2. HttpServletRequest processedRequest = request;
    3. HandlerExecutionChain mappedHandler = null;
    4. boolean multipartRequestParsed = false;
    5. WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    6. try {
    7. try {
    8. ModelAndView mv = null;
    9. Object dispatchException = null;
    10. try {
    11. processedRequest = this.checkMultipart(request);
    12. multipartRequestParsed = processedRequest != request;
    13. mappedHandler = this.getHandler(processedRequest);
    14. if (mappedHandler == null) {
    15. this.noHandlerFound(processedRequest, response);
    16. return;
    17. }
    18. HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
    19. String method = request.getMethod();
    20. boolean isGet = HttpMethod.GET.matches(method);
    21. if (isGet || HttpMethod.HEAD.matches(method)) {
    22. long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
    23. if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
    24. return;
    25. }
    26. }
    27. if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    28. return;
    29. }
    30. // 实现Controller的业务逻辑
    31. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    32. if (asyncManager.isConcurrentHandlingStarted()) {
    33. return;
    34. }
    35. this.applyDefaultViewName(processedRequest, mv);
    36. mappedHandler.applyPostHandle(processedRequest, response, mv);
    37. } catch (Exception var20) {
    38. dispatchException = var20;
    39. } catch (Throwable var21) {
    40. dispatchException = new NestedServletException("Handler dispatch failed", var21);
    41. }
    42. this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
    43. } catch (Exception var22) {
    44. this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
    45. } catch (Throwable var23) {
    46. this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
    47. }
    48. } finally {
    49. if (asyncManager.isConcurrentHandlingStarted()) {
    50. if (mappedHandler != null) {
    51. mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
    52. }
    53. } else if (multipartRequestParsed) {
    54. this.cleanupMultipart(processedRequest);
    55. }
    56. }
    57. }

    观察DispatcherServlet中的某段代码:

    我们发现,在执行后续的Controller代码之前,都会先执行这个applyPreHandle方法,于是鼠标双击 applyPreHandle,得到代码如下:

    从上述源码可以看出,在applyPreHandle中会获取所有的拦截器HandlerInterceptor并执行拦截器中的preHandle方法,这样就和之前定义的拦截器对应上了,如下图所示:

    1. package com.example.demo.common;
    2. import org.springframework.stereotype.Component;
    3. import org.springframework.web.servlet.HandlerInterceptor;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletResponse;
    6. import javax.servlet.http.HttpSession;
    7. /**
    8. * 自定义拦截器
    9. */
    10. @Component
    11. public class LoginInterceptor implements HandlerInterceptor {
    12. /**
    13. * 以下方法是调用目标方法之前执行的方法。此方法返回boolean类型的值
    14. * 返回true标识验证成功,程序会继续执行后续流程
    15. * 返回false, 表示拦截器拦截失败, 验证未通过, 后续的流程和目标方法不再执行。
    16. * @param request
    17. * @param response
    18. * @param handler
    19. * @return
    20. * @throws Exception
    21. */
    22. @Override
    23. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    24. // 用户登录判断业务
    25. HttpSession session = request.getSession(false);
    26. if (session != null && session.getAttribute("session_userinfo") != null) {
    27. // 用户已经登录
    28. return true;
    29. }
    30. response.setContentType("application/json");
    31. response.setCharacterEncoding("utf8");
    32. response.getWriter().println("asdasd");
    33. return false;
    34. }
    35. }

    1.5 统一访问前缀添加

    所有请求地址添加api前缀:

    代码如下:

    1. package com.example.demo.config;
    2. import org.springframework.context.annotation.Configuration;
    3. import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
    4. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    5. @Configuration
    6. public class AppConfig implements WebMvcConfigurer {
    7. @Override
    8. public void configurePathMatch(PathMatchConfigurer configurer) {
    9. configurer.addPathPrefix("fox", c -> true);
    10. }
    11. }

    二、统一异常处理

    2.1 为什么需要使用统一异常处理?

    通俗来讲,统一异常处理的主要目的是为了方便前端,让其更好的处理后端的信息,尽量将逻辑处理这块放置于后端,前端的目的其实主要是为用户服务。

    比如:可以跟前端约定出现异常报错时候的状态码是多少,这样方便前端的处理,也方便后续后端在日志文件中将其找到,并修改异常。

    2.2 统一异常处理的实现

    统一异常处理使用的是@ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice表示控制器通知类, @ExceptionHandler是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件,具体实现代码如下:

    以上方法表示,如果出现了异常就返回给前端一个HashMap对象, 其中包含的字段如代码定义那样。

     注意:

    方法名和返回值都是可以自定义的,另外 @ExceptionHandler()中的参数是可以选择的,这里是Exception.class:表示的是可以在程序抛出异常的时候执行这里的代码,让其返回数据给前端,如果填入的参数是NullPointerException:那么表示的是当程序出现空指针异常的时候,会执行这里的代码。

    这里的实现逻辑和Java中的异常处理是相似的,如果开发者有对Exception和NullPointerException分别进行了处理,那么当程序出现NullPointerException异常的时候,还是会根据我们写的NullPointerException执行逻辑进行处理,并不会直接走Exception的逻辑。

    示例如下:

    访问页面后效果如下:

    总结:当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配。

    三、统一数据返回格式

    3.1 为什么需要统一数据返回格式?

    统一数据返回格式的优点如下,比如以下几个:

    • 方便前端程序员更好的接受和解析后端数据接口的数据
    • 降低前端程序员和后端程序员的沟通成本
    • 有利于项目统一数据的维护和修改
    • 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容

    3.2 统一数据返回格式的实现

    统一的数据返回格式可以使用@ControllerAdvice + ResponseBodyAdvice 的方式实现,具体实现代码如下:

    1. package com.example.demo.common;
    2. import org.springframework.core.MethodParameter;
    3. import org.springframework.http.MediaType;
    4. import org.springframework.http.server.ServerHttpRequest;
    5. import org.springframework.http.server.ServerHttpResponse;
    6. import org.springframework.web.bind.annotation.ControllerAdvice;
    7. import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
    8. import java.util.HashMap;
    9. /**
    10. * 统一数据格式处理
    11. */
    12. @ControllerAdvice
    13. public class ResponseAdvice implements ResponseBodyAdvice {
    14. /**
    15. * 是否执行 beforeBodyWrite 方法, 返回 true 就执行, 返回 false 就不执行
    16. * @param returnType
    17. * @param converterType
    18. * @return
    19. */
    20. @Override
    21. public boolean supports(MethodParameter returnType, Class converterType) {
    22. return true;
    23. }
    24. /**
    25. * 返回数据之前进行数据重写
    26. * @param body 原始返回值
    27. * @param returnType
    28. * @param selectedContentType
    29. * @param selectedConverterType
    30. * @param request
    31. * @param response
    32. * @return
    33. */
    34. @Override
    35. public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    36. // 这里我们规定统一的数据返回为HashMap
    37. if (body instanceof HashMap) {
    38. return body;
    39. }
    40. // 重写返回结果,让其返回一个统一的数据格式
    41. HashMap result = new HashMap<>();
    42. result.put("code",200);
    43. result.put("data",body);
    44. result.put("msg","");
    45. return result;
    46. }
    47. }

     访问user/login1:

    经过统一功能处理后代码展现如下:

     3.3 返回值为String类型时,应该如何处理?

     但是如果将返回值改为String类型,按照以上的执行逻辑,那么就无法走上述的正常数据统一处理:

    我们发现,当返回类型为String的时候,程序会抛出异常,从而被我们的 统一异常处理模块拦截。

    观察异常信息,发现 抛出异常:java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String

    可能会感到奇怪,为什么会抛出这个异常呢?

    下面我们来看看后端返回前端时候的执行流程:

    1.  一开始,前端访问该网址时,方法返回的是 String:

    2. 统一数据返回之前会进行处理,将 String 转换为 HashMap:

    3. 将HaspMap转换成 application/json 字符串给前端(接口)

    这个步骤有两种情况,先判断原Body的类型:

    • 是 String 类型,那么就会使用 StringHttpMessageConverter 进行类型转换
    • 如果不是 String 类型,那么使用 HttpMessageConverter 进行类型转换

    以上报错就是因为原始Body是String类型,所以在类型转换时候报错了

    解决方案有如下两种:

    • 将 StringHttpMessageConverter 去掉。
    • 在统一数据返回的时候,单独处理String类型,让其返回一个String字符串,而非HashMap

    3.3.1 将 StringHttpMessageConverter 去掉。

    在配置文件中使用以下代码即可;

    1. package com.example.demo.config;
    2. import org.springframework.context.annotation.Configuration;
    3. import org.springframework.http.converter.HttpMessageConverter;
    4. import org.springframework.http.converter.StringHttpMessageConverter;
    5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    6. import java.util.List;
    7. @Configuration
    8. public class MyConfig implements WebMvcConfigurer {
    9. /**
    10. * 移除 StringHttpMessageConverter
    11. * @param converters
    12. */
    13. @Override
    14. public void configureMessageConverters(List> converters) {
    15. converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
    16. }
    17. }

     访问地址后显示如下:

    3.3.2 在统一数据返回的时候,单独处理String类型,让其返回一个String字符串,而非 HashMap

    引入ObjectMapper(ObjectMapper 是Jackson库中的一个类,用于在Java对象(POJO,Plain Old Java Objects)和JSON数据之间进行相互转换):

    对Body为String进行单独处理: 

    访问页面如下所示:

     总结:

    本文主要介绍了统一用户登录权限的效验,使用WebMvcConfigurer + HandlerInterceptor 来实现。统一异常处理使用 @ControllerAdvice + @ExceptionHandler 来实现,统一返回值处理使用@ControllerAdvice + ResponseBodyAdvice来处理。

  • 相关阅读:
    uboot源码——根目录下的mkconfig文件分析
    工业涂装行业的物联网解决方案
    基于python+django+mysql农业生产可视化系统
    K_A05_004 基于 STM32等单片机驱动2X2块(8X8)点阵模块(MAX7219)显示0-9与中文
    【自然语言处理(NLP)】基于LSTM的命名实体识别(进阶)
    XDOJ-360 结点在二叉排序树的位置
    前端代码上线前验证whistle
    【解刊】3区SCI,25天录用,4天见刊!计算机网络通信领域
    SpringCloud 微服务全栈体系(二)
    古代汉语名词解释
  • 原文地址:https://blog.csdn.net/qq_63218110/article/details/132512556