在软件设计中,模块化、可扩展性是永恒的主题。为了构建灵活、可插拔的系统架构,开发者们不断探索着各种设计模式和技术手段。而JDK原生的SPI(Service Provider Interface)机制,正是其中一种被广泛应用的利器。
SPI机制提供了一种标准化的方式,允许第三方开发者为某个接口提供实现,而无需修改原有代码。这种“即插即用”的特性,使得SPI机制在框架设计、插件系统等领域大放异彩。例如,Java的日志框架、数据库驱动、以及各种开源框架,都深度依赖SPI机制来实现扩展和定制。
然而,JDK原生的SPI机制也并非完美无缺。它存在着诸如加载顺序不可控、无法按需加载、缺乏服务发现机制等局限性。为了克服这些不足,我参照一些业界的顶级开源项目进行了一些实践。
正如上文,在某些场景中,我们希望根据业务需求指定加载某些服务提供者,而不是加载所有服务提供者。为了实现这一点,你可以通过服务过滤机制或指定服务类名来动态加载特定的服务,而不是按照配置文件中的所有服务进行加载。
文件流加载
1. 设计思路
- 按需加载:提供一种机制,允许用户指定某些服务的类名,加载时只加载这些服务,而不是加载所有服务。
- 优先级兼容:即使按需加载,仍然支持优先级排序机制。
- 配置文件兼容:仍然可以通过配置文件(如JSON、XML等)来定义服务信息。
2. 示例代码
2.1 服务接口定义
服务接口 Service 依然保持不变,所有的服务提供者都实现该接口。
package com.example.spi;
public interface Service {
void execute();
}
2.2 服务提供者
我们继续使用之前的 ServiceA 和 ServiceB 实现。
package com.example.spi.impl;
import com.example.spi.Service;
public class ServiceA implements Service {
@Override
public void execute() {
System.out.println("ServiceA is executing with priority.");
}
}
package com.example.spi.impl;
import com.example.spi.Service;
public class ServiceB implements Service {
@Override
public void execute() {
System.out.println("ServiceB is executing with priority.");
}
}
2.3 服务加载器(支持指定加载)
扩展 ServiceLoader 类,允许用户指定某些服务的类名进行加载。我们可以通过传入一个类名的列表来过滤要加载的服务。
package com.example.spi;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.w3c.dom.Document;ion;
import org.xml.sax.SAXExcept
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.stream.Collectors;
public class ServiceLoader {
private static final String JSON_CONFIG = "spi.json";
private static final String XML_CONFIG = "spi.xml";
// 服务信息类
static class ServiceInfo {
private String className;
private int priority;
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
}
// 加载所有服务提供者或指定的服务提供者
public static List<Service> loadServices(Set<String> targetServices) {
List<ServiceInfo> serviceInfos = new ArrayList<>();
// 加载JSON配置
serviceInfos.addAll(loadFromJsonConfig(JSON_CONFIG));
// 加载XML配置
serviceInfos.addAll(loadFromXmlConfig(XML_CONFIG));
// 按优先级排序
serviceInfos = serviceInfos.stream()
.sorted(Comparator.comparingInt(ServiceInfo::getPriority).reversed())
.collect(Collectors.toList());
// 加载并实例化服务,过滤出指定的服务
List<Service> services = new ArrayList<>();
for (ServiceInfo info : serviceInfos) {
// 如果targetServices为空,加载所有服务;否则只加载指定的服务
if (targetServices.isEmpty() || targetServices.contains(info.getClassName())) {
try {
Class<?> clazz = Class.forName(info.getClassName());
if (Service.class.isAssignableFrom(clazz)) {
Constructor<?> constructor = clazz.getDeclaredConstructor();
services.add((Service) constructor.newInstance());
}
} catch (Exception e) {
System.err.println("Failed to load service: " + info.getClassName());
e.printStackTrace();
}
}
}
return services;
}
// 从JSON文件加载配置
private static List<ServiceInfo> loadFromJsonConfig(String fileName) {
List<ServiceInfo> services = new ArrayList<>();
InputStream inputStream = ServiceLoader.class.getClassLoader().getResourceAsStream(fileName);
if (inputStream != null) {
try {
ObjectMapper mapper = new ObjectMapper();
ServiceInfo[] serviceArray = mapper.readValue(inputStream, ServiceInfo[].class);
return List.of(serviceArray);
} catch (IOException e) {
System.err.println("Error reading JSON config: " + fileName);
e.printStackTrace();
}
}
return services;
}
// 从XML文件加载配置
private static List<ServiceInfo> loadFromXmlConfig(String fileName) {
List<ServiceInfo> services = new ArrayList<>();
InputStream inputStream = ServiceLoader.class.getClassLoader().getResourceAsStream(fileName);
if (inputStream != null) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(inputStream);
var nodes = document.getElementsByTagName("service");
for (int i = 0; i < nodes.getLength(); i++) {
var node = nodes.item(i);
var className = node.getChildNodes().item(1).getTextContent();
var priority = Integer.parseInt(node.getChildNodes().item(3).getTextContent());
ServiceInfo info = new ServiceInfo();
info.setClassName(className);
info.setPriority(priority);
services.add(info);
}
} catch (ParserConfigurationException | SAXException | IOException e) {
System.err.println("Error reading XML config: " + fileName);
e.printStackTrace();
}
}
return services;
}
}
2.4 使用服务加载器(指定服务)
在 Main 类中,我们可以通过传递指定的服务类名来加载特定的服务。例如,用户只想加载 ServiceA 或 ServiceB。
package com.example.spi;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Main {
public static void main(String[] args) {
// 指定要加载的服务类名(如果为空,加载所有服务)
Set<String> targetServices = new HashSet<>();
targetServices.add("com.example.spi.impl.ServiceA");
// 加载指定的服务提供者
List<Service> services = ServiceLoader.loadServices(targetServices);
// 执行每个服务提供者的逻辑
for (Service service : services) {
service.execute();
}
}
}
3. 运行效果
假设你有以下两个服务提供者:
ServiceA的优先级为2ServiceB的优先级为1
当你运行 Main 类,并在代码中只指定加载 ServiceA 时,输出结果如下:
ServiceA is executing with priority.
如果你不指定任何服务类名,即传入一个空的 Set,则会加载所有服务提供者,输出如下:
ServiceA is executing with priority.
ServiceB is executing with priority.
4. 详细说明
4.1 按需加载机制
- 通过传入一个包含服务类名的
Set<String>,你可以指定需要加载的服务。ServiceLoader会根据这个集合过滤配置文件中的服务提供者,只加载被指定的服务。 - 如果
targetServices为空,表示加载所有的服务提供者。
4.2 优先级排序
- 即使是指定加载服务,程序仍然会按照配置文件中定义的优先级进行排序。优先级高的服务仍然会优先被执行。
4.3 可扩展性
- 你可以进一步扩展此功能,例如根据环境变量、系统属性等动态决定加载哪些服务。
- 还可以扩展支持更多的配置文件格式,如YAML,或者增加更多的加载策略,如懒加载等。
5. 总结
通过这种机制,用户可以灵活地指定需要加载的服务提供者,而不是无条件加载所有服务提供者。该实现兼容了优先级机制,并且支持通过外部配置文件(如JSON、XML)定义服务的信息。通过这种方式,你可以根据业务需求定制加载服务提供者的逻辑,提高服务加载的灵活性和可控性。
注解加载
实现步骤
1. 修改注解,添加 value 属性
我们扩展 @SPIService 注解,使其支持 value 属性,用于指定服务的名称。这样我们可以通过 value 动态筛选要加载的服务。
package com.example.spi.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// 注解用于标记服务提供者
@Retention(RetentionPolicy.RUNTIME)
public @interface SPIService {
// 服务提供者的优先级,默认为0
int priority() default 0;
// 服务的名称,默认为空
String value();
}
2. 修改服务实现,指定 value 属性
我们在服务实现类上使用 @SPIService 注解,并为每个服务指定一个唯一的名称,这样可以在加载时根据名称选择特定服务。
ServiceA 实现:
package com.example.spi.impl;
import com.example.spi.Service;
import com.example.spi.annotation.SPIService;
@SPIService(value = "serviceA", priority = 2) // 设置服务名称为 "serviceA",优先级为 2
public class ServiceA implements Service {
@Override
public void execute() {
System.out.println("ServiceA is executing with priority.");
}
}
ServiceB 实现:
package com.example.spi.impl;
import com.example.spi.Service;
import com.example.spi.annotation.SPIService;
@SPIService(value = "serviceB", priority = 1) // 设置服务名称为 "serviceB",优先级为 1
public class ServiceB implements Service {
@Override
public void execute() {
System.out.println("ServiceB is executing with priority.");
}
}
3. 修改服务加载器,支持按 value 加载
我们修改 ServiceLoader 类,使其根据注解中的 value 属性来加载指定的服务。如果用户指定了服务名称,那么只加载该名称对应的服务;如果没有指定名称,则默认加载所有服务。
package com.example.spi;
import com.example.spi.annotation.SPIService;
import org.reflections.Reflections;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
public class ServiceLoader {
// 加载所有服务提供者或指定名称的服务提供者
public static List<Service> loadServices(String packageName, String serviceName) {
List<ServiceInfo> serviceInfos = new ArrayList<>();
// 使用 Reflections 库扫描指定包下的所有类
Reflections reflections = new Reflections(packageName);
Set<Class<?>> serviceClasses = reflections.getTypesAnnotatedWith(SPIService.class);
// 遍历所有带有 @SPIService 注解的类
for (Class<?> clazz : serviceClasses) {
if (Service.class.isAssignableFrom(clazz)) { // 判断是否实现了 Service 接口
// 获取注解信息
SPIService annotation = clazz.getAnnotation(SPIService.class);
String value = annotation.value();
int priority = annotation.priority();
// 如果指定了服务名称,过滤出对应的服务
if (serviceName == null || serviceName.isEmpty() || serviceName.equals(value)) {
serviceInfos.add(new ServiceInfo(clazz, priority, value));
}
}
}
// 按优先级排序
serviceInfos.sort(Comparator.comparingInt(ServiceInfo::getPriority).reversed());
// 实例化服务类并返回
List<Service> services = new ArrayList<>();
for (ServiceInfo info : serviceInfos) {
try {
Constructor<?> constructor = info.getServiceClass().getDeclaredConstructor();
services.add((Service) constructor.newInstance());
} catch (Exception e) {
System.err.println("Failed to load service: " + info.getServiceClass().getName());
e.printStackTrace();
}
}
return services;
}
// 内部类,用于存储服务类和元数据信息
private static class ServiceInfo {
private final Class<?> serviceClass;
private final int priority;
private final String value;
public ServiceInfo(Class<?> serviceClass, int priority, String value) {
this.serviceClass = serviceClass;
this.priority = priority;
this.value = value;
}
public Class<?> getServiceClass() {
return serviceClass;
}
public int getPriority() {
return priority;
}
public String getValue() {
return value;
}
}
}
4. 使用服务加载器,按 value 加载服务
在 Main 类中,我们通过传递服务名称来加载指定的服务。如果 serviceName 为空或为 null,则加载所有服务;否则只加载名称匹配的服务。
package com.example.spi;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 指定服务名称(如果为空,加载所有服务)
String serviceName = "serviceA"; // 可以修改为 "serviceB" 或 NULL
// 加载指定的服务提供者
List<Service> services = ServiceLoader.loadServices("com.example.spi.impl", serviceName);
// 执行每个服务提供者的逻辑
for (Service service : services) {
service.execute();
}
}
}
5. 运行效果
假设你有两个服务提供者:
ServiceA:服务名称为"serviceA",优先级为2。ServiceB:服务名称为"serviceB",优先级为1。
如果在 Main 类中指定 serviceName = "serviceA",则只加载 ServiceA,输出结果如下:
ServiceA is executing with priority.
如果将 serviceName 置为 "serviceB",则只加载 ServiceB,输出结果如下:
ServiceB is executing with priority.
如果 serviceName 为 null 或空字符串,则加载所有服务,按照优先级从高到低执行,输出结果如下:
ServiceA is executing with priority.
ServiceB is executing with priority.
6. 详细说明
6.1 按 value 加载服务
- 我们通过
@SPIService注解的value属性为每个服务提供者指定一个唯一名称。 - 在
ServiceLoader中,通过传入的serviceName动态匹配服务的value,从而只加载特定名称的服务提供者。
6.2 优先级机制
- 即使按
value筛选服务,程序仍然会按照配置的priority对服务进行排序,并按优先级顺序加载和执行。
6.3 可扩展性
- 这种方式可以很容易地扩展为支持更多的服务元数据,例如版本控制、环境依赖等。
- 如果需要更复杂的指定加载规则,可以扩展
ServiceLoader,根据更多的条件(如系统属性、环境变量)来筛选加载服务。