目录
[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 注册配置拦截器)
[2. 统⼀数据返回格式](#2. 统⼀数据返回格式)
[2.1 快速⼊⻔](#2.1 快速⼊⻔)
[2.2 存在问题](#2.2 存在问题)
[2.3 案例代码修改](#2.3 案例代码修改)
[2.4 优点](#2.4 优点)
[3. 统⼀异常处理](#3. 统⼀异常处理)
1. 拦截器
上个章节我们完成了强制登录的功能, 后端程序根据Session来判断⽤⼾是否登录, 但是实现⽅法是⽐较⿇烦的
• 需要修改每个接⼝的处理逻辑
• 需要修改每个接⼝的返回结果
• 接⼝定义修改, 前端代码也需要跟着修改
有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 这⾥我们学习⼀种新的解决办法: 拦截器
1.1 拦截器快速入门
什么是拦截器?
拦截器是Spring框架提供的核⼼功能之⼀, 主要⽤来拦截⽤⼾的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码.
也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也可以在⽤⼾请求前阻⽌其执⾏.
在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求, 判断Session中是否有登录⽤⼾的信息. 如果有就可以放⾏, 如果没有就进⾏拦截.

⽐如我们去银⾏办理业务, 在办理业务前后, 就可以加⼀些拦截操作
办理业务之前, 先取号, 如果带⾝份证了就取号成功
业务办理结束, 给业务办理⼈员的服务进⾏评价.
这些就是"拦截器"做的⼯作
下⾯我们先来学习下拦截器的基本使⽤.
拦截器的使⽤步骤分为两步:
- 定义拦截器
- 注册配置拦截器
⾃定义拦截器:实现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()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图, 暂不了
解)
**注册配置拦截器:**实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//⾃定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册⾃定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表⽰拦截所
有请求)
}
}
启动服务, 试试访问任意请求
preHandle ⽅法执⾏之后就放⾏了, 开始执⾏⽬标⽅法, ⽬标⽅法执⾏完成之后执⾏
postHandle和afterCompletion⽅法.
我们把拦截器中preHandle⽅法的返回值改为false,拦截器拦截了请求, 没有进⾏响应
1.2 拦截器详解
拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。拦截器的使⽤细节我们主要介绍两个部分:
- 拦截器的拦截路径配置
- 拦截器实现原理
1.2.1 拦截路径
拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.
我们在注册配置拦截器的时候, 通过 addPathPatterns() ⽅法指定要拦截哪些请求. 也可以通过
excludePathPatterns() 指定不拦截哪些请求.
上述代码中, 我们配置的是 /** , 表⽰拦截所有的请求.
⽐如⽤⼾登录校验, 我们希望可以对除了登录之外所有的路径⽣效
java
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("/**")
.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,不能匹配/user/login |
以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件, JS 和 CSS 等⽂件).
1.2.2 拦截器执行流程
正常的调用顺序:

有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图

- 添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法,
这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的
⽅法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏). - controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及 afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据.
1.3 登录校验
学习拦截器的基本操作之后,接下来我们需要完成最后⼀步操作:通过拦截器来完成图书管理系统中的登录校验功能
1.3.1 定义拦截器
从session中获取⽤⼾信息, 如果session中不存在, 则返回false,并设置http状态码为401, 否则返回true.
java
import com.example.demo.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute(Constants.SESSION_USER_KEY) != null) {
return true;
}
response.setStatus(401);
return false;
}
}
1.3.2 注册配置拦截器
java
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("/**")//设置拦截器拦截的请求路径(/**表示拦截所有请求)
.excludePathPatterns("/user/login")//设置拦截器排除拦截的路径
.excludePathPatterns("/**/*.js") //排除前端静态资源
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/*.html");
}
}
也可以改成
java
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;
import java.util.Arrays;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
private List<String> excludePaths = Arrays.asList(
"/user/login",
"/**/*.js",
"/**/*.css",
"/**/*.png",
"/**/*.html"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径(/** 表示拦截所有请求)
.excludePathPatterns(excludePaths);//设置拦截器排除拦截的路径
}
}
删除之前的登录校验代码
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 || "".equals(userInfo.getUserName())){
// return Result.unlogin();
// }
//用户登录, 返回图书列表
PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);
log.info("获取图书列表222, pageRequest:{}", pageResult);
return Result.success(pageResult);
}
2. 统⼀数据返回格式
强制登录案例中, 我们共做了两部分⼯作
- 通过Session来判断⽤⼾是否登录
- 对后端返回数据进⾏封装, 告知前端处理的结果
回顾
后端统⼀返回结果
java
@Data
public class Result<T> {
private int status;
private String errorMessage;
private T data;
}
后端逻辑处理
java
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
log.info("获取图书列表, pageRequest:{}", pageRequest);
//用户登录, 返回图书列表
PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);
log.info("获取图书列表222, pageRequest:{}", pageResult);
return Result.success(pageResult);
}
Result.success(pageResult) 就是对返回数据进行了封装
拦截器帮我们实现了第⼀个功能, 接下来看SpringBoot对第⼆个功能如何⽀持
2.1 快速⼊⻔
统⼀的数据返回格式使⽤ @ControllerAdvice 和 ResponseBodyAdvice 的⽅式实现
@ControllerAdvice 表⽰控制器通知类
添加类 ResponseAdvice , 实现 ResponseBodyAdvice 接⼝, 并在类上添加@ControllerAdvice 注解
java
import com.example.demo.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;
}
@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 存在问题
当返回结果为String类型时会有错误发⽣
解决⽅案:
java
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@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) {
if(body instanceof String){
return objectMapper.writeValueAsString(Result.success(body));
}
if(body instanceof Result){
return body;
}
return Result.success(body);
}
}
这时,测试会正常
ObjectMapper 是 Jackson 包 里的核心类(SpringBoot 默认 JSON 序列化工具),作用: Java 对象 ↔ JSON 字符串 互相转换
原因分析:
SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter (从先后顺序排列分别为
ByteArrayHttpMessageConverter ,
StringHttpMessageConverter , SourceHttpMessageConverter ,
SourceHttpMessageConverter , AllEncompassingFormHttpMessageConverter )
java
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
//...
public RequestMappingHandlerAdapter() {
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
//...
}
其中AllEncompassingFormHttpMessageConverter 会根据项⽬依赖情况 添加对应的
HttpMessageConverter
java
public AllEncompassingFormHttpMessageConverter() {
if (!shouldIgnoreXml) {
try {
addPartConverter(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
}
if (kotlinSerializationJsonPresent) {
addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
addPartConverter(new JsonbHttpMessageConverter());
}
if (jackson2XmlPresent && !shouldIgnoreXml) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
if (jackson2SmilePresent) {
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
}
在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到
messageConverters 链的末尾.
Spring会根据返回的数据类型, 从 messageConverters 链选择合适的HttpMessageConverter .
当返回的数据是⾮字符串时, 使⽤的 MappingJackson2HttpMessageConverter 写⼊返回对象.
当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为
StringHttpMessageConverter 可以使⽤.
java
public abstract class AbstractMessageConverterMethodProcessor extends
AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
//...代码省略
protected <T> void writeWithMessageConverters(@Nullable T value,
MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse
outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException,
HttpMessageNotWritableException {
//...代码省略
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter
instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter)
converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
//getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的
结果
body = getAdvice().beforeBodyWrite(body, returnType,
selectedMediaType,
(Class<? extends HttpMessageConverter<?>>)
converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody,
!traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType,
selectedMediaType, outputMessage);
}
else {
//此时cover为StringHttpMessageConverter
((HttpMessageConverter) converter).write(body,
selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
//...代码省略
}
//...代码省略
}
在 ((HttpMessageConverter) converter).write(body, selectedMediaType,
outputMessage) 的处理中, 调⽤⽗类的write⽅法
由于 StringHttpMessageConverter 重写了addDefaultHeaders⽅法, 所以会执⾏⼦类的⽅法
然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String, 此
时t为Result类型, 所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常
2.3 案例代码修改
如果⼀些⽅法返回的结果已经是Result类型了, 那就直接返回Result类型的结果即可
java
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
request, ServerHttpResponse response) {
//返回结果更加灵活
if (body instanceof Result){
return body;
}
//如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化
if (body instanceof String){
return mapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
2.4 优点
- ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据
- 降低前端程序员和后端程序员的沟通成本, 按照某个格式实现就可以了, 因为所有接⼝都是这样返回的.
- 有利于项⽬统⼀数据的维护和修改.
- 有利于后端技术部⻔的统⼀规范的标准制定, 不会出现稀奇古怪的返回内容.
3. 统⼀异常处理
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,
@ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表
⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件
具体代码如下:
java
import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
}
这是全局异常处理器:
@ControllerAdvice:作用于所有 Controller@ResponseBody:方法返回值直接作为响应体返回 JSON(也可以写在类上)@ExceptionHandler:捕获异常,不传参数时默认捕获Exception,也就是所有异常- 捕获到异常后,封装为失败的
Result对象返回给前端,统一返回格式
类名, ⽅法名和返回值可以⾃定义, 重要的是注解
接⼝返回为数据时, 需要加 @ResponseBody 注解
以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类), 就返回⼀个 Result的对象, Result对象的设置参考 Result.fail(e.getMessage())
java
public static Result fail(String msg) {
Result result = new Result();
result.setStatus(ResultStatus.FAIL);
result.setErrorMessage(msg);
result.setData("");
return result;
}
我们可以针对不同的异常, 返回不同的结果.
java
import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.fail("发⽣NullPointerException:"+e.getMessage());
}
@ExceptionHandler
public Object handler(ArithmeticException e) {
return Result.fail("发⽣ArithmeticException:"+e.getMessage());
}
}
模拟制造异常:
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(){
return "t1";
}
@RequestMapping("/t2")
public boolean t2(){
int a = 10/0; //抛出ArithmeticException
return true;
}
@RequestMapping("/t3")
public Integer t3(){
String a =null;
System.out.println(a.length()); //抛出NullPointerException
return 200;
}
}
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配