自定義Mybatis自動生成程式碼規則
前言
大家都清楚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包,放入到本地倉庫裡。
程式碼地址:Mybatis-generate-demo
四、最後
如果還有其它比較好的方案。歡迎交流。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
轉載請註明出處
&n