GlassFish 如何让 Java EE 应用程序部署更加灵活

作者:Julien Ponge

GlassFish 中有四个值得注意的特性,它们可增加 Java EE 应用程序部署的灵活性。

2012 年 2 月发布

下载:

下载GlassFish

下载NetBeans IDE

下载示例代码 (Zip)

注:GlassFish 存在两个版本:GlassFish Server 开源版和 Oracle GlassFish Server。本文与两者都有关。

简介

部署和管理 Java Platform Enterprise Edition (Java EE) 应用程序似乎是相当成熟的活动。应用程序可以通过部署和取消部署组合来部署、取消部署和升级。应用程序使用各种类型的资源,如 JDBC 连接池或 Java 消息服务 (JMS) 目标。这类资源需要通过应用服务器手段(如配置文件、命令行工具和图形界面)来创建、配置和管理。尽管在各个 Java EE 应用服务器之间,这些任务变化不大,但每个应用服务器都可以自由提供更广泛的特性集,从而让开发人员和基础架构团队的工作更轻松。

本文介绍 GlassFish 中可提高 Java EE 应用程序部署灵活性的四个值得注意的特性:

  • 跨重新部署保留会话数据

  • Servlet 片段

  • 应用程序范围的资源

  • 应用程序版本控制

运行示例

本文将使用一个名为 TaskEE 的运行示例,这是一个非常简单的任务列表应用程序(参见图 1),非常适合作为要部署的应用程序。任务存储在不稳定的 Web 会话中。本文稍后将 TaskEE 改编为 TaskEEPA 以将任务存储在关系数据库而不是 Web 会话中。我们将仅展示与理解本文有关的代码片段;但有兴趣的读者可以研究这里(zip 文件)的完整但简单的源代码。

图 1:TaskEE 应用程序

跨部署保留会话

Java EE 应用程序在多个位置维护会话状态。最广为人知的位置是 HTTP Web 会话和有状态 Enterprise JavaBeans (EJB) 会话。从 Java EE 6 和引入计时器服务开始,持久 EJB 计时器也与会话数据相关联。这类数据的问题在于只要重新部署应用程序它们就会丢失。因为反复进行的应用程序开发需要频繁重新部署,所以工作效率损失很大。这是因为需要手动重现常见的用例步骤,如登录应用程序、填充一些数据以及执行某些步骤返回到重新部署之前的状态。

默认情况下,应用服务器会清除会话数据,不仅因为这样做可以使部署更易于管理,而且因为确有正当理由要清除会话数据。确实,对应用程序的更新可能包括对数据模型和业务逻辑的调整。不同版本之间数据模型可能不兼容,而业务逻辑可能导致给定版本中的一些有效会话数据在下一版本中变得不一致。这就是为什么清除会话数据通常是一个安全、合理的选择的原因。然而,清除数据经常会令开发人员头疼,因为实际上他们做的是向前兼容的更改。

GlassFish 提供了一个选项,可以跨重新部署保留会话数据。默认情况下此选项关闭,但您可以通过向 redeploy 命令传递 --keepstate=true 标志来显式启用它。

假定我们已经部署了 TaskEE。我们可以重新部署它并保留所有会话数据,如下所示:

$ asadmin redeploy --keepstate=true --name=taskee-1.0-SNAPSHOT target/taskee-1.0-SNAPSHOT.war 
Application deployed with name taskee-1.0-SNAPSHOT.
Command redeploy executed successfully.

如果您已在 TaskEE 中输入和删除了几个任务,可以轻松检查它们在重新部署之后是否得到保留。还可以通过 GlassFish Administration Console 执行保留会话的重新部署。只需确保在重新部署屏幕上选中相应的选项,如图 2 所示。结果是,GlassFish 项目提供的 NetBeans 和 Eclipse 插件在默认情况下会保留会话数据。

图 2:通过 GlassFish Administration Console 重新部署

