├── .gitignore ├── LICENSE ├── README-en.md ├── README.md ├── pom.xml └── src └── main └── java └── online └── githuboy └── jrebel └── mybatisplus ├── MybatisPlusPlugin.java └── cbp ├── Constants.java ├── MybatisConfigurationCBP.java ├── MybatisMapperAnnotationBuilderCBP.java ├── MybatisMapperProxyCBP.java ├── MybatisMapperProxyFactoryCBP.java ├── MybatisSqlSessionFactoryBeanCBP.java ├── StrictMapCBP.java └── XMLMapperBuilderCBP.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target/ 4 | dumped -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 suchu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # jrebel-mybatisplus 2 | 3 | A hook plugin for Support MybatisPlus that reloads modified SQL maps. 4 | 5 | ([中文](README.md)|English) 6 | 7 | # Preconditions 8 | 9 | 1. `mybatis-plus:3.1.1+` 10 | 11 | 2. IDEA Run/Debug Configurations 12 | 13 | > On 'Update' actions: Update classes and resources 14 | 15 | > On frame deactivation: Update classes and resources 16 | 17 | # How to use 18 | 19 | ## Use IDEA plugin 20 | 21 | You can download the plugin from jetbrains plugins market [jrebel-mybatisplus-extension](https://plugins.jetbrains.com/plugin/12682-jrebel-mybatisplus-extension). 22 | 23 | ## Local build 24 | 25 | Running the web app with the custom plugin 26 | Compile the JRebel plugin like a regular maven project: `mvn -f jr-mybatisplus/pom.xml clean package`. This will produce a plugin jar in jr-mybatisplus/target/jr-mybatisplus.jar. To enable the plugin, pass the plugin’s path to JRebel using a JVM argument: `-Drebel.plugins=/path/to/jr-mybatisplus.jar`. 27 | 28 | 29 | Running from the IDE 30 | Compile the sample application and the sample plugin using mvn clean package. Configure your favourite web server with the JVM argument -Drebel.plugins=/path/to/jr-mybatisplus.jar, start it with JRebel and deploy the sample application. 31 | 32 | 33 | Running from the command line 34 | Compile the sample application and the sample plugin using `mvn clean package`. Attach the JRebel agent as usual, but also add the path to the sample plugin: 35 | 36 | Linux and Mac OS: 37 | ``` 38 | export MAVEN_OPTS=" -Drebel.plugins=/path/to/jr-mybatisplus.jar" 39 | mvn -f demo-application/pom.xml jetty:run 40 | ``` 41 | Windows: 42 | ``` 43 | set MAVEN_OPTS=" -Drebel.plugins=/path/to/jr-mybatisplus.jar" 44 | mvn -f demo-application\pom.xml jetty:run 45 | ``` 46 | Check that the plugin works: 47 | 48 | Modify your `Mybatis mapper` xml file. The “Reloading SQL maps” message should appear in the console at the start of the next request. 49 | 50 | 51 | 52 | # Reference 53 | 54 | [Custom JRebel plugins](http://manuals.zeroturnaround.com/jrebel/advanced/custom.html#jrebelcustom) 55 | 56 | [Getting Started with Javassist](http://www.javassist.org/tutorial/tutorial.html) 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # jrebel-mybatisplus 3 | 4 | Jrebel mybatisplus热加载插件,支持重新加载修改后的SQL映射 5 | 6 | (中文|[English](README-en.md)) 7 | 8 | # 前置条件 9 | 10 | 1. **你的IDE安装了[JRebel插件](https://jrebel.com/software/jrebel/download/prev-releases/)** 11 | 12 | 2. `mybatis-plus:3.1.1+` 13 | 14 | 3. IDEA Run/Debug Configurations 配置 15 | 16 | > On 'Update' actions: Update classes and resources 17 | 18 | > On frame deactivation: Update classes and resources 19 | 20 | 21 | 22 | # 如何使用 23 | 24 | 已开发IDEA的插件 [jrebel-mybatisplus-idea-plugin](https://github.com/SweetInk/jrebel-mybatisplus-idea-plugin). 安装插件后即可使用,不需要再配置了。 25 | 26 | ## 构建插件 27 | 28 | ``` shell 29 | git clone git@github.com:SweetInk/jrebel-mybatisplus.git 30 | cd jrebel-mybatisplus 31 | mvn -f jr-mybatisplus/pom.xml clean package 32 | ``` 33 | 34 | 将构建好的插件`jrebel-mybatisplus\target\jr-mybatisplus.jar`拷贝至任意目录, 比如: `d:\jrebel\plugin\jr-mybatisplus.jar` 35 | 36 | ## 使用 37 | 38 | 打开你的IDE(Intellij IDEA or Eclipse),修改运行配置,增加VM参数:`-Drebel.plugins=d:\jrebel\plugin\jr-mybatisplus.jar`,然后以JRebel方式启动 39 | 40 | 检查插件是否生效: 41 | 42 | 修改你项目中的mapper xml 文件后,重新编译,如果重新请求接口,你应该会看到控制台输出 “Reloading SQL maps” 43 | 44 | ## debug相关 45 | 46 | ### 输出ctClass到本地 47 | 48 | ```java 49 | public class YourClassCBP extends JavassistClassBytecodeProcessor { 50 | public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception { 51 | //TODO class modify 52 | String output = "X:\\workspace\\dump"; 53 | ctClass.writeFile(output); 54 | if (ctClass.isFrozen()) { 55 | ctClass.defrost(); 56 | } 57 | } 58 | } 59 | 60 | ``` 61 | 62 | 63 | # 参考 64 | 65 | [Custom JRebel plugins](http://manuals.zeroturnaround.com/jrebel/advanced/custom.html#jrebelcustom) 66 | 67 | [Getting Started with Javassist](http://www.javassist.org/tutorial/tutorial.html) 68 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | online.githuboy 6 | jr-mybatisplus 7 | JRebel MyBatisPlus Plugin 8 | 1.0.7 9 | 10 | 11 | suchu 12 | suchu01@hotmail.com 13 | GMT+8 14 | https://github.com/sweetink 15 | 16 | 17 | zeroturnaround 18 | https://www.jrebel.com/ 19 | 20 | 21 | A hook for Support MybatisPlus that reloads modified SQL maps. 22 | 23 | UTF-8 24 | 7.0.0 25 | 26 | 27 | 28 | 29 | org.zeroturnaround 30 | jr-sdk 31 | ${sdk.version} 32 | provided 33 | 34 | 35 | org.zeroturnaround 36 | jr-utils 37 | ${sdk.version} 38 | provided 39 | 40 | 41 | com.baomidou 42 | mybatis-plus 43 | 3.1.1 44 | provided 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-compiler-plugin 53 | 3.6.1 54 | 55 | 1.8 56 | 1.8 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-jar-plugin 62 | 3.0.2 63 | 64 | 65 | 66 | online.githuboy.jrebel.mybatisplus.MybatisPlusPlugin 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | zt-public 77 | https://repos.zeroturnaround.com/nexus/content/groups/zt-public 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/MybatisPlusPlugin.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus; 2 | 3 | import online.githuboy.jrebel.mybatisplus.cbp.*; 4 | import org.zeroturnaround.javarebel.*; 5 | 6 | import java.io.IOException; 7 | import java.util.Properties; 8 | 9 | /** 10 | * Plugin Main entry 11 | * 12 | * @author suchu 13 | * @author andresluuk 14 | * @since 2019/5/9 17:56 15 | */ 16 | public class MybatisPlusPlugin implements Plugin { 17 | private static final Logger log = LoggerFactory.getLogger("MyBatisPlus"); 18 | 19 | @Override 20 | public void preinit() { 21 | Properties p = new Properties(); 22 | String version = ""; 23 | try { 24 | p.load(getClass().getClassLoader().getResourceAsStream("META-INF/maven/online.githuboy/jr-mybatisplus/pom.properties")); 25 | version = p.getProperty("version"); 26 | } catch (IOException e) { 27 | log.error("Can not read jr-mybatisplus/pom.properties:", e.getMessage()); 28 | } 29 | log.infoEcho("Ready config JRebel MybatisPlus plugin(" + version + ")..."); 30 | ClassLoader classLoader = MybatisPlusPlugin.class.getClassLoader(); 31 | Integration integration = IntegrationFactory.getInstance(); 32 | //register class processor 33 | configMybatisPlusProcessor(integration, classLoader); 34 | configMybatisProcessor(integration, classLoader); 35 | } 36 | 37 | private void configMybatisPlusProcessor(Integration integration, ClassLoader classLoader) { 38 | //if there has MybatisPlus ClassResource 39 | log.infoEcho("Add CBP for mybatis-plus core classes..."); 40 | integration.addIntegrationProcessor(classLoader, "com.baomidou.mybatisplus.core.MybatisConfiguration", new MybatisConfigurationCBP()); 41 | integration.addIntegrationProcessor(classLoader, "com.baomidou.mybatisplus.core.MybatisMapperAnnotationBuilder", new MybatisMapperAnnotationBuilderCBP()); 42 | integration.addIntegrationProcessor(classLoader, "com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean", new MybatisSqlSessionFactoryBeanCBP()); 43 | // integration.addIntegrationProcessor(classLoader, "com.baomidou.mybatisplus.core.override.MybatisMapperProxy", new MybatisMapperProxyCBP()); 44 | integration.addIntegrationProcessor(classLoader, "com.baomidou.mybatisplus.core.override.MybatisMapperProxyFactory", new MybatisMapperProxyFactoryCBP()); 45 | integration.addIntegrationProcessor(classLoader, "com.baomidou.mybatisplus.core.MybatisConfiguration$StrictMap", new StrictMapCBP()); 46 | } 47 | 48 | private void configMybatisProcessor(Integration integration, ClassLoader classLoader) { 49 | integration.addIntegrationProcessor(classLoader, "org.apache.ibatis.builder.xml.XMLMapperBuilder", new XMLMapperBuilderCBP()); 50 | } 51 | 52 | @Override 53 | public boolean checkDependencies(ClassLoader classLoader, ClassResourceSource classResourceSource) { 54 | return classResourceSource.getClassResource("com.baomidou.mybatisplus.core.MybatisConfiguration") != null; 55 | } 56 | 57 | @Override 58 | public String getId() { 59 | return "mybatis_plus_plugin"; 60 | } 61 | 62 | @Override 63 | public String getName() { 64 | return "MybatisPlus_plugin"; 65 | } 66 | 67 | @Override 68 | public String getDescription() { 69 | return "
  • A hook plugin for Support MybatisPlus that reloads modified SQL maps.
  • "; 70 | } 71 | 72 | @Override 73 | public String getAuthor() { 74 | return "suchu"; 75 | } 76 | 77 | @Override 78 | public String getWebsite() { 79 | return "https://githuboy.online"; 80 | } 81 | 82 | @Override 83 | public String getSupportedVersions() { 84 | return null; 85 | } 86 | 87 | @Override 88 | public String getTestedVersions() { 89 | return null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/Constants.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | /** 4 | * Constants 5 | * 6 | * @author suchu 7 | * @since 2019/5/9 19:42 8 | */ 9 | public class Constants { 10 | public static final String JrConfigurationClass = "org.zeroturnaround.jrebel.mybatis.JrConfiguration"; 11 | public static final String SqlMapReloaderClass = "org.zeroturnaround.jrebel.mybatis.SqlMapReloader"; 12 | public static final String JrInterceptorChain = "org.zeroturnaround.jrebel.mybatis.JrInterceptorChain"; 13 | public static final String JrSqlSessionFactoryBeanClass = "org.zeroturnaround.jrebel.mybatis.JrSqlSessionFactoryBean"; 14 | public static final String MybatisConfiguration$StrictMapClass ="com.baomidou.mybatisplus.core.MybatisConfiguration$StrictMap"; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/MybatisConfigurationCBP.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | import org.zeroturnaround.bundled.javassist.*; 4 | import org.zeroturnaround.javarebel.LoggerFactory; 5 | import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor; 6 | import org.zeroturnaround.javarebel.integration.util.JavassistUtil; 7 | 8 | /** 9 | * @author zeroturnaround 10 | * @author suchu 11 | * @since 2019/5/9 18:02 12 | */ 13 | public class MybatisConfigurationCBP extends JavassistClassBytecodeProcessor { 14 | private static final String LOGGER = (LoggerFactory.class.getName() + ".getInstance().productPrefix(\"MyBatisPlus\")"); 15 | 16 | @Override 17 | public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception { 18 | cp.importPackage("java.util.Map"); 19 | cp.importPackage("java.util.ArrayList"); 20 | cp.importPackage("java.util.HashMap"); 21 | cp.importPackage(Constants.JrConfigurationClass); 22 | cp.importPackage(Constants.SqlMapReloaderClass); 23 | cp.importPackage(Constants.JrInterceptorChain); 24 | cp.importPackage("org.apache.ibatis.plugin.Interceptor"); 25 | ctClass.addInterface(cp.get(Constants.JrConfigurationClass)); 26 | ctClass.addField(new CtField(cp.get(Constants.SqlMapReloaderClass), "reloader", ctClass)); 27 | ctClass.addField(CtField.make("private ArrayList __nonXmlInterceptors = new ArrayList();", ctClass)); 28 | overrideAddInterceptor(ctClass); 29 | overrideIsResourceLoaded(ctClass); 30 | CtConstructor[] constructors = ctClass.getConstructors(); 31 | for (CtConstructor constructor : constructors) { 32 | if (constructor.callsSuper()) { 33 | constructor.insertAfter("reloader = new " + Constants.SqlMapReloaderClass + "($0);"); 34 | } 35 | } 36 | //rewrite addMappedStatement 37 | String bodyStatement = ""; 38 | if (null != cp.getOrNull(Constants.MybatisConfiguration$StrictMapClass)) { 39 | bodyStatement = "mappedStatements.put($1.getId(), $1);"; 40 | } 41 | ctClass.getDeclaredMethod("addMappedStatement").setBody("{super.addMappedStatement($1);" + bodyStatement + "}"); 42 | ctClass.getDeclaredMethod("addInterceptor").insertBefore("if (!reloader.isInsideConf() && !reloader.isReloading()) { " + LOGGER + ".info(\"Memorizing non-xml interceptor: {}\", $1); __nonXmlInterceptors.add($1);}"); 43 | ctClass.addMethod(CtNewMethod.make("public " + Constants.SqlMapReloaderClass + " getReloader() { return reloader;}", ctClass)); 44 | ctClass.addMethod(CtNewMethod.make("public void reinit() { loadedResources.clear(); ((" + Constants.JrInterceptorChain + ") interceptorChain).jrClear();}", ctClass)); 45 | ctClass.addMethod(CtNewMethod.make("public void jrRebuildStatements() {" + (JavassistUtil.hasDeclaredMethod(ctClass, "buildAllStatements") ? " buildAllStatements();" : "") + "}", ctClass)); 46 | ctClass.addMethod(CtNewMethod.make("public void afterReloading() { " + LOGGER + ".info(\"XML configuration were reloaded, restoring memorized interceptors\"); for (java.util.Iterator iter = __nonXmlInterceptors.iterator(); iter.hasNext(); ) { Interceptor interceptor = (Interceptor) iter.next(); " + LOGGER + ".info(\"re-adding interceptor: {}\", interceptor); interceptorChain.addInterceptor(interceptor); }}", ctClass)); 47 | ctClass.getDeclaredMethod("isResourceLoaded").insertAfter("if (reloader.shouldReload($1)) { loadedResources.remove($1); $_ = false;}"); 48 | if (!JavassistUtil.hasDeclaredMethod(ctClass, "getSqlFragments")) { 49 | ctClass.addMethod(CtNewMethod.make("public Map getSqlFragments() { return super.getSqlFragments();}", ctClass)); 50 | } 51 | } 52 | 53 | /** 54 | * Override the `addInterceptor` 55 | * 56 | * @param ctClass CtClass 57 | */ 58 | private void overrideAddInterceptor(CtClass ctClass) throws CannotCompileException { 59 | ctClass.addMethod(CtNewMethod.make("public void addInterceptor(org.apache.ibatis.plugin.Interceptor interceptor){ super.addInterceptor(interceptor);}", ctClass)); 60 | } 61 | 62 | /** 63 | * Override the `IsResourceLoaded` 64 | * 65 | * @param ctClass CtClass 66 | */ 67 | private void overrideIsResourceLoaded(CtClass ctClass) throws CannotCompileException { 68 | ctClass.addMethod(CtNewMethod.make("public boolean isResourceLoaded(String resource){return super.isResourceLoaded(resource);}", ctClass)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/MybatisMapperAnnotationBuilderCBP.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | import org.zeroturnaround.bundled.javassist.*; 4 | import org.zeroturnaround.bundled.javassist.expr.ExprEditor; 5 | import org.zeroturnaround.bundled.javassist.expr.MethodCall; 6 | import org.zeroturnaround.bundled.javassist.expr.NewExpr; 7 | import org.zeroturnaround.javarebel.ClassEventListener; 8 | import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor; 9 | 10 | /** 11 | * MybatisMapperAnnotationBuilder class hook 12 | * @author zeroturnaround 13 | * @author suchu 14 | * @since 2019/5/9 19:42 15 | */ 16 | public class MybatisMapperAnnotationBuilderCBP extends JavassistClassBytecodeProcessor { 17 | class LoadXmlResourceHook extends ExprEditor { 18 | LoadXmlResourceHook() { 19 | } 20 | 21 | public void edit(NewExpr e) throws CannotCompileException { 22 | String className = e.getClassName(); 23 | if ("org.apache.ibatis.builder.xml.XMLMapperBuilder".equals(className) || 24 | "com.baomidou.mybatisplus.core.MybatisXMLMapperBuilder".equals(className)) { 25 | e.replace("{ $_ = $proceed($$); if ($2 instanceof JrConfiguration) { SqlMapReloader reloader = ((JrConfiguration) $2).getReloader(); if (reloader != null) { reloader.addMapping(Resources.getResourceURL($3), $3); } }}"); 26 | } 27 | } 28 | 29 | public void edit(MethodCall m) throws CannotCompileException { 30 | if ("getResourceAsStream".equals(m.getMethodName()) && "org.apache.ibatis.io.Resources".equals(m.getClassName())) { 31 | m.replace("{ try { $_ = $proceed($$); } catch (java.io.IOException ioe) { if (this.configuration instanceof " + Constants.JrConfigurationClass + ") { SqlMapReloader reloader = ((JrConfiguration) this.configuration).getReloader(); if (reloader != null && reloader.mappingsLoadedFromSameLocation()) { String _urlString = reloader.buildUrlBasedOnFirstMapping(type); String _path = reloader.buildPathBasedOnFirstMapping(type); java.io.InputStream _in = null; try { _in = Resources.getUrlAsStream(_urlString); } catch (java.io.IOException ignore) {} if (_in != null) { try { XMLMapperBuilder _builder = new XMLMapperBuilder(_in, this.configuration, _path, this.configuration.getSqlFragments()); _builder.parse(); } catch (Exception e) { throw new RuntimeException(\"Failed to parse mapping resource: '\" + _path + \"'\", e); } finally { ErrorContext.instance().reset(); } reloader.addMapping(new java.net.URL(_urlString), _path); } } } }}"); 32 | } 33 | } 34 | } 35 | 36 | class ParseHook extends ExprEditor { 37 | ParseHook() { 38 | } 39 | 40 | public void edit(MethodCall m) throws CannotCompileException { 41 | if ("isResourceLoaded".equals(m.getMethodName())) { 42 | m.replace("$_ = !SqlMapReloader.isReloading() && $proceed($$);"); 43 | } 44 | } 45 | } 46 | 47 | public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception { 48 | cp.importPackage("org.apache.ibatis.io"); 49 | cp.importPackage("org.apache.ibatis.executor"); 50 | cp.importPackage("org.apache.ibatis.builder.xml"); 51 | cp.importPackage("org.zeroturnaround.jrebel.mybatis"); 52 | ctClass.getDeclaredMethod("loadXmlResource").instrument(new LoadXmlResourceHook()); 53 | processAnnotationReloading(cp, ctClass); 54 | } 55 | 56 | private void processAnnotationReloading(ClassPool cp, CtClass ctClass) throws NotFoundException, CannotCompileException { 57 | cp.importPackage("org.zeroturnaround.javarebel"); 58 | cp.importPackage("org.zeroturnaround.javarebel.integration.util"); 59 | cp.importPackage("java.util"); 60 | ctClass.addInterface(cp.get(ClassEventListener.class.getName())); 61 | ctClass.addField(CtField.make("private static final Map __types = Collections.synchronizedMap(new WeakIdentityHashMap());", ctClass)); 62 | for (CtConstructor c : ctClass.getDeclaredConstructors()) { 63 | if (c.callsSuper()) { 64 | c.insertAfter("{ if (!__types.containsKey(type)) { __types.put(type, null); ReloaderFactory.getInstance().addHierarchyReloadListener(type, WeakUtil.weak(ClassLoaderLocalUtil.bind($0, type.getClassLoader()))); }}"); 65 | } 66 | } 67 | ctClass.addMethod(CtNewMethod.make("public int priority() { return ClassEventListener.PRIORITY_DEFAULT;}", ctClass)); 68 | ctClass.addMethod(CtNewMethod.make("public void onClassEvent(int eventType, Class clazz) { SqlMapReloader.enterReloading(); try { parse(); } finally { SqlMapReloader.exitReloading(); ErrorContext.instance().reset(); }}", ctClass)); 69 | ctClass.getDeclaredMethod("parse").instrument(new ParseHook()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/MybatisMapperProxyCBP.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | import org.zeroturnaround.bundled.javassist.ClassPool; 4 | import org.zeroturnaround.bundled.javassist.CtClass; 5 | import org.zeroturnaround.bundled.javassist.NotFoundException; 6 | import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor; 7 | 8 | /** 9 | * MybatisMapperProxy class hook 10 | * 11 | * @author suchu 12 | * @author zeroturnaround 13 | * @since 2019/05/01 09:19 14 | */ 15 | public class MybatisMapperProxyCBP extends JavassistClassBytecodeProcessor { 16 | public MybatisMapperProxyCBP() { 17 | } 18 | 19 | public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception { 20 | this.disableMethodCache(ctClass); 21 | } 22 | 23 | private void disableMethodCache(CtClass ctClass) throws Exception { 24 | try { 25 | ctClass.getDeclaredMethod("cachedMapperMethod").insertBefore("{ this.methodCache.clear(); }"); 26 | } catch (NotFoundException var3) { 27 | } 28 | //Compatible with mybatis-plus 3.4.0+ https://github.com/baomidou/mybatis-plus/commit/a6ae6ab4e91b126e1212d7041f6bd8365e5ab286 29 | try { 30 | ctClass.getDeclaredMethod("cachedInvoker").insertBefore("{ this.methodCache.clear(); }"); 31 | } catch (NotFoundException var3) { 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/MybatisMapperProxyFactoryCBP.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | import com.baomidou.mybatisplus.core.override.MybatisMapperProxy; 4 | import org.zeroturnaround.bundled.javassist.ClassPool; 5 | import org.zeroturnaround.bundled.javassist.CtClass; 6 | import org.zeroturnaround.bundled.javassist.CtField; 7 | import org.zeroturnaround.bundled.javassist.CtMethod; 8 | import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor; 9 | 10 | /** 11 | * MybatisMapperProxyFactory class hook
    12 | *
    13 | *

    14 | * Optimize {@link MybatisMapperProxy} to clear all method caches when Mapper reloads, 15 | * thus avoiding the need for {@link MybatisMapperProxy} to {@link MybatisMapperProxyCBP#disableMethodCache clean} up the method cache every time the {@link MybatisMapperProxy#invoke} method is executed
    16 | *

    17 | *
    18 | * Known issues 19 | *
      20 | *
    1. Probabilistic {@link StackOverflowError} is thrown when mapper is modified and reloaded,it only needs to be re-requested.
    2. 21 | *
    22 | * 23 | * @author suchu 24 | * @since 2022/07/19 25 | */ 26 | public class MybatisMapperProxyFactoryCBP extends JavassistClassBytecodeProcessor { 27 | @Override 28 | public void process(ClassPool classPool, ClassLoader classLoader, CtClass ctClass) throws Exception { 29 | ctClass.addField(CtField.make("private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(com.baomidou.mybatisplus.core.override.MybatisMapperProxyFactory.class);", ctClass)); 30 | CtMethod newInstanceMethod = ctClass.getDeclaredMethod("newInstance", new CtClass[]{classPool.get("org.apache.ibatis.session.SqlSession")}); 31 | newInstanceMethod.insertBefore(" { " + 32 | " if(!this.methodCache.isEmpty()){\n" + 33 | " logger.info(\"JRebel: clean MybatisMapperProxy:{} method cache\",mapperInterface.getName());\n" + 34 | " java.util.Iterator iterator = this.methodCache.entrySet().iterator();\n" + 35 | " while (iterator.hasNext()){\n" + 36 | " java.util.Map.Entry next =(java.util.Map.Entry) iterator.next();\n" + 37 | " java.lang.reflect.Method method =(java.lang.reflect.Method) next.getKey();\n" + 38 | " if(mapperInterface.equals(method.getDeclaringClass())){\n" + 39 | " logger.info(\"\\t method -> {}\",method.toString());\n" + 40 | " iterator.remove();\n" + 41 | " }\n" + 42 | " }\n" + 43 | "}\n" + 44 | "}\n"); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/MybatisSqlSessionFactoryBeanCBP.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | 4 | import org.zeroturnaround.bundled.javassist.*; 5 | import org.zeroturnaround.bundled.javassist.expr.ExprEditor; 6 | import org.zeroturnaround.bundled.javassist.expr.FieldAccess; 7 | import org.zeroturnaround.bundled.javassist.expr.MethodCall; 8 | import org.zeroturnaround.bundled.javassist.expr.NewExpr; 9 | import org.zeroturnaround.javarebel.ConfigurationFactory; 10 | import org.zeroturnaround.javarebel.LoggerFactory; 11 | import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor; 12 | import org.zeroturnaround.javarebel.integration.util.JavassistUtil; 13 | 14 | /** 15 | * MybatisSqlSessionFactoryBean class hook 16 | * 17 | * @author zeroturnaround 18 | * @author suchu 19 | * @since 2019/05/08 20:31 20 | */ 21 | public class MybatisSqlSessionFactoryBeanCBP extends JavassistClassBytecodeProcessor { 22 | private static final String LOGGER = LoggerFactory.class.getName() + ".getLogger(\"MyBatisPlus\")"; 23 | 24 | public MybatisSqlSessionFactoryBeanCBP() { 25 | } 26 | 27 | public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception { 28 | if (!ConfigurationFactory.getInstance().isPluginEnabled("spring_plugin")) { 29 | LoggerFactory.getLogger("MyBatisPlus").warn("MyBatisPlus Spring integration requires Spring plugin, which is currently disabled"); 30 | } else { 31 | // SqlMapReloader.HAS_MYBATIS_SPRING = true; 32 | cp.importPackage("org.apache.ibatis.builder.xml"); 33 | cp.importPackage("org.apache.ibatis.session"); 34 | cp.importPackage("org.springframework.beans"); 35 | cp.importPackage("org.springframework.beans.factory.support"); 36 | cp.importPackage("org.springframework.core.io"); 37 | cp.importPackage("java.util"); 38 | cp.importPackage("java.io"); 39 | this.createRegisterMapperLocationMethod(cp, ctClass); 40 | this.makeBeanFactoryAware(cp, ctClass); 41 | this.makeRepopulatingBean(cp, ctClass); 42 | CtMethod m = ctClass.getDeclaredMethod("buildSqlSessionFactory"); 43 | m.insertBefore("org.zeroturnaround.jrebel.mybatis.SqlMapReloader.HAS_MYBATIS_SPRING = true;"); 44 | m.addLocalVariable("__resource", cp.get("org.springframework.core.io.Resource")); 45 | m.insertBefore("{ __resource = null;}"); 46 | m.instrument(new ExprEditor() { 47 | private int count = 0; 48 | 49 | public void edit(NewExpr e) throws CannotCompileException { 50 | if ("org.apache.ibatis.builder.xml.XMLMapperBuilder".equals(e.getClassName())) { 51 | e.replace("{ $_ = $proceed($$); registerMapperLocationToReloader($2, __resource, $3);}"); 52 | } else if ("com.baomidou.mybatisplus.core.MybatisXMLMapperBuilder".equals(e.getClassName())) { 53 | e.replace("{ $_ = $proceed($$); registerMapperLocationToReloader($2, __resource, $3);}"); 54 | } else if ("com.baomidou.mybatisplus.core.MybatisXMLConfigBuilder".equals(e.getClassName())) { 55 | e.replace("{ $_ = $proceed($$); configuration = (com.baomidou.mybatisplus.core.MybatisConfiguration) $_.getConfiguration(); }"); 56 | } else if( "com.baomidou.mybatisplus.core.MybatisConfiguration".equals(e.getClassName())){ 57 | e.replace("{ $_ = $proceed($$); configuration = $_; }"); 58 | } 59 | } 60 | 61 | public void edit(MethodCall m) throws CannotCompileException { 62 | String methodName = m.getMethodName(); 63 | if ("getInputStream".equals(methodName) && "org.springframework.core.io.Resource".equals(m.getClassName())) { 64 | m.replace("{ __resource = $0; $_ = $proceed($$);}"); 65 | } 66 | } 67 | 68 | 69 | public void edit(FieldAccess f) throws CannotCompileException { 70 | if (f.getFieldName().equals("typeAliasesPackage") && ++this.count == 1) { 71 | f.replace("$_ = $proceed($$);if ($0 instanceof " + Constants.JrSqlSessionFactoryBeanClass + ") { " + Constants.SqlMapReloaderClass + " reloader = ((" + Constants.JrConfigurationClass + ") configuration).getReloader(); reloader.registerSqlSessionFactoryBean((" + Constants.JrSqlSessionFactoryBeanClass + ")$0);}"); 72 | } 73 | 74 | } 75 | }); 76 | } 77 | } 78 | 79 | private void createRegisterMapperLocationMethod(ClassPool cp, CtClass ctClass) throws CannotCompileException { 80 | ctClass.addMethod(CtNewMethod.make("public void registerMapperLocationToReloader(Configuration configuration, Resource mapperLocation, String name) { if (configuration instanceof " + Constants.JrConfigurationClass + ") { " + Constants.SqlMapReloaderClass + " reloader = ((" + Constants.JrConfigurationClass + ") configuration).getReloader(); if (reloader != null && mapperLocation != null) { java.net.URL resourceUrl = null; try { resourceUrl = mapperLocation.getURL(); } catch (Exception e) { " + LOGGER + ".error(\"Unable to get URL from resource. class=\" + mapperLocation.getClass().getName() + \", \" + \"description='\" + mapperLocation.getDescription() + \"'\", e); } if (resourceUrl != null) { reloader.addMapping(resourceUrl, name); } } }}", ctClass)); 81 | } 82 | 83 | private void makeRepopulatingBean(ClassPool cp, CtClass ctClass) throws NotFoundException, CannotCompileException { 84 | ctClass.addInterface(cp.get("org.springframework.beans.JrRepopulatingBean")); 85 | ctClass.addInterface(cp.get("org.zeroturnaround.jrebel.mybatis.JrSqlSessionFactoryBean")); 86 | ctClass.addField(CtField.make("private volatile String __name;", ctClass)); 87 | ctClass.addField(CtField.make("private volatile RootBeanDefinition __mbd;", ctClass)); 88 | ctClass.addField(CtField.make("private volatile BeanWrapper __bw;", ctClass)); 89 | ctClass.addMethod(CtNewMethod.make("public void setPopulateSources(String beanName, AbstractBeanDefinition mbd, BeanWrapper bw) { __name = beanName; __bw = bw; if (mbd instanceof RootBeanDefinition) { __mbd = (RootBeanDefinition)mbd; } else if (mbd != null) { " + LOGGER + ".info(\"setPopulateSources wrong mbd type: \" + mbd.getClass() + \" (not reloadable)\"); }}", ctClass)); 90 | boolean useInputStream = JavassistUtil.hasDeclaredConstructor(cp, "org.apache.ibatis.builder.xml.XMLMapperBuilder", new String[]{"java.io.InputStream", "org.apache.ibatis.session.Configuration", "java.lang.String", "java.util.Map"}); 91 | ctClass.addMethod(CtNewMethod.make("private void addMapperLocationsToConfiguration(List mapperLocations, Configuration configuration) { Iterator it = mapperLocations.iterator(); while (it.hasNext()) { Resource mapperLocation = (Resource)it.next(); if (mapperLocation == null) { continue; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(" + (useInputStream ? "mapperLocation.getInputStream()" : "new InputStreamReader(mapperLocation.getInputStream())") + ",configuration, mapperLocation.toString(), configuration.getSqlFragments()); registerMapperLocationToReloader(configuration, mapperLocation, mapperLocation.toString()); xmlMapperBuilder.parse(); " + LOGGER + ".info(\"Successfully added: \" + mapperLocation); } catch (Exception e) { " + LOGGER + ".error(\"Can't parse mapping: \" + mapperLocation, e); } }}", ctClass)); 92 | boolean reRegisterAlias = false; 93 | 94 | String superType; 95 | try { 96 | ctClass.getDeclaredField("typeAliasesPackage"); 97 | reRegisterAlias = true; 98 | ctClass.getDeclaredField("typeAliasesSuperType"); 99 | superType = ", typeAliasesSuperType == null ? Object.class : typeAliasesSuperType"; 100 | } catch (NotFoundException var7) { 101 | superType = ""; 102 | } 103 | 104 | if (reRegisterAlias) { 105 | ctClass.addMethod(CtNewMethod.make("private void reRegisterAliases(Configuration configuration) { if (org.springframework.util.StringUtils.hasLength(this.typeAliasesPackage)) { String[] typeAliasPackageArray = org.springframework.util.StringUtils.tokenizeToStringArray(this.typeAliasesPackage, org.springframework.context.ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for(int i = 0; i < typeAliasPackageArray.length; i++) { String packageToScan = typeAliasPackageArray[i]; configuration.getTypeAliasRegistry().registerAliases(packageToScan " + superType + "); } }}", ctClass)); 106 | } else { 107 | LoggerFactory.getLogger("MyBatis").warn("Not ReRegistering Aliases, assuming this is a version 1.0.0 of mybatis-spring."); 108 | ctClass.addMethod(CtNewMethod.make("private void reRegisterAliases(Configuration configuration) {}", ctClass)); 109 | } 110 | 111 | ctClass.addMethod(CtNewMethod.make("private void findAndAddNewMapperLocations(Resource[] mapperLocations, Resource[] previousMapperLocations, Configuration configuration) { if (mapperLocations == null) { return; } List existing = previousMapperLocations != null ? Arrays.asList(previousMapperLocations) : Collections.emptyList(); List newList = new ArrayList(1); for (int i = 0; i < mapperLocations.length; i++) { if (!existing.contains(mapperLocations[i])) { newList.add(mapperLocations[i]); } } if (newList.isEmpty()) { return; } reRegisterAliases(configuration); " + LOGGER + ".info(\"New mapperLocations discovered: \" + newList); addMapperLocationsToConfiguration(newList, configuration);}", ctClass)); 112 | ctClass.addMethod(CtNewMethod.make("public void reloadProperties(Configuration configuration) { try { if (this.jrBeanFactory != null && __name != null && __mbd != null && __bw != null) { Resource[] previousMapperLocations = this.mapperLocations; this.jrBeanFactory.jrPopulateBean(__name, __mbd, __bw); findAndAddNewMapperLocations(this.mapperLocations, previousMapperLocations, configuration); } } catch (Exception e) { " + LOGGER + ".error(\"Failed to reinject properties to SqlSessionFactoryBean: '\" + __name + \"'\", e); }}", ctClass)); 113 | } 114 | 115 | private void makeBeanFactoryAware(ClassPool cp, CtClass ctClass) throws NotFoundException, CannotCompileException { 116 | cp.importPackage("org.springframework.beans.factory"); 117 | cp.importPackage("org.springframework.beans.JrDefaultListableBeanFactory"); 118 | ctClass.addInterface(cp.get("org.springframework.beans.factory.BeanFactoryAware")); 119 | ctClass.addField(CtField.make("private volatile JrDefaultListableBeanFactory jrBeanFactory;", ctClass)); 120 | ctClass.addMethod(CtNewMethod.make("public void setBeanFactory(BeanFactory beanFactory) { if (beanFactory instanceof JrDefaultListableBeanFactory) { this.jrBeanFactory = (JrDefaultListableBeanFactory)beanFactory; }}", ctClass)); 121 | } 122 | } -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/StrictMapCBP.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | import org.zeroturnaround.bundled.javassist.CannotCompileException; 4 | import org.zeroturnaround.bundled.javassist.ClassPool; 5 | import org.zeroturnaround.bundled.javassist.CtClass; 6 | import org.zeroturnaround.bundled.javassist.expr.ExprEditor; 7 | import org.zeroturnaround.bundled.javassist.expr.MethodCall; 8 | import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor; 9 | 10 | /** 11 | * Process MybatisConfiguration$StrictMap class. 12 | * 13 | * @author suchu 14 | */ 15 | public class StrictMapCBP extends JavassistClassBytecodeProcessor { 16 | 17 | private boolean proceed = false; 18 | 19 | public StrictMapCBP() { 20 | } 21 | 22 | @Override 23 | public void process(ClassPool classPool, ClassLoader classLoader, CtClass ctClass) throws Exception { 24 | if (!proceed) { 25 | CtClass ambiguityClass = classPool.getOrNull("com.baomidou.mybatisplus.core.MybatisConfiguration$StrictMap$Ambiguity"); 26 | ctClass.getDeclaredMethod("put").instrument(new ExprEditor() { 27 | public void edit(MethodCall m) throws CannotCompileException { 28 | if ("containsKey".equals(m.getMethodName())) { 29 | m.replace("{ if (" + Constants.SqlMapReloaderClass + ".isReloading()) $_ = false; else $_ = $proceed($$);}"); 30 | } else if ("get".equals(m.getMethodName())) { 31 | //Before mybatis-plus version 3.5.7 32 | if (null != ambiguityClass) { 33 | m.replace("{ $_ = $proceed($$); if (" + Constants.SqlMapReloaderClass + ".isReloading() && !($_ instanceof com.baomidou.mybatisplus.core.MybatisConfiguration$StrictMap$Ambiguity)) $_ = null;}"); 34 | } else { 35 | m.replace("{ $_ = $proceed($$); if (" + Constants.SqlMapReloaderClass + ".isReloading() && !($_ ==AMBIGUITY_INSTANCE)) $_ = null;}"); 36 | } 37 | } 38 | } 39 | }); 40 | proceed = true; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/online/githuboy/jrebel/mybatisplus/cbp/XMLMapperBuilderCBP.java: -------------------------------------------------------------------------------- 1 | package online.githuboy.jrebel.mybatisplus.cbp; 2 | 3 | import org.zeroturnaround.bundled.javassist.*; 4 | import org.zeroturnaround.bundled.javassist.expr.ExprEditor; 5 | import org.zeroturnaround.bundled.javassist.expr.MethodCall; 6 | import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor; 7 | 8 | /** 9 | * XMLMapperBuilder class hook 10 | * 11 | * @author suchu 12 | * @since 2019/6/26 11:25 13 | */ 14 | public class XMLMapperBuilderCBP extends JavassistClassBytecodeProcessor { 15 | 16 | @Override 17 | public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception { 18 | ctClass.addField(CtField.make("private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(org.apache.ibatis.builder.xml.XMLMapperBuilder.class);", ctClass)); 19 | addClearMethod(ctClass); 20 | hookConfigurationElementMethod(ctClass); 21 | } 22 | 23 | private void hookConfigurationElementMethod(CtClass ctClass) throws NotFoundException, CannotCompileException { 24 | 25 | ctClass.getDeclaredMethod("configurationElement").instrument(new ExprEditor() { 26 | @Override 27 | public void edit(MethodCall m) throws CannotCompileException { 28 | if ("setCurrentNamespace".equals(m.getMethodName())) { 29 | m.replace("{$_=$proceed($$);this.clearInCompleteStatement();}"); 30 | } 31 | } 32 | }); 33 | } 34 | 35 | private void addClearMethod(CtClass ctClass) throws CannotCompileException { 36 | ctClass.addMethod(CtNewMethod.make(" public void clearInCompleteStatement() {\n" + 37 | " java.util.Collection incompleteStatements = this.configuration.getIncompleteStatements();\n" + 38 | " synchronized (incompleteStatements) {\n" + 39 | " java.util.Iterator iterator = incompleteStatements.iterator();\n" + 40 | " while (iterator.hasNext()) {\n" + 41 | " org.apache.ibatis.builder.xml.XMLStatementBuilder statementBuilder = (org.apache.ibatis.builder.xml.XMLStatementBuilder)iterator.next();\n" + 42 | " try {\n" + 43 | " java.lang.reflect.Field field = statementBuilder.getClass().getDeclaredField(\"builderAssistant\");\n" + 44 | " field.setAccessible(true);\n" + 45 | " org.apache.ibatis.builder.MapperBuilderAssistant tempBuilderAssistant = (org.apache.ibatis.builder.MapperBuilderAssistant) field.get(statementBuilder);\n" + 46 | " if (null != tempBuilderAssistant) {\n" + 47 | " if (tempBuilderAssistant.getCurrentNamespace().equals(builderAssistant.getCurrentNamespace())) {\n" + 48 | " logger.info(\"Cleaning {}'s incomplete statement\",builderAssistant.getCurrentNamespace());\n" + 49 | " iterator.remove();\n" + 50 | " }\n" + 51 | " }\n" + 52 | " } catch (Exception e) {\n" + 53 | " e.printStackTrace();\n" + 54 | " }\n" + 55 | " }\n" + 56 | " }\n" + 57 | " }", ctClass)); 58 | 59 | } 60 | } 61 | --------------------------------------------------------------------------------