ping コマンドで送信元IPアドレスを指定する方法

ホストに複数のインタフェースがあって複数の IPアドレス が付いていたりすると、ICMP パケットの送信元アドレスに意図しない IPアドレス が選ばれて困ったりします。そういう場合は ping コマンドにオプションを付けて対処しましょう。

Linux 系の場合

 $ ping [送信先IPアドレス] -I [送信元IPアドレス]

-I オプションの引数には IP アドレス以外にもインタフェースを指定できるようです。

BSD 系 (OSX 含む) の場合

 $ ping [送信先IPアドレス] -S [送信元IPアドレス]

Windows の場合

ないみたいです。あきらめて別の OS を仮想マシンにでもインストールしましょう。

おわりに

ping コマンドの 送信元IPアドレス はオプションで指定できます。ただしオプションは OS によって異なります。
今回 Linux 系とか BSD 系とかひとまとめにしちゃってますが、ディストリビューションによっては上記とは限らないので注意が必要です。

ANTLRv3 を Maven から使う

ANTLRJava で書かれたパーサジェネレータです。文法を定義したファイル (.g) からパーサのコードを自動生成してくれます。今回は ANTLR を antlr3-maven-plugin で Maven から使ってみます。

サンプル

Maven の設定ファイル (pom.xml) です。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>study.maven.antlr.plugin</groupId>
	<artifactId>study-maven-antlr-plugin</artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>study-antlr-helloworld</name>
	<url>http://maven.apache.org</url>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.antlr</groupId>
				<artifactId>antlr3-maven-plugin</artifactId>
				<version>3.4</version>
				<executions>
					<execution>
						<id>antlr3</id>
						<goals>
							<goal>antlr</goal>
						</goals>
						<phase>generate-sources</phase>
						<configuration>
							<outputDirectory>src/main/java</outputDirectory>
						</configuration>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.2.1</version>
				<configuration>
					<mainClass>study.maven.antlr.plugin.Main</mainClass>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.antlr</groupId>
			<artifactId>antlr-runtime</artifactId>
			<version>3.4</version>
		</dependency>
	</dependencies>
</project>

antlr3-maven-plugin の antlr3:antlr ゴールを Maven の generate-sources フェーズで実行するようにしています。これでソースをコンパイルする前にパーサが生成されます。また タグでパーサを出力するディレクトリを指定しています。 にはパーサのコードを解釈するために antlr-runtime が必要です。

続いて文法を定義したファイル (Hello.g) です。

grammar Hello;

@header { 
	package study.maven.antlr.plugin;
}

@lexer::header {
	package study.maven.antlr.plugin;
}

greet
	:	'Hello' COMMA 'World';

COMMA
	:	',';

このファイルを ${basedir}/src/main/antlr3/study/maven/antlr/plugin/Hello.g に配置します。antlr3-maven-plugin はデフォルトで ${basedir}/src/main/antlr3 ディレクトリ以下を読みます。

この状態でコンパイルするとパーサのコードが生成されます。

 $ mvn clean compile

ソースディレクトリに HelloLexer.java, HelloParser.java ができているはずです。

最後に、パーサを使う実行クラス (Main.java) です。

package study.maven.antlr.plugin;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;

public class Main {

	public static void main(String[] args) throws Exception {
		CharStream cs = new ANTLRStringStream(args[0]);
		HelloLexer lexer = new HelloLexer(cs);
		CommonTokenStream tokens = new CommonTokenStream();
		tokens.setTokenSource(lexer);
		HelloParser parser = new HelloParser(tokens);
		parser.greet();
		int errors = parser.getNumberOfSyntaxErrors();
		if (errors > 0) {
			System.out.println("ERROR!");
		} else {
			System.out.println("SUCCESS!");			
		}
	}
}

HelloLexer, HelloParser を使って標準入力をパースしています。

パースに成功した場合

$ mvn exec:java -Dexec.args="Hello,World"
(…省略…)
SUCCESS!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.669s
[INFO] Finished at: Tue Feb 21 23:06:46 JST 2012
[INFO] Final Memory: 3M/81M
[INFO] ------------------------------------------------------------------------

パースに失敗した場合

$ mvn exec:java -Dexec.args="Bye,World"  
(…省略…)
line 1:0 no viable alternative at character 'B'
line 1:1 no viable alternative at character 'y'
line 1:2 no viable alternative at character 'e'
line 1:3 missing 'Hello' at ','
ERROR!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.658s
[INFO] Finished at: Tue Feb 21 23:08:02 JST 2012
[INFO] Final Memory: 3M/81M
[INFO] ------------------------------------------------------------------------

まとめ

antlr3-maven-plugin を使うことでプログラムに ANTLR を簡単に組み込めました。

Jersey (JAX-RS) のアクセス制御

今回は Jersey (JAX-RS) で作った WebAPI でアクセス制御を行う方法について書きます。

JAX-RSJ2EE の仕様である以上、アクセス制御をロール単位で行う点に変わりはありません。とはいえ、アプローチの方法は複数あります。主に 3 つです。

1. web.xml (など) で設定する
2. アクセス制御用のフィルタを使う
3. SecurityContext を元に自分でロジックを書く

上記 3 つやり方についてサンプルを元に解説します。

サンプル

まずは web.xml です。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	<display-name>study-jersey-auth</display-name>
	<session-config>
		<session-timeout>30</session-timeout>
	</session-config>
	
	<servlet>
		<servlet-name>Jersey Auth Test Servlet</servlet-name>
		<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
		<init-param>
			<param-name>com.sun.jersey.config.property.packages</param-name>
			<param-value>study.jersey.auth.resource</param-value>
		</init-param>
		<init-param>
			<param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
			<param-value>com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>Jersey Auth Test Servlet</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
	
	<security-constraint>
		<web-resource-collection>
			<web-resource-name>Sample WebAPI</web-resource-name>
			<url-pattern>/*</url-pattern>
		</web-resource-collection>
		<auth-constraint>
			<role-name>AUTH_USER</role-name>
		</auth-constraint>
	</security-constraint>

	<login-config>
		<auth-method>BASIC</auth-method>
		<realm-name>BASIC Auth Realm</realm-name>
	</login-config>

	<security-role>
		<role-name>AUTH_USER</role-name>
	</security-role>
</web-app>

後ろの方の タグ以下が (1) のアクセス制御の設定です。これはサーブレットの開発で一般的なアクセス制御のやり方なので、ここで詳しく説明する必要もないと思います。コンテキストパス以下全てで、アクセスするのに AUTH_USER ロールが必要な設定にしています。

サーブレット "com.sun.jersey.spi.container.ResourceFilters" で指定されている "com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory" が (2) のやり方を実現するためのフィルタです。

サンプルのリソースです。

package study.jersey.auth.resource;

import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;

@Path("/test")
public class AuthTestResource {
	
	@Context
	private SecurityContext securityContext;

	// 実行に "TEST_ROLE" ロールが必要
	@RolesAllowed({"TEST_ROLE"})
	@Path("/rolesallowed")
	@GET
	public Response needAuth() {
		return Response.status(200).entity("rolesallowed").build();
	}

	// 実行に別途特別なロールは不要
	@PermitAll
	@Path("/permitall")
	@GET
	public Response noGuard() {
		return Response.status(200).entity("permitall").build();
	}
	
	@Path("/manual")
	@GET
	public Response manual() {
		// Inject された SecurityContext で判断する
		if (!this.securityContext.isUserInRole("TEST_ROLE")) {
			return Response.status(403).build();
		}
		return Response.status(200).entity("manual").build();
	}
}

(2) のやり方では、リソースやメソッドをアノテーションで修飾することで、アクセス制御を行います。needAuth() メソッドを修飾している @RolesAllowed アノテーションには、この API の実行に必要なロールを列挙します。noGuard() メソッドを修飾している @PermitAll アノテーションはロールが不要なことを示しますが、(1) のやり方で設定したロールは必要とされる点に注意してください。アノテーションは前述したフィルタが読み取って処理します。

manual() メソッドでは (3) のやり方をとっています。JAX-RS の仕様ではアクセス制御に関わるデータを保持するインスタンスとして SecurityContext が定義されています。(3) のやり方では、それを元に自分で制御するロジックを書きます。

今回、プロジェクトは Maven で作って、サーブレットコンテナは Jetty を使いました。Jetty には Maven 用のプラグインがあるので、インストールなどの手間がいらなくて便利です。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>study.jersey.auth</groupId>
	<artifactId>study-jersey-auth</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>

	<name>study-jersey-auth</name>
	<url>http://maven.apache.org</url>

	<dependencies>
		<dependency>
			<groupId>javax.annotation</groupId>
			<artifactId>jsr250-api</artifactId>
			<version>1.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-server</artifactId>
			<version>1.11</version>
		</dependency>
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-servlet</artifactId>
			<version>1.11</version>
		</dependency>
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-json</artifactId>
			<version>1.11</version>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.10</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-client</artifactId>
			<version>1.11</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.mortbay.jetty</groupId>
				<artifactId>jetty-maven-plugin</artifactId>
				<version>8.1.0.v20120127</version>
				<configuration>
					<jettyXml>src/test/resources/jetty.xml</jettyXml>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Jetty は mvn jetty:run で起動できます。

今回、認証を使うので別途 Jetty の設定が必要でした。まずは jetty.xml です。

<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">

<Configure id="Server" class="org.eclipse.jetty.server.Server">

    <Call name="addBean">
      <Arg>
        <New class="org.eclipse.jetty.security.HashLoginService">
          <Set name="name">BASIC Auth Realm</Set>
          <Set name="config">src/test/resources/realm.properties</Set>
          <Set name="refreshInterval">0</Set>
        </New>
      </Arg>
    </Call>

</Configure>

レルム (ユーザ名、パスワード、ロール) を記述している realm.properties です。

bothowner:testpassword,AUTH_USER,TEST_ROLE
authonly:testpassword,AUTH_USER
testonly:testpassword,TEST_ROLE

設定ファイルを設置したら、サーブレットが正しく起動できることを確認した上で、ブラウザなどから各レルムを入力した際にそれぞれの API がどう実行されるかを確認します。手動でやるのも良いですが、説明が面倒ですし、ここは Jersey Client を使って単体テストを書いて確認してしまいましょう。

package study.jersey.auth.resource;

import org.junit.Test;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
import junit.framework.Assert;

public class AuthTestResourceTest {
	
	// BASIC認証に対応したHTTPクライアントを取得する
	private static Client getBasicAuthClient(String username, String password) {
		ClientConfig config = new DefaultClientConfig();
		Client client = Client.create(config);
		client.addFilter(new HTTPBasicAuthFilter(username, password));
		return client;
	}
	
	// BASIC認証に対応したHTTPクライアントを使ってリソースを取得する
	private static WebResource getBasicAuthWebResource(String uri, String username, String password) {
		Client client = getBasicAuthClient(username, password);
		WebResource resource = client.resource(uri);
		return resource;
	}
	
	@Test
	public void testRolesAllowedBothOwner() {
		String uri = "http://localhost:8080/test/rolesallowed";
		String username = "bothowner";
		String password = "testpassword";
		WebResource resource = getBasicAuthWebResource(uri, username, password);
		ClientResponse response = resource.get(ClientResponse.class);
		// ロールとして AUTH_USER, TEST_ROLE 両方があるので OK
		Assert.assertEquals(response.getStatus(), 200);
	}
	
	@Test
	public void testRolesAllowedAuthOnly() {
		String uri = "http://localhost:8080/test/rolesallowed";
		String username = "authonly";
		String password = "testpassword";
		WebResource resource = getBasicAuthWebResource(uri, username, password);
		ClientResponse response = resource.get(ClientResponse.class);
		// ロールとして AUTH_USER はあるが TEST_ROLE がないので NG
		Assert.assertEquals(response.getStatus(), 403);
	}
	
	@Test
	public void testRolesAllowedTestOnly() {
		String uri = "http://localhost:8080/test/rolesallowed";
		String username = "testonly";
		String password = "testpassword";
		WebResource resource = getBasicAuthWebResource(uri, username, password);
		ClientResponse response = resource.get(ClientResponse.class);
		// ロールとして TEST_ROLE はあるが AUTH_USER がないので NG
		Assert.assertEquals(response.getStatus(), 403);
	}
	
	
	@Test
	public void testPermitAllBothOwner() {
		String uri = "http://localhost:8080/test/permitall";
		String username = "bothowner";
		String password = "testpassword";
		WebResource resource = getBasicAuthWebResource(uri, username, password);
		ClientResponse response = resource.get(ClientResponse.class);
		// AUTH_USER をもっているので OK
		Assert.assertEquals(response.getStatus(), 200);
	}

	@Test
	public void testPermitAllAuthOnly() {
		String uri = "http://localhost:8080/test/permitall";
		String username = "authonly";
		String password = "testpassword";
		WebResource resource = getBasicAuthWebResource(uri, username, password);
		ClientResponse response = resource.get(ClientResponse.class);
		// AUTH_USER しかもっていないユーザでも実行できる
		Assert.assertEquals(response.getStatus(), 200);
	}
	
	@Test
	public void testPermitAllTestOnly() {
		String uri = "http://localhost:8080/test/permitall";
		String username = "testonly";
		String password = "testpassword";
		WebResource resource = getBasicAuthWebResource(uri, username, password);
		ClientResponse response = resource.get(ClientResponse.class);
		// TEST_ROLE しかもっていないユーザには実行できない
		Assert.assertEquals(response.getStatus(), 403);
	}
	
	@Test
	public void testManualBothOwner() {
		String uri = "http://localhost:8080/test/manual";
		String username = "bothowner";
		String password = "testpassword";
		WebResource resource = getBasicAuthWebResource(uri, username, password);
		ClientResponse response = resource.get(ClientResponse.class);
		// やっていることは /test/rolesallowed と同じなので両方のロールが必要
		Assert.assertEquals(response.getStatus(), 200);
	}
	
}

BASIC認証を使う場合は、Client に HTTPBasicAuthFilter フィルタを追加すれば OK です。テストを実行する際には、サーブレットを起動しておくことをお忘れなく。

まとめ

Jersey (JAX-RS) で作った WebAPI にアクセス制御がかけられるようになりました。

(1) のやり方で、web.xml にパスとメソッドと必要なロールをだーっと書いていくこともできますが、リソースとの食い違いが起きやすそうですし、何より面倒です。このやり方は、全体で共通のロールを必要とさせる時など、大きな単位で制御するときに使うべきだと思います。

(2) のやり方では、リソースのすぐ近くにアクセス制御のコードを置ける上に、ビジネスロジックとは分離できるので、コードの見通しが良いと思います。通常はこのやり方を取るのがおすすめです。フィルタは JAX-RS 1.1 の仕様に含まれていないため、今回のコードは Jersey 以外の実装では動きませんが、同様のものは他の実装でも恐らく作れると思います。

大抵は (2) で事足りますが、どうしてもできないような複雑なパターンもたまにあるので、そのときは (3) のやり方を取ると良いと思います。とはいえ、そんな複雑な体系は不具合の元なので、設計に問題がある可能性を疑った方が良いかもしれないですが。このやり方だとビジネスロジックの中にアクセス制御のロジックが混ざって、コードの見通しが悪くなりがちです。

お財布を落としたとき、必要以上に困らないようやっておくべき 3 つのこと

去年、酔っぱらったあげくに現金にキャッシュカード、保険証など諸々一式入ったお財布を外で落として途方に暮れました。本当に色々と困ったので、その時の経験をもとに、あらかじめやっておいた方が良いことについて書いておきます。

お財布以外で現金またはそれに代わるものを持ち歩く (モバイルSuica が便利)

落としたとき、現金は全てお財布に入っていましたし、定期券も同じでした。それでも電車に乗って家に帰れたのは、携帯で モバイルSuica を使っていたからです。モバイルSuica のおかげで、コンビニで買い物もできるので食べ物なんかにも困りませんでした。とりあえず、お財布以外で現金またはそれに代わるものを持ち歩いた方が良いです。

最低二日間は生活できるだけの現金を自宅に保管しておく

落とした日は、運悪く週末でした。もし通帳と銀行印を落としていなくても、週末だと銀行の窓口でお金をおろすことができません。ぼくの場合、手元に現金がほとんどなかったので、もはや家に引きこもるしかありませんでした…。最低でも、週末を過ごせる程度の現金は自宅に常備しておいた方が良いです。

本人確認できる書類を自宅に保管しておく (住基カードがおすすめ)

これが一番困った気がします。落としたお財布には、運転免許証や保険証など公に本人確認できる書類が一通り入っていました。一旦全ての書類を失うと、本人確認するための書類を再発行するのに本人確認するための書類が必要というジレンマに陥ります…。例えば運転免許証の再発行には顔写真と住所の確認ができる書類が必要です。このジレンマに陥らないためには、普段持ち歩かない本人確認できる書類を作っておくべきです。ぼくのおすすめは住民基本台帳カードです。普段持ち歩く必要がないですし、顔写真と住所の入ったものが市役所で作れます。タバコを吸わないのであれば TASPO を作るのも良いみたいです。

まとめ

お財布は落とさないに越したことはないですが、万が一落としたときにも必要以上に困らないよう、あらかじめ準備しておきましょう。お酒は、楽しく美味しく、節度を守って!

Google Guice で JPA のトランザクションを管理する

トランザクション管理って面倒ですよね。JPA を使うときを考えると、まず EntityManagerFactory を作って、EntityManager を取得して、EntityTransaction を取得して、開始して、コミットして、例外が上がったらロールバックして…。うーん、継承などで処理を親クラスにまとめるにしても、どうもイマイチな感じが拭えません。

EJB ならコンテナが管理してくれる?いや EJB はちょっと…。Spring の AOP はどうか?いや大量の XML に悩まされるのはちょっと…。そんな好き嫌いの激しいぼく的には、シンプルな DIコンテナ Google GuiceAOP を使ってトランザクション管理ができるとうれしいなー、と思いました。最初は自分でインターセプターを書こうと意気込んでいたんですが、何とモジュールが既にあったのでこれ幸いと使ってみました。

ソース

まずは Google Guice の設定に使うモジュールです。

package study.jpa.guice.transactional;

import com.google.inject.AbstractModule;
import com.google.inject.persist.jpa.JpaPersistModule;

public class JpaPersistDaoModule extends AbstractModule {

	@Override
	protected void configure() {
		JpaPersistModule jpaPersistModule = new JpaPersistModule("example-persist-unit");
		this.install(jpaPersistModule);
		this.bind(ProductDao.class).to(ProductDaoImpl.class);
	}
	
}

JpaPersistModule クラスがトランザクションの管理に必要な処理を織り込んでくれるモジュールです。コンストラクタには JPA の永続ユニット名を指定します。JpaPersistDaoModule クラスでは JpaPersistModule のインスタンスをインストールした上で、エンティティの永続化に使う DAO を登録します。

続いて Main です。

package study.jpa.guice.transactional;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Main {

	public static void main(String[] args) throws Exception {
		Injector injector = Guice.createInjector(new JpaPersistDaoModule());
		injector.getInstance(JpaPersistInitializer.class);
		ProductDao dao = injector.getInstance(ProductDaoImpl.class);
		dao.add("Ninjin", 98);
		dao.add("Daikon", 168);
	}
}

先ほど作ったモジュールを使って Injector クラスのインスタンスを生成します。次に JpaPersistInitializer クラスのインスタンスGuice 経由で取得していますが、これは必要な初期化を行うクラスです。初期化が完了した後に Guice 経由で DAO を取得して、データを追加しています。

先ほど登場した JpaPersistInitializer クラスです。

package study.jpa.guice.transactional;

import com.google.inject.Inject;
import com.google.inject.persist.PersistService;

public class JpaPersistInitializer {

	@Inject
	public JpaPersistInitializer(PersistService service) {
		service.start();
	}
}

このクラスではコンストラクタインジェクションで PersistService のインスタンスを受け取った上で start メソッドを呼んでいます。JpaPersistModule クラスを使ってトランザクション管理をする場合には、あらかじめ PersistService#start() を呼んでおく必要があります。

ProductDao クラスは単なるインタフェースです。

package study.jpa.guice.transactional;

public interface ProductDao {
	public Integer add(String name, Integer price);
}

ProductDaoImpl クラスに嬉しさが詰まっています。

package study.jpa.guice.transactional;

import com.google.inject.persist.Transactional;
import javax.inject.Inject;
import javax.persistence.EntityManager;

public class ProductDaoImpl implements ProductDao {

	@Inject
	private EntityManager em;

	@Transactional
	@Override
	public Integer add(String name, Integer price) {
		Product product = new Product();
		product.setName(name);
		product.setPrice(price);
		em.persist(product);
		return product.getId();
	}
}

EntityManager のインスタンスGuice が注入してくれます。更に、トランザクションについては、管理してほしいメソッドを @Transactional アノテーションで修飾するだけです。うーん、なんてシンプル!

あとは至って普通の JPA を使ったアプリケーションです。永続化するエンティティの Product クラス。

package study.jpa.guice.transactional;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Product implements Serializable {

	@Id
	@GeneratedValue
	protected Integer id;
	
	@Column(nullable = false, unique = true)
	protected String name;
	
	@Column(nullable = false)
	protected Integer price;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getPrice() {
		return price;
	}

	public void setPrice(Integer price) {
		this.price = price;
	}

}

永続ユニット (persistence.xml) です。JPA の実装には Hibernate を、DB には MySQL を使いました。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
	<persistence-unit name="example-persist-unit" transaction-type="RESOURCE_LOCAL">
		<provider>org.hibernate.ejb.HibernatePersistence</provider>
		<properties>
			<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/test"/>
			<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
			<property name="javax.persistence.jdbc.user" value="****"/>
			<property name="javax.persistence.jdbc.password" value="****"/>
			<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
			<property name="hibernate.hbm2ddl.auto" value="update" />
			<property name="hibernate.show_sql" value="true" />
			<property name="hibernate.format_sql" value="true" />
		</properties>
	</persistence-unit>
</persistence>

プロジェクトの管理は Maven です。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>study.guice.jpa.transactional</groupId>
	<artifactId>study-guice-jpa-transactional</artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>study-guice-jpa-transactional</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.2.1</version>
				<configuration>
					<mainClass>study.jpa.guice.transactional.Main</mainClass>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<!-- compile -->
		<dependency>
			<groupId>com.google.inject</groupId>
			<artifactId>guice</artifactId>
			<version>3.0</version>
		</dependency>
		<dependency>
			<groupId>com.google.inject.extensions</groupId>
			<artifactId>guice-persist</artifactId>
			<version>3.0</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>4.0.1.Final</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.18</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.0.0</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-core</artifactId>
			<version>1.0.0</version>
		</dependency>
	</dependencies>
</project>

実行

Main の実行結果です。MySQL は起動しておきます。

$ mvn clean compile exec:java
(…省略…)
Hibernate: 
    insert 
    into
        Product
        (name, price) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Product
        (name, price) 
    values
        (?, ?)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.952s
[INFO] Finished at: Thu Feb 09 23:09:07 JST 2012
[INFO] Final Memory: 11M/81M
[INFO] ------------------------------------------------------------------------

上手いこと SQL が発行されたみたいですね。

DB の確認

念のため MySQL で直接確認しておきます。

mysql> select * from Product;
+----+--------+-------+
| id | name   | price |
+----+--------+-------+
|  1 | Ninjin |    98 |
|  2 | Daikon |   168 |
+----+--------+-------+
2 rows in set (0.00 sec)

意図した通りに行が追加されていますね。

ロールバックの確認

例外が上がったときに正しくロールバックされるか確認します。確認方法としては、永続化に使ったエンティティの Product クラスの name フィールドに unique 制約を付けておいたので、もう一度同じように Maven を実行するだけです。もう一度実行すると、同じ name を持った行が追加されようとすることで PersistenceException が上がって失敗します。

ロールバックの動作は MySQL のログで確認します。ログの出し方はググってください。

                    3 Query     SET autocommit=0
                    3 Query     insert into Product (name, price) values ('Ninjin', 98)
                    3 Query     rollback

トランザクションの開始、行の挿入、ロールバックの挙動が確認できます。

まとめ

面倒だった JPAトランザクション管理も Google Guice を使って簡単に分かりやすくできました。

Jackson JSON Proceccor で JSON を扱う (データバインディング編)

JavaJSON を扱うのに Jackson JSON Processor が便利です。他の JSON を扱うライブラリと比較するとパフォーマンスに優れるそうですが、何より API がシンプルで使いやすいです。

Jackson で JSON を扱うには 3 つの方法があります。今回紹介するのは、パフォーマンスは多少落ちますが最もシンプルなやり方「データバインディング」です。データバインディングでは JSONスキーマJava のクラスが 1:1 に対応します。

早速サンプルコードです。

package jp.example.jackson.databinding;

import org.codehaus.jackson.map.ObjectMapper;

public class Main {

	public static void main(String[] args) throws Exception {
		ObjectMapper mapper = new ObjectMapper();
		// JSON文字列
		String json = "{\"name\": \"Foo\", \"age\": 20}";
		// JSON文字列 を Bean に変換する
		UserBean bean = mapper.readValue(json, UserBean.class);
		// Bean の内容を標準出力に書き出す
		System.out.println(bean);
		// Bean を JSON文字列 に変換して標準出力に書き出す
		mapper.writeValue(System.out, bean);
	}
}

ObjectMapper クラスのインスタンスを通して JSON を変換します。JSON から Bean に変換するには ObjectMapper#readValue() メソッド、反対に Bean から JSON に変換するには ObjectMapper#writeValue() メソッドを使います。

今回変換する JSONスキーマには文字列のプロパティ "name" と数値のプロパティ "age" が含まれています。対応する Bean のクラスにも文字列 (String) と数値 (Integer) のフィールドを用意します。

package jp.example.jackson.databinding;

public class UserBean {

	protected String name;
	protected Integer age;
	
	public UserBean() {}
	
	public UserBean(String name, Integer age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "UserBean{" + "name=" + name + ", age=" + age + '}';
	}
	
}

何の変哲もない Bean ですね。

Main を実行すると以下のような出力が得られます。

UserBean{name=Foo, age=20}
{"name":"Foo","age":20}

上が UserBean#toString() 下が ObjectMapper#writeValue() の結果です。

ご覧のように Jackson を使うと、とっても簡単に JSON を扱えます。今回は文字列と数値だけのシンプルな変換でしたが、配列を List や Set に変換したり、連想配列を Map に変換したり、オブジェクトがネストしているような複雑な JSON も同様に扱うことができます。

Google Guice で AOP (アスペクト指向プログラミング)

プログラムのあっちゃこっちゃで頻繁に登場するような処理などを上手く取り扱うために AOP (Aspect Oriented Programming) という技法があるみたいです。このエントリは Google Guice という Java の DIコンテナ を使って AOP を試した際のメモです。

今回のサンプルは、メソッドの実行時間を計測するものです。アプリケーションのスループットを向上させるにはベンチマークが必要不可欠ですが、手動で実行時間を測定するためのコードをあっちゃこっちゃに仕込む、なんてのはありえないですよね。サンプルでは、あらかじめ適用するメソッドのルールを設定するだけで、測定できるようにします。

まずは Mainクラス です。

package test.guice.aop;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Main {
	public static void main(String[] args) {
		Injector injector = Guice.createInjector(new MyGuiceModule());
		Service service = injector.getInstance(Service.class);

		service.execute();
	}
}

Guice の基本的な使い方は DI (Dependency Injectio) です。Injectorインタフェース のインスタンスを作成した上で、そこから依存性が注入されたインスタンスを得ます。GuiceAOP では、依存性を注入したインスタンスに対して必要な処理を織り込めます。Injectorインタフェース は Guice.createInjector() メソッドで得られます。引数の MyGuiceModule は Guice の設定です。

Guice の設定となる MyGuiceModuleクラス です。AbstractModuleクラス を継承して作ります。

package test.guice.aop;

import com.google.inject.AbstractModule;
import com.google.inject.matcher.Matchers;

public class MyGuiceModule extends AbstractModule {

	@Override
	protected void configure() {
		this.bind(Service.class).to(ServiceImpl.class);
		this.bindInterceptor(Matchers.any(), Matchers.any(), new BenchmarkInterceptor());
	}
	
}

AbstractModule#bind(), AbstractModule#to() メソッドでは DI について設定しています。今回は AOP に使うインスタンスを得るためです。Serviceインタフェース を指定して Guice からインスタンスを得るときに、実装はServiceImplクラスのインスタンスになるよう設定しています。

AbstractModule#bindInterceptor() メソッドが AOP の設定です。第一引数には AOP を適用するクラスにマッチするルールを指定します。第二引数は同メソッドの指定です。第三引数は、マッチした際に適用する処理を実装した MethodInterceptor を指定します。

マッチした際の処理を記述した BenchmarkInterceptorクラス です。

package test.guice.aop;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class BenchmarkInterceptor implements MethodInterceptor {

	public Object invoke(MethodInvocation mi) throws Throwable {
		String methodName = mi.getMethod().getName();
		
		long start = System.currentTimeMillis();
		Object result = mi.proceed();
		long end = System.currentTimeMillis();
		
		System.out.println(methodName + ": " + (end - start) + "ms");
		
		return result;
	}
	
}

メソッドの実行の前後で時間を測定して、メソッド名と実行時間を標準出力しています。本来のメソッドの呼び出しを横取りしているイメージです。

以下は処理の織り込み対象のインタフェースとクラスです。

package test.guice.aop;

public interface Service {
	
	public void execute();
}
package test.guice.aop;

public class ServiceImpl implements Service {
	
	public void execute() {
		System.out.println("execute!!");
	}
	
}

今回使ったライブラリは Guice だけですが一応 pom.xml を貼っておきます。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>test.guice.aop</groupId>
	<artifactId>test-guice-aop</artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>test-guice-aop</name>
	<url>http://maven.apache.org</url>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.2.1</version>
				<configuration>
					<mainClass>test.guice.aop.Main</mainClass>
				</configuration>
			</plugin>
		</plugins>
	</build>
	
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>com.google.inject</groupId>
			<artifactId>guice</artifactId>
			<version>3.0</version>
		</dependency>
	</dependencies>
</project>

Mainクラス 実行すると以下のような結果が得られます。

execute!!
execute: 3ms

ServiceImpl#execute() メソッドの実行に 3ms かかったことがわかりました。

AOP はプログラムで頻繁に登場する処理や、モックデータへの入れ替えなどに使うと便利そうですね。有効に扱うには、プログラム中で使うインスタンスのほとんどを依存性が注入されたもので構成する必要があるでしょうか。