前言
说到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;
}
}