• 一文讲透Java核心技术之高可扩展利器SPI


    SPI的概念

    JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制

    SPI的使用场景

    Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。

    大量的框架使用了Java的SPI技术,如下:

    (1)JDBC加载不同类型的数据库驱动

    (2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类

    (3)Spring中大量使用了SPI

    • 对servlet3.0规范
    • 对ServletContainerInitializer的实现
    • 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

    (4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现

    SPI的使用

    当服务的提供者,提供了接口的一种实现后,需要在Jar包的**META-INF/services/**目录下,创建一个以接口的名称(包名.接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名+类名)。

    当外部程序通过 java.util.ServiceLoader 类装载这个接口时,就能够通过该Jar包的**META/Services/**目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI的规范规定了接口的实现类必须有一个无参构造方法。

    SPI中查找接口的实现类是通过 java.util.ServiceLoader ,而在 java.util.ServiceLoader 类中有一行代码如下:

    // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
    private static final String PREFIX = "META-INF/services/";

    这也就是说,我们必须将接口的配置文件写到Jar包的**META/Services/**目录下。

    SPI实例

    这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。

    注意:实例是基于Java8进行开发的。

    1.创建Maven项目

    在IDEA中创建Maven项目spi-demo,如下:

    编辑

    2.编辑pom.xml

    1. "1.0" encoding="UTF-8"?>
    2. "http://maven.apache.org/POM/4.0.0"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    5. spi-demo
    6. io.binghe.spi
    7. jar
    8. 1.0.0-SNAPSHOT
    9. 4.0.0
    10. org.springframework.boot
    11. spring-boot-maven-plugin
    12. org.apache.maven.plugins
    13. maven-compiler-plugin
    14. 3.6.0
    15. 1.8
    16. 1.8

    3.创建类加载工具类

    在io.binghe.spi.loader包下创建MyServiceLoader,MyServiceLoader类中直接调用JDK的ServiceLoader类加载Class。代码如下所示。

    1. package io.binghe.spi.loader;
    2. import java.util.ServiceLoader;
    3. /**
    4. * @author binghe
    5. * @version 1.0.0
    6. * @description 类加载工具
    7. */
    8. public class MyServiceLoader {
    9. /**
    10. * 使用SPI机制加载所有的Class
    11. */
    12. public static ServiceLoader loadAll(final Class clazz) {
    13. return ServiceLoader.load(clazz);
    14. }
    15. }

    4.创建接口

    在io.binghe.spi.service包下创建接口MyService,作为测试接口,接口中只有一个方法,打印传入的字符串信息。代码如下所示:

    1. package io.binghe.spi.service;
    2. /**
    3. * @author binghe
    4. * @version 1.0.0
    5. * @description 定义接口
    6. */
    7. public interface MyService {
    8. /**
    9. * 打印信息
    10. */
    11. void print(String info);
    12. }

    5.创建接口的实现类

    (1)创建第一个实现类MyServiceA

    在io.binghe.spi.service.impl包下创建MyServiceA类,实现MyService接口。代码如下所示:

    1. package io.binghe.spi.service.impl;
    2. import io.binghe.spi.service.MyService;
    3. /**
    4. * @author binghe
    5. * @version 1.0.0
    6. * @description 接口的第一个实现
    7. */
    8. public class MyServiceA implements MyService {
    9. @Override
    10. public void print(String info) {
    11. System.out.println(MyServiceA.class.getName() + " print " + info);
    12. }
    13. }

    (2)创建第二个实现类MyServiceB

    在io.binghe.spi.service.impl包下创建MyServiceB类,实现MyService接口。代码如下所示:

    1. package io.binghe.spi.service.impl;
    2. import io.binghe.spi.service.MyService;
    3. /**
    4. * @author binghe
    5. * @version 1.0.0
    6. * @description 接口第二个实现
    7. */
    8. public class MyServiceB implements MyService {
    9. @Override
    10. public void print(String info) {
    11. System.out.println(MyServiceB.class.getName() + " print " + info);
    12. }
    13. }

    6.创建接口文件

    在项目的src/main/resources目录下创建**META/Services/**目录,在目录中创建io.binghe.spi.service.MyService文件,注意:文件必须是接口MyService的全名,之后将实现MyService接口的类配置到文件中,如下所示:

    1. io.binghe.spi.service.impl.MyServiceA
    2. io.binghe.spi.service.impl.MyServiceB

    7.创建测试类

    在项目的io.binghe.spi.main包下创建Main类,该类为测试程序的入口类,提供一个main()方法,在main()方法中调用ServiceLoader类加载MyService接口的实现类。并通过Java8的Stream将结果打印出来,如下所示:

    1. package io.binghe.spi.main;
    2. import io.binghe.spi.loader.MyServiceLoader;
    3. import io.binghe.spi.service.MyService;
    4. import java.util.ServiceLoader;
    5. import java.util.stream.StreamSupport;
    6. /**
    7. * @author binghe
    8. * @version 1.0.0
    9. * @description 测试的main方法
    10. */
    11. public class Main {
    12. public static void main(String[] args){
    13. ServiceLoader loader = MyServiceLoader.loadAll(MyService.class);
    14. StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
    15. }
    16. }

    8.测试实例

    运行Main类中的main()方法,打印出的信息如下所示:

    1. io.binghe.spi.service.impl.MyServiceA print Hello World
    2. io.binghe.spi.service.impl.MyServiceB print Hello World
    3. Process finished with exit code 0

    通过打印信息可以看出,通过Java SPI机制正确加载出接口的实现类,并调用接口的实现方法。

    源码解析

    这里,主要是对SPI的加载流程涉及到的 java.util.ServiceLoader 的源码的解析。

    进入 java.util.ServiceLoader 的源码,可以看到ServiceLoader类实现了 java.lang.Iterable 接口,如下所示。

    public final class ServiceLoader  implements Iterable

    说明ServiceLoader类是可以遍历迭代的。

    java.util.ServiceLoader类中定义了如下的成员变量:

    1. // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
    2. private static final String PREFIX = "META-INF/services/";
    3. // 需要加载的接口
    4. private final Class service;
    5. // 类加载器,用于加载以接口命名的文件中配置的接口的实现类
    6. private final ClassLoader loader;
    7. // 创建ServiceLoader时采用的访问控制上下文环境
    8. private final AccessControlContext acc;
    9. // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
    10. private LinkedHashMap providers = new LinkedHashMap<>();
    11. // 用于延迟加载实现类的迭代器
    12. private LazyIterator lookupIterator;

    可以看到ServiceLoader类中定义了加载前缀为“META-INF/services/”,所以,接口文件必须要在项目的 src/main/resources 目录下的**META-INF/services/**目录下创建。

    从MyServiceLoader类调用**ServiceLoader.load(clazz)**方法进入源码,如下所示:

    1. //根据类的Class对象加载指定的类,返回ServiceLoader对象
    2. public static ServiceLoader load(Class service) {
    3. //获取当前线程的类加载器
    4. ClassLoader cl = Thread.currentThread().getContextClassLoader();
    5. //动态加载指定的类,将类加载到ServiceLoader中
    6. return ServiceLoader.load(service, cl);
    7. }

    方法中调用了**ServiceLoader.load(service, cl)**方法,继续跟踪代码,如下所示:

    1. //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
    2. public static ServiceLoader load(Class service, ClassLoader loader){
    3. return new ServiceLoader<>(service, loader);
    4. }

    可以看到**ServiceLoader.load(service, cl)**方法中,调用了ServiceLoader类的构造方法,继续跟进代码,如下所示:

    1. //构造ServiceLoader对象
    2. private ServiceLoader(Class svc, ClassLoader cl) {
    3. //如果传入的Class对象为空,则判处空指针异常
    4. service = Objects.requireNonNull(svc, "Service interface cannot be null");
    5. //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
    6. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    7. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    8. reload();
    9. }

    继续跟**reload()**方法,如下所示。

    1. //重新加载
    2. public void reload() {
    3. //清空保存加载的实现类的LinkedHashMap
    4. providers.clear();
    5. //构造延迟加载的迭代器
    6. lookupIterator = new LazyIterator(service, loader);
    7. }

    继续跟进懒加载迭代器的构造函数,如下所示。

    1. private LazyIterator(Class service, ClassLoader loader) {
    2. this.service = service;
    3. this.loader = loader;
    4. }

    可以看到,会将需要加载的接口的Class对象和类加载器赋值给 LazyIterator 的成员变量。

    当我们在程序中迭代获取对象实例时,首先在成员变量** providers 中查找是否有缓存的实例对象。如果存在则直接返回,否则调用 lookupIterator **延迟加载迭代器进行加载。

    迭代器进行逻辑判断的代码如下所示:

    1. //迭代ServiceLoader的方法
    2. public Iterator iterator() {
    3. return new Iterator() {
    4. //获取保存实现类的LinkedHashMap的迭代器
    5. Iterator> knownProviders = providers.entrySet().iterator();
    6. //判断是否有下一个元素
    7. public boolean hasNext() {
    8. //如果knownProviders存在元素,则直接返回true
    9. if (knownProviders.hasNext())
    10. return true;
    11. //返回延迟加载器是否存在元素
    12. return lookupIterator.hasNext();
    13. }
    14. //获取下一个元素
    15. public S next() {
    16. //如果knownProviders存在元素,则直接获取
    17. if (knownProviders.hasNext())
    18. return knownProviders.next().getValue();
    19. //获取延迟迭代器lookupIterator中的元素
    20. return lookupIterator.next();
    21. }
    22. public void remove() {
    23. throw new UnsupportedOperationException();
    24. }
    25. };
    26. }
    27. LazyIterator加载类的流程如下代码所示
    28. //判断是否拥有下一个实例
    29. private boolean hasNextService() {
    30. //如果拥有下一个实例,直接返回true
    31. if (nextName != null) {
    32. return true;
    33. }
    34. //如果实现类的全名为null
    35. if (configs == null) {
    36. try {
    37. //获取全文件名,文件相对路径+文件名称(包名+接口名)
    38. String fullName = PREFIX + service.getName();
    39. //类加载器为空,则通过ClassLoader.getSystemResources()方法获取
    40. if (loader == null)
    41. configs = ClassLoader.getSystemResources(fullName);
    42. else
    43. //类加载器不为空,则直接通过类加载器获取
    44. configs = loader.getResources(fullName);
    45. } catch (IOException x) {
    46. fail(service, "Error locating configuration files", x);
    47. }
    48. }
    49. while ((pending == null) || !pending.hasNext()) {
    50. //如果configs中没有更过的元素,则直接返回false
    51. if (!configs.hasMoreElements()) {
    52. return false;
    53. }
    54. //解析包结构
    55. pending = parse(service, configs.nextElement());
    56. }
    57. nextName = pending.next();
    58. return true;
    59. }
    60. private S nextService() {
    61. if (!hasNextService())
    62. throw new NoSuchElementException();
    63. String cn = nextName;
    64. nextName = null;
    65. Class c = null;
    66. try {
    67. //加载类对象
    68. c = Class.forName(cn, false, loader);
    69. } catch (ClassNotFoundException x) {
    70. fail(service,
    71. "Provider " + cn + " not found");
    72. }
    73. if (!service.isAssignableFrom(c)) {
    74. fail(service,
    75. "Provider " + cn + " not a subtype");
    76. }
    77. try {
    78. //通过c.newInstance()生成对象实例
    79. S p = service.cast(c.newInstance());
    80. //将生成的对象实例保存到缓存中(LinkedHashMap
    81. providers.put(cn, p);
    82. return p;
    83. } catch (Throwable x) {
    84. fail(service,
    85. "Provider " + cn + " could not be instantiated",
    86. x);
    87. }
    88. throw new Error(); // This cannot happen
    89. }
    90. public boolean hasNext() {
    91. if (acc == null) {
    92. return hasNextService();
    93. } else {
    94. PrivilegedAction action = new PrivilegedAction() {
    95. public Boolean run() {
    96. return hasNextService(); }
    97. };
    98. return AccessController.doPrivileged(action, acc);
    99. }
    100. }
    101. public S next() {
    102. if (acc == null) {
    103. return nextService();
    104. } else {
    105. PrivilegedAction action = new PrivilegedAction() {
    106. public S run() {
    107. return nextService(); }
    108. };
    109. return AccessController.doPrivileged(action, acc);
    110. }
    111. }

    最后,给出整个 java.util.ServiceLoader 的类,如下所示:

    1. package java.util;
    2. import java.io.BufferedReader;
    3. import java.io.IOException;
    4. import java.io.InputStream;
    5. import java.io.InputStreamReader;
    6. import java.net.URL;
    7. import java.security.AccessControlContext;
    8. import java.security.AccessController;
    9. import java.security.PrivilegedAction;
    10. public final class ServiceLoader implements Iterable {
    11. // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
    12. private static final String PREFIX = "META-INF/services/";
    13. // 需要加载的接口
    14. private final Class service;
    15. // 类加载器,用于加载以接口命名的文件中配置的接口的实现类
    16. private final ClassLoader loader;
    17. // 创建ServiceLoader时采用的访问控制上下文环境
    18. private final AccessControlContext acc;
    19. // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
    20. private LinkedHashMap providers = new LinkedHashMap<>();
    21. // 用于延迟加载实现类的迭代器
    22. private LazyIterator lookupIterator;
    23. //重新加载
    24. public void reload() {
    25. //清空保存加载的实现类的LinkedHashMap
    26. providers.clear();
    27. //构造延迟加载的迭代器
    28. lookupIterator = new LazyIterator(service, loader);
    29. }
    30. //构造ServiceLoader对象
    31. private ServiceLoader(Class svc, ClassLoader cl) {
    32. //如果传入的Class对象为空,则判处空指针异常
    33. service = Objects.requireNonNull(svc, "Service interface cannot be null");
    34. //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
    35. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    36. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    37. reload();
    38. }
    39. private static void fail(Class service, String msg, Throwable cause)
    40. throws ServiceConfigurationError
    41. {
    42. throw new ServiceConfigurationError(service.getName() + ": " + msg,
    43. cause);
    44. }
    45. private static void fail(Class service, String msg)
    46. throws ServiceConfigurationError
    47. {
    48. throw new ServiceConfigurationError(service.getName() + ": " + msg);
    49. }
    50. private static void fail(Class service, URL u, int line, String msg)
    51. throws ServiceConfigurationError
    52. {
    53. fail(service, u + ":" + line + ": " + msg);
    54. }
    55. // Parse a single line from the given configuration file, adding the name
    56. // on the line to the names list.
    57. //
    58. private int parseLine(Class service, URL u, BufferedReader r, int lc,
    59. List names)
    60. throws IOException, ServiceConfigurationError
    61. {
    62. String ln = r.readLine();
    63. if (ln == null) {
    64. return -1;
    65. }
    66. int ci = ln.indexOf('#');
    67. if (ci >= 0) ln = ln.substring(0, ci);
    68. ln = ln.trim();
    69. int n = ln.length();
    70. if (n != 0) {
    71. if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
    72. fail(service, u, lc, "Illegal configuration-file syntax");
    73. int cp = ln.codePointAt(0);
    74. if (!Character.isJavaIdentifierStart(cp))
    75. fail(service, u, lc, "Illegal provider-class name: " + ln);
    76. for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
    77. cp = ln.codePointAt(i);
    78. if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
    79. fail(service, u, lc, "Illegal provider-class name: " + ln);
    80. }
    81. if (!providers.containsKey(ln) && !names.contains(ln))
    82. names.add(ln);
    83. }
    84. return lc + 1;
    85. }
    86. private Iterator parse(Class service, URL u)
    87. throws ServiceConfigurationError
    88. {
    89. InputStream in = null;
    90. BufferedReader r = null;
    91. ArrayList names = new ArrayList<>();
    92. try {
    93. in = u.openStream();
    94. r = new BufferedReader(new InputStreamReader(in, "utf-8"));
    95. int lc = 1;
    96. while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    97. } catch (IOException x) {
    98. fail(service, "Error reading configuration file", x);
    99. } finally {
    100. try {
    101. if (r != null) r.close();
    102. if (in != null) in.close();
    103. } catch (IOException y) {
    104. fail(service, "Error closing configuration file", y);
    105. }
    106. }
    107. return names.iterator();
    108. }
    109. // Private inner class implementing fully-lazy provider lookupload
    110. private class LazyIterator
    111. implements Iterator
    112. {
    113. Class service;
    114. ClassLoader loader;
    115. Enumeration configs = null;
    116. Iterator pending = null;
    117. String nextName = null;
    118. private LazyIterator(Class service, ClassLoader loader) {
    119. this.service = service;
    120. this.loader = loader;
    121. }
    122. //判断是否拥有下一个实例
    123. private boolean hasNextService() {
    124. //如果拥有下一个实例,直接返回true
    125. if (nextName != null) {
    126. return true;
    127. }
    128. //如果实现类的全名为null
    129. if (configs == null) {
    130. try {
    131. //获取全文件名,文件相对路径+文件名称(包名+接口名)
    132. String fullName = PREFIX + service.getName();
    133. //类加载器为空,则通过ClassLoader.getSystemResources()方法获取
    134. if (loader == null)
    135. configs = ClassLoader.getSystemResources(fullName);
    136. else
    137. //类加载器不为空,则直接通过类加载器获取
    138. configs = loader.getResources(fullName);
    139. } catch (IOException x) {
    140. fail(service, "Error locating configuration files", x);
    141. }
    142. }
    143. while ((pending == null) || !pending.hasNext()) {
    144. //如果configs中没有更过的元素,则直接返回false
    145. if (!configs.hasMoreElements()) {
    146. return false;
    147. }
    148. //解析包结构
    149. pending = parse(service, configs.nextElement());
    150. }
    151. nextName = pending.next();
    152. return true;
    153. }
    154. private S nextService() {
    155. if (!hasNextService())
    156. throw new NoSuchElementException();
    157. String cn = nextName;
    158. nextName = null;
    159. Class c = null;
    160. try {
    161. //加载类对象
    162. c = Class.forName(cn, false, loader);
    163. } catch (ClassNotFoundException x) {
    164. fail(service,
    165. "Provider " + cn + " not found");
    166. }
    167. if (!service.isAssignableFrom(c)) {
    168. fail(service,
    169. "Provider " + cn + " not a subtype");
    170. }
    171. try {
    172. //通过c.newInstance()生成对象实例
    173. S p = service.cast(c.newInstance());
    174. //将生成的对象实例保存到缓存中(LinkedHashMap
    175. providers.put(cn, p);
    176. return p;
    177. } catch (Throwable x) {
    178. fail(service,
    179. "Provider " + cn + " could not be instantiated",
    180. x);
    181. }
    182. throw new Error(); // This cannot happen
    183. }
    184. public boolean hasNext() {
    185. if (acc == null) {
    186. return hasNextService();
    187. } else {
    188. PrivilegedAction action = new PrivilegedAction() {
    189. public Boolean run() {
    190. return hasNextService(); }
    191. };
    192. return AccessController.doPrivileged(action, acc);
    193. }
    194. }
    195. public S next() {
    196. if (acc == null) {
    197. return nextService();
    198. } else {
    199. PrivilegedAction action = new PrivilegedAction() {
    200. public S run() {
    201. return nextService(); }
    202. };
    203. return AccessController.doPrivileged(action, acc);
    204. }
    205. }
    206. public void remove() {
    207. throw new UnsupportedOperationException();
    208. }
    209. }
    210. //迭代ServiceLoader的方法
    211. public Iterator iterator() {
    212. return new Iterator() {
    213. //获取保存实现类的LinkedHashMap的迭代器
    214. Iterator> knownProviders = providers.entrySet().iterator();
    215. //判断是否有下一个元素
    216. public boolean hasNext() {
    217. //如果knownProviders存在元素,则直接返回true
    218. if (knownProviders.hasNext())
    219. return true;
    220. //返回延迟加载器是否存在元素
    221. return lookupIterator.hasNext();
    222. }
    223. //获取下一个元素
    224. public S next() {
    225. //如果knownProviders存在元素,则直接获取
    226. if (knownProviders.hasNext())
    227. return knownProviders.next().getValue();
    228. //获取延迟迭代器lookupIterator中的元素
    229. return lookupIterator.next();
    230. }
    231. public void remove() {
    232. throw new UnsupportedOperationException();
    233. }
    234. };
    235. }
    236. //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
    237. public static ServiceLoader load(Class service,
    238. ClassLoader loader)
    239. {
    240. return new ServiceLoader<>(service, loader);
    241. }
    242. //根据类的Class对象加载指定的类,返回ServiceLoader对象
    243. public static ServiceLoader load(Class service) {
    244. //获取当前线程的类加载器
    245. ClassLoader cl = Thread.currentThread().getContextClassLoader();
    246. //动态加载指定的类,将类加载到ServiceLoader中
    247. return ServiceLoader.load(service, cl);
    248. }
    249. public static ServiceLoader loadInstalled(Class service) {
    250. ClassLoader cl = ClassLoader.getSystemClassLoader();
    251. ClassLoader prev = null;
    252. while (cl != null) {
    253. prev = cl;
    254. cl = cl.getParent();
    255. }
    256. return ServiceLoader.load(service, prev);
    257. }
    258. /**
    259. * Returns a string describing this service.
    260. *
    261. * @return A descriptive string
    262. */
    263. public String toString() {
    264. return "java.util.ServiceLoader[" + service.getName() + "]";
    265. }
    266. }

    SPI总结

    最后,对Java提供的SPI机制进行简单的总结。

    能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

    • 多个并发多线程使用ServiceLoader类的实例是不安全的
    • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。

     

  • 相关阅读:
    【RT学习笔记1】RT-Thread外设例程——I/O口控制(高低电平、中断)
    RN(React Native)的应用程序在雷电模拟器可以运行,安卓真机运行失败问题解决记录
    Win10下使用WinSCP+PuTTY实现远程文件操作和终端访问
    【ASM】字节码操作 工具类与常用类 AdviceAdapter 介绍 打印方法进入 和 方法退出 的参数
    华为MTL流程的六个模块初步解析
    重命名conda虚拟环境后jupyter无法启动的问题
    纯CSS实现炫酷文本时钟
    已安装的nginx追加ssl模块
    三面“有赞”Java岗斩获offer:Spring+JVM+并发锁+分布式+算法
    who命令
  • 原文地址:https://blog.csdn.net/guanshengg/article/details/126359434