原文:
zh.annas-archive.org/md5/558165f0864e3ee4f6a79811b214bcf5译者:飞龙
第十五章:Jakarta Faces 应用
Jakarta Faces,现在简称 Faces,是网络应用中两种客户端渲染技术之一。另一种是Jakarta Server Pages (JSP)。在本章中,我们将检查一个 Faces 网络应用,就像我们的 Swing 和 JavaFX 示例一样,允许你执行三个常见的财务计算。
JSP 渲染方法允许在 HTML 页面上放置 Java 源代码。在 JSP 页面可以渲染之前,该文件由应用服务器转换为 servlet。如果你有 50 个 JSP 页面,那么应用服务器上将有 50 个 servlets。在应用设计中的典型方法是用 JSP 进行渲染,通过混合标准 HTML、表达式语言代码来访问数据或调用 Java 方法,以及 Java 源代码。这些文件以 .jsp 扩展名结尾。虽然你可以在页面上进行处理,但常见的方法是让 JSP 页面调用 servlet 进行处理,并决定将哪个 JSP 页面返回给浏览器。
Faces 方法相当不同。首先,框架提供了 Faces servlet。所有对 .jsf 页面的请求都由这个 servlet 处理。虽然 JSP 应用通常是 .jsp 页面和 servlets 的组合,但 Faces 应用不需要任何 servlets,尽管它们也可以使用。在 servlets 的位置,Faces 允许你通过在代码中添加一些注解来使用来自 第十三章 ,《使用 Swing 和 JavaFX 编写桌面图形用户界面编码》,的 Calculations 和 FinanceBean 类。
一个 Faces 页面包含 Facelets 以及任何标准 HTML 标签。Facelets 是 pom 文件。请参阅 进一步阅读 部分以获取这些第三方库的示例。
我们将通过使用 new 来与大多数对象交互。相反,CDI 框架在首次使用时实例化一个对象,并确定是否需要垃圾回收。我们也可以使用 if 语句。
本章现在将带你了解我们的金融计算器是如何使用 Faces、CDI 和 BV 编写的。我们将探讨以下主题:
-
配置 Faces 应用
-
创建由 CDI 管理并通过 BV 验证的对象
-
使用 XHTML、Facelets 和 Jakarta 表达语言渲染页面
-
理解 Faces 页面的生命周期
到本章结束时,你将了解如何构建 Faces 网络应用。有了这些知识,你将能够评估其他网络应用框架,如 Spring 和 Vaadin。
技术要求
对于本章,你需要以下内容:
-
Java 17
-
文本编辑器
-
安装 Maven 3.8.6 或更高版本
-
GlassFish 7.0 应用服务器
-
网络浏览器
本章的示例代码可在 github.com/PacktPublishing/Transitioning-to-Java/tree/chapter15 找到。
配置 Faces 应用
这是金融计算器应用的网页版本看起来是这样的:
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/tstn-java/img/B19088_15_01.jpg
图 15.1 -- 财务计算器网页
配置 Faces 项目开始于任何基本 Web 应用程序相同的设置,例如我们在上一章中看到的。Maven 文件夹设置是相同的。在WEB-INF文件夹中,我们有三个必需的 XML 配置文件和一个可选的。让我们从beans.xml开始:
java
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/beans_3_0.xsd"
bean-discovery-mode="annotated">
</beans>
这看起来很奇怪,因为只有一个标签beans。在注解广泛使用之前,列出每个 bean 或类是必要的,以便启用 CDI。bean-discovery-mode标签定义了任何带有 CDI 注解的 bean 现在都受 CDI 的影响。在 Jakarta EE 10 之前,要使用的发现模式是all。当前的最好实践是使用annotated而不是all。
下一个配置文件是faces-config.xml。在此文件中,您可以定义应用程序属性。其中一些属性可以是导航规则,以确定提交请求应转到哪个页面,需要实例化的 bean 对象,以及用于 i18n 支持的消息包。在此示例中,我们只使用此文件来定义此应用程序的消息包:
java
<faces-config version="4.0"
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/
web-facesconfig_4_0.xsd">
<application>
<resource-bundle>
<base-name>
com.kenfogel.bundles.MessagesBundle
</base-name>
<var>msgs</var>
</resource-bundle>
</application>
</faces-config>
您可以在base-name中看到消息包的包和基本文件名。在var下,您可以看到我们可以在 Faces XHTML 页面中使用的标识符的名称。Faces 中的包与我们为桌面应用程序配置的资源包的方式相同,后面跟着一个键和一个值。要引用消息包中的值,我们使用表达式语言,如#{msgs.result}。
最后必需的配置文件是web.xml文件。它可以承担我们在上一章中看到的相同职责。此外,我们还可以修改 Faces 的执行方式。为了简单起见,我已经删除了开闭标签,因为它们与上一章的版本相同。
第一个参数PROJECT_STAGE配置框架错误处理。使用Development错误消息比小段代码和较慢的性能携带更多信息。通常,代码完成后,你会将其更改为Production:
java
<context-param>
<param-name>jakarta.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
下一个参数确定 Faces 页面中的注释是否在发送到浏览器的 HTML 中:
java
<context-param>
<param-name>
jakarta.faces.FACELETS_SKIP_COMMENTS
</param-name>
<param-value>true</param-value>
</context-param>
如果服务器明确销毁会话对象或会话超时,则会销毁该会话对象并将其发送到垃圾回收:
java
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
当通过网站名称而不是特定页面来引用网站时,则此页面将被显示。在 Faces 中,它应该始终是一个.xhtml文件:
java
<welcome-file-list>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
最后一个可选的配置文件是glassfish-web.xml。我们可以在该文件中提供数据库连接池、安全信息以及其他应用程序服务器负责的配置信息。在我的示例项目中,我已经删除了此文件,因为我们不需要它。
在项目组织就绪并放置配置文件后,我们就可以开始我们的应用程序了。在我们关注网页设计之前,我们需要设置应用程序所需并配置它们在 CDI 的照顾下工作。
创建由上下文依赖注入管理的对象并使用 Bean Validation 进行验证
在这个程序中只使用了两个 Java 类,它们与我们用于第十三章 中,使用 Swing 和 JavaFX 进行桌面图形用户界面编码 所用的几乎相同。它们都受 CDI 控制,数据类也使用了 BV。我们不会展示这些在第十三章中看到的 bean 的完整代码,我们只会看看需要更改的部分。
FinanceBean
第一个注解@Named定义了此类受 CDI 控制。在 CDI 广泛使用之前,JSF 有自己的 CDI-like 实现,使用了@ManagedBean注解。这被认为是过时的,不应再使用。括号中的名称money是我们可以在表达式语言中使用的别名:
java
@Named("money")
范围
当在 Jakarta 应用程序中由 CDI 管理的对象被创建或销毁,并且其他类可能访问它时,它被称为范围。有以下几种类型:
-
@RequestScoped:这意味着服务器将为每个请求创建一个新的对象,并且每个用户的前一个请求对象都会被发送出去进行垃圾回收。 -
@SessionScoped:这意味着在第一次请求创建的对象将保持原位,并且只有当会话计时器结束时或明确销毁时,才会销毁。 -
@ApplicationScoped:这些对象对所有用户的每个会话都可用。 -
@ViewScoped:这是最终的范围。使用此范围创建的 bean 与一个 Faces 页面相关联。只要你不更改视图,例如通过有一个链接或按钮调用另一个页面,那么 bean 仍然有效。
现在回到代码:
java
@RequestScoped
public class FinanceBean implements Serializable {
private static final Logger LOG =
Logger.getLogger(FinanceBean.class.getName());
每个 BigDecimal 变量都使用 BV 注解声明。在这个例子中,我们设置了一个最小值和最大值。message属性是名为ValidationMessages的单独消息包的键。就像普通包一样,你需要一个默认的,然后为每种支持的语言提供一个。这些验证包预计将在resources文件夹中找到,而不是在任何子文件夹中:
java
@DecimalMin(value = "1.00",
message = "{com.kenfogel.minInput}")
@DecimalMax(value = "100000.00",
message = "{com.kenfogel.maxInput}")
private BigDecimal inputValue;
@DecimalMin(value = "0.00",
message = "{com.kenfogel.minInput}")
@DecimalMax(value = "1.00",
message = "{com.kenfogel.maxInput}")
private BigDecimal rate;
@DecimalMin(value = "1.00",
message = "{com.kenfogel.minInput}")
@DecimalMax(value = "300.00",
message = "{com.kenfogel.maxInput}")
private BigDecimal term;
private BigDecimal result;
在原始的FinanceBean类中找不到的两个新字段。第一个是calculationType字段,它定义了使用哪三个计算公式中的哪一个。它还用于更新第一个输入字段的Label名称:
java
private String calculationType;
当计算类型更改时,必须在第一个输入标签中显示新的文本。这将从资源包中读取:
java
private final ResourceBundle msgs;
public FinanceBean() {
result = BigDecimal.ZERO;
inputValue = BigDecimal.ZERO;
rate = BigDecimal.ZERO;
term = BigDecimal.ZERO;
在构造函数中,我们将计算定义为loan并初始化msgs:
java
calculationType = "loan";
msgs = ResourceBundle.getBundle(
"com.kenfogel.bundles.MessagesBundle");
}
此数据类中剩余的方法只是常规的 getter 和 setter。
关于 CDI 和 BV 的最后一点是,它们可以用于任何包含 CDI 和/或 BV 库的 Java 应用程序。该库是 Jakarta 的一部分,因此在pom文件中没有特定的引用。要在您的应用程序中使用 CDI、BV 或两者,请将以下内容添加到您的pom文件中:
java
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
在 Faces 中,有一个称为后端 bean 的数据 bean,现在我们可以查看Calculations类。
计算
Calculations类也基本没有变化。所有三个计算方法中的公式都是相同的。第一个变化是FinanceBean对象现在是一个由 CDI 实例化的类字段,而不是传递给每个方法的参数。第二个变化是计算调用到一个方法,该方法反过来选择适当的计算方法。现在让我们看看这个。
类以定义此为 CDI 管理 bean 的注解开始。作用域为@RequestScope。CDI bean 是在将其注入到另一个类时实例化的,正如我们接下来将要看到的,或者是在 Faces 页面上首次使用时:
java
@Named("calculate")
@RequestScoped
public class Calculations implements Serializable {
private static final Logger LOG =
Logger.getLogger(Calculations.class.getName());
使用@Inject注解,CDI 将检查此对象当前是否存在。如果存在,则将其引用分配给名为money的变量。如果不存在,则将在将引用传递给money之前实例化它:
java
@Inject
FinanceBean money;
此方法将从 Faces 页面调用:
java
public String performCalculation() {
switch (money.getCalculationType()) {
case "loan" ->
loanCalculation();
case "savings" ->
futureValueCalculation();
case "goal" ->
savingsGoalCalculation();
}
return null;
}
这是使用类字段获取用户输入并将结果存储的地方的计算过程之一:
java
public void loanCalculation()
throws ArithmeticException {
// Divide APR by 12
var monthlyRate = money.getRate().divide(
new BigDecimal("12"), MathContext.DECIMAL64);
我们只是对 CDI 和 BV 的表面进行了探讨。参见进一步阅读以获取有关这些功能的更多信息。现在让我们继续到 Faces 的网页渲染。
使用 XHTML、Facelets 和表达式语言来渲染页面
Faces 应用程序使用扩展名为xhtml的文件。此扩展名意味着 HTML 或自定义标签(称为 Facelets)必须遵循 XML 的规则。这意味着每个标签都必须关闭。HTML 允许使用<br>和<p>等标签,而在 XHTML 中使用这些标签时,必须有一个开标签后跟一个闭标签。标签也可以通过以正斜杠结尾来自动关闭,例如<br/>或<p/>。
让我们看看负责用户界面的index.xhtml文件。
我们首先声明此文件是 XHTML 格式:
java
<!DOCTYPE xhtml>
XML 文档将被检查以确保所有标签都是有效的。这里列出的五个命名空间代表了 Faces 中可用的常见标签集合:
java
<html xmlns:faces="jakarta.faces"
xmlns:ui="jakarta.faces.facelets"
xmlns:f="jakarta.faces.core"
xmlns:h="jakarta.faces.html"
xmlns:pt="jakarta.faces.passthrough" >
在这里,我们看到我们的第一个 Facelet,即h:head标签。当此文件被 Faces 框架处理时,每个 Facelet 都是一个调用 Java 方法的过程,该方法返回一个有效的 HTML 字符串,如果需要,还可以返回 JavaScript:
java
<h:head>
这里我们看到我们的第一个表达式语言语句。在这种情况下,我们正在从 faces-config.xml 文件中定义的消息包中检索文本。请注意,我们还在使用一个 HTML 标签和一个标题,并且这些标签在处理 Faces 页面生成的 HTML 中被保留。在任何 Facelet 与 HTML 标签匹配的情况下,您都应该始终使用 Facelet:
java
<title>#{msgs.title}</title>
<h:outputStylesheet library="css" name="main.css"/>
</h:head>
<h:body>
<h:form>
<h1>#{msgs.heading}</h1>
在这里我们可以看到单选按钮输入的 Facelets。当我们调用表达式语言中的方法而不以括号结束调用时,我们表示我们想要 getCalculationType() 或 setCalculationType()。如果不是 setter 或 getter,我们必须使用方法的全名后跟括号:
java
<h:selectOneRadio
value="#{money.calculationType}"
immediate="true" styleClass="radiocenter" >
<f:selectItem itemValue="loan"
itemLabel="#{msgs.loan_radio}"
styleclass="radiocenter"/>
<f:selectItem itemValue="savings"
itemLabel="#{msgs.savings_radio}"
styleclass="radiocenter"/>
<f:selectItem itemValue="goal"
itemLabel="#{msgs.goal_radio}"
styleclass="radiocenter" />
单选按钮的常见用途是在表单提交时提供所需的选择。在这个应用程序的设计中,我希望字段被清除,并且表单重新渲染。这种重新渲染还将更改第一个标签的输入标签文本。valueChange 事件表示将发生一个 Ajax 部分提交,这将调用 money.clear() 方法将所有值重置为零。render="@form" 属性将导致页面重新渲染:
java
<f:ajax event="valueChange" render="@form"
action="#{money.clear()}"/>
</h:selectOneRadio>
我们在这里使用 panelGrid,它创建一个 HTML 表格。您指定列数,而行数由 HTML 标签或 Facelets 的数量决定。每两行中的第一个值是一个非换行符。这将消耗一个表格单元格,但不会显示任何内容:
java
<h:panelGrid columns="2" >
<h:outputLabel value=" "/>
第二个值是 h:message。这个 Facelet 默认为空白条目。如果发生错误,如无效输入或值超出范围,则会在输入字段上方显示一条消息。您可以使用 style 属性在此属性中编写 CSS,或者使用 styleclass 来引用 CSS 文件中的类:
java
<h:message id = "inputError"
for="inputValue"
style="white-space:nowrap; color:red;
font-size: 100%;"/>
如果用户输入无效或未转换的输入,就会出现以下情况。这些消息以及 Faces 页面上的其他一切都可以使用 CSS 进行样式化:
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/tstn-java/img/B19088_15_02.jpg
图 15.2 -- h:message 输出
这个输入标签的文本是从 FinanceBean 而不是直接从消息包中检索的。这就是标签可以根据单选按钮选择而改变的原因。
每个 h:inputText 字段包含一个 f:ajax Facelet。这将触发部分提交,允许将您输入的字符串转换为 BigDecimal 并检查它是否在范围内。否则,这些检查只有在按下 提交 按钮时才会发生。没有什么比填写完表格后,在按下 提交 按钮后才发现几个输入错误更令人烦恼的了。
Faces 框架负责将 String 转换为 BigDecimal。如果由于存在无效字符而失败,则匹配的 h:message 字段将显示来自消息包文件的错误消息。converterMessage 属性包含包的键值:
java
<h:outputLabel id = "inputLabel"
value="#{money.getInputLabel()}"
for="inputValue" />
<h:inputText value="#{money.inputValue}"
id="inputValue"
converterMessage="#{msgs.invalidInput}" >
<f:ajax event="blur"
render="inputError" />
</h:inputText>
<h:outputLabel value=" "/>
<h:message id="interestError"
for="interestValue"
style="white-space:nowrap;
color:red; font-size: 100%; " />
<h:outputLabel value="#{msgs.interest}"
for="interestValue"/>
<h:inputText value="#{money.rate}"
id="interestValue"
converterMessage="#{msgs.invalidInput}" >
<f:ajax event="blur"
render="interestError" />
</h:inputText>
在文本中,我移除了这个表单上的两行,因为它们几乎与上一个相同。
在我们表单的底部有两个按钮。一个调用Calculations类来生成答案,而另一个重置所有字段并使单选按钮中的Load成为选择:
java
<h:commandButton type="submit"
action="#{calculate.performCalculation()}"
value="#{msgs.submit}" styleClass="btn" />
<h:commandButton type="reset"
value="#{msgs.clear}" styleClass="btn2" >
<f:ajax event="click" execute="@this"
render="@form" />
</h:commandButton>
</h:panelGrid>
</h:form>
这是一个基本的应用程序,但它应该能给您一个关于 Faces 应用程序如何工作的感觉。
部署 Faces Web 应用程序
就像本书中用 Maven 构建的每个示例程序一样,您只需要在project文件夹中打开一个终端/控制台窗口。在提示符下,您只需输入mvn。假设没有错误,您应该在目标文件夹中找到您的项目。
您可以将此文件复制并粘贴到上一章中讨论的autodeploy文件夹中。另一种选择是从 GlassFish 控制台部署应用程序:
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/tstn-java/img/B19088_15_03.jpg
图 15.3 -- 从服务器部署
选择部署应用程序将带您到一个表单,您可以在其中上传应用程序到服务器。随着我们的应用程序运行起来,让我们更深入地了解当我们与 Faces 页面交互时会发生什么。
理解 Faces 页面生命周期
在第十四章中,我们看到了使用 servlet 的 Web 应用程序的基本生命周期。简单来说,一个提交请求被发送到 servlet,servlet 以请求对象的形式从页面接收数据,然后您编写必要的任务,然后从 servlet 或 HTML 或 JSP 页面返回响应。Faces 与这不同。
一个从.jsf页面请求开始的 Faces 页面生命周期有六个部分。以下是一个显示 Faces 生命周期步骤的图表:
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/tstn-java/img/B19088_15_04.jpg
图 15.4 -- Faces 生命周期
让我们回顾每一部分:
-
恢复视图:当一个请求到达时,会检查查询字符串。如果不存在,那么这很可能是第一次请求这个页面。这意味着页面可以在不经过其他任何阶段的情况下渲染。
-
String变量。这允许在将数据分配给与请求关联的 bean 之前对其进行验证和转换。与页面关联的 bean 被称为后端 bean。 -
String到BigDecimal或double。您也可以编写自己的自定义转换器。如果在转换过程中出现任何问题,则忽略剩余的阶段,并调用h:message来显示错误消息。
转换之后是验证。有标准的验证器 facelets,以及允许您编写自定义验证器。如果使用 BV,它也会在这里被调用。如果验证失败,那么,就像失败的转换一样,生命周期会跳转到渲染响应。
-
更新模型值:在这个阶段,在成功转换和验证之后,值被分配给后端 bean。
-
调用应用程序:许多标签都有一个 action 属性,允许你调用后端 bean 中的方法。随着数据现在已在后端 bean 中,这些方法可以被调用。
-
渲染响应:在这里,Faces 页面被渲染为 HTML 和 JavaScript。
有可能编写一个阶段监听器类,你可以在大多数阶段添加额外的任务。理解生命周期对于开发 Faces 页面至关重要。
摘要
本章简要介绍了 Jakarta Faces,支持框架如 CDI 和 BV,以及如何部署应用程序。查看生命周期应该能让你了解 Faces servlet 在做什么。虽然网页渲染通常是 JavaScript 框架的领域,但应该将 Faces 视为 JavaScript 方法的替代方案。它与 CDI 和 BV 的集成使其成为 Web 应用的坚实基础。BV 可以确保所有验证都在服务器上完成。这并不妨碍在 JS 中使用验证。然而,使用像curl这样的简单工具,如果验证仅在 JS 客户端发生,你就可以轻松提交无效数据。
在我们接下来的最后一章中,我们将探讨如何打包 Java 应用程序以便轻松部署到服务器或作为桌面应用程序分发。
进一步阅读
-
PrimeFaces -- Facelet 库:
www.primefaces.org/ -
OmniFaces -- Faces 实用方法:
omnifaces.org/ -
Spring:
spring.io/ -
Vaadin:
vaadin.com/
第四部分:打包 Java 代码
这一部分是关于你如何分发你的代码。你将学习如何创建一个自定义 Java 运行时,并将其与一个应用程序打包在一个单一文件安装程序中。包括 Java、应用程序服务器和你的应用程序在内的整个环境的分发,使用 Docker 作为云部署的第一步,是本部分的最终主题。
这一部分包含以下章节:
- 第十六章 ,在独立包和容器中部署 Java
第十六章:在独立包和容器中部署 Java
在最后一章中,我们将探讨不同的打包和分发 Java 应用程序的方法。我们已经看到了桌面应用程序的 JAR 文件和网络应用程序的 WAR 文件,以及如何部署它们。虽然这种方法对于部署来说是足够的,但在某些情况下,这种传统方法可以改进。
Java 很大。Java SE 分发中包含许多库,尽管你的应用程序可能只需要其中的一些。第三方或外部库也是如此。现代使用 Java 模块方法的打包允许你生成只包含你将使用的库部分的 JAR 或 WAR 文件。
在网络应用程序的情况下,这种打包方式可以将 WAR 文件的大小减少到仅包含所需外部库中的所需模块,而不是整个库。在桌面应用程序的情况下,要求计算机上已经安装了 Java 语言。Java 运行时现在已经模块化。这允许你创建不需要安装 Java 版本即可运行的可执行应用程序,而是将其作为打包安装程序的一部分。
最后,我们将探讨 Docker 容器系统。想象一个由不同 操作系统 (OSes )的开发商组成的团队,他们正在开发一个应用程序。虽然 Java 是 一次编写,到处运行,但有时让每个开发商在相同的环境中工作是有利的。Docker 容器有助于满足这一需求。此外,你可以将这些容器部署到云端。虽然我们不会探讨云部署,但了解容器的工作原理将使你为在云中工作做好准备。
本章我们将探讨以下内容:
-
探索模块化 Java 是什么
-
使用
jlink创建自定义 JRE -
使用
jpackage进行打包安装 -
使用 Docker 容器系统
-
与 Docker 镜像一起工作
-
创建 Docker 镜像
-
发布镜像
我们将使用来自 第十三章 ,使用 Swing 和 JavaFX 进行桌面图形用户界面编码 ,最初名为 BankSwing,在本章中重命名为 BankSwingJUL 的修改版本来探索模块和包。为了查看 Docker,我们将使用上一章未更改的 JSF_FinancialCalculator。
技术要求
你需要以下内容来完成本章:
-
Java 17。
-
文本编辑器。
-
Maven 3.8.6 或更高版本。
-
对于
jpackage:-
rpm-build软件包 -
fakeroot软件包 -
macOS: Xcode 命令行工具
-
-
Docker Desktop (https://www.docker.com/)。要使用 Docker,你需要创建一个账户。免费的个人账户就足够了。一旦你有了账户,你就可以下载 Docker Desktop。每个操作系统都有一个版本。
本章的示例代码可在github.com/PacktPublishing/Transitioning-to-Java/tree/chapter16找到。
探索模块化 Java 是什么
到目前为止,我们看到了由类字段和方法组成的类中的代码。然后,我们将这些类分组到包中,最后作为一个 JAR 或 WAR 文件。模块化 Java 引入了一个新的分组,称为模块 。模块是一个 JAR 文件,但包含模块描述符。还有一个自动模块,其清单文件中有模块名称。这个 Java 特性被称为Java 平台模块系统 (JPMS)。
到目前为止,我们使用 Maven 构建我们的应用程序。Maven 是一个构建工具,它会下载我们需要的任何库,并确保我们的代码可以成功编译。它不做的是确定所有必需的外部库,例如 Java 本身,是否都存在。它的主要工作在代码成功编译后结束。另一方面,JPMS 专注于成功运行程序所需的库。与 Maven 不同,JPMS 检查作为模块编写的库是否存在或将在代码运行时存在。这引出了一个问题,什么是模块?
模块是一个 JAR 文件。普通 JAR 文件和模块 JAR 文件之间有一些细微的差别。至少,模块文件必须在src/main/java文件夹中包含一个名为module-info.java的文件。这个文件的一个目的是列出所需的模块。可能没有所需的模块,但这个文件的存在表示该项目可以是一个模块。并非所有用 Java 编写的库都已被重新编码为模块,但许多新的库都是这样编写的。模块文件可以用作普通 JAR 文件,也可以在使用 JPMS 工具时用作模块。你不需要两个版本的库。
曾经,用户有 Java 的两个版本可用。有一个包含 JVM 和所有必需的开发工具,如 Java 编译器的 JDK。第二个版本是Java 运行时版 (JRE)。正如其名称所暗示的,JRE 包含运行几乎任何 Java 程序所需的所有库。JRE 的大小显著减小,大约为 90 MB,而完整的 JDK 大约为 300 MB。随着 JPMS 的引入,JRE 不再作为下载提供。时代在变化,现在一些 Java 发行版又包含了 JRE。
由于 JRE 比 JDK 小得多,模块能为我们做什么呢?原因与为什么 JRE 从 Java 发行版中删除有关。使用 JPMS,你可以构建自己的自定义 JRE,只包含你需要的模块。那么,Java 语言中的模块是什么呢?在终端/控制台窗口中,输入以下内容:
java
java --list-modules
现在,你将获得每个模块的列表。在我的 Windows 11 系统上,有 71 个模块被列出------22 个以java开头,49 个以jdk开头。为了构建自定义 JRE,你需要知道你的程序使用了哪些模块。从本章的 GitHub 获取BankSwingJUL项目。与第十三章 版本的不同之处在于JUL替换了log4j2。我这样做是为了将所需的模块数量减少到 Java 发行版中的那些。构建项目,你应该在target文件夹中找到一个名为BankSwingJUL-0.1-SNAPSHOT.jar的 JAR 文件。在target文件夹中打开一个终端/控制台窗口,并输入以下命令:
java
jdeps BankSwingJUL-0.1-SNAPSHOT.jar
输出将开始于你需要用到的 Java 模块的摘要:
java
BankSwingJUL-0.1-SNAPSHOT.jar -> java.base
BankSwingJUL-0.1-SNAPSHOT.jar -> java.desktop
BankSwingJUL-0.1-SNAPSHOT.jar -> java.logging
输出的剩余部分将查看项目中的每个类,显示你正在使用的 Java 类以及它们所属的模块。java.base模块是核心类集的家园。java.desktop模块是 Swing 的家园,而java.logging模块是 JUL 的家园。现在,是时候创建我们的自定义 JRE 了。
使用 jlink 创建自定义 JRE
我们将使用 Java JDK 的一部分jlink工具来创建我们的自定义 JRE。我们将首先创建一个包含所有必需模块的 JRE:
java
jlink --add-modules ALL-MODULE-PATH --output jdk-17.0.2-jre
--strip-debug --no-man-pages --no-header-files
--compress=2
这是一行。在 Linux 中,你可以使用反斜杠(\)输入多行命令,而在 Windows 中,你使用撇号(^)。此命令的输出将是一个名为jdk-17.0.2-jre的文件夹,其中包含仅 76 MB 的 JRE。这比原始 JRE 小,但我们不需要所有 Java 模块;我们只需要三个。以下是我们的新命令:
java
jlink --add-modules java.base,java.desktop,java.logging
--output jdk-17.0.2-minimaljre --strip-debug
--no-man-pages --no-header-files --compress=2
现在,我们将在jdk-17.0.2-minimaljre文件夹中有一个新的 JRE,大小仅为 41 MB。现在,我们需要使用我们的自定义 JRE 与我们的应用程序一起使用。为了测试我们的 JRE 是否工作,你可以在自定义 JRE 的bin文件夹中打开一个终端/控制台窗口,然后执行以下命令来运行你的代码。请注意,路径是为 Windows 准备的,因此它们必须根据 Linux 或 Mac 进行调整:
java
java.exe -jar C:\dev\Packt\16\BankSwingJUL\target\
BankSwingJUL-0.1-SNAPSHOT.jar
这是一个单行命令。如果一切顺利,你的BankSwingJUL应用将运行。现在,是时候将应用程序打包成一个包含我们的应用程序和 JRE 的单个可执行文件了。这将使我们能够在不需要接收者首先安装 Java 的情况下分发我们的应用程序。
使用 jpackage 进行打包
自定义 JRE 创建完成后,我们现在可以创建自定义的可安装包了。你可以为 Windows、Linux 或 Mac 创建这些包。你必须使用你的包的目标操作系统。此外,每个操作系统都有额外的步骤。
Windows 需要您安装 WiX 工具集。您可以在wixtoolset.org/找到它。下载最新版本并安装。当您运行jpackage时,它将生成一个 EXE 文件。您可以分发此文件,当运行时,它将在C:\Program Files目录中安装运行程序所需的所有内容。可执行 EXE 文件将位于文件夹中,这就是您运行程序的方式。
Linux 用户,根据他们使用的版本,可能需要rpm-build或fakeroot包。当您运行jpackage时,它将为 Debian Linux 生成一个 DEB 文件,或其他发行版生成一个 RPM 文件。您可以分发此文件,当运行时,它将在/opt/application-name目录中安装运行程序所需的所有内容。可执行文件将位于文件夹中,这就是您运行程序的方式。
Mac 用户需要 Xcode 命令行工具。当您运行jpackage时,它将生成一个 DMG 文件。您可以分发此文件,当运行时,它将在/Applications/application-name目录中安装运行程序所需的所有内容。可执行文件将位于文件夹中,这就是您运行程序的方式。
在所有三种情况下,没有必要安装 Java。即使安装了,您也将使用自定义的 JRE。
要使用jpackage创建安装程序包,您只需在命令行中输入以下内容:
java
jpackage --name BankSwingJUL
--input C:\dev\Packt\16\BankSwingJUL\target
--main-jar BankSwingJUL-0.1-SNAPSHOT.jar
--runtime-image C:\dev\Packt\16\jre\jdk-17.0.2-minimaljre
--dest C:\temp
这是一个单行命令。以下是参数的概述:
-
--name:添加-1.0的可执行文件名称。使用--app-version后跟版本标识来覆盖此名称。 -
--input:您要打包的 JAR 文件的存放位置。 -
--main-jar:包含具有main方法的类的 JAR 文件的名称。如果您在 JAR 文件中没有包含具有main方法类的MANIFEST.MF文件,您可以使用--main-class后跟包含main方法的类的名称。 -
--runtime-image:这是您使用jlink创建的 JRE 文件夹的路径和名称。 -
--dest:默认情况下,打包的应用程序将位于您发出jpackage命令的任何文件夹中。您可以使用此参数选择您想要的文件夹。
在此命令成功完成后,您将拥有一个可执行的程序包,该程序包将安装您的程序,并包含一个可执行文件来运行它。
网络应用程序依赖于应用程序服务器而不是 JRE 来运行。因此,我们不能使用jpackage。这就是我们的下一个打包选择,Docker 容器。
使用 Docker 容器系统
Docker 是一个平台即服务系统,允许你构建一个运行中的应用程序的镜像,该镜像可以在虚拟化的 Linux 容器中运行。这些镜像可以在支持 Docker 容器的任何计算机上运行。这包括 Windows 和 Linux 发行版。这个镜像可以包含运行程序所需的一切。在本节中,我们将创建一个包含 Java 应用服务器、Java JDK 和我们的JSF_FinancialCalculator Web 应用程序的镜像,并在容器中部署它。这之所以重要,是因为大多数云提供商,如 AWS,都支持在 Docker 容器中部署云应用程序。我们不会讨论云部署,因为不同的云提供商工作方式不同。他们共同点是使用 Docker。
第一步是安装 Docker 系统。最简单的方法是从www.docker.com/下载并安装 Docker Desktop 系统。它为 Windows、Mac 和 Linux 都提供了版本,它们包含 GUI 界面以及命令行工具。在支持 WSL2 的 Windows 10 或 11 系统上,命令行工具在 Windows 终端和 WSL2 Linux 终端中都可用。这意味着,除了文件路径的描述方式不同外,所有命令在所有操作系统上都是相同的。现在,花点时间安装 Docker。
与 Docker 镜像一起工作
虽然我们可以从头开始构建镜像,但还有另一种方法。许多为云而创建软件的组织会提供预构建的镜像。我们可以将这些镜像添加到我们的应用程序中。在我们的例子中,我们想要一个包含 Java 17 和应用服务器的预构建镜像。我们将使用来自 Payara 的镜像。这家公司提供基于 GlassFish 的服务器,在开源社区版本和商业付费版本中都进行了增强。
Docker Hub 上的镜像可能是出于恶意目的而创建的。虽然 Docker 提供了一种扫描漏洞的服务,但你仍应扫描镜像中的任何可执行文件,以检查潜在的恶意行为。你注册的 Docker 计划决定了你可以从或推送到 Hub 的镜像数量。使用免费的个人订阅,你可能可以推送无限数量的公共仓库,但每天的限制为 400 次镜像拉取。商业订阅增加了从仓库拉取的次数,并可以对您的镜像执行漏洞扫描。
启动 Docker Desktop。它包含一个包含基本 Web 服务器的镜像和容器,该服务器具有 Docker 的文档页面。我们将大部分设置工作在命令行上完成,而桌面 GUI 对于查看 Docker 镜像和容器的状态非常有用。
第一步是下载我们将要修改的镜像,通过添加JSF_FinancialCalculator应用程序。我们将使用这个程序,与上一章保持不变。以下是命令:
java
docker pull payara/server-full:6.2023.2-jdk17
如果你访问 hub.docker.com/r/payara/server-full/tags,你可以看到所有可用的 Payara 服务器版本。正如你从之前的命令中看到的,我们正在拉取包含服务器和 Java 17 的 server-full:6.2023.2-jdk17 镜像。在 Docker 中,成功的命令返回一串长数字。
现在,我们需要在一个容器中运行这个镜像。虽然你可以运行多个容器,但使用 TCP 端口的网络应用程序可能会导致冲突。因此,我建议停止任何正在运行的容器。使用 Docker Desktop,从菜单中选择容器列表,查找任何列为 运行中 的容器,然后通过点击 操作 列中的方块按钮来停止它们。你还可以通过在命令行中输入以下内容来停止容器:
java
docker stop my_container
在这里,my_container 被替换为正在运行的容器或镜像的名称。
我们现在希望将这个镜像包装在一个容器中并运行这个镜像:
java
docker run --name finance -p 8080:8080 -p 4848:4848
payara/server-full:6.2023.2-jdk17
这是一个单行命令。--name 开关允许你为容器分配一个名称。如果你省略了这个开关,Docker 将分配一个随机名称。-p 开关将容器中的端口映射到计算机的端口。在这个例子中,我们将它们映射到相同的端口。镜像的名称与我们所拉取的镜像名称相同。假设没有错误,你现在可以测试容器了。打开你的浏览器,首先通过输入 http://localhost:8080 访问 Payara 主页。接下来,访问管理控制台页面 https://localhost:4848。你的浏览器可能会发出警告,因为 TLS 证书是自签名的。忽略警告,你应该会到达登录页面。用户名和密码都是 admin。
在上一章的 JSF_FinancialCalculator 示例中,你可以在项目的 target 文件夹中找到它。
你现在可以通过在浏览器中输入 http://localhost:8080/JSF_FinancialCalculator 来验证应用程序是否已正确部署。项目名称必须与 WAR 文件名称匹配。如果一切正常,计算器在你的浏览器中打开,你现在可以基于 payara/server-full:6.2023.2-jdk17 图像创建自己的容器,该容器将包含服务器上安装的计算器应用程序。
创建 Docker 图像
现在,我们准备创建自己的镜像。首先,我们需要停止我们刚刚使用的容器:
java
docker stop finance
在终端/控制台中,输入以下命令来创建你的新容器,该容器将包含 Payara 图像:
java
docker create --name transition -p 8080:8080
-p 4848:4848 payara/server-full:6.2023.2-jdk17
名称 transition 是任意的,可以是任何你想要的内容。你现在有一个基于 Payara 图像的新容器。我们希望修改这个容器以包含计算器应用程序。第一步是运行这个新容器:
java
docker start transition
在这里最常发生的错误是如果另一个容器正在监听相同的端口。确保任何运行 Payara 的容器或镜像都没有运行。Docker Desktop 应用程序可以显示哪些容器或镜像正在运行。
就像我们在测试 Payara 镜像时做的那样,使用您的浏览器打开 Payara 的管理控制台。现在,将JSF_FinancialCalculator WAR 文件部署到服务器上。通过访问应用程序的网页来验证它是否成功运行。
现在,通过输入以下内容,将容器中的镜像更改,将 Web 应用程序的添加永久化:
java
docker commit transition
最后一步。输入以下内容:
java
docker images
您将看到REPOSITORY和TAG都显示为<none>的条目:
java
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> c0236a80bba3 52 minutes ago 618MB
为了解决这个问题,并在创建镜像的最后一步,通过输入以下内容来分配一个标签名称和镜像 ID:
java
docker tag c0236a80bba3 transition-image
注意,必须使用的十六进制数字可以在之前的docker images命令的表中找到。在运行docker tag之后运行docker images,表将显示以下内容:
java
REPOSITORY TAG IMAGE ID CREATED SIZE
transition-image latest c0236a80bba 32 hours ago 618MB
您现在已经在本地仓库中配置了一个镜像。为了任何人都能使用您的镜像,您必须在 Docker Hub 网站上发布它。
发布一个镜像
如前所述,出于安全原因,您用作新镜像基础的任何镜像都必须进行漏洞扫描,特别是镜像中的任何可执行代码。免费的个人版允许您拥有无限数量的公共镜像。付费层支持私有镜像。发布的第一步是在 Hub 上创建一个仓库。为此,打开您的浏览器并转到hub.docker.com/。如果需要,请登录您的账户。
接下来,从网页顶部的选项中选择仓库 。您现在将看到您可能已经创建的任何仓库。点击创建仓库 。在此页面上,您必须填写表格,输入容器的名称以及可选的描述。它还显示了您的 Docker 用户名。确保公共是仓库类型的选项。
现在,您可以将您的镜像推送到 Hub。这里有三个步骤:
-
登录到 Docker Hub:
javadocker login --username my_username
将my_username替换为您的 Docker 用户名。您现在将被要求输入您的密码。您将收到成功登录的确认。
-
接下来,您需要更改您的镜像标签
transition-image,以匹配您创建的仓库名称omniprof/transitioning_to_java。该名称由您的用户名和仓库名称组成:javadocker tag transition-image omniprof/ transitioning_to_java -
现在是最后一步,将您的镜像推送到 Hub:
javadocker push omniprof/transitioning_to_java
为了确定您是否成功,请访问 Docker Hub 并选择omniprof/transitioning_to_java。
您现在有一个可以与您的团队或客户共享的 Docker 镜像。
摘要
在本章中,我们探讨了模块化 Java 的含义。我们利用了 Java 自身已经模块化的这一事实。这使得你可以使用jlink构建一个比 JDK 小得多的 JRE。你甚至可以通过只包含你的代码所依赖的模块来进一步减小其大小。
我们然后探讨了两种分发代码的方法。第一种方法是使用jpackage为你的应用程序创建一个安装程序。安装程序可以包含你定制的 JRE,并将安装你的程序,以及一个可执行文件来运行应用程序。这通常是分发桌面应用程序的最佳方式。
第二种分发方法使用 Docker 容器系统。Docker 允许我们构建和发布一个包含不仅我们的代码和 JDK,还包括任何其他所需程序的镜像。在我们的例子中,额外的程序是一个安装了财务应用程序的应用服务器。我们构建的镜像被发布到仓库中,例如 Docker Hub。现在,任何在任意操作系统上运行 Docker 的人都可以拉取我们的镜像,并在 Docker 容器中运行它。
这也带我们来到了本书的结尾。我的目标是提供一个参考,帮助那些需要学习和理解 Java 的资深开发者。还有很多东西要学习,但我希望这本书能让你走上正确的道路。
进一步阅读
-
使用 Java 模块的多模块 Maven 应用程序 模块:
www.baeldung.com/maven-multi-module-project-java-jpms -
Java 平台,标准版 -- 打包工具用户指南 :
docs.oracle.com/en/java/javase/14/jpackage/packaging-tool-user-guide.pdf -
Docker 文档:
docs.docker.com/