一文探索【skywalking】如何通过agent实现启动流程

一文探索【skywalking】如何通过agent实现启动流程_第1张图片

概述

本文的主角是一款非常著名且使用的链路追踪工具:SkyWalking

SkyWalking是一个APM(application performance monitor)系统,专门为微服务、云原生和基于容器(Docker、Kubernetes、Mesos)的架构而设计,包含了云原生架构下的分布式系统的监控、跟踪、诊断功能。

通过agent的方式,可以做到对代码无侵入式的介入,实时实现整个服务链路的监控。本文的将要介绍的重点是SkyWalking如何通过agent实现如此全面的监控功能。这里提到的agent是在java中使用的agent技术,本文所讲的内容也是依托于java为基础的。

一、什么是agent?

1.1 静态agent

1.1.1 简介

JDK从1.5开始,引入了agent机制,用户可以通过-javaagent参数使用agent技术。agent技术可以使JVM在加载class文件之前先加载agent文件,通过修改JVM传入的字节码来实现自定义代码的注入。

为什么称之为静态agent?因为使用此种方式,不需要通过指定VM参数的方式,所以想要修改agent必须要对服务进行重启。

1.1.2 使用

下面我们动手实现一个简单的静态agent。

1.1.2.1 配置

实现静态agent需要配置agent的启动类,用来发现方法premain,这是实现静态agent的启动方法,因为是在jvm加载类之前,所以叫做pre-agent

静态agent通常有两种方式:

  • MANIFEST.MF 文件

    需要在resources下创建META-INF文件夹,在内部创建MANIFEST.MF文件,其格式如下(注意最后一行要换行,否则idea或报错):

    Manifest-Version: 1.0
    Premain-Class: com.wjbgn.warriors.agent.StaticAgentTest
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    
    

    除此之外,还需要引入maven-assembly-plugin插件,否则MANIFEST.MF文件的内容会被maven打包后的内容覆盖掉。

    
        org.apache.maven.plugins
        maven-assembly-plugin
        
            
                jar-with-dependencies
            
            
                
                    src/main/resources/META-INF/MANIFEST.MF
                
            
        
    
    
  • 【推荐】引入编译插件 maven-assembly-plugin

    直接使用maven-assembly-plugin插件就能达到使用agent的效果,所以推荐此方式。

    
        org.apache.maven.plugins
        maven-assembly-plugin
        
            
                jar-with-dependencies
            
            
                
                    com.wjbgn.warriors.agent.StaticAgentTest
                    true
                    true
                
            
        
    
    

1.1.2.2 测试

  • 创建一个测试类:

    package com.wjbgn.warriors.agent;
    
    import java.lang.instrument.Instrumentation;
    
    /**
     * @description: 静态agent测试类
     * @author:weirx
     * @date:2022/6/30 15:13
     * @version:3.0
     */
    public class StaticAgentTest {
    
        /**
         * description: 静态agent启动类
         * @param agentArgs
         * @param inst
         * @return: void
         * @author: weirx
         * @time: 2022/6/30 15:14
         */
        public static void premain(String agentArgs, Instrumentation inst) {
            // 在springboot启动前打印一下文字
            System.out.println("this is static agent");
            // 打印vm参数配置的agent参数
            System.out.println(agentArgs);
        }
    }
    
  • 使用assembly插件打包,在idea中:

    一文探索【skywalking】如何通过agent实现启动流程_第2张图片

    当然也可以使用命令:mvn assembly:single

    打包后文件在项目的target下。

  • 在idea添加启动参数:

    一文探索【skywalking】如何通过agent实现启动流程_第3张图片

    蓝色部分是携带的参数,其余部分指定agent的jar包位置,完整命令如下:

    -javaagent:E:\workspace\warriors\warriors-agent\target\warriors-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar=[testAgnet]
    
  • 启动项目

    一文探索【skywalking】如何通过agent实现启动流程_第4张图片

    如上所示,成功输出我们预期内容。

1.2 动态agent

1.2.1 简介

JDK在1.6版本开始,又引入了attach方式,对于运行中的应用程序,可以对其附加agent,这一操作让我们可以动态的修改已经加载的类。这也是称之为动态agent的原因。

通过VirtualMachineattach(pid)可以获得VirtualMachine实例,之后通过loadAgent(agent path)方法将指定的agent加载到正在运行的JVM当中,实现动态加载。

1.2.2 使用

1.2.2.1 配置

想要使用VirtualMachine,需要引入对应的依赖:


    com.sun
    tools
    1.8
    system
    ${java.home}/../lib/tools.jar

修改配置文件:


    org.apache.maven.plugins
    maven-assembly-plugin
    
        
            jar-with-dependencies
        
        
            
                com.wjbgn.warriors.agent.DynamicAgentTest
                true
                true
            
        
    

1.2.2.2 准备测试环境

我们静态agent工程启动,保持在运行中:

一文探索【skywalking】如何通过agent实现启动流程_第5张图片

查看其进程id:

E:\workspace\warriors>jps
15248 Launcher
6480 Jps
13764 RemoteMavenServer36
4532 WarriorsAgentApplication
9188

WarriorsAgentApplication的进程id是4532

1.2.2.3 测试

  • 创建测试类

    package com.wjbgn.warriors.agent;
    
    import com.sun.tools.attach.AgentInitializationException;
    import com.sun.tools.attach.AgentLoadException;
    import com.sun.tools.attach.AttachNotSupportedException;
    import com.sun.tools.attach.VirtualMachine;
    
    import java.io.IOException;
    import java.lang.instrument.Instrumentation;
    
    /**
     * @description: 动态agent测试
     * @author:weirx
     * @date:2022/6/30 15:53
     * @version:3.0
     */
    public class DynamicAgentTest {
    
        /**
         * description: 动态agent,通过attach和loadAgent进行探针载入
         *
         * @param agentArgs
         * @param inst
         * @return: void
         * @author: weirx
         * @time: 2022/6/30 15:54
         */
        public static void agentmain(String agentArgs, Instrumentation inst) {
            // 打印以下日志
            System.out.println("this is static agent");
            // 打印参数
            System.out.println(agentArgs);
        }
    
        public static void main(String[] args) {
            VirtualMachine virtualMachine = null;
            try {
                // 指定进程号
                virtualMachine = VirtualMachine.attach("4532");
                // 指定agent jar包路径和参数
                virtualMachine.loadAgent("E:\workspace\warriors\warriors-agent\target\warriors-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar", "agentTest");
            } catch (AttachNotSupportedException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (AgentLoadException e) {
                e.printStackTrace();
            } catch (AgentInitializationException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    如上所示,包含两部分:

    • agentmain 启动程序
    • main方法执行attach和loadAgent方法,加载agent到当前指定进程id的应用程序中。
  • 使用assembly插件打包,在idea中:

    一文探索【skywalking】如何通过agent实现启动流程_第6张图片

    当然也可以使用命令:mvn assembly:single

    打包后文件在项目的target下。

  • 启动程序

运行上面的main方法,查看正在运行的4532项目的控制台:

一文探索【skywalking】如何通过agent实现启动流程_第7张图片

如上所示,已经输出了动态注入的内容。

1.3 Instrumentation

无论是静态agent,还是动态,我们发现在其对应的方法当中,都有一个Instrumentation的入参,那么它是做什么的呢?

使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

更多介绍参考:Java Instrumentation

1.4 小结

关于静态agent动态agent的简单介绍以及使用方式就介绍这么多,当然还有很多细节和有用的方面没有涉及,但是本节的重点意义在于让大家了解agent的工作方式,方便我们后面学习skywalking的agent实现。源码阅读过程中会针对涉及到的agent技术进行讲解。

二、SkyWalking源码分析

前面花费大量的篇章来解释什么是agent,从本节开始,正式进入源码阅读阶段。

需要单独说明的

  • 如果你是用的是8.7.0之前的版本,skywalking的服务端和agent的代码是放在同一个项目工程skywalking下的。

  • 如果使用8.7.0之后的版本,agent的相关代码被抽离出skywalking当中,不同的语言会对应不同的agent,需要根据需要去自行选择,比如java使用skywalking-java

  • 在开始学习源码前,建议按照上面的说明提供的地址拷贝一份源码到本地,有利于对源码的学习。

skywalking是采用静态agent的方式,所以我们首先要找到它的agent启动类SkyWalkingAgent.java的位置:

一文探索【skywalking】如何通过agent实现启动流程_第8张图片

2.1 premain 方法

我们从上至下分析下premain的逻辑,只看关键代码位置:

2.1.1 加载插件

2.1.1.1 定义插件寻找器

 // 定义一个插件寻找器 
final PluginFinder pluginFinder;

2.1.1.2 加载配置

   // 初始化指定参数,我们启动项目时候,可以指定服务的名称等参数,内部还加载配置文件agent.config的内容 
   SnifferConfigInitializer.initializeCoreConfig(agentArgs);

使用过skywalking的都知道,我们有两种配置被监控你服务的方式:

  • vm参数指定
  • agent.conf 配置

不论何种方式,都会通过此行代码进行加载。

2.1.1.3 加载插件

    //初始化插件寻找器 
    //使用PluginResourcesResolver加载并定义所有插件 
    pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());

new PluginBootstrap().loadPlugins()方法内部使用PluginResourcesResolver加载所有的组件,并且要求组件必须定义在文件skywalking-plugin.def当中:

一文探索【skywalking】如何通过agent实现启动流程_第9张图片

如上图所示,我们常用的组件都会有其对应的插件定义在这个工程中,需要加载的类都会通过skywalking-plugin.def进行配置,该文件格式如下所示:

new PluginBootstrap().loadPlugins()加载插件的流程如下:

一文探索【skywalking】如何通过agent实现启动流程_第10张图片

上图流程简介如下:

  • 初始化PluginBootstrap调用其loadPlugins()
  • loadPlugins方法内部初始化PluginResourcesResolver,并调用其getResources()
  • getResource方法内部使用AgentClassLoadergetResources("skywalking-plugin.def")方法获取所有插件下的skywalking-plugin.def文件。
  • AgentClassLoader的底层实际是ClassLoader,使用它的根据文件名称获取文件方法getResources(String name),得到Enumeration集合,通过遍历获取List
  • 遍历List,通过PluginCfg.INSTANCE.load加载插件。
    • 通过pluginDefine = reader.readLine()逐行读取内容
    • 通过PluginDefine.build(pluginDefine)构建读取到的内容
    • 实际build内部就是通过=进行截取,分别获取插件名称pluginName和类定义defineClass
    • 通过构造返回插件定义的实例new PluginDefine(pluginName, defineClass)
  • 返回插件定义集合List
  • 遍历插件集合,使用反射的方式创建插件实例。
  • 返回所有已经加载的插件集合List

得到所有的插件集合后,new PluginFinder(List)的主要作用是为已加载的插件做缓存,并且提供快速查找已加载插件的能力。

2.1.2 创建AgentBuilder

ByteBuddy:Byte Buddy是一个字节码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。

2.1.2.1 实例化ByteBuddy

    final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));

其中IS_OPEN_DEBUGGING_CLASS如果开启,skywalking会相对于agent根目录的位置创建文件夹,记录这些被插桩的类。用来和skywalking开发者解决兼容性问题。

2.1.2.2 创建AgentBulider

用来定义ByteBuddy的一些行为。如下忽略无关的类:

    AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(
        nameStartsWith("net.bytebuddy.")
                .or(nameStartsWith("org.slf4j."))
                .or(nameStartsWith("org.groovy."))
                .or(nameContains("javassist"))
                .or(nameContains(".asm."))
                .or(nameContains(".reflectasm."))
                .or(nameStartsWith("sun.reflect"))
                .or(allSkyWalkingAgentExcludeToolkit())
                .or(ElementMatchers.isSynthetic()));

2.1.2.3 定义边缘类集合:EdgeClasses

    JDK9ModuleExporter.EdgeClasses edgeClasses = new JDK9ModuleExporter.EdgeClasses();

其实EdgeClasses就是一个List,其中会包含ByteBuddy的核心类。

    public class ByteBuddyCoreClasses {
        private static final String SHADE_PACKAGE = "org.apache.skywalking.apm.dependencies.";

        public static final String[] CLASSES = {
            SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.RuntimeType",
            SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.This",
            SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.AllArguments",
            SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.AllArguments$Assignment",
            SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.SuperCall",
            SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.Origin",
            SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.Morph",
            };
    }

通过这些报名,我们发现其实它们是一些annotation(注解)

2.1.2.4 将EdgeClasses注入BoostrapClassLoader

    BootstrapInstrumentBoost.inject(pluginFinder, instrumentation, agentBuilder, edgeClasses);

这个位置涉及到后面要讲解的东西,暂时先不讲解。

但是关于类加载器的内容要简单描述一下,我们了解类加载器的同学应该知道java的类加载关系:

一文探索【skywalking】如何通过agent实现启动流程_第11张图片

为什么要注入到BoostrapClassLoader?

自定义的ClassLoader只能在最下层,而AgentClassLoader通过字节码修改的类,是不能够被BootStrapClassLoader直接使用的,所以需要注入进去。

2.1.2.5 打开读边界

这行主要为了解决jdk9中模块系统的跨模块类访问问题。与本文重点无关,略过。

    JDK9ModuleExporter.openReadEdge(instrumentation, agentBuilder, edgeClasses);

2.1.2.6 AgentBuilder属性设置

    agentBuilder
            // 指定ByteBuddy修改的符合条件的类
            .type(pluginFinder.buildMatch())
            // 指定字节码增强工具
            .transform(new Transformer(pluginFinder))
            //指定字节码增强模式,REDEFINITION覆盖修改内容,RETRANSFORMATION保留修改内容(修改名称),
            .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
            // 注册监听器-监听异常情况,输出日子
            .with(new RedefinitionListener())
            // 对transform和异常情况监听,输出日志
            .with(new Listener())
            // 将定义好的agent注册到instrumentation
            .installOn(instrumentation);

2.1.3 加载服务

ServiceManager.INSTANCE.boot();

如上所示ServiceManager基于ServerLoader实现,是JDK提供的一种SPI机制。

boot()方法:

public void boot() {
    // 加载所有的服务到bootServices
    bootedServices = loadAllServices();
    // 准备
    prepare();
    // 启动
    startup();
    //完成
    onComplete();
}

2.1.3.1 bootedServices是什么?

    private Map bootedServices = Collections.emptyMap();

其中的BootService是一个接口,其包含了服务的生命周期,插件开始工作时,需要被启动,如下所示:

    public interface BootService {
        /**
         * 准备
         * @throws Throwable
         */
        void prepare() throws Throwable;
        /**
         * 启动
         * @throws Throwable
         */
        void boot() throws Throwable;
        /**
         * 完成
         * @throws Throwable
         */
        void onComplete() throws Throwable;
        /**
         * 停止
         * @throws Throwable
         */
        void shutdown() throws Throwable;

        /**
         * 优先级,优先级高的BootService会优先启动
         */
        default int priority() {
            return 0;
        }
    }

所有实现了BootService接口的服务都会在此被加载。

2.1.3.2 loadAllServices()

这个方法通过jdk提供的SPI机制,ServiceLoader将需要的类加载进来,而这些类被定义在指定的配置文件当中:

配置文件内容如下:

    org.apache.skywalking.apm.agent.core.remote.TraceSegmentServiceClient
    org.apache.skywalking.apm.agent.core.context.ContextManager
    org.apache.skywalking.apm.agent.core.sampling.SamplingService
    org.apache.skywalking.apm.agent.core.remote.GRPCChannelManager
    org.apache.skywalking.apm.agent.core.jvm.JVMMetricsSender
    org.apache.skywalking.apm.agent.core.jvm.JVMService
    org.apache.skywalking.apm.agent.core.remote.ServiceManagementClient
    org.apache.skywalking.apm.agent.core.context.ContextManagerExtendService
    org.apache.skywalking.apm.agent.core.commands.CommandService
    org.apache.skywalking.apm.agent.core.commands.CommandExecutorService
    org.apache.skywalking.apm.agent.core.profile.ProfileTaskChannelService
    org.apache.skywalking.apm.agent.core.profile.ProfileSnapshotSender
    org.apache.skywalking.apm.agent.core.profile.ProfileTaskExecutionService
    org.apache.skywalking.apm.agent.core.meter.MeterService
    org.apache.skywalking.apm.agent.core.meter.MeterSender
    org.apache.skywalking.apm.agent.core.context.status.StatusCheckService
    org.apache.skywalking.apm.agent.core.remote.LogReportServiceClient
    org.apache.skywalking.apm.agent.core.conf.dynamic.ConfigurationDiscoveryService
    org.apache.skywalking.apm.agent.core.remote.EventReportServiceClient
    org.apache.skywalking.apm.agent.core.ServiceInstanceGenerator

通过下面的方法将上面配置的所有实现了bootService的类加载到allServices当中。

    void load(List allServices) {
        for (final BootService bootService : ServiceLoader.load(BootService.class, AgentClassLoader.getDefault())) {
            allServices.add(bootService);
        }
    }

在上一步加载完全部的类之后,需要去遍历这些类,得到一个bootedServices的集合。在看代码逻辑之前,需要看下skywalking定义的两个注解:

  • @DefaultImplementor 默认实现
  • @OverrideImplementor 覆盖实现

带有@DefaultImplementor注解的类,表示它会有类去继承它,继承它的类需要带有 @OverrideImplementor注解,并指定继承的类的名称,例如:

默认实现类:

    @DefaultImplementor
    public class JVMMetricsSender implements BootService, Runnable, GRPCChannelListener 
继承它的类:
    @OverrideImplementor(JVMMetricsSender.class)
    public class KafkaJVMMetricsSender extends JVMMetricsSender implements KafkaConnectionStatusListener 

在了解了skywalking的默认类和继承类的机制后,有代码逻辑如下:

    private Map loadAllServices() {
        Map bootedServices = new LinkedHashMap<>();
        List allServices = new LinkedList<>();
        // SPI加载
        load(allServices);
        // 遍历
        for (final BootService bootService : allServices) {
            Class bootServiceClass = bootService.getClass();
            // 是否带有默认实现
            boolean isDefaultImplementor = bootServiceClass.isAnnotationPresent(DefaultImplementor.class);
            if (isDefaultImplementor) {// 是默认实现
                // 是默认实现,没有添加到bootedServices
                if (!bootedServices.containsKey(bootServiceClass)) {
                    //加入bootedServices
                    bootedServices.put(bootServiceClass, bootService);
                } else {
                    //ignore the default service
                }
            } else {// 不是默认实现
                // 是否是覆盖实现
                OverrideImplementor overrideImplementor = bootServiceClass.getAnnotation(OverrideImplementor.class);
                // 不是覆盖
                if (overrideImplementor == null) {
                    // bootedServices没有
                    if (!bootedServices.containsKey(bootServiceClass)) {
                        //加入bootedServices
                        bootedServices.put(bootServiceClass, bootService);
                    } else {
                        throw new ServiceConflictException("Duplicate service define for :" + bootServiceClass);
                    }
                } else {
                    // 是覆盖,value获取的是其继承的类targetService
                    Class targetService = overrideImplementor.value();
                    // 如果bootedServices已经包含targetService
                    if (bootedServices.containsKey(targetService)) {
                        // 判断targetServices是否是默认实现
                        boolean presentDefault = bootedServices.get(targetService)
                                                               .getClass()
                                                               .isAnnotationPresent(DefaultImplementor.class);
                        // 是默认实现
                        if (presentDefault) {
                            // 添加进去
                            bootedServices.put(targetService, bootService);
                        } else {
                            // 没有默认实现,不能覆盖,抛出异常
                            throw new ServiceConflictException(
                                "Service " + bootServiceClass + " overrides conflict, " + "exist more than one service want to override :" + targetService);
                        }
                    } else {
                        // 是覆盖实现,它覆盖的默认实现还没有被加载进来
                        bootedServices.put(targetService, bootService);
                    }
                }
            }

        }
        return bootedServices;
    }

2.1.3.3 prepare(),startup(),onComplete()

在加载完全部的类之后,还有准备,启动和完成等分操作,它们的代码实现相同,如下所示:

private void prepare() {
    // 获取所有的类
    bootedServices.values().stream()
            // 根据优先级排序,BootService的priority
            .sorted(Comparator.comparingInt(BootService::priority))
            // 遍历
            .forEach(service -> {
        try {
            // 执行每一个BootService的实现类的prepare()方法
            service.prepare();
        } catch (Throwable e) {
            LOGGER.error(e, "ServiceManager try to pre-start [{}] fail.", service.getClass().getName());
        }
    });
}

private void startup() {
    bootedServices.values().stream()
            // 根据优先级排序,BootService的priority
            .sorted(Comparator.comparingInt(BootService::priority))
            // 遍历
            .forEach(service -> {
        try {
            // 执行每一个BootService的实现类的boot()方法
            service.boot();
        } catch (Throwable e) {
            LOGGER.error(e, "ServiceManager try to start [{}] fail.", service.getClass().getName());
        }
    });
}

private void onComplete() {
    // 遍历
    for (BootService service : bootedServices.values()) {
        try {
            // 执行每一个BootService的实现类的onComplete()方法
            service.onComplete();
        } catch (Throwable e) {
            LOGGER.error(e, "Service [{}] AfterBoot process fails.", service.getClass().getName());
        }
    }
}

2.1.4 ShutdownHook

为skywalking的运行服务添加一个shutdown的钩子。

Runtime.getRuntime()
        .addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));

shutdown方法,与准备和启动方法不同之处在于,shutdown的排序方式是按照优先级的倒序排序,为了优雅的停机,后启动的服务,先停机:

public void shutdown() {
    bootedServices.values().stream().
            // 排序,按照优先级的倒序
            sorted(Comparator.comparingInt(BootService::priority).reversed())
            .forEach(service -> {
        try {
            // 执行每个服务的shutdown
            service.shutdown();
        } catch (Throwable e) {
            LOGGER.error(e, "ServiceManager try to shutdown [{}] fail.", service.getClass().getName());
        }
    });
}

2.2 小结

本章节用了不小的篇幅讲解premain方法的源码启动过程,主要包括以下的方面:

  • 配置加载
  • 插件加载
  • AgentBUilder创建
  • skywalking服务的加载

本章主要在于讲解启动流程,代码量较大,涉及到字节码增强的关键暂时未讲解。

三、总结

到此为止,关于skywalking的相关启动流程就基本介绍完成了。之所以说是基本完成,是因为内部还以些关于插件启动流程的部分没有具体讲解,比如:

  • transform的工作流程
  • 三种插装方式的原理
    • 静态方法插桩
    • 构造器插桩
    • 实例方法插桩

限于篇幅原因,后续文章继续讲解。

通过本篇文章,相信您一定也有了不少的收获:

  • 静态agent和动态agent的概念和使用方式
  • skywalking使用def文件定义插件的方式,即插件的加载过程
  • skywaling通过ByteBuddy实现字节码增强
  • 类加载器的相关知识
  • BootService的定义和使用方式
  • JDK SPI方式实现类加载。
  • Skywalking所特有的默认实现,覆盖实现的注解定义方式。

除了以上具体的,还会看到诸如策略模式、建造者模式、观察者等设计模式。

阅读源码,虽然很多时候会让你感到晦涩难懂,但是当你坚持下来,收获绝对是意想不到的。养成阅读源码的好习惯,将会在编码工作中起到很大的帮助。

本文到此结束,感谢阅读!

一文探索【skywalking】如何通过agent实现启动流程_第12张图片

 

你可能感兴趣的:(java,大数据,java)