一、问题背景
对于具备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) in, transMeta, sname);
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(shell, input);
// 文本修改时,通知元数据实例,数据已经发生了变化。
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(lastControl, margin);
wlHelloMessage.setLayoutData(leftControlFormData);
wtHelloMessage = new TextVar(transMeta, shell, 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(lastControl, margin);
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[] { wOK, wCancel }, margin, lastControl);
// Add listeners
lsOK = new Listener() {
publicvoid handleEvent(Event e) {
ok();
}
};
lsCancel = new Listener() {
publicvoid handleEvent(Event e) {
cancel();
}
};
wOK.addListener(SWT.Selection, lsOK);
wCancel.addListener(SWT.Selection, lsCancel);
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 stepDataInterface, intcopyNr,
TransMeta transMeta, Trans trans) {
returnnew HelloKettleStep(stepMeta, stepDataInterface, copyNr, transMeta, trans);
}
@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
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;
}
}