關於Spring中的useSuffixPatternMatch

eaglelihh 發表於 2022-05-15
Spring

背景

spring-boot的版本是2.1.4.RELEASE,spring的版本是5.1.6.RELEASE

一個例子如下:

@Configuration
@Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)
@SuppressWarnings("unchecked")
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new RequestMappingHandlerMapping();
    }
}

@RestController
public class ParamController {
    @GetMapping(value = "/param/{param1}")
    public String param(@PathVariable("param1") String param1) {
        return param1;
    }
}

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

啟動一下,訪問http://127.0.0.1:8080/param/hehehttp://127.0.0.1:8080/param/hehe.hehe都返回hehe

如果訪問http://127.0.0.1:8080/param/hehe.hehe.hehe,它會返回hehe.hehe

所以會發現它把最後一個小數點後面的字元給截掉了,那如果我們想要獲取完整的字串,該怎麼辦呢?

探索

  1. 引數怎麼來的

入口在InvocableHandlerMethod.invokeForRequest,如下:
關於Spring中的useSuffixPatternMatch

是根據PathVariableMethodArgumentResolver.resolveName得來的,如下:
關於Spring中的useSuffixPatternMatch
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";

什麼時候放進attributes裡的?,在RequestMappingInfoHandlerMapping.handleMatch,如下:
關於Spring中的useSuffixPatternMatch

  1. 引數怎麼解析的
程式定義的:/param/{param1} -> /param/{param1}.*
介面傳過來的:/param/hehe.hehe

以/分割,第一個字串param裡沒有引數,所以會跳過,直接看第二個字串:
{param1}.* -> pattern=(.*)\Q.\E.*
hehe.hehe

param1=hehe

我們定義的是/param/{param1},怎麼就變成了/param/{param1}.*?在PatternsRequestCondition.getMatchingPattern
關於Spring中的useSuffixPatternMatch
可以看到如果useSuffixPatternMatch為true,並且指定的url裡沒有.,會在字尾自動增加.*

  1. useSuffixPatternMatch是在哪裡設定的?
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements MatchableHandlerMapping, EmbeddedValueResolverAware {

	private boolean useSuffixPatternMatch = true;

	@Override
	public void afterPropertiesSet() {
		this.config = new RequestMappingInfo.BuilderConfiguration();
		this.config.setUrlPathHelper(getUrlPathHelper());
		this.config.setPathMatcher(getPathMatcher());
		this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); // 這裡設定了
		this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
		this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
		this.config.setContentNegotiationManager(getContentNegotiationManager());

		super.afterPropertiesSet();
	}
}

解決

  1. 修改WebConfiggetRequestMappingHandlerMapping
@Configuration
@Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)
@SuppressWarnings("unchecked")
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        RequestMappingHandlerMapping requestMappingHandlerMapping = new RequestMappingHandlerMapping();
        requestMappingHandlerMapping.setUseSuffixPatternMatch(false);
        return requestMappingHandlerMapping;
    }
}
  1. 增加configurePathMatch方法
@Configuration
@Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)
@SuppressWarnings("unchecked")
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new RequestMappingHandlerMapping();
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseSuffixPatternMatch(false);
    }
}
  1. url中增加點

a. 中間的引數是不受影響的

@RestController
public class ParamController {
    @GetMapping(value = "/param/{param1}/{param2}")
    public String param(@PathVariable("param1") String param1, @PathVariable("param2") String param2) {
        return param1 + " " + param2;
    }
}

訪問http://127.0.0.1:8080/param/hehe.hehe/hehe.hee返回hehe.hehe hehe

b. 增加點

@RestController
public class ParamController {
    //@GetMapping(value = "/param/{param1}")
    //public String param(@PathVariable("param1") String param1) {
    //    return param1;
    //}

    @GetMapping(value = "/param/{param1}.{param2}")
    public String param(@PathVariable("param1") String param1, @PathVariable("param2") String param2) {
        return param1 + " " + param2;
    }
}

訪問http://127.0.0.1:8080/param/hehe.hehe返回hehe hehe

注意第一個方法和第二個方法不要同時出現,如果同時出現的話,則會訪問第一個方法

@RestController
public class ParamController {
    @GetMapping(value = "/param/{param1}")
    public String param(@PathVariable("param1") String param1) {
        return param1;
    }

    @GetMapping(value = "/param/{param1}.{param2}")
    public String param(@PathVariable("param1") String param1, @PathVariable("param2") String param2) {
        return param1 + " " + param2;
    }
}

訪問http://127.0.0.1:8080/param/hehe.heh返回hehe

  1. 修改引數
@RestController
public class ParamController {
    @GetMapping(value = "/param/{param1:.+}")
    public String param(@PathVariable("param1") String param1) {
        return param1;
    }
}

訪問http://127.0.0.1:8080/param/hehe.hehe,返回hehe.hehe

原因如下:

/param/{param1:.+} -> /param/{param1:.+}
/param/hehe.hehe

{param1:.+} -> pattern=(.+)
hehe.hehe

param1=hehe.hehe
得到pattern以及提取引數的類 AntPathStringMatcher
	protected static class AntPathStringMatcher {

		private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");

		private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";

		private final Pattern pattern;

		private final List<String> variableNames = new LinkedList<>();

		public AntPathStringMatcher(String pattern) {
			this(pattern, true);
		}

		public AntPathStringMatcher(String pattern, boolean caseSensitive) {
			StringBuilder patternBuilder = new StringBuilder();
			Matcher matcher = GLOB_PATTERN.matcher(pattern);
			int end = 0;
			while (matcher.find()) {
				patternBuilder.append(quote(pattern, end, matcher.start()));
				String match = matcher.group();
				if ("?".equals(match)) {
					patternBuilder.append('.');
				}
				else if ("*".equals(match)) {
					patternBuilder.append(".*");
				}
				else if (match.startsWith("{") && match.endsWith("}")) {
					int colonIdx = match.indexOf(':');
					if (colonIdx == -1) {
						patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
						this.variableNames.add(matcher.group(1));
					}
					else {
						String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
						patternBuilder.append('(');
						patternBuilder.append(variablePattern);
						patternBuilder.append(')');
						String variableName = match.substring(1, colonIdx);
						this.variableNames.add(variableName);
					}
				}
				end = matcher.end();
			}
			patternBuilder.append(quote(pattern, end, pattern.length()));
			this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) :
					Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE));
		}

		private String quote(String s, int start, int end) {
			if (start == end) {
				return "";
			}
			return Pattern.quote(s.substring(start, end));
		}

		/**
		 * Main entry point.
		 * @return {@code true} if the string matches against the pattern, or {@code false} otherwise.
		 */
		public boolean matchStrings(String str, @Nullable Map<String, String> uriTemplateVariables) {
			Matcher matcher = this.pattern.matcher(str);
			if (matcher.matches()) {
				if (uriTemplateVariables != null) {
					// SPR-8455
					if (this.variableNames.size() != matcher.groupCount()) {
						throw new IllegalArgumentException("The number of capturing groups in the pattern segment " +
								this.pattern + " does not match the number of URI template variables it defines, " +
								"which can occur if capturing groups are used in a URI template regex. " +
								"Use non-capturing groups instead.");
					}
					for (int i = 1; i <= matcher.groupCount(); i++) {
						String name = this.variableNames.get(i - 1);
						String value = matcher.group(i);
						uriTemplateVariables.put(name, value);
					}
				}
				return true;
			}
			else {
				return false;
			}
		}
	}