Как я могу иметь нечувствительные к регистру URL-адреса в Spring MVC с аннотированными сопоставлениями

У меня есть аннотированные сопоставления, которые отлично работают через мое весеннее веб-приложение mvc, однако они чувствительны к регистру. Я не могу найти способ сделать их нечувствительными к регистру. (Я бы хотел, чтобы это произошло в Spring MVC, а не каким-то образом перенаправляло трафик)


person Dave    schedule 10.11.2010    source источник
comment
Кроме того, добавьте тег «Java», это даст вам гораздо больше просмотров страниц, что обычно означает больше ответов.   -  person David Parks    schedule 13.11.2010
comment
аналогичный вопрос с подробным ответом об этой проблеме, который я задал, увидев этот вопрос. stackoverflow. ком/вопросы/12684183/   -  person Zahid Riaz    schedule 05.10.2012


Ответы (6)


Spring 4.2 будет поддерживать сопоставление путей без учета регистра. Вы можете настроить его следующим образом:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        AntPathMatcher matcher = new AntPathMatcher();
        matcher.setCaseSensitive(false);
        configurer.setPathMatcher(matcher);
    }
}
person npcode    schedule 29.07.2015
comment
Знаете ли вы какие-либо такие конфигурации для параметров запроса? - person M22an; 11.11.2015
comment
@ M22an Извините, я не знаю. Я думаю, вы можете запросить эту функцию на jira.spring.io. - person npcode; 12.11.2015
comment
В Spring Boot 2 или Spring 5 WebMvcConfigurerAdapter устарел. Вместо этого следует реализовать WebMvcConfigurer напрямую. - person Downhillski; 30.03.2018

В соответствии с этой веб-публикацией вам необходимо добавить оба a HandlerMapping и HandlerAdapter в Spring MVC. Сопоставление сопоставляет запрос с соответствующим контроллером, и адаптер отвечает за выполнение запроса с использованием контроллера.

Поэтому вам необходимо переопределить PathMatcher как для преобразователя, так и для адаптера.

Ex (сделает все @Controllers нечувствительными к регистру):

Новый матчер:

public class CaseInsenseticePathMatcher extends AntPathMatcher {
    @Override
    protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {
        System.err.println(pattern + " -- " + path);
        return super.doMatch(pattern.toLowerCase(), path.toLowerCase(), fullMatch, uriTemplateVariables);
    }
}

приложениеContext.xml:

<bean id="matcher" class="test.CaseInsenseticePathMatcher"/>

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    <property name="pathMatcher" ref="matcher"/>
</bean>

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="pathMatcher" ref="matcher"/>
    <property name="webBindingInitializer">
        <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer"/>
    </property>
    <property name="messageConverters">
        <list>
            <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter"/>
        </list>
    </property>
</bean>

<bean id="conversion-service" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"/>

Добавлено примерно то же, что и ‹mvc:annotation-driven> подойдет. (Спасибо Дэвиду Парксу за ссылку)

person smat    schedule 25.03.2011
comment
Для Spring 3.1 замените AnnotationMethodHandlerAdapter на RequestMappingHandlerAdapter. Они используют этот новый класс в 3.1. - person smat; 26.09.2012

В Spring 3.2+ / Spring Boot теперь вы можете настроить сопоставление URL-адресов без учета регистра, используя упрощенную конфигурацию Java.

Сначала вам нужно создать класс CaseInsensitivePathMatcher.groovy или Java:

import org.springframework.util.AntPathMatcher

class CaseInsensitivePathMatcher extends AntPathMatcher{

    @Override
    protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {
        super.doMatch(pattern.toLowerCase(), path.toLowerCase(), fullMatch, uriTemplateVariables)
    }
}

Затем, чтобы это произошло, у вас должен быть класс с аннотацией Springs @Configuration, который расширяет класс WebMvcConfigurerAdapter, как показано ниже (Обратите внимание, что мой код содержится в классах .groovy, поэтому ключевое слово «return» не требуется в пример):

@Configuration
public class ApplicationConfig extends WebMvcConfigurerAdapter

Затем добавьте в класс следующие 2 метода:

/**
 * Creates a patchMatcher bean that matches case insensitively
 * @return PathMatcher
 */
@Bean
public PathMatcher pathMatcher() {
    new CaseInsensitivePathMatcher()
}

/**
 * Overrides the configurePathMatch() method in WebMvcConfigurerAdapter
 * <br/>Allows us to set a custom path matcher, used by the MVC for @RequestMapping's
     * @param configurer
     */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.pathMatcher = pathMatcher()
    }
}


Вот и все, теперь у вас должны быть все настройки для нечувствительных к регистру URL-адресов с минимальной конфигурацией.

person pczeus    schedule 22.04.2015
comment
Два года спустя, но все же... WebMvcConfigurerAdapter в Spring 3.2 нет метода configurePathMatch. Минимальная версия Spring неверна, вероятно, 4.2, как указано в ответах выше. Дополнительная информация о docs.spring.io/spring/docs/3.2.13.RELEASE/javadoc-api/org/ - person Alf; 26.04.2017
comment
Вполне нормально работает с Spring 3.2.17 и выше. Единственное, что отсутствует в приведенном выше коде, — это несколько операторов возврата, которые пропущены по ошибке. Вы можете найти решение для Spring старше 3.2.17 по адресу: newmiancode.blogspot.com/2010/01/ - person Gjera; 03.04.2019
comment
Спасибо за комментарий @Gjera. вы правы, что нет операторов возврата. Как я упоминал в приведенных выше примерах, мой код написан с использованием Groovy, что позволяет вам опускать «возврат» и по умолчанию использовать последнюю строку кода, выполняемого в методе, в качестве оператора возврата. Я отредактирую пост, чтобы выделить его «жирным шрифтом», чтобы он больше выделялся. - person pczeus; 25.04.2019


Отчет о проблемах для решение от smat


В решении smat, есть один небольшой побочный эффект (я бы обвинил в этом spring-mvc).

Сначала кажется, что AntPathMatcher.doMatch() возвращает true/false в зависимости от запрашиваемого URL-адреса и строки сопоставления запроса контроллера-метода (это единственное, что нужно сделать здесь). Но этот метод используется и для еще одной цели (которая не описана в документация!). Другая цель — собрать соответствующие значения для @PathVariable в методе-контроллере. Эти значения собираются в Map<String, String> uriTemplateVariables (последний параметр). И эти собранные значения используются для передачи методу-контроллеру в качестве значения параметра.

Например, у нас есть такой метод-контроллер,

@RequestMapping("/code/{userCode}")
public String getCode(@PathVariable("userCode") String userCode) {
    System.out.println(userCode);
}

и если мы получаем доступ с помощью URL-адреса, /code/AbD, то с помощью -mappings/5438158#5438158">решение от smat AntPathMatcher.doMatch() соберет @PathVariable значение в Map<String, String> uriTemplateVariables как userCode->abd. Поскольку мы используем нижний регистр строки пути, собранные значения также имеют нижний регистр. И это значение userCode в нижнем регистре передается нашему контроллеру.

Но я благодарен решение от smat, которое до сих пор служило мне хорошо без каких-либо других проблем.


Решение


Решил эту проблему, выполнив обход решение от smat. Без строки пути или шаблона в нижнем регистре в расширенном классе AntPathMatcher я заставил свой расширенный AntPathMatcher использовать мой пользовательский AntPathStringMatcher. мой пользовательский AntPathStringMatcher выполняет сопоставление без учета регистра без изменения регистра фактической строки.

В следующем коде решения большая часть кода скопирована из исходного кода класса (код, который я хотел настроить, был скрыт для подкласса из-за частного доступа).

Пользовательское сопоставление AntPathMatcher,

