关键点
- KivaKit是一个模块化Java框架,用于开发需要Java 11+虚拟机但与Java 8源代码兼容的微服务
- KivaKit提供了实现应用程序的基本功能,包括命令行解析和应用程序配置
- KivaKit组件是轻量级组件,使用广播/侦听器消息传递系统传递状态信息
- KivaKit迷你框架,包括转换、验证、资源和日志迷你框架,通过消息传递使用和报告状态信息
- KivaKit配置并运行Jetty、Jersey、Swagger和Apache Wicket,以一致的方式提供微服务接口
- 关键的KivaKit基类也可以作为有状态特征或“mixin”提供
概述
KivaKit是一个Apache许可证开源Java框架,设计用于实现微服务。KivaKit需要一个Java11+虚拟机,但源代码与Java8和Java9项目兼容。KivaKit由一组精心集成的迷你框架组成。每个迷你框架都有一个一致的设计和自己的重点,可以与其他迷你框架配合使用,也可以单独使用。这些框架的简化依赖关系网络提供了KivaKit的良好高级视图:
每个迷你框架都解决了开发微服务时经常遇到的不同问题。本文简要概述了上图中的微型框架,并简要介绍了如何使用它们。
消息传递
如上图所示,消息传递是KivaKit的核心。消息传递在构建状态可观察的组件时非常有用,这在基于云的世界中是一个有用的功能。KivaKit中的许多对象广播或侦听状态消息,如警报、问题、警告或跟踪。大多数是中继器,侦听来自其他对象的状态消息,并将其重新广播给下游感兴趣的侦听器。这将与终端侦听器形成侦听器链:
C->B->A
通常,链中的最后一个侦听器是某种记录器,但在链的末尾也可以有多个侦听器,任何实现侦听器的对象都可以工作。例如,在Validation mini框架中,ValidationSues类捕获状态消息,然后使用该类确定验证是否成功,以及向用户显示验证失败的特定问题。
给定上面的侦听器链,C和B实现Repeater,最后一个对象A实现listener。在链中的每个类中,侦听器链都扩展为:
listener.listenTo(广播员)
要将消息发送给感兴趣的侦听器,将从广播机继承方便方法,以获得常见类型的消息:
Message |
Purpose |
problem() |
Something has gone wrong and needs to be addressed, but it’s not fatal to the current operation. |
glitch() |
A minor problem has occurred. Unlike a Warning, a Glitch indicates validation failure or data loss has occurred. Unlike a Problem, a Glitch indicates that the operation will definitely recover and continue. |
warning() |
A minor issue has occurred which should be corrected, but does not necessarily require attention. |
quibble() |
A trivial issue has occurred that does not require correction. |
announcement() |
Announces an important phase of an operation. |
narration() |
A step in some operation has started or completed. |
information() |
Commonly useful information that doesn’t represent any problem |
trace() |
Diagnostic information for use when debugging. |
广播器还提供了一种机制,通过对类和包进行模式匹配,从命令行打开和关闭跟踪消息。
混合
在KivaKit中,有两种实现中继器的方法。第一种方法是简单地扩展BaseRepeater。第二种是使用有状态特征或Mixin。实现RepeaterMixin接口与扩展BaseRepeater相同,但是repeater mixin可以在已经有基类的类中使用。注意,下面讨论的组件接口使用相同的模式。如果不能扩展BaseComponent,那么可以实现ComponentMixin。
Mixin接口为缺少的Java语言特性提供了一个解决方案。它的工作原理是将状态查找委托给包私有类MixinState,该类使用实现Mixin的类的this引用在标识哈希映射中查找关联的状态对象。Mixin接口如下所示:
public interface Mixin
{
defaultT state(Class type, Factoryfactory)
{
return MixinState.get(this, type, factory);
}
}
如果state()找不到此的state对象,则将使用给定的工厂方法创建新的state对象,然后该对象将与状态映射中的mixin关联。例如,我们的Repeatermxin接口大致如下(为了简洁起见,没有大多数方法):
public interface RepeaterMixin extends Repeater, Mixin { @Override default void addListener(Listener listener, Filterfilter) { repeater().addListener(listener, filter); } @Override default void removeListener(Listener listener) { repeater().removeListener(listener); } [...] default Repeater repeater() { return state(RepeaterMixin.class, BaseRepeater::new); } }
这里,addListener()和RemovelListener()方法各自通过repeater()检索其BaseRepeater状态对象,并将方法调用委托给该对象。正如我们所见,在KivaKit中实现mixin并不复杂。
应该注意的是,对mixin中方法的每次调用都需要在状态映射中进行查找。标识哈希映射通常应该相当有效,但对于一些组件来说,这可能是一个性能问题。与大多数性能问题一样,我们最好做最简单的事情,直到我们的分析器不这么说。
组件
KivaKit组件通常可能是微服务的关键部分。组件通过扩展BaseComponent(最常见的情况)或通过实现ComponentMixin提供对消息传递的轻松访问。从组件继承不会向对象添加任何状态,但从Repeater继承的侦听器列表除外。这使得组件非常轻量级。大量实例化它们并不是一个问题。由于组件是中继器,因此可以创建侦听器链,如上所述。
除了提供对消息的方便访问外,组件还提供以下功能:
- 注册和查找对象
- 加载和访问设置对象
- 访问包资源
让我们看看这些设施。
对象注册和查找
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中,使用RegisterAllSettings sin()方法加载的设置对象由.properties文件定义。将来,将提供一个API,以允许从其他源(如.json文件)加载属性。要实例化的设置类的名称由class属性提供。然后从其余属性中检索实例化对象的各个属性。使用KivaKit转换器(如下所述)将每个属性转换为对象。
例如:
DatabaseSettings.properties
class = com.mycompany.database.DatabaseSettings port = database.production.mypna.com:3306
DatabaseSettings.java
public class DatabaseSettings { @KivaKitPropertyConverter(Port.Converter.class) private Port port; public Connection connect() { // Return connection to database on desired port [...] } }
包资源
KivaKit提供了一个资源迷你框架,它统一了多种资源类型:
- 文件夹
- Sockets
- Zip或JAR文件条目
- 包资源
- HTTP响应
- 输入流
- 输出流
- […]
资源是应用程序可以从中读取流数据的组件。WritableResources是应用程序可以向其写入流数据的资源。文件可用的大多数方法在任何给定资源中都可用,但某些资源类型可能会使某些方法不受支持。例如,资源可能是流式的,因此它无法实现sizeInBytes()。
KivaKit文件是一种特殊的资源。它使用服务提供者接口(SPI)来允许添加新的文件系统。kivakit extensions项目提供了以下文件系统的实现:
- HDFS文件
- S3对象
- GitHub存储库(只读)
KivaKit组件便于访问PackageResources。KivaKit中封装资源的风格类似于ApacheWicket中的风格,组件的包将有一个子包,其中包含其运行所需的资源。这允许从单个源代码树轻松打包和使用组件。相对于组件对包资源的访问如下所示:
public class MyComponent extends BaseComponent { [...] var resource = listenTo(packageResource("data/data.txt")); for (var line : resource.reader().lines()) { } }
Where the package structure looks like this:
├── MyComponent └── data └── data.txt
应用
KivaKit应用程序是一个美化的组件,包含与启动、初始化和执行相关的方法。服务器是应用程序的一个子类:
微服务是KivaKit应用程序最常见的用途,但也可以实现其他类型的应用程序(桌面、web、实用程序等)。microservice应用程序的基本代码如下所示:
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()方法创建应用程序,并使用从命令行传递的参数调用应用程序基类中的run()方法。然后,微服务的构造函数将项目对象传递给超类构造函数。此对象用于初始化包含应用程序的项目以及它所依赖的任何其他项目。继续我们的示例,我们的项目类如下所示:
public class MyProject extends Project
{
private static Lazyproject = Lazy.of(MyProject::new);
public static ApplicationExampleProject get()
{
return project.get();
}
protected ApplicationExampleProject()
{
}
@Override
public Setdependencies()
{
return Set.of(ResourceProject.get());
}
}
{
可以使用get()检索MyProject的单例实例。MyProject的依赖项由dependencies()返回。在本例中,MyProject仅依赖于ResourceProject,ResourceProject是kivakit资源迷你框架的项目定义。ResourceProject也有自己的依赖项。KivaKit将确保在调用onRun()之前初始化所有可传递的项目依赖项。
部署
KivaKit应用程序可以从名为deployments的应用程序相关包中自动加载设置对象集合。将微服务部署到特定环境时,此功能非常有用。我们的应用程序的结构如下所示:
├── 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
命令行解析
应用程序还可以通过返回一组SwitchParser和/或ArgumentParser列表来解析命令行:
public class MyMicroservice extends Application
{
private SwitchParser<File> 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<SwitchParser<?>> switchParsers()
{
return Set.of(DICTIONARY);
}
}
这里,KivaKit使用switchParsers()返回的字典开关解析器来解析命令行。在onRun()方法中,通过get(DICTIONARY)检索在命令行上传递的文件参数。如果命令行存在语法问题或未通过验证,KivaKit将自动报告该问题,并提供从description()以及开关和参数解析器派生的用法帮助:
┏-------- COMMAND LINE ERROR(S) ----------- ┋ ○ Required File switch -input is missing ┗------------------------------------------ KivaKit 1.0.0 (puffy telephone) Usage: MyApplication 1.0.0 <switches> <arguments> This microservice checks spelling. Arguments: None Switches: Required: -input=File (required) : Dictionary file
Switch Parsers
In our application example, we used this code to build a SwitchParser:
private SwitchParser<File> INPUT = File.fileSwitchParser("input", "Input text file") .required() .build();
The File.fileSwitchParser() method returns a switch parser builder which can be specialized with several methods before build() is called:
public Builder<T> name(String name) public Builder<T> type(Class<T> type) public Builder<T> description(String description) public Builder<T> converter(Converter<String, T> converter) public Builder<T> defaultValue(T defaultValue) public Builder<T> optional() public Builder<T> required() public Builder<T> validValues(Set<T> validValues)
The implementation of File.fileSwitchParser() then looks like this:
public static SwitchParser.Builder<File> fileSwitchParser(String name, String description) { return SwitchParser.builder(File.class) .name(name) .converter(new File.Converter(LOGGER)) .description(description); }
所有开关和参数都是类型化对象,因此builder(Class)方法使用File类型(使用type()方法)创建一个builder。它的名称和描述传递给fileSwitchParser(),File.Converter方法用于在字符串和文件对象之间进行转换。
转换器
KivaKit提供了许多转换器,转换器可以在KivaKit的许多地方使用。转换器是将一种类型转换为另一种类型的可重用对象。它们特别容易创建,并且可以处理异常、空值或空值等常见问题:
public static class Converter extends BaseStringConverter<File>
{
public Converter(Listener listener)
{
super(listener);
}
@Override
protected File onToValue(String value)
{
return File.parse(value);
}
}
调用StringConverter.convert(字符串)将字符串转换为文件。调用StringConverter.uncert(文件)将把文件转换回字符串。转换过程中遇到的任何问题都会广播给感兴趣的侦听器,如果转换失败,则返回null。
正如我们所看到的,转换器对侦听器链采取了不同的方法。所有转换器都需要一个侦听器作为构造函数参数,而不是依赖转换器用户来调用listenTo()。这确保所有转换器都能够向至少一个侦听器报告转换问题。
验证
在上面的命令行解析代码中,使用kivakit validation mini框架验证开关和参数。另一个常见的用例是向微服务验证web应用程序用户界面的域对象。
可验证类实现:
public interface Validatable
{
/**
* @param type The type of validation to perform
* @return A {@link Validator} instance
*/
Validator validator(ValidationType type);
}
要实现此方法,可以匿名地对BaseValidator进行子类化。BaseValidator提供了检查状态一致性以及广播问题和警告的方便方法。KivaKit使用ValidationSues对象捕获这些消息。然后可以使用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))
{
[...]
}
}
}
这里捕获来自验证的消息以确定用户对象是否有效。同样的消息也会广播到MyComponent的监听器,这些消息可能会记录或显示在某些用户界面中。
日志
KivaKit记录器是一个消息侦听器,记录它听到的所有消息。基本应用程序类有一个日志记录器,用于记录从组件到应用程序级别的任何消息。这意味着不需要在应用程序或其任何组件中创建记录器,只要侦听器链从每个组件一直引导到应用程序。
最简单的记录器是ConsoleLogger。将此设计缩小到基本结构,ConsoleLogger和相关类大致如下所示(请参见下面的UML图):
:
public class ConsoleLogger extends BaseLogger
{
private Log log = new ConsoleLog();
@Override
protected Set<Log> 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)方法通过添加上下文信息将提供给它的消息转换为日志条目。然后,它将日志条目传递给logs()返回的日志列表中的每个日志。对于ConsoleLogger,将返回一个ConsoleLog实例。ConsoleLog将日志条目写入控制台。
KivaKit有一个SPI,允许从命令行动态添加和配置新的记录器。KivaKit提供的一些伐木工人包括:
- ConsoleLog
- EmailLog
- FileLog
Web和REST
kivakit扩展项目包含对Jetty、Jersey、Swagger和ApacheWicket的基本支持,因为它们在实现微服务时通常很有用。这些微型框架都集成在一起,因此启动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上已经使用了十多年。