AOP(一)——springAOP

AOP面向切面編程。至於理論網上有很多。個人理解爲對待執行的方法進行攔截,攔截後就可以爲所欲爲,想先執行些前置邏輯,或者待攔截方法執行後執行一些後置邏輯等。

正如夾心餅乾,一分爲二,中間可以加草莓醬,藍莓醬,奶油醬,等等。

廢話不多說,先來個代碼實例。

一、初試牛刀

有這麼一個ProductService,需要執行插入操作,刪除操作等。但是現在老闆突然想執行這些操作前校驗一下權限,OK,你一個個方法做修改,最後改成如下格式。

@Service
public class ProductService {
	
	@Autowired
	AuthService authService;

	public void insert(Product product) {
		authService.checkAccess();//權限校驗
		System.out.println("Insert product");
	}
	
	public void delete(long id) {
		System.out.println("Delete product id="+id);
	}
}

AuthService中有一個checkAccess()方法就是用來校驗權限的,上述改完後你心滿意足,如果還有其他上百個方法同時需要權限校驗呢?

還房貸和車貸的壓力讓你不敢甩手不幹,難道你要一個個修改方法麼?這是你需要AOP,不要998,只要一些jar包。

下面是使用AOP的方法

首先編寫一個註解,用在方法上表示這是admin用戶纔有權限操作的方法。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AdminOnly {

}

然後將ProductService的insert加上註解,那麼就可以進行權限校驗了?別傻了,還需要你寫切面類去攔截,不然系統怎麼知道你這個@AdminOnly用來幹什麼的,人工智能沒有這麼先進。

@AdminOnly
public void insert(Product product) {
    System.out.println("Insert product");
}

下面寫一個SecurityAspect.java切面類,用來處理@AdminOnly註解

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.lw.service.AuthService;

@Aspect//表示這是一個切面類
@Component//表示這個切面類交給spring容器管理
public class SecurityAspect {

	@Autowired
	AuthService authService;
	
	/**
	 * 攔截註解爲@AdminOnly的方法
	 */
	@Pointcut("@annotation(AdminOnly)")
	public void adminOnly() {
		
	}
	
	/**
	 * 在執行adminOnly方法之前需要的操作
	 */
	@Before("adminOnly()")
	public void check() {
		authService.checkAccess();
	}
}

OK,這樣就能同下面代碼一樣insert的時候進行權限校驗。

@Autowired
AuthService authService;

public void insert(Product product) {
	authService.checkAccess();
	System.out.println("Insert product");
}

這樣就結束了麼?作爲一個英俊瀟灑,風流倜儻,勤懇認真(……)的好程序猿當然要寫個測試類。

上測試類SpringAopTest.java,這個測試類可牛逼了使用@SpringBootTest,那麼自然能想到使用的是SpringBoot,得需要寫個SpringBoot的啓動類。

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAopTest {

	@Autowired
	ProductService productService;
	
	/**
	 * 匿名插入權限校驗
	 */
	@Test(expected=Exception.class)
	public void annoInsertTest() {
		CurrentUserHolder.set("king");
		productService.insert(null);
	}
	
	@Test
	public void adminInsertTest() {
		CurrentUserHolder.set("admin");
		productService.insert(null);
	}
}

SpringBoot的啓動類ApplicationContext.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ApplicationContext {

	public static void main(String[] args) {
		SpringApplication.run(ApplicationContext.class, null);
	}
}

至於springboot還需要application.yml配置文件就不列出來了,反正也是個空文件。

至此結束。

What?你他丫逗我呢?我不還是一個個方法加上@AdminOnly註解,我不還是累個半死,還想着使用AOP能早點下班回家睡覺呢。

別急下面就講匹配包攔截。

二、初窺門徑

看到within有這個匹配包或者類的功能,改造ProductService中insert()方法如下:

//@AdminOnly
public void insert(Product product) {
	System.out.println("Insert product");
}

1. 不使用AdminOnly註解。

新寫一個切面類PkgTypeAspectConfig 匹配類ProductService,這樣執行到ProductService類中任何方法時就會進行權限校驗。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.lw.service.AuthService;

@Aspect
@Component
public class PkgTypeAspectConfig {

	@Autowired
	AuthService authService;
	
	@Pointcut("within(com.lw.service.ProductService)")
	public void matchType() {
		
	}
	
	@Before("matchType()")
	public void before() {
		System.out.println("PkgTypeAspectConfig before");
		authService.checkAccess();
	}
}

看到此你想,“嗯,比我一個個添加註釋好多了。”但是不要忽略了作爲一個程序員想回家的激情,要是有很多類都需要添加權限校驗呢?你難道寫很多個如下方法?

@Pointcut(“within(com.lw.service.ProductService)”)  public void matchType() { }

別逗,要是以後新增加了類呢?難道還要修改切面類?這樣一看好像維護的東西多了,還能下班回家麼?

答案是肯定能的,看下面。

@Pointcut(“within(com.lw.service.*)”)

這樣就攔截com.lw.service包下所有類,但是不包括子類。

如果也要攔截子類需要使用

@Pointcut(“within(com.lw.service..*)”)

再考慮一點,如果我不僅僅要匹配一個類,一個包,而是匹配某個接口所有子類,怎麼辦?看下面匹配對象。

三、一竅不通

ProductService改造如下,實現一個接口IProductService。

@Service
public class ProductService implements IProductService

