SpringBoot统一功能处理

2023年 9月 26日 56.1k 0

 前言🍭

❤️❤️❤️Spring专栏更新中,各位大佬觉得写得不错,支持一下,感谢了!❤️❤️❤️

Spring + Spring MVC + MyBatis专栏

本章是讲Spring Boot 统⼀功能处理模块,也是 AOP 的实战环节,要实现的目标有以下 3 个:

  • 使用拦截器实现用户登录权限的统一验证;
  • 统⼀数据格式返回;
  • 统⼀异常处理。
  • 一、用户登录权限效验🍭

    1、最初用户登录验证🍉

    用户登录权限的发展从之前每个方法中自己验证用户登录权限,到现在统⼀的用户登录验证处理,它是⼀个逐渐完善和逐渐优化的过程。

    @RestController
    @RequestMapping("/user")
    public class UserController {
        /**
         * 某⽅法 1
         */
        @RequestMapping("/m1")
        public Object method(HttpServletRequest request) {
            // 有 session 就获取,没有不会创建
            HttpSession session = request.getSession(false);
            if (session != null && session.getAttribute("userinfo") != null) {
                // 说明已经登录,业务处理
                return true;
            } else {
                // 未登录
                return false;
            }
        }
     
        /**
         * 某⽅法 2
         */
        @RequestMapping("/m2")
        public Object method2(HttpServletRequest request) {
            // 有 session 就获取,没有不会创建
            HttpSession session = request.getSession(false);
            if (session != null && session.getAttribute("userinfo") != null) {
                // 说明已经登录,业务处理
                return true;
            } else {
                // 未登录
                return false;
            }
        }
        // 其他⽅法。。。
    }
    

    从上述代码可以看出,每个方法中都有相同的用户登录验证权限,它的缺点是:

  • 每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也⼀样要传参调用和在方法中进行判断。
  • 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成本和维护成本。
  • 这些用户登录验证的方法和接下来要实现的业务几何没有任何关联,但每个方法中都要写⼀遍。 所以提供⼀个公共的 AOP 方法来进行统⼀的用户登录权限验证迫在眉睫。
  • 2、Spring AOP 用户统⼀登录验证的问题🍉

    说到统⼀的用户登录验证,我们想到的第⼀个实现方案是 Spring AOP 前置通知或环绕通知来实现,具体实现代码如下:

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
     
    @Aspect
    @Component
    public class UserAspect {
        // 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法
        @Pointcut("execution(* com.example.demo.controller..*.*(..))")
        public void pointcut() {
        }
     
        // 前置⽅法
        @Before("pointcut()")
        public void doBefore() {
     
        }
     
        // 环绕⽅法
        @Around("pointcut()")
        public Object doAround(ProceedingJoinPoint joinPoint) {
            Object obj = null;
            System.out.println("Around ⽅法开始执⾏");
            try {
                // 执⾏拦截⽅法
                obj = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
     
            System.out.println("Around 方法结束执行");
            return obj;
        }
    }
    

    如果要在以上 Spring AOP 的切面中实现用户登录权限效验的功能,有以下两个问题:

  • 定义拦截的规则(表达式)非常难。(我们要对⼀部分方法进行拦截,而另⼀部分方法不拦截,如注册方法和登录方法是不拦截的,这样 的话排除方法的规则很难定义,甚至没办法定义)。
  • 在切面类中拿到 HttpSession 比较难
  • 那这样如何解决呢?

    Spring 拦截器🍉

    对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:

  • 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理方法。
  • 将自定义拦截器加入WebMvcConfigurer 的 addInterceptors 方法中。
  • 具体实现如下:

    目录结构:

    image.png

    Ⅰ、实现拦截器🍓

    关键步骤:
    a.实现 HandlerInterceptor 接口
    b.重写 preHeadler 方法,在方法中编写自己的业务代码

    package com.example.demo.config;
     
    import org.springframework.web.servlet.HandlerInterceptor;
     
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
     
    public class LoginInterceptor implements HandlerInterceptor {
        /**
         * 此方法返回一个 boolean,如果为 true 表示验证成功,可以继续执行后续流程如果是 false 表示验证失败,后面的流程不能执行了
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //用户登录业务判断
            HttpSession session=request.getSession(false);
            if (session != null && session.getAttribute("userinfo") != null) {
                // 说明用户已经登录
                return true;
            }
            // 可以调整到登录页面 or 返回一个 401/403 没有权限码
            response.sendRedirect("/login.html");
    //        response.setStatus(403);
            return false;
        }
    }
    

    Ⅱ、将拦截器添加到配置文件中,并且设置拦截的规则🍓

    package com.example.demo.config;
     
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
     
    @Configuration
    public class AppConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                    .addPathPatterns("/**") // 拦截所有请求
                    .excludePathPatterns("/user/login") // 排除的url地址(不拦截的url地址)
                    .excludePathPatterns("/user/reg");
        }
    }
    

    其中:

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

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

    排除所有的静态资源:

        // 拦截器
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                    .addPathPatterns("/**") // 拦截所有接⼝
                    .excludePathPatterns("/**/*.js")
                    .excludePathPatterns("/**/*.css")
                    .excludePathPatterns("/**/*.jpg")
                    .excludePathPatterns("/login.html")
                    .excludePathPatterns("/**/login"); // 排除接⼝
        }
    

    UserController:

    package com.example.demo.controller;
     
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
     
     
    @RestController
    @RequestMapping("/user")
    public class UserController {
     
        /*    @GetMapping
            public String getMethod(){
                return "执行GET请求!";
            }
            @PostMapping
            public String postMethod(){
                return "执行POST请求!";
            }*/
        @RequestMapping("/getuser")
        public String getUser() {
            System.out.println("执行了 get User~");
            return "get user";
        }
     
        @RequestMapping("/login")
        public String login() {
            System.out.println("执行了 login~");
            return "login~";
        }
     
        @RequestMapping("/reg")
        public String reg() {
            System.out.println("执行了 reg~");
            return "reg~";
        }
    }
    

    Ⅲ、启动项目:🍓

    不拦截:

    image.png

    image.png

    拦截:http://localhost:8080/user/getuser

    image.png

    为什么会显示重定向次数过多?

    这是因为这个login.html页面也被拦截了,所以它去访问时候就会一直重定向重定向去访问。

    解决方法:

    image.png

    image.png

    Ⅳ、拦截器实现原理🍓

    正常情况下的调用顺序:

    image.png

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

    image.png

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

    image.png

    而所有方法都会执行 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;
                    Exception 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);
                }
     
            }
        }
    

    从上述源码可以看出在开始执行 Controller 之前,会先调用 预处理方法 applyPreHandle,而applyPreHandle 方法的实现源码如下:

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
            for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
    // 获取项⽬中使⽤的拦截器 HandlerInterceptor
                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 并执行拦截器中的 preHandle 方法,这样就和咱们前面定义的拦截器对应上了,如下图所示:

    image.png

    此时用户登录权限的验证方法就会执行,这就是拦截器的实现原理。

    image.png

    二、统⼀异常处理🍭

    Ⅰ、实现🍓

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

    package com.example.demo.config;
     
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
     
    import java.util.HashMap;
     
    @ControllerAdvice
    @ResponseBody
    public class MyExHandler {
     
        /**
         * 拦截所有的空指针异常,进行统一的数据返回
         */
        @ExceptionHandler(NullPointerException.class)
        public HashMap nullException(NullPointerException e) {
            HashMap result = new HashMap();
            result.put("code", "-1");
            result.put("msg", "空指针异常:" + e.getMessage()); // 错误码的描述信息
            result.put("data", null);
            return result;
        }
     
        @ExceptionHandler(Exception.class)//保底的默认异常
        public HashMap exception(Exception e) {
            HashMap result = new HashMap();
            result.put("code", "-1");
            result.put("msg", "异常:" + e.getMessage()); // 错误码的描述信息
            result.put("data", null);
            return result;
        }
     
    }
    

    Ⅱ、添加异常🍓

    UserController

     @RequestMapping("/login")
        public String login() {
            Object obj = null;
            obj.hashCode();
            System.out.println("执行了 login~");
            return "login~";
        }
     
        @RequestMapping("/reg")
        public String reg() {
            int num = 10 / 0;
            System.out.println("执行了 reg~");
            return "reg~";
        }
    

    Ⅲ、启动程序:🍓

    login:

    image.png

    reg:

    image.png

    方法名和返回值可以自定义,其中最重要是 @ExceptionHandler(Exception.class) 注解。

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

    三、统一数据返回格式🍭

    1.创建一个类,并添加 @ControllerAdvice
    2.实现ResponseBodyAdvice接口,并重写supports和beforeBodywrite (统一对象就是此方法中实现的)

    1、为什么需要统一数据返回格式?🍉

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

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

    Ⅰ、实现🍓

    统⼀的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现。

    package com.example.demo.config;
     
    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;
     
    import java.util.HashMap;
     
    @ControllerAdvice
    public class ResponseAdvice implements ResponseBodyAdvice {
     
        @Autowired
        private ObjectMapper objectMapper;
     
        /**
         * 此方法返回 true 则执行下面 beforeBodyWrite 方法
         * 反之则不执行
         */
        @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 result = new HashMap();
            result.put("code", 200);
            result.put("msg", "");
            result.put("data", body);
            // 需要特殊处理,因为 String 在转换的时候会报错
            if (body instanceof String) {
                try {
                    return objectMapper.writeValueAsString(result);
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
            }
            return result;
        }
    }
    

    如果没有对Sting进行特殊处理,就会有异常出现:

    image.png

    Ⅱ、添加实现类🍓

    UserController

    @RequestMapping("/getnum")
        public Integer getNumber() {
            return new Random().nextInt(10);
        }
        @RequestMapping("/getuser")
        public String getUser() {
            System.out.println("执行了 get User~");
            return "get user";
        }
    

    Ⅲ、启动程序🍓

    image.png

    image.png

    Ⅳ、总结🍓

    上面只是以一种简单的方式来向大家介绍如何去统一数据返回格式,在实际工作中并不会这样去使用

    image.png

    而是将统⼀的返回格式(包含:status、data、msg 字段)进行封装成一个类:

    根据不同操作成功和操作失败的情况,重写了不同的方法:

    package com.example.demo.common;
     
    import lombok.Data;
     
    import java.io.Serializable;
     
    /**
     * 统一数据格式返回
     */
    @Data
    public class AjaxResult implements Serializable {//支持序列化
        // 状态码
        private Integer code;
        // 状态码描述信息
        private String msg;
        // 返回的数据
        private Object data;
     
        /**
         * 操作成功返回的结果,需要 data
         */
        public static AjaxResult success(Object data) {
            AjaxResult result = new AjaxResult();
            result.setCode(200);
            result.setMsg("");
            result.setData(data);
            return result;
        }
        //重载success
        public static AjaxResult success(int code, Object data) {
            AjaxResult result = new AjaxResult();
            result.setCode(code);
            result.setMsg("");
            result.setData(data);
            return result;
        }
     
        public static AjaxResult success(int code, String msg, Object data) {
            AjaxResult result = new AjaxResult();
            result.setCode(code);
            result.setMsg(msg);
            result.setData(data);
            return result;
        }
     
        /**
         * 返回失败结果
         */
        public static AjaxResult fail(int code, String msg) {
            AjaxResult result = new AjaxResult();
            result.setCode(code);
            result.setMsg(msg);
            result.setData(null);
            return result;
        }
     
        public static AjaxResult fail(int code, String msg, Object data) {
            AjaxResult result = new AjaxResult();
            result.setCode(code);
            result.setMsg(msg);
            result.setData(data);
            return result;
        }
    }
    

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论