Spring入門(十一):Spring AOP使用進階

  • 2019 年 10 月 3 日
  • 筆記

在上篇部落格中,我們了解了什麼是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實戰》

AOP(面向切面編程)_百度百科

5. 最後

歡迎掃碼關注微信公眾號:「申城異鄉人」,定期分享Java技術乾貨,讓我們一起進步。