聊聊HttpClient的RedirectStrategy
序
本文主要研究一下HttpClient的RedirectStrategy
RedirectStrategy
org/apache/http/client/RedirectStrategy.java
public interface RedirectStrategy {
/**
* Determines if a request should be redirected to a new location
* given the response from the target server.
*
* @param request the executed request
* @param response the response received from the target server
* @param context the context for the request execution
*
* @return {@code true} if the request should be redirected, {@code false}
* otherwise
*/
boolean isRedirected(
HttpRequest request,
HttpResponse response,
HttpContext context) throws ProtocolException;
/**
* Determines the redirect location given the response from the target
* server and the current request execution context and generates a new
* request to be sent to the location.
*
* @param request the executed request
* @param response the response received from the target server
* @param context the context for the request execution
*
* @return redirected request
*/
HttpUriRequest getRedirect(
HttpRequest request,
HttpResponse response,
HttpContext context) throws ProtocolException;
}
RedirectStrategy接口定义了isRedirected方法用于判断是否需要redirect,还定义了getRedirect方法用于返回redirect的目标地址
DefaultRedirectStrategy
org/apache/http/impl/client/DefaultRedirectStrategy.java
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class DefaultRedirectStrategy implements RedirectStrategy {
private final Log log = LogFactory.getLog(getClass());
/**
* @deprecated (4.3) use {@link org.apache.http.client.protocol.HttpClientContext#REDIRECT_LOCATIONS}.
*/
@Deprecated
public static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";
public static final DefaultRedirectStrategy INSTANCE = new DefaultRedirectStrategy();
private final String[] redirectMethods;
public DefaultRedirectStrategy() {
this(new String[] {
HttpGet.METHOD_NAME,
HttpHead.METHOD_NAME
});
}
/**
* Constructs a new instance to redirect the given HTTP methods.
*
* @param redirectMethods The methods to redirect.
* @since 4.5.10
*/
public DefaultRedirectStrategy(final String[] redirectMethods) {
super();
final String[] tmp = redirectMethods.clone();
Arrays.sort(tmp);
this.redirectMethods = tmp;
}
@Override
public boolean isRedirected(
final HttpRequest request,
final HttpResponse response,
final HttpContext context) throws ProtocolException {
Args.notNull(request, "HTTP request");
Args.notNull(response, "HTTP response");
final int statusCode = response.getStatusLine().getStatusCode();
final String method = request.getRequestLine().getMethod();
final Header locationHeader = response.getFirstHeader("location");
switch (statusCode) {
case HttpStatus.SC_MOVED_TEMPORARILY:
return isRedirectable(method) && locationHeader != null;
case HttpStatus.SC_MOVED_PERMANENTLY:
case HttpStatus.SC_TEMPORARY_REDIRECT:
return isRedirectable(method);
case HttpStatus.SC_SEE_OTHER:
return true;
default:
return false;
} //end of switch
}
/**
* @since 4.2
*/
protected boolean isRedirectable(final String method) {
return Arrays.binarySearch(redirectMethods, method) >= 0;
}
@Override
public HttpUriRequest getRedirect(
final HttpRequest request,
final HttpResponse response,
final HttpContext context) throws ProtocolException {
final URI uri = getLocationURI(request, response, context);
final String method = request.getRequestLine().getMethod();
if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) {
return new HttpHead(uri);
} else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) {
return new HttpGet(uri);
} else {
final int status = response.getStatusLine().getStatusCode();
return status == HttpStatus.SC_TEMPORARY_REDIRECT
? RequestBuilder.copy(request).setUri(uri).build()
: new HttpGet(uri);
}
}
}
DefaultRedirectStrategy实现了RedirectStrategy接口,它定义了redirectMethods,默认是Get和Head;isRedirected方法先获取response的statusCode,对于302需要location的header有值且请求method在redirectMethods中(
isRedirectable
),对于301及307仅仅判断isRedirectable,对于303返回true,其余的返回false
getRedirect方法先通过getLocationURI获取目标地址,然后针对get或者head分别构造HttpHead及HttpGet,剩下的根据statusCode判断,是307则拷贝原来的request,否则返回HttpGet
RedirectExec
org/apache/http/impl/execchain/RedirectExec.java
/**
* Request executor in the request execution chain that is responsible
* for handling of request redirects.
* <p>
* Further responsibilities such as communication with the opposite
* endpoint is delegated to the next executor in the request execution
* chain.
* </p>
*
* @since 4.3
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
public class RedirectExec implements ClientExecChain {
private final Log log = LogFactory.getLog(getClass());
private final ClientExecChain requestExecutor;
private final RedirectStrategy redirectStrategy;
private final HttpRoutePlanner routePlanner;
public RedirectExec(
final ClientExecChain requestExecutor,
final HttpRoutePlanner routePlanner,
final RedirectStrategy redirectStrategy) {
super();
Args.notNull(requestExecutor, "HTTP client request executor");
Args.notNull(routePlanner, "HTTP route planner");
Args.notNull(redirectStrategy, "HTTP redirect strategy");
this.requestExecutor = requestExecutor;
this.routePlanner = routePlanner;
this.redirectStrategy = redirectStrategy;
}
@Override
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
Args.notNull(route, "HTTP route");
Args.notNull(request, "HTTP request");
Args.notNull(context, "HTTP context");
final List<URI> redirectLocations = context.getRedirectLocations();
if (redirectLocations != null) {
redirectLocations.clear();
}
final RequestConfig config = context.getRequestConfig();
final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
HttpRoute currentRoute = route;
HttpRequestWrapper currentRequest = request;
for (int redirectCount = 0;;) {
final CloseableHttpResponse response = requestExecutor.execute(
currentRoute, currentRequest, context, execAware);
try {
if (config.isRedirectsEnabled() &&
this.redirectStrategy.isRedirected(currentRequest.getOriginal(), response, context)) {
if (redirectCount >= maxRedirects) {
throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
}
redirectCount++;
final HttpRequest redirect = this.redirectStrategy.getRedirect(
currentRequest.getOriginal(), response, context);
if (!redirect.headerIterator().hasNext()) {
final HttpRequest original = request.getOriginal();
redirect.setHeaders(original.getAllHeaders());
}
currentRequest = HttpRequestWrapper.wrap(redirect);
if (currentRequest instanceof HttpEntityEnclosingRequest) {
RequestEntityProxy.enhance((HttpEntityEnclosingRequest) currentRequest);
}
final URI uri = currentRequest.getURI();
final HttpHost newTarget = URIUtils.extractHost(uri);
if (newTarget == null) {
throw new ProtocolException("Redirect URI does not specify a valid host name: " +
uri);
}
// Reset virtual host and auth states if redirecting to another host
if (!currentRoute.getTargetHost().equals(newTarget)) {
final AuthState targetAuthState = context.getTargetAuthState();
if (targetAuthState != null) {
this.log.debug("Resetting target auth state");
targetAuthState.reset();
}
final AuthState proxyAuthState = context.getProxyAuthState();
if (proxyAuthState != null && proxyAuthState.isConnectionBased()) {
this.log.debug("Resetting proxy auth state");
proxyAuthState.reset();
}
}
currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context);
if (this.log.isDebugEnabled()) {
this.log.debug("Redirecting to '" + uri + "' via " + currentRoute);
}
EntityUtils.consume(response.getEntity());
response.close();
} else {
return response;
}
} catch (final RuntimeException ex) {
response.close();
throw ex;
} catch (final IOException ex) {
response.close();
throw ex;
} catch (final HttpException ex) {
// Protocol exception related to a direct.
// The underlying connection may still be salvaged.
try {
EntityUtils.consume(response.getEntity());
} catch (final IOException ioex) {
this.log.debug("I/O error while releasing connection", ioex);
} finally {
response.close();
}
throw ex;
}
}
}
}
RedirectExec实现了ClientExecChain接口,其构造器要求传入requestExecutor、redirectStrategy、routePlanner,其execute方法会先获取maxRedirects参数,然后执行requestExecutor.execute,接着在config.isRedirectsEnabled()以及redirectStrategy.isRedirected为true时才进入redirect逻辑,它会先判断是否超出maxRedirects,大于等于则抛出RedirectException,否则通过redirectStrategy.getRedirect获取HttpRequest,更新currentRoute,然后清理entity关闭response继续下次循环,即执行redirect逻辑。
小结
HttpClient的RedirectStrategy定义了两个方法,一个是是否需要redirect,一个是获取redirect的请求,DefaultRedirectStrategy的构造器支持传入redirectMethods,默认是Get和Head,isRedirected方法主要是对302,301,307,303进行了判断,getRedirect方法主要是通过location获取目标地址,然后根据原来的method和statusCode构造HttpUriRequest。