【SpringBoot】自定义注解<约定式i18n国际化>终极升级版方案源码Copy

零、前言

        在后端对于 SpringBoot 的 数据库数据,需要国际化的字段和主要显示字段是分离的,为了避免大耦合性,与用户端的国际化字段处理问题,统一采用主要显示数据的实体字段。为此,我设计了一套解决方案,通过自定义注解或者直接对应实体类进行国际化的管理类。

一、源码

码云地址https://gitee.com/binbinbui/i18n/tree/master/

 

adapter 适配器包

AcceptLanguageAdapterInterface 适配器接口

  • 用于约束适配的方法定义。目前只有一个方法用于返回约束的枚举类型,这个枚举类型即为 <国际化前缀字段>
public interface AcceptLanguageAdapterInterface {

    I18nPrefixFieldEnum convert(String language);

}

 I18nAcceptLanguageAdapter 适配器实现类

  • 用于将传递过来的语言参数,转换为特定的国际化实体类前缀字符串,这里返回的是限定的枚举类型。(前缀:实体类中的国际化字段前缀)
import org.springframework.stereotype.Component;

import java.util.EnumSet;

@Component
public class I18nAcceptLanguageAdapter implements AcceptLanguageAdapterInterface {

    /**
     * 通过用户端发送的语言字段,转换成指定的可使用国际化前缀
     *
     * @param language 请求头 Accept-Language 参数
     * @return 指定 Bean 实体对应的国际化前缀枚举类型
     */
    @Override
    public I18nPrefixFieldEnum convert(String language) {
        I18nPrefixFieldEnum result = I18nPrefixFieldEnum.DEFAULT;
        if (language != null) {
            if (containsLanguage(ChineseEnum.enums, language)) {
                result = I18nPrefixFieldEnum.CHINESE;
            } else if (containsLanguage(EnglishEnum.enums, language)) {
                result = I18nPrefixFieldEnum.ENGLISH;
            } else { //其它语言,一律设置英语
                result = I18nPrefixFieldEnum.ENGLISH;
            }
        }
        return result;
    }

    /**
     * 因为设计截取时比较简单,浏览器传递过来的参数是多组的,且可以选择多种,而且还有权重,这里就直接截取开头
     * 如:Accept-Language: zh-CN,zh;q=0.9,en-US,en;q=0.8
     * 解释:
     *      zh-CN: 表示首选的语言是简体中文(中国大陆)。
     *      zh;q=0.9: 表示其次选项是中文,权重为0.9。这里没有特定指定地区,可以根据浏览器和操作系统的设置而定。
     *      en-US: 表示再次选项是美国英语。
     *      en;q=0.8: 表示最后选项是英语,权重为0.8。
     *
     * @param es 枚举类里的饿汉式全部枚举值
     * @param language Accept-Language 参数,用户端是可以指定对应显示设置枚举常量的前缀字段的,如 english
     * @return 是否包含前缀值
     * @param <T> 枚举类型且 EnumGetValue 类型
     */
    private <T extends Enum<T> & EnumGetValue> boolean containsLanguage(EnumSet<T> es, String language) {
        language = language.trim();
        boolean result = false;
        for (T e : es) {
            if (language.startsWith(e.getValue()) || //判断前缀
                language.startsWith(e.getValue().toLowerCase()) || //小写
                language.startsWith(e.getValue().toUpperCase())) { //大写
                result = true;
                break;
            }
        }
        return result;
    }

}

annotation 自定义注解包

I18n 自定义注解接口

  • 范围:方法上 | 类上
  • resourceType 参数:非必须,用于指定实体类的类型。
  • targetType      参数:非必须,用于指定实体类转换的目标类型。
  • prefix              参数:非必须,默认 false,如果为 true 不管有没有指定 (resourceType & targetType),都通过 <国际化前缀> 进行转换。

        这个注解对应的切面类,不仅仅只有对于国际化字段映射主字段的功能,还能直接转换对应的实体类返回给用户端,从而去除不必要的参数字段多余返回。

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface I18n {

    /**
     * @return 指定的需要转换 VO & 国际化 的原类型
     */
    Class<?> resourceType() default Object.class;
    /**
     * @return 指定的需要转换 VO & 国际化 的目标类型
     */
    Class<?> targetType() default Object.class;

    /**
     * @return 如果使用前缀设置,那将直接字段包含前缀字段的直接成立转换
     */
    boolean prefix() default false;
}

enums 枚举包

HttpRequestHeaderAcceptXEnums 枚举类

  • 请求头的 HTTP 标准固定参数 "Accept-Language"。
  • 浏览器在发送请求时,会默认携带本地操作系统使用的语言类型进行设置到请求头并传递,所以在这里仅仅是固定约束。
import lombok.Getter;

@Getter
public enum HttpRequestHeaderAcceptXEnums { //请求头的 HTTP 标准固定参数

    ACCEPT_LANGUAGE("Accept-Language");

    private final String value;

    HttpRequestHeaderAcceptXEnums(String acceptLanguage) {
        this.value = acceptLanguage;
    }
}

ChineseEnum 枚举类

  • 此枚举类型的定义约束主要作用就判断是否是中文。
import lombok.Getter;

import java.util.EnumSet;

@Getter
public enum ChineseEnum implements EnumGetValue {

    ZH_DF("chinese"), //自定义的默认前缀,当用户端手动切换时
    ZH_CN("zh-CN"),
    ;

    private final String value;
    public static final EnumSet<ChineseEnum> enums = EnumSet.allOf(ChineseEnum.class);

    ChineseEnum(String language) {
        value = language;
    }

}

EnglishEnum 枚举类

  • 此枚举类型的定义约束主要作用就判断是否是英文。
import lombok.Getter;

import java.util.EnumSet;

@Getter
public enum EnglishEnum implements EnumGetValue {

    EN_DF("english"), //默认英语的前缀,这个是自己定义的实体类国际化前缀(当用户手动切换时)
    EN_US("en-US")
    ;

    private final String value;
    public static final EnumSet<EnglishEnum> enums = EnumSet.allOf(EnglishEnum.class);

    EnglishEnum(String language) {
        value = language;
    }

}

EnumGetValue 接口

  • 约束 enums --> language 语言包下的获取当前枚举实例的值,主要是方便获取,不定义都行。
public interface EnumGetValue {

    String getValue();

}

I18nPrefixFieldEnum 枚举类

  • 用于约束定义获取我在如 MySQL 中对应的 Bean 实体类的国际化前缀字段。

        这里的前缀字段只有一个,"english" ,因为默认设置的是 中文,这里搞笑的是中文写了两个默认值 null 即可,因为在国际化处理时无需处理逻辑,所以指定为 null。

import lombok.Getter;

@Getter
public enum I18nPrefixFieldEnum {

    ENGLISH("english"),
    CHINESE(null),
    DEFAULT(I18nPrefixFieldEnum.CHINESE.getValue()),
    ;

    private final String value;

    I18nPrefixFieldEnum(String language) {
        value = language;
    }

}

aspect 切面类包(关键逻辑)

I18nAspect 切面类

  • 功能:不管你的实体类被嵌套在哪种指定范围的类型内,都能给你搜索出来,并进行你想要的国际化设置,且最终转换成指定类型的 Bean 实体类进行返回给用户端!
  • 算法设计:递归版本 与 非递归版本,非递归版本为最终的算法优化设计(使用的是双 Stack 栈设计),保留递归版本是给大家看的哈。(使用递归设计是因为在进行 Bean 实体转换时,反序列化 Field 字段对象需要用到它对应所属的对象 Object ,也就是说,我们必须先把 实体类的包含逻辑的最低层的实体对象进行 Bean 转换了,才能将上一层转换,要不然 Field 将设置成功但值不是原值。)
  • 代码做了一些逻辑优化,但有待更高,因为本身是进行 DFS 深度优先搜索,看你怎么改咯!
