遇到的一个URL解析问题,以及解决方案
问题抛出
如图中的HTTP接口,如果客户端通过/test/next/访问的时候,会进入到哪个处理方法中呢?
答案是会走入第一个接口,springmvc会把next当做第一个接口的参数传进去,虽然在接口设计的时候可以通过参数校验或者数据校验来确保接口的功能正确,不过这种乌龙请求,springmvc还是给我们提供了一些解决方案。
源码解析
org.springframework.web.servlet.DispatcherServlet#doDispatch
这个方法是处理HTTP请求转发的,所有请求到server的都会先经过这个方法。在springmvc启动的时候,会将有controller注解(或者restController)下的method,都保存为他们成为handler的东西,
在寻找具体的handler方法,就是这个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 { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } ......省略
/** * Return the HandlerExecutionChain for this request. * <p>Tries all handler mappings in order. * @param request current HTTP request * @return the HandlerExecutionChain, or {@code null} if no handler could be found */ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; }
debug进入到最核心的方法org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal
这个方法主要是获取锁,然后进入到lookupHandlerMethod,看名字就知道是最重要的方法了。
// Handler method lookup /** * Look up a handler method for the given request. */ @Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); if (logger.isDebugEnabled()) { logger.debug("Looking up handler method for path " + lookupPath); } this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); if (logger.isDebugEnabled()) { if (handlerMethod != null) { logger.debug("Returning handler method [" + handlerMethod + "]"); } else { logger.debug("Did not find handler method for [" + lookupPath + "]"); } } return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { this.mappingRegistry.releaseReadLock(); } }
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
这个方法的总体设计分为两步,最重要的2步。首先通过url直接获取method,如果获取不到就go through all。
因为直接请求/test/next/,获取不到对应的handler,所以他go through all的时候,匹配的pattern匹配到/test/{parameter}这个接口,所以它走了乌龙请求。
/** * Look up the best-matching handler method for the current request. * If multiple matches are found, the best match is selected. * @param lookupPath mapping lookup path within the current servlet mapping * @param request the current request * @return the best-matching handler method, or {@code null} if no match * @see #handleMatch(Object, String, HttpServletRequest) * @see #handleNoMatch(Set, String, HttpServletRequest) */ protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<Match>(); List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { // No choice but to go through all mappings... addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } 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); } }
还有一个重要方法 org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingPattern
这里标志了两个配置,一个是是否使用后缀匹配,一个是是否支持'/'结尾的请求
private String getMatchingPattern(String pattern, String lookupPath) { if (pattern.equals(lookupPath)) { return pattern; } if (this.useSuffixPatternMatch) { if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) { for (String extension : this.fileExtensions) { if (this.pathMatcher.match(pattern + extension, lookupPath)) { return pattern + extension; } } } else { boolean hasSuffix = pattern.indexOf('.') != -1; if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) { return pattern + ".*"; } } } if (this.pathMatcher.match(pattern, lookupPath)) { return pattern; } if (this.useTrailingSlashMatch) { if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) { return pattern +"/"; } } return null; }
解决方案
由于它拿不到直接映射而导致的遍历,那么就给他添加一个直接映射就可以了,这是最优的解决方案。
有了这个映射缓存,在第一步获取直接映射的时候,就拿到了这个handler,不会走遍历了,这样就能准确识别接口了。
第二种解决方案,就是禁用‘/’结尾的请求,这种方式会让/test/next/请求变成404,SpringBoot下继承这个类,它提供了mvc的基本配置。
@Configuration public class SpnWebMvcConfigurer extends WebMvcConfigurerAdapter { @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setUseTrailingSlashMatch(false); } }