JavaEE7-初识-全-

JavaEE7 初识(全)

原文:zh.annas-archive.org/md5/2770519401626d3e2df91d3cd7012b18

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

当我们考虑编写这本书时,主要目标是介绍 Java EE 7 平台的新特性。但在写作过程中,我们意识到清晰地简洁地展示相关规范及其实现方法会很有趣。这促使我们想象出一个项目,该项目将帮助展示几乎受 Java EE 7 影响的全部规范,以及如何将它们组合起来以构建大型应用程序。

简而言之,这本书的目标不是展示如何实现每个 Java EE 7 规范的不同方面或列出最佳实践。相反,它将自己定位为新建城市的黄页。换句话说,这本书将帮助您发现 Java EE 7 引入的创新,并为您提供构建稳固应用程序的思路。

本书涵盖的内容

第一章,Java EE 7 的新特性,概述了 Java EE 7 平台所做的改进。

第二章,新规范,解释了 Java EE 7 中添加的新规范的相关概念,并展示了如何使用它们。

第三章,表示层,演示了 Java EE 7 平台为表示层规范带来的改进的实现。

第四章,Java 持久化 API,展示了您的 Java 应用程序如何以安全的方式将数据存储到数据库并检索数据,并解释了相关规范中做出的创新。

第五章,业务层,首先介绍了业务层的改进,然后演示了如何将各种 Java EE 7 规范组合起来以实现应用程序。

第六章,与外部系统通信,演示了 Java EE 7 应用程序如何与异构系统交互。

第七章,注解和 CDI,解释了如何使用注解和 CDI 来提高应用程序的质量。

第八章,验证器和拦截器,展示了如何在 Java EE 环境中实现数据的验证和拦截,以确保应用程序处理的数据质量。

第九章,安全,演示了在 Servlet 和 EJB 容器中实现安全以及设置个人安全模块的过程。

阅读本书所需的条件

为了实现本书中提供的各种示例,您需要以下软件:

  • NetBeans IDE 7.3.1 或更高版本

  • JDK 7

  • GlassFish 应用程序服务器 4,至少 b89 版本

  • MySQL 5.5 或更高版本的数据库管理系统

这本书面向的对象

根据主要目标,本书针对具有 Java 知识的三组人群。他们是:

  • 使用 Java EE 平台的新手,想要了解 Java EE 7 的主要规范

  • 尝试过之前版本的 Java EE 的开发者,想要了解 Java EE 7 带来了哪些新特性

  • 想要学习如何组合各种 Java EE 7 规范以构建健壮和安全的企业的建筑师

术语表

在本书中,你会找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例,以及它们含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:"我们可以通过使用 include 指令来包含其他上下文。"

代码块设置如下:

@NameBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface ZipResult {})

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中如下所示:"展开server-config菜单。"

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

小贴士和技巧以如下形式出现。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们来说很重要,以便我们开发出真正能让你受益的标题。

要向我们发送一般反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在一个主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。

错误更正

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表格链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从 www.packtpub.com/support 查看任何现有勘误。

侵权

互联网上版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上遇到我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌侵权材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. Java EE 7 的新特性

由于其使用,分布式应用程序需要一些非功能性服务,例如远程访问、安全性、事务管理、并发性和健壮性等。除非你有提供这些类型服务的 API,否则你需要从头开始实现它们,因此会增加错误数量,降低软件质量,并增加生产成本和时间。Java EE 平台旨在让开发者摆脱这些担忧。它由一组促进分布式、健壮、可扩展和互操作应用程序的开发和部署的 API 组成。

自 1999 年首次发布以来,Java EE 平台通过提供比之前版本更新、更丰富、更简单的版本而不断发展。为了让你对 Java EE 7 的改进有一个概述,本章讨论以下主题:

  • Java EE 简史

  • Java EE 7 的主要目标

  • Java EE 7 的创新之处

Java EE 简史

以前称为 J2EE,Java EE 平台的第一版于 1999 年 12 月正式发布,包含 10 个规范。在这些规范中,有用于数据展示的 Servlets 和 JavaServer Pages (JSP),用于持久数据管理的 Enterprise JavaBeans (EJB),通过 RMI-IIOP (远程方法调用跨 Internet Inter-ORB 协议) 远程访问业务服务,以及用于发送消息的 JMS (Java 消息服务) 规范。

尽管有努力和许多贡献,Java EE 的早期版本过于复杂且难以实现。这导致了大量批评,并引发了像 Spring 框架这样的竞争框架的兴起。

从之前的失败中吸取了教训,该平台在时间上经历了相当大的演变,直到 Java EE 5 的推出,这使得平台能够恢复其失去的声誉。从这个版本开始,Java EE 继续提供更简单、更丰富、更强大的平台版本。

Java EE 简史

以下图表概述了自 1999 年 12 月发布第一个版本以来对 Java EE 平台所做的重大更改。此图表突出了每个版本的发布日期、更新和主要改进。它还让我们对每个版本背后的中心主题有一个了解。

Java EE 7 的主要目标

自 2006 年 5 月以来,Java EE 平台在实现方面经历了显著的演变。首先,在 Java EE 5 中,它通过允许将简单的 Java 类(POJO 类)通过注解或 XML 描述转换为业务对象,极大地简化了应用程序的开发。在简化的道路上,Java EE 6 帮助丰富了注解,并引入了诸如修剪、RESTful Web 服务、CDI、EJB Lite、异常配置和 Web 配置文件等新概念。这使得平台能够提供许多易于部署和消费的服务。在 Java EE 6 的成功之后,JCP(Java 社区进程)设想通过提供云支持的基础设施,将平台转变为一个服务。但是,由于相关规范缺乏显著进展,它修订了其目标。正是从为 Java EE 平台向云迁移做准备的角度,Java EE 7 专注于生产力和 HTML5 支持。由于错过了大目标(即迁移到云),它将通过完成 Java EE 6 特性和添加一些新规范来实现其新目标。

生产力

Java EE 7 在许多方面提高了生产力。通过简化诸如 JMS 和 JAX-RS 之类的某些 API,Java EE 7 平台显著减少了样板代码。正如您将在接下来的章节中注意到的那样,发送一个 JMS 消息可以只占用一行,并且不再需要创建多个对象,正如 JMS 1.1 的情况,那时首先需要创建一个 ConnectionSessionMessageProducerTextMessage

Java EE 7 集成了新的 API,以更好地满足企业应用程序在处理大量数据方面的需求。例如,我们有 并发实用工具,它允许在容器内创建托管线程,并使开发者能够将大型过程分解为可以并发计算的小单元。同样,还有一个用于批处理的 Java API,用于管理大量和长时间运行的任务。

最后,Java EE 7 在注解方面得到了丰富,并侧重于异常配置。无论是数据源还是批处理,兼容的 Java EE 7 容器都提供了一系列默认对象。甚至可以仅通过少量配置就生成复杂的应用程序。

简而言之,新平台使开发者免于执行许多任务和创建多种类型的对象,这些对象对于设置应用程序是必需的。

HTML5 支持

有些人可能会 wonder 为什么对 HTML5 的支持如此重要。答案是简单的:HTML5 是 HTML 标准的最新版本。更重要的是,它提供了新的功能,简化了更强大和适合的 Web 应用程序的构建。例如,通过 HTML5 的<audio><video>元素,您可以在不使用第三方插件(如 Flash)的情况下播放、暂停和恢复音频和视频媒体内容。通过画布元素和 WebGL 库(OpenGL 的一个子集),您可以在网站上轻松集成 2D 和 3D 图形。在客户端和服务器之间的通信方面,HTML5 中 WebSocket 协议的完美集成使我们能够构建具有全双工 P2P 通信的 Web 应用程序,并克服 HTTP 在实时通信中的某些限制。使用此协议,您将不会在实现需要客户端和服务器之间实时通信的聊天应用程序或其他 Web 应用程序时遇到困难,例如交易平台和电子商务平台。在数据交换方面,HTML5 对 JSON 格式的原生支持简化了信息处理并减少了文档的大小。许多其他领域也得到了改进,但在此我们只提及这些。

在所有这些创新的基础上,JSF(JavaServer Faces)中增加了对 HTML5 特性的支持,Java EE 7 平台添加了一个新的 API 来构建 WebSocket 驱动的应用程序,并添加了另一个 API 来处理 JSON 数据格式。

Java EE 7 的新特性

Java EE 7 是作为一个 Java 规范请求(JSR 342)开发的。它总共有 31 个规范,包括 4 个新规范、10 个主要版本和 9 个 MR(维护版本)。所有这些规范都由 GlassFish Server 4.0(可通过地址glassfish.java.net/download.html访问)考虑在内,它是 Java EE 7 的参考实现。

Java EE 中引入的新规范如下:

经历了重大变更的从 Java EE 6 平台继承的 API 如下:

  • Java 平台,企业版 7 (Java EE 7) 规范 (jcp.org/en/jsr/detail?id=342),与 Java EE 6 相比,进一步简化了开发,增加了对 HTML5 的支持,并为平台迁移到云做好了准备

  • Java Servlet 3.1 规范 (jcp.org/en/jsr/detail?id=340) 引入了一些特性,例如非阻塞 I/O API 和协议升级处理

  • 表达式语言 3.0 (jcp.org/en/jsr/detail?id=341) 已从 JSP 规范请求中分离出来,并带来了许多变化,包括用于独立环境的 API、lambda 表达式和集合对象支持

  • JavaServer Faces 2.2 (jcp.org/en/jsr/detail?id=344) 集成了对 HTML5 标准的支持,并带来了资源库合同、Faces Flow 和无状态视图等特性

  • Java 持久化 2.1 (jcp.org/en/jsr/detail?id=338) 为我们带来了执行存储过程、运行时创建命名查询、通过 Criteria API 构建批量更新/删除、运行时覆盖或更改获取设置以及像 SQL 一样进行显式连接的机会

  • 企业 JavaBeans 3.2 (jcp.org/en/jsr/detail?id=345) 引入了手动禁用有状态会话 Bean 消息化的能力,并且放宽了定义默认本地或远程业务接口的规则

  • Java 消息服务 2.0 (jcp.org/en/jsr/detail?id=343) 简化了 API

  • JAX-RS 2.0:Java RESTful Web 服务 API (jcp.org/en/jsr/detail?id=339) 简化了 RESTful Web 服务的实现,并引入了包括客户端 API、异步处理、过滤器和中继器等新特性

  • Java EE 1.1 的上下文和依赖注入 (jcp.org/en/jsr/detail?id=346) 引入了许多变化,其中一些是访问当前 CDI 容器、访问 Bean 的非上下文实例以及显式销毁 Bean 实例的能力

  • Bean 验证 1.1 (jcp.org/en/jsr/detail?id=349) 引入了方法构造函数验证、分组转换和使用表达式语言的消息插值支持

只有以下 API 受维护版本的影响:

摘要

在简要介绍 Java EE 的演变并分析最新平台的目标后,我们列出了在 Java EE 7 中改进或添加的所有规范。在下一章中,我们将重点关注新规范,以突出其有用性并展示它们如何实现。

第二章:新规范

本章将仅讨论在 Java EE 7 中添加的新规范。具体来说,我们将介绍并展示如何使用以下 API:

  • Java EE 1.0 并发工具

  • Java 平台 1.0 批处理应用程序

  • Java API for JSON Processing 1.0

  • Java API for WebSocket 1.0

Java EE 1.0 并发工具

Java EE 1.0 并发工具是在 JSR 236 下开发的。本节仅为您提供一个 API 的概述。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr236/index.html下载。

为什么需要并发?

在计算机科学中,并发是指一个应用程序或系统执行多个任务的能力。在多任务系统出现之前,计算机一次只能运行一个进程。当时,程序不仅难以设计,而且它们是从头到尾顺序执行的,当机器运行一个可以访问外围设备的程序时,正在运行的程序首先被中断以允许读取外围设备。

并发的益处

多任务操作系统的开发使得在机器内同时执行多个进程(运行程序的实例)和进程内的多个线程(也称为轻量级进程;它们是可以在彼此之间并发运行的进程的子集)成为可能。由于这一进步,现在可以同时运行多个应用程序,例如,在写文本文档的同时听音乐和下载文档。

在企业应用程序中,通过在线程中异步运行重处理,并发可以提高程序的交互性。它还可以通过将大任务分解成许多小单元,这些单元将由许多线程同时执行,来提高应用程序的响应时间。

并发的风险

尽管每个线程都有其适当的执行栈,但多个线程共享相同资源或相互依赖的情况非常普遍。在这种情况下,缺乏良好的同步会使线程行为不可预测,并可能降低系统性能。例如,相互关联的线程缺乏协调可能导致死锁和无限中断处理。

并发与 Java EE

正如我们之前所看到的,线程的误用可能对应用程序产生灾难性的后果。在容器的情况下,它不仅可能损害其完整性,而且可能未能充分利用提供给其他组件的资源。这也是为什么开发者不允许在容器中创建线程的原因之一。

为了在 Java EE 组件中实现并发,Java EE 7 平台集成了并发实用工具。使用此 API,Java EE 服务器可以了解线程使用的资源,并为它们提供良好的执行上下文。此外,它允许服务器管理线程池和生命周期。

Java EE 并发 API

Java EE 1.0 并发实用工具的开发目标是以下内容:

  • 提供一个简单且灵活的并发 API 给 Java EE 平台,同时不损害容器

  • 通过提供一致性,促进从 Java SE 迁移到 Java EE

  • 允许实现常见的和高级并发模式

并发实用工具是在 Java SE 下 JSR-166 开发的并发实用工具 API 之上构建的(这有助于从 Java SE 迁移到 Java EE)。它提供了四个主要的编程接口,其实例必须作为容器管理的对象提供给应用程序组件。提供的接口有:ContextServiceManagedExecutorServiceManagedScheduledExecutorServiceManagedThreadFactory。所有这些接口都包含在javax.enterprise.concurrent包中。

这四个接口可以这样解释:

  • 托管执行服务ManagedExecutorService接口扩展了java.util.concurrent.ExecutorService接口。它允许我们提交一个将在由容器创建和管理的单独线程上运行的异步任务。默认情况下,任何符合 Java EE 7 规范的服务器都必须提供一个ManagedScheduledExecutorService,可以通过JNDI名称java:comp/DefaultManagedScheduledExecutorService访问。但是,如果您想创建自己的,您必须首先在web.xml文件中声明ManagedExecutorService资源环境引用,对于 Web 应用程序或ejb-jar.xml中的 EJB 模块。规范建议将所有ManagedExecutorService资源环境引用组织在java:comp/env/concurrent子上下文中。

    • 以下配置是一个ManagedExecutorService资源环境引用的示例声明:

      	<resource-env-ref>
      	  <resource-env-ref-name>
      	    concurrent/ReportGenerator
      	  </resource-env-ref-name>
      	  <resource-env-ref-type>
      	    javax.enterprise.concurrent.ManagedExecutorService
      	  </resource-env-ref-type>
      	</resource-env-ref>
      

      提示

      下载示例代码

      您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

    • 在声明 JNDI 引用之后,您可以使用@Resource注解将其注入,如下面的代码所示:

      	@Resource(name="concurrent/ReportGenerator")
      	ManagedExecutorService reportGenerator;
      
    • 提交给容器的任务必须实现java.lang.Runnablejava.util.concurrent.Callable接口。这两个接口之间的区别在下面的表格中展示:

      Runnable Callable
      自 JDK 1.0.0 以来。 自 JDK 5.0 以来。
      它具有 run() 方法来定义任务。 它具有 Callable<V> 实例的 call() 方法来定义任务。
      它具有 run() 方法来定义任务。 它具有 Callable<V> 实例的 call() 方法来定义任务。
      run() 方法不能抛出受检异常。 call() 方法可以抛出受检异常。
    • 以下代码演示了如何定义一个将异步运行报告的任务。

      	public class ReportGeneratorTask implements Callable<String>{
      
      	  @Override
      	  public String call() throws Exception {
      	    //generate report
      	    return "The report was generated successfully";
      	  }   
      	}
      
    • 以下代码展示了如何提交任务。我们可以看到,ManagedExecutorService 实例的 submit() 方法返回一个 Future 类型的对象,当任务可用时,将返回运行任务的结果:

      	Future<String> monitor = reportGenerator
      	.submit(new ReportGeneratorTask());
      	String result = monitor.get();         
      
  • 托管定时执行服务接口: ManagedScheduledExecutorService 接口扩展了 ManagedExecutorServicejava.util.concurrent.ScheduledExecutorService 接口,以便在特定时间执行任务。

    • 此接口的实例定义方式与 ManagedExecutorService 接口相同。以下代码演示了如何在提交后 10 秒执行任务:

      	Future<String> monitor = reportGenerator
      	  .schedule(new ReportGeneratorTask(), 10,TimeUnit.SECONDS);
      
  • 托管线程工厂接口: ManagedThreadFactory 接口提供在容器中创建托管线程实例的方法。任务必须实现 java.lang.Runnable 接口。以下代码演示了如何创建和运行一个容器管理的线程。

    Thread myThread = threadFactory.newThread(new ReportManagedThread()); 
    myThread.start();
    
  • 上下文服务接口: 此接口允许在不使用 ManagedExecutorServiceManagedScheduledExecutorService 接口的情况下创建上下文对象,正如我们在之前的案例中所做的那样,目的是为了允许扩展 Java EE 平台的并发能力。具体来说,使用此接口,您可以在容器内创建工作流系统或使用定制的 Java SE 平台 ExecutorService 实现。例如,如果您希望使用 Java SE 中 java.util.concurrent.ThreadPoolExecutor 类提供的池管理机制来管理 Java EE 组件上下文中的线程,您只需将 ManagedThreadFactoryExecutorServiceContextService 对象组合起来即可。结果如下所示:

    public class ReportCustomizedThread implements Runnable {
    
      public void run() {
        //Report processing ...
      }
    }
    
    @Resource(name=»concurrent/ReportManagedThreadGenerator»)
    ManagedThreadFactory threadFactory;
    
    @Resource(name=»concurrent/ReportContextServiceGenerator»)
    ContextService contextService; 
    
    ReportCustomizedThread reportThread = new ReportCustomizedThread();
    Runnable proxy =contextService.createContextualProxy(reportThread,Runnable.class);
    ExecutorService executorService =Executors.newFixedThreadPool(20, threadFactory);
    Future result = executorService.submit(proxy);
    //...
    

这可能是使用此功能的一个简单示例。对于更高级的示例,请参阅 上下文服务 部分的规范文档。

以下图表提供了并发工具与其他 Java EE 平台元素之间关系的概述:

Java EE 并发 API

此外,还可以优化不同资源的配置以获得更好的性能(详细信息请参阅规范文档),Java EE 1.0 的并发工具还提供了许多其他接口,如 ManagedTaskListener,可用于监控任务 Future 对象的状态。

Java 平台 1.0 的批处理应用程序

Java 平台 1.0 的批处理应用程序 API 是在 JSR 352 下开发的。本节仅为你提供一个 API 的概述。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr352/index.html下载。

什么是批处理?

根据剑桥高级学习者词典,批处理是一组同时处理或被认为类型相似的事物或人。过程是一系列你为了达到某个结果而采取的行动。基于这两个定义,我们可以这样说,批处理是为了达到某个结果而在大量数据上重复执行的一系列操作。鉴于它必须处理的大量数据,批处理通常用于日终、月终、期终和年终处理。

以下是一些你可以使用批处理的领域简短列表:

  • 从/到 XML 或 CSV 文件的数据导入/导出

  • 会计处理,如合并

  • ETL提取-转换-加载)在数据仓库中

  • 数字文件处理(下载、处理或保存)

  • 通知服务的订阅者(如论坛、群组等)

为什么需要一个专门的批处理 API?

在对批处理有了大致了解之后,有些人可能会问自己:为什么不直接设置一个foreach循环来启动多个线程呢?首先,你必须知道批处理并不仅仅关注执行速度。实际上,处理大量数据常常会受到许多异常的影响,这可能会产生许多担忧:在出现异常的情况下应该采取什么行动?我们应该因为任何异常而取消整个流程吗?如果不是,应该取消哪些操作?针对哪种类型的异常?如果你只需要取消一定数量的交易,你如何识别它们?在批处理结束时,了解有多少处理被取消总是很重要的。有多少被成功注册?有多少被忽略?

正如你所见,我们还没有完成识别批处理可能引发的问题,但我们发现这已经足够多了。试图自己构建这样的工具可能会不仅使你的应用程序复杂化,还可能引入新的错误。

理解批处理 API

Java 平台 1.0 的批处理应用程序 API 是为了解决前面列表中列出的不同需求而开发的。它针对 Java SE 和 Java EE 应用程序,并需要至少 JVM 的第 6 个版本。

这个 API 提供的功能可以概括如下:

  • 它原生地提供了读取器-处理器-写入器模式,并赋予你实现自己的批处理模式的能力。这允许你根据具体情况选择最佳的模式。

  • 它提供了为每种类型的错误定义批处理行为(跳过、重试、回滚等)的可能性。

  • 它支持许多步骤级度量,例如:rollbackCountreadSkipCountwriteSkipCount等,用于监控。

  • 它可以被配置为并行运行一些进程,并提供使用 JTA 或RESOURCE_LOCAL事务模式的可能性。

为了做到这一点,Java 平台 1.0 的批处理应用程序 API 基于一个稳固的架构,可以通过以下图表来概述。一个工作由一个JobOperator管理,并有一个或多个步骤,这些步骤可以是批处理单元。在其生命周期内,关于一个工作的信息(元数据)存储在JobRepository中,如下面的图表所示:

理解批处理 API

JobRepository

如我们之前所述,JobRepository存储有关当前和过去运行的工作的元数据。它可以通过JobOperator访问。

工作

一个工作可以被视为封装一个批处理单元的实体。它由一个或多个步骤组成,这些步骤必须在名为工作配置文件工作 XML的 XML 文件中进行配置。该文件将包含工作识别信息以及构成工作的不同步骤。下面的代码展示了工作 XML 文件的骨架。

<job id="inscription-validator-Job" version="1.0">  

  <step id="step1" >        
    ... 
  </step>    
  <step id="step2" >        
    ...   
  </step>
</job>

工作 XML 文件按照约定命名为<name>.xml(例如,inscriptionJob.xml),并且应该存储在META-INF/batch-jobs目录下,以便于便携式应用程序。

步骤

步骤是批处理的一个自主阶段。它包含定义和控制一个批处理片段所需的所有必要信息。批处理步骤要么是块要么是批处理单元(两者互斥)。以下代码的步骤是一个块类型步骤:

<job id="inscription-validator-Job" version="1.0">
  <step id="validate-notify" >        
    <chunk>
       <reader ref="InscriptionReader" />
       <processor ref="InscriptionProcessor" />
       <writer ref="StudentNotifier" />
    </chunk>     
  </step>    
</job>

是一种实现读取-处理-写入模式的步骤类型。它在可配置的事务范围内运行,并且可以接收许多配置值。以下代码是前面代码中显示的inscription-validator-Job的更完善版本。在这个列表中,我们添加了一个配置来定义将用于管理块提交行为的单元元素(checkpoint-policy="item"),以及一个配置来定义在提交之前要处理的项目(单元元素)数量(item-count="15")。我们还指定了如果块抛出任何配置的可跳过的异常,步骤将跳过的异常数量(skip-limit="30")。

以下代码是一个具有一些配置的块类型步骤示例:

<job id="inscription-validator-Job" version="1.0" 
  >   
  <step id="validate-notify" >        
    <chunk item-count="15" checkpoint-policy="item" 
      skip-limit="30">
      <reader ref="InscriptionReader" />
      <processor ref="InscriptionProcessor" />
      <writer ref="StudentNotifier" />
    </chunk>     
  </step>    
</job>

以下代码展示了块批处理批件实现的样子。InscriptionCheckpoint允许您知道正在处理哪一行。本节的源代码是一个验证程序,向候选人发送消息,让他们知道是否被接受。最后,它在网页上显示监控信息。处理是通过ChunkStepBatchProcessing.java Servlet 启动的。

以下代码是块批处理批件实现框架的示例:

public class InscriptionReader extends AbstractItemReader {
  @Override
  public Object readItem() throws Exception {
    //Read data and return the item
  }
}

public class InscriptionProcessor implements ItemProcessor{
  @Override
  public Object processItem(Object o) throws Exception {
    //Receive item from the reader, process and return the result
  }    
}

public class StudentNotifier extends AbstractItemWriter {
  @Override
  public void writeItems(List<Object> items) throws Exception {
    //Receive items from the processor then write it out
  }
}
public class InscriptionCheckpoint implements Serializable {
  private int lineNumber;

  public void incrementLineNumber(){
    lineNumber++;
  }

  public int getLineNumber() {
    return lineNumber;
  }        
}

批处理步骤

批处理步骤是实现您自己的批处理模式的一种步骤类型。与执行三个阶段(读取、处理和写入)的任务的块不同,批处理步骤只被调用一次,并在处理结束时返回一个退出状态。以下代码展示了批处理批件实现的样子。本节的源代码向所有学生发送信息消息,并显示有关批处理的一些重要信息。处理是通过BatchletStepBatchProcessing.java Servlet 启动的。

以下代码是批处理批件实现框架的示例:

public class StudentInformation extends AbstractBatchlet{

  @Override
  public String process() throws Exception {
    // process 
    return "COMPLETED";
  }    
}

批处理.xml 配置文件

batch.xml文件是一个包含批处理应用程序批处理批件的 XML 文件。它建立了批处理批件实现与在作业 XML 文件中使用的参考名称之间的对应关系。batch.xml文件必须存储在META-INF目录中,以便于便携式应用程序。以下代码给出了前面代码中显示的inscription-validator-Job作业的batch.xml文件内容。

