上篇文章:
SpringBoot系列—MyBatis-plushttps://blog.csdn.net/sniper_fandc/article/details/148979284?fromshare=blogdetail&sharetype=blogdetail&sharerId=148979284&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link
目录
1 拦截器的作用
2 拦截器的基本使用
2.1 定义拦截器
2.2 注册配置拦截器
2.3 观察方法执行顺序
3 登录拦截器
4 源码分析
4.1 init初始化
4.2 service运行
统一功能处理就是把代码中需要重复使用的功能放到一起,从而实现代码复用,减少代码量。主要有拦截器、统一数据返回格式、统一异常处理三个方面,这篇文章先来讲讲拦截器:
1 拦截器的作用
比如用户访问网站的各种功能,我们都需要判断其登录状态,如果用户未登录,则希望用户跳转到登录页面进行登录。如果在每一个功能执行前都写上登录判断逻辑,代码量就会巨大,并且冗余。
这个时候就可以把登录判断逻辑放到一个地方,每次执行其他功能的方法前先执行登录判断逻辑,这种统一功能处理的方式就是拦截器。
拦截器会在请求被处理前(Controller层执行前)先拦截请求,进行一些处理后,再执行Controller层的代码,如果有需求,也可以在Controller层代码执行后再做一些处理。
2 拦截器的基本使用
拦截器的使用有两个步骤:1.定义拦截器。2.注册配置拦截器。
2.1 定义拦截器
@Slf4j@Componentpublic class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponseresponse, Object handler) throws Exception {log.info("LoginInterceptor 目标方法执行前执行..");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponseresponse, Object handler, ModelAndView modelAndView) throws Exception {log.info("LoginInterceptor 目标方法执行后执行");}@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");}}
实现HandlerInterceptor接口,并重写其中的方法。
preHandle()方法:目标方法执行前执行。返回true: 继续执行后续操作;返回false: 中断后续操作。
postHandle()方法:目标方法执行后执行
afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图,可以不了解)。
2.2 注册配置拦截器
@Configurationpublic class WebConfig implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径(/**表示拦截所有请求)}}
注册配置拦截器通常放在Configuration包下,需要实现WebMvcConfigurer接口,并重写addInterceptors方法。重写该方法需要拦截器对象LoginInterceptor,可以使用@Autowired注入或直接new一个对象。
还需要使用addPathPatterns方法配置拦截器的工作路径,也可以使用excludePathPatterns
("/user/login")排除一些路径。常用路径如下:
路径 | 含义 |
/* | 匹配所有的一级路径,比如/user、/login,不能匹配/user/login |
/** | 匹配任意级路径 |
/xxx/* | 匹配xxx路径下的一级路径,比如/user/login、/user/reg,不能匹配/user或更多级路径 |
/xxx/** | 匹配所有以xxx路径为前缀的路径 |
/xxx | 匹配路径/xxx |
/xxx/xxx | 匹配路径/xxx/xxx |
/**/*.html、/**/*.css、/**/*.js、/**/*.png等等 | 匹配所有的静态资源,一般需要排除这些路径,否则html也会拦截就看不到页面了 |
2.3 观察方法执行顺序
当请求的地址是/user/login时,方法执行顺序:preHandle()=>login()=>postHandle()=>
afterCompletion()。
3 登录拦截器
前端登录代码:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登陆页面</title><script src="js/jquery.min.js"></script><style>.login-container {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;}.login-dialog {width: 400px;height: 400px;background-color: rgba(83, 48, 142, 0.6);border-radius: 10px;}.login-dialog h3 {padding: 50px 0;text-align: center;}.login-dialog .row {height: 50px;display: flex;justify-content: center;align-items: center;}.login-dialog .row span {display: block;width: 100px;font-weight: 700;}.login-dialog .row input {width: 200px;height: 40px;line-height: 40px;font-size: 24px;border-radius: 10px;border: none;outline: none;text-indent: 10px;}.login-dialog #submit {width: 300px;height: 50px;color: white;background-color: rgba(164, 228, 17, 0.6);border: none;border-radius: 10px;}.login-dialog #submit:active {background-color: #666;}</style></head><body><div class="login-container"><div class="login-dialog"><form action="login" method="post"><h3>登录</h3><div class="row"><span>用户名</span><input type="text" id="username" name="username"></div><div class="row"><span>密码</span><input type="password" id="password" name="password"></div><div class="row"><input type="button" value="提交" id="submit"></div></form></div></div><script>$("#submit").click(function () {$.ajax({type: "get",url: "/user/login",data: {username: $("#username").val(),password: $("#password").val()},success:function(result){if(result){location.href = "success.html";}else{alert("账号或密码错误");}}});});</script></body></html>
登录成功界面:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登录成功界面</title><script src="js/jquery.min.js"></script></head><body><div class="container"><div>登录成功,欢迎用户:</div></div><script>$.ajax({type: "get",url: "/user/isLogin",success:function(result){let div = document.createElement('div');div.className = 'new-div';div.innerHTML = result;div.style.fontSize = '100px';let parent = document.querySelector('.container');parent.appendChild(div);},error:function(result){if(result != null && result.status == 401){alert("当前用户未登录,请重新登录");location.href = "login.html";}}});</script></body></html>
后端登录代码:
@Slf4j@RequestMapping("/user")@RestControllerpublic class LoginController {@RequestMapping("/login")public boolean login(String username, String password, HttpSession session){//账号或密码为空if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){return false;}//模拟验证数据, 账号密码正确if("admin".equals(username) && "123456".equals(password)){session.setAttribute("userName",username);return true;}//账号密码错误return false;}@RequestMapping("/isLogin")public String isLogin(HttpSession session){//尝试从Session中获取用户名return (String) session.getAttribute("userName");}}
登录拦截器:
@Slf4j@Componentpublic class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponseresponse, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userName") != null) {return true;}response.setStatus(401);return false;}}
注册配置登录拦截器:
@Configurationpublic class WebConfig implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径(/**表示拦截所有请求).excludePathPatterns("/user/login")//登录接口不能拦截.excludePathPatterns("/**/*.js") //排除前端静态资源.excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.png").excludePathPatterns("/**/*.html");}}
注意:排除接口的写法还可以:
private List<String> excludePaths = Arrays.asList("/user/login","/**/*.js","/**/*.css","/**/*.png","/**/*.html");
把excludePaths作为参数传入excludePathPatterns方法中。excludePathPatterns()接收两种参数:1.String...(理解为String[],可以同时传多个参数)2.List<String>。
所有未登录的用户尝试访问登录后的接口,都会被拦截器拦截,判断未登录就返回401,让前端重定向到登录界面。
4 源码分析
4.1 init初始化
当我们访问被拦截器拦截的接口时,会发现在preHandle()执行前控制台打印了两行初始化的日志,初始化了dispatcherServlet。这是Servlet调度器,负责控制方法的执行流程。
Servlet的生命周期是:init=>service=>destroy,在init阶段,Spring对Servlet的Bean初始化所做的事涉及到三个类:DispatcherServlet、FrameworkServlet和HttpServletBean。DispatcherServlet继承FrameworkServlet,FrameworkServlet继承HttpServletBean,HttpServletBean继承HttpServlet(属于Tomcat包的内容了)。
在HttpServletBean类中,初始化Servlet时首先会调用init()方法,该方法首先根据读取Servlet的配置信息,如果配置不为空就加载配置,否则就调用HttpServletBean实例的initServletBean()方法。该方法在本类中是空方法,具体实现(重写)在FrameworkServlet类中:
public final void init() throws ServletException {PropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);if (!pvs.isEmpty()) {try {BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));this.initBeanWrapper(bw);bw.setPropertyValues(pvs, true);} catch (BeansException var4) {if (this.logger.isErrorEnabled()) {this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);}throw var4;}}this.initServletBean();}
在FrameworkServlet类中,重写了父类的initServletBean()方法,该方法主要做的事是日志打印和初始化Spring Web的上下文(可以理解为loC容器),即initWebApplicationContext()所做的事。在initWebApplicationContext()中,通过onRefresh()方法来初始化Spring Web的上下文,但是在FrameworkServlet类中的onRefresh()也是空方法,由子类DispatcherServlet实现:
protected final void initServletBean() throws ServletException {this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");if (this.logger.isInfoEnabled()) {this.logger.info("Initializing Servlet '" + this.getServletName() + "'");}long startTime = System.currentTimeMillis();try {this.webApplicationContext = this.initWebApplicationContext();this.initFrameworkServlet();} catch (RuntimeException | ServletException var4) {this.logger.error("Context initialization failed", var4);throw var4;}if (this.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";this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);}if (this.logger.isInfoEnabled()) {this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");}}protected WebApplicationContext initWebApplicationContext() {WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());WebApplicationContext wac = null;if (this.webApplicationContext != null) {wac = this.webApplicationContext;if (wac instanceof ConfigurableWebApplicationContext) {ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;if (!cwac.isActive()) {if (cwac.getParent() == null) {cwac.setParent(rootContext);}this.configureAndRefreshWebApplicationContext(cwac);}}}if (wac == null) {wac = this.findWebApplicationContext();}if (wac == null) {wac = this.createWebApplicationContext(rootContext);}if (!this.refreshEventReceived) {synchronized(this.onRefreshMonitor) {this.onRefresh(wac);}}if (this.publishContext) {String attrName = this.getServletContextAttributeName();this.getServletContext().setAttribute(attrName, wac);}return wac;}
在DispatcherServlet类中,重写了父类的onRefresh()方法,该方法主要做的事是调用initStrategies()方法。在initStrategies()方法中,完成了9大组件的初始化,9大组件是Spring可以运行的核心方法:
protected void onRefresh(ApplicationContext context) {this.initStrategies(context);}protected void initStrategies(ApplicationContext context) {this.initMultipartResolver(context);this.initLocaleResolver(context);this.initThemeResolver(context);this.initHandlerMappings(context);this.initHandlerAdapters(context);this.initHandlerExceptionResolvers(context);this.initRequestToViewNameTranslator(context);this.initViewResolvers(context);this.initFlashMapManager(context);}
在DispatcherServlet.properties中有配置默认的策略,如果9大组件的初始化过程中有配置相应的组件,就使用配置的组件;如果没有,就使用默认的:
(1)initMultipartResolver()初始化文件上传解析器MultipartResolver:从应用上下文中获取名称为multipartResolver的Bean,如果没有名为multipartResolver的Bean,则没有提供上传文件的解析器
(2)initLocaleResolver()初始化区域解析器LocaleResolver:从应用上下文中获取名称为localeResolver的Bean,如果没有这个Bean,则默认使用AcceptHeaderLocaleResolver作为区域解析器。
(3)initThemeResolver()初始化主题解析器ThemeResolver:从应用上下文中获取名称为themeResolver的Bean,如果没有这个Bean,则默认使用FixedThemeResolver作为主题解析器。
(4)initHandlerMappings()初始化处理器映射器HandlerMappings:处理器映射器作用,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx方法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进行排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器。这里的处理器就包括拦截器的处理器,Handler会负责拦截器方法的执行流程。
(5)initHandlerAdapters()初始化处理器适配器HandlerAdapter:作用是通过调用具体的方法(业务逻辑)来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进行排序;如果在ApplicationContext中没有发现处理器适配器,则默认SimpleControllerHandlerAdapter作为处理器适配器。
HandlerAdapter用到适配器模式,适配器模式简而言之就是通过适配器连接双端,从而解决双端接口不兼容问题,比如日常生活中的接口转化器。如果一个接口传输的参数是一种格式,而另一个接口传输的参数是不同的格式,两个接口无法直接调用,因此就需要适配器作为中间件,在适配器内部把两个接口的参数统一,从而实现调用。在slf4j中除了用到装饰模式(门面模式),也用到的适配器模式,slf4j作为适配器,调用的logback或log4j的api。具体设计模式见:
适配器模式https://blog.csdn.net/sniper_fandc/article/details/143468002?fromshare=blogdetail&sharetype=blogdetail&sharerId=143468002&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link
(6)initHandlerExceptionResolvers()初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进行排序;如果在ApplicationContext中没有发现异常处理器解析器,则不设置异常处理器。
(7)initRequestToViewNameTranslator()初始化RequestToViewNameTranslator:其作用是从Request中获取viewName,从ApplicationContext发现有viewNameTranslator的Bean,如果没有,则默认使用DefaultRequestToViewNameTranslator。
(8)initViewResolvers()初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolver的Bean,如果没有,则默认InternalResourceViewResolver作为视图解析器。
(9)initFlashMapManager()初始化FlashMapManager:其作用是用于检索和保存FlashMap(保存从一个URL重定向到另一个URL时的参数信息),从ApplicationContext发现有flashMapManager的Bean,如果没有,则默认使用DefaultFlashMapManager。
上述大致流程即为Spring对Servlet的初始化流程,其中除了适配器模式,还应用了模板方法模式:父类的方法延迟到子类中去实现。HttpServletBean的initServletBean()方法由在FrameworkServlet类实现;在FrameworkServlet类中的onRefresh()由子类DispatcherServlet实现。具体模式思想见:
模板方法模式
4.2 service运行
在这一阶段,Servlet主要负责运行处理请求和响应,也就是执行业务逻辑。具体在DispatcherServlet类doService()方法中:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {this.logRequest(request);Map<String, Object> attributesSnapshot = null;if (WebUtils.isIncludeRequest(request)) {attributesSnapshot = new HashMap();Enumeration<?> attrNames = request.getAttributeNames();label116:while(true) {String attrName;do {if (!attrNames.hasMoreElements()) {break label116;}attrName = (String)attrNames.nextElement();} while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));attributesSnapshot.put(attrName, request.getAttribute(attrName));}}request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());if (this.flashMapManager != null) {FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);if (inputFlashMap != null) {request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));}request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);}RequestPath previousRequestPath = null;if (this.parseRequestPath) {previousRequestPath = (RequestPath)request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);ServletRequestPathUtils.parseAndCache(request);}try {this.doDispatch(request, response);} finally {if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {this.restoreAttributesAfterInclude(request, attributesSnapshot);}if (this.parseRequestPath) {ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);}}}
该方法中主要调用了doDispatch()方法(DispatcherServlet类),该方法就是负责当来了一个请求后方法的调用流程:
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;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);}}}
该方法内重要的方法执行流程如下图所示:
下篇文章:
SpringBoot系列—统一功能处理(统一数据返回格式)https://blog.csdn.net/sniper_fandc/article/details/148998227?fromshare=blogdetail&sharetype=blogdetail&sharerId=148998227&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link