Trying to create REST-ful URLs with multiple dots

2019-01-11 04:13发布

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 optionally allow) file extension on the end of the URL (but assume HTML content type if no extension). This works out-of-the-box with Spring MVC as long as there are no dots (period/full-stop) in the filename part.

However some of my URLs require an identifier with dots in the name. E.g. like this:

http://company.com/widgets/123.456.789.500

In this case Spring looks for a content type for the extension .500 and finds none so errors. I can use work-arounds like adding .html to the end, encoding the identifier or adding a trailing slash. I'm not happy with any if these but could probably live with adding .html.

I've unsuccessfully looked for a way of overriding the default file extension detection in Spring.

Is it possible to customize or disable file extension detection for a given controller method or URL pattern, etc?

7条回答
倾城 Initia
2楼-- · 2019-01-11 04:44

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

查看更多
可以哭但决不认输i
3楼-- · 2019-01-11 04:46

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" />
查看更多
可以哭但决不认输i
4楼-- · 2019-01-11 04:49

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)

查看更多
乱世女痞
5楼-- · 2019-01-11 04:51

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);
    }
}
查看更多
够拽才男人
6楼-- · 2019-01-11 04:53

I had the same problem and I also solved it by custom PathMatcher. My solution is somewhat simplier to what axtavt proposed. My PathMatcher also has a private final AntPathMatcher target, and it delegates all calls to it unchanged, except for match() method:

@Override
public boolean match(String pattern, String path) {
    return pattern.endsWith(".*") ? false : target.match(pattern, path);
}

This works because Spring tries to match controllers by adding "." to the end. For example, with path mapping "/widgets/{id}" and URL "/widgets/1.2.3.4", Spring tries first match againts "/widgets/{id}." and after that "/widgets/{id}". The first will match, but it leaves only "1.2.3" for id.

My PatchMatcher specifically rejects patterns ending ".*", thus the first attempt fails and the second matches.

If you are using ContentNegotiatingViewResolver you can still specify content type in URL's using request parameter "format" (if the favorParameter is set to true).

-jarppe

查看更多
戒情不戒烟
7楼-- · 2019-01-11 04:54
<!-- 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

查看更多
登录 后发表回答