当前位置: 首页 > article >正文

【SpringBoot3】使用Easy-Trans做字典翻译

在项目中经常会有一些根据id从某个表中查name或者从数据字典表中根据code查name的场景。

Easy-Trans 可以非常优雅的实现以上需求,可以有效减少字段冗余。

Easy-Trans介绍

适用场景

  1. 有userId/idCardNo(身份证号码-唯一键场景) 需要 userName,无需联表查询。
  2. 有gender code 0 需要 男。
  3. 枚举指定属性给前端
  4. 反向翻译

亮点

  1. 缓存支持
  2. 跨微服务翻译支持(User和Order 是2个不同微服务,order里面有userId 需要userName)
  3. 国际化支持
  4. 多种ORM框架适配
  5. 多数据源支持
  6. 集合支持(userIds [1,2,3] 翻译为张三,李四,王五)
  7. 反向翻译支持 男->gender 0 张三->user_id 1 陕西分公司 财务部 -> org_id 1
    在这里插入图片描述

一、配置参数说明

在yaml中添加如下配置:

easy-trans:
   #启用redis缓存 如果不用redis请设置为false
   is-enable-redis: true
   #启用全局翻译(拦截所有responseBody进行自动翻译),如果对于性能要求很高可关闭此配置在方法上使用注解翻译
   is-enable-global: true 
   #启用平铺模式 手动翻译无效
   is-enable-tile: true
   #字典缓存放到redis 微服务模式请开启
   dict-use-redis: true 
   #使用@RpcTrans来标记哪些类可以进行RPC翻译,默认为关闭,多团队协作推荐开启
   is-enable-custom-rpc: true
   # ruoyi相关的框架请开启
   is-enable-map-result: true
   # 反向翻译数据库类型 mysql
   db-type: mysql 
   # Mybatis-plus 为 3.5.3.2版本以上的3.x 版本请设置为true
   mp-new: true

二、核心注解@Trans说明

1、参数说明

1、type
指定翻译的数据源
easy trans支持多种数据源,所以要手动指定。
可选值:

  • TransType.AUTO_TRANS 自动自定义数据源翻译,左侧有单独的章节讲解如何自定义数据源。
  • TransType.DICTIONARY 字典缓存为数据源
  • TransType.SIMPLE 本微服务/单体项目系统自动查表作为数据源
  • TransType.RPC 自动调用其他微服务接口进行自动查表作为数据源

2、key
指定具体数据源(type为TransType.AUTO_TRANS或者TransType.DICTIONARY才需要配置)

  • type为TransType.DICTIONARY 传入字典分组编码 比如orderStatus

3、ref
将翻译出来的数据映射到本pojo的某个属性上
比如根据userId翻译userName,vo中已经有userName了(必须手动在vo定义username属性,否则翻译失败),可以通过ref对vo的username进行赋值。

4、refs
类似ref,不过支持多个,是数组格式 不是字符串逗号分隔哦

----推荐使用平铺模式来代替ref和refs(详情见左侧平铺模式)----

5、target
TransType.SIMPLE的时候指定对方的po类
比如是userId那么就指定UserPo.class

6、targetClassName
TransType.RPC的时候指定对方的po类全名 @since 2.2.12 TransType.SIMPLE类型也支持
com.ucenter.po.User

7、fields
用来指定我需要对方类的要哪个字段(TransType.SIMPLE和TransType.RPC可用)
支持指定多个,数组格式,不是字符串分隔哦

8、alias
别名,解决翻译结果字段名重名问题
一个vo有createUserId和updateUserId 他们的结果都叫userName 就可以通过别名来区分,比如updateUserId配置了update 那么结果就为updateUserName。

9、serviceName
微服务名称(TransType.RPC 有效)
TransType.RPC的时候组件自动调用其他微服务的接口获取数据,所以要指定服务名称,easyTrans会自动去注册中心获取对应的服务实例URL进行调用。

10、serviceContextPath
服务的context-path
如果远程微服务配置了context-path 可以通过此属性来配置,保证RPC调用不会404

11、 uniqueField
vo中的属性不是对方的id,而是唯一键,这里配置target类唯一键的字段名。
TransType.RPC 和TransType.SIMPLE 可用。
注意:不支持同一个类中混用id和唯一键,比如:Student类中有createUserId 和 updateUserMobile(用户的唯一键字段)

12、dataSource
数据源名称
在多数据源环境下指定此翻译使用哪个数据源 TransType.SIMPLE 有效。

13、sort
翻译顺序
比如 文章表有createBy 字段 对应用户表 用户表有orgid字段对应 组织表。
文章vo需要展示创建人以及创建人所在部门。 就可以指定sort 先对createBy字段进行翻译,取orgid用ref设置到文章vo上,然后再用orgid翻译org的名称。

2、@Trans源码

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Trans {

    /**
     * 获取翻译类型,比如 wordbook 是字典
     *
     * @return 类型
     */
    String type();

    /**
     * 字段 比如  要翻译男女 上面的type写dictionary 此key写sex即可
     *
     * @return
     */
    String key() default "";

    /**
     * 设置到的target value  比如我有一个sex字段,有一个sexName 字段  sex是0 设置ref翻译服务可以自动把sexname设置为男
     * 目标类字段配置了多个 有teacherName,teacherage 两个字段   我想要teacherName  可以写 teacherName#name
     *
     * @return
     */
    String ref() default "";

    /**
     * ref 支持多个,为了保持兼容新加了一个字段
     * 作用同ref 只是支持多个
     *
     * @return
     */
    String[] refs() default {};

    /**
     * 目标class
     *
     * @return
     */
    Class<? extends VO> target() default TransPojo.class;

    /**
     * 需要目标class哪些字段
     *
     * @return
     */
    String[] fields() default {};

    /**
     * 别名
     *
     * @return
     */
    String alias() default "";

    /**
     * 远程服务名称
     *
     * @return
     */
    String serviceName() default "";

    /**
     * 远程服务 ContextPath
     *
     * @return
     */
    String serviceContextPath() default "";

    /**
     * @return
     */
    String targetClassName() default "";

    /**
     * 自定义的函数名(此名称需要被spring托管 并实现FuncGetter)
     * @return
     */
    String customeBeanFuncName() default "";

    /**
     * 数据源
     * @return
     */
    String dataSource() default "";

    /**
     *  唯一键字段
     * 部分的时候表里的code,身份证号码,手机号等也是唯一键
     * @return
     */
    String uniqueField() default "";

    /**
     *  排序字段,一般用于级联翻译
     * @return
     */
    int sort() default 0;
}

三、支持的转换类型

1、字典翻译(TransType.DICTIONARY)

需要使用者把字典信息刷新到DictionaryTransService 中进行缓存,使用字典翻译的时候取缓存数据源

2、简单翻译(TransType.SIMPLE)

比如有userId需要userName或者userPo给前端,原理是组件使用MybatisPlus/JPA的API自动进行查询,把结果放到transMap中。

3、跨微服务翻译(TransType.RPC)

比如订单和用户是2个微服务,但是我要在订单详情里展示订单的创建人的用户名,需要用到RPC翻译,原理是订单微服务使用restTemplate调用用户服务的一个统一的接口,把需要翻译的id传过去,然后用户微服务使用MybatisPlus/JPA的API自动进行查询把结果给订单微服务,然后订单微服务拿到数据后进行翻译,当然使用者只是需要一个注解,这些事情都是由组件自动完成的。

4、AutoTrans(TransType.AUTO)

还是id翻译name场景,但是使用者如果想组件调用自己写的方法而不通过Mybatis Plus/JPA 的API进行数据查询,就可以使用AutoTrans

5、枚举翻译(TransType.ENUM)

比如我要把SEX.BOY 翻译为男,可以用枚举翻译。

四、使用示例

1、引入依赖

<dependency>
    <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
    <artifactId>easy-trans-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>
<dependency>
    <groupId>com.fhs-opensource</groupId>
    <artifactId>easy-trans-mybatis-plus-extend</artifactId>
    <version>3.0.5</version>
</dependency>

如果使用Redis请添加redis的引用

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

反向翻译专用扩展,反向翻译的最低版本为:2.2.3-M1

<dependency>
    <groupId>com.fhs-opensource</groupId>
    <artifactId>easy-trans-untrans-driver</artifactId>
    <version>3.0.5</version>
</dependency>

2、在VO中使用

注意:需要翻译的VO要实现接口 TransPojo

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
//实现TransPojo  接口,代表这个类需要被翻译或者被当作翻译的数据源
public class Student implements TransPojo {
     // 字典翻译 ref为非必填
    @Trans(type = TransType.DICTIONARY,key = "sex",ref = "sexName")
    private Integer sex;

    //这个字段可以不写,实现了TransPojo接口后有一个getTransMap方法,sexName可以让前端去transMap取
    private String sexName;
    
    //SIMPLE 翻译,用于关联其他的表进行翻译    schoolName 为 School 的一个字段
    @Trans(type = TransType.SIMPLE,target = School.class,fields = "schoolName")
    private String schoolId;
	
	//远程翻译,调用其他微服务的数据源进行翻译
	@Trans(type = TransType.RPC,targetClassName = "com.fhs.test.pojo.School",fields = "schoolName",serviceName = "easyTrans",alias = "middle")
    private String middleSchoolId;
	
	// 枚举翻译,返回文科还是理科给前端
	@Trans(type=TransType.ENUM,key = "desc")
    private StudentType studentType = StudentType.ARTS;

    public static enum StudentType{

        ARTS("文科"),
        SCIENCES("理科");

        private String desc;
        StudentType(String desc){
            this.desc = desc;
        }
    }
}

五、转换数据字典的key-value

1)初始化数据字典数据

@Component
@Slf4j
public class MyApplicationListener implements ApplicationListener<ApplicationReadyEvent> {

    @Autowired
    private DictionaryTransService dictionaryTransService;
    @Resource
    private DictDataService dictDataService;
    @Resource
    private DictTypeMapper dictTypeMapper;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.info("初始化数据字典");
        // 处理应用启动完成事件
        List<DictTypeDO> lstType = dictTypeMapper.selectList();
        lstType.forEach(type -> {
            List<DictDataDO> lstData = dictDataService.getDictDataListByDictType(type.getType());
            //在某处将字典缓存刷新到翻译服务中,
            Map<String, String> transMap = lstData.stream().collect(Collectors.toMap(DictDataDO::getValue, DictDataDO::getLabel, (existing, replacement) -> existing));
            dictionaryTransService.refreshCache(type.getType(), transMap);
        });
        log.info("初始化数据字典完成");
    }
}

2)在 VO 中使用

@Trans(type = TransType.DICTIONARY,key = "sale_fee_type", ref = "saleFeeTypeName")
private String saleFeeType;
private String saleFeeTypeName;

六、自动翻译和手动翻译

  • easy-trans.is-enable-global 设置为true的时候,所有的responseBody框架都会拦截尝试翻译,支持嵌套。称之为自动翻译。
  • 可以注入 TransService 然后调用 transOne或者transBatch 来翻译一个或者多对象,称之为手动翻译,手动翻译不支持平铺。
  • 使用@TransMethodResult 标记方法,框架会自动翻译方法return的值 基于AOP实现。
  • 开启全局翻译后 如果某个controller的返回值不需要返回结果可以 添加@IgnoreTrans 到方法上
//手动翻译
@Autowired
private TransService transService;
transService.transOne(school);
transService.transBatch(schoolList);

//标记方法结果翻译
@TransMethodResult
public Student getStudent(){
        Student student = new Student();
        student.setTeacherId(1);
        return student;
}

//忽略翻译
@IgnoreTrans
@GetMapping("/one")
public HttpResult<Student> student(){
	 return HttpResult.success(new Student());
}

七、其他配置

1、级联翻译

vo只有区县id,不级联只能拿到区县名称,通过级联就可以拿到地市名称,省份名称。
下面是一个手敲的例子,大家理解下:

class Area{
    int id;
    String name;
    @Trans(type=simple,target=City.class,fields={"cityName","provinceId"},refs={"cityName","provinceId"},sort=0)
    int cityId;
    String cityName;
    @Trans(type=simple,target=Province.class,fields={"provinceName"},refs={"provinceName"},sort=1)
    int provinceId;
    String provinceName;
}

上述代码,表里只有cityId字段,但是可以拿到provinceName给前端展示,称之为级联。

级联翻译中,sort参数非常重要,代表翻译的先后关系,一定不能配置错误

2、平铺模式

不开启平铺(默认):

 {name:"zhangsan",friendUserId:1,transMap:{friendUserName:"小明"}}

开启平铺

 {name:"zhangsan",friendUserId:1,friendUserName:"小明"}

开启方式:

easy-trans.is-enable-tile=true

3、指定字段翻译/不翻译

比如userController 的 list接口和info接口都复用了UserVo,UserVo中包含A,B 2个字段需要翻译。
list接口只需要A字段的翻译结果,不需要翻译B字段,info接口需要翻译A,B 2个字段。

使用@TransSett注解实现

@TransSett(exclude = {"B"},include = {"A"}) 加到controller方法上即可。
excludeinclude配置一个就可以了。exclude是排除哪些字段,剩下的字段都翻译,include是只翻译哪几个,其他的不翻译。
如果2个属性都配置了,以include为准。

八、反向翻译

支持把POJO中的名字(张三)翻译为张三的id,字典 男 翻译为 字典码0;
完全支持:mysql,postgresql
不完美支持:sqlserver (CONCAT 字段在where 后面过滤不出来数据)
其他数据库: 理论上支持CONCAT 关键字就可以,支持自定义扩展。

1、字典使用

private String genderName = "男;

@UnTrans(type=UnTransType.DICTIONARY,dict="gender",refs = {"genderName"})
private Integer gender;

2、SIMPLE(自动联表)

1、根据某个字段查询其主键/唯一键 赋值

private String schoolName;
@UnTrans(type= UnTransType.SIMPLE,tableName = "school",refs = "schoolName",columns = "school_name",uniqueColumn = "id")
private Integer schoolId;

2、根据多个字段(组合唯一键) 查询主键赋值

private String studentName;
private String studentAge;
@UnTrans(type= UnTransType.SIMPLE,tableName = "t_user",refs = {"studentName","studentAge"},columns = {"name","age"},uniqueColumn = "user_id")

private Integer userId;

3、多表怎么搞?
tableName 可以自己写join 比如 t_company c join t_org o on o.company_id = c.id 然后columns 给 [“c.name”,“o.name”]

3、手动翻译

transService.unTransMore(list);
transService.unTransOne(pojo)

九、工作原理

1、通过EasyTransResponseBodyAdvice 拦截所有的ResponseBody

@Slf4j
@ControllerAdvice
@ConditionalOnProperty(name = "easy-trans.is-enable-global", havingValue = "true")
public class EasyTransResponseBodyAdvice implements ResponseBodyAdvice {

    /**
     * 开启平铺模式
     */
    @Value("${easy-trans.is-enable-tile:false}")
    private Boolean isEnableTile;

    @Autowired
    private TransService transService;

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        // 如果主动指定了忽略某个方法,则不执行翻译
        if (methodParameter.getExecutable().isAnnotationPresent(IgnoreTrans.class)) {
            return o;
        }
        Set<String> includeFields = null;
        Set<String> excludeFields = null;
        if (methodParameter.getExecutable().isAnnotationPresent(TransSett.class)) {
            TransSett transSett = methodParameter.getExecutable().getAnnotation(TransSett.class);
            if (transSett.include().length != 0) {
                includeFields = new HashSet<>(Arrays.asList(transSett.include()));
            } else {
                excludeFields = new HashSet<>(Arrays.asList(transSett.exclude()));
            }
        }
        Object result = null;
        try {
            result = TransUtil.transOne(o, transService, isEnableTile, new ArrayList<>(), includeFields, excludeFields);
        } catch (Exception e) {
            log.error("翻译错误", e);
        }
        return result == null ? o : result;
    }
}

2、通过TransMethodResultAop拦截所有被@TransSett标注的方法

@Slf4j
@Aspect
public class TransMethodResultAop implements InitializingBean {

    /**
     * 开启平铺模式
     */
    @Value("${easy-trans.is-enable-tile:false}")
    private Boolean isEnableTile;


    /**
     * 支持vo包装类是map
     */
    @Value("${easy-trans.is-enable-map-result:false}")
    private Boolean isEnableMapResult;

    @Autowired
    private TransService transService;

    @Around("@annotation(com.fhs.core.trans.anno.TransMethodResult)")
    public Object transResult(ProceedingJoinPoint joinPoint) throws Throwable {
        Object proceed = null;
        Set<String> includeFields = null;
        Set<String> excludeFields = null;
        try {
            proceed = joinPoint.proceed();
            //1.获取用户行为日志(ip,username,operation,method,params,time,createdTime)
            //获取类的字节码对象,通过字节码对象获取方法信息
            Class<?> targetCls = joinPoint.getTarget().getClass();
            //获取方法签名(通过此签名获取目标方法信息)
            MethodSignature ms = (MethodSignature) joinPoint.getSignature();
            //获取目标方法上的注解指定的操作名称
            Method targetMethod =
                    targetCls.getDeclaredMethod(
                            ms.getName(),
                            ms.getParameterTypes());
            if (targetMethod.isAnnotationPresent(TransSett.class)) {
                TransSett transSett = targetMethod.getAnnotation(TransSett.class);
                if (transSett.include().length != 0) {
                    includeFields = new HashSet<>(Arrays.asList(transSett.include()));
                }else{
                    excludeFields = new HashSet<>(Arrays.asList(transSett.exclude()));
                }
            }
        } catch (Throwable e) {
            throw e;
        }
        try {
            return TransUtil.transOne(proceed, transService, isEnableTile, new ArrayList<>(),includeFields,excludeFields);
        } catch (Exception e) {
            log.error("翻译错误", e);
        }
        return proceed;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (isEnableMapResult) {
            TransUtil.transResultMap = true;
        }
    }
}

参考

  • http://easy-trans.fhs-opensource.cn

http://www.kler.cn/a/579139.html

相关文章:

  • 使用STM32CubeMX实现LED灯每秒闪烁一次(STM32G070CBT6单片机)
  • 雷池WAF的为什么选择基于Docker
  • 密码学(终极版)
  • neo4j-解决neo4j网页版打不开
  • 011---UART协议的基本知识(一)
  • 车载网络测试-DBC文件解读【创建修改DBC】
  • 深入理解JavaScript中的深拷贝与浅拷贝
  • 人工智能里的深度学习指的是什么?
  • Android Framework 常见面试题
  • Python 文件和异常(写入文件)
  • clickhouse修改和删除数据
  • 特征表示深度解析:颜色、纹理、形状与编码
  • linux学习(五)(服务器审查,正常运行时间负载,身份验证日志,正在运行的服务,评估可用内存)
  • 观看文艺汇演问题
  • Scala 中的String常量池
  • 网络原理--JVM简介
  • 微服务与无服务器:我的理解与实践
  • C#实现软件重启的功能
  • Mysql表的复合查询
  • Java初级入门学习