看原始碼前,先了解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();
}
呼叫的元件很分散,先記住幾個關鍵元件
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做了哪些內容? 下面舉例:
從原始碼還能看出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的設計分層更多,隔離性更好,但是複雜度也很高
前面從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();
}
框架中,通過PluginAdapter和Plugin介面定義外掛的各個生命週期,並在code生成過程中進行呼叫,生命週期劃分節點非常多。下面舉例說明。
可以通過各類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);
原始碼中,主要是4處要擴充套件,預處理計算、Java類生成、xml生成、plugin擴充套件生成
至此,一個標準的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中取屬性。)