1. 拦截器(Interceptor)
我们完成了强制登录的功能, 后端程序根据Session来判断用户是否登录, 但是实现⽅法是比较麻烦的。
1.1 什么是拦截器
拦截器是Spring框架提供的核心功能之⼀, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码
1.2 拦截器的基本使用
下⾯我们先来学习下拦截器的基本使⽤.
拦截器的使⽤步骤分为两步:
1. 定义拦截器
2. 注册配置拦截器
1.2.1 ⾃定义拦截器
实现HandlerInterceptor接⼝,并重写其所有⽅法
package com.example.demo.component;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
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 视图渲染完毕后执⾏,最后执⾏");
}
}
1.2.2 注册配置拦截器
实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法
package com.example.demo.configuration;
import com.example.demo.component.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
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.3 拦截器详解
拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。
拦截器的使⽤细节我们主要介绍两个部分:
1. 拦截器的拦截路径配置
2. 拦截器实现原理
1.3.1 拦截路径
拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.
我们在注册配置拦截器的时候,
通过 addPathPatterns() ⽅法指定要拦截哪些请求.(即就是让哪些地方的拦截操作生效)【上述代码中, 我们配置的是 /** , 表⽰拦截所有的请求】
也可以通过excludePathPatterns() 指定不拦截哪些请求
在拦截器中除了可以设置 /** 拦截所有资源外,还有⼀些常⻅拦截路径设置:
拦截路径 | 含义 | 举例 |
/* | ⼀级路径 | 能匹配/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 |
//后缀名被拦截:*.html,访问后缀名为html资源时,过滤器都会被执行
以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件, JS 和 CSS 等⽂件)
1.3.2 拦截器执行流程
正常的调⽤顺序:
有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图
2.登录校验
学习拦截器的基本操作之后,接下来我们需要完成最后⼀步操作:通过拦截器来完成图书管理系统中 的登录校验功能
2.1 定义拦截器
从session中获取⽤⼾信息, 如果session中不存在, 则返回false,并设置http状态码为401, 否则返回true.
2.2 注册配置拦截器
同时,我们调用方法时发现可以传递List<>
故上述代码也可以改成
删除之前的登录校验代码
运行程序, 通过Postman进⾏测试:
1. 查看图书列表
2. 登录之后再次进行图书列表的查看
3.DispatcherServlet 源码分析(dispatch派遣)
当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.
所有请求都会先进到DispatcherServlet,执⾏doDispatch 调度⽅法.
如果有拦截器, 会先执⾏拦截器preHandle() ⽅法的代码, 如果 preHandle() 返回true, 继续访问controller中的⽅法. controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 和 afterCompletion() ,返回给DispatcherServlet,最终给浏览器响应数据.
3.1 初始化
DispatcherServlet的初始化⽅法 init() 在其⽗类 HttpServletBean 中实现的.
主要作⽤是加载 web.xml 中 DispatcherServlet 的 配置, 并调⽤⼦类的初始化.
web.xml是web项⽬的配置⽂件,⼀般的web⼯程都会⽤到web.xml来配置,主要⽤来配置
Listener,Filter,Servlet等, Spring框架从3.1版本开始⽀持Servlet3.0, 并且从3.2版本开始通过配置DispatcherServlet, 实现不再使⽤web.xml
3.2 处理请求
DispatcherServlet 接收到请求后, 执⾏doDispatch 调度⽅法, 再将请求转给Controller.
我们来看doDispatch ⽅法的具体实现
查看源码方式:
ctrl+N全局搜索类,ctrl+R在一个类中搜索方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
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;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
4.3 适配器模式
HandlerAdapter 在 Spring MVC 中使⽤了适配器模式
适配器模式定义
适配器模式, 也叫包装器模式.
将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝, 适配器让原本接⼝不兼容的类可以合作⽆间.
简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤. 把两个不兼容的接⼝通过⼀定的⽅式使之兼容.
⽐如下⾯两个接⼝, 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)
不兼容的两个接口
通过适配器的⽅式, 兼容的A,B接口
适配器模式⻆⾊
• Target: ⽬标接⼝ (可以是抽象类或接⼝), 客⼾希望直接⽤的接⼝
• Adaptee: 适配者, 但是与Target不兼容
• Adapter: 适配器类, 此模式的核⼼. 通过继承或者引⽤适配者的对象, 把适配者转为⽬标接⼝
• client: 需要使⽤适配器的对象
适配器模式应⽤场景
⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之 举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新 的功能. ⽐如版本升级等.
4. 统⼀数据返回格式
强制登录案例中, 我们共做了两部分⼯作
1. 通过Session来判断⽤⼾是否登录
2. 对后端返回数据进⾏封装, 告知前端处理的结果
拦截器帮我们实现了第⼀个功能, 接下来看SpringBoot对第⼆个功能如何⽀持
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获取类名和⽅法名
• beforeBodyWrite⽅法: 对response⽅法进⾏具体操作处理
4.1 测试
测试
添加统⼀数据返回格式之前:
添加统⼀数据返回格式之后:
加此注解
4.2 存在问题
结果显⽰(500), 发⽣内部错误
查看数据库, 发现数据操作成功
查看⽇志, ⽇志报错
4.2.1 测试方法
多测试⼏种不同的返回结果
测试代码:
测试结果
发现只有返回结果为String类型时才有这种错误发⽣!!!
4.2.2 解决⽅案
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
private static ObjectMapper mapper=new ObjectMapper();
@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) {
if(body instanceof String){//如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化
try {
return mapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return Result.success(body);
}
}
成功!!!
4.2.3 原因分析
4.2.4 案例代码修改
如果⼀些⽅法返回的结果已经是Result类型了, 那就直接返回Result类型的结果即可
5.统⼀异常处理
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的, @ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件
以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类), 就返回⼀个 Result的对象, Result 对象的设置参考 Result.fail(e.getMessage()
我们可以针对不同的异常, 返回不同的结果
测试结果
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
6.@ControllerAdvice 源码分析
统⼀数据返回和统⼀异常都是基于 @ControllerAdvice 注解来实现的, 通过分析@ControllerAdvice 的源码, 可以知道他们的执⾏流程.
- 从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件, 这也就是为什么没有五 ⼤注解, ControllerAdvice 就⽣效的原因.
- 下⾯我们看看Spring是怎么实现的, 还是从 DispatcherServlet 的代码开始分析.
- DispatcherServlet 对象在创建时会初始化⼀系列的对象
public class DispatcherServlet extends FrameworkServlet {
//...
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further
strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
//...
}
对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context) 和
initHandlerExceptionResolvers(context) 这两个⽅法.
6.1. initHandlerAdapters(context)
initHandlerAdapters(context) ⽅法会取得所有实现了 HandlerAdapter 接⼝的bean并
保存起来,其中有⼀个类型为 RequestMappingHandlerAdapter 的bean,这个bean就是
@RequestMapping 注解能起作⽤的关键,这个bean在应⽤启动过程中会获取所有被@ControllerAdvice 注解标注的bean对象, 并做进⼀步处理。
6.2. initHandlerExceptionResolvers(context)
接下来看 DispatcherServlet 的 initHandlerExceptionResolvers(context) ⽅法,
这个⽅法会取得所有实现了 HandlerExceptionResolver 接⼝的bean并保存起来,其中就有⼀
个类型为 ExceptionHandlerExceptionResolver 的bean,这个bean在应⽤启动过程中会获
取所有被 @ControllerAdvice 注解标注的bean对象做进⼀步处理
7. 案例代码
通过上⾯统⼀功能的添加, 我们后端的接⼝已经发⽣了变化(后端返回的数据格式统⼀变成了Result类型), 所以我们需要对前端代码进⾏修改
7.1 登录页面
登录界⾯没有拦截, 只是返回结果发⽣了变化, 所以只需要根据返回结果修改对应代码即可
登录结果代码修改
tips:可以根据返回的东西更新前端返回代码
7.2图书列表
针对图书列表⻚有两处变化
1. 拦截器进⾏了强制登录校验, 如果校验失败, 则http状态码返回401, 此时会⾛ajax的error逻辑处理
2. 接⼝返回结果发⽣了变化
图书列表代码修改
7.3 其他
参考图书列表, 对删除图书, 批量删除图书,添加图书, 修改图书接⼝添加⽤⼾强制登录以及统⼀格式返回的逻辑处理
7.4 测试
先后端接口测试后前端测试
//修改时遇到的问题
//统一返回String类型出现错误
//改正1.前端处理
//改正2.接口设置返回类型