免责声明
我绝对不提供任何*保证*,无论是对于本文档的准确性还是对于此处描述的软件的任何属性或功能。
请勿在关键情况或项目中使用此软件。
1. 简介与目标
biking.michael-simons.eu 是一个用于:
-
跟踪我的自行车活动
-
评估技术
-
学习知识
-
展示我的技能
关于该项目的信息,请参阅 此博客文章 作为起点。
我想要感谢所有在Spring生态系统中工作的人们,感谢他们做出的出色工作。同时也要特别感谢Gernot Starke博士和Peter Hruschka博士在2015年12月在慕尼黑举办的“掌握软件架构”研讨会,我不仅带走了一个不错的*CPSA-F*证书,还得到了非常有价值且实用的可以改善“我的”软件的模式。
上述博客文章使用了Nick Cave & The Bad Seeds的歌词(来自“Push The Sky Away”),而对于架构,我有以下引用:
简洁是一种伟大的美德,但它需要努力才能实现并需要教育来欣赏。更糟糕的是:复杂性更易销售。
— Edsger W. Dijkstra
1.1. 需求概述
biking2 的主要目的是跟踪自行车及其里程,以及将_Garmin_ 训练中心 XML (tcx) 文件转换为标准 GPS交换格式 (GPX) 文件,并以可访问的方式存储它们。
此外,biking2 用于研究和评估技术、模式和框架。功能需求足够简单,可以专注于质量目标。
-
存储自行车及其里程
-
将 tcx 文件转换为 GPX 文件,并提供一个轨迹库
-
在地图上展示这些轨迹,并提供一种将其嵌入到其他网页中的方式
-
通过图片展示骑行活动
-
可选的,几乎实时地跟踪骑行者
该应用程序只能处理一个具有写权限的用户。
大多数自行车爱好者难以理解“为什么需要多辆自行车?”,系统应能够为一个用户的2到10辆自行车之间的所有数据进行跟踪,存储每辆自行车每月的总里程。从这个持续的总数中,应该能够导出每月、每年和其他指标的所有里程,以便用户只需查看其里程表并输入数值即可。
该应用程序应该可以存储“无限数量”的轨迹。
图片应该来自 Daily Fratze,其源是所有标记为“Radtour”的图片。此外,用户应该能够提供“无限数量”的“图库图片”,并附有日期和简短描述。
1.2. 质量目标
编号 |
质量 |
动机 |
1 |
可理解性 |
功能需求足够简单,使得解决方案简单易懂,从而可以专注于学习新的Java 8特性、Spring Boot和AngularJS。 |
2 |
效率 |
收集里程数据应该是一件易如反掌的事情:读取里程表上的里程数并输入。 |
3 |
互操作性 |
应用程序应提供简单的API,允许新客户端访问。 |
4 |
吸引力 |
收集的里程应以易于理解的图表形式呈现。 |
5 |
可测试性 |
架构应该允许对所有主要构建块进行轻松测试。 |
1.3. 利益相关者
以下是该应用程序的最重要的人物角色清单:
角色/姓名 | 目标/界限 |
---|---|
开发者 |
想要了解使用Spring Boot和各种前端开发现代应用程序的开发人员,最好在后端使用*Java 8*。 |
自行车骑行者 |
寻找一种非Excel的自托管解决方案,用于跟踪他们的自行车和里程。 |
软件架构师 |
寻找_arc42_示例;希望从中获取日常工作的想法。 |
Michael Simons |
改进技能;想要在Spring Boot上撰写博客;寻找可以进行讲座的主题;需要一个可以尝试新的_Java 8_特性的项目。 |
2. 架构约束
对这个项目的少量约束体现在最终解决方案中。本部分展示了这些约束,以及如果适用的话,它们的动机。
2.1. 技术约束
约束 | 背景和/或动机 | |
---|---|---|
软件和编程约束 |
||
TC1 |
使用Java进行实现 |
应用程序应该是Java 8和Spring Boot的展示项目的一部分。接口(即API)应该是语言和框架无关的。客户端可以使用各种框架和语言来实现。 |
TC2 |
第三方软件必须可用于兼容的开源许可并可通过包管理器安装 |
感兴趣的开发人员或架构师应该能够检出源代码,编译和运行应用程序,而不会出现编译或安装依赖的问题。所有外部依赖应该可以通过操作系统的包管理器获得,或者至少通过一个安装程序获得。 |
操作系统约束 |
||
TC3 |
独立于操作系统的开发 |
应用程序应该可以在所有三个主要操作系统(Mac OS X、Linux和Windows)上编译。 |
TC4 |
可部署到Linux服务器 |
应用程序应该可以通过标准方式部署到基于Linux的服务器上。 |
硬件约束 |
||
TC5 |
友好的内存使用 |
内存可能会受限制(由于在共享主机上的可用性或部署到基于云的主机上)。如果部署到基于云的解决方案,每兆字节的内存都会产生成本。 |
2.2. 组织约束
约束 | 背景和/或动机 | |
---|---|---|
OC1 |
团队 |
Michael Simons |
OC2 |
时间表 |
从2014年初开始使用Spring Boot beta版本的原型,运行在Java 8早期访问版本上,第一个“发布”版本于2014年3月发布,与Java 8的最初版本一起发布。在可用时升级到最终的Spring Boot版本。 |
OC3 |
独立于IDE的项目设置 |
无需继续编辑器和IDE之争。该项目必须可以通过标准构建工具在命令行上编译。由于_OC2_只有一个IDE可以直接支持Java 8特性:NetBeans 8 beta和发布候选版。 |
OC4 |
配置和版本控制/管理 |
私有git存储库,包含完整的提交历史,并将公共主分支推送到GitHub,并链接到项目博客。 |
OC5 |
测试 |
使用JUnit来证明功能的正确性和集成测试,并使用JaCoCo来确保高达90%的测试覆盖率。 |
OC6 |
以开源许可发布 |
源代码,包括文档,应以Apache 2许可发布为开源。 |
2.3. 约定
约定 | 背景和/或动机 | |
---|---|---|
C1 |
架构文档 |
结构基于英文arc42-Template的第6.5版本 |
C2 |
编码约定 |
项目使用http://www.oracle.com/technetwork/java/codeconvtoc-136057.html[Java TM编程语言的代码约定]。这些约定通过Checkstyle进行强制执行。 |
C3 |
语言 |
英语。该项目及其对应的博客面向国际受众,因此整个项目应使用英语。 |
C4 |
命名约定 |
以下列出了一些命名约定,使用https://jqassistant.org[jQAssistant]进行检查和强制执行。 |
3. 系统范围和背景
本章描述了_biking2_的环境和背景:谁使用该系统,以及_biking2_依赖于哪些其他系统。
3.1. 业务背景
.png)
一个热爱骑行的人使用_biking2_来管理他的自行车、里程、轨迹,以及在游览等活动中拍摄的视觉记忆(即图片)。他还希望能够将自己的轨迹嵌入其他网站上,形成交互式地图。
Daily Fratze提供了一个根据特定主题标记的图片列表。_biking2_应该收集一个给定用户标记为“Theme/Radtour”的所有图片。
GPSBabel是一个用于以各种方式操作GPS相关文件的命令行实用程序。_biking2_使用它将TCX文件转换为GPX文件。GPSBabel完成了繁重的工作,而_biking2_会管理生成的文件。
用户可能想要在任意网站上嵌入(或者炫耀)轨迹。他只想在支持嵌入内容的网站上粘贴一个轨迹链接,从而嵌入具有给定轨迹的地图。
3.2. 技术背景
.png)
_biking2_分为2个主要组件:
API在支持的应用服务器上运行,使用嵌入式容器或外部容器。它通过操作系统进程与同一服务器上的GPSBabel通信。
与_Daily Fratze_的连接是基于http的RSS源。该源是分页的,并提供具有特定标签的*所有*图片,但当所有者决定添加数字到期时,较旧的图片可能不再可用。
此外,_biking2_为系统中存储的所有轨迹提供了一个http://oembed.com[oEmbed]接口。支持该协议的任意网站可以通过http请求嵌入内容,而无需自行处理轨迹或地图API。
前端由两个不同的组件实现,其中biking2::spa(单页应用程序)是其中的一部分。spa在任何现代Web浏览器中运行,并通过http与api通信。
业务接口 |
渠道 |
格式转换 |
系统进程,命令行接口 |
收集骑行图片 |
通过Internet的RSS源(http) |
可嵌入内容 |
通过Internet的oEmbed格式(http) |
业务功能API |
Internet(http) |
4. 解决方案策略
biking2 的核心是一个简单但功能强大的领域模型,基于一些实体,其中“Bike”和其“Milage”最为重要。
虽然以数据为中心,但该应用放弃了过多使用 SQL 来创建报告、摘要等,而是尝试使用 Java 8 的新特性,如流、lambda 和 map/reduce 函数来实现这一点。
使用 Spring Boot 构建应用是一个显而易见的选择,因为其中一个主要的quality goals是学习它。此外,将 Spring Boot 作为 Spring Framework 的“框架”,允许专注于业务逻辑。一方面,不需要理解复杂的 XML 配置,另一方面,所有构建模块仍然使用依赖注入组合在一起。
关于依赖注入和可测试性:所有注入应通过构造函数注入完成,当没有其他技术方法时才允许使用 setter 注入。这样一来,测试单元才能被正确实例化。否则,人们很容易忘记协作对象,甚至更糟糕的是:20 个被注入的属性可能没有问题,但具有 20 个参数的构造函数就会有问题。这希望能防止集中式的控制几乎应用程序的每个其他方面的“大类”。
Spring Boot 应用程序可以打包为单个的“fat jar”文件。自 Spring Boot 1.3 起,这些文件包含一个启动脚本,并且可以直接链接到标准 Linux 系统上的 /etc/init.d,满足了[TC4]。
通过使用 JSON 作为主要 API 的简单 http 协议实现互操作性。安全性不是此应用程序的主要焦点。它应该足够安全,以防止他人篡改数据,但保密性不是主要问题(即:密码可以明文传输在 http 上)。
内部单页应用程序将使用 AngularJS 实现。可部署的构件将包含此应用程序,因此无需为静态文件托管第二个 Web 服务器。
实时跟踪将使用 MQTT 协议,这是 Apache ActiveMQ 的一部分,并且由 Spring Messaging 提供了开箱即用的支持。
图形化不应在此处实现,而应使用 Highcharts 库。所有图表的配置应在服务器端计算。
注意:该项目的最初版本大量使用 Java 8 的流和 API 来计算统计数据(https://biking.michael-simons.eu/milages 下的所有内容)。 回到 2014 年和 2015 年,当 Java 8 刚推出时,它对学习该 API 很有帮助。 近 5 年后,思考了自己在数据库方面的历史,看了大量关于 SQL 的演讲后,我决定回到自己的起点。 现在所有的图表都是由专门的统计服务创建的,它基于 jOOQ 和类型安全的 SQL。
5. 构件视图
以 biking2.jar 的形式打包的应用程序包含三个主要部分中的两个(api 和 spa),如[Business Context]所示:

从这两个部分中,我们更详细地看一下 api。关于 AngularJS 1.2.x 应用程序结构的详细信息,请参阅其 开发者指南。
注意:为了符合 Java 编码风格指南,模块“bikingPictures”和“galleryPictures”位于 Java 包“bikingpictures”和“gallerypictures”中。
5.1. 白盒 biking2::api
以下图表显示了系统的主要构建模块及其相互依赖关系:

我使用“功能分解”来分离责任。api 的各个部分都封装在自己的组件中,表示为 Java 包。
所有组件都依赖于标准的 JPA EntityManager,其中一些依赖于本地文件存储。我不会详细介绍这些黑盒子。
包含的黑盒子:
管理自行车,添加每月里程,计算统计数据并生成图表。 |
|
上传轨迹(TCX 文件),将其转换为 GPX,并提供 oEmbed 接口。 |
|
管理各种行程。 |
|
用于创建新位置并通过 WebSocket 通过 stomp 实时在 websockets 上提供它们的 MQTT 和 STOMP 接口。 |
|
从 Daily Fratze 的 RSS feed 中读取自行车图片,并为其提供 API。 |
|
上传和管理任意图片。 |
|
提供统计数据的 API。 |
接口:
接口 |
描述 |
bikes Api |
包含用于读取、添加和注销自行车以及为单个自行车添加里程的所有方法的 REST API。 |
charts |
用于检索完全设置的图表定义的方法。 |
tracks Api |
上传和读取 TCX 文件的 REST API。 |
trips Api |
添加新行程的 REST API。 |
oEmbed |
基于 HTTP 的 oEmbed 接口,将 URL 解析为可嵌入内容。 |
实时位置 |
WebSocket / STOMP 接口,用于发布新位置的通知。 |
实时位置更新 |
MQTT 接口,用于接收来自 MQTT 兼容系统(如 OwnTracks)的位置更新。 |
RSS feed reader |
需要一个 Daily Fratze OAuth 令牌来访问包含自行车图片的 RSS feed。 |
galleryPictures Api |
用于上传和读取任意图片文件的 REST API(与自行车相关的图片)。 |
5.1.1. bikes(Blackbox)
意图/责任:
/* * Copyright 2016 michael-simons.eu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. / /* <!-- tag::intent[] -→ bikes provides the external API for reading, creating and manipulating bikes and their milages as well as computing statistics and generating charts. <!-- end::intent[] -→ */ package ac.simons.biking2.bikes;
接口:
接口 |
描述 |
REST 接口 /api/bikes/* |
包含所有操作自行车和它们里程的方法。 |
REST 接口 /api/charts/* |
包含用于生成图表的所有方法。 |
文件:
bikes 模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.bikes 中。
(更多内容请继续阅读……)
5.1.2. tracks(Blackbox)
意图/责任:
/* * Copyright 2016 michael-simons.eu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. / /* <!-- tag::intent[] -→ tracks manages file uploads (TCX files), converts them to GPX files and computes their surrounding rectangle (envelope) using GPSBabel. It also provides the oEmbed interface that resolves URLS to embeddable tracks. <!-- end::intent[] -→ */ package ac.simons.biking2.tracks;
接口:
接口 |
描述 |
REST 接口 /api/tracks/* |
包含操作轨迹的所有方法。 |
/api/oembed |
将轨迹 URL 解析为可嵌入轨迹(内容)。 |
/tracks/* |
可嵌入轨迹内容。 |
文件:
tracks 模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.tracks 中。
5.1.3. trips(Blackbox)
意图/责任:
/* * Copyright 2016 michael-simons.eu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. / /* <!-- tag::intent[] -→ trips manages distances that have been covered on single days without relationships to bikes. <!-- end::intent[] -→ */ package ac.simons.biking2.trips;
接口:
接口 |
描述 |
REST 接口 /api/trips/* |
包含操作行程的所有方法。 |
文件:
trips 模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.trips 中。
5.1.4. locations(Blackbox)
意图/责任:
locations 存储具有时间戳的位置,并为最近 30 分钟的位置提供访问。
接口:
接口 |
描述 |
REST 接口 /api/locations/* |
用于检索最近 30 分钟内的所有位置。 |
WebSocket / STOMP 主题 /topic/currentLocation |
获取新位置通知的接口。 |
MQTT 接口 |
监听通过 MQTT 发送的新位置,格式为 OwnTracks 格式。 |
文件:
locations 模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.tracker 中。该模块通过 ac.simons.biking2.config.TrackerConfig 进行配置。
5.1.5. bikingPictures(Blackbox)
意图/责任:
/* * Copyright 2016 michael-simons.eu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. / /* <!-- tag::intent[] -→ bikingPictures is used for regularly checking a RSS feed from Daily Fratze collecting new images and storing them locally. It also provides an API for getting all collected images. <!-- end::intent[] -→ */ package ac.simons.biking2.bikingpictures;
接口:
接口 |
描述 |
RSS Feed reader |
提供访问 Daily Fratze RSS Feed。 |
Image reader |
提供访问 Daily Fratze 上托管的图片。 |
REST 接口 /api/bikingPictures/* |
包含访问自行车图片的所有方法。 |
文件:
bikingPictures 模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.bikingpictures 中。
(更多内容请继续阅读……)
5.1.6. galleryPictures(Blackbox)
意图/责任:
/* * Copyright 2016 michael-simons.eu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. / /* <!-- tag::intent[] -→ galleryPictures manages file uploads (images). It stores them locally and provides an RSS interface for getting metadata and image data. <!-- end::intent[] -→ */ package ac.simons.biking2.gallerypictures;
接口:
接口 |
描述 |
REST 接口 /api/galleryPictures/* |
包含添加和读取任意图片的所有方法。 |
5.1.7. statistics(Blackbox)
StatisticService 是一个基于数据库的服务,使用 jOOQ 的 DSLContext 实例创建 SQL。 这些 SQL 不是生成的 SQL,而是手工编写的。 jOOQ 仅用于以类型安全的方式进行编写。
在构建过程中,jOOQ 读取了一个临时数据库,该数据库由 SQL 迁移脚本创建,并提供了几个 Java 类来表示模式。 这些类与 DSL 一起用于编写 SQL。
语句大量使用分析函数。 这是必要的,因为最初决定存储里程不是每月存储的行驶里程数,而是车辆上的累积里程。 因此,需要计算每月的值。
这是一个用于当前年份统计的查询示例:
WITH mm AS ( SELECT bikes.name, bikes.color, milages.recorded_on, (lead(milages.amount) OVER (PARTITION BY bikes.id ORDER BY milages.recorded_on) - milages.amount) value FROM bikes JOIN milages ON milages.bike_id = bikes.id ORDER BY milages.recorded_on ASC ) SELECT mm.name, mm.color, (EXTRACT(MONTH FROM mm.recorded_on) - 1) idx, mm.value, sum(mm.value) OVER (PARTITION BY mm.recorded_on) total FROM mm WHERE ( mm.value IS NOT NULL AND EXTRACT(YEAR FROM mm.recorded_on) >= EXTRACT(YEAR FROM DATE '2019-01-01') ) ORDER BY mm.name ASC
该模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.statistics 中。
(还有一些内容尚未翻译,需要我继续吗?) ==== statistics(白盒)
StatisticService 是一个基于数据库的服务,使用 jOOQ 的 DSLContext 实例创建 SQL。 这些 SQL 不是生成的 SQL,而是手工编写的。 jOOQ 仅用于以类型安全的方式进行编写。
在构建过程中,jOOQ 读取了一个临时数据库,该数据库由 SQL 迁移脚本创建,并提供了几个 Java 类来表示模式。 这些类与 DSL 一起用于编写 SQL。
语句大量使用分析函数。 这是必要的,因为最初决定存储里程不是每月存储的行驶里程数,而是车辆上的累积里程。 因此,需要计算每月的值。
这是一个用于当前年份统计的查询示例:
WITH mm AS ( SELECT bikes.name, bikes.color, milages.recorded_on, (lead(milages.amount) OVER (PARTITION BY bikes.id ORDER BY milages.recorded_on) - milages.amount) value FROM bikes JOIN milages ON milages.bike_id = bikes.id ORDER BY milages.recorded_on ASC ) SELECT mm.name, mm.color, (EXTRACT(MONTH FROM mm.recorded_on) - 1) idx, mm.value, sum(mm.value) OVER (PARTITION BY mm.recorded_on) total FROM mm WHERE ( mm.value IS NOT NULL AND EXTRACT(YEAR FROM mm.recorded_on) >= EXTRACT(YEAR FROM DATE '2019-01-01') ) ORDER BY mm.name ASC
该模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.statistics 中。
5.2. 构建块 - 二级
5.2.1. bikes(白盒)

BikeRepository 是基于 Spring Data JPA 的 BikeEntities 仓库。 BikeController 和 ChartsController 访问它,用于检索和存储 BikeEntity 实例并提供外部接口。
包含的黑盒:
highcharts |
包含用于在服务器端生成 Highcharts 配置和定义的逻辑。 |
5.2.2. tracks(白盒)

TrackRepository 是基于 Spring Data JPA 的 TrackEntities 仓库。 TracksController 和 OembedController 访问它,用于检索和存储 TrackEntity 实例并提供外部接口。
包含的黑盒:
gpx |
用于解析 GPX 文件的生成 JAXB 类。由 TracksController 使用,用于检索新轨迹的周围矩形(envelope)。 |
5.2.3. trips(白盒)

AssortedTripRepository 是基于 Spring Data JPA 的 AssortedTripEntities 仓库。 TripsController 访问它,用于检索和存储 TrackEntity 实例并提供外部接口。
5.2.4. locations(白盒)

位置通过名为 LocationRepository 的 Spring Data JPA 仓库存储和读取。仅通过 LocationService 访问此仓库。LocationService 通过 SimpMessagingTemplate 提供给连接的客户端近实时更新,并且 LocationController 使用该服务提供了访问在过去 30 分钟内创建的所有位置的功能。
新位置通过 REST 接口或 MQTT 频道上的 MessageListener 由服务创建。
5.2.5. bikingPictures(白盒)

名为 BikingPicturesRepository 的 Spring Data JPA 仓库用于所有对 BikingPictureEntities 的访问,外部 REST API 用于读取图片由 BikingPicturesController 实现。RSS feed 通过 FetchBikingPicturesJob 读取,使用 JAXBContext "rss"。提供给作业的图像文件的 URL 可能受到各种方式的保护。
包含的黑盒:
rss |
用于解析 RSS 订阅的生成 JAXB 类。由 FetchBikingPicturesJob 使用,用于读取 RSS 订阅的内容。 |
5.2.6. galleryPictures(白盒)

GalleryPictureRepository 是基于 Spring Data JPA 的 GalleryPictureEntities 仓库。GalleryController 访问它,用于检索和存储 GalleryPictureEntity 实例并提供外部接口。
5.2.7. statistics(白盒)
StatisticService 是一个基于数据库的服务,使用 jOOQ 的 DSLContext 实例创建 SQL。 这些 SQL 不是生成的 SQL,而是手工编写的。 jOOQ 仅用于以类型安全的方式进行编写。
在构建过程中,jOOQ 读取了一个临时数据库,该数据库由 SQL 迁移脚本创建,并提供了几个 Java 类来表示模式。 这些类与 DSL 一起用于编写 SQL。
语句大量使用分析函数。 这是必要的,因为最初决定存储里程不是每月存储的行驶里程数,而是车辆上的累积里程。 因此,需要计算每月的值。
这是一个用于当前年份统计的查询示例:
WITH mm AS ( SELECT bikes.name, bikes.color, milages.recorded_on, (lead(milages.amount) OVER (PARTITION BY bikes.id ORDER BY milages.recorded_on) - milages.amount) value FROM bikes JOIN milages ON milages.bike_id = bikes.id ORDER BY milages.recorded_on ASC ) SELECT mm.name, mm.color, (EXTRACT(MONTH FROM mm.recorded_on) - 1) idx, mm.value, sum(mm.value) OVER (PARTITION BY mm.recorded_on) total FROM mm WHERE ( mm.value IS NOT NULL AND EXTRACT(YEAR FROM mm.recorded_on) >= EXTRACT(YEAR FROM DATE '2019-01-01') ) ORDER BY mm.name ASC
该模块及其所有依赖项都包含在 Java 包 ac.simons.biking2.statistics 中。
(还有一些内容尚未翻译,需要我继续吗?)
6. 运行时视图
用户与 biking2 的交互和错误处理都相当基本和简单。
我挑选了两个情况,实际的运行时视图非常有趣:
6.1. 创建新的轨道

6.2. 从 Daily Fratze 获取骑行图片

7. 部署视图

节点 / 构件 |
描述 |
biking2开发 |
biking2 开发的地方,标准计算机装有 JDK 8、Maven 和 GPSBabel。 |
uberspace主机 |
在 Uberspace 上的主机,biking2.jar 在 Server JRE 内运行,内存使用受限。 |
biking2.jar |
一个“fat jar”,包含所有的Java依赖项和加载器,使得该Jar文件既可作为jar文件运行,也可作为服务脚本(在Linux主机上)运行。 |
浏览器 |
用于访问 AngularJS biking2 单页面应用程序的最新浏览器。所有主要的浏览器(Chrome、Firefox、Safari、IE / Edge)都应该可以使用。 |
8. 概念
8.1. 领域模型
biking2 是一个以数据为中心的应用程序,因此一切都基于实体关系图(ER-Diagram):

名称 |
描述 |
bikes |
存储自行车信息。包含自行车购买和报废日期、可选的链接、图表颜色,以及创建行的审核列。 |
milages |
存储自行车的里程信息(何时、多少)。 |
tracks |
存储记录和上传的GPS轨迹及其可选描述。对于每一天,轨迹名称必须是唯一的。minlat、minlon、maxlat 和 maxlon 列存储轨迹的封闭矩形。type 列受限于 "biking" 和 "running"。 |
assorted_trips |
存储日期和当天的行驶距离。允许一天内有多个距离。 |
locations |
存储给定时间戳的任意位置(基于纬度和经度),可选包含描述信息。 |
biking_pictures |
存储从 Daily Fratze 收集的图片,包括其原始发布日期、唯一的外部ID以及图片原始出现页面的链接。 |
gallery_pictures |
存储用户上传的所有图片,包括描述和拍摄日期。filename 列包含一个经过计算的单个文件名,不包含路径信息。 |
这些表格映射到以下领域模型:

名称 |
描述 |
BikeEntity |
自行车是在特定日期购买的,可以报废。它有一个颜色和一个可选的指向任意网站的链接。它可能有或没有记录的里程。有一些重要的功能,请参阅 BikeEntity 上的重要业务方法 |
MilageEntity |
里程是自行车的一部分。每辆自行车每个月可以记录一次里程。里程是其记录日期、数量和自行车的组合。 |
TrackEntity |
tracks 内容的表示。类型是一个枚举。值得注意的公共操作是 getPrettyId(基于实例ID计算一个 "漂亮" 的ID)和 getTrackFile(生成对传递的数据存储目录中GPS轨迹文件的引用)。 |
BikingPictureEntity |
用于处理从 Daily Fratze 收集的图片。BikingPictureEntity 在构造时解析图像链接并检索唯一的外部ID。 |
GalleryPictureEntity |
用于处理用户上传的图片的实体。prePersist 方法在插入数据库之前填充 createdAt 属性。 |
AssortedTripEntity |
此实体捕获在特定日期行驶的距离,并可用于跟踪未在此应用程序中存储的自行车的行程等。 |
LocationEntity |
在跟踪器模块中用于处理实时位置。 |
名称 |
描述 |
decommission |
在给定日期报废自行车。 |
addMilage |
为给定日期添加新的里程并返回它。只有当日期晚于上次记录里程的日期,并且数量大于上次里程时,才会添加里程。 |
getPeriods |
获取记录了里程的所有月份期间。 |
getMilage |
获取此自行车的总里程。 |
getLastMilage |
获取最后记录的里程。在大多数情况下与 getMilage 相同。 |
getMilageInPeriod |
获取给定期间的里程。 |
getMilagesInYear |
获取一年内的所有里程作为数组(按月份)。 |
getMilageInYear |
获取给定年份的总里程。 |
8.2. 持久化
biking2 使用 H2 数据库存储关系数据,并使用文件系统存储二进制图像文件和大型ASCII文件(特别是所有GPS文件)。
在开发和生产过程中,H2 数据库是保留的,不是基于内存的。该文件的位置通过 biking2.database-file 属性进行配置,默认值为 ./var/dev/db/biking-dev,相对于 VM 的工作目录。
所有对数据库的访问都通过 JPA 使用 Hibernate 作为提供程序进行。请参阅 [Domain Models] 查看应用程序中使用的所有实体。
JPA 实体管理器不是直接访问的,而是仅通过 Spring Data JPA 提供的功能,即仅通过存储库进行访问。
所有存储为文件的数据存储在相对于 biking2.datastore-base-directory 的位置,默认为 ./var/dev。内部包括 3 个目录:
-
bikingPictures:包含从 Daily Fratze 收集的所有图片
-
galleryPictures:包含所有上传的图片
-
tracks:包含上传的GPS数据和将TCX文件转换为GPX文件的结果
8.3. 用户界面
biking2 默认用户界面是打包在最终构件中的 JavaScript 单页应用,使用 Angular JS 以及非常基础的 Bootstrap 模板编写。
要使用实时位置更新界面,请选择其中一个众多的 MQTT 客户端。
还有一个用 Java 编写的第二用户界面,称为 bikingFX。
8.4. JavaScript 和 CSS 优化
JavaScript 和 CSS 依赖项尽可能地通过 Maven 依赖项进行管理,以 webjars 的形式进行,无需使用 brew、npm、bower 等。
此外,biking2 使用 wro4j 与一个小的 Spring Boot Starter 来优化 JavaScript 和 CSS web 资源。
wro4j 提供了如下模型:
<?xml version="1.0" encoding="UTF-8"?> <!-- Copyright 2016 michael-simons.eu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <!-- tag::wro4jconcept[] --> <groups xmlns="http://www.isdc.ro/wro"> <!-- end::wro4jconcept[] --> <!-- OSM / Openlayer Dependencies --> <group name="osm"> <css>/webjars/openlayers/@openlayers.version@/theme/default/style.css</css> <js minimize="false">/webjars/openlayers/@openlayers.version@/OpenLayers.js</js> <js>/js/OpenStreetMap.js</js> <js>/js/OpenLayersDeprecated.js</js> <js>/js/OpenLayersConfig.js</js> </group> <!-- Stripped down dependencies for the the embedded tracks --> <group name="biking2-embedded"> <group-ref>osm</group-ref> <js minimize="false">/webjars/jquery/@jquery.version@/jquery.min.js</js> <js minimize="false">/webjars/angularjs/@angularjs.version@/angular.min.js</js> <js>/js/directives.js</js> </group> <!-- tag::wro4jconcept[] --> <!-- Dependencies for the full site --> <group name="biking2"> <group-ref>osm</group-ref> <css minimize="false">/webjars/bootstrap/@bootstrap.version@/css/bootstrap.min.css</css> <css>/css/icons.css</css> <css>/css/stylesheet.css</css> <js minimize="false">/webjars/jquery/@jquery.version@/jquery.min.js</js> <js minimize="false">/webjars/bootstrap/@bootstrap.version@/js/bootstrap.min.js</js> <js minimize="false">/webjars/momentjs/@momentjs.version@/min/moment-with-locales.min.js</js> <js minimize="false">/webjars/angular-file-upload/@angular-file-upload.version@/angular-file-upload-html5-shim.min.js</js> <js minimize="false">/webjars/angularjs/@angularjs.version@/angular.min.js</js> <js minimize="false">/webjars/angularjs/@angularjs.version@/angular-route.min.js</js> <js minimize="false">/webjars/angularjs/@angularjs.version@/angular-sanitize.min.js</js> <js minimize="false">/webjars/angular-file-upload/@angular-file-upload.version@/angular-file-upload.min.js</js> <js minimize="false">/webjars/angular-ui-bootstrap/@angular-ui-bootstrap.version@/ui-bootstrap.min.js</js> <js minimize="false">/webjars/angular-ui-bootstrap/@angular-ui-bootstrap.version@/ui-bootstrap-tpls.min.js</js> <js minimize="false">/webjars/highcharts/@highcharts.version@/highcharts.js</js> <js minimize="false">/webjars/highcharts/@highcharts.version@/highcharts-more.js</js> <js minimize="false">/webjars/sockjs-client/@sockjs-client.version@/sockjs.min.js</js> <js minimize="false">/webjars/stomp-websocket/@stomp-websocket.version@/stomp.min.js</js> <js>/js/ansi_up.js</js> <js>/js/app.js</js> <js>/js/controllers.js</js> <js>/js/directives.js</js> </group> </groups> <!-- end::wro4jconcept[] -->
该模型文件经过 Maven 构建进行过滤,版本占位符将被替换,并且所有资源,无论是在 webjars 中还是文件系统中,都将作为 biking.css 和 biking.js 可用。
如何优化、缩小或以其他方式处理这些文件取决于 wro4js 的配置,但在开发过程中可以关闭缩小功能。
8.5. 事务处理
biking2 依赖于 Spring Boot 创建处理 JPA EntityManager 中本地事务所需的所有 bean。biking2 不支持分布式事务。
8.6. 会话处理
biking2 仅提供无状态的公共 API,没有会话处理。
8.7. 安全性
biking2 仅通过 HTTP 基本访问身份验证 为其 API 端点提供安全性,并在 MQTT 模块的情况下使用 MQTT 的默认安全模型。可以通过在嵌入式 Tomcat 容器中运行应用程序或配置 SSL 代理来提高安全性。
对于此处管理的数据类型,保持应用程序简单是一种商定的折衷。另请参阅 [Safety]。
8.8. 安全性
系统中没有危及生命的方面。
8.9. 通信和集成
biking2 在与应用程序相同的 VM 上使用内部 Apache ActiveMQ 代理来提供 STOMP 通道和 MQTT 传输。此代理是不稳定的,消息在应用程序重新启动期间不会持久化。
8.10. 合理性和有效性检查
通过在表示 [Domain Models] 的类上使用 JSR-303 注解来检查数据类型和范围。这些类直接绑定到外部 REST 接口。
有三个重要的业务检查:
-
报废的自行车无法修改(即它们不能有新的里程):在 BikesController 中检查。
-
对于每个唯一的月份,只能向自行车添加一次里程。在 BikeEntity 中进行检查。
-
新的里程必须大于上一个里程。也在 BikeEntity 中进行检查。
8.11. 异常/错误处理
错误处理不一致的数据(与数据模型约束相关)以及 validation 失败将被映射到 HTTP 错误。这些错误由前端控制器代码处理。技术错误(硬件、数据库等)未经处理,可能导致应用程序失败或数据丢失。
8.12. 日志记录、跟踪
Spring Boot 默认将日志配置为标准输出。在这方面,默认的 configuration 没有更改,因此所有框架日志(特别是 Spring 和 Hiberate)都以标准格式输出到标准输出,并且可以通过操作系统特定方式进行抓取或忽略。
所有业务组件都使用 Simple Logging Facade for Java(SLF4J)。日志记录的实际配置通过 Spring Boot 进行配置。没有手动包含特殊实现,而是 biking2 通过 spring-boot-starter-logging 间接依赖于它。
8.13. 可配置性
Spring Boot 提供了大量的配置选项,以下是配置 Spring Boot 和可用的启动器的主要选项:常见应用程序属性。
默认配置位于 src/main/resources/application.properties。在开发过程中,这些属性与 src/main/resources/application-dev.properties 合并。可以通过系统环境或当前 JVM 目录中的 application-*.properties 添加额外的属性。
在测试期间,可以使用额外的 application-test.properties 来添加或覆盖其他属性或值。
以下是 biking2 的特定属性:
属性 |
默认值 |
描述 |
biking2.color-of-cumulative-graph |
000000 |
累积线图的颜色 |
biking2.dailyfratze-access-token |
n/a |
Daily Fratze 的 OAuth 访问令牌 |
biking2.datastore-base-directory |
${user.dir}/var/dev |
存储文件的目录(跟踪和图像) |
biking2.fetch-biking-picture-cron |
0 0 */8 * * * |
配置 FetchBikingPicturesJob 的 Cron 表达式 |
biking2.home.longitude |
6.179489185520004 |
家庭坐标的经度 |
biking2.home.latitude |
50.75144902272457 |
家庭坐标的纬度 |
biking2.connector.proxyName |
n/a |
如果 biking2 在代理后运行,则代理的名称 |
biking2.connector.proxyPort |
80 |
如果 biking2 在代理后运行,则代理的端口 |
biking2.gpsBabel |
/opt/local/bin/gpsbabel |
GPSBabel 二进制文件的完全限定路径 |
biking2.scheduled-thread-pool-size |
10 |
作业池的线程池大小 |
biking2.tracker.host |
localhost |
跟踪器(MQTT 通道)应监听的主机 |
biking2.tracker.stompPort |
2307 |
STOMP 端口 |
biking2.tracker.mqttPort |
4711 |
MQTT 端口 |
biking2.tracker.username |
${security.user.name} |
MQTT 通道的用户名 |
biking2.tracker.password |
${security.user.password} |
MQTT 通道的密码 |
biking2.tracker.device |
iPhone |
OwnTracks 设备的名称 |
8.14. 国际化
仅支持英语。前端没有进行国际化的钩子,也没有创建国际化的计划。
8.15. 迁移
biking2 替代了基于 Sinatra 框架的 Ruby 应用程序。数据存储在 SQLite 数据库中,已手动迁移到 H2 数据库。
8.16. 可测试性
该项目包含标准 Maven 项目的 JUnit 测试。目前已经覆盖了 >95% 的代码。在构建期间必须执行测试,不应跳过测试。
8.17. 构建管理
应用程序可以使用 Maven 构建,无需在 Maven 之外的外部依赖项。然而,要运行所有测试,必须在路径中设置 gpsbabel。
9. 设计决策
9.1. 使用GPSBabel将TCX转换为GPX格式
流行的JavaScript地图框架提供了在地图上轻松包含GPX数据几何信息的方式。然而,大多数Garmin设备记录的轨迹数据是以TCX格式保存的,因此我需要一种将TCX转换为GPX的方法。这两种格式都相对简单,在GPX的情况下有着良好的文档支持。
-
转换应该能够毫无问题地处理带有单个轨迹、圈数和额外点的TCX文件。
-
此项目的重点是为AngularJS单页应用程序开发现代化的应用程序后端,而不是解析GPX数据。
-
使用外部的、非基于Java的工具会让那些只想尝试该应用程序的人感到困难。
-
虽然文档良好,但这两种文件类型可能包含各种信息(路线、轨迹、航点),这使得解析变得困难。
-
自己编写转换器
-
使用现有的GPS数据多功能工具:GPSBabel
GPSBabel可以在流行的GPS接收器(如Garmin或Magellan)与Google Earth或Basecamp等地图程序之间转换航点、轨迹和路线。它支持数百种GPS接收器和程序。它还具有对这些数据的强大操作工具,比如过滤重复点或简化轨迹。自2001年首次创建以来,已经被下载和使用了数千万次,因此它是稳定且值得信赖的。
biking2 使用GPSBabel来处理与GPS相关的繁重工作。该项目包含了一个README文件,说明必须安装GPSBabel。GPSBabel可以通过Windows安装程序在Windows上安装,在大多数Linux系统中可以通过官方软件包管理器安装。在OS X上,可以通过MacPorts或Homebrew获取。
9.2. 使用本地文件存储图像和轨迹数据
biking2 需要存储“大”对象:图像数据(骑行和画廊图片)以及轨迹数据。
-
使用类似S3的云存储
-
使用本地文件系统
我选择了本地文件系统,因为我不想花费太多精力评估云服务。如果要在基于云的设置中运行 biking2,则必须对当前使用的本地文件系统进行抽象化处理。
9.3. 使用以数据库为中心的方法
在开始阶段,biking2 并不是以数据库为中心的。 Hibernate实体已经建模,数据库已经使用Hibernates自动DDL进行了设置。 尽管没有使用分析功能,但计算是在内存中完成的。
采用像https://speakerdeck.com/michaelsimons/live-with-your-sql-fetish-and-choose-the-right-tool-for-the-job["Live with your SQL-fetish and choose the right tool for the job"]中描述的以数据库为中心的方法。
在2019年末,这个决策已经被实施。 引入了Flyway来通过SQL脚本创建表格和其他迁移。 基于实际受控的数据库,我们生成了一个jOOQ模式,所有用于计算统计信息的SQL生成都在这个模式上完成。 要了解更多信息,请查看[statistics (Whitebox),统计模块的白盒视图]。
10. 质量场景
10.1. 质量树

10.2. 评估场景
通过在开发和构建过程中使用 JaCoCo ,确保代码覆盖率至少达到95%。
架构应该设计成算法依赖于外部服务时可以进行测试而无需实际拥有该外部服务。也就是说:所有外部依赖应该是可模拟的。
例如:FetchBikingPicturesJob 需要一个包含RSS源的资源。检索资源和解析资源至少是两个不同的任务。通过一个单独的类 DailyFratzeProvider 获取资源可以使得对实际解析的测试独立于HTTP连接,因此相对简单。
11. 技术风险
biking2 现在已经运行了将近2年,根据我的使用场景,该架构不包含已知的风险。
有可能由于虚拟机意外关闭(即操作系统或硬件故障),导致H2数据库损坏。通过定期备份序列化的数据库文件来降低这种风险。
12. 术语表
术语 |
同义词 |
描述 |
AngularJS |
AngularJS 是一个由Google和个人开发者、公司共同维护的开源Web应用程序框架,用于解决开发单页应用程序中遇到的许多挑战。 |
|
Apache ActiveMQ |
Apache ActiveMQ 是一个用Java编写的开源消息代理。 |
|
Apache License |
Apache License 是一种宽松的开源许可证,专门为自由软件设计。 |
|
Bootstrap |
Bootstrap 是用于在Web上开发响应式、移动优先项目的最流行的HTML、CSS和JS框架。 |
|
Checkstyle |
Checkstyle 是一个开发工具,用于帮助程序员编写符合编码标准的Java代码。它自动化了检查Java代码的过程,省去了人类进行这项乏味(但重要)任务的时间。这使得它非常适合希望强制执行编码标准的项目。 |
|
Daily Fratze |
df, DF, DailyFratze |
一个在线社区,用户可以每天上传一张自己的照片(一张自拍照,但在这个网站上称为自拍照之前已经存在)。 |
Fat Jar |
将Java应用程序打包成一个单独的Jar文件,其中包含所有依赖项,可以是重新打包的,也可以与特殊的类加载器一起放在其原始Jar文件中。 |
|
Flyway |
Flyway 是一个SQL中心化的工具,用于以受控的方式应用数据库迁移。 |
|
Gallery picture |
由小时手动提供的照片,除了自动从 Daily Fratze 收集的照片之外。 |
|
Garmin |
Garmin 开发用于全球定位系统的消费、航空、户外、健身和海洋技术。 |
|
GPSBabel |
GPSBabel 是一个命令行实用程序,用于在流行的GPS接收器(如Garmin或Magellan)与Google Earth或Basecamp等地图程序之间转换航点、轨迹和路线。 |
|
GPX |
GPS Exchange Format |
GPX,或GPS交换格式,是一个设计用于软件应用程序的常见GPS数据格式的XML模式。它可用于描述航点、轨迹和路线。 |
JaCoCo |
JaCoCo 是一个代码覆盖库,可以在NetBeans中非常容易地使用。 |
|
Java 8 |
JDK 8 |
Java编程语言 的第八个版本,也是第一个支持Lambda表达式的功能范式。 |
jOOQ |
Java面向对象查询:jOOQ 是一个用于从Java编写类型安全SQL的流畅DSL的框架。 |
|
JUnit |
[JUnit](http://junit.org/junit5/) 是一个编写可重复测试的简单框架。它是用于单元测试框架的xUnit架构的一个实例。 |
|
MQTT |
MQ Telemetry Transport |
MQTT 是一种基于发布-订阅的“轻量级”消息传递协议,用于在TCP/IP协议之上使用。 |
NetBeans |
NetBeans 是一个免费的开源IDE,将现代开发的各个部分组合在一起。 |
|
OAuth |
OAuth 是一种授权的开放标准。OAuth为客户端应用程序提供了代表资源所有者访问服务器资源的“安全委托访问”。 |
|
oEmbed |
oEmbed 协议是允许第三方网站上的URL嵌入表示的简单轻量级格式。 |
|
RSS |
Rich Site Summary, Really Simple Syndication |
RSS 使用一系列标准Web feed格式发布频繁更新的信息:博客文章、新闻标题、音频、视频。 |
SLF4J |
Simple Logging Facade for Java |
Simple Logging Facade for Java (SLF4J) 用作各种日志框架(如java.util.logging、logback、log4j)的简单外观或抽象,允许最终用户在部署时插入所需的日志框架。 |
SPA |
spa |
单页应用程序 |
Spring Boot |
Spring Boot 让创建独立的、生产级别的基于Spring的应用程序变得轻而易举,只需“直接运行”即可。 |
|
Spring Data JPA |
Spring Data JPA 是Spring Data系列的一部分,使得可以轻松实现基于JPA的存储库。 |
|
STOMP |
The Simple Text Oriented Messaging Protocol |
STOMP 提供了一个可互操作的Wire格式,因此STOMP客户端可以与任何STOMP消息代理进行通信,为多种语言、平台和代理之间提供了易用和广泛的消息互操作性。 |
TCX |
训练中心XML(TCX)是在2007年作为Garmin的Training Center产品的一部分引入的数据交换格式。 |
关于这个模板
|
© 此文档使用了来自 arc42架构模板 的材料,可在 http://github.com/arc42 自由获取。 这些材料是开源的,并根据知识共享署名-相同方式共享4.0许可提供。它不带任何担保。使用需谨慎。 arc42及其结构由Peter Hruschka博士和Gernot Starke博士创建。 AsciiDoc版本由Markus Schärtel和Jürgen Krey发起,由Ralf Müller和Gernot Starke完成和维护。 |
Appendix A: API
自行车
列出自行车
使用 GET 请求将列出所有服务的自行车。
请求参数
响应结构
示例请求
示例响应
创建自行车
使用 POST 请求创建新自行车。
请求结构
示例请求
示例响应
为自行车添加里程
自行车在特定日期管理其总里程。为了让用户更方便,不需要计算差值,用户可以输入其自行车上的里程表显示的里程或其他信息。
使用 POST 请求将新里程添加到指定自行车。
路径参数
请求结构
示例请求
示例响应
为自行车添加故事
自行车可以有一个关联的故事,比如它们是如何组装的等等。
使用 PUT 请求将新故事更新到给定自行车上。
路径参数
请求结构
示例请求
示例响应
响应是一个更新的 bike。
删除故事
空的 PUT 请求将删除自行车的故事:
旅行
为了跟踪所有里程,包括未花费在持久性自行车上的时间,用户可以存储各种各样的旅行。一次旅行是指在某个特定日期行驶的距离。
创建旅行
使用 POST 请求创建新旅行。
请求结构
示例请求
响应结构
示例响应
横幅
应用程序在日志中启动时提供一个漂亮的横幅。不提供相应的API将是一种遗憾。要检索ASCII艺术横幅,只需使用以下API: