SpringBoot web開發(fā)源碼深入分析
一、MVC自動(dòng)配置
1、默認(rèn)支持的功能
Spring Boot為Spring MVC提供了自動(dòng)配置,默認(rèn)支持以下功能
- ContentNegotiatingViewResolver和BeanNameViewResolver視圖解析器
- 支持靜態(tài)資源,包括webjars
- 轉(zhuǎn)換器的自動(dòng)注冊(cè)、自定義轉(zhuǎn)換器GenericConverter與格式化
- 支持http消息轉(zhuǎn)換(請(qǐng)求與響應(yīng))
- MessageCodesResolver錯(cuò)誤消息
- 首頁(yè)映射
- 圖標(biāo)自定義
- 自動(dòng)使用ConfigurableWebBindingInitializer,博主百度了一下,它的主要作用就是初始化WebDataBinder,將請(qǐng)求的參數(shù)轉(zhuǎn)化為對(duì)應(yīng)的JavaBean,并且會(huì)結(jié)合類型、格式轉(zhuǎn)換等API一起使用
2、靜態(tài)資源與首頁(yè)相關(guān)源碼解析
SpringBoot啟動(dòng)時(shí)會(huì)默認(rèn)加載 xxxAutoConfiguration 類(自動(dòng)配置類),SpringMVC功能的自動(dòng)配置類為 WebMvcAutoConfiguration
@AutoConfiguration( after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class} ) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) @AutoConfigureOrder(-2147483638) public class WebMvcAutoConfiguration { ... }
然后我們可以看到他有一個(gè)靜態(tài)內(nèi)部類WebMvcAutoConfigurationAdapter,可以看到這是一個(gè)配置類
@Configuration( proxyBeanMethods = false ) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) // 將配置文件的相關(guān)屬性和括號(hào)中的兩個(gè)類進(jìn)行了綁定,然后注冊(cè)到容器中。WebMvcProperties和spring.mvc開頭的配置、WebProperties和spring.web開頭的配置 @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { ... }
然后我們發(fā)現(xiàn),這個(gè)配置類只有一個(gè)有參構(gòu)造器,在這種情況下,我們默認(rèn)有參構(gòu)造器所有參數(shù)的值都會(huì)從容器中獲取
// 這里的WebProperties 和WebMvcProperties都在上面和配置進(jìn)行綁定過了,如果我們沒有配置該配置項(xiàng),那就去類中取默認(rèn)配置的值 // ListableBeanFactory beanFactory Spring的beanFactory // 其他的可以自己去了解下,博主這里沒有特地去搜了 public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { this.resourceProperties = webProperties.getResources(); this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.dispatcherServletPath = dispatcherServletPath; this.servletRegistrations = servletRegistrations; this.mvcProperties.checkConfiguration(); }
那么我們的靜態(tài)資源映射以及webjars都是在哪里進(jìn)行配置的呢,我們往下看找到一個(gè)方法
public void addResourceHandlers(ResourceHandlerRegistry registry) { // 判斷這個(gè)isAddMappings屬性是否為false,默認(rèn)值是true,如果我們?cè)趛aml文件或者properties中改為false,那么就會(huì)進(jìn)這個(gè)條件語(yǔ)句,后面的靜態(tài)資源路徑以及webjars都不會(huì)生效了 if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); } else { // webjars的規(guī)則 this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); // 默認(rèn)靜態(tài)資源地址的處理規(guī)則 this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, "/"); registration.addResourceLocations(new Resource[]{resource}); } }); } }
那我們歡迎頁(yè)是在哪里配置的呢?
我們發(fā)現(xiàn),在這個(gè)WebMvcAutoConfiguration
下面還有一個(gè)靜態(tài)內(nèi)部類EnableWebMvcConfiguration
,它也是一個(gè)配置類
這里面有一個(gè)方法welcomePageHandlerMapping()
HandlerMapping(處理映射器):根據(jù)URL找到對(duì)應(yīng)的處理器
@Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations()); return welcomePageHandlerMapping; }
點(diǎn)進(jìn)WelcomePageHandlerMapping的構(gòu)造方法可以看到,它的邏輯大體上為,如果welcomePage不等于null,而且staticPathPattern是默認(rèn)的/**,就會(huì)去我們的靜態(tài)資源文件夾找index.html,否則就去找有沒有能處理/index接口的映射器
這里的staticPathPattern和spring.mvc.static-path-pattern是綁定在一起的
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { if (welcomePage != null && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage); this.setRootViewName("forward:index.html"); } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { logger.info("Adding welcome page template: index"); this.setRootViewName("index"); } }
3、Rest映射及源碼分析
Rest風(fēng)格支持:使用HTTP請(qǐng)求方式動(dòng)詞來(lái)表示對(duì)資源的操作
具體的可以看我之前的 【SpringMVC】Restful風(fēng)格及中文亂碼問題
- 原來(lái)獲取用戶信息–/getUSer、刪除用戶–/deleteUser、修改用戶–editUser、保存用戶/saveUser
- 使用REST風(fēng)格獲取用戶信息–GET、刪除用戶–DELETE、修改用戶–PUT、保存用戶POST
核心源碼部分:WebMvcAutoConfiguration類下的hiddenHttpMethodFilter()方法
核心配置:如果要從頁(yè)面發(fā)起PUT、DELETE請(qǐng)求,需要在yaml文件中將spring.mvc.hiddenmethod.filter.enabled
設(shè)置為true,如果是客戶端工具如postman發(fā)起,則無(wú)需開啟
@Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"} ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
我們點(diǎn)開OrderedHiddenHttpMethodFilter可以看到它繼承了HiddenHttpMethodFilter這個(gè)類
我們接著跟進(jìn)去 發(fā)現(xiàn)里面有一個(gè)doFilterInternal()方法,請(qǐng)求進(jìn)來(lái)都會(huì)被這個(gè)方法攔截
package org.springframework.web.filter; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; import org.springframework.util.StringUtils; public class HiddenHttpMethodFilter extends OncePerRequestFilter { private static final List<String> ALLOWED_METHODS; public static final String DEFAULT_METHOD_PARAM = "_method"; private String methodParam = "_method"; public HiddenHttpMethodFilter() { } public void setMethodParam(String methodParam) { Assert.hasText(methodParam, "'methodParam' must not be empty"); this.methodParam = methodParam; } // 它會(huì)對(duì)請(qǐng)求方法進(jìn)行判斷 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest requestToUse = request; // 如果是表單是post請(qǐng)求且請(qǐng)求正常,那么它會(huì)判斷請(qǐng)求參數(shù)里面是否存在_method這個(gè)參數(shù) if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) { String paramValue = request.getParameter(this.methodParam); // 判斷_method參數(shù)是否為空 if (StringUtils.hasLength(paramValue)) { // 將參數(shù)轉(zhuǎn)成大寫 String method = paramValue.toUpperCase(Locale.ENGLISH); // 判斷_method參數(shù)是否是PUT、DELETE、PATCH其中的一個(gè),如果滿足就使用requesWrapper重寫了HttpServletRequest的getMethod方法,返回的是傳入的值 if (ALLOWED_METHODS.contains(method)) { requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method); } } } // 這里過濾器放行時(shí)的request是處理后得到的HttpMethodRequestWrapper filterChain.doFilter((ServletRequest)requestToUse, response); } static { ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name())); } // HttpServletRequestWrapper類還是實(shí)現(xiàn)了HttpServletRequest接口 private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper { private final String method; public HttpMethodRequestWrapper(HttpServletRequest request, String method) { super(request); this.method = method; } public String getMethod() { return this.method; } } }
我們可以寫一個(gè)html頁(yè)面還有一個(gè)控制器測(cè)試一下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> Hello World! <form action="/test/user" method="get"> <input type="submit" value="REST-GET"> </form> <form action="/test/user" method="post"> <input type="submit" value="REST-POST"> </form> <form action="/test/user" method="post"> <input name="_method" type="hidden" value="PUT"> <input type="submit" value="REST-PUT"> </form> <form action="/test/user" method="post"> <input name="_method" type="hidden" value="DELETE"> <input type="submit" value="REST-DELETE"> </form> </body> </html>
package com.decade.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping(value = "/test") public class TestController { @RequestMapping(value = "/user", method = RequestMethod.GET) @ResponseBody public String queryUser() { return "get查詢用戶的信息"; } @RequestMapping(value = "/user", method = RequestMethod.POST) @ResponseBody public String editUser() { return "post保存用戶的信息"; } @RequestMapping(value = "/user", method = RequestMethod.DELETE) @ResponseBody public String deleteUser() { return "delete刪除用戶的信息"; } @RequestMapping(value = "/user", method = RequestMethod.PUT) @ResponseBody public String saveUser() { return "put編輯用戶的信息"; } }
驗(yàn)證的時(shí)候遇到一個(gè)問題,那就是如果引入了spring-boot-starter-security
這個(gè)依賴
那么調(diào)用POST、PUT和DELETE接口時(shí)就會(huì)出錯(cuò)
博主查了一下,這是因?yàn)镾pring Boot 與 SpringSecurity整合后,為了防御csrf攻擊,只有GET|OPTIONS|HEAD|TRACE|CONNECTION可以通過
其他方法請(qǐng)求時(shí),需要有token
我將SpringSecurity的依賴注掉之后,驗(yàn)證就通過了
拓展:如果我們要修改HiddenHttpMethodFilter里過濾方法中判斷的參數(shù)名稱,我們可以自己寫一個(gè)配置類,例如我們想將它由_method改為_m,那可以這么寫
package com.decade.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.HiddenHttpMethodFilter; @Configuration(proxyBeanMethods = false) public class MyMvcConfig { @Bean public HiddenHttpMethodFilter createHiddenHttpMethodFilter() { HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter(); // 利用setMethodParam設(shè)置自己想要的參數(shù)名 hiddenHttpMethodFilter.setMethodParam("_m"); return hiddenHttpMethodFilter; } }
4、請(qǐng)求映射原理
1)接下來(lái)我們研究一下,Spring Boot是怎么將一個(gè)個(gè)請(qǐng)求匹配到對(duì)應(yīng)的處理器(即controller)的
根據(jù)我們之前SpringMVC的學(xué)習(xí) 我們可以知道 所有的請(qǐng)求都會(huì)被DispatcherServlet攔截,我們一直跟下去可以發(fā)現(xiàn),DispatcherServlet實(shí)際上也是繼承了HttpServlet
我們著重分析一下DispatcherServlet中的doDispatch()方法,我們發(fā)現(xiàn)有一個(gè)getHandler()方法
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; Object dispatchException = null; try { // 檢查是否是文件上傳請(qǐng)求 processedRequest = this.checkMultipart(request); multipartRequestParsed = processedRequest != request; // 決定是哪個(gè)handler處理當(dāng)前請(qǐng)求,HandlerMapping(處理映射器):根據(jù)URL找到對(duì)應(yīng)的處理器 mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; }
我們嘗試發(fā)起一個(gè)請(qǐng)求,使用debug模式可以發(fā)現(xiàn),這個(gè)getHandler()方法就是對(duì)容器中的handlerMapping進(jìn)行一個(gè)遍歷,查看哪個(gè)處理映射器能處理這個(gè)請(qǐng)求
我們可以看到這里有WelcomePageHandlerMapping(與首頁(yè)相關(guān)的)等映射器
通過分析RequestMappingHandlerMapping,我們可以發(fā)現(xiàn),他這里面保存了所有@RequestMapping 路徑和與之對(duì)應(yīng)控制器類下的方法的映射規(guī)則
我們繼續(xù)深入到AbstractHandlerMapping這個(gè)類下的getHandler()---->getHandlerInternal()方法
然后AbstractHandlerMethodMapping這個(gè)類繼承了AbstractHandlerMapping,并完成了關(guān)于getHandlerInternal()的重寫,接著就是lookupHandlerMethod()----->addMatchingMappings()
------>getMatchingMapping()
然后又跟到getMatchingMapping()------->RequestMappingInfo.getMatchingCondition()
最后,我們發(fā)現(xiàn)在RequestMappingInfo這個(gè)類中,getMatchingCondition()這個(gè)方法會(huì)對(duì)請(qǐng)求類型做一個(gè)篩選,這樣就能將相同路徑不同請(qǐng)求方法的接口區(qū)分開來(lái),如果存在相同請(qǐng)求類型且請(qǐng)求路徑也相同,那么系統(tǒng)就會(huì)報(bào)錯(cuò)
同樣的,如果我們需要自定義映射處理,我們也可以自己給容器中放HandlerMapping
2)問題:那么我們還可以思考一下,我們最開始遍歷的那些handlerMapping是從哪里來(lái)的呢?
我們的目光還是回到DispatcherServlet,這里面有一個(gè)initHandlerMappings()
這里他會(huì)從容器中獲取實(shí)現(xiàn)了HandlerMapping接口的處理映射器
這樣 我們就基本完成了spring boot關(guān)于web開發(fā)的源碼分析
到此這篇關(guān)于SpringBoot web開發(fā)源碼深入分析的文章就介紹到這了,更多相關(guān)SpringBoot web開發(fā)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?18?新特性之Web服務(wù)器?jwebserver功能
JEP?408:?Simple?Web?Server,是這次Java?18推出的一個(gè)比較獨(dú)立的全新功能點(diǎn)。我們可以通過命令行工具來(lái)啟動(dòng)一個(gè)提供靜態(tài)資源訪問的迷你Web服務(wù)器,本文通過一個(gè)構(gòu)建HTML頁(yè)面的例子,來(lái)嘗試一下jwebserver的功能2022-04-04在SpringBoot項(xiàng)目中獲取Request的四種方法
這篇文章主要為大家詳細(xì)介紹了SpringBoot項(xiàng)目中獲取Request的四種方法,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴可以學(xué)習(xí)一下2023-11-11JAVA 獲取系統(tǒng)當(dāng)前時(shí)間實(shí)例代碼
這篇文章主要介紹了JAVA 獲取系統(tǒng)當(dāng)前時(shí)間實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-10-10簡(jiǎn)單了解Java方法的定義和使用實(shí)現(xiàn)詳解
這篇文章主要介紹了簡(jiǎn)單了解Java方法的定義和使用實(shí)現(xiàn)詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12Java編程guava RateLimiter實(shí)例解析
這篇文章主要介紹了Java編程guava RateLimiter實(shí)例解析,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01Java構(gòu)建菜單樹的實(shí)現(xiàn)示例
本文主要介紹了Java構(gòu)建菜單樹的實(shí)現(xiàn)示例,像一級(jí)菜單,二級(jí)菜單,三級(jí)菜單甚至更多層級(jí)的菜單,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05Java實(shí)現(xiàn)計(jì)算器設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)計(jì)算器設(shè)計(jì),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07