Zhiqim Kernel即知启蒙内核,是Zhiqim Framework的核心,负责工程的生命周期管理:包括工程开发和发布的目录结构管理、统一的配置规约、单多例服务接口定义、服务启动运行更新和销毁管理。并提供基础开发工具:包括工具类、日志类、线程池、JSON/XML编解析、HTTP客户端、时钟任务定时器等。

森中灵 最后提交于10月前 修改版本号
HttpClient.java31KB
/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙,邂逅框架梦]
 * 
 * https://zhiqim.org/project/zhiqim_framework/zhiqim_kernel.htm
 *
 * Zhiqim Kernel is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.kernel.httpclient;

import java.io.IOException;
import java.io.InputStream;
import java.net.BindException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;

import org.zhiqim.kernel.constants.HttpConstants;
import org.zhiqim.kernel.logging.Log;
import org.zhiqim.kernel.logging.LogFactory;
import org.zhiqim.kernel.model.codes.SSL;
import org.zhiqim.kernel.model.codes.SSL.DefaultHostnameVerifier;
import org.zhiqim.kernel.model.codes.URI;
import org.zhiqim.kernel.model.lists.ArrayListS;
import org.zhiqim.kernel.model.lists.ListS;
import org.zhiqim.kernel.model.maps.HashMapSO;
import org.zhiqim.kernel.model.objects.Int;
import org.zhiqim.kernel.util.Arrays;
import org.zhiqim.kernel.util.Ints;
import org.zhiqim.kernel.util.Streams;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.Stringx;
import org.zhiqim.kernel.util.Validates;

/**
 * Http客户端基类,实现子类实现execute功能
 * 
 * @see HttpGet         GET方法调用,只需URL,无需传入参数
 * @see HttpPost        POST方法调用,传入Form参数
 * @see HttpDownload    GET方法调用,得到内容是大文件,保存到本地
 * @see HttpUpload      POST方法调用,传入Form-data,包括文件和参数
 *
 * @version v1.0.0 @author zouzhigang 2014-3-21 新建与整理
 */
public abstract class HttpClient implements HttpConstants
{
    public static final String _USER_AGENT_VALUE_           = "ZhiqimHttpClient/V8.0.5";
    public static final Pattern _REGEX_FILE_NAME_           = Pattern.compile("attachment;\\s*filename=\"([\\w\\-\\._]+)\"");
    
    /**************************************************************************/
    //以下为HTTP参数
    /**************************************************************************/
    protected static final Log log = LogFactory.getLog(HttpClient.class);
    
    private boolean keepAlive = false;                        //是否长连接
    private HttpURLConnection conn;                             //HTTP连接
    
    private int connectTimeout = 10;                            //连接超时时间,单位秒,默认10秒,实现传入毫秒,=0表示一直等待直到成功,!=0时时间达到则抛出java.net.SocketTimeoutException
    private int readTimeout = 30;                               //读资源超时时间,单位秒,默认30秒,实现传入毫秒,=0表示一直等待直到成功,!=0时时间达到则抛出java.net.SocketTimeoutException
    
    private boolean doInput = true;                            //是否输入,默认=true
    private boolean doOutput = false;                          //是否输出,默认=false
    
    private boolean instanceFollowRedirects = false;           //当3XX重定向时,是否访问重定向的地址,HTTP默认是true,我们改为默认false,即返回location
    
    private boolean useCaches = false;                         //设置当前是否使用缓存,我们默认=false,系统默认取defaultUseCaches=true,也可以实际设置
    private long ifModifiedSince = 0;                           //设置资源比较时间
    private int chunkLength = -1;                               //指定资源块大小,系统默认-1,提供了一个缺省值4096参考,如果未设置,则缓存本地再发送,否则流逐步发送,
    private int fixedContentLength = -1;                        //指定读取资源内容大小,默认是未开启,按服务端ContentLength读内容大小,只有设置了才有效,默认-1
    
    private boolean allowUserInteraction = false;              //允许用户交互,默认=false
    private boolean hasUserAgent = false;                      //是否有客户端代理
    private boolean hasHost = false;                           //是否有主机
    
    private final HashMapSO requestPropertyMap;                 //请求属性参数表
    private final HashMapSO responsePropertyMap;                //响应属性参数表
    private String responseStatusLine;                           //响应状态行
    
    protected final String url;                                 //请求连接URL
    protected final URI uri;                                    //请求连接URI
    private final String method;                                //HTTP方法  [GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE]
    
    private String sslProtocol = "TLSv1.2";                      //SSL协议
    private KeyManager[] sslKeyManagers;                         //SSL密钥管理器
    private TrustManager[] sslTrustManagers;                     //SSL信任管理器
    
    protected int responseStatus;                                //响应状态码
    protected String responseText;                               //响应状态信息
    protected boolean responseClose;                            //响应是否关闭
    protected Throwable exception;                               //调用异常
    
    public HttpClient(String url, String method)
    {
        this.url = url;
        this.uri = new URI();
        this.uri.parse(url);
        this.method = method;
        this.requestPropertyMap = new HashMapSO();
        this.responsePropertyMap = new HashMapSO();
    }
    
    /** 设置请求属性完成后保持活跃 */
    public void setConnectionKeepAlive()
    {
        this.keepAlive = true;
        this.requestPropertyMap.put(_CONNECTION_, _KEEP_ALIVE_);
    }

    /** 设置请求属性完成后关闭 */
    public void setConnectionClose()
    {
        this.keepAlive = false;
        this.requestPropertyMap.put(_CONNECTION_, _CLOSE_);
    }
    
    /***********************************************************/
    //通用的执行方法,子类不可重写,子类重写以下三个方法
    /***********************************************************/
    
    public final void execute()
    {
        this.responseStatus = 0;
        this.responseText = null;
        this.responseClose = false;
        this.exception = null;
        
        if (!isValidUrl())
            return;
        
        try
        {
            //1.预置请求连接属性和消息头信息,如果返回false表示不可执行
            if (!doPreRequestProperty())
                return;
            
            if (conn == null)
            {//2.新建连接并连接
                conn = newHttpConnection();
                conn.connect();
            }
            
            //3.子类可重写该方法设置连接内容
            doWriteRequestContent(conn);
            
            //4.获取响应状态和响应属性
            responseStatus = conn.getResponseCode();
            doReadResponseProperty(conn);
            
            //5.子类可重写该方法获取连接内容
            doReadResponseContent(conn);
        }
        catch (SocketTimeoutException e) 
        {
            responseStatus = _73_SOCKET_TIMEOUT_;
            responseText = "调用服务端超时";
            exception = e;
        }
        catch (ConnectException e)
        {
            responseStatus = _91_CONNECT_EXCEPTION_;
            responseText = "连接服务器失败";
            exception = e;
        }
        catch (BindException e)
        {
            responseStatus = _92_BIND_EXCEPTION_;
            responseText = "绑定本地地址和端口失败";
            exception = e;
        }
        catch (SocketException e)
        {
            responseStatus = _90_SOCKET_EXCEPTION_;
            responseText = "调用服务端失败:"+e.getMessage();
            exception = e;
        }
        catch (IOException e)
        {
            responseStatus = _98_IO_EXCEPTION_;
            responseText = "调用服务端失败:"+e.getMessage();
            exception = e;
        }
        catch(Exception e)
        {
            responseStatus = _99_EXCEPTION_;
            responseText = "调用服务端异常:"+e.getMessage();
            exception = e;
        }
        finally
        {
            if (!keepAlive || exception != null || responseClose)
            {//指定短连接或者异常或者响应关闭
                if (conn != null)
                {//有连接的,断开
                    conn.disconnect();
                    conn = null;
                }
            }
        }
    }
    
