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

在上篇博客中,咱們瞭解了什麼是AOP以及在Spring中如何使用AOP,本篇博客繼續深刻講解下AOP的高級用法。java

1. 聲明帶參數的切點

假設咱們有一個接口CompactDisc和它的實現類BlankDisc:git

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類的基礎上,實現相同的功能。github

首先,新建切面SongCounter以下所示:spring

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:json

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自動代理。ide

2)切面SongCounter要被聲明bean,不然切面不會生效。測試

最後,新建測試類SongCounterTest以下所示:ui

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. 源碼及參考

源碼地址:github.com/zwwhnly/spr…,歡迎下載。

Craig Walls 《Spring實戰(第4版)》

汪雲飛《Java EE開發的顛覆者:Spring Boot實戰》

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

最後,歡迎關注個人微信公衆號:「申城異鄉人」,全部博客會同步更新。

相關文章
相關標籤/搜索