Generating Jakarta REST resources with Panache

很多 Web 应用程序都是单调的 CRUD 应用程序,包含非常乏味的 REST API。为了精简此任务,具有 Panache 的 REST 数据扩展可以为你的实体和存储库生成基本的 CRUD 终结点。 虽然此扩展仍然处于试验阶段且提供的功能集有限,但我们希望尽早收到反馈。目前,此扩展支持带 Panache 的 Hibernate ORM 和 MongoDB,且可以生成适用于 application/jsonapplication/hal+json 内容的 CRUD 资源。

Setting up REST Data with Panache

Quarkus 提供了以下扩展来设置 REST Data with Panache。请查看下一兼容性表格,以根据你正在使用的技术来使用正确的扩展:

Table 1. Compatibility Table
Extension Hibernate RESTEasy

hr-hibernate-orm

ORM

Classic and Reactive

hr-hibernate-reactive

Reactive

Reactive

hr-mongodb

ORM

Classic and Reactive

Hibernate ORM

  • 将所需的依赖项添加到构建文件中

    • 具有 Panache 扩展 Hibernate ORM REST Data(quarkus-hibernate-orm-rest-data-panache

    • JDBC 驱动程序扩展(quarkus-jdbc-postgresqlquarkus-jdbc-h2quarkus-jdbc-mariadb,…​)

    • 一个 RESTEasy JSON 序列化扩展(该扩展同时支持 Quarkus REST(以前为 RESTEasy Reactive)和 RESTEasy Classic)

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-rest-data-panache</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

<!-- Use this if you are using Quarkus REST -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>

<!-- Use this if you are going to use RESTEasy Classic -->
<!--
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
-->
build.gradle
implementation("io.quarkus:quarkus-hibernate-orm-rest-data-panache")
implementation("io.quarkus:quarkus-jdbc-postgresql")

// Use this if you are using Quarkus REST
implementation("io.quarkus:quarkus-rest-jackson")

// Use this if you are going to use RESTEasy Classic
// implementation("io.quarkus:quarkus-resteasy-jackson")

若要查看具有 Panache 的 Hibernate ORM REST Data 的实际操作情况,请查看 hibernate-orm-rest-data-panache-quickstart 快速启动。

Hibernate Reactive

  • 将所需依赖项添加到您的 pom.xml

    • 具有 Panache 扩展的 Hibernate Reactive REST Data(quarkus-hibernate-reactive-rest-data-panache

    • Vert.x 反应式数据库驱动程序扩展(quarkus-reactive-pg-clientquarkus-reactive-mysql-client,…​)

    • 一个 Quarkus REST 序列化扩展(quarkus-rest-jsonbquarkus-rest-jackson,…​)

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-reactive-rest-data-panache</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-reactive-pg-client</artifactId>
    </dependency>
   <!-- Use this if you are using REST Jackson for serialization -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest-jackson</artifactId>
    </dependency>
</dependencies>

MongoDB

  • 将所需的依赖项添加到构建文件中

    • 具有 Panache 扩展的 MongoDB REST Data(quarkus-mongodb-rest-data-panache

    • 一个 RESTEasy JSON 序列化扩展(quarkus-rest-jacksonquarkus-rest-jsonb

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-mongodb-rest-data-panache</artifactId>
</dependency>

<!-- Use this if you are using Quarkus REST -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>

<!-- Use this if you are going to use RESTEasy Classic -->
<!--
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
-->
build.gradle
implementation("io.quarkus:quarkus-mongodb-rest-data-panache")

// Use this if you are using Quarkus REST
implementation("io.quarkus:quarkus-rest-jackson")

// Use this if you are going to use RESTEasy Classic
// implementation("io.quarkus:quarkus-resteasy-jackson")

Generating resources

与 Panache 结合使用的 REST Data 会根据应用程序中可用的接口生成 Jakarta REST 资源。对于每一个您要生成的实体和存储库,请提供一个资源接口。_Do not implement these interfaces and don’t provide custom methods because they will be ignored._但是,您可以覆盖扩展接口的方法以对其进行自定义(请参阅结尾部分)。

PanacheEntityResource

如果您的应用程序有扩展 PanacheEntityPanacheEntityBase 类的实体(例如 Person),您可以指示与 Panache 结合使用的 REST Data 利用以下接口为其生成 Jakarta REST 资源:

public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}

PanacheRepositoryResource

如果您的应用程序有简单实体(例如 Person)和实现 PanacheRepositoryPanacheRepositoryBase 接口的存储库(例如 PersonRepository),则可以指示与 Panache 结合使用的 REST Data 使用以下接口为其生成 Jakarta REST 资源:

public interface PeopleResource extends PanacheRepositoryResource<PersonRepository, Person, Long> {
}

PanacheMongoEntityResource

如果您的应用程序有一个扩展了 PanacheMongoEntity`或 `PanacheMongoEntityBase`类的实体(例如 `Person),您可以指示 REST Data 与 Panache 使用以下接口生成其 Jakarta REST 资源:

public interface PeopleResource extends PanacheMongoEntityResource<Person, Long> {
}

PanacheMongoRepositoryResource

如果您的应用程序有一个简单的实体(例如 Person)和一个实现了 PanacheMongoRepository`或 `PanacheMongoRepositoryBase`接口的存储库(例如 `PersonRepository),您可以指示 REST Data 与 Panache 使用以下接口生成其 Jakarta REST 资源:

public interface PeopleResource extends PanacheMongoRepositoryResource<PersonRepository, Person, Long> {
}

The generated resource

生成的资源对于实体和存储库来说在功能上是等效的。唯一的区别是正在使用的数据访问模式和数据存储。

如果您已定义了上面提到的 `PeopleResource`接口之一,此扩展将使用特定数据访问策略生成其实现。然后,已实现的类将由生成的 Jakarta REST 资源使用,该资源看起来像这样:

public class PeopleResourceJaxRs { // The actual class name is going to be unique
    @Inject
    PeopleResource resource;

    @GET
    @Path("{id}")
    @Produces("application/json")
    public Person get(@PathParam("id") Long id){
        Person person = resource.get(id);
        if (person == null) {
            throw new WebApplicationException(404);
        }
        return person;
    }

    @GET
    @Produces("application/json")
    public Response list(@QueryParam("sort") List<String> sortQuery,
            @QueryParam("page") @DefaultValue("0") int pageIndex,
            @QueryParam("size") @DefaultValue("20") int pageSize) {
        Page page = Page.of(pageIndex, pageSize);
        Sort sort = getSortFromQuery(sortQuery);
        List<Person> people = resource.list(page, sort);
        // ... build a response with page links and return a 200 response with a list
    }

    @GET
    @Path("/count")
    public long count() {
        return resource.count();
    }

    @Transactional
    @POST
    @Consumes("application/json")
    @Produces("application/json")
    public Response add(Person personToSave) {
        Person person = resource.add(person);
        // ... build a new location URL and return 201 response with an entity
    }

    @Transactional
    @PUT
    @Path("{id}")
    @Consumes("application/json")
    @Produces("application/json")
    public Response update(@PathParam("id") Long id, Person personToSave) {
        if (resource.get(id) == null) {
            Person person = resource.update(id, personToSave);
            return Response.status(204).build();
        }
        Person person = resource.update(id, personToSave);
        // ... build a new location URL and return 201 response with an entity
    }

    @Transactional
    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") Long id) {
        if (!resource.delete(id)) {
            throw new WebApplicationException(404);
        }
    }
}

