詳解SpringMVC組件之HandlerMapping(一)
1、HandlerMapping簡介
HandlerMapping組件是Spring MVC核心組件,用來根據(jù)請求的request查找對應的Handler。
在Spring MVC中,有各式各樣的Web請求,每個請求都需要一個對應的Handler來處理,具體接收到一個request請求,應該有那個Handler處理呢?這就是HandlerMapping組件的作用。
在Spring MVC框架中,HandlerMapping組件及其實現(xiàn)類的如下所示:

在HandlerMapping類的層級結構圖中,MatchableHandlerMapping接口是一個從Spring4.3.1開始新增的一個接口,用來判斷給定的請求是否符合請求條件。
除此之外,HandlerMapping接口有一個公共的抽象類AbstractHandlerMapping,所有子孫實現(xiàn)類都需要繼承。
該抽象類下有三個直接子類,分別是AbstractHandlerMethodMapping、AbstractUrlHandlerMapping和RouterFunctionMapping
其中RouterFunctionMapping是從Spring MVC5.2開始引入的,主要用于WebFlux處理中;
而另外兩個直接實現(xiàn)類,代表了兩大類實現(xiàn)方式:
- AbstractUrlHandlerMapping表示根據(jù)url獲取對應的handler;
- AbstractHandlerMethodMapping表示基于方法的映射方式,這也是我們在實際工作中使用較多的一種方式。
2、AbstractHandlerMapping抽象類

