Fork me on GitHub

LocaleResolver

注意:所有文章除特别说明外,转载请注明出处.

第18章 LocaleResolver

LocaleResolver的主要作用在于根据不同的用户区域展示不同的视图,通过设置系统的环境,根据运行环境使用不同的语言显示。用户的区域称为 Locale ,这一信息可以由前端直接获取。通过这一方式可以实现国际化的目的。如:针对美国用户提供一个视图,针对中国用户提供一个视图。而LocaleResolver的使用是实现对用户不同视图的切换。LocaleResolver的作用是使用request解析出Locale。在LocaleResolver的实现类中,AcceptHeaderLocaleResolver直接使用了Header里的acceptlanguage值,不能在程序里面修改。

1.Spring针对LocaleResoler提供几种实现方式

1.FixedLocaleResolver 在声明该resolver时,需要指定一个默认的Locale,在进行Locale获取时,始终返回该Locale,并且调用其setLocale()方法也无法改变其Locale。

2.SessionLocaleResolver 其会将Locale信息存储在session中,如果用户想要修改Locale信息,可以通过修改session中对应属性的值即可。

3.CookieLocaleResolver 其读取Locale的方式是在session中通过Cookie来获取其指定的Locale的,如果修改了Cookie的值,页面视图也会同步切换。

4.AcceptHeaderLocaleResolver 其会通过用户请求中名称为Accept-Language的header来获取Locale信息,如果想要修改展示的视图,只需要修改该header信息即可。

提示:在Spring4.x之后,LocaleResolver添加子接口 LocaleContextResolver 其中增加了获取和设置LocaleContext 的能力,并添加了抽象类 AbstractLocaleContextResolver,抽象类添加了对TimeZone(时区)的支持。

注意:Spring虽然提供几个不同的获取Locale的方式,但是这些方式除 FixedLocaleResolver 以外,其它也都支持在浏览器地址栏添加Locale参数来切换Locale。而对于Locale的切换,Spring通过拦截器来实现,其提供一个LocaleChangeInterceptor,在该拦截器中的preHandler()方法中,Spring会读取浏览器参数中的locale参数,然后调用LocaleResolver.setLocale()方法实现对Locale的切换。

2.LocaleResolver接口声明

public interface LocaleResolver {
    // 根据request对象根据指定的方式获取一个Locale,如果没有获取到,则使用用户指定的默认的Locale
    Locale resolveLocale(HttpServletRequest request);

    // 用于实现Locale的切换。比如SessionLocaleResolver获取Locale的方式是从session中读取,但如果
    // 用户想要切换其展示的样式(由英文切换为中文),那么这里的setLocale()方法就提供了这样一种可能
    void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, 
        @Nullable Locale locale);
}

2.1 重写 resolveLocale(HttpServletRequest request)方法

这里使用默认的解析器 AcceptHeaderLocaleResolver 继承 LocaleResolver 重写resolverLocale(HttpServletRequest request)方法,通过检查客户端发送请求中的Accept-Language头来确定客户端Locale(地区信息)。

/**
 * 从当前的request中解析Locale
 */
@Override
public Locale resolveLocale(HttpServletRequest request) {
    // 获取默认设置,可在配置AcceptHeaderLocaleResolver Bean中设置defaultLocale属性
    Locale defaultLocale = getDefaultLocale();
    // 设置了默认值并且请求中没有Accept-Language头信息时,使用默认设置
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
        return defaultLocale;
    }
    // 从当前请求中获取Locale
    Locale requestLocale = request.getLocale();
    // 从配置中获取支持的Locale集合,可在AcceptHeaderLocaleResolver Bean中设置supportedLocales属性
    List<Locale> supportedLocales = getSupportedLocales();
    // 未设置supportedLocales或者supportedLocales中包括请求Locale,则使用请求Locale
    if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
        return requestLocale;
    }
    // 找到设置的Locale集合中是否有请求的Locale
    Locale supportedLocale = findSupportedLocale(request, supportedLocales);
    if (supportedLocale != null) {
        return supportedLocale;
    }
    return (defaultLocale != null ? defaultLocale : requestLocale);
}

/**
 * 不支持程序设置Locale
 */
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
    throw new UnsupportedOperationException(
            "Cannot change HTTP accept header - use a different locale resolution strategy");
}

2.2 在SpringMVC配置文件中配置资源加载以及AcceptHeaderLocaleResolver bean

<!-- 国际化资源文件 -->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <!-- 如果资源文件放在classpath下,basename的value必须有classpath:前缀,否则报错:No message found under code... -->
    <property name="basename" value="classpath:i18n/messages" />
    <!-- 如果在国际化资源文件中找不到对应代码的信息,就用这个代码作为名称返回  -->
    <property name="useCodeAsDefaultMessage" value="true" />
    <!--<property name="defaultEncoding" value="ISO-8859-1"/>-->
</bean>
<!-- LocaleResolver解析器 -->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver">
    <property name="defaultLocale" value="zh_CN"/>
</bean>

2.3 属性文件

2.3.1 messages_en.properties

message.locale = en

2.3.2 messages_zh_CN.properties

message.locale=zh_CN

2.4 Controller

@GetMapping(value = "/acceptHeaderLocaleResolver" , produces = "text/html;charset=UTF-8")
@ResponseBody
public String test(HttpServletRequest request) {
    String clientLocale = "";
    Enumeration<Locale> enus =  request.getLocales();
    while (enus.hasMoreElements()){
        Locale locale = enus.nextElement();
        clientLocale += locale + ",";
    }
    RequestContext requestContext = new RequestContext(request);
    String value = requestContext.getMessage("message.locale");
    return "客户端支持的Locale有:"+clientLocale+" </br>当前使用的Locale是:" + requestContext.getLocale() + " </br>使用的资源Locale文件是:" + value ;
}

