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

在使用mybatis.generator插件自动生成mapper.xml的时候发现一个问题:默认生成的dao接口为mapper结尾

MyBatis Generator的一个问题引发对插件的修改_第1张图片
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的配置示例





    
    

    
    

    

        
        
            
            
        

        
        
        


        
        
            
        


        
        

            
            
            
            
            
            
            
            
        

        
        
            
        

        
        
            
        

        
        

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命令运行

MyBatis Generator的一个问题引发对插件的修改_第2张图片
自定义插件运行结果

插件代码的调试

远程调试步骤:①服务端建立监听②使用相同代码的客户端打断点建立连接并调试

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

启动远程调试

2.可以在当前项目下通过remote连接,module选择当前插件项目
MyBatis Generator的一个问题引发对插件的修改_第3张图片
remote配置

3.然后就可以打断点debug了
MyBatis Generator的一个问题引发对插件的修改_第4张图片
debug

调试MBG并修改源码实现我们想要的效果(接口Dao结尾xml以Mapper结尾)

1.到github下载源码https://github.com/mybatis/generator/releases,这里我下载的是1.3.4的Source code(zip),在IDEA中打开项目,结构如下:

MyBatis Generator的一个问题引发对插件的修改_第5张图片
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")); //$NON-NLS-1$
        }
        return config;
    }
    private Configuration parseMyBatisGeneratorConfiguration(Element rootNode)
        throws XMLParserException {
    MyBatisGeneratorConfigurationParser parser = new MyBatisGeneratorConfigurationParser(
            extraProperties);
            //继续执行解析操作
    return parser.parseConfiguration(rootNode);
    }
}
  • 进到MyBatisGeneratorConfigurationParserparseConfiguration方法,可以发现是一层层的标签解析并封装到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> columns = getColumns(tc);

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

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

        List answer = new ArrayList();

        for (Map.Entry> 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)) { //$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里边的方法

  • 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"); //$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同样的结尾?

  • 接着看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", //$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结尾:

MyBatis Generator的一个问题引发对插件的修改_第6张图片
修改为DAOInterfaceType后

我们也可以通过writeGeneratedJavaFile生成文件时获取文件名的方法找到原因:
public String getFileName() {return compilationUnit.getType().getShortNameWithoutTypeArguments() + ".java";}
调用FullyQualifiedJavaTypebaseShortName属性,就是上面通过构造方法传参解析出来的

  • 最后了解了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一次,不然远程调试不会生效

你可能感兴趣的:(MyBatis Generator的一个问题引发对插件的修改)