前言
大家都清楚mybatis-generate-core 这个工程提供了获取表信息到生成model、dao、xml这三层代码的一个实现,但是这往往有一个痛点,比如需求来了,某个表需要增加字段,肯定需要重新运行mybatis自动生成的脚本,但是会去覆盖之前的代码,如model,dao的java代码,对于xml文件,目前有两种处理方式,一、覆盖,二、追加,本文用的版本是1.3.5版本,默认的是追加方式,本文的目的就是处理xml的一种合并方式,对于java代码的话,我个人认为无论是增加表字段还是其他情况,相对于xml文件都是比较好维护的,这里就不做讨论。
对于方式一的话,直接覆盖,肯定会导致之前自定义的sql,直接没了,还需要事先拷贝一份出来,最蛋疼的就是,可能还会在自动生成的代码文件中,增加了一些属性(如主键返回,flushCache属性),导致后来人员给忽略了,直到某个时刻才爆发出来。所以本文不采用这种方式,而是采用方式2,对于mybatis自定义的合并规则,看下文介绍。本文会对这个合并规则,进行重写,已达到我们的目标。如下
- 在启用自动生成代码后,原有的自定义sql,一律保留,包括,result|sql|select|delete|update|where|insert等标签,只要不是自动生成的
- 自动生成的标签中,手动添加的一些属性,如主键返回useGeneratedKeys="true" keyColumn="id",刷新一级缓存,flushCache="true"等属性标签也需要保留。
在重写该规则前,肯定是要摸清它的原有流程,下面分为这几个小节进行叙述
一、合并规则原理
二、重写规则
三、简述适用场景
本文采用的数据库是Mysql
一、合并规则原理
先来一段代码,莫慌,这段代码没什么特别,很常见的自动生成代码
1 package com.qm.mybatis.generate;
2
3 import org.mybatis.generator.api.MyBatisGenerator;
4 import org.mybatis.generator.config.Configuration;
5 import org.mybatis.generator.config.xml.ConfigurationParser;
6 import org.mybatis.generator.internal.DefaultShellCallback;
7
8 import java.io.InputStream;
9 import java.util.ArrayList;
10 import java.util.List;
11
12 public class GenerateTest {
13
14 public static void main(String[] args) {
15 List<String> warnings = new ArrayList<String>();
16 try {
17 boolean overwrite = true;
18 // 读取配置文件
19 InputStream resourceAsStream = GenerateTest.class.getResourceAsStream("/mybatis-generate.xml");
20 ConfigurationParser cp = new ConfigurationParser(warnings);
21 Configuration config = cp.parseConfiguration(resourceAsStream);
22 DefaultShellCallback callback = new DefaultShellCallback(overwrite);
23 MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
24 myBatisGenerator.generate(null);
25 } catch (Exception e) {
26
27 e.printStackTrace();
28 }
29
30 warnings.stream().forEach(warn -> {
31 System.out.println(warn);
32 });
33 System.out.println("生成成功!");
34 }
35 }
可见,最终的生成逻辑在MybatisGenerator#generate方法中,
1 // 最终生成代码的地方
2 public void generate(ProgressCallback callback, Set<String> contextIds,
3 Set<String> fullyQualifiedTableNames, boolean writeFiles) throws SQLException,
4 IOException, InterruptedException {
5
6 if (callback == null) {
7 callback = new NullProgressCallback();
8 }
9
10 generatedJavaFiles.clear();
11 generatedXmlFiles.clear();
12 ObjectFactory.reset();
13 RootClassInfo.reset();
14
15 // calculate the contexts to run
16 List<Context> contextsToRun;
17 if (contextIds == null || contextIds.size() == 0) {
18 contextsToRun = configuration.getContexts();
19 } else {
20 contextsToRun = new ArrayList<Context>();
21 for (Context context : configuration.getContexts()) {
22 if (contextIds.contains(context.getId())) {
23 contextsToRun.add(context);
24 }
25 }
26 }
27
28 // setup custom classloader if required
29 if (configuration.getClassPathEntries().size() > 0) {
30 ClassLoader classLoader = getCustomClassloader(configuration.getClassPathEntries());
31 ObjectFactory.addExternalClassLoader(classLoader);
32 }
33
34 // now run the introspections...
35 int totalSteps = 0;
36 for (Context context : contextsToRun) {
37 totalSteps += context.getIntrospectionSteps();
38 }
39 callback.introspectionStarted(totalSteps);
40
41 for (Context context : contextsToRun) {
42 context.introspectTables(callback, warnings,
43 fullyQualifiedTableNames);
44 }
45
46 // now run the generates
47 totalSteps = 0;
48 for (Context context : contextsToRun) {
49 totalSteps += context.getGenerationSteps();
50 }
51 callback.generationStarted(totalSteps);
52
53 for (Context context : contextsToRun) {
54 context.generateFiles(callback, generatedJavaFiles,
55 generatedXmlFiles, warnings);
56 }
57
58 // 前面各种文件都已经生成完毕,在这里进行保存到具体的文件中
59 if (writeFiles) {
60 callback.saveStarted(generatedXmlFiles.size()
61 + generatedJavaFiles.size());
62
63 // 进行xml文件保存(更新)的地方,也是本文的目标
64 for (GeneratedXmlFile gxf : generatedXmlFiles) {
65 projects.add(gxf.getTargetProject());
66 writeGeneratedXmlFile(gxf, callback);
67 }
68
69 // 保存java文件,如model,example,dao
70 for (GeneratedJavaFile gjf : generatedJavaFiles) {
71 projects.add(gjf.getTargetProject());
72 writeGeneratedJavaFile(gjf, callback);
73 }
74
75 for (String project : projects) {
76 shellCallback.refreshProject(project);
77 }
78 }
79
80 callback.done();
81 }
最终的落实地方就在writeGeneratedXmlFile方法内。
1 private void writeGeneratedXmlFile(GeneratedXmlFile gxf, ProgressCallback callback)
2 throws InterruptedException, IOException {
3 File targetFile;
4 String source;
5 try {
6 File directory = shellCallback.getDirectory(gxf
7 .getTargetProject(), gxf.getTargetPackage());
8 targetFile = new File(directory, gxf.getFileName());
9 // 如果为false,基本上就是第一次生成的时候
10 if (targetFile.exists()) {
11
12 /**
13 * 从这里也可以看出,这个参数决定xml文件的处理方式
14 * 为true时,会执行getMergedSource,透个底,改造也是改造这个方法
15 false,会继续后面两种逻辑。实际生成的内容其实是一样的。这里不做讨论
16 */
17 if (gxf.isMergeable()) {
18 source = XmlFileMergerJaxp.getMergedSource(gxf,
19 targetFile);
20 } else if (shellCallback.isOverwriteEnabled()) {
21 source = gxf.getFormattedContent();
22 warnings.add(getString("Warning.11", //$NON-NLS-1$
23 targetFile.getAbsolutePath()));
24 } else {
25 source = gxf.getFormattedContent();
26 targetFile = getUniqueFileName(directory, gxf
27 .getFileName());
28 warnings.add(getString(
29 "Warning.2", targetFile.getAbsolutePath())); //$NON-NLS-1$
30 }
31 } else {
32 source = gxf.getFormattedContent();
33 }
34
35 callback.checkCancel();
36 callback.startTask(getString(
37 "Progress.15", targetFile.getName())); //$NON-NLS-1$
38 writeFile(targetFile, source, "UTF-8"); //$NON-NLS-1$
39 } catch (ShellException e) {
40 warnings.add(e.getMessage());
41 }
42 }
饶了这么多圈,实际上我们要处理的就是重写XmlFileMergerJaxp#getMergedSource方法,或许有的人会提出疑问了,这个类能让你提供扩展吗?让你去继承?然后去改变这个规则,额(⊙o⊙)…,还真没有,这个类实际上就是一个静态方法,那这搞个毛线啊,你即使重写出来了,那你怎么将他插入进去,别告诉你准备重新编译源码。。。。。莫慌,往下看。
说到这,大家可以去了解一下类加载器和其加载的过程,本文不做过多阐述,直接来结论,你要想覆盖一个jar包里的某个方法,你就直接在你项目中,定义这个类(包名和类名需要完全一致),然后运行的时候,自然会执行你定义的这个类,千万别去想着同样的方法去覆盖jdk自带的类,没用,因为第三方jar包和jdk自带的类的类加载器不是同一个。有兴趣的可以去网上搜索一下。说了这么多,我们就是要这样做。做之前,先了解下这个merge方法的代码。
1 public static String getMergedSource(InputSource newFile,
2 InputSource existingFile, String existingFileName) throws IOException, SAXException,
3 ParserConfigurationException, ShellException {
4
5 DocumentBuilderFactory factory = DocumentBuilderFactory
6 .newInstance();
7 factory.setExpandEntityReferences(false);
8 DocumentBuilder builder = factory.newDocumentBuilder();
9 builder.setEntityResolver(new NullEntityResolver());
10
11 // 这是xml文件的解析结果,这里就暂且称为旧文件和新文件
12 Document existingDocument = builder.parse(existingFile);
13 Document newDocument = builder.parse(newFile);
14
15 DocumentType newDocType = newDocument.getDoctype();
16 DocumentType existingDocType = existingDocument.getDoctype();
17
18 // 比较两个xml文件是不是同一类型
19 if (!newDocType.getName().equals(existingDocType.getName())) {
20 throw new ShellException(getString("Warning.12", //$NON-NLS-1$
21 existingFileName));
22 }
23
24 // 获取根节点
25 Element existingRootElement = existingDocument.getDocumentElement();
26 Element newRootElement = newDocument.getDocumentElement();
27
28
29 NamedNodeMap attributes = existingRootElement.getAttributes();
30 int attributeCount = attributes.getLength();
31 for (int i = attributeCount - 1; i >= 0; i--) {
32 Node node = attributes.item(i);
33 existingRootElement.removeAttribute(node.getNodeName());
34 }
35
36 // add attributes from the new root node to the old root node
37 attributes = newRootElement.getAttributes();
38 attributeCount = attributes.getLength();
39 for (int i = 0; i < attributeCount; i++) {
40 Node node = attributes.item(i);
41 existingRootElement.setAttribute(node.getNodeName(), node
42 .getNodeValue());
43 }
44
45 // remove the old generated elements and any
46 // white space before the old nodes
47 List<Node> nodesToDelete = new ArrayList<Node>();
48 NodeList children = existingRootElement.getChildNodes();
49 int length = children.getLength();
50 for (int i = 0; i < length; i++) {
51 Node node = children.item(i);
52 if (isGeneratedNode(node)) {
53 nodesToDelete.add(node);
54 } else if (isWhiteSpace(node)
55 && isGeneratedNode(children.item(i + 1))) {
56 nodesToDelete.add(node);
57 }
58 }
59
60 for (Node node : nodesToDelete) {
61 existingRootElement.removeChild(node);
62 }
63
64 // add the new generated elements
65 children = newRootElement.getChildNodes();
66 length = children.getLength();
67 Node firstChild = existingRootElement.getFirstChild();
68 for (int i = 0; i < length; i++) {
69 Node node = children.item(i);
70 // don't add the last node if it is only white space
71 if (i == length - 1 && isWhiteSpace(node)) {
72 break;
73 }
74
75 Node newNode = existingDocument.importNode(node, true);
76 if (firstChild == null) {
77 existingRootElement.appendChild(newNode);
78 } else {
79 existingRootElement.insertBefore(newNode, firstChild);
80 }
81 }
82
83 // pretty print the result
84 return prettyPrint(existingDocument);
85 }
启动的29~43行,的目的是替换mapper节点的namespace,方式重新生成后,namespace有改变
之后的47~62行,就是删除一些节点,其实按照他这意思就是为了删掉特定的节点,具体实现逻辑在isGeneratedNode方法内,由它决定删不删。
65~81行就是将新文件中的所有节点(非空白节点)全部合并至旧文件中。
1 private static boolean isGeneratedNode(Node node) {
2 boolean rc = false;
3
4 if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
5 Element element = (Element) node;
6 String id = element.getAttribute("id"); //$NON-NLS-1$
7 if (id != null) {
8 for (String prefix : MergeConstants.OLD_XML_ELEMENT_PREFIXES) {
9 if (id.startsWith(prefix)) {
10 rc = true;
11 break;
12 }
13 }
14 }
15
16 if (rc == false) {
17 // check for new node format - if the first non-whitespace node
18 // is an XML comment, and the comment includes
19 // one of the old element tags,
20 // then it is a generated node
21 NodeList children = node.getChildNodes();
22 int length = children.getLength();
23 for (int i = 0; i < length; i++) {
24 Node childNode = children.item(i);
25 if (isWhiteSpace(childNode)) {
26 continue;
27 } else if (childNode.getNodeType() == Node.COMMENT_NODE) {
28 Comment comment = (Comment) childNode;
29 String commentData = comment.getData();
30 for (String tag : MergeConstants.OLD_ELEMENT_TAGS) {
31 if (commentData.contains(tag)) {
32 rc = true;
33 break;
34 }
35 }
36 } else {
37 break;
38 }
39 }
40 }
41 }
42
43 return rc;
44 }
逻辑其实也很简单,4~14行的逻辑就是删除id属性值带有一些特定前缀的节点,如果没找到,这删除commentNode节点,看到这,结果就出来了,按照正常情况下,根本不会把之前的就节点给删除掉。还是完完全全的保留。至此,就是我们常说的追加。
二、重写规则
从上述内容中,熟悉了原有的代码合并规则,接下来就是自定义规则了,本文就不放代码了,那样感觉很啰嗦,就直接简述一下实现思路,具体代码会在文末贴出github链接,可自行查看。
一、遍历新文件的所有非空白节点,遍历同时获取到对应旧文件中的节点,这里不考虑旧文件中有删除自动生成的节点情况,若获取到了,则遍历属性,有无增加,若增加,则移植到新文件中对应节点上,同时对该旧文件中的节点进行标记,等遍历完删掉。
二、第一个步骤完成后,然后再将新文件中的所有节点全部移植到旧文件中,最后视情况,需不需要格式化一下xml文件。
具体规则,就是图中红框处的文件
具体效果,大家可自行尝试,这里不贴效果图了。毕竟眼见为实。
注:不保证该规则适用于所有格式的xml文件,这块需要实地尝试。
三、适用场景
本文这种方式,只适用于代码来生成文件的方式,对于适用maven插件,并不适用,如果需要,这里提供一种无奈方案,就是获取对应源码,替换掉该类,重新编译成jar包,放入到本地仓库里。
四、最后
如果还有其它比较好的方案。欢迎交流。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
转载请注明出处
来源:oschina
链接:https://my.oschina.net/u/4384923/blog/4357799