package i18n.aspect;

import i18n.adapter.I18nAcceptLanguageAdapter;
import i18n.annotation.I18n;
import i18n.enums.HttpRequestHeaderAcceptXEnums;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.*;

@Aspect
@Component
/*
 * 请确保被国际化实体或被包含国际化实体的类型修饰为 Object 类型 或内置 集合类型 的顶层类型 修饰的,而不是具体自定义类型
 * 搜索并国际化实例与子实例以此类推所有的国际化前缀映射到主字段
 */
public class I18nAspect {

    private final String REQUEST_HEADER_KEY = HttpRequestHeaderAcceptXEnums.ACCEPT_LANGUAGE.getValue();

    @Resource
    private I18nAcceptLanguageAdapter i18nAcceptLanguageAdapter;

    // 方法级别的切点
    @Pointcut("@annotation(i18n)")
    public void annotatedWithTestAnnotation(I18n i18n) {}

    // 类级别的切点
    @Pointcut("@within(i18n)")
    public void withinTestAnnotation(I18n i18n) {}

    // 组合切点,处理两个条件的逻辑 (有 BUG ,null 指针注解实例问题)
    //    @Pointcut("(execution(* *(..)) && @annotation(testAnnotation)) || @within(testAnnotation)")
//    @Pointcut(value = "annotatedWithTestAnnotation(testAnnotation) || withinTestAnnotation(testAnnotation)", argNames = "testAnnotation")
//    public void testAnnotationPointcut(TestAnnotation testAnnotation) {}

    @Around(value = "withinTestAnnotation(i18n)", argNames = "proceedingJoinPoint, i18n")
    public Object aroundAdviceClass(ProceedingJoinPoint proceedingJoinPoint, I18n i18n) throws Throwable {
        return aroundAdvice(proceedingJoinPoint, i18n);
    }

    @Around(value = "annotatedWithTestAnnotation(i18n)", argNames = "proceedingJoinPoint, i18n")
    public Object aroundAdviceMethod(ProceedingJoinPoint proceedingJoinPoint, I18n i18n) throws Throwable {
        return aroundAdvice(proceedingJoinPoint, i18n);
    }

