easy-trans java数据通用翻译框架v2.3.0

之前的easy-trans是基于jdk1.8和springboot2.x的版本研发的,随着jdk和springboot的升级,以及项目中使用过程中的问题,给这个框架增加了一些新功能。之前的框架介绍见这篇文章。

我开源了一个通用的数据翻译框架easy-trans

新版本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,相信它会成为你项目中的得力助手。

github开源地址:

easy-trans

相关推荐
2601_9620558115 分钟前
Spring全部注解
java·后端·spring
chjif27 分钟前
一张照片和一段声音如何生成口播视频:MiniMax H3 音画同步实测
java·前端·javascript
何以解忧,唯有..37 分钟前
LangChain 中 Pydantic 格式输出:结构化输出的完整指南
java·开发语言·langchain
旧梦95271 小时前
Java 捕获多个异常:从基础语法到最佳实践
java·开发语言
2601_962189601 小时前
【SpringBoot】使用IDEA创建SpringBoot项目
java·spring boot·intellij-idea
步行cgn1 小时前
Spring Boot 启动报错:MissingServletWebServerFactoryBean 的排查与解决
java·spring boot·后端
XLYcmy1 小时前
HTML/CSS/JS 基础与 Vue 技术栈深度解析
java·前端·css·html·vue3·vue2·api
江畔柳前堤1 小时前
字节跳动产品全景图:从应用表象到技术深海的七层解剖
人工智能·chatgpt·架构·json·batch
风123456789~1 小时前
【架构专栏】2.7 多媒体 知识点
架构