    /***********************************************************/
    //子类可重写的方法,包括预置请求属性、写请求内容,读响应内容
    /***********************************************************/
    
    /**
     * 预处理请求属性,并检查是否可执行,默认可执行,子类重写
     * 
     * @return =true表示正常可执行,=false表示异常不向下执行
     */
    protected boolean doPreRequestProperty()
    {
        return true;
    }
    
    /**
     * 写请求内容,子类可重写
     * 
     * @param conn          HTTP/HTTPS连接
     * @throws IOException  可能的异常
     */
    protected void doWriteRequestContent(HttpURLConnection conn) throws IOException
    {
    }
    
    /**
     * 读响应内容,子类可重写
     * 
     * @param conn          HTTP/HTTPS连接
     * @throws IOException  可能的异常
     */
    protected void doReadResponseContent(HttpURLConnection conn) throws IOException
    {
        if (responseStatus == _302_FOUND_)
        {//重定向
            responseText = conn.getHeaderField("Location");
            return;
        }
        
        responseText = getResponseAsString(conn);
    }
    
    /*****************************************************************************/
    //以下为创建连接,并设置基本属性
    /*****************************************************************************/
    
    private boolean isValidUrl()
    {
        if (Validates.isUrl(url) && uri.isParsed())
            return true;
           
        responseStatus = _70_MALFORMED_URL_;
        responseText = "请求的URL不正确";
        return false;
    }
    
    /** 创建连接 */
    private HttpURLConnection newHttpConnection() throws NoSuchAlgorithmException, KeyManagementException, IOException
    {
        if (Validates.isUrlHttp(url))
        {//HTTP连接
            HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
            setConnectionProperty(conn);
            return conn;
        }
        else
        {//HTTPS连接
            HttpsURLConnection conn = (HttpsURLConnection)new URL(url).openConnection();
            conn.setSSLSocketFactory(SSL.getSocketFactory(sslProtocol, sslKeyManagers, sslTrustManagers));
            conn.setHostnameVerifier(new DefaultHostnameVerifier());
            setConnectionProperty(conn);
            return conn;
        }
    }
    
    /** 设置连接属性 */
    private void setConnectionProperty(HttpURLConnection conn)
    {
        //设置方法
        try{conn.setRequestMethod(method);}catch (ProtocolException e){}
        
        if (!doInput)
        {//有设置输入打开,输入默认打开
            conn.setDoInput(doInput);
        }
        
        if (doOutput)
        {//有设置输出打开,输出默认关闭
            conn.setDoOutput(doOutput);
        }
        
        //当3XX重定向时,当前实例是否访问重定向的地址
        conn.setInstanceFollowRedirects(instanceFollowRedirects);
        
        if (connectTimeout > 0)
        {//有设置连接超时秒数,大于0时有效
            conn.setConnectTimeout(connectTimeout * 1000);
        }
        
        if (readTimeout > 0)
        {//有设置读超时秒数,大于0时有效
            conn.setReadTimeout(readTimeout * 1000);
        }
        
        if (!useCaches)
        {//是否启用缓存,默认开启
            conn.setUseCaches(useCaches);
        }
        
        if (ifModifiedSince > 0)
        {//有设置比较资源时间
            conn.setIfModifiedSince(ifModifiedSince);
        }
        
        if (fixedContentLength != -1)
        {//有设置固定读资源内容大小,和设置资源块大小互斥,默认优先取固定读资源内容大小
            conn.setFixedLengthStreamingMode(fixedContentLength);
        }
        
        if (fixedContentLength == -1 && chunkLength != -1)
        {//有设置资源块大小,和设置固定资源大小互斥
            conn.setChunkedStreamingMode(chunkLength);
        }
        
        if (allowUserInteraction)
        {//有设置允许人工交互
            conn.setAllowUserInteraction(allowUserInteraction);
        }
            
        if (requestPropertyMap != null && !requestPropertyMap.isEmpty())
        {//请求自定义属性表
            for (Map.Entry<String, Object> entry : requestPropertyMap.entrySet())
            {
                String key = entry.getKey();
                Object value = entry.getValue();
                if (value instanceof String)
                    conn.setRequestProperty(key, (String)value);
                else
                {
                    ListS list = (ListS)value;
                    for (String val : list)
                    {
                        conn.addRequestProperty(key, val);
                    }
                }
            }
        }
        
        if (!hasUserAgent)
        {//未设置代理的,使用zhiqim默认值
            conn.addRequestProperty(_USER_AGENT_, _USER_AGENT_VALUE_);
        }
        
        if (!hasHost)
        {//未设置HOST的,从url中获取
            conn.addRequestProperty(_HOST_, uri.getHost());
        }
    }
    
    /***********************************************************/
    //增加请求属性信息,包括信任管理器,属性,是否输入输出流等
    /***********************************************************/
    
    /** 设置请求,由请求处理 */
    public void setRequest(HttpClientRequest request)
    {
        request.buildRequest(this);
    }
    
    /** 设置SSL协议 */
    public void setSSLProtocol(String protocol)
    {
        this.sslProtocol = protocol;
    }
    
    /** 设置SSL密钥管理器 */
    public void setSSLKeyManagers(KeyManager[] keyManagers)
    {
        this.sslKeyManagers = keyManagers;
    }
    
    /** 设置SSL信任管理器 */
    public void setSSLTrustManagers(TrustManager[] trustManagers)
    {
        this.sslTrustManagers = trustManagers;
    }
    
    /** 设置请求属性 */
    public void setRequestProperty(String key, int value)
    {
        setRequestProperty(key, String.valueOf(value));
    }
    
    /** 设置请求属性 */
    public void setRequestProperty(String key, String value)
    {
        if (_USER_AGENT_.equalsIgnoreCase(key))
            hasUserAgent = true;
        else if (_HOST_.equalsIgnoreCase(key))
            hasHost = true;
        
        requestPropertyMap.put(key, value);
    }
    
    /** 增加请求属性 */
    public void addRequestProperty(String key, int value)
    {
        addRequestProperty(key, String.valueOf(value));
    }
    
    /** 增加请求属性 */
    public void addRequestProperty(String key, String value)
    {
        if (_USER_AGENT_.equalsIgnoreCase(key))
            hasUserAgent = true;
        else if (_HOST_.equalsIgnoreCase(key))
            hasHost = true;
        
        Object val = requestPropertyMap.get(key);
        if (val == null)
            requestPropertyMap.put(key, value);
        else if (val instanceof ListS)
            ((ListS)val).add(value);
        else
        {
            ListS list = new ArrayListS();
            list.add((String)val);
            list.add(value);
            
            requestPropertyMap.put(key, list);
        }
    }
    
    /** 获取请求属性表,值为(String/ListS两种可能) */
    public HashMapSO getRequestPropertyMap()
    {
        return requestPropertyMap;
    }
    
    /** 获取请求属性对象(null/String/ListS三种可能) */
    public Object getRequestPropertyObj(String key)
    {
        return requestPropertyMap.get(key);
    }
    
    /** 获取请求属性 */
    public String getRequestProperty(String key)
    {
        Object value = requestPropertyMap.get(key);
        if (value == null)
            return null;
        else if (value instanceof String)
            return (String)value;
        else
            return ((ListS)value).get(0);
    }
    
    /** 是否有请求属性 */
    public boolean hasRequestProperty(String key)
    {
        return requestPropertyMap.containsKey(key);
    }
    
    /** 获取配置的URL */
    public String getUrl()
    {
        return url;
    }
    
    /** 获取配置的URL转URI */
    public URI getUri()
    {
        return uri;
    }
    
    /** 获取方法 */
    public String getMethod()
    {
        return method;
    }
    
    /** 判断是否打开了输入流 */
    public boolean isDoInput()
    {
        return doInput;
    }
    
    /** 设置是是否有输入流 */
    public void setDoInput(boolean doInput)
    {
        this.doInput = doInput;
    }
    
    /** 判断是否打开了输出流 */
    public boolean isDoOutput()
    {
        return doOutput;
    }
    
    /** 设置是是否有输出流 */
    public void setDoOutput(boolean doOutput)
    {
        this.doOutput = doOutput;
    }
    
    /** 设置连接超时时长,单位:秒 */
    public void setConnectTimeout(int connectTimeout)
    {
        this.connectTimeout = connectTimeout;
    }
    
    /** 获取连接超时时长,单位:秒 */
    public int getConnectTimeout()
    {
        return connectTimeout;
    }
    
    /** 设置数据获取超时时长,单位:秒 */
    public void setReadTimeout(int readTimeout)
    {
        this.readTimeout = readTimeout;
    }

    /** 获取读流超时时长 */
    public int getReadTimeout()
    {
        return readTimeout;
    }

    /** 设置协议是否使用缓存,默认是有条件即使用,我们修改成不使用 */
    public boolean isUseCaches()
    {
        return useCaches;
    }
    
    /** 设置协议是否使用缓存 */
    public void setUseCaches(boolean useCaches)
    {
        this.useCaches = useCaches;
    }

    /** 获取资源本地缓存上次最后修改时间 */
    public long getIfModifiedSince()
    {
        return ifModifiedSince;
    }

    /** 设置资源本地缓存上次最后修改时间 */
    public void setIfModifiedSince(long ifModifiedSince)
    {
        this.ifModifiedSince = ifModifiedSince;
    }
    
    /** 获取配置的块大小,-1表示未配置,操作时默认4096 */
    public int getChunkLength()
    {
        return chunkLength;
    }
    
    /** 设置块大小,-1表示不生效,取默认4096 */
    public void setChunkLength(int chunkLength)
    {
        this.chunkLength = chunkLength;
    }
    
    /** 获取配置的读取固定内容长度,-1表示不生效,取响应的Content-Length */
    public int getFixedContentLength()
    {
        return fixedContentLength;
    }
    
    /** 设置配置的读取固定内容长度,-1表示不生效,取响应的Content-Length */
    public void setFixedContentLength(int fixedContentLength)
    {
        this.fixedContentLength = fixedContentLength;
    }
    
    /** 判断是否允许用户交互,用于访问服务端时等待管理操作验证 */
    public boolean isAllowUserInteraction()
    {
        return allowUserInteraction;
    }

    /** 设置是否允许用户交互,默认是不允许 */
    public void setAllowUserInteraction(boolean allowUserInteraction)
    {
        this.allowUserInteraction = allowUserInteraction;
    }

    /** 判断当3XX重定向时,是否访问重定向的地址 */
    public boolean isInstanceFollowRedirects()
    {
        return instanceFollowRedirects;
    }

    /** 设置当3XX重定向时,是否访问重定向的地址 */
    public void setInstanceFollowRedirects(boolean instanceFollowRedirects)
    {
        this.instanceFollowRedirects = instanceFollowRedirects;
    }
    
    /***********************************************************/
    //获取结果信息,包括状态、内容和字节流
    /***********************************************************/
    
    /** 增加响应属性 */
    protected void doReadResponseProperty(URLConnection conn)
    {
        responseStatusLine = conn.getHeaderField(0);
        Map<String, List<String>> map = conn.getHeaderFields();
        
        for (String key : map.keySet())
        {
            if (Validates.isEmpty(key))
                continue;
            
            List<String> list = map.get(key);
            if (list == null || list.isEmpty())
                continue;
            
            if (list.size() == 1)
                responsePropertyMap.put(key.toLowerCase(), list.get(0));
            else
                responsePropertyMap.put(key.toLowerCase(), new ArrayListS(list));
        }
        
        if (keepAlive)
        {//检查服务端是否强制关闭
            Object connectionValue = responsePropertyMap.get(_CONNECTION_);
            if (connectionValue == null || !(connectionValue instanceof String))
            {//服务端未指定或指定多项的,认为服务端关闭
                responseClose = true;
            }
            else
            {//有keepAlive标志
                responseClose = !_KEEP_ALIVE_.equalsIgnoreCase((String)connectionValue);
            }
        }
    }
    
