数据分析——Kettle自定义Step插件编写

前言:公司业务需要开发Kettle的自定义Step插件,在查找资料的过程中发现网上关于Kettle的资料比较少,有的资料也比较简洁,因此记录一下自己demo插件的详细开发过程。

插件功能:用户输入需要替换的字符和替换后的字符以及需要替换的列号,插件进行自动替换,效果图如下

 

开发环境:

开发工具:IntelliJ IDEA 2020.1

开发环境:JDK1.8、Maven-3.6.3

Kettle版本:8.3.0.0-371

Kettle源代码:点击进入

插件示例代码:下载地址

官方文档:点击进入

注:本文将根据官方提供的示例代码编写Step插件。在打包插件时,如果出现Test报错的情况,则注释掉即可。

(1).下载并编译示例代码

1.下载示例代码

2.下载完成后将Step的示例代码解压出来,并使用idea打开

3.使用idea打开后,maven会去下载pom.xml下面的相关依赖,此时,我这里报错

Could not find artifact pentaho-kettle:kettle-sdk-plugin-parent:pom:8.3.0.0-371 in alimaven (https://maven.aliyun.com/repository/central)

原因是在阿里云的central仓库下没有pentaho-kettle,因此我们在pom.xml配置一下项目的仓库url即可

<build>
    ...
</build>
<repositories>
    <repository>
        <id>pentaho-public</id>
        <name>Pentaho Public</name>
        <url>http://nexus.pentaho.org/content/groups/omni</url>
    </repository>
</repositories>

4.等

这个步骤会持续较长时间,可以尝试自己换一个仓库或者通过不可描述的方法加快下载速度,但是由于需要下载的文件较大,所以还是需要耐心等待。

5.编译并打包示例插件

点击右侧Maven标签,然后选择package进行打包

完成后,在target目录下面会出现jar包和一个压缩文件,其中,jar包为插件,zip包中会包含jar包和一些资源文件(如果用到的话)

(2).添加并使用示例插件

1.将上述zip文件复制,并粘贴到Kettle的plugins目录下面,然后解压文件

2.打开Spoon,新建一个转换

3.在核心对象处搜索demo,如果在转换中看到了Demo Step,则插件添加成功

4.将插件拖入转换中,双击即可看到插件的界面,插件的具体功能请看官网的开发者中心,在这里就先不讲了

(3).开发前了解

建议大家还是先看一遍官方文档,在这里我只挑部分展示。

以下是我们至少要实现的:

PS.官方的示例代码中有英文注释,下文中会删除部分不重要的代码与注释

(4).插件编写——UI (DemoStepDialog.java)

在开始前,我们必须要了解一些方法:

DemoStepDialog.java(插件的对话框):

           open():在用户打开插件Dialog时被Spoon调用,仅当用户关闭对话框时返回,此方法必须在用户确认时返回这个步骤的名字,或者在用户取消时返回null。其中,changed标记必须反映对话框是否更改了步骤配置,用户取消时,标志位不能改变。

           populateDialog():在打开插件Dialog前进行数据填充,填充后显示Dialog,一般在shell.open()之前被open()调用。

           cancel()、ok():在点击"取消"或"确定"按钮后被调用,需要在open()中绑定控件的监听器。

1.需要的控件

我们的插件只需要一个LabelText来输入列号,一个TableView来输入替换前后的文本即可。实例插件中已经有一个wHelloFieldName的LabelText,因此我们只要再加一个TableView即可。(在代码中我将wHelloFieldName改为了changeColLabelText)

2.修改open()方法

将此类中wHelloFieldName全都修改为changeColLabelText后,为类增加一个变量:private TableView changeTableView,然后在open()方法中的changeColLabelText与wOK、wCancel中间加入以下代码来添加表格控件:

        final int FieldsCols = 2;
        final int FieldsRows = 10;
        Control lastControl = changeColLabelText;

        ColumnInfo[] colinf;
        colinf = new ColumnInfo[FieldsCols];
        colinf[0] = new ColumnInfo("替换前的字符", ColumnInfo.COLUMN_TYPE_TEXT, new String[]{""}, false);
        colinf[1] = new ColumnInfo("替换后的字符", ColumnInfo.COLUMN_TYPE_TEXT, new String[]{""}, false);

        changeTableView = new TableView(transMeta, shell, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI, colinf, FieldsRows, lsMod, props);

        FormData fd = new FormData();
        fd.left = new FormAttachment(0, 0);
        fd.top = new FormAttachment(lastControl, margin);
        fd.right = new FormAttachment(100, 0);
        fd.bottom = new FormAttachment(100, -50);
        changeTableView.setLayoutData(fd);
        lastControl = changeTableView;

此时,插件就变成了这个样子:

好像有点不对劲,不过没关系,让我们修改一下细节

        //需要修改的地方
        changeColLabelText = new LabelText(shell, "需要替换的列号", null);
        props.setLook(changeColLabelText);
        changeColLabelText.addModifyListener(lsMod);
        FormData fdValName = new FormData();
        fdValName.left = new FormAttachment(0, 0);
        fdValName.right = new FormAttachment(100, 0);
        fdValName.top = new FormAttachment(wStepname, margin);
        changeColLabelText.setLayoutData(fdValName);
        wOK = new Button(shell, SWT.PUSH);
        wOK.setText(BaseMessages.getString(PKG, "System.Button.OK"));
        wCancel = new Button(shell, SWT.PUSH);
        wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
        //需要修改的地方
        setButtonPositions(new Button[]{wOK, wCancel}, margin, lastControl);

此时,插件看起来就正常了,不过大家可以根据自己的需要自行修改

(5).插件编写——数据存储与绑定 (DemoStepDialog.java DemoStepMeta.java)

完成上面一步后,我们会发现,changeTableView中的文字在改变了以后,下次打开插件还是显示空,而changeColLabelText却不会出现此情况,那是因为changeColLabelText已经完成数据的存储与绑定。下面将介绍如何进行数据存储和绑定。

同样的,在开始前,我们必须要了解一些方法:

DemoStepMeta.java(插件元,可以将插件的变量保存在这个类):

           setDefault():每次创建Step时会被Spoon调用,一般在这里设置一些需要初始化的值。

           getter、setter:读、写插件变量的一些方法。

           clone():拷贝Step,必须在里面实现对变量的深拷贝

           getXML():在Step需要将其配置(一般指变量)序列化为XML时被Spoon调用。

           loadXML(...):在Step需要从XML加载其配置时,PDI会调用这个方法。

           saveRep(...):当Step需要将其配置(一般指变量)保存到存储库时被Spoon调用。

           readRep(...):在Step需要从存储库读取其配置时,PDI会调用这个方法。

           getFields(...):在Step对行流所做任何更改时,必须调用此方法。如果想在下一个步骤获取这个步骤新增的字段名称,必须在这里新增字段。

           check(...):在用户选择"Verify Transformation"时,Spoon将调用此方法

1.在插件元中添加变量

删去outputField,增加changeCol和changeStr,同时生成一下Getter和Setter。

    //删去outputField
    //@Injection(name = "OUTPUT_FIELD")
    //private String outputField;

    //增加changeCol来存储需要替换的列号
    private String changeCol;
    //增加changeStr来存储替换前后的字符
    private Map<String, String> changeStr;

    public String getChangeCol() {
        return changeCol;
    }

    public void setChangeCol(String changeCol) {
        this.changeCol = changeCol;
    }

    public Map<String, String> getChangeStr() {
        return changeStr;
    }

    public void setChangeStr(Map<String, String> changeStr) {
        this.changeStr = changeStr;
    }

2. 设置新建Step时初始化的值

    public void setDefault() {
        changeStr = new HashMap<>();
        changeStr.put("$", "dollar");
        changeStr.put("#", "sharp");
        setChangeStr(changeStr);
        setChangeCol("1,2,3");
    }

3.完成变量在配置中的读写

由于此处涉及到序列化与反序列化,我使用了FastJson,大家自行配置一下pom.xml,同时将fastjson的jar包放入Kettle的lib目录中。

    public String getXML() {
        StringBuilder xml = new StringBuilder();
        String obj = JSONObject.toJSONString(changeStr);
        xml.append(XMLHandler.addTagValue("changeStr", obj));
        xml.append(XMLHandler.addTagValue("changeCol", changeCol));
        return xml.toString();
    }
    public void loadXML(Node stepnode, List<DatabaseMeta> databases, IMetaStore metaStore) throws KettleXMLException {
        try {
            String obj = XMLHandler.getNodeValue(XMLHandler.getSubNode(stepnode, "changeStr"));
            setChangeStr(JSONObject.parseObject(obj, HashMap.class));
            setChangeCol(XMLHandler.getNodeValue(XMLHandler.getSubNode(stepnode, "changeCol")));
        } catch (Exception e) {
            throw new KettleXMLException("Demo plugin unable to read step info from XML node", e);
        }
    }

4.修改Dialog中的方法

    private void populateDialog() {
        wStepname.selectAll();
        Map<String, String> changeStr = meta.getChangeStr();
        Iterator iter = changeStr.entrySet().iterator();
        int row = 0;
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            Object key = entry.getKey();
            Object val = entry.getValue();
            changeTableView.setText(key.toString(), 1, row);
            changeTableView.setText(val.toString(), 2, row++);
        }
        changeColLabelText.setText(meta.getChangeCol());
    }
    private void ok() {
        stepname = wStepname.getText();
        int count = changeTableView.nrNonEmpty();
        Map<String, String> tableData = new HashMap<>();
        for (int i = 0; i < count; i++) {
            TableItem item = changeTableView.getNonEmpty(i);
            tableData.put(item.getText(1), item.getText(2));
        }
        meta.setChangeStr(tableData);
        meta.setChangeCol(changeColLabelText.getText());
        dispose();
    }

