Java EEのCDIで定義しておくと便利なプロデューサーとインターセプター

このエントリはJava EE Advent Calendar 2013の13日目の金曜日です。
昨日は@nagaseyasuhitoさんでした。
メソッドバリデーションのユニットテスト | nagaseyasuhito Daily works.
明日は@yamadamnさんが、ぼくの知らない世界のことを書いてくれるんだと思います。

その前に

Java EE 8に盛り込んで欲しい機能のアンケートが行われています。
Jersey MVCを標準に入れるべきかとか、FaceletsをJSFから切り離すべきかとか、CDIの@Stereotypeを他のアノテーションにも適用するべきかとか。MVCは欲しいし、そのMVCJSFとでFaceletsテンプレートを共有したいし@Stereotypeでアノテーションをまとめれれば「アノテーション地獄」もなくなるし。
興味ない項目は「Not sure」にすればいいと思うんで、欲しいものだけYesつけていけばいいと思います。アンケートに参加しておきましょう。
Help Shape the Future of Java EE 8 | Javalobby

はじめに

さて、年の瀬の忙しい時期ですが、みなさん快適なJava EE 7ライフを送られていることかと思います。
そんなみなさんはすでに当たり前に使っていると思いますが、これからCDIを使われる方々にはプロジェクトが進んでCDIについて理解が進んだときに気づいて「あ〜、これ最初から入れておけばちょう楽やったやん!」と叫んでもすでに多くのコードが書かれてて後の祭という事態に陥らないよう、使うと便利でどんなプロジェクトでも必須っぽいプロデューサーとインターセプターを紹介しておこうと思います。

CDIのプロデューサーとインターセプター

プロデューサーは、注入対象になるオブジェクトを取得する仕組みです。
インターセプターは、メソッドの前後に処理をはさめる仕組みで、ようするにAOPです。


永続性プロデューサー

JPAを使うときにはEntityManagerが必要になりますが、これを用意するときには永続性ユニットの名前の指定が必要になります。
これをいちいち各クラスに記述するのもイケてない。
ということで、次のようなプロデューサーを用意しておきます。

@Named
@Dependent
public class EntityManagerProducer {
    @PersistenceContext(unitName = "kis_PU")
    @Produces
    private EntityManager em;
}


そうすると、次のようにしてEntityManagerに注入ポイントを設定するだけで勝手に適切なEntityManagerが注入されます。

@Named
@ApplicationScoped
public class BunruiDao {
    @Inject
    private EntityManager em;

    @Transactional
    public void persist(
        @Valid TBunrui bunrui) 
    {
        em.persist(bunrui);
    }
}

ロガープロデューサー

ログをとるときのロガー取得で、わざわざそのロガーのあるクラスの名前を設定するというちょっとめんどくさいコードが必要になったりします。

class ImportantLogic{
    static final Logger logger = Logger.getLogger(ImportantLogic.class.getName());

こんなの。各クラスにあって見苦しい。


これを、自動的に注入させることでめんどくささから逃げようというプロデューサーです。

@Named
@Dependent
public class LoggerProducer {
    @Produces
    public Logger getLogger(InjectionPoint ip){
        return Logger.getLogger(ip.getMember().getDeclaringClass().getName());
    }
}


こんな感じで使えます。

@Named
@RequestScoped
class YabaiController{
    @Inject
    transient Logger logger;
    
    public void log(){
        logger.log(Level.SEVERE, "やばいよ〜");
    }
}


そうすると、loggerにはYabaiControllerという名前が設定されたものが注入されています。

エラー報告インターセプター

JSFでアクションを定義するときに、予期しない例外が出るととりあえずメッセージとして「システムエラーが発生しました」のように表示してログをとっておくというような場合がよくあります。
そういうときは、アクションにインターセプターをかませば便利です。


まず次のようなアノテーションを用意します。

@InterceptorBinding
@Target(value={ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Priority(Interceptor.Priority.APPLICATION + 20)
public @interface WithErrorMessage{
}


で、このアノテーションがついたメソッドの実行時に行われる処理を記述します。

@Interceptor
@Dependent
@WithErrorMessage
public class ErrorMessageInterceptor implements Serializable{
    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception{
        try{
            return ic.proceed();
        }catch(Exception ex){
            Logger logger = Logger.getLogger(ic.getTarget().getClass().getSuperclass().getName());
            logger.log(Level.SEVERE, "エラーです", ex);
            FacesContext.getCurrentInstance().addMessage(null, 
              new FacesMessage(FacesMessage.SEVERITY_ERROR, 
                "システムエラーが発生しました", 
                "システムエラーが発生しました"));
            return null;
        }
    }
}

ここで、上記のLoggerプロデューサーを使ってインターセプターのクラス名をもったロガーを取ってきても仕方ないので、ターゲットになるクラス名のロガーを使いたいことが多いと思います。
でも、単にic.getTarget().getClass().getName()なんてすると
kis.faces.CalcController$Proxy$_$$_WeldSubclass
などというCDIが内部で生成したクラス名が取れてしまうので、このクラスのスーパークラスを取ってくる必要があります。


あと、インターセプターはWEB-INF/beans.xmlに定義しておく必要があります。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
         http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="annotated">
    <interceptors>
        <class>kis.tsukuru4web.cdi.MyInterceptor</class>
        <class>kis.tsukuru4web.faces.ValidInterceptor</class>
    </interceptors>
</beans>


ともあれ、これでアクションのメソッドにアノテーションをつければ、このアクションが呼び出されて例外が発生したときに「システムエラーが発生しました」とメッセージが表示されるようになります。

    @WithErrorMessage
    public void subCalc(){
        result = calcLogic.sub(param1, param2);
    }

まとめ

あ〜、これ最初から入れておけば、以前の案件でちょう楽ができたやん!