3

开源MyBatisGenerator组件源码分析 - starmoon1900

 2 years ago
source link: https://www.cnblogs.com/starmoon1994/p/16532265.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

开源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)

@Overridepublic 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中取属性。)


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK