[Java EE 进阶] SpringBoot 统一功能处理全解:拦截器、统一返回、统一异常

主要解决以下开发中的问题

  1. 校验工作重复 , 代码冗余
  2. 接口返回值不统一 , 前端对接成本高
  3. 异常返回没有统一捕获
  4. 公共逻辑无法集中处理

本文主要内容

  1. 拦截器(登录校验,请求预处理)
  2. 统一数据返回格式
  3. 统一异常处理
  4. 原理分析
  5. 结合图书管理系统完成上述操作

一. 统一请求拦截与登录校验 (实现WebMvcConfigurer 接口 , 重写addInterceptors 方法)

1.配置拦截器

① 登录拦截器

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

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        log.info("postHandle......");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        log.info("afterCompletion.....");
    }
}

实现 HandlerInterceptor 接口 , 并重写preHandle , postHandle, afterCompletion 方法

  • preHandle : 在目标方法执行前执行 ; true 放行 , flase 拦截
  • postHandle : 在目标发发执行后执行
  • afterCompletion : 视图渲染完毕后执行 , 最后执行

② 注册配置拦截器

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器对象
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**");//设置拦截器的请求路径(/**表示拦截所有请求)
        
    }
}

实现WebMvcConfigurer 接口 , 重写addInterceptors 方法

接下来启动服务 , 访问任意请求 , 观察日志

可以看到preHandle⽅法执⾏之后就放⾏了,开始执⾏⽬标⽅法,⽬标⽅法执⾏完成之后执⾏ postHandle和afterCompletion⽅法

我们把拦截器中preHandle⽅法的返回值改为false

可以看到 , 拦截器拦截了请求 , 没有任何响应

2.拦截器详解

2.1 拦截路径

在上述方法中 , 我们可以使用

addPathPatterns()方法 指定拦截哪些请求

excludePathPatterns()方法 指定不拦截哪些请求

参数为任意路径 , 例如 :

  • /*(一级路径)
  • /**(任意级路径)
  • /user/*(/user 下的一级路径)
  • /user/**(/user 下的任意级路径)
拦截器执行流程
  • 请求 → DispatcherServlet
  • 执行拦截器 preHandle
  • true → 进入 Controller ; 为 false 则不会放行(停止执行)
  • 执行 postHandle
  • 执行 afterCompletion(视图渲染)
  • 返回响应

2.3 校验登录

完成登录拦截器后 , 可以通过拦截器来对图书管理系统中的登录进行校验

需求 : 用户需要先登录才能进行后续请求操作 ; 并且将散布在代码中的拦截方法全部删除 , 交给拦截器统一控制 , 不用让每个接口重复编写

步骤 :

① 拦截 除了登录接口 , 部分资源接口 以外的所有接口

② 只有完成登录后才能放行 , 未完成登录操作一直拦截

③ 调整原来的校验方式

注册配置拦截器
java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器对象
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")//设置拦截器的请求路径(/**表示拦截所有请求)
                .excludePathPatterns("/user/login")//设置拦截器排除拦截的路径
                .excludePathPatterns("/**/*.js") //排除前端静态资源
                .excludePathPatterns("/**/*.css")
                .excludePathPatterns("/**/*.png")
                .excludePathPatterns("/**/*.html");
    }
}
定义拦截器
java 复制代码
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle 目标方法执行前");
        //从session中获取用户信息,如果session中不存在,则返回false,并设置状态码401(未经过认证),否则返回true
        HttpSession session = request.getSession(false);
        if(!checkUser(session)){
            response.setContentType("text/html;charset=utf-8");
            response.setStatus(401);
            String msg = "用户未登录";
            response.getOutputStream().write(msg.getBytes("UTF-8"));
            return false;
        }
        return true;
    }

    public boolean checkUser(HttpSession session){
        if(session==null||session.getAttribute(Constants.SESSION_USER_KEY)==null){
            log.warn("用户未登录!");
            return false;
        }
        UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
        if (userInfo == null||userInfo.getId()<0) {
            log.warn("用户未登录!");

            return false;
        }
        log.info("用户已登录");
        return true;
    }
  • 登录了 → return true → 放行
  • 没登录 → return false → 拦截
删除原来的登录校验代码
java 复制代码
@RequestMapping("/getListByPage")
    public Result getListByPage(PageRequest pageRequest, HttpSession session){
        log.info("获取图书列表,pageRequest:{}",pageRequest);
//        if(session.getAttribute(Constants.SESSION_USER_KEY) == null){
//            return Result.unlogin();
//        }
//        UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
//        if (userInfo == null||userInfo.getId()<0) {
//            return Result.unlogin();
//        }
        PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);
        return Result.success(pageResult);
    }
此时通过 postman 测试

1.不登录直接查询图书列表

http://127.0.0.1:8080/book/getListByPage

2.登录后在查询

http://127.0.0.1:8080/user/login?name=admin&password=admin

http://127.0.0.1:8080/book/getListByPage

获取成功

修改前端

增加 error

