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

news2025/1/11 2:25:04

零、前言

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

一、源码

adapter 适配器包

AcceptLanguageAdapterInterface 适配器接口

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

    I18nPrefixFieldEnum convert(String language);

}

 I18nAcceptLanguageAdapter 适配器实现类

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

@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 (language.contains("zh-CN")) {
                result = I18nPrefixFieldEnum.CHINESE;
            } else if (EnglishEnum.contains(language)) {
                result = I18nPrefixFieldEnum.ENGLISH;
            } else { //其它语言,一律设置英语
                result = I18nPrefixFieldEnum.ENGLISH;
            }
        }
        return result;
    }

}

annotation 自定义注解包

I18n 自定义注解接口

  • 范围:方法上 | 类上
  • resourceType 参数:必须,用于指定实体类的类型。
  • targetType      参数:必须,用于指定实体类转换的目标类型。

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

import java.lang.annotation.*;

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

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

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;
    }
}

EnglishEnum 枚举类

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

import java.util.EnumSet;

@Getter
public enum EnglishEnum {

    EN_DF("english"), //默认英语的前缀
    EN_US("en-US")
    ,EN_GB("en-GB")
    ,EN_HK("en-HK")
    ,EN_HU("en-HU")
    ,EN_JP("en-JP")
    ,EN_KR("en-KR")
    ,EN_ZH("en-ZH")
    ,EN_ZW("en-ZW")
    ,EN_TW("en-TW")
    ,EN_NZ("en-NZ")
    ,EN_NZW("en-NZW")
    ,EN_TWZ("en-TWZ")
    ;

    private final String value;

    EnglishEnum(String language) {
        value = language;
    }

    public static boolean contains(String language) {
        boolean result = false;
        EnumSet<EnglishEnum> enums = EnumSet.allOf(EnglishEnum.class);
        for (EnglishEnum e : enums) {
            if (e.getValue().contains(language)) {
                result = true;
                break;
            }
        }
        return result;
    }

}

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 深度优先搜索,看你怎么改咯!
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());
            // 字段分类
            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)); //赋值
                    }
                }
            }
            // 转换 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 !

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1987489.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode-240. 搜索二维矩阵 II

题目描述 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10…

使用Spring AI 、 Qdrant 和 Ollama 实现完全本私有化的RAG应用

使用Spring AI 、 Qdrant 和 Ollama 实现完全本私有化的RAG应用 迄今为止&#xff0c;Python 一直是实现检索增强生成&#xff08;RAG&#xff09;应用程序的首选语言&#xff0c;几乎成为开发大型语言模型&#xff08;LLM&#xff09;应用程序的默认选择。然而&#xff0c;对于…

WPF 依赖属性 IsHitTestVisible

IsHitlTestVisible 仅影响本身的元素&#xff08;含内部包含的子元素&#xff09;&#xff0c;不影响父元素效果&#xff0c;且事件会传递到父元素。 Eg&#xff1a; 如父元素有click事件&#xff0c; 子元素设置了IsHitTestVisiblefalse&#xff0c; 当鼠标单击这个子元素时&…

openssl 制作 信用库与服务证书

文章目录 前言openssl 制作 信用库与服务证书1. CA 证书2. 服务器证书/秘钥库3. 创建信用库4. 注意事项 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会…

【JS】一篇BOM详解笔记 | b站李立超

文章目录 一、前言&#x1f680;&#x1f680;&#x1f680;二、BOM简介&#xff1a;☀️☀️☀️2.1 BOM是什么及有什么用2.2 BOM对象有哪些 三、BOM各类对象讲解&#xff1a;☀️☀️☀️3.1 Navigator3.2 Location3.3 History 三、补充知识&#x1f680;&#x1f680;&#…

尚硅谷谷粒商城项目笔记——六、使用navciat连接docker中的mysql容器【电脑CPU:AMD】

六、使用navciat连接docker中的mysql容器 注意&#xff1a; 因为电脑是AMD芯片&#xff0c;自己知识储备不够&#xff0c;无法保证和课程中用到的环境一样&#xff0c;所以环境都是自己根据适应硬件软件环境重新配置的&#xff0c;这里的虚拟机使用的是VMware。 1navicat免费…

浴室柜哪个牌子质量好性价比高 | 提亮空间,点缀生活!

摘要&#xff1a;作为浴室的标配&#xff0c;浴室柜不仅是重要的收纳家具&#xff0c;也是最能体现卫浴空间和美感的存在。浴室柜看似平凡&#xff0c;却在无形之中散发出自身的魅力&#xff0c;为浴室颜值加分。浴室柜哪个品牌好&#xff1f;无论是注重外观的你&#xff0c;还…

《计算机组成原理》(第3版)第4章 存储器 复习笔记

第4章 存储器 一、概述 &#xff08;一&#xff09;存储器分类 1&#xff0e;按存储介质分类 &#xff08;1&#xff09;半导体存储器&#xff1b; &#xff08;2&#xff09;磁表面存储器&#xff1b; &#xff08;3&#xff09;磁芯存储器&#xff1b; &#xff08;4&…

面试笔记8.6

缓存 1.如何保证redis与数据库一致性 redis面试&#xff1a;如何保证缓存和数据库数据的一致性&#xff1f;_使用update更新数据,json缓存不更新-CSDN博客 如果先删除缓存&#xff0c;再删除数据库&#xff0c;数据不一致&#xff0c; 解决 删 1.先操作缓存但不删除缓存&…

lambda语法,java8Stream流,maven

lambda语法 Arraylist排序 ArrayList<Integer> a new ArrayList();a.add(1);a.add(2);a.add(3);a.sort(new Comparator<Integer>() {Overridepublic int compare(Integer o1, Integer o2) {return o1-o2;//对集合a中数据进行升序排序}}); 但是这样的书写还是有…

JavaEE: 进程和线程

文章目录 进程线程的概念和区别总结如何创建线程1.继承Thread重写run2.实现Runnable重写run3.继承Thread重写run,通过匿名内部类来实现4. 实现Runnable重写run,通过匿名内部类来实现5.基于lambda表达式来创建 虚拟线程 并发编程: 通过写特殊的代码&#xff0c;把多个CPU核心都利…

Leetcode力扣刷题——182.查找重复的电子邮箱

题目 编写解决方案来报告所有重复的电子邮件。 请注意&#xff0c;可以保证电子邮件字段不为 NULL。 以 任意顺序 返回结果表。 结果格式如下例。 结果 # Write your MySQL query statement below select Email from Person group by email having count(*)>1; 知识点 c…

Java 抽象知识笔记总结(油管)

Java系列文章目录 Java Optional 容器笔记总结 文章目录 Java系列文章目录一、前言二、学习内容&#xff1a;三、问题描述四、解决方案&#xff1a;4.1 抽象类的使用4.2 抽象类与接口的区别4.2.1 接口复习4.2.2 具体区别4.2.3 使用场景4.2.3.1 抽象类使用场景4.2.3.2 接口使用…

读零信任网络:在不可信网络中构建安全系统11用户组的认证和授权

1. 用户组的认证和授权 1.1. 几乎在每个系统中都有一小部分操作需要被密切关注 1.1.1. 每个应用对这部分操作的风险容忍度各有不同&#xff0c;且没有任何下限 1.1.2. 一部分风险是由用户个人的可信度决定的 1.1.2.1. 单个用户的可信度可能很低 1.1.2.2. 多个用户组合的可信…

OpenAI人事变动:联合创始人John Schulman离职加入Anthropic

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

【JVM基础14】——垃圾回收-强引用、软引用、弱引用、虚引用的区别

目录 1- 引言&#xff1a;为什么分多种引用类型2- ⭐核心&#xff1a;2-1 强引用2-2 软引用2-3 弱引用2-4 虚引用 3- 小结&#xff1a;3-1 强引用、软引用、弱引用、虚引用的区别&#xff1f; 1- 引言&#xff1a;为什么分多种引用类型 在 Java 中&#xff0c;引用类型被分为强…

ICM-20948芯片详解(8)

接前一篇文章&#xff1a;ICM-20948芯片详解&#xff08;7&#xff09; 六、寄存器详解 1. 总述 ICM-20948共有user bank 0~3共4组寄存器。 USER BANK 0寄存器图 USER BANK 0所包含的寄存器如下图所示&#xff1a; USER BANK 1寄存器图 USER BANK 1所包含的寄存器如下图所…

校园外卖微信小程序的设计与实现

校园外卖微信小程序的设计与实现 校园外卖微信小程序的设计与实现 [摘要] 随着社会的进步和生活质量的提高&#xff0c;人们对用餐体验有了更高的要求&#xff0c;导致电话和网上订餐服务日益发展。这一趋势也推动了以大学生为主要服务对象的校园外卖的发展。此次设计主要为大…

AI大模型巡游记第二回探索幻梦

unsetunset第二回&#xff1a;探索幻梦unsetunset **随着夕阳西下&#xff0c;李逸结束了今天的探险之旅…… 李逸回到了现实世界&#xff0c;心中满是对“幻境大模型”体验的好奇与惊叹。他迫不及待地想要了解更多关于这项技术背后的秘密。于是&#xff0c;他决定访问“幻梦”…

Python写UI自动化--playwright(元素焦点的控制)

在UI测试中会遇到一种情况&#xff0c;在输入框输入内容&#xff0c;鼠标焦点离开输入框后&#xff0c;前端会对输入框内容进行判断是否合法&#xff0c;比如输入不正确格式的账号&#xff0c;会有相应的提示。该篇文章就讲一讲playwright关于元素焦点的控制&#xff1a; Play…