從做系統(tǒng)怎么找一起的收藏網(wǎng)站推廣技術(shù)
目錄
- SpringBoot 統(tǒng)?功能處理
- 攔截器
- 攔截器快速??
- 攔截器詳解
- 攔截路徑
- 攔截器執(zhí)?流程
- 登錄校驗
- 定義攔截器
- 注冊配置攔截器
- DispatcherServlet 源碼分析(了解)
- 初始化(了解)
- `DispatcherServlet`的初始化
- 1. `HttpServletBean.init()`
- 2. `FrameworkServlet.initServletBean()`
- `WebApplicationContext`的建立和配置
- 1. `FrameworkServlet.initWebApplicationContext()`
- 初始化Spring MVC的9大組件
- 1. `FrameworkServlet.onRefresh()`
- 2. `DispatcherServlet.initStrategies()`
- 總結(jié)
- 處理請求(核?)
- `DispatcherServlet.doDispatch()` 方法的流程
- 攔截器(HandlerInterceptor)的作用
- 總結(jié)
- 適配器模式
- 統(tǒng)?數(shù)據(jù)返回格式
- 快速??
- 存在問題
- 案例代碼修改
- 優(yōu)點
- 統(tǒng)?異常處理
- @ControllerAdvice 源碼分析
- 案例代碼
- 登錄??
- 圖書列表
- 其他
- 總結(jié)
SpringBoot 統(tǒng)?功能處理
- 掌握攔截器的使?, 及其原理
- 學(xué)習(xí)統(tǒng)?數(shù)據(jù)返回格式和統(tǒng)?異常處理的操作
- 了解?些Spring的源碼
攔截器
之前我們完成了強制登錄的功能, 后端程序根據(jù)Session來判斷??是否登錄, 但是實現(xiàn)?法是?較?煩的
- 需要修改每個接?的處理邏輯
- 需要修改每個接?的返回結(jié)果
- 接?定義修改, 前端代碼也需要跟著修改
有沒有更簡單的辦法, 統(tǒng)?攔截所有的請求, 并進(jìn)?Session校驗?zāi)? 這??種新的解決辦法: 攔截器
攔截器快速??
什么是攔截器?
攔截器是Spring框架提供的核?功能之?, 主要?來攔截??的請求, 在指定?法前后, 根據(jù)業(yè)務(wù)需要執(zhí)?預(yù)先設(shè)定的代碼.
攔截器的作用維度:URL
也就是說, 允許開發(fā)?員提前預(yù)定義?些邏輯, 在??的請求響應(yīng)前后執(zhí)?. 也可以在??請求前阻?其執(zhí)?.
在攔截器當(dāng)中,開發(fā)?員可以在應(yīng)?程序中做?些通?性的操作, ?如通過攔截器來攔截前端發(fā)來的請求, 判斷Session中是否有登錄??的信息. 如果有就可以放?, 如果沒有就進(jìn)?攔截.
?如我們?nèi)ャy?辦理業(yè)務(wù), 在辦理業(yè)務(wù)前后, 就可以加?些攔截操作
辦理業(yè)務(wù)之前, 先取號, 如果帶?份證了就取號成功
業(yè)務(wù)辦理結(jié)束, 給業(yè)務(wù)辦理?員的服務(wù)進(jìn)?評價.
這些就是"攔截器"做的?作
攔截器的基本使?:
攔截器的使?步驟分為兩步:
- 定義攔截器
- 注冊配置攔截器
?定義攔截器:實現(xiàn)HandlerInterceptor接?,并重寫其所有?法
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {return HandlerInterceptor.super.preHandle(request, response, handler);}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}
}
- **preHandle()**?法:?標(biāo)?法執(zhí)?前執(zhí)?. 返回true: 繼續(xù)執(zhí)?后續(xù)操作; 返回false: 中斷后續(xù)操作.
- **postHandle()**?法:?標(biāo)?法執(zhí)?后執(zhí)?
- afterCompletion()?法:視圖渲染完畢后執(zhí)?,最后執(zhí)?(后端開發(fā)現(xiàn)在?乎不涉及視圖, 暫不了解)
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("目標(biāo)方法執(zhí)行前");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("目標(biāo)方法執(zhí)行后");}
}
注冊配置攔截器:實現(xiàn)WebMvcConfigurer接?,并重寫addInterceptors?法
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**");// /**表示給所有方法添加攔截器}
}
啟動服務(wù), 試試訪問任意請求, 觀察后端?志
可以看到 preHandle ?法執(zhí)?之后就放?了, 開始執(zhí)??標(biāo)?法, ?標(biāo)?法執(zhí)?完成之后執(zhí)?postHandle和afterCompletion?法.
我們把攔截器中preHandle?法的返回值改為false, 再觀察運?結(jié)果
可以看到, 攔截器攔截了請求, 沒有進(jìn)?響應(yīng)
攔截器詳解
攔截器的??程序完成之后,接下來我們來介紹攔截器的使?細(xì)節(jié)。攔截器的使?細(xì)節(jié)我們主要介紹兩個部分:
- 攔截器的攔截路徑配置
- 攔截器實現(xiàn)原理
攔截路徑
攔截路徑是指我們定義的這個攔截器, 對哪些請求?效.
我們在注冊配置攔截器的時候, 通過 addPathPatterns()
?法指定要攔截哪些請求. 也可以通過excludePathPatterns()
指定不攔截哪些請求.
上述代碼中, 我們配置的是
/**
, 表?攔截所有的請求.
?如??登錄校驗, 我們希望可以對除了登錄之外所有的路徑?效.
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**");// /**表示給所有方法添加攔截器.excludePathPatterns("/user/login");//設(shè)置攔截器攔截的請求路徑}
}
在攔截器中除了可以設(shè)置 /**
攔截所有資源外,還有?些常?攔截路徑設(shè)置:
攔截路徑 | 含義 | 舉例 |
---|---|---|
/* | ?級路徑 | 能匹配/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 |
以上攔截規(guī)則可以攔截此項?中的使? URL,包括靜態(tài)?件(圖??件, JS 和 CSS 等?件).
攔截器執(zhí)?流程
正常的調(diào)?順序:
有了攔截器之后,會在調(diào)? Controller 之前進(jìn)?相應(yīng)的業(yè)務(wù)處理,執(zhí)?的流程如下圖
-
添加攔截器后, 執(zhí)?Controller的?法之前, 請求會先被攔截器攔截住. 執(zhí)?
preHandle()
?法,這個?法需要返回?個布爾類型的值. 如果返回true, 就表?放?本次操作, 繼續(xù)訪問controller中的?法. 如果返回false,則不會放?(controller中的?法也不會執(zhí)?). -
controller當(dāng)中的?法執(zhí)?完畢后,再回過來執(zhí)?
postHandle()
這個?法以及afterCompletion()
?法,執(zhí)?完畢之后,最終給瀏覽器響應(yīng)數(shù)據(jù).
登錄校驗
學(xué)習(xí)攔截器的基本操作之后,接下來我們需要完成最后?步操作:通過攔截器來完成圖書管理系統(tǒng)中的登錄校驗功能
定義攔截器
從session中獲取??信息, 如果session中不存在, 則返回false,并設(shè)置http狀態(tài)碼為401, 否則返回true.
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("登錄攔截器校驗...");//返回true表示放行,返回false表示攔截//檢驗用戶是否登錄HttpSession session = request.getSession(true);//true表示沒有session就創(chuàng)建一個,false表示沒有就直接返回UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);if (userInfo != null && userInfo.getId() >= 0) {return true;//放行}response.setStatus(401);//401表示未認(rèn)證登錄return false;//攔截}
}
http狀態(tài)碼401: Unauthorized
Indicates that authentication is required and was either not provided or has failed. If the request already included authorization credentials, then the 401 status code indicates that those credentials were not accepted.
中?解釋: 未經(jīng)過認(rèn)證. 指??份驗證是必需的, 沒有提供?份驗證或?份驗證失敗. 如果請求已經(jīng)包含授權(quán)憑據(jù),那么401狀態(tài)碼表?不接受這些憑據(jù)。
注冊配置攔截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;//包含一些不應(yīng)該被攔截的的URL路徑private static List<String> excludePath = Arrays.asList("/user/login",//排除這個特定的路徑//因為我們寫的不是完全的前后端分離//下面是為了攔截前端部分的靜態(tài)資源"/css/**","/js/**","/pic/**","/**/*.html");@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor)//添加了攔截器.addPathPatterns("/**")// /**表示給所有方法添加攔截器,即匹配所有路徑.excludePathPatterns(excludePath);}
}
刪除之前的登錄校驗代碼
@RequestMapping("/getBookListByPage")//為了方便更好拓展,最好返回結(jié)果也是一個對象public Result getBookListByPage(PageRequest pageRequest, HttpSession session) {log.info("查詢翻頁信息,pageRequest:{}", pageRequest);用戶登錄校驗//UserInfo userInfo= (UserInfo) session.getAttribute("session_user_key");//if(userInfo==null||userInfo.getId()<=0||"".equals(userInfo.getUserName())){// //用戶未登錄// return Result.unLogin();//}//校驗成功if (pageRequest.getPageSize() < 0 || pageRequest.getCurrentPage() < 1) {//每頁顯示條數(shù)為負(fù)或者當(dāng)前頁數(shù)不為正數(shù)則錯誤return Result.fail("參數(shù)校驗失敗");}PageResult<BookInfo> bookInfoPageResult = null;try {bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest);return Result.success(bookInfoPageResult);} catch (Exception e) {log.error("查詢翻頁信息錯誤,e:{}", e);return Result.fail(e.getMessage());}}
運?程序, 通過Postman進(jìn)?測試:
- 查看圖書列表
http://127.0.0.1:8080/book/getBookListByPage
觀察返回結(jié)果: http狀態(tài)碼401
也可以通過Fiddler抓包觀察
- 登錄
http://127.0.0.1:8080/user/login?name=admin&password=admin
- 再次查看圖書列表
數(shù)據(jù)進(jìn)?了返回
DispatcherServlet 源碼分析(了解)
觀察我們的服務(wù)啟動?志:
當(dāng)Spiring的Tomcat啟動之后, 有?個核?的類 DispatcherServlet, 它來控制程序的執(zhí)?順序.
dispatcher:調(diào)度程序
servlet的生命周期
init
service
destroy
所有請求都會先進(jìn)到 DispatcherServlet,執(zhí)? doDispatch 調(diào)度?法. 如果有攔截器, 會先執(zhí)?攔截器preHandle()
?法的代碼, 如果 preHandle()
返回true, 繼續(xù)訪問 controller 中的?法. controller 當(dāng)中的?法執(zhí)?完畢后,再回過來執(zhí)? postHandle()
和 afterCompletion()
,返回給 DispatcherServlet,最終給瀏覽器響應(yīng)數(shù)據(jù).
初始化(了解)
DispatcherServlet 的初始化?法 init() 在其?類 HttpServletBean 中實現(xiàn)的.
主要作?是加載 web.xml 中 DispatcherServlet 的 配置, 并調(diào)??類的初始化.
web.xml是web項?的配置?件,?般的web?程都會?到web.xml來配置,主要?來配置Listener,Filter,Servlet等, Spring框架從3.1版本開始?持Servlet3.0, 并且從3.2版本開始通過配置DispatcherServlet, 實現(xiàn)不再使?web.xml
init() 具體代碼如下:
public final void init() throws ServletException {// ServletConfigPropertyValues 是靜態(tài)內(nèi)部類,使? ServletConfig 獲取 web.xmlPropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);if (!pvs.isEmpty()) {try {// 使? BeanWrapper 來構(gòu)造 DispatcherServletBeanWrapper 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;}}// 讓?類實現(xiàn)的?法,這種在?類定義在?類實現(xiàn)的?式叫做模版?法模式this.initServletBean();}
在 HttpServletBean
的 init()
中調(diào)?了 initServletBean()
, 它是在FrameworkServlet 類中實現(xiàn)的, 主要作?是建? WebApplicationContext 容器(有時也稱上下?), 并加載 SpringMVC 配置?件中定義的 Bean到該容器中, 最后將該容器添加到 ServletContext 中. 下?是 initServletBean() 的具體代碼:
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 {//創(chuàng)建ApplicationContext容器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");}}
此處打印的?志, 也正是控制臺打印出來的?志
源碼跟蹤技巧:
在閱讀框架源碼的時候, ?定要抓住關(guān)鍵點, 找到核?流程.
切忌從頭到尾????代碼去看, ?個?法的去研究, ?定要找到關(guān)鍵流程, 抓住關(guān)鍵點, 先在宏觀上對整個流程或者整個原理有?個認(rèn)識, 有精?再去研究其中的細(xì)節(jié).
初始化web容器的過程中, 會通過 onRefresh 來初始化SpringMVC的容器
protected WebApplicationContext initWebApplicationContext() {//...if (!this.refreshEventReceived) {//初始化Spring MVCsynchronized(this.onRefreshMonitor) {this.onRefresh(wac);}}//...return wac;}
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);}
在initStrategies()中進(jìn)?9?組件的初始化, 如果沒有配置相應(yīng)的組件,就使?默認(rèn)定義的組件(在DispatcherServlet.properties中有配置默認(rèn)的策略, ?致了解即可)
?法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的處理?式?乎都?樣(1.2.3.7.8,9),從應(yīng)??中取出指定的Bean, 如果沒有, 就使?默認(rèn)的.
?法initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers的處理?式?乎都?樣(4,5,6,這三個重要一點)
初始化?件上傳解析器MultipartResolver:從應(yīng)?上下?中獲取名稱為multipartResolver的Bean,如果沒有名為multipartResolver的Bean,則沒有提供上傳?件的解析器
初始化區(qū)域解析器LocaleResolver:從應(yīng)?上下?中獲取名稱為localeResolver的Bean,如果沒有這個Bean,則默認(rèn)使?AcceptHeaderLocaleResolver作為區(qū)域解析器
初始化主題解析器ThemeResolver:從應(yīng)?上下?中獲取名稱為themeResolver的Bean,如果沒有這個Bean,則默認(rèn)使?FixedThemeResolver作為主題解析器
初始化處理器映射器HandlerMappings:處理器映射器作?,1)通過處理器映射器找到對應(yīng)的處理器適配器,將請求交給適配器處理;2)緩存每個請求地址URL對應(yīng)的位置(Controller.xxx?法);如果在ApplicationContext發(fā)現(xiàn)有HandlerMappings,則從ApplicationContext中獲取到所有的HandlerMappings,并進(jìn)?排序;如果在ApplicationContext中沒有發(fā)現(xiàn)有處理器映射器,則默認(rèn)BeanNameUrlHandlerMapping作為處理器映射器
初始化處理器適配器HandlerAdapter:作?是通過調(diào)?具體的?法來處理具體的請求;如果在ApplicationContext發(fā)現(xiàn)有handlerAdapter,則從ApplicationContext中獲取到所有的HandlerAdapter,并進(jìn)?排序;如果在ApplicationContext中沒有發(fā)現(xiàn)處理器適配器,則不設(shè)置異常處理器,則默認(rèn)SimpleControllerHandlerAdapter作為處理器適配器
初始化異常處理器解析器HandlerExceptionResolver:如果在ApplicationContext發(fā)現(xiàn)有handlerExceptionResolver,則從ApplicationContext中獲取到所有的HandlerExceptionResolver,并進(jìn)?排序;如果在ApplicationContext中沒有發(fā)現(xiàn)異常處理器解析器,則不設(shè)置異常處理器
初始化RequestToViewNameTranslator:其作?是從Request中獲取viewName,從ApplicationContext發(fā)現(xiàn)有viewNameTranslator的Bean,如果沒有,則默認(rèn)使?DefaultRequestToViewNameTranslator
初始化視圖解析器ViewResolvers:先從ApplicationContext中獲取名為viewResolver的Bean,如果沒有,則默認(rèn)InternalResourceViewResolver作為視圖解析器
初始化FlashMapManager:其作?是?于檢索和保存FlashMap(保存從?個URL重定向到另?個URL時的參數(shù)信息),從ApplicationContext發(fā)現(xiàn)有flashMapManager的Bean,如果沒有,則默認(rèn)使?DefaultFlashMapManager
以下是ChatGPT的回答:
這個過程涉及到加載和配置
DispatcherServlet
以及初始化與之相關(guān)的一系列Spring MVC組件。下面是對這個過程的分析和解釋:
DispatcherServlet
的初始化
DispatcherServlet
的初始化是在其父類HttpServletBean
的init()
方法中完成的。這個方法的關(guān)鍵任務(wù)是從web.xml
或者通過編程式配置加載DispatcherServlet
的配置,并調(diào)用子類(如FrameworkServlet
和DispatcherServlet
)的初始化方法。1.
HttpServletBean.init()
- 加載Servlet配置:使用
ServletConfigPropertyValues
從ServletConfig
中讀取配置,這可能包括對Spring MVC組件的定制配置。- 屬性設(shè)置:通過
BeanWrapper
為Servlet設(shè)置屬性,這包括了一些自定義編輯器和資源加載器的設(shè)置。2.
FrameworkServlet.initServletBean()
- 日志記錄:記錄初始化開始和完成的時間,這對于調(diào)試和監(jiān)控是有幫助的。
- WebApplicationContext初始化:通過調(diào)用
initWebApplicationContext()
來建立和配置Web應(yīng)用上下文。這個上下文是Spring MVC框架中所有組件交互的基礎(chǔ)。- 異常處理:處理在初始化過程中可能發(fā)生的異常。
WebApplicationContext
的建立和配置
WebApplicationContext
是Spring MVC中非常核心的部分,它負(fù)責(zé)管理Spring MVC應(yīng)用中的所有Bean。1.
FrameworkServlet.initWebApplicationContext()
- 獲取根上下文:首先嘗試從ServletContext中獲取根上下文。
- 創(chuàng)建新的上下文:如果沒有現(xiàn)成的上下文,創(chuàng)建一個新的。
- 上下文刷新:通過調(diào)用
onRefresh()
來完成上下文的初始化。初始化Spring MVC的9大組件
DispatcherServlet
在Spring MVC框架中起著中央調(diào)度器的作用。它負(fù)責(zé)初始化一系列關(guān)鍵組件,這些組件共同處理HTTP請求。1.
FrameworkServlet.onRefresh()
- 調(diào)用
initStrategies()
:這個方法負(fù)責(zé)初始化Spring MVC的九大組件。2.
DispatcherServlet.initStrategies()
- 組件初始化:包括文件上傳解析器、區(qū)域解析器、主題解析器、處理器映射器、處理器適配器、異常處理器解析器、視圖名稱翻譯器、視圖解析器和FlashMap管理器的初始化。如果應(yīng)用上下文中沒有相應(yīng)的Bean定義,則使用默認(rèn)的策略。
總結(jié)
這個初始化過程體現(xiàn)了Spring框架的靈活性和可擴(kuò)展性。通過這種方式,
DispatcherServlet
加載和配置自己的環(huán)境,并準(zhǔn)備好處理傳入的HTTP請求。每個組件都在整個請求處理流程中扮演著特定的角色,確保Spring MVC應(yīng)用能夠以高度可配置和可擴(kuò)展的方式運行。
處理請求(核?)
DispatcherServlet 接收到請求后, 執(zhí)?doDispatch 調(diào)度?法, 再將請求轉(zhuǎn)給Controller.
我們來看 doDispatch ?法的具體實現(xiàn)
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. 獲取執(zhí)?鏈//遍歷所有的 HandlerMapping 找到與請求對應(yīng)的HandlermappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {this.noHandlerFound(processedRequest, response);return;}//2. 獲取適配器//遍歷所有的 HandlerAdapter,找到可以處理該 Handler 的 HandlerAdapterHandlerAdapter 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. 執(zhí)?攔截器preHandle?法if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}//4. 執(zhí)??標(biāo)?法mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}this.applyDefaultViewName(processedRequest, mv);//5. 執(zhí)?攔截器postHandle?法mappedHandler.applyPostHandle(processedRequest, response, mv);} catch (Exception var20) {dispatchException = var20;} catch (Throwable var21) {dispatchException = new NestedServletException("Handler dispatch failed", var21);}//6. 處理視圖, 處理之后執(zhí)?攔截器afterCompletion?法this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);} catch (Exception var22) {//7. 執(zhí)?攔截器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);}}}
HandlerAdapter 在 Spring MVC 中使?了適配器模式, 下?詳細(xì)再介紹適配器模式, 也叫包裝器模式. 簡單來說就是?標(biāo)類不能直接使?, 通過?個新類進(jìn)?包裝?下, 適配調(diào)??使?.
把兩個不兼容的接?通過?定的?式使之兼容.
HandlerAdapter 主要?于?持不同類型的處理器(如 Controller、HttpRequestHandler 或者Servlet 等),讓它們能夠適配統(tǒng)?的請求處理流程。這樣,Spring MVC 可以通過?個統(tǒng)?的接?來處理來?各種處理器的請求.
從上述源碼可以看出在開始執(zhí)? Controller 之前,會先調(diào)? 預(yù)處理?法 applyPreHandle,? applyPreHandle ?法的實現(xiàn)源碼如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {// 獲取項?中使?的攔截器 HandlerInterceptorHandlerInterceptor 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
, 并執(zhí)?攔截器中的 preHandle ?法,這樣就會咱們前?定義的攔截器對應(yīng)上了,如下圖所?:
如果攔截器返回true, 整個發(fā)放就返回true, 繼續(xù)執(zhí)?后續(xù)邏輯處理
如果攔截器返回fasle, 則中斷后續(xù)操作
DispatcherServlet.doDispatch()
方法的流程
- 處理多部分請求:
- 檢查并處理請求是否為多部分(如文件上傳)。
this.checkMultipart(request)
會在請求是多部分時返回一個包裝后的請求對象。- 獲取處理器(Handler):
- 通過
this.getHandler(processedRequest)
獲取與請求相匹配的HandlerExecutionChain
(處理器執(zhí)行鏈)。這個鏈包含了處理器(如Controller)和一系列攔截器。- 獲取處理器適配器(Handler Adapter):
- 使用
this.getHandlerAdapter(mappedHandler.getHandler())
獲取能夠處理該請求的HandlerAdapter
。HandlerAdapter
負(fù)責(zé)調(diào)用實際的處理器(Controller)方法。- 執(zhí)行攔截器的preHandle方法:
mappedHandler.applyPreHandle(processedRequest, response)
會執(zhí)行攔截器鏈中所有攔截器的preHandle
方法。如果任何一個攔截器返回false
,則中斷處理流程。- 執(zhí)行目標(biāo)方法:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
調(diào)用處理器(Controller)的方法,處理請求并返回ModelAndView
對象。- 執(zhí)行攔截器的postHandle方法:
mappedHandler.applyPostHandle(processedRequest, response, mv)
在處理器方法執(zhí)行后,ModelAndView返回前執(zhí)行。- 處理視圖和模型:
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
處理ModelAndView對象,渲染視圖。- 執(zhí)行攔截器的afterCompletion方法:
- 在請求處理完畢后,無論成功還是發(fā)生異常,都會執(zhí)行
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22)
,調(diào)用攔截器的afterCompletion
方法。攔截器(HandlerInterceptor)的作用
攔截器在Spring MVC中用于在處理器(Controller)執(zhí)行前后執(zhí)行一些操作。它們通常用于日志記錄、權(quán)限檢查、事務(wù)處理等。
- preHandle:在處理器執(zhí)行前調(diào)用。如果返回
false
,則中斷執(zhí)行鏈,后續(xù)的postHandle
和處理器方法將不會被執(zhí)行。- postHandle:在處理器執(zhí)行后,但在視圖渲染前調(diào)用。
- afterCompletion:在請求完全結(jié)束后調(diào)用,用于清理資源。
總結(jié)
這個流程展示了Spring MVC如何處理一個HTTP請求:從確定處理器、適配器,到執(zhí)行攔截器和處理器,再到渲染視圖。這個過程中,攔截器的作用是在請求的前后提供了一個可插拔的方式來干預(yù)處理流程。這種架構(gòu)提供了高度的靈活性和擴(kuò)展性,允許開發(fā)者根據(jù)需要定制請求的處理過程。
適配器模式
HandlerAdapter 在 Spring MVC 中使?了適配器模式
適配器模式定義
適配器模式, 也叫包裝器模式. 將?個類的接?,轉(zhuǎn)換成客?期望的另?個接?, 適配器讓原本接?不兼容的類可以合作?間.
簡單來說就是?標(biāo)類不能直接使?, 通過?個新類進(jìn)?包裝?下, 適配調(diào)??使?. 把兩個不兼容的接?通過?定的?式使之兼容.
?如下?兩個接?, 本?是不兼容的(參數(shù)類型不?樣, 參數(shù)個數(shù)不?樣等等)
可以通過適配器的?式, 使之兼容
?常?活中, 適配器模式也是?常常?的
?如轉(zhuǎn)換插頭, ?絡(luò)轉(zhuǎn)接頭等
出國旅?必備物品之?就是轉(zhuǎn)換插頭. 不同國家的插頭標(biāo)準(zhǔn)是不?樣的, 出國后我們?機/電腦充電器可能就沒辦法使?了. ?如美國電器 110V,中國 220V,就要有?個適配器將 110V 轉(zhuǎn)化為 220V. 國內(nèi)也經(jīng)常使?轉(zhuǎn)換插頭把兩頭轉(zhuǎn)為三頭, 或者三頭轉(zhuǎn)兩頭
適配器模式??
- Target: ?標(biāo)接? (可以是抽象類或接?), 客?希望直接?的接?
- Adaptee: 適配者, 但是與Target不兼容
- Adapter: 適配器類, 此模式的核?. 通過繼承或者引?適配者的對象, 把適配者轉(zhuǎn)為?標(biāo)接?
- client: 需要使?適配器的對象
適配器模式的實現(xiàn)
場景: 前?學(xué)習(xí)的slf4j 就使?了適配器模式, slf4j提供了?系列打印?志的api, 底層調(diào)?的是log4j 或者logback來打?志, 我們作為調(diào)?者, 只需要調(diào)?slf4j的api就?了.
/*** slf4j接?*/
public interface Slf4jApi {void log(String message);
}/*** log4j 接?*/
public class Log4j {public void log(String message){System.out.println("Log4j:"+message);}
}/*** slf4j和log4j適配器*/
public class Slf4jLog4jAdapter implements Slf4jApi{private Log4j log4j;public Slf4jLog4jAdapter(Log4j log4j){this.log4j=log4j;}@Overridepublic void log(String message) {log4j.log(message);}
}/*** 客?端調(diào)?*/
public class Main {public static void main(String[] args) {Slf4jApi api=new Slf4jLog4jAdapter(new Log4j());api.log("我是通過Slf4j打印的");}
}
- Target: ?標(biāo)接?,
Slf4jApi
- Adaptee: 適配者,
Log4j
- Adapter: 適配器類,
Slf4jLog4jAdapter
- client: 需要使?適配器的對象,
Main
可以看出, 我們不需要改變log4j的api,只需要通過適配器轉(zhuǎn)換下, 就可以更換?志框架, 保障系統(tǒng)的平穩(wěn)運?.
適配器模式的實現(xiàn)并不在slf4j-core中(只定義了Logger), 具體實現(xiàn)是在針對log4j的橋接器項?slf4jlog4j12中
設(shè)計模式的使??常靈活, ?個項?中通常會含有多種設(shè)計模式.
適配器模式應(yīng)?場景
?般來說,適配器模式可以看作?種"補償模式",?來補救設(shè)計上的缺陷. 應(yīng)?這種模式算是"?奈之舉", 如果在設(shè)計初期,我們就能協(xié)調(diào)規(guī)避接?不兼容的問題, 就不需要使?適配器模式了所以適配器模式更多的應(yīng)?場景主要是對正在運?的代碼進(jìn)?改造, 并且希望可以復(fù)?原有代碼實現(xiàn)新的功能. ?如版本升級等.
統(tǒng)?數(shù)據(jù)返回格式
強制登錄案例中, 我們共做了兩部分?作
- 通過Session來判斷??是否登錄
- 對后端返回數(shù)據(jù)進(jìn)?封裝, 告知前端處理的結(jié)果
回顧
后端統(tǒng)?返回結(jié)果
@Data public class Result<T> {//業(yè)務(wù)狀態(tài)碼private ResultCode code;//0 成功 -1 失敗 -2 未登錄//錯誤信息private String errMsg;//數(shù)據(jù)private T data; }
后端邏輯處理
@RequestMapping("/getBookListByPage")//為了方便更好拓展,最好返回結(jié)果也是一個對象public Result getBookListByPage(PageRequest pageRequest, HttpSession session) {log.info("查詢翻頁信息,pageRequest:{}", pageRequest);用戶登錄校驗//UserInfo userInfo= (UserInfo) session.getAttribute("session_user_key");//if(userInfo==null||userInfo.getId()<=0||"".equals(userInfo.getUserName())){// //用戶未登錄// return Result.unLogin();//}//校驗成功if (pageRequest.getPageSize() < 0 || pageRequest.getCurrentPage() < 1) {//每頁顯示條數(shù)為負(fù)或者當(dāng)前頁數(shù)不為正數(shù)則錯誤return Result.fail("參數(shù)校驗失敗");}PageResult<BookInfo> bookInfoPageResult = null;try {bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest);return Result.success(bookInfoPageResult);} catch (Exception e) {log.error("查詢翻頁信息錯誤,e:{}", e);return Result.fail(e.getMessage());}}
Result.success(pageResult) 就是對返回數(shù)據(jù)進(jìn)?了封裝
攔截器幫我們實現(xiàn)了第?個功能, 接下來看SpringBoot對第?個功能如何?持
快速??
統(tǒng)?的數(shù)據(jù)返回格式使? @ControllerAdvice
和 ResponseBodyAdvice
接口 的?式實現(xiàn) @ControllerAdvice
表?控制器通知類
添加類 ResponseAdvice
, 實現(xiàn) ResponseBodyAdvice
接?, 并在類上添加 @ControllerAdvice
注解
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//返回之前,需要做的事情//body就是返回的結(jié)果return Result.success(body);}
}
- supports?法: 判斷是否要執(zhí)?beforeBodyWrite?法. true為執(zhí)?, false不執(zhí)?. 通過該?法可以選擇哪些類或哪些?法的response要進(jìn)?處理, 其他的不進(jìn)?處理.
從returnType獲取類名和?法名
- beforeBodyWrite?法: 對response?法進(jìn)?具體操作處理
測試
測試接?: http://127.0.0.1:8080/book/queryBookInfoById?bookId=1
添加統(tǒng)?數(shù)據(jù)返回格式之前:
添加統(tǒng)?數(shù)據(jù)返回格式之后:
存在問題
問題現(xiàn)象:
我們繼續(xù)測試修改圖書的接?: http://127.0.0.1:8080/book/updateBook
結(jié)果顯?, 發(fā)?內(nèi)部錯誤
查看數(shù)據(jù)庫, 發(fā)現(xiàn)數(shù)據(jù)操作成功
查看?志, ?志報錯
多測試?種不同的返回結(jié)果, 發(fā)現(xiàn)只有返回結(jié)果為String類型時才有這種錯誤發(fā)?.
即請求返回類型是Result時就不需要再進(jìn)行處理了
返回結(jié)果為String時不能正確處理
測試代碼:
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("t1")public Boolean t1(){return true;}@RequestMapping("t2")public Integer t2(){return 123;}@RequestMapping("t3")public String t3(){return "hello";}@RequestMapping("t4")public BookInfo t4(){return new BookInfo();}@RequestMapping("t5")public Result t5(){return Result.success("success");}
}@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;//包含一些不應(yīng)該被攔截的的URL路徑private static List<String> excludePath = Arrays.asList("/user/login",//排除這個特定的路徑//因為我們寫的不是完全的前后端分離//下面是為了攔截前端部分的靜態(tài)資源"/css/**","/js/**","/pic/**","/**/*.html","/test/**");@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor)//添加了攔截器.addPathPatterns("/**")// /**表示給所有方法添加攔截器,即匹配所有路徑.excludePathPatterns(excludePath);}
}
解決?案:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//返回之前,需要做的事情//body就是返回的結(jié)果if(body instanceof Result){return body;}if(body instanceof String){return objectMapper.writeValueAsString(Result.success(body));}return Result.success(body);}
}
重新測試, 結(jié)果返回正常:
原因分析:
SpringMVC默認(rèn)會注冊?些?帶的 HttpMessageConverter
(從先后順序排列分別為ByteArrayHttpMessageConverter
,StringHttpMessageConverter
, SourceHttpMessageConverter
,SourceHttpMessageConverter
, AllEncompassingFormHttpMessageConverter
)
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapterimplements BeanFactoryAware, InitializingBean {//...public RequestMappingHandlerAdapter() {this.messageConverters = new ArrayList<>(4);this.messageConverters.add(new ByteArrayHttpMessageConverter());this.messageConverters.add(new StringHttpMessageConverter());if (!shouldIgnoreXml) {try {this.messageConverters.add(new SourceHttpMessageConverter<>());} catch (Error err) {// Ignore when no TransformerFactory implementation is available}}this.messageConverters.add(new AllEncompassingFormHttpMessageConverter())}//...
}
其中 AllEncompassingFormHttpMessageConverter
會根據(jù)項?依賴情況 添加對應(yīng)的HttpMessageConverter
public AllEncompassingFormHttpMessageConverter() {if(!shouldIgnoreXml){try {addPartConverter(new SourceHttpMessageConverter<>());} catch (Error err) {// Ignore when no TransformerFactory implementation is available}if (jaxb2Present && !jackson2XmlPresent) {addPartConverter(new Jaxb2RootElementHttpMessageConverter());}}if(kotlinSerializationJsonPresent){addPartConverter(new KotlinSerializationJsonHttpMessageConverter());}if(jackson2Present){addPartConverter(new MappingJackson2HttpMessageConverter());}else if(gsonPresent){addPartConverter(new GsonHttpMessageConverter());}else if(jsonbPresent){addPartConverter(new JsonbHttpMessageConverter());}if(jackson2XmlPresent&&!shouldIgnoreXml){addPartConverter(new MappingJackson2XmlHttpMessageConverter());}if(jackson2SmilePresent){addPartConverter(new MappingJackson2SmileHttpMessageConverter());}
}
在依賴中引?jackson包后,容器會把 MappingJackson2HttpMessageConverter
?動注冊到messageConverters
鏈的末尾.
Spring會根據(jù)返回的數(shù)據(jù)類型, 從 messageConverters
鏈選擇合適的HttpMessageConverter
.
當(dāng)返回的數(shù)據(jù)是?字符串時, 使?的 MappingJackson2HttpMessageConverter
寫?返回對象.
當(dāng)返回的數(shù)據(jù)是字符申時, StringHttpMessageConverter
會先被遍歷到,這時會認(rèn)為StringHttpMessageConverter
可以使?.
public abstract class AbstractMessageConverterMethodProcessor extendsAbstractMessageConverterMethodArgumentResolverimplements HandlerMethodReturnValueHandler {//...代碼省略protected <T> void writeWithMessageConverters(@Nullable T value,MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponseoutputMessage)throws IOException, HttpMediaTypeNotAcceptableException,HttpMessageNotWritableException {//...代碼省略if (selectedMediaType != null) {selectedMediaType = selectedMediaType.removeQualityValue();for (HttpMessageConverter<?> converter : this.messageConverters) {GenericHttpMessageConverter genericConverter = (converterinstanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);if (genericConverter != null ?((GenericHttpMessageConverter)converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {//getAdvice().beforeBodyWrite 執(zhí)?之后, body轉(zhuǎn)換成了Result類型的結(jié)果body = getAdvice().beforeBodyWrite(body, returnType,selectedMediaType,20 (Class<? extends HttpMessageConverter<?>>)converter.getClass(),inputMessage, outputMessage);if (body != null) {Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn ->"Writing [" + LogFormatUtils.formatValue(theBody,!traceOn) + "]");addContentDispositionHeader(inputMessage, outputMessage);if (genericConverter != null) {genericConverter.write(body, targetType,selectedMediaType, outputMessage);} else {//此時cover為StringHttpMessageConverter((HttpMessageConverter) converter).write(body,selectedMediaType, outputMessage);}} else {if (logger.isDebugEnabled()) {logger.debug("Nothing to write: null body");}}return;}}}//...代碼省略}//...代碼省略
}
在 ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage)
的處理中, 調(diào)??類的write?法
由于 StringHttpMessageConverter
重寫了addDefaultHeaders?法, 所以會執(zhí)??類的?法
然??類 StringHttpMessageConverter
的addDefaultHeaders?法定義接收參數(shù)為String, 此時t為Result類型, 所以出現(xiàn)類型不匹配"Result cannot be cast to java.lang.String"的異常
案例代碼修改
如果?些?法返回的結(jié)果已經(jīng)是Result類型了, 那就直接返回Result類型的結(jié)果即可
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//返回之前,需要做的事情//body就是返回的結(jié)果if(body instanceof Result){return body;}if(body instanceof String){return objectMapper.writeValueAsString(Result.success(body));}return Result.success(body);
}
@SneakyThrows
是lombok的一個注解,會自動幫我們加上trycatch
的
優(yōu)點
-
?便前端程序員更好的接收和解析后端數(shù)據(jù)接?返回的數(shù)據(jù)
-
降低前端程序員和后端程序員的溝通成本, 按照某個格式實現(xiàn)就可以了, 因為所有接?都是這樣返回的.
-
有利于項?統(tǒng)?數(shù)據(jù)的維護(hù)和修改.
-
有利于后端技術(shù)部?的統(tǒng)?規(guī)范的標(biāo)準(zhǔn)制定, 不會出現(xiàn)稀奇古怪的返回內(nèi)容.
統(tǒng)?異常處理
統(tǒng)?異常處理使?的是 @ControllerAdvice
+ @ExceptionHandler
來實現(xiàn)的,@ControllerAdvice
表?控制器通知類, @ExceptionHandler
是異常處理器,兩個結(jié)合表?當(dāng)出現(xiàn)異常的時候執(zhí)?某個通知,也就是執(zhí)?某個?法事件
具體代碼如下:
@Slf4j
@ResponseBody
@ControllerAdvice
public class ErrorHandler {@ExceptionHandlerpublic Result exception(Exception e){log.error("發(fā)生異常,e:{}",e);return Result.fail("內(nèi)部錯誤");}
}
類名, ?法名和返回值可以?定義, 重要的是注解
接?返回為數(shù)據(jù)時, 需要加
@ResponseBody
注解,如果不加這個注解就認(rèn)為返回的是頁面類上面三個注解都要加,還有方法上的那個注解
以上代碼表?,如果代碼出現(xiàn) Exception 異常(包括 Exception 的?類), 就返回?個 Result 的對象, Result 對象的設(shè)置參考 Result.fail(e.getMessage())
public static <T>Result<T> fail(String errMsg){Result result=new Result();result.setCode(ResultCode.FAIL);result.setErrMsg(errMsg);result.setData(null);return result;
}
我們可以針對不同的異常, 返回不同的結(jié)果.
@ResponseBody
@Slf4j
@ControllerAdvice
public class ErrorHandler {@ExceptionHandlerpublic Result exception(Exception e){log.error("發(fā)生異常,e:{}",e);return Result.fail("內(nèi)部錯誤");}@ExceptionHandlerpublic Result exception(NullPointerException e){log.error("發(fā)生異常,e:{}",e);return Result.fail("NullPointerException 異常");}@ExceptionHandlerpublic Result exception(ArithmeticException e){log.error("發(fā)生異常,e:{}",e);return Result.fail("ArithmeticException 異常");}
}
模擬制造異常:
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("t1")public Boolean t1(){int a=1/0;return true;}@RequestMapping("t2")public Integer t2(){String a=null;System.out.println(a.length());return 123;}@RequestMapping("t3")public String t3(){int[] a={1,2,3};System.out.println(a[5]);return "hello";}
}
當(dāng)有多個異常通知時,匹配順序為當(dāng)前類及其?類向上依次匹配
/test/t1
拋出ArithmeticException, 運?結(jié)果如下:
/test/t2
拋出NullPointerException, 運?結(jié)果如下:
/test/t3
拋出Exception, 運?結(jié)果如下:
log.error("發(fā)生異常,e:{}",e);
以上代碼最好都加上這句,不然比如這里調(diào)用/test/t3
就不會在控制臺出現(xiàn)這些錯誤日志了
@ControllerAdvice 源碼分析
統(tǒng)?數(shù)據(jù)返回和統(tǒng)?異常都是基于 @ControllerAdvice
注解來實現(xiàn)的, 通過分析 @ControllerAdvice
的源碼, 可以知道他們的執(zhí)?流程.
點擊 @ControllerAdvice 實現(xiàn)源碼如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {@AliasFor("basePackages")String[] value() default {};@AliasFor("value")String[] basePackages() default {};Class<?>[] basePackageClasses() default {};Class<?>[] assignableTypes() default {};Class<? extends Annotation>[] annotations() default {};
}
從上述源碼可以看出 @ControllerAdvice
派?于 @Component 組件, 這也就是為什么沒有五?注解, ControllerAdvice
就?效的原因.
下?我們看看Spring是怎么實現(xiàn)的, 還是從 DispatcherServlet
的代碼開始分析.DispatcherServlet
對象在創(chuàng)建時會初始化?系列的對象:
public class DispatcherServlet extends FrameworkServlet {//...@Overrideprotected 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);}//...}
對于 @ControllerAdvice
注解,我們重點關(guān)注 initHandlerAdapters(context)
和initHandlerExceptionResolvers(context)
這兩個?法.
- initHandlerAdapters(context)
initHandlerAdapters(context) ?法會取得所有實現(xiàn)了 HandlerAdapter
接?的bean并保存起來,其中有?個類型為 RequestMappingHandlerAdapter
的bean,這個bean就是 @RequestMapping
注解能起作?的關(guān)鍵,這個bean在應(yīng)?啟動過程中會獲取所有被 @ControllerAdvice
注解標(biāo)注的bean對象, 并做進(jìn)?步處理,關(guān)鍵代碼如下:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {//.../*** 添加ControllerAdvice bean的處理*/private void initControllerAdviceCache() {if (getApplicationContext() == null) {return;}//獲取所有所有被 @ControllerAdvice 注解標(biāo)注的bean對象List<ControllerAdviceBean> adviceBeans =ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type forControllerAdviceBean:" + adviceBean);}Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType,MODEL_ATTRIBUTE_METHODS);if (!attrMethods.isEmpty()) {this.modelAttributeAdviceCache.put(adviceBean, attrMethods);}Set<Method> binderMethods =MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);if (!binderMethods.isEmpty()) {this.initBinderAdviceCache.put(adviceBean, binderMethods);}if (RequestBodyAdvice.class.isAssignableFrom(beanType) ||ResponseBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}}if (!requestResponseBodyAdviceBeans.isEmpty()) {this.requestResponseBodyAdvice.addAll(0,requestResponseBodyAdviceBeans);}if (logger.isDebugEnabled()) {int modelSize = this.modelAttributeAdviceCache.size();int binderSize = this.initBinderAdviceCache.size();int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount== 0) {logger.debug("ControllerAdvice beans: none");} else {logger.debug("ControllerAdvice beans: " + modelSize + "@ModelAttribute," + binderSize +" @InitBinder, " + reqCount + " RequestBodyAdvice, " +resCount + " ResponseBodyAdvice");}}}//...}
這個?法在執(zhí)?時會查找使?所有的 @ControllerAdvice
類,把 ResponseBodyAdvice
類放在容器中,當(dāng)發(fā)?某個事件時,調(diào)?相應(yīng)的 Advice ?法,?如返回數(shù)據(jù)前調(diào)?統(tǒng)?數(shù)據(jù)封裝?于DispatcherServlet和RequestMappingHandlerAdapter是如何交互的這就是另?個復(fù)雜的話題了,此處不贅述, 源碼部分難度?較?, 且枯燥, ?家以了解為主.
- initHandlerExceptionResolvers(context)
接下來看 DispatcherServlet
的 initHandlerExceptionResolvers(context)
?法,這個?法會取得所有實現(xiàn)了 HandlerExceptionResolver
接?的bean并保存起來,其中就有?個類型為 ExceptionHandlerExceptionResolver
的bean,這個bean在應(yīng)?啟動過程中會獲取所有被 @ControllerAdvice
注解標(biāo)注的bean對象做進(jìn)?步處理, 代碼如下:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {//...private void initExceptionHandlerAdviceCache() {if (getApplicationContext() == null) {return;}// 獲取所有所有被 @ControllerAdvice 注解標(biāo)注的bean對象List<ControllerAdviceBean> adviceBeans =ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean:" + adviceBean);}ExceptionHandlerMethodResolver resolver = newExceptionHandlerMethodResolver(beanType);if (resolver.hasExceptionMappings()) {this.exceptionHandlerAdviceCache.put(adviceBean, resolver);}if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {this.responseBodyAdvice.add(adviceBean);}}if (logger.isDebugEnabled()) {int handlerSize = this.exceptionHandlerAdviceCache.size();int adviceSize = this.responseBodyAdvice.size();if (handlerSize == 0 && adviceSize == 0) {logger.debug("ControllerAdvice beans: none");} else {logger.debug("ControllerAdvice beans: " +handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");}}}//...
}
當(dāng)Controller拋出異常時, DispatcherServlet
通過 ExceptionHandlerExceptionResolver
來解析異常,?ExceptionHandlerExceptionResolver
?通過 ExceptionHandlerMethodResolver
來解析異常, ExceptionHandlerMethodResolver 最終解析異常找到適?的@ExceptionHandler標(biāo)注的?法是這?:
public class ExceptionHandlerMethodResolver {//...private Method getMappedMethod(Class<? extends Throwable> exceptionType) {List<Class<? extends Throwable>> matches = new ArrayList();//根據(jù)異常類型, 查找匹配的異常處理?法//?如NullPointerException會匹配兩個異常處理?法://handler(Exception e) 和 handler(NullPointerException e)for (Class<? extends Throwable> mappedException :this.mappedMethods.keySet()) {if (mappedException.isAssignableFrom(exceptionType)) {matches.add(mappedException);}}//如果查找到多個匹配, 就進(jìn)?排序, 找到最使?的?法. 排序的規(guī)則依據(jù)拋出異常相對于聲明異常的深度//?如拋出的是NullPointerException(繼承于RuntimeException, RuntimeException?繼承于Exception)//相對于handler(NullPointerException e) 聲明的NullPointerException深度為0,//相對于handler(Exception e) 聲明的Exception 深度 為2//所以 handler(NullPointerException e)標(biāo)注的?法會排在前?if (!matches.isEmpty()) {if (matches.size() > 1) {matches.sort(new ExceptionDepthComparator(exceptionType));}return this.mappedMethods.get(matches.get(0));} else {return NO_MATCHING_EXCEPTION_HANDLER_METHOD;}}//...
}
案例代碼
通過上?統(tǒng)?功能的添加, 我們后端的接?已經(jīng)發(fā)?了變化(后端返回的數(shù)據(jù)格式統(tǒng)?變成了Result類型), 所以我們需要對前端代碼進(jìn)?修改
實際開發(fā)中, 后端接?的設(shè)計需要經(jīng)過多?評審檢查(review). 在接?設(shè)計時就會考慮格式化的統(tǒng)?化,盡可能的避免返?
當(dāng)前是學(xué)習(xí)階段, 給?家講了這個接?設(shè)計的演變過程
登錄??
登錄界?沒有攔截, 只是返回結(jié)果發(fā)?了變化, 所以只需要根據(jù)返回結(jié)果修改對應(yīng)代碼即可
登錄結(jié)果代碼修改
function login() {$.ajax({url:"/user/login",type:"post",data:{"userName":$("#userName").val(),"password":$("#password").val()},success:function(result){console.log(result);if(result!=null&&result.code=="SUCCESS"&&result.data==true){location.href = "book_list.html";}else{alert("用戶名或密碼錯誤");}}});}
圖書列表
針對圖書列表?有兩處變化
- 攔截器進(jìn)?了強制登錄校驗, 如果校驗失敗, 則http狀態(tài)碼返回401, 此時會?ajax的error邏輯處理
- 接?返回結(jié)果發(fā)?了變化
圖書列表代碼修改:
function getBookList() {$.ajax({type: "get",url: "/book/getBookListByPage" + location.search,success: function (result) {//真實的前端處理邏輯比后端復(fù)雜if (result.code == "UNLOGIN") {location.href = "login.html";return;}var finalHtml = "";//加載列表var pageResult = result.data;for (var book of pageResult.records) {//根據(jù)每一條記錄拼接html,也就是一個<tr>finalHtml += '<tr>';finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';finalHtml += '<td>' + book.id + '</td>';finalHtml += '<td>' + book.bookName + '</td>';finalHtml += '<td>' + book.author + '</td>';finalHtml += '<td>' + book.count + '</td>';finalHtml += '<td>' + book.price + '</td>';finalHtml += '<td>' + book.publish + '</td>';finalHtml += '<td>' + book.statusCN + '</td>';finalHtml += '<td>';finalHtml += '<div class="op">';finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';finalHtml += '<a href="javascript:void(0)" οnclick="deleteBook(' + book.id + ')">刪除</a>';finalHtml += '</div>';finalHtml += '</td>';finalHtml += '</tr>';}$("tBody").html(finalHtml);//翻頁信息$("#pageContainer").jqPaginator({totalCounts: pageResult.total, //總記錄數(shù)pageSize: 10, //每頁的個數(shù) visiblePages: 5, //可視頁數(shù)currentPage: pageResult.pageRequest.currentPage, //當(dāng)前頁碼first: '<li class="page-item"><a class="page-link">首頁</a></li>',prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一頁<\/a><\/li>',next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一頁<\/a><\/li>',last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一頁<\/a><\/li>',page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',//頁面初始化和頁碼點擊時都會執(zhí)行onPageChange: function (page, type) {console.log("第" + page + "頁, 類型:" + type);if (type == "change") {location.href = "book_list.html?currentPage=" + page;}}});},error: function (error) {console.log(error);if (error.status == 401) {console.log("401");location.href = "login.html";}}});}
其他
參考圖書列表, 對刪除圖書, 批量刪除圖書,添加圖書, 修改圖書接?添加??強制登錄以及統(tǒng)?格式返回的邏輯處理
- 刪除圖書
function deleteBook(bookId) {var isDelete = confirm("確認(rèn)刪除?");if (isDelete) {//刪除圖書$.ajax({type: "post",url: "/book/updateBook",data: {id: bookId,status: 0},success: function (result) {if (result != null && result.code == "SUCCESS" && result.data == "") {//刪除成功location.href = "book_list.html";} else {alert(result);}},error: function (error) {console.log(error);//用戶未登錄if (error != null && error.status == 401) {location.href = "login.html";}}});}}
- 批量刪除圖書
function batchDelete() {var isDelete = confirm("確認(rèn)批量刪除?");if (isDelete) {//獲取復(fù)選框的idvar ids = [];$("input:checkbox[name='selectBook']:checked").each(function () {ids.push($(this).val());});console.log(ids);$.ajax({type: "post",url: "/book/batchDelete?ids=" + ids,success: function (result) {if (result != null && result.code == "SUCCESS" && result.data == "") {//刪除成功location.href = "book_list.html";} else {alert(result);}},error: function (error) {console.log(error);//用戶未登錄if (error != null && error.status == 401) {location.href = "login.html";}}});}}
- 添加圖書
如果后端返回的結(jié)果是String類型,當(dāng)我們用統(tǒng)一結(jié)果返回時,返回的是JSON字符串,content-type 是 text/html,我們需要把它轉(zhuǎn)為JSON
如果后端進(jìn)行轉(zhuǎn)換:
@RequestMapping(value = "/addBook",produces = "application/json") public String addBook(BookInfo bookInfo)
如果前端進(jìn)行轉(zhuǎn)換:把字符串轉(zhuǎn)為對象
JSON.parse(result)
function add() {$.ajax({type: "post",url: "/book/addBook",data: $("#addBook").serialize(),//提交整個form表單success: function (result) {console.log(result);console.log(typeof result)if (result != null && result.code == "SUCCESS" && result.data == "") {//圖書添加成功location.href = "book_list.html";} else {alert(result);}},error: function (error) {console.log(error);//用戶未登錄if (error != null && error.status == 401) {location.href = "login.html";}}});}
- 獲取圖書詳情
$.ajax({type: "get",url: "/book/queryBookInfoById" + location.search,success: function (result) {if (result != null && result.code == "SUCCESS") {var book = result.data;if (book != null) {//頁面輸入框的填充$("#bookId").val(book.id);$("#bookName").val(book.bookName);$("#bookAuthor").val(book.author);$("#bookStock").val(book.count);$("#bookPrice").val(book.price);$("#bookPublisher").val(book.publish);$("#bookStatus").val(book.status);} else {alert("圖書不存在");}} else {alert(result.errMsg);}},error: function (error) {console.log(error);//用戶未登錄if (error != null && error.status == 401) {location.href = "login.html";}}});
- 修改圖書
function update() {$.ajax({type: "post",url: "/book/updateBook",data: $("#updateBook").serialize(),success: function (result) {if (result != null && result.code == "SUCCESS" && result.data == "") {location.href = "book_list.html";} else {alert(result);}},error: function (error) {console.log(error);//用戶未登錄if (error != null && error.status == 401) {location.href = "login.html";}}});}
總結(jié)
本章節(jié)主要介紹了SpringBoot 對?些統(tǒng)?功能的處理?持.
- 攔截器的實現(xiàn)主要分兩部分: 1. 定義攔截器(實現(xiàn)HandlerInterceptor 接?) 2. 配置攔截器
- 統(tǒng)?數(shù)據(jù)返回格式通過@ControllerAdvice + ResponseBodyAdvice 來實現(xiàn)
- 統(tǒng)?異常處理使?@ControllerAdvice + @ExceptionHandler 來實現(xiàn), 并且可以分異常來處理
- 學(xué)習(xí)了DispatcherServlet的?些源碼.