[译]代码优先的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的作用。