近年来,我们已经研究过的微服务概念席卷了整个行业,迅速用更小、更精简的产品取代了经过战斗考验的应用服务器。紧随微服务之后出现了一个新概念——作为服务的功能,通常称为无服务器。在本章中,您将进一步了解这个新的部署模型,并构建一个应用程序来演示如何使用它。
该应用程序将是一个使用以下技术的简单通知系统:
- 亚马逊网络服务
- 亚马逊兰博达酒店
- 亚马逊身份与访问管理(IAM)
- 亚马逊简易通知系统(SNS)
- 亚马逊简单邮件系统(SES)
- 亚马逊发电机
- JavaFX
- 云提供商提供的选择可能相当广泛,亚马逊网络服务也不例外。在本章中,我们将尝试充分利用 AWS 提供的功能,帮助我们在开发云本机应用程序时构建一个引人注目的应用程序。
在开始应用程序之前,我们应该花一些时间更好地理解术语功能即服务(FaaS。该术语本身是空白的延续,这是我们几年来看到的一种服务趋势。有很多这样的条款和产品,但三大类是基础设施即服务(IaaS)、平台即服务(PaaS)和软件即服务(SaaS)。通常情况下,这三者相互依赖,如下图所示:
最低级别的云计算产品,即基础设施即服务提供商,在云中提供基础设施相关资产**。通常,这可以像文件存储一样简单,但通常意味着虚拟机。通过将基础设施用作服务提供商,客户无需担心购买、维护或更换硬件,因为硬件是由提供商处理的。相反,客户端只根据所使用的资源计费。**
向上移动,平台即服务提供商提供云托管的应用程序执行环境。这可能包括应用服务器、数据库服务器、web 服务器等。物理环境的细节被抽象出来,客户指定存储和 RAM 需求。一些提供商还允许客户选择操作系统,因为这可能会对应用程序堆栈、支持工具等产生影响。
软件即服务是一种更高层次的抽象,它根本不关注硬件,而是提供客户订阅的托管软件,通常为每个用户,通常为每月或每年。这通常见于复杂的商业软件,如财务系统或人力资源应用程序,但也见于较简单的系统,如博客软件。用户只需订阅并使用软件,将安装和维护(包括升级)留给提供商。虽然这会降低用户的灵活性(例如,定制软件通常是不可能的),但它也会将维护成本推给提供商,并在大多数情况下保证访问最新版本的软件,从而降低运营成本。
这类服务还有其他几种变体,例如移动后端即服务(MBaas)和数据库即服务(DBaaS)。随着市场对云计算的信心不断增强,随着互联网速度的加快和价格的下降,我们可能会看到越来越多的此类系统得到开发,这就引出了本章的主题。
Function as a Service,或serverlesscomputing,是一小段代码的部署,实际上是一个函数,通常可以通过某种触发器从其他应用程序调用。用例包括图像转换、日志分析,以及我们将在本章中构建的通知系统。
不管名称serverless意味着什么,实际上涉及到一个服务器,这是有道理的;但是,作为一名应用程序开发人员,您不必对服务器考虑得太深。事实上,正如我们将在本章中看到的,我们唯一需要担心的是函数需要多少内存。服务器的所有其他方面都完全由服务提供商的功能来处理——操作系统、存储、网络,甚至启动和停止虚拟机都由服务提供商为我们处理。
基于对无服务器的基本理解,我们需要选择一个提供商。正如可以预料的那样,有很多选择——亚马逊、甲骨文、IBM、红帽等等。不幸的是,目前还没有标准化的方法来编写无服务器系统并将其部署到任意提供商,因此这意味着我们的解决方案必然会绑定到特定的提供商,即云计算服务的主要提供商Amazon Web Services(AWS。正如本章导言中提到的,我们使用了许多 AWS 产品,但核心产品将是 AWS Lambda,Amazon 的无服务器计算产品。
我们跳进去吧。
我们将构建的应用程序是一个非常简单的云通知服务。简而言之,我们的功能将侦听消息,然后将这些消息转发到系统中注册的电子邮件地址和电话号码。虽然我们的系统将有点做作,当然非常简单,但希望更实际的用例是清楚的:
- 我们的系统提醒学生和/或家长即将举行的活动
- 当孩子进入或离开某些地理边界时,会通知家长
- 系统管理员会在发生某些事件时收到通知
可能性很大。出于我们在这里的目的,我们不仅要开发基于云的系统,还要开发一个简单的桌面应用程序来模拟这些类型的场景。我们将从有趣的地方开始:云中。
作为服务的功能的核心当然是功能。在 AmazonWeb 服务中,这些服务是使用 AWS Lambda 服务部署的。正如我们已经提到的,这不是我们将使用的唯一 AWS 功能。一旦我们有了一个函数,我们就需要一种执行它的方法。这是通过一个或多个触发器完成的,函数本身有它需要执行的任务,因此我们将在最终编写函数时通过 API 调用演示更多的服务使用情况。
考虑到我们的应用程序的结构与我们所看到的任何其他应用程序的结构都有很大的不同,此时查看系统图可能会有所帮助:
大致流程如下:
- 消息将发布到简单通知系统中的主题
- 一旦验证了调用者的权限,就会传递消息
- 消息传递时,触发一个触发器,将消息从主题传递到我们的函数
- 在函数中,我们将查询亚马逊的DynamoDB以获得已注册的收件人列表,提供电子邮件地址、手机号码或两者
- 所有手机号码将通过简单通知系统发送短信
- 所有电子邮件地址将通过简单电子邮件服务发送一封电子邮件
要开始构建函数,我们需要创建一个 Java 项目。与我们的许多其他项目一样,这将是一个多模块 Maven 项目。在 NetBeans 中,单击文件|新项目| Maven | POM 项目。我们将调用CloudNotice
项目。
该项目将有三个模块——一个用于函数,一个用于测试/演示客户端,一个用于共享 API。要创建功能模块,请在项目浏览器中的Modules
节点上单击鼠标右键,然后选择“创建新模块”。在窗口中,选择 Maven | Java Application,单击 Next,并将项目名称设置为function
。重复这些步骤,创建一个名为api
的模块。
在进一步讨论之前,我们必须解决一个事实,即在编写本文时,AWS 不支持 Java9。因此,我们必须以 Java8(或更早版本)为目标,以获取我们将要交付给 Lambda 的任何东西。为此,我们需要修改我们的pom.xml
文件,如下所示:
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
修改api
和function
的 POM。希望 AWS 在 Java9 发布后能尽快支持它。在那之前,我们只需要瞄准 JDK 8。
配置好项目后,我们就可以编写函数了。AWS lambda 实现为RequestHandler
实例:
public class SnsEventHandler
implements RequestHandler<SNSEvent, Object> {
@Override
public Object handleRequest
(SNSEvent request, Context context) {
LambdaLogger logger = context.getLogger();
final String message = request.getRecords().get(0)
.getSNS().getMessage();
logger.log("Handle message '" + message + "'");
return null;
}
最终,我们希望在消息传递到 SNS 主题时触发我们的函数,因此我们指定SNSEvent
作为输入类型。我们还指定了Context
。我们可以从Context
中获得一些信息,例如请求 ID、内存限制等,但我们感兴趣的只是获取一个LambdaLogger
实例。我们可以只写标准输出和标准错误,这些消息将保存在 Amazon CloudWatch 中,但LambdaLogger
允许我们尊重系统权限和容器配置。
为了进行编译,我们需要向应用程序添加一些依赖项,因此我们在pom.xml
中添加以下行:
<properties>
<aws.java.sdk.version>[1.11, 2.0.0)</aws.java.sdk.version>
</properties>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
我们现在可以按如下方式开始实施该方法:
final List<Recipient> recipients = new CloudNoticeDAO(false)
.getRecipients();
final List<String> emailAddresses = recipients.stream()
.filter(r -> "email".equalsIgnoreCase(r.getType()))
.map(r -> r.getAddress())
.collect(Collectors.toList());
final List<String> phoneNumbers = recipients.stream()
.filter(r -> "sms".equalsIgnoreCase(r.getType()))
.map(r -> r.getAddress())
.collect(Collectors.toList());
我们有几个新的类要看,但为了先回顾一下这段代码,我们将得到一个Recipient
实例列表,它表示订阅我们服务的数量和电子邮件地址。然后,我们从列表中创建一个流,过滤每个收件人类型SMS
或Email
,通过map()
提取值,然后将其收集到List
中。
稍后我们将进入CloudNoticeDAO
和Recipient
,但让我们先结束函数。一旦我们有了清单,我们就可以按如下方式发送信息:
final SesClient sesClient = new SesClient();
final SnsClient snsClient = new SnsClient();
sesClient.sendEmails(emailAddresses, "j9bp@steeplesoft.com",
"Cloud Notification", message);
snsClient.sendTextMessages(phoneNumbers, message);
sesClient.shutdown();
snsClient.shutdown();
我们在自己的客户端类SesClient
和SnsClient
后面封装了另外两个 AWS API。这似乎有点过分,但这些类型的事情往往会增长,这种方法使我们处于一个良好的位置来管理它。
这就给我们留下了三个 API:DynamoDB、简单电子邮件服务和简单通知服务。我们会把它们整理好的。
Amazon DynamoDB 是一个 NoSQL 数据库,非常类似于 MongoDB,我们在第 9 章中看到了 MongoDB,它使用 Monetum记录笔记,尽管 DynamoDB 同时支持文档和键值存储模型。对两者进行彻底的比较,以及就选择哪一个提出建议,远远超出了我们在这里的工作范围。我们在这里选择了 DynamoDB,因为它已经在 amazonweb 服务中配置好了,因此可以轻松地为我们的应用程序进行配置。
为了开始使用 dynamodbapi,我们需要向应用程序添加一些依赖项。在api
模块中,将其添加到pom.xml
文件中:
<properties>
<sqlite4java.version>1.0.392</sqlite4java.version>
</properties>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-dynamodb</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>${aws.java.sdk.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>sqlite4java</artifactId>
<version>${sqlite4java.version}</version>
<optional>true</optional>
</dependency>
在开始编写 DAO 类之前,让我们定义一下简单的模型。DynamoDB API 提供了一个对象关系映射工具,很像 Java 持久性 API 或 Hibernate,它需要一个 POJO 和一些注释,如我们在这里看到的:
public class Recipient {
private String id;
private String type = "SMS";
private String address = "";
// Constructors...
@DynamoDBHashKey(attributeName = "_id")
public String getId() {
return id;
}
@DynamoDBAttribute(attributeName = "type")
public String getType() {
return type;
}
@DynamoDBAttribute(attributeName="address")
public String getAddress() {
return address;
}
// Setters omitted to save space
}
在我们的 POJO 中,我们声明了三个属性,id
、type
和address
,然后用@DyanoDBAttribute
注释 getter,以帮助库理解如何映射对象。
请注意,虽然大多数属性名称与表中的字段名称匹配,但您可以像我们对id
所做的那样重写属性到字段名称的映射。
在处理数据之前,我们需要声明表。请记住,DynamoDB 是一个 NoSQL 数据库,我们将使用它作为文档存储,就像使用 MongoDB 一样。然而,在我们可以存储任何数据之前,我们必须定义将其放置在的位置。在 MongoDB 中,我们将创建一个集合。然而,DynamoDB 仍然将其称为一个表,虽然从技术上讲它是无模式的,但我们确实需要定义一个主键,它由一个分区键和一个可选的排序键组成。
我们通过控制台创建表。登录到 AWS DynamoDB 控制台后,您将单击“创建表格”按钮,该按钮将显示如下屏幕:
我们将表命名为recipients
,并指定_id
作为分区键。单击“创建表格”按钮,给 AWS 创建表格的时间。
我们现在已经准备好开始编写 DAO 了。在 API 模块中,创建一个名为CloudNoticeDAO
的类,我们将向其中添加此构造函数:
protected final AmazonDynamoDB ddb;
protected final DynamoDBMapper mapper;
public CloudNoticeDAO(boolean local) {
ddb = local ? DynamoDBEmbedded.create().amazonDynamoDB()
: AmazonDynamoDBClientBuilder.defaultClient();
verifyTables();
mapper = new DynamoDBMapper(ddb);
}
local 属性用于确定是否使用本地 DynamoDB 实例。这是为了支持测试(就像对verifyTables
的调用一样),我们稍后将对此进行探讨。在生产中,我们的代码将调用AmazonDynamoDBClientBuilder.defaultClient()
来获取AmazonDynamoDB
的实例,该实例与 Amazon 托管的实例对话。最后,我们创建了一个DynamoDBMapper
实例,用于对象映射。
为了方便创建新的Recipient
,我们将添加以下方法:
public void saveRecipient(Recipient recip) {
if (recip.getId() == null) {
recip.setId(UUID.randomUUID().toString());
}
mapper.save(recip);
}
此方法将在数据库中创建新条目,如果主键已存在,则更新现有条目。在某些场景中,使用单独的 save 和 update 方法可能是有意义的,但是我们的用例非常简单,我们不需要担心这一点。如果缺少键值,我们需要做的就是创建键值。我们通过创建一个随机 UUID 来实现这一点,这有助于避免在有多个进程或应用程序写入数据库时发生密钥冲突。
删除一个Recipient
实例或获取数据库中所有Recipient
实例的列表同样简单:
public List<Recipient> getRecipients() {
return mapper.scan(Recipient.class,
new DynamoDBScanExpression());
}
public void deleteRecipient(Recipient recip) {
mapper.delete(recip);
}
在我们离开我们的 DAO 之前,让我们来看看我们如何测试它。在前面,我们注意到了local
参数和verifyTables()
方法,它们是为测试而存在的。
一般来说,大多数人都会对仅仅为了测试而向生产类添加方法表示不满,这是理所当然的。编写一个可测试的类和向类中添加测试方法是有区别的。我同意只为测试而向类添加方法是应该避免的,但是为了简单和简洁起见,我在这里稍微违反了这个原则。
verifyTables()
方法检查表是否存在;如果表没有,我们调用另一个方法来为我们创建它。当我们使用前面的控制台手动创建生产表时,我们也可以让这个方法为我们创建该表。你使用什么方法完全取决于你自己。请注意,将存在需要解决的性能和权限问题。也就是说,该方法看起来像这样:
private void verifyTables() {
try {
ddb.describeTable(TABLE_NAME);
} catch (ResourceNotFoundException rnfe) {
createRecipientTable();
}
}
private void createRecipientTable() {
CreateTableRequest request = new CreateTableRequest()
.withTableName(TABLE_NAME)
.withAttributeDefinitions(
new AttributeDefinition("_id", ScalarAttributeType.S))
.withKeySchema(
new KeySchemaElement("_id", KeyType.HASH))
.withProvisionedThroughput(new
ProvisionedThroughput(10L, 10L));
ddb.createTable(request);
try {
TableUtils.waitUntilActive(ddb, TABLE_NAME);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
通过调用describeTable()
方法,我们可以检查表是否存在。在我们的测试中,每次都会失败,这将导致创建表。在生产环境中,如果使用此方法创建表,则此调用仅在第一次调用时失败。在createRecipientTable()
中,我们可以看到如何以编程方式创建表。我们还要等到表处于活动状态,以确保在创建表时读写不会失败。
因此,我们的测试非常简单。例如,考虑下面的代码片段:
private final CloudNoticeDAO dao = new CloudNoticeDAO(true);
@Test
public void addRecipient() {
Recipient recip = new Recipient("SMS", "test@example.com");
dao.saveRecipient(recip);
List<Recipient> recipients = dao.getRecipients();
Assert.assertEquals(1, recipients.size());
}
此测试帮助我们验证模型映射是否正确,以及 DAO 方法是否按预期运行。您可以在源代码包的CloudNoticeDaoTest
类中看到其他测试。
要发送电子邮件,我们将使用 Amazon Simple Email Service,或 SES,我们将把它包装在api
模块的SesClient
类中。
IMPORTANT: Before you can send an email, you have to verify either your sending/from address or domain. The verification process is fairly simple, but how to do that is probably best left to Amazon's documentation, which you can read here: http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html.
简单的电子邮件服务 API 非常简单。我们需要创建一个Destination
,告诉系统向谁发送电子邮件;描述信息本身的Message
,包括主题、正文和收件人;还有一个SendEmailRequest
将一切联系在一起:
private final AmazonSimpleEmailService client =
AmazonSimpleEmailServiceClientBuilder.defaultClient();
public void sendEmails(List<String> emailAddresses,
String from,
String subject,
String emailBody) {
Message message = new Message()
.withSubject(new Content().withData(subject))
.withBody(new Body().withText(
new Content().withData(emailBody)));
getChunkedEmailList(emailAddresses)
.forEach(group ->
client.sendEmail(new SendEmailRequest()
.withSource(from)
.withDestination(
new Destination().withBccAddresses(group))
.withMessage(message)));
shutdown();
}
public void shutdown() {
client.shutdown();
}
不过,在前面的粗体代码中有一个重要的警告。SES 将每条邮件的收件人数量限制为 50 个,因此我们需要获取电子邮件地址列表,并一次处理 50 个。我们将使用getChunkedEmailList()
方法:
private List<List<String>> getChunkedEmailList(
List<String> emailAddresses) {
final int numGroups = (int) Math.round(emailAddresses.size() /
(MAX_GROUP_SIZE * 1.0) + 0.5);
return IntStream.range(0, numGroups)
.mapToObj(group ->
emailAddresses.subList(MAX_GROUP_SIZE * group,
Math.min(MAX_GROUP_SIZE * group + MAX_GROUP_SIZE,
emailAddresses.size())))
.collect(Collectors.toList());
}
为了找到组的数量,我们将地址的数量除以 50 并取整(例如,254 个地址将得到 6 个组——50 个组中的 5 个,4 个组中的 1 个)。然后,使用一个IntStream
从 0 到组数(排除),我们从原始列表中提取子列表。然后,这些列表中的每一个都被收集到另一个List
中,为我们提供了在方法签名中看到的嵌套Collection
实例。
Design note: Many developers will avoid using nested Collection
instances like this, as it can quickly become difficult to understand what exactly the variable represents. It is considered by many to be a best practice in situations like this to create a new type to hold the nested data. For example, if we were to follow that advice here, we could create, perhaps, a new Group
class that had a List<String>
property to hold the group's email addresses. We have not done so for the sake of brevity, but that would definitely be a good enhancement to this code.
一旦我们将列表分块,我们就可以向每个小组发送相同的Message
,从而履行 API 合同。
我们已经看到了简单的通知系统在工作,至少在理论上是这样,因为这就是将出站消息传递给我们的功能的原因:某种类型的客户端在特定的 SNS 主题中发布消息。我们订阅了该主题(稍后我将向您展示如何创建该主题),该主题使用消息调用我们的方法以供我们传递。我们现在将使用 SNS API 向已向系统订阅电话号码的用户发送文本(或 SMS)消息。
使用 SNS,要向多个电话号码发送消息,您必须通过每个号码订阅的主题发送消息。然后,我们将按照以下步骤进行操作:
- 创建一个主题。
- 订阅所有的电话号码。
- 将消息发布到主题。
- 删除该主题。
如果我们使用一个持久性主题,那么如果同时运行多个函数实例,我们可能会得到不可预测的结果。协调所有这些工作的方法如下所示:
public void sendTextMessages(List<String> phoneNumbers,
String message) {
String arn = createTopic(UUID.randomUUID().toString());
phoneNumbers.forEach(phoneNumber ->
subscribeToTopic(arn, "sms", phoneNumber));
sendMessage(arn, message);
deleteTopic(arn);
}
要创建主题,我们有以下方法:
private String createTopic(String arn) {
return snsClient.createTopic(
new CreateTopicRequest(arn)).getTopicArn();
}
要订阅主题的编号,我们有以下方法:
private SubscribeResult subscribeToTopic(String arn,
String protocol, String endpoint) {
return snsClient.subscribe(
new SubscribeRequest(arn, protocol, endpoint));
}
发布消息同样简单,正如我们在这里看到的:
public void sendMessage(String topic, String message) {
snsClient.publish(topic, message);
}
最后,您可以使用以下简单方法删除主题:
private DeleteTopicResult deleteTopic(String arn) {
return snsClient.deleteTopic(arn);
}
所有这些方法显然都非常简单,因此对 SNS API 的调用可以直接内联到调用代码中,但是这个包装器确实为我们提供了一种方法,可以从我们的业务代码中隐藏 API 的细节。这一点更为重要,例如,在createTopic()
中,需要额外的类,但为了保持一致,我们将在自己的外观后面封装所有内容。
我们现在已经完成了我们的功能,几乎可以部署它了。要做到这一点,我们需要打包。AWS 允许我们上传 ZIP 或 JAR 文件。我们将使用后者。但是,我们有一些外部依赖项,因此我们将使用Maven Shade插件构建一个包含函数及其所有依赖项的胖罐子。在function
模块中,将以下代码添加到pom.xml
文件中:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>
cloudnotice-function-${project.version}
</finalName>
</configuration>
</execution>
</executions>
</plugin>
现在,当我们构建项目时,我们将在目标目录中获得一个大文件(大约 9MB)。我们要上传的就是这个文件。
在上传函数之前,我们需要通过创建适当的角色来准备 AWS 环境。登录 AWS 并导航至身份和访问管理控制台(https://console.aws.amazon.com/iam )。在左侧的导航窗格中,单击角色,然后单击创建新角色:
当提示选择角色时,我们希望选择 AWS Lambda。在下一页,我们将附上政策:
单击 Next,将名称设置为j9bp
,然后单击 Create role。
为了简化函数和相关触发器的创建,我们将首先创建主题。导航到 SNS 控制台。鉴于并非所有 AWS 功能在每个地区都始终可用,我们需要选择一个特定的地区。我们可以在网页的左上角这样做。如果该地区没有显示 N.Virginia,请在继续之前从下拉菜单中选择它——美国东部(N.Virginia)。
正确设置区域后,单击左侧导航栏中的主题,然后单击创建新主题并指定名称为cloud-notice
:
现在,我们可以导航到 Lambda 控制台并部署我们的功能。我们将首先单击 Createalambda 函数按钮。我们将被要求选择一个蓝图。唯一适合基于 Java 的函数的选项是 Blank 函数。单击该选项后,我们将看到配置触发器屏幕。当您单击空方块时,将显示一个下拉菜单,如 AWS 控制台的屏幕截图所示:
您可以向下滚动以查找 SNS,或者在过滤器框中输入SNS
,如前一屏幕截图所示。无论哪种方式,当您单击列表中的 SNS 时,都会要求您选择要订阅的主题:
点击下一步。我们现在需要详细说明我们的功能:
向下滚动页面,我们还需要指定 Lambda 函数处理程序和角色。处理程序是完全限定的类名,后跟两个冒号和方法名:
我们现在需要通过单击 upload 按钮并选择由 Maven 构建创建的 jar 文件来选择函数归档。单击 Next,验证函数的详细信息,然后单击 Create function。
我们现在有了一个可用的 AWS Lambda 函数。我们可以使用 Lambda 控制台对其进行测试,但我们将构建一个小型 JavaFX 应用程序来进行测试,它将同时测试所有服务集成,并演示生产应用程序如何与功能交互。
为了帮助测试和演示系统,我们将在CloudNotice
项目中创建一个名为manager
的新模块。为此,单击 NetBeans 项目浏览器中的模块节点,然后单击创建新模块…|Maven | JavaFX 应用程序。调用项目Manager
并单击 Finish。
我已将MainApp
更名为CloudNoticeManager
、FXMLController
更名为CloudNoticeManagerController
、Scene.fxml
更名为manager.fxml
。
我们的Application
类看起来与以前的 JavaFX 应用程序略有不同。一些 AWS 客户机 API 要求在使用完毕后显式关闭它们。否则,我们的应用程序将不会完全退出,留下必须终止的僵尸进程。为了确保正确关闭 AWS 客户端,我们需要向控制器添加一个清理方法,我们将从应用程序中的stop()
方法调用该方法:
private FXMLLoader fxmlLoader;
@Override
public void start(final Stage stage) throws Exception {
fxmlLoader = new FXMLLoader(getClass()
.getResource("/fxml/manager.fxml"));
Parent root = fxmlLoader.load();
// ...
}
@Override
public void stop() throws Exception {
CloudNoticeManagerController controller =
(CloudNoticeManagerController) fxmlLoader.getController();
controller.cleanup();
super.stop();
}
现在,无论用户是单击文件|退出还是单击窗口上的关闭按钮,我们的 AWS 客户端都可以正确地清理。
在布局方面,没有什么新的东西可以讨论,所以我们在这里不讨论这个方面。这就是我们的 manager 应用程序的外观:
我们在左侧有一个订阅收件人列表,在右上角有一个用于添加和编辑收件人的区域,在右下角有一个用于发送测试消息的区域。我们确实有一些有趣的绑定,所以让我们来看看这个。
首先,在CloudNoticeManagerController
中,我们需要为我们的数据声明一些容器,所以我们声明了一些ObservableList
实例:
private final ObservableList<Recipient> recips =
FXCollections.observableArrayList();
private final ObservableList<String> types =
FXCollections.observableArrayList("SMS", "Email");
private final ObservableList<String> topics =
FXCollections.observableArrayList();
这三个ObservableList
实例将支持与其名称匹配的 UI 控件。我们将在initalize()
中填充其中两个列表(type
为硬编码),如下所示:
public void initialize(URL url, ResourceBundle rb) {
recips.setAll(dao.getRecipients());
topics.setAll(sns.getTopics());
type.setItems(types);
recipList.setItems(recips);
topicCombo.setItems(topics);
使用我们的 DAO 和 SES 客户端,我们获取任何已订阅的收件人,以及帐户中配置的任何主题。这将得到每个主题,所以如果你有很多,这可能是一个问题,但这只是一个演示应用程序,所以在这里应该可以。一旦有了这两个列表,我们就将它们添加到前面创建的ObservableList
实例中,然后将List
与相应的 UI 控件关联起来。
为了确保Recipient
列表正确显示,我们需要创建一个CellFactory
,如下所示:
recipList.setCellFactory(p -> new ListCell<Recipient>() {
@Override
public void updateItem(Recipient recip, boolean empty) {
super.updateItem(recip, empty);
if (!empty) {
setText(String.format("%s - %s", recip.getType(),
recip.getAddress()));
} else {
setText(null);
}
}
});
请记住,如果单元格为空,我们需要将文本设置为 null 以清除任何以前的值。如果不这样做,将导致在某个时刻出现带有幻影条目的ListView
。
接下来,当用户点击列表中的Recipient
时,我们需要更新编辑控件。我们通过在selectedItemProperty
中添加一个监听器来实现这一点,该监听器在所选项目每次更改时都会运行:
recipList.getSelectionModel().selectedItemProperty()
.addListener((obs, oldRecipient, newRecipient) -> {
type.valueProperty().setValue(newRecipient != null ?
newRecipient.getType() : "");
address.setText(newRecipient != null ?
newRecipient.getAddress() : "");
});
如果newRecipient
不为空,我们将控件的值设置为适当的值。否则,我们将清除这些值。
我们现在需要为各种按钮添加处理程序,Recipient
列表上方的添加和删除按钮,以及右侧两个表单区域中的Save
和Cancel
按钮。
UI 控件的onAction
属性可以通过直接编辑 FXML 绑定到类中的方法,如下所示:
<Button mnemonicParsing="false"
onAction="#addRecipient" text="+" />
<Button mnemonicParsing="false"
onAction="#removeRecipient" text="-" />
也可以通过在场景生成器中编辑属性将其绑定到方法,如以下屏幕截图所示:
无论哪种方式,该方法都将如下所示:
@FXML
public void addRecipient(ActionEvent event) {
final Recipient recipient = new Recipient();
recips.add(recipient);
recipList.getSelectionModel().select(recipient);
type.requestFocus();
}
我们正在添加一个Recipient
,所以我们创建了一个新的,将它添加到我们的ObservableList
,然后告诉ListView
选择这个条目。最后,我们要求type
控件请求焦点,以便用户可以根据需要轻松地用键盘更改值。在用户单击 Save(保存)之前,新的收件人不会保存到 DynamoDB,稍后我们将看到这一点。
当我们删除一个Recipient
时,我们需要将其从 UI 和 DynamoDB 中删除:
@FXML
public void removeRecipient(ActionEvent event) {
final Recipient recipient = recipList.getSelectionModel()
.getSelectedItem();
dao.deleteRecipient(recipient);
recips.remove(recipient);
}
储蓄有点复杂,但不多:
@FXML
public void saveChanges(ActionEvent event) {
final Recipient recipient =
recipList.getSelectionModel().getSelectedItem();
recipient.setType(type.getValue());
recipient.setAddress(address.getText());
dao.saveRecipient(recipient);
recipList.refresh();
}
由于我们没有将编辑控件的值绑定到列表中的选定项,因此需要获取对该项的引用,然后将值从控件复制到模型。完成后,我们通过 DAO 将其保存到数据库中,然后要求ListView
刷新自身,以便任何模型更改都反映在列表中。
我们没有将控件绑定到列表中的项,因为这会导致稍微混乱的用户体验。如果我们绑定了,因为用户对模型所做的更改ListView
将反映这些更改。可以想象,用户随后会假设这些更改正在保存到数据库中,而事实上它们并没有保存到数据库中。在用户单击 Save 之前,不会发生这种情况。为了避免这种混乱和数据丢失,我们已经没有绑定控件并手动管理数据。
要取消更改,我们只需从ListView
获取对未更改模型的引用,并将其值复制到编辑控件中的值上:
@FXML
public void cancelChanges(ActionEvent event) {
final Recipient recipient = recipList.getSelectionModel()
.getSelectedItem();
type.setValue(recipient.getType());
address.setText(recipient.getAddress());
}
这就给我们留下了 UI 的发送消息部分。由于我们的 SNS 包装 API,这些方法非常简单:
@FXML
public void sendMessage(ActionEvent event) {
sns.sendMessage(topicCombo.getSelectionModel()
.getSelectedItem(), messageText.getText());
messageText.clear();
}
@FXML
public void cancelMessage(ActionEvent event) {
messageText.clear();
}
从桌面应用程序中,我们现在可以添加、编辑和删除收件人,以及发送测试消息。
那些密切关注的人可能会问一个非常重要的问题——AWS 客户端库如何知道如何登录到我们的帐户?显然,我们需要告诉他们,我们有几个选择。
AWS SDK 在本地运行时,将检查三个位置的凭据—环境变量(AWS_ACCESS_KEY_ID
和AWS_SECRET_ACCESS_KEY
)、系统属性(aws.accessKeyId
和aws.secretKey
)以及默认凭据配置文件($HOME/.aws/credentials
。您使用什么凭据取决于您,但我将在这里向您展示如何配置配置文件文件。
就像 Unix 或 Windows 系统一样,您的 AWS 帐户有一个root
用户可以完全访问您的系统。以该用户身份运行任何连接的客户端代码都是极不明智的。为了避免这种情况,我们需要创建一个用户,我们可以在身份和访问管理控制台(上执行此操作 https://console.aws.amazon.com/iam 。
登录后,单击左侧的用户,然后在顶部添加用户,其结果显示在以下屏幕截图中:
单击 Next:Permissions 并检查我们角色的组列表中的条目j9bp
。单击下一步:查看,然后创建用户。这将带您进入添加用户屏幕,其中应该有一个成功消息框。重要的部分是屏幕底部列出的用户信息。在该表的右侧,您应该看到列 Access key ID 和 Secret Access key。单击访问键上的“显示”以显示值。请记下这两项,因为一旦离开此页面,就无法检索访问密钥。如果丢失了它,则必须生成一组新的密钥,这将破坏使用旧凭据的任何其他应用程序。
在文本编辑器中,我们需要创建~/.aws/credentials
文件。在 Unix 系统上,这可能是/home/jdlee/.aws
,在 Windows 计算机上,这可能是类似于C:\Users\jdlee\aws
的。凭据文件应如下所示:
[default]
aws_access_key_id = AKIAISQVOILE6KCNQ7EQ
aws_secret_access_key = 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
在同一个目录中,我们需要创建另一个名为config
的文件。我们将使用此文件告诉 SDK 我们要在哪个区域工作:
[default]
region = us-east-1
当 AWS 客户端现在启动时,它们将默认以us-east-1
区域中的j9bp
用户身份连接。如果您需要覆盖该文件,您可以编辑该文件,或设置上文“配置 AWS 凭证”一节中提到的环境变量或系统属性。
我们做到了!我们中的许多人都创建了第一个 AWS Lambda 函数,其实并不难。当然,这是一个简单的应用程序,但我希望您能看到这种类型的应用程序如何非常有用。以此为起点,您可以在移动应用程序的帮助下编写系统,以帮助跟踪您家人的位置。例如,使用 Raspberry PI 等嵌入式设备,您可以构建设备来跟踪在全国范围内运输的库存,报告位置、速度、环境条件、突然下降或影响等。服务器上运行的软件可以不断报告系统的各种指标,如 CPU 温度、可用磁盘空间、分配的内存、系统负载等。你的选择仅限于你的想象力。
总结一下,让我们快速回顾一下我们学到的东西。我们了解了一些不同的**。。。作为一种服务系统,今天正在提供,以及无服务器**的真正含义,以及它对我们作为应用程序开发人员的吸引力。我们学习了如何配置各种 Amazon Web 服务产品,包括身份和访问管理、简单通知系统、简单电子邮件服务,当然还有 Lambda,我们还学习了如何用 Java 编写 AWS Lambda 函数以及如何将其部署到服务中。最后,我们学习了如何配置触发器,将 SNS 发布/订阅主题与 Lambda 函数联系起来。
不可否认,我们的应用程序有点简单,在一章的篇幅内,您无法成为亚马逊 Web 服务或任何其他云提供商所提供的所有方面的专家。希望您有足够的时间使用 Java 编写基于云的应用程序,并为此感到兴奋。对于那些想深入研究的人来说,这里有许多很棒的书籍、网页等,可以帮助你深入研究这个快速变化和扩展的领域。在下一章中,我们将从云端返回,并将注意力转向 Java 开发人员的另一个巨大空间——您的手机。