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

2018-09-26  本文已影响0人  1659b264cc28

在使用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方法的解决方案。
接下来说下我的处理过程,主要涉及下面几点:

先说下MyBatis Generator插件的使用

1.pom.xml添加依赖

<!-- 使用MyBatis Generator插件自动生成代码 -->
<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.3.4</version>
    <configuration>
        <!--配置文件的路径-->
        <configurationFile>${basedir}/src/main/resources/generator/mybatis/generatorConfig.xml</configurationFile>
        <verbose>true</verbose>
        <overwrite>true</overwrite>
    </configuration>
</plugin>

2.generatorConfig.xml的配置示例

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!--导入属性配置-->
    <properties resource="generator/mybatis/db.properties"/>

    <!--指定特定数据库的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>
</generatorConfiguration>

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

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

创建一个简单的maven插件

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

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

  <groupId>com.test</groupId>
  <artifactId>maven-plugin-demo</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>maven-plugin-demo</name>
  <!-- 以maven插件方式打包 -->
  <packaging>maven-plugin</packaging>

  <dependencies>
    <!--插件开发API-->
    <dependency>
      <groupId>org.apache.maven</groupId>
      <artifactId>maven-plugin-api</artifactId>
      <version>3.5.4</version>
    </dependency>
    <!--注解定义-->
    <dependency>
      <groupId>org.apache.maven.plugin-tools</groupId>
      <artifactId>maven-plugin-annotations</artifactId>
      <version>3.5.2</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

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.使用插件

<build>
    <plugins>
        <!-- 使用自定义插件-->
        <plugin>
            <groupId>com.test</groupId>
            <artifactId>maven-plugin-demo</artifactId>
            <version>1.0-SNAPSHOT</version>
        </plugin>
    </plugins>
</build>

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<Context> contexts对象则对应XML中配置的多个context标签
             * Context类对象中的ArrayList<TableConfiguration> 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());
        }
        //......
    }
}

涉及到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();
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")); //$NON-NLS-1$
        }
        return config;
    }
    private Configuration parseMyBatisGeneratorConfiguration(Element rootNode)
        throws XMLParserException {
    MyBatisGeneratorConfigurationParser parser = new MyBatisGeneratorConfigurationParser(
            extraProperties);
            //继续执行解析操作
    return parser.parseConfiguration(rootNode);
    }
}
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属性的地方

public class MyBatisGenerator {
    public void generate(ProgressCallback callback, Set<String> contextIds,
            Set<String> 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();
    }
}

表示对IntrospectedTable表示不太理解,搜了一篇介绍IntrospectedTable是提供扩展的基础类,配置文件context标签上设置的runtime对应的就是不同的IntrospectedTable的实现,接下来我们观察代码时也会看到这点。

public class DatabaseIntrospector {
    public List<IntrospectedTable> introspectTables(TableConfiguration tc)
            throws SQLException {
        // 获取列信息
        Map<ActualTableName, List<IntrospectedColumn>> 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;
    }
}
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)) { //$NON-NLS-1$
            type = IntrospectedTableIbatis2Java2Impl.class.getName();
        } else if ("Ibatis2Java5".equalsIgnoreCase(type)) { //$NON-NLS-1$
            type = IntrospectedTableIbatis2Java5Impl.class.getName();
        } else if ("Ibatis3".equalsIgnoreCase(type)) { //$NON-NLS-1$
            type = IntrospectedTableMyBatis3Impl.class.getName();
        } else if ("MyBatis3".equalsIgnoreCase(type)) { //$NON-NLS-1$
            type = IntrospectedTableMyBatis3Impl.class.getName();
        } else if ("MyBatis3Simple".equalsIgnoreCase(type)) { //$NON-NLS-1$
            type = IntrospectedTableMyBatis3SimpleImpl.class.getName();
        }
        IntrospectedTable answer = (IntrospectedTable) createInternalObject(type);
        answer.setContext(context);
        return answer;
    }
}

createIntrospectedTableForValidation方法中通过runtime的设置,会使用不同的IntrospectedTable实现,我们之前配置文件中的是targetRuntime="MyBatis3",对应会使用IntrospectedTableMyBatis3Impl这个实现类,接下来的generateFiles流程就是用的IntrospectedTableMyBatis3Impl里边的方法

public void generateFiles(ProgressCallback callback,
        List<GeneratedJavaFile> generatedJavaFiles,
        List<GeneratedXmlFile> generatedXmlFiles, List<String> 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());
}
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"); //$NON-NLS-1$
    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同样的结尾?

public class IntrospectedTableMyBatis3Impl extends IntrospectedTable {
    @Override
    public void calculateGenerators(List<String> warnings, ProgressCallback progressCallback) {
        //生成javaClientGenerator
        AbstractJavaClientGenerator javaClientGenerator = calculateClientGenerators(warnings, progressCallback);
    }
    protected AbstractJavaClientGenerator calculateClientGenerators(List<String> 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<CompilationUnit> getCompilationUnits() {
        progressCallback.startTask(getString("Progress.17", //$NON-NLS-1$
                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";}
调用FullyQualifiedJavaTypebaseShortName属性,就是上面通过构造方法传参解析出来的

总结补充:修改core项目源码后调试时需要重新install一次,不然远程调试不会生效

上一篇下一篇

猜你喜欢

热点阅读