如何使用 java.net.URLConnection 触发和处理 HTTP 请求

新手上路,请多包涵

使用 java.net.URLConnection 在这里经常被问到, Oracle 教程 对此 过于 简洁。

该教程基本上只展示了如何触发 GET 请求和读取响应。它没有在任何地方解释如何使用它来执行 POST 请求、设置请求标头、读取响应标头、处理 cookie、提交 HTML 表单、上传文件等。

那么,我如何使用 java.net.URLConnection 来触发和处理“高级”HTTP 请求?

原文由 BalusC 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 672
2 个回答

首先事先声明:发布的代码片段都是基本示例。 You’ll need to handle trivial IOException s and RuntimeException s like NullPointerException , ArrayIndexOutOfBoundsException and consorts yourself.

如果您正在为 Android 而不是 Java 进行开发,还请注意,自从引入 API 级别 28 以来 ,默认情况下禁用 明文 HTTP 请求。我们鼓励您使用 HttpsURLConnection ,但如果确实有必要,可以在应用程序清单中启用明文。


准备中

我们首先需要至少知道 URL 和字符集。参数是可选的,取决于功能要求。

 String url = "http://example.com";
String charset = "UTF-8";  // Or in Java 7 and later, use the constant: java.nio.charset.StandardCharsets.UTF_8.name()
String param1 = "value1";
String param2 = "value2";
// ...

String query = String.format("param1=%s&param2=%s",
    URLEncoder.encode(param1, charset),
    URLEncoder.encode(param2, charset));

查询参数必须采用 name=value 格式,并由 & 连接。您通常还会使用 URLEncoder#encode() 对具有指定字符集的查询参数 进行 URL 编码

String#format() 只是为了方便。当我需要字符串连接运算符 + 两次以上时,我更喜欢它。


使用(可选)查询参数触发 HTTP GET 请求

这是一项微不足道的任务。这是默认的请求方法。

 URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...

任何查询字符串都应使用 ? 连接到 URL。 Accept-Charset 标头可能会提示服务器参数的编码方式。如果您不发送任何查询字符串,则可以保留 Accept-Charset 标头。如果您不需要设置任何标头,那么您甚至可以使用 URL#openStream() 快捷方式。

 InputStream response = new URL(url).openStream();
// ...

无论哪种方式,如果另一方是 HttpServlet ,那么它的 doGet() 方法将被调用并且参数将通过 HttpServletRequest#getParameter() 可用

出于测试目的,您可以将响应正文打印到 标准输出,如下所示:

 try (Scanner scanner = new Scanner(response)) {
    String responseBody = scanner.useDelimiter("\\A").next();
    System.out.println(responseBody);
}


使用查询参数触发 HTTP POST 请求

URLConnection#setDoOutput() 设置为 true 隐式地将请求方法设置为 POST。 Web 表单所做的标准 HTTP POST 是类型 application/x-www-form-urlencoded 其中查询字符串被写入请求正文。

 URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true); // Triggers POST.
connection.setRequestProperty("Accept-Charset", charset);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + charset);

try (OutputStream output = connection.getOutputStream()) {
    output.write(query.getBytes(charset));
}

InputStream response = connection.getInputStream();
// ...

注意:每当您想以编程方式提交 HTML 表单时,不要忘记将 name=value 成对的任何 <input type="hidden"> 元素放入查询字符串中,当然还有 name=value 一对 <input type="submit"> 你想以编程方式“按下”的元素(因为它通常在服务器端用于区分按钮是否被按下,如果是,是哪个) .

您还可以将获得的 URLConnection 转换为 HttpURLConnection 并使用其 HttpURLConnection#setRequestMethod() 代替。但是,如果您尝试使用连接进行输出,您仍然需要将 URLConnection#setDoOutput() 设置为 true

 HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...

无论哪种方式,如果另一方是 HttpServlet ,那么它的 doPost() 方法将被调用并且参数将由 HttpServletRequest#getParameter() 提供


实际触发 HTTP 请求

您可以使用 URLConnection#connect() 显式触发 HTTP 请求,但是当您想要获取有关 HTTP 响应的任何信息时,请求将按需自动触发,例如使用 URLConnection#getInputStream() 的响应主体等等。上面的示例正是这样做的,所以 connect() 调用实际上是多余的。


收集 HTTP 响应信息

  1. HTTP 响应状态

你需要一个 HttpURLConnection 在这里。如有必要,请先施放它。

     int status = httpConnection.getResponseCode();

  1. HTTP 响应标头
     for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
        System.out.println(header.getKey() + "=" + header.getValue());
    }

  1. HTTP 响应编码

