前言
这篇文章给大家分享一个热部署相关的知识点。
这可是一个好东西啊,当年在学习学 JSP 的时候,哐哐哐一顿操作,发现服务没重启,我在 JSP 里面写的东西就直接生效了。
当场我就是一个大大的震惊,而旁边教我的人只是说着一些什么热部署,字节码,classloader 重写之类的我根本就听不懂的话。
但是听不懂不重要,我会使用就行了。随即就是一顿疯狂练习,把这玩意用的炉火纯青,什么 pageContext 上下文,redirect 重定向,forward 转发,九大内置对象,完整的生命周期啥的,手到擒来。
可惜这玩意不争气,在前后端分离的大潮下,自己慢慢的没落了。搞得我空有一身 JSP 本领,无处施展。
后来啊,
后来,我就自己悄悄的在程序里面基于它做了很多后门嘛,但是我发誓,我没有一点点其他的想法,真的是出于好心,出于责任心,想要加快线上问题响应效率,就在里面各种线上骚操作,刀尖跳舞,可好用了...
好了,说回热部署。
首先我们明确下什么叫做热部署,热部署是在不重启 java 虚拟机的前提下,自动更新 class 的行为,从而更新整个运行时的逻辑。
在 java 开发领域,热部署一直是一个难以解决的问题,java 虚拟机理论上只能实现方法体的修改热部署,对于整个类结构的更改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。
OSGI
其实 java 业界有一些解决方案,比如 osgi 架构,这玩意时间比较长了,但一直没火起来。
osgi 架构的出现,可以让 java 系统变成模块化的形式,让模块重启成为可能。从一定程度上也算是个热部署的方案。
可惜这玩意以前开发起来就觉得很反人类,配置文件一大堆不说,学习成本也很大。
和 spring 结合起来,居然是一个模块一个 spring 上下文体系。并且如果模块之间有调用关系的话,重启相关的模块会让应用出现短暂的功能性休克,也就说,整个热启动过程不平滑。
这项技术现在估计很多小伙伴都没听说过,目前也渐渐的退出历史舞台,用的企业估计很少。
ASM
ASM 是一款修改字节码的框架,同类型的框架还有 Cglib。
这些框架能加载一个 class 信息,用户可以按照自己的需求增强修改这些信息,最后输出成一个新的 class。
具体实现过程,这里就不展开了。大家可以搜索下,相关技术实现文章不少。
但单纯修改字节码一般要和其他技术结合起来,单靠这个也无法完成热更新,虽然 ASM 类的框架能够修改类,但是这些 ASM 的修改逻辑也是用 java 写的,这段代码也需要执行的。
如果你把 ASM 的代码写在 java 里,也无法实现从外部来热更新。
Javaagent&Attach API结合ASM
这就是上面一段说的 ASM 要结合其他技术才能实现热更新的方案,也是目前很多开源框架采用的方案。
比如大名鼎鼎的 Arthas,就是利用 javaagent 通过 Attach API 运行时加载目标 Java 程序,最终利用 Instrumention API 或者 ASM 增强 class,来实现代码跟踪,以及代码热修改的特性。
但我认为用 Arthas 来完成线上代码的热更新,只能用于一些很紧急的场景。不能替代日常业务逻辑修改。而且操作起来也挺复杂。
你需要先修改 java 代码,然后把 java 代码放到服务器上,在 arthas 里查找这个类的类加载器的 hashcode,然后利用 arthas 提供的命令编译 java 代码输出成新的 class 文件,最用利用 redefine 命令进行热更新。
试想下,如果大量逻辑的更改。这得有多麻烦。
所以更多的是利用 arthas 对线上应用进行诊断,追踪,热更改代码其实就是 arthas 众多功能中其中一个,并不是主要功能。
换一种思路
以上方案都是基于修改 class 本身,然后让 JVM 的类加载器重新加载来实现的。
那么有没有更好的方案呢?
其实 java 代码可以运行一些脚本的,jdk 本身就支持调用脚本,从 JDK 1.6 开始,java 就支持 JSR223,可以用一致的形式在 JVM 上执行一些脚本语言,而且可实时编译,运行的效率和 java 不相上下的。
有的同学看到这里可能会拍砖了:利用脚本只能更改部分逻辑,不可能把所有的逻辑都用脚本写吧,你这篇文章探讨的不是“有没有可能所有的逻辑都可热更新”么?
别急,首先我们来确定一个事情。
你的 java 应用系统需不需要所有的逻辑都是可以热更新的?
很多代码都是大致固定不变的,比如 util 类,一些 vo 的定义也不大变更的,一些固定的业务也不需要热更新需求的。只有一些经常变更的决策部分,可能需要热更新。
那么我们只要把需要经常变的部分用脚本来定义不就可以了么。
业界有没有类似的开源框架呢?
还真有,而且是高 star 的热门开源项目,能够帮你做到用脚本进行热更新业务。
开源编排规则引擎
可能有小伙伴又要说了:你不是介绍 java 代码热部署么,怎么话题转到规则引擎上去了?
这里要说下,规则引擎的一大特性就是把决策部分逻辑剥离到外面,能够实现逻辑的变动快速热变更。
而这次介绍的规则引擎框架则更为强大,除了能剥离逻辑,还能解耦系统,让你的所有的逻辑块均可随意变更。理论上能实现所有的逻辑都可变更,不是部分哦。
这就是业界现在很火的编排式规则引擎框架:LiteFlow。
LiteFlow 的理念很简单,就是把系统中的各个逻辑切分成一小块一小块的,称之为组件,这些组件可以由 java 代码来写,也可以用脚本来写。然后一个完整业务就是把各个组件组搭一起,形成一个完整业务链。
这种模式的好处就是,不需要热更新的部分可以用java组件来写,需要经常变的部分可以用脚本来写。所有的组件均可混搭成为一个业务。
如何编排这些组件,LiteFlow 独创了 ELF 语法,拥有非常好上手的编排语法。程序员的话,十分钟就可以上手。上图粉色部分就是最简单的一种串联形式。
业务链路中组件可实时更换,也可实时增加,形成一个新的业务链。同时定义好的组件也可复用在其他的链路中。
LiteFlow 的脚本方案也是利用 JSR223 来实现的,目前已经实现的脚本有三种:
为什么说利用 LiteFlow 编排引擎框架,你的所有逻辑都是可以变更的呢?
因为你完全可以把所有的逻辑都用脚本组件来实现,LiteFlow 提供了非常强大的脚本支持,完全和 Java 底层打通,你可以在脚本中 import java 的类,也可以调用 java 的类方法,甚至于可以在脚本中去定义方法,定义类,一切写法和java中完全一样。
更夸张的是,LiteFlow 允许你在脚本中调用 spring 上下文的 bean,你可以在脚本中调用 DAO 取数据,可以在脚本中发送 rpc 给其他微服务。
只要你愿意,你可以一行 java 业务代码不写,完全把业务搬到脚本组件中去。
而且连逻辑块的顺序你也可以随意变动,因为 LiteFlow 的编排规则和脚本均可实现热变更。
LiteFlow 为经常用的存储中间件也提供了原生支持:
LiteFlow 支持所有的关系型数据库,另外 zk,etcd,nacos 均可支持,还提供了额外的扩展接口,供你自己扩展成其他的存储方式。
有想过么,你所有的逻辑和规则编排语法,都是存在于系统之外的。只要更改其脚本和逻辑,你所有节点的系统不需要做任何事,实时的进行热变更。
而这一切,LiteFlow 做到了非常平滑,所谓平滑的意思是,不用担心在热变更的时候你的业务会受到任何的中断,也不会因为热变更造成正在执行的链路产生任何的异常。
LiteFlow 编排能力有多强大呢,简单的几个关键字就可以编排出超乎想象的效果:
最后放出 LiteFlow 的官网和仓库地址,如果你觉得这款开源框架对你的业务有帮助,可以深入了解一下。
项目官网:https://liteflow.yomahub.com
gitee托管仓库:https://gitee.com/dromara/liteFlow
github托管仓库:https://github.com/dromara/liteflow
JSR223
刚刚提到了 LiteFlow 这个支持多种脚本语言的规则引擎框架就是基于 JSR223 规范来做的。
那么 JSR223 是个啥玩意呢?
有了这个规范,我们就可以在 Java 的平台里,执行其他的语言,包括且不仅限于 jvm 发展出来的语言。
有的同学可能会说:在java项目里执行其他语言,这不吃饱了撑着么,java体系那么庞大,各种工具一应俱全,放着好好的java不写,还要去执行其他语言干嘛?
写 java 的都知道,java 是需要事先编译的。
如果你想要在运行中改变编译好的 class 信息,怎么办?
前面说了,如果你用字节码等技术栈,是需要很大的成本的。
所以要想在运行中很方便的改变业务逻辑,其实用 java 去执行其他的脚本语言是一个好办法。况且有的脚本语言有着比 java 更简洁的语法特性。
而由于 JSR223 规范的存在,在 java 平台中调用其他脚本语言,其实一点都不复杂,你无需关心每种语言的实际api。
JSR223 规范最初在 Java6 平台被提出,提供了一套标准的 API 为脚本语言的执行提供了内置支持。
也就是说,你只要熟悉这一套 API 就能执行大部分的脚本语言。
而且这套 API 的使用也是非常方便的,几个核心方法仔细看个 10 分钟就能明白如何使用。
来个最简单的例子:
//获得javascript的脚本引擎
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
//进行脚本编译
String script = "function process(){\n" +
"var a=10\n" +
"var b=3\n" +
"return a*b-c\n" +
"}\n" +
"process()";
CompiledScript compiledScript = ((Compilable) scriptEngine).compile(script);
//绑定java的参数
Bindings bindings = new SimpleBindings();
bindings.put("c", 5);
//执行并打印结果
Object result = compiledScript.eval(bindings);
System.out.println(result);
上述代码演示的是用 JSR223 API 去执行 javascript 语言。
值得一提的是,java 内置了 javascript 引擎,你无需引入任何第三方包依赖就可以获得这个引擎:
整个过程分 4 块,分别是:
-
获得引擎
-
脚本编译
-
绑定 java 参数
-
执行
在实际业务中,建议在系统启动的时候去编译脚本,然后把编译好的脚本对象 compiledScript 对象给缓存起来,因为编译过程相对比较耗时,运行时每次去编译是个糟糕的设计。
如果在运行中改变了脚本,只需要重新去编译这个脚本并缓存其编译后的对象即可。
你只需要掌握以上代码,那几乎就已经掌握了 JSR223 规范的使用了。是不是很简单?
如果你想换成 groovy 脚本语言,那你需要依赖第三方依赖
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-jsr223</artifactId>
<version>3.0.8</version>
</dependency>
然后在上述的代码里获得引擎这块换成groovy即可:
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("groovy");
如果你想换成 python,需要依赖第三方依赖:
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.3</version>
</dependency>
然后在上述的代码里获得引擎这块换成 python 即可:
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("python");
其实现在很多的语言在 java 平台都推出了自己的 java 三方执行依赖包,而且很多的包都支持了 JSR223 规范。
只要支持了 JSR223 规范的语言,都可以利用上述的代码来执行。
JSR223 规范的 API 可以支持 java 和其他语言的绑定和互通,一个 java 对象通过 bindings 对象也是可以传到脚本语言中的,在脚本语言中,你可以获得 java 的对象,来进行调用其方法和进行逻辑计算。
不过不同的语言调用操作符也许有所不同,比如 groovy,javascript 都是通过点操作符,和 java 很像,Lua 脚本的对 java 对象的操作符是冒号。
所以当你的项目支持相关的脚本语言之前,你先要熟悉下相关语言的语法。
最后,希望大家都能从 JSR223 规范中找到一些设计你们相关业务系统的灵感。
·············· END ··············