Локали как часть URL в Spring MVC - PullRequest
15 голосов
/ 11 июля 2010

Я сейчас ищу фреймворк для многоязычных веб-приложений.На данный момент мне кажется, что лучший выбор - Spring MVC.Но я столкнулся с тем, что все рекомендации для разработчиков предлагают переключать языки с помощью LocaleChangeInterceptor таким образом:

http://www.somesite.com/action/?locale=en

К сожалению, есть ряд причин, по которым я хотел бы избежать этого,Как я могу сделать языковой код неотъемлемой частью URL?Например:

http://www.somesite.com/en/action

Спасибо.

UPD: Я нашел следующее решение.Это еще не завершено, но работает.Решение состоит из двух частей - фильтра сервлетов и компонента разрешения локали.Это выглядит немного хакерским, но я не вижу другого способа решить эту проблему.

public class LocaleFilter implements Filter
{

    ...

    private static final String DEFAULT_LOCALE = "en";
    private static final String[] AVAILABLE_LOCALES = new String[] {"en", "ru"};

    public LocaleFilter() {} 

    private List<String> getSevletRequestParts(ServletRequest request)
    {
        String[] splitedParts = ((HttpServletRequest) request).getServletPath().split("/");
        List<String> result = new ArrayList<String>();

        for (String sp : splitedParts)
        {
            if (sp.trim().length() > 0)
                result.add(sp);
        }

        return result;
    }

    private Locale getLocaleFromRequestParts(List<String> parts)
    {
        if (parts.size() > 0)
        {
            for (String lang : AVAILABLE_LOCALES)
            {
                if (lang.equals(parts.get(0)))
                {
                    return new Locale(lang);
                }
            }
        }

        return null;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException
    {
        List<String> requestParts = this.getSevletRequestParts(request);
        Locale locale = this.getLocaleFromRequestParts(requestParts);

        if (locale != null)
        {
            request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", locale);

            StringBuilder sb = new StringBuilder();
            for (int i = 1; i < requestParts.size(); i++)
            {
                sb.append('/');
                sb.append((String) requestParts.get(i));
            }

            RequestDispatcher dispatcher = request.getRequestDispatcher(sb.toString());
            dispatcher.forward(request, response);
        }
        else
        {
            request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", new Locale(DEFAULT_LOCALE));
            chain.doFilter(request, response);
        }
    }

    ...
}

public class FilterLocaleResolver implements LocaleResolver
{

    private Locale DEFAULT_LOCALE = new Locale("en");

    @Override
    public Locale resolveLocale(HttpServletRequest request)
    {
        Locale locale = (Locale) request.getAttribute(LocaleFilter.class.getName() + ".LOCALE");
        return (locale != null ? locale : DEFAULT_LOCALE);
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale)
    {
        request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", locale);
    }

}

Так что нет необходимости отображать локаль в каждом действии в контроллерах.Следующий пример будет работать нормально:

@Controller
@RequestMapping("/test")
public class TestController
{

    @RequestMapping("action")
    public ModelAndView action(HttpServletRequest request, HttpServletResponse response)
    {
        ModelAndView mav = new ModelAndView("test/action");
        ...
        return mav;
    }

}

Ответы [ 4 ]

7 голосов
/ 24 мая 2014

Я реализовал нечто очень похожее, используя комбинацию Filter и Interceptor.

Фильтр извлекает первую переменную пути и, если она является допустимой локалью, устанавливает его в качестве атрибута запроса, удаляет его с начала запрошенного URI и перенаправляет запрос на новый URI.

public class PathVariableLocaleFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(PathVariableLocaleFilter.class);

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    String url = defaultString(request.getRequestURI().substring(request.getContextPath().length()));
    String[] variables = url.split("/");

    if (variables.length > 1 && isLocale(variables[1])) {
        LOG.debug("Found locale {}", variables[1]);
        request.setAttribute(LOCALE_ATTRIBUTE_NAME, variables[1]);
        String newUrl = StringUtils.removeStart(url, '/' + variables[1]);
        LOG.trace("Dispatching to new url \'{}\'", newUrl);
        RequestDispatcher dispatcher = request.getRequestDispatcher(newUrl);
        dispatcher.forward(request, response);
    } else {
        filterChain.doFilter(request, response);
    }
}

private boolean isLocale(String locale) {
    //validate the string here against an accepted list of locales or whatever
    try {
        LocaleUtils.toLocale(locale);
        return true;
    } catch (IllegalArgumentException e) {
        LOG.trace("Variable \'{}\' is not a Locale", locale);
    }
    return false;
}
}

Перехватчик очень похож на LocaleChangeInterceptor, он пытается получить локаль из атрибута запроса и, если локаль найдена, он устанавливает его на LocaleResolver.

public class LocaleAttributeChangeInterceptor extends HandlerInterceptorAdapter {
public static final String LOCALE_ATTRIBUTE_NAME = LocaleAttributeChangeInterceptor.class.getName() + ".LOCALE";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

    Object newLocale = request.getAttribute(LOCALE_ATTRIBUTE_NAME);
    if (newLocale != null) {
        LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
        if (localeResolver == null) {
            throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
        }
        localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale.toString()));
    }
    // Proceed in any case.
    return true;
}
}

После того, как они у вас есть, вам нужно настроить Spring на использование перехватчика и LocaleResolver.

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LocaleAttributeChangeInterceptor());
}

@Bean(name = "localeResolver")
public LocaleResolver getLocaleResolver() {
    return new CookieLocaleResolver();
}

И добавьте фильтр к AbstractAnnotationConfigDispatcherServletInitializer.

@Override
protected Filter[] getServletFilters() {
    return new Filter[] { new PathVariableLocaleFilter() };
}

Я не проверил его полностью, но, похоже, он работает до сих пор, и вам не нужно прикасаться к контроллерам, чтобы принять переменную пути {locale}, она должна просто работать "из коробки". Возможно, в будущем у нас будет автоматическое решение Spring locale as variable / subfolder, так как кажется, что все больше и больше сайтов его принимают, и, по мнению некоторых, это способ пойти .

5 голосов
/ 21 сентября 2014

Я обнаружил, что столкнулся с той же проблемой, и после долгих исследований мне наконец-то удалось это сделать, используя также Filter и LocaleResolver.Шаг за шагом:

Сначала установите фильтр в web.xml :

<filter>
    <filter-name>LocaleFilter</filter-name>
    <filter-class>yourCompleteRouteToTheFilter.LocaleUrlFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>LocaleFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

В LocaleUrlFilter.java , который мы используемregex to:

  • добавить два атрибута (код страны и код языка) к запросу, который мы будем позже записывать в LocaleResolver:
  • убрать язык из URL

    import java.io.IOException;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    public class LocaleUrlFilter implements Filter{
    
        private static final Pattern localePattern = Pattern.compile("^/([a-z]{2})(?:/([a-z]{2}))?(/.*)?");
        public static final String COUNTRY_CODE_ATTRIBUTE_NAME = LocaleUrlFilter.class.getName() + ".country";
        public static final String LANGUAGE_CODE_ATTRIBUTE_NAME = LocaleUrlFilter.class.getName() + ".language";
    
        @Override
        public void init(FilterConfig arg0) throws ServletException {}  
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String url = request.getRequestURI().substring(request.getContextPath().length());
            Matcher matcher = localePattern.matcher(url);
            if (matcher.matches()) {
                // Set the language attributes that we will use in LocaleResolver and strip the language from the url
                request.setAttribute(COUNTRY_CODE_ATTRIBUTE_NAME, matcher.group(1));
                request.setAttribute(LANGUAGE_CODE_ATTRIBUTE_NAME, matcher.group(2));
                request.getRequestDispatcher(matcher.group(3) == null ? "/" : matcher.group(3)).forward(servletRequest, servletResponse);
            }
            else filterChain.doFilter(servletRequest, servletResponse);     
        }
    
        @Override
        public void destroy() {}
    }
    

Теперь фильтр вставил в запрос два атрибута, которые мы будем использовать для формирования языкового стандарта, и убрал язык из url для правильной обработки наших запросов.Теперь мы определим LocaleResolver для изменения локали.Для этого сначала мы изменим наш servlet.xml файл:

<!-- locale Resolver configuration-->
<bean id="localeResolver" class="yourCompleteRouteToTheResolver.CustomLocaleResolver"></bean>

И в CustomLocaleResolver.java мы соответственно установим язык.Если в URL нет языка, мы продолжаем использовать метод запроса getLocale:

import java.util.Locale;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.LocaleResolver;

/*
* Set the Locale defined in the LocaleUrlFiltes. If none is defined (in the url) return the request locale.
*/
public class CustomLocaleResolver implements LocaleResolver{

    @Override
    public Locale resolveLocale(HttpServletRequest servletRequest) {
        final String countryCode = (String)servletRequest.getAttribute(LocaleUrlFilter.COUNTRY_CODE_ATTRIBUTE_NAME);
        if (countryCode != null) {
            String languageCode = (String)servletRequest.getAttribute(LocaleUrlFilter.LANGUAGE_CODE_ATTRIBUTE_NAME);
            if (languageCode == null) {
                return new Locale(countryCode);
            }
            return new Locale(languageCode, countryCode);
        }
        return servletRequest.getLocale();
    }

    @Override
    public void setLocale(final HttpServletRequest servletRequest, final HttpServletResponse servletResponse, final Locale locale) {
        throw new UnsupportedOperationException();
    }

}

Для этого вам не нужно ничего менять в контроллерах, и посещение "/ en / home" будеттак же, как посещение "/ home" и использование вашего файла language_en.properties.Надеюсь, это поможет

3 голосов
/ 28 марта 2017

Недавно я столкнулся с той же проблемой.Поэтому я хотел бы, чтобы локаль без сохранения состояния не зависела от сеанса, файла cookie или чего-либо еще, кроме простого URL.

Я попробовал решения фильтра / перехватчика / localeResolver, предложенные в предыдущих ответах, однако они не соответствовали моим потребностям, как у меня:

  • статическое содержимое (изображения и т. Д.)
  • части страницы не зависят от локали (панель администратора)
  • RestController внутри одного и того же приложения
  • загрузчик многокомпонентных файлов

Я также хотел избежать дублирования контента по причинам SEO(В частности, я не хочу, чтобы мой английский контент был доступен по обоим путям: / landingPage и /en/landingPage).

Лучшим решением для меня было создать LanguageAwareController, а затем наследовать от него во всехконтроллеры, которые я хотел поддерживать несколько локалей.

@Controller
@RequestMapping(path = "/{lang}")
public class LanguageAwareController {
    @Autowired
    LocaleResolver localeResolver;

    @ModelAttribute(name = "locale")
    Locale getLocale(@PathVariable(name = "lang") String lang, HttpServletRequest request,
                 HttpServletResponse response){
        Locale effectiveLocale = Arrays.stream(Locale.getAvailableLocales())
            .filter(locale -> locale.getLanguage().equals(lang))
            .findFirst()
            .orElseGet(Locale::getDefault);
        localeResolver.setLocale(request, response, effectiveLocale);
        return effectiveLocale;
    }
}

Использование в одном из контроллеров:

@Controller
public class LandingPageController extends LanguageAwareController{

    private Log log = LogFactory.getLog(LandingPageController.class);

    @GetMapping("/")
    public String welcomePage(Locale locale, @PathVariable(name = "lang") String lang ){
        log.info(lang);
        log.info(locale);
        return "landing";
    }
}
0 голосов
/ 11 июля 2010

Весной 3.0 вы можете указать контроллерам искать переменные пути .например,

@RequestMapping("/{locale}/action")
public void action(@PathVariable String locale) {
    ...
}
...