fky

488 阅读8分钟

在软件设计中,模块化、可扩展性是永恒的主题。为了构建灵活、可插拔的系统架构,开发者们不断探索着各种设计模式和技术手段。而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 服务提供者

我们继续使用之前的 ServiceAServiceB 实现。

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 类中,我们可以通过传递指定的服务类名来加载特定的服务。例如,用户只想加载 ServiceAServiceB

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 的优先级为2
  • ServiceB 的优先级为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.

如果 serviceNamenull 或空字符串,则加载所有服务,按照优先级从高到低执行,输出结果如下:

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,根据更多的条件(如系统属性、环境变量)来筛选加载服务。