JBoss.orgCommunity Documentation

第 22 章 JAXB providers

22.1. JAXB 装饰者
22.2. ContextResolvers 提供插件式 JAXBContext
22.3. JAXB + XML provider
22.3.1. @XmlHeader 和 @Stylesheet
22.4. JAXB + JSON provider
22.5. JAXB + FastinfoSet provider
22.6. JAXB 对象的数组和集合
22.6.1. 在客户端检索集合
22.6.2. JSON and JAXB 集合/数组
22.7. JAXB 对象的 Maps
22.7.1. 客户端检索Maps
22.7.2. JSON and JAXB maps
22.8. 接口、抽象类和 JAXB
22.9. 配置 JAXB 编组

根据规范的要求,RESTEasy JAX-RS 包括对(取消)编组 JAXB 类的支持。RESTEasy 提供了多个 JAXB 提供程序,以解决 XJC 生成的类与简单地用 @XmlRootElement 注解或直接使用 JAXBElement 之间的一些细微差别。

在大多数情况下,使用 JAX-RS API 的开发人员将完全透明地选择调用哪个提供程序。对于希望直接访问提供程序的开发人员(大多数人不需要这样做) ,本文档描述了对于不同的配置如何选择最适合的提供程序。

RESTEasy 选择 JAXB 提供程序是通过判断参数或返回类型是否用 JAXB 注解(比如 @XmlRootEntity 或 @XmlType)的对象,或者类型是否是 JAXBElement。此外,resource 类或 resource 方法将使用@Consumes 或@Produces 注解并包含以下一个或多个值:

RESTEasy 将根据 resource 中使用的返回类型或参数类型选择不同的提供程序。本节描述选择过程是如何工作的。

当用 @XmlRootEntity 注解一个类时,RESTEasy 将选择 JAXBXmlRootElementProvider。此提供程序处理自定义 JAXB 实体的基本编组和反编组处理。

由 XJC 生成的 @XmlType 类很可能不包含 @XmlRootEntity 注解。为了编组这些类,必须将它们封装在 JAXBElement 实例中。这通常通过在类上调用一个方法来实现,该方法由 XmlRegistry 类提供,并命名方法为 ObjectFactory。

当使用 XmlType 注解而不是 XmlRootElement 注解对类进行注解时,就会选择 JAXBXmlTypeProvider 提供程序。

提供程序通过尝试定位目标类的 XmlRegistry 来简化这个任务。默认情况下,JAXB 实现将创建一个名为 ObjectFactory 的类,该类与目标类位于同一个包中。当找到这个类时,它将包含一个"create"方法,该方法将对象实例作为参数。例如,如果目标类型被称为"Contact",那么 ObjectFactory 类将有一个方法:

public JAXBElement createContact(Contact value) {..

JAXBElement<?> 如果您的资源直接使用 JAXBElement 类,RESTEasy 运行时将选择 JAXBElementProvider。此提供程序检查 JAXBElement 的 ParameterizedType 值,以选择适当的 JAXBContext。

Resteasy 的 JAXB 提供程序有一种可插入的方式来装饰 Marshaller 和 Unmarshaller 实例。它的工作方式是,您可以编写一个注解,这个注解可以触发装饰 Marshaller 或 Unmarshaller。装饰器可以执行设置 Marshaller 或 Unmarshaller 属性、设置validation,stuff之类的操作。这里有一个例子。假设我们希望有一个能够触发 XML 文档的pretty-printing、nice formatting 的注解。如果我们使用原始 JAXB,我们将在 Marshaller 的 Marshaller 上设置一个属性(JAXB_FORMATTED_OUTPUT)。让我们编写一个 Marshaller 装饰器。

首先我们定义一个注解:

import org.jboss.resteasy.annotations.Decorator;

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Decorator(processor = PrettyProcessor.class, target = Marshaller.class)
public @interface Pretty {}

为了实现这一点,我们必须使用一个名为 @Decorator 的元注解来注解我们定义的 @Pretty 注解。target() 属性必须是 JAXB Marshaller 类。processor() 属性是我们接下来要编写的类。

import org.jboss.resteasy.core.interception.DecoratorProcessor;
import org.jboss.resteasy.annotations.DecorateTypes;

import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.Produces;
import java.lang.annotation.Annotation;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@DecorateTypes({"text/*+xml", "application/*+xml"})
public class PrettyProcessor implements DecoratorProcessor<Marshaller, Pretty>
{
    public Marshaller decorate(Marshaller target, Pretty annotation,
                  Class type, Annotation[] annotations, MediaType mediaType)
    {
       target.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
    }
}

processor 实现类必须实现 DecoratorProcessor 接口,并且还应该使用 @DecorateTypes 注解。此注解指定processor 可以使用的媒体类型。现在我们已经定义了注解和processor,我们可以在 JAX-RS 资源方法或 JAXB 类型上使用它,如下所示:

@GET
@Pretty
@Produces("application/xml")
public SomeJAXBObject get() {...}

如果您感到困惑,请查看 RESTEasy 源代码以了解 @XmlHeader 的实现

除非你知道你在做什么,否则你不应该使用这个功能。

根据要编组/反编组的类,RESTEasy 将默认为每个类的类型创建和缓存 JAXBContext 实例。如果不希望 RESTEasy 创建 JAXBContexts,可以通过实现 javax.ws.rs.ext.ContextResolver 实例插入自己的实例。

public interface ContextResolver<T>
{
    T getContext(Class<?> type);
}

@Provider
@Produces("application/xml")
public class MyJAXBContextResolver implements ContextResolver<JAXBContext>
{
    JAXBContext getContext(Class<?> type)
    {
        if (type.equals(WhateverClassIsOverridedFor.class)) return JAXBContext.newInstance()...;
    }
}

您必须提供 @Produces 注解来指定上下文所针对的媒体类型。您还必须确保实现 ContextResolver<JAXBContext> 。这有助于运行时与正确的上下文解析器匹配。您还必须使用 @Provider 注解 ContextResolver 类。

有多种方法使用这个 ContextResolver 。

  1. 从 javax.ws.rs.core.Application 返回该类的实例
  2. 作为resteasy.providers中的一个provider
  3. 让 RESTEasy 在您的 WAR 文件中自动扫描它。参见配置指南
  4. 手动添加它通过 ResteasyProviderFactory.getInstance().registerProvider(Class) 或者 registerProviderInstance(Object)

RESTEasy 需要为 XML 提供 JAXB 提供者。它有一些额外的注解,可以帮助编写你的应用程序。

RESTEasy 允许您将带有 JAXB 注解的 POJOs 与 JSON 进行交互。这个提供者包装了 Jackson2 库来完成这个任务。

要集成Jackson,您需要导入 resteasy-jackson2-provider Maven 模块。

例如,考虑下面这个 JAXB 类:

@XmlRootElement(name = "book")
public class Book
{
    private String author;
    private String ISBN;
    private String title;

    public Book()
    {
    }

    public Book(String author, String ISBN, String title)
    {
        this.author = author;
        this.ISBN = ISBN;
        this.title = title;
    }

    @XmlElement
    public String getAuthor()
    {
        return author;
    }

    public void setAuthor(String author)
    {
    this.author = author;
    }

    @XmlElement
    public String getISBN()
    {
        return ISBN;
    }

    public void setISBN(String ISBN)
    {
        this.ISBN = ISBN;
    }

    @XmlAttribute
    public String getTitle()
    {
        return title;
    }

    public void setTitle(String title)
    {
        this.title = title;
    }
}

我们可以写一个方法来使用上面的实体:

@Path("/test_json")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Book test_json() {
    Book book = new Book();
    book.setTitle("EJB 3.0");
    book.setAuthor("Bill Burke");
    book.setISBN("596529260");
    return book;
}

从上面的方法请求,我们可以看到默认的 Jackson2 编组将返回如下的 JSON:

$ http localhost:8080/dummy/test_json
HTTP/1.1 200
...
Content-Type: application/json

{
"ISBN": "596529260",
"author": "Bill Burke",
"title": "EJB 3.0"
}

RESTEasy 使用带 JAXB 注解的类支持转换成 FastinfoSet 类型。与逻辑上等价的 XML 文档相比,FastinfoSet 文档的序列化和解析速度更快,体积更小。因此,只要 XML 文档的大小和处理时间存在问题,就可以使用 FastinfoSet 文档。它的配置方式与 XML JAXB 提供程序相同,因此实际上不需要其他文档。

要使用与 Fastinfoset 的集成,您需要导入 resteasy-fastinfoset-provider Maven 模块。RESTEasy 的旧版本过去常常在 resteasy-jaxb-provider 中包含这一内容,但我们决定将其更模块化。

RESTEasy 将 数组 、java.util.Set's 和 java.util.List's 的JAXB对象转换(反序列化)成 XML、 JSON、 Fastinfoset (或 Restasy 提供的任何其他新的 JAXB 映射器)之间的 JAXB 对象列表。

@XmlRootElement(name = "customer")
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer
{
    @XmlElement
    private String name;

    public Customer()
    {
    }

    public Customer(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }
}

@Path("/")
public class MyResource
{
    @PUT
    @Path("array")
    @Consumes("application/xml")
    public void putCustomers(Customer[] customers)
    {
        Assert.assertEquals("bill", customers[0].getName());
        Assert.assertEquals("monica", customers[1].getName());
    }

    @GET
    @Path("set")
    @Produces("application/xml")
    public Set<Customer> getCustomerSet()
    {
        HashSet<Customer> set = new HashSet<Customer>();
        set.add(new Customer("bill"));
        set.add(new Customer("monica"));

        return set;
    }

    @PUT
    @Path("list")
    @Consumes("application/xml")
    public void putCustomers(List<Customer> customers)
    {
        Assert.assertEquals("bill", customers.get(0).getName());
        Assert.assertEquals("monica", customers.get(1).getName());
    }
}

上面的资源可以发布和接收 JAXB 对象

<collection>
    <customer><name>bill</name></customer>
    <customer><name>monica</name></customer>
<collection>

可以使用 @org.jboss.resteasy.annotations.providers.jaxb.Wrapped 更改namespace URI、namespace tag 和集合元素名称。

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Wrapped
{
    String element() default "collection";

    String namespace() default "http://jboss.org/resteasy";

    String prefix() default "resteasy";
}

所以,如果我们想输出这个 XML

<foo:list xmlns:foo="http://foo.org">
    <customer><name>bill</name></customer>
    <customer><name>monica</name></customer>
</foo:list>

我们将使用 @Wrapped 注解如下:

@GET
@Path("list")
@Produces("application/xml")
@Wrapped(element="list", namespace="http://foo.org", prefix="foo")
public List<Customer> getCustomerSet()
{
    List<Customer> list = new ArrayList<Customer>();
    list.add(new Customer("bill"));
    list.add(new Customer("monica"));

    return list;
}

RESTEasy 将自动将 JAXB 对象的映射编组到 XML、 JSON、 Fastinfoset (或 Restasy 提供的任何其他新的 JAXB 映射)。您的参数或方法返回类型必须是一个泛型,其中 String 为键,类型为 JAXB 对象。

@XmlRootElement(namespace = "http://foo.com")
public static class Foo
{
    @XmlAttribute
    private String name;

    public Foo()
    {
    }

    public Foo(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }
}

@Path("/map")
public static class MyResource
{
    @POST
    @Produces("application/xml")
    @Consumes("application/xml")
    public Map<String, Foo> post(Map<String, Foo> map)
    {
        Assert.assertEquals(2, map.size());
        Assert.assertNotNull(map.get("bill"));
        Assert.assertNotNull(map.get("monica"));
        Assert.assertEquals(map.get("bill").getName(), "bill");
        Assert.assertEquals(map.get("monica").getName(), "monica");
        return map;
    }
}

上面的 resource 可以在一个 map 中发布和接收 JAXB 对象。默认情况下,它们包装在默认名称空间中的"map"元素中。此外,每个"map"元素都有零个或多个带有"key"属性的"entry"元素。

<map>
    <entry key="bill" xmlns="http://foo.com">
        <foo name="bill"/>
    </entry>
    <entry key="monica" xmlns="http://foo.com">
        <foo name="monica"/>
    </entry>
</map>

可以使用 @org.jboss.resteasy.annotations.providers.jaxb.WrappedMap 更改namespace URI、名称空间前缀和映射、条目以及关键元素和属性名称。

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WrappedMap
{
    /**
     * map element name
     */
    String map() default "map";

    /**
     * entry element name *
     */
    String entry() default "entry";

    /**
     * entry's key attribute name
     */
    String key() default "key";

    String namespace() default "";

    String prefix() default "";
}

所以,如果我们想输出这个 XML

<hashmap>
    <hashentry hashkey="bill" xmlns:foo="http://foo.com">
        <foo:foo name="bill"/>
    </hashentry>
</map>

我们将使用 @WrappedMap 注解如下:

@Path("/map")
public static class MyResource
{
    @GET
    @Produces("application/xml")
    @WrappedMap(map="hashmap", entry="hashentry", key="hashkey")
    public Map<String, Foo> get()
    {
        ...
        return map;
    }
}

有些对象模型大量使用抽象类和接口。不幸的是,JAXB 无法处理作为根元素的接口,而 RESTEasy 无法解封作为接口或原始抽象类的参数,因为它没有足够的信息来创建 JAXBContext。例如:

public interface IFoo {}

@XmlRootElement
public class RealFoo implements IFoo {}

@Path("/jaxb")
public class MyResource {

    @PUT
    @Consumes("application/xml")
    public void put(IFoo foo) {...}
}

在这个例子中,您将从 RESTEasy 中得到一个错误,例如"Cannot find a MessageBodyReader for..."。这是因为 RESTEasy 不知道 IFoo 的实现是 JAXB 类,也不知道如何为它创建 JAXBContext。作为一种解决方案,RESTEasy 允许您在接口上使用 JAXB 注解 @XmlSeeAlso 来纠正问题。(注意,这不适用于手动的、手工编码的 JAXB)。

@XmlSeeAlso(RealFoo.class)
public interface IFoo {}

IFoo 上的额外 @XmlSeeAlso 允许 RESTEasy 创建一个 JAXBContext,它知道如何编组 RealFoo 实例。

作为 XML 数据集的消费者,JAXB 受到一种称为 XXE (XML eXternal Entity) Attack (http://www.securiteam.com/securitynews/6D0100A5PU.html )的攻击,在这种攻击中,展开外部实体会导致加载不安全的文件。在第 21.4 节 “配置文档编排”中讨论了如何防止外部实体的扩展。同样的参数,

resteasy.document.expand.entity.references

也适用于 JAXB 解组器。

第 21.4 节 “配置文档编排”还讨论了禁止 dtd 和对实体扩展的限制以及每个元素的属性数。

resteasy.document.secure.disableDTDs

resteasy.document.secure.processing.feature

以及它们的默认值,也适用于 JAXB 对象的表示。