Spring+AspectJ+LoadTimeWeaving

前回、HibernateのLAZYロード問題をClassTransformerによるLoadTimeWeavingで解決することが出来ました。実行時にClassデータを拡張するLoadTimeWeavingに関しては、今まではあまり積極的に使おうとは思っていなかったのですが、Hibernateの件によって、Weaving環境が確立されている場合は非常に強力な選択肢に成り得るんだということを実感しました。
そこで、Springを調査するときにスルーしていたAspectJのLoadTimeWeavingに次は挑戦してみようと思います。
id:da-yoshi:20080511:1210486860の日記ではSeasar2を利用してJAX-RS JerseyのResourceクラスに対してトランザクション機能を追加しました。同じことを今度はSpring+AspectJを使ってやってみようと思います。
まずは、今回対象となるResourceクラス。前回使ったクラスを利用しますが、Spring+AspectJの為のアノテーションを追加しています。

package mapsample.service;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.ProduceMime;
import javax.ws.rs.WebApplicationException;

import mapsample.entity.Map_contents;

import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.transaction.annotation.Transactional;

@Path("/mapContents")
@Configurable
@Transactional
public class MapContentsService {

	public MapContentsService() {
		System.out.println("new MapContentsResource()");
	}

	// @PersistenceContext(unitName = "mapsample")
	@Resource
	private EntityManager em;

	@SuppressWarnings("unchecked")
	@GET
	@ProduceMime("application/json")
	public JSONArray getList() throws JSONException {

		List<Map_contents> list = em.createNamedQuery(
				"Map_contents.getMapContents").getResultList();

		List<JSONObject> jsons = new ArrayList<JSONObject>();
		for (Map_contents mc : list) {
			jsons.add(mc.getJson());
		}
		JSONArray array = new JSONArray(jsons);
		return array;
	}

	@GET
	@Path("{id}")
	@ProduceMime("application/json")
	public JSONObject find(@PathParam("id")
	String id) throws JSONException {
		Map_contents mc = em.find(Map_contents.class, Integer.valueOf(id));
		if (mc == null) {
			throw new WebApplicationException(404);
		}
		return mc.getJson();
	}

}

@PersistenceContextを使うとSpringはEntityManagerFactoryから自分でインスタンスを作成してしまうので、今回は@Resourceアノテーションを使ってEntityManagerをDIします。
ポイントは、クラスに定義した@Configurableと@Transactionalのアノテーションです。@Configurableを定義したクラスに対してSpring側でprototypeスコープのコンポーネント登録をしておき、AspectJでWeavingすることによって、普通にnewで作成することができます。つまり、JerseyなどのフレームワークにSpringを連携させる必要がなくなります。@TransactionalアノテーションをつけたクラスをAspectJでWeavingすると、Spring管理のコンポーネント同様にnewしたクラスにもトランザクションが有効になります。
続いて、Springの定義ファイル

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/tx
		http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
		http://www.springframework.org/schema/jee
		http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
		http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-2.5.xsd">
        
    <tx:jta-transaction-manager/>
    <tx:annotation-driven mode="aspectj"/>
    <jee:jndi-lookup id="mapsample"
		jndi-name="java:comp/env/persistence/mapsample" />

    <context:spring-configured/>
    <context:load-time-weaver weaver-class="org.springframework.instrument.classloading.glassfish.GlassFishLoadTimeWeaver"/>
    <context:annotation-config/>
    
    <bean id="mapContentsService" class="mapsample.service.MapContentsService"
		scope="prototype"/>

</beans>

tx:jta-transaction-managerタグにより、トランザクションマネージャをSpringが自動でAPサーバから取得してくれます。tx:annotation-drivenタグのmode属性にaspectjを指定することにより、SpringAOPではなくAspectJの@Transactional定義が有効になります。
EntityManagerはjee-jndi-lookupタグで取得。
context:spring-configuredタグにより、@Configurableが有効になります。
context:load-time-weaverタグにより、AspectJのLoadTimeWeaving環境をSpringが設定してくれます。ただし、GlassFishの場合、対応クラス名を指定した上で、earプロジェクトで実行しなければいけません。対象warを含んだearプロジェクトを作成してそのままデプロイすればOKです。GlassFish以外の対象サーバ(TomcatWebLogicなど)の場合はwarプロジェクトで大丈夫です。
context-annotation-configタグにより、対象クラス内のフィールドに対するアノテーション定義によるDIが有効になります。
最後に今回の対象クラスをprototypeで登録しています。スコープをprototypeにすれば自動登録でも大丈夫なのかな?
さて、次はAspectJの定義ファイル。WEB-INF/classes/META-INF/aop.xmlに書く定義です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE aspectj PUBLIC
        "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <weaver>
        <!-- only weave classes in our application-specific packages -->
        <include within="mapsample.service.*"/>
    </weaver>
    
    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="mapsample.aspect.LogAspect"/>
        <aspect name="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/>
        <aspect name="org.springframework.transaction.aspectj.AnnotationTransactionAspect"/>
    </aspects>

  </aspectj>

weaverタグでWeaving対象パッケージを選択、aspectsタグ内にAspectを記述します。AnnotationBeanConfigurerAspectはSpringが提供するAspect。@Configurableを定義したクラスのDIを行います。
AnnotationTransactionAspectは@Transactionalを定義したクラスに対してトランザクション定義を行います。
もう一つは自分が適当に作ったLogのAspectです。

package mapsample.aspect;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class LogAspect {
	
	private Log log = LogFactory.getLog(LogAspect.class);

	@Around("methodsToBeProfiled()")
	public Object profile(ProceedingJoinPoint pjp) throws Throwable {
		log.debug("method start");
	try {
		return pjp.proceed();
	} finally {
    		log.debug("method end");
        }
    }

    @Pointcut("execution(public * mapsample.service.*.*(..))")
    public void methodsToBeProfiled(){}
}

@Pointcutで対象クラス・メソッドを指定、@AroundがAspectの定義です。単純に前後でLogを記述するだけのAspectです。
最後に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">

	<listener>
		<listener-class>
			org.springframework.web.context.ContextLoaderListener
		</listener-class>
	</listener>

	<servlet>
		<servlet-name>Jersey Web Application</servlet-name>
		<servlet-class>
			com.sun.ws.rest.spi.container.servlet.ServletContainer
		</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>Jersey Web Application</servlet-name>
		<url-pattern>/resources/*</url-pattern>
	</servlet-mapping>

	<resource-ref>
		<res-ref-name>jdbc/mapsample</res-ref-name>
		<res-type>javax.sql.DataSource</res-type>
		<res-auth>Container</res-auth>
	</resource-ref>

	<persistence-context-ref>
		<persistence-context-ref-name>persistence/mapsample</persistence-context-ref-name>
		<persistence-unit-name>mapsample</persistence-unit-name>
	</persistence-context-ref>

</web-app>

Jerseyサーブレットは標準のクラスを定義しています。拡張の定義も書いていないので、Jersey自体には何の設定も行っていません。
これで、前回同様MapContentsServiceのgetListメソッドを呼び出してやると・・・

[#|2008-05-16T02:29:15.534+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|new MapContentsResource()|#]

[#|2008-05-16T02:29:15.534+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|
DEBUG 2008-05-16 02:29:15,534 [httpSSLWorkerThread-8080-0] Processing injected field of bean 'mapsample.service.MapContentsService': ResourceElement for private javax.persistence.EntityManager mapsample.service.MapContentsService.em

#]

[#|2008-05-16T02:29:15.535+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,535 [httpSSLWorkerThread-8080-0] Returning cached instance of singleton bean 'mapsample'

#]

[#|2008-05-16T02:29:15.537+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,535 [httpSSLWorkerThread-8080-0] method start

#]

[#|2008-05-16T02:29:15.537+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,537 [httpSSLWorkerThread-8080-0] Using transaction object [org.springframework.transaction.jta.JtaTransactionObject@caefaf]

#]

[#|2008-05-16T02:29:15.537+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,537 [httpSSLWorkerThread-8080-0] Creating new transaction with name [mapsample.service.MapContentsService.getList]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

#]

[#|2008-05-16T02:29:15.537+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,537 [httpSSLWorkerThread-8080-0] Initializing transaction synchronization

#]

[#|2008-05-16T02:29:15.540+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,540 [httpSSLWorkerThread-8080-0]
select
map_conten0_.id as id0_,
map_conten0_.area_category_id as area11_0_,
map_conten0_.comment as comment0_,
map_conten0_.created_at as created3_0_,
map_conten0_.lat as lat0_,
map_conten0_.lng as lng0_,
map_conten0_.lock_version as lock6_0_,
map_conten0_.map_category_id as map12_0_,
map_conten0_.order_by as order7_0_,
map_conten0_.title as title0_,
map_conten0_.updated_at as updated9_0_,
map_conten0_.visible as visible0_
from
Map_contents map_conten0_
where
map_conten0_.visible=1
order by
map_conten0_.order_by

#]

[#|2008-05-16T02:29:15.549+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,549 [httpSSLWorkerThread-8080-0] Triggering beforeCommit synchronization

#]

[#|2008-05-16T02:29:15.550+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,549 [httpSSLWorkerThread-8080-0] Triggering beforeCompletion synchronization

#]

[#|2008-05-16T02:29:15.550+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,550 [httpSSLWorkerThread-8080-0] Initiating transaction commit

#]

[#|2008-05-16T02:29:15.562+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,561 [httpSSLWorkerThread-8080-0] Triggering afterCommit synchronization

#]

[#|2008-05-16T02:29:15.562+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,562 [httpSSLWorkerThread-8080-0] Triggering afterCompletion synchronization

#]

[#|2008-05-16T02:29:15.562+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,562 [httpSSLWorkerThread-8080-0] Clearing transaction synchronization

#]

[#|2008-05-16T02:29:15.562+0900|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=httpSSLWorkerThread-8080-0;|DEBUG 2008-05-16 02:29:15,562 [httpSSLWorkerThread-8080-0] method end

#]

MapContentsServiceがnewされ、EntityManagerがDIされ、トランザクションが開始されてEntityManagerでQueryを実行し、トランザクションがコミットされてメソッドが終了したことがログに記述されています。自作のlogアスペクトも正常に動作しています。



・・・これ、凄いですね・・・LoadTimeWeavingを使うので、余計なコンパイル作業は発生しませんし、Springに対応していないFWに対して簡単にDI+AOP機能を提供してしまっています。更に、newで作成するオブジェクトが対象になっているので、当然状態を持つオブジェクト=ドメインモデルに対してもDI+AOPできてしまうってことになります。
SpringがAspectJ固執した理由はこれだったんですね・・・今回は試していませんが、これならJPAのEntityに対してもDI+AOPが可能になりますね。今まで自分が思っていたDIコンテナの概念が、一気に吹っ飛んでしまいました・・・