最近导师公司需要有些帮忙的地方,就去来帮忙,结果甩手一个需求JVM的小钩子程序,要求能对固定类型的固定的方法进行获取其中的参数,打印出来到其他日志的地方,而且不能修改别人的jar包。喵喵喵?什么鬼,我之前只是写JAVAEE的啊,接到需求后一脸懵。不过还好,我们可以google和度娘。
一、确定问题
我们在这里要实现的是一个小钩子程序,而且我们没有任何JVM小钩子的经验,那么根据已有的知识,无非是使用静态的代理模式或者是java jdk的动态代理,或者是使用ASM,cglib等方法来进行动态使用。不过好的是spring,mybatis有成功使用的经验,那么我们无非就是使用的简单粗暴点,肯定是能够成功进行下去的只是是一个时间和人力的问题罢了。公司(创业公司)里的软件研发部门关于java开发的就只有我一个人,一方面维护一个原来完全自己写的平台的代码,一边写着自己的毕业论文,一边研究这个新的问题,准备新的项目。
二、知识准备
因为完全没有任何经验,所以在这里就开始对相关的知识进行搜索。
首先大行其道的可能能实现我们功能的是cglib,然后我们往下挖一层可以是使用ASM,再不然直接使用JDK本身提供的Instrument,越往下挖,可做的东西的范围越广,同时其学习成本越高,封装好的东西越少。具体需要到什么程度要看开发人员本身的能力和时间以及软件开发经验。
我个人的学习顺序是从高向低进行学习,首先是cglib这个东西学习比较其他两个简单而且封装也比较完善,我们此处不多做介绍,这里介绍的是Instrument简单使用的方法。
ASM是java的字节码控制框架可以动态的生成字节码,为什么要动态的生成字节码?我个人理解上是这样的,我们进行软件开发的过程一般都是在进行纵向的开发,以SSM为例,经过springMVC,Spring,Mybatis的粘合,转换为了对持久化数据的操作。那么当我们需要进行横向开发的时候一般是进行静态代理,使用适配器或者装饰器的设计模式就可以完成对应的功能,当我们大批量进行类似操作的时候就需要进行横向的扩展,这种情况下我们就需要一个横向的框架,spring的AOP的概念也就很好的诠释了为什么进行横向扩展。
在JAVA中Instrument最早可以追溯到JDK1.5版本,在这里就提出了java.lang.instrument,这个东西是个好东西,我们可以利用它直接进行对class文件的操作,当然也需要掌握理解一些java类相关的基本概念。这里不在赘述更多东西了,我们开始进行小的demo的实验吧。
三、基础准备
首先我们需要进行基本的内容准备,包括我们实验用的interface和impl这里就简单给出对应的内容吧。
3.1 Pom.xml依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.23.1-GA</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
这里我们只需要使用javassist进行辅助开发就可以了,common-lang是用来进行一些校验使用的
3.2 测试用类型
public interface TestInterface {
void testMethod();
void testMethod(String name);
void testMethod(int name);
void testMethod(float name);
void testMethod(String name, String desc);
}
这是个接口,接口中进行了一些重载,方便我们进行测试获得数据。
public class TestObject implements TestInterface {
@Override
public void testMethod() {
System.out.println("empty method");
System.out.println("empty method end");
}
@Override
public void testMethod(String name) {
System.out.println("String method");
System.out.println(name);
System.out.println("String method end");
}
public void testMethod1(String name) {
System.out.println("String method1");
System.out.println("String method2 end");
}
@Override
public void testMethod(int name) {
System.out.println("int method");
System.out.println("int method end");
}
@Override
public void testMethod(float name) {
System.out.println("float method");
System.out.println("float method end");
}
@Override
public void testMethod(String name, String desc) {
System.out.println("String,String method");
System.out.println("String,String method end");
}
}
这是一个实现类,这个实现类中我们进行了简单的输出字符串的操作,
public class InsertLog {
public static void doLog(String doLog) {
System.out.println("this is insert log :" + doLog);
}
}
这个是我们的要进行的业务逻辑操作,这里简化为输出数据到控制台
3.3 动态代理简介
这里简单的进行了动态代理的测试,简单介绍一下的动态代理,这里面有些东西我们可以用得到。
import com.xxx.hades.test.TestInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class DynamicTestObjectProxy implements InvocationHandler {
private TestInterface obj;
public DynamicTestObjectProxy(TestInterface object) {
this.obj = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("--------------------------------------------------------");
System.out.println("before do invoke");
System.out.println("method :" + method);
Parameter[] params = method.getParameters();
if (null == params || params.length == 0) {
System.out.println("No parameter");
System.out.println("--------------------------------------------------------\n\n");
return null;
}
for (int i = 0; i < params.length; i++) {
Parameter param = params[i];
System.out.println("\t\t|param:" + param.getName());
System.out.println("\t\t\t|->type:"+param.getType());
System.out.println("\t\t\t|->annotations:"+param.getAnnotations());
System.out.println("\t\t\t|->value");
}
method.invoke(obj, args);
System.out.println("after do invoke");
System.out.println("--------------------------------------------------------\n\n");
return null;
}
}
如果需要实现动态代理,那么我们就需要进行一个操作,实现InvocationHandler,有一个构造函数保存我们要调用的内容。
简单的实现测试类如下
public class JDKTestMain {
public static void main(String[] args) {
TestInterface object = new TestObject();
InvocationHandler handler = new DynamicTestObjectProxy(object);
TestInterface subject = (TestInterface) Proxy.newProxyInstance(handler.getClass().getClassLoader(), object.getClass().getInterfaces(), handler);
System.out.println(subject.getClass().getName());
subject.testMethod();
subject.testMethod("name");
}
}
结果吗?简单看看好了。
好的 至此我们的基本介绍结束了。
四 Instrument简单使用
4.1主要转换类
我们使用Java本身自带的instrumation因此实现接口ClassFileTransformer里面的方法,这里只是简单使用。
import com.xxx.hades.test.TestInterface;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import org.apache.commons.lang3.StringUtils;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashSet;
import java.util.Set;
public class Transformer implements ClassFileTransformer {
private static Set<String> interFaceList = new HashSet<>();
static {
interFaceList.add(TestInterface.class.getName());
}
private boolean isInWatch(CtClass[] classes) {
for (int i = 0; i < classes.length; i++) {
if (interFaceList.contains(classes[i].getName()))
return true;
}
return false;
}
private byte[] doTransClass(String className, byte[] classfileBuffer) {
try {
if (StringUtils.isBlank(className))
return null;
String currentClassName = className.replaceAll("/", ".");
CtClass currentClass = ClassPool.getDefault().get(currentClassName);
CtClass[] interfaces = currentClass.getInterfaces();
if (!isInWatch(interfaces)) {
return null;
}
//引入需要使用的class对应的包
ClassPool.getDefault().importPackage("com.yunqutech.hades.bussiness");
CtBehavior[] methods = currentClass.getMethods();
for (CtBehavior method : methods) {
String methodName = method.getName();
if ("testMethod".equals(methodName)) {
CtClass[] paramsType = method.getParameterTypes();
for (CtClass type : paramsType) {
String typeName = type.getName();
System.out.println("param type:" + typeName);
if ((String.class.getName().replaceAll("/", ".")).equals(typeName)) {
System.out.println(" this is correct ");
//静态类进行设置编码
method.insertAt(0, " InsertLog.doLog($1);");
break;
}
}
}
//finish method
}
return currentClass.toBytecode();
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("doTransFormClass:" + className);
return this.doTransClass(className, classfileBuffer);
}
}
4.2预处理类
预处理是要进行勾住对应JVM程序的类,因此在这里我们使用的是这样的
import com.xxx.hades.instrument.doInstrument.Transformer;
import java.lang.instrument.Instrumentation;
public class InstrumentPreMain extends Object {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("instrumentPreMain is calling");
inst.addTransformer(new Transformer());
}
}
4.3 运行主类
import com.xxx.hades.test.TestObject;
public class InstrumentMain {
public static void main(String[] args) {
TestObject object = new TestObject();
object.testMethod("jzs");
object.testMethod(1);
object.testMethod(1.0f);
object.testMethod("jzs", "desc");
}
}
4.4打包配置
为了方便简洁,我们就打包到同一个包下了,这时我们打包相关的文件META-INF/MAININFEST.MF写的内容如下
Manifest-Version: 1.0
Main-Class: com.yunqutech.hades.instrument.InstrumentMain
Premain-Class: com.yunqutech.hades.instrument.InstrumentPreMain
Can-Redefine-Classes: true
Boot-Class-Path: javassist.jar
4.5运行截图
好吧,好吧我给你们运行结果
运行的时候的使用的命令是 : java -javaagent:hades.jar -jar .\hades.jar
结果如下:
OK结束