萌新學習SpringMVC
- 2020 年 5 月 20 日
- 筆記
前言
只有光頭才能變強。
文本已收錄至我的GitHub精選文章,歡迎Star://github.com/ZhongFuCheng3y/3y
這篇SpringMVC
被催了很久了,這陣子由於做整合系統的事,所以非常非常地忙。這周末早早就回了公司肝這篇文章了。
如果關注三歪的同學會發現,三歪最近寫的很多文章都是結合了現有的系統去寫的。這些問題都是真實開發場景會遇到的、用的上的,這些案例對未工作的同學幫助應該還是蠻大的。
不多BB了,還是進入今天的正題吧「SpringMVC」
先簡單聊聊SpringMVC
如果你們玩知乎,很可能會看到我的身影。我經常會去知乎水回答。在知乎有很多初學者都會問的一個問題:「我學習SpringMVC需要什麼樣的基礎」
我一定會讓他們先學Servlet,再學SpringMVC的。雖然說我們在現實開發中幾乎不會寫原生Servlet的程式碼了,但我始終認為學完Servlet再學SpringMVC,對理解SpringMVC是有好處的。
三歪題外話:我當時在學SpringMVC之前其實已經接觸過另外一個web框架(當然了Servlet也是學了的),那就是「大名鼎鼎」的Struts2。只要是Struts2有的功能,SpringMVC都會有。
當時初學Struts2的時候用的是XML配置的方式去開發的,再轉到SpringMVC註解的時候,覺得SpringMVC真香。
Struts2在2020年已經不用學了,學SpringMVC的基礎是Servlet,只要Servlet基礎還行,上手SpringMVC應該不成問題。
從Servlet到SpringMVC,你會發現SpringMVC幫我們做了很多的東西,我們的程式碼肯定是沒以前多了。
Servlet:
我們以前可能需要將傳遞進來的參數手動封裝成一個Bean,然後繼續往下傳:
SpringMVC:
現在SpringMVC自動幫我們將參數封裝成一個Bean
Servlet:
以前我們要導入其他的jar
包去手動處理文件上傳的細節:
SpringMVC:
現在SpringMVC上傳文件用一個MultipartFile對象都給我們封裝好了
……..
說白了,在Servlet時期我們這些活都能幹,只不過SpringMVC把很多東西都給屏蔽了,於是我們用起來就更加舒心了。
在學習SpringMVC的時候實際上也是學習這些功能是怎麼用的而已,並不會太難。這次整理的SpringMVC電子書其實也是在講SpringMVC是如何使用的
- 比如說傳遞一個日期字元串來,SpringMVC默認是不能轉成日期的,那我們可以怎麼做來實現。
- SpringMVC的文件上傳是怎麼使用的
- SpringMVC的攔截器是怎麼使用的
- SpringMVC是怎麼對參數綁定的
- ……
現在「電子書」已經放出來了,但是別急,重頭戲在後面。顯然,通過上面的電子書是可以知道SpringMVC是怎麼用的。
但是這在面試的時候人家是不會問你SpringMVC的一些用法的,而SpringMVC面試問得最多的就是:SpringMVC請求處理的流程是怎麼樣的。
其實也很簡單,流程就是下面這張圖:
再簡化一點,可以發現流程不複雜
在面試的時候甚至能一句話就講完了,但這夠嗎,這是面試官想要的嗎?那肯定不是。那我們想知道SpringMVC是做了什麼嗎?想的吧(不管你們想不想,反正三歪想看)。
由於想要主流程更加清晰一點,我會在源碼添加部分注釋以及刪減部分的程式碼
以@ResponseBody和@RequestBody的Controller程式碼講解為主,這是線上環境用得最多的
DispatcherServlet源碼
首先我們看看DispatcherServlet的類結構,可以清楚地發現實際DispatcherServlet就是Servlet介面的一個子類(這也就是為什麼網上這麼多人說DispatcherServlet的原理實際上就是Servlet)
我們在DispatcherServlet類上可以看到很多熟悉的成員變數(組件),所以看下來,我們要的東西,DispatcherServlet可全都有。
// 文件處理器
private MultipartResolver multipartResolver;
// 映射器
private List<HandlerMapping> handlerMappings;
// 適配器
private List<HandlerAdapter> handlerAdapters;
// 異常處理器
private List<HandlerExceptionResolver> handlerExceptionResolvers;
// 視圖解析器
private List<ViewResolver> viewResolvers;
然後我們會發現它們在initStrategies()
上初始化:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
請求進到DispatcherServlet,其實全部都會打到doService()
方法上。我們看看這個doService()
方法做了啥:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 設置一些上下文...(省略一大部分)
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
try {
// 調用doDispatch
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
所以請求會走到doDispatch(request, response);
裡邊,我們再進去看看:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 檢查是不是文件上傳請求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// 得到對應的hanlder適配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 攔截前置處理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 真實處理請求
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 視圖解析器處理
applyDefaultViewName(processedRequest, mv);
// 攔截後置處理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
}
}
這裡的流程跟我們上面的圖的流程幾乎是一致的了。我們從源碼可以知道的是,原來SpringMVC的攔截器是在MappingHandler的時候一齊返回的,返回的是一個HandlerExecutionChain
對象。這個對象也不難,我們看看:
public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
// 真實的handler
private final Object handler;
// 攔截器List
private HandlerInterceptor[] interceptors;
private List<HandlerInterceptor> interceptorList;
private int interceptorIndex = -1;
}
OK,整體的流程我們是已經看完了,順便要不我們去看看它是怎麼找到handler的?**三歪帶著你們沖!**我們點進去getHandler()
後,發現它就把默認實現的Handler遍歷一遍,然後選出合適的:
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 遍歷一遍默認的Handler實例,選出合適的就返回
for (HandlerMapping hm : this.handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
return null;
}
再進去getHandler
裡邊看看唄,裡邊又有幾層,我們最後可以看到它根據路徑去匹配,走到了lookupHandlerMethod
這麼一個方法
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
// 獲取路徑
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
// 對匹配的排序,找到最佳匹配的
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
lookupPath + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
找攔截器大概也是上面的一個過程,於是我們就可以順利拿到HandlerExecutionChain
了,找到HandlerExecutionChain
後,我們是先去拿對應的HandlerAdaptor
。我們也去看看裡邊做了什麼:
// 遍歷HandlerAdapter實例,找到個合適的返回
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
for (HandlerAdapter ha : this.handlerAdapters) {
if (ha.supports(handler)) {
return ha;
}
}
}
我們看一個常用HandlerAdapter實例RequestMappingHandlerAdapter
,會發現他會初始化很多的參數解析器,其實我們經常用的@ResponseBody
解析器就被內置在裡邊:
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
// ResponseBody Requestbody解析器
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), t
// 等等
return resolvers;
}
得到HandlerAdaptor後,隨之而行的就是攔截器的前置處理,然後就是真實的mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
。
這裡邊嵌套了好幾層,我就不一一貼程式碼了,我們會進入ServletInvocableHandlerMethod#invokeAndHandle
方法,我們看一下這裡邊做了什麼:
public void invokeAndHandle(ServletWebRequest webRequest,
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 處理請求
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true);
return;
}
}
//..
mavContainer.setRequestHandled(false);
try {
// 處理返回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
}
處理請求的方法我們進去看看invokeForRequest
public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 得到參數
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
// 調用方法
Object returnValue = doInvoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
}
return returnValue;
}
我們看看它是怎麼處理參數的,getMethodArgumentValues
方法進去看看:
private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 得到參數
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 找到適配的參數解析器
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
}
//.....
}
return args;
}
這些參數解析器實際上在HandlerAdaptor內置的那些,這裡不好放程式碼,所以我截個圖吧:
針對於RequestResponseBodyMethodProcessor解析器我們看看裡邊做了什麼:
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 通過Converters對參數轉換
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
// ...
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
return arg;
}
再進去readWithMessageConverters
裡邊看看:
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// ...處理請求頭
try {
inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);
// HttpMessageConverter實例去對參數轉換
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
body = genericConverter.read(targetType, contextClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
}
else {
body = null;
body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
}
break;
}
}
//...各種判斷
return body;
}
看到這裡,有沒有看不懂,想要退出的感覺了??別慌,三歪帶你們看看這份熟悉的配置:
<!-- 啟動JSON返回格式 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonMessageConverter" />
</list>
</property>
</bean>
<bean id="jacksonMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
<value>application/x-www-form-urlencoded;charset=UTF-8</value>
</list>
</property>
<property name="objectMapper" ref="jacksonObjectMapper" />
</bean>
<bean id="jacksonObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
我們在SpringMVC想要使用@ResponseBody
返回JSON格式都會在配置文件上配置上面的配置,RequestMappingHandlerAdapter
這個適配器就是上面所說的那個,內置了RequestResponseBodyMethodProcessor
解析器,然後MappingJackson2HttpMessageConverter
實際上就是HttpMessageConverter
介面的實例
然後在返回的時候也經過HttpMessageConverter去將參數轉換後,寫給HTTP響應報文。轉換的流程大致如圖所示:
視圖解析器後面就不貼了,大概的流程就如上面的源碼,我再畫個圖來加深一下理解吧:
最後
SpringMVC我們使用的時候非常簡便,在內部實際上幫我們做了很多(有各種的HandlerAdaptor),SpringMVC的請求流程面試的時候還是面得很多的,還是可以看看源碼它幫我們做了什麼,過一遍可能會發現自己能看懂以前的配置了。
參考資料:
- //www.cnblogs.com/java-chen-hao/category/1503579.html
- //www.jianshu.com/p/1bff57c74037
- //stackoverflow.com/questions/18682486/why-does-spring-mvc-need-at-least-two-contexts
各類知識點總結
- 92頁的Mybatis
- 129頁的多執行緒
- 141頁的Servlet
- 158頁的JSP
- 76頁的集合
- 64頁的JDBC
- 105頁的數據結構和演算法
- 142頁的Spring
- 58頁的過濾器和監聽器
- 30頁的HTTP
- Hibernate
- AJAX
- Redis
- ……
涵蓋Java後端所有知識點的開源項目(已有8K+ star):
如果大家想要實時關注我更新的文章以及分享的乾貨的話,微信搜索Java3y。