第六章 Kettle(PDI)解锁脚本组件:数据处理的可编程利器

文章目录

在数据集成与ETL(Extract-Transform-Load)领域,Kettle(现已更名为PDI,Pentaho Data Integration)以其可视化、低代码的特性被广泛应用。然而,面对复杂的业务逻辑、个性化的数据处理需求时,仅依靠基础组件往往难以满足要求。此时,脚本组件作为PDI中"可编程"的核心利器,能够帮助开发者突破可视化组件的限制,实现高度定制化的数据处理。本文将从脚本组件的用途、优缺点、常用组件、使用方法、典型场景、实战案例及注意事项等方面,全面解锁PDI脚本组件的强大能力。

一、脚本组件的核心用途

PDI脚本组件的本质是通过编程语言扩展PDI的数据处理能力,其核心用途可概括为以下几点:

1. 突破可视化组件的功能边界

PDI的基础组件(如"字段选择""过滤记录""合并行"等)仅能处理标准化场景,而脚本组件可应对非结构化、高复杂度的需求。例如:

  • 对JSON/XML格式数据进行深层嵌套解析;
  • 实现自定义加密/解密算法(如国密SM4);
  • 基于复杂规则(如多字段组合+正则表达式)清洗数据。

2. 对接外部系统与API

通过脚本组件可直接调用外部接口或服务,实现PDI与其他系统的无缝集成:

  • 调用RESTful API获取实时数据(如天气、汇率);
  • 连接数据库执行复杂存储过程或自定义SQL;
  • 与消息队列(如Kafka、RabbitMQ)交互实现数据流转。

3. 实现复杂业务逻辑

对于涉及多步骤、多条件判断的业务场景,脚本组件可通过代码逻辑高效实现:

  • 动态生成SQL语句(如根据日期拼接表名);
  • 基于多字段计算生成新指标(如风控评分模型);
  • 实现数据分片、分批处理(如千万级数据分批次入库)。

4. 提升数据处理效率

在大数据量场景下,脚本组件可通过优化算法或并行处理逻辑提升性能:

  • 对数据进行预过滤,减少后续步骤的数据量;
  • 使用缓存机制复用计算结果(如字典表映射);
  • 调用原生Java方法处理数据(比PDI基础组件更高效)。

二、脚本组件的优缺点分析

脚本组件在扩展PDI功能的同时,也存在一定的局限性,深入理解其优缺点有助于开发者在实际场景中合理选择使用方式。

1. 优点

(1)高度灵活性与定制化能力

脚本组件支持完整的编程语言语法(如Java的类、方法、异常处理),可实现任意复杂的业务逻辑。例如:

  • 针对非标准格式的数据(如自定义日志格式),可通过正则表达式+条件分支实现精准解析;
  • 对于动态变化的业务规则(如随季节调整的折扣计算模型),可通过脚本参数化配置,避免频繁修改ETL流程。
(2)无缝集成外部资源与技术栈

脚本组件可直接调用外部库、API或系统,打破PDI原生组件的生态限制:

  • Java脚本可集成Apache Commons、Jackson等工具库,简化JSON处理、加密等操作;
  • Python脚本可调用Pandas、Scikit-learn等数据科学库,在ETL过程中嵌入机器学习模型(如实时预测用户流失概率);
  • 通过HTTP客户端库(如OkHttp、Requests)对接企业内部系统API,实现数据实时同步。
(3)处理效率优化潜力大

对于大数据量场景,脚本组件可通过算法优化提升性能:

  • 实现数据缓存机制(如将字典表加载到内存HashMap),避免重复查询数据库;
  • 采用批量处理模式(如每1000条数据批量写入),减少I/O次数;
  • 利用多线程并行处理(需结合PDI的并行步骤配置),提升CPU利用率。
(4)复用已有代码资产

企业通常积累了大量成熟的业务代码(如Java工具类、Python数据处理脚本),脚本组件可直接复用这些资产:

  • 将Java工具类打包为Jar,通过UDJC组件引入PDI,避免重复开发;
  • 调用已有Python数据清洗脚本,快速集成到ETL流程中,降低迁移成本。
(5)支持复杂状态管理

PDI基础组件通常只能处理单行数据,而脚本组件可维护跨记录的状态信息:

  • 计算累计值(如用户全年消费总额、月度环比增长率);
  • 实现数据去重逻辑(如基于内存Set存储已处理ID);
  • 跟踪数据变化趋势(如连续3个月销量下滑的商品预警)。

2. 缺点

(1)学习成本高

脚本组件需要开发者掌握相应的编程语言(如Java、JavaScript),且需熟悉PDI的API规范(如UDJC的RowListener接口):

  • 非开发背景的ETL工程师可能难以上手,需额外学习编程知识;
  • PDI的脚本API文档不够完善,部分功能需通过源码或实践摸索。
(2)调试难度大

与可视化组件的"所见即所得"不同,脚本逻辑的错误定位较为复杂:

  • 代码中的语法错误、空指针异常可能导致整个转换崩溃;
  • 数据处理逻辑错误(如计算错误)需通过日志或断点调试,效率低于可视化组件的预览功能;
  • 跨记录状态管理(如缓存数据)的错误难以复现和排查。
(3)性能风险

若脚本编写不当,可能导致性能瓶颈:

  • 频繁在processRow方法中创建对象(如每次处理都新建JSONObject)会引发内存频繁GC;
  • 复杂的循环或递归逻辑可能导致单条数据处理耗时过长,拖累整体ETL效率;
  • 脚本引擎(如Jython)的执行效率通常低于原生Java代码,大规模数据处理时差异明显。
(4)维护成本高

脚本组件的逻辑隐藏在代码中,可读性和可维护性低于可视化组件:

  • 后续开发者需阅读代码才能理解业务逻辑,而可视化组件通过流程连线即可大致判断功能;
  • 代码版本管理复杂,需与PDI作业/转换文件同步维护;
  • 脚本逻辑修改后需重新测试,回归验证成本高于可视化组件的参数调整。
(5)兼容性问题

脚本组件对环境依赖较高,易出现兼容性问题:

  • Java版本升级可能导致UDJC组件中使用的旧API失效;
  • 第三方Jar包版本冲突(如不同组件引用同一库的不同版本)可能引发NoClassDefFoundError
  • 非Java脚本(如Python)依赖Jython等中间层,版本兼容性问题较常见。

三、PDI常用脚本组件及特性

PDI提供的脚本组件数量虽不多,但覆盖了主流编程语言和使用场景。常用组件包括以下4类:

1. User Defined Java Expression(用户定义Java表达式)

  • 定位:轻量级字段计算组件,基于Java表达式生成新字段。
  • 特性
    • 支持Java语法的表达式(如字符串拼接、数值计算、三目运算符);
    • 可调用PDI内置函数(如date2string()replace());
    • 仅能处理当前行数据,无法跨记录操作。
  • 适用场景:简单字段转换(如格式转换、条件赋值)。

2. Java Script Value(JavaScript值)

  • 定位:基于JavaScript引擎处理单条记录的字段计算。
  • 特性
    • 支持ECMAScript语法,可编写多行脚本;
    • 通过row对象访问当前行字段(如row.id);
    • 支持自定义函数和变量,逻辑复杂度高于Java表达式。
  • 适用场景:中等复杂度的行级处理(如多条件判断、字符串正则处理)。

3. User Defined Java Class(用户定义Java类)

  • 定位:功能最强大的脚本组件,支持完整Java类编写。
  • 特性
    • 需实现PDI的RowListener接口(重写processRow()方法);
    • 可访问PDI的环境变量、步骤元数据和全局对象;
    • 支持跨记录操作(如累计计算、数据缓存)和外部资源调用。
  • 适用场景:复杂业务逻辑(如批量数据校验、自定义数据结构处理)。

4. Script(脚本)

  • 定位:支持多种脚本语言的通用组件(如Python、Groovy、BeanShell)。
  • 特性
    • 可选择脚本引擎(默认BeanShell,需额外配置Python/Groovy环境);
    • 通过trans_scripts对象与PDI交互(如获取输入行、输出结果);
    • 适合熟悉非Java语言的开发者。
  • 适用场景:复用已有Python/Groovy脚本(如数据科学模型集成)。

四、脚本组件的使用方法

不同脚本组件的使用流程存在差异,以下分别介绍核心组件的详细操作步骤:

1. User Defined Java Expression(UDJE)使用步骤

  1. 添加组件:从PDI的"转换"面板拖拽"用户定义Java表达式"至画布,连接输入步骤。
  2. 配置字段 :双击组件打开配置窗口,在"字段"选项卡点击"新建":
    • 输入目标字段名(如full_name);
    • 选择数据类型(如String);
    • 在"表达式"框输入Java表达式(如row.get("first_name") + " " + row.get("last_name"))。
  3. 验证与运行:点击"验证表达式"确认语法正确,保存后运行转换。

示例:将"生日"字段转换为年龄

java 复制代码
// 计算年龄(当前年份 - 出生年份)
java.util.Calendar.getInstance().get(java.util.Calendar.YEAR) - Integer.parseInt(row.get("birth_year").toString())

2. Java Script Value(JSV)使用步骤

  1. 添加组件:拖拽"JavaScript值"至画布,连接输入步骤。
  2. 配置脚本 :双击组件,在"脚本"选项卡编写逻辑:
    • 通过row.字段名访问输入字段(如var id = row.id;);
    • 通过row.新字段名 = 值定义输出字段(如row.age_group = age > 18 ? "成年" : "未成年";)。
  3. 声明输出字段:在"字段"选项卡点击"获取字段",自动识别脚本中定义的新字段并确认数据类型。

示例:清洗手机号格式(去除空格和特殊字符)

javascript 复制代码
// 去除手机号中的非数字字符
var rawPhone = row.phone || "";
row.clean_phone = rawPhone.replace(/\D/g, "");
// 校验手机号长度
row.is_valid = row.clean_phone.length === 11 ? "1" : "0";

3. User Defined Java Class(UDJC)使用步骤

UDJC是功能最复杂的脚本组件,需遵循PDI的Java接口规范:

  1. 添加组件:拖拽"用户定义Java类"至画布,连接输入和输出步骤。

  2. 编写Java类:双击组件,在"类代码"选项卡编写代码,核心结构如下:

    java 复制代码
    import org.pentaho.di.trans.steps.userdefinedjavaclass.*;
    import org.pentaho.di.trans.step.*;
    import org.pentaho.di.core.row.*;
    import java.util.*;
    
    public class MyUDJC implements RowListener {
        // 初始化方法(仅执行一次)
        public boolean init(StepMetaInterface smi, StepDataInterface sdi) {
            return true;
        }
    
        // 处理每行数据的核心方法
        public Object[] processRow(StepMetaInterface smi, StepDataInterface sdi) throws Exception {
            // 获取输入行
            Object[] inputRow = getRow();
            if (inputRow == null) { // 无数据时返回null,结束处理
                setOutputDone();
                return null;
            }
    
            // 获取字段索引(通过字段名)
            int nameIndex = getInputRowMeta().indexOfValue("name");
            int scoreIndex = getInputRowMeta().indexOfValue("score");
    
            // 读取字段值
            String name = getInputRowMeta().getString(inputRow, nameIndex);
            Double score = getInputRowMeta().getNumber(inputRow, scoreIndex);
    
            // 业务逻辑:根据分数评级
            String level = score >= 90 ? "A" : (score >= 60 ? "B" : "C");
    
            // 构建输出行(输入行+新字段)
            Object[] outputRow = RowDataUtil.addValueData(inputRow, getInputRowMeta().size(), level);
            return outputRow;
        }
    
        // 清理资源(可选)
        public void dispose(StepMetaInterface smi, StepDataInterface sdi) {}
    }
  3. 配置输出字段 :在"字段"选项卡点击"获取字段",PDI会自动识别代码中新增的字段(如level),需手动指定数据类型(如String,长度2)。

  4. 导入依赖(可选):若代码引用外部Jar包,需在"依赖项"选项卡添加Jar文件路径。

