Javaの最近のブログ記事

概要

JPA(EntityManager) + Hibernate + Ehcache(二次キャッシュ) + Seasar2 の組み合わせで利用しているとき、二次キャッシュを利用せずにエンティティを生成 (insert) する方法について解説します。

想定するケース

  • Ehcache を使い、マルチキャストにより LAN 内で二次キャッシュを共有している。
  • あるエンティティは @Cache アノテーションにより、二次キャッシュを利用している。
  • 大量にそのエンティティを追加するときは二次キャッシュを利用したくない。
この場合、最も簡単かつ推奨する方法は
  • 二次キャッシュを利用しない設定で構築した Web サーバまたはコマンドライン・プログラムを用意し、そこで処理を実行する。
ですが、なんらかの理由で二次キャッシュを利用した web サーバ上で処理を実行しなければならない場合、以下の手順で実現できます。

設定手順

META-INF/persistence.xml に記述を追加

META-INF/persistence.xml で既存の persistenceUnit タグをコピーし、二次キャッシュを利用しない設定を行います。 具体的には、以下の修正を行います。
  • persistence-unit タグの name 属性を db1nocache_persistenceUnit のような名前に変更してください。
  • 利用するエンティティを明示的に class タグで宣言してください。中で別のエンティティを参照している場合、それらもすべて宣言する必要があります。
  • hibernate.cache.use_query_cache を false に設定。
  • hibernate.cache.use_second_level_cache を false に設定。

jpa_nocache.dicon を作成

jpa.dicon から jpa_nocache.dicon を呼び出す

jpa.dicon の components タグの後ろに、次の記述を追加します。
	

利用手順

Service クラスの中で次のように利用します。

概要

StringBuider で append() を使い1文字を追加するとき、
sb.append('\t');
のように char 型を追加するときと
sb.append("\t");
のように文字列型で追加するときに性能差を調査した。

結果

追加データ実行時間 [s]
'\t' 2.165
"\t" 7.788

1つの文字を追加する場合は、 char 型で追加したほうが文字列で追加するより 3.6 倍高速だった。

概要

Javaでwebサイトを運用し、ある程度規模が大きくなってくるとメモリ管理で悩まされる。
安定、高速なwebサイトを目指すためにはメモリ関連のモニタリングとチューニングが必要になってくる。

ガベージ・コレクション関連のログ出力設定を行う方法は簡単だが、nagios などで監視を行い、閾値に達したらアラート・メールで通知してくれると便利である。

今回は Java の snmp 監視を有効にする設定手順について説明する。

開発環境

項目
OS Windows XP Pro SP3
IDE 環境 Eclipse 3.4.1(Preiades All in One)
Java JDK 6 update 10

Windows 環境での設定手順

概要

Windows 上で起動中の Eclipse から Java プログラムを起動し、そのプロセスを snmp で監視できるよう、設定を行う。

プログラム例

次のようなプログラムを作成した。
public class Test {
	public static void main(String[] args) throws InterruptedException {
		int count = 0;
		
		for (;;) {
			Thread.sleep(1000);
		
			count++;
			System.out.println(count);
		}
	}
}
とりあえず1回実行してみる。
[実行] メニュー ⇒ 実行 ⇒ Java アプリケーション

起動オプション設定

1回実行した後、今度は実行構成を開く。
[実行] メニュー ⇒ 実行構成


[引数] タブを選択し、「VM引数」のところに次の文字列を入力する。
-Dcom.sun.management.snmp.port=161 -Dcom.sun.management.snmp.acl.file=c:/snmp.acl -Dcom.sun.management.config.file=c:/management.properties

設定ファイルのコピー

次に、C:\Program Files\Java\jre6\lib\management の下にある、次の2つのファイルを C:\ にコピーする。
  • management.properties
  • snmp.acl.template
snmp.acl.template は snmp.acl に名前を変える。

設定ファイルの修正

c:\snmp.acl
ホスト 172.16.1.2 からコミュニティ名「public」で snmp 読み込みを受け付ける場合は、次のように記述する。
   acl = {
     {
       communities = public
       access = read-only
       managers = 172.16.1.2
     }
    }
 
c:\management.properties
Java 起動時、デフォルト設定では localhost で 161 ポートを listen する。別のホストから snmp を受け付けるには、次のように Java プログラムを起動するホストの IP アドレスを設定する。
com.sun.management.snmp.interface=172.16.1.3

アクセス権の修正

エクスプローラで c:\snmp.acl 上で右クリック、[プロパティ] を選択する。


[セキュリティ] タブを開き、[詳細設定] ボタンを押す。

「子オブジェクトに適用するアクセス許可エントリを親から継承し、それらをここで明示的に定義されているものに含める」についているチェックを外す。

「アクセス許可エントリ」から、自分の名前以外を全て削除する。
Administrator や他のユーザからのアクセス権が残っていると、Java プログラム起動時に次のようなエラーメッセージが出てしまう。
エラー: パスワードファイルの読み取りアクセスは制限する必要があります。: c:/snmp.acl

これで準備完了。
サンプルプログラムを起動したあと、snmp アクセスを許可したホスト(上記の例だと 172.16.1.2) から次のようにアクセスしてみる。
$ snmpwalk -v 2c -c public 172.16.1.3 .1 -On | less
次のような内容が出力される。
.1.3.6.1.4.1.42.2.145.3.163.1.1.1.1.0 = Gauge32: 729
.1.3.6.1.4.1.42.2.145.3.163.1.1.1.4.0 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.1.0 = Gauge32: 0
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.2.0 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.3.0 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.100.1.2.1 = STRING: "CodeCacheManager"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.100.1.2.2 = STRING: "Copy"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.100.1.2.3 = STRING: "MarkSweepCompact"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.100.1.3.1 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.100.1.3.2 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.100.1.3.3 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.2.1 = STRING: "Code Cache"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.2.2 = STRING: "Eden Space"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.2.3 = STRING: "Survivor Space"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.2.4 = STRING: "Tenured Gen"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.2.5 = STRING: "Perm Gen"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.2.6 = STRING: "Perm Gen [shared-ro]"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.2.7 = STRING: "Perm Gen [shared-rw]"
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.3.1 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.3.2 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.3.3 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.3.4 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.3.5 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.3.6 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.3.7 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.4.1 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.4.2 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.4.3 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.4.4 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.4.5 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.4.6 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.4.7 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.112.1 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.112.2 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.112.3 = INTEGER: 1
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.112.4 = INTEGER: 2
.1.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.112.5 = INTEGER: 2
...
なお、-v で指定する SNMP バージョンは 2c を指定する必要がある。
1 だと、heap 値を取得できない。

参考情報

Sun から提供されている以下のドキュメントを参考。

背景 - ロック手法について

データベース更新時のロックの方法として、次の2種類が存在する。
  • 悲観的ロック (Pessimistic lock)
  • 楽観的ロック (Optimistic lock)
悲観的ロックは、更新対象を更新完了までロックし、他の人が更新を行ったり、場合によっては参照も禁止する方法。
データをより安全に更新・参照できるメリットがあるが、ロック待ちによるアプリケーションの性能低下要因となりやすい。

楽観的ロックは「自分が操作している情報は他人が同時に更新する可能性が低い」更新を行うのに向いたロック方法。
更新対象を本当に更新する段階までぎりぎりまでロックしない。

例えば、「一旦データを取得したあと、内容を変更して実際に変更しようとしたら、データ取得後に誰かが同じレコードを更新していた」というケースの場合、悲観的ロックであれば「データ取得時に行ロックを行い、誰も更新できない状況」を作ってから更新を行う。その間、他の更新処理は自分の処理が完了するまで待ち状態となる。
一方、楽観的ロックであれば、自分がデータ取得後に他人がデータを更新できてしまう。自分がその後に更新しようとすると JPA 利用時、 OptimisticLockException が発生する。

概要

JPA(Java Persistence API) 1.0では楽観的ロックのみサポートしている。2.0 からは悲観的ロックもサポートするらしい。

Seasar2 + Hibernate + JPA 1.0 の構成において悲観的ロックでデータを更新したいと思い試行錯誤したが、意外とつまづいた。
EntityManager に lock() というメソッドがあって、write lock もできそうだが、うまくいかない。seasar がサポートしていないのか、設定がうまくいっていないのか...

結局、以下の方法で実現できた。

悲観的ロックによる更新方法

次のようなコードになる。
package jp.trasis.sample.service;

import javax.persistence.EntityManager;

import jp.trasis.sample.entity.User;

import org.seasar.extension.tx.annotation.RequiresNewTx;
import org.seasar.framework.container.annotation.tiger.Binding;

public class TestService {
	@Binding
	private EntityManager entityManager;
	
	@RequiresNewTx
	public void test(Long userId, String name) {
		User user = (User) entityManager.createNativeQuery(
				"select * from User_ where id=:userId for update", User.class)
			.setParameter("userId", userId)
			.getSingleResult();
		
		user.setName(name);

		entityManager.persist(user);
	}
}

解説

@RequiresNewTx アノテーションにより、新たなトランザクションの中で更新処理を行う。

まず、更新対象のエンティティを select ... for update により、行ロックを行いながら取得する。
この SQL は createNativeQuery() メソッドで実行する必要がある。crateQuery() で使用する HQL では、 for update 文は利用できない。

そしてエンティティに対して修正を行い、 persist() でコミットを行う。メソッドを抜けるときに実際に DB へのコミット処理が行われ、トランザクションが終了し、ロックも開放される。

概要

Eclispe 上で JavaScript(.js) ファイルに日本語を使うと、保存できなかったり、再度開くと文字化けしたりする。
こちらのブログ記事 に、その対策方法が記載されている。

このブログによると、Eclipse に次の設定を行うことで解決できる。

動作確認環境

項目内容
Eclipse バージョン3.3.2
3.4.1

設定手順

エクスプローラで、次のファイルを編集する。
workspace\.metadata\.plugins\org.eclipse.core.runtime\.settings\org.eclipse.core.runtime.prefs

次の内容を記述して Eclipse を再起動。
content-types/org.eclipse.wst.javascript.core.javascriptsource/charset=

プロジェクトごとに設定できるか?

複数人で開発しているときは、全員にこの設定をしてもらわないといけないし、Eclipse を新たなPCにインストールしたときも、この設定を行わないといけないので面倒...

ということで、プロジェクトにこの設定を行うことができるかどうかを試した。
結論は、残念ながらできない

プロジェクト直下にある .setting フォルダに org.eclipse.core.runtime.prefs ファイルを作成し、上記の記述を行ってみたが、反映されなかった。

Eclipse Pleiades All-in-one で対応してくれたらいいな...

Log4JのDTD

| | コメント(0) | トラックバック(0)

概要

Log4J の設定ファイルを log4j.properties ではなく log4j.xml で記述する際、Eclipse で編集中、文法をチェックしてもらえたら便利だ。

Log4J の DTD は jar ファイル(例: log4j-1.2.14.jar) の中のorg.apache.log4j.xml の中に log4j.dtd の名前で存在している。
が、わざわざ Eclipse にこの DTD を登録したり、Log4J に新しい構文が追加されたときに設定しなおすのでは不便。

本家の web サイト上で log4j.dtd を公開してくれるのが最も良い解決法だと思うが、探しても見つからない。

解決法

log4j.xml で、次のように DOCTYPE を記述する。

概要

今回は Maven + Spring Framework + Hibernate + JPA + JUnit の組み合わせで環境構築。

開発環境

項目
OS Windows XP Pro SP2
IDE 環境 Eclipse 3.3.2(Preiades All in One)
Java JDK 6 update 6
DB PostgreSQL 8.3

準備

次の手順までHibernateを使ったシンプルなJPA環境構築と同じように進める。
  • Maven プロジェクト作成
  • Java コンパイラー準拠レベルの設定
  • pom.xml を設定する
  • 設定ファイル作成
    • META-INF/persistence.xml
    • ehcache.xml
    • log4j.properties
  • BaseEntity, User クラス作成

pom.xml 修正

次のように修正する。

src/main/resources ソース・フォルダ

META-INF/persistence.xml

次のように修正する。 Maven を使う場合、エンティティクラスは src/main/java に、META-INF/persistence.xml ファイルなどは src/main/resources フォルダに置く。
この環境で Spring Framework + Hibernate にうまくクラスのアノテーションをスキャンしてもらうために、次のように記述している。

applicationContext.xml

Spring Framework の設定。
Spring Framework 2.5 からサポートされたいくつかの記述により、前のバージョンよりも記述が簡単になっている。

context:property-placeholder によって、jdbc.properties を読み込んでいる。
${jdbc.url} のように applicationContext.xml 内に記述すると置換される。

context:annotation-config によって、クラスをスキャンし、@Service などのアノテーションを解釈する。
<context:component-scan base-package="test.test" /> によってスキャンするパッケージを明示的に指定している。

