1. ホーム
  2. java

[解決済み] java.net.URLConnectionを使用してHTTPリクエストを発生させ処理する方法

2022-03-16 06:17:49

質問

使用方法 java.net.URLConnection は、ここで結構頻繁に質問されるのですが、その際に オラクルチュートリアル を簡潔に説明します。

そのチュートリアルでは、基本的に GET をリクエストし、そのレスポンスを読みます。このメソッドを使って、特に POST リクエスト、リクエストヘッダの設定、レスポンスヘッダの読み込み、クッキーの処理、HTMLフォームの送信、ファイルのアップロードなどです。

では、どうすれば java.net.URLConnection を使用して、高度な HTTP リクエストを処理できますか?

どのように解決するのですか?

まず最初に、掲載されているコード例はすべて基本的な例であることをあらかじめお断りしておきます。些細な IOExceptionRuntimeException のように NullPointerException , ArrayIndexOutOfBoundsException と自分を慰める。

JavaではなくAndroidで開発している場合、APIレベル28の導入以降、平文のHTTPリクエストは、以下のようになることにも注意してください。 デフォルトで無効 . を使用することが推奨されます。 HttpsURLConnection しかし、本当に必要であれば、Application ManifestでClearTextを有効にすることができます。


準備中

まず、最低でも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 形式で連結され & . また、通常であれば URLエンコード を使うと、クエリパラメータを指定した文字セットで表示することができます。 URLEncoder#encode() .

String#format() は単なる便宜的なものです。私は、文字列連結演算子 + を2回以上使用します。


を発射する 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に設定します。ウェブフォームが行う標準的な 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 リクエストを発行する

HTTP リクエストを明示的に起動するには URLConnection#connect() を使用してレスポンスボディなどの 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());
     }
    
    
  2. 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.
    }


セッションの維持

サーバー側のセッションは、通常、クッキーによってバックアップされています。いくつかのウェブフォームでは、ログインしていること、および/または、セッションによって追跡されることが要求されます。そのような場合は CookieHandler APIを使用してCookieを保持することができます。を用意する必要があります。 CookieManager と共に CookiePolicy ACCEPT_ALL は、すべてのHTTPリクエストを送信する前に

// 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();
// ...

この方法は、すべての状況下で常に正しく動作するわけではないことが知られていることに注意してください。もしこれがうまくいかない場合は、手動でクッキーヘッダを収集し、設定するのが一番です。基本的には、すべての Set-Cookie ヘッダは、ログイン時のレスポンス、あるいは最初の GET を作成し、これを後続のリクエストに渡します。

// 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] のように、サーバサイドに無関係なクッキーの属性を取り除くために存在します。 expires , path など。あるいは cookie.substring(0, cookie.indexOf(';')) の代わりに split() .


ストリーミングモード

HttpURLConnection はデフォルトでバッファリングされます。 全体 リクエストのボディを実際に送信する前に、コンテンツの長さを connection.setRequestProperty("Content-Length", contentLength); . このため OutOfMemoryException は、大きな POST リクエスト (ファイルのアップロードなど) を同時に送信するたびに発生します。これを避けるには、POST リクエストの際に HttpURLConnection#setFixedLengthStreamingMode() .

httpConnection.setFixedLengthStreamingMode(contentLength);

しかし、コンテンツの長さが本当に事前にわからない場合は、チャンクドストリーミングモードを利用するために HttpURLConnection#setChunkedStreamingMode() に従ってください。これによってHTTPの Transfer-Encoding ヘッダーを chunked これは、リクエストボディをチャンクで送信することを強制します。以下の例では、1KBのチャンクでボディを送信します。

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");


ファイルのアップロード

通常は multipart/form-data エンコーディングは、POST コンテンツ (バイナリおよび文字データ) が混在している場合に使用します。このエンコーディングの詳細については 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();
}

もし相手が HttpServlet であれば、その doPost() メソッドが呼び出され、部品が HttpServletRequest#getPart() (注、このように ではなく getParameter() などなど!)。は getPart() しかし、このメソッドは比較的新しく、Servlet 3.0 (Glassfish 3, Tomcat 7 など) で導入されました。Servlet 3.0 より前のバージョンでは、最も適した方法は アパッチコモンズファイルアップロード をパースして multipart/form-data リクエストに対応します。また この回答 は、FileUploadとServelt 3.0の両方のアプローチの例を示しています。


信頼されていないサイトや設定ミスのあるHTTPSサイトへの対応

Javaではなく、Android向けに開発する場合。 気をつける 開発中に正しい証明書が配備されない場合、以下の回避策で救われるかもしれません。しかし、本番環境では使用しないでください。最近(2021年4月)、Googleは安全でないホスト名検証器を検出した場合、アプリをPlay Storeで配布することを許可しないようになっています。 https://support.google.com/faqs/answer/7188426。

ウェブスクレイパーを作成している場合など、HTTPSのURLを接続する必要がある場合があります。そのような場合、おそらく javax.net.ssl.SSLException: Not trusted server certificate は、SSL証明書を最新にしていないHTTPSサイト、あるいは java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found または javax.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を解析してデータを取り出すだけなら、以下のようなHTMLパーサーを使うと良いでしょう。 Jsoup .