如何在返回多种类型的 XML 的 URL 上使用 Spring RestTemplate 和 JAXB 编组

2022-09-04 21:31:15

我需要对返回 a 或 a 且始终状态代码的服务进行 Rest POST。(蹩脚的第三方产品!<job/><exception/>200

我有这样的代码:

Job job = getRestTemplate().postForObject(url, postData, Job.class);

我的应用程序Context.xml看起来像这样:

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg ref="httpClientFactory"/>

    <property name="messageConverters">
        <list>
            <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
                <property name="marshaller" ref="jaxbMarshaller"/>
                <property name="unmarshaller" ref="jaxbMarshaller"/>
            </bean>
            <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
        </list>
    </property>
</bean>

<bean id="jaxbMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
    <property name="classesToBeBound">
        <list>
            <value>domain.fullspec.Job</value>
            <value>domain.fullspec.Exception</value>
        </list>
    </property>
</bean>

当我尝试进行此调用并且服务失败时,我得到:

 Failed to convert value of type 'domain.fullspec.Exception' to required type 'domain.fullspec.Job'

在postForObject()电话中,我要求一份工作.class没有得到一份工作,它越来越难过。

我想我需要能够做一些事情:

Object o = getRestTemplate().postForObject(url, postData, Object.class);
if (o instanceof Job.class) {
   ...
else if (o instanceof Exception.class) {
}

但这不起作用,因为JAXB抱怨它不知道如何封送到Object.class - 这并不奇怪。

我试图创建编组HttpMessageConverter的子类并覆盖readFromSource()

protected Object readFromSource(Class clazz, HttpHeaders headers, Source source) {

    Object o = null;
    try {
        o = super.readFromSource(clazz, headers, source);
    } catch (Exception e) {
        try {
            o = super.readFromSource(MyCustomException.class, headers, source);
        } catch (IOException e1) {
            log.info("Failed readFromSource "+e);
        }
    }

    return o;
}

不幸的是,这不起作用,因为在我重试时,源中的基础输入流已关闭。

任何建议都非常感谢收到,

汤姆

更新:我通过获取输入流的副本来使其工作

protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) {
    InputStream is = ((StreamSource) source).getInputStream();

    // Take a copy of the input stream so we can use it for initial JAXB conversion
    // and if that fails, we can try to convert to Exception
    CopyInputStream copyInputStream = new CopyInputStream(is);

    // input stream in source is empty now, so reset using copy
    ((StreamSource) source).setInputStream(copyInputStream.getCopy());

    Object o = null;
    try {
        o = super.readFromSource(clazz, headers, source);
      // we have failed to unmarshal to 'clazz' - assume it is <exception> and unmarshal to MyCustomException

    } catch (Exception e) {
        try {

            // reset input stream using copy
            ((StreamSource) source).setInputStream(copyInputStream.getCopy());
            o = super.readFromSource(MyCustomException.class, headers, source);

        } catch (IOException e1) {
            e1.printStackTrace();  
        }
        e.printStackTrace();
    }
    return o;

}

CopyInputStream取自 http://www.velocityreviews.com/forums/t143479-how-to-make-a-copy-of-inputstream-object.html,我会把它粘贴在这里。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class CopyInputStream
{
private InputStream _is;
private ByteArrayOutputStream _copy = new ByteArrayOutputStream();

/**
 * 
 */
public CopyInputStream(InputStream is)
{
    _is = is;

    try
    {
        copy();
    }
    catch(IOException ex)
    {
        // do nothing
    }
}

private int copy() throws IOException
{
    int read = 0;
    int chunk = 0;
    byte[] data = new byte[256];

    while(-1 != (chunk = _is.read(data)))
    {
        read += data.length;
        _copy.write(data, 0, chunk);
    }

    return read;
}

public InputStream getCopy()
{
    return (InputStream)new ByteArrayInputStream(_copy.toByteArray());
}
}

答案 1

在尝试解决同一问题时,我找到了以下解决方案。

我使用的是 RestTemplate 的默认实例,并使用 xjc 生成文件。调用的转换器是Jaxb2RootElementHttpMessageConverter。

事实证明,转换器返回“real”类型,以防输入类使用 XmlRootElement 注释进行批注。即方法

protected Object readFromSource(Class clazz, HttpHeaders headers, Source source)

可能会返回一个不是 clazz 实例的对象,因为 clazz 存在一个 XmlRootElement 注释。在这种情况下,clazz 仅用于创建一个将取消 marshall clazz 的 unmarshaller。

以下技巧解决了这个问题:如果我们定义

@XmlRootElement()
@XmlSeeAlso({ Exception.class, Job.class })
public static abstract class XmlResponse {}

并将 XmlResponse.class 传递给 postForObject(...),则响应将为 Exception 或 Job。

这有点像黑客攻击,但它解决了postForObject方法无法返回多个对象类的问题。


答案 2

@Tom:我不认为创建自定义的MarshallingHttpMessageConverter会对你有任何好处。当服务失败时,内置转换器将返回正确的类(Exception 类),但它不知道如何将 Exception 类返回给被调用方,因为您已将响应类型指定为 Job 类。RestTemplate

我阅读了 RestTemplate 源代码,您当前正在调用此 API:-

public <T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException {
    HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request, responseType);
    HttpMessageConverterExtractor<T> responseExtractor =
            new HttpMessageConverterExtractor<T>(responseType, getMessageConverters());
    return execute(url, HttpMethod.POST, requestCallback, responseExtractor);
}

