Попытка создания URL-адресов REST-ful с несколькими точками в части "filename" - Spring 3.0 MVC

Я использую Spring MVC (3.0) с управляемыми аннотациями контроллерами. Я хотел бы создать URL-адреса REST-ful для ресурсов и иметь возможность не требовать (но все же необязательно) расширение файла в конце URL-адреса (но предположим, что тип содержимого HTML не имеет расширения). Это работает из коробки с Spring MVC, если в части имени файла нет точек (период/полная остановка).

Однако некоторые из моих URL-адресов требуют идентификатора с точками в имени. Например. например:

http://company.com/widgets/123.456.789.500

В этом случае Spring ищет тип контента для расширения .500 и не находит таких ошибок. Я могу использовать обходы, такие как добавление .html в конец, кодирование идентификатора или добавление конечной косой черты. Я не доволен, если бы они были, но могли бы жить с добавлением .html.

Я безуспешно искал способ переопределить обнаружение расширения файла по умолчанию в Spring.

Можно ли настроить или отключить обнаружение расширения файла для данного метода контроллера или шаблона URL и т.д.?

Ответы

Ответ 1

Соответствие шаблону @PathVariable немного дергается, когда дело касается точек в URL-адресе (см. SPR-5778). Вы можете сделать его менее судорожным (но более придирчивым) и получить лучший контроль над URL-адресами с большой точкой, установив свойство useDefaultSuffixPattern на DefaultAnnotationHandlerMapping до false.

Если вы еще не объявили явный текст DefaultAnnotationHandlerMapping в своем контексте (и большинство людей этого не делает, так как оно объявлено вам неявно), вы можете явно добавить его и установить это свойство.

Ответ 2

Возможно, это уродливый взлом, я просто хотел изучить расширяемость Spring @MVC. Вот настраиваемый PathMatcher. Он использует $ в шаблоне в качестве конечного маркера - если шаблон заканчивается с ним, маркер удаляется, а шаблон сопоставляется по умолчанию, но если шаблон имеет $ в середине (например, ...$.*), шаблон не сопоставляется.

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>

И использование (с учетом "/hello/1.2.3", value равно "1.2.3" ):

@RequestMapping(value = "/hello/{value}$", method = RequestMethod.GET)
public String hello(@PathVariable("value") String value, ModelMap model)

РЕДАКТИРОВАТЬ:: теперь не нарушается правило "trailing slash does not matter"

Ответ 3

<!-- 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; //... } }

//Над кодом должен соответствовать/widgets/111.222.333.444

Ответ 4

Spring 3.2 изменилось и предлагает установить свойства в RequestMappingHandlerMapping bean либо явно (если не использовать пространство имен mvc) или с помощью BeanPostProcessor, например, следующего (вам нужно будет сканировать или создать его экземпляр):

@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; }
}

Вы также можете просто добавить :.* к вашему @RequestMapping, например. "/{documentPath:.*}" (см. комментарий JIRA)

Ответ 5

У меня была та же проблема, и я также решил ее с помощью настраиваемого PathMatcher. Мое решение несколько проще, чем предлагалось. Мой PathMatcher также имеет закрытую конечную цель AntPathMatcher, и он делегирует все обращения к нему без изменений, за исключением метода match():

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

Это работает, потому что Spring пытается сопоставить контроллеры, добавив ".". к концу. Например, с отображением пути "/widgets/{id}" и URL "/widgets/1.2.3.4", Spring пытается сначала совместить повторы "/widgets/{id}" . и после этого "/widgets/{id}" . Первый будет соответствовать, но он оставляет только "1.2.3" для id.

My PatchMatcher специально отклоняет шаблоны, заканчивающиеся на ". *", поэтому первая попытка завершается с ошибкой, а вторая соответствует.

Если вы используете ContentNegotiationViewResolver, вы все же можете указать тип контента в URL-адресе, используя параметр запроса "format" (если для параметра favorParameter установлено значение true).

-jarppe

Ответ 6

JFY: в Spring 4 эта проблема исправлена ​​с помощью: WebMvcConfigurerAdapter.

@Configuration 
class MvcConfiguration extends WebMvcConfigurerAdapter {

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
    configurer.setUseSuffixPatternMatch(false);
}
}

Или через WebMvcConfigurationSupport, например здесь.

Ответ 7

Чтобы добавить к ответу скаффмана, если вы используете <mvc:annotation-driven/>, и вы хотите переопределить значение useDefaultSuffixPattern, вы можете заменить тег <mvc:annotation-driven> следующим образом:

<!-- 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" />