Enhancer - 轻量化的字节码增强组件包

本文涉及的产品
可观测链路 OpenTelemetry 版,每月50GB免费额度
简介: 一、问题描述当我们的业务发展到一定阶段的时候,系统的复杂度往往会非常高,不再是一个简单的单体应用所能够承载的,随之而来的是系统架构的不断升级与演变。一般对于大型的 To C 的互联网企业来说,整个系统都是构建于微服务的架构之上,原因是 To C 的业务有着天生的微服务化的诉求:需求迭代快、业务系统多、领域划分多、链路调用关系复杂、容忍延迟低、故障传播快。微服务化之后带来的问题也很明显:服务的管理复杂、链路的梳理复杂、系统故障会在整个链路中迅速传播。

一、问题描述
当我们的业务发展到一定阶段的时候,系统的复杂度往往会非常高,不再是一个简单的单体应用所能够承载的,随之而来的是系统架构的不断升级与演变。一般对于大型的 To C 的互联网企业来说,整个系统都是构建于微服务的架构之上,原因是 To C 的业务有着天生的微服务化的诉求:需求迭代快、业务系统多、领域划分多、链路调用关系复杂、容忍延迟低、故障传播快。微服务化之后带来的问题也很明显:服务的管理复杂、链路的梳理复杂、系统故障会在整个链路中迅速传播。

这里我们不讨论链路的依赖或服务的管理等问题,本次要解决的问题是怎么防止单个系统故障影响整个系统。这是一个复杂的问题,因为服务的传播特性,一个服务出现故障,其他依赖或被依赖的服务都会受到影响。为了找到解决问题的办法,我们试着通过 5why 提问法来找答案。

PS:这里说的系统故障,是特指由于慢调用、慢查询等影响系统性能而导致的系统故障。

Q1 怎么防止单个系统故障影响整个系统? A:避免耽搁系统的故障的传播。

Q2 怎么避免故障的传播? A:找到系统故障的原因,解决故障。

Q3 怎么找到故障的原因? A:找到并优化系统中耗时长的方法。

Q4 怎么找到系统中耗时长的方法? A:通过对特定方法进行 AOP 拦截。

Q5 怎么对特定方法做 AOP 拦截? A:通过字节码增强的方式对目标方法做拦截并植入内联代码。

通过 5why 提问法,我们得到了解决问题的方法,我们需要对目标方法做 AOP 拦截,统计业务方法及各个子方法的耗时,得到所有方法的耗时分布,快速定位到比较慢的方法,最后找出业务系统的性能瓶颈在哪里。

二、方案选型
我们知道 AOP 是一种编码思想,跟 OOP 不同,AOP 是将特定的方法逻辑,以切面的形式编织到目标方法中,这里不再赘述 AOP 的思想。

如果在网上搜一下 “AOP 的实现方式”,你会得到大致相同的结果:AOP 的实现方式是通过动态代理或 Cglib 代理。其实这不太准确,准确的来说,AOP 可以通过代理或 Advice 两种方式来实现。请注意这里说的 Advice 并不是 Spring 所依赖的 aspectj 中的 Advice,而是一种代码织入的技术,它与代理的区别在于,代码织入技术不需要创建代理类。

如果用图形表示的话,可以更简单更直观的感受到两者的区别。代码织入的方式,不会创建代理类,而是直接在目标方法的方法体的前后织入一段内联的代码,以达到增强的效果,如下图所示:

我选择代码织入技术而不是 AOP,原因是可以避免创建大量的代理类增加元空间的内存占用,另外代码织入技术更底层一些,能实现的能力更强,此外内联代码会随着原方法一起执行,性能也更好。

有了具体的技术选型的方案之后,我们还需要确定该方案的建设目标,以下整理了一些基本的目标:

三、技术方案
代码织入的时机也有多种方式,比如 Lombok 是通过在编译器对代码进行织入,主要依赖的是在 Javac 编译阶段利用 “Annotation Processor”,对自定义的注解进行预处理后生成代码然后织入;其他的像 CGLIB、ByteBuddy 等框架是在运行时对代码进行织入的,主要依赖的是 Java Agent 技术,通过 JVMTI 的接口实现在运行时对字节码进行增强。

本次的技术方案,用一句话可以概括为:通过字节码增强,对指定的目标方法进行拦截,并在方法前后织入一段内联代码,在内联代码中计算目标方法的耗时,最后将统计到的方法信息进行分析。

1 项目结构
整个方案的代码实现非常简单,用一个图描述如下:

项目的代码结构如下所示,核心代码非常少:

2 核心组件
其中 Enhancer 是增强器的入口类,在增强器启动时会扫描所有的插件:EnhancedPlugin。

EnhancedPlugin 表示的是一个执行代码增强的插件,其中定义了几个抽象方法,需要由用户自己实现:

/**

  • 执行代码增强的插件
    *
  • @auther houyi.wh
  • @date 2023-08-15 20:12:01
  • @since 0.0.1
    */
    public abstract class EnhancedPlugin {

    /**

    • 匹配特定的类型
      *
    • @return 类型匹配器
    • @since 0.0.1
      */
      public abstract ElementMatcher.Junction typeMatcher();

      /**

    • 匹配特定的方法
      *
    • @return 方法匹配器
    • @since 0.0.1
      */
      public abstract ElementMatcher.Junction methodMatcher();

      /**

    • 负责执行增强逻辑的拦截器
      *
    • @return 拦截器
    • @since 0.0.1
      */
      public abstract Class<? extends Interceptor> interceptorClass();

}

此外 EnhancedPlugin 中还需要指定一个 Interceptor,一个 Interceptor 是对目标方法执行代码增强的拦截器,主要的拦截逻辑定义在 Interceptor 中。

3 增强原理
扫描到 EnhancedPlugin 之后,会构建 ByteBuddy 的 AgentBuilder,主要的构建过程为:

(1)找到所有匹配的类型

(2)找到所有匹配的方法

(3)传入执行代码增强的 Transformer

最后通过 AgentBuilder.install 方法将增强的代码 Transformer,传递给 Instrumentation 实例,实现运行时的字节码 retransformation。

这里的 Transformer 是由 Advice 负责实现的,而在 Advice 中实现了增强逻辑的 dispatch,即根据不同的 EnhancedPlugin 可以将增强逻辑交给指定的 Interceptor 拦截器去实现,主要在拦截器中抽象了两个方法。一个是 beforeMethod,负责在目标方法调用之前进行拦截:

/**

  • 在方法执行前进行切面
    *
  • @param pluginName 绑定在该目标方法上的插件名称
  • @param target 目标方法所属的对象,需要注意的是@Advice.This不能标识构造方法
  • @param method 目标方法
  • @param arguments 方法参数
  • @return 方法执行返回的临时数据
  • @since 0.0.1
    */
    @Advice.OnMethodEnter
    public static T beforeMethod(
     // 接收动态传递过来的参数
     @PluginName String pluginName,
     // optional=true,表示this注解可以接收:构造方法或静态方法(会将this赋值为null),而不报错
     @Advice.This(optional = true) Object target,
     // 目标方法
     @Advice.Origin Method method,
     // nullIfEmpty=true,表示可以接收空参数
     @Advice.AllArguments(nullIfEmpty = true) Object[] arguments
    
    ) {
    String[] parameterNames = new String[]{};
    T transmitResult = null;
    try {
     InstanceMethodInterceptor<T> interceptor = getInterceptor(pluginName);
     // 执行beforeMethod的拦截逻辑
     transmitResult = interceptor.beforeMethod(target, method, parameterNames, arguments);
    
    } catch (Throwable e) {
     InternalLogger.AutoDetect.INSTANCE.error("InstanceMethodAdvice beforeMethod occurred error", e);
    
    }
    return transmitResult;
    }

一个是 afterMethod,负责在目标方法被调用之后进行拦截:

/**

  • 在方法执行后进行切面
    *
  • @param pluginName 绑定在该目标方法上的插件名称
  • @param transmitResult beforeMethod所传递过来的临时数据
  • @param originResult 目标方法原始返回结果,如果目标方法是void型,则originResult为null
  • @param throwable 目标方法抛出的异常
    */
    @Advice.OnMethodExit(onThrowable = Throwable.class)
    public static void afterMethod(
     // 接收动态传递过来的参数
     @PluginName String pluginName,
     // beforeMethod传递过来的临时数据
     @Advice.Enter T transmitResult,
     // typing=DYNAMIC,表示可以接收void类型的方法
     @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object originResult,
     // 目标方法自己抛出的运行时异常,可以在方法中进行捕获,看具体的需求
     @Advice.Thrown Throwable throwable
    
    ) {
    try {
     InstanceMethodInterceptor<T> interceptor = getInterceptor(pluginName);
     // 执行afterMethod的拦截逻辑
     interceptor.afterMethod(transmitResult, originResult);
    
    } catch (Throwable e) {
     InternalLogger.AutoDetect.INSTANCE.error("InstanceMethodAdvice afterMethod occurred error", e);
    
    }
    }

Advice 的特点是:不会更改目标类的字节码结构,比如:不会增加字段、方法,不会修改方法的参数等等。

四、方案实现
该增强组件是一个轻量化的通用的增强包,几乎可以实现你能想到的任意功能,本次我们的需求是要采集特定目标方法的方法耗时,以便分析出方法的性能瓶颈。

1 定义插件
基于该组件我们需要实现两个类:一个是插件,一个是拦截器。

插件中主要实现的是两个方法:匹配特定的类型,匹配特定的方法。

这里的类型匹配或方法匹配,是采用的 ByteBuddy 的 ElementMatcher,它是一个非常灵活的匹配器,在 ElementMatchers 中有很多内置的匹配实现,只要你能想到的匹配方式,通过它几乎都能实现匹配。

匹配特定的类型目前我定义了两种匹配方式,一种是根据类名 (或者包名),一种是根据方法上的注解,具体的代码实现如下:

public class MethodCallPlugin extends EnhancedPlugin {

private final List<String> anyClassNameStartWith;
private final List<String> anyAnnotationNameOnMethod;

/**
 * 方法调用拦截插件
 *
 * @param anyClassNameStartWith     任何包路径,或者全限定类名
 * @param anyAnnotationNameOnMethod 任何方法上的注解的全限定名称
 */
public MethodCallPlugin(List<String> anyClassNameStartWith, List<String> anyAnnotationNameOnMethod) {
    boolean nameStartWithInvalid = anyClassNameStartWith == null || anyClassNameStartWith.isEmpty();
    boolean annotationNameOnMethodInvalid = anyAnnotationNameOnMethod == null || anyAnnotationNameOnMethod.isEmpty();
    if (nameStartWithInvalid && annotationNameOnMethodInvalid) {
        throw new IllegalArgumentException("anyClassNameStartWith and anyAnnotationNameOnMethod can't be both empty");
    }
    this.anyClassNameStartWith = anyClassNameStartWith;
    this.anyAnnotationNameOnMethod = anyAnnotationNameOnMethod;
}

@Override
public ElementMatcher.Junction<TypeDescription> typeMatcher() {
    ElementMatcher.Junction<TypeDescription> anyTypes = none();
    if (anyClassNameStartWith != null && !anyClassNameStartWith.isEmpty()) {
        for (String classNameStartWith : anyClassNameStartWith) {
            // 根据类的前缀或者全限定类名进行匹配
            anyTypes = anyTypes.or(nameStartsWith(classNameStartWith));
        }
    }
    if (anyAnnotationNameOnMethod != null && !anyAnnotationNameOnMethod.isEmpty()) {
        ElementMatcher.Junction<MethodDescription> methodsWithAnnotation = none();
        for (String annotationNameOnMethod : anyAnnotationNameOnMethod) {
            // 根据方法上是否有特定注解进行匹配
            methodsWithAnnotation = methodsWithAnnotation.or(isAnnotatedWith(named(annotationNameOnMethod)));
        }
        anyTypes = anyTypes.or(declaresMethod(methodsWithAnnotation));
    }
    return anyTypes;
}

}

匹配特定方法的逻辑就比较简单了,可以匹配除了构造方法之外的任意方法:

public class MethodCallPlugin extends EnhancedPlugin {

@Override
public ElementMatcher.Junction<MethodDescription> methodMatcher() {
    return any().and(not(isConstructor()));
}

}

2 实现拦截器
类型匹配和方法都匹配到之后,就需要实现方法增强的拦截器了:

我们需要获取方法调用的信息,包括方法名、调用堆栈及深度、调用的耗时,所以我们需要定义三个 ThreadLocal 用来保存方法调用的堆栈:

/**

  • 方法调用信息的拦截器
  • 在方法调用之前进行拦截,将方法调用信息封装后,放入堆栈中,
  • 在方法调用之后,从堆栈中将所有方法取出来,按照进入堆栈的顺序进行排序,
  • 得到方法调用信息的列表,最后将该列表交给{@link MethodCallHandler}进行处理
  • 如果用户指定了自己的{@link MethodCallHandler}则优先使用用户自定义的Handler进行处理
  • 否则使用SDK内置的{@link MethodCallHandler.PrintLogHandler}进行处理,即将方法调用信息打印到日志中
    *
  • @auther houyi.wh
  • @date 2023-08-16 10:16:48
  • @since 0.0.1
    */
    public class MethodCallInterceptor implements InstanceMethodInterceptor {

    /**

    • 当前方法进入方法栈的顺序
    • 用以最后一个方法出栈后,进行方法调用栈的排序
      *
    • @since 0.0.1
      */
      private static final ThreadLocal methodEnterStackOrderThreadLocal = new TransmittableThreadLocal() {
      @Override
      protected AtomicInteger initialValue() {

       return new AtomicInteger(0);
      

      }
      };

      /**

    • 当前方法调用栈
      *
    • @since 0.0.1
      */
      private static final ThreadLocal> methodStackThreadLocal = new ThreadLocal>() {
      @Override
      protected Deque initialValue() {

       return new ArrayDeque<>();
      

      }
      };

      /**

    • 当前方法栈中所有方法调用的信息
      *
    • @since 0.0.1
      */
      private static final ThreadLocal> methodCallThreadLocal = new ThreadLocal>() {
      @Override
      protected ArrayList initialValue() {
       return new ArrayList<>();
      
      }
      };

}

这里主要使用了三个 ThreadLocal 来保存方法调用过程中的数据:方法的完整堆栈、方法进入堆栈的顺序、方法的调用信息列表,为什么使用 ThreadLocal 而不是 TransmittableThreadLocal,这里先按下不表,后面我们通过具体的例子来分析下原因。

紧接着,我们需要定义方法进入前的拦截逻辑,将方法调用信息压入堆栈中:

@Override
public MethodCall beforeMethod(Object target, Method method, String[] parameters, Object[] arguments) {
// 排除掉各种非法拦截到的方法
if (target == null) {
return null;
}
String methodName = target.getClass().getName() + ":" + method.getName() + "()";
Deque methodCallStack = methodStackThreadLocal.get();
// 当前方法进入整个方法调用栈的顺序
int methodEnterOrder = methodEnterStackOrderThreadLocal.get().addAndGet(1);
// 当前方法在整个方法栈中的深度
int methodInStackDepth = methodCallStack.size() + 1;

MethodCall methodCall = MethodCall.Default.of()
        .setMethodName(methodName)
        .setCallTime(System.nanoTime())
        .setThreadName(Thread.currentThread().getName())
        .setCurrentMethodEnterStackOrder(methodEnterOrder)
        .setCurrentMethodInStackDepth(methodInStackDepth);
// 将当前方法的调用信息压入调用栈
methodCallStack.push(methodCall);
return methodCall;

}

最后在方法退出时,我们需要从 ThreadLocal 中取出方法调用信息,并做相关的处理:

@Override
public void afterMethod(MethodCall transmitResult, Object originResult) {
if (target == null) {
return null;
}
Deque methodCallStack = methodStackThreadLocal.get();
MethodCall lastMethodCall = methodCallStack.pop();
// 毫秒单位的耗时
double costTimeInMills = (double) (System.nanoTime() - lastMethodCall.getCallTime()) / 1000000.0;
lastMethodCall.setCostInMills(costTimeInMills);

List<MethodCall> methodCallList = methodCallThreadLocal.get();
methodCallList.add(lastMethodCall);
// 如果堆栈空了,则说明最顶层的方法已经退出了
if (methodCallStack.isEmpty()) {
    // 对方法调用列表进行排序
    sortMethodCallList(methodCallList);
    // 获取MethodCallHandler对MethodCall的信息进行处理
    MethodCallHandler methodCallHandler = Configuration.Global.getGlobal().getMethodCallHandler();
    methodCallHandler.handle(methodCallList);

    // 方法退出时,将ThreadLocal中保存的内容清空掉,而不是将ThreadLocal remove,
    // 因为如果每次方法退出时,都将ThreadLocal都清空,当下一个方法再进入时又需要初始化新的ThreadLocal,性能会有损耗
    methodCallStack.clear();
    methodCallList.clear();
    // 将临时保存的方法调用顺序清空
    methodEnterStackOrderThreadLocal.get().set(0);
}

}

private void sortMethodCallList(List methodCallList) {
methodCallList.sort(new Comparator() {
@Override
public int compare(MethodCall o1, MethodCall o2) {
// 根据每个方法进入方法栈的顺序进行排序
return Integer.compare(o1.getCurrentMethodEnterStackOrder(), o2.getCurrentMethodEnterStackOrder());
}
});
}

需要注意的是,这里我定义了一个 MethodCallHandler 接口,该接口可以实现对采集到的方法调用信息的处理,用户可以自定义自己的 MethodCallHandler。组件中也提供了默认的实现,即将采集到的方法调用信息打印到日志中:

五、方案测试
1 普通方法
我们定义一个方法调用的测试样例类,其中定义了很多普通的方法,如下所示:

public class MethodCallExample {
public void costTime1() {
System.out.println("costTime1");
randomSleep();
innerCostTime1();
}
public void costTime2() {
System.out.println("costTime2");
randomSleep();
innerCostTime2();
}
public void costTime3() {
System.out.println("costTime3");
randomSleep();
}
public void innerCostTime1() {
System.out.println("innerCostTime1");
randomSleep();
}
public void innerCostTime2() {
System.out.println("innerCostTime2");
randomSleep();
}
private void randomSleep() {
Random random = new Random();
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

启动 Enhancer,并调用测试样例中的方法:

public static void main(String[] args) {
MethodCallPlugin plugin = new MethodCallPlugin(Collections.singletonList("com.shizhuang.duapp.enhancer.example"), null);
Enhancer enhancer = Enhancer.Default.INSTANCE;
enhancer.enhance(Configuration.of().setPlugins(Collections.singletonList(plugin)));

MethodCallExample example = new MethodCallExample();
example.costTime1();
example.costTime2();
example.costTime3();

try {
    // 这里主要是防止主线程提前结束
    Thread.sleep(5000);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}

}

执行后,可以得到如下的结果:

从结果上看已经可以满足绝大多数的情况了,我们拿到了每个方法的调用耗时,以及整个方法的调用堆栈信息。

但是这里的方法都是同步方法,如果有异步方法,会怎么样呢?

2 异步方法
我们将其中一个方法改成异步线程执行:

private void randomSleep() {
new Thread(() -> {
Random random = new Random();
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}

再次执行后,得到如下的结果:

从结果中可以看到,因为 randomSleep 方法中通过 Thread 变成了异步执行,而增强器拦截到的 randomSleep 实际是 Thread.start () 的方法耗时,Thread 内部的 Runnable 的方法耗时没有采集到。

3 表达式
为什么 Runnable 的方法耗时没有采集到呢?原因是 Runnable 内部是一个 lambda 表达式,生成的是一个匿名方法,而匿名方法的默认是无法被拦截到的。

具体的原因可以参考这篇文章:

ByteBuddy 的作者解释了 lambda 的特殊性,包括为什么无法对 lambda 做 instrument,以及 ByteBuddy 为了实现对 lambda 表达式的拦截做了一些支持。

不过只在 OpenJDK8u40 版本以上才能生效,因为之前版本的 JDK 在 invokedynamic 指令上有 bug。

我们打开这个 Lambda 的策略开关:

可以拦截到 lambda 表达式生成的匿名方法了:

如果我们不打开 Lambda 的策略开关,也可以将匿名方法实现为具名方法:

private void randomSleep() {
new Thread(() -> {
doSleep();
}).start();
}

private void doSleep() {
Random random = new Random();
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

甚至可以拦截到 lambda 方法中的具名方法:

4 TransmittableThreadLocal
上面我提了一个问题,为什么拦截器中保存方法调用信息的 ThreadLocal 不用 TransmittableThreadLocal,而是用普通的 ThreadLocal,这里我们把拦截器中的代码改一下:

执行后发现效果如下:

可以看到异步方法和主方法合并到一起了,原因是我们保存方法调用堆栈信息使用了 TransmittableThreadLocal,而 TTL 是会在主子线程中共享变量的,当主线程中的 costTime1 方法还未退出堆栈时,子线程中的 doSleep 方法已经进入堆栈了,所以导致堆栈信息一直未清空,而我们是在每个方法退出时判断当前线程中的堆栈是否为空,如果为空则说明方法调用的最顶层方法已经退出了,但是 TTL 导致堆栈不为空,只有当所有方法执行完毕后堆栈才为空,所以出现了这样的情况。所以这里保存方法调用堆栈的 ThreadLocal 需要用原生的 ThreadLocal。

5 串联主子线程
那么怎么实现一个方法的主方法在不同的主子线程中串起来呢?

通过常规的共享堆栈的方案无法实现主子线程中的方法的串联,那么可以通过 TraceId 来实现方法的串联,链路追踪的技术方案中提供了 TraceId 和 rpcId 两字字段,分别用来表示一个请求的唯一链路以及每个方法在该链路中的顺序 (通过 rpcId 来表示)。这里我们只需要利用链路追踪里面的 TraceId 来串联同一个方法即可。具体的原理可以描述如下:

由于不同的链路追踪的实现方式不同,我这里定义了一个 Tracer 接口,由用户指定具体的 Tracer 实现:

/**

  • 链路追踪器
    *
  • @auther houyi.wh
  • @date 2023-08-22 14:59:50
  • @since 0.0.1
    */
    public interface Tracer {

    /**

    • 获取链路id
      *
    • @return 链路id
    • @since 0.0.1
      */
      String getTraceId();

      /**

    • 一个空的实现类
    • @since 0.0.1
      */
      enum Empty implements Tracer {
      INSTANCE;

      @Override
      public String getTraceId() {

       return "";
      

      }
      }
      }

然后在 Configuration 中设置该 Tracer:

// 启动代码增强
Enhancer enhancer = Enhancer.Default.INSTANCE;
Configuration config = Configuration.of()
// 指定自定义的Tracer
.setTracer(yourTracer)
.xxx() // 其他配置项
;
enhancer.enhance(config);

需要注意的是,如果不指定 Tracer,则会默认使用内置的空实现:

六、性能测试
该组件的主要是通过拦截器进行代码增强,因为我们需要对拦截器的 beforeMethod 和 afterMethod 进行性能测试,通常常规的性能测试,是通过 JMH 基准测试工具来做的。

我们定义一个基准测试的类:

/*

  • 因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。
  • 所以为了让 benchmark 的结果更加接近真实情况就需要进行预热
  • 其中的参数 iterations 是预热轮数
    /
    @Warmup(iterations = 1)
    /
  • 基准测试的类型:
  • Throughput:吞吐量,指1s内可以执行多少次操作
  • AverageTime:调用时间,指1次调用所耗费的时间
    /
    @BenchmarkMode({Mode.AverageTime, Mode.Throughput})
    /
  • 测试的一些度量
  • iterations:进行测试的轮次
  • time:每轮进行的时长
  • timeUnit:时长单位
    /
    @Measurement(iterations = 2, time = 1)
    /
  • 基准测试结果的时间类型。一般选择秒、毫秒、微秒。
    /
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    /
  • fork出几个进场进行测试。
  • 如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
    /
    @Fork(value = 2)
    /
  • 每个进程中测试线程的个数。
    /
    @Threads(8)
    /
  • State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。
  • 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。
  • Scope 主要分为三种:
  • Thread - 该状态为每个线程独享。
  • Group - 该状态为同一个组里面所有线程共享。
  • Benchmark - 该状态在所有线程间共享。
    */
    @State(Scope.Benchmark)
    public class MethodCallInterceptorBench {

    private MethodCallInterceptor methodCallInterceptor;
    private Object target;
    private Method method;
    private String[] parameters;
    private Object[] arguments;

    @Setup
    public void prepare() {

     methodCallInterceptor = new MethodCallInterceptor();
     target = new MethodCallExample();
     try {
         method = target.getClass().getMethod("costTime1");
     } catch (NoSuchMethodException e) {
         throw new RuntimeException(e);
     }
     parameters = null;
     arguments = null;
    

    }

    @Benchmark
    public void testMethodCallInterceptor_beforeMethod() {

     methodCallInterceptor.beforeMethod(target, method, parameters, arguments);
    

    }

    public static void main(String[] args) throws RunnerException {

     Options opt = new OptionsBuilder()
             .include(MethodCallInterceptorBench.class.getSimpleName())
             .build();
    
     new Runner(opt).run();
    

    }

}

基准测试的结果如下:

针对 beforeMethod 方法做了吞吐量和平均耗时的测试,每次调用的平均耗时为 0.592ms,而吞吐量则为 1ms 内可以执行 82.99 次调用。

七、使用方式
引入该 Enhancer 组件的依赖:


com.shizhuang.duapp
commodity-common-enhancer
${commodity-common-enhancer-version}

使用很简单,只需要在项目启动之后,调用代码增强的方法即可,对现有的业务代码几乎无侵入。

不指定配置信息,直接启动:

public class CommodityAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CommodityAdminApplication.class, args);
// 启动代码增强
Enhancer enhancer = Enhancer.Default.INSTANCE;
enhancer.enhance(null);
}
}

指定配置信息启动:

public class CommodityAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CommodityAdminApplication.class, args);
// 启动代码增强
Enhancer enhancer = Enhancer.Default.INSTANCE;
Configuration config = Configuration.of()
.setPlugins(Collections.singletonList(plugin))
.xxx() // 其他配置项
;
enhancer.enhance(config);
}
}

1 实现方法耗时过滤
比如你只想对方法耗时大于 xx 毫秒的方法进行分析,你可以在定义的 MethodCallHandler 中引入 ark 配置,然后过滤出耗时大于 xx 毫秒的方法,如:

enum MyCustomHandler implements MethodCallHandler {
INSTANCE;

private double maxCostTime() {
    // 这里可以通过动态配置想要分析的方法耗时的最小值
    return 500;
}

@Override
public void handle(List<MethodCall> methodCallList) {
    logger.info("=========================================================================");
    // 检查方法耗时超过xx时,才打印
    MethodCall firstMethodCall = methodCallList.stream().findFirst().orElse(null);
    if (firstMethodCall == null) {
        return;
    }
    // 方法耗时
    double costInMills = firstMethodCall.getCostInMills();
    int currentMethodEnterStackOrder = firstMethodCall.getCurrentMethodEnterStackOrder();
    // 如果整体的方法小于500毫秒,则直接放弃
    if (currentMethodEnterStackOrder == 1 && costInMills < maxCostTime()) {
        return;
    }
    // 然后在这里实现方法耗时的打印
    logger.info(getMethodCallInfo(methodCallList));
}

}

2 实现整体开关控制
比如你想通过动态开关来控制对方法耗时的统计分析,可以实现 MethodCallSwither 接口,然后在 Configuration 中传入自定义的 MethodCallSwitcher,如下所示:

请注意,如果用户不指定 MethodCallSwitcher,SDK 会使用内置的 MethodCallSwitcher.NeverStop 实现,表示永远不会停止采集。

/**

  • 是否停止采集MethodCall的开关
    *
  • @auther houyi.wh
  • @date 2023-08-27 18:56:47
  • @since 0.0.1
    */
    public interface MethodCallSwitcher {

    /**

    • 是否停止对方法的MethodCall的采集
    • 如果返回true,则会停止对方法MethodCall的采集
      *
    • @return true:停止采集 false:继续采集
      */
      boolean stopScratch();

      /**

    • 永远不停止采集
      */
      enum NeverStop implements MethodCallSwitcher {
      INSTANCE;

      @Override
      public boolean stopScratch() {

       // 一直进行采集
       return false;
      

      }
      }

}

八、扩展能力
用户如果想要实现自己的扩展能力,只需要实现 EnhancedPlugin,以及 Interceptor 即可。

1 实现自定义插件
通过如下方式实现自定义插件:

public MyCustomePlugin extends EnhancedPlugin {

@Override
public ElementMatcher.Junction<TypeDescription> typeMatcher() {
  // 实现类型匹配
}

@Override
public ElementMatcher.Junction<MethodDescription> methodMatcher() {
  // 实现方法匹配
}

@Override
public Class<? extends Interceptor> interceptorClass() {
  // 指定拦截器
  return MyInterceptor.class;
}

}

2 实现拦截器
// 临时传递数据的对象
public class Carrier {

}

public class MyInterceptor implements InstanceMethodInterceptor {

@Override
public Carrier beforeMethod(Object target, Method method, String[] parameters, Object[] arguments) {
     // 实现方法调用前拦截
}
@Override
public void afterMethod(Carrier transmitResult, Object originResult) {
    // 实现方法调用后拦截
}

}

3 启动插件
最后在项目启动时,启用自定义的插件,如下所示:

public class CommodityAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CommodityAdminApplication.class, args);
// 启动代码增强
Enhancer enhancer = Enhancer.Default.INSTANCE;
Configuration config = Configuration.of()
// 指定自定义的插件
.setPlugins(Collections.singletonList(new MyCustomePlugin()))
.xxx() // 其他配置项
;
enhancer.enhance(config);
}
}

九、总结与规划
本篇文章我们介绍了在项目中遇到的性能诊断的需求和场景,并提供了一种通过插桩的方式对具体方法进行分析的技术方案,介绍了方案中遇到的难点以及解决方法,以及实际使用过程中可能存在的扩展场景。

未来我们将使用 Enhancer 在运行时动态的获取应用系统的性能分析数据,比如通过对某些性能有问题的嫌疑代码进行增强,提取到性能分析的数据后,最后结合 Grafana 大盘,

相关实践学习
基于OpenTelemetry构建全链路追踪与监控
本实验将带领您快速上手可观测链路OpenTelemetry版,包括部署并接入多语言应用、体验TraceId自动注入至日志以实现调用链与日志的关联查询、以及切换调用链透传协议以满足全链路打通的需求。
分布式链路追踪Skywalking
Skywalking是一个基于分布式跟踪的应用程序性能监控系统,用于从服务和云原生等基础设施中收集、分析、聚合以及可视化数据,提供了一种简便的方式来清晰地观测分布式系统,具有分布式追踪、性能指标分析、应用和服务依赖分析等功能。 分布式追踪系统发展很快,种类繁多,给我们带来很大的方便。但在数据采集过程中,有时需要侵入用户代码,并且不同系统的 API 并不兼容,这就导致了如果希望切换追踪系统,往往会带来较大改动。OpenTracing为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范。OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。Skywalking基于OpenTracing规范开发,具有性能好,支持多语言探针,无侵入性等优势,可以帮助我们准确快速的定位到线上故障和性能瓶颈。 在本套课程中,我们将全面的讲解Skywalking相关的知识。从APM系统、分布式调用链等基础概念的学习加深对Skywalking的理解,从0开始搭建一套完整的Skywalking环境,学会对各类应用进行监控,学习Skywalking常用插件。Skywalking原理章节中,将会对Skywalking使用的agent探针技术进行深度剖析,除此之外还会对OpenTracing规范作整体上的介绍。通过对本套课程的学习,不止能学会如何使用Skywalking,还将对其底层原理和分布式架构有更深的理解。本课程由黑马程序员提供。
相关文章
|
1月前
|
Java API 开发者
【Java模块化新飞跃】JDK 22模块化增强:构建更灵活、更可维护的应用架构!
【9月更文挑战第9天】JDK 22的模块化增强为开发者构建更灵活、更可维护的应用架构提供了强有力的支持。通过模块化设计、精细的依赖管理和丰富的工具支持,开发者可以更加高效地开发和管理应用,提高应用的性能和可维护性。
60 10
|
3月前
|
Java 关系型数据库 MySQL
GraalVM 静态编译下 OTel Java Agent 的自动增强方案与实现
在 2024 OpenTelemetry Community Day 会议中,阿里云可观测工程师张乎兴(望陶)和饶子昊(铖朴)为大家带来了《GraalVM 静态编译下 OTel Java Agent 的自动增强方案与实现》的演讲分享,介绍阿里云在相关领域的探索方案,本文是相关分享对应的中文整理。
237 17
|
2月前
|
监控 Java API
分布式链路监控系统问题之对Java应用实现字节码增强的方式的问题如何解决
分布式链路监控系统问题之对Java应用实现字节码增强的方式的问题如何解决
|
2月前
|
人工智能 自然语言处理 前端开发
LangChain 构建问题之MetaGPT 和 ChatDev 的支持功能差异如何解决
LangChain 构建问题之MetaGPT 和 ChatDev 的支持功能差异如何解决
40 0
|
4月前
|
存储 XML Java
探索Java常用的包:从核心到扩展
探索Java常用的包:从核心到扩展
66 3
|
4月前
|
C++
c++语言核心技术要点,《运行时类型识别RTTI》
c++语言核心技术要点,《运行时类型识别RTTI》
45 2
|
4月前
|
运维 JavaScript Serverless
函数计算产品使用问题之构建自己的流水线时,已经有一个基础第三方包的层,想在这个基础上进行额外的第三方包安装,该如何实现
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
|
存储 安全 编译器
嵌入 WebAssembly 运行时和实例化 WebAssembly 模块的几大要素
嵌入 WebAssembly 运行时和实例化 WebAssembly 模块的几大要素
|
存储 测试技术 数据安全/隐私保护
RobotFrameWork接口项目分层及通用控制方式
RobotFrameWork接口项目分层及通用控制方式
1001 0
RobotFrameWork接口项目分层及通用控制方式
|
JavaScript 前端开发 安全
hyengine - 面向移动端的高性能通用编译/解释引擎
"有hyengine就够全家用了" - hyengine是为统一移动技术所需的各种脚本语言(wasm/js/python 等)执行引擎而生,以轻量级、高性能、多语言支持为设计和研发目标。目前已通过对 wasm3/quickjs 的 jit 编译及 runtime 优化,以极小包体积的代价实现了 wasm/js 执行速度 2~3 倍的提升,未来将通过实现自有字节码和 runtime 增加对 python 及其他语言的支持。
hyengine - 面向移动端的高性能通用编译/解释引擎