请求映射(handlerMapping)以及原理

请求映射原理

也就是说我们每次发请求,它到底是怎么找到我们哪个方法来去处理这个请求,因为我们知道所有的请求过来都会来到DispatcherServletspringboot底层还是使用的是springMVC所以springMVCDispatcherServlet是处理所以请求的开始,他的整个请求处理方法是,我们来找一下:

DispatcherServlet说起来也是一个servlet它继承FrameworkServlet又继承于HttpServletBean又继承于HttpServlet。说明DispatcherServlet是一个HttpServlet,继承于Servlet必须重写doGetdoPost之类的方法Ctril + F12(打开HttpServlet整个结构)我们发现这里没有doGet()doSet()方法,那说明子类里面有没有重写。它的继承树是:

DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet

我们原生的Servlet本来有doGetdoPost方法,但是我们发现在HttpServletBean里面没有找到,那就在FrameworkServlet里面有没有,Ctril + F12看看有没有doGetdoPostprotected final void doGet()是继承了HttpServletBean。在FrameworkServlet我们发现无论是doGet还是doPost最终都是调用我们本类的processRequest说明我们请求处理一开始我们HttpServletdoGet最终会调用到我们FrameworkServlet里面的processRequest

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = this.buildLocaleContext(request);
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
    this.initContextHolders(request, localeContext, requestAttributes);
    //这些都是初始化过程

    try {
        this.doService(request, response);
    } catch (IOException | ServletException var16) {
        failureCause = var16;
        throw var16;
    } catch (Throwable var17) {
        failureCause = var17;
        throw new NestedServletException("Request processing failed", var17);
    } finally {
        this.resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }

        this.logResult(request, response, (Throwable)failureCause, asyncManager);
        this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
    }

}

processRequest -> doService()

我们尝试执行一个doService()方法,执行完后都是一些清理过程(catch)

protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;

它由于是一个抽象方法(abstract),他也没有重写和实现,只能来到子类(DispatcherServlet)来到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);
            }

        }

    }

也就是说最终DispatcherServlet里面对doService()进行了实现

只要看到getset都是在里面放东西进行初始化的过程

我们一连串请求一进来应该是调HttpServlet的doGet,在FrameworkServlet重写了 唯一有效语句是抽象类doService()方法,而这个方法在DispatcherServlet类中实现

我们看核心的方法调用:

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

finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
                this.restoreAttributesAfterInclude(request, attributesSnapshot);
            }

            if (this.parseRequestPath) {
                ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
            }

        }

意思是把我们doDispatch()请求做派发,看看doDispatch():

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 {
                processedRequest = this.checkMultipart(request);//检查我们是否有文件上传请求
                multipartRequestParsed = processedRequest != request;//如果是文件上传请求它在这进行一个转化
                // Determine handler for the current request.	就是我们来决定哪个handler(Controller)能处理当前请求(current)
                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);
        }

    }
}

我们发现这里才是真正有功能的方法

 processedRequest = this.checkMultipart(request);  //checkMultipart():检查文件上传

doDispatch()才是我们DispatcherServlet里面最终要研究的方法,每一个请求进来都要调用doDispatch方法


我们打上断点,来看整个请求处理, 包括 它是怎么找到我们每一个请求要调用谁来处理的

如果我来发送请求(登录(localhost:8080))放行,知道页面出来后我们点击REST-GET请求,我们来看,protected void doDispatch(HttpServletRequest request, HttpServletResponse response)传入原生的requestresponse;我们点进request里面我们发现我们整个请求的路径(coyoteRequest)是/user,请求的详细信息都在这(coyoteRequest),路劲(decodeUriMB)是/user。我们接下开看要用谁调用的。

HttpServletRequest processedRequest = request;相当于把原生的请求(request)拿过来包装一下(processedRequest)。

这有个HandlerExecutionChain mappedHandler = null;执行量我们后来再说

然后继续,说multipartRequestParsed是不是一个文件上传请求,默认是false,然后包括我们整个请求期间有没有异步(getAsyncManager()),如果有异步使用异步管理器(WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);)暂时在这块我们不用管。