tx:annotation-driven によって、@Transactional アノテーションを解釈する。

jdbc.properties

新たに作成。applicationContext.xml の中で ${jdbc.driverClassName} などが置換される。

Dao 作成

src/main/java フォルダの中で test.test.dao.UserDao クラスを作成する。
@Service アノテーションを指定すると、Spring Framework のビーンとして解釈され、他のビーンから @Resource アノテーションなどによって呼び出せる。

@Transactional アノテーションによって、このクラスのメソッド呼び出し時に、トランザクションがまだオープンされていなければ、自動的にオープンする。

JUnit4 によるテスト

テストケースの作成

src/test/java フォルダの中で test.test.DBTest を作成する。

Java Persistence API(JPA) とは

Java Persistence API(JPA) は Java の O/R マッピング標準 API。
概要は (日経BPの記事)が分かりやすい。
メリットは
  • オブジェクト指向で DB 開発がサクサクできる。
  • テーブルや制約などを自動的に作成できる。
  • オブジェクト・キャッシュの仕組みなどによって DB アクセスの負荷軽減・高速化が期待できる。
という点。デメリット(注意点)は
  • DB や SQL に関する知識が不要になるわけではない。
    上手に DB 設計しないとメモリを馬鹿食いしたり、逆に性能低下で悩まされやすい。
    エンティティ設計者は、どういうテーブル・制約・SQLが生成されるかを意識する必要がある。
  • ということで、開発者は既存の DB や SQL に加え、JPA に関する知識の習得を要求される。
といったところ。

デメリットに関しては一人、熟練技術者がいれば解決し、メリットだけを享受できるので、ぜひ「一家に一台」そういう技術者が欲しいところ。

概要

JPA 実装として代表的なのが Hibernate。最近だと EclipseLink(TopLink) も活気が出てきている。

実際の開発では Spring FrameworkSeasar などの DI コンテナとともに使われることが多い Hibernate。
今回は Hibernate だけでシンプルな JPA 環境を構築する。

開発環境

項目
OS Windows XP Pro SP2
IDE 環境 Eclipse 3.3.2(Preiades All in One)
Java JDK 6 update 6
DB PostgreSQL 8.3

まずは環境構築

Maven プロジェクト作成

まずは Eclipse 上で Maven プロジェクトを作成する。
  • [ファイル] メニュー ⇒ 新規 ⇒ プロジェクト
  • Maven ⇒ Maven Project。「New Manve Project」ダイアログが開く。
  • [次へ] ボタンを押す。
  • archetype 選択画面でそのまま [次へ] ボタンを押す。(maven-archetype-quickstart を選択)
  • グループId とアーティファクトID に test と入力して [終了]ボタンを押す。すると、 test プロジェクトが作成される。

Java コンパイラー準拠レベルの設定

  • Eclipse 上でプロジェクト名 test を右クリック ⇒ プロパティ を選択。
  • 左側のメニューから「Java コンパイラー」を選択。
  • 「プロジェクト固有の設定を可能にする」にチェックを入れる。
  • 「コンパイラー準拠レベル」を「6.0」に変更して [OK] ボタンを押す。
  • 「プロジェクトをビルドしますか?」と聞かれるので、「はい」を押す。

pom.xml を設定する

プロジェクト・トップに作成された pom.xml を編集し、利用するソフトウェアを自動ダウンロードする。
解説
Hibernate を使った JPA 環境を構築するには Hibernate EntityManager を利用するので、まず hibernate-entitymanager が必要。
Hibernate のログ出力環境として commons-logging と log4j も指定。
DB に PostgreSQL を使うので postgresql を指定。

作成するエンティティの中で ToStringBuilder を使用するので、 commons-lang を指定。

設定ファイル作成

まず、maven のルールに従い、リソースファイル置き場 src/main/resources フォルダを作成する。
  • プロジェクト test の上で右クリック、[新規] ⇒ [ソース・フォルダー] を選択する。
  • フォルダー名に「src/main/resources」と入力して [終了] ボタンを押す。
作成するファイルは次の3つ。
  • META-INF/persistence.xml
  • ehcache.xml
  • log4j.properties

META-INF/persistence.xml

persistence.xml は JPA の設定ファイル。

src/main/resources フォルダの中に META-INF フォルダを作成。その中に作成する。
いくつか設定項目いついて解説。詳しい設定項目についてはこちら
hibernate.cache.provider_class で二次キャッシュ機能を提供してくれるクラスを指定する。

hibernate.cache.use_query_cache はクエリキャッシュ機能を有効にするかどうか。
キャッシュしたくないクエリに対しては次のように設定する。
Query query = entityManager.createNativeQuery("...", User.class);

query.setHint("org.hibernate.cacheable", Boolean.FALSE);
または
((HibernateQuery) query).getHibernateQuery().setCacheable(false);

hibernate.cache.use_second_level_cache で、二次キャッシュ機能を有効(true)に設定している。
バッチ処理などで大量の insert や update を行う場合(Bulk Insert/Bulk Update)は、ここを false にしておいた方が、余計なキャッシュ処理でメモリや CPU リソースを消費せずに済むらしい。

hibernate.hbm2ddl.auto は、実行時にテーブルなどの作成を行うかどうか。開発中は update、運用時は none、開発環境で作り直すときは create-drop を指定するとよい。
  • update を指定すると、テーブルなどが存在しなければ自動的に作成。
  • create-drop を指定すると、削除して再作成する。
  • none を指定すると、何もしない。

hibernate.show_sql は、実行された SQL をログに出力するかどうか。開発時にチューニングを行う時に true、それ以外は false を指定するといい。

ehcache.xml

src/main/resources フォルダの中に作成する。

log4j.properties

src/main/resources フォルダの中に作成する。

エンティティ作成

ここまでで環境構築は終了。次にエンティティを作成する。

BaseEntity クラス作成

まずは、全てのエンティティのベースとなる BaseEntity を作成する。
src/main/java フォルダの中で test.test.entity パッケージを作成し、その中に BaseEntity クラスを作成する。

ついでに自動的に作成された App クラスは不要なので、消しておく。

User クラス作成

続いて、User エンティティを作成する。

@Entity アノテーションを設定すると、JPA が自動的に「このクラスはエンティティである」と判断してくれる。
テーブル名は User_ とする。
mailAddress, hashedPassword フィールドを持つ。BaseEntity を継承しているので、id, createTimestamp, updateTimestamp も持つ。
mailAddress にはユニークキーを @UniqueConstraint で設定している。
@Column(nullable = false) で mailAddress, hashedPassword フィールドは null のまま DB に追加できないよう設定している。

利用プログラム作成

insert - エンティティ追加

まずはデータの追加を行うプログラム。
src/test/java フォルダの test.test パッケージの中に InsertSample クラスを作成する。
DB に対して insert や update などを行うときはトランザクションの中で行う。

select - ネイティブクエリによる検索

次は SelectSample1 クラスを作成。

select - HSQL による検索

次の SelectSample2 クラスでは HSQL による検索を行っている。

update - データ更新

次の UpdateSample1 クラスではオブジェクトの値を書き換えることで更新を行っている。

update - ネイティブクエリによる更新

次の UpdateSample2 クラスではネイティブクエリによる更新処理を行っている。
生成される SQL を見ると分かるように、自分でネイティブクエリを書いたほうが最適な更新処理を行える。
ただし、1つの EntityManager セッションの中でキャッシュされているオブジェクトの値とずれが生じている点に注意。

delete - エンティティの削除

次の DeleteSample ではエンティティを削除している。

参考文献

概要

プログラムからディスクやメモリ、ネットワーク上にテキストデータを書き出す際、明示的・暗黙的に文字コードが指定されている。
文字コードの種類によって、どのくらい性能差があるのかを測定した。

結論

ASCII 文字列のみの出力の場合、us-ascii, iso-8859-1 での出力が最も高速、UTF-8 もほぼ変わらない性能で出力できる。
日本語文字列の場合、UTF-8 が高速。

MS932, EUC-JP はいずれのケースでも、他の文字コードより性能が劣る。とくに EUC-JP はこれらの中で最も遅い。

調査1 - Windows で ASCII 文字列を出力

調査方法

次のプログラムのように、ascii 文字列を繰り返し、ByteArrayOutputStream に書き出し、実行時間を測定する。
public class Test1_2 {
	public static void main(String[] args) throws IOException {
		String s = "The quick brown fox jumps over the lazy dog";

		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < 100000; i++) {
			sb.append(s);
		}

		Charset charset = Charset.forName("iso-8859-1");

		long t = System.currentTimeMillis();

