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 製作這個流程
呼叫 interceptor 的 before()
呼叫 target object method
當 method throw exception,呼叫 afterThrowing()
當 method 正常執行完成,呼叫 afterReturning()
呼叫 after()
如果 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 ......
沒有留言:
張貼留言