注意:只有一个ModelAndView mv = null;Object dispatchException = null;这些都是空初始化的一些数据。加下来看我们第一个有功能的一块:在doDispatch()里面第一个有功能的叫checkMultipart(检查我们是否有文件上传请求,响应文件上传再说)如果是文件上传请求它在这进行一个转化(multipartRequestParsed = processedRequest != request;)注意这有一个// Determine handler for the current request.,就是我们来决定哪个handler能处理当前请求(current)我们放行(mappedHandler = this.getHandler(processedRequest);)mappedHandler就会看到:它直接给我们找到了HelloCOntrollergetUser()方法来处理这个请求

神奇的地方就在这个(mappedHandler = this.getHandler(processedRequest);)它到底是怎么找到我当前的/User请求会要调用那个方法进行处理的。

我们从HttpServletRequest processedRequest = request;直接放行到mappedHandler = this.getHandler(processedRequest);,getHandler他要依据当前请求(processedRequest)当前请求里面肯定有哪个url地址这是http传过来的不用管(Step into)进来,这里有一个东西叫handlerMappings也就是获取到所有的(这个handlerMappings有五个)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oZM3UdIV-1637476541444)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211120174611768.png)]

1、handlerMapping:处理器映射

也就是说我们springMVC怎么知道哪个请求要用谁处理,是根据处理器里面的映射规则。也就是说:

/xxx请求 -> xxx处理都有这映射规则,而这些规则都被保存到handlerMapping里面,如上图所示,我们现在有5个handlerMapping,其中有几个handlerMapping大家可能有点熟悉,比如:WelcomePageHandlerMapping(欢迎页的处理请求),我们之前说资源管理规则的时候我们发现我们springMVC自动的会给容器中放一个欢迎页的handlerMapping然后这个handlerMapping()里面也有保存规则,保存什么规则?就是我们所有的index请求你的比如这个PathMatcher(路劲匹配)我们这个/(当前项目下的’/‘你直接访问这个)我给你访问到哪?我们的’/‘会直接rootHandler下的View路劲(这里是等于到了’index’)所以我们首页要访问到的是我们这个WelcomePageHandlerMapping里面保存了一个规则,所以就有首页的访问了。

我们加下来还有一个RequestMappingHandlerMapping


2、RequestMappingHandlerMapping

我们以前有一个注解叫@RequestMapping相当于是@RequestMapping注解的所有处理器映射,也就是说这个东西(RequestMappingHandlerMapping)里面保存了所有@RequestMappinghandler的规则。而它又是怎么保存的,那其实是我们的应用一启动springMVC自动扫描我们所有的Controller并解析注解,把你的这些注解信息全部保存到HandlerMapping里面。所以它会在这五个handlerMapping里面(大家注意增强for循环)挨个找我们所有的请求映射,看谁能处理这个请求,我们找到第一个handlerMapping相当于我们的RequestMappingHandlerMapping,它里面保存了哪些映射信息,有个mappingRegistry(相当于我们映射的注册中心)这个中心里面打开你就会发现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C0s06BZ5-1637476541446)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211120181934966.png)]

我们当前项目里写的所有的路径它在这个都有映射:POST[/user]是哪个Controller哪个方法处理的,包括系统自带的/error它是哪个Controller哪个方法处理的

相当于是我们RequestMappingHandlerMapping保存了我们当前系统我们每一个自己写的类,每一个类每一个方法都能处理什么请求。我们当前请求/user,我们遍历到第一个HandlerMapping的时候相当于它的注册中心(mappingRegistry)里面就能找到/user是谁来处理,所以最终在这决定是在HelloController#getUser来处理的。所以它在这(HandlerExecutionChain handler = mapping.getHandler(request);)所以它在mapping里面getHandler(mapping.getHandler);从我们的HandlerMapping里面获取(getHandler())我们的handler就是处理器,点进getHandler()

