基于Eclipse搭建Kettle插件调试环境的两种方法

一、问题背景

对于具备Java开发背景知识的ETL技术人员而言,定制开发插件是一种最为灵活的解决方案。一般两种情况下需要定制插件,一是Kettle中不具备满足需要的组件,二是现有组件存在缺陷。虽然刚开始开发插件是情非得已,但一旦掌握Kettle插件的开发技术后,其随心所欲的定制能力会让人感觉如鱼得水,欲罢不能。

Kettle插件类型非常多,如Kettle 8.0版本支持的插件有25种。最常用的插件类型包括数据库(Database)、日志(Logging)、作业入口(JobEntry)、转换步骤(Step)、扩展点(ExtensionPoint)等。

插件的开发,需要具备类、接口、SWT等Java语言基础知识,也需要对Kettle内部架构有基本了解。如果想要透彻理解Kettle源码,搭建开发调试环境并实际动手制作插件应是一种捷径。本文将讲述基于Eclipse搭建Kettle插件调试环境的两种方法,并通过一个HelloKettle实例介绍插件开发的入门知识。


二、远程调试方法

远程调试方法最为简洁。该方法通过Java进程之间的调试协议(JDWP)实现Spoon、Kitchen、Pan等远程进程与本地集成开发环境(Eclipse、IntelliJ等)调试进程的通信,从而方便插件代码的调试开发。其中,远程进程涉及到的JVM主要参数解释如下:

  • dt_socket:使用的通信方式,包括套接字(dt_socket)和共享内存(dt_shmem)

  • server:是主动连接调试器还是作为服务器等待调试器连接

  • suspend:是否在启动JVM时就暂停,并等待调试器连接

  • address:地址和端口。地址可以省略,两者用冒号分隔

以Windows环境下Spoon.bat为例,需要加入的完整参数部分如下图1。

图1       

                    

配置前务必确保端口号(上例为4275)未被现有进程占用。在新版本(版本1.5以上)的Java虚拟机上,也可以将上述参数:

"-Xdebug""-Xnoagent" "-Djava.compiler=NONE""-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=4275"

修改为:

"-Xdebug""-Xnoagent" "-Djava.compiler=NONE""-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=4275"

下面以Windows环境下Spoon进程为例,详解远程调试方法各步骤。

1、修改配置文件并启动进程

按照上述参数配置,修改Spoon.bat,保存后双击Spoon.bat启动远程调试进程。

2、插件打包部署

Eclipse中,建立一个Java项目(假设命名为plugin_develop),编写好插件相应代码和资源文件,并编译打包(假设文件名为hello-kettle-0.1.jar)。在Kettle软件包的plugins文件夹中,新建一个文件夹(假设文件夹名称为kettle-doctor-plugin),将hello-kettle-0.1.jar拷贝到此文件夹中。图2为部署后的软件包结构。

图2

 

3、配置本地调试环境

在项目中调试位置设置断点,然后在Eclipse的Debug Configurations对话框中,选择Remote Java Application,并点击新建按钮。输入名称,选择项目,录入远程进程的地址和端口号,单击Debug按钮开始调试。详细配置如图3所示。

图3

在Spoon中进行插件相应操作,如满足断点激活条件,Eclipse会自动在断点处开启调试过程(如图4所示)。

图4

此方法的优点有:

  • 工作量小。只需一个普通JavaProject,引入Kettle核心库,即可进行开发调试。

  • 可进行远程调测。即使被调试进程不在本地机器上,也可以启动调试过程。

       此方法的缺点是:
  • 性能差。由于采用跨进程或者跨物理机器的远程通信,导致开发效率明显下降。

  • 工作效率低。如果要修改资源文件,则需要重新打包部署到plugins目录,并重启远程调试进程。如果这个部分频繁修改,那么开发效率比较低。

  • 不便于代码参考。很多情况下,现有核心插件代码都有部分可以重用或者参考。但在此方法中,由于Kettle内核以及其他插件的代码无法运行,导致可以参考的资源非常少,不便于提高对内核的理解。

 

