kettle源码分析 :本次源码分析 基于 kettle v4.0 分析:
背景:
因最近新增一个需求,需要将原来在 windows平台上的 kettle应用 迁移到 linux 上,并且 进行 定时调度,新增前台管理页面,可对任务进行 动态更新,增加,删除等操作外,还需 设置任务的调度时间 !
初期遇到的问题:
1.版本选择:
因 原来windows应用,是选用当时的经典版4.4.x版本,目前,kettle 最新版本已更新到8.x,是否仍然选择4.x版本?
2.数据资源库选择:
既是迁移,迁移在linux平台后,当然是以数据库的方式来保存资源!
但 原应用中任务以".kjb",".ktr"等方式保存在磁盘上,任务数量众多,以数据库的方式尚无法完全兼容!
3...
4.涉及到发送邮件,数据导出所需的各种依赖 等等,包含windows 和 linux 不同平台所依赖的不同的jar(这个有点坑,早起kettle的版本依赖) 。
当然,最终的选择,还是得看老板的选择,基于对业务的请求,并发,实际使用的场景,后续任务的数量以及 任务的种类 等综合因素考量!
比如,执行一个 "生成日结" 的任务,可能需要耗费的时间相对较长达到数小时!
首先对kettle的大致结构,功能模块大致熟悉后,当然需要对其 插件注册原理,资源库加载初始化等流程需要有一个详细的了解(后续很多功能都需要涉及到);
源码版本,因当前 windows 运行的版本为4.x,所以目前的分析版本也为 kettle 4.x(kettle后续的几次更新,改动都较大);
整个源码结构 并不算复杂,相对 jstorm内核,netty 源码 而言,其 抽象结构 还是比较容易看懂的!
插件注册,资源库初始化 整个流程还是比较简单的!
资源库初始化流程:
一:环境初始化;
1.1 此处后面分析,kettle环境初始化,其实就是 初始化 kettle插件模板后,加载指定的各类插件,插件模式!
二:资源库元数据:
本地磁盘资源库KettleFileRepositoryMeta 实现了本地文件解析的 getXML()和 loadXML(,),其与 数据库资源库不同的地方之一!
2.1 无参构造方法
public static String REPOSITORY_TYPE_ID = "KettleFileRepository";
public KettleFileRepositoryMeta() {
super(REPOSITORY_TYPE_ID);
}
REPOSITORY_TYPE_ID:资源库ID,默认为"KettleFileRepository" ;
作用:在 生成JobMeta时,"*.kjb"文件结构 的 解析,XMLHandler ;
2.2 有参构造方法
public KettleFileRepositoryMeta(String id, String name, String description, String baseDirectory) {
super(id, name, description);
this.baseDirectory = baseDirectory;
}
参数解释:
id: 资源库Id;
name: 资源库名称;
description: 资源库描述;
dir: 资源库目录;
2.2.1 id,name,description作用:
在 加载 kjb文件为document,最终转换为InputStream时 !
在 将kjb文件 转换为xml文档时!
(后面分析 jobMeta 时会详细介绍 ! )
2.2.2 dir:
2.3 获取资源库信息,以XML的方式;
public String getXML() {
StringBuffer retval = new StringBuffer(100);
retval.append(" ").append(XMLHandler.openTag("repository"));
retval.append(super.getXML());
retval.append(" ").append(XMLHandler.addTagValue("base_directory", this.baseDirectory));
retval.append(" ").append(XMLHandler.addTagValue("read_only", this.readOnly));
retval.append(" ").append(XMLHandler.addTagValue("hides_hidden_files", this.hidingHiddenFiles));
retval.append(" ").append(XMLHandler.closeTag("repository"));
return retval.toString();
}
基本逻辑 将 所有的解析实现委托给 XMLHandler 去实现(后面分析XMLhandler)!
2.4 加载 本地文件资源库,以文档的方式!
public void loadXML(Node repnode, List databases) throws KettleException {
super.loadXML(repnode, databases);
try {
this.baseDirectory = XMLHandler.getTagValue(repnode, "base_directory");
this.readOnly = "Y".equalsIgnoreCase(XMLHandler.getTagValue(repnode, "read_only"));
this.hidingHiddenFiles = "Y".equalsIgnoreCase(XMLHandler.getTagValue(repnode, "hides_hidden_files"));
} catch (Exception var4) {
throw new KettleException("Unable to load Kettle file repository meta object", var4);
}
}
KettleFileRepositoryMeta的整体结构还是较为简单,
2.5 初始化本地资源库信息;
public void init(RepositoryMeta repositoryMeta) {
this.serviceMap = new HashMap();
this.serviceList = new ArrayList();
this.repositoryMeta = (KettleFileRepositoryMeta)repositoryMeta;
this.securityProvider = new KettleFileRepositorySecurityProvider(repositoryMeta);
this.serviceMap.put(RepositorySecurityProvider.class, this.securityProvider);
this.serviceList.add(RepositorySecurityProvider.class);
this.log = new LogChannel(this);
}
初始化流程:
serviceList :提供安全检查的接口,此处 使用 RepositorySecurityProvider.class,可根据具体的自定义实现 安全检查的接口逻辑!
serviceMap :在对 资源库 操作时,进行的安全检查!
主要做了如下几件事:
1.初始化 repositoryMeta ;
2.定义安全检查接口,可以自定义安全检查实现 !
3.配置日志输出信息 !
2.6 一般方法
public LogChannelInterface getLog() {
return this.log;//获取资源库日志输出信息,可搭配 websocket 显示在前台上!
}
public boolean isConnected() {
return true;//此为数据库链接的方式,此处做一般实现,无实际意义;
}
public RepositorySecurityProvider getSecurityProvider() {
return this.securityProvider;//执行对资源库的操作前,判断资源库的特征,比如是否只读等!
}
2.7 加载 资源库目录 结构
public RepositoryDirectoryInterface loadRepositoryDirectoryTree() throws KettleException {
//初始化时,dir and name is all null;
RepositoryDirectory root = new RepositoryDirectory();
//设置 root 目录为 "/" ;
root.setObjectId(new StringObjectId("/"));
return this.loadRepositoryDirectoryTree(root);
}
public RepositoryDirectoryInterface loadRepositoryDirectoryTree(RepositoryDirectoryInterface dir) throws KettleException {
//获取 本地资源库的 目录名字
//如: E://kettle/data-->E://kettle/data/
String folderName = this.calcDirectoryName(dir);
//VFS加载 资源库目录下的所有文件,包括子目录(不包括子目录下的文件);
FileObject folder = KettleVFS.getFileObject(folderName);
//获取资源库目录下的所有的文件对象
FileObject[] arr$ = folder.getChildren();
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; ++i$) {
FileObject child = arr$[i$];
//判断文件类型 以及 文件属性是否 hidden ,
if (child.getType().equals(FileType.FOLDER) && (!child.isHidden() || !this.repositoryMeta.isHidingHiddenFiles())) {
//获取文件名称 或者 子目录名称,递归使用
// child.getName().getBaseName():获取文件名称或 子目录名
RepositoryDirectory subDir = new RepositoryDirectory(dir, child.getName().getBaseName());
subDir.setObjectId(new StringObjectId(this.calcObjectId((RepositoryDirectoryInterface)subDir)));
dir.addSubdirectory(subDir);
//递归调用,遍历子目录下的文件
this.loadRepositoryDirectoryTree(subDir);
}
}
return dir;
}
加载格式化目录: 指定目录下的 文件 路径:相对路径"subJect/fzaccSub.kjb";
private String calcDirectoryName(RepositoryDirectoryInterface dir) {
StringBuilder directory = new StringBuilder();
String baseDir = this.repositoryMeta.getBaseDirectory();
baseDir = Const.replace(baseDir, "\\", "/");
directory.append(baseDir);
if (!baseDir.endsWith("/")) {
directory.append("/");
}
if (dir != null) {
String path = this.calcRelativeElementDirectory(dir);
if (path.startsWith("/")) {
directory.append(path.substring(1));
} else {
//指定目录下的 文件 路径;
directory.append(path);
}
if (!path.endsWith("/")) {
directory.append("/");
}
}
return directory.toString();
}
2.7.3 获取绝对 资源库 根据指定文件路径下的 格式化后的路径:
public String calcObjectId(RepositoryDirectoryInterface dir) {
StringBuilder id = new StringBuilder();
String path = this.calcRelativeElementDirectory(dir);
id.append(path);
if (!path.endsWith("/")) {
id.append("/");
}
return id.toString();
}
2.7.4 根据指定的路径判断 是否存在资源库中
public boolean exists(String name, RepositoryDirectoryInterface repositoryDirectory, RepositoryObjectType objectType) throws KettleException {
try {
FileObject fileObject = KettleVFS.getFileObject(this.calcFilename(repositoryDirectory, name, objectType.getExtension()));
return fileObject.exists();
} catch (Exception var5) {
throw new KettleException(var5);
}
}
2.8 保存任务文件
public void save(RepositoryElementInterface repositoryElement, String versionComment, ProgressMonitorListener monitor, ObjectId parentId, boolean used) throws KettleException {
try {
if (!(repositoryElement instanceof XMLInterface) && !(repositoryElement instanceof SharedObjectInterface)) {
throw new KettleException("Class [" + repositoryElement.getClass().getName() + "] needs to implement the XML Interface in order to save it to disk");
} else {
if (!Const.isEmpty(versionComment)) {
this.insertLogEntry(versionComment);
}
ObjectId objectId = new StringObjectId(this.calcObjectId(repositoryElement));
FileObject fileObject = this.getFileObject(repositoryElement);
//JobMeta,TransMeta继承XMLInterface的原因
String xml = ((XMLInterface)repositoryElement).getXML();
OutputStream os = KettleVFS.getOutputStream(fileObject, false);
os.write(xml.getBytes("UTF-8"));
os.close();
if (repositoryElement instanceof ChangedFlagInterface) {
//复位状态位
((ChangedFlagInterface)repositoryElement).clearChanged();
}
if (repositoryElement.getObjectId() != null && !repositoryElement.getObjectId().equals(objectId)) {
//删除旧的文件名称
this.delObject(repositoryElement.getObjectId());
}
repositoryElement.setObjectId(objectId);
}
} catch (Exception var10) {
throw new KettleException("Unable to save repository element [" + repositoryElement + "] to XML file : " + this.calcFilename(repositoryElement), var10);
}
}
2.9 根据 指定的 路径 创建资源库的文件路径
public RepositoryDirectoryInterface createRepositoryDirectory(RepositoryDirectoryInterface parentDirectory, String directoryPath) throws KettleException {
String folder = this.calcDirectoryName(parentDirectory);
String newFolder;
if (folder.endsWith("/")) {
newFolder = folder + directoryPath;
} else {
newFolder = folder + "/" + directoryPath;
}
FileObject parent = KettleVFS.getFileObject(newFolder);
try {
parent.createFolder();
} catch (FileSystemException var7) {
throw new KettleException("Unable to create folder " + newFolder, var7);
}
RepositoryDirectory newDir = new RepositoryDirectory(parentDirectory, directoryPath);
parentDirectory.addSubdirectory(newDir);
newDir.setObjectId(new StringObjectId(newDir.toString()));
return newDir;
}
KettleDatabaseRepository源码分析(数据库存储) :
一:初始化
1.初始化
1.1 无参构造
public KettleDatabaseRepository() {
}
1.2 初始化
1.2.1 指定 参数,即 资源库元数据repositoryMeta:
public void init(RepositoryMeta repositoryMeta) {
this.repositoryMeta = (KettleDatabaseRepositoryMeta)repositoryMeta;
//与kettleFilerepository原理类似,封装 对资源库 的操作的安全检查 和 权限
this.serviceList = new ArrayList();
this.serviceMap = new HashMap();
// 注册日志组件,定义kettle 日志 输出级别
this.log = new LogChannel(this);
// 无参 初始化函数
this.init();
}
1.2.2 资源初始化,加载各种组件;
private void init() {
//操作 TransMeta 的委托模式执行者
this.transDelegate = new KettleDatabaseRepositoryTransDelegate(this);
//操作 JobMeta 的委托
this.jobDelegate = new KettleDatabaseRepositoryJobDelegate(this);
//初始化 数据库操作的委托执行
this.databaseDelegate = new KettleDatabaseRepositoryDatabaseDelegate(this);
//分布式 工作节点
this.slaveServerDelegate = new KettleDatabaseRepositorySlaveServerDelegate(this);
//集群模式
this.clusterSchemaDelegate = new KettleDatabaseRepositoryClusterSchemaDelegate(this);
//
this.partitionSchemaDelegate = new KettleDatabaseRepositoryPartitionSchemaDelegate(this);
//资源库目录
this.directoryDelegate = new KettleDatabaseRepositoryDirectoryDelegate(this);
//操作数据库的代理执行
this.connectionDelegate = new KettleDatabaseRepositoryConnectionDelegate(this, this.repositoryMeta.getConnection());
//资源库用户信息,如用于登录等
this.userDelegate = new KettleDatabaseRepositoryUserDelegate(this);
//行级锁
this.conditionDelegate = new KettleDatabaseRepositoryConditionDelegate(this);
this.valueDelegate = new KettleDatabaseRepositoryValueDelegate(this);
this.notePadDelegate = new KettleDatabaseRepositoryNotePadDelegate(this);
this.stepDelegate = new KettleDatabaseRepositoryStepDelegate(this);
this.jobEntryDelegate = new KettleDatabaseRepositoryJobEntryDelegate(this);
this.creationHelper = new KettleDatabaseRepositoryCreationHelper(this);
}
1.2.3 创建资源库元素据
public RepositoryMeta createRepositoryMeta() {
return new KettleDatabaseRepositoryMeta();
}
- 链接 资源 数据库:
2.1
public void connect(String username, String password, boolean upgrade) throws KettleException {
this.connectionDelegate.connect(upgrade, upgrade);
...
}
在链接数据库时,将 链接操作 Delegate 给 connectionDelegate.
upgrade : 链接资源数据库后,是否验证资源库版本编号!
2.2 在 connectionDelegate 中,以 验证版本的方式,链接数据库!
public synchronized void connect(boolean no_lookup, boolean ignoreVersion) throws KettleException {
//1.repository资源库初始化时,connected 默认为false状态;
//2.在 connection 成功之后,才进行 setConnected(true);
if (this.repository.isConnected()) {
throw new KettleException("Repository is already by class " + this.repository.isConnected());
} else {
try {
//主要操作:1.初始化 系统/应用 配置的属性,System.getProperties()
//2. 更新initialized 标识为 :已初始化;
this.database.initializeVariablesFrom((VariableSpace)null);
//利用 database 链接数据库
this.database.connect();
//链接资源库后,是否需要验证 资源库版本
if (!ignoreVersion) {
this.verifyVersion();
}
//是否开启事务
this.setAutoCommit(false);
// 更新 资源数据链接库 状态
this.repository.setConnected(true);
if (!no_lookup) {
try {
//
this.repository.connectionDelegate.setLookupStepAttribute();
this.repository.connectionDelegate.setLookupTransAttribute();
this.repository.connectionDelegate.setLookupJobEntryAttribute();
this.repository.connectionDelegate.setLookupJobAttribute();
} catch (KettleException var4) {
throw new KettleException("Error setting lookup prep.statements", var4);
}
}
} catch (KettleException var5) {
throw new KettleException("Error connecting to the repository!", var5);
}
}
}
链接资源库后,校验资源库版本:
select * from R_VERSION;
资源库初始版本---"4.0"
MAJOR_VERSION=4
MINOR_VERSION = 0
反之,抛出:Repository.UpgradeRequired.Message
2.3 在DataBase 中 链接数据库;
public synchronized void connect(String group, String partitionId) throws KettleDatabaseException {
//初始化资源数据库时, 分组和 分区Id为空参!
if (!Const.isEmpty(group)) {
this.connectionGroup = group;
this.partitionId = partitionId;
DatabaseConnectionMap map = DatabaseConnectionMap.getInstance();
Database lookup = map.getDatabase(group, partitionId, this);
if (lookup == null) {
this.normalConnect(partitionId);
++this.opened;
this.copy = this.opened;
map.storeDatabase(group, partitionId, this);
} else {
this.connection = lookup.getConnection();
lookup.setOpened(lookup.getOpened() + 1);
this.copy = lookup.getOpened();
}
} else {
//正常逻辑 链接 ,不包含分区id逻辑
this.normalConnect(partitionId);
}
}
2.4 JDBC的方式连接数据库
public void normalConnect(String partitionId) throws KettleDatabaseException {
if (this.databaseMeta == null) {
throw new KettleDatabaseException("No valid database connection defined!");
} else {
try {
//是否配置使用连接池初始化
//dbAccessTypeCode = new String[]{"Native", "ODBC", "OCI", "Plugin", "JNDI"};
//配置使用连接池初始化步骤:
if (this.databaseMeta.isUsingConnectionPool() && this.databaseMeta.getAccessType() != 4) {
try {
this.connection = ConnectionPoolUtil.getConnection(this.log, this.databaseMeta, partitionId);
} catch (Exception var3) {
throw new KettleDatabaseException("Error occured while trying to connect to the database", var3);
}
} else {
//jdbc 连接 数据库
this.connectUsingClass(this.databaseMeta.getDriverClass(), partitionId);
if (this.log.isDetailed()) {
this.log.logDetailed("Connected to database.");
}
// databaseInterface 反射时,内部配置并没有在初始化时配置
//在 dataBaseMeta时 并未进行赋值,所以此处为空
//此处 ConnectSQL 主要用于 kettle log 显示,将 query result 显示在 log plug 中;
String sql = this.environmentSubstitute(this.databaseMeta.getConnectSQL());
if (!Const.isEmpty(sql) && !Const.onlySpaces(sql)) {
this.execStatements(sql);
if (this.log.isDetailed()) {
this.log.logDetailed("Executed connect time SQL statements:" + Const.CR + sql);
}
}
}
} catch (Exception var4) {
throw new KettleDatabaseException("Error occured while trying to connect to the database", var4);
}
}
}
连接数据库:
this.connectUsingClass(this.databaseMeta.getDriverClass(), partitionId);
/**
classname:pluginRaw.dirveClass
partitionId:null
dbAccessTypeCode = new String[]{"Native"-0, "ODBC"-1, "OCI"-2, "Plugin"-3, "JNDI"-4};
**/
private void connectUsingClass(String classname, String partitionId) throws KettleDatabaseException {
//JNDI数据源时
if (this.databaseMeta.getAccessType() == 4) {
this.initWithNamedDataSource(this.environmentSubstitute(this.databaseMeta.getDatabaseName()));
} else {
try {
Class var3 = DriverManager.class;
//并发加载时,此处会产生死锁等问题,jdk1.8版本并发加载时应该不会了
synchronized(DriverManager.class) {
Class.forName(classname);
}
} catch (NoClassDefFoundError var10) {
throw new KettleDatabaseException("Exception while loading class", var10);
} catch (ClassNotFoundException var11) {
throw new KettleDatabaseException("Exception while loading class", var11);
} catch (Exception var12) {
throw new KettleDatabaseException("Exception while loading class", var12);
}
try {
String url;
//默认是没有配置集群,分区
if (this.databaseMeta.isPartitioned() && !Const.isEmpty(partitionId)) {
url = this.environmentSubstitute(this.databaseMeta.getURL(partitionId));
} else {
//拼接的 connection url
url = this.environmentSubstitute(this.databaseMeta.getURL());
}
String clusterUsername = null;
String clusterPassword = null;
if (this.databaseMeta.isPartitioned() && !Const.isEmpty(partitionId)) {
PartitionDatabaseMeta partition = this.databaseMeta.getPartitionMeta(partitionId);
if (partition != null) {
clusterUsername = partition.getUsername();
clusterPassword = Encr.decryptPasswordOptionallyEncrypted(partition.getPassword());
}
}
String password;
String username;
if (!Const.isEmpty(clusterUsername)) {
username = clusterUsername;
password = clusterPassword;
} else {
//在初始化数据库插件 databaseInterface 时,在 DataBaseMeta 中
username = this.environmentSubstitute(this.databaseMeta.getUsername());
password = Encr.decryptPasswordOptionallyEncrypted(this.environmentSubstitute(this.databaseMeta.getPassword()));
}
//jdbc连接时的逻辑校验,校验当前 interface服务 是否支持 url 连接
//默认所有的 数据库插件 都支持此配置
if (this.databaseMeta.supportsOptionsInURL()) {
if (Const.isEmpty(username) && Const.isEmpty(password)) {
this.connection = DriverManager.getConnection(url);
} else if (this.databaseMeta.getDatabaseInterface() instanceof MSSQLServerNativeDatabaseMeta) {
String instance = this.environmentSubstitute(this.databaseMeta.getSQLServerInstance());
if (Const.isEmpty(instance)) {
this.connection = DriverManager.getConnection(url + ";user=" + username + ";password=" + password);
} else {
this.connection = DriverManager.getConnection(url + ";user=" + username + ";password=" + password + ";instanceName=" + instance);
}
} else {
this.connection = DriverManager.getConnection(url, Const.NVL(username, " "), Const.NVL(password, ""));
}
} else {
Properties properties = this.databaseMeta.getConnectionProperties();
if (!Const.isEmpty(username)) {
properties.put("user", username);
}
if (!Const.isEmpty(password)) {
properties.put("password", password);
}
//以 key-value 的形式 连接
this.connection = DriverManager.getConnection(url, properties);
}
} catch (SQLException var13) {
throw new KettleDatabaseException("Error connecting to database: (using class " + classname + ")", var13);
} catch (Throwable var14) {
throw new KettleDatabaseException("Error connecting to database: (using class " + classname + ")", var14);
}
}
}
问题分析:
1.校验 是否分区,是否配置集群时,在 DataBaseMeta初始化时,并没有对 Partitioned 以及Clustered初始化
public boolean isPartitioned() {
String isClustered = this.attributes.getProperty("IS_CLUSTERED");
return "Y".equalsIgnoreCase(isClustered);
}
初始化流程:
初始化 DataBaseMeta 时,会首先初始化数据库插件,即 通过反射得到 BaseDatabaseMeta 的实例!
获取数据库插件实例后,将 root,password等参数,以 DataBaseMeta 的 set方式,将参数 初始化到 插件中!
2.在 获取 数据库连接驱动时,如何获取到的?
通过 加载数据库插件,以及 指定的"MYSQL"数据库类型,通过反射得到databaseInterface时,直接获取到的!
并没有预先赋值!
3.如何根据 传入的数据库类型 来 获得 对应的数据库的模型驱动?
kettle利用 插件的模式,将 多种类型的数据库驱动 封装为 可插拔式的接口调用!
数据库插件结构图:DatabasePluginType;
3.1 初始化流程:
DatabaseMeta dataMeta = new DatabaseMeta("kl_kettle", "MYSQL", "Native","127.0.0.1", "kettles", "3306","root","dawei");
databaseTypeDesc:"MYSQL";
private static final DatabaseInterface findDatabaseInterface(String databaseTypeDesc) throws KettleDatabaseException {
PluginRegistry registry = PluginRegistry.getInstance();//插件模式
//获取 数据库 驱动(注册驱动式在哪里呢)
PluginInterface plugin = registry.getPlugin(DatabasePluginType.class, databaseTypeDesc);
if (plugin == null) {
plugin = registry.findPluginWithName(DatabasePluginType.class, databaseTypeDesc);
}
if (plugin == null) {
throw new KettleDatabaseException("database type with plugin id [" + databaseTypeDesc + "] couldn't be found!");
} else {
return (DatabaseInterface)getDatabaseInterfacesMap().get(plugin.getIds()[0]);
}
}
3.2 初始化 数据库插件(此后的分析将不按照初始化流程 ):
synchronized修饰,避免 并发造成的 getInstance() 初始化时的 双重检查的问题!
private static List pluginTypes = new ArrayList();
public static synchronized void init() throws KettlePluginException {
PluginRegistry registry = getInstance();
PluginTypeInterface pluginType;
long startScan;
for(Iterator i$ = pluginTypes.iterator(); i$.hasNext(); LogChannel.GENERAL.logDetailed("Registered " + registry.getPlugins(pluginType.getClass()).size() + " plugins of type '" + pluginType.getName() + "' in " + (System.currentTimeMillis() - startScan) + "ms.")) {
pluginType = (PluginTypeInterface)i$.next();
//注册插件 统一管理
registry.registerPluginType(pluginType.getClass());
startScan = System.currentTimeMillis();
//初始化资源,加载插件,数据库插件加载 ".xml" 文件
pluginType.searchPlugins();
//加载插件方式二:在系统配置中 配置插件加载路径
String pluginClasses = EnvUtil.getSystemProperty("KETTLE_PLUGIN_CLASSES");
if (!Const.isEmpty(pluginClasses)) {
String[] classNames = pluginClasses.split(",");
String[] arr$ = classNames;
int len$ = classNames.length;
for(int i$ = 0; i$ < len$; ++i$) {
String className = arr$[i$];
try {
PluginAnnotationType annotationType = (PluginAnnotationType)pluginType.getClass().getAnnotation(PluginAnnotationType.class);
Class extends Annotation> annotationClass = annotationType.value();
Class> clazz = Class.forName(className);
Annotation annotation = clazz.getAnnotation(annotationClass);
if (annotation != null) {
//获取插件实例,并 初始化(利用反射注解生成)
pluginType.handlePluginAnnotation(clazz, annotation, new ArrayList(), true, (URL)null);
}
} catch (Exception var15) {
LogChannel.GENERAL.logError("Error registring plugin class from KETTLE_PLUGIN_CLASSES: " + className, var15);
}
}
}
}
JarFileCache.getInstance().clear();
}
pluginTypes : 类型为PluginTypeInterface;
所有加载的插件,最终都存放在pluginTypes中,由 PluginRegistry 统一 注册,加载 和 管理!
PluginTypeInterface:各类插件的接口,所有新增插件,均需实现此接口!
PluginTypeInterface定义如下:
包含一个插件的最基本的功能定义 !
根据如上信息,可以 扩展自定义实现我们自己的插件功能,只需实现 PluginTypeInterface 接口,
并在初始环境中注册此插件即可!
3.3 初始化资源,加载插件:
代码如下:
pluginType.searchPlugins();
-->DatabasePluginType.registerNatives();
----------------------------------------
加载 kettle-database-types.xml 配置文件
kettle-database-types.xml配置文件 定义了二十多种数据库类型,包括很多常见的数据库 !
protected void registerNatives() throws KettlePluginException {
String xmlFile = "kettle-database-types.xml" ;
try {
InputStream inputStream = this.getClass().getResourceAsStream(xmlFile);
if (inputStream == null) {
inputStream = this.getClass().getResourceAsStream("/" + xmlFile);
}
if (inputStream == null) {
throw new KettlePluginException("Unable to find native kettle database types definition file: " + xmlFile);
} else {
Document document = XMLHandler.loadXMLFile(inputStream, (String)null, true, false);
Node repsNode = XMLHandler.getSubNode(document, "database-types");
List repsNodes = XMLHandler.getNodes(repsNode, "database-type");
Iterator i$ = repsNodes.iterator();
while(i$.hasNext()) {
Node repNode = (Node)i$.next();
//每一个 database-type 作为一个Node遍历,初始化为一个 数据库接口服务;
this.registerPluginFromXmlResource(repNode, "./", this.getClass(), true, (URL)null);
}
}
} catch (KettleXMLException var8) {
throw new KettlePluginException("Unable to read the kettle database types XML config file: " + xmlFile, var8);
}
}
XML:
MySQL
org.pentaho.di.core.database.MySQLDatabaseMeta
主要事件:
1.读取配置数据库驱动的".xml”文件 !
2.读取database-type节点,遍历每一个数据库驱动节点 repNode!
3.生成数据库插件接库服务,注册到Registry统一 加载,初始化!
List list = (List)this.pluginMap.get(pluginType);
if (list == null) {
list = new ArrayList();
this.pluginMap.put(pluginType, list);
}
//魔鬼藏在细节中啊,细节,细节
int index = ((List)list).indexOf(plugin);
if (index < 0) {
((List)list).add(plugin);
} else {
((List)list).set(index, plugin);
}
此段逻辑,为 整个插件 加载初始化 最重要的一步,到这里,仅仅只是将 数据库插件服务 初始化完毕!
生成具体的数据库驱动实例 在 loadClass中完成的!
3.4 初始化数据库驱动实例
代码片段一:
try {
DatabaseInterface databaseInterface = (DatabaseInterface)registry.loadClass(plugin);
databaseInterface.setPluginId(plugin.getIds()[0]);
databaseInterface.setPluginName(plugin.getName());
tmpAllDatabaseInterfaces.put(plugin.getIds()[0], databaseInterface);
}
代码片段二(摘选核心实现代码):
Class extends T> cl = null;
if (plugin.isNativePlugin()) {
cl = Class.forName(className);
return cl.newInstance();
} else {
//此处 体现 kettle的强大之处;
}
综上整个流程为 kettle 加载数据库驱动的完整实现!
其结构实现,不得不说,体现kettle的强大之处,在抽象插件功能模块时:
1.本地代码方式配置插件,也可以 在java 系统属性配置插件进行加载初始化!
2.可以根据自身的功能需求,对 kettle 现有的插件进行扩展重写;
3.可以加载本地已有的插件,也可以将插件封装为jar包的形式网络调用加载 !
仅仅只是一个 插件配置,加载的设计,所涉及到的使用场景,其考虑的周全,细腻,不得不佩服,足以体现了作者的抽象设计能力,这就是差距,看来还是差的很远啦!
4.kettle 的配置文件是如何加载的 ?
4.1.框架的配置文件(这块比较简单):
框架的配置文件 是 在 环境初始化时,注册插件时,根据插件类型去 加载指定的配置文件 !
// 注册原生类型和各个所需的插件
PluginRegistry.addPluginType(StepPluginType.getInstance());
PluginRegistry.addPluginType(PartitionerPluginType.getInstance());
PluginRegistry.addPluginType(JobEntryPluginType.getInstance());
PluginRegistry.addPluginType(RepositoryPluginType.getInstance());
代码片段:
protected void registerNatives() throws KettlePluginException {
String xmlFile = "kettle-repositories.xml";
InputStream inputStream = this.getClass().getResourceAsStream(xmlFile);
if (inputStream == null) {
inputStream = this.getClass().getResourceAsStream("/" + xmlFile);
}
Document document = XMLHandler.loadXMLFile(inputStream, (String)null, true, false);
Node repsNode = XMLHandler.getSubNode(document, "repositories");
List repsNodes = XMLHandler.getNodes(repsNode, "repository");
Iterator i$ = repsNodes.iterator();
while(i$.hasNext()) {
Node repNode = (Node)i$.next();
this.registerPluginFromXmlResource(repNode, (String)null, this.getClass(), true, (URL)null);
}
}
后续加载的逻辑,与 数据库插件初始化 流程相同,有兴趣的同学可以自己扒源码看看~ ~。
4.2.自定义配置
自定义的一些配置,比如数据库链接属性等,这个也比较简单!
目前封装的结构来说,所有与数据库有关的配置被映射在 数据库驱动插件类中:如MYSQL:MySQLDatabaseMeta类中!
MySQLDatabaseMeta 类 被封装在 DataBaseMeta中使用!自定义的配置,最终都将在DataBaseMeta中被映射到数据库驱动类中!
5.数据库插件初始化,加载驱动时,为甚么会加载"org.gjt.mm.mysql.Driver"包 ?
5.1 MySQLDatabaseMeta源码:
public String getDriverClass() {
return this.getAccessType() == 1 ? "sun.jdbc.odbc.JdbcOdbcDriver" : "org.gjt.mm.mysql.Driver";
}
dbAccessTypeCode = new String[]{"Native"-0, "ODBC"-1, "OCI"-2, "Plugin"-3, "JNDI"-4};
5.2 org.gjt.mm.mysql.Driver 驱动包为甚么可以加载成功?
引用:
"org.gjt.mm.mysql.Driver 是当时最好的MySQL JDBC,但不是MySQL公司推出的,然后MySQL将 MM公司的 JDBC驱动收为官方的JDBC驱动,所以将驱动的package也改了,
但还保留了org.gjt.mm.mysql.Driver这个路径的引用,也就是你使用新版的JDBC驱动时还可以通过这个来引用,你打开下载的新版JDBC驱动的jar文件可以看到,
只有一个文件的目录是org.gjt.mm.mysql,就是为了兼容而设计的
6.既然kettle底层使用jdbc的方式去链接数据库,那以什么样的方式 保证connection的有效性 和 避免OOM?
6.1 kettle底层,以 jdbc的方式,去访问数据库 !
6.2 如何避免大数据下产生的OOM?(后续)
通常而言,若不采用第三方框架,而使用JDBC操作数据库,OOM总是不可避免的(Mybatis是另一种方式实现的),在生产环境中,它会是最隐形,最频繁的一种bug !
DataBase是如何尽可能的避免这种情况的呢?
一直在想,为什么没有采用common-dbutils jar呢?
下面的代码可以当作 JDBC的模板来使用(整个逻辑代码,要比dbutils实现的更好一些):
6.3 并发场景下(后续):
DataBase 中封装着 DataBaseMeta 和 connection:
前者用于对 参数的初始化,驱动的加载 以及 与数据库有关的逻辑校验。
后者用于 jdbc的链接 和 与数据库有关的操作!
整个结构还是非常清晰!
总结:
kettle 支持数据源,默认支持JNDI,但是可以通过 插件功能,自定义扩展,重写 kettle 数据源配置!
从最初 kettle 就被作者定义为一个开放的工具,代码结构设计中,处处体现着这一点!比如:插件功能(很强大)!
迁移的几点思考:
1.迁移到Linux平台后,对原有的kettle任务进行调度,调度逻辑如何实现?
当前国内主流的分布式调度框架,比如当当的elastic-job,淘宝的TBSchedule,唯品会的Saturn ,其工作节点定时调度逻辑,多基于Quartz(你懂得)+zk的模式!
SpringBoot:
1.1.利用spring对Quartz的高度支持,其定时调度实现的非常轻量级,利用时间驱动,实现的非常巧妙!另:不得不说spring对jdk线程池的优化来支持其定时调度!
1.2.可能出现的瓶颈在于 执行器上,对于不同的任务,耗时较长 或者 任务过多时,springBoot当前的实现,会造成任务异常中断且后续不再执行等!
1.3.spring完善的生态体系 和 功能强大的框架组合,虽然提高了开发效率,但其冗杂无续的依赖,也是让人头疼的!
Quartz:
...
时间轮:
当前,以netty4.x版本中,Hash***的实现,其理想状态,可将延时控制在1s以内(最多不会超过1s)。
适合于 时间精度要求不是非常高,且 任务量巨大的场景!
阿里内部rocketMq版本,其 定时调度 就以时间轮实现,利用链表分区 以及 链表并行,提高并发效率!
2.初期的结构设想:
调度器:定时调度应用中所有的任务,任务在运行之前,必须将任务的信息注册到调度器中,由调度器 进行统一的调度!
kettle-task:动态的创建,更新 task,
Executor:并发的执行已调度中的任务;
当然实际的场景,也许比这更复杂,需要实时前台展示执行的日志,异常的处理,任务版本的变化监控等等!
最终的功能需求,设计结构 还需后续!
fdsf