自己动手实现springboot运行时执行java源码(运行时编译、加载、注册bean、调用)

标签: springboot 运行时 java | 发表时间:2021-05-16 10:10 | 作者:码小D
出处:https://juejin.cn/backend

  看来断点、单步调试还不够硬核,根本没多少人看,这次再来个硬核的。依然是由于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 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 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 classBytes = new HashMap();

    public DynamicClassLoader(Map 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 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 getBean(ApplicationContext applicationContext, String beanName) {
        return (T) ((ConfigurableApplicationContext) applicationContext).getBeanFactory().getBean(beanName);
    }

    /**
     * 使用spring上下文拿到指定类型的对象
     */
    public static  T getBean(ApplicationContext applicationContext, Class 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代码了

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

    
复制代码

相关 [springboot 运行时 java] 推荐:

自己动手实现springboot运行时执行java源码(运行时编译、加载、注册bean、调用)

- - 掘金 后端
  看来断点、单步调试还不够硬核,根本没多少人看,这次再来个硬核的. 依然是由于apaas平台越来越流行了,如果apaas平台选择了java语言作为平台内的业务代码,那么不仅仅面临着IDE外的断点、单步调试,还面临着为了实现预览效果,需要将写好的java源码动态的装载到spring容器中然后调用源码内的某个方法.

Java运行时的内存模型

- - CSDN博客编程语言推荐文章
每个线程单独的数据区(线程间不共享). 每个线程都有一片单独的内存区域,这里面包含:程序计数器(program counter register),JVM栈和本地方法栈(Native Method Stack). 当一个新的线程被创建的时候,这片内存就已经被分配出来了. 程序计数器:为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储.

SpringBoot-Metrics监控

- -
Metrics基本上是成熟公司里面必须做的一件事情,简单点来说就是对应用的监控,之前在一些技术不成熟的公司其实是不了解这种概念,因为业务跟技术是相关的. 当业务庞大起来,技术也会相对复杂起来,对这些复杂的系统进行监控就存在必要性了,特别是在soa化的系统中,完整一个软件的功能分布在各个系统中,针对这些功能进行监控就更必要了.

JAVA内存结构之运行时数据区域

- - Java - 编程语言 - ITeye博客
1       内存区域. 1.1              运行时数据区域. Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域. 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线锃的启动和结束而建立和销毁.

SpringBoot的事务管理

- - ImportNew
Springboot内部提供的事务管理器是根据autoconfigure来进行决定的. 比如当使用jpa的时候,也就是pom中加入了spring-boot-starter-data-jpa这个starter之后(之前我们分析过 springboot的自动化配置原理). Springboot会构造一个JpaTransactionManager这个事务管理器.

springboot aop日志记录

- - 编程语言 - ITeye博客
一、POM增加AOP JAR包. 三、SysAspect类. 注:@annotation(cn.com.hfai.controller.system.Logweb) 一定要指定Logweb类. 四、在Controller类的方法之上加上注解 @Logweb 即可. 注:这个只是打印在控制台上,若想放到数据库中,则需要增加操作数据库的业务代码.

springboot单元测试技术

- - 海思
整个软件交付过程中,单元测试阶段是一个能够最早发现问题,并且可以重复回归问题的阶段,在单元测试阶段做的测试越充分,软件质量就越能得到保证. 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-unit-test.

Azul发布开源工具jHiccup,为Java提供运行时响应时间分析

- - InfoQ cn
Azul System公司于12月13日 宣称发布了开源工具jHiccup,设计该工具的目的是对与应用程序底层运行平台相关的暂停和延迟(或“hiccups”)做出度量. 新工具的功能与Azul的JitterMeter有部分重叠,但它为创建图形化的输出增加了基于Excel的工具. 另外它还计划增加基于Java用于生成图表的工具.

K8S部署SpringBoot应用_都超的博客-CSDN博客_k8s springboot

- -
K8S环境机器做部署用,推荐一主双从. Docker Harbor私有仓库,准备完成后在需要使用仓库的机器docker login. 开发机器需要Docker环境,build及push使用. 一、构建基本Springboot工程,本例所用版本及结构如下图. 创建测试代码,简单打印几行log. .

springboot集成shiro 实现权限控制

- - CSDN博客编程语言推荐文章
apache shiro 是一个轻量级的身份验证与授权框架,与spring security 相比较,简单易用,灵活性高,springboot本身是提供了对security的支持,毕竟是自家的东西. springboot暂时没有集成shiro,这得自己配. 本文实现从数据库读取用户信息,获取当前用户的权限或角色,通过配置文件过滤用户的角色或权限.