三、本地调试方法

本地调试方法相较于远程调试方法,工作量偏大,但工作效率更高且易于加深对Kettle源码的理解。以下同样以Spoon进程为例,解释本地调试方法的各个步骤。

1、建立Kettle核心代码工程

通过Kettle开源项目源码地址,下载最新代码,并抽取core、engine、dbdialog、ui四个核心项目。各项目之间的依赖关系如下图5所示。

图5

这个过程比较繁琐,并对Java基础要求较高,但成功后可对Kettle开发相关技术有全盘了解。大多数接触Kettle源码的技术人员,会被Git上几百个Maven项目所吓倒,且在线编译打包成功多数依赖于网络条件。强烈建议只从Kettle的四个核心项目开始研究。

当然也欢迎获取本人制作好的压缩包。压缩包解压后,直接可以通过Eclipse的File/Import菜单,选择General/Existing Projects into Workspace,选择所有项目,点击Finish即可。完成后,得到项目结构如下图6所示,一套完整学习调试环境数秒钟内可以搭建成功。

图6

2、创建插件项目

按照第一个方法中类似步骤,建立一个插件项目,准备好代码和资源文件,假设项目名称为plugin_develop。

3、建立项目依赖

建立ui项目对plugin_develop项目的依赖关系,以便在Spoon进程的类路径中,能够找到插件相关类。具体配置如图7所示。

图7

4、配置启动类

配置一个JavaApplication的启动类。项目为kettle-ui,主类为org.pentaho.di.ui.spoon.Spoon。另外,需增加一个虚拟机参数:

-DKETTLE_PLUGIN_CLASSES=org.pentaho.di.trans.steps.hellokettle.HelloKettleMeta

其中等号后面为插件类名称,如果包含多个插件类需要调试,可以用逗号分隔。图8为该插件类的代码截图,@Step注解标识该插件类型为转换步骤插件。

图8

5、启动进程

通过上一步配置的应用,启动进程,开启调试,效果与图3类似,不再赘述。

如需从Spoon进程之外的其他入口进行调试,须知Pan与Kitchen进程都在kettle-engine项目中,主类分别为org.pentaho.di.pan.Pan和org.pentaho.di.kitchen.Kitchen。


四、插件编程要点

Kettle新版本中插件均基于注解方式进行编程(基于XML配置文件的插件继续支持)。对于本例的Step插件来说,一般需要编写四个类:

  • 对话框类:在Spoon中编辑调试时,每个步骤都有对应属性对话框,此类用于实现该对话框的显示、操作、数据校验等功能,一般可以从BaseStepDialog类继承,以重用基类的方法。主要代码如图9所示。

  • 元数据类:实现步骤数据结构控制、文件保存与读取、资源库保存与读取等功能,一般可以从BaseStepMeta类继承,以重用基类的方法,主要代码如图7所示。插件的类型,由注解决定,例如数据库(@DatabaseMetaPlugin)、日志(@LoggingPlugin)、作业入口(@JobEntry)、转换步骤(@Step)、扩展点(@ExtensionPoint)等。

  • 步骤类:实现步骤实际运行时对输入流数据的逐行操作并输出。一般可以从BaseStep继承,以重用基类的方法。主要代码如图10所示。

  • 数据类:用于步骤执行过程中状态数据处理,一般从BaseStepData类继承。本例无事实处理逻辑,所以该类无实际内容。

 

图9

图10


五、总结

本文介绍了Kettle插件调试环境的两种搭建方法,并制作了一个简单的插件实例。个人比较推荐第二种方法,因为随时可以从Kettle的核心源码中得到启示。

附件为插件源码的几个核心类,如有需要可以仔细研读。由于仅仅演示插件开发的框架,所以示例插件代码功能上有诸多局限(例如不能保存到资源库等)。

如需整套工程文件,请联系微信号carol_sxh有偿获取,添加好友时请注明所需文件名KettleSample005(内含全套Kettle6核心源代码和示例插件代码工程的压缩文件下载地址)。

 

【注意】本文转自博主公众号“Kettle博士”,本公众号所发文章皆为原创,如转载请注明出处及作者。


微信扫一扫,关注该公众号

 

附件:插件源代码清单

packageorg.pentaho.di.ui.trans.steps.hellokettle;

 

importorg.eclipse.swt.SWT;

importorg.eclipse.swt.events.ModifyEvent;

import org.eclipse.swt.events.ModifyListener;

importorg.eclipse.swt.events.SelectionAdapter;

importorg.eclipse.swt.events.SelectionEvent;

importorg.eclipse.swt.events.ShellAdapter;

importorg.eclipse.swt.events.ShellEvent;

importorg.eclipse.swt.layout.FormAttachment;

importorg.eclipse.swt.layout.FormData;

importorg.eclipse.swt.layout.FormLayout;

importorg.eclipse.swt.widgets.Button;

importorg.eclipse.swt.widgets.Control;

importorg.eclipse.swt.widgets.Display;

importorg.eclipse.swt.widgets.Event;

import org.eclipse.swt.widgets.Label;

importorg.eclipse.swt.widgets.Listener;

importorg.eclipse.swt.widgets.Shell;

importorg.eclipse.swt.widgets.Text;

importorg.pentaho.di.core.Const;

importorg.pentaho.di.i18n.BaseMessages;

import org.pentaho.di.trans.TransMeta;

importorg.pentaho.di.trans.step.BaseStepMeta;

importorg.pentaho.di.trans.step.StepDialogInterface;

importorg.pentaho.di.trans.steps.hellokettle.HelloKettleMeta;

importorg.pentaho.di.ui.core.widget.TextVar;

import org.pentaho.di.ui.trans.step.BaseStepDialog;

 

publicclass HelloKettleDialog extends BaseStepDialog implements StepDialogInterface {

 

    privatestatic Class<?> PKG = HelloKettleMeta.class;

    private HelloKettleMeta input;

    TextVar wtHelloMessage;

   public HelloKettleDialog(Shell parent, Object in, TransMeta transMeta, String sname) {

       super(parent, (BaseStepMeta) intransMetasname);

       input = (HelloKettleMeta) in;

    }

 

    @Override

    public String open() {

       Shell parent = getParent();

       Display display = parent.getDisplay();

       shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | SWT.MIN);

       props.setLook(shell);

       setShellImage(shellinput);

 

       // 文本修改时,通知元数据实例,数据已经发生了变化。

       ModifyListener lsMod = new ModifyListener() {

           publicvoid modifyText(ModifyEvent e) {

               input.setChanged();

           }

       };

 

       changed = input.hasChanged();

       FormLayout formLayout = new FormLayout();

       formLayout.marginWidth = Const.FORM_MARGIN;

       formLayout.marginHeight = Const.FORM_MARGIN;

 

       shell.setLayout(formLayout);

       shell.setText(BaseMessages.getString(PKG"ForcastSampleCreator.Stepname.Label"));

 

       intmiddle = props.getMiddlePct();

       intmargin = Const.MARGIN;

 

       // “步骤名称静态文本控件

       wlStepname = new Label(shell, SWT.RIGHT);

        wlStepname.setText(BaseMessages.getString(PKG"ForcastSampleCreator.Stepname.Label"));

       props.setLook(wlStepname);

       // 步骤名称静态文本控件布局信息

       fdlStepname = new FormData();

       fdlStepname.left = new FormAttachment(0, 0);

       fdlStepname.right = new FormAttachment(middle, -margin);                 fdlStepname.top = new FormAttachment(0, margin);       wlStepname.setLayoutData(fdlStepname);

       // “步骤名称文本输入框

       wStepname = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);

       wStepname.setText(stepname);

       props.setLook(wStepname);

       wStepname.addModifyListener(lsMod);

       // “步骤名称文本输入框控件布局信息

       fdStepname = new FormData();

       fdStepname.left = new FormAttachment(middle, 0);

       fdStepname.top = new FormAttachment(0, margin);

       fdStepname.right = new FormAttachment(100, 0);

       wStepname.setLayoutData(fdStepname);

 

       Control lastControl = wStepname;

      

       Label wlHelloMessage = new Label(shell, SWT.RIGHT);

     wlHelloMessage.setText(BaseMessages.getString(PKG"ForcastSampleCreator.TimeElapsed.Label"));

       props.setLook(wlHelloMessage);

       FormData leftControlFormData = new FormData();

       leftControlFormData.left = new FormAttachment(0, 0);

       leftControlFormData.right = new FormAttachment(middle, -margin);

       leftControlFormData.top = new FormAttachment(lastControlmargin);

     wlHelloMessage.setLayoutData(leftControlFormData);

       wtHelloMessage = new TextVar(transMetashell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);

       props.setLook(wtHelloMessage);

       FormData rightControlFormData = new FormData();

       rightControlFormData.left = new FormAttachment(middle, 0);

       rightControlFormData.right = new FormAttachment(100, 0);

       rightControlFormData.top = new FormAttachment(lastControlmargin);

     wtHelloMessage.setLayoutData(rightControlFormData);

       wtHelloMessage.addModifyListener(lsMod);

 

       lastControl = wtHelloMessage;

 

       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[] { wOKwCancel }, marginlastControl);

 

       // Add listeners

       lsOK = new Listener() {

           publicvoid handleEvent(Event e) {

               ok();

           }

       };

       lsCancel = new Listener() {

           publicvoid handleEvent(Event e) {

               cancel();

           }

       };

 

       wOK.addListener(SWT.SelectionlsOK);

       wCancel.addListener(SWT.SelectionlsCancel);

 

       lsDef = new SelectionAdapter() {

           publicvoid widgetDefaultSelected(SelectionEvent e) {

               ok();

           }

       };

 

       wStepname.addSelectionListener(lsDef);

 

       // Detect X or ALT-F4 or something that kills thiswindow...

       shell.addShellListener(new ShellAdapter(){

           publicvoid shellClosed(ShellEvent e) {

               cancel();

           }

       });

 

       getData();

       input.setChanged(changed);

       setSize();

       shell.open();

       while (!shell.isDisposed()) {

           if (!display.readAndDispatch()) {

               display.sleep();

           }

       }

       returnstepname;

    }

 

    privatevoid getData() {

     this.wtHelloMessage.setText(input.getHelloMessage());

       wStepname.selectAll();

       wStepname.setFocus();

    }

 

    privatevoid cancel() {

       stepname = null;

       input.setChanged(changed);

       dispose();

    }

 

    privatevoid ok() {

       if (Const.isEmpty(wStepname.getText())) {

           return;

       }

     input.setHelloMessage(this.wtHelloMessage.getText());

 

       stepname = wStepname.getText(); // return value

       dispose();

    }

 

}

 

packageorg.pentaho.di.trans.steps.hellokettle;

 

importorg.pentaho.di.core.exception.KettleException;

import org.pentaho.di.trans.Trans;

import org.pentaho.di.trans.TransMeta;

import org.pentaho.di.trans.step.BaseStep;

importorg.pentaho.di.trans.step.StepDataInterface;

importorg.pentaho.di.trans.step.StepInterface;

import org.pentaho.di.trans.step.StepMeta;

importorg.pentaho.di.trans.step.StepMetaInterface;

 

public class HelloKettleStep extendsBaseStep implements StepInterface {

         privatestatic Class<?> PKG = HelloKettleMeta.class;

         privateHelloKettleMeta meta;

         privateHelloKettleData data;

         publicboolean first = true;

 

