1. 程式人生 > >Spring Boot 2.0 讀書筆記_02:AOP

Spring Boot 2.0 讀書筆記_02:AOP

寫在開頭,自該專欄建立起,9月初發布第一篇文章後,兩個月過去了,專欄文章沒啥進度。處於個人實習原因,以及同步的Vue專欄,所以關於SpringBoot 2.0 的讀書筆記專欄暫時擱置了。

雖然部落格專欄擱置更新,但是技術的使用每天都在使用。基於Spring Boot 2.0 的新特性也即將進行更新,主要參考《Spring Boot 2精髓》以及碼雲上維護的配套案例Demo。 詳情:https://gitee.com/xiandafu/Spring-Boot-2.0-Samples

李家智,《Spring Boot 2精髓》作者,Beetl模板引擎作者

形式上以讀書筆記的形式進行,針對作者在碼雲上的配套案例進行分析學習,主要重溫下Spring框架以及SpringBoot 2.0 的新特性學習。

讀書筆記建立在本人技術認知以上進行記錄,廢話不多數,開始第一章:AOP

1. AOP:面向切面程式設計

談到AOP,我們就會想到很多實際場景。比如,日誌、事務、公共操作(快取、資料庫)等,所謂面向切面程式設計就是在原有的縱向業務處理過程中新增一個切面,切入些公有、特定的處理操作。

我們可以這樣認為:AOP將應用中的業務邏輯和系統級服務進行了分離,採用AOP可以在應用過程中動態的在需要的地方進行"織入"公共服務程式碼。

之前在學習Guns腳手架工具時候,遇到了很多AOP的例子,日誌記錄、資料來源切換、許可權控制等。那麼在這裡結合配套案例中的ch1、ch2進行分析學習,下面是案例目錄結構:
在這裡插入圖片描述


在我閱讀這本書的時候,開篇來了個訪問控制的例子,的確很開門見山啊,我們也結合這個例子進行詳細的分析。

首先入門前我們需要了解幾個基本概念:

名稱 含義
Aspect(切面) 通常是一個類,裡面可以定義切入點和通知。
JointPoint(連線點) 程式執行過程中明確的點,一般是方法的呼叫。
Advice(通知) AOP在特定的切入點上執行的增強處理,有before,after,afterReturning,afterThrowing,around。
Pointcut(切入點) 帶有通知的連線點,在程式中主要體現為書寫切入點表示式。
AOP代理 AOP框架建立的物件,代理就是目標物件的加強。Spring中的AOP代理可以使JDK動態代理,也可以是CGLIB代理,前者基於介面,後者基於子類。

關於ch1、ch2,由於ch2案例中的內容在ch1中以及有所體現,這裡整合到一起進行說明。

案例核心是:認證控制,我們對Function、RoleAcessConfig、以及HelloworldController進行分析。

HelloworldController:控制器,請求處理

@RequestMapping("/sayhello.html")
@Function()
public @ResponseBody String say(String name){
	return "hello "+name;
}

常規控制器方法:@RequestMapping 標識請求對映路徑,@ResponseBody 標識將controller的方法返回的物件通過適當的轉換器轉換為指定的格式之後,寫入到response物件,常用來返回JSON資料。

那麼這個 @Function() 是什麼東東呢?--------- 自定義註解,請向下看:

Function:自定義註解

在解決這個問題之前,我們先來了解一下: 註解

Annotation(註解)就是Java提供了一種元程式中的元素關聯任何資訊和著任何元資料(metadata)的途徑和方法。
Annotion(註解)是一個介面,程式可以通過反射來獲取指定程式元素的Annotion物件,然後通過Annotion物件來獲取註解裡面的元資料。

元資料(metadata):關於資料的資料,可以用來建立文件,跟蹤程式碼的依賴性,執行編譯時格式檢查,代替已有的配置檔案。

根據使用方法和用途對註解分類:系統註解,元註解,自定義註解

系統註解:

1. @Override:用於重寫父類的方法 或者是寫介面實現類時用到該註解。
2. @Deprecated:用於表示該方法是一個過期的方法。
3. @suppressWarnings:表示該方法在編譯時自動忽略警告。

元註解:

1. @Target:用於描述註解的使用範圍。
    ElementType取值:
    1. CONSTRUCTOR:用於描述構造器
    2. FIELD:用於描述域
    3. LOCAL_VARIABLE:用於描述區域性變數
    4. METHOD:用於描述方法
    5. PACKAGE:用於描述包
    6. PARAMETER:用於描述引數
    7. TYPE:用於描述類、介面(包括註解型別) 或enum宣告
    例:@Target({ElementType.METHOD}) 用於描述方法。
