【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 实体对应的国际化前缀枚举类型*/@Overridepublic 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();@Resourceprivate 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 RunningObject 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<>(); //倒置存储弹出 Nodestack.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();@Resourceprivate I18nAcceptLanguageAdapter acceptLanguageAdapter;@Overridepublic 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; //这是嵌套本身@Overridepublic void english() {name = englishName;}
}

2.2 Controller 增加一个测试接口

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

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

    @Resourceprivate 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.rhkb.cn/news/397805.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

el-form-item,label在上方显示,输入框在下方展示

本来是两排展示去写&#xff0c;设计要求一排展示&#xff0c;label再上方&#xff0c;输入框、勾选框在下方&#xff1b;只能调整样式去修改&#xff1b;参考label-position这个属性 代码如下&#xff1a; <el-form ref"form" :model"formData" clas…

React应用(基于react脚手架)

react脚手架 1.xxx脚手架&#xff1a;用来帮助程序员快速创建一个基于xxx库的模板项目 包含了所有需要的配置&#xff08;语法检查&#xff0c;jsx编译&#xff0c;devServer&#xff09;下载好了所有相关的依赖可以直接运行一个简单结果 2.react提供了一个用于创建react项目…

AWVS——Web 应用漏洞扫描的强大工具

一、引言 在网络安全日益重要的今天&#xff0c;Web 应用的安全性备受关注。Acunetix Web Vulnerability Scanner&#xff08;简称 AWVS&#xff09;作为一款知名的 Web 应用漏洞扫描工具&#xff0c;为保障 Web 应用的安全发挥了重要作用。本文将详细介绍 AWVS 的功能、特点、…

【vulhub靶场之spring】——

简介&#xff1a; Spring是Java EE编程领域的一个轻量级开源框架&#xff0c;该框架由一个叫Rod Johnson的程序员在2002年最早提出并随后创建&#xff0c;是为了解决企业级编程开发中的复杂性&#xff0c;业务逻辑层和其他各层的松耦合问题&#xff0c;因此它将面向接口的编程思…

【Postman工具】

一.接口扫盲 1.什么是接口&#xff1f; 接口是系统之间数据交互的通道。拿小红到沙县点餐为例&#xff1a;小红想吃鸭腿饭。她要用什么语言来表达&#xff1f;跟谁表达&#xff1f;通过什么表达&#xff1f;按照生活习惯应该是&#xff1a;小红根据菜单对服务员用中文表达她想要…

联通数科如何基于Apache DolphinScheduler构建DataOps一体化能力平台

各位小伙伴晚上好&#xff0c;我是联通数字科技有限公司数据智能事业部的王兴杰。 更好的阅读体验可前往原文阅读:巨人肩膀 | 联通数科如何基于Apache DolphinScheduler构建DataOps一体化能力平台 今天&#xff0c;我将和大家聊一聊联通数字科技有限公司是如何基于Apache Dol…

k8s创建secret并在container中获取secret

k8s创建secret并在container中获取secret 本文使用的deployment和service与我的上一篇文章一样。link也放在下面了&#xff0c;如果不懂什么事deployment和service&#xff0c;可以先看我的上一篇文章。 k8s使用kustomize来部署应用 下面我们将通过创建secret开始。secret是我…

保姆教程篇:手把手教你从零开始本地部署Dify

本教程将指导您在个人电脑上安装和配置 Dify。 为什么需要Dify 在开始具体的教程之前&#xff0c;先搞清楚为什么要选择 Dify。 6 月份&#xff0c;阿里巴巴全球数学竞赛中&#xff0c;首次接受AI参赛。结果令人大跌眼镜&#xff1a;AI选手们的表现完全无法与人类选手相提并…

萌啦数据软件价格多少,萌啦数据软件价格是多少

在当今这个数据驱动的时代&#xff0c;无论是企业运营、市场分析还是个人研究&#xff0c;都离不开高效、准确的数据处理与分析工具。萌啦数据软件&#xff0c;作为业界一颗璀璨的新星&#xff0c;凭借其强大的功能、友好的用户界面以及灵活的数据处理能力&#xff0c;赢得了众…

[SWPUCTF 2021 新生赛]PseudoProtocols(构造伪协议)

