在网上找了很多后端国际化的资料,总是不适合自己。因为脑袋本能的就会取拒绝吸收知识,其实认真看理解思路i18n国际化其实很简单。
那么先简单上手一下
1.首先在resources创建i18n的文件夹,文件夹内创建各个国家的脚本
@Configuration
public class ExLangConfig{
/**
* 默认解析器 其中locale表示默认语言
* CookieLocalResolve 是获取Accept-Language的值,如果没有则为默认值
* 如果不是文本中的国家,则自动展示message中的文本。要是逻辑没有错的话
*/
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(Locale.getDefault());
return localeResolver;
}
/**
* 默认拦截器 其中lang表示切换语言的参数名
*/
@Bean
public WebMvcConfigurer localeInterceptor() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
localeInterceptor.setParamName("lang");
registry.addInterceptor(localeInterceptor);
}
};
}
}
2.首先说下这种配置,这种配置需要在url上添加?lang=zh_CN
spring:
thymeleaf:
cache: false #关闭页面缓存
prefix: classpath:/views/ #页面拼接路径
messages:
basename: static/i18n/message #相对路径 首部请勿添加斜杠
3.当然还要定义配置文件
4.前端拿到i8n的方式
然后我们就可以编辑后端访问前端的接口了,修改url后的lang值就可以实现国际化了。这种博客一搜一大堆。这里就不具体演示了。
·
·
·
你以为这么快就完了吗?实际工作比这个复杂多了。
我的需求是后端的异常国际化,用cookie中的lang值来判断具体返回的文本信息
首先分析一下:国际化(如上操作),异常处理(全局异常处理类),cookie(修改翻译文本与lang值得对应关系)。
看样子蛮简单的但是实际上有很多坑,加之自己的技术菜,每次都急于求成。。。这个需求变得异常头疼。
来看看是怎么实现的吧
第一个版本
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.nacos.common.utils.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
public class I18nLocalResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = null;
//获取请求中的语言参数
Cookie[] cookies = request.getCookies();
if (ObjectUtil.isNotEmpty(cookies)) {
for(Cookie cookie : cookies) {
String cookieName = cookie.getName();
if ("lang".equals(cookieName)) {
language = cookie.getValue();
}
}
}
//如果没有返回值就用默认的
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(language)){
if ("zh-CN".equals(language)) {
locale = new Locale("zh", "CN");
} else if ("ru".equals(language)) {
locale = new Locale("ru", "RU");
} else {
locale = new Locale("en", "US");
}
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
1.这里需要重写resolveLocale方法,这个方法的返回值会作用在翻译的文本语言上。
这里还需要注意的是,应为前端拿到的浏览器的语言是en,ru,zh-CN;和我们后端i18n设置的
zh-CN,ru_RU,en_US不同,所以需要特殊处理。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class I18nConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
return new I18nLocalResolver();
}
}
2.将我们重写的方法注入localeResolver
到了这一步,我们定义好翻译文本后,那么全局异常处理类怎么拿到翻译文本呢?
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
public class I18nMessage {
private static MessageSourceAccessor accessor;
private static MessageSource messageSource;
private static final Set set = new HashSet<>();
private static ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
static {
// 资源配置路径
addLocation("classpath*:i18n/*");
}
/**
* 初始化资源文件的存储器
*/
protected static void initMessageSourceAccessor() {
String[] basenames = new String[set.size()];
set.toArray(basenames);
ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
// 配置MessageSource Bean主要是配置资源文件的 baseNames,当然也可以通过 yml 属性spring.messages.basename配置
reloadableResourceBundleMessageSource.setBasenames(basenames);
reloadableResourceBundleMessageSource.setCacheSeconds(5);
reloadableResourceBundleMessageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
accessor = new MessageSourceAccessor(reloadableResourceBundleMessageSource);
messageSource = reloadableResourceBundleMessageSource;
}
/**
* 加载指定位置的资源文件
*
* @param locationPattern
*/
public static void addLocation(String locationPattern) {
try {
Resource[] resources = resourcePatternResolver.getResources(locationPattern);
for (Resource resource : resources) {
String url = resource.getURL().toString();
int lastIndex = url.lastIndexOf("/");
String prefix = url.substring(0, lastIndex + 1);
String suffix = url.substring(lastIndex + 1);
suffix = suffix.split("\\.")[0].split("_")[0];
set.add(prefix + suffix);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
initMessageSourceAccessor();
}
private I18nMessage() {
}
/**
* 获取messageSource
*
*/
public static MessageSource getMessageSource() {
return messageSource;
}
/**
* 获取国际化资源信息
*
* @param code 消息键
* @return String
*/
public static String getMessage(String code) {
return accessor.getMessage(code, LocaleContextHolder.getLocale());
}
/**
* 获取国际化资源信息
*
* @param code
* @param defaultMessage
*/
public static String getMessage(String code, String defaultMessage) {
return accessor.getMessage(code, defaultMessage, LocaleContextHolder.getLocale());
}
/**
* 获取国际化资源信息
*
* @param code
* @param args
* @return String
*/
public static String getMessage(String code, Object[] args) {
return accessor.getMessage(code, args, LocaleContextHolder.getLocale());
}
public static String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
return accessor.getMessage(code, args, defaultMessage, locale);
}
/**
* 获取国际化资源信息
*
* @param code
* @param args
* @param defaultMessage
* @return String
*/
public static String getMessage(String code, Object[] args, String defaultMessage) {
return accessor.getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale());
}
}
3.i18n里给我们了一个messageSource,我们可以从这个类中定义方法的参数和返回。
那么进入到异常处理类,找到具体需要处理报错的方法
String message = I18nMessage.getMessage(exceptionEnum.getCode(), args, MessageFormatter.arrayFormat(errMsg, argArr).getMessage());
//这是定义在i18n下具体的翻译文本,对应着枚举类的code
//zh_CN
AE000000={0} 不能为空
AE000001={0} 最大长度 {1}
AE000002={0} 范围大小 {1} - {2}
//en_US
AE000000={0} can not be empty
AE000001=List {0} max length {1}
AE000002={0} Range size {1} - {2}
//ru_RU
AE000000={0} не должен быть пустым
AE000001=Список {0}, максимальная длина {1}
AE000002={0} Размер диапазона {1}–{2}
4.将返回的信息包装一层,exceptionEnum.getCode()这个是枚举类中的code,我们将code与翻译文本相同。{0}为占位符,可以当作args传入I18nMessage.getMessage()方法
那么如何自定义报错语言,而不通过枚举类的code呢?
throw new BizException(PortalExceptionEnum.PATTERN_REGEXP, I18nMessage.getMessage("AE006.enName"));
5.直接用I18nMessage.getMessage(),在翻译文本定义AE006.enName
现在还有一种情况,就是带@Validated参数校验的怎么做呢?
@NotBlank(message = "联系人姓名不能为空")
private String contactName;
//改为
@NotBlank(message = "AE001.fieldContactName")
private String contactName;
然后一样再异常处理类中,定义字段检验报错的方法中,包一层i18nMessage。这里其实用的是继承@Validated的报错的类,ValidationException,然后重写里面的方法。
到这里国际化异常的差不多了,自己更改cookie值,然后测试就OK了。。。
直到我以为我弄完了的时候,组长跟我说需要修改一下,不用i18n了。改成直接使用枚举类,再后面多添加一些语言,然后报错的时候直接在全局异常里面判断lang值,返回对应枚举就行了。
所以我一开始为什么要用i18n呢?特此记录