转转技术

同源异构:Jolt如何解决质检报告的差异化展示难题

  • 1 背景
  • 2 Jolt简介
    • 2.1 什么是Jolt?
    • 2.2 为什么Jolt与众不同?
    • 2.3 Jolt的核心优势
    • 2.4 核心概念一览
    • 2.5 Jolt的典型应用场景
  • 3 场景化实践
    • 3.1 改造蓝图:基于Jolt的报告生成流程
    • 3.2 Jolt语法速成: 快速掌握核心操作
    • 3.3 在Spring Boot中集成Jolt
  • 4 未来规划

1 背景

在二手奢侈品行业,一份专业的质检报告是建立信任的基石。然而,随着业务的深入发展,我们发现了一个有趣的现象:

同样一份质检数据,在不同人眼中却有着完全不同的价值。

  • B端商户 更关心商品的真伪鉴定结果、成色等级评定等影响定价的核心信息。
  • C端卖家 则更在意验货流程的透明度、瑕疵详情等影响交易决策的要素。
  • 平台运营 需要从差异分析角度,持续优化验货标准和流程。

更棘手的是,这些关注点并非一成不变。市场在变,用户在变,需求自然也在变。昨天还是次要信息的配件完整度,今天可能就成了用户最关心的焦点。

面对频繁变化的展示需求,我们曾经试过最"简单直接"的方法——改代码。结果可想而知:开发疲于奔命,业务等待焦急,系统越来越难维护。

如果展示逻辑可以像配置文件一样灵活调整,如果业务人员也能参与报告格式的设计,如果一份数据真的可以"随需而变"……

这正是Jolt给我们带来的改变。

2 Jolt简介

2.1 什么是Jolt?

Jolt是一个专注于JSON到JSON转换的Java库,由Bazaarvoice公司开源。它的核心理念很简单:用JSON来描述JSON的转换规则。

想象一下,如果数据转换也能像配置文件一样简单——这正是Jolt带来的革命性改变。

2.2 为什么Jolt与众不同?

在传统的开发模式中,数据转换往往意味着:

// 传统方式:硬编码的痛苦,需要隐藏数据或者调整数据位置都需要重新部署if (Objects.equals("B2B",userType)) {    merchantQualityReport.setAdditionalInfo(base.getAd());    merchantQualityReport.setBasicFunctionInfo(base.getBa());// ... 几十行甚至上百行的处理结构映射的代码逻辑} elseif (Objects.equals("B2C",userType)) {    qualityReport.setFinenessLevel(base.getFin());    qualityReport.setOtherDetailInfo(base.getOther());// ... 又是一大段不同的映射逻辑}

而使用Jolt,同样的逻辑变成了:

// 我们通过声明或内置指令可以实现动态转换和展示{"operation": "shift","spec": {"ad": "additionalInfo","base": "basicFunctionInfo"  }}

2.3 Jolt的核心优势

1. 配置化管理

  • 转换逻辑存储在JSON文件中,可以把转换规则放在配置中心
  • 无需重新编译,随时调整

2. 强大的转换能力

  • 支持复杂的嵌套结构转换,支持自定义操作符
  • 提供丰富的内置操作符
  • 可以处理数组、条件转换等高级场景

3. 链式操作

  • 多个转换可以串联执行
  • 每个转换专注做一件事
  • 复杂逻辑通过简单操作组合实现

2.4 核心概念一览

Jolt提供了几种基础操作,就像积木块一样,可以组合出强大的转换能力:

操作
作用
使用场景
shift
移动和重组数据
调整JSON结构,字段重命名
default
添加默认值
补充缺失字段
remove
删除字段
过滤敏感信息
sort
排序字段
规范化输出格式
cardinality
转换数据
单值与数组互转

2.5 Jolt的典型应用场景

  • API网关:统一不同微服务的数据格式
  • 数据迁移:新旧系统的数据格式转换
  • 报表系统:灵活的数据展示需求

3 场景化实践

3.1 改造蓝图:基于Jolt的报告生成流程

传统架构的问题分析

在引入Jolt之前,我们的质检报告面临着典型的"硬编码困境":

// 传统方式:每种报告都需要独立的转换类publicclassB2BReportConverter{public B2BReport convert(InspectionData data){        B2BReport report = new B2BReport();        report.setProductCode(data.getBasicInfo().getCode());        report.setAuthResult(data.getAuthentication().getResult());// ... 数百行的字段映射代码    }}publicclassB2CReportConverter{public B2CReport convert(InspectionData data){// 又是一套完全不同的转换逻辑    }}

痛点总结:

  • 每新增一种报告格式,需要新增一个转换类,每个转换类需要写大量逻辑。
  • 质检报告中质检项调整需要修改代码、测试、重新上线。

新流程设计思路

基于Jolt将转换逻辑从代码中解耦出来:针对不同的报告需求定义其对应的业务code,针对业务code编写Jolt规则。将所有Jolt规则统一存储在配置中心,支持热更新和版本管理的功能。Image

3.2 Jolt语法速成: 快速掌握核心操作

让我们先通过简单的示例快速掌握Jolt的核心语法。

3.2.1 shift操作 - 数据的搬运工

shift是Jolt最常用的操作,它就像一个精确的数据搬运工,可以将数据从一个位置移动到另一个位置。

基础示例:字段重命名

// 原始数据{"productName": "LV Neverfull","productPrice": 8500}// Jolt规则[  {"operation": "shift","spec": {"productName": "name",    // 将productName移动到name"productPrice": "price"// 将productPrice移动到price    }  }]// 输出结果{"name": "LV Neverfull","price": 8500}

进阶示例:数据重组

// 原始数据{"brand": "Hermès","model": "Birkin 30","color": "黑色","material": "Togo皮"}// Jolt规则 - 将平铺的字段组织成嵌套结构[  {"operation": "shift","spec": {"brand": "basicInfo.brand","model": "basicInfo.model","color": "appearance.color","material": "appearance.material"    }  }]// 输出结果{"basicInfo": {"brand": "Hermès","model": "Birkin 30"  },"appearance": {"color": "黑色","material": "Togo皮"  }}

3.2.2 default操作 - 默认值填充器

default操作用于为缺失的字段添加默认值,确保输出数据的完整性。

// 原始数据{"productId": "LX001","authenticity": "真品"}// Jolt规则 - 添加默认值[  {"operation": "default","spec": {"status": "待审核","priority": "normal","tags": ["二手奢侈品"],"inspection": {"required": true,"level": "standard"      }    }  }]// 输出结果{"productId": "LX001","authenticity": "真品","status": "待审核","priority": "normal","tags": ["二手奢侈品"],"inspection": {"required": true,"level": "standard"  }}

3.2.3 remove操作 - 敏感信息过滤器

remove操作用于删除不需要的字段,特别适合过滤敏感信息。

// 原始数据{"orderId": "2024001","customerName": "张女士","customerPhone": "138****1234","internalCost": 5000,"sellingPrice": 8500,"profit": 3500}// Jolt规则 - 删除内部敏感信息[  {"operation": "remove","spec": {"internalCost": "","profit": ""    }  }]// 输出结果{"orderId": "2024001","customerName": "张女士","customerPhone": "138****1234","sellingPrice": 8500}

3.2.4 modify-overwrite-beta操作 - 内容转换器

modify-overwrite-beta时Jolt提供数据转换的操作符,常用于数据格式化、类型转换等场景。

modify支持一系列函数,函数其本质是Java自带的,在Spec以下是函数及其对应的源码:

