• SpringBoot统一功能处理



    在上一篇博客中介绍了 SpringAOP 的原生操作,此时我们就可以去着手写一个统一处理用户登陆权限验证的功能;我们可以想到使用 SpringAOP 的前置通知方法或者环绕通知方法来进行实现,但是在真正使用原生 SpringAOP 对该功能进行实现时,会有如下问题:

    • 首先是要验证用户的登陆状态,就要先获取到内存中的 Session 对象,但是通过前置或者环绕通知的方式是很难拿到请求对象的,也就很难拿到 Session 对象进行判断。
    • 其次是与我们用户相关的控制器中并非所有方法都要进行拦截判断(像登录、注册方法),那这样就大大增加了通过原生 SpringAOP 的切点表达式配置拦截规则的难度。

    这几个问题也是 SpringAOP 原生操作不常用的原因,相比较下其实是有更好的解决方案的,那就是使用 Spring 拦截器。

    一. Spring拦截器

    Spring 拦截器和传统 AOP 的区别就类似于 Servlet 和 Spring 的区别,拦截器也是将传统 AOP 进行了封装, 内置了reuqestresponse对象,提供了更加方便的功能。

    🍂实现拦截器的两大步骤

    1️⃣第一步,创建自定义拦截器,实现HandlerInterceptor接口并重写preHandle(执行方法前的预处理)方法。

    2️⃣第二步,将自定义拦截器加入WebMvcConfigureraddInterceptors方法中(配置拦截规则)。

    • a) 给当前的类添加@Configuration注解;
    • b) 实现WebMvcConfigurer接口;
    • c) 重写addInterceptors方法。

    1. 自定义拦截器

    我们模拟用户登录验证的场景,这里设定一个登录时 Session 会话的 key 值,设置为一个全局变量。

    package com.example.demo.common;
    /**
     * 全局变量
     */
    public class AppVar {
        // Session Key
        public static final String SESSION_KEY = "SESSION_KEY";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们实现的自定义拦截器其实就是一个普通的类,实现HandlerInceptor接口,并重写preHandle方法;方法如果返回true,就表示拦截器验证成功,会继续走后续的流程,执行目标方法;如果返回false,表示拦截器验证失败,后续的流程和目标方法就不会执行了。

    package com.example.demo.config;
    
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    /**
     * 自定义拦截器
     */
    @Component
    public class LoginInterceptor implements HandlerInterceptor {
        // 调用目标方法执行之前的方法
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 用户登录判断业务
            HttpSession session = request.getSession(false);
            if(session != null && session.getAttribute("session_userinfo") != null) {
                //用户已经登录
                return true;
            }
            // 登录失败, 页面返回一个错误状态码
            response.setStatus(401);
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2. 将自定义拦截器加入到系统配置中

    上面实现的自定义拦截器,只是一个普通的类,我们还需要使用@Configuration将它加入到系统配置中,并实现WebMvcConfigurer接口重写addInterceptors方法配置拦截规则后,才是一个真正有用的拦截器,配置拦截规格涉及到以下方法:

    1. addInterceptor:将自定义拦截器添加到系统配置中。
    2. addPathPatterns:表示需要拦截的 URL。
    3. excludePathPatterns:表示不拦截,需要排除的 URL。

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

    我们规定除了登录和注册功能不拦截外,需要拦截其他所有 URL。

    package com.example.demo.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class AppConfig implements WebMvcConfigurer {
        @Autowired //属性注入 (注入 loginInterceptor 对象)
        private LoginInterceptor loginInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/**") // 拦截所有 url
                    .excludePathPatterns("/api/user/login")  // 排除 url /user/login 登录不拦截
                    .excludePathPatterns("/api/user/reg")    // 注册不拦截
                    .excludePathPatterns("/image/**");  // 排除 image 文件夹下的所有文件
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    完成了上面的步骤后,此时我们可以定义个UserController,来验证我们自定义拦截器的功能:

    @RestController
    @RequestMapping("/user")
    public class UserController {
        @RequestMapping("/login")
        public String login() {
            return "登录成功";
        }
        @RequestMapping("/reg")
        public String reg() {
            return "注册成功";
        }
        @RequestMapping("/index")
        public String index() {
            return "其他方法";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    启动程序,通过浏览器访问 url,下面来进行验证一下拦截器是否生效。

    img

    img

    img

    可以看到其中登录和注册功能都是未拦截成功执行了的,而 index() 方法并没有成功执行,而是被我们自定义的拦截器拦截了,并且设置了状态码为401,都是符合我们的预期的。

    3. 拦截器实现原理

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

    img

    首先我们要知道所有 Controller 的执行都会通过一个调度器DispatcherServlet来实现,随便访问 controller 中的一个方法就能在控制台的打印信息就能看到,这个可以类比到线程的调度上。

    img

    然后所有 Controller 中方法都会执行 DispatcherServlet 中的调度方法doDispatch()

    img

    img

    源码中的关键步骤就是预处理的过程,就和上面代码 LoginInterceptor 拦截器做的事情是一致的,判断拦截的方法是否符合要求,如果符合要求,就返回 true,然后继续执行后续业务代码;否则,后面的代码都不执行。

    img

    进入applyPreHandle()方法继续分析;

    img

    我们发现源码中就是通过遍历存放拦截器的 List,然后不断判断每一个拦截器是否都返回 true 了,但凡其中有一个拦截器返回 false,后面的拦截器都不要走了,并且后面的业务代码也不执行了。

    通过以上内容, 我们可以发现其实 Spring 中的拦截器其实就是封装了传统的 AOP,它也是通过动态代理和环绕通知的思想来实现的。

    4. 统一访问前缀添加

    如果我们想要在所有的请求地址前面加一个地址,我们可以进行以下操作,我们以加前缀api为例。

    @Configuration
    public class MyConfig implements WebMvcConfigurer {
         // 所有的接口添加 api 前缀
         @Override
         public void configurePathMatch(PathMatchConfigurer configurer) {
             configurer.addPathPrefix("api", c -> true);
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们addPathPrefix的第二个参数是一个表达式,设置为true表示启动前缀

    img

    在浏览器输入url,测试结果如下:

    img

    二. 统一异常的处理

    为什么要统一异常的处理呢?

    就拿用户在银行取钱这件事来说,如果用户在办理业务的时候,后端程序报错了,它不返回任何信息, 或者它返回的信息不统一,这都会让前端程序猿不知道咋办,他不知道咋办,那么就无法给用户提供相应的提示;此时用户见程序没反应。他自己也会怀疑是自己没点到,还是程序出 bug 了,所以需要进行统一异常的处理.

    实现统一异常的处理是需要两个注解来实现的:

    • @ControllerAdvice:定义一个全局异常处理类,用于处理在 Controller 层中抛出的各种异常,并对这些异常进行统一的处理;使用 @ControllerAdvice 注解可以将异常处理逻辑从 Controller 中解耦,提高代码复用性。
    • @ExceptionHandler:定义异常处理方法,使用 @ExceptionHandler,可以根据不同类型异常进行处理。

    二者结合表示,当出现异常的时候执行某个通知(执行某个方法事件)。

    1️⃣第一步,建立统一异常处理类,并加入 @ControllerAdvice 注解

    @ControllerAdvice  // 表示这个类将被用于全局的异常处理
    @ResponseBody
    // @RestControllerAdvice // 相当于上面两个注解功能的结合体
    public class ExceptionAdvice {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2️⃣第二步,定义异常处理方法,使用 @ExceptionHandler,可以根据不同类型异常进行处理。

    我们来进行一个空指针异常处理:

    @ControllerAdvice  // 表示这个类将被用于全局的异常处理
    @ResponseBody
    // @RestControllerAdvice // 相当于上面两个注解功能的结合体
    public class ExceptionAdvice {
        // 这个注解表示当应用程序中发生 NullPointerException 时, 会调用此方法进行处理
        @ExceptionHandler(NullPointerException.class)
        // 这个方法返回一个 HashMap, 其中包含了异常处理的结果; 结果以键值对的形式存储, 键是字符串, 值是Object对象。
        public HashMap<String,Object> doNullPointerException(NullPointerException e) {
            HashMap<String ,Object> result = new HashMap<>();
            result.put("code",-1);   // "code": 异常的状态码, 这里是 -1, 表示发生了错误.
            // "msg": 异常的描述, 这里添加了 "空指针" 的前缀,并附加上异常的具体信息 (e.getMessage()获取异常的信息).
            result.put("msg","空指针" + e.getMessage());
            // "data": 这里设置为 null, 表示没有返回的数据.
            result.put("data", null);
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们在 UserController 再添加一个方法,这里我们自己创建一个null对象,用来调用hashCode方法时,就会抛出NullPointerException

    @RequestMapping("/getuser")
    public int getUser() {
        Object obj = null;
        System.out.println(obj.hashCode());
        return 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们来访问 getUser,看是什么情况(此时先将拦截器注掉):

    此时,我们可以发现前端这里并没有直接报错,而是显示出了错误的详细信息。

    img

    为了看出区别,我们来做两个测试:
    🍂①首先我们将 @ControllerAdvice 注解注释掉

    img

    再次访问,我们发现直接报了500的错误信息了,此时前端这里并不会知道服务器具体出了什么错误。

    img

    所以说,异常处理 @ControllerAdvice 注解一定是要有的。

    🍂②再将@ControllerAdvice注解恢复,但如果将空指针异常换成是算术异常有会出现什么情况呢?

    img

    再次访问,我们发现同样还是报了500的错误信息,因为我们统一异常处理只是处理了空指针异常。

    img

    🎯这种情况,我们需要再写一个算术处理异常的处理,但异常的出现又不是我们可以提前预知的,把所有异常都写一遍处理?也行,但很麻烦,没必要;我们可以直接使用所有异常类的父类 Exception 进行异常处理,这样所有的异常都能处理到了。

    @ExceptionHandler(Exception.class)
    public HashMap<String,Object> doException(Exception e) {
        HashMap<String ,Object> result = new HashMap<>();
        result.put("code",-1);
        result.put("msg","Exception" + e.getMessage());
        result.put("data", null);
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们这个方法就相当于是异常处理的最后一道防线,如果异常匹配上了就执行对应的异常处理就可以,如果没有匹配上,就会执行该方法。

    我们再来进行测试:

    img

    三. 统一数据返回格式

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

    1. 方便前端程序员更好的接收和解析后端数据接口返回的数据
    2. 降低前端程序员和后端程序员的沟通成本,按照某个格式进行
    3. 有利于项目统一数据的维护和修改
    4. 后端的统一规范的标准制定

    1. 实现统一数据返回格式的功能

    我们这里可以封装一个数据返回的格式,返回的数据由状态码,状态码的描述信息,返回数据三个部分组成。

    package com.example.demo.common;
    
    import lombok.Data;
    
    /**
     * 统一对象
     */
    @Data
    public class ResultAjax {
        private int code; // 状态码
        private String msg; // 状态码的描述信息
        private Object data; // 返回数据
        
        public static ResultAjax succ(Object data) {
            ResultAjax resultAjax = new ResultAjax();
            resultAjax.setCode(200);
            resultAjax.setMsg("");
            resultAjax.setData(data);
            return resultAjax;
        }
    
        public static ResultAjax succ(String msg, Object data) {
            ResultAjax resultAjax = new ResultAjax();
            resultAjax.setCode(200);
            resultAjax.setMsg(msg);
            resultAjax.setData(data);
            return resultAjax;
        }
    
        public static ResultAjax fail(int code,String msg){
            ResultAjax resultAjax = new ResultAjax();
            resultAjax.setCode(code);
            resultAjax.setMsg(msg);
            resultAjax.setData(null);
            return resultAjax;
        }
    
        public static ResultAjax fail(int code,String msg,Object data){
            ResultAjax resultAjax = new ResultAjax();
            resultAjax.setCode(code);
            resultAjax.setMsg(msg);
            resultAjax.setData(data);
            return resultAjax;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    实现步骤可以分为以下两步:

    1️⃣第一步,自定义统一数据返回处理类,标注上@ControllerAdvice注解同时实现ResponseBodyAdvice接口。

    2️⃣第二步,重写接口中的supports方法和beforeBodyWrite方法 并在该方法中进行统一数据格式的处理。

    实现代码如下:

    • supports决定是否执行beforeBodyWrite(数据重写),返回true表示重写,false表示不重写。
    package com.example.demo.config;
    
    import com.example.demo.common.ResultAjax;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.MethodParameter;
    import org.springframework.http.MediaType;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
    
    
    //定义的全局响应体建议 (Response Body Advice)
    //其作用是在返回数据到客户端之前, 对返回的数据进行处理或修改
    @ControllerAdvice
    public class ResponseAdvice implements ResponseBodyAdvice {
        @Autowired
        private ObjectMapper objectMapper;
    
        // 是否执行 beforeBodyWrite 方法, true = 执行, 重写返回结果
        @Override
        //supports方法, 用于决定是否对返回的数据进行处理
        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) {
            // 如果是标准格式直接返回
            if(body instanceof ResultAjax) {
                return body;
            }
            
            //重写返回结果, 让其返回一个统一的数据格式
            return ResultAjax.succ(body);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    我们写一下 Contoller,试着返回几组数据:

    @RestController
    @RequestMapping("/user")
    public class UserController { 
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 规定格式数据:
    @RequestMapping("/reg")
    public ResultAjax reg() {
        return ResultAjax.succ("注册成功!");
    }
    
    • 1
    • 2
    • 3
    • 4

    img

    1. 返回基础数据类型:
    @RequestMapping("/getnum")
    public int getNum() {
        return 1;
    }
    
    • 1
    • 2
    • 3
    • 4

    img

    1. 返回对象

    img

    @RequestMapping("/user")
    public User user(){
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setPassword("123");
        return user;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    img

    我们可以发现即使我们在 Controller 层返回的不是标准格式的数据,也会进行重写。

    🎯此时我们可以将上面的统一异常处理返回的数据也改成规定格式:

    @ExceptionHandler(NullPointerException.class)
    public ResultAjax doNullPointerException(NullPointerException e) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("空指针: " + e.getMessage());
        resultAjax.setData(null);
        return resultAjax;
    }
    
    @ExceptionHandler(Exception.class)
    public ResultAjax doException(Exception e){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("Exception: " + e.getMessage());
        resultAjax.setData(null);
        return resultAjax;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2. 特殊情况,返回String类型

    @RequestMapping("/getstr")
    public String getStr(){
        return "hello";
    }
    
    • 1
    • 2
    • 3
    • 4

    img

    img这里报了类型转换异常,ResultAjax 不能转换为 String,为什么会出现这个问题呢,我们返回 String 会进行三个步骤:

    1. 方法返回 String
    2. 统一数据格式返回的是 -> String 转为 ResultAjax
    3. 将 ResultAjax 转换为 application/json 字符串给前端

    img

    那么问题到底出现在哪一步呢?答案是我们在进行类型转换时出错了

    • 对于String 类型的数据 -> 会使用StringHttpMessageConverter这个转换器进行类型转换,但其实进行类型转换时,这个转换器实际上还没有初始化好,也就无法将 ResultAjax 转换为 String,所以会报错。

    • 对于非 String 类型的数据 -> 会使用HttpMessageConverter这个转换器 进行类型转换。

    🎯解决方案:

    🍂方式一,将StringHttpMessageConverter转换器去掉, 那么在进行类型转换的时候就会使用HttpMessageConverter这个转换器了

    @Configuration
    public class MyConfig implements WebMvcConfigurer {
        @Override
        // configureMessageConverters 方法使用 removeIf 方法删除了所有 StringHttpMessageConverter 的实例.
        // 这样, SpringMVC 就不会再使用 StringHttpMessageConverter 来处理 String 类型的数据了.
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    img

    🍂方式二,在统一数据重写时,单独处理 String 类型,直接让其返回一个 json 格式的字符串,而不是 ResultAjax。

    @ControllerAdvice
    public class ResponseAdvice implements ResponseBodyAdvice {
        @Autowired
        private ObjectMapper objectMapper;
    
        // 是否执行 beforeBodyWrite 方法, true = 执行, 重写返回结果
        @Override
        //supports方法, 用于决定是否对返回的数据进行处理
        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) {
            // 如果是标准格式直接返回
            if(body instanceof ResultAjax) {
                return body;
            }
            // 对字符串进行判断和处理
            if (body instanceof String) {
                ResultAjax resultAjax = ResultAjax.succ(body);
                try {
                    return objectMapper.writeValueAsString(resultAjax);
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
            }
            //重写返回结果, 让其返回一个统一的数据格式
            return ResultAjax.succ(body);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    img

  • 相关阅读:
    STM32单片机酒精检测防酒驾系统酒精报警器
    金融工程学学习笔记第一章
    五分钟搞懂POM设计模式
    基于springboot的通知反馈系统
    向量数据库入坑:入门向量数据库 Milvus 的 Docker 工具镜像
    uniapp 使用了uview 的dropdown下拉菜单后出现页面无法滚动
    ssm基于Springboot的大学生竞赛辅导网站系统java-maven项目
    【电子器件笔记4】电感参数和选型
    STM32F4X SDIO(六) 例程讲解-SD_PowerON
    dataframe保存excel格式比csv格式小很多很多
  • 原文地址:https://blog.csdn.net/Trong_/article/details/132789028