Java Review - SPI 原理、实现与最佳实践


在这里插入图片描述


Pre

插件 - 通过SPI方式实现插件管理

插件 - 一份配置,离插件机制只有一步之遥

插件 - 插件机制触手可及


1. Java SPI 概述

SPI(Service Provider Interface)是Java的一种机制,用于实现服务的动态发现和加载。SPI允许开发者在不修改应用程序代码的情况下,通过配置文件动态地为接口添加不同的实现。

SPI的典型应用场景

  • 插件机制:允许第三方开发者扩展功能。
  • 动态加载模块:根据需求加载不同模块或功能。
  • 解耦合:通过接口和实现的分离,使得代码更加模块化和灵活。

2. SPI 基本用法

在这里插入图片描述

2.1 创建一个SPI接口

首先,我们创建一个简单的SPI接口。例如,我们定义一个RpcAccessPoint接口:

package com.artisan.rpc;

import java.io.Closeable;
import java.net.URI;

public interface RpcAccessPoint extends Closeable {

    /**
     * 客户端获取远程服务的引用
     * @param uri 远程服务地址
     * @param serviceClass  服务的接口类的Class
     * @param <T> 服务接口的类型
     * @return  远程服务的引用
     */
    <T> T getRemoteService(URI uri , Class<T> serviceClass);


    /**
     * 向远程服务添加服务提供者
     * @param service  实现实例
     * @param serviceClass
     * @param <T> 服务接口的类型
     * @return 远程服务的地址
     */
    <T> URI addServiceProvider(T service , Class<T> serviceClass);


    /**
     * 服务端启动RPC框架,监听接口,开始提供远程服务。
     * @return 服务实例,用于程序停止的时候安全关闭服务。
     */
    Closeable startServer() throws  Exception ;

}

2.2 创建多个SPI实现

接下来,我们创建两个不同的实现类,例如RpcAccessPointImplRpcAccessPointImpl2

package com.artisan.rpc.impl;

import com.artisan.rpc.RpcAccessPoint;
import com.artisan.rpc.spi.Singleton;

import java.io.Closeable;
import java.io.IOException;
import java.net.URI;

/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2024/8/26 10:11
 * @mark: show me the code , change the world
 */
@Singleton
public class RpcAccessPointImpl implements RpcAccessPoint {
    @Override
    public <T> T getRemoteService(URI uri, Class<T> serviceClass) {
        System.out.println("getRemoteService called");
        return null;
    }

    @Override
    public <T> URI addServiceProvider(T service, Class<T> serviceClass) {
        return null;
    }

    @Override
    public Closeable startServer() throws Exception {
        return null;
    }

    @Override
    public void close() throws IOException {

    }
}
    
package com.artisan.rpc.impl;

import com.artisan.rpc.RpcAccessPoint;

import java.io.Closeable;
import java.io.IOException;
import java.net.URI;

/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2024/8/26 10:11
 * @mark: show me the code , change the world
 */
public class RpcAccessPointImpl2 implements RpcAccessPoint {
    @Override
    public <T> T getRemoteService(URI uri, Class<T> serviceClass) {
        return null;
    }

    @Override
    public <T> URI addServiceProvider(T service, Class<T> serviceClass) {
        return null;
    }

    @Override
    public Closeable startServer() throws Exception {
        return null;
    }

    @Override
    public void close() throws IOException {

    }
}
    

2.3 在META-INF/services中配置服务提供者

为让Java知道这些实现类,我们需要在META-INF/services目录下创建一个文件。文件名应与SPI接口的完全限定名一致,例如:META-INF/services/com.artisan.rpc.RpcAccessPoint

在这里插入图片描述

文件内容列出所有实现类的完全限定名:

com.artisan.rpc.impl.RpcAccessPointImpl
com.artisan.rpc.impl.RpcAccessPointImpl2
com.artisan.rpc.impl.RpcAccessPointImpl3

2.4 使用ServiceLoader加载和使用服务实现

现在,我们可以使用ServiceLoader来加载这些服务实现:

package com.artisan.rpc.spi;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * 提供服务加载功能的支持类,特别是处理单例服务
 * @author artisan
 */
public class ServiceSupport {
    /**
     * 存储单例服务的映射,确保每个服务只有一个实例
     */
    private final static Map<String, Object> singletonServices = new HashMap<>();

    /**
     * 加载单例服务实例
     * 本方法通过ServiceLoader加载服务,并确保加载的是单例服务实例
     * 如果无法找到对应的服务实例,将抛出ServiceLoadException
     *
     * @param service 服务类的Class对象,用于指定需要加载的服务类型
     * @param <S> 服务类的类型参数,表示服务类型的泛型
     * @return 单例服务实例,返回指定类型的单例服务对象
     * @throws ServiceLoadException 如果找不到服务实例,将抛出此自定义异常
     */
    public synchronized static <S> S load(Class<S> service) {
        // 使用ServiceLoader加载服务提供者,并通过stream进行处理
        // 调用ServiceSupport中的singletonFilter方法对服务实例进行过滤,确保是单例实例
        // findFirst方法找到第一个符合条件的服务实例
        // 如果没有找到服务实例,则抛出ServiceLoadException异常
        return StreamSupport.
                stream(ServiceLoader.load(service).spliterator(), false)
                .map(ServiceSupport::singletonFilter)
                .findFirst().orElseThrow(ServiceLoadException::new);
    }


    /**
     * 加载所有服务实例
     *
     * @param service 服务类的Class对象
     * @param <S> 服务类的类型参数
     * @return 所有服务实例的集合
     */
    public synchronized static <S> Collection<S> loadAll(Class<S> service) {
        // 使用ServiceLoader加载服务实例,并通过stream进行处理
        // 使用ServiceSupport的singletonFilter方法筛选服务实例
        // 最终将筛选后的服务实例收集到一个集合中
        return StreamSupport.
                stream(ServiceLoader.load(service).spliterator(), false)
                .map(ServiceSupport::singletonFilter).collect(Collectors.toList());
    }


    /**
     * 对服务实例进行单例过滤
     *
     * @param service 服务实例
     * @param <S> 服务类的类型参数
     * @return 单例过滤后的服务实例,如果该服务是单例的并且已有实例存在,则返回已存在的实例
     */
    @SuppressWarnings("unchecked")
    private static <S> S singletonFilter(S service) {
        // 检查服务类是否被标记为单例
        if(service.getClass().isAnnotationPresent(Singleton.class)) {
            // 获取服务类的全限定名
            String className = service.getClass().getCanonicalName();
            // 使用Double-checked locking原则,确保只有一个实例
            Object singletonInstance = singletonServices.putIfAbsent(className, service);
            // 如果之前没有实例存在,则返回当前实例;否则返回已存在的实例
            return singletonInstance == null ? service : (S) singletonInstance;
        } else {
            // 如果不是单例,则直接返回服务实例
            return service;
        }
    }

}

所有在META-INF/services中配置的RpcAccessPoint实现都会被实例化和调用。

ServiceSupport类负责加载服务实例。它使用Java的ServiceLoader机制来加载服务实现,并对这些实现进行单例检查和过滤.

ServiceSupport类中,singletonFilter方法用于检查服务实例是否是单例的。如果服务类上标注了@Singleton注解,则确保返回的是同一个实例

package com.artisan.rpc.spi;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton {
}

测试

import com.artisan.rpc.RpcAccessPoint;
import com.artisan.rpc.spi.ServiceSupport;
import org.testng.annotations.Test;

import java.util.Collection;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
@Test
public class SpiTest {

    @Test
    public void testSpi() {
        RpcAccessPoint rpcAccessPoint = ServiceSupport.load(RpcAccessPoint.class);
        System.out.println(rpcAccessPoint);
        // 测试是否是单例  看输出的内存地址
        rpcAccessPoint = ServiceSupport.load(RpcAccessPoint.class);
        System.out.println(rpcAccessPoint);

        // 自行方法调用
        rpcAccessPoint.getRemoteService(null, null);

        System.out.println("===============");

        Collection<RpcAccessPoint> rpcAccessPoints = ServiceSupport.loadAll(RpcAccessPoint.class);
        rpcAccessPoints.forEach(System.out::println);

        rpcAccessPoints = ServiceSupport.loadAll(RpcAccessPoint.class);
        rpcAccessPoints.forEach(System.out::println);
    }


}

在这里插入图片描述


通过SPI机制,项目可以实现服务的动态加载和插件化。ServiceSupport类作为服务加载的核心组件,使用ServiceLoader机制加载服务实现,并对这些实现进行单例检查和过滤。

这种机制使得项目具有良好的扩展性和灵活性,可以方便地添加新的服务提供者实现。


3. SPI与设计模式的结合

3.1 SPI与工厂模式结合

工厂模式常用于创建对象的实例。当结合SPI时,工厂类可以动态地选择服务实现,增强系统的灵活性。例如:

public interface PaymentServiceFactory {
    PaymentService createPaymentService();
}

通过SPI机制,我们可以有多个PaymentServiceFactory实现。使用ServiceLoader加载合适的工厂类来创建对象:

public class PaymentServiceFactoryLoader {
    public static PaymentService loadPaymentService() {
        ServiceLoader<PaymentServiceFactory> loader = ServiceLoader.load(PaymentServiceFactory.class);
        return loader.iterator().next().createPaymentService();
    }
}

这种方式将工厂模式与SPI结合,使得在不修改代码的情况下添加新工厂实现变得更加容易。

3.2 SPI与策略模式结合

策略模式允许在运行时选择算法或行为。通过SPI,可以动态加载策略,实现行为的灵活切换。例如:

public interface CompressionStrategy {
    void compress(String data);
}

可以通过SPI加载不同的压缩策略:

public class CompressionContext {
    private CompressionStrategy strategy;

    public CompressionContext() {
        ServiceLoader<CompressionStrategy> loader = ServiceLoader.load(CompressionStrategy.class);
        this.strategy = loader.iterator().next();
    }

    public void compressData(String data) {
        strategy.compress(data);
    }
}

3.3 SPI与观察者模式结合

SPI结合观察者模式可以用于实现事件驱动的架构。通过SPI动态加载观察者,实现对特定事件的响应。例如:

public interface EventListener {
    void onEvent(Event event);
}

在事件发生时,使用ServiceLoader加载并通知所有观察者:

public class EventManager {
    public void publishEvent(Event event) {
        ServiceLoader<EventListener> loader = ServiceLoader.load(EventListener.class);
        for (EventListener listener : loader) {
            listener.onEvent(event);
        }
    }
}

这种实现方式适用于需要动态添加或更改事件处理逻辑的场景。

34 SPI与装饰器模式结合

装饰器模式用于在不修改原有对象的情况下动态地为其增加功能。结合SPI,可以在运行时动态加载装饰器。例如:

public interface MessageService {
    String sendMessage(String message);
}

装饰器可以通过SPI动态加载并叠加:

public class MessageServiceDecorator implements MessageService {
    private MessageService delegate;

    public MessageServiceDecorator(MessageService delegate) {
        this.delegate = delegate;
    }

    @Override
    public String sendMessage(String message) {
        // 增加一些功能
        return delegate.sendMessage(message) + " [Decorated]";
    }
}

通过ServiceLoader加载并应用多个装饰器:

public class DecoratorLoader {
    public static MessageService loadDecoratedService() {
        ServiceLoader<MessageServiceDecorator> loader = ServiceLoader.load(MessageServiceDecorator.class);
        MessageService service = new BaseMessageService();
        for (MessageServiceDecorator decorator : loader) {
            service = new MessageServiceDecorator(service);
        }
        return service;
    }
}

具体应用案例:构建可扩展的日志框架

我们将实现一个可扩展的日志框架,通过SPI和工厂模式实现动态日志处理,并通过装饰器模式增加日志功能。

定义日志接口

public interface Logger {
    void log(String message);
}

实现基本的日志类

例如,控制台日志和文件日志:

public class ConsoleLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("Console Logger: " + message);
    }
}

public class FileLogger implements Logger {
    @Override
    public void log(String message) {
        // 简化:模拟文件日志
        System.out.println("File Logger: " + message);
    }
}

配置SPI

META-INF/services/com.example.Logger中列出所有日志实现:

com.example.ConsoleLogger
com.example.FileLogger

使用工厂模式加载日志器

public class LoggerFactory {
    public static Logger getLogger() {
        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
        return loader.iterator().next(); // 返回第一个日志器
    }
}

添加装饰器

public class TimestampLoggerDecorator implements Logger {
    private Logger logger;

    public TimestampLoggerDecorator(Logger logger) {
        this.logger = logger;
    }

    @Override
    public void log(String message) {
        logger.log("[" + System.currentTimeMillis() + "] " + message);
    }
}

使用装饰器:

public class LoggerFactoryWithDecorator {
    public static Logger getLogger() {
        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
        Logger logger = loader.iterator().next();
        return new TimestampLoggerDecorator(logger);
    }
}

使用日志框架

public class Application {
    public static void main(String[] args) {
        Logger logger = LoggerFactoryWithDecorator.getLogger();
        logger.log("Application started");
    }
}

高级功能:动态切换日志实现

通过外部配置文件或系统属性,动态选择加载不同的日志实现。

public class LoggerFactoryWithDynamicSelection {
    public static Logger getLogger(String loggerType) {
        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
        for (Logger logger : loader) {
            if (logger.getClass().getSimpleName().equalsIgnoreCase(loggerType)) {
                return new TimestampLoggerDecorator(logger);
            }
        }
        throw new IllegalArgumentException("No suitable logger found for type: " + loggerType);
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值