前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >skywalking源码分析之javaAgent工具ByteBuddy的应用

skywalking源码分析之javaAgent工具ByteBuddy的应用

原创
作者头像
kl博主
发布2018-04-13 16:42:18
3.2K0
发布2018-04-13 16:42:18
举报
文章被收录于专栏:kl的专栏

前言碎语

关于skywalking请看我上一篇博文,其使用javaAgent技术,使得应用接入监控0耦合。今天在分析skywaking过程中,对javaAgent技术有了更深入的了解。skywalking使用的javaAgent工具ByteBuddy是一个比ASM更上层的针对java字节码操作的封装,基于ByteBuddy,我们可以快速方便的对java字节码进行增强处理,更高效的开发javaAgent应用。

Byte Buddy官网:http://bytebuddy.net/#/

github项目地址:https://github.com/raphw/byte-buddy

本文共分三个部分,分别为skywalking的agent模块源码分析,javaAgent技术应用,ByteBuddy工具应用

Agent模块源码分析

agent的入口方法premain在apm-sniffer模块的SkyWalkingAgent类中,整个agent逻辑如下:

第一步,加载配置信息:

加载Agentjar包所在录入的/config/agent.config文件,参数加载的优先级分别为:系统环境变量 > VM参数(-D) > /config/agent.config中的配置。所以Agent安装包的目录别轻易改动,相关的读取配置在代码里写死了的

第二步,加载需要被Agent的插件:

插件代码在apm-sck-plugin模块下,目前共有24个插件支持,包含主流Rpc如(dubbo,motan,grpc)等,下面以dubbo的Agent插件列,看skywalking如何开发相关套件的。

分两步:

  1. 实现InstanceMethodsAroundInterceptor接口,实现beforeMethod和afterMethod方法,环绕增强目标方法,如rpc和http的请求等
  2. 定义需要拦截的类和增强的方法,继承ClassInstanceMethodsEnhancePluginDefine,dubbo插件增强的是监控的MonitorFilter类中的invoke方法,所以如果你的应用没有开启数据监控服务,skywalking是收集不到dubbo的调用数据的。选择增强monitorfilter可能也是为了考量加入agent的性能问题。代码如下:
代码语言:javascript
复制
public class DubboInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {

    private static final String ENHANCE_CLASS = "com.alibaba.dubbo.monitor.support.MonitorFilter";
    private static final String INTERCEPT_CLASS = "org.skywalking.apm.plugin.dubbo.DubboInterceptor";

    @Override
    protected ClassMatch enhanceClass() {
        return byName(ENHANCE_CLASS);
    }

    @Override
    protected ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
        return null;
    }

    @Override
    protected InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[] {
            new InstanceMethodsInterceptPoint() {
                @Override
                public ElementMatchergetMethodsMatcher() {
                    return named("invoke");
                }

                @Override
                public String getMethodsInterceptor() {
                    return INTERCEPT_CLASS;
                }

                @Override
                public boolean isOverrideArgs() {
                    return false;
                }
            }
        };
    }
}

第三步,加载Agent端所需要的服务:

通过java的spi机制ServiceLoader.load(BootService.class)加载agen端的所需服务,Agent端共有七个基础service服务,分别如下

  • AppAndServiceRegisterClient:服务注册客户端服务
  • CollectorDiscoveryService:连接通讯发现服务
  • ContextManager:trace等上下文管理服务,在业务应用代码中,可在此服务中获取到当前的traceId等信息
  • GRPCChannelManager:grpc通讯连接通道管理服务
  • JVMService:jvm监控信息收集服务,主要收集cpu,内存等信息
  • SamplingService:数据取样服务,可配置,默认为-1,将所有数据发送到收集器。skywalking考虑到序列化/反序列化的CPU成本和网络带宽可以设置为不将所有采样数据发送到收集器。
  • TraceSegmentServiceClient:trace和span信息组装客户端服务

第四步,使用ByteBuddy增强插件定义的所有class:代码如下

代码语言:javascript
复制
new AgentBuilder.Default().type(pluginFinder.buildMatch()).transform(new AgentBuilder.Transformer() {
            @Override
            public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
                ListpluginDefines = pluginFinder.find(typeDescription, classLoader);
                if (pluginDefines.size() > 0) {
                    DynamicType.Builder newBuilder = builder;
                    EnhanceContext context = new EnhanceContext();
                    for (AbstractClassEnhancePluginDefine define : pluginDefines) {
                        DynamicType.Builder possibleNewBuilder = define.define(typeDescription.getTypeName(), newBuilder, classLoader, context);
                        if (possibleNewBuilder != null) {
                            newBuilder = possibleNewBuilder;
                        }
                    }
                    if (context.isEnhanced()) {
                        logger.debug("Finish the prepare stage for {}.", typeDescription.getName());
                    }

                    return newBuilder;
                }

                logger.debug("Matched class {}, but ignore by finding mechanism.", typeDescription.getTypeName());
                return builder;
            }
        }).with(new AgentBuilder.Listener() {
            @Override
            public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {

            }

            @Override
            public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module,
                boolean loaded, DynamicType dynamicType) {
                if (logger.isDebugEnable()) {
                    logger.debug("On Transformation class {}.", typeDescription.getName());
                }
            }

            @Override
            public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module,
                boolean loaded) {

            }

            @Override public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded,
                Throwable throwable) {
                logger.error("Failed to enhance class " + typeName, throwable);
            }

            @Override
            public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
            }
        }).installOn(instrumentation);

使用ByteBuddy代码字节码增强特别简单,开发agent应用不用再操作instrumentation的相关接口了

javaAgent的应用

Java agent是在另外一个Java应用(“目标”应用)启动之前要执行的Java程序,这样agent就有机会修改目标应用或者应用所运行的环境。在本文中,我们将会从基础内容开始,逐渐增强其功能,借助字节码操作工具Byte Buddy,使其成为高级的agent实现。

在最基本的用例中,Java agent会用来设置应用属性或者配置特定的环境状态,agent能够作为可重用和可插入的组件。如下的样例描述了这样的一个agent,它设置了一个系统属性,在实际的程序中就可以使用该属性了:

代码语言:javascript
复制
public class Agent {
  public static void premain(String arg) {
    System.setProperty("my-property", “foo”);
  }
}

如上面的代码所述,Java agent的定义与其他的Java程序类似,只不过它使用premain方法替代main方法作为入口点。顾名思义,这个方法能够在目标应用的main方法之前执行。相对于其他的Java程序,编写agent并没有特定的规则。有一个很小的区别在于,Java agent接受一个可选的参数,而不是包含零个或更多参数的数组。

如果要使用这个agent,必须要将agent类和资源打包到jar中,并且在jar的manifest中要将Agent-Class属性设置为包含premain方法的agent类。(agent必须要打包到jar文件中,它不能通过拆解的格式进行指定。)接下来,我们需要启动应用程序,并且在命令行中通过javaagent参数来引用jar文件的位置:

代码语言:javascript
复制
java -javaagent:myAgent.jar -jar myProgram.jar

通过重复使用javaagent命令,能够添加多个agent。

但是,Java agent的功能并不局限于修改应用程序环境的状态,Java agent能够访问Java instrumentation API,这样的话,agent就能修改目标应用程序的代码。Java虚拟机中这个鲜为人知的特性提供了一个强大的工具,有助于实现面向切面的编程。

如果要对Java程序进行这种修改,我们需要在agent的premain方法上添加类型为Instrumentation的第二个参数。Instrumentation参数可以用来执行一系列的任务,比如确定对象以字节为单位的精确大小以及通过注册ClassFileTransformers实际修改类的实现。ClassFileTransformers注册之后,当类加载器(class loader)加载类的时候都会调用它。当它被调用时,在类文件所代表的类加载之前,类文件transformer有机会改变或完全替换这个类文件。按照这种方式,在类使用之前,我们能够增强或修改类的行为,如下面的样例所示:

代码语言:javascript
复制
public class Agent {
 public static void premain(String argument, Instrumentation inst) {
   inst.addTransformer(new ClassFileTransformer() {
     @Override
     public byte[] transform(
       ClassLoader loader,
       String className,
       Class classBeingRedefined, // 如果类之前没有加载的话,值为null
       ProtectionDomain protectionDomain,
       byte[] classFileBuffer) {
       // 返回改变后的类文件。
     }
   });
 }
}

通过使用Instrumentation实例注册上述的ClassFileTransformer之后,每个类加载的时候,都会调用这个transformer。为了实现这一点,transformer会接受一个二进制和类加载器的引用,分别代表了类文件以及试图加载类的类加载器。

Java agent也可以在Java应用的运行期注册,如果是在这种场景下,instrumentation API允许重新定义已加载的类,这个特性被称之为“HotSwap”。不过,重新定义类仅限于替换方法体。在重新定义类的时候,不能新增或移除类成员,并且类型和签名也不能进行修改。当类第一次加载的时候,并没有这种限制,如果是在这样的场景下,那classBeingRedefined会被设置为null。

BYTE BUDDY应用

Byte Buddy的目的并不仅仅是为了生成Java agent。它提供了一个API用于生成任意的Java类,基于这个生成类的API,Byte Buddy提供了额外的API来生成Java agent。

作为Byte Buddy的简介,如下的样例展现了如何生成一个简单的类,这个类是Object的子类,并且重写了toString方法,用来返回“Hello World!”。与原始的ASM类似,“intercept”会告诉Byte Buddy为拦截到的指令提供方法实现:

代码语言:javascript
复制
Class dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .method(ElementMatchers.named("toString"))
  .intercept(FixedValue.value("Hello World!"))
  .make()
  .load(getClass().getClassLoader(),          
        ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

从上面的代码中,我们可以看到Byte Buddy要实现一个方法分为两步。首先,编程人员需要指定一个ElementMatcher,它负责识别一个或多个需要实现的方法。Byte Buddy提供了功能丰富的预定义拦截器(interceptor),它们暴露在ElementMatchers类中。在上述的例子中,toString方法完全精确匹配了名称,但是,我们也可以匹配更为复杂的代码结构,如类型或注解。

当Byte Buddy生成类的时候,它会分析所生成类型的类层级结构。在上述的例子中,Byte Buddy能够确定所生成的类要继承其超类Object的名为toString的方法,指定的匹配器会要求Byte Buddy重写该方法,这是通过随后的 Implementation 实例实现的,在我们的样例中,这个实例也就是FixedValue

当创建子类的时候,Byte Buddy始终会拦截(intercept)一个匹配的方法,在生成的类中重写该方法。但是,我们在本文稍后将会看到Byte Buddy还能够重新定义已有的类,而不必通过子类的方式来实现。在这种情况下,Byte Buddy会将已有的代码替换为生成的代码,而将原有的代码复制到另外一个合成的(synthetic)方法中。

在我们上面的代码样例中,匹配的方法进行了重写,在实现里面,返回了固定的值“Hello World!”。intercept方法接受Implementation类型的参数,Byte Buddy自带了多个预先定义的实现,如上文所使用的FixedValue类。但是,如果需要的话,可以使用前文所述的ASM API将某个方法实现为自定义的字节码,Byte Buddy本身也是基于ASM API实现的。

定义完类的属性之后,就能通过make方法来进行生成。在样例应用中,因为用户没有指定类名,所以生成的类会给定一个任意的名称。最终,生成的类将会使用ClassLoadingStrategy来进行加载。通过使用上述的默认 WRAPPER策略,类将会使用一个新的类加载器进行加载,这个类加载器会使用环境类加载器作为父加载器。

类加载之后,使用Java反射API就可以访问它了。如果没有指定其他构造器的话,Byte Buddy将会生成类似于父类的构造器,因此生成的类可以使用默认的构造器。这样,我们就可以检验生成的类重写了 toString方法,如下面的代码所示:

代码语言:javascript
复制
assertThat(dynamicType.newInstance().toString(), 
           is("Hello World!"));

当然,这个生成的类并没有太大的用处。对于实际的应用来讲,大多数方法的返回值是在运行时计算的,这个计算过程要依赖于方法的参数和对象的状态。

通过委托实现Instrumentation

要实现某个方法,有一种更为灵活的方式,那就是使用Byte Buddy的MethodDelegation。通过使用方法委托,在生成重写的实现时,我们就有可能调用给定类和实例的其他方法。按照这种方式,我们可以使用如下的委托器(delegator)重新编写上述的样例:

代码语言:javascript
复制
class ToStringInterceptor {
  static String intercept() {
    return “Hello World!”;
  }
}

借助上面的POJO拦截器,我们就可以将之前的FixedValue实现替换为MethodDelegation.to(ToStringInterceptor.class):

代码语言:javascript
复制
Class dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .method(ElementMatchers.named("toString"))
  .intercept(MethodDelegation.to(ToStringInterceptor.class))
  .make()
  .load(getClass().getClassLoader(),          
        ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

使用上述的委托器,Byte Buddy会在to方法所给定的拦截目标中,确定最优的调用方法。就ToStringInterceptor.class来讲,选择过程只是非常简单地解析这个类型的唯一静态方法而已。在本例中,只会考虑一个静态方法,因为委托的目标中指定的是一个类。与之不同的是,我们还可以将其委托给某个类的实例,如果是这样的话,Byte Buddy将会考虑所有的虚方法(virtual method)。如果类或实例上有多个这样的方法,那么Byte Buddy首先会排除掉所有与指定instrumentation不兼容的方法。在剩余的方法中,库将会选择最佳的匹配者,通常来讲这会是参数最多的方法。我们还可以显式地指定目标方法,这需要缩小合法方法的范围,将ElementMatcher传递到MethodDelegation中,就会进行方法的过滤。例如,通过添加如下的filter,Byte Buddy只会将名为“intercept”的方法视为委托目标:

代码语言:javascript
复制
MethodDelegation.to(ToStringInterceptor.class)
                .filter(ElementMatchers.named(“intercept”))

执行上面的拦截之后,被拦截到的方法依然会打印出“Hello World!”,但是这次的结果是动态计算的,这样的话,我们就可以在拦截器方法上设置断点,所生成的类每次调用toString时,都会触发拦截器的方法。

当我们为拦截器方法设置参数时,就能释放出MethodDelegation的全部威力。这里的参数通常是带有注解的,用来要求Byte Buddy在调用拦截器方法时,注入某个特定的值。例如,通过使用@Origin注解,Byte Buddy提供了添加instrument功能的方法的实例,将其作为Java反射API中类的实例:

代码语言:javascript
复制
class ContextualToStringInterceptor {
  static String intercept(@Origin Method m) {
    return “Hello World from ” + m.getName() + “!”;
  }
}

当拦截toString方法时,对instrument方法的调用将会返回“Hello world from toString!”。

除了@Origin注解以外,Byte Buddy提供了一组功能丰富的注解。例如,通过在类型为Callable的参数上使用@Super注解,Byte Buddy会创建并注入一个代理实例,它能够调用被instrument方法的原始代码。如果对于特定的用户场景,所提供的注解不能满足需求或者不太适合的话,我们甚至能够注册自定义的注解,让这些注解注入用户特定的值。

实现方法级别的安全性

可以看到,我们在运行时可以借助简单的Java代码,使用MethodDelegation来动态重写某个方法。这只是一个简单的样例,但是这项技术可以用到更加实际的应用之中。在本文剩余的内容中,我们将会开发一个样例,它会使用代码生成技术实现一个注解驱动的库,用来限制方法级别的安全性。在我们的第一个迭代中,这个库会通过生成子类的方式来限制安全性。然后,我们将会采取相同的方式来实现Java agent,完成相同的功能。

样例库会使用如下的注解,允许用户指定某个方法需要考虑安全因素:

代码语言:javascript
复制
@interface Secured {
  String user();
}

例如,假设应用需要使用如下的Service类来执行敏感操作,并且只有用户被认证为管理员才能执行该方法。这是通过为执行这个操作的方法声明Secured注解来指定的:

代码语言:javascript
复制
class Service {
  @Secured(user = “ADMIN”)
  void doSensitiveAction() {
    // 运行敏感代码...
  }
}

我们当然可以将安全检查直接编写到方法中。在实际中,硬编码横切关注点往往会导致复制-粘贴的逻辑,使其难以维护。另外,一旦应用需要涉及额外的需求时,如日志、收集调用指标或结果缓存,直接添加这样的代码扩展性不会很好。通过将这样的功能抽取到agent中,方法就能很纯粹地关注其业务逻辑,使得代码库能够更易于阅读、测试和维护。

为了让我们规划的库保持尽可能得简单,按照注解的协议声明,如果当前用户不具备注解的用户属性时,将会抛出IllegalStateException异常。通过使用Byte Buddy,这种行为可以用一个简单的拦截器来实现,如下面样例中的SecurityInterceptor所示,它会通过其静态的user域,跟踪当前用户已经进行了登录:

代码语言:javascript
复制
class SecurityInterceptor {

  static String user = “ANONYMOUS”

  static void intercept(@Origin Method method) {
    if (!method.getAnnotation(Secured.class).user().equals(user)) {
      throw new IllegalStateException(“Wrong user”);
    }
  }
}

通过上面的代码,我们可以看到,即便给定用户授予了访问权限,拦截器也没有调用原始的方法。为了解决这个问题,Byte Buddy有很多预定义的方法可以实现功能的链接。借助MethodDelegation类的andThen方法,上述的安全检查可以放到原始方法的调用之前,如下面的代码所示。如果用户没有进行认证的话,安全检查将会抛出异常并阻止后续的执行,因此原始方法将不会执行。

将这些功能集合在一起,我们就能生成Service的一个子类,所有带有注解方法的都能恰当地进行安全保护。因为所生成的类是Service的子类,所以它能够替代所有类型为Service的变量,并不需要任何的类型转换,如果没有恰当认证的话,调用doSensitiveAction方法就会抛出异常:

代码语言:javascript
复制
new ByteBuddy()
  .subclass(Service.class)
  .method(ElementMatchers.isAnnotatedBy(Secured.class))
  .intercept(MethodDelegation.to(SecurityInterceptor.class)
                             .andThen(SuperMethodCall.INSTANCE)))
  .make()
  .load(getClass().getClassLoader(),   
        ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded()
  .newInstance()
  .doSensitiveAction();

不过坏消息是,因为实现instrumentation功能的子类是在运行时创建的,所以除了使用Java反射以外,没有其他办法创建这样的实例。因此,所有instrumentation类的实例都应该通过一个工厂来创建,这个工厂会封装创建instrumentation子类的复杂性。这样造成的结果就是,子类instrumentation通常会用于框架之中,这些框架本身就需要通过工厂来创建实例,例如,像依赖管理的框架Spring或对象-关系映射的框架Hibernate,而对于其他类型的应用来讲,子类instrumentation实现起来通常过于复杂。

实现安全功能的JAVA AGENT

通过使用Java agent,上述安全框架的一个替代实现将会修改Service类的原始字节码,而不是重写它。这样做的话,我们就没有必要创建托管的实例了,只需简单地调用

代码语言:javascript
复制
new Service().doSensitiveAction()

即可,如果对应的用户没有进行认证的话,就会抛出异常。为了支持这种方式,Byte Buddy提供一种称之为rebase某个类的理念。当rebase某个类的时候,不会创建子类,所采用的策略是实现instrumentation功能的代码将会合并到被instrument的类中,从而改变其行为。在添加instrumentation功能之后,在被instrument的类中,其所有方法的原始代码均可进行访问,因此像SuperMethodCall这样的instrumentation,工作方式与创建子类是完全一样的。

创建子类与rebase的行为是非常类似的,所以两种操作的API执行方式是一致的,都会使用相同的DynamicType.Builder接口来描述某个类型。两种形式的instrumentation都可以通过ByteBuddy类来进行访问。为了使Java agent的定义更加便利,Byte Buddy还提供了 AgentBuilder类,它希望能够以一种简洁的方式应对一些通用的用户场景。为了定义Java agent实现方法级别的安全性,将如下的类定义为agent的入口点就足以完成该功能了:

代码语言:javascript
复制
class SecurityAgent {
  public static void premain(String arg, Instrumentation inst) {
    new AgentBuilder.Default()
    .type(ElementMatchers.any())
    .transform((builder, type) -> builder
    .method(ElementMatchers.isAnnotatedBy(Secured.class)
    .intercept(MethodDelegation.to(SecurityInterceptor.class)
               .andThen(SuperMethodCall.INSTANCE))))
    .installOn(inst);
  }
}

如果将这个agent打包为jar文件并在命令行中进行指定,那么所有带有Secured注解的方法将会进行“转换”或重定义,从而实现安全保护。如果不激活这个Java agent的话,应用在运行时就不包含额外的安全检查。当然,这意味着如果对带有注解的代码进行单元测试的话,这些方法的调用并不需要特殊的搭建过程来模拟安全上下文。Java运行时会忽略掉无法在classpath中找到的注解类型,因此在运行带有注解的方法时,我们甚至完全可以在应用中移除掉安全库。

另外一项优势在于,Java agent能够很容易地进行叠加。如果在命令行中指定多个Java agent的话,每个agent都有机会对类进行修改,其顺序就是在命令行中所指定的顺序。例如,我们可以采取这种方式将安全、日志以及监控框架联合在一起,而不需要在这些应用间增添任何形式的集成层。因此,使用Java agent实现横切的关注点提供了一种更为模块化的代码编写方式,而不必针对某个管理实例的中心框架来集成所有的代码。

特别说明:ByteBuddy部分节选Rafael Winterhalter的《Easily Create Java Agents with Byte Buddy》

译文地址:http://www.infoq.com/cn/articles/Easily-Create-Java-Agents-with-ByteBuddy

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • BYTE BUDDY应用
    • 通过委托实现Instrumentation
    • 实现方法级别的安全性
    • 实现安全功能的JAVA AGENT
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档