乔纳森·洛克
译者:王强
策划|丁小云
KivaKit 是一个开源的 Java 框架,用于在 Apache License 下实现微服务。KivaKit 需要 Java 11+ 虚拟机,但源代码兼容 Java 8 和 9 项目。KivaKit 由一组集成良好的迷你框架组成。每个迷你框架都有一致的设计和各自的重点。它可以单独使用,也可以与其他迷你框架配合使用。下图是这些框架的依赖关系网络的简单示意图。这是 KivaKit 非常直观的高级架构图:
每个微框架都解决了开发微服务时的一个常见问题,本文将简单介绍上图中的每个微框架以及如何使用它们。
消息传递
从上图可以看出,消息传递是 KivaKit 的核心。在构建具有可观察状态的组件时,消息传递非常有用,这在基于云的世界中是一项方便的功能。许多对象广播或监听状态消息,例如 alert、Problem、Warning 或 Trace。它们中的大多数都是转发器,监听来自其他对象的状态消息并将它们重新广播给下游感兴趣的监听器(监听器)。这与终端监听器形成了一个监听器链:
C->B->A
通常,链中的最后一个监听器是某种记录器,但链的末尾可以有多个监听器,可以是任何实现监听器的对象。在迷你框架中,状态消息由 ValidationIssues 类捕获,然后用于确定验证是否成功,如果验证失败,则向用户显示特定问题。给定上面的监听器链,C 和 B 实现中继器,最后由类型 A 的对象实现监听器。在链中的每个类中,监听器链扩展为:
listener.listenTo(broadcaster)
为了向感兴趣的听众传递消息,有一些从 Broadcaster 继承的针对常见消息类型的便捷方法:
广播器还提供了一种通过对类和包进行模式匹配来从命令行打开和关闭跟踪消息的机制。
Mixin
在 KivaKit 中有两种方法可以实现中继器。第一种是简单地扩展 baseRepeater。第二种是使用有状态特征或 Mixin。实现 RepeaterMixin 接口与扩展 baseRepeater 相同,但 mixin repeater 可以在已经有基类的类中使用。请注意,下面讨论的 Component 接口使用相同的模式。如果您无法扩展 baseComponent,则可以实现 ComponentMixin。
Mixin 接口是 Java 语言所缺少的一个功能,它提供了一种解决方案。它们的工作原理是将状态查找委托给包私有类 MixinState;此类在身份哈希映射中使用实现 Mixin 的类的 this 引用。查找关联的状态对象。Mixin 接口如下:
public interface Mixin
{
default
T state(Class extends Mixin> type, Factory factory) {
return MixinState.get(this, type, factory);
}
}
如果 state() 未找到此状态对象,则将使用给定的工厂方法创建一个新的状态对象,然后将其与状态图中的 mixin 关联。例如,我们的 RepeaterMixin 接口看起来像这样(为简洁起见进行了替换。为简单起见,省略了大多数方法):
public interface RepeaterMixin extends Repeater, Mixin
{
@Override
default void addListener(Listener listener, Filter
filter) {
repeater().addListener(listener, filter);
}
@Override
default void removeListener(Listener listener)
{
repeater().removeListener(listener);
}
[...]
default Repeater repeater()
{
return state(RepeaterMixin.class, baseRepeater::new);
}
}
在这里初始化软件许可证失败,addListener() 和 removeListener() 方法分别通过 repeater() 检索其 baseRepeater 状态对象,并将方法调用委托给该对象。如我们所见,在 KivaKit 中实现 mixin 并不复杂。应该注意的是,问题在于每次调用 mixin 中的方法都需要在状态图中进行查找。身份哈希映射通常很好,但对于某些组件,这可能会导致性能问题。与大多数性能问题一样,我们最好尽可能做最简单的事情,直到我们的分析器告诉我们其他情况。
成分
KivaKit 组件通常是微服务的关键部分。组件可以通过扩展 baseComponent(最常见的情况)或实现 ComponentMixin 来轻松访问消息。除了从 Repeater 继承的侦听器列表外,从 Component 继承的组件根本没有向对象添加任何状态。这使得组件非常轻量级。实例化大量组件也不是问题。由于组件都是中继器,因此您可以创建侦听器链,如上所述。
除了提供方便的消息访问之外,该组件还提供以下功能:
对象注册和查找
KivaKit 使用服务定位器设计模式,而不是依赖注入。在组件中使用此模式很简单。一个组件可以使用 registerObject() 注册一个对象,另一个组件可以使用 require() 查找该对象:
Database database = [...]
registerObject(database);
[...]
var database = require(Database.class);
如果需要注册单个类的多个实例,可以使用枚举值来区分它们:
enum Database { PRODUCTS, SERVICES }
registerObject(database, Database.PRODUCTS);
[...]
var database = require(Database.class, Database.SERVICES);
在 KivaKit 中,任何可能使用依赖注入的地方,我们都使用 register 和 require。
设置
KivaKit 中的组件还可以使用 require() 方法轻松访问设置信息:
require(DatabaseSettings.class);
与注册对象一样,当存在多个相同类型的设置对象时,可以使用枚举来区分:
require(DatabaseSettings.class, Database.PRODUCTS);
有多种方法可以注册设置:
registerAllSettingsIn(Folder)
registerAllSettingsIn(Package)
registerSettingsObject(Object)
registerSettingsObject(Object, Enum)
在 KivaKit 1.0 中,使用 registerAllSettingsIn() 方法加载的设置对象由.properties 文件定义。将来,框架将提供一个 API 以支持从其他来源加载属性,例如.json 文件。要实例化的设置类的名称由类属性给出。接下来从其余属性中检索实例化对象的各个属性。每个属性都使用一个 KivaKit 转换器(如下所述)转换为对象。例如:
数据库设置.properties
class = com.mycompany.database.DatabaseSettings
port = database.production.mypna.com:3306
数据库设置.java
public class DatabaseSettings
{
@KivaKitPropertyConverter(Port.Converter.class)
private Port port;
public Connection connect()
{
// Return connection to database on desired port
[...]
}
}
包资源
KivaKit 提供了一个统一多种资源类型的资源微框架:
资源是应用程序可从中读取流数据的组件。可写资源是应用程序可向其写入流数据的资源。文件可用的大多数方法也适用于任何给定的资源。,但某些资源类型可能不支持某些方法。例如,资源可能是流式的,因此它可能无法实现 sizeInBytes()。
KivaKit 文件是一种特殊资源。它使用服务提供程序接口 (SPI) 来允许添加新文件系统。kivakit-extensions 项目为以下文件系统提供实现:
public class MyComponent extends baseComponent
{
[...]
var resource = listenTo(packageResource("data/data.txt"));
for (var line : resource.reader().lines())
{
}
}
软件包结构如下:
├── MyComponent
└── data
└── data.txt
应用程序
KivaKit Application 是一个特殊的组件,其中包含与启动、初始化和执行相关的方法。Server 是 Application 的子类:
微服务是 KivaKit 应用程序最常见的用途,但我们也可以实现其他类型的应用程序(桌面、Web、实用程序等)。微服务应用程序的基本代码如下:
public class MyMicroservice extends Server
{
public static void main(final String[] arguments)
{
new MyApplication().run(arguments);
}
private MyApplication()
{
super(MyProject());
}
@Override
protected void onRun()
{
[...]
}
}
此处的 main() 方法创建应用程序,并使用从命令行传递的参数调用 Application 基类中的 run() 方法。然后,微服务构造函数将 Project 对象传递给超类构造函数。此对象用于初始化包含您的应用程序及其所依赖的任何其他项目的项目。继续我们的示例,我们的 Project 类如下所示:
public class MyProject extends Project
{
private static Lazy
project = Lazy.of(MyProject::new); public static ApplicationExampleProject get()
{
return project.get();
}
protected ApplicationExampleProject()
{
}
@Override
public Set
dependencies() {
return Set.of(ResourceProject.get());
}
}
可以使用 get() 检索 MyProject 的单例实例。MyProject 的依赖项由 dependency() 返回。在这种情况下,MyProject 仅依赖于 ResourceProject,它是 kivakit-resource 迷你框架的项目定义。ResourceProject 又有自己的依赖项。KivaKit 将确保在调用 onRun() 之前初始化所有传递项目依赖项。
部署
KivaKit 应用程序可以自动从名为 Deployment 的应用程序相关包中加载设置对象集合。将微服务部署到特定环境时,此功能非常有用。我们的应用程序结构如下:
├── MyMicroservice
└── deployments
├── development
│ ├── WebSettings.properties
│ └── DatabaseSettings.properties
└── production
├── WebSettings.properties
└── DatabaseSettings.properties
当您在命令行上将开关 -deployment= 传递给应用程序时,它将从命名的部署(在本例中为开发或生产)加载设置。使用打包的部署设置对于微服务特别有用,因为应用程序使用它看起来非常简单:
java -jar my-microservice.jar -deployment=development [...]
这样,即使你对此不太了解,也可以轻松地在 Docker 容器中运行应用程序。如果你不需要打包部署设置,则可以通过设置环境变量 KIVAKIT_SETTINGS_FOLDERS 来使用外部文件夹:
-DKIVAKIT_SETTINGS_FOLDERS=/Users/jonathan/my-microservice-settings
命令行解析
应用程序还可以通过返回 SwitchParsers 和/或 ArgumentParsers 列表来解析命令行:
public class MyMicroservice extends Application
{
private SwitchParser
DICTIonARY = File.fileSwitchParser("input", "Dictionary file")
.required()
.build();
@Override
public String description()
{
return "This microservice checks spelling.";
}
@Override
protected void onRun()
{
var input = get(DICTIONARY);
if (input.exists())
{
[...]
}
else
{
problem("Dictionary does not exist: $", input.path());
}
}
@Override
protected Set
> switchParsers() {
return Set.of(DICTIONARY);
}
}
这里,KivaKit 使用 switchParsers() 返回的 DICTIonARY 开关解析器来解析命令行。在 onRun() 方法中,get(DICTIONARY) 用于检索在命令行上传递的 File 参数。如果命令行有语法问题或验证失败,KivaKit 将自动报告问题并提供从 description() 以及开关和参数解析器派生的使用帮助:
┏-------- COMMAND LINE ERROR(S) -----------
┋ ○ Required File switch -input is missing
┗------------------------------------------
KivaKit 1.0.0 (puffy telephone)
Usage: MyApplication 1.0.0
This microservice checks spelling.
Arguments:
None
Switches:
Required:
-input=File (required) : Dictionary file
Switch 解析器
在我们的示例应用程序中,我们使用以下代码构建了一个 SwitchParser:
private SwitchParser
INPUT = File.fileSwitchParser("input", "Input text file")
.required()
.build();
File.fileSwitchParser() 方法返回一个切换解析器构建器,在调用 build() 之前可以通过多种方式对其进行自定义:
public Builder
name(String name) public Builder
type(Class type) public Builder
description(String description) public Builder
converter(Converter converter) public Builder
defaultValue(T defaultValue) public Builder
optional() public Builder
required() public Builder
validValues(Set validValues)
File.fileSwitchParser()的实现如下:
public static SwitchParser.Builder
fileSwitchParser(String name, String description) {
return SwitchParser.builder(File.class)
.name(name)
.converter(new File.Converter(LOGGER))
.description(description);
}
所有开关和参数都是类型化对象,因此 builder(Class) 方法创建一个 File 类型的构建器(使用 type() 方法)。它被赋予传递给 fileSwitchParser() 的名称和描述,并使用 File.Converter 方法在 String 和 File 对象之间进行转换。
转换器
KivaKit 提供了许多转换器,可以在 KivaKit 中的许多地方使用。转换器是可重复使用的对象,用于将一种类型转换为另一种类型。它们非常容易创建,并且可以处理许多常见问题,例如异常和 null 或空值:
public static class Converter extends baseStringConverter
{
public Converter(Listener listener)
{
super(listener);
}
@Override
protected File onToValue(String value)
{
return File.parse(value);
}
}
调用 StringConverter.convert(String) 可将字符串转换为文件。调用 StringConverter.unconvert(File) 可将文件转换回字符串。转换过程中遇到的任何问题都会广播给感兴趣的侦听器。如果失败,则返回 null。正如我们所见,转换器采用不同的侦听器链方法。所有转换器都需要一个侦听器作为构造函数参数,而不是依赖转换器用户调用 listenTo()。这确保所有转换器都能够向至少一个侦听器报告转换问题。
核实
在上面的命令行解析代码中,使用 kivakit-validation 迷你框架验证开关和参数。另一个常见用例是验证微服务的 Web 应用程序用户界面的域对象。
Validatable 类实现:
public interface Validatable
{
Validator validator(ValidationType type);
}
要实现此方法,您可以匿名子类化 basevalidator。basevalidator 提供了方便的方法来检查状态一致性并广播问题和警告。KivaKit 使用 ValidationIssues 对象捕获这些消息。然后您可以使用 Validatable 接口中的默认方法来查询此状态。用法如下:
public class User implements Validatable
{
String name;
[...]
@Override
public Validator validator(ValidationType type)
{
return new basevalidator()
{
@Override
protected void onValidate()
{
problemIf(name == null, "User must have a name");
}
};
}
}
public class MyComponent extends baseComponent
{
public void myMethod()
{
var user = new User("Jonathan");
if (user.isValid(this))
{
[...]
}
}
}
此处的验证消息将被捕获,以确定 User 对象是否有效。相同的消息还会广播到 MyComponent 的侦听器,这些消息可能会被记录或显示在某些用户界面中。
日志记录
KivaKit Logger 是一个消息监听器,它记录它听到的所有消息。基础 Application 类有一个 Logger,它记录从组件冒泡到应用程序级别的任何消息。这意味着需要在应用程序或其任何组件中创建 Logger,只要监听器链从每个组件一直延伸到应用程序即可。
最简单的logger就是ConsoleLogger,从基本结构上看,ConsoleLogger以及相关的类大致如下(见下面的UML图):
public class ConsoleLogger extends baseLogger
{
private Log log = new ConsoleLog();
@Override
protected Set
logs() {
return Sets.of(log);
}
}
public class baseLogger implements Logger
{
void onMessage(final Message message)
{
log(message);
}
public void log(Message message)
{
[...]
for (var log : logs())
{
log.log(entry);
}
}
}
public class ConsoleLog extends baseTextLog
{
private Console console = new Console();
@Override
public synchronized void onLog(LogEntry entry)
{
console.printLine(entry.message().formatted());
}
}
baseLogger.log(Message) 方法通过添加上下文信息将提供的消息转换为 LogEntry。然后它将日志条目传递给 logs() 返回的日志列表中的每个 Log。对于 ConsoleLogger,返回的是 ConsoleLog 单个实例,它将 LogEntry 写入控制台。
KivaKit 有一个 SPI,允许从命令行动态添加和配置新的记录器。KivaKit 提供的一些记录器包括:
Web 和 REST
kivakit-extensions 项目包含对 Jetty、Jersey、Swagger 和 Apache Wicket 的基本支持,因为这些工具在实现微服务时通常很有用。这些微框架都已集成,因此可以轻松启动 Jetty 服务器初始化软件许可证失败,为微服务提供 REST 和 Web 访问:
@Override
protected void onRun()
{
final var port = (int) get(PORT);
final var application = new MyRestApplication();
// and start up Jetty with Swagger, Jersey and Wicket.
listenTo(new JettyServer())
.port(port)
.add("/*", new JettyWicket(MyWebApplication.class))
.add("/open-api/*", new JettySwaggerOpenApi(application))
.add("/docs/*", new JettySwaggerIndex(port))
.add("/webapp/*", new JettySwaggerStaticResources())
.add("/webjar/*", new JettySwaggerWebJar(application))
.add("/*", new JettyJersey(application))
.start();
}
这里的JettyServer可以让Jersey、Wicket、Swagger三者结合起来,使用统一的API,使得代码清晰简洁,通常这也是我们所需要的。
总结
尽管 KivaKit 刚刚发布了全新的 1.0 版本,但它实际上已在 Telenav 中使用了十多年。开发团队欢迎开源社区的贡献,包括反馈、错误报告、功能创意、文档、测试和代码贡献。
以下资源可以帮助您深入了解框架细节:
关于作者
Jonathan Locke 自 1996 年以来一直使用 Java,是 Sun Microsystems Java 团队的成员。作为一名开源作者,他是 Apache Wicket Web 框架和 Java UML 文档工具 Lexakai 的创始人。Jonathan 是 Telenav 的首席软件架构师。
原文链接: