看來斷點、單步調試還不夠硬核,根本沒多少人看,這次再來個硬核的。依然是由於apaas平台越來越流行了,如果apaas平台選擇了java語言作為平台內的業務代碼,那么不僅僅面臨着IDE外的斷點、單步調試,還面臨着為了實現預覽效果,需要將寫好的java源碼動態的裝載到spring容器中然后調用源碼內的某個方法。這篇文章主要就是實現spring/springboot運行時將源碼先編譯成class字節碼數組,然后字節碼數組再經過自定義類加載器變成Class對象,接着Class對象注冊到spring容器成為BeanDefinition,再接着直接獲取到對象,最后調用對象中指定方法。相信在網上其他地方已經找不到類似的實現了,畢竟像我這樣專門做這種別人沒有的原創的很少很少,大多都是轉載下別人的,或者寫些網上一大堆的知識點,哈哈!
個人認為分析復雜問題常見思維方式可以類比軟件領域的分治思想,將復雜問題分解成一個個小問題去解決。或者是使用減治思想,將復雜問題每次解決一小部分,留下的問題繼續解決一個小部分,這樣循環直到問題全部解決。所以軟件世界和現實世界確實是想通的,很多思想都可以啟迪我們的生活,所以我一直認為一個很會生活的程序員,一個把生活中出現的問題都解決的很好的程序員一定是個好程序員,表示很羡慕這種程序員。
那么我們先分解下這個復雜問題,我們要將一個java類的源碼直接加載到spring容器中調用,大致要經歷的過程如下:
1、先將java類源碼動態編譯成字節數組。這一點在java的tools.jar已經有工具可以實現,其實tools.jar工具包真的是一個很好的東西,往往你走投無路不知道怎么實現的功能在tools.jar都有工具,比如斷點調試,比如運行時編譯,呵呵
2、拿到動態編譯的字節碼數組后,就需要將字節碼加載到虛擬機,生成Class對象。這里應該不難,直接通過自定義一個類加載器就可以搞定
3、拿到Class對象后,再將Class轉成Spring的Bean模板對象BeanDefinition。這里可能需要一點spring的知識隨便看一點spring啟動那里的源碼就懂了。
4、使用spring的應用上下文對象ApplicationContext的getBean拿到真正的對象。這個應該用過spring的都知道
5、調用對象的指定方法。這里為了不需要用反射,一般生成的對象都繼承一個明確的基類或者實現一個明確的接口,這樣就可以由多肽機制,通過接口去接收實現類的引用,然后直接調用指定方法。
下面先看看動態編譯的實現,核心源碼如下
/** * 動態編譯java源碼類 * @author rongdi * @date 2021-01-06 */ public class DynamicCompiler { /** * 編譯指定java源代碼 * @param javaSrc java源代碼 * @return 返回類的全限定名和編譯后的class字節碼字節數組的映射 */ public static Map<String, byte[]> compile(String javaSrc) { Pattern pattern = Pattern.compile("public\\s+class\\s+(\\w+)"); Matcher matcher = pattern.matcher(javaSrc); if (matcher.find()) { return compile(matcher.group(1) + ".java", javaSrc); } return null; } /** * 編譯指定java源代碼 * @param javaName java文件名 * @param javaSrc java源碼內容 * @return 返回類的全限定名和編譯后的class字節碼字節數組的映射 */ public static Map<String, byte[]> compile(String javaName, String javaSrc) { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null); try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) { JavaFileObject javaFileObject = manager.makeStringSource(javaName, javaSrc); JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject)); if (task.call()) { return manager.getClassBytes(); } } catch (IOException e) { e.printStackTrace(); } return null; } }
然后就是自定義類加載器的實現了
/** * 自定義動態類加載器 * @author rongdi * @date 2021-01-06 */ public class DynamicClassLoader extends URLClassLoader { Map<String, byte[]> classBytes = new HashMap<String, byte[]>(); public DynamicClassLoader(Map<String, byte[]> classBytes) { super(new URL[0], DynamicClassLoader.class.getClassLoader()); this.classBytes.putAll(classBytes); } /** * 對外提供的工具方法,加載指定的java源碼,得到Class對象 * @param javaSrc java源碼 * @return */ public static Class<?> load(String javaSrc) throws ClassNotFoundException { /** * 先試用動態編譯工具,編譯java源碼,得到類的全限定名和class字節碼的字節數組信息 */ Map<String, byte[]> bytecode = DynamicCompiler.compile(javaSrc); if(bytecode != null) { /** * 傳入動態類加載器 */ DynamicClassLoader classLoader = new DynamicClassLoader(bytecode); /** * 加載得到Class對象 */ return classLoader.loadClass(bytecode.keySet().iterator().next()); } else { throw new ClassNotFoundException("can not found class"); } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] buf = classBytes.get(name); if (buf == null) { return super.findClass(name); } classBytes.remove(name); return defineClass(name, buf, 0, buf.length); } }
接下來就是將源碼編譯、加載、放入spring容器的工具了
package com.rdpaas.core.utils; import com.rdpaas.core.compiler.DynamicClassLoader; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; /** * 基於spring的應用上下文提供一些工具方法 * @author rongdi * @date 2021-02-06 */ public class ApplicationUtil { /** * 注冊java源碼代表的類到spring容器中 * @param applicationContext * @param src */ public static void register(ApplicationContext applicationContext, String src) throws ClassNotFoundException { register(applicationContext, null, src); } /** * 注冊java源碼代表的類到spring容器中 * @param applicationContext * @param beanName * @param src */ public static void register(ApplicationContext applicationContext, String beanName, String src) throws ClassNotFoundException { /** * 使用動態類加載器載入java源碼得到Class對象 */ Class<?> clazz = DynamicClassLoader.load(src); /** * 如果beanName傳null,則賦值類的全限定名 */ if(beanName == null) { beanName = clazz.getName(); } /** * 將applicationContext轉換為ConfigurableApplicationContext */ ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext; /** * 獲取bean工廠並轉換為DefaultListableBeanFactory */ DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); /** * 萬一已經有了這個BeanDefinition了,先remove掉,不然一次容器啟動沒法多次調用,這里千萬別用成 * defaultListableBeanFactory.destroySingleton()了,BeanDefinition的注冊只是放在了beanDefinitionMap中,還沒有 * 放入到singletonObjects這個map中,所以不能用destroySingleton(),這個是沒效果的 */ if (defaultListableBeanFactory.containsBeanDefinition(beanName)) { defaultListableBeanFactory.removeBeanDefinition(beanName); } /** * 使用spring的BeanDefinitionBuilder將Class對象轉成BeanDefinition */ BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz); /** * 以指定beanName注冊上面生成的BeanDefinition */ defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition()); } /** * 使用spring上下文拿到指定beanName的對象 */ public static <T> T getBean(ApplicationContext applicationContext, String beanName) { return (T) ((ConfigurableApplicationContext) applicationContext).getBeanFactory().getBean(beanName); } /** * 使用spring上下文拿到指定類型的對象 */ public static <T> T getBean(ApplicationContext applicationContext, Class<T> clazz) { return (T) ((ConfigurableApplicationContext) applicationContext).getBeanFactory().getBean(clazz); } }
再給出一些必要的測試類
package com.rdpaas.core.dao; import org.springframework.stereotype.Component; /** * 模擬一個簡單的dao實現 * @author rongdi * @date 2021-01-06 */ @Component public class TestDao { public String query(String msg) { return "msg:"+msg; } }
package com.rdpaas.core.service; import com.rdpaas.core.dao.TestDao; import org.springframework.beans.factory.annotation.Autowired; /** * 模擬一個簡單的service抽象類,其實也可以是接口,主要是為了把dao帶進去, * 所以就搞了個抽象類在這里 * @author rongdi * @date 2021-01-06 */ public abstract class TestService { @Autowired protected TestDao dao; public abstract String sayHello(String msg); }
最后就是測試的入口類了
package com.rdpaas.core.controller; import com.rdpaas.core.service.TestService; import com.rdpaas.core.utils.ApplicationUtil; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * 測試入口類 * @author rongdi * @date 2021-01-06 */ @Controller public class DemoController implements ApplicationContextAware { private static String javaSrc = "package com;" + "public class TestClass extends com.rdpaas.core.service.TestService{" + " public String sayHello(String msg) {" + " return \"我查到了數據,\"+dao.query(msg);" + " }" + "}"; private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * 測試接口,實際上就是完成動態編譯java源碼、加載字節碼變成Class,裝載Class到spring容器, * 獲取對象,調用對象的測試 * @return * @throws Exception */ @RequestMapping("/test") @ResponseBody public String test() throws Exception { /** * 美滋滋的注冊源碼到spring容器得到一個對象 * ApplicationUtil.register(applicationContext, javaSrc); */ ApplicationUtil.register(applicationContext,"testClass", javaSrc); /** * 從spring上下文中拿到指定beanName的對象 * 也可以 TestService testService = ApplicationUtil.getBean(applicationContext,TestService.class); */ TestService testService = ApplicationUtil.getBean(applicationContext,"testClass"); /** * 直接調用 */ return testService.sayHello("haha"); } }
想想應該有點激動了,使用這套代碼至少可以實現如下風騷的效果
1、開放一個動態執行代碼的入口,將這個代碼內容放在一個post接口里提交過去,然后直接執行返回結果
2、現在你有一個apaas平台,里面的業務邏輯使用java代碼實現,寫好保存后,直接放入spring容器,至於執行不執行看你自己業務了
3、結合上一篇文章的斷點調試,你現在已經可以實現在自己平台使用java代碼寫邏輯,並且支持斷點和單步調試你的java代碼了
好了,這次的主題又接近尾聲了,如果對我的文章感興趣或者需要詳細源碼,請支持一下我的同名微信公眾號,方便大家可以第一時間收到文章更新,同時也讓我有更大的動力繼續保持強勁的熱情,替大家解決一些網上搜索不到的問題,當然如果有啥想讓我研究的,也可以文章留言或者公眾號發送信息。如果有必要,我會花時間替大家研究研究。