之前的easy-trans是基于jdk1.8和springboot2.x的版本研发的,随着jdk和springboot的升级,以及项目中使用过程中的问题,给这个框架增加了一些新功能。之前的框架介绍见这篇文章。
新版本easy-trans是基于jdk21和springboot4.x。
功能简介
- 注解驱动:翻译关系写在字段上,业务代码零侵入。
- 数据源可插拔(双形态仓库) :实现
TransRepository接口,或在任意对象的方法上标注@TransMethod------一个方法即是一个仓库,DTO 侧按仓库名路由,无需为每个数据源建类。 - 拦截器链 :仓库查询被组织成环绕式拦截器链,内置请求合并(
CoalescingInterceptor)与分批(BatchInterceptor);Spring 下另有缓存(@TransCache)、重试(@TransRetry)、并发限制(@TransRateLimit);声明式治理;自定义拦截器实现TransInterceptor即可插入链路。 - 并行取数 & 请求级去重 :同一对象内不同数据源并行查询(虚拟线程);单次
trans()调用内对查询做请求级去重,减少重复查库。 - 多级嵌套(两套机制) :同类级联树 (
省→市→县,按字段引用关系自动构建,内置环检测);@TransNest递归翻译 嵌套对象 / 集合(如List<OrderDto>、子AddressDto),全图一次性批量翻译。 - 集合 / 数组翻译 :源或目标为
List/Set/ 数组时,框架按元素批量翻译,保持顺序。 - 包装对象 & 异步/响应式 :
Result<T>、PageData<T>等业务对象经TransValueResolver自定义拆包;CompletableFuture/Mono/Flux等内置拆包支持。 - 对象填充 :当目标字段类型与仓库返回类型一致时直接填入整个对象,否则按
key提取属性。 - 不可变 DTO(Java record) :支持 record 作为被翻译的 DTO。
- 可观测(通用测量总线) :定义
TransMetrics抽象接口,Spring 下自动桥接 Micrometer Observation(Timer + 链路追踪),亦可自定义后端(日志 / OpenTelemetry)。 - 零运行时依赖 :
easy-trans-core仅依赖 JDK,可嵌入任意项目,亦可用于 GraalVM Native Image。
一、record支持
Java Record 作为不可变数据载体,在新版本中得到原生支持。你可以在 Record 组件上直接使用 @TransRepo 和 @Trans 注解,框架会按声明顺序完成级联翻译。
record定义如下:
java
record ChainRecord(
@TransRepo(using = CityTransRepository.class) Long areaId,
@Trans(trans = "areaId", key = "name", using = CityTransRepository.class) String areaName,
@Trans(trans = "areaId", key = "pid", using = CityTransRepository.class)
@TransRepo(using = CityTransRepository.class) Long cityId,
@Trans(trans = "cityId", key = "name") String cityName,
@Trans(trans = "cityId", key = "pid") Long provinceId,
@Trans(trans = "provinceId", key = "name", using = CityTransRepository.class) String provinceName) {
}
java
public class CityTransRepository implements TransRepository<Long, CityEntity> {
@Override
public Map<Long, CityEntity> getTransValueMap(List<Long> transValues) {
return data().stream()
.filter(x -> transValues.contains(x.getId()))
.collect(Collectors.toMap(CityEntity::getId, x -> x));
}
private List<CityEntity> data() {
List<CityEntity> cityEntities = new ArrayList<>();
cityEntities.add(new CityEntity(1L, "湖南省", 0L));
cityEntities.add(new CityEntity(2L, "长沙市", 1L));
cityEntities.add(new CityEntity(3L, "株洲市", 1L));
cityEntities.add(new CityEntity(4L, "湘潭市", 1L));
cityEntities.add(new CityEntity(5L, "雨花区", 2L));
cityEntities.add(new CityEntity(6L, "岳麓区", 2L));
cityEntities.add(new CityEntity(7L, "长沙县", 2L));
cityEntities.add(new CityEntity(8L, "测试县", 10L));
return cityEntities;
}
}
使用如下:
java
ChainRecord origin = new ChainRecord(5L, null, null, null, null, null);
ChainRecord translated = transService.trans(origin);
Assertions.assertEquals("雨花区", translated.areaName());
Assertions.assertEquals(2L, translated.cityId(), "中间结果 cityId 来自缓冲区而非原实例");
Assertions.assertEquals("长沙市", translated.cityName());
Assertions.assertEquals(1L, translated.provinceId());
Assertions.assertEquals("湖南省", translated.provinceName());
具体示例见TransRecordTest。
二、缓存、限流、重试支持
之前的设计是让用户在自己定义的repository里面,去实现相关的缓存、限流、重试等功能的的。后面使用的过程中,发现有很多通用的代码,比如缓存,实现其实就是会先判断传过来的key,有没有,有的话,直接从缓存获取,没有的话,从数据库中获取,然后再填充缓存。伪代码如下:
java
// ❌ 以前用户自己实现缓存:每个 repository 都要写一遍这种重复逻辑
@Override
public Map<Long, CityEntity> getTransValueMap(List<Long> keys) {
Map<Long, CityEntity> result = new HashMap<>();
List<Long> missed = new ArrayList<>();
// 1. 先查缓存
for (Long key : keys) {
CityEntity cached = cache.get(key);
if (cached != null) {
result.put(key, cached);
} else {
missed.add(key);
}
}
// 2. 未命中的查数据库
if (!missed.isEmpty()) {
List<CityEntity> dbList = mapper.selectBatchIds(missed);
for (CityEntity entity : dbList) {
cache.put(entity.getId(), entity); // 回填
result.put(entity.getId(), entity);
}
}
return result;
}
后面,重构增加了Repository拦截器链,这样就能方便实现一些这些通用功能。
java
public interface TransInterceptor {
/**
* 是否介入该仓库目标。入参是只读的 {@link RepositoryTarget}
* (无 {@code invoke}),从类型上保证匹配阶段不会误推进链路。默认全部介入;
* 治理拦截器通常按 {@link RepositoryTarget#hasAnnotation} 判定。
*/
default boolean supports(RepositoryTarget target) {
return true;
}
/**
* 拦截仓库执行。
*
* @param keys 待翻译的键列表(可能已被上游拦截器裁剪)
* @param invoker 剩余链路;调用 {@code invoker.invoke(keys)} 推进
* @return 翻译结果
*/
Map<Object, Object> intercept(List<Object> keys, RepositoryInvoker invoker);
}
缓存实现如下:
java
public class CacheInterceptor implements TransInterceptor {
private final CacheManager cacheManager;
public CacheInterceptor(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public boolean supports(RepositoryTarget target) {
return target.hasAnnotation(TransCache.class);
}
@Override
public Map<Object, Object> intercept(List<Object> keys, RepositoryInvoker invoker) {
// 无 CacheManager(用户未引入缓存)时优雅降级:直接透传,不做缓存。
if (cacheManager == null) {
return invoker.invoke(keys);
}
TransCache anno = invoker.target().getAnnotation(TransCache.class);
String cacheName = anno.cacheNames().isEmpty()
? invoker.target().getRepoName()
: anno.cacheNames();
org.springframework.cache.Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
return invoker.invoke(keys);
}
Map<Object, Object> result = new HashMap<>();
List<Object> missed = new ArrayList<>();
for (Object key : keys) {
org.springframework.cache.Cache.ValueWrapper cached = cache.get(key);
if (cached != null) {
result.put(key, cached.get());
} else {
missed.add(key);
}
}
if (!missed.isEmpty()) {
Map<Object, Object> loaded = invoker.invoke(missed);
for (Map.Entry<Object, Object> e : loaded.entrySet()) {
cache.put(e.getKey(), e.getValue());
result.put(e.getKey(), e.getValue());
}
}
return result;
}
}
使用:
java
@TransCache(cacheNames = "test")
static class CachedRepo {
}
现在框架里面已经内置了@TransCache,@TransRateLimit,@TransRetry等注解。
三、GraalVM Native Image支持
框架内置了RuntimeHints,原生支持Native Image,springboot项目只要正常引入,自动注册翻译相关的类、方法、字段等反射信息,使得 Spring Boot 应用可以无缝打包为 GraalVM Native Image,具体demo可以见easy-trans-demo-aot,不需要用户进行任何配置操作。
四、递归嵌套 @TransNest
当 DTO 里嵌着另一个也需要翻译的 DTO(或其 List / Set / 数组)时,用 @TransNest 标记该字段即可,框架会递归处理嵌套结构。
java
class UserDto {
@TransRepo(using = OrderTransRepository.class)
private Long orderId;
@Trans(trans = "orderId", key = "statusName")
private String orderStatusName; // UserDto 自身的翻译
@TransNest
private List<OrderDto> orders; // 每个 OrderDto.statusName 自动被填
@TransNest
private AddressDto address; // AddressDto.cityName 自动被填
}
class OrderDto {
@TransRepo(using = StatusTransRepository.class)
private Long statusId;
@Trans(trans = "statusId", key = "name")
private String statusName;
}
调用 transService.trans(userDto) 后,所有层级的翻译字段都会被填充,且整个嵌套图内的批量查询会合并优化,避免 N+1 问题
五、上下文传播
之前使用过程中,经常会有需要从上下文里面获取用户信息,但是,之前会有并行查询,这样导致,用户的上下文会丢失,新版本里面增加了相关的SPI,可以保存用户的上下文信息。
java
public interface TransContextPropagator {
/**
* 在调用线程上抓取当前上下文快照。返回值将原样传给同一批并行任务的 {@link #restore(Object)}。
*
* @return 上下文快照(可为 {@code null})
*/
Object capture();
/**
* 在并行虚拟线程任务内、执行仓库查询<b>之前</b>调用,恢复 {@link #capture()} 得到的快照。
*
* @param snapshot {@link #capture()} 的返回值
*/
void restore(Object snapshot);
/**
* 在并行任务结束后({@code finally})调用,清理本虚拟线程上被写入的上下文,避免线程复用时的污染。
*/
void clear();
}
使用示例:
java
public class SpringSecurityContextPropagator implements TransContextPropagator {
@Override
public Object capture() {
return SecurityContextHolder.getContext();
}
@Override
public void restore(Object snapshot) {
if (snapshot instanceof SecurityContext) {
SecurityContextHolder.setContext((SecurityContext) snapshot);
}
}
@Override
public void clear() {
SecurityContextHolder.clearContext();
}
}
结语
新版本 easy-trans 在保持原有易用性的同时,大幅增强了扩展性、性能和可观测性,并拥抱了 Java 生态的最新成果(JDK 21 虚拟线程、Record、GraalVM Native)。无论是简单的单表翻译,还是复杂嵌套 + 缓存 + 限流的企业级场景,都能游刃有余。
如果你正在寻找一个轻量、高效、无侵入的翻译框架,不妨试试新版 easy-trans,相信它会成为你项目中的得力助手。