自己動手實現springboot運行時執行java源碼(運行時編譯、加載、注冊bean、調用)


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

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM