SPI机制

85 阅读2分钟

是什么

SPI(ServiceProviderInterFace)服务提供接口。是Java提供的服务加载机制。

解决了什么

可以在不修改源代码的情况下替换接口的实现类;可以根据不同的环境引入不同的jar包来替换接口实现。

怎么用

加载

加载类路径下(使用System.getProperty("java.class.path")可以看到类加载路径)的META-INF/services路径下的A文件,A文件名称为要提供服务的接口的全路径,文件的内容是该接口所有实现类的全路径。

使用

使用Serviceloder.load(X.class)方法加载实现类

Demo案例

image.png

  1. 创建文件夹META-INF/services
  2. services文件夹中创建接口映射文件spi.car.Pay spi.car.Pay文件(文件名为需要使用的接口的全路径);文件内容为实现类的全路径
spi.car.impl.OfflinePayment
spi.car.impl.OnlinePayment
  1. 创建接口和对应的实现类,对应这spi包下的内容

Pay.java

package spi.car;

public interface Pay {
    void payment();
}

OfflinePayment.java

package spi.car.impl;

import spi.car.Pay;

public class OfflinePayment implements Pay {
    @Override
    public void payment() {
        System.out.println("OfflinePayment");
    }
}

OnlinePayment.java

package spi.car.impl;

import spi.car.Pay;

public class OnlinePayment implements Pay {
    @Override
    public void payment() {
        System.out.println("OnlinePayment");
    }
}
  1. 测试

Test.java

package spi;

import spi.car.Pay;

import java.util.Iterator;
import java.util.ServiceLoader;

public class Test {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.class.path"));

        ServiceLoader<Pay> pays = ServiceLoader.load(Pay.class);
        Iterator<Pay> iterator = pays.iterator();

        while(iterator.hasNext()) {
            Pay pay = iterator.next();
            pay.payment();
        }
    }
}

实际案例

场景

项目A是集团内部使用的,里面的中间件使用的都是集团内部的,目前需要将该项目部署到外部环境,集团内部的中间件全部都不能使用需要替换成开源的中间件进行替换。

解决方案

方案优点缺点
新建一个新项目,通用代码不需要改动,修改受影响部分的代码。实现简单,修改受影响代码即可1.要对公共代码进行更新时很麻烦;2.维护多个项目成本高;
一个项目部署多个环境,使用IFELSE区分环境执行不同的实现代码。只有一套项目,可以进行统一更新管理违反开闭原则,新增环境就要修改代码风险高
使用SPI机制动态加载实现类根据环境加载不同的实现jar包替换实现类实现复杂

pom文件:

<profiles>
 <profile>
            <id>environment1</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>com.alibaba.work.demo</groupId>
                    <artifactId>environment1</artifactId>
                </dependency>
            </dependencies>
 </profile>

<profile>
            <id>environment2</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>com.alibaba.work.demo</groupId>
                    <artifactId>environment2</artifactId>
                </dependency>
            </dependencies>
 </profile>
<profiles>

SPI加载类ServiceLoaderContainer,用于保存类及其对应的类加载器:

public class ServiceLoaderContainer {

    private static Map<Class<?>, Object> container = new HashMap<Class<?>, Object>();

    @SuppressWarnings( {"unchecked", "unused"} )
    protected <T> T getService(Class<T> cls) {
        T obj = (T) container.get(cls);
        if (obj != null) {
            return obj;
        }
        synchronized (this) {
            // 并行场景下 当前线程上下文类加载器有可能为null
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            if (cl == null) {
                cl = this.getClass().getClassLoader();
            }
            ServiceLoader<T> loaders = ServiceLoader.load(cls, cl);
            for (T loader : loaders) {
                container.put(cls, loader);
                return loader;
            }
        }
        throw new RuntimeException(e.getMessage());
    }

}

使用

@service
public class MyRoleServiceImpl extends ServiceLoaderContainer  implments MyRoleService{
    protected RoleService getRoleService() {
        return getService(RoleService.class);
    }
    ...
}

RoleServiceImpl有两个不同的实现,分别在包environment1和environment2中,使用的时候只需要使用maven -P 指定编译打包时需要使用到的jar包依赖,在运行时通过ServiceLoaderContainer调用ServiceLoader<T> loaders = ServiceLoader.load(cls, cl);加载接口类RoleService的具体的类实现。