Resource customisation

REST Data 与 Panache 提供了 `@ResourceProperties`和 `@MethodProperties`注释,可用于自定义资源的某些功能。

它可以在您的资源接口中使用:

@ResourceProperties(hal = true, path = "my-people")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
    @MethodProperties(path = "all")
    List<Person> list(Page page, Sort sort);

    @MethodProperties(exposed = false)
    boolean delete(Long id);
}

Available options

@ResourceProperties

  • exposed - 资源是否可以公开。一个全局资源属性,可以针对每种方法进行覆盖。默认值是 true

  • path - 资源基本路径。默认路径为小写连字符分隔的资源名称,没有后缀 resource`或 `controller

  • rolesAllowed - 允许访问资源的安全角色列表。它需要 Quarkus 安全扩展来呈现,否则它将被忽略。默认值为空。

  • paged - 集合响应是否应分页。如果存在,则响应头将包含第一页、最后一页、上一页和下一页 URI。请求页面索引和大小取自 page`和 `size`查询参数,它们分别默认为 `0`和 `20。默认值是 true

  • hal - 除了标准 application/json`响应之外,还会生成其他方法,这些方法可以通过 `Accept`头进行请求时返回 `application/hal+json`响应。默认值是 `false

  • halCollectionName - 在生成 hal 集合响应时应使用的名称。默认名称为小写连字符分隔的资源名称,没有后缀 resource`或 `controller

@MethodProperties

  • exposed - 设置为 false`时,不公开特定 HTTP 动词。默认值是 `true

  • path - 操作路径(这是附加到资源基本路径的)。默认值为空字符串。

  • rolesAllowed - 允许访问此操作的安全角色列表。它需要 Quarkus 安全扩展来呈现,否则它将被忽略。默认值为空。

Adding additional methods to the generated resource

您可以通过向资源接口添加这些方法,用 REST Data 和 Panache 扩展来向生成的资源添加其他方法,例如:

@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
    @GET
    @Path("/name/{name}")
    @Produces("application/json")
    default List<Person> findByName(@PathParam("name") String name) {
        return Person.find("name = :name", Collections.singletonMap("name", name)).list();
    }
}

该方法将与使用 `http://localhost:8080/people/name/Johan`生成的方法一起公开。

Securing endpoints

REST Data with Panache 将使用 jakarta.annotation.security 包中在资源接口上定义的安全注释:

import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.RolesAllowed;

@DenyAll
@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
    @RolesAllowed("superuser")
    boolean delete(Long id);
}

此外,如果您只想指定允许使用资源的角色,那么 @ResourceProperties@MethodProperties 注释具有字段 rolesAllowed,以列出允许访问资源或操作的安全角色。

Query parameters to list entities

REST Data with Panache 支持以下查询参数来获取实体列表:

  • page - 列表操作应返回的页码。它仅适用于分页资源,并且是一个从 0 开始的数字。默认值为 0。

  • size - 列表操作应返回的页面大小。它仅适用于分页资源,并且是一个从 1 开始的数字。默认值为 20。

  • sort - 由逗号分隔的字段列表,该列表应用于对列表操作的结果进行排序。除非字段以 - 为前缀,否则字段将按升序排列。例如,?sort=name,-age 将按 name 升序、按 age 降序对结果进行排序。

  • namedQuery - 应使用注释 @NamedQuery 在实体级别配置的命名查询。

例如,如果您想在第一页中获取两个 People 实体,则应调用 http://localhost:8080/people?page=0&size=2,响应应如下所示:

[
  {
    "id": 1,
    "name": "John Johnson",
    "birth": "1988-01-10"
  },
  {
    "id": 2,
    "name": "Peter Peterson",
    "birth": "1986-11-20"
  }
]

此外,您还可以通过添加查询参数(包括字段名称和值)按实体字段进行筛选,例如,调用 http://localhost:8080/people?name=Peter Peterson 将返回:

[
  {
    "id": 2,
    "name": "Peter Peterson",
    "birth": "1986-11-20"
  }
]

仅支持按 String、Boolean、Character、Double、Float、Integer、Long、Short、Byte 和基本类型按字段进行筛选。

Complex filtering to list entities using @NamedQuery

列出实体时,您可以指定一个命名查询以进行筛选。例如,在实体中具有以下命名查询:

@Entity
@NamedQuery(name = "Person.containsInName", query = "from Person where name like CONCAT('%', CONCAT(:name, '%'))")
public class Person extends PanacheEntity {
  String name;
}

在此示例中,我们添加了一个命名查询,以列出 name 字段中包含某些文本的所有人员。

接下来,我们可以在使用生成的资源列出实体时设置一个查询参数 namedQuery,其中包括我们要使用的命名查询的名称,例如,调用 http://localhost:8080/people?namedQuery=Person.containsInName&name=ter 将返回所有姓名包含文本 "ter" 的人。

有关命名查询的工作原理的更多信息,请参阅 Hibernate ORM 指南或 Hibernate Reactive 指南。

Resource Method Before/After Listeners

REST Data with Panache 支持订阅以下资源方法钩子:

  • Before/After add resource

  • Before/After update resource

  • Before/After delete resource

要注册资源方法侦听器,您需要提供一个实现接口 RestDataResourceMethodListener 的 bean,例如:

@ApplicationScoped
public class PeopleRestDataResourceMethodListener implements RestDataResourceMethodListener<Person> {
    @Override
    public void onBeforeAdd(Person person) {
        System.out.println("Before Save Person: " + person.name);
    }
}

Response body examples

如上所述,REST Data with Panache 支持 application/jsonapplication/hal+json 响应内容类型。以下是如何为 getlist 操作响应主体的示例,假设数据库中有五个 Person 记录。

GET /people/1

Accept: application/json

{
  "id": 1,
  "name": "John Johnson",
  "birth": "1988-01-10"
}

Accept: application/hal+json

{
  "id": 1,
  "name": "John Johnson",
  "birth": "1988-01-10",
  "_links": {
    "self": {
      "href": "http://example.com/people/1"
    },
    "remove": {
      "href": "http://example.com/people/1"
    },
    "update": {
      "href": "http://example.com/people/1"
    },
    "add": {
      "href": "http://example.com/people"
    },
    "list": {
      "href": "http://example.com/people"
    }
  }
}

GET /people?page=0&size=2

Accept: application/json

[
  {
    "id": 1,
    "name": "John Johnson",
    "birth": "1988-01-10"
  },
  {
    "id": 2,
    "name": "Peter Peterson",
    "birth": "1986-11-20"
  }
]

Accept: application/hal+json

{
  "_embedded": [
    {
      "id": 1,
      "name": "John Johnson",
      "birth": "1988-01-10",
      "_links": {
        "self": {
          "href": "http://example.com/people/1"
        },
        "remove": {
          "href": "http://example.com/people/1"
        },
        "update": {
          "href": "http://example.com/people/1"
        },
        "add": {
          "href": "http://example.com/people"
        },
        "list": {
          "href": "http://example.com/people"
        }
      }
    },
    {
      "id": 2,
      "name": "Peter Peterson",
      "birth": "1986-11-20",
      "_links": {
        "self": {
          "href": "http://example.com/people/2"
        },
        "remove": {
          "href": "http://example.com/people/2"
        },
        "update": {
          "href": "http://example.com/people/2"
        },
        "add": {
          "href": "http://example.com/people"
        },
        "list": {
          "href": "http://example.com/people"
        }
      }
    }
  ],
  "_links": {
    "add": {
      "href": "http://example.com/people"
    },
    "list": {
      "href": "http://example.com/people"
    },
    "first": {
      "href": "http://example.com/people?page=0&size=2"
    },
    "last": {
      "href": "http://example.com/people?page=2&size=2"
    },
    "next": {
      "href": "http://example.com/people?page=1&size=2"
    }
  }
}

这两个响应都将包含以下标头:

将不会包含 previous 链接标头(以及 hal 链接),因为不存在前一页。

Include/Exclude Jakarta REST classes

Using Build time conditions

得益于在构建时的条件,Quarkus 可以直接启用或禁用 Jakarta REST 资源、提供商和特性,与 CDI Bean 相同。因此,可以使用配置文件条件 (@io.quarkus.arc.profile.IfBuildProfile@io.quarkus.arc.profile.UnlessBuildProfile) 和/或属性条件 (io.quarkus.arc.properties.IfBuildPropertyio.quarkus.arc.properties.UnlessBuildProperty) 对 REST 数据和 Panache 接口进行注释,以在构建时向 Quarkus 指示生成的 Jakarta REST 类应在哪些条件下包含。

在以下示例中,Quarkus 将仅当启用了构建配置文件 app1 时才包含从 PeopleResource 接口生成的资源。

@IfBuildProfile("app1")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}

Using a runtime property

仅在使用 Quarkus REST Quarkus 拓展时才可用此选项。

Quarkus 还可基于使用 @io.quarkus.resteasy.reactive.server.EndpointDisabled 注释的运行时属性的值有条件地禁用生成的 Jakarta REST 资源。

在以下示例中,如果应用程序已将 some.property 配置为 "disable",则 Quarkus 将在运行时从 PeopleResource 接口排除生成的资源。

@EndpointDisabled(name = "some.property", stringValue = "disable")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}