前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >谈谈一个框架的扩展加载

谈谈一个框架的扩展加载

作者头像
leobhao
发布2023-03-11 17:05:38
4270
发布2023-03-11 17:05:38
举报
文章被收录于专栏:涓流

springboot 中的扩展加载(SpringFactoriesLoader)

在 SpringBoot 启动类上都会标注@SpringBootApplication这个注解,其中的最重要的组成是@EnableAutoConfiguration, 再进入会发现是 @Import (AutoConfigurationImportSelector.class)这个注解在起作用。

自动加载配置的逻辑是:selectImports->selectImports->getAutoConfigurationEntry,不过这里不是分析 SpringBoot 启动流程,我们关心的是他如何加载配置的。可以在getAutoConfigurationEntry看到使用的是SpringFactoriesLoader,主要代码:

代码语言:javascript
复制
public static List < String > loadFactoryNames(Class < ? > factoryClass, @Nullable ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map < String, List < String >> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap < String, String > result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        // 扫描所有 jar 包类路径下  META-INF/spring.factories
        Enumeration < URL > urls = (classLoader != null ?
            classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
            ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap < > ();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            // 把扫描到的这些文件的内容包装成 properties 对象
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry < ? , ? > entry : properties.entrySet()) {
                String factoryClassName = ((String) entry.getKey()).trim();
                for (String factoryName: StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    // 从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        cache.put(classLoader, result);
        return result;
    } catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
            FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

这样就把我们配置在 META-INF/spring.factories 中的配置类给找出来了,完成了自动配置的加载(spring 的 @import 机制会完成这个 bean 的注入过程)

Java SPI

SPI 全称 Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件,可以根据使用者的配置,来加载接口的具体实现类。

使用

先看看怎么使用。首先创建一个Repository接口,和两个实现类:

代码语言:javascript
复制
public interface Repository {
    void connect();
}
public class MongoRepository implements Repository {
    @Override
    public void connect() {
        System.out.println("mongo repository");
    }
}
public class MysqlRepository implements Repository {
    @Override
    public void connect() {
        System.out.println("mysql connected");
    }
}

然后在META-INF/services下新建一个与接口同名的文件:com.java.Repository,里面的内容是具体实现类的类的全名:

代码语言:javascript
复制
com.java.impl.MongoRepository
com.java.MysqlRepository

运行方法:

代码语言:javascript
复制
@Test
public void jdkSpi() {
    ServiceLoader<Repository> serviceLoader = ServiceLoader.load(Repository.class);
    Iterator<Repository> iterable = serviceLoader.iterator();
    while (iterable.hasNext()) {
        System.out.println(iterable.next().getClass().getName());
    }
}

可以在控制台上看到,输出了配置文件中的类,也就是代表了配置文件中的类已经被加载了。

原理

jdk 的这种机制,把约定(接口)和实现分离,我们当引入具体实现的时候,不会给服务使用方带来任何代码上的修改,并且只有服务方主动使用的时候才会真正的去初始化,完成动态的加载。

可以看到 jdk 是通过 ServiceLoader 来实现, 它的 load() 流程如下:

代码语言:javascript
复制
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取当前线程上下文加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

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

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 如果传入 ClassLoader 为空则使用系统类加载器    
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // 访问权限判断
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 往下面看
    reload();
}

上面的内容是获取当前类的加载器,传给成员变量loader,并进入reload流程, reload是初始化了一个LazyIterator对象, 看看它的next():

代码语言:javascript
复制
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() {
    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
}

// 判断是否有,如果有则赋值给 nextName
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 路径是 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;
}

可以看到,整个流程还是很清晰的,就是去找META-INF/service下对应的class全名的文件,然后返回出去。

Dubbo 中 扩展点的加载

Dubbo 文档中描述 Dubbo SPI 改进了 JDK SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了(不会上报真正失败的原因)。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

dubbo的扩展点加载具体以下特点:

  • 自动包装: ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。Wrapper类可以为扩展点之上做一些处理,类似于 AOP
  • 自动装配: 扩展点实现类的成员如果是其他扩展点,ExtensionLoader会自动注入依赖的扩展点。
  • 扩展点自适应: ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是哪一个扩展点实现。
  • 扩展点自动激活,如果扩展点有多个实现类,可以使用@Activate来自动激活,简化配置
使用

借用官网的例子,新建接口Robot和两个实现类:

代码语言:javascript
复制
// 这里需要标注是一个 SPI 扩展
@SPI
public interface Robot {
    void sayHello();
}

public class Bumblebee implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

public class Bumblebee implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

然后在 META-INF/dubbo/ 下新建与接口同名的文件com.code.service.Robot,并配置扩展实现类:

代码语言:javascript
复制
bumblebee = com.code.service.impl.Bumblebee
optimusPrime = com.code.service.impl.OptimusPrime

执行:

代码语言:javascript
复制
public static void main(String[] args) {
    ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
    Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
    optimusPrime.sayHello();
}

控制台输出:

代码语言:javascript
复制
Hello, I am Optimus Prime.
加载扩展对象

上面的使用例子可以看出,首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。

getExtensionLoader

getExtensionLoader()用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例:

代码语言:javascript
复制
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null)
        throw new IllegalArgumentException("Extension type == null");
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
    }
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type(" + type +
                ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
    }
    // 从缓存中获取对应扩展点的 ExtensionLoader 实例
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    // 若缓存没有命中,则创建一个实例
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}
getExtension

getExtension(String name)获取扩展类对象:

代码语言:javascript
复制
public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 获取默认的拓展实现类
        return getDefaultExtension();
    }
    // Holder 用于持有目标对象
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // 双重检查
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 创建拓展实例
                instance = createExtension(name);
                // 设置实例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上述逻辑较为简单,尝试从缓存中获取对象,若缓存没有则创建扩展对象,创建对象createExtension(String name)流程如下:

代码语言:javascript
复制
private T createExtension(String name) {
    // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 通过反射创建实例
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向实例中注入依赖
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循环创建 Wrapper 实例
            for (Class<?> wrapperClass : wrapperClasses) {
                // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
                // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
                 // 所以最终返回的是一个 wrapper 类的实例
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

这里主要分为四个步骤

  1. 通过 getExtensionClasses 获取所有的拓展类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖(dubbo 的 IOC 机制)
  4. 将拓展对象包裹在相应的 Wrapper 对象中
获取所有扩展类

getExtensionClasses() 会根据配置文件解析出 扩展项->扩展类 的映射关系表(Map<名称, 拓展类>),流程如下:

代码语言:javascript
复制
private Map<String, Class<?>> getExtensionClasses() {
    // 从缓存中获取已加载的拓展类
    Map<String, Class<?>> classes = cachedClasses.get();
    // 双重检查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加载拓展类
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

// loadExtensionClasses 加载扩展类
private Map<String, Class<?>> loadExtensionClasses() {
    // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的(接口类型)
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 对 SPI 注解内容进行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 检测 SPI 注解内容是否合法,不合法则抛出异常
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }
            // 设置默认名称
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // 加载指定文件夹下的配置文件
    // META-INF/dubbo/internal/
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    // META-INF/dubbo/
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    // META-INF/services/
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

// loadDirectory 加载指定文件夹配置
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 文件夹路径 + type 全限定名 
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        // 根据文件名加载所有的同名文件
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // 加载资源, 这个方法做的事情比较多
                // 1. 解析配置文件(=号分割)
                // 2. 通过反射加载扩展实现类
                // 3. 调用 loadClass 缓存各种类
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("...");
    }
}

获取缓存的逻辑不复杂,就是有点长, 到这里基本就完成了上述的1、2步,获取了扩展实现类并通过反射创建了扩展对象

Dubbo IOC

Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。整个过程对应的代码如下:

代码语言:javascript
复制
private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            // 遍历目标类的所有方法
            for (Method method : instance.getClass().getMethods()) {
                // 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public
                if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                    // 获取 setter 方法参数类型
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        // 获取属性名,比如 setName 方法对应属性名 name
                        String property = method.getName().length() > 3 ? 
                            method.getName().substring(3, 4).toLowerCase() + 
                            	method.getName().substring(4) : "";
                        // 从 ObjectFactory 中获取依赖对象
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            // 通过反射调用 setter 方法设置依赖
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("fail to inject via method...");
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}
自适应扩展

Dubbo 的扩展都是通过 SPI 机制进行加载,有时候希望扩展被调用前,根据运行参数进行加载。这里感觉起来就很矛盾,既然没有被加载,怎么去调用呢? Dubbo 的自适应扩展机制解决了这个问题。

这里的代码有些复杂,主要的流程是通过@Adaptive注解标注在类或方法上,如果是标注在类上就会生成代理类(这种模式比较简单),如果标注在方法上,就会由框架自动生成加载扩展的逻辑。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019年12月25日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • springboot 中的扩展加载(SpringFactoriesLoader)
  • Java SPI
    • 使用
      • 原理
      • Dubbo 中 扩展点的加载
        • 使用
          • 加载扩展对象
            • getExtensionLoader
            • getExtension
          • 自适应扩展
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档