• 自己动手实现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代码了

      好了,这次的主题又接近尾声了,如果对我的文章感兴趣或者需要详细源码,请支持一下我的同名微信公众号,方便大家可以第一时间收到文章更新,同时也让我有更大的动力继续保持强劲的热情,替大家解决一些网上搜索不到的问题,当然如果有啥想让我研究的,也可以文章留言或者公众号发送信息。如果有必要,我会花时间替大家研究研究。

     
  • 相关阅读:
    Web测试与App测试的区别-总结篇
    Shell之基本用法
    Samba服务部署
    Linux基础(3)
    linux基础(2)
    linux基础(2)
    Linux基础(1)
    网络基础及网络协议
    操作系统简介
    计算机基础重要性
  • 原文地址:https://www.cnblogs.com/rongdi/p/14374481.html
Copyright © 2020-2023  润新知