Spring AOP 功能使用詳解

相關文章

Spring 中 bean 註冊的源碼解析html

Spring bean 建立過程源碼解析java

Spring 的 getBean 方法源碼解析spring

前言

AOP 既熟悉又陌生,瞭解過 Spring 人的都知道 AOP 的概念,即面向切面編程,能夠用來管理一些和主業務無關的周邊業務,如日誌記錄,事務管理等;陌生是由於在工做中基本沒有使用過,AOP 的相關概念也是雲裏霧裏;最近在看 Spring 的相關源碼,因此仍是先來捋一捋 Spring 中 AOP 的一個用法。編程

相關概念

在學習 Spring AOP 的用法以前,先來看看 AOP 的相關概念,ide

Spring AOP 的詳細介紹,請參考官網 Aspect Oriented Programming with Spring學習

1. Join point 鏈接點,表示程序執行期間的一個點,在 Spring AOP 表示的就是一個方法,即一個方法能夠看做是一個 Join point測試

2. pointcut 切點,就是與鏈接點匹配的謂詞,什麼意思呢,就是須要執行 Advice 的鏈接點就是切點this

3. Advice 加強,在鏈接點執行的操做,分爲前置、後置、異常、最終、環繞加強五種spa

4. Aspect 切面,由 pointcut 和 Advice 組成,能夠簡單的認爲 @Aspect 註解的類就是一個切面.net

5. Target object :目標對象,即 織入 advice 的目標對象

6. AOP proxy :代理類,在 Spring AOP 中, 一個 AOP 代理是一個 JDK 動態代理對象或 CGLIB 代理對象

7. Weaving 織入,將 Aspect 應用到目標對象中去

注:上述幾個概念中,比較容易混淆的是 Join point   和  pointcut能夠這麼來理解,在 Spring AOP 中,全部的可執行方法都是 Join point,全部的 Join point 均可以植入 Advice;而 pointcut 能夠看做是一種描述信息,它修飾的是 Join point,用來確認在哪些 Join point 上執行 Advice,

栗子

在瞭解了 AOP 的概念以後,接下來就來看看如何使用  Spring Aop

 1. 要想使用 Spring  AOP ,首先先得在 Spring 配置文件中配置以下標籤:

<aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"/>

該標籤有兩個屬性, expose-proxy 和 proxy-target-class ,默認值都爲 false;

expose-proxy  :是否須要將當前的代理對象使用 ThreadLocal 進行保存,這是什麼意思呢,例如 Aop 須要對某個接口下的全部方法進行攔截,可是有些方法在內部進行自我調用,以下所示:

public void test_1()
    {   
        this.test_2();
    }
    public void test_2()
    {
    }

調用 test_1,此時 test_2 將不會被攔截進行加強,由於調用的是 AOP 代理對象而不是當前對象,而 在 test_1 方法內部使用的是 this 進行調用,因此 test_2 將不會被攔截加強,因此該屬性 expose-proxy  就是用來解決這個問題的,即 AOP 代理的獲取。

proxy-target-class :是否使用 CGLIB 進行代理,由於 Spring AOP 的底層技術就是使用的是動態代理,分爲 JDK 代理 和 CGLIB 代理,該屬性的默認值爲 false,表示 AOP 底層默認使用的使用 JDK 代理,當須要代理的類沒有實現任何接口的時候纔會使用 CGLIB 進行代理,若是想都是用 CGLIB 進行代理,能夠把該屬性設置爲 true 便可。

2. 定義須要 aop 攔截的方法,模擬一個 User 的增刪改操做:

接口:

public interface IUserService {
    void add(User user);
    User query(String name);
    List<User> qyertAll();
    void delete(String name);
    void update(User user);
}
接口實現:
@Service("userServiceImpl")
public class UserServiceImpl implements IUserService {

    @Override
    public void add(User user) {
        System.out.println("添加用戶成功,user=" + user);
    }

    @Override
    public User query(String name) {
        System.out.println("根據name查詢用戶成功");
        User user = new User(name, 20, 1, 1000, "java");
        return user;
    }

    @Override
    public List<User> qyertAll() {
        List<User> users = new ArrayList<>(2);
        users.add(new User("zhangsan", 20, 1, 1000, "java"));
        users.add(new User("lisi", 25, 0, 2000, "Python"));
        System.out.println("查詢全部用戶成功, users = " + users);
        return users;
    }

    @Override
    public void delete(String name) {
        System.out.println("根據name刪除用戶成功, name = " + name);
    }

    @Override
    public void update(User user) {
        System.out.println("更新用戶成功, user = " + user);
    }
}

3. 定義 AOP 切面

在 Spring AOP 中,使用 @Aspect  註解標識的類就是一個切面,而後在切面中定義切點(pointcut)和 加強(advice):

