From 7de0a70f0c21d60f244aee0116ef1657d6bca283 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 16 Oct 2014 11:46:22 +0200 Subject: [PATCH] Netty support for (Async)RestTemplate This commit introduces an AsyncClientHttpRequestFactory based on Netty 4, for use with the (Async)RestTemplate. --- build.gradle | 1 + .../http/client/Netty4ClientHttpRequest.java | 186 ++++++++++++++++++ .../Netty4ClientHttpRequestFactory.java | 159 +++++++++++++++ .../http/client/Netty4ClientHttpResponse.java | 90 +++++++++ ...stractAsyncHttpRequestFactoryTestCase.java | 46 +++-- .../AbstractHttpRequestFactoryTestCase.java | 26 ++- .../client/AbstractJettyServerTestCase.java | 9 +- ...ty4AsyncClientHttpRequestFactoryTests.java | 56 ++++++ .../Netty4ClientHttpRequestFactoryTests.java | 56 ++++++ 9 files changed, 608 insertions(+), 21 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java create mode 100644 spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java create mode 100644 spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java create mode 100644 spring-web/src/test/java/org/springframework/http/client/Netty4AsyncClientHttpRequestFactoryTests.java create mode 100644 spring-web/src/test/java/org/springframework/http/client/Netty4ClientHttpRequestFactoryTests.java diff --git a/build.gradle b/build.gradle index 8cf26a43910..b19aa6e799a 100644 --- a/build.gradle +++ b/build.gradle @@ -672,6 +672,7 @@ project("spring-web") { optional("commons-fileupload:commons-fileupload:1.3.1") optional("org.apache.httpcomponents:httpclient:4.3.5") optional("org.apache.httpcomponents:httpasyncclient:4.0.2") + optional("io.netty:netty-all:4.0.23.Final") optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}") optional("com.google.code.gson:gson:${gsonVersion}") diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java new file mode 100644 index 00000000000..a8f3b4f6c79 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2014 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.io.OutputStream; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpVersion; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +/** + * {@link org.springframework.http.client.ClientHttpRequest} implementation that uses + * Netty 4 to execute requests. + * + *

Created via the {@link Netty4ClientHttpRequestFactory}. + * + * @author Arjen Poutsma + * @since 4.2 + */ +class Netty4ClientHttpRequest extends AbstractAsyncClientHttpRequest implements ClientHttpRequest { + + private final Bootstrap bootstrap; + + private final URI uri; + + private final HttpMethod method; + + private final ByteBufOutputStream body; + + Netty4ClientHttpRequest(Bootstrap bootstrap, URI uri, HttpMethod method, int maxRequestSize) { + this.bootstrap = bootstrap; + this.uri = uri; + this.method = method; + this.body = new ByteBufOutputStream(Unpooled.buffer(maxRequestSize)); + } + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException { + return body; + } + + @Override + protected ListenableFuture executeInternal(final HttpHeaders headers) + throws IOException { + final SettableListenableFuture responseFuture = + new SettableListenableFuture(); + + ChannelFutureListener connectionListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + Channel channel = future.channel(); + channel.pipeline() + .addLast(new SimpleChannelInboundHandler() { + + @Override + protected void channelRead0( + ChannelHandlerContext ctx, + FullHttpResponse msg) throws Exception { + responseFuture + .set(new Netty4ClientHttpResponse(ctx, + msg)); + } + + @Override + public void exceptionCaught( + ChannelHandlerContext ctx, + Throwable cause) throws Exception { + responseFuture.setException(cause); + } + }); + + FullHttpRequest nettyRequest = + createFullHttpRequest(headers); + + channel.writeAndFlush(nettyRequest); + } + else { + responseFuture.setException(future.cause()); + } + + } + }; + + bootstrap.connect(uri.getHost(), getPort(uri)).addListener(connectionListener); + + return responseFuture; + + } + + @Override + public ClientHttpResponse execute() throws IOException { + try { + return executeAsync().get(); + } + catch (InterruptedException ex) { + throw new IOException(ex.getMessage(), ex); + } + catch (ExecutionException ex) { + if (ex.getCause() instanceof IOException) { + throw (IOException) ex.getCause(); + } else { + throw new IOException(ex.getMessage(), ex); + } + } + } + + private static int getPort(URI uri) { + int port = uri.getPort(); + if (port == -1) { + if ("http".equalsIgnoreCase(uri.getScheme())) { + port = 80; + } + else if ("https".equalsIgnoreCase(uri.getScheme())) { + port = 443; + } + } + return port; + } + + private FullHttpRequest createFullHttpRequest(HttpHeaders headers) { + io.netty.handler.codec.http.HttpMethod nettyMethod = + io.netty.handler.codec.http.HttpMethod.valueOf(method.name()); + + FullHttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + nettyMethod, this.uri.getRawPath(), + this.body.buffer()); + + nettyRequest.headers() + .set(io.netty.handler.codec.http.HttpHeaders.Names.HOST, uri.getHost()); + nettyRequest.headers() + .set(io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION, + io.netty.handler.codec.http.HttpHeaders.Values.CLOSE); + + for (Map.Entry> entry : headers.entrySet()) { + nettyRequest.headers().add(entry.getKey(), entry.getValue()); + } + + return nettyRequest; + } + + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java new file mode 100644 index 00000000000..75000b6513a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2014 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 io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.ssl.SslContext; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that + * uses Netty 4 to create requests. + * + *

