(八)从零搭建后端框架——听起来高大上的国际化

1,179 阅读4分钟

前言

第一次听到国际化这个词,都会觉得它相当的高大上。

国际化它到底是什么?

国际化的目的是使同一个项目能够在不同地区运行。说简单点,页面上的文字或提示信息能够随着语言环境而改变。

国际化如何实现?

这里分以下几步:

  1. 语言的保存和切换
  2. 获取国际化数据
  3. 统一处理返回结果

具体实现

语言的保存和切换

要想实现语言的保存和切换,需要实现LocaleResolver和LocaleChangeInterceptor。如下:

@Slf4j
@Configuration
public class I18nConfig implements WebMvcConfigurer {

    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setDefaultLocale(Locale.CHINA);
        return localeResolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}
  • LocaleResolver用于存储语言,这里将语言的信息保存在Cookie中,所以使用现成的实现CookieLocaleResolver。 如果需要将语言保存在Session中,可以使用SessionLocaleResolver。

  • LocaleChangeInterceptor是拦截器,用于切换语言。若请求中有lang参数,就会根据该值切换语言。 通过实现WebMvcConfigurer的addInterceptors方法,将该拦截器放入拦截器链中。

实现后,我们测试下:

@RestController
@RequestMapping("/language")
public class LanguageController extends BaseController {

    @ApiOperation("切换语言")
    @PostMapping("/change")
    public ApiResult<String> changeLanguage(@RequestParam(name = "lang") @ApiParam(name = "lang", value = "语言", required = true) String lang) {
        // 在LocaleChangeInterceptor中已切换,此处无需处理
        return getLanguage();
    }

    @ApiOperation("获得当前语言")
    @GetMapping
    public ApiResult<String> getLanguage() {
        Locale locale = LocaleContextHolder.getLocale();
        return ApiResult.success(locale.toString());
    }
}

调用切换语言的接口:curl -X POST "http://localhost:8080/language/change?lang=en_US"

可以在Cookie中看到语言信息:

Cookie

调用获得当前语言接口:curl -X GET "http://localhost:8080/language" -H "accept: */*"

可以获取返回结果:

{
  "code": 200,
  "data": "en_US",
  "message": "成功"
}

至此,我们成功的实现了语言的保存和切换。

下面来实现根据语言获取相应的国际化数据。

获取国际化数据

要想获取国际化数据,首先需要添加国际化文件。默认的文件名为messages*.properties,也可以通过配置spring.messages.basename=messages修改文件名。存放的文件路径为:/resources/i18n/,如下:

国际化文件

在文件中以键值对的形式存储数据。

默认数据:messages.properties

success=API调用成功
fail=API调用失败

英文数据:messages_en_US.properties

success=success
fail=fail

中文数据:messages_en_US.properties

success=API调用成功
fail=API调用失败

国际化数据有了,语言有了,那如何获取到当前语言的数据?

这里使用ResourceBundleMessageSource来解析国际化文件,如下:

@Slf4j
@Configuration
public class I18nConfig{

    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}

需要获取数据时,直接注入进行使用:

@RestController
@RequestMapping("/message")
public class MessageController {

    @Autowired
    private MessageSource messageSource;

    @ApiOperation("成功")
    @GetMapping("/success")
    public ApiResult<String> success() {
        // 获取当前语言
        Locale locale = LocaleContextHolder.getLocale();
        // 根据Code值和语言,获取国际化数据
        String message = messageSource.getMessage(ResultCode.SUCCESS.getMessage(), null, locale);
        return new ApiResult<>(ResultCode.SUCCESS.getCode(), null, message);
    }

    @ApiOperation("失败")
    @GetMapping("/fail")
    public ApiResult<String> failure() {
        Locale locale = LocaleContextHolder.getLocale();
        String message = messageSource.getMessage(ResultCode.FAIL.getMessage(), null, locale);
        return new ApiResult<>(ResultCode.SUCCESS.getCode(), null, message);
    }
}
  • LocaleContextHolder.getLocale(); 获取当前语言
  • messageSource.getMessage(); 获取国际化数据

这里用到ResultCode:

@Getter
@AllArgsConstructor
public enum ResultCode {

    SUCCESS(200, "success"),

    FAIL(500, "fail");

    private Integer code;

    private String message;
}

至此,我们能成功的获取国际化数据。

但是,每个请求都需要调用,不觉得很麻烦吗?

程序员可不是只会Ctrl+C和Ctrl+V的。一般将提示信息返回到前端时才会调用国际化,我们可以对返回结果进行统一处理来解决这个问题。

统一处理返回结果

【统一基类、接口、返回对象设计】这一篇文章中, 约定了接口的返回对象都是ApiResult,那我们只要把该返回值拦截到,然后统一进行国际化即可。

通过注解@ControllerAdvice和实现ResponseBodyAdvice接口, 数据返回前都调用beforeBodyWrite方法,在这里我们将code值转换成国际化数据。如下:

@ControllerAdvice
public class ApiResultAdvice implements ResponseBodyAdvice<ApiResult> {

    @Autowired
    private MessageSource messageSource;

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.getParameterType().isAssignableFrom(ApiResult.class);
    }

    @Override
    public ApiResult beforeBodyWrite(ApiResult body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        Locale locale = LocaleContextHolder.getLocale();
        String message = messageSource.getMessage(body.getMessage(), null, locale);
        body.setMessage(message);
        return body;
    }
}

在这里统一处理之后,就不需要在Controller进行处理。

调用切换语言的接口:curl -X POST "http://localhost:8080/language/change?lang=en_US"

返回结果如下:

{
  "code": 200,
  "data": "en_US",
  "message": "success"
}

至此,我们实现了国际化,并且不需要对以前的代码进行多大的改动。 只需要将原来的提示信息等数据换成Code值,然后将Code值在国际化文件里配置即可。

总结

首先通过LocaleResolver和LocaleChangeInterceptor,实现了对语言的保存和切换。

然后通过MessageSource,实现了国际化数据的读取。

最后通过注解@ControllerAdvice和实现ResponseBodyAdvice,接口实现对返回结果的统一处理。

至此,完成了多国际化的实现,并尽可能的使使用简单,尽可能减少对以前代码的改动。

感谢阅读,如果感觉有帮助的话,不妨随手点个赞!

源码

github.com/zhuqianchan…

往期回顾