JavaEE8-秘籍-全-

JavaEE8 秘籍(全)

原文:zh.annas-archive.org/md5/68a4f8945a727a4e0aca3f4ea0fac1de

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Java EE 是一个成熟且在全球范围内广泛使用的平台。它也是一个通过个人、供应商、团队领导者和社区的努力而演变的行业标准。它拥有一个完整的市场和生态系统,拥有数百万用户,这也意味着一个庞大且活跃的社区,他们总是愿意帮助它向前发展。

由于这些原因,本书的目的是满足那些依赖 Java EE 提供真正出色企业解决方案的专业人士的需求,不仅讨论针对实际问题的真实解决方案,还展示如何以实际的方式去实现。

本书从对 Java EE 及其在版本 8 中的改进的快速概述开始,然后带你通过最重要的 API 进行实战之旅。

你将学习如何使用 Java EE 进行服务器端开发、网络服务和网络应用。你还将了解如何正确提高你企业解决方案的安全性。

如果 Java EE 应用不遵循标准,那么它就不够好,为此,你可以依赖 Java EE 应用服务器。本书将教你如何使用市场上最重要的服务器,并为你项目获取它们所能提供的最佳服务。

从架构的角度来看,本书将涵盖微服务、云计算和容器。同时,它也不会忘记为你提供所有构建反应式 Java EE 应用所需的工具,不仅包括 Java EE 特性,还包括 Java 核心特性,如 lambda 和 CompletableFuture。

整个 Java 世界都是关于社区的,因此我们还将向你展示社区驱动的专业人士如何提高他们项目的结果,甚至在他们的事业中达到更高的水平。

本书基于一个我称之为“让 Java EE 专业人士远离伟大项目的五大错误”的概念。当我没有做以下事情时,我正在毁掉我的职业生涯:

  • 保持自己与时俱进

  • 了解 API(所有 API 的概述以及掌握最重要的 API)

  • 了解最常用的 Java EE 应用服务器

  • 了解高级架构

  • 分享我所知

因此,本书是针对这些错误的一个直接、实用且有帮助的解决方案。我可以自信地说,正确处理这些问题可以改变世界各地许多开发者的职业生涯和生活。我知道,因为它们已经改变了我,而且是永久性的。

本书面向的对象

本书是为那些希望学习如何使用 Java EE 8 满足实际企业应用需求的开发者而编写的。他们应该熟悉应用开发,并需要具备至少基本的 Java 知识、云计算的基本概念以及网络服务知识。

读者应该希望学习如何在一个安全且快速解决方案中结合多个 API,为此,他们需要了解 API 是如何工作的以及何时使用每个 API。

为了充分利用本书

读者应该熟悉应用开发,并且需要至少具备 Java 的基本知识。还假定读者对云计算和 Web 服务有基本了解。

下载示例代码文件

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

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载与勘误表”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

书籍的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Java-EE-8-Cookbook。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包可供选择,请访问github.com/PacktPublishing/。查看它们!

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“然后从SseResource中取出两个关键方法。”

代码块设置如下:

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>

任何命令行输入或输出都应如下编写:

Info:   destroy

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“现在让我们转到“附加属性”部分。”

警告或重要注意事项看起来像这样。

小技巧和窍门看起来像这样。

部分

在本书中,您会发现一些经常出现的标题(准备就绪如何操作...它是如何工作的...还有更多...参见)。

为了清楚地说明如何完成食谱,请按照以下方式使用这些部分:

准备就绪

本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何操作...

本节包含遵循食谱所需的步骤。

它是如何工作的...

本节通常包含对上一节发生情况的详细解释。

更多内容...

本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。

参见

本节提供了其他对食谱有用的信息链接。

联系我们

我们欢迎读者的反馈。

一般反馈:请将邮件发送至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com发送邮件给我们。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评价

请留下您的评价。一旦您阅读并使用过这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,并且我们的作者可以查看他们对书籍的反馈。谢谢!

想了解更多关于 Packt 的信息,请访问packtpub.com

第一章:新功能和改进

Java EE 8 是一个重大的版本,全球社区期待了大约四年。现在,整个平台比以往任何时候都更加健壮、成熟和稳定。

本章将介绍我们可以突出显示的 Java EE 8 的主要 API。虽然它们并不是这个版本涵盖的唯一主题——远非如此——但在企业环境中它们起着重要作用,值得仔细研究。

在本章中,我们将介绍以下食谱:

  • 运行你的第一个 Bean Validation 2.0 代码

  • 运行你的第一个 CDI 2.0 代码

  • 运行你的第一个 JAX-RS 2.1 代码

  • 运行你的第一个 JSF 2.3 代码

  • 运行你的第一个 JSON-P 1.1 代码

  • 运行你的第一个 JSON-B 1.0

  • 运行你的第一个 Servlet 4.0 代码

  • 运行你的第一个 Security API 1.0

  • 运行你的第一个 MVC 1.0 代码

运行你的第一个 Bean Validation 2.0 代码

Bean Validation 是一个 Java 规范,基本上帮助您保护您的数据。通过其 API,您可以验证字段和参数,使用注解表达约束,并扩展您自定义的验证规则。

它可以与 Java SE 和 Java EE 一起使用。

在这个食谱中,您将一瞥 Bean Validation 2.0。无论您是初学者还是已经在使用 1.1 版本,这些内容都将帮助您熟悉其一些新功能。

准备工作

首先,您需要将正确的 Bean Validation 依赖项添加到您的项目中,如下所示:

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <version>1.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.8.Final</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>3.0.1-b10</version>
        </dependency>
</dependencies>

如何做到这一点...

  1. 首先,我们需要创建一个包含一些待验证字段的对象:
public class User {

    @NotBlank
    private String name;

    @Email
    private String email;

    @NotEmpty
    private List<@PositiveOrZero Integer> profileId;

    public User(String name, String email, List<Integer> profileId) {
        this.name = name;
        this.email = email;
        this.profileId = profileId;
    }
}
  1. 然后,我们创建一个test类来验证这些约束:
public class UserTest {

    private static Validator validator;

    @BeforeClass
    public static void setUpClass() {
        validator = Validation.buildDefaultValidatorFactory()
        .getValidator();
    }

    @Test
    public void validUser() {
        User user = new User(
            "elder", 
            "elder@eldermoraes.com", 
            asList(1,2));

            Set<ConstraintViolation<User>> cv = validator
            .validate(user);
            assertTrue(cv.isEmpty());
    }

    @Test
    public void invalidName() {
        User user = new User(
            "", 
            "elder@eldermoraes.com", 
            asList(1,2));

            Set<ConstraintViolation<User>> cv = validator
            .validate(user);
            assertEquals(1, cv.size());
    }

    @Test
    public void invalidEmail() {
        User user = new User(
        "elder", 
        "elder-eldermoraes_com", 
        asList(1,2));

        Set<ConstraintViolation<User>> cv = validator
        .validate(user);
        assertEquals(1, cv.size());
    } 

    @Test
    public void invalidId() {
        User user = new User(
            "elder", 
            "elder@eldermoraes.com", 
            asList(-1,-2,1,2));

            Set<ConstraintViolation<User>> cv = validator
            .validate(user);
            assertEquals(2, cv.size());
    } 
}

它是如何工作的...

我们的User类使用了 Bean Validation 2.0 引入的三个新约束:

  • @NotBlank: 确保值不是 null、空或空字符串(在评估之前会修剪值,以确保没有空格)。

  • @Email: 只允许有效的电子邮件格式。忘记那些疯狂的 JavaScript 函数吧!

  • @NotEmpty: 确保列表至少有一个项目。

  • @PositiveOrZero: 保证一个数字等于或大于零。

然后我们创建一个test类(使用 JUnit)来测试我们的验证。它首先实例化Validator

@BeforeClass
public static void setUpClass() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
}

Validator是一个 API,根据为它们定义的约束来验证 bean。

我们第一个test方法测试了一个有效的用户,它是一个具有以下属性的User对象:

  • 名称不能为空

  • 有效的电子邮件

  • profileId列表只包含大于零的整数:

User user = new User(
   "elder", 
   "elder@eldermoraes.com", 
   asList(1,2));

最后,进行验证:

Set<ConstraintViolation<User>> cv = validator.validate(user);

Validatorvalidate()方法返回找到的约束违规集(如果有),如果没有违规,则返回一个空集。

因此,对于一个有效的用户,它应该返回一个空集:

assertTrue(cv.isEmpty());

其他方法与此模型略有不同:

  • invalidName(): 使用一个空名称

  • invalidEmail(): 使用一个格式错误的电子邮件

  • invalidId(): 向列表中添加一些负数

注意,invalidId()方法将两个负数添加到列表中:

asList(-1,-2,1,2));

因此,我们预计会有两个约束违规:

assertEquals(2, cv.size());

换句话说,Validator 不仅检查违反的约束,还检查它们被违反的次数。

参见

运行您的第一个 CDI 2.0 代码

上下文和依赖注入CDI)无疑是 Java EE 平台最重要的 API 之一。在 2.0 版本中,它也支持 Java SE。

现在,CDI 对 Java EE 平台上的许多其他 API 都产生了影响。正如在 Java EE 8 – The Next Frontier 项目的一次采访中所说:

“如果我们在创建 JSF 的时候就有 CDI,它将会变得完全不同。”

– Ed Burns,JSF 规范负责人

CDI 2.0 中有很多新特性。本菜谱将涵盖观察者排序,让您快速入门。

准备工作

首先,您需要将正确的 CDI 2.0 依赖项添加到您的项目中。为了简化这一点,我们将使用 CDI SE,这是一个允许您在没有 Java EE 服务器的情况下使用 CDI 的依赖项:

<dependency>
    <groupId>org.jboss.weld.se</groupId>
    <artifactId>weld-se-shaded</artifactId>
    <version>3.0.0.Final</version>
</dependency>

如何做到...

本菜谱将向您展示 CDI 2.0 引入的主要功能之一:有序观察者。现在,您可以将观察者的工作变成可预测的:

  1. 首先,让我们创建一个要观察的事件:
public class MyEvent {

    private final String value;

    public MyEvent(String value){
        this.value = value;
    }

    public String getValue(){
        return value;
    }
}
  1. 现在,我们构建我们的观察者和将触发它们的服务器:
public class OrderedObserver {

    public static void main(String[] args){
        try(SeContainer container =    
        SeContainerInitializer.newInstance().initialize()){
            container
                .getBeanManager()
                .fireEvent(new MyEvent("event: " + 
                System.currentTimeMillis()));
        }
    }

    public void thisEventBefore(
            @Observes @Priority(Interceptor.Priority
            .APPLICATION - 200) 
            MyEvent event){

        System.out.println("thisEventBefore: " + event.getValue());
    }

    public void thisEventAfter(
            @Observes @Priority(Interceptor.Priority
           .APPLICATION + 200) 
            MyEvent event){

        System.out.println("thisEventAfter: " + event.getValue());
    }  
}

此外,别忘了将 beans.xml 文件添加到 META-INF 文件夹中:

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

       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
       http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="all">
</beans>
  1. 一旦运行,您应该看到如下结果:
INFO: WELD-ENV-002003: Weld SE container 
353db40d-e670-431d-b7be-4275b1813782 initialized
 thisEventBefore: event -> 1501818268764
 thisEventAfter: event -> 1501818268764

它是如何工作的...

首先,我们正在构建一个服务器来管理我们的事件和观察者:

public static void main(String[] args){
    try(SeContainer container =  
    SeContainerInitializer.newInstance().initialize()){
        container
            .getBeanManager()
            .fireEvent(new ExampleEvent("event: " 
            + System.currentTimeMillis()));
    }
}

这将为我们提供运行菜谱所需的所有资源,就像它是一个 Java EE 服务器一样。

然后我们构建一个观察者:

public void thisEventBefore(
        @Observes @Priority(Interceptor.Priority.APPLICATION - 200) 
        MyEvent event){

    System.out.println("thisEventBefore: " + event.getValue());
}

因此,我们有三个重要的话题:

  • @Observes:此注解用于告诉服务器它需要观察使用 MyEvent 触发的事件

  • @Priority:此注解通知观察者需要运行的优先级顺序;它接收一个 int 参数,执行顺序是升序的

  • MyEvent event:正在观察的事件

thisEventBefore 方法和 thisEventAfter 中,我们只更改了 @Priority 的值,服务器负责按正确的顺序运行它。

还有更多...

在 Java EE 8 服务器中,行为将完全相同。您只需不需要 SeContainerInitializer,并且需要将依赖项更改为以下内容:

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>

参见

运行您的第一个 JAX-RS 2.1 代码

JAX-RS 是一个 API,旨在为 Java 提供一种便携和标准的方式来构建 RESTful Web 服务。这是在应用程序之间传输数据(包括互联网)时最常用的技术之一。

2.1 版本引入的最酷的功能之一是服务器发送事件SSE),它将在本菜谱中介绍。SSE 是由 HTML5 创建的一个规范,其中在服务器和客户端之间建立了一个通道,单向从服务器到客户端。它是一种传输包含一些数据的消息的协议。

准备工作

让我们从向我们的项目中添加正确的依赖项开始:

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-grizzly2-http</artifactId>
            <version>2.26-b09</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
            <version>2.26-b09</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-sse</artifactId>
            <version>2.26-b09</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

你肯定注意到了我们在这里使用的是 Jersey。为什么?因为 Jersey 是 JAX-RS 的参考实现,这意味着所有 JAX-RS 规范都是首先通过 Jersey 实现的。

此外,使用 Jersey,我们可以使用 Grizzly 启动一个小型本地服务器,这对于这个菜谱很有用,因为我们只需要几个服务器功能来展示 SSE 行为。

在本书的后续内容中,我们将使用完整的 GlassFish 来构建更多的 JAX-RS 菜谱。

如何做到这一点...

  1. 首先,我们创建一个将成为我们服务器的类:
public class ServerMock {

    public static final URI CONTEXT = 
    URI.create("http://localhost:8080/");
    public static final String BASE_PATH = "ssevents";

    public static void main(String[] args) {
        try {
            final ResourceConfig resourceConfig = new 
            ResourceConfig(SseResource.class);

            final HttpServer server = 
            GrizzlyHttpServerFactory.createHttpServer(CONTEXT, 
            resourceConfig, false);
            server.start();

            System.out.println(String.format("Mock Server started
            at %s%s", CONTEXT, BASE_PATH));

            Thread.currentThread().join();
        } catch (IOException | InterruptedException ex) {
            System.out.println(ex.getMessage());
        }
    }
}
  1. 然后,我们创建一个 JAX-RS 端点,将事件发送到客户端:
@Path(ServerMock.BASE_PATH)
public class SseResource {

    private static volatile SseEventSink SINK = null;

    @GET
    @Produces(MediaType.SERVER_SENT_EVENTS)
    public void getMessageQueue(@Context SseEventSink sink) {
        SseResource.SINK = sink;
    }

    @POST
    public void addMessage(final String message, @Context Sse sse) 
    throws IOException {
        if (SINK != null) {
            SINK.send(sse.newEventBuilder()
                .name("sse-message")
                .id(String.valueOf(System.currentTimeMillis()))
                .data(String.class, message)
                .comment("")
                .build());
        }
    }
}
  1. 然后,我们创建一个客户端类来消费从服务器生成的事件:
public class ClientConsumer {

    public static final Client CLIENT = ClientBuilder.newClient();
    public static final WebTarget WEB_TARGET = 
    CLIENT.target(ServerMock.CONTEXT
    + BASE_PATH);

    public static void main(String[] args) {
        consume();
    }

    private static void consume() {

        try (final SseEventSource sseSource =
                     SseEventSource
                             .target(WEB_TARGET)
                             .build()) {

            sseSource.register(System.out::println);
            sseSource.open();

            for (int counter=0; counter < 5; counter++) {
                System.out.println(" ");
                for (int innerCounter=0; innerCounter < 5; 
                innerCounter++) {
                    WEB_TARGET.request().post(Entity.json("event "
                    + innerCounter));
                }
                Thread.sleep(1000);
            }

            CLIENT.close();
            System.out.println("\n All messages consumed");
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
    }
}

要尝试它,你首先需要运行ServerMock类,然后是ClientConsumer类。如果一切顺利,你应该会看到类似这样的内容:

InboundEvent{name='sse-message', id='1502228257736', comment='',    data=event 0}
 InboundEvent{name='sse-message', id='1502228257753', comment='',   data=event 1}
 InboundEvent{name='sse-message', id='1502228257758', comment='',   data=event 2}
 InboundEvent{name='sse-message', id='1502228257763', comment='',   data=event 3}
 InboundEvent{name='sse-message', id='1502228257768', comment='',   data=event 4}

这些是从服务器发送到客户端的消息。

它是如何工作的...

这个菜谱由三个部分组成:

  • 服务器,由ServerMock类表示

  • SSE 引擎,由SseResource类表示

  • 客户端,由ClientConsumer类表示

因此,一旦ServerMock被实例化,它就会注册SseResource类:

final ResourceConfig resourceConfig = new ResourceConfig(SseResource.class);
final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(CONTEXT, resourceConfig, false);
server.start();

然后,SseResource的两个关键方法开始执行。第一个方法将消息添加到服务器队列:

addMessage(final String message, @Context Sse sse)

第二个方法消费这个队列并将消息发送到客户端:

@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
public void getMessageQueue(@Context SseEventSink sink)

注意,这个版本引入了一个媒体类型SERVER_SENT_EVENTS,正是为了这个目的。最后,我们有我们的客户端。在这个菜谱中,它既是发布消息也是消费消息。

这里消费:

sseSource.register(System.out::println);
sseSource.open();

这里发布:

ServerMock.WEB_TARGET.request().post(Entity.json("event " + innerCounter));

参见

运行你的第一个 JSF 2.3 代码

JavaServer FacesJSF)是 Java 技术,旨在简化 UI 的构建过程,尽管它是为前端设计的,而 UI 是在后端构建的。

使用 JSF,你可以构建组件并以可扩展的方式在 UI 中使用(或重用)它们。你还可以使用其他强大的 API,如 CDI 和 Bean 验证,来改进你的应用程序及其架构。

在这个菜谱中,我们将使用 ValidatorConverter 接口,以及版本 2.3 引入的新功能,即可以使用泛型参数使用它们。

准备就绪

首先,我们需要添加所需的依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 让我们创建一个 User 类作为我们菜谱的主要对象:
public class User implements Serializable {

    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email; 
    }

    //DON'T FORGET THE GETTERS AND SETTERS
    //THIS RECIPE WON'T WORK WITHOUT THEM
}
  1. 现在,我们创建一个 UserBean 类来管理我们的 UI:
@Named
@ViewScoped
public class UserBean implements Serializable {

    private User user;

    public UserBean(){
        user = new User("Elder Moraes", "elder@eldermoraes.com");
    }

    public void userAction(){
        FacesContext.getCurrentInstance().addMessage(null, 
                new FacesMessage("Name|Password welformed"));
    }

    //DON'T FORGET THE GETTERS AND SETTERS
    //THIS RECIPE WON'T WORK WITHOUT THEM
}
  1. 现在,我们使用 User 参数实现 Converter 接口:
@FacesConverter("userConverter")
public class UserConverter implements Converter<User> {

    @Override
    public String getAsString(FacesContext fc, UIComponent uic, 
    User user) {
        return user.getName() + "|" + user.getEmail();
    }

    @Override
    public User getAsObject(FacesContext fc, UIComponent uic, 
    String string) {
        return new User(string.substring(0, string.indexOf("|")), 
        string.substring(string.indexOf("|") + 1));
    }

}
  1. 现在,我们使用 User 参数实现 Validator 接口:
@FacesValidator("userValidator")
public class UserValidator implements Validator<User> {

    @Override
    public void validate(FacesContext fc, UIComponent uic, 
    User user) 
    throws ValidatorException {
        if(!user.getEmail().contains("@")){
            throw new ValidatorException(new FacesMessage(null, 
                                         "Malformed e-mail"));
        }
    }
}
  1. 然后,我们使用所有这些来创建我们的用户界面:
<h:body>
    <h:form>
        <h:panelGrid columns="3">
            <h:outputLabel value="Name|E-mail:" 
            for="userNameEmail"/>
            <h:inputText id="userNameEmail" 
            value="#{userBean.user}" 
            converter="userConverter" validator="userValidator"/> 
            <h:message for="userNameEmail"/>
        </h:panelGrid>
        <h:commandButton value="Validate" 
        action="#{userBean.userAction()}"/>
    </h:form> 
</h:body>

不要忘记在 Java EE 8 服务器上运行它。

它是如何工作的...

UserBean 类管理 UI 和服务器之间的通信。一旦实例化 user 对象,它对两者都是可用的。

这就是为什么当您运行它时,Name | E-mail 已经填写(当服务器创建 UserBean 类时,user 对象被实例化)。

我们将 UserBean 类中的 userAction() 方法关联到 UI 的 Validate 按钮上:

<h:commandButton value="Validate" action="#{userBean.userAction()}"/>

您可以在 UserBean 中创建其他方法并执行相同的操作以增强您的应用程序。

我们菜谱的整个核心在 UI 中只代表一行:

<h:inputText id="userNameEmail" value="#{userBean.user}" converter="userConverter" validator="userValidator"/>

因此,我们在这里使用的两个实现接口是 userConverteruserValidator

基本上,UserConverter 类(具有 getAsStringgetAsObject 方法)根据您定义的逻辑将对象转换为字符串或从字符串转换为对象,反之亦然。

我们已经在前面的代码片段中提到了它:

value="#{userBean.user}"

服务器使用 userConverter 对象,调用 getAsString 方法,并使用前面的表达式语言打印结果。

最后,当您提交表单时,会自动调用 UserValidator 类,通过调用其 validate 方法并应用您定义的规则。

更多内容...

您可以通过添加 Bean Validation 来增加验证器,例如,使用 @Email 约束定义 User 中的 email 属性。

参见

运行您的第一个 JSON-P 1.1 代码

JSON-Pointer 是用于 JSON 处理的 Java API。我们所说的处理,是指生成、转换、解析和查询 JSON 字符串和/或对象。

在这个菜谱中,您将学习如何以非常简单的方式使用 JSON Pointer 从 JSON 消息中获取特定的值。

准备就绪

让我们获取我们的 dependency

<dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
</dependency>

如何做到这一点...

  1. 首先,我们定义一个 JSON 消息来表示一个 User 对象:
{
  "user": {
    "email": "elder@eldermoraes.com",
    "name": "Elder",
    "profile": [
      {
        "id": 1
      },
      {
        "id": 2
      },
      {
        "id": 3
      }
    ]
  }
}
  1. 现在,我们创建一个方法来读取它并打印我们想要的值:
public class JPointer {

    public static void main(String[] args) throws IOException{
        try (InputStream is = 
        JPointer.class.getClassLoader().getResourceAsStream("user.json");
                JsonReader jr = Json.createReader(is)) {

            JsonStructure js = jr.read();
            JsonPointer jp = Json.createPointer("/user/profile");
            JsonValue jv = jp.getValue(js);
            System.out.println("profile: " + jv);
        }
    }
}

执行此代码会打印以下内容:

profile: [{"id":1},{"id":2},{"id":3}]

它是如何工作的...

JSON Pointer 是由 互联网工程任务组IETF)在 请求评论RFC)6901 中定义的标准。该标准基本上说 JSON Pointer 是一个字符串,用于标识 JSON 文档中的特定值。

没有 JSON Pointer,你需要解析整个消息并遍历它,直到找到所需值;可能需要很多 if、else 以及类似的东西。

因此,JSON Pointer 通过以非常优雅的方式执行此类操作,帮助你显著减少编写代码的数量。

参考以下内容

运行你的第一个 JSON-B 代码

JSON-B 是一个 API,用于以标准化的方式将 Java 对象转换为 JSON 消息。它定义了一个默认的映射算法,将 Java 类转换为 JSON,同时仍然允许你自定义自己的算法。

随着 JSON-B 的加入,Java EE 现在有一套完整的工具来处理 JSON,如 JSON API 和 JSON-P。不再需要第三方框架(尽管你仍然可以自由使用它们)。

本快速菜谱将展示如何使用 JSON-B 将 Java 对象转换为 JSON 消息,并从 JSON 消息中转换回来。

准备工作

让我们在项目中添加我们的依赖项:

    <dependencies>
        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <version>1.0</version>
        </dependency> 
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.1</version>
        </dependency> 
    </dependencies>

如何操作...

  1. 让我们创建一个 User 类作为我们 JSON 消息的模型:
public class User {

    private String name;
    private String email;

    public User(){        
    }

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" + "name=" + name + ", email=" + email + '}';
    }

    //DON'T FORGET THE GETTERS AND SETTERS
    //THIS RECIPE WON'T WORK WITHOUT THEM

}
  1. 然后,让我们创建一个类来使用 JSON-B 将对象进行转换:
public class JsonBUser {

    public static void main(String[] args) throws Exception {
        User user = new User("Elder", "elder@eldermoraes.com");

        Jsonb jb = JsonbBuilder.create();
        String jsonUser = jb.toJson(user);
        User u = jb.fromJson(jsonUser, User.class);

        jb.close();
        System.out.println("json: " + jsonUser);
        System.out.println("user: " + u);

    }
}

打印的结果是:

json: {"email":"elder@eldermoraes.com","name":"Elder"}
 user: User{name=Elder, email=elder@eldermoraes.com}

第一行是将对象转换为 JSON 字符串。第二行是将相同的字符串转换回对象。

工作原理...

它使用 User 类中定义的获取器和设置器进行双向转换,这就是为什么它们如此重要的原因。

参考以下内容

运行你的第一个 Servlet 4.0 代码

Servlet 4.0 是 Java EE 8 中最大的 API 之一。自从 Java EE 平台(旧 J2EE)的诞生以来,Servlet 规范始终扮演着关键角色。

本版本最酷的添加功能无疑是 HTTP/2.0 和服务器推送。两者都为你的应用程序带来了性能提升。

本菜谱将使用服务器推送来完成网页中最基本的任务之一——加载图片。

准备工作

让我们添加我们需要的依赖项:

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>

如何操作...

  1. 我们将创建一个 servlet:
@WebServlet(name = "ServerPush", urlPatterns = {"/ServerPush"})
public class ServerPush extends HttpServlet {

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

        PushBuilder pb = request.newPushBuilder();
        if (pb != null) {
            pb.path("images/javaee-logo.png")
              .addHeader("content-type", "image/png")
              .push();
        }

        try (PrintWriter writer = response.getWriter();) {
            StringBuilder html = new StringBuilder();
            html.append("<html>");
            html.append("<center>");
            html.append("<img src='images/javaee-logo.png'><br>");
            html.append("<h2>Image pushed by ServerPush</h2>");
            html.append("</center>");
            html.append("</html>");
            writer.write(html.toString());
        }
    }
}
  1. 要尝试它,请在 Java EE 8 服务器上运行项目并打开此 URL:
https://localhost:8080/ch01-servlet/ServerPush

工作原理...

我们使用PushBuilder对象在img src标签请求之前将图像发送到客户端。换句话说,浏览器不需要再进行另一个请求(它通常使用img src进行)来渲染图像。

对于单个图像来说,这似乎没有太大的区别,但如果是有数十、数百或数千个图像,那就大不相同了。减少客户端和服务器端的流量。对所有都带来更好的性能!

还有更多...

如果您使用 JSF,您可以免费获得服务器推送的好处!您甚至不需要重写一行代码,因为 JSF 依赖于服务器推送规范。

只需确保您在 HTTPS 协议下运行它,因为 HTTP/2.0 仅在此协议下工作。

另请参阅

运行您的第一个 Security API 代码

在构建企业应用程序时,安全性是首要关注的问题之一。幸运的是,Java EE 平台现在有一个 API,可以以标准化的方式处理许多企业需求。

在这个食谱中,您将学习如何根据管理敏感数据的方法中定义的规则来定义角色并授予它们正确的授权。

准备工作

我们首先将项目依赖项添加到项目中:

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomee</groupId>
            <artifactId>openejb-core</artifactId>
            <version>7.0.4</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

  1. 我们首先创建一个User实体:
@Entity 
public class User implements Serializable{

    @Id
    private Long id;
    private String name;
    private String email;

    public User(){        
    }

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    //DON'T FORGET THE GETTERS AND SETTERS
    //THIS RECIPE WON'T WORK WITHOUT THEM
}
  1. 在这里,我们创建一个类来存储我们的安全角色:
public class Roles {
    public static final String ADMIN = "ADMIN";
    public static final String OPERATOR = "OPERATOR";
}
  1. 然后,我们创建一个有状态的 bean 来管理我们的用户操作:
@Stateful
public class UserBean {

    @PersistenceContext(unitName = "ch01-security-pu", 
    type = PersistenceContextType.EXTENDED)
    private EntityManager em;

    @RolesAllowed({Roles.ADMIN, Roles.OPERATOR})
    public void add(User user){
        em.persist(user);
    }

    @RolesAllowed({Roles.ADMIN})
    public void remove(User user){
        em.remove(user);
    }

    @RolesAllowed({Roles.ADMIN})
    public void update(User user){
        em.merge(user);
    }

    @PermitAll
    public List<User> get(){
        Query q = em.createQuery("SELECT u FROM User as u ");
        return q.getResultList();
    }
  1. 现在,我们需要为每个角色创建一个执行器:
public class RoleExecutor {

    public interface Executable {
        void execute() throws Exception;
    }

    @Stateless
    @RunAs(Roles.ADMIN)
    public static class AdminExecutor {
        public void run(Executable executable) throws Exception {
            executable.execute();
        }
    }

    @Stateless
    @RunAs(Roles.OPERATOR)
    public static class OperatorExecutor {
        public void run(Executable executable) throws Exception {
            executable.execute();
        }
    }
}
  1. 最后,我们创建一个测试类来测试我们的安全规则。

我们的代码使用了三个测试方法:asAdmin()asOperator()asAnonymous()

  1. 首先,它测试asAdmin()
    //Lot of setup code before this point

    @Test
    public void asAdmin() throws Exception {
        adminExecutor.run(() -> {
            userBean.add(new User(1L, "user1", "user1@user.com"));
            userBean.add(new User(2L, "user2", "user2@user.com"));
            userBean.add(new User(3L, "user3", "user3@user.com"));
            userBean.add(new User(4L, "user4", "user4@user.com"));

            List<User> list = userBean.get();

            list.forEach((user) -> {
                userBean.remove(user);
            });

            Assert.assertEquals("userBean.get()", 0, 
            userBean.get().size());
        });
    }
  1. 然后它测试asOperator()
    @Test
    public void asOperator() throws Exception {

        operatorExecutor.run(() -> {
            userBean.add(new User(1L, "user1", "user1@user.com"));
            userBean.add(new User(2L, "user2", "user2@user.com"));
            userBean.add(new User(3L, "user3", "user3@user.com"));
            userBean.add(new User(4L, "user4", "user4@user.com"));

            List<User> list = userBean.get();

            list.forEach((user) -> {
                try {
                    userBean.remove(user);
                    Assert.fail("Operator was able to remove user " + 
                    user.getName());
                } catch (EJBAccessException e) {
                }
            });
            Assert.assertEquals("userBean.get()", 4, 
            userBean.get().size());
        });
    }
  1. 最后,它测试asAnonymous()
    @Test
    public void asAnonymous() {

        try {
            userBean.add(new User(1L, "elder", 
            "elder@eldermoraes.com"));
            Assert.fail("Anonymous user should not add users");
        } catch (EJBAccessException e) {
        }

        try {
            userBean.remove(new User(1L, "elder", 
            "elder@eldermoraes.com"));
            Assert.fail("Anonymous user should not remove users");
        } catch (EJBAccessException e) {
        }

        try {
            userBean.get();
        } catch (EJBAccessException e) {
            Assert.fail("Everyone can list users");
        }
    }

这个类非常大!要查看完整的源代码,请查看食谱末尾的链接。

它是如何工作的...

这个食谱的整个重点在于@RolesAllowed@RunsAs@PermitAll注解。它们定义了每个角色可以执行的操作以及当用户尝试使用错误角色执行操作时会发生什么。

还有更多...

我们在这里所做的是称为程序化安全;也就是说,我们通过代码(程序)定义了安全规则和角色。还有一种称为声明式安全的方法,其中您通过应用程序和服务器配置声明规则和角色。

对于这个食谱来说,一个很好的提升步骤是将角色管理扩展到应用程序之外,例如数据库或服务。

另请参阅

运行你的第一个 MVC 1.0 代码

如果你正在关注 Java EE 8 的新闻,你现在可能想知道:为什么 MVC 1.0 仍然存在,尽管它已经被从 Java EE 8 的伞下移除?

是的,这是真的。MVC 1.0 已经不再属于 Java EE 8 版本。但这并没有减少这个伟大 API 的重要性,我相信它将在未来的版本中改变一些其他 API 的工作方式(例如,JSF)。

那为什么不在这里介绍它呢?你无论如何都会用到它。

这个菜谱将展示如何使用 Controller(C)将 Model(M)注入到 View(V)中。它还引入了一些 CDI 和 JAX-RS 技术。

准备工作

将适当的依赖项添加到你的项目中:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.mvc</groupId>
            <artifactId>javax.mvc-api</artifactId>
            <version>1.0-pr</version>
        </dependency>

如何做到这一点...

  1. 首先为你的 JAX-RS 端点创建一个根:
@ApplicationPath("webresources")
public class AppConfig extends Application{
}
  1. 创建一个 User 类(这将是你的 MODEL):
public class User {

    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    //DON'T FORGET THE GETTERS AND SETTERS
    //THIS RECIPE WON'T WORK WITHOUT THEM
}
  1. 现在,创建一个 Session Bean,稍后将在你的 Controller 中注入:
@Stateless
public class UserBean {

    public User getUser(){
        return new User("Elder", "elder@eldermoraes.com");
    }
}
  1. 然后,创建 Controller:
@Controller
@Path("userController")
public class UserController {

    @Inject
    Models models;

    @Inject
    UserBean userBean;

    @GET
    public String user(){
        models.put("user", userBean.getUser());
        return "/user.jsp";
    }
}
  1. 最后,是网页(View):
<head>
    <meta http-equiv="Content-Type" content="text/html; 
    charset=UTF-8">
    <title>User MVC</title>
</head>
<body>
    <h1>${user.name}/${user.email}</h1>
</body>

在 Java EE 8 服务器上运行它,并访问此 URL:

http://localhost:8080/ch01-mvc/webresources/userController

它是如何工作的...

整个场景中的主要角色是注入到 Controller 中的 Models 类:

@Inject
Models models;

它是 MVC 1.0 API 中的一个类,在这个菜谱中负责让 User 对象在 View 层可用。它通过 CDI 注入,并使用另一个注入的 Bean,userBean 来实现:

models.put("user", userBean.getUser());

因此,View 可以轻松地通过表达式语言访问 User 对象的值:

<h1>${user.name}/${user.email}</h1>

参见

第二章:服务器端开发

Java EE 可以看作是专为服务器端开发而设计的。大多数 API 都针对服务器端处理和管理非常强大。

本章将为您提供一些作为 Java EE 开发者可能会遇到的一些常见和有用的场景,并展示您应该如何处理它们。

在本章中,我们将介绍以下菜谱:

  • 使用 CDI 注入上下文和依赖

  • 使用 Bean Validation 进行数据验证

  • 使用 Servlet 进行请求和响应管理

  • 使用服务器推送来预先提供对象

  • 使用 EJB 和 JTA 进行事务管理

  • 使用 EJB 处理并发

  • 使用 JPA 进行智能数据持久化

  • 使用 EJB 和 JPA 进行数据缓存

  • 使用批处理

使用 CDI 注入上下文和依赖

Java EE 上下文和依赖注入(CDI)是 Java EE 范围内最重要的 API 之一。自 Java EE 6 引入以来,它现在对许多其他 API 产生了重大影响。

在这个菜谱中,您将学习如何以几种不同的方式和情况使用 CDI。

准备就绪

首先,让我们添加所需的依赖项:

<dependency>
    <groupId>javax.enterprise</groupId>
    <artifactId>cdi-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
</dependency> 
<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-web-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
</dependency>

如何做到这一点...

  1. 我们将构建一个基于 JAX-RS 的应用程序,因此我们将首先准备应用程序以执行:
@ApplicationPath("webresources")
public class Application extends javax.ws.rs.core.Application {
}
  1. 然后,我们创建一个User应用程序作为我们的主要对象:
public class User implements Serializable {

    private String name;
    private String email;

    //DO NOT FORGET TO ADD THE GETTERS AND SETTERS
}

我们的User类没有默认构造函数,所以当 CDI 尝试注入它时不知道如何构建这个类。因此,我们创建一个工厂类,并在其方法上使用@Produces注解:

public class UserFactory implements Serializable{

    @Produces
    public User getUser() {
        return new User("Elder Moraes", "elder@eldermoraes.com");
    }

}
  1. 让我们创建一个枚举来列出我们的配置类型:
public enum ProfileType {
    ADMIN, OPERATOR;
}
  1. 在这里,我们创建一个自定义注解:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Profile {
    ProfileType value();
}
  1. 将它们添加到接口中以原型化用户配置文件行为:
public interface UserProfile {
    ProfileType type();
}

现在我们已经定义了配置列表及其与用户的关联行为,我们可以为管理员配置文件提供一个适当的实现:

@Profile(ProfileType.ADMIN)
public class ImplAdmin implements UserProfile{

    @Override
    public ProfileType type() {
        System.out.println("User is admin");
        return ProfileType.ADMIN;
    }   
}

同样,也可以为操作员配置文件做同样的事情:

@Profile(ProfileType.OPERATOR)
@Default
public class ImplOperator implements UserProfile{

