源码角度了解Skywalking之SPI在SKywalking中应用

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码角度了解Skywalking之SPI在SKywalking中应用相关的知识,希望对你有一定的参考价值。

源码角度了解Skywalking之SPI在SKywalking中应用

上篇文章中我们说到SKywalking的启动流程是怎样的,其中有一步是利用JDK的SPI机制来启动插件服务,今天我们就看一下具体是怎么利用JDK的SPI机制的。

JDK 的SPI

所谓SPI就是Service Provider Interface,我们通过日志的接口实现类来演示一下SPI的使用

第一步定义接口

public interface Log 
    void log(String info);

定义实现接口的两个实现类:

public class Log4j implements Log 
    @Override
    public void log(String info) 
        System.out.println("Log4j:" + info);
    

public class Logback implements Log 
    @Override
    public void log(String info) 
        System.out.println("Logback:" + info);
    

第二步:在resources的META-INF/services下定义文件,文件名就是接口的全限定名com.xiepanpan.Log,文件的内容就是对应的实现类的全限定名

com.xiepanpan.impl.Log4j
com.xiepanpan.impl.Logback

第三步:使用接口实现类

我们看一下main()方法:

public class Main 
    public static void main(String[] args) 
        ServiceLoader<Log> serviceLoader =ServiceLoader.load(Log.class);
        Iterator<Log> iterator = serviceLoader.iterator();
        while (iterator.hasNext()) 
            Log log = iterator.next();
            log.log("JDK SPI");
        
    

这段代码很好理解,通过ServiceLoader的load()方法加载Log接口的实现类,遍历实现类,依次调用实现类的log()方法,输出结果:

Log4j:JDK SPI
Logback:JDK SPI

有些同学就会产生疑问❓,为什么使用ServiceLoader.load(Log.class)就能加载到对应的实现类呢?

我们看一下这个方法具体做了什么,

ServiceLoader.load()

进入ServiceLoader.load()方法,最终调用的是ServiceLoader的reload()方法,方法中 创建了LazyIterator 迭代器对象

serviceLoader.iterator()

serviceLoader.iterator()中创建了Iterator对象,这个类的hasNext()方法和next()方法中都是先从缓存中查询如果有返回,如果没有就调用LazyIterator的hasNext()方法和next()方法

LazyIterator类

我们看一下LazyIterator类的hasNext()方法和next()方法的实现

LazyIterator的hasNext()方法

hasNext()方法会调用hasNextService()方法:

private boolean hasNextService() 
            if (nextName != null) 
                return true;
            
            if (configs == null) 
                try 
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                 catch (IOException x) 
                    fail(service, "Error locating configuration files", x);
                
            
            while ((pending == null) || !pending.hasNext()) 
                if (!configs.hasMoreElements()) 
                    return false;
                
                pending = parse(service, configs.nextElement());
            
            nextName = pending.next();
            return true;
        
  1. 根据前缀和服务接口名加载配置文件,其中PREFIX的值为META-INF/services/,service.getName()的值为接口的全限定名,即com.xiepanpan.Log,这样就找到resources文件夹下我们定义的文件了
  2. 解析文件更新nextName,并返回true

LazyIterator的next()方法

next()方法会调用nextService()方法:

这个方法主要是通过Class.forName()方法来实例化nextName指向的类

现在我们知道为什么可以通过文件来配置我们需要使用的类了。

SPI在Skywalking中的应用

ServiceManager.INSTANCE.boot()方法

Skywalking启动初始化的时候调用ServiceManager.INSTANCE.boot()方法,我们看一下这个方法:

public void boot() 
        bootedServices = loadAllServices();

        prepare();
        startup();
        onComplete();
    
   
  1. BootService是所有远程的接口,需要在插件机制开始工作时启动,而ServiceManager是用来管理BootService的,boot()方法中调用loadAllServices()方法加载所有的BootService,
  2. 依次遍历BootService集合调用对象的prepare()准备方法
  3. 依次遍历BootService集合调用对象的boot()启动方法
  4. 依次遍历BootService集合调用对象的onComplete()完成方法

ServiceManager的loadAllServices()方法

ServiceManager的loadAllServices()方法:

private Map<Class, BootService> loadAllServices() 
        Map<Class, BootService> bootedServices = new LinkedHashMap<Class, BootService>();
        List<BootService> allServices = new LinkedList<BootService>();
        load(allServices);
        Iterator<BootService> serviceIterator = allServices.iterator();
        while (serviceIterator.hasNext()) 
            。。。。。
        
        return bootedServices;
    
  1. 调用load()方法加载所有的BootService
  2. 遍历得到的所有BootService对象,针对 BootService 上的 @DefaultImplementor 和 @OverrideImplementor 注解进行处理,@DefaultImplementor表示是默认实现,@OverrideImplementor 注解可以覆盖默认实现,value值指定覆盖的默认实现。具体是没有这两个注解的加入到BootService集合中,有@DefaultImplementor标记的加入集合,有@OverrideImplementor标记的并且value指向的服务有@DefaultImplementor标记的对其覆盖,value指向的服务没有@DefaultImplementor注解就报错。
  3. 返回BootService集合

ServiceManager的load()方法

ServiceManager的load()方法:

void load(List<BootService> allServices) 
        Iterator<BootService> iterator = ServiceLoader.load(BootService.class, AgentClassLoader.getDefault()).iterator();
        while (iterator.hasNext()) 
            allServices.add(iterator.next());
        
    

看看是不是似曾相识,和我们写的日志加载的demo非常相像,也是利用ServiceLoader的load()方法来加载,这里自然是加载resources/META-INF/services/org.apache.skywalking.apm.agent.core.boot.BootService文件了,在apm-agent-core模块中有定义这个文件,文件中都是BootService接口的实现类

总结

这篇文章我们讲了什么JDK的SPI以及它的原理,同时介绍了Skywalking在加载BootService接口的实现类的时候是怎么利用SPI机制的。JDK的SPI机制应用很广泛,在jdbc驱动加载和dubbo等服务中也有使用,可见JDK的SPI机制非常好用和受欢迎,值得我们了解它的原理和使用。

❤️ 感谢大家

如果你觉得这篇内容对你挺有有帮助的话:

  1. 欢迎关注我❤️,点赞

    以上是关于源码角度了解Skywalking之SPI在SKywalking中应用的主要内容,如果未能解决你的问题,请参考以下文章

    源码角度了解Skywalking之告警机制是怎么实现的

    源码角度了解Skywalking之@Trace注解的原理

    源码角度了解Skywalking之Trace可以跨线程吗

    源码角度了解Skywalking之服务端OAP的启动流程

    源码角度了解Skywalking之服务端OAP对Trace的处理

    源码角度了解Skywalking之服务端OAP对服务注册与服务实例注册的处理