天天看點

javaagent暴打紀實(一)

前言:回想起多少個夜晚都被Javaagent的噩夢萦繞,簡直是人間悲慘。真是應了那句:基礎不牢,地動山搖!!!

1. 背景

        現階段微服務已經日益成熟,而k8s的興起無疑大大地加快了各大公司微服務化的腳步,它使我們能夠便捷高效地管理成百上千個服務。而在大型應用的疊代開發過程中,由于靈活開發導緻的子項目、團隊劃分。經常存在好幾個團隊同時修改同一個服務或應用。這時候存在兩種情況:

  1. 如果隻有一套測試環境,那麼這些團隊在進行自測、聯調、測試驗收時都要去進行沖突服務的代碼合并還要去解決合并後的代碼沖突、bug等。嚴重拖慢疊代進度,使靈活變得笨拙。
  2. 如果有多套環境,給每個團隊提供一套全量的應用環境,那麼沖突将不複存在。但是新的問題是如果多個團隊進行協作,他們各自負責的服務在在自己的環境中已經更疊了數個版本,但是在對方的環境中由于未進行該應用疊代,可能遠遠落後于目前版本。這很可能在進行疊代合作時由于版本陳舊導緻測試失敗。而且還有一個比較大的問題就是維護多套環境的成本很高!!!在網際網路倡導開源(猿)節流的今天,你不幹掉它,它可能就幹掉你了。(舉個例子,我們目前維護一個大一點的子產品單個環境成本大概1.4w/月)

        是以有什麼辦法既有使用一套環境的穩定、便捷又能有像多套環境的隔離性呢?

        答案是:流量染色與穿梭

2. 實作思路簡介

注意:這裡以一套基于k8s部署的應用為例。服務發現通過k8s自有的service解析來實作、使用feign進行服務間調用,我們暫且稱該實作為榕樹平台。

首先先看一下理想中的效果,如下圖,基準環境中部署了全量服務。各靈活小組在進行疊代開發時隻需要建立一個屬于自己的環境,然後部署需要進行更改的應用即可擁有與在基準環境中進行疊代測試的體驗。

javaagent暴打紀實(一)

那麼具體怎麼實作呢,如下是一個簡單的部署架構圖。使用者在web端通過nginx通路到具體的服務。每個namespace下都有一系列該業務域的服務。不同環境建立的service與基準環境的service放在同一個namespace,通過環境尾綴進行區分。每套環境對應着自己獨有的nginx,請求從對應環境發起後在nginx層帶上header,路由到對應的服務,而服務間的調用則利用agent技術根據目前環境的應用釋出情況動态選擇正确的下遊服務。

javaagent暴打紀實(一)

那麼要實作上述效果主要有以下幾個問題:

  1. nginx如何根據環境釋出情況選擇具體的應用?例如serviceA、serviceA-test1如何進行upstream。
  2. 服務間調用如何選擇正确的服務?例如test1環境中部署有服務serviceC-test1。在serviceB調用serviceC-時需要test1的應用部署情況選擇serviceC-test1而不是調用serviceC。
  3. kafka等中間件如何選擇正确的消息接收方?
  4. 如何将這些動态選擇邏輯植入到衆多微服務中?

問題1:

将nginx抽離為模闆,在建立應用時動态去替換upstream。具體方式則為nginx中植入腳本定時輪詢,檢查應用釋出情況并更新upstream。

upstream test{
	server http://ServiceA.namespace1;
}
           

nginx釋出時檢測到test1中有ServiceA則把test的nginx替換為

upstream test{
	server http://ServiceA-test1.namespace1;
}
           

問題2:

由于請求是攜帶了header的,是以在請求進入時通過攔截器或者過濾器将請求的來源環境target_env放線上程上下文中,然後在在feign調用下遊服務時通過feign的攔截器來動态修改目的位址實作調用在環境間的穿梭。當然動态修改要依據線程上下文中的target-env以及目的服務在注冊中心(這裡是k8s dns,當然也可以是其他的第三方注冊中心)的注冊情況來進行判斷。

問題3:

對于kafka,可以讓不用環境的應用作為不同的group消費同一個topic。發送方在消息中添加目前環境 target_env,消費方消費消息是先去拉去改topic下的所有group,如果存在target_env對應的group則繼續判斷自己是不是在該group,這樣保證了隻在對應環境的消費者才會進行消費。(ps:暫時還未經實踐,歡迎溝通交流)

問題4:

對于問題4這裡走了點彎路,咱們詳細說。

3. 植入實作之——spring boot starter

首先先明确一下我們的目标:将應用間調用的動态選擇邏輯植入到所有的服務中去,包括請求進入服務時線程上下文中設定環境變量以及feign請求時的動态選擇。

因為目前所有應用都是基于spring boot建構的,是以理所當然地想着通過spring boot starter進行邏輯的注入,實作起來當然也還簡潔明了,但問題在于所有的應用都要引入這個spring boot starter jar包,且要解決spring boot相關jar包的版本沖突。這無疑加大了實施難度,是以最終沒能選擇該方案。

javaagent暴打紀實(一)

事實證明思路還是要open,解決方案要多進行調研、評審對比。

3.1 demo

簡單看下利用spring-boot如何實作feign調用流量轉發的,首先看下feign是如何工作的,俗話說知己知彼才能百戰不殆。

spring-cloud-starter-openfeign通過org.springframework.cloud.openfeign.FeignAutoConfigurationfeign來初始化相關元件。其中最重要的就是feign.Client執行個體的初始化,他根據項目中是否依賴ApacheHttpClient或者OkHttpclient來進行具體的連接配接池初始化。如果都沒有則會使用feign.Client的預設實作,通過java的URLconnection來進行遠端通路。

// apache http clent
@ConditionalOnClass(ApacheHttpClient.class)
	@Bean
		@ConditionalOnMissingBean(Client.class)
		public Client feignClient(HttpClient httpClient) {
			return new ApacheHttpClient(httpClient);
		}
		
// okhttpclient
@ConditionalOnClass(OkHttpClient.class)
	@Bean
		@ConditionalOnMissingBean(Client.class)
		public Client feignClient(okhttp3.OkHttpClient client) {
			return new OkHttpClient(client);
		}
           
@FeignClient(name = "courseop-base",url="http://courseop-base")
public interface TestFeignClient {

    @PostMapping("/course/info/{courseId}")
    CourseInfo get(@PathParam Long courseId);
}
           

而對于我們通過@FeignClient自定義的client。feign會掃描并生成對應的代理類,然後通過具體的feign.client中的方法feign.Client#execute來進行遠端通路。是以我們可以在feign client調用時通過修改request的參數來動态選擇具體的服務。例如将ServiceC替換為ServiceC-test1完成對應環境的服務調用。

public interface Client {

  /**
 1. Executes a request against its {@link Request#url() url} and returns a response.
 2.  3. @param request safe to replay.
 4. @param options options to apply to this request.
 5. @return connected response, {@link Response.Body} is absent or unread.
 6. @throws IOException on a network error connecting to {@link Request#url()}.
   */
  Response execute(Request request, Options options) throws IOException;
  }
           

是以我們可以動過代理Client,或者自己實作client來進行邏輯植入,當然這種方法需要重寫AutoConfiguration來在FeignAutoConfiguration之前進行feign.Client的。具體邏輯如下,就不詳細介紹了:

javaagent暴打紀實(一)

3.2 總結

使用該方案的部分問題在前面也提到了。這裡總結一下

  1. 實作上不太雅觀,魔改AutoConfiguration。
  2. spring boot、 spring cloud 、等元件jar包容易沖突。
  3. 成百上千的應用進行內建比較麻煩。

4. 植入實作之——位元組碼增強

jdk 1.5提供了Instrumentation功能,這使得我們能夠動态地修改位元組碼。premain是instrumentation中一個比較重要的功能。其寫法如下

public static void premain(String agentArgs, Instrumentation inst);

public static void premain(String agentArgs);
           

其中agentArgs是我們在運作時(Java -javaagent:xxx.jar[args])傳入的參數。而Instrumentation是jvm提供的用于修改位元組碼的入口。雖然有這種入口,但是直接編輯位元組碼還是有較高門檻的,于是出現了許多用于便捷修改位元組碼的類庫,例如asm、javaassistant、bytebuddy等等。這裡以Java Assistant和bytebuddy為例。

Java Assistant

4.1 demo

廢話不多說,先簡單看個demo,這裡以feign調用時根據環境名修改遠端服務位址為例

Javaagent入口,包含premain方法。調用TransformerManager.registryTransformer進行transformer注冊。

public class ToggleAgent {
    public static void premain(String agentArgs, Instrumentation inst) throws IOException {
        TransformerManager.registryTransformer(inst);
    }
}
           

注冊管理器,簡單将需要注冊的transformer進行組合,注冊。

public class TransformerManager {
    private static final Logger log = LoggerFactory.getLogger(TransformerManager.class);
  
    public static void registryTransformer(Instrumentation inst) {
        CompoundTransformer compoundTransformer = new CompoundTransformer();
        //feign
        compoundTransformer.register(new TomcatTransformer());
        //tomcat
        compoundTransformer.register(new FeignHttpClientTransformer());
        //consul
        //compoundTransformer.register(new ConsulRegistryTransformer());
        //ribbon
        compoundTransformer.register(new RibbonRequestTransformer());
        inst.addTransformer(compoundTransformer);
    }

    public static class CompoundTransformer implements ClassFileTransformer {
        private Map<String, MyTransformer> myTransformerMap = new HashMap<String, MyTransformer>();
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            if (className == null) {
                return classfileBuffer;
            }
            try {
                MyTransformer myTransformer = this.myTransformerMap.get(className);
                if (myTransformer != null) {
                    return myTransformer.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
                }
            } catch (Throwable e) {
                log.error( "failed to change byte code for class{}:",className,e);
            }
            return classfileBuffer;
        }

        public void register(MyTransformer myTransformer) {
            Set<String> names = myTransformer.getNames();
            if (names != null && names.size() > 0) {
                for (String name : names) {
                    this.myTransformerMap.put(name, myTransformer);
                }
            }
        }
    }
}

           

feign client具體的增強實作,可以看到這裡的增強代碼是用string注入的,可以說是很讓人抓狂了,而且增強的代碼中必須使用全限定名,例如String必須用java.lang.String。屬實有些難頂。

public class RibbonRequestTransformer implements MyTransformer {
    private static final Logger log = LoggerFactory.getLogger(RibbonRequestTransformer.class);

    @Override
    public Set<String> getNames() {
        Set<String> set = new HashSet<String>();
        set.add("com/netflix/client/ClientRequest");
        set.add("com/netflix/niws/client/http/HttpClientRequest");
        set.add("com/netflix/client/http/HttpRequest");
        return set;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        log.info("registry ribbon request transformer");
        String finalClassName = className.replace("/", ".");
        CtClass ctClass;
        try {
            ctClass = ClassPool.getDefault().get(finalClassName);
            CtMethod ctMethod = ctClass.getDeclaredMethod("replaceUri");
            StringBuilder sb = new StringBuilder();
            sb.append("{");
            sb.append("try{");
            sb.append("java.lang.String targetEnv = com.netease.edu.envjavaagent.utils.EnvInfoUtil.getEnv();");
            sb.append("com.netease.edu.envcore.utils.EnvHostRewriter envHostRewriter = new com.netease.edu.envcore.utils.EnvHostRewriter(new com.netease.edu.envcore.client.DefaultClient());");
            sb.append("java.lang.String url = $1.toString();");
            sb.append("java.lang.String newUrl = envHostRewriter.replaceAppCurEnvHost(targetEnv,url);");
            sb.append("if(!url.equals(newUrl)){");
            sb.append("$1 = new java.net.URI(newUrl);");
            sb.append("}");
            sb.append("System.out.println(\"replace++++++\"+$1.toString());");
            sb.append(" } catch (Exception e) {");
            sb.append("e.printStackTrace();");
            sb.append("}");
            sb.append("}");
            ctMethod.insertBefore(sb.toString());
            return ctClass.toBytecode();
        } catch (Exception e) {
            log.error("registry ribbon request transformer error",e);
        }
        return null;
    }
}
           

4.2 遇到的問題

過程中也遇到了不少問題,這裡把大家可能會遇到的兩個典型問題記錄一下。

  1. 在premain方法中重寫類的位元組碼時,由于classpool是從以APPclassloader的類加載路徑為基準的,是以spring boot、Tomcat加載的類需要先append後才能查找。

    如下是Javaassistant類加載的路徑,可以發現就是appClassloader的加載路徑。

if (ClassFile.MAJOR_VERSION < 53) {
    return this.appendClassPath((ClassPath)(new ClassClassPath()));
} else {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return this.appendClassPath((ClassPath)(new LoaderClassPath(cl)));
}
           

解決方法,将transformer接口方法中傳入的classloader的patch加入Javaassistant的類加載路徑中。

classPool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);
           
  1. 對于植入的代碼中使用的class,javaassist會嘗試從目前classpool中可通路的路徑中來加載查找class(既APPclassloader的路徑),如果這裡使用自定義的類加載器加載的,則會在transform時找不到class。
Caused by: compile error: no such class: com.netease.edu.envjavaagent.transformer.feign.FeignHttpClientTransformer
	at org.apache.ibatis.javassist.compiler.MemberResolver.searchImports(MemberResolver.java:479)
	at org.apache.ibatis.javassist.compiler.MemberResolver.lookupClass(MemberResolver.java:422)
	at org.apache.ibatis.javassist.compiler.MemberResolver.lookupClassByJvmName(MemberResolver.java:329)
	at org.apache.ibatis.javassist.compiler.TypeChecker.atCallExpr(TypeChecker.java:711)
	at org.apache.ibatis.javassist.compiler.JvstTypeChecker.atCallExpr(JvstTypeChecker.java:170)
	at org.apache.ibatis.javassist.compiler.ast.CallExpr.accept(CallExpr.java:49)
	at org.apache.ibatis.javassist.compiler.TypeChecker.atVariableAssign(TypeChecker.java:274)
	at org.apache.ibatis.javassist.compiler.TypeChecker.atAssignExpr(TypeChecker.java:243)
	at org.apache.ibatis.javassist.compiler.ast.AssignExpr.accept(AssignExpr.java:43)
	at org.apache.ibatis.javassist.compiler.CodeGen.doTypeCheck(CodeGen.java:266)
	at org.apache.ibatis.javassist.compiler.CodeGen.atStmnt(CodeGen.java:360)
	at org.apache.ibatis.javassist.compiler.ast.Stmnt.accept(Stmnt.java:53)
	at org.apache.ibatis.javassist.compiler.CodeGen.atStmnt(CodeGen.java:381)
	at org.apache.ibatis.javassist.compiler.ast.Stmnt.accept(Stmnt.java:53)
	at org.apache.ibatis.javassist.compiler.Javac.compileStmnt(Javac.java:578)
	at org.apache.ibatis.javassist.CtBehavior.insertBefore(CtBehavior.java:786)
 
 private CtClass lookupClass0(String classname, boolean notCheckInner) throws NotFoundException {
    CtClass cc = null;

    do {
        try {
            //從classpool中加載
            cc = this.classPool.get(classname);
        } catch (NotFoundException var7) {
            int i = classname.lastIndexOf(46);
            if (notCheckInner || i < 0) {
                throw var7;
            }

            StringBuffer sbuf = new StringBuffer(classname);
            sbuf.setCharAt(i, '$');
            classname = sbuf.toString();
        }
    } while(cc == null);

    return cc;
}
           

解決方法

将自己的classloader的路徑append到classpool

4.3 總結

使用Java assistant進行位元組碼增強還是存在很多弊端的:

  1. javaassistant對于位元組碼的操作上還是不夠便捷,需要自己寫很多text代碼。編寫難度大、容易出錯,且調試麻煩。
  2. 類庫間的沖突依然存在。(可以通過自定義類加載器解決,後文詳述)
  3. 而且據bytebuddy表示,其性能不如他
    javaagent暴打紀實(一)

2. byte buddy

介紹bytebuddy之前我們來看一下前面兩種方案中提到的類庫沖突是這麼回事,由于Java agent的jar包在啟動時由appclassloader加載,未與應用進行區分,是以當Java agent引入與應用中相同的jar包時很容易由于版本差距大導緻代碼不相容,出現異常。是以在進行位元組碼增強時,考慮類加載的隔離是非常有意義且必要的。解決這個問題有下述兩個思路:

  1. 利用maven-shade-plugin重命名jar包,例如
<plugin>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <configuration>
                    <relocations>
                        <relocation>
                            <pattern>net.bytebuddy</pattern>
                            <shadedPattern>net.bytebuddy.test</shadedPattern>
                        </relocation>
                    </relocations>
                </configuration>
  </plugin>
           
  1. 自定義類加載器實作與應用的類加載隔離。

    其中第一個方法治标不治本,且重命名時容易有漏網之魚,是以建議用第二個方法。

