前言:回想起多少个夜晚都被Javaagent的噩梦萦绕,简直是人间悲惨。真是应了那句:基础不牢,地动山摇!!!

1. 背景

        现阶段微服务已经日益成熟,而k8s的兴起无疑大大地加快了各大公司微服务化的脚步,它使我们能够便捷高效地管理成百上千个服务。而在大型应用的迭代开发过程中,由于敏捷开发导致的子项目、团队划分。经常存在好几个团队同时修改同一个服务或应用。这时候存在两种情况:

  1. 如果只有一套测试环境,那么这些团队在进行自测、联调、测试验收时都要去进行冲突服务的代码合并还要去解决合并后的代码冲突、bug等。严重拖慢迭代进度,使敏捷变得笨拙。
  2. 如果有多套环境,给每个团队提供一套全量的应用环境,那么冲突将不复存在。但是新的问题是如果多个团队进行协作,他们各自负责的服务在在自己的环境中已经更迭了数个版本,但是在对方的环境中由于未进行该应用迭代,可能远远落后于当前版本。这很可能在进行迭代合作时由于版本陈旧导致测试失败。而且还有一个比较大的问题就是维护多套环境的成本很高!!!在互联网倡导开源(猿)节流的今天,你不干掉它,它可能就干掉你了。(举个例子,我们当前维护一个大一点的模块单个环境成本大概1.4w/月)

        因此有什么办法既有使用一套环境的稳定、便捷又能有像多套环境的隔离性呢?
        答案是:流量染色与穿梭

2. 实现思路简介

注意:这里以一套基于k8s部署的应用为例。服务发现通过k8s自有的service解析来实现、使用feign进行服务间调用,我们暂且称该实现为榕树平台。

首先先看一下理想中的效果,如下图,基准环境中部署了全量服务。各敏捷小组在进行迭代开发时只需要新建一个属于自己的环境,然后部署需要进行更改的应用即可拥有与在基准环境中进行迭代测试的体验。
在这里插入图片描述

那么具体怎么实现呢,如下是一个简单的部署架构图。用户在web端通过nginx访问到具体的服务。每个namespace下都有一系列该业务域的服务。不同环境新建的service与基准环境的service放在同一个namespace,通过环境尾缀进行区分。每套环境对应着自己独有的nginx,请求从对应环境发起后在nginx层带上header,路由到对应的服务,而服务间的调用则利用agent技术根据当前环境的应用发布情况动态选择正确的下游服务。
在这里插入图片描述

那么要实现上述效果主要有以下几个问题:

  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包的版本冲突。这无疑加大了实施难度,所以最终没能选择该方案。
在这里插入图片描述
事实证明思路还是要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的。具体逻辑如下,就不详细介绍了:
在这里插入图片描述

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表示,其性能不如他
    在这里插入图片描述

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的类加载机制。这里也就不深入介绍了。
在这里插入图片描述
这里使用自定义类加载器会遇到一个比较棘手的问题,如下图所示
在这里插入图片描述
假如我的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中,具体加载实例如下。
在这里插入图片描述

2.2 demo

按照上述思路的实现如下:

2.2.1 包结构

在这里插入图片描述
主要包含三个jar包,他们分别的类加载器为
在这里插入图片描述

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. 还有等等一系列相关博客,就不一一列举了上述几个开源项目的实现是可以多加借鉴的。
Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