什么是SPI?SPI在框架中的使用原理

什么是SPI

SPI的英文全称是Service Provider Interface,字面意思是服务提供接口,其实翻译成服务提供规范更好理解一些。
无论是在编程世界里还是在现实生活中,很多的组件或者商品我们都希望能够互相配合,无缝对接,以此来提高组件的复用率,减少组件适配带来的高昂成本、漫长等待等,这就是我们为什么一直提倡面向规范或者接口进行制造或者开发的原因了。因此,在生产制造商品或者组件时,只需要按照定义好的规范进行生产制造就好了,不同的规范有很多个实现,我们在组装软件或者商品时,需要哪个就具体的去指定哪个实现就好了。

SPI的使用场景

最常见的场景当我们连接数据库时,我们都需要为java.mysql.Driver这个规范根据不同的数据库厂商指定不同的驱动实现,如果数据库是MySQL,我们一般指定的实现是:com.mysql.cj.jdbc.Driver,如果数据库是Oricle,我们一般指定的实现是:oracle.jdbc.driver.OracleDriver。MySQL和Oricle的驱动实现都遵循java.mysql.Driver这个接口规范。还有其它的应用场景,比如Spring Boot 的扩展机制之 Spring Factories,这是JAVA SPI的变种,原理是一模一样的。

SPI手动实现

下面就简化SPI在框架中的使用核心,具体剖析SPI是如何工作的

业务场景

定义一个打招呼的规范 IHello,然后实现两个打招呼的组件HelloImpl1,HelloImpl2,然后在加载服务时具体的指定某个组件作为实现。话不多说,直接开码。

1、准备环境

1.1 pom文件导入以下依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>
<!--日志-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.28</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.28</version>
</dependency>
<!--yaml读取-->
<dependency>
    <groupId>org.jyaml</groupId>
    <artifactId>jyaml</artifactId>
    <version>1.3</version>
</dependency>

1.2 resources目录结构如下

准备日志配置,SPI配置,业务配置
在这里插入图片描述

  1. 日志配置log4j.properties
# 日记级别(单个级别) 文件/控制台
log4j.rootLogger=debug, stdout,file

# Redirect log messages to console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

# Rirect log messages to a log file
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=test.log
log4j.appender.file.MaxFileSize=5MB
log4j.appender.file.MaxBackupIndex=10
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
  1. SPI接口文件com.freesky.spi.myspi.IHello
com.freesky.spi.myspi.HelloImpl1
com.freesky.spi.myspi.HelloImpl2
  1. 配置文件myapplication.yml
hello:
  # 指定实现
  impl: com.freesky.spi.myspi.HelloImpl2

2、准备服务规范和实现组件

2.1 创建服务规范接口

public interface IHello {
    void hello();
}

2.2 准备两个实现组件

public class HelloImpl1 implements IHello {
    @Override
    public void hello() {
        System.out.println("妖怪!出来,我是你孙爷爷!");
    }
}
public class HelloImpl2 implements IHello {
    @Override
    public void hello() {
        System.out.println("大师兄不好了,师傅和二师兄被妖怪抓走了");
    }
}

2.3 yaml读取工具类

用来读取yaml配置文件内容,获取IHello指定的实现方式

import org.ho.yaml.Yaml;
import java.io.InputStream;
import java.util.Map;
import java.util.Objects;

/**
 * @author 陈玉林
 * @projectName java-basic
 * @className YamlUtil
 * @description TODO
 * @date 2019/12/12 21:27
 */
public class YamlUtil {
    private YamlUtil() {}
    private static final String YAML_SPLIT_SYMBOL = "\\.";

    @SuppressWarnings("unchecked")
    public static Map<String, String> getYamlMap(String ymalResourceName) {
        InputStream resourceAsStream = YamlUtil.class.getClassLoader().getResourceAsStream(ymalResourceName);
        return (Map<String, String>) Yaml.load(resourceAsStream);
    }
    public static String getYamlValue(Map map, String property) {
        Objects.requireNonNull(property, "属性不可为空");
        String[] propertyKeys = property.split(YAML_SPLIT_SYMBOL);
        for (String next : propertyKeys) {
            Object o = map.get(next);
            if (o instanceof Map) {
                map = (Map) o;
            } else {
                return String.valueOf(map.get(next));
            }
        }
        return "";
    }
}

2.4 实现服务加载器MyServiceLoader

自己实现一个拥有缓存功能的ServiceLoader

import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.URL;
import java.util.*;

@Slf4j
public class MyServiceLoader<S> {

    /**
     * 配置文件的路径
     */
    private static final String PREFIX = "META-INF/myservices/";
    /**
     * 被加载的服务类或接口
     */
    private final Class<S> service;
    /**
     * 缓存已加载的服务类集合
     */
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
    /**
     * 类加载器
     */
    private final ClassLoader loader;

    private MyServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "服务接口不可为空");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        reload();
    }

    private void reload() {
        providers.clear();
        String fullName = PREFIX + service.getName();
        try {
            Enumeration<URL> resources = loader.getResources(fullName);
            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                String filename = url.getFile();
                List<String> implNames = readLines(new File(filename));
                Set<String> noRepeatImplNames = getNoRepeatImplNames(implNames);
                log.info("实现类名称{}", Arrays.toString(noRepeatImplNames.toArray()));
                if (!noRepeatImplNames.isEmpty()) {
                    noRepeatImplNames.forEach(name -> {
                        S result = getInstance(name);
                        if (Objects.nonNull(result)) {
                            providers.put(name, result);
                        }
                    });
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * @description 服务文件内容读取 
     * @status 使用中
     * @param 
     * @author 陈玉林
     * @date 2019/12/13 20:34
     */
    @SuppressWarnings("unchecked")
    private List<String> readLines(File file) throws IOException {
        if (file.exists()) {
            if (file.isDirectory()) {
                throw new IOException("文件 '" + file + "'是文件夹");
            } else if (!file.canRead()) {
                throw new IOException("文件 '" + file + "' 不可读");
            } else {
                FileInputStream input = new FileInputStream(file);
                InputStreamReader reader = new InputStreamReader(input);
                BufferedReader bufferedReader = new BufferedReader(reader);
                List<String> list = new ArrayList();

                for(String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) {
                    list.add(line);
                }
                return list;
            }
        } else {
            throw new FileNotFoundException("文件"+file+"不存在");
        }
    }

    /**
     * @description 反射获取实现类对象 
     * @status 使用中
     * @param 
     * @author 陈玉林
     * @date 2019/12/13 20:34
     */
    private S getInstance(String name) {
        try {
            Class<?> aClass = Class.forName(name, false, loader);
            Object instance = aClass.newInstance();
            return service.cast(instance);
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @param
     * @description 去重
     * @status 使用中
     * @author 陈玉林
     * @date 2019/12/13 17:28
     */
    private Set<String> getNoRepeatImplNames(List<String> implNames) {
        return new HashSet<>(implNames);
    }

    private List<S> getServices() {
        List<S> list = new ArrayList<>();
        Set<Map.Entry<String, S>> entries = providers.entrySet();
        entries.forEach(i -> {
            list.add(i.getValue());
        });
        return list;
    }


    public static <S> List<S> load(Class<S> s, ClassLoader cl) {
        return new MyServiceLoader<>(s, cl).getServices();
    }

}

3 测试

一切准备就绪,可以测试了

3.1 使用HelloImpl2作为实现

import com.freesky.common.YamlUtil;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        final String targetImpl = YamlUtil.getYamlValue(YamlUtil.getYamlMap("myapplication.yml"), "hello.impl");
        List<IHello> services = MyServiceLoader.load(IHello.class, MyServiceLoader.class.getClassLoader());

        services.forEach(item -> {
            //模拟调用场景,进行配置文件配置实现类的服务调用
            if (item.getClass().getName().equals(targetImpl)) {
                item.hello();
            }
        });
    }
}

输出:

2019-12-13 20:42:43 INFO  MyServiceLoader:44 - 实现类名称[com.freesky.spi.myspi.HelloImpl2, com.freesky.spi.myspi.HelloImpl1]
大师兄不好了,师傅和二师兄被妖怪抓走了

3.2 使用HelloImpl1作为实现

修改yml配置文件为以下内容

hello:
  # 指定实现
  impl: com.freesky.spi.myspi.HelloImpl1

输出:

2019-12-13 20:42:18 INFO  MyServiceLoader:44 - 实现类名称[com.freesky.spi.myspi.HelloImpl2, com.freesky.spi.myspi.HelloImpl1]
妖怪!出来,我是你孙爷爷!
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值