Как использовать Spring RestTemplate и JAXB маршалинг для URL, который возвращает несколько типов XML - PullRequest
13 голосов
/ 02 марта 2011

Мне нужно сделать Rest POST для службы, которая возвращает либо <job/>, либо <exception/> и всегда код состояния 200.(Хромой сторонний продукт!).

У меня есть такой код:

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 (Класс clazz, заголовки 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());
}
}

Ответы [ 2 ]

5 голосов
/ 25 января 2014

Пытаясь решить ту же проблему, я нашел следующее решение.

Я использую экземпляр RestTemplate по умолчанию и генерирую файлы с использованием xjc.Преобразователь, который вызывается, является Jaxb2RootElementHttpMessageConverter.

Оказывается, преобразователь возвращает «реальный» тип, если входной класс аннотирован аннотацией XmlRootElement.То есть метод

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

может возвращать объект, который не является экземпляром clazz, учитывая, что в clazz присутствует аннотация XmlRootElement.В этом случае clazz используется только для создания unmarshaller, который будет unmarshall clazz.

Следующая уловка решает проблему: если мы определим

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

и передадим XmlResponse.class в postForObject (...), то ответом будет Exception или Job.

Это что-то вроде хака, но это решает проблему, связанную с тем, что метод postForObject не может возвращать более одного класса объектов.

4 голосов
/ 02 марта 2011

@ Том: Я не думаю, что создание собственного MarshallingHttpMessageConverter пойдет вам на пользу.Встроенный конвертер возвращает вам правильный класс (класс исключений), когда происходит сбой службы, но это 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 и добавить новый postForObject() API, который возвращает объект вместо типа T, чтобы вы могли выполнить проверку 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;
}

Если есть обстоятельства, когда вы хотите повторно использовать некоторые изAPI от RestTemplate, вы можете создать адаптер, который обернет вашу пользовательскую реализацию и повторно использовать RestTemplate API, фактически не выставляя RestTemplate API по всему вашему коду.

Например, вы можете создать интерфейс адаптераПримерно так: -

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

Я все еще думаю, что этот подход намного чище, чем использование подклассов RestTemplateбольше контроля над тем, как вы хотите обрабатывать результаты вызовов веб-службы.

...