零、前言
在后端对于 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 !