@Override
	@Nullable
	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}

		// Ensure presence of cached lookupPath for interceptors and others
		if (!ServletRequestPathUtils.hasCachedPath(request)) {
			initLookupPath(request);
		}

		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		if (logger.isTraceEnabled()) {
			logger.trace("Mapped to " + handler);
		}
		else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
			logger.debug("Mapped to " + executionChain.getHandler());
		}

		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = getCorsConfiguration(handler, request);
			if (getCorsConfigurationSource() != null) {
				CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
				config = (globalConfig != null ? globalConfig.combine(config) : config);
			}
			if (config != null) {
				config.validateAllowCredentials();
			}
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}

getHandlerInterna()我们来获取,怎么获取(step into):

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
		try {
			return super.getHandlerInternal(request);
		}
		finally {
			ProducesRequestCondition.clearMediaTypesAttribute(request);
		}
	}

step intogetHandlerInternal():

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		String lookupPath = initLookupPath(request);
		this.mappingRegistry.acquireReadLock();//拿到一把锁
		try {
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

它先拿到request原生请求,我们现在想要访问的路劲(lookupPath)我们想要访问的路劲是/user然后带着这个路劲,它还拿到一把锁(acquireReadLock())害怕我们并发查询我们这个mappingRegistry这个mappingRegistry也看到了是我们这个RequestMappingHandlerMapping,handlerMapping里面的一个属性mappingRegistry它里面保存了我们所有请求调用哪个方法处理

所以它最终相当于是在我们当前请求(request)这个路劲(lookupPath)到底谁来处理(handlerMethod)

(step into):

@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {//没找到
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);//添加一些空的东西
		}
		if (!matches.isEmpty()) {
			Match bestMatch = matches.get(0);//它把它找到的你里面的第一个拿过来
			if (matches.size() > 1) {
				Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
				matches.sort(comparator);
				bestMatch = matches.get(0);
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				if (CorsUtils.isPreFlightRequest(request)) {
					for (Match match : matches) {
						if (match.hasCorsConfig()) {
							return PREFLIGHT_AMBIGUOUS_MATCH;
						}
					}
				}
				else {
					Match secondBestMatch = matches.get(1);
					if (comparator.compare(bestMatch, secondBestMatch) == 0) {
						Method m1 = bestMatch.getHandlerMethod().getMethod();
						Method m2 = secondBestMatch.getHandlerMethod().getMethod();
						String uri = request.getRequestURI();
						throw new IllegalStateException(
								"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
					}
				}
			}
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.getHandlerMethod();
		}
		else {
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		}
	}

形参:lookupPath(当前我们要找的路劲);request(我们原生的请求),接下来就在下面开始找。从我们这个Registry(mappingRegistry)里面,使用我们这个路劲(getMappingsByDirectPath(lookupPath))然后去找谁能处理;问题就是在于我们这个Registry(RequestMappingHandlerMapping)里面他的路劲光靠/user请求其实有四个人的路劲都是这样只是请求方式不对。我们来看是怎么找:

先是根据url(getMappingsByDirectPath()视频里是getMappingByUrl())来找,按照前面的来能找到4个(directPathMatches (+ArraysList@7307 size=4))(GETPOSTPUTDELETE方式的user)找到了以后接下来它把所有找到的添加到我们这个匹配的集合里面(addMatchingMappings(directPathMatches, matches, request);),如果没找到(if (matches.isEmpty()))它就添加一些空的东西(addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);),如果找到了还不为空(if (!matches.isEmpty()))接下来它把它找到的你里面的第一个拿过来(Match bestMatch = matches.get(0);)如果它同时找到了很多他就认为第一个是最匹配的,而且大家注意: 如果我们现在的matches.size()大于1(if (matches.size() > 1))也就是相当于我们找到了非常多的matches;注意:这个matches在这(addMatchingMappings(directPathMatches, matches, request);),在这一块(directPathMatches)找到了四个,它(matches)把这四个调用addMatchingMappings()这个方法获取到能匹配的集合里面(matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping))););addMatchingMappings()肯定以请求方式匹配好了

addMatchingMappings()

