1. 程式人生 > >Spring入門(十一):Spring AOP使用進階

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

在上篇部落格中,我們瞭解了什麼是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技術乾貨,讓我們一起進步。

相關推薦

Spring入門()Spring AOP使用

在上篇部落格中,我們瞭解了什麼是AOP以及在Spring中如何使用AOP,本篇部落格繼續深入講解下AOP的高階用法。 1. 宣告帶引數的切點 假設我們有一個介面CompactDisc和它的實現類BlankDisc: package chapter04.soundsystem; /** * 光碟 */ p

Spring Boot()Spring Boot中MongoDB的使用

Spring Boot(十一):Spring Boot中MongoDB的使用 mongodb是最早熱門非關係資料庫的之一,使用也比較普遍,一般會用做離線資料分析來使用,放到內網的居多。由於很多公司使用了雲服務,伺服器預設都開放了外網地址,導致前一陣子大批 MongoDB 因配置漏洞被攻擊,資料被刪,引起了人

Spring入門(二)Spring MVC使用講解

1. Spring MVC介紹 提到MVC,參與過Web應用程式開發的同學都很熟悉,它是展現層(也可以理解成直接展現給使用者的那一層)開發的一種架構模式,M全稱是Model,指的是資料模型,V全稱是View,指的是檢視頁面,如JSP、Thymeleaf等,C全稱是Controller,指的是控制器,用來處理使

Spring入門(四)Spring MVC控制器的2種測試方法

作為一名研發人員,不管你願不願意對自己的程式碼進行測試,都得承認測試對於研發質量保證的重要性,這也就是為什麼每個公司的技術部都需要質量控制部的原因,因為越早的發現程式碼的bug,成本越低,比如說,Dev環境發現bug的成本要低於QA環境,QA環境發現bug的成本要低於Prod環境,Prod環境發現bug的成本

springboot()Spring boot中mongodb的使用

gpo for 當前 window 公司 多表 erlang 大量 secondary mongodb是最早熱門非關系數據庫的之一,使用也比較普遍,一般會用做離線數據分析來使用,放到內網的居多。由於很多公司使用了雲服務,服務器默認都開放了外網地址,導致前一陣子大批 Mong

Spring Cloud 之訊息匯流排-

1. 簡介 Spring Cloud Bus links the nodes of a distributed system with a lightweight message broker. This broker can then be used to

Spring Security原始碼分析Spring Security OAuth2整合JWT

Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準(RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者

深入Spring Boot ()整合Redis詳解

Spring Boot為Redis的Lettuce和Jedis客戶端庫提供了基本的自動配置,並且可以使用Spring Data Redis完成更多的操作。本篇將介紹如何整合Redis及使用Redis實現簡單的查詢快取,主要包括以下7部分內容: 快取 Redi

Spring Boot2()Mybatis使用總結(自增長、多條件、批量操作、多表查詢等等)

一、前言 上次用Mybatis還是2017年做專案的時候,已經很久過去了。中途再沒有用過Mybatis。導致現在學習SpringBoot過程中遇到一些Mybatis的問題,以此做出總結(XML極簡模式)。當然只是實用方面的總結,具體就不深究♂了。這裡只總結怎麼用!!! (這次直接跳到十一,是因為中間是Rabb

Spring入門(五)使用Spring JDBC操作資料庫

在本系列的之前部落格中,我們從沒有講解過操作資料庫的方法,但是在實際的工作中,幾乎所有的系統都離不開資料的持久化,所以掌握操作資料庫的使用方法就非常重要。 在Spring中,操作資料庫有很多種方法,我們可以使用JDBC、Hibernate、MyBatis或者其他的資料持久化框架,本篇部落格的重點是講解下在Sp

SpringSpring事物

一個 簡單介紹 數據 數據庫操作 nag 原子性 pre spring pointcut 事務是訪問數據庫的一個操作序列,DB應用系統通過事務集來完成對數據的存取。 事務必須遵循4個原則,即常說的 ACID A,Automicity,原子性,即事務要麽被全部執行

spring入門筆記-()、spring boot HelloWorld

etc sample pid edi ren ref tin del result 什麽是spring boot Spring Boot是由Pivotal團隊提供的全新框架,其設計目的是用來簡化新Spring應用的初始搭建以及開發過程。該框架使用了特定的方式來進行配置,從而

Spring Cloud (四)Spring Cloud 開源軟體都有哪些?

學習一門新的技術如果有優秀的開源專案,對初學者的學習將會是事半功倍,通過研究和學習優秀的開源專案,可以快速的瞭解此技術的相關應用場景和應用示例,參考優秀開源專案會降低將此技術引入到專案中的成本。為此抽了一些時間為大家尋找了一些非常優秀的 Spring Cloud 開源軟體供大家學習參考。 上次寫了一篇文章Sp

Spring Boot(五)spring boot+jpa+thymeleaf增刪改查示例

app 配置文件 quest 重啟 fin nbu 生產 prot html Spring Boot(十五):spring boot+jpa+thymeleaf增刪改查示例 一、快速上手 1,配置文件 (1)pom包配置 pom包裏面添加jpa和thymeleaf的相關包引

Spring Boot 2()Spring Boot 2.0新特性

方案 oauth 2.0 hiberna 浪費 快的 ali 升級 log security Spring Boot 2(一):Spring Boot 2.0新特性 Spring Boot依賴於Spring,而Spring Cloud又依賴於Spring Boot,因此Sp

《partner4java 講述Spring入門》之spring cache支援(spring3.1如何使用cache 快取)

(以下內容參照自官方文件;p4jorm下載地址http://blog.csdn.net/partner4java/article/details/8629578;cache demo下載地址http://download.csdn.net/detail/partner4ja

spring mvc 小記()spring註解與java原註解

使用spring已有2年之久,卻還是停留在使用階段,感覺這麼下去不是辦法,所以還是想往深處一探究竟。 今天無意中查詢到java註解,才瞭解到原來那些框架裡的註解全是基於java所提供的元註解上編寫的,也就是說,我們自己也可以自定義註解。參考資料出處:http://blog.

Spring事務管理Spring事務管理的優點

(翻譯自spring-framework-reference.pdf 第四章 Data Access) 通常情況下,J2EE有2種事務管理方式:全域性事務和本地事務,2種事務都比較明顯的缺陷。 全域性事務:         全域性事務允許跨多個事務資源的事務管理(通常是關係

Spring入門()Spring AOP使用講解

1. 什麼是AOP? AOP是Aspect Oriented Programming的縮寫,意思是:面向切面程式設計,它是通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。 可以認為AOP是對OOP(Object Oriented Programming 面向物件程式設計)的補充,主要使用在日誌

Spring Boot教程()入門

ava 輸入 圖片 imp 測試 code 輸出 clas web 使用maven構建項目   1. 先在系統中安裝好開發環境,本教程使用Eclipse Photon 和 Java JDK 1.8 進行開發   2. 訪問連接https://start.spring.io/