美文网首页
全网最骚SpringBoot国际化i18n配置

全网最骚SpringBoot国际化i18n配置

作者: 识1DD编程 | 来源:发表于2021-06-17 16:42 被阅读0次

    前言

    忽然来了个需求让我搞国际化配置,通过添加header确定返回哪种语言信息。个人认知里信息国际化无非是常量信息的分类,根据相应环境进行返回,大致的返回流程就两步:

    1. 获取信息key
    2. 根据当前语言环境与信息key返回相应语言信息

    根据该流程想出了两种实现方式:

    • 通过类进行语言划分,各语言信息与key都写进常量类中,通过Map映射返回
    • 将信息都写到配置文件中,根据环境与key读取文件返回,这块SpringBoot也有相应的支持

    本来想着项目代码这么烂,要不就按第一种算了,都是常量信息的整理,都是无法缩减的硬工作量,相比用SpringBoot的方式可以少一些配置。但尝试开展时发现了以下问题:

    1. 信息枚举类里存各种语言信息属性
    2. 做两层映射,各国语言常量:<信息key:各信息枚举>

    但存在以下问题:

    • 语言扩展很不方便,设信息类包含了各种语言的信息,但有新语言时就要修改硬编码在信息枚举类中添加新语言信息属性(虽然个人不认为不会再有扩展)
    • 信息枚举类会很难看(预料之中)

    虽然我不认为项目语言还会有扩展,但编码上的可扩展性一直都是个人的看重点,于是放弃了语言信息存类中的想法,转而考虑存文件中。
    SpringBoot提供了LocaleResolver来根据语言解析相应properties文件返回语言信息的功能,hibernate参数校验返回相应的语言信息便是据此实现,但需创建相应语言的资源文件,而个人期望放到一个或少量文件中,且有更好的想法,于是放弃了常见的SpringBoot i18n实现方式。

    实现思路

    信息国际化只是针对语言与信息key进行信息分类映射返回,而Spring Boot 2.0起后就支持通过配置文件进行Map属性的注入,于是个人就以Map属性注入方式创建了所需的国际化类:

    1. (核心)项目初始化时将key与各国语言信息的映射注入到Map,后端不维护语言类型常量,由前端header传参决定
    2. 业务异常ApiException抛出时抛出的是key而非message
    3. 业务拦截器GlobalExceptionHandler根据key与language header取相应message

    由此添加了以下i18n类:

    • 接口I18NKey:返回信息key。所有信息key枚举需实现的接口,枚举根据业务进行分类
    • 组件类I18NMessage:国际化信息类,根据配置文件进行信息自动注入,根据I18NKey与语言类型返回相应信息

    此外,需添加通过I18NKey构造ApiException方法,更改拦截器GlobalExceptionHandler的信息返回处理。

    示例与相关类

    请求示例流程:

    1. 前端携带语言类型header请求
    2. 后端业务逻辑通过ApiAssert断言到异常时抛出message为I18NKey中key字符串的ApiExceptionApiException中的message一律视为i18n字符串key处理)
    3. GlobalExceptionHandler获取ApiExceptionmessage,通过注入的I18NMessage根据语言类型header将该message/key转化为实际所需的message,如果I18NMessage中的信息映射没匹配到该key,则视为message直接返回

    i18n信息配置文件 application-i18n.yaml

    业务的划分可以通过在配置文件中添加注释或key添加前缀实现,可随意扩展语言信息

    i18n:
      # 若前端无header传参则返回中文信息
      default-lang: cn
      message:
        # admin
        password_not_null:
          en: Password can't be null
          cn: 密码不能为空
        password_cannot_same:
          en: The old and new passwords cannot be the same
          cn: 新旧密码不能相同
          xx: 随意扩展语言,啥都行,前端header值能匹配上就行
    

    i18n信息类I18NMessage

    @Data
    @Component
    @ConfigurationProperties("i18n")
    public class I18NMessage {
        /**
         * message-key:<lang:message>
         */
        private Map<String, Map<String, String>> message;
        /**
         * Default language setting (Default "cn").
         */
        private String defaultLang = "cn";
    
        /**
         * get i18n message
         *
         * @param key
         * @param language
         * @return
         */
        public String message(I18NKey key, String language) {
            return Optional.ofNullable(message.get(key.key()))
                    .map(map -> map.get(language == null ? defaultLang : language))
                    .orElse(key.key());
        }
    
        /**
         * get i18n message
         *
         * @param key
         * @param language
         * @return
         */
        public String message(String key, String language) {
            return Optional.ofNullable(message.get(key))
                    .map(map -> map.get(language == null ? defaultLang : language))
                    .orElse(key);
        }
    }
    

    I18NKey

    public interface I18NKey {
        /**
         * get i18n message key
         *
         * @return
         */
        String key();
    }
    

    AdminI18NKey

    @AllArgsConstructor
    public enum AdminI18NKey implements I18NKey {
        /**
         * admin i18n message key enum
         *  秘诀:IDEA CTRL+SHIFT+U可转大小写
         */
        PASSWORD_NOT_NULL("password_not_null") ,
        NEW_OLD_PASSWORD_CANNOT_SAME("password_cannot_same") ;
    
        // 需与配置文件中的信息key值一致
        private final String key;
    
        @Override
        public String key() {
            return key;
        }
    }
    

    业务异常类ApiException

    @Getter
    public class ApiException extends RuntimeException {
        private final Integer code;
        ......
        public ApiException(I18NKey key) {
            super(key.key());
            this.code = HttpCodeMsg.SERVER_ERROR.code();
        }
        ......
    }
    

    ApiException断言类ApiAssert

    /**
     * Assert to throw ApiException
     * @author Wilson
     */
    public final class ApiAssert {
    
        ......
    
        /**
         * Check is a not equals to b.
         *
         * @param a
         * @param b
         * @param key
         * @throws ApiException collection is empty
         */
        public static <E> void notEquals(Object a, Object b, I18NKey key) {
            if (Objects.equals(a, b)) {
                throw new ApiException(key.key());
            }
        }
    }
    

    全局异常处理器GlobalExceptionHandler

    @Slf4j
    @ResponseBody
    @RestControllerAdvice
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    @AllArgsConstructor
    public class GlobalExceptionHandler {
        private final I18NMessage i18NMessage;
        private final HttpServletRequest request;
        private static final String I18N_HEADER = "Lang";
        
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(value = ApiException.class)
        public ServerResponse<?> businessExceptionHandler(ApiException e) {
            log.error("业务错误: {}", e.getMessage());
            // 通过I18NMessage进行消息映射返回
            return ServerResponse.of(e.getCode(), i18NMessage.message(e.getMessage(),request.getHeader(I18N_HEADER)));
        }
    
        ......
    }
    

    AdminController示例

    @RestController
    @RequestMapping("/admin")
    public class AdminController {
        @PutMapping("/password")
        public ServerResponse<?> changePassword(@RequestParam String old, @RequestParam String newPassword,
                                                @RequestHeader(value = "Lang", required = false) String lang) {
            ApiAssert.notEquals(old, newPassword, AdminI18NKey.NEW_OLD_PASSWORD_CANNOT_SAME);
            return ServerResponse.success();
        }
    }
    
    example

    总结

    1. SpringBoot的Map注入用处很广泛的,你甚至可以用来一直套娃,这里就有两层示例了
    2. 没啥技术难度,思路骚点就行,就是常量的映射处理罢了,没啥额外知识带来的负担(躺平最爱)
    3. yaml文件的配置分类可比properties香多了,拒绝反驳。不搞啥不同语言不同properties文件,全部放一个不香吗?业务信息多了根据业务分类不香吗?application-i18n-admin.yaml看起来不比admin_en.properties + admin_zn_cn.properties + ..... 简便吗?方便CV与CTRL+D

    相关文章

      网友评论

          本文标题:全网最骚SpringBoot国际化i18n配置

          本文链接:https://www.haomeiwen.com/subject/lxbcyltx.html