Content-Type 包含 charset 参数时,响应主体可能是基于文本的,然后我们希望使用服务器端指定的字符编码处理响应主体。

     String contentType = connection.getHeaderField("Content-Type");
    String charset = null;

    for (String param : contentType.replace(" ", "").split(";")) {
        if (param.startsWith("charset=")) {
            charset = param.split("=", 2)[1];
            break;
        }
    }

    if (charset != null) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset))) {
            for (String line; (line = reader.readLine()) != null;) {
                // ... System.out.println(line)?
            }
        }
    } else {
        // It's likely binary content, use InputStream/OutputStream.
    }


维护会话

服务器端会话通常由 cookie 支持。某些 Web 表单要求您登录和/或被会话跟踪。您可以使用 CookieHandler API 来维护 cookie。在发送所有 HTTP 请求之前,您需要准备一个 CookieManager 和一个 CookiePolicyACCEPT_ALL

 // First set the default cookie manager.
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));

// All the following subsequent URLConnections will use the same cookie manager.
URLConnection connection = new URL(url).openConnection();
// ...

connection = new URL(url).openConnection();
// ...

connection = new URL(url).openConnection();
// ...

请注意,众所周知,这并不总是在所有情况下都能正常工作。如果它对您来说失败了,那么最好是手动收集和设置 cookie 标头。您基本上需要从登录响应或第一个 GET 请求中获取所有 Set-Cookie 标头,然后通过后续请求传递它。

 // Gather all cookies on the first request.
URLConnection connection = new URL(url).openConnection();
List<String> cookies = connection.getHeaderFields().get("Set-Cookie");
// ...

// Then use the same cookies on all subsequent requests.
connection = new URL(url).openConnection();
for (String cookie : cookies) {
    connection.addRequestProperty("Cookie", cookie.split(";", 2)[0]);
}
// ...

split(";", 2)[0] 是为了摆脱与服务器端无关的 cookie 属性,例如 expires , path , 等 --- 或者你也可以使用 cookie.substring(0, cookie.indexOf(';')) 而不是 split()


串流模式

HttpURLConnection 默认情况下会在实际发送之前缓冲 整个 请求主体,无论您是否使用 connection.setRequestProperty("Content-Length", contentLength); 自己设置了固定的内容长度。这可能会导致 OutOfMemoryException 每当您同时发送大型 POST 请求(例如上传文件)时。为避免这种情况,您需要设置 HttpURLConnection#setFixedLengthStreamingMode()

 httpConnection.setFixedLengthStreamingMode(contentLength);

但是,如果事先确实不知道内容长度,则可以通过相应地设置 HttpURLConnection#setChunkedStreamingMode() 来使用分块流模式。这会将 HTTP Transfer-Encoding 标头设置为 chunked 这将强制请求正文以块的形式发送。下面的示例将以 1 KB 的块发送正文。

 httpConnection.setChunkedStreamingMode(1024);


用户代理

可能会发生 请求返回意外响应,而它在真实的网络浏览器上运行良好。服务器端可能正在阻止基于 User-Agent 请求标头的请求。 URLConnection 默认将其设置为 Java/1.6.0_19 最后一部分显然是 JRE 版本。您可以按如下方式覆盖它:

 connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); // Do as if you're using Chrome 41 on Windows 7.

使用 最近浏览器 中的 User-Agent 字符串。


错误处理

如果 HTTP 响应代码是 4nn (客户端错误)或 5nn (服务器错误),那么如果服务器必须查看 HttpURLConnection#getErrorStream() 发送任何有用的错误信息。

 InputStream error = ((HttpURLConnection) connection).getErrorStream();

如果 HTTP 响应代码为 -1,则连接和响应处理出现问题。 HttpURLConnection 实现在较旧的 JRE 中,在保持连接活动方面存在一些问题。您可能希望通过将 http.keepAlive 系统属性设置为 false 来关闭它。您可以通过以下方式在应用程序的开头以编程方式执行此操作:

 System.setProperty("http.keepAlive", "false");


上传文件

您通常会对混合 POST 内容(二进制和字符数据)使用 multipart/form-data 编码。编码在 RFC2388 中有更详细的描述。

 String param = "value";
File textFile = new File("/path/to/file.txt");
File binaryFile = new File("/path/to/file.bin");
String boundary = Long.toHexString(System.currentTimeMillis()); // Just generate some unique random value.
String CRLF = "\r\n"; // Line separator required by multipart/form-data.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);

try (
    OutputStream output = connection.getOutputStream();
    PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);
) {
    // Send normal param.
    writer.append("--" + boundary).append(CRLF);
    writer.append("Content-Disposition: form-data; name=\"param\"").append(CRLF);
    writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF);
    writer.append(CRLF).append(param).append(CRLF).flush();

    // Send text file.
    writer.append("--" + boundary).append(CRLF);
    writer.append("Content-Disposition: form-data; name=\"textFile\"; filename=\"" + textFile.getName() + "\"").append(CRLF);
    writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF); // Text file itself must be saved in this charset!
    writer.append(CRLF).flush();
    Files.copy(textFile.toPath(), output);
    output.flush(); // Important before continuing with writer!
    writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.

    // Send binary file.
    writer.append("--" + boundary).append(CRLF);
    writer.append("Content-Disposition: form-data; name=\"binaryFile\"; filename=\"" + binaryFile.getName() + "\"").append(CRLF);
    writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(binaryFile.getName())).append(CRLF);
    writer.append("Content-Transfer-Encoding: binary").append(CRLF);
    writer.append(CRLF).flush();
    Files.copy(binaryFile.toPath(), output);
    output.flush(); // Important before continuing with writer!
    writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.

    // End of multipart/form-data.
    writer.append("--" + boundary + "--").append(CRLF).flush();
}

If the other side is an HttpServlet , then its doPost() method will be called and the parts will be available by HttpServletRequest#getPart() (note, thus not getParameter() 等等!)。 getPart() 方法相对较新,它在 Servlet 3.0(Glassfish 3、Tomcat 7 等)中引入。在 Servlet 3.0 之前,您最好的选择是使用 Apache Commons FileUpload 来解析 multipart/form-data 请求。另请参阅 此答案 以获取 FileUpload 和 Servelt 3.0 方法的示例。


处理不受信任或配置错误的 HTTPS 站点

如果您正在为 Android 而不是 Java 进行开发,请注意:如果您在开发期间没有部署正确的证书,下面的解决方法可能会节省您的时间。但是你不应该将它用于生产。这些天(2021 年 4 月)如果 Google 检测到不安全的主机名验证程序,Google 将不允许您的应用程序在 Play 商店中分发,请参阅 https://support.google.com/faqs/answer/7188426。

有时您需要连接 HTTPS URL,可能是因为您正在编写网络抓取工具。在这种情况下,您可能会在某些未更新 SSL 证书的 HTTPS 站点上面临 javax.net.ssl.SSLException: Not trusted server certificate ,或者 java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] foundjavax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name 一些错误配置的 HTTPS 站点。

以下一次性运行 static 网络爬虫类中的初始化程序应该使 HttpsURLConnection 对于那些 HTTPS 站点更加宽松,因此不再抛出这些异常。

 static {
    TrustManager[] trustAllCertificates = new TrustManager[] {
        new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null; // Not relevant.
            }
            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
                // Do nothing. Just allow them all.
            }
            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
                // Do nothing. Just allow them all.
            }
        }
    };

    HostnameVerifier trustAllHostnames = new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true; // Just allow them all.
        }
    };

    try {
        System.setProperty("jsse.enableSNIExtension", "false");
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCertificates, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(trustAllHostnames);
    }
    catch (GeneralSecurityException e) {
        throw new ExceptionInInitializerError(e);
    }
}


最后的话

Apache HttpComponents HttpClient 在这方面要方便 得多:)


解析和提取 HTML

如果您只想从 HTML 中解析和提取数据,那么最好使用像 Jsoup 这样的 HTML 解析器。

原文由 BalusC 发布,翻译遵循 CC BY-SA 4.0 许可协议

使用 HTTP 时,引用 HttpURLConnection 而不是基类 URLConnection 几乎总是更有用(因为 URLConnection.openConnection() URLConnection 是一个抽象类 --- 在 HTTP URL 上,无论如何你都会得到)。

然后,您可以不依赖 URLConnection#setDoOutput(true) 隐式地将请求方法设置为 POST 而不是 httpURLConnection.setRequestMethod("POST") 有些人可能会觉得更自然(并且还允许您指定其他请求方法,例如 _放_, _删除_,…)。

它还提供了有用的 HTTP 常量,因此您可以:

 int responseCode = httpURLConnection.getResponseCode();

if (responseCode == HttpURLConnection.HTTP_OK) {

原文由 Paal Thorstensen 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题