    private Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint, I18n i18n) throws Throwable {
        // Before
//        System.out.println("========== AroundAdvice Before Advice ==========");
//        System.out.println("当前执行类全限定名: "+ proceedingJoinPoint.getTarget().getClass().getName());
//        System.out.println("当前执行类: "+ proceedingJoinPoint.getTarget().getClass().getSimpleName());
//        System.out.println("方法名: "+ proceedingJoinPoint.getSignature().getName());
        //获取方法传入参数列表
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
//        String[] parameterNames = methodSignature.getParameterNames();
        Object[] args = proceedingJoinPoint.getArgs();
//        System.out.println("方法传入参数列表: " + Arrays.toString(args) + " length: " + args.length);
        //获得 HttpServletRequest 对象 并从请求头中获取 key == REQUEST_HEADER_KEY 的前缀值
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String language = i18nAcceptLanguageAdapter.convert(request.getHeader(REQUEST_HEADER_KEY)).getValue();
        System.out.println("language == " + request.getHeader(REQUEST_HEADER_KEY) + "  convert == " + language); //浏览器自动传递的 语言字段,我们需要自行映射转换成指定 Bean 实体里的语言字段
        // Method Running
        Object proceed = proceedingJoinPoint.proceed(args);
        //对返回结果进行转换
//        DFSFindField(proceed, proceed, null, null, language, i18n);
        DFSStackFindField(new Node(proceed, proceed, null, null), language, i18n);
        return proceed;
    }

    /**
     * ---- 扫描的引用类型设置 ----
     * 如果实体中有需要国际化的字段,请保证该字段是 Object 类型 或内置 集合类型 的顶层类型 修饰的,而不是具体自定义类型
     * @param clazz 类型参数
     * @return 类类型 | 引用类型 | 数组类型
     */
    private boolean isClass(Class<?> clazz, I18n i18n) {
        if (clazz == null) { return false; }
        if (clazz.isPrimitive()) { return false; }
        return clazz.isAssignableFrom(i18n.resourceType()) || clazz.isAssignableFrom(Object.class) || clazz.isArray() || clazz.isAssignableFrom(List.class) || clazz.isAssignableFrom(Map.class) ||
                clazz.isAssignableFrom(Set.class) ||
                clazz.isAssignableFrom(TreeMap.class) ||
                clazz.isAssignableFrom(TreeSet.class);
    }

    /**
     *  parent 父对象引用
     *  child 子对象引用
     *  field 子对象操作 Field
     *  index 当获取的操作 cutField 为数组时,需要的位置参数
     */
    private static class Node {
        Object parent;
        Object child;
        Field field; // 对于 parent 父对象的 field 的直接操作 child 的引用
        Integer index; //如果 parent 是数组,需要设置索引加以判断
        Map<String, Field> mainFieldMap;
        Map<String, Field> langFieldMap;
        Node(Object parent, Object child, Field field, Integer index) {
            this.parent = parent;
            this.child = child;
            this.field = field;
            this.index = index;
        }
    }

    /**
     * 非递归优化版
     *
     * @param node 操作对象集合
     * @param language 国际化前缀字段
     * @param i18n 国际化注解对象
     */
    private void DFSStackFindField(Node node, String language, I18n i18n) throws IllegalAccessException, InstantiationException {
        if (node == null || i18n == null || node.child == null) { return; }
        Stack<Node> stack = new Stack<>(); //主运行栈
        Stack<Node> buffer = new Stack<>(); //倒置存储弹出 Node
        stack.push(node);
        //检索指定的国际化语言
        boolean isNotNullLanguage = language != null && !language.isEmpty();
        while (!stack.isEmpty()) {
            Node pop = stack.pop();
            if (pop.child == null) { continue; }
            // 判断源类型, 不为初始对象时才执行
            boolean flag = (pop.field != null && pop.child.getClass().isAssignableFrom(i18n.resourceType())) ||
                            (pop.field != null && i18n.prefix()); //关键条件,就算不设置原与目标转换类型VO,也通过国际化前缀进行字段值覆盖
            // 字段分类
            HashMap<String, Field> mainFieldMap = flag ? new HashMap<>() : null; //存储主字段,除了指定映射语言字段(当然也可能包含其它语言的字段)
            HashMap<String, Field> langFieldMap = flag ? new HashMap<>() : null; //存储语言字段
            // 迭代搜索字段
            Field[] fields = pop.child.getClass().getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                Class<?> fieldType = field.getType();
                if (isClass(fieldType, i18n)) { //判断类类型且不是原始类型
                    if (fieldType.isArray()) {
                        if (fieldType.getComponentType() != char.class) { // Map 类型 的 key -> value 转换时为 char[] 数组,需要特判
                            Object[] values = (Object[]) field.get(pop.child); //获得数组对象值(引用)
                            if (values != null) {
                                for (int i = 0; i < values.length; ++i) { //需要拿到索引,后期在设置引用值直接定位
                                    Object value = values[i];
                                    if (value != null) stack.push(new Node(pop.child, value, field, i)); //压栈
                                }
                            }
                        }
                    } else {
                        Object value = field.get(pop.child); //获得对象值(引用)
                        if (value != null) stack.push(new Node(pop.child, value, field, null)); //压栈
                    }
                }
                if (flag) {
                    String fieldName = field.getName();
                    if (isNotNullLanguage && fieldName.startsWith(language)) {
                        langFieldMap.put(fieldName, field);
                    } else {
                        mainFieldMap.put(fieldName, field);
                    }
                }
            }
            if (flag) { //当前 POP 加入转换 VO 操作
                pop.mainFieldMap = mainFieldMap;
                pop.langFieldMap = langFieldMap;
                buffer.push(pop); //压栈
            }
        }
        while (!buffer.isEmpty()) {
            Node pop = buffer.pop();
            if (isNotNullLanguage) { //只要需要语言国际化时才执行
                for (Field field : pop.langFieldMap.values()) {
                    field.setAccessible(true);
                    String fieldName = field.getName();
                    //切割出主映射字段名且首字母替换为小写
                    StringBuilder sbMFieldName = new StringBuilder(fieldName.replace(language, ""));
                    char first = (char) (sbMFieldName.substring(0, 1).charAt(0) ^ 32); //首字母转小写 (需规范驼峰命名时)
                    sbMFieldName.setCharAt(0, first);
                    String mFieldName = sbMFieldName.toString();
                    Field mainField = pop.mainFieldMap.get(mFieldName);
                    if (mainField != null) { //实体对应主映射字段不为空
                        mainField.setAccessible(true);
                        mainField.set(pop.child, field.get(pop.child)); //赋值
                    }
                }
            }
            if (! (i18n.resourceType().isAssignableFrom(Object.class) || i18n.targetType().isAssignableFrom(Object.class))) {
                // 转换 VO 对象
                Object targetType = i18n.targetType().newInstance();
                for (Field field : targetType.getClass().getDeclaredFields()) {
                    field.setAccessible(true);
                    String fieldName = field.getName();
                    Field mainField = pop.mainFieldMap.get(fieldName);
                    if (mainField != null) {
                        mainField.setAccessible(true);
                        field.set(targetType, mainField.get(pop.child));
                    }
                }
                // 最重要的一步,地址赋值到父的引用中
                pop.field.setAccessible(true);
                if (pop.index != null) { //父是数组时
                    Object[] arr = (Object[]) pop.field.get(pop.parent);
                    arr[pop.index] = targetType;
                } else { //父是对象时
                    pop.field.set(pop.parent, targetType);
                }
            }
        }
    }

    /**
     * 递归深度搜索
     *
     * @param parent 父对象引用
     * @param child 子对象引用
     * @param cutField 子对象操作 Field
     * @param index 当获取的操作 cutField 为数组时,需要的位置参数
     * @param language 国际化前缀字段
     * @param i18n 国际化注解对象
     */
    private void DFSFindField(Object parent, Object child, Field cutField, Integer index, String language,  I18n i18n) throws IllegalAccessException, InstantiationException {
        if (child == null) { return; }
        Field[] fields = child.getClass().getDeclaredFields();
        // 判断源类型, 不为初始对象时才执行
        boolean flag = cutField != null && child.getClass().isAssignableFrom(i18n.resourceType());
        boolean isNotNullLanguage = language != null && !language.isEmpty();
        // 字段分类
        HashMap<String, Field> mainFieldMap = flag ? new HashMap<>() : null; //存储主字段,除了指定映射语言字段(当然也可能包含其它语言的字段)
        HashMap<String, Field> langFieldMap = flag ? new HashMap<>() : null; //存储语言字段
        for (Field field : fields) {
            field.setAccessible(true);
            Class<?> fieldType = field.getType();
            if (isClass(fieldType, i18n)) { //判断类类型且不是原始类型
                if (fieldType.isArray()) {
                    if (fieldType.getComponentType() != char.class) { // Map 类型 的 key -> value 转换时为 char[] 数组,需要特判
                        Object[] values = (Object[]) field.get(child); //获得数组对象值(引用)
                        if (values != null) {
                            for (int i = 0; i < values.length; ++i) { //需要拿到索引,后期在设置引用值直接定位
                                Object value = values[i];
                                if (value != null) DFSFindField(child, value, field, i, language, i18n);
                            }
                        }
                    }
                } else {
                    Object value = field.get(child); //获得对象值(引用)
                    if (value != null) DFSFindField(child, value, field, null, language, i18n);
                }
            }
            if (flag) {
                String fieldName = field.getName();
                if (isNotNullLanguage && fieldName.startsWith(language)) {
                    langFieldMap.put(fieldName, field);
                } else {
                    mainFieldMap.put(fieldName, field);
                }
            }
        }
        if (flag) {
            executeFieldSetValue(parent, child, cutField, index, language, i18n, isNotNullLanguage, mainFieldMap, langFieldMap);
        }
    }

    private void executeFieldSetValue(Object parent, Object child, Field cutField, Integer index, String language,  I18n i18n, boolean isNotNullLanguage, HashMap<String, Field> mainFieldMap, HashMap<String, Field> langFieldMap) throws IllegalAccessException, InstantiationException {
        if (isNotNullLanguage) { //只要需要语言国际化时才执行
            for (Field field : langFieldMap.values()) {
                field.setAccessible(true);
                String fieldName = field.getName();
                //切割出主映射字段名且首字母替换为小写
                StringBuilder sbMFieldName = new StringBuilder(fieldName.replace(language, ""));
                char first = (char) (sbMFieldName.substring(0, 1).charAt(0) ^ 32); //首字母转小写 (需规范驼峰命名时)
                sbMFieldName.setCharAt(0, first);
                String mFieldName = sbMFieldName.toString();
                Field mainField = mainFieldMap.get(mFieldName);
                if (mainField != null) { //实体对应主映射字段不为空
                    mainField.setAccessible(true);
                    mainField.set(child, field.get(child)); //赋值
                }
            }
        }
        // 转换 VO 对象
        Object targetType = i18n.targetType().newInstance();
        for (Field field : targetType.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            String fieldName = field.getName();
            Field mainField = mainFieldMap.get(fieldName);
            if (mainField != null) {
                mainField.setAccessible(true);
                field.set(targetType, mainField.get(child));
            }
        }
        // 最重要的一步,地址赋值到父的引用中
        cutField.setAccessible(true);
        if (index != null) { //父是数组时
            Object[] arr = (Object[]) cutField.get(parent);
            arr[index] = targetType;
        } else { //父是对象时
            cutField.set(parent, targetType);
        }
    }

}

