【Java】i18n国际化解决方案:通过AOP切面实现多语言的配置

需求背景

国际化多语言配置。

相较于常规的方法,这次采取了切面的方式,来完成所有字段->不同语言的映射。

大致逻辑:
  1. 按常规的国际化,写一个获取语言的方法:getMsg(String code);

  2. 写一个深层遍历对象的方法traverseObject(Object obj),通过反射,获取所有类型为字符串的字段,并实现对字段的重新赋值;(此处需要额外处理对象内部有List和Map的情况)

    以上,就完成了国际化的基础:对某个对象进行遍历,获取其字符串属性的值,检查配置文件中是否有该值对应的国际化语言,有的话就重新赋值

    eg.
    * 配置文件中: xxModule.name = 名字
    * 业务逻辑层中: obj.setName("xxModule.name")
    * 然后通过遍历对象traverseObject(obj),获取到obj.name,然后检查配置文件中是否有对应的语言,getMsg(obj.name),有的话对该属性重新赋值

  3. AOP切面的配置:
    3.1 定义一个注解
    3.2 定义一个切面,写一个@AfterReturning的方法,并且限定该方法只对上面定义的注解生效
    3.3 @AfterReturning的方法中写入步骤1和步骤2的逻辑实现.

  4. 把该注解加在需要做国际化的controller方法上

    这样就完成了切面+国际化的配置,有时候觉得不那么靠谱,可能是我没想到更好的处理方法,大家谨慎参考。

!!!支持的场景有限:支持list,map,object(直接的object对象或者自定义class的实例,不支持单纯的string类型。

支持的几种返回值类型示例:
List: ["aaa","bbb","ccc"]
List: ["aaa","bbb",{name:"111",gender:"男性"}]
Map<String,Object>: {name:"aaa",sports:{type:1,name:"football"}},
Object,或者自定义类的实例:{type:1,name:"football"}

不支持的类型示例:
String: "name"
(这是因为递归遍历的方法是void,没有return的内容,而String类型的属性不能修改其属性值,大家可以根据业务情况重新写)

代码实现

1. 国际化基础配置
1.1 resources目录下新建目录i18n,然后在i18n中新建三个properties文件:

-- messages.properties
-- messages_en_US.properties
-- messages_zh_CN.properties

(框出来的是自动生成的,不需要手动建立)

请在.properties文件右下角检查文件的编码是否是utf-8

1.2 在application.yaml文件中添加spring配置:

spring:
messages:
encoding: utf-8
basename: i18n.messages

2. 国际化工具方法编写

创建文件i18nUtils.java

@Slf4j
public class I18nUtils {

    private static final MessageSource MESSAGE_SOURCE = SpringUtil.getBean(MessageSource.class);

    /**
     * 通过code获取对应语言
     *
     * @param code 配置文件中的key
     * @return 对应的语言 如果不存在则直接返回code
     */
    public static String get(String code) {
        try {
            return MESSAGE_SOURCE.getMessage(code, null, LocaleContextHolder.getLocale());
        } catch (Exception e) {
            return code;
        }
    }

    /**
     * 这个方法看起来没什么用......
     * 实际上用处也不是很大......
     * 主要用来log国际化操作耗费的时间和做一些其他的处理
     */
    public static void transformMsg(Object obj) {
        traverseObjectForLanguage(obj);
    }

    /**
     * 深层遍历对象
     */
    public static void traverseObjectForLanguage(Object object) {

        // 空对象处理:直接返回
        if (object == null) {
            return;
        }

        Class<?> clazz = object.getClass();

        String name = clazz.getName();

        // 如果第一次传来的object是个String
        if ("java.lang.String".equals(name)) {
            // String类型不能重新赋值 跳过
            System.out.printf("String 类型不能重新赋值,跳过");
            return;
        }

        // 基本类型的处理:如果对象是lang包下,又不直接属于Object类型,直接返回,主要为了绕过基本类型
        if (name.contains("java.lang") && !"java.lang.Object".equals(name)) {
            return;
        }

        // Map的处理:继续遍历map的value对象
        if (object instanceof Map) {
            for (String key : ((Map<String, Object>) object).keySet()) {
                Object mapValue = ((Map<String, Object>) object).get(key);
                // Map中value为String的处理
                if (mapValue instanceof String) {
                    ((Map<String, Object>) object).put(key, get(mapValue.toString()));
                } else {
                    // 否则继续进入下一轮遍历
                    traverseObjectForLanguage(mapValue);
                }
            }
        }

        // List的处理:继续遍历list内的值
        if (object instanceof List) {
            for (Object obj : (List) object) {
                if (obj instanceof String) {
                    // list里面有string类型的值 进行替换
                    int index = ((List<Object>) object).indexOf(obj);
                    ((List<Object>) object).set(index, get(obj.toString()));
                } else {
                    // 继续下一轮遍历
                    traverseObjectForLanguage(obj);
                }
            }
        }

        // Object类型或自定义类的实例的处理
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {

            // 不操作static 或者 final修饰的属性
            Boolean isStatic = Modifier.isStatic(field.getModifiers());
            Boolean isFinal = Modifier.isFinal(field.getModifiers());

            if (isStatic || isFinal) {
                continue;
            }

            // 开放属性的访问权限
            Boolean isAccess = field.isAccessible();
            field.setAccessible(true);

            try {
                Object value = field.get(object);
                if (value instanceof String) {
                    // check if string value exists in i18n config
                    String result = get((String) value);
                    field.set(object, result);
                } else {
                    // 字段值为内部对象
                    traverseObjectForLanguage(value);
                }

                // 还原属性的访问权限
                field.setAccessible(isAccess);
            } catch (Exception e) {
                throw new BusinessException("traverse object error:pls check the i18n annotation methods", ResultCode.UNKNOWN_ERROR);
            }
        }
    }
}

创建文件I18nAspectUtils.java,用来实现切面

@Aspect
@Component
public class I18nAspectUtils {
    @AfterReturning(returning = "object", value = "@annotation(I18nAspectConfig)")
    public void doAfterReturning(Object object) {
        I18nUtils.transformMsg(object);
    }
}

创建文件I18nAspectConfig,用来实现注解

@Retention(RetentionPolicy.RUNTIME)
public @interface I18nAspectConfig {

}

到这里,注解+AOP就完成了。

运用:在controller方法上添加注解@I18nAspectConfig就可以。

posted on 2024-03-18 13:59  northwest  阅读(249)  评论(0编辑  收藏  举报

导航