Spring
的Web
开发过程中,我们往往使用@PathVariable
注解来获取URL
上指定的值。例如:
@Controller
public class MyController {
@GetMapping("/hello/{name}")
@ResponseBody
public String hello(@PathVariable("name") String name) {
return Math.random() + "name: " + name;
}
}
访问对应的路径进行测试:
可见,程序自动识别到了我们的name变量,并将其值赋值给了对应的变量,然后输出到页面中。但是,假如我们对这个变量添加一个特殊字符,看看效果会怎么样。
第一种:http://localhost:8080/hello/ljj/
,结果如下:
第二种:http://localhost:8080/hello/ljj/hello
,结果如下:
Spring
中,对于URL
的解析工作,交给AbstractHandlerMethodMapping.lookupHandlerMethod()
来完成:
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
// 1.根据URL进行精确匹配
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
// 做一个缓存记录
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// 遍历所有映射,根据请求来进行模糊匹配
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
// 如果匹配结果有多个,那么可以进行筛选
if (matches.size() > 1) {
// ...
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
// 匹配不上,报错
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
}
对应的代码:
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
此时我们的lookupPath
就是我们的URL
,例如:
我们跟进去这个函数:
@Nullable
public List<T> getMappingsByUrl(String urlPath) {
return this.urlLookup.get(urlPath);
}
此时this.urlLookup
中并没有我们访问的URL
路径:
那么此时精确匹配出来的结果就是null
,因此根据代码逻辑,需要在进行一次模糊匹配过程。
这段逻辑对应代码:
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
我们先来看下getMappings()
里面的东西是啥:
首先明确的是,我们的/hello/{name}
已经在待匹配的候选列表中了。那么具体是如何匹配的呢?我们跟进代码:
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.getMappings().get(mapping)));
}
}
}
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
if (methods == null) {
return null;
}
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
if (params == null) {
return null;
}
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
if (headers == null) {
return null;
}
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
if (consumes == null) {
return null;
}
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (produces == null) {
return null;
}
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {
return null;
}
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
}
到这里我们可以看到,具体匹配的时候,会查到当前请求的所有相关信息,例如:请求头、参数、请求类型等。如果有一项不匹配,就直接提前返回null
。当然,还会匹配这个请求的Pattern
。以我们的案例http://localhost:8080/hello/ljj/
为例:
那么也就是说,最终的匹配肯定看的是下述这段代码是否能够成功匹配到:
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
那么如果我们访问http://localhost:8080/hello/ljj/hello
,将会在patterns
的校验上提前返回:
那么为何ljj/
就可以正常访问呢?我们跟进下getMatchingCondition
函数,最终会走到
public class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
private String getMatchingPattern(String pattern, String lookupPath) {
// ..
if (this.useTrailingSlashMatch) {
if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
return pattern + "/";
}
}
return null;
}
}
说白了,Spring
会尝试在URL
的末尾加一个 /
然后在进行匹配,如果能匹配上,在最终返回 Pattern
时就隐式自动上一个加 /
。
既然Spring
中的URL
匹配,是根据Pattern
来进行的,那么我们可以使用 *
去进行匹配。然后手动获取URL,通过split去进行分割取值。 同时要考虑到前缀重复的情况(即name
的值依旧包含了相同的前缀)。
@Controller
public class MyController {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@GetMapping("/hello/**")
@ResponseBody
public String hello(HttpServletRequest request) {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String matchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
String res = antPathMatcher.extractPathWithinPattern(matchPattern, path);
return Math.random() + ",name: " + res;
}
}
结果如下:
当然,最好的解决方案就是在使用URL上进行动态传承的同时,避免参数值带有 /
这样的特殊字符。