模仿JDK实现SPI服务

此文章不是介绍SPI服务的概念,是基于对SPI有一定了解,想深入研究其实现原理的文章。本文介绍了SPI实现的整个过程,并结合配置文件的配置模拟实际业务场景中服务的调用。话不多说,直接开码。

1、准备环境

1.1 安装lombok插件,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目录结构如下

在这里插入图片描述

  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.2 yaml读取工具类

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.3 实现服务加载器MyServiceLoader

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();
    }

}

2.4 测试

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]
大师兄不好了,师傅和二师兄被妖怪抓走了

修改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]
妖怪!出来,我是你孙爷爷!
posted @ 2019-12-13 20:45  _chenyl  阅读(62)  评论(0编辑  收藏  举报