Как использовать Spring Маршрутизация RestTemplate и JAXB по URL-адресу, который возвращает несколько типов XML

Мне нужно сделать 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;
}

К сожалению, это не работает, потому что основной входной поток внутри источника был закрыт к моменту его повторного запуска.

Любые предложения, полученные с благодарностью,

Tom

ОБНОВЛЕНИЕ: у меня есть это для работы, взяв копию 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());
}
}

Ответ 1

@Tom: Я не думаю, что создание настраиваемого MarshallingHttpMessageConverter принесет вам любую пользу. Встроенный конвертер возвращает вам правильный класс (класс исключения), когда служба выходит из строя, но это RestTemplate, который не знает, как вернуть класс исключения вызываемому, поскольку вы указали тип ответа как класс Job.

Я прочитал исходный код 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 и добавить новый API postForObject(), который возвращает объект вместо типа T, чтобы вы могли выполнить проверку instanceof на возвращаемом объекте.

UPDATE

Я думал о решении этой проблемы, вместо использования встроенного 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, вы можете создать адаптер, который обертывает вашу собственную реализацию и повторно использовать API-интерфейсы RestTemplate, без фактического раскрытия API-интерфейсов RestTemplate по всему вашему коду.

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

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, и у вас больше контроля над тем, как вы хотите обрабатывать результаты вызовов веб-службы.

Ответ 2

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

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

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

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

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

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

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

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

Это несколько хак, но он решает проблему метода postForObject, не способную вернуть более одного класса объектов.