AbstractHandlerMapping是HandlerMapping的抽象類,所有子類都是繼承于該抽象類。該抽象類采用了模板方法,定義了HandlerMapping的核心邏輯。
在抽象類AbstractHandlerMapping中,通過繼承WebApplicationObjectSupport類(間接實現(xiàn)了ApplicationContextAware接口),實現(xiàn)了攔截器相關信息的初始化,然后實現(xiàn)了接口中的getHandler()方法,通過調(diào)用抽象方法getHandlerInternal()獲取Handler,然后把Handler封裝成HandlerExecutionChain對象(該對象包括了攔截器信息和跨域訪問相關信息),并進行返回。
攔截器初始化:
首先分析,AbstractHandlerMapping抽象類如何初始化攔截器的。因為AbstractHandlerMapping抽象類間接繼承了ApplicationContextAware接口,所以容器初始化是會自動調(diào)用setApplicationContext()方法,該setApplicationContext()方法經(jīng)過ApplicationObjectSupport、WebApplicationObjectSupport層級,最終調(diào)用了AbstractHandlerMapping抽象類的initApplicationContext()方法,攔截器的初始化工作就是在這個方法中實現(xiàn)的,代碼如下:
@Override
protected void initApplicationContext() throws BeansException {
extendInterceptors(this.interceptors);
detectMappedInterceptors(this.adaptedInterceptors);
initInterceptors();
}其中,extendInterceptors()方法是模板方法,供子類重寫,提供添加或修改攔截器的入口;detectMappedInterceptors()方法,用于將容器(包括父級容器)中所有注冊的MappedInterceptor類型的Bean添加到adaptedInterceptors屬性中;initInterceptors()方法用于初始化攔截器。
detectMappedInterceptors()方法 通過BeanFactoryUtils.beansOfTypeIncludingAncestors()方法查詢所有的MappedInterceptor類型的實例,并添加到adaptedInterceptors屬性中。
protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
mappedInterceptors.addAll(
BeanFactoryUtils.beansOfTypeIncludingAncestors(
obtainApplicationContext(), MappedInterceptor.class, true, false).values());
}initInterceptors()方法 在初始化方法中,主要實現(xiàn)了把interceptors屬性中的攔截器根據(jù)攔截器類型,進行適配,然后放到adaptedInterceptors變量中。
在Spring4.2之前的版本,有一個變量mappedInterceptors ,所以MappedInterceptor類型的攔截器會放到該變量中,但是再Spring4.2及之后的版本,該變量被移除,所以interceptors屬性中的MappedInterceptor類型(HandlerInterceptor的子類)的實例,進行適配時按照HandlerInterceptor類型進行,并統(tǒng)一保存到了adaptedInterceptors變量,不再進行區(qū)分。
protected void initInterceptors() {
if (!this.interceptors.isEmpty()) {
for (int i = 0; i < this.interceptors.size(); i++) {
Object interceptor = this.interceptors.get(i);
if (interceptor == null) {
throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
}
this.adaptedInterceptors.add(adaptInterceptor(interceptor));
}
}
}
protected HandlerInterceptor adaptInterceptor(Object interceptor) {
if (interceptor instanceof HandlerInterceptor) {
return (HandlerInterceptor) interceptor;
}
else if (interceptor instanceof WebRequestInterceptor) {
return new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor) interceptor);
}
else {
throw new IllegalArgumentException("Interceptor type not supported: " + interceptor.getClass().getName());
}
}getHandler()方法實現(xiàn)
在HandlerMapping接口中,通過getHandler()方法獲取request對應的處理器Handler和攔截器Interceptor的,在AbstractHandlerMapping抽象類的實現(xiàn)如下:
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//調(diào)用模板方法,獲取request對應的處理器Handler
Object handler = getHandlerInternal(request);
if (handler == null) {//如果handler為空,獲取默認的handler
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// 如果handler是實例的名稱,需要從容器中獲取對應的Bean實例
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
//根據(jù)Handler獲取對應的executionChain對象,該過程會把對應的攔截器添加到executionChain對象中
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
logger.debug("Mapped to " + executionChain.getHandler());
}
//處理跨域問題
if (hasCorsConfigurationSource(handler)) {
//初始化時如果有跨域配置,則獲取config 對象
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
//如果當前handler中有跨域配置,獲取handlerConfig對象
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
//合并配置
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
//把跨域處理的攔截器,添加到HandlerExecutionChain對象中
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}在getHandler()方法中,主要做了以下幾件事:
- 調(diào)用模板方法getHandlerInternal(),獲取request對應的handler。模板方法由子類實現(xiàn)。
- 當獲取的handler為空時,獲取默認的handler,即獲取對應的defaultHandler屬性
- 判斷handler是否時Bean的名稱,如果是,獲取對應的Bean實例
- 獲取對應的HandlerExecutionChain對象,封裝了初始化時的攔截器
- 處理跨域問題,處理跨域問題涉及到了內(nèi)部類PreFlightHandler、CorsInterceptor和屬性CorsProcessor對應的實現(xiàn)類,這里暫不深入分析。
3、AbstractUrlHandlerMapping類
AbstractUrlHandlerMapping系列的類都繼承自AbstractUrlHandlerMapping抽象類,主要用來通過URL進行匹配。思路如下:把URL與Handler的對應關系存到一個Map中,然后在getHandlerInternal方法中,根據(jù)URL去獲取對應的Handler對象,在AbstractUrlHandlerMapping抽象類中,主要實現(xiàn)了根據(jù)url獲取對應Handler的方法,如何初始化這個Map對象,交由子類進行實現(xiàn)。
3.1、定義的屬性
//根處理器,處理“/”的處理器 @Nullable private Object rootHandler; //是否匹配尾部的“/”,比如:如果設置為ture,則"/users"的匹配模式,也會匹配"/users/" private boolean useTrailingSlashMatch = false; //設置是否延遲加載,只對單例的處理器有效 private boolean lazyInitHandlers = false; //保存request和Handler對應關系的變量 private final Map<String, Object> handlerMap = new LinkedHashMap<>();
3.2、getHandlerInternal()方法
實現(xiàn)了父類中的抽象方法,根據(jù)request獲取對應的handler,實際上還有由定義的lookupHandler()方法實現(xiàn)。如果沒有獲取對應的handler,就會嘗試獲取根處理器或默認處理器。
@Override
@Nullable
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
//獲取lookupPath,并保存到request屬性中
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
request.setAttribute(LOOKUP_PATH, lookupPath);
//獲取lookupPath 對應的handler
Object handler = lookupHandler(lookupPath, request);
if (handler == null) {//如果沒有獲取到對應的handler,則進行下面處理
// We need to care for the default handler directly, since we need to
// expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
Object rawHandler = null;
if ("/".equals(lookupPath)) {//如果時根路徑,則獲取根處理器,即屬性rootHandler中保存的處理器
rawHandler = getRootHandler();
}
if (rawHandler == null) {//獲取默認處理器,在父類中定義,即父類中的defaultHandler屬性
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {//獲取對應的Bean實例
String handlerName = (String) rawHandler;
rawHandler = obtainApplicationContext().getBean(handlerName);
}
//模板方法,校驗處理器,交由子類實現(xiàn)或擴展
validateHandler(rawHandler, request);
//根據(jù)原始的handler構建實際的handler,主要實現(xiàn)構建HandlerExecutionChain對象,并在request添加對應的參數(shù),后續(xù)在詳細分析
handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
}
}
return handler;
}在代碼上,添加了注釋,不再分析處理邏輯,我們下面詳細分析其中的lookupHandler()方法和buildPathExposingHandler()方法。
3.2.1、lookupHandler()方法
因為從Map對象獲取對應的Handler,不是簡單的Map.get(),因為還涉及到了正則匹配等問題,所以在專門的方法中進行處理,在該方法中,主要實現(xiàn)了根據(jù)lookupPath獲取對應的handler,然后處理當獲取多個匹配handler后,如何獲取最佳匹配的Handler等。
代碼如下:
@Nullable
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
// urlPath正好對應Map的key,則直接獲取,并進行處理
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(handler, request);
return buildPathExposingHandler(handler, urlPath, urlPath, null);
}
// 模式匹配,獲取handlerMap中對應的所有可能匹配的模式
List<String> matchingPatterns = new ArrayList<>();
for (String registeredPattern : this.handlerMap.keySet()) {
if (getPathMatcher().match(registeredPattern, urlPath)) {
matchingPatterns.add(registeredPattern);
}
else if (useTrailingSlashMatch()) {
if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
matchingPatterns.add(registeredPattern + "/");
}
}
}
String bestMatch = null;
//定義最佳匹配的比較器,為了獲取最佳的處理器Handler
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
if (!matchingPatterns.isEmpty()) {
//根據(jù)比較器進行排序,最佳handler排在第一個
matchingPatterns.sort(patternComparator);
if (logger.isTraceEnabled() && matchingPatterns.size() > 1) {
logger.trace("Matching patterns " + matchingPatterns);
}
//獲取最佳的handler
bestMatch = matchingPatterns.get(0);
}
if (bestMatch != null) {//獲取最佳處理器對應的key,然后進一步處理Handler
//獲取key對應的Handler
handler = this.handlerMap.get(bestMatch);
if (handler == null) {//如果沒有獲取到,則移除尾部的斜桿繼續(xù)嘗試
if (bestMatch.endsWith("/")) {
handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
}
if (handler == null) {//還是獲取不到,直接拋出異常
throw new IllegalStateException(
"Could not find handler for best pattern match [" + bestMatch + "]");
}
}
// Bean name or resolved handler?
if (handler instanceof String) {//如果時bean的名稱,則獲取對應的實例
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(handler, request);
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);
//最佳匹配可能存在多個,比如:/book/{id}和/book/{name},那就再添加一個UriTemplateVariablesHandlerInterceptor攔截器,并在request設置一個key為 org.springframework.web.servlet.HandlerMapping.uriTemplateVariables的變量。
Map<String, String> uriTemplateVariables = new LinkedHashMap<>();
for (String matchingPattern : matchingPatterns) {
if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
uriTemplateVariables.putAll(decodedVars);
}
}
if (logger.isTraceEnabled() && uriTemplateVariables.size() > 0) {
logger.trace("URI variables " + uriTemplateVariables);
}
return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
}
// No handler found...
return null;
}3.2.2、buildPathExposingHandler()方法
在Map對象中映射的是真正的handler對象,在buildPathExposingHandler()方法中,實現(xiàn)了把rawHandler封裝成HandlerExecutionChain對象,然后添加內(nèi)部攔截器類PathExposingHandlerInterceptor和UriTemplateVariablesHandlerInterceptor。
protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern,
String pathWithinMapping, @Nullable Map<String, String> uriTemplateVariables) {
HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping));
if (!CollectionUtils.isEmpty(uriTemplateVariables)) {
chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables));
}
return chain;
}其中,PathExposingHandlerInterceptor攔截器主要設置了request中的 “xxx.bestMatchingHandler”、".introspectTypeLevelMapping"(一直為false)、.bestMatchingPattern"和".pathWithinHandlerMapping"屬性,UriTemplateVariablesHandlerInterceptor攔截器,主要是當uriTemplateVariables不為空時設置,主要添加了"xxx.uriTemplateVariables"屬性,值就是變量uriTemplateVariables。其中,xxx表示HandlerMapping.class.getName(),即為“org.springframework.web.servlet.HandlerMapping”。
3.3、handlerMap的初始化
在AbstractUrlHandlerMapping抽象類中,handlerMap(request和handler映射關系)的初始化主要由registerHandler()方法來實現(xiàn)的。而registerHandler()方法,一般在子類中進行調(diào)用,從而實現(xiàn)不同的子類就可以通過注冊不同的Handler將組件創(chuàng)建出來。
//第一個方法:該重載方法,循環(huán)調(diào)用第二種實現(xiàn)真正的處理器注冊
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
Assert.notNull(urlPaths, "URL path array must not be null");
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}
//第二個方法:真正實現(xiàn)處理器的注冊
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(urlPath, "URL path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;
// 加載處理器(當處理器是非延時加載時進行)
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
ApplicationContext applicationContext = obtainApplicationContext();
if (applicationContext.isSingleton(handlerName)) {
resolvedHandler = applicationContext.getBean(handlerName);
}
}
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {//如果Map中存在處理器,且和當前的不一樣,就直接拋出異常。
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {//當前Map不存在處理器
if (urlPath.equals("/")) {//設置根處理器
if (logger.isTraceEnabled()) {
logger.trace("Root mapping to " + getHandlerDescription(handler));
}
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {//設置默認處理器
if (logger.isTraceEnabled()) {
logger.trace("Default mapping to " + getHandlerDescription(handler));
}
setDefaultHandler(resolvedHandler);
}
else {//在Map中添加urlPath和處理器對應的關系
this.handlerMap.put(urlPath, resolvedHandler);
if (logger.isTraceEnabled()) {
logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
}4、BeanNameUrlHandlerMapping類
4.1、使用方法
1、定義一個Controller
public class TestController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mav = new ModelAndView("test");
mav.addObject("context", “test”);
return mav;
}
}2、TestController 注入到Spring容器
@Configuration
public class ControllerBeanConfig {
/**
* 注意 :
* 1. 該 bean 實現(xiàn)了接口 org.springframework.web.servlet.mvc.Controller,
* 2. 該 bean 沒有使用注解 @Controller,
* (如果使用了注解@Controller,就會被RequestMappingHandlerMapping接管,而不是由BeanNameUrlHandlerMapping處理)
* 3. 映射到匹配 /test/* 的url
* @return
*/
@Bean(name = "/test/*")
public TestController beanTestController() {
return new TestController();
}
}通過上述配置,在初始化時,會采用BeanNameUrlHandlerMapping類,進行獲取request對應de 處理器。在這個過程中,BeanNameUrlHandlerMapping類是如何產(chǎn)生作用的呢?下面我們詳細的分析。
4.2、AbstractDetectingUrlHandlerMapping、BeanNameUrlHandlerMapping原理
BeanNameUrlHandlerMapping類繼承了抽象類AbstractDetectingUrlHandlerMapping,然后又繼承了AbstractUrlHandlerMapping抽象類,AbstractUrlHandlerMapping抽象類在前面已經(jīng)分析過了。在前面介紹抽象類AbstractUrlHandlerMapping的時候,我們知道會在spring初始化的時候,執(zhí)行initApplicationContext()方法,我們閱讀AbstractDetectingUrlHandlerMapping類的代碼時,發(fā)現(xiàn)它又重寫了initApplicationContext()方法,所以在spring初始化的時候,會執(zhí)行該方法。代碼如下:
@Override
public void initApplicationContext() throws ApplicationContextException {
super.initApplicationContext();
detectHandlers();
}其中,super.initApplicationContext()方法調(diào)用了父類中的方法,前面已經(jīng)介紹過了。同時,又調(diào)用了detectHandlers()方法,該方法主要實現(xiàn)了檢測容器中注冊的所有的handler。根據(jù)detectHandlersInAncestorContexts參數(shù)的配置,可以檢查當前容器或者當前容器及其祖先容器中注冊的handler。代碼如下:
protected void detectHandlers() throws BeansException {
ApplicationContext applicationContext = obtainApplicationContext();
String[] beanNames = (this.detectHandlersInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
applicationContext.getBeanNamesForType(Object.class));
// Take any bean name that we can determine URLs for.
for (String beanName : beanNames) {
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
// URL paths found: Let's consider it a handler.
registerHandler(urls, beanName);
}
}
if ((logger.isDebugEnabled() && !getHandlerMap().isEmpty()) || logger.isTraceEnabled()) {
logger.debug("Detected " + getHandlerMap().size() + " mappings in " + formatMappingName());
}
}在detectHandlers()方法中,通過處理器實例,可以獲取對應的url,該邏輯由determineUrlsForHandler()方法實現(xiàn),該方法由子類BeanNameUrlHandlerMapping中實現(xiàn)。然后再把urls和對應的handler通過父類AbstractUrlHandlerMapping中的registerHandler()方法,注冊到Map對象中。
最后,determineUrlsForHandler()方法的實現(xiàn),這里主要通過判斷對應實例的name或aliases 來判斷,只有以“/”開頭的才是有效的url,代碼如下:
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
@Override
protected String[] determineUrlsForHandler(String beanName) {
List<String> urls = new ArrayList<>();
if (beanName.startsWith("/")) {
urls.add(beanName);
}
String[] aliases = obtainApplicationContext().getAliases(beanName);
for (String alias : aliases) {
if (alias.startsWith("/")) {
urls.add(alias);
}
}
return StringUtils.toStringArray(urls);
}
}通過上面的分析,我們知道:在initApplicationContext()方法中實現(xiàn)初始化工作,其中AbstractDetectingUrlHandlerMapping類主要實現(xiàn)了處理器的檢測,BeanNameUrlHandlerMapping類實現(xiàn)handler對應url的判斷。
5、其他
通過前面的分析,我們知道HandlerMapping有兩個分支的實現(xiàn),在這篇中我們已經(jīng)分析了AbstractUrlHandlerMapping系列的實現(xiàn)
由于篇幅原因,AbstractHandlerMethodMapping系列的分析,我們在《詳解SpringMVC組件之HandlerMapping(二)》中進行。
到此這篇關于詳解SpringMVC組件之HandlerMapping(一)的文章就介紹到這了,更多相關SpringMVC的HandlerMapping內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java 實戰(zhàn)項目錘煉之在線購書商城系統(tǒng)的實現(xiàn)流程
讀萬卷書不如行萬里路,只學書上的理論是遠遠不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+jsp+mysql+servlet+ajax實現(xiàn)一個在線購書商城系統(tǒng),大家可以在過程中查缺補漏,提升水平2021-11-11
Springboot jar文件如何打包zip在linux環(huán)境運行
這篇文章主要介紹了Springboot jar文件如何打包zip在linux環(huán)境運行,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-02-02
Java連接數(shù)據(jù)庫JDBC技術之prepareStatement的詳細介紹
這篇文章主要介紹了Java連接數(shù)據(jù)庫JDBC技術之prepareStatement的詳細介紹,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07
springboot實現(xiàn)修改請求狀態(tài)404改為200
這篇文章主要介紹了springboot實現(xiàn)修改請求狀態(tài)404改為200方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07
Spring的@Scheduled 如何動態(tài)更新cron表達式
這篇文章主要介紹了Spring的@Scheduled 如何動態(tài)更新cron表達式的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
SpringBoot @ModelAttribute使用場景分析
這篇文章主要介紹了SpringBoot @ModelAttribute使用場景分析,文中通過實例代碼圖文相結合給大家介紹的非常詳細,需要的朋友可以參考下2021-08-08

