关于java:玩转-Java-动态编译秀了秀了~

5次阅读

共计 3600 个字符,预计需要花费 9 分钟才能阅读完成。

起源:https://zhenbianshu.github.io

问题

之前的文章从 Spring 的环境到 Spring Cloud 的配置中提到过,咱们在应用 Spring Cloud 进行动态化配置,它的实现步骤是先将动静配置通过 @Value 注入到一个动静配置 Bean,并将这个 Bean 用注解标记为 @RefreshScope,在配置变更后,这些动静配置 Bean 会被对立销毁。

之后 Spring Cloud 的 ContextRefresher 会将变更后的配置作为一个新的 Spring Environment 加载进 ApplicationContext,因为 Scoped Bean 都是 Lazy Init 的,它们会在下一次应用时被应用新的 Environment 从新创立。

这套动静配置加载流程在使咱们服务更加灵便的同时,也带来了很大的危险。首先从业务上,批改配置不像上线这么”重量级”,不必要找 QA 进行回归测试,这就有可能引发一系列奇怪的 Bug,而且长时间发现不了,另外,Spring Cloud 自身没有“fallback”机制,一旦配置的数据类型出了问题,就会导致服务不可用。

为此,我给 Spring Cloud 提了个 issue,但作者认为变动太大,不好改也不用改。

其实我也明确这个问题的窘境,每个人都得为本人要批改的配置负责,即便框架反对了 fallback,但将谬误吞掉,配置批改后不失效也没什么变动可能也并不合乎用户的冀望。所以,尽量让用户要批改的配置正确成为了新的指标。

基于这种需要,我增加了一个动静配置的校验器,但实现里一部分代码来自 github,所以本文在总结思路的同时,也帮忙我了解所有代码。

整体思路

因为框架层没法做太多事件,所以我的打算是将这些配置取出来,结构出一个独立的 Java 类,并在服务外新建一个 ApplicationContext 试图通过结构进去的 Java 类初始化一个 Spring Bean,如果这个 Spring Bean 初始化过程中报错了,阐明配置是有问题的。

动静编译

通过配置结构 Java 类

首先要通过 .properties 文件结构出一个 Java 类,但问题是在配置里咱们是不晓得这些配置将要被怎么应用的,不晓得它要被 Spring EL 如何解决,又将被转成什么类型。

这里我采纳的策略是给配置增加正文,正文里应用肯定的格局申明 EL 表达式和要生成的字段类型,当然这种实现有点 low,有人提议把这些信息放到配置项的 key 里,之后会再进行优化。

把各个字段解析实现后放到筹备到的类模板中,就生成了一个 Config.java 类字符串,之后就要将这个字符串编译成字节码并由 Spring 加载成 Bean。

JavaCompiler

因为 Config.java 是在运行时生成的,所以编译也只能在运行时了,万幸 Java 有提供 javax.util.JavaCompiler 类进行 Java 类的动静编译,省去了”写入文件 —— 命令行编译 —— 类加载 —— 清理文件”的简单流程。

JavaCompiler 的典型利用示例如下:

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 对象里。

委托模式

因为 JavaCompiler 的默认实现都是通过文件进行的,这不合乎我的冀望,我须要的是输出和输入都在内存进行,所以须要批改 JavaCompiler 的实现,JavaCompiler、JavaFileManager、JavaFileObject(Input/Output) 别离应用委托模式实现。其中 JavaFileManager 曾经有 ForwardingJavaFileManager 的实现,JavaFileObject 也有 SimpleJavaFileObject 的实现,咱们继承其实现后重写局部办法即可。

我参考的源码:https://github.com/trung/InMe…

Spring Bean 实例化

要将 Config 类实例化成 Bean,咱们能够在 xml 里预约义它,在编译完结后创立一个繁难的 FileSystemXmlApplicationContext 实例化这个 xml 内的 Bean。

类加载器

首先要让 Spring 可能加载到这些编译好的字节码,这就须要 ClassLoader 的配合。类加载器的默认实现不可能晓得去加载咱们内存里编译好的字节码,只好新加一个 ClassLoader,实现也很简略,继承 ClassLoader 抽象类,并实现 findClass 办法即可。

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 一下使其被创立。

最终的代码如下:

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");

小结

小我的项目实现的过程中,温习了很多常识,也尝试了业务代码中简直不会用到的设计模式,充斥了挑战性。

当然它当初还有配置不够不便、谬误提醒不够明确、没解决配置 namespace 等问题,留到前面缓缓优化吧~

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0