让 MyBatis Generator 用数据库注释作 Java 注释,并支持附加注解

背景

这两天详细了解了 MyBatis Generator 这款工具,它生成 POJO 以及 Mapper 的功能还是比较实用的,而且由于生成的代码也是工程代码的一部分,自定义起来相对库来说也会方便一些,因此我打算将其用于公司的工程中。但是,MyBatis Generator 也是有一些不足的,其默认生成的 Java 注释实在惨不忍睹,包含了大量重复的无效信息;此外,我想实现在数据库中写一次注释,在 Java 代码中复用这种效果,同时还需要在生成 Java POJO 的过程中附加上公司自研 RPC 框架的注解。这两点功能 MyBatis Generator 默认提供的注释生成器是不能很好的支持的。因此需要自定义一个注释生成器来实现这些特定的需求。

实现

想要自己定义注释生成器,先要找到 MyBatis Generator 提供的注释生成器接口。这个接口是 org.mybatis.generator.api.CommentGenerator 其默认实现为 org.mybatis.generator.internal.DefaultCommentGenerator (默认实现也是唯一实现),其实我翻了一翻 DefaultCommentGenerator 的代码,发现其默认是支持将数据库注释作为生成的 Java 类的注释的,只是功能默认是关闭的,但是附加注解,默认实现是不支持的,我们就来自己实现吧。由于 MyBatis Generator 并没有为我们提供抽象类级别的生成器,只有一个顶层借口,而且顶层接口包含了过量的方法,大部分我们都用不到,因此们需要自己实现一个抽象类,来屏蔽不需要的方法接口,然后让自定义注释生成器继承抽象类(当然也可以直接继承默认实现,但我觉得那不是好的选择)。

拿起键盘就是干!哈哈,敲代码吧(工程在我的 GitHub 上):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package io.github.since1986.mybatis.comment.generator;

import org.mybatis.generator.api.CommentGenerator;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.*;
import org.mybatis.generator.api.dom.xml.XmlElement;

import java.util.Properties;
import java.util.Set;

// 抽象类,屏蔽掉接口中过多的不需要实现的方法,不需要的什么都不做就好了
public abstract class AbstractCommentGenerator implements CommentGenerator {

@Override
public void addConfigurationProperties(Properties properties) {

}

@Override
public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {

}

@Override
public void addFieldComment(Field field, IntrospectedTable introspectedTable) {

}

@Override
public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {

}

@Override
public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable) {

}

@Override
public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable, boolean markAsDoNotDelete) {

}

@Override
public void addEnumComment(InnerEnum innerEnum, IntrospectedTable introspectedTable) {

}

@Override
public void addGetterComment(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {

}

@Override
public void addSetterComment(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {

}

@Override
public void addGeneralMethodComment(Method method, IntrospectedTable introspectedTable) {

}

@Override
public void addJavaFileComment(CompilationUnit compilationUnit) {

}

@Override
public void addComment(XmlElement xmlElement) {

}

@Override
public void addRootComment(XmlElement rootElement) {

}

@Override
public void addGeneralMethodAnnotation(Method method, IntrospectedTable introspectedTable, Set<FullyQualifiedJavaType> imports) {

}

@Override
public void addGeneralMethodAnnotation(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn, Set<FullyQualifiedJavaType> imports) {

}

@Override
public void addFieldAnnotation(Field field, IntrospectedTable introspectedTable, Set<FullyQualifiedJavaType> imports) {

}

@Override
public void addFieldAnnotation(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn, Set<FullyQualifiedJavaType> imports) {

}

@Override
public void addClassAnnotation(InnerClass innerClass, IntrospectedTable introspectedTable, Set<FullyQualifiedJavaType> imports) {

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package io.github.since1986.mybatis.comment.generator;

import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.config.PropertyRegistry;
import org.mybatis.generator.internal.util.StringUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

// 这个是我们具体的实现,可以借鉴默认实现的一些办法去写
public class CommentWithAnnotationCommentGenerator extends AbstractCommentGenerator {

private static final Logger LOGGER = LoggerFactory.getLogger(CommentWithAnnotationCommentGenerator.class);

private static final String SUPPRESS_ALL_ANNOTATIONS = "suppressAllAnnotations";
private static final String FIELD_ANNOTATION_FULLY_QUALIFIED_NAMES = "fieldAnnotationFullyQualifiedNames";
private static final String CLASS_ANNOTATION_FULLY_QUALIFIED_NAMES = "classAnnotationFullyQualifiedNames";

private static final String FIELD_COMMENT_TEMPLATE = "/** %s */";
private static final String CLASS_COMMENT_TEMPLATE = "/** %s */";

private Properties properties = new Properties();

// 保留官方的重要设置属性,这样更符合直觉
private boolean suppressAllComments;

private boolean suppressAllAnnotations;
private List<String> fieldAnnotationFullyQualifiedNames = new LinkedList<>();
private List<String> classAnnotationFullyQualifiedNames = new LinkedList<>();

@Override
public void addConfigurationProperties(Properties properties) {
this.properties.putAll(properties);
suppressAllComments = StringUtility.isTrue(this.properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_ALL_COMMENTS));
suppressAllAnnotations = StringUtility.isTrue(this.properties.getProperty(SUPPRESS_ALL_ANNOTATIONS));
fieldAnnotationFullyQualifiedNames.addAll(Util.toList(this.properties.getProperty(FIELD_ANNOTATION_FULLY_QUALIFIED_NAMES)));
classAnnotationFullyQualifiedNames.addAll(Util.toList(this.properties.getProperty(CLASS_ANNOTATION_FULLY_QUALIFIED_NAMES)));
}

@Override
public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
if (!suppressAllComments && Util.isNotBlank(introspectedColumn.getRemarks())) {
LOGGER.debug(String.format("field = %s, column = %s, columnRemarks = %s", field.getName(), introspectedColumn.getActualColumnName(), introspectedColumn.getRemarks()));
field.addJavaDocLine(String.format(FIELD_COMMENT_TEMPLATE, introspectedColumn.getRemarks()));
}
if (!suppressAllAnnotations) {
LOGGER.debug(String.format("fieldAnnotationFullyQualifiedNames = %s]", fieldAnnotationFullyQualifiedNames));
fieldAnnotationFullyQualifiedNames.forEach(fieldAnnotationFullyQualifiedName -> {
field.addJavaDocLine(Util.atAnnotationName(fieldAnnotationFullyQualifiedName));
});
} else {
LOGGER.debug(String.format("suppressAllComments = %b, suppressAllAnnotations = %b", suppressAllComments, suppressAllAnnotations));
}
}

@Override
public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
if (!suppressAllComments && Util.isNotBlank(introspectedTable.getRemarks())) {
LOGGER.debug(String.format("class = %s, table = %s, tableRemarks = %s", topLevelClass.getType(), introspectedTable.getFullyQualifiedTable(), introspectedTable.getRemarks()));
topLevelClass.addJavaDocLine(String.format(CLASS_COMMENT_TEMPLATE, introspectedTable.getRemarks()));
}
if (!suppressAllAnnotations) {
LOGGER.debug(String.format("classAnnotationFullyQualifiedNames = %s]", classAnnotationFullyQualifiedNames));
classAnnotationFullyQualifiedNames.forEach(classAnnotationFullyQualifiedName -> {
topLevelClass.addJavaDocLine(Util.atAnnotationName(classAnnotationFullyQualifiedName));
topLevelClass.addImportedType(new FullyQualifiedJavaType(classAnnotationFullyQualifiedName));
});
fieldAnnotationFullyQualifiedNames.forEach(fieldAnnotationFullyQualifiedName -> {
topLevelClass.addImportedType(new FullyQualifiedJavaType(fieldAnnotationFullyQualifiedName));
});
} else {
LOGGER.debug(String.format("suppressAllComments = %b, suppressAllAnnotations = %b", suppressAllComments, suppressAllAnnotations));
}
}

static class Util {

static boolean isBlank(String string) {
return string == null || string.trim().length() == 0;
}

static boolean isNotBlank(String string) {
return !isBlank(string);
}

static List<String> toList(String commaSeparatedString) {
List<String> list = new LinkedList<>();
assert isNotBlank(commaSeparatedString);
StringTokenizer stringTokenizer = new StringTokenizer(commaSeparatedString, ",");
String token;
while (stringTokenizer.hasMoreTokens()) {
token = stringTokenizer.nextToken();
list.add(token);
}
if (list.size() == 0) {
list.add(commaSeparatedString);
}
return list;
}

static String atAnnotationName(String annotationFullyQualifiedName) {
int lastIndexOfDot = annotationFullyQualifiedName.lastIndexOf(".");
assert lastIndexOfDot != -1;
String atAnnotationName = annotationFullyQualifiedName.substring(lastIndexOfDot + 1);
assert isNotBlank(atAnnotationName);
return String.format("@%s", atAnnotationName);
}
}
}

上边两个类是我们自定义注释生成器的所有核心内容,我在实现的时候踩了一个小小的坑,在注释生成器接口中有几个 addFieldAnnotation addClassAnnotation 命名中包含注解字眼的方法,我一开始想当然的以为附加注解需要在这里面实现,后来才发现,两者没什么关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Adds a @Generated annotation to a class.
*
* @param innerClass
* the class
* @param introspectedTable
* the introspected table
* @param imports
* the comment generator may add a required imported type to this list
*
* @since 1.3.6
*/
void addClassAnnotation(InnerClass innerClass, IntrospectedTable introspectedTable,
Set<FullyQualifiedJavaType> imports);

上边是 addClassAnnotation 的注释,可以看到,其根@Generated有关。我们自己想要附加注解,实际上还是在附加注释那一步以字符串的形式附加上去的,与这几个*Annotation方法无关。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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="package/to/your/data_source.properties"/>
<context id="context_mysql" defaultModelType="flat" targetRuntime="MyBatis3">
<property name="javaFileEncoding" value="UTF-8"/>

<!-- CommentWithAnnotationCommentGenerator -->
<commentGenerator type="io.github.since1986.mybatis.comment.generator.CommentWithAnnotationCommentGenerator">
<property name="fieldAnnotationFullyQualifiedNames" value="one.your.customer.FieldAnnotationClass,another.your.customer.FieldAnnotationClass"/>
<property name="classAnnotationFullyQualifiedNames" value="one.your.customer.ClassAnnotationClass,another.your.customer.ClassAnnotationClass"/>
</commentGenerator>

<jdbcConnection driverClass="${driverClass}" connectionURL="${connectionURL}" userId="${userId}" password="${password}">
<!-- better keep this -->
<property name="useInformationSchema" value="true"/>
</jdbcConnection>

<javaModelGenerator targetPackage="your.model" targetProject="/path/to/your/project/src/main/java">
</javaModelGenerator>


<sqlMapGenerator targetPackage="your.mapper" targetProject="src/main/resources">
</sqlMapGenerator>


<javaClientGenerator targetPackage="your.mapper" type="ANNOTATEDMAPPER" targetProject="src/main/java">
</javaClientGenerator>

<table tableName="your_table" domainObjectName="YourDomain">
</table>
</context>
</generatorConfiguration>

在生成的 Java 中会将数据库注释作为注释,诠释了DRY原则,另外付加入了我们需要的框架注解,不用再手工处理重复劳动了,解放了生产力,可以早点下班回家,多休息休息了(做梦中…)

参考资料

mybatis-generator自定义注释生成

Mybatis Generator最完整配置详解