    @Override
    public ProfileType type() {
        System.out.println("User is operator");
        return ProfileType.OPERATOR;
    }   
}
  1. 然后,我们通过将其将要使用的所有对象注入其中来创建一个 REST 端点:
@Path("userservice/")
@RequestScoped
public class UserService {

    @Inject
    private User user;

    @Inject
    @Profile(ProfileType.ADMIN)
    private UserProfile userProfileAdmin;

    @Inject
    @Profile(ProfileType.OPERATOR)
    private UserProfile userProfileOperator;

    @Inject
    private UserProfile userProfileDefault;

    @Inject
    private Event<User> userEvent;

    ...
  1. 这个方法通过 CDI 注入用户并将其发送到结果页面:
    @GET
    @Path("getUser")
    public Response getUser(@Context HttpServletRequest request, 
            @Context HttpServletResponse response) 
            throws ServletException, IOException{

        request.setAttribute("result", user);
        request.getRequestDispatcher("/result.jsp")
        .forward(request, response);
        return Response.ok().build();
    }
  1. 这个与管理员配置文件做的是同样的事情:
    @GET
    @Path("getProfileAdmin")
    public Response getProfileAdmin(@Context HttpServletRequest request, 
            @Context HttpServletResponse response) 
            throws ServletException, IOException{

            request.setAttribute("result", 
            fireUserEvents(userProfileAdmin.type()));
             request.getRequestDispatcher("/result.jsp")
             .forward(request, response);
        return Response.ok().build();
    }
  1. 这个与操作员配置文件做的是同样的事情:
    @GET
    @Path("getProfileOperator")
    public Response getProfileOperator(@Context HttpServletRequest request, 
            @Context HttpServletResponse response) 
            throws ServletException, IOException{

            request.setAttribute("result", 
            fireUserEvents(userProfileOperator.type())); 
            request.getRequestDispatcher("/result.jsp")
            .forward(request, response);
        return Response.ok().build();
    }
  1. 最后,我们将默认配置发送到结果页面:
    @GET
    @Path("getProfileDefault")
    public Response getProfileDefault(@Context HttpServletRequest request, 
            @Context HttpServletResponse response) 
            throws ServletException, IOException{

            request.setAttribute("result", 
            fireUserEvents(userProfileDefault.type())); 
            request.getRequestDispatcher("/result.jsp")
            .forward(request, response);
            return Response.ok().build();
    }
  1. 我们使用fireUserEvents方法触发一个事件和异步事件,针对之前注入的User对象:
    private ProfileType fireUserEvents(ProfileType type){
        userEvent.fire(user);
        userEvent.fireAsync(user);
        return type;
    }

    public void sendUserNotification(@Observes User user){
        System.out.println("sendUserNotification: " + user);
    }

    public void sendUserNotificationAsync(@ObservesAsync User user){
        System.out.println("sendUserNotificationAsync: " + user);
    }
  1. 因此,我们构建一个页面来调用每个端点方法:
<body>
 <a href="http://localhost:8080/ch02-
 cdi/webresources/userservice/getUser">getUser</a>
 <br>
 <a href="http://localhost:8080/ch02-
 cdi/webresources/userservice/getProfileAdmin">getProfileAdmin</a>
 <br>
 <a href="http://localhost:8080/ch02-
 cdi/webresources/userservice/getProfileOperator">getProfileOperator</a>
 <br>
 <a href="http://localhost:8080/ch02-
 cdi/webresources/userservice/getProfileDefault">getProfileDefault</a>
</body>
  1. 最后,我们使用表达式语言在结果页面上打印结果:
<body>
    <h1>${result}</h1>
    <a href="javascript:window.history.back();">Back</a>
</body>

它是如何工作的...

好吧,上一节中发生了很多事情!我们首先应该看看@Produces注解。这是一个 CDI 注解,它告诉服务器:“嘿!这个方法知道如何构建一个 User 对象。”

由于我们没有为User类创建默认构造函数,因此我们的工厂中的getUser方法将被注入到我们的上下文中作为其中一个。

第二个注解是我们自定义的@Profile注解,它以我们的枚举ProfileType作为参数。它是UserProfile对象的限定符。

现在,让我们看看这些声明:

@Profile(ProfileType.ADMIN)
public class ImplAdmin implements UserProfile{
   ...
}

@Profile(ProfileType.OPERATOR)
@Default
public class ImplOperator implements UserProfile{
   ...
}

这段代码将教导CDI 如何注入一个UserProfile对象:

  • 如果对象被标注为@Profile(ProfileType.ADMIN),则使用ImplAdmin

  • 如果对象被标注为@Profile(ProfileType.OPERATOR),则使用ImplOperator

  • 如果对象没有被标注,则使用ImplOperator,因为它有@Default注解

我们可以在我们的端点声明中看到它们的作用:

    @Inject
    @Profile(ProfileType.ADMIN)
    private UserProfile userProfileAdmin;

    @Inject
    @Profile(ProfileType.OPERATOR)
    private UserProfile userProfileOperator;

    @Inject
    private UserProfile userProfileDefault;

因此,CDI 正在帮助我们使用上下文来注入我们的UserProfile接口的正确实现。

看看端点方法,我们看到这个:

    @GET
    @Path("getUser")
    public Response getUser(@Context HttpServletRequest request, 
            @Context HttpServletResponse response) 
            throws ServletException, IOException{

        request.setAttribute("result", user);
        request.getRequestDispatcher("/result.jsp")
        .forward(request, response);
        return Response.ok().build();
    }

注意,我们为我们的方法包含了HttpServletRequestHttpServletResponse作为参数,但将它们标注为@Context。所以即使这不是 servlet 上下文(当我们容易访问请求和响应引用时),我们也可以要求 CDI 给我们提供适当的引用。

最后,我们有我们的用户事件引擎:

    @Inject
    private Event<User> userEvent;

    ...

    private ProfileType fireUserEvents(ProfileType type){
        userEvent.fire(user);
        userEvent.fireAsync(user);
        return type;
    }

    public void sendUserNotification(@Observes User user){
        System.out.println("sendUserNotification: " + user);
    }

    public void sendUserNotificationAsync(@ObservesAsync User user){
        System.out.println("sendUserNotificationAsync: " + user);
    }

因此,我们使用@Observes@ObserversAsync注解告诉 CDI:“嘿 CDI!监视 User 对象...当有人在其上触发事件时,我想让你做点什么。

对于“某物”,CDI 将其理解为调用sendUserNotificationsendUserNotificationAsync方法。试试看!

显然,@Observers将同步执行,而@ObservesAsync将异步执行。

还有更多...

我们使用 GlassFish 5 来运行这个菜谱。你可以使用任何你想要的 Java EE 8 兼容服务器,你甚至可以在没有服务器的情况下使用 CDI 与 Java SE。看看第一章的 CDI 菜谱,新特性和改进

参见

使用 Bean Validation 进行数据验证

你可以使用 Bean Validation 以许多不同的方式约束你的数据。在这个菜谱中,我们将使用它来验证 JSF 表单,这样我们就可以在用户尝试提交时立即进行验证,并立即避免任何无效数据。

准备中

首先,我们添加我们的依赖项:

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.1.Final</version>
        </dependency> 
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

如何做到...

  1. 让我们创建一个将被附加到我们的 JSF 页面的User对象:
@Named
@RequestScoped
public class User {

    @NotBlank (message = "Name should not be blank")
    @Size (min = 4, max = 10,message = "Name should be between 
    4 and 10 characters")
    private String name;

    @Email (message = "Invalid e-mail format")
    @NotBlank (message = "E-mail shoud not be blank")
    private String email;

    @PastOrPresent (message = "Created date should be 
    past or present")
    @NotNull (message = "Create date should not be null")
    private LocalDate created;

    @Future (message = "Expires should be a future date")
    @NotNull (message = "Expires should not be null")
    private LocalDate expires;

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS

    ...
  1. 然后,我们定义一个方法,当所有数据都有效时将触发:
    public void valid(){
        FacesContext
                .getCurrentInstance()
                .addMessage(
                    null, 
                    new FacesMessage(FacesMessage.SEVERITY_INFO,
                    "Your data is valid", ""));
    }
  1. 现在我们 JSF 页面引用了每个User类声明的字段:
<h:body>
 <h:form>
 <h:outputLabel for="name" value="Name" />
 <h:inputText id="name" value="#{user.name}" />
 <br/>
 <h:outputLabel for="email" value="E-mail" />
 <h:inputText id="email" value="#{user.email}" />
 <br/>
 <h:outputLabel for="created" value="Created" />
 <h:inputText id="created" value="#{user.created}">
     <f:convertDateTime type="localDate" pattern="dd/MM/uuuu" /> 
 </h:inputText>
 <br/>
 <h:outputLabel for="expire" value="Expire" />
 <h:inputText id="expire" value="#{user.expires}">
     <f:convertDateTime type="localDate" pattern="dd/MM/uuuu" /> 
 </h:inputText>
 <br/>
 <h:commandButton value="submit" type="submit" action="#{user.valid()}" />
 </h:form>
</h:body>

现在,如果你运行这段代码,一旦你点击提交按钮,你将得到所有字段的验证。试试看!

它是如何工作的...

让我们检查每个声明的约束:

    @NotBlank (message = "Name should not be blank")
    @Size (min = 4, max = 10,message = "Name should be between 
           4 and 10 characters")
    private String name;

@NotBlank注解不仅拒绝 null 值,还拒绝空白值,而@Size则不言自明:

    @Email (message = "Invalid e-mail format")
    @NotBlank (message = "E-mail shoud not be blank")
    private String email;

@Email约束将检查电子邮件字符串格式:

    @PastOrPresent (message = "Created date should be past or present")
    @NotNull (message = "Create date should not be null")
    private LocalDate created;

@PastOrPresentLocalDate约束为过去或直到当前日期。它不能在未来。

在这里,我们不能使用@NotBlank,因为没有空白日期,只有 null,所以我们使用@NotNull来避免它:

    @Future (message = "Expires should be a future date")
    @NotNull (message = "Expires should not be null")
    private LocalDate expires;

这与上一个相同,但约束条件是未来的日期。

在我们的 UI 中,有两个地方值得仔细查看:

 <h:inputText id="created" value="#{user.created}">
     <f:convertDateTime type="localDate" pattern="dd/MM/uuuu" /> 
 </h:inputText>

 ...

 <h:inputText id="expire" value="#{user.expires}">
     <f:convertDateTime type="localDate" pattern="dd/MM/uuuu" /> 
 </h:inputText>

我们使用 convertDateTime 自动将输入到 inputText 中的数据转换为 dd/MM/uuuu 格式。

参见

使用 servlet 进行请求和响应管理

Servlet API 是在 Java EE 存在之前创建的——实际上是在 J2EE 存在之前!它在 1999 年的 J2EE 1.2(Servlet 2.2)中成为 EE 的一部分。

这是一个处理请求/响应上下文的有力工具,这个配方将展示如何操作的示例。

准备工作

让我们添加我们的依赖项:

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.0-b05</version>
            <scope>provided</scope>
        </dependency> 
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

  1. 让我们为我们的配方创建一个 User 类:
public class User {

    private String name;
    private String email;

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS

}
  1. 然后是我们的 servlet:
@WebServlet(name = "UserServlet", urlPatterns = {"/UserServlet"})
public class UserServlet extends HttpServlet {

    private User user;

    @PostConstruct
    public void instantiateUser(){
        user = new User("Elder Moraes", "elder@eldermoraes.com");
    }

   ...

我们在 instantiateUser() 方法上使用 @PostConstruct 注解。它告诉服务器,每当这个 servlet 被构建(一个新的实例被创建)时,它可以运行这个方法。

  1. 我们还实现了 init()destroy() 超类方法:
    @Override
    public void init() throws ServletException {
        System.out.println("Servlet " + this.getServletName() + 
                           " has started");
    }

    @Override
    public void destroy() {
        System.out.println("Servlet " + this.getServletName() + 
                           " has destroyed");
    }
  1. 我们还实现了 doGet()doPost()
    @Override
    protected void doGet(HttpServletRequest request, 
    HttpServletResponse response)
            throws ServletException, IOException {
        doRequest(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, 
    HttpServletResponse response)
            throws ServletException, IOException {
        doRequest(request, response);
    }
  1. doGet()doPost() 都将调用我们的自定义方法 doRequest()
    protected void doRequest(HttpServletRequest request, 
    HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet UserServlet</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h2>Servlet UserServlet at " + 
                         request.getContextPath() + "</h2>");
            out.println("<h2>Now: " + new Date() + "</h2>");
            out.println("<h2>User: " + user.getName() + "/" + 
                        user.getEmail() + "</h2>");
            out.println("</body>");
            out.println("</html>");
        }
    }
  1. 最后,我们有一个网页可以调用我们的 servlet:
    <body>
        <a href="<%=request.getContextPath()%>/UserServlet">
        <%=request.getContextPath() %>/UserServlet</a>
    </body>

它是如何工作的...

Java EE 服务器本身将根据调用者使用的 HTTP 方法调用 doGet()doPost() 方法。在我们的配方中,我们将它们都重定向到同一个 doRequest() 方法。

init() 方法属于由服务器管理的 servlet 生命周期,并在 servlet 实例化后作为第一个方法执行。

destroy() 方法也属于 servlet 生命周期,并在实例释放前作为最后一个方法执行。

还有更多...

init() 的行为类似于 @PostConstruct,但最后一个是在 init() 之前执行的,所以使用两者时要记住这一点。

@PostConstruct 在默认构造函数之后执行。

使用 destroy() 方法时要小心,避免持有任何内存引用;否则,你可能会搞乱 servlet 生命周期并遇到内存泄漏。

参见

使用服务器推送提前使对象可用

Servlet 4.0 最重要的新特性之一是 HTTP/2.0 支持。它带来了另一个酷且可靠的功能——服务器推送。

这个配方将展示如何在过滤器中使用服务器推送并在每个请求中推送所需的资源。

准备工作

我们首先应该添加所需的依赖项:

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.0-b07</version>
            <scope>provided</scope>
        </dependency> 
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

  1. 我们首先创建 UserServlet,它调用 user.jsp
@WebServlet(name = "UserServlet", urlPatterns = {"/UserServlet"})
public class UserServlet extends HttpServlet {

    protected void doRequest(HttpServletRequest request, 
                             HttpServletResponse response)
            throws ServletException, IOException {
        request.getRequestDispatcher("/user.jsp")
        .forward(request, response);
        System.out.println("Redirected to user.jsp");
    }

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

    @Override
    protected void doPost(HttpServletRequest request, 
                          HttpServletResponse response)
            throws ServletException, IOException {
        doRequest(request, response);
    }
}
  1. 我们还用 ProfileServlet 做同样的操作,但通过调用 profile.jsp
@WebServlet(name = "ProfileServlet", urlPatterns = {"/ProfileServlet"})
public class ProfileServlet extends HttpServlet {

    protected void doRequest(HttpServletRequest request, 
                             HttpServletResponse response)
            throws ServletException, IOException {
        request.getRequestDispatcher("/profile.jsp").
        forward(request, response);
        System.out.println("Redirected to profile.jsp");
    }

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

    @Override
    protected void doPost(HttpServletRequest request, 
                          HttpServletResponse response)
            throws ServletException, IOException {
        doRequest(request, response);
    }
}
  1. 然后我们创建一个将在每个请求上执行的过滤器(urlPatterns = {"/*"}):
@WebFilter(filterName = "PushFilter", urlPatterns = {"/*"})
public class PushFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, 
    ServletResponse response,
            FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpReq = (HttpServletRequest)request;
        PushBuilder builder = httpReq.newPushBuilder();

        if (builder != null){
            builder
                .path("resources/javaee-logo.png")
                .path("resources/style.css")
                .path("resources/functions.js")
                .push(); 
            System.out.println("Resources pushed");
        }

        chain.doFilter(request, response);

    }   
}
  1. 在这里,我们创建一个页面来调用我们的 servlets:
<body>
 <a href="UserServlet">User</a>
 <br/>
 <a href="ProfileServlet">Profile</a>
</body>
  1. 以下是 servlets 调用的页面。首先是user.jsp页面:
    <head>
        <meta http-equiv="Content-Type" content="text/html; 
         charset=UTF-8">
        <link rel="stylesheet" type="text/css"   
         href="resources/style.css">
        <script src="img/functions.js"></script>
        <title>User Push</title>
    </head>

    <body>
        <h1>User styled</h1>
        <img src="img/javaee-logo.png">
        <br />
        <button onclick="message()">Message</button>
        <br />
        <a href="javascript:window.history.back();">Back</a>
    </body>
  1. 其次,调用profile.jsp页面:
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" type="text/css" href="resources/style.css">
        <script src="img/functions.js"></script>
        <title>User Push</title>
    </head>

    <body>
        <h1>Profile styled</h1>
        <img src="img/javaee-logo.png">
        <br />
        <button onclick="message()">Message</button>
        <br />
        <a href="javascript:window.history.back();">Back</a>
    </body>

它是如何工作的...

在 HTTP/1.0 下运行的 Web 应用程序在找到图像文件、CSS 文件和其他渲染网页所需的资源引用时向服务器发送请求。

使用 HTTP/2.0 您仍然可以这样做,但现在您可以做得更好:服务器现在可以预先推送资源,避免不必要的新的请求,减少服务器负载,并提高性能。

在此菜谱中,我们的资源由以下表示:

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" type="text/css" href="resources/style.css">
        <script src="img/functions.js"></script>

推送发生在我们过滤器的这个部分:

        HttpServletRequest httpReq = (HttpServletRequest)request;
        PushBuilder builder = httpReq.newPushBuilder();

        if (builder != null){
            builder
                .path("resources/javaee-logo.png")
                .path("resources/style.css")
                .path("resources/functions.js")
                .push(); 
            System.out.println("Resources pushed");
        }

因此,当浏览器需要这些资源来渲染网页时,它们已经可用。

还有更多...

注意,您的浏览器需要支持服务器推送功能;否则,您的页面将像往常一样工作。所以请确保在使用PushBuilder之前检查它是否为 null,并确保所有用户都将拥有一个正常工作的应用程序。

注意,JSF 2.3 是基于服务器推送功能构建的,所以如果您只是将您的 JSF 应用程序迁移到 Java EE 8 兼容的服务器,您将免费获得其性能提升!

相关内容

使用 EJB 和 JTA 进行事务管理

Java 事务 API,或称 JTA,是一个允许在 Java EE 环境中进行分布式事务的 API。当您将事务管理委托给服务器时,它最为强大。

此菜谱将向您展示如何做到这一点!

准备工作

首先,添加依赖项:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.1.Final</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <version>1.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.openejb</groupId>
            <artifactId>openejb-core</artifactId>
            <version>4.7.4</version>
            <scope>test</scope>
        </dependency>

如何做到这一点...

  1. 首先,我们需要创建我们的持久化单元(在persistence.xml中):
    <persistence-unit name="ch02-jta-pu" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <jta-data-source>userDb</jta-data-source>
        <non-jta-data-source>userDbNonJta</non-jta-data-source>

        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="javax.persistence.schema-
             generation.database.action" 
             value="create"/>
        </properties>
    </persistence-unit>
  1. 然后,我们创建一个User类作为实体(@Entity):
@Entity
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
    private String email;

    protected User() {
    } 

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS
}
  1. 我们还需要一个 EJB 来执行对User实体的操作:
@Stateful
public class UserBean {

    @PersistenceContext(unitName = "ch02-jta-pu", 
    type = PersistenceContextType.EXTENDED)
    private EntityManager em;

    public void add(User user){
        em.persist(user);
    }

    public void update(User user){
        em.merge(user);
    }

    public void remove(User user){
        em.remove(user);
    }

    public User findById(Long id){
        return em.find(User.class, id);
    }
}
  1. 然后我们创建我们的单元测试:
public class Ch02JtaTest {

    private EJBContainer ejbContainer;

    @EJB
    private UserBean userBean;

    public Ch02JtaTest() {
    }

    @Before
    public void setUp() throws NamingException {
        Properties p = new Properties();
        p.put("userDb", "new://Resource?type=DataSource");
        p.put("userDb.JdbcDriver", "org.hsqldb.jdbcDriver");
        p.put("userDb.JdbcUrl", "jdbc:hsqldb:mem:userdatabase");

        ejbContainer = EJBContainer.createEJBContainer(p);
        ejbContainer.getContext().bind("inject", this);
    }

    @After
    public void tearDown() {
        ejbContainer.close();
    }

    @Test
    public void validTransaction() throws Exception{
        User user = new User(null, "Elder Moraes", 
                             "elder@eldermoraes.com");

        userBean.add(user);
        user.setName("John Doe");
        userBean.update(user);

        User userDb = userBean.findById(1L);
        assertEquals(userDb.getName(), "John Doe");

    }

}

它是如何工作的...

此菜谱中 JTA 的关键代码行就在这里:

<persistence-unit name="ch02-jta-pu" transaction-type="JTA">

当您使用transaction-type='JTA'时,您是在告诉服务器,它应该处理在此上下文中进行的所有事务。如果您使用RESOURCE-LOCAL,则表示您正在处理事务:

    @Test
    public void validTransaction() throws Exception{
        User user = new User(null, "Elder Moraes", 
        "elder@eldermoraes.com");

        userBean.add(user);
        user.setName("John Doe");
        userBean.update(user);

        User userDb = userBean.findById(1L);
        assertEquals(userDb.getName(), "John Doe");

    }

UserBean的每个被调用方法都会启动一个事务以完成,如果在事务活跃期间出现任何问题,则会回滚;如果事务顺利完成,则会提交。

还有更多...

另一段重要的代码如下:

@Stateful
public class UserBean {

    @PersistenceContext(unitName = "ch02-jta-pu", 
                        type = PersistenceContextType.EXTENDED)
    private EntityManager em;

    ...
}

因此,我们在这里将我们的PersistenceContext定义为EXTENDED。这意味着此持久化上下文绑定到@Stateful豆,直到它从容器中移除。

另一个选项是TRANSACTION,这意味着持久化上下文将仅在事务期间存在。

相关内容

使用 EJB 处理并发

并发管理是 Java EE 服务器提供的最大优势之一。你可以依赖一个现成的环境来处理这个棘手的话题。

此菜谱将向您展示如何设置您的 bean 以使用它!

准备工作

只需将 Java EE 依赖项添加到您的项目中:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

此菜谱将展示三个场景。

在第一个场景中,LockType在类级别定义:

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
@Lock(LockType.READ)
@AccessTimeout(value = 10000)
public class UserClassLevelBean {

    private int userCount;

    public int getUserCount() {
        return userCount;
    }

    public void addUser(){
        userCount++;
    }

}

在第二个场景中,LockType在方法级别定义:

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
@AccessTimeout(value = 10000)
public class UserMethodLevelBean {

    private int userCount;

    @Lock(LockType.READ)
    public int getUserCount(){
        return userCount;
    }

    @Lock(LockType.WRITE)
    public void addUser(){
        userCount++;
    }
}

第三个场景是一个自管理 bean:

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
public class UserSelfManagedBean {

    private int userCount;

    public int getUserCount() {
        return userCount;
    }

    public synchronized void addUser(){
        userCount++;
    }
}

它是如何工作的...

首先看看以下内容:

@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)

这完全是多余的!单例 bean 默认由容器管理,因此您不需要指定它们。

单例设计用于并发访问,因此它们是此菜谱的完美用例。

现在,让我们检查在类级别定义的LockType

@Lock(LockType.READ)
@AccessTimeout(value = 10000)
public class UserClassLevelBean {
    ...
}

当我们在类级别使用@Lock注解时,通知的LockType将用于所有类方法。

在这种情况下,LockType.READ意味着许多客户端可以同时访问一个资源。这在读取数据时很常见。

在锁定的情况下,LockType将使用@AccessTimeout注解定义的时间来决定是否运行到超时。

现在,让我们检查在方法级别定义的LockType

    @Lock(LockType.READ)
    public int getUserCount(){
        return userCount;
    }

    @Lock(LockType.WRITE)
    public void addUser(){
        userCount++;
    }

因此,我们基本上是在说getUserCount()可以同时被许多用户访问(LockType.READ),而addUser()则每次只能被一个用户访问(LockType.WRITE)。

最后一个案例是自管理 bean:

@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
public class UserSelfManagedBean{

    ...

    public synchronized void addUser(){
        userCount++;
    }

    ...
}

在这种情况下,你必须在你自己的代码中管理你的 bean 的所有并发问题。我们以同步限定符为例。

还有更多...

除非你真的真的需要,否则不要使用自管理 bean。Java EE 容器(非常好)设计用来以非常高效和优雅的方式完成这项工作。

参见

使用 JPA 进行智能数据持久化

Java 持久化 API 是一个规范,它描述了使用 Java EE 管理关系数据库的接口。

它简化了数据操作,并大大减少了为其编写的代码,尤其是如果您习惯于 SQL ANSI。

此菜谱将向您展示如何使用它来持久化您的数据。

准备工作

让我们首先添加所需的依赖项:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.1.Final</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <version>1.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.openejb</groupId>
            <artifactId>openejb-core</artifactId>
            <version>4.7.4</version>
            <scope>test</scope>
        </dependency>

如何操作...

  1. 让我们先创建一个实体(您可以将其视为一个表):
@Entity
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
    private String email;

    protected User() {
    } 

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS
}
  1. 在这里,我们声明我们的持久化单元(在persistence.xml中):
    <persistence-unit name="ch02-jpa-pu" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <jta-data-source>userDb</jta-data-source>

        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="javax.persistence.schema-
             generation.database.action" 
             value="create"/>
        </properties>
    </persistence-unit>
  1. 然后我们创建一个会话 bean 来管理我们的数据:
@Stateless
public class UserBean {

    @PersistenceContext(unitName = "ch02-jpa-pu", 
    type = PersistenceContextType.TRANSACTION)
    private EntityManager em;

    public void add(User user){
        em.persist(user);
    }

    public void update(User user){
        em.merge(user);
    }

    public void remove(User user){
        em.remove(user);
    }

    public User findById(Long id){
        return em.find(User.class, id);
    }
}
  1. 而在这里,我们使用单元测试来尝试它:
public class Ch02JpaTest {

    private EJBContainer ejbContainer;

    @EJB
    private UserBean userBean;

    public Ch02JpaTest() {
    }

    @Before
    public void setUp() throws NamingException {
        Properties p = new Properties();
        p.put("userDb", "new://Resource?type=DataSource");
        p.put("userDb.JdbcDriver", "org.hsqldb.jdbcDriver");
        p.put("userDb.JdbcUrl", "jdbc:hsqldb:mem:userdatabase");

        ejbContainer = EJBContainer.createEJBContainer(p);
        ejbContainer.getContext().bind("inject", this);
    }

    @After
    public void tearDown() {
        ejbContainer.close();
    }

    @Test
    public void persistData() throws Exception{
        User user = new User(null, "Elder Moraes", 
        "elder@eldermoraes.com");

        userBean.add(user);
        user.setName("John Doe");
        userBean.update(user);

        User userDb = userBean.findById(1L);
        assertEquals(userDb.getName(), "John Doe");

    }

}

它是如何工作的...

让我们分解我们的持久化单元pu)。

这行代码定义了 pu 名称和使用的交易类型:

<persistence-unit name="ch02-jpa-pu" transaction-type="JTA">

以下行显示了 JPA 使用的提供者:

<provider>org.hibernate.ejb.HibernatePersistence</provider>

是通过 JNDI 访问的数据源名称:

<jta-data-source>userDb</jta-data-source>

这行代码使得所有你的实体都可以用于这个 pu,因此你不需要为每个实体声明:

<exclude-unlisted-classes>false</exclude-unlisted-classes>

这个块允许在不存在时创建数据库对象:

        <properties>
            <property name="javax.persistence.schema-
             generation.database.action" 
             value="create"/>
        </properties>

现在,让我们看看UserBean

@Stateless
public class UserBean {

    @PersistenceContext(unitName = "ch02-jpa-pu", 
                        type = PersistenceContextType.TRANSACTION)
    private EntityManager em;

   ...

}

EntityManager是负责 bean 与数据源之间接口的对象。它通过@PersistenceContext注解绑定到上下文中。

我们如下检查EntityManager操作:

    public void add(User user){
        em.persist(user);
    }

persist()方法用于将新数据添加到数据源。执行结束时,对象被附加到上下文中:

    public void update(User user){
        em.merge(user);
    }

merge()方法用于在数据源上更新现有数据。首先在上下文中找到对象,然后在数据库中更新,并使用新状态附加到上下文中:

    public void remove(User user){
        em.remove(user);
    }

remove()方法,猜猜看它是做什么的?

    public User findById(Long id){
        return em.find(User.class, id);
    }

最后,find()方法使用id参数搜索具有相同 ID 的数据库对象。这就是为什么 JPA 要求你的实体必须使用@Id注解声明一个 ID。

参见

使用 EJB 和 JPA 进行数据缓存

了解如何为你的应用程序构建一个简单且本地的缓存是一项重要的技能。它可能对某些数据访问性能有重大影响,而且做起来相当简单。

这个菜谱将向你展示如何做。

准备工作

简单地为你的项目添加一个 Java EE 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

如何做...

  1. 让我们创建一个User类作为我们的缓存对象:
public class User {

    private String name;
    private String email;

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS
}
  1. 然后创建一个单例来保存我们的用户列表缓存:
@Singleton
@Startup
public class UserCacheBean {

    protected Queue<User> cache = null;

    @PersistenceContext
    private EntityManager em;

    public UserCacheBean() {
    }

    protected void loadCache() {
        List<User> list = em.createQuery("SELECT u FROM USER 
                                         as u").getResultList();

        list.forEach((user) -> {
            cache.add(user);
        });
    }

    @Lock(LockType.READ)
    public List<User> get() {
        return cache.stream().collect(Collectors.toList());
    }

    @PostConstruct
    protected void init() {
        cache = new ConcurrentLinkedQueue<>();
        loadCache();
    }
}

它是如何工作的...

让我们先了解我们的 bean 声明:

@Singleton
@Startup
public class UserCacheBean {

    ...

    @PostConstruct
    protected void init() {
        cache = new ConcurrentLinkedQueue<>();
        loadCache();
    }
}

我们使用单例模式,因为它在应用程序上下文中只有一个实例。这正是我们想要的数据缓存的方式,因为我们不希望允许不同数据被共享的可能性。

还要注意,我们使用了@Startup注解。它告诉服务器,一旦加载了这个 bean,就应该执行它,并且使用带有@PostConstruct注解的方法。

因此,我们利用启动时间来加载我们的缓存:

    protected void loadCache() {
        List<User> list = em.createQuery("SELECT u FROM USER 
                                         as u").getResultList();

        list.forEach((user) -> {
            cache.add(user);
        });
    }

现在,让我们检查持有我们缓存的对象:

protected Queue<User> cache = null;

...

cache = new ConcurrentLinkedQueue<>();

ConcurrentLinkedQueue是一个列表,它有一个主要目的——在线程安全的环境中由多个进程访问。这正是我们所需要的,而且它在其成员访问上提供了很好的性能。

最后,让我们检查对我们的数据缓存的访问:

    @Lock(LockType.READ)
    public List<User> get() {
        return cache.stream().collect(Collectors.toList());
    }

我们使用LockType.READget()方法进行了注释,这样它就告诉并发管理器,它可以以线程安全的方式同时被多个进程访问。

还有更多...

如果你需要在你的应用程序中使用大而复杂的缓存,你应该使用一些企业级缓存解决方案以获得更好的结果。

参见

使用批处理

批处理是本章的最后一个菜谱。在企业环境中运行后台任务是实用且重要的技能。

你可以用它来批量处理数据,或者只是将其从 UI 流程中分离出来。这个菜谱将向你展示如何做到这一点。

准备工作

让我们添加我们的依赖项:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.2.10.Final</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 我们首先定义我们的持久化单元:
  <persistence-unit name="ch02-batch-pu" >
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <jta-data-source>java:app/userDb</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="javax.persistence.schema- 
       generation.database.action" 
       value="create"/>
      <property name="hibernate.transaction.jta.platform" 
       value="org.hibernate.service.jta.platform
       .internal.SunOneJtaPlatform"/>
    </properties>
  </persistence-unit>
  1. 然后我们声明一个User实体:
@Entity
@Table(name = "UserTab")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @NotNull
    private Integer id;

    private String name;

    private String email;

    public User() {
    }

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS
}
  1. 在这里我们创建一个工作读取器:
@Named
@Dependent
public class UserReader extends AbstractItemReader {

    private BufferedReader br;

    @Override
    public void open(Serializable checkpoint) throws Exception {
        br = new BufferedReader(
                new InputStreamReader(
                        Thread.currentThread()
                        .getContextClassLoader()
                        .getResourceAsStream
                        ("META-INF/user.txt")));
    }

    @Override
    public String readItem() {
        String line = null;

        try {
            line = br.readLine();
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
        }

        return line;
    }
}
  1. 然后我们创建一个工作处理器:
@Named
@Dependent
public class UserProcessor implements ItemProcessor {

    @Override
    public User processItem(Object line) {
        User user = new User();

        StringTokenizer tokens = new StringTokenizer((String)
        line, ",");
        user.setId(Integer.parseInt(tokens.nextToken()));
        user.setName(tokens.nextToken());
        user.setEmail(tokens.nextToken());

        return user;
    }
}
  1. 然后我们创建一个工作写入器:
@Named
@Dependent
public class UserWriter extends AbstractItemWriter {

    @PersistenceContext
    EntityManager entityManager;

    @Override
    @Transactional
    public void writeItems(List list) {
        for (User user : (List<User>) list) {
            entityManager.persist(user);
        }
    }
}

处理器、读取器和写入器由位于META-INF.batch-jobsacess-user.xml文件引用:

<?xml version="1.0" encoding="windows-1252"?>
<job id="userAccess" 

     version="1.0">
    <step id="loadData">
        <chunk item-count="3">
            <reader ref="userReader"/>
            <processor ref="userProcessor"/>
            <writer ref="userWriter"/>
        </chunk>
    </step>
</job>
  1. 最后,我们创建一个用于与批处理引擎交互的 bean:
@Named
@RequestScoped
public class UserBean {

    @PersistenceContext
    EntityManager entityManager;

    public void run() {
        try {
            JobOperator job = BatchRuntime.getJobOperator();
            long jobId = job.start("acess-user", new Properties());
            System.out.println("Job started: " + jobId);
        } catch (JobStartException ex) {
            System.out.println(ex.getMessage());
        }
    }

    public List<User> get() {
        return entityManager
                .createQuery("SELECT u FROM User as u", User.class)
                .getResultList();
    }
}

为了本例的目的,我们将使用一个 JSF 页面来运行工作并加载数据:

<h:body>
 <h:form>
 <h:outputLabel value="#{userBean.get()}" />
 <br />
 <h:commandButton value="Run" action="index" actionListener="#{userBean.run()}"/>
 <h:commandButton value="Reload" action="index"/>
 </h:form>
</h:body>

在 Java EE 服务器上运行它,点击运行按钮,然后点击重新加载按钮。

它是如何工作的...

为了理解正在发生的事情:

  1. UserReader扩展了具有两个关键方法的AbstractItemReader类:open()readItem()。在我们的案例中,第一个打开META-INF/user.txt,第二个读取文件的每一行。

  2. UserProcessor类扩展了具有processItem()方法的ItemProcessor类。它通过readItem()(从UserReader)获取项目,生成我们想要的User对象。

  3. 一旦所有项目都处理完毕并可用在列表(内存)中,我们使用UserWriter类;它扩展了AbstractItemWriter类并具有writeItems方法。在我们的案例中,我们使用它来持久化从user.txt文件中读取的数据。

一切准备就绪,我们只需使用UserBean来运行工作:

    public void run() {
        try {
            JobOperator job = BatchRuntime.getJobOperator();
            long jobId = job.start("acess-user", new Properties());
            System.out.println("Job started: " + jobId);
        } catch (JobStartException ex) {
            System.out.println(ex.getMessage());
        }
    }

job.start()方法引用了acess-user.xml文件,使我们的读取器、处理器和写入器能够协同工作。

参考以下内容

第三章:使用 JSON 和 RESTful 功能构建强大的服务

现在,使用 JSON 通过 REST 服务进行数据传输是 HTTP 协议中应用程序之间数据传输最常见的方法,这不是巧合——这是快速且容易实现的。它易于阅读,易于解析,并且使用 JSON-P,易于编码!

以下食谱将向您展示一些常见场景以及如何应用 Java EE 来处理它们。

本章涵盖了以下食谱:

  • 使用 JAX-RS 构建服务器端事件

  • 使用 JAX-RS 和 CDI 提高服务的能力

  • 使用 JSON-B 简化数据和对象表示

  • 使用 JSON-P 解析、生成、转换和查询 JSON 对象

使用 JAX-RS 和 CDI 构建服务器端事件

通常,Web 应用程序依赖于客户端发送的事件。所以,基本上,服务器只有在被要求时才会做些什么。

但是,随着互联网周围技术的演变(HTML5、移动客户端、智能手机等等),服务器端也必须进化。因此,这就产生了服务器端事件,即由服务器引发的事件(正如其名所示)。

通过这个食谱,您将学习如何使用服务器端事件来更新用户视图。

准备工作

首先添加 Java EE 依赖项:

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

如何做到这一点...

首先,我们构建一个 REST 端点来管理我们将要使用的服务器事件,并且为了使用 REST,我们应该首先正确地配置它:

@ApplicationPath("webresources")
public class ApplicationConfig extends Application {

}

以下是一段相当大的代码块,但别担心,我们将将其拆分并理解每一部分:

@Path("serverSentService")
@RequestScoped
public class ServerSentService {

    private static final Map<Long, UserEvent> POOL = 
    new ConcurrentHashMap<>();

    @Resource(name = "LocalManagedExecutorService")
    private ManagedExecutorService executor;

    @Path("start")
    @POST
    public Response start(@Context Sse sse) {

        final UserEvent process = new UserEvent(sse);

        POOL.put(process.getId(), process);
        executor.submit(process);

        final URI uri = UriBuilder.fromResource(ServerSentService.class).path
        ("register/{id}").build(process.getId());
        return Response.created(uri).build();
    }

    @Path("register/{id}")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    @GET
    public void register(@PathParam("id") Long id,
            @Context SseEventSink sseEventSink) {
        final UserEvent process = POOL.get(id);

        if (process != null) {
            process.getSseBroadcaster().register(sseEventSink);
        } else {
            throw new NotFoundException();
        }
    }

    static class UserEvent implements Runnable {

        private final Long id;
        private final SseBroadcaster sseBroadcaster;
        private final Sse sse;

        UserEvent(Sse sse) {
            this.sse = sse;
            this.sseBroadcaster = sse.newBroadcaster();
            id = System.currentTimeMillis();
        }

        Long getId() {
            return id;
        }

        SseBroadcaster getSseBroadcaster() {
            return sseBroadcaster;
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(5);
                sseBroadcaster.broadcast(sse.newEventBuilder().
                name("register").data(String.class, "Text from event " 
                                      + id).build());
                sseBroadcaster.close();
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

这里,我们有一个用于管理 UI 并帮助我们更好地了解服务器中发生情况的 bean:

@ViewScoped
@Named
public class SseBean implements Serializable {

    @NotNull
    @Positive
    private Integer countClient;

    private Client client;

    @PostConstruct
    public void init(){
        client = ClientBuilder.newClient();
    }

    @PreDestroy
    public void destroy(){
        client.close();
    }

    public void sendEvent() throws URISyntaxException, InterruptedException {
        WebTarget target = client.target(URI.create("http://localhost:8080/
                                                    ch03-sse/"));
        Response response = 
        target.path("webresources/serverSentService/start")
                .request()
                .post(Entity.json(""), Response.class);

        FacesContext.getCurrentInstance().addMessage(null,
                new FacesMessage("Sse Endpoint: " + 
                response.getLocation()));

        final Map<Integer, String> messageMap = new ConcurrentHashMap<>
        (countClient);
        final SseEventSource[] sources = new 
        SseEventSource[countClient];

        final String processUriString = 
        target.getUri().relativize(response.getLocation()).
        toString();
        final WebTarget sseTarget = target.path(processUriString);

        for (int i = 0; i < countClient; i++) {
            final int id = i;
            sources[id] = SseEventSource.target(sseTarget).build();
            sources[id].register((event) -> {
                final String message = event.readData(String.class);

                if (message.contains("Text")) {
                    messageMap.put(id, message);
                }
            });
            sources[i].open();
        }

        TimeUnit.SECONDS.sleep(10);

        for (SseEventSource source : sources) {
            source.close();
        }

        for (int i = 0; i < countClient; i++) {
            final String message = messageMap.get(i);

            FacesContext.getCurrentInstance().addMessage(null,
                    new FacesMessage("Message sent to client " + 
                                     (i + 1) + ": " + message));
        }
    }

    public Integer getCountClient() {
        return countClient;
    }

    public void setCountClient(Integer countClient) {
        this.countClient = countClient;
    }

}

最后,UI 是一个简单的 JSF 页面的代码:

<h:body>
    <h:form>
        <h:outputLabel for="countClient" value="Number of Clients" />
        <h:inputText id="countClient" value="#{sseBean.countClient}" />

        <br />
        <h:commandButton type="submit" action="#{sseBean.sendEvent()}" 
         value="Send Events" />
    </h:form>
</h:body>

它是如何工作的...

我们从我们的 SSE 引擎开始,ServerEvent 类,以及一个 JAX-RS 端点——这些包含了我们需要为此食谱的所有方法。

让我们了解第一个:

    @Path("start")
    @POST
    public Response start(@Context Sse sse) {

        final UserEvent process = new UserEvent(sse);

        POOL.put(process.getId(), process);
        executor.submit(process);

        final URI uri = UriBuilder.fromResource(ServerSentService.class).
        path("register/{id}").build(process.getId());
        return Response.created(uri).build();
    }

以下是一些主要点:

  1. 首先,这个方法将创建并准备一个由服务器发送给客户端的事件。

  2. 然后,刚刚创建的事件被放入一个名为 POOL 的 HashMap 中。

  3. 然后,我们的事件被附加到一个 URI 上,该 URI 代表这个类中的另一个方法(详细信息将在下面提供)。

注意这个参数:

@Context Sse sse

它从服务器上下文中带来了服务器端事件功能,并允许您按需使用它,当然,它通过 CDI 注入(是的,CDI 到处都是!)。

现在我们看到我们的 register() 方法:

    @Path("register/{id}")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    @GET
    public void register(@PathParam("id") Long id,
            @Context SseEventSink sseEventSink) {
        final UserEvent event = POOL.get(id);

        if (event != null) {
            event.getSseBroadcaster().register(sseEventSink);
        } else {
            throw new NotFoundException();
        }
    }

这是将事件发送到您的客户端的非常方法——检查 @Produces 注解;它使用新的媒体类型 SERVER_SENT_EVENTS

引擎之所以能工作,多亏了这段小代码:

@Context SseEventSink sseEventSink

...

event.getSseBroadcaster().register(sseEventSink);

SseEventSink 是由 Java EE 服务器管理的事件队列,并且通过上下文注入为您提供。

然后,您获取进程广播器并将其注册到这个接收器,这意味着该进程广播的任何内容都将通过 SseEventSink 由服务器发送。

现在我们检查我们的事件设置:

    static class UserEvent implements Runnable {

        ...

        UserEvent(Sse sse) {
            this.sse = sse;
            this.sseBroadcaster = sse.newBroadcaster();
            id = System.currentTimeMillis();
        }

        ...

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(5);
                sseBroadcaster.broadcast(sse.newEventBuilder().
                name("register").data(String.class, "Text from event " 
                + id).build());
                sseBroadcaster.close();
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
    }

如果你注意这条线:

this.sseBroadcaster = sse.newBroadcaster();

您会记得我们刚刚在上一个类中使用了这个广播器。在这里我们看到这个广播器是由服务器注入的 Sse 对象带来的。

此事件实现了 Runnable 接口,因此我们可以使用它与执行器(如前所述),一旦运行,你就可以向你的客户端广播:

sseBroadcaster.broadcast(sse.newEventBuilder().name("register").
data(String.class, "Text from event " + id).build());

这正是发送给客户端的消息。这可能是你需要的任何消息。

对于此配方,我们使用了另一个类来与 Sse 交互。让我们突出最重要的部分:

        WebTarget target = client.target(URI.create
        ("http://localhost:8080/ch03-sse/"));
        Response response = target.path("webresources/serverSentService
                                        /start")
                .request()
                .post(Entity.json(""), Response.class);

这是一段简单的代码,您可以使用它来调用任何 JAX-RS 端点。

最后,这个模拟客户端最重要的部分:

        for (int i = 0; i < countClient; i++) {
            final int id = i;
            sources[id] = SseEventSource.target(sseTarget).build();
            sources[id].register((event) -> {
                final String message = event.readData(String.class);

                if (message.contains("Text")) {
                    messageMap.put(id, message);
                }
            });
            sources[i].open();
        }

每个广播的消息都在这里读取:

final String message = messageMap.get(i);

可以是任何你想要的客户端,另一个服务,一个网页,一个移动客户端,或任何东西。

然后我们检查我们的 UI:

<h:inputText id="countClient" value="#{sseBean.countClient}" />
...
<h:commandButton type="submit" action="#{sseBean.sendEvent()}" 
value="Send Events" />

我们使用 countClient 字段来填充客户端的 countClient 值,因此您可以随意使用尽可能多的线程。

更多...

重要的是要提到,SSE 不受 MS IE/Edge 网络浏览器支持,并且它的可扩展性不如 WebSockets。如果您想在桌面端实现全浏览器支持并且/或者更好的可扩展性(因此,不仅限于移动应用,还包括可以打开更多连接的实例的 Web 应用),那么应该考虑使用 WebSockets。幸运的是,标准 Java EE 自 7.0 起就支持 WebSockets。

参见

使用 JAX-RS 和 CDI 提升服务能力

此配方将向您展示如何利用 CDI 和 JAX-RS 功能来减少编写强大服务的努力并降低复杂性。

准备工作

首先添加 Java EE 依赖项:

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

如何做到这一点...

  1. 我们首先创建一个 User 类,将通过我们的服务进行管理:
public class User implements Serializable{

    private String name;
    private String email;

    public User(){

    }

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS

}
  1. 为了有多个 User 对象的来源,我们创建了一个 UserBean 类:
@Stateless
public class UserBean {

    public User getUser(){
        long ts = System.currentTimeMillis();
        return new User("Bean" + ts, "user" + ts + 
                        "@eldermoraes.com"); 
    }
}
  1. 最后,我们创建我们的 UserService 端点:
@Path("userservice")
public class UserService implements Serializable{

    @Inject
    private UserBean userBean;

    private User userLocal;

    @Inject
    private void setUserLocal(){
        long ts = System.currentTimeMillis();
        userLocal = new User("Local" + ts, "user" + ts + 
                             "@eldermoraes.com"); 
    }

    @GET
    @Path("getUserFromBean")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUserFromBean(){
        return Response.ok(userBean.getUser()).build();
    }

    @GET
    @Path("getUserFromLocal")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUserFromLocal(){
        return Response.ok(userLocal).build();
    } 

}
  1. 为了加载我们的 UI,我们有 UserView 类,它将在 UI 和服务之间充当控制器:
@ViewScoped
@Named
public class UserView implements Serializable {

    public void loadUsers() {
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target(URI.create
        ("http://localhost:8080/ch03-rscdi/"));
        User response = target.path("webresources/userservice/
                                     getUserFromBean")
                .request()
                .accept(MediaType.APPLICATION_JSON)
                .get(User.class);

        FacesContext.getCurrentInstance()
                .addMessage(null,
                        new FacesMessage("userFromBean: " + 
                                         response));

        response = target.path("webresources/userservice
                               /getUserFromLocal")
                .request()
                .accept(MediaType.APPLICATION_JSON)
                .get(User.class);

        FacesContext.getCurrentInstance()
                .addMessage(null,
                        new FacesMessage("userFromLocal: 
                                         " + response));
        client.close();
    }

}
  1. 我们添加一个简单的 JSF 页面,仅用于显示结果:
 <h:body>
 <h:form>
 <h:commandButton type="submit" 
 action="#{userView.loadUsers()}" 
 value="Load Users" />
 </h:form>
 </h:body>

它是如何工作的...

我们使用了两种注入方式:

  • UserBean,当 UserService 附着到上下文

  • UserService 本身

UserBean 注入是最简单的方式:

    @Inject
    private UserBean userBean;

UserService 本身注入也很简单:

    @Inject
    private void setUserLocal(){
        long ts = System.currentTimeMillis();
        userLocal = new User("Local" + ts, "user" + ts + 
                             "@eldermoraes.com"); 
    }

这里,@Inject 的工作方式类似于 @PostConstruct 注解,区别在于服务器上下文中运行方法。但结果相当相同。

所有一切都已注入,现在只是获取结果的问题:

response = target.path("webresources/userservice/getUserFromBean")
                .request()
                .accept(MediaType.APPLICATION_JSON)
                .get(User.class);

...

response = target.path("webresources/userservice/getUserFromLocal")
                .request()
                .accept(MediaType.APPLICATION_JSON)
                .get(User.class);

更多...

如您所见,JAX-RS 简化了大量的对象解析和表示:

    @GET
    @Path("getUserFromBean")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUserFromBean(){
        userFromBean = userBean.getUser();
        return Response.ok(userFromBean).build();
    }

    @GET
    @Path("getUserFromLocal")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUserFromLocal(){
        return Response.ok(userLocal).build();
    }

通过返回一个 Response 对象并使用 @Produces(MediaType.APPLICATION_JSON),您让框架承担了解析您的 user 对象到 JSON 表示的重任。只需几行代码就能节省大量精力!

您还可以使用生产者(@Produces 注解)注入用户。请参阅第一章(86071f26-42aa-43e2-8409-6feaed4759e0.xhtml)的 CDI 菜谱,新特性和改进,以获取更多详细信息。

相关内容

使用 JSON-B 轻松表示数据和对象

此菜谱将向您展示如何使用新的 JSON-B API 的力量为您的数据表示提供一些灵活性,并帮助将您的对象转换为 JSON 消息。

准备工作

首先添加 Java EE 依赖项:

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

如何操作...

  1. 我们首先创建一个具有一些自定义的 User 类(详情见后):
public class User {

    private Long id;

    @JsonbProperty("fullName")
    private String name;

    private String email;

    @JsonbTransient
    private Double privateNumber;

    @JsonbDateFormat(JsonbDateFormat.DEFAULT_LOCALE)
    private Date dateCreated;

    public User(Long id, String name, String email, 
                Double privateNumber, Date dateCreated) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.privateNumber = privateNumber;
        this.dateCreated = dateCreated;
    }

    private User(){
    }

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS
}
  1. 我们使用 UserView 将用户 JSON 返回到 UI:
@ViewScoped
@Named
public class UserView implements Serializable{

    private String json;

    public void loadUser(){
        long now = System.currentTimeMillis();
        User user = new User(now, 
                "User" + now, 
                "user" + now + "@eldermoraes.com",
                Math.random(),
                new Date());

        Jsonb jb = JsonbBuilder.create();
        json = jb.toJson(user);
        try {
            jb.close();
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
    }

    public String getJson() {
        return json;
    }

    public void setJson(String json) {
        this.json = json;
    }
}
  1. 我们只是添加一个 JSF 页面来显示结果:
 <h:body>
 <h:form>
 <h:commandButton type="submit" action="#{userView.loadUser()}" 
  value="Load User" />

 <br />

 <h:outputLabel for="json" value="User JSON" />
 <br />
 <h:inputTextarea id="json" value="#{userView.json}" 
  style="width: 300px; height: 300px;" />
 </h:form>
 </h:body>

工作原理...

我们使用一些 JSON-B 注解来自定义我们的用户数据表示:

    @JsonbProperty("fullName")
    private String name;

使用 @JsonbProperty 将字段名称更改为其他值:

    @JsonbTransient
    private Double privateNumber;

当您想防止某些属性出现在 JSON 表示中时,使用 @JsonbTransient

    @JsonbDateFormat(JsonbDateFormat.DEFAULT_LOCALE)
    private Date dateCreated;

使用 @JsonbDateFormat,您可以使用 API 自动格式化您的日期。

然后我们使用我们的 UI 管理器来更新视图:

    public void loadUser(){
        long now = System.currentTimeMillis();
        User user = new User(now, 
                "User" + now, 
                "user" + now + "@eldermoraes.com",
                Math.random(),
                new Date());

        Jsonb jb = JsonbBuilder.create();
        json = jb.toJson(user);
    }

相关内容

使用 JSON-P 解析、生成、转换和查询 JSON 对象

处理 JSON 对象是您无法避免的活动。因此,如果您可以通过依赖一个强大且易于使用的框架来操作它——那就更好了!

此菜谱将向您展示如何使用 JSON-P 执行一些不同的操作,使用或生成 JSON 对象。

准备工作

首先添加 Java EE 依赖项:

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

如何操作...

  1. 让我们创建一个 User 类来支持我们的操作:
public class User {

    private String name;
    private String email;
    private Integer[] profiles;

    public User(String name, String email, Integer[] profiles) {
        this.name = name;
        this.email = email;
        this.profiles = profiles;
    }

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS
}
  1. 然后创建一个 UserView 类来执行所有 JSON 操作:
@ViewScoped
@Named
public class UserView implements Serializable{

    private static final JsonBuilderFactory BUILDERFACTORY = 
    Json.createBuilderFactory(null);
    private final Jsonb jsonbBuilder = JsonbBuilder.create();

    private String fromArray;
    private String fromStructure;
    private String fromUser;
    private String fromJpointer;

    public void loadUserJson(){
        loadFromArray();
        loadFromStructure();
        loadFromUser();
    }

    private void loadFromArray(){
        JsonArray array = BUILDERFACTORY.createArrayBuilder()
                .add(BUILDERFACTORY.createObjectBuilder()
                        .add("name", "User1")
                        .add("email", "user1@eldermoraes.com"))
                .add(BUILDERFACTORY.createObjectBuilder()
                        .add("name", "User2")
                        .add("email", "user2@eldermoraes.com"))
                .add(BUILDERFACTORY.createObjectBuilder()
                        .add("name", "User3")
                        .add("email", "user3@eldermoraes.com")) 
                .build(); 
        fromArray = jsonbBuilder.toJson(array);
    }

    private void loadFromStructure(){
        JsonStructure structure = 
        BUILDERFACTORY.createObjectBuilder()
                .add("name", "User1")
                .add("email", "user1@eldermoraes.com")
                .add("profiles", BUILDERFACTORY.createArrayBuilder()
                        .add(BUILDERFACTORY.createObjectBuilder()
                                .add("id", "1")
                                .add("name", "Profile1"))
                        .add(BUILDERFACTORY.createObjectBuilder()
                                .add("id", "2")
                                .add("name", "Profile2")))
                .build();
        fromStructure = jsonbBuilder.toJson(structure);

        JsonPointer pointer = Json.createPointer("/profiles");
        JsonValue value = pointer.getValue(structure);
        fromJpointer = value.toString();
    }

    private void loadFromUser(){
        User user = new User("Elder Moraes", 
        "elder@eldermoraes.com", 
        new Integer[]{1,2,3});
        fromUser = jsonbBuilder.toJson(user);
    }

    //DO NOT FORGET TO IMPLEMENT THE GETTERS AND SETTERS
}
  1. 然后我们创建一个 JSF 页面来显示结果:
 <h:body>
 <h:form>
 <h:commandButton type="submit" action="#{userView.loadUserJson()}" 
 value="Load JSONs" />

 <br />

 <h:outputLabel for="fromArray" value="From Array" />
 <br />
 <h:inputTextarea id="fromArray" value="#{userView.fromArray}" 
 style="width: 300px; height: 150px" />
 <br />

 <h:outputLabel for="fromStructure" value="From Structure" />
 <br />
 <h:inputTextarea id="fromStructure" value="#{userView.fromStructure}" 
 style="width: 300px; height: 150px" />
 <br />

 <h:outputLabel for="fromUser" value="From User" />
 <br />
 <h:inputTextarea id="fromUser" value="#{userView.fromUser}" 
 style="width: 300px; height: 150px" />

  <br />
  <h:outputLabel for="fromJPointer" value="Query with JSON Pointer 
  (from JsonStructure Above)" />
  <br />
  <h:inputTextarea id="fromJPointer" 
   value="#{userView.fromJpointer}"  
  style="width: 300px; height: 100px" />
 </h:form>
 </h:body>

工作原理...

首先,loadFromArray() 方法:

    private void loadFromArray(){
        JsonArray array = BUILDERFACTORY.createArrayBuilder()
                .add(BUILDERFACTORY.createObjectBuilder()
                        .add("name", "User1")
                        .add("email", "user1@eldermoraes.com"))
                .add(BUILDERFACTORY.createObjectBuilder()
                        .add("name", "User2")
                        .add("email", "user2@eldermoraes.com"))
                .add(BUILDERFACTORY.createObjectBuilder()
                        .add("name", "User3")
                        .add("email", "user3@eldermoraes.com")) 
                .build(); 
        fromArray = jsonbBuilder.toJson(array);
    }

它使用 BuilderFactorycreateArrayBuilder 方法轻松构建 JSON 数组(每次调用 createObjectBuilder 都会创建另一个数组成员)。最后,我们使用 JSON-B 将其转换为 JSON 字符串:

    private void loadFromStructure(){
        JsonStructure structure = BUILDERFACTORY.createObjectBuilder()
                .add("name", "User1")
                .add("email", "user1@eldermoraes.com")
                .add("profiles", BUILDERFACTORY.createArrayBuilder()
                        .add(BUILDERFACTORY.createObjectBuilder()
                                .add("id", "1")
                                .add("name", "Profile1"))
                        .add(BUILDERFACTORY.createObjectBuilder()
                                .add("id", "2")
                                .add("name", "Profile2")))
                .build();
        fromStructure = jsonbBuilder.toJson(structure);

        JsonPointer pointer = new JsonPointerImpl("/profiles");
        JsonValue value = pointer.getValue(structure);
        fromJpointer = value.toString();
    }

在这里,我们不是构建一个数组,而是构建一个单独的 JSON 结构。再次,我们使用 JSON-B 将 JsonStructure 转换为 JSON 字符串。

我们还利用了已准备好的 JsonStructure 并使用它通过 JsonPointer 对象查询用户配置文件:

private void loadFromUser(){
        User user = new User("Elder Moraes", "elder@eldermoraes.com",
                    new Integer[]{1,2,3});
        fromUser = jsonbBuilder.toJson(user);
    }

这是最简单的:创建一个对象,并让 JSON-B 将其转换为 JSON 字符串。

相关内容

第四章:Web 和客户端-服务器通信

Web 开发是使用 Java EE 的最好方式之一。实际上,自从 J2EE 时代之前,我们就可以使用 JSP 和 servlets,这就是 Java Web 开发的开始。

本章将展示一些用于 Web 开发的先进特性,这将使你的应用程序更快、更好——对你和你的客户来说都是如此!

本章涵盖了以下菜谱:

  • 使用 servlet 进行请求和响应管理

  • 使用模板功能构建 UI 的 JSF

  • 使用服务器推送提高响应性能

使用 servlet 进行请求和响应管理

Servlets 是使用 Java EE 处理请求和响应的核心地方。如果你还不熟悉它,知道即使是 JSP 也只是当页面被调用时构建 servlet 的一种方式。

这个菜谱将展示你在使用 servlets 时可以使用的三个特性:

  • 启动时加载

  • 参数化 servlets

  • 异步 servlets

准备工作

首先,将依赖项添加到你的项目中:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

启动时加载的 servlet

让我们从我们的 servlet 开始,这个 servlet 将在服务器启动时加载:

@WebServlet(name = "LoadOnStartupServlet", urlPatterns = {"/LoadOnStartupServlet"}, 
loadOnStartup = 1)
public class LoadOnStartupServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("*******SERVLET LOADED 
                           WITH SERVER's STARTUP*******");
    }

}

带有初始化参数的 servlet

现在我们添加一个带有一些初始化参数的 servlet:

@WebServlet(name = "InitConfigServlet", urlPatterns = {"/InitConfigServlet"}, 
        initParams = {
                @WebInitParam(name = "key1", value = "value1"),
                @WebInitParam(name = "key2", value = "value2"),
                @WebInitParam(name = "key3", value = "value3"),
                @WebInitParam(name = "key4", value = "value4"),
                @WebInitParam(name = "key5", value = "value5")
        }
)
public class InitConfigServlet extends HttpServlet {

    Map<String, String> param = new HashMap<>();

    @Override
    protected void doPost(HttpServletRequest req, 
    HttpServletResponse resp)   
    throws ServletException, IOException {
        doProcess(req, resp);
    }

    @Override
    protected void doGet(HttpServletRequest req, 
    HttpServletResponse resp) 
    throws ServletException, IOException {
        doProcess(req, resp);
    }

    private void doProcess(HttpServletRequest req, 
    HttpServletResponse resp) 
    throws IOException{
        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();

        if (param.isEmpty()){
            out.println("No params to show");
        } else{
            param.forEach((k,v) -> out.println("param: " + k + ", 
                                        value: " + v + "<br />"));
        }
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("init");
        List<String> list = 
        Collections.list(config.getInitParameterNames());
        list.forEach((key) -> {
            param.put(key, config.getInitParameter(key));
        });
    }

}

异步 servlet

然后我们实现我们的异步 servlet:

@WebServlet(urlPatterns = "/AsyncServlet", asyncSupported = true)
public class AsyncServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

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

        long startTime = System.currentTimeMillis();
        System.out.println("AsyncServlet Begin, Name="
                + Thread.currentThread().getName() + ", ID="
                + Thread.currentThread().getId());

        String time = request.getParameter("timestamp");
        AsyncContext asyncCtx = request.startAsync();

        asyncCtx.start(() -> {
            try {
                Thread.sleep(Long.valueOf(time));
                long endTime = System.currentTimeMillis();
                long timeElapsed = endTime - startTime;
                System.out.println("AsyncServlet Finish, Name="
                        + Thread.currentThread().getName() + ", ID="
                        + Thread.currentThread().getId() + ", Duration="
                        + timeElapsed + " milliseconds.");

                asyncCtx.getResponse().getWriter().write
                ("Async process time: " + timeElapsed + " milliseconds");
                asyncCtx.complete();
            } catch (InterruptedException | IOException ex) {
                System.err.println(ex.getMessage());
            }
        });
    }
}

最后,我们需要一个简单的网页来尝试所有这些 servlet:

<body>
    <a href="${pageContext.request.contextPath}/InitConfigServlet">
    InitConfigServlet</a>
    <br />
    <br />
    <form action="${pageContext.request.contextPath}/AsyncServlet" 
     method="GET">
        <h2>AsyncServlet</h2>
        Milliseconds
        <br />
        <input type="number" id="timestamp" name="timestamp" 
        style="width: 200px" value="5000"/>
        <button type="submit">Submit</button>
    </form>

</body>

它是如何工作的...

启动时加载的 servlet

如果你希望你的 servlet 在服务器启动时初始化,那么这就是你需要的东西。通常你会用它来加载一些缓存,启动一个后台进程,记录一些信息,或者你需要在服务器刚刚启动且不能等待有人调用 servlet 时需要做的任何事情。

这种 servlet 的关键点如下:

  • loadOnStartup参数:接受任意数量的 servlet。这个数字定义了服务器在启动时运行所有 servlet 的顺序。所以如果你有多个 servlet 以这种方式运行,记得要定义正确的顺序(如果有)。如果没有定义数字或负数,服务器将选择默认顺序。

  • init方法:记得在启动时使用你想要执行的操作覆盖init方法,否则你的 servlet 将不会做任何事情。

带有初始化参数的 servlet

有时候你需要为你的 servlet 定义一些参数,这些参数超出了局部变量的范围——initParams就是做这件事的地方:

@WebServlet(name = "InitConfigServlet", urlPatterns = 
{"/InitConfigServlet"}, 
        initParams = {
                @WebInitParam(name = "key1", value = "value1"),
                @WebInitParam(name = "key2", value = "value2"),
                @WebInitParam(name = "key3", value = "value3"),
                @WebInitParam(name = "key4", value = "value4"),
                @WebInitParam(name = "key5", value = "value5")
        }
)

@WebInitParam注解会为你处理它们,并且这些参数将通过ServletConfig对象在服务器上可用。

异步 servlet

让我们把我们的AsyncServlet类拆分成几个部分,这样我们就可以理解它:

@WebServlet(urlPatterns = "/AsyncServlet", asyncSupported = true)

在这里,我们通过使用asyncSupported参数定义了我们的 servlet 以接受异步行为:

AsyncContext asyncCtx = request.startAsync();

我们使用了正在处理的请求来启动一个新的异步上下文。

然后我们开始我们的异步过程:

asyncCtx.start(() -> {...

然后我们打印输出以查看响应并完成异步过程:

                asyncCtx.getResponse().getWriter().write("Async 
                process time: " 
                + timeElapsed + " milliseconds");
                asyncCtx.complete();

参见

使用模板功能构建 UI 的 JSF

JavaServer FacesJSF)是一个强大的 Java EE API,用于构建出色的 UI,同时使用客户端和服务器功能。

与使用 JSP 相比,它走得更远,因为您不仅在使用 HTML 代码中使用 Java 代码,而且实际上是在引用注入到服务器上下文中的代码。

本食谱将向您展示如何使用 Facelet 的模板功能,从布局模板中获得更多灵活性和可重用性。

准备工作

首先向您的项目添加依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

  1. 让我们先创建我们的页面布局,包括页眉、内容区域和页脚:
<h:body>
    <div id="layout">
        <div id="header">
            <ui:insert name="header" >
                <ui:include src="img/header.xhtml" />
            </ui:insert>
        </div>
        <div id="content">
            <ui:insert name="content" >
                <ui:include src="img/content.xhtml" />
            </ui:insert>
        </div>
        <div id="footer">
            <ui:insert name="footer" >
                <ui:include src="img/footer.xhtml" />
            </ui:insert>
        </div>
    </div>
</h:body>
  1. 定义默认页眉区域:
<body>
    <h1>Template header</h1>
</body>
  1. 默认内容区域:
<body>
   <h1>Template content</h1>
</body>
  1. 默认页脚区域:
<body>
   <h1>Template content</h1>
</body>
  1. 然后使用我们的默认模板创建一个简单的页面:
<h:body>
    <ui:composition template="WEB-INF/template/layout.xhtml">

    </ui:composition>
</h:body>
  1. 现在,让我们创建另一个页面,并仅覆盖内容区域:
<h:body>
    <ui:composition template="/template/layout.xhtml">
        <ui:define name="content">
            <h1><p style="color:red">User content. Timestamp: #
            {userBean.timestamp}</p></h1>
        </ui:define>
    </ui:composition>
</h:body>
  1. 由于此代码正在调用UserBean,让我们定义它:
@Named
@RequestScoped
public class UserBean implements Serializable{

    public Long getTimestamp(){
        return new Date().getTime();
    }

}
  1. 此外,别忘了在WEB-INF文件夹中包含beans.xml文件;否则,此 Bean 将无法按预期工作:
<?xml version="1.0" encoding="UTF-8"?>
<beans 

       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
       http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="all">
</beans>

如果您想尝试此代码,请在 Java EE 兼容的服务器上运行它,并访问以下 URL:

  • http://localhost:8080/ch04-jsf/

  • http://localhost:8080/ch04-jsf/user.xhtml

它是如何工作的...

解释尽可能简单:layout.xhtml是我们的模板。只要您为每个区域命名(在我们的例子中是页眉、内容和页脚),任何使用它的 JSF 页面都将继承其布局。

任何使用此布局并希望自定义其中一些定义区域的页面,只需像我们在user.xhtml文件中所做的那样描述所需的区域:

<ui:composition template="/template/layout.xhtml">
    <ui:define name="content">
        <h1><font color="red">User content. Timestamp: #
            {userBean.timestamp}
        </font></h1>
    </ui:define>
</ui:composition>

参见

通过服务器推送提高响应性能

HTTP/2.0 的一个主要特性是服务器推送。当它可用时,这意味着,协议、服务器和浏览器客户端都支持它——它允许服务器在客户端请求之前发送(推送)数据到客户端。

这是 JSF 2.3 中最受欢迎的特性之一,如果您基于 JSF 的应用程序,它可能也是使用起来最不费力的一项——只需迁移到 Java EE 8 兼容的服务器,然后您就完成了。

本食谱将向您展示如何在您的应用程序中使用它,并允许您在同一场景下比较 HTTP/1.0 和 HTTP/2.0 的性能。

准备工作

首先向您的项目添加依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

本食谱只有一个单独的 servlet:

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

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

    private void doRequest(HttpServletRequest request,
    HttpServletResponse response) throws IOException{
        String usePush = request.getParameter("usePush");

        if ("true".equalsIgnoreCase(usePush)){
            PushBuilder pb = request.newPushBuilder();
            if (pb != null) {
                for(int row=0; row < 5; row++){
                    for(int col=0; col < 8; col++){
                        pb.path("image/keyboard_buttons/keyboard_buttons-" 
                                + row + "-" + col + ".jpeg")
                          .addHeader("content-type", "image/jpeg")
                          .push();
                    }
                }
            }
        }

        try (PrintWriter writer = response.getWriter()) {
            StringBuilder html = new StringBuilder();
            html.append("<html>");
            html.append("<center>");         
            html.append("<table cellspacing='0' cellpadding='0'
                         border='0'>");

            for(int row=0; row < 5; row++){
                html.append(" <tr>");
                for(int col=0; col < 8; col++){
                    html.append(" <td>");
                    html.append("<img 
                    src='image/keyboard_buttons/keyboard_buttons-" +
                         row + "-" + col + ".jpeg' style='width:100px;   
                         height:106.25px;'>"); 
                    html.append(" </td>"); 
                }
                html.append(" </tr>"); 
            }

            html.append("</table>");            
            html.append("<br>");

            if ("true".equalsIgnoreCase(usePush)){
                html.append("<h2>Image pushed by ServerPush</h2>");
            } else{
                html.append("<h2>Image loaded using HTTP/1.0</h2>");
            }

            html.append("</center>");
            html.append("</html>");
            writer.write(html.toString());
        }
    }

}

然后,我们创建一个简单的页面来调用 HTTP/1.0 和 HTTP/2.0 的情况:

<body>
    <a href="ServerPushServlet?usePush=true">Use HTTP/2.0 (ServerPush)</a>
    <br />
    <a href="ServerPushServlet?usePush=false">Use HTTP/1.0</a>
</body>

并且在 Java EE 8 兼容的服务器上使用此 URL 尝试它:

https://localhost:8181/ch04-serverpush

它是如何工作的...

在这个菜谱中加载的图片被分成了 25 份。当没有 HTTP/2.0 可用时,服务器将等待由img src(来自 HTML)发出的 25 个请求,然后对每个请求回复相应的图片。

使用 HTTP/2.0,服务器可以事先推送它们。这里的“魔法”就在这里:

            PushBuilder pb = request.newPushBuilder();
            if (pb != null) {
                for(int row=0; row < 5; row++){
                    for(int col=0; col < 8; col++){
                        pb.path("image/keyboard_buttons/keyboard_buttons-" 
                                + row + "-" + col + ".jpeg")
                          .addHeader("content-type", "image/jpeg")
                          .push();
                    }
                }
            }

要检查你的图片是否是通过服务器推送加载的,请打开浏览器中的开发者控制台,转到网络监控,然后加载页面。每个图片的相关信息之一应该是谁将它发送到浏览器。如果有类似 Push 或 ServerPush 的内容,说明你正在使用它!

还有更多...

服务器推送仅在 SSL 下工作。换句话说,如果你正在使用 GlassFish 5 并尝试运行这个菜谱,你的 URL 应该是这样的:

https://localhost:8181/ch04-serverpush

如果你错过了它,代码仍然可以工作,但使用 HTTP/1.0 意味着当代码请求newPushBuilder时,它将返回 null(不可用):

if (pb != null) {
   ...
}

参见

第五章:企业架构安全

本章涵盖了以下食谱:

  • 使用认证进行域保护

  • 通过授权授予权利

  • 使用 SSL/TLS 保护数据机密性和完整性

  • 使用声明式安全

  • 使用程序化安全

简介

安全无疑是软件行业有史以来最热门的话题之一,而且没有理由认为这种情况会很快改变。实际上,随着时间的推移,它可能会变得更加热门。

由于所有数据都通过云传输,经过无数的服务器、链接、数据库、会话、设备等等,您至少会期望它得到良好的保护、安全,并且其完整性得到保持。

现在,最后,Java EE 有它自己的安全 API,Soteria 是其参考实现。

安全是一个值得数十本书讨论的主题;这是一个事实。但本章将涵盖一些您可能在日常项目中遇到的常见用例。

使用认证进行域保护

认证是用于定义谁可以访问您的域的任何过程、任务和/或策略。例如,它就像您用来进入办公室的徽章。

在应用程序中,认证最常见的使用是允许已注册的用户访问您的域。

这个食谱将向您展示如何使用简单的代码和配置来控制谁可以访问以及谁不能访问您应用程序的一些资源。

准备工作

我们首先添加我们的依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点

  1. 首先,我们在web.xml文件中进行一些配置:
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>CH05-Authentication</web-resource-name>
            <url-pattern>/authServlet</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>role1</role-name>
        </auth-constraint>
    </security-constraint>

    <security-role>
        <role-name>role1</role-name>
    </security-role>
  1. 然后,我们创建一个 servlet 来处理我们的用户访问:
@DeclareRoles({"role1", "role2", "role3"})
@WebServlet(name = "/UserAuthenticationServlet", urlPatterns = {"/UserAuthenticationServlet"})
public class UserAuthenticationServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Inject
    private javax.security.enterprise.SecurityContext 
    securityContext;

    @Override
    public void doGet(HttpServletRequest request, 
    HttpServletResponse response) throws ServletException, 
    IOException {

        String name = request.getParameter("name");
        if (null != name || !"".equals(name)) {
            AuthenticationStatus status = 
            securityContext.authenticate(
                    request, response, 
                    AuthenticationParameters.withParams().credential
                    (new CallerOnlyCredential(name)));

            response.getWriter().write("Authentication status: " 
            + status.name() + "\n");
        }

        String principal = null;
        if (request.getUserPrincipal() != null) {
            principal = request.getUserPrincipal().getName();
        }

        response.getWriter().write("User: " + principal + "\n");
        response.getWriter().write("Role \"role1\" access: " + 
        request.isUserInRole("role1") + "\n");
        response.getWriter().write("Role \"role2\" access: " + 
        request.isUserInRole("role2") + "\n");
        response.getWriter().write("Role \"role3\" access: " + 
        request.isUserInRole("role3") + "\n");
        response.getWriter().write("Access to /authServlet? " + 
        securityContext.hasAccessToWebResource("/authServlet") + 
        "\n");
    }
}
  1. 最后,我们创建一个将定义我们的认证策略的类:
@ApplicationScoped
public class AuthenticationMechanism implements 
HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(HttpServletRequest 
    request, 
    HttpServletResponse response, HttpMessageContext  
    httpMessageContext) 
    throws AuthenticationException {

        if (httpMessageContext.isAuthenticationRequest()) {

            Credential credential = 
            httpMessageContext.getAuthParameters().getCredential();
            if (!(credential instanceof CallerOnlyCredential)) {
                throw new IllegalStateException("Invalid 
                mechanism");
            }

            CallerOnlyCredential callerOnlyCredential = 
            (CallerOnlyCredential) credential;

            if ("user".equals(callerOnlyCredential.getCaller())) {
                return 
                httpMessageContext.notifyContainerAboutLogin
                (callerOnlyCredential.getCaller(), new HashSet<> 
                (Arrays.asList("role1","role2")));
            } else{
                throw new AuthenticationException();
            }

        }

        return httpMessageContext.doNothing();
    }

}

如果您在一个 Java EE 8 兼容的服务器上运行此项目,您应该使用此 URL(假设您是在本地运行。如果不是,请进行适当的更改):

http://localhost:8080/ch05-authentication/UserAuthenticationServlet?name=user

这应该会生成一个包含以下信息的页面:

Authentication status: SUCCESS
User: user
Role "role1" access: true
Role "role2" access: true
Role "role3" access: false
Access to /authServlet? true

尝试对name参数进行任何更改,例如这样:

http://localhost:8080/ch05-authentication/UserAuthenticationServlet?name=anotheruser

然后,结果将如下所示:

Authentication status: SEND_FAILURE
User: null
Role "role1" access: false
Role "role2" access: false
Role "role3" access: false
Access to /authServlet? false

它是如何工作的...

让我们把之前显示的代码分开,这样我们可以更好地理解正在发生的事情。

web.xml文件中,我们创建一个安全约束:

    <security-constraint>
       ...
    </security-constraint>

我们在它内部定义一个资源:

        <web-resource-collection>
            <web-resource-name>CH05-Authentication</web-resource-name>
            <url-pattern>/authServlet</url-pattern>
        </web-resource-collection>

我们正在定义一个授权策略。在这种情况下,它是一个角色:

        <auth-constraint>
            <role-name>role1</role-name>
        </auth-constraint>

现在我们有UserAuthenticationServlet。我们应该注意这个注解:

@DeclareRoles({"role1", "role2", "role3"})

它定义了哪些角色是特定 servlet 上下文的一部分。

这个场景中的另一个重要角色是这个:

    @Inject
    private SecurityContext securityContext;

在这里,我们正在请求服务器给我们一个安全上下文,以便我们可以用它来实现我们的目的。这将在一分钟内变得有意义。

然后,如果name参数被填充,我们将到达这一行:

            AuthenticationStatus status = securityContext.authenticate(
                    request, response, withParams().credential(new 
                    CallerOnlyCredential(name)));

这将要求 Java EE 服务器处理一个认证。但是...基于什么?这就是我们的HttpAuthenticationMechanism实现发挥作用的地方。

如前所述的代码创建了CallerOnlyCredential,因此我们的认证机制将基于它:

            Credential credential = httpMessageContext.getAuthParameters()
            .getCredential();
            if (!(credential instanceof CallerOnlyCredential)) {
                throw new IllegalStateException("Invalid mechanism");
            }

            CallerOnlyCredential callerOnlyCredential = 
           (CallerOnlyCredential) credential;

一旦我们有一个credential实例,我们可以检查用户“是否存在”:

            if ("user".equals(callerOnlyCredential.getCaller())) {
                ...
            } else{
                throw new AuthenticationException();
            }

例如,我们刚刚比较了名称,但在实际情况下,你可以搜索数据库、LDAP 服务器等。

如果用户存在,我们将根据一些规则进行认证。

return httpMessageContext.notifyContainerAboutLogin
(callerOnlyCredential.getCaller(), new HashSet<>(asList("role1","role2")));

在这种情况下,我们表示用户可以访问"role1""role2"

认证完成后,它将返回到 servlet 并使用结果来完成过程:

        response.getWriter().write("Role \"role1\" access: " + 
        request.isUserInRole("role1") + "\n");
        response.getWriter().write("Role \"role2\" access: " + 
        request.isUserInRole("role2") + "\n");
        response.getWriter().write("Role \"role3\" access: " + 
        request.isUserInRole("role3") + "\n");
        response.getWriter().write("Access to /authServlet? " + 
        securityContext.hasAccessToWebResource("/authServlet") + "\n");

因此,这段代码将为"role1""role2"打印true,而为"role3"打印false。因为"/authServlet""role1"是允许的,所以用户将能够访问它。

参见

通过授权授予权限

如果认证是定义谁可以访问特定资源的方式,那么授权就是定义用户一旦获得域访问权限后可以做什么和不能做什么的方式。

这就像允许某人进入你的房子,但拒绝他们访问你的电视遥控器(顺便说一句,这是一个非常重要的访问权限)。或者,允许访问遥控器,但拒绝访问成人频道。

一种实现方式是通过配置文件,这正是我们将在这个菜谱中要做的。

准备工作

让我们先添加依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何实现...

  1. 首先,我们在一个单独的类中定义一些角色,以便我们可以重用它:
public class Roles {
    public static final String ROLE1 = "role1";
    public static final String ROLE2 = "role2";
    public static final String ROLE3 = "role3";
}
  1. 然后我们定义一些应用程序用户可以执行的操作:
@Stateful
public class UserActivity {

    @RolesAllowed({Roles.ROLE1})
    public void role1Allowed(){
        System.out.println("role1Allowed executed");
    }

    @RolesAllowed({Roles.ROLE2})
    public void role2Allowed(){
        System.out.println("role2Allowed executed");
    }

    @RolesAllowed({Roles.ROLE3})
    public void role3Allowed(){
        System.out.println("role3Allowed executed");
    }

    @PermitAll
    public void anonymousAllowed(){
        System.out.println("anonymousAllowed executed");
    }

    @DenyAll
    public void noOneAllowed(){
        System.out.println("noOneAllowed executed");
    } 

}
  1. 让我们创建一个用于可执行任务的接口:
public interface Executable {
    void execute() throws Exception;
}
  1. 然后让我们为执行它们的角色创建另一个:
public interface RoleExecutable {
    void run(Executable executable) throws Exception;
}
  1. 对于每个角色,我们创建一个执行器。它将像是一个拥有该角色权利的环境:
@Named
@RunAs(Roles.ROLE1)
public class Role1Executor implements RoleExecutable {

    @Override
    public void run(Executable executable) throws Exception {
        executable.execute();
    }
}
@Named
@RunAs(Roles.ROLE2)
public class Role2Executor implements RoleExecutable {

    @Override
    public void run(Executable executable) throws Exception {
        executable.execute();
    }
}
@Named
@RunAs(Roles.ROLE3)
public class Role3Executor implements RoleExecutable {

    @Override
    public void run(Executable executable) throws Exception {
        executable.execute();
    }
}
  1. 然后我们实现HttpAuthenticationMechanism
@ApplicationScoped
public class AuthenticationMechanism implements 
HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(HttpServletRequest 
     request, HttpServletResponse response, HttpMessageContext 
     httpMessageContext) throws AuthenticationException {

        if (httpMessageContext.isAuthenticationRequest()) {

            Credential credential = 
            httpMessageContext.getAuthParameters()
            .getCredential();
            if (!(credential instanceof CallerOnlyCredential)) {
                throw new IllegalStateException("Invalid 
                mechanism");
            }

            CallerOnlyCredential callerOnlyCredential = 
            (CallerOnlyCredential) credential;

            if (null == callerOnlyCredential.getCaller()) {
                throw new AuthenticationException();
            } else switch (callerOnlyCredential.getCaller()) {
                case "user1":
                    return  
                    httpMessageContext.
                    notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(),
                     new HashSet<>
                    (asList(Roles.ROLE1)));
                case "user2":
                    return 
                    httpMessageContext.
                    notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(), 
                     new HashSet<>
                    (asList(Roles.ROLE2)));
                case "user3":
                    return 
                    httpMessageContext.
                    notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(), 
                     new HashSet<>
                    (asList(Roles.ROLE3)));
                default:
                    throw new AuthenticationException();
            }

        }

        return httpMessageContext.doNothing();
    }

}
  1. 最后,我们创建一个管理所有这些资源的 servlet:
@DeclareRoles({Roles.ROLE1, Roles.ROLE2, Roles.ROLE3})
@WebServlet(name = "/UserAuthorizationServlet", urlPatterns = {"/UserAuthorizationServlet"})
public class UserAuthorizationServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Inject
    private SecurityContext securityContext;

    @Inject
    private Role1Executor role1Executor;

    @Inject
    private Role2Executor role2Executor;

    @Inject
    private Role3Executor role3Executor;

    @Inject
    private UserActivity userActivity;

    @Override
    public void doGet(HttpServletRequest request, 
    HttpServletResponse 
    response) throws ServletException, IOException {

        try {
            String name = request.getParameter("name");
            if (null != name || !"".equals(name)) {
                AuthenticationStatus status = 
                securityContext.authenticate(
                        request, response, withParams().credential(
                        new CallerOnlyCredential(name)));

                response.getWriter().write("Authentication 
                status: " + status.name() + "\n");
            }

            String principal = null;
            if (request.getUserPrincipal() != null) {
                principal = request.getUserPrincipal().getName();
            }

            response.getWriter().write("User: " + principal +
            "\n");
            response.getWriter().write("Role \"role1\" access: " + 
            request.isUserInRole(Roles.ROLE1) + "\n");
            response.getWriter().write("Role \"role2\" access: " + 
            request.isUserInRole(Roles.ROLE2) + "\n");
            response.getWriter().write("Role \"role3\" access: " + 
            request.isUserInRole(Roles.ROLE3) + "\n");

            RoleExecutable executable = null;

            if (request.isUserInRole(Roles.ROLE1)) {
                executable = role1Executor;
            } else if (request.isUserInRole(Roles.ROLE2)) {
                executable = role2Executor;
            } else if (request.isUserInRole(Roles.ROLE3)) {
                executable = role3Executor;
            }

            if (executable != null) {
                executable.run(() -> {
                    try {
                        userActivity.role1Allowed();
                        response.getWriter().write("role1Allowed 
                        executed: true\n");
                    } catch (Exception e) {
                        response.getWriter().write("role1Allowed  
                        executed: false\n");
                    }

                    try {
                        userActivity.role2Allowed();
                        response.getWriter().write("role2Allowed 
                        executed: true\n");
                    } catch (Exception e) {
                        response.getWriter().write("role2Allowed  
                        executed: false\n");
                    }

                    try {
                        userActivity.role3Allowed();
                        response.getWriter().write("role2Allowed 
                        executed: true\n");
                    } catch (Exception e) {
                        response.getWriter().write("role2Allowed 
                        executed: false\n");
                    }

                });

            }

            try {
                userActivity.anonymousAllowed();
                response.getWriter().write("anonymousAllowed  
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("anonymousAllowed 
                executed: false\n");
            }

            try {
                userActivity.noOneAllowed();
                response.getWriter().write("noOneAllowed 
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("noOneAllowed 
                executed: false\n");
            } 

        } catch (Exception ex) {
            System.err.println(ex.getMessage());
        }

    }
}

要尝试这段代码,你可以运行以下 URL:

  • http://localhost:8080/ch05-authorization/UserAuthorizationServlet?name=user1

  • http://localhost:8080/ch05-authorization/UserAuthorizationServlet?name=user2

  • http://localhost:8080/ch05-authorization/UserAuthorizationServlet?name=user3

例如,user1的结果将如下所示:

Authentication status: SUCCESS
User: user1
Role "role1" access: true
Role "role2" access: false
Role "role3" access: false
role1Allowed executed: true
role2Allowed executed: false
role2Allowed executed: false
anonymousAllowed executed: true
noOneAllowed executed: false

如果你尝试使用一个不存在的用户,结果将如下所示:

Authentication status: SEND_FAILURE
User: null
Role "role1" access: false
Role "role2" access: false
Role "role3" access: false
anonymousAllowed executed: true
noOneAllowed executed: false

它是如何工作的...

好吧,这里发生了很多事情!让我们从我们的UserActivity类开始。

我们使用@RolesAllowed注解来定义可以访问该类每个方法的角色:

    @RolesAllowed({Roles.ROLE1})
    public void role1Allowed(){
        System.out.println("role1Allowed executed");
    }

你可以在注解内添加多个角色(它是一个数组)。

我们还有两个其他有趣的注解,@PermitAll@DenyAll

  • @PermitAll 注解允许任何人访问该方法,甚至无需任何认证。

  • @DenyAll 注解拒绝所有人访问该方法,即使是拥有最高权限的已认证用户。

然后我们有了我们所说的执行器:

@Named
@RunAs(Roles.ROLE1)
public class Role1Executor implements RoleExecutable {

    @Override
    public void run(Executable executable) throws Exception {
        executable.execute();
    }
}

我们在类级别使用了 @RunAs 注解,这意味着这个类继承了定义的角色(在这种情况下,"role1")的所有权限。这意味着这个类的每一个方法都将拥有 "role1" 权限。

现在,看看 UserAuthorizationServlet,在开始的地方有一个重要的对象:

    @Inject
    private SecurityContext securityContext;

在这里,我们要求服务器给我们一个安全上下文实例,以便我们可以用它进行认证。

然后,如果 name 参数被填写,我们就到达这一行:

            AuthenticationStatus status = securityContext.authenticate(
                    request, response, withParams().credential(new 
                    CallerOnlyCredential(name)));

这将要求 Java EE 服务器处理认证。这就是我们的 HttpAuthenticationMechanism 实现发挥作用的地方。

由于前面的代码创建了 CallerOnlyCredential,我们的认证机制将基于它:

            Credential credential = httpMessageContext.getAuthParameters().
            getCredential();
            if (!(credential instanceof CallerOnlyCredential)) {
                throw new IllegalStateException("Invalid mechanism");
            }

            CallerOnlyCredential callerOnlyCredential = 
           (CallerOnlyCredential) credential;

一旦我们有了凭证实例,我们可以检查用户是否存在:

            if (null == callerOnlyCredential.getCaller()) {
                throw new AuthenticationException();
            } else switch (callerOnlyCredential.getCaller()) {
                case "user1":
                    return httpMessageContext.notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(), new HashSet<>
                    (asList(Roles.ROLE1)));
                case "user2":
                    return httpMessageContext.notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(), new HashSet<>
                    (asList(Roles.ROLE2)));
                case "user3":
                    return httpMessageContext.notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(), new HashSet<>
                    (asList(Roles.ROLE3)));
                default:
                    throw new AuthenticationException();
            }

因此,我们说 "user1" 可以访问 "role1""user2" 可以访问 "role2",依此类推。

一旦定义了用户角色,我们就回到了 servlet,并可以选择使用哪个环境(执行器):

            if (request.isUserInRole(Roles.ROLE1)) {
                executable = role1Executor;
            } else if (request.isUserInRole(Roles.ROLE2)) {
                executable = role2Executor;
            } else if (request.isUserInRole(Roles.ROLE3)) {
                executable = role3Executor;
            }

然后我们尝试 UserActivity 类的所有方法。只有允许该特定角色的方法将被执行;其他方法将抛出异常,除了 @PermitAll 方法,它无论如何都会运行,以及 @DenyAll,它无论如何都不会运行。

参见

使用 SSL/TLS 保护数据机密性和完整性

安全性也意味着保护你的数据传输,为此我们有了最流行的方法,被称为安全套接字层SSL)。

传输层安全性,或TLS,是 SSL 的最新版本。因此,GlassFish 5.0 支持 SSL 3.0 和 TLS 1.0 作为协议。

这个菜谱将向你展示如何使 GlassFish 5.0 能够正确地与 SSL 一起工作。所有 Java EE 服务器都有自己实现这一点的独特方式。

准备工作

要在 GlassFish 中启用 SSL,你需要配置 SSL HTTP 监听器。你只需要做以下这些:

  1. 确保 GlassFish 正在运行。

  2. 使用 create-ssl 命令创建你的 SSL HTTP 监听器。

  3. 重新启动 GlassFish 服务器。

如何做到这一点...

  1. 要完成这个任务,你需要访问 GlassFish 的远程命令行界面CLI)。你可以通过访问以下路径来完成:

$GLASSFISH_HOME/bin

  1. 一旦你到了那里,执行以下命令:
./asadmin
  1. 当提示准备好后,你可以执行这个命令:
create-ssl --type http-listener --certname cookbookCert http-listener-1
  1. 然后您可以重新启动服务器,您的http-listener-1将使用 SSL 工作。如果您想从监听器中删除 SSL,只需回到提示并执行此命令:
delete-ssl --type http-listener http-listener-1

它是如何工作的...

使用 SSL,客户端和服务器在发送数据之前加密数据,在接收数据时解密数据。当浏览器打开一个加密网站(使用 HTTPS)时,会发生一个称为握手的过程。

在握手过程中,浏览器向服务器请求一个会话;服务器通过发送证书和公钥来回答。浏览器验证证书,如果有效,则生成一个唯一的会话密钥,使用服务器公钥加密它,并将其发送回服务器。一旦服务器收到会话密钥,它就会使用其私钥解密它。

现在,客户端和服务器,只有它们,拥有会话密钥的副本,并可以确保通信是安全的。

还有更多...

强烈建议您使用来自认证机构CA)的证书,而不是像我们在本配方中那样使用自签名证书。

您可以查看letsencrypt.org,在那里您可以获取免费的证书。

使用它的过程是相同的;你只需更改--certname参数的值。

参见

使用声明式安全

在构建应用程序的安全功能时,你可以基本上使用两种方法:编程安全声明式安全

  • 编程方法是指您使用代码定义应用程序的安全策略。

  • 声明式方法是指通过声明策略然后相应地应用它们来执行。

这个配方将向你展示声明式方法。

准备工作

让我们从添加依赖项开始:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 让我们为我们的应用程序创建一个角色列表:
public class Roles {
    public static final String ADMIN = "admin";
    public static final String USER = "user";
}
  1. 然后我们创建一个只能由一个角色执行的任务列表,一个每个人都可以执行的任务,以及一个没有人可以执行的任务:
@Stateful
public class UserBean {

    @RolesAllowed({Roles.ADMIN})
    public void adminOperation(){
        System.out.println("adminOperation executed");
    }

    @RolesAllowed({Roles.USER})
    public void userOperation(){
        System.out.println("userOperation executed");
    }

    @PermitAll
    public void everyoneCanDo(){
        System.out.println("everyoneCanDo executed");
    }

    @DenyAll
    public void noneCanDo(){
        System.out.println("noneCanDo executed");
    } 

}
  1. 现在,我们为USERADMIN角色创建一个环境,以便它们执行各自的操作:
@Named
@RunAs(Roles.USER)
public class UserExecutor implements RoleExecutable {

    @Override
    public void run(Executable executable) throws Exception {
        executable.execute();
    }
}
@Named
@RunAs(Roles.ADMIN)
public class AdminExecutor implements RoleExecutable {

    @Override
    public void run(Executable executable) throws Exception {
        executable.execute();
    }
}
  1. 然后我们实现HttpAuthenticationMechanism:
@ApplicationScoped
public class AuthenticationMechanism implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(HttpServletRequest 
    request, HttpServletResponse response, HttpMessageContext 
    httpMessageContext) 
    throws AuthenticationException {

        if (httpMessageContext.isAuthenticationRequest()) {

            Credential credential = 
            httpMessageContext.getAuthParameters().
            getCredential();
            if (!(credential instanceof CallerOnlyCredential)) {
                throw new IllegalStateException("Invalid 
                mechanism");
            }

            CallerOnlyCredential callerOnlyCredential = 
            (CallerOnlyCredential) 
            credential;

            if (null == callerOnlyCredential.getCaller()) {
                throw new AuthenticationException();
            } else switch (callerOnlyCredential.getCaller()) {
                case Roles.ADMIN:
                    return httpMessageContext
                    .notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(),
                     new HashSet<>
                    (asList(Roles.ADMIN)));
                case Roles.USER:
                    return httpMessageContext
                   .notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(),
                    new HashSet<> 
                    (asList(Roles.USER)));
                default:
                    throw new AuthenticationException();
            }

        }

        return httpMessageContext.doNothing();
    }

}
  1. 最后,我们为每个角色(USERADMIN)创建一个 servlet:
@DeclareRoles({Roles.ADMIN, Roles.USER})
@WebServlet(name = "/UserServlet", urlPatterns = {"/UserServlet"})
public class UserServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Inject
    private SecurityContext securityContext;

    @Inject
    private UserExecutor userExecutor;

    @Inject
    private UserBean userActivity;

    @Override
    public void doGet(HttpServletRequest request, 
    HttpServletResponse response) throws ServletException, 
    IOException {

        try {
            securityContext.authenticate(
                    request, response, withParams().credential(new 
                    CallerOnlyCredential(Roles.USER)));

            response.getWriter().write("Role \"admin\" access: " + 
            request.isUserInRole(Roles.ADMIN) + "\n");
            response.getWriter().write("Role \"user\" access: " + 
            request.isUserInRole(Roles.USER) + "\n");

            userExecutor.run(() -> {
                try {
                    userActivity.adminOperation();
                    response.getWriter().write("adminOperation 
                    executed: true\n");
                } catch (Exception e) {
                    response.getWriter().write("adminOperation 
                    executed: false\n");
                }

                try {
                    userActivity.userOperation();
                    response.getWriter().write("userOperation 
                    executed: true\n");
                } catch (Exception e) {
                    response.getWriter().write("userOperation  
                    executed: false\n");
                }

            });

            try {
                userActivity.everyoneCanDo();
                response.getWriter().write("everyoneCanDo 
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("everyoneCanDo
                executed: false\n");
            }

            try {
                userActivity.noneCanDo();
                response.getWriter().write("noneCanDo 
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("noneCanDo 
                executed: false\n");
            }

        } catch (Exception ex) {
            System.err.println(ex.getMessage());
        }

    }
}
@DeclareRoles({Roles.ADMIN, Roles.USER})
@WebServlet(name = "/AdminServlet", urlPatterns = {"/AdminServlet"})
public class AdminServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Inject
    private SecurityContext securityContext;

    @Inject
    private AdminExecutor adminExecutor;

    @Inject
    private UserBean userActivity;

    @Override
    public void doGet(HttpServletRequest request, 
    HttpServletResponse 
    response) throws ServletException, IOException {

        try {
            securityContext.authenticate(
                    request, response, withParams().credential(new 
                    CallerOnlyCredential(Roles.ADMIN)));

            response.getWriter().write("Role \"admin\" access: " + 
            request.isUserInRole(Roles.ADMIN) + "\n");
            response.getWriter().write("Role \"user\" access: " + 
            request.isUserInRole(Roles.USER) + "\n");

            adminExecutor.run(() -> {
                try {
                    userActivity.adminOperation();
                    response.getWriter().write("adminOperation 
                    executed: true\n");
                } catch (Exception e) {
                    response.getWriter().write("adminOperation 
                    executed: false\n");
                }

                try {
                    userActivity.userOperation();
                    response.getWriter().write("userOperation 
                    executed: true\n");
                } catch (Exception e) {
                    response.getWriter().write("userOperation 
                    executed: false\n");
                }

            });

            try {
                userActivity.everyoneCanDo();
                response.getWriter().write("everyoneCanDo 
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("everyoneCanDo
                executed: false\n");
            }

            try {
                userActivity.noneCanDo();
                response.getWriter().write("noneCanDo 
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("noneCanDo 
                executed: false\n");
            }

        } catch (Exception ex) {
            System.err.println(ex.getMessage());
        }

    }
}

它是如何工作的...

查看UserServlet(适用于USER角色),我们首先看到认证步骤:

            securityContext.authenticate(
                    request, response, withParams().credential(new 
                    CallerOnlyCredential(Roles.ADMIN)));

例如,我们使用角色名称作为用户名,因为如果我们查看AuthenticationMechanism类(实现HttpAuthenticationMechanism),我们会看到它在执行所有认证和为用户分配正确角色的艰苦工作:

            Credential credential = 
            httpMessageContext.getAuthParameters()
            .getCredential();
            if (!(credential instanceof CallerOnlyCredential)) {
                throw new IllegalStateException("Invalid mechanism");
            }

            CallerOnlyCredential callerOnlyCredential = 
            (CallerOnlyCredential) 
            credential;

            if (null == callerOnlyCredential.getCaller()) {
                throw new AuthenticationException();
            } else switch (callerOnlyCredential.getCaller()) {
                case Roles.ADMIN:
                    return httpMessageContext.notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(), new HashSet<>
                    (asList(Roles.ADMIN)));
                case Roles.USER:
                    return httpMessageContext.notifyContainerAboutLogin
                    (callerOnlyCredential.getCaller(), new HashSet<>
                    (asList(Roles.USER)));
                default:
                    throw new AuthenticationException();
            }

然后回到我们的UserServlet,现在用户已经分配了适当的角色,他们能做什么以及不能做什么就只是个问题了:

            userExecutor.run(() -> {
                try {
                    userActivity.adminOperation();
                    response.getWriter().write("adminOperation  
                    executed: true\n");
                } catch (Exception e) {
                    response.getWriter().write("adminOperation 
                    executed: false\n");
                }

                try {
                    userActivity.userOperation();
                    response.getWriter().write("userOperation 
                    executed: true\n");
                } catch (Exception e) {
                    response.getWriter().write("userOperation 
                    executed:  false\n");
                }

            });

此外,我们还尝试了每个人都可以执行以及没有人可以执行的任务:

            try {
                userActivity.everyoneCanDo();
                response.getWriter().write("everyoneCanDo 
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("everyoneCanDo 
                executed: false\n");
            }

            try {
                userActivity.noneCanDo();
                response.getWriter().write("noneCanDo 
                executed: true\n");
            } catch (Exception e) {
                response.getWriter().write("noneCanDo 
                executed: false\n");
            }

AdminServlet类使用AdminExecutor环境执行完全相同的步骤,因此为了节省空间,我们将省略它。

要尝试这段代码,只需在 Java EE 8 兼容的服务器上使用以下 URL 运行它:

  • http://localhost:8080/ch05-declarative/AdminServlet

  • http://localhost:8080/ch05-declarative/UserServlet

AdminServlet的结果示例将如下所示:

Role "admin" access: true
Role "user" access: false
adminOperation executed: true
userOperation executed: false
everyoneCanDo executed: true
noneCanDo executed: false

参见

使用程序化安全

我们已经看到了声明式方法,现在让我们看看程序化方法。

准备中

让我们先添加依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 让我们先定义我们的角色列表:
public class Roles {
    public static final String ADMIN = "admin";
    public static final String USER = "user";
}
  1. 然后,让我们定义一个基于角色的任务列表:
@Stateful
public class UserBean {

    @RolesAllowed({Roles.ADMIN})
    public void adminOperation(){
        System.out.println("adminOperation executed");
    }

    @RolesAllowed({Roles.USER})
    public void userOperation(){
        System.out.println("userOperation executed");
    }

    @PermitAll
    public void everyoneCanDo(){
        System.out.println("everyoneCanDo executed");
    }

}
  1. 现在,让我们实现IndentityStore接口。在这里,我们定义了验证用户身份的策略:
@ApplicationScoped
public class UserIdentityStore implements IdentityStore {

    @Override
    public CredentialValidationResult validate(Credential credential) {
        if (credential instanceof UsernamePasswordCredential) {
            return validate((UsernamePasswordCredential) credential);
        }

        return CredentialValidationResult.NOT_VALIDATED_RESULT;
    }

    public CredentialValidationResult validate(UsernamePasswordCredential 
    usernamePasswordCredential) {

        if (usernamePasswordCredential.
        getCaller().equals(Roles.ADMIN)
                && usernamePasswordCredential.
                getPassword().compareTo("1234")) 
        {

            return new CredentialValidationResult(
                    new CallerPrincipal
                    (usernamePasswordCredential.getCaller()),
                    new HashSet<>(Arrays.asList(Roles.ADMIN)));
        } else if (usernamePasswordCredential.
          getCaller().equals(Roles.USER)
                && usernamePasswordCredential.
                getPassword().compareTo("1234")) 
        {

            return new CredentialValidationResult(
                    new CallerPrincipal
                    (usernamePasswordCredential.getCaller()),
                    new HashSet<>(Arrays.asList(Roles.USER)));
        }

        return CredentialValidationResult.INVALID_RESULT;
    }

}
  1. 这里,我们实现了HttpAuthenticationMethod接口:
@ApplicationScoped
public class AuthenticationMechanism implements HttpAuthenticationMechanism {

    @Inject
    private UserIdentityStore identityStore;

    @Override
    public AuthenticationStatus validateRequest(HttpServletRequest 
    request, 
    HttpServletResponse response, HttpMessageContext 
    httpMessageContext) 
    throws AuthenticationException {

        if (httpMessageContext.isAuthenticationRequest()) {

            Credential credential = 
            httpMessageContext.getAuthParameters()
            .getCredential();
            if (!(credential instanceof UsernamePasswordCredential)) {
                throw new IllegalStateException("Invalid 
                mechanism");
            }

            return httpMessageContext.notifyContainerAboutLogin
            (identityStore.validate(credential));
        }

        return httpMessageContext.doNothing();
    }

}
  1. 最后,我们创建一个 servlet,用户将在这里进行身份验证并执行他们的操作:
@DeclareRoles({Roles.ADMIN, Roles.USER})
@WebServlet(name = "/OperationServlet", urlPatterns = {"/OperationServlet"})
public class OperationServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Inject
    private SecurityContext securityContext;

    @Inject
    private UserBean userActivity;

    @Override
    public void doGet(HttpServletRequest request, 
    HttpServletResponse 
    response) throws ServletException, IOException {

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

        Credential credential = new UsernamePasswordCredential(name, 
        new Password(password));

        AuthenticationStatus status = securityContext.authenticate(
                request, response, 
        withParams().credential(credential));

        response.getWriter().write("Role \"admin\" access: " + 
        request.isUserInRole(Roles.ADMIN) + "\n");
        response.getWriter().write("Role \"user\" access: " + 
        request.isUserInRole(Roles.USER) + "\n");

        if (status.equals(AuthenticationStatus.SUCCESS)) {

            if (request.isUserInRole(Roles.ADMIN)) {
                userActivity.adminOperation();
                response.getWriter().write("adminOperation 
                executed: true\n");
            } else if (request.isUserInRole(Roles.USER)) {
                userActivity.userOperation();
                response.getWriter().write("userOperation 
                executed: true\n");
            }

            userActivity.everyoneCanDo();
            response.getWriter().write("everyoneCanDo 
            executed: true\n");

        } else {
            response.getWriter().write("Authentication failed\n");
        }

    }
}

要尝试这段代码,请在 Java EE 8 兼容的服务器上使用以下 URL 运行它:

  • http://localhost:8080/ch05-programmatic/OperationServlet?name=user&password=1234

  • http://localhost:8080/ch05-programmatic/OperationServlet?name=admin&password=1234

ADMIN角色的一个示例结果如下:

Role "admin" access: true
Role "user" access: false
adminOperation executed: true
everyoneCanDo executed: true

如果您使用错误的用户名/密码对,您将得到以下结果:

Role "admin" access: false
Role "user" access: false
Authentication failed

它是如何工作的...

与声明式方法(参见本章前面的菜谱)相反,这里我们使用代码来验证用户。我们通过实现IdentityStore接口来完成它。

例如,即使我们已经硬编码了密码,您也可以使用相同的代码片段来验证密码与数据库、LDAP、外部端点等:

        if (usernamePasswordCredential.getCaller().equals(Roles.ADMIN)
                && 
        usernamePasswordCredential.getPassword().compareTo("1234")) 
        {

            return new CredentialValidationResult(
                    new CallerPrincipal(usernamePasswordCredential
                    .getCaller()),
                    new HashSet<>(asList(Roles.ADMIN)));
        } else if (usernamePasswordCredential.getCaller()
          .equals(Roles.USER)
                && usernamePasswordCredential.
                getPassword().compareTo("1234")) 
        {

            return new CredentialValidationResult(
                    new CallerPrincipal(usernamePasswordCredential
                   .getCaller()),
                    new HashSet<>(asList(Roles.USER)));
        }

        return INVALID_RESULT;

使用IdentityStore进行身份验证意味着只是通过HttpAuthenticationMethod进行委托:

            Credential credential = 
            httpMessageContext.getAuthParameters().getCredential();
            if (!(credential instanceof UsernamePasswordCredential)) {
                throw new IllegalStateException("Invalid mechanism");
            }

            return httpMessageContext.notifyContainerAboutLogin
            (identityStore.validate(credential));

然后,OperationServlet将尝试进行身份验证:

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

        Credential credential = new UsernamePasswordCredential(name, 
        new Password(password));

        AuthenticationStatus status = securityContext.authenticate(
                request, response, 
               withParams().credential(credential));

基于此,我们将定义接下来会发生什么流程:

        if (status.equals(AuthenticationStatus.SUCCESS)) {

            if (request.isUserInRole(Roles.ADMIN)) {
                userActivity.adminOperation();
                response.getWriter().write("adminOperation 
                executed: true\n");
            } else if (request.isUserInRole(Roles.USER)) {
                userActivity.userOperation();
                response.getWriter().write("userOperation 
                executed: true\n");
            }

            userActivity.everyoneCanDo();
            response.getWriter().write("everyoneCanDo executed:

        true\n");

        } else {
            response.getWriter().write("Authentication failed\n");
        }

注意!这是您定义每个角色将做什么的代码。

参见

第六章:通过依赖标准减少编码工作量

本章涵盖了以下食谱:

  • 准备你的应用使用连接池

  • 使用消息服务进行异步通信

  • 理解 servlet 的生命周期

  • 事务管理

简介

关于 Java EE,你需要了解的最重要概念之一是:它是一个标准。如果你访问Java 社区进程JCP)网站,你会找到 Java EE 平台(对于版本 8 是 JSR 366)的Java 规范请求JSR)。

一个标准...是什么?嗯,对于一个应用服务器!例如,一个 Java EE 应用服务器。

这意味着你可以开发 Java EE 应用,知道它将在一个提供了一堆你可以依赖的资源的环境中运行。

这也意味着你可以轻松地从一台应用服务器切换到另一台,只要你坚持使用 Java EE 模式而不是某些供应商特定的功能(被认为是不良实践)。无论你使用什么 Java EE 兼容服务器,你的应用都应该保持相同的行为。

哦,是的!Java EE 不仅是一个标准,还是一个认证。一个 Java EE 服务器要想被认为兼容,必须通过一系列测试来保证它实现了规范中的每一个点(JSR)。

这个令人惊叹的生态系统允许你减少应用的编码量,并给你机会专注于对你或你的客户真正重要的事情。如果没有标准环境,你需要实现自己的代码来管理请求/响应、队列、连接池和其他东西。

如果你愿意,你当然可以这样做,但你不一定必须这样做。实际上,如果你想的话,甚至可以编写自己的 Java EE 应用服务器。

话虽如此,让我们继续本章的内容!在接下来的食谱中,你将学习如何利用你最喜欢的 Java EE 应用服务器上已经实现的一些酷炫功能。

示例将基于 GlassFish 5,但正如我之前提到的,它们应该对任何其他兼容实现都有相同的行为。

准备你的应用使用连接池

在我们的一生中,在进食之后,我们应该学习的第一件事就是使用连接池。尤其是在我们谈论数据库时。这正是这里讨论的情况。

为什么?因为与数据库打开的连接在资源使用上代价很高。更糟糕的是,如果我们更仔细地看看打开新连接的过程,它会消耗大量的 CPU 资源,例如。

如果你只有两个用户使用包含几个表中的几个记录的数据库,可能不会有太大的区别。但是,如果你有数十个用户,或者数据库很大,当你有数百个用户使用大型数据库时,它可能会开始引起麻烦。

实际上,我自己在 J2EE 1.3 的早期(那一年是 2002 年),看到有一个由 20 人使用的应用程序中的连接池解决了性能问题。用户不多,但数据库真的很大,设计得也不是很好(应用程序也是如此,我必须说)。

但你可能想说:为什么连接池能帮助我们解决这个问题?因为一旦配置好,服务器将在启动时打开你请求的所有连接,并为你管理它们。

你唯一需要做的就是问:“嘿,服务器!你能借我一个数据库连接吗?”,用完后(这意味着尽可能快地)友好地归还它。

这个菜谱将向你展示如何操作。

准备就绪

首先,将正确的依赖项添加到你的项目中:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如果你还没有将 GlassFish 5 下载到你的开发环境,现在是做这件事的正确时机。

如何操作...

  1. 我们将首先在 GlassFish 5 中配置我们的连接池。一旦启动并运行,请访问以下 URL:

http://localhost:8080

  1. 现在点击“转到管理控制台”链接,或者如果你更喜欢,直接访问以下 URL:

http://localhost:4848/

  1. 然后按照左侧菜单中的以下路径:

资源 | JDBC | JDBC 连接池

  1. 点击“新建”按钮。它将打开“新建 JDBC 连接池”页面。填写字段,如此处所述:
  • 连接池名称:MysqlPool

  • 资源类型:javax.sql.DataSource

  • 数据库驱动供应商:MySql

当然,你可以根据自己的喜好进行自定义选择,但那时我们将遵循不同的路径!

  1. 点击“下一步”按钮。它将打开我们的池创建过程的第二步。

这个新页面有三个部分:常规设置、池设置和事务及附加属性。对于我们的菜谱,我们只处理常规设置和附加属性。

  1. 在常规设置部分,确保数据源类名已选择此值:

com.mysql.jdbc.jdbc2.optional.MysqlDatasource

  1. 现在,让我们转到附加属性部分。可能会有很多属性列出来,但我们只需填写其中的一些:
  • 数据库名称:sys

  • 服务器名称:localhost

  • 用户:root

  • 密码:mysql

  • 端口号:3306

  1. 点击“完成”按钮,哇!你的连接池已经准备好了...或者几乎准备好了。

在进行更多配置之前,你无法访问它。在相同菜单的左侧,按照以下路径:

资源 | JDBC | JDBC 资源

  1. 点击“新建”按钮,然后填写如下字段:
  • JNDI 名称:jdbc/MysqlPool

  • 连接池名称:MysqlPool

现在你已经准备好了!你的连接池已经准备好使用。让我们构建一个简单的应用程序来测试它:

  1. 首先,我们创建一个类来从连接池获取连接:
public class ConnectionPool {

    public static Connection getConnection() throws SQLException, 
    NamingException {
        InitialContext ctx = new InitialContext();
        DataSource ds = (DataSource) ctx.lookup("jdbc/MysqlPool");

        return ds.getConnection();
    }
}
  1. 然后,一个我们将用作sys_config表(MySQL 的系统表)表示的类:
public class SysConfig {

    private final String variable;
    private final String value;

    public SysConfig(String variable, String value) {
        this.variable = variable;
        this.value = value;
    }

    public String getVariable() {
        return variable;
    }

    public String getValue() {
        return value;
    }
}
  1. 这里我们创建另一个类,根据从数据库返回的数据创建一个列表:
@Stateless
public class SysConfigBean {

    public String getSysConfig() throws SQLException, NamingException {
        String sql = "SELECT variable, value FROM sys_config";

        try (Connection conn = ConnectionPool.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
                ResultSet rs = ps.executeQuery()
                Jsonb jsonb = JsonbBuilder.create()) {

            List<SysConfig> list = new ArrayList<>();
            while (rs.next()) {
                list.add(new SysConfig(rs.getString("variable"), 
                rs.getString("value")));
            }

            Jsonb jsonb = JsonbBuilder.create();
            return jsonb.toJson(list);
        }
    }
}
  1. 最后,一个将尝试所有操作的 servlet:
@WebServlet(name = "PoolTestServlet", urlPatterns = {"/PoolTestServlet"})
public class PoolTestServlet extends HttpServlet {

    @EJB
    private SysConfigBean config;

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

        try (PrintWriter writer = response.getWriter()) {
            config = new SysConfigBean();
            writer.write(config.getSysConfig());
        } catch (SQLException | NamingException ex) {
            System.err.println(ex.getMessage());
        }
    }
}

要尝试它,只需在浏览器中打开此 URL:

http://localhost:8080/ch06-connectionpooling/PoolTestServlet

还有更多...

决定您的连接池将保留多少连接,以及所有其他参数,是基于诸如数据类型、数据库设计、应用程序和用户行为等因素做出的架构决策。我们完全可以写一本书来讨论这个问题。

但如果您是从零开始,或者仍然不需要太多信息,请考虑一个介于 10% 到 20% 的并发用户数。换句话说,如果您的应用程序有,例如,100 个并发用户,您应该向您的池提供 10 到 20 个连接。

如果某些方法从连接池获取连接所需的时间过长(这应该根本不需要时间),您就会知道您的连接不足。这意味着在那个时刻服务器没有可用的连接。

因此,您需要检查是否有某些方法执行时间过长,或者您的代码中某些部分没有关闭连接(考虑将连接返回给服务器)。根据问题,这可能不是连接池问题,而是一个设计问题。

处理连接池的另一个重要事项是使用我们在这里使用的 "try-with-resources" 语句:

        try (Connection conn = ConnectionPool.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
                ResultSet rs = ps.executeQuery()) {

这将保证一旦方法执行完毕,这些资源将被正确关闭,并处理它们各自的异常,同时也有助于您编写更少的代码。

参见

使用消息服务进行异步通信

由 Java EE 提供的消息服务,通过 Java 消息服务JMS)API 实现,是 Java EE 环境提供的重要且多功能特性之一。

它使用生产者-消费者方法,其中一个对等方(生产者)将消息放入队列,另一个对等方(消费者)从那里读取消息。

生产者和消费者可以是不同的应用程序,甚至使用不同的技术。

此菜谱将向您展示如何使用 GlassFish 5 构建消息服务。每个 Java EE 服务器都有其设置服务的方式,因此如果您使用其他实现,您应该查看其文档。

另一方面,这里生成的 Java EE 代码将在任何 Java EE 8 兼容的实现上运行。标准总是胜出!

准备工作

首先向您的项目中添加适当的依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 我们将首先在 GlassFish 5 中配置我们的消息服务。一旦服务器启动并运行,请访问以下 URL:

http://localhost:8080

  1. 现在点击 "转到管理控制台" 链接,或者如果您愿意,可以直接访问以下 URL:

http://localhost:4848/

然后遵循左侧菜单中的此路径:

资源 | JMS 资源 | 连接工厂

  1. 点击“新建”按钮。当页面打开时,填写“常规设置”部分的字段如下:
  • JNDI 名称:jms/JmsFactory

  • 资源类型:javax.jms.ConnectionFactory

我们不会在这里触摸“池设置”部分,所以只需点击“确定”按钮来注册你的新工厂。

  1. 现在在左侧菜单中按照以下路径操作:

资源 | JMS 资源 | 目标资源

  1. 点击“新建”按钮。当页面打开时,填写如下所示的章节字段:
  • JNDI 名称:jms/JmsQueue

  • 物理目标名称:JmsQueue

  • 资源类型:javax.jms.Queue

点击“确定”按钮,你就准备好了!现在你有一个连接工厂来访问你的 JMS 服务器和一个队列。所以让我们构建一个应用程序来使用它:

  1. 首先,我们创建一个消息驱动 BeanMDB)作为对我们队列中丢弃的任何消息的监听器。这是消费者:
@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationLookup", 
    propertyValue = "jms/JmsQueue"),
    @ActivationConfigProperty(propertyName = "destinationType", 
    propertyValue = "javax.jms.Queue")
})
public class QueueListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        TextMessage textMessage = (TextMessage) message;
        try {
            System.out.print("Got new message on queue: ");
            System.out.println(textMessage.getText());
            System.out.println();
        } catch (JMSException e) {
            System.err.println(e.getMessage());
        }
    }
}
  1. 现在我们定义生产者类:
@Stateless
public class QueueSender {

    @Resource(mappedName = "jms/JmsFactory")
    private ConnectionFactory jmsFactory;

    @Resource(mappedName = "jms/JmsQueue")
    private Queue jmsQueue;

    public void send() throws JMSException {
        MessageProducer producer;
        TextMessage message;

        try (Connection connection = jmsFactory.createConnection(); 
             Session session = connection.createSession(false, 
             Session.AUTO_ACKNOWLEDGE)) {

            producer = session.createProducer(jmsQueue);
            message = session.createTextMessage();

            String msg = "Now it is " + new Date();
            message.setText(msg);
            System.out.println("Message sent to queue: " + msg);
            producer.send(message);

            producer.close();
        }
    }
}
  1. 以及一个用于访问生产者的 servlet:
@WebServlet(name = "QueueSenderServlet", urlPatterns = {"/QueueSenderServlet"})
public class QueueSenderServlet extends HttpServlet {

    @Inject
    private QueueSender sender;

    @Override
    protected void doGet(HttpServletRequest request, 
    HttpServletResponse response)
            throws ServletException, IOException {
        try(PrintWriter writer = response.getWriter()){
            sender.send();
            writer.write("Message sent to queue. 
            Check the log for details.");
        } catch (JMSException ex) {
            System.err.println(ex.getMessage());
        }
    }
}
  1. 最后,我们创建一个页面来调用我们的 servlet:
<html>
    <head>
        <title>JMS recipe</title>
        <meta http-equiv="Content-Type" content="text/html; 
         charset=UTF-8">
    </head>
    <body>
        <p>
            <a href="QueueSenderServlet">Send Message to Queue</a>
        </p>
    </body>
</html>

现在只需部署并运行它。每次你调用QueueSenderServlet时,你应该在你的服务器日志中看到类似以下内容:

Info: Message sent to queue: Now it is Tue Dec 19 06:52:17 BRST 2017
Info: Got new message on queue: Now it is Tue Dec 19 06:52:17 BRST 2017

它是如何工作的...

多亏了 Java EE 8 服务器中实现的标准化,我们的 MDB 100%由容器管理。这就是为什么我们只需引用队列而无需回头查看:

@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationLookup", 
    propertyValue = "jms/JmsQueue"),
    @ActivationConfigProperty(propertyName = "destinationType", 
    propertyValue = "javax.jms.Queue")
})

我们可以自己手动构建一个消费者,但这将需要三倍的代码行数,并且是同步的。

我们从我们的工厂提供的会话中获取容器生产者,并为我们的队列创建:

        try (Connection connection = jmsFactory.createConnection(); 
             Session session = connection.createSession(false, 
             Session.AUTO_ACKNOWLEDGE)) {

            producer = session.createProducer(jmsQueue);
            ...
        }

然后我们只需要创建并发送消息:

            message = session.createTextMessage();

            String msg = "Now it is " + new Date();
            message.setText(msg);
            System.out.println("Message sent to queue: " + msg);
            producer.send(message);

参见

理解 servlet 的生命周期

如果你习惯于使用 Java EE 创建 Web 应用程序,你可能已经意识到:大多数时候都是关于处理请求和响应,最流行的方式是通过使用 Servlet API。

这个配方将向你展示服务器如何处理其生命周期,以及你在代码中应该和不应该做什么。

准备工作

首先,将适当的依赖项添加到你的项目中:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

只需编写这个简单的 servlet:

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

    @Override
    protected void doGet(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
        try(PrintWriter writer = resp.getWriter()){
            writer.write("doGet");
            System.out.println("doGet");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
        try(PrintWriter writer = resp.getWriter()){
            writer.write("doPost");
            System.out.println("doPost");
        }
    } 

    @Override
    protected void doDelete(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
        try(PrintWriter writer = resp.getWriter()){
            writer.write("doDelete");
            System.out.println("doDelete");
        }
    }

    @Override
    protected void doPut(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
        try(PrintWriter writer = resp.getWriter()){
            writer.write("doPut");
            System.out.println("doPut");
        }
    } 

    @Override
    public void init() throws ServletException {
        System.out.println("init()");
    }

    @Override
    public void destroy() {
        System.out.println("destroy");
    } 
}

一旦部署到你的 Java EE 服务器上,我建议你使用 SoapUI 或类似工具尝试它。这将允许你使用GETPOSTPUTDELETE发送请求。浏览器只会做GET

如果你这样做,你的系统日志将看起来就像这样:

Info: init(ServletConfig config)
 Info: doGet
 Info: doPost
 Info: doPut
 Info: doDelete

如果你取消部署你的应用程序,它看起来将如下所示:

Info:   destroy

它是如何工作的...

如果你注意观察,你会注意到init日志只有在你的 servlet 第一次被调用后才会显示。那时它真正被加载,这也是这个方法唯一被调用的时候。所以如果你为这个 servlet 有一些一次性代码,那就是你该做的。

谈到doGetdoPostdoPutdoDelete方法,请注意,它们都是根据接收到的请求由服务器自动调用的。这是由于服务器实现的一个名为service的另一个方法才成为可能的。

如果你愿意,可以覆盖service方法,但这是一种坏习惯,应该避免。只有当你确切知道你在做什么时才这样做,否则你可能会给某些请求错误的地址。这一章是关于依赖标准的,那么为什么你不遵守它们呢?

最后,当你的应用程序未部署时,会调用destroy方法。这就像是你的 servlet 的最后一口气。在这里添加一些代码也是一个坏习惯,因为这可能会阻止某些资源被释放,或者遇到一些进程错误。

参考以下内容

事务管理

事务管理是计算机科学中比较棘手的话题之一。一行错误,一个不可预测的情况,你的数据和/或你的用户将承受后果。

因此,如果我们可以依赖服务器为我们做这件事,那就太好了。大多数时候我们都可以,所以让我向你展示如何做到这一点。

准备工作

首先向你的项目中添加适当的依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 让我们构建一个将执行我们所需所有事务的 bean:
@Stateful
@TransactionManagement
public class UserBean {

    private ArrayList<Integer> actions;

    @PostConstruct
    public void init(){
        actions = new ArrayList<>();
        System.out.println("UserBean initialized");
    }

    public void add(Integer action){
        actions.add(action);
        System.out.println(action + " added");
    }

    public void remove(Integer action){
        actions.remove(action);
        System.out.println(action + " removed");
    }

    public List getActions(){
        return actions;
    }

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

    @Remove
    public void logout(){
        System.out.println("User logout. Resources will be 
        released.");
    }

    @AfterBegin
    public void transactionStarted(){
        System.out.println("Transaction started");
    }

    @BeforeCompletion
    public void willBeCommited(){
        System.out.println("Transaction will be commited");
    }

    @AfterCompletion
    public void afterCommit(boolean commited){
        System.out.println("Transaction commited? " + commited);
    }   
}
  1. 以及一个测试类来尝试它:
public class UserTest {

    private EJBContainer ejbContainer;

    @EJB
    private UserBean userBean;

    public UserTest() {
    }

    @Before
    public void setUp() throws NamingException {
        ejbContainer = EJBContainer.createEJBContainer();
        ejbContainer.getContext().bind("inject", this); 
    }

    @After
    public void tearDown() {
        ejbContainer.close();
    }

    @Test
    public void test(){
        userBean.add(1);
        userBean.add(2);
        userBean.add(3);
        userBean.remove(2);
        int size = userBean.getActions().size();
        userBean.logout();
        Assert.assertEquals(2, size); 
    }   
}
  1. 如果你尝试这个测试,你应该看到以下输出:
 UserBean initialized
 Transaction started
 1 added
 Transaction will be commited
 Transaction commited? true
 Transaction started
 2 added
 Transaction will be commited
 Transaction commited? true
 Transaction started
 3 added
 Transaction will be commited
 Transaction commited? true
 Transaction started
 2 removed
 Transaction will be commited
 Transaction commited? true
 Transaction started
 Transaction will be commited
 Transaction commited? true
 Transaction started
 User logout. Resources will be released.
 UserBean will be destroyed
 Transaction will be commited
 Transaction commited? true

它是如何工作的...

我们首先做的事情是标记我们的 bean 以保持状态,并让服务器管理其事务:

@Stateful
@TransactionManagement
public class UserBean {
    ...
}

那么,接下来会发生什么?如果你注意的话,处理添加或删除内容的任何方法都不会进行事务管理。但它们仍然被管理:

 Transaction started
 1 added
 Transaction will be commited
 Transaction commited? true

因此,你可以在不写一行事务代码的情况下拥有所有事务智能。

即使 bean 会释放其资源,它也会进行事务处理:

 Transaction started
 User logout. Resources will be released.
 UserBean will be destroyed
 Transaction will be commited
 Transaction commited? true

参考以下内容

第七章:在主要 Java EE 服务器上部署和管理应用程序

本章涵盖了以下配方:

  • Apache TomEE 使用

  • GlassFish 使用

  • WildFly 使用

简介

作为 Java EE 开发者,您应该具备的最重要技能之一是了解如何与市场上最常用的 Java EE 应用服务器一起工作。

正如我们在前面的章节中所提到的,Java EE 生态系统中的标准允许您无论使用哪个服务器,都可以重用您已经拥有的大部分知识。

然而,当我们谈论部署和一些管理任务时,事情可能会有所不同(通常都是)。这些差异不在于它们的工作方式,而在于它们执行的方式。

因此,在本章中,我们将介绍 Apache TomEE、GlassFish 和 WildFly 的一些重要和常见任务。

Apache TomEE 使用

如果您已经使用过 Apache Tomcat,您可以考虑自己准备好使用 Apache TomEE。它基于 Tomcat 的核心并实现了 Java EE 规范。

准备工作

首先,您需要将其下载到您的环境中。在撰写本文时,TomEE 没有与 Java EE 8 兼容的版本(实际上,只有 GlassFish 5)。然而,这里涵盖的任务在未来的版本中不应该改变,因为它们与 Java EE 规范无关。

要下载它,只需访问 tomee.apache.org/downloads.html。此配方基于 7.0.2 Plume 版本。

在可能的情况下,我们将专注于使用配置文件执行任务。

如何操作...

参考以下详细任务。

部署 EAR、WAR 和 JAR 文件

对于 EAR 和 WAR 文件,部署文件夹是:

$TOMEE_HOME/webapps

对于 JAR 文件,文件夹是:

$TOMEE_HOME/lib

创建数据源和连接池

为了创建数据源和连接池以帮助您在项目中使用数据库,请编辑 $TOMEE_HOME/conf/tomee.xml 文件。

<tomee> 节点内部,插入如下子节点:

  <Resource id="MyDataSouceDs" type="javax.sql.DataSource">
      jdbcDriver = org.postgresql.Driver
      jdbcUrl = jdbc:postgresql://[host]:[port]/[database]
      jtaManaged = true
      maxActive = 20
      maxIdle = 20
      minIdle = 0
      userName = user
      password = password
  </Resource>

示例针对的是 PostgreSQL,因此您需要对其他数据库进行一些更改。当然,您还需要根据您的需求更改其他参数。

日志设置和轮转

要配置 Apache TomEE 的日志记录,请编辑 $TOMEE_HOME/conf/logging.properties 文件。该文件使用类似以下的处理程序:

1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.

因此,您可以根据需要定义日志级别、目录和前缀。

如果您需要配置日志轮转,只需将这些行添加到您的处理程序中:

1catalina.org.apache.juli.AsyncFileHandler.limit = 1024
1catalina.org.apache.juli.AsyncFileHandler.count = 10

在这个例子中,我们定义此文件在每 1024 千字节(1 MB)时旋转,并保留我们磁盘上的最后 10 个文件。

启动和停止

要启动 Apache TomEE,只需执行此脚本:

$TOMEE_HOME/bin/startup.sh

要停止它,执行以下脚本:

$TOMEE_HOME/bin/shutdown.sh

会话集群

如果您想使用 Apache TomEE 节点构建集群,您需要编辑 $TOMEE_HOME/conf/server.xml 文件。然后,找到以下行:

<Engine name="Catalina" defaultHost="localhost">

插入一个子节点,如下所示:

        <Cluster 
         className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
                       channelSendOptions="8">

                <Manager 
                className="org.apache.catalina.ha.session
                         .DeltaManager"
                         expireSessionsOnShutdown="false"
                         notifyListenersOnReplication="true"/>

                <Channel 
                 className="org.apache.catalina.tribes.group
                 .GroupChannel">
                  <Membership                     
                  className="org.apache.catalina
                 .tribes.membership .McastService"
                              address="228.0.0.4"
                              port="45564"
                              frequency="500"
                              dropTime="3000"/>
                  <Receiver className="org.apache.catalina.tribes
                  .transport.nio.NioReceiver"
                            address="auto"
                            port="4000"
                            autoBind="100"
                            selectorTimeout="5000"
                            maxThreads="6"/>

                  <Sender className="org.apache.catalina.tribes
                  .transport.ReplicationTransmitter">
                    <Transport className="org.apache.catalina.tribes
                   .transport.nio.PooledParallelSender"/>
                  </Sender>
                  <Interceptor className="org.apache.catalina.tribes
                 .group.interceptors.TcpFailureDetector"/>
                  <Interceptor className="org.apache.catalina.tribes
                  .group.interceptors.MessageDispatchInterceptor"/>
                </Channel>

                <Valve className="org.apache.catalina
                .ha.tcp.ReplicationValve"
                       filter=""/>
                <Valve className="org.apache.catalina
                .ha.session.JvmRouteBinderValve"/>

                <Deployer className="org.apache.catalina
                .ha.deploy.FarmWarDeployer"
                          tempDir="/tmp/war-temp/"
                          deployDir="/tmp/war-deploy/"
                          watchDir="/tmp/war-listen/"
                          watchEnabled="false"/>

                <ClusterListener className="org.apache.catalina
                .ha.session.ClusterSessionListener"/>
        </Cluster>

此块将设置服务器以在动态发现集群中运行。这意味着在相同网络中使用此配置运行的每个服务器都将成为集群的新成员,并共享活动会话。

所有这些参数都极其重要,所以我真心建议您保留所有这些参数,除非您绝对确定自己在做什么。

还有更多...

目前设置 Java EE 集群的最佳方式是使用容器(特别是 Docker 容器)。因此,我建议您查看第十一章,迈向云端 – Java EE、容器和云计算。如果您将本章内容与该章节内容相结合,将为您的应用程序提供一个强大的环境。

要允许您的应用程序与集群中的所有节点共享会话,您需要编辑web.xml文件,找到web-app节点,并插入以下内容:

<distributable/>

如果没有它,您的会话集群将无法工作。您还需要保持会话中持有的所有对象为可序列化。

参考信息

GlassFish 使用

GlassFish 的伟大之处在于它是参考实现RI)。因此,每当有新的 Java EE 版本发布时,作为开发者,您已经拥有了相应的 GlassFish 版本来尝试它。

准备工作

首先,您需要将其下载到您的环境中。在撰写本文时,GlassFish 5 是唯一已发布的 Java EE 8 服务器。

要下载它,只需访问javaee.github.io/glassfish/download。本食谱基于版本 5(Java EE 8 兼容)。

在可能的情况下,我们将专注于使用配置文件执行任务。

如何操作...

参考以下详细任务。

部署 EAR、WAR 和 JAR 文件

对于 EAR 和 WAR 文件,部署文件夹是:

$GLASSFISH_HOME/glassfish/domains/[domain_name]/autodeploy

通常domain_namedomain1,除非您在安装过程中更改了它。

对于 JAR 文件,文件夹是:

$GLASSFISH_HOME/glassfish/lib

创建数据源和连接池

要创建数据源和连接池以帮助您在项目中使用数据库,请编辑$GLASSFISH_HOME/glassfish/domains/[domain_name]/config/domain.xml文件。在<resources>节点内,插入如下子节点:

<jdbc-connection-pool 
  pool-resize-quantity="4" 
  max-pool-size="64" 
  max-wait-time-in-millis="120000" 
  driver-classname="com.mysql.jdbc.Driver" 
  datasource-classname="com.mysql.jdbc.jdbc2.optional
  .MysqlDataSource" 
  steady-pool-size="16" 
  name="MysqlPool" 
  idle-timeout-in-seconds="600" 
  res-type="javax.sql.DataSource">
      <property name="databaseName" value="database"></property>
      <property name="serverName" value="[host]"></property>
      <property name="user" value="user"></property>
      <property name="password" value="password"></property>
      <property name="portNumber" value="3306"></property>
</jdbc-connection-pool>
<jdbc-resource pool-name="MySqlDs" jndi-name="jdbc/MySqlDs">
</jdbc-resource>

然后,查找以下节点:

<server config-ref="server-config" name="server">

向其中添加以下子节点:

<resource-ref ref="jdbc/MySqlDs"></resource-ref>

该示例针对 MySQL,因此您需要对其他数据库进行一些更改。当然,您还需要根据需要更改其他参数。

日志设置和轮换

要为 GlassFish 配置日志记录,请编辑$GLASSFISH_HOME/glassfish/domains/domain1/config/logging.properties文件:

该文件与以下处理程序一起工作:

handlers=java.util.logging.ConsoleHandler
handlerServices=com.sun.enterprise.server.logging.GFFileHandler
java.util.logging.ConsoleHandler.formatter=com.sun.enterprise.server.logging.UniformLogFormatter
com.sun.enterprise.server.logging.GFFileHandler.formatter=com.sun.enterprise.server.logging.ODLLogFormatter
com.sun.enterprise.server.logging.GFFileHandler.file=${com.sun.aas.instanceRoot}/logs/server.log
com.sun.enterprise.server.logging.GFFileHandler.rotationTimelimitInMinutes=0
com.sun.enterprise.server.logging.GFFileHandler.flushFrequency=1
java.util.logging.FileHandler.limit=50000
com.sun.enterprise.server.logging.GFFileHandler.logtoConsole=false
com.sun.enterprise.server.logging.GFFileHandler.rotationLimitInBytes=2000000
com.sun.enterprise.server.logging.GFFileHandler.excludeFields=
com.sun.enterprise.server.logging.GFFileHandler.multiLineMode=true
com.sun.enterprise.server.logging.SyslogHandler.useSystemLogging=false
java.util.logging.FileHandler.count=1
com.sun.enterprise.server.logging.GFFileHandler.retainErrorsStasticsForHours=0
log4j.logger.org.hibernate.validator.util.Version=warn
com.sun.enterprise.server.logging.GFFileHandler.maxHistoryFiles=0
com.sun.enterprise.server.logging.GFFileHandler.rotationOnDateChange=false
java.util.logging.FileHandler.pattern=%h/java%u.log
java.util.logging.FileHandler.formatter=java.util.logging.XMLFormatter

因此,您可以根据需要定义日志级别、目录、格式等。

如果你需要配置日志轮换,你必须关注这些行:

com.sun.enterprise.server.logging.GFFileHandler
.rotationTimelimitInMinutes=0
com.sun.enterprise.server.logging.GFFileHandler
.rotationLimitInBytes=2000000
com.sun.enterprise.server.logging.GFFileHandler
.maxHistoryFiles=0
com.sun.enterprise.server.logging.GFFileHandler
.rotationOnDateChange=false

在这个例子中,我们定义这个文件在每 2000 千字节(2 MB)时旋转,并且不会在日期更改时旋转。历史文件没有限制。

启动和停止

要启动 GlassFish,只需执行以下脚本:

$GLASSFISH_HOME/bin/asadmin start-domain --verbose

要停止它,执行以下脚本:

$GLASSFISH_HOME/bin/asadmin stop-domain

会话集群

使用 GlassFish 构建集群有点棘手,需要同时使用命令行和管理面板,但这是完全可以做到的!让我们来看看。

首先,你需要两个或更多实例(称为节点)启动并运行。你可以以任何你喜欢的方式做到这一点——每个在不同的机器上运行,使用虚拟机或容器(我最喜欢的选项)。无论你选择哪种方式,启动集群的方式都是相同的:

  1. 因此,获取你的第一个节点并打开其管理面板:

https://[hostname]:4848

  1. 点击左侧菜单中的“集群”选项,然后点击“新建”按钮。将集群命名为myCluster并点击“确定”按钮。

  2. 从列表中选择你的集群。在打开的页面中,选择选项卡中的“实例”选项,然后点击“新建”。将实例命名为node1并点击“确定”。

  3. 现在,转到“常规”选项卡,然后点击“启动集群”按钮。哇!你的集群已经启动并运行,并且已经有了第一个节点。

  4. 现在,转到已经安装了 GlassFish 的第二台机器(虚拟机、容器、其他服务器或任何服务器)并运行此命令:

$GLASSFISH_HOME/bin/asadmin --host [hostname_node1] --port 4848 create-local-instance --cluster myCluster node2

这将把第二台机器设置为集群的成员。如果你在第一台机器上刷新集群页面,你会看到新的成员(node2)。

  1. 你会注意到node2已经停止了。点击它,然后在新的页面中点击 Node 链接(它通常会显示node2的主机名)。

  2. 在打开的页面中,将“类型”值更改为SSH。在SSH部分将显示一些新的字段。

  3. 将 SSH 用户认证改为SSH 密码,并在 SSH 用户密码字段中填写正确的密码。

  4. 点击“保存”按钮。如果你遇到一些 SSH 错误(通常是连接被拒绝),将“强制”选项设置为启用,然后再次点击“保存”按钮。

  5. 返回到托管node2的机器上的命令行并运行此命令:

$GLASSFISH_HOME/glassfish/lib/nadmin start-local-instance --node [node2_hostname] --sync normal node2

如果一切顺利,你的node2应该已经启动并运行,你现在应该有一个真正的集群。你可以根据需要重复这些步骤来添加新的节点到你的集群。

还有更多...

对于使用 GlassFish 的这种集群,一个常见的问题是你没有在节点上运行 SSH 服务;由于许多操作系统有大量的选项,我们在这里不会涵盖每一个。

目前设置 Java EE 集群的最佳方式是使用容器(特别是 Docker 容器)。因此,我建议您查看第十一章 Rising to the Cloud – Java EE, Containers, and Cloud Computing,这样,如果您将这部分内容与当前内容结合,将为您的应用程序提供一个强大的环境。

要允许您的应用程序与集群中的所有节点共享会话,您需要编辑 web.xml 文件,找到 web-app 节点,并插入以下内容:

<distributable/>

没有它,您的会话集群将无法工作。您还需要确保所有在会话中持有的对象都是可序列化的。

最后,还有商业版本的 GlassFish,即 Payara Server。如果您正在寻找支持和其他商业优惠,您应该看看它。

参见

WildFly 使用

WildFly 是另一个优秀的 Java EE 实现。它曾被称为 JBoss AS,但几年前更改了名称(尽管我们仍然有 JBoss EAP 作为 企业生产就绪 版本)。由于其管理和使用方式与 Apache TomEE 和 GlassFish 略有不同,因此仔细研究它是值得的。

准备工作

首先,您需要将其下载到您的环境中。在撰写本文时,WildFly 没有与 Java EE 8 兼容的版本(实际上只有 GlassFish 5)。然而,这里涵盖的任务在未来版本中不应发生变化,因为它们与 Java EE 规范无关。要下载它,只需访问 wildfly.org/downloads/

此示例基于版本 11.0.0.Final(Java EE7 全功能和 Web 发行版)。

在可能的情况下,我们将专注于使用配置文件来完成这些任务。

如何操作...

参考以下详细任务。

部署 EAR、WAR 和 JAR 文件

对于 EAR 和 WAR 文件,部署文件夹是:

$WILDFLY_HOME/standalone/deployments

对于 JAR 文件(例如 JDBC 连接),WildFly 创建了一个灵活的文件夹结构。因此,最佳的分发方式是使用其 UI,正如我们将在连接池主题(下一个主题)中展示的那样。

创建数据源和连接池

按以下步骤创建您的数据源和连接池:

  1. 要创建数据源和连接池以帮助您在项目中使用数据库,启动 WildFly 并访问以下 URL:

http://localhost:9990/

  1. 点击“部署”然后点击“添加”按钮。在打开的页面中,选择“上传新的部署”并点击“下一步”按钮。在打开的页面中,选择适当的 JDBC 连接器(我们将使用 MySQL 进行此示例)并点击“下一步”。

  2. 验证打开页面中的信息并点击“完成”。

  3. 现在您的 JDBC 连接器已在服务器中可用,您可以继续创建数据源。转到管理面板中的“首页”并点击“配置”选项。

  4. 在打开的页面中,按照以下路径:

子系统 | 数据源 | 非 XA | 数据源 | 添加

  1. 在打开的窗口中选择 MySQL 数据源,然后点击下一步。然后填写字段如下:
  • 名称: MySqlDS

  • JNDI 名称: java:/MySqlDS

  1. 点击下一步。在下一页,点击检测到的驱动程序,选择适当的 MySQL 连接器(你刚刚上传的那个),然后点击下一步。

  2. 最后一步是填写连接设置字段,如下所示:

  • 连接 URL: jdbc:mysql://localhost:3306/sys

  • 用户名: root

  • 密码: mysql

  1. 点击下一步按钮,查看信息,然后点击完成。你新创建的连接将出现在数据源列表中。你可以点击下拉列表并选择测试来检查一切是否正常工作。

日志设置和轮换

要配置 WildFly 的日志,编辑 $WILDFLY_HOME/standalone/configuration/standalone.xml 文件。

要自定义日志属性,找到 <profile> 节点,然后在其中找到 <subsystem xmlns='urn:jboss:domain:logging:3.0'>

它基于这样的处理程序:

<console-handler name="CONSOLE">
                <level name="INFO"/>
                <formatter>
                    <named-formatter name="COLOR-PATTERN"/>
                </formatter>
            </console-handler>
            <periodic-rotating-file-handler name="FILE" 
             autoflush="true">
                <formatter>
                    <named-formatter name="PATTERN"/>
                </formatter>
                <file relative-to="jboss.server.log.dir" 
                 path="server.log"/>
                <suffix value=".yyyy-MM-dd"/>
                <append value="true"/>
            </periodic-rotating-file-handler>
            <logger category="com.arjuna">
                <level name="WARN"/>
            </logger>
            <logger category="org.jboss.as.config">
                <level name="DEBUG"/>
            </logger>
            <logger category="sun.rmi">
                <level name="WARN"/>
            </logger>
            <root-logger>
                <level name="INFO"/>
                <handlers>
                    <handler name="CONSOLE"/>
                    <handler name="FILE"/>
                </handlers>
            </root-logger>
            <formatter name="PATTERN">
                <pattern-formatter pattern=
                "%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
            </formatter>
            <formatter name="COLOR-PATTERN">
                <pattern-formatter pattern="%K{level}%d
                {HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
            </formatter>

默认情况下,它将每天轮换:

            <periodic-rotating-file-handler name="FILE" 
             autoflush="true">
                <formatter>
                    <named-formatter name="PATTERN"/>
                </formatter>
                <file relative-to="jboss.server.log.dir" 
                 path="server.log"/>
                <suffix value=".yyyy-MM-dd"/>
                <append value="true"/>
            </periodic-rotating-file-handler>

如果你想根据大小进行轮换,你应该移除前面的处理器,然后插入这个:

            <size-rotating-file-handler name="FILE" autoflush="true">
                <formatter>
                    <pattern-formatter pattern="%d{yyyy-MM-dd 
                     HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
                </formatter>
                <file relative-to="jboss.server.log.dir" 
                path="server.log"/>
                <rotate-size value="2m"/>
                <max-backup-index value="5"/>
                <append value="true"/>
            </size-rotating-file-handler>

在这种情况下,当文件达到 2 MB 时,日志将进行轮换,并保留五个文件的备份历史。

启动和停止

要启动 GlassFish,只需执行此脚本:

$WILDFLY_HOME/bin/standalone.sh

要停止它,执行此脚本:

$WILDFLY_HOME/bin/jboss-cli.sh --connect command=:shutdown

会话集群

如果你转到 $WILDFLY_HOME/standalone/configuration 文件夹,你会看到这些文件:

  • standalone.xml

  • standalone-ha.xml

  • standalone-full.xml

  • standalone-full-ha.xml

standalone.xml 是默认配置,包含所有默认配置。要构建集群,我们需要使用 standalone-ha.xml 文件(ha 来自 高可用性),因此将其重命名为以 standalone.xml 结尾。

然后,启动服务器。你不应该做以下事情:

$WILDFLY_HOME/bin/standalone.sh

相反,你应该这样做:

$WILDFLY_HOME/bin/standalone.sh -b 0.0.0.0 -bmanagement 0.0.0.0

你现在应该在你想加入集群的任何其他节点(机器、虚拟机、容器等)上做同样的事情。当然,它们需要处于同一个网络中。

更多内容...

目前设置 Java EE 集群的最好方法是使用容器(特别是 Docker 容器)。因此,我建议你查看第十一章 Rising to the Cloud – Java EE, Containers, and Cloud Computing,了解如何将内容与本章内容结合,这将为你提供一个强大的应用程序环境。

要允许你的应用程序与集群中的所有节点共享其会话,你需要编辑 web.xml 文件,找到 web-app 节点,并插入以下内容:

<distributable/>

没有它,你的会话集群将无法工作。

参见

  • 想了解更多关于 WildFly 的信息,请访问 wildfly.org/

第八章:使用微服务构建轻量级解决方案

本章涵盖了以下食谱:

  • 从单体应用构建微服务

  • 构建解耦的服务

  • 为微服务构建自动化流水线

简介

微服务确实是当今最热门的 buzzword 之一。很容易理解为什么:在一个不断增长的软件行业中,服务、数据和用户的数量疯狂增长,我们确实需要一种方法来构建和交付更快、解耦和可扩展的解决方案。

为什么微服务很好?为什么要使用它们?

实际上,随着需求的增长,单独处理每个模块的需求也在增加。例如,在你的客户应用程序中,用户信息可能需要以不同于地址信息的方式扩展。

在单体范式下,你需要原子性地处理它:你为整个应用程序构建一个集群,或者你提升(或降低)整个主机的规模。这种方法的问题是你不能将精力和资源集中在特定的功能、模块或功能上:你总是被当时的需求所引导。

在微服务方法中,你是分开处理的。然后你不仅可以扩展(提升或降低)应用程序中的一个单一单元,而且还可以为每个服务(你应该这样做)分离数据、分离技术(最适合的工具完成最适合的工作),等等。

除了扩展技术,微服务旨在扩展人员。随着应用程序、架构和数据库的增大,团队也会变大。如果你像单体应用程序一样构建团队,你可能会得到类似的结果。

因此,随着应用程序被分割成几个(或很多)模块,你也可以定义跨职能团队来负责每个模块。这意味着每个团队都可以拥有自己的程序员、设计师、数据库管理员、系统管理员、网络专家、经理等等。每个团队对其处理的模块都有责任。

它为思考、交付软件以及维护和演进软件的过程带来了敏捷性。

在本章中,有一些食谱可以帮助你开始使用微服务或更深入地进行你的现有项目。

从单体应用构建微服务

我已经听到过很多次的一个常见问题是,“我该如何将单体应用拆分成微服务?”,或者,“我该如何从单体方法迁移到微服务?”

好吧,这正是这个食谱的主题。

准备工作

对于单体和微服务项目,我们将使用相同的依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何操作...

让我们从构建一个可以拆分成微服务的单体应用开始。

构建单体应用

首先,我们需要代表应用程序保存的数据的实体。

这里是User实体:

@Entity
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String name;

    @Column
    private String email;

    public User(){   
    }

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    } 
}

这里是UserAddress实体:

@Entity
public class UserAddress implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    @ManyToOne
    private User user;

    @Column
    private String street;

    @Column
    private String number;

    @Column
    private String city;

    @Column
    private String zip;

    public UserAddress(){

    }

    public UserAddress(User user, String street, String number, 
                       String city, String zip) {
        this.user = user;
        this.street = street;
        this.number = number;
        this.city = city;
        this.zip = zip;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getZip() {
        return zip;
    }

    public void setZip(String zip) {
        this.zip = zip;
    }
}

现在我们定义一个 bean 来处理每个实体的交易。

这里是UserBean类:

@Stateless
public class UserBean {

    @PersistenceContext
    private EntityManager em;

    public void add(User user) {
        em.persist(user);
    }

    public void remove(User user) {
        em.remove(user);
    }

    public void update(User user) {
        em.merge(user);
    }

    public User findById(Long id) {
        return em.find(User.class, id);
    }

    public List<User> get() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<User> cq = cb.createQuery(User.class);
        Root<User> pet = cq.from(User.class);
        cq.select(pet);
        TypedQuery<User> q = em.createQuery(cq);
        return q.getResultList();
    }

}

这里是UserAddressBean类:

@Stateless
public class UserAddressBean {

    @PersistenceContext
    private EntityManager em;

    public void add(UserAddress address){
        em.persist(address);
    }

    public void remove(UserAddress address){
        em.remove(address);
    }

    public void update(UserAddress address){
        em.merge(address);
    }

    public UserAddress findById(Long id){
        return em.find(UserAddress.class, id);
    }

    public List<UserAddress> get() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<UserAddress> cq = cb.createQuery(UserAddress.class);
        Root<UserAddress> pet = cq.from(UserAddress.class);
        cq.select(pet);
        TypedQuery<UserAddress> q = em.createQuery(cq);
        return q.getResultList();
    } 
}

最后,我们构建两个服务以在客户端和 bean 之间进行通信。

这里是UserService类:

@Path("userService")
public class UserService {

    @EJB
    private UserBean userBean;

    @GET
    @Path("findById/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response findById(@PathParam("id") Long id){
        return Response.ok(userBean.findById(id)).build();
    }

    @GET
    @Path("get")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response get(){
        return Response.ok(userBean.get()).build();
    } 

    @POST
    @Path("add")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON) 
    public Response add(User user){
        userBean.add(user);
        return Response.accepted().build();
    } 

    @DELETE
    @Path("remove/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON) 
    public Response remove(@PathParam("id") Long id){
        userBean.remove(userBean.findById(id));
        return Response.accepted().build();
    }
}

这里是UserAddressService类:

@Path("userAddressService")
public class UserAddressService {

    @EJB
    private UserAddressBean userAddressBean;

    @GET
    @Path("findById/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response findById(@PathParam("id") Long id){
        return Response.ok(userAddressBean.findById(id)).build();
    }

    @GET
    @Path("get")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response get(){
        return Response.ok(userAddressBean.get()).build();
    } 

    @POST
    @Path("add")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON) 
    public Response add(UserAddress address){
        userAddressBean.add(address);
        return Response.accepted().build();
    } 

    @DELETE
    @Path("remove/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON) 
    public Response remove(@PathParam("id") Long id){
        userAddressBean.remove(userAddressBean.findById(id));
        return Response.accepted().build();
    }
}

现在让我们将其分解!

从单体架构构建微服务

我们的单体架构处理UserUserAddress。因此,我们将它分解为三个微服务:

  • 用户微服务

  • 用户地址微服务

  • 网关微服务

网关服务是应用程序客户端和服务之间的 API。使用它可以使您简化这种通信,同时也给您提供了自由,可以随意处理您的服务,而不会破坏 API 合约(或者至少最小化它)。

用户微服务

User实体、UserBeanUserService将保持与单体架构中完全相同。但现在它们将作为一个独立的部署单元提供。

用户地址微服务

UserAddress类将从单体版本中仅经历一个更改,但将保留其原始 API(这对于客户端来说是非常好的)。

这里是UserAddress实体:

@Entity
public class UserAddress implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private Long idUser;

    @Column
    private String street;

    @Column
    private String number;

    @Column
    private String city;

    @Column
    private String zip;

    public UserAddress(){

    }

    public UserAddress(Long user, String street, String number, 
                       String city, String zip) {
        this.idUser = user;
        this.street = street;
        this.number = number;
        this.city = city;
        this.zip = zip;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getIdUser() {
        return idUser;
    }

    public void setIdUser(Long user) {
        this.idUser = user;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getZip() {
        return zip;
    }

    public void setZip(String zip) {
        this.zip = zip;
    }
}

注意,User不再是UserAddress实体中的一个属性/字段,而只是一个数字(idUser)。我们将在下一节中更详细地介绍这一点。

网关微服务

首先,我们创建一个帮助我们处理响应的类:

public class GatewayResponse {

    private String response;
    private String from;

    public String getResponse() {
        return response;
    }

    public void setResponse(String response) {
        this.response = response;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }
}

然后,我们创建我们的网关服务:

@Consumes(MediaType.APPLICATION_JSON)
@Path("gatewayResource")
@RequestScoped
public class GatewayResource {

    private final String hostURI = "http://localhost:8080/";
    private Client client;
    private WebTarget targetUser;
    private WebTarget targetAddress;

    @PostConstruct
    public void init() {
        client = ClientBuilder.newClient();
        targetUser = client.target(hostURI + 
        "ch08-micro_x_mono-micro-user/");
        targetAddress = client.target(hostURI +
        "ch08-micro_x_mono-micro-address/");
    }

    @PreDestroy
    public void destroy(){
        client.close();
    }

    @GET
    @Path("getUsers")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUsers() {
        WebTarget service = 
        targetUser.path("webresources/userService/get");

        Response response;
        try {
            response = service.request().get();
        } catch (ProcessingException e) {
            return Response.status(408).build();
        }

        GatewayResponse gatewayResponse = new GatewayResponse();
        gatewayResponse.setResponse(response.readEntity(String.class));
        gatewayResponse.setFrom(targetUser.getUri().toString());

        return Response.ok(gatewayResponse).build();
    }

    @POST
    @Path("addAddress")
    @Produces(MediaType.APPLICATION_JSON) 
    public Response addAddress(UserAddress address) {
        WebTarget service = 
        targetAddress.path("webresources/userAddressService/add");

        Response response;
        try {
            response = service.request().post(Entity.json(address));
        } catch (ProcessingException e) {
            return Response.status(408).build();
        }

        return Response.fromResponse(response).build();
    }

}

当我们在网关中接收到UserAddress实体时,我们也要在网关项目中有一个它的版本。为了简洁,我们将省略代码,因为它与UserAddress项目中的代码相同。

它是如何工作的...

让我们了解这里的工作原理。

单体架构

单体应用程序的结构非常简单:只是一个包含两个服务、使用两个 bean 来管理两个实体的项目。如果您想了解有关 JAX-RS、CDI 和/或 JPA 的细节,请查看本书前面的相关食谱。

微服务

因此,我们将单体架构拆分为三个项目(微服务):用户服务、用户地址服务和网关服务。

在从单体版本迁移后,用户服务类保持不变。因此,没有太多可评论的。

UserAddress类必须更改才能成为微服务。第一个更改是在实体上进行的。

这里是单体版本的UserAddress

@Entity
public class UserAddress implements Serializable {

    ...

    @Column
    @ManyToOne
    private User user;

    ...

    public UserAddress(User user, String street, String number, 
                       String city, String zip) {
        this.user = user;
        this.street = street;
        this.number = number;
        this.city = city;
        this.zip = zip;
    }

    ...

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    ...

}

这里是微服务版本的代码:

@Entity
public class UserAddress implements Serializable {

    ...

    @Column
    private Long idUser;

    ...

    public UserAddress(Long user, String street, String number, 
                       String city, String zip) {
        this.idUser = user;
        this.street = street;
        this.number = number;
        this.city = city;
        this.zip = zip;
    }

    public Long getIdUser() {
        return idUser;
    }

    public void setIdUser(Long user) {
        this.idUser = user;
    }

    ...

}

注意,在单体版本中,userUser实体的一个实例:

private User user;

在微服务版本中,它变成了一个数字:

private Long idUser;

这主要是由于两个主要原因:

  1. 在单体架构中,我们有两个表位于同一个数据库中(UserUserAddress),并且它们都有物理和逻辑关系(外键)。因此,保持这两个对象之间的关系也是有意义的。

  2. 微服务应该有自己的数据库,完全独立于其他服务。因此,我们选择只保留用户 ID,因为当客户端需要加载地址时,这已经足够了。

这个更改也导致了构造函数的变化。

这里是单体版本:

public UserAddress(User user, String street, String number, 
                   String city, String zip)

这里是微服务版本:

public UserAddress(Long user, String street, String number, 
                   String city, String zip)

这可能导致与客户端关于构造函数签名的更改的合同变更。但多亏了它的构建方式,这并不是必要的。

这里是单体版本:

public Response add(UserAddress address)

这里是微服务版本:

public Response add(UserAddress address)

即使方法被更改,也可以很容易地通过@Path注解来解决,或者如果我们真的需要更改客户端,那也仅仅是方法名而不是参数(这曾经更痛苦)。

最后,我们有网关服务,这是我们实现的 API 网关设计模式。基本上,它是访问其他服务的唯一单一点。

好处在于你的客户端不需要关心其他服务是否更改了 URL、签名,甚至它们是否可用。网关会处理这些。

坏处在于它也只有一个故障点。或者换句话说,没有网关,所有服务都不可达。但你可以通过集群等方式来处理它。

还有更多...

虽然 Java EE 非常适合微服务,但还有其他基于相同基础的选择,在某些场景下可能更轻量。

其中之一是 KumuluzEE (ee.kumuluz.com/)。它基于 Java EE,具有许多微服务必备功能,如服务发现。它赢得了 Duke Choice Awards 奖项,这是一个巨大的成就!

另一个是 Payara Micro (www.payara.fish/payara_micro)。Payara 是拥有 GlassFish 商业实现的 Payara 公司,从 Payara Server 中,他们创建了 Payara Fish。酷的地方在于它只是一个 60 MB 的 JAR 文件,你通过命令行启动它,然后!你的微服务就运行起来了。

最后,这两个项目的酷之处在于它们与 Eclipse MicroProfile 项目(microprofile.io/)保持一致。MicroProfile 目前正在定义 Java EE 生态系统中微服务的路径和标准,所以值得关注。

关于这个菜谱中涵盖的代码的最后一点:在现实世界的解决方案中使用 DTO 来分离数据库表示和服务表示会更好。

参见

这个菜谱的完整源代码可以在以下存储库中找到:

构建解耦服务

也许你至少听说过在软件世界中构建解耦事物的概念:解耦的类、解耦的模块,以及解耦的服务。

但一个软件单元从另一个解耦意味着什么呢?

从实际的角度来看,当对其中一个进行任何更改时,需要你同时更改另一个,这两个事物就是耦合在一起的。例如,如果你有一个返回字符串的方法,并将其更改为返回双精度浮点数,那么调用该方法的全部方法都需要进行更改。

耦合有层次之分。例如,你可以将所有类和方法设计得非常松散耦合,但它们都是用 Java 编写的。如果你将其中一个更改为.NET,并且希望将它们全部(在同一部署包中)保留在一起,你需要将其他所有方法更改为新语言。

关于耦合的另一件事是,一个单元对另一个单元了解多少。当它们对彼此了解很多时,它们是紧密耦合的,反之,如果它们对彼此了解很少或几乎一无所知,它们就是松散耦合的。这种观点主要与两个(或更多)部分的行为相关。

考虑耦合的另一种方式是契约。如果更改契约会破坏客户端,那么它们是紧密耦合的。如果不是,它们是松散耦合的。这就是为什么使用接口来促进松散耦合是最好的方式。因为它们为其实施者创建契约,使用它们在类之间进行通信可以促进松散耦合。

嗯...服务呢?在我们的案例中,是微服务。

当更改一个服务时不需要更改另一个服务时,一个服务与另一个服务是松散耦合的。你可以从行为或契约的角度来考虑这两个服务。

当谈到微服务时,这一点尤为重要,因为你的应用程序中可能有成百上千个微服务,如果更改其中一个需要你更改其他所有服务,你可能会破坏整个应用程序。

这个菜谱将向你展示如何在微服务中避免紧密耦合,从第一行代码开始,这样你就可以避免未来的重构(至少出于这个原因)。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何实现...

  1. 首先,我们创建一个User POJO:
public class User implements Serializable{

    private String name;
    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }   
}
  1. 然后我们创建一个包含两个方法(端点)的类来返回User
@Path("userService")
public class UserService {

    @GET
    @Path("getUserCoupled/{name}/{email}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUserCoupled(
            @PathParam("name") String name, 
            @PathParam("email") String email){
        //GET USER CODE

        return Response.ok().build();
    }

    @GET
    @Path("getUserDecoupled")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUserDecoupled(@HeaderParam("User") 
    User user){
        //GET USER CODE

        return Response.ok().build();
    }
}
  1. 最后,我们创建另一个服务(另一个项目)来消费UserService
@Path("doSomethingService")
public class DoSomethingService {

    private final String hostURI = "http://localhost:8080/";
    private Client client;
    private WebTarget target;

    @PostConstruct
    public void init() {
        client = ClientBuilder.newClient();
        target = client.target(hostURI + "ch08-decoupled-user/");
    } 

    @Path("doSomethingCoupled")
    @Produces(MediaType.APPLICATION_JSON)
    public Response doSomethingCoupled(String name, String email){
        WebTarget service = 
        target.path("webresources/userService/getUserCoupled");
        service.queryParam("name", name);
        service.queryParam("email", email);

        Response response;
        try {
            response = service.request().get();
        } catch (ProcessingException e) {
            return Response.status(408).build();
        }

        return 
        Response.ok(response.readEntity(String.class)).build();
    }

    @Path("doSomethingDecoupled")
    @Produces(MediaType.APPLICATION_JSON)
    public Response doSomethingDecoupled(User user){
        WebTarget service = 
        target.path("webresources/userService/getUserDecoupled");

        Response response;
        try {
            response = service.request().header("User", 
            Entity.json(user)).get();
        } catch (ProcessingException e) {
            return Response.status(408).build();
        }

        return 
        Response.ok(response.readEntity(String.class)).build();
    } 
}

它是如何工作的...

正如你可能已经注意到的,我们在代码中创建了两种情况:一种是明显耦合的(getUserCoupled)另一种是解耦的(getUserDecoupled):

    public Response getUserCoupled(
            @PathParam("name") String name, 
            @PathParam("email") String email)

为什么这是一个耦合的方法,因此也是一个耦合的服务?因为它高度依赖于方法签名。想象它是一个搜索服务,"name""email"是过滤器。现在想象一下,在未来的某个时候,你需要添加另一个过滤器。签名中再增加一个参数。

好吧,你可以同时保留两个方法,这样你就不必打破客户端并更改客户端。有多少个?移动端、服务、网页等等。所有这些都需要更改以支持新功能。

现在看看这个:

    public Response getUserDecoupled(@HeaderParam("User") User user)

在这个User搜索方法中,如果你需要向过滤器中添加一个新参数怎么办?好吧,继续添加!合同没有变化,所有客户端都很高兴。

如果你的User POJO 最初只有两个属性,一年后增加到一百个,没问题。你的服务合同保持不变,甚至那些没有使用新字段客户端仍然可以正常工作。太棒了!

耦合/解耦服务的结果可以在调用服务中看到:

    public Response doSomethingCoupled(String name, String email){
        WebTarget service = 
        target.path("webresources/userService/getUserCoupled");
        service.queryParam("name", name);
        service.queryParam("email", email);

        ...
    }

调用服务完全耦合到被调用服务:它必须知道被调用服务属性的名称,并且每次它改变时都需要添加/更新。

现在看看这个:

    public Response doSomethingDecoupled(User user){
        WebTarget service = 
        target.path("webresources/userService/getUserDecoupled");

        Response response;
        try {
            response = service.request().header("User", 
            Entity.json(user)).get();
            ...
    }

在这种情况下,你只需要引用唯一的服务参数("User")并且它永远不会改变,无论User POJO 如何变化。

参见

在以下链接中查看完整源代码:

为微服务构建自动化管道

也许你在想,“为什么在 Java EE 8 书中会有自动化食谱?”或者甚至,“Java EE 8 下是否有任何规范定义了管道自动化?”

第二个问题的答案是没有。至少在现在这个时刻是这样的。第一个问题的答案我将在下面解释。

在许多会议上,我经常被问到这样的问题,“我该如何将我的单体应用迁移到微服务?”这个问题有一些变体,但最终问题都是一样的。

人们出于不同的原因想要这样做:

  • 他们想要跟上潮流

  • 他们想要与看起来像新时尚的东西一起工作

  • 他们想要扩展应用程序

  • 他们希望能够在同一个解决方案下使用不同的技术栈

  • 他们希望看起来很酷

任何这些原因都是可以接受的,如果你想的话,你可以用任何一个理由来证明你的迁移到微服务的合理性。我可能会质疑其中一些人的真实动机,但...

而不是给他们提供建议、技巧、指南或其他技术讲座,我通常问一个简单的问题:“你已经有了一个用于你的单体的自动化流水线吗?”

大多数时候,答案是失望的“不”,然后是一个好奇的,“为什么?”

好吧,答案很简单:如果你不自动化流水线,你,单体,一个单独的包,有时你会遇到问题,那么你为什么认为当你有几十、几百甚至几千个部署文件时,事情会更容易呢?

让我更具体地说:

  • 你是手动构建部署工件吗?使用 IDE 或其他工具?

  • 你是手动部署的吗?

  • 你是否因为任何原因(如错误、缺失工件或其他任何原因)遇到过部署问题?

  • 你是否因为缺乏测试而遇到过问题?

如果你至少对这些问题中的一个问题回答了“是”,并且没有自动化流水线,想象一下这些问题被成倍放大……又是几十、几百或几千。

有些人甚至不写单元测试。想象一下那些隐藏的错误在无数被称为微服务的工件中进入生产。你的微服务项目可能甚至在没有上线之前就会失败。

所以是的,在考虑微服务之前,你需要尽可能地在你的流水线中自动化尽可能多的东西。这是防止问题扩散的唯一方法。

自动化流水线有三个成熟阶段:

  1. 持续集成CI):基本上,这确保了你的新代码将尽快合并到主分支(例如,master分支)。这是基于这样一个事实:你合并的代码越少,你添加的错误就越少。它主要通过在构建时运行单元测试来实现。

  2. 持续交付:这是 CI 的进一步发展,其中你只需点击一下按钮就可以保证你的工件准备好部署。这通常需要一个用于你的二进制文件的工件存储库和一个管理它的工具。在采用持续交付时,你决定何时进行部署,但最佳实践是尽可能快地进行部署,以避免一次性在生产中添加大量新代码。

  3. 持续部署CD):这是自动化的最后一部分,也是最前沿的部分。在 CD 中,从代码提交到部署到生产中,没有人为的交互。唯一可能阻止工件部署的是流水线阶段中的任何错误。全球所有主要的微服务成功案例都在他们的项目中使用了 CD,每天进行数百甚至数千次部署。

这个配方将向你展示你如何将任何 Java EE 项目从零(完全没有自动化)发展到三(CD)。这是一个概念性的配方,但也包含一些代码。

不要反对概念;它们是你作为 Java EE 开发者的职业关键。

“走向微服务”是一件大事,在应用程序和组织中意味着很多。有些人甚至说微服务完全是关于扩展人员,而不是技术。

当然,我们将在技术方面继续前进。

准备就绪

由于微服务涉及很多方面,它们也会带来很多工具。这个食谱并不打算深入到每个工具的设置,而是展示它们在微服务自动化管道中的工作方式。

这里选择的技术工具并不是执行这些角色的唯一选择。它们只是我在这些角色中的最爱。

准备应用程序

为了准备你的应用程序——你的微服务——进行自动化,你需要:

  • Apache Maven:这主要用于构建阶段,它还将帮助你处理与之相关的许多活动。它管理依赖项,运行单元测试,等等。

  • JUnit:这用于编写将在构建阶段执行的单元测试。

  • Git:为了想象中最神圣的事物,请为你的源代码使用一些版本控制。在这里,我将基于 GitHub。

准备环境

为了准备你的管道环境,你需要:

  • Sonatype Nexus:这是一个二进制仓库。换句话说,当你构建你的工件时,它将被存储在 Nexus 中,并准备好部署到你需要/想要的地方。

  • Jenkins:我以前说过 Jenkins 是万能的自动化工具。实际上,我在一个项目中使用它为大约 70 个应用程序构建了自动化管道(持续交付),这些应用程序使用了完全不同的技术(语言、数据库、操作系统等)。你基本上会使用它来进行构建和部署。

如何操作...

你将指导达到三个自动化成熟阶段:持续集成、持续交付和持续部署。

持续集成

在这里,你需要尽快让你的新代码合并到主分支。你可以通过以下方式实现:

  • Git

  • Maven

  • JUnit

因此,你将确保你的代码构建正确,测试计划并成功执行。

Git

我不会深入讲解如何使用 Git 及其命令,因为这不是本书的重点。如果你是 Git 世界的完全新手,可以从查看这张速查表开始:

education.github.com/git-cheat-sheet-education.pdf

Maven

Maven 是我见过的最强大的工具之一,因此它内置了许多功能。如果你是新手,可以查看这个参考:

maven.apache.org/guides/MavenQuickReferenceCard.pdf

在基于 Maven 的项目中,最重要的文件是pom.xmlPOM代表项目对象模型)。例如,当你创建一个新的 Java EE 8 项目时,它应该看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<project   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.eldermoraes</groupId>
    <artifactId>javaee8-project-template</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <name>javaee8-project-template</name>

    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArguments>
                        <endorseddirs>${endorsed.dir}</endorseddirs>
                    </compilerArguments>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

然后你的项目就准备好使用 Maven 构建了,如下所示(在pom.xml所在的同一文件夹中运行):

mvn

JUnit

你将使用 JUnit 来运行你的单元测试。让我们检查一下。

这里是一个要测试的类:

public class JUnitExample {

    @Size (min = 6, max = 10,message = "Name should be between 6 and 10 
           characters")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

这里是一个测试类:

public class JUnitTest {

    private static Validator VALIDATOR;

    @BeforeClass
    public static void setUpClass() {
        VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Test
    public void smallName(){
        JUnitExample junit = new JUnitExample();

        junit.setName("Name");

        Set<ConstraintViolation<JUnitExample>> cv = 
        VALIDATOR.validate(junit);
        assertFalse(cv.isEmpty());
    }

    @Test
    public void validName(){
        JUnitExample junit = new JUnitExample();

        junit.setName("Valid Name");

        Set<ConstraintViolation<JUnitExample>> cv = 
        VALIDATOR.validate(junit);
        assertTrue(cv.isEmpty());
    }

    @Test
    public void invalidName(){
        JUnitExample junit = new JUnitExample();

        junit.setName("Invalid Name");

        Set<ConstraintViolation<JUnitExample>> cv = 
        VALIDATOR.validate(junit);
        assertFalse(cv.isEmpty());
    } 
}

每次你运行此项目的构建过程时,前面的测试将会执行,并确保这些条件仍然有效。

现在,你已经准备好进行持续集成。只需确保尽快将你的新代码和有效代码合并到主分支。现在让我们继续到持续交付。

持续交付

现在你已经成为一个提交者机器,让我们更进一步,让你的应用程序随时可以部署。

首先,你需要确保你刚刚构建的工件可以在适当的存储库中可用。这就是我们使用 Sonatype Nexus 的时候。

我不会在这本书中详细介绍设置细节。一种简单的方法是使用 Docker 容器。你可以在以下链接中了解更多信息,hub.docker.com/r/sonatype/nexus/

一旦你的 Nexus 可用,你需要转到pom.xml文件并添加以下配置:

    <distributionManagement>
        <repository>
            <id>Releases</id>
            <name>Project</name>
            <url>[NEXUS_URL]/nexus/content/repositories/releases/</url>
        </repository>
     </distributionManagement>

现在,不再构建,而是使用以下内容:

mvn

你会这样做:

mvn deploy

因此,一旦你的工件构建完成,Maven 将会将其上传到 Sonatype Nexus。现在它已经适当地存储起来,以供未来的部署使用。

现在你几乎准备好跳起自动化之舞了。让我们把 Jenkins 带到派对上。

如同 Nexus 所提到的,我不会深入介绍设置 Jenkins 的细节。我也建议你使用 Docker 进行设置。有关详细信息,请参阅以下链接:

hub.docker.com/_/jenkins/

如果你完全不知道如何使用 Jenkins,请参考此官方指南:

jenkins.io/user-handbook.pdf

一旦你的 Jenkins 启动并运行,你将创建两个作业:

  1. Your-Project-Build:这个作业将用于从源代码构建你的项目。

  2. Your-Project-Deploy:这个作业将在工件在 Nexus 构建和存储后用于部署。

你将配置第一个作业下载你的项目源代码并使用 Maven 构建它。第二个将从中下载并部署到应用程序服务器。

记住,部署过程在大多数情况下涉及一些步骤:

  1. 停止应用程序服务器。

  2. 删除上一个版本。

  3. 从 Nexus 下载新版本。

  4. 部署新版本。

  5. 启动应用程序服务器。

因此,你可能需要创建一个 shell 脚本,由 Jenkins 执行。记住,我们正在自动化,所以没有手动过程。

下载工件可能有点棘手,所以你可能在你的 shell 脚本中使用类似以下的内容:

wget --user=username --password=password "[NEXUS_URL]/nexus/service/local/artifact/maven/content?g=<group>&a=<artifact>
&v=<version>&r=releases"

如果一切顺利,那么你将有两个按钮:一个用于构建,另一个用于部署。你已经准备好,无需使用任何 IDE 进行部署,也无需触摸应用程序服务器。

现在你确信这两个过程(构建和部署)将每次都以完全相同的方式进行执行。你现在可以计划在更短的时间内执行它们。

好吧,现在我们将进入下一步,也是最好的一步:持续部署。

持续部署

从交付到部署是一个成熟的过程——你需要一个可靠的过程来确保只有可工作的代码进入生产环境。

你已经在每次构建时运行了单元测试。实际上,你没有忘记编写单元测试,对吧?

在每次成功执行后,你的构建工件将被妥善存储,并且你将管理好你应用程序的正确版本。

你已经掌握了你应用程序的部署过程,正确处理了可能出现的任何条件。你的应用程序服务器不会再在没有你知情的情况下宕机,而你只用了两个按钮就实现了这一点!构建和部署。你太棒了!

如果你已经到了这个阶段,你的下一步不应该是个大问题。你只需要自动化这两个作业,这样你就不需要再按按钮了。

在构建作业中,你可以设置它在 Jenkins 发现源代码仓库中的任何更改时执行(如果你不知道如何操作,请查看文档)。

一旦完成,就只剩下最后一个配置:让构建作业在构建步骤中调用另一个作业——部署作业。所以每次构建成功执行时,部署也会立即执行。

干杯!你已经成功了。

还有更多...

当然,你不仅需要执行单元测试或 API 测试。如果你有 UI,你还需要测试你的 UI。

我建议使用 Selenium Webdriver 来完成这个操作。更多信息请参考这里,www.seleniumhq.org/docs/03_webdriver.jsp

在这种情况下,你可能希望将你的应用程序部署到 QA 环境,运行 UI 测试,如果一切正常,然后进入生产环境。所以这只是一个在管道中添加一些新作业的问题,现在你知道如何做了。

参考以下内容

第九章:在企业上下文中使用多线程

本章涵盖了以下配方:

  • 使用返回结果构建异步任务

  • 使用事务处理异步任务

  • 检查异步任务的状况

  • 使用返回结果构建管理线程

  • 使用返回结果调度异步任务

  • 使用注入代理进行异步任务

简介

线程是大多数软件项目中的常见问题,无论涉及哪种语言或其他技术。当谈到企业应用时,事情变得更加重要,有时也更难。

在某些线程中犯的一个错误可能会影响整个系统,甚至整个基础设施。想想看,一些资源永远不会释放,内存消耗永远不会停止增加,等等。

Java EE 环境有一些处理这些以及其他许多挑战的出色功能,本章将向您展示其中的一些。

使用返回结果构建异步任务

如果你从未处理过异步任务,你将面临的一个首要挑战是:如果你不知道执行何时结束,你该如何从异步任务中返回结果?

好吧,这个配方会告诉你如何做。AsyncResponse 赢了!

准备工作

让我们首先添加我们的 Java EE 8 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 首先,我们创建一个 User POJO:
package com.eldermoraes.ch09.async.result;

/**
 *
 * @author eldermoraes
 */
public class User {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ", name="
                     + name + '}';
    }
}
  1. 然后我们创建 UserService 来模拟一个 远程 慢速端点:
@Stateless
@Path("userService")
public class UserService {

    @GET
    public Response userService(){
        try {
            TimeUnit.SECONDS.sleep(5);
            long id = new Date().getTime();
            return Response.ok(new User(id, "User " + id)).build();
        } catch (InterruptedException ex) {
            return 
            Response.status(Response.Status.INTERNAL_SERVER_ERROR)
            .entity(ex).build();
        }
    }
}
  1. 现在我们创建一个异步客户端,它将到达该端点并获取结果:
@Stateless
public class AsyncResultClient {

    private Client client;
    private WebTarget target;

    @PostConstruct
    public void init() {
        client = ClientBuilder.newBuilder()
                .readTimeout(10, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .build();
        target = client.target("http://localhost:8080/
                 ch09-async-result/userService");
    }

    @PreDestroy
    public void destroy(){
        client.close();
    }

    public CompletionStage<Response> getResult(){
        return 
        target.request(MediaType.APPLICATION_JSON).rx().get();
    }

}
  1. 最后,我们创建一个服务(端点),它将使用客户端将结果写入响应:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Inject
    private AsyncResultClient client;

    @GET
    public void asyncService(@Suspended AsyncResponse response)
    {
        try{
            client.getResult().thenApply(this::readResponse)
            .thenAccept(response::resume);
        } catch(Exception e){
            response.resume(Response.status(Response.Status.
            INTERNAL_SERVER_ERROR).entity(e).build());
        }
    }

    private String readResponse(Response response) {
        return response.readEntity(String.class);
    }
}

要运行此示例,只需将其部署到 GlassFish 5,并在您的浏览器中打开此 URL:

http://localhost:8080/ch09-async-result/asyncService

它是如何工作的...

首先,我们的远程端点正在创建 User 并将其转换为响应实体:

return Response.ok(new User(id, "User " + id)).build();

因此,毫不费力,您的 User 现在已经是一个 JSON 对象。

现在让我们看看 AsyncResultClient 中的关键方法:

    public CompletionStage<Response> getResult(){
        return target.request(MediaType.APPLICATION_JSON).rx().get();
    }

rx() 方法是 Java EE 8 中引入的响应式客户端 API 的一部分。我们将在下一章中更详细地讨论响应式编程。它基本上返回 CompletionStageInvoker,这将允许你获取 CompletionStage<Response>(此方法的返回值)。

换句话说,这是一段异步/非阻塞代码,它从远程端点获取结果。

注意,我们使用 @Stateless 注解与此客户端一起,这样我们就可以将其注入到我们的主要端点:

    @Inject
    private AsyncResultClient client;

这是我们的异步方法来写入响应:

    @GET
    public void asyncService(@Suspended AsyncResponse response) {
        client.getResult().thenApply(this::readResponse)
        .thenAccept(response::resume);
    }

注意,这是一个 void 方法。它不返回任何内容,因为它会将结果返回给回调。

@Suspended 注解与 AsyncResponse 结合使用,将在处理完成后恢复响应,这是因为我们使用了美丽的一行,Java 8 风格的代码:

client.getResult().thenApply(this::readResponse)
.thenAccept(response::resume);

在深入细节之前,让我们先澄清我们的本地 readResponse 方法:

    private String readResponse(Response response) {
        return response.readEntity(String.class);
    }

它只是读取嵌入在Response中的User实体并将其转换为String对象(一个 JSON 字符串)。

这一行代码的另一种写法可以是这样的:

        client.getResult()
                .thenApply(r -> readResponse(r))
                .thenAccept(s -> response.resume(s));

但第一种方法更简洁、更简洁、更有趣!

关键在于AsyncResponse对象的resume方法。它将响应写入回调并返回给请求者。

另请参阅

使用事务与异步任务

使用异步任务可能已经是一个挑战:如果你需要添加一些特色并添加一个事务怎么办?

通常,事务意味着像代码阻塞这样的东西。将两个对立的概念结合起来不是有点尴尬吗?嗯,不是!它们可以很好地一起工作,就像这个菜谱将向您展示的那样。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 让我们先创建一个User POJO:
public class User {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ",
                     name=" + name + '}';
    } 
}
  1. 这里是一个将返回User的慢速 bean:
@Stateless
public class UserBean {

    public User getUser(){
        try {
            TimeUnit.SECONDS.sleep(5);
            long id = new Date().getTime();
            return new User(id, "User " + id);
        } catch (InterruptedException ex) {
            System.err.println(ex.getMessage());
            long id = new Date().getTime();
            return new User(id, "Error " + id);
        }
    }
}
  1. 现在我们创建一个要执行的任务,该任务将使用一些事务相关的内容返回User
public class AsyncTask implements Callable<User> {

    private UserTransaction userTransaction;
    private UserBean userBean;

    @Override
    public User call() throws Exception {
        performLookups();
        try {
            userTransaction.begin();
            User user = userBean.getUser();
            userTransaction.commit();
            return user;
        } catch (IllegalStateException | SecurityException | 
          HeuristicMixedException | HeuristicRollbackException | 
          NotSupportedException | RollbackException | 
          SystemException e) {
            userTransaction.rollback();
            return null;
        }
    }

    private void performLookups() throws NamingException{
        userBean = CDI.current().select(UserBean.class).get();
        userTransaction = CDI.current()
        .select(UserTransaction.class).get();
    }    
}
  1. 最后,这里是使用任务将结果写入响应的服务端点:
@Path("asyncService")
@RequestScoped
public class AsyncService {

    private AsyncTask asyncTask;

    @Resource(name = "LocalManagedExecutorService")
    private ManagedExecutorService executor; 

    @PostConstruct
    public void init(){
        asyncTask = new AsyncTask();
    }

    @GET
    public void asyncService(@Suspended AsyncResponse response){

        Future<User> result = executor.submit(asyncTask);

        while(!result.isDone()){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ex) {
                System.err.println(ex.getMessage());
            }
        }

        try {
            response.resume(Response.ok(result.get()).build());
        } catch (InterruptedException | ExecutionException ex) {
            System.err.println(ex.getMessage());
            response.resume(Response.status(Response
            .Status.INTERNAL_SERVER_ERROR)
            .entity(ex.getMessage()).build());
        }

    }
}

要尝试此代码,只需将其部署到 GlassFish 5 并打开此 URL:

http://localhost:8080/ch09-async-transaction/asyncService

它是如何工作的...

魔法发生在AsyncTask类中,我们将首先看看performLookups方法:

    private void performLookups() throws NamingException{
        Context ctx = new InitialContext();
        userTransaction = (UserTransaction) 
        ctx.lookup("java:comp/UserTransaction");
        userBean = (UserBean) ctx.lookup("java:global/
        ch09-async-transaction/UserBean");
    }

它将提供来自应用服务器的UserTransactionUserBean实例。然后你可以放松并依赖为你已经实例化的东西。

因为我们的任务实现了一个需要实现call()方法的Callable<V>对象:

    @Override
    public User call() throws Exception {
        performLookups();
        try {
            userTransaction.begin();
            User user = userBean.getUser();
            userTransaction.commit();
            return user;
        } catch (IllegalStateException | SecurityException | 
                HeuristicMixedException | HeuristicRollbackException 
                | NotSupportedException | RollbackException | 
                SystemException e) {
            userTransaction.rollback();
            return null;
        }
    }

你可以将Callable看作是一个返回结果的Runnable接口。

我们的事务代码存放在这里:

            userTransaction.begin();
            User user = userBean.getUser();
            userTransaction.commit();

如果有任何问题,我们有以下情况:

        } catch (IllegalStateException | SecurityException | 
           HeuristicMixedException | HeuristicRollbackException 
           | NotSupportedException | RollbackException | 
           SystemException e) {
            userTransaction.rollback();
            return null;
        }

现在我们将查看AsyncService。首先,我们有一些声明:

    private AsyncTask asyncTask;

    @Resource(name = "LocalManagedExecutorService")
    private ManagedExecutorService executor; 

    @PostConstruct
    public void init(){
        asyncTask = new AsyncTask();
    }

我们要求容器给我们一个ManagedExecutorService的实例,它负责在企业上下文中执行任务。

然后我们调用一个init()方法,bean 被构建(@PostConstruct)。这实例化了任务。

现在我们有了任务执行:

    @GET
    public void asyncService(@Suspended AsyncResponse response){

        Future<User> result = executor.submit(asyncTask);

        while(!result.isDone()){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ex) {
                System.err.println(ex.getMessage());
            }
        }

        try {
            response.resume(Response.ok(result.get()).build());
        } catch (InterruptedException | ExecutionException ex) {
            System.err.println(ex.getMessage());
            response.resume(Response.status(Response.
            Status.INTERNAL_SERVER_ERROR)
            .entity(ex.getMessage()).build());
        }

    }

注意,执行器返回Future<User>

Future<User> result = executor.submit(asyncTask);

这意味着这个任务将被异步执行。然后我们检查其执行状态,直到它完成:

        while(!result.isDone()){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ex) {
                System.err.println(ex.getMessage());
            }
        }

一旦完成,我们就将其写入异步响应:

response.resume(Response.ok(result.get()).build());

另请参阅

检查异步任务的状态

除了执行异步任务,这开辟了许多可能性之外,有时获取这些任务的状态是有用且必要的。

例如,你可以用它来检查每个任务阶段的耗时。你也应该考虑日志和监控。

这个菜谱将向你展示一个简单的方法来做这件事。

准备工作

首先,添加我们的 Java EE 8 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 首先,我们创建一个User POJO:
public class User {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ", 
        name=" + name + '}';
    }
}
  1. 然后我们创建一个慢速的 Bean 来返回User
public class UserBean {

    public User getUser(){
        try {
            TimeUnit.SECONDS.sleep(5);
            long id = new Date().getTime();
            return new User(id, "User " + id);
        } catch (InterruptedException ex) {
            long id = new Date().getTime();
            return new User(id, "Error " + id);
        }
    }
}
  1. 现在我们创建一个托管任务,以便我们可以监控它:
@Stateless
public class AsyncTask implements Callable<User>, ManagedTaskListener {

    private final long instantiationMili = new Date().getTime();

    private static final Logger LOG = Logger.getAnonymousLogger();

    @Override
    public User call() throws Exception {
        return new UserBean().getUser();
    }

    @Override
    public void taskSubmitted(Future<?> future, 
    ManagedExecutorService mes, Object o) {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskSubmitted: {0} - 
        Miliseconds since instantiation: {1}",
        new Object[]{future, mili - instantiationMili});
    }

    @Override
    public void taskAborted(Future<?> future, 
    ManagedExecutorService mes, Object o, Throwable thrwbl) 
    {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskAborted: {0} - 
        Miliseconds since instantiation: {1}", 
        new Object[]{future, mili - instantiationMili});
    }

    @Override
    public void taskDone(Future<?> future, 
    ManagedExecutorService mes, Object o, 
    Throwable thrwbl) {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskDone: {0} -
        Miliseconds since instantiation: {1}", 
        new Object[]{future, mili - instantiationMili});
    }

    @Override
    public void taskStarting(Future<?> future, 
    ManagedExecutorService mes, Object o) {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskStarting: {0} - 
        Miliseconds since instantiation: {1}", 
        new Object[]{future, mili - instantiationMili});
    }

}
  1. 最后,我们创建一个服务端点来执行我们的任务并返回其结果:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Resource
    private ManagedExecutorService executor;

    @GET
    public void asyncService(@Suspended AsyncResponse response) {
        int i = 0;

        List<User> usersFound = new ArrayList<>();
        while (i < 4) {
            Future<User> result = executor.submit(new AsyncTask());

            while (!result.isDone()) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ex) {
                    System.err.println(ex.getMessage());
                }
            }

            try {
                usersFound.add(result.get());
            } catch (InterruptedException | ExecutionException ex) {
                System.err.println(ex.getMessage());
            }

            i++;
        }

        response.resume(Response.ok(usersFound).build());
    }

}

要尝试这段代码,只需将其部署到 GlassFish 5 并打开此 URL:

http://localhost:8080/ch09-task-status/asyncService

它是如何工作的...

如果你已经完成了上一个菜谱,你将已经熟悉了Callable任务,所以在这里我不会给出更多细节。但现在,我们正在使用CallableManagedTaskListener接口来实现我们的任务。第二个接口提供了检查任务状态的所有方法:

    @Override
    public void taskSubmitted(Future<?> future, 
    ManagedExecutorService mes, Object o) {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskSubmitted: {0} - 
        Miliseconds since instantiation: {1}", 
        new Object[]{future, mili - instantiationMili});
    }

    @Override
    public void taskAborted(Future<?> future, 
    ManagedExecutorService mes, Object o, Throwable thrwbl) {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskAborted: {0} - 
        Miliseconds since instantiation: {1}", 
        new Object[]{future, mili - instantiationMili});
    }

    @Override
    public void taskDone(Future<?> future, 
    ManagedExecutorService mes, Object o, Throwable thrwbl) {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskDone: {0} - 
        Miliseconds since instantiation: {1}", 
        new Object[]{future, mili - instantiationMili});
    }

    @Override
    public void taskStarting(Future<?> future, 
    ManagedExecutorService mes, Object o) {
        long mili = new Date().getTime();
        LOG.log(Level.INFO, "taskStarting: {0} - 
        Miliseconds since instantiation: {1}", 
        new Object[]{future, mili - instantiationMili});
    }

最好的部分是,你不需要调用任何一个——ManagedExecutorService(将在下一节中解释)会为你做这件事。

最后,我们有AsyncService。第一个声明是为我们的执行器:

    @Resource
    private ManagedExecutorService executor;

在服务本身中,我们正在从我们的异步任务中获取四个用户:

        List<User> usersFound = new ArrayList<>();
        while (i < 4) {
            Future<User> result = executor.submit(new AsyncTask());

            while (!result.isDone()) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ex) {
                    System.err.println(ex.getMessage());
                }
            }

            try {
                usersFound.add(result.get());
            } catch (InterruptedException | ExecutionException ex) {
                System.err.println(ex.getMessage());
            }

            i++;
        }

一旦完成,它将被写入异步响应:

response.resume(Response.ok(usersFound).build());

现在,如果你查看你的服务器日志输出,将会有来自ManagedTaskListener接口的消息。

参见

使用返回结果的托管线程构建

有时候你需要改进你看待你正在使用的线程的方式;也许是为了改进你的日志功能,也许是为了管理它们的优先级。如果你也能从它们那里获取结果,那就太好了。这个菜谱将向你展示如何做到这一点。

准备工作

首先,添加我们的 Java EE 8 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何做到这一点...

  1. 首先,我们创建一个User POJO:
public class User {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ",
        name=" + name + '}';
    } 
}
  1. 然后,我们创建一个慢速的 Bean 来返回User
@Stateless
public class UserBean {

    @GET
    public User getUser(){
        try {
            TimeUnit.SECONDS.sleep(5);
            long id = new Date().getTime();
            return new User(id, "User " + id);
        } catch (InterruptedException ex) {
            long id = new Date().getTime();
            return new User(id, "Error " + id);
        }
    }
}
  1. 最后,我们创建一个端点来获取任务的结果:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Inject
    private UserBean userBean;

    @Resource(name = "LocalManagedThreadFactory")
    private ManagedThreadFactory factory;

    @GET
    public void asyncService(@Suspended AsyncResponse 
    response) {
        Thread thread = factory.newThread(() -> {
            response.resume(Response.ok(userBean
            .getUser()).build());
        });

        thread.setName("Managed Async Task");
        thread.setPriority(Thread.MIN_PRIORITY);
        thread.start();
    }

}

要尝试这段代码,只需将其部署到 GlassFish 5 并打开此 URL:

http://localhost:8080/ch09-managed-thread/asyncService

它是如何工作的...

在企业环境中使用线程的唯一方式,如果你真的想使用它,就是当应用程序服务器创建线程时。所以在这里,我们礼貌地请求容器使用factory来创建线程:

    @Resource(name = "LocalManagedThreadFactory")
    private ManagedThreadFactory factory;

使用一些函数式风格的代码,我们创建我们的线程:

        Thread thread = factory.newThread(() -> {
            response.resume(Response.ok(userBean.getUser()).build());
        });

现在,转向托管部分,我们可以设置新创建的线程的名称和优先级:

        thread.setName("Managed Async Task");
        thread.setPriority(Thread.MIN_PRIORITY);

并且不要忘记要求容器启动它:

        thread.start();

参见

使用返回结果的异步任务调度

使用任务意味着也能够定义它们应该在何时执行。这个配方就是关于这个主题的,也是关于在它们返回时获取返回结果。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何实现...

  1. 让我们先创建一个User POJO:
public class User {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ",
        name=" + name + '}';
    }
}
  1. 然后,我们创建一个慢速 bean 来返回User
public class UserBean {

    public User getUser(){
        try {
            TimeUnit.SECONDS.sleep(5);
            long id = new Date().getTime();
            return new User(id, "User " + id);
        } catch (InterruptedException ex) {
            long id = new Date().getTime();
            return new User(id, "Error " + id);
        }
    }
}
  1. 现在我们创建一个简单的Callable任务与 bean 通信:
public class AsyncTask implements Callable<User> {

    private final UserBean userBean = 
    CDI.current().select(UserBean.class).get();

    @Override
    public User call() throws Exception {
        return userBean.getUser();
    }
}
  1. 最后,我们创建我们的服务以安排任务并将结果写入响应:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Resource(name = "LocalManagedScheduledExecutorService")
    private ManagedScheduledExecutorService executor;

    @GET
    public void asyncService(@Suspended AsyncResponse response) {

        ScheduledFuture<User> result = executor.schedule
        (new AsyncTask(), 5, TimeUnit.SECONDS);

        while (!result.isDone()) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ex) {
                System.err.println(ex.getMessage());
            }
        }

        try {
            response.resume(Response.ok(result.get()).build());
        } catch (InterruptedException | ExecutionException ex) {
            System.err.println(ex.getMessage());
            response.resume(Response.status(Response.Status
           .INTERNAL_SERVER_ERROR).entity(ex.getMessage())
           .build());
        }

    }

}

要尝试这段代码,只需将其部署到 GlassFish 5 并打开此 URL:

http://localhost:8080/ch09-scheduled-task/asyncService

它是如何工作的...

所有魔法都依赖于AsyncService类,所以我们将重点关注它。

首先,我们向服务器请求一个执行器的实例:

    @Resource(name = "LocalManagedScheduledExecutorService")
    private ManagedScheduledExecutorService executor;

但它不仅仅是一个执行器——这是一个专门用于调度的执行器:

ScheduledFuture<User> result = executor.schedule(new AsyncTask(), 
5, TimeUnit.SECONDS);

因此,我们将任务安排在五秒后执行。请注意,我们也没有使用常规的Future,而是使用ScheduledFuture

其余的都是常规的任务执行:

        while (!result.isDone()) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ex) {
                System.err.println(ex.getMessage());
            }
        }

这就是我们将结果写入响应的方式:

response.resume(Response.ok(result.get()).build());

参见

使用注入的代理进行异步任务

当使用任务时,您也可以创建自己的执行器。如果您有非常具体的需求,这可能会非常有用。

这个配方将向您展示如何创建一个可以注入并用于应用程序整个上下文的代理执行器。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

如何实现...

  1. 首先,我们创建一个User POJO:
public class User implements Serializable{

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ", 
        name=" + name + '}';
    }
}
  1. 然后我们创建一个慢速 bean 来返回User
public class UserBean {

    public User getUser(){
        try {
            TimeUnit.SECONDS.sleep(5);
            long id = new Date().getTime();
            return new User(id, "User " + id);
        } catch (InterruptedException ex) {
            long id = new Date().getTime();
            return new User(id, "Error " + id);
        }
    }
}
  1. 现在我们创建一个简单的Callable任务与慢速 bean 通信:
@Stateless
public class AsyncTask implements Callable<User>{

    @Override
    public User call() throws Exception {
        return new UserBean().getUser();
    }

}
  1. 在这里,我们调用我们的代理:
@Singleton
public class ExecutorProxy {

    @Resource(name = "LocalManagedThreadFactory")
    private ManagedThreadFactory factory;

    @Resource(name = "LocalContextService")
    private ContextService context;

    private ExecutorService executor;

    @PostConstruct
    public void init(){
        executor = new ThreadPoolExecutor(1, 5, 10, 
        TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
        factory);
    }

