⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 一只小闪闪 「一只小闪闪」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

背景

为啥会有这个项目呢?因为我在做架构设计时,发现大多数项目中,或多或少的需要一些“翻译”的地方,比如:字典、分类、标签、基础元数据等。比较常用的做法是,把几张表的关联查询结果返回给前端,现在用的比较多的都是自动化 orm 框架,大概逻辑如下:

// 查询a表数据
List<A> alist = aDao.findAll(query);
// 根据a表数据,获取b表id
List<Long> bids = alist.stream().map(a::bid).tolist();
// 根据b表id 查询b表数据 并映射成map
Map<Long,B> bMap = bDao.findAll(bids).stream().toMap(b::id,x->x);
// 组装数据
。。。。

这些逻辑随处可见,如果是关联 join 查询,压力都给到了 db。如果我有一部分数据提前放到了缓存中,那这种方式的局限性就暴露出来了。另外,不少同事觉得,这种代码写多了就比较烦,因此,需要一款框架来帮助我们实现这种转换或者说是翻译的逻辑。

调研了一下市面上的框架,发现有一款框架 easy_trans 能解决这个问题,但是,这个框架对于我们来说,也有一些问题:

  1. 框架太重了,比如框架引入了 redis 做缓存,有些项目没有 redis,启动需要手动排除
  2. 数据翻译的实体需要继承框架默认的接口,改造较多
  3. 翻译后的字段是生成在增加 map 字段里面的,和前端联调时不方便,因为用的是 swagger 生成文档的,希望翻译后的字段能在文档中展示
  4. 自己造轮子,顺便也能更好的公司内部推广

由于,大部分的场景下,翻译的需求比较简单,因此,决定自己开发一款框架解决这些问题。

架构设计

参考了easy_trans的源码,并设想翻译框架和spring validation一样,通过自定义不同注解,拓展翻译功能。
架构如下:

因此,设计一个翻译核心注解。

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

/**
* @return 需要翻译的字段
*/
String trans() default "";

/**
* @return 提取的字段
*/
String key() default "";

/**
* @return 翻译数据获取仓库
*/
Class<? extends TransRepository> using();

}

优点

  1. 核心源码简单,仅几百行,无任何依赖项;
  2. 高度可拓展,拓展逻辑仅仅只需要实现TransRepository接口
  3. 支持数据库翻译、字典翻译、集合翻译、嵌套翻译等
  4. 并行翻译,翻译不同字段是并行翻译的,性能高
  5. 拥有完整的知识产权,信创可以大胆的用

四、基本使用

maven 引入。

<dependency>
<groupId>io.github.orangewest</groupId>
<artifactId>easy-trans-core</artifactId>
<version>0.0.3</version>
</dependency>

比如现在有一个老师的实体对象。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TeacherDto {

private Long id;

private String name;

// 关联教哪个学科
private Long subjectId;

}

课程科目实体对象。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SubjectDto {

private Long id;

private String name;
}

学生实体对象。

@Data
public class UserDto {
private Long id;

private String name;

private String sex;

@DictTrans(trans = "sex", group = "sexDict")
private String sexName;

private String job;

@DictTrans(trans = "job", group = "jobDict")
private String jobName;

// 关联老师id
private Long teacherId;

@Trans(trans = "teacherId", key = "name", using = TeacherTransRepository.class)
private String teacherName;

@Trans(trans = "teacherId", key = "subjectId", using = TeacherTransRepository.class)
private Long subjectId;

@Trans(trans = "subjectId", using = SubjectTransRepository.class, key = "name")
private String subjectName;

public UserDto(Long id, String name, Long teacherId, String sex, String job) {
this.id = id;
this.name = name;
this.teacherId = teacherId;
this.sex = sex;
this.job = job;
}
}

我们在 teacherName 上增加 @Trans 的注解,其中trans 指名需要翻译哪个字段,key说明需要使用的是哪个字段,using 说明的是使用哪个数据仓库获取数据 TeacherTransRepository 代码如下:

public class TeacherTransRepository implements TransRepository {
@Override
public Map<Object, Object> getTransValueMap(List<Object> transValues, Annotation transAnno) {
return getTeachers().stream().filter(x -> transValues.contains(x.getId())).collect(Collectors.toMap(TeacherDto::getId, x -> x));
}

public List<TeacherDto> getTeachers() {
List<TeacherDto> teachers = new ArrayList<>();
teachers.add(new TeacherDto(1L, "老师1", 1L));
teachers.add(new TeacherDto(2L, "老师2", 2L));
teachers.add(new TeacherDto(3L, "老师3", 3L));
teachers.add(new TeacherDto(4L, "老师4", 4L));
return teachers;
}
}

模拟根据 id 查询,获取指定 id 的数据。SubjectTransRepository。

public class SubjectTransRepository implements TransRepository {

@Override
public Map<Object, Object> getTransValueMap(List<Object> transValues, Annotation transAnno) {
return getSubjects().stream().filter(x -> transValues.contains(x.getId())).collect(Collectors.toMap(SubjectDto::getId, x -> x));
}

public List<SubjectDto> getSubjects() {
List<SubjectDto> subjects = new ArrayList<>();
subjects.add(new SubjectDto(1L, "语文"));
subjects.add(new SubjectDto(2L, "数学"));
subjects.add(new SubjectDto(3L, "英语"));
subjects.add(new SubjectDto(4L, "物理"));
return subjects;
}
}

注册翻译仓库:

TransRepositoryFactory.register(new TeacherTransRepository());
TransRepositoryFactory.register(new SubjectTransRepository());
TransRepositoryFactory.register(new DictTransRepository(new DictLoader() {
@Override
public Map<String, String> loadDict(String dictGroup) {
return dictMap().getOrDefault(dictGroup, new HashMap<>());
}

private Map<String, Map<String, String>> dictMap() {
Map<String, Map<String, String>> map = new HashMap<>();
map.put("sexDict", new HashMap<>());
map.put("jobDict", new HashMap<>());
map.get("sexDict").put("1", "男");
map.get("sexDict").put("2", "女");
map.get("jobDict").put("1", "学习委员");
map.get("jobDict").put("2", "生活委员");
map.get("jobDict").put("3", "宣传委员");
map.get("jobDict").put("4", "班长");
map.get("jobDict").put("5", "团支书");
map.get("jobDict").put("6", "团长");
return map;
}

}));

代码测试:

@Test
void trans() {
UserDto userDto = new UserDto(1L, "张三", 2L, "1", "2");
System.out.println("翻译前:" + userDto);
transService.trans(userDto);
System.out.println("翻译后:" + userDto);
List<UserDto> userDtoList = new ArrayList<>();
UserDto userDto2 = new UserDto(2L, "李四", 1L, "2", "1");
UserDto userDto3 = new UserDto(3L, "王五", 2L, "1", "3");
UserDto userDto4 = new UserDto(4L, "赵六", 3L, "2", "4");
userDtoList.add(userDto4);
userDtoList.add(userDto3);
userDtoList.add(userDto2);
System.out.println("翻译前:" + userDtoList);
transService.trans(userDtoList);
System.out.println("翻译后:" + userDtoList);
}

结果输出

翻译前:UserDto(id=1, name=张三, sex=1, sexName=null, job=2, jobName=null, teacherId=2, teacherName=null, subjectId=null, subjectName=null)
翻译后:UserDto(id=1, name=张三, sex=1, sexName=男, job=2, jobName=生活委员, teacherId=2, teacherName=老师2, subjectId=2, subjectName=数学)
翻译前:[UserDto(id=4, name=赵六, sex=2, sexName=null, job=4, jobName=null, teacherId=3, teacherName=null, subjectId=null, subjectName=null), UserDto(id=3, name=王五, sex=1, sexName=null, job=3, jobName=null, teacherId=2, teacherName=null, subjectId=null, subjectName=null), UserDto(id=2, name=李四, sex=2, sexName=null, job=1, jobName=null, teacherId=1, teacherName=null, subjectId=null, subjectName=null)]
翻译后:[UserDto(id=4, name=赵六, sex=2, sexName=女, job=4, jobName=班长, teacherId=3, teacherName=老师3, subjectId=3, subjectName=英语), UserDto(id=3, name=王五, sex=1, sexName=男, job=3, jobName=宣传委员, teacherId=2, teacherName=老师2, subjectId=2, subjectName=数学), UserDto(id=2, name=李四, sex=2, sexName=女, job=1, jobName=学习委员, teacherId=1, teacherName=老师1, subjectId=1, subjectName=语文)]

高级功能

自定义注解

使用@Trans注解标注在自定义注解上即可,自定义注解中需要有trans方法。

示例:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
// 使用@Trans标注
@Trans(using = TeacherTransRepository.class)
public @interface TeacherTrans {

/**
* 需要翻译的字段
*/
String trans() default "";

/**
* key 提取的字段
*/
String key() default "";
}
@Data
public class UserDto2 {
private Long id;
private String name;
private List<Long> teacherIds;

private List<String> jobIds;

@DictTrans(trans = "jobIds", group = "jobDict")
private List<String> jobNames;

@TeacherTrans(trans = "teacherIds", key = "name")
private List<String> teacherName;

@TeacherTrans(trans = "teacherIds", key = "subjectId")
private List<Long> subjectIds;

@Trans(using = SubjectTransRepository.class, trans = "subjectIds", key = "name")
private List<String> subjectNames;

public UserDto2(Long id, String name, List<Long> teacherIds, List<String> jobIds) {
this.id = id;
this.name = name;
this.teacherIds = teacherIds;
this.jobIds = jobIds;
}
}

