玩转 Java 动态编译,太秀了~!

沙海 2021年7月4日12:58:26Java评论41字数 4583阅读15分16秒阅读模式
摘要

智能摘要

智能摘要文章源自JAVA秀-https://www.javaxiu.com/36542.html

它们会在下一次使用时被使用新的Environment重新创建。一旦配置的数据类型出了问题,就会导致服务不可用。试图通过构造出来的Java类初始化一个Spring Bean,如果这个Spring Bean初始化过程中报错了,说明配置是有问题的。Java类的动态编译,省去了"写入文件——命令行编译——类加载——清理文件"的复杂流程。MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。文章源自JAVA秀-https://www.javaxiu.com/36542.html

原文约 4503 | 图片 8 | 建议阅读 10 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!

点击关注 ? 芋道源码 文章源自JAVA秀-https://www.javaxiu.com/36542.html

收录于话题文章源自JAVA秀-https://www.javaxiu.com/36542.html

#芋道源码文章源自JAVA秀-https://www.javaxiu.com/36542.html

170个文章源自JAVA秀-https://www.javaxiu.com/36542.html

点击上方“芋道源码”,选择“设为星标文章源自JAVA秀-https://www.javaxiu.com/36542.html

管她前浪,还是后浪?文章源自JAVA秀-https://www.javaxiu.com/36542.html

能浪的浪,才是好浪!文章源自JAVA秀-https://www.javaxiu.com/36542.html

每天 8:55 更新文章,每天掉亿点点头发...文章源自JAVA秀-https://www.javaxiu.com/36542.html

文章源自JAVA秀-https://www.javaxiu.com/36542.html

源码精品专栏文章源自JAVA秀-https://www.javaxiu.com/36542.html

 文章源自JAVA秀-https://www.javaxiu.com/36542.html

文章源自JAVA秀-https://www.javaxiu.com/36542.html

来源:zhenbianshu.github.io文章源自JAVA秀-https://www.javaxiu.com/36542.html

  • 背景文章源自JAVA秀-https://www.javaxiu.com/36542.html

  • 动态编译文章源自JAVA秀-https://www.javaxiu.com/36542.html

  • Spring Bean 实例化文章源自JAVA秀-https://www.javaxiu.com/36542.html

  • 小结玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

背景

之前的文章 从 Spring 的环境到 Spring Cloud 的配置 中提到过我们在使用 Spring Cloud 进行动态化配置,它的实现步骤是先将动态配置通过 @Value 注入到一个动态配置 Bean,并将这个 Bean 用注解标记为 @RefreshScope,在配置变更后,这些动态配置 Bean 会被统一销毁,之后 Spring Cloud 的 ContextRefresher 会将变更后的配置作为一个新的 Spring Environment 加载进 ApplicationContext,由于 Scoped Bean 都是 Lazy Init 的,它们会在下一次使用时被使用新的 Environment 重新创建。文章源自JAVA秀-https://www.javaxiu.com/36542.html

这套动态配置加载流程在使我们服务更加灵活的同时,也带来了很大的风险。首先从业务上,修改配置不像上线这么”重量级”,不必要找 QA 进行回归测试,这就有可能引发一系列奇怪的 Bug,而且长时间发现不了,另外,Spring Cloud 本身没有 “fallback” 机制,一旦配置的数据类型出了问题,就会导致服务不可用。为此,我给 Spring Cloud 提了个 issue scope refreshed but new properties invalid leads to application unavailable ,但作者认为变动太大,不好改也不必改。文章源自JAVA秀-https://www.javaxiu.com/36542.html

其实我也明白这个问题的困境,每个人都得为自己要修改的配置负责,即使框架支持了 fallback,但将错误吞掉,配置修改后不生效也没什么变化可能也并不符合用户的期望。所以,尽量让用户要修改的配置正确成为了新的目标。文章源自JAVA秀-https://www.javaxiu.com/36542.html

基于这种需求,我添加了一个动态配置的校验器,但实现里一部分代码来自 github,所以本文在总结思路的同时,也帮助我理解所有代码。文章源自JAVA秀-https://www.javaxiu.com/36542.html

由于框架层没法做太多事情,所以我的计划是将这些配置取出来,构造出一个独立的 Java 类,并在服务外新建一个 ApplicationContext 试图通过构造出来的 Java 类初始化一个 Spring Bean,如果这个 Spring Bean 初始化过程中报错了,说明配置是有问题的。文章源自JAVA秀-https://www.javaxiu.com/36542.html

动态编译

首先要通过 .properties 文件构造出一个 Java 类,但问题是在配置里我们是不知道这些配置将要被怎么使用的,不知道它要被 Spring EL 如何处理,又将被转成什么类型。文章源自JAVA秀-https://www.javaxiu.com/36542.html

这里我采用的策略是给配置添加注释,注释里使用一定的格式声明 EL 表达式和要生成的字段类型,当然这种实现有点 low,有人提议把这些信息放到配置项的 key 里,之后会再进行优化。文章源自JAVA秀-https://www.javaxiu.com/36542.html

把各个字段解析完成后放到准备到的类模板中,就生成了一个 Config.java 类字符串,之后就要将这个字符串编译成字节码并由 Spring 加载成 Bean。文章源自JAVA秀-https://www.javaxiu.com/36542.html

由于 Config.java 是在运行时生成的,所以编译也只能在运行时了,万幸 Java 有提供 javax.util.JavaCompiler 类进行 Java 类的动态编译,省去了”写入文件 —— 命令行编译 —— 类加载 —— 清理文件” 的复杂流程。文章源自JAVA秀-https://www.javaxiu.com/36542.html

JavaCompiler 的典型应用示例如下:文章源自JAVA秀-https://www.javaxiu.com/36542.html

        JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();        JavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);        CompilationTask task = javaCompiler.getTask(out, fileManager, diagnosticListener, options, classes, compilationUnits);        task.call();        FileObject outputFile = fileManager.getFileForOutput(null, null, null, null);        outputFile.getCharContent(true);

流程如下图:JavaCompiler 通过 JavaFileManager 管理输入和输出文件,使用时通过 getTask() 方法提交一个异步 CompilationTask 进行代码编译,代码编译时,JavaCompiler 通过 getCharContent() 从传入的 compilationUnits 获取到 .java 文件内容,把编译后的结果调用 CompiledByteCode 的 openOutputStream() 方法写到 CompiledByteCode 对象里。文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

由于 JavaCompiler 的默认实现都是通过文件进行的,这不符合我的期望,我需要的是输入和输出都在内存进行,所以需要修改 JavaCompiler 的实现,JavaCompiler、JavaFileManager、JavaFileObject(Input/Output) 分别使用委托模式实现。其中 JavaFileManager 已经有 ForwardingJavaFileManager 的实现,JavaFileObject 也有 SimpleJavaFileObject 的实现,我们继承其实现后重写部分方法即可。文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

我参考的源码:GitHub-trung/InMemoryJavaCompiler文章源自JAVA秀-https://www.javaxiu.com/36542.html

Spring Bean 实例化

要将 Config 类实例化成 Bean,我们可以在 xml 里预定义它,在编译结束后创建一个简易的 FileSystemXmlApplicationContext 实例化这个 xml 内的 Bean。文章源自JAVA秀-https://www.javaxiu.com/36542.html

首先要让 Spring 能够加载到这些编译好的字节码,这就需要 ClassLoader 的配合。类加载器的默认实现不可能知道去加载我们内存里编译好的字节码,只好新加一个 ClassLoader,实现也很简单,继承 ClassLoader 抽象类,并实现 findClass 方法即可。文章源自JAVA秀-https://www.javaxiu.com/36542.html

class MemoryClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException {     // 在 CompiledByteCode 类里将编译后的字节码放到 classLoader 的 classBytes 字段内。  byte[] buf = classBytes.get(name);  if (buf == null) {   return super.findClass(name);  }  return defineClass(name, buf, 0, buf.length); }}

由于 Config Bean 的初始化依赖动态配置,我们还要把这些配置也添加到 Spring 环境内,我们知道 Spring 环境配置是由多个 PropertySource 构成的,向里面添加一个实现即可。然后就可以调用 application 的 refresh() 方法初始化上下文了,另外 Config Bean 被设置为懒加载了,不要忘记 get 一下使其被创建。文章源自JAVA秀-https://www.javaxiu.com/36542.html

最终的代码如下:文章源自JAVA秀-https://www.javaxiu.com/36542.html

        FileSystemXmlApplicationContext applicationContext = new FileSystemXmlApplicationContext();        applicationContext.setClassLoader(memoryClassLoader);        applicationContext.setConfigLocation("classpath*:/test.xml");        Map<String, Object> propertyMap = buildDynamicPropertyMap();        MapPropertySource mapPropertySource = new MapPropertySource("validate_source", propertyMap);        applicationContext.getEnvironment().getPropertySources().addFirst(mapPropertySource);        applicationContext.refresh();        applicationContext.getBean("config");

小结

小项目完成的过程中,复习了很多知识,也尝试了业务代码中几乎不会用到的设计模式,充满了挑战性。文章源自JAVA秀-https://www.javaxiu.com/36542.html

当然它现在还有配置不够方便、错误提示不够明确、没解决配置 namespace 等问题,留到后面慢慢优化吧~文章源自JAVA秀-https://www.javaxiu.com/36542.html

- END -文章源自JAVA秀-https://www.javaxiu.com/36542.html

欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

已在知识星球更新源码解析如下:文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

玩转 Java 动态编译,太秀了~!文章源自JAVA秀-https://www.javaxiu.com/36542.html

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。文章源自JAVA秀-https://www.javaxiu.com/36542.html

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。文章源自JAVA秀-https://www.javaxiu.com/36542.html

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。文章源自JAVA秀-https://www.javaxiu.com/36542.html

文章源自JAVA秀-https://www.javaxiu.com/36542.html

文章有帮助的话,在看,转发吧。谢谢支持哟 (*^__^*)
文章源自JAVA秀-https://www.javaxiu.com/36542.html

阅读原文文章源自JAVA秀-https://www.javaxiu.com/36542.html

继续阅读
速蛙云 - 极致体验,强烈推荐!!!购买套餐就免费送各大视频网站会员!快速稳定、独家福利社、流媒体稳定解锁!速度快,全球上网、视频、游戏加速、独立IP均支持!基础套餐性价比很高!这里不多说,我一直正在使用,推荐购买:https://www.javaxiu.com/59919.html
weinxin
资源分享QQ群
本站是JAVA秀团队的技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定