Spring Boot 统一处理功能

目录

1.用户登陆权限验证

[1.1 每个方法验证](#1.1 每个方法验证)

[1.2 Spring AOP 用户统一登陆验证](#1.2 Spring AOP 用户统一登陆验证)

[1.3 拦截器](#1.3 拦截器)

[1.3.1 自定义拦截器](#1.3.1 自定义拦截器)

[1.3.2 将自定义拦截器配置到系统设置中,并且设置拦截规则](#1.3.2 将自定义拦截器配置到系统设置中,并且设置拦截规则)

[1.3.3 排除所有的静态资源](#1.3.3 排除所有的静态资源)

[1.4 登录拦截器(练习)](#1.4 登录拦截器(练习))

[1.5 拦截器原理](#1.5 拦截器原理)

2.统一异常处理

3.统一数据返回格式

[3.1 统一数据处理(强制执行)](#3.1 统一数据处理(强制执行))


统一处理的功能包括:

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

1.用户登陆权限验证

之前学到的是在每个方法中验证用户登录权限,现在是统一的用户登录验证处理

1.1 每个方法验证

最初的验证方法

java 复制代码
@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;
         }
    }
}

每个方法中都有相同的用户登录验证权限,它的缺点是:

  • 每个方法都要单独写一个用户登录验证的方法
  • 添加的控制器越多,调⽤⽤户登录验证的⽅法也越多,这样就增加了后期的修改成本和维护成本。
  • 这些⽤户登录验证的⽅法和接下来要实现的业务⼏何没有任何关联,但每个⽅法中都要写⼀遍

1.2 Spring AOP 用户统一登陆验证

通过前置通知或者环绕通知实现:

java 复制代码
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;
     }
}

缺点:

  • 无法获取 HttpSession 对象
  • 我们要对⼀部分方法机型拦截,⽽另⼀部分方法不拦截,如注册方法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。

1.3 拦截器

Spring 提供了具体的实现拦截器:HandlerInterceptor

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

1.3.1 自定义拦截器

用户登陆拦截器

  • 实现 HandlerInterceptor 接口 的拦截器
  • 实现 preHandle(执行具体方法之前的预处理)方法:执行目标方法之前判断是否有登录
  • 重新 preHandle 方法:返回 true(拦截器验证成功,继续执行后续的方法);返回 false(拦截器验证失败,不会执行后续目标方法)
  • 写业务方法:得到 Session 对象,判断 Session 中是否有登陆用户,有------true;无------false
java 复制代码
package com.example.demo.config;
import com.example.demo.common.AppVar;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
 * 自定义拦截器
 */
public class UserInterceptor implements HandlerInterceptor {
    /**
     * 返回 true:表示拦截器验证成功,继续执行后续的方法
     *     false:拦截器验证失败,不会执行后续目标方法
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //业务方法
        //得到 Session 对象:判断 Session 中是否有登陆用户,有------true;无------false
        HttpSession session = request.getSession(false);//默认 true
        if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
            //用户已经登陆
            return true;
        }
        return false;
    }
}

定义 Session Key 值:

java 复制代码
package com.example.demo.common;
//全局变量
public class AppVar {
    //Session Key
    public static final String SESSION_KEY = "SESSION_KEY";
}

1.3.2 将自定义拦截器配置到系统设置中,并且设置拦截规则

配置文件 AppConfig:

  • 实现 WebMvcConfigurer,表明是一个系统的配置文件
  • 重写 addInterceptors 方法(支持添加多个拦截器)

添加多个拦截器注入有两种方法:

1️⃣

java 复制代码
registry.addInterceptor(new UserInterceptor());

2️⃣ UserInterceptor 类中添加 @Component 注解,随着 spring 启动而启动;通过 @Autowired 注入进去

@Configuration 注解是随着 spring 启动而启动,将当前配置设置到系统中

java 复制代码
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.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private UserInterceptor userInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //registry.addInterceptor(new UserInterceptor());
        registry.addInterceptor(userInterceptor)
                .addPathPatterns("/**")//拦截规则:/**拦截所有的请求
                .excludePathPatterns("/user/reg")//不拦截
                .excludePathPatterns("/user/login");
    }
}

拦截规则:

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

新建 Controller 类(一个拦截、两个不拦截):

java 复制代码
package com.example.demo.controller;
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("do getUser()");
        return "user";
    }

    @RequestMapping("/reg")
    public String reg() {
        System.out.println("do reg()");
        return "reg";
    }

    @RequestMapping("/login")
    public String login() {
        System.out.println("do login()");
        return "login";
    }
}

在拦截器里边执行一个 do UserInterceptor:

java 复制代码
@Component
public class UserInterceptor implements HandlerInterceptor {
    /**
     * 返回 true:表示拦截器验证成功,继续执行后续的方法
     *     false:拦截器验证失败,不会执行后续目标方法
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("do UserInterceptor");
        //业务方法
        //得到 Session 对象:判断 Session 中是否有登陆用户,有------true;无------false
        HttpSession session = request.getSession(false);//默认 true
        if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
            //用户已经登陆
            return true;
        }
        return false;
    }
}

运行启动类,首先我们先看不拦截的:访问 localhost:8080/user/reg

被拦截的:访问 localhost:8080/user/getuser

1.3.3 排除所有的静态资源

java 复制代码
// 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
     registry.addInterceptor(new LoginInterceptor())
         .addPathPatterns("/**") // 拦截所有接⼝
         .excludePathPatterns("/**/*.js")
         .excludePathPatterns("/**/*.css")
         .excludePathPatterns("/**/*.jpg")
         .excludePathPatterns("/login.html")
         .excludePathPatterns("/**/login"); // 排除接⼝
}
  • /*:一级目录
  • /**:所有目录

在这里有一个问题就是排除图片,图片的格式有很多,总不能一个一个排除,这样的情况我们可以在 resource 下的 static 创建一个 image 目录,把所有图片都放在这里边,排除 image 里边的所有东西

java 复制代码
 .excludePathPatterns("/image/**");

1.4 登录拦截器(练习)

1.登录、注册页面不拦截,其他页面拦截

2.当登录成功写入 session 之后,拦截的页面也可以正常访问

1.拦截功能

java 复制代码
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.WebMvcConfigurer;

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private UserInterceptor userInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //registry.addInterceptor(new UserInterceptor());
        registry.addInterceptor(userInterceptor)
                .addPathPatterns("/**")//拦截规则:/**拦截所有的请求
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/editor/**")
                .excludePathPatterns("/img/**")
                .excludePathPatterns("/js/**")
                .excludePathPatterns("/**/login"); // 排除接⼝
        ;
    }
}

2.拦截器

java 复制代码
package com.example.demo.config;
import com.example.demo.common.AppVar;
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 UserInterceptor implements HandlerInterceptor {
    /**
     * 返回 true:表示拦截器验证成功,继续执行后续的方法
     *     false:拦截器验证失败,不会执行后续目标方法
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("do UserInterceptor");
        //业务方法
        //得到 Session 对象:判断 Session 中是否有登陆用户,有------true;无------false
        HttpSession session = request.getSession(false);//默认 true
        if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
            //用户已经登陆
            return true;
        }
        return false;
    }
}

1.5 拦截器原理

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

而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,源码一部分:

java 复制代码
//调用预处理【重点】
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}

执行所有的拦截器:所有拦截器不等于 false,才会进行之后的代码

java 复制代码
// 执⾏ Controller 中的业务
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
    return;
}

所有的 Controller 执行都会通过⼀个调度器 DispatcherServlet 来实现,而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法(其中会执行 applyPreHandle 方法,所有拦截器不等于 false,才会执行 Controller 方法)

applyPreHandle 源码:

java 复制代码
    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;
    }    

执行一个目标方法的时候,循环所有的拦截器,拿到容器中的所有拦截器,再去执行拦截器的预执行方法(preHandle 方法),如果有一个 preHandle 方法为 false,就返回 false,就不执行后续流程,直接返回;返回true,执行 controller 方法

2.统一异常处理

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

默认返回的是一个页面(需要的是一个数据不是页面),这个时候需要加 @ResponseBody ,返回一个数据,而 @ResponseBody 既可以加到方法上也可以加在类上,所以可以直接使用注解 @RestControllerAdvice

假设构建一个空指针异常(UserController 类):

java 复制代码
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/reg")
    public String reg() {
        System.out.println("do reg()");
        Object obj = null;
        System.out.println(obj.hashCode());
        return "reg";
    }
}

定义统一的返回对象:

java 复制代码
package com.example.demo.common;
import lombok.Data;
//统一的对象
@Data
public class ResultAjax {
    private int code;//状态码
    private String msg;//状态码的描述信息
    private Object data;//返回数据
}

空指针异常处理:

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

java 复制代码
package com.example.demo.config;
import com.example.demo.common.ResultAjax;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler(NullPointerException.class)
    public ResultAjax doNullPointerException(NullPointerException e) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("空指针异常:" + e.getMessage());
        resultAjax.setData(null);
        return resultAjax;
    }
}

访问 localhost:8080/user/reg

上述为空指针异常,那么异常有很多,我们难道全要写这些异常?

这个时候我们可以设置一个默认异常,所有异常都是基于 Exception

java 复制代码
@RestControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public ResultAjax doException(Exception e) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("异常:" + e.getMessage());
        resultAjax.setData(null);
        return resultAjax;
    }
}

3.统一数据返回格式

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

3.1 统一数据处理(强制执行)

  • 使用 @ControllerAdvice
  • 实现 ResponseBodyAdvice 接口,并重写它的两个方法,supports 必须返回 true,beforeBodyWrite 方法中进行重新判断和重写操作
java 复制代码
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;
/**
 * 听译返回值的保底实现类
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;//spring 自带的
    /**
     * true -> 才会调用 beforeBodyWrite 方法
     * 反之则永远不会调用 beforeBodyWrite 方法
     * @param returnType
     * @param converterType
     * @return
     */
    @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) {
        // 已经包装好的对象
        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);
    }
}
java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/getnum")
    public int getNum() {
        return 1;
    }

    @RequestMapping("/getstr")
    public String getStr() {
        return "hello";
    }
}

String 比较特殊,既不属于对象,也不属于基本数据类型,在返回值的时候也是需要单独判断

相关推荐
路在脚下@20 分钟前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一21 分钟前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien23 分钟前
Spring Boot常用注解
java·spring boot·后端
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
盛派网络小助手2 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl