文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

SpringBoot统一功能处理

2023-09-03 21:00

关注

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

Spring + Spring MVC + MyBatis_冷兮雪的博客-CSDN博客

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

  1. 使用拦截器实现用户登录权限的统一验证;
  2. 统⼀数据格式返回;
  3. 统⼀异常处理。

1、最初用户登录验证🍉

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

@RestController@RequestMapping("/user")public class UserController {        @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;        }    }        @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;        }    }    // 其他⽅法。。。}

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

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

2、Spring AOP 用户统⼀登录验证的问题🍉

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

import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Aspect@Componentpublic 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 的切面中实现用户登录权限效验的功能,有以下两个问题:

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

那这样如何解决呢?

Spring 拦截器🍉

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

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

具体实现如下:

目录结构:

Ⅰ、实现拦截器🍓

关键步骤:
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 {        @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;@Configurationpublic class AppConfig implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new LoginInterceptor())                .addPathPatterns("*.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 {        @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~";    }}

Ⅲ、启动项目:🍓

不拦截:

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

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

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

解决方法:

Ⅳ、拦截器实现原理🍓

正常情况下的调用顺序:

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

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

而所有方法都会执行 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 方法,这样就和咱们前面定义的拦截器对应上了,如下图所示:

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

Ⅰ、实现🍓

统一异常处理使用的是 @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@ResponseBodypublic 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:

 reg:

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

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

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

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

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

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

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;@ControllerAdvicepublic class ResponseAdvice implements ResponseBodyAdvice {    @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 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进行特殊处理,就会有异常出现: 

Ⅱ、添加实现类🍓

UserController

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

Ⅲ、启动程序🍓

Ⅳ、总结🍓

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

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

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

package com.example.demo.common;import lombok.Data;import java.io.Serializable;@Datapublic class AjaxResult implements Serializable {//支持序列化    // 状态码    private Integer code;    // 状态码描述信息    private String msg;    // 返回的数据    private Object 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;    }}

来源地址:https://blog.csdn.net/m0_63951142/article/details/132246523

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