再寫一個攔截IProductService接口的切面:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ObjectAspectConfig {

	/**
	 * 攔截實現了接口IProductService的所有類
	 */
	@Pointcut("this(com.lw.service.IProductService)")
	public void matchCondition() {
		System.out.println("this");
	}
	
	/**
	 * 攔截實現了接口IProductService的所有類
	 */
	@Pointcut("target(com.lw.service.IProductService)")
	public void matchCondition02() {
		System.out.println("target");
	}
	
	/**
	 * 只攔截ProductService,相當於within(com.lw.service.ProductService)
	 */
	@Pointcut("bean(ProductService)")
	public void matchCondition03() {
		
	}
	
	@Before("matchCondition()")
	public void before() {
		System.out.println("ObjectAspectConfig before");
	}
}

有兩種實現方式,this,target,至於區別,我他丫也懵逼。OK,反正能實現接口攔截。所以第三個標題爲一竅不通。

此處有一點注意,如果IProductService接口實現了insert方法,那麼SpringDataTest.java中就需要將ProductService類改爲IProductService注入,否則報錯,如下

@Autowired
IProductService productService;
public interface IProductService {

// public void insert(Product product);

}

四、四通八達

這裏有匹配參數那麼如何攔截ProductService中insert方法呢?

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ArgsAspectConfig {

	/**
	 * 攔截ProductService類中方法參數爲Product的方法
	 */
	@Pointcut("args(com.lw.domain.Product) && within(com.lw.service.ProductService)")
	public void matchArgs() {
		
	}
	
	@Before("matchArgs()")
	public void before() {
		System.out.println("ArgsAspectConfig before");
	}
}

看這個切面類,此切面類如果是自定義類需要args(com.lw.domain.Product)寫成這樣,否則光寫個Product不識別。

注意這個@args也可以表現爲傳入的參數有@Repository註解的攔截,也就是說入參有此註解就會攔截。

好傢伙,這個execution()攔截牛逼了,大家用了都說好。因爲他支持類似正則表達式的表示方式。當然理解每個字段含義就不難。

首先modifier-pattern? 表示方法修飾符 public private protected,?表示非必選,可有可無。

ret-type-pattern 表示方法返回類型,比如com.lw.domain.Product,當然對於Java類型直接寫String,Long,Integer等就可以了。

declaring-type-pattern?  表示方法所在的包,雖然是?,但是這個一般不會省略。

name-pattern 指方法名

param-pattern 指方法入參

throws-pattern? 表示方法拋出異常

說了這麼多廢話,感覺還不如來個列子實在。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ExecutionAspectConfig {

	/**
	 * 攔截com.lw.service包下以Service結尾的類,方法是 public 任意返回值,參數任意
	 */
	@Pointcut("execution(public * com.lw.service.*Service.*(..))")
	public void matchCondition() {
		
	}
	
	@Before("matchCondition()")
	public void before() {
		System.out.println("ExecutionAspectConfig before");
	}
}

@AfterReturning 攔截的方法有返回值,就執行此註解,入參是方法返回的值

@AfterThrowing 攔截的方法拋出了異常,執行此註解

重點講解下@Around註解

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.springframework.stereotype.Component;

@Aspect
@Component
public class AdviceAspectConfig {

	/**
	 * 匹配任意返回值,com.lw.service.ProductService此類的任意方法,任意入參
	 */
	@Pointcut("execution(* com.lw.service.ProductService.*(..))")
	public void matchCondition() {}
	
	@Around("matchCondition()")
	public Object after(ProceedingJoinPoint joinPoint) {
		System.out.println("AdviceAspectConfig before");
		Object result = null;
		try {
			result = joinPoint.proceed(joinPoint.getArgs());
			System.out.println("AdviceAspectConfig return");
		}catch(Throwable e) {
			System.out.println(e.getMessage());
		}finally {
			System.out.println("AdviceAspectConfig finally");
		}
		return result;
	}
}

執行結果如下:

AdviceAspectConfig before

Insert product

AdviceAspectConfig return

AdviceAspectConfig finally

哇,閉幕謝禮,雖然講得很粗糙,但是項目中應用也就是這麼粗糙,將錯就錯吧。

五、錦上添花

介紹一點基礎小知識,關於線程的。

public class CurrentUserHolder {

	private static final ThreadLocal<String> holder= new ThreadLocal<>();
	
	public static String get() {
		return holder.get()==null? "unknown":holder.get();
	}
	
	public static void set(String user) {
		holder.set(user);
	}
}

SpringDataTest.java中:

@Test
public void adminInsertTest() {
	CurrentUserHolder.set("admin");
	productService.insert(null);
}

AuthService.java類:

import org.springframework.stereotype.Component;

import com.lw.security.CurrentUserHolder;

/**
 * 權限校驗類
 * @author king
 *
 */
@Component
public class AuthService {

	public void checkAccess() {
		String user = CurrentUserHolder.get();
		if(!"admin".equals(user)) {
			throw new RuntimeException(String.format("用戶%s沒有權限!", user));
		}
	}
}

看到什麼沒,在SpringDataTest.java中設置的線程變量,可以在AuthService類中獲取,線程全局變量啊。

六、管中窺豹

pom.xml

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>1.5.3.RELEASE</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>

<!-- spring-boot-starter-aop -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!--only use for class SpringAopTest test -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

學習視頻:https://www.imooc.com/learn/869

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章