Spring Boot + Spring AOP 超初心者向け

いま作業している案件はSpring Framework + Spring Bootなんですが、はじめたてで、まだまだ知らないことが山ほどあるので、判ったことを備忘としてここに書きます。

今回は「Spring AOP」についてです。話題の範囲は「とにかく動かす」+「完結してないけど判ったこと」になります。当然ですが、知っている人には何の役にも立たないので、別のことに時間を当ててください!

■Spring AOPというか、Aspectについて簡単に

そもそも「Spring AOP」はSpring Frameworkに組み込まれている、Aspectな動作を助けてくれるライブラリです。では、Aspectな動作とはなにか。正直言って、私自身ちゃんと理解できていないので恐縮なのですが、ともかく「色んなクラスの処理に対して、クラスの関係性に縛られず、実装も変更せずに、処理を追加する」ことと考えればよいようです。べたな用途として「既存実装を変えずにログ処理を追加する」「トランザクション処理を追加する」があるそうです。例えば、製造後期に全体的にちょっとした処理を追加して帳尻を合わせたくなることはままありますが、この「Spring AOP」を使用すれば、実装をいじらずに実現できてしまうのです。魔法みたいです。

参考:

■今度こそSpring AOPについて

さて「Spring AOP」は(おそらく)Javaのライブラリである「AspectJ」ライブラリに準拠しているそうで、こいつの単語が把握できれば、「Spring AOP」の大筋が理解できます。

Spring側のマニュアルには以下のように書いてあります。

10.1.1 AOP concepts
Let us begin by defining some central AOP concepts and terminology. These terms are not
Spring-specific… unfortunately, AOP terminology is not particularly intuitive;
however, it would be even more confusing if Spring used its own terminology.

Aspect: a modularization of a concern that cuts across multiple classes.
Transaction management is a good example of a crosscutting concern in enterprise Java
applications. In Spring AOP, aspects are implemented using regular classes
(the schema-based approach) or regular classes annotated with the
@Aspect annotation (the @AspectJ style).

Join point: a point during the execution of a program, such as the execution of a
method or the handling of an exception. In Spring AOP, a join point always
represents a method execution.

Advice: action taken by an aspect at a particular join point. Different types of
advice include “around,” “before” and “after” advice. (Advice types are discussed
below.) Many AOP frameworks, including Spring, model an advice as an interceptor,
maintaining a chain of interceptors around the join point.

Pointcut: a predicate that matches join points. Advice is associated with a
pointcut expression and runs at any join point matched by the pointcut (for example,
the execution of a method with a certain name). The concept of join points as matched
by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut
expression language by default.

Introduction: declaring additional methods or fields on behalf of a type. Spring
AOP allows you to introduce new interfaces (and a corresponding implementation) to any
advised object. For example, you could use an introduction to make a bean implement an
IsModified interface, to simplify caching. (An introduction is known as an
inter-type declaration in the AspectJ community.)

Target object: object being advised by one or more aspects. Also referred to as
the advised object. Since Spring AOP is implemented using runtime proxies, this
object will always be a proxied object.

AOP proxy: an object created by the AOP framework in order to implement the aspect
contracts (advise method executions and so on). In the Spring Framework, an AOP proxy
will be a JDK dynamic proxy or a CGLIB proxy.

Weaving: linking aspects with other application types or objects to create an
advised object. This can be done at compile time (using the AspectJ compiler, for
example), load time, or at runtime. Spring AOP, like other pure Java AOP frameworks,
performs weaving at runtime.

参考:

知識も足らず、英語も読めずなので、なんとも心もと限りなのですが、ともかく以下みたいです。これが判れば、最低限使う分には大丈夫のようです。

  • Join point:Aspectな処理を追加(織り込み:weave)しようと思えばできる処理の総称。「Spring AOP」ではメソッドが対象になると思っておけば良いようです。
  • Advice:Join pointに織り込む処理。今回こいつを用意します(例:ログを出力する処理等)。
  • Pointcut:Join pointの中から、特定の条件のものを切り出し、ひとまとめにした処理のグループ(例:@RequestMappingをもつメソッドのみ等)。

特定の条件に合うメソッド(Pointcut)が実行される前後で、用意しておいた処理(Advice)を動かすのが今回の目的です。

■Spring Framework + Spring Boot環境で実装するときのポイント

Spring Boot: 1.2.5

作業チェックリストは以下。

  • pomに「org.springframework.boot」「spring-boot-starter-aop」のモジュールを読み込む旨追記(今回はすでに読み込まれているとwarningが出たので追記していない)。
  • Adviceのクラスを用意する(今回はControllerAspect)。

流石、Spring Bootは手軽です。ですが今回もやはりというか、まったく、いつも通り詰まりました。ポイントは以下です。

  • Join pointはpublicのメソッドのみ対象のようです(そういえばマニュアルのどこかにあったような)。アクセス修飾子未指定かつAdviceと同ネームスペースでも使用できませんでした。
  • AdviceはPointcut指定を兼ねることができる。
  • Pointcut条件
    • 「&&」でAND、「||」でOR。
    • execution条件で対象メソッド指定する。複雑。「*」「..」が使用できる。
    • @annotation条件でアノテーションを指定できる。
    • 絶対パスでも、引数名指定でも可(引数ならAdviceの引数と同じ文字列でないとダメ)。
  • args条件で引数を条件にできる。引数が欲しい場合はこれで指定する。
    • 絶対パスと引数名指定が可能。
    • 「*」「..」も使用可能。
    • 引数の並びも関係あり。先頭には「..」は指定できない。
  • Adviceの引数に「JoinPoint jp」を指定すると、Join pointの情報が手に入る。引数もここからでも取得可能。

参考:

■実装例

MVCで@RequestMappingアノテーションを持つメソッドが起動したとき、viewにログイン情報を渡す処理になります。

□ControllerAspect(Advice)
@Aspect
@Component
public class ControllerAspect {

    /**
     * ログインユーザーの情報を画面に渡す処理
     * @param model
     * @param loginUser
     */
    @Before("execution(* jp.regrex.lista.web.*.*(..))"
            + " && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void addAttributeLoginUserInfo(JoinPoint jp)
    {
        /**
         * JoinPointの処理が return "redirect: " でリダイレクトしていた場合、modelに値をセットしているとリダイレクトしない。
         * そのため、返り値を取得しredirectで始まる場合は処理を行わない。
         */
        if(retVal != null && retVal.toString().startsWith("redirect:")) {
            return;
        }
        
        Model model = null;
        LoginUserDetails loginUser = null;
        for (Object arg : jp.getArgs()) {
            if(arg instanceof Model) {
                model = ((Model)arg);
            } else if(arg instanceof LoginUserDetails) {
                /*
                 * メソッドに引数として指定されているログイン情報を取得する
                 * もしJoinPointでログイン情報を書き換えている場合、この方法でないと最新が取得できない
                 */
                loginUser = ((LoginUserDetails)arg);
            }
        }
        
        if(model != null) {
            // メソッドに引数が指定されていなければログイン情報を取得する
            if(loginUser == null) {
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if(authentication.getPrincipal() instanceof LoginUserDetails) {
                    loginUser = (LoginUserDetails)authentication.getPrincipal();
                }
            }
            
            // 変数をview側に渡す
            model.addAttribute("request", request);                                                            // リクエスト
            model.addAttribute("isGroupLeader", (loginUser != null) ? loginUser.isGroupLeader() : false);    // グループリーダーであるかどうか
        }
    }
}
□UserRegistController(Join point)
@RequestMapping(value = "/edit", method = RequestMethod.GET)
public String editForm(HttpServletRequest request, Model model, @AuthenticationPrincipal LoginUserDetails loginUser) {
return editForm(loginUser.getUser().getId(), request, model, loginUser);
}

説明が中途半端で、どこらへんを対象にした記事だか判らなくなりましたがこれで終わりです。探せばもっと詳しく的確な説明をしてくれている記事があります!