Skip to main content

后端服务

后端服务提供可供所有后端插件和模块使用的共享功能。 它们通过服务引用提供,服务引用嵌入了代表服务接口的类型,类似于实用 API要在插件或模块中使用某项服务,需要使用服务引用来请求该服务的实现。

服务系统的存在是为了在服务接口与其实现之间提供一定程度的间接性。 它是依赖注入的一种实现方式,其中每个后端实例都是依赖注入容器。 每个服务的实现都由服务工厂提供,该工厂封装了每个服务实例的创建逻辑。

服务接口

服务接口可以是任何 TypeScript 类型,但最好是具有多个方法的对象接口。 适用于接口设计的一般准则是:保持简单、精干,方法少但功能强大。 注意避免锁定单个方法的发展方式。 通常情况下,您希望坚持使用以选项对象作为唯一参数的方法,并返回结果对象。 如果有任何理由不确定方法是否应为异步,则一定要使其异步。 例如,最小接口通常应使用以下模式:

export interface FooService {
foo(options: FooOptions): Promise<FooResult>;
}

服务参考

一旦定义了服务接口,就需要使用createServiceRef函数。ServiceRef实例,它是您导出的引用,以便用户与您的服务进行交互。 从概念上讲,这与ApiRef例如

import { createServiceRef } from '@backstage/backend-plugin-api';

export interface FooService {
foo(options: FooOptions): Promise<FooResult>;
}

export const fooServiceRef = createServiceRef<FooService>({
id: 'example.foo', // the owner of this service is in this case the 'example' plugin
});

fooServiceRef应被导出,然后可用于声明对FooService接口,并在运行时接收其实现。

创建服务引用时,需要给它一个 ID。 该 ID 必须是全局唯一的,格式一般为'<pluginId>.<serviceName>'有关服务的更多命名模式,请参见命名模式page.

关于命名的说明:前端和后端系统有意将非常相似的概念分别命名为 "API "和 "服务"。 这是为了避免两者在文档和讨论中以及代码中混淆。 虽然两个系统非常相似,但并不完全相同,不能互换使用。

服务工厂

为了能够通过服务引用来依赖服务接口,我们当然还需要有某种方法来创建它的具体实现。 为了封装这一逻辑,我们使用了服务工厂,它既定义了服务实例的创建方式,也定义了它们的实现依赖于哪些其他服务。

服务工厂有多种来源,有内置的服务工厂,也有从其他软件包导入的外部工厂,还可以创建自己的工厂。 特定的服务工厂安装在每个后端实例中,作为依赖注入容器。 对于任何给定的后端实例,每个服务只能有一个指定的服务工厂。

要定义服务工厂,我们使用createServiceFactory:

import { createServiceFactory } from '@backstage/backend-plugin-api';

class DefaultFooService implements FooService {
async foo(options: FooOptions): Promise<FooResult> {
// ...
}
}

export const fooServiceFactory = createServiceFactory({
service: fooServiceRef,
deps: { bar: barServiceRef },
factory({ bar }) {
return new DefaultFooService(bar);
},
});

要创建服务工厂,我们需要提供对service工厂将为其创建实例,一个deps对象,其中列出了工厂依赖的其他服务,以及一个factory函数来创建服务实例。 后端系统将调用factory函数的对象,该对象包含在deps如果一项服务的实现不依赖于任何其他服务,则deps将作为一个空对象 ({})。factory函数必须返回一个实现服务接口的值。

如果需要异步创建服务实例,可以将factory例如

export const fooServiceFactory = createServiceFactory({
service: fooServiceRef,
deps: {},
async factory() {
const foo = new DefaultFooService();
await foo.init();
return foo;
},
});

请注意,不允许服务工厂之间存在循环依赖关系。 这将在运行时进行验证,如果后端实例检测到任何冲突,它将拒绝启动。 同样,如果服务工厂依赖于任何已注册服务工厂都不提供的服务,后端也将无法启动。

核心服务

后端系统提供了许多核心服务定义,这些定义既有助于实现后端的主要功能,也为日志记录、数据库访问、作业调度等常见问题提供了一系列实用程序。 这些核心服务将始终存在于使用以下功能创建的后端实例中createBackend如果需要,它们都可以用自定义实现来覆盖。

所有核心服务的服务引用都通过各自的coreServices对象,可从@backstage/backend-plugin-api例如,日志服务可通过coreServices.logger.

有关核心服务的更多信息以及如何使用这些服务,请参阅核心服务节。

服务范围

默认情况下,服务的作用域是单个插件,这意味着将为每个插件创建单独的服务实例。fooFactory的单独实例。DefaultFooService这样就可以为各个插件量身定制服务实现,同时也确保了插件之间一定程度的分离。

服务范围是在调用createServiceRef的定义,而插件作用域是默认的。 我们上面对fooServiceRef因此等价于

export const fooServiceRef = createServiceRef<FooService>({
scope: 'plugin',
id: 'example.foo',
});

只有两种可能的服务范围、'plugin''root'.

根范围服务

如果服务被定义为根作用域服务,工厂创建的实现将在所有插件和服务中共享。 根作用域服务工厂的另一个不同之处在于,无论是否有插件依赖于这些服务,它们始终是初始化的。 这使它们适用于实现后端范围内的关注点,而这些关注点并不特定于任何单个插件。

根作用域服务的使用有一个限制,即其实现只能依赖于其他根作用域服务。 而插件作用域服务则既可以依赖于根作用域服务,也可以依赖于插件作用域服务。 由于存在这一限制,定义根作用域服务的主要原因之一就是使其他根作用域服务可以依赖于它。

由于这些限制以及根作用域服务的特殊用例,它们往往比插件作用域服务更为罕见。 一般来说,除非您要实现上述两种用例中的任何一种,否则您应该更倾向于将服务定义为插件作用域。

有些服务由插件和根范围服务定义组成。rootLogger服务是根作用域服务,而logger服务是一个插件作用域服务。rootLogger服务提供主要的日志记录功能,而logger服务只是在rootLogger来添加特定于插件的标签。 这种划分的存在是为了让其他根作用域服务也能访问日志服务,但如果能避免这种划分,那就最好不过了。 如果最终要实现这种模式,根作用域服务的前缀应为root,这是为了鼓励使用插件范围服务。

插件元数据

插件作用域服务可以访问插件元数据服务,这是后端系统提供的一种特殊服务,无法覆盖。 插件元数据服务提供关于正在为其创建服务实例的插件的信息。 它本身是一个插件作用域服务,可以像其他服务一样通过coreServices.pluginMetadata参考文献

插件元数据服务是所有插件特定定制服务的基础。 例如,插件作用域日志记录器服务的默认实现使用插件元数据服务将插件 ID 作为字段附加到所有日志消息中:

export const loggerServiceFactory = createServiceFactory({
service: coreServices.logger,
deps: {
rootLogger: coreServices.rootLogger,
pluginMetadata: coreServices.pluginMetadata,
},
factory({ rootLogger, pluginMetadata }) {
return rootLogger.child({ plugin: pluginMetadata.getId() });
},
});

服务工厂的根本背景

某些服务可能会受益于在服务的所有实例中共享上下文。 当然,这只适用于插件作用域的服务,因为根作用域的服务永远只有一个实例。 例如,根上下文可用于共享用于数据库访问的公共连接池、用于开发的生成Secret或任何其他类型的共享设施。 请注意,您不应该使用它在生产中的插件之间共享状态,因为这违反了插件隔离规则.

作为服务工厂的一部分,根上下文是通过传递createRootContext选择:

export const fooServiceFactory = createServiceFactory({
service: fooServiceRef,
deps: { rootLogger: coreServices.rootLogger, bar: barServiceRef },
createRootContext({ rootLogger }) {
return new FooRootContext(rootLogger);
}
factory({ bar }, ctx) {
return ctx.forPlugin(bar)
},
});

无论createRootContext函数共享,并在每次调用factory这样,您就可以创建一个共享上下文,在创建每个插件实例时使用。 与factory功能,则createRootContext函数只会接收根作用域服务作为其依赖项,但就像factory功能,也可以async.

默认服务工厂

有很多服务都默认安装在任何标准的Backstage实例中。 你可以期待这些服务始终存在,而不需要采取任何额外的步骤来使它们可用。 对于从外部软件包导入的服务来说,情况未必如此,因为你的插件或模块的用户可能没有在他们的后端为该服务安装工厂。 为了避免要求你的插件集成商为你所依赖的服务安装服务工厂,可以为服务定义一个默认工厂。

作为服务引用的一部分,默认服务工厂是通过传递defaultFactory选项createServiceRef:

import {
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';

export const fooServiceRef = createServiceRef<FooService>({
id: 'example.foo',
defaultFactory: async service =>
createServiceFactory({
service,
deps: {},
factory() {
return new DefaultFooService();
},
}),
});

请注意,我们不使用fooServiceRef在创建服务工厂时,使用service参数,这是因为在默认工厂回调中尝试使用fooServiceRef直接引用会导致循环引用。

如果服务定义了默认工厂,则在后端没有为该服务注册明确工厂的情况下,将使用该工厂。 这样,服务用户就可以直接导入和使用服务,而不必担心服务是否已安装。 建议始终为任何要导出供其他插件或模块使用的服务定义默认工厂。

为服务定义默认工厂时,有可能会在运行时出现重复实现的情况。 这既适用于工厂中的任何共享根上下文,也适用于服务的特定插件实例。 这是因为软件包的依赖版本范围可能不会完全一致,从而导致同一软件包的重复安装。 这种情况既可能发生在使用同一服务的两个不同插件中,也可能发生在一个插件及其模块中。 如果您的服务会在这种情况下中断,则不应为其定义默认工厂,而应要求服务的用户在其后端实例中显式安装工厂。

维修工厂选项

注意:不鼓励使用这种模式,只有在必要时才使用。 如果可能,您最好通过静态配置来配置服务。

在声明服务工厂时,可以包含一个选项回调。 这样,在后端安装工厂时,就可以通过代码对其进行自定义。 例如,在没有任何选项的情况下,可以这样在后端安装一个显式工厂实例:

const backend = createBackend();

backend.add(fooServiceFactory());

请注意,我们将fooServiceFactory这是因为createServiceFactory总是返回一个创建实际服务工厂的工厂函数。 要为服务工厂添加选项,可以将传递给createServiceFactory的回调中接受所需的选项。 请注意,选项必须始终是可选的。 例如

export interface FooFactoryOptions {
transform: (foo: string) => string;
}

export const fooServiceFactory = createServiceFactory(
(options?: FooFactoryOptions) => ({
service: fooServiceRef,
factory() {
return new DefaultFooService(options?.transform);
},
}),
);

从外观上看,服务工厂和以前一样,只是现在我们可以在安装工厂时传递选项:

const backend = createBackend();

backend.add(fooServiceFactory({ transform: foo => foo.toUpperCase() }));