3

background

When the system encounters an unusual condition, plus you want to print the log under critical information, or change the next logic code, but do not want to restart, restart because too cumbersome and too time-consuming may destroy the scene, and even some scenes in a test environment can not It can not be reproduced after simulation. At this time, I hope to update the code without restarting and take effect immediately.
Objective: Add, delete, modify and check the code, and hot update in real time.

  1. increase: insert code.
  2. Delete: Delete the code.
  3. change: replace the code.
  4. check: Download the class file of the specified class. If it is modified, then the downloaded class file is the modified 060f6b09f61ad4 file.
  5. restore: restore the code before modification.
Ali has a Arthas supports online problem diagnosis and online code modification, but there are still limitations: the steps to modify the code are cumbersome, you need to log in to the server to operate, and you can only update the code of one service node at a time. If multiple nodes are deployed , You need to log in to each one.

concept

Instrumentation

Using Insrumentation , developers can build an application-independent agent program ( Agent ), monitor and assist programs running on the JVM, and even replace and modify certain class definitions. Simply put, developers can use Instrumentation to achieve a virtual machine-level AOP implementation.
Instrumentation maximal effect is class definitions and dynamically change operation. When the program is running, specify a specific jar file -javaagent parameter to start the Instrumentation agent program. In fact, this is familiar to many people: xmind , idea permanent cracking use agent , Mockito and other Mock libraries also use agent , and some monitoring software (such as skywalking ).
implement Instrumentation in java?

1. Create a proxy class

Java Agent supports loading when the JVM starts, and also supports JVM is running. These two different loading modes will use different entry functions.
If you need to load the Agent when the target JVM starts, you can choose to implement the following methods:

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

If you want to load the Agent when the target JVM is running, you need to implement the following method:

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

The first parameter options is the parameter passed to the agent through the command line, and the second parameter is the Instrumentation instance ClassTransformer
method 1 has a higher priority than method 2. When method 1 and method 2 exist at the same time, method 2 will be ignored.

Transition occurs premain after performing the function, main before the function is executed, each time a class loading, transform method will be executed once, so transform method can be used className.equals(myClassName) to determine whether the current class to be converted, return null means that the current word Section does not need to be converted.

2. Create a class converter

The operation on the Java class file can be understood as the operation on the java binary byte array, modifying the original byte array, and returning the modified byte array.
ClassFileTransformer interface has only one transform method. The parameters are passed in including the class loader, class name, original byte code byte stream, etc., and the converted byte code byte stream is returned.

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. Create the MANIFEST.MF file

Create a new META-INF/MANIFEST.MF file in the resource directory, where Premain-Class is the class name containing the package name

Mainfest-Version: 1.0
Premain-Class: com.xxx.AgentTester
Can-Redefine-Classes: true
Can-Retransform-Classes: true

According to different loading methods, choose configuration Premain-Class or Agent-Class .

4. Package & Run

MANIFEST.MF files can be generated by Maven's org.apache.maven.plugins and maven-assembly-plugin plug-ins, and 060f6b09f620b8 files can also be automatically generated by the above plug-ins.
javaagent start command,

java -javaagent:/文件路径/myAgent.jar -jar myProgram.jar

E.g:

java -javaagent:/usr/local/dev/MyAgent.jar -jar /usr/local/dev/MyApplication.jar

We can also set optional agent parameters on the location path.

java -javaagent:/usr/local/dev/MyAgent.jar=Hello -jar /usr/local/dev/MyApplication.jar

Javassist

The Java bytecode is stored in the .class binary, and each .class file contains a Java class or interface. Regarding the processing of java bytecode, there are many libraries, such as bcel , asm . But these all need to directly deal virtual machine instructions. If you don't want to understand the virtual machine instructions, javassist is a good choice. Javassist can add a new method to an already compiled class, or modify an existing method, and does not require an in-depth understanding of bytecode.
The following example is to modify fun MyApp class. When entering the method, first print a line of before .

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.MyApp");
CtMethod m = cc.getDeclaredMethod("fun");
m.insertBefore("{ System.out.println(\"before\"); }");
cc.writeFile();

The most important of Javassist are the four classes ClassPool、CtClass、CtMethod、CtField
ClassPool is CtClass container object, a storage CtClass information HashTable , Key is class name, value of CtClass object, it reads the class files needed to construct CtClass the object, and the cache CtClass objects for later use.
CtClass is an abstract expression in the code of a class file. The modification of CtClass is equivalent to the modification of the class file.
CtMethod and CtField correspond to the methods and attributes in the class.

With the previous Instrumentation , the class code can be converted 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);
    }
}

Because JAVA Agent -javaagent on the command line, this increases the cost of introducing . When making public components, simple to use and is also a point to consider.

Byte Buddy

Byte Buddy provides a more simplified API. The following example shows how to generate a simple class, which is a subclass of Object , and rewrites the toString method to return 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!"));

When choosing a bytecode operation library, you should also consider the performance of the library itself. The official website has performed a performance test on the library, and the following results are shown:

As can be seen from the performance report, Byte Buddy main focus is to generate code with minimal runtime should be noted that these tests measure the performance of Java code, the Java virtual machine by the time compiler optimized, if you The code runs occasionally and is not optimized by the virtual machine, and performance may deviate.

Unfortunately, Byte Buddy does not support modifying the code in the existing method. such as deletes a line of code. This requirement cannot be achieved through Byte Buddy . But Byte Buddy Instrumentation object through the following method when the project is running, and there is no need to configure Agent .

Instrumentation instrumentation = ByteBuddyAgent.install();

Development

Base code

1. Conversion processing

The following code is the change execution tool class:

public class Instrumentations {

    private final static Instrumentation instrumentation;
    private final static ClassPool classPool;

    static {
        instrumentation = ByteBuddyAgent.install();
        classPool = ClassPool.getDefault();
        // 指定路径,否则可能会出现javassist.NotFoundException的问题
        classPool.insertClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
    }

    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. Converter

added, deleted, changed converter code will have some common logic, so extract the common code first.

@Slf4j
@AllArgsConstructor
public abstract class AbstractResettableTransformer implements ClassFileTransformer {
    protected String fullClassName;
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            className = convertClassName(className);
            // 必须做这一层过滤, 实践发现即使调用ClassFileTransformer时指定了类,但还是会有其他类也被执行了transformer
            if (!fullClassName.equals(className)) {
                return classfileBuffer;
            }
            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);
        }
    }
}    

Analyze the above code step by step:
1. convert the class name, ClassFileTransformer packet path is / separated.

protected String convertClassName(String sourceClassName) {
    return sourceClassName.replace("/", ".");
}

2. This important operation must be recorded in a log.

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. Back up the original class byte
This step is to . Sometimes we may just temporarily increase the debugging code, and restore the code after debugging.

  • ClassFileTransformer of transform method if it returns null , that means not enhanced, will also class bytecode reduction, but this approach will injury, the class will be restored to most primitive state, if there are other class / Plug-ins have also been enhanced, for example, there is a custom agent , these enhancements will also be restored.
  • Javassist of CtClass class detach method also clears Javassist changes to the code, detach will from ClassPool liquidation out CtClass cache, and Javassist in CtClass would correspond to a class bytes, so the class byte changes are a direct manifestation In CtClass CtClass is cleaned up, it is equivalent to resetting the code modification Javassist This approach will cause accidental injury as above.

To sum up, restore uses save the byte array before modification, and reconstruct the class through the byte array when restoring.

@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.
If a CtClass object is converted into a class file writeFile() , toClass() , toBytecode() CtClass object will be frozen and no further modification is defrost . It can be unfrozen by the 060f6b09f62b80 method

protected void defrost(CtClass ctClass) {
    if (ctClass.isFrozen()) {
        ctClass.defrost();
    }
}

"Change": modify the code of the specified line

Here first to modified example, because the use of Javassist achieved change, then, is in fact the first deleted then add, deleted and growing code from extract (copy) out of the change.
modified ClassFileTransformer class implements the doTransform method of the parent class.

@Slf4j
@Data
public class ReplaceLineCodeTransformer extends AbstractResettableTransformer {
    public ReplaceLineCodeTransformer(String fullClassName, String methodName, Integer lineNumber) {
        super(fullClassName);
        this.methodName = methodName;
        this.lineNumber = lineNumber;
        this.code = 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 need to specify class methods to modify, be replaced row, to replace the block, here in two steps:

  • Clean up the code of the specified line: CodeAttribute.NOP the bytes of the specified line to 060f6b09f62d41, that is, there is no operation.
  • Insert code in the specified line: If it is a multi-sentence code, the inserted code needs to be wrapped in {} , for example: { int i = 0; System.out.println(i); } , if it is a single sentence, it is not required.

Pay attention to Javassist does not change number of lines of code of the original, such as original codes line 10 is int i = 0; , this time if the execution insertAt(10, "int j = 0;") , that the code on line 10 will become int j = 0;int i = 0; , the code will be inserted in front of the original code , And will not wrap. The same clean-up line of code only changes the cleaned line into a blank line, and the next line of code will not move up.

The above code is the code for operating bytes at the bottom layer. Now an entry for online modification of the code needs to be provided, and the solution of providing an interface is adopted here.
The plan needs to consider several points:

  1. Security: The interface cannot be called casually.
  2. Multi-node: The business service is deployed on multiple nodes, and the node that receives the change request must distribute the data to other nodes.

interface

First look at the interface code:

@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不能为空");
    }
}

Example of request JSON string:

{
    "className":"com.xxxxx.controller.MonitorController",
    "methodName":"health",
    "lineNumber":30,
    "code":"{ int i = 0; System.out.println(i); }"
}

The interface content is divided into three steps:

  1. Security check.
  2. Modify bytes.
  3. distribution.

Safety

Security mainly starts from two aspects:

  1. Switch: The switch is configured to close regular time, that is, modification is prohibited. When you want to modify, then turned on, and after the modification is completed, turned off.
  2. Authentication token: On switch is turned on, the calling interface also needs to pass a token for comparison. The token is placed on HTTP Header .

The reason why the interface content is not encrypted and transmitted is that when modifying bytes, most of the time the interface is manually called (for example, using PostMan), which will affect the operation efficiency, and the security requirements have basically been met under the switch + token scheme .

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

If the token value is not configured, the string will be randomly generated by default, and the randomly generated token can be found through the log.

distribution

After the interface receives the request, it publishes the Redis event. All nodes listen to the event and update their own code after receiving the event. In order to prevent the node that distributes the event from modifying the class byte again after listening to the event, when the system starts, a unique node ID (UUID) of generated for each node, and the current node ID of distributed data. When the data is received, if the node ID in the current node ID of 160f6b09f63099, the event will be ignored.

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

subscription

After the node listens to the event, it will do different processing according to the event type and content:

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 reason why the @Value(value = "${classByte.optCode:}") annotation is added to the setOptCode method of 060f6b09f631f6 is because when I design the system, I not only support through the interface modification, but also support through the modification of the configuration file. ), because each node will receive the modified content when the configuration file is modified, so broadcase is false during processing, that is, not distributed.
By modifying the configuration file, you need to consider a scenario: the data was configured last time, and it was not (forgotten) cleaned up after the modification. When the next startup is started, the @Value injection is executed, and the method will be executed immediately (unexpected modification). Since Spring is the first injection property, and then initialization, so @Value come into force setOptCode when init method has not been executed, that is, optHandler has not been initialized, it can optHandler == null to filter out events at startup.

Restore: restore to the code before modification

After debugging, if you need to revert to the code before modification, first take out the initial byte array from the cache, and then construct class byte array.
There are two types of reduction:

  • restore the specified 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;
      }
    }     

    Recall, Javassist of CtClass corresponds to a Class , byte arrays can be configured by the CtClass object and replace ClassPool in cache CtClass object Javassist the ClassPool the makeClass way to meet these requirements.

  • restore all modified classes
    Retrieve all the classes from the cache, and execute the restoration cycle one by one.

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

"Check": Download class files

After the modification, if you want to see the modified code content, you can convert CtClass to a binary array, and then download the array as a file.

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

to sum up

After the introduction of Instrumentation in Java5, Java allows bytes to be changed at runtime, but the original Instrumentation Api requires developers to have an in-depth understanding of bytecode. Bytecode manipulation libraries such as ASM, Javassist and ByteBuddy provide a more simplified API. , So that developers no longer need to have an in-depth understanding of bytecode, this project is based on the above components to achieve the online hot update code function, the function includes addition, deletion, and Class .


noname
314 声望49 粉丝

一只菜狗