乐趣区

关于spring:这篇文章让你搞懂-SpringMVC-国际化

松哥之前写过 Spring Boot 国际化的问题,不过那一次没讲源码,这次咱们整点源码来深刻了解下这个问题。

国际化,也叫 i18n,为啥叫这个名字呢?因为国际化英文是 internationalization,在 i 和 n 之间有 18 个字母,所以叫 i18n。咱们的利用如果做了国际化就能够在不同的语言环境下,不便的进行切换,最常见的就是中文和英文之间的切换,国际化这个性能也是相当的常见。

1.SpringMVC 国际化配置

还是先来说说用法,再来说源码,这样大家不容易犯迷糊。咱们先说在 SSM 中如何解决国际化问题。

首先国际化咱们可能有两种需要:

  • 在页面渲染时实现国际化(这个借助于 Spring 标签实现)
  • 在接口中获取国际化匹配后的音讯

大抵上就是下面这两种场景。接下来松哥通过一个简略的用法来和大家演示下具体玩法。

首先咱们在我的项目的 resources 目录下新建语言文件,language_en_US.properties 和 language_zh-CN.properties,如下图:

内容别离如下:

language_en_US.properties:

login.username=Username
login.password=Password

language_zh-CN.properties:

login.username= 用户名
login.password= 用户明码 

这两个别离对应英中文环境。配置文件写好之后,还须要在 SpringMVC 容器中提供一个 ResourceBundleMessageSource 实例去加载这两个实例,如下:

<bean class="org.springframework.context.support.ResourceBundleMessageSource" id="messageSource">
    <property name="basename" value="language"/>
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

这里配置了文件名 language 和默认的编码格局。

接下来咱们新建一个 login.jsp 文件,如下:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<spring:message code="login.username"/> <input type="text"> <br>
<spring:message code="login.password"/> <input type="text"> <br>
</body>
</html>

在这个文件中,咱们通过 spring:message 标签来援用变量,该标签会依据以后的理论状况,抉择适合的语言文件。

接下来咱们为 login.jsp 提供一个控制器:

@Controller
public class LoginController {
    @Autowired
    MessageSource messageSource;
    @GetMapping("/login")
    public String login() {String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());
        String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());
        System.out.println("username =" + username);
        System.out.println("password =" + password);
        return "login";
    }
}

控制器中间接返回 login 视图即可。

另外我这还注入了 MessageSource 对象,次要是为了向大家展现如何在处理器中获取国际化后的语言文字。

配置实现后,启动我的项目进行测试。

默认状况下,零碎是依据申请头的中 Accept-Language 字段来判断以后的语言环境的,该这个字段由浏览器主动发送,咱们这里为了测试不便,能够应用 POSTMAN 进行测试,而后手动设置 Accept_Language 字段。

首先测试中文环境:

而后测试英文环境:

都没问题,完满!同时察看 IDEA 控制台,也能正确打印出语言文字。

下面这个是基于 AcceptHeaderLocaleResolver 来解析出以后的区域和语言的。

有的时候,咱们心愿语言环境间接通过申请参数来传递,而不是通过申请头来传递,这个需要咱们通过 SessionLocaleResolver 或者 CookieLocaleResolver 都能够实现。

先来看 SessionLocaleResolver。

首先在 SpringMVC 配置文件中提供 SessionLocaleResolver 的实例,同时配置一个拦截器,如下:

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
            <property name="paramName" value="locale"/>
        </bean>
    </mvc:interceptor>
</mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.SessionLocaleResolver" id="localeResolver">
</bean>

SessionLocaleResolver 是负责区域解析的,这个没啥好说的。拦截器 LocaleChangeInterceptor 则次要是负责参数解析的,咱们在配置拦截器的时候,设置了参数名为 locale(默认即此),也就是说咱们未来能够通过 locale 参数来传递以后的环境信息。

配置实现后,咱们还是来拜访方才的 login 控制器,如下:

此时咱们能够间接通过 locale 参数来管制以后的语言环境,这个 locale 参数就是在后面所配置的 LocaleChangeInterceptor 拦截器中被主动解析的。

如果你不想配置 LocaleChangeInterceptor 拦截器也是能够的,间接本人手动解析 locale 参数而后设置 locale 也行,像上面这样:

@Controller
public class LoginController {
    @Autowired
    MessageSource messageSource;
    @GetMapping("/login")
    public String login(String locale,HttpSession session) {if ("zh-CN".equals(locale)) {session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("zh", "CN"));
        } else if ("en-US".equals(locale)) {session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("en", "US"));
        }
        String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());
        String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());
        System.out.println("username =" + username);
        System.out.println("password =" + password);
        return "login";
    }
}

SessionLocaleResolver 所实现的性能也能够通过 CookieLocaleResolver 来实现,不同的是前者将解析进去的区域信息保留在 session 中,而后者则保留在 Cookie 中。 保留在 session 中,只有 session 没有发生变化,后续就不必再次传递区域语言参数了,保留在 Cookie 中,只有 Cookie 没变,后续也不必再次传递区域语言参数了

应用 CookieLocaleResolver 的形式很简略,间接在 SpringMVC 中提供 CookieLocaleResolver 的实例即可,如下:

<bean class="org.springframework.web.servlet.i18n.CookieLocaleResolver" id="localeResolver"/>

留神这里也须要应用到 LocaleChangeInterceptor 拦截器,如果不应用该拦截器,则须要本人手动解析并配置语言环境,手动解析并配置的形式如下:

@GetMapping("/login3")
public String login3(String locale, HttpServletRequest req, HttpServletResponse resp) {CookieLocaleResolver resolver = new CookieLocaleResolver();
    if ("zh-CN".equals(locale)) {resolver.setLocale(req, resp, new Locale("zh", "CN"));
    } else if ("en-US".equals(locale)) {resolver.setLocale(req, resp, new Locale("en", "US"));
    }
    String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());
    String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());
    System.out.println("username =" + username);
    System.out.println("password =" + password);
    return "login";
}

配置实现后,启动我的项目进行测试,这次测试的形式跟 SessionLocaleResolver 的测试形式统一,松哥就不再多说了。

除了后面介绍的这几种 LocaleResolver 之外,还有一个 FixedLocaleResolver,因为比拟少见,松哥这里就不做过多介绍了。

2.Spring Boot 国际化配置

2.1 根本应用

Spring Boot 和 Spring 一脉相承,对于国际化的反对,默认是通过 AcceptHeaderLocaleResolver 解析器来实现的,这个解析器,默认是通过申请头的 Accept-Language 字段来判断以后申请所属的环境的,进而给出适合的响应。

所以在 Spring Boot 中做国际化,这一块咱们能够不必配置,间接就开搞。

首先创立一个一般的 Spring Boot 我的项目,增加 web 依赖即可。我的项目创立胜利后,默认的国际化配置文件放在 resources 目录下,所以咱们间接在该目录下创立四个测试文件,如下:

  • 咱们的 message 文件是间接创立在 resources 目录下的,IDEA 在展现的时候,会多出一个 Resource Bundle,这个大家不必管,千万别手动去创立这个目录。
  • messages.properties 这个是默认的配置,其余的则是不同语言环境下的配置,en_US 是英语 (美国),zh_CN 是中文简体,zh_TW 是中文繁体(文末附录里边有一个残缺的语言简称表格)。

四个文件创建好之后,第一个默认的咱们能够先空着,另外三个别离填入以下内容:

messages_zh_CN.properties

user.name= 江南一点雨 

messages_zh_TW.properties

user.name= 江南壹點雨 

messages_en_US.properties

user.name=javaboy

配置实现后,咱们就能够间接开始应用了。在须要应用值的中央,间接注入 MessageSource 实例即可。

在 Spring 中须要配置的 MessageSource 当初不必配置了,Spring Boot 会通过 org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration 主动帮咱们配置一个 MessageSource 实例。

创立一个 HelloController,内容如下:

@RestController
public class HelloController {
    @Autowired
    MessageSource messageSource;
    @GetMapping("/hello")
    public String hello() {return messageSource.getMessage("user.name", null, LocaleContextHolder.getLocale());
    }
}

