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