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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

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

相关文章
|
6天前
【Azure 应用服务】Web App Service 中的 应用程序配置(Application Setting) 怎么获取key vault中的值
【Azure 应用服务】Web App Service 中的 应用程序配置(Application Setting) 怎么获取key vault中的值
|
7天前
|
关系型数据库 MySQL Linux
【Azure 应用服务】在创建Web App Service的时候,选Linux系统后无法使用Mysql in App
【Azure 应用服务】在创建Web App Service的时候,选Linux系统后无法使用Mysql in App
【Azure 应用服务】在创建Web App Service的时候,选Linux系统后无法使用Mysql in App
|
7天前
|
Java
【Azure 应用服务】如何查看App Service Java堆栈JVM相关的参数默认配置值?
【Azure 应用服务】如何查看App Service Java堆栈JVM相关的参数默认配置值?
【Azure 应用服务】如何查看App Service Java堆栈JVM相关的参数默认配置值?
|
7天前
|
安全 Java 应用服务中间件
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
|
6天前
|
Shell PHP Windows
【Azure App Service】Web Job 报错 UNC paths are not supported. Defaulting to Windows directory.
【Azure App Service】Web Job 报错 UNC paths are not supported. Defaulting to Windows directory.
|
7天前
|
Linux 应用服务中间件 网络安全
【Azure 应用服务】查看App Service for Linux上部署PHP 7.4 和 8.0时,所使用的WEB服务器是什么?
【Azure 应用服务】查看App Service for Linux上部署PHP 7.4 和 8.0时,所使用的WEB服务器是什么?
|
7天前
|
存储 Java 开发工具
【Azure 存储服务】Java Azure Storage SDK V12使用Endpoint连接Blob Service遇见 The Azure Storage endpoint url is malformed
【Azure 存储服务】Java Azure Storage SDK V12使用Endpoint连接Blob Service遇见 The Azure Storage endpoint url is malformed
|
7天前
|
编解码 Java API
【Azure 媒体服务】记录使用Java调用Media Service API时候遇见的一些问题
【Azure 媒体服务】记录使用Java调用Media Service API时候遇见的一些问题
|
7天前
【Azure 应用服务】通过 Web.config 开启 dotnet 应用的 stdoutLog 日志,查看App Service 产生500错误的原因
【Azure 应用服务】通过 Web.config 开启 dotnet 应用的 stdoutLog 日志,查看App Service 产生500错误的原因
|
7天前
|
Java 开发工具 Spring
【Azure Spring Cloud】使用azure-spring-boot-starter-storage来上传文件报错: java.net.UnknownHostException: xxxxxxxx.blob.core.windows.net: Name or service not known
【Azure Spring Cloud】使用azure-spring-boot-starter-storage来上传文件报错: java.net.UnknownHostException: xxxxxxxx.blob.core.windows.net: Name or service not known
下一篇
云函数