JHipster 全栈开发(二)
原文:
zh.annas-archive.org/md5/1cf2727d6ba6d885f78c31bfa2415676译者:飞龙
第五章:定制和进一步开发
在上一章中,我们看到了如何使用 JHipster 领域语言来建模和生成我们的领域模型。我们还学习了实体关系和 import-jdl 子生成器。在本章中,我们将看到如何进一步定制和添加业务逻辑到生成的应用程序以满足我们的需求。我们将学习以下内容:
-
使用 Spring DevTools 和 BrowserSync 进行实时重新加载
-
为实体定制 angular 前端
-
编辑使用 JHipster 实体生成器创建的实体
-
使用 Bootstrap 主题更改应用程序的外观和感觉
-
使用 JHipster 语言生成器添加新的 i18n 语言
-
使用 Spring Security 添加基于角色的额外授权来定制生成的 REST API
-
创建新的 Spring Data JPA 查询和方法
开发时的实时重新加载
在开发应用程序时,最令人烦恼和耗时的一部分是重新编译代码和重新启动服务器以查看我们所做的代码更改。传统上,JavaScript 代码更容易,因为它不需要编译,你只需刷新浏览器就能看到更改。然而,尽管当前的 MVVM 堆栈使客户端比以前更重要,但它们也引入了副作用,如客户端代码的转译等。所以,如果你正在重构实体的一个字段,你传统上需要执行以下任务才能在浏览器中看到更改:
-
编译服务器端 Java 代码。
-
将表更改应用到数据库中。
-
重新编译客户端代码。
-
重新启动应用程序服务器。
-
刷新浏览器。
这需要花费很多时间,每次进行小改动时都令人沮丧,导致你在检查之前做出更多更改,从而影响生产力。
如果我告诉你,你不必做任何这些事情,而且所有这些都可以在你使用 IDE 保存更改时自动发生,那会怎么样?那会非常棒,不是吗?
使用 JHipster,你将得到完全相同的功能。JHipster 使用 Spring Boot DevTools、webpack 开发服务器和 BrowserSync 来为端到端代码提供良好的实时重新加载功能。
让我们快速了解一下所使用的这些技术。
Spring Boot DevTools
Spring Boot DevTools (docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html) 允许 Spring Boot 应用程序在类路径发生变化时重新加载嵌入的服务器。它声明如下——本模块的目的是尝试改善在开发 Spring Boot 应用程序时的开发体验,并且它确实做到了这一点。它使用自定义类加载器在类更新和重新编译时重启应用程序,并且由于服务器是热重载的,所以比冷启动快得多。
它不如 JRebel 或类似技术那样酷,这些技术可以即时重新加载,但它比手动操作要好,并且不需要任何额外配置即可启用。
JHipster DevTools 在 dev 配置文件中自动启用,使用可以自动在保存时重新编译类的 IDE。DevTools 将确保应用程序重新加载并保持最新。由于使用了 Liquibase,任何使用正确变更日志的架构更新也将得到更新。请确保不要更改现有的变更日志,因为这会导致校验和错误。应用程序的重新加载也可以通过简单地使用命令 mvnw compile 或 gradlew compileJava 来触发,具体取决于所使用的构建工具。
如果你选择 NoSQL 数据库,例如 MongoDB、Cassandra 或 Couchbase,JHipster 也为这些数据库提供了数据库迁移工具。
Webpack 开发服务器和 BrowserSync
Webpack 开发服务器 (github.com/webpack/webpack-dev-server) 使用 webpack 开发中间件提供了一个简单的 Express 服务器,并在资源更改时支持实时重新加载。Webpack 开发中间件支持热模块替换和内存文件访问等功能。
在 Webpack 版本 4 及以上版本中,使用了一个名为 webpack-serve (github.com/webpack-contrib/webpack-serve) 的新替代方案,而不是 Webpack 开发服务器。它利用了较新浏览器中的原生 WebSocket 支持。
BrowserSync (browsersync.io/) 是一个 Node.js 工具,它通过同步多个浏览器和设备上网页的文件更改和交互来帮助进行浏览器测试。它提供了诸如文件更改时的自动重新加载、同步 UI 交互、滚动等功能。JHipster 将 BrowserSync 与 Webpack 开发服务器集成,以提供高效的开发环境。这使得在不同的浏览器和设备上测试网页变得非常简单。CSS 的更改无需刷新浏览器即可加载。
要在客户端使用实时重新加载,你需要运行 yarn start,这将启动开发服务器并打开指向 http://localhost:9000 的浏览器。注意端口号 9000。BrowserSync 将使用此端口,而应用程序的后端将在 8080 上提供服务,所有请求将通过 webpack 开发中间件代理。
打开另一个浏览器,例如,如果 BrowserSync 已经打开了 Chrome,则打开 Firefox,反之亦然。现在将它们并排放置,并与应用程序交互。你会看到你的操作被复制,多亏了 BrowserSync。尝试更改一些代码并保存文件,以查看实时重新加载的效果。
为应用程序设置实时重新加载
让我们开始为创建的应用程序设置完美的开发环境。在终端中,通过运行 ./gradlew 以开发模式启动服务器,在另一个终端中,通过运行 yarn start 启动客户端开发服务器。
现在当你在服务器端进行任何更改时,只需运行./gradlew compileJava,或者如果你使用的是 IDE,点击编译按钮。
使用 IntelliJ IDEA,文件会自动保存,因此你可以设置 Ctrl + S 来编译类,从而提供一个良好的工作流程。在 Eclipse 中,保存类会自动编译。
当你在客户端进行更改时,只需保存文件并启动 webpack 开发服务器和 BrowserSync,它将完成剩余的工作。
为实体自定义 Angular 前端
现在我们已经创建了工作着的实体领域模型,让我们让它更易于使用。产品列表屏幕由 JHipster 生成的表格视图;它对于简单的 CRUD 操作来说是足够的,但并不是最适合想要浏览我们的产品列表的最终用户的用户体验。让我们看看我们如何轻松地将其更改为更吸引人的东西。我们还将添加一个不错的客户端筛选选项来筛选列表。我们将使用 Angular 和 Bootstrap 的功能来完成这项工作。
首先,让我们找到我们需要编辑的源代码。在你的首选编辑器/IDE 中导航到src/main/webapp/app/entities/product:
让我们从自定义product.component.html文件开始,以更新产品列表的 UI 视图。当前的 HTML 代码渲染一个表格视图,并使用一些 Angular 指令来增强视图,包括排序和分页。让我们首先将视图从表格更改为列表,但首先通过导航到http://localhost:9000打开 BrowserSync 的开发 web 服务器(如果尚未打开)。登录并导航到实体 | 产品类别,创建一个类别,然后导航到实体 | 产品,创建一些新产品,以便我们有东西可以列出:
我们可以使用 Bootstrap 列表组(getbootstrap.com/docs/4.0/components/list-group/)组件来实现这个目的。让我们使用以下代码片段并更改视图。将div替换为class="table-responsive"的以下代码:
<div *ngIf="products">
<div class="*list-group*">
<a [routerLink]="['../product', product.id ]"
class="*list-group-item list-group-item-action* flex-column
align-items-start"
*ngFor="let product of products; trackBy: trackId">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{product.name}}</h5>
<small *ngIf="product.productCategory">
<a [routerLink]="['../product-category',
product.productCategory?.id ]" >
{{product.productCategory?.id}}
</a>
</small>
</div>
<small class="mb-1">{{product.description}}</small>
<p class="mb-1">Price: {{product.price}}</p>
<small>
Size:
<span jhiTranslate="{{'storeApp.Size.' +
product.size}}">
{{product.size}}
</span>
</small>
</a>
</div>
</div>
如你所见,我们正在使用 Angular 指令*ngFor="let product of products; trackBy: trackId"在锚点元素上迭代产品,以便为列表中的每个产品创建元素。我们用*ngIf="products"指令包裹这个,这样只有在产品对象定义时才会渲染视图。[routerLink]="['../product', product.id ]"指令将使用 Angular 路由为锚点创建一个 href,这样我们就可以导航到特定的产品路由。然后我们使用产品的属性在模板字符串中使用{{product.name}}语法进行渲染。当你保存代码时,你可能会注意到视图会自动刷新,这要归功于 BrowserSync。
在ngFor中使用的trackBy函数让 Angular 决定哪些项目被添加或从集合中删除。这提高了渲染性能,因为现在 Angular 可以精确地找出需要添加或从 DOM 中删除哪些项目,而不必重新创建整个集合。在这里,我们提供trackId作为函数来唯一标识集合中的项目。
这将产生以下结果:
虽然这是一个好的开始,但还不够。所以,让我们进去并让它变得更好。首先,让我们将图片添加到列表中。修改代码以添加 Bootstrap 行和列,如下所示,原始的渲染内容代码被移动到第二列,并且保持不变:
<div *ngIf="products">
<div class="list-group">
<a [routerLink]="['../product', product.id ]" class="list-group-item list-group-item-action flex-column align-items-start"
*ngFor="let product of products; trackBy: trackId">
<div class="row">
<div class="col-2 col-xs-12 justify-content-center">
<img [src]="'data:' + product.imageContentType +
';base64,' + product.image"
style="max-height:150px;" alt="product image"/>
</div>
<div class="col col-xs-12">
<div class="d-flex w-100 justify-content-between">
...
</div>
<small class="mb-1">{{product.description}}</small>
<p class="mb-1">Price: {{product.price}}</p>
<small>
...
</small>
</div>
</div>
</a>
</div>
</div>
看一下加粗的代码。我们添加了一个 Bootstrap 行,其中包含两个列 div,第一个 div 在 12 列网格中占用两个列,指定为col-2,同时我们还说当显示为xs(额外小)时,div标签应该使用col-xs-12占用 12 列。第二个div通过仅指定col来保持响应式,它占用第一个div之后的剩余可用列,当显示为额外小尺寸时,它也占用 12 列。第一个列div中的图片使用数据 URL 作为src来渲染图片。现在我们有一个更好的视图:
我们可以进一步润色它。我们可以使用 Angular 货币管道(angular.io/api/common/CurrencyPipe)来显示价格,并通过将其更改为{{product.price | currency:'USD'}}来移除其冗余标签。我们还可以为列表右侧显示的分类添加一个标签。
最后,我们可以将编辑和删除按钮重新添加回来,但我们只需要为具有ADMIN角色的用户显示它们,这样普通用户就只能查看产品列表。我们可以从原始表格中复制edit和delete按钮的 HTML 代码。最终的代码如下:
<div *ngIf="products">
<div class="list-group">
<a [routerLink]="['../product', product.id ]"
class="list-group-item list-group-item-action flex-column
align-items-start"
*ngFor="let product of products; trackBy: trackId">
<div class="row">
<div class="col-2 col-xs-12 justify-content-center">
<img [src]="'data:' + product.imageContentType +
';base64,' + product.image"
style="max-height:150px;" alt="product image"/>
</div>
<div class="col col-xs-12">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{product.name}}</h5>
<small *ngIf="product.productCategory">
<a [routerLink]="['../product-category',
product.productCategory?.id ]" >
Category: {{product.productCategory?.id}}
</a>
</small>
</div>
<small class="mb-1">{{product.description}}</small>
<p class="mb-1">{{product.price | currency:'USD'}}</p>
<small>
Size:
<span jhiTranslate="{{'storeApp.Size.' +
product.size}}">
{{product.size}}
</span>
</small>
<div *jhiHasAnyAuthority="'ROLE_ADMIN'">
<button type="submit"
[routerLink]="['/',
{ outlets: { popup: 'product/'+
product.id + '/edit'} }]"
replaceUrl="true"
queryParamsHandling="merge"
class="btn btn-primary btn-sm">
<span class="fa fa-pencil"></span>
<span class="d-none d-md-inline"
jhiTranslate="entity.action.edit">Edit</span>
</button>
<button type="submit"
[routerLink]="['/',
{ outlets: { popup: 'product/'+
product.id + '/delete'} }]"
replaceUrl="true"
queryParamsHandling="merge"
class="btn btn-danger btn-sm">
<span class="fa fa-remove"></span>
<span class="d-none d-md-inline"
jhiTranslate="entity.action.delete">Delete</span>
</button>
</div>
</div>
</div>
</a>
</div>
</div>
*jhiHasAnyAuthority="'ROLE_ADMIN'"指令由 JHipster 提供,可以用于根据用户角色控制展示。默认情况下,JHipster 提供ROLE_ADMIN和ROLE_USER,但仅在客户端控制这并不安全,因为它很容易被绕过,所以我们应该在服务器端也进行安全控制。我们将在本章后面讨论这个问题。使用用户账户注销并重新登录以查看指令的作用:
现在,让我们也将*jhiHasAnyAuthority="'ROLE_ADMIN'"指令添加到创建按钮元素中。
现在既然我们的视图已经变得更好,让我们恢复我们最初拥有的排序功能。由于我们没有表头了,我们可以使用一些按钮根据某些重要的字段进行排序。
让我们使用 Bootstrap 按钮组(getbootstrap.com/docs/4.0/components/button-group/) 来实现这个功能。将以下代码片段放置在我们之前创建的 <div class="list-group"> 元素上方:
<div class="mb-2 d-flex justify-content-end align-items-center">
<span class="mx-2 col-1">Sort by</span>
<div class="btn-group" role="group"
jhiSort [(predicate)]="predicate" [(ascending)]="reverse"
[callback]="transition.bind(this)">
<button type="button" class="btn btn-light" jhiSortBy="name">
<span jhiTranslate="storeApp.product.name">Name</span>
<span class="fa fa-sort"></span>
</button>
<button type="button" class="btn btn-light" jhiSortBy="price">
<span jhiTranslate="storeApp.product.price">Price</span>
<span class="fa fa-sort"></span>
</button>
<button type="button" class="btn btn-light" jhiSortBy="size">
<span jhiTranslate="storeApp.product.size">Size</span>
<span class="fa fa-sort"></span>
</button>
<button type="button" class="btn btn-light"
jhiSortBy="productCategory.id">
<span
jhiTranslate="storeApp.product.productCategory">Product Category</span>
<span class="fa fa-sort"></span>
</button>
</div>
</div>
我们可以使用 Bootstrap 外边距和 flexbox 工具类,如 mb-2 d-flex justify-content-end align-items-center 来正确定位和排列项目。我们在一个 div 元素上使用 btn-group 类来将按钮元素组合在一起,并在这些按钮上放置了 jhiSort 指令及其绑定的属性,如 predicate、ascending 和 callback。在按钮本身上,我们使用 jhiSortBy 指令来指定它将使用哪个字段进行排序。现在我们的页面看起来如下,产品按价格排序:
最后,让我们为页面添加一些传统的客户端过滤功能。
JHipster 提供了一个选项,可以使用 JPA 元模型启用服务器端过滤。另一个选项是启用 Elasticsearch,对于每个实体,JHipster 将自动创建全文搜索字段。因此,对于任何严肃的过滤需求,你应该使用这些。
首先,让我们在 product.component.ts 文件中的 ProductComponent 类中添加一个新的字符串类型实例变量 filter:
export class ProductComponent implements OnInit, OnDestroy {
...
filter: string;
constructor(
...
) {
...
}
...
}
现在,让我们在 product.component.html 文件中使用这个变量。将以下代码中的高亮部分添加到我们为排序按钮创建的 div 中:
<div class="mb-2 d-flex justify-content-end align-items-center">
<span class="mr-2 col-2">Filter by name</span>
<input type="search" class="form-control" [(ngModel)]="filter">
<span class="mx-2 col-1">Sort by</span>
<div class="btn-group" role="group"
...
</div>
</div>
我们使用 ngModel 指令将过滤变量绑定到一个输入元素上,使用 [()] 确保变量的双向绑定。
[(ngModel)]="filter" 创建双向绑定,[ngModel]="filter" 从模型到视图创建单向绑定,(ngModel)="filter" 从视图到模型创建单向绑定。
最后,将我们的列表项元素上的 ngFor 指令更新如下。我们使用 JHipster 提供的管道通过产品的名称字段过滤列表:
*ngFor="let product of (products | pureFilter:filter:'name'); trackBy: trackId"
就这样,我们在屏幕上得到了一个闪亮的过滤选项:
与之前相比,用户体验有了很大的提升,但在实际应用中,你可以为面向客户的网站构建一个更好的用户界面,包括添加商品到购物车、在线支付等功能,并将这部分留给后台办公使用。让我们将这个更改提交到 git:这对于以后管理项目更改非常重要。运行以下命令:
> git add --all
> git commit -am "update product listing page UI"
使用 JHipster 实体子生成器编辑实体
在查看生成的实体屏幕时,我们可能会意识到一些影响用户体验的小问题。例如,在产品屏幕上,我们有一个与产品类别的关联,但在创建时从下拉菜单中选择产品类别,或者在列表中显示类别时,我们通过其 ID 显示类别,这对用户来说并不友好。如果我们可以显示产品类别名称,那就更好了。这是 JHipster 的默认行为,但在定义关系时可以进行自定义。让我们看看如何通过编辑 JDL 模型来使我们的生成屏幕更加用户友好。这将覆盖现有文件,但由于我们使用git,我们可以轻松地选择我们所做的更改,我们将在稍后看到这是如何完成的。
在我们的 JDL 中,我们使用以下代码定义了实体之间的关系:
relationship OneToOne {
Customer{user} to User
}
relationship ManyToOne {
OrderItem{product} to Product
}
relationship OneToMany {
Customer{order} to ProductOrder{customer},
ProductOrder{orderItem} to OrderItem{order},
ProductOrder{invoice} to Invoice{order},
Invoice{shipment} to Shipment{invoice},
ProductCategory{product} to Product{productCategory}
}
通过在 JDL 中使用(<字段名>)语法指定用于显示关系的字段,我们可以更改客户端代码显示关系的方式:
relationship OneToOne {
Customer{user(login)} to User
}
relationship ManyToOne {
OrderItem{product(name)} to Product
}
relationship OneToMany {
Customer{order} to ProductOrder{customer(email)},
ProductOrder{orderItem} to OrderItem{order(code)},
ProductOrder{invoice} to Invoice{order(code)},
Invoice{shipment} to Shipment{invoice(code)},
ProductCategory{product} to Product{productCategory(name)}
}
使用import-jdl命令来运行这个操作。该命令仅生成自上次运行以来发生变化的实体。但在运行之前,我们也要切换到一个新的分支,因为将主要更改放在单独的分支上并合并回来是一种良好的实践,这样你可以有更多的控制权:
> git checkout -b entity-update-display-name
> jhipster import-jdl online-store.jh
在这里了解更多关于 Git flow 的信息:guides.github.com/introduction/flow/。
接受文件更改,等待构建完成。现在,让我们查看实体页面,以验证显示名称是否被正确使用,并创建一些实体来尝试一下。现在我们意识到Invoice实体有空的下拉菜单,这是因为Invoice实体没有名为code的字段。由于我们在模板中使用{{invoice.order?.code}},符号?使 Angular 跳过未定义的值,从而防止渲染错误。
这很容易修复。有时我们可能想在创建实体后使用 JDL 和import-jdl命令进行一些小的更改。最好的方法是在 JDL 中进行更改,并使用导入 JDL 命令重新生成,就像我们在前面的代码中看到的那样。现在还有一个选项,即实体子生成器,它可以产生相同的结果。为了熟悉这个选项,让我们使用它来向我们的Invoice实体添加字段:
- 运行以下命令:
> jhipster entity Invoice
- 从选项中选择“是”,添加更多字段和关系:
Using JHipster version installed globally
Executing jhipster:entity Invoice
Options:
Found the .jhipster/Invoice.json configuration file, entity can be automatically generated!
The entity Invoice is being updated.
? Do you want to update the entity? This will replace the existing files for this entity, all your custom code will be overwritten
Yes, re generate the entity
❯ Yes, add more fields and relationships
Yes, remove fields and relationships
No, exit
- 对于下一个问题选择“是”,并提供后续问题中的字段名、类型和验证:
Generating field #7
? Do you want to add a field to your entity? Yes
? What is the name of your field? code
? What is the type of your field? String
? Do you want to add validation rules to your field? Yes
? Which validation rules do you want to add? Required
================= Invoice =================
Fields
date (Instant) required
details (String)
status (InvoiceStatus) required
paymentMethod (PaymentMethod) required
paymentDate (Instant) required
paymentAmount (BigDecimal) required
code (String) required
Relationships
shipment (Shipment) one-to-many
order (ProductOrder) many-to-one
Generating field #8
? Do you want to add a field to your entity? (Y/n)
-
对于后续的提示选择“n”以添加更多字段和关系。接受提议的文件更改,这样就完成了。
-
现在,请确保更新 JDL,使实体
Invoice具有code String required字段。
你也可以运行jhipster export-jdl online-store.jh来将当前模型导回到 JDL。
现在我们已经正确显示了实体关系,我们还需要确保某些实体具有必填的关系值。例如,对于客户来说,必须有一个用户,ProductOrder 必须有一个客户,订单项必须有一个订单,发票必须有一个订单,最后,运输必须有一个发票。由于 JHipster 支持使关系成为必填项,我们可以使用 JDL 进行这些更改。在 online-store.jh 中更新关系到以下片段:
relationship OneToOne {
Customer{user(login) required} to User
}
relationship ManyToOne {
OrderItem{product(name) required} to Product
}
relationship OneToMany {
Customer{order} to ProductOrder{customer(email) required},
ProductOrder{orderItem} to OrderItem{order(code) required},
ProductOrder{invoice} to Invoice{order(code) required},
Invoice{shipment} to Shipment{invoice(code) required},
ProductCategory{product} to Product{productCategory(name)}
}
现在,运行 jhipster import-jdl online-store.jh 并接受提出的更新。请确保使用 git diff 命令或你的 Git UI 工具检查更改。
让我们提交这一步,以便在需要时可以回滚:
> git add --all
> git commit -am "entity relationships display names and required update"
现在我们遇到了问题,重新生成实体覆盖了所有文件,这意味着我们丢失了我们为产品列表页面所做的所有更改,但因为我们使用了 git,所以很容易恢复。到目前为止,我们的项目只有几个提交,所以将我们为产品列表 UI 更改所做的提交 cherry-pick 并应用到当前代码库上将会很容易。然而,在现实世界的场景中,在可以重新生成 JDL 之前可能会有很多更改,因此需要一些努力来验证和合并所需更改。始终依赖拉取请求,这样你就可以看到更改了什么,其他人可以审查并发现任何问题。
让我们 cherry-pick 我们需要的更改。
请参考文档了解如何在git-scm.com/docs/git-cherry-pick中 cherry-pick 高级选项。
由于我们需要的是 master 分支上的最后一个提交,我们可以简单地使用 git cherry-pick master。我们也可以切换到 master 分支并使用 git log 命令列出提交,然后复制所需提交的提交哈希值,并使用 git cherry-pick <commit-sha>。
现在,这会导致合并冲突,因为我们当前分支的顶端提交中更新了 product.component.html 文件。我们需要从提交中获取传入的更改,但还需要更新产品类别显示名称从 ID 到代码,所以让我们接受传入的更改,并从 {{product.productCategory?.id}} 手动更新到 {{product.productCategory?.name}}。
通过暂存文件并提交来解决冲突。现在我们可以将分支合并到 master:
> git add src/main/webapp/app/entities/product/product.component.html
> git commit -am "cherrypick: update product listing page UI"
> git checkout master
> git merge --no-ff entity-update-display-name
如果你刚开始使用 Git,建议使用 SourceTree 或 GitKraken 等界面工具进行 cherry-pick 和解决合并冲突。IntelliJ 和 VSCode 等 IDE 以及编辑器也提供了良好的选项。
现在我们的页面视图应该很好:
当然,我们也可以通过将产品列表作为主页来使其更加用户友好。但就目前而言,让我们跳过这个步骤。
由于我们一直在处理客户端代码,我们没有注意到在此期间服务器端代码的更改。我们需要编译 Java 代码来重新加载我们的服务器。让我们运行 ./gradlew compileJava。
不幸的是,在重新加载过程中,我们遇到了一个错误,由于校验和错误,Liquibase 无法更新数据库变更日志:
liquibase.AsyncSpringLiquibase : Liquibase could not start correctly, your database is NOT ready: Validation Failed:
5 change sets check sum
config/liquibase/changelog/20180114123500_added_entity_Customer.xml::20180114123500-1::jhipster was: 7:3e0637bae010a31ecb3416d07e41b621 but is now: 7:01f8e1965f0f48d255f613e7fb977628
config/liquibase/changelog/20180114123501_added_entity_ProductOrder.xml::20180114123501-1::jhipster was: 7:0ff4ce77d65d6ab36f27b229b28e0cda but is now: 7:e5093e300c347aacf09284b817dc31f1
config/liquibase/changelog/20180114123502_added_entity_OrderItem.xml::20180114123502-1::jhipster was: 7:2b3d9492d127add80003e2f7723903bf but is now: 7:4beb407d4411d250da2cc2f1d84dc025
config/liquibase/changelog/20180114123503_added_entity_Invoice.xml::20180114123503-1::jhipster was: 7:5afaca031815e037cad23f0a0f5515d6 but is now: 7:fadec7bfabcd82dfc1ed22c0ba6c6406
config/liquibase/changelog/20180114123504_added_entity_Shipment.xml::20180114123504-1::jhipster was: 7:74d9167f5da06d3dc072954b1487e11d but is now: 7:0b1b20dd4e3a38f7410b6b3c81e224fd
这是因为 JHipster 对原始变更日志所做的更改。在一个理想的世界里,新的架构更改应该在新的变更日志中完成,以便 Liquibase 可以应用它们,但 JHipster 目前还没有默认生成。对于使用 H2 数据库的本地开发,我们可以运行 ./gradlew clean 来清除数据库并重新启动应用程序,但在实际使用案例中,你可能会使用实际的数据库,并且希望保留数据,因此我们在这里必须手动使用 Liquibase 提供的 diff 功能来处理这个问题。
JHipster 在 Gradle 和 Maven 构建中提供了 Liquibase 的集成。你可以利用它来创建新的变更日志和创建 diff 变更日志。在这些情况下,当我们希望保留数据并解决冲突时,Liquibase 的 diff 功能是我们的好朋友。使用 Gradle,你可以运行 ./gradlew liquibaseDiffChangeLog 命令来创建更改集和数据库的 diff 变更日志。你可以将这个更改集添加到 src/main/resources/config/liquibase/master.xml 文件中,它将在你下次重新启动服务器时应用。默认情况下,该命令配置为针对开发数据库运行,如果你想针对生产数据库运行,只需更新 gradle/liquibase.gradle 文件中的 liquibaseCommand 命令定义,并包含生产数据库的详细信息。有关更多信息,请参阅 www.jhipster.tech/development/#using-a-database。
如果你想要清除数据库中的校验和,请使用 ./gradlew liquibaseClearChecksums 任务。
改变应用程序的外观和感觉
使用 Bootstrap 的好处是它让我们能够轻松地使用任何可用的 Bootstrap 主题来改变应用程序的外观和感觉。让我们看看我们如何为我们的应用程序安装一个酷炫的主题,然后我们将使用 Bootstrap 提供的 Sass 变量来调整样式以适应我们的需求。
现在市面上有数百种 Bootstrap 主题。由于我们正在使用 Bootstrap 4,因此选择一个为 Bootstrap 4 定制的主题非常重要。
Bootswatch 是 Bootstrap 主题的精美集合;查看所有可用的主题,请访问:bootswatch.com/。
让我们使用一个名为 materia 的 Bootswatch 主题。
在你的终端中,运行 yarn add bootswatch 来安装所有主题。不用担心;我们只会导入我们想要使用的主题,所以你不需要担心安装所有主题。
现在,让我们使用 Sass 导入这个文件。打开src/main/webapp/content/scss/vendor.scss并找到行@import 'node_modules/bootstrap/scss/bootstrap';,然后添加以下加粗的代码:
// Override Boostrap variables
@import "bootstrap-variables";
@import 'node_modules/bootswatch/dist/materia/variables'; // Import Bootstrap source files from node_modules
@import 'node_modules/bootstrap/scss/bootstrap';
@import "node_modules/bootswatch/dist/materia/bootswatch";
这里主题的名称是 materia,你可以在 Bootswatch 中在这里使用任何可用的主题。确保名称全部是小写。同时,注意导入的顺序。在导入 Bootstrap 变量和主题之后导入主题变量和样式非常重要,以确保 SASS 变量和样式被正确覆盖。
我们可以通过覆盖在src/main/webapp/content/scss/_bootstrap-variables.scss中定义的 Bootstrap 变量来进一步自定义主题。
你可以覆盖 Bootstrap 支持的任何变量。支持的变量完整列表可以在node_modules/bootstrap/scss/_variables.scss中找到。
例如,让我们按照以下方式更改一些颜色,在_bootstrap-variables.scss中:
$primary: #032b4e;
$success: #1df54f;
$info: #17a2b8;
$warning: #ffc107;
$danger: #fa1a30;
当你应用一个新的主题时,可能会有一些 UI 错误,你可以通过更新生成的 SASS 文件来解决它们。
例如,将以下 CSS 添加到src/main/webapp/content/scss/global.scss中,以修复主题更改后出现的复选框错误:
.form-check-input {
height: 18px;
width: 18px;
}
我们现在有一个酷炫的新主题:
更多参考信息可以在以下链接找到:getbootstrap.com/docs/4.0/getting-started/theming/
让我们提交这个更改:
> git add --all
> git commit -am "update bootstrap theme using bootswatch"
添加新的 i18n 语言
由于我们为我们的应用程序启用了 i18n 支持,我们可以轻松地随时添加新的 i18n 语言,使用 JHipster 语言生成器添加一个新的语言到我们的应用程序。
在终端中,切换到一个新的分支并运行以下命令:
> git checkout -b add-french-language
> jhipster languages
你现在会看到一个类似这样的提示,你可以选择任何可用的语言:
Using JHipster version installed locally in current project's node_modules
Executing jhipster:languages
Options:
Languages configuration is starting
? Please choose additional languages to install (Press <space> to select, <a> to toggle all, <i> to inverse selection) >◯ Arabic (Libya)
◯ Armenian
◯ Catalan
◯ Chinese (Simplified)
◯ Chinese (Traditional)
◯ Czech
◯ Danish
(Move up and down to reveal more choices)
现在,我们先选择法语。接受文件更改建议,然后我们就可以继续了。一旦应用程序自动刷新,你可以在应用程序菜单的语言下拉菜单中看到新的语言。现在,这不是很容易吗!
现在有一个问题,因为我们有一些实体并且添加了新的语言。我们需要为实体获取 i18n 法语文件。我们可以通过运行jhipster --with-entities命令轻松完成此操作,该命令将重新生成应用程序以及实体。现在请确保仔细地仅从差异中暂存你需要的更改(与 i18n 相关的 JSON 文件),并丢弃其余的更改。以下是需要暂存的文件和文件夹:
.yo-rc.json
src/main/resources/i18n/messages_fr.properties
src/main/webapp/app/shared/language/find-language-from-key.pipe.ts
src/main/webapp/app/shared/language/language.constants.ts
webpack/webpack.common.js
src/main/webapp/i18n/fr
现在,让我们提交这个更改并将其合并回 master 分支。如果我们只选择了与 i18n 相关的更改,那么我们不应该有任何合并冲突:
> git add --all
> git commit -am "add French as additional language"
> git merge --no-ff add-french-language
使用 Spring Security 进行授权
如您可能已注意到,当涉及到生成的代码时,JHipster 在基于角色的安全、授权管理等方面并没有提供很多功能。这是故意的,因为这些功能高度依赖于具体的使用场景,通常与应用程序的业务逻辑相关。因此,最好由开发者手动编码,作为业务代码的一部分来实现。
普通用户在用户管理中分配有ROLE_USER权限,而管理员用户有ROLE_ADMIN权限。对于我们的用例,存在一些安全漏洞需要我们注意:
-
普通用户应仅能访问查看产品列表、产品订单、订单项、发票和装运。
-
普通用户不应通过 CRUD API 创建/编辑/删除实体。
-
普通用户不应能够访问其他用户的商品订单、订单项、发票和装运。
我们可以使用 Spring Security 提供的功能来克服这些问题。
限制实体的访问权限
首先,让我们限制普通用户的访问权限。这可以在 API 级别轻松使用 Spring Security 完成。将以下片段添加到src/main/java/com/mycompany/store/config/SecurityConfiguration.java的 configure 方法中。
在.antMatchers("/api/**").authenticated()行之前添加它。位置非常重要:
.antMatchers("/api/customers").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/product-categories").hasAuthority(AuthoritiesConstants.ADMIN)
我们指定当请求路径匹配api/customers或api/product-categories时,用户应该有ROLE_ADMIN权限才能访问它们。现在注销并作为user用户登录,尝试访问客户实体页面。查看浏览器开发工具中的控制台,你应该会看到对GET http://localhost:9000/api/customers的调用返回了403 Forbidden错误。
现在既然我们的后端已经正确处理了这个问题,让我们在菜单中隐藏这些条目以供普通用户访问。让我们在src/main/webapp/app/layouts/navbar/navbar.component.html中为客户和产品类别的元素添加一个*jhiHasAnyAuthority="'ROLE_ADMIN'"指令。
现在只有管理员用户会在菜单中看到这些项目。
限制创建/编辑/删除实体的访问权限
现在我们需要确保只有管理员用户可以编辑实体,普通用户只能查看授权给他们的实体。为此,最好在 API 级别使用 Spring Security 的PreAuthorize注解来处理。让我们从订单项开始。转到src/main/java/com/mycompany/store/web/rest/OrderItemResource.java,并将@PreAuthorize("hasAuthority('ROLE_ADMIN')")添加到createOrderItem、updateOrderItem和deleteOrderItem方法中:
@DeleteMapping("/order-items/{id}")
@Timed
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public ResponseEntity<Void> deleteOrderItem(@PathVariable Long id) {
...
}
我们要求 Spring Security 拦截器仅在用户具有ROLE_ADMIN权限时提供对这些方法的访问。PreAuthorize注解会在执行方法之前阻止访问。Spring Security 还提供了PostAuthorize和更通用的Secured注解。更多关于这些内容的信息可以在 Spring Security 文档中找到:projects.spring.io/spring-security/。
使用 ./gradlew compileJava 或 IDE 编译后端。现在转到订单项页面,尝试创建一个订单项。你将在网页控制台上的 API 调用中收到一个 POST http://localhost:9000/api/order-items 403 (Forbidden) 错误。现在让我们将注解添加到所有实体 Resource 类的创建、更新和删除方法中。你可以跳过客户和产品类别实体,因为它们对 ROLE_USER 已经完全禁止访问。
让我们使用 *jhiHasAnyAuthority="'ROLE_ADMIN'" 指令从 Angular 视图中隐藏创建、编辑和删除按钮。
限制对其他用户数据的访问
现在这一点稍微有点棘手,因为它要求我们在后端的服务层更改代码,但这并不难。让我们直接开始。
让我们从产品订单实体开始。让我们修改 src/main/java/com/mycompany/store/service/ProductOrderService.java 中的 findAll 方法,如下所示:
@Transactional(readOnly = true)
public Page<ProductOrder> findAll(Pageable pageable) {
log.debug("Request to get all ProductOrders");
if (SecurityUtils.isCurrentUserInRole(AuthoritiesConstants.ADMIN)) {
return productOrderRepository.findAll(pageable);
} else
return productOrderRepository.findAllByCustomerUserLogin(
SecurityUtils.getCurrentUserLogin().get(),
pageable
);
}
如您所见,我们修改了原始的 productOrderRepository.findAll(pageable) 调用,使其仅在当前用户具有 Admin 角色时调用,否则调用 findAllByCustomerUserLogin,但我们的生成的 ProductOrderRepository 接口还没有这个方法,所以让我们添加它。在 src/main/java/com/mycompany/store/repository/ProductOrderRepository.java 中,让我们添加以下新方法。目前,该接口没有任何方法,只使用从 JpaRepository 继承的方法:
Page<ProductOrder> findAllByCustomerUserLogin(String login, Pageable pageable);
这里正在进行很多魔法操作。这是一个 Spring Data 接口,因此我们可以简单地编写一个新方法,并期望 Spring Data 自动创建实现;我们只需要遵循命名约定。在我们的用例中,我们需要找到所有用户关系为客户的订单,其登录信息与当前登录用户相同。在 SQL 中,这将是以下这样:
select * from product_order po cross join customer c cross join jhi_user u where po.customer_id=c.id and c.user_id=u.id and u.login=:login
简单来说,我们可以说 查找所有产品订单,其中 customer.user.login 等于 login,这正是我们作为 findAllByCustomerUserLogin 方法所写的。正在操作的实体是隐含的,因此省略了产品订单。通过提供 Pageable 参数,我们告诉 Spring Data 从分页实体列表中提供给我们一个页面。您可以参考 Spring Data 文档 (docs.spring.io/spring-data/jpa/docs/current/reference/html/) 获取更多信息。
在调用 productOrderRepository.findAllByCustomerUserLogin 方法时,我们可以使用 SecurityUtils.getCurrentUserLogin() 方法传递当前用户登录信息。SecurityUtils 类也是由 JHipster 生成的,它包含一些有用的方法,例如 getCurrentUserLogin、getCurrentUserJWT、isAuthenticated 和 isCurrentUserInRole。
就这些了。现在以管理员身份登录,创建两个新用户,创建两个客户,并为每个客户创建产品订单。然后注销并再次以默认用户身份登录,看看您是否可以看到新创建用户的产品订单。
现在我们为其他服务进行类似的更新。这些服务的存储库方法如下:
对于 src/main/java/com/mycompany/store/repository/InvoiceRepository:
Page<Invoice> findAllByOrderCustomerUserLogin(String login, Pageable pageable);
对于 src/main/java/com/mycompany/store/repository/OrderItemRepository:
Page<OrderItem> findAllByOrderCustomerUserLogin(String login, Pageable pageable);
对于 src/main/java/com/mycompany/store/repository/ShipmentRepository:
Page<Shipment> findAllByInvoiceOrderCustomerUserLogin(String login, Pageable pageable);
现在,我们需要对服务上的 findOne 方法进行类似的更改。
对于 ProductOrderService,如下所示:
@Transactional(readOnly = true)
public ProductOrder findOne(Long id) {
log.debug("Request to get ProductOrder : {}", id);
if (SecurityUtils.isCurrentUserInRole(AuthoritiesConstants.ADMIN)) {
return productOrderRepository.findOne(id);
} else
return productOrderRepository.findOneByIdAndCustomerUserLogin(
id,
SecurityUtils.getCurrentUserLogin().get()
);
}
如您所见,我们将查找一个通过 ID 和客户用户登录的方法进行了更改。相同的存储库方法如下:
ProductOrder findOneByIdAndCustomerUserLogin(Long id, String login);
对于 src/main/java/com/mycompany/store/repository/InvoiceRepository:
Invoice findOneByIdAndOrderCustomerUserLogin(Long id, String login);
对于 src/main/java/com/mycompany/store/repository/OrderItemRepository:
OrderItem findOneByIdAndOrderCustomerUserLogin(Long id, String login);
对于 src/main/java/com/mycompany/store/repository/ShipmentRepository:
Shipment findOneByIdAndInvoiceOrderCustomerUserLogin(Long id, String login);
同样的查询可以使用 Spring Data 提供的 @Query 注解来编写。
就这些了。我们已经为应用程序实现了良好的基于角色的授权逻辑。
让我们提交这个检查点:
> git add --all
> git commit -am "update role based authorization logic"
在现实世界的场景中,我们迄今为止所做的更改对于电子商务网站来说还不够。但鉴于我们的目标是学习 JHipster 及其支持的工具,而不是创建一个功能完美的应用程序,请将此视为一个最小可行产品。为了使这个电子商务应用程序可用,我们需要构建更多功能,例如购物车、发票生成、客户注册等。为什么不把它作为一个作业来尝试,看看您是否可以为这个应用程序构建更多功能?这将是您完成本书后的下一步行动的一部分。用例和说明将在第十四章中详细说明,JHipster 的最佳实践。
摘要
在本章中,我们看到了如何轻松定制使用 JHipster 创建的 Web 应用程序。我们还学习了在定制我们的产品列表页面时关于 Angular 和 Bootstrap 的知识。除此之外,我们还看到了如何使用 Spring Security 通过基于角色的授权来保护我们的应用程序。我们还学习了 Spring Data,并使用 Git 来正确管理我们的源代码。我们看到了我们的应用程序随着业务逻辑的发展而变得更加用户友好。在下一章中,我们将看到如何使用 Jenkins 将持续集成集成到我们的应用程序中。
第六章:测试和持续集成
现在我们已经搭建并开发了我们的电子商务应用程序,是时候让它为部署到我们的生产环境做好准备。在此之前,我们需要关注两个重要的工程方面,质量 和 稳定性。在本章中,我们将探讨如何使用现代 DevOps 实践,如持续集成和自动化测试来实现这一点。
我们还将看到以下内容:
-
修复和运行测试
-
CI/CD(持续集成/持续部署)工具
-
使用 JHipster CI-CD 子生成器设置 CI
DevOps 是一种将软件开发(Dev)和软件运营(Ops)统一在一起的软件工程实践。DevOps 的主要重点是软件工程所有阶段的自动化和监控,如开发、集成、测试、部署和基础设施管理。DevOps 是本十年最受欢迎的工程实践之一,持续集成和持续部署是其核心方面之二。
修复和运行测试
在我们深入研究持续集成工具之前,让我们首先确保我们的测试在上一章所做的更改后仍然可以正常工作并通过。在一个理想的世界里,如果软件开发是使用诸如 TDD(测试驱动开发)之类的实践进行的,那么编写和修复测试是与代码的开发同时进行的,规范是在实际开发代码之前编写的。你应该尝试遵循这一实践,以便首先编写失败的测试以实现预期的结果,然后开发使测试通过的代码。由于我们的测试是由 JHipster 自动生成的,我们至少可以确保在修改生成的代码时它们是正常工作的。
JHipster 还可以使用 Gatling 为实体生成性能测试。这非常有用,如果你正在开发一个高可用性和高流量的网站,这是必须的。这可以在创建应用程序时启用。有关更多详细信息,请参阅 www.jhipster.tech/running-tests/。
让我们运行我们的单元测试和集成测试,看看是否有任何失败的测试:
-
首先转到你的终端,并导航到 online-store 文件夹。
-
首先使用 Gradle 运行服务器端测试:
> ./gradlew test
注意,JHipster 为服务器端生成单元测试和集成测试。单元测试,文件名为 *UnitTest.java,是简单的 JUnit 测试,用于单元测试功能。集成测试,文件名为 *IntTest.java,旨在使用整个 Spring 环境测试 Spring 组件。它们使用 SpringRunner 类运行,通常启动 Spring 环境,配置所有必需的 bean,并运行测试。
一些测试失败,出现以下错误跟踪:
com.mycompany.store.web.rest.ProductOrderResourceIntTest > getProductOrder FAILED
java.lang.AssertionError at ProductOrderResourceIntTest.java:229
com.mycompany.store.web.rest.ProductOrderResourceIntTest > getAllProductOrders FAILED
java.lang.AssertionError at ProductOrderResourceIntTest.java:213
com.mycompany.store.web.rest.ProductOrderResourceIntTest > getNonExistingProductOrder FAILED
java.lang.AssertionError at ProductOrderResourceIntTest.java:242
com.mycompany.store.web.rest.ShipmentResourceIntTest > getAllShipments FAILED
java.lang.AssertionError at ShipmentResourceIntTest.java:176
com.mycompany.store.web.rest.ShipmentResourceIntTest > getShipment FAILED
java.lang.AssertionError at ShipmentResourceIntTest.java:192
com.mycompany.store.web.rest.ShipmentResourceIntTest > getNonExistingShipment FAILED
java.lang.AssertionError at ShipmentResourceIntTest.java:205
com.mycompany.store.web.rest.InvoiceResourceIntTest > getInvoice FAILED
java.lang.AssertionError at InvoiceResourceIntTest.java:309
com.mycompany.store.web.rest.InvoiceResourceIntTest > getNonExistingInvoice FAILED
java.lang.AssertionError at InvoiceResourceIntTest.java:326
com.mycompany.store.web.rest.InvoiceResourceIntTest > getAllInvoices FAILED
java.lang.AssertionError at InvoiceResourceIntTest.java:289
com.mycompany.store.web.rest.OrderItemResourceIntTest > getNonExistingOrderItem FAILED
java.lang.AssertionError at OrderItemResourceIntTest.java:247
com.mycompany.store.web.rest.OrderItemResourceIntTest > getAllOrderItems FAILED
java.lang.AssertionError at OrderItemResourceIntTest.java:218
com.mycompany.store.web.rest.OrderItemResourceIntTest > getOrderItem FAILED
java.lang.AssertionError at OrderItemResourceIntTest.java:234
2018-02-11 13:55:55.693 INFO 27458 --- [ Thread-10] c.m.store.config.CacheConfiguration : Closing Cache Manager
217 tests completed, 12 failed
您也可以从您的 IDE 中运行测试,以便您有更好的错误信息和失败报告。选择整个 src/test 文件夹,右键单击,并选择运行所有测试。
- 这些测试预期会失败,因为我们已经在上一章将这些实体的
Resource类更改为处理授权,失败意味着它运行得非常完美。幸运的是,使用 Spring 修复测试并不困难。我们可以使用 Spring 测试上下文提供的@WithMockUser注解为我们的测试提供一个模拟用户。将以下代码中突出显示的用户详细信息添加到所有失败的测试类中:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StoreApp.class)
@WithMockUser(username="admin", authorities={"ROLE_ADMIN"}, password = "admin")
public class InvoiceResourceIntTest {
...
}
-
我们在这里提供了一个具有
ADMIN角色的模拟用户。同样添加到OrderItemResourceIntTest、ProductOrderResourceIntTest和ShipmentResourceIntTest中。再次运行测试,它们应该通过。 -
通过运行
git commit -am "fix server side tests with mockUser"提交所做的更改。 -
现在,让我们确保我们的客户端 Karma 单元测试正在运行。由于我们在客户端没有进行任何逻辑更改,因此不应该有任何失败。运行以下命令:
> yarn test
- 所有测试都应该通过。让我们转到
src/test/javascript/spec/app/entities/product/product.component.spec.ts。我们使用 Jasmine 框架进行测试。现有的测试具有以下结构。beforeEach块设置了 AngularTestBed:
...
describe('Component Tests', () => {
describe('Product Management Component', () => {
...
beforeEach(() => {
TestBed.configureTestingModule({
...
})
.overrideTemplate(ProductComponent, '')
.compileComponents();
...
});
it('Should call load all on init', () => {
...
});
...
});
});
- 现在,让我们确保我们的 protractor
e2e测试正在运行。在两个不同的终端中运行以下命令。首先启动服务器。让我们通过运行清理任务来清除数据库,以便测试在全新设置上运行。由于我们正在运行清理任务,我们还需要运行webpackBuildDev任务来重新构建客户端:
> ./gradlew clean webpackBuildDev bootRun
- 现在运行 e2e 测试:
> yarn e2e
如果您不想通过 Yarn 或 NPM 运行脚本,您也可以通过 Gradle 使用 JHipster 提供的节点集成来运行它们。例如,您可以使用 ./gradlew yarn_e2e 代替 yarn e2e,并使用 ./gradlew yarn_test 代替 yarn test。如果您不想安装 NodeJS 和 Yarn,并且希望 Gradle 为您管理一切,这很有用。如果您选择 Maven 而不是 Gradle,该功能也适用于 Maven。
- 所有测试都应该在这里通过。但如果您查看生成的
e2e测试,例如,查看src/test/javascript/e2e/entities/customer.spec.ts,您会看到有一个测试被注释掉了。如果实体有一个必需的关系字段,在生成过程中会注释掉一些测试,因为我们必须首先创建关系并设置其值,以便测试能够运行。让我们只关注Customer页面的测试。取消注释名为 should create and save Customers 的测试,并将测试文件中的describe函数更改为fdescribe,以便只执行此测试文件:
fdescribe('Customer e2e test', () => {
...
});
- 现在执行
yarn e2e,我们应该看到一个失败的测试。首先,让我们通过提供有效的电子邮件格式来修复电子邮件字段:
it('should create and save Customers', () => {
...
customerDialogPage.setEmailInput('email@email.com');
expect(customerDialogPage.getEmailInput()).toMatch('email@email.com');
...
});
- 再次运行
yarn e2e并这次它应该会通过。但由于用户和客户之间存在一对一的关系,如果我们再次运行它,测试将失败,因此我们需要删除它之后创建的行。让我们添加一个删除操作的测试用例。在文件中定义的CustomerComponentsPage类(如果你使用的是 JHipster 5,这个类将在src/test/javascript/e2e/page-objects/customer-page-object.ts下可用)中,添加以下新的属性和方法:
table = element.all(by.css('.table-responsive tbody tr'));
getTable() {
return this.table;
}
deleteFirstItem() {
this.table.first().element(by.css('button.btn-
danger')).click();
}
- 现在将
expect(customerComponentsPage.getTable().isPresent()).toBeTruthy();添加为之前测试的最后一行,以确认是否创建了行。然后添加以下测试以删除该行:
it('should create and save Customers', () => {
...
expect(customerComponentsPage.getTable().isPresent()).toBeTruthy();
});
it('should delete Customers', () => {
customerComponentsPage.deleteFirstItem();
const deleteBtn = element.all(by.css('.modal-footer
.btn.btn-danger'));
deleteBtn.click();
expect(customerComponentsPage.getTable().isPresent()).toBeFalsy();
});
-
再次运行
yarn e2e以验证。不要忘记从文件中移除fdescribe,以便所有测试都能执行。恭喜!你添加了你的第一个 Protractore2e测试。 -
类似地,修复
src/test/javascript/e2e/entities下其他文件中注释掉的e2e测试。这是 下一步操作 的一部分。
持续集成
自动化测试确保我们创建的代码没有错误,同时也确保没有从新代码中引入回归。JHipster 通过为生成的代码创建单元和集成测试在一定程度上有所帮助,但在实际使用案例中,这还不够。我们还需要为引入的业务逻辑添加服务器端单元测试,并为新添加的 API 添加集成测试。你还需要为客户端处理的企业逻辑添加更多单元测试和 e2e 测试,因为 JHipster 只为你生成少量示例测试,并且对您的业务逻辑一无所知。
你拥有的测试越多,你在更改代码时就越有信心,回归的可能性就越小。
测试和持续集成是全栈开发的一个组成部分,也是 DevOps 的重要方面。测试应该被视为与构建高质量产品时开发功能同等重要。持续集成不过是持续地将你的新代码更改合并并测试到隔离环境中,与你的 master/main/stable 代码库进行对比,以识别潜在的错误和回归。这是通过运行针对代码的自动化单元、集成、端到端和其他测试套件来实现的。例如,如果你使用 Git,这些测试通常会在你向 master 分支提交每次更改时以及你创建的每个 pull request 时运行。
一旦我们有了自动化测试,我们可以利用持续集成实践来确保我们引入的新代码不会对我们的稳定代码库造成任何回归。这将给我们合并新代码并将其部署到生产环境的信心。
现代 DevOps 团队通常会更进一步,进行持续交付(持续集成 + 持续部署)。他们通常会定义 CI/CD 管道,以完全自动化的方式持续集成、测试和将代码部署到生产环境中。
拥有良好持续集成和持续部署设置的团队可以更频繁地交付更多功能,同时出现更少的错误。
我是否已经足够强调了持续集成的重要性?
CI/CD 工具
JHipster 为知名的 CI/CD 工具提供了出色的支持。让我们首先看看可用的选项。
Jenkins
Jenkins (jenkins.io/) 是领先的 CI/CD 工具之一。它是免费和开源的。这是一个用 Java 编写的自动化服务器,支持与各种版本控制工具集成,如 Git、CVS、SVN 等。Jenkins 拥有庞大的插件生态系统,这使得它成为最灵活的平台之一。Jenkins 可以用于构建项目、运行自动化测试、自动化部署等。它作为各种平台的可执行二进制文件和 Docker 镜像提供。Blue Ocean 是 Jenkins 的最新 UI 接口,为它带来了急需的新鲜空气。Jenkins 有管道的概念,通过使用多个插件和 Groovy DSL 来定义 CI/CD 管道。Jenkins 管道插件提供了一种基于 DSL 的全面配置,可以在名为 Jenkinsfile 的文件中定义。
Travis CI
Travis CI (travis-ci.org/) 是一个开源的托管 PaaS(平台即服务) 解决方案,用于 CI/CD。对于公共/OSS 项目是免费的,而对于私人/企业项目则需要订阅。它支持用各种语言和平台编写的应用程序,并且被包括 JHipster 在内的开源项目广泛用于持续集成需求。它与版本控制工具有很好的集成,并提供企业版本。它非常容易设置和使用,具有简单的基于 YAML 的配置。高级设置通常使用可以通过钩子由 YAML 配置触发的 shell 脚本来完成。
GitLab CI
GitLab CI (about.gitlab.com/features/gitlab-ci-cd/) 是 GitLab 的一部分 CI/CD 解决方案,GitLab 是 Git 之上的一个 Web UI。它与平台良好集成,当使用 GitLab 时是一个极佳的选择。对于公共项目它是免费和开源的,同时也有企业版本。它既有托管解决方案,也有用于本地部署的二进制文件。
CircleCI
CircleCI (circleci.com/) 是另一个提供托管 PaaS 和本地选项的开源 CI/CD 解决方案。它为小型团队提供免费选项,并为大型团队和企业提供订阅计划。配置简单,基于 YAML,类似于 Travis CI。它提供了选择不同操作系统环境进行构建的选项,并且非常容易设置。
设置 Jenkins
让我们使用 Jenkins 作为我们应用程序的 CI 工具。我们首先需要设置一个本地 Jenkins 实例:
如果你已经熟悉 Docker,你可以使用 Jenkins 提供的官方 Docker 镜像,并可以跳过以下步骤。Docker 镜像将在下一节创建 CD/CI 流水线时由 JHipster 自动生成。有关更多详细信息,请访问www.jhipster.tech/setting-up-ci-jenkins2/。
-
让我们从
mirrors.jenkins.io/war-stable/latest/jenkins.war下载最新的二进制文件。 -
现在打开一个终端,导航到文件下载的文件夹。
-
从终端执行
java -jar jenkins.war --httpPort=8989以启动一个 Jenkins 服务器。端口号不应与我们的应用程序端口冲突。默认密码将在控制台上打印出来。请复制它。 -
导航到
localhost:8989,并粘贴之前复制的密码。 -
点击下一页上的“安装建议插件”按钮,并等待插件安装完成。
-
在下一页创建一个管理员用户并完成。
现在我们已经准备好了 Jenkins 服务器,让我们继续为我们的项目创建一个 Jenkins 流水线。
使用 JHipster 创建 Jenkins 流水线
我们可以使用 JHipster 的ci-cd sub-generator来为我们的项目创建Jenkinsfile:
- 在终端中,首先导航到
online-store文件夹。现在运行以下命令:
> jhipster ci-cd
- 你将需要从以下列表中选择一个选项:
Welcome to the JHipster CI/CD Sub-Generator
? What CI/CD pipeline do you want to generate? (Press <space> to select, <a> to toggle all, <i> to inverse selection) >◯ Jenkins pipeline
◯ Travis CI
◯ GitLab CI
◯ CircleCI
- 让我们从其中选择 Jenkins 流水线。接下来,我们将有一个选项来选择额外的阶段:
? What CI/CD pipeline do you want to generate? Jenkins pipeline
? Jenkins pipeline: what tasks/integrations do you want to include? >◯ Perform the build in a Docker container
◯ Analyze code with Sonar
◯ Send build status to GitLab
◯ Build and publish a Docker image
- 让我们跳过这一步,因为我们现在不需要这些,然后继续。接下来,我们将被询问是否需要从 CI/CD 流水线自动部署到 Heroku:
? What CI/CD pipeline do you want to generate? Jenkins pipeline
? Jenkins pipeline: what tasks/integrations do you want to include?
? Deploy to heroku? >◯ In Jenkins pipeline
- 让我们选择这个选项,因为我们稍后会用到它。一旦选择了选项,JHipster 将生成文件并在控制台上记录以下输出。
create Jenkinsfile
create src/main/docker/jenkins.yml
create src/main/resources/idea.gdsl
Congratulations, JHipster execution is complete!
如果你想使用 Travis 而不是 Jenkins,你可以通过选择 Travis 选项,然后将仓库作为公开仓库发布到 GitHub 来实现。一旦发布,请转到https://github.com/<username>/<repoName>/settings/installations,添加 Travis CI 作为服务并按照说明操作。现在,当你提交时,你可以看到自动构建。有关详细信息,请参阅docs.travis-ci.com/user/getting-started/。
如您所见,我们在根目录中生成了一个Jenkinsfile,在src/main/docker目录中创建了一个 Jenkins 的 Docker 镜像。我们还得到了一个idea.gdsl文件,该文件由 IntelliJ Idea 用于自动完成。
Jenkinsfile 及其阶段
让我们看看生成的Jenkinsfile,它使用 Groovy DSL 定义了我们的流水线:
#!/usr/bin/env groovy
node {
stage('checkout') {
checkout scm
}
stage('check java') {
sh "java -version"
}
stage('clean') {
sh "chmod +x gradlew"
sh "./gradlew clean --no-daemon"
}
stage('install tools') {
sh "./gradlew yarn_install -PnodeInstall --no-daemon"
}
stage('backend tests') {
try {
sh "./gradlew test -PnodeInstall --no-daemon"
} catch(err) {
throw err
} finally {
junit '**/build/**/TEST-*.xml'
}
}
stage('frontend tests') {
try {
sh "./gradlew yarn_test -PnodeInstall --no-daemon"
} catch(err) {
throw err
} finally {
junit '**/build/test-results/karma/TESTS-*.xml'
}
}
stage('packaging') {
sh "./gradlew bootRepackage -x test -Pprod -PnodeInstall --
no-daemon"
archiveArtifacts artifacts: '**/build/libs/*.war',
fingerprint: true
}
stage('deployment') {
sh "./gradlew deployHeroku --no-daemon"
}
}
我们定义了多个按顺序运行的阶段,用粗体突出显示;确切地说有八个。它从从版本控制中检出分支开始,以部署到 Heroku 结束(我们将在下一章中了解更多关于这一点)。
步骤相当直接,因为其中大部分只是触发 Gradle 任务。让我们看看每一个:
stage('checkout') {
checkout scm
}
checkout阶段对触发构建的源代码修订版本进行本地检出:
stage('check java') {
sh "java -version"
}
这个check java阶段只是打印出 Jenkins 环境中安装的 Java 版本:
stage('clean') {
sh "chmod +x gradlew"
sh "./gradlew clean --no-daemon"
}
clean阶段首先在类 Unix 操作系统上为 Gradle wrapper 授予执行权限,然后执行 Gradle clean 任务。--no-daemon标志禁用了 Gradle 守护进程功能,这在 CI 环境中不是必需的:
stage('install tools') {
sh "./gradlew yarn_install -PnodeInstall --no-daemon"
}
install tools阶段通过运行 Gradle 的yarn install确保安装了 NodeJS 和所有 NPM 模块。
-PnodeInstall标志确保如果尚未安装,首先安装 NodeJS:
stage('backend tests') {
try {
sh "./gradlew test -PnodeInstall --no-daemon"
} catch(err) {
throw err
} finally {
junit '**/build/**/TEST-*.xml'
}
}
backend tests阶段通过触发 Gradle 测试任务运行所有服务器端集成和单元测试。如果出现错误,它将使 Jenkins 管道失败,并在测试运行完成后使用 JUnit 插件在 Jenkins web UI 上注册测试报告:
stage('frontend tests') {
try {
sh "./gradlew yarn_test -PnodeInstall --no-daemon"
} catch(err) {
throw err
} finally {
junit '**/build/test-results/karma/TESTS-*.xml'
}
}
与之前类似,frontend tests阶段通过触发Gradle任务中的 yarn test 命令运行客户端单元测试。它也会在出现错误时使管道失败,并在测试运行完成后使用 JUnit 插件在 Jenkins web UI 上注册测试报告:
stage('packaging') {
sh "./gradlew bootRepackage -x test -Pprod -PnodeInstall --no-
daemon"
archiveArtifacts artifacts: '**/build/libs/*.war', fingerprint:
true
}
packaging阶段通过使用prod配置触发Gradle bootRepackage任务,并将创建的 WAR 文件存档,带有唯一的指纹:
stage('deployment') {
sh "./gradlew deployHeroku --no-daemon"
}
最后一个阶段是用于deployment的,它也使用 Gradle 任务来完成。我们将在下一章中详细说明。现在,让我们注释掉这个阶段。我们稍后会重新启用它。
现在让我们通过运行这些命令将所有内容提交到git。确保你处于主分支,否则请将分支与主分支合并:
> git add --all
> git commit -am "add Jenkins pipeline for ci/cd"
在 Jenkins 服务器上设置 Jenkinsfile
既然我们的Jenkinsfile已经准备好了,让我们为我们的应用程序设置 CI/CD。首先,我们需要将我们的应用程序上传到 Git 服务器,例如 GitHub、GitLab 或 BitBucket。让我们使用 GitHub (github.com/) 来做这件事。确保你首先在 GitHub 上创建了一个账户:
- 在 GitHub 中创建一个新的仓库 (
github.com/new);让我们称它为 online-store。不要选择“使用 README 初始化此仓库”选项。一旦创建,你将看到添加代码的说明。让我们选择通过在 online-store 应用程序文件夹内运行以下命令从命令行推送现有仓库的选项。不要忘记将<username>替换为你的实际 GitHub 用户名:
> cd online-store
> git remote add origin https://github.com/<username>/online-store.git
> git push -u origin master
-
现在,通过访问
http://localhost:8989/进入 Jenkins 服务器 web UI,并使用“创建新作业”链接创建一个新的作业。 -
输入一个名称,从列表中选择“Pipeline”,然后点击“确定”:
-
然后,在下一页上,执行以下操作:
-
滚动或点击“构建触发器”部分。
-
选择“Poll SCM”复选框。
-
将 cron 计划值输入为 H/01 ** ** ** **,以便 Jenkins 每分钟轮询我们的仓库,并在有新提交时构建:
-
-
接下来,在同一页面上:
-
滚动页面或点击“管道”部分。
-
从下拉菜单中选择“从 SCM 选择管道脚本”作为定义字段。
-
从下拉菜单中选择“Git”作为 SCM 字段。
-
添加应用的仓库 URL。
-
最后,点击“保存”:
-
点击“立即构建”以触发新的构建来测试我们的管道:
-
我们现在应该能在 Web UI 上看到构建已经开始,其进度如下截图所示:
恭喜!我们已经成功为我们的应用设置了 CI/CD。当您提交新更改时,构建将被触发。
您还可以使用 Jenkins Blue Ocean 插件的新 UI 查看管道状态。从插件管理器安装插件(点击顶部菜单中的 Jenkins,然后转到管理 Jenkins | 管理插件 | 可用插件,搜索Blue Ocean并安装它)。左侧菜单上有“打开 Blue Ocean”链接。构建将如下所示:
点击一个构建来查看管道。您可以在进度指示器上点击每个阶段,列出从该阶段开始的步骤,然后展开列表项以查看该步骤的日志:
摘要
在本章中,我们探讨了 CI/CD 是什么,以及 JHipster 支持的工具。我们还学习了如何设置 Jenkins,并使用 JHipster 和 Jenkins 创建我们的 CI/CD 管道。我们还修复了我们的自动化测试,并使它们在 CI 服务器上运行。
在下一章中,我们将看到如何使用云托管提供商(如 Heroku)将我们的应用部署到生产环境。
第七章:进入生产环境
我们的应用程序几乎准备好了,现在是时候进入生产环境了。由于这是云计算的时代,我们将把我们的应用程序部署到云服务提供商——具体来说,是 Heroku。在我们继续部署我们的应用程序到生产环境之前,我们需要确保我们的应用程序在我们的本地环境中已经准备好。熟悉在这个阶段将会有用的技术和工具也会很有益处。
在本章中,我们将学习以下内容:
-
Docker 简介
-
使用 Docker 启动生产数据库
-
Spring 配置简介
-
打包应用程序以进行本地部署
-
升级到 JHipster 的最新版本
-
JHipster 支持的部署选项简介
-
在 Heroku 云上进行生产部署
Docker 简介
Docker 是近年来在 DevOps 领域占据中心舞台的最具颠覆性的技术之一。Docker 是一种使操作系统级别的虚拟化或容器化成为可能的技术,它也是开源的,并且可以免费使用。Docker 旨在用于 Linux,但可以使用 Docker for Mac 和 Docker for Windows 等工具在 Mac 和 Windows 上使用。
Docker 容器
当我们在 Docker 世界中谈论容器时,从技术上讲,我们是在谈论 Linux 容器。正如红帽在其网站上所述(www.redhat.com/en/topics/containers/whats-a-linux-container):
Linux 容器是一组从系统其余部分隔离出来的进程,它们从一个提供所有必要文件以支持这些进程的独立镜像中运行。通过提供一个包含应用程序所有依赖项的镜像,它在从开发到测试,最后到生产的移动过程中是可移植的和一致的。
虽然这个概念并不新颖,但 Docker 使得创建易于构建、部署、版本控制和共享的容器成为可能。Docker 容器仅包含在宿主操作系统上运行应用程序所需的依赖项;它共享宿主系统硬件的操作系统和其他依赖项。这使得 Docker 容器在大小和资源使用方面比虚拟机(VM)更轻,因为它不需要传输整个操作系统和模拟虚拟硬件。因此,Docker 在很多传统使用场景中使虚拟机变得过时,这些场景原本是通过虚拟机技术处理的。这也意味着,与使用虚拟机相比,使用 Docker 我们可以在相同的硬件上运行更多的应用程序。Docker 容器是 Docker 镜像的实例,Docker 镜像是一组层,它描述了正在容器化的应用程序。它们包含运行应用程序所需的代码、运行时、库、环境变量和配置文件。
Dockerfile
Dockerfile 是一组指令,告诉 Docker 如何构建 Docker 镜像。通过在特定的 Dockerfile 上运行 docker build 命令,我们将生成一个可用于创建 Docker 容器的 Docker 镜像。现有的 Docker 镜像可以用作新 Dockerfile 的基础,因此可以重用和扩展现有镜像。
以下代码来自我们应用程序的 Dockerfile:
FROM openjdk:8-jre-alpine
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
JHIPSTER_SLEEP=0 \
JAVA_OPTS=""
CMD echo "The application will start in ${JHIPSTER_SLEEP}s..." && \
sleep ${JHIPSTER_SLEEP} && \
java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /app.war
EXPOSE 8080 5701/udp
ADD *.war /app.war
FROM 指令指定初始化构建时使用的基镜像。在这里,我们指定 Open JDK 8 作为我们的 Java 运行时。
ENV 指令用于设置环境变量,而 CMD 指令用于指定要执行的命令。
EXPOSE 指令用于指定容器在运行时监听的端口。
访问 docs.docker.com/engine/reference/builder/ 获取完整的参考。
Docker Hub
Docker Hub (hub.docker.com/) 是 Docker 提供的在线注册库。它可以用来发布公共和私有 Docker 镜像。这使得共享和重用 Docker 镜像变得极其容易。
要从注册库获取 Docker 镜像,我们只需运行 docker pull <image-name>。
这使得在不本地安装第三方工具的情况下,只需从注册库拉取并运行容器即可轻松使用第三方工具。
Docker Compose
Docker Compose 是 Docker 平台中的一个工具,用于定义和运行多容器应用程序。它让我们定义容器在生产环境中运行时的行为,还让我们定义它所依赖的其他服务以及服务之间如何协同工作。每个应用程序都是一个服务,因为它定义了容器的行为,例如它运行在哪个端口上,它使用哪些环境变量,等等。使用 YAML 文件进行此操作。单个 docker-compose.yml 文件可以定义多容器应用程序所需的所有服务,然后可以通过单个命令启动。我们将在第十一章 Deploying with Docker Compose 中了解更多关于 Docker 和 docker-compose 的内容。
访问 docs.docker.com/get-started/ 了解更多关于 Docker 的信息。
下表列出了 Docker 和 Docker Compose 的有用命令:
docker build -t myapp:1.0. | 从当前目录的 Dockerfile 构建镜像并标记镜像 |
|---|---|
docker images | 列出本地存储的所有 Docker 镜像 |
docker pull alpine:3.4 | 从注册库拉取镜像 |
docker push myrepo/myalpine:3.4 | 将镜像推送到注册库 |
docker login | 登录到注册库(默认为 Docker Hub) |
| docker run --rm -it -p 5000:80 -v /dev/code alpine:3.4 /bin/sh | 运行 Docker 容器**--rm**:容器退出后自动删除 -it:将容器连接到终端
-p:外部暴露端口 5000 并映射到端口 80
-v:在容器内创建一个主机挂载卷
alpine:3.4:从该镜像实例化的容器
/bin/sh:在容器内运行的命令
docker stop myApp | 停止一个正在运行的容器 |
|---|---|
docker ps | 列出正在运行的容器 |
docker rm -f $(docker ps -aq) | 删除所有正在运行和已停止的容器 |
docker exec -it web bash | 在容器内创建一个新的 bash 进程并将其连接到终端 |
docker logs --tail 100 web | 打印容器日志的最后 100 行 |
docker-compose up | 启动当前文件夹中 docker-compose.yml 文件定义的服务 |
docker-compose down | 停止当前文件夹中 docker-compose.yml 文件定义的服务 |
使用 Docker 启动生产数据库
JHipster 为应用程序创建一个 Dockerfile,并为我们在 src/main/docker 下选择的所有技术(如数据库、搜索引擎、Jenkins 等)提供 docker-compose 文件:
├── app.yml - Main compose file for the application
├── Dockerfile - The Dockerfile for the application
├── hazelcast-management-center.yml - Compose file hazelcast management center
├── jenkins.yml - Compose file for Jenkins
├── mysql.yml - Compose file for the database that we choose.
└── sonar.yml - COmpose file for SonarQube.
让我们看看如何使用在 src/main/docker/mysql.yml 下提供的组合文件来使用 Docker 启动我们的生产数据库。你将需要使用终端来执行以下指令:
-
运行
docker --version和docker-compose --version以确保这些已安装。 -
运行
docker ps以列出正在运行的容器。如果你还没有运行任何容器,你应该看到一个空列表。 -
让我们通过运行
docker-compose -f src/main/docker/mysql.yml up来启动数据库。
你将看到以下控制台输出:
如果你想在后台运行服务,将 -d 标志传递给命令。docker-compose -f src/main/docker/mysql.yml up -d 将允许你继续使用相同的终端,而无需切换到另一个。
现在如果你再次运行 docker ps,它应该会列出我们启动的数据库服务:
Spring 配置文件的介绍
在我们准备应用程序投入生产之前,让我们简单谈谈 Spring 配置文件。
Spring 配置文件(docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-definition-profiles-java)允许您根据环境更改应用程序的行为。这是通过使用 @Profile 注解和特定配置文件来实现的,可以通过指定 spring.profiles.active 属性来激活。根据我们在这里设置的配置文件,Spring 将选择适当的 application.properties/application.yml 文件,并使用 Java 源代码中的 @Profile 注解包含/排除特定配置文件包含/排除的组件。例如,如果我们设置 spring.profiles.active=prod,所有具有 @Profile("prod") 的 Spring 组件都将被实例化,任何具有 @Profile("!prod") 的组件将被排除。同样,如果 application-prod.yml 或 application-prod.properties 文件在类路径上可用,Spring 将加载并使用它。
JHipster 默认配置了 dev 和 prod 配置文件,并在 src/main/resources/config 文件夹中包含了 application-dev.yml 和 application-prod.yml 文件,以及基本的 application.yml 文件。JHipster 还更进一步,为 Gradle 构建提供了 dev 和 prod 配置文件(同样适用于 Maven),这样我们就可以为特定配置文件构建/运行应用程序,这非常方便。以下是 application-dev.yml 文件中定义的配置文件和数据库配置:
...
spring:
profiles:
active: dev
include: swagger
...
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:h2:file:./build/h2db/db/store;DB_CLOSE_DELAY=-1
username: store
password:
...
JHipster 应用程序中可用的配置文件如下:
dev | 专为开发和生产力优化,启用了 Spring 开发工具、内存数据库等 |
|---|---|
prod | 专为生产优化,侧重于性能和稳定性 |
swagger | 启用 API 的 Swagger 文档 |
no-liquibase | 禁用 Liquibase,在不需要 Liquibase 运行的生产环境中很有用 |
打包应用程序以进行本地部署
现在,让我们构建我们的应用程序并将其本地部署。这可以通过两种方式完成,要么使用 Docker,要么构建并执行一个 WAR 文件。
使用 Docker 构建和部署
让我们使用 Gradle 任务来构建我们的 Docker 镜像。
使用 ./gradlew tasks 命令列出所有可用任务。
-
在您的终端中,进入项目根目录并执行;
./gradlew bootRepackage -Pprod buildDocker:-
bootRepackage:为应用程序构建可执行归档(WAR)文件
-
-Pprod:指定要使用的配置文件
-
buildDocker:基于
src/main/docker文件夹中的 Dockerfile 构建 Docker 镜像
-
如果您使用的是 JHipster 版本 5 或更高版本,请使用 bootWar 命令代替 Gradle 中的 bootRepackage 命令。
- 任务成功完成后,我们可以通过运行以下命令来部署我们的应用程序:
> docker-compose -f src/main/docker/app.yml up
如果您还没有启动 MySQL 数据库,这将也会启动它。如果您已经在之前的步骤中启动了它,那么 docker-compose 将会跳过这一步。
当我们在控制台看到以下输出时,我们的应用程序就准备好了。如您所见,它正在使用 prod 和 swagger 配置运行:
使用您最喜欢的浏览器访问 http://localhost:8080 来查看应用程序的实际运行情况。
构建和部署可执行存档
如果您不想使用 Docker,则可以通过完成以下步骤在本地使用生产配置部署应用程序:
-
首先,请确保 MySQL 数据库在之前的步骤中正在运行;如果不是,请使用
docker-compose -f src/main/docker/mysql.yml up -d启动它。 -
现在让我们通过运行
./gradlew bootRepackage -Pprod来为 prod 配置创建一个可执行的存档。 -
一旦构建成功,
build/libs下将创建两个存档(WAR)。store-0.0.1-SNAPSHOT.war文件是一个可执行存档,可以直接在 JVM 上运行,而store-0.0.1-SNAPSHOT.war.original是一个普通的 WAR 文件,可以部署到服务器,如 JBoss 或 Tomcat。 -
让我们使用可执行存档。只需运行
./build/libs/store-0.0.1-SNAPSHOT.war来启动应用程序。如果您使用的是 Windows,请运行java -jar build/libs/store-0.0.1-SNAPSHOT.war。
一旦应用程序启动,您将在控制台上看到打印的 URL。使用您最喜欢的浏览器访问 http://localhost:8080 来查看应用程序的实际运行情况。
升级到 JHipster 的最新版本
JHipster 提供了一个升级子生成器 (www.jhipster.tech/upgrading-an-application/),以帮助您使用新的 JHipster 版本升级应用程序。它非常有用,因为它为您自动化了许多手动步骤,您唯一需要做的是在升级完成后解决任何合并冲突。让我们升级我们的应用程序,怎么样?
- 在您的终端中,执行
jhipster upgrade命令。如果有新的 JHipster 版本可用,则升级过程将开始;如果没有,则进程将退出。
一旦进程开始,您将看到详细的控制台日志,显示正在发生的事情。如您所见,这个子生成器使用的是全局 JHipster 版本,而不是本地版本,这与其他子生成器不同:
Using JHipster version installed globally
Executing jhipster:upgrade
Options:
Welcome to the JHipster Upgrade Sub-Generator
This will upgrade your current application codebase to the latest JHipster version
Looking for latest generator-jhipster version...
yarn info v1.5.1
4.14.1
Done in 0.16s.
New generator-jhipster version found: 4.14.1
info git rev-parse -q --is-inside-work-tree
Git repository detected
info git status --porcelain
info git rev-parse -q --abbrev-ref HEAD
info git rev-parse -q --verify jhipster_upgrade
info git checkout --orphan jhipster_upgrade
Created branch jhipster_upgrade
info Removing .angular-cli.json
...
Cleaned up project directory
Installing JHipster 4.13.3 locally
info yarn add generator-jhipster@4.13.3 --dev --no-lockfile --ignore-scripts
yarn add v1.5.1
...
Done in 6.16s.
Installed generator-jhipster@4.13.3
Regenerating application with JHipster 4.13.3...
warning package.json: No license field
/home/deepu/Documents/jhipster-book/online-store/node_modules/.bin
info "/home/deepu/Documents/jhipster-book/online-store/node_modules/.bin/jhipster" --with-entities --force --skip-install
Using JHipster version installed globally
Running default command
Executing jhipster:app
Options: withEntities: true, force: true, skipInstall: true, with-entities: true, skip-install: true
...
Server application generated successfully.
...
Client application generated successfully.
...
Entity generation completed
...
Congratulations, JHipster execution is complete!
Successfully regenerated application with JHipster 4.13.3
info Removing src/main/resources/keystore.jks
info git add -A
info git commit -q -m "Generated with JHipster 4.13.3" -a --allow-empty
Committed with message "Generated with JHipster 4.13.3"
info git checkout -q master
Checked out branch "master"
info git --version
info git merge --strategy=ours -q --no-edit --allow-unrelated-histories jhipster_upgrade
Current code has been generated with version 4.13.3
info git checkout -q jhipster_upgrade
Checked out branch "jhipster_upgrade"
Updating generator-jhipster to 4.14.1 . This might take some time...
info yarn add generator-jhipster@4.14.1 --dev --no-lockfile --ignore-scripts
...
Done in 30.40s.
Updated generator-jhipster to version 4.14.1
info Removing .angular-cli.json
...
Cleaned up project directory
Regenerating application with JHipster 4.14.1...
/home/deepu/Documents/jhipster-book/online-store/node_modules/.bin
info "/home/deepu/Documents/jhipster-book/online-store/node_modules/.bin/jhipster" --with-entities --force --skip-install
Using JHipster version installed globally
Running default command
Executing jhipster:app
Options: withEntities: true, force: true, skipInstall: true, with-entities: true, skip-install: true
...
Entity generation completed
Congratulations, JHipster execution is complete!
Successfully regenerated application with JHipster 4.14.1
info Removing src/main/resources/keystore.jks
info git add -A
info git commit -q -m "Generated with JHipster 4.14.1" -a --allow-empty
Committed with message "Generated with JHipster 4.14.1"
info git checkout -q master
Checked out branch "master"
Merging changes back to master...
info git merge -q jhipster_upgrade
Merge done!
info git diff --name-only --diff-filter=U package.json
WARNING! There are conflicts in package.json, please fix them and then run yarn
Start your Webpack development server with:
yarn start
info git diff --name-only --diff-filter=U
Upgraded successfully.
WARNING! Please fix conflicts listed below and commit!
gradle/wrapper/gradle-wrapper.properties
package.json
src/test/java/com/mycompany/store/web/rest/CustomerResourceIntTest.java
Congratulations, JHipster execution is complete!
子生成器按以下顺序执行:
-
检查是否有新的 JHipster 版本可用(如果使用
--force则不适用)。 -
检查应用程序是否已经初始化为 GIT 仓库;如果不是,JHipster 将为您初始化一个,并将当前代码库提交到 master 分支。
-
检查确保仓库中没有未提交的本地更改。如果发现任何未提交的更改,则进程将退出。
-
检查是否存在
jhipster_upgrade分支。如果不存在,则创建一个分支。 -
检出
jhipster_upgrade分支。 -
将 JHipster 升级到最新版本。
-
清理当前项目目录。
-
使用
jhipster --force --with-entities命令重新生成应用程序。 -
将生成的代码提交到
jhipster_upgrade分支。 -
将
jhipster_upgrade分支合并回启动jhipster upgrade命令的原始分支。
在解决合并冲突之前,先看看升级后有什么变化。查看已暂存的更改。仔细检查更改,确保一切正常,特别是在我们之前进行过自定义的文件中。我的变更日志如下;注意,由于有 147 个文件被更新,所以我截断了底部:
幸运的是,我们只有三个冲突,因此它们应该很容易解决。package.json中的冲突源于我们为了集成 Bootswatch 所做的更改。仔细解决文件中的冲突暂存,然后继续下一个文件。
一旦所有冲突都得到解决,暂存文件并将它们提交:
> git add --all
> git commit -am "update to latest JHipster version"
确保一切正常工作。使用./gradlew test && yarn && yarn test运行服务器端和客户端测试,并通过运行./gradlew clean webpackBuildDev bootRun命令启动应用程序来验证这一点。
JHipster 支持的部署选项简介
现在我们已经通过本地部署验证了我们的生产构建,让我们看看如何通过使用云服务将其推向实际生产。JHipster 默认支持大多数云平台,并为流行的平台如 Heroku、Cloudfoundry 和 AWS 提供了特殊的子生成器命令。
JHipster 还支持如 OpenShift、Google Cloud(使用 Kubernetes)和 Rancher 等平台,但让我们在接下来的章节中了解它们,因为它们更适合微服务。尽管如此,从理论上讲,您也可以将它们用于单体部署。
Heroku
Heroku (www.heroku.com/)是 Salesforce 的云平台。它允许您在云上部署、管理和监控应用程序。Heroku 专注于应用程序而不是容器,并支持从 NodeJS 到 Java 再到 Go 的各种语言。JHipster 提供了 Heroku 子生成器,该生成器由 Heroku 构建和维护,使得将 JHipster 应用程序部署到 Heroku 云变得容易。它使用 Heroku CLI,您需要一个 Heroku 账户才能使用它。子生成器可用于部署和更新您的应用程序到 Heroku。
访问www.jhipster.tech/heroku/获取更多信息。
Cloud Foundry
Cloud Foundry 是由 Cloud Foundry 基金会管理的多云计算平台。它最初由 VMWare 创建,现在是 Spring 框架背后的公司 Pivotal 的旗下。它提供了一种多云解决方案,目前由 Pivotal Cloud Foundry (PCF), Pivotal Web Services (PWS), Atos Canary, SAP 云平台和 IBM Bluemix 等支持。该平台是开源的,因此可以用来设置自己的私有实例。JHipster 提供了一个子生成器,可以轻松地将 JHipster 应用程序部署到任何 Cloud Foundry 提供商。它使用 Cloud Foundry 命令行工具。
访问 www.jhipster.tech/cloudfoundry/ 获取更多信息。
Amazon Web Services
Amazon Web Services (AWS) 是领先的云计算平台,提供平台、软件和基础设施作为服务。AWS 提供了 Elastic Beanstalk 作为在云上部署和管理应用程序的简单平台。JHipster 提供了一个子生成器,可以将 JHipster 应用程序部署到 AWS 或 Boxfuse (www.jhipster.tech/boxfuse/),一个替代服务。
访问 www.jhipster.tech/aws/ 获取更多信息。
将生产部署到 Heroku 云平台
我们需要选择一个云提供商。对于这个演示,让我们选择 Heroku。
虽然 Heroku 账户是免费的,并且你可以获得免费额度,但为了使用 MySQL 和其他附加组件,你将需要提供信用卡信息。只有当你超出免费配额时,你才会被收费。
让我们通过以下步骤将我们的应用程序部署到 Heroku:
-
首先,你需要在 Heroku 中创建一个账户 (
signup.heroku.com/)。这是免费的,你也会获得免费额度。 -
按照以下链接
devcenter.heroku.com/articles/heroku-cli安装 Heroku CLI 工具。 -
通过运行
heroku --version验证 Heroku CLI 是否安装正常。 -
通过运行
heroku login登录到 Heroku。当提示时,输入你的 Heroku 电子邮件和密码。 -
现在运行
jhipster heroku命令。你将开始看到问题。 -
当被问及部署名称时,选择你喜欢的名称:(存储)。默认情况下,它将使用应用程序名称。尽量选择一个独特的名称,因为 Heroku 命名空间是共享的。
-
接下来,你将被要求选择一个区域——你希望在哪个区域部署?选择美国或欧盟,然后继续。
-
生成器将创建所需的文件,并接受 Gradle 构建文件建议的更改。
控制台输出将看起来像这样:
生成的 .yml 文件为应用程序添加了 Heroku 特定的配置。Procfile 包含了在 Heroku 上执行的应用程序特定命令。Gradle 构建也被修改以包含 Heroku 所需的依赖项。
生成文件后,它将构建应用程序并开始上传工件。这可能会根据你的网络延迟需要几分钟。一旦成功完成,你应该会看到以下屏幕:
现在运行 heroku open 命令以在浏览器中打开已部署的应用程序。就这样,你已成功使用几个命令将应用程序部署到 Heroku。
当你进一步更新应用程序时,你可以使用 ./gradlew -Pprod bootRepackage 命令重新构建包,然后使用 heroku deploy:jar --jar build/libs/*war 命令重新部署它。
不要忘记通过执行以下命令将所做的更改提交到 git:
> git add --all
> git commit -am "add heroku configuration"
摘要
部署到生产是应用程序开发中最重要阶段之一,也是最重要的一个。借助 JHipster,我们轻松地将应用程序部署到了云服务提供商。我们还了解了 Docker 和其他可用的各种部署选项。我们还使用了升级子生成器来确保我们的应用程序与 JHipster 保持最新。
到目前为止,我们看到了如何使用 JHipster 开发和部署单体电子商务应用程序。我们从一个单体开始,在接下来的章节中,我们将看到如何在 JHipster 的帮助下将我们的应用程序扩展到微服务架构。在下一章中,我们将学习关于不同的微服务技术和工具。所以,请保持关注!
第八章:微服务服务器端技术简介
使用 JHipster 开发一个生产就绪的单体应用不是很容易吗?到目前为止,我们已经从头开始创建了一个应用,使用 JDL Studio 添加了一些实体,然后将其与测试一起部署到生产环境中。我们还添加了持续集成和持续交付管道。这种体验不是更快、更简单、更好吗?
那么,接下来是什么?是的,你猜对了——微服务!
微服务现在是到处都在谈论的热门词汇。许多公司都在尝试用微服务来解决他们的问题。我们已经在第一章“现代 Web 应用开发简介”中看到了微服务的好处概述。
在本章中,我们将探讨以下内容:
-
微服务相对于单体应用的好处
-
我们需要构建完整的微服务架构的组件
在本章中,我们将看到我们之前创建的单体应用如何被转换成一个微服务应用。
之后,我们将看到使用 JHipster 提供的选项创建微服务架构是多么容易。
微服务应用与单体应用对比
通过与单体架构的比较,可以更好地理解微服务架构的好处。
当它们被正确设计和部署时,微服务相对于单体应用的好处是惊人的。
这并不像根据结构、组件或功能将单体应用拆分,然后作为独立服务部署那么简单。这行不通。将单体应用或甚至单体设计转换为微服务需要一个清晰的产品愿景。这包括了解项目的哪些部分会改变,哪些部分会保持一致。我们必须有低级别的细节,比如我们应该将哪些实体分组在一起,哪些可以分离。
这清楚地说明了需要一个不断发展的模型的需求。拆分应用中使用的技术比拆分相互依赖的模型或应用的业务逻辑要容易得多。因此,将项目的主要重点放在核心领域及其逻辑上至关重要。
微服务应该是独立的。当一个组件与另一个组件紧密耦合时,它们会失败。最棘手的部分是识别和隔离组件。
当我们完成这个任务后,它相对于单体应用有以下好处。
单体代码是一个单一单元。因此,应用的所有部分共享相同的内存。对于更大的系统,我们需要更大的基础设施。当应用增长时,我们需要根据需要扩展基础设施。对已经较大的基础设施进行扩展总是对运营来说既困难又昂贵的任务。
尽管它们在单个地方拥有处理产品中任何事物的所有必要代码(无需担心延迟或可用性),但处理其运行所消耗的资源却很困难,而且肯定不可扩展。如果应用程序的任何一部分失败,整个产品都会受到影响。当产品的任何线程或查询粘附在内存上时,影响将会被数百万客户看到。
相反,微服务由于我们将应用程序拆分为更小的组件,因此运行时所需的内存更少。这反过来又降低了基础设施的成本。例如,运行 10 个 2GB 实例(在 AWS 上的成本约为每月 170 美元)比运行单个 16GB 实例(在 AWS 上的成本约为每月 570 美元)更便宜。每个组件都在自己的环境中运行,这使得微服务对开发者和云原生更加友好。同样,微服务也增加了服务之间的吞吐量。一个服务上的内存密集型操作不会影响任何其他服务。
随着时间的推移,单体架构将消除团队的敏捷性,从而延迟应用程序的发布。这意味着当添加新功能或现有功能中的某些内容出现问题时,人们往往会花更多的时间来寻找解决方案以解决问题。单体架构将带来更多的低效,从而增加技术债务。
另一方面,微服务在架构方面减少了技术债务,因为一切都被简化为单个组件。团队往往更加敏捷,他们会发现处理变更更容易。
代码越少,错误越少,意味着痛苦更少,修复时间更短。
单体应用程序的工作量更大。想象一下有一个大型的单体应用程序,你需要在服务层中撤销一个if 条件。在更改代码后,通常需要几分钟来构建,然后你必须测试整个应用程序,这将降低团队的表现。
对于微服务架构,你可以几秒钟内重新启动或重新加载应用程序。当你需要撤销一个if 条件时,你不需要等待几分钟来构建和部署应用程序以进行测试,你可以在几秒钟内完成。这将减少执行日常任务所需的时间。
更快的迭代/发布和减少停机时间是提高用户参与度和用户保留率的关键,这反过来又会导致更好的收入。
人类大脑(除非你是超人)只能处理有限的信息量。因此,从认知角度来看,微服务帮助人们减少杂乱,专注于功能。这使生产力更高,部署更快。
采用微服务将:
-
最大化生产力
-
提升敏捷性
-
提升客户体验
-
加速开发/单元测试(如果设计得当)
-
提升收入
微服务架构的构建块
运行微服务架构需要许多组件/功能,并涉及许多高级概念。为了理解这些概念,想象我们有一个基于微服务的应用程序,用于我们的电子商务购物网站。这包括以下服务:
-
定价服务:负责根据需求给出产品的价格
-
需求服务:负责根据销售和剩余库存计算产品的需求
-
库存服务:负责跟踪库存中剩余的数量
-
许多其他服务
我们将在本节中看到的一些概念是:
-
服务注册表
-
服务发现
-
健康检查
-
动态路由和弹性
-
安全性(身份验证和授权)
-
容错和故障转移
服务注册表
微服务是独立的,但许多用例将需要它们相互依赖。这意味着某些服务要正常工作,需要从另一个服务获取数据,而该服务可能或可能不依赖于其他服务或来源。
例如,我们的定价服务将直接依赖于需求服务,而需求服务又依赖于库存服务。但这三个服务是完全独立的,也就是说,它们可以部署在任何主机、端口或位置,并且可以随意扩展。
如果定价服务想要与需求服务通信,它必须知道确切的位置,可以向其发送请求以获取所需信息。同样,需求服务也应该了解库存服务的详细信息,以便进行通信。
因此,我们需要一个服务注册表来注册所有其他服务和它们的位置。所有服务在服务启动时应自行注册到该注册表服务,当服务关闭时应自行注销。
服务注册表应充当服务数据库,记录所有可用的实例及其详细信息。
服务发现
服务注册表有服务的详细信息。但为了找出所需服务的位置以及要连接哪些服务,我们需要进行服务发现。
当定价服务想要与需求服务通信时,它需要知道需求服务的网络位置。在传统架构的情况下,这是一个固定的物理地址,但在微服务世界中,这是一个动态地址,它被动态分配和更新。
定价服务(客户端)必须在服务注册表中定位需求服务,确定位置,然后对可用的需求服务进行负载均衡。反过来,需求服务将响应请求客户端(定价服务)的请求。
服务发现用于发现客户端应连接的确切服务,以获取必要的详细信息。
服务发现帮助 API 网关发现请求的正确端点。
他们还将有一个负载均衡器,它调节流量并确保服务的高可用性。
根据负载均衡发生的地点,服务发现被分为:
- 客户端端发现模式
负载均衡将在客户端服务端发生。客户端服务将确定请求发送到何处,负载均衡的逻辑将位于客户端服务中。例如,Netflix Eureka (github.com/Netflix/eureka) 是一个服务注册表。它提供注册和发现服务的端点。
当定价服务想要调用需求服务时,它将连接到服务注册表,然后找到可用的服务。然后,根据配置的负载均衡逻辑,定价服务(客户端)将确定请求哪个需求服务。
服务将进行智能和特定于应用的负载均衡。缺点是,这为每个服务添加了一个额外的负载均衡层,这增加了开销:
- 服务器端发现模式
定价服务将请求负载均衡器连接到需求服务。然后,负载均衡器将连接到服务注册表以确定可用的实例,然后根据配置的负载均衡路由请求。
例如,在 Kubernetes 中,每个 Pod 都有自己的服务器或代理。所有请求都通过这个代理(它有一个与之关联的专用 IP 和端口)发送。
负载均衡逻辑从服务中移除,并隔离到一个单独的服务中。缺点是,它需要一个额外的、高度可用的服务来处理请求。
健康检查
在微服务世界中,实例可以随机启动、更改、更新和停止。它们也可以根据流量和其他设置进行上下伸缩。这需要一个健康检查服务,它将不断监控服务的可用性。
服务可以定期向这个健康检查服务发送它们的状态,这有助于跟踪服务状态。当一个服务宕机时,健康检查服务将停止从该服务接收心跳。然后,健康检查服务将标记该服务为宕机,并将信息级联到服务注册表。同样,当服务恢复时,心跳将被发送到健康检查服务。在接收到几个积极的心跳后,服务将被标记为 UP,然后信息被发送到服务注册表。
健康检查服务可以通过两种方式检查健康:
-
推送配置:所有服务将定期向健康检查服务发送心跳。
-
拉取配置:单个健康检查服务实例将定期查询系统的可用性。
这也要求有一个高可用性系统。所有服务都应该连接到这个服务以共享它们的心跳,并且这个服务必须连接到服务注册表以告诉它们服务是否可用。
动态路由和弹性
健康检查服务将跟踪可用服务的健康状态,并将有关服务健康状态的详细信息发送到服务注册表。
基于此,服务应智能地将请求路由到健康实例,并关闭不健康实例的流量。
由于服务动态更改其位置(地址/端口),每次客户端想要连接到服务时,它都应该首先检查服务注册表中的服务可用性。每个客户端连接也需要添加一个超时时间,在此时间之后,请求必须被服务或重试(配置)到另一个实例。这样我们可以最小化级联故障。
安全性
当客户端调用可用服务时,我们需要验证请求。为了防止不想要的请求堆积,我们应该有一个额外的安全层。客户端的请求应该经过认证和授权才能调用其他服务,以防止对服务的未授权调用。服务反过来应该解密请求,了解它是否有效或无效,并完成剩余的操作。
为了提供安全的微服务,它应该具有以下特征:
-
机密性:仅允许授权客户端访问和消费信息。
-
完整性:可以保证从客户端接收到的信息的完整性,并确保它不会被第三方修改(例如,当网关和服务相互通信时,任何一方都不能篡改或更改它们之间发送的消息。这是一种经典的中间人攻击)。
-
可用性:安全的 API 服务应该具有高度的可用性。
-
可靠性:应该可靠地处理请求并处理它们。
关于中间人攻击(MITM)的更多信息,请查看以下链接:www.owasp.org/index.php/Man-in-the-middle_attack.
容错和故障转移
在微服务架构中,可能有许多故障原因。优雅地处理故障或故障转移非常重要,如下所示:
-
当请求需要很长时间才能完成时,应设置一个预定的超时时间,而不是等待服务响应。
-
当请求失败时,识别服务器,通知服务注册表,并停止连接到该服务器。这样,我们可以防止其他请求发送到该服务器。
-
当服务没有响应时,关闭服务并启动一个新的服务以确保服务按预期工作。
这可以通过以下方式实现:
-
容错库,通过隔离不响应或响应时间超过服务等级协议(SLA)的远程实例和服务来防止级联故障。这防止其他服务调用失败的或不健康的实例。
-
分布式跟踪系统库有助于跟踪服务或系统的时序和延迟,并突出显示与协议 SLA 的任何差异。它们还帮助您了解性能瓶颈在哪里,以便您可以采取行动。
JHipster 提供了实现许多先前概念的选项。其中最重要的包括以下内容:
-
JHipster 注册表
-
HashiCorp Consul
-
JHipster 网关
-
JHipster 控制台
-
Prometheus
-
JHipster UAA 服务器
JHipster 注册表
JHipster 提供了 JHipster Registry (www.jhipster.tech/jhipster-registry/) 作为默认的服务注册表。JHipster Registry 是一个运行时应用程序,所有微服务应用程序都会注册并从中获取其配置。它还提供了监控和健康检查仪表板等附加功能。
JHipster 注册表由以下内容组成:
-
Netflix Eureka 服务器
-
Spring Cloud 配置服务器
Netflix Eureka 服务器
Eureka (github.com/Netflix/eureka) 包括以下内容:
- Eureka 服务器
Eureka 是一个基于 REST 的服务。它用于定位用于负载均衡和故障转移中间层的服务。
Eureka 服务器帮助在实例之间进行负载均衡。它们在可用性间歇的云环境中更有用。另一方面,传统的负载均衡器有助于在已知和固定实例之间进行流量负载均衡。
- Eureka 客户端
Eureka 提供了一个 Eureka 客户端,使得服务器之间的交互无缝。它是一个基于 Java 的客户端。
Eureka 充当一个中间层负载均衡器,帮助负载均衡中间层服务的宿主。它们默认提供基于轮询的简单负载均衡。可以根据需要使用包装器自定义负载均衡算法。
它们不能提供粘性会话。它们也非常适合基于客户端的负载均衡场景(如前所述)。
Eureka 对通信技术没有限制。我们可以使用任何东西,例如 Thrift、HTTP 或任何 RPC 机制进行通信。
假设我们的应用程序位于不同的 AWS 可用区。我们在每个区域注册一个 Eureka 集群,该集群只包含该区域可用的服务信息,并在每个区域启动 Eureka 服务器以处理区域故障。
所有服务都将注册到 Eureka 服务器并发送心跳。当客户端不再发送心跳时,服务将从注册表中移除,并且信息将在集群中的 Eureka 节点之间传递。然后,任何区域的任何客户端都可以查找注册信息以定位它,然后进行任何远程调用。此外,我们还需要确保区域之间的 Eureka 集群之间不相互通信。
Eureka 更倾向于可用性而不是一致性。这就是当服务连接到 Eureka 服务器并共享服务之间的完整配置时。这使得服务即使在 Eureka 服务器关闭的情况下也能运行。在生产中,我们必须在高度可用集群中运行 Eureka 以获得更好的一致性。
Eureka 还具有动态添加或删除服务器的能力。这使得它成为服务注册和服务发现的正确选择。
Spring 云配置服务器
在微服务架构中,服务本质上是动态的。它们将根据流量或任何其他配置进行上下文切换。由于这种动态特性,应该有一个单独的、 高度可用 的服务器来保存所有服务器都需要知道的基本配置细节。
例如,我们的定价服务需要知道注册服务在哪里以及它如何与注册服务通信。另一方面,注册服务应该是高度可用的。如果出于任何原因服务器必须关闭,我们将启动一个新的服务器。定价服务需要与配置服务通信,以了解注册服务。另一方面,当注册服务发生变化时,它必须将更改通知给配置服务器,然后配置服务器将信息级联到所有必要的服务中。
Spring 云配置服务器 (github.com/spring-cloud/spring-cloud-config) 为外部配置提供了服务器和客户端支持。
使用云配置服务器,我们有一个中心位置来管理所有环境中的外部属性。这个概念类似于客户端和服务器上基于 Spring 的环境属性源抽象。它们适用于任何用任何语言运行的应用程序。
它们还有助于在各个(开发/测试/生产)环境之间传输配置数据,并有助于更容易地进行迁移。
Spring 配置服务器提供了一个基于 HTTP、资源型的 API 用于外部配置。它们将加密和解密属性值。它们绑定到配置服务器并使用远程属性源初始化 Spring 环境。配置可以存储在 Git 仓库或文件系统中。
HashiCorp Consul
Consul (www.consul.io/) 主要是由 Hashicorp 提供的服务发现客户端。它侧重于一致性。Consul 完全是用 Go 编写的。
这意味着它将具有更小的内存占用。除此之外,我们还可以使用 Consul 与用任何编程语言编写的服务一起使用。
使用 Consul 的主要优势如下:
-
它具有更小的内存占用
-
它可以与用任何编程语言编写的服务一起使用
-
它侧重于一致性而非可用性
Consul 还提供服务发现、故障检测、多数据中心配置和存储。
这是 JHipster 注册的一个替代选项。在应用程序创建期间,可以选择使用 JHipster 注册或 Consul。
Eureka(JHipster 注册)要求每个应用程序使用其 API 进行注册和发现自身。它侧重于可用性而非一致性。它仅支持用 Spring Boot 编写的应用程序或服务。
另一方面,Consul 作为服务中的一个代理运行,检查健康信息和之前列出的其他一些额外操作。
服务发现
Consul 可以提供服务,其他客户端可以使用 Consul 来发现特定服务的提供者。通过 DNS 或 HTTP,应用程序可以轻松找到它们所依赖的服务。
健康发现
Consul 客户端可以提供任意数量的健康检查,这些检查可以与特定的服务相关联,也可以与本地节点相关联。这些信息可以被健康检查服务用来监控服务的健康状态,反过来,这些信息也被用来发现服务组件并将流量从不健康的节点路由到健康的节点。
K/V 存储
Consul 提供了一个易于使用的 HTTP API,这使得应用程序能够简单地使用 Consul 的键/值存储来动态配置服务、在当前领导者宕机时选举领导者,以及根据功能隔离容器。
多数据中心
Consul 支持开箱即用的多数据中心。这意味着你不必担心构建额外的抽象层来扩展到多个区域。
Consul 应该是一个分布式且高度可用的服务。为 Consul 提供服务的每个节点都运行一个 consul 代理,主要负责健康检查。这些代理将与一个或多个 Consul 服务器通信,这些服务器收集并添加这些信息。这些服务器之间也会选举出一个领导者。
因此,Consul 充当服务注册、服务发现、健康检查和 K/V 存储。
JHipster 网关
在微服务架构中,我们需要一个入口点来访问所有运行中的服务。因此,我们需要一个充当网关的服务。这将代理或路由客户端的请求到相应的服务。在 JHipster 中,我们为此提供了 JHipster 网关。
JHipster 网关是一个可以生成的微服务应用程序。它集成了 Netflix Zuul 和 Hystrix,以提供路由、过滤、安全、断路器等功能。
Netflix Zuul
在微服务架构中,Zuul 是所有请求的前门(门卫)。它充当边缘服务应用。Zuul 是为了在服务之间实现动态路由、监控、弹性和安全性而构建的。它还具有根据需要动态路由请求的能力。
冷知识:在 Ghostbusters 中,Zuul 是门卫。
Zuul 的工作基于不同类型的过滤器,使我们能够快速灵活地将功能应用于我们的边缘服务。
这些过滤器帮助我们执行以下功能:
-
身份验证和安全:识别每个资源的身份验证要求,并拒绝不符合要求的请求
-
洞察和监控:在边缘跟踪数据和统计信息,并深入了解生产应用
-
动态路由:根据健康和其他因素,根据需要动态地将请求路由到不同的后端集群
-
多区域弹性(AWS):为了多样化我们的弹性负载均衡器使用,并将我们的边缘更靠近我们的成员,跨 AWS 区域路由请求
如需更多关于 Zuul 的信息,请查看 github.com/Netflix/zuul/wiki.
Hystrix
Hystrix (github.com/Netflix/Hystrix) 是一个用于隔离远程系统、服务和第三方库访问点的延迟和容错库,旨在停止级联故障;并在不可避免失败的复杂分布式系统中实现弹性。
Hystrix 设计来完成以下任务:
-
在复杂的分布式系统中停止故障级联
-
保护系统免受网络依赖项故障的影响
-
控制系统的延迟
-
快速恢复并快速失败以防止级联
-
在可能的情况下进行回退和优雅降级
-
启用近乎实时的监控、警报和操作控制
在复杂的分布式架构中,应用程序有许多依赖项,每个依赖项最终都会在某些时候失败。如果主机应用程序没有从这些外部故障中隔离,它可能会被它们拖垮。
JHipster 控制台
JHipster 控制台 (github.com/jhipster/jhipster-console) 是一个用于微服务的监控解决方案,它使用 ELK Stack 构建。它附带预设的仪表板和配置。它以 Docker 镜像的形式提供作为运行时组件。
ELK Stack 由 Elasticsearch、Logstash 和 Kibana 组成。
Logstash 可以用来标准化数据(通常来自日志),然后使用 Elasticsearch 来更快地处理相同的数据。最后,使用 Kibana 来可视化数据。
Elasticsearch
Elasticsearch 是数据分析中广泛使用的搜索引擎。它帮助您从数据堆中快速提取数据。它还帮助提供实时分析和数据提取。它具有高度可扩展性、可用性和多租户性。
它还提供了基于全文搜索的文档保存选项。这些文档将根据数据的任何更改进行更新和修改。这反过来将提供更快的搜索并分析数据。
Logstash
Logstash (www.elastic.co/products/logstash) 会接收日志,处理它们,并将它们转换为输出。它们可以读取任何类型的日志,如系统日志、错误日志和应用程序日志。它们是这个堆栈的重工作组件,有助于存储、查询和分析日志。
它们作为事件处理的管道,能够通过过滤器处理大量数据,并与 Elasticsearch 一起快速交付结果。JHipster 确保日志以正确的格式存储,以便可以正确地分组和可视化。
Kibana
Kibana (www.elastic.co/products/kibana) 构成了 ELK 堆栈的前端。它用于数据可视化。它仅仅是一个日志数据仪表板。它有助于可视化那些否则难以阅读和解释的数据趋势和模式。它还提供了一个分享/保存的选项,这使得数据可视化更有用。
Zipkin
Zipkin (zipkin.io/) 是一个分布式跟踪系统。微服务架构总是存在延迟问题,需要一个系统来排查延迟问题。Zipkin 通过收集时间数据来帮助解决问题。Zipkin 还帮助搜索数据。
所有注册的服务都将报告时间数据到 Zipkin。Zipkin 根据接收到的跟踪请求为每个应用程序或服务创建一个依赖关系图。然后,它可以用来分析、找出处理时间较长的应用程序,并根据需要进行修复。
当发起请求时,跟踪度量将记录标签,将跟踪头添加到请求中,并最终记录时间戳。然后,请求被发送到原始目的地,响应被发送回跟踪度量,然后记录持续时间并与 Zipkin 收集器共享结果,Zipkin 负责存储信息。
默认情况下,JHipster 会生成一个禁用了 Zipkin 的应用程序,但可以在应用程序的 <env>.yml 文件中启用它。
Prometheus
在微服务架构中,我们需要持续监控我们的服务,任何问题都应立即触发警报。我们需要一个独立的服务,它可以持续监控并在发生任何异常时立即通知我们。
Prometheus 由以下部分组成:
-
Prometheus 服务器,负责抓取和存储时间序列数据
-
用于对应用程序代码进行度量的库
-
一个用于支持短期作业的推送网关
-
一个用于在 Grafana 中可视化数据的导出器
-
一个警报管理器
-
其他支持工具
Prometheus 是 JHipster 控制台的替代品。它提供监控和警报支持。这需要单独运行 Prometheus 服务器以获取更多信息。要开始使用 Prometheus,请访问 prometheus.io/。
它提供多维数据模型,这些模型是时间序列的,由指标名称和键值对标识。它具有灵活的动态查询语言。它支持开箱即用的时间序列提取,并通过中介网关推送时间序列。它具有多种图表和仪表板支持模式。
当出现故障时,它有助于发现问题。由于它是自主的,不依赖于任何远程服务,因此数据足以找到基础设施损坏的位置。
它有助于记录时间序列数据并通过机器或高度动态的服务导向架构进行监控。
在选择 Prometheus 而不是 JHipster 控制台时,需要考虑以下事项:
-
Prometheus 非常擅长利用应用程序的指标,而不会监控日志或跟踪。另一方面,JHipster 控制台使用 ELK 堆栈,并监控应用程序的日志、跟踪和指标。
-
Prometheus 可以用于查询大量时间序列数据。在 JHipster 控制台上,ELK 在跟踪和搜索指标和日志方面更加灵活。
-
JHipster 控制台使用 Kibana 可视化数据,而 Prometheus 使用 Grafana (
grafana.com/) 可视化指标。
JHipster UAA 服务器
JHipster 用户账户和授权(UAA)服务仅是一个 OAuth2 服务器,可用于集中式身份管理。为了访问受保护资源并避免对 API 的未授权访问,必须有一个授权服务器来授权请求并提供对资源的访问。
OAuth2 是一个授权框架,它根据令牌提供对请求的访问。客户端请求访问服务;如果用户被授权,应用程序将收到授权许可。在收到许可后,客户端从授权服务器请求令牌。一旦收到令牌,客户端将请求资源服务器获取必要的信息。
JHipster 支持标准 LDAP 协议,并通过 JSON API 调用。
JHipster UAA 是一个用户账户和授权服务,用于通过 OAuth2 授权协议确保 JHipster 微服务的安全性。
JHipster UAA 是一个由 JHipster 生成的应用程序,包括用户和角色管理。它还拥有一个完整的 OAuth2 授权服务器。这是灵活的,并且可以完全自定义。
在微服务架构中,安全性至关重要。以下是为确保微服务安全的基本要求。
它们应该在同一个地方进行身份验证。用户应该将整个体验视为一个单一单元。一旦最终用户登录到应用程序,他们应该能够访问他们有权访问的内容。他们应该在登录到系统的整个时间内持有会话相关信息。
安全服务应该是无状态的。无论服务如何,安全服务都应该能够为请求提供身份验证。
它们还需要有能力为机器和用户提供身份验证。它们应该能够区分它们并追踪它们。它们的功能应该是授权传入的请求,而不是识别最终用户。
由于底层服务是可扩展的,安全服务也应该有能力根据需求进行扩展和缩减。
当然,它们应该免受攻击。任何已知的漏洞都应该在需要时得到修复和更新。
之前的要求是通过使用 OAuth2 协议来满足的。
JHipster UAA 是一个集中式服务器,有助于对用户进行身份验证和授权。它们还有会话相关信息,以及系统内部可用的用户和角色管理帮助下的基于角色的访问控制。
OAuth2 协议通常提供基于提供的详细信息的令牌进行身份验证,这使得它们无状态并且能够从任何来源验证请求。
摘要
到目前为止,我们已经看到了微服务架构相对于单体应用程序的优势,以及我们需要运行微服务应用程序(如 JHipster Registry、Consul、Zuul、Zipkin、ELK 堆栈、Hystrix、Prometheus 和 JHipster UAA 服务器)所需的组件。在我们下一章中,我们将看到如何使用 JHipster 构建微服务。我们还将了解我们如何选择之前的组件,以及使用 JHipster 设置它们有多容易。
第九章:使用 JHipster 构建微服务
现在,是时候构建一个完整的微服务堆栈了。到目前为止,我们已经使用 JHipster 生成了、开发了和部署了一个单体应用程序,在前一章中,我们看到了微服务堆栈提供的优势。在这一章中,我们将探讨如何使用 JHipster 构建微服务。
我们将首先将我们的单体商店应用程序转换为微服务网关应用程序。接下来,我们将作为独立的微服务应用程序向我们的电子商务商店添加新的功能。然后我们将看到这些应用程序如何相互通信,并作为一个单一的应用程序为我们的最终用户提供服务。
在这一章中,我们将:
-
生成网关应用程序:
-
检查生成的代码
-
简要介绍 JWT
-
-
生成微服务应用程序:
-
发票服务
-
通知服务
-
应用程序架构
我们在第三章中使用了 JHipster 构建了一个在线电子商务商店,标题为使用 JHipster 构建单体 Web 应用程序。第三章。由于范围较小,这是一个更容易的选择。假设我们的电子商务商店在用户和范围方面增长巨大,导致了一种更加苛刻的情况。团队发现很难以单体架构快速推出功能,并希望对应用程序的各个部分有更多的控制。
解决这个问题的方法之一是采用微服务架构。该应用程序是使用 JHipster 创建的;迁移到微服务的选项更容易实现。JHipster 遵循代理微服务模式,其中在服务前面有一个聚合器/代理,它作为最终用户的网关。用更简单的话说,JHipster 创建了一个网关(处理所有用户请求)和通过网关与用户通信的各个服务。
也就是说,我们需要有一个网关服务,以及一个或几个可以独立运行的微服务应用程序。
我们的客户在发票方面遇到了一些问题,因为系统响应时间变长了。客户还抱怨他们没有收到通知,无法跟踪他们的订单。为了解决这个问题,我们将从我们的单体应用程序中移除发票服务,并使其成为一个独立的服务,然后创建一个独立的通知服务来处理通知。对于前者,我们将继续使用相同的 SQL 数据库。对于后者,我们将使用 NoSQL 数据库。
让我们看看我们将要生成的应用程序架构:
网关应用程序生成
我们将首先将我们生成的单体应用程序转换为微服务网关应用程序。
尽管微服务由内部的不同服务组成,但对于最终用户来说,它应该是一个单一、统一的产品。有许多服务被设计成以多种不同的方式工作,但应该有一个单一的入口点供用户使用。因此,我们需要一个网关应用程序,因为它们构成了应用程序的前端。
将内部合约和服务与外部用户分离。我们可能有一些应用级别的内部服务,我们不应该将其暴露给外部用户,因此这些可以被隐藏起来。这也为应用程序增加了另一个安全层。
更容易模拟服务进行测试,有助于在集成测试中独立验证服务。
将单体应用转换为微服务网关
我们已经生成了我们的单体应用程序以及我们的实体。作为单体应用程序生成的一部分,我们已经通过 JHipster CLI 选择了一些选项。当我们生成微服务网关应用程序时,我们将坚持相同的选项(数据库、认证类型、包名、i18n 等)。
注意:我们将在稍后看到如何在单体应用中应用我们应用的定制化。
现在是编码时间了,让我们使用 JHipster CLI 开始构建网关应用程序。
这里的第一步是将单体应用转换为具有几乎相同配置的微服务网关应用,就像我们创建单体应用时使用的配置一样。
现在,让我们转到终端(如果使用 Windows,则为命令提示符),首先导航到我们创建单体应用的文件夹。一旦进入文件夹,创建一个新的 Git 分支,这样我们就可以在完成后干净地合并回 master 分支:
> cd e-commerce-app/online-store
> git checkout -b gateway-conversion
现在,打开您最喜欢的文本编辑器或 IDE 中的.yo-rc.json文件,并更改以下值:
为了将单体应用转换为微服务网关应用,我们只需更改.yo-rc.json文件中的前述值。由于对于单体应用来说,服务发现不是强制性的,我们已经将服务发现类型添加到 Eureka 中。
显然,下一个更改是将应用程序类型从单体更改为网关。
应用程序生成
现在,让我们运行jhipster命令来生成应用程序:
JHipster 会询问您是否想要覆盖冲突的文件或使用您现有的文件,以及一些其他选项。用户可以使用任何想要的选项。
目前,我们将选择选项a。它将覆盖所有其他文件,包括高亮显示的文件。
如果您在应用程序上编写了大量自定义代码,此提示将非常有用。您可以选择适当的选项以获得所需的结果。
这将覆盖我们在单体应用程序中做的所有自定义设置。我们可以通过使用 GIT 从我们的 master 分支中 cherry pick 所需的更改,轻松地将它们带回这个分支。你可以遵循我们在第五章 Customization and Further Development 中看到的类似方法。一旦所有更改都应用了,我们就可以将这个分支合并回 master。你还需要在 第十章 Working with Microservices 中对实体文件做同样的操作。
生成新的网关
如果你不想转换现有的单体应用程序并希望从头开始,请遵循以下步骤。
在终端中,导航到 e-commerce-app 文件夹,创建一个名为 app-gateway 的新文件夹,然后切换到 app-gateway 目录,并运行 jhipster 命令。
因此,显然,第一个问题是,我们想创建哪种 类型 的应用程序?我们将选择微服务网关(第三个选项),然后按 Enter:
然后,我们将输入我们应用程序的基本名称。我们将使用名称 store:
由于我们正在使用微服务,存在很高的端口冲突风险。为了避免这些冲突,JHipster 将要求你为每个微服务应用程序(包括网关和应用程序)选择一个端口。默认情况下,我们将使用 8080 作为端口,但我们可以根据需要更改端口。现在,我们将使用默认端口,因为网关将在 8080 上运行,类似于我们的单体应用程序:
然后,我们输入我们应用程序的包名。我们将使用可用的默认名称,即 com.mycompany.store:
对于下一个问题,JHipster 将要求你配置注册服务。我们将选择要配置、监控和扩展我们的微服务和网关应用程序所需的注册服务。我们可以选择使用 JHipster 注册或 Consul。这也是可选的;我们在这里不需要选择任何注册服务。然后我们可以选择无服务发现。
当你选择无服务发现时,微服务 URL 将硬编码在属性文件中。
对于下一个问题,JHipster 将要求你选择认证类型。JHipster 提供了三种认证类型的选项,分别是 JWT、OAuth2 和基于 UAA 服务器的。JWT 是无状态的,而 UAA 在不同的服务器(和应用程序)上运行。另一方面,OAuth2 将提供授权令牌,而授权是在第三方系统上完成的。
JHipster 提供了一个创建 UAA 服务器应用程序的选项。
我们将在稍后更详细地了解 JWT。现在,我们将选择 JWT 认证。
我们将选择数据库类型。我们有选项选择 SQL 和 NoSQL。在 NoSQL 方面,我们可以选择 MongoDB 或 Cassandra。我们将选择 SQL 数据库:
然后,我们将选择我们将用于生产和开发的数据库。JHipster 提供了一个选项,可以在生产和开发环境中使用不同的数据库。这确实有助于更快、更轻松地启动应用程序开发。
我们将选择 MySQL 数据库用于生产:
然后,我们将选择基于磁盘持久性的 H2 用于开发:
在选择数据库之后,我们将为第二级 Hibernate 缓存选择“是”:
然后,我们将选择 Gradle 用于构建后端。我们有选项选择 Gradle 用于后端开发:
然后,我们可以选择我们需要的任何其他附加技术。JHipster 提供了一个选项来选择 Elasticsearch、使用 Hazelcast 进行集群应用程序、WebSocket 和 Swagger Codegen 用于基于 API 的开发以及基于 Kafka 的异步消息传递。我们将在此选择 WebSocket,类似于我们在单体存储中使用的:
由于我们的网关应用程序需要一个用户界面,对于下一个问题,我们可以选择我们需要的客户端框架。我们将选择Angular 5:
然后,我们将选择是否需要使用基于 SASS 的预处理器用于 CSS。我们将在此使用 SASS,因此我们将选择 y:
然后,我们将选择是否需要启用国际化支持。我们将为此选择“是”:
然后,我们将选择英语作为我们的母语:
然后,选择任何其他附加语言:
然后,选择任何其他测试框架,例如 Gatling、Cucumber 和/或 Protractor,因为这是必需的。我们将选择 Protractor 作为测试工具:
最后,JHipster 要求我们安装来自市场的任何其他生成器;我们将在此选择“否”:
这将创建所有必要的文件并使用 Yarn 安装前端依赖项:
现在,我们的网关应用程序已生成。JHipster 将自动将生成的文件提交到 Git;如果您希望手动执行此步骤,可以在执行期间传递 skip-git 标志,例如,jhipster --skip-git,然后手动执行以下步骤:
> git init
> git add --all
> git commit -am "converted into gateway application"
网关配置
网关应用程序的生成方式与单体应用程序类似,但涉及 Zuul 代理、Eureka 客户端和 Hystrix 的配置:
@ComponentScan
@EnableAutoConfiguration(exclude = {MetricFilterAutoConfiguration.class, MetricRepositoryAutoConfiguration.class, MetricsDropwizardAutoConfiguration.class})
@EnableConfigurationProperties({LiquibaseProperties.class, ApplicationProperties.class})
@EnableDiscoveryClient
@EnableZuulProxy
public class GatewayApp {
...
}
我们为我们的注册表服务选择了 JHipster 注册表。这将是一个独立的注册服务器,其他微服务应用程序和网关将自动注册自己:
-
@EnableDiscoveryClient添加到 Spring Boot 的主类中,这将启用 Netflix Discovery 客户端。微服务应用程序和网关需要将自己注册到注册表服务。它使用 Spring Cloud 的发现客户端抽象来查询其自己的主机和端口,然后将它们添加到注册服务器。 -
另一方面,Zuul 是门卫。这有助于将授权请求路由到相应的端点,限制每个路由的请求,并将必要的令牌中继到微服务应用程序。
-
@EnableZuulProxy帮助微服务网关应用程序根据application.yml中提供的配置将请求路由到相应的微服务应用程序:
zuul: # those values must be configured depending on the application specific needs
host:
max-total-connections: 1000
max-per-route-connections: 100
semaphore:
max-semaphores: 500
在网关应用程序中,我们已经指定了上述 Zuul 配置设置。一个代理可以保持打开的最大总连接数保持在 1000。一个代理可以保持打开的最大路由连接数保持在 100。信号量保持在最大 500。 (信号量类似于一个用于线程和进程之间同步的计数器。)
后端微服务端点的访问由 AccessControlFilter 控制,该过滤器将检查请求是否已授权,并允许请求端点:
public class AccessControlFilter extends ZuulFilter {
...
public boolean shouldFilter() {
...
return !isAuthorizedRequests(serviceUrl, serviceName,
requestUri);
}
...
}
Zuul 作为门卫,还充当速率限制器。在生成的应用程序中添加了一个速率限制过滤器,该过滤器限制了每个客户端发出的 HTTP 调用次数。这可以通过以下条件启用:
@ConditionalOnProperty("jhipster.gateway.rate-limiting.enabled")
public static class RateLimitingConfiguration {
...
}
SwaggerBasePathRewritingFilter 也被使用,这将有助于重写微服务 Swagger URL 基础路径:
@Component
public class SwaggerBasePathRewritingFilter extends SendResponseFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
if(!context.getResponseGzipped()) {
context.getResponse().setCharacterEncoding("UTF-8");
}
// rewrite the base path and send down the response
}
...
添加了一个 TokenRelayFilter 以从 Zuul 的忽略列表中移除授权。这将有助于传播生成的授权令牌:
@Component
public class TokenRelayFilter extends ZuulFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Set<String> headers = (Set<String>) ctx.get("ignoredHeaders");
// JWT tokens should be relayed to the resource servers
headers.remove("authorization");
return null;
}
...
每个应用程序都应该有一个 Eureka 客户端,该客户端帮助在服务之间进行请求负载均衡,并将健康信息发送到 Eureka 服务器或注册表。Eureka 客户端在 application-dev.yml 中配置如下:
eureka:
client:
enabled: true
healthcheck:
enabled: true
fetch-registry: true
register-with-eureka: true
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
instance:
appname: gateway
instanceId: gateway:${spring.application.instance-id:${random.value}}
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
status-page-url-path: ${management.context-path}/info
health-check-url-path: ${management.context-path}/health
metadata-map:
zone: primary # This is needed for the load balancer
profile: ${spring.profiles.active}
version: ${info.project.version}
我们选择启用健康检查,并将注册和复制的间隔设置为 10 秒,以及我们定义的租约更新间隔和过期持续时间。
我们将在 Hystrix 中配置超时,超过这个时间服务器将被认为是关闭的:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
如果服务器在10秒内没有响应,则认为服务器已死亡,并在注册服务中注册。这确保了在服务器变为活动状态之前不会向该服务器发送后续请求。
JWT 身份验证
我们需要在微服务之间安全地传输信息。请求必须经过验证并数字签名,应用程序验证请求的真实性并对其进行响应。
我们需要在 REST 或 HTTP 世界中以紧凑的方式处理这些信息,因为信息需要与每个请求一起发送。JWT 正是为此而来。JWT 基本上是开放网络标准中的 JSON Web Tokens,有助于在各方(应用程序)之间安全地传输信息。JWT 将使用秘密、基于 HMAC 算法或使用公钥/私钥进行签名。它们是紧凑且自包含的。
对于高级用途,我们需要添加 Bouncy Castle(库)。
紧凑型:它们很小,可以发送到每个请求。
自包含:有效载荷包含有关用户的全部必要细节,这防止我们查询数据库进行用户身份验证。
JWT 由头部、有效载荷和签名组成。它们是 base64 编码的字符串,由.(点)分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlNlbmRpbCBLdW1hciBOIiwiYWRtaW4iOnRydWV9.ILwKeJ128TwDZmLGAeeY7qiROxA3kXiXOG4MxTQVk_I
#Algorithm for JWT generation
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
)
JWT 是如何工作的
当用户登录系统时,会根据有效载荷(即用户信息和秘密密钥)生成一个令牌。生成的令牌将存储在本地。对于所有未来的请求,此令牌将添加到请求中,应用程序将在响应请求之前验证令牌:
令牌的格式如下:
Authorization: Bearer <token>
在 JHipster 中,我们使用来自 Okta 的JJWT(基于 Java 的 JSON Web Tokens)。这是一个基于简化构建者模式的库,用于生成和签名令牌作为生产者,以及解析和验证令牌作为消费者。
创建令牌:
public class TokenProvider {
...
public String createToken(Authentication authentication, Boolean rememberMe) {
...
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS512, secretKey)
.setExpiration(validity)
.compact();
}
}
验证令牌:
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
log.info("Invalid JWT signature.");
log.trace("Invalid JWT signature trace: {}", e);
} catch (MalformedJwtException e) {
log.info("Invalid JWT token.");
log.trace("Invalid JWT token trace: {}", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
log.trace("Expired JWT token trace: {}", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
log.trace("Unsupported JWT token trace: {}", e);
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
log.trace("JWT token compact of handler are invalid trace: {}", e);
}
return false;
}
到目前为止,我们已经创建了一个网关应用程序,它将作为我们应用程序和服务的单一入口点。现在,我们将使用 JHipster 生成一个微服务应用程序。
微服务应用程序是服务。在这本书中,我们将构建两个示例服务,并讨论 JHipster 提供的特点,一个使用 SQL 数据库(MySQL),另一个使用 NoSQL 数据库(MongoDB)。这些服务是独立的,并且松散耦合。
使用 JHipster,你可以构建作为 REST 端点的微服务应用程序。
微服务应用程序 - 带有 MySQL 数据库的发票服务
我们可以从我们的单体应用程序中提取发票服务,将其分离,并使其成为独立的微服务应用程序。让我们称它为Invoice Service。此服务负责创建和跟踪发票。
应用程序生成
首先,让我们看看我们如何生成一个微服务应用程序。在 e-commerce-app 文件夹中,创建一个新的文件夹,用于存放微服务应用程序。让我们将文件夹命名为 invoice。进入目录,通过输入 jhipster 开始创建应用程序:
第一个问题是我们被要求选择我们希望创建的应用程序类型。我们必须选择微服务应用程序,然后点击 Enter:
然后,您需要为您的应用程序提供一个基本名称。我们将使用默认的应用程序名称,invoice(默认情况下,JHipster 选择与应用程序名称相同的文件夹名称):
然后,我们将选择应用程序必须运行的默认端口。默认情况下,JHipster 提示 8081 作为微服务的默认端口,因为我们使用 8080 作为网关应用程序:
然后,我们将选择默认的包名:
由于我们已选择 JHipster Registry 作为网关应用程序,因此在这里我们将选择相同的选项。同样,如果我们选择了 Consul 作为网关应用程序,那么我们也可以选择 Consul。我们甚至可以选择不使用注册表,然后添加任何自定义注册表:
然后,JHipster 会询问我们希望使用的身份验证类型。我们将选择 JWT 身份验证,与网关应用程序中选择的相同:
然后,选择我们需要使用的数据库类型。如突出所示,发票服务将使用 SQL 数据库。我们将选择 SQL 选项。JHipster 提供了一个选项来选择不使用数据库本身。当未选择数据库时,应用程序将生成不带数据库连接:
我们将选择生产数据库为 MySQL:
然后,我们将选择开发数据库为 H2,并使用基于磁盘的持久性:
然后,我们将选择 HazelCast 缓存作为 Spring 缓存抽象。Hazelcast 为所有会话提供共享缓存。可以在集群或 JVM 层面上保持持久数据。我们可以有不同的模式可供选择,包括单节点或多节点。
Ehcache 是一个本地缓存,它适用于在单个节点中存储信息。Infinispan 和 HazelCast 能够创建集群并在多个节点之间共享信息,其中 HazelCast 使用分布式缓存,每个节点相互连接。另一方面,Infinispan 是一个混合缓存:
然后,我们将选择 Hibernate 2 级缓存:
我们将选择 Gradle 作为构建工具:
然后,JHipster 会询问我们是否还有其他技术想要添加。我们这里不会选择任何内容,并使用默认选项:
然后,我们将选择国际化(i18n):
然后,我们将选择默认选项为英语:
选择我们需要的附加语言:
然后,选择我们想要添加到 Gatling 或 Cucumber 的任何其他测试框架。请注意,由于它不会生成前端应用程序,因此选项如 Protractor 未列出:
最后,我们将从 JHipster 市场中选择我们需要的任何其他生成器进行安装。目前,我们将不会选择任何其他生成器(默认选项):
然后,生成服务器应用程序:
我们已生成微服务应用程序。JHipster 将自动将生成的文件提交到 Git。如果您希望手动执行此步骤,可以在执行期间传递 skip-git 标志,例如,jhipster --skip-git,然后手动执行以下步骤:
> git init
> git add --all
> git commit -am "generated invoice microservice application"
微服务配置
生成的应用程序将不包含任何前端。再次强调,发票服务是基于 Spring Boot 的应用程序。安全功能在 MicroserviceSecurityConfiguration.java. 中配置。
忽略所有与前端相关的请求,因此当用户尝试访问任何与前端相关的资源,如 HTML、CSS 和 JS 时,请求将由发票服务忽略:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/bower_components/**")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**")
.antMatchers("/h2-console/**");
}
由于服务是独立的,它们可以部署并运行在具有不同 IP 地址的另一个服务器上。这要求我们默认禁用 CSRF(跨站请求伪造)。我们还将启用会话管理中的无状态会话策略。这使得我们的应用程序无法创建或维护任何会话。每个请求都是基于令牌进行认证和授权的。
我们还将使用会话管理中的无状态会话策略。这是最严格的会话策略。这不会允许我们的应用程序生成会话,因此我们的请求必须附有每个请求的(时间限制)令牌。这增强了我们服务的安全性。它们的无状态约束是使用 REST API 的另一个优点。
关于会话策略的更多选项和信息,请参阅以下文档:docs.spring.io/autorepo/docs/spring-security/4.2.3.RELEASE/apidocs/org/springframework/security/config/http/SessionCreationPolicy.html。
然后,一旦请求被授权(基于 JWT 令牌),就应该允许所有与 API 相关的请求和 Swagger 资源:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.and()
.apply(securityConfigurerAdapter());
}
在资源方面,在bootstrap.yml中,我们已定义了与注册表相关的信息。
我们当前的微服务应用程序使用 JHipster 注册表作为注册表服务,以便在心跳中注册和注销其存在。我们需要提供注册表服务的密码,以便应用程序可以连接到注册表服务:
jhipster:
registry:
password: admin
此外,Spring Boot 服务的名称和默认的 Spring Cloud Config 参数已在bootstrap.yml中启用。我们还添加了必须连接以获取注册表服务配置的 URI:
spring:
application:
name: invoice
...
cloud:
config:
fail-fast: false # if not in "prod" profile, do not force to use Spring Cloud Config
uri: http://admin:${jhipster.registry.password}@localhost:8761/config
# name of the config server's property source (file.yml) that we want to use
name: invoice
...
与网关类似,其余的服务相关配置都是在application.yml文件中完成的。
Eureka 配置与网关应用程序中的配置完全相同。所有生成的应用程序都将具有类似的 Eureka 配置:
eureka:
client:
enabled: true
healthcheck:
enabled: true
fetch-registry: true
register-with-eureka: true
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
instance:
appname: invoice
instanceId: invoice:${spring.application.instance-id:${random.value}}
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
status-page-url-path: ${management.context-path}/info
health-check-url-path: ${management.context-path}/health
metadata-map:
zone: primary # This is needed for the load balancer
profile: ${spring.profiles.active}
version: ${info.project.version}
数据库和 JPA 配置如下:
spring:
profiles:
active: dev
...
datasource:
type: <connector jar>
url: <db url>
username: <username>
password: <password>
...
jpa:
database-platform: <DB platform>
database: <H2 or MySQL or any SQL database>
show-sql: true
properties:
hibernate.id.new_generator_mappings: true
hibernate.cache.use_second_level_cache: true
hibernate.cache.use_query_cache: false
hibernate.generate_statistics: true
hibernate.cache.region.factory_class: com.hazelcast.hibernate.HazelcastCacheRegionFactory
hibernate.cache.hazelcast.instance_name: invoice
hibernate.cache.use_minimal_puts: true
hibernate.cache.hazelcast.use_lite_member: true
其余的配置与网关应用程序中生成的配置相似,可以根据您的需求进行调整或定制。
现在,我们可以与应用程序和注册表服务一起启动应用程序。由于应用程序首先尝试连接到注册表服务,如果指定的位置没有可用的注册表服务,则应用程序将不知道连接何处以及响应谁。
因此,发票服务已生成。现在,我们可以使用 NoSQL 作为后端数据库生成通知服务。
微服务应用程序 - 使用 NoSQL 数据库的通知服务
对于电子商务网站来说,订单的跟踪和用户在正确的时间收到通知是非常关键的。我们将创建一个通知服务,该服务将在用户的订单状态发生变化时通知用户。
应用程序生成
让我们在e-commerce-app文件夹中生成我们的第二个微服务应用程序(通知服务)。在将微服务应用程序保存在的新文件夹中创建一个新文件夹。让我们将文件夹命名为notification。进入目录,通过运行jhipster开始创建应用程序。
我们被问到的第一个问题是选择我们想要创建的应用程序类型。我们必须选择微服务应用程序,然后点击Enter:
然后,我们将选择默认的应用程序名称,notification:
然后,我们将选择应用程序的端口号。由于我们已将 8080 用于单体应用程序和 8081 用于发票服务,因此我们将使用端口号 8082 用于通知服务:
对于接下来的三个问题,我们将使用之前相同的选项:
然后,我们将选择 MongoDB 作为数据库。在选择了 MongoDB 之后,JHipster 现在将询问您希望用于开发和生产服务器的不同类型的数据库。我们将使用 MongoDB 作为开发和生产数据库:
对于剩余的问题,我们将选择与我们在发票服务中选择的选项类似的选项:
服务器已成功生成:
我们的微服务应用程序已生成。JHipster 将自动将生成的文件提交到 Git。如果您希望手动执行此步骤,可以在执行期间传递 skip-git 标志,例如,jhipster --skip-git,然后按照以下步骤手动执行:
> git init
> git add --all
> git commit -am "generated notification microservice application"
微服务配置
由于我们为两个微服务选择了类似选项,应用程序最终生成。生成的代码将类似,但数据库配置除外:
摘要
好的,在本章中,我们已经生成了一个网关应用程序和两个微服务应用程序。我们已经向您展示了使用 JHipster 生成微服务包是多么容易。现在,在我们运行应用程序之前,我们需要启动我们的注册服务器。
在下一章中,我们将启动注册服务器,我们还将了解如何将实体添加到我们的新服务中。