3.CookieLocaleResolver解析器

CookieLocaleResolver解析器通过不同的Locale展示不同的视图。CookieLocaleResolver类的入口是:resolveLocaleContext(final HttpServletRequest request); SpringMVC接收到客户端请求之后会调用该方法。

@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
    // 解析Cookie信息
    parseLocaleCookieIfNecessary(request);
    // 返回Locale和TimeZone
    return new TimeZoneAwareLocaleContext() {
        @Override
        @Nullable
        public Locale getLocale() {
            return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
        }
        @Override
        @Nullable
        public TimeZone getTimeZone() {
            return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME);
        }
    };
}

private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
    // 第一次请求为null
    if (request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME) == null) {
        Locale locale = null; // 地区
        TimeZone timeZone = null; // 时区

        // 获取cookie的名称,取自Spring MVC配置,默认为:CookieLocaleResolver.DEFAULT_COOKIE_NAME
        String cookieName = getCookieName();
        if (cookieName != null) {
            // 根据名称获取当前请求中的Cookie(第一次访问为null)
            Cookie cookie = WebUtils.getCookie(request, cookieName);
            if (cookie != null) {
                // 以下主要是从客户端Cookie中解析出Locale
                String value = cookie.getValue();
                String localePart = value;
                String timeZonePart = null;
                int spaceIndex = localePart.indexOf(' ');
                if (spaceIndex != -1) {
                    localePart = value.substring(0, spaceIndex);
                    timeZonePart = value.substring(spaceIndex + 1);
                }
                try {
                    locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
                    if (timeZonePart != null) {
                        timeZone = StringUtils.parseTimeZoneString(timeZonePart);
                    }
                }
                catch (IllegalArgumentException ex) {
                    if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
                        // Error dispatch: ignore locale/timezone parse exceptions
                        if (logger.isDebugEnabled()) {
                            logger.debug("Ignoring invalid locale cookie '" + cookieName +
                                    "' with value [" + value + "] due to error dispatch: " + ex.getMessage());
                        }
                    }
                    else {
                        throw new IllegalStateException("Invalid locale cookie '" + cookieName +
                                "' with value [" + value + "]: " + ex.getMessage());
                    }
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale +
                            "'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : ""));
                }
            }
        }
        // 把Locale设置到请求的Attribute区,客户端请求没有携带Cookie,取Spring MVC中配置的defaultLocale
        request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
                (locale != null ? locale : determineDefaultLocale(request)));
        request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
                (timeZone != null ? timeZone : determineDefaultTimeZone(request)));
    }
}

// 设置Locale
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
    setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
}

// 主要是把Locale信息写回客户端
@Override
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,
        @Nullable LocaleContext localeContext) {

    Assert.notNull(response, "HttpServletResponse is required for CookieLocaleResolver");

    Locale locale = null;
    TimeZone timeZone = null;
    if (localeContext != null) {
        locale = localeContext.getLocale();
        if (localeContext instanceof TimeZoneAwareLocaleContext) {
            timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
        }
        addCookie(response,
                (locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
    }
    else {
        removeCookie(response);
    }
    request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
            (locale != null ? locale : determineDefaultLocale(request)));
    request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
            (timeZone != null ? timeZone : determineDefaultTimeZone(request)));
}

3.1 配置xml文件

<context:component-scan base-package="mvc"/>
<mvc:annotation-driven/>

<mvc:interceptors>
    <!--添加拦截器,用于对Locale的切换-->
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
    <bean class="mvc.interceptor.MyHandlerInterceptor"/>
</mvc:interceptors>

<!--指定ViewResolver是ResourceBundleViewResolver是因为其支持通过不同的Locale进行不同的视图切换-->
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver"/>

<!--指定LocaleResolver为CookieLocaleResolver-->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
    <!--指定defaultlocal是zh_CN-->
    <property name="defaultLocale" value="zh_CN"/>
</bean>

3.2 声明接口

@Controller
@RequestMapping("/user")
public class UserController {
  @Autowired
  private UserService userService;

  @RequestMapping(value = "/detail", method = RequestMethod.GET)
  public ModelAndView detail(@RequestParam("id") long id, @ModelAttribute("message") String message, Locale locale) {
    System.out.println(message);
    ModelAndView view = new ModelAndView("user");
    User user = userService.detail(id);
    view.addObject("user", user);
    view.addObject("locale", locale);
    return view;
  }
}

提示:这里返回的视图是:user 并将Locale信息返回给前端。这里获取Locale数据的方式就只需要简单的声明一个类型为Locale的参数。

3.3 视图的展示

由于需要根据不同的Locale展示不同的视图,我们在上面的接口中没有发现这样的路由。在实际中,这个路由是根据ResourceBundleViewResolver类实现的,在使用该ViewResolver时,其会到class路径下查找名称为views的Resource Bundle,同时通过用户指定的Locale,唯一定位到某个 Resource Bundle。 然后在该Resource Bundle 中查找指定的视图信息。

总结

这里我们的LocaleResolver的bean名称必须是 localeResolver,并且需要指定的 ViewResolver 辅以支持,否则切换的视图可能无法工作。

本文标题:LocaleResolver

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:17:21

原始链接:http://bangjinhu.github.io/undefined/第18章 LocaleResolver/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.