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}