一: SpringBoot 统一功能处理
1.1 拦截器
拦截器是 Spring 框架提供的核心功能之⼀, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码,也就是说, spring 允许开发人员提前预定义⼀些逻辑, 在用户的请求响应前后执行. 也可以在用户请求前阻止其执行.
这好比我们去银行办理业务, 在办理业务前后, 就可以加⼀些拦截操作,办理业务之前, 先取号, 如果带身份证了就取号成功,业务办理结束, 给业务办理人员的服务进行评价,这些就是 “拦截器” 做的工作.
1.1.1 拦截器的基本使用
拦截器的使用步骤分为两步:
- 定义拦截器
- 注册配置拦截器
第一步:自定义拦截器:实现 HandlerInterceptor 接口,并重写其所有方法
@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 方法
@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.1.2 拦截器详解
接下来我们来介绍拦截器的使用细节。拦截器的使用细节我们主要介绍两个部分:
- 拦截器的拦截路径配置
- 拦截器实现原理
1.1.2.1 拦截路径
拦截路径是指我们定义的这个拦截器, 对哪些请求生效.
我们在注册配置拦截器的时候, 通过 addPathPatterns() 方法指定要拦截哪些请求. 也可以通过 excludePathPatterns() 指定不拦截哪些请求.
比如用户登录校验, 我们希望可以对除了登录之外所有的路径生效.
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");//设置拦截器拦截的请求路径(/** 表⽰拦截所有请求)
}
}
registry.addInterceptor(loginInterceptor)
: 这行代码通过registry
对象调用addInterceptor
方法,向Spring框架注册一个拦截器,这个拦截器是loginInterceptor
。.addPathPatterns("/**")
: 这里使用addPathPatterns
方法指定了拦截器要拦截的请求路径模式。/**
表示匹配所有路径,意味着该拦截器会拦截所有进入应用的请求。.excludePathPatterns("/user/login")
: 这里使用excludePathPatterns
方法指定了拦截器不拦截的请求路径模式。/user/login
是一个具体的路径,意味着该路径下的请求不会被loginInterceptor
拦截。
在拦截器中除了可以设置 /** 拦截所有资源外,还有⼀些常见拦截路径设置:
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/user,/book,/login,不能匹配 /user/login |
/** | 任意级 | 能匹配/user,/user/login,/user/reg |
/book/* | /book下的一级路径 | 能匹配/book/addBook,不能匹配/book/addBook/1,/book |
/book/** | /book下的任意级路径 | 能匹配,/book/addBook/book/addBook/2,不能匹配/user/login |
1.2.2.2 拦截器执行流程
正常的调用顺序:
有了拦截器之后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图:
添加拦截器后, 执行 Controller 的方法之前, 请求会先被拦截器拦截住. 执行 preHandle() 方法,这个方法需要返回⼀个布尔类型的值. 如果返回 true, 就表示放行本次操作, 继续访问 controller 中的方法. 如果返回 false,则不会放行 (controller 中的方法也不会执⾏行).
controller 当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据.
1.1.3 登录校验
学习拦截器的基本操作之后,我们就可以通过拦截器来完成一个简单的图书管理系统中登录校验功能
1.1.3.1 定义拦截器
从 session 中获取用户信息, 如果 session 中不存在, 则返回 false,并设置 http 状态码为 401, 否则返回 true.
import com.example.demo.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.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.1.3.2 注册配置拦截器
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");
}
}
当然也可以这样写:
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);//设置拦截器排除拦截的路径
}
}
1.1.4 DispatcherServlet 源码分析
当 Tomcat 启动之后, 有⼀个核心的类 DispatcherServlet, 它来控制程序的执行顺序.
所有请求都会先进到 DispatcherServlet,执行 doDispatch 调度方法. 如果有拦截器, 会先执行拦截器 preHandle() 方法的代码, 如果 preHandle() 返回 true, 继续访问 controller 中的方法. controller 当中的方法执行完毕后,再回过来执行 postHandle() 和 afterCompletion() ,返回给 DispatcherServlet, 最终给浏览器响应数据.
1.1.4.1 初始化
DispatcherServlet 的初始化方法 init() 在其父类 HttpServletBean 中实现的,主要作用是加载 web.xml 中 DispatcherServlet 的配置, 并调用子类的初始化.
init() 具体代码如下:
@Override
public final void init() throws ServletException {
try {
// ServletConfigPropertyValues 是静态内部类,使用 ServletConfig 获取
// web.xml 中配置的参数
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
// 使用 BeanWrapper 来构造 DispatcherServlet
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResource(getServletContext());
bwCustomEditor(Resource.class, new ResourceEditor(resourceLoader getEnvironment()));
initBeanWrapper(b);
bw.setPropertyValues(pvs,);
} catch (BeansException ex) {}
// 让子类实现的方法,这种在父类在子类实现的方式叫做模板方法式
initServletBean();
}
在 HttpServletBean 的 init() 中调用了 initServletBean() , 它是在 FrameworkServlet 类中实现的, 主要作用是建立 WebApplicationContext 容器(有时也称上下文), 并加载 SpringMVC 配置文件中定义的 Bean到该容器中, 最后将该容器添加到 ServletContext 中.
下面是 initServletBean() 的具体代码:
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean
* properties have been set. Creates this servlet's WebApplicationContext.
*/
@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 {
// 创建ApplicationContext容
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (Exception | 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");
}
}
此处打印的日志, 也正是控制台打印出来的日志
初始化 web 容器的过程中, 会通过 onRefresh 来初始化 SpringMVC 的容器
protected WebApplicationContext initWebApplicationContext() {
//...
if (!this.refreshEventReceived) {
//初始化Spring MVC
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
return wac;
}
@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);
}
在 initStrategies() 中进行 9 大组件的初始化, 如果没有配置相应的组件,就使用默认定义的组件
方法 initMultipartResolver、initLocaleResolver、initThemeResolver、
initRequestToViewNameTranslator、initFlashMapManager 的处理方式几乎都⼀样 (1.2.3.7.8,9),从应用文中取出指定的 Bean, 如果没有, 就使用默认的.
方法 initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers 的处理方式几乎都一样 (4,5,6)
- 初始化文件上传解析器 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 中没有发现有处理器映射器,则默认 eanNameUrlHandlerMapping 作为处理器映射器
- 初始化处理器适配器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.1.4.2 处理请求
DispatcherServlet 接收到请求后, 执行 doDispatch 调度方法, 再将请求转给 Controller.
我们来看 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 {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 1. 获取执行链
// 遍历所有的 HandlerMapping 找到与请求对应的Handler
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
// 2. 获取适配器
// 遍历所有的 HandlerAdapter,找到可以处理该 Handler 的
HandlerAdapter 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;
}
}
// 3. 执行拦截器preHandle方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 4. 执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
// 5. 执行拦截器postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
// 6. 处理视图, 处理之后执行拦截器afterCompletion方法
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
// 7. 执行拦截器afterCompletion方法
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);
}
}
}
从上述源码可以看出在开始执行 Controller 之前,会先调用预处理方法 applyPreHandle,而 applyPreHandle 方法的实现源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项目中使用的拦截器 HandlerInterceptor
HandlerInterceptor interceptor = (HandlerInterceptor) this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception) null);
return false;
}
}
return true;
}
在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor , 并执行拦截器中的
preHandle 方法,这样就会咱们前面定义的拦截器对应上了,如下图所示:
如果拦截器返回 true, 整个发放就返回 true, 继续执行后续逻辑处理,如果拦截器返回 fasle, 则中断后续操作
1.1.4.3 适配器模式
适配器模式, 也叫包装器模式. 将⼀个类的接口,转换成客⼾期望的另⼀个接口, 适配器让原本接口不兼容的类可以合作无间,简单来说就是目标类不能直接使用, 通过⼀个新类进行包装⼀下, 适配调用方使用. 把两个不兼容的接口通过⼀定的方式使之兼容.
通过适配器的方式, 使之兼容
HandlerAdapter 在 Spring MVC 中使用了适配器模式,他有多种适配器模式角色
- Target: 目标接口 (可以是抽象类或接口), 客户希望直接用的接口
- Adaptee: 适配者, 但是与 Target 不兼容
- Adapter: 适配器类, 此模式的核心. 通过继承或者引用适配者的对象, 把适配者转为目标接口
- client: 需要使用适配器的对象
1.1.4.3.1 适配器模式的实现
前⾯学习的 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打印⽇志");
}
}
可以看出, 我们不需要改变 log4 的 api,只需要通过适配器转换下, 就可以更换日志框架, 保障系统的平稳运行