All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.wjybxx.agent.ClassReloadAgent Maven / Gradle / Ivy

There is a newer version: 1.3.0
Show newest version
/*
 * Copyright 2023-2024 wjybxx([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.wjybxx.agent;

import com.sun.tools.attach.VirtualMachine;

import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.Objects;
import java.util.jar.JarFile;

/**
 * Instrumentation开发指南
 * IBM文档
 * 

热更新限制

* 1. 热更新只可以更改方法体和常量池,不可以增删属性和方法,不可以修改继承关系。 * 2. 已初始化的类不会再次进行初始化(注意静态代码块)。 * 3. 热更的方法,只有再次进入时才会生效。 * 4. 被内联的方法可能提示热更新成功,却永远得不到执行(注意热点代码)。 * 5. class对象引用不会改变,即不需要向其它热更新方式那样迁移数据(这是很大的优势)。 * 6. 不可增删lambda表达式,不可以增删方法引用。 * 7. 内部类和外部类必须一起热更新。 * *

违背直觉的情况

* 1. lambda表达式:如果lambda表达式捕获的变量变更,将无法热更(因为编译时会生成特殊的粘合类,粘合类的成员属性会变更)。 * 2. 内部类:如果需要访问另一个类的private字段,将无法热更(因为编译时会为其生成特殊的桥接方法,新增了静态方法)。 * 3. switch:大型的switch语句无法热更(大型switch语句建议使用map进行映射)。 * *

奇巧淫技

* 1. 每个manager额外定义一个通用方法 {@code Object execute(String cmd, Object params)},当需要某个manager的功能和属性时,可以迂回救国。 * 2. 每个manager额外定义一个黑板,比如就一个Map,这样当需要新增属性时,可以添加到map中。加上上一条,最好有个manager基类? * 3. 每个玩家额外定义两个黑板,比如两个map,一个存库,一个不存库。这样当需要在玩家身上新增属性时,可以存储到map中。 * 4. 玩家与服务器之间可以预留几条通用协议,用于救急。 * 5. 内部类的属性尽量声明为包级(默认权限),尽量少使用private,或提供getter/setter方法。 * *

使用方式

* 1. 由于代理必须以jar包形式存在,因此文件检测,执行更新等逻辑,请写在自己的业务逻辑包中,不要写在这里,方便扩展。 * 2. 热更新时不要一次更新太多类,否则可能导致停顿时间过长。 * 3. 由于该API于IDE热更新是一套API,因此必须在本机上进行热更新测试,本机能通过,基本上运行环境也能通过。 * 4. 每次启服后,需要进行一次热更流程,避免使用的是旧的class文件。 * 5. 除非重启服务器,否则热更的代码不可删除。因此,只有在更版本的时候才可以删除热更代码,替换为正式的代码(这也是启服后必须执行一次热更的原因)。 * 6. 热更只应该用于修改重大bug,不建议动不动就热更,平时要保证代码质量。 * *

* debug下使用{@link #agentmain(String, Instrumentation)}比较方便,直接在ide中添加启动参数就可以。 * 线上使用{@link #premain(String, Instrumentation)}方式比较方便。 * * @author wjybxx * date 2023/9/9 */ public class ClassReloadAgent { private static volatile Instrumentation instrumentation; private static void setInstrumentation(Instrumentation instrumentation) { ClassReloadAgent.instrumentation = Objects.requireNonNull(instrumentation); } /** * 这是instrument开发规范规定的固定格式的方法,当java程序启动时,如果指定了javaagent参数(classpath下的jar包名字),则会自动调用到这个方法。 * 注意: * 1. 需要在启动参数中指定 javaagent参数。eg: * -javaagent:game-classreloadagent-1.0.jar=test * 则agentArgs为test(若无等号则为null) * 2. 不能方便的调试,必须在命令行中启动。 * * @param agentArgs 启动参数 * @param instrumentation 我们需要的实例,需要将其保存下来 */ public static void premain(String agentArgs, Instrumentation instrumentation) { System.out.println("premain invoked, agentArgs: " + agentArgs); ClassReloadAgent.setInstrumentation(instrumentation); } /** * 使用动态attach的方式获取{@link Instrumentation}。 * 这是instrument开发规范规定的固定格式的方法,当使用{@link VirtualMachine#loadAgent(String, String)}连接到JVM时,会触发该方法。 *

* 注意 * 1. 如果要attach到自身所在JVM,需要添加启动参数 -Djdk.attach.allowAttachSelf=true 否则会抛出异常。 * 2. 它的参数来自{@link VirtualMachine#loadAgent(String, String)}的第二个参数(options) * 3. 可直接在debug环境下使用(在debug参数中指定vm options即可)。 * * @param agentArgs {@link VirtualMachine#loadAgent(String, String)}中的options * @param instrumentation 我们需要的实例,需要将其保存下来 */ public static void agentmain(String agentArgs, Instrumentation instrumentation) { System.out.println("agentmain invoked, agentArgs: " + agentArgs); ClassReloadAgent.setInstrumentation(instrumentation); } /** * 查询是否启用了重定义类功能。 * 注意:该返回值与{@link #isModifiableClass(Class)}返回值没有关系。 */ public static boolean isRedefineClassesSupported() { return instrumentation.isRedefineClassesSupported(); } /** * 查询一个类是否可以被修改(被重定义),是否可以用于{@link #redefineClasses(ClassDefinition...)}方法。 * 注意:该返回值与{@link #isRedefineClassesSupported()}的返回值没有关系。 * 所以:要热更新类时需要保证{@link #isRedefineClassesSupported()}为true,且要更新的类调用当前方法返回值为true。 * 基本上:你在ide中能热更新,那么这里就能热更新,所以最好现在ide上测试一下。 * * @param theClass 要热更新的类 * @return true/false */ public static boolean isModifiableClass(Class theClass) { return instrumentation.isModifiableClass(theClass); } /** * 重定义类文件(热更新类文件) * 注意:请保证{@link #isRedefineClassesSupported()}为true,且所有要更新的类{@link #isModifiableClass(Class)}为true。 * * @param definitions 要热更新的类(要重定义的类) * 注意:该方法的参数是数组,如果类之间有关系的话,最好一起更新(原子方式更新)。 */ public static void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException { instrumentation.redefineClasses(definitions); } /** * 添加指定jar包到根类加载器路径中 * * @param jarfile 要加载的jar包 */ public static void appendToBootstrapClassLoaderSearch(JarFile jarfile) { instrumentation.appendToBootstrapClassLoaderSearch(jarfile); } /** * 添加指定jar包到系统类加载器路径中 * * @param jarfile 要加载的jar包 */ public static void appendToSystemClassLoaderSearch(JarFile jarfile) { instrumentation.appendToSystemClassLoaderSearch(jarfile); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy