背景

当零碎遇到异常情况时,想要加上日志打印下要害信息,或者改下逻辑代码,但又不想重启,因为重启太麻烦太耗时且可能会毁坏现场,甚至有些场景在测试环境无奈模仿进去导致无奈复现。这时候就心愿能在不重启的状况下更新代码并立刻失效。
指标:对代码的增删改查,并且实时热更新。

  1. :插入代码。
  2. :删除代码。
  3. :替换代码。
  4. :下载指定类的class文件 ,如果是批改过的,那下载的就是批改后的class文件。
  5. 还原:还原回批改前的代码。

概念

Instrumentation

应用 Insrumentation,开发者能够构建一个独立于应用程序的代理程序(Agent),监测和帮助运行在JVM上的程序,甚至能够替换和批改某些类的定义。简略的来说开发者应用Instrumentation能够实现一种虚拟机级别的AOP实现。
Instrumentation的最大作用,就是类定义动静扭转和操作。程序运行时,通过-javaagent参数指定一个特定的jar文件来启动Instrumentation的代理程序。其实这个对很多人来说不生疏:xmind, idea永恒破解都应用了agentMockitoMock类库也用到了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-ClassAgent-Class

4. 打包&运行

通过Maven的org.apache.maven.pluginsmaven-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字节码的解决,有很多类库,如bcelasm。不过这些都须要间接跟虚拟机指令打交道。如果你不想理解虚拟机指令,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这四个类:
ClassPoolCtClass对象的容器,一张保留CtClass信息的HashTable,key为类名,value为CtClass对象,它按需读取类文件来结构CtClass对象,并且缓存CtClass对象以便当前应用。
CtClass是一个class文件在代码中的形象表现形式,对CtClass的批改相当于对class文件的批改。
CtMethodCtField对应的是类中的办法和属性。

配合后面的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. 备份原始的类字节
这一步是为了后续还原做筹备,有时候咱们可能只是长期减少下调试代码,调试完之后还要还原代码。

  • ClassFileTransformertransform的办法如果返回null,即示意不做加强,也会将class的字节码还原,然而这种做法会有误伤,会将class还原到最原始的状态,如果有其余类/插件也做了加强,比方有个自定义的agent,这些加强也都会被还原。
  • JavassistCtClass类的detach办法,也会革除Javassist对代码的批改,detach会从ClassPool中清理掉CtClass的缓存,而 JavassistCtClass就对应一个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;,代码会插在原先代码的后面,并且不会换行,同样的清理行代码,也只是把清理的那一行变成空行,下一行代码并不会上移。

以上代码是底层操作字节的代码,当初须要提供一个在线批改代码的入口,这里采纳了提供接口的计划。
计划须要思考几个点:

  1. 平安:接口不能轻易被调用。
  2. 多节点:业务服务部署在多个节点上,接管到变更申请的节点要把数据散发到其余节点。

接口

先看下接口代码:

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

接口内容分三步:

  1. 安全检查。
  2. 批改字节。
  3. 散发。

平安

平安次要从两方面动手:

  1. 开关:惯例工夫将开关配置成敞开,也就是禁止批改,要批改时再关上,批改完之后再敞开
  2. 鉴权令牌:在开关关上的前提下,调用接口还要传一个令牌来比对,令牌放在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;    }}         

ClassByteControllersetOptCode形式上之所以加上@Value(value = "${classByte.optCode:}")注解,是因为我在设计零碎时,不仅反对通过接口批改,还反对通过批改配置文件来批改(零碎目前应用的配置文件系统,是反对热更新配置的),因为配置文件批改时,每个节点都会收到批改内容,所以解决时broadcase为false,即不散发
通过配置文件批改的形式,须要思考一种场景:上次配置了数据,批改之后未(遗记)清理,下次启动时,@Value注入被执行,办法就会立即执行(属于预期以外的批改),因为Spring是先注入属性,再初始化,所以在@Value失效执行setOptCodeinit办法还没被执行,也就是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;  }}     

    后面讲过,JavassistCtClass对应一个Class,所以能够通过字节数组结构出CtClass对象,并替换掉ClassPool里缓存的CtClass对象,JavassistClassPoolmakeClass办法能够满足以上需要。

  • 还原所有被批改的类
    从缓存里取出所有的类,再一一循环执行还原

    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增删改查