javascript 复制代码
function getBookList() {
    $.ajax({
        type: "get",
        url: "/book/getListByPage" + location.search,
        success: function (result) {

            if (result == null || result.code =="UNLOGIN") {
                alert("用户未登录, 请先登录");
                location.href = "login.html";
            }
            //其他情况判断, 此处省略
            if(result ==null || result.data == null){
                return;
            }
            var data = result.data;

            var books = data.records;
            var finalHtml = "";
            for (var book of books) {
                finalHtml += '<tr>';
                finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';
                finalHtml += '<td>' + book.id + '</td>';
                finalHtml += '<td>' + book.bookName + '</td>';
                finalHtml += '<td>' + book.author + '</td>';
                finalHtml += '<td>' + book.count + '</td>';
                finalHtml += '<td>' + book.price + '</td>';
                finalHtml += '<td>' + book.publish + '</td>';
                finalHtml += '<td>' + book.statusCN + '</td>';
                finalHtml += '<td><div class="op">';
                finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
                finalHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';
                finalHtml += '</div></td></tr>';
            }
            $("tbody").html(finalHtml);

            //翻页信息
            $("#pageContainer").jqPaginator({
                totalCounts: data.total, //总记录数
                pageSize: 10,    //每页的个数
                visiblePages: 5, //可视页数
                currentPage: data.pageRequest.currentPage,  //当前页码
                first: '<li class="page-item"><a class="page-link">首页</a></li>',
                prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
                next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
                last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
                page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
                //页面初始化和页码点击时都会执行
                onPageChange: function (page, type) {
                    if (type == "change") {
                        location.href = "book_list.html?currentPage=" + page;
                    }
                }
            });
        },
        error: function(error){
            if(error.status==401){
                alert("用户未登录, 请先登录");
                location.href = "login.html";
            }

        }
    });

添加了error 当 ajax 请求失败时会进入 error

此时当使用浏览器访问 127.0.0.1:8080/book_list.html

然后进行跳转

二.统一数据返回格式(通过 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现)

目的: 前端只需要解析一种规则 ; 状态,消息,数据分离,结构清晰,方便统一处理失败/成功

1.快速入门

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    //判断是否执行beforeBodyWrite, true执行, false不执行
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Nullable
    @Override
    public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body);
    }
}

通过 postman 观察

添加统一返回前

添加统一返回后

2.异常分析

测试修改图书接口时

http://127.0.0.1:8080/book/updateBook?id=1&count=12

状态码是 500 但是有显示成功 , 查询数据库时 也发现修改完成

观察后端日志 大致为 : Spring 把你的 Result 对象,当成 String 类型去处理了,导致类型转换失败

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    ObjectMapper objectMapper;
    //判断是否执行beforeBodyWrite, true执行, false不执行
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Nullable
    @Override
    public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String){
            return objectMapper.writeValueAsString(Result.success(body));
        }
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
    
}

解决方法 : 判断类型 , 手动将 JackSon 序列化为 JSON 字符串

原因:

再次测试

修改前端

将所有判空操作都现成类似上述形式,需要结合后端返回的类型 ; 此处省略

三.统一异常处理(使用@ControllerAdvice+@ExceptionHandler 实现)

java 复制代码
@ControllerAdvice
@ResponseBody
@Slf4j
public class ErrorAdvice {
    @ExceptionHandler
    public Object handler(Exception e){
        log.error("异常:{}",e);
        return Result.fail("内部错误!!!");
    }
}

如果直接写 Exception 捕获的范围太大了 , 会把浏览器的异常也捕获到

加上下面的异常处理即可

java 复制代码
//先处理:忽略 favicon / 404 异常
@ExceptionHandler(NoResourceFoundException.class)
public Result handleNoResource() {
    // 不返回错误,直接返回正常结果,不打印日志
    return Result.success(null);
}

四. DispatcherServlet 与 ControllerAdvice

1. DispatcherServlet 执行流程

所有请求统一进入 doDispatch 方法:

  1. 获取 HandlerExecutionChain(拦截器 + Controller)
  2. 执行拦截器 preHandle
  3. 执行 Controller
  4. 执行拦截器 postHandle
  5. 处理视图 / 异常
  6. 执行 afterCompletion

2. @ControllerAdvice 原理

  • @ControllerAdvice 本质是 @Component
  • 启动时被 RequestMappingHandlerAdapter 扫描
  • 统一管理:
    • ResponseBodyAdvice:统一返回
    • @ExceptionHandler:统一异常
    • @InitBinder:参数绑定
    • @ModelAttribute:全局数据
相关推荐
北风朝向2 小时前
Spring Boot 集成 Open WebUI 实现 AI 流式对话
人工智能·spring boot·状态模式
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【53】Interrupts 中断机制:动态中断
java·人工智能·spring
用户298698530142 小时前
Java 操作 Word 文档:数学公式与符号的插入方法
java·后端
见青..2 小时前
JAVA安全靶场环境搭建
java·web安全·靶场·java安全
一坨阿亮2 小时前
Docker 离线部署
java·spring cloud·docker
LucaJu2 小时前
一次 OOM 线上排查实录
java·jvm·oom·内存溢出
SimonKing2 小时前
Firefox 太卡?换了这浏览器,内存占用直接降了 70%
java·后端·程序员
咖啡八杯2 小时前
GoF设计模式——建造者模式
java·后端
l软件定制开发工作室2 小时前
Spring开发系列教程(41)——集成Open API
java·后端·spring
折哥的程序人生 · 物流技术专研2 小时前
《Java 100 天进阶之路》第14篇:Java final关键字详解
java·开发语言·后端·面试