主要解决以下开发中的问题
- 校验工作重复 , 代码冗余
- 接口返回值不统一 , 前端对接成本高
- 异常返回没有统一捕获
- 公共逻辑无法集中处理
本文主要内容
- 拦截器(登录校验,请求预处理)
- 统一数据返回格式
- 统一异常处理
- 原理分析
- 结合图书管理系统完成上述操作
一. 统一请求拦截与登录校验 (实现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 方法:
- 获取 HandlerExecutionChain(拦截器 + Controller)
- 执行拦截器
preHandle - 执行 Controller
- 执行拦截器
postHandle - 处理视图 / 异常
- 执行
afterCompletion
2. @ControllerAdvice 原理
@ControllerAdvice本质是@Component- 启动时被
RequestMappingHandlerAdapter扫描 - 统一管理:
-
ResponseBodyAdvice:统一返回@ExceptionHandler:统一异常@InitBinder:参数绑定@ModelAttribute:全局数据