続・設定ファイルの内容をAOPでオブジェクトに設定する実験
(前回の続き)ConstructorInterceptorを使った設定ファイル内容のオブジェクトへの自動適用の仕組みを作ったものの、ターゲットとして想定していたDI+AOP環境であるSeasar2では(そのままでは)ConstructorInterceptorを使用できないようなのでその辺をなんとかする。
方針
コンテナ内のコンポーネント生成の機構に手を入れて、ConstructorInterceptorを使えるようにする。
S2のソースコードを読むと、コンポーネント(オブジェクト)生成、つまりコンストラクタの呼び出しの際には、ConstructorAssemblerインタフェースが使用される。ConstructorAssemblerの実装クラスを自前のものに取り替えることで、コンストラクタの呼び出しを制御する事が出来そうだ。ConstructorAssemblerの実装クラスであるAutoConstructorAssembler、DefaultConstructorConstructorAssemblerはAssemblerFactoryが使用しているProviderインタフェースの実装であるDefaultProviderクラス内でnewされているため、Providerも自前のものに取り替える事が必要になる。これは、AssemblerFactoryのstaticメソッドであるsetProvider()を使用することによって可能である。
S2では、コンポーネント生成時にコンテナ内に既に登録されているコンポーネントをコンストラクタインジェクションしている(autoBindingの設定によるが。) その際に、例えばAutoConstructorAssemblerでは自動バインド可能で引数の数が多いコンストラクタが優先して使用されるようだ。つまり、コンポーネント生成時にならないとどのコンストラクタが使用されるかは分からない。ConstructorInterceptor上でのgetConstructor()やgetArguments()を実際の生成時のものと一致させたいので、AutoConstructorAssemblerの代替クラスはAutoConstructorAssemblerのサブクラスとして実装し、getSuitableConstructor()やgetArgs()などの親のメソッドを利用するようにする。
S2でMethodInterceptorを使用する時には、diconファイルでコンポーネント定義に
InterceptorChainや複数のConstructorInterceptorには対応しない。使用されるコンストラクタ毎にInterceptorを設定する機能等も準備しない。
ソースコード
まず、ConstructorInvocationを実装する抽象スーパークラスを作る。staticPartは実際に使用されるコンストラクタだろうし、thisは生成されたインスタンスになるはずなので、その辺りを決め打ちにして差分はサブクラスに任せる。
package sample.assembler; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import org.aopalliance.intercept.ConstructorInvocation; /** * ConstractorInvocationを実装する為の抽象スーパークラス */ public abstract class AbstractConstructorInvocation implements ConstructorInvocation { private Object instance = null; /** * ConstructorInvocationの場合、staticPart=コンストラクタ * @return getConstructor()の値 */ public final AccessibleObject getStaticPart() { return getConstructor(); } /** * @return コンストラクタが呼ばれた際に生成したオブジェクトを戻す。 */ public final Object getThis() { return instance; } /** * コンストラクタを呼び出し、生成されたインスタンスを戻す * @return 生成されたインスタンスを戻す * @throws Throwable コンストラクタ呼び出し時の例外 */ public final Object proceed() throws Throwable { this.instance = doProceed(); return this.instance; } /** * 実際にコンストラクタを呼び出しをおこなう。 * @return 生成されたインスタンス * @throws Throwable コンストラクタ呼び出し時の例外 */ protected abstract Object doProceed() throws Throwable; public abstract Constructor getConstructor(); public abstract Object[] getArguments(); }
AutoConstructorAssembler, DefaultConstructorConstructorAssemblerそれぞれを継承し、assemble()時にConstructorInterceptorを呼び出す仕組みを実装する。
package sample.assembler; import java.lang.reflect.Constructor; import org.aopalliance.intercept.ConstructorInterceptor; import org.seasar.framework.container.ComponentDef; import org.seasar.framework.container.IllegalConstructorRuntimeException; import org.seasar.framework.container.assembler.AutoConstructorAssembler; public class InterceptableAutoConstructorAssembler extends AutoConstructorAssembler { private Class aClass; private ConstructorInterceptor interceptor; public InterceptableAutoConstructorAssembler(ComponentDef cd, ConstructorInterceptor interceptor) { super(cd); this.aClass = cd.getConcreteClass(); this.interceptor = interceptor; } public Object assemble() throws IllegalConstructorRuntimeException { try { return this.interceptor.construct(new AbstractConstructorInvocation(){ public Constructor getConstructor() { return getSuitableConstructor(); } public Object[] getArguments() { return getArgs(getSuitableConstructor().getParameterTypes()); } public Object doProceed() throws Throwable { return assembleInternally(); } }); } catch (Throwable e) { throw new IllegalConstructorRuntimeException(this.aClass, e); } } private Object assembleInternally() throws IllegalConstructorRuntimeException { return super.assemble(); } }
package sample.assembler; import java.lang.reflect.Constructor; import org.aopalliance.intercept.ConstructorInterceptor; import org.seasar.framework.container.ComponentDef; import org.seasar.framework.container.IllegalConstructorRuntimeException; import org.seasar.framework.container.assembler.DefaultConstructorConstructorAssembler; import org.seasar.framework.util.ClassUtil; public class InterceptableDefaultConstructorConstructorAssembler extends DefaultConstructorConstructorAssembler { private Class aClass; private ConstructorInterceptor interceptor; public InterceptableDefaultConstructorConstructorAssembler(ComponentDef cd, ConstructorInterceptor interceptor) { super(cd); this.aClass = cd.getConcreteClass(); this.interceptor = interceptor; } public Object assemble() throws IllegalConstructorRuntimeException { try { return this.interceptor.construct(new AbstractConstructorInvocation(){ public Constructor getConstructor() { return ClassUtil.getConstructor(aClass, null); } public Object[] getArguments() { return new Object[0]; } public Object doProceed() throws Throwable { return assembleInternally(); } }); } catch (Throwable e) { throw new IllegalConstructorRuntimeException(this.aClass, e); } } private Object assembleInternally() throws IllegalConstructorRuntimeException { return super.assemble(); } }
Providerの実装。
DefaultProviderを拡張し、ConstructorInterceptorが設定されている場合にAutoConstructorAssembler, DefaultConstructorConstructorAssemblerの代わりに上記の2クラスをnewするように該当メソッドをオーバーライドする。
package sample.assembler; import org.aopalliance.intercept.ConstructorInterceptor; import org.aopalliance.intercept.Interceptor; import org.seasar.framework.container.AspectDef; import org.seasar.framework.container.ComponentDef; import org.seasar.framework.container.ConstructorAssembler; import org.seasar.framework.container.assembler.AssemblerFactory; public class ConstructorInterceptableProvider extends AssemblerFactory.DefaultProvider { /* * AutoBindingConstructorDef, AutoBindingAutoDef使用時に呼ばれる */ @Override public ConstructorAssembler createAutoConstructorAssembler(ComponentDef cd) { ConstructorInterceptor interceptor = findConstructorInterceptor(cd); if (interceptor != null) { return new InterceptableAutoConstructorAssembler(cd, interceptor); } else { return super.createAutoConstructorAssembler(cd); } } /* * AutoBindingNoneDef, AutoBindingPropertyDef, AutoBindingSemiAutoDef使用時に呼ばれる */ @Override public ConstructorAssembler createDefaultConstructorConstructorAssembler( ComponentDef cd) { ConstructorInterceptor interceptor = findConstructorInterceptor(cd); if (interceptor != null) { return new InterceptableDefaultConstructorConstructorAssembler(cd, interceptor); } else { return super.createDefaultConstructorConstructorAssembler(cd); } } /** * ComponentDefに含まれるMethodInterceptorから、ConstructorInterceptorを実装しているもの * を探す。つまり、MethodInterceptorであるというのは只の擬態。 * @param cd ComponentDef * @return 見つかった最初のConstructorInterceptorを戻す */ private ConstructorInterceptor findConstructorInterceptor(ComponentDef cd) { int countOfAspects = cd.getAspectDefSize(); for (int index = 0; index < countOfAspects; index++) { AspectDef aspectDef = cd.getAspectDef(index); Interceptor interceptor = aspectDef.getAspect().getMethodInterceptor(); if (interceptor instanceof ConstructorInterceptor) { return (ConstructorInterceptor) interceptor; } } return null; } }
使用サンプル
レポートメール送信クラスに対する、接続先等の設定をおこなうイメージ。
まず、レポートメール送信クラス(ダミー)
package sample; import java.util.List; import org.apache.commons.lang.builder.ToStringBuilder; public class ReportMailer { private String smtpHost; private Integer smtpPort; private String fromAddress; private String subject; private List<String> bccAddresses; private String templatePath; public ReportMailer() { } public String getSmtpHost() { return smtpHost; } public void setSmtpHost(String smtpHost) { this.smtpHost = smtpHost; } public Integer getSmtpPort() { return smtpPort; } public void setSmtpPort(Integer smtpPort) { this.smtpPort = smtpPort; } public List<String> getBccAddresses() { return bccAddresses; } public void setBccAddresses(List<String> bccAddresses) { this.bccAddresses = bccAddresses; } ...(以下setter/getterが続く) public void sendReport(String toAddress, Map bindings) { .... }
diconファイル。
sample.ReportMailerクラスには、今回作成したconfigurationInterceptorの他にTraceInterceptorもセットして、実際に設定ファイルから読み込まれた値がセットされる様子が見えるようにした。
<?xml version="1.0" encoding="Shift_JIS"?> <!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN" "http://www.seasar.org/dtd/components21.dtd"> <components> <component name="configManager" class="sample.config.ConfigurationManager"> <arg>"config"</arg> </component> <component name="configurationInterceptor" class="sample.config.ConfigurationInterceptor"> <arg>configManager</arg> </component> <component class="sample.ReportMailer"> <aspect pointcut="<init>">configurationInterceptor</aspect> <aspect pointcut="set.*"> <component class="org.seasar.framework.aop.interceptors.TraceInterceptor"/> </aspect> </component> </components>
Provider設定(AssemblerFactory.setProxy(Provider)呼び出し)部分
// diconファイル該当箇所処理前のどこかに入れる AssemblerFactory.setProvider(new ConstructorInterceptableProvider());
設定ファイルconfig/sample.ReportMailer(前前回と同じ)
<?xml version = "1.0" encoding = "UTF-8" ?> <map> <string key="smtpHost">mx010</string> <integer key="smtpPort">25</integer> <string key="fromAddress">terazzo@example.com</string> <string key="subject">Report Mail</string> <list key="bccAddresses"> <string>admin1@example.com</string> <string>admin2@example.com</string> <string>admin3@example.com</string> </list> <string key="templatePath">mail_template/ReportMail.html</string> </map>
実行結果
DEBUG [main] (Logger.java:105) - BEGIN sample.ReportMailer#setBccAddresses([admin1@example.com, admin2@example.com, admin3@example.com]) DEBUG [main] (Logger.java:105) - END sample.ReportMailer#setBccAddresses([admin1@example.com, admin2@example.com, admin3@example.com]) : null DEBUG [main] (Logger.java:105) - BEGIN sample.ReportMailer#setFromAddress(terazzo@example.com) DEBUG [main] (Logger.java:105) - END sample.ReportMailer#setFromAddress(terazzo@example.com) : null DEBUG [main] (Logger.java:105) - BEGIN sample.ReportMailer#setSmtpHost(mx010) DEBUG [main] (Logger.java:105) - END sample.ReportMailer#setSmtpHost(mx010) : null DEBUG [main] (Logger.java:105) - BEGIN sample.ReportMailer#setSmtpPort(25) DEBUG [main] (Logger.java:105) - END sample.ReportMailer#setSmtpPort(25) : null DEBUG [main] (Logger.java:105) - BEGIN sample.ReportMailer#setSubject(Report Mail) DEBUG [main] (Logger.java:105) - END sample.ReportMailer#setSubject(Report Mail) : null DEBUG [main] (Logger.java:105) - BEGIN sample.ReportMailer#setTemplatePath(mail_template/ReportMail.html) DEBUG [main] (Logger.java:105) - END sample.ReportMailer#setTemplatePath(mail_template/ReportMail.html) : null
環境
- J2SE 5.0
- commons-digester-1.8.jar
- commons-logging-1.0.4.jar
- commons-beanutils-core-1.7.0.jar
- Seasar 2.4.13
- 付属のlibの中身全部
- log4j-1.2.9.jar
備考
PropertiesとかConfiguration系のクラスを使用する際、キーに使用する文字列が必要なため定数(static final String)地獄になりがちなんだけど、受動的にセットしてもらう方式にすることでかなりすっきりすると思う。
設定値の更新タイミングとオブジェクトのライフサイクルによっては、完全に受動的では使いにくい場合もあるかも。その場合はConfigurableインタフェースを作ってConfigurationオブジェクトをセット(or 追加)出来るようにするとか、ConfigurationManager自体をセット出来るようにするなどの方法が考えられる。
複数のConstructorInterceptorに対応するのは、難しくなそう。process()内で次のinterceptorのconstruct()を呼ぶようなConstructorInvocationを実装してネストにすれば良い。チェーンも多分同じように実装出来る。
一つのコンポーネントに複数の設定ファイルを指定したい場合、インタフェースを切ってインタフェース毎にファイルを作成すれば良い。というよりそういう場合そもそも単一責務になっていない臭い。「設定ファイルを分割したくなったらリファクタリングの合図」ということらしい。
その他
フレームワークをカスタマイズして使う場合、どのあたりまで現在の実装に依存しても良いのか悩む。今回だとせめてConstructorAssemblerの部分にDecoratorパターンが使えればもう少し実装依存度が下げられた気がする。