kettle源码分析之资源库初始化流程

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插件模板后,加载指定的各类插件,插件模式!

二:资源库元数据:

image.png

本地磁盘资源库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();
}
  1. 链接 资源 数据库:
    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;


image.png

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 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定义如下:


image.png

包含一个插件的最基本的功能定义 !

根据如上信息,可以 扩展自定义实现我们自己的插件功能,只需实现 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 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类中!


image.png

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,就是为了兼容而设计的
image.png

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:并发的执行已调度中的任务;

当然实际的场景,也许比这更复杂,需要实时前台展示执行的日志,异常的处理,任务版本的变化监控等等!
最终的功能需求,设计结构 还需后续!

image.png
fdsf

你可能感兴趣的:(kettle源码分析之资源库初始化流程)