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

news2024/9/20 18:47:57

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

数据库部分字段数据

不发送国际化对应字段

发送国际化对应字段

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

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

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

相关文章

【电子电路学习笔记】——模电笔记

关于专栏&#xff1a;本专栏用于分享学习电子电路过程中记录的笔记。模电部分使用的教材是高等教育出版社&#xff0c;华成英主编的《模拟电子技术基础&#xff08;第六版&#xff09;》&#xff08;其他版本的内容差不多&#xff0c;建议使用最新版教材&#xff09;&#xff1…

【完全二叉树的权值】

题目 代码1&#xff08;队列&#xff09; #include<bits/stdc.h> using namespace std; #define x first #define y second typedef pair<int, int> PII;const int N 1e510; int n; long long a[N]; queue<PII> q; long long sum[25]; int main() {memset(…

Leetcode—233. 数字 1 的个数【困难】

2024每日刷题&#xff08;152&#xff09; Leetcode—233. 数字 1 的个数 算法思想 参考自k神 实现代码 class Solution { public:int countDigitOne(int n) {long digit 1;long high n / 10;long low 0;long cur n % 10;long ans 0;while(high ! 0 || cur ! 0) {if(cu…

Python Web开发之“基于flask的轻量级Web应用”

参考文章1&#xff1a;https://cloud.tencent.com/developer/article/2373503 参考文章2&#xff1a;基于Flask的自定义网站设计与实现&#xff08;代码全文讲解V1.0&#xff09;_flask框架制作网页-CSDN博客 参考文章3&#xff1a;PythonFlaskMysqL设计网页 - 李明惠 - 博客…

Candance Allegro 入门教程笔记:如何绘制PCB封装库?

文章目录 一、PCB封装库的组成元素二、使用Padstack Edictor制作封装焊盘引脚三、PCB Editor软件创建贴片封装&#xff08;STM32F103T8U6 QFN36 为例&#xff09;3.1、新建PCB封装对象3.2、计算引脚启始坐标3.3、添加焊盘Pad路径3.4、放置焊盘引脚3.5、绘制装配线3.6、放置字符…

sqli-labs-php7-master\Less-1

1&#xff0c;进入mysql数据库 mysql -u root -p 接着&#xff1a; show databases; use security; select * from where id1 LIMIT 0,1; 函数的基本用法 system_user() #当前系统用户 user() #当前登录用户 current_user() #当前登录用…

MOS场效应管常见损坏原因

造成损坏的原因可能有几个: 1.缺少保护电路&#xff0c;一般来说电路中应适当设置保护电路&#xff0c;以吸收电路中的瞬间高压&#xff0c;浪涌电压保护关键元件。 2.参数选取不合理&#xff0c;没有余地&#xff1b;场效应管的耐压&#xff0c;电流都应该流有一定的余地&…

【网络】网络层

网络层 一、前置知识二、IP协议1、协议头格式2、网段划分3、特殊的IP地址&#xff1a;4、IP地址数量限制5、私有IP地址和公网IP地址6、浅谈运营商7、路由8、IP分片 一、前置知识 1、首先要对每台主机要有一个唯一标识符&#xff0c;所以要有源ip地址和目的ip地址来标识目的主机…

linux包管理工具与软件安装

目录 TAR工具的使用 常用选项&#xff1a; 对文件进行打包 查看文件包 向包文件里放添加文件 解包到当前路径 解包到指定路径 zip工具的使用 gzip压缩 bzip2压缩 gzip解压缩 bzip2解压缩 查看压缩文件内有哪些文件 将当前目录下压缩文件解压到指定目录下 软件安…

OpenGL ES->工作机制

渲染流程 渲染目的&#xff1a;输入3D立体坐标&#xff0c;输出绘制后的2D平面像素工作流程&#xff1a;顶点着色器->图元装配->几何着色器->光栅化->片段着色器->测试与混合&#xff0c;整个工作流程被封装在GPU内部&#xff0c;无法改变。运行在CPU的代码调用…

word加密文档忘记密码要如何打开

我们在日常工作中&#xff0c;经常需要使用word来编写文档&#xff0c;有时为了保证资料的安全性&#xff0c;会给word文档加密。虽然这样可以保障安全&#xff0c;但时间间隔一长就容易忘记密码&#xff0c;word又没有密码重置功能&#xff0c;忘记密码就很麻烦&#xff01;那…

Jenkins 部署Vue项目指引: Vue项目本地跨域代理 、解决ERR_UNSAFE_PORT

文章目录 引言I Jenkins 部署Vue项目配置插件安装系统配置NodeJS安装目录和别名设置新建任务(通用类型)构建环境Build Steps(构建步骤)II nginx部署站点(端口和站点目录的映射)查找Nginx配置文件端口和站点目录的映射III Vue项目本地跨域代理,屏蔽掉后端服务API的网关IP…

设计模式-领域逻辑模式-数据源架构模式

行数据入口&#xff08;Row Data Gateway&#xff09; 充当数据源中单条记录入口的对象。每行一个实例 运行机制 行数据入口和单条记录极为相似&#xff0c;数据库中的每一列变成了一个域。适用于事务脚本只能设置单独的查找方法对象&#xff0c;对行数据入口进行操作。如果行…

golang判断某个文件内容是否是二进制文件方法, LimitReader, 获取文件大小,字符串0写入后的byte数据为48, byte零值

go语言中判断某个文件是否是二进制文件的方法&#xff0c; 通过LimitReader读取指定大小的数据后对数据进行判断&#xff0c; 这里有一个很有趣的知识点就是 字符串0在写入文件后&#xff0c;再通过io read读取后的byte数据他在内存中显示的可不是0 而是变成了 48, 十六进制 0x…

Java Web——第二天

什么是JavaScript? JavaScript(简称:JS) 是一门跨平台、面向对象的脚本语言。是用来控制网页行为的&#xff0c;它能使网页可交互 JavaScript和Java是完全不同的语言&#xff0c;不论是概念还是设计。但是基础语法类似 JavaScript在1995年由 Brendan Eich 发明&#xff0c;…

8月6日Spring Boot学习笔记

MyBatis动态SQL 动态 SQL 大大减少了编写代码的工作量&#xff0c;更体现了 MyBatis 的灵活性、高度可配置性和可维护性。 if标签 <if test"判断条件">SQL语句</if> 当判断条件为 true 时&#xff0c;才会执行所包含的 SQL 语句。 choose、when和otherw…

我在杭州的Day30_进程间通信(IPC)——20240805

一、相关练习 1.使用有名管道实现&#xff0c;一个进程用于给另一个进程发消息&#xff0c;另一个进程收到消息后&#xff0c;展示到终端上&#xff0c;并且将消息保存到文件上一份 1.1> 01homework.c #include <myhead.h>int main(int argc, const char *argv[]) …

怎么实现外地分公司与总公司软件连接?

为了确保外地分公司能够顺利连接总公司的管理软件和财务软件&#xff0c;建立一个安全可靠的网络基础设施是必不可少的。首先&#xff0c;我们可以通过搭建虚拟专用网络来实现分公司与总公司内部网络的互联互通。虚拟专用网络不仅能够保障数据传输的安全性&#xff0c;还能提供…

SpringBoot简单项目(二维码扫描)

pom.xml中导入依赖 <!-- zxing --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.0</version></dependency><dependency><groupId>com.google.zxing</gro…

systemd-manage系统服务图形化管理工具使用教程

1. systemd-manage介绍 systemd-manage是一个开源的基于systemd服务管理的图形化工具&#xff0c;使用qt图形库进行开发&#xff0c;可以提供服务管理&#xff0c;用户会话&#xff0c;配置文件修改&#xff0c;日志查询&#xff0c;性能分析&#xff0c;进程管理等功能。图形…