Java SPI 技术在Android上的应用与原理分析

一、前言

今年初,我在做需求时一位好友告诉我,有一种非常高明的技术方案可以将现有项目中的代码变的更加精简、漂亮,它的名字叫SPI,问我想不想搞。由于我当时忙着搞一堆紧急需求,实在没心思搞新技术,于是含糊的婉拒了。现在回想起这事儿,有点后悔,若当时挤出时间搞搞,说不定我后来再要工资时还能再多加点,哈哈哈哈哈。

二、SPI 技术原理剖析

2.1 传统面向接口编程的局限性

在正式讲SPI 编程前,先用一个接口编程举个例子作为话题的引入。
现在出一个关于播放音乐的业务接口:

Business.kt

interface Business {
    
    
    fun play()
}
  • A 渠道要求每次播放前先弹一个Toast。
  • B 渠道要求每次播放前先弹一个对话框。
    接下来我们按照传统的面向接口形式开发去实现上述业务。
A.kt

class A :Business{
    
    
    override fun play() {
    
    
        // 显示Toast
    }
}
B.kt

class B :Business {
    
    
    override fun play() {
    
    
        // 显示弹窗
    }
}
class Core {
    
    
    
    lateinit var mAction:Business
	
    /**
     * 2、注册接口实现
     */
    fun registerBusiness(action : Business){
    
    
        mAction = action
    }

    /**
     * 3、执行播放操作
     */
    fun doPlay(){
    
    
        mAction.play()
    }
}
User.kt

class User {
    
    

    init {
    
    
        // 1、指明一个具体实现。
        Core().registerBusiness(A())
    }
}

用户在播放时显示Toast,直接在使用时注册A对象即可。
上面的写法也是绝大多数面向接口编程的操作(基本都是这个操作流程)。但是这样的写法流程是否真的很完美?在我看来有如下问题:

  1. 实现模块注册的时候需要具体指明,如果要用户想要换成B实现,需要手动修改代码,注册方法里变成B()。
  2. 注册对象单一,如果一个接口有多个实现,上述的mAction不支持,需要用数组包装,不够智能。
  3. 注册流程较为繁琐,不够精简。

上面列的问题,最核心的就是第一条,即每次要换个业务的具体实现就要改代码!违反了软件设计原则中的”可插拔原则“。

那么有什么办法可以解决这个问题 ?

2.2 SPI 技术

2.2.1 SPI技术简介

SPI的全名为Service Provider Interface.java spi,机制的思想: 系统里抽象的各个模块,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦
有了SPI标准,SUN公司只需要提供一个播放接口,在实现播放的功能上通过ServiceLoad的方式加载服务,那么第三方只需要实现这个播放接口,再按SPI标准的约定进行打包,再放到classpath下面就OK了,没有一点代码的侵入性。

2.2.2 SPI的约定

java spi的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

2.2.3 补充

前面说的这个约定操作是不是很眼熟,没错,它和自定义Gradle插件非常相似!只是META-INF后面的文件名和SPI定义的不同。
在这里插入图片描述
上图是自定义Gradle插件的资源声明。
在这里插入图片描述
上图是SPI文件的声明。

2.3 使用SPI技术改造

接口的声明和接口的实现部分,和之前写的一样。
接下来主要有两步:

  • 创建业务接口SPI文件,并声明要使用的业务接口实现类的类路径信息。
    在这里插入图片描述
    还是这张图,这里的"com.coffer.test1.Business",对应的就是接口Business的路径。在这个文件里,将所需要使用的业务实现接口实现类的路径写上。这个时候你再点击图中"coffer.test1.A",会自动跳转A类文件中。
  • 加载业务实现类,并调用接口方法,具体实现如下:
Core.kt

class Core {
    
    
	// 1、使用ServiceLoader加载Business接口的实现类,并实例化对象,存储到内部的HashMap中
    val businessService: ServiceLoader<Business> = ServiceLoader.load(Business::class.java)

    fun play() {
    
    
        // 2、遍历map里的实现类,调用实现类的接口方法
        val business: Iterator<Business> = businessService.iterator()
        while (business.hasNext()) {
    
    
            business.next().play()
        }
    }
}

对比之前的写法,是不是发现精简了很多。总结下:

  • 使用SPI配置文件,取代手动业务注册。想要使用哪个业务实现类,直接修改SPI文件里的声明接口,不需要写代码,这个文件设置可以后台动态下发。
  • 使用ServiceLoader加载SPI文件,并自动创建该业务接口所配置实现的对象(实例化),同时也支持多个接口的实现,不需要手动去创建HashMap去管理接口实现。
    关于ServiceLoader的原理,我在下一小结分析。

2.4 SPI技术原理分析

SPI 的核心就是ServiceLoader.java。接下来就带着各位分析这个类的实现原理。
Android SDK下的ServiceLoader.java,和JDK下的ServiceLoader.java实现上会有有点点不同。我下面分析ServiceLoader.java是Android-28里的。

扫描二维码关注公众号,回复: 14773203 查看本文章
ServiceLoader.java

