什麼是字節碼,在這裏就不在贅述了,網上教程不少。Java 爲了能讓 Java 程序編譯一次處處運行,用 Java 編譯器將程序對源代碼編譯生成固定格式的字節碼(.class文件)供 JVM 使用,所以理論上來講,只要符合 JVM 規範的字節碼文件,就能夠在 JVM 上運行,不一樣的 JVM 類語言(如Scala、Groovy、Kotlin)編譯成字節碼均可在 JVM 運行,除此以外,若是你對 JVM 的字節碼規範很是瞭解的話,經過本身按照 JVM 規範本身寫也是能夠的。java
那麼什麼是字節碼加強呢?簡單理解就是經過某種手段或者技術修改編譯好的字節碼,讓新生成的字節碼能知足咱們的定製需求,這裏說的需求就有不少了,好比經常使用的 AOP 底層不少就是使用字節碼加強來達到切面攔截,再好比微服務中的鏈路追蹤就使用了字節碼加強(僅僅只一些 Java 客戶端)來進行埋點標記來記錄調用鏈關係的,因此瞭解字節碼加強對一些框架能有更深刻對理解,對問題排查有很大對幫助。程序員
上面說的經過某種手段或者技術到底指哪些呢?咱們最經常使用的 Java Proxy 也是一種加強技術,另外經常使用的還有 ASM,AspectJ,Javassist等經常使用的技術,其中ASM在指令層次操做字節碼的,須要對JVM的指令有必定的瞭解,同時衆多的指令也很難記住,操做比較高;AspectJ擴展了 Java,定義了一些專門的AOP語法,其中 Spring AOP 就使用了 AspectJ;Javassist 是強調源代碼層次操做字節碼的框架,操做起來很容易入手。redis
使用Javassist須要使用javassist.jar。apache
優點:數組
缺點:app
Javassist 使用 ClassPool 來操做全部的 Java 類。這個類的工做方式與 JVM 類裝載器很是類似,可是有一個重要的區別是它不是將裝載的、要執行的類做爲應用程序的一部分連接,類池使所裝載的類能夠經過 Javassist API 做爲數據使用。可使用默認的類池(ClassPool.getDefault()
),它是從 JVM 搜索路徑中裝載的,也能夠定義一個搜索您本身的路徑列表的類池。甚至能夠直接從字節數組或者流中裝載二進制類,以及從頭開始建立新類。框架
裝載到類池中的類由 CtClass 實例表示。與標準的 Class 類同樣, CtClass 提供了檢查類數據(如字段和方法)的方法。不過,這只是 CtClass 的部份內容,它還定義了在類中添加新字段、方法和構造函數、以及改變類、父類和接口的方法。函數
字段、方法和構造函數分別由 CtField、CtMethod 和 CtConstructor 的實例表示。這些類定義了修改由它們所表示的對象的全部方法的方法,包括方法或者構造函數中的實際字節碼內容。微服務
Javassist 經常使用類的說明:工具
Javassist 加強的代碼片斷是使用字符串來編寫的,基本和平時寫的 Java 源代碼一致,主要的不一樣是一些是以 $
開頭的標識符,用於表示方法或者構造函數參數、方法返回值等。
好比:
public void method1(String arg1, Object arg2) {
// 加強代碼片斷
{
System.out.println("入參 1: " + $1); // arg1
System.out.println("入參 2: " + $2); // arg2
}
}複製代碼
經過一個平時常常須要用的業務日誌記錄來學習,平時業務日誌操做記錄基本都是經過 AOP 實現的,此次就使用字節碼加強技術來進行實現,對業務代碼基本無任何侵入。
首先定義一個業務 service
public class BizService {
public void bizProcess(Map map) {
System.out.println("do biz process");
}
}複製代碼
編寫加強代碼片斷
public class BizServiceInteceptor {
public static void preProcess(Map map) {
System.out.println("preProcess");
// do log
}
}複製代碼
編寫測試類
public class JavassistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException {
ClassPool pool = ClassPool.getDefault(); // 獲取默認的類池
CtClass clazz = pool.getOrNull("com.zmc.learning.javassist.BizService");
if (clazz == null) {
System.out.println("bizService not found");
return;
}
// 獲取須要加強的代碼方法
CtMethod bizProcessMethod = clazz.getDeclaredMethod("bizProcess");
// 植入加強代碼片斷
StringBuffer sb = new StringBuffer();
sb.append("{");
sb.append("com.zmc.learning.javassist.BizServiceInteceptor.preProcess($1);"); //獲取入參
sb.append("}");
bizProcessMethod.insertBefore(sb.toString()); // 是否是有點像 AOP 的 before?
// 加強後的 class
clazz.toClass();
BizService bizService = new BizService();
bizService.bizProcess(new HashMap());
}
}複製代碼
代碼也很簡單,須要注意的是,BizServiceInteceptor
是靜態方法,同時入參須要和原方法一致。這個 demo 是否是很 easy,看的出來 Javassist 在源代碼層面上操做字節碼對程序員仍是很友好的。
需求:需求也比較簡單,在跨系統進行 Http 調用的時候,須要記錄請求的來源和調用的鏈路,若是是你你會怎麼作呢。
// 基於 Apache httpclient 進行 http 調用
// 調用方
{
httpRequest.setHeader("source", "sys1");
}
// 處理方
{
Header header = httpRequest.getHeaders("source")[0];
String sourceSystem = header.getValue();
}複製代碼
這樣也能比較容易的實現鏈路的記錄,可是這樣的方式明顯不適合。爲何呢?第一這段代碼其實跟業務關係不大,每一個業務方都須要編寫 http 調用的時候加上這麼一段前置的邏輯,仔細想一想,若是有10個系統經過 http 相互調用呢。第二若是 A 系統 set header 的 key 爲 source,那下游也得知道你 set 的 key 值,同時下游再調用下游的的時候,若是它set 的 key 不是 source 呢,那是否是就不統一了,這時你可能會說各個系統協調好,統一不就行了,是的,這樣確實能夠,那爲何不使用相似 AOP 同樣的技術進行攔截統一加呢,這樣又不會對代碼有侵入性。
http client 依賴
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.8</version>
</dependency>複製代碼
加強的代碼片斷
public class HttpClientInterceptor {
public static void intercept(HttpRequest httpRequest) {
httpRequest.setHeader("source", "test");
}
}複製代碼
加強的工具類
public class HttpClientInstrumentation {
private static final String ENHANCE_CLASS = "org.apache.http.impl.client.InternalHttpClient"; // 加強的 client
private static final String ENHANCE_METHOD = "doExecute"; // 加強的方法
public static void enhance() throws NotFoundException, CannotCompileException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getOrNull(ENHANCE_CLASS);
if (ctClass == null) {
System.out.println("http client not found");
return;
}
CtMethod doExecuteMethod = ctClass.getDeclaredMethod(ENHANCE_METHOD);
String sb = "{" +
"com.zmc.learning.javassist.HttpClientInterceptor.intercept" + "($2);" + // 獲取入參 HttpRequest
"}";
doExecuteMethod.insertBefore(sb); // 植入代碼片斷
ctClass.toClass();
}
}複製代碼
測試類
public class HttpClientTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException {
HttpClientInstrumentation.enhance();
HttpGet httpGet = sendGet();
Header header = httpGet.getHeaders("source")[0];
System.out.println(header.getValue());
}
private static HttpGet sendGet() {
//建立默認的httpClient實例
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet get = null;
try {
//用get方法發送http請求
get = new HttpGet("http://www.baidu.com/");
System.out.println("執行get請求:...." + get.getURI());
CloseableHttpResponse httpResponse = null;
// get.setHeader("source", "abc");
//發送get請求
httpResponse = httpClient.execute(get);
try {
//response實體
HttpEntity entity = httpResponse.getEntity();
if (null != entity) {
System.out.println("響應狀態碼:" + httpResponse.getStatusLine());
System.out.println("-------------------------------------------------");
System.out.println("響應內容:" + EntityUtils.toString(entity));
System.out.println("-------------------------------------------------");
}
} finally {
httpResponse.close();
}
} catch (Exception ignore) {
;
} finally {
try {
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return get;
}
}複製代碼
這樣一個 http client 的加強就行了,在業務方須要使用 http client 調用的系統中只須要在啓動的時候調用 HttpClientInstrumentation.enhance
方法便可,對業務方來講是透明的,須要注意的是爲何加強的目標方法是 doExecute
呢?由於這個方法是 http client 最底層的方法,上層的方法最後都會到這個方法來發起請求,所以攔截這個方法便可,另外,這個 demo 中只攔截了 InternalHttpClient
類,若是須要全面一點的話可能還須要攔截 MinimalHttpClient
,AbstractHttpClient
等,須要注意的是這個僅僅對 http client 同步的客戶端進行了加強。
有了這個 demo 是否是對其餘對跨系統調用的客戶端也能夠實現呢,redis,kafka,nsq,grpc?大體的思路實際上是一致的。說到這裏再補一句,其實鏈路追蹤的框架調用鏈這塊大體的思路也是這樣的,經過加強各個中間件的客戶端來把系統的調用鏈串起來。
參照Jdk Java Proxy 編寫一個動態代理的工具,Java Proxy 相關的請自行了解。
編寫ProxyFactory
public class ProxyFactory {
public static Object newProxyInstance(ClassLoader classLoader, Class<?> interfaceClass, InvocationHandler h) throws Throwable {
ClassPool pool = ClassPool.getDefault();
// 1.建立代理類 ProxyClass
CtClass proxyClass = pool.makeClass("ProxyClass");
// 2.給代理類添加字段:private InvocationHandler handler;
CtClass handlerCc = pool.get(InvocationHandler.class.getName());
CtField handlerField = new CtField(handlerCc, "handler", proxyClass); // CtField(CtClass fieldType, String fieldName, CtClass addToThisClass)
handlerField.setModifiers(AccessFlag.PRIVATE);
proxyClass.addField(handlerField);
// 3.添加構造函數:public NewProxyClass(InvocationHandler handler) { this.handler = handler; }
CtConstructor ctConstructor = new CtConstructor(new CtClass[] { handlerCc }, proxyClass);
ctConstructor.setBody("$0.handler = $1;"); // $0表明this, $1表明構造函數的第1個參數
proxyClass.addConstructor(ctConstructor);
// 4.爲代理類添加相應接口方法及實現
CtClass interfaceCc = pool.get(interfaceClass.getName());
// 4.1 爲代理類添加接口:public class ProxyClass implements IHello
proxyClass.addInterface(interfaceCc);
// 4.2 爲代理類添加相應方法及實現
CtMethod[] ctMethods = interfaceCc.getDeclaredMethods();
for (CtMethod ctMethod : ctMethods) {
String methodFieldName = ctMethod.getName(); // 新的方法名
// 4.2.1 爲代理類添加反射方法字段
// 如:private static Method method1 = Class.forName("com.zmc.learning.javassist.IHello").getDeclaredMethod("sayHello", new Class[] { Integer.TYPE });
// 構造反射字段聲明及賦值語句
String classParamsStr = "new Class[0]"; // 方法的多個參數類型以英文逗號分隔
if (ctMethod.getParameterTypes().length > 0) { // getParameterTypes獲取方法參數類型列表
for (CtClass clazz : ctMethod.getParameterTypes()) {
classParamsStr = (("new Class[0]".equals(classParamsStr)) ? clazz.getName() : classParamsStr + "," + clazz.getName()) + ".class";
}
classParamsStr = "new Class[] {" + classParamsStr + "}";
}
String methodFieldTpl = "private static java.lang.reflect.Method %s=Class.forName(\"%s\").getDeclaredMethod(\"%s\", %s);";
String methodFieldBody = String.format(methodFieldTpl, ctMethod.getName(), interfaceClass.getName(), ctMethod.getName(), classParamsStr);
// 爲代理類添加反射方法字段. CtField.make(String sourceCodeText, CtClass addToThisClass)
CtField methodField = CtField.make(methodFieldBody, proxyClass);
proxyClass.addField(methodField);
// 4.2.2 爲方法添加方法體
// 構造方法體. this.handler.invoke(this, 反射字段名, 方法參數列表);
String methodBody = "$0.handler.invoke($0, " + methodFieldName + ", $args)";
// 若是方法有返回類型,則須要轉換爲相應類型後返回,由於invoke方法的返回類型爲Object
if (CtPrimitiveType.voidType != ctMethod.getReturnType()) {
// 對8個基本類型進行轉型
// 例如:((Integer)this.handler.invoke(this, this.m2, new Object[] { paramString, new Boolean(paramBoolean), paramObject })).intValue();
if (ctMethod.getReturnType() instanceof CtPrimitiveType) {
CtPrimitiveType ctPrimitiveType = (CtPrimitiveType) ctMethod.getReturnType();
methodBody = "return ((" + ctPrimitiveType.getWrapperName() + ") " + methodBody + ")." + ctPrimitiveType.getGetMethodName() + "()";
} else { // 對於非基本類型直接轉型便可
methodBody = "return (" + ctMethod.getReturnType().getName() + ") " + methodBody;
}
}
methodBody += ";";
// 爲代理類添加方法. CtMethod(CtClass returnType, String methodName, CtClass[] parameterTypes, CtClass addToThisClass)
CtMethod newMethod = new CtMethod(ctMethod.getReturnType(), ctMethod.getName(),
ctMethod.getParameterTypes(), proxyClass);
newMethod.setBody(methodBody);
proxyClass.addMethod(newMethod);
}
// 5.生成代理實例. 將入參InvocationHandler handler設置到代理類的InvocationHandler handler變量
Class newClass = proxyClass.toClass(classLoader, null);
return newClass.getConstructor(InvocationHandler.class).newInstance(h);
}
}複製代碼
ProxyFactory 和 Jdk Java Proxy 方法簽名都一致,使用的方式也是一致的,須要注意的是對8個基本類型進行轉型。