CLOVER🍀

That was when it all began.

ProxySelectorを使用して、動的にプロキシを設定する

最近仕事で使っているサードパーティ製のライブラリで、

        URL url = new URL("http://...");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

みたいな感じでURL#openConnectionを使っているコードがあるのですが、

という課題にぶつかっておりまして、けっこう困っています。

JavaでURL#openConnectionを使う場合にプロキシを適用するには、

  • -Dhttp.proxyHost および -Dhttp.proxyPort(-Dhttps.proxyHost、-Dhttps.proxyPortなど)でプロキシを設定する
  • -Djava.net.useSystemProxies=trueを設定する

など、システムプロパティで設定し、除外するホストはhttp.nonProxyHostsなどを使用するのが一般的だと思います。

が、どちらかというと「プロキシを適用したいホストを決めたい」のですが、これだと「プロキシを適用しないホストを列挙する」という形になってしまっていて、少々苦しいです。

で、どうしようかなぁと思っていたのですが、ProxySelectorというものを使用すれば、なんとかなりそうですね。知りませんでした。

参考)
http://www.mwsoft.jp/programming/java/http_proxy.html
http://docs.oracle.com/javase/jp/7/technotes/guides/net/proxies.html

というわけで、これでプロキシを動的に設定してみましょう。

プロキシサーバを用意する

とりあえず、プロキシサーバがいるので、簡単に用意することに。当方、Utunbu 12.04 LTSです。

Apacheインストール。

$ sudo apt-get install apache2

mod_proxy有効化。

$ cd /etc/apache2
$ sudo cp mods-available/proxy.load mods-enabled/
$ sudo cp mods-available/proxy_http.load mods-enabled/

設定(/etc/apache2/conf.d配下)。

Listen 9000

<VirtualHost *:9000>
  ProxyRequests On
  ProxyVia On

<Proxy *>
  Order deny,allow
  Deny from all
  Allow from localhost
</Proxy>
</VirtualHost>

リッスンポート9000のプロキシサーバ。簡単ですが、こんなものでしょう。

起動。

$ sudo service apache2 start

プロキシを使ってアクセスする

まずは、標準的なコード。
ProxySample.java

import java.net.HttpURLConnection;
import java.net.URL;

public class ProxySample {
    public static void main(String... args) throws Exception {
        URL url = new URL(args[0]);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        System.out.printf("Access URL => %s%n", url);
        System.out.printf("Response Code => %d%n", conn.getResponseCode());
    }
}

コードの上では、プロキシ関係ないですか…。

実行。

$ java -Dhttp.proxyHost=localhost -Dhttp.proxyPort=9000 ProxySample http://d.hatena.ne.jp/Kazuhira/
Access URL => http://d.hatena.ne.jp/Kazuhira/
Response Code => 200

このブログに、プロキシ越しにアクセスします。

Apacheのアクセスログには、このように出力されます。

127.0.0.1:9000 localhost - - [06/Jul/2014:17:11:46 +0900] "GET http://d.hatena.ne.jp/Kazuhira/ HTTP/1.1" 200 4508 "-" "Java/1.8.0_05"

ちゃんと、プロキシ越しにアクセスしていますね。もちろん、-Dhttp.proxy〜を除外すると、プロキシを踏まなくなります。

ProxySelectorについて

ProxySelectorとは、与えられたURLに対してプロキシサーバを選択するクラスです。Javaの実行時に、デフォルトのProxySelectorが登録されており、ProxySelector#getDefaultで取得、ProxySelector#setDefaultで設定することができます。

デフォルトの実装を確認してみましょう。
DefaultProxy.java

import java.net.ProxySelector;

public class DefaultProxy {
    public static void main(String... args) {
        ProxySelector proxySelector = ProxySelector.getDefault();
        System.out.println(proxySelector.getClass().getName());
    }
}

実行。

$ java DefaultProxy
sun.net.spi.DefaultProxySelector

デフォルトでは、「sun.net.spi.DefaultProxySelector」というクラスが使用されているようです。

ちなみに、「http.proxyHost」や「http.proxyPort」などを解決しているクラスは、実はこのクラスになります。
http://www.docjar.com/html/api/sun/net/spi/DefaultProxySelector.java.html

ProxySelectorを実装するには、ProxySelectorクラスを継承し、selectメソッドおよびconnectFailedメソッドを実装する必要があります。

独自のProxySelectorを作成する

それでは、自分でProxySelectorを作成してみましょう。