    public Future<User> submit(Callable<User> task){
        Callable<User> ctxProxy = 
        context.createContextualProxy(task, Callable.class);
        return executor.submit(ctxProxy);
    }
}
  1. 最后,我们创建将使用代理的端点:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Inject
    private ExecutorProxy executor;

    @GET
    public void asyncService(@Suspended AsyncResponse response) 
    {
        Future<User> result = executor.submit(new AsyncTask());
        response.resume(Response.ok(result).build());
    }

}

要尝试这段代码,只需将其部署到 GlassFish 5 并打开此 URL:

http://localhost:8080/ch09-proxy-task/asyncService

它是如何工作的...

真正的魔法就在这里的ExecutorProxy任务中。首先请注意,我们是这样定义它的:

@Singleton

我们确保在上下文中只有一个实例。

现在请注意,尽管我们正在创建自己的执行器,但我们仍然依赖于应用程序服务器上下文:

    @Resource(name = "LocalManagedThreadFactory")
    private ManagedThreadFactory factory;

    @Resource(name = "LocalContextService")
    private ContextService context;

这保证了您不会违反任何上下文规则并永久性地破坏您的应用程序。

然后我们创建一个线程池来执行线程:

    private ExecutorService executor;

    @PostConstruct
    public void init(){
        executor = new ThreadPoolExecutor(1, 5, 10,
        TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), factory);
    }

最后,我们创建将任务发送到执行队列的方法:

    public Future<User> submit(Callable<User> task){
        Callable<User> ctxProxy = context.createContextualProxy(task, 
        Callable.class);
        return executor.submit(ctxProxy);
    }

现在我们的代理已准备好注入:

    @Inject
    private ExecutorProxy executor;

它也准备好被调用并返回结果:

    @GET
    public void asyncService(@Suspended AsyncResponse response) {
        Future<User> result = executor.submit(new AsyncTask());
        response.resume(Response.ok(result).build());
    }

参见

第十章:使用事件驱动编程来构建响应式应用程序

本章涵盖了以下食谱:

  • 使用异步 servlet 构建响应式应用程序

  • 使用事件和观察者构建响应式应用程序

  • 使用 websockets 构建响应式应用程序

  • 使用消息驱动的 bean 构建响应式应用程序

  • 使用 JAX-RS 构建响应式应用程序

  • 使用异步会话 bean 构建响应式应用程序

  • 使用 lambda 和CompletableFuture来改进响应式应用程序

简介

响应式开发成为许多开发者会议、聚会、博客文章和其他无数内容来源(包括在线和离线)的热门话题。

但什么是响应式应用程序?嗯,有一个官方的定义包含在被称为《响应式宣言》(请参阅www.reactivemanifesto.org以获取更多详细信息)的东西中。

简而言之,根据宣言,响应式系统是:

  • 响应性:如果可能的话,系统会及时响应

  • 弹性:面对失败时,系统保持响应性

  • 弹性:系统在变化的工作负载下保持响应性

  • 消息驱动:响应式系统依赖于异步消息传递来建立组件之间的边界,确保松散耦合、隔离和位置透明

因此,本章将向您展示如何使用 Java EE 8 功能来满足一个或多个那些响应式系统要求。

使用异步 servlet 构建响应式应用程序

Servlet 可能是最著名的 Java EE 技术之一(也许是最著名的)。实际上,servlet 在 J2EE 成为真正的规范之前就已经存在了。

这个食谱将向您展示如何异步使用 servlet。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

如何做到这一点...

  1. 首先,我们创建一个User POJO:
public class User implements Serializable{

    private Long id;
    private String name;

    public User(long id, String name){
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
  1. 然后,创建一个慢速的UserBean来返回User
@Stateless
public class UserBean {

    public User getUser(){
        long id = new Date().getTime();

        try {
            TimeUnit.SECONDS.sleep(5);
            return new User(id, "User " + id);
        } catch (InterruptedException ex) {
            System.err.println(ex.getMessage());
            return new User(id, "Error " + id);
        }
    }
}
  1. 最后,创建我们的异步 servlet:
@WebServlet(name = "UserServlet", urlPatterns = {"/UserServlet"}, asyncSupported = true)
public class UserServlet extends HttpServlet {

    @Inject
    private UserBean userBean;

    private final Jsonb jsonb = JsonbBuilder.create();

    @Override
    protected void doGet(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, 
    IOException {
        AsyncContext ctx = req.startAsync();
        ctx.start(() -> {
            try (PrintWriter writer = 
            ctx.getResponse().getWriter()){
                writer.write(jsonb.toJson(userBean.getUser()));
            } catch (IOException ex) {
                System.err.println(ex.getMessage());
            }
            ctx.complete();
        });
    }

    @Override
    public void destroy() {
        try {
            jsonb.close();
        } catch (Exception ex) {
            System.err.println(ex.getMessage());
        }
    }

}

它是如何工作的...

在这里所有重要的事情中,我们应该从一个简单的注解开始:

asyncSupported = true

这将告诉应用程序服务器这个 servlet 支持异步功能。顺便说一句,你需要在整个 servlet 链中(包括过滤器,如果有)使用它,否则应用程序服务器将无法工作。

由于 servlet 是由服务器实例化的,我们可以在其上注入其他上下文成员,例如我们的无状态 bean:

@Inject
private UserBean userBean;

主要 servlet 方法持有实际的请求和响应引用,请求将给我们异步 API 的上下文引用:

AsyncContext ctx = req.startAsync();

然后,您可以以前非阻塞的方式执行您之前的阻塞函数:

ctx.start(() -> {
    ...
    ctx.complete();
});

参见

使用事件和观察者构建响应式应用程序

事件和观察者是编写反应式代码的绝佳方式,无需过多思考,这要归功于 CDI 规范的出色工作。

这个配方将向您展示如何轻松地使用它来提高您应用程序的用户体验。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

如何实现...

  1. 让我们先创建一个 User POJO:
public class User implements Serializable{

    private Long id;
    private String name;

    public User(long id, String name){
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
  1. 然后,让我们创建一个具有事件和观察器功能的 REST 端点:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Inject
    private Event<User> event;

    private AsyncResponse response;

    @GET
    public void asyncService(@Suspended AsyncResponse response){
        long id = new Date().getTime();
        this.response = response;
        event.fireAsync(new User(id, "User " + id));
    }

    public void onFireEvent(@ObservesAsync User user){
        response.resume(Response.ok(user).build());
    }
}

它是如何工作的...

首先,我们要求应用服务器为 User POJO 创建一个 Event 源:

@Inject
private Event<User> event;

这意味着它将监听针对任何 User 对象触发的任何事件。因此,我们需要创建一个方法来处理它:

public void onFireEvent(@ObservesAsync User user){
    response.resume(Response.ok(user).build());
}

因此,现在这种方法是合适的监听器。@ObserversAsync 注解保证了这一点。所以一旦异步事件被触发,它就会执行我们要求(或编码)的任何操作。

然后,我们创建了一个简单的异步端点来触发它:

@GET
public void asyncService(@Suspended AsyncResponse response){
    long id = new Date().getTime();
    this.response = response;
    event.fireAsync(new User(id, "User " + id));
}

参见

使用 WebSocket 构建反应式应用程序

Websockets 是为您的应用程序创建解耦通信通道的绝佳方式。以异步方式执行甚至更好,更酷,因为这样可以实现非阻塞功能。

这个配方将展示如何实现。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

如何实现...

  1. 我们需要的第一件事是我们的服务器端点:
@Singleton
@ServerEndpoint(value = "/asyncServer")
public class AsyncServer {

    private final List<Session> peers = Collections.synchronizedList(new ArrayList<>());

    @OnOpen
    public void onOpen(Session peer){
        peers.add(peer);
    }

    @OnClose
    public void onClose(Session peer){
        peers.remove(peer);
    }

    @OnError
    public void onError(Throwable t){
        System.err.println(t.getMessage());
    }

    @OnMessage
    public void onMessage(String message, Session peer){
        peers.stream().filter((p) -> 
        (p.isOpen())).forEachOrdered((p) -> {
            p.getAsyncRemote().sendText(message + 
            " - Total peers: " + peers.size());
        });
    }
}
  1. 然后,我们需要一个客户端与服务器进行通信:
@ClientEndpoint
public class AsyncClient {

    private final String asyncServer = "ws://localhost:8080
    /ch10-async-websocket/asyncServer";

    private Session session;
    private final AsyncResponse response;

    public AsyncClient(AsyncResponse response) {
        this.response = response;
    }

    public void connect() {
        WebSocketContainer container = 
        ContainerProvider.getWebSocketContainer();
        try {
            container.connectToServer(this, new URI(asyncServer));
        } catch (URISyntaxException | DeploymentException | 
          IOException ex) {
            System.err.println(ex.getMessage());
        }

    }

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        response.resume(message);
    }

    public void send(String message) {
        session.getAsyncRemote().sendText(message);
    }

    public void close(){
        try {
            session.close();
        } catch (IOException ex) {
            System.err.println(ex.getMessage());
        }
    }
}
  1. 最后,我们需要一个简单的 REST 端点与客户端通信:
@Stateless
@Path("asyncService")
public class AsyncService {

    @GET
    public void asyncService(@Suspended AsyncResponse response){
        AsyncClient client = new AsyncClient(response);
        client.connect();
        client.send("Message from client " + new Date().getTime());
        client.close();
    }
}

它是如何工作的...

我们服务器中的第一件重要的事情是这个注解:

@Singleton

当然,我们必须确保我们只有一个服务器端点实例。这将确保所有 peers 都在同一个伞下管理。

让我们继续讨论 peers

private final List<Session> peers = Collections.synchronizedList
(new ArrayList<>());

存储它们的列表是同步的。这很重要,因为您将在迭代列表时添加/删除对等方,如果不保护它,事情可能会变得混乱。

所有默认的 WebSocket 方法都由应用服务器管理:

@OnOpen
public void onOpen(Session peer){
    peers.add(peer);
}

@OnClose
public void onClose(Session peer){
    peers.remove(peer);
}

@OnError
public void onError(Throwable t){
    System.err.println(t.getMessage());
}

@OnMessage
public void onMessage(String message, Session peer){
    peers.stream().filter((p) -> (p.isOpen())).forEachOrdered((p) -> 
    {
        p.getAsyncRemote().sendText(message + " - Total peers: " 
        + peers.size());
    });
}

此外,让我们特别提一下我们 onMessage 方法上的代码:

    @OnMessage
    public void onMessage(String message, Session peer){
        peers.stream().filter((p) -> (p.isOpen())).forEachOrdered((p)
        -> {
            p.getAsyncRemote().sendText(message + " - Total peers: "
            + peers.size());
        });
    }

我们只在连接打开时向对等方发送消息。

现在看看我们的客户端,我们有一个指向服务器 URI 的引用:

private final String asyncServer = "ws://localhost:8080/
ch10-async-websocket/asyncServer";

注意,协议是 ws,这是针对 WebSocket 通信的特定协议。

然后,我们有一个方法可以打开与服务器端点的连接:

public void connect() {
    WebSocketContainer container = 
    ContainerProvider.getWebSocketContainer();
    try {
        container.connectToServer(this, new URI(asyncServer));
    } catch (URISyntaxException | DeploymentException | IOException ex) {
        System.err.println(ex.getMessage());
    }
}

一旦我们从服务器收到消息确认,我们就可以对它采取行动:

@OnMessage
public void onMessage(String message, Session session) {
    response.resume(message);
}

这个响应将出现在调用客户端的端点上:

@GET
public void asyncService(@Suspended AsyncResponse response){
    AsyncClient client = new AsyncClient(response);
    client.connect();
    client.send("Message from client " + new Date().getTime());
}

我们正在传递客户端的引用,以便客户端可以使用它来在它上面写入消息。

参见

使用消息驱动豆构建反应式应用程序

Java 消息服务是 Java EE 最古老的 API 之一,它从一开始就是反应式的:只需阅读本章引言中链接的宣言。

本食谱将展示如何使用消息驱动豆,或称为 MDB,仅通过几个注解即可发送和消费异步消息。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

要检查 GlassFish 5 中队列设置的详细信息,请参阅第五章使用消息服务进行异步通信的食谱企业架构的安全性

如何做到这一点...

  1. 首先,我们创建一个User POJO:
public class User implements Serializable{

    private Long id;
    private String name;

    public User(long id, String name){
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
  1. 然后,我们创建一个消息发送者:
@Stateless
public class Sender {

    @Inject
    private JMSContext context;

    @Resource(lookup = "jms/JmsQueue")
    private Destination queue;

    public void send(User user){
        context.createProducer()
                .setDeliveryMode(DeliveryMode.PERSISTENT)
                .setDisableMessageID(true)
                .setDisableMessageTimestamp(true)
                .send(queue, user);
    }

}
  1. 现在,我们创建一个消息消费者。这是我们自己的 MDB:
@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationLookup", 
    propertyValue = "jms/JmsQueue"),
    @ActivationConfigProperty(propertyName = "destinationType", 
    propertyValue = "javax.jms.Queue")
})
public class Consumer implements MessageListener{

    @Override
    public void onMessage(Message msg) {
        try {
            User user = msg.getBody(User.class);
            System.out.println("User: " + user);
        } catch (JMSException ex) {
            System.err.println(ex.getMessage());
        }
    }

}
  1. 最后,我们创建一个端点,仅用于向队列发送模拟用户:
@Stateless
@Path("mdbService")
public class MDBService {

    @Inject
    private Sender sender;

    public void mdbService(@Suspended AsyncResponse response){
        long id = new Date().getTime();
        sender.send(new User(id, "User " + id));
        response.resume("Message sent to the queue");
    }
}

它是如何工作的...

我们首先向应用程序服务器请求一个 JMS 上下文实例:

@Inject
private JMSContext context;

我们还发送了一个我们想要与之工作的队列的引用:

@Resource(lookup = "jms/JmsQueue")
private Destination queue;

然后,使用上下文,我们创建一个生产者来将消息发送到队列:

context.createProducer()
        .setDeliveryMode(DeliveryMode.PERSISTENT)
        .setDisableMessageID(true)
        .setDisableMessageTimestamp(true)
        .send(queue, user);

注意这三个方法:

  • setDeliveryMode:此方法可以是PERSISTENTNON_PERSISTENT。如果使用PERSISTENT,服务器将特别关注消息,不会丢失它。

  • setDisableMessageID:此选项用于创建MessageID,这会增加服务器创建和发送消息的努力,并增加其大小。此属性(truefalse)向服务器提供提示,表明您不需要/使用它,因此它可以改进此过程。

  • setDisableMessageTimestamp:这与setDisableMessageID相同。

此外,请注意,我们正在向队列发送一个User实例。因此,您可以轻松地发送对象实例,而不仅仅是文本消息,只要它们实现了可序列化接口。

MDB 本身,或我们的消息消费者,基本上是几个注解和一个接口实现。

这里是其注解:

@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationLookup", 
    propertyValue = "jms/JmsQueue"),
    @ActivationConfigProperty(propertyName = "destinationType", 
    propertyValue = "javax.jms.Queue")
})

在这里,我们使用了两个属性:一个用于定义我们正在查找哪个队列(destinationLookup),另一个用于定义它确实是我们要使用的队列类型(destinationType)。

这里是其实施:

@Override
public void onMessage(Message msg) {
    try {
        User user = msg.getBody(User.class);
        System.out.println("User: " + user);
    } catch (JMSException ex) {
        System.err.println(ex.getMessage());
    }
}

注意,从消息体中获取User实例很容易:

User user = msg.getBody(User.class);

完全没有繁重的任务。

并且用于发送消息的端点非常简单。我们注入了Sender(这是一个无状态豆):

@Inject
private Sender sender;

然后,我们调用一个异步方法:

public void mdbService(@Suspended AsyncResponse response){
    long id = new Date().getTime();
    sender.send(new User(id, "User " + id));
    response.resume("Message sent to the queue");
}

参见

使用 JAX-RS 构建反应式应用程序

JAX-RS API 还有一些针对事件驱动编程的出色功能。本食谱将展示您如何使用异步调用者从请求中编写回调函数来生成响应。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

如何做到这一点...

  1. 首先,我们创建一个User POJO:
public class User implements Serializable{

    private Long id;
    private String name;

    public User(long id, String name){
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
  1. 在这里,我们定义了UserBean,它将作为远程端点:
@Stateless
@Path("remoteUser")
public class UserBean {

    @GET
    public Response remoteUser() {
        long id = new Date().getTime();
        try {
            TimeUnit.SECONDS.sleep(5);
            return Response.ok(new User(id, "User " + id))
            .build();
        } catch (InterruptedException ex) {
            System.err.println(ex.getMessage());
            return Response.ok(new User(id, "Error " + id))
            .build();
        }
    }

}
  1. 然后,最后,我们定义一个本地端点,它将消费远程端点:
@Stateless
@Path("asyncService")
public class AsyncService {

    private Client client;
    private WebTarget target;

    @PostConstruct
    public void init() {
        client = ClientBuilder.newBuilder()
                .readTimeout(10, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .build();
        target = client.target("http://localhost:8080/
        ch10-async-jaxrs/remoteUser");
    }

    @PreDestroy
    public void destroy(){
        client.close();
    }

    @GET
    public void asyncService(@Suspended AsyncResponse response){
        target.request().async().get(new 
        InvocationCallback<Response>() {
            @Override
            public void completed(Response rspns) {
                response.resume(rspns);
            }

            @Override
            public void failed(Throwable thrwbl) {
                response.resume(Response.status(Response.Status.
                INTERNAL_SERVER_ERROR).entity(thrwbl.getMessage())
                .build());
            }
        });

    }

}

它是如何工作的...

我们通过在 Bean 实例化时直接与远程端点建立通信来启动 Bean。这样做将避免在调用发生时稍后执行的开销:

private Client client;
private WebTarget target;

@PostConstruct
public void init() {
     client = ClientBuilder.newBuilder()
            .readTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .build();
    target = client.target("http://localhost:8080/
    ch10-async-jaxrs/remoteUser");
}

然后,我们在我们的异步调用器中创建了一个匿名InvocationCallback实现:

        target.request().async().get(new InvocationCallback<Response>()   
        {
            @Override
            public void completed(Response rspns) {
                response.resume(rspns);
            }

            @Override
            public void failed(Throwable thrwbl) {
                System.err.println(thrwbl.getMessage());
            }
        });

这样,我们可以依赖completedfailed事件,并妥善处理它们。

参见

使用异步会话 Bean 构建反应式应用程序

会话 Bean 也可以仅通过使用注解成为反应式和事件驱动的。这个菜谱将向您展示如何做到这一点。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

如何做...

  1. 首先,我们创建一个User POJO:
public class User implements Serializable{

    private Long id;
    private String name;

    public User(long id, String name){
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
  1. 然后,我们创建我们的异步会话 Bean:
@Stateless
public class UserBean {

    @Asynchronous
    public Future<User> getUser(){
        long id = new Date().getTime();
        User user = new User(id, "User " + id);
        return new AsyncResult(user);
    }

    @Asynchronous
    public void doSomeSlowStuff(User user){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException ex) {
            System.err.println(ex.getMessage());
        }
    }
}
  1. 最后,我们创建了将调用 Bean 的端点:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Inject
    private UserBean userBean;

    @GET
    public void asyncService(@Suspended AsyncResponse response){
        try {
            Future<User> result = userBean.getUser();

            while(!result.isDone()){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ex) {
                    System.err.println(ex.getMessage());
                }
            }

            response.resume(Response.ok(result.get()).build());
        } catch (InterruptedException | ExecutionException ex) {
            System.err.println(ex.getMessage());
        }
    }
}

它是如何工作的...

让我们先检查会话 Bean 中的getUser方法:

    @Asynchronous
    public Future<User> getUser(){
        long id = new Date().getTime();
        User user = new User(id, "User " + id);
        return new AsyncResult(user);
    }

一旦我们使用@Asynchronous注解,我们必须将其返回值转换为某种(在我们的情况下,User)的Future实例。

我们还创建了一个void方法来向您展示如何使用会话 Bean 创建非阻塞代码:

    @Asynchronous
    public void doSomeSlowStuff(User user){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException ex) {
            System.err.println(ex.getMessage());
        }
    }

最后,我们创建了我们的调用端点:

    @GET
    public void asyncService(@Suspended AsyncResponse response){
        try {
            Future<User> result = userBean.getUser();

            while(!result.isDone()){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ex) {
                    System.err.println(ex.getMessage());
                }
            }

            response.resume(Response.ok(result.get()).build());
        } catch (InterruptedException | ExecutionException ex) {
            System.err.println(ex.getMessage());
        }
    }

由于getUser返回Future,我们可以处理异步状态检查。一旦完成,我们将结果写入响应(也是异步的)。

参见

使用 lambda 和 CompletableFuture 来提高反应式应用程序

Java 语言一直以冗长著称。但自从 lambda 出现以来,这个问题已经大大改善。

我们可以使用 lambda 表达式,并引入CompletableFuture来提高不仅编码,而且反应式应用程序的行为。这个菜谱将向您展示如何做到这一点。

准备工作

让我们先添加我们的 Java EE 8 依赖项:

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

如何做...

  1. 首先,我们创建一个User POJO:
public class User implements Serializable{

    private Long id;
    private String name;

    public User(long id, String name){
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
  1. 然后,我们调用UserBean以返回一个User实例:
@Stateless
public class UserBean {

    public User getUser() {
        long id = new Date().getTime();
        try {
            TimeUnit.SECONDS.sleep(5);
            return new User(id, "User " + id);
        } catch (InterruptedException ex) {
            System.err.println(ex.getMessage());
            return new User(id, "Error " + id);
        }
    }

}
  1. 最后,我们创建一个异步端点来调用 Bean:
@Stateless
@Path("asyncService")
public class AsyncService {

    @Inject
    private UserBean userBean;

    @GET
    public void asyncService(@Suspended AsyncResponse response) 
    {
        CompletableFuture
                .supplyAsync(() -> userBean.getUser())
                .thenAcceptAsync((u) -> {
                    response.resume(Response.ok(u).build());
                }).exceptionally((t) -> {
                    response.resume(Response.status
                    (Response.Status.
                    INTERNAL_SERVER_ERROR).entity(t.getMessage())
                    .build());
                    return null;
                });
    }
}

它是如何工作的...

我们基本上使用两个CompletableFuture方法:

  • supplyAsync:这将启动对您放入其中的任何内容的异步调用。我们放入一个 lambda 调用。

  • thenAcceptAsync:一旦异步过程完成,返回值将在这里。多亏了 lambda,我们可以将这个返回值称为u(可以是任何我们想要的)。然后,我们用它来写入异步响应。

参见

第十一章:迈向云端——Java EE、容器和云计算

本章涵盖了以下食谱:

  • 使用 Docker 构建 Java EE 容器

  • 在云端使用 Oracle Cloud 进行容器编排

  • 在云端使用 Jelastic 进行容器编排

  • 在云端使用 OpenShift 进行容器编排

  • 在云端使用 AWS 进行容器编排

简介

计算机行业发生了两件改变其命运的事情——云计算容器。云计算先出现,改变了看待基础设施、消费软件以及许多业务增长的方式。现在,计算已成为一种商品。

容器正在改变,并且正在改变我们构建和交付软件的方式。它们也是 DevOps 的关键粘合剂,以及将 CI/CD 提升到另一个层次的方法。

将它们组合起来,您将拥有 IT 领域中最强大的环境之一。但是 Java EE 能否利用它呢?当然可以!如果应用程序服务器是 Java EE 应用的抽象,那么容器就是服务器的抽象,一旦您在 Docker 这样的标准中构建了它们,您就有权使用这些工具来管理应用程序服务器。

本章将向您展示如何将您的 Java EE 应用程序放入容器中,以及如何使用我们今天拥有的最佳提供商之一交付这个容器。

使用 Docker 构建 Java EE 容器

从第一天起,Java EE 就基于容器。如果您对此表示怀疑,只需查看这张图:

Java EE 架构:https://docs.oracle.com/javaee/6/tutorial/doc/bnacj.html

它属于 Oracle 公司官方的 Java EE 6 文档,实际上,自从 Sun 时代以来,其架构一直保持不变。

如果您注意观察,您会注意到存在不同的容器:一个 Web 容器、一个 EJB 容器和应用程序客户端容器。在这个架构中,这意味着使用这些 API 开发的应用程序将依赖于容器提供的许多特性和服务。

当我们将 Java EE 应用程序服务器放入 Docker 容器中时,我们正在做同样的事情——它依赖于 Docker 环境提供的一些特性和服务。

这个食谱将向您展示如何将 Java EE 应用程序打包在一个容器包中,这被称为设备

准备工作

当然,首先您需要在您的环境中安装 Docker 平台。有很多选择,所以我建议您访问以下链接以获取更多详细信息:

docs.docker.com/install/

如果您不熟悉 Docker 命令,我建议您查看这份精美的速查表:

zeroturnaround.com/rebellabs/docker-commands-and-best-practices-cheat-sheet/

你还需要在 Docker Hub 上创建一个账户,以便你可以存储自己的镜像。查看它:hub.docker.com/

公共镜像免费。

如何操作...

要构建你的 Java EE 容器,你首先需要一个 Docker 镜像。要构建它,你需要一个像这样的 Dockerfile:

FROM openjdk:8-jdk

ENV GLASSFISH_HOME /usr/local/glassfish
ENV PATH ${GLASSFISH_HOME}/bin:$PATH
ENV GLASSFISH_PKG latest-glassfish.zip
ENV GLASSFISH_URL https://download.oracle.com/glassfish/5.0/nightly/latest-glassfish.zip

RUN mkdir -p ${GLASSFISH_HOME}

WORKDIR ${GLASSFISH_HOME}

RUN set -x \
  && curl -fSL ${GLASSFISH_URL} -o ${GLASSFISH_PKG} \
    && unzip -o $GLASSFISH_PKG \
    && rm -f $GLASSFISH_PKG \
  && mv glassfish5/* ${GLASSFISH_HOME} \
  && rm -Rf glassfish5

RUN addgroup glassfish_grp \
    && adduser --system glassfish \
    && usermod -G glassfish_grp glassfish \
    && chown -R glassfish:glassfish_grp ${GLASSFISH_HOME} \
    && chmod -R 777 ${GLASSFISH_HOME}

COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh

USER glassfish

ENTRYPOINT ["/docker-entrypoint.sh"]

EXPOSE 4848 8080 8181
CMD ["asadmin", "start-domain", "-v"]

这个镜像将成为我们的基础镜像,我们将从这个镜像构建本章中的其他镜像。现在我们需要构建它:

docker build -t eldermoraes/gf-javaee-jdk8 .

好吧,把它推送到你的 Docker Hub Docker Registry:

docker push eldermoraes/gf-javaee-jdk8

现在,你可以通过自定义上一个镜像来创建另一个镜像,然后将你的应用程序放在上面:

FROM eldermoraes/gf-javaee-jdk8

ENV DEPLOYMENT_DIR ${GLASSFISH_HOME}/glassfish/domains/domain1/autodeploy/

COPY app.war ${DEPLOYMENT_DIR}

在同一个文件夹中,我们有一个 Java EE 应用程序文件(app.war),它将被部署在容器内。查看“也见”部分以下载所有文件。

保存你的 Dockerfile 后,你可以构建你的镜像:

docker build -t eldermoraes/gf-javaee-cookbook .

现在你可以创建容器:

docker run -d --name gf-javaee-cookbook \
 -h gf-javaee-cookbook \
 -p 80:8080 \
 -p 4848:4848 \
 -p 8686:8686 \
 -p 8009:8009 \
 -p 8181:8181 \
 eldermoraes/gf-javaee-cookbook

等待几秒钟,然后在你的浏览器中打开此 URL:

http://localhost/app

它是如何工作的...

让我们理解我们的第一个 Dockerfile:

FROM openjdk:8-jdk

这个FROM关键字将要求 Docker 拉取openjdk:8-jdk镜像,但这意味着什么?

这意味着在某个地方有一个注册表,你的 Docker 可以在那里找到预构建的镜像。如果你的本地环境中没有镜像注册表,它将在 Docker Hub 上搜索,这是云中的官方和公共 Docker 注册表。

当你说你使用的是预构建镜像时,这意味着你不需要从头开始构建,在我们的例子中,整个 Linux 容器。已经有一个你可以依赖的模板:

ENV GLASSFISH_HOME /usr/local/glassfish
ENV PATH ${GLASSFISH_HOME}/bin:$PATH
ENV GLASSFISH_PKG latest-glassfish.zip
ENV GLASSFISH_URL https://download.oracle.com/glassfish/5.0/nightly/latest-glassfish.zip

RUN mkdir -p ${GLASSFISH_HOME}

WORKDIR ${GLASSFISH_HOME}

这里有一些环境变量,可以帮助进行编码。

RUN set -x \
 && curl -fSL ${GLASSFISH_URL} -o ${GLASSFISH_PKG} \
 && unzip -o $GLASSFISH_PKG \
 && rm -f $GLASSFISH_PKG \
 && mv glassfish5/* ${GLASSFISH_HOME} \
 && rm -Rf glassfish5

Dockerfile 中的RUN子句在容器创建时执行一些 bash 命令。基本上,这里发生的事情是 GlassFish 正在被下载并在容器中准备:

RUN addgroup glassfish_grp \
 && adduser --system glassfish \
 && usermod -G glassfish_grp glassfish \
 && chown -R glassfish:glassfish_grp ${GLASSFISH_HOME} \
 && chmod -R 777 ${GLASSFISH_HOME}

为了安全起见,我们定义了将持有 GlassFish 文件和进程权限的用户:

COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh

在这里,我们在容器内包含一个 bash 脚本以执行一些 GlassFish 管理任务:

#!/bin/bash

if [[ -z $ADMIN_PASSWORD ]]; then
  ADMIN_PASSWORD=$(date| md5sum | fold -w 8 | head -n 1)
  echo "##########GENERATED ADMIN PASSWORD: $ADMIN_PASSWORD
  ##########"
fi

echo "AS_ADMIN_PASSWORD=" > /tmp/glassfishpwd
echo "AS_ADMIN_NEWPASSWORD=${ADMIN_PASSWORD}" >> /tmp/glassfishpwd

asadmin --user=admin --passwordfile=/tmp/glassfishpwd change-admin-password --domain_name domain1
asadmin start-domain

echo "AS_ADMIN_PASSWORD=${ADMIN_PASSWORD}" > /tmp/glassfishpwd

asadmin --user=admin --passwordfile=/tmp/glassfishpwd enable-secure-admin
asadmin --user=admin stop-domain
rm /tmp/glassfishpwd

exec "$@"

在将 bash 文件复制到容器后,我们进入最后的块:

USER glassfish

ENTRYPOINT ["/docker-entrypoint.sh"]

EXPOSE 4848 8080 8181
CMD ["asadmin", "start-domain", "-v"]

USER子句定义了从文件此点开始将使用的用户。这很棒,因为从那里开始,所有任务都将由glassfish用户执行。

ENTRYPOINT子句将执行docker-entrypoint.sh脚本。

EXPOSE子句将定义使用此镜像的容器可用的端口。

最后,CMD子句将调用初始化容器的 GlassFish 脚本。

现在我们来理解我们的第二个 Dockerfile:

FROM eldermoraes/gf-javaee-jdk8

我们需要考虑与预构建镜像相同的考虑因素,但现在这个镜像是由你制作的。恭喜你!

ENV DEPLOYMENT_DIR ${GLASSFISH_HOME}/glassfish/domains/domain1/autodeploy/

在这里,我们正在构建一个环境变量来帮助部署。它是以与 Linux 系统相同的方式进行:

COPY app.war ${DEPLOYMENT_DIR}

这个COPY命令将直接将app.war文件复制到DEPLOYMENT_DIR环境变量定义的文件夹中。

从这里开始,你就可以构建一个镜像并创建一个容器。镜像构建器是自我解释的:

docker build -t eldermoraes/gf-javaee-cookbook .

让我们检查docker run命令:

docker run -d --name gf-javaee-cookbook \
 -h gf-javaee-cookbook \
 -p 80:8080 \
 -p 4848:4848 \
 -p 8686:8686 \
 -p 8009:8009 \
 -p 8181:8181 \
 eldermoraes/gf-javaee-cookbook

如果我们将其分解,以下是命令中各个元素的含义:

  • -h: 定义容器的主机名。

  • -p: 定义了哪些端口将被暴露以及如何实现。例如,当多个容器默认使用相同的端口时,这很有用——你只需以不同的方式使用它们。

  • eldermoraes/gf-javaee-cookbook: 对你刚刚构建的镜像的引用。

相关链接

在云中使用 Oracle Cloud 进行容器编排

在云中使用容器最佳方式是通过使用提供商。为什么?因为它们可以以较低的价格提供良好的基础设施和优质的服务。

本食谱将向您展示如何获取本章第一个食谱中创建的容器,并使用 Oracle Cloud 进行交付。

准备工作

如果您没有 Oracle Cloud 账户,您可以在cloud.oracle.com/tryit注册试用。

这就是所有你需要做的,除了在本章第一个食谱中创建 Docker 镜像之外。

如何操作...

  1. 登录平台后,您将看到此仪表板:

图片

Oracle Cloud 仪表板

  1. 滚动页面,直到找到 Oracle Cloud Infrastructure - 容器服务并点击它:

图片

容器服务访问

  1. 在容器服务的主页(以下截图)上,点击“我的服务 URL”链接:

图片

容器服务概述页面

  1. 您将进入云服务仪表板。点击“容器经典”:

图片

云服务仪表板

  1. 在打开的页面上,点击“打开服务控制台”按钮:

图片

服务控制台访问

  1. 在下一页上,点击“创建实例”按钮:

图片

容器云服务欢迎页面

  1. 填写表格如下:

图片

实例创建字段

在“SSH 公钥”字段中,您需要设置一个有效的公钥,它有一个私钥对。没有它,您将无法使用 SSH 登录到服务。

点击“下一步”。

  1. 在打开的页面上,确认您的数据并点击“创建”按钮:

图片

数据确认

  1. 在服务创建过程中,您将返回主页面(注意“创建服务...”标签):

图片

新服务正在创建

  1. 一旦服务创建完成,点击“选项”按钮并点击“容器控制台”:

图片

访问容器控制台

您现在位于您刚刚创建的服务仪表板:

图片

容器云服务仪表板

  1. 点击“服务”(左侧)然后点击“新建服务”(右侧):

图片

服务页面

  1. 在弹出的窗口中,为服务命名(服务名称字段),并在“镜像”字段中填写预构建镜像的详细信息:

图片

服务创建字段

  1. 在“可用选项”字段中,勾选“端口”选项。它将在“环境变量”下的“端口”部分打开。点击“添加”按钮,并在弹出的窗口中填写表格,如下所示:

图片

端口转发

  1. 现在您的服务已出现在这个列表中。点击其“部署”按钮:

图片

服务列表

  1. 在弹出的窗口中,填写截图所示的表格,然后点击部署:

图片

服务部署弹窗

  1. 现在,只需稍等片刻,直到您的新服务启动并运行:

图片

进行中的部署

  1. 一旦部署完成,它将变为绿色,您将获得有关您创建的容器的信息。点击“容器名称”标签下的链接:

图片

部署完成

您现在将看到有关您的容器的详细信息:

图片

容器详情

  1. 点击标签页上标有“环境变量”的部分,找到名为 OCCS_HOSTIPS 的变量。在同一行中,有一个标有 public_ip 的 IP 地址。复制它:

图片

容器环境变量

使用它导航到http://[public_ip]:8080/app

图片

我们应用程序的测试页面

如果您能看到前面的图片,说明您已经成功了!现在您的容器正在使用 Oracle Cloud 进行云编排。

它是如何工作的...

这之所以如此简单,是因为您正在使用一个旨在使其简化的平台。因此,您在自己的基础设施中需要做的所有繁重工作都由平台完成。

更多...

您应该使用提供商在云中编排容器的理由不仅是因为创建服务的便捷性,而且因为平台将负责保持其正常运行。

因此,如果您的容器出现故障并需要停止、重启,甚至杀死并重新创建,平台将自动完成这些操作。

在云中使用 Jelastic 进行容器编排

在云中使用容器最好的方式是通过使用提供商。为什么?因为它们可以以较低的价格为您提供良好的基础设施和优质的服务。

本教程将向您展示如何使用 Jelastic 将本章第一篇教程中创建的容器交付。

准备工作

如果您没有 Jelastic 账户,您可以在jelastic.com/注册免费试用。

如何操作...

  1. 登录平台后,您将到达这个主页:

图片

Jelastic 主页

  1. 首先,点击右上角的设置按钮。它将打开账户设置部分(左下角):

图片

账户设置

  1. 点击 SSH 密钥链中的“公共”并上传您的公钥 SSH 密钥:

图片

SSH 公钥信息

  1. 确保您的 SSH 密钥已真正上传,否则您将无法使用 SSH 登录平台:

图片

SSH 确认

  1. 在页面顶部,点击市场按钮。转到其他部分并选择 Docker Engine CE。点击安装:

图片

市场弹出窗口

  1. 给这个环境起一个名字并点击安装:

图片

Docker Engine CE 配置弹出窗口

等待完成:

图片

安装状态

  1. 完成后,将弹出一个包含您需要用于登录平台的命令的窗口。复制它:

图片

安装确认和连接命令

  1. 在您的机器上打开终端并粘贴复制的命令:

图片

终端上的命令执行

在控制台窗口的输出末尾有这个命令:

docker-machine env [environment-name]

输出将如下所示:

图片

环境变量输出

  1. 现在,您只需运行命令即可创建容器:
docker run -d --name gf-javaee-cookbook \
 -h gf-javaee-cookbook \
 -p 80:8080 \
 -p 4848:4848 \
 -p 8686:8686 \
 -p 8009:8009 \
 -p 8181:8181 \
 eldermoraes/gf-javaee-cookbook

检查输出:

图片

容器日志输出

这与您在自己的本地机器上运行几乎一样,但实际上您是在 Jelastic 平台上运行。

现在,如果您回到主页,您将看到您的环境正在运行:

图片

创建节点的主页

在 Docker Engine CE 标签下有您环境的 URL。只需点击它并在末尾添加/app

图片

我们应用程序的测试页面

如果您能看到这个页面,恭喜!您的应用程序已部署到 Jelastic。

它是如何工作的...

这之所以如此简单,是因为您正在使用一个旨在使其简化的平台。因此,您在自己的基础设施中需要做的所有繁重工作都由平台完成。

更多...

您应该使用提供商在云中编排容器的原因不仅在于创建服务的便捷性,还因为平台将负责保持其正常运行。

因此,如果您的容器出现故障并需要停止、重启,甚至杀死并重新创建,平台将自动完成这些操作。

在云中使用 OpenShift 进行容器编排

在云中使用容器最好的方式是通过使用提供商。为什么?因为它们可以以较小的价格为您提供良好的基础设施和优质的服务。

此配方将向您展示如何使用 OpenShift 将本章第一个配方中创建的容器交付。

准备工作

如果您没有 OpenShift 账户,可以免费试用。访问www.openshift.com/并点击免费注册。

如何操作...

  1. 登录平台后,您将看到此主页:

图片

OpenShift 主页

  1. 点击创建项目按钮并填写空白。点击创建:

图片

填写新项目字段

  1. 一旦您的项目创建完成,点击它:

图片

新项目的访问权限

  1. 在打开的页面中,点击右上角的添加到项目,然后部署镜像:

图片

项目主页

  1. 在弹出窗口中选择镜像名称,填写表单并使用我们预构建的镜像(eldermoraes/gf-javaee-cookbook),然后点击搜索图标。

您将看到如下警告:

图片

图像部署弹出窗口

让我为您节省时间:不要部署它,因为它不会工作。OpenShift 平台要求您的容器应以除root以外的用户运行。因此,我们需要为它构建另一个镜像。

幸运的是,这相当简单。新的 Dockerfile 如下所示:

FROM eldermoraes/gf-javaee-jdk8

ENV DEPLOYMENT_DIR ${GLASSFISH_HOME}/glassfish/domains/domain1/autodeploy/

COPY app.war ${DEPLOYMENT_DIR}

USER root

RUN chown -R glassfish:glassfish_grp ${DEPLOYMENT_DIR}/app.war \
    && chmod -R 777 ${DEPLOYMENT_DIR}/app.war

USER glassfish
  1. 然后根据此 Dockerfile 构建一个新的镜像:
docker build -t eldermoraes/gf-javaee-cookbook-os .
  1. 然后将这个新镜像推送到 Docker Hub:
docker push eldermoraes/gf-javaee-cookbook-os

现在您可以开始了:

图片

图像部署弹出窗口

  1. 没有警告,所以请继续点击部署。在打开的页面中,点击继续到项目概览标签:

图片

图像部署确认

  1. 观察以下页面,直到 Pod 图标变为蓝色。准备好后,点击创建路由链接:

图片

监控 Pod 创建

  1. 在弹出窗口中,填写路径字段为/app,在目标端口选择 8080 -> 8080(TCP):

图片

路由创建

  1. 点击创建并等待:

图片

路由确认

  1. 完成后,点击左上角的概览菜单。在应用程序名称所在的同一行,有一个指向您的容器的 URL:

图片

我们应用程序的测试页面

如果您能看到页面,恭喜!您的应用程序现在已在 OpenShift 上编排。

它是如何工作的...

这之所以如此简单,是因为您正在使用一个旨在使其简化的平台。因此,您在自己的基础设施中需要做的所有繁重工作都由平台完成。

我们为使应用程序在 OpenShift 上运行所做的更改非常简单:

USER root

RUN chown -R glassfish:glassfish_grp ${DEPLOYMENT_DIR}/app.war \
    && chmod -R 777 ${DEPLOYMENT_DIR}/app.war

USER glassfish

首先,我们使用root用户更改app.war的权限。然后主要目的是指定使用glassfish用户。这个特性告诉 Docker 内部进程将由glassfish用户拥有,而不是root

更多...

您应该在云中使用提供者来编排容器的原因不仅是因为创建服务的简便性,还因为平台将负责保持其运行。

因此,如果您的容器出现错误需要停止、重启,甚至需要杀死并重新创建,平台将自动完成这些操作。

相关内容

在云中使用 AWS 进行容器编排

在云中使用容器的最佳方式是通过使用提供者。为什么?因为它们可以以较低的价格为您提供良好的基础设施和优质的服务。

本食谱将向您展示如何获取本章第一个食谱中创建的容器,并使用亚马逊网络服务AWS)交付它。

准备工作

如果您没有 AWS 账户,请在aws.amazon.com/free/start-your-free-trial/注册免费试用。

如何操作...

  1. 一旦您登录到平台,您将到达这个主页面:

图片

AWS 主页

  1. 点击左上角的“服务”菜单,然后选择“弹性容器服务”(在“计算”菜单下):

图片

服务列表

  1. 在打开的页面上,点击“开始”:

图片

ECS 的入门页面

  1. 仅选择“在 Amazon ECS 集群上部署示例应用程序”选项。然后点击“继续”:

图片

创建 ECS 的第一页

  1. 按照以下方式填写空白,特别关注“镜像”字段,您将使用我们的预构建镜像:

图片

任务定义页

  1. 滚动页面并设置端口号映射,如图所示。点击“继续”:

图片

端口号映射

  1. 给服务起一个名字,并将期望的任务数量设置为1。点击“下一步”:

图片

服务和网络配置

  1. 按照以下方式配置集群:

图片

集群配置

  1. 滚动到页面并点击启动实例 & 运行服务:

图片

启动实例

  1. 你可以在以下页面上跟踪进程的状态。完成后,点击查看服务按钮:

图片

启动状态

  1. 你将在以下页面上看到你的服务详情。点击默认 > 标签:

图片

集群信息

在打开的页面上,你可以看到更多关于集群的详细信息:

图片

集群详情

  1. 点击任务标签以查看关于任务和创建的容器的信息:

图片

容器任务

  1. 点击容器实例标签以查看已创建容器的详细信息:

图片

容器详情

  1. 检查公共 IP 标签并复制 IP。它自动映射到8080端口。使用http://[public-ip]/app来尝试:

图片

我们应用程序的测试页面

如果你看到了相同的截图,那就太好了!你现在正在 AWS 中编排你的容器。

它是如何工作的...

它之所以如此简单,是因为你正在使用一个旨在使其简化的平台。因此,你原本需要在自己的基础设施中完成的繁重工作,现在都由平台来完成。

还有更多...

你应该使用提供商在云中编排你的容器的原因,不仅是因为创建服务的便捷性,还因为平台将负责保持其运行。

因此,如果你的容器出现错误并需要停止、重启,甚至杀死并重新创建,平台将自动完成这些操作。

附录:分享知识的力量

本附录涵盖了以下主题:

  • 为什么参与“采用 JSR”计划可以使你成为一个更好的专业人士

  • 解开你的项目、你的职业...甚至你的人生之谜!

引言

等等...职业、知识分享、社区...在烹饪书中?

好吧,我真的很应该感谢我的编辑们,他们屈服于我的魅力(和坚持),并允许我把这一章放入这本书中。

我强调这一点的原因如下:

  • 我确信这个内容对你和你的职业生涯来说非常重要,甚至可以改变你的生活。

  • 我不知道我是否或何时会再写一本书,所以我现在想抓住这个机会

我认为这个内容与本书的其他内容一样重要。实际上,如果你将它的原则应用到你的职业生涯中,你可能会成为下一个写书的人。

为什么参与“采用 JSR”计划可以使你成为一个更好的专业人士

你是否听说过“帮助他人就是帮助自己”这句话?是的?本节就是关于这个话题的。相信我,我确实写了这一点。

也许你从未听说过“采用 JSR”计划,或者你可能听说过它,但不知道它是什么。或者你可能知道它,但不知道它与你的职业生涯有什么关系。

让我在接下来的几页中占用你的时间,并享受这个过程。

理解“采用 JSR”计划

首先,什么是Adopt a JSR计划?

这是一个旨在在 Java 演变过程中使社区更加紧密团结的倡议。当我们提到社区时,我们指的是Java 用户组JUGs)、个人以及任何其他类型的组织。

为了理解它,也许我们应该稍作停留,理解 Java 的演变过程。

Java 技术是一套称为JSR的标准,它是Java 规范请求的缩写。每个 API 和语言的任何方面都必须以某种 JSR 编写。

每个 JSR 都有一个规范领导者,负责领导该规范构建过程的个人。每个规范领导者都与一个称为EG专家小组的团队一起工作,该团队与规范领导者合作,并承担创建和/或演变 JSR 的所有繁重工作。

在每个 JSR 中,也有贡献者,他们是来自社区的志愿者,愿意与 JSR 合作。他们虽然不像规范领导者或 EG 那样有相同的角色,但也可以为这个过程做出很多贡献。

对于每个 JSR,都有一个参考实现RI)。RI 是作为真实 Java 代码运行的 JSR 的真实代码。它的存在是为了将规范中的所有概念性线条带入现实世界。它对于证明曾经指定的内容确实可行至关重要。

RIs 的例子包括 JSF 的 Mojarra、安全的 Soteria 和 Java EE 的 GlassFish。

当我们说 RI 是某些 JSR 真正起作用的证明时,这不仅仅是某种概念性的东西。它是真正经过测试的。这就是为什么我们有TCKs,或者称为技术兼容性套件

TCKs 是一组旨在测试 JSR 中指定的实现的测试。因此,当编写 RI 时,它应该在 TCKs 中通过,以证明它确实在正常工作(至少在理论上)。

这三个部分——JSR、RI 和 TCK——是经过 Java Community ProcessJCP 认可的组成部分。

所以在 JCP 中,你拥有所有正在工作的 JSR 和它们自己的由 执行委员会EC)监控的过程,该委员会由公司、个人和 JUGs 组成,保证所有 JSR 都在 JCP 定义的最佳实践中进行,并朝着为 Java 生态系统及其依赖的社区带来最佳结果的方向发展。

所以下次当你思考 "我如何为 Java 做出贡献?""我如何使我的贡献有所作为?" 或者,更具体地,"我如何采用 JSR?" 时,要知道你可以通过以下方式做到:

  • 作为贡献者加入一些 JSR。大多数规范领导者都乐于并愿意接受帮助,以推动 JSR 的发展。

  • 帮助编写规范或至少提供有用的建议。

  • 为 TCK 编写测试或帮助解决在 TCK 测试中发现的任何问题。

  • 为 RI 编码。

所有这些话题你都可以自己完成,但如果你与社区一起做,它们会更有成效(并且更有趣!)你可以通过加入一个 JUG,或者在你公司中成立一个小团队,或者在任何你能找到一些人一起工作的地方。这是一项大量工作,所以最好有人陪伴!

关于采用 JSR 计划的更多信息,你可以查看以下链接:

要开始工作,请访问:jcp.org.

协同推进 Java EE 的未来

所以如果你正在阅读这本书,我相信你对 Java EE 感兴趣。如果你读到这一行,我希望我开始让你相信你可以帮助 Java EE 前进,而且它可以帮助你的职业生涯。

是的,你绝对可以帮助 Java EE 前进,从我自己的经验来看,我可以向你保证你应该立即开始!但是,在流程方面,Java EE 的情况略有不同。

几个月前,Oracle 决定将 Java EE 转移到 Eclipse 基金会。所以当我写下这些话的时候,转移过程正在进行中!

顺便提一下:这仅适用于 Java EE,不适用于 Java!其他 Java 规范将继续在 JCP 下进行(至少目前是这样)。

在协作方面,这对你有什么改变?没有。仍然有小组、规格、测试、RIs 等等。唯一改变的是名称,因为现在这个过程由 Eclipse 基金会拥有。

因此,Java EE 被转移到 Eclipse,成为Eclipse Enterprise for Java,或EE4J。它是包含其下所有其他项目的项目伞。这些其他项目是以前的 JSR。关于 EE4J 的更多详细信息请参阅projects.eclipse.org/projects/ee4j/charter

转移过程和 EE4J 本身在这里有很多解答问题:

www.eclipse.org/ee4j/faq.php

在 Eclipse 中,项目由项目管理委员会PMC)领导,就像 JCP 中的 EC。关于 PMC 的更多详细信息请参阅projects.eclipse.org/projects/ee4j/pmc

这里最重要的是,你可以,我相信你也应该,为 Java EE 的未来做出贡献。也许你认为你没有所需的条件。是的,你有!每个建议都很有价值,每个好主意,每一行有效的代码,每个通过测试。试一试,看看结果!

为协作做好准备

为了在 Java EE 的未来上协作,你需要做一些事情。

为其预留特定时间

如果你只是在你有时间的时候做,你可能永远也不会做!所以请安排时间。定义你每周愿意投入的时间(每天一小时,每周三小时等)。写下它并预约。

选择你将集中精力的地方

向数十个规格领导者发送邮件,请求他们加入小组并协作是徒劳的。我知道,我之前已经这样做过了。

相反,退一步,思考你真正感兴趣的是什么,然后选择一个单一的规格开始。加入邮件列表,在 GitHub 上找到它的仓库,并开始关注它。

在任何开源项目上开始协作的一个好方法是文档。它很重要,但通常参与编写规格和编码的人没有足够的时间深入研究文档。所以,当有人愿意做这件事时,他们通常会很高兴。

我知道今天以这种方式开始协作的许多人已经致力于一些最大的开源项目。

做吧!

任何计划只有在付诸行动时才有意义。所以停止拖延,开始工作!不要等到周一,或者假期之后,或者大学毕业后,或者当你得到更好的工作,或者 whatever。

请记住,你可能永远不会觉得自己准备好了。所以停止等待,开始行动,即使你不想这么做。如果你做了艰苦的工作,结果就会到来,请放心!

解锁你的项目、你的职业生涯...甚至你的人生!的秘诀

你在职业生涯中感到停滞不前吗?我也曾有过这种感觉。让我给你讲一个故事和一个秘密,它让我的职业生涯爆发。

这一年是 2002 年。我在圣保罗的美国商会参加 Sun Tech Days。场地爆满,我有点迷失方向。

可能“迷失”这个词并不能很好地描述它。感觉“格格不入”要好得多。毕竟,我只是中间巨人的技术新手。

我看到了一些熟悉的面孔。布鲁诺·苏萨,法比奥·韦洛索。我应该自我介绍吗?

当然不是...我是谁呢?别打扰那些人,他们在这个会议中太忙了。

我看了看节目单,看到主会议室有一个主题演讲。看起来是一个叫詹姆斯·戈斯林的重要人物。我不知道他是谁,但我还是去了那里。

我是第一个。当然,我是一个新手!当我在门口独自一人时,每个人都正在交谈。他们在谈论什么?肯定是一些我无法理解的超级技术讨论。最好在这里等着。

在他们开门前五分钟,后面还有 200 个人排在我后面。嘿,看起来我是个幸运的新手,对吧?!

我走进去,在第二排坐下,等待主题演讲开始。

天哪,詹姆斯·戈斯林是 Java 的创造者!我是个多么愚蠢的新手啊...

他的演讲太棒了!你知道,他不是世界上最好的演讲者,但他的演讲中有一些事情让房间里每个人都感到惊讶。也许是因为他的热情,他的知识,甚至是他正在工作的超级酷炫项目:一个遥控火星探测车的操作系统。真棒!

会议已经进行到第二天,我感到非常困扰:Java 这个玩意儿有如此多的可能性。我自己尝试过一点,但看到那些 Sun 传教士谈论真实而酷炫的项目,让我在脑海中打开了整个新的可能性世界。我的职业生涯需要朝那个方向发展。

经过几天不在办公室,我又回来了,忍不住想告诉每个人我那几天看到的事情。你知道,我们大多数人都在这里用 Visual Basic 和 Delphi 工作...但 Java 给我们项目带来了新的可能性。

自从参加了那些 Sun Tech Days 以来,已经过去了六个月,我现在正在进行我的第一个 Java 项目。是的!公司外包了一个项目,要求我和我们的合作伙伴一起工作。

多么糟糕的想法!我们合作伙伴的首席开发者对 Java 的了解和我一样多...好吧,让我们这么做。至少我有机会参与一个真正的 Java 项目。

2004 年,我即将参加有史以来第一次的大型会议。我必须承认,我感到非常害怕。但实际上,我是在和一个新朋友,毛里西奥·利尔一起参加一个讨论。他是巴西最顶尖的 Java 影响者之一,并同意和我一起在 Just Java 会议上发表演讲。或者是我同意了他的提议?现在来说这些已经不重要了...

对于我来说,去那里非常困难,因为我的母亲在几个月前开始与癌症作斗争。我不仅非常关心她,而且我没有足够的时间为会议做好准备。然而,她自己鼓励我来到这里,并说她为她孩子在大事件上发言而感到自豪。谢谢,妈妈!

我们进行了演讲,效果很好!我有许多东西可以向 Mauricio 和他的所有 Java 朋友学习。实际上,我需要继续参与这个社区活动:活动、开源、演讲等等。

现在是 2005 年,我决定加入一个我过去三年一直在工作的同公司的重大项目。不,这不是一个 Java 项目,但它如此庞大,我无法错过这个机会。这对我的职业生涯有好处,我作为项目经理将有一些机会。

我们现在是 2006 年 6 月,我的母亲刚刚在与癌症的斗争中失败。我崩溃了。我从未想过在我整个一生中,我会失去她,当时她只有 58 岁,而我 26 岁...谁在乎事业?谁在乎工作?谁在乎任何东西?

年份是 2015 年。月份是十二月。我正在开车。我的妻子在我身边,我的小女儿坐在后座。我在告诉我妻子我对我的职业生涯非常关心。

我不再是孩子了。我 36 岁,有一份好工作,从中得到不错的收入,但……我陷入了困境。从什么时候开始……2004 年?我知道,加入那个项目是一个大错误,尽管它很大。我们都失败了。

我告诉她:"你知道,我必须做点什么……"。

经过几晚的失眠,一些互联网上的研究,和一些参考书的阅读,我认为我为一个多年没有做什么的人列出了一个很好的清单:

  • 为发表撰写一篇技术文章

  • 在某个小型活动中进行技术演讲

  • 获得 Java EE 架构师认证

我决定一直跟着 Java 走到底。我对它了解很多。我已经学习它并使用它很多年了。我必须专注于它,我一定能做到。

突然,在这场大混乱和许多怀疑的中间,我成功了!现在我是公司的合伙人。

嗯,也许我确实做了一些正确的事情,对吧?!这么多年来的辛勤工作和学习终于得到了回报。

但……我在想什么?我讨厌销售,我讨厌与客户打交道,我讨厌谈判,我讨厌穿西装,我讨厌追逐金钱。我讨厌这个合伙人角色!

拥有自己的企业一直是一个梦想,但我的生活现在更像是一场噩梦。这个错误的决定让一切都变得支离破碎。情况糟糕到我现在需要药物来治疗抑郁症。

我心中的这些毒素让我想:"我到底在用我的生活做什么?"这不是我想走的路。我的意思是...是的,公司很棒,他们做得很好,但不是对我有好处的方式。

我真的需要改变。我需要做出改变。如果不这样做,我的家庭未来会怎样?当我老了退休时,我能提供什么样的支持给我的妻子和女儿?

这只是又一个糟糕的一天,然后我收到了一封来自...布鲁诺·苏萨?巴西的 JavaMan?这家伙是怎么得到我的电子邮件的?哦,是的...我订阅了一些邮件列表。

他在谈论梦想,说他的一个朋友今年会帮助他实现职业梦想,所以他决定帮助别人。他说:“告诉我你 2016 年的职业梦想,我会尽力帮助你。”

好吧,我敢肯定这个人甚至不会读我的电子邮件,但让我还是回复它。至少写下我今年的梦想会帮助我可视化它们。我会用几周前告诉妻子的那个清单。

什么?只花了半小时他就回复了?

好吧...他说他可以通过这种方式帮助我:

  • 文章:他能否帮我找到一个好的主题并在 Oracle 技术网络上发表?这真的严肃吗?我只是在想写一篇博客或者类似的东西。

  • 演讲:一旦我们有了文章,他可以帮助我把它变成演讲。好吧,听起来很有趣。

  • 认证:他不会帮我任何忙。我应该坐下来学习。是的,有道理。

在与布鲁诺的所有对话中,有一件事始终是首要的:分享。分享知识,分享你所知道的东西,分享以帮助他人,分享,分享,分享。看起来这个人真的很想帮助别人。

所以我设法离开了公司(以及合伙关系),最终得到了我真正想要的位置:系统架构师!

就这样,我喜欢架构,我喜欢在从头开始规划应用程序或对一些遗留应用程序进行扩展/重构时处理所有这些权衡。

就这样,现在我找到了我的位置!

别急,伙计。别急,大约一个月后,公司更换了 CEO,这个人就决定 Java 会立即在这里死去。现在的重点是.NET!好吧,让我们试试。

同时,我和布鲁诺在 OTN 上发表了我们的第一篇文章,几天内就获得了数千次浏览。太棒了!

这篇文章成为了一个关于同一主题的提案,提交给了开发者大会(拉丁美洲最大的开发者活动)和 JavaOne 拉丁美洲。两个提案都被接受了,我有机会在这些活动中与布鲁诺向数百人演讲。

在提交给旧金山 JavaOne 的前一天,我决定放弃它。我没有足够的钱去负担它。布鲁诺差点踢了我的屁股,说:“来吧!提交它!如果被批准了,你想办法弄到钱。”

演讲被批准了,克里斯蒂娜·斎藤,一个前老板(也是合伙人!)送了我一份礼物:JavaOne 的机票。她说她为我感到骄傲。我可能永远无法对她足够的善良和慷慨表示感谢,我希望在这里的提及能多少表达我的感激之情。

这很难相信。自从我打开布鲁诺的邮件,自从我开始服用抗抑郁药物,自从我职业生涯中最黑暗的时刻,已经过去了 10 个月,我现在在加利福尼亚州的旧金山。几分钟之后,我将与布鲁诺在 JavaOne 上做演讲,这是世界上最大的 Java 活动。一个电影突然闪过我的脑海。而现在我就在这里。

演讲很棒!有些事情出了差错,但我们还是做到了!看来这种分享的东西真的很有效。我感到非常自信,迫不及待地想回到巴西,回到工作中。完成事情。攀登我自己的成功之山。

所以我来到了巴西,去了办公室,然后... 被解雇了?真的吗?我以为所有这些分享的事情会帮助我,而不是让我失去工作...有人骗了我!

好的,好的... 让我们深呼吸一下... 你知道,我现在更有信心了。不,我之前并没有为像现在这样在职业生涯中取得最佳成就后失业这样的情况做好准备,但... 我想我会找到解决办法的。

不用多久,我就得到了 Summa Technologies 的一个职位。是的,分享是有效的:我甚至不需要发送简历。他们听说了我(因为分享)所以我现在在这里,做着我在谈论和写作的事情。

这家公司很棒,团队技术高超,项目具有挑战性。但是,你知道……六个月后,事情看起来又陷入了僵局……结果只是还可以,项目也只是还可以,这里没有太多值得学习或做的事情。

我们现在在 2017 年 5 月。几个月后,Java EE 8 将发布。如果我们采访来自世界各地的顶级 Java EE 影响者,并分享他们关于它的所有信息、期望和新闻,会怎么样呢?听起来不错。让我们称之为 Java EE 8 - 新的边疆。

布鲁诺有足够的技巧来说服我去做这个 Java EE 8 的事情,而且 SouJava 会提供所有需要的支持。实际上,这从一开始就是 SouJava 的倡议。

但是,看在上帝的份上,为什么所有那些 Java EE 专家会给我做采访?我是谁?

我与 SouJava 合作进行 Java EE 项目才仅仅三个月。我们已经采访了 15 位顶级 Java EE 影响者。来自近 70 个国家的数千名开发者观看了这些采访。我们的 YouTube 播放列表在官方 Java 频道上被推荐。所有内容每月都有数千次观看。

我必须说实话:我从未想过 Java EE 社区会对这个倡议如此开放。我的意思是,他们好像在期待这样的内容。他们愿意消费它。

我一直缺少的是什么?专注!任何没有专注的事情几乎都是无用的。它可能有所帮助,但不会有连续性。

这些项目让我写下了你现在正在阅读的这本书。在我与 Packt 的一次对话中,我询问他们是如何找到我的。他们说,“嗯,你分享了大量的 Java EE 8 内容...这正是我们所需要的”。

就在我与 Packt 签约后的几天,我接到了一个电话……来自甲骨文?就是那个“那个”甲骨文?

所以我就在这里,写着这些文字,在世界最大的公司之一工作,做的就是我在本章告诉你的事情:分享知识。

我坚持告诉你:如果分享改变了我的职业生涯,它也可以改变你的。不要认为你没有这个能力:你有!我可以向你保证,你了解许多其他人愿意学习的东西。

你为什么不找个好方法去帮助他们呢?我能给你一些建议,告诉你如何根据你刚才读到的内容去帮助他人吗?这里就是:

  • 你可以根据这本书中学到的东西写一小段代码。在 Twitter 或某些博客文章中分享它。

  • 录制一个视频,解释你在阅读这本书时的一些见解。分享它!

  • 如果你现在不想暴露自己,就给我发邮件告诉我这本书教会了你什么。我会很乐意阅读它!请写信到elder@eldermoraes.com

分享是一种习惯。练习它!

posted @ 2025-09-10 15:08  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报