開源MyBatisGenerator元件原始碼分析

2022-07-29 18:01:25

開源MyBatisGenerator元件原始碼分析

看原始碼前,先了解Generator能做什麼?

MyBatisGenerator是用來生成mybatis的Mapper介面和xml檔案的工具,提供多種啟用方式,如Java類啟動、shell啟動、mavenPlugin啟動等

具體點,可以連線DB,讀取表資訊,生成Model物件、JavaMapper、xmlMapper檔案等。

整體程式碼工程分層

    org.mybatis.generator
    ----api  內外部使用的主要介面,關鍵類MyBatisGenerator
    ----codegen  程式碼生成的實際類,如XMLMapperGenerator/BaseRecordGenerator/JavaMapperGenerator
    ------ibatis2  適配ibatis2
    ------mybatis3  適配mybatis3
    ----config  設定處理(1)xml設定讀取/轉化(2)如JavaClientGeneratorConfiguration設定生成檔案目錄、PluginConfiguration設定擴充套件外掛
    ----exception
    ----internal  內部擴充套件和工具類,
    ----logging
    ----plugins  所有的擴充套件外掛,如ToStringPlugin(生成ToString方法)
    ----ant  適配ant編譯工具

寫個demo看看怎麼呼叫

/**
    * 極簡版【Java類啟動】生成
    */
public static void simpleGenModelAndMapper(String tableName, String modelName) {
    Context context = new Context(ModelType.FLAT);
    context.setId("starmoon");
    context.setTargetRuntime("MyBatis3");  // MyBatis3Simple 是不帶Example類的生成模式

    JDBCConnectionConfiguration connection = new JDBCConnectionConfiguration();
    connection.setConnectionURL(JDBC_URL);
    connection.setUserId(JDBC_USERNAME);
    connection.setPassword(JDBC_PASSWORD);
    connection.setDriverClass(JDBC_DIVER_CLASS_NAME);
    context.setJdbcConnectionConfiguration(connection);

    JavaModelGeneratorConfiguration c1 = new JavaModelGeneratorConfiguration();
    c1.setTargetProject(PROJECT_PATH + JAVA_PATH);
    c1.setTargetPackage(MODEL_PACKAGE);
    context.setJavaModelGeneratorConfiguration(c1);

    SqlMapGeneratorConfiguration s1 = new SqlMapGeneratorConfiguration();
    s1.setTargetProject(PROJECT_PATH + RESOURCES_PATH);
    s1.setTargetPackage("mapper");
    context.setSqlMapGeneratorConfiguration(s1);

    JavaClientGeneratorConfiguration j1 = new JavaClientGeneratorConfiguration();
    j1.setTargetProject(PROJECT_PATH + JAVA_PATH);
    j1.setTargetPackage(MAPPER_PACKAGE);
    j1.setConfigurationType("XMLMAPPER"); // XMLMAPPER
    context.setJavaClientGeneratorConfiguration(j1);

    PluginConfiguration toStringPluginConf = new PluginConfiguration();
    toStringPluginConf.setConfigurationType("org.mybatis.generator.plugins.ToStringPlugin");
    toStringPluginConf.addProperty("useToStringFromRoot", "true");
    context.addPluginConfiguration(toStringPluginConf);

    TableConfiguration tableConfiguration = new TableConfiguration(context);
    tableConfiguration.setTableName(tableName);
    context.addTableConfiguration(tableConfiguration);

    try {
        Configuration config = new Configuration();
        config.addContext(context);
        config.validate();
        List<String> warnings = new ArrayList<String>();
        MyBatisGenerator generator = new MyBatisGenerator(config, new DefaultShellCallback(true), warnings);
        // 開始生成
        generator.generate(null);
        if (generator.getGeneratedJavaFiles().isEmpty() || generator.getGeneratedXmlFiles().isEmpty()) {
            throw new RuntimeException("生成Model和Mapper失敗:" + warnings);
        }
    } catch (Exception e) {
        throw new RuntimeException("生成Model和Mapper失敗", e);
    }
}

從入口MyBatisGenerator.generate()看看做了什麼?

MyBatisGenerator呼叫過程

// 精簡了不重要的程式碼
public void generate(ProgressCallback callback, Set<String> contextIds,
        Set<String> fullyQualifiedTableNames, boolean writeFiles) throws SQLException,
        IOException, InterruptedException {

    // 清除快取中,上一次生成內容
    generatedJavaFiles.clear();
    generatedXmlFiles.clear();
    ObjectFactory.reset();
    RootClassInfo.reset();

    // 計算需執行的設定組 (這裡有些過度設計,一般情況單次執行一個Context就足夠)
    // calculate the contexts to run
    List<Context> contextsToRun;
    if (contextIds == null || contextIds.size() == 0) {
        contextsToRun = configuration.getContexts();
    } else {
        contextsToRun = new ArrayList<Context>();
        for (Context context : configuration.getContexts()) {
            if (contextIds.contains(context.getId())) {
                contextsToRun.add(context);
            }
        }
    }

    // 載入指定的Classloader (暫時沒看到使用場景)
    // setup custom classloader if required
    if (configuration.getClassPathEntries().size() > 0) {
        ClassLoader classLoader = getCustomClassloader(configuration.getClassPathEntries());
        ObjectFactory.addExternalClassLoader(classLoader);
    }

    // 內部設定載入(為什麼要這麼做? 實際上可以對每一張表做客製化化生成,針對超大複雜性工程適用)
    // now run the introspections...
    int totalSteps = 0;
    for (Context context : contextsToRun) {
        totalSteps += context.getIntrospectionSteps();
    }
    callback.introspectionStarted(totalSteps); // 預留的勾點 (暫時沒看到使用場景)

    // 【重要1】通過設定,加工表資訊,形成內部表資料
    for (Context context : contextsToRun) {
        context.introspectTables(callback, warnings, fullyQualifiedTableNames);
        // (1)連線db,獲取連結 
        // (2)通過connection的MetaData,拿到所有表資訊
        // (3)針對要生成的表,加工內部表資料
        // (4)釋放連結
    }

    // now run the generates 
    totalSteps = 0;
    for (Context context : contextsToRun) {
        totalSteps += context.getGenerationSteps();
    }
    callback.generationStarted(totalSteps);

    // 開始組長檔案內容資訊(此處還不會寫到檔案中)
    for (Context context : contextsToRun) {
        // 【重要2】Java檔案內容組裝、XML檔案內容組裝、各類plugin呼叫
        context.generateFiles(callback, generatedJavaFiles, generatedXmlFiles, warnings);
    }

    // 建立檔案、內容寫入檔案到磁碟中
    // now save the files  
    if (writeFiles) {
        callback.saveStarted(generatedXmlFiles.size() + generatedJavaFiles.size());

        for (GeneratedXmlFile gxf : generatedXmlFiles) {
            // 【重要3】按指定目錄 寫入xml
            projects.add(gxf.getTargetProject());
            writeGeneratedXmlFile(gxf, callback);
        }

        for (GeneratedJavaFile gjf : generatedJavaFiles) {
            // 【重要4】按指定目錄 寫入Java類  Mapper檔案、DO檔案
            projects.add(gjf.getTargetProject());
            writeGeneratedJavaFile(gjf, callback);
        }

        for (String project : projects) {
            shellCallback.refreshProject(project);
        }
    }

    callback.done();
}

呼叫的元件很分散,先記住幾個關鍵元件

  1. 【API入口】org.mybatis.generator.api.MyBatisGenerator 生成程式碼的主入口API
  2. 【設定與上下文】Configuration 存設定的容器 / Context 存放執行期資料的容器
  3. 【檔案內容生成】XMLMapperGenerator JavaMapperGenerator等
  4. 【擴充套件外掛】org.mybatis.generator.api.Plugin 在程式碼生成過程中,通過不同生命週期介面,個性化處理生成內容,如init、contextGenerateAdditionalJavaFiles、contextGenerateAdditionalXmlFiles

再詳細看看錶資訊,Table資訊如何轉化為Java類元資訊

org.mybatis.generator.config.Context#generateFiles

// 生成檔案內容
public void generateFiles(ProgressCallback callback,
        List<GeneratedJavaFile> generatedJavaFiles, // 存放結構化的Java生成內容
        List<GeneratedXmlFile> generatedXmlFiles,  // 存放結構化的Xml生成內容
        List<String> warnings)
        throws InterruptedException {

    // 載入plugin,裝載到Aggregator集合中,在內容生成的各個生命週期,plugin方法會被呼叫            
    pluginAggregator = new PluginAggregator();
    for (PluginConfiguration pluginConfiguration : pluginConfigurations) {
        Plugin plugin = ObjectFactory.createPlugin(this, pluginConfiguration);
        if (plugin.validate(warnings)) {
            pluginAggregator.addPlugin(plugin);
        } else {
            warnings.add(getString("Warning.24", //$NON-NLS-1$
                    pluginConfiguration.getConfigurationType(), id));
        }
    }

    // 表資訊加工,生成Java物件和xml內容
    if (introspectedTables != null) {
        for (IntrospectedTable introspectedTable : introspectedTables) {
            callback.checkCancel();
            
            // 根據給定的表資訊,初始化(如類名、xml檔名),執行外掛生命週期【initialized】
            // 選定生成規則 如FlatModelRules(控制example、單獨PrimaryKey型別是否生成)
            introspectedTable.initialize();  

            // 預載入需呼叫的Generator  此處的元件更小,例如PrimaryKey生成、ExampleExample處理
            introspectedTable.calculateGenerators(warnings, callback);

            // 開始生成Java檔案內容,將表資訊轉換成檔案內容,後文詳解
            generatedJavaFiles.addAll(introspectedTable.getGeneratedJavaFiles());
            // 開始生成Xml檔案內容
            generatedXmlFiles.addAll(introspectedTable.getGeneratedXmlFiles());

            // 僅有回撥plugin
            generatedJavaFiles.addAll(pluginAggregator.contextGenerateAdditionalJavaFiles(introspectedTable));
            generatedXmlFiles.addAll(pluginAggregator.contextGenerateAdditionalXmlFiles(introspectedTable));
        }
    }

    // 僅有回撥plugin
    generatedJavaFiles.addAll(pluginAggregator.contextGenerateAdditionalJavaFiles());
    generatedXmlFiles.addAll(pluginAggregator.contextGenerateAdditionalXmlFiles());
}

introspectedTable.getGeneratedJavaFiles()解析 (IntrospectedTableMyBatis3Impl)

@Override
public List<GeneratedJavaFile> getGeneratedJavaFiles() {
    List<GeneratedJavaFile> answer = new ArrayList<GeneratedJavaFile>();

    // javaModelGenerators/clientGenerators 在前面calculate過程中,已初始化

    // 常用類 ExampleGenerator BaseRecordGenerator
    for (AbstractJavaGenerator javaGenerator : javaModelGenerators) {
        // 此處生成不同結果單元,很關鍵。不同類,處理不同資料
        List<CompilationUnit> compilationUnits = javaGenerator.getCompilationUnits();
        for (CompilationUnit compilationUnit : compilationUnits) {
            GeneratedJavaFile gjf = new GeneratedJavaFile(compilationUnit,
                    context.getJavaModelGeneratorConfiguration()
                            .getTargetProject(),
                            context.getProperty(PropertyRegistry.CONTEXT_JAVA_FILE_ENCODING),
                            context.getJavaFormatter());
            // 將CompilationUnit裝載到Java檔案資訊中                
            answer.add(gjf);
        }
    }
    // 常用類 JavaMapperGenerator
    for (AbstractJavaGenerator javaGenerator : clientGenerators) {
        List<CompilationUnit> compilationUnits = javaGenerator.getCompilationUnits();
        for (CompilationUnit compilationUnit : compilationUnits) {
            GeneratedJavaFile gjf = new GeneratedJavaFile(compilationUnit,
                    context.getJavaClientGeneratorConfiguration()
                            .getTargetProject(),
                            context.getProperty(PropertyRegistry.CONTEXT_JAVA_FILE_ENCODING),
                            context.getJavaFormatter());
            answer.add(gjf);
        }
    }

    // 一般生成3個GeneratedJavaFile( DO/Example/Mapper )此時的answer內容已經是處理完成的Java資訊
    // 如果isStatic / isFinal /annotations
    return answer;
}

AbstractJavaGenerator.getCompilationUnits做了哪些內容? 下面舉例:

  1. org.mybatis.generator.codegen.mybatis3.javamapper.JavaMapperGenerator
    1. 組裝各類待生成方法的屬性,如CountByExample/InsertSelective
  2. BaseRecordGenerator 組裝各類基本屬性、構造器

從原始碼還能看出mybatis,在不同版本,對資料庫操作層的不同命名,ibatis2中叫[DAO/DAOImpl],對應DAOGenerator,mybatis3中叫[Mapper],對應JavaMapperGenerator

到此為止,仍然沒有生成具體的code內容文字,mybatis3中在後面寫檔案過程時才會組裝,例如org.mybatis.generator.codegen.mybatis3.javamapper.JavaMapperGenerator。文字在後續getFormattedContent中才會組裝。

但ibatis2在此時已經組裝了code內容文字(例如org.mybatis.generator.codegen.ibatis2.model.ExampleGenerator)

很明顯,mybatis3的設計分層更多,隔離性更好,但是複雜度也很高

再看code如何拼接出來

前面從generatedJavaFiles.addAll(introspectedTable.getGeneratedJavaFiles())看進來,會發現一路呼叫到幾個小元件,

    org.mybatis.generator.api.dom.DefaultJavaFormatter#getFormattedContent
        org.mybatis.generator.api.dom.java.TopLevelClass#getFormattedContent  // 拼接import/package資訊
            org.mybatis.generator.api.dom.java.InnerClass#getFormattedContent  // 拼接Javadoc/類修飾關鍵字/具體介面方法/屬性
                // 完成組裝後,附上'}',返回字串

檔案落地到磁碟,沒有特殊操作,標準的檔案留操作

    private void writeFile(File file, String content, String fileEncoding) throws IOException {
        FileOutputStream fos = new FileOutputStream(file, false);
        OutputStreamWriter osw;
        if (fileEncoding == null) {
            osw = new OutputStreamWriter(fos);
        } else {
            osw = new OutputStreamWriter(fos, fileEncoding);
        }
        
        BufferedWriter bw = new BufferedWriter(osw);
        bw.write(content);
        bw.close();
    }

plugin體系

框架中,通過PluginAdapter和Plugin介面定義外掛的各個生命週期,並在code生成過程中進行呼叫,生命週期劃分節點非常多。下面舉例說明。

  1. ToStringPlugin
    1. modelBaseRecordClassGenerated() 在DO生成時被呼叫,用於組裝【toString方法】
  2. SerializablePlugin
    1. modelPrimaryKeyClassGenerated() 在PrimaryKey生成時被呼叫,用於組裝【類實現序列化介面】

可以通過各類plugin,在各個節點做些個性化處理,如統一增加copyright。

常用設定項

        Context context = new Context(ModelType.HIERARCHICAL);  
        // HIERARCHICAL FLAT CONDITIONAL (一般使用CONDITIONAL即可,也是預設設定)
        1. HIERARCHICAL 層次模式,(1)生成獨立的主鍵類 (2)針對text大欄位,生成xxxWithBLOBs包裝類
        2. FLAT  扁平模式,不生成獨立的主鍵類
        3. CONDITIONAL  條件模式,(1)可選生成獨立的主鍵類(單一欄位主鍵不生成獨立類,非單一欄位則生成(聯合主鍵)) (2)有2個以上text大欄位,生成xxxWithBLOBs包裝類


        context.setTargetRuntime("MyBatis3");  // MyBatis3Simple 是不帶Example類的生成模式  MyBatis3 帶有example


        // 隱藏預設註釋
        CommentGeneratorConfiguration commentGeneratorConfiguration = new CommentGeneratorConfiguration();
        context.setCommentGeneratorConfiguration(commentGeneratorConfiguration);
        commentGeneratorConfiguration.addProperty("suppressDate", "true");
        commentGeneratorConfiguration.addProperty("suppressAllComments", "true");
        // Java類 生成toString方法
        PluginConfiguration toStringPluginConf = new PluginConfiguration();
        toStringPluginConf.setConfigurationType("org.mybatis.generator.plugins.ToStringPlugin");
        toStringPluginConf.addProperty("useToStringFromRoot", "true");
        context.addPluginConfiguration(toStringPluginConf);
        // Java類 實現serializable介面
        PluginConfiguration serializablePluginConf = new PluginConfiguration();
        serializablePluginConf.setConfigurationType("org.mybatis.generator.plugins.SerializablePlugin");
        context.addPluginConfiguration(serializablePluginConf);

如何擴充套件

1. 想增加一個查詢方法DeleteListByExampleAndLimit,要怎麼做

原始碼中,主要是4處要擴充套件,預處理計算、Java類生成、xml生成、plugin擴充套件生成

  1. 預處理計算
    1. org.mybatis.generator.api.IntrospectedTable#calculateXmlAttributes
  2. Java類生成
    1. org.mybatis.generator.codegen.mybatis3.javamapper.JavaMapperGenerator#getCompilationUnits方法增加
      1. 實現通用介面DeleteListByExampleAndLimitMethodGenerator.java
  3. xml生成
    1. org.mybatis.generator.codegen.mybatis3.xmlmapper.XMLMapperGenerator#getSqlMapElement
      1. DeleteListByExampleAndLimitElementGenerator.java
  4. plugin擴充套件生成
    1. Plugin.java PluginAggregator增加介面
      1. org.mybatis.generator.internal.PluginAggregator#sqlMapDeleteListByExampleAndLimitElementGenerated

總結

至此,一個標準的Java檔案完成組裝、檔案生成。

回頭看,整個思路其實很簡單,讀取db資訊、加工成內部標準格式資料、通過資料生成DO/Mapper。但複雜的是,去適配不同的設定模式,動態的組裝、拼接。

Generato只能做code生成嗎? 再想想還可以做什麼?拿到db資訊後,進一步生成service介面、controller介面)

表資訊一定要連DB嗎? 從DDL檔案中讀? 從ERM讀? 進而擴充套件到,在源頭上管理表結構和JavaDO的對映)

其他可以借鑑的內容

可以學習其中的Configuration組織模式,適配上PropertyHolder,屬性做到了高內聚。

(思考,CommentGeneratorConfiguration用的suppressDate屬性,為何不直接定義在類中,而是放在PropertyHolder? 可能是使用方的介面已經定義org.mybatis.generator.api.CommentGenerator#addConfigurationProperties,只能從Properties中取屬性。)