2026/7/6

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 ......