虽然说kettle提供了可视化的操作界面,但对于一些共有相同功能的ETL流程我们不希望每次都在spoon上面拖拉组件去创建流程,感觉比较繁琐,所以在基于kettle做二次开发的时候希望可以把相同类型的流程提取成公共的模板,每次只需要把不同的参数传入到插件里面即可实例化成不同的模板作业,kettle的插件式设计很符合我们的需求,它提供的变量设置、获取以及元数据注入组件基本能满足工们所有的功能。
变量与元数据注入组件一般是结合起来使用才能使用作业设计得通用一点(所有参数都可以做到动态化),使用表输入组件,其中sql与记录数据限制可以通过选取一个变量来设置值:
但是对于其它一些属性,如步骤名称、数据库连接等就只能配置死了,这时候我们可以利用元数据注入插件把其余或者全部参数设置进去,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HXBXkMZr-1659423362092)(https://img-blog.csdn.net/20170710101022372?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY3ptYWNk/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
当然了,元数据注入只管把参数注入到其它插件,至于注入的参数值是什么一般还是通过获取变量来从外部获得到。
元数据注入step可以把元数据注入到模板转换中的step中,从而不用在step设置界面中静态地设置元数据,做到运行时动态,所以可以解决重复ETL工作。
在本文基于kettle5.3版本,也只有一小部分step组件默认支持元数据注入而已,可以看一下StepMetaInjectionInterface
接口的实现类:
在图中还有一些是我们在原来的基础上增加实现的。其实kettle中插件式的设计,每个step都有对应的xxxMeta
类与之对应,这个类就保存着step的所有
所有元数据,ETL元数据注入就是通过设置xxxMeta
类中的属性值把元数据注入到运行时的step中,使作业具有不同的行为。
要想一些默认没支持元数据注入的step实现支持该功能其实很方便,不需要大改动原码,下面以REST Client为示例进行扩展。
根据名称大概可以找到REST Client步骤相关源码类为Rest
、RestMeta
、RestData
,在同package下创建RestMetaInjection
类,该类需要实现StepMetaInjectionInterface
接口,可以看到子类需要实现接口的3个方法:
//该接口是获取所有需要注入的元数据实体,例如'步骤名称'就是一个需要注入的元数据实体
public List getStepInjectionMetadataEntries() throws KettleException;
//该接口是实现注入的具体实现,主要是初始化一些元数据的值
public void injectStepMetadataEntries( List metadata ) throws KettleException;
//以标准的方式提取出step的元数据,这个接口的逻辑我们可以不实现
public List extractStepMetadataEntries() throws KettleException;
既然最终都是为了操作元数据,那么我样需要拿到RestMeta
对象,因此添加一个构造函数来接收:
public RestMetaInjection(RestMeta meta) {
this.meta = meta;
}
Entry枚举实现了StepMetaInjectionEntryInterface
接口,它描述了需要注入的元数据有哪些,方便下面接口进行具体的注入操作,我们在编写这个枚举时需要参照对应的界面代码类RestDialog
,从界面中找到哪些输入元素是我们将要实现注入的,这些根据一些Label的message信息应该都很容易找得到,代码如下:
public enum Entry implements StepMetaInjectionEntryInterface {
PARAMETERS(ValueMetaInterface.TYPE_NONE, BaseMessages.getString(PKG, "RestDialog.Parameters.Label")),
PARAMETER(ValueMetaInterface.TYPE_NONE, "one parameter"), //对应grid中的一行记录
PARAMETER_FIELD(ValueMetaInterface.TYPE_STRING, BaseMessages.getString(PKG, "RestDialog.ColumnInfo.ParameterField")), //对应一行的参数字段column
PARAMETER_NAME(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.ColumnInfo.ParameterName")),
//上面几个描述了参数的tab中的表格属性,最终需要注入的只是PARAMETER_FIELD与PARAMETER_NAME,但其它两个只是需要在元数据注入step中显示
MOTHEDFIELD(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.MethodField.Label")), //注入method
BODY(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.Body.Label")), //注入body
APPLICATIONTYPE(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.ApplicationType.Label")), //注入applicationType
HTTPLOGIN(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.HttpLogin.Label")), //注入Http验证登录
HTTPPASSWORD(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.HttpPassword.Label")),//注入验证密码
PROXYHOST(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.ProxyHost.Label")),//注入代理服务器
PROXYPORT(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.ProxyPort.Label")),//注入代理端口
TRUSTSTOREFILE(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.TrustStoreFile.Label")),//注入Trust store file
TRUSTSTOREPASSWORD(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.TrustStorePassword.Label")), //注入Trust store password
HEADERS(ValueMetaInterface.TYPE_NONE,BaseMessages.getString(PKG, "RestDialog.Headers.Label")),
HEADER(ValueMetaInterface.TYPE_NONE,BaseMessages.getString(PKG, "one header")),
HEADER_FIELD(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.ColumnInfo.Field")),
HEADER_NAME(ValueMetaInterface.TYPE_STRING,BaseMessages.getString(PKG, "RestDialog.ColumnInfo.Name")),
;
//同参数的注入,对于一个表格的注入参考这样的方式即可
private int valueType;
private String description;
private Entry(int valueType, String description) {
this.valueType = valueType;
this.description = description;
}
@Override
public int getValueType() {
return this.valueType;
}
@Override
public String getDescription() {
return this.description;
}
public static Entry findEntry(String key) {
return Entry.valueOf(key);
}
}
getStepInjectionMetadataEntries
是获取所有step中需要注入的StepInjectionMetaEntry
,然后显示在ETL元数据注入step的注入元数据树表格中,该方法的逻辑就是根据上面Entry枚举进行组装List,参考以下的代码:
@Override
public List getStepInjectionMetadataEntries() throws KettleException {
List all = new ArrayList();
Entry[] topEntries = new Entry[] { Entry.MOTHEDFIELD, Entry.BODY, Entry.APPLICATIONTYPE, Entry.HTTPLOGIN,
Entry.HTTPPASSWORD, Entry.PROXYHOST, Entry.PROXYPORT ,Entry.TRUSTSTOREFILE,Entry.TRUSTSTOREPASSWORD};
StepInjectionUtil.addTopEntry(topEntries, all); //设置一些顶级的注入实体
StepInjectionMetaEntry parameters = new StepInjectionMetaEntry(Entry.PARAMETERS.name(),
Entry.PARAMETERS.getValueType(), Entry.PARAMETERS.getDescription());
StepInjectionMetaEntry parameter = new StepInjectionMetaEntry(Entry.PARAMETER.name(),
Entry.PARAMETER.getValueType(), Entry.PARAMETER.getDescription());
Entry[] subEntries1 = new Entry[] { Entry.PARAMETER_FIELD, Entry.PARAMETER_NAME };
StepInjectionUtil.addSubEntry(subEntries1, parameter);
parameters.getDetails().add(parameter);
all.add(parameters);
//对于表格我们分两个层次显示
StepInjectionMetaEntry headers=new StepInjectionMetaEntry(Entry.HEADERS.name(),Entry.HEADERS.getValueType(),Entry.HEADERS.getDescription());
StepInjectionMetaEntry header=new StepInjectionMetaEntry(Entry.HEADER.name(),Entry.HEADER.getValueType(),Entry.HEADER.getDescription());
Entry[] subEntries2 = new Entry[] { Entry.HEADER_FIELD, Entry.HEADER_NAME };
StepInjectionUtil.addSubEntry(subEntries2, header);
headers.getDetails().add(header);
all.add(headers);
//对于表格我们分两个层次显示
return all;
}
injectStepMetadataEntries
是对元数据注入的具体实现,在这个方法才是真正地改变元数据,这个方法接收一个List
参数,对应着上一个方法的返回参数,但是该方法的合集参数中的第一个元素(元数据注入实体)已经包含了需要注入的具体值,而我们需要的是把这个值设置到RestMeta
对象的对应属性中去,因此其实就是上一个方法的逆向解析过程:
@Override
public void injectStepMetadataEntries(List metadata) throws KettleException {
for (StepInjectionMetaEntry lookFields : metadata) { //循环所有注入实体
Entry lookEntry = Entry.findEntry(lookFields.getKey());
if (lookEntry != null) {
switch (lookEntry) {
case PARAMETERS: //如果是注入参数,由于参数是一个表格,一般对应RestMeta里的集合类型属性,注入逻辑比较多,所以单独实现到一个方法
injectParameterFields(lookFields);
break;
case HEADERS: //与注入参数一样
injectHeaderFields(lookFields);
break;
case MOTHEDFIELD: //注入method,一些单一的参数,比较简单在RestMeta就是一个基本类型属性,下同
meta.setMethod(String.valueOf(lookFields.getValue()));
break;
case BODY: //注入body
meta.setBodyField(String.valueOf(lookFields.getValue()));
break;
case APPLICATIONTYPE:
meta.setApplicationType(String.valueOf(lookFields.getValue()));
break;
case HTTPLOGIN:
meta.setHttpLogin(String.valueOf(lookFields.getValue()));
break;
case HTTPPASSWORD:
meta.setHttpPassword(String.valueOf(lookFields.getValue()));
break;
case PROXYHOST:
meta.setProxyHost(String.valueOf(lookFields.getValue()));
break;
case PROXYPORT:
meta.setProxyPort(String.valueOf(lookFields.getValue()));
break;
case TRUSTSTOREFILE:
meta.setTrustStoreFile(String.valueOf(lookFields.getValue()));
break;
case TRUSTSTOREPASSWORD:
meta.setTrustStorePassword(String.valueOf(lookFields.getValue()));
break;
default:
break;
}
}
}
}
injectParameterFields
实现代码如下:
/**
* 注入http参数
*
* @param lookFields
*/
private void injectParameterFields(StepInjectionMetaEntry lookFields) {
List paramFields = new ArrayList<>();
List paramNames = new ArrayList<>();
for (StepInjectionMetaEntry lookField : lookFields.getDetails()) {// 一行(参数字段、参数名称)
Entry lookEntry = Entry.findEntry(lookField.getKey());
if (lookEntry == Entry.PARAMETER) {
for (StepInjectionMetaEntry lookDetail : lookField.getDetails()) { // 一列:参数字段或参数名称
Entry detailEntry = Entry.findEntry(lookDetail.getKey());
if (detailEntry != null) {
String strValue = (String) lookDetail.getValue();
switch (detailEntry) {
case PARAMETER_FIELD:
paramFields.add(strValue);
break;
case PARAMETER_NAME:
paramNames.add(strValue);
break;
default:
break;
}
}
}
}
}
if (paramFields.size() > 0 && paramNames.size() > 0 && paramFields.size() == paramNames.size()) {
String[] paramFieldArr = new String[paramFields.size()];
String[] paramNameArr = new String[paramNames.size()];
meta.setParameterField(paramFields.toArray(paramFieldArr));
meta.setParameterName(paramNames.toArray(paramNameArr));
}
}
injectHeaderFields
实现代码如下:
/**
* 注入http请求头
* @param lookFields
*/
private void injectHeaderFields(StepInjectionMetaEntry lookFields) {
List headFields = new ArrayList<>();
List headNames = new ArrayList<>();
for (StepInjectionMetaEntry lookField : lookFields.getDetails()) {// 一行(参数字段、参数名称)
Entry lookEntry = Entry.findEntry(lookField.getKey());
if (lookEntry == Entry.HEADER) {
for (StepInjectionMetaEntry lookDetail : lookField.getDetails()) { // 一列:参数字段或参数名称
Entry detailEntry = Entry.findEntry(lookDetail.getKey());
if (detailEntry != null) {
String strValue = (String) lookDetail.getValue();
switch (detailEntry) {
case HEADER_FIELD:
headFields.add(strValue);
break;
case HEADER_NAME:
headNames.add(strValue);
break;
default:
break;
}
}
}
}
}
if (headFields.size() > 0 && headNames.size() > 0 && headFields.size() == headNames.size()) {
String[] headFieldArr = new String[headFields.size()];
String[] headNameArr = new String[headNames.size()];
meta.setHeaderField(headFields.toArray(headFieldArr));
meta.setHeaderName(headNames.toArray(headNameArr));
}
}
至此,RestMetaInjection
类已经编写完毕。
我们虽然实现了RestMetaInjection
类,但还没与step关联起来,还虽然对RestMeta
类稍作修改,重新实现一下getStepMetaInjectionInterface
接口,代码如下:
@Override
public StepMetaInjectionInterface getStepMetaInjectionInterface() {
return new RestMetaInjection(this);
}
Rest Client组件的元数据注入已经实现,重新把源码编译,或者单独编译engin模块并更新对应的jar包,然后来验证一下扩展是否成功,随便创建两个转换,一个包含Rest Client插件,另一个包含ETL元数据注入步骤,最直观的方式就是在spoon界面中看下是否获取到Rest Client的注入属性:
经过测试验证,Rest Client通过元数据注入的方式可以正常运行,可以使用同样的方式来扩展我们一些常用的step。