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