2. @Retention:用於描述註解的生命週期。
    RetentionPoicy取值
    1. SOURCE:在原始檔中有效
    2. CLASS:在class檔案中有效
    3. RUNTIME:在執行時有效
    例:@Retention(RetentionPolicy.RUNTIME) 執行時有效
3. @Documented:宣告需要加入JavaDoc。
4. @Inhrited:表明了被標註的型別是被繼承的。
   如果一個使用了@Inherited修飾的annotation型別被用於一個class,則這個annotation將被用於該class的子類。

自定義註解:@interface用來宣告一個自定義註解,自動繼承java.lang.annotation.Annotation介面。

自定義註解格式:public @interface 註解名{註解體}
自定義註解體格式:基本資料型別/String/Class/enum/Annotation
例:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    String value() default "";
}
注意:自定義註解訪問修飾符只有public和default。

這裡給出一個關於元註解 @Inhrited 的小例子

首先我們自定義一個註解:具體自定義註解內容會在下面指出

//自定義註解:標註被繼承、執行時有效           
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    String value() default "";
}
//測試類
public class Test1 {

    @TestAnnotation
    class A{}

	// B 繼承 A 類
    class B extends A{}

    public static void main(String[] args) {
        System.out.println(B.class.isAnnotationPresent(TestAnnotation.class));
    }
}

測試結果:true 證實上述說法

這裡給出關於註解的思維導圖:

那麼我們再看Function 這個自定義註解

@Retention(RetentionPolicy.RUNTIME)
public @interface Function {
	public String value() default "";
}

執行時生效,註解引數value,型別String,預設值" ",分析完畢 over

註解 Function 是自定義一個註解,接受一個字串,表示 Controller 方法對應的業務功能
使用者是否能訪問" user.add " 功能將在資料庫中配置。

RoleAcessConfig:切面類,認證控制

我們先看看這個切面類:@Aspect,宣告這是一個切面類;@Configuration,宣告配置類;

@Aspect
@Configuration
public class RoleAcessConfig {
	//內部內容省略		
}

關於@Configuration註解,它與@Component註解,兩者都是將被註解的類交給Spring容器進行管理,但是兩者是存在區別的:

這裡比較淺的談一下兩者區別,詳細的區別有大牛分析貼,詳情見:https://blog.csdn.net/isea533/article/details/78072133?locationNum=7&fps=1

兩者區別:這裡需要注意,這兩個註解都是用於類上的註解

1. @Component註解的範圍最廣,所有類都可以註解。

Spring 2.5 中除了提供 @Component 註釋外,還定義了幾個擁有特殊語義的註釋,它們分別是:@Repository、@Service 和 @Controller

在目前的 Spring 版本中,這 3 個註釋和 @Component 是等效的,但是從註釋類的命名上,很容易看出這 3 個註釋分別和持久層、業務層和控制層(Web 層)相對應。

雖然目前這 3 個註釋和 @Component 相比沒有什麼新意,但 Spring 將在以後的版本中為它們新增特殊的功能。

所以,如果 Web 應用程式採用了經典的三層分層結構的話,最好在持久層、業務層和控制層分別採用 @Repository、@Service 和 @Controller 對分層中的類進行註釋,用 @Component 對那些比較中立的類進行註釋

2. @Configuration註解一般註解在配置類上:具有@Value註解的成員變數和@Bean註解的方法。

該部分的程式碼主要分為兩塊:

1. 針對含有@Function的方法進行切面處理

	/**
	 * 所有使用Function的註解的方法,且在Controller註解標註的類裡
	 */
	@Around("within(@org.springframework.stereotype.Controller *) && @annotation(function)")
	public Object functionAccessCheck(final ProceedingJoinPoint pjp, Function function) throws Throwable {
		if (function != null) {
			String functionName = function.value();
			if (!canAccess(functionName)) {
				MethodSignature ms = (MethodSignature) pjp.getSignature();
				throw new RuntimeException("Can not Access " + ms.getMethod());
			}
		}
		// 繼續處理原有呼叫
		Object o = pjp.proceed();
		return o;

	}

	protected boolean canAccess(String functionName) {
		if (functionName.length() == 0) {
			return true;
		} else {
			// 取出當前使用者對應的所有角色,從資料庫查詢角色是否有訪問functionName的許可權。
			return false;
		}
	}