Allows to use a pre-configured {@link EventLoopGroup} instance - useful for sharing + * across multiple clients. + * + * @author Arjen Poutsma + * @since 4.2 + */ +public class Netty4ClientHttpRequestFactory + implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, + InitializingBean, DisposableBean { + + /** + * The default maximum request size. + * @see #setMaxRequestSize(int) + */ + public static final int DEFAULT_MAX_REQUEST_SIZE = 1024 * 1024 * 10; + + private final EventLoopGroup eventLoopGroup; + + private final boolean defaultEventLoopGroup; + + private SslContext sslContext; + + private int maxRequestSize = DEFAULT_MAX_REQUEST_SIZE; + + private Bootstrap bootstrap; + + /** + * Creates a new {@code Netty4ClientHttpRequestFactory} with a default + * {@link NioEventLoopGroup}. + */ + public Netty4ClientHttpRequestFactory() { + int ioWorkerCount = Runtime.getRuntime().availableProcessors() * 2; + eventLoopGroup = new NioEventLoopGroup(ioWorkerCount); + defaultEventLoopGroup = true; + } + + /** + * Creates a new {@code Netty4ClientHttpRequestFactory} with the given + * {@link EventLoopGroup}. + * + *

NOTE: the given group will not be + * {@linkplain EventLoopGroup#shutdownGracefully() shutdown} by this factory; doing + * so becomes the responsibility of the caller. + */ + public Netty4ClientHttpRequestFactory(EventLoopGroup eventLoopGroup) { + Assert.notNull(eventLoopGroup, "'eventLoopGroup' must not be null"); + this.eventLoopGroup = eventLoopGroup; + this.defaultEventLoopGroup = false; + } + + /** + * Sets the default maximum request size. The default is + * {@link #DEFAULT_MAX_REQUEST_SIZE}. + * @see HttpObjectAggregator#HttpObjectAggregator(int) + */ + public void setMaxRequestSize(int maxRequestSize) { + this.maxRequestSize = maxRequestSize; + } + + /** + * Sets the SSL context. + */ + public void setSslContext(SslContext sslContext) { + this.sslContext = sslContext; + } + + private Bootstrap getBootstrap() { + if (this.bootstrap == null) { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + if (sslContext != null) { + pipeline.addLast(sslContext.newHandler(ch.alloc())); + } + pipeline.addLast(new HttpClientCodec()); + pipeline.addLast(new HttpObjectAggregator(maxRequestSize)); + } + }); + this.bootstrap = bootstrap; + } + return this.bootstrap; + } + + @Override + public void afterPropertiesSet() throws Exception { + getBootstrap(); + } + + private Netty4ClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) { + return new Netty4ClientHttpRequest(getBootstrap(), uri, httpMethod, maxRequestSize); + } + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) + throws IOException { + return createRequestInternal(uri, httpMethod); + } + + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) + throws IOException { + return createRequestInternal(uri, httpMethod); + } + + @Override + public void destroy() throws InterruptedException { + if (defaultEventLoopGroup) { + // clean up the EventLoopGroup if we created it in the constructor + eventLoopGroup.shutdownGracefully().sync(); + } + } + + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java new file mode 100644 index 00000000000..7e2ae0ae6fb --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2014 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.io.InputStream; +import java.util.Map; + +import io.netty.buffer.ByteBufInputStream; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.http.client.ClientHttpResponse} implementation that uses + * Netty 4 to execute requests. + * + * @author Arjen Poutsma + * @since 4.2 + */ +class Netty4ClientHttpResponse extends AbstractClientHttpResponse { + + private final ChannelHandlerContext context; + + private final FullHttpResponse nettyResponse; + + private final ByteBufInputStream body; + + private HttpHeaders headers; + + + Netty4ClientHttpResponse(ChannelHandlerContext context, + FullHttpResponse nettyResponse) { + Assert.notNull(context, "'context' must not be null"); + Assert.notNull(nettyResponse, "'nettyResponse' must not be null"); + this.context = context; + this.nettyResponse = nettyResponse; + this.body = new ByteBufInputStream(this.nettyResponse.content()); + this.nettyResponse.retain(); + } + + @Override + public int getRawStatusCode() throws IOException { + return this.nettyResponse.getStatus().code(); + } + + @Override + public String getStatusText() throws IOException { + return this.nettyResponse.getStatus().reasonPhrase(); + } + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + HttpHeaders headers = new HttpHeaders(); + for (Map.Entry entry : this.nettyResponse.headers()) { + headers.add(entry.getKey(), entry.getValue()); + } + this.headers = headers; + } + return this.headers; + } + + @Override + public InputStream getBody() throws IOException { + return this.body; + } + + @Override + public void close() { + this.nettyResponse.release(); + this.context.close(); + } +} diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractAsyncHttpRequestFactoryTestCase.java b/spring-web/src/test/java/org/springframework/http/client/AbstractAsyncHttpRequestFactoryTestCase.java index 580baa1ea68..aa06557b5ce 100644 --- a/spring-web/src/test/java/org/springframework/http/client/AbstractAsyncHttpRequestFactoryTestCase.java +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractAsyncHttpRequestFactoryTestCase.java @@ -22,9 +22,12 @@ import java.util.Arrays; import java.util.Locale; import java.util.concurrent.Future; +import org.junit.After; +import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -34,8 +37,6 @@ import org.springframework.util.StreamUtils; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureCallback; -import static org.junit.Assert.*; - public abstract class AbstractAsyncHttpRequestFactoryTestCase extends AbstractJettyServerTestCase { protected AsyncClientHttpRequestFactory factory; @@ -49,6 +50,13 @@ public abstract class AbstractAsyncHttpRequestFactoryTestCase extends AbstractJe } } + @After + public final void destroyFactory() throws Exception { + if (factory instanceof DisposableBean) { + ((DisposableBean) factory).destroy(); + } + } + protected abstract AsyncClientHttpRequestFactory createRequestFactory(); @@ -60,7 +68,11 @@ public abstract class AbstractAsyncHttpRequestFactoryTestCase extends AbstractJe assertEquals("Invalid HTTP URI", uri, request.getURI()); Future futureResponse = request.executeAsync(); ClientHttpResponse response = futureResponse.get(); - assertEquals("Invalid status code", HttpStatus.NOT_FOUND, response.getStatusCode()); + try { + assertEquals("Invalid status code", HttpStatus.NOT_FOUND, response.getStatusCode()); + } finally { + response.close(); + } } @Test @@ -74,24 +86,24 @@ public abstract class AbstractAsyncHttpRequestFactoryTestCase extends AbstractJe @Override public void onSuccess(ClientHttpResponse result) { try { - System.out.println("SUCCESS! " + result.getStatusCode()); - System.out.println("Callback: " + System.currentTimeMillis()); - System.out.println(Thread.currentThread().getId()); assertEquals("Invalid status code", HttpStatus.NOT_FOUND, result.getStatusCode()); } catch (IOException ex) { - ex.printStackTrace(); + fail(ex.getMessage()); } } @Override public void onFailure(Throwable ex) { - System.out.println("FAILURE: " + ex); + fail(ex.getMessage()); } }); ClientHttpResponse response = listenableFuture.get(); - System.out.println("Main thread: " + System.currentTimeMillis()); - assertEquals("Invalid status code", HttpStatus.NOT_FOUND, response.getStatusCode()); - System.out.println(Thread.currentThread().getId()); + try { + assertEquals("Invalid status code", HttpStatus.NOT_FOUND, response.getStatusCode()); + } + finally { + response.close(); + } } @Test @@ -129,7 +141,7 @@ public abstract class AbstractAsyncHttpRequestFactoryTestCase extends AbstractJe } } - @Test(expected = IllegalStateException.class) + @Test public void multipleWrites() throws Exception { AsyncClientHttpRequest request = factory.createAsyncRequest(new URI(baseUrl + "/echo"), HttpMethod.POST); final byte[] body = "Hello World".getBytes("UTF-8"); @@ -146,13 +158,17 @@ public abstract class AbstractAsyncHttpRequestFactoryTestCase extends AbstractJe ClientHttpResponse response = futureResponse.get(); try { FileCopyUtils.copy(body, request.getBody()); + fail("IllegalStateException expected"); + } + catch (IllegalStateException ex) { + // expected } finally { response.close(); } } - @Test(expected = UnsupportedOperationException.class) + @Test public void headersAfterExecute() throws Exception { AsyncClientHttpRequest request = factory.createAsyncRequest(new URI(baseUrl + "/echo"), HttpMethod.POST); request.getHeaders().add("MyHeader", "value"); @@ -163,6 +179,10 @@ public abstract class AbstractAsyncHttpRequestFactoryTestCase extends AbstractJe ClientHttpResponse response = futureResponse.get(); try { request.getHeaders().add("MyHeader", "value"); + fail("UnsupportedOperationException expected"); + } + catch (UnsupportedOperationException ex) { + // expected } finally { response.close(); diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java index 8639e1f0405..5ebb3f8bb0e 100644 --- a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java @@ -22,17 +22,20 @@ import java.net.URI; import java.util.Arrays; import java.util.Locale; +import org.junit.After; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.StreamingHttpOutputMessage; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; -import static org.junit.Assert.*; - /** @author Arjen Poutsma */ public abstract class AbstractHttpRequestFactoryTestCase extends AbstractJettyServerTestCase { @@ -40,10 +43,21 @@ public abstract class AbstractHttpRequestFactoryTestCase extends protected ClientHttpRequestFactory factory; @Before - public final void createFactory() { + public final void createFactory() throws Exception { factory = createRequestFactory(); + if (factory instanceof InitializingBean) { + ((InitializingBean) factory).afterPropertiesSet(); + } + } + + @After + public final void destroyFactory() throws Exception { + if (factory instanceof DisposableBean) { + ((DisposableBean) factory).destroy(); + } } + protected abstract ClientHttpRequestFactory createRequestFactory(); @Test @@ -53,7 +67,11 @@ public abstract class AbstractHttpRequestFactoryTestCase extends assertEquals("Invalid HTTP method", HttpMethod.GET, request.getMethod()); assertEquals("Invalid HTTP URI", uri, request.getURI()); ClientHttpResponse response = request.execute(); - assertEquals("Invalid status code", HttpStatus.NOT_FOUND, response.getStatusCode()); + try { + assertEquals("Invalid status code", HttpStatus.NOT_FOUND, response.getStatusCode()); + } finally { + response.close(); + } } @Test diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractJettyServerTestCase.java b/spring-web/src/test/java/org/springframework/http/client/AbstractJettyServerTestCase.java index 5d5ce0edac3..0b6dde9cef8 100644 --- a/spring-web/src/test/java/org/springframework/http/client/AbstractJettyServerTestCase.java +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractJettyServerTestCase.java @@ -31,12 +31,11 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; +import static org.junit.Assert.assertEquals; import org.junit.BeforeClass; -import org.springframework.util.FileCopyUtils; import org.springframework.util.SocketUtils; - -import static org.junit.Assert.*; +import org.springframework.util.StreamUtils; /** @author Arjen Poutsma */ public class AbstractJettyServerTestCase { @@ -147,6 +146,8 @@ public class AbstractJettyServerTestCase { private void echo(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(HttpServletResponse.SC_OK); + response.setContentType(request.getContentType()); + response.setContentLength(request.getContentLength()); for (Enumeration e1 = request.getHeaderNames(); e1.hasMoreElements();) { String headerName = e1.nextElement(); for (Enumeration e2 = request.getHeaders(headerName); e2.hasMoreElements();) { @@ -154,7 +155,7 @@ public class AbstractJettyServerTestCase { response.addHeader(headerName, headerValue); } } - FileCopyUtils.copy(request.getInputStream(), response.getOutputStream()); + StreamUtils.copy(request.getInputStream(), response.getOutputStream()); } } } diff --git a/spring-web/src/test/java/org/springframework/http/client/Netty4AsyncClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/Netty4AsyncClientHttpRequestFactoryTests.java new file mode 100644 index 00000000000..363356b1684 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/Netty4AsyncClientHttpRequestFactoryTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2014 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 io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.springframework.http.HttpMethod; + +/** + * @author Arjen Poutsma + */ +public class Netty4AsyncClientHttpRequestFactoryTests + extends AbstractAsyncHttpRequestFactoryTestCase { + + private static EventLoopGroup eventLoopGroup; + + @BeforeClass + public static void createEventLoopGroup() { + eventLoopGroup = new NioEventLoopGroup(); + } + + @AfterClass + public static void shutdownEventLoopGroup() throws InterruptedException { + eventLoopGroup.shutdownGracefully().sync(); + } + + @Override + protected AsyncClientHttpRequestFactory createRequestFactory() { + return new Netty4ClientHttpRequestFactory(eventLoopGroup); + } + + @Override + @Test + public void httpMethods() throws Exception { + assertHttpMethod("patch", HttpMethod.PATCH); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/client/Netty4ClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/Netty4ClientHttpRequestFactoryTests.java new file mode 100644 index 00000000000..61aaf0493ae --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/Netty4ClientHttpRequestFactoryTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2014 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 io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.springframework.http.HttpMethod; + +/** + * @author Arjen Poutsma + */ +public class Netty4ClientHttpRequestFactoryTests + extends AbstractHttpRequestFactoryTestCase { + + private static EventLoopGroup eventLoopGroup; + + @BeforeClass + public static void createEventLoopGroup() { + eventLoopGroup = new NioEventLoopGroup(); + } + + @AfterClass + public static void shutdownEventLoopGroup() throws InterruptedException { + eventLoopGroup.shutdownGracefully().sync(); + } + + @Override + protected ClientHttpRequestFactory createRequestFactory() { + return new Netty4ClientHttpRequestFactory(eventLoopGroup); + } + + @Override + @Test + public void httpMethods() throws Exception { + assertHttpMethod("patch", HttpMethod.PATCH); + } + +}