如您所见,它根据您的响应类型返回类型 T。您可能需要做的是子类化并添加一个新的 API,该 API 返回对象而不是类型 T,以便您可以对返回的对象执行检查。RestTemplatepostForObject()instanceof

更新

我一直在思考这个问题的解决方案,而不是使用内置的,为什么不自己写呢?我认为这比添加新方法的子类化更好。RestTemplateRestTemplate

这是我的例子...当然,我没有测试这个代码,但它应该给你一个想法:-

// reuse the same marshaller wired in RestTemplate
@Autowired
private Jaxb2Marshaller jaxb2Marshaller;

public Object genericPost(String url) {
    // using Commons HttpClient
    HttpClient client = new HttpClient();
    PostMethod method = new PostMethod(url);

    // add your data here
    method.addParameter("data", "your-data");

    try {
        int returnCode = client.executeMethod(method);

        // status code is 200
        if (returnCode == HttpStatus.SC_OK) {
            // using Commons IO to convert inputstream to string
            String xml = IOUtil.toString(method.getResponseBodyAsStream());
            return jaxb2Marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(xml.getBytes("UTF-8"))));
        }
        else {
            // handle error
        }
    }
    catch (Exception e) {
        throw new RuntimeException(e);
    }
    finally {
        method.releaseConnection();
    }

    return null;
}

如果在某些情况下,您希望重用 中的某些 API,则可以构建一个适配器来包装自定义实现并重用 API,而无需在代码中实际公开 API。RestTemplateRestTemplateRestTemplate

例如,您可以创建一个适配器接口,如下所示:-

public interface MyRestTemplateAdapter {
    Object genericPost(String url);

    // same signature from RestTemplate that you want to reuse
    <T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables);
}

具体的自定义休息模板看起来像这样:-

public class MyRestTemplateAdapterImpl implements MyRestTemplateAdapter {
    @Autowired
    private RestTemplate    restTemplate;

    @Autowired
    private Jaxb2Marshaller jaxb2Marshaller;

    public Object genericPost(String url) {
        // code from above
    }

    public <T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) {
        return restTemplate.postForObject(url, request, responseType);
    }
}

我仍然认为这种方法比子类化干净得多,您可以更好地控制如何处理Web服务调用的结果。RestTemplate


推荐