public final class ServiceLoader<S>
    implements Iterable<S>
{
    
    	
	// 1
	private static final String PREFIX = "META-INF/services/";
    ....
    // 2
    private final ClassLoader loader;
    // 3
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    ....
  1. 指向的就是我们创建SPI所在的文件夹的位置。
  2. 这里的loader 是指当前类所在的父ClassLoader,注意 : 这里我格外强调它是因为在Android平台,有个插件化技术,可以自定义ClassLoader,一旦ClassLoader找错了,这里必崩!一定要注意!!!
  3. providers是一个LinkedHashMap,其中key就是SPI文件里接口实现类的路径字符串,S就是接口实现类的实例化对象。
 public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
.....

构造方法的调用链较长,且逻辑不复杂,这里我说下流程,不展示源码。

  1. 给前面提到的成员变量赋值,例如loader。并对变量的有效性校验。
  2. 重置成员变量的状态,例如providers.clear()。
    总结一句话,load方法并没有真正的load数据,只是做了load的准备操作。
    那真正load数据是什么时候 ?
		// 1
		val business: Iterator<Business> = businessService.iterator()
        // 2
        while (business.hasNext()) {
    
    
            business.next().play()
        }

关于迭代器Iterator,ServiceLoader里自己实现了一个LazyIterator,并重写了里面的方法。
加载解析逻辑较为复杂,这里我分三个部分来解析

public Iterator<S> iterator() {
    
    
        return new Iterator<S>() {
    
    
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            public boolean hasNext() {
    
    
                if (knownProviders.hasNext())
                    return true;
                // 1
                return lookupIterator.hasNext();
            }
				// 
            public S next() {
    
    
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
			.....
        };
    }

public boolean hasNext() {
    
    
	return hasNextService();
}

private boolean hasNextService() {
    
    
            if (nextName != null) {
    
    
                return true;
            }
            if (configs == null) {
    
    
                try {
    
    
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
    
    
					......
                }
            }
            while ((pending == null) || !pending.hasNext()) {
    
    
                if (!configs.hasMoreElements()) {
    
    
                    return false;
                }
                // 2
                pending = parse(service, configs.nextElement());
            }
            // 3
            nextName = pending.next();
            return true;
        }
  1. 首次加载时,providers里没有数据,因此执行下面的hasNext,hasNext会中转执行hasNextService。
  2. 这是去读SPI文件里的信息,主要是将文件里的接口实现类的地址字符串放到Iterator中。
  3. 获取一个接口实现类的地址字符串,方便后面使用。
    接下来业务调用方法看
business.next().play()

里面的具体实现如下:

public S next() {
    
    
    return nextService();        
}

private S nextService() {
    
    
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
    
    
                // 1
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
    
    
                ...
            }
            ......
            try {
    
    
                // 2
                S p = service.cast(c.newInstance());
                // 3
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
    
    
                ....
            }
            ....
        }

始化接口实例化,并将实例化的对象放入providers 中,进而可以调用实现的接口方法。
总结:
ServiceLoader主要做的事情就是读取SPI文件里的接口实现类信息,然后通过反射创建接口实现类的对象,并存储到LinkedHash中方便后续使用。

三、Android 使用SPI技术

既然SPI 技术如此好用,为什么在Android领域很少看到?

3.1 原生SPI 技术在Android 上的局限性

  • 易造成资源冲突;Java中的SPI是随jar包发布的,每个不同的jar都包含一堆SPI的配置信息,而Android应用在构建的时候最终会将项目中所有的jar包进行合并,这会导致相同的SPI会产生资源冲突。
  • 影响性能;根据前面关于SPI 原理的分析,SPI 是通过ClassLoader在运行时从jar包中读取,由于apk是签名的,在从jar中读取的时候,签名校验的耗时问题严重影响应用的启动速度。同时ServiceLoader每次加载都有读文件的操作,以及使用反射创建对象,这些对性能的影响不能忽视。
  • 配置繁琐;每个接口都要手动创建一个SPI文件,并且还要手动去写文件的内容。如果项目中的接口数量非常多,这个配置SPI文件的工作量将会非常的大!

3.2 自定义Android SPI

由于原生SPI 有很大的局限性,因此,只要我们理解了ServiceLoader的工作原理,我们自己实现一个ServiceLoader,然后逐个攻破原生的局限性。

3.2.1 设计思路

  1. 为了解决资源冲突问题,可以将SPI文件不放在META-INF中,而是放在一个我们自定义的文件夹中,为此需要自定义ServiceLoader,修改文件解析方法。
  2. 由于第一个问题和第二个问题是关联性的,因此第二个问题也很好解决,同时可以直接new对象,无需反射。
  3. 关于配置繁琐问题,我们可以自定义Gradle插件,在编译编译APK时,通过扫描所有的class文件,生成对应的SPI文件,并将该文件写入我们自定义的文件夹中。
  4. 还是关于第三个问题优化,为了方便控制哪些接口需要写SPI,哪些接口不写SPI,可以给需要声明SPI接口的类添加一个自定义的注解。在编译扫描class时,只要该类上有我们声明的注解标签,就为该接口创建一个实现类。
  5. 原生的ServiceLoader比较功能比较臃肿。它将SPI文件解析、注册、存储都集结与一身,可以优化下,将文件解析、注册放入单独的一个类去做,这里建议放在自定义Gradle插件中去自动生成实现。

3.2.2 功能实现

根据前面的分析,我们需要设计如下几个部分:

  1. 自定义ServiceLoader。
  2. 自定义Gradle插件去写SPI文件,同时还需要在编译期创建一个注册类将SPI文件的接口和实现类进行绑定。这里涉及到JavaCompile、JavaFile相关的知识。
  3. 自定义注解,以及注解解析。
    上面三个部分,主要是第二部分最为复杂,自定义注解的解析也是放在自定义Gradle插件中去做的。因此接下来代码实现,我将不会把完整的代码贴出,而是伪代码和部分实现代码相结合。各位主要搞懂实现原理,这个很重要。

【关于完整的Demo,这里先挖个坑,后续有时间把Demo代码完善后再提交】

3.2.2.1 自定义Gradle插件

所要做的事情:

  • 创建SPI文件。
  • 创建SPI注册描述类。
  • 声明注解、解析注解

先声明一个自定义注解:

CofferTag.java

// 这里的注解是精简版,可以根据自己的需求加很多属性
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CofferTag {
    
    
    // 需要生成的SPI接口类
    Class<?>[] value();
}
CofferPlugin.java

public class CofferPlugin  implements Plugin<Project> {
    
    
    @Override
    public void apply(Project target) {
    
    
        // 1、创建一个SPI文件夹
        File spiFile;
        // 2、创建一个SPI信息注册管理类,这里可以新开一个Task去实现
        CofferReisterTask task;
        // 3、创建一个编译Java的文件对象,将刚刚生成的Java文件进行编译
        JavaCompile compile;
        // 4、设置任务依赖关系和执行顺序,必须是先生成Java文件,然后才能对文件进行编译
        ......
    }
}
CofferReisterTask.java

public class CofferTask extends DefaultTask {
    
    
    
    @TaskAction
    protected void generateCofferCode(){
    
    
        // 1、根据文件路径,加载工程中所有的类,这里会涉及到读文件的过程
        List<CtClass> classes = loadClass();
        // 2、遍历这个集合,过滤并生成SPI文件
        process();
        // 3、生成SPI 信息注册实现类CofferReister.java
        generateCode();
    }
}

private void process(){
    
    
	// 1、判断这个类是否包含自定义注解,如果不包含,直接return
    // 2、获取注解属性的值
    // 3、根据注解的值、前面传入的生成SPI文件夹的信息,生成当前接口对应的SPI文件
}

// 这个方法除了要生成CofferReister.java
// CofferReister.java索要做的事情就是遍历SPI文件的接口信息,然后行注册,
// 将接口类和接口实现类使用map进行关联存储。
private void generateCode(){
    
    
	// 使用JPT(JavaPoet)技术生成CofferReister.java,这个东西其实也不是很难,我举个例子
    // 比如要import java.util.map;
    ClassName.get("java.util","map");
    // 如果要新增一个get方法
    TypeSpec.classBuilder("CofferRegister")
    	.addMethod(...)
   // 这个方法的复杂程度,取决于你要生成的类的复杂程度
   // 最后记得把Java文件写入自定义的文件夹
   javaFile.builder(...).write(...);
}

3.2.2.1 自定义ServiceLoader

由于将SPI文件的解析、接口的对象的创建放到了我们在编译时生成的CofferReister.java文件,因此我们自定义的ServiceLoader将会变的非常精简。

public final class ServiceLoader<S>
    implements Iterable<S>
{
    
    
 // 将原先关于SPI文件解析、定义Iterator统统不要,这些都在CofferReister.java处理了
 // 我们只需要调用初始化方法即可。
 // 
 }

3.2.2.3 效果

接下来还是用我们前面写的demo进行改造

@CofferTag(Business::class)
class A :Business{
    
    
    override fun play() {
    
    
        // 显示Toast
    }
}

@CofferTag(Business::class)
class B :Business{
    
    
    override fun play() {
    
    
        // 显示弹窗
    }
}

在这里插入图片描述
我们在引入自定义的Gradle插件,编译后就会在上述目录中生成如上内容。

class Core {
    
    

    val businessService: ServiceLoader<Business> = ServiceLoader.load(Business::class.java)

    fun play() {
    
    
        val business: Iterator<Business> = businessService.iterator()
        while (business.hasNext()) {
    
    
            business.next().play()
        }
    }
}

调用逻辑还是不变。
如果该实现类不想要了,直接去掉实现类上的注解去掉。如果想要新增,直接加上注解即可,其他的什么都不用管。

四、总结

很多好的东西,虽有瑕疵,但只要动脑筋改造,就会变的完美。

五、参考资料

Java SPI 机制.

猜你喜欢

转载自blog.csdn.net/qq_26439323/article/details/121363030
今日推荐