文章目录
- 拦截器
- 什么是拦截器?
- 为什么要使用拦截器?
- 拦截器的使用
- 拦截路径
- 执行流程
- 典型应用场景
- DispatcherServlet源码分析
- 适配器模式
- 适配器模式定义
- 适配器模式角色
- 适配器模式的实现
- 适配器模式应用场景
- 统⼀数据返回格式
- 优点
- 统一处理异常
- 总结
拦截器
什么是拦截器?
拦截器是Spring框架提供的核心功能之⼀,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码.
为什么要使用拦截器?
在拦截器当中,开发人员可以在应用程序中做⼀些通用性的操作,如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息.如果有就可以放行,如果没有就进行拦截.
比如我们去医院看病,在看病前后,就可以加⼀些拦截操作
看医生之前,先挂号,如果带身份证了就挂号成功
看完医生,给医护人员的服务进行评价.
再比如我们去银行办理业务
办理业务之前,先取号,如果带身份证了就挂号成功
业务办理结束,给业务办理人员的服务进行评价.
这些就是"拦截器"做的工作.
拦截器的使用
1.定义拦截器
2.注册配置拦截器
需求:写一个登录拦截器
自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 请求到达 Controller 前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
// Controller 执行后,视图渲染前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// 可修改响应数据(如统一包装返回值)
}
// 请求完成后执行(视图渲染完毕)
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
}
-
preHandle
:在请求处理之前执行(在控制器方法执行之前)。返回true表示继续流程,返回false则中断请求。 -
postHandle
:在请求处理之后,视图渲染之前执行 -
注意:如果控制器方法中发生了异常,则该方法不会被执行。
-
afterCompletion
:在整个请求结束之后执行(视图渲染完成之后),通常用于资源清理等。无论是否有异常,该方法都会执行。(后端开发现在几乎不涉及视图,暂不了解)
注册配置拦截器:创建一个配置类,实现WebMvcConfigurer接口,并重写addInterceptors方法,指定拦截的路径模式
拦截路径
拦截路径是指我们定义的这个拦截器,对哪些请求生效.
我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些请求.也可以通过excludePathPatterns() 指定不拦截哪些请求.
常见的拦截路径设置:
拦截路径 | 含义 |
---|---|
/* | ⼀级路径 |
/** | 任意级路径 |
/book/* | /book下的⼀级路径 |
/book/** | /book下的任意级路径 |
以上拦截规则可以拦截此项目中的使用URL,包括静态文件(图片文件,JS和CSS等文件).
示例:拦截blog,user路径的任意路径,除了路径为user/login
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/blog/**", "/user/**")
.excludePathPatterns("/user/login");
}
}
执行流程
正常的调用顺序
有了拦截器之后,会在调用Controller之前进行相应的业务处理,执行的流程如下图
- 添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住.执行 preHandle() 方法.这个方法需要返回⼀个布尔类型的值.
如果返回true,就表示放行本次操作,继续访问controller中的方法.
如果返回false,则不会放行(controller中的方法也不会执行). - controller当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及
afterCompletion()方法,执行完毕之后,最终给浏览器响应数据.
典型应用场景
场景 | 实现方式 | 示例 |
---|---|---|
身份认证 | preHandle 中校验 Token | 拦截未登录用户访问 /user/** |
权限控制 | preHandle 中检查角色 | 禁止普通用户访问 /admin/** |
请求日志 | preHandle 记录入参 | 打印请求 URL、IP、参数 |
性能监控 | preHandle 开始计时,afterCompletion 结束 | 统计接口耗时 |
数据预处理 | preHandle 设置线程变量 | 存储当前用户信息到 ThreadLocal |
响应统一包装 | postHandle 修改响应结构 | 将返回值包装为 {code:200, data:...} |
资源清理 | afterCompletion 释放资源 | 清除 ThreadLocal 数据 |
示例:对于没有提供token的路径不允许访问,必须先进行登录操作,除了user/login的路径之外都要进行拦截
LoginInterceptor 类
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前" );
HttpSession session=request.getSession(false);
if(!checkUser(session)){
//设置状态码
response.setStatus(401);
response.setContentType("text/html;charset=utf-8");
String msg="用户未登录";
response.getOutputStream().write(msg.getBytes(StandardCharsets.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;
}
log.info("用户已登录");
return true;
}
}
WebConfig 类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
//注入登录拦截器
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/book/**").excludePathPatterns("/user/login");//设置拦截器拦截的请求路径( /** 表⽰拦截有请求
}
}
http的状态码为401
登录成功之后可进行访问
DispatcherServlet源码分析
当Tomcat启动之后,有⼀个核心的类DispatcherServlet,它来控制程序的执行顺序.
所有请求都会先进到DispatcherServlet执行doDispatch调度方法.
如果有拦截器,会先执行拦截器preHandle() 方法的代码,如果 preHandle() 返回true,继续访问controller中的方法.
controller当中的方法执行完毕后,再回过来执行 postHandle() 和afterCompletion() ,返回给DispatcherServlet,最终给浏览器响应数据.
继承关系
public class DispatcherServlet extends FrameworkServlet {
// 核心实现
}
FrameworkServlet:继承自 HttpServletBean
HttpServletBean:继承自 HttpServlet
初始化
DispatcherServlet的初始化方法init()是在其父类HttpServletBean中实现的
主要作用是加载web.xml中DispatcherServlet的配置,并调用子类的初始化.
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
在HttpServletBean 的init() 中调用了initServletBean() ,它是在FrameworkServlet类中实现的,主要作用是建立WebApplicationContext容器(有时也称上下文),加载SpringMVC配置⽂件中定义的Bean到该容器中,最后将该容器添加到ServletContext中.
下面是initServletBean()的具体代码:
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
此处打印的日志,也正是控制台打印出来的日志
在initStrategies()中的 onRefresh() 方法中调用 initStrategies() 初始化九大组件:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context); // 文件上传解析器
initLocaleResolver(context); // 本地化解析器
initThemeResolver(context); // 主题解析器
initHandlerMappings(context); // 处理器映射器(重点)
initHandlerAdapters(context); // 处理器适配器(重点)
initHandlerExceptionResolvers(context); // 异常解析器
initRequestToViewNameTranslator(context);
initViewResolvers(context); // 视图解析器
initFlashMapManager(context); // FlashMap管理器
}
处理核心(doDispatch)
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 {
// 1. 文件上传处理
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 2. 获取处理器执行链
//遍历所有的 HandlerMapping 找到与请求对应的Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 3. 获取处理器适配器
//遍历所有的 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;
}
}
// 4. 执行拦截器preHandle()
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;// 被拦截
}
// 5. 实际执行目标方法(Controller方法)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 7. 执行拦截器postHandle()方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
// 8. 处理分发结果(渲染视图)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 9. 执行拦截器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);
}
}
}
}
HandlerAdapter主要用于支持不同类型的处理器(如Controller、HttpRequestHandler或者Servlet等),让它们能够适配统⼀的请求处理流程。这样,SpringMVC可以通过⼀个统⼀的接口来处理来自各种处理器的请求.
从上述源码可以看出在开始执行Controller之前,会先调用预处理方法applyPreHandle
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
// 获取项⽬中使⽤的拦截器 HandlerInterceptor
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
在applyPreHandle中会获取所有的拦截器HandlerInterceptor ,并执行拦截器中的preHandle方法
适配器模式
HandlerAdapter在SpringMVC中使用了适配器模式
适配器模式定义
适配器模式,也叫包装器模式.将⼀个类的接口,转换成客户期望的另⼀个接口,适配器让原本接口不兼
容的类可以合作无间.
简单来说就是目标类不能直接使用,通过⼀个新类进行包装⼀下,适配调用方使用.把两个不兼容的接口通过⼀定的方式使之兼容
比如下面两个接口,本身是不兼容的(参数类型不⼀样,参数个数不⼀样等等)
可以通过适配器的方式,使之兼容
日常生活中的例子:转换插头,网络转接头
适配器模式角色
• Target:目标接口(可以是抽象类或接口),客户希望直接用的接口
• Adaptee:适配者,但是与Target不兼容
• Adapter:适配器类,此模式的核心.通过继承或者引用适配者的对象,把适配者转为目标接口
• client:需要使用适配器的对象
适配器模式的实现
场景:前面学习的slf4j就使用了适配器模式,slf4j提供了⼀系列打印日志的api,底层调用的是log4j或者
logback来打日志,我们作为调用者,只需要调用slf4j的api就行了.
/**
* slf4j接口
*/
interface Slf4jApi{
void log(String message);
}
/**
* log4j 接口
*/
class Log4j{
void log4jLog(String message){
System.out.println("Log4j打印:"+message);
}
}
/**
* slf4j和log4j适配器
*/
class Slf4jLog4JAdapter implements Slf4jApi{
private Log4j log4j;
public Slf4jLog4JAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jLog(message);
}
}
/**
* 客户端调用
*/
public class Slf4jDemo {
public static void main(String[] args) {
Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());
slf4jApi.log("使⽤slf4j打印⽇志");
}
}
可以看出,我们不需要改变log4j的api,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行.
适配器模式应用场景
⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷.应用这种模式算是"无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了
所以适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以复用原有代码实现新的功能.比如版本升级等.
统⼀数据返回格式
统⼀的数据返回格式使用@ControllerAdvice 和ResponseBodyAdvice 的方式实现@ControllerAdvice 表示控制器通知类
添加类 ResponseAdvice ,实现 ResponseBodyAdvice 接口,并在类上添加@ControllerAdvice 注解
@ResponseBody
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
//使用Springboot内置的jackson实现信息的序列化
private static ObjectMapper mapper=new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
//统一处理返回给前端的数据
@SneakyThrows//lombok,直接对方法抛异常
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof String){
return mapper.writeValueAsString(Result.success(body));
}
if(body instanceof Result){
return body;
}
return Result.success(body) ;
}
}
• supports方法:判断是否要执行beforeBodyWrite方法.true为执行,false不执行.
通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理.
• beforeBodyWrite方法:对response方法进行具体操作处理
存在问题:
当返回值结果是String类型,需要使用SpringBoot内置提供的Jackson实现信息的序列化
使用writeValueAsString方法把Object对象转换成String类型
private static ObjectMapper mapper=new ObjectMapper();
if(body instanceof String){
return mapper.writeValueAsString(Result.success(body));
}
优点
1.方便前端程序员更好的接收和解析后端数据接口返回的数据
2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回
的.
3. 有利于项目统⼀数据的维护和修改.
4. 有利于后端技术部门的统⼀规范的标准制定,不会出现稀奇古怪的返回内容.
统一处理异常
统⼀异常处理使用的是@ControllerAdvice +@ExceptionHandler 来实现的
@ControllerAdvice 表示控制器通知类, @ExceptionHandler 是异常处理器,两个结合表
示当出现异常的时候执行某个通知,也就是执行某个方法事件
@RestControllerAdvice=@ResponseBody+@ControllerAdvice
针对不同的异常返回不同的结果
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
//内部异常
@ExceptionHandler(Exception.class)
public Result handler(Exception e){
log.error("发生异常, e:", e);
return Result.error("内部错误, 请联系管理员");
}
//空指针异常
@ExceptionHandler(NullPointerException.class)
public Result handler2(Exception e){
log.error("发生异常, e:", e);
return Result.error("发生空指针异常, 请联系管理员");
}
//数组越界异常
@ExceptionHandler(IndexOutOfBoundsException.class)
public Result handler3(Exception e){
log.error("发生异常, e:", e);
return Result.error("数组越界异常, 请联系管理员");
}
}
当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配
总结
- 拦截器的实现主要分两部分:定义拦截器(实现HandlerInterceptor接口)和配置拦截器
- 统⼀数据返回格式通过@ControllerAdvice+ResponseBodyAdvice来实现
- 统⼀异常处理使用@ControllerAdvice+@ExceptionHandler来实现,并且可以分异常来处理
- 学习了DispatcherServlet的⼀些源码.