4. Script组件(Python示例)使用步骤

  1. 环境准备 :在PDI安装目录的lib文件夹放入Jython Jar包(如jython-standalone-2.7.3.jar),重启PDI支持Python脚本。

  2. 添加组件:拖拽"脚本"至画布,连接输入步骤,双击打开配置窗口。

  3. 选择脚本类型:在"脚本类型"下拉框选择"Python",在"脚本"选项卡编写代码:

    python 复制代码
    # 获取输入行
    input_row = trans_scripts.getInputRow()
    if input_row is None:
        trans_scripts.setOutputDone()
    
    # 读取字段值(字段名区分大小写)
    price = input_row.get("price")
    quantity = input_row.get("quantity")
    
    # 计算总价(处理空值)
    total = price * quantity if (price and quantity) else 0.0
    
    # 添加输出字段
    input_row.put("total", total)
    
    # 输出行
    trans_scripts.putOutputRow(input_row)
  4. 配置输出字段 :在"输出字段"选项卡手动添加新字段(如total,类型Number)。

五、脚本组件的典型使用场景

脚本组件在实际业务中应用广泛,以下是几个高频场景:

1. 数据清洗与标准化

场景描述 :原始数据存在格式混乱(如日期格式混杂yyyy-MM-ddMM/dd/yyyy、手机号带区号等),需统一标准化。
解决方案 :使用JavaScript Value组件,通过正则表达式和条件判断清洗数据。
示例:统一日期格式

javascript 复制代码
var rawDate = row.raw_date || "";
var standardDate = "";
// 匹配yyyy-MM-dd格式
if (/^\d{4}-\d{2}-\d{2}$/.test(rawDate)) {
    standardDate = rawDate;
}
// 匹配MM/dd/yyyy格式并转换
else if (/^\d{2}\/\d{2}\/\d{4}$/.test(rawDate)) {
    var parts = rawDate.split("/");
    standardDate = parts[2] + "-" + parts[0] + "-" + parts[1];
}
row.standard_date = standardDate;

2. 实时API数据集成

场景描述 :ETL过程中需调用外部API(如获取用户的地理位置信息)补充数据。
解决方案 :使用User Defined Java Class组件,通过Java的HttpURLConnectionOkHttp库调用API。
核心代码片段

java 复制代码
// 调用IP地址解析API
String ip = getInputRowMeta().getString(inputRow, ipIndex);
String apiUrl = "http://ip-api.com/json/" + ip;

// 发送HTTP请求
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
    response.append(line);
}
in.close();

// 解析JSON响应(使用org.json库)
JSONObject json = new JSONObject(response.toString());
String country = json.getString("country");
String city = json.getString("city");

// 添加到输出行
Object[] outputRow = RowDataUtil.addValueData(inputRow, getInputRowMeta().size(), country, city);

3. 复杂业务指标计算

场景描述 :根据用户的消费记录、登录次数、会员等级等多维度数据计算用户活跃度评分(0-100分)。
解决方案 :使用User Defined Java Class组件,实现加权评分算法。
核心逻辑

java 复制代码
// 读取字段值
int loginCount = getInputRowMeta().getInteger(inputRow, loginIndex); // 月登录次数
double consumeAmount = getInputRowMeta().getNumber(inputRow, consumeIndex); // 月消费金额
int memberLevel = getInputRowMeta().getInteger(inputRow, memberIndex); // 会员等级(1-5)

// 计算各项得分(加权求和)
int loginScore = Math.min(loginCount * 2, 30); // 登录分(最高30)
int consumeScore = Math.min((int)(consumeAmount / 100), 50); // 消费分(最高50)
int memberScore = memberLevel * 4; // 会员分(最高20)
int totalScore = loginScore + consumeScore + memberScore;

// 添加到输出行
Object[] outputRow = RowDataUtil.addValueData(inputRow, getInputRowMeta().size(), totalScore);

4. 大数据量分批处理

场景描述 :处理千万级订单数据时,需每1000条批量插入数据库以提升性能。
解决方案 :使用User Defined Java Class组件,缓存数据并批量提交。
核心代码片段

java 复制代码
private List<Object[]> batchData = new ArrayList<>();
private int batchSize = 1000;

public Object[] processRow(...) throws Exception {
    Object[] inputRow = getRow();
    if (inputRow == null) {
        // 处理剩余数据
        if (!batchData.isEmpty()) {
            insertBatch(batchData); // 自定义批量插入方法
            batchData.clear();
        }
        setOutputDone();
        return null;
    }
    
    batchData.add(inputRow);
    if (batchData.size() >= batchSize) {
        insertBatch(batchData);
        batchData.clear();
    }
    return null; // 不输出单行,由批量插入处理
}