此时,重新创建一个Demo Step插件,它就会有初始值,同时改变以后也可以保存了。

(5).插件编写——业务逻辑实现 (DemoStep.java)

同样的,在开始前,我们必须要了解一些类与方法:

DemoStep.java(插件元,可以将插件的变量保存在这个类):

           init(...):初始化方法,可以建立数据库链接、获取文件句柄等操作,会被PDI调用。

           processRow(...):读取行的业务逻辑,会被PDI调用,当此方法返回false时,完成行读取。

           dispose():析构函数,用来释放资源,会被PDI调用​。​​​​​​

1.添加变量

    private List<String> changeColList;
    private Map<String, String> changeStr;

2.初始化

由于输入的时String类型的列号,以","分割,因此我们在初始化时需要处理一下,并保存到changeColList中

    public boolean init(StepMetaInterface smi, StepDataInterface sdi) {
        DemoStepMeta meta = (DemoStepMeta) smi;
        DemoStepData data = (DemoStepData) sdi;
        if (!super.init(meta, data)) {
            return false;
        }
        String changeColNamesStr = meta.getChangeCol();
        changeColList = new ArrayList<>();
        while (StringUtils.isNotBlank(changeColNamesStr)) {
            int i;
            String str;
            if ((i = changeColNamesStr.indexOf(",")) > 0) {
                str = changeColNamesStr.substring(0, i).trim();
                if (StringUtils.isNotBlank(str)) {
                    changeColList.add(str);
                }
                changeColNamesStr = changeColNamesStr.substring(i + 1);
            } else {
                str = changeColNamesStr.trim();
                if (StringUtils.isNotBlank(str)) {
                    changeColList.add(str);
                }
                break;
            }
        }
        changeStr = meta.getChangeStr();
        return true;
    }

之后只需要在processRow(...)中实现具体的业务逻辑即可

    public boolean processRow(StepMetaInterface smi, StepDataInterface sdi) throws KettleException {
        DemoStepMeta meta = (DemoStepMeta) smi;
        DemoStepData data = (DemoStepData) sdi;
        //从输入流中读取一行
        Object[] r = getRow();

        //若读不到下一行,则读写完成,调用setOutputDone(),return false
        if (r == null) {
            setOutputDone();
            return false;
        }

        if (first) {
            first = false;
            //如果是第一行则保存数据行元信息到data类中,后续使用
            data.outputRowMeta = (RowMetaInterface) getInputRowMeta().clone();
        }

        //字符替换的业务逻辑
        for (int i = 0; i < r.length && r[i] != null; i++) {
            String str = data.outputRowMeta.getString(r, i);
            if (changeColList.indexOf((i + 1) + "") >= 0) {
                Iterator iter = changeStr.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    Object before = entry.getKey();
                    Object after = entry.getValue();
                    str = str.replace(String.valueOf(before), String.valueOf(after));
                }
                //很重要,必须用getBytes()
                r[i] = str.getBytes();
            }
        }

        //将行放入输出行流
        putRow(data.outputRowMeta, r);

        //如有需要,可以进行日志记录
        if (checkFeedback(getLinesRead())) {
            logBasic(BaseMessages.getString(PKG, "DemoStep.Linenr", getLinesRead()));
        }

        //返回true则表示还应继续使用processRow()读取下一行
        return true;
    }

在这里有个大坑,输出String类型必须要用getBytes()方法,之后我应该会写一篇博客介绍我遇到的这个坑。

至此,我们的插件编写完成。

(6).打包测试

把我们写好的插件打包,设置一下我们的字段,跑一下,就能出我们想要的结果

 

当然,我们也可以在调用populateDialog()之后添加如下代码来让TableView变得稍微好看那么一点点

        changeTableView.removeEmptyRows();
        changeTableView.setRowNums();
        changeTableView.optWidth(true);

效果如下

至此,简单的Kettle自定义Step插件编写完成。

由于本人也是自己看源代码、文档和注释摸索的,同时英语也不是很好,因此有错误的话也拜托大家多多包涵,谢谢。

代码下载地址:https://download.csdn.net/download/xhy1999/12805227

GitHub地址:https://github.com/xhy1999/kettle-sdk-step-plugin-demo

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值