public class CaseInsensitivePathMatcher extends AntPathMatcher {

private final Map<String, CaseInsensitiveAntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<String, CaseInsensitiveAntPathStringMatcher>();

/**
 * Actually match the given <code>path</code> against the given
 * <code>pattern</code>.
 * 
 * @param pattern
 *            the pattern to match against
 * @param path
 *            the path String to test
 * @param fullMatch
 *            whether a full pattern match is required (else a pattern match
 *            as far as the given base path goes is sufficient)
 * @return <code>true</code> if the supplied <code>path</code> matched,
 *         <code>false</code> if it didn't
 */
protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {

    if (path.startsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR) != pattern.startsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR)) {
        return false;
    }

    String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, AntPathMatcher.DEFAULT_PATH_SEPARATOR);
    String[] pathDirs = StringUtils.tokenizeToStringArray(path, AntPathMatcher.DEFAULT_PATH_SEPARATOR);

    int pattIdxStart = 0;
    int pattIdxEnd = pattDirs.length - 1;
    int pathIdxStart = 0;
    int pathIdxEnd = pathDirs.length - 1;

    // Match all elements up to the first **
    while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
        String patDir = pattDirs[pattIdxStart];
        if ("**".equals(patDir)) {
            break;
        }
        if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
            return false;
        }
        pattIdxStart++;
        pathIdxStart++;
    }

    if (pathIdxStart > pathIdxEnd) {
        // Path is exhausted, only match if rest of pattern is * or **'s
        if (pattIdxStart > pattIdxEnd) {
            return (pattern.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR) ? path.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR) : !path
                    .endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR));
        }
        if (!fullMatch) {
            return true;
        }
        if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR)) {
            return true;
        }
        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
            if (!pattDirs[i].equals("**")) {
                return false;
            }
        }
        return true;
    } else if (pattIdxStart > pattIdxEnd) {
        // String not exhausted, but pattern is. Failure.
        return false;
    } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
        // Path start definitely matches due to "**" part in pattern.
        return true;
    }

    // up to last '**'
    while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
        String patDir = pattDirs[pattIdxEnd];
        if (patDir.equals("**")) {
            break;
        }
        if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
            return false;
        }
        pattIdxEnd--;
        pathIdxEnd--;
    }
    if (pathIdxStart > pathIdxEnd) {
        // String is exhausted
        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
            if (!pattDirs[i].equals("**")) {
                return false;
            }
        }
        return true;
    }

    while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
        int patIdxTmp = -1;
        for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
            if (pattDirs[i].equals("**")) {
                patIdxTmp = i;
                break;
            }
        }
        if (patIdxTmp == pattIdxStart + 1) {
            // '**/**' situation, so skip one
            pattIdxStart++;
            continue;
        }
        // Find the pattern between padIdxStart & padIdxTmp in str between
        // strIdxStart & strIdxEnd
        int patLength = (patIdxTmp - pattIdxStart - 1);
        int strLength = (pathIdxEnd - pathIdxStart + 1);
        int foundIdx = -1;

        strLoop: for (int i = 0; i <= strLength - patLength; i++) {
            for (int j = 0; j < patLength; j++) {
                String subPat = pattDirs[pattIdxStart + j + 1];
                String subStr = pathDirs[pathIdxStart + i + j];
                if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
                    continue strLoop;
                }
            }
            foundIdx = pathIdxStart + i;
            break;
        }

        if (foundIdx == -1) {
            return false;
        }

        pattIdxStart = patIdxTmp;
        pathIdxStart = foundIdx + patLength;
    }

    for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
        if (!pattDirs[i].equals("**")) {
            return false;
        }
    }

    return true;
}

/**
 * Tests whether or not a string matches against a pattern. The pattern may
 * contain two special characters:<br>
 * '*' means zero or more characters<br>
 * '?' means one and only one character
 * 
 * @param pattern
 *            pattern to match against. Must not be <code>null</code>.
 * @param str
 *            string which must be matched against the pattern. Must not be
 *            <code>null</code>.
 * @return <code>true</code> if the string matches against the pattern, or
 *         <code>false</code> otherwise.
 */
private boolean matchStrings(String pattern, String str, Map<String, String> uriTemplateVariables) {
    CaseInsensitiveAntPathStringMatcher matcher = this.stringMatcherCache.get(pattern);
    if (matcher == null) {
        matcher = new CaseInsensitiveAntPathStringMatcher(pattern);
        this.stringMatcherCache.put(pattern, matcher);
    }
    return matcher.matchStrings(str, uriTemplateVariables);
}

}

Пользовательское сопоставление AntPathStringMatcher,

public class CaseInsensitiveAntPathStringMatcher {
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<String>();


/** Construct a new instance of the <code>AntPatchStringMatcher</code>. */
CaseInsensitiveAntPathStringMatcher(String pattern) {
    this.pattern = createPattern(pattern);
}

private Pattern createPattern(String pattern) {
    StringBuilder patternBuilder = new StringBuilder();
    Matcher m = GLOB_PATTERN.matcher(pattern);
    int end = 0;
    while (m.find()) {
        patternBuilder.append(quote(pattern, end, m.start()));
        String match = m.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);
                variableNames.add(m.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);
                variableNames.add(variableName);
            }
        }
        end = m.end();
    }
    patternBuilder.append(quote(pattern, end, pattern.length()));
    return Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE);    // this line is updated to create case-insensitive pattern object
}

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</code> if the string matches against the pattern, or <code>false</code> otherwise.
 */
public boolean matchStrings(String str, Map<String, String> uriTemplateVariables) {
    Matcher matcher = pattern.matcher(str);
    if (matcher.matches()) {
        if (uriTemplateVariables != null) {
            // SPR-8455
            Assert.isTrue(variableNames.size() == matcher.groupCount(),
                    "The number of capturing groups in the pattern segment " + 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;
    }
}
person samir    schedule 17.07.2014

Пример из bean-файла в Spring 4.2, и это ТОЛЬКО ПОДДЕРЖИВАЕТСЯ v4.2+:

<mvc:annotation-driven validator="validator">
   <mvc:path-matching path-matcher="pathMatcher" />
</mvc:annotation-driven>

...

<!--Set endpoints case insensitive, spring is case-sensitive by default-->
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
  <property name="caseSensitive" value="false" />
</bean>
person shershon    schedule 02.06.2016

Ну не могу ответить на ваш вопрос (пробовал, думал разберусь). Но, учитывая, что вы не получили ни одного ответа за 2 дня, вот, по крайней мере, несколько зацепок:

Этот пример, кажется, предполагает, что это возможно:

http://webcache.googleusercontent.com/search?q=cache:ELj-ZQ8G4z0J:www.springbyexample.org/examples/sdms-simple-spring-mvc-web-module.html+безучетарегистра+requestmapping+spring&cd=3&hl=en&ct=clnk&client=firefox-a

Он ссылается на этот класс в Spring

http://static.springsource.org/spring/docs/3.0.4.RELEASE/javadoc-api/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.html

Я предполагаю (и это только предположение), что вам нужно расширить <mvc:annotation-driven/> и реализовать отдельные компоненты с правильными параметрами, чтобы сделать его нечувствительным к регистру. Видеть:

http://rapid-web.tumblr.com/post/296916668/what-does-annotation-driven-do

Последнее замечание: я заметил где-то еще, читая, что там сказано, что все пути по умолчанию в нижнем регистре. Вы убедились, что /MyPath не обрабатывается @RequestMapping("/mypath")?

Опять же, просто пища для размышлений, насколько я могу это сделать. Может быть, это продвинет вас достаточно далеко, чтобы задать более конкретный вопрос, который приведет вас к ответу — иногда такие вещи работают именно так. Удачи!

person David Parks    schedule 13.11.2010