3.1 前置加強,@Before(),在目標方法執行以前執行

@Component
@Aspect
public class UserAspectj {

    // 在方法執行以前執行
    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
    public void before_1(){
        System.out.println("log: 在 add 方法以前執行....");
    }
}

上述的方法 before_1() 是對接口的 add() 方法進行 前置加強,即在 add() 方法執行以前執行,

測試:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/resources/myspring.xml")
public class TestBean {

    @Autowired
    private IUserService userServiceImpl;

    @Test
    public void testAdd() {
        User user = new User("zhangsan", 20, 1, 1000, "java");
        userServiceImpl.add(user);
    }
}
// 結果:
// log: 在 add 方法以前執行....
// 添加用戶成功,user=User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

若是想要獲取目標方法執行的參數等信息呢,咱們可在 切點的方法中添參數 JoinPoint ,經過它了獲取目標對象的相關信息:

@Before("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
    public void before_2(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        User user = null;
        if(args[0].getClass() == User.class){
            user = (User) args[0];
        }
        System.out.println("log: 在 add 方法以前執行, 方法參數 = " + user);
    }

從新執行上述測試代碼,結果以下:

// log: 在 add 方法以前執行, 方法參數 = User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}
// 添加用戶成功,user=User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

3.2 後置加強,@After(),在目標方法執行以後執行,不管是正常退出仍是拋異常,都會執行

// 在方法執行以後執行
    @After("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
    public void after_1(){
        System.out.println("log: 在 add 方法以後執行....");
    }

執行 3.1 的測試代碼,結果以下:

// 添加用戶成功,user=User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}
// log: ==== 方法執行以後 =====

3.3 返回加強,@AfterReturning(),在目標方法正常返回後執行,出現異常則不會執行,能夠獲取到返回值:

@AfterReturning(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", returning="object")
public void after_return(Object object){
    System.out.println("在 query 方法返回後執行, 返回值= " + object);
}

測試:

@Test
public void testQuery() {
	userServiceImpl.query("zhangsan");
}
// 結果:
// 根據name查詢用戶成功
// 在 query 方法返回後執行, 返回值= User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

當一個方法同時被 @After() 和 @AfterReturning() 加強的時候,先執行哪個呢?

@AfterReturning(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", returning="object")
public void after_return(Object object){
	System.out.println("===log: 在 query 方法返回後執行, 返回值= " + object);
}

@After("execution(* main.tsmyk.mybeans.inf.IUserService.query(..))")
public void after_2(){
	System.out.println("===log: 在 query 方法以後執行....");
}

測試:

// 根據name查詢用戶成功
// ===log: 在 query 方法以後執行....
// ===log: 在 query 方法返回後執行, 返回值= User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

能夠看到,即便 @After() 放在  @AfterReturning() 的後面,它也先被執行,即 @After()  @AfterReturning() 以前執行。

3.4 異常加強,@AfterThrowing,在拋出異常的時候執行,不拋異常不執行

@AfterThrowing(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", throwing = "ex")
public void after_throw(Exception ex){
	System.out.println("在 query 方法拋異常時執行, 異常= " + ex);
}

如今來修改一下它加強的 query() 方法,讓它拋出異常:

@Override
public User query(String name) {
	System.out.println("根據name查詢用戶成功");
	User user = new User(name, 20, 1, 1000, "java");
	int a = 1/0;
	return user;
}

測試:

@Test
public void testQuery() {
	userServiceImpl.query("zhangsan");
}

// 結果:
// 在 query 方法拋異常時執行, 異常= java.lang.ArithmeticException: / by zero
// java.lang.ArithmeticException: / by zero ...........

3.5 環繞加強,@Around,在目標方法執行以前和以後執行

// 目標方法:
@Override
public void update(User user) {
    System.out.println("更新用戶成功, user = " + user);
}

@Around("execution(* main.tsmyk.mybeans.inf.IUserService.delete(..))")
public void test_around(ProceedingJoinPoint joinPoint) throws Throwable {
	Object[] args = joinPoint.getArgs();
	System.out.println("log : delete 方法執行以前, 參數 = " + args[0].toString());
	joinPoint.proceed();
	System.out.println("log : delete 方法執行以後");
}

測試:

@Test
public void test5(){
    userServiceImpl.delete("zhangsan");
}

// 結果:
// log : delete 方法執行以前, 參數 = zhangsan
// 根據name刪除用戶成功, name = zhangsan
// log : delete 方法執行以後

以上就是 Spring AOP 的幾種加強。

上面的栗子中,在每一個方法上方的切點表達式都須要寫一遍,如今能夠使用 @Pointcut 來聲明一個可重用的切點表達式,以後在每一個方法的上方引用這個切點表達式便可。:

// 聲明 pointcut
@Pointcut("execution(* main.tsmyk.mybeans.inf.IUserService.query(..))")
public void pointcut(){
}

@Before("pointcut()")
public void before_3(){
	System.out.println("log: 在 query 方法以前執行");
}
@After("pointcut()")
public void after_4(){
	System.out.println("log: 在 query 方法以後執行....");
}

指示符

在上面的栗子中,使用了 execution 指示符,它用來匹配方法執行的鏈接點,也是 Spring AOP 使用的主要指示符,在切點表達式中使用了 通配符 (*)  和  (.. ),其中,(* )能夠表示任意方法,任意返回值,(..)表示方法的任意參數 ,接下來來看下其餘的指示符。

1. within

匹配特定包下的全部類的全部 Joinpoint(方法),包括子包,注意是全部類,而不是接口,若是寫的是接口,則不會生效,如 within(main.tsmyk.mybeans.impl.* 將會匹配 main.tsmyk.mybeans.impl 包下全部類的全部 Join point;within(main.tsmyk.mybeans.impl..* 兩個點將會匹配該包及其子包下的全部類的全部 Join point。

栗子:

@Pointcut("within(main.tsmyk.mybeans.impl.*)")
public void testWithin(){
}

@Before("testWithin()")
public void test_within(){
	System.out.println("test within 在方法執行以前執行.....");
}

執行該包下的類 UserServiceImpl 的 delete 方法,結果以下:

@Test
public void test5(){
	userServiceImpl.delete("zhangsan");
}

// 結果:
// test within 在方法執行以前執行.....
// 根據name刪除用戶成功, name = zhangsan

2. @within

匹配全部持有指定註解類型的方法,如 @within(Secure),任何目標對象持有Secure註解的類方法;必須是在目標對象上聲明這個註解,在接口上聲明的對它不起做用。

3. target

匹配的是一個目標對象,target(main.tsmyk.mybeans.inf.IUserService) 匹配的是該接口下的全部 Join point :

@Pointcut("target(main.tsmyk.mybeans.inf.IUserService)")
public void anyMethod(){
}

@Before("anyMethod()")
public void beforeAnyMethod(){
	System.out.println("log: ==== 方法執行以前 =====");
}

@After("anyMethod()")
public void afterAnyMethod(){
	System.out.println("log: ==== 方法執行以後 =====");
}

以後,執行該接口下的任意方法,都會被加強。

3. @target

匹配一個目標對象,這個對象必須有特定的註解,如 

@target(org.springframework.transaction.annotation.Transactional) 匹配任何 有 @Transactional 註解的 方法

4. this

匹配當前AOP代理對象類型的執行方法,this(service.IPointcutService),當前AOP對象實現了 IPointcutService接口的任何方法

5. arg

匹配參數,

// 匹配只有一個參數 name 的方法
    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(name)")
    public void test_arg(){

    }

    // 匹配第一個參數爲 name 的方法
    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(name, ..)")
    public void test_arg2(){

    }
    
    // 匹配第二個參數爲 name 的方法
    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(*, name, ..)")
    public void test_arg3(){

    }

6. @arg

匹配參數,參數有特定的註解,@args(Anno)),方法參數標有Anno註解。

7. @annotation

匹配特定註解

@annotation(org.springframework.transaction.annotation.Transactional) 匹配 任何帶有 @Transactional 註解的方法。

8. bean 

匹配特定的 bean 名稱的方法

// 匹配 bean 的名稱爲 userServiceImpl 的全部方法
    @Before("bean(userServiceImpl)")
    public void test_bean(){
        System.out.println("===================");
    }

    // 匹配 bean 名稱以 ServiceImpl 結尾的全部方法
    @Before("bean(*ServiceImpl)")
    public void test_bean2(){
        System.out.println("+++++++++++++++++++");
    }

測試:

執行該bean下的方法:

@Test
public void test5(){
	userServiceImpl.delete("zhangsan");
}
//結果:
// ===================
// +++++++++++++++++++
// 根據name刪除用戶成功, name = zhangsan

以上就是 Spring AOP 全部的指示符的使用方法了。

Spring AOP 原理

Spring AOP 的底層使用的使用 動態代理;共有兩種方式來實現動態代理,一個是 JDK 的動態代理,一種是 CGLIB 的動態代理,下面使用這兩種方式來實現以上面的功能,即在調用 UserServiceImpl 類方法的時候,在方法執行以前和以後加上日誌。

JDK 動態代理

實現 JDK 動態代理,必需要實現 InvocationHandler 接口,並重寫 invoke 方法:

public class UserServiceInvocationHandler implements InvocationHandler {

    // 代理的目標對象
    private Object target;

    public UserServiceInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("log: 目標方法執行以前, 參數 = " + args);

        // 執行目標方法
        Object retVal = method.invoke(target, args);

        System.out.println("log: 目標方法執行以後.....");

        return retVal;
    }
}

測試:

public static void main(String[] args) throws IOException {

	// 須要代理的對象
	IUserService userService = new UserServiceImpl();
	InvocationHandler handler = new UserServiceInvocationHandler(userService);
	ClassLoader classLoader = userService.getClass().getClassLoader();
	Class[] interfaces = userService.getClass().getInterfaces();

	// 代理對象
	IUserService proxyUserService = (IUserService) Proxy.newProxyInstance(classLoader, interfaces, handler);

	System.out.println("動態代理的類型  = " + proxyUserService.getClass().getName());
	proxyUserService.query("zhangsan");
    
    // 把字節碼寫到文件
    byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy", new Class[]{UserServiceImpl.class});
    FileOutputStream fos =new FileOutputStream(new File("D:/$Proxy.class"));
    fos.write(bytes);
    fos.flush();

}

結果:

動態代理的類型  = com.sun.proxy.$Proxy0
log: 目標方法執行以前, 參數 = [Ljava.lang.Object;@2ff4acd0
根據name查詢用戶成功
log: 目標方法執行以後.....

能夠看到在執行目標方法的先後已經打印了日誌;剛在上面的 main 方法中,咱們把代理對象的字節碼寫到了文件裏,如今來分析下:

反編譯 &Proxy.class 文件以下:

能夠看到它經過實現接口來實現的。

JDK 只能代理那些實現了接口的類,若是一個類沒有實現接口,則沒法爲這些類建立代理。此時能夠使用 CGLIB 來進行代理。

CGLIB 動態代理

接下來看下 CGLIB 是如何實現的。

首先新建一個須要代理的類,它沒有實現任何接口:

public class UserServiceImplCglib{
    public User query(String name) {
        System.out.println("根據name查詢用戶成功, name = " + name);
        User user = new User(name, 20, 1, 1000, "java");
        return user;
    }
}

如今須要使用 CGLIB 來實如今方法 query 執行的先後加上日誌:

使用 CGLIB 來實現動態代理,也須要實現接口 MethodInterceptor,重寫 intercept 方法:

public class CglibMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        System.out.println("log: 目標方法執行以前, 參數 = " + args);

        Object retVal = methodProxy.invokeSuper(obj, args);

        System.out.println("log: 目標方法執行以後, 返回值 = " + retVal);
        return retVal;
    }
}

測試:

public static void main(String[] args) {

	// 把代理類寫入到文件
	System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\");

	Enhancer enhancer = new Enhancer();
	enhancer.setSuperclass(UserServiceImplCglib.class);
	enhancer.setCallback(new CglibMethodInterceptor());

	// 建立代理對象
	UserServiceImplCglib userService = (UserServiceImplCglib) enhancer.create();
	System.out.println("動態代理的類型 = " + userService.getClass().getName());

	userService.query("zhangsan");
}

結果:

動態代理的類型 = main.tsmyk.mybeans.impl.UserServiceImplCglib$$EnhancerByCGLIB$$772edd85
log: 目標方法執行以前, 參數 = [Ljava.lang.Object;@77556fd
根據name查詢用戶成功, name = zhangsan
log: 目標方法執行以後, 返回值 = User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

能夠看到,結果和使用 JDK 動態代理的同樣,此外,能夠看到代理類的類型爲 main.tsmyk.mybeans.impl.UserServiceImplCglib$$EnhancerByCGLIB$$772edd85,它是 UserServiceImplCglib 的一個子類,即 CGLIB 是經過 繼承的方式來實現的。

總結

1. JDK 的動態代理是經過反射和攔截器的機制來實現的,它會爲代理的接口生成一個代理類。

2. CGLIB 的動態代理則是經過繼承的方式來實現的,把代理類的class文件加載進來,經過修改其字節碼生成子類的方式來處理。

3. JDK 動態代理只能對實現了接口的類生成代理,而不能針對類。

4. CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法,可是由於採用的是繼承, 因此 final 類或方法沒法被代理。

5. Spring AOP 中,若是實現了接口,默認使用的是 JDK 代理,也能夠強制使用 CGLIB 代理,若是要代理的類沒有實現任何接口,則會使用 CGLIB 進行代理,Spring 會進行自動的切換。

 

上述實現 Spring AOP 的栗子採用的是 註解的方法來實現的,此外,還能夠經過配置文件的方式來實現 AOP 的功能。以上就是 Spring AOP 的一個詳細的使用過程。

相關文章
相關標籤/搜索