从Java SPI机制实现到Spring Boot SPI扩展

Java SPI实现

可参考上一篇文章:
https://blog.csdn.net/shang_xs/article/details/86560469

Spring Boot SPI实现

1. SPI特点

就是可以选中不同的实现来执行具体的代码,定义SPI接口,以及SPI的使用姿势(前提) 一个生成代理类的FactoryBean (核心)

public interface ISpi<T> {
boolean verify(T condition);
}

看到上面的实现之后,就会有一个疑问,如果有多个子类都满足这个条件怎么办?因此可以加一个排序的接口,返回优先级最高的匹配者

public interface ISpi<T> {
 boolean verify(T condition);
/**
 * 排序,数字越小,优先级越高
 * @return
 */
default int order() {
    return 10;
}

接口定义之后,使用者应该怎么用呢?

2. 使用约束 spi实现的约束

基于JDK的代理模式,一个最大的前提就是,只能根据接口来生成代理类,因此在使用SPI的时候,我们希望使用者先定义一个接口来继承ISpi,然后具体的SPI实现这个接口即可

其次就是在Spring的生态下,要求所有的SPI实现都是Bean,需要自动扫描或者配置注解方式声明,否者代理类就不太好获取所有的SPI实现了

3. spi使用的约束

在使用SPI接口时,通过接口的方式来引入,因为我们实际注入的会是代理类,因此不要写具体的实现类

4. 获取所有的SPI实现类

(org.springframework.beans.factory.ListableBeanFactory#getBeansOfType(java.lang.Class)) 通过jdk生成代理类,代理类中,遍历所有的SPI实现,根据传入的第一个参数作为条件进行匹配,找出首个命中的SPI实现类,执行 将上面的步骤具体实现,也就比较简单了

public class SpiFactoryBean<T> implements FactoryBean<T> {
private Class<? extends ISpi> spiClz;

private List<ISpi> list;

public SpiFactoryBean(ApplicationContext applicationContext, Class<? extends ISpi> clz) {
    this.spiClz = clz;

    Map<String, ? extends ISpi> map = applicationContext.getBeansOfType(spiClz);
    list = new ArrayList<>(map.values());
    list.sort(Comparator.comparingInt(ISpi::order));
}

@Override
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
    // jdk动态代理类生成
    InvocationHandler invocationHandler = new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            for (ISpi spi : list) {
                if (spi.verify(args[0])) {
                    // 第一个参数作为条件选择
                    return method.invoke(spi, args);
                }
            }

            throw new NoSpiChooseException("no spi server can execute! spiList: " + list);
        }
    };

    return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{spiClz},
            invocationHandler);
}

@Override
public Class<?> getObjectType() {
    return spiClz;
}

实例演示 话说方案设计之后,应该就是实现了,然而因为实现过于简单,设计的过程中,也就顺手写了,就是上面的一个接口定义 ISpi 和一个用来生成动态代理类的SpiFactoryBean
接下来写一个简单的实例用于功能演示,定义一个IPrint用于文本输出,并给两个实现,一个控制台输出,一个日志输出

public interface IPrint extends ISpi<Integer> {

default void execute(Integer level, Object... msg) {
    print(msg.length > 0 ? (String) msg[0] : null);
}

void print(String msg);
具体的实现类如下,外部使用者通过execute方法实现调用,其中level<=0时选择控制台输出;否则选则日志文件方式输出

@Component public class ConsolePrint implements IPrint { @Override public void print(String msg) { System.out.println("console print: " + msg); }

@Override
public boolean verify(Integer condition) {
    return condition <= 0;
}
}

@Slf4j @Component public class LogPrint implements IPrint { @Override public void print(String msg) { log.info("log print: {}", msg); }

@Override
public boolean verify(Integer condition) {
    return condition > 0;
}

前面的步骤和一般的写法没有什么区别,使用的姿势又是怎样的呢?

@SpringBootApplication public class Application {

public Application(IPrint printProxy) {
    printProxy.execute(10, " log print ");
    printProxy.execute(0, " console print ");
}

public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
}

看上面的Application的构造方法,要求传入一个IPrint参数,Spring会从容器中找到一个bean作为参数传入,而这个bean就是我们生成的代理类,这样才可以根据不同的参数来选中具体的实现类

所以问题就是如何声明这个代理类了,配置如下,通过FactoryBean的方式来声明Bean,并添加上@Primary注解,这样就可以确保注入的是我们声明的代理类了

@Configuration public class PrintAutoConfig {
@Bean
public SpiFactoryBean printSpiPoxy(ApplicationContext applicationContext) {
    return new SpiFactoryBean(applicationContext, IPrint.class);
}

@Bean
@Primary
public IPrint printProxy(SpiFactoryBean spiFactoryBean) throws Exception {
    return (IPrint) spiFactoryBean.getObject();
}



@SpringBootApplication
public class SpringBootApplication {
public SpringBootApplication (Iprint pringProxy){
    pringProxy.execute(10,"log.print");
    pringProxy.execute(1,"console.print");
   }

public static void main(String[] args) {
    SpringApplication.run(SpringBootApplication.class, args);
  }
}

具体实现参见:https://github.com/dwyanewede/project-learn

扩展点加载:com.learn.demo.spi.ISpi

展开阅读全文

没有更多推荐了,返回首页