在 HelloController 中咱们能够间接注入 MessageSource 实例,而后调用该实例中的 getMessage 办法去获取变量的值,第一个参数是要获取变量的 key,第二个参数是如果 value 中有占位符,能够从这里传递参数进去,第三个参数传递一个 Locale 实例即可,这相当于以后的语言环境。

接下来咱们就能够间接去调用这个接口了。

默认状况下,在接口调用时,通过申请头的 Accept-Language 来配置以后的环境,我这里通过 POSTMAN 来进行测试,后果如下:

小伙伴们看到,我在申请头中设置了 Accept-Language 为 zh-CN,所以拿到的就是简体中文;如果我设置了 zh-TW,就会拿到繁体中文:

是不是很 Easy?

2.2 自定义切换

有的小伙伴感觉切换参数放在申请头里边如同不太不便,那么也能够自定义解析形式。例如参数能够当成一般参数放在地址栏上,通过如下配置能够实现咱们的需要。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        registry.addInterceptor(interceptor);
    }
    @Bean
    LocaleResolver localeResolver() {SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return localeResolver;
    }
}

在这段配置中,咱们首先提供了一个 SessionLocaleResolver 实例,这个实例会替换掉默认的 AcceptHeaderLocaleResolver,不同于 AcceptHeaderLocaleResolver 通过申请头来判断以后的环境信息,SessionLocaleResolver 将客户端的 Locale 保留到 HttpSession 对象中,并且能够进行批改(这意味着以后环境信息,前端给浏览器发送一次即可记住,只有 session 无效,浏览器就不用再次通知服务端以后的环境信息)。

另外咱们还配置了一个拦截器,这个拦截器会拦挡申请中 key 为 lang 的参数(不配置的话是 locale),这个参数则指定了以后的环境信息。

好了,配置实现后,启动我的项目,拜访形式如下:

咱们通过在申请中增加 lang 来指定以后环境信息。这个指定只须要一次即可,也就是说,在 session 不变的状况下,下次申请能够不用带上 lang 参数,服务端曾经晓得以后的环境信息了。

CookieLocaleResolver 也是相似用法,不再赘述。

2.3 其余自定义

默认状况下,咱们的配置文件放在 resources 目录下,如果大家想自定义,也是能够的,例如定义在 resources/i18n 目录下:

然而这种定义形式零碎就不晓得去哪里加载配置文件了,此时还须要 application.properties 中进行额定配置 (留神这是一个相对路径):

spring.messages.basename=i18n/messages

另外还有一些编码格局的配置等,内容如下:

spring.messages.cache-duration=3600
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=true

spring.messages.cache-duration 示意 messages 文件的缓存生效工夫,如果不配置则缓存始终无效。

spring.messages.fallback-to-system-locale 属性则略显神奇,网上居然看不到一个明确的答案,起初翻了一会源码才看出端倪。

这个属性的作用在 org.springframework.context.support.AbstractResourceBasedMessageSource#getDefaultLocale 办法中失效:

protected Locale getDefaultLocale() {if (this.defaultLocale != null) {return this.defaultLocale;}
    if (this.fallbackToSystemLocale) {return Locale.getDefault();
    }
    return null;
}

从这段代码能够看出,在找不到以后零碎对应的资源文件时,如果该属性为 true,则会默认查找以后零碎对应的资源文件,否则就返回 null,返回 null 之后,最终又会调用到零碎默认的 messages.properties 文件。

3.LocaleResolver

国际化这块次要波及到的组件是 LocaleResolver,这是一个凋谢的接口,官网默认提供了四个实现。以后该应用什么环境,次要是通过 LocaleResolver 来进行解析的。

LocaleResolver

public interface LocaleResolver {Locale resolveLocale(HttpServletRequest request);
    void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);

}

这里两个办法:

  1. resolveLocale:依据以后申请解析器出 Locale 对象。
  2. 设置 Locale 对象。

咱们来看看 LocaleResolver 的继承关系:

尽管两头有几个抽象类,不过最终负责实现的其实就四个:

  • AcceptHeaderLocaleResolver:依据申请头中的 Accept-Language 字段来确定以后的区域语言等。
  • SessionLocaleResolver:依据申请参数来确定区域语言等,确定后会保留在 Session 中,只有 Session 不变,Locale 对象就始终无效。
  • CookieLocaleResolver:依据申请参数来确定区域语言等,确定后会保留在 Cookie 中,只有 Session 不变,Locale 对象就始终无效。
  • FixedLocaleResolver:配置时间接提供一个 Locale 对象,当前不能批改。

接下来咱们就对这几个类逐个进行剖析。

3.1 AcceptHeaderLocaleResolver

AcceptHeaderLocaleResolver 间接实现了 LocaleResolver 接口,咱们来看它的 resolveLocale 办法:

@Override
public Locale resolveLocale(HttpServletRequest request) {Locale defaultLocale = getDefaultLocale();
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {return defaultLocale;}
    Locale requestLocale = request.getLocale();
    List<Locale> supportedLocales = getSupportedLocales();
    if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {return requestLocale;}
    Locale supportedLocale = findSupportedLocale(request, supportedLocales);
    if (supportedLocale != null) {return supportedLocale;}
    return (defaultLocale != null ? defaultLocale : requestLocale);
}
  1. 首先去获取默认的 Locale 对象。
  2. 如果存在默认的 Locale 对象,并且申请头中没有设置 Accept-Language 字段,则间接返回默认的 Locale。
  3. 从 request 中取出以后的 Locale 对象,而后查问出反对的 supportedLocales,如果 supportedLocales 或者 supportedLocales 中蕴含 requestLocale,则间接返回 requestLocale。
  4. 如果后面还是没有匹配胜利的,则从 request 中取出 locales 汇合,而后再去和反对的 locale 进行比对,抉择匹配胜利的 locale 返回。
  5. 如果后面都没能返回,则判断 defaultLocale 是否为空,如果不为空,就返回 defaultLocale,否则返回 defaultLocale。

再来看看它的 setLocale 办法,间接抛出异样,意味着通过申请头解决 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");
}

3.2 SessionLocaleResolver

SessionLocaleResolver 的实现多了一个抽象类 AbstractLocaleContextResolver,AbstractLocaleContextResolver 中减少了对 TimeZone 的反对,咱们先来看下 AbstractLocaleContextResolver:

public abstract class AbstractLocaleContextResolver extends AbstractLocaleResolver implements LocaleContextResolver {
    @Nullable
    private TimeZone defaultTimeZone;
    public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) {this.defaultTimeZone = defaultTimeZone;}
    @Nullable
    public TimeZone getDefaultTimeZone() {return this.defaultTimeZone;}
    @Override
    public Locale resolveLocale(HttpServletRequest request) {Locale locale = resolveLocaleContext(request).getLocale();
        return (locale != null ? locale : request.getLocale());
    }
    @Override
    public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
    }

}

能够看到,多了一个 TimeZone 属性。从申请中解析出 Locale 还是调用了 resolveLocaleContext 办法,该办法在子类中被实现,另外调用 setLocaleContext 办法设置 Locale,该办法的实现也在子类中。

咱们来看下它的子类 SessionLocaleResolver:

@Override
public Locale resolveLocale(HttpServletRequest request) {Locale locale = (Locale) WebUtils.getSessionAttribute(request, this.localeAttributeName);
    if (locale == null) {locale = determineDefaultLocale(request);
    }
    return locale;
}

间接从 Session 中获取 Locale,默认的属性名是 SessionLocaleResolver.class.getName() + ".LOCALE",如果 session 中不存在 Locale 信息,则调用 determineDefaultLocale 办法去加载 Locale,该办法会首先找到 defaultLocale,如果 defaultLocale 不为 null 就间接返回,否则就从 request 中获取 Locale 返回。

再来看 setLocaleContext 办法,就是将解析进去的 Locale 保存起来。

@Override
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,
        @Nullable LocaleContext localeContext) {
    Locale locale = null;
    TimeZone timeZone = null;
    if (localeContext != null) {locale = localeContext.getLocale();
        if (localeContext instanceof TimeZoneAwareLocaleContext) {timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();}
    }
    WebUtils.setSessionAttribute(request, this.localeAttributeName, locale);
    WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone);
}

