JavaEE8-设计模式和最佳实践-全-
JavaEE8 设计模式和最佳实践(全)
原文:
zh.annas-archive.org/md5/8384663b1ec8e10d12dac46e1fbd92dc译者:飞龙
前言
随着时间的推移,企业界在优化流程和帮助企业增加利润以及改善服务或产品方面投入了越来越多的技术和应用。企业环境面临需要面对的挑战,以实施良好的解决方案,例如服务的高可用性、在需要时改变的能力、服务扩展的能力以及处理大量数据的能力。因此,创建了新的应用来优化流程和增加利润。Java 语言和 Java EE 是创建企业环境应用的优秀工具,因为 Java 语言是跨平台的、开源的、经过广泛测试的,并且拥有强大的社区和生态系统。此外,Java 语言拥有 Java EE,这是一个允许我们开发者开发企业应用而不依赖于供应商的规范集合。企业应用的开发有一些众所周知的问题会反复出现。这些问题涉及服务的集成、应用程序的高可用性和弹性。
这本书将解释 Java EE 8 的概念、其层级结构以及如何使用 Java EE 8 最佳实践来开发企业应用。此外,这本书将展示我们如何使用 Java EE 8 与设计模式和企业模式相结合,以及我们如何使用面向方面编程、响应式编程和 Java EE 8 的微服务来优化我们的解决方案。在这本书的整个过程中,我们将了解集成模式、响应式模式、安全模式、部署模式和操作模式。这本书的结尾,我们将对 MicroProfile 有一个概述,以及它如何帮助我们使用微服务架构开发应用。
这本书面向的对象
这本书是为想要学习使用设计模式、企业模式和 Java 最佳实践来开发和交付企业应用的 Java 开发者而写的。读者需要了解 Java 语言和 Java EE 的基本概念。
为了充分利用这本书
-
在阅读这本书之前,读者需要了解面向对象的概念、Java 语言以及 Java EE 的基本概念。在这本书中,我们假设读者已经了解 Java EE 伞形规范的一些规格,例如 EJB、JPA 和 CDI 等。
-
要测试这本书的代码,您需要一个支持 Java EE 8 的应用服务器,例如 GlassFish 5.0。此外,您需要使用 IntelliJ、Eclipse、NetBeans 或其他支持 Java 语言的任何 IDE。
下载示例代码文件
您可以从www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择“支持”标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载后,请确保使用最新版本的软件解压或提取文件夹:
-
Windows 下的 WinRAR/7-Zip
-
Mac 下的 Zipeg/iZip/UnRarX
-
Linux 下的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Java-EE-8-Design-Patterns-and-Best-Practices。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包可供使用,网址为github.com/PacktPublishing/。请查看它们!
下载彩色图像
我们还提供包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/JavaEE8DesignPatternsandBestPractices_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“还重要的是要注意,@Electronic修饰符标识了被修饰的对象。”
代码块设置如下:
public interface Engineering {
List<String> getDisciplines ();
}
public class BasicEngineering implements Engineering {
@Override
public List<String> getDisciplines() {
return Arrays.asList("d7", "d3");
}
}
@Electronic
public class ElectronicEngineering extends BasicEngineering {
...
}
@Mechanical
public class MechanicalEngineering extends BasicEngineering {
...
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
@Loggable
@Interceptor
public class LoggedInterceptor implements Serializable {
@AroundInvoke
public Object logMethod (InvocationContext invocationContext) throws
Exception{
System.out.println("Entering method : "
+ invocationContext.getMethod().getName() + " "
+ invocationContext.getMethod().getDeclaringClass()
);
return invocationContext.proceed();
}
}
任何命令行输入或输出都如下所示:
creating bean.
intercepting post construct of bean.
post construct of bean
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“用户登录后,当他们访问应用程序 1、应用程序 2或应用程序 3时,他们无需再次登录。”
警告或重要提示如下所示。
技巧和窍门如下所示。
联系我们
我们欢迎读者的反馈。
一般反馈:请通过电子邮件feedback@packtpub.com发送反馈,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件questions@packtpub.com联系我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至copyright@packtpub.com与我们联系。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且对撰写或参与一本书籍感兴趣,请访问 authors.packtpub.com.
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packtpub.com.
第一章:设计模式简介
本章将介绍设计模式,探讨使用它们的原因,它们与业务模式的不同,以及它们在现实世界中的表现。
由于我们假设你已经熟悉 Java 编程语言和 Java EE,我们的目标不是教授 Java EE,而是展示其最常见的设计模式。我们还将演示使用 Java EE 8 实现设计模式的示例。此外,我们将展示实现设计模式的最优方法,并讨论使用设计模式和业务模式的好处。如果你不了解设计模式和业务模式,那么这本书将是一个学习设计模式和业务模式概念及其实现的大好工具。如果你已经了解设计模式和业务模式,那么这本书将是一个在实现它们时参考的绝佳点。在本章中,我们将涵盖以下主题:
-
理解设计模式
-
理解设计模式的优势
-
定义 Java 世界的基本设计模式
-
解释业务模式
-
解释设计模式与业务模式之间的区别
解释设计模式
设计模式是一系列针对在开发中反复出现的常见设计问题的解决方案。它们作为一个解决方案模板,其中描述了一个常见问题的抽象解决方案,然后用户应用它,根据他们的问题进行适配。在面向对象编程中,设计模式提供了一种为特定问题设计可重用类和对象的方法,以及定义对象和类之间的关系。此外,设计模式在编程语言中提供了一种共同的表达方式,使得架构师和软件开发者能够在使用不同编程语言的情况下就共同和反复出现的问题进行沟通。有了这个,我们能够通过模式名称识别问题和解决方案,并通过在语言编程细节的高抽象层次上从模型角度思考解决方案。
设计模式主题在 1994 年得到了加强,当时“四人帮”(由 Rich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 组成)撰写了《设计模式:可复用面向对象软件元素》。在这里,他们描述了后来被称为 GoF 设计模式的 23 个设计模式,这些模式至今仍在使用。
解释四人帮设计模式
GoF(四人帮)设计模式是 23 种模式,分为创建型模式、结构型模式和行怍型模式。创建型模式控制对象的创建和初始化以及类选择;结构型模式定义类和对象之间的关系,行怍型模式控制对象之间的通信和交互。此外,GoF 设计模式有两种类型的范围,定义了解决方案的重点。这些范围是对象范围,解决关于对象关系的问题,以及类范围,解决关于类关系的问题。
对象范围与组合一起工作,行为变化在运行时完成。因此,对象可以具有动态行为。类范围与继承一起工作,其行为在编译时是静态固定的。然后,要改变类范围模式的行怍,我们需要更改类并重新编译。
被归类为类范围的模式解决关于类之间关系的问题,并且是静态的(在编译时固定,一旦编译后就不能更改)。然而,归类于对象范围的模式解决关于对象之间关系的问题,并且可以在运行时更改。
下面的图示展示了三种分类,以及它们的模式和范围:

在前面的图中,我们可以看到工厂方法模式位于类部分,而抽象工厂模式位于对象部分。这是因为工厂方法与继承一起工作,而抽象方法模式与组合一起工作。然后,工厂方法在编译时是静态固定的,编译后不能更改。然而,抽象工厂是动态的,可以在运行时更改。
GoF 设计模式通常使用图形符号,如用例图和实现代码的示例来描述。所使用的符号必须能够描述类和对象,以及这些类和对象之间的关系。
模式的名称是设计模式的重要组成部分。这是因为开发者用它来快速识别与模式相关的问题,并理解模式将如何解决它。模式的名称必须简短,并指代问题和解决方案。
设计模式是软件开发设计的一个伟大工具,但它的使用需要分析,以确定设计模式是否真正需要来解决该问题。
GoF 设计模式目录
设计模式的名称需要简洁,以便于识别。这是因为设计模式为开发者创造了一种与编程语言无关的交流词汇,允许开发者仅通过设计模式的名称来识别问题和解决方案。
在设计模式中,目录是一组模式名称,旨在允许开发者之间更好的沟通。
GoF( Gang of Four)的设计模式目录有 23 个模式,如前图所示。以下是这些模式的描述:
-
抽象工厂(Abstract Factory): 这提供了一个创建对象的接口,而不指定它们的具体类,使得业务逻辑和对象创建逻辑解耦成为可能。通过这种方式,我们可以轻松地更新对象创建逻辑。
-
适配器(Adapter): 这提供了一个接口,使得两个不兼容的接口能够一起工作。适配器模式作为接口之间的桥梁,将这些接口适配以协同工作。此外,适配器可以采用一个类或对象。
-
桥接(Bridge): 这种模式解耦了抽象与其实现,使它们可以独立变化。通过这种方式,我们可以修改实现而不影响抽象,也可以修改抽象而不影响实现。抽象类的类隐藏了实现及其复杂性。
-
建造者(Builder): 这种模式将复杂对象的构建与其表示分离。通过这种方式,我们可以使用相同的过程构建具有复杂构建过程的多个表示的对象。因此,我们创建了一个具有复杂构建过程的对象的标准化构建过程。
-
责任链(Chain of responsibility): 这种模式避免了请求发送者和接收者之间的耦合,创建了一些有机会处理请求的对象。这些对象为发送者的请求创建了一个接收者对象链。链中的每个对象都接收请求并验证是否处理此请求。
-
命令(Command): 这种模式封装了对对象的请求,并创建了一个包含请求信息的请求包装器。通过这种方式,我们可以向某个对象发送参数请求,而无需了解此操作。此外,命令允许我们执行
撤销操作。 -
组合(Composite): 这种模式将对象组合成树状结构,表示部分-整体层次结构。它允许你将一组对象视为单个对象。
-
装饰器(Decorator): 这种模式允许以灵活的方式扩展类的功能,而不需要使用子类。它允许你动态地为对象附加新的职责。
-
外观(Facade): 这隐藏了系统的复杂性,为子系统上的多个接口提供了一个统一的接口。这使得子系统易于使用。
-
工厂方法(Factory Method): 这定义了一个创建对象的接口,子类指定要初始化的类。
-
享元(Flyweight): 这通过共享有效地支持大量细粒度对象。这种模式减少了创建的对象数量。
-
解释器(Interpreter): 这种模式表示语言语法,并使用它来解释它们作为语言的句子。
-
迭代器(Iterator): 这个模式提供了一种按顺序访问一组对象元素的方法,而无需知道其底层表示。
-
中介者(Mediator): 通过创建一个封装所有对象之间通信和交互的对象来减少通信的复杂性。
-
备忘录(Memento): 这个模式在不损害封装概念的情况下捕获对象的内部状态,通过这个,对象的状态可以通过对象本身恢复。这个模式作为一个备份,维护对象的当前状态。
-
观察者(Observer): 这定义了对象之间的一对多依赖关系。这意味着如果一个对象被修改,所有依赖它的对象都会自动收到通知并更新。
-
原型(Prototype): 这个模式允许我们使用对象或实例作为原型来创建一个新的对象。这个模式创建了一个对象的副本,创建了一个具有作为原型使用的对象相同状态的新对象。
-
代理(Proxy): 这个模式为另一个对象(原始对象)创建一个代理对象(代理对象),以便控制对原始对象的访问。
-
状态(State): 这允许对象在内部状态改变时改变其行为。
-
单例(Singleton): 这确保在整个项目中一个类只有一个实例,每次执行创建过程时都返回相同的对象实例。
-
策略(Strategy): 这创建了一个算法族,封装每个算法并使它们可互换。这个模式允许你在运行时更改算法。
-
模板方法(Template Method): 这在操作中定义了一个算法的骨架,子类定义了算法的一些步骤。这个模式算法结构和子类重新定义了此算法的一些步骤,而不修改其结构。
-
访问者(Visitor): 这代表对对象结构执行的操作。这个模式允许我们在不修改其类的情况下向元素添加新的操作。
理解设计模式的优点
创建面向对象的设计是一项艰巨的任务。这是因为我们需要考虑我们将在其中工作的场景和我们将解决的问题的几个重要元素。这包括定义我们需要创建以达成解决方案的适当对象;定义对象的粒度并查看我们需要创建哪些接口。这些任务需要在设计过程中解决。可以创建对象来表示现实世界中的对象,或者表示具有其算法和责任的过程。此外,我们甚至需要考虑对象的数量、大小以及我们需要访问的接口。
设计模式是帮助我们识别不表示现实世界对象和不太明显的抽象对象的伟大工具。此外,设计模式帮助我们以最细粒度应用对象,并允许我们将问题和解决方案作为一个模型来分析和应用。设计模式使设计灵活,提供类和对象之间的解耦。它们还提供了组织解决方案的能力,允许将责任委托给以最佳方式实现这些解决方案的类。
对于公司来说,构建软件是一个昂贵的流程,因为它需要能够构建和维护软件的专业人才和基础设施。设计模式,凭借其灵活性和解耦设计,使得维护变得容易,从而降低了成本。
理解 Java 世界的经典设计模式
所有 GoF 模式都有良好的目的,解决了面向对象设计中的主要问题,但有些模式在 Java 和 Java EE 生态系统中最常使用。在这本书中,这些模式被视为基本设计模式,因为它们最常用于在 Java 的 API、框架和算法上实现解决方案。因此,理解这些模式将帮助我们理解这些 API、框架和算法,反过来,我们也能使用 Java 创建更好的解决方案。这些模式包括 Singleton、Abstract Factory、Facade、Iterator 和 Proxy。
解释 Singleton
在一个软件项目中,在某些解决方案中,我们可能希望确保在整个项目中一个类只有一个对象实例,并且这个对象在任何时候都可以访问。创建一个全局实例或静态实例并不能保证这个类在另一个实例的另一个点不会被使用。解决这个问题的最佳方式是使用 Singleton 模式,它确保在整个项目中只有一个类的实例。在下面的图中,我们展示了 Singleton 的结构及其设计方式:

在这里,我们有一个名为Singleton的类,它有一个private构造函数,以及一个 Singleton 引用变量和一个返回其唯一实例的方法。一个很好的应用例子是,当我们想要创建一个负责应用程序配置(一些资源的路径、访问文件系统的参数、环境的操作行为)的类时。通常,应用程序有一些配置,我们需要一个类来表示这些应用程序配置。因此,这个应用程序配置类不需要多个实例,只需要一个实例。
Singleton 的另一个应用场景是当我们想要创建一个将在下一个小节中解释的 Abstract Factory 时。通常,在整个应用程序中我们只有一个 Abstract Factory。通过这种方式,我们可以使用 Singleton 来保证我们只有一个 Abstract Factory 的实例。
这种模式通常用于框架和 API 中,但这种情况在 Java EE 项目的代码中也很常见。
单例模式的使用取决于场景,但根据场景的不同,单例的使用可能是一个好的实践,也可能是一个坏的做法。当对象是状态性的并且保持状态时,不应该使用单例,因为单例意味着同一个对象实例被应用程序的所有进程共享,如果某个进程更新了这个对象的状态,那么应用程序的所有进程都将受到这个更新的影响。此外,我们可能会遇到单例状态并发更新的问题。
解释抽象工厂
有时候,在项目中我们需要创建一系列的对象。想象一下,我们有一个电子商务网站,我们有各种产品,如手机、笔记本电脑和平板电脑。这些产品属于同一系列。如果我们在一个软件中创建了这些对象,当我们需要修改这个对象的初始化过程时,我们将会遇到问题。
使用抽象工厂可以帮助我们解决包括一个应该独立于其产品创建方式而存在的系统、一个应该使用多个产品系列之一的系统,以及一个应该与设计用来一起使用的对象一起工作的系统在内的问题。使用这种模式将有益,因为它隔离了具体类。这意味着,使用这种模式,我们可以控制在软件中可以初始化哪些类对象。此外,它还允许轻松地交换产品,并在产品之间提供一致性。
抽象工厂模式为对象创建提供了一个单一的创作点,如果我们需要更改对象创建的算法,我们只需要修改具体工厂。在下面的图中,你可以看到抽象工厂的结构以及它的设计方式:

在我们的例子中,抽象工厂的结构有三个主要类——AbstractFactory、Product和Sale。AbstractFactory的具体类是CellPhoneFactory、NotebookFactory和TabletFactory。CellPhoneFactory是一个具体类,负责创建具体类CellphoneProduct和CellphoneSale,NotebookFactory是一个具体类,负责创建具体类NotebookProduct和NotebookSale,而TabletFactory是一个具体类,负责创建具体类TabletProduct和TabletSale。Client是一个负责使用AbstractFactory来创建AbstractProduct和AbstractSale的类。具体工厂在运行时创建,然后创建具体的产品和销售。
抽象工厂模式有时会与之前描述的单例模式等其他模式一起使用。抽象工厂是一个创建点,通常在整个系统中只需要一个实例。有了这个,使用单例模式可以帮助我们创建更好的设计,并更高效。
这种模式通常用于具有复杂创建过程的框架和 API,例如连接或会话。
解释外观
项目有时可能会变得非常复杂和庞大,这使得它们难以设计和组织。为了解决这个问题,一个很好的解决方案是将系统分解为子系统(分而治之),使它们更简单且更有组织。
外观模式创建了一个更高层次的接口,以隐藏子系统内一组接口的复杂性。这种模式减少了复杂性耦合,最小化了子系统之间的通信和依赖。在下面的图中,你可以看到外观(Facade)的结构及其设计方式:

在前面的图中,我们可以看到外观模式封装了所有对子系统的调用,并隐藏了这些调用对客户端的可见性。系统有一个接口,即外观(Facade),客户端通过调用这个接口来调用子系统。因此,客户端不会直接调用子系统。使用这种解决方案,客户端不需要了解子系统及其复杂性。
这种模式通常用于具有高复杂性的项目和系统,需要将它们分解为子系统。
解释迭代器
想象一下,我们想要一种方法来按顺序访问聚合对象的元素,而不暴露其内部结构。迭代器模式正是这样做的。
迭代器模式负责按顺序访问聚合对象,并定义一个接口来访问元素,而不暴露其内部结构。这个接口不会在聚合对象上放置新元素,而只是读取元素到它。在下面的图中,你可以看到迭代器(Iterator)的结构及其设计方式:

在前面的图中,我们可以看到聚合和迭代器接口及其具体的子类。客户端是使用迭代器来访问聚合元素的那个类。
这种模式在 Java 集合如列表、双端队列和集合中使用。理解这种模式将有助于你理解 Java 集合。
解释代理
有时,创建一个新的对象可能是一个大过程,并且创建此对象可能涉及多个规则。想象一下,我们想要创建一个对象列表,这些对象代表电信设备,它们需要大量的计算来生成每个对象的信息。此外,这些对象不会同时被访问,而是按需访问。一个好的策略是在访问对象时创建每个对象,从而最小化创建所有对象所需的时间和成本,并且只访问一些。代理可以帮我们解决这个问题。
代理模式是一种代理对象实例(原始对象)到另一个对象实例(代理对象)的模式,允许对原始对象进行访问控制。在下面的图中,你可以看到 代理 的结构和它的设计方式:

从前面的图中,我们可以看到 代理 模式的结构。如果 Subject 是客户端用来访问对象操作的接口,那么 RealSubject 是原始对象的类,而 Proxy 是充当 代理 的类。然后,当客户端访问对象时,他们将访问 代理 对象,而 代理 对象将访问 RealSubject 对象,并将此对象返回给客户端。
这种模式用于实现 JPA 规范和 对象关系映射(ORM)的框架和 API 中。
解释企业模式
随着时间的推移,技术不断发展,新的工具不断涌现并帮助改变了一些领域。看到这些技术的潜力,组织越来越多地开始使用并投资这些工具来自动化他们的流程并优化他们的成本。这些工具随后开始被称为企业软件。
企业软件是一种在组织、公司或政府中广泛使用的软件类型,它提供了一种服务,以改善其流程并优化成本和效率。随着时间的推移,这种软件的复杂性不断增加,因为它们开始提供许多服务。随着不同服务对更多通信的需求,可扩展性变得越来越重要。因此,一些问题出现了。
企业模式是一套针对在企业软件中由于企业环境复杂性而产生的常见问题的解决方案。许多企业模式基于 GoF 模式,只是在实现方式上有所不同。在 Java EE 中,企业模式分为三组:表现层模式、业务层模式和集成层模式。这些模式作用于表现层、业务层和集成层,我们将在第二章“表现层模式”中详细讨论,该章节涵盖了表现层模式,第三章“业务层模式”,该章节涵盖了业务层模式,以及第四章“集成层模式”,该章节涵盖了集成层模式。
企业模式对于创建软件的专业人士来说非常重要,因为软件创建中的不良实践可能会增加项目涉及的成本和风险。由于企业软件的复杂性,错误可能会随着时间的推移和环境的变化而传播,使得企业环境难以持续。
定义设计模式与企业模式之间的区别
将设计模式与企业模式进行比较并非易事,因为某些行为是相似的。设计模式是首先出现的主题,这在 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著的《设计模式:可复用面向对象软件元素》一书中有所涉及。这些设计模式也是其他模式的基础。企业模式成为设计模式无法解决的问题。这是因为设计模式描述了类和对象之间关系的解决方案,但企业环境有其他需求,如系统之间的集成和软件关系。然而,企业模式使用一些设计模式来解决问题。
设计模式与企业模式之间的主要区别在于目标;设计模式旨在组织和优化面向对象设计,而企业模式则侧重于提高 Java EE 工具的使用效率以及改善 Java EE 组件之间的通信。设计模式关注面向对象和类与对象的关系,而企业模式关注 Java EE 组件之间的通信。
设计模式的使用使得算法的重用成为可能,同时也使得设计更加灵活;企业设计模式促进了 Java EE 工具复杂性的抽象,使得架构变更更加容易。
由于架构的复杂性,企业模式的最小使用往往比设计模式的最小使用更差。这是因为,在没有使用企业模式的情况下,专业人士将始终与 Java EE 的复杂性打交道,增加了出错的可能性。
一些 Java EE 模式的实现已经存在于 Java EE 工具中,这使得它们易于使用。在接下来的章节中,我们将描述这些模式及其使用 Java EE 工具的实现,并看看这些模式将如何有利于您项目的架构和设计。
摘要
在本章中,我们向您介绍了设计模式,解释了 GoF 设计模式和它们的目录。我们简要介绍了 Java 世界的基本设计模式,包括 Singleton、Abstract Factory、Facade、Iterator 和 Proxy。此外,我们还探讨了企业模式和它们与设计模式之间的区别。
在下一章中,我们将解释展示模式,包括它们的概念和实现。我们还将展示展示模式的概念以及它们如何帮助我们编写更好的软件。然后,我们将通过现实世界的问题展示展示模式的实现示例。
第二章:呈现模式
在本章中,我们将通过解释概念并展示实现示例来涵盖每个主题。阅读本章后,您将了解这些概念,并能够使用 Java EE 8 来实现它们。以下是即将在后续章节中涵盖的主题:
-
解释呈现层
-
解释拦截器过滤模式
-
使用 Java EE 8 实现拦截器过滤模式
-
解释前端控制器模式
-
实现前端控制器模式
-
解释应用控制器模式
-
实现应用控制器模式
解释呈现层
Java EE 平台是一个分布式多层应用模型,有三个广泛使用的通用层。这些层是呈现层(或 Web 层)、业务层和集成层(或 EIS 层)。
呈现层,也称为 Web 层,包含创建 Web 应用的组件。这一层有许多使用 HTTP 协议、构建用户视图和界面以及提供 Web 服务的组件。这些组件被称为 Web 组件。呈现层有两种类型:
-
面向呈现的层:这种类型的呈现层包含使用 HTML 和 XHTML 构建交互式网页和动态内容的组件。这些组件是 JavaServer Faces 技术、Java Servlet 技术和 JavaServer Page 技术,它们允许我们构建交互式网页。
-
面向服务的层:这一层包含构建 Web 服务端点的组件。这些组件是 JAX-RS 和 JAX-WS。
呈现层通常用于使用基于组件的规范,如JavaServer Faces技术,或者使用基于动作的 Java Servlet 技术和JavaServer Pages技术的 Web 应用。面向服务的层通常用于创建 REST API 或 Web 服务,这些服务由运行在移动平台或浏览器上的客户端消费。在下面的图中,我们可以看到呈现层是如何工作的:

如前图所示,客户端向服务器发送请求;呈现层处理请求并将其发送到业务层;如果请求不是异步的,业务层随后向呈现层发送响应,最后呈现层处理并发送响应到客户端。
由于呈现层负责促进与外部用户的 HTTP 通信和连接,因此这一层满足了 Web 组件之间的许多交互和通信。为了使这些工作良好,需要执行许多任务。这些任务包括验证客户端发送的数据、格式化它、将其发送到正确的业务组件或类,以及过滤数据和请求。
解释拦截过滤器模式
当客户端向服务器发送请求时,服务器有时会处理这个请求/响应以执行以下任务:
-
验证认证
-
生成日志
-
验证约束
-
验证客户端的浏览器
-
检查请求和响应之间的持续时间;计算响应时间
-
设置 cookie
然而,我们不想将这些任务放在处理主要请求的逻辑中。因此,创建一个预处理和/或后处理来执行这些任务是解耦主要逻辑和补充逻辑的好方法。
拦截过滤器模式是在我们想要插入不是主要逻辑一部分的逻辑但希望保持两种逻辑分离和松耦合时解决的问题的模式。将新逻辑与主要逻辑合并是一种不良实践,因为这些会变得耦合。此模式创建一个过滤器来预处理和后处理请求,允许创建一个逻辑块来解决不是主要问题的某些问题,从而解耦这两部分逻辑。使用此模式,我们可以创建一个可插入的解决方案,而无需修改主要逻辑。请查看以下图中拦截过滤器模式的模型:

在前面的图中,我们有客户端、FilterManager、FilterChain、FilterOne、FilterTwo、FilterThree和目标。客户端向服务器发送请求;FilterManager 创建一个按顺序排列的FilterChain并启动处理;FilterChain是有序的独立过滤器集合;FilterOne、FilterTwo和FilterThree是FilterChain中的过滤器,可以包括 N 个过滤器;目标是包含主要逻辑的资源。过滤器执行的顺序很重要,因为某些过滤器通常需要首先执行。过滤器优先级的一个例子是验证认证的任务,这通常需要首先执行,因为一些任务仅在客户端认证之后执行。
使用 Java EE 8 实现拦截过滤器模式
为了使用 Java EE 8 的最佳实践来实现此模式,我们将使用 Java Servlet 规范中的 servlet 过滤器。通过 servlet 过滤器,我们可以创建一个有序的请求拦截器来处理请求和响应。这些拦截器通过 URL 模式或 servlet 名称进行映射。servlet 过滤器可以通过 XML(在web.xml中)或注解进行配置。在我们的案例中,我们将假设我们想要创建一个记录所有发送到服务器的请求的日志。我们还将有两个过滤器——一个用于记录访问时间,另一个用于记录客户端使用的浏览器信息。为了记录访问时间,我们将创建一个名为LogAccessFilter的过滤器,为了记录浏览器信息,我们将创建一个名为LogBrowserFilter的过滤器。
实现 LogAccessFilter
这里,我们有LogAccessFilter的实现:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Date;
@WebFilter(filterName = "LogAccess", urlPatterns = "/*")
public class LogAccessFilter implements javax.servlet.Filter {
private static Logger logger = LogManager.getLogger(LogAccess.class);
public void destroy() {
}
public void doFilter(javax.servlet.ServletRequest req,
javax.servlet.ServletResponse resp, javax.servlet.FilterChain
chain) throws javax.servlet.ServletException, IOException {
//Gets the initial date of request.
Date dateInitRequest = new Date();
//Get IP of Client that sent a resquest.
String ip = ((HttpServletRequest)req).getRemoteAddr();
//Following to next filter. If none next filter exist, follows
//for main logic.
chain.doFilter(req, resp);
//Gets the end date of request.
Date dateEndRequest = new Date();
//Logging the informations of IP and access time.
logger.info("IP: "+ip +" Access time : "
+ Long.toString(dateEndRequest.getTime()
- dateInitRequest.getTime())
+ " ms");
}
public void init(javax.servlet.FilterConfig config) throws
javax.servlet.ServletException {
}
}
如代码所示,要创建一个 servlet 过滤器,我们需要创建一个继承自javax.servlet.Filter的类,并在类定义之前使用带有filterName和urlPatterns参数的@WebFilter注解,这些参数定义了过滤器名称和要过滤的 URL。以下是一个演示如何做到这一点的代码片段:
@WebFilter(filterName = "LogAccess", urlPatterns = "/*")
public class LogAccessFilter implements javax.servlet.Filter{
...
}
注意,servlet 过滤器使用责任链模式遍历过滤器(servlet 过滤器对象)。以下是一个使用责任链模式的代码片段:
chain.doFilter(req, resp);
在前面的代码行中,我们通过filterName参数将过滤器名称设置为LogAccess。这将过滤所有请求,因为urlPatterns参数的值是"/*"。如果我们根据 servlet 名称进行过滤,我们需要使用以下注解:
//Servlet1 and Servlet2 are the servlets to filter.
@WebFilter(filterName = "LogAccess", servletNames = {"servlet1","servlet2"})
doFilter方法负责预处理和后处理,并确定何时将请求传递给下一个过滤器或 servlet(主逻辑)。要传递请求到下一个过滤器或 servlet,需要执行以下代码:
//Following to next filter or servlet.
chain.doFilter(req, resp);
当前面的代码执行时,当前过滤器只有在其他过滤器和 servlet 完成其处理之后才会执行下一行。
实现 LogBrowserFilter
LogBrowserFilter的实现如下:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(filterName = "LogBrowser",urlPatterns = "/*")
public class LogBrowserFilter implements Filter {
private static Logger logger = LogManager.getLogger(LogBrowser.class);
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
//Get userAgent that contain browse informations.
String userAgent = ((HttpServletRequest)req).getHeader("User-Agent");
//Get IP of Client that sent a resquest.
String ip = ((HttpServletRequest)req).getRemoteAddr();
//Logging the informations of IP and Browser.
logger.info("IP: "+ip +" Browser info: "+userAgent);
//Following to the next filter. If none next filter exist, follow to main logic.
chain.doFilter(req, resp);
}
public void init(FilterConfig config) throws ServletException {
}
}
在前面的过滤器中,我们获取客户端 IP 和浏览器信息并将它们记录下来。LogBrowserFilter的操作与LogAccessFilter类似。
要定义过滤器执行的顺序,我们需要配置web.xml并添加过滤器映射信息。在这里,我们可以看到web.xml及其配置:
<web-app version="3.1"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<filter>
<filter-name>LogBrowser</filter-name>
<filter-class>com.rhuan.filter.LogBrowserFilter</filter-class>
</filter>
<filter>
<filter-name>LogAccess</filter-name>
<filter-class>com.rhuan.filter.LogAccessFilter</filter-class>
</filter>
</web-app>
在web.xml中定义的配置会覆盖注解配置。因此,如果我们把urlPattern配置放在web.xml中,那么考虑的配置就是web.xml的配置。我们不需要在web.xml上放置过滤器映射信息,因为这在代码中的注解配置上已经存在了。web.xml配置定义了顺序——LogBrowserFilter将首先被调用,然后是LogAccessFilter,最后是主逻辑(servlet)。
决定过滤器映射
定义映射方法是实现拦截过滤器模式的关键。这是因为不良的映射方法可能会直接影响项目并导致返工。我们有两种过滤器映射类型——UrlPattern和 servlet 名称。
当我们想要过滤 HTTP 请求到非特定资源或文件,但又想过滤各种未知资源时,会使用UrlPatterns。以下是一些示例:
-
*.jsp:这个过滤器会过滤所有对 JSP 页面的请求。如果服务器上添加了一个 JSP 页面,那么过滤器将过滤新的 JSP 页面,而无需进行任何修改。 -
/*: 这将过滤服务器上所有资源或文件的请求。如果服务器上添加了一个资源或文件,则过滤器将过滤此新资源或文件,而无需进行任何修改。 -
/user/*: 这将过滤以/user开头的 URI 的所有请求到服务器上的所有资源或文件。如果添加了一个以/user开头的 URI 访问的资源或文件到 servlet,则过滤器将过滤此新资源或文件,而无需进行任何修改。
用于映射过滤器的 servlet 名称表示当您想要过滤特定 servlet 时,独立于其 urlPattern。这种映射方式允许我们修改映射 servlet 的一个 urlPattern,而无需对过滤器进行任何修改。以下是一些示例:
-
{servlet1}: 这只映射名为servlet1的 servlet。如果修改servlet1的urlPatterns,则不需要修改过滤器。 -
{servlet1,servlet2}: 这将两个名为servlet1和servlet2的 servlet 进行映射。其行为类似于前面展示的示例,其中只映射了一个 servlet。
解释 FrontController 模式
在 Java EE 世界中,我们通常处理具有相似功能和流程的复杂项目。有时,使用各种控制器来处理请求是一种不良做法,因为它需要在多个端点进行配置,并产生大量创建和维护成本。因此,创建一个处理请求的中心点是一个非常好的解决方案,因为它创建了一个管理所有或一组请求的点,然后将此请求发送到正确的处理过程。然后我们可以处理所有对所有功能都通用的点,并将请求发送到处理特定于一个功能的问题的过程。一些配置,如会话配置、请求的最大大小限制、cookie 和 header,对所有请求都是通用的,并且可以从中央点进行配置。
FrontController 模式是一种创建中央管理器来处理应用程序的所有请求或请求组的模式,然后将请求发送到特定的一个处理过程,这通常是一个命令。由于今天我们有现成的解决方案,因此这种模式在普通项目中很少使用,实现这种模式通常是不必要的。此模式被 JSF、Spring MVC 和 struts 等框架使用。以下图表展示了此模式:

在前面的图表中,我们有 FrontController、AbstractCommand、Command1 和 Command2。FrontController 接收所有请求,处理请求的一些通用点,并将此请求发送到匹配的命令。AbstractCommand 是命令的 abstract 类。Command1 和 Command2 是命令的子类,它们实现了相应的逻辑。
在我们的案例中,我们将有两个页面——主页和登录页。如果用户在发送请求时已经登录,那么应用程序将启动登录页面,然后是主页。
实现前端控制器
在这里,我们有一个MyAppController的实现,它是一个处理应用程序所有请求的前端控制器:
import com.rhuan.action.Command.AbstractCommand;
import com.rhuan.action.Command.HomeCommand;
import com.rhuan.action.Command.LoginCommand;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "MyAppController", urlPatterns = "/myapp/*")
public class MyAppController extends HttpServlet {
private static Logger logger =
LogManager.getLogger(MyAppController.class);
private final String PAGE_ERROR = "/pageError.jsp";
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processRequest(request,response);
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processRequest(request,response);
}
protected void processRequest(HttpServletRequest
request, HttpServletResponse response)
throws ServletException, java.io.IOException {
String resultPage;
AbstractCommand command = null;
try {
//Create a correspondent Command.
if(request.getSession().getAttribute("USER") == null)
command = new LoginCommand();
else command = new HomeCommand();
//Execute the Command that return a page.
resultPage = command.execute();
} catch (Exception e) {
logger.error(e.getMessage());
resultPage = PAGE_ERROR;
}
//Dispatch to correspondent page.
getServletContext().getRequestDispatcher(resultPage)
.forward(request, response);
}
}
urlPattern is used to define which requests a context will send to our controller. Here's how we do this:
//Defining the urlPattern to Front Controller
@WebServlet(name = "MyAppController", urlPatterns = "/myapp/*")
public class MyAppController extends HttpServlet {
...
}
"/myapp/*") establishes that all requests to the myapp URI are sent to our controller. For example, http://ip:port/context/myapp/myfuncionality is sent to our controller.
当我们实现这个模式时,非常重要的是要注意 servlet 上的属性的使用,因为 servlet 上的所有类属性都与所有线程或所有请求共享。
所有GET请求或POST请求都发送到processRequest方法,该方法实现了将请求发送到相应命令并执行相应逻辑的逻辑。在设置正确的命令后,相应的命令被执行,页面被分发。在这里,我们有一条执行命令并将请求分发到正确页面的代码行:
//Execute a Command
resultPage = command.execute();
将请求分发到相应的页面:
//Dispatch to correspondent page.
getServletContext().getRequestDispatcher(resultPage)
.forward(request, response);
实现命令
在这里,我们有一个AbstractCommand,它是一个包含一个execute方法的abstract类。这是抽象命令,execute方法在子类中实现:
public abstract class AbstractCommand {
public abstract String execute();
}
HomeCommand, which is the implementation of AbstractCommand. The method execute() returns the path to the home page (/home.jsp):
public class HomeCommand extends AbstractCommand {
@Override
public String execute() {
return "/home.jsp";
}
}
在这里,我们有一个LoginCommand子类,它是AbstractCommand的实现。execute()方法返回登录页面的路径(/login.jsp):
public class LoginCommand extends AbstractCommand {
@Override
public String execute() {
return "/login.jsp";
}
}
应用程序控制器模式
一些 Web 应用在定义正确的视图、内容或要调用的操作时具有复杂的逻辑。可以使用 MVC 控制器来做出这个决定并获取正确的视图、内容或操作。然而,有时定义决策的逻辑非常困难,使用 MVC 控制器来做这件事可能会导致大量代码的重复。为了解决这个问题,我们需要将逻辑集中在一个点上,以便于维护和集中逻辑点。
应用程序控制器模式是一种允许集中所有视图逻辑并促进定义页面流程的唯一过程的模式。这种模式与前面讨论的FrontController一起使用,是FrontController和Command之间的中介。使用此模式,我们将促进视图处理和请求处理的解耦。以下图表表示了这一点:

在前面的图表中,我们可以看到位于FrontController和AbstractController之间的ApplicationController。当客户端发送请求时,FrontController接收这个请求并处理与请求相关的点。然后FrontController将这个请求发送到ApplicationController,它处理与视图和流程相关的点,并定义正确的Command来执行。
在我们的示例场景中,我们希望在服务器上创建一个下载文件的点,并且这个点只能由登录用户访问。此外,我们只接受 PDF 下载和 JPG 文件。在这个例子中,我们将创建一个名为 DownloadFrontController 的类来接收请求。我们还将创建一个名为 DownloadApplicationController 的类来处理视图和内容选择逻辑。AbstractCommand 是命令的 abstract 类。此外,我们还将创建 PdfCommand,它是 AbstractCommand 的一个实现,用于处理下载单个 PDF 文件的逻辑。最后,我们将创建 JpgCommand,它是 AbstractCommand 的一个实现,用于处理下载单个 JPG 文件的逻辑。
实现 DownloadFrontController
这里,我们实现了 DownloadFrontController,这是一个用于下载文件的 Servlet:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "DownloadFrontController", urlPatterns = "/download/*")
public class DownloadFrontController extends HttpServlet {
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processRequest(request,response);
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processRequest(request,response);
}
protected void processRequest(HttpServletRequest
request, HttpServletResponse
response)
throws ServletException, java.io.IOException {
//If user is logged the request is sent to
ApplicationController,
// then one error 401 is sent to client.
if(Objects.nonNull(request.getSession().getAttribute("USER"))) {
//Send the request to ApplicationController
new DownloadApplicationController(request,
response).process();
}
else {
response.sendError(401);
}
}
}
在前面的代码块中,我们有 DownloadFrontController 类,其中包含处理请求的逻辑。这个类是一个 servlet,它使用以下代码行响应对 /download/* 发送的所有请求:
@WebServlet(name = "DownloadFrontController", urlPatterns = "/download/*")
所有 GET 请求或 POST 请求都发送到 processRequest 方法,其中包含将请求发送到 DownloadApplicationController 的代码。以下代码行正是这样做的:
//Send the request to ApplicationController
new DownloadApplicationController(request, response).process();
实现 DownloadApplicationController
这里,我们实现了 DownloadApplicationController,它负责决定发送请求的正确命令。决定正确命令的过程可以通过多种方式执行,包括反射和注解、使用 switch 案例和映射等。在我们的例子中,我们使用映射来帮助我们:
import com.rhuan.action.Command.AbstractCommand;
import com.rhuan.action.Command.PdfCommand;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class DownloadApplicationController {
private static Logger logger = LogManager.getLogger(DownloadApplicationController.class);
private final String PAGE_ERROR = "/pageError.jsp";
private HttpServletRequest request;
private HttpServletResponse response;
private Map<String, Class> map;
private String key;
public DownloadApplicationController(HttpServletRequest
request, HttpServletResponse
response){
//On our example, only PDF and JPG is acepted to download.
this.map = new HashMap<String, Class>();
this.map.put("PDF", PdfCommand.class);
this.map.put("JPG", PdfCommand.class);
this.request = request;
this.response = response;
}
public void process(){
//Processes the URI and creates the key using URI.
this.processUri();
//Validates if the request is valid.
if (!validate()) {
try {
response.sendError(400);
} catch (IOException e1) {
logger.error(e1.getMessage());
}
return;
}
//Get the correspondent command.
Class commandClass = map.get(key);
boolean error = false;
try {
AbstractCommand command = (AbstractCommand)
commandClass.newInstance();
//Executes the command.
command.execute(request,response);
} catch (InstantiationException e) {
logger.error(e.getMessage());
error = true;
} catch (IllegalAccessException e) {
logger.error(e.getMessage());
error = true;
} catch (ServletException e) {
logger.error(e.getMessage());
error = true;
} catch (IOException e) {
logger.error(e.getMessage());
error = true;
}
//If an error ocorred, response 500.
if(error){
try {
response.sendError(500);
} catch (IOException e1) {
logger.error(e1.getMessage());
return;
}
}
}
private void processUri(){
String uri = request.getRequestURI();
if(uri.startsWith("/")) uri = uri.replaceFirst("/", "");
String[] uriSplitted = uri.split("/");
if(uriSplitted.length > 2)
key = uriSplitted[2].toUpperCase();
}
private boolean validate(){
String uri = request.getRequestURI();
if(uri.startsWith("/")) uri = uri.replaceFirst("/", "");
String[] uriSplitted = uri.split("/");
return uriSplitted.length == 3 && map.containsKey(key);
}
}
key, creates a new instance of the command, and executes the following command:
//Get the correspondent command.
Class commandClass = map.get(key);
然后使用 newInstance() 方法创建命令的新实例:
//Instantiate the command
AbstractCommand command = (AbstractCommand) commandClass.newInstance();
然后执行命令,传递 request 和 response 作为参数:
//Executes the command.
command.execute(request,response);
实现 commands
这里,我们有 abstract 类 AbstractCommand,它包括抽象的 execute 方法。所有此命令的实现都扩展了 AbstractCommand,它是一个 abstract 类:
package com.rhuan.action.Command;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public abstract class AbstractCommand {
public abstract void execute(HttpServletRequest
request, HttpServletResponse response)
throws ServletException, java.io.IOException ;
}
在以下代码块中,我们有 PdfCommand 类。这是一个 AbstractCommand 的子类,实现了下载 PDF 文件的逻辑:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
public class PdfCommand extends AbstractCommand {
@Override
public void execute(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
String fileName = request.getParameter("fileName");
// for example application/pdf, text/plain, text/html,
image/jpg
response.setContentType("application/pdf");
// Make sure to show the download dialog
response.setHeader("Content-disposition","attachment;
filename=myapp_download.pdf");
// Assume file name is retrieved from database
// For example D:\\file\\test.pdf
File file = new File(fileName);
// This should send the file to browser
OutputStream out = response.getOutputStream();
FileInputStream in = new FileInputStream(file);
byte[] buffer = new byte[4096];
int length;
while ((length = in.read(buffer)) > 0){
out.write(buffer, 0, length);
}
in.close();
out.flush();
}
}
在前面的代码块中,我们有 execute() 方法,该方法处理下载 PDF 文件的逻辑。在此阶段,所有请求的处理和主要验证都已经执行,execute() 方法只需要执行下载过程。
这里,我们有 JpgCommand 类,它是一个 AbstractCommand 的子类,实现了下载 JPG 文件的逻辑:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
public class JpgCommand extends AbstractCommand {
@Override
public void execute(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
//Gets the file name sent by paramenter.
String fileName = request.getParameter("fileName");
//Configures the content type.
response.setContentType("image/jpg");
// Configure the dialog to download.
response.setHeader("Content-disposition","attachment;
filename=myapp_download.pdf");
//Read file and send to client.
File file = new File(fileName);
OutputStream out = response.getOutputStream();
FileInputStream in = new FileInputStream(file);
byte[] buffer = new byte[4096];
int length;
while ((length = in.read(buffer)) > 0){
out.write(buffer, 0, length);
}
in.close();
out.flush();
}
}
在前面的代码块中,我们有 execute() 方法,该方法处理下载 JPG 文件的逻辑。在此阶段,所有请求和主要验证过程都已经完成。
应用控制器和前端控制器模式之间的区别
应用控制器模式和前端控制器模式非常相似,可能会让一些用户感到困惑。应用控制器模式和前端控制器模式解决的问题相似,因为两者都用于在一点集中逻辑。此外,两者都使用设计模式命令协同工作。
应用控制器和前端控制器之间的主要区别在于,应用控制器旨在解决视图和页面流程逻辑的复杂性,而前端控制器旨在解决请求及其配置的复杂性。当视图和页面流程逻辑简单时,所有逻辑有时会插入到前端控制器中,而应用控制器则不再使用。然而,当视图和页面流程中的逻辑复杂时,建议使用应用控制器,以便解耦视图和页面流程逻辑并组织代码。
摘要
在本章中,我们学习了表示层,并探讨了拦截过滤器模式、前端控制器模式和应用程序控制器模式,以及如何实现它们。在现实世界中,我们很少需要实现这些模式,因为一些框架、API 和规范已经为我们实现了它们。然而,了解这些模式对于提高我们对实现这些模式的框架、API 和规范的理解非常重要。此外,我们有时需要实现框架之外的某些组件。因此,使用这种模式是个好主意。
下一章将讨论业务层及其模式。在这里,我们将涵盖作用于业务层的模式。了解这些模式将补充我们对企业模式的概念和实现以及这些模式如何促进在业务环境中使用良好工具来解决常见问题的知识。
第三章:业务模式
在本章中,我们将涵盖业务代理模式、会话外观模式和业务对象模式的定义。我们将展示使用这些设计模式的原因、每种模式的常见方法、它们与其他模式的交互、它们的演变以及在现实世界中的行为。我们还将展示这些模式实现的一些示例。
在本章结束时,你将能够识别正确的应用业务模式场景并选择实现它们的最佳方法。以下主题将被涵盖:
-
理解业务层
-
解释业务代理模式
-
解释会话外观模式
-
实现会话外观模式
-
解释业务对象模式
-
实现业务对象模式
理解业务层
在从 JEE8 及其技术的角度讨论业务模式和这些模式的使用之前,我们必须确定应用程序的业务逻辑将在 JEE 框架中的哪个位置。正如我们已经看到的,JEE 架构基本上有三个层次。大多数 JEE 技术,如企业 JavaBeans (EJB)和Java 持久化 API (JPA),都与业务层相关。EJB 容器位于业务层,但还有一些其他技术在整个 JEE 框架中导航,如 CDI 和 Bean Validation。然而,最重要的是要知道,核心业务逻辑应用程序是在业务层执行的。
我们将在业务层看到三个重要的模式。我们将简要解释每个模式的定义和目标:
-
业务代理模式:它是业务服务的代理,隐藏了服务查找和远程调用的过程。
-
会话外观模式:封装业务规则并向客户端暴露粗粒度服务。
-
业务对象模式:这些是具有属性和方法的真实世界对象,适用于具有高度复杂性的应用程序,有助于将业务逻辑从应用程序的其他部分分离出来,促进业务逻辑与其他应用程序部分的解耦。
让我们看看以下图示:

我们将在本章后面看到,尽管业务代理是业务层组件,但其物理位置位于 Web 层。
解释业务代理模式
为了解释业务代理模式,我们需要理解一些表明该模式真实目标和证据的要点。因此,我们将展示这些要点并详细解释业务代理模式。
客户端层、表示层和业务层
在继续之前,这里是对层次和层概念的一个简要说明。
层次
层仅是应用架构中具有职责的逻辑划分。这是一种组织应用程序代码的逻辑方式。马丁·福勒的书籍《企业应用架构模式》描述了三个主要层及其职责:
| 层 | 职责 |
|---|---|
| 表示 | 用户交互、输入字段验证、显示数据格式化 |
| 业务 | 应用逻辑 |
| 数据 | 数据库通信、消息系统通信 |
因此,应用程序的类根据它们的职责在逻辑上被分离。有些类用于数据访问层,而其他类则准备数据以作为表示层的一部分进行显示。这种划分完全是逻辑上的。良好的架构实践是拥有一个分层链,其中一层与其相邻层交互,提供和消费服务。这导致了更高的内聚性(相同的职责包含在同一层中)和低耦合性。
让我们看看以下图表:

我们可以看到,JSF、JSP 和 HTML 页面、后端 bean 类,甚至 servlet 在逻辑上属于表示层组,因为它们具有相同的基本职责,即向用户发送信息并接收用户的请求。EJB、servlet(其中一部分)和业务对象属于业务层。DAO 类和 JPA 实体属于数据层。
级别
级别是一个物理单元,它与硬件和软件组件相关。它是层组件部署和执行的基础设施。级别的例子包括网络浏览器、应用服务器和数据库服务器。一个典型的n层应用程序由以下级别定义:
| 级别 | 基础设施 |
|---|---|
| 客户端 | 网络浏览器、移动设备 |
| 表示 | Web 服务器(容器)、HTTP 协议 |
| 业务 | 应用服务器(如 Java EE 服务器) |
| 数据/集成 | 数据库服务器、消息服务、Web 服务 |
让我们看看以下图表:

在这里,非常重要的一点是要注意客户端级别和表示级别之间的区别。客户端级别是客户端应用程序被执行的地方(通过浏览器或移动应用程序等平台)。通常,客户端级别是客户端计算机或设备,而表示级别由 Web 服务器表示。表示层从客户端层接收数据请求,准备数据(如果需要,使用先前定义的某些格式),并将其发送到业务层。这是在 JEE 场景中处理数据的经典机制。我们可以在表示层中识别一些技术,如 Servlets、JSP、JSF、WebSockets、JAX-RS 和 JAX-WS、Java API 用于 JSON 处理、JSON-B、CDI 和 bean 验证。
让我们看看下面的图:

如前所述,所有业务逻辑都在业务层执行。表示层是业务层的客户端,因为它需要业务层的操作并接收来自业务层的结果。在此阶段,我们可以看到表示层的一个额外责任,即定位服务并发出请求。如果有一个将请求委托给真实服务的机制将会很有趣。这正是业务代表模式的作用,它防止业务层服务的细节暴露给表示层。因此,表示层和业务层之间的耦合减少,并且因此业务层的修改对表示层的影响最小。
业务代表模式充当客户端的输入门。它负责接收请求,识别或定位真实的企业服务,并调用发送请求的服务。之后,代表接收服务响应,然后将响应发送回客户端。
经典的业务代表模式场景
在经典的业务代表模式场景中,业务代表模式的实现从 Java 客户端接收请求,并将响应发送回客户端。此外,为了最小化表示层和业务层之间的耦合,代表负责定位远程服务(在大多数情况下,是远程 EJB 服务)并为访问业务服务提供缓存机制以减少网络流量。
因此,当过去使用 EJB 作为远程服务时,业务代表模式与另一个模式(服务定位器模式)一起使用,该模式负责定位远程(和本地)EJB。此外,远程 EJB 的存根(基于 RMI(远程方法调用)协议的一种 EJB 引用)被代表缓存。
下面的图展示了业务代表模式的类图。这代表了该模式的基本结构。客户端向业务代表发送请求,而业务代表则访问正确的企业服务。在远程服务查找的情况下,业务代表可以使用服务定位器:

当业务代表将业务请求重新传递给业务服务时,在代码开发中的一种自然方法是让两个类(业务代表和业务服务)实现相同的企业接口。
这在下面的图中展示:

在下面的图中,我们展示了业务代表模式的序列图:

业务代表模式的好处
根据旧的 J2EE 架构,商业代表者的好处包括:
-
隐藏底层业务服务的细节。在远程服务的情况下,使用商业代表者使得命名和查找服务对表示层来说是透明的。
-
处理业务服务异常。商业代表者可以捕获具有技术意义的服务异常,并将它们转换为更友好的异常,向客户端生成应用程序异常级别。例如,商业代表者可以将业务服务产生的最终远程异常转换为特定的应用程序异常。
-
商业代表者可以透明地执行失败的服务执行的重新尝试,并从客户端隐藏问题。
-
此外,商业代表者可以缓存远程业务服务的引用以提高性能。调用远程服务是一个昂贵的操作,重复远程服务调用会大大增加网络流量。
然而,随着新场景的出现,分布式应用程序开发的世界也在不断发展。JEE 架构也随之变化。随着现代移动应用程序和 Web 客户端的发展,新的客户端应用程序,带有丰富的 JavaScript 框架,正在涌现。因此,商业代表者被视为表示层(其中驻有 Servlet、JSP 和 JSF 机制等技术)和业务层(其中驻有 EJB 等技术)之间的桥梁或门户。
商业代表者 – 已过时还是未过时
在旧的 J2EE 架构中,远程和本地业务服务,如 EJB,使用服务定位器机制。然而,现在使用依赖注入来访问本地 EJB(并且本地服务的选项越来越被使用)。在许多情况下,由于这个原因,使用商业代表者查找本地服务已经变得有些过时。因此,有人可能会质疑仅为了处理远程通信而使用模式,如商业代表者。这是因为,自从 JEE5 以来,我们已经开始使用 DI 注解轻松引用本地 EJB。然而,如果我们把商业代表者视为 Session Bean EJB 的桥梁,例如,那么我们可以在必要时更改这些 EJB,而无需担心表示层是否会损坏。如果会话 EJB 发生变化,那么商业代表者的职责就是处理这种变化,并保持表示层的完整性。
下面的图显示了应用程序的经典架构:

在某些情况下,这种架构被其他架构所取代,如下面的图所示:


观察前两种替代方案,我们可以看到在需要更改业务服务层时,如何使用业务代表(Business Delegate)。这可以在不影响表示层的情况下完成。此外,当我们需要处理业务服务异常并且有一个非网页浏览器的客户端时,我们可以使用带有查找机制(JNDI)的业务服务(EJBs)的业务代表。
在应用构建中可以使用几种其他架构。我们将看到业务代表模式的使用与一些其他模式一起发生,特别是在图示中显示的会话外观模式。另一个常见的模式是业务对象模式,它使用属性和方法表示现实世界的业务对象,不一定包括 getter 和 setter 方法。
解释会话外观模式
在我们介绍会话外观模式之前,重要的是要介绍外观模式,这些模式是《设计模式:可复用面向对象软件的基础》(Gang of Four,GoF)一书中提到的结构型设计模式之一。
主要目标是封装业务逻辑的复杂性到一个业务接口中。从广义上讲,这个接口仅向客户端暴露少量粗粒度方法。这些接口方法中的每一个都负责控制底层业务逻辑的复杂性。通过这种方式,更细粒度的内部服务可以被组合成接口方法暴露的一组服务。
使用外观模式的优点如下:
-
它为可用服务提供粗粒度方法。
-
它减少了远程调用。远程客户端不需要调用许多细粒度业务对象。相反,它执行对暴露接口方法的远程调用,该方法负责对细粒度对象进行本地调用。
-
它可以创建一个到遗留后端系统的单一通道。
-
它减少了客户端和细粒度对象之间的耦合。
例如,假设有一个城市酒店空位检查的系统。酒店提供关于空位的咨询服务。一个想要知道有多少空位可用的客户端应用程序必须对每个网络服务进行调用。但如果我们调用外观层,这个外观可以负责搜索网络服务。除了减少调用外,外观还消除了客户端和网络服务之间可能存在的高度耦合。
通过理解 GoF 外观模式作为解决方案的问题,我们可以看到 JEE 中存在一个类似的问题。在这种情况下,服务器端组件被实现为 业务对象(BOs)或 POJOs。几乎每个来自客户端的请求都需要与 BO 交互,并且每个参与请求过程的 BO 可能与其他 BO 有关系。此外,BO 可能使用 DAO 模式访问了集成层。我们不希望向客户端暴露业务组件及其内部关系的复杂性——尤其是远程客户端。因此,会话外观模式作为解决这个问题的方案。
会话外观的好处
重要的是不要让客户端接触到使用这些细粒度 BOs 的复杂性。频繁访问大量细粒度组件会大大增加 BOs 控制的复杂性。事务控制、安全管理和服务查找都是这种复杂性的例子。
类似于 GoF 外观模式,在 JEE 中使用粗粒度层减少了客户端与由细粒度 BOs(业务对象)表示的业务组件之间的耦合(我们可以将会话外观视为 JEE 中 GoF 外观模式的扩展)。会话外观模式代表了这一粗粒度层。使用会话外观模式构建的架构为客户端提供了更通用的(或粗粒度)方法的外观。使用会话外观模式的两个最大好处如下:
-
它不会暴露业务对象(BOs)及其关系的复杂性。
-
它减少了网络流量。这是因为远程调用仅限于会话外观暴露的粗粒度方法,而不是细粒度业务对象。
当然,与当前架构相比,在旧的 JEE 场景中,EJB 远程调用使用得更多。这一点需要考虑。
在 JEE 中实现会话外观模式
在 JEE 架构中,会话外观是由无状态或有状态的 EJB 实现的。EJB 对象可以使用或组合其他 POJOs、业务对象和 EJBs。在这个阶段,我们必须小心不要积累过多的不必要的层,因为我们面临着一个 EJB 链的风险,其中一个 EJB 调用另一个更内部的 EJB,依此类推。服务必须得到良好的映射和设计。
由于会话外观主要是由 EJBs 实现的,因此事务控制和安全管理等服务自然地适用于这项技术。通常在这个层面,我们会处理大多数内部对象的事务控制,例如 POJOs,它们代表了 JPA 技术中的实体。对于 EJB 来说,JPA 实体事务控制是原生的,这意味着它是由 JEE 容器提供的。这为开发过程提供了大幅的生产力提升。
经典的会话外观模式场景
会话外观模式可以在几种架构中使用。以下图表显示了会话外观使用的经典模型:

观察前面的图表,我们可以看到客户端(通常是 Web 组件或业务代表实现)访问外观层。在这个架构中,我们发现了一些描述会话外观使用的选项。
外观可以处理不同的业务对象(BO)。在本章的后面部分,我们将看到对 BO 和业务对象模式的更好描述。业务对象是概念模型的表示,这是一个现实世界中的对象。BO 可能有描述其行为的方法。在这种情况下,我们将说这个 BO 反映了一个非贫血模型(贫血领域对象包含一些业务方法,如验证和计算)。在这里,BO 可以使用数据访问对象(DAO)模式作为执行 CRUD 操作的策略。
外观可以直接访问 POJO JPA(Java 持久化 API)实体。如果业务对象的概念模型与数据模型非常接近,我们可以完全表示这个业务对象(应用用例的一个参与者)作为一个持久化实体。大多数情况下,会话外观被实现为一个 EJB 会话。尽管 JPA 实体不需要 EJB 容器来运行,因为它可以在 JSE 和 JEE 环境中运行,但 EJB 和 JPA 技术组合非常成功。自从 JEE 5.0 平台以来,JPA 已经成为对象关系映射(OR 映射)和持久化管理默认规范。JPA 1.0 是 JSR 220 规范(EJB 3.0)的一部分。EJB 3.0 规范的最终结果是产生了三个独立的文档,第三个是 Java 持久化 API。它描述了 JSE 和 JEE 环境的持久化模型。EJB 技术的实现自然提供了更多内部服务,如事务和安全控制:
- 在大多数应用中,会话外观使用 DAO 实现来与持久化层执行 CRUD 操作。我们稍后会看到 DAO 模式封装了与 CRUD 相关的细节,可以直接使用 JDBC 实现或 JPA 实现来执行 CRUD 工作。
以下是与会话外观模式相关的组件层活动图:

以下显示了会话外观模式的序列图:

实现会话外观模式
让我们创建一个与学术世界相关的小型应用程序。我们将创建两个门面——一个门面来管理应用程序的财务部分,另一个来管理应用程序的学术部分。我们还将构建一些其他类,例如 DAO 类和领域模型类。没有数据库;所有数据都通过 DAO 类保存在内存中。因此,用于查找信息的方法被构建到 DAO 类中。让我们创建以下领域模型类:Discipline、Course、Member(Member是一个抽象类,代表学院的成员)、Professor和Student:
import java.io.Serializable;
public class Discipline implements Serializable{
private String name;
private String code;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((code == null) ? 0 : code.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Discipline other = (Discipline) obj;
if (code == null) {
if (other.code != null)
return false;
} else if (!code.equals(other.code))
return false;
return true;
}
public Discipline() {
}
public Discipline(String code, String name) {
this.setCode(code);
this.setName(name);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
这是Course类:
import java.io.Serializable;
public class Course implements Serializable {
private String code;
private String name;
public Course() {
}
public Course (String code, String name) {
this.setCode(code);
this.setName(name);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这里是像之前的类一样实现Serializable包的Member类:
import java.io.Serializable;
import java.time.LocalDate;
public abstract class Member implements Serializable {
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Member other = (Member) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
private String name;
private LocalDate initDate;
private String email;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDate getInitDate() {
return initDate;
}
public void setInitDate(LocalDate initDate) {
this.initDate = initDate;
}
}
现在,Professor类继承自Member类:
import java.util.Date;
import java.time.LocalDate;
public class Professor extends Member {
private LocalDate initTeachDate;
public Professor() {
}
public Professor(String name, LocalDate initDate) {
this.setName(name);
this.setInitDate(initDate);
}
public Professor(String name) {
this.setName(name);
}
public LocalDate getInitTeachDate() {
return initTeachDate;
}
public void setInitTeachDate(LocalDate initTeachDate) {
this.initTeachDate = initTeachDate;
}
}
以下是继承自Member类的Student类:
public class Student extends Member {
private String enrollment;
public Student() {
}
public Student(String enrollment) {
this.setEnrollment(enrollment);
}
public Student(String enrollment, String name) {
this.setEnrollment(enrollment);
this.setName(name);
}
public String getEnrollment() {
return enrollment;
}
public void setEnrollment(String enrollment) {
this.enrollment = enrollment;
}
}
我们可以用一个表示唯一实体的id整数类型属性来创建这些应用程序实体。通常,会扩展一个包含此 ID 的抽象实体类。然而,对于学院成员,我们简化了它,并使用name属性来执行标识任务。在Discipline和Member类中,我们实现了 equals 方法来检查集合内的相等对象。
让我们创建一些 DAO 类。在这些示例中没有 POJO JPA 实体。模型对象之间的关系被插入到 DAO 类中:
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CourseDAO {
private static Map<Course, List<Discipline>> courseXDisciplines;
static {
Discipline d1 = new Discipline("D1", "discipline 1");
Discipline d2 = new Discipline("D2", "discipline 2");
Discipline d3 = new Discipline("D3", "discipline 3");
Discipline d4 = new Discipline("D4", "discipline 4");
courseXDisciplines = new HashMap<>();
courseXDisciplines.put (new Course ("C1", "Course 1"), Arrays.asList (d1, d2, d4));
courseXDisciplines.put (new Course ("C2", "Course 2"), Arrays.asList (d1, d3));
courseXDisciplines.put (new Course ("C3", "Course 3"), Arrays.asList (d2, d3, d4));
}
public List<Discipline> getDisciplinesByCourse(Course course) {
return courseXDisciplines.get(course);
}
}
这是DisciplineDAO类:
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class DisciplineDAO {
private static Map<Discipline, List<Discipline>> disciplineXPreReqDisciplines = new HashMap<>();
private static Map<Professor, List<Discipline>> professorXDisciplines = new HashMap<>();
private static Map<Discipline, List<String>> disciplineXBooks = new HashMap<>();
private static List<Discipline> disciplines;
static {
Discipline d1 = new Discipline("D1", "discipline 1");
Discipline d2 = new Discipline("D2", "discipline 2");
Discipline d3 = new Discipline("D3", "discipline 3");
Discipline d4 = new Discipline("D4", "discipline 4");
disciplines = Arrays.asList(d1, d2, d3, d4);
disciplineXPreReqDisciplines.put (d3, Arrays.asList (d1, d2));
disciplineXPreReqDisciplines.put (d4, Arrays.asList (d2));
professorXDisciplines.put (new Professor ("professor a"), Arrays.asList (d1, d2));
professorXDisciplines.put (new Professor ("professor b"), Arrays.asList (d3));
professorXDisciplines.put (new Professor ("professor cv"), Arrays.asList (d1, d3, d4));
disciplineXBooks.put (d1, Arrays.asList ("book x", "book y"));
disciplineXBooks.put (d2, Arrays.asList ("book x", "book a", "book w"));
disciplineXBooks.put (d3, Arrays.asList ("book x", "book b"));
disciplineXBooks.put (d4, Arrays.asList ("book z"));
}
public List<Discipline> getPreRequisiteDisciplines (Discipline discipline) {
return disciplineXPreReqDisciplines.get (discipline);
}
public List<Discipline> getDisciplinesByProfessor(Professor professor) {
return professorXDisciplines.get (professor);
}
public List<String> getBooksByDiscipline(Discipline discipline) {
return disciplineXBooks.get (discipline);
}
public List<Professor> getProfessorByDiscipline (Discipline discipline) {
return professorXDisciplines.keySet()
.stream()
.filter (p->professorXDisciplines.get(p).contains(discipline))
//.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
.collect(Collectors.toList());
}
public Discipline getDisciplineByCode (String code) {
return disciplines
.stream()
.filter(s->s.getCode().equals(code))
.findAny()
.get();
}
}
现在,你将创建StudentDAO类:
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StudentDAO {
public static enum FINANCIAL_STATUS {
OK (true, "OK"), PENDING (false, "Payment pending"), DOC_PENDING (true, "Document pending");
private boolean status;
private String description;
public boolean isStatus() {
return status;
}
public String getDescription() {
return description;
}
FINANCIAL_STATUS (boolean status, String description){
this.status = status;
this.description = description;
}
}
public static enum ACADEMIC_STATUS {
APPROVED , FAILED;
}
private static List<Student> students;
private static Map<String, FINANCIAL_STATUS> studentStatusPayment = new HashMap<>();
private static Map<Student, List<String>> studentXCourseName = new HashMap<>();
static {
Student s1 = new Student ("20010001", "student 1");
Student s2 = new Student ("20010002", "student 2");
Student s3 = new Student ("20010003", "student 3");
Student s4 = new Student ("20010004", "student 4");
students = Arrays.asList(s1, s2, s3, s4);
studentStatusPayment.put ("20010001", FINANCIAL_STATUS.OK);
studentStatusPayment.put ("20010002", FINANCIAL_STATUS.OK);
studentStatusPayment.put ("20010003", FINANCIAL_STATUS.PENDING);
studentStatusPayment.put ("20010004", FINANCIAL_STATUS.OK);
studentXCourseName.put (s1, Arrays.asList ("C01", "C02"));
studentXCourseName.put (s2, Arrays.asList ("C03"));
studentXCourseName.put (s3, Arrays.asList ("C04"));
studentXCourseName.put (s4, Arrays.asList ("C03", "C04"));
}
public static Map<String, FINANCIAL_STATUS> getStudentStatusPayment() {
return studentStatusPayment;
}
public List<Student> getEnrolledStudentByCourse(Course course) {
return studentXCourseName.keySet()
.stream()
.filter(s->studentXCourseName.get(s).contains(course.getCode()))
.collect(Collectors.toList());
}
public Student getStudentByEnrollment (String enrollment) {
return students
.stream()
.filter(s->s.getEnrollment().equals(enrollment))
.findAny()
.get();
}
}
让我们看看ProfessorDAO类:
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class ProfessorDAO {
private static Set<Professor> professors;
static {
Professor p1 = new Professor ("professor a", LocalDate.of (2001, 03, 22)),
p2 = new Professor ("professor b", LocalDate.of (1994, 07, 05)),
p3 = new Professor ("professor c", LocalDate.of (1985, 10, 12)),
p4 = new Professor ("professor cv", LocalDate.of (2005, 07, 17));
professors = Arrays
.stream (new Professor[]{p1, p2, p3, p4})
.collect (Collectors.toSet());
}
public Professor findByName (String name) {
return professors
.stream()
.filter(p->p.getName().equals(name))
.findAny()
.get();
}
}
为了简化,我们在DisciplineDAO中赋予了它很多责任。我们本可以增加CourseDAO或ProfessorDAO类的访问范围,以便访问与Professor实体相关的数据。
现在,以下类是两个会话门面实现:AcademicFacadeImpl和FinancialFacadeImpl。重要的是要注意,这仅仅是构建此类应用程序的几种方法之一。本章的下一部分将介绍业务对象模式,在这里我们将创建一个业务对象,它集中了应用程序的业务规则,而不是会话门面:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Set;
import javax.ejb.Asynchronous;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
/**
* Session Bean implementation class AcademicFacadeImpl
*/
@Stateless
@LocalBean
public class AcademicFacadeImpl {
@Inject
private CourseDAO courseDAO;
@Inject
private DisciplineDAO disciplineDAO;
@Inject
private StudentDAO studentDAO;
@Inject
private ProfessorDAO professorDAO;
public List<Discipline> getDisciplinesByCourse(Course course) {
return courseDAO.getDisciplinesByCourse (course);
}
public List<Discipline> getPreRequisiteDisciplines (Discipline discipline) {
return disciplineDAO.getPreRequisiteDisciplines(discipline);
}
public List<Discipline> getDisciplinesByProfessor(Professor professor) {
return disciplineDAO.getDisciplinesByProfessor(professor);
}
public List<String> getBooksByDiscipline(Discipline discipline) {
return disciplineDAO.getBooksByDiscipline(discipline);
}
public List<Student> getEnrolledStudentByCourse(Course course) {
return studentDAO.getEnrolledStudentByCourse (course);
}
public void requestTestReview (Student student, Discipline discipline, LocalDate testDate) {
// TODO
}
private LocalDateTime scheduleTestReview (TestRevisionTO testRevisionTO)
{
LocalDateTime dateTime = null;
try {
Thread.sleep(10000);
// get some code to calculate the schedule date for the test review
Thread.sleep (5000); // simulate some delay during calculation
dateTime = LocalDateTime.now().plusDays(10);
if (dateTime.getDayOfWeek().equals(DayOfWeek.SUNDAY)) {
dateTime = dateTime.plusDays(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return dateTime;
}
private void sendEmail (TestRevisionTO testRevisionTO, LocalDateTime dateTime) {
Student student = studentDAO.getStudentByEnrollment (testRevisionTO.getEnrollment());
String enrollment = student.getEnrollment();
String studentName = student.getName();
String email = student.getEmail();
Discipline discipline = disciplineDAO.getDisciplineByCode (testRevisionTO.getDisciplineCode());
String disciplineName = discipline.getName();
String disciplineCode = discipline.getCode(); // testRevisionTO.getDisciplineCode()
String date = dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE);
String time = dateTime.format(DateTimeFormatter.ofPattern("HH:mm"));
// sending an email using the above information ...
System.out.println("sending an email to : " + studentName + " ...");
}
public void requestTestReview (@ObservesAsync TestRevisionTO testRevisionTO) {
System.out.println("matricula " + testRevisionTO.getEnrollment());
LocalDateTime dateTime = scheduleTestReview (testRevisionTO);
sendEmail (testRevisionTO, dateTime); // send an email with the schedule date for the test review:
}
public List<Professor> getProfessorsByDiscipline(Discipline discipline) {
return disciplineDAO.getProfessorByDiscipline(discipline);
}
public boolean canProfessorTeachDiscipline (Professor professor, Discipline discipline) {
return disciplineDAO.getDisciplinesByProfessor(professor) .contains(discipline);
}
}
现在,让我们看看FinancialFacadeImpl类:
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.inject.Inject;
/**
* Session Bean implementation class FinancialFacadeImpl
*/
@Stateless
@LocalBean
public class FinancialFacadeImpl {
@Inject
private StudentDAO studentDAO;
public FinancialFacadeImpl() {
}
public boolean canStudentEnroll (Student student) {
return studentDAO.getStudentStatusPayment().get (student.getEnrollment()).isStatus();
}
public boolean isStudentPending (Student student) {
FINANCIAL_STATUS status = studentDAO.getStudentStatusPayment().get (student.getEnrollment());
return (status.equals (FINANCIAL_STATUS.PENDING)) || (status.equals (FINANCIAL_STATUS.DOC_PENDING));
}
}
我们可以在 EJB 会话门面中观察到@LocalBean注解。这意味着该 bean 有一个无接口视图。这只是一个简化,因为没有必要为会话门面的解释应用本地或远程接口。只需记住,自 EJB 3.1 以来,已经取消了本地接口的要求。
AcademicFacadeImpl会话门面有一个带有事件监听器参数的异步方法。此方法负责在学生请求时提供测试复习的日期和时间。例如:
public void requestTestReview (@ObservesAsync TestRevisionTO testRevisionTO) {
System.out.println("Enrollment : " + testRevisionTO.getEnrollment());
LocalDateTime dateTime = scheduleTestReview (testRevisionTO);
sendEmail (testRevisionTO, dateTime); // send an email with the schedule date for the test review:
}
此事件可以从一个门面客户端触发,通常是代表者或 Web 组件(例如 servlet 或 JSF 管理的 bean)。事件被注入到客户端并根据请求触发。然后它与TestRevisionTO对象一起触发:
// Event Injection :
@Inject
Event<TestRevisionTO> event;
...
...
// get the schedule date for a test revision:
TestRevisionTO testRevisionTO = new TestRevisionTO();
testRevisionTO.setEnrollment ("20010003");
testRevisionTO.setDisciplineCode("D3");
LocalDate date = LocalDate.of(2017, 11, 21);
LocalTime time = LocalTime.of (8, 30);
LocalDateTime dateTime = LocalDateTime.of(date, time);
testRevisionTO.setTestDateTime (dateTime);
event.fire (testRevisionTO);
TestRevisionTO类如下触发:
import java.io.Serializable;
import java.time.LocalDateTime;
public class TestRevisionTO implements Serializable {
private String enrollment;
private String disciplineCode;
private LocalDateTime testDateTime;
public String getEnrollment() {
return enrollment;
}
public void setEnrollment(String enrollment) {
this.enrollment = enrollment;
}
public String getDisciplineCode() {
return disciplineCode;
}
public void setDisciplineCode(String disciplineCode) {
this.disciplineCode = disciplineCode;
}
public LocalDateTime getTestDateTime() {
return testDateTime;
}
public void setTestDateTime(LocalDateTime testDateTime) {
this.testDateTime = testDateTime;
}
}
解释业务对象模式
如其名所示,业务对象代表现实世界中的某些事物以及与应用程序业务相关的事物。业务对象就像应用程序用例中的参与者。业务对象的例子包括银行账户、汽车保险、大学教授、学生、员工、采购订单以及应付或应收账款。
当涉及到具有非常少业务复杂性的简单应用程序时,也就是说,具有很少(或没有)业务规则时,系统中可能不需要 BO。更好的是,可以认为代表数据库实体的 POJO 实体就是一个 BO。在这里看到区别很重要。一个实体或实体的 POJO 代表(如 JPA POJO)比业务模型对象更接近技术和结构。因此,在这个例子中,一个如大学生这样的实体也可以被认为是一个 BO 或大学生用例的参与者。实际上,在这些更简单的案例中,数据模型足以满足业务需求,因此不需要定义业务对象。
在这种情况下,我们说与大学生相关的数据模型紧密代表了与学生相关的概念域模型。
应用程序通常非常简单,以至于业务层客户端,如 Session Façade(甚至表示层客户端),可以直接通过 DAO 访问数据模型。不需要模型对象来处理应用程序业务中的更大复杂性。
具有复杂业务规则的程序
假设我们想要增加系统的复杂性(比如说系统需要包含更多功能)。让我们再想象一下,一个大学有由教授、员工和学生组成的成员。假设有一个member实体,它几乎与相关数据库中的member表相匹配。此外,我们知道教授、学生和员工都是大学的成员,因此他们具有每个成员都应该拥有的共同特征。然而,教授、学生和员工也有他们自己的特征。教授的主要特征是他们是一位教授并拥有硕士或博士学位的成员。
同样,学生也有他们自己的特征,例如大学入学和注册的课程数量。教授和学生都将与其它实体有联系。为此,我们可以从数据库的角度来架构应用程序,创建四个表——Member、Student、Employee 和 Professor。我们可以在 Student 和 Member、Professor 和 Member 以及 Employee 和 Member 之间建立一对一的关系。在任何实现中,我们都可以有与这些表相关的四个 JPA 实体。
然而,教授是具有一些业务规则的 teaches discipline 用例的演员。这个更复杂的 professor 对象结合了 Member 和 Professor 实体。然后我们可以定义一个 ProfessorBO(业务对象)对象,它是 Member 与 Professor 的结合。此外,ProfessorBO 可能具有提供更丰富行为价值的方法,因为这些方法在用例中使用。没有这些方法,对象就变成了贫血对象。然而,可以说这仍然是一个相对较低复杂度的。因此,我们可以考虑一个用例,展示教授教授的所有学科,或者教授教授特定学科所需的技能。在这里,我们还有一个表和实体:Discipline。
此外,学生与特定的课程有关联。这是一个具有一个更多实体的用例:Course。学术系统的可能性是无限的,并且远远不是一个简单的数据模型系统。业务对象可以用于复杂用例。
下面的图显示了 Professor、Student 和 Employee 作为更复杂的对象:

这种场景只是几种可能的实现方式之一。在我们的小型示例中,我们考虑了存在一个名为 ProfessorBO 的对象,它使用三个实体(Professor、Member 和 Discipline)并且具有相关的业务方法。
有些人可能会争论说,没有必要有 ProfessorBO。我们可以有一个教授外观(Professor Facade),它可以实现会话外观(Session Façade)模式,使用业务方法,并且还会使用 DAO(用于执行 CRUD 操作)来操作不同的组合实体。
我们可以同意这种架构,并且根据系统的复杂性,它甚至可能是最佳选择。然而,我们选择了这种架构,它清楚地说明了 BO 模式的定义和使用,所以让我们继续我们的例子。
因此,一个 ProfessorBO 对象代表了一位教授,这位教授是针对与概念教授模型相关的一个或多个用例的演员。
使用业务对象模式的动机
当概念模型涉及更大的复杂性时,我们使用业务对象模式。这种高复杂性可能是因为 BO 使用了其他对象的组合,并且具有复杂的业务逻辑,例如验证规则。因此,需要将此业务逻辑从应用程序的其他部分分离出来(数据持久性就是一个例子)。
不实现此模式可能导致问题,例如降低代码的可重用性。因此,有几种可能的解决方案,这些解决方案会使代码维护变得耗时,因为它会失去使用设计模式带来的统一性。
业务对象模式使用的优点
下面是业务对象模式的优点总结:
-
BOs 负责管理其业务规则和持久性。这导致代码更具可重用性。客户端访问完全负责应用程序行为的 BOs。在我们的例子中,
ProfessorBO可以从多个点调用。除了可重用性之外,还有行为的一致性。因此,另一个好处是更快、更高效的维护。 -
BOs 负责将业务逻辑与应用程序的其他部分分离,这增加了代码的凝聚力(责任分离)。
-
BOs 有助于将业务逻辑与数据持久性分离。
下面是业务对象模式的类图:

对于更复杂的应用,我们通常有一个表示一组相关用例的 Session Façade。正如我们已经看到的,Session Façade 为客户端提供了高级方法。对于其本身,Session Façade 可以管理和组合充当真实代理或真实世界对象代表的 BOs。
下面是 业务对象 的序列图:

实现业务对象模式
我们现在将输入一些代码来展示业务对象模式。然而,我们必须再次注意,可能还有其他方法可以得到结果。例如,我们可以使用 O-R 映射(JPA 或 Hibernate 技术)来映射实体。
例如,Professor 实体与 Discipline 实体之间存在 n-to-n 的关系,这是通过 JPA 注解实现的。然而,我们知道这里的使用案例远不止映射实体那么简单。
我们将使用 ProfessorBO、Professor、Discipline、ProfessorDAO 和 DisciplineDAO。让我们利用 Session Façade 示例中展示的类。我们在 AcademicFacadeImpl 类中做了一些小的改动。现在,这个 Session Façade 使用一个名为 ProfessorBO 的 BO 来处理与 Professor 相关的业务。
让我们回顾一下 ProfessorBO 类:
import java.time.LocalDate;
import java.util.List;
import javax.inject.Inject;
public class ProfessorBO {
private Professor professor;
private List<Discipline> disciplines;
@Inject
private ProfessorDAO professorDAO;
@Inject
private DisciplineDAO disciplineDAO;
public void setProfessor (Professor professor ) {
this.professor = professorDAO.findByName (professor.getName());
}
public boolean canTeachDiscipline (Discipline discipline) {
if (disciplines == null) {
disciplines = disciplineDAO.getDisciplinesByProfessor (professor);
}
return disciplines.stream().anyMatch(d->d.equals(discipline));
//return disciplines.contains(discipline);
}
public LocalDate getInitDate () {
return professor.getInitDate();
}
public String getName () {
return professor.getName();
}
}
让我们再看看 AcademicFacadeImpl 类:
@Stateless
@LocalBean
public class AcademicFacadeImpl implements AcademicFacadeRemote, AcademicFacadeLocal {
...
...
@Inject
private ProfessorBO professorBO;
@Override
public List<Professor> getProfessorsByDiscipline(Discipline discipline) {
return disciplineDAO.getProfessorByDiscipline(discipline);
}
public boolean canProfessorTeachDiscipline (Professor professor, Discipline discipline) {
/*return disciplineDAO.getDisciplinesByProfessor (professor).contains(discipline);*/
professorBO.setProfessor (professor);
return professorBO.canTeachDiscipline(discipline);
}
}
如前述代码块所示,AcademicFacadeImpl会话外观从注入的ProfessorBO豆中调用canTeachDiscipline方法。然后ProfessorBO使用ProfessorDAO和DisciplineDAO。接下来,我们将看到ProfessorBO豆使用的DisciplineDAO代码部分:
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DisciplineDAO {
static {
Discipline d1 = new Discipline("D1", "discipline 1");
Discipline d2 = new Discipline("D2", "discipline 2");
Discipline d3 = new Discipline("D3", "discipline 3");
Discipline d4 = new Discipline("D4", "discipline 4");
disciplines = Arrays.asList(d1, d2, d3, d4);
...
professorXDisciplines.put (new Professor ("professor a"), Arrays.asList (d1, d2));
professorXDisciplines.put (new Professor ("professor b"), Arrays.asList (d3));
professorXDisciplines.put (new Professor ("professor cv"), Arrays.asList (d1, d3, d4));
}
...
public List<Discipline> getDisciplinesByProfessor(Professor professor) {
return professorXDisciplines.get (professor);
}
...
}
让我们看看ProfessorDAO类使用的代码:
public class ProfessorDAO {
private static Set<Professor> professors;
static {
Professor p1 = new Professor ("professor a", LocalDate.of (2001, 03,
22)),
p2 = new Professor ("professor b", LocalDate.of (1994, 07, 05)),
p3 = new Professor ("professor c", LocalDate.of (1985, 10, 12)),
p4 = new Professor ("professor cv", LocalDate.of (2005, 07, 17));
professors = Arrays
.stream (new Professor[]{p1, p2, p3, p4})
.collect (Collectors.toSet());
}
public Professor findByName (String name) {
return professors
.stream()
.filter(p->p.getName().equals(name))
.findAny()
.get();
}
}
最后,让我们看看ProfessorBO类:
import java.time.LocalDate;
import java.util.List;
import javax.inject.Inject;
public class ProfessorBO {
private Professor professor;
private List<Discipline> disciplines;
@Inject
private ProfessorDAO professorDAO;
@Inject
private DisciplineDAO disciplineDAO;
public void setProfessor (Professor professor ) {
this.professor = professorDAO.findByName (professor.getName());
}
public boolean canTeachDiscipline (Discipline discipline) {
if (disciplines == null) {
disciplines = disciplineDAO.getDisciplinesByProfessor
(professor);
}
return disciplines.stream().anyMatch(d->d.equals(discipline));
//return disciplines.contains(discipline);
}
public LocalDate getInitDate () {
return professor.getInitDate();
}
public String getName () {
return professor.getName();
}
}
摘要
在本章中,我们探讨了业务代表的主要目标是隐藏服务实现的细节,以避免展示层。我们还看到,在某些情况下,其使用已被 CDI 技术(这种技术负责以类型安全的方式将组件注入到应用程序中,例如注入 EJB 组件)所取代,但我们认为这还不够。业务代表在处理更技术性的异常方面仍然被广泛使用——例如,当有远程 EJB 调用时。此外,代表保护展示层免受服务层可能的变化的影响,反之亦然,当有除网页浏览器之外的其他类型客户端时,使用代表使这些新客户端访问服务变得更加容易。
会话外观集中了业务逻辑,同时不向客户端暴露涉及业务对象的复杂交互。此外,会话外观封装了业务层组件,并向本地和远程客户端暴露粗粒度服务。因此,客户端访问会话外观而不是直接访问业务组件。一些服务,如事务控制或安全管理,使用会话外观实现,例如 EJB。
当应用程序呈现高度复杂性时,必须使用业务对象模式。这可能包括当现实世界对象的表示不能仅仅作为数据模型对象来翻译,因此需要业务解决方案的可重用性和一致性时。除了可重用性之外,这种模式的直接好处包括高效的代码维护以及由于职责分离而产生的层之间的丰富凝聚力。这是因为业务对象将业务逻辑和持久性从应用程序的其他部分分离出来。
第四章:集成模式
在本章中,我们将解释一些集成模式,并查看它们在 Java EE 的集成层上的工作方式。阅读本章后,您将能够实现这些模式,并使用它们来解决资源或系统之间的集成问题。您还将能够在集成层上工作,并熟悉与集成模式相关的概念。本章的主题如下:
-
解释集成层的理念
-
解释数据访问对象模式的理念
-
实现数据访问对象模式
-
解释领域存储模式的理念
-
实现领域存储模式
-
解释服务激活模式的理念
-
实现服务激活模式
解释集成层的理念
如前几章所述,Java EE 分为三个著名的层——表示层、业务层和集成层。这些层共同工作,以促进高度解耦的解决方案。
在商业环境中,软件开发非常困难,因为我们需要考虑企业的整个生态系统。一个生态系统包括其数据源、软件、数据政策、安全和设备。因此,开发者需要考虑如何在这些数据源中读取和写入数据,软件之间如何相互通信,数据政策如何在系统中实施,商业环境中的安全机制如何运作,等等。在这种情况下,创建一个层来解决所有集成和通信问题将是有益的,因为它们的解决方案将与业务逻辑解耦。这就是产生集成层的思维过程。
集成层是负责在整个应用程序中将业务逻辑与集成逻辑解耦的层。这个层具有与外部资源或系统通信的逻辑,并且与业务逻辑保持分离。这个层将使得从外部源读取和写入数据成为可能,使得应用程序和商业生态系统组件之间的通信变得可行。此外,这个层将隐藏所有通信复杂性,使业务层接收数据时无需了解组件之间的通信复杂性和它们的结构。
现在,开发一个没有与外部资源集成但具有某种集成功能的程序极为罕见。这是因为应用程序总是需要从源读取数据,而这个源通常位于应用程序之外,如数据库或文件系统中。如果应用程序依赖外部数据源,那么这个应用程序需要集成以从外部数据源访问数据。因此,更简单的应用程序将需要一个集成层。
随着时间的推移,资源或系统之间集成的复杂性增加,因为越来越多的业务逻辑需要集成以促进对业务的良好响应。因此,有必要创建一个通用的解决方案来解决反复出现的集成问题,并且因此产生了集成模式。
解释数据访问对象模式的概念
在商业世界中,应用程序总是需要与数据源集成,以便读取、写入、删除或更新数据。这个数据源可以是关系数据库、NoSQL 数据库、LDAP(轻量级目录访问协议)或文件系统,例如。每种类型的数据源都有其结构,并且连接、读取和写入数据都有其复杂性。这些复杂性不应该暴露给业务逻辑,而应该与之解耦。
数据访问对象模式是一种用于从业务层抽象和隐藏所有数据源访问的模式。该模式封装了所有数据源访问逻辑及其复杂性,从业务层解耦所有数据源访问逻辑。如果我们想用另一个数据源替换它,我们只需要修改数据访问对象模式的代码,这种修改在业务层是不可见的。以下图显示了数据访问对象模式模型:

在前面的图中,我们看到了BusinessObject,它包含业务逻辑;DAO,它包含数据访问逻辑;TransferObject,它是用于传输的对象;以及数据源,它是数据存储的外部本地。当BusinessObject需要访问数据时,它会从 DAO 请求数据。DAO 访问数据源并读取数据,然后将数据作为TransferObject返回给BusinessObject。一些开发人员和专业人士认为这种模式仅应与关系数据库和NoSql一起使用,但当我们数据源是文件系统或其他类型的数据持久化时,我们也应该使用 DAO,以促进业务逻辑和持久化逻辑之间的解耦,以及组织我们的代码。
实现数据访问对象模式
要使用 Java EE 8 的最佳实践来实现此模式,我们将使用关系型数据库,并通过 JPA 规范实现数据的读取和写入。在这个例子中,我们将有一个名为 employee 的表,其中包含员工数据。我们还将创建一个名为 EmployeeDao 的类,它将包含四个方法 —— save(employee)、findByName(name)、findAll() 和 delete(employee)。save 方法将接收一个员工对象并将其保存到数据库中,findByName 将接收一个参数作为名称,并在数据库中按名称查找员工,delete 将接收一个员工对象并将其从数据库中删除。此外,我们还将创建一个名为 Employee 的传输对象,这是一个 JPA 实体类,它具有与数据库表的映射。
使用 JPA 实现实体
JPA 实体是一个表示数据库中的某个表或视图的类。实体需要一个属性来唯一标识一个实体,需要有一个无参构造函数,并且 JPA 实体的每个对象都只标识表或视图中的一行。
在以下代码中,我们有一个名为 Entity 的接口,所有 JPA 实体都将根据以下方法实现:
public interface Entity < T > {
public T getId();
}
在以下代码中,我们有一个名为 Employee 的传输对象,它是一个 JPA 实体。这个类有一个名为 Employee 的映射表,以及应用程序使用的列。业务层只需要知道传输对象、DAO 和发送给 DAO 的参数:
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.Objects;
@javax.persistence.Entity(name = "Employee")
public class Employee implements Entity<Long> {
@Id
@GeneratedValue
@Column(name = "id")
private Long id;
@NotNull
@Column(name="name")
private String name;
@Column(name="address")
private String address;
@NotNull
@Column(name="salary")
private Double salary;
public Employee(){}
public Employee( String name, String address, Double salary){
this.name = name;
this.address = address;
this.salary = salary;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
@Override
public Long getId() {
return this.id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
实现 DAO
为了重用大量代码并推广实现 DAO 的最佳实践,我们将创建一个抽象 DAO,称为 AbstractDao,它是所有 DAO 的超类,具有所有 DAO 都可以使用的通用逻辑方法:
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public abstract class AbstractDao <T extends Entity>{
//EntityManager that provide JPA functionalities
@PersistenceContext
protected EntityManager em;
//Get the type of Subclass that implements Entity interface
protected Class<T> getType() {
ParameterizedType genericType = (ParameterizedType)
this.getClass().getGenericSuperclass();
return (Class<T>) genericType.getActualTypeArguments()[0];
}
//Find entity filtering by id.
public Optional<T> findById ( T entity ){
return Optional.ofNullable( em.find( (Class<T>)
entity.getClass(), entity.getId() ) );
}
public Optional<T> persist (T entity ){
em.persist( entity );
return Optional.of( entity );
}
public Optional<T> update ( T entity ){
return Optional.ofNullable( em.merge( entity ) );
}
public void delete ( T entity ){
em.remove( entity );
}
protected List<T> listWithNamedQuery(String namedQuery, Map<String,
Object> parameters){
TypedQuery<T> query = em.createNamedQuery( namedQuery,
getType() );
parameters.keySet().stream().forEach( key-> query.setParameter(
key, parameters.get( key ) ) );
return query.getResultList();
}
protected Optional<T> findWithNamedQuery(String namedQuery,
Map<String, Object> parameters){
TypedQuery<T> query = em.createNamedQuery( namedQuery,
getType() );
parameters.keySet().stream().forEach( key-> query.setParameter(
key, parameters.get( key ) ) );
return Optional.ofNullable(query.getSingleResult());
}
}
为了防止用户实例化 AbstractDao,我们在前面的代码中将该类创建为一个抽象类。另一个 AbstractDao 的特点是方法的返回值,它只返回 Entity 或 Entity 的列表。这是一个好的实践,因为我们知道这个方法返回的对象类型,并且它组织了我们的代码,因为我们知道我们的方法可能返回什么类型的值:
import javax.ejb.Stateless;
import java.util.Collections;
import java.util.List;
@Stateless
public class EmployeeDao extends AbstractDao <Employee> {
public List<Employee> findByName(String name ){
return this.listWithNamedQuery("Employee.findByName",
Collections.singletonMap(
"name", name ) );
}
public List<Employee> findAll(){
return this.listWithNamedQuery("Employee.findAll",
Collections.emptyMap());
}
}
我们可以看到EmployeeDao类,这是读取和写入Employee数据的 DAO。这个类是一个 EJB,这意味着它可以控制所有由 JTA 规范定义的事务——假设事务由 Java EE 容器管理——并使事务控制对应用程序透明。为了读取员工数据,我们可以调用findAll、findByName和findById方法;为了写入员工数据,我们可以调用persist和update方法;为了删除(删除)员工数据,我们可以调用delete方法。请注意,业务类——一个在业务层上执行操作的类——不知道读取和写入数据的过程;它只知道要调用的方法的参数以及它的返回值。因此,我们可以替换数据源而不影响业务逻辑。我们有EmployeeBusiness使用 DAO 来读取和写入数据,我们将在下面看到:
import com.packt.javaee8.dao.EmployeeDao;
import com.packt.javaee8.entity.Employee;
import javax.ejb.Stateless;
import javax.inject.Inject;
import java.util.List;
import java.util.Optional;
@Stateless
public class EmployeeBusiness{
@Inject
protected EmployeeDao employeeDao;
public List<Employee> listByName( String name ){
return employeeDao.findByName( name );
}
public boolean save ( Employee employee ){
return employeeDao.persist( employee ).isPresent();
}
public List<Employee> listAll(){
return employeeDao.findAll();
}
public Optional<Employee> findById(Employee employee ){
return employeeDao.findById(employee);
}
}
这是一个很好的模式;它的应用非常广泛,大多数应用程序都实现了它。在实现这个模式时,请注意不要暴露数据源特性,例如,通过将 SQL 查询作为参数发送给 DAO 的执行,或者通过将文件系统的路径作为参数发送给 DAO 的执行。
解释领域存储模式的概念
在上一节中,我们介绍了数据访问对象模式,并探讨了该模式如何从业务层抽象数据访问逻辑。然而,数据访问对象模式是一个无状态模式,它不保存状态和智能过程。一些问题中数据之间存在复杂的关系,数据持久化需要通过智能过程来完成。为了促进这一特性,数据访问对象模式不参与。这是因为 DAO 不应该维护状态,不应该包含任何智能过程,只需要包含保存或更新的过程。为了解决这个问题,领域存储模式被创建——这是一个可以为 DAO 添加功能的模式。
领域存储模式是一种使对象模型持久化透明化的模式,将持久化逻辑与对象模型分离,使得应用程序可以根据对象状态选择持久化逻辑。在这个模式中存在一个 DAO,它被设计用来与数据源进行通信和操作数据,但这个 DAO 对应用程序是隐藏的。由于 JPA 已经作为一个领域存储模式工作,因此开发者很少实现这种模式。这是因为 JPA 实现了一些智能过程来定义何时以及如何保存数据,并且这些智能过程是以 JPA 实体上的映射为导向的。然而,当我们决定在另一种类型的数据源中实现持久化时,我们可能希望实现这种模式并在我们的应用程序中使用它。在下面的图中,我们可以看到领域存储模式模型:

许多开发者认为在 JPA 之后 DAO 已经过时。这是因为 JPA 与领域存储模式一起工作,并且已经有一个内置的 DAO。然而,对我们来说,DAO 是 Java EE 项目上使用的好模式。这是因为 JPA 与关系数据库有更强的关系,如果我们用另一种类型的数据源替换关系数据库,我们可能需要移除 JPA 并使用另一种机制。如果所有 JPA 调用都在 DAO 内部,应用程序将只看到 DAO,JPA 实现将隐藏于应用程序之外。这使得业务层与集成层和持久化逻辑解耦。
实现领域存储模式
这是一个非常长的模式。为了实现和促进这种实现,我们将所有数据保存在 HashMap 中。这是一个重要的步骤,因为本小节的重点是展示如何实现领域存储模式。为了便于理解,我们将查看在实现数据访问对象模式小节中涵盖的相同场景。在这里,我们有传输对象,称为Employee,我们还将对数据源进行读写操作。然而,持久化将面向对象状态,并在其逻辑中具有智能。在这个实现中,我们有以下类、接口和注解:
-
PersistenceManagerFactory: 这作为一个工厂模式工作,负责创建PersistenceManager类的实例。PersistenceManagerFactory是一个单例,在整个应用程序中只有一个实例。 -
PersistenceManager: 这管理持久化和查询数据。这些数据是一个作为工作单元的对象模型。 -
Persistence: 这是一个用作 CDI资格的注解。这个资格用于定义PersistenceManagerFactory的方法,它负责创建PersistenceManager实例。 -
EmployeeStoreManager: 这作为一个数据访问对象(DAO)工作,与数据源交互并封装所有数据源复杂性。DAO 负责在数据源上读取和写入员工数据。 -
StageManager: 这是一个用于创建所有StageManager实现的接口。 -
EmployeeStageManager: 这根据其状态和规则协调数据的读写操作。 -
TransactionFactory: 这作为一个工厂模式工作,负责创建Transaction实例。TransactionFactory是一个单例,在整个应用程序中只有一个实例。 -
Transaction: 这用于创建面向事务的策略。此类控制事务的生命周期并定义事务限制。 -
Transaction(注解):用作 CDI资格的注解。这个资格用于定义TransactionFactory的方法,它负责创建Transaction实例。
为了实现这个模式,我们将从PersistenceManagerFactory类开始,这是一个PersistenceManager的工厂。
实现 PersistenceManagerFactory 类
在下面的代码中,我们有用于注入PersistenceManager类的限定符:
import javax.inject.Qualifier;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface Persistence {
}
查看以下代码,我们可以看到我们有一个PersistenceManagerFactory类,该类负责创建PersistenceManager的新实例。这个类使用了@Singleton注解,这是一个用于通过 Java EE 机制创建单例模式的 EJB 注解。getPersistenceManager方法有@Produces注解,它用于定义一个负责创建新实例的方法。它还包含@Persistence注解,它用作一个限定符:
import javax.ejb.Singleton;
import javax.enterprise.inject.Produces;
import java.util.HashSet;
import java.util.Set;
@Singleton
public class PersistenceManagerFactory {
Set<StageManager> stateManagers = new HashSet<StageManager>();
public @Produces @Persistence PersistenceManager
getPersistenceManager(){
//Logic to build PersistenceManager
return new PersistenceManager();
}
}
实现 PersistenceManager 类
我们需要管理所有持久化和查询数据的过程。为此,我们需要创建一个类。
PersistenceManager类负责管理所有的持久化和查询过程:
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class PersistenceManager {
private Set<StageManager> stateManagers;
@Inject @Transactional Transaction transaction;
@PostConstruct
public void init(){
stateManagers = new LinkedHashSet<StageManager>();
}
public Optional<Entity> persist ( Entity entity ) {
if ( entity instanceof Employee ){
stateManagers.add( new EmployeeStageManager( (Employee)
entity ) );
}
return Optional.ofNullable(entity);
}
public void begin() throws Exception {
if( !transaction.isOpened() ){
transaction.begin();
}
else{
throw new Exception( "Transaction already is opened" );
}
}
public Optional<Entity> load(Object id){
Entity entity = stateManagers.stream()
.filter( e-> e.getEntity().getId().equals( id ) )
.map( s->s.getEntity() )
.findFirst()
.orElseGet( ()-> new EmployeeStoreManager().load( id ) );
if( Optional.ofNullable(entity).isPresent()
&& stateManagers.stream().map( s->s.getEntity()
).collect( Collectors.toList() ).contains( entity ) )
stateManagers.add( new EmployeeStageManager( (Employee)
entity) );
return Optional.ofNullable(entity);
}
public void commit() throws Exception {
if( transaction.isOpened() ){
stateManagers.stream().forEach( s-> s.flush() );
transaction.commit();
}
else{
throw new Exception( "Transaction is not opened" );
}
}
public void rollback() throws Exception {
if( transaction.isOpened() ) {
stateManagers = new LinkedHashSet<StageManager>();
transaction.rollback();
}
else {
throw new Exception( "Transaction is not opened" );
}
}
}
在前面的代码中,我们有PersistenceManager类。这个类有一个stateManagers属性,它是一组StateManager,用于控制每个对象模型的读写。它还有一个Transaction属性,用于控制事务的生命周期。这个类还有一个persist方法,用于写入由对象模型表示的数据;begin方法,用于开始事务;load方法,用于读取对象模型的数据;commit方法,用于提交事务;最后,rollback方法,用于回滚事务。
实现 EmployeeStoreManager 类
EmployeeStoreManager类负责与数据源连接以及读取和写入员工数据:
import com.packt.javaee8.entity.Employee;
import java.util.HashMap;
import java.util.Map;
public class EmployeeStoreManager {
private Map<Object, Employee> dataSource = new HashMap<>();
public void storeNew ( Employee employee ) throws Exception {
if( dataSource.containsKey( employee.getId() ) ) throw new Exception( "Data already exist" );
dataSource.put( employee.getId(), employee );
}
public void update ( Employee employee ) throws Exception {
if( !dataSource.containsKey( employee.getId() ) ) throw new Exception( "Data not exist" );
dataSource.put( employee.getId(), employee );
}
public void delete ( Employee employee ) throws Exception {
if( !dataSource.containsKey( employee.getId() ) ) throw new Exception( "Data not exist" );
dataSource.remove( employee.getId() );
}
public Employee load(Object key){
return dataSource.get( key );
}
}
在前面的代码块中,我们有EmployeeStoreManager类,该类负责与数据源连接以及读取和写入员工数据。这个类作为一个 DAO 工作,封装了所有的数据源复杂性。它还有一个dataSource属性,这是一个表示数据源的映射。此外,这个类有storeNew方法,用于写入由对象模型表示的新员工数据。它还有一个update方法,用于写入由对象模型表示的现有员工数据。此方法用于更新存储的数据。delete方法也用于删除现有员工数据,而load方法用于以对象模型读取员工数据。
实现 StageManager 接口
StageManager是一个接口,由EmployeeStageManager实现:
public interface StageManager {
public void flush();
public void load();
public Entity getEntity();
}
在前面的代码块中,我们有StageManager接口。这个接口由EmployeeStageManager实现:
public class EmployeeStageManager implements StageManager {
private boolean isNew;
private Employee employee;
public EmployeeStageManager ( Employee employee ){
this.employee = employee;
}
public void flush(){
EmployeeStoreManager employeeStoreManager = new EmployeeStoreManager();
if( isNew ){
try {
employeeStoreManager.storeNew( employee );
} catch ( Exception e ) {
e.printStackTrace();
}
isNew = false;
}
else {
try {
employeeStoreManager.update( employee );
} catch ( Exception e ) {
e.printStackTrace();
}
}
}
public void load() {
EmployeeStoreManager storeManager =
new EmployeeStoreManager();
Employee empl = storeManager.load( employee.getId() );
updateEmployee( empl );
}
private void updateEmployee( Employee empl ) {
employee.setId( empl.getId() );
employee.setAddress( empl.getAddress() );
employee.setName( empl.getName() );
employee.setSalary( empl.getSalary() );
isNew = false;
}
public Entity getEntity() {
return employee;
}
}
在前面的代码块中,我们看到了isNew属性,用于定义对象模型是否为新的写入(将在数据源上创建新数据)。代码还包含employee属性,这是用于表示员工数据的对象模型。我们还可以看到flush方法,用于执行在数据源上写入数据的过程;load方法,用于从数据源读取数据;updateEmployee方法,这是一个私有方法,用于更新employee属性;以及getEntity方法,用于返回employee属性。
实现 TransactionFactory 类
在下面的代码中,我们看到了Transactional注解,这是一个Qualifier,用于注入Transaction类:
import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface Transactional {
}
在前面的代码块中,我们看到了TransactionFactory类,该类负责创建Transaction类的新实例。这个类使用了@Singleton注解,这是一个用于通过 Java EE 机制创建单例模式的 EJB 注解。getTransaction方法有@Produces注解,用于定义一个负责创建新实例的方法,以及@Transactional注解,用作限定符:
import javax.ejb.Singleton;
import javax.enterprise.inject.Produces;
@Singleton
public class TransactionFactory {
public @Produces @Transactional Transaction getTransaction(){
//Logic to create Transations.
return new Transaction();
}
}
实现 Transaction 类
Transaction类负责控制事务生命周期和定义事务界限:
package com.packt.javaee8.domainstore;
import javax.annotation.PostConstruct;
public class Transaction {
private boolean opened;
@PostConstruct
public void init(){
this.opened = false;
}
public void commit() throws Exception {
if( !opened ) throw new Exception("Transaction is not opened");
opened = false;
}
public void rollback() throws Exception {
if( !opened ) throw new Exception("Transaction is not opened");
opened = false;
}
public void begin() throws Exception {
if( opened ) throw new Exception("Transaction already is opened");
opened = true;
}
public boolean isOpened(){
return opened;
}
}
在前面的代码块中,我们看到了Transaction类,该类负责控制事务生命周期和定义事务界限。这个类有init()方法,被@PostConstruct注解,配置该方法在构造函数执行后调用。此外,这个类还有commit方法,用于用户需要确认事务时;rollback方法,用于撤销所有事务;begin方法,用于开启事务;以及isOpened方法,用于验证事务是否开启。
如果没有调用begin方法,或者调用了commit或rollback方法但没有再次调用begin方法,则事务将关闭。
实现 EmployeeBusiness 类
EmployeeBusiness类包含员工业务逻辑:
import javax.ejb.Stateless;
import javax.inject.Inject;
import java.util.Optional;
@Stateless
public class EmployeeBusiness{
@Inject @Persistence
protected PersistenceManager persistenceManager;
public boolean save ( Employee employee ) throws Exception {
//Begin Transaction
persistenceManager.begin();
persistenceManager.persist(employee);
//End Transaction
persistenceManager.commit();
return true;
}
public Optional<Employee> findById(Employee employee ){
return Optional.ofNullable( (Employee) persistenceManager.load(employee.getId()).get());
}
}
在前面的代码块中,我们看到了EmployeeBusiness类,该类包含员工业务逻辑。这个类有save(Employee)方法,用于保存员工数据,以及findById(Employee )方法,用于根据 ID 查找员工。请注意,在save方法中,我们同时调用了PersistenceManager类的begin()和commit()方法。这些调用定义了事务的界限,并且只有在调用commit()方法时,数据才会被保存在数据源中。
解释服务激活器模式的概念
假设一个客户端需要请求一个业务服务,这是一个耗时较长的过程。在这种情况下,客户端不应该以同步的方式等待过程结束。相反,必须有一种方法来进行异步服务调用,这样就不会阻塞客户端或用户。这个服务可以在未来的某个时刻被激活。过程延迟可能有几个原因。例如,可能有一个消耗大量时间的数据库查询,或者对当前应用程序控制之外的遗留系统的访问。异步执行所需任务的模式被称为服务激活器。
因此,当客户端需要异步调用服务时,总是使用服务激活器模式。这意味着客户端发出请求,并不等待响应。
我们可以想象一些替代的解决方案来解决这个问题。一种方法是将请求发送到队列,而另一个服务将从这个队列中读取请求并在其中执行任务。或者,当客户端请求一个服务时,这个服务可以被放置在数据库中,并且可能有一个监听器或作业来检查尚未执行的任务。如果任务尚未执行,它将被执行。
实际上,JEE 规范为我们提供了非常好的解决方案,用于实现服务激活器模式。以下是对这些解决方案的描述:
-
Java 消息服务(JMS)
-
EJB 异步方法
-
异步事件:生产者和观察者
这三个解决方案是在 JEE 规范演化的过程中按顺序提出的。在接下来的章节中,我们将更详细地探讨每个解决方案。
Java 消息服务(JMS)
消息驱动中间件(MOM)是一种使用消息交换的架构,这指的是在应用程序的模块之间或分布式系统之间发送和接收消息。MOM 提供了一些良好的服务,例如消息持久性或消息投递保证。例如,消息代理基于 MOM。
Java 消息服务(JMS)是一个应用程序编程接口(API),为希望进行异步处理的客户端提供了一个消息中间件(MOM)接口。JMS 成为 EJB 2.0 规范的一部分,并引入了一个新的会话 Bean:消息驱动 Bean(MDB)。
MDB Bean 是一个无状态的会话 Bean,用于监听到达 JMS 队列的请求或对象。需要注意的是,MDB 可以实现任何类型的消息,但它更常用于处理 JMS 消息。
消息驱动 Bean 监听发送到队列或主题的消息。然而,我们只将看到发送到队列的消息示例。消息可以由任何 JEE 组件或 JEE 上下文之外的应用程序发送。
以下图表显示了消息的轨迹,从发送到由 MDB 接收:

在本章的后面部分,我们将看到一个消息生产者和消息接收器的实现示例。消息接收器将通过 MDB 实现。现在,我们将引用一些关于 MDB 的重要事项。MDB 实现具有以下结构:
@MessageDriven (mappedName = "myQueue")
public class BeanMessage implements MessageListener {
@Override
public void onMessage (Message message) {
try {
// message process
}catch (JMSException ex) {
// handle exception
}
}
}
@MessageDriven注解将 bean 设置为消息驱动的。此外,bean 类必须是公共的,但不能是抽象的或最终的,并且类必须包含一个无参数的公共构造函数。
mappedName属性指定了将接收要消费的消息的 JMS 的 JNDI 名称。@MessageDriven注解中还有其他属性用于配置 bean。例如,activationConfig属性可能包含一个@ActivationConfigProperty注解数组,该数组包含一个键/值对,用于改进 bean 的配置。
该 bean 必须实现MessageListener接口,该接口只有一个方法,称为onMessage。当消息被 MDB 消费时,容器会调用此方法。
我们可以识别以下 MDB 的特征:
-
它不在与消息发送者相同的事务上下文中
-
它不是由另一个会话 bean 直接调用的
-
它可以调用其他会话 bean
-
它可以发送 JMS 消息
-
它没有与客户端访问相关的远程或本地接口
EJB 异步方法
EJB 3.1 规范通过使用@javax.ejb.Asynchronous注解将异步方法纳入会话 bean。因此,我们可以从对 EJB 方法的简单调用中获得异步处理。
@javax.ejb.Asynchronous注解可以应用于会话 bean 的类或应用于此类中的单个方法。如果应用于整个类,则此 bean 的所有业务方法都将异步调用。否则,只有带有注解的方法才会异步调用:
@Stateless
public class MyBean
@Asynchronous
public void veryTimeConsumingProcess1 (SomeFilterBean filter) {
//código para cadastrar um pedido
}
@Asynchronous
public Future veryTimeConsumingProcess2 (SomeFilterBean filter) {
//autoriza demora .....
}
}
第二个方法返回一个java.util.concurrent.Future实例。通过此对象,客户端可以检查结果是否已经到达,甚至可以中止任务。然而,该方法会立即返回给客户端线程,并不会阻塞进程。
异步事件 - 生产者和观察者
在 JEE 平台演化的尺度上出现的另一个替代方案是 CDI 规范的一部分事件机制。该机制由事件的生产者和消费者组成,这意味着一个组件触发事件,而另一个应用组件接收事件,充当监听器或观察者。
在 JEE8 规范之前,此事件机制是同步执行的。随着 JEE8 规范中 CDI 2.0 的引入,事件 API 包括了异步使用等改进。
以下图展示了发送和接收事件的异步机制:

现在,我们将看到如何实现异步事件的 生产者 和 观察者 代码的基本方法:
public class SomeProducer {
@Inject private Event<BeanTO> someEvent;
public void finalizaCompra() {
BeanTO bean = new BeanTO(...);
someEvent.fireAsync(bean);
}
}
public class SomeObserver {
public void doSomething (@ObservesAsync BeanTO bean) {
// do some task with bean (like send email, calling another business process, etc.
}
}
someEvent 事件被注入到 SomeProducer 类中,Event.fireAsync() 方法负责异步事件触发。经过一段时间后,观察者也会异步地接收到事件。观察者方法具有带有 @ObservesAsync 注解的参数。
异步观察者是在新的事务上下文中被调用的。然而,它们属于与 Event.fireAsync 调用相同的上下文安全。
实现 service-activator 模式
现在,我们将展示 Java EE 平台提供的三个解决方案的代码示例。
使用 JMS 发送和接收消息
以下是一个 JMS 消息发送者的示例。这是一个负责发送消息的 CDI Bean:
public class MessageSender {
@Inject
@JMSConnectionFactory("jms/connectionFactory")
JMSContext context;
@Resource(mappedName = "jms/myQueue")
Destination queue;
public void sendSomeMessage (String message) {
context.createProducer().send(queue, message);
}
}
@JMSConnectionFactory 注解指示应该使用哪个 ConnectionFactory 来创建 JMSContext。以下代码块展示了一个接收前面描述的生产者生成的消息的 MDB:
@MessageDriven(
activationConfig = { @ActivationConfigProperty(
propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class EmailService implements MessageListener {
@Resource
private MessageDrivenContext mdc;
public void onMessage (Message message) {
try {
String str = message.getBody (String.class);
}
catch (JMSException ex){
// handling exception ...
mdc.setRollbackOnly();
}
}
如前所述,@MessageDriven 注解将一个简单的 Bean 转换为一个消息驱动 Bean (MDB)。此注解具有许多由 JMS 规范定义的激活配置属性。示例中显示的两个重要属性如下:
-
destinationLookup:队列或主题的 JNDI 查询名称 -
destinationType:队列类型,javax.jms.Queue或javax.jms.Topic
与无状态会话 Bean 类似,容器可以创建一个实例池来处理多个消息。onMessage 方法属于单个事务上下文,并且这个上下文会传播到 onMessage 内部调用的其他方法。
可以将 MessageDrivenContext 对象注入到 MDB 中。此对象在运行时提供对 MDB 上下文的访问。例如,我们可以使用前面提到的示例中的 setRollbackOnly() 方法回滚事务。
实现 EJB 异步方法
以下代码展示了无状态 EJB 异步方法的示例。假设有一个负责批准和安排学生测试审查的方法,我们希望以异步方式调用此方法:
@javax.ejb.Stateless
public AcademicServiceBean {
@javax.ejb.Asynchronous
public Future requestTestReview( Test test) {
// ...
}
}
我们将查看一个 Bean 客户端,该客户端请求测试审查服务,如下所示:
@Named
@SessionScope
public TestController {
@Inject
private AcademicServiceBean academicBean;
private Future statusTestReview;
public void requestTestReview(){
/* get Test object which has the test data, like: test date; student who made the test; discipline, etc.*/
Test testToBeReviewed = ...;
this.statusTestReview = academicBean.requestTestReview (testToBeReviewed);
}
public Future<TestReview> checkTestReviewStatus()
{
// ...
}
}
异步方法可以返回 void 或 java.util.concurrent.Future <T> 的实例。在先前的示例中,返回的是一个 Future <TestReview> 的实例。异步方法的调用结果会立即返回,并且客户端线程没有锁。然而,客户端可以随时查询这个 Future 实例以检查结果。TestReview 对象包含诸如布尔类型、测试审查是否被批准以及测试审查的日程日期等信息。
实现异步事件 - 生产者和观察者
假设一个客户想要向大学生发送电子邮件,邀请他们参加一个科技研讨会。并且假设发送的电子邮件会生成用于后续分析的统计数据。在这种情况下,客户不需要立即等待回复。我们可以创建一个事件生产者和两个事件观察者:一个负责发送电子邮件本身的观察者,另一个负责统计控制。这两个过程是相互独立的。以下代码展示了生产者和两个观察者。
这是SeminarProducer类:
public class SeminarProducer {
@Inject private Event<Seminar> seminarEvent;
public void sendEmailProcess(Date date, String title, String description) {
Seminar seminar = new Seminar(date, title, description);
seminarEvent.fireAsync(seminar);
}
}
这些是两个观察者:
public class SeminarServiceBean {
public void inviteToSeminar (@ObservesAsync Seminar seminar) {
// sen email for the college students inviting for the seminar
}
}
public class StatisticControlingBean {
public void generateStatistic (@ObservesAsync Seminar seminar) {
// create some statistic data
}
}
摘要
在本章中,你学习了集成层,以及集成模式和它们的实现方式。我们学习到的集成模式包括数据访问对象模式、领域存储模式和业务激活者模式。我们还学习了这些模式的实现。此外,我们学习了数据访问对象模式的概念,以及如何使用 Java EE 8 的最佳实践来实现它。我们还学习了领域存储模式的概念,领域存储模式与数据访问对象模式之间的区别,以及何时以及如何实现领域存储模式。最后,我们学习了业务激活者模式的概念,以及如何使用 JMS、EJB 异步方法、异步事件机制和 Java EE 8 的最佳实践来实现它。
在下一章中,我们将介绍反应式模式,重点关注何时使用它们以及如何使用 Java EE 8 的最佳实践来实现它们。
第五章:面向方面编程与设计模式
在本章中,我们将探讨面向方面编程(AOP)的概念,重点关注哪些情况下应该使用 AOP,以及如何通过使用 CDI 拦截器和装饰器来实现 AOP。最后,我们还将探讨其实现的一些示例。到本章结束时,你将能够使用拦截器和装饰器识别需要 AOP 的情况。此外,你还将能够识别实现这些概念的最佳方法。本章涵盖的主题如下:
-
面向方面编程
-
JEE 场景中的 AOP – 拦截器
-
EJB 拦截器实现
-
CDI 拦截器实现
-
装饰器模式
-
JEE 场景中的装饰器模式
-
装饰器实现
面向方面编程
AOP 是一种编程范式,它允许我们将业务逻辑与跨越所有应用程序的一些技术代码分离。换句话说,AOP 允许分离横切关注点。当我们向某些方法中输入日志代码以显示技术支持信息时,我们会遇到横切代码。我们也会在输入统计代码以查看方法被调用多少次或使用应用程序的用户是谁,或者甚至用于异常和错误处理时遇到它。我们在应用程序的几乎所有部分都能看到这种代码——这是在整个应用程序中重复的代码。这种代码有其自己的目标和关注点,将其从与应用程序用例相关的业务代码中分离出来是一个非常好的主意。
这些系统方面(如日志或异常处理)在模块化实现中非常困难。我们在这里说的是,我们不希望将这些方面与业务代码混合。通过混合这两种类型的代码,我们得到的是一个更难维护的最终代码。
使用 AOP,我们可以在不改变源代码的情况下添加或连接可执行代码,这意味着源代码保持完整。正如我们所说,如果我们想记录一个方法,例如,我们可以那样做而不破坏我们的业务代码,比如使用日志服务这样的服务代码。
编译时与运行时 AOP
AOP 是通过编译时或运行时代码注入来实现的。
在编译时实现 AOP 的框架会改变二进制代码(即.class文件),因此当拦截器代码被注入时,我们得到的是一个与未注入代码生成的.class文件不同的.class文件。结果.class文件与源代码不兼容。
另一方面,运行时注入不会修改源代码或.class文件。在这种情况下,拦截方法是在包含方法的类或位置之外的单独代码中完成的。因此,在方法原始调用之前和之后执行的拦截代码位于另一个类中。
Java 没有提供 AOP 的内置解决方案,但一些第三方框架,如 Eclipse AspectJ 和 Spring,在 Java 应用程序中得到了广泛使用。例如,我们可以使用 Eclipse AspectJ 来实现 AOP。Eclipse AspectJ 是 Java 编程语言的无缝面向方面扩展。它是一个兼容且易于使用的 Java 平台。然而,JEE 使用一种称为拦截器的新概念来实现 AOP,我们将在下一节中看到它是如何工作的。所有 JEE 程序员都可以使用拦截器来实现 AOP,而不需要获取与 AspectJ 解决方案相关的 JAR 依赖项并将其放入我们的应用程序中。
JEE 场景中的 AOP – 拦截器
面向方面的软件开发使得对横切关注点的清晰模块化成为可能。为了所有 JEE 程序员的利益,我们还可以将业务代码与横切关注点分离,这是拦截器技术通过实现 JEE 提供的解决方案。
当调用 EJB 方法或托管 Bean 时,如果与此调用关联有拦截器(我们很快将看到它是如何实现的),我们可以在方法调用之前和之后(即方法返回之后)编写要立即执行的代码。
拦截过滤器模式是另一种用于拦截请求的模式。在 JEE 中,我们可以使用 Web 过滤器和 Servlet 实现过滤器模式。通过使用 Web 过滤器,Java Web 应用程序实现了对 Web 请求和响应的拦截。Web 应用程序可以拦截请求,执行操作,也可以拦截响应。以下列表包含了一些具有许多操作的过滤器示例:
-
认证/授权过滤器
-
日志和审计过滤器
-
图像转换过滤器
-
数据压缩过滤器
-
加密过滤器
-
分词过滤器
拦截器充当过滤器的作用,因此我们可以将 Web 过滤器视为拦截器。然而,这种机制与 Web 请求和响应相关。在本章中,我们不会讨论这种类型的拦截。相反,我们将介绍在托管 Bean 中使用的拦截器模式,即 EJB 拦截器、CDI Bean 拦截器以及一种称为装饰器的业务逻辑拦截器。
简要介绍 CDI 和 Bean
我们假设读者已经了解围绕 CDI 的基本特征,以及其主要目标和用途。然而,强调 CDI 的一些定义和方面非常重要。
CDI 技术是 JEE 平台的一个支柱,它存在于其大多数服务中。这意味着这些服务以某种方式依赖于 CDI 机制——CDI 与 EE 规范中的 bean 概念密切相关,因此让我们看看 bean 实际上是什么。
豆类
bean 的概念相当通用。在这里,我们不是在谈论 JavaBeans 的概念,即带有 getters 和 setters 访问方法的概念。相反,我们是在谈论 bean 作为 Web 组件或业务组件的概念。在这种情况下,我们会遇到几种类型的 bean,例如 Web 和 JEE 相关的 Java 类,如 EJB bean 和 JSF 托管 bean。甚至还有一些 JEE 规范之外的第三方框架(如 Spring)对 bean 有不同的概念。JEE bean 的概念被称为托管 bean,它由容器管理,需要程序员很少的干预,并且具有明确的生命周期。此外,托管 bean 可以提供拦截其执行和生命周期回调方法的机制,并且可以被注入到其他 bean 或 JEE 对象中。
因此,具有良好定义的生命周期上下文的对象可以被视为一个 bean 或一个托管 bean。换句话说,托管 bean 是容器构建和销毁(作为管理其生命周期的一部分)的组件。CDI 为几种不同类型的 bean 提供了内置支持。以下 Java EE 组件可以注入:
-
托管 bean:EJB 会话 bean、使用
@ManagedBean注解的类、装饰器以及符合 CDI 规则成为托管 bean 的类 -
Java EE 资源:可以从组件环境命名空间引用的资源
-
任意对象:由生产者方法和字段返回的对象
托管 bean 也可以注入其他 bean。
注意:JSF 规范还描述了一种称为托管 bean 的技术。这些托管 bean 与我们这里描述的不同。Java EE 基础托管 bean 是 JSF 的一个泛化,并不限于 Web 模块。
JEE 平台还有其他对象(如 servlet 和拦截器对象)不被归类为 JEE bean,并且可能通过 CDI 机制注入 bean。可能注入 bean 的 JEE 平台组件如下:
-
Servlets(包括 Servlet 过滤器和服务监听器)
-
JSP 标签处理器和标签库事件监听器
-
JSF 作用域托管 bean
-
JAX-RS 组件
-
JAX-WSS 服务端点和处理器
-
WebSocket 端点
CDI 中的托管 bean
有几种方法可以将 Java 类声明为托管 bean。例如,如果我们使用@ManageBean注解一个类,它就被定义为托管 bean。然而,CDI 规范坚持认为,一个类必须根据某些标准被视为托管 bean。这些标准如下:
-
它不是一个非静态内部类
-
它是一个具体类,或者它被
@Decorator注解 -
它不是一个 EJB 组件
-
它不实现
javax.enterprise.inject.spi.Extension -
它要么有一个无参构造函数,要么声明了一个带有
@Inject注解的构造函数
松散耦合
在 JEE 上下文中使用模式的一些目标是为了减少不同层之间——甚至同一层——的类之间的耦合,以及代码重用(即使用提供一定功能的现有代码)。有了这两个目标,我们能够提高软件维护和质量。但如何做到呢?
松散耦合允许程序员在最小影响其他层的情况下,在一个层中修复错误或开发新的功能。此外,随着每一段测试过的代码,可以从应用程序的不同部分调用特殊豆或对象的方法,从而提高软件组织和清晰度。因此,软件质量得到提升,开发时间减少。
如我们所知,松散耦合使代码更容易维护。
实际上,CDI 提供了松散耦合。此外,CDI 使用资格注解而不是字符串标识符,以强大、安全的方式工作。
在 JEE 中,使用事件、拦截器和装饰器提供松散耦合,如下列所示:
-
事件通知:在事件生成客户端和事件监听器(观察者)之间建立解耦
-
拦截器:将技术关注点与业务逻辑分离
-
装饰器:允许扩展业务关注点(向现有业务功能添加业务功能)
在本章中,我们将看到拦截器和装饰器如何实现这一目标。
JEE 平台中的拦截器
一个 Oracle 教程将拦截器定义为如下:
"拦截器是一个用于在关联的目标类中插入调用方法或生命周期事件的类。"
如前所述,拦截器通常用于技术任务,称为横切任务。这包括审计、记录、控制统计等。这些任务与业务逻辑分开,并且可能在整个应用程序中重复。因此,我们可以将拦截器代码放入一个与我们要拦截的目标类不同的单独类中,以提高代码维护性。
拦截器的概念首次在 JEE5 中引入,并且最初仅用于 EJB(会话 Bean 和消息驱动 Bean)。随着 JEE6 中上下文和依赖注入(CDI)的引入,拦截器被扩展到所有托管 Bean——即满足 CDI 托管 Bean 的 Bean。
拦截器方法在关联的目标类上被调用。
我们可以在目标类内部定义一个拦截器作为拦截器方法,或者我们可以在一个单独的类(称为拦截器类)中定义拦截器,该类包含拦截器方法。拦截器方法总是在目标类方法被注解为需要拦截并被调用时,或者当生命周期回调方法被拦截(例如在 bean 构造或销毁之前)时被调用。需要注意的是,对于简单的应用,我们可以在目标类中放置拦截器。然而,对于更复杂的应用,或者当应用变得复杂时,我们应该将拦截器放在一个单独的类中。
每个你想拦截的元素被称为建议。我们说拦截器装饰了建议,每次建议被调用时,都会执行拦截器代码(如果存在)。此代码执行点的位置被称为切入点。
当建议目标类的方法被调用,或者当有生命周期事件并且与事件相关的生命周期回调方法被调用时,将调用关联的目标类上的拦截器方法。
以下图显示了拦截器序列图:

此序列图显示了拦截器链。当客户端调用业务方法并且与之关联有一个拦截器链时,首先调用第一个拦截器并执行某些操作(即,执行此第一个拦截器方法的代码),然后显式调用第二个拦截器,依此类推。这个过程一直持续到链中的最后一个拦截器调用托管 bean 的业务方法。我们将在稍后看到这个调用是如何进行的。现在,我们可以这样说,拦截器链中的每个元素以相同的方式调用下一个元素,直到最后一个元素——即托管 bean 本身的业务方法被调用。
我们可以使用注解来定义拦截器类和拦截器方法,或者,作为替代,我们可以定义应用的部署描述符。然而,在本节中,我们只将涵盖注解的使用。以下表格显示了在拦截器方法中使用的注解,这些注解定义了拦截的条件或拦截发生的时间:
| 拦截器注解 | 描述 |
|---|---|
javax.interceptor.AroundConstruct |
这定义了一个在目标类构造函数被调用时接收回调的拦截方法 |
javax.interceptor.AroundInvoke |
这定义了一个拦截方法,当目标类中被标记为需要通过注解进行拦截的方法被调用时执行 |
javax.interceptor.AroundTimeout |
这定义了一个拦截超时方法的拦截器方法 |
javax.annotation.PostConstruct |
这定义了一个用于后构造生命周期事件的拦截器方法 |
javax.annotation.PreDestroy |
这定义了一个用于预销毁生命周期事件的拦截器方法 |
EJB 拦截器实现
在本节中,我们将查看一个原始 EJB 拦截器实现的示例。
假设存在一个无状态会话 EJB,例如AcademicFacadeImpl,以及一个名为testReview的业务方法。这个方法负责安排学生测试复习。假设我们想从统计学的角度知道哪些学生提出了最多的测试复习请求。我们可以在业务代码中这样做,但这不是我们想要的。相反,我们希望将统计逻辑与业务逻辑分开——我们希望有一个统计拦截器。
首先,我们将创建一个名为AcademicFacadeImpl的 EJB 目标类(这个类已经在上一章中创建过)。一开始,没有拦截器引用,如下代码所示:
@Stateless
@LocalBean
public class AcademicFacadeImpl {
...
...
// this method will be intercepted for some statistical
// interceptor:
public void requestTestReview (@Observes TestRevisionTO
testRevisionTO) {
System.out.println("enrollment : " +
testRevisionTO.getEnrollment());
LocalDateTime dateTime = scheduleTestReview (testRevisionTO);
// send an email with the schedule date for review:
sendEmail (testRevisionTO, dateTime);
}
}
因此,我们使用前表中列出的拦截器注解之一来定义一个拦截器。这个拦截器可以在目标类内部或单独的拦截器类中找到。以下代码展示了在目标类中声明的@AroundInvoke拦截器方法:
@Stateless
public class AcademicFacadeImpl {
...
...
@AroundInvoke
public Object statisticMethod (InvocationContext invocationContext) throws Exception{
...
}
}
或者,我们可以使用一个单独的拦截器类。我们可以应用@Interceptor注解,但拦截器类不必注解。拦截器类必须有一个public和无参数的构造函数。以下代码展示了带有@Interceptor和@AroundInvoke拦截器方法的拦截器类:
@Interceptor
public class StatisticInterceptor implements Serializable {
@Inject
private Event<String> event;
@AroundInvoke
public Object statisticMethod (InvocationContext invocationContext) throws Exception{
System.out.println("Statistical method : "
+ invocationContext.getMethod().getName() + " "
+ invocationContext.getMethod().getDeclaringClass()
);
// get the enrollment:
TestRevisionTO testRevisionTO =
(TestRevisionTO)invocationContext.getParameters()[0];
System.out.println("Enrolment : " +
testRevisionTO.getEnrollment());
// fire an asynchronous statistical event:
event.fire (testRevisionTO.getEnrollment());
return invocationContext.proceed();
}
}
在这个例子中,我们使用 CDI 事件 API。这个 API 用于实现事件触发机制。基本上,它使用一个 bean 来触发事件,一个或多个 bean 观察这个触发。这个机制是从一个Event类对象实现的,如下代码所示。我们使用String类型来限定事件,但我们可以使用任何其他类型,或者使用限定符来限定我们的事件。
我们还可以看到statisticMethod方法的参数InvocationContext对象。这个对象提供了关于拦截调用上下文的信息。此外,这个对象有控制链式拦截器的方法。例如,proceed方法调用拦截器链中的下一个拦截器,或者业务对象的拦截方法。我们将在后面了解更多关于InvocationContext的信息。
接下来,我们必须使用@Interceptors注解定义至少一个拦截器。我们可以在目标类的类或方法级别定义拦截器,通过指定我们是否想拦截所有业务方法或只是某些特定的业务方法。以下代码仅展示了在类级别声明的一个拦截器:
@Stateless
@Interceptors({StatisticInterceptor.class})
public class AcademicFacadeImpl
以下代码声明了相同的拦截器,但位于方法级别:
@Stateless
public class AcademicFacadeImpl{
...
@Interceptors({StatisticInterceptor.class})
public void requestTestReview (@Observes TestRevisionTO
testRevisionTO) {
...
}
}
我们将在方法级别放置@Interceptors。这是因为我们不想拦截所有其他 EJB 方法。
最后,我们必须创建一个将观察统计事件的类。这个类将处理统计问题。如下代码所示:
public class StatisticalFacadeImpl {
public void control (@Observes String enrolment ) {
System.out.println("This enrolment is asking for a test
revision : " + enrolment);
// Here we can persist this information, for example.
}
}
当调用AcademicFacadeImpl类的requestTestReview方法时,它会被拦截(此方法带有拦截器标记),因此会调用StatisticInterceptor类的statisticMethod方法。在执行此方法期间,会触发一个统计事件。这里的目的是对业务方法的执行进行异步的统计控制。
作为一种替代方案,我们可以将StatisticalFacadeImpl转换为 EJB,并使用@Asynchronous注解control方法。结果,我们就不必触发统计事件,而是可以调用以下异步方法:
@Stateless
@LocalBean
public class StatisticalFacadeImpl {
@Asynchronous
public void control (String enrolment ) {
System.out.println("This enrolment is asking for a test
revision : " + enrolment);
}
}
拦截方法调用
如果我们想让一个特定方法成为切入点,我们必须使用@AroundInvoke注解它。@AroundInvoke方法(或切入点方法)必须返回一个对象,并且必须有一个InvocationContext类型的参数。此外,切入点方法应该抛出异常。
可以调用InvocationContext方法来访问有关当前上下文的信息,例如切入点的名称、方法注解、方法参数等。然而,重要的是要注意,在拦截情况下,只有当@AroundInvoke方法调用proceed方法时,目标类方法才会被调用。因此,@AroundInvoke方法必须调用proceed方法来调用目标类方法。在拦截器链的情况下,每次调用 EJB 业务方法时,每个链拦截器的每个@AroundInvoke方法都会按照它们配置的顺序(通过@Interceptors)被调用,直到调用目标类方法。
这些@AroundInvoke拦截器方法可以具有任何访问级别修饰符——公共的、受保护的、私有的或包级别的。然而,方法不能是静态的或最终的。请记住,每个类只允许有一个@AroundInvoke拦截器方法。
AroundInvoke拦截器方法具有以下形式:
@AroundInvoke
[public, protected, private or package-level ] Object method-name (InvocationContext) throws Exception { ...}
每个类只允许有一个AroundInvoke拦截器方法。
另一个需要记住的重要事情是,AroundInvoke拦截器可以与目标方法具有相同的事务上下文。
拦截器类和多个方法拦截器
在单独的类中拥有拦截器可以通过在类之间提供清晰的职责划分来提高代码维护性,这意味着类变得更加紧密。此外,拦截器类可能被注入资源和托管 Bean。
另一个值得注意的重要因素是,拦截器方法属于同一个 EJB 事务上下文。因此,每次我们想要拦截业务逻辑以执行技术任务,例如日志记录或统计持久化时,我们必须小心。例如,我们不希望在统计控制期间抛出任何错误,从而生成业务事务回滚。
在这种情况下,我们应该执行异步统计控制。这将保证任何最终错误都会在一个新的事务中发生,与当前事务分离。因此,在编码之前实现良好的软件设计和强大的业务知识是至关重要的。
拦截器类可以注入资源和托管 Bean。
@Interceptors 注解用于将一个或多个拦截器注入到 EJB 目标类或 EJB 目标方法中。请参见以下代码:
@Interceptors({PrimaryInterceptor.class, SecondaryInterceptor.class,
LastInterceptor.class})
public void method(String prm) { ... }
拦截器按照在 @Interceptors 注解中定义的顺序被调用。然而,如果拦截器也在部署描述符中定义,则可以覆盖此顺序。因此,如果存在拦截器链,拦截器将按照 @Interceptors 中指定的顺序通过调用 InvocationContext.proceed 方法来调用下一个拦截器。这将继续,直到调用业务方法。
拦截器类在 EJB 目标类中被引用,这导致目标类发现拦截器类。这种情况在两个类之间建立了耦合。我们将在稍后介绍 CDI 拦截器机制如何纠正这种情况。
拦截生命周期回调事件
用于生命周期回调事件(AroundConstruct、PostConstruct 和 PreDestroy)的拦截器方法遵循与之前描述的拦截器实现模型相同的模式,关于位置方面。这可以在目标类、拦截器类或两个类中定义。在本章中,我们将仅看到 PostConstruct 和 PreDestroy 回调事件的示例。
如前表中的拦截器注解所述,带有 @PostConstruct 注解的方法用于拦截 PostConstruct 生命周期事件,而带有 @PreDestroy 注解的方法用于拦截 PreDestroy 生命周期事件。拦截器类具有与目标类相同的生命周期。这意味着当目标类的一个实例被创建时,目标类中每个拦截器类的声明都会创建一个拦截器对象。
然而,还有另一种方法可以解决这个问题。我们可以为生命周期事件定义回调方法,并为这些回调方法定义拦截器方法。因此,我们真正想要拦截的是回调方法。目标类内部的生命周期事件回调方法(或目标类侧的生命周期事件拦截器)具有以下语法:
void someMethod() { ... }
这的例子可能是以下内容:
@PostConstruct
void initialize() { ... }
@PreDestroy
void finalize () { ...}
在拦截器类中定义的此回调方法(或在拦截器类侧的生命周期事件拦截器)的拦截器方法具有以下语法:
void someOtherMethod(InvocationContext ctx) { ... }
一个例子可能是以下内容:
@PostConstruct
void initialize (InvocationContext ctx){...}
@PreDestroy
void cleanup(InvocationContext ctx) { ... }
我们可以定义生命周期事件(AroundConstruct、PostConstruct和PreDestroy)的回调方法,我们也可以为这些回调方法定义拦截器方法。
在测试各种情况时,我们注意到一些有趣的事情。
对于类级别的@Interceptors,如果目标类和拦截器类中定义了同一生命周期事件的两个方法,那么只调用回调拦截器。然而,如果在回调拦截器内部显式调用了InvocationContext.proceed,则目标类中的回调方法也会被调用。让我们看看当我们用@PostConstruct注解同时注解 EJB 类和Interceptor类时会发生什么。
下面的代码显示了 EJB AcademicFacadelImpl类的initialize方法(它有一个@PostConstruct注解):
public AcademicFacadelImpl () {
System.out.println ("creating bean.");
}
@PostConstruct
public void initialize () {
System.out.println ("post construct of bean.");
}
下面的代码显示了Interceptor类的initialize方法(它有一个@PostConstruct注解):
@PostConstruct
public void initialize (InvocationContext ctx) {
System.out.println ("intercepting post construct of bean.");
}
我们将收到以下响应:
creating bean.
intercepting post construct of bean.
然而,我们也可以在Interceptor类中这样做,如下所示:
@PostConstruct
public void initialize (InvocationContext ctx) {
System.out.println ("intercepting post construct of bean.");
ctx.proceed();
}
通过这样做,我们将收到以下响应:
creating bean.
intercepting post construct of bean.
post construct of bean
对于方法级别的@Interceptors,只有回调方法会被调用,无论在拦截器类中是否调用了InvocationContext.proceed。
CDI 拦截器实现
在介绍 CDI 机制的同时,基于之前看到的 Java EE 拦截器的功能,建立了一种新的拦截器方法。使用 CDI 引擎,您可以拦截 CDI 管理的业务方法,而不仅仅是 EJB 方法。在 CDI 上下文中,拦截器的一个关键区别是拦截器注入到管理 bean 的方式。我们现在有了拦截器绑定类型的概念。这是一个注解,用于限定 bean 所需的拦截器类型。
假设我们想要创建一个用于日志记录的拦截器。在这种情况下,我们最初会创建一个名为@Loggable的拦截器绑定类型,其描述如下:
@Inherited
@InterceptorBinding
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface Loggable {
}
然后,我们将创建一个拦截器类。拦截器类同时注解了拦截器绑定类型和@Interceptor注解。在这个例子中,我们创建了LoggedInterceptor类,如下面的代码所示:
@Loggable
@Interceptor
public class LoggedInterceptor implements Serializable {
@AroundInvoke
public Object logMethod (InvocationContext invocationContext) throws
Exception{
System.out.println("Entering method : "
+ invocationContext.getMethod().getName() + " "
+ invocationContext.getMethod().getDeclaringClass()
);
return invocationContext.proceed();
}
}
@AroundInvoke方法与之前在 EJB 拦截器中描述的方式相同。
此外,@Loggable作为类拦截器的限定符。由于它是一种限定符,因此总是很有趣地将拦截器绑定类型声明为形容词。因此,我们使用单词可记录的,这意味着它适合于日志记录。
一旦我们创建了拦截器绑定类型和拦截器类,我们就可以用绑定类型注解整个 bean 或某些单个 bean 方法,以拦截所有 bean 方法或特定方法。看看以下示例:
@Loggable
public class ProfessorDAO implements Serializable {
...
}
对于特定方法,我们将使用以下方法:
public class ProfessorDAO implements Serializable {
...
@Loggable
public Professor findByName (String name) {
...
}
}
要调用拦截器 CDI,必须在beans.xml中指定,如下面的代码所示:
<interceptors>
<class>academic.interceptors.LoggedInterceptor</class>
</interceptors>
一个应用程序可以使用多个拦截器。如果一个应用程序使用了多个拦截器,那么拦截器的调用顺序将按照beans.xml文件中指定的顺序进行。
CDI 拦截器和过去的 EJB 拦截器之间存在根本性的区别。CDI bean 对拦截器实现类一无所知;它只知道拦截器绑定类型。这种方法导致了松耦合。
让我们使用与 EJB 拦截器相同的示例。然而,这次我们将使用 CDI 拦截器机制。我们将使用相同的AcademicFacadeImpl EJB,但我们将使用一个名为Statistic的拦截器绑定类型,并用此绑定类型注解StatisticInterceptor类。
让我们看看以下代码:
@Inherited
@InterceptorBinding
@Retention (RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Statistic {
}
@Statistical
@Interceptor
public class StatisticInterceptor implements Serializable {
@Inject
Event<String> event;
@AroundInvoke
public Object statisticMethod (InvocationContext invocationContext)
throws Exception{
System.out.println("Statistical method : "
+ invocationContext.getMethod().getName() + " "
+ invocationContext.getMethod().getDeclaringClass()
);
// get the enrolment:
TestRevisionTO testRevisionTO = (TestRevisionTO)invocationContext.getParameters()[0];
System.out.println("Enrollment : " + testRevisionTO.getEnrollment());
// fire an asynchronous statistical event:
event.fire (testRevisionTO.getEnrollment());
return invocationContext.proceed();
}
}
我们将使用与StatisticalFacadeImpl相同的 EJB 来处理统计元素,如下面的代码所示:
/**
* Session Bean implementation class StatisticalFacadeImpl
*/
@Stateless
public class StatisticalFacadeImpl {
/**
* Default constructor.
*/
public StatisticalFacadeImpl() {
// TODO Auto-generated constructor stub
}
public void control (@Observes String enrolment ) {
System.out.println("This enrolment is asking for a test revision : " + enrolment);
}
}
最后,我们将使用@Statistic注解我们的业务方法(或类)。在这种情况下,我们将注解一个业务方法:
**
* Session Bean implementation class AcademicFacadeImpl
*/
@Stateless
public class AcademicFacadeImpl {
...
...
@Statistic
public void requestTestReview (@Observes TestRevisionTO testRevisionTO) {
System.out.println("Enrollment : " + testRevisionTO.getEnrollment());
LocalDateTime dateTime = scheduleTestReview (testRevisionTO);
sendEmail (testRevisionTO, dateTime); // send an email with the schedule date for the test review:
}
...
...
}
每当客户端调用带有@Statistic注解的 EJB 方法(或任何 CDI 管理的 bean 的任何被拦截的方法)时,首先调用StatisticInterceptor类的@AroundInvoke方法。在我们的示例中,@AroundInvoke方法触发一个异步事件。这种方式是故意设置的,以便统计控制不参与业务流程。
装饰器
装饰器是一种模式,它使得在运行时动态地扩展特定功能性的业务逻辑成为可能。装饰器模式作为一个业务组件包装器,拦截负责执行功能的call方法。换句话说,这个模式通过包装相同的对象来装饰原始业务对象,同时提供额外的功能,同时保持现有功能完整。装饰器模式是一种结构型设计模式。
这种模式在运行时动态地改变对象的功能方式,而不影响现有对象的功能。简而言之,这个模式通过包装对象来向对象添加额外的行为。
装饰器模式
装饰器模式是 GoF 结构模式中最常用的模式之一。其类图如下所示:

该模式的构成元素在《设计模式》:可复用面向对象软件元素(GoF 书籍)中进行了描述,并在下表中展示:
Component |
这定义了可以动态添加职责的对象的接口 |
|---|---|
ConcreteComponent |
这定义了一个对象,可以附加额外的职责 |
Decorator |
这维护了对组件对象的引用,并定义了一个符合组件接口的接口 |
ConcreteDecorator |
这向组件添加职责 |
我们可以在编译时使用继承或组合来扩展业务类或域的基本行为。然而,我们无法在运行时这样做。装饰器模式用于在运行时扩展功能。
为了更好地解释装饰器模式,让我们从一个学术领域的例子开始。想象一下,我们有一个名为 BasicEngineering 的域类,并且这个类有一个执行列出基本工程所使用的学科的任务的方法。让我们假设我们还有两个其他工程模型——机械工程和电气工程。每个模型都有自己的学科。因此,每个工程模型都有一个基本学科的列表加上其自己的特定学科的列表。
以下类图显示了这种安排:

我们可以在经典的装饰器模式中实现这个模式,但这不是我们的目标。实际上,我们不需要以简单的方式实现它,因为 JEE 技术已经为我们提供了一个使用 CDI 技术实现装饰器模式的机制。我们将使用 @Decorator 注解来实现这个模式。
JEE 场景中的装饰器
首先,我们创建一个 Engineering 接口,该接口具有我们想要装饰的业务方法。然后,我们创建一个名为 BasicEngineering 的抽象类,它扩展了 Engineering。如下代码所示,我们可以使用两个具体装饰器来创建一个抽象的 EngineeringDecorator,或者我们可以创建两个直接实现 Engineering 的抽象装饰器类。没有义务保持装饰器类是具体的。
为了简化,让我们创建两个装饰器类为抽象的,而不需要从基本装饰器类扩展它们。因此,ElectronicDecorator 和 MechanicalDecorator 类都将装饰 Engineering 对象(ElectronicEngineering 和 MechanicalEngineering)。
以下代码显示了在此示例中使用的模型类:
public interface Engineering {
List<String> getDisciplines ();
}
public class BasicEngineering implements Engineering {
@Override
public List<String> getDisciplines() {
return Arrays.asList("d7", "d3");
}
}
@Electronic
public class ElectronicEngineering extends BasicEngineering {
...
}
@Mechanical
public class MechanicalEngineering extends BasicEngineering {
...
}
我们可以看到,ElectronicEngineering 和 MechanicalEngineering 类分别被 @Electronic 和 @Mechanical 装饰器修饰。我们可以看到,通过限定符,我们可以识别出将要装饰的对象类型。因此,MechanicalDecorator 装饰任何具有 @Mechanical 限定符的 Engineering 对象,而 ElectronicDecorator 装饰任何具有 @Electronic 限定符的 Engineering 对象。
装饰器实现
以下代码显示了MechanicalDecorator和ElectronicDecorator装饰器类:
@Decorator
public abstract class MechanicalDecorator implements Engineering {
@Mechanical
@Any
@Inject
@Delegate
Engineering engineering;
@Override
public List<String> getDisciplines() {
System.out.println("Decorating Mechanical Engineering");
List<String> disciplines = new ArrayList<>
(engineering.getDisciplines());
disciplines.addAll (Arrays.asList("d31", "d37", "d33", "d34",
"d32"));
return disciplines;
}
}
@Decorator
public abstract class EngineeringDecorator implements Engineering {
@Electronic
@Any
@Inject
@Delegate
Engineering engineering;
@Override
public List<String> getDisciplines() {
System.out.println("Decorating Electronic");
List<String> disciplines = new ArrayList<>
(engineering.getDisciplines());
disciplines.addAll (Arrays.asList("d21", "d27", "d23", "d24",
"d22"));
return disciplines;
}
}
我们可以看到Engineering对象被注解为@Delegate。这是将被装饰的业务对象。当被调用时,getDisciplines业务方法会将调用传递给装饰器的getDisciplines方法。在这个时候,装饰器负责处理这种情况。装饰器的getDisciplines方法调用代理对象的getDisciplines方法并添加装饰学科。这意味着它通过添加特定的学科来扩展基本功能。
同样重要的是要记住,@Electronic限定符标识了被装饰的对象。因此,ElectronicDecorator装饰了@Electronic对象,而MechanicalDecorator装饰了@Mechanical对象。
有理由怀疑为什么getDisciplines方法不能放入每个模型类中。根据业务类型的不同,这种方法可能更好。然而,假设学科可能有很大的差异,并且我们有大量的工程模型。在这种情况下,事情可能会变得更加复杂,使用装饰器可以为已经建立的东西添加功能。
装饰器可以声明为抽象类,这样它就不必实现接口的所有业务方法。
此外,为了在 CDI 上下文中调用装饰器,它必须在beans.xml文件中指定,就像拦截器一样。因此,我们的示例装饰器指定如下:
<decorators>
<class>book.chapter3.decorator.ElectronicDecorator</class>
<class>book.chapter3.decorator.MechanicalDecorator</class>
</decorators>
如果应用程序使用多个装饰器,则装饰器按在beans.xml文件中指定的顺序调用。
如果应用程序既有拦截器又有装饰器,则首先调用拦截器。
装饰器必须在beans.xml文件中像拦截器一样指定。装饰器按在beans.xml文件中指定的顺序调用。
实际上,我们可以创建另一个名为BasicEngineeringDecorator的装饰器,它将负责装饰任何Engineering对象。然后我们只需简单地输入@any限定符来限定Engineering对象。当然,我们也应该担心顺序。这应该是首先调用的因素。因此,装饰器在beans.xml文件中按如下顺序列出:
<decorators>
<class>book.chapter3.decorator.BasicEngineeringDecorator</class>
<class>book.chapter3.decorator.ElectronicEngineeringDecorator</class>
<class>book.chapter3.decorator.MechanicalEngineeringDecorator</class>
</decorators>
摘要
在本章中,我们了解到拦截器和装饰器是 JEE 平台提供面向方面编程的平台。拦截器用于在关联的目标类中插入某些方法调用或生命周期事件的调用。拦截器负责处理在整个应用程序中重复出现的技术任务,称为横切任务,例如日志记录、审计和异常处理。这些任务与业务逻辑分开,将拦截器放在单独的类中以便于维护是一个好主意。
我们学习了经典拦截器机制在 EJB 中的应用,以及 CDI 检查器机制,它可以拦截任何托管 Bean,而不仅仅是 EJB 管理的 Bean。
当拦截器负责技术任务时,我们可以通过装饰器向现有的业务逻辑中添加功能。我们了解到装饰器模式是一种众所周知的结构型设计模式。装饰器是业务类的一种拦截器,当我们想要在不破坏业务类的前提下为其添加功能时,就会使用它。
使用拦截器和装饰器促进了低耦合和易于维护。然而,我们必须谨慎使用它们。例如,关于装饰器,我们应该避免过度地在应用程序中扩散它们。过度的分散化可能会产生相反的效果,并恶化代码维护。
拦截器属于它所拦截的业务类的相同事务上下文。由于拦截器只处理技术问题,与之相关的任务不能影响业务逻辑的执行。因此,在技术任务中产生的错误不能抛给业务逻辑。同样地,这些任务的执行时间不能组成业务逻辑的执行时间。
第六章:响应式模式
在本章中,我们将探讨响应式模式的概念和实现,探讨我们如何使用它们来实现更好的应用程序。我们还将涵盖响应式编程概念,重点关注它们如何有助于应用程序开发。阅读本章后,我们将能够使用响应式模式,并采用 Java EE 8 的最佳实践。
本章将探讨以下主题:
-
解释响应式编程的概念
-
解释 CDI 中事件的概念
-
在 CDI 中实现事件
-
解释异步 EJB 方法的概念
-
实现异步 EJB 方法
-
解释异步 REST 服务的概念
-
实现异步 REST 服务
很长时间以来,应用程序都以同步方式处理所有请求。在同步过程中,用户请求资源并等待其响应。当此类过程开始时,每个步骤都紧随前一个步骤之后执行。
在以下图中,我们可以看到在同步方式下执行的工作流程过程:

在同步过程中,对函数或资源的每次调用都以逐步、顺序的方式进行。然而,一些任务执行时间较长,从而长时间阻塞此过程。一个可能需要长时间执行的示例任务是当应用程序从磁盘或数据源读取数据时的 I/O 请求。
随着 Web 应用程序访问量的增长,许多 Web 应用程序需要能够接收和处理大量请求,以响应这些请求。因此,同步处理开始遇到响应大量请求和快速构建响应的问题。为了解决这个问题,Web 应用程序开始通过异步过程工作,使得应用程序能够快速构建响应。
在异步过程中,对函数或资源的调用可以并行进行,无需等待一个任务结束即可执行下一个任务。正因为如此,执行 I/O 请求不会延迟下一个任务的执行——它们可以同时进行。
响应式编程是一种以函数形式开发应用程序的编程风格。在这里,开发发生在请求者发送的异步数据流中。此外,我们可以有一个资源与数据流反应,因为数据流被接收。每个任务然后作为一个函数工作,并且不处理其作用域之外的变量,这使得该函数能够在并行方式下执行多次而不产生任何副作用。
在响应式编程中,我们有对事件做出反应的元素,然后当用户请求资源时,它启动一个事件来操作数据流。当这个事件开始时,一个元素或任务对数据流做出反应并处理其算法。这使得 Web 应用程序能够处理大量数据并轻松扩展。这个范例与以下四个概念一起工作:
-
Elastic: 这是对需求做出反应。应用程序可以使用多核和多台服务器来处理请求。
-
弹性: 这是对故障做出反应。应用程序可以响应并从软件、硬件和网络中的故障和错误中恢复。
-
消息驱动: 这是对事件做出反应。应用程序由异步和非阻塞的事件管理器组成,而不是由多个同步线程组成。
-
响应式: 这是对用户做出反应。应用程序提供实时丰富的迭代。
Java EE 8 提供了工具,允许开发者在其应用程序中使用响应式编程。其中一些工具是 CDI 中的事件、异步 EJB 方法和异步 REST 服务。
解释 CDI 中事件的概念
随着响应式编程在开发环境中的增长,Java 语言和 Java EE 需要创建工具,以允许开发者使用响应式编程来开发系统。在 Java EE 上,引入了一些解决方案,使得可以使用函数式风格并创建异步过程。其中之一是 CDI 中的事件;这个解决方案可以使用 CDI 启动同步和阻塞事件,或异步和非阻塞事件。
在 CDI 中,事件是 Java EE 使用观察者模式构建的解决方案,使得开发并启动一个事件成为可能,以便将处理事件的组件分离,作为一个同步和阻塞或异步和非阻塞的过程。这个分离的任务是一个观察者,它对其使用数据启动的事件做出反应。在本章中,我们将重点关注异步 CDI,使用 CDI 中的事件来启动异步事件。
实现 CDI 中的事件
作为在 CDI 中实现事件的示例,我们考虑异步 CDI 并想象一个场景,在这个场景中我们想要创建一个应用程序,使其能够上传三种类型(或扩展名)的文件——这包括 ZIP、JPG 和 PDF 扩展名。根据接收到的请求扩展名,目的是启动一个事件,一个观察者将使用异步过程将其文件保存到磁盘上。每个扩展名都将有一个观察者,它将有一个算法使得文件能够保存到磁盘上。为了开发这个示例,我们有以下类:
-
FileUploadResource: 这是一个表示接收所有请求以上传并根据文件扩展名启动相应事件的类的资源。 -
FileEvent: 这是一个包含文件数据的 Bean,并将其发送到事件中。 -
FileHandler:这是所有观察者的接口。在这个例子中,所有对FileEvent做出响应的类都需要实现FileHandler。 -
JpgHandler:这是一个将 JPG 文件保存到磁盘上的FileHandler实现。这个类是一个观察者,它对启动到 JPG 文件的FileEvent做出响应。 -
PdfHandler:这是一个FileHandler的实现,它将 PDF 文件保存到磁盘上。这个类是一个观察者,它对启动到 PDF 文件的FileEvent做出响应。 -
ZipHandler:这是一个将 ZIP 文件保存到磁盘上的FileHandler实现。这个类是一个观察者,它对启动到 ZIP 文件的FileEvent做出响应。 -
Jpg:这是一个用于建立JpgHandler观察者需要响应事件的限定符。 -
Pdf:这是一个用于建立PdfHandler观察者需要响应事件的限定符。 -
Zip:这是一个用于建立ZipHandler观察者需要响应事件的限定符。 -
FileSystemUtils:这是一个用于处理文件系统问题的实用类。
实现 FileUploadResource 类
FileUploadResource是一个资源类,它使用 JAX-RS 创建一个 RESTful 服务,用于上传具有 JPG、PDF 和 ZIP 扩展名的文件。在以下代码中,我们有用于选择正确观察者以响应事件的限定符代码以及FileUploadResource的代码:
事件上发送的 bean
FileEvent是一个发送到事件的 bean——观察者将接收这个:
import java.io.File;
public class FileEvent {
private File file;
private String mimeType;
public FileEvent(){}
public FileEvent(File file, String mimeType){
this.file = file;
this.mimeType = mimeType;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
}
选择用于响应事件的JpgHandler观察者的限定符
在以下代码中,我们有Jpg限定符,用于定义正确的事件处理器:
import javax.inject.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
public @interface Jpg {
}
选择用于响应事件的PdfHandler观察者的限定符
在以下代码中,我们有Pdf限定符,用于定义正确的事件处理器:
import javax.inject.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
public @interface Pdf {
选择用于响应事件的ZipHandler观察者的限定符
在以下代码中,我们有Zip限定符,用于定义正确的事件处理器:
import javax.inject.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
public @interface Zip {
}
FileUploadResource 类
在以下代码块中,我们有JFileUploadResource类,它使用 JAX-RS 并且是一个 REST 服务:
import javax.enterprise.event.Event;
import javax.enterprise.util.AnnotationLiteral;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import java.io.File;
import java.util.Objects;
@Path("upload")
public class FileUploadResource {
@Inject
Event<FileEvent> fileEvent;
@Consumes("application/pdf")
@POST
public Response uploadPdf(File file){
FileEvent fileEvent = new FileEvent(file, "pdf");
Event<FileEvent> pdfEvent = this.fileEvent.select(new
AnnotationLiteral<Pdf>() {});
pdfEvent.fireAsync(fileEvent)
.whenCompleteAsync((event, err)->{
if( Objects.isNull( err ) )
System.out.println("PDF saved");
else
err.printStackTrace();
});
return Response.ok().build();
}
@Consumes("image/jpeg")
@POST
public Response uploadJpg(File file){
FileEvent fileEvent = new FileEvent(file, "jpg");
Event<FileEvent> jpgEvent = this.fileEvent.select( new
AnnotationLiteral<Jpg>() {} );
jpgEvent.fireAsync(fileEvent)
.whenCompleteAsync((event, err)->{
if( Objects.isNull( err ) )
System.out.println( "JPG saved" );
else
err.printStackTrace();
});
return Response.ok().build();
}
@Consumes("application/zip")
@POST
public Response uploadZip( File file){
FileEvent fileEvent = new FileEvent( file, "zip" );
Event<FileEvent> zipEvent = this.fileEvent.select(new
AnnotationLiteral<Zip>() {});
zipEvent.fireAsync(fileEvent)
.whenCompleteAsync( (event, err)->{
if( Objects.isNull( err ) )
System.out.println( "PDF saved" );
else
err.printStackTrace();
});
return Response.ok().build();
}
}
Event, using an annotation as a qualifier:
Event<FileEvent> zipEvent = this.fileEvent.select(new AnnotationLiteral<Zip>() {});
建立正确事件启动的另一种方法是使用在对象注入点使用@Inject时的限定符,但这种方式下,事件变为静态的,并且由Event对象启动的所有事件都是同一类型。使用select (Annotation... var)方法,我们可以启动动态事件以及其他事件类型。以下是一个具有静态事件类型的Event示例:
@Inject
@Pdf //Qualifier
Event<FileEvent> pdfEvent;
在前面的示例中,pdfEvent将始终向由@Pdf限定符标记的事件处理观察者启动事件。
要启动异步事件,我们需要调用fireAsync(U var)方法,该方法返回CompletionStage。在以下代码块中,我们有一个调用此方法并准备在过程完成后执行回调函数的代码片段:
zipEvent.fireAsync(fileEvent)
.whenCompleteAsync( (event, err)->{
if( Objects.isNull( err ) )
System.out.println( "PDF saved" );
else
err.printStackTrace();
});
实现观察者
当一个启动器启动事件时,一些元素将对此事件做出反应,并使用事件上提供的数据处理一个任务。这些元素被称为 观察者,它们作为观察者模式工作,在对象之间创建一对一的关系。
这发生在其中一个对象是主题,而其他对象是观察者的情况下。然后,当主题对象更新时,所有与该主题对象相关的观察者对象也会更新。
CDI 有一个创建将对其事件做出反应的观察者的机制。在我们的示例中,我们将启动一个事件,创建观察者以对这些事件做出反应,并处理一个任务。为此,我们将创建代表我们的观察者和处理任务的处理器。这些处理器将是实现 FileHandler 接口以及名为 handle(FileEvent file) 的方法的类。请注意,handler(FileEvent file) 方法的参数是 FileEvent 类型。这与前面示例中发送到事件的类型相同。在以下示例中,我们有 FileHandler 接口及其实现的代码:
import java.io.IOException;
public interface FileHandler {
public void handle( FileEvent file ) throws IOException;
}
在以下代码中,我们有一个名为 JpgHandler 的类,它是 FileHandler 的一个实现。这负责在文件系统上保存 JPG 文件。当向 @Jpg 标识符启动事件时,将调用此观察者:
import javax.enterprise.event.ObservesAsync;
import java.io.IOException;
import java.util.Date;
public class JpgHandler implements FileHandler {
@Override
public void handle(@ObservesAsync @Jpg FileEvent file) throws
IOException {
FileSystemUtils.save( file.getFile(),"jpg","jpg_"+ new
Date().getTime() + ".jpg" );
}
}
在前面的代码块中,我们有一个方法处理程序,它带有 @ObservesAsync 注解以及 @Jpg。这是一个 CDI 注解,用于配置此方法以观察 FileEvent 文件,以及 Qualifier 以配置观察者仅对启动到 @Jpg 标识符的事件做出反应。
在以下代码块中,我们有一个名为 PdfHandler 的类,它是 FileHandler 的一个实现,负责在文件系统上持久化 PDF 文件。当向 @Pdf 标识符启动事件时,将调用此观察者:
import javax.enterprise.event.ObservesAsync;
import java.io.IOException;
import java.util.Date;
public class PdfHandler implements FileHandler {
@Override
public void handle(@ObservesAsync @Pdf FileEvent file) throws
IOException {
FileSystemUtils.save( file.getFile(),"pdf","pdf_"+ new
Date().getTime() + ".pdf" );
}
}
在前面的代码中,我们有一个带有 @ObservesAsync 注解以及 @Pdf. 的处理方法。这是一个 CDI 注解,用于配置 handle(FileEvent file) 方法以观察 FileEvent 文件,以及 Qualifier 以配置观察者仅对启动到 @Pdf 标识符的事件做出反应。
在以下代码中,我们有一个名为 ZipHandler 的类,它是 FileHandler 的一个实现,负责在文件系统上保存 ZIP 文件。当向 @Zip 标识符启动事件时,将调用此观察者:
import javax.enterprise.event.ObservesAsync;
import java.io.IOException;
import java.util.Date;
public class ZipHandler implements FileHandler {
@Override
public void handle(@ObservesAsync @Zip FileEvent file) throws
IOException {
FileSystemUtils.save( file.getFile(),"zip","zip_"+ new
Date().getTime() + ".zip" );
}
}
在前面的代码中,我们有一个处理方法,它带有 @ObservesAsync 和 @Zip 注解。这是一个 CDI 注解,用于配置此方法以观察 FileEvent 文件,以及 Qualifier 以配置此观察者仅对启动到 @Zip 标识符的事件做出反应。
解释异步 EJB 方法的概念
向响应这些事件的元素启动事件是一种在开发过程中解决许多类型问题的良好机制。然而,有时有必要在不阻塞进程直到该方法完成执行的情况下调用类方法。
异步 EJB 方法是 EJB 的一种机制,允许客户端调用一个方法,并在方法被调用时立即接收其返回值。方法的返回值由代表异步调用的 Future<T> 对象控制。客户端可以控制异步方法的执行。这些操作可以取消调用方法,检查调用是否完成,检查调用是否抛出异常,以及检查调用是否已取消。
异步 EJB 方法与 CDI 事件之间的区别
CDI 中的事件和异步 EJB 方法具有使任务非阻塞调用的相似特性。此外,客户端可以取消并监控异步过程的调用。然而,异步 EJB 方法与 CDI 事件并不共享所有相同的特性。它们之间的主要区别在于异步 EJB 方法在调用者和被调用者之间是一对一的关系。这是因为当该方法被调用时,它只会处理任务,客户端知道将要处理的方法是什么。而在 CDI 事件中,调用者和被调用者之间的关系是一对多的。这是因为调用者启动了一个事件,一个或多个观察者可以对此做出反应。异步 EJB 方法与 CDI 事件之间的另一个区别是,在 CDI 中,事件与观察者模式一起工作,使得在另一个时间执行回调方法成为可能。异步 EJB 方法不与观察者模式一起工作,并且没有应用回调方法的能力。
实现异步 EJB 方法
在我们的实现示例中,我们将使用与 CDI 事件示例相同的场景。在这里,我们将创建一个应用程序,使其能够上传三种类型(或扩展名)的文件——ZIP、JPG 和 PDF 扩展名。根据接收到的扩展名类型,接收到的文件将保存在文件系统中的相应目录下。为了开发此示例,我们将使用以下类:
-
FileUploadResource: 这是一个表示接收所有上传请求的资源类,并根据文件扩展名调用相应的 EJB。 -
JpgHandler: 这是一个具有异步方法的 EJB,用于处理磁盘上 JPG 文件的保存过程。 -
PdfHandler: 这是一个具有异步方法的 EJB,用于处理磁盘上 PDF 文件的保存过程。 -
ZipHandler: 这是一个具有异步方法的 EJB,用于处理磁盘上 ZIP 文件的保存过程。 -
FileSystemUtils: 这是一个旨在处理文件系统问题的实用工具类。
实现 EJB
要使用异步 EJB 方法,我们需要创建一个会话 bean 并将其配置为具有异步方法。在下面的代码中,我们有一个名为PdfHandler的会话 bean 实现的示例,该 bean 负责在文件系统上保存 PDF 文件:
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.Future;
@Stateless
public class PdfHandler {
@Asynchronous
public Future<String> handler (FileBean file) throws IOException {
return new AsyncResult(
FileSystemUtils.save(
file.getFile(),
"pdf",
"pdf_"+ new Date().getTime() + ".pdf" ));
}
}
在前面的代码块中,我们有一个PdfHandler类,它包含一个handler(FileBean file)方法。此方法使用@Asynchronous注解来配置它为一个异步方法。
以下代码演示了handle(FileBean file)方法的配置:
@Asynchronous
public Future<String> handler (FileBean file) throws IOException {
//Business logic
}
此方法需要返回Future<T>。在我们的示例中,我们返回AsyncResult,它是Future接口的一个实现。在我们的示例中,与Future对象一起返回的数据包含文件系统上文件路径的信息。在下面的代码中,我们有一个Future返回的示例:
return new AsyncResult(
FileSystemUtils.save(
file.getFile(),
"pdf",
"pdf_"+ new Date().getTime() + ".pdf" ));
在下面的代码块中,我们有一个名为JpgHandler的会话 bean 实现的示例,该 bean 负责在文件系统上保存 JPG 文件:
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.Future;
@Stateless
public class JpgHandler {
@Asynchronous
public Future<String> handler (FileBean file) throws IOException {
return new AsyncResult(
FileSystemUtils.save(
file.getFile(),
"jpg",
"jpg_"+ new Date().getTime() + ".jpg" ));
}
}
此方法与PdfHandler方法类似,但将文件保存在另一个目录,并使用另一个文件名模式。在我们的示例中,与Future对象一起返回的数据是文件系统上文件的路径。
在下面的代码中,我们有一个名为JpgHandler的会话 bean 实现的示例,该 bean 负责在文件系统上保存 JPG 文件:
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.Future;
@Stateless
public class ZipHandler {
@Asynchronous
public Future<String> handler (FileBean file) throws IOException {
return new AsyncResult(
FileSystemUtils.save(
file.getFile(),
"zip",
"zip_"+ new Date().getTime() + ".zip" ));
}
}
此方法与PdfHandler和JpgHandler方法类似,但将文件保存在另一个目录,并使用另一个文件名模式。在我们的示例中,与Future对象一起返回的数据是文件系统上文件的路径。
实现 FileUploadResource 类
FileUploadResource是一个资源类,它使用 JAX-RS 创建一个 RESTful 服务来上传具有 JPG、PDF 和 ZIP 扩展名的文件。下面的代码包含FileUploadResource资源类:
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
@Path("upload")
public class FileUploadResource {
@Inject
private PdfHandler pdfHandler;
@Inject
private JpgHandler jpgHandler;
@Inject
private ZipHandler zipHandler;
@Consumes("application/pdf")
@POST
public Response uploadPdf(File file) throws IOException {
FileBean fileBean = new FileBean(file, "pdf");
pdfHandler.handler( fileBean );
return Response.ok().build();
}
@Consumes("image/jpeg")
@POST
public Response uploadJpg(File file) throws IOException {
FileBean fileBean = new FileBean(file, "jpg");
jpgHandler.handler( fileBean );
return Response.ok().build();
}
@Consumes("application/zip")
@POST
public Response uploadZip( File file) throws IOException {
FileBean fileBean = new FileBean( file, "zip" );
zipHandler.handler( fileBean );
return Response.ok().build();
}
}
在下面的代码块中,EJB 的PdfHandler、JpgHandler和ZipHandler使用 CDI 的@Inject注解进行注入。在下面的示例中,我们有注入代码:
@Inject
private PdfHandler pdfHandler;
@Inject
private JpgHandler jpgHandler;
@Inject
private ZipHandler zipHandler;
当向应用程序发送请求时,负责处理请求的方法获取文件并构建一个FileBean。此方法调用异步 EJB 方法来处理此任务。在下面的代码块中,我们有调用异步 EJB 方法的示例。
调用一个异步 EJB 方法来保存 PDF 文件
我们使用以下方式调用异步 EJB 方法。以下代码块通过调用 EJB 方法演示了保存 PDF 文件:
FileBean fileBean = new FileBean(file, "pdf");
pdfHandler.handler( fileBean );
调用一个异步 EJB 方法来保存 JPG 文件
以下代码块通过调用异步 EJB 方法演示了保存 JPG 文件:
FileBean fileBean = new FileBean(file, "jpg");
jpgHandler.handler( fileBean );
调用一个异步 EJB 方法来保存 ZIP 文件
以下代码块演示了通过调用异步 EJB 方法保存 ZIP 文件:
FileBean fileBean = new FileBean( file, "zip" );
zipHandler.handler( fileBean );
解释异步 REST 服务的概念
随着时间的推移,REST 应用程序的数量不断增加,许多 API 已经创建出来,以在各种环境中提供各种类型的服务。与其他应用程序一样,一些 REST 应用程序需要异步过程,并与非阻塞过程一起工作。
异步 REST 服务是一个异步过程,使处理线程更容易。相比之下,在发送到服务器的请求中,可以调用一个新线程来处理非阻塞任务,例如文件系统上的操作。JAX-RS 在客户端 API 和服务器 API 中支持异步处理,但异步 REST 服务是在服务器 API 中实现的。这是因为服务器 API 提供服务。客户端 API 消费服务,因此我们将客户端 API 中的异步处理称为异步 REST 消费。
客户端 API 可以通过异步调用完成,一旦请求完成,就返回一个Future<T>对象,允许客户端控制这个调用并对它执行操作。这可以通过响应式编程来实现,并且可以在请求完成后立即返回一个CompletionState<T>对象。这意味着可以控制调用并选择一个回调方法,根据阶段执行。在下一主题中涵盖的实现示例中,我们将使用响应式编程的调用。
实现异步 REST 服务
作为实现异步 REST 服务的示例,我们将使用 CDI 事件示例中使用的相同场景和异步 EJB 方法示例。在这里,我们将创建一个应用程序,使其能够上传三种类型(或扩展名)的文件——ZIP、JPG 和 PDF 扩展名。根据接收到的扩展名,我们希望接收到的文件被保存在文件系统上的相应目录中。为了开发这个示例,我们有以下类:
-
FileUploadResource:这是一个表示与异步过程一起工作的资源,用于接收所有上传请求,并根据文件扩展名调用相应的 EJB。 -
JpgHandler:这是一个 EJB,提供了一个在磁盘上保存 JPG 文件的方法。 -
PdfHandler:这是一个 EJB,提供了一个在磁盘上保存 PDF 文件的方法。 -
ZipHandler:这是一个 EJB,提供了一个在磁盘上保存 ZIP 文件的方法。 -
FileSystemUtils:这是一个处理文件系统问题的实用类。 -
FileUploadClient:这是一个通过 JAX-RS 实现的客户端 API,它对 REST 服务进行异步调用。
实现 EJBs
在这个阶段,我们将实现PdfHandler、JpgHandler和ZipHandler,这些 EJB 负责处理保存文件逻辑。
在以下代码中,我们有PdfHandler,它负责保存 PDF 文件:
import javax.ejb.Stateless;
import java.io.IOException;
import java.util.Date;
@Stateless
public class PdfHandler {
public String handler (FileBean file) throws IOException {
return FileSystemUtils.save(
file.getFile(),
"pdf",
"pdf_"+ new Date().getTime() + ".pdf" );
}
}
在以下代码块中,我们有JpgHandler,它负责保存 JPG 文件:
import javax.ejb.Stateless;
import java.io.IOException;
import java.util.Date;
@Stateless
public class JpgHandler {
public String handler (FileBean file) throws IOException {
return FileSystemUtils.save(
file.getFile(),
"jpg",
"jpg_"+ new Date().getTime() + ".jpg" );
}
}
在下面的代码块中,我们有ZipHandler,它负责保存 ZIP 文件:
import javax.ejb.Stateless;
import java.io.IOException;
import java.util.Date;
@Stateless
public class ZipHandler {
public String handler (FileBean file) throws IOException {
return FileSystemUtils.save(
file.getFile(),
"zip",
"zip_"+ new Date().getTime() + ".zip" );
}
}
实现 FileUploadResource 类
FileUploadResource是一个资源,旨在允许客户端通过异步请求使用 REST 将文件上传到服务器。这个类使用 JAX-RS 创建 REST 资源,并根据文件扩展名,此资源将保存文件的任务委托给相应的 EJB。在下面的代码块中,我们有FileUploadResource类:
import javax.annotation.Resource;
import javax.enterprise.concurrent.ManagedExecutorService;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@Path("upload")
public class FileUploadResource {
@Resource
private ManagedExecutorService executor;
@Consumes("application/pdf")
@POST
public CompletionStage<String> uploadPdf(File file) {
BeanManager beanManager = getBeanManager();
CompletableFuture<String> completionStage = new
CompletableFuture<>();
executor.execute(() -> {
FileBean fileBean = new FileBean(file, "pdf");
Bean<PdfHandler> bean = (Bean)
beanManager.getBeans(PdfHandler.class).iterator().next();
CreationalContext cCtx =
beanManager.createCreationalContext(bean);
PdfHandler pdfHandler = (PdfHandler)
beanManager.getReference(bean, PdfHandler.class, cCtx);
try {
completionStage.complete(pdfHandler.handler( fileBean ));
} catch (IOException e) {
e.printStackTrace();
completionStage.completeExceptionally(e);
}
});
return completionStage;
}
private BeanManager getBeanManager(){
try {
// manual JNDI lookupCDI for the CDI bean manager (JSR 299)
return (BeanManager) new InitialContext().lookup(
"java:comp/BeanManager");
} catch (NamingException ex) {
throw new IllegalStateException(
"cannot perform JNDI lookup for CDI BeanManager");
}
}
@Consumes("image/jpeg")
@POST
public CompletionStage<String> uploadJpg(File file) throws
IOException {
BeanManager beanManager = getBeanManager();
CompletableFuture<String> completionStage = new
CompletableFuture<>();
executor.execute(() -> {
FileBean fileBean = new FileBean(file, "jpg");
Bean<JpgHandler> bean = (Bean)
beanManager.getBeans(JpgHandler.class).iterator().next();
CreationalContext cCtx =
beanManager.createCreationalContext(bean);
JpgHandler jpgHandler = (JpgHandler)
beanManager.getReference(bean, JpgHandler.class, cCtx);
try {
completionStage.complete(jpgHandler.handler( fileBean ));
} catch (IOException e) {
e.printStackTrace();
completionStage.completeExceptionally(e);
}
});
return completionStage;
}
@Consumes("application/zip")
@POST
public CompletionStage<String> uploadZip( File file) throws
IOException {
BeanManager beanManager = getBeanManager();
CompletableFuture<String> completionStage = new
CompletableFuture<>();
executor.execute(() -> {
FileBean fileBean = new FileBean(file, "zip");
Bean<ZipHandler> bean = (Bean)
beanManager.getBeans(ZipHandler.class).iterator().next();
CreationalContext cCtx =
beanManager.createCreationalContext(bean);
ZipHandler zipHandler = (ZipHandler)
beanManager.getReference(bean, ZipHandler.class, cCtx);
try {
completionStage.complete(zipHandler.handler( fileBean ));
} catch (IOException e) {
e.printStackTrace();
completionStage.completeExceptionally(e);
}
});
return completionStage;
}
}
FileUploadResource, which has an executor attribute injection:
@Resource
private ManagedExecutorService executor;
此外,这个类包含三个方法,它们接收请求,准备FileBean,并将其发送到 EJB 以处理保存文件的任务。在下面的代码块中,我们有uploadPdf(File file)方法,它负责处理所有上传 PDF 文件的请求:
@Consumes("application/pdf")
@POST
public CompletionStage<String> uploadPdf(File file) {
BeanManager beanManager = getBeanManager();
CompletableFuture<String> completionStage = new
CompletableFuture<>();
executor.execute(() -> {
FileBean fileBean = new FileBean(file, "pdf");
//get the EJB by CDI
Bean<PdfHandler> bean = (Bean)
beanManager.getBeans(PdfHandler.class).iterator().next();
CreationalContext cCtx =
beanManager.createCreationalContext(bean);
PdfHandler pdfHandler = (PdfHandler)
beanManager.getReference(bean, PdfHandler.class, cCtx);
try {
completionStage.complete(pdfHandler.handler( fileBean ));
} catch (IOException e) {
e.printStackTrace();
completionStage.completeExceptionally(e);
}
});
return completionStage;
}
AsyncResponse (@Suspended final AsyncResponse ar) as a parameter of the service method, or this method would need to return a CompletionState<T> to the client. In our example, we returned CompletionState to the client. Further, we need to create a separate thread in order to execute a nonblocking task. In the following code block, we have a code snippet of the uploadPdf method, which creates a separate task to call the task that saves the PDF file:
executor.execute(() -> {
FileBean fileBean = new FileBean(file, "pdf");
Bean<PdfHandler> bean = (Bean)
beanManager.getBeans(PdfHandler.class).iterator().next();
CreationalContext cCtx =
beanManager.createCreationalContext(bean);
PdfHandler pdfHandler = (PdfHandler)
beanManager.getReference(bean, PdfHandler.class, cCtx);
try {
completionStage.complete(pdfHandler.handler( fileBean ));
} catch (IOException e) {
e.printStackTrace();
completionStage.completeExceptionally(e);
}
});
实现客户端 API
现在,为了使用反应式编程发送异步请求,我们创建了一个 JAX-RS 客户端 API 的示例。在下面的代码中,我们可以看到一个客户端 API 的示例:
public class ClientAPI
{
private static final String URL = "http://localhost:8080/asyncRestService/resources/upload";
private static final String FILE_PATH = "test.pdf";
public static void main( String[] args ) {
Client client = ClientBuilder.newClient();
WebTarget target = client.target(URL);
try {
CompletionStage<String> csf = target.request()
.rx()
.post(Entity.entity(new FileInputStream(new
File(FILE_PATH)),"application/pdf"),String.class);
csf.whenCompleteAsync((path, err)->{
if( Objects.isNull( err ) )
System.out.println("File saved on: " + path);
else
err.printStackTrace();
});
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
ClientAPI, which defines a callback method to be executed after the asynchronous processing finishes:
csf.whenCompleteAsync((path, err)->{
if( Objects.isNull( err ) )
System.out.println("File saved on: " + path);
else
err.printStackTrace();
});
摘要
在第七章《微服务模式》中,我们探讨了反应式编程范式以及它是如何通过 Java EE 8 机制实现的。我们还展示了如何使用 Java EE 8 机制进行异步调用,以及如何通过异步处理来控制这些调用并对它们应用操作。
CDI 中的事件是 CDI 规范中的一种机制,可以在应用程序的所有层中使用。然而,当与表示层一起工作时,推荐使用此机制。这是因为 CDI 主要关注表示层,其作用域与 HTTP 交互和 HTTP 会话直接相关。此外,我们可以启动一个包含各种响应此事件的元素的事件。
异步 EJB 方法不使用反应式编程范式,而是一个异步过程,这使得减少对客户端响应时间成为可能。这是一个 EJB 机制,建议在业务层使用它。使用此机制的好处是,我们还可以使用其他 EJB 机制,例如事务控制。
异步 REST 服务是 JAX-RS 规范中的一种机制,能够创建一个可以进行异步处理的 REST 服务。使用此机制,一旦发送请求,过程控制就返回,因此客户端不需要长时间等待来处理其他任务。此机制始终在表示层实现。
在下一章中,我们将介绍微服务模式及其实现方法。我们还将探讨聚合器模式、代理模式、链式模式、分支模式和异步消息模式。
第七章:微服务模式
在本章中,我们将学习微服务模式是什么。我们还将将这些模式与单体模式进行比较,探讨基于微服务的应用程序的优势和缺点,以及学习何时使用微服务。此外,我们还将通过实例演示如何从传统的单体应用程序切换到微服务应用程序,并使用实现示例。然后,我们将探讨用于组合微服务的模式。阅读本章后,您将能够识别出应用程序代码中哪些部分适合作为微服务,并且您还将了解如何使用 Java EE 8 实现基于微服务模式的程序。
本章将涵盖以下主题:
-
解释微服务模式
-
解释微服务架构是如何工作的
-
解释何时使用微服务架构
-
基于微服务的应用程序的优势和缺点
-
微服务架构模式
-
实施微服务
解释微服务模式
多年前,我有机会在一个行政-财务系统上担任系统架构师和开发者。这涉及到使用诸如应收账款、应付账款、库存控制、采购、工资、会计等模块。
整个系统由几个模块组成,系统的交付也是以模块化的方式构建的。在应用程序开发结束时,我们得到了一个庞大、集成的系统,模块之间存在许多依赖关系。显然,系统应该集成,我们也非常清楚这一点。然而,这种集成是通过许多依赖关系和模块之间的强耦合实现的。后来,我们还发现这些依赖关系和强耦合是不必要的。
在维护应用程序时,我们遇到了许多问题(如实施新框架的困难以及过度官僚主义),包括每当需要包含来自需求的功能时,我们必须找到负责问题的代码部分,进行更改,测试,然后重新部署整个应用程序。除此之外,有时还会出现其他相关问题,例如使用第三方库或框架来实现这个新功能。在这方面也有一些官僚主义。简而言之,我们花费了大量时间来更改应用程序,而且随着应用程序的增大,这个问题变得更加严重。
系统准备就绪后,我们想知道如果每个模块都有自己的生命——也就是说,如果每个模块都像是一个小程序,那么它就可以独立部署,而不会影响其他模块。此外,如果每个模块可以实际并行开发,并且需要最小的耦合,那么这对应用程序的开发和维护将非常有益。
回到当今时代,我们注意到,在系统组件最常期望的几个特性中,我们可以突出低耦合,这保证了并行开发和更好的维护。
存在一种名为微服务架构的架构模式,它将应用程序划分为几个具有特定功能或职责的小型服务,这些服务之间的耦合度非常低,并且提供了非常好的特性,例如卓越的演进维护。
当涉及到基于微服务的架构时,将行政-财务应用程序的每个模块作为微型应用程序来实现要容易得多。例如,应付账款模块需要其前端,并且需要依赖于一些微服务的组合(例如,提供固定账户的服务,提供变动账户的服务,提供工资单的服务等等)才能工作。
当然,基于微服务的方法既有优点也有缺点,我们应该仔细查看应用程序,以确定是否值得使用这种架构。基于微服务的架构作为传统单体架构的对立面,其中模块的耦合度要高得多。因此,为了讨论微服务架构并更好地定义微服务是什么,我们必须谈论单体架构。这正是我们接下来要做的。
这里是对微服务和 SOA 之间的一种简要比较。有些人喜欢将微服务架构称为轻量级的 SOA。这两种架构的目标都是打破单体场景,以增加可扩展性和维护性。然而,虽然 SOA 是面向企业的,其中应用程序可以更容易地进行通信,但微服务是由形成一个或多个业务功能的小型、集成服务组成的。这是主要区别——微服务是将单体应用程序分解成小型、智能且松散耦合(如 SOA)的组件,这些组件具有明确的职责和易于维护的结果。
在单体应用程序内部
典型应用程序包含一个表示层,即应用程序的客户端,一个持久层或数据库,以及一个中介层,即应用程序的服务器端,包含业务逻辑。我们感兴趣的层是中介层。它接收来自客户端层的请求,执行一些业务逻辑,如果需要则访问数据库(执行查询和更新),并将结果提供给客户端层。
这一层服务器端通常由几个模块或服务组成。尽管应用程序具有模块化架构,但应用程序的部署和打包却作为一个单体块。由类和文件表示的模块包含在打包文件(EAR、WAR)中。这些文件一起部署,属于同一个部署。大多数基于 Java 的应用程序都打包在一个单独的.war或.ear文件中。
以下图表显示了大学中一个学术-财务系统的示例:

应用程序的各个模块被部署到应用服务器上的单个文件。通常,应用程序在一个单独的进程中运行:

对于具有有限业务任务的简单应用程序,如编码(服务器端编码通常只有一种语言,并且有独特的数据持久化技术)、测试和部署的单体方式也相当简单。然而,随着应用程序复杂性的增加,微服务也将被用来解决这些问题。在本章的后面部分,我们将有一个专门的部分展示微服务架构的优点和缺点。
这里有一些这些问题:
-
实现新功能和修复错误困难
-
长时间应用程序启动时间
-
低效的持续部署
-
可靠性低
-
使用新框架和技术困难
实现新功能和修复错误困难
几年来,应用程序不断增长,更多的代码行被添加到现有代码中。即使使用最佳面向对象实践和模式(低耦合和代码重用)谨慎地接近代码,类和库的数量也往往会增加。维护变得更加困难。常见问题可能包括某些类的责任增加。这个问题更加关键,因为开发团队总是变化的。代码变得更加难以理解,一些必要的更改可能会被错误地执行。因此,我们会有实施延迟。
长时间应用程序启动时间
随着应用程序变大(更多类、库和文件),其初始化速度会减慢,开发者启动应用程序将花费大量时间。每天这样做几次会大大增加开发时间,因此这也是影响应用程序维护的另一个因素。
低效的持续部署
对于应用中每个必需的更改,您需要将其全部部署。持续部署非常困难,因为每次代码的一部分被更改时,整个应用都必须重新部署。当然,在测试环境中,您可以一起分组多个更改并执行单一部署。然而,这仍然效率低下,因为它要求所有实现都已完成。对于每天需要多次部署到生产环境的应用,这种情况变得更糟。
低可靠性
单体应用的模块通常在同一个进程中运行。模块中的任何错误或缺陷(如内存泄漏)都会影响整个进程,进而影响整个应用。即使应用在集群系统的多个节点上执行,即使有多个应用实例,如果错误是由于代码引起的,问题也可能通过其他应用实例传播。
使用新框架和技术时的困难
在一个拥有成千上万行代码的大型单体应用中,替换框架或添加新功能都是困难的,因为这需要应用多个部分的更改。有时我们几乎需要重写所有代码;这种情况非常常见。产品的质量并没有提高,而维持这种质量所需的努力却大大增加,因为开发团队没有足够的时间引入新的框架。
规模立方体
《可扩展性艺术》 由马丁·L·艾博特和迈克尔·T·费舍尔所著,提出了一个称为“规模立方体”的三维可扩展性模型。更多详情请参见以下图表:

该模型表明我们可以以三种不同的方式扩展应用,如下所示:
-
X-轴扩展: 这是最常见的模型,它建议在负载均衡器的监督下执行多个应用副本。这种可扩展性也称为水平扩展,其中满足需求的解决方案是添加更多服务器。
-
Z-轴扩展: 这与 X 轴扩展(运行相同代码的多个实例)类似,但在此情况下,每个服务器负责一部分数据。这种可扩展性使用了分片的概念,最常见于数据库。
-
Y-轴扩展: 这具有将应用分解为不同服务的策略。微服务架构是 Y 轴扩展的一个例子。使用这种可扩展性方法,单体应用被分解为一组服务。每个服务负责一组相关的功能。
实际上的微服务是什么
我们可以将微服务架构定义为使用一系列小型、低耦合、独立且可部署的服务来开发应用程序的方法。每个微服务都定义得非常明确,这意味着每个微服务都负责一个定义良好的单一任务。
因此,微服务是执行特定任务的小型组件,可能被用来为商业带来利益。因此,当在微服务架构下开发应用程序时,我们必须将这个应用程序分解成相同的微服务组件。以及我们将在后面看到的其他特征,微服务可以在不降低应用程序本身质量的情况下进行更改和部署。
微服务并非万能的解决方案
任何技术解决方案都有其优点和缺点,微服务也不例外,这些将在本章后面进行探讨。如果微服务应用得当,那么它们将提高复杂增长的应用程序的可靠性和可伸缩性。然而,微服务的本质在实施不当时可能会带来问题。
微服务的强独立性意味着它们可以使用不同的语言创建,并且可以使用不同的协议和 API 进行通信。此外,一个应用程序可能包含许多用于商业利益的微服务。微服务的多样性和数量使得对这些相同微服务的控制相当复杂。在基于微服务架构的应用程序实现中,应该格外小心并投入额外的精力。
解释微服务架构是如何工作的
尽管在实施微服务架构时还没有一个正式的模型可以遵循,但我们仍然可以强调一些共同的特征。我们还可以在开发微服务时验证良好的实践。
具有一个或多个相关功能,微服务是应用程序中用于商业利益的小部分。从这个微服务的定义出发,我们现在将详细探讨以下微服务的常见特征:
-
应用程序被分解成更小的组件
-
多任务团队
-
产品焦点
-
简单而智能的处理。
-
库和 API 的去中心化管理
-
单一职责原则
-
容错性
-
进化系统
-
去中心化数据
应用程序被分解成更小的组件
在开发基于微服务架构的应用程序时,我们应该考虑将应用程序分解成更小的组件,这些组件将分别进行更改和部署(与每次代码更改都必须完全部署的单一应用程序相反)。然而,将应用程序分解成独立操作的小型服务需要更长的时间来开发。
下图显示了应用程序客户端对业务组件的访问:

以下图显示了相同的客户端通过微服务访问相同的功能:

由于微服务小且独立,它们可能导致访问更复杂服务的问题。这些问题如下所述:
-
调用多个微服务以实现功能:通常,客户端需要的服务或功能可能涉及对多个微服务的调用。例如,在一个学术-财务管理系统中,一个想要了解大学生历史详细信息的客户端可能需要访问许多微服务。因此,对各种微服务进行多次调用会降低网络性能,这在存在移动客户端的情况下尤为重要。
-
微服务可以使用不同的协议:由于微服务是独立的,它们可能需要不同的协议来访问,例如 REST、WebSocket 等。换句话说,访问微服务的协议并不统一,我们可以想象,如果需要与每个微服务进行通信,这将多么耗时。
-
不同类型的客户端:存在不同类型的客户端,例如移动客户端和桌面客户端。这些客户端各自也有不同的需求。提供给移动客户端的信息量通常比桌面客户端少。另一个重要问题是,不同类型的客户端的网络性能不同。移动网络比本地网络慢。
在以下图中,我们可以看到一个系统中存在的一组功能及其对应用程序中各个微服务的相应调用:

将应用程序分解成一组微服务的代价是相当明显的。控制对微服务的各种调用是一项复杂的任务,而且仍然存在单一数据库的问题。在这些情况下,事务控制变得更加复杂。
为了解决这一系列问题,存在一个名为 API 网关的解决方案,它为基于微服务的应用程序客户端建立了一种前端。API 网关位于客户端和微服务之间。有时,请求只是对微服务的调用,但在其他情况下,API 网关充当一个粗粒度层,接收对服务的请求并对多个微服务进行调用。
我们可以在以下图中看到这一点:

客户端应用程序访问各种微服务的方式与使用 API 时相同。API 网关负责访问不同的微服务,并关注用于此访问的不同协议。然而,根据客户端应用程序的类型(例如,是移动应用还是浏览器应用),某些系统功能可能不同。例如,如果我们在一个移动应用中,访问学生历史记录时需要查询的信息量将少于浏览器应用访问的信息量(要么是因为展示屏幕不同,要么是因为访问各种微服务时的性能问题)。考虑到这个问题,除了作为微服务调用的接口层之外,API 网关还可以为每种类型的客户端提供特定的 API。
因此,移动客户端会访问移动 API,而 Web 客户端或浏览器会访问 Web API。我们可以在以下图中看到这一点:

简而言之,API 网关充当通用翻译器的角色。因此,客户端专注于业务,而不是请求或响应的翻译。此外,客户端可以一次性调用(如果功能需要多个微服务),这有效地提高了网络性能。
多任务团队
在单体应用中,每个应用部分都有专门的团队——数据库团队、后端(Java)开发团队和设计团队。
在基于微服务的应用中,有一个多任务团队,负责微服务的所有部分。因此,该团队必须是全能的。但我们知道,即使是对于单体应用,开发者通常也作为全栈开发者工作,负责开发应用的所有部分。
产品导向
在单体应用中,焦点始终是应用项目,代码通常交付给应用客户。然而,在基于微服务的应用中,焦点始终是产品。负责微服务的团队可以改变和演进产品,而不会像在单体应用中那样经历典型的延迟。产品属于负责微服务的开发团队,如下面的图所示:

简化且智能的处理
微服务通过极其实用的操作执行专业和智能的任务——有一个请求,处理某些内容,并将响应返回给微服务客户端。
图书馆和 API 的去中心化治理
微服务被用于一个或多个应用程序的点,或者被各种应用程序使用。此外,我们必须牢记微服务的独立性。因此,对 API 和库的去中心化和治理有自然的促进作用。这意味着微服务开发者应该使用他们认为对微服务开发必要的工具,只要客户端的接口不改变。
单一职责原则
微服务应该始终具有一组小的职责。这个定义基于一个称为单一职责原则的原则,该原则指出“一个类必须只有一个改变的理由”。换句话说,具有多个改变动机的类会有更多的职责,并且不会具有内聚性。这会导致问题,例如类职责之间的高耦合性、代码重用困难以及维护困难。
一个具有负责计算的方法以及格式化结果的方法的类,将是一个具有很少内聚性的类的例子。让我们看看以下示例:
class Insurance {
public double calculateCotation (...) {...}
public String generatePDF (...){...}
public String generateXLS (...){...}
}
第一个方法(calculateCotation)与业务领域直接相关,而其他方法(generatePDF和generateXLS)与计算值的展示相关。
单一职责原则(SRP)是被称为 SOLID 原则的五个原则之一,这些原则由罗伯特·塞西尔·马丁(俗称 Uncle Bob)在其著作《敏捷软件开发:原则、模式和最佳实践》出版后普及。实际上,这五个原则是书中报告的几个原则的子集。SOLID 原则是面向对象编程的原则,其目的是使代码更加有序和易于阅读。我们可以将“solid”这个词与“solid code”这个表达联系起来。SOLID 这个缩写的每个字母都是原则的首字母:
Single responsibility principle(SRP)
Open closed 原则(OCP)
Liskov 替换原则(LSP)
** I**nterface segregation principle(ISP)
** D**ependency inversion principle(DIP)
容错
由于应用程序现在分解为多个微服务,因此失败的可能性更大,因为一个或多个微服务可能会失败,甚至很可能失败。
实现基于微服务的应用程序时应考虑这一点。微服务以这种方式实现,即失败时影响很小,并且可以尽快恢复。因此,基于微服务的应用程序必须以容错为目标进行开发。当然,这样的实现增加了开发的复杂性。
由于微服务的性质,恢复任务得到了简化。因为它是一个小服务,所以它可以快速创建或初始化。
进化系统
基于微服务的架构非常适合随着时间的推移而增加要求和功能的系统。那些业务需求显著增加的系统需要可重用的服务。他们还需要职责的清晰分离,这正是微服务提供的。
分散式数据
每个微服务都有自己的数据库——也就是说,每个微服务都有自己管理数据的方式,而不是整个应用程序或整个公司的单一、集中式数据库。这意味着,根据服务类型,数据库可能相当不同。我们可以有一个关系型数据库(或多个),我们可以有几种类型的 NoSQL 作为文档数据库、图数据库或其他类型的数据库。
解释何时使用微服务架构
考虑基于微服务架构的应用程序特性,我们可以确定何时使用微服务架构的一些标准,如下面的项目符号列表所示:
-
当一个系统的需求和功能增加时,这意味着它已经迅速发展。在这种情况下,系统开始在其模块之间混合职责。
-
当我们需要重用服务时。
-
当 API 的集中化开始阻碍系统的演变时。
-
当需要新功能、API、库和框架时,我们不想为此重写所有软件。
想象一下,我们有一个令人震惊且创新的商业模式,我们必须迅速为客户提供应用程序,让他们享受这项业务。原则上,采用单体模型会使应用程序开发迅速,测试和部署也很快。在这个阶段,我们可能不需要考虑系统的演变——我们现在想考虑公司的财务目标。不幸的是,立即的利润可能会掩盖未来可能带来更大利润的软件发展。
有两个相互冲突的问题:
-
快速交付产品,以便您能够接触客户、利用公司,并在以后考虑产品的演变规模,冒着创建维护不良软件的风险
-
一旦我们确定了使用微服务策略的标准,我们将在基于微服务的应用程序开发上花费更多时间
在这一点上,架构和项目管理团队都应该有一些问题在心中:
-
应用程序是否会演变?
-
团队是否多任务处理?
-
与单体产品相比,在微服务产品上最初将花费多少开发时间?
如何将应用程序分解为微服务
我们有两种分解应用程序的方法——要么有一个单体应用程序,我们想要将其分解为微服务,要么应用程序不存在,我们想要使用微服务策略来创建它。
在现有单体应用的情况下,总是有可能建立一个中间阶段,从单体系统开始创建混合架构,例如。在这个中间阶段,可能没有立即的需求或足够的时间来改变整个单体应用,并用基于微服务架构的另一个应用来替换它。例如,我们面临单数据库的关键问题。
实际上并没有一个确定的分解模型,但我们可以执行一些可能有所帮助的任务:
-
识别微服务
-
注意提取适合微服务的应用模块的过程
-
为应用建立六边形模型
识别微服务
微服务可以非常多样。有技术微服务,它们在整个应用中使用并满足非功能性需求,还有与业务应用相关的微服务。
我们可以采用两种方法来创建业务微服务——使用业务能力或用例:
-
业务能力:这表示为业务应用生成价值的能力。分解较小,每个服务都会有更大的范围。例如,在一个管理大学的系统中,我们可以有以下业务能力,如教授管理服务、学生管理服务、课程管理服务、学生财务管理服务、大学活动服务。
-
用例:我们可以根据用例对服务进行分解。在这种情况下,分解较大。例如,一所大学可能有学生注册服务、月费支付服务、考试评审服务以及学生的学术状况服务。
注意提取适合微服务的应用模块的过程
在这里,我们必须提取具有良好定义接口的应用组件或模块。这些模块有资格成为微服务。查看这些模块接口,我们必须试图找出以下两种情况中的哪一种:
-
模块是独立的,这意味着服务定义良好且责任较少。在这里,我们必须注意不要生成单体模块。
-
模块是可重用的。一些可以重用的模块的明显例子是那些在应用中用于所有技术任务的模块,如打印服务、电子邮件服务、存储服务、文档下载服务等。具有更高业务内容的模块——如票务支付服务、信用卡支付服务等——也可以重用。
让我们看看以下图表所示的示例:

在创建技术微服务时,我们必须小心不要将其中的业务功能一起携带。
一旦采取这种第一种方法,我们仍然有一个代表核心业务的单体块。我们可以根据业务能力或用例对这个块进行分解,也可以发现具有明确定义接口的新业务模块。然后我们可以有如学生状态服务、教授服务、学院活动服务和财务学生服务等模块。此外,我们还可以有更细粒度的业务微服务,如票务支付服务。
让我们看看下面的图:

同样,业务相关的微服务不应承担其他微服务候选模块的责任。这意味着我们必须看到每个微服务的大小,这与它们自己的责任相关。微服务应该承担较少的责任,体现为一个内聚的服务(我们始终要记住单一职责原则)。避免依赖服务的一种方法是要记住微服务的独立性。如果有两个微服务需要一起部署,或者一个没有另一个就无法存在,那么这些服务很可能只有一个服务。
获取这种分解的另一种方法是设计我们的应用程序为六边形模型,我们将在下一节中看到。
为应用程序建立六边形模型
典型应用将它的各种功能模块保持在一起。通常,这种典型应用具有六边形结构或架构(我们将在后面看到六边形架构的更好定义),核心业务位于结构中心。核心业务包含应用提供的所有服务的管理。还有与核心外部各种组件的链接,如消息服务、数据库、客户端机器等。这些与外部世界的连接具有特定的适配器和协议,如数据库适配器、REST API、Web API、消息 API、WebSocket 协议等,如下面的图所示:

六边形模型背后的思想是将应用程序的核心领域从对技术基础设施的访问中隔离出来,例如数据库、消息队列、存储等。正如之前所述,一旦核心领域被隔离,我们就可以更深入地分析它,并尝试提取与业务相关的服务。
基于微服务的应用的优缺点
就像任何其他技术一样,使用基于微服务的架构既有优点也有缺点。在微服务的实施过程中可能会出现一些问题,但优点可以克服开发过程中遇到的复杂性。
微服务架构的优势如下:
-
小型多任务团队。
-
服务可以用不同的语言编写。这是一个优点,因为根据服务,一种特定的语言可能比另一种语言提供更多获取服务所提供功能性的工具。
-
部署更快,集成更自动化。需求变更意味着只需部署相关的微服务。
-
最新的库、框架和技术可以快速使用。
-
更高的容错性。
-
微服务与产品相关,而不是与项目相关。开发者有更多的自由,因此可以更快地开发服务。
-
微服务的知识可以更快地传递给开发团队。因此,必要的代码更改和维护可以更快地进行。
以下是一些微服务架构的缺点:
-
微服务的独立性可能带来复杂性,因为服务在通信和数据方面可能采用不同的协议。
-
微服务的增加使得对这些服务的控制和管理工作更加复杂。例如,为了保持容错性,必须投入更多的编程工作。
-
除了不同协议的服务问题外,由于某些应用功能可能涉及对多个微服务的调用,这加剧了网络流量,因此服务之间的通信也更加复杂。在这些情况下,开发者应使用 API 网关等工具和策略。
-
事务控制是一个复杂因素,尤其是在从单体系统迁移到基于微服务的架构时——例如,在特定功能或用例需要多个微服务的情况下。
-
每个微服务都在单个进程中运行,这意味着增加内存消耗。
-
由于应用程序分布在微服务中,测试变得更加困难。
微服务架构模式
到目前为止,我们已经看到微服务架构基于功能分解,产生了独立、自给自足的服务,这些服务可能以不同的方式使用定义良好的接口与外界通信。这有利于低耦合和定义良好的功能,允许高内聚(定义良好的责任和较少的功能)。
尽管服务独立运作,但这些服务的目的是创建一个应用程序——即一组与业务相关的功能。
基于这些特性,我们可以提取一些可用于微服务架构实现的模式。以下是一些这些模式:
-
聚合模式
-
代理模式
-
链模式
-
分支模式
-
异步消息模式
聚合模式
正如其名所示,此模式建立了存在或创建一个相对更复杂的服务,该服务调用更多内部服务的功能。因此,此聚合微服务充当一个中介服务,是其他微服务的组合,从意义上讲,它调用微服务,从每个微服务获取单个响应,并应用必要的业务规则,将最终响应返回给客户端。让我们看一下以下图示:

此图似乎表明解决方案的某些部分被重复,这表明存在设计模式。我们可以将此与充当业务外观的微服务进行比较,该服务调用特定的微服务以在应用业务层面执行更复杂的功能。在更大规模上,此聚合器微服务可能作为被更高层级的聚合器微服务调用的微服务个体。显然,对应用业务规则有深入了解至关重要。
代理模式
此模式是聚合器模式的一种变体,但有一个重要区别。对于所有内部微服务没有数据收集,这意味着没有聚合。代理模式将调用指向特定的微服务,请求的分析决定了应该调用哪个微服务(这是代理模式典型的行为)。
然而,在实现此代理的过程中可能有一些好处。在将响应返回给客户端之前,可能会有将此响应转换为请求客户端期望的格式的转换。记住,不同的客户端(包括网络浏览器、移动设备等)都可以发起请求。让我们看一下以下图示:

我们现在可以明白为什么 API 网关明显基于聚合器和代理模式。
连接模式
连接模式的目的在于通过一系列按顺序通信的服务,向客户端提供一个更一致和明确的最终响应。这样,作为链中第一个服务的服务 A与服务 B通信。服务 B与服务 C通信,依此类推。调用是同步的,客户端在最终响应从服务 A返回到客户端之前保持锁定状态。
在这里需要注意的是,到达链式服务的请求可能不同。因此,从服务 A发送到服务 B的请求可能与从服务 B发送到服务 C的请求非常不同,依此类推。同样,服务的响应也可能不同。从服务 C到服务 B的响应可能与从服务 B到服务 A的响应不同。
让我们看一下以下图示:

这个实现的实际例子是生成用于打印银行支票的数据。假设一家银行提供了两个 SOAP 网络服务。一个是用于在银行注册银行支票数据的,另一个是用于获取已注册在银行的银行支票数据。我们的应用程序包含两个定义好的微服务——服务 A,它将银行支票数据注册到数据银行(即,它调用第一个注册网络服务);以及服务 B,它获取最终的银行支票数据,如条形码(意味着它调用第二个查询网络服务),并返回用于银行支票生成的数据。
客户端应用程序通知服务 A用户的银行数据。服务 A负责与金融机构注册,并在注册返回后,服务 A向服务 B发出请求。服务 B随后获取用于生成银行支票的最终数据。服务 B将此数据传递给服务 A,然后它可以对该数据进行一些转换,并将其返回给客户端。
同样的例子可以使用聚合器模式实现。然而,每当需要按顺序执行步骤以生成最终响应时,我们可以将链模式视为解决方案。
分支模式
分支模式作为聚合器模式的扩展,可以并行调用不同的服务链。在分支模式实现示例中,一个称为服务 A的初始服务被客户端调用,并可以作为聚合器,调用服务链来组合响应。或者,根据请求,调用特定的服务链,从而充当代理模式。让我们看看以下图表:

实际上,链模式与分支模式之间的主要区别在于调用不同的微服务链。然而,如果我们进行抽象,并将每个链视为一个独立的微服务,本质上,我们将有一个聚合器或代理模式。
异步模式
当我们从实际的角度考虑微服务时,我们立刻会想到使用 REST 协议实现的微服务。然而,基于 REST 模式的微服务是同步的,因此是阻塞的。有时,我们需要可以异步调用的微服务。异步机制是通过针对每个应用程序特定的技术(消息队列、发送异步事件等)开发的。让我们看看以下图表:

服务 A 从客户端接收请求并以同步方式调用服务 B。然而,服务 B依赖于服务 C,并通过消息队列异步调用它,例如。服务 C从队列中读取消息,进行必要的逻辑处理,并使用队列机制向服务 B返回响应。(我们应始终牢记被调用服务的独立性。)
服务 B 和 服务 C 之间的一种通信方式是服务 B向QRequest队列发送请求消息,并监听来自QReply队列的响应。然后服务 C从QRequest队列中读取请求消息,执行必要的处理,并将响应发送到QReply队列。服务 B从QReply队列中读取响应消息,应用转换或添加其他信息,然后将响应发送给服务 A。很明显,应该考虑具体实现的细节,例如等待服务 C超时。
实现微服务
我们示例中微服务实现的思路是实现一个微服务,为客户端提供银行数据以生成支付凭证。返回的数据将是支付凭证的条形码。
银行提供两个 SOAP 网络服务,一个用于将支付凭证注册到银行,另一个用于提供已注册的支付凭证生成数据。因此,我们将创建两个微服务,一个用于注册支付凭证数据,另一个用于查询已注册的支付凭证。 这些微服务中的每一个都将调用相关的网络服务。接下来,我们将构建一个更复杂的第三个微服务,它将添加两个调用:一个调用注册微服务,另一个调用检索已注册的内容。让我们看看以下图表:

实际上,我们在这里可以创建一个微服务链;然而,我们选择了聚合器模式,因为我们假设可能有多个支付凭证已经被批量注册。
我们将创建三个类——每个微服务一个。每个微服务都将实现为一个 REST 资源。为了使代码更简单,我们为注册和查询微服务只使用一个请求类和一个响应类。 为了简化代码,我们只使用一个请求类和一个响应类来处理这两个微服务;注册和查询微服务。
支付凭证注册类如下:
@Path ("/register")
public class PaymentRegisterService {
@Path ("/paymentSlip/")
@POST
@Produces (MediaType.APPLICATION_JSON)
@Consumes (MediaType.APPLICATION_JSON)
public Response register (RequestVO requestVO) {
// calls the bank register web service:
// ...
// Prepare the response with the register web service data:
ResponseVO responseVO = new ResponseVO();
responseVO.setRegisterNumber (7554433452L); // this is a simulation //because this information must be returned from the bank's web service
responseVO.setUserName (requestVO.getUserName());
responseVO.setValue (requestVO.getValue());
responseVO.setStatus ("OK"); // everything is supposed to be correct
return Response.ok (responseVO).build();
}
}
以下为支付凭证查询类:
@Path ("/query")
public class PaymentQueryService {
@Path ("/paymentSlip/")
@POST
@Produces (MediaType.APPLICATION_JSON)
@Consumes (MediaType.APPLICATION_JSON)
public Response query (RequestVO requestVO) {
// calls the bank query web service:
// ...
// Prepare the response with the query web service data:
ResponseVO responseVO = new ResponseVO();
responseVO.setCodeBar("8888999977776666");// this is a simulation //because this information must be returned from the bank's web service
responseVO.setUserName (requestVO.getUserName());
responseVO.setStatus ("OK"); // everything is supposed to be correct
responseVO.setValue(requestVO.getValue());
return Response.ok (responseVO).build();
}
}
以下为聚合器类:
@Path ("/bank")
public class PaymentSlipGenerating {
private static String BASE_PATH = "http://localhost:8080/microservice/ws";
@Inject private UserService userService;
private Response callMicroservice (String path, Object entity) {
Client client = ClientBuilder.newClient();
WebTarget target = client.target (BASE_PATH);
Response resp = target.path (path).request().post(Entity.json (entity));
return resp;
}
@Path ("/paymentSlip/{id}/{value}")
@GET
@Produces (MediaType.APPLICATION_JSON)
public Response getPaymentSlipData (@PathParam("id") long id, @PathParam("value") String strValue) {
// get the payment slip value:
double value = convertToValue (strValue);
// get the user information from local DataBase:
User user = userService.getUserById (id);
// prepare the payment slip registering request:
RequestVO registerRequest = new RequestVO();
registerRequest.setUserCode (user.getCode());
registerRequest.setUserName (user.getName());
registerRequest.setValue (value);
// call registering microservice:
Response resp = callMicroservice ("/register/paymentSlip", registerRequest);
ResponseVO responseRegisterVO = resp.readEntity (ResponseVO.class);
// prepare the payment slip query request:
RequestVO queryRequest = new RequestVO();
queryRequest.setUserName(registerRequest.getUserName());
queryRequest.setRegisterNumber (responseRegisterVO.getRegisterNumber());
queryRequest.setValue (value);
// call query microservice:
resp = callMicroservice ("/query/paymentSlip", registerRequest);
ResponseVO responseQueryVO = resp.readEntity (ResponseVO.class);
return Response.ok (responseQueryVO).build();
}
}
任何客户端都可以调用这个聚合器微服务以获取条形码和其他信息,从而生成支付凭证。
摘要
在本书的几乎每个部分,我们都表明,除了是常见问题的解决方案外,模式还旨在实现应用组件的重用和低耦合。遵循这一原则,将应用分解成执行智能和特定任务的小独立部分,使该应用能够以自然和有序的方式进化。这些执行特定任务的小部分被称为微服务,而基于将应用分解成这些小部分来开发系统的方法被称为微服务架构。
然而,像任何技术一样,这种技术也有其优缺点。开发应用并考虑各种微服务的控制是一个复杂任务,但一旦微服务定义得很好,应用就能比单体应用更好地进化。这样,应用的维护就更快,新功能的添加和现有功能的更新也更快。
由于微服务的去中心化,新框架、API 和库的引入速度大大加快。教给未来的开发团队微服务也更快、更简单,部署也更快,因为不需要因为应用某一部分的改变而重新部署整个应用。我们还研究了名为 API 网关的机制。API 网关根据微服务为应用的各种客户端建立了一种前端,将客户端的调用转换为对微服务的调用。
最后,在这一章中,我们探讨了在微服务架构开发中使用的某些主要设计模式,例如聚合器模式、代理模式和链式模式。
下一章,第八章,云原生应用模式,将讨论云原生应用模式。在这里,我们将研究云应用,以及开发云应用时必须遵循的一些关键模式。
第八章:云原生应用程序模式
在本章中,我们将解释云原生应用程序模式,探讨云原生应用程序是什么以及其目标是什么。我们还将展示之前章节中描述的图案以及针对基于云的应用程序出现的新图案。阅读本章后,读者将能够理解表征云架构的概念和模式。
在本章中,我们将涵盖以下主题:
-
解释云原生应用程序的概念
-
解释云应用程序的目标
-
解释云设计模式
解释云原生应用程序的概念
云及其资源正越来越多地成为企业生活的一部分。在过去,云资源被用于存储管理、电子邮件、文档和照片等解决方案。如今,在云结构中部署企业应用程序无疑指明了公司应遵循的道路。
云已成为开发业务应用程序时首先考虑的策略之一。这里的重大挑战是使此应用程序使用云提供的功能,如弹性、可扩展性和可用性,以利于业务。
首先,单体应用程序被迁移并部署到云中。我们取得了一些成果,如更好的管理和控制,以及可用性。但我们真正想要的是与云提供的功能集成的应用程序,即利用云计算资源更好地解决业务问题。
然后是云原生应用程序,这是一种利用云提供的优势和能力的应用程序。该应用程序的开发使用了一套设计模式。我们将在本章后面更详细地探讨云架构中使用的某些主要设计模式。
云计算环境在基本意义上是弹性的,即计算资源根据需求使用和释放。因此,与数据中心控制下的应用程序相比,可扩展性和可用性以更少的复杂性实现。
基于云原生计算基础,指导云原生应用程序的特性如下:
-
容器封装:应用程序在隔离单元中执行
-
动态管理:有一个中央编排来管理应用程序,提高资源利用率,并降低运营成本
-
面向微服务:云中的应用程序是松散耦合的
因此,云原生应用程序具有高度分布的特性。这是因为该应用程序部署在云中,并且这些特性得到了满足。
解释云原生应用程序的目标
云设计模式旨在构建安全、可靠的云应用程序。以下列表将展示云原生应用程序要实现的其他特征:
-
可用性:应用程序运行和运行的时间。所期望的是所有系统功能的持续运行。
-
数据管理:应用程序处理的数据是云应用程序的基石。数据可以在多个服务器(或集群)之间分布或复制,以实现可伸缩性、可用性,甚至性能。
-
消息传递:为了提高可伸缩性,云中的服务或应用程序具有低耦合性,并且通常使用异步消息进行通信。
-
管理和监控:由于云应用程序在远程环境中运行,必须有一种方法可以通过日志和报告来监控非功能性属性状态——例如计算资源的使用情况。此外,我们必须能够部署新的实现,而无需停止应用程序(保持可用性特性)。
-
性能和可伸缩性:即使在计算资源的使用和应用程序扩展增加的情况下,也能保持性能。
-
弹性:云应用程序应该能够快速克服故障。这个问题对于保持可用性非常重要。
-
安全性:云应用程序向用户公开了其多个元素,应该能够防止恶意用户或恶意程序发起的攻击。
在这里需要注意的是,在实现分布式应用程序,尤其是云原生应用程序时,有一些问题应该予以考虑。这些由 L. Peter Deutsch 和 Sun Microsystems 的其他人提出的“分布式计算谬误”描述了关于分布式编程的错误假设:
-
网络可靠
-
延迟为零
-
带宽无限
-
网络安全
-
拓扑结构不改变
-
只有一位管理员
-
传输成本为零
-
网络同质
一方面,云原生应用程序要实现的目标和挑战,另一方面,分布式应用程序实现中一直存在的困难,我们推出了设计模式来解决云原生应用程序面临的问题。随着我们的进展,我们将展示在云架构中使用的的主要设计模式。
解释云设计模式
既然云应用程序的概念及其挑战已经定义,让我们直接进入正题,讨论用于实现云架构的设计模式:
-
复合应用程序(微服务)
-
抽象
-
十二要素
-
API 网关
-
服务注册表
-
配置服务器
-
电路断路器
复合应用程序(微服务)
在第七章,《微服务模式》中,我们展示了将应用程序分解为函数的优势(以及劣势),并从中获得多方面的好处,始终以应用业务为目标。在该章中,确立了基于微服务的架构的特点,即通过将应用程序分解为小型、功能独立且具有良好定义的松散耦合通信接口的组件。
抽象
此模式指出,重点必须放在客户端的需求上,而不是现有的硬件结构上。从这个意义上说,云计算的计算资源是按需使用的,这体现了弹性可伸缩性。这样,资源以抽象的方式看待,并根据客户的需求进行改变。
十二要素
十二要素方法基于指导创建成功 SaaS 项目的十二要素。这项技术是在 Heroku 的开发过程中创建的,Heroku 是一个支持多种编程语言的云服务平台(PaaS),包括 Ruby、Java、Node.js、Scala、Clojure、Python 和 PHP。
在开发和支持 SaaS 应用程序期间获得的经验被编目,即记录遇到的错误,并为出现的问题制定解决方案。因此,创建了十二要素方法,这是一套使开发基于云的应用程序更加容易的指南。为了教学目的,这里有一些定义——SaaS 代表软件即服务,PaaS 代表平台即服务。SaaS 最广为人知,因为它在市场上的增长率最高。以下为十二要素:
-
代码库: 在版本控制中跟踪一个代码库,同时进行多次部署
-
依赖项: 明确声明并隔离依赖项
-
配置: 在环境中存储配置
-
备份服务: 将备份服务视为附加资源
-
构建、发布、运行: 严格分离构建和运行阶段
-
进程: 以一个或多个无状态进程执行应用程序
-
端口绑定: 通过端口绑定导出服务
-
并发: 通过进程模型进行扩展
-
可丢弃性: 通过快速启动和优雅关闭最大化鲁棒性
-
开发/生产一致性: 尽可能保持开发、预发布和生产环境的相似性
-
日志: 将日志视为事件流
-
管理进程: 将管理/管理任务作为一次性进程运行
代码库
代码库是指任何单一存储代码的仓库。每个应用程序只能有一个代码库。在十二要素方法中,多个应用程序不能共享相同的代码库。在这种情况下,一种解决方案是通过生成作为依赖项进入项目的库来重构共享代码。
代码库必须由版本控制系统(如 subversion 或 Git)进行管理。从这个代码库中生成各种部署,每个部署针对不同的环境——开发、预发布和生产。
开发者在本地开发环境中运行应用程序的一个版本(即部署):

依赖项
项目使用的每个依赖项都必须声明,并且与代码隔离。在这种情况下,我们可以使用一些包管理工具(如 Maven、grundle 和 npm)。例如,Maven 工具用于构建和记录项目。依赖项在pom.xml文件中声明,我们可以声明项目依赖项:

配置
应用程序的配置是不同部署之间所有变化的配置。它可以包括:
-
数据库访问的主机、端口和凭证
-
数据库模式
-
缓存设置
-
访问消息队列的主机、端口和凭证
十二要素方法要求将配置与代码分离。因此,配置永远不应该在代码中,因为配置可能在不同部署之间有所不同,但代码不会。十二要素方法不推荐将配置放在文件中,如 Java 属性文件,因为总有可能在不同环境中放置相同的配置数据(例如,当开发者在版本控制存储库中提交其本地配置时)。另一个弱点是这些配置文件的安全问题。十二要素方法建议使用环境变量,这些变量可以在部署应用程序到特定环境时注入:

后备服务
后备服务是应用程序使用的外部服务,例如数据库、消息服务、文件存储库或电子邮件服务。十二要素方法将应用程序外部每个服务视为一种资源。这些资源必须通过 URL 或位置以及应用配置中的凭证属性来访问。这样,对服务位置(如数据库或文件存储库)的任何更改都不会影响应用程序代码。这种变化对代码来说是不可察觉的,从而实现了低耦合。我们还可以使用抽象或接口来访问这些服务,除了应用配置之外。
作为第一个例子,我们有一个存储在存储库中的文件。无论本地还是远程访问(如 Amazon S3),都可以通过 URL 访问存储库。为此,我们使用应用配置。当然,在最后实例中,存储文件本地或远程的方式可能不同,但使用接口可以极大地改善这个问题。
作为第二个例子,我们有在不同环境中使用 SQL 数据库的情况(生产、预发布和开发)。我们应该只通过应用程序的配置来更改连接到数据库的 URL。这种策略应该扩展到数据库管理系统可能的变化,例如从 MySQL 到 DB2。当然,我们知道在实践中,使用特定数据库的机制可以提高性能。然而,如果发生变化,这些变化将是微小的:

构建、发布、运行
将代码库转换为特定环境的过程必须分为三个严格独立的阶段:
-
构建:编译并生成可执行包;例如,生成 EAR 或 WAR 存档。
-
发布:将应用程序的配置应用于可执行包。生成的发布,即构建过程中生成的包与应用程序配置的组合,已准备好在包含配置的环境中扫描。
-
运行(或运行时):在特定环境中初始化应用程序。
这些步骤的分离,即定义它们的职责,对于提高系统维护和自动化非常重要。在这些步骤中使用了持续集成工具(Maven 和 Jenkins 是这些工具的例子):

进程
十二因素方法论强调,所有应用程序的过程或组件都必须是无状态的和无共享的,也就是说,它们不应该存储信息。因此,在分解为微服务应用程序中,每个微服务都不应该在内存中存储信息或使用服务器缓存。这是提升应用程序的一个重要因素。如果有在请求之间存储某些状态或稍后要使用的一些数据的需求,我们可以使用数据库。
当我们使用内存来存储信息以供后续请求使用时,我们面临风险,因为下一个请求可能位于不同的进程(或者请求甚至可以由另一个服务器处理):

端口绑定
十二因素应用程序是完全自包含的,不依赖于使用外部服务器(如 Tomcat 或 Apache)作为服务导出。十二因素应用程序必须通过端口绑定导出 HTTP 服务,这意味着应用程序也通过 URL 与世界交互。这样,一个应用程序可以成为另一个应用程序的后端服务或外部资源。
并发
十二要素应用必须设计成使用允许并行执行进程的进程模型来扩展。这就像在执行守护进程时使用的 UNIX 进程模型。因此,我们可以设计只处理 HTTP 请求的进程,同时也可以有处理非常长且在后台执行的任务的进程。当应用需要扩展时,进程模型显示出其重要性,因为此时应用被复制,而不是执行一个新实例。
可丢弃性
在云计算的世界里,进程不断涌现和消亡;这正是导致按需扩展加剧的原因。在这些条件下,进程的诞生或起源必须尽可能快,你的中断应该尽可能快,并且影响尽可能小。
十二要素应用的进程是可丢弃的,这意味着它们可以随时初始化和停止。一个没有造成影响的进程意味着它应该优雅地结束,如果需要的话保存状态,并释放分配的计算资源。
开发/生产一致性
十二要素应用应尽可能保持生产、测试和开发环境的状态相似。这有助于持续部署的过程,同时避免在构建从开发环境发送到生产环境时可能产生的错误。
日志
十二要素应用的日志应被视为事件流。在传统环境中,日志可以生成一个文件。然而,可能会出现问题,例如磁盘空间不足。我们知道在云计算环境中,根据需求具有弹性可伸缩性,进程不断诞生和消亡。同样,机器和容器可能不再被使用。在云中的这种调整过程中,日志文件可能会丢失。在云平台上将日志视为事件流是至关重要的。这样,日志可以被导向任何地方。例如,它们可以被导向 NoSQL 数据库、另一个服务、存储库中的文件、日志索引和分析系统,或者数据仓库系统。
管理进程
十二要素方法指出,维护任务,如数据迁移的脚本执行、初始数据加载和缓存清理,应该自动化并在规定时间内执行。因为我们的应用将在多个环境中运行,并且跨越多个服务器,因此有必要使用相同的一套工具、软件和配置文件来执行这些任务。因此,不同环境之间的并行问题减少。
API 网关
将应用程序分解成小型、智能且定义良好的组件是云计算领域中的一个重要设计模式。这些组件是绑定应用程序业务的微服务。然而,正如第七章《微服务模式》中所述,随着应用程序历史中功能的增长,对这些微服务的控制变得复杂。解决这个问题的方法之一被称为API 网关。由于 API 网关已经在第七章《微服务模式》中进行了详细探讨,我们现在将只简要介绍它。
API 网关作为云应用程序客户端的前端。有时,请求是对特定微服务的简单调用,但通常 API 网关作为一个粗粒度层接收请求并对与所需功能相关的微服务执行多个调用。这个问题也与微服务聚合器模式和微服务代理模式有关。API 名称正好来自客户端访问微服务的方式,这与 API 的使用方式类似,如下面的图表所示:

根据所讨论的客户端类型,给定系统的功能有时可能有所不同,应用程序可能会以不同的信息响应。当调用系统服务时,移动客户端接收的信息集合比桌面计算机使用的 Web 客户端(浏览器)要小。考虑到这一点,API 网关有一个扩展。扩展的 API 网关可以为每种类型的客户端提供特定的 API。这样,移动客户端将访问移动 API,而 Web 客户端或浏览器将访问 Web API:

服务注册模式
我们知道微服务通过 HTTP/REST 等访问协议暴露。这意味着服务通过 URL 访问。然而,我们处于具有弹性可伸缩性的云世界中。容器和虚拟机的 IP 地址是动态的,并且可以频繁更改。因此,驻留在这些容器中的服务的位置也可能会发生变化。正如我们之前所说,微服务的实例不断被创建和终止。因此,以下问题产生了——微服务的客户端如何处理这个问题?解决方案是实现服务注册模式。服务注册表是已注册服务的数据库。当微服务诞生时,它被记录在这个数据库上,当它死亡时,它从数据库中注销。
微服务客户端访问负责了解微服务是否可用的服务注册表,并为客户端提供其位置。让我们看一下以下图表:

配置服务器
一个应用程序,无论是否在云中,都有诸如凭证和数据库位置、应用程序特定信息和访问外部资源的 URL 等属性。对于传统应用程序,这些属性通常位于属性文件中。如果我们更改这些属性中的一个的值,我们必须停止应用程序并重新启动应用程序容器。当存在不同的环境,如生产、预发布和开发时,这个问题变得更加关键。每个环境都有自己的配置。
在云的背景下,微服务可能位于不同的位置或服务器上。让我们假设这些属性位于同一个微服务位置。要更改一个属性,我们必须定位微服务,更改属性,并重新启动容器。在大量微服务的情况下,这将会变得非常关键。
为了解决这个问题,引入了一层名为云配置服务器的层,其作用是基于微服务管理应用程序的属性。这一层负责维护这些属性,并且每当这些属性中的任何一个发生变化时,这种变化都会在无需重建或重启服务的情况下反映到微服务(或应用程序)中。
配置服务器负责为每个已注册的微服务(服务注册)提供属性。当配置服务器初始化时,微服务属性是从在微服务注册时指定的路径中获取的。获取到的属性随后存储在内存中。这个路径由版本服务器(如 git 或 subversion)控制。当已注册的微服务初始化时,它会前往配置服务器并获取相关的属性。当需要更改微服务的属性时,我们必须前往配置服务器中的路径,更改属性,并重新启动配置服务器,如下面的图示所示:

断路器模式
在具有分布式系统的云场景中,由于连接不足或服务不可用等原因,存在服务失败的概率。在云中创建的服务必须依赖于这种场景,并准备好容错。这样,当出现故障时,故障的原因可能会得到修复,服务将再次工作。然而,也存在由于完全意想不到的事件导致的故障,尽管服务试图再次工作,但它无法做到。当存在超时管理时,这种情况变得更加关键。
对于失败在合理时间内未消失的情况,非常长的等待期会导致计算资源因超时时间而被阻塞。此外,当相同服务的其他要求到达时,情况变得更糟。依赖于该服务(即等待故障结束)的服务也处于不可操作状态,导致问题级联。解决这个问题的方法是实现断路器模式。断路器模式处理需要很长时间才能恢复的故障。
断路器模式防止操作在执行过程中可能失败的情况下反复尝试运行。此外,此模式检查失败是否已解决。如果已解决,代理将请求发送到操作。如果操作失败,失败计数器立即增加。
断路器机制
断路器通过将请求路由到操作或立即返回异常来充当代理。这个模式之所以叫这个名字,是因为它的操作类似于电路。这个代理有三个不同的状态,如下面的图所示:
-
关闭状态:代理将请求发送到操作。代理维护一个之前失败的顺序计数器。如果操作的执行失败,则该计数器增加。如果失败计数器超过设定的阈值,代理将切换到开启状态。然而,此时,启动一个具有指定超时时间的计时器。这样做的目的是在超时期间纠正失败。当计时器到期时,代理变为半开启状态。
-
开启状态:当请求到达时,立即返回异常。
-
半开启状态:将有限数量的请求传递到操作。如果这些请求成功,状态变为关闭,并且失败计数器重置。如果有任何请求失败,状态变为开启,并且重新启动超时计时器。半开启状态对于避免在短时间内向操作发送大量请求非常重要。

概述
在本章中,我们解释了云原生应用程序的概念,以及云应用程序的目标和特性。我们看到了有助于构建云应用程序的主要设计模式。在主要模式中,我们回顾了微服务架构和 API 网关。我们还了解了十二要素方法,它有助于云应用程序的实施。最后,我们探讨了服务注册模式,它返回服务位置,配置服务器,为微服务提供必要的配置,而无需重新启动任何容器,以及断路器,这是一种处理长期故障的模式。
第九章:安全模式
在本章中,我们将探讨安全模式的概念以及它们如何帮助我们实现更好的安全应用程序。我们还将了解单点登录模式以及它如何帮助我们提供安全的应用程序。此外,我们还将了解认证机制和认证拦截器,重点关注如何实现这些概念。阅读本章后,我们将能够创建一个安全应用程序并使用 Java EE 8 实现它。本章涵盖的主题如下:
-
解释安全模式的概念
-
解释单点登录模式的概念
-
实现单点登录模式
-
解释认证机制
-
实现认证机制
-
解释认证拦截器
-
实现认证拦截器
解释安全模式的概念
应用程序与数据和其存储有密切关系。这是因为应用程序基本上由管理数据组成,以便通过自动化任务优化业务,帮助决策,组织任务和管理特定区域。此外,许多公司需要存储敏感数据并验证访问控制。随着时间的推移,对安全软件的需求显著增长,许多公司越来越多地投资于创建安全的应用程序。安全的一个基本要素是安全信息,它遵循以下基本原则:
-
机密性:数据不应被非授权用户或请求访问数据的任何实体访问。
-
完整性:数据不能以非授权的方式更新或修改。
-
可用性:数据应在需要时可用。
-
不可否认性:用户不能使用数据或任何其他过程否认或否认关系。
为了使应用程序安全,它需要提供至少以下基本原则。
安全模式是一组针对常见安全问题的解决方案,这些问题反复出现。这些安全模式中的很大一部分致力于解决与认证相关的问题,这与机密性和完整性原则相关。使用安全模式,开发者可以编写具有高安全性的软件,针对已知的问题和问题使用经过测试和验证的解决方案。
解释单点登录模式的理念
在商业环境中,当用户登录到系统时,他们通常会自动登录到业务内的各种其他系统,而无需再次输入他们的登录详细信息。一个例子是谷歌服务。在这里,如果用户登录到一个谷歌应用程序(Gmail、YouTube、Google Drive),他们将登录到所有可用的谷歌服务。例如,如果我们登录到 Gmail,我们可以访问 YouTube 而无需再次登录。
单点登录是一种安全模式,它创建了一个认证服务,该服务被一个域的多个应用程序共享,以实现认证的中心验证,并在这个域中只对用户进行一次认证。用户随后可以访问该域的所有应用程序,而无需再次进行认证。所有依赖于此类型的应用程序都会与认证服务通信,以验证用户的认证并登录,如果他们尚未登录。这些应用程序可以用任何语言或技术制作,并可以位于多个网络或不同的物理位置。在下面的图中,我们可以看到使用单点登录的认证过程:

在前面的图中,我们有三个用户可以访问的应用程序以及一个登录和验证认证的单一点。这个单一点在我们的图中通过认证服务表示。当用户(在图中由参与者表示)向一个应用程序(应用程序 1、应用程序 2或应用程序 3)发送请求时,该应用程序等待认证服务验证用户是否已登录。如果没有,他们将被重定向到登录页面,用户可以在那里登录。用户登录后,当他们访问应用程序 1、应用程序 2或应用程序 3时,他们不需要再次登录。
单点注销模式是在用户注销某个东西时发生的过程。此时,他们将从这个认证服务登录的所有应用程序中注销。这是一个很好的模式,因为它使得用户只需登录和注销一次即可,同时也将所有认证逻辑从业务逻辑中分离出来,将其放入一个独立的服务中。这促进了认证逻辑和业务逻辑的解耦,使得在不影响业务逻辑的情况下更改认证逻辑成为可能。此模式可以使用 OAuth、SAML 或自定义认证过程来实现。在现实世界中,使用第三方解决方案非常普遍,例如 OAuth0 和红帽单点登录(RH-SSO)。在我们的例子中,我们将完成一个自定义认证过程。
实现单点登录模式
在我们实现 单点登录(SSO)的示例中,我们将通过自定义过程创建认证服务以认证用户,并允许用户使用其登录凭证登录。之后,将生成一个令牌并发送给用户。进一步,我们将创建两个应用程序(App1 和 App2),当用户尝试在不登录的情况下访问这些应用程序时,应用程序将在认证服务上验证用户,用户将无需再次登录即可访问 App1 和 App2。认证服务将是一个使用 JAX-RS 编写的 REST 应用程序,而 App1 和 App2 将是实现 JAX-RS 客户端以验证用户访问的应用程序。因此,将创建以下类以配合我们的示例使用:
-
AuthenticationResource:此功能负责处理登录请求并验证用户的认证。此类使用 JAX-RS 编写,位于认证服务应用程序内部。 -
AuthSession:这是一个包含登录数据和信息的会话。此类具有应用程序范围,即 Java EE 范围。 -
Auth:这是一个表示已登录用户的 Bean。此类包含用户的登录详情、密码和上次登录日期。 -
TokenUtils:这是一个包含生成令牌方法的类。 -
App1:如果用户已登录,则此应用程序发送Welcome to App1文本。如果用户未登录,则此应用程序会引发错误。 -
App2:如果用户已登录,则此应用程序发送Welcome to App2文本。如果用户未登录,则此应用程序会引发错误。 -
Auth:这是一个具有调用认证服务方法的责任的接口。 -
AuthImpl:这是一个实现Auth接口的类。此类是一个 EJB。
App1 和 App2 应用程序没有登录所需的任何流程或逻辑;这是认证服务的责任(验证认证的资源),该服务具有负责此功能的 AuthenticationResource 类。
实现 AuthenticationResource 类
AuthenticationResource 是一个 JAX-RS 资源,这使得登录和验证应用程序的认证成为可能。在下面的代码中,我们有它的实现:
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.util.Date;
import java.util.Optional;
@Path("auth")
public class AuthenticationResource {
@Inject
private AuthSession authSession;
@POST
@Consumes("application/x-www-form-urlencoded")
public Response login(@FormParam("login") String login, @FormParam("password") String password ) {
//If user already logged, then get it token
Optional<String> key = authSession.getToken(login, password);
if ( key.isPresent() ){
return Response.ok(key.get()).build();
}
//Validade login and password on data source
if( !authSession.getDataSource().containsKey(login)
|| !authSession.getDataSource()
.get(login)
.getPassword()
.equals( password) )
return Response.status(Response.Status.UNAUTHORIZED).build();
String token = TokenUtils.generateToken();
//Persiste the information of authentication on AuthSession
authSession.putAuthenticated( token, new Auth(login, password, new Date()));
return Response.ok(token).build();
}
@HEAD
@Path("/{token}")
public Response checkAuthentication(@PathParam("token")String token) {
if( authSession.getAuthenticated().containsKey( token )){
return Response.ok().build();
}
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
在前面的代码块中,我们有AuthenticationResource,它包含用于在数据源中持久化登录信息并获取包含用于验证登录凭证的用户信息的数据源的authSession属性。此外,AuthenticationResource有两个方法:login(String login, String password),用于处理登录请求,以及checkAuthentication(String token),用于允许客户端检查用户是否已认证。在下面的代码块中,我们有login方法,用于登录用户:
@POST
@Consumes("application/x-www-form-urlencoded")
public Response login(@FormParam("login") String login, @FormParam("password") String password ) {
//If user already logged, then get it token
Optional<String> key = authSession.getToken(login, password);
if ( key.isPresent() ){
return Response.ok(key.get()).build();
}
//Validate the login and password on data source
if( !authSession.getDataSource().containsKey(login)
|| !authSession.getDataSource()
.get(login)
.getPassword()
.equals( password) )
return Response.status(Response.Status.UNAUTHORIZED).build();
String token = TokenUtils.generateToken();
//Persiste the information of authentication on the AuthSession.
authSession.putAuthenticated( token, new Auth(login, password, new Date()));
return Response.ok(token).status(Response.Status.CREATED).build();
}
在前面的代码块中,我们可以看到如果用户已经登录,则返回一个令牌作为响应。如果用户未登录,则验证登录和密码详情,并生成一个新的令牌作为响应返回。注意,当客户端向该资源发送POST请求时,会调用此方法。
另一个方法是checkAuthentication(String token),用于允许客户端检查用户是否已认证。在先前的代码块中,我们有这个方法。如果用户已登录,则该方法向客户端返回 200 HTTP 状态码,如果用户未登录,则返回 401 HTTP 状态码:
@HEAD
@Path("/{token}")
public Response checkAuthentication(@PathParam("token")String token) {
if( authSession.getAuthenticated().containsKey( token )){
return Response.ok().build();
}
return Response.status(Response.Status.UNAUTHORIZED).build();
}
注意,当客户端发送HEAD请求时,会调用checkAuthentication(String token)方法。
AuthSession类在AuthenticationResource类中使用。AuthSession类具有应用范围,用于持久化有关已登录用户的信息,并且有一个包含所有用户登录凭证的数据源:
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@ApplicationScoped
public class AuthSession {
private Map<String,Auth> authenticated;
private Map<String,Auth> dataSource;
@PostConstruct
public void init(){
authenticated = new HashMap<>();
dataSource = new HashMap<>();
for(int i = 1; i <= 50; i++){
dataSource.put("login"+i, new Auth("login"+i, "123456") );
}
}
public AuthSession putAuthenticated(String key, Auth auth){
authenticated.put(key, auth);
return this;
}
public AuthSession removeAuthenticated(String key, Auth auth){
authenticated.remove(key, auth);
return this;
}
public Map<String, Auth> getAuthenticated() {
return authenticated;
}
public Map<String, Auth> getDataSource() {
return dataSource;
}
public Optional<String> getToken(String login, String password){
for( String key : authenticated.keySet() ){
Auth auth = authenticated.get( key );
if( auth.getLogin().equals(login)
&& auth.getPassword().equals( password )){
return Optional.of(key);
}
}
return Optional.empty();
}
}
Auth是一个包含用户登录详情信息的 bean:
import java.util.Date;
public class Auth {
private String login;
private String password;
private Date loginDate;
public Auth(){}
public Auth(String login, String password){
this.login = login;
this.password = password;
}
public Auth(String login, String password, Date loginDate){
this.login = login;
this.password = password;
this.loginDate = loginDate;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Date getLoginDate() {
return loginDate;
}
public void setLoginDate(Date loginDate) {
this.loginDate = loginDate;
}
}
如所示,TokenUtils是一个使用generateToken()方法生成新令牌的类:
import java.security.SecureRandom;
import java.util.Date;
public class TokenUtils {
public static String generateToken(){
SecureRandom random = new SecureRandom();
long longToken = Math.abs( random.nextLong() );
return Long.toString(new Date().getTime()) + Long.toString( longToken, 16 );
}
}
实现App1和App2类
在前面的代码块中,我们有App1应用的代码。当这个应用通过一个GET请求被访问时,会向认证服务发送一个请求以验证用户是否已经登录:
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.util.Objects;
@Path("app1")
public class App1 {
@Inject
private Auth auth;
@GET
public Response helloWorld( String token ){
if( !auth.isLogged( token ) ){
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
return Response.ok("Hello World. Welcome to App1!").build();
}
@POST
@Consumes("application/x-www-form-urlencoded")
public Response helloWorld(@FormParam("login") String login, @FormParam("password") String password ) {
if( Objects.isNull(login) || Objects.isNull(password) ){
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
String token = auth.login(login,password);
return Response
.ok("Hello World. Welcome to App1!")
.header("token",token)
.build();
}
}
在前面的代码中,我们有App1类,它包含auth参数,这是一个用于与认证服务集成的 EJB。此外,这个类有两个名为helloWorld的方法,具有不同的签名。在helloWorld(String login, String password)中,完成登录后,将Hello World. Welcome to App1!消息发送给用户。在helloWorld(String token)中,验证令牌,如果它是有效的令牌并且用户已登录,则将Hello World. Welcome to App1!消息发送给用户。
在下面的代码块中,我们有App2类。这个类与App1具有相同的代码,但向用户打印另一条消息。这个类位于另一个应用中,对我们来说称为App2:
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.util.Objects;
@Path("app2")
public class App2 {
@Inject
private Auth auth;
@GET
public Response helloWorld( String token ){
if( !auth.isLogged( token ) ){
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
return Response.ok("Hello World. Welcome to App2!").build();
}
@POST
@Consumes("application/x-www-form-urlencoded")
public Response helloWorld(@FormParam("login") String login, @FormParam("password") String password ) {
if( Objects.isNull(login) || Objects.isNull(password) ){
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
String token = auth.login(login,password);
return Response
.ok("Hello World. Welcome to App2!")
.header("token",token)
.build();
}
}
在下面的代码块中,我们有Auth接口。此接口包含与负责与认证服务集成、验证认证和登录的方法的合约:
public interface Auth {
public boolean isLogged(String token);
public String login(String login, String password);
String logout(String token);
}
在下面的代码块中,我们有AuthImpl类,它实现了Auth接口,并且是一个无状态的 EJB:
import javax.ejb.Stateless;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
@Stateless
public class AuthImpl implements Auth {
private String URL = "http://localhost:8080/javaEE8ExampleSSOAppService/resources/auth";
@Override
public boolean isLogged(String token) {
return prepareWebTarget().path("/"+ token)
.request()
.head().getStatus() == 200;
}
@Override
public String login(String login, String password) {
return prepareWebTarget()
.request()
.post(Entity.form(new Form("login", login )
.param("password", password)),
String.class);
}
@Override
public String logout(String token) {
return prepareWebTarget().path("/"+ token)
.request()
.delete(String.class);
}
protected WebTarget prepareWebTarget() {
return ClientBuilder.newClient().target(URL);
}
}
在前面的代码块中,我们有三个方法,分别称为isLogged、login和logout,它们的签名分别是isLogged(String token)、login(String login, String password)和logout(String token)。
当用户登录到应用程序(无论是App1还是App2),并且当用户使用令牌导航到另一个应用程序时,此用户将不需要再次登录。
解释认证机制
在企业环境中,每个应用程序或资源都需要验证用户访问并确保所有访问这些应用程序或资源的用户都将被识别和验证,以防止未经授权的访问。当用户请求访问受保护区域时,他们需要进行认证,然后服务器将验证他们的权限。为了允许开发者进行用户认证和验证,Java EE 8 必须有一个认证机制,这是 Java EE 8 中的一种常见解决方案,允许开发者以快速和简单的方式对用户进行认证和验证。Java EE 8 有五种机制来认证用户:
-
基本认证
-
基于表单的认证
-
摘要认证
-
客户端认证
-
互惠认证
认证机制通过识别定义用户权限的角色来工作。身份通常通过使用用户名和密码登录来定义。关于用户的信息需要保存在一个数据库中,通常称为域,以便服务器验证信息。为了使用认证机制,我们使用部署描述符文件对其进行配置,使用注解或编程方式。通常,我们将使用注解或部署描述符文件指定一个认证机制。
解释基本认证
如果开发者在未定义认证机制的情况下使用认证机制,则基本认证是默认机制。使用这种认证机制时,如果用户在发送请求时未进行认证,则会返回一个请求用户名和密码的对话框。这种机制并不完全安全,因为用户名和密码容易被捕获,使得中间人攻击成为可能。为了使用这种认证机制,建议使用安全的传输机制,例如 SSL(HTTPS)或 VPN。以下图表示了一个 Java EE 教程,展示了使用基本认证时会发生什么:

解释基于表单的认证
基于表单的认证是一种使用表单请求用户名和密码的认证机制,允许开发者自定义登录和错误屏幕。当用户请求访问受保护资源时,认证机制会向用户发送登录页面,然后用户输入用户名和密码并发送给服务器。如果用户名或密码不正确,服务器将返回错误页面或返回请求的资源。此机制也不安全,因为用户名和密码也容易以这种方式被捕获,使得中间人攻击成为可能。如前所述,建议在使用此认证机制时使用安全的传输机制,例如 SSL(HTTPS)或 VPN。
以下图表示一个 Java EE 教程,展示了使用基于表单的认证时会发生什么:

解释摘要认证
摘要认证是一种使用密码和附加数据的一向加密散列的认证机制。使用此机制,开发者不需要使用安全的传输机制来保护登录凭证。这是因为此机制已经提供了安全性。当用户向服务器发送摘要时,摘要认证需要明文密码以便进行摘要和验证访问,比较发送的明文密码与服务器上保存的密码。
解释客户端认证
客户端认证是一种通过使用其公钥证书而不是用户名和密码来认证客户端的机制,例如在基本和基于表单的认证中。此机制被认为更安全,因为它使用基于 SSL 的 HTTP(HTTPS)并使用客户端的公钥证书。
解释相互认证
相互认证是一种服务器认证客户端,客户端也认证服务器的认证机制。相互认证与基于证书或基于用户名/密码的工作。在基于证书的情况下,客户端请求访问受保护资源,然后服务器响应,向客户端发送其证书。之后,客户端验证服务器证书,如果服务器证书有效,客户端将发送其证书给服务器。服务器随后验证客户端证书,如果客户端证书有效,服务器将授予客户端访问受保护资源的权限。以下图显示了基于证书的相互认证时会发生什么:

在基于用户名/密码的认证中,客户端请求访问受保护的资源,然后服务器响应,向客户端发送其证书。客户端随后验证服务器证书,如果它是有效的,客户端将发送其用户名和密码到服务器。随后,服务器验证凭证,如果这些凭证有效,服务器将授予客户端访问受保护资源的权限。以下图示显示了当使用基于用户名/密码的相互认证时会发生什么:

何时使用部署描述符、注解或程序化配置
要指定和配置认证机制,我们可以使用部署描述符文件、使用注解,或者以程序化的方式定义配置。通常,我们选择注解,因为它在 Java EE 项目中易于使用。然而,有时使用部署描述符或程序化配置可能更简单、更有趣。如果是这种情况,那么在什么适当的场景下使用每种方法呢?
当我们使用部署描述符时,我们定义一个配置文件并创建一组资源的配置。这使得我们能够在单个位置配置我们的安全策略并将其与一组资源关联。然而,当我们使用注解时,我们在特定资源上定义一个策略,如果存在部署描述符,它将覆盖部署描述符。使用注解,我们创建认证的配置,因为它不需要设置 XML 文件。然而,当我们使用程序化配置时,我们可以认证不存在其他特性,并且我们可以以动态的方式使用认证机制。此外,当我们想要为一系列资源创建配置时,我们可以使用部署描述符文件。当我们想要使用动态行为创建认证时,我们使用程序化配置,而当我们需要以简单的方式创建配置时,我们使用注解。
实现认证机制
在我们实现认证机制的示例中,我们将创建一个包含接收请求并返回 hello world 消息给用户的资源的应用程序。然而,此资源受到保护,用户需要认证才能访问此资源。此外,我们将在部署描述符文件中设置一些安全策略和关联,一些安全策略通过注解应用于某些资源,一些安全策略通过程序性配置应用于某些资源。我们还将使用基本认证。为了使用部署描述符文件和程序性配置配置安全策略,我们将使用 JAX-RS 资源。为了使用注解配置安全策略,我们将使用 Servlet。这里使用的所有示例在 Web 应用中都是安全的,但 Java EE 8 允许在企业应用中使用认证机制。在这个例子中,使用了以下类:
-
web.xml:这是一个用于配置认证机制的部署描述符文件。 -
HelloWorld:这是一个包含由部署描述符文件和程序性配置使用的认证机制的 JAX-RS 资源类。 -
HelloWorldServlet:这是一个包含由注解使用的认证机制的 Servlet 类。
实现 web.xml 文件
web.xml 是一个 Web 应用的部署描述符,它包含许多关于 Web 应用的配置。在下面的部署描述符中,我们有我们 Web 应用的安全配置:
<?xml version="1.0" encoding="UTF-8" ?>
<web-app >
<security-constraint>
<web-resource-collection>
<web-resource-name>helloworld</web-resource-name>
<url-pattern>/resources/helloworld/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
</web-app>
web.xml that defines a URL pattern:
<web-resource-collection>
<web-resource-name>helloworld</web-resource-name>
<url-pattern>/resources/helloworld/*</url-pattern>
</web-resource-collection>
web.xml that defines security roles:
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
web.xml that defines the authentication mechanism:
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
实现 HelloWorld 类
HelloWorld 类是一个响应发送到 /helloworld 路径的请求的 JAX-RS 资源。这个类有两个方法,分别具有 helloWorldWithDeploymentDescriptor() 和 helloWorldWithProgrammatically() 签名。在 helloWorldWithDeploymentDescriptor() 中,认证验证由部署描述符配置导向,而在 helloWorldWithProgrammatically() 中,认证验证由开发者编写的代码导向:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
@Path("helloworld")
public class HelloWorld {
@Context
private SecurityContext securityContext;
@GET
@Path("/deploymentdescriptor")
public Response helloWorldWithDeploymentDescriptor(){
return Response
.ok("Hello World. Welcome to App with validation by deployment descriptor!")
.build();
}
@GET
@Path("/programmatically")
public Response helloWorldWithProgrammatically() {
if (!securityContext.isUserInRole("user")) {
return Response.status(401).header("WWW-Authenticate", "Basic").build();
}
return Response
.ok("Hello World. Welcome to App with validation by programmatically.")
.build();
}
}
HelloWorld class that includes the helloWorldWithDeploymentDescriptor() method:
@GET
@Path("/deploymentdescriptor")
public Response helloWorldWithDeploymentDescriptor(){
return Response
.ok("Hello World. Welcome to App with validation by deployment descriptor!")
.build();
}
HelloWorld class that includes a helloWorldWithProgrammatically() method that uses a code to validate the role associated with each user that sent a request:
@GET
@Path("/programmatically")
public Response helloWorldWithProgrammatically() {
if (!securityContext.isUserInRole("user")) {
return Response.status(401).header("WWW-Authenticate", "Basic").build();
}
return Response
.ok("Hello World. Welcome to App with validation by programmatically.")
.build();
}
要以程序方式在浏览器中显示对话框,请使用以下代码:
if (!securityContext.isUserInRole("user")) {
return Response.status(401).header("WWW-Authenticate",
"Basic").build();
}
实现 HelloWordServlet 类
HelloWorldServlet 类是一个响应发送到 /helloworld/annotation 路径的请求的 Servlet 类。这个类有一个 doGet(HttpServletRequest request, HttpServletResponse response) 方法,它响应所有 GET 请求。在此方法处理完毕后,进行认证验证,如果用户与 user 角色相关联,则处理该方法。如果不相关联,则将对话框返回给用户:
import javax.annotation.security.DeclareRoles;
import javax.servlet.ServletException;
import javax.servlet.annotation.HttpConstraint;
import javax.servlet.annotation.ServletSecurity;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "HelloWorldServlet", urlPatterns = "/helloworld/annotation")
@DeclareRoles("user")
@ServletSecurity(@HttpConstraint(transportGuarantee = ServletSecurity.TransportGuarantee.NONE,
rolesAllowed = {"user"}))
public class HelloWorldServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("Hello World. Welcome to App with
validation by annotation!");
}
}
解释认证拦截器
在企业应用中,认证是一个非常重要的过程,这可以通过许多技术来完成。在 Java EE 8 中,这些技术之一是拦截器模式。
拦截器模式是一种高级编程技术,它使得在处理对象调用之前或之后拦截对象调用并处理算法成为可能。这就像面向切面编程(AOP)一样,使得在不改变其逻辑的情况下将行为包含在过程中成为可能。要使用 Java EE 8 实现此功能,我们可以使用 EJB 拦截器或 CDI 拦截器。我们还可以根据我们是否正在拦截 EJB 类/方法或 CDI 类/方法来选择要使用的拦截器类型。
认证拦截器是一种使用 EJB 拦截器或 CDI 拦截器通过拦截器模式的技术。这样做是为了在面向业务逻辑的方面使用业务逻辑之前或之后添加认证逻辑。然后我们可以验证认证,而无需将认证逻辑与业务逻辑耦合。在 EJB 类中使用业务逻辑时,最好使用 EJB 拦截器,而在 Web 层中使用 CDI 类时,最好使用 CDI 拦截器。
实现认证拦截器
在我们实现认证拦截器的最终示例中,我们将创建一个包含资源的应用程序,这些资源接收用户请求并向用户返回hello world消息,但此资源受到保护,用户需要认证才能访问资源。然而,这种保护是通过使用 CDI 拦截器实现的拦截器来实现的,它捕获对资源的调用并验证用户访问。此外,我们将使用基本认证机制进行认证验证;用户数据源将由应用程序服务器和 Java EE 管理,而不是由自定义数据源管理。在这个例子中,以下类被使用:
-
DataSource: 这是一个包含安全用户信息的数据库源。 -
Auth: 这是一个用于定义验证认证和授权方法的接口。 -
AuthImpl: 这是一个实现Auth接口的类。 -
Authentication: 这是一个用于配置要拦截的方法的限定符。 -
AuthenticationInterceptor: 这是一个拦截对HelloWorld方法调用的拦截器,并验证用户访问权限。 -
HelloWorld: 这是一个包含部署描述符文件和程序配置中使用的认证机制的 JAX-RS 资源类。 -
AuthUtils: 这是一个用于认证的实用工具类。
实现 CDI 拦截器
要实现 CDI 拦截器,我们需要创建一个用于配置要拦截的类或方法的限定符。在下面的代码块中,我们有一个名为Authentication的限定符,它有一个名为 roles 的参数,这些参数用于声明允许访问资源的角色:
import javax.enterprise.util.Nonbinding;
import javax.interceptor.InterceptorBinding;
import java.lang.annotation.*;
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Authentication {
@Nonbinding String[] roles() default {};
}
在下面的代码块中,我们有AuthenticationInteceptor类,它用作拦截器。这个类被@Authentication注解标注,表示这个类将拦截所有标注了@Authentication的方法调用,并将处理标注了@AroundInvoke的方法:
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
@Authentication
@Interceptor
public class AuthenticationInterceptor implements Serializable{
@Inject
private Auth auth;
@AroundInvoke
public Object authentication(InvocationContext context) throws
IOException {
HttpServletRequest request = getHttpServletRequest( context );
HttpServletResponse response = getHttpServletResponse( context );
String[] credentials = AuthUtils.readBasicAuthHeader( request );
if(credentials.length < 2){
prepareDialogBox( response );
return null;
}
String login = credentials[AuthUtils.INDEX_LOGIN];
String password = credentials[AuthUtils.INDEX_PASSWORD];
Authentication authentication =
context.getMethod().getAnnotation( Authentication.class );
if( !auth.isAuthenticated( login, password ) ){
prepareDialogBox( response );
return null;
}
if ( Arrays.stream(authentication.roles()).noneMatch( role ->
auth.isAuthorized( login, role )) ){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return null;
}
try {
return context.proceed();
} catch (Exception e) {
e.printStackTrace();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
return null;
}
public void prepareDialogBox( HttpServletResponse response ) throws
IOException {
response.addHeader("WWW-Authenticate", "Basic");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
private HttpServletRequest getHttpServletRequest(InvocationContext
ic) {
return (HttpServletRequest) Arrays
.stream(ic.getParameters()).filter(p -> p instanceof
HttpServletRequest )
.findFirst()
.get();
}
private HttpServletResponse
getHttpServletResponse(InvocationContext ic) {
return (HttpServletResponse) Arrays
.stream(ic.getParameters()).filter(p -> p instanceof
HttpServletResponse )
.findFirst()
.get();
}
}
@Authentication:
@Authentication
@Interceptor
public class AuthenticationInterceptor implements Serializable{
...
}
注意,这个拦截器期望目标方法的参数为HttpServletRequest和HttpServletResponse。这样做是为了允许对认证和授权进行验证。
在下面的代码块中,我们有一个拦截器的片段。它配置了一个在调用被拦截时处理的方法:
@AroundInvoke
public Object authentication(InvocationContext context) throws IOException {
...
}
在前面的代码块中,我们有Auth接口:
public interface Auth {
public Boolean isAuthorized(String login, String role);
public Boolean isAuthenticated(String login, String password);
}
在前面的代码块中,我们有实现Auth接口的类。在这个类中,我们将从数据源中查询用户信息。这个类是一个 EJB:
import javax.ejb.Stateless;
import javax.inject.Inject;
@Stateless
public class AuthImpl implements Auth {
@Inject
private DataSource dataSource;
@Override
public Boolean isAuthorized(String login,String role) {
return dataSource.readUserRoles( login ).contains( role );
}
@Override
public Boolean isAuthenticated(String login, String password) {
return dataSource.readUserPassword( login ).contains( password );
}
}
在前面的代码块中,我们有一个DataSource类。这个类包含读取用户信息逻辑,并且在这个数据源中的所有信息都保持为映射形式。然而,我们可以将这些信息保存到关系型数据库、非关系型数据库、文件系统或 LDAP 中。这个类具有应用范围,并在整个应用生命周期中维护所有安全数据:
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ApplicationScoped
public class DataSource {
private Map<String, List<String>> roles;
private Map<String, String> passwords;
@PostConstruct
public void init(){
roles = new HashMap<>();
roles.put("rhuan", Arrays.asList("user","admin"));
roles.put("joao", Arrays.asList("user","admin"));
passwords = new HashMap<>();
passwords.put("rhuan", "123456");
passwords.put("joao", "123456");
}
public List<String> readUserRoles(String login){
return roles.get( login );
}
public String readUserPassword(String login){
return passwords.get( login );
}
}
在下面的代码块中,我们有AuthUtils。这是一个认证工具类。这个类使用readBasicHeader方法,该方法接收HttpServletRequest作为参数,并在请求使用基本认证时提取用户名和密码:
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.Charset;
import java.util.Base64;
public class AuthUtils {
public static final int INDEX_LOGIN = 0;
public static final int INDEX_PASSWORD = 1;
public static String[] readBasicAuthHeader( HttpServletRequest request ){
final String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Basic")) {
String base64Credentials = authorization.substring("Basic".length()).trim();
String credentials =
new String(
Base64.getDecoder().decode(
base64Credentials),
Charset.forName("UTF-8"));
return credentials.split(":", 2);
}
return new String[0];
}
}
在下面的代码块中,我们有beans.xml,它包含一个<interceptors>标签,声明了所有 CDI 拦截器。CDI 拦截器要正常工作,需要这个配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
<interceptors>
<class>com.packt.rhuan.interceptors.AuthenticationInterceptor</class>
</interceptors>
</beans>
实现 JAX-RS 资源
HelloWorld. This class receives a request and returns a message to the client. Before the execution of the resource logic, AuthenticationInterceptor intercepts a call to a method and processes the authentication logic:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
@Path("helloworld")
public class HelloWorld {
@GET
@Authentication(roles = {"user"})
public Response helloWorld(@Context HttpServletRequest request,
@Context HttpServletResponse response) {
return Response
.ok("Hello World. Welcome to App with validation by
authentication interceptor!")
.build();
}
}
注意,helloWorld(@Context HttpServletRequest request, @Context HttpServletResponse response)方法具有@Authentication(roles={"user"})注解,以及HttpServletRequest和HttpServletResponse参数。通过@Authentication(roles={"user"}),我们配置了helloWorld(@Context HttpServletRequest request, @Context HttpServletResponse response)方法被AuthenticationInterceptor拦截。如前所述,目标方法必须具有HttpServletRequest和HttpServletResponse作为参数:
@GET
@Authentication(roles = {"user"})
public Response helloWorld(@Context HttpServletRequest request, @Context HttpServletResponse response) {
...
}
摘要
在本章中,我们介绍了安全模式以及如何使用 Java EE 及其最佳实践实现安全应用。我们还探讨了单点登录(SSO)、认证机制和认证拦截器。此外,我们展示了如何使用 Java EE 8 实现这些功能。
在单点登录这个话题上,我们使用 JAX-RS 实现单点登录,并创建了一个服务来处理所有的认证和授权逻辑。正如讨论的那样,实现单点登录通常由第三方应用程序完成,例如红帽单点登录(RH-SSO)或 Oracle 企业单点登录,但我们也可以创建自己的解决方案。
我们学习了认证机制以及如何使用 Java EE 8 中的这个 HTTP 工具。使用 Java EE 8,我们实现了一个基本机制,并在应用服务器上的领域保存了用户信息。此外,我们还演示了如何在 servlet 和 REST 资源中实现认证机制。
我们使用 CDI 拦截器实现了一个认证拦截器,并将其配置为拦截 JAX-RS 资源。JAX-RS 资源认证拦截器通常是一个更好的解决方案,但我们也可以在 servlet 或 EJB 类中使用这个解决方案。
在下一章中,我们将探讨部署模式,为什么这些模式对于项目的成功至关重要,什么是蓝绿部署,为什么使用蓝绿部署很重要,什么是持续部署,以及为什么我们应该使用持续部署。
第十章:部署模式
在本章中,我们将探讨部署模式,为什么我们会使用它们,以及它们如何影响应用程序的交付。我们还将涵盖金丝雀部署、蓝绿部署、A/B 部署和持续部署的概念。阅读本章后,我们应该熟悉部署模式的概念。本章我们将涵盖以下主题:
-
部署模式的概念
-
金丝雀部署的概念
-
蓝绿部署的概念
-
A/B 测试的概念
-
持续部署的概念
解释部署模式的概念
应用程序不断接收更新,这些更新旨在创建新功能或纠正任何问题。这些新功能和更新需要推广到生产级别,而不会造成任何问题或延迟服务。此外,有时需要将应用程序的新版本交付给特定用户群体,例如某些国家或某些业务领域的用户。
当我们在这个背景下谈论交付时,我们是指将应用程序的新版本发布到生产环境中。一个软件项目有各种步骤需要评估和考虑,以便允许交付优质的软件。
这些步骤包括遵循良好的流程以获得业务角色、测试应用程序的良好流程、开发应用程序代码的良好流程以及将项目交付到生产环境的良好流程,这是项目的目标。所有软件项目都有一个重要的共同目标——在不产生任何附带影响的情况下交付优质、高质量的软件。特别是部署步骤是一个非常重要的步骤,因为这是软件交付的时候;项目的整体目标将在这里实现,但如果发生任何错误,则所有项目都可能受到影响。因此,部署模式应运而生。
部署模式(也称为部署策略)是一组针对常见部署相关问题的解决方案。这些模式使部署过程更安全,并降低新版本中发生错误的可能性。一些部署模式的例子包括:
-
-
金丝雀部署
-
蓝绿部署
-
A/B 测试
-
持续部署也是一种部署模式,但我们没有将其包括在列表中,因为它是最全面的方法,它创建了一个管道并在交付的所有步骤中工作。此外,持续部署更准确地描述了自动化开发阶段的解决方案,并且可以与之前列出的任何部署模式一起使用。因此,我们提到的模式在较小的范围内工作,而持续部署模式在较大的范围内工作。在以下图中,您可以了解这两种类型的部署模式是如何工作的,以及它们遵循的阶段:

在某些文献中,持续部署的概念与持续交付的概念一起被覆盖。这是因为这两个概念非常相似,它们之间只有一些细微的差别。这些概念的差异将在本章的“解释金丝雀部署的概念”部分详细展示。
解释金丝雀部署的概念
如前所述,当我们编写应用的新版本时,我们需要在不停止或以任何方式延迟服务的情况下交付它。因此,一个重要的步骤是首先部署和测试新版本。如果发生错误,我们需要回滚部署并维护应用的旧版本,直到解决新版本中的任何问题。金丝雀部署就是为了解决此类部署相关的问题而创建的。
金丝雀部署是一种部署模式,它允许我们将应用的新版本交付给服务器子集。然后,可以测试应用的新版本,如果发生错误,则交付回滚并保持旧版本,将新版本传播到剩余服务器。在此模式中,我们可以定义一些服务器为金丝雀服务器。部署首先在金丝雀服务器上发生,之后在金丝雀服务器上进行测试。如果满意,新版本将被传播(或部署)到剩余服务器。此模式包括以下四个基本步骤:
-
定义金丝雀服务器
-
将应用部署到金丝雀服务器
-
测试应用并验证它是否满足我们的标准
-
将应用部署到剩余服务器
以下图示展示了这些步骤的视觉表示:

要将金丝雀部署作为交付应用的解决方案,您首先需要配置一个代理,该代理将请求重定向到非金丝雀服务器。以下图示展示了金丝雀部署的工作原理:

定义金丝雀服务器
在此步骤中,将选择一些服务器作为金丝雀服务器,这是在剩余服务器上部署应用后用于测试新版本的服务器。在此,我们选择哪些服务器应该是金丝雀服务器。确保所有金丝雀部署步骤完成而不影响访问应用的用户非常重要。在创建定义时需要考虑的一些重要问题如下:
-
我们是否想要测试应用在多个服务器实例中的行为?
-
我可以使用多少服务器作为金丝雀服务器而不会影响用户访问?
-
我的程序与同一环境中的新版本和旧版本程序的工作情况如何?
-
将执行哪些测试?
使用的金丝雀服务器数量非常重要,所以我们建议金丝雀服务器的数量不超过生产环境中服务器总数的 50%。此外,在定义金丝雀服务器的数量时,我们需要决定我们将测试哪些场景。无论我们是否测试应用程序与多台服务器良好工作的能力,我们仍然至少需要两个金丝雀服务器。使用两个金丝雀服务器通常是一个好主意,因为这个数字很少会超过生产环境中服务器总数的 50%。换句话说,生产环境中的服务器总数通常大于四台。如果我们假设在生产环境中使用两个服务器作为金丝雀服务器时,总共有四台服务器,那么其他两台服务器将用于访问应用程序的旧版本。根据访问用户数量,应用程序不应该有任何性能问题。
将应用程序部署到金丝雀服务器上
当应用程序部署在金丝雀服务器上时,用户无法访问金丝雀服务器,代理也无法将请求重定向到这些服务器。此时,生产环境保持两个应用程序版本——旧版本和新版本。应用程序需要准备在相同环境中与两个版本一起工作。请注意,在这种情况下,用户不会失去对应用程序的访问,新版本的部署以透明的方式完成,且不会影响用户。
测试应用程序并验证其是否满足我们的标准
在此步骤中,通过评估某些过程来测试应用程序是否满足我们的标准,例如与其他应用程序集成、CPU、内存、磁盘使用率和数据库连接。这些测试可以通过自动化测试、内部用户或少数最终用户来完成。如果在这一步骤中发现错误,则部署将被中止并回滚,金丝雀服务器将接收旧版本,然后提供给最终用户。
将应用程序部署到剩余的服务器上
此步骤仅在应用程序已被批准并验证为满意的情况下初始化。当此步骤开始时,金丝雀服务器成为最终用户访问的服务器;其他服务器不会在此接收新的请求,因为部署过程已经开始。然后代理将所有最终用户的请求重定向到具有新版本(换句话说,金丝雀服务器)的服务器,并开始另一个服务器的部署。一旦部署完成,所有服务器都能够接收来自最终用户的访问。
使用金丝雀部署时,最终用户不应意识到正在发生部署。只有在接口发生变化,或者某些功能对用户可用或不可用时,用户才会意识到。
解释蓝/绿部署的概念
蓝绿部署在某种程度上与金丝雀部署相似,因为它使用分区过程部署应用程序的新版本。蓝绿方法将应用程序部署到生产环境中的服务器子集,然后将新版本传播到剩余的服务器。这种部署模式在目标上与金丝雀部署不同,因为蓝绿部署的目标是在部署期间减少应用程序的停机时间。然而,金丝雀部署的目标是减少与新版本相关的生产环境中错误的发生。此外,在金丝雀部署中,生产环境可以同时保留新版本和旧版本并接收请求,而在蓝绿部署中,只有一个版本响应请求。
蓝绿部署是一种部署模式,它使得在不对最终用户使应用程序失效的情况下部署应用程序的新版本成为可能。使用此模式,部署通过一组服务器进行。如果部署成功完成,则剩余的服务器将进行部署。
此模式有三个步骤:
-
定义接收首次部署的服务器组
-
将应用程序部署到一组服务器
-
将应用程序部署到剩余的服务器
定义接收首次部署的服务器组
在蓝绿部署中,定义接收首次部署的服务器组非常简单,因为不会运行任何测试,也不会进行检查;部署过程只需成功完成即可。一个良好的做法是将生产环境分为两组服务器,并选择其中一组首先接收部署。每个组通常将拥有大约 50%的生产服务器。
将应用程序部署到一组服务器
在此步骤中,请求被重定向到该组的服务器,以便进行部署,并且要部署的组接收新版本应用程序的部署。在此期间,要部署的组的所有服务器都将处于“死亡”状态,并且只有在组内的部署过程完成后才会响应请求。以下图表说明了此过程的示例:

将应用程序部署到剩余的服务器
只有当部署过程成功完成,并且选择了接收首次部署的组时,此步骤才会启动。当此步骤启动时,选择接收首次部署的服务器组返回活动状态,并且所有由最终用户发送的请求都由部署了新版本的这个组的服务器处理。然后初始化剩余服务器的部署,并且在部署完成之前,剩余的服务器处于“死亡”状态。当部署完成后,所有服务器都将能够接收和处理请求,并且所有服务器都将保持应用程序的新版本。此过程在以下图表中展示:

解释 A/B 测试的概念
我们有时可能会更新我们的应用程序,并希望检查其对最终用户及其行为的影响。通常,这类更新与应用程序的可用性或受欢迎程度有关,并关联到 UI 更改。为了使我们能够检查更新对应用程序的影响,我们需要创建一组最终用户组,他们将收到新的更新并要求评估它们。我们之前讨论的部署模式并不能解决我们的问题,因为它们无法将应用程序的新版本推向一个单独的最终用户组。尽管它们允许我们在一个分离的组中测试应用程序的功能,但那个组并不持久。
然而,A/B 测试是一种部署模式,它允许我们仅将应用程序的新版本推向选定的最终用户组。这使得我们能够评估应用程序新版本对最终用户的影响,从而决定是否将其推向所有最终用户。这种部署模式在 Facebook、LinkedIn 和 Twitter 等流行应用程序中普遍使用,因为这些应用程序在其功能在用户中受欢迎时最为成功。此模式可以通过应用级开关或通过将最终用户重定向到相应应用程序的代理来实现。以下图表展示了 A/B 测试模式的示例:

此模式需要以下四个步骤:
-
定义一组接收应用程序新版本的最终用户
-
定义接收新版本的服务器
-
将应用程序的新版本部署到选定的服务器
-
评估应用程序新版本的影响
定义一组最终用户
在此步骤中,我们选择一组最终用户以接收应用程序的新版本。为了定义此组,需要评估多个因素。其中一些评估包括:
-
新版本中存在的功能
-
最终用户的本地化
-
业务角色
为 A/B 测试所选的最终用户需要由应用程序定义并标记,以便它可以区分能够访问新版本的最终用户和无法访问的用户。这可以通过使用 cookie、如果组与位置相关则通过 IP 地址过滤,或者使用其他授予最终用户访问新版本权限的机制来完成。
定义接收新版本的服务器
在这一步,我们定义将接收新版本部署的服务器组。接收部署的服务器选择和数量需要根据能够访问新版本的最终用户百分比来评估。根据各种场景,我们还应该评估用户的本地化情况。
部署新版本
在这一步,我们将新版本的应用程序部署到所选的服务器组。然后这些服务器将变为无效,部署将开始。在这一步,建议使用金丝雀部署或蓝绿部署来最小化错误的可能性。一旦部署完成,我们将允许所选的最终用户访问应用程序的新版本。
评估新版本的影响
在这一步,我们评估应用程序新版本的影响。这一步可以使用许多不同的工具和技术来完成,其使用取决于我们对于新功能的目标是什么,涉及这些新功能的角色是什么,以及根据业务逻辑可能出现的任何其他问题。这项任务通常包括收集数据并分析它以验证最终用户使用新功能的方式。评估完成后,我们就可以决定是否将新版本推向剩余的最终用户,或者从应用程序中移除他们。
解释持续部署的概念
向用户交付发布版本的过程包括重要的步骤,我们有时可以通过手动过程完成这些步骤。世界正在快速发展,因此发布交付也需要迅速。考虑到这一点,自动化流程是提高发布交付速度的绝佳解决方案;因此,持续部署作为持续交付的演变而产生,而持续交付本身又是持续集成的演变。
持续部署是一种部署模式,它创建了一个管道,其中每个步骤都通过自动化过程执行。所有步骤都将在没有人为干预的情况下执行。如果新的发布版本进入管道,除非发生错误,否则所有步骤都将自动执行。重要的是要知道,持续部署不是持续交付或持续集成——尽管这些部署模式有许多相似之处,但它们都是不同的。它们之间的主要区别在于自动化程度;例如,持续集成比持续交付更自动化,而持续交付又比持续部署更自动化。以下图表说明了这两种部署模式的管道以及它们的自动化程度:

持续部署的主要目标是最大限度地减少发布开发与在生产环境中交付之间的时间。它自动化了开发者的步骤,以最大限度地减少人为错误的可能性并使交付更安全。正如所讨论的,持续部署可以与蓝绿部署、金丝雀部署和 A/B 测试一起使用,这些模式将在部署到生产和部署后测试步骤中发挥作用。
要实现持续部署,必须拥有良好的测试文化。这是因为测试的质量将决定发布版本的质量以及其实施的成功。此外,文档需要与所有新版本一起更新,确保应用程序的所有更新都反映在文档中。
摘要
在本章中,我们介绍了部署模式,包括金丝雀部署、蓝绿部署、A/B 测试和持续部署。我们还讨论了使用这些部署模式的原因以及如何决定它们最佳的使用方式。
在解释部署模式的概念部分,我们探讨了部署模式的基本概念,并研究了它们在商业环境中的应用。同样,在解释金丝雀部署的概念、解释蓝绿部署的概念、解释 A/B 测试的概念和解释持续部署的概念部分,我们探讨了每种模式是什么以及为什么使用它们。最后,在解释持续部署的概念部分,我们讨论了持续集成、持续交付和持续部署之间的区别。
在下一章中,我们将介绍操作模式、性能和可扩展性模式以及管理和监控模式的概念。
第十一章:操作模式
在本章中,我们将探讨操作模式,特别是关注为什么我们使用它们以及它们如何影响应用项目。然后,我们将涵盖性能和可扩展性模式以及管理和监控模式。阅读本章后,我们将了解操作模式的概念。本章将涵盖以下主题:
-
操作模式的概念
-
性能和可扩展性模式的概念
-
管理和监控模式的概念
解释操作模式的概念
要成功解决问题,我们需要评估多个重要步骤,以确保错误的可能性最小化。这些步骤如下:
-
正确识别问题
-
定义解决问题的操作
-
开发解决问题的操作
在商业环境中,无论是在应用项目中还是在其他领域,快速响应问题是任务成功的关键。同时,我们还需要创建一个定义明确的解决问题流程,这将增加成功的可能性,使我们的工作更有条理,并导致更可靠的解决方案。
在第一步,如前所述,我们确定需要解决的问题。在这里,定义问题的边界以及评估所识别的问题是否确实是问题,这一点非常重要。
接下来,我们为问题定义一个高级解决方案。在这一步,解决方案将被描述,但无需担心如何实现。相反,我们关注在较高层次思想中的解决方案的线索或操作。高级、中级和低级思想指的是通用思考的程度,其中高级思想比中级更通用,而中级思想又比低级思想更通用。
在第三步,我们开发出解决问题的操作。在这一步,工作更加实际,因此我们需要以中低层次的思想进行工作。
操作模式通过促进对重复出现的问题的常见高级解决方案来工作。然而,尽管操作模式描述了问题的解决方案,但它们并不关心解决方案是如何开发的。因此,这些模式帮助我们简化问题和解决方案,并定义一个良好、可执行的过程。
解释性能和可扩展性模式的概念
在商业环境中,为了生成响应和解决方案,过程和任务需要快速完成。考虑到这一点,应用程序也需要更加高效和可扩展。性能指的是应用程序响应请求的速度,而可扩展性指的是应用程序在增加请求时能够响应而不影响其性能或容量的能力。换句话说,性能更多地关于服务请求所需的时间,而可扩展性是关于系统能够根据需要升级和降级资源。
在商业环境中,关于性能的问题通常是围绕读取数据或外部资源(如文件系统或网络中的其他应用程序)的问题。由于商业环境通常没有硬编码的逻辑或具有长数学计算的代码,因此由错误的逻辑或算法引起的性能问题较为罕见。商业环境的算法通常使用逻辑通过角色读取和保存数据;因此,性能增长通常包括尽可能快地读取和写入数据以及访问资源。
可扩展性与性能直接相关,因为它包括扩展应用程序的规模。换句话说,如果我们扩展了应用程序的规模,我们应该能够响应请求的增加而不影响性能,否则应用程序将以增加的性能响应请求。
性能和可扩展性模式是用于解决性能和可扩展性常见问题的操作模式,它们为应用程序提供始终如一的高性能和可用性。在本章中,我们将介绍以下性能和可扩展性模式:
-
缓存旁路(Cache-aside): 这种模式根据需求从数据存储中将数据加载到缓存中。
-
CQRS(Command Query Responsibility Segregation): 这种模式通过使用单独的接口将读取数据的操作与更新数据的操作分离。
-
事件溯源(Event sourcing): 这种模式使用只追加存储来记录对领域内数据所采取的操作。
-
索引表(Index table): 这种模式在数据存储中创建索引,这些索引经常被查询引用。
-
物化视图(Materialized view): 当数据不是理想地格式化以进行所需的查询操作时,这种模式会在一个或多个数据存储中生成预填充的视图。
-
分片(Sharding): 这种模式将数据存储划分为一组水平分区或分片。
-
静态内容托管(Static content hosting): 这种模式将静态内容部署到可以直接向客户端交付数据的基于云的存储服务。
缓存旁路模式
如前所述,提高性能通常意味着最小化读取数据所需的时间。实现这一目标的一种方法是通过缓存旁路模式。
缓存旁路模式是一种按需从数据存储将数据加载到缓存中的模式。如果应用程序更新了任何数据缓存,它也会将数据更新到数据存储。当用户请求数据且数据不可用时,数据会被加载到缓存中。因此,当再次访问此数据时,应用程序将从缓存中读取并响应用户。这种模式帮助我们保持缓存中的数据和数据存储之间的数据一致性。缓存是另一个数据源,它维护着从原始数据存储中复制的数据,允许应用程序快速读取数据。然而,这种模式并不能保证数据存储和缓存之间的数据一致性,因为另一个应用程序可以更新数据存储中的数据,从而使缓存过时。
以下图示说明了缓存旁路模式的工作原理:

正如我们所见,当应用程序从数据存储读取任何数据时,应用程序首先咨询缓存中的数据。然而,如果数据不存在,则应用程序会咨询数据存储中的数据。当应用程序咨询数据存储中的数据时,数据存储返回的数据随后被加载到缓存中,以便应用程序在将来更快地读取数据。当应用程序更新缓存中的数据时,数据将被保存到数据存储中,然后缓存中的数据将被更新或使无效。
何时使用缓存旁路模式
建议在数据不经常更新或并发更新不频繁时使用缓存旁路模式。重要的是要注意,缓存旁路模式只有在必要时才应使用。这是因为缓存增加了应用程序的复杂性,因为涉及数据定义的脏读有时可能发生。考虑到这一点,我们建议您只有在读取数据是一个缓慢的过程时才应用此模式。
缓存数据的生命周期
缓存数据的生命周期是关于缓存旁路模式的一个重要问题,因为生命周期策略决定了缓存是否高效。生命周期定义了数据的过期时间,根据一个过期策略,如果数据在指定的时间限制内未被访问,则该策略将使数据无效并从缓存中移除。因此,定义一个好的生命周期策略意味着定义一个既不太长也不太短的生命周期。还强烈建议您不要创建一个全局过期策略,而应该为数据存储中的每种类型的项目创建一个。这是因为某些数据比其他数据更新得更频繁。
驱除数据
一些缓存有有限的大小,并需要策略来弹出数据。此外,一些缓存选择最近最少使用的数据来弹出。然而,我们可以自定义此策略,甚至可以使用我们自己的逻辑从缓存中删除数据。通常,用于删除数据的逻辑大小有限,但我们也可以使用有关更新的逻辑。
弹出数据与设置过期策略不同,因为过期策略是一种静态逻辑,当数据过期时从缓存中删除数据。然而,弹出数据涉及动态逻辑。过期策略允许我们最小化脏读的可能性,而弹出数据允许我们创建逻辑,以最小化缓存达到其限制的可能性并优化缓存数据的一致性。
缓存预热
我们可能希望以针对应用程序使用优化的数据开始我们的缓存。因此,我们应该使用应用程序启动过程中的数据填充缓存。重要的是指出,我们需要评估在启动过程中加载的数据的过期时间;如果数据在应用程序使用之前过期,则加载启动过程数据是不必要的。
一致性
缓存旁路模式不能保证缓存中的数据与数据存储中的数据之间的一致性。这是因为外部应用程序或资源可以在任何时间更新缓存中没有反映的数据。在实现时记住一致性非常重要,以避免脏读的风险。
本地(内存)缓存
为了提高读取数据的过程,只要数据被频繁访问,缓存可以是本地和内存的。由于本地缓存是私有的,因此需要创建另一个缓存。在这种情况下,数据可以复制到任何本地缓存。为了实现本地缓存,我们需要一个具有足够内存的服务器,以允许我们的应用程序运行,并确保我们不会导致内存溢出。
CQRS 模式
在应用程序中,使用相同的模型或实体执行从数据存储读取和写入数据的命令是一种好方法。这是因为管理数据在编程中通常很容易,我们还可以使用脚手架来生成项目。然而,当存在大量写入数据,并且并发写入操作的风险很高时,合并数据的需求更为迫切。当我们使用相同的模型来执行读取和写入数据时,读取数据的操作通常读取模型中的所有数据,写入数据的操作通常写入模型中的所有数据。有时,我们可能需要写入比模型中更少的列数据。此外,使用单个模型读取和写入数据可以提高安全性,因为某些数据仅供查询使用。
CQRS(命令和查询责任分离)是一种将读取操作与写入操作分离的模型,为这些操作创建了一个独立且解耦的接口。考虑到这一点,我们需要创建一个用于读取数据的模型和另一个用于写入数据的模型,同时只使用这些操作所需的数据。
何时使用 CQRS 模式
建议在应用程序具有复杂业务角色且其数据频繁由资源或事件更新,从而增加写入操作风险时使用 CQRS 模式。当数据合并和写入操作产生的性能问题并发时,也建议实施此模式。使用 CQRS 模式会增加应用程序的复杂性,因此此模式仅在必要时使用。
事件源模式
在典型的方法中,当用户与数据交互时,应用程序会改变数据的状态。然而,这种方法经常会减慢性能和响应速度,也可能生成其他并发更新。除非我们使用额外的机制,否则我们没有审计和记录操作或生成历史记录的机制。
事件源是一种模式,它维护当前的数据状态,并将数据更新事件保存到事件存储库中。此存储库作为一个只追加存储,当消费者访问数据时,它会接收事件并在领域上应用更新。使用此模式通过解耦事件逻辑和可伸缩性以及审计对数据应用的所有操作来提高性能。
理解事件源事件
在事件源模式中,事件被视为将应用于数据的更新。当用户请求数据更新时,应用程序不会实时更新它,而是获取数据,创建一个更新的强制描述,并保存带有该描述的事件。然后,该事件被保存在事件存储库中。请注意,事件消费者可以是另一个应用程序或资源。
使用事件源模式,最终用户能够看到数据的旧版本,因为数据仅在事件存储库上保存并运行时才更新。考虑到这一点,最终用户将不会实时看到更新,因为更新是计划好的,可以在任何时间执行。记住,事件存储库可以是关系数据库、文件系统或其他数据源。
消费者可以是保存事件到事件存储库的同一应用程序的一部分,也可以是另一个应用程序。事件消费者在接收到事件时总是检查它是否可以运行它。
提高性能
使用事件源模式应提高性能。这是因为任何更新都可以由消费者在任何时候完成,更新操作可以锁定数据并影响数据查询。因此,这个过程最小化了更新操作冲突的风险。此外,更新操作可以作为在服务器上运行的背景进程完成。
促进解耦
使用事件源模式也将促进事件逻辑的解耦,因为更新可以由另一个应用程序或资源完成。事件发布者不需要了解事件消费者,事件可以被分割以允许多个应用程序或资源执行更新过程的某一部分。
促进可伸缩性
使用事件源模式将促进可伸缩性,因为它也促进了事件发布者和事件消费者的解耦。因此,一旦考虑了向应用程序或资源的请求量,我们就可以复制事件发布者和事件消费者。
促进审计
使用事件源模式将促进对应用于数据的所有操作的审计,因为事件的历史可以用来重新生成关于数据的老信息。这是因为事件存储库不允许您更新或删除事件。所有数据历史都在事件存储库中显示。
解释索引表模式
这些天,数据量不断增长,应用程序需要能够读取大量数据。为了能够更快地读取数据,通常需要求助于数据结构或索引。在商业环境中,数据可以组织为具有主键的实体集合。然而,我们还想能够使用不包含索引的属性作为查询中的过滤器来读取数据。以下图表说明了数据组织;如果数据保持组织良好,我们可以使用一个检索速度更快的算法。
关系型数据库通过索引工作,并允许我们创建索引以加快数据读取速度。其他数据存储不会使用索引来读取数据,因此我们需要创建自己的索引机制以促进更快的数据读取。
索引表模式是一种创建使用其他数据或特定键组织数据的表的模式。使用此模式,我们可以更快地读取数据。该模式使用三种策略;如下所述:
-
复制数据并在不同的键中组织它
-
创建按不同键组织并使用主键引用原始数据的索引表
-
创建按不同键组织且重复频繁检索字段的索引表
以下图表说明了第一种策略:

在以下图中展示的策略中,数据在每个索引表中重复。这根据反规范化工作,更适合用于主要静态数据。以下图示说明了第二种策略:

在以下图中展示的策略中,数据是根据不同的键组织起来的,并使用主键引用原始数据。在这个策略中,数据的主键首先通过一个属性检索,然后通过使用 ID。以下图示说明了第三种策略:

在前一个图中展示的策略中,数据是根据不同的键组织起来的,这些键在部分归一化的索引表中频繁地重复检索属性。
索引表模式是一个快速检索数据的良好机制。然而,过度使用此模式可能会在数据更新期间产生性能问题,因为它们需要我们重新组织我们的索引表。
物化视图模式
有时,业务角色需要读取需要非平凡查询的数据。用户有时可能需要看到由不同物理位置组合而成的数据。这通常会导致性能问题,使得读取数据变慢。对于关系型数据,这些位置是表。因此,为了提高性能,一种策略可以是创建来自多个物理位置的预填充视图。因此,当应用程序执行查询时,数据已经准备好,因此可以更快地返回。
物化视图模式是一种创建和物化数据以符合应用程序在查询中所需格式的模式。如果查询涉及连接或需要计算的数据,物化视图模式可以提高性能,使得读取数据更快。请注意,物化视图中的数据永远不会在物化视图中更新;它只是实际数据的快照。当实际数据更新时,物化视图需要重建。以下图示说明了物化视图模式:

重建物化视图
当数据源上的数据发生变化时,必须重建物化视图以反映这些变化。重建物化视图可以通过自动或手动过程完成。为了定义重建物化视图所需的策略,我们首先需要定义数据是否允许脏读,以及查看这种读取的影响。如果数据不能有脏读,那么物化视图需要在原始数据更新的同时重建。
何时使用物化视图模式
物化视图模式可以显著提高性能。这是因为任何数据读取都不需要执行连接或计算来检索数据。然而,如果数据变化得太快,物化视图将比必要的次数重建更多次,这会影响性能。
使用物化视图模式的好处与原始数据变化速度成反比。因此,当数据很少修改且不动态时,建议使用物化视图。
解释分片模式
如果我们有一个快速增长的表,在业务环境中读取和写入数据时会产生问题,我们需要应用一个既能解决性能问题又能解决数据量问题的解决方案。
分片模式是一种将数据存储水平分割成分区或分片的模式。这可以提高可扩展性和性能。分片可以在同一个节点或多个节点上运行,但每个分片具有相同的模式。当我们把单个数据库分割成分片时,数据库的行会在它们之间分布。此外,如果数据库支持,分片模式可以通过数据库实现,或者通过应用程序实现。
分片模式在实施过程中有三种常见的策略。这些策略包括以下内容:
-
查找策略:这种策略将数据分成多个分片,并通过分片逻辑实现一个映射,使用分片键将请求路由到数据。之后,每个分片通过分片键来识别,并包含其自己的数据集。
-
范围策略:这种策略将数据分成多个分片,并通过分片逻辑将相关数据分组在一起。这些分片通过分片键来识别。当我们需要使用范围查询来检索数据时,这种策略非常有用。
-
哈希策略:这种策略将数据分成多个分片,并通过分片逻辑实现数据在分片之间的均匀分布。这些分片通过分片键来识别。在这里,分片是平衡的,减少了分片中负载不均的风险。
何时使用分片模式
使用分片模式的好处是显而易见的,但同时也可能增加项目的复杂性。正因为如此,这个模式只有在必要时才应该使用,因为增加项目的复杂性会增加出现错误的机会。
当需要提高数据检索性能时,应该使用分片模式——特别是在数据集非常大的时候。分片减少了用于检索数据的数据量,从而提高了性能。此外,使用分片模式使得数据存储可以扩展,并且数据可以高度可用。
解释管理和监控模式的概念
在云中运行的应用程序数量不断增加,这些应用程序通常在远程数据中心运行。因此,我们无法完全控制应用程序的基础设施,管理远程运行的应用程序可能会变得困难。
管理和监控模式被创建出来,以便我们能够管理和监控我们的应用程序,并在不重新部署应用程序的情况下,暴露关于应用程序的运行时信息,以支持业务变更和定制。使用此模式,我们可以将监控逻辑与应用程序逻辑解耦,并且我们还可以更新使节而不影响应用程序。在本节中,我们将解释以下管理和监控模式:
-
使节模式
-
健康端点监控模式
-
外部配置存储模式
使节模式
在某些情况下,您可能想在云应用程序上实现路由、计费或监控等功能;您可能还希望更新网络配置。然而,有时维护应用程序和更新代码可能很困难。此外,可能有一些库不是由我们维护的,因此无法修改。
使节模式创建了一个外部进程,包括满足管理和监控需求所需的所有逻辑、库和框架。该外部进程充当应用程序或外部服务之间的代理。以下图展示了使节模式:

何时使用使节模式
使节模式的目的是在需要支持云以连接需求时或需要修改应用程序时进行指示。使节模式还可以帮助为多种语言或框架构建一组通用的客户端连接功能。然而,当存在关键请求延迟时,不建议使用此模式。这是因为使节模式在网络中引入了开销,可能会影响应用程序。
解释健康端点监控模式
在商业环境中,应用程序始终需要可用并正确执行。因此,有必要监控所有服务是否如此。然而,在云上监控应用程序通常很困难。
健康端点监控模式通过向应用程序的端点发送请求并验证它们返回的状态来实现健康监控。然后,该模式分析应用程序返回的结果并执行健康验证检查,如下面的图所示:

在前图中,我们可以看到应用程序和代理。这代表了一种向应用程序和服务发送请求并检查其返回结果的解决方案。请注意,代理还检查数据存储。
健康端点监控模式可以执行以下检查:
-
验证响应代码;在 HTTP 协议中,状态码 200 表示成功
-
检查检测到的错误响应的内容
-
测量请求与其响应之间的时间间隔
-
检查位于应用程序外部资源或服务
-
检查 SSL 证书的过期情况
-
验证 DNS 查找返回的 URL 以确保正确条目
何时使用健康端点监控模式
健康端点监控模式建议用于监控 Web 应用程序或服务的可用性和性能。它可以帮助监控中间层和共享服务,以便识别和隔离故障。
使用此模式可以帮助早期识别故障并采取解决问题的必要措施,这对最终用户的影响很小。今天,快速识别故障的能力对商业来说非常重要。
解释外部配置存储模式
应用程序始终需要遵循一组配置才能执行其功能并推广服务。通常,这些配置是通过应用程序读取并从中获取信息的文件来实现的。这些配置文件通常与应用程序包一起部署。这些配置文件中的某些更新需要重新部署应用程序,从而增加了配置文件更新的复杂性。此外,与应用程序一起部署配置文件需要为每个应用程序创建每个配置文件,并且这些文件不能共享。
外部配置存储模式创建一个外部配置模式仓库,为应用程序提供读取配置文件的接口。使用此接口,我们可以更新配置文件而无需重新部署应用程序,我们还可以在多个应用程序之间共享配置,使环境更加有序且易于管理,如下图所示:

如前图所示,应用程序从称为配置仓库的公共位置读取配置。如果配置文件随后被更新,所有应用程序都将看到更新。
何时使用外部配置存储模式
当与应用程序共享配置文件或在无需重新部署应用程序的情况下需要更新时,建议使用外部配置存储模式。共享配置文件使我们能够轻松管理应用程序的配置,因为所有应用程序都将看到并受到在单一位置进行的更新的影响。使用此模式还可以最大限度地减少更新配置文件时的错误风险,因此采用此模式是一种良好的实践。
摘要
在本章中,我们探讨了操作模式、性能和可扩展性模式以及管理和监控模式。我们还讨论了为什么我们应该使用这些操作模式以及如何评估它们的最优使用方式。
在性能和可扩展性模式的话题上,我们研究了如缓存旁路、CQRS、事件溯源、索引表、物化视图和分片等性能和可扩展性模式。然后,我们解释了每个模式的概念、它的好处以及何时实现它。我们现在熟悉了提高企业应用程序性能所需的技术,以及如何使应用程序可扩展。
在管理和监控模式的话题上,我们探讨了如大使模式、健康端点监控模式和外部配置存储模式等管理和监控模式。我们解释了每个模式的概念、它的好处以及何时实现它。我们现在熟悉了管理和监控企业应用程序所需的技术。
在下一章中,我们将探讨 Eclipse MicroProfile 项目及其规范。
第十二章:MicroProfile
本章将概述 Eclipse MicroProfile 项目,包括其目标、预期成果、何时使用以及使用它来开发我们的应用程序的好处。本章仅是一个概述,我们不会探讨如何使用 MicroProfile 项目实现应用程序。
以下将是本章将涵盖的中心主题:
- 解释 Eclipse MicroProfile 项目方法
解释 Eclipse MicroProfile 项目方法
现在,微服务架构的使用正在迅速增加。出现了新的工具,以促进和开发使用 MicroProfile 模式和最佳实践的微服务应用程序。Eclipse MicroProfile 项目的创建是为了使使用 Java EE 的力量来优化企业 Java 以适应微服务架构成为可能。
Eclipse MicroProfile 项目是一系列规范,用于使用微服务架构开发应用程序。这个伞形项目包含一些 Java EE 规范和一些专有规范(由 Eclipse MicroProfile 创建)。因此,Eclipse MicroProfile 项目允许我们使用 Java EE 规范,如 CDI、JAX-RS 和 JSON-B,来开发微服务应用程序。此外,MicroProfile 项目在多个企业 Java 运行时之间提供可移植的微服务架构,并允许互操作微服务架构,这允许多语言运行时(不仅仅是 Java)之间的通信。到目前为止,Eclipse MicroProfile 项目的当前版本是 2.0,具有以下规范:
-
Eclipse MicroProfile Config 1.3
-
Eclipse MicroProfile 故障转移 1.1
-
Eclipse MicroProfile 健康检查 1.0
-
Eclipse MicroProfile JWT 认证 1.1
-
Eclipse MicroProfile Metrics 1.1
-
Eclipse MicroProfile OpenAPI 1.0
-
Eclipse MicroProfile OpenTracing 1.1
-
Eclipse MicroProfile Rest 客户端 1.1
-
CDI 2.0
-
常见注解 1.3
-
JAX-RS 2.1
-
JSON-B 1.0
-
JSON-P 1.1
MicroProfile 项目被设计为可以在多个运行时之间移植,但只有当我们使用前面的规范来开发它时,它才是可移植的。如果我们使用额外的 API 或框架,则可移植性不能保证。此外,MicroProfile 项目支持 Java 8,但不支持 Java 7 及更早版本。
Eclipse MicroProfile Config 1.3
应用程序需要从相同的内部或外部位置读取配置。Eclipse MicroProfile Config 1.3 使得从一些内部或外部来源获取配置属性成为可能,这些属性通过依赖注入或查找提供。通过这种方式,我们可以实现外部配置存储模式并消费配置属性。配置的格式可以是系统属性、系统环境变量、.properties、.xml 或数据源。
Eclipse MicroProfile 故障转移 1.1
使用微服务架构的应用程序需要具备容错能力。容错是关于利用不同的策略来引导逻辑的执行和结果。Eclipse MicroProfile 容错 1.1 提供了将业务逻辑与执行逻辑解耦的能力,分离这些逻辑。有了这个功能,我们可以处理 API,如 TimeOut、RetryPolicy、Fallback、Bulkhead 和 Circuit Breaker,这些都是与微服务一起使用的最流行概念。
Eclipse MicroProfile 健康检查 1.0
在微服务架构中,当涉及到检测故障时,提升服务健康检查的能力非常重要。Eclipse MicroProfile 健康检查 1.0 提供了从另一台机器验证计算节点状态的能力。有了这个功能,我们可以使用 MicroProfile 项目功能实现健康端点监控模式,如第十一章[2f8a0a53-0ca6-4f8f-8248-d62db3021f4c.xhtml]中所述的操作模式。
Eclipse MicroProfile JWT 身份验证 1.1
今天,微服务架构最常使用的是 RESTful 架构风格。然而,RESTful 架构风格与无状态服务协同工作,并且不提供自身的安全性。因此,最常见的做法是创建基于令牌的安全服务。Eclipse MicroProfile JWT 身份验证 1.1 提供了基于角色的访问控制(RBAC),使用OpenID Connect(OIDC)的微服务端点,以及JSON Web Tokens(JWT)。
Eclipse MicroProfile 指标 1.1
指标对于微服务架构非常重要。这是因为它们使我们能够更好地评估我们的服务。Eclipse MicroProfile 指标 1.1 为 MicroProfile 服务器提供了一个统一的出口,将监控数据导出到管理代理。指标还将提供一个通用的 Java API,用于公开它们的遥测数据。这个功能可以用来生成有关应用程序的信息,使得能够实现健康端点监控模式,以便更好地评估我们的服务。
Eclipse MicroProfile OpenAPI 1.0
当我们开发微服务应用程序时,我们创建相互通信的服务。因此,API 文档对于通过不了解合同来最小化错误非常重要。因此,Eclipse MicroProfile OpenAPI 1.0 提供了一个统一的 Java API,用于 OpenAPI v3 规范,所有应用程序开发者都可以使用它来公开他们的 API 文档。因此,我们可以创建服务之间的合同,并且 API 被公开。
Eclipse MicroProfile OpenTracing 1.1
在微服务应用程序中,分析或调试操作工作流是困难的。为了便于调试,我们可以使用 OpenTracing。OpenTracing 是一个用于分布式跟踪的微服务仪器化标准 API,它通过检查和记录请求在分布式系统中传播来帮助调试微服务。
Eclipse MicroProfile OpenTracing 1.1 定义了一个 API 和相关的行为,允许服务轻松参与分布式跟踪环境。有了这个,我们可以分析或调试操作的工作流程,而无需添加任何代码以完成跟踪。
Eclipse MicroProfile REST Client 1.1
在微服务架构中,通信通常是通过 HTTP 协议使用 RESTful 实现的。然后我们需要创建一个客户端,该客户端发送遵守定义合同的 HTTP 请求。在这种情况下,Eclipse MicroProfile REST 客户端提供了一种类型安全的调用 HTTP 上 RESTful 服务的途径。MicroProfile REST 客户端基于 JAX-RS 2.1 API,以确保一致性和易用性。
CDI 2.0
MicroProfile 项目在其内部嵌入了一个 Java EE 规范。其中之一是 CDI,它为 MicroProfile 2.0 中包含的越来越多的 API 提供了基础。自 MicroProfile 2.0 版本起,允许使用 CDI 2.0 以外的实现,但不是必需的。
常见注释 1.3
MicroProfile 项目在其内部嵌入了一些 Java EE 规范。其中之一是常见注释,它为 Java SE 和 Java EE 平台上的各种个别技术提供了跨多种语义概念的注释。自 MicroProfile 2.0 版本起,允许使用 Common annotations 1.3 以外的实现,但不是必需的。
JAX-RS 2.1
Eclipse MicroProfile 项目在其内部嵌入了一些 Java EE 规范。其中之一是 JAX-RS,它为 MicroProfile 2.0 应用程序提供了标准客户端和服务器 API,用于 RESTful 通信。自 MicroProfile 2.0 版本起,允许使用 JAX-RS 2.1 以外的实现,但不是必需的。
JSON-B 1.0
MicroProfile 项目中嵌入的另一个 Java EE 规范是 JSON-B,它作为 MicroProfile 2.0 的一部分包含在内,旨在提供将 JSON 文档绑定到 Java 代码的标准 API。自 MicroProfile 2.0 版本起,允许使用 JSON-B 1.0 以外的实现,但不是必需的。
JSON-P 1.1
MicroProfile 项目中嵌入的另一个 Java EE 规范是 JSON-P,它是 MicroProfile 2.0 的一部分,旨在提供处理 JSON 文档的标准 API。自 MicroProfile 2.0 版本起,允许使用 JSON-P 1.1 以外的实现,但不是必需的。
为什么我们应该使用 MicroProfile 项目?
使用 MicroProfile 项目,我们可以利用 Java EE 的特性创建具有微服务架构的应用程序,并在 Java EE 运行时上运行。此外,使用 MicroProfiles 意味着我们能够创建具有指标、容错、文档、应用程序的独立配置以及易于调试等特性的应用程序,包括将业务代码与这些特性的代码解耦。
Java EE 是一套经过广泛测试的规范。使用这些规范来创建具有微服务架构的应用程序的想法非常有益;使用规范使用户免受供应商的约束,同时也使应用程序能够在不同的运行时之间移植。
社区
Eclipse MicroProfile 项目由 MicroProfile 社区([microprofile.io/](http://microprofile.io/))维护,这是一个半新的社区。这个群体中的主要参与者包括 IBM、Red Hat、Tomitribe、Payara、伦敦 Java 社区(LJC)和 SouJava。这个项目的目标是成为一个基于社区的项目。它还旨在在短时间内迭代和创新,获得社区批准,发布新版本,并重复这个过程。随着使用微服务架构开发的应用程序的增长,MicroProfile 的使用将增加,还将引入其他功能,最终增强 MicroProfile 的能力。
未来工作
Eclipse MicroProfile 项目支持了一些 Java EE 规范,但目标是扩大这种支持并添加其他 Java EE 规范。预期以下规范将是下一个被纳入 MicroProfile 项目的:
-
JCache
-
持久化
-
Bean 验证
-
WebSockets
摘要
在本章中,我们概述了 Eclipse MicroProfile 项目。在这个概述中,我们讨论了 Eclipse MicroProfile 项目是什么,它的规范是什么,为什么我们使用 MicroProfile 项目,以及 Eclipse MicroProfile 项目的未来发展方向。
本章仅作为一个概述,并不旨在教我们如何使用 MicroProfile。如果您想了解更多关于 Eclipse MicroProfile 项目的信息,请访问 https://projects.eclipse.org/projects/technology.microprofile/releases/microprofile-2.0。


浙公网安备 33010602011771号