目录
1.拦截器概述
2.拦截器的基本使用
3.拦截路径配置
4.拦截器执行流程
5.实现登录校验
5.1定义拦截器
5.2注册配置拦截器
6.DispatcherServlet源码分析
6.1初始化
6.2处理请求(核心)
7.适配器模式
1.拦截器概述
拦截器(Interceptor)是一种软件设计模式,用于在程序执行过程中拦截或截取特定的操作或事件。它可以在操作发生之前、之后或替代操作本身进行自定义的处理。
之前完成强制登录的功能是根据Session来判断用户是否登录, 但是实现方法是比较麻烦的
• 需要修改每个接口的处理逻辑
• 需要修改每个接口的返回结果
• 接口定义修改, 前端代码也需要跟着修改
有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 学习⼀种新的解决办法: 拦截器
2.拦截器的基本使用
拦截器的使用步骤分为两步:
1. 定义拦截器
2. 注册配置拦截器
自定义拦截器:实现HandlerInterceptor接口 ,并重写其所有方法
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse res
log.info("LoginInterceptor ⽬标⽅法执⾏前执⾏ ..");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse respo
log.info("LoginInterceptor ⽬标⽅法执⾏后执⾏");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
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, 再观察运行结果
可以看到, 拦截器拦截了请求, 没有进行响应.
3.拦截路径配置
拦截路径是指我们定义的这个拦截器, 对哪些请求生效
我们在注册配置拦截器的时候, 通过 addPathPatterns()方法指定要拦截哪些请求
也可以通过excludePathPatterns()指定不拦截哪些请求
比如用户登录校验, 我们希望可以对除了登录之外所有的路径生效.
@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 等文件).
4.拦截器执行流程
正常的调用顺序:
有了拦截器之后 ,会在调用Controller 之前进行相应的业务处理 ,执行的流程如下图
添加拦截器后, 执行Controller的⽅法之前, 请求会先被拦截器拦截住. 执行preHandle()方法,这个方法需要返回⼀个布尔类型的值. 如果返回true, 就表示放行本次操作, 继续访问controller中的方法. 如果返回false ,则不会放行(controller中的方法也不会执行).
controller当中的方法执行完毕后 ,再回来执行postHandle()方法以及afterCompletion()方法
5.实现登录校验
学习拦截器的基本操作之后 ,接下来我们完成通过拦截器来完成登录校验功能
5.1定义拦截器
从session中获取用户信息, 如果session中不存在, 则返回false,设置http状态码为401, 否则返回true
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("登录拦截器校验...");
HttpSession session = request.getSession();
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
if (userInfo!=null && userInfo.getId()>=0){
return true;
}
response.setStatus(401);//401 表示未认证登录
return false;
}
}
5.2注册配置拦截器
@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");
}
}
也可以改成
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
private static List<String> excludePath = Arrays.asList(
"/user/login",
"/css/**",
"/js/**",
"/pic/**",
"/**/*.html",
"/test/**"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")// /**表示给所有方法添加拦截器
.excludePathPatterns(excludePath);
}
}
运行程序, 通过Postman进行测试:
不登录直接进入图书列表页面
http://127.0.0.1:8080/book/getListByPage
观察返回结果: http状态码401 也可以通过Fiddler抓包观察
进行登录
http://127.0.0.1:8080/user/login?name=admin&password=admin
再次查看图书列表
数据进行了返回
6.DispatcherServlet源码分析
观察我们的服务启动日志:
当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.
所有请求都会先进到DispatcherServlet,执⾏doDispatch调度方法. 如果有拦截器, 会先执行拦截器的preHandle()方法的代码,如果preHandle()返回true,则继续访问Controller中的方法,Controller当中的方法执行完毕后,再回过来执行postHandle()和aferCompletion()返回给DispatcherServlet,最终给浏览器返回数据
6.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
init() 具体代码如下:
@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");
}
}
此处打印的日志, 也正是控制台打印出来的日志
初始化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 ob
*/
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大组件的初始化, 如果没有配置相应的组件 ,就使用默认定义的组件(在 DispatcherServlet.properties中有配置默认的策略, 大致了解即可)
1. 初始化⽂件上传解析器MultipartResolver:从应用上下⽂中获取名称为multipartResolver的Bean ,如果没有名为multipartResolver的Bean ,则没有提供上传⽂件的解析器
2. 初始化区域解析器LocaleResolver:从应⽤上下⽂中获取名称为localeResolver的Bean ,如果没有这个Bean ,则默认使⽤AcceptHeaderLocaleResolver作为区域解析器
3. 初始化主题解析器ThemeResolver:从应⽤上下⽂中获取名称为themeResolver的Bean ,如果没有这个Bean ,则默认使⽤FixedThemeResolver作为主题解析器
4. 初始化处理器映射器HandlerMappings:处理器映射器作⽤ ,1) 通过处理器映射器找到对应的 处理器适配器 ,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx ⽅法) ;如果在ApplicationContext发现有HandlerMappings ,则从ApplicationContext中获取 到所有的HandlerMappings ,并进⾏排序;如果在ApplicationContext中没有发现有处理器映射器 ,则默认BeanNameUrlHandlerMapping作为处理器映射器
5. 初始化处理器适配器HandlerAdapter:作用是通过调用具体的⽅法来处理具体的请求;如果在 ApplicationContext发现有handlerAdapter ,则从ApplicationContext中获取到所有的
HandlerAdapter ,并进⾏排序;如果在ApplicationContext中没有发现处理器适配器 ,则不设置 异常处理器 ,则默认SimpleControllerHandlerAdapter作为处理器适配器
6. 初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有
handlerExceptionResolver ,则从ApplicationContext中获取到所有的
HandlerExceptionResolver ,并进⾏排序;如果在ApplicationContext中没有发现异常处理器解析器 ,则不设置异常处理器
7. 初始化RequestToViewNameTranslator:其作⽤是从Request中获取viewName ,从 ApplicationContext发现有viewNameTranslator的Bean ,如果没有 ,则默认使⽤
DefaultRequestToViewNameTranslator
8. 初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolver的Bean, 如果没有 ,则默认InternalResourceViewResolver作为视图解析器
9. 初始化FlashMapManager:其作⽤是⽤于检索和保存FlashMap(保存从⼀个URL重定向到另⼀个URL时的参数信息),从ApplicationContext发现有flashMapManager的Bean ,如果没有 ,则默认使用DefaultFlashMapManager
6.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 {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
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;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
HandlerAdapter 在 Spring MVC 中使用了适配器模式, 下⾯详细再介绍
适配器模式, 也叫包装器模式. 简单来说就是⽬标类不能直接使⽤ , 通过⼀个新类进⾏包装⼀下, 适配 调用方使⽤ .
把两个不兼容的接口通过⼀定的方式使之兼容.
HandlerAdapter 主要⽤于⽀持不同类型的处理器(如 Controller、 HttpRequestHandler 或者
Servlet 等),让它们能够适配统⼀的请求处理流程。这样 ,Spring MVC 可以通过⼀个统⼀的接口来处理来自各种处理器的请求.
从上述源码可以看出在开始执⾏ Controller 之前 ,会先调用预处理⽅法applyPreHandle ,而applyPreHandle 方法的实现源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
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方法,这就会和上面我们定义的拦截器对应上了,如下图所示:
如果拦截器返回true, 整个方法就返回true, 继续执行后续逻辑处理 如果拦截器返回fasle, 则中断后续操作
7.适配器模式
HandlerAdapter 在 Spring MVC 中使⽤了适配器模式 适配器模式定义
适配器模式, 也叫包装器模式. 将⼀个类的接口,转换成客户期望的另⼀个接口 , 适配器让原本接口不兼 容的类可以合作无间.
简单来说就是目标类不能直接使⽤ , 通过⼀个新类进行包装⼀下, 适配调用方使用 . 把两个不兼容的接口通过⼀定的方式使之兼容.
⽐如下面两个接口 , 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)
可以通过适配器的方式, 使之兼容
适配器模式角色
• 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,只需要通过适配器转换下, 就可以更换⽇志框架, 保障系统的平稳 运⾏
适配器模式的实现并不在slf4j-core中(只定义了Logger), 具体实现是在针对log4j的桥接器项⽬slf4j- log4j12中
设计模式的使⽤⾮常灵活, ⼀个项⽬中通常会含有多种设计模式.
适配器模式应用场景
⼀般来说 ,适配器模式可以看作⼀种"补偿模式" ,⽤来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之举", 如果在设计初期 ,我们就能协调规避接口不兼容的问题, 就不需要使用适配器模式了
所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进行改造, 并且希望可以复⽤原有代码实现新的功能. 比如版本升级等.