面對上述程式碼:

  • @Around:環繞通知方式,在方法執行前後呼叫Advice,這是Spring框架和應用系統一種最為常用的方式。

  • @within:within(@org.springframework.stereotype.Controller *),對所有使用@Controller註解的類進行切面邏輯(AOP)。

  • @annotation:@annotation(function),對具有@Function註解的方法進行切面邏輯(AOP)。

    @within、@annotation均為切點表示式,即上述提到的@PointCut,因此還可以這樣去寫。

      @Pointcut(value = "@annotation(function)")
      public void cutService() {}
    
      @Around("cutService()")
    
  • ProceedingJoinPoint:JoinPoint物件的子類,封裝了AOP切面中方法的資訊,在切面方法中新增JoinPoint引數,就可以獲取封裝該方法資訊的JoinPoint物件。

JoinPoint常用API:

方法名 功能
Signature getSignature() 獲取封裝了署名資訊的物件,在該物件中可以獲取到目標方法名,所屬類的Class等資訊
Object[] getArgs() 獲取傳入目標方法的引數物件
Object getTarget() 獲取被代理的物件
Object getThis() 獲取代理物件

ProceedingJoinPoint物件是JoinPoint的子介面,該物件只用在@Around的切面方法中。此外,在此物件中添加了兩個新方法:

方法名 功能
Object proceed() throws Throwable 執行目標方法
Object proceed(Object[] args) throws Throwable 傳入的新的引數去執行目標方法

該補充的內容補充好了,我們看看這個functionAccessCheck方法都做了什麼吧!

方法引數上,接收了ProceedingJoinPoint、Function兩個引數。

方法體:首先做了Function物件的判空操作,若Function物件不為空,取Function物件的value成員變數,進行認證判斷。

canAccess方法:接收function.value()引數,做了長度判斷length(),表示若未攜帶引數 (@Function()),表示許可權通過。相反,攜帶引數 (@Function(value = “user.add”)),表示該方法是必須具備相應許可權的操作使用者才可進行訪問。

這裡我們可以腦補一下認證邏輯:
首先假設已經登入該系統
	獲取當前登入使用者資訊
	查詢資料庫中使用者資訊所屬角色組
	到所屬角色組下檢視時候具有相應的user.add許可權(基於URL進行許可權資料儲存)
	返回許可權檢查結果(通過,方法執行;不通過,丟擲異常)
這裡提到了許可權控制的相關內容,許可權管理三要素:使用者 - 角色 - 許可權

做個演示:我們不新增認證邏輯,預設返回false(模擬通不過認證的情況)

@RequestMapping("/sayhello.html")
@Function(value = "user.add")
public @ResponseBody String say(String name){
	return "hello "+name;
}

啟動專案,觀察Debug模式下方法執行過程,檢視瀏覽器結果:
方法執行過程:
在這裡插入圖片描述
瀏覽器異常資訊:
在這裡插入圖片描述
當方法上新增@Function(value = “user.add”)的時候,標識該方法受到了許可權控制,只有使用者符合該許可權角色組才可進行該方法內部的業務呼叫。通過切面 + 自定義註解的方式實現了簡單的認證邏輯。

2. 針對含有@controller宣告的類中全部方法進行切面處理

	/**
	 * 所有Controller方法
	 */
	@Around("@within(org.springframework.stereotype.Controller) ")
	public Object simpleAop(final ProceedingJoinPoint pjp) throws Throwable {
		try {
			Object[] args = pjp.getArgs();
			System.out.println("args:" + Arrays.asList(args));
			Object o = pjp.proceed();
			System.out.println("return :" + o);
			return o;

		} catch (Throwable e) {
			throw e;
		}
	}

這個切面方法做的操作時針對所有被@Controller註解的類下的方法,在pjp.proceed()方法前後做了方法引數及返回值的獲取。

我們在Controller中添加個方法做個測試:

@RequestMapping("/sayhello.html")
public @ResponseBody String say(String name){
	return "hello "+name;
}

@RequestMapping("/test")
@ResponseBody
public String test() {
	return "test";
}

啟動專案,分別請求/sayHello.html、/test,檢視控制檯資訊:
在這裡插入圖片描述
可以看出:所有的方法都會進入參與該切面邏輯(記錄方法引數及返回值)