001/*
002 * Copyright 2002-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http.client;
018
019import java.io.IOException;
020import java.net.URI;
021import java.util.concurrent.TimeUnit;
022import javax.net.ssl.SSLException;
023
024import io.netty.bootstrap.Bootstrap;
025import io.netty.channel.ChannelConfig;
026import io.netty.channel.ChannelInitializer;
027import io.netty.channel.ChannelPipeline;
028import io.netty.channel.EventLoopGroup;
029import io.netty.channel.nio.NioEventLoopGroup;
030import io.netty.channel.socket.SocketChannel;
031import io.netty.channel.socket.SocketChannelConfig;
032import io.netty.channel.socket.nio.NioSocketChannel;
033import io.netty.handler.codec.http.HttpClientCodec;
034import io.netty.handler.codec.http.HttpObjectAggregator;
035import io.netty.handler.ssl.SslContext;
036import io.netty.handler.ssl.SslContextBuilder;
037import io.netty.handler.timeout.ReadTimeoutHandler;
038
039import org.springframework.beans.factory.DisposableBean;
040import org.springframework.beans.factory.InitializingBean;
041import org.springframework.http.HttpMethod;
042import org.springframework.util.Assert;
043
044/**
045 * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation
046 * that uses <a href="https://netty.io/">Netty 4</a> to create requests.
047 *
048 * <p>Allows to use a pre-configured {@link EventLoopGroup} instance: useful for
049 * sharing across multiple clients.
050 *
051 * <p>Note that this implementation consistently closes the HTTP connection on each
052 * request.
053 *
054 * @author Arjen Poutsma
055 * @author Rossen Stoyanchev
056 * @author Brian Clozel
057 * @author Mark Paluch
058 * @since 4.1.2
059 */
060public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
061                AsyncClientHttpRequestFactory, InitializingBean, DisposableBean {
062
063        /**
064         * The default maximum response size.
065         * @see #setMaxResponseSize(int)
066         */
067        public static final int DEFAULT_MAX_RESPONSE_SIZE = 1024 * 1024 * 10;
068
069
070        private final EventLoopGroup eventLoopGroup;
071
072        private final boolean defaultEventLoopGroup;
073
074        private int maxResponseSize = DEFAULT_MAX_RESPONSE_SIZE;
075
076        private SslContext sslContext;
077
078        private int connectTimeout = -1;
079
080        private int readTimeout = -1;
081
082        private volatile Bootstrap bootstrap;
083
084
085        /**
086         * Create a new {@code Netty4ClientHttpRequestFactory} with a default
087         * {@link NioEventLoopGroup}.
088         */
089        public Netty4ClientHttpRequestFactory() {
090                int ioWorkerCount = Runtime.getRuntime().availableProcessors() * 2;
091                this.eventLoopGroup = new NioEventLoopGroup(ioWorkerCount);
092                this.defaultEventLoopGroup = true;
093        }
094
095        /**
096         * Create a new {@code Netty4ClientHttpRequestFactory} with the given
097         * {@link EventLoopGroup}.
098         * <p><b>NOTE:</b> the given group will <strong>not</strong> be
099         * {@linkplain EventLoopGroup#shutdownGracefully() shutdown} by this factory;
100         * doing so becomes the responsibility of the caller.
101         */
102        public Netty4ClientHttpRequestFactory(EventLoopGroup eventLoopGroup) {
103                Assert.notNull(eventLoopGroup, "EventLoopGroup must not be null");
104                this.eventLoopGroup = eventLoopGroup;
105                this.defaultEventLoopGroup = false;
106        }
107
108
109        /**
110         * Set the default maximum response size.
111         * <p>By default this is set to {@link #DEFAULT_MAX_RESPONSE_SIZE}.
112         * @see HttpObjectAggregator#HttpObjectAggregator(int)
113         * @since 4.1.5
114         */
115        public void setMaxResponseSize(int maxResponseSize) {
116                this.maxResponseSize = maxResponseSize;
117        }
118
119        /**
120         * Set the SSL context. When configured it is used to create and insert an
121         * {@link io.netty.handler.ssl.SslHandler} in the channel pipeline.
122         * <p>A default client SslContext is configured if none has been provided.
123         */
124        public void setSslContext(SslContext sslContext) {
125                this.sslContext = sslContext;
126        }
127
128        /**
129         * Set the underlying connect timeout (in milliseconds).
130         * A timeout value of 0 specifies an infinite timeout.
131         * @see ChannelConfig#setConnectTimeoutMillis(int)
132         */
133        public void setConnectTimeout(int connectTimeout) {
134                this.connectTimeout = connectTimeout;
135        }
136
137        /**
138         * Set the underlying URLConnection's read timeout (in milliseconds).
139         * A timeout value of 0 specifies an infinite timeout.
140         * @see ReadTimeoutHandler
141         */
142        public void setReadTimeout(int readTimeout) {
143                this.readTimeout = readTimeout;
144        }
145
146
147        @Override
148        public void afterPropertiesSet() {
149                if (this.sslContext == null) {
150                        this.sslContext = getDefaultClientSslContext();
151                }
152        }
153
154        private SslContext getDefaultClientSslContext() {
155                try {
156                        return SslContextBuilder.forClient().build();
157                }
158                catch (SSLException ex) {
159                        throw new IllegalStateException("Could not create default client SslContext", ex);
160                }
161        }
162
163
164        @Override
165        public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
166                return createRequestInternal(uri, httpMethod);
167        }
168
169        @Override
170        public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException {
171                return createRequestInternal(uri, httpMethod);
172        }
173
174        private Netty4ClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) {
175                return new Netty4ClientHttpRequest(getBootstrap(uri), uri, httpMethod);
176        }
177
178        private Bootstrap getBootstrap(URI uri) {
179                boolean isSecure = (uri.getPort() == 443 || "https".equalsIgnoreCase(uri.getScheme()));
180                if (isSecure) {
181                        return buildBootstrap(uri, true);
182                }
183                else {
184                        if (this.bootstrap == null) {
185                                this.bootstrap = buildBootstrap(uri, false);
186                        }
187                        return this.bootstrap;
188                }
189        }
190
191        private Bootstrap buildBootstrap(final URI uri, final boolean isSecure) {
192                Bootstrap bootstrap = new Bootstrap();
193                bootstrap.group(this.eventLoopGroup).channel(NioSocketChannel.class)
194                                .handler(new ChannelInitializer<SocketChannel>() {
195                                        @Override
196                                        protected void initChannel(SocketChannel channel) throws Exception {
197                                                configureChannel(channel.config());
198                                                ChannelPipeline pipeline = channel.pipeline();
199                                                if (isSecure) {
200                                                        Assert.notNull(sslContext, "sslContext should not be null");
201                                                        pipeline.addLast(sslContext.newHandler(channel.alloc(), uri.getHost(), uri.getPort()));
202                                                }
203                                                pipeline.addLast(new HttpClientCodec());
204                                                pipeline.addLast(new HttpObjectAggregator(maxResponseSize));
205                                                if (readTimeout > 0) {
206                                                        pipeline.addLast(new ReadTimeoutHandler(readTimeout,
207                                                                        TimeUnit.MILLISECONDS));
208                                                }
209                                        }
210                                });
211                return bootstrap;
212        }
213
214        /**
215         * Template method for changing properties on the given {@link SocketChannelConfig}.
216         * <p>The default implementation sets the connect timeout based on the set property.
217         * @param config the channel configuration
218         */
219        protected void configureChannel(SocketChannelConfig config) {
220                if (this.connectTimeout >= 0) {
221                        config.setConnectTimeoutMillis(this.connectTimeout);
222                }
223        }
224
225
226        @Override
227        public void destroy() throws InterruptedException {
228                if (this.defaultEventLoopGroup) {
229                        // Clean up the EventLoopGroup if we created it in the constructor
230                        this.eventLoopGroup.shutdownGracefully().sync();
231                }
232        }
233
234}