摘要
Apache Camel支持多种方法来定义REST服务。特别是,Apache Camel提供了REST DSL(特定于域的语言),这是一种简单但功能强大的流畅API,可以在任何REST组件上分层并提供与Swagger的集成。
Camel中的REST概述
概览
Apache Camel提供了许多用于在Camel应用程序中定义REST服务的方法和组件。本节提供了这些不同方法和组件的快速概述,以便您可以确定最适合您要求的实现和API。
什么是REST?
具象状态传输(REST)是一种用于分布式应用的架构,各地的HTTP数据传输中心,只用了四个基本的HTTP动词:GET,POST,PUT,和DELETE。
与像SOAP这样的将HTTP视为SOAP消息的单纯传输协议的协议相反,REST体系结构直接利用了HTTP。关键的见解是,HTTP协议本身(通过一些简单的约定进行了扩展)非常适合用作分布式应用程序的框架。
REST调用示例
由于REST体系结构是围绕标准HTTP动词构建的,因此在许多情况下,您可以将常规浏览器用作REST客户端。例如,要调用在主机和端口上运行的简单Hello World REST服务localhost:9091,可以在浏览器中导航至如下所示的URL:
http://localhost:9091/say/hello/Garp
然后,Hello World REST服务可能会返回响应字符串,例如:
Hello Garp
这将显示在浏览器窗口中。仅使用标准浏览器(或curl命令行实用程序)就可以轻松调用REST服务,这是REST协议迅速普及的众多原因之一。
REST包装器层
以下REST包装器层提供了用于定义REST服务的简化语法,并且可以在不同REST实现的基础上分层:
-
REST DSL
REST DSL(位于
camel-core)是外观或包装层,它提供了用于定义REST服务的简化构建器API。REST DSL本身并不提供REST实现:它必须与基础REST实现结合使用。例如,以下Java代码显示了如何使用REST DSL 定义简单的Hello World服务:rest("/say") .get("/hello/{name}").route().transform().simple("Hello ${header.name}"); -
REST 组件
Rest组件(位于
camel-core)是包装层,使您可以使用URI语法定义REST服务。像REST DSL一样,Rest组件本身并不提供REST实现。它必须与基础REST实现结合使用。如果未明确配置HTTP传输组件,则REST DSL通过检查类路径上的可用组件来自动发现要使用的HTTP组件。REST DSL查找任何HTTP组件的默认名称,并使用找到的第一个名称。如果类路径上没有HTTP组件,并且您未明确配置HTTP传输,则默认HTTP组件为camel-http。注意
自动发现要使用的HTTP组件的功能是Camel 2.18中的新增功能。在Camel 2.17中不可用。
以下Java代码显示了如何使用camel-rest组件定义简单的Hello World服务:
from("rest:get:say:/hello/{name}").transform().simple("Hello ${header.name}");
REST实现
Apache Camel通过以下组件提供了几种不同的REST实现:
-
Spark-Rest 组件
Spark-Rest组件(位于
camel-spark-rest)是一个REST实现,使您能够使用URI语法定义REST服务。在Spark框架本身是一个Java API,它是松散的基础上Sinatra框架(一个Python API)。例如,以下Java代码显示了如何使用Spark-Rest组件定义简单的Hello World服务:from("spark-rest:get:/say/hello/:name").transform().simple("Hello ${header.name}");注意,在对比的是休息部件,在URI的变量的语法是
:name代替{name}。注意
Spark-Rest组件需要Java 8。
-
Restlet 组件
Restlet组件(位于
camel-restlet)是一个REST实现,原则上可以在不同的传输协议之上分层(尽管仅针对HTTP协议对该组件进行了测试)。该组件还提供了与Restlet Framework的集成,Restlet Framework是用于以Java开发REST服务的商业框架。例如,以下Java代码显示了如何使用Restlet组件定义简单的Hello World服务:from("restlet:http://0.0.0.0:9091/say/hello/{name}?restletMethod=get") .transform().simple("Hello ${header.name}"); -
Servlet 组件
Servlet组件(位于
camel-servlet)是将Java Servlet绑定到Camel路由的组件。换句话说,Servlet组件使您可以打包和部署Camel路由,就好像它是标准Java Servlet一样。因此,如果您需要在Servlet容器内部署Camel路由(例如,部署到Apache Tomcat HTTP服务器或JBoss Enterprise Application Platform容器中),则 Servlet组件特别有用。但是,Servlet组件本身并没有提供任何方便的REST API来定义REST服务。因此,使用Servlet组件的最简单方法是将其与REST DSL组合在一起,以便您可以使用用户友好的API定义REST服务。
JAX-RS REST实现
JAX-RS(用于RESTful Web服务的Java API)是用于将REST请求绑定到Java对象的框架,其中Java类必须用JAX-RS批注装饰以定义绑定。JAX-RS框架相对成熟,并提供了用于开发REST服务的复杂框架,但是编程也有些复杂。
与Apache Camel的JAX-RS集成由CXFRS组件实现,该组件位于Apache CXF之上。概括而言,JAX-RS使用以下注释将REST请求绑定到Java类(其中,这只是许多可用注释的不完整示例):
-
@Path
可以将上下文路径映射到Java类或将子路径映射到特定Java方法的注释。
-
@ GET,@ POST,@ PUT,@ DELETE
将HTTP方法映射到Java方法的注释。
-
@PathParam
将URI参数映射到Java方法参数,或将URI参数注入字段的注释。
-
@QueryParam
将查询参数映射到Java方法参数或将查询参数注入字段的注释。
通常,REST请求或REST响应的主体应采用JAXB(XML)数据格式。但是Apache CXF还支持将JSON格式转换为JAXB格式,因此也可以解析JSON消息。
注意
CXFRS组件未与REST DSL集成。
使用REST DSL定义服务
REST DSL是一个外观
REST DSL实际上是一种外观,它提供了用于在Java DSL或XML DSL(特定于域的语言)中定义REST服务的简化语法。REST DSL实际上并不提供REST实现,它只是现有 REST实现(在Apache Camel中有多个)的包装。
REST DSL的优势
REST DSL包装器层具有以下优点:
- 用于定义REST服务的现代易用语法。
- 与多个不同的Apache Camel组件兼容。
- Swagger集成(通过
camel-swagger组件)。
与REST DSL集成的组件
由于REST DSL不是实际的REST实现,因此您需要做的第一件事就是选择一个Camel组件来提供基础实现。目前,以下Camel组件已与REST DSL集成:
- Servlet组件(
camel-servlet)。 - Spark REST组件(
camel-spark-rest)。 - Netty4 HTTP组件(
camel-netty4-http)。 - jetty组件(
camel-jetty)。 - restlet组件(
camel-restlet)。
注意
Rest组件(位于
camel-core)不是REST实现。像REST DSL一样,Rest组件也是一个外观,它提供了简化的语法以使用URI语法定义REST服务。Rest组件还需要底层的REST实现。
配置REST DSL以使用REST实现
要指定REST实现,请使用restConfiguration()构建器(在Java DSL中)或restConfiguration元素(在XML DSL中)。例如,要将REST DSL配置为使用Spark-Rest组件,您将在Java DSL中使用类似于以下内容的构建器表达式:
restConfiguration().component("spark-rest").port(9091);
并且您将camelContext在XML DSL中使用如下元素(作为的子元素):
<restConfiguration component="spark-rest" port="9091"/>
语法
用于定义REST服务的Java DSL语法如下:
rest("BasePath").Option().
.Verb("Path").Option().[to() | route().CamelRoute.endRest()]
.Verb("Path").Option().[to() | route().CamelRoute.endRest()]
...
.Verb("Path").Option().[to() | route().CamelRoute];
其中CamelRoute是可选的嵌入式骆驼路由(使用标准Java DSL语法定义路由)。
REST服务定义以rest()关键字开头,后跟一个或多个处理特定URL路径段的动词子句。的HTTP谓词可以是一个get(),head(),put(),post(),delete(),patch()或verb()。每个动词子句都可以使用以下两种语法之一:
-
以
to()关键字结尾的动词子句。例如:get("...").Option()+.to("...") -
以
route()关键字结尾的动词子句(用于嵌入骆驼路线)。例如:get("...").Option()+.route("...").CamelRoute.endRest()
Java REST DSL
在Java中,要使用REST DSL定义服务,请将REST定义放入RouteBuilder.configure()方法的主体中,就像对常规Apache Camel路由所做的一样。例如,要使用带有Spark-Rest组件的REST DSL 定义简单的Hello World服务,请定义以下Java代码:
restConfiguration().component("spark-rest").port(9091);
rest("/say")
.get("/hello").to("direct:hello")
.get("/bye").to("direct:bye");
from("direct:hello")
.transform().constant("Hello World");
from("direct:bye")
.transform().constant("Bye World");
前面的示例具有三种不同类型的构建器:
-
restConfiguration()将REST DSL配置为使用特定的REST实现(Spark-Rest)。
-
rest()使用REST DSL定义服务。每个动词子句都由
to()关键字终止,该关键字将传入的消息转发到direct端点(direct组件将同一应用程序中的路由拼接在一起)。 -
from()定义常规的骆驼路线。
XML REST DSL
在XML中,要使用XML DSL定义服务,请将rest元素定义为元素的子camelContext元素。例如,要使用带有Spark-Rest组件的REST DSL 定义简单的Hello World服务,请定义以下XML代码(在蓝图中):
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
<restConfiguration component="spark-rest" port="9091"/>
<rest path="/say">
<get uri="/hello">
<to uri="direct:hello"/>
</get>
<get uri="/bye">
<to uri="direct:bye"/>
</get>
</rest>
<route>
<from uri="direct:hello"/>
<transform>
<constant>Hello World</constant>
</transform>
</route>
<route>
<from uri="direct:bye"/>
<transform>
<constant>Bye World</constant>
</transform>
</route>
</camelContext>
指定基本路径
的rest()关键字(爪哇DSL)或path所述的属性rest元件(XML DSL)允许用户定义一个基本路径,然后将其前缀的路径中的所有动词条文。例如,给出以下Java DSL片段:
rest("/say")
.get("/hello").to("direct:hello")
.get("/bye").to("direct:bye");
或给出以下XML DSL片段:
<rest path="/say">
<get uri="/hello">
<to uri="direct:hello"/>
</get>
<get uri="/bye" consumes="application/json">
<to uri="direct:bye"/>
</get>
</rest>
REST DSL构建器为您提供以下URL映射:
/say/hello
/say/bye
基本路径是可选的。如果愿意,可以(不太优雅)在每个动词子句中指定完整路径:
rest()
.get("/say/hello").to("direct:hello")
.get("/say/bye").to("direct:bye");
使用动态To
REST DSL支持toD动态 to参数。使用此参数可以指定URI。
例如,在JMS中,可以通过以下方式定义动态端点URI:
public void configure() throws Exception {
rest("/say")
.get("/hello/{language}").toD("jms:queue:hello-${header.language}");
}
在XML DSL中,相同的细节如下所示:
<rest uri="/say">
<get uri="/hello//{language}">
<toD uri="jms:queue:hello-${header.language}"/>
</get>
<rest>
URI模板
在动词自变量中,可以指定URI模板,该模板使您可以捕获命名属性中的特定路径段(然后将其映射到Camel消息头)。例如,如果您希望个性化Hello World应用程序,以便它按名称与调用方打招呼,则可以定义REST服务,如下所示:
rest("/say")
.get("/hello/{name}").to("direct:hello")
.get("/bye/{name}").to("direct:bye");
from("direct:hello")
.transform().simple("Hello ${header.name}");
from("direct:bye")
.transform().simple("Bye ${header.name}");
URI模板捕获{name}路径段的文本,并将捕获的文本复制到name消息头中。如果您通过发送以URL结尾的GET HTTP请求来调用服务/say/hello/Joe,则HTTP响应为Hello Joe。
嵌入式路由语法
而不是终止动词从句用的to()关键字(Java的DSL)或to元素(XML DSL),你必须直接嵌入了Apache的骆驼航线进入休息DSL,使用的选项route()关键字(Java的DSL)或route元素(XML DSL )。该route()关键字允许你嵌入的路线变成个动词从句,语法如下:
RESTVerbClause.route("...").CamelRoute.endRest()
凡endRest()关键字(仅适用于Java的DSL)是一个必要的标点符号,使您能够分开动词条款(如果有不止一个动词在条款rest()建设者)。
例如,您可以重构Hello World示例以使用嵌入式Camel路由,如下Java DSL中所示:
rest("/say")
.get("/hello").route().transform().constant("Hello World").endRest()
.get("/bye").route().transform().constant("Bye World");
并在XML DSL中如下所示:
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
...
<rest path="/say">
<get uri="/hello">
<route>
<transform>
<constant>Hello World</constant>
</transform>
</route>
</get>
<get uri="/bye">
<route>
<transform>
<constant>Bye World</constant>
</transform>
</route>
</get>
</rest>
</camelContext>
注意
如果您在current中定义了任何异常子句(使用
onException())或拦截器(使用intercept()),则CamelContext这些异常子句和拦截器在嵌入式路由中也处于活动状态。
REST DSL和HTTP传输组件
如果未明确配置HTTP传输组件,则REST DSL通过检查类路径上的可用组件来自动发现要使用的HTTP组件。REST DSL查找任何HTTP组件的默认名称,并使用找到的第一个名称。如果类路径上没有HTTP组件,并且您未明确配置HTTP传输,则默认HTTP组件为camel-http。
指定请求和响应的内容类型
您可以使用Java中的和选项或XML中的和属性来过滤HTTP请求和响应的内容类型。例如,以下是一些常见的内容类型(正式称为Internet媒体类型): consumes()``produces()``consumes``produces
text/plaintext/htmltext/xmlapplication/jsonapplication/xml
内容类型在REST DSL中的动词子句中指定为选项。例如,要限制动词子句仅接受text/plainHTTP请求并仅发送text/htmlHTTP响应,可以使用如下的Java代码:
rest("/email")
.post("/to/{recipient}").consumes("text/plain").produces("text/html").to("direct:foo");
在XML中,您可以设置consumes和produces属性,如下所示:
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
...
<rest path="/email">
<post uri="/to/{recipient}" consumes="text/plain" produces="text/html">
<to "direct:foo"/>
</get>
</rest>
</camelContext>
您还可以将参数指定为consumes()或produces()作为逗号分隔的列表。例如,consumes("text/plain, application/json")。
其他HTTP方法
某些HTTP服务器实现支持附加的HTTP方法,其不通过在REST DSL标准组动词提供,get(),head(),put(),post(),delete(),patch()。要访问其他HTTP方法,可以verb()在Java DSL中使用通用关键字verb,在XML DSL中使用通用元素。
例如,要在Java中实现TRACE HTTP方法:
rest("/say")
.verb("TRACE", "/hello").route().transform();
where transform()将IN消息的主体复制到OUT消息的主体,从而回显HTTP请求。
要在XML中实现TRACE HTTP方法:
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
...
<rest path="/say">
<verb uri="/hello" method="TRACE">
<route>
<transform/>
</route>
</get>
</camelContext>
定义自定义HTTP错误消息
如果您的REST服务需要发送错误消息作为其响应,则可以如下定义自定义HTTP错误消息:
-
通过设置指定HTTP错误代码
Exchange.HTTP_RESPONSE_CODE标头键错误代码值(例如,400,404,等等)。此设置向REST DSL指示您要发送错误消息答复,而不是常规响应。 -
用您的自定义错误消息填充消息正文。
-
设置
Content-Type标题(如果需要)。 -
如果将REST服务配置为与Java对象进行封送处理(
bindingMode即已启用),则应确保该skipBindingOnErrorCode选项已启用(默认情况下为启用)。这是为了确保REST DSL在发送响应时不会尝试取消封送消息正文。
以下Java示例显示了如何定义自定义错误消息:
/// Java
// Configure the REST DSL, with JSON binding mode
restConfiguration().component("restlet").host("localhost").port(portNum).bindingMode(RestBindingMode.json);
// Define the service with REST DSL
rest("/users/")
.post("lives").type(UserPojo.class).outType(CountryPojo.class)
.route()
.choice()
.when().simple("${body.id} < 100")
.bean(new UserErrorService(), "idTooLowError")
.otherwise()
.bean(new UserService(), "livesWhere");
在此示例中,如果输入的ID小于100,我们将使用UserErrorServiceBean 返回自定义错误消息,该消息的实现方式如下:
// Java
public class UserErrorService {
public void idTooLowError(Exchange exchange) {
exchange.getIn().setBody("id value is too low");
exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "text/plain");
exchange.getIn().setHeader(Exchange.HTTP_RESPONSE_CODE, 400);
}
}
在UserErrorServiceBean中,我们定义了自定义错误消息,并将HTTP错误代码设置为400。
参数默认值
可以为传入的骆驼消息的标题指定默认值。
您可以使用诸如verbose查询参数之类的关键字来指定默认值。例如,在下面的代码中,默认值为false。这意味着如果没有为verbose键的头提供其他任何值,false则将默认插入该值。
rest("/customers/")
.get("/{id}").to("direct:customerDetail")
.get("/{id}/orders")
.param()
.name("verbose")
.type(RestParamType.query)
.defaultValue("false")
.description("Verbose order details")
.endParam()
.to("direct:customerOrders")
.post("/neworder").to("direct:customerNewOrder");
在自定义HTTP错误消息中包装JsonParserException
您可能想返回自定义错误消息的常见情况是包装JsonParserException异常。例如,您可以方便地利用Camel异常处理机制来创建具有HTTP错误代码400的自定义HTTP错误消息,如下所示:
// Java
onException(JsonParseException.class)
.handled(true)
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(400))
.setHeader(Exchange.CONTENT_TYPE, constant("text/plain"))
.setBody().constant("Invalid json data");
REST DSL选项
通常,REST DSL选项可以直接应用于服务定义的基础部分(即紧随其后的部分rest()),如下所示:
rest("/email").consumes("text/plain").produces("text/html")
.post("/to/{recipient}").to("direct:foo")
.get("/for/{username}").to("direct:bar");
在这种情况下,指定的选项适用于所有从属动词从句。或者可以将选项应用于每个单独的动词子句,如下所示:
rest("/email")
.post("/to/{recipient}").consumes("text/plain").produces("text/html").to("direct:foo")
.get("/for/{username}").consumes("text/plain").produces("text/html").to("direct:bar");
在这种情况下,指定的选项仅适用于相关的动词子句,并覆盖基础部分中的所有设置。
REST DSL选项总结了REST DSL支持的选项。
| Java DSL | XML DSL | 描述 |
|---|---|---|
bindingMode() | @bindingMode | 指定绑定模式,该模式可用于将传入消息封送到Java对象(以及可选地,将Java对象解封到输出消息)。可以有以下值:off(默认), ,auto,json,。 xml``json_xml |
consumes() | @consumes | 限制verb子句仅接受HTTP请求中的指定Internet媒体类型(MIME类型)。典型值是:text/plain,text/http,text/xml,application/json,application/xml。 |
customId() | @customId | 为JMX管理定义自定义ID。 |
description() | description | 记录REST服务或动词子句。对于JMX管理和工具很有用。 |
enableCORS() | @enableCORS | 如果为true,则在HTTP响应中启用CORS(跨域资源共享)标头。默认值为false。 |
id() | @id | 为REST服务定义唯一的ID,这对于定义JMX管理和其他工具很有用。 |
method() | @method | 指定此动词子句处理的HTTP方法。通常与通用verb()关键字结合使用。 |
outType() | @outType | 启用对象绑定时(即启用bindingMode选项时),此选项指定表示HTTP响应消息的Java类型。 |
produces() | produces | 限制verb子句在HTTP响应中仅产生指定的Internet媒体类型(MIME类型)。典型值是:text/plain,text/http,text/xml,application/json,application/xml。 |
type() | @type | 启用对象绑定时(即启用bindingMode选项时),此选项指定表示HTTP请求消息的Java类型。 |
*VerbURIArgument* | @uri | 指定路径段或URI模板作为动词的参数。例如,。 get(*VerbURIArgument*) |
*BasePathArgument* | @path | 在rest()关键字(Java DSL)或rest元素(XML DSL)中指定基本路径。 |
与Java对象之间的编组
序列化Java对象以通过HTTP传输
使用REST协议的最常见方法之一是在消息正文中传输Java Bean的内容。为了使它起作用,您需要一种将Java对象与适当的数据格式进出编组的机制。REST DSL支持以下适用于编码Java对象的数据格式:
-
JSON
(JavaScript对象表示法)是一种轻量级的数据格式,可以轻松地与Java对象进行映射。JSON语法紧凑,类型轻巧,易于人类读写。由于所有这些原因,JSON已作为REST服务的消息格式变得流行。
例如,下面的JSON代码可以表示
User豆具有两个属性字段,id和name:{ "id" : 1234, "name" : "Jane Doe" } -
JAXB
(用于XML绑定的Java体系结构)是一种基于XML的数据格式,可以轻松地与Java对象进行映射。为了将XML编组为Java对象,还必须注释要使用的Java类。
例如,下面的代码JAXB可以代表一个
User豆具有两个属性字段,id和name:<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <User> <Id>1234</Id> <Name>Jane Doe</Name> </User>注意
从Camel 2.17.0开始,JAXB数据格式和类型转换器支持使用XML到POJO的类的转换,这些类使用
ObjectFactory代替XmlRootElement。此外,Camel上下文应包含CamelJaxbObjectFactory值为true 的属性。但是,由于优化,默认值为false。
JSON和JAXB与REST DSL的集成
当然,您可以编写所需的代码来亲自将消息主体与Java对象之间来回转换。但是REST DSL提供了自动执行此转换的便利。尤其是,JSON和JAXB与REST DSL的集成具有以下优点:
- 自动进行Java对象之间的序列化(给定适当的配置)。
- REST DSL可以自动检测数据格式(JSON或JAXB)并执行适当的转换。
- REST DSL提供了一个抽象层,因此您编写的代码并不特定于特定的JSON或JAXB实现。因此,您可以稍后再打开实施,对应用程序代码的影响最小。
支持的数据格式组件
Apache Camel提供了JSON和JAXB数据格式的许多不同实现。REST DSL当前支持以下数据格式:
- JSON
- Jackson数据格式(
camel-jackson)(默认) - GSon数据格式(
camel-gson) - XStream数据格式(
camel-xstream)
- Jackson数据格式(
- JAXB
- JAXB数据格式(
camel-jaxb)
- JAXB数据格式(
如何启用对象序列化
要在REST DSL中启用对象序列化,请注意以下几点:
-
通过设置
bindingMode选项来启用绑定模式。 -
在带有
type选项(必填)的传入消息上和带有outType选项(可选)的传出消息上,指定要转换为(或从)转换的Java类型。 -
如果要在Java对象和JAXB数据格式之间进行转换,则必须记住用适当的JAXB注释对Java类进行注释。
-
使用
jsonDataFormat和/或xmlDataFormat选项(可以在restConfiguration构建器上指定)指定基础数据格式实现。 -
如果您的路由以JAXB格式提供返回值,则通常应将交换正文的Out消息设置为带有JAXB批注(一个JAXB元素)的类的实例。但是,如果您希望直接以XML格式提供JAXB返回值,则将其
dataFormatProperty与键设置xml.out.mustBeJAXBElement为false(可以在restConfiguration构建器上指定)。例如,在XML DSL语法中:<restConfiguration ...> <dataFormatProperty key="xml.out.mustBeJAXBElement" value="false"/> ... </restConfiguration> -
将所需的依赖项添加到您的项目构建文件中。例如,如果您使用的是Maven构建系统,并且使用的是Jackson数据格式,则可以将以下依赖项添加到您的Maven POM文件中:
<?xml version="1.0" encoding="UTF-8"?> <project ...> ... <dependencies> ... <!-- use for json binding --> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-jackson</artifactId> </dependency> ... </dependencies> </project> -
将应用程序部署到OSGi容器时,请记住为所选的数据格式安装必需的功能。例如,如果使用的是Jackson数据格式(默认),则可以
camel-jackson通过输入以下Karaf控制台命令来安装功能部件:JBossFuse:karaf@root> features:install camel-jackson或者,如果要部署到Fabric环境中,则可以将该功能添加到Fabric配置文件中。例如,如果您使用概要文件,
MyRestProfile那么可以通过输入以下控制台命令来添加功能部件:JBossFuse:karaf@root> fabric:profile-edit --features camel-jackson MyRestProfile
配置绑定方式
该bindingMode选项是off默认选项,因此必须启用它才能显式配置Java对象。TABLE显示了受支持的绑定模式的列表。
注意
从Camel 2.16.3开始,仅当content-type标头包含json或**xml时,**才会发生从POJO到JSon / JAXB的绑定。如果不应使用绑定将消息正文封送,这使您可以指定自定义内容类型。例如,如果消息正文是自定义二进制有效负载,则这很有用。
表4.2. REST DSL绑定模式
| 装订模式 | 描述 |
|---|---|
off | 绑定已关闭**(默认)**。 |
auto | 已为JSON和/或XML启用绑定。在此模式下,Camel根据传入消息的格式自动选择JSON或XML(JAXB)。你是不是需要启用这两种数据格式,但是:无论是JSON实现,XML实现,或两者都可以提供在类路径中。 |
json | 绑定仅对JSON启用。必须在类路径上提供JSON实现(默认情况下,Camel尝试启用该camel-jackson实现)。 |
xml | 绑定仅对XML启用。必须在类路径上提供XML实现(默认情况下,Camel尝试启用该camel-jaxb实现)。 |
json_xml | JSON和XML均启用了绑定。在此模式下,Camel根据传入消息的格式自动选择JSON或XML(JAXB)。您需要在类路径上提供两种数据格式。 |
在Java中,这些绑定模式值表示为以下enum类型的实例:
org.apache.camel.model.rest.RestBindingMode
您可以在几个不同的级别上设置bindingMode,如下所示:
-
REST DSL配置
您可以
bindingMode从restConfiguration构建器设置选项,如下所示:restConfiguration().component("servlet").port(8181).bindingMode(RestBindingMode.json); -
服务定义基础部分
您可以
bindingMode在rest()关键字之后(在动词子句之前)立即设置选项,如下所示:rest("/user").bindingMode(RestBindingMode.json).get("/{id}").VerbClause -
动词从句
您可以
bindingMode在动词子句中设置选项,如下所示:rest("/user") .get("/{id}").bindingMode(RestBindingMode.json).to("...");
示例
对于完整的代码示例,该示例演示如何使用Servlet组件作为REST实现来使用REST DSL,请看一下Apache Camel camel-example-servlet-rest-blueprint示例。您可以通过安装独立的Apache Camel发行版(apache-camel-2.21.0.fuse-750033-redhat-00001.zip位于extras/Fuse安装的子目录中)找到此示例。
安装独立的Apache Camel发行版后,您可以在以下目录下找到示例代码:
ApacheCamelInstallDir/examples/camel-example-servlet-rest-blueprint
将Servlet组件配置为REST实现
在该camel-example-servlet-rest-blueprint示例中,REST DSL的基础实现由Servlet组件提供。
示例为REST DSL配置Servlet组件
<?xml version="1.0" encoding="UTF-8"?>
<blueprint ...>
<!-- to setup camel servlet with OSGi HttpService -->
<reference id="httpService" interface="org.osgi.service.http.HttpService"/>
<bean class="org.apache.camel.component.servlet.osgi.OsgiServletRegisterer"
init-method="register"
destroy-method="unregister">
<property name="alias" value="/camel-example-servlet-rest-blueprint/rest"/>
<property name="httpService" ref="httpService"/>
<property name="servlet" ref="camelServlet"/>
</bean>
<bean id="camelServlet" class="org.apache.camel.component.servlet.CamelHttpTransportServlet"/>
...
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
<restConfiguration component="servlet"
bindingMode="json"
contextPath="/camel-example-servlet-rest-blueprint/rest"
port="8181">
<dataFormatProperty key="prettyPrint" value="true"/>
</restConfiguration>
...
</camelContext>
要使用REST DSL配置Servlet组件,您需要配置一个由以下三层组成的堆栈:
-
REST DSL层
REST DSL层由
restConfiguration元素配置,该元素通过将component属性设置为值与Servlet组件集成servlet。 -
Servlet组件层
Servlet组件层实现为类的实例
CamelHttpTransportServlet,其中示例实例的bean ID为camelServlet。 -
HTTP容器层
Servlet组件必须部署到HTTP容器中。Karaf容器通常配置有默认的HTTP容器(Jetty HTTP容器),该容器在端口8181上侦听HTTP请求。要将Servlet组件部署到默认的Jetty容器,您需要执行以下操作:
a. 获取对
org.osgi.service.http.HttpServiceOSGi服务的OSGi参考,其中该服务是标准化的OSGi接口,可提供对OSGi中默认HTTP服务器的访问。b. 创建实用程序类的实例
OsgiServletRegisterer,以将Servlet组件注册到HTTP容器中。该OsgiServletRegisterer班是简化了管理Servlet组件的生命周期的工具。创建此类的实例时,它将自动registerServlet在HttpServiceOSGi服务上调用该方法;实例销毁后,它将自动调用该unregister方法。
所需的依赖项
此示例具有两个依赖关系,这些依赖关系对REST DSL至关重要,如下所示:
-
Servlet组件
提供REST DSL的基础实现。这在Maven POM文件中指定,如下所示:
<dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-servlet</artifactId> <version>${camel-version}</version> </dependency>并且,在将应用程序捆绑包部署到OSGi容器之前,必须安装Servlet组件功能,如下所示:
JBossFuse:karaf@root> features:install camel-servlet -
Jackson 数据格式
提供JSON数据格式实现。这在Maven POM文件中指定,如下所示:
<dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-jackson</artifactId> <version>${camel-version}</version> </dependency>并且在将应用程序捆绑包部署到OSGi容器之前,必须安装Jackson数据格式功能,如下所示:
JBossFuse:karaf@root> features:install camel-jackson
响应的Java类型
示例应用程序User在HTTP请求和响应消息中来回传递类型对象。该UserJava类被定义为示于
示例 JSON响应的用户类
// Java
package org.apache.camel.example.rest;
public class User {
private int id;
private String name;
public User() {
}
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
所述User类在JSON格式数据相对简单的表示。例如,以JSON格式表示的此类的典型实例为:
{
"id" : 1234,
"name" : "Jane Doe"
}
带有JSON绑定REST DSL路由的示例
带有JSON绑定的REST DSL路由中显示了此示例的REST DSL配置和REST服务定义。
<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...>
...
<!-- a bean for user services -->
<bean id="userService" class="org.apache.camel.example.rest.UserService"/>
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
<restConfiguration component="servlet"
bindingMode="json"
contextPath="/camel-example-servlet-rest-blueprint/rest"
port="8181">
<dataFormatProperty key="prettyPrint" value="true"/>
</restConfiguration>
<!-- defines the REST services using the base path, /user -->
<rest path="/user" consumes="application/json" produces="application/json">
<description>User rest service</description>
<!-- this is a rest GET to view a user with the given id -->
<get uri="/{id}" outType="org.apache.camel.example.rest.User">
<description>Find user by id</description>
<to uri="bean:userService?method=getUser(${header.id})"/>
</get>
<!-- this is a rest PUT to create/update a user -->
<put type="org.apache.camel.example.rest.User">
<description>Updates or create a user</description>
<to uri="bean:userService?method=updateUser"/>
</put>
<!-- this is a rest GET to find all users -->
<get uri="/findAll" outType="org.apache.camel.example.rest.User[]">
<description>Find all users</description>
<to uri="bean:userService?method=listUsers"/>
</get>
</rest>
</camelContext>
</blueprint>
REST操作
示例中 的REST服务定义了以下REST操作:
-
GET /camel-example-servlet-rest-blueprint/rest/user/{id}获取由标识的用户的详细信息
{id},其中HTTP响应以JSON格式返回。 -
PUT /camel-example-servlet-rest-blueprint/rest/user创建一个新用户,该用户的详细信息包含在PUT消息的主体中,并以JSON格式编码(以匹配
User对象类型)。 -
GET /camel-example-servlet-rest-blueprint/rest/user/findAll获取所有用户的详细信息,其中,HTTP响应作为用户数组以JSON格式返回。
调用REST服务的URL
通过检查示例的REST DSL定义,可以将调用每个REST操作所需的URL拼凑在一起。例如,要调用第一个REST操作(该操作返回具有给定ID的用户的详细信息),则URL的构建如下:
-
http://localhost:8181在中
restConfiguration,协议默认为http,端口明确设置为8181。 -
/camel-example-servlet-rest-blueprint/rest由元素的
contextPath属性指定restConfiguration。 -
/user由元素的
path属性指定rest。 -
/{id}由动词元素的
uri属性指定get。
因此,可以curl通过在命令行中输入以下命令来使用实用程序调用此REST操作:
curl -X GET -H "Accept: application/json" http://localhost:8181/camel-example-servlet-rest-blueprint/rest/user/123
同样,curl通过输入以下示例命令,可以使用调用其余的REST操作:
curl -X GET -H "Accept: application/json" http://localhost:8181/camel-example-servlet-rest-blueprint/rest/user/findAll
curl -X PUT -d "{ \"id\": 666, \"name\": \"The devil\"}" -H "Accept: application/json" http://localhost:8181/camel-example-servlet-rest-blueprint/rest/user
配置REST DSL
使用Java配置
在Java中,您可以使用restConfiguration()构建器API 配置REST DSL 。例如,将REST DSL配置为使用Servlet组件作为基础实现:
restConfiguration().component("servlet").bindingMode("json").port("8181")
.contextPath("/camel-example-servlet-rest-blueprint/rest");
使用XML进行配置
在XML中,您可以使用restConfiguration元素配置REST DSL 。例如,将REST DSL配置为使用Servlet组件作为基础实现:
<?xml version="1.0" encoding="UTF-8"?>
<blueprint ...>
...
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
...
<restConfiguration component="servlet"
bindingMode="json"
contextPath="/camel-example-servlet-rest-blueprint/rest"
port="8181">
<dataFormatProperty key="prettyPrint" value="true"/>
</restConfiguration>
...
</camelContext>
</blueprint>
配置选项
| Java DSL | XML DSL | 描述 |
|---|---|---|
component() | @component | 指定骆驼分量作为传输REST(例如,使用servlet,restlet,spark-rest,等等)。该值可以是标准组件名称,也可以是自定义实例的bean ID。如果未指定此选项,则Camel RestConsumerFactory在类路径或bean注册表中查找的实例。 |
scheme() | @scheme | 用于公开REST服务的协议。依赖于底层REST实现,但http和https通常的支持。默认值为http。 |
host() | @host | 用于公开REST服务的主机名。 |
port() | @port | 用于公开REST服务的端口号。注意:此设置被Servlet组件忽略,该组件使用容器的标准HTTP端口。对于Apache Karaf OSGi容器,标准的HTTP端口通常为8181。尽管如此,为实现JMX和工具,设置端口值仍然是一个好习惯。 |
contextPath() | @contextPath | 为REST服务设置领先的上下文路径。可以与诸如Servlet之类的组件一起使用,其中使用context-path设置来部署已部署的Web应用程序。 |
hostNameResolver() | @hostNameResolver | 如果未明确设置主机名,则此解析器将确定REST服务的主机。可能的值是RestHostNameResolver.localHostName(Java DSL)或localHostName(XML DSL),它们解析为主机名格式。和RestHostNameResolver.localIp(Java的DSL)或localIp(XML DSL),它解析为点分十进制IP地址格式。从骆驼2.17 RestHostNameResolver.allLocalIp可用于解析为所有本地IP地址。默认值为localHostNameCamel 2.16。在Camel 2.17中,默认值为allLocalIp。 |
bindingMode() | @bindingMode | 启用JSON或XML格式消息的绑定模式。可能的值有:off,auto,json,xml,或json_xml。默认值为off。 |
skipBindingOnErrorCode() | @skipBindingOnErrorCode | 指定是否存在自定义HTTP错误代码标头,是否跳过输出绑定。这使您可以构建不绑定到JSON或XML的自定义错误消息,否则成功消息就会这样做。默认值为true。 |
enableCORS() | @enableCORS | 如果为true,则在HTTP响应中启用CORS(跨域资源共享)标头。默认值为false。 |
jsonDataFormat() | @jsonDataFormat | 指定Camel用于实现JSON数据格式的组件。可能的值有:json-jackson,json-gson,json-xstream。默认值为json-jackson。 |
xmlDataFormat() | @xmlDataFormat | 指定Camel用于实现XML数据格式的组件。可能的值为:jaxb。默认值为jaxb。 |
componentProperty() | componentProperty | 使您可以在基础REST实现上设置任意组件级别的属性。 |
endpointProperty() | endpointProperty | 使您可以在基础REST实现上设置任意端点级别的属性。 |
consumerProperty() | consumerProperty | 使您可以在基础REST实现上设置任意的使用者终结点属性。 |
dataFormatProperty() | dataFormatProperty | 使您可以在基础数据格式组件(例如,Jackson或JAXB)上设置任意属性。从Camel 2.14.1开始,您可以将以下前缀附加到属性键:json.in``json.out``xml.in``xml.out将属性设置限制为特定的格式类型(JSON或XML)和特定的消息方向(IN或OUT)。 |
corsHeaderProperty() | corsHeaders | 使您可以将自定义CORS标头指定为键/值对。 |
默认的CORS标头
如果启用了CORS(跨域资源共享),则默认设置以下标头。您可以选择通过调用corsHeaderPropertyDSL命令来覆盖默认设置。
默认的CORS标头
| 标题键 | 标头值 |
|---|---|
Access-Control-Allow-Origin | \* |
Access-Control-Allow-Methods | GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATCH |
Access-Control-Allow-Headers | Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers |
Access-Control-Max-Age | 3600 |
启用或禁用Jackson JSON功能
您可以通过在dataFormatProperty选项中配置以下键来启用或禁用特定的Jackson JSON功能:
json.in.disableFeaturesjson.in.enableFeatures
例如,禁用杰克逊的FAIL_ON_UNKNOWN_PROPERTIES功能(如果JSON输入具有无法映射到Java对象的属性,这将导致杰克逊失败):
restConfiguration().component("jetty")
.host("localhost").port(getPort())
.bindingMode(RestBindingMode.json)
.dataFormatProperty("json.in.disableFeatures", "FAIL_ON_UNKNOWN_PROPERTIES");
您可以通过指定以逗号分隔的列表来禁用多个功能。例如:
.dataFormatProperty("json.in.disableFeatures", "FAIL_ON_UNKNOWN_PROPERTIES,ADJUST_DATES_TO_CONTEXT_TIME_ZONE");
以下示例显示了如何在Java DSL中禁用和启用Jackson JSON功能:
restConfiguration().component("jetty")
.host("localhost").port(getPort())
.bindingMode(RestBindingMode.json)
.dataFormatProperty("json.in.disableFeatures", "FAIL_ON_UNKNOWN_PROPERTIES,ADJUST_DATES_TO_CONTEXT_TIME_ZONE")
.dataFormatProperty("json.in.enableFeatures", "FAIL_ON_NUMBERS_FOR_ENUMS,USE_BIG_DECIMAL_FOR_FLOATS");
以下示例显示了如何在XML DSL中禁用和启用Jackson JSON功能:
<restConfiguration component="jetty" host="localhost" port="9090" bindingMode="json">
<dataFormatProperty key="json.in.disableFeatures" value="FAIL_ON_UNKNOWN_PROPERTIES,ADJUST_DATES_TO_CONTEXT_TIME_ZONE"/>
<dataFormatProperty key="json.in.enableFeatures" value="FAIL_ON_NUMBERS_FOR_ENUMS,USE_BIG_DECIMAL_FOR_FLOATS"/>
</restConfiguration>
可以禁用或启用的Jackson功能对应于enum以下Jackson类的ID
- com.fasterxml.jackson.databind.SerializationFeature
- com.fasterxml.jackson.databind.DeserializationFeature
- com.fasterxml.jackson.databind.MapperFeature
Swagger整合
概览
您可以使用Swagger服务为CamelContext文件中的任何REST定义的路由和端点创建API文档。为此,请将Camel REST DSL与camel-swagger-java纯基于Java 的模块一起使用。该camel-swagger-java模块创建一个与CamelContext集成的servlet,该servlet从每个REST端点提取信息以生成JSON或YAML格式的API文档。
如果使用Maven,则编辑pom.xml文件以添加对camel-swagger-java组件的依赖关系:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-swagger-java</artifactId>
<version>x.x.x</version>
<!-- Specify the version of your camel-core module. -->
</dependency>
配置CamelContext以启用Swagger
要在Camel REST DSL中启用Swagger API,请调用apiContextPath()设置Swagger生成的API文档的上下文路径。例如:
public class UserRouteBuilder extends RouteBuilder {
@Override
public void configure() throws Exception {
// Configure the Camel REST DSL to use the netty4-http component:
restConfiguration().component("netty4-http").bindingMode(RestBindingMode.json)
// Generate pretty print output:
.dataFormatProperty("prettyPrint", "true")
// Set the context path and port number that netty will use:
.contextPath("/").port(8080)
// Add the context path for the Swagger-generated API documentation:
.apiContextPath("/api-doc")
.apiProperty("api.title", "User API").apiProperty("api.version", "1.2.3")
// Enable CORS:
.apiProperty("cors", "true");
// This user REST service handles only JSON files:
rest("/user").description("User rest service")
.consumes("application/json").produces("application/json")
.get("/{id}").description("Find user by id").outType(User.class)
.param().name("id").type(path).description("The id of the user to get").dataType("int").endParam()
.to("bean:userService?method=getUser(${header.id})")
.put().description("Updates or create a user").type(User.class)
.param().name("body").type(body).description("The user to update or create").endParam()
.to("bean:userService?method=updateUser")
.get("/findAll").description("Find all users").outTypeList(User.class)
.to("bean:userService?method=listUsers");
}
}
Swagger模块配置选项
下表中描述的选项使您可以配置Swagger模块。设置一个选项,如下所示:
- 如果将
camel-swagger-java模块用作servlet,请通过更新web.xml文件并init-param为要设置的每个配置选项指定一个元素来设置选项。 - 如果使用的是
camel-swagger-java从骆驼REST部件模块,通过调用适当的设定的选项RestConfigurationDefinition的方法,例如enableCORS(),host(),或contextPath()。api.xxx使用RestConfigurationDefinition.apiProperty()方法设置选项。
| 选项 | 类型 | 描述 |
|---|---|---|
api.contact.email | 字符串 | 用于API相关通信的电子邮件地址。 |
api.contact.name | 字符串 | 要联系的人员或组织的名称。 |
api.contact.url | 字符串 | 网站的URL,以获取更多联系信息。 |
apiContextIdListing | 布尔型 | 如果您的应用程序使用多个CamelContext对象,则默认行为是仅在current中列出REST端点CamelContext。如果要CamelContext在运行REST服务的JVM中运行的每个REST端点的列表,请将此选项设置为true。如果apiContextIdListing为true,则Swagger会将CamelContext根路径中的ID(例如)输出为/api-docsJSON格式的名称列表。要访问Swagger生成的文档,请将REST上下文路径附加到CamelContextID,例如api-docs/myCamel。您可以使用该apiContextIdPattern选项来过滤此输出列表中的名称。 |
apiContextIdPattern | 字符串 | 用于过滤哪些CamelContext ID出现在上下文列表中的模式。您可以指定正则表达式并将*用作通配符。这与骆驼拦截功能使用的模式匹配工具相同。 |
api.license.name | 字符串 | 用于API的许可证名称。 |
api.license.url | 字符串 | 用于API的许可证的URL。 |
api.path | 字符串 | 设置可用于生成文档的REST API的路径,例如/api-docs。指定相对路径。例如,请勿指定http或https。该camel-swagger-java模块在运行时以以下格式计算绝对路径:protocol://host:port/context-path/api-path。 |
api.termsOfService | 字符串 | API服务条款的网址。 |
api.title | 字符串 | 应用程序的标题。 |
api.version | 字符串 | API版本。默认值为0.0.0。 |
base.path | 字符串 | 需要。设置REST服务可用的路径。指定相对路径。也就是说,请勿指定http或https。该camel-swagger-java模件计算在运行时以这种格式的绝对路径:protocol://host:port/context-path/base.path。 |
cors | 布尔型 | 是否启用HTTP访问控制(CORS)。这使CORS仅用于查看REST API文档,而不用于访问REST服务。默认值为false。建议改用此CorsFilter选项,如下表所述。 |
host | 字符串 | 设置运行Swagger服务的主机的名称。默认为基于计算主机名localhost。 |
schemes | 字符串 | 使用的协议方案。例如,用逗号分隔多个值,"http,https".默认值为http。 |
swagger.version | 字符串 | Swagger规范版本。默认值为2.0。 |
使用CORS筛选器启用CORS支持
如果使用Swagger用户界面查看REST API文档,则可能需要启用对HTTP访问控制(CORS)的支持。当托管Swagger用户界面并在与运行REST API的主机名/端口不同的主机名/端口上运行Swagger用户界面时,需要此支持。
要启用对CORS的支持,请将添加RestSwaggerCorsFilter到您的web.xml文件中。CORS过滤器添加了启用CORS的HTTP标头。例如:
<!-- Enable CORS filter to allow use of Swagger UI for browsing and testing APIs. -->
<filter>
<filter-name>RestSwaggerCorsFilter</filter-name>
<filter-class>org.apache.camel.swagger.rest.RestSwaggerCorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RestSwaggerCorsFilter</filter-name>
<url-pattern>/api-docs/*</url-pattern>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
该RestSwaggerCorsFilter规定对所有请求以下标题:
- Access-Control-Allow-Origin= *
- Access-Control-Allow-Methods = GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH
- Access-Control-Max-Age = 3600’
- Access-Control-Allow-Headers = Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers
RestSwaggerCorsFilter是一个简单的过滤器。如果需要阻止某些客户端或为给定客户端设置不同的标头值,则可能需要更复杂的过滤器。
获取JSON或YAML输出
从Camel 2.17开始,该camel-swagger-java模块支持JSON和YAML格式的输出。要指定所需的输出,请在/swagger.json或/swagger.yamlURL上添加。如果请求URL未指定格式,则camel-swagger-java模块将检查HTTP Accept标头以检测是否可以接受JSON或YAML。如果两个都被接受,或者没有一个被设置为接受,那么JSON是默认的返回格式。
例子
在Apache Camel发行,camel-example-swagger-cdi并camel-example-swagger-java演示该camel-swagger-java模块的使用。
增强Swagger生成的文档
从Camel 2.16开始,您可以通过定义参数详细信息(例如名称,描述,数据类型,参数类型等)来增强Swagger生成的文档。如果使用XML,请指定param元素以添加此信息。以下示例显示如何提供有关ID路径参数的信息:
<!-- This is a REST GET request to view information for the user with the given ID: -->
<get uri="/{id}" outType="org.apache.camel.example.rest.User">
<description>Find user by ID.</description>
<param name="id" type="path" description="The ID of the user to get information about." dataType="int"/>
<to uri="bean:userService?method=getUser(${header.id})"/>
</get>
以下是Java DSL中的相同示例:
.get("/{id}").description("Find user by ID.").outType(User.class)
.param().name("id").type(path).description("The ID of the user to get information about.").dataType("int").endParam()
.to("bean:userService?method=getUser(${header.id})")
如果定义名称为的参数,body则还必须指定body该参数的类型。例如:
<!-- This is a REST PUT request to create/update information about a user. -->
<put type="org.apache.camel.example.rest.User">
<description>Updates or creates a user.</description>
<param name="body" type="body" description="The user to update or create."/>
<to uri="bean:userService?method=updateUser"/>
</put>
以下是Java DSL中的相同示例:
.put().description("Updates or create a user").type(User.class)
.param().name("body").type(body).description("The user to update or create.").endParam()
.to("bean:userService?method=updateUser")
3378

被折叠的 条评论
为什么被折叠?



