cccicl
cccicl
发布于 1周前

[译]代码优先的Java 9模块系统教程(二)

服务监控(ServiceMonitor)

让我们来想象一个提供娱乐服务的网络,可能是社交网络或者是视频网络。我们希望监控这些服务,以确定系统的健康状况,并且在发生问题时能够发现,而不是客户报告。 这就是ServiceMonitor示例程序所要做的:监视这些服务(另一个惊喜)。

幸运的是,服务已经收集了我们想要的数据,ServiceMonitor所需要做的就是定期查询。 不幸的是,并不是所有的服务都暴露相同的REST API —— 有两个版本在用,Alpha和Beta。 这就是为什么ServiceObserver接口有两个实现。

一旦我们有了诊断数据,用DiagnosticDataPoint表示,这些数据会传给Statistician,它会把数据聚合给Statistics。这些数据会依次被存储在StatisticsRepository,并且会使用MonitorServer的REST公开。Monitor类会将所有内容联系起来。

总而言之,我们最终得到以下类型:

  • DiagnosticDataPoint: 一段时间内的服务数据
  • ServiceObserver: 返回DagnosticDataPoint的服务观察者接口
  • AlphaServiceObserver and BetaServiceObserver: ServiceObserver接口的Alpha和Beta的实现
  • Statistician: 根据DiagnosticDataPoint统计得到的Statistics
  • Statistics: 保存统计信息
  • StatisticsRepository: 保存和查询Statistics
  • MonitorServer: 统计信息的REST调用
  • Monitor: 联系所有的内容

程序依赖于Spark微型web框架,因此我们引用模块spark.core。 它可以在libs目录以及它的传递依赖关系中找到。

到目前为止,我们已经知道如何将应用程序组织为单个模块。 首先,我们在项目的根目录下创建模块声明module-info.java:

module monitor {
    requires spark.core;
}

注意到我们本应该选一个类似org.codefx.demo.monitor的模块名,但这将包含这些示例,所有我还是坚持使用较短的模块名monitor。如之前所说,它依赖于spark.core。因为程序没有有意义的API,所有它不导出任何包(package)。

接着我们可以编译,打包以及运行它,如下所示:

$ javac
    --module-path libs
    -d classes/monitor
    ${source-files}
$ jar --create
    --file mods/monitor.jar
    --main-class monitor.Main
    -C classes/monitor .
$ java
    --module-path mods
    --module monitor

你可以看到,我们不再使用Maven的target目录,而是在mods的classes和modules下新建类。 这使得示例更容易解析。 请注意,与以前不同的是,因为程序有非JDK的依赖,我们必须在编译期使用模块路径。

到此我们创建了单一模块ServiceMonitor!

拆分模块

现在我们有了一个模块,是时候真正开始使用模块系统将ServiceMonitor拆分。 对于这种大小的应用程序,将其变成几个模块当然是可笑的,但它只是一个演示,所有我们继续。

模块化应用程序的最常见的方法是关注点分离。 ServiceMonitor有以下内容,括号里包含相关类型:

  • 从服务收集数据 ( ServiceObserver, DiagnosticDataPoint)
  • 聚合数据来统计 ( Statistician, Statistics)
  • 持久化统计数据 ( StatisticsRepository)
  • 用REST API暴露统计数据 ( MonitorServer)

但不仅只有领域逻辑方面的需求, 还有技术方面:

  • 数据收集必须隐藏在API后面
  • Alpha和Beta服务都需要单独实现该API(AlphaServiceObserver和BetaServiceObserver)
  • 编排所有关注点(Monitor)

这得到以下模块以及上述公开可见的类型:

  • monitor.observer ( ServiceObserver, DiagnosticDataPoint)
  • monitor.observer.alpha ( AlphaServiceObserver)
  • monitor.observer.beta ( BetaServiceObserver)
  • monitor.statistics ( Statistician, Statistics)
  • monitor.persistence ( StatisticsRepository)
  • monitor.rest ( MonitorServer)
  • monitor ( Monitor)

将这些模块放在在类图上,很容易看出模块之间的依赖关系:


现实的项目由许多不同类型的文件组成。 显然,源文件是最重要的文件,但只有一种 —— 其他的是测试代码,资源,构建脚本或项目描述,文档,源码管理信息等等。 任何项目都必须选择一个目录结构来组织这些文件,重要的是要确保它不会与模块系统的特性相冲突。

如果你已经遵照Jigsaw项目下的模块系统开发,学习过官方的快速入门指南或一些早期教程,你可能注意到了他们使用了一个特别的目录结构,在src目录下每个项目有一个子目录。这样ServiceMonitor将如下所示:

ServiceMonitor
 + classes
 + mods
 - src
    + monitor
    - monitor.observer
       - monitor
          - observer
             DiagnosticDataPoint.java
             ServiceObserver.java
       module-info.java
    + monitor.observer.alpha
    + monitor.observer.beta
    + monitor.persistence
    + monitor.rest
    + monitor.statistics
 - test-src
    + monitor
    + monitor.observer
    + monitor.observer.alpha
    + monitor.observer.beta
    + monitor.persistence
    + monitor.rest
    + monitor.statistics

这会得到层次,我不喜欢这样。 大多数由几个子项目(我们现在称为模块)组成的项目都喜欢单独的根目录,其中每个子目录包含单个模块的源码,测试,资源以及之前提到的其他所有内容。 他们使用层次,这是建立的项目结构提供的。

Maven和Gradle等工具的默认目录结构就是按这种层次结构实现。 首先,他们给每个模块自己的目录树。 在该树中,src目录包含产品代码和资源(分别在main/java和main/resources中)以及测试代码和资源(分别在test/java和test/resources中):

ServiceMonitor
 + monitor
 - monitor.observer
    - src
       - main
          - java
             - monitor
                - observer
                   DiagnosticDataPoint.java
                   ServiceObserver.java
             module-info.java
          + resources
       + test
          + java
          + resources
    + target
 + monitor.observer.alpha
 + monitor.observer.beta
 + monitor.persistence
 + monitor.rest
 + monitor.statistics

我将会像这样组织ServiceMonitor,唯一的区别是,我将在classes目录下创建字节码,在mods目录下创建JAR包,它们都放在ServiceMonitor下,因为这让脚本更短,更易读。

现在我们来看看这些声明的含义,以及我们如何编译和运行应用程序。
我们已经介绍了如何使用module-info.java来声明模块,因此不需要详细介绍。 一旦你知道模块是如何相互依赖的(你的构建工具应该知道这一点;否则请问JDeps)。

module monitor.observer {
    exports monitor.observer;
}
 
module monitor.observer.alpha {
    requires monitor.observer;
    exports monitor.observer.alpha;
}
 
module monitor.observer.beta {
    requires monitor.observer;
    exports monitor.observer.beta;
}
 
module monitor.statistics {
    requires monitor.observer;
    exports monitor.statistics;
}
 
module monitor.persistence {
    requires monitor.statistics;
    exports monitor.persistence;
}
 
module monitor.rest {
    requires spark.core;
    requires monitor.statistics;
    exports monitor.rest;
}
 
module monitor {
    requires monitor.observer;
    requires monitor.observer.alpha;
    requires monitor.observer.beta;
    requires monitor.statistics;
    requires monitor.persistence;
    requires monitor.rest;
}

顺便说一下,你可以使用JDep创建初始模块声明。 无论是自动或手动创建,在实际项目中,你应该验证依赖关系和API是否符合要求。 很可能随着时间的推移,一些快速修复会引入一些你想避免的关系。 现在解决它,或是挤压一些问题。
与以前非常相似,把它当作单一的模块,只是更多而已:

$ javac
    -d classes/monitor.observer
    ${source-files}
$ jar --create
    --file mods/monitor.observer.jar
    -C classes/monitor.observer .
 
# monitor.observer.alpha depends on monitor.observer,
# so we place 'mods', which contains monitor.observer.jar,
# on the module path
$ javac
    --module-path mods
    -d classes/monitor.observer.alpha
    ${source-files}
$ jar --create
    --file mods/monitor.observer.alpha.jar
    -C classes/monitor.observer.alpha .
 
# more of the same ... until we come to monitor,
# which once again defines a main class
$ javac
    --module-path mods
    -d classes/monitor
    ${source-files}
$ jar --create
    --file mods/monitor.jar
    --main-class monitor.Main
    -C classes/monitor .

恭喜,你已经掌握了基础知识! 你现在知道如何组织,声明,编译,打包和启动模块,并且了解了模块路径,可读性图和模块化JAR的作用。

原文:Code First Java 9 Module System Tutorial