このようなクラスを作成してみました。Javaのドキュメントを、かなり真似たものではありますが。
ProxySelectorExample.java

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.List;

public class ProxySelectorExample {
    public static void main(String... args) throws Exception {
        ProxySelector proxySelector = ProxySelector.getDefault();

        ProxySelector myProxySelector = new MyProxySelector(proxySelector);
        ProxySelector.setDefault(myProxySelector);

        URL url = new URL(args[0]);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        System.out.printf("Access URL => %s%n", url);
        System.out.printf("Response Code => %d%n", conn.getResponseCode());
    }

    public static class MyProxySelector extends ProxySelector {
        private ProxySelector delegate;

        public MyProxySelector(ProxySelector delegate) {
            this.delegate = delegate;
        }

        @Override
        public List<Proxy> select(URI uri) {
            if ("http".equals(uri.getScheme()) &&
                "d.hatena.ne.jp".equals(uri.getHost())) {
                return Arrays.asList(new Proxy(Proxy.Type.HTTP,
                                               new InetSocketAddress("localhost", 9000)));
            }

            if (delegate != null) {
                return delegate.select(uri);
            } else {
                return Arrays.asList(Proxy.NO_PROXY);
            }
        }

        @Override
        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
            ioe.printStackTrace();
        }
    }
}

ProxySelectorの実装は、staticなインナークラスにしています。

実装としては、コンストラクタで委譲先(今回はデフォルトの実装を想定)を受け取り、

        public MyProxySelector(ProxySelector delegate) {
            this.delegate = delegate;
        }

ProxySelector#selectが呼び出された時に、プロトコルが「http」でホストが「d.hatena.ne.jp」であれば、先ほど構築したプロキシサーバを選択するように実装しました。

        @Override
        public List<Proxy> select(URI uri) {
            if ("http".equals(uri.getScheme()) &&
                "d.hatena.ne.jp".equals(uri.getHost())) {
                return Arrays.asList(new Proxy(Proxy.Type.HTTP,
                                               new InetSocketAddress("localhost", 9000)));
            }

それ以外であれば、委譲先のProxySelectorが設定されていればselectメソッドを呼び出し、なければプロキシなしとしています。

           if (delegate != null) {
                return delegate.select(uri);
            } else {
                return Arrays.asList(Proxy.NO_PROXY);
            }

connectFailedメソッドは、selectメソッドで返したいずれかのプロキシへの接続が失敗した時に呼び出されるようですが、今回はスタックトレースを出力して終了としました。

        @Override
        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
            ioe.printStackTrace();
        }

今回は、デフォルトのProxySelectorを取得して、自作のProxySelectorの委譲先として設定した後に、デフォルトのProxySelectorとして登録します。

        ProxySelector proxySelector = ProxySelector.getDefault();

        ProxySelector myProxySelector = new MyProxySelector(proxySelector);
        ProxySelector.setDefault(myProxySelector);

では、実行。

$ java ProxySelectorExample http://d.hatena.ne.jp/Kazuhira/
Access URL => http://d.hatena.ne.jp/Kazuhira/
Response Code => 200

特にシステムプロパティは設定していませんが、Apacheのアクセスログにはちゃんと記録されます。

127.0.0.1:9000 localhost - - [06/Jul/2014:17:22:16 +0900] "GET http://d.hatena.ne.jp/Kazuhira/ HTTP/1.1" 200 10315 "-" "Java/1.8.0_05"

それ以外の場合は、プロキシは使用されません。

$ java ProxySelectorExample http://www.yahoo.co.jp/Access
URL => http://www.yahoo.co.jp/
Response Code => 200

もちろん、「http.proxyHost」や「http.proxyPort」を指定すればデフォルトの挙動が変わるので、プロキシを使うようになりますが。

とまあ、システムプロパティではなくて、Java側で動的にプロキシを決定することができることがわかりました。

とはいえ、実行環境のJavaVM全体にかかってしまうところは、相変わらずですが。

補足

今回は、URL#openConnectionの呼び出しを変えられないという前提で話を進めましたが、ここを扱えるのであれば、URL#openConnectionの引数にProxyを渡せば設定可能なようです。

SocketAddress addr = new InetSocketAddress("webcache.example.com", 8080);
Proxy proxy = new Proxy(Proxy.Type.HTTP, addr);
URL url = new URL("http://java.example.org/");
URConnection conn = url.openConnection(proxy);

今回、ここまではやりませんでしたけど。