Hibernate6-入门手册-五-

141 阅读41分钟

Hibernate6 入门手册(五)

原文:Beginning Hibernate 6

协议:CC BY-NC-SA 4.0

十、过滤搜索结果

您的应用通常只需要处理数据库表中数据的子集。在这些情况下,您可以创建一个 Hibernate 过滤器来使查询忽略不需要的数据。筛选器为您的应用提供了一种将查询结果限制为符合筛选器条件的数据的方法。过滤器并不是一个新概念——使用 SQL 数据库视图或者命名查询也可以达到同样的效果——但是 Hibernate 为它们提供了一个集中的管理系统。

与数据库视图不同, 1 休眠过滤器可以在休眠会话期间启用或禁用。此外,Hibernate 过滤器是参数化的,这在您基于 Hibernate 构建使用安全角色或个性化的应用时特别有用。22

当您有许多带有可归纳选择子句的类似查询时,过滤器特别有用。过滤器允许您使用通用查询,根据需要添加查询条件。

何时使用过滤器

例如,考虑一个使用用户和组的应用。用户具有指示该用户是活动的还是非活动的状态以及一组成员资格;如果您的应用需要根据状态和/或组成员来管理用户,您会看到四个单独的查询(或者通配符查询,这看起来很傻):一个用于所有的状态和组,一个用于状态的子集,一个用于组的子集,一个用于状态和组的子集。通配符查询确实可以工作,但是它会给数据库增加不应该有的负担,特别是如果一组通配符非常常见的话。

如果我们要使用四个不同的查询(“所有用户”、“所有状态为这个的用户”、“所有状态为那个组的用户”和“所有状态为这个的用户和所有状态为那个组的用户”),我们不仅要测试和维护四个查询,还要有一种方法来跟踪我们在任何给定时间应该使用哪个查询。

我们还可以为每次执行使用定制查询(根据需要构建查询,而不是存储一组查询)。这是可行的,但并不完全有效,而且查询数据会污染您的服务。

过滤器允许我们定义限制集。我们可以创建一个过滤器,并在查询数据库时应用它,而不是自定义查询或类似的查询集,这样,即使数据集发生变化,我们的实际查询也不会发生变化。

使用 Hibernate 过滤器的优点是,您可以在应用代码中以编程方式打开或关闭过滤器,并且您的过滤器被定义在一致的位置,以便于维护。过滤器的主要缺点是您不能在运行时创建新的过滤器。相反,应用需要的任何过滤器都需要在适当的 Hibernate 注释或映射文档中指定。虽然这听起来有点限制,但过滤器可以参数化的事实使它们非常灵活。对于我们的用户状态过滤器示例,只需要在映射文档中定义一个过滤器(尽管分为两部分)。该筛选器将指定状态列必须与命名参数匹配。您不需要在 Hibernate 注释或映射文档中定义 status 列的可能值——应用可以在运行时指定这些参数。

尽管用 Hibernate 编写不使用过滤器的应用是完全可能的,但我们发现它们是解决某些类型问题的优秀解决方案——特别是安全性和个性化。

入门指南

在我们走得更远之前,我们应该看一看项目模型。这一次,它非常简单,因为过滤器是 Hibernate 本身的一部分,除了util项目将引入的内容之外,我们没有外部依赖性。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hibernate-6-parent</artifactId>
        <groupId>com.autumncode.books.hibernate</groupId>
        <version>5.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>chapter10</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.autumncode.books.hibernate</groupId>
            <artifactId>util</artifactId>
            <version>${project.parent.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>
</project>

Listing 10-1chapter10/pom.xml

定义和附加过滤器

第一步是创建过滤器定义。过滤器定义类似于过滤器的元数据,包括参数定义;它不是过滤器本身,但它可以作为一个起点。 3 你有了过滤器定义后,你自己创建过滤器;这包含实际的滤波器规格。使用过滤器很简单,只需按名称启用过滤器并填充参数(如果有的话)。

将过滤器定义与过滤器本身分开的原因是什么?这是因为过滤器定义通常是为特定的数据库编写的,因此它们趋向于不可移植。如果过滤器和它们的定义是统一的,那么保持它们的可移植性就更加困难了;有了单独的定义,很容易将过滤器放入单独包含的资源中。

让我们来看看一些过滤器,让他们的用法更清楚。

带注释的过滤器

要使用带注释的过滤器,您需要使用@FilterDef@ParamDef@Filter注释。@FilterDef注释定义了过滤器,属于类或包。要定义一个类的过滤器,在@Entity注释旁边添加一个@FilterDef注释。

在您定义了过滤器之后,您可以使用@Filter注释将它们附加到类或集合上。@Filter注释有两个参数:名称和条件。该名称引用了我们之前在注释中描述过的过滤器定义。条件参数是一个 HQL WHERE子句。条件中的参数用冒号表示,类似于 HQL 中的命名参数。必须在过滤器定义中定义参数。以下是过滤器注释的框架示例:

@Entity
@FilterDef(name = "byStatus", parameters = @ParamDef(name = "status", type = "boolean"))
@Filter(name = "byStatus", condition = "status = :status")
public class User {
   // other fields removed for brevity's sake
   boolean status;
}

在每个类上定义过滤器很简单,但是如果您为多个实体使用一个给定的过滤器,您将会有很多重复。例如,byStatus过滤器可能适用于除了User实体之外的事物。要在包级别定义任何注释,您需要在包中创建一个名为package-info.java的 Java 源文件。package-info.java应该只包含包级注释,然后立即声明包。它并不意味着是一个 Java 类。在配置 Hibernate 时,您还需要告诉 Hibernate 映射包,要么通过AnnotationConfiguration上的addPackage()方法,要么在您的 Hibernate 配置 XML 中:

SessionFactory factory = new MetadataSources(registry)
      .addPackage("com.autumncode.entities")
      .buildMetadata()
      .buildSessionFactory();

在 XML 中,您的映射可能如下所示:

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!--  Database connection settings  -->
    <property name="connection.driver_class">org.h2.Driver</property>
    <property name="connection.url">jdbc:h2:file:./db10</property>
    <property name="connection.username">sa</property>
    <property name="connection.password"/>
    <property name="dialect">org.hibernate.dialect.H2Dialect</property>

    <mapping class="chapter10.model.User"/>
    <mapping package="chapter10.model" />
  </session-factory>
</hibernate-configuration>

使用 XML 映射文档的过滤器

对于 XML 映射文档,在一个.hbm.xml文件中使用<filter-def> XML 元素。这些过滤器定义必须包含过滤器的名称以及任何过滤器参数的名称和类型。使用<filter-param> XML 元素指定过滤器参数。下面是一个节选自一个定义了名为latePaymentFilter的过滤器的映射文档,在一个映射一个Account实体的Account.hbm.xml文件中:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping
   PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
   "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="Account" table="ACCOUNTS">
    <id name="id" type="int" column="id">
      <generator class="native" />
    </id>
    <property name="dueDate" column="dueDate" type="date" />
    <property name="dueAmount" column="dueAmount" type="double" />
  </class>
  <filter-def name="latePaymentFilter">
    <filter-param name="dueDate" type="date"/>
  </filter-def>
</hibernate-mapping>

一旦创建了过滤器定义,就需要将过滤器附加到一个类或映射元素的集合上。您可以将单个筛选器附加到多个类或集合。为此,您向每个类和/或集合添加一个<filter> XML 元素。XML 元素有两个属性:名称和条件。该名称引用了过滤器定义(例如,latePaymentFilter)。该条件表示 HQL 中的 WHERE 子句。这里有一个例子:

<class ...
  <filter name="latePaymentFilter" condition="paymentDate = :dueDate"/>
</class>

每个<filter> XML 元素必须对应一个<filter-def>元素。

对于大多数应用,更喜欢使用注释而不是 XML。

在应用中使用过滤器

您的应用以编程方式决定为给定的 Hibernate 会话激活或停用哪些过滤器。每个Session可以有一组不同参数值的不同过滤器。默认情况下,会话没有任何活动的筛选器,您必须以编程方式为每个会话显式启用筛选器。会话界面包含几种使用过滤器的方法,如下所示:

public Filter enableFilter(String filterName)
public Filter getEnabledFilter(String filterName)
public void disableFilter(String filterName)

这些是不言自明的——enableFilter(String filterName)方法激活指定的过滤器;disableFilter(String filterName)方法停用过滤器;如果您已经激活了一个已命名的过滤器,getEnabledFilter(String filterName)将检索该过滤器(如果该过滤器未启用,则返回null)。

org.hibernate.Filter接口有六个方法。你不太可能用validate();Hibernate 在处理过滤器时使用这种方法。其他五种方法如下:

public Filter setParameter(String name, Object value)
public Filter setParameterList(String name, Collection values)
public Filter setParameterList(String name, Object[] values)
public String getName()
public FilterDefinition getFilterDefinition()

setParameter()方法是最有用的。您可以用任何 Java 对象替换该参数,尽管其类型应该与您在定义过滤器时为该参数指定的类型相匹配。两个setParameterList()方法对于在过滤器中使用 IN 子句很有用。如果您想使用BETWEEN子句,请使用两个不同名称的过滤器参数。最后,getFilterDefinition()方法允许您检索代表过滤器元数据的FilterDefinition对象(其名称、参数名称和参数类型)。

一旦您在会话上启用了特定的过滤器,您就不必对您的应用做任何其他事情来利用过滤器,正如我们在下面的示例中演示的那样。

一个基本的过滤例子

因为过滤器非常简单,所以一个基本示例允许我们演示大多数过滤器功能,包括激活过滤器和在映射文档中定义过滤器。

我们将创建一个User实体,具有活动状态和组成员资格。我们将定义三个过滤器:一个非常简单的不带参数的过滤器(只是为了演示如何使用),然后是两个参数化的过滤器,我们将在各种组合中应用它们。

我们将定义两个过滤器,以涵盖我们在本章前面提到的四个查询,因为过滤器是附加的——如果我们需要按状态过滤,我们启用状态过滤器,如果我们需要按组过滤,我们可以单独启用过滤器*,两个过滤器协同工作。*

*我们将坚持使用注释配置,因为我们使用单个数据库(H2),这极大地简化了我们的示例。我们还将恢复使用 Lombok,因为这将通过消除大量样板方法来缩短我们的示例代码。这里是User.java

package chapter10.model;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.*;

import javax.persistence.*;
import javax.persistence.Entity;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

@Entity
@Data
@NoArgsConstructor
@FilterDefs({
  @FilterDef(
    name = "byStatus",
    parameters = @ParamDef(name = "status", type = "boolean")),
  @FilterDef(
    name = "byGroup",
    parameters = @ParamDef(name = "group", type = "string")),
  @FilterDef(
    name = "userEndsWith1")
})
@Filters({
  @Filter(name = "byStatus", condition = "active = :status"),
  @Filter(name = "byGroup",
    condition =
      ":group in (select ug.groups from user_groups ug where ug.user_id = id)"),
  @Filter(name = "userEndsWith1", condition = "name like '%1'")
})
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @Column(unique = true)
  String name;
  boolean active;
  @ElementCollection
  Set<String> groups;

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

  public void addGroups(String... groupSet) {
    if (getGroups() == null) {
      setGroups(new HashSet<>());
    }
    getGroups().addAll(Arrays.asList(groupSet));
  }
}

Listing 10-2chapter10/src/main/java/chapter10/model/User.java