		for (int i = 0; i < 20000 * 80; i++) {
			doTest(s, charset);
		}

		long t2 = System.currentTimeMillis();
		System.out.println((t2 - t) + " msec");
	}

	private static void doTest(String s, Charset charset) throws IOException {
		OutputStreamWriter out = new OutputStreamWriter(
				new ByteArrayOutputStream(), charset);
		out.write(s);
		out.close();
	}
}

実行環境

  • PC: DELL Vostro 200
  • CPU: Core2Duo E8500 3.16GHz
  • OS: Windows XP Pro SP2

  • Java 6 update 6

測定結果

us-ascii, iso-8859-1, UTF-8, MS932, EUC-JP の順に高速だった。
ASCII 文字列の出力であるにもかかわらず、MS932, EUC-JP での出力は遅い。

なお、文字コードをしていない場合、Windows 環境のデフォルト値 MS932 が使用される。
Windows の場合、明示的に us-ascii や UTF-8 を指定したほうが高速化できる。

実行時間[msec] 文字コード
4374 us-ascii
4375 iso-8859-1
4531 UTF-8
5406 MS932
6954 EUC-JP

調査2 - Windows で 日本語文字列を出力

調査方法

調査1のプログラムにおいて、文字列を日本語に変更し、実行。
日本語出力のため、調査文字コードは UTF-8, MS932, EUC-JP とする。

測定結果

UTF-8 が最も高速に実行できた。Java 内部コードと同じ文字コードだけに、余計な手間がかかっていないのだろう。

実行時間[msec] 文字コード
4829 UTF-8
6359 MS932
7500 EUC-JP

調査3 - Linux で ASCII 文字列を出力

さきほどのプログラムを Linux 上で実行。

実行環境

  • PC: IBM Blade HS21 Model G6J
  • CPU: Xeon E5450 3GHz
  • OS: CentOS 5.2
  • kerne: 2.6.18-92.1.6.el5PAE
  • glibc: glibc-2.5-24

  • Java 6 update 6

測定結果

Widows と同様、us-ascii, iso-8859-1, UTF-8, MS932, EUC-JP の順に高速だった。

実行時間[msec] 文字コード
5960 us-ascii
5974 iso-8859-1
5999 UTF-8
6378 MS932
7503 EUC-JP

調査4 - Linux で 日本語文字列を出力

測定結果

Windows と同様の結果となった。

実行時間[msec] 文字コード
6093 UTF-8
6620 MS932
7503 EUC-JP

概要

通常ECサイトなどを構築する場合、VeriSign などの「信頼できる認証局」によって発行された証明書を使ってSSLサーバを構築する。

しかし、テスト環境などで自分で発行した証明書を使ったSSLサーバを構築した場合、ブラウザでアクセスしようとすると画面上に警告が表示されたりする。

同様に、Javaプログラムから接続しようとすると エラーが発生する。
証明書のチェックを行わないように設定することで、この問題を回避できる。

java.net.URL を使った方法

問題再現手順

次のように自分で発行した証明書のサーバに接続しようとすると、エラーが発生する。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

import javax.net.ssl.HttpsURLConnection;

public class Test1 {
    public static void main(String[] args) throws Exception {
        String href = "https://www.example.com/";
        String encoding = "UTF-8";

        URLConnection connection = new URL(href)
                .openConnection();
        HttpsURLConnection httpsconnection = (HttpsURLConnection) connection;

        int responseCode = httpsconnection.getResponseCode();
        System.out.println(responseCode);

        BufferedReader reader = new BufferedReader(new InputStreamReader(
                httpsconnection.getInputStream(), encoding));
        String buffer;
        while ((buffer = reader.readLine()) != null) {
            System.out.println(buffer);
        }
    }
}
発生するエラー
Caused by: sun.security.provider.certpath.SunCertPathBuilderException:
     unable to find valid certification path to requested target

エラー回避方法