保留到 Session 中即可。大家能够看到,这种保留形式其实和咱们后面演示的本人保留代码基本一致,必由之路。

3.3 FixedLocaleResolver

FixedLocaleResolver 有三个构造方法,无论调用哪一个,都会配置默认的 Locale:

public FixedLocaleResolver() {setDefaultLocale(Locale.getDefault());
}
public FixedLocaleResolver(Locale locale) {setDefaultLocale(locale);
}
public FixedLocaleResolver(Locale locale, TimeZone timeZone) {setDefaultLocale(locale);
    setDefaultTimeZone(timeZone);
}

要么本人传 Locale 进来,要么调用 Locale.getDefault() 办法获取默认的 Locale。

再来看 resolveLocale 办法:

@Override
public Locale resolveLocale(HttpServletRequest request) {Locale locale = getDefaultLocale();
    if (locale == null) {locale = Locale.getDefault();
    }
    return locale;
}

这个应该就不必解释了吧。

须要留神的是它的 setLocaleContext 办法,间接抛异样进去,也就意味着 Locale 在前期不能被批改。

@Override
public void setLocaleContext( HttpServletRequest request, @Nullable HttpServletResponse response,
        @Nullable LocaleContext localeContext) {throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
}

3.4 CookieLocaleResolver

CookieLocaleResolver 和 SessionLocaleResolver 比拟相似,只不过存储介质变成了 Cookie,其余都差不多,松哥就不再反复介绍了。

4. 附录

搜刮了一个语言简称表,分享给各位小伙伴:

语言 简称
简体中文 (中国) zh_CN
繁体中文 (中国台湾) zh_TW
繁体中文 (中国香港) zh_HK
英语 (中国香港) en_HK
英语 (美国) en_US
英语 (英国) en_GB
英语 (寰球) en_WW
英语 (加拿大) en_CA
英语 (澳大利亚) en_AU
英语 (爱尔兰) en_IE
英语 (芬兰) en_FI
芬兰语 (芬兰) fi_FI
英语 (丹麦) en_DK
丹麦语 (丹麦) da_DK
英语 (以色列) en_IL
希伯来语 (以色列) he_IL
英语 (南非) en_ZA
英语 (印度) en_IN
英语 (挪威) en_NO
英语 (新加坡) en_SG
英语 (新西兰) en_NZ
英语 (印度尼西亚) en_ID
英语 (菲律宾) en_PH
英语 (泰国) en_TH
英语 (马来西亚) en_MY
英语 (阿拉伯) en_XA
韩文 (韩国) ko_KR
日语 (日本) ja_JP
荷兰语 (荷兰) nl_NL
荷兰语 (比利时) nl_BE
葡萄牙语 (葡萄牙) pt_PT
葡萄牙语 (巴西) pt_BR
法语 (法国) fr_FR
法语 (卢森堡) fr_LU
法语 (瑞士) fr_CH
法语 (比利时) fr_BE
法语 (加拿大) fr_CA
西班牙语 (拉丁美洲) es_LA
西班牙语 (西班牙) es_ES
西班牙语 (阿根廷) es_AR
西班牙语 (美国) es_US
西班牙语 (墨西哥) es_MX
西班牙语 (哥伦比亚) es_CO
西班牙语 (波多黎各) es_PR
德语 (德国) de_DE
德语 (奥地利) de_AT
德语 (瑞士) de_CH
俄语 (俄罗斯) ru_RU
意大利语 (意大利) it_IT
希腊语 (希腊) el_GR
挪威语 (挪威) no_NO
匈牙利语 (匈牙利) hu_HU
土耳其语 (土耳其) tr_TR
捷克语 (捷克共和国) cs_CZ
斯洛文尼亚语 sl_SL
波兰语 (波兰) pl_PL
瑞典语 (瑞典) sv_SE
西班牙语 (智利) es_CL

5. 小结

好啦,明天次要和小伙伴们聊了下 SpringMVC 中的国际化问题,以及 LocaleResolver 相干的源码,置信大家对 SpringMVC 的了解应该又更近一步了吧。

退出移动版