关于这一点有几件事很突出,特别是关于群体。

首先,组被定义为一个Set,用@ElementCollection标注。这将创建一个表USER_GROUPS,它将包含一个用户 ID 和一个单独的列,在一个以集合命名的列中有不同的组名(因此是“组”而不是“组”)。

如果我们忽略 Hibernate 将为我们创建的外键,表示这种结构的 SQL 可能如下所示:

create table User (
  id integer not null,
  active boolean not null,
  name varchar(255),
  primary key (id)
  );
create table User_groups (
  User_id integer not null,
  groups varchar(255)
  );

然后,按组选择的筛选器使用子选择来限制返回的用户。这种情况是特定于数据库的,并且使用实际表结构的知识;过滤器会做一些内省,但做得还不够。准备做一些分析,以准确地计算出应该是什么样的过滤条件。

我们还将从我们的util模块中修改SessionUtil类,添加两个方法:doWithSession()returnFromSession()。这些方法将使我们有机会避免一些管理事务和会话的样板文件。

这是来自util模块的 SessionUtil.java

//tag::preamble[]
package com.autumncode.hibernate.util;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.function.Consumer;
import java.util.function.Function;

public class SessionUtil {
  private static final SessionUtil instance = new SessionUtil();
  private static final String CONFIG_NAME = "/configuration.properties";
  private SessionFactory factory;
  private Logger logger = LoggerFactory.getLogger(this.getClass());

  private SessionUtil() {

    initialize();
  }

  public static Session getSession() {
    return getInstance().factory.openSession();
  }

  public static void forceReload() {
    getInstance().initialize();
  }

  private static SessionUtil getInstance() {
    return instance;
  }

  private void initialize() {
    logger.info("reloading factory");
    StandardServiceRegistry registry =
      new StandardServiceRegistryBuilder()
        .configure()
        .build();
    factory = new MetadataSources(registry)
      .buildMetadata()
      .buildSessionFactory();
  }
  //end::preamble[]

  public static void doWithSession(Consumer<Session> command) {
    try (Session session = getSession()) {

      Transaction tx = session.beginTransaction();

      command.accept(session);
      if (tx.isActive() &&
        !tx.getRollbackOnly()) {
        tx.commit();
      } else {
        tx.rollback();
      }
    }
  }

  public static <T> T returnFromSession(Function<Session, T> command) {
    try (Session session = getSession()) {
      Transaction tx = null;
      try {
        tx = session.beginTransaction();

        return command.apply(session);
      } catch (Exception e) {
        throw new RuntimeException(e);
      } finally {
        if (tx != null) {
          if (tx.isActive() &&
            !tx.getRollbackOnly()) {
            tx.commit();
          } else {
            tx.rollback();
          }
        }
      }
    }
  }
}

Listing 10-3util/src/main/java/com/autumncode/hibernate/util/SessionUtil.java

现在让我们开始创建一些测试。正如我们最近所做的,让我们创建一个chapter10.first.TestBase来构建我们的测试数据。

package chapter10.first;

import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

public class TestBase {
  @BeforeMethod
  public void setupTest() {
    SessionUtil.doWithSession((session) -> {
      User user = new User("user1", true);
      user.addGroups("group1", "group2");
      session.save(user);
      user = new User("user2", true);
      user.addGroups("group2", "group3");
      session.save(user);
      user = new User("user3", false);
      user.addGroups("group3", "group4");
      session.save(user);
      user = new User("user4", true);
      user.addGroups("group4", "group5");
      session.save(user);
    });
  }

  @AfterMethod
  public void endTest() {
    SessionUtil.doWithSession((session) -> {
      // need to manually delete all of the Users since
      // HQL delete doesn't cascade over element collections
      Query<User> query = session.createQuery("from User", User.class);
      for (User user : query.list()) {
        session.delete(user);
      }
    });
  }
}

Listing 10-4chapter10/src/test/java/chapter10/first/TestBase.java

我们的第一个测试根本没有使用过滤器。这是我们的基线测试,一路上演示了doWithSession()方法的用法。

package chapter10.first;

import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestNoFilter extends TestBase {
  @Test
  public void testSimpleQuery() {
    SessionUtil.doWithSession((session) -> {
      Query<User> query = session.createQuery("from User", User.class);
      List<User> users = query.list();
      assertEquals(users.size(), 4);
    });
  }
}

Listing 10-5chapter10/src/test/java/chapter10/first/TestNoFilter.java

那是…不是真的特别有趣,或者有趣。让我们用我们的userEndsWith1过滤器来测试一下,它应用了一个非常简单的条件,接受名字以数字1结尾的任何用户。 4 这将向我们展示如何启用过滤器并验证过滤器的应用。

注意,TestNoFilterTestSimpleFilter类中的查询是相同的:"from User"。Hibernate 在执行查询时将过滤器作为一个附加的where子句来应用。我们将反复使用这个查询,并使用过滤器来修改结果。

package chapter10.first;

import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestSimpleFilter extends TestBase {
  @Test
  public void testNoParameterFilter() {
    SessionUtil.doWithSession((session) -> {
      Query<User> query = session.createQuery("from User", User.class);

      session.enableFilter("userEndsWith1");
      List<User> users = query.list();
      assertEquals(users.size(), 1);
      assertEquals(users.get(0).getName(), "user1");
    });
  }
}

Listing 10-6chapter10/src/test/java/chapter10/first/TestSimpleFilter.java

现在让我们看看如何用过滤器设置参数。这里,我们使用一个 TestNG 数据提供者来传递一个状态和我们期望看到的具有该状态的用户数。

package chapter10.first;

import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestParameterFilter extends TestBase {
  @DataProvider
  Object[][] statuses() {
    return new Object[][]{
      {true, 3},
      {false, 1}
    };
  }

  @Test(dataProvider = "statuses")
  public void testFilter(boolean status, int count) {
    SessionUtil.doWithSession((session) -> {
      Query<User> query = session.createQuery("from User", User.class);

      session
        .enableFilter("byStatus")
        .setParameter("status", status);

      List<User> users = query.list();
      assertEquals(users.size(), count);
    });
  }
}

Listing 10-7chapter10/src/test/java/chapter10/first/TestParameterFilter.java

你也可以组合滤镜。让我们再来一个测试类,但是这次它将有两个单独的测试。第一个将使用我们的byGroup滤镜,第二个将使用两个滤镜—byGroupbyStatus

再次注意,我们根本没有改变我们的基本查询-from User。如果我们愿意,我们可以。过滤器只需将它们的标准添加到基本查询中。

这是过滤器的实际功率。我们有一个基本的查询—from User——我们可以通过编程来决定对查询应用标准,而不必对查询本身做任何额外的事情。

package chapter10.first;

import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestMultipleFilters extends TestBase {
  @Test
  public void testGroupFilter() {
    SessionUtil.doWithSession((session) -> {
      Query<User> query = session.createQuery("from User", User.class);

      session
        .enableFilter("byGroup")
        .setParameter("group", "group4");

      List<User> users = query.list();
      assertEquals(users.size(), 2);

      session
        .enableFilter("byGroup")
        .setParameter("group", "group1");

      users = (List<User>) query.list();
      assertEquals(users.size(), 1);

      // should be user 1
      assertEquals(users.get(0).getName(), "user1");
    });
  }

  @Test

  public void testBothFilters() {
    SessionUtil.doWithSession((session) -> {
      Query<User> query = session.createQuery("from User", User.class);

      session
        .enableFilter("byGroup")
        .setParameter("group", "group4");
      session
        .enableFilter("byStatus")
        .setParameter("status", Boolean.TRUE);

      List<User> users = query.list();

      assertEquals(users.size(), 1);
      assertEquals(users.get(0).getName(), "user4");
    });
  }

}

Listing 10-8chapter10/src/test/java/chapter10/first/TestMultipleFilters.java

摘要

过滤器是将一些数据库问题从代码的其余部分中分离出来的有用方法。一组过滤器可以降低应用其余部分中使用的 HQL 查询的复杂性,但会牺牲一些运行时灵活性。与使用视图(必须在数据库级别创建)不同,您的应用可以利用动态过滤器,这些过滤器可以在需要时被激活。

接下来,让我们看看 servlets 的集成,在这里我们将最终开始看到数据传输对象模式的使用。

Footnotes 1

数据库视图是一个存储查询的视图,因此可以有一个带有“活动”字段的用户表,例如,一个视图可以被定义为select * from users u where u.active=true的结果。正如您将看到的,过滤器为您提供了类似的功能,但是您也可以将它们参数化,因此它们在实践中非常灵活。

  2

安全性和个性化不是与数据库相关的概念,而是与用户体验相关的概念,因此,它们不在本书的讨论范围之内。

  3

在这一点上,它就像 JNDI;您在筛选器定义中引用了一个名称,但筛选器本身是在别处定义的。

  4

一个更好的例子可能是一个不变的状态,而不是“名称以 1 结尾”,但是状态也是参数化的。如果没有人为的例子,数据模型就会被污染,所以这就是我们所拥有的。

 

*

十一、整合到网络中

我们已经看到了很多关于如何使用 Hibernate 的内容,包括一系列的示例模型,但是没有一个真正展示了 Hibernate 在“真实世界”中的用法我们所有的运行代码都被嵌入到测试中,这对开发来说并不是一件坏事,但这并不是一个明确如何将 Hibernate 集成到人们可能会使用的应用中的好方法。

测试是一个很好的例子,说明你在“深层代码”、后端服务或嵌入式应用中会看到什么,它们不会直接向最终用户公开 Hibernate 对象——事实上,大多数现代开发人员会建议直接向用户公开 Hibernate 实体是一个坏主意。 1

因此,让我们深入一个 web 应用,展示 Hibernate 实际上是如何集成的。

将 Hibernate 集成到 web 应用中有很多方法,而最佳方法几乎完全取决于部署架构的选择。我们将从非常简单的东西开始,在那里我们控制一切,然后我们将尝试涵盖你在“真实世界”中遇到的更复杂的场景

搭建舞台

Java 中的 Web 应用最初是围绕“servletss”的思想构建的,servlet 基本上是生成对 HTTP 调用的响应的类。这种模式仍然主导着 web 应用的设计,尽管实际的交付方式已经发生了很大的变化。

这个想法是系统将运行一个容器,一个被设计用来管理模块化应用的应用,它使用一个被称为 Java 2 企业版、 2 的规范,旨在为那些应用提供服务以支持特定的功能。例如,像 Tomcat 这样的 web 容器会为 web 模块提供一个 API,这样就可以用一个集中的控制面板来控制它们,并且像 JDBC 连接这样的资源和其他东西可以在运行时提供给这些模块。

这些模块实现功能的方式最初是通过 servletss 的**,模块将 servlet 映射到特定的 URL。Web 浏览器(或任何其他应用)将使用这些 URL 和各种 HTTP 方法,如GETPOST来运行 servlets 中的代码。**

**那是一个更简单的世界,虽然那种机制起作用(而且,就其本身而言),但它并不是最吸引人的开发过程;如果您想查看客户列表,您的 servlet 必须呈现 HTML,这相当冗长且容易出错。

库的出现使它变得简单了一点(例如,Apache ECS, 3 等等),但是实际上构建一个 web 应用有太多的顾虑;您必须了解 web 模块配置,加上编写 servlet 的功能,再加上将渲染结果编写成 HTML…如果您在显微镜下观察这个过程,这并不是很大的负担,但实际上它给大多数开发人员带来了相当大的负担,而附加值相对较小;大多数网站都没有达到值得努力的流量水平。

自然,社区以多种有益的方式做出了回应(嗯,大部分是有益的)。模型-视图-控制器范式变得非常普遍,在 Struts 和 WebWork 之类的库的帮助下,servlets 只是协调数据处理发生后呈现什么视图。出现了像 JavaServer Pages 这样的渲染引擎,以及像 Velocity 和 Thymeleaf 这样的模板引擎,这甚至还没有开始描述思想和方法的大规模扩散;您可以使用 JVM,用 web 应用编程的书籍单独构建一个库,新的框架会不断涌现,具有各种各样的优势。 4

除此之外,配置也变得更加简单。Spring 框架继承了 Java EE 的重量级特性,并彻底改变了它,强调更容易的部署、配置和开发。更重要的是,由于 Java 在很大程度上是由社区驱动的,Spring 为架构所做的事情被纳入到 Java EE 规范中,就像 Hibernate 对 JPA 所做的一样, 5 就像用 Hibernate 影响 JPA 来为数据库建模 Java 类变得更好一样,使用 Spring 和其他库来影响 Java EE 中以 web 为中心的 API,web 应用开发变得更加容易。

计划

遗憾的是,这本书不是关于 web 应用开发的,而且无论如何也没有合理的方法可以让一本书很好地涵盖 Java 中的 web 开发。尽管如此,在 web 应用中使用 Hibernate 有一些常见的问题,我们可以解决。

我们将使用 Servlets 开发一个 web 应用,其中的某些方面将无法正常工作,因为目的是展示一些问题及其解决方案。本质上,我们不打算设计一个用户界面;这不仅不是你的作者的技能范围,而且是次要问题。我们在这里编写的 servlets 将生成 JSON,它适合使用运行在浏览器中的客户端应用进行渲染;人们可以用 Angular、React 或任何数量的其他 JavaScript 框架来编写这样的应用。

