Javassist埋点做性能监控

埋点实现在方法前后动态插入代码,获取方法的执行时间。

常见的方法有以下3钟:

1 硬编码

2 spirng aop 动态代理

3  动态插入字节码

其中 1 和 2 系统代码侵入性大,方法3不用更改系统代码。

javaAgent技术

JavaAgent是从JDK1.5及以后引入的,在1.5之前无法使用,也可以叫做java代理。利用 java代理,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。

使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。Instrumentation 的*大作用,就是类定义动态改变和操作。

开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 -javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。开发者可以让 Instrumentation 代理在 main 函数运行前执行premain函数。

基本步骤:

1  编写premian函数

2  将监控程序打包jar,META-INF/MAINIFEST.MF 必须包含 Premain-Class

3  使用java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]运行被监控的程序

新建项目JAgent

1 增加pom依赖

<dependency>
<groupId>jboss</groupId>
<artifactId>javassist</artifactId>
<version>3.8.0.GA</version>
</dependency>
2 编写permian函数

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class JAgent {

public static void main(String[] args) {
System.out.println(“main”);
}

/**
* 在这个 premain 函数中,开发者可以进行对类的各种操作。
* @param agentOps
* agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,
* 这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
* @param inst
* 是一个 java.lang.instrument.Instrumentation 的实例,
* 由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
* 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
*/
public static void premain(String agentOps, Instrumentation inst) {

System.out.println(“premain:”+agentOps);

inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

//判断要埋点的类
if(!”com/chy/JSercice”.equals(className)) {
return null;
}

try {
ClassPool classPool=new ClassPool();
classPool.insertClassPath(new LoaderClassPath(loader));
CtClass ctClass= classPool.get(className.replace(“/”,”.”));
CtMethod ctMethod= ctClass.getDeclaredMethod(“run”);

//插入本地变量
ctMethod.addLocalVariable(“begin”,CtClass.longType);
ctMethod.addLocalVariable(“end”,CtClass.longType);

ctMethod.insertBefore(“begin=System.currentTimeMillis();System.out.println(\”begin=\”+begin);”);
//前面插入:*后插入的放*上面
ctMethod.insertBefore(“System.out.println( \”埋点开始-2\” );”);
ctMethod.insertBefore(“System.out.println( \”埋点开始-1\” );”);

ctMethod.insertAfter(“end=System.currentTimeMillis();System.out.println(\”end=\”+end);”);
ctMethod.insertAfter(“System.out.println(\”性能:\”+(end-begin)+\”毫秒\”);”);

//后面插入:*后插入的放*下面
ctMethod.insertAfter(“System.out.println( \”埋点结束-1\” );”);
ctMethod.insertAfter(“System.out.println( \”埋点结束-2\” );”);
return ctClass.toBytecode();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
}
catch (IOException e){
e.printStackTrace();
}

return new byte[0];
}
});
}
}
3 打包jar

<build>
<plugins>

<!–编译Java源码–>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>

<!– 打成jar时,设定manifestEntries的参数 –>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifestEntries>
<Project-name>${project.name}</Project-name>
<Project-version>${project.version}</Project-version>
<Premain-Class>com.chy.JAgent</Premain-Class>
<Can-Redefine-Classes>false</Can-Redefine-Classes>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>

<!– 方法1: 包含所有依赖的jar文件,依赖以class的方式存在 –>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.2.1</version>
<configuration>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation=”org.apache.maven.plugins.shade.resource.ManifestResourceTransformer”>
<mainClass>com.chy.JAgent</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

</plugins>
</build>
<Premain-Class>com.chy.JAgent</Premain-Class> 指定 premain 函数入口

%title插图%num
确保 META-INF/MAINIFEST.MF 必须包含 Premain-Class

新建项目JAgentTest (埋点项目)

public class App
{
public static void main( String[] args )
{
System.out.println( “JAgentTest is run” );

// run中埋点统计运行时间
new JSercice().run();
}
}
public class JSercice {

public void call() {
String name = “JSercice”;
for (int j = 1; j <= 10000; j++) {
System.out.println(j);
}
System.out.println(name + ” is end”);
}

public void run() {
System.out.println(“JSercice is start”);
call();
}
}
配置项目 vm 参数

%title插图%num

javaagent:G:\java\intellij_idea\IdeaProjects\javaByteCode\JAgent\target\JAgent-1.0-SNAPSHOT.jar=JAgent

运行JAgentTest 打印结果如下

premain:JAgent
JAgentTest is run
埋点开始-1
埋点开始-2
begin=1530337378023

JSercice is start

……….

JSercice is end
end=1530337378024
性能:1毫秒
埋点结束-1
埋点结束-2

Javassist简单应用小结

Javassist是一款字节码编辑工具,可以直接编辑和生成Java生成的字节码,以达到对.class文件进行动态修改的效果。熟练使用这套工具,可以让Java编程更接近与动态语言编程。
下面一个方法的目的是获取一个类加载器(ClassLoader),以加载指定的.jar或.class文件,在之后的代码中会使用到。

%title插图%num
private static ClassLoader getLocaleClassLoader() throws Exception {
List<URL> classPathURLs = new ArrayList<>();
// 加载.class文件路径
classPathURLs.add(classesPath.toURI().toURL());

// 获取所有的jar文件
File[] jarFiles = libPath.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(“.jar”);
}
});
Assert.assertFalse(ObjectHelper.isArrayNullOrEmpty(jarFiles));

// 将jar文件路径写入集合
for (File jarFile : jarFiles) {
classPathURLs.add(jarFile.toURI().toURL());
}

// 实例化类加载器
return new URLClassLoader(classPathURLs.toArray(new URL[classPathURLs.size()]));
}

获取类型信息

%title插图%num
@Test
public void test() throws NotFoundException {
// 获取默认类型池对象
ClassPool classPool = ClassPool.getDefault();

// 获取指定的类型
CtClass ctClass = classPool.get(“java.lang.String”);

System.out.println(ctClass.getName()); // 获取类名
System.out.println(“\tpackage ” + ctClass.getPackageName()); // 获取包名
System.out.print(“\t” + Modifier.toString(ctClass.getModifiers()) + ” class ” + ctClass.getSimpleName()); // 获取限定符和简要类名
System.out.print(” extends ” + ctClass.getSuperclass().getName()); // 获取超类
// 获取接口
if (ctClass.getInterfaces() != null) {
System.out.print(” implements “);
boolean first = true;
for (CtClass c : ctClass.getInterfaces()) {
if (first) {
first = false;
} else {
System.out.print(“, “);
}
System.out.print(c.getName());
}
}
System.out.println();
}

修改类方法

%title插图%num
@Test
public void test() throws Exception {
// 获取本地类加载器
ClassLoader classLoader = getLocaleClassLoader();
// 获取要修改的类
Class<?> clazz = classLoader.loadClass(“edu.alvin.reflect.TestLib”);

// 实例化类型池对象
ClassPool classPool = ClassPool.getDefault();
// 设置类搜索路径
classPool.appendClassPath(new ClassClassPath(clazz));
// 从类型池中读取指定类型
CtClass ctClass = classPool.get(clazz.getName());

// 获取String类型参数集合
CtClass[] paramTypes = {classPool.get(String.class.getName())};
// 获取指定方法名称
CtMethod method = ctClass.getDeclaredMethod(“show”, paramTypes);
// 赋值方法到新方法中
CtMethod newMethod = CtNewMethod.copy(method, ctClass, null);
// 修改源方法名称
String oldName = method.getName() + “$Impl”;
method.setName(oldName);

// 修改原方法
newMethod.setBody(“{System.out.println(\”执行前\”);” + oldName + “($$);System.out.println(\”执行后\”);}”);
// 将新方法添加到类中
ctClass.addMethod(newMethod);

// 加载重新编译的类
clazz = ctClass.toClass(); // 注意,这一行会将类冻结,无法在对字节码进行编辑
// 执行方法
clazz.getMethod(“show”, String.class).invoke(clazz.newInstance(), “hello”);
ctClass.defrost(); // 解冻一个类,对应freeze方法
}

动态创建类

%title插图%num
@Test
public void test() throws Exception {
ClassPool classPool = ClassPool.getDefault();

// 创建一个类
CtClass ctClass = classPool.makeClass(“edu.alvin.reflect.DynamiClass”);
// 为类型设置接口
//ctClass.setInterfaces(new CtClass[] {classPool.get(Runnable.class.getName())});

// 为类型设置字段
CtField field = new CtField(classPool.get(String.class.getName()), “value”, ctClass);
field.setModifiers(Modifier.PRIVATE);
// 添加getter和setter方法
ctClass.addMethod(CtNewMethod.setter(“setValue”, field));
ctClass.addMethod(CtNewMethod.getter(“getValue”, field));
ctClass.addField(field);

// 为类设置构造器
// 无参构造器
CtConstructor constructor = new CtConstructor(null, ctClass);
constructor.setModifiers(Modifier.PUBLIC);
constructor.setBody(“{}”);
ctClass.addConstructor(constructor);
// 参数构造器
constructor = new CtConstructor(new CtClass[] {classPool.get(String.class.getName())}, ctClass);
constructor.setModifiers(Modifier.PUBLIC);
constructor.setBody(“{this.value=$1;}”);
ctClass.addConstructor(constructor);
// 为类设置方法
CtMethod method = new CtMethod(CtClass.voidType, “run”, null, ctClass);
method.setModifiers(Modifier.PUBLIC);
method.setBody(“{System.out.println(\”执行结果\” + this.value);}”);
ctClass.addMethod(method);

// 加载和执行生成的类
Class<?> clazz = ctClass.toClass();
Object obj = clazz.newInstance();
clazz.getMethod(“setValue”, String.class).invoke(obj, “hello”);
clazz.getMethod(“run”).invoke(obj);

obj = clazz.getConstructor(String.class).newInstance(“OK”);
clazz.getMethod(“run”).invoke(obj);
}

创建代理类

%title插图%num
@Test
public void test() throws Exception {
// 实例化代理类工厂
ProxyFactory factory = new ProxyFactory();

//设置父类,ProxyFactory将会动态生成一个类,继承该父类
factory.setSuperclass(TestProxy.class);

//设置过滤器,判断哪些方法调用需要被拦截
factory.setFilter(new MethodFilter() {
@Override
public boolean isHandled(Method m) {
return m.getName().startsWith(“get”);
}
});

Class<?> clazz = factory.createClass();
TestProxy proxy = (TestProxy) clazz.newInstance();
((ProxyObject)proxy).setHandler(new MethodHandler() {
@Override
public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
//拦截后前置处理,改写name属性的内容
//实际情况可根据需求修改
System.out.println(thisMethod.getName() + “被调用”);
try {
Object ret = proceed.invoke(self, args);
System.out.println(“返回值: ” + ret);
return ret;
} finally {
System.out.println(thisMethod.getName() + “调用完毕”);
}
}
});

proxy.setName(“Alvin”);
proxy.setValue(“1000”);
proxy.getName();
proxy.getValue();
}
其中,TestProxy类内容如下:

%title插图%num
public class TestProxy {
private String name;
private String value;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

获取方法名称

%title插图%num
@Test
public void test() throws Exception {
// 获取本地类加载器
ClassLoader classLoader = getLocaleClassLoader();
// 获取要修改的类
Class<?> clazz = classLoader.loadClass(“edu.alvin.reflect.TestLib”);

// 实例化类型池
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new ClassClassPath(clazz));
CtClass ctClass = classPool.get(clazz.getName());

// 获取方法
CtMethod method = ctClass.getDeclaredMethod(“show”, ObjectHelper.argumentsToArray(CtClass.class, classPool.get(“java.lang.String”)));
// 判断是否为静态方法
int staticIndex = Modifier.isStatic(method.getModifiers()) ? 0 : 1;

// 获取方法的参数
MethodInfo methodInfo = method.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute localVariableAttribute = (LocalVariableAttribute)codeAttribute.getAttribute(LocalVariableAttribute.tag);

for (int i = 0; i < method.getParameterTypes().length; i++) {
System.out.println(“第” + (i + 1) + “个参数名称为: ” + localVariableAttribute.variableName(staticIndex + i));
}
}
关于“获取方法名称”,其主要作用是:当Java虚拟机加载.class文件后,会将类方法“去名称化”,即丢弃掉方法形参的参数名,而是用形参的序列号来传递参数。如果要通过Java反射获取参数的参数名,则必须在编辑是指定“保留参数名称”。Javassist则不存在这个问题,对于任意方法,都能正确的获取其参数的参数名。
Spring MVC就是通过方法参数将请求参数进行注入的,这一点比struts2 MVC要方便很多,Spring也是借助了Javassist来实现这一点的。