最近仕事で使っているサードパーティ製のライブラリで、
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
このブログに、プロキシ越しにアクセスします。
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);
今回、ここまではやりませんでしたけど。