【SpringBoot】自定义注解 I18n <约定式>国际化 (源码分享直接Copy)

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. 效果展示--接口测试

数据库部分字段数据

不发送国际化对应字段

发送国际化对应字段

如果有代码问题,欢迎指正,谢谢各位大佬!

Spring Boot提供了简单易用的国际化i18n)支持。以下是使用Spring Boot进行国际化的步骤: 1. 在src/main/resources目录下创建一个新的文件夹,命名为"i18n"。在该文件夹下创建多个属性文件,分别对应不同的语言。例如,可以创建messages.properties(默认语言)和messages_zh.properties(文)。 2. 在属性文件添加键值对,以便将文本翻译成不同的语言。例如,在messages.properties可以添加"welcome.message=Welcome!",在messages_zh.properties可以添加"welcome.message=欢迎!"。 3. 在Spring Boot的配置文件(application.properties或application.yml)添加以下配置: ``` spring.messages.basename=i18n/messages spring.messages.fallback-to-system-locale=false ``` 这样配置后,Spring Boot将会自动加载位于i18n文件夹下的属性文件。 4. 在需要进行国际化的地方使用`@Autowired`注解注入`org.springframework.context.MessageSource`对象,并使用`getMessage`方法获取对应的文本。例如: ```java @Autowired private MessageSource messageSource; public String getWelcomeMessage() { return messageSource.getMessage("welcome.message", null, LocaleContextHolder.getLocale()); } ``` `LocaleContextHolder.getLocale()`方法可用于获取当前请求的语言环境。 5. 运行应用程序并访问相应的接口或页面,Spring Boot将会根据请求的语言环境自动加载对应的属性文件,实现国际化效果。 这些是使用Spring Boot进行国际化的基本步骤,你可以根据需要进行进一步的定制和扩展。希望对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

虚妄狼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值