SSL 証明書検証をせずに SSL サーバに接続するには、次のように修正する。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class Test2 {
    public static void main(String[] args) throws Exception {
        String href = "https://www.example.com/";
        String encoding = "UTF-8";

        URLConnection connection = new URL(href).openConnection();
        HttpsURLConnection httpsconnection = (HttpsURLConnection) connection;

        // SSL 証明書検証をしない。
        ignoreValidateCertification(httpsconnection);

        int responseCode = httpsconnection.getResponseCode();
        System.out.println(responseCode);

        BufferedReader reader = new BufferedReader(new InputStreamReader(
                httpsconnection.getInputStream(), encoding));
        String buffer;
        while ((buffer = reader.readLine()) != null) {
            System.out.println(buffer);
        }
    }

    private static void ignoreValidateCertification(
            HttpsURLConnection httpsconnection)
            throws NoSuchAlgorithmException, KeyManagementException {
        KeyManager[] km = null;
        TrustManager[] tm = { new X509TrustManager() {
            public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                    throws CertificateException {
            }

            public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                    throws CertificateException {
            }

            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
        } };
        SSLContext sslcontext = SSLContext.getInstance("SSL");
        sslcontext.init(km, tm, new SecureRandom());
        httpsconnection.setSSLSocketFactory(sslcontext.getSocketFactory());
    }

}

HttpClientを使ったSSLサーバへの接続

Jakarta Commons の HttpClient を使った場合も同様である。

エラー再現

次のように同じエラーが発生する。
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;

public class HttpClientTest1 {
    public static void main(String[] args) throws Exception {
        String href = "https://www.example.com/";

        HttpClient httpClient = new HttpClient();

        GetMethod method = new GetMethod(href);

        int retCode = httpClient.executeMethod(method);
        System.out.println(retCode);

        byte[] buf = method.getResponseBody();
        System.out.write(buf);
    }
}

エラー回避

次のような修正で回避できる。MySSLSocketFactory クラスを別途作成する必要がある。
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.protocol.Protocol;

/**
 * テスト2: 証明書チェックを行わない。
 *
 */
public class HttpClientTest2 {

    private static void initHttpClient() {
        // 証明書チェックを行わない。
        Protocol.registerProtocol("https", new Protocol("https",
                new MySSLSocketFactory(), 443));
    }

    public static void main(String[] args) throws Exception {

        // HttpClient の初期設定。
        initHttpClient();

        String href = "https://www.example.com/";

        HttpClient httpClient = new HttpClient();

        GetMethod method = new GetMethod(href);

        int retCode = httpClient.executeMethod(method);
        System.out.println(retCode);

        byte[] buf = method.getResponseBody();
        System.out.write(buf);
    }
}

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.httpclient.ConnectTimeoutException;
import org.apache.commons.httpclient.HttpClientError;
import org.apache.commons.httpclient.params.HttpConnectionParams;
import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;

public class MySSLSocketFactory implements SecureProtocolSocketFactory {
    private SSLContext sslcontext = null;

    private static SSLContext createEasySSLContext() {
        try {
            SSLContext context = SSLContext.getInstance("SSL");
            context.init(null, new TrustManager[] { new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] arg0,
                        String arg1) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] arg0,
                        String arg1) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            } }, null);
            return context;
        } catch (Exception e) {
            throw new HttpClientError(e.toString());
        }
    }

    private SSLContext getSSLContext() {
        if (this.sslcontext == null) {
            this.sslcontext = createEasySSLContext();
        }
        return this.sslcontext;
    }

    @Override
    public Socket createSocket(Socket socket, String host, int port,
            boolean autoClose) throws IOException, UnknownHostException {
        return getSSLContext().getSocketFactory().createSocket(socket, host,
                port, autoClose);
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException,
            UnknownHostException {
        return getSSLContext().getSocketFactory().createSocket(host, port);
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress clientHost,
            int clientPort) throws IOException, UnknownHostException {
        return getSSLContext().getSocketFactory().createSocket(host, port,
                clientHost, clientPort);
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localAddress,
            int localPort, HttpConnectionParams params) throws IOException,
            UnknownHostException, ConnectTimeoutException {
        if (params == null) {
            throw new IllegalArgumentException("Parameters may not be null");
        }
        int timeout = params.getConnectionTimeout();
        SocketFactory socketfactory = getSSLContext().getSocketFactory();
        if (timeout == 0) {
            return socketfactory.createSocket(host, port, localAddress,
                    localPort);
        } else {
            Socket socket = socketfactory.createSocket();
            SocketAddress localaddr = new InetSocketAddress(localAddress,
                    localPort);
            SocketAddress remoteaddr = new InetSocketAddress(host, port);
            socket.bind(localaddr);
            socket.connect(remoteaddr, timeout);
            return socket;
        }
    }

}

2012年4月

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30