    /** 获取响应状态 */
    public int getResponseStatus()
    {
        return responseStatus;
    }
    
    /** 判断响应是否成功 */
    public boolean isResponseSuccess()
    {
        return responseStatus == 200;
    }
    
    /** 获取响应内容(正确内容或错误内容) */
    public String getResponseText()
    {
        return responseText;
    }
    
    /** 返回指定的Http调用结果 */
    public HttpResult getResult()
    {
        return new HttpResult(responseStatus, responseText);
    }
    
    /** 返回指定的Int类型的调用结果 */
    public Int getResultInt()
    {
        return new Int(responseStatus==200?0:responseStatus, responseText);
    }
    
    /** 获取调用前异常(responseStatus < 100) */
    public Throwable getException()
    {
        return exception;
    }
    
    /** 获取响应状态行 */
    public String getResponseStatusLine()
    {
        return responseStatusLine;
    }
    
    /** 获取响应属性表 */
    public HashMapSO getResponsePropertyMap()
    {
        return responsePropertyMap;
    }
    
    /** 获取响应属性(String/ListS两种可能) */
    public Object getResponsePropertyObj(String key)
    {
        return responsePropertyMap.get(key.toLowerCase());
    }
    
    /** 获取响应属性,列表取第一个 */
    public String getResponseProperty(String key)
    {
        Object value = responsePropertyMap.get(key.toLowerCase());
        if (value == null)
            return null;
        else if (value instanceof String)
            return (String)value;
        else
            return ((ListS)value).get(0);
    }
    
    /** 是否有响应属性 */
    public boolean hasResponseProperty(String key)
    {
        return responsePropertyMap.containsKey(key.toLowerCase());
    }
    
    /** 获取响应内容长度 */
    public int getResponseContentLength()
    {
        String value = getResponseProperty(_CONTENT_LENGTH_);
        if (Validates.isEmptyBlank(value))
            return 0;
        
        value = Strings.trim(value);
        if (!Validates.isIntegerPositive(value))
            return 0;
        
        return Ints.toInt(value);
    }
    
    /** 获取响应内容类型 */
    public String getResponseContentType()
    {
        return getResponseProperty(_CONTENT_TYPE_);
    }
    
    /** 获取响应内容字符集,默认UTF-8 */
    public String getResponseCharset()
    {
        return getResponseCharset(getResponseContentType());
    }
    
    /** 获取响应内容编码 */
    public String getResponseContentEncoding()
    {
        return getResponseProperty(_CONTENT_ENCODING_);
    }
    
    /** 判断响应内容编码是否是Gzip */
    public boolean isResponseGzip()
    {
        return _ENCODING_GZIP_.equals(getResponseContentEncoding());
    }
    
    /*****************************************************************************/
    //以下为静态方法
    /*****************************************************************************/
    
    /**
     * 判断是否响应使用GZIP压缩
     * 
     * @param conn  HTTP连接
     * @return      =true表示启用,=false表示未启用
     */
    public static boolean isResponseGzip(HttpURLConnection conn)
    {
        return "gzip".equals(conn.getContentEncoding());
    }
    
    /**
     * 从响应中获取内容字符集
     * 
     * @param contentType   内容类型
     * @return              String, 默认UTF-8
     */
    public static String getResponseCharset(String contentType)
    {
        if (Validates.isEmptyBlank(contentType))
            return _UTF_8_;
        
        String[] params = Arrays.toStringArray(contentType.trim(), ";");
        for (String param : params)
        {
            if (!param.startsWith(_CHARSET_))
                continue;
            
            String[] pair = Arrays.toStringArray(param, "=");
            if (pair.length != 2 || Validates.isEmpty(pair[1]))
                continue;

            return pair[1];
        }

        return _UTF_8_;
    }
    
