最近導師公司需要有些幫忙的地方,就去來幫忙,結果甩手一個需求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結束