2.1 類加載機制

首先老生常談的,先看一下Java的類加載機制。這裡也就不深入介紹了。

javaagent暴打紀實(一)

這裡使用自定義類加載器會遇到一個比較棘手的問題,如下圖所示

javaagent暴打紀實(一)

假如我的agent jar包中有一個工具類ThreadUtil用來存放變量到線程上下文,類加載隔離後它由自定義的類加載器加載,而在給FeignClient的增強代碼中使用了該類,而feignclient所在類加載器是spring boot自定義的classloader,其在加載ThreadUtil時使用雙親委派依次向上尋找,結果可想。那麼我們該如何解決這個問題呢?把ThreadUtils丢到appclassloader?那不是掩耳盜鈴版的類加載隔離?反觀我們的jvm是如何打破類加載的雙親委派機制的,他将APP classloader放線上程上下文中,友善APP classloader或者Extension classloader來擷取他們不能看到的class。例如在4.2中,Javaassistant就通過這樣的方式來擷取APP classloader,可我們總不能魔改jvm把自定義classloader放入線程上下文中把。但是我們可以使用類似線程上線文類似的機制,定義一個通用上線文持有所有的增強類型以及對應增強類。并把該上下文類放入到bootstrap classloader中,具體加載執行個體如下。

javaagent暴打紀實(一)

2.2 demo

按照上述思路的實作如下:

2.2.1 包結構
javaagent暴打紀實(一)

主要包含三個jar包,他們分别的類加載器為

javaagent暴打紀實(一)
2.2.2 各包代碼詳解
  1. env-javaagent-premain

    這個jar包主要包含premain入口以及自定義的classloader,有APPclassloader加載。

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * @author dewey
 * 加載env-javaagent中class,與應用隔離
 */
public class EnvClassLoader extends URLClassLoader {


    public EnvClassLoader(File jarFile) throws MalformedURLException {
        super(new URL[]{jarFile.toURI().toURL()});
    }

    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        final Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }

        // 優先從parent(SystemClassLoader)裡加載系統類,避免抛出ClassNotFoundException
        if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {
            return super.loadClass(name, resolve);
        }

        try {
            Class<?> aClass = findClass(name);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception e) {
            // ignore
        }
        return super.loadClass(name, resolve);
    }
}
           

premain的入口,初始化增強上下文,并将env-javaagent-common.jar放入bootstrap classloader,使用自定義類加載器加載env-javaagent.jar。

import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;


/**
 * @author dewey
 */
public class ToggleAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("++++++++++++++  premain");
        try {
            String envBootClassName = "com.netease.edu.envjavaagent.TransformerManager";
            //env-javaagent-common
            JarFile commonJar = new JarFile(new File("env-javaagent-common-1.0.0-SNAPSHOT.jar"));
            inst.appendToBootstrapClassLoaderSearch(commonJar);

            //env-javaagent
            File agentJar = new File("env-javaagent-1.0.0-SNAPSHOT.jar");
            EnvClassLoader classLoader = new EnvClassLoader(agentJar);
            Class.forName(envBootClassName,true,classLoader)
                    .getMethod("init", Instrumentation.class,File.class)
                    .invoke(null,inst,agentJar);
        }catch (Exception e){
            System.out.println("env premain error");
            e.printStackTrace();
        }
    }
    
}

           
  1. env-javaagent-common

    包含增強的上下文類以及具體增強類的接口,由bootstrap classloader加載

import com.netease.edu.envjavaagentcommon.advice.EnvAdvice;
import java.util.HashMap;
import java.util.Map;

/**
 * @author dewey
 */
public class EnvInstrumentationDispatcher {
    private static Map<String, EnvAdvice> map = new HashMap<>();

    public static void init(Map<String, EnvAdvice> map){
        EnvInstrumentationDispatcher.map= map;
    }
    
    public static EnvAdvice getAdvice(String name){
        System.out.println("EnvInstrumentationDispatcher: get "+name);
        return map.get(name);
    }
}
           
import java.lang.reflect.Method;

/**
 * @author dewey
 */
public interface EnvAdvice {

    /**
     * 傳回修改後的參數,對于基礎類型,需要将arguments重新指派,不能直接改
     * @param thiz
     * @param method
     * @param arguments
     * @return
     */
    Object[] onMethodEnter(Object thiz, Method method,Object[] arguments);

    void onMethodExit();
}

           
  1. env-javaagent

    具體的增強邏輯實作,會初始化增強上下文,有自定義類加載器加載。

    TransformerManager負責加載所有的具體增強類(通過serviceloader加載)并進行增強上線文類的初始化。同時将所有的增強邏輯植入到jvm中。

import com.netease.edu.envjavaagent.advice.EnvAdviceAnnotation;
import com.netease.edu.envjavaagent.advice.EnvAdviceClass;
import com.netease.edu.envjavaagent.instrumentation.EnvInstrumentation;
import com.netease.edu.envjavaagent.utils.LoggerFactory;
import com.netease.edu.envjavaagentcommon.advice.EnvAdvice;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;

import java.io.File;
import java.lang.instrument.Instrumentation;
import java.util.*;
import java.util.logging.Logger;

import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.not;

/**
 * @author dewey
 */
public class TransformerManager {
    private static final Logger log = LoggerFactory.getLogger(TransformerManager.class);

    public static void init(Instrumentation inst, File agentJar)  {
        log.info("TransformerManager init======");
        ClassLoader envClassLoader = TransformerManager.class.getClassLoader();

        List<EnvInstrumentation> instrumentations = loadAllEnvInstrumentation(envClassLoader);
        initDispatcher(instrumentations);

        AgentBuilder agentBuilder = new AgentBuilder.Default().with(new ByteBuddy());

        for (EnvInstrumentation envInstrumentation:instrumentations){
            agentBuilder = applyAdvice(agentBuilder,envInstrumentation);
        }
        agentBuilder.installOn(inst);

        System.out.println("================Client============ premain ================finish===========");
    }

    /**
     * 注冊增強
     * @param agentBuilder
     * @return
     */
    public static AgentBuilder applyAdvice(AgentBuilder agentBuilder,EnvInstrumentation envInstrumentation){
        return agentBuilder.type(envInstrumentation.getTypeMatcher())
                .transform(getTransformer(envInstrumentation));
    }

    /**
     * 擷取具體增強類的transformer
     * @param envInstrumentation
     * @return
     */
    public static AgentBuilder.Transformer getTransformer(EnvInstrumentation envInstrumentation){
        final ElementMatcher<? super MethodDescription> methodMatcher = new ElementMatcher.Junction.Conjunction<>(envInstrumentation.getMethodMatcher(), not(isAbstract()));

        return (builder, typeDescription, classLoader, module) -> {
            Advice advice = Advice.withCustomMapping().bind(EnvAdviceAnnotation.class,envInstrumentation.getAdviceClassName()).to(EnvAdviceClass.class);
            return builder.visit(advice.on(methodMatcher));
        };
    }
    /**
     * 加載所有的增強配置
     * @param classLoader
     * @return
     */
    public static List<EnvInstrumentation> loadAllEnvInstrumentation(ClassLoader classLoader){
        List<EnvInstrumentation> instrumentations = new ArrayList<>();
        try {
            ServiceLoader<EnvInstrumentation> serviceLoader = ServiceLoader.load(EnvInstrumentation.class,classLoader);
            for (EnvInstrumentation envInstrumentation : serviceLoader) {
                Class clazz = envInstrumentation.getEnvAdviceClass();
                if (!EnvAdvice.class.isAssignableFrom(clazz)) {
                    System.out.println("advice 非法");
                } else {
                    instrumentations.add(envInstrumentation);
                }
            }
            return instrumentations;
        }catch (Exception e){
            System.out.println("loadAllEnvInstrumentation error ");
            e.printStackTrace();
        }
        return instrumentations;
    }

    /**
     * 初始化dispatcher
     * @param instrumentations
     */
    public static void initDispatcher(List<EnvInstrumentation> instrumentations){
        try {
            String dispatcherClass = "com.netease.edu.envjavaagentcommon.instrumentation.EnvInstrumentationDispatcher";
            Map<String, EnvAdvice> map = new HashMap<>();
            for (EnvInstrumentation envInstrumentation : instrumentations) {
                Class clazz = envInstrumentation.getEnvAdviceClass();
                map.put(envInstrumentation.getAdviceClassName(), (EnvAdvice) clazz.newInstance());
            }

            Class.forName(dispatcherClass)
                    .getMethod("init", Map.class)
                    .invoke(null, map);
        }catch (Exception e){
            System.out.println("initDispatcher error");
            e.printStackTrace();
        }
    }
}
           

通用增強邏輯,通過增強上下文擷取具體的增強邏輯并執行。注意這裡通過制定一個注解EnvAdviceAnnotation來傳遞需要執行的具體增強類,這個參數在上述TransformerManager初始化過程中指定。

import com.netease.edu.envjavaagentcommon.advice.EnvAdvice;
import com.netease.edu.envjavaagentcommon.instrumentation.EnvInstrumentationDispatcher;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.implementation.bytecode.assign.Assigner;

import java.lang.reflect.Method;

/**
 * @author dewey
 */
public class EnvAdviceClass {

    @Advice.OnMethodEnter
    public static void onMethodEnter(@Advice.This(optional = true) Object thiz,
                                     @Advice.Origin Method method,
                                     @Advice.AllArguments(readOnly = false,typing = Assigner.Typing.DYNAMIC) Object[] arguments,
                                     @EnvAdviceAnnotation String adviceClass) {
        EnvAdvice envAdvice = EnvInstrumentationDispatcher.getAdvice(adviceClass);
        if(adviceClass == null || envAdvice == null){
            return;
        }
        //必須重新指派才會被bytebuddy處理,實作參數替換
        arguments = envAdvice.onMethodEnter(thiz,method,arguments);

        //并不會執行
//        arguments[0] = 1000;
    }
}
           

具體的增強邏輯,在這裡可以盡情使用自定義類加載器加載的類了。

import com.netease.edu.envjavaagent.advice.AbstractEnvAdvice;
import com.netease.edu.envjavaagent.constants.EnvConstants;
import com.netease.edu.envjavaagent.utils.ThreadLocalUtil;
import org.apache.catalina.connector.Request;

import java.lang.reflect.Method;

/**
 * @author dewey
 */
public class TomcatAdvice extends AbstractEnvAdvice implements EnvConstants {

    @Override
    public Object[] onMethodEnter(Object thiz, Method method, Object[] arguments) {
        Request request = (Request) arguments[0];
        String curEnv = request.getHeader(HTTP_HEADER_ORIGIN_ENV);
        if(curEnv == null){
            ThreadLocalUtil.setEnv(STANDARD_ENV);
        }
        ThreadLocalUtil.setEnv(curEnv);
        return arguments;
    }
}
           

5. 總結

上述源碼雖然實作了類加載隔離,但是每次在執行增強時邏輯時有個動态選擇的過程

@Advice.OnMethodEnter
    public static void onMethodEnter(@Advice.This(optional = true) Object thiz,
                                     @Advice.Origin Method method,
                                     @Advice.AllArguments(readOnly = false,typing = Assigner.Typing.DYNAMIC) Object[] arguments,
                                     @EnvAdviceAnnotation String adviceClass) {
        EnvAdvice envAdvice = EnvInstrumentationDispatcher.getAdvice(adviceClass);
        if(adviceClass == null || envAdvice == null){
            return;
        }
        //必須重新指派才會被bytebuddy處理,實作參數替換
        arguments = envAdvice.onMethodEnter(thiz,method,arguments);

        //并不會執行
//        arguments[0] = 1000;
    }
           

後面可以持續深入bytebuddy來看看是否直接把真正的實作邏輯寫入目标類的位元組碼,而不是寫入EnvAdvice envAdvice = EnvInstrumentationDispatcher.getAdvice(adviceClass);。

6. 參考

  1. https://bytebuddy.net/#/
  2. https://github.com/elastic/apm-agent-java
  3. https://github.com/open-telemetry/opentelemetry-java-instrumentation
  4. https://github.com/alibaba/arthas
  5. https://www.modb.pro/db/189635
  6. https://www.bilibili.com/video/BV1eh41167Lx?from=search&seid=2331425974823529414&spm_id_from=333.337.0.0
  7. https://stackoverflow.com/questions/62077684/how-to-replace-input-arguments-using-bytebuddys-advice-allarguments
  8. 還有等等一系列相關部落格,就不一一列舉了上述幾個開源項目的實作是可以多加借鑒的。