0. 已做全新升级版
链接:【SpringBoot】自定义注解终极升级版<i18n国际化>方案源码Copy
链接:【SpringBoot】自定义注解终极升级版<i18n国际化>方案源码Copy
链接:【SpringBoot】自定义注解终极升级版<i18n国际化>方案源码Copy
重要的事情三遍
注解接口 I18n
package annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface I18n {
/**
* 在方法的参数列表中检索是否包含此参数名才进行转换,如自定义需要进行指定
* 在参数值返回的是国际化前缀字段 映射 到主字段,如 englishName --> name
* @return 参数名
*/
String language() default "language";
}
切面实现类 I18nAspect
package aspect;
import cn.iocoder.yudao.module.nmkj.annotation.I18n;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.*;
@Aspect
@Component
/*
* 请确保被国际化实体或被包含国际化实体的类型修饰为 Object 类型 或内置 集合类型 的顶层类型 修饰的,而不是具体自定义类型
* 搜索并国际化实例与子实例以此类推所有的国际化前缀映射到主字段
*/
public class I18nAspect {
// 方法级别的切点
@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);
//拿到指定名字前缀语言字段参数
String lang = null;
for (int i = 0; i < parameterNames.length; i++) {
if (parameterNames[i].equals(i18n.language()) && args[i] instanceof String) { // 找到指定的语言前缀参数名
// System.out.println("获得语言参数: " + testAnnotation.lang() + " ---> " + args[i]);
lang = (String) args[i];
break;
}
}
// Method Running
// System.out.println("========== Around Advice Method Running ===========");
Object proceed = proceedingJoinPoint.proceed(args);
//对返回结果进行转换
if (lang != null) { //当参数列表有 lang 字段名时才执行转换
BFSTargetField(proceed, lang);
}
// After
// System.out.println("========== Around Advice After End ===========");
return proceed;
}
private void BFSTargetField(Object object, String startsWithFieldName) throws IllegalAccessException {
if (object == null) { return; }
Queue<Object> queue = new LinkedList<>();
queue.offer(object);
while (!queue.isEmpty()) {
Object obj = queue.poll();
Field[] fields = obj.getClass().getDeclaredFields();
boolean flag = false; //当前实体是否需要国际化
for (Field field : fields) {
field.setAccessible(true); //设置属性可访问性
Class<?> fieldType = field.getType();
// System.out.println("字段名" + " ---> " + field.getName() + " ---> " + isClass(fieldType));
if (isClass(fieldType)) { //判断类类型且不是原始类型
// System.out.println(field.getName() + " 类型:" + field.getClass().getSimpleName() + " toStr: " + field);
if (fieldType.isArray()) {
if (fieldType.getComponentType() != char.class) { // Map 类型 的 key -> value 转换时为 char[] 数组,需要特判
Object[] values = (Object[]) field.get(obj); //获得数组对象值(引用)
for (Object value : values) {
// System.out.println("Array Value ---> " + value);
if (value != null) queue.offer(value);
}
}
} else {
Object value = field.get(obj); //获得对象值(引用)
// System.out.println("Type Value ---> " + value);
if (value != null) queue.offer(value);
}
} else if (field.getName().startsWith(startsWithFieldName)) { //找到实体,标记 (之后继续执行时是看实体内是否还有需要国际化实体)
flag = true;
}
}
if (flag) {
// System.out.println("========== Find Target ===========");
// O(n) 复杂度查找实例属性
HashMap<String, Field> mainFieldMap = new HashMap<>(); //存储主字段,除了语言映射字段
HashMap<String, Field> langFieldMap = new HashMap<>(); //存储语言主字段
for (Field tField : fields) { //存储映射分类
tField.setAccessible(true);
String tFieldName = tField.getName();
if (tFieldName.startsWith(startsWithFieldName)) {
langFieldMap.put(tFieldName, tField);
} else {
mainFieldMap.put(tFieldName, tField);
}
}
// O(1) 复杂度映射 lang 字段数量
for (Field tField : langFieldMap.values()) {
tField.setAccessible(true);
String tFieldName = tField.getName();
//切割出主映射字段名且首字母替换为小写
StringBuilder sbMFieldName = new StringBuilder(tFieldName.replace(startsWithFieldName, ""));
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(obj, tField.get(obj)); //赋值
}
}
}
}
}
/**
* 如果实体中有需要国际化的字段,请保证该字段是 Object 类型 或内置 集合类型 的顶层类型 修饰的,而不是具体自定义类型
* @param clazz 类型参数
* @return 类类型 | 引用类型 | 数组类型
*/
private boolean isClass(Class<?> clazz) {
if (clazz == null) { return false; }
if (clazz.isPrimitive()) { return false; }
return 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);
}
}
使用教程
0. 假设实体类与接口地址
实体类:
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;
}
接口地址:
import cn.CommonResult;
import annotation.I18n;
import aspect.TestVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "测试接口 - Admin - Test")
@RestController
@RequestMapping("/nmkj/test")
@Validated
@I18n(language = "lang")
public class AdminTestController {
@GetMapping("/get-simple")
@Operation(summary = "获取 test 信息 simple")
public CommonResult<TestVO> get(String lang){
TestVO testVO = new TestVO();
testVO.setName("很好");
testVO.setEnglishName(lang);
TestVO testVO2 = new TestVO();
testVO2.setName("实体内");
testVO2.setEnglishName(lang);
testVO.setTestVO(testVO2);
return success(testVO);
}
}
1. 约定规则描述
- @I18n 注解接口参数名的扫描:默认值:language (String 类型),会查找方法参数列表中有 language 参数名且 String 类型的参数的值,会把这个值当成 字段映射扫描前缀。
- 方法参数名扫描自定义:@I18n(language = "lang"),这里的 lang 值则表示你的方法参数名。
- @I18n 注解字段前缀扫描规则:通过上面得到前缀值后,对方法的返回结果,只要某个对象或子对象的字段名中有如:english 前缀,就会对当前字段实例执行国际化操作,把前缀字段 englishXXX -----> XXX 映射到 XXX 字段。
- 注解的使用范围:方法上 || 类上
- 注意实体对象在 切面类 中的 isClass() 方法 中判断问题:如果扫描不到你想要的类型,可以自己添加指定类型:如你想要扫描到的类型:XXX.class 。
2. 使用效果接口测试
不发送前缀字段:
发送前缀字段:
3. 效率问题
PS:虽然说我已经优化了递归变成 队列的迭代方式,但是它依然会深层次的进行搜索,不过层次也因加特判效果效率明显提升,但如果返回的结果深层嵌套,那效率绝对会降低,不过也可能只是第一次,可以用 Redis 进行优化查询即可。
优势:相对于自定义映射,可以简化超多代码。
如果有代码问题,欢迎指正,谢谢各位大佬!
方案二:使用接口约束自定义实体的映射
I18nInterface 接口:
PS:用于实体类自定义实现国际化接口。
public interface I18nInterface {
void english();
}
I18nManagerInterface 接口:
PS:用于约定管理类实现的方法,也可以省略掉,直接写类。
import java.util.List;
import java.util.Map;
public interface I18nManagerInterface {
void i18n(List<? extends I18nInterface> list, String i18n);
void i18n(I18nInterface obj, String i18n);
void iteration(List<? extends I18nInterface> list, String i18n);
void map(Map<Object, ? extends I18nInterface> map, String i18n);
}
I18nManager 管理实现类:
PS:用于直接操作实体类进行国际化。
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
public class I18nManager implements I18nManagerInterface {
public static final String I18N_ENGLISH = "english";
@Override
public void i18n(List<? extends I18nInterface> list, String i18n) {
if (i18n == null) return;
switch (i18n) {
case I18N_ENGLISH:
iteration(list, i18n);
break;
}
}
@Override
public void i18n(I18nInterface obj, String i18n) {
if (i18n == null) return;
switch (i18n) {
case I18N_ENGLISH:
obj.english();
break;
}
}
@Override
public void iteration(List<? extends I18nInterface> list, String i18n) {
if (i18n == null) return;
for (I18nInterface obj : list) {
i18n(obj, i18n);
}
}
@Override
public void map(Map<Object, ? extends I18nInterface> map, String i18n) {
if (i18n == null) return;
for (I18nInterface obj : map.values()) {
i18n(obj, i18n);
}
}
}
0. 效果展示--假设接口 & 实体实现类 I18nInterface 接口
实体类:
@Data
public class CarouselDO extends BaseDO implements I18nInterface {
/**
* 轮播ID
*/
private Long id;
/**
* 轮播地址
*/
private String img;
/**
* 排序权值
*/
private Long sort;
/**
* 轮播权重
*/
private Integer status;
/**
* 内容1
*/
private String content1;
/**
* 内容2
*/
private String content2;
/**
* 内容3
*/
private String content3;
/**
* 国际化内容1
*/
private String englishContent1;
/**
* 国际化内容2
*/
private String englishContent2;
/**
* 国际化内容3
*/
private String englishContent3;
@Override
public void english() { //实现直接赋值
this.content1 = this.englishContent1;
this.content2 = this.englishContent2;
this.content3 = this.englishContent3;
}
}
接口:
@Tag(name = "用户APP - 首页轮播")
@RestController
@RequestMapping("/nmkj/carousel")
@Validated
public class AppCarouselController {
@Resource
private CarouselService carouselService;
@Resource
private I18nManager i18nManager; // 国际化管理类
@GetMapping("/list")
@Operation(summary = "获得首页轮播列表")
// @I18n
public CommonResult<List<AppCarouseIRespVO>> getCarouselList(@Valid CarouselPageReqVO reqVO, String language) {
reqVO.setStatus(1); //用户端请求的数据必须为启用状态
List<CarouselDO> lsitResult = carouselService.getCarouselList(reqVO);
i18nManager.i18n(lsitResult, language); // 直接传递应用的语言参数与集合的 VO国际化实现类
return success(BeanUtils.toBean(lsitResult, AppCarouseIRespVO.class));
}
}
1. 效果展示--接口测试
数据库部分字段数据:
不发送国际化对应字段:
发送国际化对应字段:
如果有代码问题,欢迎指正,谢谢各位大佬!