最近项目开发需求中,使用了第三方供应商提供的jar包形式的sdk ,sdk中的日志由其自己管理打印,现在想获取到日志打印时传入的message,就必须想办法对sdk的源码进行改动。
首先想到的是反编译jar包,然后修改后重新打包,尝试了一下后感觉很麻烦,而且很不cool。后来就查到了javaassist工具可以完美解决这个问题,可以实现我们熟悉的AOP功能,记录下使用过程。
javaassist是通过类加载层面,通过修改class文件代替原来的class来实现我们的目的的。
首先建立一个agent代理项目,pom结构如下:
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.23.1-GA</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>5.1</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.5.7</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.5.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<artifactSet>
<includes>
<include>javassist:javassist:jar:</include>
<include>net.bytebuddy:byte-buddy:jar:</include>
<include>net.bytebuddy:byte-buddy-agent:jar:</include>
</includes>
</artifactSet>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
<resource>
<directory>${basedir}/src/main/java</directory>
</resource>
</resources>
</build>
需要注意的是我们需要建立src/main/resources/META-INF/MANIFEST.MF才能使这个项目打出的jar包正确运行,内容如下:
Manifest-Version: 1.0
Premain-Class: com.xxx.agent.LogAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist.jar
Premain-Class是我们项目中要建立的主类的名称,内容如下:
public class LogAgent {
public static void premain(String agentArgs, Instrumentation inst) {
ClassFileTransformer transformer = new FileSdkLoggerTransformer();
inst.addTransformer(transformer);
}
}
premain是在你要代理的项目的main函数之前执行的,在主项目执行之前就已经把我们的类给修改掉了。
FileSdkLoggerTransformer类是我们自己建的转化类,用来拦截修改原项目中的类。
public class FileSdkLoggerTransformer implements ClassFileTransformer {
private static final Set<String> classNameSet = new HashSet<>();
static {
classNameSet.add("com.sportradar.sdk.common.classes.FileSdkLogger");
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
if(className != null){
String currentClassName = className.replaceAll("/",".");
if(!classNameSet.contains(currentClassName)) return null;
ClassPool.getDefault().importPackage("com.xxx.betradar");
CtClass ctClass = ClassPool.getDefault().get(currentClassName);
CtBehavior[] declaredBehaviors = ctClass.getMethods();
for (CtBehavior ctBehavior:declaredBehaviors) {
if(ctBehavior.getName().equals("logTraffic")){
CtClass[] parameterTypes = ctBehavior.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
System.out.println(parameterTypes[i].toString());
}
//增强方法
//ctBehavior.insertAt(0,"{ SDKStarter.getXmlQueue().add($1); }");
ctBehavior.insertAt(0,"{ SDKStarter.getXmlQueue().add(message); }");
}
}
return ctClass.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
这里要注意,如果要使用代理方法的参数,可以用$1这种方式,1表示参数占位符index,也可以直接用参数名,比如代码中的message。
如果要在代理方法中使用别的类,比如自己写的代码SDKStarter,需要将这个类的路径引入,相当于import,ClassPool.getDefault().importPackage("com.xxx.betradar"); 。
运行原项目时,加上-javaagent:xxx.jar即可,xxx.jar是代理项目打出来的jar包。