深度理解Java SPI机制
SPI 是一种用于动态加载服务的机制。它的核心思想就是解耦,属于典型的微内核架构模式。SPI 在 Java 世界应用非常广泛,如:Dubbo、Spring Boot 等框架。本文从源码入手分析,深入探讨 Java SPI 的特性、原理,以及在一些比较经典领域的应用。
1.了解SPI机制的概念和原理
SPI简介
SPI(Service Provider Interface)是Java提供的一种服务发现机制,旨在由第三方实现或扩展的 API,它允许在运行时动态地加载实现某个特定接口的类。SPI主要用于框架和库的扩展,SPI 机制主要思想是将装配的控制权移到程序之外,在程序模块化设计中这个机制尤为重要,其核心思想就是解耦。
SPI的原理
SPI的原理是基于Java的ClassLoader类加载机制实现的。在Java中,类的加载是由ClassLoader负责的。ClassLoader可以从不同的源加载类,例如从本地文件系统、网络、JAR文件或其他任何资源中加载类。SPI将服务的接口定义放在一个模块中,服务的实现放在另外的模块中,并通过ClassLoader动态地加载实现类。
SPI机制的优缺点
SPI机制的优点是灵活性高,可以通过简单地添加或替换实现类来扩展应用程序的功能。同时,SPI机制也具有一定的可扩展性和可维护性,因为它将应用程序和具体实现解耦,实现了高内聚、低耦合的目标。
SPI机制的缺点是需要开发者手动编写实现类并在META-INF/services目录下创建相关配置文件,这样会增加代码量和工作量。同时,SPI机制也存在安全风险,因为实现类是由外部提供的,安全性由开发者自行把控,可能存在恶意实现类的风险。
实现SPI机制
使用SPI接口有四个要素:
定义SPI接口:定义一个接口,声明一些抽象方法,为服务提供者定义实现类约定的的解耦或者抽象类。
创建SPI接口实现类:创建一个或多个实现该接口的类。
SPI配置文件:在META-INF/services/目录下创建一个以接口全限定名为命名的文件,内容为实现类的全限定名,文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
ServiceLoader加载配置:Java SPI的核心类,用于加载SPI配置文件并解析实现类。
注意事项
配置文件必须放在META-INF/services/目录下。
配置文件的文件名必须为接口的全限定名。
配置文件中每行只能有一个实现类的全限定名。
实现类必须有一个无参构造函数。
下面我们通过一个示例来演示如何使用SPI机制。
实现案例
假设我们有一个接口Animal和两个实现类Cat和Dog,我们希望通过SPI机制来加载实现类。
1.定义接口
public interface Animal {
void sayHello();
}
2.创建实现类
public class Cat implements Animal {
@Override
public void sayHello() {
System.out.println("Cat says hello.");
}
}
public class Dog implements Animal {
@Override
public void sayHello() {
System.out.println("Dog says hello.");
}
}
3.配置文件
在src/main/resources/META-INF/services/目录下创建一个名为com.example.Animal的文件,内容为实现类的全限定名,每行一个。
com.example.Cat
com.example.Dog
4.加载配置
public class Main {
public static void main(String[] args) {
ServiceLoader<Animal> loader = ServiceLoader.load(Animal.class);
for (Animal animal : loader) {
animal.sayHello();
}
}
}
运行结果:
Cat says hello.
Dog says hello.
可以看到,我们使用SPI机制成功加载了实现类,并调用了sayHello()方法。
SPI机制的优点在于可以通过配置文件来动态指定实现类,从而实现灵活的扩展和替换。缺点在于实现类必须有一个无参构造函数,且无法传递参数。
2.掌握SPI机制的使用方式
SPI机制的主要接口和类
在Java中,SPI(Service Provider Interface)是一种面向接口编程的方式,它是一组标准的Java API,用于在运行时发现和加载实现某个接口的服务提供者。
SPI机制的主要接口和类包括:
ServiceLoader类:该类是Java提供的用于加载和查找服务提供者实现的工具类。它通过读取类路径下的META-INF/services目录中的配置文件,自动加载并实例化配置文件中指定的服务提供者实现类。
Provider接口:该接口是服务提供者实现类需要实现的接口。它通常是一个空接口,用于标识服务提供者实现类的身份。
如何创建和配置SPI实现
要创建和配置SPI实现,需要进行以下步骤:
1.创建一个服务接口:定义一个服务接口,用于描述该服务的功能和方法。例如,定义一个数据库访问接口:
public interface DatabaseAccess {
public void connect();
public void disconnect();
public boolean isConnected();
}
2.创建一个服务提供者实现类:实现服务接口,并在该实现类中添加一个名为META-INF/services/服务接口全限定名
的文件。该文件中包含了该服务提供者实现类的全限定名。例如,创建一个MySQL数据库访问服务提供者实现类:
public class MySQLDatabaseAccessImpl implements DatabaseAccess {
@Override
public void connect() {
// Connect to MySQL database
}
@Override
public void disconnect() {
// Disconnect from MySQL database
}
@Override
public boolean isConnected() {
// Check if connected to MySQL database
return false;
}
}
3.在该实现类的META-INF/services/服务接口全限定名
文件中添加以下内容:
com.example.DatabaseAccess
com.example.MySQLDatabaseAccessImpl
4.使用ServiceLoader类加载服务提供者实现类:使用ServiceLoader类加载服务提供者实现类,可以通过以下代码实现:
ServiceLoader<DatabaseAccess> loader = ServiceLoader.load(DatabaseAccess.class);
for (DatabaseAccess databaseAccess : loader) {
// Do something with databaseAccess
}
如何获取SPI实现
要获取SPI实现,只需要使用ServiceLoader类即可。ServiceLoader类提供了以下方法:
load(Class<S> service)
:加载指定接口的服务提供者实现。reload()
:重新加载所有的服务提供者实现。iterator()
:获取服务提供者实现的迭代器。
以下代码展示了如何获取MySQL数据库访问服务提供者实现类:
ServiceLoader<DatabaseAccess> loader = ServiceLoader.load(DatabaseAccess.class);
for (DatabaseAccess databaseAccess : loader) {
if (databaseAccess instanceof MySQLDatabaseAccessImpl) {
MySQLDatabaseAccessImpl mySQLDatabaseAccess = (MySQLDatabaseAccessImpl) databaseAccess;
mySQLDatabaseAccess.connect();
// Do something with mySQLDatabaseAccess
}
}
上面的代码,ServiceLoader类加载了DatabaseAccess接口的所有实现类,然后使用forEach()方法遍历所有实现类,并调用其方法进行数据库操作。
SPI机制在实际项目中的应用非常广泛,常见的应用场景有:
日志框架。例如SLF4J、Log4j等都使用了SPI机制,让用户自由选择使用不同的实现库。
数据库访问框架。例如Mybatis、Hibernate等都使用了SPI机制,让用户自由选择使用不同的数据库驱动。
RPC框架。例如Dubbo、Motan等都使用了SPI机制,让用户自由选择使用不同的序列化协议、负载均衡算法等。
容器框架。例如Spring、Guice等都使用了SPI机制,让用户自由选择使用不同的依赖注入、AOP等实现。
总之,SPI机制在Java开发中有着广泛的应用,可以让应用程序更加灵活、可扩展。但是,需要注意的是,SPI机制的实现需要遵循一定的规范,否则可能会引发一些问题。同时,SPI机制也有一些缺陷,例如无法在运行时动态添加实现类等,需要开发者在实际应用中进行权衡和选择。
3.深入了解SPI机制的实现细节
SPI实现的加载过程
SPI机制的实现需要遵循一定的规则,主要是在META-INF/services目录下创建以接口的全限定名命名的文件,并将实现类的全限定名按行写入该文件。例如,如果我们有一个名为com.example.MyService的接口,那么在META-INF/services目录下应该创建一个名为com.example.MyService的文件,并将实现类的全限定名写入该文件。
SPI机制的加载过程主要涉及以下步骤:
当应用程序调用ServiceLoader.load(service)方法时,ServiceLoader类会通过当前线程的上下文类加载器(context class loader)来加载服务提供者配置文件。
ServiceLoader类会将服务提供者配置文件中的每一行作为一个服务实现类的全限定名,使用类加载器加载并实例化这些类,最后返回实现了该服务接口的所有对象的集合(Lazy Loading)。
当应用程序需要使用服务时,可以通过ServiceLoader.iterator()方法获取一个迭代器,遍历并使用服务提供者的实现。
如何在META-INF/services目录下注册SPI实现
在META-INF/services目录下注册SPI实现需要创建以接口的全限定名命名的文件,并将实现类的全限定名按行写入该文件。例如,如果我们有一个名为com.example.MyService的接口,那么在META-INF/services目录下应该创建一个名为com.example.MyService的文件,并将实现类的全限定名写入该文件。
以DatabaseAccess接口为例,我们可以在META-INF/services目录下创建名为com.example.DatabaseAccess的文件,并将实现类的全限定名按行写入该文件,如下所示:
com.example.DatabaseAccessImpl1
com.example.DatabaseAccessImpl2
如何使用SPI机制加载不同的实现
使用SPI机制加载不同的实现可以通过以下代码实现:
ServiceLoader<DatabaseAccess> serviceLoader = ServiceLoader.load(DatabaseAccess.class);
for (DatabaseAccess databaseAccess : serviceLoader) {
databaseAccess.queryData();
}
通过ServiceLoader.load(DatabaseAccess.class)方法加载指定接口的实现,并通过迭代器遍历获取实现对象,即可使用不同的实现。
如何避免SPI机制的安全问题
SPI机制存在安全问题,因为SPI的实现类是由应用程序的上下文类加载器加载的,而如果存在恶意的SPI实现,它可能会通过修改ClassPath的方式来影响应用程序。为了避免SPI机制的安全问题,可以考虑以下几个方面:
验证实现类的合法性:SPI实现类必须是提供者定义的、公开可见的、具有无参构造函数并实现了SPI接口,如果不符合这些条件则应该抛出异常或忽略掉该实现类。
防止恶意实现类:SPI实现类在被加载时,其构造函数可能会被执行,因此应该避免在构造函数中执行任何具有副作用的代码,以防止恶意实现类的攻击。
使用安全沙箱机制:可以使用Java提供的安全沙箱机制,对SPI实现类的代码进行隔离和控制,防止恶意实现类对系统进行攻击。
定期更新SPI实现:由于SPI实现通常是通过外部库或框架提供的,因此应该定期更新这些库或框架,以确保其包含的SPI实现都是安全的。
不依赖SPI实现的具体实现类:在代码中不应该直接依赖于SPI实现的具体实现类,而应该通过接口或抽象类来定义API,以便在需要时更换不同的实现类。
4.学习SPI机制的高级应用
如何扩展和定制SPI机制
SPI机制在Java平台上已经得到广泛的应用,而在某些场景下,我们可能需要扩展和定制SPI机制以满足特定的需求。下面介绍一些常见的扩展和定制方法:
1.自定义SPI接口
可以定义自己的SPI接口,实现SPI机制的扩展和定制。比如,可以定义一个新的SPI接口,实现与标准SPI接口不同的实现机制,或者在标准SPI接口的基础上添加新的功能。
2.自定义SPI实现
除了自定义SPI接口之外,也可以自定义SPI实现来扩展和定制SPI机制。这种方式可以在标准SPI实现的基础上,添加自己的实现逻辑,或者修改标准SPI实现的行为。
3.自定义SPI配置文件
可以通过自定义SPI配置文件,来扩展和定制SPI机制。SPI配置文件的格式与标准的SPI配置文件相同,只是内容不同。在自定义SPI配置文件中,可以定义新的SPI实现,或者修改标准SPI实现的行为。
如何使用SPI机制实现插件化架构
插件化架构是一种通过插件扩展系统功能的设计模式。在Java平台上,可以使用SPI机制来实现插件化架构。下面是一个简单的插件化示例:
首先,定义一个插件接口:
public interface Plugin {
void execute();
}
然后,定义两个插件实现类:
public class PluginA implements Plugin {
@Override
public void execute() {
System.out.println("PluginA.execute() is called.");
}
}
public class PluginB implements Plugin {
@Override
public void execute() {
System.out.println("PluginB.execute() is called.");
}
}
接着,创建一个SPI配置文件META-INF/services/com.example.Plugin
,并在其中定义插件实现类:
com.example.PluginA
com.example.PluginB
最后,通过ServiceLoader类加载插件实现类,并调用插件的执行方法:
public class Main {
public static void main(String[] args) {
ServiceLoader<Plugin> plugins = ServiceLoader.load(Plugin.class);
for (Plugin plugin : plugins) {
plugin.execute();
}
}
}
执行该程序,可以看到输出结果:
PluginA.execute() is called.
PluginB.execute() is called.
通过SPI机制,我们可以将插件的实现类动态地加载到程序中,从而实现插件化架构。
如何使用SPI机制实现动态配置
使用SPI机制可以实现动态配置,这是因为在SPI机制中,不同的实现类都通过一定的配置方式注册到META-INF/services目录下,因此可以通过修改或替换META-INF/services目录下的配置文件来实现动态配置。
具体实现方法如下:
1.定义接口
首先,需要定义一个接口,例如:
public interface Configurable {
void configure(Properties properties);
}
该接口包含一个configure方法,用于接收配置参数。
2.实现接口
在不同的实现类中,可以根据具体需求实现该接口。例如:
public class MyConfigurableImpl implements Configurable {
@Override
public void configure(Properties properties) {
// 从properties中读取配置参数,并做相应处理
}
}
3.注册实现类
将实现类的全限定名写入META-INF/services/com.example.Configurable配置文件中。例如,在项目中创建META-INF/services/com.example.Configurable文件,写入以下内容:
com.example.MyConfigurableImpl
4.加载并配置实现类
在需要使用实现类的地方,可以使用ServiceLoader类加载实现类,并调用configure方法进行配置。例如:
ServiceLoader<Configurable> serviceLoader = ServiceLoader.load(Configurable.class);
Properties properties = loadPropertiesFromConfigFile();
for (Configurable configurable : serviceLoader) {
configurable.configure(properties);
}
在上述代码中,首先通过ServiceLoader类加载Configurable接口的实现类,然后从配置文件中读取配置参数,并依次调用每个实现类的configure方法进行配置。
通过修改META-INF/services/com.example.Configurable配置文件,可以动态修改实现类,从而实现动态配置。
如何使用SPI机制实现服务发现和注册
SPI机制也可以用于实现服务发现和注册的功能。服务发现和注册是指在分布式系统中,服务提供者将自己提供的服务注册到服务注册中心,服务消费者从服务注册中心获取可用的服务列表,并调用相应的服务。
在Java中,可以使用SPI机制实现服务注册和发现。具体实现方式为,在服务提供者实现接口时,在META-INF/services目录下创建一个以接口全限定名命名的文件,文件中每行填写一个实现类的全限定名,表示这个实现类是服务提供者提供的服务。服务消费者使用ServiceLoader类加载这个接口的实现,获取可用的服务列表,并调用相应的服务。
以下是一个示例代码:
// 服务提供者接口
public interface UserService {
void login(String username, String password);
}
// 服务提供者实现类1
public class UserServiceImpl1 implements UserService {
public void login(String username, String password) {
System.out.println("UserServiceImpl1 login: " + username + ", " + password);
}
}
// 服务提供者实现类2
public class UserServiceImpl2 implements UserService {
public void login(String username, String password) {
System.out.println("UserServiceImpl2 login: " + username + ", " + password);
}
}
// 服务提供者在META-INF/services目录下注册服务
// 文件名为服务接口的全限定名,文件内容为实现类的全限定名
// META-INF/services/com.example.UserService
// com.example.UserServiceImpl1
// com.example.UserServiceImpl2
// 服务消费者使用ServiceLoader类获取服务列表并调用服务
public class UserServiceConsumer {
public static void main(String[] args) {
ServiceLoader<UserService> serviceLoader = ServiceLoader.load(UserService.class);
for (UserService userService : serviceLoader) {
userService.login("admin", "password");
}
}
}
在这个例子中,服务提供者实现了UserService接口,将自己的实现类注册到META-INF/services/com.example.UserService文件中。服务消费者使用ServiceLoader类加载UserService接口的实现,并调用它们的login方法。这样,服务消费者就可以通过SPI机制发现并使用服务提供者提供的服务了。
需要注意的是,服务提供者和消费者需要约定服务接口和SPI文件的格式。如果格式不正确,SPI机制就无法正常工作。同时,服务提供者还需要注意不要将敏感信息泄露到SPI文件中,以免造成安全问题。
5.实践SPI机制的应用案例
SPI机制在Java框架中的应用
SPI机制在Java框架中得到了广泛应用,以下是一些常见的使用场景:
JDBC驱动:Java中的JDBC规范定义了一组接口,允许应用程序访问不同数据库的统一方式。JDBC驱动程序实现了这些接口。Java应用程序通过SPI机制加载所需的数据库驱动程序。
Servlet容器:Java Servlet API定义了一组接口,用于处理HTTP请求和响应。Web服务器或Servlet容器通过SPI机制加载Servlet API实现,以便可以执行应用程序定义的Servlet。
日志系统:Java中的日志系统允许开发人员在应用程序中记录消息和异常。许多常见的日志系统都使用SPI机制加载不同的日志实现。
Spring框架:Spring框架使用SPI机制实现了许多核心功能,如依赖注入、AOP、事务管理等。
SPI机制在开源项目中的应用
除了Java框架之外,许多开源项目也使用SPI机制实现插件化、扩展性和可配置性。以下是一些常见的使用场景:
Elasticsearch:Elasticsearch是一款分布式搜索和分析引擎,使用SPI机制来加载插件。Elasticsearch本身只提供了一组核心功能,如文档存储和搜索。其他功能,如集群管理、安全性和监控等,则由插件实现。
Dubbo:Dubbo是一款高性能、轻量级的RPC框架,使用SPI机制来加载扩展点。Dubbo本身只提供了一组核心功能,如服务注册和发现、负载均衡、容错处理等。其他功能,如协议、序列化、路由等,则由扩展点实现。
Hadoop:Hadoop是一款分布式计算框架,使用SPI机制来加载各种文件系统。Hadoop支持不同类型的文件系统,如HDFS、S3、Swift等。每种文件系统都由独立的模块实现,这些模块通过SPI机制加载。
如何使用SPI机制实现跨组件的扩展性和可配置性
SPI机制可以帮助实现跨组件的扩展性和可配置性,具体方法如下:
定义SPI接口,定义需要扩展的功能,并提供接口方法。
实现SPI接口,编写具体的实现逻辑,并在META-INF/services目录下创建对应的配置文件,将实现类的全类名写入配置文件中。
在需要使用SPI功能的组件中,通过ServiceLoader类加载SPI接口的所有实现类,得到实现类的实例,实现扩展性和可配置性。
下面以一个简单的例子说明如何使用SPI机制实现跨组件的扩展性和可配置性:
1.定义SPI接口:
public interface DataProvider {
String getData();
}
2.实现SPI接口:
public class FileDataProvider implements DataProvider {
@Override
public String getData() {
// 从文件中读取数据
return "data from file";
}
}
在META-INF/services目录下创建文件 "com.example.DataProvider",并写入 "com.example.FileDataProvider",表示FileDataProvider是DataProvider的实现类。
3.使用SPI功能的组件中加载DataProvider的实现类:
public class Main {
public static void main(String[] args) {
ServiceLoader<DataProvider> serviceLoader = ServiceLoader.load(DataProvider.class);
for (DataProvider provider : serviceLoader) {
System.out.println(provider.getData());
}
}
}
通过ServiceLoader类加载DataProvider接口的实现类,得到FileDataProvider实例,并调用getData()方法获取数据。
这样,通过SPI机制,可以方便地实现跨组件的扩展性和可配置性,将不同组件的功能进行解耦和灵活配置。
6.实践SPI机制的应用案例
应用案例之JDBC DriverManager
作为Java工程师,尤其是CRUD工程师,相比都非常熟悉JDBC。众所周知,关系型数据库有很多种,例如:MySQL,Oracle,PostgreSQL。JDBC是如何识别各种数据库的驱动呢?
在JDBC4.0之前,Java应用程序连接数据的常用做法,通常是通过Class.forName("com.mysql.jdbc.Driver")
方法来加载数据库对应的驱动,然后在获取数据库连接,继而进行CRUD等操作。
在JDBC4.0,不再需要用Class.forName(XXX)
方法来加载数据库驱动,直接获取连接就可以了。显然,这种方式很方便,但是如何做到的呢?主要分为一下四个步骤:
(1)JDBC 接口:首先,Java 中内置了接口 java.sql.Driver。
(2)JDBC 接口实现:各个数据库的驱动自行实现 java.sql.Driver 接口,用于管理数据库连接。
MySQL:在 MySQL的 Java 驱动包 mysql-connector-java-XXX.jar 中,可以找到 META-INF/services 目录,该目录下会有一个名字为java.sql.Driver
的文件,文件内容是com.mysql.cj.jdbc.Driver
。
com.mysql.cj.jdbc.Driver
正是 MySQL 版的 java.sql.Driver 实现。如下图所示:
(3)创建数据库连接
以MySQL为例,创建数据库连接代码如下:
final String DB_URL = String.format("jdbc:mysql://%s:%s/%s", DB_HOST, DB_PORT, DB_SCHEMA); connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
(4)DriverManager
查看
com.mysql.cj.jdbc.Driver
可以发现DriverManager是创建数据库连接的关键,查看源码可以看到它的工作原理。可以看到DriverManager是用来加载实例化驱动的,看看loadInitialDrivers方法:
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 通过 classloader 获取所有实现 java.sql.Driver 的驱动类 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 利用 SPI,记载所有 Driver 服务 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 获取迭代器 Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ // 遍历迭代器 while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); // 打印数据库驱动信息 println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); // 尝试实例化驱动 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
上面代码的主要步骤:
从系统变量中获取驱动的实现类。
利用 SPI 来获取所有驱动的实现类。
遍历所有驱动,尝试实例化各个实现类。
根据第 1 步获取到的驱动列表来实例化具体的实现类。
需要关注的是这行代码:ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这里实际获取的是java.util.ServiceLoader.LazyIterator 迭代器。调用其 hasNext 方法时,会搜索 classpath 下以及 jar 包中的 META-INF/services 目录,查找 java.sql.Driver 文件,并找到文件中的驱动实现类的全限定名。调用其 next 方法时,会根据驱动类的全限定名去尝试实例化一个驱动类的对象。
应用案例之Common-Loggin
common-logging(也称 Jakarta Commons Logging,缩写 JCL)是常用的日志门面工具包。common-logging 的核心类是入口是 LogFactory,LogFatory 是一个抽象类,它负责加载具体的日志实现。
其入口方法是 LogFactory.getLog 方法,源码如下:
public static Log getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}
public static Log getLog(String name) throws LogConfigurationException {
return getFactory().getInstance(name);
}
从以上源码可知,getLog 采用了工厂设计模式,是先调用 getFactory 方法获取具体日志库的工厂类,然后根据类名称或类型创建日志实例。
LogFatory.getFactory 方法负责选出匹配的日志工厂,其源码如下:
public static LogFactory getFactory() throws LogConfigurationException {
// 省略...
// 加载 commons-logging.properties 配置文件
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
// 省略...
// 决定创建哪个 LogFactory 实例
// (1)尝试读取全局属性 org.apache.commons.logging.LogFactory
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Looking for system property [" + FACTORY_PROPERTY +
"] to define the LogFactory subclass to use...");
}
try {
// 如果指定了 org.apache.commons.logging.LogFactory 属性,尝试实例化具体实现类
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
if (factoryClass != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass +
"' as specified by system property " + FACTORY_PROPERTY);
}
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
} else {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] No system property [" + FACTORY_PROPERTY + "] defined.");
}
}
} catch (SecurityException e) {
// 异常处理
} catch (RuntimeException e) {
// 异常处理
}
// (2)利用 Java SPI 机制,尝试在 classpatch 的 META-INF/services 目录下寻找 org.apache.commons.logging.LogFactory 实现类
if (factory == null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
"] to define the LogFactory subclass to use...");
}
try {
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
if( is != null ) {
// This code is needed by EBCDIC and other strange systems.
// It's a fix for bugs reported in xerces
BufferedReader rd;
try {
rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
} catch (java.io.UnsupportedEncodingException e) {
rd = new BufferedReader(new InputStreamReader(is));
}
String factoryClassName = rd.readLine();
rd.close();
if (factoryClassName != null && ! "".equals(factoryClassName)) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Creating an instance of LogFactory class " +
factoryClassName +
" as specified by file '" + SERVICE_ID +
"' which was present in the path of the context classloader.");
}
factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
}
} else {
// is == null
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] No resource file with name '" + SERVICE_ID + "' found.");
}
}
} catch (Exception ex) {
// note: if the specified LogFactory class wasn't compatible with LogFactory
// for some reason, a ClassCastException will be caught here, and attempts will
// continue to find a compatible class.
if (isDiagnosticsEnabled()) {
logDiagnostic(
"[LOOKUP] A security exception occurred while trying to create an" +
" instance of the custom factory class" +
": [" + trim(ex.getMessage()) +
"]. Trying alternative implementations...");
}
// ignore
}
}
// (3)尝试从 classpath 目录下的 commons-logging.properties 文件中查找 org.apache.commons.logging.LogFactory 属性
if (factory == null) {
if (props != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic(
"[LOOKUP] Looking in properties file for entry with key '" + FACTORY_PROPERTY +
"' to define the LogFactory subclass to use...");
}
String factoryClass = props.getProperty(FACTORY_PROPERTY);
if (factoryClass != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic(
"[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
}
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
// TODO: think about whether we need to handle exceptions from newFactory
} else {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
}
}
} else {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] No properties file available to determine" + " LogFactory subclass from..");
}
}
}
// (4)以上情况都不满足,实例化默认实现类 org.apache.commons.logging.impl.LogFactoryImpl
if (factory == null) {
if (isDiagnosticsEnabled()) {
logDiagnostic(
"[LOOKUP] Loading the default LogFactory implementation '" + FACTORY_DEFAULT +
"' via the same classloader that loaded this LogFactory" +
" class (ie not looking in the context classloader).");
}
factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
}
if (factory != null) {
/**
* Always cache using context class loader.
*/
cacheFactory(contextClassLoader, factory);
if (props != null) {
Enumeration names = props.propertyNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
String value = props.getProperty(name);
factory.setAttribute(name, value);
}
}
}
return factory;
}
从 getFactory 方法的源码可以看出,其核心逻辑分为 4 步:
首先,尝试查找全局属性org.apache.commons.logging.LogFactory,如果指定了具体类,尝试创建实例。
利用 Java SPI 机制,尝试在 classpatch 的 META-INF/services 目录下寻找org.apache.commons.logging.LogFactory 的实现类。
尝试从 classpath 目录下的 commons-logging.properties 文件中查找org.apache.commons.logging.LogFactory 属性,如果指定了具体类,尝试创建实例。
以上情况如果都不满足,则实例化默认实现类,即org.apache.commons.logging.impl.LogFactoryImpl。
应用案例之Spring Boot
Spring Boot 是基于 Spring 构建的框架,其设计目的在于简化 Spring 应用的配置、运行。在 Spring Boot 中,大量运用了自动装配来尽可能减少配置。
下面是一个 Spring Boot 入口示例,可以看到,代码非常简洁。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
return String.format("Hello %s!", name);
}
}
那么,Spring Boot 是如何做到寥寥几行代码,就可以运行一个 Spring Boot 应用的呢。我们不妨带着疑问,从源码入手,一步步探究其原理。
@SpringBootApplication 注解
首先,Spring Boot 应用的启动类上都会标记一个@SpringBootApplication 注解。
@SpringBootApplication 注解定义如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
// 略
}
除了 @Target、 @Retention、@Documented、@Inherited 这几个元注解,
@SpringBootApplication 注解的定义中还标记了 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 三个注解。
@SpringBootConfiguration 注解
从@SpringBootConfiguration 注解的定义来看,@SpringBootConfiguration 注解本质上就是一个 @Configuration 注解,这意味着被@SpringBootConfiguration 注解修饰的类会被 Spring Boot 识别为一个配置类。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
@EnableAutoConfiguration 注解
@EnableAutoConfiguration 注解定义如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
@EnableAutoConfiguration 注解包含了 @AutoConfigurationPackage与 @Import({AutoConfigurationImportSelector.class}) 两个注解。
@AutoConfigurationPackage 注解
@AutoConfigurationPackage 会将被修饰的类作为主配置类,该类所在的 package 会被视为根路径,Spring Boot 默认会自动扫描根路径下的所有 Spring Bean(被 @Component 以及继承 @Component 的各个注解所修饰的类)。
这就是为什么 Spring Boot 的启动类一般要置于根路径的原因。这个功能等同于在 Spring xml 配置中通过 context:component-scan 来指定扫描路径。@Import 注解的作用是向 Spring 容器中直接注入指定组件。@AutoConfigurationPackage 注解中注明了@Import({Registrar.class})。Registrar 类用于保存 Spring Boot 的入口类、根路径等信息。
SpringFactoriesLoader.loadFactoryNames 方法
@Import(AutoConfigurationImportSelector.class) 表示直接注入AutoConfigurationImportSelector。
AutoConfigurationImportSelector 有一个核心方法getCandidateConfigurations 用于获取候选配置。该方法调用了SpringFactoriesLoader.loadFactoryNames 方法,这个方法即为 Spring Boot SPI 的关键,它负责加载所有 META-INF/spring.factories 文件,加载的过程由 SpringFactoriesLoader 负责。
Spring Boot 的 META-INF/spring.factories 文件本质上就是一个 properties 文件,数据内容就是一个个键值对。
SpringFactoriesLoader.loadFactoryNames 方法的关键源码:
// spring.factories 文件的格式为:key=value1,value2,value3
// 遍历所有 META-INF/spring.factories 文件
// 解析文件,获得 key=factoryClass 的类名称
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, 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 {
// 获取资源文件路径
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);
// 遍历解析出的 properties,组装数据
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.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 文件,加载过程有 SpringFactoriesLoader 负责。
在 CLASSPATH 中搜寻所有 META-INF/spring.factories 配置文件。
然后,解析 spring.factories 文件,获取指定自动装配类的全限定名。
Spring Boot 的 AutoConfiguration 类
Spring Boot 有各种 starter 包,可以根据实际项目需要,按需取材。在项目开发中,只要将 starter 包引入,我们就可以用很少的配置,甚至什么都不配置,即可获取相关的能力。通过前面的 Spring Boot SPI 流程,只完成了自动装配工作的一半,剩下的工作如何处理呢 ?
以 spring-boot-starter-web 的 jar 包为例,查看其 maven pom,可以看到,它依赖于 spring-boot-starter,所有 Spring Boot 官方 starter 包都会依赖于这个 jar 包。而 spring-boot-starter 又依赖于 spring-boot-autoconfigure,Spring Boot 的自动装配秘密,就在于这个 jar 包。
从 spring-boot-autoconfigure 包的结构来看,它有一个 META-INF/spring.factories ,显然利用了 Spring Boot SPI,来自动装配其中的配置类。
下面是 spring-boot-autoconfigure 的 META-INF/spring.factories 文件的部分内容,可以看到其中注册了一长串会被自动加载的 AutoConfiguration 类。
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer
# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider
以 RedisAutoConfiguration 为例,这个配置类中,会根据 @ConditionalXXX 中的条件去决定是否实例化对应的 Bean,实例化 Bean 所依赖的重要参数则通过 RedisProperties 传入。
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
RedisProperties 中维护了 Redis 连接所需要的关键属性,只要在 yml 或 properties 配置文件中,指定 spring.redis 开头的属性,都会被自动装载到 RedisProperties 实例中。
@ConfigurationProperties(
prefix = "spring.redis"
)
public class RedisProperties {
private int database = 0;
private String url;
private String host = "localhost";
private String username;
private String password;
private int port = 6379;
private boolean ssl;
private Duration timeout;
private Duration connectTimeout;
private String clientName;
private ClientType clientType;
private Sentinel sentinel;
private Cluster cluster;
private final Jedis jedis = new Jedis();
private final Lettuce lettuce = new Lettuce();
//xxx
}
通过以上分析,已经一步步解读出 Spring Boot 自动装载的原理。
应用案例之Dubbo
Dubbo 并未直接使用 Java SPI,而是自己封装了一套新的 SPI 机制。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容形式如下:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样可以按需加载指定的实现类。Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性。
ExtensionLoader 入口
Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,可以加载指定的实现类。
ExtensionLoader 的 getExtension 方法是其入口方法,其源码如下:
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 方法创建拓展对象。那么,createExtension 是如何创建拓展对象的呢,其源码如下:
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 变量
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
}
createExtension 方法的的工作步骤可以归纳为:
通过 getExtensionClasses 获取所有的拓展类
通过反射创建拓展对象
向拓展对象中注入依赖
将拓展对象包裹在相应的 Wrapper 对象中
以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。
获取所有的拓展类
Dubbo 在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码分析如下:
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;
}
这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 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...");
}
// 设置默认名称,参考 getDefaultExtension 方法
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// 加载指定文件夹下的配置文件
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 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();
// 加载资源
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("...");
}
}
loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。
private void loadResource(Map<String, Class<?>> extensionClasses,
ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
String line;
// 按行读取配置内容
while ((line = reader.readLine()) != null) {
// 定位 # 字符
final int ci = line.indexOf('#');
if (ci >= 0) {
// 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
// 以等于号 = 为界,截取键与值
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// 加载类,并通过 loadClass 方法对类进行缓存
loadClass(extensionClasses, resourceURL,
Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class...");
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class...");
}
}
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL,
Class<?> clazz, String name) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("...");
}
// 检测目标类上是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
// 设置 cachedAdaptiveClass缓存
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("...");
}
// 检测 clazz 是否是 Wrapper 类型
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
// 存储 clazz 到 cachedWrapperClasses 缓存中
wrappers.add(clazz);
// 程序进入此分支,表明 clazz 是一个普通的拓展类
} else {
// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
clazz.getConstructor();
if (name == null || name.length() == 0) {
// 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("...");
}
}
// 切分 name
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
// 存储 name 到 Activate 注解对象的映射关系
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
// 存储 Class 到名称的映射关系
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
// 存储名称到 Class 的映射关系
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("...");
}
}
}
}
}
如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。