RESTEasy Classic
|
This guide is about RESTEasy Classic which used to be the default JAX-RS implementation until Quarkus 2.8. It is now recommended to use RESTEasy Reactive, which supports equally well traditional blocking workloads and reactive workloads. For more information about RESTEasy Reactive, please see the introductory REST JSON guide or the more detailed RESTEasy Reactive guide. |
| there is another guide if you need a REST client based on RESTEasy Classic (including support for JSON). |
创建Maven项目
首先,我们需要一个新的项目。使用以下命令创建一个新的项目:
This command generates a new project importing the RESTEasy/JAX-RS and Jackson extensions, and in particular adds the following dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-resteasy-jackson")
|
为了提高用户体验,Quarkus注册了三个Jackson Java 8模块 ,所以你不需要手动操作。 |
Quarkus also supports JSON-B so, if you prefer JSON-B over Jackson, you can create a project relying on the RESTEasy JSON-B extension instead:
This command generates a new project importing the RESTEasy/JAX-RS and JSON-B extensions, and in particular adds the following dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
implementation("io.quarkus:quarkus-resteasy-jsonb")
创建你的第一个JSON REST服务
在这个例子中,我们将创建一个应用程序来管理fruit列表。
首先,让我们创建 Fruit 实体类,如下所示:
package org.acme.rest.json;
public class Fruit {
public String name;
public String description;
public Fruit() {
}
public Fruit(String name, String description) {
this.name = name;
this.description = description;
}
}
这非常的简单。需要注意的一件事是, JSON 序列化层需要具有默认构造函数。
现在,创建 org.acme.rest.json.FruitResource 类,如下所示。
package org.acme.rest.json;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Set;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@Path("/fruits")
public class FruitResource {
private Set<Fruit> fruits = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap<>()));
public FruitResource() {
fruits.add(new Fruit("Apple", "Winter fruit"));
fruits.add(new Fruit("Pineapple", "Tropical fruit"));
}
@GET
public Set<Fruit> list() {
return fruits;
}
@POST
public Set<Fruit> add(Fruit fruit) {
fruits.add(fruit);
return fruits;
}
@DELETE
public Set<Fruit> delete(Fruit fruit) {
fruits.removeIf(existingFruit -> existingFruit.name.contentEquals(fruit.name));
return fruits;
}
}
The implementation is pretty straightforward, and you just need to define your endpoints using the JAX-RS annotations.
|
When a JSON extension is installed such as If you don’t want JSON by default you can set If you don’t rely on the JSON default, it is heavily recommended to annotate your endpoints with the |
配置JSON支持
Jackson
在Quarkus中,通过CDI获得的默认Jackson ObjectMapper (并由Quarkus扩展使用)被配置为忽略未知属性(通过禁用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 功能)。
You can restore the default behavior of Jackson by setting quarkus.jackson.fail-on-unknown-properties=true in your application.properties
or on a per-class basis via @JsonIgnoreProperties(ignoreUnknown = false).
此外, ObjectMapper 被配置为ISO-8601的日期和时间格式(通过禁用 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS 功能)。
Jackson的默认行为可以通过在你的 application.properties 中设置 quarkus.jackson.write-dates-as-timestamps=true 来设置。如果你想改变单个字段的默认行为,你可以使用 @JsonFormat 注解。
另外,Quarkus使得通过CDI beans来配置各种Jackson设置变得非常容易。最简单的(也是建议的)方法是定义一个类型为 io.quarkus.jackson.ObjectMapperCustomizer 的CDI Bean,在其中可以使用任何Jackson配置。
需要注册自定义模块的示例如下所示:
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;
import javax.inject.Singleton;
@Singleton
public class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer {
public void customize(ObjectMapper mapper) {
mapper.registerModule(new CustomModule());
}
}
如果用户选择的话,他们甚至可以提供自己的 ObjectMapper bean。如果这样做,在产生 ObjectMapper 的CDI生产者中,手动注入和应用所有 io.quarkus.jackson.ObjectMapperCustomizer Bean是非常重要的。如果不这样做,就会阻止各种扩展所提供的Jackson特定的自定义功能被应用。
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;
import javax.enterprise.inject.Instance;
import javax.inject.Singleton;
public class CustomObjectMapper {
// Replaces the CDI producer for ObjectMapper built into Quarkus
@Singleton
ObjectMapper objectMapper(Instance<ObjectMapperCustomizer> customizers) {
ObjectMapper mapper = myObjectMapper(); // Custom `ObjectMapper`
// Apply all ObjectMapperCustomizer beans (incl. Quarkus)
for (ObjectMapperCustomizer customizer : customizers) {
customizer.customize(mapper);
}
return mapper;
}
}
JSON-B
如上所述,Quarkus通过使用 quarkus-resteasy-jsonb 扩展提供了使用JSON-B而不是Jackson的选项。
按照上一节所述的相同方法,JSON-B可以使用 io.quarkus.jsonb.JsonbConfigCustomizer bean进行配置。
例如,如果需要使用 JSON-B 注册类型为 com.example.Foo 的名为 FooSerializer 的自定义序列化程序,则添加如下所示的 bean 就足够了:
import io.quarkus.jsonb.JsonbConfigCustomizer;
import javax.inject.Singleton;
import javax.json.bind.JsonbConfig;
import javax.json.bind.serializer.JsonbSerializer;
@Singleton
public class FooSerializerRegistrationCustomizer implements JsonbConfigCustomizer {
public void customize(JsonbConfig config) {
config.withSerializers(new FooSerializer());
}
}
一个更高级的选择是直接提供一个 javax.json.bind.JsonbConfig 的bean(具有 Dependent 范围),或者在极端情况下,提供一个 javax.json.bind.Jsonb 的bean(具有 Singleton 范围)。如果采用后一种方法,那么在产成 javax.json.bind.Jsonb 的CDI生产者中手动注入和应用所有 io.quarkus.jsonb.JsonbConfigCustomizer Bean是非常重要的。如果不这样做,就会阻止由各种扩展提供的JSON-B特定的自定义功能被应用。
import io.quarkus.jsonb.JsonbConfigCustomizer;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Instance;
import javax.json.bind.JsonbConfig;
public class CustomJsonbConfig {
// Replaces the CDI producer for JsonbConfig built into Quarkus
@Dependent
JsonbConfig jsonConfig(Instance<JsonbConfigCustomizer> customizers) {
JsonbConfig config = myJsonbConfig(); // Custom `JsonbConfig`
// Apply all JsonbConfigCustomizer beans (incl. Quarkus)
for (JsonbConfigCustomizer customizer : customizers) {
customizer.customize(config);
}
return config;
}
}
JSON Hypertext Application Language (HAL) support
The HAL standard is a simple format to represent web links.
To enable the HAL support, add the quarkus-hal extension to your project. Also, as HAL needs JSON support, you need to add either the quarkus-resteasy-jsonb or the quarkus-resteasy-jackson extension.
| GAV | Usage |
|---|---|
|
After adding the extensions, we can now annotate the REST resources to produce the media type application/hal+json (or use RestMediaType.APPLICATION_HAL_JSON). For example:
@Path("/records")
public class RecordsResource {
@GET
@Produces({ MediaType.APPLICATION_JSON, "application/hal+json" })
@LinkResource(entityClassName = "org.acme.Record", rel = "list")
public List<TestRecord> getAll() {
// ...
}
@GET
@Path("/first")
@Produces({ MediaType.APPLICATION_JSON, "application/hal+json" })
@LinkResource(rel = "first")
public TestRecord getFirst() {
// ...
}
}
Now, the endpoints /records and /records/first will accept the media type both json and hal+json to print the records in Hal format.
For example, if we invoke the /records endpoint using curl to return a list of records, the HAL format will look like as follows:
& curl -H "Accept:application/hal+json" -i localhost:8080/records
{
"_embedded": {
"items": [
{
"id": 1,
"slug": "first",
"value": "First value",
"_links": {
"list": {
"href": "http://localhost:8081/records"
},
"first": {
"href": "http://localhost:8081/records/first"
}
}
},
{
"id": 2,
"slug": "second",
"value": "Second value",
"_links": {
"list": {
"href": "http://localhost:8081/records"
},
"first": {
"href": "http://localhost:8081/records/first"
}
}
}
]
},
"_links": {
"list": {
"href": "http://localhost:8081/records"
}
}
}
When we call a resource /records/first that returns only one instance, then the output is:
& curl -H "Accept:application/hal+json" -i localhost:8080/records/first
{
"id": 1,
"slug": "first",
"value": "First value",
"_links": {
"list": {
"href": "http://localhost:8081/records"
},
"first": {
"href": "http://localhost:8081/records/first"
}
}
}
创建一个网页
现在让我们添加一个简单的网页来与我们的 FruitResource 进行交互。Quarkus自动提供位于 META-INF/resources 目录下的静态资源。在 src/main/resources/META-INF/resources 目录中,添加一个 fruits.html 文件,其中包含这个https://github.com/quarkusio/quarkus-quickstarts/blob/2.16/rest-json-quickstart/src/main/resources/META-INF/resources/fruits.html[fruits.html] 文件的内容。
现在你可以与你的REST服务进行交互:
-
启动Quarkus:
CLIquarkus devMaven./mvnw quarkus:devGradle./gradlew --console=plain quarkusDev -
打开浏览器访问
<a href="http://localhost:8080/fruits.html" class="bare">http://localhost:8080/fruits.html</a> -
通过表格添加新的fruits到列表中
构建一个本地可执行文件
你可以使用常用命令构建本机可执行文件:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
运行它就像执行 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner 一样简单。
然后你可以使用的浏览器访问 <a href="http://localhost:8080/fruits.html" class="bare">http://localhost:8080/fruits.html</a> 来使用你的应用程序。
关于序列化
JSON序列化库使用Java反射来获取一个对象的属性并将其序列化。
当使用GraalVM的本地可执行文件时,需要注册所有将与反射一起使用的类。好消息是,Quarkus在大多数时候都会为你做这项工作。到目前为止,我们还没有注册任何类,甚至没有注册 Fruit ,并且一切正常。
当Quarkus能够从REST方法中推断出序列化的类型时,它会发挥一些作用。当你有以下的REST方法时,Quarkus确定 Fruit 将被序列化:
@GET
public List<Fruit> list() {
// ...
}
Quarkus通过在构建时分析REST方法自动为你执行此操,这就是为什么我们在本指南的第一部分不需要任何反射注册。
JAX-RS世界中另一个常见的模式是使用 Response 对象。 Response 有一些很好的好处:
-
你可以根据你的方法中发生的情况,返回不同的实体类型(例如,
Legume或Error)。 -
你可以设置
Response的属性(在出现错误的情况下,会想到状态)。
你的 REST 方法如下所示:
@GET
public Response list() {
// ...
}
Quarkus不可能在构建时确定 Response 中包含的类型,因为该信息不可用。在这种情况下,Quarkus将无法自动注册反映所需的类。
这将我们引向下一节。
使用Response
让我们创建一个将被序列化为 JSON 的 Legume 类,遵循与我们的 Fruit 类相同的模型:
package org.acme.rest.json;
public class Legume {
public String name;
public String description;
public Legume() {
}
public Legume(String name, String description) {
this.name = name;
this.description = description;
}
}
现在让我们创建一个 LegumeResource REST服务,它只有一个返回legumes类列表的方法。
该方法返回一个 Response ,而不是一个 Legume 列表。
package org.acme.rest.json;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/legumes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class LegumeResource {
private Set<Legume> legumes = Collections.synchronizedSet(new LinkedHashSet<>());
public LegumeResource() {
legumes.add(new Legume("Carrot", "Root vegetable, usually orange"));
legumes.add(new Legume("Zucchini", "Summer squash"));
}
@GET
public Response list() {
return Response.ok(legumes).build();
}
}
现在让我们添加一个简单的网页来显示我们的legumes列表。在 src/main/resources/META-INF/resources 目录中,添加一个 legumes.html 文件,其中包含这个https://github.com/quarkusio/quarkus-quickstarts/blob/2.16/rest-json-quickstart/src/main/resources/META-INF/resources/legumes.html[legumes.html] 文件的内容。
Open a browser to http://localhost:8080/legumes.html, and you will see our list of legumes.
有趣的部分是在将应用程序作为本机可执行文件运行时开始的:
-
创建本地可执行文件。
CLIquarkus build --nativeMaven./mvnw install -DnativeGradle./gradlew build -Dquarkus.package.type=native -
用以下方式执行它
./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner -
打开浏览器,访问 http://localhost:8080/legumes.html
那里没有legumes。
As mentioned above, the issue is that Quarkus was not able to determine the Legume class will require some reflection by analyzing the REST endpoints.
The JSON serialization library tries to get the list of fields of Legume and gets an empty list, so it does not serialize the fields' data.
|
目前,当JSON-B或Jackson尝试获取一个类的字段列表时,如果该类没有注册反射,则不会抛出异常。GraalVM将简单地返回一个空的字段列表。 希望这在将来会有所改变,使错误更加明显。 |
我们可以通过在我们的 Legume 类上添加 @RegisterForReflection 注解来手动注册 Legume 进行反射:
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class Legume {
// ...
}
@RegisterForReflection 注解指示Quarkus在本地编译过程中保留该类和其成员。关于 @RegisterForReflection 注解的更多细节,请访问 本地应用程序提示 。
|
让我们这样做,并遵循与之前相同的步骤:
-
点击
Ctrl+C,停止应用程序。 -
创建本地可执行文件。
CLIquarkus build --nativeMaven./mvnw install -DnativeGradle./gradlew build -Dquarkus.package.type=native -
用以下方式执行它
./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner -
打开浏览器,访问 http://localhost:8080/legumes.html
这一次,你可以看到我们的legumes列表。
响应式
|
For reactive workloads, please always use RESTEasy Reactive. |
你可以返回 响应式类型 来处理异步处理。Quarkus推荐使用 Mutiny 来编写响应式和异步代码。
To integrate Mutiny and RESTEasy, you need to add the quarkus-resteasy-mutiny dependency to your project:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-mutiny</artifactId>
</dependency>
implementation("io.quarkus:quarkus-resteasy-mutiny")
Then, your endpoint can return Uni or Multi instances:
@GET
@Path("/{name}")
public Uni<Fruit> getOne(@PathParam String name) {
return findByName(name);
}
@GET
public Multi<Fruit> getAll() {
return findAll();
}
当你有一个单一的结果时,使用 Uni 。当你有多个可能被异步发射的项目时,使用 Multi 。
您可以使用 Uni 和 Response 返回异步 HTTP 响应:Uni<Response>。
有关 Mutiny 的更多详细信息,请参见外部参考:Mutiny - 一个直观的响应式编程库。
HTTP filters and interceptors
Both HTTP request and response can be intercepted by providing ContainerRequestFilter or ContainerResponseFilter
implementations respectively. These filters are suitable for processing the metadata associated with a message: HTTP
headers, query parameters, media type, and other metadata. They also have the capability to abort the request
processing, for instance when the user does not have the permissions to access the endpoint.
Let’s use ContainerRequestFilter to add logging capability to our service. We can do that by implementing
ContainerRequestFilter and annotating it with the @Provider annotation:
package org.acme.rest.json;
import io.vertx.core.http.HttpServerRequest;
import org.jboss.logging.Logger;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
@Provider
public class LoggingFilter implements ContainerRequestFilter {
private static final Logger LOG = Logger.getLogger(LoggingFilter.class);
@Context
UriInfo info;
@Context
HttpServerRequest request;
@Override
public void filter(ContainerRequestContext context) {
final String method = context.getMethod();
final String path = info.getPath();
final String address = request.remoteAddress().toString();
LOG.infof("Request %s %s from IP %s", method, path, address);
}
}
Now, whenever a REST method is invoked, the request will be logged into the console:
2019-06-05 12:44:26,526 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /legumes from IP 127.0.0.1
2019-06-05 12:49:19,623 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:50:44,019 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request POST /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:51:04,485 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 127.0.0.1
CORS filter
Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.
Quarkus comes with a CORS filter. Read the HTTP Reference Documentation to learn how to use it.
GZip Support
Quarkus comes with GZip support (even though it is not enabled by default). The following configuration knobs allow to configure GZip support.
quarkus.resteasy.gzip.enabled=true (1)
quarkus.resteasy.gzip.max-input=10M (2)
| 1 | Enable Gzip support. |
| 2 | Configure the upper limit on deflated request body. This is useful to mitigate potential attacks by limiting their reach. The default value is 10M.
This configuration option would recognize strings in this format (shown as a regular expression): [0-9]+[KkMmGgTtPpEeZzYy]?. If no suffix is given, assume bytes. |
Once GZip support has been enabled you can use it on an endpoint by adding the @org.jboss.resteasy.annotations.GZIP annotation to your endpoint method.
The configuration property quarkus.http.enable-compression has no effect on compression support of RESTEasy Classic endpoints.
|
Multipart Support
RESTEasy supports multipart via the RESTEasy Multipart Provider.
Quarkus provides an extension called quarkus-resteasy-multipart to make things easier for you.
This extension slightly differs from the RESTEasy default behavior as the default charset (if none is specified in your request) is UTF-8 rather than US-ASCII.
You can configure this behavior with the following configuration properties:
Configuration property fixed at build time - All other configuration properties are overridable at runtime
类型 |
默认 |
|
|---|---|---|
Default charset. Note that the default value is UTF-8 which is different from RESTEasy’s default value US-ASCII. Environment variable: Show more |
|
|
The default content-type. Environment variable: Show more |
string |
|
Servlet compatibility
In Quarkus, RESTEasy can either run directly on top of the Vert.x HTTP server, or on top of Undertow if you have any servlet dependency.
As a result, certain classes, such as HttpServletRequest are not always available for injection. Most use-cases for this particular
class are covered by JAX-RS equivalents, except for getting the remote client’s IP. RESTEasy comes with a replacement API which you can inject:
HttpRequest, which has the methods
getRemoteAddress()
and getRemoteHost()
to solve this problem.
RESTEasy and REST Client interactions
In Quarkus, the RESTEasy extension and the REST Client extension share the same infrastructure. One important consequence of this consideration is that they share the same list of providers (in the JAX-RS meaning of the word).
For instance, if you declare a WriterInterceptor, it will by default intercept both the servers calls and the client calls,
which might not be the desired behavior.
However, you can change this default behavior and constrain a provider to:
-
only consider server calls by adding the
@ConstrainedTo(RuntimeType.SERVER)annotation to your provider; -
only consider client calls by adding the
@ConstrainedTo(RuntimeType.CLIENT)annotation to your provider.
What’s Different from Jakarta EE Development
No Need for Application Class
Configuration via an application-supplied subclass of Application is supported, but not required.
Only a single JAX-RS application
In contrast to JAX-RS (and RESTeasy) running in a standard servlet-container, Quarkus only supports the deployment of a single JAX-RS application.
If multiple JAX-RS Application classes are defined, the build will fail with the message Multiple classes have been annotated with @ApplicationPath which is currently not supported.
If multiple JAX-RS applications are defined, the property quarkus.resteasy.ignore-application-classes=true can be used to ignore all explicit Application classes. This makes all resource-classes available via the application-path as defined by quarkus.resteasy.path (default: /).
Support limitations of JAX-RS application
The RESTEasy extension doesn’t support the method getProperties() of the class javax.ws.rs.core.Application. Moreover, it only relies on the methods getClasses() and getSingletons() to filter out the annotated resource, provider and feature classes.
It doesn’t filter out the built-in resource, provider and feature classes and also the resource, provider and feature classes registered by the other extensions.
Finally, the objects returned by the method getSingletons() are ignored, only the classes are taken into account to filter out the resource, provider and feature classes, in other words the method getSingletons() is actually managed the same way as getClasses().
Lifecycle of Resources
In Quarkus all JAX-RS resources are treated as CDI beans.
It’s possible to inject other beans via @Inject, bind interceptors using bindings such as @Transactional, define @PostConstruct callbacks, etc.
If there is no scope annotation declared on the resource class then the scope is defaulted.
The default scope can be controlled through the quarkus.resteasy.singleton-resources property.
If set to true (default) then a single instance of a resource class is created to service all requests (as defined by @javax.inject.Singleton).
If set to false then a new instance of the resource class is created per each request.
An explicit CDI scope annotation (@RequestScoped, @ApplicationScoped, etc.) always overrides the default behavior and specifies the lifecycle of resource instances.
Include/Exclude JAX-RS classes with build time conditions
Quarkus enables the inclusion or exclusion of JAX-RS Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans.
Thus, the various JAX-RS classes can be annotated with profile conditions (@io.quarkus.arc.profile.IfBuildProfile or @io.quarkus.arc.profile.UnlessBuildProfile) and/or with property conditions (io.quarkus.arc.properties.IfBuildProperty or io.quarkus.arc.properties.UnlessBuildProperty) to indicate to Quarkus at build time under which conditions these JAX-RS classes should be included.
In the following example, Quarkus includes the endpoint sayHello if and only if the build profile app1 has been enabled.
@IfBuildProfile("app1")
public class ResourceForApp1Only {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
Please note that if a JAX-RS Application has been detected and the method getClasses() and/or getSingletons() has/have been overridden, Quarkus will ignore the build time conditions and consider only what has been defined in the JAX-RS Application.