一.拦截器
在上一章我们完成了强制登入这个功能,后端可以通过session来判断用户是否登入,但是实现方法是比较麻烦的,需要修改每个接口的处理逻辑和返回结果,接口定义修改,前端也需要跟着修改.
所以在这里,我们学习一种新的解决办法,拦截器,用来统一拦截所有请求,并进行session校验.
1.1 拦截器快速入门
什么是拦截器(interceptor)?
拦截器是spring框架提供的核心功能之一,主要是用来拦截用户的请求,在执行制定方法前后,根据业务需要执行预先设定的代码.
下面我们先来学习下拦截器的基本使用.
使用拦截器分为两步:定义拦截器 和注册配置拦截器.
自定义拦截器: 实现HandlerInterceptor接口,并重写其所有方法.
HandlerInterceptor 是 SpringMVC 提供的拦截器接口,作用于 Controller 请求执行前后,对接口请求做拦截、预处理、后置处理。
- preHandle () 方法:目标方法执行前执行。返回 true: 继续执行后续操作;返回 false: 中断后续操作.
- postHandle () 方法:目标方法执行后执行
- afterCompletion () 方法:视图渲染完毕后执行,最后执行 (后端开发现在几乎不涉及视图,暂不了解)
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视图渲染完毕后执行,最后执行...");
}
}
这里我们已经重写了HandlerInterceptor的方法,接着注册配置拦截器.
注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptor方法.
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()指定不拦截哪些请求.
在上述代码中,我们配置的是/**,表示对所有路径生效.
此时我们可以看到无论我们用什么接口都会经过拦截器.
我们想要的是拦截器可以对除了登入以外的所有路径生效.

在拦截器中,除了可以设置/**拦截所有资源以外,还有一些常见的拦截路径的设置.

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
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if(session != null && session.getAttribute(Constants.SESSION_USER_KEY) != null){
return true;
}
response.setStatus(401);
return false;
}
1.3.2 注册配置拦截器
java
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
@RequestMapping("/getBookListByPage")
public Result getBookListByPage(PageInfo pageinfo, HttpSession ssession){
log.info("获取图书列表:"+ pageinfo);
//判断用户是否登入
// if(ssession.getAttribute(Constants.SESSION_USER_KEY) == null){
// log.info("用户未登入");
// return Result.unlogin();
// }
UserInfo userInfo = (UserInfo) ssession.getAttribute(Constants.SESSION_USER_KEY);
// if (userInfo == null || userInfo.getId() == null ||userInfo.getId() < 0||"".equals(userInfo.getUserName())){
// log.info("用户信息错误");
// return Result.unlogin();
// }
PageResult<BookInfo> list = bookService.getBookListByPage(pageinfo);
log.info("用户:" + userInfo.getUserName() + "获取图书列表");
return Result.success(list);
}
再次运行程序,通过postman来测试.
查看图书列表:

可以看到在没登入的时候状态码为401,并且被拦截了.

然后我们先登入,再查看图书列表.


此时就可以成功拿到数据.
1.4 DispatcherServlet源码分析(了解)
通过观察我们的服务启动日志.

我们可以看到,在tomcat启动之后,有一个核心的类DispatcherServlet,他来控制程序的执行顺序.
现在要了解源码还是有些困难,简单来说,DispatcherServlet是springMVC的总调度员,Tomcat负责接收请求,DispatcherServlet负责把请求分发给正确的controller.
所有的请求都会先进行到DispatcherServlet,执行doDispatch调度方法,如果有拦截器,会先执行拦截器的preHandle()方法的代码.如果返回true,继续访问controller中的方法,controller中的方法执行完毕后,再返回来执行postHandle()和afterCompletion()方法,最后返回给DispatcherServlet,最终给浏览器响应数据.

1.4.1 初始化(了解)

1.4.2 处理请求(核心)
DispatcherServlet接收到请求后,执行doDispatch调度方法,再将请求转给Controller.
我们看一下doDispatch方法的具体实现.

我们不需要全部看懂这段源码,简单来说流程就是:

这里提到了HandlerAdapter,使用的是适配器模式,下面会详细介绍.
HandlerAdapter的作用是把"怎么调用 Controller"这件复杂的事情封装起来,让 DispatcherServlet 只需要调用统一的 handle()。
1.4.3 适配器模式
HandlerAdapter 在SpringMVC中使用了适配器模式
适配器模式,也叫包装器模式。将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用。把两个不兼容的接口通过一定的方式使之兼容。

之前我们使用slf4j就是用的是适配器模式,slf4j提供了一系列的打印日志的api,底层调用的是log4j或者logback来打印日志,但是我们作为调用者,只需要调用slf4j的api就可以了.

可以看出,我们不需要改变log4j的api,只需要通过适配器转换,就可以更换日志框架,保障系统的平稳运行.
一般来说,适配器模式可以看作一种 "补偿模式",用来补救设计上的缺陷。应用这种模式算是 "无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了
所以适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以复用原有代码实现新的功能。比如版本升级等。
二.统一数据返回格式
在强制登录案例中,我们一共做了两部分的工作.
1.通过session来判断用户是否登入.
2.对后端返回数据进行封装,告知前端处理的结果.


拦截器帮我们实现了第一个功能,接下来看SpringBoot对第二个功能如何支持.
2.1 快速入门
统一的数据返回格式使用@ControllerAdvice和ResponseBodyAdvice的方式实现
@ControllerAdvice表示控制器通知类.
添加类ResponseBodyAdvice,实现ResponseBodyAdvice接口,并在类上添加@ControllerAdvice注解.
java
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
//返回true表示全部处理
}
@Override
public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
log.info("执行beforeBodyWrite");
return Result.success(body);
}
}
supports方法表示对那些方法进行处理,返回true表示全部都处理.
beforeBodyWrite方法表示的是在返回body前做的处理.
其中body就是方法返回的内容,这里我们再包装一层result.
这个是没有进行统一结果处理的时候,只返回一个true.

进行统一结果处理:

此时就把true包装成Result类了.
我们再测试其他接口.
获取图书列表接口,这里我们就发现了问题,因为我们之前已经对图书列表进行了Result类的包装,所以这里我们不希望再次进行包装,后续需要单独进行处理.

测试删除图书接口:
这里又发现一个问题,我们先看看数据库结果是否更改.

可以看到这里数据库中id为1的图书状态已经改为0,所以说删除图书已经生效,让我们看看报错信息.

可以发现,报错信息大致说的是Result不能匹配String.
这里我们进行几个测试.

测试t1

t2:

t3:

t4:

有兴趣的也可以去测试一下别的类型,结果就是只有返回类型为String的时候会进行报错.
2.2 存在问题
1.会对已经包装过的返回结果再次进行包装.
这里我们需要在beforeBodyWrite方法中进行判断,如果结果已经是Result类型直接返回即可.

再次测试图书列表接口.不会被多次包装.

2.当返回类型为String类型时会报错.
当body为String类型的时候,我们需要转换为Json.

此时测试删除图书接口,就正常了.但是此时这个结果是一个字符串,我们还需要转换为JSON类型.

在注解后面添加produce转换为JSON格式.

再次运行.结果就是JSON格式了.我们需要在返回类型为String的接口都加上.

这么看String类型还是需要一个一个手动加有点麻烦,所以说我们在开发的时候还是尽量避免使用string类型.前端处理起来比较麻烦.
2.3 优点
1.方便前端程序员更好的接收和解析后端数据接口返回的数据
2.降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的。
3.有利于项目统一数据的维护和修改。
4.有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容。
三.统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的, @ControllerAdvice
表示控制器通知类, @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
添加图书,漏掉一个publish参数,应该报错,但是这边给前端的code还是200,这样就不合理了.
出现这种情况的原因,是因为我们在这边对结果统一用的success进行包装,


所以接下来我们要对这种情况进行处理.
当我们发现接口中出现异常情况,就不应该使用success进行包装,要对不同的异常进行不同处理.

这里我们先小试一下,创建Exception.class文件,然后通过@ControllerAdvice来处理所有Controller抛出的异常,@ExceptionHandler是用来对不同异常分别进行处理.
我们之前已经了解过不同异常,这里我们先写个笼统的Exception,然后做个测试.

测试一下t1,这里会发生异常.看看能不能进行正确处理

可以看到这边处理结果和预想一样
尝试去除统一异常处理,再次测试.
再次运行t1.就还是会出现之前不合理的情况.

这个是对所有的异常进行一个统一处理,我们还可以细分异常的各种类型.比如数组越界异常,空指针异常等等.接下来我们来测试其他异常


分别运行t2,t3,t1



至于前端代码,我们只需要修改一下对应的接口就可以正常运行了.
有关SpringBoot统一功能处理相关的内容就讲到这里了.