2012年3月12日月曜日

DI framework. at Google-Guice



普段Javaで開発することが多いですが、DIフレームワークとして以前は、spring framework を使っていましたが、最近 google-guice を最近使うようになりました。以下guiceの使い方メモです。

spring framework における定義. applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
   "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  <bean id="target" class="Target" >
    <property name="message" >
      <value>Hello World!</value>
    </property>
  </bean>
</beans>

guiceにおける定義は、GenericsとAnnotationを使って定義します。

@ImplementedBy(value = DefaultFace.class)
public interface Face {
  void print();
}

@Singleton
public class DefaultFace implements Face {
  @Override
  public void print() {
    System.out.println("(^_^;)");
  }
}

public class FacePrinter {
  @Inject Face face;


  public void printFace() {
    face.print();
  }
}

public class InjectTest {
  @Test
  public void test_default() {
    FacePrinter printer = new FacePrinter();
    Guice.createInjector(Stage.PRODUCTION, new AbstractModule() {
      @Override
      protected void configure() {
      }
    }).injectMembers(printer);
    printer.printFace();
  }
}

では、Faceインタフェースに他のインスタンスをインジェクトしてみましょう.

public class HappyFace implements Face {
  @Override
  public void print() {
    System.out.println("(∩´∀`)∩ワーイ");
  }
}

public class InjectTest {
  @Test
  public void test_inject() {
    FacePrinter printer = new FacePrinter();
    Guice.createInjector(Stage.PRODUCTION, new AbstractModule() {
      @Override
      protected void configure() {
        bind(Face.class).to(HappyFace.class).in(Scopes.SINGLETON);
      }
    }).injectMembers(printer);
    printer.printFace();
  }
}

はい。これでFacePrinterインスタンスにインジェクトされるFaceメンバ変数は、HappyFaceのインスタンスになったと思います。
では、もうちょっと複雑なものを...

public class FacePrinter2 {


  @Inject Map<String, Face> faces;
  @Inject Face defaultFace;


  static int cnt = 1;


  @Inject(optional = true)
  public void setPrintCounts(@Named("printer.printcounts") int n) {
    cnt = n;
  }


  public void printFace(String yourheart) {
    for ( int i = 0; i < cnt; i++) {
      if ( faces.containsKey(yourheart)) faces.get(yourheart).print();
      else defaultFace.print();
    }
  }
}

上記クラスの Map<String, Face> は、yourheartパラメータによってFaceを設定できるようにします。また、
setPrintCounts(int) でデフォルトのprint回数を指定できます。

class DefaultModule extends AbstractModule {


  @Override protected void configure() {
    bind(Face.class).annotatedWith(Names.named("!!")).to(KitaFace.class).in(Scopes.SINGLETON);
    bind(Face.class).annotatedWith(Names.named("happy")).to(HappyFace.class).in(Scopes.SINGLETON);
    bind(Face.class).annotatedWith(Names.named("unhappy")).to(UnhappyFace.class).in(Scopes.SINGLETON);
    bind(new TypeLiteral<Map<String, Face>>() {}).toProvider(FaceProvider.class);
  }


  static class FaceProvider implements Provider<Map<String, Face>> {
    static Map<String, Face> faces = new HashMap<String, Face>();
 
    @Override
    public Map<String, Face> get() {
      return faces;
    }
 
    @Inject
    public FaceProvider(@Named("!!") Face kita, @Named("happy") Face happy, @Named("unhappy") Face unhappy) {
      faces.put("!!", kita);
      faces.put("happy", happy);
      faces.put("unhappy", unhappy);
    }
  }
}

public class InjectTest2 {
  @Test public void test() {
    System.out.println("### test injectMembers.");
    FacePrinter2 printer = new FacePrinter2();
    Guice.createInjector(Stage.PRODUCTION, new DefaultModule()).injectMembers(printer);
    printer.printFace("!!");
    printer.printFace("happy");
    printer.printFace("unhappy");
    printer.printFace("default");
  }
}

さぁ実行してみましょう。正しく実行されましたね!!

bind(Face.class).annotatedWith(Names.named("!!")).to(KitaFace.class).in(Scopes.SINGLETON);
annotatedWith(@Named("!!")) で"!!"という名前のFaceインタフェースはKitaFace.classをsingletonでインジェクトするという定義です。
これをTypeLiteral で Map<String,Face> というtypeにインジェクトするという定義が以下で行われています。
bind(new TypeLiteral<Map<String, Face>>() {}).toProvider(FaceProvider.class);

では、FacePrinter2クラスで @Inject(optional=true)とありますが、ここではModuleクラスで定義をしていませんので、初期値の1が適用されていると思います。
テストケースを以下に編集し実行してみましょう。propsは、propertiesファイルを用意してロードしても問題ありません。

public class InjectTest3 {
  @Test public void test2() {
    FacePrinter2 printer = new FacePrinter2();
    Guice.createInjector(Stage.PRODUCTION, new DefaultModule() {
      @Override protected void configure() {
        super.configure();
        // key-values data configuration settings.
        Map<String, String> props = new HashMap<String, String>();
        props.put("printer.printcounts", new Integer(3).toString());
        Names.bindProperties(this.binder(), props);
      }
    }).injectMembers(printer);
    printer.printFace("!!");
  }
}

これで、指定した回数(今回は3)printされたと思います。

