乐趣区

关于java:无需重启在线更新代码

背景

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

  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.0
Premain-Class: com.xxx.AgentTester
Can-Redefine-Classes: true
Can-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. 转换器

转换器的代码会有一些公共逻辑,所以先抽取出公共代码。

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

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

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

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

接口

先看下接口代码:

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

接口内容分三步:

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

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

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

    后面讲过,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增删改查

退出移动版