背景
当零碎遇到异常情况时,想要加上日志打印下要害信息,或者改下逻辑代码,但又不想重启,因为重启太麻烦太耗时且可能会毁坏现场
,甚至有些场景在测试环境无奈模仿进去导致无奈复现。这时候就心愿能在不重启的状况下更新代码并立刻失效。
指标:对代码的增删改查,并且实时热更新。
增
:插入代码。删
:删除代码。改
:替换代码。查
:下载指定类的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.0Premain-Class: com.xxx.AgentTesterCan-Redefine-Classes: trueCan-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. 转换器
-增
、删
、改
转换器的代码会有一些公共逻辑,所以先抽取出公共代码。
@Slf4jpublic 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
的计划,
@Slf4jpublic 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@Datapublic 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")@Slf4jpublic 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); } }}@Datapublic class BaseCodeReq { /** * @author * @date */ public void check() { }}@Data@NoArgsConstructor@AllArgsConstructorpublic 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); }} @Slf4jpublic 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); } } @Slf4jpublic 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); } }; }}@Slf4jpublic 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@AllArgsConstructorpublic class ResetClassCodeReq extends BaseCodeReq { @NotBlank private String className; @Override public void check() { PredicateUtils.ifTrueThrowRuntimeException(StringUtils.isBlank(className), "className不能为空"); }}@Slf4j@AllArgsConstructorpublic 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
的增删改查
。