共计 21532 个字符,预计需要花费 54 分钟才能阅读完成。
背景
当零碎遇到异常情况时,想要加上日志打印下要害信息,或者改下逻辑代码,但又不想重启,因为重启太麻烦太耗时且可能会 毁坏现场
,甚至有些场景在测试环境无奈模仿进去导致无奈复现。这时候就心愿能在不重启的状况下更新代码并立刻失效。
指标:对代码的增删改查,并且实时热更新。
增
:插入代码。删
:删除代码。改
:替换代码。查
:下载指定类的class
文件,如果是批改过的,那下载的就是批改后的class
文件。还原
:还原回批改前的代码。
概念
Instrumentation
应用 Insrumentation
,开发者能够构建一个独立于应用程序的代理程序(Agent
), 监测和帮助运行在 JVM 上的程序,甚至能够替换和批改某些类的定义。简略的来说开发者应用 Instrumentation
能够实现一种 虚拟机级别的 AOP
实现。Instrumentation
的最大作用,就是 类定义动静扭转和操作
。程序运行时,通过-javaagent
参数指定一个特定的 jar 文件
来启动 Instrumentation 的代理程序
。其实这个对很多人来说不生疏:xmind
, idea
永恒破解都应用了 agent
,Mockito
等Mock
类库也用到了 agent
,一些监控软件(如skywalking
)也用了。
在 java 中如何实现 Instrumentation?
1. 创立代理类
Java Agent
反对指标 JVM 启动时加载
,也反对在指标JVM 运行时加载
,这两种不同的加载模式会应用不同的入口函数。
如果须要在指标 JVM 启动的同时加载 Agent,那么能够抉择实现上面的办法:
public class MyAgent {
// 形式一
public static void premain(String options, Instrumentation instrumentation) {System.out.println("Java Agent premain");
instrumentation.addTransformer(new MyTransformer());
}
// 形式二
public static void premain(String options){System.out.println("Java Agent premain");
instrumentation.addTransformer(new MyTransformer());
}
}
如果心愿在指标 JVM 运行时加载 Agent,则须要实现上面的办法:
public class MyAgent {
// 形式一
public static void agentmain(String options, Instrumentation instrumentation) {System.out.println("Java Agent agentmain");
instrumentation.addTransformer(new MyTransformer());
}
// 形式二
public static void agentmain(String options){System.out.println("Java Agent agentmain");
instrumentation.addTransformer(new MyTransformer());
}
}
第一个参数 options
是通过命令行传递给 agent 的参数,第二个参数是用 JVM 提供的用于注册类转换器 (ClassTransformer
) 的 Instrumentation 实例。形式一
的优先级比 形式二
高,当 形式一
和形式二
两个办法同时存在时,形式二
办法将被疏忽。
转换产生在 premain
函数执行之后,main
函数执行之前,这时每装载一个类,transform
办法就会执行一次,所以在 transform
办法中,能够用 className.equals(myClassName)
来判断以后的类是否须要转换,return null
即示意以后字节 不须要转换
。
2. 创立类转换器
对 Java 类文件的操作,能够了解为对 java 二进制字节数组
的操作,批改原始的字节数组,返回批改后的字节数组。ClassFileTransformer
接口只有一个 transform
办法,参数传入包含该类的类加载器,类名,原字节码字节流等,返回被转换后的字节码字节流。
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//className 是以 / 宰割
if (!className.equals("com/xxx/AgentTester")){return null;}
// 业务操作
......
}
}
3. 创立 MANIFEST.MF 文件
在 resource
目录下新建 META-INF/MANIFEST.MF
文件,其中 Premain-Class
的值是蕴含包名的类名
Mainfest-Version: 1.0
Premain-Class: com.xxx.AgentTester
Can-Redefine-Classes: true
Can-Retransform-Classes: true
依据不同的加载形式,抉择配置 Premain-Class
或Agent-Class
。
4. 打包 & 运行
通过 Maven 的 org.apache.maven.plugins
和maven-assembly-plugin
插件生成 jar 文件,MANIFEST.MF
文件也能够通过以上插件主动生成。
启动命令上退出javaagent
,
java -javaagent:/ 文件门路 /myAgent.jar -jar myProgram.jar
例如:
java -javaagent:/usr/local/dev/MyAgent.jar -jar /usr/local/dev/MyApplication.jar
咱们还能够在地位门路上设置可选的 agent
参数。
java -javaagent:/usr/local/dev/MyAgent.jar=Hello -jar /usr/local/dev/MyApplication.jar
Javassist
Java 字节码以 二进制
的模式存储在 .class
文件中,每一个.class 文件蕴含一个 Java 类或接口。对于 java 字节码的解决,有很多类库,如 bcel
,asm
。不过这些都须要间接跟 虚拟机指令
打交道。如果你不想理解虚拟机指令,javassist
是一个不错的抉择。Javassist
能够在一个曾经编译好的类中增加新的办法,或者是批改已有的办法,并且不须要对字节码方面有深刻的理解。
以下例子,是批改 MyApp
类的 fun
办法,进入办法时先打印一行before
。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.MyApp");
CtMethod m = cc.getDeclaredMethod("fun");
m.insertBefore("{ System.out.println(\"before\"); }");
cc.writeFile();
Javassist 最重要的是 ClassPool、CtClass、CtMethod、CtField
这四个类:ClassPool
是 CtClass
对象的容器,一张保留 CtClass
信息的 HashTable
,key 为 类名
,value 为CtClass 对象
,它按需读取类文件来结构CtClass
对象,并且缓存 CtClass
对象以便当前应用。CtClass
是一个 class 文件在代码中的 形象表现形式
,对 CtClass 的批改相当于对 class 文件的批改。CtMethod
、CtField
对应的是类中的办法和属性。
配合后面的 Instrumentation
,能够在ClassFileTransformer
内对类代码做转换:
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(className.replace("/", "."));
CtMethod m = cc.getDeclaredMethod("fun");
m.insertBefore("{ System.out.println(\"before\"); }");
return cc.toBytecode();} catch (Exception e) {throw new RuntimeException(e);
}
}
因为 JAVA Agent
须要通过在命令行上加上 -javaagent
来执行,这就进步了组件 引入老本
,在做公共组件时, 应用简略
也是要思考的一个点。
Byte Buddy
Byte Buddy
提供了更简化的 API,如下的示例展示了如何生成一个简略的类,这个类是 Object
的子类,并且重写了 toString
办法,返回Hello World!
。
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(getClass().getClassLoader())
.getLoaded();
assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
在抉择字节码操作库时,还要思考库自身的 性能
,官网对库进行了性能测试,给出以下后果图:
从性能报告中能够看出,Byte Buddy
的次要侧重点在于 以起码的运行时
生成代码,须要留神的是,这些掂量 Java 代码性能的测试,都由 Java 虚拟机即时编译器优化过,如果你的代码只是偶然运行,没有失去虚拟机的优化,可能性能会有所偏差。
遗憾的是 Byte Buddy
不反对批改已有办法内的代码,例如 删除一行代码
这种需要是无奈通过 Byte Buddy
来实现的。然而 Byte Buddy
在我的项目运行时,能够通过以下办法获取到 Instrumentation
对象,无需配置Agent
。
Instrumentation instrumentation = ByteBuddyAgent.install();
开发
根底代码
1. 转换解决类
以下代码是变更执行工具类:
public class Instrumentations {
private final static Instrumentation instrumentation;
private final static ClassPool classPool;
static {instrumentation = ByteBuddyAgent.install();
classPool = ClassPool.getDefault();}
private Instrumentations() {}
/**
* @param classFileTransformer
* @param classes
* @author
* @date
*/
public static void transformer(ClassFileTransformer classFileTransformer, Class<?>... classes) {
try {
// 增加.class 文件转换器
instrumentation.addTransformer(classFileTransformer, true);
int size = classes.length;
Class<?>[] classArray = new Class<?>[size];
// 复制字节码到 classArray
System.arraycopy(classes, 0, classArray, 0, size);
if (classArray.length > 0) {instrumentation.retransformClasses(classArray);
}
} catch (Exception e) {throw new RuntimeException(e);
} finally {
// 加强结束,移除 transformer
instrumentation.removeTransformer(classFileTransformer);
}
}
/**
* @return java.lang.instrument.Instrumentation
* @author
* @date
*/
public static Instrumentation getInstrumentation() {return instrumentation;}
/**
* @return javassist.ClassPool
* @author
* @date
*/
public static ClassPool getClassPool() {return classPool;}
}
2. 转换器
–增
、 删
、改
转换器的代码会有一些公共逻辑,所以先抽取出公共代码。
@Slf4j
public abstract class AbstractResettableTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {className = convertClassName(className);
logTransform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
CtClass cc = Instrumentations.getClassPool().get(className);
saveInitialSnapshot(className, classBeingRedefined, classfileBuffer);
defrost(cc);
return doTransform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer, cc);
} catch (Exception e) {logTransformError(loader, className, classBeingRedefined, protectionDomain, classfileBuffer, e);
throw new RuntimeException(e);
}
}
}
分步解析以上的代码:
1. 转换类名称,ClassFileTransformer
的包门路是以 /
分隔的。
protected String convertClassName(String sourceClassName) {return sourceClassName.replace("/", ".");
}
2. 这种重要的操作,肯定要记录日志。
protected void logTransform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {log.info("[{}]加强类 [{}] 代码!", this.getClass().getName(), className);
}
protected void logTransformError(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer, Exception e) {log.error("[{}]加强类 [{}] 代码异样!", this.getClass().getName(), className, e);
}
3. 备份原始的类字节
这一步是为了后续 还原
做筹备,有时候咱们可能只是长期减少下调试代码,调试完之后还要还原代码。
ClassFileTransformer
的transform
的办法如果返回null
,即示意不做加强,也会将class
的字节码还原,然而这种做法会有误伤
,会将 class 还原到最原始
的状态,如果有其余类 / 插件
也做了加强,比方有个自定义的agent
,这些加强也都会被还原。Javassist
的CtClass
类的detach
办法,也会革除Javassist
对代码的批改,detach
会从ClassPool
中清理掉CtClass
的缓存,而Javassist
中CtClass
就对应一个class
的字节,所以对 class 字节的批改都间接体现在对CtClass
的批改,如果清理掉CtClass
,那就相当于重置了Javassist
对代码的批改。这种做法跟下面一样会有误伤
。
综上所述,还原
采纳了 保留批改前的字节数组,还原时通过字节数组从新结构 class
的计划,
@Slf4j
public abstract class AbstractResettableTransformer implements ClassFileTransformer {final static ConcurrentHashMap<String, ByteCache> INITIAL_CLASS_BYTE = new ConcurrentHashMap<>();
protected void saveInitialSnapshot(String className, Class<?> classBeingRedefined, byte[] classfileBuffer) {if (!INITIAL_CLASS_BYTE.containsKey(className)) {INITIAL_CLASS_BYTE.putIfAbsent(className, new ByteCache(classBeingRedefined, classfileBuffer));
}
}
@Data
@AllArgsConstructor
public static class ByteCache {
private Class<?> clazz;
private byte[] bytes;}
}
4. 冻结对象
如果一个 CtClass
对象通过 writeFile()
、toClass()
、toBytecode()
被转换成一个类文件,此 CtClass
对象会被解冻起来,不容许再批改,通过 defrost
办法能够冻结
protected void defrost(CtClass ctClass) {if (ctClass.isFrozen()) {ctClass.defrost();
}
}
“改”:批改指定行的代码
这里先以 改
为例,因为应用 Javassist
来实现 改
的话,实际上就是先 删
再 增
,删
和增
的代码都能够从 改
中抽取(拷贝)进去。改
的ClassFileTransformer
类实现父类的 doTransform
办法。
@Slf4j
@AllArgsConstructor
@Data
public class ReplaceLineCodeTransformer extends AbstractResettableTransformer {
private String methodName;
private Integer lineNumber;
private String code;
@Override
public byte[] doTransform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer, CtClass cc) throws Exception {CtMethod m = cc.getDeclaredMethod(getMethodName());
clearline(m);
m.insertAt(getLineNumber(), code);
return cc.toBytecode();}
/**
* @param m
* @author
* @date
*/
protected void clearline(CtMethod m) throws Exception {CodeAttribute codeAttribute = m.getMethodInfo().getCodeAttribute();
LineNumberAttribute lineNumberAttribute = (LineNumberAttribute) codeAttribute
.getAttribute(LineNumberAttribute.tag);
int startPc = lineNumberAttribute.toStartPc(lineNumber);
int endPc = lineNumberAttribute.toStartPc(lineNumber + 1);
byte[] code = codeAttribute.getCode();
for (int i = startPc; i < endPc; i++) {code[i] = CodeAttribute.NOP;
}
}
}
ReplaceLineCodeTransformer
须要指定要 要批改的类办法
、 要替换的行
、 要替换的代码块
,这里分两步:
- 清理指定行的代码:将指定行的字节都改为
CodeAttribute.NOP
,即没有任何操作。 - 在指定行插入代码:如果是多句代码,插入的代码须要用
{}
包起来,例如:{int i = 0; System.out.println(i); }
,如果是单句,则不须要。
要留神 Javassist
并不会扭转 原先代码的行数
,例如原先代码 第 10 行
是int i = 0;
,这时候如果执行 insertAt(10, "int j = 0;")
,那 第 10 行
的代码会变成 int j = 0;int i = 0;
,代码会插在原先代码的后面,并且不会换行,同样的清理行代码,也只是把清理的那一行变成 空行
,下一行代码并不会上移。
以上代码是底层操作字节的代码,当初须要提供一个在线批改代码的入口,这里采纳了提供接口的计划。
计划须要思考几个点:
- 平安:接口不能轻易被调用。
- 多节点:业务服务部署在多个节点上,接管到变更申请的节点要把数据散发到其余节点。
接口
先看下接口代码:
@RestController
@RequestMapping("/classByte")
@Slf4j
public class ClassByteController {@PostMapping(value = "/replaceLineCode")
public void replaceLineCode(@Validated @RequestBody ReplaceLineCodeReq replaceLineCodeReq,
@RequestHeader("auth") String auth,
@RequestParam(required = false, defaultValue = "true") boolean broadcast) {auth(auth);
try {
Instrumentations.transformer(new ReplaceLineCodeTransformer(replaceLineCodeReq.getMethodName(),
replaceLineCodeReq.getLineNumber(), replaceLineCodeReq.getCode()),
Class.forName(replaceLineCodeReq.getClassName()));
if (broadcast) {broadcast(replaceLineCodeReq, auth, ByteOptType.REPLACE_LINE);
}
} catch (Exception e) {throw new RuntimeException(e);
}
}
}
@Data
public class BaseCodeReq {
/**
* @author
* @date
*/
public void check() {}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReplaceLineCodeReq extends BaseCodeReq {
@NotBlank
private String className;
@NotBlank
private String methodName;
@NotNull
@Min(1)
private Integer lineNumber;
@NotBlank
private String code;
@Override
public void check() {PredicateUtils.ifTrueThrowRuntimeException(StringUtils.isBlank(className), "className 不能为空");
PredicateUtils.ifTrueThrowRuntimeException(StringUtils.isBlank(methodName), "methodName 不能为空");
PredicateUtils.ifTrueThrowRuntimeException(lineNumber == null, "lineNumber 不能为空");
PredicateUtils.ifTrueThrowRuntimeException(StringUtils.isBlank(code), "code 不能为空");
}
}
申请 JSON 串
示例:
{
"className":"com.xxxxx.controller.MonitorController",
"methodName":"health",
"lineNumber":30,
"code":"{int i = 0; System.out.println(i); }"
}
接口内容分三步:
- 安全检查。
- 批改字节。
- 散发。
平安
平安次要从两方面动手:
- 开关:惯例工夫将开关配置成
敞开
,也就是禁止批改,要批改时再关上
,批改完之后再敞开
。 - 鉴权令牌:在
开关
关上的前提下,调用接口还要传一个令牌来比对,令牌放在HTTP Header
上。
之所以接口内容没有加密传递,是思考到批改字节时,大部分时候是手动调用接口(比方用 PostMan),这样会影响操作效率,且在 开关 + 令牌
的计划下根本曾经满足平安需要。
public class ClassByteController {@Value("${byte.canOpt:false}")
private boolean classByteCanOpt;
@Value("#{'${byte.auth:}'.isEmpty() ? T(com.xxxxx.common.util.UUIDGenerator).generateString() :'${byte.auth:}'}")
private String auth;
@PostConstruct
public void init() {log.info("ClassByteController auth :" + auth);
}
/**
* @param auth
* @author
* @date
*/
private void auth(String auth) {if (!classByteCanOpt || !this.auth.equals(auth)) {throw new BusinessException("unsupport!");
}
}
}
如果没有配置 令牌值
,则默认会随机生成字符串,能够通过日志查到随机生成的令牌。
散发
接口在接管申请后,公布 Redis 事件
,所有节点都监听该事件,在收到事件之后也更新本身的代码。为了避免散发事件的节点监听到事件之后再次批改类字节,系统启动时给每个节点生成一个 惟一的节点 ID(UUID)
,散发的数据里带上 以后节点 ID
,收到数据时,如果 数据里节点 ID
跟 以后节点的 ID
统一,则疏忽事件。
public class ClassByteController {
@Autowired
private Broadcaster broadcaster;
/**
* 播送告诉其余节点
*
* @param baseCodeReq
* @param auth
* @param optType
* @author
* @date
*/
private void broadcast(BaseCodeReq baseCodeReq, String auth, ByteOptType optType) {broadcaster.pubEvent(baseCodeReq, auth, optType);
}
}
@Slf4j
public class Broadcaster {
public static final String BYTE_BROADCAST_CHANNEL = "BYTE_BROADCAST_CHANNEL";
private String nodeUniqCode;
private RedisTemplate redisTemplate;
@Autowired
private ClassByteController classByteController;
public Broadcaster(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
nodeUniqCode = UUIDGenerator.generateString();}
/**
* @param baseCodeReq
* @param auth
* @param byteOptType
* @author
* @date
*/
public void pubEvent(BaseCodeReq baseCodeReq, String auth, ByteOptType byteOptType) {String message = JSON.toJSONString(buildEventData(baseCodeReq, auth, byteOptType));
redisTemplate.publish(BYTE_BROADCAST_CHANNEL, message);
log.info("实现发送字节变更音讯[{}]!", message);
}
/**
* @param baseCodeReq
* @param auth
* @param byteOptType
* @return com.xxxxx.common.byt.Broadcaster.EventData
* @author
* @date
*/
private EventData buildEventData(BaseCodeReq baseCodeReq, String auth, ByteOptType byteOptType) {EventData eventData = (EventData) new EventData().setNodeUniqCode(nodeUniqCode)
.setOptType(byteOptType)
.setAuth(auth);
BeanUtils.copyProperties(baseCodeReq, eventData);
return eventData;
}
}
public enum ByteOptType {
INSERT_LINE,
REPLACE_LINE,
CLEAR_LINE,
RESET_CLASS,
RESET_ALL_CLASSES;
/**
* @param value
* @return com.xxxxx.common.byt.model.ByteOptType
* @author
* @date
*/
public static ByteOptType getType(String value) {if (StringUtils.isBlank(value)) {return null;}
for (ByteOptType e : ByteOptType.values()) {if (e.toString().equals(value)) {return e;}
}
return null;
}
/**
* @param value
* @return boolean
* @author
* @date
*/
public static boolean isType(String value) {return getType(value) != null;
}
}
@Configuration
@ConditionalOnClass(JedisTemplate.class)
public class JedisConfiguration {
/**
* @param jedisTemplate
* @return com.xxxxx.common.byt.Broadcaster
* @author
* @date
*/
@Bean
public Broadcaster getJedisBroadcaster(@Autowired JedisTemplate jedisTemplate) {return new Broadcaster(jedisTemplate);
}
}
订阅
节点在监听到事件之后,依据事件类型和内容别离做不同的解决:
public class ClassByteController {
private Map<ByteOptType, Consumer<OptCode>> optHandler;
@PostConstruct
public void init() {log.info("ClassByteController auth :" + auth);
optHandler = Maps.newHashMap();
optHandler.put(ByteOptType.INSERT_LINE, optCode -> {InsertLineCodeReq req = new InsertLineCodeReq(optCode.getClassName(), optCode.getMethodName(),
optCode.getLineNumber(),
optCode.getCode());
req.check();
insertLineCode(req, optCode.getAuth(), false);
});
optHandler.put(ByteOptType.REPLACE_LINE, optCode -> {ReplaceLineCodeReq req = new ReplaceLineCodeReq(optCode.getClassName(), optCode.getMethodName(),
optCode.getLineNumber(),
optCode.getCode());
req.check();
replaceLineCode(req, optCode.getAuth(), false);
});
optHandler.put(ByteOptType.CLEAR_LINE, optCode -> {ClearLineCodeReq req = new ClearLineCodeReq(optCode.getClassName(), optCode.getMethodName(),
optCode.getLineNumber());
req.check();
clearLineCode(req, optCode.getAuth(), false);
});
optHandler.put(ByteOptType.RESET_CLASS, optCode -> {ResetClassCodeReq req = new ResetClassCodeReq(optCode.getClassName());
req.check();
resetClassCode(req, optCode.getAuth(), false);
});
optHandler.put(ByteOptType.RESET_ALL_CLASSES, optCode -> {resetAllClasses(optCode.getAuth(), false);
});
}
/**
* @param optCode
* @return com.xxxxx.common.byt.controller.ClassByteController
* @author
* @date
*/
@Value(value = "${classByte.optCode:}")
public void setOptCode(String optCode) {if (optHandler == null) {
// 系统启动时注入的内容,疏忽不解决,因为是历史解决过的
return;
}
log.info("接管到操作码:{}", optCode);
if (StringUtils.isBlank(optCode) || !StringUtil.simpleJudgeJsonObjectContent(optCode)) {return;}
OptCode optCodeValue = JSONObject.parseObject(optCode, OptCode.class);
if (StringUtils.isBlank(optCodeValue.getAuth())) {log.error("[" + optCode + "]auth 不能为空!");
return;
}
if (optCodeValue.getOptType() == null) {log.error("[" + optCode + "]操作类型异样!");
return;
}
optHandler.get(optCodeValue.getOptType()).accept(optCodeValue);
}
}
@Slf4j
public class Broadcaster {
/**
* @param message
* @author minchin
* @date 2021-04-29 10:22
*/
public void subscribe(String message) {EventData eventData = JSON.parseObject(message, EventData.class);
if (nodeUniqCode.equals(eventData.getNodeUniqCode())) {log.info("收到的字节变更音讯 [{}] 是以后节点本人收回的,疏忽掉!", message);
return;
}
classByteController.setOptCode(message);
}
}
@Configuration
@ConditionalOnClass(JedisTemplate.class)
public class JedisConfiguration {
/**
* @param jedisTemplate
* @param broadcaster
* @return com.xxxxx.common.redis.event.BaseRedisPubSub
* @author
* @date
*/
@Bean
public RedisPubSub getJedisBroadcasterPubSub(
@Autowired JedisTemplate jedisTemplate,
@Autowired Broadcaster broadcaster) {return new RedisPubSub(Broadcaster.BYTE_BROADCAST_CHANNEL, jedisTemplate) {
@Override
public void onMessage(String channel, String message) {logger.info("BroadcasterPubSub channel[{}] receive message[{}]", channel, message);
broadcaster.subscribe(message);
}
};
}
}
@Slf4j
public abstract class RedisPubSub implements BaseRedisPubSub {
protected ExecutorService pool;
private String channelName;
private RedisTemplate redisTemplate;
protected static final Logger logger = LoggerFactory.getLogger(RedisPubSub.class);
public RedisPubSub(String channelName, RedisTemplate redisTemplate) {if (StringUtils.isBlank(channelName)) {throw new IllegalArgumentException("channelName required!");
}
Assert.notNull(redisTemplate, "redisTemplate required!");
this.channelName = channelName;
this.redisTemplate = redisTemplate;
}
public RedisPubSub(String channelName, RedisTemplate redisTemplate, ExecutorService pool) {this(channelName, redisTemplate);
this.pool = pool;
}
@PostConstruct
public void init() {if (getPool() == null) {
setPool(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("redis-" + channelName + "-notify-pool-%d").build()));
}
getPool().execute(() -> {
// 梗塞,外部采纳轮询形式,监听是否有音讯,直到调用 unsubscribe 办法
getRedisTemplate().subscribe(this, channelName);
});
}
@PreDestroy
public void destroy() {ThreadUtils.shutdown(pool, 10, TimeUnit.SECONDS);
}
/**
* @return the pool
*/
public ExecutorService getPool() {return pool;}
/**
* @param pool the pool to set
*/
public void setPool(ExecutorService pool) {this.pool = pool;}
public RedisTemplate getRedisTemplate() {return redisTemplate;}
}
ClassByteController
的 setOptCode
形式上之所以加上 @Value(value = "${classByte.optCode:}")
注解,是因为我在设计零碎时,不仅反对 通过接口批改
,还反对 通过批改配置文件来批改
(零碎目前应用的配置文件系统,是反对热更新配置的),因为配置文件批改时,每个节点都会收到批改内容,所以解决时broadcase 为 false
,即 不散发
。
通过配置文件批改的形式,须要思考一种场景:上次配置了数据,批改之后未(遗记)清理,下次启动时,@Value
注入被执行,办法就会立即执行(属于预期以外的批改),因为 Spring
是先 注入属性
,再 初始化
,所以在@Value
失效执行 setOptCode
时init
办法还没被执行,也就是 optHandler
还未初始化,所以能够通过 optHandler == null
来过滤掉启动时的事件。
还原:还原到批改前的代码
当调试完之后,如果须要还原到批改前的代码,先从缓存里取出 初始字节数组
,再通过 字节数组
结构出 class
。
还原分两种:
-
还原指定的类
public class ClassByteController {@PostMapping(value = "/resetClassCode") public void resetClassCode(@Validated @RequestBody ResetClassCodeReq resetClassCodeReq, @RequestHeader("auth") String auth, @RequestParam(required = false, defaultValue = "true") boolean broadcast) {auth(auth); try { Instrumentations.transformer(new ResetClassCodeTransformer(), Class.forName(resetClassCodeReq.getClassName())); if (broadcast) {broadcast(resetClassCodeReq, auth, ByteOptType.RESET_CLASS); } } catch (Exception e) {throw new RuntimeException(e); } } } @Data @NoArgsConstructor @AllArgsConstructor public class ResetClassCodeReq extends BaseCodeReq { @NotBlank private String className; @Override public void check() {PredicateUtils.ifTrueThrowRuntimeException(StringUtils.isBlank(className), "className 不能为空"); } } @Slf4j @AllArgsConstructor public class ResetClassCodeTransformer extends AbstractResettableTransformer { @Override public byte[] doTransform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer, CtClass cc) throws Exception {if (!INITIAL_CLASS_BYTE.containsKey(className)) {return null;} Instrumentations.getClassPool() .makeClass(new ByteArrayInputStream(INITIAL_CLASS_BYTE.get(className).getBytes())); INITIAL_CLASS_BYTE.remove(className); return null; } }
后面讲过,
Javassist
的CtClass
对应一个Class
,所以能够通过字节数组结构出CtClass
对象,并替换掉ClassPool
里缓存的CtClass
对象,Javassist
的ClassPool
的makeClass
办法能够满足以上需要。 -
还原所有被批改的类
从缓存里取出所有的类,再一一循环执行还原
。public class ClassByteController {@PostMapping(value = "/resetAllClasses") public String resetAllClasses(@RequestHeader("auth") String auth, @RequestParam(required = false, defaultValue = "true") boolean broadcast) {auth(auth); try {String ret = AbstractResettableTransformer.resetAllClasses(); if (broadcast) {broadcast(new BaseCodeReq(), auth, ByteOptType.RESET_ALL_CLASSES); } return ret; } catch (Exception e) {throw new RuntimeException(e); } } } public abstract class AbstractResettableTransformer implements ClassFileTransformer {public static String resetAllClasses() {if (INITIAL_CLASS_BYTE.isEmpty()) {return Strings.EMPTY;} Class<?>[] classes = INITIAL_CLASS_BYTE.entrySet().stream() .map(v -> v.getValue().clazz) .collect(Collectors.toList()) .toArray(new Class<?>[INITIAL_CLASS_BYTE.size()]); String caches = StringUtils.join(INITIAL_CLASS_BYTE.keySet(), ","); Instrumentations.transformer(new ResetClassCodeTransformer(), classes); INITIAL_CLASS_BYTE.clear(); return caches; } }
“查”:下载 class 文件
批改之后,如果想看批改后的代码内容,能够将 CtClass
转为 二进制数组
,再将数组下载为文件。
public class ClassByteController {@GetMapping(value = "/getCode")
public ResponseEntity<ByteArrayResource> getCode(HttpServletResponse response,
@RequestParam String className,
@RequestParam String auth) {auth(auth);
byte[] bytes = Instrumentations.getClassBytes(className);
String fileName = className.substring(className.lastIndexOf(".") + 1);
ByteArrayResource resource = new ByteArrayResource(bytes);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment;filename=" + fileName + ".class")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(bytes.length)
.body(resource);
}
}
public class Instrumentations {public static byte[] getClassBytes(String className) {
try {CtClass cc = getClassPool().get(className);
return cc.toBytecode();} catch (Exception e) {throw new RuntimeException(e);
}
}
}
总结
在 Java5 引入 Instrumentation 之后,Java 容许运行时扭转字节,然而原始的 Instrumentation Api 须要开发者对字节码方面有深刻的理解,ASM、Javassist 和 ByteBuddy 等字节码操作库,提供了更简化的 API,让开发者不再须要对字节码方面有深刻的理解,本我的项目基于以上组件实现了 在线热更新代码
性能,性能蕴含对 Class
的增删改查
。