         publicHelloKettleStep(StepMeta stepMeta, StepDataInterface stepDataInterface, intcopyNr,

                            TransMetatransMeta, Trans trans) {

                   super(stepMeta,stepDataInterface, copyNr, transMeta, trans);

         }

 

         @Override

         publicboolean processRow(StepMetaInterface smi, StepDataInterface sdi) throwsKettleException {

                    meta = (HelloKettleMeta) smi;

                    data = (HelloKettleData) sdi;

 

                   Object[]rowData = getRow();

                   if(rowData == null) {

                            setOutputDone();

                            returnfalse;

                   }

                   if(first) {

                            first= false;

                            if(rowData != null) {

                                     data.outputRowMeta= getInputRowMeta().clone();

                                     data.inputRowMeta= getInputRowMeta();

                            }

                   }

                   if(rowData != null)

                            putRow(data.outputRowMeta,rowData);

                   returntrue;

         }

 

         @Override

         publicboolean init(StepMetaInterface smi, StepDataInterface sdi) {

                   meta= (HelloKettleMeta) smi;

                   data= (HelloKettleData) sdi;

 

                   if(super.init(smi, sdi)) {

                            returntrue;

                   }else {

                            returnfalse;

                   }

         }

 

         @Override

         publicvoid dispose(StepMetaInterface sii, StepDataInterface sdi) {

         }

}

 

packageorg.pentaho.di.trans.steps.hellokettle;

 

importjava.util.List;

 

importorg.pentaho.di.core.annotations.Step;

importorg.pentaho.di.core.database.DatabaseMeta;

importorg.pentaho.di.core.exception.KettleXMLException;

importorg.pentaho.di.core.xml.XMLHandler;

importorg.pentaho.di.trans.Trans;

importorg.pentaho.di.trans.TransMeta;

importorg.pentaho.di.trans.step.BaseStepMeta;

importorg.pentaho.di.trans.step.StepDataInterface;

importorg.pentaho.di.trans.step.StepInterface;

importorg.pentaho.di.trans.step.StepMeta;

importorg.pentaho.di.trans.step.StepMetaInterface;

importorg.pentaho.metastore.api.IMetaStore;

importorg.w3c.dom.Node;

 

@Step(id = "HelloKettle", image = "images/pa.png", name = "HelloKettle.Step.Name",

description="HelloKettle.Step.Desc",

categoryDescription="KD.Category.Name" ,i18nPackageName="com.kettle.doctor")

publicclass HelloKettleMeta extends BaseStepMeta implements StepMetaInterface {

 

    privatestatic Class<?> PKG = HelloKettleMeta.class;

    /**

     * 问候消息

     */

    private String helloMessage = null;

   

    @Override

    publicvoid setDefault() {

       setHelloMessage("Hello Kettle Plugin World!");

    }

 

    @Override

    public StepInterface getStep(StepMeta stepMeta, StepDataInterface stepDataInterfaceintcopyNr,

           TransMeta transMeta, Trans trans) {

       returnnew HelloKettleStep(stepMetastepDataInterfacecopyNrtransMetatrans);

    }

 

    @Override

    public StepDataInterface getStepData() {

       returnnew HelloKettleData();

    }

 

    public String getXML() {

       StringBuffer retval = new StringBuffer(300);

       retval.append("    ").append(XMLHandler.addTagValue("helloMessage"this.getHelloMessage()));    

       returnretval.toString();

    }

 

    publicvoid loadXML(Node stepnode, List<DatabaseMeta> databases, IMetaStore metaStorethrows KettleXMLException {

       try {

        this.setHelloMessage(XMLHandler.getTagValue(stepnode"helloMessage"));

          

       } catch (Exception e) {

           thrownew KettleXMLException("Read XML Failed."e);

       }

    }

 

    public String getHelloMessage() {

       returnhelloMessage;

    }

 

    publicvoid setHelloMessage(String timeElapsed) {

       this.helloMessage = timeElapsed;

    }

 

}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值