1. 程式人生 > 程式設計 >Spring系列__04AOP簡介

Spring系列__04AOP簡介

AOP簡介

今天來介紹一下AOP。AOP,中文常被翻譯為“面向切面程式設計”,其作為OOP的擴充套件,其思想除了在Spring中得到了應用,也是不錯的設計方法。通常情況下,一個軟體系統,除了正常的業務邏輯程式碼,往往還有一些功能性的程式碼,比如:記錄日誌、資料校驗等等。最原始的辦法就是直接在你的業務邏輯程式碼中編寫這些功能性程式碼,但是,這樣除了當時開發的時候比較方便以外;程式碼的閱讀性、可維護性都會大大降低。而且,當你需要頻繁使用一個功能的時候(比如記錄日誌),你還需要重複編寫。而使用AOP的好處,簡單來說就是,它能把這種重複性的功能程式碼抽離出來,在需要的時候,通過動態代理技術,在不修改原始碼的情況下提供增強性功能。 優勢:

  • 減少重複程式碼
  • 提高開發效率
  • 程式碼更加整潔,提高了可維護性 說了這麼多,簡單演示一下,我們假定現在要實現一個賬戶轉賬的功能,這裡面會涉及到一些事務的控制,從程式碼的合理性角度出發,我們將其放在service層。
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;
}