i18n 包下的(目前作者用的)

        PS:如果大家想不写多余的代码的话,可以直接使用 @I18n 注解即可,这里开始是对于 Bean 实体类实现接口进行手动映射国际化的属性值,理论比 @I18n 注解快,缺点就是要写代码。

I18nInterface 接口

  • 用于 Bean 实体类进行实现接口,手动映射国际化字段与主字段的属性值。
public interface I18nInterface {

    void english();

}

I18nManagerInterface 管理类接口

  • 用于约束 I18n 管理操作类的主要实现方法。
  • 注意方法I18nPrefixFieldEnum getHttpRequestHeaderLanguage(); ,这个方法返回的是实体类国际化前缀字段的枚举类型。
import java.util.List;
import java.util.Map;

public interface I18nManagerInterface {

    I18nPrefixFieldEnum getHttpRequestHeaderLanguage();

    void i18n(List<? extends I18nInterface> list);

    void i18n(Map<Object, ? extends I18nInterface> map);

    void i18n(I18nInterface obj);

}

I18nManager 国际化操作管理类

  • 目前可以传递三种,要么直接传入实体进行国际化,要么传入目前实现的 List 或者 Map 包含的 I18nInterface 实现类 Bean 实体。
  • 缺点:目前没有在这里做 Bean 实体转换功能,因为已经有第三方的转换框架,后续可能会自己写一个反序列化的加上,哈哈,大家也可以手动加入。
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Component
public class I18nManager implements I18nManagerInterface {

