spring-loaded Spring官方的热更新agent Spring Loaded allows you to add/modify/delete methods/fields/constructors
评价
- 使用asm进行字节增强,速度较快,但是字节增强的代码比较难懂,可以将增强后的类保存下来反编译查看增强的结果
- 不依赖于DCEVM,可以直接在开发的jdk中使用
- 自己实现的类更新监控,一个类只能监控一个文件,不能监控自定义目录,没有使用nio系统文件监控,自己启动一个线程循环遍历比较监控的文件更新时间,效率感觉有点低
- 只能在IDE中实现热更新,不能给Spring boot jar包的类资源创建监听
使用
IDE中配置vm启动参数
-noverify -javaagent:/tmp/springloaded-1.3.0.RELEASE.jar -Dspringloaded="verbose=true;logging=true;watchJars=dependency.jar"
springloaded 支持配置多个参数,用封号拼接 参数说明
- logging=true 打印日志
- verbose=true 打印更多的细节
- watchJars=dependency.jar 监听依赖jar包的变更热更新
- dump=a.b,c.d,可以将类的.class文件dump下来,然后用jad反编译查看增强类的字节, 例如
dump=com.study.ServiceTest2,com.study.ServiceTest - dumpFolder=/tmp/dump 指定dump字节文件保存的目录
- cleanCache
- caching
- allowSplitPackages
- debugplugins 是否打印插件的信息
- enumlimit
- profile
- cacheDir
- callsideRewritingOn
- verifyReloads
- maxClassDefinitions 默认是100
- asserts 启用强校验
- rebasePaths 可以更换监控目录
- 例如"a.b=c.d,e.f=g.h",监控资源的时候会将一个类监控的前缀改写,加入原来类的前缀是a.b.myClass,监控的是c.d.myClass文件变更
- inclusions 需要增强监控的包名
- exclusions 不需要增强监控的包名
- plugins 加载自定义插件,多个用逗号拼接
- investigateSystemClassReflection
- rewriteAllSystemClasses
- explain
编译学习
- 打开工程用gradle编译即可,没有jdk限制,打包比较简单
- 有很多单元测试用例可以用来学习
- 配置dump可以保存增强类,查看增强后的结果
核心代码
org.springsource.loaded.agent.SpringLoadedAgent agent启动类org.springsource.loaded.agent.ClassPreProcessorAgentAdapter transform字节
- 如果是重新加载类,返回ReloadableType定义的字节
- 类第一次加载的时候调用
org.springsource.loaded.agent.SpringLoadedPreProcessor#preProcess 进行增强 org.springsource.loaded.agent.SpringLoadedPreProcessor 字节增强
public byte[] preProcess(ClassLoader classLoader, String slashedClassName, ProtectionDomain protectionDomain,
byte[] bytes) {
// 如果禁用则不增强
if (disabled) {
return bytes;
}
// 遍历插件进行字节增强
for (Plugin plugin : getGlobalPlugins()) {
if (plugin instanceof LoadtimeInstrumentationPlugin) {
LoadtimeInstrumentationPlugin loadtimeInstrumentationPlugin = (LoadtimeInstrumentationPlugin) plugin;
if (loadtimeInstrumentationPlugin.accept(slashedClassName, classLoader, protectionDomain, bytes)) {
bytes = loadtimeInstrumentationPlugin.modify(slashedClassName, classLoader, bytes);
}
}
}
// 确保系统类被加载
tryToEnsureSystemClassesInitialized(slashedClassName);
// 得到类注册器TypeRegistry
TypeRegistry typeRegistry = TypeRegistry.getTypeRegistryFor(classLoader);
...
if (typeRegistry == null) { // A null type registry indicates nothing is being made reloadable for the classloader
......此处省略,可以通过配置决定是否对系统类进行增强
return bytes;
}
// 决策类是否可以重新加载What happens here? The aim is to determine if the type should be made reloadable.
// 1. If NO, but something in this classloader might be, then rewrite the call sites.
// 2. If NO, and nothing in this classloader might be, return the original bytes.
// 3. If YES, make the type reloadable (including rewriting call sites)
ReloadableTypeNameDecision isReloadableTypeName = typeRegistry.isReloadableTypeName(slashedClassName,
protectionDomain, bytes);
if (isReloadableTypeName.isReloadable) {
..... 一大堆逻辑,字节增强,核心就是创建ReloadableType对象和资源监控
try {
String dottedClassName = slashedClassName.replace('/', '.');
String watchPath = getWatchPathFromProtectionDomain(protectionDomain, slashedClassName);
if (watchPath == null) {
// 如果是jar包运行的时候会执行到这里来
.... For a CGLIB generated type, we may still need to make the type reloadable
if(....) {
return bytes;
} else {
...
return rtype.bytesLoaded;
}
}
// 创建ReloadableType
ReloadableType rtype = typeRegistry.addType(dottedClassName, bytes);
if (rtype == null && GlobalConfiguration.callsideRewritingOn) {
// it is not a candidate for being made reloadable (maybe it is an annotation type)
// but we still need to rewrite call sites.
bytes = typeRegistry.methodCallRewrite(bytes);
}
else {
if (GlobalConfiguration.fileSystemMonitoring && watchPath != null) {
// 添加资源更新监控
typeRegistry.monitorForUpdates(rtype, watchPath);
}
return rtype.bytesLoaded;
}
}
....
}
else {
try {
// Skipping the CallSiteClassLoader here because types from there will already have been dealt
// with due to GroovyPlugin class that intercepts define in that infrastructure
if (needsClientSideRewriting(slashedClassName) &&
(classLoader == null || !classLoader.getClass().getName().equals(
"org.codehaus.groovy.runtime.callsite.CallSiteClassLoader"))) {
// 改写类的调用方式,改写第一次写入缓存,后续从缓存中加载改写的类字节
bytes = typeRegistry.methodCallRewriteUseCacheIfAvailable(slashedClassName, bytes);
}
}
....
}
return bytes;
}
org.springsource.loaded.GlobalConfiguration 配置类,静态代码块中执行配置解析,配置变量都保存为静态变量org.springsource.loaded.TypeRegistry 所有可重载类的生产和注册中心 The type registry tracks all reloadable types loaded by a specific class loaderorg.springsource.loaded.ReloadableType 可重载类的实现核心,如果一个类是需要可重载的,会这个类会进行增强生成一个ReloadableType的静态变量r$type ,方法调用和加载新版本字节都是通过这个r$type 实现的org.springsource.loaded.agent.FileSystemWatcher#FileSystemWatcher 自己实现的简单的单线程监控文件变更,不断循环遍历扫描文件的更新时间,不能监控spring boot jar包中的类org.springsource.loaded.ReloadableType .MergedRewrite 内部类,给需要可重载的类进行增强,通过ChainAdaptor嵌套改写字节增强类方法(有点难懂)
org.springsource.loaded.MethodInvokerRewriter.RewriteClassAdaptor 一大堆代码,惭愧没看懂增强为什还要这个RewriteClassAdaptororg.springsource.loaded.TypeRewriter.RewriteClassAdaptor 增强都是在这里做的
- In every method, introduce logic to check it it the latest version of that method
- Creates additional methods to aid with field setting/getting
- Creates additional fields to help reloading (reloadable type instance, new field value holders)
- Creates catchers for inherited methods. Catchers are simply passed through unless a new version of the class
org.springsource.loaded.ChildClassLoader 自定义ClassLoader防止内存泄露
- 通过WeakReference引用,防止 ChildClassLoader初始化就被gc
- 增加了一个definedCount计数,用来统计加载类的个数,
TypeRegistry#checkChildClassLoader 会判断加载类的个数是否超过maxClassDefinitions ,如果超过以后重新创建一个ChildClassLoader,老的那个ChildClassLoader及其加载的所有类会等待系统自动gc - 一个可重载类的新版本字节才是通过
ChildClassLoader 加载的,类第一次加载都是用的默认classLoader public class ChildClassLoader extends URLClassLoader {
private static URL[] NO_URLS = new URL[0];
private int definedCount = 0;
public ChildClassLoader(ClassLoader classloader) {
super(NO_URLS, classloader);
}
public Class<?> defineClass(String name, byte[] bytes) {
definedCount++;
return super.defineClass(name, bytes, 0, bytes.length);
}
public int getDefinedCount() {
return definedCount;
}
}
实用工具类
Utils.loadDottedClassAsBytes 加载.class文件字节org.springsource.loaded.ClassRenamer 可以修改类的名字,方法的名字org.springsource.loaded.test.infra.ClassPrinter 将byte数组打印成字节码 - org.springsource.loaded.TypeDiffComputer#computeDifferences 比较两个类的字节byte[]的差异
类增强
使用demo
TypeRegistry typeRegistry = getTypeRegistry("data.HelloWorld");
ReloadableType rtype = typeRegistry.addType("data.HelloWorld", loadBytesForClass("data.HelloWorld"));
runUnguarded(rtype.getClazz(), "greet");
rtype.loadNewVersion("000", bytes);
rtype.loadNewVersion("000", rtype.bytesInitial);
可重载原理解析
可重载的类进行增强,类增强代码过于复杂,可以直接dump增强后的类来理解原理。
- 生成了一个
ReloadableType对 象,赋值给类的静态变量的r$type ,ReloadableType 会监控类资源是否变更,如果变更以后会调用loadNewVersion 方法加载最新版本的字节类 - 在所有的方法前面注入了一段代码,如果类变更以后调用新版本字节类的方法,否则执行原来的代码, 对非静态变量进行了拦截判断
原来的类
package com.study;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ServiceTest {
private static final String NAME = "staticName";
private String name = "name";
@Autowired
private ServiceTest2 serviceTest2;
public String hello(){
String staticName = NAME;
String nonStaticName = this.name;
return "hello," + staticName + nonStaticName;
}
public String hello2() {
String hello = serviceTest2.hello2();
return hello;
}
}
增强后的类
package com.study;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springsource.loaded.C;
import org.springsource.loaded.ISMgr;
import org.springsource.loaded.ReloadableType;
import org.springsource.loaded.SSMgr;
import org.springsource.loaded.TypeRegistry;
@Service
public class ServiceTest {
public static ReloadableType r$type = TypeRegistry.getReloadableType(0, 516);
public transient ISMgr r$fields;
public static final SSMgr r$sfields;
public static String NAME = "staticName";
public String name;
@Autowired
public ServiceTest2 serviceTest2;
public Object r$get(Object paramObject, String paramString) {
if (this.r$fields == null)
this.r$fields = new ISMgr(this, r$type);
return this.r$fields.getValue(r$type, paramObject, paramString);
}
public void r$set(Object paramObject1, Object paramObject2, String paramString) {
if (this.r$fields == null)
this.r$fields = new ISMgr(this, r$type);
this.r$fields.setValue(r$type, paramObject2, paramObject1, paramString);
}
public static void r$sets(Object paramObject, String paramString) {
if (r$sfields == null)
r$sfields = new SSMgr();
r$sfields.setValue(r$type, paramObject, paramString);
}
public static Object r$gets(String paramString) {
if (r$sfields == null)
r$sfields = new SSMgr();
return r$sfields.getValue(r$type, paramString);
}
public static void ___clinit___() {
((ServiceTest__I)r$type.fetchLatest()).___clinit___();
}
public ServiceTest(C paramC) {}
public void ___init___() {
((ServiceTest__I)r$type.getLatestDispatcherInstance(true)).___init___(this);
}
public ServiceTest() {
if (this.r$fields == null)
this.r$fields = new ISMgr(this, r$type);
if (this.r$fields == null)
this.r$fields = new ISMgr(this, r$type);
if (TypeRegistry.instanceFieldInterceptionRequired(516, "name")) {
r$set("name", this, "name");
} else {
this.name = "name";
}
}
public String hello() {
if (r$type.changed(0) != 0) {
if (r$type.changed(0) != 1)
throw new NoSuchMethodError("com.red.study.ServiceTest.hello()Ljava/lang/String;");
return ((ServiceTest__I)r$type.fetchLatest()).hello(this);
}
r$type.changed(0);
String staticName = "staticName";
String nonStaticName = TypeRegistry.instanceFieldInterceptionRequired(516, "name") ? (String)r$get(this, "name") : this.name;
return "hello," + staticName + nonStaticName;
}
public String hello2() {
if (r$type.changed(1) != 0) {
if (r$type.changed(1) != 1)
throw new NoSuchMethodError("com.red.study.ServiceTest.hello2()Ljava/lang/String;");
return ((ServiceTest__I)r$type.fetchLatest()).hello2(this);
}
r$type.changed(1);
ServiceTest2 var2 = TypeRegistry.instanceFieldInterceptionRequired(516, "serviceTest2") ? (ServiceTest2)this.r$get(this, "serviceTest2") : this.serviceTest2;
String hello = TypeRegistry.ivicheck(524, "hello2()Ljava/lang/String;") ? (String)var2.__execute((Object[])null, var2, "hello2()Ljava/lang/String;") : var2.hello2();
return hello;
}
static {
if (r$sfields == null)
r$sfields = new SSMgr();
if (r$type.clinitchanged() != 0) {
((ServiceTest__I)r$type.fetchLatest()).___clinit___();
return;
}
}
public int hashCode() {
if (r$type.fetchLatestIfExists(2) != null)
return ((ServiceTest__I)r$type.fetchLatestIfExists(2)).hashCode(this);
r$type.fetchLatestIfExists(2);
return super.hashCode();
}
public boolean equals(Object paramObject) {
if (r$type.fetchLatestIfExists(3) != null)
return ((ServiceTest__I)r$type.fetchLatestIfExists(3)).equals(this, paramObject);
r$type.fetchLatestIfExists(3);
return super.equals(paramObject);
}
public Object clone() throws CloneNotSupportedException {
if (r$type.fetchLatestIfExists(4) != null)
return ((ServiceTest__I)r$type.fetchLatestIfExists(4)).clone(this);
r$type.fetchLatestIfExists(4);
return super.clone();
}
public String toString() {
if (r$type.fetchLatestIfExists(5) != null)
return ((ServiceTest__I)r$type.fetchLatestIfExists(5)).toString(this);
r$type.fetchLatestIfExists(5);
return super.toString();
}
public Object __execute(Object[] paramArrayOfObject, Object paramObject, String paramString) {
if (r$type.determineDispatcher(this, paramString) != null)
return r$type.determineDispatcher(this, paramString).__execute(paramArrayOfObject, this, paramString);
r$type.determineDispatcher(this, paramString);
throw new NoSuchMethodError(paramString);
}
}
插件
有两种插件LoadtimeInstrumentationPlugin 和ReloadEventProcessorPlugin
- LoadtimeInstrumentationPlugin 在类第一次加载的时候插桩增强,有accept和modify两个方法,一个用来判断是否需要修改类,一个用来修改类
- ReloadEventProcessorPlugin 在类重新加载的时候处理,有shouldRerunStaticInitializer和reloadEvent方法,一个用来判断是否需要重新初始化类的静态变量和静态方法,一个用来处理重新加载的事件
JVMPlugin插件
如果加载了java/beans/Introspector ,introspectorLoaded = true 如果加载了java/beans/ThreadGroupContext 类,threadGroupContextLoaded = true 当类被重新加载的时候
- 如果threadGroupContextLoaded=true, clearThreadGroupContext
- 如果introspectorLoaded = true会清理Introspector的缓存
SpringPlugin插件
- 插桩增强
在AnnotationMethodHandlerAdapter 、RequestMappingHandlerMapping 、LocalVariableTableParameterNameDiscoverer 等构造函数初始化的时候将bean实例对象注册到SpringPlugin 的静态变量List中 bytesWithInstanceCreationCaptured 方法可以在类字节的构造函数后面追加调用另外一个类的一个方法的代码
private byte[] bytesWithInstanceCreationCaptured(byte[] bytes, String classToCall, String methodToCall) {
ClassReader cr = new ClassReader(bytes);
ClassVisitingConstructorAppender ca = new ClassVisitingConstructorAppender(classToCall, methodToCall);
cr.accept(ca, 0);
byte[] newbytes = ca.getBytes();
return newbytes;
}
- 更新reloadEvent,清理所有的缓存, 重新构建HandlerMapping
public void reloadEvent(String typename, Class<?> clazz, String versionsuffix) {
removeClazzFromMethodResolverCache(clazz);
removeClazzFromDeclaredMethodsCache(clazz);
clearCachedIntrospectionResults(clazz);
reinvokeDetectHandlers();
reinvokeInitHandlerMethods();
clearLocalVariableTableParameterNameDiscovererCache(clazz);
}
CglibPlugin插件
- 对以
/cglib/core/AbstractClassGenerator 结尾的类才进行增强修改,包括net/sf/cglib/core/AbstractClassGenerator 和org/springframework/cglib/core/AbstractClassGenerator 等 - 调用
CglibPluginCapturing.catchGenerate(bytes) 进行字节修改
如何使用监听类变更
在FileSystemWatcher中启动了一个单线程,循环遍历监听类资源,通过修改时间判断类是否有变更,每个可重载的类都会添加一个监听文件到这个类中
为什么Spring不需要重新初始化bean
因为所有可重载的类都被增强了,实例对象的方法都进行了改写,如果新新版本类产生则执行新版本的类方法,入口还是原来实例对象,所以重载事件中只做了清理缓存的操作
版本不断更新后如何防止内存泄露
核心就是前面讲到的ChildClassLoader 和TypeRegistry#checkChildClassLoader
ChildClassLoader 用的是一个空URL数组,没有任何URL引用ChildClassLoader 用的是WeakReference弱引用,可以被gc回收ChildClassLoader 有一个类加载计数变量definedCount统计所有被该累加载器加载的类个数,只有一个类产生了新版本的时候才会使用ChildClassLoader 进行加载,typeRegistry.defineClass 方法的第三个传参permanent如果为true使用默认classLoader加载,如果为false才使用ChildClassLoader 加载,可重载类在第一次加载的时候用传的是true,加载新版本的时候传的是falseTypeRegistry#checkChildClassLoader 在每次加载新版本类的时候会判断加载类的个数是否超过maxClassDefinitions (默认500,可以通过参数修改),如果超过以后重新创建一个ChildClassLoader ,原来的弱引用指向新建的这个ChildClassLoader ,同时将已经加载的每个类引用清空,这样老的那个ChildClassLoader 及其加载的所有类会等待系统gc
public void checkChildClassLoader(ReloadableType currentlyDefining) {
ChildClassLoader ccl = childClassLoader == null ? null : childClassLoader.get();
int definedCount = (ccl == null ? 0 : ccl.getDefinedCount());
long time = System.currentTimeMillis();
if (definedCount > maxClassDefinitions && ((time - lastTidyup) > 5000)) {
lastTidyup = time;
ccl = new ChildClassLoader(classLoader.get());
this.childClassLoader = new WeakReference<ChildClassLoader>(ccl);
for (int i = 0; i < reloadableTypesSize; i++) {
ReloadableType rtype = reloadableTypes[i];
if (rtype != null && rtype != currentlyDefining) {
rtype.clearClassloaderLinks();
rtype.reloadMostRecentDispatcherAndExecutor();
}
}
for (int i = 0; i < reloadableTypesSize; i++) {
ReloadableType rtype = reloadableTypes[i];
if (rtype != null && rtype != currentlyDefining && rtype.hasBeenReloaded()) {
if (rtype.getLiveVersion().staticInitializedNeedsRerunningOnDefine) {
rtype.runStaticInitializer();
}
}
}
int count = ccl.getDefinedCount() + 3;
if (count > maxClassDefinitions) {
maxClassDefinitions = count;
}
}
}
|