自己动手实现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代码了
好了,这次的主题又接近尾声了,如果对我的文章感兴趣或者需要详细源码,请支持一下我的同名微信公众号,方便大家可以第一时间收到文章更新,同时也让我有更大的动力继续保持强劲的热情,替大家解决一些网上搜索不到的问题,当然如果有啥想让我研究的,也可以文章留言或者公众号发送信息。如果有必要,我会花时间替大家研究研究。
自己动手实现springboot运行时执行java源码(运行时编译、加载、注册bean、调用)的更多相关文章
- Springboot中mybatis执行逻辑源码分析
Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...
- 从SpringBoot源码分析 配置文件的加载原理和优先级
本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级 跟入源码之前,先提一个问题: SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数( ...
- JAVA 用命令提示符执行java找不到或者无法加载主类
使用cmd编译执行java文件时候,报错---找不到或者无法加载主类如下图 把红色部分去掉,再次编译执行如下解决问题 ,执行成功!!!!!! 2.当我们在eclipes中执行运行的时候 ggggggg ...
- jvm源码解读--01 jvm加载java/lang/object过程
现在做一下记录,这个看了两天,看的过程发现了很多c++的高级特性,没接触过,还得慢慢撸,禁止很慢 那么现在开始 吧 先打两个断点 java.c:351 JavaMain(void * _args) { ...
- 控制台执行java找不到或无法加载主类
- 在命令提示符下,运行Java程序时,提示"找不到或无法加载主类"
小白:在命令提示符下,运行Java程序时,提示"找不到或无法加载主类". 大神:运行Java程序的作用是让Java解释器装载,检验并运行字节码文件(.class).因此,在运行Ja ...
- 【JVM源码解析】模板解释器解释执行Java字节码指令(上)
本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...
- 【JDK命令行 一】手动编译Java源码与执行字节码命令合集(含外部依赖引用)
写作目标 记录常见的使用javac手动编译Java源码和java手动执行字节码的命令,一方面用于应对 Maven 和 Gradle 暂时无法使用的情况,临时生成class文件(使用自己的jar包):另 ...
- 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(6)-Unity 2.x依赖注入by运行时注入[附源码]
原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(6)-Unity 2.x依赖注入by运行时注入[附源码] Unity 2.x依赖注入(控制反转)IOC,对 ...
随机推荐
- MySQL常用的一些(就几个)聚合函数
聚合函数 (常用) 函数名称 描述 CONUT() 记数 SUM() 求和 AVG() 平均值 MAX() 最大值 MIN() 最小值 -- ================= 聚合函数 ====== ...
- (十)Python装饰器
装饰器:本质就是函数,功能是为其他函数添加附加功能. 两个原则: 1.不修改被修饰函数的源代码 2.不修改被修饰函数的调用方式 一个栗子 def test(): res = 0 for i in ra ...
- 【MySQL】MySQL知识图谱
MySQL 文章目录 MySQL 表 锁 索引 连接管理 事务 日志系统 简单记录 极客时间 - MySQL实战45讲 MySQL知识图谱 表 表 引擎选择 编码问题 表空间管理 字段设计 备份和恢复 ...
- 【Oracle】更改oracle中的用户名称
修改oracle中的用户名,要需要修改oracle基表中的相关内容, 1.查看user#, select user#,name from user$ s where s.name='用户修改前的'; ...
- 攻防世界 - Misc(一)
base64÷4: 1.下载附件,是一个.txt文件,打开是一串字符, 666C61677B453333423746443841334238343143413936393945444442413234 ...
- [Usaco2007 Jan]Balanced Lineup 飞盘比赛
题目描述 每天,农夫 John 的N(1 <= N <= 50,000)头牛总是按同一序列排队. 有一天, John 决定让一些牛们玩一场飞盘比赛. 他准备找一群在对列中为置连续的牛来进行 ...
- 24V转5V芯片,高输入电压LDO线性稳压器
PW6206系列是一个高精度,高输入电压低静态电流,高速,低功耗降线性稳压器具有高纹波抑制.输入电压高达40V,负载电流为在VOUT=5V和VIN=7V时高达300mA.该设备采用BCD工艺制造.PW ...
- EntityFramework Core如何映射动态模型?
前言 本文我们来探讨下映射动态模型的几种方式,相信一部分童鞋项目有这样的需求,比如每天/每小时等生成一张表,此种动态模型映射非常常见,经我摸索,这里给出每一步详细思路,希望能帮助到没有任何头绪的童鞋, ...
- Docker 如何动态修改容器端口映射
Docker端口映射往往是Docker Run命令时通过-p将容器内部端口映射到宿主机的指定端口上,一般来说容器的端口所对应的端口是提前确定需要映射的.但是有些情况下不得不需要临时映射端口,例如Doc ...
- 阿里云VOD(二)
一.准备工作 1.设置不转码 测试之前设置默认"不转码",以节省开发成本 2.找到子账户的AccessKey ID 3.给子账户添加授权 AliyunVODFullAccess 4 ...