privatestaticfinal Map<String, Function> STOCK_FUNCTIONS = new HashMap<>(  );static {        STOCK_FUNCTIONS.put( "toLower", new Strings.toLowerCase() );        STOCK_FUNCTIONS.put( "toUpper", new Strings.toUpperCase() );        STOCK_FUNCTIONS.put( "concat", new Strings.concat() );        STOCK_FUNCTIONS.put( "join", new Strings.join() );        STOCK_FUNCTIONS.put( "split", new Strings.split() );        STOCK_FUNCTIONS.put( "substring", new Strings.substring() );        STOCK_FUNCTIONS.put( "trim", new Strings.trim() );        STOCK_FUNCTIONS.put( "leftPad", new Strings.leftPad() );        STOCK_FUNCTIONS.put( "rightPad", new Strings.rightPad() );        .....更多函数不多赘述    }

modify修改数据示例

/ 原始数据 - 质检员录入的原始数据{"brand": "LV","model": "Neverfull MM","price": 12800,"authenticity": true,"condition": "A"}// Jolt规则 - 转换为用户友好的展示格式[  {"operation": "modify-overwrite-beta", "spec": {"brand": "=concat(@(1,brand), ' - 路易威登')","model": "=toUpper(@(1,model))","price": "=concat('¥', @(1,price), '.00')","authenticity": "=toString(@(1,authenticity))","condition": "=concat('成色:', @(1,condition), '级')"    }  }]// 输出结果 - 格式化后的展示数据{"brand": "LV - 路易威登","model": "NEVERFULL MM","price": "¥12800.00","authenticity": "true","condition": "成色:A级"}

3.2.5 自定义操作符

在实际场景中,上面modify提供的操作符不能完全满足业务需求,因此我们就需要扩展自定义操作符。

首先定义方法实现

//一个equals的示例publicclassEqualsFunctionimplementsFunction{@Overridepublic Optional<Object> apply(Object... args){if (args == null || args.length < NumberConstant.NUMBER_2) {return Optional.empty();        }if (args[0] == null || args[1] == null) {return Optional.of(false);        }return Optional.of(Objects.equals(args[0], args[1]));    }}

然后需要自定义Transform

/** * 自定义Jolt转换器 - 支持自定义函数的数据转换 * 实现了SpecDriven接口(支持JSON配置)和ContextualTransform接口(支持上下文传递) */publicclassCustomizeTransformimplementsSpecDriven, ContextualTransform{// 根节点的转换规则对象,用于存储和执行转换逻辑privatefinal ModifierCompositeSpec rootSpec;/**     * 构造函数 - 初始化自定义转换器     * @param spec JSON格式的转换规则配置     */publicCustomizeTransform(Object spec){// 创建自定义的equals函数实例        EqualsFunction equalsFunction = new EqualsFunction();// 创建函数映射表,用于注册自定义函数        Map<String, Function> functionsMap = new HashMap<>();// 注册equals函数,使其可以在Jolt规则中通过"=equals(...)"调用        functionsMap.put("equals", equalsFunction);// 将函数映射表设为不可修改,确保线程安全        functionsMap = Collections.unmodifiableMap(functionsMap);// 创建模板规则构建器,指定操作模式为OVERWRITE(覆盖模式)// 并传入自定义函数映射表        TemplatrSpecBuilder templatrSpecBuilder = new TemplatrSpecBuilder(OpMode.OVERWRITR, functionsMap);// 构建根节点的转换规则// ROOT_KEY: 根节点标识符// spec: 用户提供的转换规则// OpMode.OVERWRITR: 覆盖模式,会替换已存在的值        rootSpec = new ModifierCompositeSpec(ROOT_KEY, (Map<String, Object>) spec, OpMode.OVERWRITR,                templatrSpecBuilder);    }/**     * 执行转换操作     * @param input 输入的JSON数据     * @param context 上下文信息(可包含额外的变量供转换时使用)     * @return 转换后的数据     */@Overridepublic Object transform(Object input, Map<String, Object> context){// 创建上下文包装器,将传入的上下文放在ROOT_KEY下// 这样可以在转换规则中通过路径访问上下文数据        Map<String, Object> contextWrapper = new HashMap<>();        contextWrapper.put(ROOT_KEY, context);// 创建根元素的匹配对象        MatchedElement rootLpe = new MatchedElement(ROOT_KEY);// 创建路径追踪器,用于记录当前处理的数据路径        WalkedPath walkedPath = new WalkedPath();// 将输入数据和根元素添加到路径中        walkedPath.add(input, rootLpe);// 应用转换规则// ROOT_KEY: 当前处理的键名// Optional.of(input): 包装的输入数据// walkedPath: 当前路径信息// null: 输出路径(这里不需要)// contextWrapper: 包含上下文的数据        rootSpec.apply(ROOT_KEY, Optional.of(input), walkedPath, null, contextWrapper);// 返回转换后的数据(原地修改)return input;    }}

最后就是jolt规则使用自定义操作符

[ {//自定义类"operation": "com.luxuryqc.jolt.CustomizeTransform", "spec": {// 生成总体评级"assessment.overallRating": "=equals(@(1,assessment.authenticityConfirmed), true) && =equals(@(1,assessment.qualityGrade), true) ? 'S级精品' : 'A级优品'"  }]

3.3 在Spring Boot中集成Jolt

将Jolt集成到Spring Boot项目中非常简单:

3.3.1 添加依赖

<dependency><groupId>com.bazaarvoice.jolt</groupId><artifactId>jolt-core</artifactId><version>0.1.6</version></dependency>

3.3.2 创建转换服务

@ServicepublicclassJoltTransformService{public <T> T transformData(QualityReportDo reportDo, String key, TypeReference<T> typeReference){try {//根据规则初始化chainr            Chainr chainr = Chainr.fromSpec(getRuleInfo(key));//进行规则转换            Object transform = chainr.transform(reportDo);if (transform == null) {thrownew BusinessException("transform is null");            }//返回数据return JsonUtil.silentString2Object(JsonUtil.silentObject2String(transform), typeReference);        } catch (Exception e) {            log.error("transformData error key: {} ", key, e);thrownew BusinessException("数据规则转换异常");        }    }}public List<Object> getRuleInfo(String key){//拉取jolt规则配置,可以配置在配置中心或者自定义数据表中        String rule = getRule(key);return JsonUtils.jsonToList(rule);}

3.3.3 调用侧使用

@RestController@RequestMapping("/api/report")publicclassReportController{@Autowiredprivate JoltTransformService transformService;public MerchantReport generateMerchantReport(String reportId){// 1. 获取原始质检数据        QualityReportDo reportDo = getQualityReport(reportId);// 2. 使用Jolt转换        MerchantReport merchantReport = joltTransformService.transformData(            reportDo, "MERCHANT_REPORT_RULE",  // 配置中心的规则keynew TypeReference<MerchantReport>() {}        );// 3. 业务后处理(如需要)        postProcessMerchantReport(merchantReport);return merchantReport;    }}

4 未来规划

虽然当前基于Jolt的质检报告方案已经满足了大部分业务灵活调整的需求,实现了从硬编码到配置化的重要跨越,但仍存在一定的技术门槛。目前的JSON配置方式要求使用者理解Jolt语法、熟悉JSON结构、掌握路径表达式等技术细节,这对于非技术背景的业务同学来说是一个不小的挑战。

因此,我们计划在下一阶段推出可视化拖拽组件库,彻底打破技术壁垒,真正实现"零代码"配置。通过直观的图形界面和拖拽交互,让业务同学能够像搭积木一样轻松定制质检报告,实现以下愿景: 未来的可视化方案将带来:

  • 所见即所得:实时预览报告效果,即时验证配置结果
  • 降低门槛:业务人员无需编程知识即可独立完成配置
  • 提升效率:从"天"级别的开发周期缩短到"分钟"级别的配置时间
  • 减少错误:通过可视化约束和智能提示,避免配置错误
  • 赋能业务:让最懂业务的人直接参与产品优化,快速响应市场变化

刘斌 转转回收技术部研发工程师

想了解更多转转公司的业务实践,欢迎点击关注下方公众号: