谈谈一个框架的扩展加载

2022-06-28 18:35:53 浏览数 (1)

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注解标注在类或方法上,如果是标注在类上就会生成代理类(这种模式比较简单),如果标注在方法上,就会由框架自动生成加载扩展的逻辑。

0 人点赞