该项目将使用嵌入式 servlet 引擎(Undertow,位于 https://undertow.io/ )进行测试,但也可以部署在任何兼容的 Java EE 容器中——您可以在 Tomcat、WildFly、Open Liberty 或任何其他您喜欢的引擎中随意尝试。同样,该应用的某些部分预计不会正常工作,但这是为了说明问题。

再次声明,这不是关于 web 开发的章节,而是关于在 web 开发中使用 Hibernate 的章节。在本章中,我们不会使用最新、最热门的技术——当涉及到 web 时,我们有目的地降低目标,因为获取我们将要学习的经验并将其应用于更高级的 Web 框架是微不足道的,并且通过关注简单性,我们可以避免陷入大量配置或框架术语中。

应用

我们将设计最普通的应用,博客。

当然,博客是一种“网络日志”,是一种在线日志,这样的应用主宰着网络。如今,大多数博客都托管在大型服务器农场上,如 Medium ( https://medium.com )和 Substack ( https://substack.com ),而大多数个人托管的博客都在软件平台上,如 WordPress ( https://wordpress.org/ )或使用静态站点生成器,如 Jekyll 或 Hugo。 6

我们不会为其中任何一项打造竞争对手。然而,我们将受到作者写有评论的帖子的模式的启发。一个有事业心的读者理论上可以增加安全性(一个需求)和功能性用户界面(另一个需求),然后构建一些有价值的东西。

所以让我们开始吧。

项目模型

首先,我们需要有我们的项目模型。这将包括我们已经看到的许多依赖项,但packaging将是war——因为这将生成一个“web 归档”——它还将包括 Undertow 和 Jackson。

Undertow 是 JBoss 中使用的 servlet 引擎;Jackson ( https://github.com/FasterXML/jackson )是一个数据处理库,我们将用它来读写 JSON。我们还将需要jackson-datatype-jsr310作为依赖项,因为,正如我们很快就会看到的,我们正在使用日期和时间 API 中的一些时间戳类型。 7 我们还包括我们的util库来处理Session的获取和管理,而 Lombok 将为我们节省很多样板文件。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
          http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <groupId>com.autumncode.books.hibernate</groupId>
    <artifactId>hibernate-6-parent</artifactId>
    <version>5.0</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <packaging>jar</packaging>

  <properties>
    <war.name>chapter11</war.name>
    <undertow.version>2.2.8.Final</undertow.version>
  </properties>

  <artifactId>chapter11</artifactId>

  <dependencies>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
      <groupId>com.autumncode.books.hibernate</groupId>
      <artifactId>util</artifactId>
      <version>${project.parent.version}</version>
    </dependency>
    <dependency>
      <groupId>io.undertow</groupId>
      <artifactId>undertow-core</artifactId>
      <version>${undertow.version}</version>
    </dependency>
    <dependency>
      <groupId>io.undertow</groupId>
      <artifactId>undertow-servlet</artifactId>
      <version>${undertow.version}</version>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
    </dependency>
    <dependency>

      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>
  </dependencies>
  <build>
    <finalName>${war.name}</finalName>
  </build>
</project>

Listing 11-1chapter11/pom.xml

数据模型

我们有三个要处理的实体,都在chapter11.model包中:UserPostComment。一个User可以有许多PostComment条目,一个Post有许多Comment实体。在每一个中,我们都覆盖了 Lombok 的toString(),因为我们想确保不会无意中引用集合。(我们根本不希望 Lombok 输出集合。)

我们不太可能经常使用 toString(),但是它对“目测”结果很有用。如果您在代码中跟随,或者当您开发您自己的模型时,您可能会像您的作者一样,在运行您的测试时使用日志来查看值。

img/321250_5_En_11_Fig1_HTML.jpg

图 11-1

博客的实体关系图

首先,我们来看看User。我们将posts初始化为一个空的ArrayList;我们可能不需要这样做,除非我们发现自己在设计一个过程,通过这个过程,我们在用户被持久化之前向用户添加一个User和帖子,实际上,这样初始化帖子列表本质上是防御性的。(举例来说,这可以防止我们在保存之前无意中引用了帖子列表中的一个null。)

package chapter11.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Data
@NoArgsConstructor
public class User {
  @Id

  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;

  @Column(unique = true, nullable = false)
  String name;
  boolean active;

  @OneToMany(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id")
  @OrderBy("createDate")
  List<Post> posts = new ArrayList<>();

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

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

Listing 11-2chapter11/src/main/java/chapter11/model/User.java

这里的toString()相当普通,但是我们从 Lombok 的toString()中覆盖了它,因为 Lombok 默认包含每个属性。一般来说,这没什么问题,但是如果我们使用 Hibernate 实体——也就是从 Hibernate 加载的对象——那么我们最终会冒着在一个Session关闭后需要初始化惰性集合的风险,这意味着访问posts可能会导致我们获得一个org.hibernate.LazyInitializationException

要求会话在任何时候都在范围内——或者,好吧,当我们应该完成对Session的访问时——是本章的主要关注点之一。

Post是我们拥有的三个实体中最有趣的一个,因为它使用了 Hibernate 过滤器定义。我们先来看一下,然后在一些有趣的方面游走。

package chapter11.model;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.*;

import javax.persistence.*;
import javax.persistence.OrderBy;
import java.time.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Data

@NoArgsConstructor
@FilterDefs({
  @FilterDef(
    name = "byTerm",
    parameters = @ParamDef(name = "term", type = "string")),
  @FilterDef(
    name = "byName",
    parameters = @ParamDef(name = "name", type = "string")
  )
})
@Filters({
  @Filter(name = "byTerm",
    condition = "title like :term"),
  @Filter(name = "byName",
    condition = "user_id = (select u.id from User as u where u.name=:name)"),
})
public class Post {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;

  @Column(nullable = false)
  String title;

  @Column(nullable = false)
  @Lob

  String content;

  @ManyToOne
  User user;

  @OneToMany(fetch = FetchType.LAZY)
  @JoinColumn(name = "post_id")
  @OrderBy("createDate")
  List<Comment> comments = new ArrayList<>();

  @Temporal(TemporalType.TIMESTAMP)
  @Column(nullable = false)
  LocalDateTime createDate;

  @Override
  public String toString() {
    return "Post{" +
      "id=" + id +
      ", title='" + title + '\'' +
      ", content='" + content + '\'' +
      ", user=" + user +
      ", createDate=" + createDate +
      '}';
  }

}

Listing 11-3chapter11/src/main/java/chapter11/model/Post.java

我们实际上有两个过滤器:byTermbyName

第一个是byTerm,是一个相当简单的单词搜索过滤器,更多的是作为一个占位符,而不是一个实际的搜索工具;它包括任何标题包含通配符的Post,即过滤器名称中的“term”。

byName滤镜更有趣一点。它实际上包括任何其user_id字段与具有给定名称的User相匹配的Post——因此,它通过用户名为任何Post提供过滤器。之所以这样写是因为它是一个过滤器而不是一个查询;编写一个 HQL 查询来实现相同的目标可能会更容易,但是当我们开始使用过滤器时,过滤器的原因就足够清楚了。

JPA 有一个标准查询 API,它提供了与 Hibernate 过滤器相似的特性。然而,尽管过滤器可能变得冗长,但它们仍然比标准查询冗长得多,并且它们需要较少的设置工作。

我们的最后一个实体是Comment,在看过PostUser之后,它看起来有点普通和无聊。

package chapter11.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Data
@NoArgsConstructor
public class Comment {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;

  @Column(nullable = false)
  @Lob
  String content;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "post_id")
  Post post;

  @ManyToOne(optional = false, fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id")
  User user;

  @Temporal(TemporalType.TIMESTAMP)

  @Column(nullable = false)
  LocalDateTime createDate;

  @Override
  public String toString() {
    return "Comment{" +
      "id=" + id +
      ", content='" + content + '\'' +
      ", createDate=" + createDate +
      '}';
  }
}

Listing 11-4chapter11/src/main/java/chapter11/model/Comment.java

如果没有 Hibernate 配置,我们就不能拥有 Hibernate 应用,所以它在这里;注意,除了实际的特定映射之外,它与所有其他的都非常相似。它也位于src/main/resources而不是src/test/resources中,主要是因为我们希望我们的可部署工件是完整的和可部署的;如果 Hibernate 配置位于test树中,它就不是可部署工件的一部分。

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!--  Database connection settings  -->
    <property name="connection.driver_class">org.h2.Driver</property>
    <property name="connection.url">jdbc:h2:./db11</property>
    <property name="dialect">org.hibernate.dialect.H2Dialect</property>

    <!--  Echo all executed SQL to stdout  -->
    <property name="show_sql">true</property>
    <property name="use_sql_comments">true</property>

    <!--  Drop and re-create the database schema on startup  -->
    <property name="hbm2ddl.auto">create-drop</property>

    <mapping class="chapter11.model.User"/>
    <mapping class="chapter11.model.Post"/>
    <mapping class="chapter11.model.Comment"/>
  </session-factory>
</hibernate-configuration>

Listing 11-5chapter11/src/main/resources/hibernate.cfg.xml

构建我们的第一个 Servlet 测试

接下来,我们要看看可能是整章中最长的类,我们的TestBase servlet。

实际上,有很多方法可以解决为 web 应用编写代码的问题。最优选的方法是编写映射到我们的用户故事的服务——像“添加用户”或“添加帖子”这样的功能——然后使用 servlets 调用这些服务。

这个模型实际上工作得非常好,但是它确实设法自然地将您与在分布式应用中使用 Hibernate 的一些架构问题隔离开来。当您像这样分离关注点时——“添加用户”在一个完全独立的类中——您有一个自然的事务边界,并且很容易意外地正确和干净地做事情。我们将在后续章节中使用该模型,但我们需要理解为什么我们将做出的一些选择存在,所以现在我们要做一些稍微次优的事情。

但是,结果是,我们的测试不能只关注服务——我们需要建立一个实际的 web 服务器,使用实时的 servlets。这不是一个完全的集成测试, 8 但是它实际上使用一个真正的 HTTP 客户端(Java 11 中包含的那个)并发出真正的 HTTP 请求;如果您愿意,您可以手动做与测试完全相同的事情。

*那么,我们的TestBase有很多角色:它需要启动(和停止)逆流,以及注册一系列 servlets。它还需要提供一种简单的方法来发出 HTTP 请求,并构建一种简单的方法来将 JSON 映射到可导航的数据结构中。

我们不需要使它完美,只要对我们的目的足够好就行了。通过各种方式让TestBase变得更好并不需要付出太多的努力,但它已经够长了。

TestBase中的大部分复杂性存在于populateServlets()方法中,它制造了一个DeploymentInfo。它加载一个名为servlets.json的 JSON 文件,其中包含一个 JSON 字典;每个条目都是一个 servlet,它有关于每个条目的数据,比如 Servlet 的实现类和 URL,以及它可能使用的任何初始化参数。

这里是TestBase.java类,我们将在其后立即包含servlets.json,这样您就可以看到它的结构。 9

package chapter11.servlets;

import chapter11.model.Comment;
import chapter11.model.Post;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.server.handlers.PathHandler;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import org.hibernate.query.Query;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

import java.util.Map;

public class TestBase {
  Undertow server;
  TypeReference<Map<String, Object>> mapOfMaps =
    new TypeReference<>() {
    };
  protected ObjectMapper mapper = new ObjectMapper()
    .setSerializationInclusion(JsonInclude.Include.NON_NULL)
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  {
    mapper.registerModule(new JavaTimeModule());
  }

  @BeforeClass
  void start() throws ServletException, IOException {
    DeploymentInfo servletBuilder = Servlets.deployment()
      .setClassLoader(TestBase.class.getClassLoader())
      .setContextPath("/myapp")
      .setDeploymentName("test.war");
    populateServlets(servletBuilder);

    DeploymentManager manager = Servlets
      .defaultContainer()
      .addDeployment(servletBuilder);
    manager.deploy();
    PathHandler path = Handlers.path(Handlers.redirect("/myapp"))
      .addPrefixPath("/myapp", manager.start());
    server = Undertow.builder()
      .addHttpListener(8080, "localhost")
      .setHandler(path)
      .build();
    server.start();
  }

  private void populateServlets(DeploymentInfo servletBuilder)
    throws IOException {
    Map<String, Object> servlets = mapper
      .readValue(
        this
          .getClass()
          .getResourceAsStream("/servlets.json"
          ), mapOfMaps);
    servlets.entrySet().forEach(entry -> {
      Map<String, Object> data =
        (Map<String, Object>) entry.getValue();
      try {
        var servlet = Servlets.servlet(
          entry.getKey(),
          (Class<? extends Servlet>) Class.forName(
            data.get("class").toString()
          ));

        if (data.containsKey("initParams")) {
          Map<String, Object> params =
            (Map<String, Object>) data.get("initParams");
          params.entrySet().forEach(param -> {
            servlet.addInitParam(
              param.getKey(),
              param.getValue().toString()
            );
          });
        }
        servlet.addMapping(data.get("mapping").toString());
        servletBuilder.addServlets(servlet);
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      }
    });
  }

  @AfterClass
  void stop() {
    server.stop();
  }

  @BeforeMethod
  void clearAll() {
    SessionUtil.doWithSession(session -> {
      Query<Comment> commentQuery =
        session.createQuery("from Comment", Comment.class);
      for (var obj : commentQuery.list()) {
        session.delete(obj);
      }

      Query<Post> postQuery =
        session.createQuery("from Post", Post.class);
      for (Post post : postQuery.list()) {
        session.delete(post);
      }

      Query<User> query =
        session.createQuery("from User", User.class);
      for (User user : query.list()) {
        session.delete(user);
      }
    });
  }

  protected HttpResponse<String> issueRequest(String path)
    throws IOException, InterruptedException {
    HttpClient client = HttpClient.newBuilder().build();
    HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("http://localhost:8080/myapp/" + path))
      .timeout(Duration.ofSeconds(3))
      .build();
    HttpResponse<String> response =
      client.send(request, HttpResponse.BodyHandlers.ofString());
    return response;
  }
}

Listing 11-6chapter11/src/test/java/chapter11/servlets/TestBase.java

这是一个相当大的类,与我们到目前为止所拥有的许多源文件相比,但是它并不特别复杂。大部分都与设置我们的 servlets 有关,但是它也清理了数据库,并且有一个方便的方法来发出 HTTP 请求。

mapper引用为我们提供了一种简单的方法来正确测试带有日期的序列化数据。

start()方法在每个测试类之前运行,启动 Undertow servlet 引擎,还有populateServlets(),它使用一个 JSON 文件构建一个入口点列表,我们接下来会看到。它使用原始的 JSON 结构,这并不理想;通常,我们会构建一个对象模型,并使用 that ,但是我们试图选择更少的类。

stop()方法在每个测试类测试后运行,并关闭 Undertow 服务器。

clearAll()方法清理所有的数据,因为它被标记为@BeforeMethod,所以它在每次测试之前运行;从数据的角度来看,每个测试都是从一张白纸开始的。

最后,issueRequest()提供了一种简单的方法来构建用于测试的 HTTP 请求。这是荒谬的简单;它只处理请求路径,不进行参数化,并且只处理 HTTP GET请求。然而,它确实去除了许多直接发出请求的样板文件。

那么,为什么连都有呢?毕竟,我们不需要使用 HTTP 作为传输协议;我们可以拥有执行 servlet 提供的动作的类,并直接调用这些类。服务器或协议不需要。尽管如此,协议障碍是我们试图证明的东西,所以所有的设置都是为了说明在某种“真实世界”情况下的架构边界,如 HTTP 的使用,即使在“真实世界”中没有人会合理地手动进行任何操作。

这里是本章使用的实际的servlets.json文件,包括了每个 servlet。(如果您在阅读时手动构建了本章的代码库,那么您会想要参考这个清单,而不是大量使用它,直到您实现了所有的 servlets。)

{
  "HelloServlet": {
    "class": "chapter11.servlets.HelloWorld",
    "initParams": {
      "message": "Hello World"
    },
    "mapping": "/hello"
  },
  "BadAddUserServlet": {
    "class": "chapter11.servlets.BadAddUserServlet",
    "mapping": "/badadduser"
  },
  "AddUserServlet": {
    "class": "chapter11.servlets.AddUserServlet",
    "mapping": "/adduser"
  },
  "SimpleGetPostsServlet": {
    "class": "chapter11.servlets.SimpleGetPostsServlet",
    "mapping": "/simplegetposts"
  },

  "AddPostServlet": {
    "class": "chapter11.servlets.AddPostServlet",
    "mapping": "/addpost"
  },
  "GetPostsServlet": {
    "class": "chapter11.servlets.GetPostsServlet",
    "mapping": "/getposts"
  },
  "GetPostServlet": {
    "class": "chapter11.servlets.GetPostServlet",
    "mapping": "/getpost"
  },
  "AddCommentServlet": {
    "class": "chapter11.servlets.AddCommentServlet",
    "mapping": "/addcomment"
  }
}

Listing 11-7chapter11/src/test/resources/servlets.json

我们的 servlets 被设计成发出 JSON,具体来说,这听起来是创建一个ServletBase、一个抽象的HttpServlet实现的理想理由,它提供了一种将Object写成 JSON 的便捷方式。

package chapter11.servlets;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

abstract class ServletBase extends HttpServlet {
  protected ObjectMapper mapper = new ObjectMapper()
    .setSerializationInclusion(JsonInclude.Include.NON_NULL)
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  {
    mapper.registerModule(new JavaTimeModule());
  }

  /* simple validation of parameters */
  protected Map<String, String> getValidatedParameters(
    HttpServletRequest req,
    String... fields
  ) {
    Map<String, String> map = new HashMap<>();
    List<String> badFields = new ArrayList<>();
    for (String fieldName : fields) {
      String value = req.getParameter(fieldName);
      if (value == null || value.isEmpty()) {
        badFields.add(fieldName);
      } else {
        map.put(fieldName, value);
      }
    }
    if (badFields.size() > 0) {
      throw new RuntimeException(
        "bad fields provided: " + badFields
      );
    }
    return map;
  }

  /* write out a valid response */
  protected void write(
    HttpServletResponse r,
    int code,
    Object entity
  ) throws IOException {
    r.setContentType("application/json");
    r.setStatus(code);
    r.getWriter().write(mapper
      .writerWithDefaultPrettyPrinter()
      .writeValueAsString(entity)
    );
  }

  /* write out an exception */
  protected final void writeError(
    HttpServletResponse resp,
    Throwable throwable
  ) throws IOException {
    write(resp,
      HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
      Map.of("error", throwable.getMessage())
    );
  }
}

Listing 11-8chapter11/src/main/java/chapter11/servlets/ServletBase.java

我们使用setSerializationInclusion(JsonInclude.Include.NON_NULL)是因为我们希望在序列化数据时忽略包含null的字段。这个重要吗?嗯,不在这里;毕竟,我们没有真正的用户界面,而且我们拥有的总计字段的数量如此之少,以至于包含一些对空数据的引用在任何情况下都无关紧要。尽管如此,这允许一个潜在的用户界面检查字段的存在,而不必检查字段是否为空*。这只是防御性编程。*

*我们还禁用了WRITE_DATES_AS_TIMESTAMPS特性,因为我们使用了LocalDateTime并且我们真的希望它以人类可读的形式编写。不过,要做到这一点,我们需要在 Jackson 中注册JavaTimeModule(),这是用初始化器块完成的。

我们还有一个getValidatedParameters()方法,该方法返回字段名及其值的一个Map<String, String>——如果一个字段没有被提供(并且是必需的),则抛出一个RuntimeException。这是非常原始的做法,有更好的方法。

接下来,我们有write()writeError()方法,它们为我们的输出以标准形式格式化输出。

ServletBase功能正常但不太好的。您可能不想在这个层次上编写 servlets,但是有时理解底层技术在做什么是很重要的。

我们在servlets.json中看到的第一个 servlet 是一个“Hello,World”servlet,名为“HelloServlet”,在一个名为HelloWorld的类中实现。这是一个简单的 servlet,它验证我们的 Undertow 实例是否工作,并使用我们的ServletBase写入输出。

package chapter11.servlets;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class HelloWorld extends ServletBase {
  @Override
  protected void doGet(
    HttpServletRequest request,
    HttpServletResponse response)
    throws IOException {
    Map<String, String> data=Map.of(
      "response", this.getInitParameter("message")
    );
    write(response, HttpServletResponse.SC_OK, data);
  }

}

Listing 11-9chapter11/src/main/java/chapter11/servlets/HelloWorld.java

这里有两件有趣的事情。一个是使用一个“init 参数”来获取响应的值(在servlets.json<initParams>节点中提供),另一个是我们首先构建一个Map<String, String>来生成响应;JSON 是一个数据结构,我们不能用 JSON 写一个简单的字符串作为输出。当然,我们可以直接编写输出,但是我们正在尝试实际测试我们的应用将使用的管道。

我们现在需要测试我们的HelloWorld servlet。有了TestBase,我们可以创建一个HelloWorldTest,看起来像清单 11-10 中所示的那个。

package chapter11.servlets;

import org.testng.annotations.Test;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.Map;

import static org.testng.Assert.assertEquals;

public class HelloWorldTest extends TestBase {
  @Test
  public void testHelloWorld()
    throws IOException, InterruptedException {
    HttpResponse<String> response =
      issueRequest("hello");

    Map<String, Object> data =
      mapper.readValue(response.body(), mapOfMaps);

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK

    );
    assertEquals(
      data.get("response"),
      "Hello World"
    );
  }
}

Listing 11-10chapter11/src/test/java/chapter11/servlets/HelloWorldTest.java

我们的HelloWorldTest真的非常简单:在通过TestBase初始化之后,它发出一个请求(通过晦涩命名的issueRequest()方法,保存在TestBase中),返回一个String——一个应该包含 JSON 的字符串。

然后,我们通过保存在TestBase中的mapper实例对其进行解析,并验证响应代码是否为SC_OK(即 200,表示请求成功的 HTTP 代码),以及 JSON map 是否有一个名为response的属性,其值为Hello World

我们可以创建一个包含response字段的对象,并将我们的 JSON 映射到该对象,而不是使用我们在这里演示的“映射的映射”方法。更重要的是,这可能是做这件事的“正确方法”,我们将在下一个例子中看到这样的例子;我们在这里避免了它,因为它只是另一件需要解释的事情。我们已经引入了足够多的新概念。如果我们把它们分开一些会更好。

这种发出请求并检查响应的模式将在我们的测试中重复出现。

我们的第一个(错误的)Servlet:添加用户

让我们创建一个 servlet 来向系统添加一个新用户。在其中,我们将接受一个用户名,并检查该用户是否已经存在;如果用户不存在,我们将创建用户,然后将用户作为 JSON 返回。

这个过程表面上看起来是正确的。然而,这个代码是错误的。我们很快就会看到原因。

package chapter11.servlets;

import chapter11.dto.UserDTO;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;

import javax.persistence.NoResultException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class BadAddUserServlet extends ServletBase {
  @Override
  protected void doGet(
    HttpServletRequest req,
    HttpServletResponse resp
  )
    throws ServletException, IOException {
    try {
      Map<String, String> input =
        getValidatedParameters(req, "userName");

      User user = SessionUtil.returnFromSession(
        session -> createUser(session, input.get("userName"))
      );
      write(resp,
        HttpServletResponse.SC_OK,
        user);
    } catch (Exception e) {
      writeError(resp, e);
    }
  }

  private User createUser(Session session, String userName) {
    User entity;
    try {
      Query<User> query = session.createQuery(
        "from User u where u.name=:name",
        User.class);
      query.setParameter("name", userName);

      entity = query.getSingleResult();
    } catch (NoResultException nre) {
      entity = new User(userName, true);
      session.save(entity);
    }

    return entity;
  }
}

Listing 11-11chapter11/src/main/java/chapter11/servlets/BadAddUserServlet.java

我们的 servlet 功能的入口点是doGet(),它映射到用于调用 Servlet 的 HTTP 方法。(因此,它响应GET请求,出于我们的目的,其他方法将被忽略。)它使用getValidatedParameters()来验证参数。

然后,它创建一个User引用,并通过调用createUser()来调用SessionUtil.returnFromSession()。在那之后,除了异常处理之外,实际上没什么可做的了:它将User引用写入输出。

然而,用来获取User的机制是这个 servlet 失败的地方。

我们的设计是返回映射到周期userNameUser。如果User已经存在,它从数据库加载用户并返回那个。(从整体来看,这是否明智还存在争议;对于一个“真正的应用”,您不会像这样返回一个现有的用户,以防有人发送“添加用户”功能来查找现有的用户名。)

错误很简单:我们使用的 JSON 库 Jackson 将遍历User中的每个属性,并在 JSON 中为它创建一个表示。然而,如果用户是由createUser()创建的,那么Userposts引用被设置为一个简单的ArrayList——但是如果它是从数据库加载的,那么posts引用实际上是一个代理值,映射User的尝试将导致 Hibernate 尝试从数据库加载Post列表。

然而,映射发生在λ存在的之后的*:Session不再活动,因此我们将得到一个LazyInitializationError。*

这是我们的测试类,它实际上练习了 Servlet 中的大部分代码。

package chapter11.servlets;

import org.testng.annotations.Test;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;

import static org.testng.Assert.assertEquals;

public class BadAddUserServletTest extends TestBase{
  String getServletName() {
    return "badadduser";
  }

  @Test
  void emptyUserNameProvided()
    throws IOException, InterruptedException {
    HttpResponse<String> response =
      issueRequest(getServletName()+"?userName=");
    System.out.println(response.body());

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_INTERNAL_SERVER_ERROR
    );
  }

  @Test
  void noUserNameProvided()
    throws IOException, InterruptedException {
    HttpResponse<String> response =
      issueRequest(getServletName());
    System.out.println(response.body());

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_INTERNAL_SERVER_ERROR
    );
  }

  @Test
  void runAddUser()
    throws IOException, InterruptedException {
    HttpResponse<String> response =
      issueRequest(getServletName()+"?userName=ts");
    System.out.println(response.body());

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK
    );

    response = issueRequest("badadduser?userName=ts");
    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_INTERNAL_SERVER_ERROR
    );
  }
}

Listing 11-12chapter11/src/test/java/chapter11/servlets/BadAddUserServletTest.java

首先,它有一个emptyUserNameProvided——这将触发一个验证错误,就像根本不提供userName参数一样,如noUserNameProvided测试所示。(它使用getServletName()是因为我们想在另一个测试中重用其中的一些方法,除了另一个 servlet。)

然后我们进入runAddUser测试,该测试调用我们的 servlet 两次:一次是首先添加用户,然后取回同一个用户*……除了第二次调用失败并出错,这是因为惰性初始化。我们不测试实际的异常;我们可以,因为我们控制着 servlet 的源代码,但是在实践中,人们通常不会向最终用户公开原始的应用异常,这不会是一个“可测试的结果”(您可以将这样的异常转换成对用户来说更可展示的东西,并测试内容。)*

AddUserServlet,已更正

理论上,我们有几种方法可以解决BadAddUserServlet的问题。一种是用@com.fasterxml.jackson.annotation.JsonIgnore标记posts元素,这告诉 Jackson 忽略该属性。然而,可能有些情况下我们确实想要整个用户的历史;将字段标记为“忽略”感觉有点宽泛。

一个更好的解决方案可能是 10 使用一个叫做“数据传输对象”的东西,这个类的目的纯粹是为了在架构边界之间进行传输。

例如,在这种情况下,我们不关心posts——事实上,我们也不想要它们,因为加载一个活跃用户的帖子集可能需要相当长的时间。因此,我们的数据传输对象,或 DTO,将只有来自User : idnameactive的我们真正关心的字段。清单 11-13 展示了它可能的样子。

package chapter11.dto;

import lombok.Data;

@Data
public class UserDTO {
  int id;
  String name;
  boolean active;

  public UserDTO() {
  }

  public UserDTO(
    int id,
    String name,
    boolean active
  ) {
    this.id = id;
    this.name = name;
    this.active = active;
  }
}

Listing 11-13chapter11/src/main/java/chapter11/dto/UserDTO.java

我们给它一个惟一的名字UserDTO,因为当 DTO 和实体在同一个源文件中时,我们可能不得不使用完全限定的类名(即chapter11.dto.User)。

那么我们将如何利用它呢?好吧,这里有一个工作的AddUserServlet,写的结构和BadAddUserServlet几乎一模一样,除了用UserDTO来回传递数据,而不是用User

package chapter11.servlets;

import chapter11.dto.UserDTO;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;

import javax.persistence.NoResultException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import java.util.Map;

public class AddUserServlet extends ServletBase {
  @Override
  protected void doGet(
    HttpServletRequest req,
    HttpServletResponse resp
  )
    throws ServletException, IOException {
    try {
      Map<String, String> input =
        getValidatedParameters(req, "userName");

      UserDTO user = SessionUtil.returnFromSession(
        session -> createUser(session, input.get("userName"))
      );
      write(resp,
        HttpServletResponse.SC_OK,
        user);
    } catch (Exception e) {
      writeError(resp, e);
    }
  }

  protected UserDTO createUser(Session session, String userName) {
    User entity;
    try {
      Query<User> query = session.createQuery(
        "from User u where u.name=:name",
        User.class);
      query.setParameter("name", userName);

      entity = query.getSingleResult();
    } catch (NoResultException nre) {
      entity = new User(userName, true);
      session.save(entity);
    }

    UserDTO dto = new UserDTO();
    dto.setId(entity.getId());
    dto.setName(entity.getName());
    dto.setActive(entity.isActive());

    return dto;
  }

}

Listing 11-14chapter11/src/main/java/chapter11/servlets/AddUserServlet.java

我们可以使用不同类型的查询直接构建 DTO,而不是通过从UserUserDTO的映射。我们仍然需要能够构造一个User实体,这意味着我们仍然需要手动映射到一个UserDTO实例。在本章的剩余部分,我们不会遵循这种模式,但是这里有一个基于AddUserServlet的示例实现,覆盖了createUser()来展示这个过程。

package chapter11.servlets;

import chapter11.dto.UserDTO;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;

import javax.persistence.NoResultException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class AddUserServletDTO extends AddUserServlet {
  protected UserDTO createUser(Session session, String userName) {
    UserDTO dto;
    try {
      Query<UserDTO> query = session.createQuery(
        "select new chapter11.dto.UserDTO(u.id, u.name,u.active) "
        +"from User u where u.name=:name",
        UserDTO.class);
      query.setParameter("name", userName);

      dto = query.getSingleResult();
    } catch (NoResultException nre) {
      User u = new User(userName, true);
      session.save(u);
      dto = new UserDTO(u.getId(), u.getName(), u.isActive());

    }

    return dto;
  }

}

Listing 11-15chapter11/src/main/java/chapter11/servlets/AddUserServletDTO.java

如果您想使用这个类而不是AddUserServlet,您可以修改servlets.json来简单地引用这个类名。

自然要问是否管用。我们将继承来自BadAddUserServletTest的几乎所有东西,因为我们想要验证使用不正确的参数仍然会导致错误,但是我们将覆盖runAddUser来实际验证它获得了 HTTP 响应代码 200——这意味着成功的请求——而不是预期的错误。清单 11-16 显示了来源。

package chapter11.servlets;

import org.testng.annotations.Test;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.Map;

import static org.testng.Assert.assertEquals;

public class AddUserServletTest
  extends BadAddUserServletTest {
  String getServletName() {
    return "adduser";
  }

  @Override
  @Test
  void runAddUser()
    throws IOException, InterruptedException {
    HttpResponse<String> response =
      issueRequest("adduser?userName=jbo");

    Map<String, Object> data =
      mapper.readValue(response.body(), mapOfMaps);

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK
    );

    response = SimpleGetPostsService.getSimplePosts();

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK
    );
  }
}

Listing 11-16chapter11/src/test/java/chapter11/servlets/AddUserServletTest.java

现在我们可以说我们可以正确地添加用户了,使用我们对“添加用户”过程的定义:不管数据库是否被修改,它都会正确地返回一个User表示。

dto 闪耀的地方

展示的经验法则很简单,它强化了我们在本书第三章的早期内容。从 Hibernate 加载的实体实际上是由代理的对象,只要它们由Session管理,它们的数据就会被填充。一旦实体与Session分离,它们的状态就固定了;如果他们的数据没有从数据库中加载,那么他们的数据就不能从数据库中加载,直到他们以某种方式再次连接到Session

如果你没有从本章学到其他东西,那就从上一段吸取教训。这一章中有很多非常有趣的代码,尽管其中大部分对“真正的应用”来说用处有限,但是前面的段落是主要的要点。其他的都是证明和示范。

那么,这两种方法是在从实体被加载时返回之前完全加载实体和创建实体的分离版本,这就是我们对UserDTO所做的。我们也可以用User的副本来完成:

User user=Session.load(userId, User.class);
// we never use clone() in Java, right?
User copy=new User();
copy.setId(user.getId());
copy.setName(user.getName());
return copy;

通过创建一个新的*User而不是让它成为一个托管对象,这可以像使用UserDTO一样容易地避免惰性初始化问题。*

*如果需要维护的类更少,为什么不使用创建实体的非托管实例的策略呢?没有一个真正好的、可靠的答案,尽管有一些促成因素。对我来说,这主要归结于目的;我希望班级有一个特定的角色。应该使用像User这样的实体类将数据映射到数据库或从数据库映射数据;使用它在 servlet 和富客户机之间传输数据是要求它做双重工作,在这种情况下,很容易忘记给定的类扮演什么角色;这个User实例是托管的,还是被用来序列化 JSON?

如果我使用一个UserDTO,那么这不是一个我必须问的问题;如果我正在序列化为 JSON,那就是一个UserDTO,句号。如果我从数据库中读取,它是一个User实体,我知道如果我像这样跨越架构边界,我需要来回转换。此外,我可以创建任意多的数据传输类型——如果我想要一个包含用户帖子的 DTO,我可以创建一个UserPostsDTO,或者如果我想要包含他们的评论,我可以创建一个UserCommentsDTO,并精确地控制数据。 11

完善应用

那我们该怎么办?应用需求的核心方面可能类似于

  1. 创建用户(我们已经展示过了)

  2. 创建帖子

  3. 给帖子添加评论

  4. 按日期获取帖子

  5. 按用户获取帖子

  6. 通过关键字获取帖子

大多数都很简单,尽管最后三个很有趣,因为它们将使用我们在Post实体中设置的过滤器。甚至很有可能将大量功能抽象成流程——毕竟,对于每一个流程,我们都要验证输入(以一种相当简单的方式),然后调用一个方法来生成输出。

实际上,对于读者来说,进行这种重构可能是一项值得做的工作,但是如果不进行重构,我们的清单将会很长,所以我们将直接进入。先来看看我们是怎么创建帖子的。

创建帖子

根据我们的模型,为我们创建一个帖子包括帖子的内容和标题以及有效的用户参考。实现这一点的实际代码相当简单——测试将带来一些非常严重的影响。

例如,考虑获取有效用户引用的过程。我们的AddUserServlet有这样做的代码,它不是很长——19 行代码,带有打印的硬包装和到UserDTO的冗长转换。

在正常情况下,我们会创建一个服务类来保存我们的功能,所以我们会调用一个getOrCreateUser()方法,不管有没有Session——例如,我们在第三章中已经看到了这种模式。这给了我们容易嵌入的功能,而不必担心会话分界12——对象离开托管状态的地方。

事实上,服务对象允许我们几乎不用考虑就能维护分界障碍,这是使用它们的一个很好的理由。然而,在这一章中,会话的开始和结束是我们试图研究的核心课程——我们正在研究障碍以及如何考虑它们——因此,为了了解解决方案为什么以及如何工作,我们避免使用明显的解决方案。

所以,首先,让我们开始为我们的测试构建工具,这样我们就可以构建可组合的服务调用来对我们隐藏issueRequest()。我们仍然需要HttpResponse对象,所以我们可以测试响应的主体和响应代码,但至少我们可以隐藏许多进行服务调用本身的常见方面。

我们将从一个叫做BaseService的类开始。我们将把这些设计成静态类,因为它们完全没有状态要管理,虽然这是实际实现的反模式,但该反模式仅限于本章我们的测试结构。我们并没有将它们设计成可以替代实际的服务呼叫。 13

package chapter11.servlets;

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.time.Duration;

public class BaseService {
  static String encode(String value) {
    return URLEncoder.encode(
      value,
      Charset.defaultCharset()
    );
  }

  static HttpResponse<String> issueRequest(String path)
    throws IOException, InterruptedException {
    HttpClient client = HttpClient.newBuilder().build();

    HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("http://localhost:8080/myapp/" + path))
      .timeout(Duration.ofSeconds(3))
      .build();

    HttpResponse<String> response =
      client.send(request, HttpResponse.BodyHandlers.ofString());
    return response;
  }
}

Listing 11-17chapter11/src/test/java/chapter11/servlets/BaseService.java

正如您所看到的,这个类简单地保存了一个简单的issueRequest()以及一个对 HTTP 参数进行编码的方法。通过这样做,我们可以节省一些代码,但不是很多——毕竟,我们已经在TestBase中有了一个issueRequest()的副本,正如我们对“添加用户”端点的测试所示。

让我们看看另一个服务类AddUserService。这将扩展BaseService,因此它可以访问encode()issueRequest(),并通过调用AddUserServlet端点返回HttpResponse

package chapter11.servlets;

import java.io.IOException;
import java.net.http.HttpResponse;

public class AddUserService extends BaseService {
  static HttpResponse<String> addUser(
    String userName)
    throws IOException, InterruptedException {
    String path = String.format(
      "adduser?userName=%s",
      encode(userName)
    );
    return issueRequest(path);
  }
}

Listing 11-18chapter11/src/test/java/chapter11/servlets/AddUserService.java

我们添加一个Post的服务非常相似,尽管我们还没有看到添加帖子的端点。

package chapter11.servlets;

import java.io.IOException;
import java.net.http.HttpResponse;

public class AddPostService extends BaseService {
  static HttpResponse<String> addPost(
    String title,
    String content,
    String userName
  ) throws IOException, InterruptedException {
    String path = String.format(
      "addpost?title=%s&content=%s&userName=%s",
      encode(title),
      encode(content),
      encode(userName));
    return issueRequest(path);
  }
}

Listing 11-19chapter11/src/test/java/chapter11/servlets/AddPostService.java

最后,我们还需要一个服务调用来获取个帖子。这是一个执行简单查询的调用——没有任何类型的参数——主要用于早期测试。我们还没有看到它的终结点,但是我们很快就会看到它——不久之后,我们将制作另一个具有更强大功能的“获取帖子”终结点。

package chapter11.servlets;

import java.io.IOException;
import java.net.http.HttpResponse;

public class SimpleGetPostsService extends BaseService{
  static HttpResponse<String> getSimplePosts()
    throws IOException, InterruptedException {
    return issueRequest("simplegetposts");
  }
}

Listing 11-20chapter11/src/test/java/chapter11/servlets/SimpleGetPostsService.java

让我们来看看我们提到的两个端点——简单地获取帖子和添加帖子——然后我们来看看第一次将所有这些集合在一起的测试。SimpleGetPostsServlet是第一个,因为它非常简单。

package chapter11.servlets;

import chapter11.dto.PostDTO;
import chapter11.model.Post;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

public class SimpleGetPostsServlet extends ServletBase {
  @Override

  protected void doGet(
    HttpServletRequest req, HttpServletResponse resp
  ) throws ServletException, IOException {
    List<PostDTO> posts = SessionUtil.returnFromSession(session ->
      getPosts(session));
    write(
      resp,
      HttpServletResponse.SC_OK,
      posts
    );
  }

  private List<PostDTO> getPosts(Session session) {
    Query<Post> postQuery = session
      .createQuery("from Post p", Post.class);
    postQuery.setMaxResults(20);
    return postQuery.list().stream().map(post -> {
      PostDTO dto = new PostDTO();
      dto.setId(post.getId());
      dto.setUser(post.getUser().getName());
      dto.setContent(post.getContent());
      dto.setTitle(post.getTitle());
      dto.setCreatedDate(post.getCreateDate());
      return dto;
    }).collect(Collectors.toList());
  }
}

Listing 11-21chapter11/src/main/java/chapter11/servlets/SimpleGetPostsServlet.java

这里唯一需要注意的是将一个Post转换成一个PostDTO的流操作。当然,我们不能没有来源的PostDTO

package chapter11.dto;

import lombok.Data;

import java.time.LocalDateTime;
import java.util.List;

@Data
public class PostDTO {
  int id;
  String user;
  String title;
  String content;
  List<CommentDTO> comments=List.of();
  LocalDateTime createdDate;
}

Listing 11-22chapter11/src/main/java/chapter11/servlets/PostDTO.java

当然,这个类是一个非常简单的Post的表示。它将comments属性初始化为一个空列表,因为我们希望能够用它来发送回带有评论的帖子…但我们不想被迫这样做。尽管如此,我们也需要包括我们的CommentDTO

package chapter11.dto;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class CommentDTO {
  String user;
  String content;
  LocalDateTime createdDate;

}

Listing 11-23chapter11/src/main/java/chapter11/servlets/CommentDTO.java

现在我们终于可以开始我们的AddPostServlet了。这个类看起来相当长——将近 70 行——但是它真的非常简单。获取一个User的代码本来可以被抽象掉,但是那个代码只有三条语句(尽管这里取了七行 14 )。在我们得到一个User——如果User不存在就抛出一个隐式异常——之后,我们创建一个Post,用Session.save()将其保存在数据库中,然后创建一个PostDTO来序列化响应。

最后我们进行了一项测试,将所有这些部件组合在一起。首先,让我们看一下测试类,然后我们将遍历它做什么。

package chapter11.servlets;

import chapter11.dto.PostDTO;
import com.fasterxml.jackson.core.type.TypeReference;
import org.testng.annotations.Test;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.List;

import static org.testng.Assert.assertEquals;

public class AddPostServletTest
  extends TestBase {

  TypeReference<List<PostDTO>> listOfPosts =
    new TypeReference<>() {
    };

  void addPost()

    throws IOException, InterruptedException {

    HttpResponse<String> response = AddPostService.addPost(
      "test post",
      "my test post",
      "jbo"
    );
    System.out.println(response.body());

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK,
      "invalid user"
    );

    PostDTO data =
      mapper.readValue(
        response.body(),
        PostDTO.class
      );

    response = SimpleGetPostsService.getSimplePosts();

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK
    );

    System.out.println(response.body());

    List<PostDTO> dtos=mapper.readValue(response.body(),
      listOfPosts);
    System.out.println(dtos);
    assertEquals(dtos.size(), 1);
  }

  @Test(
    expectedExceptions = AssertionError.class,
    expectedExceptionsMessageRegExp = "invalid user.*"
  )
  void addPostNoUser() throws IOException, InterruptedException {
    addPost();
  }

  @Test
  void addPostWithValidUser()
    throws IOException, InterruptedException {

    HttpResponse<String> response =
      AddUserService.addUser("jbo");

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK
    );

    addPost();
  }
}

Listing 11-24chapter11/src/test/java/chapter11/servlets/AddPostServletTest.java

这门课有四个“片段”。首先是一个TypeReference的声明,这样我们可以告诉我们的ObjectMapper在反序列化 JSON 时使用什么类型——我们基本上是在告诉 Jackson 如何创建一个PostDTO引用的List。我们在我们的TestBase.java类中看到了同样的事情,除了我们用了一个Map<String, Object>作为一个更一般化的形式。(这里一个等价的通用的——也是无用的——类型引用可能是TypeReference<List<Map<String, Object>>>,但是我们显然不希望。我们实际上并没有使用引用引用PostDTO,但是如果我们想这样做,我们已经准备好了。)

我们看到的下一个方法是一个实用方法,实际上是添加一篇文章。这很简单。它调用我们的AddPostService.addPost()方法,然后通过assertEquals检查响应代码,如果断言失败,将“无效用户”添加到失败异常消息中。(我们将在我们的第一个测试方法中看到这一点。)

假设断言通过,它将 JSON 映射到一个PostDTO——我们不使用这个,但是如果我们想要验证创建,我们可以。为了节省空间,我们没有进行全面的验证。

*然后,addPost()方法调用GetSimplePostsServlet,因为我们想确保帖子确实被创建了。(否则,AddPostServlet可能会返回一个新的PostDTO,并且什么都不保留——但仍然通过测试。)我们确保调用返回状态码 200,然后我们将 JSON 映射到一个PostDTO对象的列表中——并验证列表中有一个帖子(我们刚刚创建的帖子)。

我们的第一个测试方法是addPostNoUser(),它只是委托给了一个addPost()方法。顾名思义,当调用addPost()时,没有用户被构造,我们期待一个异常。实际的异常类型将是AssertionError(因为它是由assertEquals()触发的,当我们验证添加帖子的响应状态是 200 时),我们还验证异常消息——因为我们希望通过addPostNoUser()测试*,当且仅当*因为没有用户在场而抛出异常时。

第二个测试addPostWithValidUser只比addPostNoUser稍微多一点:它首先添加用户*,然后调用addPost(),并期望没有任何异常。*

那么,我们从这一节中学到了什么?我们已经看到了很多——我们正在构建类来处理对我们构建的 servlets 的实际调用,尽管它们仍然很低级。我们还从概念上构建了我们的 dto 的其余部分,我们已经展示了我们可以适当地添加帖子——我们还展示了如何获取*帖子,尽管这还没有做好。

接下来让我们更好地处理获得帖子的问题。

一个更好的“获取帖子”Servlet

如果你回头看一下Post实体,你会注意到我们有两个过滤器定义:byNamebyTerm。是时候我们用这些来创造一个更好的版本了。从结构上来说,这是一样的,但是我们将使用两个可选的请求参数来指定一个“名称”——即文章的作者——或者一个“术语”,即文章标题的简单通配符。

package chapter11.servlets;

import chapter11.dto.PostDTO;
import chapter11.model.Post;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

public class GetPostsServlet extends ServletBase {
  @Override
  protected void doGet(
    HttpServletRequest req, HttpServletResponse resp
  ) throws ServletException, IOException {
    List<PostDTO> posts = SessionUtil.returnFromSession(session ->
      getPosts(
        session,
        req.getParameter("userName"),
        req.getParameter("term"))
    );
    write(
      resp,
      HttpServletResponse.SC_OK,
      posts
    );
  }

  private List<PostDTO> getPosts(
    Session session,
    String userName,
    String term
  ) {
    if (userName != null && !userName.isEmpty()) {
      session
        .enableFilter("byName")
        .setParameter("name", userName);
    }

    if (term != null && !term.isEmpty()) {
      session
        .enableFilter("byTerm")
        .setParameter("term", "%" + term + "%");
    }

    Query<Post> postQuery = session
      .createQuery(
        "from Post p order by p.createDate ",
        Post.class
      );

    return postQuery.list().stream().map(post -> {
      PostDTO dto = new PostDTO();
      dto.setId(post.getId());
      dto.setUser(post.getUser().getName());
      dto.setContent(post.getContent());
      dto.setTitle(post.getTitle());
      dto.setCreatedDate(post.getCreateDate());
      return dto;
    }).collect(Collectors.toList());
  }

}

Listing 11-25chapter11/src/main/java/chapter11/servlets/GetPostsServlet.java

getPosts()方法几乎是同一方法的SimpleGetPostsServlet版本的克隆,加入了一些额外的东西。它接受两个参数,可以是 null 或空的(因此我们不能使用我们的ServletBase.getValidatedParameters()调用),以及Session;然后,它根据这些参数的存在启用各种过滤器。

然后它执行一个简单的查询:from Posts p order by p.createDate,Hibernate 根据过滤器对于这个Session是否有效来应用过滤器。我们不必构建自定义查询或类似的东西。

当然,如果不编写一个测试来展示它,我们就不能拥有这样一个 servlet。我们有一个可能性矩阵要管理,所以我们将再次使用DataProvider

首先,我们需要一个GetPostsService来匹配我们在其他测试中使用的服务模型。

package chapter11.servlets;

import java.io.IOException;
import java.net.http.HttpResponse;

public class GetPostsService extends BaseService {
  static HttpResponse<String> getPosts(String userName, String term)
    throws IOException, InterruptedException {
    StringBuilder path = new StringBuilder("getposts");
    String separator = "?";
    if (userName != null && !userName.isEmpty()) {
      path
        .append(separator)
        .append("userName=")
        .append(userName);
      separator = "&";
    }

    if (term != null && !term.isEmpty()) {
      path
        .append(separator)
        .append("term=")
        .append(term);
    }
    return issueRequest(path.toString());
  }
}

Listing 11-26chapter11/src/test/java/chapter11/servlets/GetPostsService.java

这个类非常简单;它基本上是基于我们的搜索词建立一个查询并发出一个请求。

我们的测试也相当简单:它包含了一个@BeforeMethod,用两个用户(“jbo”和“ts”)填充我们的数据库,还为这些用户添加了五个帖子,在他们之间分配。实际内容并不特别重要;我们只是在寻找一个数据集,我们可以预测我们的测试。

然后我们有一个searchCriteria()方法作为@DataProvider。这里,我们构建了一系列包含四个元素的数组:

  1. “搜索用户”值

  2. “搜索标题”值

  3. 给定搜索词的预期记录数

  4. 对该行所描述内容的描述

我们实际的测试方法非常简单。它从GetPostsService.getPosts()获取一个响应,并验证它是否以一个成功的状态码响应,然后将响应的主体转换成一个List<PostDTO>(就像我们的AddPostServletTest所做的那样),并验证响应的计数是否与数据提供者所说的相符。

package chapter11.servlets;

import chapter11.dto.PostDTO;
import com.fasterxml.jackson.core.type.TypeReference;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.stream.Collectors;

import static org.testng.Assert.assertEquals;

public class GetPostsServletTest
  extends TestBase {
  TypeReference<List<PostDTO>> listOfPosts =
    new TypeReference<>() {
    };

  @BeforeMethod
  void createUsersAndPosts() throws IOException, InterruptedException {
    List<Integer> errorCodes = List.of(
      AddUserService.addUser("jbo"),
      AddUserService.addUser("ts"),
      AddPostService.addPost("raccoons 1", "raccoons are cool", "jbo"),
      AddPostService.addPost("i like dogs", "see title", "jbo"),
      AddPostService.addPost("never seen no cat", "what are cats", "jbo"),
      AddPostService.addPost("raccoons 2", "raccoons are trash pandas", "ts"),
      AddPostService.addPost("dogs are good", "i named mine scooby", "ts")
    )
      .stream()
      .map(HttpResponse::statusCode)
      .filter(status -> status != 200)
      .collect(Collectors.toList());
    if (errorCodes.size() > 0) {
      throw new RuntimeException(
        "An error was encountered seeding data"
      );
    }
  }

  @DataProvider
  Object[][] searchCriteria() {
    return new Object[][]{
      {null, null, 5, "all posts"},
      {"jbo", null, 3, "jbo posts"},
      {"jbo", "cat", 1, "jbo cat posts"},
      {null, "raccoons", 2, "raccoon posts"},
      {"arl", null, 0, "invalid user posts"},
      {null, "crow", 0, "search term with no results"},
      {"ts", "cat", 0, "ts has no cat posts"}
    };
  }

  @Test(dataProvider = "searchCriteria")
  void getPosts(String userName, String term, int count, String desc)
    throws IOException, InterruptedException {
    HttpResponse<String> response =
      GetPostsService.getPosts(userName, term);

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_OK
    );

    List<PostDTO> dtos = mapper.readValue(
      response.body(),
      listOfPosts);

    System.out.println(dtos);
    assertEquals(dtos.size(), count);
  }
}

Listing 11-27chapter11/src/test/java/chapter11/servlets/GetPostsServletTest.java

完善“应用”

有两个功能我们还没有写,它们都与特定的文章相关。首先,我们没有提供检索特定帖子的方法,也没有提供向帖子添加评论的方法。

我们已经看到了两种镜像功能的流程。毕竟,我们知道如何得到一个User,尽管我们还没有看到将一个User作为一个UserDTO返回,具体地说;获得一个PostDTO就是通过帖子的 id 检索一个PostDTO,并适当地填充它的评论集。向帖子添加评论的过程与为用户添加帖子的过程大致相同。

由于这些过程与我们所看到的非常相似,我们将实现 servlets 并进行一个测试,测试检索给定帖子并检查其评论的机制。

当然,我们已经有了数据传输对象,所以让我们深入 servlet 来获得一个特定的 post。它将接受一个参数,一个帖子 id,并返回一个完全填充的PostDTO(包括评论)。注意,这个名字很像我们的另一个 Servlet:这是GetPostServlet,不是GetPostsServlet

package chapter11.servlets;

import chapter11.dto.CommentDTO;
import chapter11.dto.PostDTO;
import chapter11.model.Post;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Session;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.stream.Collectors;

public class GetPostServlet extends ServletBase {
  @Override

  protected void doGet(
    HttpServletRequest req,
    HttpServletResponse resp)
    throws ServletException, IOException {
    try {
      Map<String, String> input = getValidatedParameters(req, "id");
      Integer id = Integer.parseInt(input.get("id"));

      PostDTO postDTO = SessionUtil
        .returnFromSession(session -> getPost(session, id));

      write(
        resp,
        HttpServletResponse.SC_OK,
        postDTO
      );
    } catch (Exception e) {
      handleException(resp, e);
    }
  }

  protected void handleException(
    HttpServletResponse resp,
    Exception e
  ) throws IOException {
    if (e.getCause() instanceof ObjectNotFoundException) {
      write(
        resp,
        HttpServletResponse.SC_NOT_FOUND,
        Map.of("error", e.getCause().getMessage())
      );
    } else {
      writeError(resp, e);
    }

  }

  protected PostDTO getPost(Session session, Integer id) {
    Post post = session.load(Post.class, id);
    PostDTO postDTO = new PostDTO();

    postDTO.setId(id);
    postDTO.setTitle(post.getTitle());
    postDTO.setContent(post.getContent());
    postDTO.setCreatedDate(post.getCreateDate());
    postDTO.setUser(post.getUser().getName());

    postDTO.setComments(
      post

        .getComments()
        .stream()
        .map(
          comment -> {
            CommentDTO commentDTO = new CommentDTO();
            commentDTO.setContent(comment.getContent());
            commentDTO.setCreatedDate(comment.getCreateDate());
            commentDTO.setUser(comment.getUser().getName());
            return commentDTO;
          })
        .collect(Collectors.toList())
    );
    return postDTO;
  }
}

Listing 11-28chapter11/src/main/java/chapter11/servlets/GetPostServlet.java

注意getPost()方法,它相当简单,但是有很多代码。它所做的只是由id加载一个Post——如果id不存在,它将抛出一个异常——并用数据填充一个PostDTO包括将一组注释转换成一列CommentDTO对象的过程。

虽然在catch()块中,doGet()方法值得讨论。handleException()方法的存在是因为SessionUtil.returnFromSession()调用没有像人们希望的那样从 lambda 返回一个ObjectNotFoundException;它实际上抛出了一个RuntimeException,实际的潜在原因是RuntimeException的一部分。

在这种情况下,我们实际上想要返回一个 404——一个 HTTP“未找到”消息——而不是一个“服务器错误”消息,如果用户提交了一个帖子 id,而这个 id可能是正确的却不存在。 15

因此,我们必须做的是在捕获异常时检查异常的原因——如果它是一个ObjectNotFoundException,那么PostSession.load()失败了,我们希望返回一个 404,而不是 500。

我们要看的下一个 servlet 是AddCommentServlet,它是 GetPostServlet的扩展——因为我们想要重用我们刚刚看到的getPost()方法。它加载一个User,然后通过id加载Post,然后创建一个简单的Comment并将其添加到Post's现有的注释列表中;然后,它从GetPostServlet返回getPost()的值,以返回填充的PostDTO——此时大部分是从缓存加载的,因为我们始终使用同一个Session

这里的关键是注意我们如何管理Session;一切都发生在用SessionUtil.returnFromSession()初始化的 lambda 的上下文中,所以缓存是活动的,我们可以完全访问每个对象的数据,因为如果调用发生时它还没有被填充——就像post.getComments().add(comment);Session可以按需加载它需要的任何东西。更重要的是,由于所有的都是在单个Session中发生的,即使我们试图再次获取*,我们的数据也会被缓存。*

*我们可以编写简单明了的代码,并且我们可以利用高速缓存的优势使其快速运行,尽管由于安装和拆卸时间的原因,我们的测试并不是特别快。

这是我们的AddCommentServlet

package chapter11.servlets;

import chapter11.dto.CommentDTO;
import chapter11.dto.PostDTO;
import chapter11.model.Comment;
import chapter11.model.Post;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.stream.Collectors;

public class AddCommentServlet extends GetPostServlet {
  @Override
  protected void doGet(
    HttpServletRequest req,
    HttpServletResponse resp)
    throws ServletException, IOException {
    try {
      Map<String, String> input = getValidatedParameters(
        req,
        "id",
        "userName",
        "content"
      );
      Integer id = Integer.parseInt(input.get("id"));

      PostDTO postDTO = SessionUtil
        .returnFromSession(session ->
          addComment(
            session,
            id,
            input.get("userName"),
            input.get("content")
          )

        );

      write
        (resp,
          HttpServletResponse.SC_OK,
          postDTO
        );
    } catch (Exception e) {
      handleException(resp, e);
    }
  }

  PostDTO addComment(
    Session session,
    Integer id,
    String userName,
    String content
  ) {
    Query<User> userQuery = session.createQuery(
      "from User u where u.name=:name",
      User.class
    );
    userQuery.setParameter("name", userName);
    User user = userQuery.getSingleResult();

    Post post = session.load(Post.class, id);

    Comment comment = new Comment();
    comment.setUser(user);
    comment.setPost(post);
    comment.setContent(content);
    comment.setCreateDate(LocalDateTime.now());

    session.save(comment);

    post.getComments().add(comment);

    return getPost(session, id);
  }
}

Listing 11-29chapter11/src/main/java/chapter11/servlets/AddCommentServlet.java

当然,我们需要进行测试。这也意味着我们有一些服务代理来使服务调用更容易阅读。因此,我们先来看看GetPostServiceAddCommentService,然后是AddCommentServletTest

谢天谢地,服务时间相当短。

package chapter11.servlets;

import java.io.IOException;

import java.net.http.HttpResponse;

public class AddCommentService extends BaseService {
  static HttpResponse<String> addComment(
    Integer id,
    String content,
    String userName
  ) throws IOException, InterruptedException {
    String path = String.format(
      "addcomment?id=%s&content=%s&userName=%s",
      id,
      encode(content),
      encode(userName));
    return issueRequest(path);
  }
}

Listing 11-31chapter11/src/test/java/chapter11/servlets/AddCommentService.java

package chapter11.servlets;

import java.io.IOException;
import java.net.http.HttpResponse;

public class GetPostService extends BaseService {
  static HttpResponse<String> getPost(Integer id)
    throws IOException, InterruptedException {
    return issueRequest(
      String.format("getpost?id=%d", id)
    );
  }

}

Listing 11-30chapter11/src/test/java/chapter11/servlets/GetPostService.java

现在我们开始测试中最有趣的部分(好吧,如果这样的事情很有趣的话):第AddCommentServletTest

有两个测试。设置很简单:createUsersAndPosts()方法设置两个用户和一篇文章并保存文章,这样我们就可以使用它的id来添加评论。

第一个测试testAddComment()加载Post并验证它没有注释,因为它不应该有注释!然后添加一个注释,并验证从AddCommentServlet返回的PostDTO有一个注释;然后,它重复这个过程来验证我们可以按顺序添加多个注释。

最后,它通过GetPostServlet再次加载Post,以确保结果与来自AddCommentServlet的调用相同。

第二个测试稍微简单一点——它获取一个不应该存在的帖子,使用存在的帖子的id作为派生新的id的基础。

package chapter11.servlets;

import chapter11.dto.PostDTO;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;

import static org.testng.Assert.assertEquals;

public class AddCommentServletTest extends TestBase {
  PostDTO post = null;

  @BeforeMethod

  void createUsersAndPosts()
    throws IOException, InterruptedException {
    AddUserService.addUser("jbo");
    AddUserService.addUser("ts");

    HttpResponse<String> postData =
      AddPostService.addPost("raccoons", "raccoons are neat", "jbo");

    // this is how we get the post's id.
    post = mapper.readValue(postData.body(), PostDTO.class);
  }

  @Test
  void testAddComment() throws IOException, InterruptedException {
    HttpResponse<String> response =
      GetPostService.getPost(post.getId());
    validatePost(response, 0);

    response = AddCommentService.addComment(
      post.getId(),
      "what's the deal with raccoons, really",
      "ts"
    );
    assertEquals(response.statusCode(), 200);
    validatePost(response, 1);

    response = AddCommentService.addComment(
      post.getId(),
      "they're the coolest",
      "jbo"
    );
    assertEquals(response.statusCode(), 200);
    validatePost(response, 2);

    response =
      GetPostService.getPost(post.getId());
    validatePost(response, 2);
  }

  @Test
  void testInvalidGetPost()
    throws IOException, InterruptedException {

    HttpResponse<String> response =
      GetPostService.getPost(post.getId() + 1);

    assertEquals(
      response.statusCode(),
      HttpServletResponse.SC_NOT_FOUND
    );
  }

  void validatePost(
    HttpResponse<String> response,
    int commentSize
  ) throws IOException {
    assertEquals(response.statusCode(), 200);

    PostDTO retrieved = mapper.readValue(response.body(), PostDTO.class);

    assertEquals(retrieved.getComments().size(), commentSize);
    assertEquals(retrieved.getTitle(), "raccoons");
  }
}

Listing 11-32chapter11/src/test/java/chapter11/servlets/AddCommentServletTest.java

摘要

这是一个巨大的篇章!我们用它来演示如何将 Hibernate 集成到一个或多或少可以工作的管理博客的 web 应用中;这不是一个好的应用,但是可以作为一个应用的基础。

我们学到了很多东西:我们已经了解了如何设置一个嵌入式 servlet 引擎(Undertow ),我们已经了解了会话划分和一种很好地管理它的方法,我们还了解了整个过程的一组相当详尽的测试。

不得不说,这一章中的大部分代码并不特别是有用;有了足够的努力和意图,可以让变得有用,但是我们在这里真正寻找的是理解一些关于跨架构墙传递数据的问题,例如,从一个实体到 JSON。

我们接下来的章节将集中在将 Hibernate 集成到一个很可能在现实世界中遇到的框架中。

Footnotes 1

我认为我说“直接公开 Hibernate 实体是个坏主意”破坏了整个章节。哎呦。

  2

Java 2 企业版通常被称为 J2EE,最终成为 Java EE,因为没有人真正理解“2”的原因——是的,有一个——并且最近迁移到“Jakarta EE”名称。

  3

Apache ECS 已经被弃用很多年了,但是如果你对探索历史非常感兴趣,它仍然在网上的https://projects.apache.org/project.html?attic-ecs——只是要知道现在有更好的方法。

  4

如果你想在 JVM 上建立一个 web 应用编程书籍的图书馆,请参见www.apress.com——那里有很多参考资料!

  5

我打赌你几乎忘记了这是一本 Hibernate 书,是吗?

  6

当然,这本书是用 AsciiDoctor 编写的——它本身可以生成一个静态站点。

  7

日期和时间 API 是 JSR-310,来自 www.jcp.org/en/jsr/detail?id=310 ,在相当多的版本中都是 JVM 标准运行时库的一部分。然而,Jackson 也兼容旧的JVM,因此我们需要启用对 JSR-310 类型的支持。

  8

集成测试将涉及到容器中的正式部署,或者可能是像我们的测试工具一样包含 Undertow 的主要入口点,但是我们不打算在这里这样做。毕竟,我们还在使用嵌入式数据库。

  9

精明或有经验的 Java EE 程序员会认识到,servlets.json基本上是对web.xml的替代,Undertow 本身并不使用它。我们本可以在类路径中扫描 servlets 毕竟,@WebServlet是一个存在的注释——但那会更复杂。

  10

在编程中,很难说一个解决方案是绝对是更好。我本打算在这个脚注中对我不喜欢的东西进行一些廉价的攻击,但我忍住了,因为我想象着我的编辑因为我不喜欢THIS HAS BEEN REDACTED, JOSEPH而对我大吼大叫。

  11

我们在这一章所做的是平衡这些方法;每个实体只有一个 DTO,默认情况下,DTO 由“空数据”填充。

  12

“分界”是事物之间的分界线,所以当我们提到 Hibernate 的“会话分界”时,我们指的是事物何时变成托管或非托管。

  13

老实说:我希望有一种方法可以遵循“好的设计,而不需要在书上再增加几百页。我把自己逼疯了,试图找出做出什么样的妥协,在“有效教学”和“编写我在现实世界中拒绝的代码”之间找到平衡。“如果你想抱怨所有的设计捷径,我完全理解,相信我。但是我想请你考虑如何保持内容的可读性,同时举例说明我们试图讨论的概念;如果你有更好的解决方案,不会增加不可接受的长度,请告诉我,我会在下一版书中使用它。

  14

同样,这里有一个平衡。我们本来可以创建另一个类来封装 get aUser,但是这意味着又一个代码清单,以及每一个其他类似的调用,这可能已经让许多读者不知所措了。

  15

handleException()方法实际上是ServletBase中的一个很好的候选方法,但是我们试图在遇到它时引入复杂性,而不是一下子引入。

 

********