次にAOP(Aspect Oriented Programming)についてのサンプルです。

@ImplementedBy(value=ServiceImpl.class)
public interface Service {
  void execute(String a);
}

@Singleton
public class ServiceImpl implements Service {
  @TraceLog
  public void execute(String a) {
    System.out.println("this is 'Guice' Aspect oriented programming!! #" + a);
  }
}

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@BindingAnnotation
public @interface TraceLog {
}

public class TraceLogInterceptor implements org.aopalliance.intercept.MethodInterceptor {
  @Override
  public Object invoke(org.aopalliance.intercept.MethodInvocation invocation) throws Throwable {
    System.out.println("### start trace log !!");
    long bef = System.currentTimeMillis();
    Object rtn = invocation.proceed();
    long tat = System.currentTimeMillis() - bef;
    System.out.println("### end trace log !! tat:" + tat + "ms");
    return rtn;
  }
}

public class AopModule extends AbstractModule {
  @Override
  protected void configure() {
    Matcher<Object> classes = Matchers.any();
    Matcher<AnnotatedElement> methods = Matchers.annotatedWith(TraceLog.class);
    bindInterceptor(classes, methods, new TraceLogInterceptor());
  }
}

public class AOPTest {
  @Test
  public void test() {
    Injector injector = Guice.createInjector(Stage.PRODUCTION, new AopModule());
    Service sercie = injector.getInstance(Service.class);
    sercie.execute(" hello world!!");
  }
}

AOP Alliance を使ってメソッドにトレースログを入れるサンプルです。これもAnnotationだけで実装されています。
http://aopalliance.sourceforge.net/

このように、Guiceでは、DIをGenericsとAnnotationを使って全て定義することができるのです。
springframeworkのように全てをXMLファイルで管理するメリットはソースとDI定義を分離し、ソースの変更が必要ないことがメリットと思います。ただ、大規模開発となり分業で開発が必要になった場合、クラス名などを編集するとあわせてXMLファイルも修正する必要があります。また多くの定義を行うとどこに記述されているかなど管理面でコストがかかるように個人的には感じます。
その点、guiceではソースコードに全てが定義されますのでソースの改変が合った場合のリファクタリングもeclipseのようなIDEを使っている場合はあわせておこなってくれますし、間違っていればコンパイル時にエラーとなります。またソースの検索なども便利です。

補足としてservletでguiceを使うとき、

public class GuiceContainer implements ServletContextListener {


  public static final String INJECTOR_ATTRIBUTE = Injector.class.getName();
  public static final String INJECTOR_DOMAIN = "guice-context";
  private static boolean initialized = false;


  @Override
  public void contextDestroyed(ServletContextEvent ctx) {
    ctx.getServletContext().removeAttribute(INJECTOR_ATTRIBUTE);
  }


  @Override
  public void contextInitialized(ServletContextEvent ctx) {
    String modules = ctx.getServletContext().getInitParameter("guice-modules");
    List<Module> _modules = new ArrayList<Module>();
    if ( modules != null && modules.trim().length() != 0) {
      for ( String fqcn : modules.split(":")) {
        fqcn = fqcn.trim();
        if ( fqcn.trim().length() > 0) {
          try {
            _modules.add((Module) Class.forName(fqcn).newInstance());
          } catch (Throwable e) {
            throw new RuntimeException(e);
          }
        }
      }
    }
    Injector injector = Guice.createInjector(Stage.PRODUCTION, _modules);
    ctx.getServletContext().setAttribute(INJECTOR_ATTRIBUTE, injector);
 
    if ( !initialized) {
      try {
        Manager.manage(INJECTOR_DOMAIN, injector);
        initialized = true;
      } catch (Exception e) {
      }
    }
  }
}

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
  version="2.4">


  <display-name>sample-webapp</display-name>
  <description>sample-webapp</description>


  <context-param>
    <param-name>guice-modules</param-name>
    <param-value>
        org.test.modules.InjectModule:
        org.test.modules.AopModule
    </param-value>
  </context-param>


  <listener>
    <listener-class>org.test.web.GuiceContainer</listener-class>
  </listener>


  <!-- Your web-app Settings -->
</web-app>

これで、サーブレットコンテナが起動したときにguiceの初期化を行います。必要なModuleをinjectorに設定するだけです。そしてサーブレットとフィルターでは以下のようにinitメソッドで自分のメンバにinjectします


public abstract class InjectedFilter implements Filter {


  protected Injector injector;


  @Override
  public void init(FilterConfig fc) throws ServletException {
    ServletContext context = fc.getServletContext();
    injector = (Injector) context.getAttribute(GuiceContainer.INJECTOR_ATTRIBUTE);
    if ( injector != null) {
      injector.injectMembers(this);
    }
  }
}

public abstract class InjectedServlet extends HttpServlet {


  protected Injector injector;


  @Override
  public void init(ServletConfig sc) throws ServletException {
    super.init(sc);
    injector = (Injector)sc.getServletContext().getAttribute(GuiceContainer.INJECTOR_ATTRIBUTE);
    if ( injector != null) {
      injector.injectMembers(this);
    }
  }
}

また、Android版DIフレームワークとしてguiceをベースとした、roboguiceというものもあります。
Android開発するときには是非使ってみたいと思います。
http://code.google.com/p/roboguice/