-
Notifications
You must be signed in to change notification settings - Fork 8
Java Agent
Java
从1.5
提供了java.lang.instrument
包,用于给JVM
植入java
编程语言编写的Instrumentation
,这种通过java.lang.instrument
实现工具被称为java agent
。java agent
可以在JVM
加载class
时候修改目标字节码,或者修改已经加载的class
的信息(已经加载的class
不会再次初始化,可以调整方法体,但是不能添加/删除/重命名字段或方法,不能修改方法的签名或者更改继承)。
目前java agent
支持两种模式加载,启动时加载,通过在JVM
启动时配置-javaagent:jarpath[=options]
,还有一种通过Attach API
动态加载java agent
。
目前基于java agent
实现应用的工具有很多,例如SkyWalking(APM)
,Arthas
(诊断),ChaosBlade
(混沌注入),JaCoCo
(代码覆盖率)等等。
JVM
命令行启动配置模式:
首先命令行需要增加-javaagent:jarpath[=options]
参数,jarpath
为java agent Jar
包的路径,agent Jar
中的manifest
文件必须包含Premain-Class
属性,属性值为agent class
,该类必须包含premain
方法,premain
方法可以有两种形式:
public static void premain(String agentArgs, Instrumentation inst); (两个方法同时出现时,该方法优先级较高)
public static void premain(String agentArgs);
java agent
由system class loader
负责加载(ClassLoader.getSystemClassLoader)
,所以在premain
阶段,很多时候是看不到当前目标程序引用的资源的。通过agentArgs
可以获取到Agent
配置的options
,具体参数的解析需要由Agent
自身解析。
JVM
启动后Attach
模式:
在目标JVM
启动后attach
上去,agent Jar
中的manifest
文件必须包含Agent-Class
属性,属性值为agent class
,该类必须包含agentmain
方法,agentmain
方法可以有两种形式:
public static void agentmain(String agentArgs, Instrumentation inst);(两个方法同时出现时,该方法优先级较高)
public static void agentmain(String agentArgs);
attach
模式java agent
也是由system class loader
负责加载(ClassLoader.getSystemClassLoader)
。
目标代码:
package io.manbang.asm.demo;
/**
* @author weilong.hu
* @since 2021/11/23 14:17
*/
public class CargoTest {
public static void main(String[] args){
publishCargo();
}
/**
* 发货
*/
public static void publishCargo() {
System.out.println("publish cargo success...");
}
}
java agent
代码
package io.manbang.asm.demo;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @author weilong.hu
* @since 2021/09/24 10:14
*/
public class Premain {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if ("io/manbang/asm/demo/CargoTest".equals(className)) {
final ClassReader classReader = new ClassReader(classfileBuffer);
final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理
final ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
final byte[] data = classWriter.toByteArray();
return data;
}
return null;
}
});
}
public static class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
//Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
if (name.equals("publishCargo") && mv != null) {
mv = new MyMethodVisitor(mv);
}
return mv;
}
class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start.......");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end.......");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}
}
pom
配置
<!--增加asm依赖-->
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>9.2</version>
</dependency>
</dependencies>
<!--增加manifest配置,也可以自己手动配置文件-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>
io.manbang.asm.demo.Premain
</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
无论是命令行启动测试用例,还是在ide
中启动测试用例,在VM Options
中加上-javaagent:jarpath[=options]
配置,示例如下:指向java agent jar
包
-javaagent:your\path\asm-demo-1.0-SNAPSHOT.jar
启动后命令行CargoTest
的main
方法后,控制台输出如下:
start.......
publish cargo success...
end.......
可以看到相关的织入逻辑已经成功织入目标class
中,这个简单的示例只是演示agent
如何使用,还有一些其他的问题没有考虑,比如agent
由system class loader
负责加载,如果用户也引入了asm
相关包,如何解决类的冲突,与用户资源如何隔离,如何发现用户的资源等等问题。
字节码结构,字节码指令解析相关文章较多,这里不在赘述,可以参考相关文章:字节码简介 ,官方文档。
基于字节码操作衍生出的框架有很多,主流的有ASM
、Apache BCEL
、Javassist
、cglib
、Byte Buddy
等等(JDK
动态代理,如果从动态创建class
方面来说,也可以当作JDK
自带的一个字节码框架), 各自有各自的特性,资料也很多,这里不在赘述。字节码操作应用范围在Spring AOP
,ORM
框架,热部署,诊断检测框架中都很常见,甚至一些大家写的自研中间件中也很常见。
如果不讨论字节码操作方面,从字节码本身应用场景出发,字节码不仅作为JVM
加载class
的基石,还有其他很多应用场景,比如大家在开发中间件框架时,绝大多数情况,对于目标class
的判断更多是通过反射实现,基于反射,可以获取一个class
的绝大部分信息,在绝大部分场景下是没有问题的,但是对于一个面向管理生命周期的框架,通过反射可能不是最优解决方式。
比如Servlet3.0
中的ServletContainerInitializer
,通过在实现类上配置@HandlesTypes
注解,即可在Servlet
启动阶段获取@HandlesTypes
注解上标注的类的实现类。比如spring
中的SpringServletContainerInitializer
就实现了ServletContainerInitializer
。
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
......
}
}
这时候用户实现的WebApplicationInitializer
的相关class
,不需要配置SPI(Service Provider Interface)
,也没有APT(Annotation Processing Tool)
做相关处理,也没有其他相关配置,但是在启动阶段,可以被发现处理,这时候需要研究下是如何实现的。
可以参考tomcat
对于ServletContainerInitializer
的处理方式,org.apache.catalina.startup.ContextConfig#checkHandlesTypes
protected void checkHandlesTypes(JavaClass javaClass,
Map<String,JavaClassCacheEntry> javaClassCache) {
......
populateJavaClassCache(className, javaClass, javaClassCache);
JavaClassCacheEntry entry = javaClassCache.get(className);
if (entry.getSciSet() == null) {
try {
populateSCIsForCacheEntry(entry, javaClassCache);
} catch (StackOverflowError soe) {
throw new IllegalStateException(sm.getString(
"contextConfig.annotationsStackOverflow",
context.getName(),
classHierarchyToString(className, entry, javaClassCache)));
}
}
......
}
private void populateJavaClassCache(String className, JavaClass javaClass,
Map<String,JavaClassCacheEntry> javaClassCache) {
if (javaClassCache.containsKey(className)) {
return;
}
// Add this class to the cache
javaClassCache.put(className, new JavaClassCacheEntry(javaClass));
populateJavaClassCache(javaClass.getSuperclassName(), javaClassCache);
for (String interfaceName : javaClass.getInterfaceNames()) {
populateJavaClassCache(interfaceName, javaClassCache);
}
}
private void populateJavaClassCache(String className,
Map<String,JavaClassCacheEntry> javaClassCache) {
if (!javaClassCache.containsKey(className)) {
String name = className.replace('.', '/') + ".class";
try (InputStream is = context.getLoader().getClassLoader().getResourceAsStream(name)) {
if (is == null) {
return;
}
ClassParser parser = new ClassParser(is);
JavaClass clazz = parser.parse();
populateJavaClassCache(clazz.getClassName(), clazz, javaClassCache);
} catch (ClassFormatException e) {
log.debug(sm.getString("contextConfig.invalidSciHandlesTypes",
className), e);
} catch (IOException e) {
log.debug(sm.getString("contextConfig.invalidSciHandlesTypes",
className), e);
}
}
}
org.apache.tomcat.util.bcel.classfile.ClassParser#parse
相关代码:
public JavaClass parse() throws IOException, ClassFormatException {
/****************** Read headers ********************************/
// Check magic tag of class file
readID();
// Get compiler version
readVersion();
/****************** Read constant pool and related **************/
// Read constant pool entries
readConstantPool();
// Get class information
readClassInfo();
// Get interface information, i.e., implemented interfaces
readInterfaces();
/****************** Read class fields and methods ***************/
// Read class fields, i.e., the variables of the class
readFields();
// Read class methods, i.e., the functions in the class
readMethods();
// Read class attributes
readAttributes();
// Return the information we have gathered in a new object
return new JavaClass(class_name, superclass_name,
access_flags, constant_pool, interface_names,
runtimeVisibleAnnotations);
}
private void readID() throws IOException, ClassFormatException {
if (dataInputStream.readInt() != MAGIC) {
throw new ClassFormatException("It is not a Java .class file");
}
}
可以看到class
的解析并不是直接通过反射获取关键信息,是通过BCEL
的ClassParser
获取class
字节流,解析字节码,获取关键信息。像这种解析字节码方式获取关键信息,而不是通过很常见常用的反射方式,可能是 1.为了避免加载过多无用的class
,占用内存空间 2.避免class
的提前初始化,防止提前触发一些不必要逻辑。
例如spring
中也有很多解析字节码的操作,比如常见的org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
中的使用:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
......
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
......
}
//org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
......
return scanCandidateComponents(basePackage);
......
}
//org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
......
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
......
}
//org.springframework.core.type.classreading.SimpleMetadataReaderFactory#getMetadataReader(org.springframework.core.io.Resource)
@Override
public MetadataReader getMetadataReader(Resource resource) throws IOException {
return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader());
}
//org.springframework.core.type.classreading.SimpleMetadataReader#SimpleMetadataReader
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
getClassReader(resource).accept(visitor, PARSING_OPTIONS);
this.resource = resource;
this.annotationMetadata = visitor.getMetadata();
}
private static ClassReader getClassReader(Resource resource) throws IOException {
try (InputStream is = resource.getInputStream()) {
try {
return new ClassReader(is);
}
catch (IllegalArgumentException ex) {
throw new NestedIOException("ASM ClassReader failed to parse class file - " +
"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
}
}
}
上述代码可以看到spring
使用asm
的ClassReader
,使用visitor
模式,解析目标类。
正因为使用这种方式,对于常见的@Controller
,@Service
,@Repository
,@Component
的注解判断其实也是通过字符串,而不是通过反射获取目标注解,然后进行equals
判断。
//org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#registerDefaultFilters
protected void registerDefaultFilters() {
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
......
}
//org.springframework.core.type.filter.AnnotationTypeFilter#matchSelf
@Override
protected boolean matchSelf(MetadataReader metadataReader) {
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
return metadata.hasAnnotation(this.annotationType.getName()) ||
(this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
}
通过上述步骤,spring
就可以既完成了BeanDefinition
的扫描,不用提前初始化class
(因为反射可能会导致当前class
引用的其他class
初始化),又排除了无效class
的提前初始化。
例如spring aop
配置的切点,对于注解成配置的@Retention(RetentionPolicy.CLASS)
级别的(CLASS
级别只存在于class
文件中,反射获取不到,RUNTIME
级别存在于运行时,可以反射获取到),依旧可以命中该切点,正是因为使用的字节码解析,而不是通过反射。
所以对于一个管理生命周期的框架来说,解析字节码可能是个很常见的操作,对于常见的中间件组件,直面字节码解析可能较少,通过反射会更常见一些。
常规看class
的字节码很简单,ide
中就可以看到,如果有源码包,在ide
中甚至可以直接看一个第三方class
的源码。但是对于运行时生成的class
,就没办法直接看到字节码了。
对于JDK动态代理/cglib
可以通过配置-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true / -Dcglib.debugLocation=PATH
获取动态生成的class
,但是限制大,只能获取各自场景的,不能很随意。
一种方式是通过java agent
,利用Instrumentation和ClassFileTransformer
可以获取到类的字节码了,但是对于已经加载过的类,如果想获取的话,必须触发java.lang.instrument.Instrumentation#retransformClasses
,才能重新获取到字节码,,而且如果有多个java agent
,因为顺序原因,可能获取到的字节码不是最终字节码。
还有一种方式使用sa-jdi.jar
无侵入,直接获取到目标想要的字节码。
java -classpath ".;./bin;%JAVA_HOME%/lib/sa-jdi.jar" -Dsun.jvm.hotspot.tools.jcore.outputDir=D:/dump -Dsun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList=com.ymm sun.jvm.hotspot.tools.jcore.ClassDump 28984
通过这种方式,就可以直接在相应目录下,直接获取到正在运行JVM中的class的字节码了
如果还有一些特殊的过滤需求,比如目前这种方式如果在两个ClassLoader
中有两个相同的class
类名,会出现文件覆盖现象,可以自己定义导出规则。
<!--首先项目中引入sa-jdi.jar-->
<dependency>
<groupId>jdk.tools</groupId>
<artifactId>jdk.tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${JAVA_HOME}/lib/sa-jdi.jar</systemPath>
</dependency>
import sun.jvm.hotspot.oops.InstanceKlass;
import sun.jvm.hotspot.tools.jcore.ClassFilter;
public class MyFilter implements ClassFilter {
@Override
public boolean canInclude(InstanceKlass kls) {
if(kls.getClassLoader()!=null){
final String s = kls.getClassLoader().getKlass().getName().asString();
if(!s.contains("AppClassLoader")){
return false;
}
}
String klassName = kls.getName().asString();
return klassName.startsWith("com/dianping/lion/client");
}
}
上述命令行再新增-Dsun.jvm.hotspot.tools.jcore.filter=MyFilter
配置,就可以使用自定义过滤逻辑,导出AppClassLoader
下的com/dianping/lion/client
包下的类。甚至可以复制sun.jvm.hotspot.tools.jcore.ClassDump
的代码,自定义一些导出逻辑配置,翻看sun.jvm.hotspot.tools.jcore.ClassDump
源码,可以看到是如何找到过滤器的。
//MyClassDump#run
public void run() {
......
dirName = System.getProperty("sun.jvm.hotspot.tools.jcore.filter", "sun.jvm.hotspot.tools.jcore.PackageNameFilter");
try {
Class filterClass = Class.forName(dirName);
if (this.pkgList == null) {
this.classFilter = (ClassFilter) filterClass.newInstance();
} else {
Constructor con = filterClass.getConstructor(String.class);
this.classFilter = (ClassFilter) con.newInstance(this.pkgList);
}
} catch (Exception var5) {
System.err.println("Warning: Can not create class filter!");
}
......
}
//sun.jvm.hotspot.tools.jcore.PackageNameFilter#PackageNameFilter()
public PackageNameFilter() {
this(System.getProperty("sun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList"));
}
//sun.jvm.hotspot.tools.jcore.PackageNameFilter#canInclude
public boolean canInclude(InstanceKlass kls) {
String klassName = kls.getName().asString().replace('/', '.');
int len = this.pkgList.length;
if (len == 0) {
return true;
} else {
for(int i = 0; i < len; ++i) {
if (klassName.startsWith((String)this.pkgList[i])) {
return true;
}
}
return false;
}
}
manifest:
https://docs.oracle.com/javase/10/docs/specs/jar/jar.html#jar-manifest
https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html
Java Agent:
https://nullwy.me/2018/10/java-agent/
bytecode:
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
https://dzone.com/articles/introduction-to-java-bytecode