spring-boot-aop
什么是aop
面向切面的程序設(shè)計(Aspect-oriented programming,AOP,又譯作面向方面的程序設(shè)計、剖面導(dǎo)向程序設(shè)計)是計算機(jī)科學(xué)中的一種程序設(shè)計思想,旨在將橫切關(guān)注點(diǎn)與業(yè)務(wù)主體進(jìn)行進(jìn)一步分離,以提高程序代碼的模塊化程度。通過在現(xiàn)有代碼基礎(chǔ)上增加額外的通知(Advice)機(jī)制,能夠?qū)Ρ宦暶鳛椤扒悬c(diǎn)(Pointcut)”的代碼塊進(jìn)行統(tǒng)一管理與裝飾,如“對所有方法名以‘set*’開頭的方法添加后臺日志”。該思想使得開發(fā)人員能夠?qū)⑴c代碼核心業(yè)務(wù)邏輯關(guān)系不那么密切的功能(如日志功能)添加至程序中,同時又不降低業(yè)務(wù)代碼的可讀性。面向切面的程序設(shè)計思想也是面向切面軟件開發(fā)的基礎(chǔ)。
面向切面的程序設(shè)計將代碼邏輯切分為不同的模塊(即關(guān)注點(diǎn)(Concern),一段特定的邏輯功能)。幾乎所有的編程思想都涉及代碼功能的分類,將各個關(guān)注點(diǎn)封裝成獨(dú)立的抽象模塊(如函數(shù)、過程、模塊、類以及方法等),后者又可供進(jìn)一步實(shí)現(xiàn)、封裝和重寫。部分關(guān)注點(diǎn)“橫切”程序代碼中的數(shù)個模塊,即在多個模塊中都有出現(xiàn),它們即被稱作“橫切關(guān)注點(diǎn)(Cross-cutting concerns, Horizontal concerns)”。
日志功能即是橫切關(guān)注點(diǎn)的一個典型案例,因?yàn)槿罩竟δ芡鶛M跨系統(tǒng)中的每個業(yè)務(wù)模塊,即“橫切”所有有日志需求的類及方法體。而對于一個信用卡應(yīng)用程序來說,存款、取款、帳單管理是它的核心關(guān)注點(diǎn),日志和持久化將成為橫切整個對象結(jié)構(gòu)的橫切關(guān)注點(diǎn)。
切面的概念源于對面向?qū)ο蟮某绦蛟O(shè)計的改進(jìn),但并不只限于此,它還可以用來改進(jìn)傳統(tǒng)的函數(shù)。與切面相關(guān)的編程概念還包括元對象協(xié)議、主題(Subject)、混入(Mixin)和委托(Delegate)。
AOP中的相關(guān)概念
看過了上面解釋,想必大家對aop已經(jīng)有個大致的雛形了,但是又對上面提到的切面之類的術(shù)語有一些模糊的地方,接下來就來講解一下AOP中的相關(guān)概念,了解了AOP中的概念,才能真正的掌握AOP的精髓。
Aspect(切面): Aspect 聲明類似于 Java 中的類聲明,在 Aspect 中會包含著一些 Pointcut 以及相應(yīng)的 Advice。
Joint point(連接點(diǎn)):表示在程序中明確定義的點(diǎn),典型的包括方法調(diào)用,對類成員的訪問以及異常處理程序塊的執(zhí)行等等,它自身還可以嵌套其它 joint point。
Pointcut(切點(diǎn)):表示一組 joint point,這些 joint point 或是通過邏輯關(guān)系組合起來,或是通過通配、正則表達(dá)式等方式集中起來,它定義了相應(yīng)的 Advice 將要發(fā)生的地方。
Advice(增強(qiáng)):Advice 定義了在 Pointcut 里面定義的程序點(diǎn)具體要做的操作,它通過 before、after 和 around 來區(qū)別是在每個 joint point 之前、之后還是代替執(zhí)行的代碼。
Target(目標(biāo)對象):織入 Advice 的目標(biāo)對象.。
Weaving(織入):將 Aspect 和其他對象連接起來, 并創(chuàng)建 Adviced object 的過程
spring aop
Spring AOP使用純Java實(shí)現(xiàn),它不需要專門的編譯過程,也不需要特殊的類裝載器,它在運(yùn)行期通過代理方式向目標(biāo)類織入增強(qiáng)代碼。在Spring中可以無縫地將Spring AOP、IoC和AspectJ整合在一起。Spring AOP構(gòu)建在動態(tài)代理基礎(chǔ)之上,因此,Spring對AOP的支持局限于方法攔截。在Java中動態(tài)代理有兩種方式:JDK動態(tài)代理和CGLib動態(tài)代理
- jdk proxy
java動態(tài)代理是利用反射機(jī)制生成一個實(shí)現(xiàn)代理接口的匿名類,在調(diào)用具體方法前調(diào)用InvokeHandler來處理。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* <p>
*
* @author leone
* @since 2018-11-09
**/
public class JdkProxy {
interface IUserService {
Integer delete(Integer userId);
}
static class UserServiceImpl implements IUserService {
@Override
public Integer delete(Integer userId) {
// 業(yè)務(wù)
System.out.println("delete user");
return userId;
}
}
// 自定義InvocationHandler
static class UserServiceProxy implements InvocationHandler {
// 目標(biāo)對象
private Object target;
public UserServiceProxy(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------方法調(diào)用前---------");
//執(zhí)行相應(yīng)的目標(biāo)方法
Object result = method.invoke(target, args);
System.out.println("------方法調(diào)用后---------");
return result;
}
}
public static void main(String[] args) {
IUserService userService = new UserServiceImpl();
// 創(chuàng)建調(diào)用處理類
UserServiceProxy handler = new UserServiceProxy(userService);
// 得到代理類實(shí)例
IUserService proxy = (IUserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(),
new Class[]{IUserService.class}, handler);
// 調(diào)用代理類的方法
Integer userId = proxy.delete(3);
System.out.println(userId);
}
}
- cglib proxy
而cglib動態(tài)代理是利用asm開源包,對代理對象類的class文件加載進(jìn)來,通過修改其字節(jié)碼生成子類來處理。
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* <p>
*
* @author leone
* @since 2018-11-09
**/
public class CglibProxy {
static class UserService implements MethodInterceptor {
private Object target;
/**
* 業(yè)務(wù)方法
*
* @param userId
* @return
*/
public Integer delete(Integer userId) {
System.out.println("delete user");
return userId;
}
/**
* 利用Enhancer類生成代理類
*
* @param target
* @return
*/
public Object getInstance(Object target) {
this.target = target;
// 創(chuàng)建加強(qiáng)器,用來創(chuàng)建動態(tài)代理類
Enhancer enhancer = new Enhancer();
// 為加強(qiáng)器指定要代理的業(yè)務(wù)類(即:為下面生成的代理類指定父類)
enhancer.setSuperclass(target.getClass());
// 設(shè)置回調(diào):對于代理類上所有方法的調(diào)用,都會調(diào)用CallBack,而Callback則需要實(shí)現(xiàn)intercept()方法進(jìn)行攔
enhancer.setCallback(this);
// 創(chuàng)建動態(tài)代理類對象并返回
return enhancer.create();
}
/**
* @param o
* @param method
* @param objects
* @param methodProxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable {
System.out.println("------方法調(diào)用前---------");
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("------方法調(diào)用后---------");
return object;
}
}
public static void main(String[] args) {
UserService userService = new UserService();
UserService proxy = (UserService) userService.getInstance(userService);
Integer userId = proxy.delete(2);
System.out.println(userId);
}
}
1、如果目標(biāo)對象實(shí)現(xiàn)了接口,默認(rèn)情況下會采用JDK的動態(tài)代理實(shí)現(xiàn)AOP,可以強(qiáng)制使用CGLIB實(shí)現(xiàn)AOP
2、如果目標(biāo)對象沒有實(shí)現(xiàn)了接口,必須采用CGLIB庫,spring會自動在JDK動態(tài)代理和CGLIB之間轉(zhuǎn)換
spirng boot aop
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Advice的主要類型
@Before:該注解標(biāo)注的方法在業(yè)務(wù)模塊代碼執(zhí)行之前執(zhí)行,其不能阻止業(yè)務(wù)模塊的執(zhí)行,除非拋出異常;
@AfterReturning:該注解標(biāo)注的方法在業(yè)務(wù)模塊代碼執(zhí)行之后執(zhí)行;
@AfterThrowing:該注解標(biāo)注的方法在業(yè)務(wù)模塊拋出指定異常后執(zhí)行;
@After:該注解標(biāo)注的方法在所有的Advice執(zhí)行完成后執(zhí)行,無論業(yè)務(wù)模塊是否拋出異常,類似于finally的作用;
@Around:該注解功能最為強(qiáng)大,其所標(biāo)注的方法用于編寫包裹業(yè)務(wù)模塊執(zhí)行的代碼,其可以傳入一個ProceedingJoinPoint用于調(diào)用業(yè)務(wù)模塊的代碼,無論是調(diào)用前邏輯還是調(diào)用后邏輯,都可以在該方法中編寫,甚至其可以根據(jù)一定的條件而阻斷業(yè)務(wù)模塊的調(diào)用;
@DeclareParents:其是一種Introduction類型的模型,在屬性聲明上使用,主要用于為指定的業(yè)務(wù)模塊添加新的接口和相應(yīng)的實(shí)現(xiàn)。
切點(diǎn)表達(dá)式
1.通配符
[*] 匹配任意字符,但只能匹配一個元素
[..] 匹配任意字符,可以匹配任意多個元素,表示類時,必須和*聯(lián)合使用
[+] 必須跟在類名后面,如Horseman+,表示類本身和繼承或擴(kuò)展指定類的所有類
2.邏輯運(yùn)算符
表達(dá)式可由多個切點(diǎn)函數(shù)通過邏輯運(yùn)算組成
- && 與操作,求交集,也可以寫成and
例如 execution(* chop(..)) && target(Horseman) 表示Horseman及其子類的chop方法
- || 或操作,任一表達(dá)式成立即為true,也可以寫成 or
例如 execution(* chop(..)) || args(String) 表示名稱為chop的方法或者有一個String型參數(shù)的方法
- ! 非操作,表達(dá)式為false則結(jié)果為true,也可以寫成 not
例如 execution(* chop(..)) and !args(String) 表示名稱為chop的方法但是不能是只有一個String型參數(shù)的方法
- execution() 方法匹配模式串
表示滿足某一匹配模式的所有目標(biāo)類方法連接點(diǎn)。如execution(* save(..))表示所有目標(biāo)類中的 save()方法。
由于Spring切面粒度最小是達(dá)到方法級別,而execution表達(dá)式可以用于明確指定方法返回類型,類名,方法名和參數(shù)名等與方法相關(guān)的部件,并且在Spring中,大部分需要使用AOP的業(yè)務(wù)場景也只需要達(dá)到方法級別即可,因而execution表達(dá)式的使用是最為廣泛的。如下是execution表達(dá)式的語法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(<修飾符> <返回類型> <類路徑> <方法名>(<參數(shù)列表>) <異常模式> )
modifiers-pattern:方法的可見性,如public,protected;
ret-type-pattern:方法的返回值類型,如int,void等;
declaring-type-pattern:方法所在類的全路徑名,如com.spring.Aspect;
name-pattern:方法名類型,如buisinessService();
param-pattern:方法的參數(shù)類型,如java.lang.String;
throws-pattern:方法拋出的異常類型,如java.lang.Exception;
切點(diǎn)函數(shù)
-
@annotation(annotation-type) 方法注解類名
如下示例表示匹配使用com.leone.aop.AopTest注解標(biāo)注的方法:
@annotation(com.leone.aop.AopTest)
-
args(param-pattern) 方法入?yún)⑶悬c(diǎn)函數(shù)
如下示例表示匹配所有只有一個參數(shù),并且參數(shù)類型是java.lang.String類型的方法:
args(java.lang.String)
-
@args(annotation-type) 方法入?yún)㈩愖⒔馇悬c(diǎn)函數(shù)
如下示例表示匹配使用了com.leone.aop.AopTest注解標(biāo)注的類作為參數(shù)的方法:
@args(com.leone.aop.AopTest)
-
within(declaring-type-pattern) 類名匹配切點(diǎn)函數(shù)
within表達(dá)式只能指定到類級別,如下示例表示匹配com.leone.aop.UserService中的所有方法:
within(com.leone.aop.UserService)
-
@within(annotation-type) 類注解匹配切點(diǎn)函數(shù)
如下示例表示匹配使用org.springframework.web.bind.annotation.RestController注解標(biāo)注的類:
@within(org.springframework.web.bind.annotation.RestController)
-
target(declaring-type-pattern) 類名切點(diǎn)函數(shù)
如下示例表示匹配com.leone.aop.UserService中的所有方法:
target(com.leone.aop.UserService)
this
spring-boot-aop 實(shí)戰(zhàn)
- 配置切面類,實(shí)現(xiàn)代理
1.在類上使用 @Component 注解把切面類加入到IOC容器中
2.在類上使用 @Aspect 注解使之成為切面類
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* 描述一個切面類
*
* @author leone
* @since 2018-06-21
**/
@Slf4j
@Aspect
@Component
public class AopConfig {
/**
* 1.通配符
* [*] 匹配任意字符,但只能匹配一個元素
* <p>
* [..] 匹配任意字符,可以匹配任意多個元素,表示類時,必須和*聯(lián)合使用
* <p>
* [+] 必須跟在類名后面,如Horseman+,表示類本身和繼承或擴(kuò)展指定類的所有類
* <p>
* 切點(diǎn)表達(dá)式分為 修飾符 返回類型 包路徑 方法名 參數(shù)
* <p>
* 2.切點(diǎn)表達(dá)式
* <p>
* 3.邏輯運(yùn)算符
* 表達(dá)式可由多個切點(diǎn)函數(shù)通過邏輯運(yùn)算組成
* ** && 與操作,求交集,也可以寫成and
* <p>
* 例如 execution(* chop(..)) && target(Horseman) 表示Horseman及其子類的chop方法
* <p>
* ** || 或操作,任一表達(dá)式成立即為true,也可以寫成 or
* <p>
* 例如 execution(* chop(..)) || args(String) 表示名稱為chop的方法或者有一個String型參數(shù)的方法
* <p>
* ** ! 非操作,表達(dá)式為false則結(jié)果為true,也可以寫成 not
* <p>
* 例如 execution(* chop(..)) and !args(String) 表示名稱為chop的方法但是不能是只有一個String型參數(shù)的方法
*/
@Pointcut("execution(* com.leone.boot.aop.service.*.*(..))")
public void pointCut() {
}
/**
* 環(huán)繞通知在 target 開始和結(jié)束執(zhí)行
*
* @param point
* @return
*/
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint point) {
long start = System.currentTimeMillis();
String methodName = point.getSignature().getName();
log.info("around method name: {} params: {}", methodName, Arrays.asList(point.getArgs()));
try {
log.info("around end time: {}", (System.currentTimeMillis() - start) + " ms!");
return point.proceed();
} catch (Throwable e) {
log.error("message: {}", e.getMessage());
}
return null;
}
/**
* 前置通知在 target 前執(zhí)行
*
* @param joinPoint
*/
// @Before("@annotation(com.leone.boot.aop.anno.AopBefore)")
// @Before("within(com.leone.boot.aop.controller.*)")
// @Before("@within(org.springframework.web.bind.annotation.RestController)")
// @Before("target(com.leone.boot.aop.controller.UserController)")
@Before("@target(com.leone.boot.aop.anno.ClassAop) && @annotation(com.leone.boot.aop.anno.AopBefore)")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
log.info("before inform method name: {} param: {}", methodName, args);
}
/**
* 后置通知在target后執(zhí)行
*
* @param joinPoint
*/
@After("@args(org.springframework.stereotype.Component) &&
execution(* com.leone.boot.aop.controller.*.*(..))")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
log.info("after inform method name : {} param: {}", methodName, args);
}
/**
* 后置返回在target返回后執(zhí)行
*
* @param joinPoint
* @param result
*/
@AfterReturning(value = "within(com.leone.boot.aop.controller.*)", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
log.info("afterReturning inform method name: {} return value: {}", methodName, result);
}
/**
* 后置異常通知在target異常后執(zhí)行
*
* @param joinPoint
* @param ex
*/
@AfterThrowing(value = "args(com.leone.boot.common.entity.User) &&
execution(* com.leone.boot.aop.controller.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
log.info("afterThrowing inform method name: {} exceptions: {}" + methodName, ex);
}
}
- 測試類
import com.leone.boot.aop.anno.AopBefore;
import com.leone.boot.aop.anno.ClassAop;
import com.leone.boot.aop.interf.UserService;
import com.leone.boot.common.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @author leone
* @since 2018-06-21
**/
@Slf4j
@ClassAop
@RestController
@RequestMapping("/api")
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@AopBefore
@RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
public User findOne(@PathVariable Long userId) {
return userService.findOne(userId);
}
@AopBefore
@RequestMapping("/user")
public User save(User user) {
return user;
}
}