    /**
     * 从响应中获取字符串,包括成功和失败的内容
     * 
     * @param conn          HTTP连接
     * @return              成功或失败的内容   
     * @throws IOException  可能的异常
     */
    public static String getResponseAsString(HttpURLConnection conn) throws IOException
    {
        boolean isGzip = isResponseGzip(conn);
        String charset = getResponseCharset(conn.getContentType());
        if (conn.getErrorStream() != null)
        {//有出错返回
            String message = null;
            if (isGzip)
                message = Streams.getStringGzip(conn.getErrorStream(), charset);
            else
                message = Streams.getString(conn.getErrorStream(), charset);
            
            if (Validates.isEmptyBlank(message))
                message = "调用服务端接口失败,错误码:"+conn.getResponseCode();
            
            return message;
        }
        else if (conn.getInputStream() != null)
        {//正常返回
            if (isGzip)
                return Streams.getStringGzip(conn.getInputStream(), charset);
            else
                return Streams.getString(conn.getInputStream(), charset);
        }
        else
        {//无出错内容返回
            return "调用服务端接口失败,错误码:"+conn.getResponseCode();
        }
    }
    
    /**
     * 读取错误流中的内容
     * 
     * @param conn          HTTP连接
     * @return              成功或失败的内容   
     * @throws IOException  可能的异常
     */
    public static String getResponseError(HttpURLConnection conn) throws IOException
    {
        InputStream stream = conn.getErrorStream();
        if (stream == null)
            return "调用服务端接口失败,错误码:"+conn.getResponseCode();
        
        boolean isGzip = isResponseGzip(conn);
        String charset = getResponseCharset(conn.getContentType());
        String message = null;
        if (isGzip)
            message = Streams.getStringGzip(stream, charset);
        else
            message = Streams.getString(stream, charset);
        
        if (Validates.isEmptyBlank(message))
            message = "调用服务端接口失败,错误码:"+conn.getResponseCode();
        
        return message;
    }
    
    /**
     * 通过连接获取下载名称
     * 
     * @param conn HttpURLConnection
     * @return fileName
     */
    public static String getFileName(HttpURLConnection conn)
    {
        String contentDisposition = conn.getHeaderField(_CONTENT_DISPOSITION_);
        if (contentDisposition == null)
            return null;
        
        Matcher matcher = _REGEX_FILE_NAME_.matcher(contentDisposition);
        if (matcher.find())
            return Stringx.trim(matcher.group(1));
        return null;
    }
    
    /**
     * 获取上传文件时文本参数配置
     * 
     * @param name          参数名
     * @param encoding      参数编码
     * @return              参数配置说明
     * @throws IOException  异常
     */
    public static byte[] getTextDisposition(String name, String encoding) throws IOException 
    {
        StringBuilder strb = new StringBuilder()
            .append("Content-Disposition: form-data; name=\"").append(name).append("\"").append(_BR_)
            .append(_BR_);
        return strb.toString().getBytes(encoding);
    }

    /**
     * 获取上传文件时文件参数配置
     * 
     * @param name          文件参数名
     * @param fileName      文件名
     * @param mimeType      文件类型
     * @param encoding      编码
     * @return              参数配置说明
     * @throws IOException  异常
     */
    public static byte[] getFileDisposition(String name, String fileName, String mimeType, String encoding) throws IOException 
    {
        StringBuilder strb = new StringBuilder()
            .append("Content-Disposition: form-data; name=\"").append(name).append("\"; filename=\"").append(fileName).append("\"").append(_BR_)
            .append("Content-Type:").append(mimeType).append(_BR_)
            .append(_BR_);
        return strb.toString().getBytes(encoding);
    }
}