自己動手實現springboot執行時執行java原始碼(執行時編譯、載入、註冊bean、呼叫)

碼小D發表於2021-02-08

  看來斷點、單步除錯還不夠硬核,根本沒多少人看,這次再來個硬核的。依然是由於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程式碼了

  好了,這次的主題又接近尾聲了,如果對我的文章感興趣或者需要詳細原始碼,請支援一下我的同名微信公眾號,方便大家可以第一時間收到文章更新,同時也讓我有更大的動力繼續保持強勁的熱情,替大家解決一些網上搜尋不到的問題,當然如果有啥想讓我研究的,也可以文章留言或者公眾號傳送資訊。如果有必要,我會花時間替大家研究研究。

 

相關文章