以下代码是batch.xml的一个示例:

<batch-artifacts > 
  <ref id="InscriptionReader" 
  class="com.packt.ch02.batchprocessing.chunk.InscriptionReader" /> 
  <ref id="StudentNotifier" 
  class="com.packt.ch02.batchprocessing.chunk.StudentNotifier" /> 
  <ref id="InscriptionProcessor" 
  class="com.packt.ch02.batchprocessing.chunk.InscriptionProcessor" /> 
</batch-artifacts>

任务操作员

JobOperator实例可以通过BatchRuntime类的getJobOperator()方法访问。它提供了一组操作来管理(启动停止重启等)作业并访问JobRepositorygetJobNamesgetJobInstancesgetStepExecutions等)。以下代码展示了如何在没有特定属性的情况下启动前面显示的inscription-validator-Job作业。重要的是要注意,在JobOperator.start命令中指定的inscriptionJob值是作业 XML 文件的名称(而不是作业的 ID)。在 Servlet ChunkStepBatchProcessing中,您将看到如何检索状态以及如何从JobOperator实例监控批处理信息。

以下代码是启动作业的示例代码:

JobOperator jobOperator = BatchRuntime.getJobOperator();
if(jobOperator != null)
  jobOperator.start("inscriptionJob", null);

Java API for JSON Processing 1.0

Java API for JSON Processing 1.0 是在 JSR 353 下开发的。本节仅为您提供了 API 的概述。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr353/index.html下载。

什么是 JSON?

JavaScript 对象表示法JSON)是一种轻量级的数据交换文本格式。它基于 JavaScript 的一个子集,但它完全与语言无关。JSON 格式常用于客户端和服务器或 Web 服务之间的数据交换。但是,当您需要存储或传输相对较小的数据,这些数据可以轻松地表示为键值对组合时,它也可以使用。

JSON 是基于两种结构构建的,这两种结构是:一组键值对集合和有序值列表。这些结构由三种数据类型组成:对象数组

对象

对象是无序的 name:value 对集合,用花括号({})括起来。每个名称后面都有一个冒号(:),名称值对用逗号(,)分隔。名称是 字符串 类型,而值的类型可以是 字符串对象 等等。以下文本给出了一个 JSON 对象的示例,其中包含有关学生的某些信息:

{"name":"Malinda","gender":"F","birthday":"14/03/1976","weight":78.5}

数组

数组是有序值集合,值之间用逗号(,)分隔,并用方括号([])括起来。以下文本给出了一个 JSON 数组的示例,其中包含按字母顺序排列的学生及其分数列表。

[{"name":"Amanda","score"=12.9},{"name":"Paolo","score"=14},{"name":"Zambo","score"=12.3}]

JSON 的 可以是双引号中的 字符串布尔truefalse对象数组null

为什么选择 JSON?

XML可扩展标记语言Extensible Markup Language)在SGML标准化通用标记语言,它功能强大且可扩展,但复杂)和HTML超文本标记语言,SGML 的简单版本,专注于数据展示)之后发布,以克服这两种语言的不足。它的强大、灵活和简单使其在许多应用中受到青睐,如配置管理、存储、数据传输等。随着AJAX技术的出现,XML 在浏览器和 Web 服务器之间的交换中得到了广泛应用。但是,它也带来了一些限制:由于信息重复、加载和处理数据复杂,XML 文档在本质上较重,有时处理 XML 文档还依赖于浏览器。

为了解决这些问题,JSON 格式被开发出来作为 XML 的替代品。实际上,尽管 JSON 具有可移植性和灵活性,但它不支持命名空间,数据访问需要了解文档,并且到目前为止,还没有 XSDDTD 来验证文档的结构。

以下表格展示了 XML 和 JSON 数据展示之间的简单比较:

XML 数据展示 JSON 数据展示

|

<student>
  <id>854963</id>
  <name>LouisPoyer</name>
  <weight>78.6</weight>
  <gender>M</gender>
  <contact>
    <address>Rue9632</address>
    <phone>985-761-0</phone>
  </contact>   
</student>

|

{"student": {
  "id":"854963", 
  "name":"LouisPoyer", 
  "weight":78.6,
  "gender":"M",
  "contact":[
    {"address":"Rue632"},
    {"phone":"985-761-0"} ]
  }  
}

|

Java JSON 处理 API

Java JSON 处理 API 定义了一个 API,通过使用流式 API 或对象模型 API 来处理(解析、生成、转换和查询)JSON 文档。

流式 API

流式 API是针对 JSON 的,就像StAX API是针对 XML 的。换句话说,流式 API 是一种基于事件的 JSON 解析。它按顺序解析 JSON 文件,并在遇到流中的新标签时触发事件(新的值字符串、新的对象开始、对象结束、新的数组开始……)。下面的示例展示了如何获取上一页上展示的 JSON 数据中的联系信息。

使用流式 API 处理 JSON 的示例:

public String getStudentContact(String jsonData) {
  JsonParser parser = Json.createParser(new StringReader(jsonData));
  Event event = null;
  boolean found = false;
  String information = "";

  //Advance to the contact key
  while (parser.hasNext()) {
    event = parser.next();            
    if ((event == Event.KEY_NAME) && 
      "contact".equals(parser.getString())) {
        found = true;
        event = parser.next();              
        break;
      }
    }

    if (!found) {
      return "contact information does not exist";
    }

    //get contact information 
    while (event != Event.END_ARRAY) {         
    switch (event) {
      case KEY_NAME:
        information += parser.getString() + " = ";
        break;
      case START_ARRAY: break;
      case END_ARRAY: break;
      case VALUE_FALSE: break;
      case VALUE_NULL: break;
      case VALUE_NUMBER:
        if (parser.isIntegralNumber()) {
          information += parser.getLong()+", ";
        } else {
          information += parser.getBigDecimal()+", ";
        }
        break;
      case VALUE_STRING:
        information += parser.getString()+", ";
        break;
      case VALUE_TRUE:
        information += " TRUE, ";
        break;
    }
    event = parser.next();
  }
  return information;
}

流式 API 由五个接口、一个枚举类和两个异常组成。所有这些都在javax.json.stream包中。在这些接口中,我们有JsonParser接口,它包含用于逐步只读访问 JSON 数据的方法,以及JsonGenerator接口,它提供逐步生成(写入)JSON 的方法。这些接口的实例可以通过JsonParserFactoryJsonGeneratorFactory工厂分别创建。流式 API 触发的事件都包含在JsonParser.Event枚举类中。

建议使用流式 API 来解析大型 JSON 文件,因为与对象模型 API 不同,它不需要在处理之前加载整个文件。这确保了良好的内存管理。

对象模型 API

对象模型 API 是针对 JSON 的,就像 DOM API 是针对 XML 的。这意味着它在内存中将 JSON 文档表示为树结构,在提供导航或查询文档的可能性之前。此 API 通过提供对任何数据的随机访问,提供了解析 JSON 文档的最灵活方式。但作为交换,它需要更多的内存。这就是为什么它不适合大型文档。

对象模型 API 由十三个接口、一个类、一个枚举类和一个异常组成。所有这些都在javax.json包中。在接口中,我们有JsonArrayBuilderJsonObjectBuilder,分别用于从头开始构建 JSON 数组和 JSON 对象;JsonArray用于将 JSON 数组的有序值作为列表访问,JsonObject用于将 JSON 对象的值作为 Map 访问,以及JsonBuilderFactory用于创建JsonObjectBuilderJsonArrayBuilder实例;JsonReader用于从输入源读取 JSON,JsonReaderFactory用于创建JsonReader实例;JsonWriter用于将 JSON 写入输出源,以及JsonWriterFactory用于创建JsonWriter实例。以下代码演示了如何从头创建对象模型并访问其中的数据。

以下代码是使用对象模型 API 处理 JSON 的示例:

JsonObject objModel = Json.createObjectBuilder()
.add("student",Json.createObjectBuilder()
  .add("id", "854963")
  .add("name", "Louis Poyer")
  .add("weight", 78.6)
  .add("gender","M")
  .add("contact",Json.createArrayBuilder()
    .add(Json.createObjectBuilder()
    .add("address","Rue 632"))
    .add(Json.createObjectBuilder()
    .add("phone","985-761-0")))                        
).build();

JsonObject student = objModel.getJsonObject("student");
String name = student.getString("name");
JsonArray contact = student.getJsonArray("contact");
String address = contact.getJsonObject(0).getString("address");
String phone = contact.getJsonObject(1).getString("phone"));

Java API for WebSocket 1.0

Java API for WebSocket 1.0 是在 JSR 356 下开发的。本节仅为您提供一个 API 的概述。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr356/index.html下载。

什么是 WebSocket?

在 HTML5 规范的先前版本中最初被称为TCPConnectionWebSocket是一个建立在TCP传输控制协议)之上的独立协议,它使得客户端和服务器之间能够进行双向和全双工通信。

在 Web 应用程序中打开 WebSocket 连接时,Web 客户端使用 HTTP 请求请求服务器将连接升级为 WebSocket 连接。如果服务器支持并接受 WebSocket 协议连接请求,它将通过 HTTP 返回响应。从那一刻起,通信就建立了,双方可以通过仅使用 WebSocket 协议来发送和接收数据。

为什么选择 WebSocket?

现在,许多 Web 应用程序(即时通讯、交易平台、一些电子商务平台、在线游戏等)需要在客户端(浏览器)和服务器之间进行实时通信。如果您不知道,HTTP 协议是一个无状态的半双工协议。这意味着,为了访问新信息和更新网页,客户端必须始终打开到服务器的连接,发送请求,等待服务器响应,然后关闭连接。因此,在实时环境中,客户端将频繁地向服务器发送请求,以检测新数据的出现,并且即使没有新信息,也会进行许多请求-响应操作。

为了解决这个问题,已经提出了许多解决方案。其中最有效的方法无疑是长轮询,它可以这样描述:客户端向服务器发送请求;如果服务器有数据可用,则服务器响应。否则,它将等待直到有新信息出现后再响应。在收到响应后,客户端发送另一个请求,如此循环。尽管看起来不错,但这种技术需要专有解决方案(彗星),并且当数据频繁更新时,循环连接-请求-响应-断开连接可能会对网络产生负面影响。

WebSocket 不是基于 HTTP 的技术,它是一个提供了一种新的、更好的方式来克服 Web 客户端和 Web 服务器/服务之间实时通信中 HTTP 协议的不足的协议。

WebSocket API

Java API for WebSocket 1.0 定义了一个标准 API,用于在 Java EE 平台上构建 WebSocket 驱动的应用程序。

一个 WebSocket 应用程序由两种类型的组件组成,称为端点:客户端端点和服务器端点。客户端端点是发起 WebSocket 连接的组件,而服务器端点则是等待连接。使用 Java API for WebSocket 1.0,这两种组件类型可以通过使用注解以编程方式或声明方式创建。在本章中,我们只将在一个小型学生聊天室应用程序中看到注解端点。

服务器端点

以下代码演示了如何创建一个 WebSocket 端点,该端点能够接受客户端连接并发送消息:

@ServerEndpoint("/chatserver")
public class ChatServerEndPoint {    
  @OnOpen
  public void openConnection(Session session) throws Exception {
    //...
  }

  @OnMessage
  public void onMessage(Session session, String msg)throws Exception {
    //...
  }

  @OnClose
  public void closeConnection(Session session) throws Exception {
    //...
  }
}

@ServerEndpoint 注解定义了一个服务器类型端点以及它将被部署的路径。你也会注意到 API 提供了注解来描述端点生命周期中每个步骤要执行的方法。下表列出了 WebSocket 端点生命周期注解的列表及其作用。

下表列出了 WebSocket 端点生命周期注解:

注解 作用
@OnOpen 在连接打开时执行的方法
@OnMessage 在接收到消息时执行的方法
@OnError 在发生连接错误时执行的方法
@OnClose 在连接关闭时执行的方法

任何由 WebSocket 客户端发送的消息都会被onMessage()方法拦截,该方法接收客户端会话和消息作为参数(有关可以接受的其它参数,请参阅规范)。消息可以通过Session.getBasicRemote()方法同步发送,或者通过Session.getAsyncRemote()方法异步发送。这些方法中的每一个都用于发送类型为:textbinaryobjectpingpong帧的消息。以下代码演示了如何向所有已连接客户端发送文本消息:

static Set<Session> users = Collections.synchronizedSet(new HashSet());

  @OnOpen
  public void openConnection(Session session) throws Exception {
    users.add(session);        
  }

  @OnMessage
  public void onMessage(Session session, String msg)throws Exception {
    for (Session s : users) {
      s.getBasicRemote().sendText(msg);
    }
  }

会话对象包含一个变量用于存储一些用户特定的信息。下面的代码演示了如何通过每次发送消息的人的名字与许多客户进行通信:

//...
static Set<String> usersId = Collections.synchronizedSet(new HashSet());
//...

@OnMessage
  public void onMessage(Session session, String msg)throws Exception {
    if (msg.startsWith("ID")) {//if it is a connection message
      String id = msg.split("-")[1];
      session.getUserProperties().put("id", id);
      //save the ID of the user
      usersId.add(id);
      //add the ID in the list of connected users
      Object[] obj1 = new Object[]{"wel","Welcome to the chat room "+id +"!"};
      String jsonString = getJsonObject(obj1);
      //json message transformation
      //send a welcome message to the new user
      session.getBasicRemote().sendText(jsonString);
      //send the list of connected users to all users
      Object[] obj2 = new Object[]{"users",usersId};
      jsonString = getJsonObject(obj2);
      for (Session s : users) {
        s.getBasicRemote().sendText(jsonString);
      }
    } else { //if it is a message to the chat room
      //get ID of the user who sends message
      String id = (String) session.getUserProperties().get("id");
      Object[] obj = new Object[]{"msg",id + ">>" + msg.split("-")[1]};
      String jsonString = getJsonObject(obj);//json transformation
      //sends the message to all connected users
      for (Session s : users) {
        s.getBasicRemote().sendText(jsonString);
      }
    }
  }

客户端端点

我们的客户端 WebSocket 端点是基于 JavaScript 代码的.jsp网页(websocketChatClient.jsp)。正如你所见,客户端具有相同的生命周期方法,通过 JSON 的力量,我们可以轻松访问并显示服务器发送的消息。

以下代码是一个 Web 客户端 WebSocket 端点的示例:

//complete URI of the chat server endpoint
var clientUri = "ws://"+document.location.host+"/chapter02NewSpecifications/chatserver";
var wsocket;

//connection request when loading the web page
window.addEventListener("load", connect, false);

//Connection method
function connect() {
  wsocket = new WebSocket(clientUri);
  //binding of the websocket lifecycle methods
  wsocket.onmessage = onMessage;
  wsocket.onerror = onError;
}

function joinChatRoom() {//method to join the chat room
    wsocket.send("ID-" + txtMessage.value);
}

function sendMessage() {//method to send a message to the chat room
    wsocket.send("M-" + txtMessage.value);
}

function onMessage(event) {//method to perform incoming messages
  var parsedJSON = eval('(' + event.data + ')');
  if (parsedJSON.wel != null) {//if welcome message
    userState.innerHTML = parsedJSON.wel;
  }
  if (parsedJSON.msg != null) {//if chat room message
    userMessage.innerHTML += "\n"+parsedJSON.msg;
  }
  if (parsedJSON.users.length > 0) {//if new new connection user
    userId.innerHTML = "";
    for (i = 0; i < parsedJSON.users.length; i++) {
      userId.innerHTML += i + "-" + parsedJSON.users[i] + "\n";
    }
  }
}

摘要

在本章中,我们试图展示 Java EE 7 新规范的有用性和实现。在接下来的章节中,我们将分析继承自 Java EE 6 的规范所进行的改进,并利用这个机会展示如何将新规范与现有规范集成。

第三章 表示层

在本章中,我们将回顾 Java EE 平台在表示层方面的改进。具体来说,我们将讨论以下规范:

  • Servlet 3.1

  • 表达式语言 3.0

  • JavaServer Faces 2.2

Servlet 3.1

Servlet 3.1 规范是在 JSR 340 下开发的。本节仅为您概述 API 的改进。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr340/index.html下载。

什么是 Servlet?

在计算机科学的历史上,曾经有一段时间我们无法创建动态网页。那时,用户只能访问静态网页,例如报纸上的网页。在众多解决方案中,第一个 Java 解决方案是Servlet,这是一种革命性的技术,用于扩展基于请求-响应编程模型的 Web 服务器功能。它使 Web 服务器能够处理 HTTP 请求并根据用户参数动态生成网页。从那时起,技术已经取得了很大的进步,以促进 Web 应用程序的开发。然而,Servlet 技术仍然是处理 HTTP 请求/响应的 Java 解决方案中最广泛使用的。

话虽如此,在几乎所有针对 HTTP 协议的 Java 框架(如 JSF、Struts、Spring MVC、BIRT、Web 服务解决方案)的底层,你至少会找到一个 Servlet(在 JSF 中是FacesServlet,在 BIRT 中是ViewerServletBirtEngineServlet)。你明白为什么这项技术应该引起我们的注意,因为 Servlet 规范的任何变化都会对众多工具产生影响。

带有 Servlet 的登录页面

具体来说,Servlet 是一个直接或间接实现 Servlet 接口的 Java 类。以下代码代表了一个 Servlet 的示例,该 Servlet 向用户返回一个连接接口,并在验证其输入后将其重定向到另一个接口:

@WebServlet(name = "connexionServlet", urlPatterns = {"/connexionServlet"})
public class ConnexionServlet extends HttpServlet {

    Logger logger = Logger.getLogger(ConnexionServlet.class.getName());

    protected void processRequest(HttpServletRequest request, HttpServletResponse response) {
        response.setContentType("text/html;charset=UTF-8");       
        try (PrintWriter out = response.getWriter();){
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Online pre-registration site</title>");            
            out.println("</head>");
            out.println("<body>");
            out.write("        <form method=\"post\">");
            out.write("            <h4>Your name</h4>");
            out.write("            <input type=\"text\" name=\"param1\" />");
            out.write("            <h4>Your password</h4>");
            out.write("            <input type=\"password\" name=\"param2\" />");
            out.write("            <br/> <br/> <br/>");
            out.write("            <input type=\"submit\"  value=\"Sign it\"/>");
            out.write("            <input type=\"reset\" value=\"Reset\" />");
            out.write("        </form>");
            out.println("</body>");
            out.println("</html>");

            String name = request.getParameter("param1");
            String password = request.getParameter("param2");

            String location = request.getContextPath();

            if("arnoldp".equals(name) && "123456".equals(password)){
                response.sendRedirect(location+"/WelcomeServlet?name="+name);
            }else if((name != null) && (password != null))
                 response.sendRedirect(location+"/ConnexionFailureServlet"); 

        } catch(IOException ex){
            logger.log(Level.SEVERE, null, ex);
        }   
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }
}

如你所见,我们的ConnexionServlet类扩展了javax.servlet.http. HttpServlet;这是一个实现了Servlet接口的抽象类。它定义了Servlet对象的生命周期方法(doGetdoPost),这使我们能够处理 HTTP 服务请求并返回响应。要访问由这个 Servlet 生成的页面,你必须输入一个类似于以下的 URL:http://localhost:8080/chapter03PresentationLayer/connexionServlet。在这里,connexionServlet是在@WebServlet注解中给出的名称。在这个页面上,你会看到使用以下指令显示的登录按钮:`

out.write("            <input type=\"submit\"  value=\"Sign it\"/>");

点击此按钮会生成一个 HTTP 请求,该请求将执行processRequest(HttpServletRequest request, HttpServletResponse response)方法。根据connexion参数验证的结果,您将被重定向到错误页面或主页。在重定向到主页的情况下,我们将在 URL 中添加一个包含用户名的参数,以便适应问候语。主页的 URL 如下:

http://localhost:8080/chapter03PresentationLayer/WelcomeServlet?name=arnoldp

WelcomeServlet Servlet 中,为了访问name参数,我们执行以下指令:out.println("<h1>Welcome Mr " + request.getParameter("name")+ "</h1>");

Servlet 3.1 的最新改进实例

随着关注于开发便捷性、可插拔性、异步处理和安全增强的 Servlet 3.0 的推出,Servlet 3.1 对上一版本的功能进行了一些澄清和修改;主要的变化包括:非阻塞 I/O API 和协议升级处理。

非阻塞 I/O API

非阻塞 I/O API 依赖于异步请求处理和升级处理来提高 Web 容器的可伸缩性。实际上,Servlet 3.0 中异步处理的引入使得通过启用处理客户端请求的线程并委托其他线程执行重过程,以便准备好接受新的请求,从而减少了请求之间的等待时间。但是,由于传统的使用while循环收集数据输入/输出的方式(见以下代码),负责请求处理的主线程可能会因为待处理的数据而被阻塞。例如,当您通过网络向一个非常强大的服务器发送大量数据时,数据收集所需的时间将与网络的带宽成反比。带宽越小,服务器完成工作所需的时间越长。

public class TraditionnalIOProcessing extends HttpServlet {

    Logger logger = Logger.getLogger(TraditionnalIOProcessing.class.getName());

    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        try (ServletInputStream input = request.getInputStream();
                FileOutputStream outputStream = new FileOutputStream("MyFile");) {

            byte b[] = new byte[3072];
            int data = input.read(b);

            while (data != -1) {
                outputStream.write(b);
                data = input.read(b);
            }
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }
}

为了解决这个问题,Java EE 平台增加了两个监听器(ReadListenerWriteListener),并在ServletInputStreamServletOutputStream中引入了新的 API。

以下表格描述了非阻塞 I/O API 的新监听器:

Listener 回调函数 描述
ReadListener void onDataAvailable() 当可以无阻塞地读取数据时调用此方法
void onAllDataRead() ServletRequest的所有数据都被读取时调用此方法
void onError(Throwable t) 当请求处理过程中发生错误或异常时调用此方法
WriteListener void onWritePossible() 当可以无阻塞地写入数据时调用此方法
void onError(Throwable t) 在响应处理过程中发生错误或异常时调用此方法

下表描述了非阻塞 I/O API 的新 API:

方法 描述
ServletInputStream void setReadListener(Readlistener ln) 此方法将Readlistener与当前的ServletInputStream关联
boolean isFinished() ServletInputStream的所有数据都已读取时返回true
boolean isReady() 如果可以无阻塞地读取数据,则返回true
ServletOutputStream boolean isReady() 如果可以成功写入ServletOutputStream,则返回true
void setWriteListener(WriteListener ln) 此方法将WriteListener与当前的ServletOutputStream关联

通过使用非阻塞 I/O API,前面显示的TraditionnalIOProcessing类的doGet(HttpServletRequest request, HttpServletResponse response)方法可以转换为以下代码表示的doGet(HttpServletRequest request, HttpServletResponse response)方法。如您所见,数据接收已被委托给一个监听器(ReadListenerImpl),每当有新数据包可用时,它都会被通知。这防止了服务器在等待新数据包时被阻塞。

protected void doGet(HttpServletRequest request, HttpServletResponse response) {
    try (ServletInputStream input = request.getInputStream();
            FileOutputStream outputStream = new FileOutputStream("MyFile");) {
       AsyncContext context = request.startAsync();
       input.setReadListener(new ReadListenerImpl(context, input,outputStream));
    }catch (IOException ex) {
       logger.log(Level.SEVERE, null, ex);
    }
}

前面代码片段中使用的ReadListenerImpl实现如下:

public class ReadListenerImpl implements ReadListener {

    AsyncContext context;
    ServletInputStream input;
    FileOutputStream outputStream;

    public ReadListenerImpl(AsyncContext c, ServletInputStream i, FileOutputStream f) {
        this.context = c;
        this.input = i;
        outputStream = f;
    }

    @Override
    public void onDataAvailable() throws IOException {
        byte b[] = new byte[3072];
        int data = input.read(b);
        while (input.isReady() && data != -1) {
            outputStream.write(b);
            data = input.read(b);
        }
    }

    @Override
    public void onAllDataRead() throws IOException {
        System.out.println("onAllDataRead");
    }

    @Override
    public void onError(Throwable t) {
        System.out.println("onError : " + t.getMessage());
    }
}

协议升级处理

协议升级处理是 HTTP 1.1 中引入的一种机制,旨在提供从 HTTP 协议切换到另一个(完全不同的)协议的可能性。协议升级处理使用的具体示例是从 HTTP 协议迁移到 WebSocket 协议,客户端首先向服务器发送 WebSocket 请求。客户端请求通过 HTTP 发送,如果服务器接受连接请求,它将通过 HTTP 进行响应。从这一刻起,所有其他通信将通过建立的 WebSocket 通道进行。Servlet 3.1 规范中对该机制的支持是通过向HttpServletRequest添加upgrade方法以及两个新接口:javax.servlet.http.HttpUpgradeHandlerjavax.servlet.http.WebConnection来实现的。

下表显示了协议升级方法、接口和类的描述:

类/接口 方法 描述
HttpServletRequest HttpUpgradeHandler upgrade(Class handler) 此方法启动升级处理,实例化并返回实现HttpUpgradeHandler接口的处理程序类。
HttpUpgradeHandler void init(WebConnection wc) 当 Servlet 接受升级操作时调用此方法。它接受一个WebConnection对象,以便协议处理程序可以访问输入/输出流。
void destroy() 当客户端断开连接时调用此方法。
WebConnection ServletInputStream getInputStream() 此方法提供对连接输入流的访问。
ServletOutputStream getOutputStream() 此方法提供了对连接输出流的访问。

