1. 拦截器
前面的图书管理系统中我们完成了强制登录的功能,后端根据 Session 来判断用户是否登录,但是实现方法是比较麻烦的
- 需要修改每个接口的处理逻辑
- 需要修改每个接口的返回结果
- 接口定义修改,前端代码也要跟着修改
下面介绍一种更简单的,统一拦截所有请求并进行 Session 校验的解决办法:拦截器
1.1 拦截器入门
拦截器是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码。
也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行,也可以在用户请求前阻止其执行。
在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断 Session 中是否有登录用户的信息。如果有就可以放行,如果没有就进行拦截。
拦截器的使用步骤分为两步:
- 定义拦截器
- 注册配置拦截器
自定义拦截器:实现 HandlerInterceptor 接口,并重写其所有方法
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// true -- 放行;false -- 拦截
log.info("目标方法执行 前 执行 preHandle...");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("目标方法执行 后 执行 postHandle...");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("视图渲染完毕后执行 afterCompletion... 最后执行");
}
}
preHandle() 方法:目标方法执行前执行。返回true:继续执行后续操作;返回false:中断后续操作。
postHandle() 方法:目标方法执行后执行。
afterCompletion() 方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图,了解即可)。
注册配置拦截器:实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法
java
import com.example.springbook.interceptor.LoginInterceptor;
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 WebConfig implements WebMvcConfigurer {
// 自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); // /** 表示拦截所有请求
// 上下两种方法均可,上面方法需要将 LoginInterceptor 交给 Spring 管理
// registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
}
}
启动服务,观察后端日志
可以看到 preHandle 方法执行之后就放行了,开始执行目标方法,目标方法执行完成之后执行 postHandle 和 afterCompletion 方法。
我们把拦截器中 preHandle 方法的返回值改为 false,再观察运行结果。
可以看到,拦截器拦截了请求,没有进行响应
1.2 拦截器详解
1.2.1 拦截路径
拦截路径是指我们定义的这个拦截器,对哪些请求生效。
我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些请求,也可以通过 excludePathPatterns() 指定不拦截哪些请求。
上述代码中,我们配置的是 /** ,表示拦截所有的请求。
比如用户登录校验,我们希望可以对除了登录之外所有的路径生效。
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 对哪些路径生效
.excludePathPatterns("/user/login"); // 对哪些路径不生效
}
}
在拦截器中除了可以设置 /** 拦截所有资源外,还有一些常见的拦截路径设置
|------------|---------------|------------------------------------------------------------|
| 拦截路径 | 含义 | 举例 |
| /* | 一级路径 | 能匹配 /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,不能匹配 /uesr/login |
以上拦截规则可以拦截此项目中使用的 URL,包括静态文件、图片文件、JS、CSS 等文件
1.2.2 拦截器执行流程
正常的调用顺序:
有了拦截器之后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下:
- 添加拦截器后,执行 Controller 的方法之前,请求会先被拦截器拦截住。执行 preHandle() 方法,这个方法需要返回一个布尔类型的值。如果返回 true,就表示放行本次操作,继续访问 controller 中的方法。如果返回 false,则不会放行 (controller 中的方法也不会执行)。
- controller 当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及 afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据。
1.3 登录校验
上面介绍拦截器的基础操作之后,下面通过拦截器来完成图书管理系统中的登录校验功能
1.3.1 定义拦截器
从 Session 中获取用户信息,如果 Session 中不存在,则返回 fasle,并设置 http 状态码为 401,否则返回 true
java
import com.example.springbook.constant.Constants;
import com.example.springbook.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
// 拦截器代码,需先在 WebConfig 中注册
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// true -- 放行;false -- 拦截
log.info("登录拦截...");
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(Constants.SESSION_USER_KEY) == null ||
!StringUtils.hasLength((String)session.getAttribute(Constants.SESSION_USER_KEY))) {
// 用户未登录
log.error("用户未登录,进行拦截...");
response.setStatus(HttpStatus.UNAUTHORIZED.value()); // 设置用户未登录的状态码
// 统一返回一个 result JSON 对象
Result result =Result.unlogin();
response.getOutputStream().write(objectMapper.writeValueAsString(result).getBytes()); // 设置用户未登录的值
response.setContentType("application/json;charset=utf-8"); // 设置 HTTP 响应的内容类型为application/json,并且指定字符编码为utf-8
return false;
}
log.info("用户登录校验通过...");
return true;
}
}
http 状态码 401: Unauthorized
Indicates that authentication is required and was either not provided or has failed. If the request already included authorization credentials, then the 401 status code indicates that those credentials were not accepted.
中文解释: 未经过认证. 指示身份验证是必需的,没有提供身份验证或身份验证失败。如果请求已经包含授权凭据,那么 401 状态码表示不接受这些凭据。
1.3.2 注册配置拦截器
java
import com.example.springbook.interceptor.LoginInterceptor;
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 WebConfig implements WebMvcConfigurer {
// 自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/book/**"); // 对哪些路径生效
}
}
删除之前的登录校验代码
java
// 查询图书信息(翻页使用)
@RequestMapping("/getListByPage")
public Result<PageResponse<BookInfo>> getListByPage(PageRequest pageRequest, HttpSession session) {
log.info("获取图书列表,pageRequest:{}", pageRequest);
// 判断用户是否登录
// HttpSession session = request.getSession(false);
// if (session==null || session.getAttribute(Constants.SESSION_USER_KEY)==null ||
// !StringUtils.hasLength((String)session.getAttribute(Constants.SESSION_USER_KEY))){
// //用户未登录
// return Result.unlogin();
// }
// 参数校验还没写,包括不能为空,不等于负数等等
PageResponse<BookInfo> bookInfoPageResponse = new PageResponse<>();
try {
bookInfoPageResponse = bookService.getListByPage(pageRequest);
} catch (Exception e) {
log.error("获取图书列表失败");
return Result.fail();
}
return Result.success(bookInfoPageResponse);
}
运行程序,通过 Postman 测试
观察返回结果:http 状态码 401
也可以通过 Fiddler 抓包观察
- 再次查看图书列表
数据进行了返回
tip:Spring Banner 设置
java
/**
* _ooOoo_
* o8888888o
* 88" . "88
* (| -_- |)
* O\ = /O
* ____/`---'\____
* .' \\| |// `.
* / \\||| : |||// \
* / _||||| -:- |||||- \
* | | \\\ - /// | |
* | \_| ''\---/'' | |
* \ .-\__ `-` ___/-. /
* ___`. .' /--.--\ `. . __
* ."" '< `.___\_<|>_/___.' >'"".
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
* \ \ `-. \_ __\ /__ _/ .-` / /
* ======`-.____`-.___\_____/___.-`____.-'======
* `=---='
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* 佛祖保佑 永无BUG
*/
在 resources 目录下创建 banner.txt 文件,将上述代码添加到其中即可
1.4 DispatcherServlet 源码分析
项目启动后的日志:
当在图书管理系统中,用户未登录,去查询图书信息时:
第一次访问完后,再点击一次访问
看日志:
当 Tomcat 启动之后,有一个核心的类 DispatcherServlet,它来控制程序的执行顺序
所有请求都会先进到 DispatcherServlet,执行 doDispatch 调度方法. 如果有拦截器,会先执行拦截器 preHandle() 方法的代码,如果preHandle() 返回 true,继续访问 controller 中的方法,controller 当中的方法执行完毕后,再回过来执行 postHandle() 和 afterCompletion(),返回给 DispatcherServlet,最终给浏览器响应数据.
1.4.1 初始化
tip:Servlet 生命周期
- init:创建,只执行一次
- service:使用,可执行多次
- destory:销毁,只执行一次
只要是 Servlet,肯定要实现 init、service、destroy 这三个方法,即使有些地方没有实现,那也肯定是其他方法在别的地方帮它实现了(可能是父类帮它实现了)
下面看一下 service 做了些什么
在 HttpServlet 类中:
下面分析,为什么 DispatcherServlet 在收到 http://127.0.0.1:8080/book/getListByPage?id=1 这样的请求后,能够去 BookController 中找到对应的 getListByPage
先找 init 方法,DispatcherServlet 类中没有,去它的父类 FrameworkServlet 中找,还没有,在往上 HttpServletBean 中找,找到了:
看源码时,catch 先不看,打印日志的代码先不看
在 FrameworkServlet 中实现了 initServletBean:
tip:快捷键
Ctrl + Alt + ⬅:上一步
Ctrl + Alt + ➡:下一步
进入到 initWebApplicationContext 方法
先看返回值,返回了 wac,说明该方法主要就是在处理 wac
可以看到对 wac 的处理有很多 if,我们不知道它真正走的哪一个 if,因此可在源码上打断点来确定
在源码打断点之前,需先将其下载下来,如下图:
打断点,进行调试:
将没进入的删掉不看
将看懂的 赋值操作 和与 wac 无关的操作删掉:
只剩上面一条语句,点进去
之所以能直接使用 Spring,就是因为在这里初始化了
初始化文件上传解析器MultipartResolver:从应用上下文中获取名称为multipartResolver的Bean,如果没有名为multipartResolver的Bean,则没有提供上传文件的解析器
初始化区域解析器LocaleResolver:从应用上下文中获取名称为localeResolver的Bean,如果没有这个Bean,则默认使用AcceptHeaderLocaleResolver作为区域解析器
初始化主题解析器ThemeResolver:从应用上下文中获取名称为themeResolver的Bean,如果没有这个Bean,则默认使用FixedThemeResolver作为主题解析器
初始化处理器映射器HandlerMappings:处理器映射器作用,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx方法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进行排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器
初始化处理器适配器HandlerAdapter:作用是通过调用具体的方法来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进行排序;如果在ApplicationContext中没有发现处理器适配器,则默认SimpleControllerHandlerAdapter作为处理器适配器
初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进行排序;如果在ApplicationContext中没有发现异常处理器解析器,则不设置异常处理器
初始化RequestToViewNameTranslator:其作用是从Request中获取viewName,从ApplicationContext发现有viewNameTranslator的Bean,如果没有,则默认使用DefaultRequestToViewNameTranslator
初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolver的Bean,如果没有,则默认InternalResourceViewResolver作为视图解析器
初始化FlashMapManager:其作用是用于检索和保存FlashMap(保存从一个URL重定向到另一个URL时的参数信息),从ApplicationContext发现有flashMapManager的Bean,如果没有,则默认使用DefaultFlashMapManager
1.4.2 处理请求
DispatcherServlet 接收到请求后,执行 doDispatch 调度方法,再将请求转给 Controller
下面看 doDispatch 方法的具体实现
java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 1. 获取执行链
// 遍历所有的 HandlerMappping 找到与请求对应的 Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 获取适配器
// 遍历所有的 HandlerAdapter,找到可以处理该 Handler 的 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
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;
}
}
// 3. 执行拦截器 preHandle 方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 4. 执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 5. 执行拦截器 postHandle 方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
// 6. 处理视图,处理之后执行拦截器 afterCompletion 方法
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 7. 执行拦截器 afterCompletion 方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
cleanupMultipart(processedRequest);
}
}
}
}
通过打断点的方式来查看其中的核心代码:
在 mappedHandler = getHandler(processedRequest); 这一段代码中,就已经根据请求找到了对应的 Controller 和对应的方法
在这里的参数就是上面 Controller 中的方法,根据 Controller 和方法拿到一个适配器
点进去:
如果拦截器拦截到了,则直接返回,后面的代码都不再执行:
总体而言:
1.4.3 适配器模式
HandlerAdapter 在 Spring MVC 中使用了适配器模式
适配器模式定义:
适配器模式,也叫包装器模式。将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用。把两个不兼容的接口通过一定的方式使之兼容。
比如下面两个接口,本身是不兼容的(参数类型不一样,参数个数不一样等等)
可以通过适配器的方式,使之兼容
适配器模式角色
- Target: 目标接口(可以是抽象类或接口),客户希望直接用的接口
- Adaptee: 适配者,但是与Target不兼容
- Adapter: 适配器类,此模式的核心. 通过继承或者引用适配者的对象,把适配者转为目标接口
- client: 需要使用适配器的对象
适配器模式的实现
场景:前面学习的 slf4j 就使用了适配器模式,slf4j 提供了一系列打印日志的 api,底层调用的是 log4j 或者 logback 来打印日志,我们作为调用者,只需要调用 slf4j 的 api 就行了
java
// 用户使用接口
public interface Slf4jApi {
void log(String log);
}
// 真实实现方法
public class Log4jApi {
void print(String log) {
System.out.println("Log4jApi 打印日志:" + log);
}
}
// 写适配器,让用户可以从 Slf4jApi 调用到 Log4jApi 中的 print 方法
public class Slf4jLog4jAdapter implements Slf4jApi{
// 想调用 Log4jApi,先要有其对象
private Log4jApi log4jApi;
// 为使 log4jApi 不为空,下面重写构造方法,没有写默认的构造方法,因此,要使用 Slf4jLog4jAdapter 对象,必须要传入一个 Log4jApi 对象
public Slf4jLog4jAdapter(Log4jApi log4jApi) {
this.log4jApi = log4jApi;
}
@Override
public void log(String log) {
log4jApi.print(log);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Slf4jApi api = new Slf4jLog4jAdapter(new Log4jApi());
// 这样做,没有直接调用 Log4jApi 的 print 方法,但是使用的就是 print 方法
api.log("打印日志");
}
}
打印结果:
可以看出,我们不需改变 Log4jApi,仅需通过适配器转换,就可更换日志框架,保障系统的平稳运行
适配器模式应用场景
一般来说,适配器模式可以看作一种"补偿模式",用来补救设计上的缺陷。应用这种模式算是"无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了。
所以适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以复用原有代码实现新的功能。比如版本升级等。
适配器源码
2. 统一数据返回格式
前面图书管理系统中的强制登录模块,我们共做了两部分工作
-
通过 Session 来判断用户是否登录
-
对后端返回数据进行封装,告知前端处理的结果
后端统一返回结果
java@Data public class Result<T> { private int code; // 200 用户登录成功;-1 用户未登录;-2 用户登录成功,但程序出错 (业务状态码,非 http 状态码) private String errMsg; // 错误信息 private T data; }
后端逻辑处理
java// 查询图书信息(翻页使用) @RequestMapping("/getListByPage") public Result<PageResponse<BookInfo>> getListByPage(PageRequest pageRequest, HttpSession session) { log.info("获取图书列表,pageRequest:{}", pageRequest); // 用户登录,返回图书列表 PageResponse<BookInfo> bookInfoPageResponse = new PageResponse<>(); try { bookInfoPageResponse = bookService.getListByPage(pageRequest); } catch (Exception e) { log.error("获取图书列表失败"); return Result.fail(); } return Result.success(bookInfoPageResponse); }
Result.success(bookInfoPageResponse); 就是对返回数据进行了封装
拦截器帮我们实现了第一个功能,下面来看 Spring Boot 对第二个功能的支持实现
2.1 使用入门
统一的数据返回格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现 @ControllerAdvice 表示控制器通知类
添加类 ResponseAdvice,实现 ResponseBodyAdvice 接口,并在类上添加 @ControllerAdvice 注解
java
import com.example.springbook.model.Result;
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 {
// 是否支持对结果重写
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
// 如何对结果重写,其中 body 表示目标方法返回的结果
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return Result.success(body);
}
}
supports方法:判断是否要执行 beforeBodyWrite 方法。true为执行,false不执行。
通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理。
从returnType获取类名和方法名
java// 获取执行的类 Class<?> declaringClass = returnType.getMethod().getDeclaringClass(); // 获取执行的方法 Method method = returnType.getMethod();
beforeBodyWrite 方法:对 response 方法进行具体操作处理
测试:
2.2 存在问题
查询图书列表:
发现其原本就是 Result 类型,现又封装了一层,解决方法如下:
测试更新图书:将 活着2 修改为 活着
错误码 500,但是数据库中图书更新成功了,查看报错信息:
报了类型不匹配异常,是因为 body 是 Result 类型,而源码中的实现方法要接收的参数为 String 类型
解决方法:
2.3 优点
- 方便前端程序员更好的接收和解析后端数据接口返回的数据
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的。
- 有利于项目统一数据的维护和修改。
- 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容。
3. 统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类, @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件。
具体代码:
java
import com.example.springbook.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler
public Object handler(Exception e) {
log.error("发生异常,e:", e);
return Result.fail();
}
}
类名、方法名、返回值都可自定义,其中最重要的是注解
接口返回数据时,需要加 @ResponseBody 注解
模拟制造异常:
java
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
int a = 10 / 0;
return "t1";
}
@RequestMapping("/t2")
public int t2() {
String aa = null;
return aa.length();
}
@RequestMapping("/t3")
public int t3() {
int[] a = {1, 2, 3};
return a[4];
}
}
使用 postman 测试
针对不同异常,返回不同的结果:
当有多个异常通知时,匹配顺序为当前类及其子类向上一次匹配
/test/t2 抛出异常如下:
当指定了状态码后(使用注解 @ResponseStatus):/test/t3 抛出异常如下:
4. @ControllerAdvice 源码分析
4.1 下面是 ResponseAdvice 起的作用
4.2 下面是异常部分初始化做的事情:
4.2 异常是如何匹配的
测试 t3 为什么能找到数组越界异常
5. 改进图书管理系统代码
通过上面统一功能的添加,我们后端的接口已经发生了变化(后端返回的数据格式统一变成了 Result 类型),所以下面需要对前端代码进行修改
5.1 登录页面
登录界面没有拦截,只是返回结果发生了变化,所以只需根据返回结果修改对应代码即可
前端代码:
5.2 图书删除
发现其并不是 Result 类型返回,而是一个 String 类型,解决方法如下:
将其返回值类型改为 json 即可
5.3 登录错误
当服务重新启动,刷新图书列表页面,应该要跳转到登录页面,但现在却出现了如下情况:
这是因为请求失败时,状态码为 401,而不是 200,所以进不去前端代码中的 success
在 success 后面加上一个 error 即可:(http 请求成功会进入 success,失败会进入 error)
tip:区分,状态码表示业务的成功与失败
5.4 修改其他前端代码
- 删除图书
- 批量删除图书
- 添加图书
- 获取图书详情
- 修改图书
5.5 解决图标异常问题
方法一:捕获异常
方法二:找一个图标加上
网上随便找一个 ico 生成器,将生成的 favicon.ico 文件放到 static 文件夹下即可