    private final String REQUEST_HEADER_KEY = HttpRequestHeaderAcceptXEnums.ACCEPT_LANGUAGE.getValue();

    @Resource
    private I18nAcceptLanguageAdapter acceptLanguageAdapter;

    @Override
    public I18nPrefixFieldEnum getHttpRequestHeaderLanguage() {
        //获得 HttpServletRequest 对象 并从请求头中获取 key == REQUEST_HEADER_KEY 的前缀值
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        return acceptLanguageAdapter.convert(request.getHeader(REQUEST_HEADER_KEY));
    }

    public void i18n(List<? extends I18nInterface> list) {
        I18nPrefixFieldEnum language = getHttpRequestHeaderLanguage();
        for (I18nInterface i18n : list) {
            i18n(i18n, language);
        }
    }

    public void i18n(Map<Object, ? extends I18nInterface> map) {
        I18nPrefixFieldEnum language = getHttpRequestHeaderLanguage();
        for (I18nInterface i18n : map.values()) {
            i18n(i18n, language);
        }
    }

    public void i18n(I18nInterface obj) {
        I18nPrefixFieldEnum language = getHttpRequestHeaderLanguage();
        i18n(obj, language);
    }

    private void i18n(I18nInterface obj, I18nPrefixFieldEnum language) {
        if (obj == null || language == null) return;
        switch (language) {
            case ENGLISH:
                obj.english();
                break;
            case DEFAULT:
            case CHINESE:

                break;
        }
    }

}

二、效果测试

1. 自定义 @I18n 注解效果测试

1.1 原实体类 

import lombok.Data;
import org.springframework.stereotype.Component;

@Data
@Component
public class TestVO {

    private Long id;

    private String name;

    private String englishName; //注意这是国际化字段

    private Object testVO; //这是嵌套本身

}

1.2 目标转换实体类

import lombok.Data;
import org.springframework.stereotype.Component;

@Data
@Component
public class TestTargetVO {

    private Long id;

    private String name;

    private Object testVO;

}

1.3 Controller 接口类实现

注意:这里仅为展示,代码不完全,如需复制,请删除不必要的方法或修改返回类型。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Tag(name = "测试接口 - App - Test")
@RestController
@RequestMapping("/nmkj/test")
@Validated
@I18n(resourceType = TestVO.class, targetType = TestTargetVO.class) //使用国际化注解并制定原与转换Bean类型
public class AppTestController {

    @GetMapping("/get-simple")
    @Operation(summary = "获取 test 信息 simple")
    public CommonResult<TestVO> get(){
        TestVO testVO = new TestVO();
        testVO.setName("很好");
        testVO.setEnglishName("English");

        TestVO testVO2 = new TestVO();
        testVO2.setName("实体内1");
        testVO2.setEnglishName("English");

        TestVO testV3 = new TestVO();
        testV3.setName("实体内2");
        testV3.setEnglishName("English");

        testVO.setTestVO(testVO2);
        testVO2.setTestVO(testV3);

        return success(testVO);
    }

    @GetMapping("/get-list")
    @Operation(summary = "获取 test 信息 List")
    public CommonResult<List<TestVO>> getList(){
        TestVO testVO = new TestVO();
        testVO.setName("很好");
        testVO.setEnglishName("English");

        TestVO testVO1 = new TestVO();
        testVO1.setName("牛逼");
        testVO1.setEnglishName("English");

        TestVO testVO2 = new TestVO();
        testVO2.setName("实体内");
        testVO2.setEnglishName("English");

        testVO1.setTestVO(testVO2);

        List<TestVO> list = new ArrayList<>();
        list.add(testVO);
        list.add(testVO1);
        return success(list);
    }

    @GetMapping("/get-map")
    @Operation(summary = "获取 test 信息 Map")
    public CommonResult<Map<String, TestVO>> getMap(){
        TestVO testVO = new TestVO();
        testVO.setName("很好");
        testVO.setEnglishName("English");

        TestVO testVO1 = new TestVO();
        testVO1.setName("牛逼");
        testVO1.setEnglishName("English");

        TestVO testVO2 = new TestVO();
        testVO2.setName("实体内");
        testVO2.setEnglishName("English");

        testVO1.setTestVO(testVO2);

        Map<String, TestVO> map = new HashMap<>();
        map.put("1", testVO);
        map.put("2", testVO1);
        return success(map);
    }

}

1.4 测试截图

1.4.1 测试1 实体嵌套

1.4.2 测试2 List

1.4.3 测试3 Map

2. Bean 实现 I18nInterface 接口

2.1 原实体类改进

import i18n.I18nInterface;
import lombok.Data;
import org.springframework.stereotype.Component;

@Data
@Component
public class TestVO implements I18nInterface {

    private Long id;

    private String name;

    private String englishName; //注意这是国际化字段

    private Object testVO; //这是嵌套本身

    @Override
    public void english() {
        name = englishName;
    }
}

2.2 Controller 增加一个测试接口

注意:代码不完全,这里用到了转换 Bean 工具类。

注意:我这里把原来的类上的自定义国际化 @I18n 注解注释了,之后重启项目。

    @Resource
    private I18nManager i18nManager; //注入国际化管理实例

    @GetMapping("/i18n-bean-get-list")
    @Operation(summary = "获取 test 信息 List")
    public List<TestTargetVO> getI18nBeanList(){
        TestVO testVO = new TestVO();
        testVO.setName("很好");
        testVO.setEnglishName("English");

        TestVO testVO1 = new TestVO();
        testVO1.setName("牛逼");
        testVO1.setEnglishName("English");

        TestVO testVO2 = new TestVO();
        testVO2.setName("实体内");
        testVO2.setEnglishName("English");

        testVO1.setTestVO(testVO2);

        List<TestVO> list = new ArrayList<>();
        list.add(testVO);
        list.add(testVO1);

        i18nManager.i18n(list); //国际化操作
        
        return BeanUtils.toBean(list, TestTargetVO.class);
    }

2.3 测试截图

注意Bean工具转换类只在一层进行转换,I18nManager 一样也是在一层进行国际化映射。

三、作者乱言

  • @I18n 这个自定义注解,优点:无需写多余代码,包含(国际化与转换Bean逻辑),缺点:速度问题,在不更新数据的情况下,有 Redis 缓存即可解决。但在大型的频繁更新数据来说,有小点的不合适,哈哈哈。
  • I18nManager 管理类,优点直接定位逻辑,速度快,但未实现 Bean 转换功能,这个靠大家啦,哈哈哈,因为有现成的转换 VO,在下所以懒了。

最后:如果代码有啥问题,欢迎各位元佬大佬指正 Jvav QVQ !

SpringBoot中可以自定义注解来实现特定的功能。自定义注解的步骤如下: 1. 使用`@interface`关键字来定义注解,可以在注解中设置属性。 2. 可以通过注解的属性来传递参数,比如设置注解中的属性值。 3. 可以通过判断某个类是否有特定注解来进行相应的操作。 在SpringBoot中,自定义注解可以用于实现日志记录、定时器等功能。通过使用注解,可以简化代码,并提高开发效率。同时,自定义注解也是Spring框架中广泛应用的一种方,可以在SpringMVC框架中使用注解来配置各种功能。而在SpringBoot框架中,更是将注解的使用推向了极致,几乎将传统的XML配置都替换为了注解。因此,对于SpringBoot来说,自定义注解是非常重要的一部分。123 #### 引用[.reference_title] - *1* *3* [springboot 自定义注解(含源码)](https://blog.csdn.net/yb546822612/article/details/88116654)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *2* [SpringBoot-自定义注解](https://blog.csdn.net/weixin_44809337/article/details/124366325)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虚妄狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值