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.
increase: insert code.
Delete: Delete the code.
change: replace the code.
check: Download the
class
file of the specified class. If it is modified, then the downloadedclass
file is the modified 060f6b09f61ad4 file.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
oftransform
method if it returnsnull
, that means not enhanced, will alsoclass
bytecode reduction, but this approach willinjury, 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
ofCtClass
classdetach
method also clearsJavassist
changes to the code,detach
will fromClassPool
liquidation outCtClass
cache, andJavassist
inCtClass
would correspond to aclass
bytes, so the class byte changes are a direct manifestation InCtClass
CtClass
is cleaned up, it is equivalent to resetting the code modificationJavassist
This approach will causeaccidental 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:
- Security: The interface cannot be called casually.
- 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:
- Security check.
- Modify bytes.
- distribution.
Safety
Security mainly starts from two aspects:
- 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.
- 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
ofCtClass
corresponds to aClass
, byte arrays can be configured by theCtClass
object and replaceClassPool
in cacheCtClass
objectJavassist
theClassPool
themakeClass
way to meet these requirements.restore all modified classes
Retrieve all the classes from the cache, and execute therestoration 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
.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。