KivaKit 是一个基于 Apache 许可证的开源 Java 框架,用于实现微服务。KivaKit 需要一个 Java 11+虚拟机,但与 Java 8 和 9 项目源代码兼容。KivaKit 由一组精心集成的迷你框架组成。每个迷你框架都有一致的设计和自己的关注点,可以单独使用,也可与其他迷你框架协同使用。下图是这些框架的依赖网络简图,这是一张很直观的 KivaKit 高级架构图:

每个微型框架都解决了开发微服务时经常遇到的某个问题。本文将简要介绍上图中的各个微型框架及它们的使用方法。

消息传递

正如我们在上图中所见,消息传递(Messaging)是 KivaKit 的核心。消息传递在构建状态可观察的组件时很有用,这在基于云的世界中是一项好用的特性。KivaKit 中的许多对象会广播或收听状态消息,例如 Alert、Problem、Warning 或 Trace。它们大多数是中继器(Repeater),侦听来自其他对象的状态消息并将它们重新广播给下游感兴趣的侦听器(listener)。这形成了一个带有终端侦听器的侦听器链:

C->B->A

复制代码

通常,链中的最后一个侦听器是某种日志记录器(Logger),但链的末端也可以有多个侦听器,可以是任何实现侦听器的对象。例如,在验证(Validation)迷你框架中,状态消息由 ValidationIssues 类捕获,然后用于确定验证是否成功,还能在验证失败时向用户展示特定问题。给定上面的侦听器链,C 和 B 实现了中继器,最终的对象 A 实现了侦听器。在链中的每个类中,侦听器链扩展为:

listener.listenTo(broadcaster)

复制代码

为了向感兴趣的侦听器传输消息,这里从 Broadcaster 继承了一些针对常见消息类型的便利方法:

消息 含义
problem() 出了点问题,需要解决,但这对当前的操作来说并不致命。
glitch() 出现了一个小问题。与Warning不同,Glitch表示出现了验证失败或发生数据丢失的问题。与Problem不同,Glitch表示操作肯定会恢复并继续。<br>
warning() 出现了一个小问题,应该更正,但不一定需要注意。
quibble() 发生了一个不需要更正的小问题。
announcement() 宣布一项操作的一个重要阶段。
narration() 某个操作中的一个步骤已经开始或完成。
infromation() 不代表任何问题的常用信息。
trace() 调试时使用的诊断信息。

广播器(Broadcaster)还提供了一种机制,可以通过对类和包的模式匹配,从命令行打开和关闭 Trace 消息。

Mixin

在 KivaKit 中有两种方法可以实现中继器。第一种方法是简单地扩展 BaseRepeater。第二种是使用有状态的 trait 或 Mixin。实现 RepeaterMixin 接口与扩展 BaseRepeater 是一样的,但是 mixin 中继器可以在已经有基类的类中使用。请注意,下面讨论的 Component 接口使用了相同的模式。如果无法扩展 BaseComponent,则可以实现 ComponentMixin。

Mixin 接口 Java 语言缺少的一项特性提供了一种解决方法。它的工作原理是将状态查找委托给一个包私有类,MixinState;该类使用实现 Mixin 的类的 this 引用,在身份哈希映射中查找关联的状态对象。Mixin 界面如下所示:

public interface Mixin{
      default <T> T state(Class<? extends Mixin> type, Factory<T> factory)    {
          return MixinState.get(this, type, factory);    }}

复制代码

如果 state()没有找到 this 的 state 对象,将使用给定的工厂方法创建一个新的 state 对象,然后该对象将与 statemap 中的 mixin 相关联。例如,我们的 RepeaterMixin 接口大致如下所示(为简洁起见,省略了大部分方法):

public interface RepeaterMixin extends Repeater, Mixin{
      @Override    default void addListener(Listener listener, Filter<Transmittable> 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 组件(Component)往往会是微服务的关键部分。组件可以通过扩展 BaseComponent(最常见的情况)或通过实现 ComponentMixin 来轻松访问消息。除了从中继器继承的侦听器列表之外,从 Component 继承根本不会向对象添加任何状态。这样组件就变得非常轻量级。实例化大量组件也不是什么问题。由于组件都是中继器,因此可以创建侦听器链,如上所述。

除了提供对消息的便捷访问之外,组件还提供以下功能:

  • 注册和查找对象

  • 加载和访问设置对象

  • 访问包资源下面依次分析。

对象注册和查找

KivaKit 使用的是 服务定位器 设计模式,而不是依赖注入。在组件中使用这种模式是很简单的。一个组件可以使用 registerObject()注册一个对象,另一个组件可以使用 require()查找它:

Database database = [...]registerObject(database);[...]var database = require(Database.class);

复制代码

如果需要注册单个类的多个实例,可以使用一个枚举(enum)值来区分它们:

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 转换器(如下所述)转换为对象。例如:

DatabaseSettings.properties

class = com.mycompany.database.DatabaseSettingsport  = 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 提供了一个资源迷你框架,统一了多种资源类型:

  • 文件

  • 套接字

  • Zip 或 JAR 文件条目

  • 包资源

  • HTTP 响应

  • 输入流

  • 输出流

  • [……]

资源(Resource)指的是应用程序可以从中读取流数据的组件。WritableResources 是指应用程序可以在其中写入流数据的资源。文件(File)可用的大多数方法在任何给定资源中都可用,但某些资源类型可能会不支持某些方法。例如,一个资源可能是流式传输的,所以它不能实现 sizeInBytes()。

KivaKit 文件是一种特殊的资源。它使用服务提供者接口(SPI)来允许添加新的文件系统。kivakit-extensions 项目为以下文件系统提供了实现:

  • HDFS 文件

  • S3 对象

  • GitHub 存储库(只读)KivaKit 组件提供了对 PackageResources 的简便访问。KivaKit 中封装资源的风格与 Apache Wicket 中的类似,也就是说一个组件的包将有一个包含其运行所需资源的子包。这允许我们从单个源树中轻松打包和使用组件。访问与组件相关的包资源的方法如下所示:

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)是应用程序的子类:

微服务是 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<MyProject> project = Lazy.of(MyProject::new);    public static ApplicationExampleProject get()    {
          return project.get();    }    protected ApplicationExampleProject()    {
      }    @Override    public Set<Project> dependencies()    {
          return Set.of(ResourceProject.get());    }}