下面的两段代码展示了如何使用新方法和新接口来接受给定的客户端协议升级请求。

以下是一个升级请求的示例:

protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");        
    try (PrintWriter out = response.getWriter();){
            System.out.println("protocol : "+request.getHeader("Upgrade"));
        if ("CYPHER".equals(request.getHeader("Upgrade"))) {
            response.setStatus(101);
            response.setHeader("Upgrade", "CYPHER");
            response.setHeader("Connection", "Upgrade");
            CypherUpgradeHandler cHandler = request.upgrade(CypherUpgradeHandler.class);                
        } else {
            out.println("The "+request.getHeader("Upgrade")+" protocol is not supported");
        }
    } 
}

以下是一个升级处理类实现的示例:

public class CypherUpgradeHandler implements HttpUpgradeHandler{

    Logger logger = Logger.getLogger(CypherUpgradeHandler.class.getName());
    public void init(WebConnection wc) {
        ServletInputStream input = null;
        ServletOutputStream output = null;
        try {
            System.out.println("A client just logged in");
            input = wc.getInputStream();
            // use input stream
            output = wc.getOutputStream();
            //use output stream
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
        }        
    }

    public void destroy() {
         System.out.println("A client just logged out");
    }    
}

表达式语言 3.0

表达式语言 3.0 规范是在 JSR 341 下开发的。本节仅为您提供了 API 改进的概述。完整的文档规范(更多信息)可以从 jcp.org/aboutJava/communityprocess/final/jsr341/index.html 下载。

什么是表达式语言?

表达式语言EL)是一种用于访问和操作 JSP 或 JSF 网页中数据的语言。它提供了一种简单的方法:

  • 从/向 JavaBean 组件属性读写数据

  • 调用静态和公共方法

  • 执行算术、关系、逻辑和条件操作

一个 EL 表达式看起来像 ${expr}#{expr}。前者语法通常用于即时评估,而后者用于延迟评估。以下代码演示了如何从 JSF 页面访问 JSF 实例属性,以及如何使用 EL 表达式在两个整数之间执行操作:

<h:form>             
   <h:outputText 
     id="beanProperty" 
     value="Bean property value : #{studentBean.identity}" />
  <br/>
  <h:outputText 
       id="operator" 
       value="operator : 3 + 12 = #{3 + 12}"  />
</h:form>

EL 3.0 的最新改进实例

EL 最初是为 JSP 标准标签库JSTL)设计的,后来与 JSP 规范相关联,然后是 JSF 规范。由于这两个规范在开始时有不同的需求,每个规范都使用了 EL 的一个变体。JSP 2.1 EL 的出现导致了 JSP 和 JSF 页面中使用的 EL 的统一;这催生了一个专门的 EL 规范文档,尽管 EL 总是依赖于与 JSP 相同的 JSR。3.0 版本是在一个独立的 JSR 下开发的:JSR 341。这个新规范带来了许多变化;其中最重要的是:独立环境的 API、lambda 表达式、集合对象支持、字符串连接运算符、赋值运算符、分号运算符以及静态字段和方法。

独立环境的 API

自 EL 3.0 以来,现在可以在独立环境中处理 EL。为此,它提供了 ELProcessor 类,该类允许直接评估 EL 表达式,并简化了函数、变量和本地存储库实体的定义。以下代码演示了如何在独立环境中使用 ELProcessor 类。当前案例是 Servlet 的内容,但您也可以在 Java SE 应用程序中做同样的事情。

ELProcessor el = new ELProcessor();

//Simple EL evaluation
 out.println("<h1>'Welcome to the site!' : "
             + "" + el.eval("'Welcome to the site!'") + "</h1>");
//Definition of local repository bean
el.defineBean("student", new StudentBean());
//Direct evaluation of EL expression 
out.println("<h1>" + el.eval("'The id of : '+=student.lastName+=' "
             + "is : '+=student.identity") + "</h1>");
//Function definition
el.defineFunction("doub", "hex", "java.lang.Double","toHexString");
//Access to a function defined
out.println("<h1> The hexadecimal of 29 is : "
              + el.eval("doub:hex(29)") + "</h1>");

总是在独立环境的 API 上下文中,EL 3.0 添加了 ELManager 类来提供低级 API,这些 API 可以用于管理 EL 解析和评估环境。使用这个类,您可以导入类或向 ELProcessor**** 添加自己的解析器。

Lambda 表达式

Lambda 表达式是一个匿名函数,它由括号中的一个或多个参数(如果有多个)、Lambda 操作符(->)和 Lambda 表达式的主体组成。表达式:x-> x * x 是一个用于确定数字平方的 Lambda 表达式。基本上,Lambda 表达式可以让你不必为单个方法创建整个类,或者为仅使用一次的非常简单的操作声明方法。因此,它们可以帮助编写更易于阅读和维护的代码。

Lambda 表达式可以有多种形式,如下所示:

  • 它可能涉及多个参数,并且可以立即调用。表达式:((x,y,z)->x+y*z)(3,2,4) 返回 11。

  • 它可以与一个标识符相关联,并在以后调用。表达式:diff = (x,y)-> x-y; diff(10,3) 返回 7。

  • 它可以作为方法的一个参数传递,或者嵌套在另一个 Lambda 表达式中。表达式:diff=(x,y)->(x-y);diff(10,[ 2,6,4,5].stream().filter(s->s < 4).max().get()) 返回 8。

集合对象支持

EL 3.0 规范中对集合对象的支持是通过两种方式实现的:集合对象的构建和实现用于操作它们的操作。

集合对象构建

关于集合的创建,EL 允许我们通过表达式或字面量动态地创建 java.lang.util.Setjava.lang.util.Listjava.lang.util.Map 类型的对象。

对象构建的不同类型如下:

  • Set 对象构建:

    Set 集合类型的构建会产生一个 Set<Object> 的实例,并且按照以下语法进行:

    SetCollectionObject = '{'elements '}'
    

    这里,elements 的形式为 (expression (',' expression)* )?

    例如:{1, 2, 3, 4, 5}, {'one','two','three','four'}, {1.3, 2, 3,{4.9, 5.1}}

  • List 对象构建:

    List 集合类型的构建会产生一个 List<Object> 的实例,并且按照以下语法进行:

    ListCollectionObject = '['elements']'
    

    这里,elements 的形式为 (expression (',' expression)* )?

    例如:[one, 'two', ['three', 'four'],five], [1, 2, 3, [4,5]]

  • Map 对象构建:

    Map 对象类型的构建会产生一个 Map<Object> 的实例,并且按照以下语法进行:

    MapCollectionObject = '{' MapElements '}'
    

    在这里,MapElements 的形式为 (MapElement (',' MapElement)* )?,而 MapElement 的形式为 expression ':' expression

    例如:{1:'one', 2:'two', 3:'three', 4:'four'}

集合操作

EL 3.0 中集合支持的第二个方面是集合操作。对于这个方面,规范仅定义了要使用ELResolvers实现的集合操作的标准集合的语法和行为。它具有允许开发者通过提供自己的ELResolvers来修改默认行为的优点。

集合操作的执行是通过一个由以下组成的流管道完成的:

  • 一个表示管道源的stream对象;它从集合或数组的stream()方法中获取。在映射的情况下,映射的集合视图可以用作源。

  • 零个或多个中间stream方法,这些方法返回一个stream对象。

  • 一个终端操作,它是一个返回无值的stream方法。

以下代码示例演示了通过给出集合操作的示例来构建管道的结构:

public class ELTestMain {
    static ELProcessor el = new ELProcessor();

    public static void main(String[] args) {
        List l = new ArrayList();
        l.add(1); l.add(8); l.add(7); l.add(14); l.add(2);
        el.defineBean("list", l);

        out.println("Evaluation of " + l + " is : " + el.eval("list"));
        out.println("The ordering of: " + l + " is : " 
                 + el.eval("list.stream().sorted().toList()"));
        out.println("List of number < 7 : " 
                 + el.eval("list.stream().filter(s->s < 7).toList()"));
        out.println("The sum of : " + l + " is : " 
                + el.eval("list.stream().sum()"));
    }
}

字符串连接运算符(+=

+=运算符返回运算符两侧操作数的连接。例如,1 += 2返回 12,而1 + 2返回 3。为了欢迎一位新连接的学生到我们的网站,我们只需要在网页中找到以下表达式:

#{'Welcome' += studentBean.lastName}.

赋值运算符(=

A = B表达式将B的值赋给A。为了使这一点成为可能,A必须是一个可写属性。赋值运算符(=)可以用来更改属性值。例如,#{studentBean.identity = '96312547'}表达式将值96312547赋给属性studentBean.identity

注意

赋值运算符返回一个值,它是右结合的。表达式a = b = 8 * 3a = (b = 8 * 3)相同。

分号运算符(;

分号运算符可以像 C 或 C++中的逗号运算符一样使用。当两个表达式 exp1 和 exp2 由分号运算符分隔时,第一个表达式在第二个表达式之前被评估,并且返回的是第二个表达式的结果。第一个表达式可能是一个中间操作,例如增量,其结果将用于最后一个表达式。

表达式:a = 6+1; a*2返回 14。

静态字段和方法

使用 EL 3.0,现在可以通过使用语法MyClass.fieldMyClass.method直接访问 Java 类的静态字段和方法,其中MyClass是包含静态变量或方法的类的名称。下面的代码演示了如何访问Integer类的MIN_VALUE字段,以及如何使用Integer类的静态parseInt方法将字符串'2'解析为int

ELProcessor el = new ELProcessor();
//static variable access
out.println("<h1> The value of Integer.MIN_VALUE : " 
                 + el.eval("Integer.MIN_VALUE") + "</h1>");
//static method access
out.println("<h1> The value of Integer.parseInt('2') : " 
                + el.eval("Integer.parseInt('2')") + "</h1>");

JavaServer Faces 2.2

JavaServer Faces 2.2 规范是在 JSR 344 下开发的。本节仅为您提供了 API 改进的概述。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr344/index.html下载。

什么是 JavaServer Faces?

JavaServer FacesJSF)是一个基于组件的架构,提供了一套标准的 UI 小部件和辅助标签(convertDateTimeinputTextbuttonstableconverterinputFileinputSecretselectOneRadio)。它是在 Servlet 和 JSP 规范之后发布的,旨在促进面向组件的 Web 应用的开发和维护。在这方面,它为开发者提供了以下能力:

  • 创建符合 MVC(模型-视图-控制器)设计模式的 Web 应用。这种设计模式允许将表示层与其他层清晰分离,并便于整个应用维护。

  • 创建不同类型的组件(小部件、验证器等)。

  • 根据需要重用和自定义规范提供的多个组件。

  • 使用表达式语言EL)将 Java 组件绑定到不同的视图,并通过 EL 轻松地操作它们。

  • 通过渲染工具生成不同格式的网页(HTML、WML 等)。

  • 拦截表单上发生的各种事件,并根据请求作用域管理 Java 组件的生命周期。

为了实现这一点,JSF 应用的生命周期包括六个阶段(恢复视图阶段、应用请求值、处理验证、更新模型值、调用应用和渲染响应),每个阶段在处理表单时管理一个特定的方面,而不是像 Servlet 那样仅仅管理请求/响应。

使用 JSF 的标识页面

以下代码展示了如何使用 JSF 页面输入个人信息,例如姓名和国籍。它还包含选择列表和复选框等组件。正如您所看到的,制作一个好的工作并不需要您是一个极客。为了在参数验证后管理导航,我们使用commandButton组件的action属性,该属性期望从onclickValidateListener方法返回一个值。接下来的网页将根据返回的值显示,并在 Web 应用的faces-config.xml文件中定义。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 

      >
    <h:head>
        <title>Online pre-registration site</title>
    </h:head>
    <h:body>
        <f:view>            
            <h:form >                
                <dir align="center" >
                    <h:panelGrid columns="2" style="border: solid blue">
                        <h:outputText value="First name  : " />
                        <h:inputText value="#{studentBean.firstName}" />
                        <h:outputText value="Last name : " />
                        <h:inputSecret value="#{studentBean.lastName}" />
                        <h:outputText value="Birth date: " />
                        <h:inputSecret value="#{studentBean.birthDate}" />
                        <h:outputText value="Birth place : " />
                        <h:inputSecret value="#{studentBean.birthPlace}" />
                        <h:outputText value="Nationality : " />
                        <h:selectOneMenu value="#{studentBean.nationality}">
                            <f:selectItems value="#{studentBean.nationalities}" />
                        </h:selectOneMenu>
                        <h:outputText value="Gender : " />
                        <h:selectOneRadio value="#{studentBean.gender}">
                            <f:selectItem itemValue="M" itemLabel="Male" />
                            <f:selectItem itemValue="F" itemLabel="Female" />                           
                        </h:selectOneRadio>
                        <h:outputText value="Language : " />
                        <h:selectOneMenu value="#{studentBean.language}">
                            <f:selectItems value="#{studentBean.languages}" />
                        </h:selectOneMenu>
                        <dir align="right">
                            <h:panelGroup>
                                <h:commandButton value="Validate" 
                                                 action="#{studentBean.onclickValidateListener}" />
                                <h:commandButton value="Cancel" 
                                                 actionListener="#{studentBean.onclickCancelListener}"  />
                            </h:panelGroup> 
                        </dir>
                    </h:panelGrid>
                </dir>  
            </h:form>
        </f:view>
    </h:body>
</html>

JSF 2.2 的最新改进在行动中

由于 HTML5 带来的巨大改进,JSF 2.2 的一个重点是整合语言的新特性;但这并非唯一的大变化。除了整合 HTML5 之外,JSF 2.2 规范还带来了资源库合同,宣布了多模板功能、Faces Flow 和无状态视图。

HTML5 友好的标记

如我们之前所见,JSF 是一种基于组件的架构。这解释了为什么相对复杂用户界面特性的创建是通过开发 JavaServer Faces 组件来完成的。这些组件在将正确的内容发送到浏览器之前,在服务器端进行处理。尽管这种方法使开发者免于处理每个组件中涉及的 HTML、脚本和其他资源的复杂性,但要知道,组件的创建并不总是容易的,生成的代码也不总是最轻量或最优化。

HTML5 的出现通过引入新特性、新元素和新属性,极大地简化了 Web 应用程序的开发。为了避免 JSF 组件开发者重复造轮子,JSF 2.2 通过两个主要概念集成了对标记的支持:透传属性和透传元素。

透传属性

在生成将发送到浏览器的网页过程中,每个 JSF 组件的属性由 UIComponent 或 Renderer 进行解释和验证。与将 HTML5 属性添加到所有 JSF 组件中以便它们可以被 UIComponent 或 Renderer 验证不同,透传属性使开发者能够列出将直接传递到浏览器而不会被 UIComponent 或 Renderer 解释的一组属性。这可以通过三种不同的方法实现:

  • 通过引入命名空间 ;这将用于作为前缀,将所有必须无解释复制到浏览器网页中的组件属性(参见以下代码中的 `Pass through attributes 1`)

  • 通过在 `UIComponent` 标签内嵌套 `<f:passThroughAttribute>` 标签来为单个属性(参见以下代码中的 `Pass through attributes 2`)

  • java` By nesting the `<f:passThroughAttributes>` tag within a `UIComponent` tag for an EL value that is evaluated to `Map<String, Object>` (see `Pass through attributes 3` in the code that follows) <html ... > <h:form> <h:inputText pta:type="image" pta:src="img/img_submit.gif" value="image1" pta:width="58" pta:height="58" /> <h:inputText value="image2" > <f:passThroughAttribute name="type" value="image" /> <f:passThroughAttribute name="src" value="img_submit.gif" /> <f:passThroughAttribute name="width" value="68" /> <f:passThroughAttribute name="height" value="68" /> </h:inputText> <h:inputText value="image3" > <f:passThroughAttributes value="#{html5Bean.mapOfParameters}" /> </h:inputText> </h:form> ```java ````

透传元素

与允许你将 HTML 属性传递给浏览器而不进行解释的透传属性不同,透传元素允许你将 HTML 标签用作 JSF 组件。这为你提供了丰富 HTML 标签以包含 JSF 功能和利用 JSF 组件生命周期的机会。为了实现这一点,框架将在开发者为浏览器渲染的 HTML 标记和用于服务器端处理的等效 JSF 组件之间建立对应关系。

要在给定的 HTML 标签中使用透传元素,你必须至少将其中一个属性的前缀设置为分配给`http://xmlns.jcp.org/jsf`命名空间的短名称。

以下代码片段显示了如何使用透传元素:

<!-- namespace -->
<html ...
      ">

<h:form>
    <!-- Pass through element -->
    <input type="submit" value="myButton" 
     pte:actionListener="#{html5Bean.submitListener}"/>
</h:form>

资源库合约

资源库合约提供了 JSF 机制,将模板应用于 Web 应用程序的不同部分。这个特性宣布了一个重大变化:能够通过按钮或管理控制台下载外观和感觉(主题),并将其应用于你的账户或网站,就像 Joomla!一样。

目前,资源库合约允许你在你的 Web 应用程序的`contracts`文件夹中分组你的各种模板的资源(模板文件、JavaScript 文件、样式表和图像)。为了提高应用程序的可维护性,每个模板的资源可以分组到一个名为`contract`的子文件夹中。以下代码演示了一个包含三个模板的 Web 应用程序,这些模板存储在三个不同的`contracts`中:`template1`、`template2`和`template3`。

src/main/webapp
    WEB-INF/
    contracts/
        template1/
            header.xhtml
            footer.xhtml
            style.css
            logo.png
            scripts.js
        template2/
            header.xhtml
            footer.xhtml
            style.css
            logo.png
            scripts.js
        Template3/
            header.xhtml
            footer.xhtml
            style.css
            logo.png
            scripts.js

    index.xhtml
    ...

除了在`contracts`文件夹中的部署之外,你的模板还可以打包到一个 JAR 文件中;在这种情况下,它们必须存储在 JAR 的`META-INF`/`contracts`文件夹中,该 JAR 将被部署到应用程序的`WEB-INF`/`lib`文件夹中。

一旦定义,模板必须在应用程序的`faces-config.xml`文件中使用`resource-library-contracts`元素进行引用。以下请求的配置意味着`template1`应用于 URL 符合模式`/templatepages/*`的页面。而对于其他页面,将应用`template2`。

<resource-library-contracts>
    <contract-mapping>
        <url-pattern>/templatepages/*</url-pattern>
        <contracts>template1</contracts>
    </contract-mapping>
    <contract-mapping>
        <url-pattern>*</url-pattern>
        <contracts>template2</contracts>
    </contract-mapping>
</resource-library-contracts>

以下代码片段显示了`template1`的头部看起来像什么。它只包含要在头部显示的图片。如果你想的话,可以添加文本、样式和颜色。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html 

      >
    <h:head>
        <title>Resource Library Contracts</title>
    </h:head>
    <h:body>
     <ui:insert name="header" >
       <img src="img/image.jpg" width="400" height="50" alt="Header image"/>                                               
     </ui:insert>
    </h:body>
</html>

以下代码演示了如何在网页中使用模板:

<f:view>
    <h:form>               
        <h:panelGrid border="1" columns="3" >        
            <f:facet name="header">
                <ui:composition template="/header.xhtml">

                </ui:composition>
            </f:facet>            
            <f:facet name="footer">
                <ui:composition template="/footer.xhtml">

                </ui:composition>
            </f:facet>
        </h:panelGrid> 
    </h:form>
</f:view> 

Faces Flow

Faces Flow 用于定义和执行跨越多个表单的过程。如果我们以在线注册为例,注册表单可以分散在多个页面中,每个页面代表一个步骤。在我们的案例中,我们有:接受条件、输入身份信息、联系信息、医疗信息、学校信息,最后是验证。要使用 JSF 的早期版本实现此类应用程序,需要使用会话作用域的 bean 并声明构成流程的页面之间的硬链接。这降低了流程在另一个应用程序中的可用性,并且不提供在多个窗口中打开相同流程的可能性。

一个流程由一个称为起始点的入口,一个称为返回节点的出口点以及零个或多个其他节点组成。一个节点可以是 JSF 页面(`ViewNode`),一个导航决策(`SwitchNode`),一个应用逻辑调用(`MethodCallNode`),对另一个流程的调用(`FlowCallNode`),或者返回到调用流程(`ReturnNode`)。

一个流程可以通过 XML 配置文件或编程方式配置。它可以打包在一个 JAR 文件或文件夹中。以下示例演示了如何使用 Faces Flow(我们的流程使用 XML 配置文件进行配置;对于程序配置,请参阅 Java EE 7 教程)实现一个在线预注册网站。

在将流程打包到文件夹中的情况下,默认遵循以下约定:

  • 流程的包文件夹与流程的名称相同

  • 流程的起始节点与流程的名称相同

  • 除了出口点外,假设流程的所有页面都在同一个文件夹中

  • 对于使用 XML 配置文件配置的流程,配置文件是一个名为`<name_of_flow>-flow.xml`的`faces-config`文件

根据我们刚刚提出的规则,显示的树状图中包含一个名为`inscriptionFlow`的流程,该流程有六个视图。此流程在`inscriptionFlow-flow.xml`中配置,其起始节点是`inscriptionFlow.xhtml`。

webapp
  WEB-INF
  inscriptionFlow
      inscriptionFlow-flow.xml  
  inscriptionFlow.xhtml
  inscriptionFlow1.xhtml
  inscriptionFlow2.xhtml
  inscriptionFlow3.xhtml
  inscriptionFlow4.xhtml
  inscriptionFlow5.xhtml
  ...
   index.xhtml 

``在配置文件中,我们必须定义流程的 ID 和出口点的 ID。以下代码显示了文件inscriptionFlow-flow.xml的内容:`

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"

    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

    <flow-definition id="inscriptionFlow">
        <flow-return id="inscriptionFlowExit">
            <from-outcome>#{inscriptionFlowBean.exitValue}</from-outcome>          
        </flow-return>        
    </flow-definition>
</faces-config> 

在不同视图之间进行导航可以通过将要激活下一个视图的标签的`action`属性来完成。在这个属性中,你需要在当前页面之后输入你想要前往的页面的名称。以下代码显示了`inscriptionFlow1`视图的内容。这个视图对应于个人信息的输入表单;它包含一个用于输入名称的字段,一个前往下一个视图(`inscriptionFlow2`)的按钮,以及一个返回上一个视图(`inscriptionFlow`)的按钮。

<!-- inscriptionFlow1 view -->
<f:view>
<h:form>
   <h1>Identification information</h1>
   <p>Name : <h:inputText id="name" 
           value="#{inscriptionFlowBean.name}" /></p>

  <p><h:commandButton value="Next" action="inscriptionFlow2" /></p>
  <p><h:commandButton value="Back" action="inscriptionFlow" /></p>
</h:form>
</f:view>

要结束一个流程,只需将配置文件中定义的退出点 ID(`inscriptionFlowExit`)传递给为此动作指定的标签的`action`属性。并且要在不同的视图之间保存数据,你必须使用一个 Flow-Scoped Managed Bean。以下代码显示了我们在注册流程中使用的`inscriptionFlowBean`管理 Bean 的框架:

@Named
@FlowScoped(value="inscriptionFlow")
public class InscriptionFlowBean {
    //...
}
If all settings have been made, you can call your inscriptionFlow  in the start page with a button as follows:
<h:commandButton id="start" value="Registration" 
                              action="inscriptionFlow">
   <f:attribute name="toFlowDocumentId" value=""/>
</h:commandButton>

无状态视图

JSF 2.2 不仅添加了新的小部件,还提高了内存使用效率。在规范版本 2.0 之前,每当视图有任何变化时,整个组件树都会被保存和恢复。这降低了系统性能并填满了内存。从版本 2.0 开始,规范引入了部分状态保存机制。该机制仅保存组件树创建后发生变化的州,从而减少了需要保存的数据量。同样,JSF 2.2 为我们提供了定义无状态视图的可能性。正如其名所示,视图组件的`UIComponent`状态数据将不会被保存。

要将一个简单视图转换为无状态视图,你只需将`f:view`标签的 transient 属性值指定为`true`(见以下代码)。

<h:head>
    <title>Facelet Title</title>
</h:head>
<h:body>
    <f:view transient="true">
        <h:form>
            Hello from Facelets
        </h:form>
    </f:view>
</h:body>

# 摘要 在本章中,我们讨论了 Java EE 7 中改进的数据展示相关规范。这些是:Servlet、表达式语言和 JSF 规范。每个展示都紧跟着对各种改进的分析以及一个小示例,以展示如何实现这些新功能。在下一章中,我们将讨论用于与数据库通信的 Java API,这将引导我们进入另一章,该章将重点介绍如何组合我们已看到的所有 API。

第四章 Java 持久化 API

本章讨论了与数据源通信的 API 的改进。尽管 Java 是面向对象的,但它被设计成可以以对象的形式处理关系模型中的数据,这可能会引起严重问题,因为这两个概念在理论上并不兼容。除了向您介绍对象关系映射的世界之外,本章还将向您展示如何透明地以事务方式操作(创建、删除、搜索或编辑)关系模型的数据。本章涵盖的主题包括:

  • Java 持久化 API 2.1

  • Java 事务 API 1.2

Java 持久化 API 2.1

Java 持久化 API 2.1 规范是在JSR-338下开发的。本节仅为您概述 API 的改进。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr338/index.html下载。

JPA(Java 持久化 API)

JPAJava 持久化 API)是一个旨在定义 ORM(对象关系映射)标准特性的 Java 规范。然而,JPA 不是一个产品,而是一组需要实现接口。最著名的实现如下:HibernateToplinkOpenJPAEclipseLink,这是参考实现。

简而言之,我们可以这样说,ORM(对象关系映射)是一个用于在对象模型和关系数据库之间建立对应关系的 API。它使您能够像处理对象一样处理数据库中的数据,而不必过多地担心物理模式。

JPA 实践

JPA 基于实体概念,以便实现对象关系映射。实体是一个简单的 Java 类(如POJO),带有@Entity注解(或 XML 等效),其名称默认与数据库中具有相同名称的表相关联。除了@Entity注解之外,实体类还必须至少有一个与@Id注解(或 XML 等效)指定的主键等效属性。对于实体的其他属性,提供者将它们中的每一个关联到表中具有相同名称的列,如下面的截图所示:

JPA 实践

指示将关联到一组实体的数据库的参数必须在应用程序的persistence.xml文件中的持久化单元中定义。

以下代码是 Java SE 应用程序持久化单元的示例:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 

  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence 
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="chapter04PU"transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>com.packt.ch04.entities.Student</class>
    <properties>
      <property name="javax.persistence.jdbc.url"value="jdbc:derby://localhost:1527/ONLINEREGISTRATION"/>
      <property name="javax.persistence.jdbc.password"value="userapp"/>
      <property name="javax.persistence.jdbc.driver"value="org.apache.derby.jdbc.ClientDriver"/>
      <property name="javax.persistence.jdbc.user"value="userapp"/>
    </properties>
  </persistence-unit>
</persistence>

关于实体的操作,JPA 通过EntityManager接口提供了一套创建、读取、更新和删除数据的方法(见下表)。

下表展示了操作实体的一些方法:

方法 描述
void persist(Object o) 这用于保存作为参数传递的实体。
T merge(T t) 这允许您将作为参数传递的实体与持久化上下文合并。它返回要合并的实体的管理版本。
void remove(Object o) 这允许您在数据库中删除作为参数传递的实体。
T find(Class<T> type, Object o) 这允许您使用其标识符搜索实体。
void detach(Object o) 这允许您将实体从持久化上下文中分离出来,以便更改不会被保存

以下代码演示了如何在 Java SE 应用程序中使用 JPA 进行保存、读取、更新和删除数据:

public static void main( String[] args ) {
  EntityManagerFactory emf =Persistence.createEntityManagerFactory("chapter04PU");
  EntityManager em = emf.createEntityManager();
  //create entity manager

  Student student = createStudent();

  em.getTransaction().begin();//begin transaction
  em.persist(student);//save the student
  em.getTransaction().commit(); // commit transaction
  Student std = em.find(Student.class, student.getId());
  //find student

  System.out.println("ID : "+std.getId()+",last name : "+std.getLastname());       
  em.getTransaction().begin();//begin transaction
  std.setLastname("NGANBEL");//Update student's last name
  em.getTransaction().commit(); // commit transaction

  std = em.find(Student.class, student.getId());//find student
  System.out.println("ID : "+std.getId()+",last name : "+std.getLastname()); 

  em.getTransaction().begin();//begin transaction
  em.remove(std);//remove student
  em.getTransaction().commit(); // commit transaction
}

JPA 2.1 的最新改进在行动中

自从它的上一个版本(JPA 2.0)以来,JPA 规范已经进行了许多增强。最重要的增强是在以下功能中:持久化上下文同步、实体、JPQLCriteria API数据定义语言DDL)生成。

持久化上下文同步

在 JPA 2.1 之前,容器管理的持久化上下文会自动加入当前事务,并且对持久化上下文所做的任何更新都会传播到底层资源管理器。根据新的规范,现在可以有一个不会自动注册到任何 JTA 事务中的持久化上下文。这可以通过简单地创建一个同步类型为SynchronizationType.UNSYNCHRONIZED的容器管理实体管理器来实现,如下面的代码所示。

创建和注册SynchronizationType.UNSYNCHRONIZED持久化上下文:

@Stateless
@LocalBean
public class MySessionBean {

  /* Creation of an entity manager for 
   * unsynchronized persistence context
  */
  @PersistenceContext(synchronization = SynchronizationType.UNSYNCHRONIZED)
  EntityManager em;

  public void useUnsynchronizedEntityManager(){
      //enlisting of an unsynchronized persistence context
      em.joinTransaction();
      //...
  }
}

在前面的代码中,您会注意到我们调用了EntityManager.joinTransaction()方法;这是有道理的,因为只有在调用EntityManager.joinTransaction()方法之后,类型为SynchronizationType.UNSYNCHRONIZED的持久化上下文才会被注册到 JTA 事务中,并且在提交或回滚之后,SynchronizationType.UNSYNCHRONIZED持久化上下文将与它注册的事务解除关联。您需要再次调用EntityManager.joinTransaction()方法来注册解除关联的持久化上下文。

实体

实体监听器是一个简单的 Java 类(不是一个实体),它允许您定义可以用于一个或多个实体生命周期事件的回调方法。JPA 2.1 规范向这些类添加了对CDI 注入的支持以及定义@PostConstruct@PreDestroy生命周期回调方法的能力。这些方法分别在依赖注入之后和实体监听器销毁之前被调用。以下代码展示了一个具有后构造和预销毁方法的实体监听器,其中包含EJB 注入。之后是展示如何将实体监听器关联到实体的代码。

public class LogEntityListener {
  @EJB
  BeanLoggerLocal beanLogger;

  @PrePersist
  public void prePersistCallback(Object entity){
    beanLogger.logInformation(entity);
  }

  @PostConstruct
  public void init(){
    System.out.println("Dependency injected inLogEntityListener");
  }

  @PreDestroy
  public void destroy(){
    System.out.println("LogEntityListener will be destroy");
  }
}

@Entity
@EntityListeners(LogEntityListener.class)
public class Student implements Serializable {
  //
}

新的注解

JPA 2.1 增加了一个注解(@Index),用于在从实体生成模式时在表上创建索引,以及一个注解(@ForeignKey)用于指定表的键。

@Index注解有一个强制参数(columnList),用于列出构成索引的列以及不同的排序顺序。它还有两个可选参数:name参数,允许你更改索引的默认名称,以及unique参数,用于设置索引是否为唯一。同时,@Index注解被添加为TableSecondaryTableCollectionTableJoinTableTableGenerator注解的一部分。

@ForeignKey可以用作JoinColumnJoinColumnsMapKeyJoinColumnMapKeyJoinColumnsPrimaryKeyJoinColumnPrimaryKeyJoinColumnsCollectionTableJoinTableSecondaryTableAssociationOverride注解的元素,用于定义或修改表上的外键约束。它接受三个参数:名称、约束的值和外键的定义。这三个参数都是可选的。

以下代码展示了具有外键和索引列的实体示例:

@Entity
@Table(indexes = @Index(columnList = "name ASC, id DESC"))
public class MyEntity implements Serializable {
  @Id
  private Long id;    
  private String name;
  @JoinColumn(foreignKey = @ForeignKey(name = "FK"))
  private Student student;

  //...    
}

实体图

当我们谈论实体图时,必须牢记涉及多个相关实体的数据结构。在 JPA 的早期版本中,实体数据的有效加载主要通过获取设置来管理。结果是,在编译应用程序(或在 XML 配置的情况下部署之前)之前,必须设置某些注解的获取属性,以请求实体属性被预先加载(当加载实体时)或延迟加载(当需要数据时)。通过实体图,你现在可以在运行时覆盖或更改获取设置。

实体图可以通过使用大量的NamedEntityGraphNamedEntityGraphsNamedSubgraphNamedAttributeNode注解静态定义,或者通过EntityGraphsubgraphAttributeNode接口动态定义。

静态或命名实体图

@NamedEntityGraph注解用于定义一个实体图,该图可以在执行查询或使用find()方法时在运行时使用。以下代码显示了定义具有一个字段students的命名实体图的示例。

@Entity
@NamedEntityGraph(name="includeThis",attributeNodes={@NamedAttributeNode("students")})
public class Department implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @Basic(optional = false)
  private String id;
  private String name;
  private Integer nbrlevel;
  private String phone;    
  @OneToMany(mappedBy = "depart",fetch = FetchType.LAZY)
  private List<Student> students;

  /*getter and setter*/
}

一旦定义,我们需要使用EntityManagergetEntityGraph()方法检索我们的命名实体图,以便在执行查找方法或查询时将其用作属性,或者作为查询的提示。执行以下代码后,你将注意到在第一次搜索中,students属性不会被加载,而在第二次搜索中它会被加载。

以下代码是使用命名实体图的示例:

EntityManager em = emf.createEntityManager();
//create entity manager
PersistenceUnitUtil pUtil = emf.getPersistenceUnitUtil();

Department depart = (Department) em.createQuery("Select e from Department e")
.getResultList().get(0);
System.out.println("students Was loaded ? "+pUtil.isLoaded(depart, "students"));

EntityGraph includeThis = em.getEntityGraph("includeThis");
depart = (Department) em.createQuery("Select e from Department e")
.setHint("javax.persistence.fetchgraph", includeThis)
.getResultList().get(0);
System.out.println("students Was loaded ? "+pUtil.isLoaded(depart,"students"));

动态实体图

实体图也可以在运行时定义。为此,我们必须使用实体管理器的createEntityGraph()方法,而不是像命名实体图那样使用getEntityGraph()。一旦定义,动态实体图就像命名实体图一样与find()方法或查询相关联,如下面的代码所示。

以下代码是使用动态实体图的示例:

EntityManager em = emf.createEntityManager();
//create entity manager
PersistenceUnitUtil pUtil = emf.getPersistenceUnitUtil();

Department depart = (Department) em.createQuery("Select e from Department e")
.getResultList().get(0);
System.out.println("students Was loaded ? " + pUtil.isLoaded(depart, "students"));

EntityGraph includeThis = em.createEntityGraph(Department.class);
includeThis.addAttributeNodes("students");

depart = (Department) em.createQuery("Select e from Department e")
.setHint("javax.persistence.fetchgraph", includeThis)
.getResultList().get(0);
System.out.println("students Was loaded ? " +pUtil.isLoaded(depart, "students"));

JPQL

JPQLJava Persistence Query Language)是一种面向对象的类似 SQL 的查询语言。它是平台无关的,允许你通过实体而不是操作数据库的物理结构来访问你的数据。以下代码演示了如何查询所有 ID 大于 123 的已注册学生。

以下代码是 JPQL 查询的示例:

String queryString = "SELECT a FROM Student a WHERE a.id > 123";
Query query = em.createQuery(queryString);
System.out.println("result : "+query.getResultList());

尽管 JPQL 功能强大且内容丰富,但它仍在持续获得重大改进。在 JPA 2.1 中,它除了其他增强之外,还集成了对存储过程的支持,增加了新的保留标识符,并支持在运行时创建命名查询。

存储过程支持

JPA 2.1 现在允许你执行存储过程。通过它提供的各种 API,你可以定义和执行命名存储过程或动态存储过程。

以下是一个在 MySQL 中创建存储过程的脚本示例:

DELIMITER $$
CREATE
  PROCEDURE `ONLINEREGISTRATION`.`getStudentsName`()    
  BEGIN
    SELECT ID,LASTNAME FROM STUDENT ORDER BY LASTNAME ASC;
  END$$
DELIMITER ;

以下代码演示了如何执行我们刚刚创建的getStudentsName存储过程:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("chapter04PUM");
EntityManager em = emf.createEntityManager();
//create entity manager
StoredProcedureQuery spQuery = em.createStoredProcedureQuery("getStudentsName",Student.class);
List<Student> results = spQuery.getResultList();
for(Student std : results)
  System.out.println(std.getLastname());

新的保留标识符

JQPL 引入了以下新关键字:

  • ON: 此关键字允许我们使用ON条件进行显式连接,就像 SQL 中的连接一样。在此之前,连接是通过两个实体之间的联系属性来完成的,这需要最少的配置。以下代码演示了ON的使用:

    String queryString = "SELECT a FROM Student a "+" JOIN Department b ON a.departId = b.id";
    Query query = em.createQuery(queryString);
    System.out.println("result : "+query.getResultList());
    
  • FUNCTION: 此关键字允许你在查询中调用除 JPQL(如SUBSTRINGLENGTHABSTRIM等)原本意图之外的功能。使用此关键字,你可以使用数据库函数或你自己定义的函数。以下查询通过使用derby 数据库month()方法来提取出生日期的月份,从而得到 7 月出生的学生名单:

    String queryString= "SELECT a FROM Student a "+" WHERE FUNCTION('MONTH',a.birthdate) = 7 ";      
    Query query = em.createQuery(queryString);
    System.out.println("result : "+query.getResultList());
    
  • TREAT: 此关键字允许你对实体进行向下转型以获得子类状态。它在FROMWHERE子句中使用。在以下代码中,实体Appuser继承自实体Person;使用TREAT关键字,我们可以对基实体(Person)中不包含的属性设置条件。

    //Entity downcasting
    String queryString = "SELECT  a FROM Person a "+" WHERE TYPE(a) = Appuser AND "+" TREAT(a AS Appuser).userLogin = 'adwiner'";
    Query query = em.createQuery(queryString);
    System.out.println("result : "+query.getResultList());
    

支持在运行时创建命名查询

在 JPA 2.1 之前,命名查询是在编译程序之前作为元数据静态定义的。通过添加到EntityManagerFactory接口的addNamedQuery方法,你现在可以在运行时创建命名查询,如下面的代码所示:

EntityManagerFactory emf =Persistence.createEntityManagerFactory("chapter04PU");        
EntityManager em = emf.createEntityManager();

Query query = em.createQuery("SELECT a FROM Student a");
emf.addNamedQuery("runtimeNamedQuery", query);

System.out.println("result :"+em.createNamedQuery("runtimeNamedQuery").getResultList());

评估 API

自从 JPA 2.0 版本以来,JPA 提供了两种定义实体查询的选项。第一种选项是 JPQL,它是一种基于 SQL 的查询语言。第二种选项是 Criteria API,其中查询主要是用 Java 对象构建的,如下面的代码所示:

EntityManagerFactory emf =Persistence.createEntityManagerFactory("chapter04PU");
EntityManager em = emf.createEntityManager();
//create entity manager
//criteria builder declaration
CriteriaBuilder cb = em.getCriteriaBuilder();
//declaration of the object that will be returned by the query
CriteriaQuery<Student> cq = cb.createQuery(Student.class);
//Declaration of the entity to which the request is made
Root<Student> student = cq.from(Student.class);
//Query construction
cq.select(student).where(cb.greaterThan(student.<String>get("id"), "123"));
TypedQuery<Student> tq = em.createQuery(cq);
//execution of the query
System.out.println("result : "+tq.getResultList());

//JPQL equivalent query
SELECT a FROM Student a WHERE a.id > 123

由于这两个解决方案的发展速度不同,Criteria API 的主要变化是支持批量更新/删除和新保留标识符。

批量更新/删除的支持

在 Criteria API 中,批量更新和删除分别使用javax.persistence.criteria.CriteriaUpdatejavax.persistence.criteria.CriteriaDelete接口构建。以下代码演示了如何仅通过一个 Criteria API 请求来更新大量信息:

//bulk update
CriteriaUpdate cUpdate = cb.createCriteriaUpdate(Student.class);
Root root = cUpdate.from(Student.class);
cUpdate.set(root.get("departId"), "GT").where(cb.equal(root.get("departId"), "GI"));          
Query q = em.createQuery(cUpdate);

em.getTransaction().begin();//begin transaction
int num = q.executeUpdate();
em.getTransaction().commit();//commit transaction
System.out.println("number of update : "+num);
//JPQL equivalent query
UPDATE Student a SET a.departId = 'GT' WHERE a.departId = 'GI'

新保留标识符的支持

就像 JPQL 一样,Criteria API 包含了进行向下转型的可能性,并使用ON条件定义连接。为此,在javax.persistence.criteria.CriteriaBuilder接口中添加了重载的treat()方法以实现向下转型,同时向javax.persistence.criteria包的一些接口(如JoinListJoinSetJoinMapJoinCollectionJoinFetch)添加了on()getOn()方法以实现带有ON条件的连接。以下查询与前面代码中显示的 JPQL 向下转型等效:

//Downcasting
CriteriaQuery<Person> cqp = cb.createQuery(Person.class);
Root<Person> person = cqp.from(Person.class);
cqp.select(person).where(cb.equal(person.type(),Appuser.class),cb.equal(cb.treat(person, Appuser.class).get("userLogin"),"adwiner"));
TypedQuery<Person> tqp = em.createQuery(cqp);
System.out.println("result : " + tqp.getResultList());

DDL 生成

自从 JPA 规范的前一个版本以来,可以创建或删除并创建表。然而,这个功能的支持并非必需,规范文档让我们明白使用这个功能可能会降低应用程序的可移植性。好吧,随着 JPA 2.1 的推出,DDL数据定义语言)生成不仅被标准化,而且还得到了扩展,现在成为必需。

在这种情况下,已添加了新的属性。例如,你有以下属性:

  • javax.persistence.schema-generation.database.action: 这定义了提供者应采取的操作(无、创建、删除并创建或删除)。

  • javax.persistence.schema-generation.create-source: 这定义了在 DDL 生成的情况下,提供者将使用的源(实体、特定脚本或两者兼具)。

  • javax.persistence.schema-generation.drop-source: 这定义了在删除表的情况下,提供者将使用的源(实体、特定脚本或两者兼具)。

  • javax.persistence.schema-generation.connection: 这定义了用于 DDL 模式生成的JDBC连接参数,以便考虑某些数据库(如 Oracle)中权限的管理。这个参数是为 Java EE 环境考虑的。

以下持久化单元提供了一个在创建 EntityManagerFactory 时生成表的配置示例。此生成将基于实体信息(元数据)进行,并且只有在要创建的表不存在时才会发生,因为我们为提供者的操作定义了创建而不是先删除再创建。

<persistence-unit name="chapter04PUM" transaction-type="RESOURCE_LOCAL">
  <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
  <class>com.packt.ch04.entities.Department</class>
  <class>com.packt.ch04.entities.Person</class>
  <class>com.packt.ch04.entities.Student</class>
  <properties>
    <property name="javax.persistence.jdbc.url"value="jdbc:mysql://localhost:3306/onlineregistration"/>
    <property name="javax.persistence.jdbc.password"value="onlineapp"/>
    <property name="javax.persistence.jdbc.driver"value="com.mysql.jdbc.Driver"/>
    <property name="javax.persistence.jdbc.user" value="root"/>
    <property name="javax.persistence.schema-generation.database.action" value="create"/>
    <property name="javax.persistence.schema-generation.create-source" value="metadata"/>      
  </properties>
</persistence-unit>

标准化的另一个方面是添加了一个新方法(Persistence.generateSchema()),这提供了更多的生成机会。在此之前(在 JPA 2.0 中),DDL 生成是在实体管理器创建时进行的。从现在起,您可以在创建 EntityManagerFactory 之前、期间或之后生成您的表。

以下代码演示了如何在不考虑 EntityManagerFactory 创建的情况下生成表:

Map props = new HashMap();
props.put("javax.persistence.schema-generation.database.action", "create");
props.put("javax.persistence.schema-generation.create-source", "metadata");
Persistence.generateSchema("chapter04PUM", props);

以下代码演示了在创建 EntityManagerFactory 时生成表的另一种方法:

Map props = new HashMap();
props.put("javax.persistence.schema-generation.database.action", "create");
props.put("javax.persistence.schema-generation.create-source", "metadata");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("chapter04PUM", props);

Java 事务 API 1.2

Java 事务 API 1.2 规范是在 JSR 907 下开发的。本节仅为您概述了 API 的改进。完整的文档规范(更多信息)可以从 jcp.org/aboutJava/communityprocess/mrel/jsr907/index2.html 下载。

Java 事务 API

Java 事务 APIJTA)是用于在服务器环境中管理一个或多个资源(分布式事务)的标准 Java API。它包括三个主要 API:javax.transaction.UserTransaction 接口,由应用程序用于显式事务界定;javax.transaction.TransactionManager 接口,由应用程序服务器代表应用程序隐式界定事务;以及 javax.transaction.xa.XAResource,它是用于分布式事务处理的标准化 XA 接口的 Java 映射。

JTA 实战

正如我们所说,JTA 事务在 Java EE 环境中使用。为了启用此事务类型,持久化单元的 transaction-type 属性应设置为 JTA 而不是 RESOURCE_LOCAL,并且数据源(如果有),应在 <jta-datasource> 元素内定义。以下代码给出了一个使用 JTA 管理事务的持久化单元示例:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 

  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence 
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

  <persistence-unit name="chapter04PU" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <jta-data-source>onlineRegDataSource</jta-data-source>
  </persistence-unit>

</persistence>

在声明了 JTA 事务类型持久化单元之后,开发者可以选择将事务管理留给服务器(默认情况下,容器将方法视为事务)或者接管控制并程序化定义事务边界。

以下代码是一个容器管理事务的示例:

@Stateless
public class StudentServiceCMT {

  @PersistenceContext
  EntityManager em;  

  public void createStudent(){
    Student student = new Student();
    student.setBirthdate(new Date());
    student.setDepartid("GI");
    student.setId(""+ new Date().getTime());
    student.setFirstname("CMT - FIRST NAME");
    student.setLastname("CMT - Last name");

    em.persist(student);
  }
}

以下代码是一个 Bean 管理事务的示例:

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class StudentServiceBMT {

  @PersistenceContext
  EntityManager em;

  @Resource
  UserTransaction userTx;

  public void createStudent() throws Exception {
    try {
      userTx.begin();//begin transaction

      Student student = new Student();
      student.setBirthdate(new Date());
      student.setDepartid("GI");
      student.setId(""+ new Date().getTime());
      student.setFirstname("BMT - FIRST NAME");
      student.setLastname("BMT - Last name");

      em.persist(student);

      userTx.commit(); // commit transaction
      } catch (Exception ex) {
      userTx.rollback();//rollback transaction
      throw ex;
    } 
  }
}

JTA 1.2 引入的创新

与 JPA 规范不同,JTA 只有少数改进,以下是一些总结。首先,我们增加了两个新的注解。第一个是javax.transaction.Transactional,它提供了在 CDI 管理的 bean 或由 Java EE 规范定义为管理 bean 的类上声明性标记事务的可能性。第二个增加的注解是javax.transaction.TransactionScoped,它提供了定义与当前事务生命周期相同的 bean 的可能性。JTA API 还增加了一个异常类javax.transaction.TransactionalException

摘要

在本章中,我们通过示例介绍了两个 API 提供的改进,这两个 API 的主要目标是简化与数据库的交互。第一个介绍的是 JPA API,它通过使用 Java 对象,使您能够创建、读取、更新和删除数据库中的数据。第二个是 JTA API,这是一个为在一个或多个数据源中透明管理事务而设计的 API。

在下一章中,我们将讨论EJB,并且会制作一个小示例,这个示例将包括将我们所学的大多数 API 组合在一起。

第五章:业务层

在这里,我们将从业务层的改进开始介绍,然后在一个小型项目中尝试将之前看到的某些规范组合起来。将要涉及的主题包括:

  • 企业 JavaBeans 3.2

  • 将所有 Java EE 7 规范合并在一起

企业 JavaBeans 3.2

企业 JavaBeans 3.2 规范是在 JSR 345 下开发的。本节仅为您概述 API 的改进。完整的文档规范(更多信息)可以从 jcp.org/aboutJava/communityprocess/final/jsr345/index.html 下载。

应用程序的业务层位于表示层和数据访问层之间。以下图展示了简化的 Java EE 架构。正如您所看到的,业务层充当数据访问和表示层之间的桥梁。

企业 JavaBeans 3.2

它实现了应用程序的业务逻辑。为此,它可以使用一些规范,如 Bean Validation 用于数据验证,CDI 用于上下文和依赖注入,拦截器用于拦截处理等。由于此层可以位于网络中的任何位置,并且预期为多个用户服务,因此它需要最小限度的非功能性服务,如安全、事务、并发和远程访问管理。通过 EJB,Java EE 平台为开发者提供了在不担心不同必需的非功能性服务的情况下实现此层的机会。

通常,本规范不会引入任何新的主要功能。它继续上一版本开始的工作,将某些已过时的功能的实现变为可选,并对其他功能进行轻微修改。

修剪一些功能

在 Java EE 6 引入的移除过时功能的修剪过程之后,Java EE 7 平台对某些功能的支持已变为可选,其描述已移至另一份名为 EJB 3.2 可选功能评估 的文档中。涉及此迁移的功能包括:

  • EJB 2.1 及更早版本实体 Bean 组件合约,用于容器管理持久性

  • EJB 2.1 及更早版本实体 Bean 组件合约,用于 Bean 管理持久性

  • EJB 2.1 及更早版本实体 Bean 的客户端视图

  • EJB QL:容器管理持久性查询方法的查询语言

  • 基于 JAX-RPC 的 Web 服务端点

  • JAX-RPC Web 服务客户端视图

EJB 3.2 的最新改进

对于那些必须使用 EJB 3.0 和 EJB 3.1 的人来说,你会注意到 EJB 3.2 实际上只对规范进行了微小的修改。然而,一些改进不容忽视,因为它们提高了应用程序的可测试性,简化了会话 bean 或消息驱动 bean 的开发,并提高了对事务和有状态 bean 钝化管理控制的改进。

会话 bean 增强

会话 bean 是一种 EJB 类型,允许我们实现可被本地、远程或 Web 服务客户端视图访问的业务逻辑。会话 bean 有三种类型:无状态用于无状态处理,有状态用于需要在不同方法调用之间保持状态的过程,以及单例用于在不同客户端之间共享一个对象的单个实例。

以下代码展示了保存实体到数据库的无状态会话 bean 的示例:

@Stateless
public class ExampleOfSessionBean  {

    @PersistenceContext EntityManager em;

    public void persistEntity(Object entity){
        em.persist(entity);
    }
}

讨论会话 bean 的改进时,我们首先注意到有状态会话 bean 中的两个变化:在用户定义的事务上下文中执行生命周期回调拦截器方法的能力,以及手动禁用有状态会话 bean 钝化的能力。

可以定义一个必须根据 EJB bean 的生命周期(构造后、销毁前)执行的过程。由于@TransactionAttribute注解,你可以在这些阶段执行与数据库相关的过程,并控制它们对系统的影响。以下代码在初始化后检索一个实体,并确保在 bean 销毁时将持久上下文中所做的所有更改发送到数据库。正如你在以下代码中所看到的,init()方法的TransactionAttributeTypeNOT_SUPPORTED;这意味着检索到的实体将不会包含在持久上下文中,对其所做的任何更改也不会保存到数据库中:

@Stateful
public class StatefulBeanNewFeatures  {

    @PersistenceContext(type= PersistenceContextType.EXTENDED)
    EntityManager em;

    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    @PostConstruct
    public void init(){
         entity = em.find(...);
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    @PreDestroy
    public void destroy(){
        em.flush();
    }
}

以下代码演示了如何控制有状态 bean 的钝化。通常,在一段时间的不活跃后,会话 bean 会被从内存中移除并存储到磁盘上。这个过程需要数据序列化,但在序列化过程中,所有瞬态变量都会被跳过并恢复到其数据类型的默认值,对于对象来说是null,对于int来说是零等等。为了防止这种类型的数据丢失,你可以通过将false值传递给@Stateful注解的passivationCapable属性来简单地禁用有状态会话 bean 的钝化。

@Stateful(passivationCapable = false)
public class StatefulBeanNewFeatures  {
   //...
}

为了简化,EJB 3.2 放宽了定义会话 bean 默认本地或远程业务接口的规则。以下代码展示了如何根据情况将一个简单的接口视为本地或远程:

//In this example, yellow and green are local interfaces
public interface yellow { ... }
public interface green { ... }

@Stateless
public class Color implements yellow, green { ... }

//In this example, yellow and green are local interfaces
public interface yellow { ... }
public interface green { ... }

@Local
@Stateless
public class Color implements yellow, green { ... }

//In this example, yellow and green are remote interfaces
public interface yellow { ... }
public interface green { ... }

@Remote
@Stateless
public class Color implements yellow, green { ... }

//In this example, only the yellow interface is exposed as a remote interface
@Remote
public interface yellow { ... }
public interface green { ... }

@Stateless
public class Color implements yellow, green { ... }

//In this example, only the yellow interface is exposed as a remote interface
public interface yellow { ... }
public interface green { ... }

@Remote(yellow.class)
@Stateless
public class Color implements yellow, green { ... }

EJB Lite 改进

在 EJB 3.1 之前,实现一个 Java EE 应用程序需要使用一个包含超过二十个规范的完整 Java EE 服务器。这对于只需要某些规范的应用程序来说可能过于沉重(就像有人要求你用锤子打死一只苍蝇一样)。为了适应这种情况,JCP(Java 社区进程)引入了配置文件和 EJB Lite 的概念。具体来说,EJB Lite 是 EJB 的一个子集,它将本地事务性和安全处理的基本功能分组在一起。有了这个概念,现在可以在不使用 Java EE 服务器的情况下对 EJB 应用程序进行单元测试,同时也使得在 Web 应用程序或 Java SE 中有效地使用 EJB 成为可能。

除了 EJB 3.1 中已有的特性之外,EJB 3.2 规范增加了对本地异步会话 bean 调用和非持久 EJB 定时服务的支持。这丰富了可嵌入的EJBContainer、Web 配置文件,并增加了可测试的特性的数量。以下代码展示了包含两个方法的 EJB 打包在 WAR 存档中。asynchronousMethod()是一个异步方法,允许你比较客户端方法调用结束和服务器端方法执行结束之间的时间差。nonPersistentEJBTimerService()方法演示了如何定义一个每分钟执行一次的非持久 EJB 定时服务:

@Stateless
public class EjbLiteSessionBean {

    @Asynchronous
    public void asynchronousMethod(){
        try{
          System.out.println("EjbLiteSessionBean - start : "+new Date());
          Thread.sleep(1000*10);
          System.out.println("EjbLiteSessionBean - end : "+new Date());        
        }catch(Exception ex){
            ex.printStackTrace();
        }
    }    

    @Schedule(persistent = false, minute = "*", hour = "1")
    public void nonPersistentEJBTimerService(){
        System.out.println("nonPersistentEJBTimerService method executed");
    }
}

对 TimerService API 所做的更改

EJB 3.2 规范通过一个名为getAllTimers()的新方法增强了TimerService API。此方法使你能够访问 EJB 模块中的所有活动定时器。以下代码演示了如何创建不同类型的定时器、访问它们的信息以及取消它们;它使用了getAllTimers()方法:

@Stateless
public class ChangesInTimerAPI implements ChangesInTimerAPILocal {

    @Resource
    TimerService timerService;
    public void createTimer(){
        //create a programmatic timer
        long initialDuration = 1000*5;
        long intervalDuration = 1000*60;
        String timerInfo = "PROGRAMMATIC TIMER";
        timerService.createTimer(initialDuration, intervalDuration, timerInfo);
    }

    @Timeout
    public void timerMethodForProgrammaticTimer(){
        System.out.println("ChangesInTimerAPI - programmatic timer : "+new Date());
    }

    @Schedule(info = "AUTOMATIC TIMER", hour = "*", minute = "*")
    public void automaticTimer(){
        System.out.println("ChangesInTimerAPI - automatic timer : "+new Date());
    }

    public void getListOfAllTimers(){
        Collection<Timer> alltimers = timerService.getAllTimers();

        for(Timer timer : alltimers){            
            System.out.println("The next time out : "+timer.getNextTimeout()+", "
                    + " timer info : "+timer.getInfo());
            timer.cancel();            
        }
    }
}

除了这种方法之外,规范还移除了仅允许在 bean 内部使用javax.ejb.Timerjavax.ejb.TimerHandlereferences的限制。

与 JMS 的新特性相协调

消息驱动 BeanMDB)是一种 JMS 消息监听器,允许 Java EE 应用程序异步处理消息。要定义这样的 bean,只需用@MessageDriven注解装饰一个简单的 POJO 类,并使其实现javax.jms.MessageListener接口。该接口使 MDB 可用onMessage方法,该方法将在与 bean 关联的队列中发布新消息时被调用。这就是为什么你必须在其中放置处理传入消息的业务逻辑。以下代码给出了一个 MDB 的示例,该示例通过在控制台写入来通知你新消息的到达:

@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationType", 
                               propertyValue = "javax.jms.Queue"),
    @ActivationConfigProperty(propertyName = "destinationLookup", 
                              propertyValue = "jms/messageQueue")
})
public class MessageBeanExample implements MessageListener {

    public MessageBeanExample() {
    }

    @Override
    public void onMessage(Message message) {
        try{
          System.out.println("You have received a new message of type : "+message.getJMSType());
        }catch(Exception ex){
            ex.printStackTrace();
        }
    }
}

由于 JMS 2.0 规范的变化,EJB 3.2 规范对 JMS MDB 激活属性进行了修订,以符合标准属性的列表。这些属性包括:destinationLookupconnectionFactoryLookupclientIdsubscriptionNameshareSubscriptions。此外,它还在 MDB 中添加了实现无方法消息监听器的功能,从而将 bean 的所有公共方法暴露为消息监听器方法。

其他改进

正如我们之前所说的,EJB 3.1 规范给了开发者机会在完整的 Java EE 服务器之外测试 EJB 应用程序。这是通过一个可嵌入的EJBContainer实现的。以下示例演示了如何使用可嵌入的EJBContainer测试 EJB:

@Test
public void testAddition(){            
    Map<String, Object> properties = new HashMap<String, Object>();  
    properties.put(EJBContainer.APP_NAME, "chapter05EmbeddableEJBContainer");
    properties.put(EJBContainer.MODULES, new File("target\\classes"));   
    EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(properties);
    try {
        NewSessionBean bean = (NewSessionBean) container.getContext().lookup("java:global/chapter05EmbeddableEJBContainer/NewSessionBean");
        int restult = bean.addition(10, 10);
        Assert.assertEquals(20, restult);
    } catch (NamingException ex) {
        Logger.getLogger(AppTest.class.getName()).log(Level.FINEST, null, ex);
    } finally {
       container.close();
    }
}

由于在编写本书时(这导致了错误“没有可用的 EJBContainer 提供程序”),maven 引用的可嵌入EJBContainer没有更新,所以我直接以以下方式处理了glassfish-embedded-static-shell.jar文件:

  • Maven 变量声明:

    <properties>
       <glassfish.installed.embedded.container>glassfish_dir\lib\embedded\glassfish-embedded-static-shell.jar</glassfish.installed.embedded.container>
    </properties>
    
  • 依赖声明:

    <dependency>
        <groupId>glassfish-embedded-static-shell</groupId>
        <artifactId>glassfish-embedded-static-shell</artifactId>
        <version>3.2</version>
        <scope>system</scope>           
        <systemPath>${glassfish.installed.embedded.container}</systemPath>
    </dependency>
    

在运行过程中,可嵌入的EJBContainer会获取通常在进程结束时释放的资源,以便其他应用程序能够利用机器的最大功率。在规范的前一个版本中,开发者使用EJBContainer.close()方法在一个finally块中执行这个任务。但是,随着 Java SE 7 中引入的try-with-resources语句,EJB 3.2 在EJBContainer类中添加了对java.lang.AutoCloseable接口的实现,以使开发者摆脱一个容易忘记的任务,并可能对机器的性能产生负面影响。现在,只要在try-with-resources语句中将可嵌入的EJBContainer声明为资源,它将在语句结束时自动关闭。因此,我们不再需要像早期示例中那样的finally块,这简化了代码。以下示例演示了如何在测试使用可嵌入的EJBContainer的 EJB 时利用try-with-resources语句:

@Test
public void testAddition(){
    //...           
    try(EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(properties);) {
         //...
    } catch (NamingException ex) {
       Logger.getLogger(AppTest.class.getName()).log(Level.FINEST, null, ex);
    }
}

本规范的最终改进涉及取消当你想要从 bean 中访问文件系统中的文件或目录时获取当前类加载器的限制。

将所有内容整合在一起

将使我们能够将自第一章以来已经学习的大多数 API 整合在一起的例子是一个在线预注册网站。在这个例子中,我们不会编写任何代码。我们限制自己只对分析一个问题的展示,这个问题将帮助你理解如何使用本书中用于说明各点的每一块代码,以便基于 Java EE 7 的最新功能制作一个高质量的应用程序。

展示项目

虚拟企业软件技术从一所私立大学获得了创建一个应用程序的订单,用于管理学生在线预注册(候选人注册、申请验证和不同候选人的通知),并为连接的学生提供一个实时聊天室。此外,出于统计目的,系统将允许教育部访问来自异构应用程序的某些信息。

被称为 ONPRINS 的系统必须在注册期间稳健、高效,并且全天候可用。

下面的业务领域模型表示了我们系统的主对象(所需的应用程序将基于这些对象构建):

展示项目

注意

免责声明

这些图是由 Sparx Systems 在 Enterprise Architect 中设计和构建的。

用例图 (UCD)

下面的图表示了我们系统将支持的所有功能。我们有以下三个参与者:

  • 候选人是指任何希望为某个系进行预注册的用户。为此,它具有查看系列表、选择系以及完成和提交申请表的能力。通过聊天室,他/她可以与所有与给定主题相关的候选人分享他的/她的想法。

  • 管理员是一个特殊用户,他有权运行预注册的验证过程。正是这个过程创建了学生并向不同候选人发送电子邮件,让他们知道他们是否被选中。

  • 教育部是系统的次要参与者;它寻求访问一个学年内预注册学生的数量和学生名单。用例图 (UCD)

类图

下面的类图显示了用于实现我们在线预注册的所有主要类。此图还突出了不同类之间存在的关系。

CandidateSessionBean 类是一个通过 registerCandidate 方法记录候选人预注册的 bean。它还提供了访问所有已注册候选人(listOfCandidates)和预注册学生(listOfStudents)的方法。

InscriptionValidationBean类包含startValidationBatchJob方法,正如其名所示,它启动批量处理以验证预注册并通知不同候选人。这里展示的批量处理是块类型,其中ValidationReader类用于读取验证有用的数据,ValidationProcessor类用于验证预注册,ValidationWriter类用于通知候选人。此类还用于在候选人被选中时创建学生。正如你所见,为了发送电子邮件,ValidationWriter类首先通过MsgSenderSessionBean将 JMS 消息发送到负责发送电子邮件的组件。这使我们能够在连接中断时避免ValidationWriter中的阻塞。此外,在批量处理中,我们还有ValidationJobListener监听器,它使我们能够在批量处理结束时在验证表中记录一定量的信息。

为了简化重用性,候选人在预注册期间(departmentList.xhtmlacceptanceConditions.xhtmlidentificationInformation.xhtmlcontactInformation.xhtmlmedicalInformation.xhtmlschoolInformation.xhtmlInformationValidation.xhtml)在网页间的导航将使用 Faces Flow。另一方面,各个页面的内容将以资源库合同进行结构化,聊天室中的通信将通过 WebSocket 进行管理;这就是为什么你有ChatServerEndPoint类,它是这种通信的服务器端点。

预注册验证过程的执行是从inscriptionValidation.xhtml面页进行的。为了给管理员提供验证过程进度的反馈,面页将包含实时更新的进度条,这又使我们再次使用 WebSocket 协议。

类图

组件图

以下图显示了构成我们系统的各种组件。正如你所见,部应用与ONPRINS之间的数据交换将通过 Web 服务进行,目的是使两个系统完全独立,而我们的系统使用连接器来访问存储在大学 ERP 系统上的用户信息。

组件图

摘要

如承诺的那样,在本章中,我们介绍了 EJBs 引入的创新,然后专注于在线预注册应用程序的分析和设计。在这个练习中,我们能够查看实际案例,使我们能够使用已经讨论过的几乎所有概念(WebSocket 和 Faces Flow)并发现新的概念(Web 服务、连接器和 Java 邮件)。在下一章中,我们将专注于这些新概念,以便尝试回答以下问题:何时以及如何实现这些概念?

第六章. 与外部系统通信

在本章中,我们将添加在应用程序中与不同系统通信的可能性。从技术上讲,我们将解决系统集成问题。系统集成问题包括几种情况:两个同步或异步交换数据的程序,一个访问由另一个程序提供的信息的应用程序,一个执行在另一个程序中实现的过程的应用程序,等等。鉴于今天存在的解决方案数量,了解根据问题选择哪种解决方案是必要的,因此本章的重要性。在本章结束时,你将能够选择一个集成解决方案,并对以下 API 所做的更改有一个概述:

  • JavaMail

  • Java EE 连接器架构

  • Java 消息服务

  • JAX-RS:Java API for RESTful Web Services

JavaMail

JavaMail 1.5 规范是在 JSR 919 下开发的。本节仅为您概述 API 的改进。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/mrel/jsr919/index2.html下载。

在 Java 中发送电子邮件

互联网的扩展极大地促进了通过电子邮件(电子邮件)在全球范围内的通信。今天,地球上两端的人们可以在非常短的时间内交换信息。为了实现这一点,必须有用于存储交换数据的邮件服务器和客户端(例如,Outlook)用于发送和检索数据。这些元素之间的通信需要不同类型的协议,例如,SMTP简单邮件传输协议)用于发送邮件,POP 3邮局协议)用于接收邮件,IMAP互联网消息访问协议)用于接收电子邮件。这种协议的多样性可能会给开发者带来问题。

由于协议众多和底层编程的困难,Java 语言提供了JavaMail API,以便简化发送和检索电子邮件,无论底层协议如何。但 JavaMail API 并不足够;因为它被设计来处理消息的传输方面(连接参数、源、目标、主题等),消息体由JavaBeans Activation FrameworkJAF 框架)管理。这就是为什么,除了mail.jar库之外,你还需要导入activation.jar库。

通过 SMTP 协议发送电子邮件

使用 JavaMail 发送电子邮件的方法如下:

  1. 获取session对象。此对象封装了各种信息,例如邮件服务器的地址。以下代码展示了如何获取类型为Session的对象:

    Properties prop = System.getProperties();
    //serveurAddress is the host of you mail server
    prop.put("mail.smtp.host", serveurAddress);
    Session session = Session.getDefaultInstance(prop,null);
    
  2. 构建消息。要发送电子邮件,必须定义一些参数,例如电子邮件的内容、发件人和目的地。除了这些设置外,您可能还需要指定电子邮件的主题和其标题。所有这些都可以通过提供构建给定会话消息的几个方法的MimeMessage类来实现。以下代码展示了如何获取MimeMessage类型的对象并构建要发送的邮件:

    Message msg = new MimeMessage(session);
    msg.setFrom(new InternetAddress("xxx-university@yahoo.fr"));
    InternetAddress[] internetAddresses = new InternetAddress[1];
    internetAddresses[0] = new InternetAddress("malindaped@yahoo.fr");
    msg.setRecipients(Message.RecipientType.TO,internetAddresses);
    msg.setSubject("Pre-inscription results");
    msg.setText("Dear Malinda, we inform you that …");
    
  3. 发送消息。我们使用Transport类在一行中发送消息。以下代码展示了如何发送消息:

    Transport.send(msg);
    

以下代码展示了如何从 Gmail 账户发送个别候选人的预注册结果。如您所见,Gmail 发件人账户及其密码作为参数传递给send方法。这使得应用程序在发送消息时可以通过服务器进行认证。要测试与本章相关的发送代码,您需要有一个 Gmail 账户,并将username替换为您的账户用户名,将user_password替换为该账户的密码。

以下代码是使用 JavaMail API 通过 Gmail SMTP 服务器发送电子邮件的示例:

public class MailSender {

  private final String userName = "username@gmail.com";
  private final String userPassword = "user_password";    
  private Session session;

  public MailSender() {
    Properties props = new Properties();
    props.put("mail.smtp.auth", "true");
    props.put("mail.smtp.starttls.enable", "true");
    props.put("mail.smtp.host", "smtp.gmail.com");
    props.put("mail.smtp.port", "587");

    session = Session.getInstance(props, null);
  }

  public void sendMesage(String message, String toAddress) {
    try {

      Message msg = new MimeMessage(session);
      InternetAddress[] internetAddresses =new InternetAddress[1];
      internetAddresses[0] = new InternetAddress(toAddress);
      msg.setRecipients(Message.RecipientType.TO, internetAddresses);
      msg.setSubject("Pre-inscription results");
      msg.setText(message);

      Transport.send(msg, userName, userPassword);
    } catch (Exception ex) {
      ex.printStackTrace();
    }

  }
}

当然,JavaMail API 提供了检索消息、将文档附加到您的消息、以 HTML 格式编写消息以及执行许多其他操作的能力。

最新改进在行动中

尽管受到维护版本的影响,JavaMail 1.5 规范已经经历了许多变化。其中最重要的可以分为三类,即:添加注解、添加方法和更改某些访问修饰符。

添加的注解

总的来说,JavaMail 1.5 引入了两个新的注解(@MailSessionDefinition@MailSessionDefinitions),用于在 Java EE 7 应用程序服务器中配置 JavaMail 会话资源。

@MailSessionDefinition注解包含几个参数(见以下代码中的Java类),目的是提供定义一个将被注册在任何有效的 Java EE 命名空间中并通过JNDI被其他组件访问的邮件会话的可能性。

以下代码突出了@MailSessionDefinition注解的属性:

public @interface MailSessionDefinition {

  String description() default "";

  String name();

  String storeProtocol() default "";

  String transportProtocol() default "";

  String host() default "";
  String user() default "";

  String password() default "";

  String from() default "";

  String[] properties() default {};
}

通过这个注解,我们现在可以定义和使用Session类型的对象,就像以下代码示例所展示的那样,它展示了如何使用@MailSessionDefinition

@MailSessionDefinition(
  name = "java:app/env/MyMailSession",
  transportProtocol = "SMTP",
  user = "username@gmail.com",
  password = "user_password"        
  //...
)
@WebServlet(name = "MailSenderServlet")
public class MailSenderServlet extends HttpServlet {

  @Resource(lookup="java:app/env/MyMailSession")
  Session session;

  public void doPost(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {

    //...
  }
}

虽然@MailSessionDefinition注解允许我们定义MailSession,但@MailSessionDefinitions注解允许我们配置多个MailSession实例。以下代码展示了如何一次使用@MailSessionDefinitions实例定义两个MailSession

@MailSessionDefinitions(
        { @MailSessionDefinition(name = "java:/en/..."),
        @MailSessionDefinition(name = "java:/en/...") }
)

添加的方法

为了减轻开发者的工作负担,JavaMail 1.5 添加了提供真正有趣快捷方式的新方法。例如,Transport.send(msg, username, password) 方法的添加,在发送消息时避免了为认证参数创建额外的对象。在此之前,认证参数是在 session 对象中定义的,如下面的代码所示:

Session session = Session.getInstance(props,new javax.mail.Authenticator() {
  protected PasswordAuthentication getPasswordAuthentication() {
    return new PasswordAuthentication(username, password);
    }
});

作为添加方法的一个另一个例子,您有 Message.getSession() 方法,它允许您访问用于创建消息的 session 类型的对象。这可能会防止您在处理过程中拖动会话。我们将讨论的最后一个添加的方法是 MimeMessage.reply(replyToAll, setAnswered) 方法,由于第二个参数,当您响应,例如,一条消息时,它允许您自动将 Re 前缀添加到主题行。

一些访问修饰符的变化

关于访问修饰符,JavaMail 1.5 规范在一些类中强调了良好的实践,并促进了其他类的扩展。

您将看到,例如,javax.mail.search 包中最终类的保护字段的访问修饰符已更改为私有。实际上,最终类包含具有公共 getter/setter 方法的保护字段并不重要。因此,最好将它们设为私有,并让 getter/setter 保持为公共,这样我们就可以从外部访问/编辑它们的值。

尽管如此,在访问修饰符的变化中,JavaMail 1.5 已经将 cachedContent 字段(属于 MimeBodyPartMimeMessage 类)以及 MimeMultipart 类的字段从私有改为保护,以便于扩展相关类。

Java EE 连接器架构 (JCA)

Java EE 连接器架构 1.7 规范是在 JSR 322 下开发的。本节仅为您概述了 API 的改进。完整的文档规范(更多信息)可以从 jcp.org/aboutJava/communityprocess/final/jsr322/index.html 下载。

什么是 JCA?

通常,大型公司的企业信息系统EISs)由一系列工具组成,例如企业资源计划应用(ERP,即 SAP)、客户关系管理应用(CRM,即 salesforce.com)、主机事务处理应用、遗留应用和数据库系统(如 Oracle)。在这样的环境中,开发新的解决方案可能需要访问这些工具之一或多个以检索信息或执行处理:我们这时所说的就是企业应用集成EAI)。在没有标准解决方案的情况下,这种集成对供应商和开发者来说都将非常昂贵。供应商将开发 API 来管理不同类型服务器之间的通信,开发者将针对 EISs 逐个处理,并实现应用程序所需的技术特性(连接轮询、事务安全机制等)。因此,JCA 的需求就产生了。

Java EE 连接器架构(JCA)是一个旨在从 Java EE 平台标准化访问异构现有企业信息系统(EISs)的规范。为此,它定义了一系列合约,使开发者能够通过一个称为通用客户端接口CCI)的通用接口无缝访问不同的 EISs。对于那些已经使用过 JDBC 的人来说,理解 JCA 的功能要容易一些。JCA 连接器由两个主要元素组成:

  • 通用客户端接口CCI):这个 API 对于 EISs 来说就像 JDBC 对于数据库一样。换句话说,CCI 定义了一个标准的客户端 API,允许组件访问 EISs 并执行处理。

  • 资源适配器:这是针对特定 EIS 的 CCI 的特定实现。它由供应商提供,并保证通过 JCA 执行其 EIS 的功能。资源适配器被打包在一个名为 Resource Adapter Module.rar 归档中,并且它必须遵守一些合约(系统级合约),以便集成到 Java EE 平台并利用连接、事务和安全管理等服务。

话虽如此,当您想访问提供资源适配器的 EIS 时,可以考虑使用 JCA。

JCA 应用实例

由于未能提供一个具体的示例来展示如何使用连接器访问由 SAP 管理的员工列表(这将非常长),以便您理解其基本功能),以下代码仅展示了 JCA API 的概述。这包括连接的一般原则、数据操作的可能性和断开连接。

对于希望进一步学习的人来说,GlassFish 提供了一个完整的示例,演示了如何实现一个连接器以访问邮件服务器,并且可在www.ibm.com/developerworks/java/tutorials/j-jca/index.html找到的教程提供了额外的信息。

以下代码是资源适配器交互的概述:

try {
  javax.naming.Context ic = new InitialContext();
  javax.resource.cci.ConnectionFactory cf =
  (ConnectionFactory)
  ic.lookup("java:comp/env/eis/ConnectionFactory");
  //Connection
  javax.resource.cci.Connection ctx = cf.getConnection();

  System.out.println("Information about the result set functionality " + "supported by the connected EIS : " +ctx.getResultSetInfo());

  System.out.println("Metadata about the connection : " + ctx.getMetaData());

  //Get object for accessing EIS functions
  javax.resource.cci.Interaction interaction =ctx.createInteraction();

  //Get record factory
  javax.resource.cci.RecordFactory rfact = cf.getRecordFactory();

  javax.resource.cci.IndexedRecord input =rfact.createIndexedRecord("<recordName>");
  javax.resource.cci.IndexedRecord output =rfact.createIndexedRecord("<recordName>");
  //Look up a preconfigured InteractionSpec
  javax.resource.cci.InteractionSpec interSp = ... ;
  interaction.execute(interSp, input, output);
  int index_of_element = ...;//index of element to return
  System.out.println("The result : "+output.get(index_of_element));
  //close
  interaction.close();
  ctx.close();
} catch (Exception ex) {
  ex.printStackTrace();
}

最新改进

说到新特性,Java EE 连接器架构 1.7 略有改进。实际上,在这个规范中,更多的是澄清和要求声明。话虽如此,JCA 1.7 引入了以下更改:

  • 当调用endpointActivationendpointDeactivation方法时,它强调端点组件环境命名空间对资源适配器的可用性

  • 它添加了ConnectionFactoryDefinitionAdministeredObjectDefinition注解,用于定义和配置资源适配器的资源

  • 它阐明了当使用 CDI(上下文与依赖注入)管理的 JavaBean 时,依赖注入的行为

Java 消息服务(JMS)

Java 消息服务 2.0 规范是在 JSR 343 下开发的。本节仅提供 API 改进的概述。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr343/index.html下载。

何时使用 JMS

JMS 是用于与消息导向中间件MOM)交互的 Java API。这种中间件源于解决观察到的同步连接限制的需求。这是因为同步连接容易受到网络故障的影响,并且要求连接的系统同时可用。因此,MOMs 提供了一个基于消息交换的集成系统,这些消息可以根据集成系统的可用性同步或异步处理。

以下图像展示了一种通过 MOM(消息导向中间件)进行系统通信的架构:

何时使用 JMS

基于上述内容,我们得出结论,JMS 可以在以下情况下使用:

  • 通过不稳定网络处理大量事务数据(例如,数据库同步)

  • 通信系统之间不是总是同时可用的

  • 向多个系统发送数据

  • 异步处理

以此点结束,你应该注意到,基于 JMS(Java 消息服务)的集成系统的建立要求所有需要集成的组件都在你的控制之下。因此,JMS 更适合用于公司内部解决方案的集成。

最新改进的实际应用

2002 年 3 月发布的 JMS 1.1 规范与其他经过平台演变而简化的 Java EE 平台 API 相比,显得有些过时和笨重。基于这一观察,你会理解为什么 JMS 2.0 API 的一个主要目标就是更新 API,使其尽可能简单,并能轻松与其他平台 API 集成。为此,审查了几个领域;这些包括减少样板代码、移除冗余项、添加新功能和集成 Java 语言的新特性。

新特性

在 JMS 2.0 规范中,强调了三个新特性:异步发送消息、投递延迟和修改 JMSXDeliveryCount 消息属性。

异步发送消息

在同步处理中,如果方法 A 调用方法 B,方法 A 将保持阻塞状态,直到方法 B 完成。这可能导致时间的浪费。为了克服这个问题,JMS 2.0 提供了一套方法,可以在不失去操作进度的情况下异步发送消息。以下代码演示了如何异步发送消息。setAsync() 方法接受一个监听器作为参数,允许你在过程结束时或当抛出异常时得到通知。如果监听器不为空,消息将异步发送(过程将由与调用者线程不同的另一个线程执行)。否则,消息将同步发送。

public void sendMessageAsynchronously(ConnectionFactory cfactory,Queue destination){
  try(JMSContext context = cfactory.createContext();){
    context.createProducer().setAsync(new Completion()).send(destination, "Hello world");
  }
}

class Completion implements CompletionListener{

  public void onCompletion(Message message) {     
    System.out.println("message sent successfully");
  }

  public void onException(Message message, Exception ex) {   
    System.out.println(ex.getMessage());
  }
}

投递延迟

除了可以异步发送消息之外,JMS 现在还允许我们延迟已发送到代理(即 MOM 服务器)的消息的投递时间。发送后,消息将被存储在代理上,但直到发送者设定的时间,它对接收者来说是未知的。以下代码中的消息将在发送后至少一小时后发送给接收者。

public void sendMessageWithDelay(ConnectionFactory cfactory,Queue destination){
  try(JMSContext context = cfactory.createContext();){
    context.createProducer().setDeliveryDelay(1000*60*60).send(destination, "Hello world");
  }
}

处理 JMSXDeliveryCount 消息属性

自 1.1 版本以来,JMS 规范定义了一个可选的 JMSXDeliveryCount 消息属性,可用于确定被投递多次的消息,并在投递次数超过最大值时执行操作。但是,由于对这个属性的管理是可选的,所有提供者都没有义务增加它,这导致使用它的应用程序不可移植。JMS 2.0 规范将此作为标准引入,以便我们可以以可移植的方式自定义毒消息的管理。毒消息是指一个 JMS 消息,它已经超过了给定接收者的最大投递次数。以下代码展示了如何检索 JMSXDeliveryCount 消息属性并指定当一条消息被投递超过五次时采取的操作:

public class JmsMessageListener implements MessageListener {

  @Override
  public void onMessage(Message message) {
    try {
      int jmsxDeliveryCount =message.getIntProperty("JMSXDeliveryCount");
      //...
      if(jmsxDeliveryCount > 5){
        // do something
      }
    } catch (JMSException ex) {
      ex.printStackTrace();
    }
  }
}

API 简化

JMS 2.0 规范引入了三个新的接口(JMSContextJMSProducerJMSConsumer),这些接口有助于消除样板代码并简化 API。重要的是要注意,这些接口(构成了简化 API)与旧接口共存,以提供替代方案。因此,JMSContext取代了ConnectionSession对象,JMSProducer取代了MessageProducer对象,JMSConsumer取代了旧版本中的MessageConsumer对象。正如您在以下代码中所见,两种方法之间的差异非常明显。在基于 JMS API 1.1 的发送方法(sendMessageJMSWithOldAPI)中,我们注意到:过多的对象创建、强制抛出异常以及需要显式关闭连接。

相反,在基于 JMS API 2.0 的发送方法(sendMessageJMSWithNewdAPI)中,我们有:try-with-resources 语句,它使开发者免于显式关闭连接,以及将发送代码简化到仅包含一个JMSContext对象的基本要素。

//Sending message with JMS 1.1
public void sendMessageJMSWithOldAPI(ConnectionFactory connectionFactory, Queue destination) throws JMSException {
 Connection connection = connectionFactory.createConnection();
 try {
     Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
     MessageProducer messageProducer = session.createProducer(destination);
     TextMessage textMessage = session.createTextMessage("Message send with the old API");
     messageProducer.send(textMessage);
  } finally {
    connection.close();
  }
}

//Sending message with JMS 2.0
public void sendMessageJMSWithNewdAPI(ConnectionFactory connectionFactory, Queue destination) {
     try (JMSContext context = connectionFactory.createContext();) {
        context.createProducer().send(destination, "Message send with the new API");
   }
}

Java API for RESTful Web Services

Java API for RESTful Web Services 2.0 规范是在 JSR 339 下开发的。本节仅为您概述 API 的改进。完整的文档规范(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr339/index.html下载。

何时使用 Web 服务

Web 服务是一种基于开放标准(如 HTTP、XML 和 URI)的软件系统,旨在允许网络中应用程序之间的交换。通过使用这些开放标准,它具备了成为集成异构系统最合适解决方案所需的一切。然而,正如我们在讨论 JMS 时所看到的,选择集成解决方案应在一系列问题之后进行:网络连接是否良好?进程是否是事务性的?要处理的数据量是否巨大?处理必须是同步的吗?等等。

如果经过调查,您的选择是 Web 服务,那么您现在必须选择要实现的 Web 服务类型:基于 SOAP(简单对象访问协议)和 XML 的SOAP Web 服务,或者专注于资源共享并因此其功能基于 Web 的 RESTful Web 服务。在这本书中,我们只讨论 RESTful Web 服务。

JAX-RS 实践

RESTful Web 服务是 Web 服务的一种变体,其中任何可以寻址的概念(功能或数据)都被视为资源,因此可以通过统一资源标识符URIs)访问。一旦定位到资源,其表示或状态将以 XML 或 JSON 文档的形式传输。在我们的在线预注册应用程序中,资源可能是选定的学生列表,表示将以 JSON 文档的形式出现。

JAX-RS 是实现 RESTful Web 服务的 Java API。以下代码演示了如何编写一个返回所有被选学生列表的 REST 服务:

@Path("students")
@Stateless
@Produces({MediaType.APPLICATION_JSON})
public class StudentInformation {

  @PersistenceContext(unitName = "integrationPU")
  private EntityManager em;

  @GET
  @Path("getListOfStudents")
  public List<Student> getListOfStudents(){
    TypedQuery<Student> query = em.createQuery("SELECT s FROM Student s", Student.class);
    return query.getResultList();
  }
} }

最新改进措施

JAX-RS 2.0 不仅简化了 RESTful Web 服务的实现,还在 API 中引入了新功能,其中包括客户端 API、异步处理、过滤器以及拦截器。

客户端 API

自从 1.0 版本以来,JAX-RS 规范没有定义客户端 API 来与 RESTful 服务交互。因此,每个实现都提供了一个专有的 API,这限制了应用程序的可移植性。JAX-RS 2.0 通过提供一个标准的客户端 API 来填补这一空白。

以下代码演示了客户端的实现,该客户端将通过前面代码中公开的 REST 服务访问选定的学生列表:

String baseURI ="http://localhost:8080/chapter06EISintegration-web";
Client client = ClientBuilder.newClient();
WebTarget target = client.target(baseURI+"/rs-resources/students/getListOfStudents");      
GenericType<List<Student>> list = new GenericType<List<Student>>() {};
List<Student> students =target.request(MediaType.APPLICATION_JSON).get(list);

异步处理

除了客户端 API 的标准化外,JAX-RS 2.0 还集成了 Java EE 平台许多 API 中已经存在的功能,即异步处理。现在,JAX-RS 客户端可以异步发送请求或处理响应。

以下代码演示了 JAX-RS 客户端如何异步执行 GET 请求并被动等待响应。如代码所示,JAX-RS 请求的异步执行需要调用async()方法。此方法返回一个类型为AsyncInvoker的对象,其 get、post、delete 和 put 方法允许我们获得用于进一步处理响应的对象类型Future

以下代码是 JAX-RS 客户端中异步过程执行的一个示例:

public class AppAsynchronousRestfulClient {

  public static void main(String[] args) {    
  String baseURI ="http://localhost:8080/chapter06EISintegration-web";
  String location = "/rs-resources";
  String method = "/students/getListOfAllStudentsAs";
  Client client = ClientBuilder.newClient();
  WebTarget target =
    (WebTarget) client.target(baseURI+location+method);
  System.out.println("Before response : "+new Date());
  Future<String> response = target.request(MediaType.APPLICATION_JSON).async().get(String.class);            

  new PassiveWaiting(response).start();   

  System.out.println("After PassiveWaiting : "+new Date());
  }

  static class PassiveWaiting extends Thread {
    Future<String> response;

    public PassiveWaiting(Future<String> response){
      this.response = response;
    }

    public void run(){
      try{
        System.out.println("response :"+response.get()+", time : "+new Date());
      }catch(Exception ex){
        ex.printStackTrace();
      }
    }
  }
}

为了确保处理是异步执行的,我们在getListOfAllStudentsAs方法中定义了一个 20 秒的暂停,在执行 JPQL 查询之前。以下代码,这是一个慢速处理的模拟,显示了客户端执行的方法的内容:

@GET
@Path("getListOfAllStudentsAs")
public List<Student> getListOfAllStudentsAs() {
  try{
    Thread.sleep(20*1000);//20 seconds
  }catch(Exception ex){}
  TypedQuery<Student> query = em.createQuery("SELECT s FROM Student s", Student.class);
  return query.getResultList();
}

类似地,JAX-RS 服务器能够异步运行进程。包含执行异步任务指令的方法必须将带有 @Suspended 注解的 AsyncResponse 对象作为方法参数注入。然而,你应该知道服务器异步模式与客户端异步模式不同;前者在处理请求期间挂起发送请求的客户端连接,稍后通过 AsyncResponse 对象的 resume() 方法恢复连接。该方法本身不会异步运行。要使其异步,你必须将进程委托给一个线程(这就是我们在以下示例的 getListOfAllStudentsAs2 方法中所做的),或者用 @Asynchronous 注解装饰它。以下代码演示了如何在服务器端执行异步处理。

以下代码是 JAX-RS 服务器中异步执行过程的示例:

@Path("students")
@Stateless
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class StudentInformation {

  @PersistenceContext(unitName = "integrationPU")
  private EntityManager em;

  @Resource(lookup ="java:comp/DefaultManagedScheduledExecutorService")
  ManagedExecutorService taskExecutor;

  @GET
  @Path("getListOfAllStudentsAs2")
  public void getListOfAllStudentsAs2(final @Suspended AsyncResponse response) {
      System.out.println("before time : "+new Date());
      taskExecutor.submit(
      new Runnable() {
        public void run() {
          String queryString = "SELECT s FROM Student sWHERE 1 = 1";
          TypedQuery<Student> query = em.createQuery(queryString, Student.class);

          List<Student> studentList = query.getResultList();
          try {
            Thread.sleep(10 * 1000);//1 second
          } catch (Exception ex) {
          }
          response.resume(studentList);
        }
      });
    System.out.println("After time : "+new Date());
  }
}

过滤器和实体拦截器

JAX-RS 2.0 规范的另一个重要内容是引入了两种拦截机制:过滤器和拦截器。这些新特性为规范带来了一种标准化的拦截处理方式,以便无缝管理 JAX-RS 服务器与将访问服务器资源的不同客户端之间的安全、压缩、编码、日志记录、编辑和审计交换。

尽管这两个概念非常相似(因为它们都与拦截有关),但我们必须说过滤器通常用于处理请求或响应的头部。而拦截器通常被设置来操作消息的内容。

过滤器

JAX-RS 2.0 规范定义了四种类型的过滤器:每一边(客户端和服务器)各有两种类型的过滤器。在客户端,一个必须在发送 HTTP 请求之前运行的过滤器实现了 ClientRequestFilter 接口,另一个必须在收到来自服务器的响应后立即运行(但在将控制权渲染到应用程序之前),实现了 ClientResponseFilter 接口。在服务器端,将在访问 JAX-RS 资源之前执行的过滤器实现了 ContainerRequestFilter 接口,而将在向客户端发送响应之前运行的过滤器实现了 ContainerResponseFilter 接口。以下代码展示了 ContainerRequestFilter 实现的示例,该实现验证了确保外部用户能够安全访问我们在线预注册应用程序中可用资源的详细信息。以下代码中 MyJaxRsRequestFilter 类顶部的 @Provider 注解允许过滤器被容器自动发现并应用于服务器上的所有资源。如果不使用此注解,你必须手动注册过滤器。

以下代码是ContainerRequestFilter实现的示例:

@Provider
public class MyJaxRsRequestFilter implements ContainerRequestFilter {

  @Override
  public void filter(ContainerRequestContext crq) {
    //        If the user has not been authenticated
    if(crq.getSecurityContext().getUserPrincipal() == null)
      throw new WebApplicationException(Status.UNAUTHORIZED);

      List<MediaType> supportedMedia =crq.getAcceptableMediaTypes();
    if("GET".equals(crq.getMethod()) &&!supportedMedia.contains(MediaType.APPLICATION_JSON_TYPE))
      throw new WebApplicationException(Status.UNSUPPORTED_MEDIA_TYPE);

      //      external users must only access student methods
      String path = crq.getUriInfo().getPath();
    if(!path.startsWith("/students"))
      throw new WebApplicationException(Status.FORBIDDEN);

      List<String> encoding = crq.getHeaders().get("accept-encoding");  
      //   If the client does not support the gzip compression
    if(!encoding.toString().contains("gzip"))
      throw new WebApplicationException(Status.EXPECTATION_FAILED);            
  }
}

实体拦截器

除了在过滤器与实体拦截器之间的差异之外,JAX-RS 提供了两种类型的实体拦截器而不是四种。有一个读取拦截器,它实现了ReaderInterceptor接口,还有一个写入拦截器,它实现了WriterInterceptor接口。由于它们需要处理的元素(消息体),拦截器可以用来压缩大量内容以优化网络利用率;它们也可以用于一些处理,例如生成和验证数字签名。

考虑到我们的在线预注册应用程序的数据库将包含数千名学生,以下代码演示了我们可以如何利用拦截器在数据交换中与教育部进行通信,以避免在传输学生信息时网络过载。

以下代码展示了服务器端WriterInterceptor的实现,该实现将压缩数据发送到 JAX-RS 客户端。@ZipResult注解允许我们将拦截器仅绑定到一些 JAX-RS 资源。如果我们移除此注解,我们应用程序的所有 JAX-RS 资源将自动被压缩。

以下代码是WriterInterceptor实现的示例:

@ZipResult
@Provider
public class MyGzipWriterJaxRsInterceptor implements WriterInterceptor{  

    @Override
    public void aroundWriteTo(WriterInterceptorContext wic) throws IOException {        
        try (GZIPOutputStream gzipStream = new GZIPOutputStream(wic.getOutputStream());) {
            wic.setOutputStream(gzipStream);   
            wic.proceed();
        }        
    }
}

要将MyGzipWriterJaxRsInterceptor拦截器绑定到资源,我们只需用@ZipResult注解装饰给定的资源。以下代码演示了如何将MyGzipWriterJaxRsInterceptor拦截器绑定到资源,以便其表示在发送到客户端之前始终被压缩。

以下代码是拦截器绑定的示例:

@GET
@ZipResult
@Path("getListOfAllStudentsGzip")
public List<Student> getListOfAllStudentsGzip() {        
  TypedQuery<Student> query = em.createQuery("SELECT s FROM Student s", Student.class);       
  return query.getResultList();
}

以下代码是@ZipResult注解声明的示例:

@NameBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface ZipResult {}

以下代码展示了客户端上ReaderInterceptor接口的实现,该实现将使用MyGzipWriterJaxRsInterceptor类解压缩服务器压缩的数据:

public class MyGzipReaderJaxRsInterceptor implements ReaderInterceptor {
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException {           
        try (InputStream inputStream = context.getInputStream();) {
            context.setInputStream(new GZIPInputStream(inputStream));
            return context.proceed();            
        }       
    }    
}

要将拦截器绑定到特定的客户端,我们将使用Client对象的register方法。以下代码演示了如何将拦截器或过滤器关联到 JAX-RS 客户端:

public static void main(String[] args) throws IOException {
  String baseURI = "http://localhost:8080/chapter06EISintegration-web";
  String location = "/rs-resources";
  String method = "/students/getListOfAllStudentsGzip";
  //client creation and registration of the interceptor/filter
  Client client = ClientBuilder.newClient().register(MyGzipReaderJaxRsInterceptor.class);
  WebTarget target = (WebTarget)client.target(baseURI + location + method);
  System.out.println("response : " + target.request(MediaType.APPLICATION_JSON).get(String.class));        
}

概述

在分析上一章中展示的在线预注册应用程序的过程中,我们意识到我们的系统应该与其他系统进行通信。本章为我们提供了识别和实现与不同类型的异构系统交换数据最佳方式的知识。在下一章中,我们将回顾一些我们自然使用过的概念,以便您能更好地理解它们。

第七章:注解和 CDI

直至此刻,我们不得不使用注解和依赖注入,而不去尝试理解它们是如何工作的。因此,本章旨在介绍和强调相关 API 的改进。相关的 API 包括:

  • Java 平台通用注解 1.2

  • 上下文和依赖注入 1.1

Java 平台的通用注解

Java 平台通用注解 1.2 规范是在 JSR 250 下开发的。本节仅为您概述 API 的改进。完整的文档规范(更多信息)可以从 jcp.org/aboutJava/communityprocess/mrel/jsr250/index.html 下载。

本规范的目标

注解是一种元数据,通常用于描述、配置或标记 Java 代码中的元素(如类、方法和属性)。在以下代码中,我们使用 @Stateless 注解将 MySessionBean 类配置为无状态会话 Bean,我们使用 @Deprecated 注解标记 oldMethod() 方法为过时,最后我们使用 @TransactionAttribute 注解设置 save() 方法,使其始终使用专用事务。

@Stateless
public class MySessionBean {

  @Deprecated
  public void oldMethod(){}

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void save(){}
}

自 JDK 5 以来,注解已经集成到 Java 语言中,并且现在在许多 API 中被广泛使用。为了避免在多个 API 中重新定义一些注解,JCP 开发了 Java 平台规范的通用注解,目的是将不同 Java EE API 中的通用注解重新分组,从而避免冗余并简化重新分组注解的维护。以下代码展示了来自 Java 平台 API 的通用注解 @Resource 的示例,它允许我们在 Web 容器和 EJB 容器中访问 SessionContext 类型的对象。

@Stateless
public class MySessionBean {

  @javax.annotation.Resource
  private SessionContext sctx;

  //...	
}
@ManagedBean
public class MyJsfManagedBean {
  @javax.annotation.Resource
  private SessionContext sctx;
  //...
}

构建自己的注解

尽管已经存在几个注解,但如果需要,Java 提供了创建自定义注解的机会。为此,你应该知道注解被声明为 Java 接口。唯一的区别是,在注解的情况下,关键字 interface 必须由字符 @ 预先指定。以下代码展示了自定义注解 Unfinished 的声明。这个注解包含一个名为 message 的参数,其默认值为 Nothing has been done

public @interface Unfinished {
  String message() default "Nothing has been done";
}

一旦声明了你的注解,现在你必须定义其特性。注解的基本特性是通过包含在 java.lang.annotation 包中的专用注解来定义的。这些注解如下:

  • @Target:用于定义可以注解的元素类型(如类、方法和属性),例如 @Target({ElementType.METHOD, ElementType.TYPE})

  • @Retention:这是用来定义你的注解的保留级别(例如RUNTIMECLASSSOURCE),例如@Retention(RetentionPolicy.RUNTIME)

  • @Inherited:这是用来表示该注解将自动应用于继承具有该注解的类的类

  • @Documented:这是用来使你的注解出现在包含它的代码的Javadoc

重要的是要注意,对于自定义 CDI 作用域注解,还有其他特性,如作用域(使用@ScopeType设置)。

在所有更改完成后,我们的注解形式如下所示。根据设置,此注解可以装饰方法、对象类型(如classinterfaceenum)和属性。它将在编译时被移除(因为保留级别是SOURCE)。

@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Unfinished {
  String message() default "Nothing has been done";
}

以下代码演示了Unfinished注解的使用:

public class App {
    @Unfinished(message = "Make sure that this element is not null")
    String size;

    @Unfinished
    public static void main(String[] args) {
        System.out.println("Hello World annotation!");
    }
}

尽管我们的注解看起来像是一个标准的注解,但它还没有生效。为此,必须有一个名为processor的类可供编译器使用。这个类将描述当项目被我们的自定义注解标注时应采取的操作。

要为 Java 6 注解实现自定义处理器,我们主要需要实现javax.annotation.processing.Processor接口的process()方法,并使用@SupportedAnnotationTypes注解定义此处理器支持的注解。以下代码展示了我们自定义的Unfinished注解的处理器。如您所见,在实现process()方法时,我们使用了实现Processor接口的抽象类AbstractProcessor。(这防止了我们不得不实现该接口中定义的所有方法。)

@SupportedAnnotationTypes("com.packt.ch07.annotations.Unfinished")
public class UnfinishedProcessor extends AbstractProcessor {

  /**
  * For the ServiceLoader
  */
  public UnfinishedProcessor() {
  }

  @Override
  public boolean process(Set<? extends TypeElement>annotations, RoundEnvironment roundEnv) {
    try {
      //For each annotated element do ...
      for (Element e :roundEnv.getElementsAnnotatedWith(Unfinished.class)) {        
        Unfinished unf = e.getAnnotation(Unfinished.class);
        System.out.println("***** Class :"+e.getEnclosingElement()+", "+ "Annotated element : " + e.getSimpleName()+", "+ " Kind : "+e.getKind()+", Message :"+unf.message()+"**** ");                
      }
    } catch (Exception ex) {
      ex.printStackTrace();
    }
    return true;
  }
}

一旦实现了处理器,我们现在必须声明它,以便编译器可以找到它。最简单的方法是使用以下步骤通过 Java 机制进行服务声明:

  1. 将你的注解打包在一个JAR文件中。

  2. 在此JAR文件中包含一个META-INF/services目录。

  3. META-INF/services目录中包含一个名为javax.annotation.processing.Processor的文件。

  4. 在此文件中指定包含在JAR文件中的处理器的完全限定名称(每行一个处理器)。

以下截图显示了包含Unfinished注解的项目结构。如果像我们的示例那样,没有将注解和处理器放在同一个项目中,你可以为注解和处理器使用一个项目。但无论你的选择如何,都不要忘记在包含处理器的META-INF/services项目目录中定义服务。

构建自己的注解

以下截图显示了文件 javax.annotation.processing.Processor 的内容。由于该包只包含一个处理器,因此很明显,在这个文件中我们将只有一行,如下面的截图所示:

构建自己的注解

对于使用 Maven v2.3.2 的用户,为了实现包含处理器的项目,他们必须在 maven-compiler-plugin 插件的配置中设置选项 <compilerArgument>-proc:none</compilerArgument>,以确保代码被正确编译。

现在,您可以将包含注解的包导入到另一个项目中,并按需使用它。在编译我们前面的 App 类时,我们得到以下结果:

***** Class :com.packt.ch07.App, Annotated element : size,  Kind : FIELD, Message : Make sure that this element is not null****
***** Class :com.packt.ch07.App, Annotated element : main,  Kind : METHOD, Message : Nothing has been done****

最新改进措施

受维护版本的影响,通用注解规范没有发生很大变化。总的来说,我们增加了一个新的注解并更新了规范文档的一些部分。

新的注解

新增到规范中的注解涉及在使用一系列类时管理优先级。这是 javax.annotation.priority 注解。

这个注解的确切角色和可接受的值范围由使用它的每个规范定义。

例如,这个注解可以用来管理拦截器的执行顺序。

上下文和依赖注入

Java EE 1.1 规范中的上下文和依赖注入(CDI)是在 JSR 346 下开发的。本节仅为您概述 API 的改进。完整的规范文档(更多信息)可以从 jcp.org/aboutJava/communityprocess/final/jsr346/index.html 下载。

什么是 CDI?

从 Java EE 平台 6.0 版本引入的上下文和依赖注入(CDI)是一个规范,它为平台带来了一组简化对象生命周期管理、标准化并鼓励在 Java EE 环境中使用依赖注入的服务。具体来说,这个规范使我们能够以松散耦合和类型安全的方式轻松地将不同层(表示层、业务层和数据访问层)连接起来。为了做到这一点,CDI 主要依赖于两个服务,它们是:

  • 上下文:这是基于它们的范围来管理对象的生命周期(创建和销毁的时间)。

  • 依赖注入:这包括将一个组件注入到另一个组件中、为给定接口选择要注入的实现以及提供用于访问注入依赖的对象类型:一个代理或直接访问实例的引用。

为了更好地了解 CDI 的强大功能,让我们举一些例子。

示例 1 – POJO 的实例化

假设我们有一个 JSF 管理 Bean,它想要访问实现接口的 POJO 的一个实例。基本方法是使用new关键字在管理 Bean 中创建 POJO 的实例,如下面的代码所示:

@ManagedBean
public class MyManagedBean {

  IHelloWorld hw = new HelloWorld();

  public String getMyHelloWorld(){
    return hw.getHelloWorld();
  }
}
public class HelloWorld implements IHelloWorld{

  @Override
  public String getHelloWorld() {
    return "Hello World";
  }    
}

这种方法的缺点是HelloWorld类的实例是在硬编码中创建的,这导致管理 Bean 与IHeloWorld接口的实现之间有很强的耦合。因此,要更改IHelloWorld实现,你必须能够访问管理 Bean 并修改它。

使用 CDI,管理 Bean 只需声明对IHelloWorld实例的依赖并注入它。这给我们以下代码:

@ManagedBean
public class MyManagedBean {

    @Inject
    IHelloWorld hw;

    public String getMyHelloWorld(){
        return hw.getHelloWorld();
    }
}

CDI 将查找IHelloWorld接口的实现,实例化并注入它。更好的是,CDI 将负责管理将被注入的 Bean 的生命周期。因此,要更改IHelloWorld接口的实现,我们只需更改HelloWorld类。我们将通过使用@RequestScoped注解指定 POJO 的作用域来完成我们的代码。

@RequestScoped
public class HelloWorld implements IHelloWorld{

  //...    
}

示例 2 – 从 JSF 页面访问 EJB

假设我们有一个 JSF 页面,我们想要访问一个 EJB 组件的方法。典型的场景需要你首先从与 JSF 页面关联的管理 Bean 中访问 EJB 的一个实例,然后在 JSF 页面将要调用的管理 Bean 方法中调用 EJB 方法。以下代码展示了如何进行转换。

以下代码是一个 EJB 组件的示例:

@Stateless
public class MyEJB implements IMyEJB{

  public String getHelloWorld(){
    return "Hello world By EJB";
  }
}

以下代码是一个 JSF 管理 Bean 的示例:

@ManagedBean
public class MyManagedBean {

  @EJB
  IMyEJB ejb;

  public String getMyEjbHelloWorld(){
    return ejb.getHelloWorld();
  }
}

从 JSF 页面,我们可以调用myEjbHelloWorld方法。

Hello EJB
 <br/>
 The message : #{myManagedBean.myEjbHelloWorld}

使用 CDI,我们不一定需要通过管理 Bean 来访问 EJB 的方法。实际上,我们只需在我们的 EJB 组件上添加@Named注解,它就可以像简单的 JSF 管理 Bean 一样从我们的 JSF 页面访问。这两个注解(@Named@ManagedBean)之间的区别至少在两个点上很明显:第一个点是关于作用域的。确实,@ManagedBean注解是特定于 JSF 规范的,而@Named注解可以创建对更多规范(包括 JSF)可访问的管理 Bean,并在处理 JavaBean 组件方面提供更多灵活性。第二个点与组件可用的功能相关。@Named注解允许你创建 CDI Bean,这给你提供了使用你将无法在 JSF Bean 中访问的功能的机会,例如:拦截器、ProducerDisposer。一般来说,建议尽可能使用 CDI Bean。

以下代码展示了带有 CDI @Named注解的 EJB 组件:

@Named
@Stateless
public class MyEJB implements IMyEJB {
  //...
}

以下代码展示了从 JSF 页面访问 EJB 的方法:

CDI Hello EJB

<br/>
The message : #{myEJB.helloWorld}

示例 3 – 为简单操作设置具有特定作用域的 Bean

由于某种原因,你可能想实现单例模式。在传统方法中,即使你并不一定需要此类组件提供的服务(可伸缩性、基于角色的安全性、并发管理、事务管理等),你也将实现单例 EJB 类型。

使用 CDI,你可以创建具有所需作用域的 bean,而无需实现用于边际处理的重量级组件。实际上,CDI 提供了多种作用域类型,可以使用注解(@ApplicationScoped@RequestScoped@SessionScoped)来定义。因此,为了在不使 EJB 组件提供的服务杂乱无章的情况下实现单例模式,我们可以简单地使用 CDI 的应用程序作用域注解,如下面的代码所示:

@ApplicationScoped
public class MySingletonBean {
    //...
}

示例 4 – 使用通常由工厂创建的对象

你想通过 JMS 从 EJB 发送异步消息。经典方法将需要你实例化许多对象,如下面的代码所示:

@Stateless
public class SendMessageBean {

  @Resource(name = " java:global/jms/javaee7ConnectionFactory")
  private ConnectionFactory connectionFactory;
  @Resource(name = " java:global/jms/javaee7Queue")
  private Queue queue;

  public void sendMessage(String message) {
    try {
      Connection connection =connectionFactory.createConnection();
      Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
      MessageProducer messageProducer =session.createProducer(queue);
      TextMessage textMessage =session.createTextMessage(message);
      messageProducer.send(textMessage);
      connection.close();
    } catch (JMSException ex) {
      // handle exception (details omitted)
    }
  }
}

使用 CDI,所有这些代码量可以缩减到一行,如下面的代码所示:

@Stateless
public class SendMessageBean2 {

  @Inject
  JMSContext context;
  @Resource(lookup = "java:global/jms/javaee7Queue")
  Queue queue;

  public void sendMessage(String message) {
    context.createProducer().send(queue, message);
  }
}

CDI 规范的最新改进

从 Java EE 6 平台引入以来,CDI 已成为 Java EE 平台中面向组件编程的重要解决方案。现在它只需将其触角扩展到平台几乎所有的规范中,以便能够无缝地连接更多组件和集成更多 API。在已经做出的众多改进中,我们将介绍其中的一些,包括:避免 bean 被 CDI 处理的可能性、访问当前 CDI 容器、访问 bean 的非上下文实例,以及最终能够显式销毁 bean 实例的能力。关于拦截器和装饰器的 CDI 改进将在下一章中介绍,届时我们将讨论相关规范。

避免在 bean 上进行 CDI 处理

CDI 规范的 1.1 版本引入了 @vetoed 注解,该注解防止对象被视为 CDI bean。然而,带有此注解的 bean 不能拥有与上下文实例类似的生命周期。因此,它不能被注入。

通过查看这个注解,有些人可能会对其有用性感到疑惑。为了保持某些数据的一致性,可能需要控制某些组件的使用。但是,通过使用 CDI,你的组件可以从任何其他组件中被操作。因此,@vetoed 注解的作用。以下代码展示了在 Student 实体上使用 @vetoed 注解,以避免可能导致不一致的未知操作:

@Entity
@Vetoed
public class Student implements Serializable {
  @Id
  private String id;
  private String firstname;

  //...
}

访问 bean 的非上下文实例

此版本还增加了注入和执行未管理 bean 实例的生命周期回调的能力。以下代码演示了如何注入和执行 Student bean 非上下文实例的生命周期回调:

Unmanaged<Student> unmanagedBean = newUnmanaged<Student>(Student.class);
UnmanagedInstance<Student> beanInstance =unmanagedBean.newInstance();
Student foo =beanInstance.produce().inject().postConstruct().get();
// Usage of the injected bean
beanInstance.preDestroy().dispose();

访问当前 CDI 容器

CDI 规范 1.1 增加了通过程序访问当前 CDI 容器并执行一些操作的能力。以下代码演示了如何访问 CDI 容器以显式销毁上下文对象:

CDI container = CDI.current();
container.destroy(destroableManagedInstance);

显式销毁 CDI bean 实例

为了允许在应用程序中显式销毁 bean 实例,CDI 1.1 引入了AlterableContext接口,该接口包含void destroy(Contextual<?> contextual)方法。扩展应该实现此接口而不是Context接口。

摘要

在专注于使用 Java EE 7 平台实现完整系统的几个章节之后,本章让我们有机会休息一下,尝试回顾一些我们正在使用的关键概念。因此,我们学会了创建自己的注解并链接多层应用层。在下一章中,我们将继续通过集成来实施我们的应用程序,这次是集成不同层之间交换数据的验证。

第八章. 验证器和拦截器

在本章中,我们将看到使用约束进行数据验证。这将给我们机会将一小部分AOP面向方面编程)付诸实践,并发现验证和拦截 API 中的新特性。相关的规范包括:

  • Bean Validation 1.1

  • Interceptors 1.2

Bean Validation

Bean Validation 1.1 规范是在 JSR 349 下开发的。本节仅为您概述 API 的改进。完整的规范文档(更多信息)可以从jcp.org/aboutJava/communityprocess/final/jsr349/index.html下载。

我们几乎完成了我们的在线预注册应用程序的实现。在前几章中,我们开发了应用程序的不同层,现在我们需要验证将由该应用程序处理的数据。

验证您的数据

Java 语言为 Java SE 和 Java EE 开发者提供了 Bean Validation 规范,该规范允许我们表达对对象的约束。默认情况下,它提供了一小部分约束(与您可能的需求相比),称为内置约束(见下表)。但是,它为您提供了将这些约束组合起来的机会,以创建更复杂的约束(自定义约束),以满足您的需求。这正是其强大的原因。该规范可以与许多其他规范(如 CDI、JSF、JPA 和 JAX-RS)一起使用。

以下表格显示了 Bean Validation 1.1 中的内置约束列表:

约束 支持的类型 描述
@Null Object 这确保了对象值为 null
@NotNull Object 这确保了对象值不为 null
@AssertTrue boolean, Boolean 这确保了对象值为 true
@AssertFalse boolean, Boolean 这确保了对象值为 false
@Min BigDecimal, BigInteger byte, short, int, long以及相应的包装器(如ByteShort 这确保了对象值大于或等于注解中指定的值
@Max BigDecimal, BigInteger byte, short, int, long以及相应的包装器(如ByteShort 这确保了对象值小于或等于注解中指定的值
@DecimalMin BigDecimal, BigInteger, CharSequence byte, short, int, long以及相应的包装器(如ByteShort 这确保了对象值大于或等于注解中指定的值
@DecimalMax BigDecimal, BigInteger, CharSequence, byte, short, int, long 以及相应的包装器(如 ByteShort 这确保了对象的值小于或等于注解中指定的值
@Size CharSequence, Collection, Array, 和 Map 这确保了对象的尺寸在定义的范围内
@Digits BigDecimal, BigInteger, CharSequence, byte, short, int, long 以及相应的包装器(如 asByteShort 这确保了对象的值在定义的范围内
@Past java.util.Datejava.util.Calendar 这确保了对象中包含的日期早于处理日期
@Future java.util.Datejava.util.Calendar 这确保了对象中包含的日期晚于处理日期
@Pattern CharSequence 这确保了项目的值符合注解中定义的正则表达式

此规范的一个优点是能够通过注解定义它提供的不同约束,这有助于其使用。根据注解的特性(在第七章中详细解释,注解和 CDI),你可以为类、字段或属性表达约束。以下示例显示了一个带有内置约束的 Student 实体。你可以在以下代码中看到约束以避免空值或定义属性的尺寸和格式:

@Entity
public class Student implements Serializable {
  @Id
  @NotNull
  @Size(min = 1, max = 15)
  private String id;
  @Size(max = 30)
  private String firstname;
  @Pattern(regexp="^\\(?(\\d{3})\\)?[- ]?(\\d{3})[- ]?(\\d{4})$", message="Invalid phone/fax format,should be as xxx-xxx-xxxx")
  //if the field contains phone or fax number consider using this//annotation to enforce field validation
  @Size(max = 10)
  private String phone;
  @Pattern(regexp="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:a-z0-9?\\.)+a-z0-9?",message="Invalid email")
  //if the field contains email address consider using this//annotation to enforce field validation
  @Size(max = 60)
  @Email
  private String email;

  //...
}

一旦定义了约束,Bean Validation 规范允许你通过其他规范手动或自动地验证受约束的数据。我们首先介绍手动验证。以下示例演示了如何手动验证类的约束。我们必须说,Validator API 还提供了方法来验证单个属性或特定值,如下面的代码所示:

public static void main(String[] args) {
  Student student = new Student();
  student.setEmail("qsdfqsdfqsdfsqdfqsdfqsdf");
  student.setPhone("dfqsdfqsdfqsdfqsdfqsdfqsd");

  ValidatorFactory factory =Validation.buildDefaultValidatorFactory();
  Validator validator = factory.getValidator();

  Set<ConstraintViolation<Student>> violations =validator.validate(student);
  System.out.println("Number of violations : "+violations.size());
  for(ConstraintViolation<Student> cons : violations){
    System.out.println("Calss :"+cons.getRootBeanClass()+",Instance : "+cons.getLeafBean()+", "
     + " attribute : "+cons.getPropertyPath()+",message :"+cons.getMessage());
  }
}    

正如我们提到的,Bean Validation 规范可以与其他规范结合使用。在下面的示例中,我们展示了 Bean Validation 与 JSF 之间的耦合。我们借此机会强调自动验证。下面的示例演示了如何在我们的在线预注册网站上验证学生的输入:

@ManagedBean
public class InscriptionBean {
  @Size(min=4, message="The full name must have "+ " at least four characters!")
  private String name;
  @Past
  private Date birthday;
  @NotNull
  @Size(min=1, max=1,message="Enter only one character")
  private String gender;
  @Pattern(regexp="^\\(?(\\d{3})\\)?[- ]?(\\d{3})[- ]?(\\d{4})$", 
  message="Invalid phone format, should be as xxx-xxx-xxxx")
  @Size(max = 10)
  private String phone;
  @Pattern(regexp="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)"
  + "*@(?:a-z0-9?\\.)+a-z0-9?", 
  message="Invalid email")
  private String email;

  //...getter and setter
}

以下代码显示了一个网页内容的示例,允许候选人输入他们的个人身份信息。正如你所见,我们使用了在第三章中解释的通过属性,表示层,来使用 HTML5 的日历,并将 <h:message/> 标签放在具有相关字段 ID 的每个字段旁边,以便在违反约束的情况下显示错误信息。这使我们能够拥有以下截图所示的屏幕截图。

以下代码是identificationInformationPage.xml JSF 页面的内容示例:

<html 

  >
  <h:head>
    <title>Inscription information</title>
  </h:head>
  <h:body>
    <f:view>
      <h:form>
        <table border="0">                    
          <tbody>
            <tr>
              <th>Name :</th>
              <th><h:inputText value="#{inscriptionBean.name}"id="name"/></th>
              <th><h:message for="name" style="color:red"/></th>
            </tr>
            <tr>
              <td>Birthday :</td>
              <td><h:inputText pta:type="date"value="#{inscriptionBean.birthday}"  id="birth">
              <f:convertDateTime pattern="yyyy-MM-dd" />
              </h:inputText></td>
              <th><h:message for="birth" style="color:red"/></th>
            </tr>
            <tr>
              <td>Gender :</td>
              <td><h:inputText value="#{inscriptionBean.gender}"id="gender"/></td>
              <th><h:message for="gender" style="color:red"/></th>
            </tr>
            <tr>
              <td>Phone :</td>
              <td><h:inputText value="#{inscriptionBean.phone}"id="phone"/></td>
              <th><h:message for="phone" style="color:red"/></th>
            </tr>
            <tr>
              <td>Email :</td>
              <td><h:inputText value="#{inscriptionBean.email}"id="email"/></td>
              <th><h:message for="email" style="color:red"/></th>
            </tr>                        
          </tbody>
        </table>
        <p>
          <h:commandButton value="Submit" />
        </p>
      </h:form>
    </f:view>
  </h:body>
</html>

如以下截图所示,在提交条目时,表单的内容将自动进行验证,并将错误信息返回到表单中。因此,这种关联(JSF 和 Bean 验证)允许您在单个 bean 上定义约束,并用于多个表单。

验证的结果如下截图所示:

验证您的数据

构建自定义约束

在前面的例子中,我们想要有一个约束,可以确保Gender字段的值是大写的,但这个约束并不存在。为此,我们必须对正则表达式有一些了解,并使用@Pattern注解。这需要一些背景知识。幸运的是,我们有创建自定义约束的能力。我们将设置一个约束,允许我们执行这项任务。

创建新约束的过程基本上遵循创建简单注解的相同规则(如第七章 Annotations and CDI 中所述)。基本区别在于,在约束的情况下,我们不会实现处理器,而是实现验证器。也就是说,创建自定义约束包括以下步骤:创建约束注解和实现验证器。

创建约束注解

虽然目标是创建一个确保字符串字符首字母大写的约束,但我们将创建一个通用注解。这个注解将接受预期大小写的类型作为参数。因此,它可能在未来允许我们测试字符是大写还是小写。

我们将创建枚举CaseType,它包含不同的大小写类型,如下面的代码所示:

public enum CaseType {
    NONE,
    UPPER,
    LOWER
}

一旦我们定义了可能的大小写类型,我们将创建我们的注解并直接定义其特征。应该注意的是,除了我们在创建注解时看到的基线功能外,您还需要添加定义此约束验证器的@Constraint注解。对于其他功能,请参阅第七章 Annotations and CDI。以下是我们注解的代码:

@Target({ElementType.FIELD, ElementType.METHOD,ElementType.PARAMETER,ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CaseValidator.class)
public @interface Case {
  String message() default "This value must be uppercase";    
  CaseType type() default CaseType.UPPER;
  Class<? extends Payload>[] payload() default {};
  Class<?>[] groups() default {}; 
}

实现验证器

与需要用于简单注释的处理器不同,约束需要实现javax.validation.ConstraintValidator <A extends Annotation, T extends Object>接口,该接口提供了以下两个方法:

  • void initialize(A constraintAnnotation): 这个方法总是在处理约束之前被调用。它允许您初始化在isValid()方法执行期间将有用的参数。

  • boolean isValid(T value, ConstraintValidatorContext context): 此方法包含验证逻辑。

以下代码显示了我们的约束验证器:

public class CaseValidator implements ConstraintValidator<Case, String>{
  private CaseType type;

  public void initialize(Case annotation) {
    type = annotation.type();
  }

  public boolean isValid(String value,ConstraintValidatorContext context) {
  if(value == null)
    return true;        

    if (type == CaseType.UPPER) {
      return value.equals(value.toUpperCase());
    } else {
      return value.equals(value.toLowerCase());
    }
  }    
}

在创建您的验证器之后,您必须注册该服务(参见第七章,注解和 CDI)。然后,导入包含您的注解的包。以下截图显示了我们在其中定义注解的项目结构:

实现验证器

现在,我们只需要在String类型的属性上添加@Case (type = CaseType.UPPER),以确保值始终为大写字母。以下代码显示了之前展示的InscriptionBean Bean 代码中的更改:

@Case(type= CaseType.UPPER, message="This value must be uppercase")
private String gender;

结果简单而美丽,如下截图所示:

实现验证器

最新改进的实际应用

Bean Validation 1.1 规范文档的第二章介绍了本版本的重大变更。这些变更包括:开放性、支持依赖注入、更好地与 CDI 集成、支持方法和构造函数验证、支持分组转换,以及最终支持使用表达式语言进行消息插值。

开放性

Bean Validation 1.1 规范的实现被管理为一个开源项目。因此,API 的源代码、参考实现和测试兼容性套件对社区是可访问的。更多信息,请访问网站 beanvalidation.org

支持依赖注入和 CDI 集成

Bean Validation 1.1 规范已经标准化了在容器内部实现验证器所使用的对象的管理,并审查了提供给这些对象的所有服务。这有助于支持 Bean Validation 组件中的依赖注入并改善与 CDI 的集成。从现在起,我们可以使用@Resource@Inject注解注入ValidatorFactoryValidator类型的对象。以下示例演示了如何使用@Inject注解通过 Bean Validation 组件验证一个对象:

@Singleton
@Startup
public class InjectValidators {    
  private Logger logger =Logger.getLogger(InjectValidators.class.getName());

  @Inject
  private Validator validator;

  @PostConstruct
  public void init() {
    Student student = new Student();
    Set<ConstraintViolation<Student>> violations =validator.validate(student);
    logger.info("InjectValidators-Number of violations : " +violations.size());        
  }
}

支持方法和构造函数验证

Bean Validation 1.1 规范增加了在方法或构造函数的参数上定义约束的能力。它还允许定义方法返回值的约束。以下代码演示了如何声明和验证方法参数及其返回值的约束:

@Singleton
@Startup
public class ParameterConstraints {
  private Logger logger =Logger.getLogger(InjectValidators.class.getName());
  @Inject
  ExecutableValidator validator;

  @PostConstruct
  public void init() {                
    try {
      ParameterConstraints pc = new ParameterConstraints();
      Method method = ParameterConstraints.class.getMethod("createStudent", Student.class);
      Object[] params = {null};
      Set<ConstraintViolation<ParameterConstraints>>violations = validator.validateParameters(pc, method, params);

      logger.info("ParameterConstraints-Number of violations : " + violations.size());
    } catch (Exception ex) {
      Logger.getLogger(ParameterConstraints.class.getName()).log(Level.SEVERE, null, ex);
    } 
  }

  @Size(max = 2)
  public String createStudent(@NotNull Student std) {
    return "123456";
  }
}

支持分组转换

在级联数据验证时,可能会出现要验证的数据属于与请求组不同的组的情况。具体示例,考虑以下两个类StudentAddress

public class Student {
  @NotNull
  @Size(min = 1, max = 15)
  private String id;
  @Size(max = 30)
  private String firstname;
  @Size(max = 30)
  private String lastname;

  @Valid//To propagate the validation of a student object
  private Address address;

  //getter and setter
}

public class Address {
  @NotNull(groups=AddressCheck.class)    
  @Size(max = 10,groups=AddressCheck.class)
  private String phone;

  @NotNull(groups=AddressCheck.class)   
  @Email(groups=AddressCheck.class)
  private String email;    
  //getter and setter
}
public interface AddressCheck { }

为了逐步验证对象,Bean Validation 规范提出了组的概念。这让你能够定义一个可以单独验证的约束子集。默认情况下,验证约束属于 Default 组。如果在验证数据时没有指定验证组,则只检查 Default 组的约束。这解释了为什么 testDefaultGroup() 方法的代码将完全无误地运行。尽管 Address 类的电话和电子邮件属性不符合约束,但它们将不会被验证,仅仅是因为装饰它们的约束不是 Default 组的一部分。这可以从以下代码中看出:

public  void testDefaultGroup(){
  ValidatorFactory factory =Validation.buildDefaultValidatorFactory();
  Validator validator = factory.getValidator();

  Student student = new Student(); 
  student.setId("ST23576");
  student.setFirstname("Stelba");
  student.setLastname("estelle");
  student.setAddress(new Address());

  //Only the default group will be test. 
  Set<ConstraintViolation<Student>> constraintViolations =validator.validate(student);       
  assertEquals(0, constraintViolations.size());                
} 

因此,为了在验证 Student 对象的同时验证 Address 对象的属性,你有两种选择。第一种是将所有组在 validate() 方法中列出,如下代码所示:

Student student = new Student(); 
student.setId("ST23576");
student.setFirstname("Stelba");
student.setLastname("estelle");
student.setAddress(new Address());

Set<ConstraintViolation<Student>> constraintViolations =validator.validate(student, Default.class, AddressCheck.class); 
assertEquals(2, constraintViolations.size());  

第二种方法是使用 @ConvertGroup@ConvertGroup.List 的组转换概念进行多个转换。正如其名所示,这个特性让你能够从一个组转换到另一个组以验证约束属于不同于请求组的属性。以下代码展示了为了利用组转换功能,需要在 Student 类的 Address 属性上添加的更改:

@Valid//To propagate the validation of a student object
@ConvertGroup(from=Default.class, to=AddressCheck.class)
private Address address;

以下代码展示了使用 @ConvertGroup 注解后 Student 对象的联合验证属性和 Address 对象的属性。正如以下代码所示,我们不必列出所有约束组。

Student student = new Student(); 
student.setId("ST23576");
student.setFirstname("Stelba");
student.setLastname("estelle");
student.setAddress(new Address());
Set<ConstraintViolation<Student>> constraintViolations =validator.validate(student);       
assertEquals(2, constraintViolations.size());

以下代码展示了如何使用 @ConvertGroup.List 注解:

//annotation
@ConvertGroup.List({
  @ConvertGroup(from = Default.class, to = Citizen.class),
  @ConvertGroup(from = Salaried.class, to = Foreign.class)
})
List<Student> studentList;

支持使用表达式语言进行消息插值

在这个规范版本中,当定义错误消息时可以使用表达式语言。这有助于更好地格式化消息并在消息描述中使用条件。以下代码展示了在定义错误消息时可能使用表达式语言的示例:

public class Department implements Serializable {

  @Size(max = 30, message="A department must have at most {max}level${max > 1 ? 's' : ''}")
  private Integer nbrlevel;

  //...
}

拦截器

Interceptors 1.2 规范是在 JSR 318 下开发的。本节仅为您概述了 API 的改进。完整的文档规范(更多信息)可以从 jcp.org/aboutJava/communityprocess/final/jsr318/index.html 下载。

拦截某些进程

拦截器是 Java 机制,允许我们实现一些 AOP 的概念,从某种意义上说,它让我们能够将代码与诸如日志记录、审计和安全等横切关注点分离。因此,由于这个规范,我们可以拦截方法调用、生命周期回调事件和超时事件。

拦截器允许你拦截方法调用以及某些事件的爆发。在拦截过程中,你可以访问方法名、方法参数以及大量其他信息。也就是说,拦截器可以用来管理横切关注点,如日志记录、审计、安全(确保用户有执行方法的权限)以及修改方法参数。你可以在专用类或目标类中直接定义它们。

拦截器的签名如下:Object <method_name>(InvocationContext ctx) throws Exception { ... }void <method_name>(InvocationContext ctx) { ... }。它可以抛出Exception类型的异常,并且应该用定义它必须拦截的元素类型的注解来装饰。例如,使用@AroundInvoke来拦截方法,使用@AroundTimeout来拦截服务的定时器。如果没有使用这些注解,你总是可以利用 XML 配置。

在目标类中定义拦截器

以下代码展示了一个包含方法和定时服务拦截器的会话 bean。服务定时拦截器(targetClassTimerInterceptor)只进行日志记录,而方法拦截器(targetClassMethodInterceptor),除了进行一些日志记录外,还演示了如何访问和修改被拦截方法的参数。在这种情况下,我们检查候选人的名字是否以Sir开头,如果不是,则添加。

以下代码是一个包含拦截器的会话 bean 的示例:

@Stateless
public class StudentSessionBean {    

  private Logger logger = Logger.getLogger(
    "studentSessionBean.targetClassInterceptor");

  public Student createEntity(Student std){
    logger.info("createEntity-Name of the student :"+std.getFirstname());        
    return std;
  }

  @AroundInvoke
  public Object targetClassMethodInterceptor(InvocationContext ctx) throws Exception{
    logger.info("targetClassMethodInterceptor - method :"+ctx.getMethod().getName()+", "
    + "parameters : "+Arrays.toString(ctx.getParameters())+", date: "+new Date());
    if(ctx.getMethod().getName().equals("createEntity")){
      Student std = (Student) ctx.getParameters()[0];
      logger.info("targetClassMethodInterceptor -Name of student before : "+std.getFirstname());
      if(!std.getFirstname().startsWith("Sir")){
        std.setFirstname("Sir "+std.getFirstname());
      }
    }  
    return  ctx.proceed();
  }

  @Schedule(minute="*/2", hour="*")
  public void executeEvery2Second(){
    logger.info("executeEvery2Second - executeEvery5Second - date: "+new Date());
  }

  @AroundTimeout
  public Object targetClassTimerInterceptor(InvocationContext ctx) throws Exception{
    logger.info("targetClassTimerInterceptor - method :"+ctx.getMethod().getName()+", timer : "+ctx.getTimer());
    return  ctx.proceed();
  }
}

在拦截器类中定义拦截器

以下代码展示了一个可以作为拦截器使用的类。为了完成这个类,我们从StudentSessionBean类中提取了拦截器方法。正如你所见,这个类没有特殊的注解。但为了明确起见,你可以用javax.interceptor.Interceptor注解来装饰它(在我们的例子中,我们没有这样做,以展示这是可选的)。

public class MyInterceptor {
  private Logger logger = Logger.getLogger(
    "studentSessionBean.targetClassInterceptor");

  @AroundInvoke
  public Object targetClassMethodInterceptor(InvocationContext ctx) throws Exception{
    logger.info("targetClassMethodInterceptor - method :"+ctx.getMethod().getName()+", "
    + "parameters : "+Arrays.toString(ctx.getParameters())+", date:     "+new Date());
    if(ctx.getMethod().getName().equals("createEntity")){
      Student std = (Student) ctx.getParameters()[0];
      logger.info("targetClassMethodInterceptor - Name of studentbefore : "+std.getFirstname());
      if(!std.getFirstname().startsWith("Sir")){
        std.setFirstname("Sir "+std.getFirstname());
      }
    }  
    return ctx.proceed();
  }

  @AroundTimeout
  public Object targetClassTimerInterceptor(InvocationContext ctx)throws Exception{
    logger.info("targetClassTimerInterceptor - method :+ctx.getMethod().getName()+", timer : "+ctx.getTimer());
    return  ctx.proceed();
  }
}

以下代码展示了如何声明一个拦截器类以拦截给定类的某些过程。结果与前面代码中展示的StudentSessionBean类的情况相同。

@Interceptors(MyInterceptor.class)
@Stateless
public class StudentSessionBeanWithoutInterceptor {
    private Logger logger = Logger.getLogger(
            "studentSessionBean.targetClassInterceptor");

     @Schedule(minute="*/2", hour="*")
    public void executeEvery2Second(){
        logger.info("executeEvery2Second - executeEvery5Second - date : "+new Date());
    }

     public Student createEntity(Student std){
        logger.info("createEntity-Name of the student : "+std.getFirstname());        
        return std;
    } 
}

最新改进在行动中

对于添加到拦截器 1.2 规范的所有新功能,最重要的是:为构造函数添加生命周期回调拦截器、添加用于管理拦截器执行顺序的标准注解,以及最后,将拦截器绑定从 CDI 规范转移到拦截器规范 1.2。

拦截构造函数调用

由于@AroundConstruct注解,你可以定义一个在目标实例创建之前运行的拦截器,以拦截目标实例构造函数的执行。带有此注解的拦截器方法不应定义在目标类中。

以下代码演示了如何使用@AroundConstruct。示例是记录不同方法被调用的时刻,以确保@AroundConstruct方法确实在构造函数之前运行。它还展示了如何访问构造函数的名称及其参数。

public class AroundConstructInterceptor {
  private Logger logger = Logger.getLogger("AroundConstructInterceptor.interceptorClass");    

  @AroundConstruct
  public Object initialize(InvocationContext ctx) throws Exception{
    logger.info("initialize - constructor :"+ctx.getConstructor()+", "
    + "parameters : "+Arrays.toString(ctx.getParameters())+","
    + " execution time : "+new Date());
    return ctx.proceed();
  }    
}

@Stateless
@Interceptors(AroundConstructInterceptor.class)
public class AroundConstructBean  {

  private Logger logger = Logger.getLogger("AroundConstructManagedBean.interceptorClass");

  public AroundConstructBean(){     
    logger.info("AroundConstructManagedBean - Execution time :"+new Date());
  }    
}

使用拦截器绑定将拦截器与类关联

使用拦截器绑定关联拦截器,来自拦截器 1.2 规范文档的第三章,是从CDI 规范文档的第九章中提取出来的。它讨论了使用注解将拦截器与另一个非拦截器的组件关联的可能性。为了实现这一点,你必须:创建拦截器绑定类型,声明拦截器绑定,并将此拦截器绑定到所需的组件上。

创建拦截器绑定类型

拦截器绑定类型的创建方式与简单的注解完全相同,只是在定义拦截器绑定特性的注解中添加了至少一个@InterceptorBinding。以下代码展示了如何声明一个用于记录一些信息的拦截器绑定类型:

@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {    
}

声明拦截器绑定

我们可以通过使用拦截器绑定类型和@javax.interceptor.Interceptor注解来声明拦截器绑定。以下代码演示了如何声明拦截器绑定:

@Log
@Interceptor
public class LoggerInterceptor {
  // Interceptors methods
}

使用拦截器绑定类型绑定拦截器

经过所有这些操作后,你需要使用拦截器绑定类型来装饰一个非拦截器组件,以便将拦截器绑定到组件上。以下代码演示了如何将LoggerInterceptor拦截器绑定到我们的 EJB 上:

@Stateless
@Log
public class StudentSessionBeanWithoutInterceptor {
    //Method to intercept
}

默认情况下,拦截器是禁用的。为了做到这一点,你必须像以下这样在bean.xml文件中声明拦截器:

<interceptors>
    <class>com.packt.ch08.bean.LoggerInterceptor</class>
</interceptors>

定义拦截器的执行顺序

当我们在第七章中讨论 CDI 规范时,注解和 CDI,我们讨论了添加@Priority注解。这个注解被拦截器 1.2 规范所采用,并允许我们为使用拦截器绑定声明的拦截器定义执行顺序。当使用这个注解时,具有最小优先级的拦截器首先被调用。以下代码演示了如何使用这个注解。在我们的例子中,LoggerInterceptor拦截器将在LoggerInterceptor1拦截器之前被调用。

@Log
@Interceptor
@Priority(2000)
public class LoggerInterceptor {
    // interceptor method
}

@Log1
@Interceptor
@Priority(2050)
public class LoggerInterceptor1 {
  //Interceptor method
}

@Stateless
@Log1
@Log
public class StudentSessionBeanWithoutInterceptor {
    //Methods to intercept
}

与此同时,该注解允许我们启用拦截器。换句话说,它让你免于在bean.xml文件中使用<interceptors>元素,就像我们在前面的例子中所做的那样。

概述

在本章结束时,我们现在能够通过 Bean Validation 规范验证 JSF 表单的输入以及应用程序将要操作的数据。我们还学习了如何拦截不同类型的流程,例如对象的创建、方法的调用或服务定时器的执行,以便审计或修改方法的参数。在下一章中,我们将通过解决我们在线预注册应用程序的安全方面,结束我们对 Java EE 7 世界的探索之旅。

第九章。安全

我们将通过使用 Java EE 解决方案来保护我们的项目来完成我们的项目。但首先,我们将分析相关 API 的改进。本章的开发将专注于 JASPIC 1.1。

JASPIC 1.1

Java 容器认证 SPIJASPIC)规范是在 JSR 196 下开发的。本节仅为您概述 API 的改进。有关更多信息,可以下载完整的规范文档,链接为jcp.org/aboutJava/communityprocess/final/jsr349/index.html

表单的安全访问

也称为 JASPI,JASPIC 规范定义了一组标准接口,用于开发认证模块,允许安全访问网络资源(Servlets、JSP 等)。一般来说,JASPIC 规范是为了消息级安全而设计的;这意味着 JASPIC 模块被要求集成到消息处理容器中,从而为 SOAP 和 HttpServlet 等协议提供透明的安全机制。

实现认证模块

在您不想使用预定义的认证模块的情况下,JASPIC 规范允许您开发自己的模块。这需要实现javax.security.auth.message.module.ServerAuthModule接口。由于我们将在后面解释的原因,您可能需要实现以下接口:

  • javax.security.auth.message.config.ServerAuthConfig

  • javax.security.auth.message.config.ServerAuthContext

  • javax.security.auth.message.config.AuthConfigProvider

实现 ServerAuthModule 接口

ServerAuthModule接口包含五个必须由认证模块实现的方法。这些方法如下:

  • initialize(): 此方法用于初始化模块并检索验证资源访问所需的必要对象。

  • getSupportedMessageTypes(): 此方法返回一个对象数组,指定模块支持的消息类型。例如,对于将兼容 Servlet 容器配置的模块,返回的数组将包含HttpServletRequest.classHttpServletResponse.class对象。

  • validateRequest(): 当容器收到一个HttpServletRequest以处理传入的消息时,会调用此方法。为此,它从容器接收HttpServletRequestHttpServletResponse对象作为MessageInfo参数。在请求处理结束时,此方法必须返回一个状态,以确定容器中的操作顺序。

  • secureResponse(): 当容器在向客户端返回响应时调用此方法。通常,它应该返回状态SEND_SUCCESS

  • cleanSubject(): 此方法用于从主题参数中删除一个或多个原则。

以下代码提供了一个ServerAuthModule接口方法的示例实现:

public class ServerAuthModuleImpl implements ServerAuthModule {

    private MessagePolicy requestPolicy;
    private CallbackHandler handler;
    public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler handler, Map options) throws AuthException {
        this.requestPolicy = requestPolicy;
        this.handler = handler;
    }

    public Class[] getSupportedMessageTypes() {
        return new Class[]{HttpServletRequest.class, HttpServletResponse.class};
    }

    public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException {
        try {

            String username = validation(messageInfo, clientSubject);
            if (username == null && requestPolicy.isMandatory()) {

                HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage();

                HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage();

                String header = "Basic" + " realm=\"" + request.getServerName() + "\"";
                response.setHeader("WWW-Authenticate", header);

                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return AuthStatus.SEND_CONTINUE;
            }

            handler.handle(new Callback[]{
                new CallerPrincipalCallback(clientSubject, username)});
            if (username != null) {
                messageInfo.getMap().put("javax.servlet.http.authType", "ServerAuthModuleImpl");
            }

            return AuthStatus.SUCCESS;
        } catch (Exception e) {
            e.printStackTrace();
            throw new AuthException(e.getMessage());
        }
    }

    public String validation(MessageInfo mInfo, Subject cSubject) throws Exception {
        HttpServletRequest request = (HttpServletRequest) mInfo.getRequestMessage();

        String headerAutho = request.getHeader("authorization");

        if (headerAutho != null && headerAutho.startsWith("Basic")) {

            headerAutho = headerAutho.substring(6).trim();

            String decodedAutho = new String(Base64.decode(headerAutho.getBytes()));

            int colon = decodedAutho.indexOf(':');
            if (colon <= 0 || colon == decodedAutho.length() - 1) {
                return null;
            }

            String username = decodedAutho.substring(0, colon);
            String password = decodedAutho.substring(colon + 1);

            //Container password validation, you can put your
            //own validation process instead of delegating it to the container
            PasswordValidationCallback pwdValidCallback =
                    new PasswordValidationCallback(cSubject, username, password.toCharArray());

            handler.handle(new Callback[]{pwdValidCallback});
            //Removes the stored password
            pwdValidCallback.clearPassword();
            password = null;

            if (pwdValidCallback.getResult()) {//if the user is authenticated
                return username;
            }
        }
        return null;
    }

    public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException {
        return AuthStatus.SEND_SUCCESS;
    }

    public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException {
    }
}

安装和配置认证模块

通过将模块的 JAR 文件复制到 GlassFish 服务器install_glassfish\ glassfish\domains\domain1\lib目录中来安装认证模块。

一旦模块安装完成,您可以在 GlassFish 管理控制台中按照以下方式配置它:

  1. 登录到 GlassFish 管理控制台。

  2. 展开server-config菜单。

  3. 在出现的菜单中,展开Security菜单。

  4. 在子菜单中,展开消息安全菜单。

  5. 点击HttpServlet菜单。

  6. 在出现的表单中,点击Providers标签以添加一个新的提供者。

  7. 点击New按钮并填写适当的表单。在记录您的输入之前,您的表单应如下截图所示:安装和配置认证模块

将认证模块绑定到 Web 应用

在 GlassFish 中,要将认证模块绑定到应用,您有两个选项:

  • 第一个选项(到目前为止这是最简单的)是在应用的glassfish-web.xml文件中配置glassfish-web-app元素的httpservlet-security-provider属性。此配置的目的是让您使用 GlassFish 提供的AuthConfigProvider实现来实例化您的安全模块。以下代码显示了我们的应用glassfish-web.xml文件的内容。如您所见,我们将提供者的 ID 传递给了httpservlet-security-provider属性。因此,每当需要分析请求的安全性时,GlassFish 服务器将通过其AuthConfigProvider实现来实例化我们的安全模块,以便使其可用。

    <glassfish-web-app error-url=""  httpservlet-security-provider="AuthModule1">
      <class-loader delegate="true"/>  
    </glassfish-web-app>
    
  • 第二种方法是实现AuthConfigProvider接口的自定义实现。因此,在这种情况下,您需要实现javax.security.auth.message.config.ServerAuthConfigjavax.security.auth.message.config.ServerAuthContextjavax.security.auth.message.config.AuthConfigProvider接口。对于那些对这次冒险感到兴奋的人,您将在这个博客中找到所有必要的信息:arjan-tijms.blogspot.com/2012/11/implementing-container-authentication.html

创建一个域

我们将告诉 GlassFish 服务器所有可以访问我们应用安全部分的关联用户和组存储在哪里。换句话说,我们将配置我们应用的域。

为了您的信息,GlassFish 提供了定义多种类型域的能力。它们如下列出:

  • file域,用于在文件中存储用户信息。这是默认域。

  • ldap域,用于在 LDAP 目录服务器中存储。

  • jdbc域,用于在数据库中存储。

  • solaris 区域,用于基于 Solaris 用户名和密码的认证管理。

  • certificate 区域,用于使用证书进行认证管理。

  • 如果这些区域中没有满足您需求的,请不要担心;GlassFish 提供了创建您自己的区域的可能性。

在我们的案例中,我们选择了 jdbc 区域;我们需要一个数据库结构来存储必要的信息(用户名、密码以及它所属的组)。以下截图显示了存储我们信息的表结构:

创建区域

realm_users 表将存储所有用户 ID 和密码,realm_groups 表将存储所有应用程序的组 ID 及其描述,而 users_groups 表将告诉我们用户属于哪些组。因此,一个用户可以属于多个组。

一旦您定义了将托管不同用户的数据库结构,您必须配置 GlassFish 以使其能够连接到您的数据库(在我们的案例中是 MySQL 5)并访问认证信息。为此,您必须首先将数据库的 Java 连接器(在我们的案例中是 mysql-connector-java-5.1.23-bin.jar)复制到目录:glassfish_install_dir\glassfish\domains\domain1\lib。然后,您必须连接到 GlassFish 管理控制台,通过导航到 配置 | 服务器配置 | 安全 | 区域 来获取创建区域的表单。通过点击 区域 菜单,将显示以下表单;然后您需要点击 新建 按钮,区域创建表单将出现:

创建区域

以下表格显示了您需要为 JDBCRealm 填写的字段:

字段 示例值 描述
名称 MyJDBCRealm 将用于配置应用程序安全性的区域名称
类名 com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm 实现要配置的区域(在我们的案例中为 JDBCRealm)的类
JAAS 上下文 jdbcRealm JAAS(Java 认证和授权服务)上下文 ID
JNDI jdbcRealmDataSource 连接到包含区域的数据库的 JDBC 资源的 JNDI 名称
用户表 realm_users 包含带有密码的系统用户列表的表名称
用户名列 USERID 包含用户在 realm_users 表中 ID 的列名称
密码列 PASSWORD 包含用户密码的列名称
组表 users_groups 关联组和用户的表名称
组表用户名列 USERID 关联表中包含用户 ID 的列名称
组名列 GROUPID 关联表中包含组标识符的列的名称
密码加密算法 SHA-256 设置密码加密算法
摘要算法 SHA-256(即使它是默认值)

填写表单后,您可以保存您的领域。使用此配置,我们现在可以使用容器提供的密码验证机制来验证传入的连接。这就是我们在验证方法中使用以下语句所做的事情:

PasswordValidationCallback pwdValidCallback = new PasswordValidationCallback(cSubject, username, password.toCharArray());

除了使用容器验证机制外,您还可以访问您的数据库并自行进行此验证。

安全配置

要配置应用程序的安全性,您需要执行以下操作:

  1. 确定应用程序的不同角色并在 web.xml 中声明它们。在我们的应用程序中,我们只需要一个管理员角色来执行批量处理和一些管理任务。以下代码演示了如何为此目的创建一个名为 admin 的角色:

    <security-role>
       <role-name>admin</role-name>
    </security-role>
    
  2. web.xml 文件中将 URL 模式映射到适当的角色。这将定义每个角色可以访问的表单。在执行此配置之前,您必须根据您想要定义的访问约束对表单进行分组。在我们的应用程序中,我们将表单分为两个文件夹:注册文件夹中的预注册表单文件夹和行政文件夹中的管理表单文件夹。因此,为了确保只有管理员角色的用户才能访问管理表单,我们将 URL 模式 /faces/administration/* 关联到 admin 角色。以下代码演示了如何定义一个将 URL 模式 /faces/administration/* 关联到 admin 角色的约束(前一个模式中的 faces 单词代表 web.xml 文件中的 <servlet-mapping> 元素中定义的模式)。

    <security-constraint>
            <display-name>Constraint1</display-name>
            <web-resource-collection>
                <web-resource-name>Administration</web-resource-name>
                <url-pattern>/faces/administration/*</url-pattern>
            </web-resource-collection>
            <auth-constraint>
                <role-name>admin</role-name>
            </auth-constraint>
    </security-constraint>
    
  3. glassfish-web.xml 文件中将每个角色与用户组关联。在领域内,每个用户都与一个用户组关联。然而,URL 模式与角色关联。因此,您需要告诉服务器一个角色属于哪个组。在 GlassFish 中,这可以通过 <security-role-mapping> 元素实现。以下代码显示了包含角色-组组合的 glassfish-web.xml 文件的完整内容:

    <glassfish-web-app error-url="" httpservlet-security-provider="AuthModule1">
      <security-role-mapping>
        <role-name>admin</role-name>
        <group-name>administrator</group-name>
      </security-role-mapping>
      <class-loader delegate="true"/>
    </glassfish-web-app>
    
  4. web.xml 中声明我们的应用程序将使用的领域和认证类型。以下代码演示了如何声明我们在上一步中创建的 MyJDBCRealm。我们选择的认证类型是 DIGEST。它以加密形式传输密码。

    <login-config>
        <auth-method>DIGEST</auth-method>
        <realm-name>MyJDBCRealm</realm-name>
    </login-config>
    

完成这些配置后,候选人可以无任何问题地访问注册表单。但是,如果他们尝试连接到管理表单,将显示类似于以下窗口的窗口:

安全配置

在完成这个项目之前,你应该知道可以自定义登录屏幕,甚至将其集成到你的应用程序中。URL:blog.eisele.net/2013/01/jdbc-realm-glassfish312-primefaces342.html上的教程可以帮助你。

最新改进措施

JASPIC 规范维护发布 B 版本已经做出了一些重大更改,其中一些有助于标准化无论服务器如何使用规范;其他有助于丰富用户体验。在更改中,我们只介绍了一些相对重要的更改,并建议你浏览以下规范文档和博客:arjan-tijms.blogspot.com/2013_04_01_archive.html,它将为你提供更多信息。

集成调用的认证、登录和注销方法

自 Servlet 3.0 版本以来,认证、登录和注销方法已添加到HttpServletRequest接口中,用于以编程方式管理登录和注销。然而,在调用这三个方法之一后,JASPIC 模块的行为并没有明确确定。它留给了服务器供应商来提供他们自己的登录和注销方法。直接后果是 Java EE 兼容服务器之间应用程序的非可移植性。

在最近的变化中,JASPIC 1.1 版本明确定义了在调用这三个方法之一后 JASPIC 模块的预期行为。我们现在知道:

  • login方法的容器实现必须在login方法和配置的认证机制之间存在不兼容性时抛出ServletException

    注意

    在这里,调用login方法后模块的行为没有明确定义。

  • authenticate方法的调用必须调用validateRequest方法。如果authenticate方法不是在调用validateRequest的环境中调用,则这是正确的。

  • logout方法的调用必须调用cleanSubject方法。如果logout方法不是在调用cleanSubject方法的环境中调用,则这是正确的。

标准化访问应用上下文标识符

应用上下文标识符是一个用于识别或选择给定应用程序中的AuthConfigProviderServerAuthConfig对象的 ID(它包含在appContext参数中)。在 JASPIC 1.1 之前,没有标准的方式来获取它。通常,每个服务器供应商都提出了一个特定于供应商的方法。现在,使用以下代码可以在标准中实现:

ServletContext context = ...
 //...
String appContextID = context.getVirtualServerName() + " " + context.getContextPath();

支持前进和包含机制

JASPIC 1.1 规范强调,认证模块必须在处理validateRequest方法的过程中能够转发和包含。具体来说,这可以通过在MessageInfo参数类型中使用requestresponse来实现。以下代码概述了基于条件结果的重定向到错误页面的过程:

public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException {

    HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage();
    HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage();

    try{
      if(...)
        request.getServletContext().getRequestDispatcher("specificErrorPage")
               .forward(request, response);
    }catch(Exception ex){}

    return SEND_CONTINUE;
}

摘要

在达到本书的最后一章,也就是本章的结尾时,我们现在能够部署一个至少具备一定安全级别的 Java EE 公共解决方案。实际上,通过本章,读者已经了解到一个允许他们限制表单访问的规范。然而,重要的是要注意,鉴于本书的目标,我们仅仅处理了安全性的一个很小方面。我们要求您通过额外的阅读来完善您对安全性的知识。这是因为该领域由多个方面组成,例如数据在网络中的传输、方法执行、构建和执行 SQL 查询。

posted @ 2025-09-10 15:08  绝不原创的飞龙  阅读(17)  评论(0)    收藏  举报