public interface AccountDao {
    /**
     * 查詢所有
     * @return
*/ List<Account> findAllAccount(); /** * 查詢一個 * @return */ Account findAccountById(Integer accountId); /** * 儲存 * @param account */ void saveAccount(Account account); /** * 更新 * @param account */ void updateAccount(Account account); /** * 刪除 * @param acccountId */ void deleteAccount(Integer acccountId); /** * 根據名稱查詢賬戶 * @param accountName * @return
如果有唯一的一個結果就返回,如果沒有結果就返回null * 如果結果集超過一個就拋異常 */ Account findAccountByName(String accountName); } public class AccountServiceImpl_OLD implements AccountService { private AccountDao accountDao; private TransactionManager txManager; public void setTxManager(TransactionManager txManager) { this.txManager = txManager; } public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } @Override public List<Account> findAllAccount() { try { //1.開啟事務 txManager.beginTransaction(); //2.執行操作 List<Account> accounts = accountDao.findAllAccount(); //3.提交事務 txManager.commit(); //4.返回結果 return accounts; }catch (Exception e){ //5.回滾操作 txManager.rollback(); throw new RuntimeException(e); }finally { //6.釋放連線 txManager.release(); } } @Override public Account findAccountById(Integer accountId) { try { //1.開啟事務 txManager.beginTransaction(); //2.執行操作 Account account = accountDao.findAccountById(accountId); //3.提交事務 txManager.commit(); //4.返回結果 return account; }catch (Exception e){ //5.回滾操作 txManager.rollback(); throw new RuntimeException(e); }finally { //6.釋放連線 txManager.release(); } } @Override public void saveAccount(Account account) { try { //1.開啟事務 txManager.beginTransaction(); //2.執行操作 accountDao.saveAccount(account); //3.提交事務 txManager.commit(); }catch (Exception e){ //4.回滾操作 txManager.rollback(); }finally { //5.釋放連線 txManager.release(); } } @Override public void updateAccount(Account account) { try { //1.開啟事務 txManager.beginTransaction(); //2.執行操作 accountDao.updateAccount(account); //3.提交事務 txManager.commit(); }catch (Exception e){ //4.回滾操作 txManager.rollback(); }finally { //5.釋放連線 txManager.release(); } } @Override public void deleteAccount(Integer acccountId) { try { //1.開啟事務 txManager.beginTransaction(); //2.執行操作 accountDao.deleteAccount(acccountId); //3.提交事務 txManager.commit(); }catch (Exception e){ //4.回滾操作 txManager.rollback(); }finally { //5.釋放連線 txManager.release(); } } @Override public void transfer(String sourceName,String targetName,Float money) { try { //1.開啟事務 txManager.beginTransaction(); //2.執行操作 //2.1根據名稱查詢轉出賬戶 Account source = accountDao.findAccountByName(sourceName); //2.2根據名稱查詢轉入賬戶 Account target = accountDao.findAccountByName(targetName); //2.3轉出賬戶減錢 source.setMoney(source.getMoney()-money); //2.4轉入賬戶加錢 target.setMoney(target.getMoney()+money); //2.5更新轉出賬戶 accountDao.updateAccount(source); int i=1/0; //2.6更新轉入賬戶 accountDao.updateAccount(target); //3.提交事務 txManager.commit(); }catch (Exception e){ //4.回滾操作 txManager.rollback(); e.printStackTrace(); }finally { //5.釋放連線 txManager.release(); } } 複製程式碼

在這裡,我們看見了很噁心的程式碼:大量重複性的記錄日誌的程式碼,而且,當你更改的時候,你發現並不方便。後續我們會對這個程式碼進行改寫。

AOP的實現方式

AOP通過動態代理的來實現。

動態代理簡介

在這裡先簡單介紹一下動態代理:使用一個代理將物件包裝起來,然後用該代理物件取代原始物件.。任何對原始物件的呼叫都要通過代理. 代理物件決定是否以及何時將方法呼叫轉到原始物件上。其呼叫過程如下圖所示:

特點:

  • 位元組碼隨用隨建立,隨用隨載入。
  • 它與靜態代理的區別也在於此。因為靜態代理是位元組碼一上來就建立好,並完成載入。
  • 裝飾者模式就是靜態代理的一種體現。

動態代理有兩種形式

  • 基於介面的動態代理 提供者: JDK 官方的 Proxy 類。 要求:被代理類最少實現一個介面
  • 基於子類的動態代理 提供者:第三方的 CGLib,如果報 asmxxxx 異常,需要匯入 asm.jar。 要求:被代理類不能用 final 修飾的類(最終類)。 下面結合示例來解釋一下這兩種動態代理的實現方式: 筆者最近更換了一臺新的電腦,就以買電腦來舉個例子吧。現在大家買電腦,已經很少去實體店了,多半是通過電商渠道。不管是什麼,都是從中間商來買,這一行為,在無形中就體現了代理模式的思想。 電腦生產商最開始的時候,除了生產和組裝電腦,同時還可以將電腦出售給消費者或者經銷商(代理商),而他對顧客來說,需要完成兩種服務:銷售商品和售後服務。當行業發展到一定階段,電腦生產商不斷增多,人們就會制定一些行業規範來讓大家共同遵守(也就是抽象出來的介面)。而且,電腦生產商為了節約成本,不再提供直接和消費者銷售的服務,我們消費者也因此只能從代理商那裡購買新的電腦。這便是典型的代理模式。

使用 JDK 官方的 Proxy 類建立代理物件

public interface IProducer {
 
    public void saleProduct(float money);

    public void afterService(float money);
}

public class Producer implements IProducer {
 
    @Override
    public void saleProduct(float money) {
        System.out.println("銷售產品,並拿到錢:" + money);
    }

  
    @Override
    public void afterService(float money) {
        System.out.println("提供售後服務,並拿到錢:" + money);
    }
}

//消費者
public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();
        /**
         *  如何建立代理物件:
         *  使用Proxy類中的newProxyInstance方法
         *  建立代理物件的要求:
         *      被代理類最少實現一個介面,如果沒有則不能使用
         *  newProxyInstance方法的引數:
         *      ClassLoader:類載入器
         *          它是用於載入代理物件位元組碼的。和被代理物件使用相同的類載入器。固定寫法。
         *      Class[]:位元組碼陣列
         *          它是用於讓代理物件和被代理物件有相同方法。固定寫法。
         *     InvocationHandler:用於提供增強的程式碼
         *          它是讓我們寫如何代理。我們一般都是些一個該介面的實現類,通常情況下都是匿名內部類,但不是必須的。
         *          此介面的實現類都是誰用誰寫。
         */

        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),producer.getClass().getInterfaces(),new InvocationHandler() {
                    /**
                     * 作用:執行被代理物件的任何介面方法都會經過該方法
                     * 方法引數的含義
                     * @param proxy   代理物件的引用
                     * @param method  當前執行的方法
                     * @param args    當前執行方法所需的引數
                     * @return 和被代理物件方法有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
                        //提供增強的程式碼
                        Object returnValue = null;

                        //1.獲取方法執行的引數
                        Float money = (Float)args[0];
                        //2.判斷當前方法是不是銷售,如果是的話,打八折
                        if("saleProduct".equals(method.getName())) {
                            returnValue = method.invoke(producer,money*0.8f);
                        }
                        return returnValue;
                    }
                });
        proxyProducer.saleProduct(10000f);


    }
}

複製程式碼

使用jdk提供的Proxy來建立代理物件的時候,要求別代理物件至少要實現一個介面,代理類需要實現同樣的介面並由同一類載入器載入。如果沒有這樣,就不能使用這種方式了。其他具體內容,請參考官方檔案。

cglib方式來實現動態代理

其實,說AOP是OOP的延伸,還是很容易證明的:jdk提供動態代理的方式是實現介面,而cglib的實現方式就是利用了OOP的繼承。原理大同小異,主要區別就是不用實現介面而是改用繼承,也因此具備繼承的限制:被代理的類不能是被final修飾。

public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();
        /**
         *  create方法的引數:
         *      Class:位元組碼
         *          它是用於指定被代理物件的位元組碼。
         *
         *      Callback:用於提供增強的程式碼
         *          它是讓我們寫如何代理。我們一般都是些一個該介面的實現類,通常情況下都是匿名內部類,但不是必須的。
         *          此介面的實現類都是誰用誰寫。
         *          我們一般寫的都是該介面的子介面實現類:MethodInterceptor
         */
        Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(),new MethodInterceptor() {
            @Override
            public Object intercept(Object o,Object[] objects,MethodProxy methodProxy) throws Throwable {
                Object result = null;
                Float price = (Float) objects[0];
                if ("saleProduct".equals(method.getName())) {
                    result = method.invoke(o,price * 0.8f);
                }
                return result;
            }
        });

        proxyProducer.saleProduct(10000f);
    }
}

ublic class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();
        /**
         *  create方法的引數:
         *      Class:位元組碼
         *          它是用於指定被代理物件的位元組碼。
         *
         *      Callback:用於提供增強的程式碼
         *          它是讓我們寫如何代理。我們一般都是些一個該介面的實現類,通常情況下都是匿名內部類,但不是必須的。
         *          此介面的實現類都是誰用誰寫。
         *          我們一般寫的都是該介面的子介面實現類:MethodInterceptor
         */
        Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(),price * 0.8f);
                }
                return result;
            }
        });

        proxyProducer.saleProduct(10000f);
    }
}
複製程式碼

Spring中的AOP

以上所述的兩種生成代理物件的方法,在Spring中都會應用:預設優先使用jdk自帶的方式,當發現別代理類沒有實現介面時改用cglib方式。

專業術語直白翻譯

  • Joinpoint(連線點): 所謂的連擊點,就是你的業務邏輯中的每一個方法,都被稱作連線點。而且,和AspectJ和JBoss不同,Spring不支援欄位和構造器連線點,只支援方法級別的連線點。
  • Pointcut(切入點): 當你要對一個連線點進行額外的功能新增時,這個連線點就是切入點。
  • Advice(通知/增強): 通知就是你攔截了切點後要做的事情。根據你要做的時機,分為:前置通知、後置通知、返回通知、異常通知和環繞通知。
  • Introduction(引介): 引介是一種特殊的通知在不修改類程式碼的前提下,Introduction 可以在執行期為類動態地新增一些方法或 Field。
  • Target(目標物件): 代理的目標物件。
  • Weaving(織入): 是指把增強應用到目標物件來建立新的代理物件的過程。spring 採用動態代理織入,而 AspectJ 採用編譯期織入和類裝載期織入。
  • Proxy(代理) : 一個類被 AOP 織入增強後,就產生一個結果代理類。
  • Aspect(切面): 是切入點和通知(引介)的結合。

實戰演練

這次我們打算做一個簡單一點的功能:實現一個能夠進行加減乘除運算的計算器,並進行相應的日誌記錄 過程主要是以下幾步: 1.開發業務邏輯程式碼 2.開發切面程式碼 3.配置ioc,將計算器和切面配置到Spring容器中 4.切面配置,開啟AOP 對於配置的方式,主要是還是兩種方式:

Java配置:

public interface ArithmeticCalculator {

	int add(int i,int j);
	int sub(int i,int j);
	
	int mul(int i,int j);
	int div(int i,int j);
	
}

@Component("arithmeticCalculator")
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

	@Override
	public int add(int i,int j) {
		int result = i + j;
		return result;
	}

	@Override
	public int sub(int i,int j) {
		int result = i - j;
		return result;
	}

	@Override
	public int mul(int i,int j) {
		int result = i * j;
		return result;
	}

	@Override
	public int div(int i,int j) {
		int result = i / j;
		return result;
	}

}


package com.spring.demo.springaop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 可以使用 @Order 註解指定切面的優先順序,值越小優先順序越高
 */
@Aspect
@Component
public class LoggingAspect {
    @Pointcut("execution(public int com.spring.demo.springaop.ArithmeticCalculator.*(..))")
    public void declareJoinPoint() {}

    @Before("declareJoinPoint()")
    public void beforeMehtod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("the " + methodName + " begins with " + Arrays.asList(args));
    }


    @AfterReturning(value = "declareJoinPoint()",returning = "result")
    public void afterMethod(JoinPoint joinPoint,Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("the " + methodName + " ends successfully with result is " + result);
    }

    @AfterThrowing(value = "declareJoinPoint()",throwing = "e")
    public void afterException(JoinPoint joinPoint,Exception e) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("the " + methodName + "occurs a Exception by" + e.getMessage());
    }

    /**
     * 環繞通知需要攜帶 ProceedingJoinPoint 型別的引數.
     * 環繞通知類似於動態代理的全過程: ProceedingJoinPoint 型別的引數可以決定是否執行目標方法.
     * 且環繞通知必須有返回值,返回值即為目標方法的返回值
     */
	/*
	@Around("execution(public int com.spring.demo.springaop.ArithmeticCalculator.*(..))")
	public Object aroundMethod(ProceedingJoinPoint pjd){

		Object result = null;
		String methodName = pjd.getSignature().getName();

		try {
			//前置通知
			System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
			//執行目標方法
			result = pjd.proceed();
			//返回通知
			System.out.println("The method " + methodName + " ends with " + result);
		} catch (Throwable e) {
			//異常通知
			System.out.println("The method " + methodName + " occurs exception:" + e);
			throw new RuntimeException(e);
		}
		//後置通知
		System.out.println("The method " + methodName + " ends");

		return result;
	}
	*/
}



@Order(1)
@Aspect
@Component
public class VlidationAspect {

	@Before("com.spring.demo.springaop.LoggingAspect.declareJoinPoint()")
	public void validateArgs(JoinPoint joinPoint){
		System.out.println("-->validate:" + Arrays.asList(joinPoint.getArgs()));
	}
	
}

@EnableAspectJAutoProxy
@Configuration
@ComponentScan
public class MainConcig {

}


public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.spring.demo" +
                ".springaop");
        ArithmeticCalculator arithmeticCalculator = (ArithmeticCalculator) context.getBean("arithmeticCalculator");
        int add = arithmeticCalculator.add(100,200);
    }
}

複製程式碼

xml檔案配置

JavaBean還是這些,只是將各個註解刪除即可,而bean的配置和aop功能的開啟,由配置檔案來宣告。要引入aop名稱空間。

<!-- 配置 bean -->
	<bean id="arithmeticCalculator" 
		class="com.spring.demo.springaop.xml.ArithmeticCalculatorImpl"></bean>

	<!-- 配置切面的 bean. -->
	<bean id="loggingAspect"
		class="com.spring.demo.springaop.xml.LoggingAspect"></bean>

	<bean id="vlidationAspect"
		class="com.spring.demo.springaop.xml.VlidationAspect"></bean>

	<!-- 配置 AOP -->
	<aop:config>
		<!-- 配置切點表示式 -->
		<aop:pointcut expression="execution(* com.spring.demo.springaop.ArithmeticCalculator.*(int,int))"
			id="pointcut"/>
		<!-- 配置切面及通知 -->
		<aop:aspect ref="loggingAspect" order="2">
			<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
			<aop:after method="afterMethod" pointcut-ref="pointcut"/>
			<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
			<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
			<!--  
			<aop:around method="aroundMethod" pointcut-ref="pointcut"/>
			-->
		</aop:aspect>	
		<aop:aspect ref="vlidationAspect" order="1">
			<aop:before method="validateArgs" pointcut-ref="pointcut"/>
		</aop:aspect>
	</aop:config>
複製程式碼

切入點表示式說明(引用別人的,懶得寫了)

execution:匹配方法的執行(常用) execution(表示式) 表示式語法: execution([修飾符] 返回值型別 包名.類名.方法名(引數)) 寫法說明: 全匹配方式: public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account) 訪問修飾符可以省略 void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account) 返回值可以使用號,表示任意返回值 * com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account) 包名可以使用號,表示任意包,但是有幾級包,需要寫幾個*

  • ....AccountServiceImpl.saveAccount(com.itheima.domain.Account) 使用..來表示當前包,及其子包
  • com..AccountServiceImpl.saveAccount(com.itheima.domain.Account) 類名可以使用*號,表示任意類
  • com...saveAccount(com.itheima.domain.Account) 方法名可以使用號,表示任意方法
  • com...( com.itheima.domain.Account) 引數列表可以使用*,表示引數可以是任意資料型別,但是必須有引數
  • com...(*) 引數列表可以使用..表示有無引數均可,有引數可以是任意型別
  • com...(..) 全通配方式:
  • ...(..) 注: 通常情況下,我們都是對業務層的方法進行增強,所以切入點表示式都是切到業務層實現類。 execution( com.itheima.service.impl..(..))

補充說明: 引入通知 引入通知是一種特殊的通知型別. 它通過為介面提供實現類,允許物件動態地實現介面,就像物件已經在執行時擴充套件了實現類一樣。

引入通知可以使用兩個實現類 MaxCalculatorImpl 和 MinCalculatorImpl,讓 ArithmeticCalculatorImpl 動態地實現 MaxCalculator 和 MinCalculator 介面. 而這與從 MaxCalculatorImpl 和 MinCalculatorImpl 中實現多繼承的效果相同. 但卻不需要修改 ArithmeticCalculatorImpl 的原始碼。 引入通知也必須在切面中宣告。

程式碼演示