原文作者:Grzegorz Skorupa
原文地址:https://dzone.com/articles/minimalistic-cms-microservice-for-java
Cricket 是一个用来快速创建 Java 微服务的平台。它在最新的 “Microsite” 发布版里面提供了一套现成的功能,让我们可以很轻松地部署 CMS (Content Management Server,内容管理服务器)或其他 Web 应用。我们也可以在更大的应用的前端或后端里用到这一平台。
Cricket 有着模块化、事件驱动式的架构,可用于构建在 Raspberry PI Zero 等平台上运行的微型服务,也能利用云系统的底层设备部署一个分布式的系统。
本文便会展示使用该平台构建一个简单的 CMS 来管理网站的方式。我们还会展示 Docker Hub 自带的 Docker 容器的使用方法,还会在末尾部分介绍直接在 Java 环境里面运行这一平台的方法。
Java 开发者面临着一个艰难的选择。在运行网站或者 Web 应用的时候,如果要一个快速的、轻量级的解决方案的话,那么面向 Java 的 Web 及门户(portal)的开发平台就会因显得规模过大、设计过度,且消耗系统资源过高而备受诟病。也因为这点,我们会经常去跟集合了很多门技术的系统打交道,其中 Java 比较可能用在后端。
这一多种技术的组合既可以很完美地实现目标,也可以为开发者带来新的挑战,从而丰富自身的知识。然而,随着技术的种类以及系统复杂度的增加,扩增技术栈、更改功能或是向团队引入新人所带来的风险还有问题也会越来越多。
微服务概念的出现让我们很自然地产生了对快速构建原型和减少系统资源需求的期望,而 Cricket 微网站平台的出现便在某种意义上响应了这种期望。它为架构师还有 Java 开发者提供了一套可以一用的工具。
Cricket 平台最简单的用法就是跑一个静态网页应用。我们用现成的 Docker 镜像就可以在几分钟内弄好一个网站,而不用额外安装任何软件。
比如说,我们可以新建一个 mywww 文件夹,然后在里面放一个简单的 HTML 页面,然后运行 Docker Hub 里面的 "cricket-micosite" 镜像:
$ mkdir mywww
$ echo "Hello!">mywww/index.html
$ docker run --name mycricket -d -v "$(pwd)"/mywww:/cricket/www \
-p 127.0.0.1:8080:8080 gskorupa/cricket-microsite:latest
Cricket 内置了一个 HTTP 服务器,后者会默认监听 8080
端口并会对外提供 /cricket/www
及其子目录里面的所有文件。在上面的例子里面,我们的网站便能通过 URL http://127.0.0.1:8080
来访问。现在我们就能试着去修改 index.html 文件,或是添加别的页面和文件了。
注意:以这种方式启动的平台不会自动刷新内部缓存,因此只有在重启容器之后,文件中的所有更改才会在浏览器中可见。
我们可以通过以下命令来停止一个正在运行的 Docker 容器:
$ docker stop mycricket
若要启动处于停止状态的容器,就使用这个命令:
$ docker start mycricket
在我们讨论下一个例子之前,我们先来看看这一平台的关键组件。
Cricket 实现了一种 “端口与适配器”(见下图六边形)的架构。这种架构下的系统能调用特定的适配器来实现一些服务的功能。除此之外,这一方案还有个优势,那便是它能轻松安全地更改单个适配器的实现方法。比如我们可以很方便地更改数据库的类型,或者用一个外部消息中继器来取代内置的解决方案。
微网站会用这些服务所对应的适配器:
UserService,AuthService,ContentService 和 ContentManager 对应的适配器使得这些服务可以通过 REST API 来调用。
注意:在默认情况下,微网站是作为一个服务提供的,但微网站也可以分成几个更小的微服务,从而提供更好的可扩展性并提高实施解决方案的安全性。
内容管理系统(CMS,或者叫 WCM,即 Web 内容管理)简化了对网站内容的管理,能帮助我们在不用理会它的布局的前提下修改显示在网站里面的内容。
Cricket Microsite 也属于这一类系统,但和其中一些系统不同。它的功能有限,使其难以被商业级别的用户所接受(例如其缺乏版本控制和对工作流程的简化,只能编辑或删除已经发布的内容),而它主要针对的是程序员和一般的网站设计者。
CM(ContentManager)模块负责管理存储在数据库中的内容元素(即文档),让我们能创建、编辑、发布、取消发布以及删除这些元素。具有管理员或者编辑者角色权限的登录用户可以访问这一模块。
我们会区分三种类型的文档:
文档的主要特点有:
uid + language
来作为其唯一标识。其中 uid
又由文档的路径和文档名两个参数组合得到。 uid
但 language
参数不同的文件其实是同一个文档,只是它们的显示语言不同。在前面的示例里面,我们用这个平台(使用了 -v 参数来启动的容器)来发布了指定的文件夹里面的页面,可是在这一模式下,ContentManager 模块是不可用的。因此,为了体验完整的功能,我们应该在不挂载本地文件夹的前提下启动 Docker 容器:
$ docker run --name mycricket -d -p 127.0.0.1:8080:8080 \
gskorupa/cricket-microsite:latest
容器会自动地在本地的文件系统中创建两个分卷来存储工作所需的文件(比如数据库还有页面的模板)。我们也可以通过这个命令找出这两个分卷的实际位置:
$ docker container inspect mycricket
本文在末尾部分给出了一些对平台启动方式的补充说明。
在系统完成启动之后,我们就能用初始账号 admin
和初始密码 cricket
登录到 ContentManagement 模块(https://127.0.0.1:8080/cricket
)里面。
在菜单选中选项 Content > Documents
之后,我们就能在屏幕上看到符合特定状态和语言条件的文件的列表:
Content Manager 管理模块是基于 Riot 和 Bootstrap 库所构建的,并且支持了 RWD 应用开发技术,因此我们也能用智能手机打开这个界面。
在我们保存文档后,文档状态还只是 wip
,只有能访问 ContentManager 模块的用户才能访问这个文档。我们要通过点击 “Status” 图标,把文档发布出去才能让文档被所有访问者看到。把文档的状态改成已发布是不需要确认的。只要点击这个图标,文档就会从 wip 文档列表里消失。
文档的取消发布操作也是以类似的方式完成的。
注意:发布的文档可以编辑,并且对文档的编辑在保存之后立即生效,能被所有访问者见到!
Cricket Microsite 可以将静态网页应用平滑地迁移到一套 WCM 的解决方案里面。这要归因于其内置的 HTTP 服务器特有的文件提供方式。
在接收到对指定文件的特定路径的 GET 请求时,服务器会首先搜索标识符(UID)里的路径参数与给定路径相同的 FILE 或 CODE 类型的文档。对这一类文档的搜索位置的优先顺序如下:
正是上述方式让我们能用一个选定模板里面的文件快速地运行一个静态网站的原型,然后用 CM 模块里面的相应文档来连续地替换磁盘上的文件。
Cricket 有个内置的默认页面模板(可以在 http://127.0.0.1:8080
里面看到)。这里我们就用自己的动态网站来替换它。这一过程有以下步骤:
1. 登录到 CM 模块并用以下参数创建一个新文档:
参数 | 值 |
---|---|
type | CODE |
name | index.html |
path | / |
content | Hello! |
MIME type | text/html |
2. 保存文档并发布
3. 检查主页(http://127.0.0.1:8080)的内容是否已更改为上述文档所指定的内容
注:在以这一章节的模式启动时无需重启服务。CM 模块中的文档更改会立即显示在网站上。
4. 在 CM 模块中找到已发布文档的列表,然后编辑其中的 index.html 文档。在 “Content” 字段中输入如下所示的 HTML 代码:
<html>
<head>
<link rel="stylesheet" type="text/css" href="theme.css">
</head>
<body>
<img src="/images/logo.png"/>
<h1>Hello World!</h1>
</body>
</html>
5. 保存并检查页面有没有改变
6. 创建一个样式定义表文档:
参数 | 值 |
---|---|
type | CODE |
name | theme.css |
path | / |
MIME type | text/css |
body {
color: darkblue;
text-align: center;
}
7. 创建一个作为 logo 的文档:
参数 | 值 |
---|---|
type | CODE |
name | logo.png |
path | /images |
MIME type | image/png |
file | 从硬盘里选出来的文件 |
8. 发布所有创建的文档后,我们的网站应该会变成这个样子:
我们还可以用同样的方式来创建或管理后续的页面,从而根据需求扩展我们的网站。
通过改代码的方式来在页面上发布信息是很不方便的。像 WordPress 这样的 WCM 平台会采用一种避免修改网页源代码的内容编辑方式来简化这个过程。这也使网站开发和文档编辑的分离成为了可能。
在 Cricket Microsite 里面,我们也能利用 ARTICLE 类型的文档来达成相同的效果。
得益于这一平台的特性,我们就可以根据自己的需求设计并构建解决方案,而不会有过分的系统资源需求,并且能比一体化的系统更加易用。
描述关于设计动态网页、网页间的导航、搜索文档还有响应用户操作的所有方面的需求已经远远超出了本文的范围。这里我们只描述一个解决方案的例子。有经验的读者应该能在此基础上开发出自己的一套方案。
我们将部署一个有这些功能的 RWD 新闻网站:
为了开发页面模板,我们会用到以下类库。它们非常符合 Cricket Microsite 的极简理念:
我们的网站将由存储在内容管理模块中的几种文档构建而成:
UID | 目的 |
---|---|
/index.html | 主页模板 |
/local/js/routing.js | Riot 路由组件 |
/local/components/app_main.tag | Riot 组件,作为主页 |
/local/components/app_menu.tag | Riot 组件,显示菜单 |
/local/components/app_list.tag | Riot 组件,显示文章列表 |
/local/components/app_articleview.tag | Riot 组件,下载并显示选定的消息 |
/local/components/app_article.tag | Riot 组件,呈现 ARTICLE 类型文档的内容 |
/tags/raw.tag | Riot 组件,让我们能将 HTML 片段注入另一个组件里面。 |
/local/css/styles.css | 样式表 |
/welcome_text | 显示在主页上的内容 |
/news/art1 | 新闻示例 1 |
/news/art2 | 新闻示例 2 |
注:raw
组件(raw.tag
)和数据访问功能(data-api.js
文件)是 Cricket Microsite 发行版的固有部分,在此之后不作讨论。
这以文件的任务是加载所需的 JavaScript 库,样式表和 Riot 组件。在它被创建之后,它只有在修改组件列表时才需要修改。
参数 | 值 |
---|---|
type | CODE |
name | index.html |
path | / |
MIME type | text/html |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<!-- Custom styles -->
<link rel="stylesheet" href="/local/css/styles.css">
</head>
<body>
<app_main></app_main>
<!-- SCRIPTS: jQuery first, then Bootstrap JS, then Riot -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/riot/3.9.3/riot+compiler.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/riot-route@3.1.2/dist/route.min.js"></script>
<!-- Application scripts -->
<script src="/local/js/routing.js"></script>
<script src="/js/data-api.js"></script>
<!-- Riot components -->
<script data-src="/tags/raw.tag" type="riot/tag"></script>
<script data-src="/local/components/app_article.tag" type="riot/tag"></script>
<script data-src="/local/components/app_main.tag" type="riot/tag"></script>
<script data-src="/local/components/app_menu.tag" type="riot/tag"></script>
<script data-src="/local/components/app_list.tag" type="riot/tag"></script>
<script data-src="/local/components/app_articleview.tag" type="riot/tag"></script>
<script>
app.csAPI = "http://localhost:8080/api/cs";
riot.mount('*');
route.start(true);
</script>
</body>
</html>
路由功能 是由与页面间转移有关的事件(单击菜单里的链接,刷新页面,或者按退格键等)所触发的。它让我们能定义与导航相关的在浏览器中的活动。
参数 | 值 |
---|---|
type | CODE |
name | routing.js |
path | /local/js |
MIME type | text/javascript |
route(function (id) {
if(id.startsWith('news')){
app.currentPage='news'
app.selectedDoc = id.substring(4).replace(/,/g , '/')
}else{
app.currentPage='home'
app.selectedDoc=''
}
globalEvents.trigger('pageselected')
riot.update()
})
app_main 组件负责把组件嵌入到页面中,并基于应用的当前状态调整组件的可见性:在这里的例子则是根据选定的页面和文档来调整。其文档参数为:
参数 | 值 |
---|---|
type | CODE |
name | app_main.tag |
path | /local/components |
MIME type | text/html |
<app_main>
<app_menu></app_menu>
<app_articleview if={app.currentPage=='home'} uri='/welcome_text' mode='intro'></app_articleview>
<app_list if={app.currentPage=='home'} limit='1'></app_list>
<app_articleview if={app.currentPage=='news' && app.selectedDoc!=''} uri={app.selectedDoc} mode='view'></app_articleview>
<app_list if={app.currentPage=='news' && app.selectedDoc==''}></app_list>
</app_main>
app_menu 组件则会展示一个简单的菜单。
参数 | 值 |
---|---|
type | CODE |
name | app_menu.html |
path | /local/component |
MIME type | text/html |
<app_menu>
<div class="container menu">
<div class="row" >
<div class="col">
<a href="/">Home</a> <a href="#news">News</a>
</div>
</div>
</div>
</app_menu>
app_list 会使用 CS 模块的 REST API 加载特定路径下的文档列表,然后使用 app_article 组件呈现这些文档的链接。
参数 | 值 |
---|---|
type | CODE |
name | app_list.html |
path | /local/component |
MIME type | text/html |
<app_list>
<div class="container">
<virtual each={item in list}>
<div class="row">
<div class="col">
<app_article title={item.title} summary={item.summary} mode='list' page='#news' uid={ item.uid } />
</div>
</div>
</virtual>
</div>
<script>
var self=this
self.limit=0
self.list=[]
self.on('mount', function(){
self.limit=opts.limit
loadDocs()
})
var loadDocs = function () {
getData(app.csAPI + '?path=/news/&language=' + app.language, null, null, setDocList, self)
}
var setDocList = function (text) {
self.list = JSON.parse(text)
if(self.limit>0){
self.list=self.list.slice(0,self.limit)
}
var i
for (i = 0; i < self.list.length; i++) {
self.list[i]=decodeDocument(self.list[i])
}
self.update()
}
</script>
</app_list>
app_articleview 则使用 CS 模块的 REST API 加载指定 uid
的文章,然后将代表此文档的 JSON 对象呈递给 app_article 组件。
参数 | 值 |
---|---|
type | CODE |
name | app_articleview.tag |
path | /local/component |
MIME type | text/html |
<app_articleview>
<div class="container">
<div class="row" >
<div class="col">
<app_article title={doc.title} mode={mode} />
</div>
</div>
</div>
<script>
var self=this
self.doc={ title: '.....'}
self.on('mount', function(){
self.uri=opts.uri
self.mode=opts.mode
self.articleTag=self.tags.app_article
getData(app.csAPI + self.uri+'?language=en', null, null, setDocument, self)
})
var setDocument = function (text) {
self.doc = decodeDocument(JSON.parse(text))
self.articleTag.update(self.doc)
self.articleTag.update({mode:self.mode})
}
</script>
</app_articleview>
app_article 组件的任务是使用适当的格式来将选定的文档嵌入到网页里面。我们所选的格式会根据文档的参数还有链接来决定呈现文档的具体样式。
参数 | 值 |
---|---|
类型 | CODE |
名称 | app_article.tag |
路径 | /local/component |
MIME 类型 | text/html |
<app_article>
<article class='standard'>
<header>
<h1 class={mode}>{title}</h1>
<div class='intro' if={summary}><raw html={summary}/></div>
</header>
<div if={content}><raw html={content}/></div>
<footer>
{ published }</div></footer>
<div if={ mode=='view' }><a href="#" onClick="history.back()" }>Back</a></div>
<div if={ mode=='list' }><a href={detailsLink}>More ...</a></div>
</article>
<script>
var self=this
self.title=opts.title
self.summary=opts.summary
self.content=opts.content
self.author=opts.author
self.published=opts.published
self.type=opts.type
self.page=opts.page
self.uid=opts.uid
if(opts.mode){
self.mode=opts.mode
}else{
self.mode='default'
}
self.detailsLink=''+self.page+self.uid
self.detailsLink=self.detailsLink.replace(/\//g , ',')
</script>
</app_article>
样式表文档可用于管理网站的外观。
参数 | 值 |
---|---|
type | CODE |
name | styles.css |
path | /local/css |
MIME type | text/css |
article > header {
margin-top: 20px;
}
article > header > h1 {
font-size: x-large;
font-weight: bold;
}
article > header > h1.view {
font-size: xx-large;
font-weight: bold;
color: green;
}
.menu {
padding: 10px;
background: linear-gradient(white,lightgray);
}
我们在这里准备的主页会显示存储在 CM 模块中的标识符为 /welcome_text
的文档的内容(即在 /
路径下,名为 welcome_text
的文档)。若现在还没有弄好这个文档的话,现在也是时候准备了。
首页还会显示 /news
路径中的所有文章的列表。为了简单起见,这里的代码没有限制文档的显示数量,也没有分页机制。
若上述的所有步骤均已完成,那么我们就可以在 http://127.0.0.1:8080
上看到我们的成果:
参数 | 含义 |
---|---|
uid | 唯一的文件标识符(路径 + 名称) |
path | 文档在存储库结构中的位置(路径) |
name | 特定路径下的文档的唯一名称(只包含字母数字,不含空格) |
type | 文档类型(ARTICLE / CODE / FILE) |
author | 文档作者 |
title | 文档的标题 |
summary | 文档的摘要 |
content | 文档的内容。对 FILE 类型的文档对应文件在平台的本地文件系统的路径 |
tag | (暂无用处) |
language | 用两个字符表示的文档语言代码 |
commentable | (暂无用处) |
MIME type | 文档的 MIME 类型 |
status | 文档状态(wip / published) |
size | 文档大小 |
created | 创建日期 |
modified | 最后一次修改的日期 |
published | 上次发布的日期 |
createdBy | 登录文档创建者 |
$ docker run --name cricketsite -d -e CRICKET_URL='https//www.mysite.com' \
-p 127.0.0.1:8080:8080 gskorupa/cricket-microsite:latest
运行参数 | 含义 |
---|---|
-- name | 会给容器指定一个便于我们进行识别的名称。 |
-d | 在后台运行容器 |
-e | 设置容器的环境变量值。例如,环境变量 CRICKET_URL 会存储我们服务的 URL 地址。如果要从浏览器异步引用服务(CM 模块的 Web 应用所使用的机制),则必须设置此变量。 |
-p | 端口映射。我们可以在此指定 Cricket 的 HTTP 服务器所监听的 IP 地址以及由 Docker 来暴露的端口号。 |
$ docker run --name mycricket -d -v "$(pwd)"/mywww:/cricket/www \
-p 127.0.0.1:8080:8080 gskorupa/cricket-microsite:latest
参数 | 含义 |
---|---|
-v | 将本地文件系统中的指定文件夹连接到容器里面来作为容器的一个分卷 |
如果不想用 Docker,也可以直接在 JRE 上运行这一系统。这对希望用这个平台的人来进行一些测试(比如说:修改静态页面的模板,更改数据库,或是使用自己的后端组件来扩展这一平台等)的人来说也是一种可行的选择:
1. 下载最新版本的平台并将其解压到我们选择的文件夹。
$ wget https://github.com/gskorupa/Cricket/releases/download/1.2.41/cricket-microsite.zip
$ mkdir myservice
$ unzip cricket-microsite.zip -d myservice
2. 开始服务
$ cd myservice
$ sh run.sh
注:服务的运行参数只适用于 Java 9 / 10,但我们可以通过编辑 run.sh 文件来更改它。
3. 根据需要修改和扩展服务。
我们可以直接修改 myservice/work/www
这一路径中的文件结构。