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.remoting.httpinvoker; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.util.Locale; 023import java.util.zip.GZIPInputStream; 024 025import org.apache.http.Header; 026import org.apache.http.HttpResponse; 027import org.apache.http.NoHttpResponseException; 028import org.apache.http.StatusLine; 029import org.apache.http.client.HttpClient; 030import org.apache.http.client.config.RequestConfig; 031import org.apache.http.client.methods.Configurable; 032import org.apache.http.client.methods.HttpPost; 033import org.apache.http.config.Registry; 034import org.apache.http.config.RegistryBuilder; 035import org.apache.http.conn.socket.ConnectionSocketFactory; 036import org.apache.http.conn.socket.PlainConnectionSocketFactory; 037import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 038import org.apache.http.entity.ByteArrayEntity; 039import org.apache.http.impl.client.HttpClientBuilder; 040import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 041 042import org.springframework.context.i18n.LocaleContext; 043import org.springframework.context.i18n.LocaleContextHolder; 044import org.springframework.lang.Nullable; 045import org.springframework.remoting.support.RemoteInvocationResult; 046import org.springframework.util.Assert; 047 048/** 049 * {@link org.springframework.remoting.httpinvoker.HttpInvokerRequestExecutor} implementation that uses 050 * <a href="https://hc.apache.org/httpcomponents-client-ga/httpclient/">Apache HttpComponents HttpClient</a> 051 * to execute POST requests. 052 * 053 * <p>Allows to use a pre-configured {@link org.apache.http.client.HttpClient} 054 * instance, potentially with authentication, HTTP connection pooling, etc. 055 * Also designed for easy subclassing, providing specific template methods. 056 * 057 * <p>As of Spring 4.1, this request executor requires Apache HttpComponents 4.3 or higher. 058 * 059 * @author Juergen Hoeller 060 * @author Stephane Nicoll 061 * @since 3.1 062 * @see org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor 063 */ 064public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor { 065 066 private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100; 067 068 private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5; 069 070 private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000); 071 072 073 private HttpClient httpClient; 074 075 @Nullable 076 private RequestConfig requestConfig; 077 078 079 /** 080 * Create a new instance of the HttpComponentsHttpInvokerRequestExecutor with a default 081 * {@link HttpClient} that uses a default {@code org.apache.http.impl.conn.PoolingClientConnectionManager}. 082 */ 083 public HttpComponentsHttpInvokerRequestExecutor() { 084 this(createDefaultHttpClient(), RequestConfig.custom() 085 .setSocketTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS).build()); 086 } 087 088 /** 089 * Create a new instance of the HttpComponentsClientHttpRequestFactory 090 * with the given {@link HttpClient} instance. 091 * @param httpClient the HttpClient instance to use for this request executor 092 */ 093 public HttpComponentsHttpInvokerRequestExecutor(HttpClient httpClient) { 094 this(httpClient, null); 095 } 096 097 private HttpComponentsHttpInvokerRequestExecutor(HttpClient httpClient, @Nullable RequestConfig requestConfig) { 098 this.httpClient = httpClient; 099 this.requestConfig = requestConfig; 100 } 101 102 103 private static HttpClient createDefaultHttpClient() { 104 Registry<ConnectionSocketFactory> schemeRegistry = RegistryBuilder.<ConnectionSocketFactory>create() 105 .register("http", PlainConnectionSocketFactory.getSocketFactory()) 106 .register("https", SSLConnectionSocketFactory.getSocketFactory()) 107 .build(); 108 109 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(schemeRegistry); 110 connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS); 111 connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE); 112 113 return HttpClientBuilder.create().setConnectionManager(connectionManager).build(); 114 } 115 116 117 /** 118 * Set the {@link HttpClient} instance to use for this request executor. 119 */ 120 public void setHttpClient(HttpClient httpClient) { 121 this.httpClient = httpClient; 122 } 123 124 /** 125 * Return the {@link HttpClient} instance that this request executor uses. 126 */ 127 public HttpClient getHttpClient() { 128 return this.httpClient; 129 } 130 131 /** 132 * Set the connection timeout for the underlying HttpClient. 133 * A timeout value of 0 specifies an infinite timeout. 134 * <p>Additional properties can be configured by specifying a 135 * {@link RequestConfig} instance on a custom {@link HttpClient}. 136 * @param timeout the timeout value in milliseconds 137 * @see RequestConfig#getConnectTimeout() 138 */ 139 public void setConnectTimeout(int timeout) { 140 Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value"); 141 this.requestConfig = cloneRequestConfig().setConnectTimeout(timeout).build(); 142 } 143 144 /** 145 * Set the timeout in milliseconds used when requesting a connection from the connection 146 * manager using the underlying HttpClient. 147 * A timeout value of 0 specifies an infinite timeout. 148 * <p>Additional properties can be configured by specifying a 149 * {@link RequestConfig} instance on a custom {@link HttpClient}. 150 * @param connectionRequestTimeout the timeout value to request a connection in milliseconds 151 * @see RequestConfig#getConnectionRequestTimeout() 152 */ 153 public void setConnectionRequestTimeout(int connectionRequestTimeout) { 154 this.requestConfig = cloneRequestConfig().setConnectionRequestTimeout(connectionRequestTimeout).build(); 155 } 156 157 /** 158 * Set the socket read timeout for the underlying HttpClient. 159 * A timeout value of 0 specifies an infinite timeout. 160 * <p>Additional properties can be configured by specifying a 161 * {@link RequestConfig} instance on a custom {@link HttpClient}. 162 * @param timeout the timeout value in milliseconds 163 * @see #DEFAULT_READ_TIMEOUT_MILLISECONDS 164 * @see RequestConfig#getSocketTimeout() 165 */ 166 public void setReadTimeout(int timeout) { 167 Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value"); 168 this.requestConfig = cloneRequestConfig().setSocketTimeout(timeout).build(); 169 } 170 171 private RequestConfig.Builder cloneRequestConfig() { 172 return (this.requestConfig != null ? RequestConfig.copy(this.requestConfig) : RequestConfig.custom()); 173 } 174 175 176 /** 177 * Execute the given request through the HttpClient. 178 * <p>This method implements the basic processing workflow: 179 * The actual work happens in this class's template methods. 180 * @see #createHttpPost 181 * @see #setRequestBody 182 * @see #executeHttpPost 183 * @see #validateResponse 184 * @see #getResponseBody 185 */ 186 @Override 187 protected RemoteInvocationResult doExecuteRequest( 188 HttpInvokerClientConfiguration config, ByteArrayOutputStream baos) 189 throws IOException, ClassNotFoundException { 190 191 HttpPost postMethod = createHttpPost(config); 192 setRequestBody(config, postMethod, baos); 193 try { 194 HttpResponse response = executeHttpPost(config, getHttpClient(), postMethod); 195 validateResponse(config, response); 196 InputStream responseBody = getResponseBody(config, response); 197 return readRemoteInvocationResult(responseBody, config.getCodebaseUrl()); 198 } 199 finally { 200 postMethod.releaseConnection(); 201 } 202 } 203 204 /** 205 * Create an HttpPost for the given configuration. 206 * <p>The default implementation creates a standard HttpPost with 207 * "application/x-java-serialized-object" as "Content-Type" header. 208 * @param config the HTTP invoker configuration that specifies the 209 * target service 210 * @return the HttpPost instance 211 * @throws java.io.IOException if thrown by I/O methods 212 */ 213 protected HttpPost createHttpPost(HttpInvokerClientConfiguration config) throws IOException { 214 HttpPost httpPost = new HttpPost(config.getServiceUrl()); 215 216 RequestConfig requestConfig = createRequestConfig(config); 217 if (requestConfig != null) { 218 httpPost.setConfig(requestConfig); 219 } 220 221 LocaleContext localeContext = LocaleContextHolder.getLocaleContext(); 222 if (localeContext != null) { 223 Locale locale = localeContext.getLocale(); 224 if (locale != null) { 225 httpPost.addHeader(HTTP_HEADER_ACCEPT_LANGUAGE, locale.toLanguageTag()); 226 } 227 } 228 229 if (isAcceptGzipEncoding()) { 230 httpPost.addHeader(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP); 231 } 232 233 return httpPost; 234 } 235 236 /** 237 * Create a {@link RequestConfig} for the given configuration. Can return {@code null} 238 * to indicate that no custom request config should be set and the defaults of the 239 * {@link HttpClient} should be used. 240 * <p>The default implementation tries to merge the defaults of the client with the 241 * local customizations of the instance, if any. 242 * @param config the HTTP invoker configuration that specifies the 243 * target service 244 * @return the RequestConfig to use 245 */ 246 @Nullable 247 protected RequestConfig createRequestConfig(HttpInvokerClientConfiguration config) { 248 HttpClient client = getHttpClient(); 249 if (client instanceof Configurable) { 250 RequestConfig clientRequestConfig = ((Configurable) client).getConfig(); 251 return mergeRequestConfig(clientRequestConfig); 252 } 253 return this.requestConfig; 254 } 255 256 private RequestConfig mergeRequestConfig(RequestConfig defaultRequestConfig) { 257 if (this.requestConfig == null) { // nothing to merge 258 return defaultRequestConfig; 259 } 260 261 RequestConfig.Builder builder = RequestConfig.copy(defaultRequestConfig); 262 int connectTimeout = this.requestConfig.getConnectTimeout(); 263 if (connectTimeout >= 0) { 264 builder.setConnectTimeout(connectTimeout); 265 } 266 int connectionRequestTimeout = this.requestConfig.getConnectionRequestTimeout(); 267 if (connectionRequestTimeout >= 0) { 268 builder.setConnectionRequestTimeout(connectionRequestTimeout); 269 } 270 int socketTimeout = this.requestConfig.getSocketTimeout(); 271 if (socketTimeout >= 0) { 272 builder.setSocketTimeout(socketTimeout); 273 } 274 return builder.build(); 275 } 276 277 /** 278 * Set the given serialized remote invocation as request body. 279 * <p>The default implementation simply sets the serialized invocation as the 280 * HttpPost's request body. This can be overridden, for example, to write a 281 * specific encoding and to potentially set appropriate HTTP request headers. 282 * @param config the HTTP invoker configuration that specifies the target service 283 * @param httpPost the HttpPost to set the request body on 284 * @param baos the ByteArrayOutputStream that contains the serialized 285 * RemoteInvocation object 286 * @throws java.io.IOException if thrown by I/O methods 287 */ 288 protected void setRequestBody( 289 HttpInvokerClientConfiguration config, HttpPost httpPost, ByteArrayOutputStream baos) 290 throws IOException { 291 292 ByteArrayEntity entity = new ByteArrayEntity(baos.toByteArray()); 293 entity.setContentType(getContentType()); 294 httpPost.setEntity(entity); 295 } 296 297 /** 298 * Execute the given HttpPost instance. 299 * @param config the HTTP invoker configuration that specifies the target service 300 * @param httpClient the HttpClient to execute on 301 * @param httpPost the HttpPost to execute 302 * @return the resulting HttpResponse 303 * @throws java.io.IOException if thrown by I/O methods 304 */ 305 protected HttpResponse executeHttpPost( 306 HttpInvokerClientConfiguration config, HttpClient httpClient, HttpPost httpPost) 307 throws IOException { 308 309 return httpClient.execute(httpPost); 310 } 311 312 /** 313 * Validate the given response as contained in the HttpPost object, 314 * throwing an exception if it does not correspond to a successful HTTP response. 315 * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid 316 * parsing the response body and trying to deserialize from a corrupted stream. 317 * @param config the HTTP invoker configuration that specifies the target service 318 * @param response the resulting HttpResponse to validate 319 * @throws java.io.IOException if validation failed 320 */ 321 protected void validateResponse(HttpInvokerClientConfiguration config, HttpResponse response) 322 throws IOException { 323 324 StatusLine status = response.getStatusLine(); 325 if (status.getStatusCode() >= 300) { 326 throw new NoHttpResponseException( 327 "Did not receive successful HTTP response: status code = " + status.getStatusCode() + 328 ", status message = [" + status.getReasonPhrase() + "]"); 329 } 330 } 331 332 /** 333 * Extract the response body from the given executed remote invocation request. 334 * <p>The default implementation simply fetches the HttpPost's response body stream. 335 * If the response is recognized as GZIP response, the InputStream will get wrapped 336 * in a GZIPInputStream. 337 * @param config the HTTP invoker configuration that specifies the target service 338 * @param httpResponse the resulting HttpResponse to read the response body from 339 * @return an InputStream for the response body 340 * @throws java.io.IOException if thrown by I/O methods 341 * @see #isGzipResponse 342 * @see java.util.zip.GZIPInputStream 343 */ 344 protected InputStream getResponseBody(HttpInvokerClientConfiguration config, HttpResponse httpResponse) 345 throws IOException { 346 347 if (isGzipResponse(httpResponse)) { 348 return new GZIPInputStream(httpResponse.getEntity().getContent()); 349 } 350 else { 351 return httpResponse.getEntity().getContent(); 352 } 353 } 354 355 /** 356 * Determine whether the given response indicates a GZIP response. 357 * <p>The default implementation checks whether the HTTP "Content-Encoding" 358 * header contains "gzip" (in any casing). 359 * @param httpResponse the resulting HttpResponse to check 360 * @return whether the given response indicates a GZIP response 361 */ 362 protected boolean isGzipResponse(HttpResponse httpResponse) { 363 Header encodingHeader = httpResponse.getFirstHeader(HTTP_HEADER_CONTENT_ENCODING); 364 return (encodingHeader != null && encodingHeader.getValue() != null && 365 encodingHeader.getValue().toLowerCase().contains(ENCODING_GZIP)); 366 } 367 368}