复制代码

可以使用 get()检索 MyProject 的单例实例。MyProject 的依赖项由 dependencies()返回。在这种情况下,MyProject 仅依赖于 ResourceProject,这是 kivakit-resource 迷你框架的项目定义。ResourceProject 又有自己的依赖项。KivaKit 将确保在调用 onRun()之前初始化所有可传递的项目依赖项。

部署

KivaKit 应用程序可以从名为 deployments 的应用程序相关包中自动加载设置对象的集合。在将微服务部署到特定环境时,此功能是很有用的。我们应用程序的结构如下所示:

├── MyMicroservice└── deployments    ├── development    │   ├── WebSettings.properties    │   └── DatabaseSettings.properties    └── production        ├── WebSettings.properties        └── DatabaseSettings.properties

复制代码

在命令行上将开关 -deployment= 传递给应用程序时,它将从命名过的部署(在本例中为 development 或 production)加载设置。对微服务使用打包部署设置特别好用,因为应用程序使用起来非常简单:

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<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()返回的 DICTIONARY 开关解析器来解析命令行。在 onRun()方法中,通过 get(DICTIONARY)检索命令行上传递的 File 参数。如果命令行存在语法问题或未通过验证,KivaKit 将自动报告问题并提供从 description()以及 switch 和 argument 解析器派生的使用帮助:

┏-------- 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:  NoneSwitches:    Required:      -input=File (required) : Dictionary file

复制代码

Switch 解析器

在我们的应用程序示例中,我们使用以下代码构建了一个 SwitchParser:

private SwitchParser<File> INPUT =     File.fileSwitchParser("input", "Input text file")                .required()                .build();

复制代码

File.fileSwitchParser()方法返回一个 switch 解析器构建器,它可以在调用 build()之前对几个方法做定制:

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)

复制代码

File.fileSwitchParser()的实现如下所示:

public static SwitchParser.Builder<File> fileSwitchParser(String name, String description){
      return SwitchParser.builder(File.class)            .name(name)            .converter(new File.Converter(LOGGER))            .description(description);}

复制代码

所有开关(switch)和参数(argument)都是类型化对象,因此 builder(Class)方法创建一个具有 File 类型的构建器(使用 type()方法)。它被赋予传递给 fileSwitchParser()的名称和描述,并使用 File.Converter 方法在 String 和 File 对象之间转换。

转换器

KivaKit 提供了很多转换器(Converter),转换器可以用在 KivaKit 的很多地方。转换器是可重用的对象,可将一种类型转换为另一种类型。它们特别容易创建,并且可以处理很多常见问题,例如异常和 null 或空值:

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(String)会将字符串转换为文件。调用 StringConverter.unconvert(File)会将 File 重新转换为 String。转换过程中遇到的任何问题都会广播给感兴趣的侦听器,如果转换失败,则返回 null。正如我们所见,转换器对侦听器链采取了一种不一样的方法。所有转换器都需要一个侦听器作为构造器参数,而不是依赖转换器用户调用 listenTo()。这确保所有转换器都能够向至少一个侦听器报告转换问题。

验证

在上面的命令行解析代码中,开关和参数使用 kivakit-validation 迷你框架进行验证。另一个常见用例是为一个微服务验证一个 Web 应用程序用户界面的域对象。

Validatable 类实现:

public interface Validatable{
      /**     * @param type The type of validation to perform     * @return A {@link Validator} instance     */    Validator validator(ValidationType type);}

复制代码

要实现此方法,可以匿名地对一个 BaseValidator 进行子类化。BaseValidator 提供了方便的方法来检查状态一致性,以及广播问题(problem)和警告(warning)。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,用于记录从组件冒泡到应用程序级别的任何消息。这意味着不需要在应用程序或其任何组件中创建记录器,只要侦听器链从每个组件一直连回应用程序即可。

最简单的日志记录器是 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)方法通过添加上下文信息将其提供的消息转换为 LogEntry。然后它将日志条目传递给由 logs()返回的日志列表中的每个 Log。对于 ConsoleLogger 来说,返回的是 ConsoleLog 的单个实例。ConsoleLog 将 LogEntry 写入控制台。

KivaKit 有一个 SPI,允许从命令行动态添加和配置新的日志记录器。KivaKit 提供的一些日志记录器包括:

  • ConsoleLog

  • EmailLog

  • FileLog

Web 和 REST

kivakit-extensions 项目包含了对 Jetty、Jersey、Swagger 和 Apache Wicket 的基本支持,因为这些工具在实现微服务时通常很有用。这些微型框架都集成在一起,因此我们很容易就能启动一个 Jetty 服务器,为微服务提供 REST 和 Web 访问:

@Overrideprotected 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 担任首席软件架构师。

原文链接:

Introducing the KivaKit Framework

如果觉得本文对你有帮助麻烦点赞关注支持一下