2026/07/06

Spring AOP

AOP 概念

Aspect-Oriented-Programming 就是在 java business service method 上加工,製造 method 呼叫前後的切面斷點,用以提供一些通用的商務邏輯,最常見的就是 business method 加上 database transaction 的機制。

博客來-深入淺出Spring Boot 3.x 裡面有一個部分,嘗試用程式的方式,透過 spring 使用的 cglib 套件實作 AOP 的概念。

spring boot 有兩種 proxy bean 的方法:JDK 或 CGLIB,預設是用 cglib,以下是用 cglib 的 Enhancer 產生動態 proxy bean。

  • 產生 service 跟 impl

HelloService.java

package com.test.interceptor.service;

public interface HelloService {
    public void sayHello(String name);
}

HelloServiceImpl.java

package com.test.interceptor.service.impl;

import com.test.interceptor.service.HelloService;

public class HelloServiceImpl implements HelloService {

    @Override
    public void sayHello(String name) {
        if (name == null || "".equals(name.trim())) {
            throw new RuntimeException("parameter is null!!");
        }
        System.out.println("hello " + name);
    }

}
  • interceptor

Interceptor.java

package com.test.interceptor.interceptor;

import com.test.interceptor.Invocation;

public interface Interceptor {
    // before method invoke
    public void before();

    // after method invoke
    public void after();

    /**
     * wrap method to add around actions
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    public Object around(Invocation invocation) throws Throwable;

    // after return if no exception
    public void afterReturning();

    // after return if throws exception
    public void afterThrowing();

    // use around to wrap method
    public default boolean useAround() {
        return false;
    }
}

InterceptorImpl.java

package com.test.interceptor.interceptor;

import com.test.interceptor.Invocation;

public class InterceptorImpl implements Interceptor {

    @Override
    public void before() {
        System.out.println("before ......");
    }

    @Override
    public boolean useAround() {
        return true;
    }

    @Override
    public void after() {
        System.out.println("after ......");
    }

    @Override
    public Object around(Invocation invocation) throws Throwable {
        System.out.println("around before ......");
        Object obj = invocation.proceed();
        System.out.println("around after ......");
        return obj;
    }

    @Override
    public void afterReturning() {
        System.out.println("afterReturning......");
    }

    @Override
    public void afterThrowing() {
        System.out.println("afterThrowing......");
    }
}

透過 Invocation 製作這個流程

  1. 呼叫 interceptor 的 before()

  2. 呼叫 target object method

  3. 當 method throw exception,呼叫 afterThrowing()

  4. 當 method 正常執行完成,呼叫 afterReturning()

  5. 呼叫 after()

  6. 如果 interceptor 的 useAround() 回傳 true,會用 around() 取代上面的流程

Invocation.java

package com.test.interceptor;

import com.test.interceptor.interceptor.Interceptor;
import lombok.Data;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@Data
public class Invocation {
    private Object[] params;
    private Method method;
    private Object target;
    private Interceptor interceptor;

    public Invocation(Object target, Method method, Object[] params, Interceptor interceptor) {
        this.target = target;
        this.method = method;
        this.params = params;
        this.interceptor = interceptor;
    }

    // reflection
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        Object retObj = null; // return object
        boolean exceptionFlag = false; // exception flag
        // invoke before()
        this.interceptor.before();
        try {
            // use reflection to call method
            retObj = method.invoke(target, params);
        } catch (Exception ex) {
            // exception flag
            exceptionFlag = true;
        }
        if (exceptionFlag) {
            this.interceptor.afterThrowing();
        } else {
            this.interceptor.afterReturning();
        }
        this.interceptor.after();
        return retObj;
    }

}
  • 將 target object 跟 interceptor 合併為流程

ProxyBean.java

package com.test.interceptor;

import com.test.interceptor.interceptor.Interceptor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class ProxyBean implements MethodInterceptor {
    private Interceptor interceptor = null;
    private Object target = null;

    /**
     * create proxy bean
     * @param target
     * @param interceptor
     * @return
     */
    public static Object getProxy(Object target, Interceptor interceptor) {
        var proxyBean = new ProxyBean();
        // create cglib Enhancer
        var enhancer = new Enhancer();
        // set proxy bean class
        enhancer.setSuperclass(target.getClass());
        // set proxy bean interfaces
        enhancer.setInterfaces(target.getClass().getInterfaces());
        // target object
        proxyBean.target = target;
        // interceptor
        proxyBean.interceptor = interceptor;
        // call proxyBean intercept()
        enhancer.setCallback(proxyBean);
        // create proxy bean
        var proxy = enhancer.create();
        return proxy;
    }

    /**
     * @param proxy  proxy bean
     * @param method interceptor method
     * @param args   method args
     * @param mproxy method proxy
     * @return return object
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy mproxy) throws Throwable {
        // invoke proxy bean
        var invocation = new Invocation(this.target, method, args, this.interceptor);
        Object result = null;
        if (this.interceptor.useAround()) { // enable around
            result = this.interceptor.around(invocation);
        } else {
            result = invocation.proceed();
        }
        return result;
    }
}
  • 測試

InterceptorTest.java

package com.test.interceptor;

import com.test.interceptor.interceptor.InterceptorImpl;
import com.test.interceptor.service.HelloService;
import com.test.interceptor.service.impl.HelloServiceImpl;

public class InterceptorTest {

    public static void main(String[] args) {
        testProxy();
    }

    public static void testProxy() {
        // target object
        var helloService = new HelloServiceImpl();
        // create proxy bean & interceptor
        var proxy = (HelloService) ProxyBean.getProxy(helloService, new InterceptorImpl());
        // call proxy bean method
        proxy.sayHello("Hello World");
        System.out.println("###############Testing Exception###############");
        proxy.sayHello(null);
    }

}
  • 執行結果
around before ......
before ......
hello Hello World
afterReturning......
after ......
around after ......
###############Testing Exception###############
around before ......
before ......
afterThrowing......
after ......
around after ......

AOP 術語

  • join point 連接點

    連接點就是告訴 AOP 在哪裡需要做 AOP,因為 AOP 只支援 method,被攔截的一定只能是 method

  • point cut 切點

    有時候需要啟動 AOP 的不是單一個 method,而是多個類別的不同 method。這邊可透過 regular expressoin 與指示器的規則定義 point cut,讓 AOP 根據定義匹配多個 methods。

  • advice 通知

    分為 before advice, after advice, around advice, afterReturning advice, afterThrowing advice

  • target 目標對象

    代理的物件對象,例如上面例子的 HelloServiceImpl

  • introduction 引用

    引用新的類別及 method,可增強現有bean的功能

  • weaving

    透過動態代理的技術,為目標物件生成代理物件。利用切點 join cut 的定義匹配的 join point 決定各類 advice 通知加入流程的過程

  • aspect 切面

    一個類別,透過它定義 AOP 的 point cut, advice

flowchart TB
    a1["target: HelloServiceImpl.sayHello()"]-->b2
    subgraph one [service]
    a1-->a2["cglib: weaving InterceptorImpl"]
    end
    subgraph two [InterceptorImpl, around advice]
    b1["before advice"]-->b2
    b2["method reflection"]-->b3{exception?}
    b3--yes-->b4
    b3--no-->b5
    b4["afterThrowing advice"]-->b6["after advice"]
    b5["afterReturning advice"]-->b6
    end

開發

pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

確認 target

假設有一個 UserService, UserServiceImpl,裡面有 printUser method,printUser 就是 target

UserService.java

package com.test.aspect.service;

import com.test.aspect.pojo.User;

public interface UserService {
    public void printUser(User user);

    public void multiAspects();
}

UserServiceImpl.java

package com.test.aspect.service.impl;

import com.test.aspect.service.UserService;
import com.test.aspect.pojo.User;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void printUser(User user) {
        if (user == null) {
            throw new RuntimeException("user is null!");
        }
        System.out.println("user=" + user);
    }

    @Override
    public void multiAspects() {
        System.out.println("testing multiAspects");
    }
}

開發切面 @Aspect

內容主要是各種 Apsect advice

@Before, @After, @AfterReturning, @AfterThrowing 分別用一個相同的 regular expression 定義哪個地方要啟用 AOP,匹配 target 的 join point 連結點(method)

MyAspectTemp.java

package com.test.aspect;

import com.test.aspect.pojo.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;

// 宣告為 Aspect,要加上 Component 註冊到 IoC
@Aspect
@Component
public class MyAspectTemp {
    // 以 regular expression,指定 join point (包含 class, method)
    // execution  代表在執行時,要攔截匹配的 regluar expression
    // * 代表不在意 method 回傳任何資料型別
    // com.test.aspect.service.impl.UserServiceImpl  指定 target object
    // printUser  指定 method
    // (..) 代表匹配任意參數
    private static final String aopExp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.printUser(..))";

    @Before(aopExp)
    public void before() {
        System.out.println("before ......");
    }

    @Before(aopExp)
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println("beforeParam ......");
    }

    @After(aopExp)
    public void after() {
        System.out.println("after ......");
    }

    @AfterReturning(aopExp)
    public void afterReturning() {
        System.out.println("afterReturning ......");
    }

    @AfterThrowing(aopExp)
    public void afterThrowing() {
        System.out.println("afterThrowing ......");
    }

}

定義切點 Point cut

剛剛的 Aspect0.java 每個註解都有相同的 regular expression,這邊改用一個 method 定義切點 point cut @Pointcut(aopExp),再用該 point cut 定義 AOP advices

MyAspect.java

package com.test.aspect;

import com.test.aspect.validator.UserValidator;
import com.test.aspect.validator.impl.UserValidatorImpl;
import com.test.aspect.pojo.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 宣告為 Aspect,要加上 Component 註冊到 IoC
@Aspect
@Component
public class MyAspect {
    // 以 regular expression,指定 join point (包含 class, method)
    // execution  代表在執行時,要攔截匹配的 regluar expression
    // * 代表不在意 method 回傳任何資料型別
    // com.test.aspect.service.impl.UserServiceImpl  指定 target object
    // printUser  指定 method
    // (..) 代表匹配任意參數
    private static final String aopExp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.printUser(..))";

//    // 定義 Enhancer bean 與 class
//    @DeclareParents(
//            // 需要引入增强的Bean
//            value = "com.test.aspect.service.impl.UserServiceImpl",
//            // 用這個類別增強
//            defaultImpl = UserValidatorImpl.class)
//    // Enhancer interface
//    public UserValidator userValidator;

    // 用 @Pointcut 定義切點,後面的 advice 可用這個 method 指定 join point
    @Pointcut(aopExp)
    public void pointCut() {
    }

    @Before("pointCut()") // 用 point cut 定義
    public void before() {
        System.out.println("before ......");
    }

    // args(user) 代表要傳遞的參數
    @Before("pointCut() && args(user)")
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println("beforeParam ......");
    }

    @After("pointCut()") // 用 point cut 定義
    public void after() {
        System.out.println("after ......");
    }

    @AfterReturning("pointCut()") // 用 point cut 定義
    public void afterReturning() {
        System.out.println("afterReturning ......");
    }

    @AfterThrowing("pointCut()") // 用 point cut 定義
    public void afterThrowing() {
        System.out.println("afterThrowing ......");
    }

//    @Around("pointCut()")
//    public void around(ProceedingJoinPoint jp) throws Throwable {
//        System.out.println("around before......");
//        // call back target object method
//        jp.proceed();
//        System.out.println("around after......");
//    }
}

@AspectJ關於@Pointcut 的指示器

類型 desc
arg() 限制 join point method 的參數
@args() 以 join point method 參數的註解進行限制
execution() 匹配 join point 的執行 method
this() 匹配當前 AOP 代理物件類別的方法
target target object
@target() 匹配當前 target object 類別的方法,target object 必須標注指定的註解
within 限制 join point 匹配指定的類別
@within() 限制 join point 帶有匹配註解的類別
@annotation 限制帶有指定註解的join point

測試AOP

加上 AopConfig.java

package com.test.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.test.*")
@EnableAspectJAutoProxy // 啟動 AOP
public class AopConfig {

}

AopTest1Service.java

package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest1Service {

    public static void main(String[] args) {
        testAop();
    }

    public static void testAop() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            var user = new User();
            user.setId(1L);
            user.setUserName("username1");
            user.setNote("note1");
            userService.printUser(user);
            System.out.println("########## exception test #####");
            userService.printUser(null);
        } finally {
            ctx.close();
        }
    }

}

執行結果

before ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......
########## exception test #####
before ......
afterThrowing ......
after ......
Exception in thread "main" java.lang.RuntimeException: user is null!

around advice

只有在需要大幅度修改原本 target object 的商業邏輯時,才需要使用 around,一般狀況下,不要使用 around

修改 MyAspect.java 加上 around

    @Around("pointCut()")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("around before......");
        // call back target object method
        jp.proceed();
        System.out.println("around after......");
    }

AopTest1Service.java 執行結果

around before......
before ......
beforeParam ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......
around after......
########## exception test #####
around before......
before ......
beforeParam ......
afterThrowing ......
after ......

Introduction

在使用 UserService 時,會因為 null 發生 exception,可利用 introduction 功能增強,而不需要修改原本的 UserServiceImpl

UserValidator.java

package com.test.aspect.validator;

import com.test.aspect.pojo.User;

public interface UserValidator {
    // testing user object is null or not
    public boolean validate(User user);
}

UserValidatorImpl.java

package com.test.aspect.validator.impl;

import com.test.aspect.validator.UserValidator;
import com.test.aspect.pojo.User;

public class UserValidatorImpl implements UserValidator {
    @Override
    public boolean validate(User user) {
        System.out.println("validate user with class: " + UserValidator.class.getSimpleName());
        return user != null;
    }
}

修改剛剛的 MyAspect.java

    // 定義 Enhancer bean 與 class
    @DeclareParents(
            // 需要 introduction 增强的Bean
            value = "com.test.aspect.service.impl.UserServiceImpl",
            // 用這個類別增強
            defaultImpl = UserValidatorImpl.class)
    // Enhancer interface
    public UserValidator userValidator;

測試

將 userService 轉換為 UserValidator 後使用 validate()

AopTest2Introduction.java

package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest2Introduction {

    public static void main(String[] args) {
        testIntroduction();
    }

    public static void testIntroduction() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            var user = new User();
            user.setId(1L);
            user.setUserName("username1");
            user.setNote("note1");
            // convert to UserValidator interface
            var userValidator = (UserValidator) userService;
            // 检查user is null ot not
            if (userValidator.validate(user)) {
                // 檢查通過,列印user 資訊
                userService.printUser(user);
            }

        } finally {
            ctx.close();
        }
    }

}

執行結果,就不會發生 exception

validate user with class: UserValidator
before ......
beforeParam ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......

advice parameter

要給 advice 通知加上參數,只需要修改 regular expression 與指示器

    // args(user) 代表要傳遞的參數
    @Before("pointCut() && args(user)")
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println("beforeParam ......");
    }

weaving

先用一個 interface 再加上實作,是 spring 推薦的方式,但 AOP 並不一定只能用在 interface。

動態代理有 JDK, cglib 及 JDK, javaassist。JDK 動態代理是要求代理對象必須要是 interface,但 cglib 沒有限制。預設狀況,如果有 interface,spring 會使用 JDK 動態代理,如果沒有,就用 cglib。

spring boot 預設就是使用 cglib。上面的 AopConfig.java 如果加上 @SpringBootApplication 就代表這是 spring boot

# false: 採用原本 spring 的方法
# true: 預設值,使用 spring boot 的方法,永遠使用 cglib
spring.aop.proxy-target-class=false

多個切面

AOP 可同時對一個 method 支援多個 aspects。

首先定義三個 Aspects

MyAspect1.java

package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
//@Order(3)
public class MyAspect1 implements Ordered {

    // 順序
    @Override
    public int getOrder() {
        return 3;
    }

    private static final String exp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))";

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before("multiAspects()")
    public void before() {
        System.out.println("MyAspect1 before ......");
    }

    @After("multiAspects()")
    public void after() {
        System.out.println("MyAspect1 after ......");
    }

    @AfterReturning("multiAspects()")
    public void afterReturning() {
        System.out.println("MyAspect1 afterReturning ......");
    }

}

MyApsect2.java

package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
//@Order(2)
public class MyAspect2 implements Ordered {

    // 順序
    @Override
    public int getOrder() {
        return 2;
    }

    private static final String exp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))";

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before("multiAspects()")
    public void before() {
        System.out.println("MyAspect2 before ......");
    }

    @After("multiAspects()")
    public void after() {
        System.out.println("MyAspect2 after ......");
    }

    @AfterReturning("multiAspects()")
    public void afterReturning() {
        System.out.println("MyAspect2 afterReturning ......");
    }

}

MyAspect3.java

package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class MyAspect3 {

//    // 順序
//    @Override
//    public int getOrder() {
//        return 1;
//    }

    private static final String exp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))";

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before("multiAspects()")
    public void before() {
        System.out.println("MyAspect3 before ......");
    }

    @After("multiAspects()")
    public void after() {
        System.out.println("MyAspect3 after ......");
    }

    @AfterReturning("multiAspects()")
    public void afterReturning() {
        System.out.println("MyAspect3 afterReturning ......");
    }

}

Aspects 的執行先後順序是用 @Order 或是 override Order interface 的 public int getOrder(),上面設定執行順序為 MyAspect3, MyAspect2, MyAspect1

AopTest3MultiAspect.java

package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest3MultiAspect {

    public static void main(String[] args) {
        testMultiAspects();
    }

    public static void testMultiAspects() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            // 測試 multiple aspects
            userService.multiAspects();
        } finally {
            ctx.close();
        }
    }
}

測試結果

MyAspect3 before ......
MyAspect2 before ......
MyAspect1 before ......
testing multiAspects
MyAspect1 afterReturning ......
MyAspect1 after ......
MyAspect2 afterReturning ......
MyAspect2 after ......
MyAspect3 afterReturning ......
MyAspect3 after ......

沒有留言:

張貼留言