java后端国际化

1,094 阅读6分钟

最近公司拓展海外市场,要做海外版app,因此需要做国际化,在此对后端国际化的实现过程做了梳理。

几个改动方面

首先整理了项目中需要国际化的地方,大体来说有异常提示,入参非空校验,枚举类返回等。

这几种方式的代码写法如下:

1.异常提示:

throw new FeignException(UserResultCode.*ADD_COLLECT_FAILED*.getCode(),  
        I18nUtils.*i18n*("UserResultCode.ADD_COLLECT_FAILED"));

2.入参非空校验:

@NotNull(message = "{reqData.goodsId}")
private Long goodsId;

3.枚举类:

public enum PayStatus {

    PAY_STATUS_1(1, "待支付"),

    PAY_STATUS_2(2, "部分支付"),

    PAY_STATUS_3(3, "全部支付"),

    ;

    private Integer key;

    private String value;

    PayStatus(Integer key, String value) {

        this.key = key;

        this.value = value;

    }

    public Integer getKey() {

        return key;

    }

    public void setKey(Integer key) {

        this.key = key;

    }

    public String getValue() {

        return  I18nUtils.tryI18n("payStatusEnum"+"."+this.getDeclaringClass().getSimpleName()+"."+this.name());

    }

    public void setValue(String value) {

        this.value = value;

    }

}

具体实现思路

如下:

1. 配置文件添加配置,指定国际化文件路径,编码。


 spring:

   messages:

     basename: i18n/messages

     cache-duration: -1

     encoding: UTF-8

     fallback-to-system-locale: true

2.在指定的路径下创建国际化配置文件。

1.png

3.创建国际化工具类,使用该工具类进行文案的国际化。

@Component

public class I18nUtils {

    private static MessageSource messageSource;

    @Autowired
    public void setMessageSource(MessageSource messageSource) {

        I18nUtils.messageSource = messageSource;

    }

    public static String i18n(String code, Object... args) {

        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());

    }

    public static String i18nOrDefault(String code, String defaultMessage, Object... args) {

        return messageSource.getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale());

    }

    @NonNull
    public static String tryI18nOrDefault(@NonNull String code,String defaultMessage, @NonNull Object... args) {

        String res;

        try {

            res = i18n(code, args);

        } catch (Exception ignored) {

            res = code;

            if(StringUtils.hasLength(defaultMessage)){

                res = defaultMessage;

            }

        }

        return res;

    }

    @NonNull
    public static String tryI18n(@NonNull String code, @NonNull Object... args) {

        String res;

        try {

            res = i18n(code, args);

        } catch (Exception ignored) {

            res = code;

        }

        return res;

    }

}

4.创建配置类。

@Configuration

public class LocaleConfig implements WebMvcConfigurer {

    @Bean
    public SessionLocaleResolver localeResolver() {

        SessionLocaleResolver localeResolver = new SessionLocaleResolver();

        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);

        return localeResolver;

    }

    @Bean
    public WebMvcConfigurer localeInterceptor() {

        return new WebMvcConfigurer() {

            @Override

            public void addInterceptors(InterceptorRegistry registry) {

                registry.addInterceptor(new LocaleInterceptor());

            }

        };

    }

    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {

        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();

        // 设置消息源

        bean.setValidationMessageSource(resourceBundleMessageSource());

        return bean;

    }

    @Bean
    public MessageSource resourceBundleMessageSource() {

        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();

        messageSource.setDefaultEncoding(StandardCharsets.UTF_8.toString());

        // 多语言文件地址

        messageSource.addBasenames("i18n/messages");

        return messageSource;

    }

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {

        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();

        processor.setValidator(localValidatorFactoryBean().getValidator());

        return processor;

    }

    @Override
    public Validator getValidator() {

        return localValidatorFactoryBean();

    }

}

5.创建拦截器,获取请求头中的语言类型。

@Slf4j

public class LocaleInterceptor extends LocaleChangeInterceptor {

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

        String newLocale = request.getHeader(GrayscaleConstant.DW_LANG);

        if(StringUtils.isEmpty(newLocale)){

            newLocale = "zh_CN";

        }

        LocaleContextHolder.setLocale(parseLocaleValue(newLocale),true);

        return true;

    }

}

国际化原理

spring-webmvc提供了支持国际化的i18n包,可以根据前端传来的参数动态读取配置。 MessageSource 接口定义了获取国际化消息的方法,而 ResourceBundleMessageSource 是其默认实现类。

2.png

ResourceBundleMessageSource类使用 java.util.ResourceBundle 来加载和解析属性文件作为国际化资源。以下是其关键源码逻辑:

3.png

LocaleResolver 接口用于解析用户的区域设置(Locale),以确定使用哪种语言。而 AcceptHeaderLocaleResolver 是 LocaleResolver 接口的默认实现类。

4.png

在它们的抽象子类实现中提供了默认的Locale对象(Locale是java.util包下提供了所有语言和国家地区映射关系的枚举对象)。

5.png

AcceptHeaderLocaleResolver 是根据请求头中的 Accept-Language 头部信息来解析用户的首选语言。

6.png

请求头中没有Accept-Language字段会用默认的语言环境,如果有,会从request对象中获取Locale对象,如果当前解析器不支持该类型的语言对象(即!supportedLocales.contains(requestLocale)),那么findSupportedLocale会在已经支持的Locale集合里和request.getLocales()里寻找到一对locale.getLanguage()相同的结果,并返回当前的Locale对象。

7.png

ReloadableResourceBundleMessageSource类负责读取配置文件。

8.png

calculateAllFilenames方法负责根据用户指定的basename名称解析出绑定了具体语言、区域的资源配置文件。

9.png

当根据basename找不到对应的Loadle映射的资源文件名时,calculateFilenamesForLocale方法会根据当前传入的Locale对象和basename解析出对应的资源文件名。

10.png

getProperties方法源码:

11.png

加载国际化资源文件:

12.png

然后将PropertiesHolder对象和文件名的映射关系保存到全局变量cachedProperties中去:

13.png

同一个basename下的其它资源配置文件会在遍历中被逐个加载。 这时,回到resolveCode方法,在通过getProperties方法获取了PropertiesHolder对象后,直接getMessageFormat调用即可。

14.png

这里返回的是一个MessageFormat对象。 而PropertiesHolder是作为ReloadableResourceBundleMessageSource的一个内部类存在。

15.png

重点看下getMessageFormat方法,因为国际化的功能都是通过此方法来获取资源值的:

16.png

通过properties.getProperty获取到code对应的配置值msg,然后再将这个msg和Locale对象封装成一个MessageFormat对象result,又将result和locale再做一层映射关系保存到localeMap,再将这个localeMap和code做绑定存储到ConcurrentMap<String,Map<Locale,MessageFormat>>中。

 17.png

spring-boot-autoconfigure:2.2.5.RELEASE的META-INF/spring.factories文件中对MessageSourceAutoConfiguration类进行自动装配:

18.png

messageSource方法配置了@Bean注解并返回了ResourceBundleMessageSource对象,所以在spring boot中资源文件是由ResourceBundleMessageSource类去解析的。

DispatcherServlet类在初始化方法中,会拿到语言环境解析器的bean:

19.png

initLocaleResolver方法如下: 20.png

DispatcherServlet类继承了父类FrameworkServlet,在父类实现中,完成了上下文和环境等一系列的初始化工作 21.png

同时重写了HTTP请求的八大方法,让请求调用统一的前置处理器,再进入到子类DispatcherServlet的doService中: 22.png

前置处理器processRequest如下:

23.png

processRequest通过LocaleContextHolder获取LocaleContext对象,然后通过buildLocaleContext方法获取Locale。

24.png

buildLocaleContext方法从DispatcherServlet初始化时拿到的LocaleResolver去调用了resolveLocale方法获取Locale对象,如果没有的话就直接取HttpServletRequest中的Locale返回,然后this.initContextHolders方法将解析后的Locale对象设值到LocaleContextHolder中:

25.png

通常语言环境解析器会配合LocaleChangeInterceptor一起使用,LocaleChangeInterceptor是i18n包下提供的拦截器,可以通过setParamName方法设置该拦截器的拦截参数名,它的preHandle方法会从请求中获取设置的拦截参数值,并设置到LocaleResolver里去:

26.png

总的来说步骤如下:

  1. MessageSource 和 ResourceBundleMessageSource:

   - MessageSource 接口定义了获取国际化消息的方法,其中最常用的方法是 getMessage()

   - ResourceBundleMessageSource 是 MessageSource 接口的默认实现类,用于加载和解析属性文件(.properties)作为国际化资源。

  1. LocaleResolver 和 AcceptHeaderLocaleResolver:

   - LocaleResolver 接口定义了解析用户区域设置(Locale)的方法,其中最常用的方法是resolveLocale()。

   - AcceptHeaderLocaleResolver 是 LocaleResolver 接口的默认实现类,根据请求头中的 Accept-Language 头部信息解析用户的首选语言。

  1. LocaleContextHolder:

   - LocaleContextHolder 是一个用于存储当前用户区域设置的上下文工具类。它使用 ThreadLocal 来保存当前的 Locale,确保在应用程序的任何地方都可以访问当前的区域设置。

   - LocaleContextHolder类提供了静态方法 getLocale() 和 setLocale() 用于获取和设置当前的 Locale。

  1. 配置文件加载:

   - Spring Boot 的配置文件(如 application.properties 或 application.yml)中可以设置 spring.messages.basename 属性来指定国际化资源文件的基本名称。

   - 通过加载配置文件,Spring Boot 会在内部创建一个 MessageSource 实例,并将基本名称传递给 ResourceBundleMessageSource。

  1. 获取国际化消息的源码调用链:

   - 在代码中,通过注入 MessageSource 实例来获取国际化消息。

   - 具体调用链通常涉及以下类和方法:Controller -> Service -> MessageSource.getMessage()。

作者:洞窝-廷双