大家都清楚mybatis-generate-core 这个工程提供了获取表信息到生成model、dao、xml这三层代码的一个实现,但是这往往有一个痛点,比如需求来了,某个表需要增加字段,肯定需要重新运行mybatis自动生成的脚本,但是会去覆盖之前的代码,如model,dao的java代码,对于xml文件,目前有两种处理方式,一、覆盖,二、追加,本文用的版本是1.3.5版本,默认的是追加方式,本文的目的就是处理xml的一种合并方式,对于java代码的话,我个人认为无论是增加表字段还是其他情况,相对于xml文件都是比较好维护的,这里就不做讨论。
对于方式一的话,直接覆盖,肯定会导致之前自定义的sql,直接没了,还需要事先拷贝一份出来,最蛋疼的就是,可能还会在自动生成的代码文件中,增加了一些属性(如主键返回,flushCache属性),导致后来人员给忽略了,直到某个时刻才爆发出来。所以本文不采用这种方式,而是采用方式2,对于mybatis自定义的合并规则,看下文介绍。本文会对这个合并规则,进行重写,已达到我们的目标。如下
在重写该规则前,肯定是要摸清它的原有流程,下面分为这几个小节进行叙述
一、合并规则原理
二、重写规则
三、简述适用场景
本文采用的数据库是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包,放入到本地仓库里。
代码地址: Mybatis-generate-demo
如果还有其它比较好的方案。欢迎交流。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
转载请注明出处