解耦与模块化:鸿蒙平台上的服务注册与查找机制

295 阅读6分钟

背景

服务注册和查找机制是一种常见的设计模式,它允许我们在运行时动态地加载和使用服务。本文的背景是为了在鸿蒙上实现我们Android版本中的服务注册和查找机制。在Android中我们利用服务注册和查找机制,将基座和第三方SDK的开发分离开来,用户可以按照我们给定的框架,自由开发他们的SDK,再和基座一起打包成APK。基座则基于服务框架,调用第三方SDK的能力。通过这种方式,将基座开发和第三方SDK的开发完全解耦。

一、Java中的服务注册和查找

1.1 原理

在Java中,ServiceLoader类提供了一种服务提供者框架,它允许模块化应用程序在运行时动态加载、查找和使用服务提供者。

ServiceLoader是Java的一种服务提供者加载设施,它遵循了服务提供者框架模式(Service Provider Framework Pattern)。这种模式包含三个组件:服务接口(Service Interface)、提供者注册API(Provider Registration API)、服务访问API(Service Access API)。在Java中,ServiceLoader类就是提供者注册API和服务访问API的实现。

ServiceLoader的工作原理主要基于Java的类路径(Classpath)搜索和META-INF/services目录。当使用ServiceLoader.load(Class<S> service)方法时,ServiceLoader会搜索类路径下所有META-INF/services/目录中名为服务接口全限定名的文件。这个文件是一个简单的文本文件,其中每一行都是一个服务提供者类的全限定名。ServiceLoader会读取这个文件,然后使用类加载器(ClassLoader)加载并实例化这些服务提供者类。

这种机制允许服务提供者在运行时被发现和加载,而无需在编译时进行硬编码,从而提供了很好的模块化和解耦。

1.2 例子

以下是一个简单的例子:

  1. 定义一个服务接口:
public interface IService {
    void doSomething();
}
  1. 实现这个接口:
public class MyService implements IService {
    public void doSomething() {
        System.out.println("Doing something...");
    }
}
  1. META-INF/services/目录下创建一个名为com.example.IService的文件(全限定名),文件内容是MyService的全限定名:
com.example.MyService
  1. 使用ServiceLoader加载和使用服务:
ServiceLoader<IService> services = ServiceLoader.load(IService.class);
for (IService service : services) {
    service.doSomething();
}

在这个例子中,当我们运行上述代码时,ServiceLoader会自动找到并加载MyService,然后调用其doSomething方法。

二、鸿蒙中使用TypeScript实现的服务注册和查找

2.1 使用反射和配置文件模拟Java中的SeviceLoader

2.1.1 SDK侧

  1. 定义一个服务接口:
export interface IService {
    doSomething(): void;
}
  1. 实现这个接口:
export class MyService implements IService {
    doSomething(): void {
        console.log("Doing something...");
    }
}
  1. 定义一个服务注册接口:
export interface IServiceRegister {
    registerService(): Array<IService>;
}
  1. 实现服务注册接口:
export class MyServiceRegister implements IServiceRegister {
    registerService(): Array<IService> {
        // 返回一个IService的实例数组
        return [new MyService(), new MyService()];
    }
}
  1. lib_third_libraryIndex.ets中暴露接口和服务注册实现类:
export { IService, IServiceRegister } from './src/main/ets/third_library/MyServiceRegister'
export { MyServiceRegister } from './src/main/ets/third_library/MyServiceRegister'

2.1.2 基座侧

  1. 基座调用方:
interface ClassData {
    className: string[];
}

import('lib_third_library').then((ns: any) => {
    let fileName = 'meta.json';
    let res = $rawfile(fileName);
    // 读取文件内容并转换为字符串
    let jsonString = bytes2Str(ResUtil.getResManagerByResource(res).getRawFileContentSync(fileName));
    // 将字符串解析为 JSON 对象,并将其类型断言为 ClassData
    let classData = JSON.parse(jsonString) as ClassData;
    classData.className.forEach((className) => {
        console.log(className);
        let registerServiceFunctionName = 'registerService';
        // 使用类名实例化类
        let myServiceRegisterImpl: any = new ns[className]();
        // 调用 registerService 方法并获取返回的列表,列表中的每个元素都是 IService 类型
        let serviceList: IService[] = myServiceRegisterImpl[registerServiceFunctionName]();
        serviceList.forEach((service: IService) => {
            // 运行服务函数
            service.doSomething();
        });
    });
});
  1. meta.json文件:
{
  "className":["MyServiceRegister"]
}

在这个例子中,我们首先创建了一个 MyServiceRegister 来注册所有的服务。然后,我们从 MyServiceRegister 中获取 MyService,并调用其 doSomething 方法。

2.1.3 类结构图

总结一下上文的类设计:

classDiagram
    class IService {
        <<interface>>
        +doSomething(): void
    }
    class MyService {
        +doSomething(): void
    }
    class IServiceRegister {
        <<interface>>
        +registerService(): Array<IService>
    }
    class MyServiceRegister {
        +registerService(): Array<IService>
    }
    IService <|-- MyService
    IServiceRegister <|-- MyServiceRegister
    MyServiceRegister --> MyService: creates

IService 是一个接口,它定义了一个 doSomething 方法。MyServiceIService 的一个实现。

IServiceRegister 是一个接口,它定义了一个 registerService 方法,该方法返回一个 IService 的数组。MyServiceRegisterIServiceRegister 的一个实现,它创建了 MyService 的实例。

2.2 关键技术点

1.  meta.json是为了模拟java的META-INF/services/目录,需要SDK实现方写好实现类的名字:

{
  "className":["MyServiceRegister"]
}

2.  通过反射,调用第三方SDK中的服务注册类的注册函数,模拟java中的SeviceLoader.load。

  • registerService 注册服务的函数名字固定不变
  • registerService返回的是service的数组
let registerServiceFunctionName = 'registerService';
// 使用类名实例化类
let myServiceRegisterImpl: any = new ns[className]();
// 调用 registerService 方法并获取返回的列表,列表中的每个元素都是 IService 类型
let serviceList: IService[] = myServiceRegisterImpl[registerServiceFunctionName]();

三、优缺点分析

Java和TypeScript中的服务注册和查找机制都有其各自的优缺点:

标准化易用性延迟加载服务生命周期管理解耦支持类型检查灵活度
Java是,Java的ServiceLoader是Java平台的一部分,所有的Java开发者都可以使用它,无需额外的库或框架。需要在META-INF/services/目录下创建配置文件,并且需要手动输入服务提供者的全限定名,这可能会导致错误。是,ServiceLoader支持延迟加载,只有在真正需要服务提供者时,才会加载和实例化它们。无法管理服务提供者的生命周期,例如,无法控制服务提供者的创建和销毁。是,通过使用ServiceLoader,服务的使用者和提供者可以完全解耦,它们只需要知道服务接口,而无需知道具体的实现。不支持,Java在编译时并不会检查服务的类型,可能会在运行时出现类型错误。较低,Java的ServiceLoader机制相对固定,不易进行定制。
TypeScript否,TypeScript没有标准的服务注册和查找机制,不同的项目可能会有不同的实现。需要实现接口并注册服务,创建一份自定义的配置文件。否,TypeScript的服务注册和查找机制无法实现服务的延迟加载。TypeScript的服务注册和查找机制通常无法管理服务的生命周期,除非在服务注册表中添加额外的逻辑。是,通过使用服务注册表,服务的使用者和提供者可以完全解耦,它们只需要知道服务接口,而无需知道具体的实现。是,由于TypeScript支持静态类型检查,因此可以在编译时检查服务的类型,避免了运行时类型错误。较高,TypeScript的服务注册和查找机制更加灵活,可以根据需要自定义服务注册表。

Java和TypeScript中的服务注册和查找机制各有优缺点,选择哪种机制取决于具体的需求和场景。

四、结论

Java的ServiceLoader提供了一种动态的、松耦合的服务加载机制,适合构建模块化的、可扩展的应用程序。本文在鸿蒙上模拟Java的ServiceLoader的机制,给出了一种可以在大型工程中解耦第三方SDK的鸿蒙实现,希望能给读者一些思路启发。