上篇文章:
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