本文重點說一下,JavaAgent 能給我們帶來什麼?
自己實現一個 JavaAgent xxxxxx
基於 JavaAgent 的 spring-loaded 實現 jar 包的熱更新,也就是在不重啓服務器的情況下,使我們某個更新的 jar 被重新加載。
一、基於 JavaAgent 的應用實例
JDK5中只能通過命令行參數在啓動JVM時指定javaagent參數來設置代理類,而JDK6中已經不僅限於在啓動JVM時通過配置參數來設置代理類,JDK6中通過 Java Tool API 中的 attach 方式,我們也可以很方便地在運行過程中動態地設置加載代理類,以達到 instrumentation 的目的。
Instrumentation 的最大作用,就是類定義動態改變和操作。
最簡單的一個例子,計算某個方法執行需要的時間,不修改源代碼的方式,使用Instrumentation 代理來實現這個功能,給力的說,這種方式相當於在JVM級別做了AOP支持,這樣我們可以在不修改應用程序的基礎上就做到了AOP,是不是顯得略吊。
創建一個 ClassFileTransformer 接口的實現類 MyTransformer
實現 ClassFileTransformer 這個接口的目的就是在class被裝載到JVM之前將class字節碼轉換掉,從而達到動態注入代碼的目的。那麼首先要了解MonitorTransformer 這個類的目的,就是對想要修改的類做一次轉換,這個用到了javassist對字節碼進行修改,可以暫時不用關心javassist的原理,用ASM同樣可以修改字節碼,只不過比較麻煩些。
接着上一篇文章的2個工程,分別添加下面的類。
MyTransformer.java 添加到 MyAgent 工程中。
package com.shanhy.demo.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
/**
* 檢測方法的執行時間
*
* @author 單紅宇(365384722)
* @myblog http://blog.csdn.net/catoop/
* @create 2016年3月30日
*/
public class MyTransformer implements ClassFileTransformer {
final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";
// 被處理的方法列表
final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();
public MyTransformer() {
add("com.shanhy.demo.TimeTest.sayHello");
add("com.shanhy.demo.TimeTest.sayHello2");
}
private void add(String methodString) {
String className = methodString.substring(0, methodString.lastIndexOf("."));
String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
List<String> list = methodMap.get(className);
if (list == null) {
list = new ArrayList<String>();
methodMap.put(className, list);
}
list.add(methodName);
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/", ".");
if (methodMap.containsKey(className)) {// 判斷加載的class的包路徑是不是需要監控的類
CtClass ctclass = null;
try {
ctclass = ClassPool.getDefault().get(className);// 使用全稱,用於取得字節碼類<使用javassist>
for (String methodName : methodMap.get(className)) {
String outputStr = "\nSystem.out.println(\"this method " + methodName
+ " cost:\" +(endTime - startTime) +\"ms.\");";
CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 得到這方法實例
String newMethodName = methodName + "$old";// 新定義一個方法叫做比如sayHello$old
ctmethod.setName(newMethodName);// 將原來的方法名字修改
// 創建新的方法,複製原來的方法,名字爲原來的名字
CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null);
// 構建新的方法體
StringBuilder bodyStr = new StringBuilder();
bodyStr.append("{");
bodyStr.append(prefix);
bodyStr.append(newMethodName + "($$);\n");// 調用原有代碼,類似於method();($$)表示所有的參數
bodyStr.append(postfix);
bodyStr.append(outputStr);
bodyStr.append("}");
newMethod.setBody(bodyStr.toString());// 替換新方法
ctclass.addMethod(newMethod);// 增加新方法
}
return ctclass.toBytecode();
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
return null;
}
}
TimeTest.java 添加到 MyProgram 工程中。
package com.shanhy.demo;
/**
* 被測試類
*
* @author 單紅宇(365384722)
* @myblog http://blog.csdn.net/catoop/
* @create 2016年3月30日
*/
public class TimeTest {
public static void main(String[] args) {
sayHello();
sayHello2("hello world222222222");
}
public static void sayHello() {
try {
Thread.sleep(2000);
System.out.println("hello world!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sayHello2(String hello) {
try {
Thread.sleep(1000);
System.out.println(hello);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
修改MyAgent.java 的 permain 方法,如下:
public static void premain(String agentOps, Instrumentation inst) {
System.out.println("=========premain方法執行========");
System.out.println(agentOps);
// 添加Transformer
inst.addTransformer(new MyTransformer());
}
修改MANIFEST.MF內容,增加 Boot-Class-Path 如下:
Manifest-Version: 1.0
Premain-Class: com.shanhy.demo.agent.MyAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist-3.18.1-GA.jar
對2個工程分別打包爲 myagent.jar 和 myapp.jar 然後將 javassist-3.18.1-GA.jar 和 myagent.jar 放在一起。
最後執行命令測試,結果如下:
G:\>java -javaagent:G:\myagent.jar=Hello1 -jar myapp.jar
=========premain方法執行========
Hello1
hello world!!
this method sayHello cost:2000ms.
hello world222222222
this method sayHello2 cost:1000ms.
二、使用 spring-loaded 實現 jar 包熱部署
在項目開發中我們可以把一些重要但又可能會變更的邏輯封裝到某個 logic.jar 中,當我們需要隨時更新實現邏輯的時候,可以在不重啓服務的情況下讓修改後的 logic.jar 被重新加載生效。
spring-loaded是一個開源項目,項目地址:https://github.com/spring-projects/spring-loaded
使用方法:
在啓動主程序之前指定參數
-javaagent:C:/springloaded-1.2.5.RELEASE.jar -noverify
如果你想讓 Tomat 下面的應用自動熱部署,只需要在 catalina.sh 中添加:
set JAVA_OPTS=-javaagent:springloaded-1.2.5.RELEASE.jar -noverify
這樣就完成了 spring-loaded 的安裝,它能夠自動檢測Tomcat 下部署的webapps ,在不重啓Tomcat的情況下,實現應用的熱部署。
通過使用 -noverify 參數,關閉 Java 字節碼的校驗功能。
使用參數 -Dspringloaded=verbose;explain;watchJars=tools.jar 指定監視的jar (verbose;explain; 非必須),多個jar用“冒號”分隔,如 watchJars=tools.jar:utils.jar:commons.jar
當然,它也有一些小缺限:
1. 目前官方提供的1.2.4 版本在linux上可以很好的運行,但在windows還存在bug,官網已經有人提出:https://github.com/spring-projects/spring-loaded/issues/145
2. 對於一些第三方框架的註解的修改,不能自動加載,比如:spring mvc的@RequestMapping
3. log4j的配置文件的修改不能即時生效。