アーキテクチャをスマートに。

株式会社ネオジニア代表。ITアーキテクトとしてのお仕事や考えていることなどをたまに綴っています。(記事の内容は個人の見解に基づくものであり、所属組織を代表するものではありません)

Swingアプリで HTTP プロキシの自動選択がおかしい

JavaGUIフレームワーク「Swing」を使ったアプリケーションにおいて、HTTP通信でサーバアクセスしたいときのことです。
Java実行環境(JRE 6)では、OS側のHTTPプロキシの設定があると、Java VMでもそれ基づいてプロキシを適用してくれます。
ところが、ある特定の環境だけなぜかプロキシ設定のとおりに動いてくれないことがありました。
困り果てていろいろ試行錯誤したことを備忘録としてここに残します。

現象

プロキシを使用しない例外ホストとして設定しているはずのホストに Java からHTTPアクセスしようとしたとき、プロキシを経由してしまう。
HTTP通信は、URL#openConnection() を使っています。
ネットワークの構成図は以下のような感じ。

プロキシ設定の例外ホストとは、
[コントロールパネル]→[インタネットオプション] の [接続]→[LANの設定]→[詳細設定] 画面で、「次で始まるアドレスにはプロキシを使用しない」の項目にセットされたホストことです。(下図参照)
読んで字のごとく、プロキシを使わずに直接接続するホストをここで指定します。ワイルドカードも使うことができて、例えば 192.168.56.* を指定すると、IPアドレスの前から3つが 192.168.56. であるホストにアクセスする際にはプロキシは使わないでください、といった内容の設定となります。

さて、問題はこのワイルドカードを使った指定を行ったときです。
192.168.56.* が例外設定されているとき、192.168.56.99 へアクセスすると、どうなるのが正しいでしょうか。
正解は、例外設定にマッチするので、プロキシを使わないでアクセスする、ということになります。
ところが、一部の環境でどうしてもプロキシへ接続しにいってしまう現象が見られました。

実験

いろいろなパターンで実験してみた結果、以下のようなデータが得られました。

各プログラムから http://192.168.56.99:8084/ へアクセスしたとき、プロキシの例外設定によって、プロキシを使うか直接つながるかを調べました。

実験結果はこのとおりです。

例外設定  IE  Swingアプリ Javaコンソールアプリ
192.168.56.99 直接つながる 直接つながる 直接つながる
192.168.56.* 直接つながる プロキシを使う 直接つながる
192.168.56 プロキシを使う(※) 直接つながる 直接つながる
なし プロキシを使う プロキシを使う プロキシを使う

※システム環境

  • Windows XP Pro SP3
  • IE8
  • JRE Java 6 update 30


つまり

  • Swingアプリケーション
  • プロキシの例外ホストにワイルドカードを使った指定を行っている

の場合に、プロキシを使ってしまう、ということがわかりました。


ところで、※印のところもちょっとおかしいですね。
でも、Windowsの設定とIEの組み合わせが標準であり、その挙動が常に正しいと捉えるならば、Javaコンソールアプリの方がおかしいことになります。しかし、例外設定の入力欄には「次で始まるアドレスにはプロキシを使用しない」とあるので、192.168.56.* と192.168.56 は同義と考えれば、IEの挙動がおかしいということになります。
どっちなんでしょうね。。。

この議論は、今回は取り上げません。

ここでは、192.168.56.* を例外設定した環境でのお話です。

考察

JREでは、URL#openConnection() を使う場合、プロキシの選択は自動的に行われます。Apache HTTPClient を使う場合でも同様です。プログラマがとくに意識しなくても、実行時にURLによって適切なプロキシが選択されます。

すごいです。
便利です。
ホンマ、ようでけたぁるわ。
まぁ感心するのはそれぐらいにしといて。

このときの仕組みをちょっと考えてみましょう。
JRE内部では java.net.ProxySelector を用いてプロキシを選択しています。ProxySelector.getDefault() すると、デフォルトのプロキシセレクタが取得できます。
プロキシセレクタに対して ProxySelector#select("http://xxx.xxx.xxx/") と問い合わせると適用されるプロキシが選択されて List が返ってきます。
この List の中身を見てみると、どのようなプロキシが選択されたかがわかる、というわけです。

以下のようなコードで、実際にその様子を調べることができます。

ProxySelector s = ProxySelector.getDefault();

for (java.net.Proxy p : s.select(new URI(url))) {
	System.out.println(p.address());
}            

この調査方法で、JREによるプロキシの選択結果を検証してみると、先述の実験結果と確かに一致しました。

解決に向けて

まず試したこと。

  • VM引数 -Dhttp.nonProxyHosts を指定する。
  • VM引数 -Dhttp.proxyHost を指定する。(ブランクにする)
  • URL#openConnection() の引数に java.net.Proxy.NO_PROXY を指定する。

でもどれもダメでした。
(このへんの試行錯誤もかなり長かったのですが、記事として面白くないので省略します)

なぜ Swingアプリケーションのときだけオカシクなるのかが分かりませんが、

とにかくプロキシを使わずに直接接続してくれればよかったので、ProxySelector のインスタンスを自分で作り、ProxySelector.setDefault() してやれ!ということで試してみました。

ProxySelectorを自作する

まず ProxySelector.getDefault() で返されるオブジェクトを調べてみると、sun.net.spi.DefaultProxySelector クラスとなっていました。このクラス名でググッてみるとソースコードが見つかるので、それを参考にしながら、「プロキシを選択しないプロキシセレクタ」を作ってデフォルトに設定するコードを書いてみました。

public final class ProxyDisabler {
	
	public static void apply() {
		ProxySelector.setDefault(new  ProxySelector() {
			@Override
			public List<Proxy> select(URI uri) {
				List<Proxy> l = new ArrayList<Proxy>(1);
				l.add(Proxy.NO_PROXY);
				return l;
			}
			
			@Override
			public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {}
		});
	}
}

これを main() の最初の方で

ProxyDisabler.apply();

のように呼び出すようにしました。

結果、これでバッチリ!
Swingアプリケーションでもプロキシを使わないでHTTP通信することができました。

ただし、このハックはプロキシを絶対に使わないようにしてしまうので、注意が必要です。
まともな解決策を考えるなら、sun.net.spi.DefaultProxySelector クラスを参考に、IEのプロキシ選択と同じ挙動をするプロキシセレクタを作るべきです。

終わりに

実は今回の問題発生は、そもそもアーキテクチャの設計があまりよろしくないことに起因しているのですが、その辺の話はまた別の機会に。。。

アーキテクチャの設計ってほんとに大切だなとつくづく思い知らされました。