web-dev-qa-db-ja.com

複数のタイプのXMLを返すURLでSpringRestTemplateとJAXBマーシャリングを使用する方法

<job/>または<exception/>のいずれかを返し、常にステータスコード200を返すサービスにREST POSTを作成する必要があります。(サードパーティ製品のラメ!)。

私は次のようなコードを持っています:

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

そして私のapplicationContext.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()呼び出しで、Job.classを要求していますが、取得できず、動揺しています。

私は次のようなことをする必要があると考えています。

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

しかし、これは機能しません。これは、JAXBがObject.classにマーシャリングする方法がわからないと文句を言うためです。当然のことながら。

MarshallingHttpMessageConverterのサブクラスを作成し、readFromSource()をオーバーライドしようとしました

保護されたオブジェクトreadFromSource(クラスクラズ、HttpHeadersヘッダー、ソースソース){

    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;
}

残念ながら、これは機能しません。これは、ソース内の基になる入力ストリームが、再試行するまでに閉じられているためです。

ありがたいことに受け取った提案は、

トム

更新:inputStreamのコピーを取得することでこれを機能させることができます

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());
}
}
13
Tom

@Tom:カスタムのMarshallingHttpMessageConverterを作成しても何の役にも立たないと思います。組み込みのコンバーターは、サービスが失敗したときに適切なクラス(Exceptionクラス)を返しますが、応答タイプを指定したため、呼び出し先にExceptionクラスを返す方法がわからないのは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を返します。おそらく行う必要があるのは、RestTemplateをサブクラス化し、タイプTの代わりにオブジェクトを返す新しいpostForObject() AP​​Iを追加して、instanceofチェックを実行できるようにすることです。返されたオブジェクト。

[〜#〜]更新[〜#〜]

組み込みのRestTemplateを使用する代わりに、この問題の解決策を考えていました。自分で作成してみませんか?新しいメソッドを追加するためにRestTemplateをサブクラス化するよりも良いと思います。

これが私の例です...確かに、私はこのコードをテストしませんでしたが、それはあなたにアイデアを与えるはずです:-

_// 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;
}
_

RestTemplateのAPIの一部を再利用したい状況がある場合は、カスタム実装をラップするアダプターを構築し、実際にRestTemplateAPIを公開せずにRestTemplateAPIを再利用できます。コード全体。

たとえば、次のようなアダプタインターフェイスを作成できます。-

_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);
}
_

具体的なカスタムRESTテンプレートは次のようになります。-

_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);
    }
}
_

このアプローチは、RestTemplateをサブクラス化するよりもはるかにクリーンであり、Webサービス呼び出しの結果をどのように処理するかをより細かく制御できると思います。

5
limc

同じ問題を解決しようとしたときに、次の解決策を見つけました。

RestTemplateのデフォルトのインスタンスを使用しており、xjcを使用してファイルを生成しています。呼び出されるコンバーターはJaxb2RootElementHttpMessageConverterです。

入力クラスにXmlRootElementアノテーションが付けられている場合、コンバーターは「実際の」タイプを返すことがわかります。つまり、メソッド

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

clazzにXmlRootElementアノテーションが存在する場合、clazzのインスタンスではないオブジェクトを返す可能性があります。この場合、clazzは、clazzをアンマーシャルするアンマーシャラーを作成するためにのみ使用されます。

次のトリックが問題を解決します。

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

xmlResponse.classをpostForObject(...)に渡すと、応答はExceptionまたはJobになります。

これは多少のハックですが、postForObjectメソッドが複数のオブジェクトクラスを返すことができないという問題を解決します。

5
Lior