四时宝库

程序员的知识宝库

Mybatis Generator这样用不担心自定义开发的Mapper方法被覆盖了


前言

说到ORM工具,Mybatis无疑是当下最流行的一款。搭建一个新的项目首先就要集成Mybatis,通过Mybatis Generator逆向生成基本的增删查改xml文件和Mapper接口文件,代码中可以直接使用其进行数据库操作可以说非常方便。但是基本的增删查改不能满足复杂的业务需求,当我们在xml文件和Mapper接口文件中编写了大量的自定义方法后,有一天需求需要变更字段,这时就需要重新生成xml文件和Mapper接口文件,如果在原项目目录中直接生成原来文件会被覆盖掉,我们编写的大量的自定义方法会因此而丢失。之前的做法是换一个目录生成,将生成的新文件内容拷贝到原来的文件中,这样做是可行的,但是原来的文件已经被自定义方法污染严重,生成的方法和自定义的方法交融在一起,需要很仔细地去寻找一不小心就会删除过多代码,导致程序出错。

本文将以一种全新的方式解决上面的问题,不需要人工拷贝代码替换。具体做法是通过开发Mybatis Generator Plugin的方式对原来的Mybatis Generator功能进行增强,使其完美解决各种现实开发场景遇到问题。

Mybatis Generator Plugin介绍

Mybatis Generator工具除了具有基本的生成代码功能,还提供了插件功能,用户拓展其功能,工具内置了许多拓展插件,如下:

其中CachePlugin插件将为生成的Xml添加缓存支持相关代码,如MybatisGerneratorConfig.xml文件中添加代码如下:

<plugin type="org.mybatis.generator.plugins.CachePlugin" >
    <property name="cache_type" value="org.mybatis.caches.ehcache.LoggingEhcache"/>
</plugin>

添加代码后,MybatisGerneratorConfig.xml文件内容片段如下:

最终生成的xml文件片段如下:

接下来通过开发自己的Plugin来解决数据库变更后,再次使用Mybatis Generator生成代码导致文件覆盖的问题。

开发自己的插件

以生成t_test表为例,pojo类为Test.java,表结构如下:

CREATE TABLE `t_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `msg` varchar(32) DEFAULT NULL COMMENT '内容',
  PRIMARY KEY (`id`)
);


1. 开发思路

pojo类处理

增强代码使其生成的pojo类作为基类BaseTest.java,同时生成Test.java类继承至BaseTest.java,代码中pojo的引用全部为子类,自定义方法全部写在子类中,插件只更新基类不更新子类,当表结构变动时生成的pojo会覆盖BaseTest.java,而Test.java中的自定义方法全部保留。

mapper类处理

增强代码使其生成的Mapper接口作为基类BaseTestMapper.java,同时生成TestMapper.java类继承至BaseTestMapper.java,代码中Mapper的引用全部为子类,自定义方法全部写在子类中,插件只更新基类不更新子类,当表结构变动时生成的Mapper会覆盖BaseTestMapper.java,而TestMapper.java中的自定义方法全部保留。

xml文件处理

通过动态解析xml的方式获取文件中所有节点,通过节点Id属性判断节点是否为工具生成,是则替换,不是则表示节点为自定义节点保留,然后将新生成的节点和自定义节点合并后替换原来文件即可。

2. 编写插件

定义插件

名称为:`BaseClassPlugin`继承至 `org.mybatis.generator.api.PluginAdapter`;为插件提供两个属性:`useBaseEntity`默认值为`true`表示采用基类的方式生成pojo类,`false`表示不使用基类方式生成pojo类;`useBaseMapper`默认值`true`表示采用基类方式生成mapper接口,`false`表示不使用基类形式生成mapper接口。

处理pojo类

重写`org.mybatis.generator.api.PluginAdapter`的`public void initialized(IntrospectedTable introspectedTable)`方法,获取原pojo类全路径重置类全路径为基类全路径,创建子类并继承至基类,代码如下:

if(useBaseEntity){
    String baseRecordType = introspectedTable.getBaseRecordType();
    String[] split = baseRecordType.split("\.");
    StringBuilder sb = new StringBuilder();
    for (int i = 0,len = split.length; i < len; i++) {
        if(i == len - 1){
            sb.append("Base");
        }
        sb.append(split[i]);
        if(i != len - 1){
            sb.append(".");
        }
    }
    baseEntityName = sb.toString();
    introspectedTable.setBaseRecordType(baseEntityName);
    subEntityClass = new TopLevelClass(baseRecordType);
    subEntityClass.setVisibility(JavaVisibility.PUBLIC);
    subEntityClass.setSuperClass(baseEntityName);
}

重写`org.mybatis.generator.api.PluginAdapter`的`public List<GeneratedJavaFile> contextGenerateAdditionalJavaFiles(IntrospectedTable introspectedTable)`方法,将子类添加到带生成Java文件集合中,代码如下:

List<GeneratedJavaFile> awser = new ArrayList<>(2);
String targetProject = introspectedTable.getContext().getJavaModelGeneratorConfiguration().getTargetProject();
File subEntityFile = new File(targetProject + "/" + subEntityClass.getType().getFullyQualifiedName().replace(".", "/") + ".java");
if(useBaseEntity && !subEntityFile.exists()){
    GeneratedJavaFile javaEntityFile = new GeneratedJavaFile(subEntityClass,targetProject,new DefaultJavaFormatter());
    awser.add(javaEntityFile);
}
return awser;

处理mapper类

重写`org.mybatis.generator.api.PluginAdapter`的`public void initialized(IntrospectedTable introspectedTable)`方法,获取原mapper类全路径重置类全路径为基类全路径,创建子类并继承至基类,代码如下:

if(useBaseMapper){
    String myBatis3JavaMapperType = introspectedTable.getMyBatis3JavaMapperType();
    String[] split = myBatis3JavaMapperType.split("\.");
    StringBuilder sb = new StringBuilder();
    for (int i = 0,len = split.length; i < len; i++) {
        if(i == len - 1){
            sb.append("Base");
        }
        sb.append(split[i]);
        if(i != len - 1){
            sb.append(".");
        }
    }
    baseMapperName = sb.toString();
    introspectedTable.setMyBatis3JavaMapperType(baseMapperName);
    subMapperClass = new Interface(myBatis3JavaMapperType);
    subMapperClass.setVisibility(JavaVisibility.PUBLIC);
}

重写`org.mybatis.generator.api.PluginAdapter`的`public List<GeneratedJavaFile> contextGenerateAdditionalJavaFiles(IntrospectedTable introspectedTable)`方法,将子类添加到带生成Java文件集合中,代码如下:

List<GeneratedJavaFile> awser = new ArrayList<>(2);
String targetProject = introspectedTable.getContext().getJavaModelGeneratorConfiguration().getTargetProject();
File subMapperFile = new File(targetProject + "/" + subMapperClass.getType().getFullyQualifiedName().replace(".", "/") + ".java");
if(useBaseMapper && !subMapperFile.exists()){
    GeneratedJavaFile javaMapperFile = new GeneratedJavaFile(subMapperClass,targetProject,new DefaultJavaFormatter());
    awser.add(javaMapperFile);
}
return awser;

重写`org.mybatis.generator.api.PluginAdapter`的`public boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass, IntrospectedTable introspectedTable)`方法,替换mapper接口中方法对pojo的引用为子类,代码如下:

if(useBaseEntity){
    interfaze.addImportedType(subEntityClass.getType());
}
if(useBaseMapper){
    subMapperClass.addSuperInterface(interfaze.getType());
}
if(useBaseEntity){
    for (Method method : interfaze.getMethods()) {
        List<Parameter> paramList = new ArrayList<>();
        List<Parameter> parameters = method.getParameters();
        Iterator<Parameter> iterator = parameters.iterator();
        while (iterator.hasNext()){
            Parameter next = iterator.next();
            if (next.getType().getFullyQualifiedName().equals(baseEntityName)) {
                paramList.add(new Parameter(subEntityClass.getType(),next.getName(),next.getAnnotations().stream().collect(Collectors.joining(" ")),next.isVarargs()));
            }else{
                paramList.add(new Parameter(next.getType(),next.getName(),next.getAnnotations().stream().collect(Collectors.joining(" ")),next.isVarargs()));
            }
            iterator.remove();
        }
        parameters.addAll(paramList);


        FullyQualifiedJavaType returnType = method.getReturnType();
        if(returnType.getFullyQualifiedName().equals(baseEntityName)){
            method.setReturnType(subEntityClass.getType());
        }

        List<FullyQualifiedJavaType> typeArguments = returnType.getTypeArguments();
        List<FullyQualifiedJavaType> typwArgList = new ArrayList<>();
        Iterator<FullyQualifiedJavaType> typeIterator = typeArguments.iterator();
        while (typeIterator.hasNext()){
            FullyQualifiedJavaType next = typeIterator.next();
            if (next.getFullyQualifiedName().equals(baseEntityName)) {
                typwArgList.add(subEntityClass.getType());
            }else{
                typwArgList.add(next);
            }
            typeIterator.remove();
        }
        typeArguments.addAll(typwArgList);
    }

    interfaze.getImportedTypes().removeIf(next -> next.getFullyQualifiedName().equals(baseEntityName));
}
return true;

处理xml文件

工具在原xml文件已经存在时,默认会将文件中属性Id值在指定集合内的节点删除,集合为`org.mybatis.generator.config.MergeConstants`类字段`OLD_XML_ELEMENT_PREFIXES`的值,其他的节点将和新生成的节点合并写入原xml文件。字段值为:

public static final String[] OLD_XML_ELEMENT_PREFIXES = new String[]{"ibatorgenerated_", "abatorgenerated_"};

基于此,重写`org.mybatis.generator.api.PluginAdapter`的`public void initialized(IntrospectedTable introspectedTable)`方法,修改字段值为工具生成的节点Id集合,代码如下:

try {
    Constructor<?>[] constructors = MergeConstants.class.getDeclaredConstructors();
    constructors[0].setAccessible(true);
    Object newInstance = constructors[0].newInstance();
    java.lang.reflect.Field field = MergeConstants.class.getField("OLD_XML_ELEMENT_PREFIXES");
    field.setAccessible(true);
    java.lang.reflect.Field modifiers = field.getClass().getDeclaredField("modifiers");
    modifiers.setAccessible(true);
    modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
    field.set(newInstance,new String[]{"updateByPrimaryKey", "updateByPrimaryKeySelective","updateByExample","updateByExampleSelective","countByExample","insertSelective","insert","deleteByExample","deleteByPrimaryKey","selectByPrimaryKey","Update_By_Example_Where_Clause","Example_Where_Clause","BaseResultMap","Base_Column_List","selectByExample","UpdateByExampleWithBLOBs","SelectByExampleWithBLOBs","ResultMapWithBLOBs","Blob_Column_List"});
    modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
} catch (Exception e) {
    e.printStackTrace();
}

重写`org.mybatis.generator.api.PluginAdapter`的`public boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable)`方法,替换对pojo、mapper的引用为子类,代码如下:

XmlElement rootElement = document.getRootElement();
if(useBaseMapper){
    List<Attribute> attributes = rootElement.getAttributes();
    Iterator<Attribute> iterator = attributes.iterator();
    String attrName = null;
    while (iterator.hasNext()){
        attrName = null;
        Attribute next = iterator.next();
        if(next.getValue().equals(baseMapperName)){
            attrName = next.getName();
            iterator.remove();
        }
    }
    if(attrName!= null){
        attributes.add(new Attribute(attrName,subMapperClass.getType().getFullyQualifiedName()));
    }
}

if(useBaseEntity){
    List<Element> elements = rootElement.getElements();
    for (Element element : elements) {
        XmlElement  xl = (XmlElement) element;
        List<Attribute> attributes = xl.getAttributes();
        Iterator<Attribute> iterator = attributes.iterator();
        String attrName = null;
        while (iterator.hasNext()){
            attrName = null;
            Attribute next = iterator.next();
            if(next.getValue().equals(baseEntityName)){
                attrName = next.getName();
                iterator.remove();
            }
        }
        if(attrName!= null){
            attributes.add(new Attribute(attrName,subEntityClass.getType().getFullyQualifiedName()));
        }
    }
}
return true;

使用自己的插件生成代码

1. 将自己的插件添加到MybatisGerneratorConfig.xml文件中,文件内容片段如下:

2. 运行插件,生成结果如图:

插件为一张表生成5个类文件1个xml文件,其中

Test类代码如下:

public class Test extends BaseTest {

}

BaseTest类代码片段如下:

public class BaseTest {
    private Integer id;

    private String msg;

    private String test1;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getTest1() {
        return test1;
    }

    public void setTest1(String test1) {
        this.test1 = test1;
    }
}

TestMapper类代码如下:

public interface TestMapper extends BaseTestMapper {

}

BaseTestMapper类代码片段如下:

public interface BaseTestMapper {
    long countByExample( TestExample example);

    int deleteByExample( TestExample example);

    int deleteByPrimaryKey( Integer id);

    int insert( Test record);

    int insertSelective( Test record);

    List<Test> selectByExampleWithBLOBs( TestExample example);

    List<Test> selectByExample( TestExample example);

    Test selectByPrimaryKey( Integer id);

    int updateByExampleSelective(@Param("record") Test record, @Param("example") TestExample example);

    int updateByExampleWithBLOBs(@Param("record") Test record, @Param("example") TestExample example);

    int updateByExample(@Param("record") Test record, @Param("example") TestExample example);

    int updateByPrimaryKeySelective( Test record);

    int updateByPrimaryKeyWithBLOBs( Test record);

    int updateByPrimaryKey( Test record);
}

TestMapper.xml文件内容片段如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.farmer.restuarant.mapper.TestMapper">
  <resultMap id="BaseResultMap" type="com.farmer.restuarant.entity.Test">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="msg" jdbcType="VARCHAR" property="msg" />
  </resultMap>

附:插件完整代码:

package com.farmer.generator.plugin;

import org.mybatis.generator.api.GeneratedJavaFile;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.DefaultJavaFormatter;
import org.mybatis.generator.api.dom.java.*;
import org.mybatis.generator.api.dom.xml.Attribute;
import org.mybatis.generator.api.dom.xml.Document;
import org.mybatis.generator.api.dom.xml.Element;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.config.MergeConstants;
import org.mybatis.generator.internal.util.StringUtility;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author tech-farmer
 * @ClassName: BaseClassPlugin
 * @Description: (这里用一句话描述这个类的作用)
 * @date 2021/11/5 13:23
 */
public class BaseClassPlugin extends PluginAdapter {

    private String baseEntityName;
    private String baseMapperName;
    private TopLevelClass subEntityClass;
    private Interface subMapperClass;

    private boolean useBaseEntity;
    private boolean useBaseMapper;

    @Override
    public boolean validate(List<String> list) {
        useBaseEntity = StringUtility.isTrue(this.getProperties().getProperty("useBaseEntity","true"));
        useBaseMapper = StringUtility.isTrue(this.getProperties().getProperty("useBaseMapper","true"));
        return true;
    }

    public BaseClassPlugin() {
    }

    @Override
    public void initialized(IntrospectedTable introspectedTable) {
        super.initialized(introspectedTable);
        if(useBaseEntity){
            String baseRecordType = introspectedTable.getBaseRecordType();
            String[] split = baseRecordType.split("\.");
            StringBuilder sb = new StringBuilder();
            for (int i = 0,len = split.length; i < len; i++) {
                if(i == len - 1){
                    sb.append("Base");
                }
                sb.append(split[i]);
                if(i != len - 1){
                    sb.append(".");
                }
            }
            baseEntityName = sb.toString();
            introspectedTable.setBaseRecordType(baseEntityName);
            subEntityClass = new TopLevelClass(baseRecordType);
            subEntityClass.setVisibility(JavaVisibility.PUBLIC);
            subEntityClass.setSuperClass(baseEntityName);
        }
        if(useBaseMapper){
            String myBatis3JavaMapperType = introspectedTable.getMyBatis3JavaMapperType();
            String[] split = myBatis3JavaMapperType.split("\.");
            StringBuilder sb = new StringBuilder();
            for (int i = 0,len = split.length; i < len; i++) {
                if(i == len - 1){
                    sb.append("Base");
                }
                sb.append(split[i]);
                if(i != len - 1){
                    sb.append(".");
                }
            }
            baseMapperName = sb.toString();
            introspectedTable.setMyBatis3JavaMapperType(baseMapperName);
            subMapperClass = new Interface(myBatis3JavaMapperType);
            subMapperClass.setVisibility(JavaVisibility.PUBLIC);
        }
        try {
            Constructor<?>[] constructors = MergeConstants.class.getDeclaredConstructors();
            constructors[0].setAccessible(true);
            Object newInstance = constructors[0].newInstance();
            java.lang.reflect.Field field = MergeConstants.class.getField("OLD_XML_ELEMENT_PREFIXES");
            field.setAccessible(true);
            java.lang.reflect.Field modifiers = field.getClass().getDeclaredField("modifiers");
            modifiers.setAccessible(true);
            modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            field.set(newInstance,new String[]{"updateByPrimaryKey", "updateByPrimaryKeySelective","updateByExample","updateByExampleSelective","countByExample","insertSelective","insert","deleteByExample","deleteByPrimaryKey","selectByPrimaryKey","Update_By_Example_Where_Clause","Example_Where_Clause","BaseResultMap","Base_Column_List","selectByExample","UpdateByExampleWithBLOBs","SelectByExampleWithBLOBs","ResultMapWithBLOBs","Blob_Column_List"});
            modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable) {
        XmlElement rootElement = document.getRootElement();
        if(useBaseMapper){
            List<Attribute> attributes = rootElement.getAttributes();
            Iterator<Attribute> iterator = attributes.iterator();
            String attrName = null;
            while (iterator.hasNext()){
                attrName = null;
                Attribute next = iterator.next();
                if(next.getValue().equals(baseMapperName)){
                    attrName = next.getName();
                    iterator.remove();
                }
            }
            if(attrName!= null){
                attributes.add(new Attribute(attrName,subMapperClass.getType().getFullyQualifiedName()));
            }
        }

        if(useBaseEntity){
            List<Element> elements = rootElement.getElements();
            for (Element element : elements) {
                XmlElement  xl = (XmlElement) element;
                List<Attribute> attributes = xl.getAttributes();
                Iterator<Attribute> iterator = attributes.iterator();
                String attrName = null;
                while (iterator.hasNext()){
                    attrName = null;
                    Attribute next = iterator.next();
                    if(next.getValue().equals(baseEntityName)){
                        attrName = next.getName();
                        iterator.remove();
                    }
                }
                if(attrName!= null){
                    attributes.add(new Attribute(attrName,subEntityClass.getType().getFullyQualifiedName()));
                }
            }
        }
        return true;
    }

    @Override
    public List<GeneratedJavaFile> contextGenerateAdditionalJavaFiles(IntrospectedTable introspectedTable) {
        List<GeneratedJavaFile> awser = new ArrayList<>(2);
        String targetProject = introspectedTable.getContext().getJavaModelGeneratorConfiguration().getTargetProject();
        File subEntityFile = new File(targetProject + "/" + subEntityClass.getType().getFullyQualifiedName().replace(".", "/") + ".java");
        if(useBaseEntity && !subEntityFile.exists()){
            GeneratedJavaFile javaEntityFile = new GeneratedJavaFile(subEntityClass,targetProject,new DefaultJavaFormatter());
            awser.add(javaEntityFile);
        }
        File subMapperFile = new File(targetProject + "/" + subMapperClass.getType().getFullyQualifiedName().replace(".", "/") + ".java");
        if(useBaseMapper && !subMapperFile.exists()){
            GeneratedJavaFile javaMapperFile = new GeneratedJavaFile(subMapperClass,targetProject,new DefaultJavaFormatter());
            awser.add(javaMapperFile);
        }
        return awser;
    }

    public boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
        if(useBaseEntity){
            interfaze.addImportedType(subEntityClass.getType());
        }
        if(useBaseMapper){
            subMapperClass.addSuperInterface(interfaze.getType());
        }
        if(useBaseEntity){
            for (Method method : interfaze.getMethods()) {
                List<Parameter> paramList = new ArrayList<>();
                List<Parameter> parameters = method.getParameters();
                Iterator<Parameter> iterator = parameters.iterator();
                while (iterator.hasNext()){
                    Parameter next = iterator.next();
                    if (next.getType().getFullyQualifiedName().equals(baseEntityName)) {
                        paramList.add(new Parameter(subEntityClass.getType(),next.getName(),next.getAnnotations().stream().collect(Collectors.joining(" ")),next.isVarargs()));
                    }else{
                        paramList.add(new Parameter(next.getType(),next.getName(),next.getAnnotations().stream().collect(Collectors.joining(" ")),next.isVarargs()));
                    }
                    iterator.remove();
                }
                parameters.addAll(paramList);


                FullyQualifiedJavaType returnType = method.getReturnType();
                if(returnType.getFullyQualifiedName().equals(baseEntityName)){
                    method.setReturnType(subEntityClass.getType());
                }

                List<FullyQualifiedJavaType> typeArguments = returnType.getTypeArguments();
                List<FullyQualifiedJavaType> typwArgList = new ArrayList<>();
                Iterator<FullyQualifiedJavaType> typeIterator = typeArguments.iterator();
                while (typeIterator.hasNext()){
                    FullyQualifiedJavaType next = typeIterator.next();
                    if (next.getFullyQualifiedName().equals(baseEntityName)) {
                        typwArgList.add(subEntityClass.getType());
                    }else{
                        typwArgList.add(next);
                    }
                    typeIterator.remove();
                }
                typeArguments.addAll(typwArgList);
            }

            interfaze.getImportedTypes().removeIf(next -> next.getFullyQualifiedName().equals(baseEntityName));
        }
        return true;
    }
}

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接