还可使用其他方法来指示 GlassFish 保留会话。在 Web 应用程序部署为 WAR 存档的情况下,您可以创建或编辑 glassfish-web.xml 配置文件,如下所示:

<glassfish-web-app>
(...)
   <keep-state>true</keep-state> 
(...)
</glassfish-web-app>

同样,您可以通过类似方式编辑 glassfish-ejb-jar.xml 文件以在 EJB 容器内部保留会话数据:

<glassfish-ejb-jar>
(...)
   <keep-state>true</keep-state>
(...)
</glassfish-ejb-jar>

 

最后,对于 EAR 部署,可以通过相同的方式在 glassfish-application.xml 文件内部全局启用保留会话数据。您向 asadmin redeploy 传递的 –keepstate=true 标志优先于 XML 描述文件。Administration Console 的相应复选框同样如此,因为该复选框与 asadmin 在管理功能方面是等同的。

会话保留工作原理

我们来深入了解一下原理,会话存储是基于内存与持久存储之间的 Java 对象序列化的一个非常简单的过程。重新部署应用程序时,将丢弃其关联的类加载器并为新的应用程序代码和资源建立新的类加载器。会话通过反序列化从会话存储进行恢复。因此,更改类定义可能导致无法恢复会话。在这样的情况中,只要遇到问题,根本就无法恢复任何会话数据。当恢复失败时,还会在服务器日志中发出警告。

对于 Web 和 EJB 容器,对会话持久性类型的选择仅限于 file;否则,不会执行会话保留。这不需要您进一步的操作,因为 file 是默认值。但这意味着启用高可用性和会话故障切换时不可能实现会话保留,这是合理的,因为会话保留是面向开发的特性。从理论上来说,您也可以在生产中尝试会话保留,但是要知道,GlassFish 不会运行任何验证测试来在生产环境中支持会话保留。

最后,还应知道,如果您的需求超出 GlassFish 的会话保留特性所提供的范围,还有更多精心设计的解决方案可以减少重新部署应用程序的麻烦。一个流行的解决方案是来自 ZeroTurnaround 的 JRebel,当对应用程序字节码和资源进行修改时,该方案会对它们进行热修补。JRebel 还与 GlassFish 服务器和 NetBeans 之类的集成开发环境完美集成。相反,GlassFish 不处理字节码。

Servlet 3.0 片段

Servlet 3.0 规范引入了片段作为提高 Java EE Web 应用程序模块化程度的手段。片段技术可以将整个的 Web 应用程序分解成包含依赖项(如框架和库)的许多部分,分别嵌入。但还不止如此。Web 应用程序更常依赖于一组静态资产:级联样式表 (CSS)、图片、JavaScript 库和配置文件。大多数开发人员习惯于将这些文件存放在其 WAR 存档的结构中,但这样做并不是最佳选择。具体而言,您可能正在开发多个项目,这些项目将共享一些资产,如视觉品牌和客户端 JavaScript 代码。

只要这些资产需要更新,您就必须在每个项目中手动更新它们。

得益于 Servlets 3.0 片段,现在您可以将库之类的资产封装在 JAR 文件内。这是非常有用的,因为您可以分别为 CSS 主题、JavaScript 库(如 jQuery)、自己的 JavaScript 库等分别创建一个 JAR 文件。于是,您可以轻松地将它们作为项目的一部分组装在一起,尤其是当您使用依赖管理系统(如 Apache Maven 或 Apache Ant 及 Apache Ivy)时。

片段 JAR 文件的结构非常简单:所有资源都应放在 META-INF/resources 下,例如:

jquery-1.6.4.jar/
???¤?¤ META-INF
    ???¤?¤ resources
        ???¤?¤ jquery-1.6.4.min.js

然后就可以通过将每个片段 JAR 文件放在 WEB-INF/lib 下(就像任何其他常规依赖项那样)将其嵌入为 Web 应用程序的一部分。WEB-INF/lib 下的 JAR 文件的 META-INF/resources 中的每个资源可以直接通过已部署应用程序 Web 上下文的根目录来使用。

回到上一个示例,jquery-1.6.4.min.js 可通过类似以下的 URL 来提供:

http://server:port/webcontext/jquery-1.6.4.min.js

此特性对于更好地管理 Web 应用程序组合件十分有用,不过它还可以用于其他类型的资产,以包容开发与生产配置文件之间的变化,或针对具有特定品牌要求的不同客户。

在最后这个示例中,可以使用自定义 CSS 图片和图像针对每个客户制作视觉品牌 JAR 文件。通过使用一个具有依赖项管理功能的构建工具(如 Maven),可以将项目划分为包含应用程序自身的“核心”模块、几个产生片段 JAR 文件的“品牌”模块,以及每个客户的“组合件”模块(每个组合件对“核心”模块和所需的品牌模块具有依赖关系)。这种方法便于按行业生产构件,同时可保持高度模块化。

应用程序范围的资源

Java EE 架构的一个要点就在于应用程序模块易于与其资源配置、管理和绑定分离。使用 Java 命名和目录接口 (JDNI) 注册表,应用程序只需知道每个所需资源的声明名称。一个典型的例子是使用 java:jdbc/MyDatasource 这样的 JNDI 名称访问 JDBC 连接池。

使用此方法,应用程序完全无需了解技术细节,如数据库的物理主机、所使用的 JDBC 驱动程序、数据库供应商、数据库登录凭证或者连接池大小。此方法还有其他好处,因为开发人员可以工作在一个易于建立的轻型体系上,而基础架构专家无需重新封装部署构件。他们只需声明、配置、监视和微调 Java EE 应用程序工作所需的资源。必须提取 WAR 文件的内容、编辑配置文件、重新封装该 WAR 文件并进行部署从来就不会让人感到愉快。

然而在有些情形下,在应用服务器中单独声明和配置资源实际上会不必要地增加部署应用程序的复杂性。对软件供应商来说,一个好的例子是,他们需要以独立的自包含包的形式提供应用程序的一个版本。当目标用户对于如何运行应用服务器和向其部署应用程序的技术知识十分有限时,提供独立版本很有用,这对于发布产品的评估版本也很有用。这两种情况下,应用程序都将随应用服务器一起提供并提供一个简单直接的启动 脚本。

需要在应用程序中嵌入资源声明的另一种情况是复杂的持续部署流水线,在这种情况下,直接向应用服务器实例部署构件比单独处理其配置更容易。大多数云/PaaS(平台即服务)/托管环境就是这种情况,在这些环境下,服务器配置必须是通用的,不能自定义。

GlassFish 支持通过可部署构件声明资源。这些资源称为应用程序范围的资源,意味着它们仅在应用程序的范围内可见。这种情况会产生一个有趣的效果,就是部署到应用服务器上的其他应用程序不能访问这些资源。比如说,如果您声明一个包含 20 个实例的应用程序范围的 JDBC 连接池,就不会存在其他应用程序使用这些资源并导致潜在延迟和资源匮乏的风险。

使用应用程序范围的资源

我们对 TaskEE 进行了修改,使其变成了 TaskEEPA(参见图 3)。该应用程序看起来没有变化。现在,我们将任务项存储在关系数据库中,而不是存储在 Web 会话中。这意味着所有用户现在共享同一任务列表,但该程序只是为本文需要而开发的,所以没有什么问题。

图 3:TaskEEPA(或 TaskEE)获得关系数据库持久性

这里将一个任务定义为一个 Java Persistence API (JPA) 映射的实体 bean。请看下面清单 1 所示的代码片段。

@Entity
public class Task implements Serializable {

    @Id
    @GeneratedValue
    private Long id;

    private String description;

    public Task(String description) {
        this.description = description;
    }

    public Task() {
    }
    
    // (...) boilerplate getters and setters

清单 1. 定义任务

这些实体作为持久性单元的一部分来进行管理。JPA 要求将配置定义为 META-INF/persistence.xml 的一部分。在本例中,其内容为清单 2 所示的代码:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
                 http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
    <persistence-unit name="tasks-pu" transaction-type="JTA">
        <jta-data-source>java:app/jdbc/tasks-datasource</jta-data-source>
        <class>taskee.entities.Task</class>
        <properties>
            <property name="eclipselink.ddl-generation" value="create-tables"/>
            <property name="eclipselink.logging.level" value="FINEST"/>
        </properties>
    </persistence-unit>
</persistence>

清单 2. 定义配置

对于已经熟悉 JPA 的大多数 Java EE 开发人员来说,此配置文件的内容并不令人惊奇。这里我们有一个实体 (taskee.entities.Task) 要管理,我们使用名为 EclipseLink 的 JPA 实现将创建必要的数据库表,除非这些表已经存在。

该配置引用一个 JNDI 名为 java:app/jdbc/tasks-datasource 的数据源。请注意该 JNDI 路径中的 app 命名空间,它用于表示这是一个应用程序范围的资源。如果该数据源是按传统方式在应用程序构件之外进行的声明,那么我们可能将其命名为了 java:jdbc/tasks-datasource。

应用程序范围的资源作为一个名为 glassfish-resources.xml 的文件的一部分声明。在 WAR 部署的情况下,该文件放在 WEB-INF/glassfish-resources 中。在本例中,我们对该文件声明了数据源,代码如清单 3 所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN"
        "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
    <jdbc-connection-pool
            max-pool-size="10"
            datasource-classname="org.apache.derby.jdbc.ClientDataSource"
            res-type="javax.sql.DataSource"
            name="java:app/jdbc/tasks-pool"
            pool-resize-quantity="10">
        <property name="user" value="APP"/>
        <property name="PortNumber" value="1527"/>
        <property name="password" value="APP"/>
        <property name="ServerName" value="localhost"/>
        <property name="databaseName" value="tasks-db"/>
        <property name="connectionAttributes" value=";create=true"/>
    </jdbc-connection-pool>
    <jdbc-resource
            pool-name="java:app/jdbc/tasks-pool"
            jndi-name="java:app/jdbc/tasks-datasource"/>
</resources>

清单 3. 声明数据源

resources 标记可以包含不同类型的资源声明。这里,我们声明了一个到 Apache Derby 服务器的 JDBC 连接池,然后将其绑定到 java:app/jdbc/tasks-datasource 应用程序范围的 JNDI 资源名称。其他类型的候选资源包括外部 JNDI 引用、邮件资源、连接器资源和资源适配器。

应用程序范围的资源将在应用程序部署时创建。它们将在应用程序取消部署或重新部署时删除。注意,这并不一定意味着也会删除结果数据!在 TaskEEPA 的情况下,我们所引用的放入 Apache Derby 数据库中的数据将保持不变。您可以请求在执行重新部署时保留资源。要从 asadmin 执行该操作,请传递 --preserveAppScopedResources=true 标志。Administration Console 中也有相关选项。

@DataSource 批注

对于数据源的声明,有一个较不为人所知的选项。我们引入的应用程序范围的资源机制是针对 GlassFish 的。Java EE 6 规范提供 @DataSource 批注,它可以应用于类型。您可以传递 JDBC 参数作为批注参数,如清单 4 所示:

@DataSourceDefinition (
    name="java:app/env/UserDB",
    className="javax.sql.DataSource",
    portNumber=1527,
    serverName="localhost",
    databaseName="sample",
    user="APP",
    password="APP")
@Singleton
public class DataSources { }

清单 4. 传递 JDBC 参数

当将 DataSources singleton 加载到 EJB 容器中时,即以 java:app/env/UserDB JNDI 名称声明了一个应用程序范围的数据源。当有多个数据源需要声明时,可以声明多个类,每一个均以 @DataSource 批注。或者,还可以使用单个应用 @DataSources 批注的类,可在该批注中嵌入多个 @DataSource 批注。

应用程序版本控制

已部署的应用程序与一个名称相关联,以便使用这个名称来执行各种管理任务,包括重新部署、取消部署、监视等等。默认情况下,该名称对应于构件的名称。

例如,部署 taskee-1.0-SNAPSHOT.war 时,将自动为我们部署的 Web 应用程序定义名称 taskee-1.0-SNAPSHOT。此名称可在部署时通过使用 --name 标志更改,例如:

$ asadmin deploy --name=taskee target/taskee-1.0-SNAPSHOT.war 
Application deployed with name taskee.
Command deploy executed successfully.

从 GlassFish 3.1 开始,还可以用一个“版本”来标记应用程序名。此功能允许将一个应用程序的共存版本同时部署到一个应用服务器。

一次只能有一个版本处于活动状态,而其他版本保持已部署和已配置状态。此要求有助于在版本之间快速切换并在必要时回滚到早期版本。这也有助于减少执行升级时的停机时间,因为在要取消部署的早期版本和要部署和运行的新版本之间需要多个任务。例如,可以将新版本部署到集群,并在所有实例准备好运行时立即切换为活动版本。同时,先前版本保持活动并继续服务客户端请求。

应用程序版本控制是通过在 --name 参数后面添加冒号和版本标记作为后缀来完成的。假设我们要将 TaskEE 部署为 TaskEE 版本 1.0:

$ asadmin deploy --name=taskee:1.0 target/taskee-1.0-SNAPSHOT.war 
Application deployed with name taskee:1.0.
Command deploy executed successfully.

然后,我们可能决定使用清单 5 中所示的部署命令将 TaskEEPA 部署为 Taskee:2.0。(其中的警告非常不错,因为我们先前已经对其进行了部署且 EclipseLink 已经为我们创建了数据库表。)

$ asadmin deploy --name=taskee:2.0 target/taskeepa-1.0-SNAPSHOT.war 

PER01003: Deployment encountered SQL Exceptions:
	PER01000: Got SQLException executing statement "CREATE TABLE TASK (ID BIGINT NOT NULL, 
DESCRIPTION VARCHAR(255), PRIMARY KEY (ID))": java.sql.SQLException: Table/View 'TASK' 
already exists in Schema 'APP'.
	PER01000: Got SQLException executing statement "CREATE TABLE SEQUENCE 
(SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(15), PRIMARY KEY (SEQ_NAME))": 
java.sql.SQLException: Table/View 'SEQUENCE' already exists in Schema 'APP'.
	PER01000: Got SQLException executing statement "INSERT INTO 
SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 0)": 
java.sql.SQLIntegrityConstraintViolationException: The statement was aborted because 
it would have caused a duplicate key value in a unique or primary key constraint or 
unique index identified by 'SQL110817213731180' defined on 'SEQUENCE'.
Command deploy completed with warnings.

清单 5:将 TaskEEPA 部署为 Taskee 2.0

我们可以检查所有应用程序和版本以查看活动版本是否是最后一个部署的:

$ asadmin list-applications -l
NAME        TYPE   STATUS    
taskee:1.0    disabled   taskee:2.0    enabled    

版本标记须经过验证符合以下正则表达式:

[A-Z|a-z|0-9]+([_|.|-][A-Z|a-z|0-9]+)* 

此验证机制为支持常用命名方案提供了广泛的选择。有效的版本标记可以是 1.0-SNAPSHOT、1.5_GA、RC-10.2011_08_26、internal-3.0.1-beta3 等等。

我们可以使用 enable 和 disable 命令来回切换活动版本,如清单 6 所示。注意,尽管一个应用程序只能有一个活动版本,但也可以一个也没有。

$ asadmin enable taskee:1.0
Command enable executed successfully.
$ asadmin list-applications -l
NAME        TYPE   STATUS    
taskee:1.0  <web>  enabled   
taskee:2.0  <web>  disabled  
Command list-applications executed successfully.
$ asadmin disable taskee:1.0
Command disable executed successfully.
taskee:1.0  <web>  disabled  
taskee:2.0  <web>  disabled  
Command list-applications executed successfully.
$ asadmin enable taskee:2.0
Command enable executed successfully.
$ asadmin list-applications -l
NAME        TYPE   STATUS    
taskee:1.0  <web>  disabled  
taskee:2.0  <web>  enabled   

清单 6:启用和禁用活动版本

应用程序版本可以部署为disabled。这很有用,比如,在一个早期版本正在运行时进行滚动升级并在稍后在新版本已部署后切换到该新版本。为此需要将 --enabled=false 参数传递到 deploy 命令:

$ asadmin deploy --enabled=false  --name=taskee:2.0 taskeepa-1.0-SNAPSHOT.war

在需要 --name 参数的位置(如 deployredeployundeploy show-component-status 命令)可以使用以版本标记限定的名称。我们可以取消部署特定版本,如下所示:

$ asadmin undeploy taskee:2.0
Command undeploy executed successfully.

考虑到您可能部署了单个应用程序的许多版本,您可以在版本中使用通配符 (*)。例如,--name=’1.0-*’ 匹配以 1.0- 开头的任何版本,如 1.0-beta1 和 1.0-beta2。您应注意要将这些版本编码在引号中以防止从终端使用 asadmin 时发生 shell 扩展。

我们可以使用通配符毫不费力地取消 TaskEE 的所有版本的部署,如清单 7 所示:

$ asadmin list-applications -l
NAME        TYPE   STATUS    
taskee:1.0  <web>  disabled  
taskee:2.0  <web>  enabled   
Command list-applications executed successfully.
$ asadmin undeploy 'taskee:*'
Command undeploy executed successfully.
$ asadmin list-applications -l
Nothing to list.
Command list-applications executed successfully.

清单 7:取消部署 TaskEE 的所有版本

应用程序版本控制由法国工程服务公司 Serli 提供,这证明 GlassFish 是真正的开源项目。

总结

本文介绍了 GlassFish 的 4 个特性,它们为开发人员和基础架构工程师带来了 Java EE 应用程序部署方面的灵活性。

  • 会话保留可以在开发应用程序时节省大量时间。

  • Servlet 片段让我们可以更轻松地将静态资产封装成库,从而便于管理大量部署配置文件和目标的组合件。

  • 应用程序范围的资源让我们可以将资源声明嵌入应用程序构件,如 WAR 存档。尽管这违背了 Java EE 模型的优美原则(应用程序与其资源声明和配置相分离),但当务实的考虑鼓励采取此类折衷方法时,嵌入资源使 GlassFish 成为一个很有吸引力的选择。

  • 最后,应用程序版本控制让我们可以在版本之间快速切换、回滚到早前的版本、最大程度减少执行升级时的停机时间。

这些特性相互补充,因为公有和私有云计算平台都在越来越多地用于部署应用程序。与 DevOps 流水线联系在一起时,云计算平台需要更多的部署灵活性;对最终用户而言,版本升级和回滚应该轻松、透明,而服务器实例应易于供应和取消供应。

另请参见

关于作者

Julien Ponge 是一位长期从事开源工作的技术高人。他创建了 IzPack 安装程序框架,还参与了其他几个项目,包括与 Sun Microsystems 合作的 GlassFish 应用服务器。他拥有 UNSW Sydney 和 UBP Clermont-Ferrand 的计算机科学博士学位,目前是 INSA de Lyon 计算机科学与工程系的副教授,致力于研究一个结合使用编程语言、虚拟机和中间件的办法。由于熟练掌握行业和学术两个领域中的语言,因此他正在积极推进这两个领域之间更进一步的协作。