【线上问题】记一次公司日志基础组件SPI使用不当导致业务中断

Informal Essay By English

It is always a pleasure to learn

背景

叮叮叮、叮叮叮....,某年某月某日晚上,上海某出租屋内,刚被放在桌上的手机的铃声在安静的屋内显得很piercing。来电显示是一个广东电话号码,电话号码非常的熟悉,是系统的告警专用电话。我平静的打开电脑,打开钉钉,看了一下alert群内的异常信息。然后开始熟练的打开公司的日志平台,进行异常聚合搜索。嗯~,很好,有很多的异常,看来有的看了。然后15分钟后,不出意料的找到了异常的根因,这次告警有好几处异常,本文只分析、描述跟业务无关的异常。

问题描述

当时在日志平台上输出的异常如下: 由于完整的日志输出涉及到公司的代码, 这里只截图部分关键堆栈信息。抛出异常的类是属于基建日志组件包,贴一下异常抛出点的代码:

java 复制代码
public class Operators {
	static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);
	    public static Object current() {
	        //dosomething
	        for (OperatorGetter i : OperatorGetter) {
	            Object operator = i.currentOperator();
	            if (operator != null) {
	                return operator;
	            }
	        }
	        return null;
	    }
    }

问题分析

问题出现在前端调用一个后端业务接口没有成功。在用户层面的来看,表现为用户触发一次业务请求没有成功。

java.util.NoSuchElementException 是 Java 编程语言中的一个异常类,属于 java.util 包。这个异常通常在试图访问一个枚举(Enumeration)、迭代器(Iterator)或者其他类型的集合中的元素,但已经没有更多的元素时抛出。

当时看到这个异常一开始以为是META-INF/services/下面没有定义相关接口文件,但是后面通过分析拉到的jar,发现里面有相应的接口定义文件与实现。到这里已经先排除SPI没有找到对应的实现类而抛出异常的场景。到这一步SPI的错误的使用方式场景我们已经排除,接下来就只能从SPI的实现角度去分析这个问题。SPI这个知识点博主在之前的文章中已经有了详细的介绍,感兴趣的可以去看SPI详解 ,但是为了使文章能够顺畅的阅读下去,这里还是对SPI最核心的一些实现进行简单的描述。

SPI

Java的SPI(Service Provider Interface)是一种服务发现机制。它允许服务提供者在运行时被发现和加载,而不是在编译时硬编码。SPI是一种为某些接口寻找服务实现的方式,是Java提供的一种原生的插件功能。它主要用于可以插拔的组件之间的解耦。

在Java的SPI机制中,服务提供者会在类路径下的 META-INF/services 目录中创建一个名字为服务接口全限定名的文件。该文件内部列出了实现该服务接口的具体实现类的全限定名。在运行时,Java的SPI机制会查找这些配置文件,并加载并实例化这些实现类,从而实现了服务的动态查找与加载。

Java的SPI广泛应用于JDK中,例如java.sql.Driver 接口,JDBC驱动就是通过SPI机制被加载的。应用程序可以通过 ServiceLoader 类来加载服务:

java 复制代码
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
    // 使用service
}

这里,MyService 是服务接口,而具体的实现类可以在运行时通过放置在 META-INF/services 目录下的配置文件来指定。

SPI的基本介绍完成,我们再来看看SPI的核心api的实现。

java.util.ServiceLoader#load
java 复制代码
public static <S> ServiceLoader<S> load(Class<S> service) {
		//获取应用类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        //调用了另一个load方法进行ServiceLoader对象的创建
        return ServiceLoader.load(service, cl);
    }

public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

load方法完成ServiceLoader对象的创建,其中需要我们关注的是在ServiceLoader构造器的中会调用一个reload方法,此方法会进行迭代器类的创建,此类是SPI最核心的实现类。

java 复制代码
private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    
public void reload() {
        providers.clear();
        //在此处进行懒加载迭代器类对象的创建
        lookupIterator = new LazyIterator(service, loader);
    }
java.util.ServiceLoader.LazyIterator#hasNext
java 复制代码
public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                	//这里的PREFIX就是META-INF/services/
                    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;
        }

本文不对hasNextService()方法里面的各种处理去做详细的分析,但是有一个点需要我们知道的是,这个方法没有进行并发场景下的处理。

java.util.ServiceLoader.LazyIterator#next
java 复制代码
public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

private S nextService() {
		//这里的NoSuchElementException~~~~大家自己想象⛄️
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

这个方法就是代码案例获取实例对象最终会调用的方法,这里的if (!hasNextService())throw new NoSuchElementException();对于后面分析问题很重要~

至此,SPI的使用与实现我们都有大概的了解。这里再针对SPI的并发问题做一个解释,SPI本身的概念并不直接涉及线程安全问题。线程安全主要取决于SPI的具体实现。也就是说,一个服务提供者实现的线程安全性是由提供该服务的类或者库的作者来保证的。

到这里大家其实都已经知道这次的异常是什么原因导致。那我们就直接开始问题处理

问题处理

处理方式一: 通过加锁进行处理,加锁又有synchronized、juc lock两种方式,下面贴下两种处理方式代码:

java 复制代码
public class Operators {
    static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);
    static ReentrantLock lock = new ReentrantLock();
    static Object monitor = new Object();

    public static Object current() {
        CallContext context = CallContexts.get();
        if (context != null) {
            return context.getOperator();
        }
        lock.lock();
        try {
            for (OperatorGetter i : OperatorGetter) {
                Object operator = i.currentOperator();
                if (operator != null) {
                    return operator;
                }
            }
        } finally {
            lock.unlock();
        }
        synchronized (monitor){
            for (OperatorGetter i : OperatorGetter) {
                Object operator = i.currentOperator();
                if (operator != null) {
                    lock.unlock();
                    return operator;
                }
            }
        }
        return null;
    }
}

处理方式二: static方法块保证线程安全,代码如下:

java 复制代码
public class Operators {
    static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);
    static Object operator;

    static {
        for (OperatorGetter i : OperatorGetter) {
            Object object = i.currentOperator();
            if (operator != null) {
                operator = object;
            }
        }
    }

    public static Object current() {
        CallContext context = CallContexts.get();
        if (context != null) {
            return context.getOperator();
        }
        for (OperatorGetter i : OperatorGetter) {
            Object operator = i.currentOperator();
            if (operator != null) {
                return operator;
            }
        }
        return null;
    }
}

最后提出一个问题,如果是你碰到这个问题,你会怎么去处理呢?

相关推荐
杨荧7 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
喜欢打篮球的普通人22 分钟前
rust高级特征
开发语言·后端·rust
Ling_suu38 分钟前
Spring——单元测试
java·spring·单元测试
ModelBulider40 分钟前
十三、注解配置SpringMVC
java·开发语言·数据库·sql·mysql
苹果酱05671 小时前
C语言 char 字符串 - C语言零基础入门教程
java·开发语言·spring boot·mysql·中间件
csucoderlee1 小时前
eclipse mat leak suspects report和 component report的区别
java·ide·eclipse
代码小鑫1 小时前
A032-基于Spring Boot的健康医院门诊在线挂号系统
java·开发语言·spring boot·后端·spring·毕业设计
训山1 小时前
4000字浅谈Java网络编程
java·开发语言·网络
VertexGeek1 小时前
Rust学习(四):作用域、所有权和生命周期:
java·学习·rust
豌豆花下猫1 小时前
REST API 已经 25 岁了:它是如何形成的,将来可能会怎样?
后端·python·ai