天天看點

SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP

文章目錄

  • AOP的引入
    • 案例中存在的問題
    • 解決辦法
    • 新的問題
    • 新問題的解決
      • 動态代理介紹
      • 基于接口的動态代理
      • 基于子類的動态代理
  • Spring 中的 AOP
    • 什麼是 AOP
    • AOP 中的術語
    • 學習 spring 中的 AOP 要明确的事

AOP的引入

在上一篇文章中傳送門,我們做了一個賬戶的 CRUD 的小案例,在講 AOP 之前,我們先來分析該案例,一步一步地了解為什麼要有 AOP 以及什麼是 AOP。

案例中存在的問題

  • 回顧上篇文章中的業務層代碼:
package com.cz.service.impl;

import com.cz.dao.AccountDao;
import com.cz.domain.Account;
import com.cz.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;


/**
 * 賬戶的業務層實作類
 */
package com.cz.service.impl;

import com.cz.dao.AccountDao;
import com.cz.domain.Account;
import com.cz.service.AccountService;

import java.util.List;


/**
 * 賬戶的業務層實作類
 */
public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccount() {
        return accountDao.findAllAccount();
    }

    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }

    public void saveAccount(Account account) {
        return accountDao.saveAccount(account);
    }

    public void updateAccount(Account account) {
        return accountDao.updateAccount(account);
    }

    public void removeAccount(Integer accountId) {
        return accountDao.removeAccount(accountId);
    }
}

           

問題就是:

  事務被自動送出了。換言之,我們使用了

connection

對象的

setAutoCommit(true)

此方式控制事務,如果我們每次都執行一條 sql 語句,沒有問題,但是如果業務方法一次要執行多條 sql語句,這種方式就無法實作功能了。請看下面的示範:

  • 修改業務層和持久層,添加一個轉賬方法

業務層:

/**
 * 賬戶業務層接口
 */
public interface AccountService {
    /**
     * 轉賬
     * @param sourceName  轉出賬戶名稱
     * @param targetName  轉入賬戶名稱
     * @param money  轉賬金額
     */
    void transfer(String sourceName,String targetName,Float money);
}


/**
 * 賬戶的業務層實作類
 */
public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;
    public void transfer(String sourceName, String targetName, Float money) {
        //1.根據名稱查詢轉出賬戶
        Account source = accountDao.findAccountByName(sourceName);
        //2.根據名稱查詢轉入賬戶
        Account target = accountDao.findAccountByName(targetName);
        //3.轉出賬戶減錢
        source.setMoney(source.getMoney()-money);
        //4.轉入賬戶加錢
        target.setMoney(target.getMoney()+money);
        //5.更新轉出賬戶
        accountDao.updateAccount(source);
        int i=1/0; //模拟轉賬異常
        //6.更新轉入賬戶
        accountDao.updateAccount(target);
    }
}
           

持久層:

/**
 * 賬戶的持久層接口
 */
public interface AccountDao {
    /**
     * 根據名稱查詢賬戶
     * @param accountName
     * @return 如果有唯一結果,就傳回。如果沒有結果就傳回null;
     *          如果結果集超過一個就抛異常
     */
    Account findAccountByName(String accountName);
}


/**
 * 賬戶的持久層實作類
 */

public class AccountDaoImpl implements AccountDao {
    public Account findAccountByName(String accountName) {
        try{
            List<Account> accounts =  runner.query("select * from account where name = ?",new BeanListHandler<Account>(Account.class),accountName);
            if (accounts == null || accounts.size() == 0){
                return null;
            }
            if (accounts.size() > 1){
                throw new RuntimeException("結果集不為1,資料異常");
            }
            return accounts.get(0);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}
           

實作類:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer(){
        accountService.transfer("張三","李四",200f);
    }
}
           
SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP
SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP
由于執行有異常,轉賬失敗。但是因為每次執行持久層方法都是獨立事務,導緻無法實作事務控制 ( 不符合事務的一緻性)

解決辦法

為了解決這個問題,我們可以讓業務層來控制事務的送出和復原。

首先我們先對業務層的實作類的代碼進行分析:

SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP
  • 建立工具類