private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
		for (T mapping : mappings) {
			T match = getMatchingMapping(mapping, request);
			if (match != null) {
				matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
			}
		}
	}

所以最终给我们留下我们最佳匹配的这个matches里面集合里面只有一个,他(bestMatch = matches.get(0);)就拿到这个,所以最终给我们留下一个我们最佳匹配的我们这个matches(里面只有一个),然后它(matches.get(0);)就拿到这个(matches),这个matches(matches.get(0);)已经得到最佳匹配的了,如果你写了多个方法同时都能处理/GET请求,那你的这个matches就能大于1(if (matches.size() > 1))大于1后(if里面)各种排序排完以后在这(Match secondBestMatch = matches.get(1);)最后给你测试,把你能匹配的一俩个全都拿来进行对比,最终比完后给你抱一个错说:throw new IllegalStateException( "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");(相当于我们这个handler你能处理这个uri (路径)有俩个方法都能处理)说明就会抛出异常(IllegalStateException),所以springMVC要求我们:同样的一个请求方式不能有多个方法同时能处理,只能有一个。

springMVC要求我们:同样的一个请求方式不能有多个方法同时能处理,只能有一个(原因在上面一段)

最终我们就找到我们最佳匹配规则(GET方式的user)能匹配,而GET方式的User是Controller#getUser()方法

所以简单总结起来就是一句话:怎么知道哪个请求谁能处理?所有的请求映射都保存在了``HandlerMapping`中。

我们springboot一启动给我们配置了welcomePageHandlerMapping(欢迎页的handlerMapping)

  • springboot自动配置欢迎页的handlerMaping。访问’/'能访问到’index’页面
  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
    • 如果有就找个请求对象的handler
    • 如果没有就是下一个HandlerMapping

总结:(这里以欢迎页为例)localhost:8080/进入到首页,我们请求一进来(HttpServletRequest processedRequest = request;)它来找(mappedHandler = getHandler(processedRequest);(当前请求访问的是/的请求))看谁能处理(getHandler())接下来就进入了遍历循环(forEach)所有HandlerMapping的时候了,我们先来找到第一个handlerMapping(HandlerExecutionChain handler = mapping.getHandler(request);)第一个HandlerMapping由于我们这个里面映射只保存了相当于是我们自己Controller写的这个路径映射没人能处理,所以如果在第一个里面找,你找到的handler肯定是空的,找到是空的,所以继续for循环;再次for循环来到第二个handlerMappingWelcomePageHandlerMapping,它正好处理的路径就是’/'所以我们现在找,就找到handler了,而这个handler是什么?就是我们springMVC里面默认处理我们’index’页面,人家给我们的ViewController访问我们的’index’页面就有人了。这就是handlerMapping的匹配规则。所有的handleMapping全部来进行匹配谁能匹配用谁的

springboot帮我们配置了哪些HandlerMapping

我们来到webMvcAutoCOnfig看看有没有跟HandlerMapping有关的:

首先第一个:

  • RequestMappingHandlerMapping
    • 相当于我们容器中(@Bean),我们第一个HandlerMaping是我们springboot给我们容器放的默认的。 这个组件就是来解析我们当前所有的方法(Controller里的方法)标了@RequestMapping注解(GET PUT都一样)标了这些注解的时候它(RequestMappingHandlerMapping)整的。
  • WelcomePageHandlerMapping
    • 欢迎页的HandlerMapping
  • 还有我们系统兼容的BeanNameUrlHandlerMappingRouterFunctionMappingSimpleUrlHandlerMapping
  • 一句话: 我们需要自定义的映射处理,我们也可以自己在容器中放HandlerMapping(就是来保存一个请求谁来处理,甚至于是发一个(我们经常自定义handlerMappingapi/v1/userapi/v2/user(v2版本获取用户)不一样,v1版本调用哪个v2版本调用哪个)这就可能不止止是Controller的变化,我们希望能自定义handlerMapping的规则。如果是v1版本,所有的请求,比如去哪个包里找;如果是v2版本给我去哪个包里找。这样就会非常方便)自定义handlerMapping
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

结城明日奈是我老婆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值