打开题目所给的环境我们可以看到这样一句话&#xff1a; 这里我先尝试访问/hint.php &#xff0c;但是发现什么都没有发生&#xff0c; F12查看源代码也并没有发现什么&#xff0c;到这里来看的话似乎没有思路了&#xff0c;但是这个题的题目已经给了我们很明显的提示&#xff…

类和对象(中)(1)

类和对象&#xff08;中&#xff09;(1) 类的默认成员函数 默认成员函数就是用户没有显式实现&#xff0c;编译器会⾃动⽣成的成员函数称为默认成员函数。 ⼀个类&#xff0c;我们不写的情况下编译器会默认⽣成以下6个默认成员函数&#xff0c;需要注意的是这6个中最重要的是…

云计算实训24——python基本环境搭建、变量和数据类型、数据集合、py脚本

一、python环境搭建 确保拥有阿里云镜像 查看python环境 [rootpython ~]# yum list installed | grep python 查看epel是否安装 [rootpython ~]# yum list installed | grep epel 安装epel [rootpython ~]# yum -y install epel-release.noarch 查看是否安装python3 [rootpyt…

【数据结构】mapset详解

&#x1f341;1. Set系列集合 Set接口是一种不包含重复元素的集合。它继承自Collection接口&#xff0c;所以可以使用Collection所拥有的方法&#xff0c;Set接口的实现类主要有HashSet、LinkedHashSet、TreeSet等&#xff0c;它们各自以不同的方式存储元素&#xff0c;但都遵…

OceanBase V4.2特性解析:MySQL模式下GIS空间表达式的场景及能力解析

1. 背景 1.1. OceanBase Mysql gis空间表达式的应用场景及能力 在OceanBase 4.1版本中&#xff0c;mysql模式下支持了gis数据类型以及部分空间对象相关的表达式&#xff0c;随着客户使用空间数据的需求日益增长&#xff0c;需要快速地补齐空间数据存储和计算分析的能力&#…

实景三维:解锁地理信息新维度,引领未来城市智慧之钥

在这个信息爆炸与科技日新月异的时代&#xff0c;地理信息与遥感技术正以前所未有的速度改变我们认知世界的方式。在推动“实景三维平台”这一前沿科技的构建上&#xff0c;它不仅是地理信息的立体呈现&#xff0c;更是智慧城市的基石&#xff0c;打开了通往未来城市规划、管理…

C++设计模式(代理模式)

1. 电话虫 在海贼中&#xff0c;有一种神奇的通信工具叫做电话虫&#xff08;Den Den Mushi&#xff09;&#xff0c;外形如蜗牛&#xff0c;身上带有斑点或条纹或通体纯色&#xff0c;壳顶上有对讲机或按键&#xff0c;不接通时会睡觉&#xff0c;接通时会惊醒&#xff0c;并发…

数据结构之链表

写在前面 链表是一种常用的线性数据结构&#xff0c;在jdk中也提供具体的实现类java.util.LinkedList。本文来看下其相关内容。 1&#xff1a;链表的特点 链表是一种由很多个节点组成的线性数据结构&#xff0c;每个节点都有一个指向下一个节点的引用&#xff0c;从而构成链…

Unity(2022.3.38LTS) - 下载,安装

目录 A. 简介 B. 下载和安装UnityHub C. 下载安装unity编辑器 安装页面 选择版本 添加模块 D.总结 A. 简介 Unity 是一款广泛使用的跨平台游戏开发引擎。 一、主要特点 跨平台性&#xff1a; 支持多种主流平台&#xff0c;包括 Windows、Mac、Linux、iOS、Android、Xb…

LeetCode_sql_day15(262.行程与用户)

描述&#xff1a;262. 行程和用户 - 力扣&#xff08;LeetCode&#xff09; 取消率 的计算方式如下&#xff1a;(被司机或乘客取消的非禁止用户生成的订单数量) / (非禁止用户生成的订单总数)。 编写解决方案找出 "2013-10-01" 至 "2013-10-03" 期间非禁止…

Vue 应用实例的关键方法与配置案例一

目录 createApp createSSRApp app.mount app.unmount app.component app.directive Vue3.X自定义全局指令 Vue2.X自定义全局指令 app.use app.mixin 非 VIP 用户能够免费下载博文资源 createApp createApp 是 Vue 3.0 中用于创建应用实例的方法。它接收一个…