测试。

@Test
void trans2() {
List<Long> teacherIds = new ArrayList<>();
teacherIds.add(1L);
teacherIds.add(2L);
List<String> jobIds = new ArrayList<>();
jobIds.add("1");
jobIds.add("2");
UserDto2 userDto = new UserDto2(1L, "张三", teacherIds, jobIds);
System.out.println("翻译前:" + userDto);
transService.trans(userDto);
System.out.println("翻译后:" + userDto);
List<UserDto2> userDtoList = new ArrayList<>();
UserDto2 userDto2 = new UserDto2(2L, "李四", teacherIds, jobIds);
List<Long> teacherIds2 = new ArrayList<>();
teacherIds2.add(3L);
teacherIds2.add(4L);
List<String> jobIds2 = new ArrayList<>();
jobIds2.add("3");
jobIds2.add("4");
UserDto2 userDto3 = new UserDto2(3L, "王五", teacherIds2, jobIds2);
UserDto2 userDto4 = new UserDto2(4L, "赵六", teacherIds2, jobIds2);
userDtoList.add(userDto4);
userDtoList.add(userDto3);
userDtoList.add(userDto2);
System.out.println("翻译前:" + userDtoList);
transService.trans(userDtoList);
System.out.println("翻译后:" + userDtoList);
}

结果输出。

翻译前:UserDto2(id=1, name=张三, teacherIds=[1, 2], jobIds=[1, 2], jobNames=null, teacherName=null, subjectIds=null, subjectNames=null)
翻译后:UserDto2(id=1, name=张三, teacherIds=[1, 2], jobIds=[1, 2], jobNames=[学习委员, 生活委员], teacherName=[老师1, 老师2], subjectIds=[1, 2], subjectNames=[语文, 数学])
翻译前:[UserDto2(id=4, name=赵六, teacherIds=[3, 4], jobIds=[3, 4], jobNames=null, teacherName=null, subjectIds=null, subjectNames=null), UserDto2(id=3, name=王五, teacherIds=[3, 4], jobIds=[3, 4], jobNames=null, teacherName=null, subjectIds=null, subjectNames=null), UserDto2(id=2, name=李四, teacherIds=[1, 2], jobIds=[1, 2], jobNames=null, teacherName=null, subjectIds=null, subjectNames=null)]
翻译后:[UserDto2(id=4, name=赵六, teacherIds=[3, 4], jobIds=[3, 4], jobNames=[宣传委员, 班长], teacherName=[老师3, 老师4], subjectIds=[3, 4], subjectNames=[英语, 物理]), UserDto2(id=3, name=王五, teacherIds=[3, 4], jobIds=[3, 4], jobNames=[宣传委员, 班长], teacherName=[老师3, 老师4], subjectIds=[3, 4], subjectNames=[英语, 物理]), UserDto2(id=2, name=李四, teacherIds=[1, 2], jobIds=[1, 2], jobNames=[学习委员, 生活委员], teacherName=[老师1, 老师2], subjectIds=[1, 2], subjectNames=[语文, 数学])]

值提取

有些翻译类型匹配结果的时候是通过值去匹配的,比如说字典翻译,我们需要根据字典值去字典组里面去匹配数据,框架里面只需要标记 key 的值用#val即可实现,比如框架自带的 @DictTrans 注解。

@Trans(using = DictTransRepository.class, key = "#val")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DictTrans {
/**
* @return 需要翻译的字段
*/
String trans();

/**
* 字典组
*
* @return 字典分组
*/
String group();
}

包装类翻译

有些类是包装类,比如返回的结果,返回的分页对象等,需要翻译的数据一般都是里面的实际业务对象,这时候,需要我们去配置解析包装类的解析器。
示例:

@Data
@AllArgsConstructor
public class Result<T> {
private T data;

private String message;
}

配置解析器,实现TransObjResolver接口即可

public class ResultResolver implements TransObjResolver {
@Override
public boolean support(Object obj) {
return obj instanceof Result;
}

@Override
public Object resolveTransObj(Object obj) {
return ((Result<?>) obj).getData();
}
}
TransObjResolverFactory.register(new ResultResolver());

测试:

@Test
void trans3() {
List<Long> teacherIds = new ArrayList<>();
teacherIds.add(1L);
teacherIds.add(2L);
List<String> jobIds = new ArrayList<>();
jobIds.add("1");
jobIds.add("2");
jobIds.add("3");
UserDto2 userDto = new UserDto2(1L, "张三", teacherIds, jobIds);
Result<UserDto2> result = new Result<>(userDto, "success");
System.out.println("翻译前:" + result);
transService.trans(result);
System.out.println("翻译后:" + result);
UserDto2 userDto2 = new UserDto2(2L, "李四", teacherIds, jobIds);
Result<UserDto2> result2 = new Result<>(userDto2, "success");
Result<Result<UserDto2>> result3 = new Result<>(result2, "success");
System.out.println("翻译前:" + result3);
transService.trans(result3);
System.out.println("翻译后:" + result3);
}

结果输出:

翻译前:Result(data=UserDto2(id=1, name=张三, teacherIds=[1, 2], jobIds=[1, 2, 3], jobNames=null, teacherName=null, subjectIds=null, subjectNames=null), message=success)
翻译后:Result(data=UserDto2(id=1, name=张三, teacherIds=[1, 2], jobIds=[1, 2, 3], jobNames=[学习委员, 生活委员, 宣传委员], teacherName=[老师1, 老师2], subjectIds=[1, 2], subjectNames=[语文, 数学]), message=success)
翻译前:Result(data=Result(data=UserDto2(id=2, name=李四, teacherIds=[1, 2], jobIds=[1, 2, 3], jobNames=null, teacherName=null, subjectIds=null, subjectNames=null), message=success), message=success)
翻译后:Result(data=Result(data=UserDto2(id=2, name=李四, teacherIds=[1, 2], jobIds=[1, 2, 3], jobNames=[学习委员, 生活委员, 宣传委员], teacherName=[老师1, 老师2], subjectIds=[1, 2], subjectNames=[语文, 数学]), message=success), message=success)

与springboot集成

maven 引入。

<dependency>
<groupId>io.github.orangewest</groupId>
<artifactId>easy-trans-spring-start</artifactId>
<version>0.0.3</version>
</dependency>

翻译仓库实现TransRepository,翻译解析器实现TransObjResolver,并在实现类上标注@Component;在需要翻译的对象属性上面标注好相关注解;需要翻译方法上使用@AutoTrans注解,框架会自动拦截需要翻译的对象,实现翻译。

@GetMapping("/query")
@AutoTrans
public Result<PageData<BizDTO>> page(Query query) {
PageData<BizDTO> page = bizService.page(query);

return new Result<PageData<BizDTO>>().ok(page);
}

与orm框架集成

通常很多翻译需求,都是根据id去查询实体对象,我们可以通过orm框架对此进行统一翻译。

示例:与mybatis-plus集成,定义翻译注解。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Trans(using = DbTransRepository.class)
public @interface DbTrans {

String trans();

String key() default "";

/**
* 数据库目标class
*/
Class<? extends BaseEntity> entity() default BaseEntity.class;
}

翻译仓库实现。

@Component
public class DbTransRepository implements TransRepository {

@Resource
private TransDriver transDriver;

@Override
public Map<Object, Object> getTransValueMap(List<Object> transValues, Annotation transAnno) {
if (transAnno instanceof DbTrans) {
DbTrans dbTrans = (DbTrans) transAnno;
List<? extends BaseEntity> entities = transDriver.findByIds(getIds(transValues), dbTrans.entity());
return entities.stream().collect(Collectors.toMap(BaseEntity::getId, x -> x));
}
return Collections.emptyMap();
}

/**
* 获取查询id
*/
private List<Long> getIds(List<Object> transValues) {
return transValues.stream()
.map(x -> (Long) x)
.collect(Collectors.toList());
}
}
public interface TransDriver {

/**
* 根据ids获取集合
*
* @param ids ids
* @param targetClass 目标类类名
*/
List<? extends BaseEntity> findByIds(List<? extends Serializable> ids, Class<? extends BaseEntity> targetClass);
}
@Component
public class MybatisTransDriver implements TransDriver {
@Override
public List<? extends BaseEntity> findByIds(List<? extends Serializable> ids, Class<? extends BaseEntity> targetClass) {
try (SqlSession sqlSession = SqlHelper.sqlSession(targetClass)) {
BaseMapper<? extends BaseEntity> mapper = SqlHelper.getMapper(targetClass, sqlSession);
return mapper.selectBatchIds(ids);
}
}
}

以上轮子思路,欢迎大家借鉴,也希望大家都能够获得更好的 kpi 🤣,以及其它收获。

文章目录
  1. 1. 背景
  2. 2. 架构设计
  3. 3. 优点
  4. 4. 四、基本使用
  5. 5. 高级功能
    1. 5.1. 自定义注解
    2. 5.2. 值提取
    3. 5.3. 包装类翻译
    4. 5.4. 与springboot集成
    5. 5.5. 与orm框架集成