SPR-7494 - Introduce interceptors for RestTemplate

This commit is contained in:
Arjen Poutsma 2011-01-28 14:24:52 +00:00
parent 62425568b9
commit c4a954a618
8 changed files with 704 additions and 3 deletions

View File

@ -0,0 +1,44 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client;
import java.io.IOException;
import org.springframework.http.HttpRequest;
/**
* Represents the context of a client-side HTTP request execution.
*
* <p>Used to invoke the next interceptor in the interceptor chain, or - if the calling interceptor is last - execute
* the request itself.
*
* @author Arjen Poutsma
* @see ClientHttpRequestInterceptor
* @since 3.1
*/
public interface ClientHttpRequestExecution {
/**
* Execute the request with the given request attributes and body, and return the response.
*
* @param request the request, containing method, URI, and headers
* @param body the body of the request to execute
* @return the response
* @throws IOException in case of I/O errors
*/
ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException;
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client;
import java.io.IOException;
import org.springframework.http.HttpRequest;
/**
* Intercepts client-side HTTP requests. Implementations of this interface can be {@linkplain
* org.springframework.web.client.RestTemplate#setInterceptors(ClientHttpRequestInterceptor[]) registered} with the
* {@link org.springframework.web.client.RestTemplate RestTemplate}, as to modify the outgoing {@link ClientHttpRequest}
* and/or the incoming {@link ClientHttpResponse}.
*
* <p>The main entry point for interceptors is {@link #intercept(HttpRequest, byte[], ClientHttpRequestExecution)}.
*
* @author Arjen Poutsma
* @since 3.1
*/
public interface ClientHttpRequestInterceptor {
/**
* Intercept the given request, and return a response. The given {@link ClientHttpRequestExecution} allows
* the interceptor to pass on the request and response to the next entity in the chain.
*
* <p>A typical implementation of this method would follow the following pattern:
* <ol>
* <li>Examine the {@linkplain HttpRequest request} and body</li>
* <li>Optionally {@linkplain org.springframework.http.client.support.HttpRequestWrapper wrap} the request to filter HTTP attributes.</li>
* <li>Optionally modify the body of the request.</li>
* <li><strong>Either</strong>
* <ul>
* <li>execute the request using {@link ClientHttpRequestExecution#execute(org.springframework.http.HttpRequest, byte[])},</li>
* <strong>or</strong>
* <li>do not execute the request to block the execution altogether.</li>
* </ul>
* <li>Optionally wrap the response to filter HTTP attributes.</li>
* </ol>
*
* @param request the request, containing method, URI, and headers
* @param body the body of the request
* @param execution the request execution
* @return the response
* @throws IOException in case of I/O errors
*/
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException;
}

View File

@ -0,0 +1,96 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Iterator;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.util.FileCopyUtils;
/**
* Wrapper for a {@link ClientHttpRequest} that has support for {@link ClientHttpRequestInterceptor}s.
*
* @author Arjen Poutsma
* @since 3.1
*/
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
private final ClientHttpRequestFactory requestFactory;
private final ClientHttpRequestInterceptor[] interceptors;
private HttpMethod method;
private URI uri;
protected InterceptingClientHttpRequest(ClientHttpRequestFactory requestFactory,
ClientHttpRequestInterceptor[] interceptors,
URI uri,
HttpMethod method) {
this.requestFactory = requestFactory;
this.interceptors = interceptors;
this.method = method;
this.uri = uri;
}
public HttpMethod getMethod() {
return method;
}
public URI getURI() {
return uri;
}
@Override
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
RequestExecution requestExecution = new RequestExecution();
return requestExecution.execute(this, bufferedOutput);
}
private class RequestExecution implements ClientHttpRequestExecution {
private final Iterator<ClientHttpRequestInterceptor> iterator;
private RequestExecution() {
this.iterator = Arrays.asList(interceptors).iterator();
}
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = iterator.next();
return nextInterceptor.intercept(request, body, this);
}
else {
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());
delegate.getHeaders().putAll(request.getHeaders());
if (body.length > 0) {
FileCopyUtils.copy(body, delegate.getBody());
}
return delegate.execute();
}
}
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client;
import java.io.IOException;
import java.net.URI;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
* Wrapper for a {@link ClientHttpRequestFactory} that has support for {@link ClientHttpRequestInterceptor}s.
*
* @author Arjen Poutsma
* @since 3.1
*/
public class InterceptingClientHttpRequestFactory implements ClientHttpRequestFactory {
private final ClientHttpRequestFactory requestFactory;
private final ClientHttpRequestInterceptor[] interceptors;
/**
* Creates a new instance of the {@code InterceptingClientHttpRequestFactory} with the given parameters.
*
* @param requestFactory the request factory to wrap
* @param interceptors the interceptors that are to be applied. Can be {@code null}.
*/
public InterceptingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory,
ClientHttpRequestInterceptor[] interceptors) {
Assert.notNull(requestFactory, "'requestFactory' must not be null");
this.requestFactory = requestFactory;
this.interceptors = interceptors != null ? interceptors : new ClientHttpRequestInterceptor[0];
}
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
return new InterceptingClientHttpRequest(requestFactory, interceptors, uri, httpMethod);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.support;
import java.net.URI;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.util.Assert;
/**
* Provides a convenient implementation of the {@link HttpRequest} interface that can be overridden to adapt the
* request. Methods default to calling through to the wrapped request object.
*
* @author Arjen Poutsma
* @since 3.1
*/
public class HttpRequestWrapper implements HttpRequest {
private final HttpRequest request;
/**
* Creates a new {@code HttpRequest} wrapping the given request object.
*
* @param request the request object to be wrapped
*/
public HttpRequestWrapper(HttpRequest request) {
Assert.notNull(request, "'request' must not be null");
this.request = request;
}
/**
* Returns the wrapped request.
*/
public HttpRequest getRequest() {
return request;
}
/**
* Returns the method of the wrapped request.
*/
public HttpMethod getMethod() {
return this.request.getMethod();
}
/**
* Returns the URI of the wrapped request.
*/
public URI getURI() {
return this.request.getURI();
}
/**
* Returns the headers of the wrapped request.
*/
public HttpHeaders getHeaders() {
return this.request.getHeaders();
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.support;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.util.ObjectUtils;
/**
* Base class for {@link org.springframework.web.client.RestTemplate} and other HTTP accessing gateway helpers, adding
* interceptor-related properties to {@link HttpAccessor}'s common properties.
*
* <p>Not intended to be used directly. See {@link org.springframework.web.client.RestTemplate}.
*
* @author Arjen Poutsma
*/
public abstract class InterceptingHttpAccessor extends HttpAccessor {
private ClientHttpRequestInterceptor[] interceptors;
/**
* Sets the request interceptors that this accessor should use.
*/
public void setInterceptors(ClientHttpRequestInterceptor[] interceptors) {
this.interceptors = interceptors;
}
/**
* Return the request interceptor that this accessor uses.
*/
public ClientHttpRequestInterceptor[] getInterceptors() {
return interceptors;
}
@Override
public ClientHttpRequestFactory getRequestFactory() {
ClientHttpRequestFactory delegate = super.getRequestFactory();
if (!ObjectUtils.isEmpty(getInterceptors())) {
return new InterceptingClientHttpRequestFactory(delegate, getInterceptors());
}
else {
return delegate;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -33,7 +33,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpAccessor;
import org.springframework.http.client.support.InterceptingHttpAccessor;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
@ -113,7 +113,7 @@ import org.springframework.web.util.UriUtils;
* @see ResponseErrorHandler
* @since 3.0
*/
public class RestTemplate extends HttpAccessor implements RestOperations {
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader());

View File

@ -0,0 +1,308 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.support.HttpRequestWrapper;
import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
*/
public class InterceptingClientHttpRequestFactoryTest {
private InterceptingClientHttpRequestFactory requestFactory;
private RequestFactoryMock requestFactoryMock;
private RequestMock requestMock;
private ResponseMock responseMock;
@Before
public void setUp() throws Exception {
requestFactoryMock = new RequestFactoryMock();
requestMock = new RequestMock();
responseMock = new ResponseMock();
}
@Test
public void basic() throws Exception {
NoOpInterceptor interceptor1 = new NoOpInterceptor();
NoOpInterceptor interceptor2 = new NoOpInterceptor();
NoOpInterceptor interceptor3 = new NoOpInterceptor();
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock,
new ClientHttpRequestInterceptor[]{interceptor1, interceptor2, interceptor3});
ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET);
ClientHttpResponse response = request.execute();
assertTrue(interceptor1.invoked);
assertTrue(interceptor2.invoked);
assertTrue(interceptor3.invoked);
assertTrue(requestMock.executed);
assertSame(responseMock, response);
}
@Test
public void noExecution() throws Exception {
ClientHttpRequestInterceptor interceptor1 = new ClientHttpRequestInterceptor() {
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
return responseMock;
}
};
NoOpInterceptor interceptor2 = new NoOpInterceptor();
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock,
new ClientHttpRequestInterceptor[]{interceptor1, interceptor2});
ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET);
ClientHttpResponse response = request.execute();
assertFalse(interceptor2.invoked);
assertFalse(requestMock.executed);
assertSame(responseMock, response);
}
@Test
public void changeHeaders() throws Exception {
final String headerName = "Foo";
final String headerValue = "Bar";
ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
return execution.execute(new HttpRequestWrapper(request) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set(headerName, headerValue);
return headers;
}
}, body);
}
};
requestMock = new RequestMock() {
@Override
public ClientHttpResponse execute() throws IOException {
assertEquals(headerValue, getHeaders().getFirst(headerName));
return super.execute();
}
};
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock,
new ClientHttpRequestInterceptor[]{interceptor});
ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET);
request.execute();
}
@Test
public void changeURI() throws Exception {
final URI changedUri = new URI("http://example.com/2");
ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
return execution.execute(new HttpRequestWrapper(request) {
@Override
public URI getURI() {
return changedUri;
}
}, body);
}
};
requestFactoryMock = new RequestFactoryMock() {
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
assertEquals(changedUri, uri);
return super.createRequest(uri, httpMethod);
}
};
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock,
new ClientHttpRequestInterceptor[]{interceptor});
ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET);
request.execute();
}
@Test
public void changeMethod() throws Exception {
final HttpMethod changedMethod = HttpMethod.POST;
ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
return execution.execute(new HttpRequestWrapper(request) {
@Override
public HttpMethod getMethod() {
return changedMethod;
}
}, body);
}
};
requestFactoryMock = new RequestFactoryMock() {
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
assertEquals(changedMethod, httpMethod);
return super.createRequest(uri, httpMethod);
}
};
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock,
new ClientHttpRequestInterceptor[]{interceptor});
ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET);
request.execute();
}
@Test
public void changeBody() throws Exception {
final byte[] changedBody = "Foo".getBytes();
ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
return execution.execute(request, changedBody);
}
};
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock,
new ClientHttpRequestInterceptor[]{interceptor});
ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET);
request.execute();
assertTrue(Arrays.equals(changedBody, requestMock.body.toByteArray()));
}
private static class NoOpInterceptor implements ClientHttpRequestInterceptor {
private boolean invoked = false;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
invoked = true;
return execution.execute(request, body);
}
}
private class RequestFactoryMock implements ClientHttpRequestFactory {
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
requestMock.setURI(uri);
requestMock.setMethod(httpMethod);
return requestMock;
}
}
private class RequestMock implements ClientHttpRequest {
private URI uri;
private HttpMethod method;
private HttpHeaders headers = new HttpHeaders();
private ByteArrayOutputStream body = new ByteArrayOutputStream();
private boolean executed = false;
private RequestMock() {
}
public URI getURI() {
return uri;
}
public void setURI(URI uri) {
this.uri = uri;
}
public HttpMethod getMethod() {
return method;
}
public void setMethod(HttpMethod method) {
this.method = method;
}
public HttpHeaders getHeaders() {
return headers;
}
public OutputStream getBody() throws IOException {
return body;
}
public ClientHttpResponse execute() throws IOException {
executed = true;
return responseMock;
}
}
private static class ResponseMock implements ClientHttpResponse {
private HttpStatus statusCode = HttpStatus.OK;
private String statusText = "";
private HttpHeaders headers = new HttpHeaders();
public HttpStatus getStatusCode() throws IOException {
return statusCode;
}
public String getStatusText() throws IOException {
return statusText;
}
public HttpHeaders getHeaders() {
return headers;
}
public InputStream getBody() throws IOException {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
public void close() {
}
}
}