AOP 與 註解的那些事兒~
- 2020 年 11 月 16 日
- 筆記
- JAVA, springboot, 碼猿技術專欄
持續原創輸出,點擊上方藍字關注我

目錄
-
前言 -
什麼是AOP? -
AOP的相關概念(面試常客) -
Spring Boot 如何整合AOP自定義一個註解? -
使用攔截器如何自定義註解? -
內部調用導致AOP註解失效 -
總結
前言
註解相信大家都用過,尤其是Spring Boot
這個框架,比如@Controller
。
這篇文章就來介紹下Spring Boot
中如何自定義一個註解,順帶介紹一下Spring Boot
與 AOP
如何整合。
什麼是AOP?
AOP
即是面向切面,是Spring
的核心功能之一,主要的目的即是針對業務處理過程中的橫向拓展,以達到低耦合的效果。
舉個栗子,項目中有記錄操作日誌的需求、或者流程變更是記錄變更履歷,無非就是插表操作,很簡單的一個save
操作,都是一些記錄日誌或者其他輔助性的程式碼。一遍又一遍的重寫和調用。不僅浪費了時間,又將項目變得更加的冗餘,實在得不償失。
此時AOP
的就該出場了,能夠在不改變原邏輯的基礎上實現相關功能。
AOP的相關概念(面試常客)
要理解Spring Boot
整合Aop
的實現,就必須先對面向切面實現的一些Aop
的概念有所了解,不然也是雲里霧裡。
「切面(Aspect)」:一個關注點的模組化。以註解@Aspect
的形式放在類上方,聲明一個切面。
「連接點(Joinpoint)」:在程式執行過程中某個特定的點,比如某方法調用的時候或者處理異常的時候都可以是連接點。
「通知(Advice)」:通知增強,需要完成的工作叫做通知,就是你寫的業務邏輯中需要比如事務、日誌等先定義好,然後需要的地方再去用。增強包括如下五個方面:
-
@Before
:在切點之前執行 -
@After
:在切點方法之後執行 -
@AfterReturning
:切點方法返回後執行 -
@AfterThrowing
:切點方法拋異常執行 -
@Around
:屬於環繞增強,能控制切點執行前,執行後,用這個註解後,程式拋異常,會影響@AfterThrowing
這個註解。
「切點(Pointcut)」:其實就是篩選出的連接點,匹配連接點的斷言,一個類中的所有方法都是連接點,但又不全需要,會篩選出某些作為連接點做為切點。
「引入(Introduction)」:在不改變一個現有類程式碼的情況下,為該類添加屬性和方法,可以在無需修改現有類的前提下,讓它們具有新的行為和狀態。其實就是把切面(也就是新方法屬性:通知定義的)用到目標類中去。
「目標對象(Target Object)」:被一個或者多個切面所通知的對象。也被稱做被通知(adviced
)對象。既然Spring AOP
是通過運行時代理實現的,這個對象永遠是一個被代理(proxied
)對象。
「AOP代理(AOP Proxy)」:AOP
框架創建的對象,用來實現切面契約(例如通知方法執行等等)。在Spring
中,AOP
代理可以是JDK
動態代理或者CGLIB
代理。
「織入(Weaving)」:把切面連接到其它的應用程式類型或者對象上,並創建一個被通知的對象。這些可以在編譯時(例如使用AspectJ
編譯器),類載入時和運行時完成。Spring
和其他純Java AOP
框架一樣,在運行時完成織入。
Spring Boot 如何整合AOP自定義一個註解?
在實際開發中對於橫向公共的邏輯需要抽取出來,這時候就需要使用AOP
,比如日誌的記錄、許可權的驗證等等,這些功能都可以用註解輕鬆的完成。
下面介紹如何在Spring Boot
使用AOP
定義一個註解。
添加依賴starter
AOP
整合Spring Boot
有一個starter
,只需要添加依賴即可,如下:
<!--springboot集成Aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
開啟AOP
在配置類上標註@EnableAspectJAutoProxy
註解即可開啟AOP
,這個註解有什麼用呢,源碼如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {}
最重要的是如下一行程式碼:
@Import(AspectJAutoProxyRegistrar.class)
@Import
這個註解很熟悉了吧,快速注入一個類,這裡是注入一個AnnotationAwareAspectJAutoProxyCreator
。
自定義一個註解
就以日誌處理為例子,定義一個日誌處理的註解,如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String value() default "";
}
定義一個切面
一個切面的滿足條件如下:
-
類上標註了 @Aspect
註解 -
注入到IOC容器中,比如 @Component
註解
定義的日誌切面如下:
@Component
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SysLogAspect {
}
@Order
指定了切面執行的優先順序,假如有多個切面,肯定是要有先後的執行順序,這樣才能保證邏輯性。
定義切點表達式
這裡需要攔截的肯定是@SysLog
這個註解,只要方法上標註了該註解都將會被攔截,表達式如下:
@Pointcut("@annotation(com.example.annotation_demo.annotation.SysLog)")
public void pointCut() {}
添加通知方法
既然是日誌記錄,肯定是在方法執行前,執行後都需要記錄,因此需要定義一個環繞通知,如下:
@Around("pointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//邏輯開始時間
long beginTime = System.currentTimeMillis();
//執行方法
Object result = point.proceed();
//todo,保存日誌,自己完善
saveLog(point,beginTime);
return result;
}
測試
以上配置完成後即可使用,只需要在需要的方法上標註@SysLog
註解即可,如下:
@SysLog
@PostMapping("/add")
public String add(){
return "";
}
使用攔截器如何自定義註解?
使用AOP
自定義的註解在每個方法上都會被攔截驗證,首先效率上就不高。
然而攔截器是在每個Controller
方法執行之前進行攔截,其他的方法都不會生效,比如service
方法。
比如許可權的驗證、防止瞬間重複點擊等等需求就適合使用攔截器自定義的註解。
自定義一個註解
就以防止瞬間重複點擊的例子來創建一個註解,如下:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 默認失效時間5秒
*/
long seconds() default 5;
}
自定義攔截器
需要在請求執行之前完成驗證,邏輯很簡單,就是判斷方法上有沒有標註@RepeatSubmit
註解,程式碼如下:
/**
* description:重複提交註解的攔截器
*/
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
//只攔截標註了@RepeatSubmit該註解
HandlerMethod handlerMethod=(HandlerMethod)handler;
//獲取controller方法上標註的註解
RepeatSubmit repeatSubmit = AnnotationUtils.findAnnotation(handlerMethod.getMethod(),RepeatSubmit.class);
//沒有限制重複提交,直接跳過
if (Objects.isNull(repeatSubmit))
return true;
//todo 一個值,標誌這個請求的唯一性,比如IP+userId+uri+請求參數
String flag="";
//存在即返回false,不存在即返回true
Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(flag, "", repeatSubmit.seconds(), TimeUnit.SECONDS);
if (ifAbsent!=null&&!ifAbsent)
//todo: 此處拋出異常,需要在全局異常解析器中捕獲
throw new RepeatSubmitException();
}
return true;
}
}
注入的攔截器
將上述自定義的攔截器注入到Sprign Boot
中,這裡不再演示了,前面教程有介紹過,請看:Spring Boot 第六彈,攔截器如何配置,看這兒~。
測試
在需要攔截方法上添加@RepeatSubmit
註解即可,如下:
@RepeatSubmit
@GetMapping("/add")
public String add(){
return "";
}
內部調用導致AOP註解失效
這個問題在事務中也是經常被忽略的問題,網上很多人說是AOP
的Bug
,其實在我看來這真不是一個BUG
,並且也是有辦法解決的。
先來看一下失效的案例,如下:
public class ArticleServiceImpl{
@SysLog
public void A(){
......
}
public void B(){
this.A();
}
}
在上述的程式碼中,如果執行方法B
,則@SysLog
註解將會失效。
失效的原因
AOP
使用的是動態代理的機制,它會給類生成一個代理類,事務的相關操作都在代理類上完成。內部方式使用this
調用方式時,使用的是實例調用,並沒有通過代理類調用方法,所以會導致事務失效。
解決方法
其實解決方法有很多,下面將會一一介紹。
1. 引入自身的Bean
在類內部通過@Autowired
將本身bean
引入,然後通過調用自身bean
,從而實現使用AOP
代理操作。程式碼如下:
public class ArticleServiceImpl{
/**
* 注入自身的Bean
*/
@Autowired
private ArticleService articleService;
@SysLog
public void A(){
......
}
public void B(){
articleService.A();
}
}
2. 通過ApplicationContext引入bean
通過ApplicationContext
獲取bean
,通過bean
調用內部方法,就使用了bean
的代理類。
需要先創建一個ApplicationContext
的工具類獲取ApplicationContext
,然後才能調用getBean()
方法,程式碼如下:
public class ArticleServiceImpl{
@SysLog
public void A(){
......
}
public void B(){
ApplicationContextUtils.getApplicationContext().getBean(ArticleService.class).A();
}
}
3. 通過AopContext獲取當前類的代理類
此種方法需要設置@EnableAspectJAutoProxy
中的exposeProxy
為true
。
使用AopContext
獲取當前的代理對象,程式碼如下:
public class ArticleServiceImpl{
@SysLog
public void A(){
......
}
public void B(){
((ArticleService)AopContext.currentProxy()).A();
}
}
總結
這篇文章介紹了AOP
的相關概念、AOP
實現自定義註解以及攔截器實現自定義註解,都是日常開發中必備的知識點,希望這篇文章對各位有所幫助。
源碼已經上傳,回復關鍵詞
AOP註解
獲取。
最後,別忘了點贊哦!!!
另外作者的第一本PDF
書籍已經整理好了,由淺入深的詳細介紹了Mybatis基礎以及底層源碼,有需要的朋友回復關鍵詞「Mybatis進階」即可獲取,目錄如下:

