Java RESTful Web Service实战(第2版) 2.3 传输格式-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

Java RESTful Web Service实战(第2版) 2.3 传输格式

简介:

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服务更具可扩展性。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: