I\'m using Spring MVC (3.0) with annotation-driven controllers. I would like to create REST-ful URLs for resources and be able to not require (but still opt
JFY: in Spring 4 this issue is fixed via: WebMvcConfigurerAdapter.
@Configuration
class MvcConfiguration extends WebMvcConfigurerAdapter {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
}
}
Or via WebMvcConfigurationSupport like here.
and in Spring 5:
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
}
}
The @PathVariable
pattern matching is a bit twitchy when it comes to dots in the URL (see SPR-5778). You can make it less twitchy (but more picky), and get better control over dot-heavy URLs, by setting the useDefaultSuffixPattern property on DefaultAnnotationHandlerMapping to false
.
If you haven't already explicitly declared a DefaultAnnotationHandlerMapping
in your context (and most people don't since it's declared implicitly for you), then you can add it explicitly, and set that property.
Probably, it's an ugly hack, I just wanted to explore extensibility of Spring @MVC. Here is a customized PathMatcher
. It uses $
in the pattern as the end marker - if pattern ends with it, marker is removed and pattern is matched by the default matcher, but if pattern has $
in the middle (e.g. ...$.*
), such a pattern is not matched.
public class CustomPathMatcher implements PathMatcher {
private PathMatcher target;
public CustomPathMatcher() {
target = new AntPathMatcher();
}
public String combine(String pattern1, String pattern2) {
return target.combine(pattern1, pattern2);
}
public String extractPathWithinPattern(String pattern, String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return "";
}
return target.extractPathWithinPattern(pattern, path);
}
public Map<String, String> extractUriTemplateVariables(String pattern,
String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return Collections.emptyMap();
}
return target.extractUriTemplateVariables(pattern, path);
}
public Comparator<String> getPatternComparator(String pattern) {
final Comparator<String> targetComparator = target.getPatternComparator(pattern);
return new Comparator<String>() {
public int compare(String o1, String o2) {
if (isEncoded(o1)) {
if (isEncoded(o2)) {
return 0;
} else {
return -1;
}
} else if (isEncoded(o2)) {
return 1;
}
return targetComparator.compare(o1, o2);
}
};
}
public boolean isPattern(String pattern) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return true;
}
return target.isPattern(pattern);
}
public boolean match(String pattern, String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return false;
}
return target.match(pattern, path);
}
public boolean matchStart(String pattern, String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return false;
}
return target.match(pattern, path);
}
private boolean isEncoded(String pattern) {
return pattern != null && pattern.contains("$");
}
private String resolvePattern(String pattern) {
int i = pattern.indexOf('$');
if (i < 0) return pattern;
else if (i == pattern.length() - 1) {
return pattern.substring(0, i);
} else {
String tail = pattern.substring(i + 1);
if (tail.startsWith(".")) return null;
else return pattern.substring(0, i) + tail;
}
}
}
Config:
<bean id = "pathMatcher" class = "sample.CustomPathMatcher" />
<bean class = "org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name = "pathMatcher" ref="pathMatcher" />
</bean>
<bean class = "org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name = "pathMatcher" ref="pathMatcher" />
</bean>
And usage (given "/hello/1.2.3", value
is "1.2.3"):
@RequestMapping(value = "/hello/{value}$", method = RequestMethod.GET)
public String hello(@PathVariable("value") String value, ModelMap model)
EDIT:: Now doesn't break "trailing slash doesn't matter" rule
<!-- language: lang-java -->
@Controller
public class MyController {
@RequestMapping(value="/widgets/{preDot}.{postDot}")
public void getResource(@PathVariable String preDot, @PathVariable String postDot) {
String fullPath = preDot + "." + postDot;
//...
}
}
// Above code should match /widgets/111.222.333.444
To add to skaffman's answer, if you're using <mvc:annotation-driven/>
and you want to override the useDefaultSuffixPattern value, you can replace the <mvc:annotation-driven>
tag with the following:
<!-- Maps requests to @Controllers based on @RequestMapping("path") annotation values -->
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="order" value="1" />
<property name="useDefaultSuffixPattern" value="false" />
</bean>
<!-- Enables annotated @Controllers -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
Spring 3.2 has changed, and suggests that you set properties on the RequestMappingHandlerMapping
bean, either explicitly (if not using the mvc namespace) or by using a BeanPostProcessor such as the following (you'll need to scan or instantiate it):
@Component
public class IncludeExtensionsInRequestParamPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerMapping) {
RequestMappingHandlerMapping mapping = (RequestMappingHandlerMapping)bean;
mapping.setUseRegisteredSuffixPatternMatch(false);
mapping.setUseSuffixPatternMatch(false);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) { return bean; }
}
You can also just append :.*
to your @RequestMapping, e.g. "/{documentPath:.*}"
(see JIRA comment)