    ConnectionUtils

    ,該類用于擷取和線程綁定的

    Connection

    對象,保證每個線程中隻能有一個控制事務的

    Connection

    對象
package com.cz.utils;

import javafx.scene.chart.PieChart;

import javax.sql.DataSource;
import java.sql.Connection;

/**
 * 連接配接的工具類,它用于從資料源中擷取一個連接配接,并且實作和線程的綁定
 */
public class ConnectionUtils {

    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 擷取目前線程上的連接配接
     * @return
     */
    public Connection getThreadConnection(){
        try {
            //1.先從ThredLocal擷取
            Connection conn = tl.get();
            //2.判斷目前線程上是否有連接配接
            if (conn == null){
                //3.從資料源上擷取一個連接配接,并且存入ThreadLocal中
                conn = dataSource.getConnection();
                // 與目前線程綁定
                tl.set(conn);
            }
            //4.傳回目前線程上的連接配接
            return conn;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 将連接配接和線程解綁
     */
    public void removeConnection() {
        tl.remove();
    }
}

           
  • 建立事務控制類

    TransactionManager

package com.cz.utils;

import java.sql.Connection;

/**
 * 和事務管理相關的工具類,它包含了開啟事務,送出事務,復原事務和釋放連接配接
 */
public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    /**
     * 開啟事務
     */
    public void beginTransaction(){
        try{
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 送出事務
     */
    public void commit(){
        try{
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 復原事務
     */
    public void rollback(){
        try{
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 釋放連接配接
     */
    public void release(){
        try{
            //将連接配接還回連接配接池
            connectionUtils.getThreadConnection().close();
            //将連接配接與線程解綁,防止下次使用報錯。因為連接配接被關閉後不能再調用
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
           
要注意

release()

方法中,因為使用連接配接池,是以調用

Connection

對象的

close()

方法後,是将連接配接歸還到連接配接池中,是以要把連接配接與綁定的線程解綁,不然下次從線程池中取出該線程,并擷取綁定的連接配接進行使用時,由于連接配接被調用過

close()

方法,會抛出異常

You can't operate on a closed Connection

(問題存在于 WEB 工程中)
  • 修改業務層代碼,為所有方法加上事務管理
package com.cz.service.impl;

import com.cz.dao.AccountDao;
import com.cz.domain.Account;
import com.cz.service.AccountService;
import com.cz.utils.TransactionManager;

import java.util.List;


/**
 * 賬戶的業務層實作類
 * 事務控制都在業務層
 */
public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;
    private TransactionManager tsManager;

    public void setTsManager(TransactionManager tsManager) {
        this.tsManager = tsManager;
    }

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccount() {
        try{
            //1.開啟事務
            tsManager.beginTransaction();
            //2.執行操作
            List<Account> accounts = accountDao.findAllAccount();
            //3.送出事務
            tsManager.commit();
            //4.傳回結果
            return accounts;
        }catch (Exception e){
            //5.復原操作
            tsManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //6.釋放連接配接
            tsManager.release();
        }
    }

    public Account findAccountById(Integer accountId) {
        try{
            //1.開啟事務
            tsManager.beginTransaction();
            //2.執行操作
            Account account = accountDao.findAccountById(accountId);
            //3.送出事務
            tsManager.commit();
            //4.傳回結果
            return account;
        }catch (Exception e){
            //5.復原操作
            tsManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //6.釋放連接配接
            tsManager.release();
        }

    }

    public void saveAccount(Account account) {
        try{
            //1.開啟事務
            tsManager.beginTransaction();
            //2.執行操作
            accountDao.saveAccount(account);
            //3.送出事務
            tsManager.commit();
        }catch (Exception e){
            //4.復原操作
            tsManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //5.釋放連接配接
            tsManager.release();
        }
    }

    public void updateAccount(Account account) {
        try{
            //1.開啟事務
            tsManager.beginTransaction();
            //2.執行操作
            accountDao.updateAccount(account);
            //3.送出事務
            tsManager.commit();
        }catch (Exception e){
            //4.復原操作
            tsManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //5.釋放連接配接
            tsManager.release();
        }
    }

    public void removeAccount(Integer accountId) {
        try{
            //1.開啟事務
            tsManager.beginTransaction();
            //2.執行操作
            accountDao.removeAccount(accountId);
            //3.送出事務
            tsManager.commit();
        }catch (Exception e){
            //4.復原操作
            tsManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //5.釋放連接配接
            tsManager.release();
        }

    }

    public void transfer(String sourceName, String targetName, Float money) {
        try{
            //1.開啟事務
            tsManager.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.送出事務
            tsManager.commit();
        }catch (Exception e){
            //4.復原操作
            tsManager.rollback();
        }finally {
            //5.釋放連接配接
            tsManager.release();
        }
    }
}
           
  • 修改持久層代碼,因為我們現在的

    Connection

    對象是由

    ConnectionUtils

    工具類來提供的(與線程綁定),而不是讓

    QueryRunner

    對象自動去連接配接池中取(沒有與線程綁定),是以

    QueryRunner

    對象不再需要注入資料源了。
package com.cz.dao.impl;

import com.cz.dao.AccountDao;
import com.cz.domain.Account;
import com.cz.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import java.util.List;


/**
 * 賬戶的持久層實作類
 */

public class AccountDaoImpl implements AccountDao {

    private QueryRunner runner;
    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    public List<Account> findAllAccount() {
        try{
            return runner.query(connectionUtils.getThreadConnection(),"select * from account",new BeanListHandler<Account>(Account.class));
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    public Account findAccountById(Integer accountId) {
        try{
            return runner.query(connectionUtils.getThreadConnection(),"select * from account where id = ?",new BeanHandler<Account>(Account.class),accountId);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    public int saveAccount(Account account) {
        try{
            return runner.update(connectionUtils.getThreadConnection(),"insert into account(name,money)values(?,? )",account.getName(),account.getMoney());
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    public int updateAccount(Account account) {
        try{
            return runner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    public int removeAccount(Integer accountId) {
        try{
            return runner.update(connectionUtils.getThreadConnection(),"delete from account  where id=?",accountId);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    public Account findAccountByName(String accountName) {
        try{
            List<Account> accounts =  runner.query(connectionUtils.getThreadConnection(),"select * from account where name = ?",new BeanListHandler<Account>(Account.class),accountName);
            if (accounts == null || accounts.size() == 0){
                return null;
            }
            if (accounts.size() > 1){
                throw new RuntimeException("結果集不為1,資料異常");
            }
            return accounts.get(0);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}
           
  • 編寫配置檔案,管理依賴關系
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--配置service-->
    <bean id="accountService" class="com.cz.service.impl.AccountServiceImpl">
        <!--注入dao-->
        <property name="accountDao" ref="accountDao"></property>
        <!-- 注入事務管理器-->
        <property name="tsManager" ref="tsManager"/>
    </bean>
    <!-- 配置dao對象-->
    <bean id="accountDao" class="com.cz.dao.impl.AccountDaoImpl">
        <!--注入QueryRunner-->
        <property name="runner" ref="runner"></property>
        <!-- 注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"/>
    </bean>
    <!-- 配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>

    <!-- 配置資料源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入連接配接資料庫的必備資訊-->
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring?serverTimezone=UTC&amp;characterEncoding=utf-8"/>
        <property name="user" value="root"/>
        <property name="password" value="7107883"/>
    </bean>

    <!-- 配置 Connection 工具類 ConnectionUtils-->
    <bean id="connectionUtils" class="com.cz.utils.ConnectionUtils">
        <!-- 注入資料源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事務管理器 -->
    <bean id="tsManager" class="com.cz.utils.TransactionManager">
        <!-- 注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"/>
    </bean>
</beans>
           

新的問題

  通過對業務層加上事務控制,此時的轉賬方法已經可以正常運作,但是由于我們的業務層每個方法都加上了事務控制,是以顯得業務層的方法特别臃腫,業務核心代碼可能就

accountDao.removeAccount(accountId);

這麼一句,其餘的都是和事務有關,這樣也就造成了業務層方法的耦合度很高。

  如果此時我們的

TransactionManager

類中的方法名進行了更改,那麼我們需要在每處調用的地方都進行修改,這樣不利于我們後期的開發。

新問題的解決

那麼如何解決呢?這裡我們可以使用動态代理,把事務的控制交給代理對象,是以下面我們先來看看關于動态代理的基本使用。

動态代理介紹

之前在講mybatis的入門案例的時候也給大家貼過兩篇其他大佬寫的動态代理,下面我們簡單的了解一下,想深入的朋友可以去看看那兩篇文章。

什麼是動态代理:

簡單來說就是使用反射動态建立代理對象,使用代理對象來代替目标對象,是以達到增強目标對象的功能

動态代理的特點

位元組碼随用随建立,随用随加載。它與靜态代理的差別也在于此,因為靜态代理是位元組碼一上來就建立好,并完成加載。裝飾者模式就是靜态代理的一種展現。

動态代理的常用兩種方式

基于接口的動态代理

基于子類的動态代理

基于接口的動态代理

提供者:JDK 官方的 Proxy 類。

要求:被代理類最少實作一個接口。

  • 在這裡我們使用廠家的例子:以前廠家不僅要對産品進行售後處理,還得負責銷售的環節,把産品賣給客戶。但是随着時間的推移,出現了一個新的角色:代理商,這時候廠家隻需要負責對産品的售後處理,銷售的事情交給代理商即可。如果客戶需要售後維修,那麼隻需要将産品交給代理商,由代理商送往廠家進行維修。如下圖:
    SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP
  • 定義接口,表示廠家需要具備的功能(銷售以及售後)
package com.cz.proxy;

/**
 * 生産廠家需要實作的接口
 */
public interface IProducer {

    /**
     * 銷售
     * @param money
     */
     void saleProduct(float money);

    /**
     * 售後
     * @param money
     */
     void afterService(float money);
}
           
  • 定義廠家類
package com.cz.proxy;

/**
 * 一個生産者
 */
public class Producer implements IProducer{

    /**
     * 銷售
     * @param money
     */
    public void saleProduct(float money){
        System.out.println("銷售産品,并拿到錢:"  +money);
    }

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

           
  • 定義測試類,進行動态代理
package com.cz.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 消費者
 */
public class Client {

    public static void main(String[] args) {
        //廠家
        final Producer producer = new Producer();

        // 該對象相當于代理商
        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 作用:執行被代理對象的任何方法都會被該方法攔截到
                     * @param proxy 代理對象的引用
                     * @param method 目前執行的方法
                     * @param args 目前執行方法所需的參數
                     * @return 傳回值類型與被代理對象方法一緻
                     * @throws Throwable
                     */
                    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())){
                            System.out.println("消費者花了 " + money + "元購買商品...");
                            // 對參數進行增強,代理商需要抽取 2 成利潤
                            returnValue = method.invoke(producer,money*0.8f);
                        }
                        return returnValue;
                    }
                });
        proxyProducer.saleProduct(10000f);
    }
}

           
  • 運作結果如圖
SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP
  • 使用

    Proxy.newProxyInstance()

    建立代理對象,參數如下:
    • ClassLoader

      :類加載器,用于加載代理對象位元組碼,和被代理對象使用相同的類加載器。固定寫法:

      被代理對象.getClass().getClassLoader()

    • Class[]

      :位元組碼數組,用于讓代理對象和被代理對象具有相同的接口方法。固定寫法:

      被代理對象.getClass().getInterfaces()

    • InvocationHandler

      : 處理器,用于提供增強的代碼,一般都是編寫一個該接口的匿名内部類。此接口的實作類都是誰用誰寫。
  • invoke()

    方法用于攔截被代理對象的方法,執行被代理對象的任何方法都會被該方法攔截到,參數如下:
    • Object proxy

      :代理對象的引用
    • Method method

      :目前執行的方法
    • Object[] args

      :目前執行方法所需的參數
    • 傳回值類型與被代理對象方法一緻
  • 匿名内部類通路外部方法的成員變量時都要求外部成員變量添加

    final

    修飾符,

    final

    修飾變量代表該變量隻能被初始化一次,以後不能被修改。參考連結:

    匿名内部類如何通路外部類的成員變量

    匿名内部類通路方法成員變量需要加final的原因及證明

基于子類的動态代理

要求:被代理對象不能是最終類
  • 還是使用上面的例子
  • 基于子類的動态代理是使用第三方庫

    CGLIB

    提供的

    Enhancer

    類,要求被代理類不能是被

    final 修飾的類(最終類)

  • 導入第三方庫依賴

    CGLIB

    或者手動導入jar 包

    cglib-2.1.3.jar

    asm.jar

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.1_3</version>
</dependency>
           

老樣子,報錯的看這兩篇文章傳送門1

傳送門2

  • 廠家類無需實作接口
package com.cz.cglib;

/**
 * 一個生産者
 */
public class Producer{

    /**
     * 銷售
     * @param money
     */
    public void saleProduct(float money){
        System.out.println("銷售産品,廠家拿到錢:"  +money);
    }

    /**
     * 售後
     * @param money
     */
    public void afterService(float money){
        System.out.println("提供售後服務,并拿到錢:"+money);
    }
}
           
  • 編寫測試類
package com.cz.cglib;

import com.cz.proxy.IProducer;
import com.cz.proxy.Producer;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 消費者
 */
public class Client {

    public static void main(String[] args) {
        //廠家
        final Producer producer = new Producer();

        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(),
                new MethodInterceptor() {
                    /**
                     *執行被代理對象的任何方法,都會經過該方法。在此方法内部就可以對被代理對象的任何方法進行增強。
                     * @param proxy
                     * @param method
                     * @param args
                     * 前三個和基于接口的動态代理是一樣的。
                     * @param methodProxy 目前執行方法的代理對象。
                     * @return 目前執行方法的傳回值
                     * @throws Throwable
                     */
                    @Override
                    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                        //提供增強代碼
                        Object returnValue = null;
                        //1.擷取方法執行的參數
                        Float money = (Float)args[0];
                        //2.判斷目前方法是不是銷售
                        if ("saleProduct".equals(method.getName())){
                            System.out.println("消費者花了 " + money + "元購買商品...");
                            // 對參數進行增強,代理商需要抽取 2 成利潤
                            returnValue = method.invoke(producer,money*0.8f);
                        }
                        return returnValue;
                    }
                });
        cglibProducer.saleProduct(120000f);
    }
}
           
  • 使用

    Enhancer.create()

    建立代理對象,參數如下:
    • Class

      : 位元組碼,用于指定被代理對象的位元組碼。

      固定寫法:被代理對象.getClass()

    • Callback

      : 回調接口,用于提供增強的代碼,一般都是編寫該接口的子接口實作類

      MethodInterceptor

  • intercept()

    方法用于攔截被代理對象的方法,執行被代理對象的任何方法都會被該方法攔截到,參數如下:
    • Object proxy

      :代理對象的引用
    • Method method

      :目前執行的方法
    • Object[] args

      :目前執行方法所需的參數
    • MethodProxy methodProxy

      :目前執行方法的代理對象
    • 傳回值類型與被代理對象方法一緻

動态代理的基本使用就講到這了,接下來我們回歸正題。如何解決問題:

  • 修改業務層代碼,将其恢複原樣,不再添加事務的控制代碼
  • 建立代理工廠,用于擷取 service 的代理對象
package com.cz.factory;

import com.cz.service.AccountService;
import com.cz.utils.TransactionManager;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 用于建立Service的代理對象的工廠
 */
public class BeanFactory {

    private AccountService accountService;
    private TransactionManager tsManager;

    public void setTsManager(TransactionManager tsManager) {
        this.tsManager = tsManager;
    }

    public final void setAccountService(AccountService accountService) {
        this.accountService = accountService;
    }

    /**
     * 擷取Service代理對象
     */
    public AccountService getAccountService() {
        return  (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 添加對事務的支援
                     *
                     * @param proxy
                     * @param method
                     * @param args
                     * @return
                     * @throws Throwable
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object rtValue = null;
                        try {
                            //1.開啟事務
                            tsManager.beginTransaction();
                            //2.執行操作
                            rtValue = method.invoke(accountService, args);
                            //3.送出事務
                            tsManager.commit();
                            //4.傳回結果
                            return rtValue;
                        } catch (Exception e) {
                            //5.復原操作
                            tsManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            //6.釋放連接配接
                            tsManager.release();
                        }
                    }
                });
    }
}

           
  • 修改配置檔案
<!-- 配置代理的service -->
    <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
    <!-- 配置 BeanFactory -->
    <bean id="beanFactory" class="com.cz.factory.BeanFactory">
        <!-- 注入 service -->
        <property name="accountService" ref="accountService"/>
        <!-- 注入事務管理器 -->
        <property name="tsManager" ref="tsManager"/>
    </bean>

    <!--配置service-->
    <bean id="accountService" class="com.cz.service.impl.AccountServiceImpl">
        <!--注入dao-->
        <property name="accountDao" ref="accountDao"></property>
    </bean>
           
到這裡的時候,改造已經完成,通過代理對象一樣可以控制住事務,同時業務層不再需要編寫一堆的重複事務控制代碼。但是如果每次管理事務的時候,我們都要像這樣自己建立代理對象,那不是也挺麻煩的嗎?有沒有更好的方法呢?這時候就需要 AOP 的出場了。

Spring 中的 AOP

什麼是 AOP

  • AOP 即面向切面程式設計,英文全稱為 Aspect Oriented Programming
    • 簡單的說它就是把我們程式重複的代碼抽取出來,在需要執行的時候,

      使用動态代理的技術

      ,在不修改源碼的基礎上,對我們的已有方法進行增強。
  • 作用
    • 在程式運作期間,不修改源碼對已有方法進行增強

  • 優勢
    • 減少重複代碼
    • 提高開發效率
    • 維護友善

AOP 中的術語

Joinpoint (連接配接點)

  • 所謂連接配接點是指那些

    被攔截到的方法

    ,在 spring 中,這些點指的是方法,因為 spring 隻支援方法類型的連接配接點
  • 比如說,我們的業務層中的方法都是連接配接點,因為我們對業務層中的所有方法都進行了攔截

Pointcut (切入點)

  • 所謂切入點是指我們要對哪些 Joinpoint 進行攔截的定義,簡單來說就是

    被增強的方法

  • 比如說,我們在業務層新增了一個方法

    test()

    ,但是在代理工廠中,我們不對該方法進行增強,而是直接放行,那麼此時的

    test()

    就不是切入點,僅僅是一個連接配接點,而其他的方法都被事務管理,也就是切入點
    SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP

Advice (通知/ 增強)

  • 所謂通知是指攔截到方法之後所要做的事情,簡單來說就是

    對切入點進行的增強操作

  • 通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知
    SSM之Sping系列(五)---- Spring中AOP的引入及相關概念AOP的引入Spring 中的 AOP

Introduction (引介)

  • 引介是一種特殊的通知,在不修改類代碼的前提下,Introduction 可以在運作期為類動态地添加一些方法或 Field

Target (目标對象)

  • 被代理的對象

Weaving (織入)

  • 是指把增強應用到目标對象來建立新的代理對象的過程

Proxy (代理)

  • 一個類被 AOP 織入增強後,就産生一個結果代理類

Aspect (切面)

  • 是切入點和通知(引介)的結合

學習 spring 中的 AOP 要明确的事

  1. 開發階段(我們做的)

      編寫核心業務代碼(開發主線):大部分程式員來做,要求熟悉業務需求。

      把公用代碼抽取出來,制作成通知。(開發階段最後再做):AOP 程式設計人員來做。

      在配置檔案中,聲明切入點與通知間的關系,即切面。:AOP 程式設計人員來做。

  2. 運作階段(Spring 架構完成的)

      Spring 架構監控切入點方法的執行。一旦監控到切入點方法被運作,使用代理機制,動态建立目标對

      象的代理對象,根據通知類别,在代理對象的對應位置,将通知對應的功能織入,完成完整的代碼邏輯運作。

這篇文章就到這了,有點長,能看完的都很了不起。下一篇文章将講解Spring 中基于注解的 AOP 配置