// 批量插入逻辑(使用JDBC)
private void insertBatch(List<Object[]> data) throws SQLException {
    String sql = "INSERT INTO orders (...) VALUES (...)";
    PreparedStatement pstmt = getDatabaseMeta().getConnection().prepareStatement(sql);
    for (Object[] row : data) {
        // 设置参数
        pstmt.setString(1, row[0].toString());
        // ...其他参数
        pstmt.addBatch();
    }
    pstmt.executeBatch();
    pstmt.close();
}

六、实战案例:电商用户标签生成系统

项目背景

某电商平台需根据用户行为数据(浏览、加购、购买、评价)生成用户标签(如"价格敏感型""高频购买用户"),用于精准营销。标签规则复杂且常变更,需通过脚本组件实现灵活配置。

技术方案

  1. 数据输入 :从Hive数据仓库读取用户行为表(user_behavior)和订单表(orders)。
  2. 数据预处理:使用"过滤记录"组件筛选近30天数据,通过"分组依据"组件聚合用户行为指标(如浏览次数、购买金额)。
  3. 标签计算 :使用User Defined Java Class组件实现标签规则:
    • 价格敏感型:近30天购买商品均价低于品类均价的70%;
    • 高频购买用户:近30天购买次数≥5次;
    • 活跃评价用户:近30天评价数≥3条且好评率≥90%。
  4. 数据输出 :将标签结果写入MySQL用户标签表(user_tags)。

核心代码实现(UDJC)

java 复制代码
import org.pentaho.di.trans.steps.userdefinedjavaclass.*;
import org.pentaho.di.trans.step.*;
import org.pentaho.di.core.row.*;
import java.util.*;

public class UserTagGenerator implements RowListener {
    // 品类均价(从参数获取,支持动态更新)
    private double categoryAvgPrice;

    public boolean init(StepMetaInterface smi, StepDataInterface sdi) {
        // 从PDI参数读取品类均价
        categoryAvgPrice = Double.parseDouble(getTrans().getParameterValue("CATEGORY_AVG_PRICE"));
        return true;
    }

    public Object[] processRow(StepMetaInterface smi, StepDataInterface sdi) throws Exception {
        Object[] inputRow = getRow();
        if (inputRow == null) {
            setOutputDone();
            return null;
        }

        // 获取字段索引
        int userIdIndex = getInputRowMeta().indexOfValue("user_id");
        int avgPriceIndex = getInputRowMeta().indexOfValue("avg_purchase_price");
        int buyCountIndex = getInputRowMeta().indexOfValue("purchase_count");
        int commentCountIndex = getInputRowMeta().indexOfValue("comment_count");
        int praiseRateIndex = getInputRowMeta().indexOfValue("praise_rate");

        // 读取字段值
        String userId = getInputRowMeta().getString(inputRow, userIdIndex);
        double avgPrice = getInputRowMeta().getNumber(inputRow, avgPriceIndex);
        int buyCount = getInputRowMeta().getInteger(inputRow, buyCountIndex);
        int commentCount = getInputRowMeta().getInteger(inputRow, commentCountIndex);
        double praiseRate = getInputRowMeta().getNumber(inputRow, praiseRateIndex);

        // 计算标签
        List<String> tags = new ArrayList<>();
        if (avgPrice < categoryAvgPrice * 0.7) {
            tags.add("价格敏感型");
        }
        if (buyCount >= 5) {
            tags.add("高频购买用户");
        }
        if (commentCount >= 3 && praiseRate >= 0.9) {
            tags.add("活跃评价用户");
        }

        // 标签拼接为字符串(用逗号分隔)
        String tagStr = String.join(",", tags);

        // 构建输出行(用户ID+标签)
        Object[] outputRow = new Object[2];
        outputRow[0] = userId;
        outputRow[1] = tagStr;

        return outputRow;
    }

    public void dispose(StepMetaInterface smi, StepDataInterface sdi) {}
}

运行效果

通过该方案,系统每日可处理500万用户数据,生成标签耗时约20分钟,标签准确率达98%。后续如需新增标签(如"复购用户"),仅需修改UDJC中的processRow方法,无需调整整体ETL流程,灵活性显著提升。

七、使用注意事项

脚本组件虽强大,但使用不当可能导致性能问题或逻辑错误,需注意以下几点:

1. 性能优化

  • 避免在循环中创建对象 :如Java脚本中,new JSONObject()等操作应放在processRow方法外(通过init初始化),减少内存开销。
  • 控制数据量:脚本组件处理速度慢于基础组件,建议先用"过滤记录""取样"等组件减少输入数据量。
  • 复用资源连接 :数据库连接、API客户端等资源应在init方法中创建,dispose方法中关闭,避免频繁创建/销毁。

2. 错误处理

  • 添加异常捕获 :脚本中必须包含try-catch块,避免单条数据错误导致整个转换失败。例如:

    java 复制代码
    try {
        // 业务逻辑
    } catch (Exception e) {
        // 记录错误日志
        logError("处理用户" + userId + "失败:" + e.getMessage());
        // 跳过错误行或返回默认值
        return RowDataUtil.addValueData(inputRow, getInputRowMeta().size(), "ERROR");
    }
  • 日志输出 :使用logBasic()logError()等方法记录关键信息,便于问题排查。

3. 兼容性与依赖

  • Java版本匹配 :UDJC组件依赖PDI的Java环境(通常为Java 8/11),避免使用高版本Java语法(如var关键字)。
  • 外部依赖管理 :引用第三方Jar包时,需确保所有节点(如集群环境)均已部署该Jar,避免ClassNotFoundException

4. 调试技巧

  • 分步测试:将脚本组件拆分为多个步骤,逐步验证逻辑(如先输出中间结果至文本文件)。
  • 使用预览功能:PDI的"预览"按钮可查看脚本处理后的前N行数据,快速定位字段错误。
  • 打印变量值 :在脚本中输出关键变量(如logBasic("当前价格:" + price)),辅助调试逻辑。

5. 安全性

  • 避免硬编码敏感信息:数据库密码、API密钥等应通过PDI的"命名参数"或"环境变量"传入,而非直接写在脚本中。
  • 过滤输入数据 :对用户输入的字符串(如nameaddress)进行转义处理,防止SQL注入或脚本注入攻击。

八、总结

脚本组件是PDI中应对复杂数据处理需求的"双刃剑"------既通过编程语言的灵活性突破了可视化组件的功能边界,又因学习成本和维护难度带来了新的挑战。本文从用途、优缺点、组件特性、使用方法、场景案例等方面全面解析了脚本组件的应用,并提供了性能优化和错误处理的实践建议。

在实际开发中,应遵循"能用基础组件就不用脚本,用脚本必考虑优化"的原则:简单字段转换用"User Defined Java Expression",中等逻辑用"JavaScript Value",复杂场景则优先使用"User Defined Java Class"。同时,需通过完善的测试、日志和文档降低维护成本,平衡灵活性与可靠性。

掌握PDI脚本组件,将助力开发者从"可视化工具使用者"升级为"数据处理架构师",在复杂业务场景中发挥更大价值。

相关推荐
聪明的笨猪猪4 小时前
Java JVM “内存(1)”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
程序员清风4 小时前
快手二面:乐观锁是怎么用它来处理多线程问题的?
java·后端·面试
一线大码5 小时前
SpringBoot 优雅实现接口的多实现类方式
java·spring boot·后端
花伤情犹在5 小时前
Java Stream 高级应用:优雅地扁平化(FlatMap)递归树形结构数据
java·stream·function·flatmap
yaoxin5211235 小时前
212. Java 函数式编程风格 - Java 编程风格转换:命令式 vs 函数式(以循环为例)
java·开发语言
摇滚侠5 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 属性优先级 行内写法 变量选择 笔记42
java·spring boot·笔记
滑水滑成滑头5 小时前
**发散创新:多智能体系统的探索与实践**随着人工智能技术的飞速发展,多智能体系统作为当今研究的热点领域,正受到越来越多关注
java·网络·人工智能·python
摇滚侠5 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 总结 热部署 常用配置 笔记44
java·spring boot·笔记
十年小站5 小时前
一、新建一个SpringBoot3项目
java·spring boot