MyBatis Generator的一个问题引发对插件的修改

祁飞扬
2023-12-01

在使用mybatis.generator插件自动生成mapper.xml的时候发现一个问题:默认生成的dao接口为mapper结尾
mapper结尾
当然我们知道在不同的ORM框架中名称表示不同,例如:mybatis中称为Mapper,spring Data JPA中称为Repository,但是习惯用***Dao结尾表示数据访问层接口的应该怎么办?

其实mybatis generator支持修改这个后缀:通过generatorConfig.xml配置文件添加table标签的mapperName属性,但是修改后会存在另一个问题:生成的xml由原本的Mapper结尾变成了Dao结尾,也就是只能跟设置的mapperName属性一致,网上搜索了相关问题,只发现一个通过修改插件源码中的calculateMyBatis3XmlMapperFileName方法的解决方案。
接下来说下我的处理过程,主要涉及下面几点:

generator插件的使用
maven创建自定义插件
插件的调试(远程调试)
generator源码的修改
先说下MyBatis Generator插件的使用
1.pom.xml添加依赖

org.mybatis.generator mybatis-generator-maven-plugin 1.3.4 ${basedir}/src/main/resources/generator/mybatis/generatorConfig.xml true true 2.generatorConfig.xml的配置示例 <?xml version="1.0" encoding="UTF-8"?>
<!--指定特定数据库的jdbc驱动jar包的位置-->
<classPathEntry location="${jdbc.driverLocation}"/>

<context id="default" targetRuntime="MyBatis3">

    <!-- optional,旨在创建class时,对注释进行控制 -->
    <commentGenerator>
        <property name="suppressDate" value="true"/>
        <property name="suppressAllComments" value="true"/>
    </commentGenerator>

    <!--jdbc的数据库连接 -->
    <jdbcConnection
            driverClass="${jdbc.driverClass}"
            connectionURL="${jdbc.connectionURL}"
            userId="${jdbc.userId}"
            password="${jdbc.password}">
    </jdbcConnection>


    <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
    <javaTypeResolver>
        <property name="forceBigDecimals" value="false"/>
    </javaTypeResolver>


    <!-- Model模型生成器,用来生成含有主键key的类,记录类 以及查询Example类
        targetPackage     指定生成的model生成所在的包名
        targetProject     指定在该项目下所在的路径
    -->
    <javaModelGenerator targetPackage="com.test.entity"
                        targetProject="src/main/java">

        <!-- 是否允许子包,即targetPackage.schemaName.tableName -->
        <property name="enableSubPackages" value="false"/>
        <!-- 是否对model添加 构造函数 -->
        <property name="constructorBased" value="true"/>
        <!-- 是否对类CHAR类型的列的数据进行trim操作 -->
        <property name="trimStrings" value="true"/>
        <!-- 建立的Model对象是否 不可改变  即生成的Model对象不会有 setter方法,只有构造方法 -->
        <property name="immutable" value="false"/>
    </javaModelGenerator>

    <!--Mapper映射文件生成所在的目录 为每一个数据库的表生成对应的SqlMap文件 -->
    <sqlMapGenerator targetPackage="com.test.dao.mapper"
                     targetProject="src/main/java">
        <property name="enableSubPackages" value="false"/>
    </sqlMapGenerator>

    <!-- 客户端代码,生成易于使用的针对Model对象和XML配置文件 的代码
            type="ANNOTATEDMAPPER",生成Java Model 和基于注解的Mapper对象
            type="MIXEDMAPPER",生成基于注解的Java Model 和相应的Mapper对象
            type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口
    -->
    <javaClientGenerator targetPackage="com.test.dao"
                         targetProject="src/main/java" type="XMLMAPPER">
        <property name="enableSubPackages" value="true"/>
    </javaClientGenerator>

    <!-- mybatis.generator1.3.4支持修改mapperName,需要源码calculateMyBatis3XmlMapperFileName修改mapper后缀 -->
    <table tableName="user" domainObjectName="User"
           enableCountByExample="false" enableUpdateByExample="false"
           enableDeleteByExample="false" enableSelectByExample="false"
           selectByExampleQueryId="false" mapperName="UserDao">
    </table>

</context>
generatorConfig.xml的配置可以参考《MyBatis从入门到精通》第5章

3.执行mybatis-generator:generate命令即可生成配置的table对应代码

创建一个简单的maven插件
参考maven实战第17章
了解插件的基本实现以及插件的运行入口类对接下来的源码调试修改有所帮助

1.插件本身也是maven项目,区别的地方是打包方式必须为maven-plugin
首先pom.xml需要导入两个依赖:

maven-plugin-api包含插件开发必须的类
maven-plugin-annotations提供注解支持
com.test
maven-plugin-demo
1.0-SNAPSHOT

maven-plugin-demo

maven-plugin

org.apache.maven maven-plugin-api 3.5.4 org.apache.maven.plugin-tools maven-plugin-annotations 3.5.2 provided 2.为插件编写目标:创建一个类继承AbstractMojo并实现execute()方法,Maven称为Mojo(maven old java object与Pojo对应),实际上我们执行插件命令时会执行对应的Mojo中的execute()方法

@Mojo(name = “hi”)
public class Helloworld extends AbstractMojo {
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
System.out.println(“Hello World!”);
}
}
@Mojo(name = “hi”)定义了插件的目标名称,执行插件时通过groupId:artifactId:version:名称,例如:上面我们定义的插件执行命令为com.test:maven-plugin-demo:1.0-SNAPSHOT:hi
其他的注解还有@Parameter用于读取参数配置等

3.使用插件

首先我们把创建的自定义插件项目install到本地仓库
然后其他项目就可以在pom.xml中使用标签引入插件了




com.test
maven-plugin-demo
1.0-SNAPSHOT



IntelliJIdea可以在Maven Projects插件栏看到我们引入的插件而直接运行,也可以通过com.test:maven-plugin-demo:1.0-SNAPSHOT:hi命令运行
自定义插件运行结果
插件代码的调试
远程调试步骤:①服务端建立监听②使用相同代码的客户端打断点建立连接并调试

1.maven提供mvnDebug命令行模式启动,默认8000端口号,mvnDebug groupId:artifactId:version:名称,执行mvnDebug com.test:maven-plugin-demo:1.0-SNAPSHOT:hi
启动远程调试

2.可以在当前项目下通过remote连接,module选择当前插件项目
remote配置

3.然后就可以打断点debug了
debug
调试MBG并修改源码实现我们想要的效果(接口Dao结尾xml以Mapper结尾)
1.到github下载源码https://github.com/mybatis/generator/releases,这里我下载的是1.3.4的Source code(zip),在IDEA中打开项目,结构如下:
MBG项目结构

2.比较重要的是plugin和core两个工程,而且plugin依赖core工程
plugin依赖core

3.在plugin工程中可以找到以Mojo结尾的项目入口类,那么我们就可以在execute()打上断点调试
@Mojo(name = “generate”,defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class MyBatisGeneratorMojo extends AbstractMojo {
public void execute() throws MojoExecutionException {
{
//…
try {
ConfigurationParser cp = new ConfigurationParser(
project.getProperties(), warnings);
/**
* 解析后返回Configuration对象,对应XML中的generatorConfiguration根标签
* Configuration对象中的List contexts对象则对应XML中配置的多个context标签
* Context类对象中的ArrayList tableConfigurations则对应XML配置的多个table标签
* 根据它们之间的包含关系,可以看到TableConfiguration类中就有mapperName属性
*/
Configuration config = cp.parseConfiguration(configurationFile);
// ShellCallback作用于IDE执行环境的支持:主要是文件创建,已存在文件时是否支持覆盖,java文件支持合并,以及文件创建完成提醒IDE刷新project
ShellCallback callback = new MavenShellCallback(this, overwrite);

        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
                callback, warnings);
        /**
         * 执行generate生成mapper
         * MavenProgressCallback:log日志打印执行过程,verbose:默认false不打印
         * contextsToRun:参数配置,限制哪些context应该被执行
         * fullyqualifiedTables:参数配置,限制哪些table应该被生成
         */
        myBatisGenerator.generate(new MavenProgressCallback(getLog(),
                verbose), contextsToRun, fullyqualifiedTables);

    } catch (XMLParserException e) {
        for (String error : e.getErrors()) {
            getLog().error(error);
        }

        throw new MojoExecutionException(e.getMessage());
    }
    //......
}

}
Configuration config = cp.parseConfiguration(configurationFile);首先是generatorConfig.xml配置文件的加载和解析,点进方法里边可以看到是使用Document读取XML文档的方式,也就是需要解析到我们的配置文件
涉及到XML配置的首先想到都是要先读取解析XML,我们在《Spring源码深度解析》、《MyBatis技术内幕》都可以看到先从XML配置文件的解析开始

使用Document读取XML文档的简单流程:

//使用DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
//得到Document对象, builder.parse可以接收InputStream/file或者url
Document doc = builder.parse(file);
Element root = doc.getDocumentElement();//获取root根节点对象
NodeList nodelist = root.getChildNodes();
接下来我们进入parseConfiguration()方法,可以发现他把解析工作交给了MyBatisGeneratorConfigurationParser类去解析:
public class ConfigurationParser {
private Configuration parseConfiguration(InputSource inputSource) throws IOException, XMLParserException {
//…略
Configuration config;
Element rootNode = document.getDocumentElement();
DocumentType docType = document.getDoctype();
if (rootNode.getNodeType() == Node.ELEMENT_NODE
&& docType.getPublicId().equals(
XmlConstants.IBATOR_CONFIG_PUBLIC_ID)) {
config = parseIbatorConfiguration(rootNode);
} else if (rootNode.getNodeType() == Node.ELEMENT_NODE
&& docType.getPublicId().equals(
XmlConstants.MYBATIS_GENERATOR_CONFIG_PUBLIC_ID)) {
//DTD文档PUBLIC:根据generatorconfig.xml的文档头部定义的PUBLIC区分使用MyBatis文档方式解析
config = parseMyBatisGeneratorConfiguration(rootNode);
} else {
throw new XMLParserException(getString(“RuntimeError.5”)); // N O N − N L S − 1 NON-NLS-1 NONNLS1
}
return config;
}
private Configuration parseMyBatisGeneratorConfiguration(Element rootNode)
throws XMLParserException {
MyBatisGeneratorConfigurationParser parser = new MyBatisGeneratorConfigurationParser(
extraProperties);
//继续执行解析操作
return parser.parseConfiguration(rootNode);
}
}
进到MyBatisGeneratorConfigurationParser的parseConfiguration方法,可以发现是一层层的标签解析并封装到Configuration类对应的属性中,我们可以通过以下顺序找到mapperName:
parseConfiguration()->parseContext()->parseTable()
protected void parseTable(Context context, Node node) {
TableConfiguration tc = new TableConfiguration(context);
context.addTableConfiguration(tc);
//获取mapperName属性并设置到TableConfiguration对象中
String mapperName = attributes.getProperty(“mapperName”);
if (stringHasValue(mapperName)) {
tc.setMapperName(mapperName);
}
}
通过Configuration config = cp.parseConfiguration(configurationFile);我们了解到XML配置文件会解析封装为Configuration对象,而且也找到了解析读取mapperName属性的地方

接下来可以看执行生成的主流程myBatisGenerator.generate(new MavenProgressCallback(getLog(), verbose), contextsToRun, fullyqualifiedTables);
public class MyBatisGenerator {
public void generate(ProgressCallback callback, Set contextIds,
Set fullyQualifiedTableNames, boolean writeFiles) throws SQLException,
IOException, InterruptedException {
//…
// now run the introspections…
for (Context context : contextsToRun) {
//连接数据库并读取保存table信息,等待后面的generateFiles生成文件
context.introspectTables(callback, warnings,
fullyQualifiedTableNames);
}

    // now run the generates
    for (Context context : contextsToRun) {
        //生成GeneratedJavaFile/GeneratedXmlFile对象,用于后面生成文件
        context.generateFiles(callback, generatedJavaFiles,
                generatedXmlFiles, warnings);
    }
    // now save the files
    if (writeFiles) {
        callback.saveStarted(generatedXmlFiles.size()
            + generatedJavaFiles.size());

        for (GeneratedXmlFile gxf : generatedXmlFiles) {
            projects.add(gxf.getTargetProject());
            writeGeneratedXmlFile(gxf, callback);
        }

        for (GeneratedJavaFile gjf : generatedJavaFiles) {
            projects.add(gjf.getTargetProject());
            //获取java文件内容source = gjf.getFormattedContent()可以看interfaze类中拼接内容的方法
            writeGeneratedJavaFile(gjf, callback);
        }

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

}
其中先通过introspectTables方法获取表的信息,然后再执行generateFiles生成GeneratedJavaFile保存要生成的文件结构,然后再通过writeGeneratedJavaFile获取文件内容以及编码等信息在目录下生成文件。
表示对IntrospectedTable表示不太理解,搜了一篇介绍IntrospectedTable是提供扩展的基础类,配置文件context标签上设置的runtime对应的就是不同的IntrospectedTable的实现,接下来我们观察代码时也会看到这点。

先看context.introspectTables方法,里边主要是获取了数据库连接Connection,以及调用List tables = databaseIntrospector .introspectTables(tc);方法:
public class DatabaseIntrospector {
public List introspectTables(TableConfiguration tc)
throws SQLException {
// 获取列信息
Map<ActualTableName, List> columns = getColumns(tc);

    removeIgnoredColumns(tc, columns);
    calculateExtraColumnInformation(tc, columns);
    applyColumnOverrides(tc, columns);
    calculateIdentityColumns(tc, columns);

    List<IntrospectedTable> introspectedTables = calculateIntrospectedTables(
            tc, columns);
    // ......略
    return introspectedTables;
}
private List<IntrospectedTable> calculateIntrospectedTables(
        TableConfiguration tc,
        Map<ActualTableName, List<IntrospectedColumn>> columns) {
    boolean delimitIdentifiers = tc.isDelimitIdentifiers()
            || stringContainsSpace(tc.getCatalog())
            || stringContainsSpace(tc.getSchema())
            || stringContainsSpace(tc.getTableName());

    List<IntrospectedTable> answer = new ArrayList<IntrospectedTable>();

    for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns
            .entrySet()) {
        ActualTableName atn = entry.getKey();
        //过滤一些没有指定的不必要的信息
        FullyQualifiedTable table = new FullyQualifiedTable(
                //......略
                delimitIdentifiers, context);
        //创建IntrospectedTable并返回
        IntrospectedTable introspectedTable = ObjectFactory
                .createIntrospectedTable(tc, table, context);

        for (IntrospectedColumn introspectedColumn : entry.getValue()) {
            introspectedTable.addColumn(introspectedColumn);
        }
        calculatePrimaryKey(table, introspectedTable);
        enhanceIntrospectedTable(introspectedTable);
        answer.add(introspectedTable);
    }
    return answer;
}

}
可以看到,getColumns(tc)方法通过访问数据库获取到列信息,然后可以发现createIntrospectedTable创建IntrospectedTable的方法:
public class ObjectFactory {
public static IntrospectedTable createIntrospectedTable(
TableConfiguration tableConfiguration, FullyQualifiedTable table,
Context context) {
IntrospectedTable answer = createIntrospectedTableForValidation(context);
answer.setFullyQualifiedTable(table);
answer.setTableConfiguration(tableConfiguration);
return answer;
}
public static IntrospectedTable createIntrospectedTableForValidation(Context context) {
String type = context.getTargetRuntime();
if (!stringHasValue(type)) {
type = IntrospectedTableMyBatis3Impl.class.getName();
} else if (“Ibatis2Java2”.equalsIgnoreCase(type)) { // N O N − N L S − 1 NON-NLS-1 NONNLS1
type = IntrospectedTableIbatis2Java2Impl.class.getName();
} else if (“Ibatis2Java5”.equalsIgnoreCase(type)) { // N O N − N L S − 1 NON-NLS-1 NONNLS1
type = IntrospectedTableIbatis2Java5Impl.class.getName();
} else if (“Ibatis3”.equalsIgnoreCase(type)) { // N O N − N L S − 1 NON-NLS-1 NONNLS1
type = IntrospectedTableMyBatis3Impl.class.getName();
} else if (“MyBatis3”.equalsIgnoreCase(type)) { // N O N − N L S − 1 NON-NLS-1 NONNLS1
type = IntrospectedTableMyBatis3Impl.class.getName();
} else if (“MyBatis3Simple”.equalsIgnoreCase(type)) { // N O N − N L S − 1 NON-NLS-1 NONNLS1
type = IntrospectedTableMyBatis3SimpleImpl.class.getName();
}
IntrospectedTable answer = (IntrospectedTable) createInternalObject(type);
answer.setContext(context);
return answer;
}
}
createIntrospectedTableForValidation方法中通过runtime的设置,会使用不同的IntrospectedTable实现,我们之前配置文件中的是targetRuntime=“MyBatis3”,对应会使用IntrospectedTableMyBatis3Impl这个实现类,接下来的generateFiles流程就是用的IntrospectedTableMyBatis3Impl里边的方法

context.generateFiles生成GeneratedJavaFile/GeneratedXmlFile对象,用于后面生成文件,可以说这里边即将构建生成的就是最终的文件结构,后面的writeFile生成文件也只是读取里边的信息生成文件而已:
public void generateFiles(ProgressCallback callback,
List generatedJavaFiles,
List generatedXmlFiles, List warnings)
throws InterruptedException {
//…略
if (introspectedTables != null) {
for (IntrospectedTable introspectedTable : introspectedTables) {
callback.checkCancel();
//这里的initialize/calculateGenerators/getGeneratedJavaFiles方法都是调用runtime对应实现类里边的方法
introspectedTable.initialize();
introspectedTable.calculateGenerators(warnings, callback);
generatedJavaFiles.addAll(introspectedTable
.getGeneratedJavaFiles());
generatedXmlFiles.addAll(introspectedTable
.getGeneratedXmlFiles());

        generatedJavaFiles.addAll(pluginAggregator
                .contextGenerateAdditionalJavaFiles(introspectedTable));
        generatedXmlFiles.addAll(pluginAggregator
                .contextGenerateAdditionalXmlFiles(introspectedTable));
    }
}
generatedJavaFiles.addAll(pluginAggregator
        .contextGenerateAdditionalJavaFiles());
generatedXmlFiles.addAll(pluginAggregator
        .contextGenerateAdditionalXmlFiles());

}
主要分析里边调用的IntrospectedTableMyBatis3Impl的3个方法(initialize,calculateGenerators,getGeneratedJavaFiles)
首先是初始化initialize:
public void initialize() {
//设置java客户端接口的属性
calculateJavaClientAttributes();
//设置model实体类的属性
calculateModelAttributes();
//设置XML
calculateXmlAttributes();
//…
}
protected void calculateJavaClientAttributes() {
//…
sb.setLength(0);
sb.append(calculateJavaClientInterfacePackage());
sb.append(’.’);
sb.append(fullyQualifiedTable.getDomainObjectName());
sb.append(“DAO”); // N O N − N L S − 1 NON-NLS-1 NONNLS1
setDAOInterfaceType(sb.toString());//DAO接口的名称!

sb.setLength(0);
sb.append(calculateJavaClientInterfacePackage());
sb.append('.');
if (stringHasValue(tableConfiguration.getMapperName())) {//设置了Mapper
    sb.append(tableConfiguration.getMapperName());
} else {
    sb.append(fullyQualifiedTable.getDomainObjectName());
    sb.append("Mapper"); //$NON-NLS-1$
}
setMyBatis3JavaMapperType(sb.toString());

}
这里我们可以发现Mapper的设置,以及产生一个疑问:DAOInterfaceType明明单独设置了接口是DAO为什么生成的时候却变成跟下面的Mapper同样的结尾?

接着看calculateGenerators方法
public class IntrospectedTableMyBatis3Impl extends IntrospectedTable {
@Override
public void calculateGenerators(List warnings, ProgressCallback progressCallback) {
//生成javaClientGenerator
AbstractJavaClientGenerator javaClientGenerator = calculateClientGenerators(warnings, progressCallback);
}
protected AbstractJavaClientGenerator calculateClientGenerators(List warnings, ProgressCallback progressCallback) {
AbstractJavaClientGenerator javaGenerator = createJavaClientGenerator();
return javaGenerator;
}
protected AbstractJavaClientGenerator createJavaClientGenerator() {
if (context.getJavaClientGeneratorConfiguration() == null) {
return null;
}
String type = context.getJavaClientGeneratorConfiguration()
.getConfigurationType();

    AbstractJavaClientGenerator javaGenerator;
    if ("XMLMAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
        javaGenerator = new JavaMapperGenerator();
    } else if ("MIXEDMAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
        javaGenerator = new MixedClientGenerator();
    } else if ("ANNOTATEDMAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
        javaGenerator = new AnnotatedClientGenerator();
    } else if ("MAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
        javaGenerator = new JavaMapperGenerator();
    } else {
        javaGenerator = (AbstractJavaClientGenerator) ObjectFactory
                .createInternalObject(type);
    }
    return javaGenerator;
}

}
javaClientGenerator标签配置客户端代码,我们使用的是XMLMAPPER单独生成XML和接口文件的方式,对应代码这里会使用JavaMapperGenerator这个生成器,在getGeneratedJavaFiles方法中我们主要看到调用javaGenerator.getCompilationUnits();

public class JavaMapperGenerator extends AbstractJavaClientGenerator {
@Override
public List getCompilationUnits() {
progressCallback.startTask(getString(“Progress.17”, // N O N − N L S − 1 NON-NLS-1 NONNLS1
introspectedTable.getFullyQualifiedTable().toString()));
CommentGenerator commentGenerator = context.getCommentGenerator();
//使用的是MyBatis3JavaMapperType,而不是DAOInterfaceType
FullyQualifiedJavaType type = new FullyQualifiedJavaType(
introspectedTable.getMyBatis3JavaMapperType());
//…
return answer;
}
}
这里发现在java客户端代码生成器里边统一使用的都是MyBatis3JavaMapperType,猜测是这里写死了值导致的,代码改成introspectedTable.getDAOInterfaceType()后再install执行插件,果然变成了DAO结尾:
修改为DAOInterfaceType后
我们也可以通过writeGeneratedJavaFile生成文件时获取文件名的方法找到原因:
public String getFileName() {return compilationUnit.getType().getShortNameWithoutTypeArguments() + “.java”;}
调用FullyQualifiedJavaType的baseShortName属性,就是上面通过构造方法传参解析出来的

最后了解了MyBatis Generator的工作流程我们也可以参考有mapperName的地方添加多一个daoName,实现接口文件和xml文件各自的自定义属性:
①最开始解析XML的parseTable添加String daoName = attributes.getProperty(“daoName”);
②introspectedTable.initialize();中的calculateJavaClientAttributes方法参考Mapper修改DAO
③给本地项目中XML文档也添加个daoName属性resources\org\mybatis\generator\config\xml\mybatis-generator-config_1_0.dtd
④install后执行效果:
添加自定义daoName属性
总结补充:修改core项目源码后调试时需要重新install一次,不然远程调试不会生效作者:ta_19260817链接:https://www.jianshu.com/p/465628091a0c来源:简书

 类似资料: