2.3 传输格式
本节要考虑的就是如何设计表述,即传输过程中数据采用什么样的数据格式。通常,REST接口会以XML和JSON作为主要的传输格式,这两种格式数据的处理是本节的重点。那么Jersey是否还支持其他的数据格式呢?答案是肯定的,让我们逐一掌握各种类型的实现。
2.3.1 基本类型
Java的基本类型又叫原生类型,包括4种整型(byte、short、int、long)、2种浮点类型(float、double)、Unicode编码的字符(char)和布尔类型(boolean)。
阅读指南
本节的前4小节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.response。
Jersey支持全部的基本类型,还支持与之相关的引用类型。前述示例已经呈现了整型(int)等Java的基本类型的参数,本例展示字节数组类型作为请求实体类型、字符串作为响应实体类型的示例,示例代码如下。
@POST
@Path("b")
public String postBytes(final byte[] bs)
{//关注点1:测试方法入参
for (final byte b : bs) {
LOGGER.debug(b);
}
return "byte[]:" + new String(bs);
}
@Test
public void testBytes() {
final String message = "TEST STRING";
final Builder request = target(path).path("b").request();
final Response response = request.post(
Entity.entity(message,
MediaType.TEXT_PLAIN_TYPE), Response.class);
result = response.readEntity(String.class);
//关注点2:测试断言
Assert.assertEquals("byte[]:" + message, result);
}
在这段代码中,资源方法postBytes()的输入参数是byte[]类型,输出参数是String类型,见关注点1;单元测试方法testBytes()的断言是对字符串"TEST STRING"的验证,见关注点2。
2.3.2 文件类型
Jersey支持传输File类型的数据,以方便客户端直接传递File类实例给服务器端。文件类型的请求,默认使用的媒体类型是Content-Type: text/html,示例代码如下。
@POST
@Path("f")
//关注点1:测试方法入参
public File postFile(final File f) throws
FileNotFoundException, IOException {
//关注点2:try-with-resources
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
String s;
do {
s = br.readLine();
LOGGER.debug(s);
} while (s != null);
return f;
}
}
@Test
public void testFile() throws
FileNotFoundException, IOException {
//关注点3:获取文件全路径
final URL resource =
getClass().getClassLoader().getResource("gua.txt");
//关注点4:构建File实例
final String file = resource.getFile();
final File f = new File(file);
final Builder request = target(path).path("f").request();
//关注点5:提交POST请求
Entity<File> e = Entity.entity(f, MediaType.TEXT_PLAIN_TYPE);
final Response response = request.post(e, Response.class);
File result = response.readEntity(File.class);
try (BufferedReader br = new BufferedReader(new FileReader(result))) {
String s;
do {
s = br.readLine();//关注点6:逐行读取文件
LOGGER.debug(s);
} while (s != null);
}
}
在这段代码中,资源方法postFile()的输入参数类型和返回值类型都是File类型,见关注点1;服务器端对File实例进行解析,最后将该资源释放,即try-with-resources,见关注点2;在测试方法testFile()中,构建了File类型的"gua.txt"文件的实例,见关注点3;作为请求实体提交,见关注点4;并对响应实体进行逐行读取的校验,见关注点5;需要注意的是,由于我们使用的是Maven构建的项目,测试文件位于测试目录的resources目录,其相对路径为/simple-service-3/src/test/resources/gua.txt,获取该文件的语句为getClass().getClassLoader().getResource("gua.txt"),见关注点6。
另外,文件的资源释放使用了JDK7的try-with-resources语法,见关注点2。
2.3.3 InputStream类型
Jersey支持Java的两大读写模式,即字节流和字符流。本示例展示字节流作为REST方法参数,示例如下。
@POST
@Path("bio")
//关注点1:资源方法入参
public String postStream(final InputStream
is) throws FileNotFoundException, IOException {
//关注点2:try-with-resources
try (BufferedReader br = new BufferedReader(new InputStreamReader(is)))
{
StringBuilder result = new StringBuilder();
String s = br.readLine();
while (s != null) {
result.append(s).append("\n");
LOGGER.debug(s);
s = br.readLine();
}
return result.toString();//关注点3:资源方法返回值
}
}
@Test
public void testStream() {
//关注点4:获取文件全路径
final InputStream resource =
getClass().getClassLoader().getResourceAsStream("gua.txt");
final Builder request = target(path).path("bio").request();
Entity<InputStream> e = Entity.entity(resource,
MediaType.TEXT_PLAIN_TYPE);
final Response response = request.post(e, Response.class);
result = response.readEntity(String.class);
//关注点5:输出返回值内容
LOGGER.debug(result);
}
在这段代码中,资源方法postStream()的输入参数类型是InputStream,见关注点1;服务器端从中读取字节流,并最终释放该资源,见关注点2;返回值是String类型,内容是字节流信息,见关注点3;测试方法testStream()构建了"gua.txt"文件内容的字节流,作为请求实体提交,见关注点4;响应实体预期为String类型的"gua.txt"文件内容信息,见关注点5。
2.3.4 Reader类型
本示例展示另一种Java读写模式,以字符流作为REST方法参数,示例如下。
@POST
@Path("cio")
//关注点1:资源方法入参
public String postChars(final Reader r)
throws FileNotFoundException, IOException {
//关注点2:try-with-resources
try (BufferedReader br = new BufferedReader(r)) {
String s = br.readLine();
if (s == null) {
throw new Jaxrs2GuideNotFoundException("NOT FOUND FROM
READER");
}
while (s != null) {
LOGGER.debug(s);
s = br.readLine();
}
return "reader";
}
}
@Test
public void testReader() {
//关注点3:构建并提交Reader实例
ClassLoader classLoader = getClass().getClassLoader();
final Reader resource =
new
InputStreamReader(classLoader.getResourceAsStream("gua.txt"));
final Builder request = target(path).path("cio").request();
Entity<Reader> e = Entity.entity(resource,
MediaType.TEXT_PLAIN_TYPE);
final Response response = request.post(e, Response.class);
result = response.readEntity(String.class);
//关注点4:输出返回值内容
LOGGER.debug(result);
}
在这段代码中,资源方法postChars()的输入参数类型是Reader,见关注点1;服务器端从中读取字符流,并最终释放该资源,返回值是String类型,见关注点2;测试方法testReader()构建了"gua.txt"文件内容的Reader实例,将字符流作为请求实体提交,见关注点3;响应实体预期为String类型的"gua.txt"文件内容信息,见关注点4。
2.3.5 XML类型
XML类型是使用最广泛的数据类型。Jersey对XML类型的数据处理,支持Java领域的两大标准,即JAXP(Java API for
XML Processing,JSR-206)和JAXB(Java Architecture for XML Binding,JSR-222)。
阅读指南
本节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.media.xml。
1. JAXP标准
JAXP包含了DOM、SAX和StAX 3种解析XML的技术标准。
DOM是面向文档解析的技术,要求将XML数据全部加载到内存,映射为树和结点模型以实现解析。
SAX是事件驱动的流解析技术,通过监听注册事件,触发回调方法以实现解析。
StAX是拉式流解析技术,相对于SAX的事件驱动推送技术,拉式解析使得读取过程可以主动推进当前XML位置的指针而不是被动获得解析中的XML数据。
对应的,JAXP定义了3种标准类型的输入接口Source(DOMSource,SAXSource,StreamSource)和输出接口Result(DOMResult,SAXResult,StreamResult)。Jersey可以使用JAXP的输入类型作为REST方法的参数,示例代码如下。
@POST
@Path("stream")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public StreamSource getStreamSource(
javax.xml.transform.stream.StreamSource
streamSource) {
//关注点1:资源方法入参
return streamSource;
}
@POST
@Path("sax")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
//关注点2:支持SAX技术
public SAXSource
getSAXSource(javax.xml.transform.sax.SAXSource saxSource) {
return saxSource;
}
@POST
@Path("dom")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
//关注点3:支持DOM技术
public DOMSource
getDOMSource(javax.xml.transform.dom.DOMSource domSource) {
return domSource;
}
@POST
@Path("doc")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
//关注点4:支持DOM技术
public Document
getDocument(org.w3c.dom.Document document) {
return document;
}
在这段代码中,资源方法getStreamSource()使用StAX拉式流解析技术支持输入输出类型为StreamSource的请求,见关注点1;getSAXSource()方法使用SAX是事件驱动的流解析技术支持输入输出类型为SAXSource的请求,见关注点2;getDOMSource()方法和getDocument()方法使用DOM面向文档解析的技术,支持输入输出类型分别为DOMSource和Document的请求,见关注点3和关注点4。
2. JAXB标准
JAXP的缺点是需要编码解析XML,这增加了开发成本,但对业务逻辑的实现并没有实质的贡献。JAXB只需要在POJO中定义相关的注解(早期人们使用XML配置文件来做这件事),使其和XML的schema对应,无须对XML进行程序式解析,弥补了JAXP的这一缺点,因此本书推荐使用JAXB作为XML解析的技术。
JAXB通过序列化和反序列化实现了XML数据和POJO对象的自动转换过程。在运行时,JAXB通过编组(marshall)过程将POJO序列化成XML格式的数据,通过解编(unmarshall)过程将XML格式的数据反序列化为Java对象。JAXB的注解位于javax.xml.bind.annotation包中,详情可以访问JAXB的参考实现网址是https://jaxb.java.net/tutorial。
需要指出的是,从理论上讲,JAXB解析XML的性能不如JAXP,但使用JAXB的开发效率很高。笔者所在的开发团队使用JAXB解析XML,从实践体会而言,笔者并不支持JAXB影响系统运行性能这样的观点。因为计算机执行的瓶颈在IO,而无论使用哪种技术解析,XML数据本身是一样的,区别仅在于解析手段。而REST风格以及敏捷思想的宗旨就是简单—开发过程简单化、执行逻辑简单化,因此如果连XML数据都趋于简单,JAXP带来的性能优势就可以忽略不计了。综合考量,实现起来更简单的JAXB更适合做REST开发。
Jersey支持使用JAXBElement作为REST方法参数的形式,也支持直接使用POJO作为REST方法参数的形式,后一种更为常用,示例代码如下。
@POST
@Path("jaxb")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public Book
getEntity(JAXBElement<Book> bookElement) {
Book book = bookElement.getValue();
LOGGER.debug(book.getBookName());
return book;
}
@POST
@Consumes({ MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_XML)
public Book getEntity(Book book) {
LOGGER.debug(book.getBookName());
return book;
}
以上JAXP和JAXB的测试如下所示,其传输内容是相同的,不同在于服务器端的REST方法定义的解析类型和返回值类型。
1 > Content-Type: application/xml
<?xml version="1.0"
encoding="UTF-8" standalone="yes"?><book
bookId="100" bookName="TEST BOOK"/>
2 < Content-Length: 79
2 < Content-Type: text/html
<?xml version="1.0"
encoding="UTF-8"?><book bookId="100"
bookName="TEST BOOK"/>
从测试结果可以看到,POJO类的字段是作为XML的属性组织起来的,详见如下的图书实体类定义。
@XmlRootElement
public class Book implements Serializable {
//关注点1:JAXB属性注解
@XmlAttribute(name = "bookId")
public Long getBookId() {
return bookId;
}
@XmlAttribute(name = "bookName")
public String getBookName() {
return bookName;
}
@XmlAttribute(name = "publisher")
public String getPublisher() {
return publisher;
}
}
(1)property和element
本例的POJO类Book的字段都定义为XML的属性(property)来组织,POJO的字段也可以作为元素(element)组织,见关注点1。如何定义通常取决于对接系统的设计。需要注意的是,如果REST请求的传输数据量很大,并且无须和外系统对接的场景,建议使用属性来组织XML,这样可以极大地减少XML格式的数据包的大小。
(2)XML_SECURITY_DISABLE
Jersey默认设置了XMLConstants.FEATURE_SECURE_PROCESSING(http://javax.xml.XML
Constants/feature/secure-processing)属性,当属性或者元素过多时,会报“well-formedness
error”这样的警告信息。如果业务逻辑确实需要设计一个繁琐的POJO,可以通过设置MessageProperties.XML_SECURITY_DISABLE参数值为TRUE来屏蔽。服务器端和客户端,示例代码如下。
@ApplicationPath("/*")
public class XXXResourceConfig extends
ResourceConfig {
public XXXResourceConfig() {
packages("xxx.yyy.zzz");
property(MessageProperties.XML_SECURITY_DISABLE, Boolean.TRUE);
}
}
ClientConfig config = new ClientConfig();
config.property(MessageProperties.XML_SECURITY_DISABLE,
Boolean.TRUE);
2.3.6 JSON类型
JSON类型已经成为Ajax技术中数据传输的实际标准。Jersey提供了4种处理JSON数据的媒体包。表2-6展示了4种技术对3种解析流派(基于POJO的JSON绑定、基于JAXB的JSON绑定以及低级的(逐字的)JSON解析和处理)的支持情况。MOXy和Jackon的处理方式相同,它们都不支持以JSON对象方式解析JSON数据,而是以绑定方式解析。Jettison支持以JSON对象方式解析JSON数据,同时支持JAXB方式的绑定。JSON-P就只支持JSON对象方式解析这种方式了。
表2-6 Jersey对JSON的处理方式列表
解析方式\JSON支持包 MOXy JSON-P Jackson Jettison
POJO-based JSON Binding 是 否 是 否
JAXB-based JSON Binding 是 否 是 是
Low-level JSON parsing & processing 否 是 否 是
下面将介绍MOXy、SON-P、Jackson和Jettison这4种Jersey支持的JSON处理技术在REST式的Web服务开发中的使用。
1. 使用MOXy处理JSON
MOXy是EclipseLink项目的一个模块,其官方网站http://www.eclipse.org/eclipselink/moxy.php宣称EclipseLink的MOXy组件是使用JAXB和SDO作为XML绑定的技术基础。MOXy实现了JSR 222标准(JAXB2.2)和JSR 235标准(SDO2.1.1),这使得使用MOXy的Java开发者能够高效地完成Java类和XML的绑定,所要花费的只是使用注解来定义它们之间的对应关系。同时,MOXy实现了JSR-353标准(Java API for Processing JSON1.0),以JAXB为基础来实现对JSR353的支持。下面开始讲述使用MOXy实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的MOXy示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-1.simple-service-moxy。
(1)定义依赖
MOXy是Jersey默认的JSON解析方式,可以在项目中添加MOXy的依赖包来使用MOXy。
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-moxy</artifactId>
</dependency>
(2)定义Application
使用Servlet3可以不定义web.xml配置,否则请参考1.6节的讲述。
MOXy的Feature接口实现类是MoxyJsonFeature,默认情况下,Jersey对其自动探测,无须在Applicaion类或其子类显式注册该类。如果不希望Jersey这种默认行为,可以通过设置如下属性来禁用自动探测:CommonProperties.MOXY_JSON_FEATURE_DISABLE两端禁用,ServerProperties.MOXY_JSON_FEATURE_DISABLE服务器端禁用,ClientProperties.MOXY_JSON_FEATURE_DISABLE客户端禁用。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends
ResourceConfig {
public JsonResourceConfig() {
register(BookResource.class);
//property(org.glassfish.jersey.CommonProperties.MOXY_JSON_FEATURE_DISABLE,
true);
}
}
(3)定义资源类
接下来,我们定义一个图书资源类BookResource,并在其中实现表述媒体类型为JSON的资源方法getBooks()。支持JSON格式的表述的资源类定义如下。
@Path("books")
//关注点1:@Produces注解和@Consumes注解上移到接口
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class BookResource {
private static final HashMap<Long, Book> memoryBase;
...
@GET
//关注点2:实现类方法无需再定义@Produces注解和@Consumes注解
public Books getBooks() {
final List<Book> bookList = new ArrayList<>();
final Set<Map.Entry<Long, Book>> entries = BookResource.memoryBase.entrySet();
final Iterator<Entry<Long, Book>> iterator =
entries.iterator();
while (iterator.hasNext()) {
final Entry<Long, Book> cursor = iterator.next();
BookResource.LOGGER.debug(cursor.getKey());
bookList.add(cursor.getValue());
}
final Books books = new Books(bookList);
BookResource.LOGGER.debug(books);
return books;
}
}
在这段代码中,资源类BookResource定义了@Consumes(MediaType.APPLICATION_JSON)和@Produces(MediaType.APPLICATION_JSON),表示该类的所有资源方法都使用MediaType.APPLICATION_JSON类型作为请求和响应的数据类型,见关注点1;因此,getBooks()方法上无须再定义@Consumes和@Produces,见关注点2。
如果REST应用处于多语言环境中,不要忘记统一开放接口的字符编码;如果统一开放接口同时供前端jsonp使用,不要忘记添加相关媒体类型,示例如下。
@Produces({"application/x-javascript;charset=UTF-8",
"application/json;charset=UTF-8"})
在这段代码中,REST API将支持jsonp、json,并且统一字符编码为UTF-8。
(4)单元测试
JSON处理的单元测试主要关注请求的响应中JSON数据的可用性、完整性和一致性。在本章使用的单元测试中,验证JSON处理无误的标准是测试的返回值是一个Java类型的实体类实例,整个请求处理过程中没有异常发生,测试代码如下。
public class JsonTest extends JerseyTest {
private final static Logger LOGGER = Logger.getLogger(JsonTest.class);
@Override
protected Application configure() {
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
return new ResourceConfig(BookResource.class);
}
@Test
public void testGettingBooks() {
//关注点1:在请求中定义媒体类型为JSON
Books books =
target("books").request(MediaType.APPLICATION_JSON_TYPE).
get(Books.class);
for (Book book : books.getBookList()) {
LOGGER.debug(book.getBookName());
}
}
}
在这段代码中,测试方法testGettingBooks()定义了请求资源的数据类型为MediaType.APPLICATION_JSON_TYPE来匹配服务器端提供的REST API,其作用是定义请求的媒体类型为JSON格式的,见关注点1。
(5)集成测试
除了单元测试,我们使用cURL来做集成测试。首先启动本示例,然后输入如下所示的命令。
curl
http://localhost:8080/simple-service-moxy/api/books
curl -H "Content-Type:
application/json" http://localhost:8080/simple-service-moxy/api/books
返回JSON格式的数据如下。
{"bookList":{"book":[{"bookId":1,"bookName":"JSF2和RichFaces4使用指南","publisher":"电子工业出版社","isbn":"9787121177378","publishTime":"2012-09-01"},{"bookId":2,"bookName":"Java
Restful Web Services实战","publisher":"机械工业出版社","isbn":"9787111478881","publishTime":"2014-09-01"},{"bookId":3,"bookName":"Java
EE 7 精髓","publisher":"人民邮电出版社","isbn":"9787115375483","publishTime":"2015-02-01"},{"bookId":4,"bookName":"Java
Restful Web Services实战II","publisher":"机械工业出版社"}]}}
2. 使用JSON-P处理JSON
JSON-P的全称是 Java API for
JSON Processing(Java的JSON处理API),而不是JSON with padding(JSONP),两者只是名称相仿,用途大相径庭。JSON-P是JSR 353标准规范,用于统一Java处理JSON格式数据的API,其生产和消费的JSON数据以流的形式,类似StAX处理XML,并为JSON数据建立Java对象模型,类似DOM。而JSONP是用于异步请求中传递脚本的回调函数来解决跨域问题。下面开始讲述使用JSON-P实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的JSON-P示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-2.simple-service-jsonp。
(1)定义依赖
使用JSON-P方式处理JSON类型的数据,需要在项目的Maven配置中声明如下依赖。
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-processing</artifactId>
</dependency>
(2)定义Application
使用JSON-P的应用,默认不需要在其Application中注册JsonProcessingFeature,除非使用了如下设置。依次用于在服务器和客户端两侧去活JSON-P功能、在服务器端去活JSON-P功能、在客户端去活JSON-P功能。
CommonProperties.JSON_PROCESSING_FEATURE_DISABLE
ServerProperties.JSON_PROCESSING_FEATURE_DISABLE
ClientProperties.JSON_PROCESSING_FEATURE_DISABLE
JsonGenerator.PRETTY_PRINTING属性用于格式化JSON数据的输出,当属性值为TRUE时,MesageBodyReader和MessageBodyWriter实例会对JSON数据进行额外处理,使得JSON数据可以格式化打印。该属性的设置在Application中,见关注点1,示例代码如下。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends
ResourceConfig {
public JsonResourceConfig() {
register(BookResource.class);
//关注点1:配置JSON格式化输出
property(JsonGenerator.PRETTY_PRINTING, true);
}
}
(3)定义资源类
资源类BookResource同上例一样定义了类级别的@Consumes和@Produces,媒体格式为MediaType.APPLICATION_JSON,资源类BookResource的示例代码如下。
@Path("books")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class BookResource {
...
static {
memoryBase = com.google.common.collect.Maps.newHashMap();
//关注点1:构建JsonObjectBuilder实例
JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
//关注点2:构建JsonObject实例
JsonObject newBook1 = jsonObjectBuilder.add("bookId", 1)
.add("bookName", "Java Restful Web Services实战")
.add("publisher", "机械工业出版社")
.add("isbn",
"9787111478881")
.add("publishTime", "2014-09-01")
.build();
...
}
@GET
public JsonArray getBooks() {
//关注点3:构建JsonArrayBuilder实例
final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
final Set<Map.Entry<Long, JsonObject>> entries =
BookResource.memoryBase.entrySet();
final Iterator<Entry<Long, JsonObject>> iterator =
entries.iterator();
while (iterator.hasNext()) {
...
}
//关注点4:构建JsonArray实例
JsonArray result = arrayBuilder.build();
return result;
}
}
在这段代码中,JsonObjectBuilder用于构造JSON对象,见关注点1;JsonArrayBuilder用于构造JSON数组对象,见关注点2;JsonObject是JSON-P定义的JSON对象类,见关注点3;JsonArray是JSON数组类,见关注点4。
(4)单元测试
JSON-P示例的单元测试需要关注JSON-P定义的JSON类型,测试验收标准在前一小节MOXy的单元测试中已经讲述,示例代码如下。
public class JsonTest extends JerseyTest {
private final static Logger LOGGER = Logger.getLogger(JsonTest.class);
@Override
protected Application configure() {
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
return new ResourceConfig(BookResource.class);
}
@Test
public void testGettingBooks() {
//关注点1:请求的响应类型为JsonArray
JsonArray books = target("books").request(MediaType.APPLICATION_JSON_TYPE).
get(JsonArray.class);
for (JsonValue jsonValue : books) {
//关注点2:强转JsonValue为JsonObject
JsonObject book = (JsonObject) jsonValue;
LOGGER.debug(book.getString("bookName"));//关注点3:打印输出测试结果
}
}
}
在这段代码片段中,JsonArray是getBooks()方法的返回类型,get()请求发出后,服务器端对应的方法是getBooks()方法,见关注点1;JsonValue类型是一种抽象化的JSON数据类型,此处类型强制转化为JsonObject,见关注点2;getString()方法是将JsonObject对象的某个字段以字符串类型返回,见关注点3。
(5)集成测试
使用cURL对本示例进行集成测试的结果如下所示,JSON数据结果可以格式化打印输出。
curl
http://localhost:8080/simple-service-jsonp/api/books
[
{
"bookId":1,
"bookName":"Java Restful Web Services实战",
"publisher":"机械工业出版社",
"isbn":"9787111478881",
"publishTime":"2014-09-01"
},
{
"bookId":2,
"bookName":"JSF2和RichFaces4使用指南",
"publisher":"电子工业出版社",
"isbn":"9787121177378",
"publishTime":"2012-09-01"
},
{
"bookId":3,
"bookName":"Java EE 7精髓",
"publisher":"人民邮电出版社",
"isbn":"9787115375483",
"publishTime":"2015-02-01"
},
{
"bookId":4,
"bookName":"Java Restful Web Services实战II",
"publisher":"机械工业出版社"
}
]
curl
http://localhost:8080/simple-service-jsonp/api/books/book?id=1
{
"bookId":1,
"bookName":"Java Restful Web Services实战",
"publisher":"机械工业出版社",
"isbn":"9787111478881",
"publishTime":"2014-09-01"
}
curl -H "Content-Type:
application/json" -X POST \
-d
"{\"bookName\":\"abc\",\"publisher\":\"me\"}"
\
http://localhost:8080/simple-service-jsonp/api/books
{
"bookId":23670621181527,
"bookName":"abc",
"publisher":"me"
}
3.使用Jackson处理JSON
Jackson是一种流行的JSON支持技术,其源代码托管于Github,地址是:https://github.com/FasterXML/jackson。Jackson提供了3种JSON解析方式。
第一种是基于流式API的增量式解析/生成JSON的方式,读写JSON内容的过程是通过离散事件触发的,其底层基于StAX API读取JSON使用org.codehaus.jackson.JsonParser,写入JSON使用org.codehaus.jackson.JsonGenerator。
第二种是基于树型结构的内存模型,提供一种不变式的JsonNode内存树模型,类似DOM树。
第三种是基于数据绑定的方式,org.codehaus.jackson.map.ObjectMapper解析,使用JAXB的注解。
下面开始讲述使用Jackson实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的Jackson示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-3.simple-service-jackson。
(1)定义依赖
使用Jackson方式处理JSON类型的数据,需要在项目的Maven配置中声明如下依赖。
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
</dependency>
(2)定义Application
使用Jackson的应用,需要在其Application中注册JacksonFeature。同时,如果有必要根据不同的实体类做详细的解析,可以注册ContextResolver的实现类,示例代码如下。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends
ResourceConfig {
public JsonResourceConfig() {
register(BookResource.class);
register(JacksonFeature.class);
//关注点1:注册ContextResolver的实现类JsonContextProvider
register(JsonContextProvider.class);
}
}
在这段代码中,注册了ContextResolver的实现类JsonContextProvider,用于提供JSON数据的上下文,见关注点1。有关ContextResolver详细信息参考3.2节。
(3)定义POJO
本例定义了3种不同方式的POJO,以演示Jackson处理JSON的多种方式。分别是JsonBook、JsonHybridBook和JsonNoJaxbBook。第一种方式是仅用JAXB注解的普通的POJO,示例类JsonBook如下。
@XmlRootElement
@XmlType(propOrder = {"bookId",
"bookName", "chapters"})
public class JsonBook {
private String[] chapters;
private String bookId;
private String bookName;
public JsonBook() {
bookId = "1";
bookName = "Java Restful Web Services实战";
chapters = new String[0];
}
...
}
第二种方式是将JAXB的注解和Jackson提供的注解混合使用的POJO,示例类JsonHybridBook如下。
//关注点1:使用JAXB注解
@XmlRootElement
public class JsonHybridBook {
//关注点2:使用Jackson注解
@JsonProperty("bookId")
private String bookId;
@JsonProperty("bookName")
private String bookName;
public JsonHybridBook() {
bookId = "2";
bookName = "Java Restful Web Services实战";
}
}
在这段代码中,分别使用了JAXB的注解javax.xml.bind.annotation.XmlRootElement,见关注点1,和Jackson的注解org.codehaus.jackson.annotate.JsonProperty,见关注点2,定义XML根元素和XML属性。
第三种方式是不使用任何注解的POJO,示例类JsonNoJaxbBook如下。
public class JsonNoJaxbBook {
private String[] chapters;
private String bookId;
private String bookName;
public JsonNoJaxbBook() {
bookId = "3";
bookName = "Java Restful Web Services使用指南";
chapters = new String[0];
}
...
}
这样的3种POJO如何使用Jackson处理来处理呢?我们继续往下看。
(4)定义资源类
资源类BookResource用于演示Jackson对上述3种不同POJO的支持,示例代码如下。
@Path("books")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class BookResource {
@Path("/emptybook")
@GET
//关注点1:支持第一种方式的POJO类型
public JsonBook getEmptyArrayBook() {
return new JsonBook();
}
@Path("/hybirdbook")
@GET
//关注点2:支持第二种方式的POJO类型
public JsonHybridBook getHybirdBook() {
return new JsonHybridBook();
}
@Path("/nojaxbbook")
@GET
//关注点3:支持第三种方式的POJO类型
public JsonNoJaxbBook getNoJaxbBook() {
return new JsonNoJaxbBook();
}
……
在这段代码中,资源类BookResource定义了路径不同的3个GET方法,返回类型分别对应上述的3种POJO,见关注点1到3。有了这样的资源类,就可以向其发送GET请求,并获取不同类型的JSON数据,以研究Jackson是如何支持这3种POJO的JSON转换。
(5)上下文解析实现类
JsonContextProvider是ContextResolver(上下文解析器)的实现类,其作用是根据上下文提供的POJO类型,分别提供两种解析方式。第一种是默认的方式,第二种是混合使用Jackson和Jaxb。两种解析方式的示例代码如下。
@Provider
public class JsonContextProvider implements
ContextResolver<ObjectMapper> {
final ObjectMapper d;
final ObjectMapper c;
public JsonContextProvider() {
//关注点1:实例化ObjectMapper
d = createDefaultMapper();
c = createCombinedMapper();
}
private static ObjectMapper createCombinedMapper() {
Pair ps = createIntrospector();
ObjectMapper result = new ObjectMapper();
result.setDeserializationConfig(
result.getDeserializationConfig().withAnnotationIntrospector(ps));
result.setSerializationConfig(
result.getSerializationConfig().withAnnotationIntrospector(ps));
return result;
}
private static ObjectMapper createDefaultMapper() {
ObjectMapper result = new ObjectMapper();
result.configure(Feature.INDENT_OUTPUT, true);
return result;
}
private static Pair createIntrospector() {
AnnotationIntrospector p = new JacksonAnnotationIntrospector();
AnnotationIntrospector s = new JaxbAnnotationIntrospector();
return new Pair(p, s);
}
@Override public ObjectMapper
getContext(Class<\?> type) {
//关注点2:判断POJO类型返回相应的ObjectMapper实例
if (type == JsonHybridBook.class) {
return c;
} else {
return d;
}
}
}
在这段代码中,JsonContextProvider定义并实例化了两种类型ObjectMapper,见关注点1;在实现接口方法getContext()中,通过判断当前POJO的类型,返回两种ObjectMapper实例之一,见关注点2。通过这样的实现,当流程获取JSON上下文时,既可使用Jackson依赖包完成对相关POJO的处理。
(6)单元测试
单元测试类BookResourceTest的目的是对支持上述3种POJO的资源地址发起请求并测试结果,示例如下。
public class BookResourceTest extends
JerseyTest {
private static final Logger LOGGER =
Logger.getLogger(BookResourceTest.class);
WebTarget booksTarget = target("books");
@Override
protected ResourceConfig configure() {
//关注点1:服务器端配置
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
ResourceConfig resourceConfig = new ResourceConfig(BookResource.class);
//关注点2:注册JacksonFeature
resourceConfig.register(JacksonFeature.class);
return resourceConfig;
}
@Override
protected void configureClient(ClientConfig config) {
//关注点3:注册JacksonFeature
config.register(new JacksonFeature());
config.register(JsonContextProvider.class);
}
@Test
//关注点4:测试出参为JsonBook类型的资源方法
public void testEmptyArray() {
JsonBook book =
booksTarget.path("emptybook").request(MediaType.APPLICATION_JSON).get(JsonBook.class);
LOGGER.debug(book);
}
@Test
//关注点5:测试出参为JsonHybridBook类型的资源方法
public void testHybrid() {
JsonHybridBook book =
booksTarget.path("hybirdbook").request(MediaType
.APPLICATION_JSON).get(JsonHybridBook.class);
LOGGER.debug(book);
}
@Test
//关注点6:测试出参为JsonNoJaxbBook类型的资源方法
public void testNoJaxb() {
JsonNoJaxbBook book =
booksTarget.path("nojaxbbook").request(MediaType.
APPLICATION_JSON).get(JsonNoJaxbBook.class);
LOGGER.debug(book);
}
……
在这段代码中,首先要在服务器端注册支持Jackson功能,见关注点2;同时在客户端也要注册支持Jackson功能并注册JsonContextProvider,见关注点3;该测试类包含了用于测试3种类型POJO的测试用例,见关注点4到6;注意,configure()方法是覆盖测试服务器实例行为,configureClient()方法是覆盖测试客户端实例行为,见关注点1。
(7)集成测试
使用cURL对本例进行集成测试,结果如下所示。
curl http://localhost:8080/simple-service-jackson/api/books
{
"bookList" : [ {
"bookId" : 1,
"bookName" : "JSF2和RichFaces4使用指南",
"isbn" : "9787121177378",
"publisher" : "电子工业出版社",
"publishTime" : "2012-09-01"
},
{
"bookId" : 2,
"bookName" : "Java Restful Web Services实战",
"isbn" : "9787111478881",
"publisher" : "机械工业出版社",
"publishTime" : "2014-09-01"
},
{
"bookId" : 3,
"bookName" : "Java EE 7 精髓",
"isbn" : "9787115375483",
"publisher" : "人民邮电出版社",
"publishTime" : "2015-02-01"
},
{
"bookId" : 4,
"bookName" : "Java Restful Web Services实战II",
"isbn" : null,
"publisher" : "机械工业出版社",
"publishTime" : null
} ]
}
curl
http://localhost:8080/simple-service-jackson/api/books/emptybook
{
"chapters" : [ ],
"bookId" : "1",
"bookName" : "Java Restful Web Services实战"
}
curl
http://localhost:8080/simple-service-jackson/api/books/hybirdbook
{"JsonHybridBook":{"bookId":"2","bookName":"Java
Restful Web Services实战"}}
curl http://localhost:8080/simple-service-jackson/api/books/nojaxbbook
{
"chapters" : [ ],
"bookId" : "3",
"bookName" : "Java Restful Web Services实战"
}
4. 使用Jettison处理JSON
Jettison是一种使用StAX来解析JSON的实现。项目地址是:http://jettison.codehaus.org。Jettison项目起初用于为CXF提供基于JSON的Web服务,在XStream的Java对象的序列化中也使用了Jettison。Jettison支持两种JSON映射到XML的方式。Jersey默认使用MAPPED方式,另一种叫做BadgerFish方式。
下面开始讲述使用Jettison实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的Jettison示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-4.simple-service-jettison。
(1)定义依赖
使用Jettison方式处理JSON类型的数据,需要在项目的Maven配置中声明如下依赖。
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jettison</artifactId>
</dependency>
(2)定义Application
使用Jettison的应用,需要在其Application中注册JettisonFeature。同时,如果有必要根据不同的实体类做详细的解析,可以注册ContextResolver的实现类,示例代码如下。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends
ResourceConfig {
public JsonResourceConfig() {
register(BookResource.class);
//关注点1:注册JettisonFeature和ContextResolver的实现类JsonContextResolver
register(JettisonFeature.class);
register(JsonContextResolver.class);
}
}
在这段代码中,注册了Jettison功能JettisonFeature和ContextResolver的实现类JsonContextResolver,以便使用Jettison处理JSON,见关注点1。
(3)定义POJO
本例定义了两个类名不同、内容相同的POJO(JsonBook和JsonBook2),用以演示Jettison对JSON数据以JETTISON_MAPPED(default notation)和BADGERFISH两种不同方式的处理情况。
@XmlRootElement
public class JsonBook {
private String bookId;
private String bookName;
public JsonBook() {
bookId = "1";
bookName = "Java Restful Web Services实战";
}
...
}
(4)定义资源类
资源类BookResource为两种JSON方式提供了资源地址,示例如下。
@Path("books")
public class BookResource {
...
@Path("/jsonbook")
@GET
//关注点1:返回类型为JsonBook的GET方法
public JsonBook getBook() {
final JsonBook book = new JsonBook();
BookResource.LOGGER.debug(book);
return book;
}
@Path("/jsonbook2")
@GET
//关注点2:返回类型为JsonBook2的GET方法
public JsonBook2 getBook2() {
final JsonBook2 book = new JsonBook2();
BookResource.LOGGER.debug(book);
return book;
}
}
在这段代码中,资源类BookResource定义了路径不同的两个GET方法,返回类型分别是JsonBook和JsonBook2,见关注点1和2。有了这样的资源类,就可以向其发送GET请求,并获取不同类型的JSON数据,以研究Jettison是如何处理JETTISON_MAPPED和BADGERFISH两种不同格式的JSON数据的。
(5)上下文解析实现类
JsonContextResolver实现了ContextResolver接口,示例如下。
@Provider
public class JsonContextResolver implements
ContextResolver<JAXBContext> {
private final JAXBContext context1;
private final JAXBContext context2;
@SuppressWarnings("rawtypes")
public JsonContextResolver() throws Exception {
Class[] clz = new Class[]{JsonBook.class, JsonBook2.class, Books.class,
Book.class};
//关注点1:实例化JettisonJaxbContext
this.context1 = new JettisonJaxbContext(JettisonConfig.DEFAULT, clz);
this.context2 = new
JettisonJaxbContext(JettisonConfig.badgerFish().build(), clz);
}
@Override
public JAXBContext getContext(Class<\?> objectType) {
//关注点2:判断POJO类型返回相应的JAXBContext实例
if (objectType == JsonBook2.class) {
return context2;
} else {
return context1;
}
}
}
在这段代码中,JsonContextResolver定义了两种JAXBContext分别使用MAPPED方式或者BadgerFish方式,见关注点1。这两种方式的参数信息来自Jettision依赖包的JettisonConfig类。在实现接口方法getContext()中,根据不同的POJO类型,返回两种JAXBContext实例之一,见关注点2。通过这样的实现,当流程获取JSON上下文时,既可使用Jettision依赖包完成对相关POJO的处理。
(6)单元测试
单元测试类BookResourceTest的目的是对支持上述两种JSON方式的资源地址发起请求并测试结果,示例如下。
public class BookResourceTest extends
JerseyTest {
private static final Logger LOGGER =
Logger.getLogger(BookResourceTest.class);
@Override
protected ResourceConfig configure() {
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
ResourceConfig resourceConfig = new ResourceConfig(BookResource.class);
//关注点1:注册JettisonFeature和JsonContextResolver
resourceConfig.register(JettisonFeature.class);
resourceConfig.register(JsonContextResolver.class);
return resourceConfig;
}
@Override
protected void configureClient(ClientConfig config) {
//关注点2:注册JettisonFeature和JsonContextResolver
config.register(new
JettisonFeature()).register(JsonContextResolver.class);
}
@Test
public void testJsonBook() {
//关注点3:测试返回类型为JsonBook的GET方法
JsonBook book = target("books").path("jsonbook")
.request(MediaType.APPLICATION_JSON).get(JsonBook.class);
LOGGER.debug(book);
//{"jsonBook":{"bookId":1,"bookName":"abc"}}
}
@Test
public void testJsonBook2() {
//关注点4:测试返回类型为JsonBook2的GET方法
JsonBook2 book = target("books").path("jsonbook2")
.request(MediaType.APPLICATION_JSON).get(JsonBook2.class);
LOGGER.debug(book);
//{"jsonBook2":{"bookId":{"$":"1"},"bookName":{"$":"abc"}}}
}
...
}
在这段代码中,首先要在服务器和客户端两侧注册Jettison功能和JsonContextResolver,见关注点1和2。该测试类包含了用于测试两种格式JSON数据的测试用例,见关注点3和4。
(7)集成测试
使用cURL对本例进行集成测试,结果如下所示。可以看到Mapped和Badgerfish两种方式的JSON数据内容不同。
curl
http://localhost:8080/simple-service-jettison/api/books
{"books":{"bookList":{"book":[{"@bookId":"1","@bookName":"JSF2和RichFaces4使用指南","@publisher":"电子工业出版社","isbn":9787121177378,"publishTime":"2012-09-01"},{"@bookId":"2","@bookName":"Java
Restful Web Services实战","@publisher":"机械工业出版社","isbn":9787111478881,"publishTime":"2014-09-01"},{"@bookId":"3","@bookName":"Java
EE 7 精髓","@publisher":"人民邮电出版社","isbn":9787115375483,"publishTime":"2015-02-01"},{"@bookId":"4","@bookName":"Java
Restful Web Services实战II","@publisher":"机械工业出版社"}]}}}
Jettison mapped notation
curl
http://localhost:8080/simple-service-jettison/api/books/jsonbook
{"jsonBook":{"bookId":1,"bookName":"Java
Restful Web Services实战"}}
Badgerfish notation
curl
http://localhost:8080/simple-service-jettison/api/books/jsonbook2
{"jsonBook2":{"bookId":{"$":"1"},"bookName":{"$":"Java
Restful Web Services实战"}}}
最后简要介绍一下Atom类型。
Atom是一种基于XML的文档格式,该格式的标准定义在IETF RFC 4287(Atom Syndication Format,Atom联合格式),其推出的目的是用来替换RSS。AtomPub是基于Atom的发布协议,定义在IETF RFC 5023(Atom Publishing Protocol)。
Jersey2没有直接引入支持Atom格式的媒体包,但Jersey1.x中包含jersey-atom包。这说明Jersey的基本架构可以支持基于XML类型的数据,这种可插拔的媒体包支持对于Jersey本身更具灵活性,对使用Jersey的REST服务更具可扩展性。