SpringBoot 统一功能处理

目录

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

[1.1 什么是拦截器](#1.1 什么是拦截器)

[1.2 拦截器的使用](#1.2 拦截器的使用)

[1.2.1 定义拦截器](#1.2.1 定义拦截器)

[1.2.2 注册配置拦截器](#1.2.2 注册配置拦截器)

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

[1.3.1 拦截路径](#1.3.1 拦截路径)

[1.3.2 拦截器执行流程](#1.3.2 拦截器执行流程)

[1.4 登录校验](#1.4 登录校验)

[1.4.1 定义拦截器](#1.4.1 定义拦截器)

[1.4.2 注册配置拦截器](#1.4.2 注册配置拦截器)

[2 适配器模式](#2 适配器模式)

[3 统一数据返回](#3 统一数据返回)

[3.1 存在问题](#3.1 存在问题)

[3.2 优点](#3.2 优点)

[4 统一异常处理](#4 统一异常处理)

1 拦截器

1.1 什么是拦截器

拦截器是Spring框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码,例如在一个项目中,不管进入哪个模块,都需要强制登录,此时拦截器的作用就发挥出来了

1.2 拦截器的使用

拦截器的使用步骤分为两步:

1)定义拦截器 2)注册配置拦截器

1.2.1 定义拦截器

实现HandlerInterceptor接口,并重写其所有方法

java 复制代码
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        log.info("LoginInterceptor 目标方法执行前执行");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 
                           ModelAndView modelAndView) throws Exception {
        log.info("LoginInterceptor 目标方法执行后执行");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 
            throws Exception {
        log.info("LoginInterceptor 视图渲染完毕后执⾏,最后执⾏");
    }
}

preHandle()方法:目标方法执行前执行,返回true,表示放行,返回false,表示拦截

postHandle()方法:目标方法执行后执行

afterCompletion()方法:视图渲染完毕之后执行,最后执行

1.2.2 注册配置拦截器

实现WebMvcConfigurer接口,并重写addInterceptors方法

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    //自定义的拦截对象
    @Autowired
    private LoginInterceptor loginInterceptor;
    private List<String> excludePaths = Arrays.asList(
            "/user/login",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/**/*.html"

    );
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截对象
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") //给所有请求设置拦截
                .excludePathPatterns(excludePaths); //排除登录的拦截
    }
}

启动服务器,访问任意请求,观察后端日志

可以看到perHandle方法执行之后就放行了,开始执行目标方法,目标方法执行完之后执行postHandle方法

1.3 拦截器详情

1.3.1 拦截路径

拦截路径是指我们定义的这个拦截器,对哪些请求生效,在注册配置拦截器的时候,通过addPathPatterns()方法指定要拦截哪些请求,也可以通过excludePathPatterns()指定放行哪些请求,在上述代码中,除了登录和前端的代码,对其他所有的代码进行了拦截

|------------|--------------|---------------------------------------------------------|
| 拦截路径 | 含义 | 举例 |
| /* | 一级路径 | 能匹配/user,/book,/login,不能匹配 /user/login |
| /** | 任意级路径 | 能匹配/user,/user/login,/user/reg |
| /book/* | /book下的⼀级路径 | 能匹配/book/addBook,不能匹配/book/addBook/1,/book |
| /book/** | /book下的任意级路径 | 能匹配/book,/book/addBook,/book/addBook/2,不能匹 配/user/login |

1.3.2 拦截器执行流程

正常的调用顺序是,Controller层调用Service层,Service层调用数据持久层这样的三层架构,有了拦截器之后,会在调用Controller层之前进行响应的业务处理,执行流程如下图

1 添加拦截器之后,在执行Controller层的方法之前,请求会先被拦截,执行preHandle()方法,如果这个方法返回true,就表示放行,继续访问Controller层中的方法,也就是执行三层架构,Controller------Service------Mapper,如果返回false,表示拦截,后续Controller层的方法不会执行

2 Controller当中的方法执行完毕之后,在回过来执行postHandle方法,执行完毕之后,最终给浏览器响应数据

1.4 登录校验

通过拦截器来完成登录的校验功能

1.4.1 定义拦截器

java 复制代码
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("登录拦截器校验");
        HttpSession session =  request.getSession();
        UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
        if (userInfo != null && userInfo.getId() > 0) {
            //表示放行
            return true;
        }
        response.setStatus(401);
        //表示拦截
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("目标方法执行后");
    }
}

1.4.2 注册配置拦截器

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    //自定义的拦截对象
    @Autowired
    private LoginInterceptor loginInterceptor;
    private List<String> excludePaths = Arrays.asList(
            "/user/login",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/**/*.html"

    );
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截对象
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") //给所有请求设置拦截
                .excludePathPatterns(excludePaths); //排除登录的拦截
    }
}

使用postman来进行测试

没有登陆时,访问任意一个模块,此时会被拦截

登录之后访问,此时就会放行

2 适配器模式

适配器模式,也叫包装器模式,将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作,简单来说就是将一个新类进行包装一下,适配调用使用,把两个不兼容的接口通过一定的方式使之兼容

适配器模式角色

Target:目标接口(可以是抽象类或者接口),客户希望直接使用的接口

Adaptee:适配者,但是与Target不兼容

Adapter:适配器类,此模式的核心,通过继承或者引用适配者的对象,把适配者转为目标接口

client:需要使用的适配器对象

适配器模式的实现

**场景:**SLF4J就是用了适配器模式,SLF4J提供了一系列打印日志的api,底层调用的是log4j或者logback来打印日志,我们作为为调用者,只需要调用slf4j的api就可以了

java 复制代码
/**
 * slf4j接口
 */
interface Slf4jApi {
    void log(String mes);
}
/**
 * log4j接口
 */
class Log4j {
    void log4jLog(String mes) {
        System.out.println("log4jLog打印:" + mes);
    }
}

/**
 * slf4j和log4j适配器
 */
class Slf4jLog4JAdapter implements Slf4jApi{
    private Log4j log4j;
    public Slf4jLog4JAdapter(Log4j log4j) {
        this.log4j = log4j;
    }
    @Override
    public void log(String mes) {
        log4j.log4jLog(mes);
    }
}

/**
 * 客户端使用
 */
public class Slf4jDemo {
    public static void main(String[] args) {
        Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());
        slf4jApi.log("使用slf4j打印日志");
    }
}

上述代码中。不需要改变log4j的api,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行

3 统一数据返回

统一数据返回格式使用@ControllerAdvice和 ResponseBodyAdvice的方式来实现

@ControllerAdvice表示控制器通知类,添加类 ResponseAdvice,实现ResponseBodyAdvice接口,并在类上添加@ControllerAdvice注解

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @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) {
        return Result.success(body);
    }
}
java 复制代码
@Data
public class Result {
    /**
     * 业务状态码
     */
    private ResultCode code;
    /**
     * 错误信息
     */
    private String errMsg;
    /**
     * 数据
     */
    private Object data;
    public static Result success(Object data) {
        Result result = new Result();
        result.setCode(ResultCode.SUCCESS);
        result.setErrMsg("");
        result.setData(data);
        return result;
    }
    public static Result fail(String errMsg) {
        Result result = new Result();
        result.setCode(ResultCode.FAIL);
        result.setErrMsg(errMsg);
        result.setData("");
        return result;
    }
    public static Result unLogin() {
        Result result = new Result();
        result.setCode(ResultCode.UNLOGIN);
        result.setErrMsg("用户未登录");
        result.setData(null);
        return result;
    }
}

以上的Result类是我在图书项目中定义的,我会在另一篇博客完整的写整个图书后端的代码

support方法:判断是否执行beforeBodyWrite方法,true为执行,false为不执行,通过该方法可以选择哪些类或者哪些方法的response要进行处理

beforeBodyWrite方法:对response方法进行具体操作处理

添加统一数据返回格式之前

添加统一数据返回格式之后

3.1 存在问题

如果返回的类型为String类型时,会发生内部错误,但是在数据库中,这条数据修改成功了

查看日志,会发现日志不能捕获到String类型

解决方案:

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper mapper;
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
    //使用这个注解来捕获异常
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
    Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //在返回之前,如果body是返回结果直接返回
        if (body instanceof Result) {
            return body;
        }
        //如果返回的是String类型,需要使用SpringBoot内置的Jackson来实现信息的序列化
        if (body instanceof String) {
            return mapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

此时的结果可以正确的返回了

3.2 优点

1 方便前端更好的接收和解析后端数据接口返回的数据

2 降低前后端的沟通成本,按照某个格式实现就可以

3 有利于项目统一数据的维护和修改

4 统一异常处理

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

java 复制代码
@Slf4j
@ControllerAdvice
@ResponseBody
public class ErrorHandler {
    @ExceptionHandler
    public Object exception(Exception e) {
        log.error("发生异常,e:" + e);
        return Result.fail(e.getMessage());
    }
}

如果上述代码出现 Exception异常(包括Exception的子类),就会返回一个Result对象

java 复制代码
public static Result fail(String errMsg) {
    Result result = new Result();
    result.setCode(ResultCode.FAIL);
    result.setErrMsg(errMsg);
    result.setData("");
    return result;
}

上述代码需要注意:

1)接口返回为数据时,需要加上@ResponseBody注解

2)@ControllerAdvice + @ExceptionHandler两个注解不能忘记,必须加上

可以针对不同的异常,来返回不同的结果

java 复制代码
@Slf4j
@ControllerAdvice
@ResponseBody
public class ErrorHandler {
    @ExceptionHandler
    public Object exception(Exception e) {
        log.error("发生异常,e:" + e);
        return Result.fail("发生异常" + e.getMessage());
    }
    @ExceptionHandler
    public Object exception(NullPointerException e) {
        log.error("发生异常,e:" + e);
        return Result.fail("发生NullPointerException:" + e.getMessage());
    }
    @ExceptionHandler
    public Object exception(ArithmeticException e) {
        log.error("发生异常,e:" + e);
        return Result.fail("发生ArithmeticException:" + e.getMessage());
    }
}

下面制造一些异常,依次测试其抛出的异常

java 复制代码
@RestController
@RequestMapping("/test")
public class TextController {
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }
    @RequestMapping("/t2")
    public boolean t2(){
        int a = 10/0; 
        return true;
    }
    @RequestMapping("/t3")
    public Integer t3() {
        String a = null;
        System.out.println(a.length()); 
        return 200;
    }
}

可以看出t2抛出算数异常,t3抛出空指针异常 ,虽然ArithmeticException和NullPointerException都继承Exception,但是有多个异常的时候,匹配顺序为当前类及其子类向上依次匹配,会找到离它最近的那个异常

相关推荐
前端啊龙1 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠5 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小灰灰__20 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭23 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
小远yyds25 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果44 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林1 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store