在上篇博客中,我們了解了什么是AOP以及在Spring中如何使用AOP,本篇博客繼續深入講解下AOP的高級用法。
1. 聲明帶參數的切點
假設我們有一個接口CompactDisc和它的實現類BlankDisc:
package chapter04.soundsystem;
/**
* 光盤
*/
public interface CompactDisc {
void play();
void play(int songNumber);
}
package chapter04.soundsystem;
import java.util.List;
/**
* 空白光盤
*/
public class BlankDisc implements CompactDisc {
/**
* 唱片名稱
*/
private String title;
/**
* 藝術家
*/
private String artist;
/**
* 唱片包含的歌曲集合
*/
private List<String> songs;
public BlankDisc(String title, String artist, List<String> songs) {
this.title = title;
this.artist = artist;
this.songs = songs;
}
@Override
public void play() {
System.out.println("Playing " + title + " by " + artist);
for (String song : songs) {
System.out.println("-Song:" + song);
}
}
/**
* 播放某首歌曲
*
* @param songNumber
*/
@Override
public void play(int songNumber) {
System.out.println("Play Song:" + songs.get(songNumber - 1));
}
}
現在我們的需求是記錄每首歌曲的播放次數,按照以往的做法,我們可能會修改BlankDisc類的邏輯,在播放每首歌曲的代碼處增加記錄播放次數的邏輯,但現在我們使用切面,在不修改BlankDisc類的基礎上,實現相同的功能。
首先,新建切面SongCounter如下所示:
package chapter04.soundsystem;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.util.HashMap;
import java.util.Map;
@Aspect
public class SongCounter {
private Map<Integer, Integer> songCounts = new HashMap<>();
/**
* 可重用的切點
*
* @param songNumber
*/
@Pointcut("execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber)")
public void songPlayed(int songNumber) {
}
@Before("songPlayed(songNumber)")
public void countSong(int songNumber) {
System.out.println("播放歌曲計數:" + songNumber);
int currentCount = getPlayCount(songNumber);
songCounts.put(songNumber, currentCount + 1);
}
/**
* 獲取歌曲播放次數
*
* @param songNumber
* @return
*/
public int getPlayCount(int songNumber) {
return songCounts.getOrDefault(songNumber, 0);
}
}
重點關注下切點表達式execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber)
,其中int代表參數類型,songNumber代表參數名稱。
新建配置類SongCounterConfig:
package chapter04.soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableAspectJAutoProxy
public class SongCounterConfig {
@Bean
public CompactDisc yehuimei() {
List<String> songs = new ArrayList<>();
songs.add("東風破");
songs.add("以父之名");
songs.add("晴天");
songs.add("三年二班");
songs.add("你聽得到");
BlankDisc blankDisc = new BlankDisc("葉惠美", "周傑倫", songs);
return blankDisc;
}
@Bean
public SongCounter songCounter() {
return new SongCounter();
}
}
注意事項:
1)配置類要添加@EnableAspectJAutoProxy
注解啟用AspectJ自動代理。
2)切面SongCounter要被聲明bean,否則切面不會生效。
最后,新建測試類SongCounterTest如下所示:
package chapter04.soundsystem;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SongCounterConfig.class)
public class SongCounterTest {
@Autowired
private CompactDisc compactDisc;
@Autowired
private SongCounter songCounter;
@Test
public void testSongCounter() {
compactDisc.play(1);
compactDisc.play(2);
compactDisc.play(3);
compactDisc.play(3);
compactDisc.play(3);
compactDisc.play(3);
compactDisc.play(5);
compactDisc.play(5);
assertEquals(1, songCounter.getPlayCount(1));
assertEquals(1, songCounter.getPlayCount(2));
assertEquals(4, songCounter.getPlayCount(3));
assertEquals(0, songCounter.getPlayCount(4));
assertEquals(2, songCounter.getPlayCount(5));
}
}
運行測試方法testSongCounter(),測試通過,輸出結果如下所示:
播放歌曲計數:1
Play Song:東風破
播放歌曲計數:2
Play Song:以父之名
播放歌曲計數:3
Play Song:晴天
播放歌曲計數:3
Play Song:晴天
播放歌曲計數:3
Play Song:晴天
播放歌曲計數:3
Play Song:晴天
播放歌曲計數:5
Play Song:你聽得到
播放歌曲計數:5
Play Song:你聽得到
2. 限定匹配帶有指定注解的連接點
在之前我們聲明的切點中,切點表達式都是使用全限定類名和方法名匹配到某個具體的方法,但有時候我們需要匹配到使用某個注解的所有方法,此時就可以在切點表達式使用@annotation來實現,注意和之前在切點表達式中使用execution的區別。
為了更好的理解,我們還是通過一個具體的例子來講解。
首先,定義一個注解Action:
package chapter04;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Action {
String name();
}
然后定義2個使用@Action注解的方法:
package chapter04;
import org.springframework.stereotype.Service;
@Service
public class DemoAnnotationService {
@Action(name = "注解式攔截的add操作")
public void add() {
System.out.println("DemoAnnotationService.add()");
}
@Action(name = "注解式攔截的plus操作")
public void plus() {
System.out.println("DemoAnnotationService.plus()");
}
}
接着定義切面LogAspect:
package chapter04;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class LogAspect {
@Pointcut("@annotation(chapter04.Action)")
public void annotationPointCut() {
}
@After("annotationPointCut()")
public void after(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Action action = method.getAnnotation(Action.class);
System.out.println("注解式攔截 " + action.name());
}
}
注意事項:
1)切面使用了@Component
注解,以便Spring能自動掃描到並創建為bean,如果這里不添加該注解,也可以通過Java配置或者xml配置的方式將該切面聲明為一個bean,否則切面不會生效。
2)@Pointcut("@annotation(chapter04.Action)")
,這里我們在定義切點時使用了@annotation來指定某個注解,而不是之前使用execution來指定某些或某個方法。
我們之前使用的切面表達式是execution(* chapter04.concert.Performance.perform(..))
是匹配到某個具體的方法,如果想匹配到某些方法,可以修改為如下格式:
execution(* chapter04.concert.Performance.*(..))
然后,定義配置類AopConfig:
package chapter04;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AopConfig {
}
注意事項:配置類需要添加
@EnableAspectJAutoProxy
注解啟用AspectJ自動代理,否則切面不會生效。
最后新建Main類,在其main()方法中添加如下測試代碼:
package chapter04;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
DemoAnnotationService demoAnnotationService = context.getBean(DemoAnnotationService.class);
demoAnnotationService.add();
demoAnnotationService.plus();
context.close();
}
}
輸出結果如下所示:
DemoAnnotationService.add()
注解式攔截 注解式攔截的add操作
DemoAnnotationService.plus()
注解式攔截 注解式攔截的plus操作
可以看到使用@Action注解的add()和plus()方法在執行完之后,都執行了切面中定義的after()方法。
如果再增加一個使用@Action注解的subtract()方法,執行完之后,也會執行切面中定義的after()方法。
3. 項目中的實際使用
在實際的使用中,切面很適合用來記錄日志,既滿足了記錄日志的需求又讓日志代碼和實際的業務邏輯隔離開了,
下面看下具體的實現方法。
首先,聲明一個訪問日志的注解AccessLog:
package chapter04.log;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 訪問日志 注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLog {
boolean recordLog() default true;
}
然后定義訪問日志的切面AccessLogAspectJ:
package chapter04.log;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AccessLogAspectJ {
@Pointcut("@annotation(AccessLog)")
public void accessLog() {
}
@Around("accessLog()")
public void recordLog(ProceedingJoinPoint proceedingJoinPoint) {
try {
Object object = proceedingJoinPoint.proceed();
AccessLog accessLog = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getAnnotation(AccessLog.class);
if (accessLog != null && accessLog.recordLog() && object != null) {
// 這里只是打印出來,一般實際使用時都是記錄到公司的日志中心
System.out.println("方法名稱:" + proceedingJoinPoint.getSignature().getName());
System.out.println("入參:" + JSON.toJSONString(proceedingJoinPoint.getArgs()));
System.out.println("出參:" + JSON.toJSONString(object));
}
} catch (Throwable throwable) {
// 這里可以記錄異常日志到公司的日志中心
throwable.printStackTrace();
}
}
}
上面的代碼需要在pom.xml中添加如下依賴:
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
然后定義配置類LogConfig:
package chapter04.log;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class LogConfig {
}
注意事項:不要忘記添加@EnableAspectJAutoProxy注解,否則切面不會生效。
然后,假設你的對外接口是下面這樣的:
package chapter04.log;
import org.springframework.stereotype.Service;
@Service
public class MockService {
@AccessLog
public String mockMethodOne(int index) {
return index + "MockService.mockMethodOne";
}
@AccessLog
public String mockMethodTwo(int index) {
return index + "MockService.mockMethodTwo";
}
}
因為要記錄日志,所以每個方法都添加了@AccessLog注解。
最后新建Main類,在其main()方法中添加如下測試代碼:
package chapter04.log;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LogConfig.class);
MockService mockService = context.getBean(MockService.class);
mockService.mockMethodOne(1);
mockService.mockMethodTwo(2);
context.close();
}
}
輸出日志如下所示:
方法名稱:mockMethodOne
入參:[1]
出參:"1MockService.mockMethodOne"
方法名稱:mockMethodTwo
入參:[2]
出參:"2MockService.mockMethodTwo"
如果某個方法不需要記錄日志,可以不添加@AccessLog注解:
public String mockMethodTwo(int index) {
return index + "MockService.mockMethodTwo";
}
也可以指定recordLog為false:
@AccessLog(recordLog = false)
public String mockMethodTwo(int index) {
return index + "MockService.mockMethodTwo";
}
這里只是舉了個簡單的記錄日志的例子,大家也可以把切面應用到記錄接口耗時等更多的場景。
4. 源碼及參考
源碼地址:https://github.com/zwwhnly/spring-action.git,歡迎下載。
Craig Walls 《Spring實戰(第4版)》
汪雲飛《Java EE開發的顛覆者:Spring Boot實戰》
原創不易,如果覺得文章能學到東西的話,歡迎點個贊、評個論、關個注,這是我堅持寫作的最大動力。
如果有興趣,歡迎添加我的微信:zwwhnly,等你來聊技術、職場、工作等話題(PS:我是一名奮斗在上海的程序員)。