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

    xiaoxiao2024-06-04  123

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

    相关资源:Java RESTful Web Service实战 (第2版)-随书源码
    最新回复(0)