Spring AOP概念及作用


一:SpringAOP概念

  面向切面編程(Aspect Oriented Programming)提高了另一種角度來思考程序的結構,通過預編譯方式和運行期間的動態代理實現程序功能的統一維護的一種技術。它用這種結構方式來彌補了面向對象編程(Object Oriented Programming)的不足。除此以外,AOP還提供了切面,對關注點進行模塊化,使用模塊化對業務邏輯的各個部分隔離,從而使得各部分業務邏輯直接的耦合度降低,提高程序的可重用性。Spring AOP是是Spring的重要組件之一,但是Spring IOC並不依賴Spring AOP,這意味着我們可以自由選擇是否使用AOP,AOP還提供了強大的中間件解決方案,這使得Spring IOC更加完善了,我們可以通過AOP完成對事務管理、權限控制、日志監聽等。

1:Spring AOP介紹

①:切面(Aspect):
  一個關注點的模塊化,這個關注點可能會橫切多個對象。事務管理就是一個關於橫切關注點很好的例子,在Spring中我們可以通過XML或者注解
  來實現對程序的切面。
②:連接點(Join point):
  程序執行過程中的某個特定的點,比如某個方法被調用的時候或者處理異常的時候。在Spring AOP中一個連接點總是代表一個方法的執行,其實
  AOP攔截的方法就是一個連接點。
③:通知(Advice):
  在切面的某個連接點上執行的動作,通知類型包括“before”、“after”、“around”等。許多AOP框架都是以攔截器作為通知的模型,並維護一個
  以連接點為中心的攔截器鏈,Spring也不例外。總之就是AOP對攔截點的處理通過通知來執行,所以說通知是指一個方法被AOP攔截到的時候要執
  行的代碼。
④:切入點(Pointcut):
  匹配連接點的斷言。通知和切入點表達式關聯,並與切入點匹配的任何連接點上運行。切入點表達式如何跟連接點匹配是AOP的核心,Spring默認
  使用Aspectj作為切入點語法。說白了就是切面指定一個方法被AOP攔截到的時候要執行的代碼。
⑤:引入(Introduction):
  聲明額外的方法和字段。Spring AOP允許你向任何被通知對象引入一個新的接口(及其實現類)。說白了就是AOP允許在運行的時候動態的向
  代理對象實現新的接口來完成一些額外的功能,並不影響現有的對象功能
⑥:目標對象(Target Object):
  被一個或多個切面所通知的對象,也稱作被通知對象。由於Spring AOP是通過運行時代理實現的,所以這個對象永遠時被代理對象。說白了就是
  所以的對象在AOP中都會產生一個代理類,AOP整個過程都是針對代理類進行處理
⑦:AOP代理(AOP proxy):
  AOP框架創建的對象,用來實現切面(包括通知方法執行等功能),在Spring中AOP可以是JDK動態代理或Cglib代理。
⑧:織入(Weaving):
  把切面連接到其它應用程序類型或者對象上,並創建一個被通知對象。這些可以在編譯時(例如使用AspectJ編譯器),類加載時和運行時完成,
  Spring和其它純AOP框架一樣,在運行時完成織入,說白了就是把切面跟對象關聯並創建該對象的代理對象的過程。

補充:通知(Advice)的類型: ①:前置通知(Before advice):
    在某個連接點(Join point)之前執行的通知,但這個通知不能阻止連接點的執行(除非它拋出一個異常)。
②:返回后通知(After returning advice):
    在某個連接點(Join point)正常完成后執行的通知。例如,一個方法沒有拋出任何異常正常返回。
③:拋出異常后通知(After throwing advice):
    在方法拋出異常后執行的通知。
④:后置通知(After(finally)advice):
    當某個連接點(Join point)退出的時候執行的通知(不論是正常返回還是發生異常退出)。
⑤:環繞通知(Around advice):
    包圍一個連接點(Join point)的通知,如方法調用。這是最強大的一種通知類型。環繞通知可以在方法前后完成自定義的行為。
    它也會選擇是否繼續執行連接點或直接返回它們自己的返回值或拋出異常來結束執行。

 二:簡單案例引入AOP概念及手動解決(了解

1:轉賬問題代碼

本案例我使用注解加xml兩種方式的結合來完成案例的展示及問題所在  數據庫建表語句

<dependencies>
        <!--mysql驅動坐標-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.32</version>
        </dependency>

        <!--c3p0連接池坐標-->
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>

        <!--dbutils工具類坐標-->
        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
            <version>1.7</version>
        </dependency>

        <!--junit單元測試坐標-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <!--Spring-context主要坐標-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>

        <!--Spring-test測試坐標-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>

        <!--annotation坐標-->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
pom.xml
public class Student {
    private int sid;            //主鍵id
    private String sname;       //姓名
    private String ssex;        //性別
    private int sage;           //年齡
    private double scredit;     //學分
    private double smoney;      //零花錢
    private String saddress;    //住址
    private String senrol;      //入學時間
    //因為簡單的單表CRUD就不涉及到外鍵
    //private int fid;            //外鍵 連接家庭表信息學生對家庭,一對一
    //private int tid;            //外鍵 連接老師信息 學生對老師,一對一
    //創建構造器/get/set/toString就不展示了
}
實體類
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!--開啟注解-->
    <context:component-scan base-package="cn.xw"></context:component-scan>

    <!--把C3P0連接池放入容器中-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo_school"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123"></property>
    </bean>

    <!--注冊dbutils里面的QueryRunner對象-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>
</beans>
applicationContext.xml
######StudentDao接口

/**
 * 學生數據操作Dao接口
 * @author ant
 */
public interface StudentDao {
    //根據姓名查詢
    Student findByName(String name);
    //修改金額
    void update(Student student);
}

+++++++++++++++++++++++++++++++++++++++++

######StudentDaoImpl接口實現類

/**
 * 學生數據操作Dao實現類
 * @author ant
 */
@Repository("studentDao")
public class StudentDaoImpl implements StudentDao {
    //聚合dbutils工具類
    @Resource(name = "queryRunner")
    private QueryRunner query;

    //查詢單個學生根據id
    public Student findByName(String name) {
        try {
            List<Student> students = query.query("select * from student where sname=?", new BeanListHandler<Student>(Student.class), name);
            //如果沒數據返回null
            if (students == null || students.size() < 0) {
                return null;
            }
            //如果查詢的數據只要一個就返回
            if (students.size() == 1) {
                return students.get(0);
            }
        } catch (SQLException e) {
            throw new RuntimeException("查詢不到此學生 無法轉賬");
        }
        return null;
    }

    //更新學生
    public void update(Student student) {
        try {
            query.update("update student set smoney=? where sid=? ", student.getSmoney(), student.getSid());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

+++++++++++++++++++++++++++++++++++++++++

######StudentService接口

/**
 * 學生業務層Service 接口
 *
 * @author ant
 */
public interface StudentService {
    //轉賬
    void transfer(String currentName, String targetName, double money);
}

+++++++++++++++++++++++++++++++++++++++++

######StudentServiceImpl實現類

/**
 * 業務接口實現類 學生ServiceStudent
 * @author ant
 */
@Service("studentService")
public class StudentServiceImpl implements StudentService {
    //聚合數據操作層
    @Autowired
    @Qualifier("studentDao")
    private StudentDao studentDao;
    /**
     * 轉賬
     * @param currentName 當前學生姓名
     * @param targetName  目標學生姓名
     * @param money       轉賬金額
     */
    public void transfer(String currentName, String targetName, double money) {
        /*為了演示 我就不做那么細致的數據判斷等等操作*/
        //①:查詢學生
        Student currentStudent = studentDao.findByName(currentName);
        Student targetStudent = studentDao.findByName(targetName);
        //打印最初結果
        System.out.println("轉賬前: 當前學生姓名和余額:" + currentStudent.getSname() + "  " + currentStudent.getSmoney());
        System.out.println("轉賬前: 目標學生姓名和余額:" + targetStudent.getSname() + "  " + targetStudent.getSmoney());
        //②:開始轉賬
        currentStudent.setSmoney(currentStudent.getSmoney() - money);
        targetStudent.setSmoney(targetStudent.getSmoney() + money);

        //③:調用更新
        studentDao.update(currentStudent);
        studentDao.update(targetStudent);

        //打印最終結果
        Student currentStudentEnd = studentDao.findByName(currentName);
        Student targetStudentEnd = studentDao.findByName(targetName);
        System.out.println("轉賬后: 當前學生姓名和余額:" + currentStudentEnd.getSname() + "  " + currentStudentEnd.getSmoney());
        System.out.println("轉賬后: 目標學生姓名和余額:" + targetStudentEnd.getSname() + "  " + targetStudentEnd.getSmoney());
    }
}
StudentService接口及實現類/StudentDao接口及實現類
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {

    @Autowired
    @Qualifier("studentService")
    private StudentService studentService;
    @Test
    public void transfer() {
        //調用轉賬方法
        studentService.transfer("王生安", "王霞", 10.0);
    }
}
測試類

在完成了代碼的編輯,我們就來運行一下轉賬案例吧!

mysql> select * from student where sid in(1,5);   //轉賬前
+-----+--------+------+------+---------+--------+----------+------------+------+------+
| sid | sname  | ssex | sage | scredit | smoney | saddress | senrol     | fid  | tid  |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
|   1 | 王生安 | 女   |   23 |    21.0 |  460.6 | 安徽六安   | 2019-03-15 |    1 |    4 |
|   5 | 王  霞 | 男   |   16 |    55.6 |  293.0 | 安徽六安   | 2012-08-08 |    5 |    3 |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
mysql> select * from student where sid in(1,5);   //轉賬后
+-----+--------+------+------+---------+--------+----------+------------+------+------+
| sid | sname  | ssex | sage | scredit | smoney | saddress | senrol     | fid  | tid  |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
|   1 | 王生安 | 女   |   23 |    21.0 |  450.6 | 安徽六安   | 2019-03-15 |    1 |    4 |
|   5 | 王  霞 | 男   |   16 |    55.6 |  303.0 | 安徽六安   | 2012-08-08 |    5 |    3 |
+-----+--------+------+------+---------+--------+----------+------------+------+------+

//程序運行結果
轉賬前: 當前學生姓名和余額:王生安  460.6
轉賬前: 目標學生姓名和余額:王  霞   293.0
轉賬后: 當前學生姓名和余額:王生安  450.6
轉賬后: 目標學生姓名和余額:王  霞  303.0
運行案例演示

  其實上面這個運行是正常操作,但是如果在轉賬的時候遇到問題的時候怎么辦,比如我在StudentServiceImpl實現類里面的轉賬方法我來手動寫個異常,接下來運行轉賬就好出現很可怕的問題

//③:調用更新
studentDao.update(currentStudent);
int a=1/0;//必拋異常
studentDao.update(targetStudent);
//程序運行的結果  在轉賬的中間發生的異常,所以后面是異常打印
轉賬前: 當前學生姓名和余額:王生安  450.6
轉賬前: 目標學生姓名和余額:王霞  303.0
java.lang.ArithmeticException: / by zero

//查看數據庫
//轉賬前
mysql> select * from student where sid in(1,5);
+-----+--------+------+------+---------+--------+----------+------------+------+------+
| sid | sname  | ssex | sage | scredit | smoney | saddress | senrol     | fid  | tid  |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
|   1 | 王生安 | 女   |   23 |    21.0 |  450.6  | 安徽六安 | 2019-03-15 |    1 |    4 |
|   5 | 王霞   | 男   |   16 |    55.6 |  303.0  | 安徽六安 | 2012-08-08 |    5 |    3 |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
//轉賬后
mysql> select * from student where sid in(1,5);
+-----+--------+------+------+---------+--------+----------+------------+------+------+
| sid | sname  | ssex | sage | scredit | smoney | saddress | senrol     | fid  | tid  |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
|   1 | 王生安 | 女   |   23 |    21.0 |  440.6  | 安徽六安 | 2019-03-15 |    1 |    4 |
|   5 | 王霞   | 男   |   16 |    55.6 |  303.0  | 安徽六安 | 2012-08-08 |    5 |    3 |
+-----+--------+------+------+---------+--------+----------+------------+------+------+

這里出現了一個問題,因為在轉賬的時候突然遇到問題,導致轉出去的錢已經扣除了,而執行目標學生加錢的前一步遇到異常,所以就程序停止下來了,
后面就沒執行了,因為每次更新的事務不一樣,執行完自動提交事務,解決方法只有統一事務,等全部執行完才提交,遇到異常回滾事務
問題代碼運行結果

2:使用代碼來解決前面的事務問題

  使用代碼來控制事務,其原理就是讓當前線程下只開啟一個Connection連接,然后開啟事務,異常就回滾,上面的pom.xml、實體類、測試類沒發生改變,下面就不再重復寫了

注:java.lang.ThreadLocal類是為了給當前線程提供局部變量,用於在當前線程中共享數據。ThreadLocal工具底層就是相當於一個Map,key次方當前線程,value存放需要共享的數據;但是key我們無法設置默認就是當前線程,值我們可以設置任意類型,前提每個線程只可綁定一個共享數據,若有多個請存放到對象里,否則會被覆蓋

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <!--開啟注解-->
    <context:component-scan base-package="cn.xw"></context:component-scan>
    <!--把C3P0連接池放入容器中-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo_school"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123"></property>
    </bean>
    <!--注冊dbutils里面的QueryRunner對象-->
    <!--因為要保證Connection在當前線程下是唯一的,QueryRunner就不依賴它了,后期傳入當前線程下的Connection-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner"></bean>
</beans>
applicationContext.xml
##### StudentDao接口
/**
 * 學生數據操作Dao接口
 * @author ant
 */
public interface StudentDao {
    //根據姓名查詢
    Student findByName(String name);
    //修改金額
    void update(Student student);
}

+++++++++++++++++++++++++++++++++++++++++

#####StudentDaoImpl實現類

/**
 * 學生數據操作Dao實現類
 * @author ant
 */
@Repository("studentDao")
public class StudentDaoImpl implements StudentDao {
    //聚合dbutils工具類
    @Resource(name = "queryRunner")
    private QueryRunner query;
    //聚合ConnectionUtils 返回一個線程安全的Connection連接,說白了就是當前線程下的Connection
    @Resource(name="connectionUtils")
    private ConnectionUtils connectionUtils;

    //查詢單個學生根據id
    public Student findByName(String name) {
        try {
            List<Student> students = query.query(connectionUtils.getThreadConnection(),"select * from student where sname=?", new BeanListHandler<Student>(Student.class), name);
            //如果沒數據返回null
            if (students == null || students.size() < 0) {
                return null;
            }
            //如果查詢的數據只要一個就返回
            if (students.size() == 1) {
                return students.get(0);
            }
        } catch (SQLException e) {
            throw new RuntimeException("查詢不到此學生 無法轉賬");
        }
        return null;
    }

    //更新學生
    public void update(Student student) {
        try {
            query.update(connectionUtils.getThreadConnection(),"update student set smoney=? where sid=? ", student.getSmoney(), student.getSid());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

++++++++++++++++++++++++++++++++++++++++
##### StudentService接口

/**
 * 學生業務層Service 接口
 * @author ant
 */
public interface StudentService {
    //轉賬
    void transfer(String currentName, String targetName, double money);
}

+++++++++++++++++++++++++++++++++++++++++

##### StudentServiceImpl實現類

/**
 * 業務接口實現類 學生ServiceStudent
 *
 * @author ant
 */
@Service("studentService")
public class StudentServiceImpl implements StudentService {
    //聚合數據操作層
    @Autowired
    @Qualifier("studentDao")
    private StudentDao studentDao;

    //聚合 TransactionManger事務工具
    @Autowired
    @Qualifier("transactionManger")
    private TransactionManger manger;

    /**
     * 轉賬
     * @param currentName 當前學生姓名
     * @param targetName  目標學生姓名
     * @param money       轉賬金額
     */
    public void transfer(String currentName, String targetName, double money) {
        /**
         * 這里我使用了try catch,在里面寫了對事務的每一步操作,都是調用工具類TransactionManger
         * 這樣寫的壞處 如果后期Service類里面有特別多方法 而且每個方法都要有try catch處理異常從而控制事務
         * 這樣就會使代碼變得臃腫,所以后面會使用代理方式來完成改造
         */
        try {
            manger.beginTransaction();      //開啟事務
            /*為了演示 我就不做那么細致的數據判斷等等操作*/
            //①:查詢學生
            Student currentStudent = studentDao.findByName(currentName);
            Student targetStudent = studentDao.findByName(targetName);
            //打印最初結果
            System.out.println("轉賬前: 當前學生姓名和余額:" + currentStudent.getSname() + "  " + currentStudent.getSmoney());
            System.out.println("轉賬前: 目標學生姓名和余額:" + targetStudent.getSname() + "  " + targetStudent.getSmoney());
            //②:開始轉賬
            currentStudent.setSmoney(currentStudent.getSmoney() - money);
            targetStudent.setSmoney(targetStudent.getSmoney() + money);
            //③:調用更新
            studentDao.update(currentStudent);
            int a = 1 / 0;//必拋異常
            studentDao.update(targetStudent);
            //打印最終結果
            Student currentStudentEnd = studentDao.findByName(currentName);
            Student targetStudentEnd = studentDao.findByName(targetName);
            System.out.println("轉賬后: 當前學生姓名和余額:" + currentStudentEnd.getSname() + "  " + currentStudentEnd.getSmoney());
            System.out.println("轉賬后: 目標學生姓名和余額:" + targetStudentEnd.getSname() + "  " + targetStudentEnd.getSmoney());
            manger.commit();        //提交事務
        } catch (Exception e) {
            manger.rollback();      //回滾事務
        } finally {
            manger.close();         //關閉事務
        }
    }
}
StudentService接口及實現類/StudentDao接口及實現類
/**
 * 工具類 保證Connection在多線程下安全
 * @author ant
 */
@Component(value = "connectionUtils")
public class ConnectionUtils {
    //創建一個ThreadLocal,其實這個是線程局部變量
    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
    //注入DataSource數據連接池
    @Autowired
    @Qualifier(value = "dataSource")
    private DataSource dataSource;

    //從連接池獲取保存到局部變量池后返回Connection連接
    public Connection getThreadConnection() {
        //判斷tl里面如果沒有連接則創建,反之返回連接
        if (tl.get() == null) {
            try {
                tl.set(dataSource.getConnection());
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return tl.get();
    }

    //移除常量池里的變量對象
    public void close() {
        tl.remove();
    }
}
工具類 ConnectionUtils 返回一個當前線程下安全的Connection連接
/**
 * 用來處理事務的類
 * @author ant
 */
@Component("transactionManger")
public class TransactionManger {

    //注入 從線程局部變量類下獲取Connection連接 方便統一事務
    @Resource(name="connectionUtils")
    private ConnectionUtils connectionUtils;

    //開啟事務
    public void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
            System.out.println("設置事務提交方式為手動提交");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    //提交事務
    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
            System.out.println("完成提交事務");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    //回滾事務
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
            System.out.println("回滾事務執行");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    //關閉連接 (還回池中 不是真正關閉)
    public void close(){
        try {
            //把Connection連接還回池中
            connectionUtils.getThreadConnection().close();
            //移除線程局部變量
            connectionUtils.close();
            System.out.println("關閉連接");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
工具類 TransactionManger 用來管理事務 方便后期插入Service層業務從而達到提交或回滾

  在完成上面的代碼后我們來一個簡答的測試,因為上一段代碼的StudentServiceImpl實現類的轉賬方法我手動添加個異常了,看這次轉賬失敗是否可以回滾!

//轉賬前
mysql> select * from student where sid in(1,5);
+-----+--------+------+------+---------+--------+----------+------------+------+------+
| sid | sname  | ssex | sage | scredit | smoney | saddress | senrol     | fid  | tid  |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
|   1 | 王生安 | 女   |   23 |    21.0 |  440.6 | 安徽六安   | 2019-03-15 |    1 |    4 |
|   5 | 王霞   | 男   |   16 |    55.6 |  303.0 | 安徽六安   | 2012-08-08 |    5 |    3 |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
//程序執行轉賬出現異常和程序打印信息
設置事務提交方式為手動提交
轉賬前: 當前學生姓名和余額:王生安  440.6
轉賬前: 目標學生姓名和余額:王霞  303.0
回滾事務執行
關閉連接
//異常出現后 我再次查詢數據庫
mysql> select * from student where sid in(1,5);
+-----+--------+------+------+---------+--------+----------+------------+------+------+
| sid | sname  | ssex | sage | scredit | smoney | saddress | senrol     | fid  | tid  |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
|   1 | 王生安 | 女   |   23 |    21.0 |  440.6 | 安徽六安   | 2019-03-15 |    1 |    4 |
|   5 | 王霞   | 男   |   16 |    55.6 |  303.0 | 安徽六安   | 2012-08-08 |    5 |    3 |
+-----+--------+------+------+---------+--------+----------+------------+------+------+
程序運行結果

  我們通過代碼改造,已經實現了事務控制,但是我們添加了事務也產生了一個新的問題業務層方法變得臃腫了,里面充斥着很多重復代碼。並且業務層方法和事務控制方法耦合了。試想一下,如果我們此時提交,回滾,釋放資源中任何一個方法名變更,都需要修改業務層的代碼,況且這還只是一個業務層實現類,而實際的項目中這種業務層實現類可能有十幾個甚至幾十個。

3:使用JDK動態代理完成事務問題

  在完成了使用普通代碼控制線程當前的Connection連接唯一后對其事務操作后,發現代碼特別臃腫,每次在寫Service實現的方法后都要處理事務,發現方法一旦變多將是毀滅性打擊,有什么方法可以使不在Service下寫事務處理就可以達到想要的效果呢?大家肯定第一個就想到動態代理,沒錯,這就是一個好辦法。

注:除了這下面3個地方要修改,其它全部和上一個案例一樣

/**
 * 代理工廠類
 * @author ant
 */
public class ProxyFactory {

    //傳入被代理對象StudentService
    private StudentService studentService;
    //傳入事務
    private TransactionManger manger;
    //后面2個set方法方便XML注入數據
    public void setStudentService(StudentService studentService) {
        this.studentService = studentService;
    }
    public void setManger(TransactionManger manger) {
        this.manger = manger;
    }
    
    //返回一個StudentService 后期在xml通過工廠方式注入給測試類 使用工廠調用
    public StudentService getStudentService() {
        return (StudentService) Proxy.newProxyInstance(studentService.getClass().getClassLoader(), studentService.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object obj = null;
                        try {
                            manger.beginTransaction();//開啟事務
                            obj = method.invoke(studentService, args);
                            manger.commit();         //提交事務
                        } catch (Exception e) {
                            manger.rollback();       //回滾事務
                            throw new RuntimeException("轉賬失敗");
                        } finally {
                            manger.close();          //關閉事務
                        }
                        return obj;
                    }
                });
    }
}
編寫一個代理工廠類放utils包下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <!--開啟注解-->
    <context:component-scan base-package="cn.xw"></context:component-scan>
    <!--把C3P0連接池放入容器中-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo_school"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123"></property>
    </bean>
    <!--注冊dbutils里面的QueryRunner對象-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner"></bean>
    
    <!--創建代理工廠類 分別注入對象-->
    <bean id="proxyFactory" class="cn.xw.utils.ProxyFactory">
        <property name="studentService" ref="studentService"></property>
        <property name="manger" ref="transactionManger"></property>
    </bean>
    <!--通過代理工廠類 來生成一個StudentService代理對象-->
    <bean id="studentServiceProxy" factory-bean="proxyFactory" factory-method="getStudentService"></bean>
</beans>
更改applicationContext.xml
//更改測試類
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:applicationContext.xml") public class Client { @Autowired @Qualifier("studentServiceProxy")  //注入代理對象的StudentService private StudentService studentService; @Test public void transfer() { //調用轉賬方法 studentService.transfer("王生安", "王霞", 10.0);
     //其打印結果和上一個案例一樣 } }

  小總結:學完動態代理完成對事務的控制發現動態代理要方便簡單,但是要注意的是JDK動態代理的類必須要有一個父類接口,否則無法代理,但是如果使用Cglib代理的話則不需要  詳解代理模式

三:Spring AOP切面解決事務(非常重要

   第一節中我們介紹了AOP,但到這里才真正運用Spring AOP,它是Spring的第二核心,在程序運行期間,對已有的方法進行加強

准備:maven坐標導入

  <!--AOP核心功能 如代理工廠等等-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>

        <!--支持解析切入點表達式等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>

        <!--支持AOP相關注解等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.5</version>
        </dependency>

  介紹:我們要想實現Spring AOP編程就必須導入上面的幾個包,但是隨着Spring版本的不斷更新及maven的導包的更新,導致有些坐標不導入也是可行的!還有一個要說明的Spring的Aop本身就是通過AspectJ實現的,然而在Spring較高版本(大概在3.0)之后,就不再一起發布以來包了,而是將aspectjweaver作為可選依賴引入,所以說Spring Aop依賴aspectj來實現。
①:spring-aop:
  當前的這個坐標在maven工程中可以不用手動導入,在maven中導入spring-context坐標后,其實maven就為我們默認導入了spring-aop坐標了
②:aspectjweaver:
  這個坐標是我們在使用AOP切面編程的時候必須導入的,因為它可以解析切入點表達式,但是呢aspectjweaver包含了aspectirt
③:aspectjrt:
  這個坐標說明開啟AOP的注解功能,但是aspectjweaver已經包含了它,所以單單注解可以使用此坐標

1:基於XML配置完成Spring AOP(入門)

 <dependencies>
        <!--導入Spring核心包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--導入Spring測試包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--導入AOP切入點表達式解析包-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--junit單元測試-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--annotation -->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
pom.xml坐標文件
##### StudentDao接口

public interface StudentDao {
    //模擬查詢全部
    void findAll();
    //模擬查詢一個
    Object findById();
    //模擬添加數據
    void saveStudet(Object obj);
}

+++++++++++++++++++++++++++++++++++++++++

##### StudentDaoImpl實現類

@Repository(value="studentDao")
public class StudentDaoImpl implements StudentDao {
    //模擬查詢全部
    public void findAll() {
        System.out.println("查詢全部完成 現在打印");
    }
    //模擬查詢一個
    public Object findById() {
        System.out.println("查詢單個完成");
        return null;
    }
    //模擬添加數據
    public void saveStudet(Object obj) {
        System.out.println("添加數據完成");
    }
}

“++++++++++++++++++++++++++++++++++++++++

##### StudentService接口

public interface StudentService {
    //模擬查詢全部
    void findAll();
    //模擬查詢一個
    Object findById();
    //模擬添加數據
    void saveStudet(Object obj);
}

+++++++++++++++++++++++++++++++++++++++++

##### StudentServiceImpl實現類

@Service(value="studentService")
public class StudentServiceImpl implements StudentService {
    @Autowired
    @Qualifier(value="studentDao")
    private StudentDao studentDao;
    //模擬查詢全部
    public void findAll() {
        studentDao.findAll();
    }
    //模擬查詢一個
    public Object findById() {

        return studentDao.findById();
    }
    //模擬添加數據
    public void saveStudet(Object obj) {
        studentDao.saveStudet(obj);
    }
}
service接口及實現類/dao接口及實現類
/**
 * 編寫模擬事務類
 * @author ant
 */
public class TransactionManager {
    public void beforePrint() {
        System.out.println("前置通知");
    }
    public void afterPrint() {
        System.out.println("最終通知");
    }
    public void afterReturningPrint() {
        System.out.println("后置運行通知");
    }
    public void afterWarningPrint() {
        System.out.println("后置異常通知");
    }
}
TransactionManager模擬事務類
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <!--開啟注解掃描-->
    <context:component-scan base-package="cn.xw"></context:component-scan>
    <!--把事務類放入容器-->
    <bean id="transaction" class="cn.xw.utils.TransactionManager"></bean>
    <!--編寫aop切面-->
    <aop:config>
        <aop:aspect id="aspects" ref="transaction">
            <!--編寫前 后 異常和運行通知-->
            <aop:before method="beforePrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())"></aop:before>
            <aop:after-returning method="afterReturningPrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())"></aop:after-returning>
            <aop:after-throwing method="afterWarningPrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())"></aop:after-throwing>
            <aop:after method="afterPrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>
applicationContext.xml
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    @Autowired
    @Qualifier("studentService")
    private StudentService studentService;
    @Test
    public void fun() {
        studentService.findAll();
    }
}
測試類
###運行結果
前置通知 查詢全部完成 現在打印 后置運行通知 最終通知

2:介紹導入的applicationContext.xml約束

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"        
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
     <!--導入上面的3條鏈接約束 代表開啟AOP切面功能 -->
</beans>

3:通知類編寫

public class TransactionManager {
    public void beforePrint() { System.out.println("前置通知");}
    public void afterPrint() { System.out.println("最終通知"); }
    public void afterReturningPrint() { System.out.println("后置運行通知");}
    public void afterWarningPrint() { System.out.println("后置異常通知"); }
}

  上面這個是一個簡單的通知類,和入門案例的一樣,其實最重要的也就是這個類了,這4個通知類里面可以寫像我們之前處理事務代碼,也可以寫各種增強事務代碼,包括后期寫打印日志、處理事務、代碼增強、權限控制等等。但是僅僅一個通知類是無法運行的,必須要在XML里面配置AOP才可以指定通知

4:配置XML里的AOP

<!--把通知類放入容器-->
 <bean id="manger" class="cn.xw.utils.TransactionManager"></bean>

 <!--配置AOP切面-->
  <aop:config>
    <!--配置切面 說白了就是使用哪個通知類去增強那些要增強的方法-->
      <aop:aspect id="aspects" ref="manger">
        <!--配置前置通知:在切入點表達式執行前執行 說白了就是在增強的方法之前執行-->
        <aop:before method="beforePrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())" />
        <!--配置后置運行通知:在切入點方法正常執行時執行-->
        <aop:after-returning method="afterReturningPrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())" />
        <!--配置異常通知:在程序運行出異常執行-->
        <aop:after-throwing method="afterWarningPrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())" />
        <!--配置最終通知:無論切入點表達式是否有異常,在執行完方法的前一步執行-->
        <aop:after method="afterPrint" pointcut="execution(public void cn.xw.service.impl.StudentServiceImpl.findAll())" />
      </aop:aspect>
  </aop:config>

  通過上面的配置可以看出,我配置的切入點方法都是findAll,那如果我要對當前Service下的所有方法都要增強怎么辦呢?是不是每個方法都要寫前、后、異常、運行通知呢,肯定是不用的。而且現在的代碼第一眼看上去不舒服,pointcut后面跟一大串切入點表達式,這個后面會慢慢簡化

①:aop:config 標簽

  用於聲明AOP配置,把AOP的配置全部寫在里面

屬性:
 proxy-target-class:
  用於指定代理方式;默認false;當設置true時Spring內部就會采用cglib代理方式,前提子類不能為final,要確保待增強的子類可繼承
 expose-proxy:
  用於指定是否暴露代理對象,通過AopContext可以進行直接訪問代理對象
<aop:config proxy-target-class="false" expose-proxy="false">
  ......省略內部配置 </aop:config>

②:aop:aspect 標簽

  它里面有2個常用的屬性,分別是id和ref,另一個不常用的order

id:給切面起一個唯一標識,因為aop:aspect標簽可以有多個 ref:引用配置好的通知類bean的id
order:
用於指定多個切面中,相同通知類型的執行順序,設置整數,數值越大優先權越小
<aop:config proxy-target-class="false" expose-proxy="false">
<aop:aspect id="aspects" ref="transactionManger" order="1">
...省略部分內部配置
</aop:aspect>
</aop:config>

③:aop:pointcut 標簽

  此標簽可以配置切入點表達式,可以作用域在外部當全局,也可作用在內部當局部

<aop:config proxy-target-class="false" expose-proxy="false">
  <!--全局,可以提供全部aop:aspect標簽使用 前提要在全部aop:aspct標簽前面-->   <aop:pointcut id="pointGlobal" expression="execution(* cn.xw.service.impl.*.*(..))"/>   <aop:aspect id="aspects" ref="transactionManger" order="1">
    <!--作用域內部 僅供當前的aop:aspect使用-->     <aop:pointcut id="pointLocal" expression="execution(* cn.xw.service.impl.*.*(..))"/> ...省略通知配置后面有介紹   </aop:aspect> </aop:config>

④:aop:aspectj-autoproxy 標簽

  用於開啟注解切面通知

⑤:常用的幾種通知

<aop:config">
    <aop:pointcut id="point" expression="execution(* cn.xw.service.impl.*.*(..))"/>
    <aop:aspect id="aspects" ref="transactionManger">
        <!--編寫前 后 異常和運行通知-->
        <aop:before method="beforePrint" pointcut-ref="point" />
        <!--配置了returning成功后返回數據的變量名稱-->
        <aop:after-returning method="afterReturningPrint" pointcut-ref="point" returning="successMessage"/>
        <!--配置了throwing失敗后返回數據的變量名稱-->
        <aop:after-throwing method="afterWarningPrint" pointcut-ref="point" throwing="errorMessage"/>
        <aop:after method="afterPrint" pointcut-ref="point"/>
    </aop:aspect>
</aop:config>

通知介紹
    aop:before:用於配置前置通知。指定增強的方法在切入點方法之前執行。切入點方法執行之前執行
    aop:after-returning:用於配置后置通知。切入點方法正常執行之后。它和異常通知只能有一個執行
    aop:after-throwing:用於配置異常通知。切入點方法執行產生異常后執行。它和后置通知只能執行一個
    aop:after:用於配置最終通知。無論切入點方法執行時是否有異常,它都會在其后面執行。
    aop:around:用於配置環繞通知。集成上面4種通知
屬性介紹:
    method:指定通知中方法的名稱。
    pointct:定義切入點表達式
    pointcut-ref:指定切入點表達式的引用
    returning:用於指定成功通知后返回的數據變量名稱
    throwing:用於指定異常通知中異常變量名稱
    arg-names用於指定通知方法的參數名稱,要求表達式中必須有描述args語句
        如:<aop:pointcut id="xx" expression="execution(xx) and args(xx)"/> //通知類 @Component public class TransactionManger { //開啟事務  JoinPoint:接收原方法的參數,下面的一些通知均可使用 public void beforePrint(JoinPoint joinPoint) { System.out.println("開啟事務先打印增強的方法參數:"+Arrays.toString(joinPoint.getArgs())); System.out.println("開啟事務"); } //提交事務  在通知xml配置了成功提交后返回數據的參數名稱 public void afterReturningPrint(Object successMessage) { System.out.println("打印成功事務返回的信息"+successMessage); System.out.println("提交事務"); } //回滾事務  配置了失敗后的參數名稱,一定要使用Throwable public void afterWarningPrint(Throwable errorMessage) { System.out.println("回滾事務先打印增強的方法參數:"+Arrays.toString(joinPoint.getArgs())); System.out.println("打印失敗事務返回的信息"+errorMessage); System.out.println("回滾事務"); } //關閉連接 public void afterPrint() { System.out.println("關閉連接"); } }

  關於arg-names屬性詳細介紹

⑥:切入點表達式說明

  在上面我們可以看到每個通知里面都要使用一個pointct屬性,但是里面的屬性該寫什么呢?接下來我就和大家介紹一下,切入點表達式解析后可以准確找到要通知的方法,比如:

  pointcut="execution( public   void   cn.xw.service.impl.StudentServiceImpl . findAll() )"

大家可以很清晰看出這一個切入點表達式,每個切入點表達式都要被execution包裹。

execution(表達式)
表達式語法:execution([修飾符] 返回值類型 包名.類名.方法名(參數))

寫法說明:
①:全匹配方式
execution( public void cn.xw.StudentService.impl.StudentServiceImpl.saveStudent(cn.xw.domain.Student) ) ②:訪問修飾符可以省去
execution( void cn.xw.StudentService.impl.StudentServiceImpl.saveStudent(cn.xw.domain.Student) ) ③:返回值類型可以省去 用*表示
execution( * cn.xw.StudentService.impl.StudentServiceImpl.saveStudent(cn.xw.domain.Student) ) ④:包名可以使用*,表示任意包,但是有幾級包,需要寫幾個 *
execution( * *.*.*.*.StudentServiceImpl.saveStudent(cn.xw.domain.Student) ) ⑤:優化第④寫法 使用..來表示當前包,及其子包
execution( * cn..StudentServiceImpl.saveStudent(cn.xw.domain.Student) ) ⑥:類名可以使用*號 表示任意類
execution( * cn..*.saveStudent(cn.xw.domain.Student) ) ⑦:方法名可以使用*號 表示任意方法
execution( * cn..*.*(cn.xw.domain.Student) ) ⑧:參數列表可以使用*,表示參數可以是任意數據類型,但是必須有參數
execution( * cn..*.*(*) ) ⑨:參數列表可以使用..表示有無參數均可,有參數可以是任意類型
execution( * cn..*.*(..) ) ⑩:全通配 每個方法都會被攔截通知
execution( * *..*.*(..) )
注:通常情況下,我們都是對業務層的方法進行增強,所以切入點表達式都是切到業務層實現類。
execution(* cn.xw.service.impl.*.*(..))

⑦:配置切入點表達式並引入

   大家看上面一副我畫黃色底線的切入點表達式了吧,不是句子寫錯了,大家是否發現,我每寫一個通知就要重復寫一下切入點表達式,那有沒有把這些表達式抽取出去的方法呢?顯然是有的,接下來我就帶大家來看一下

<!--配置AOP切面-->
    <aop:config>
        <!--切入點表達式抽取 配置在外邊表示全局的,可以供多個配置切面使用--> <aop:pointcut id="pidOn" expression="execution(* cn.xw.service.impl.*.*(..))"/>
        <!--配置切面-->
        <aop:aspect id="aspects" ref="manger">
            <!--切入點表達式抽取 配置在里面表示局部的,可以供自己使用--> <aop:pointcut id="pidIn" expression="execution(* cn.xw.service.impl.*.*(..))"/>
            <!--配置前置通知-->
            <aop:before method="beforePrint" pointcut-ref="pidOn" />
            <!--配置后置運行通知-->
            <aop:after-returning method="afterReturningPrint" pointcut-ref="pidOn" />
            <!--配置異常通知-->
            <aop:after-throwing method="afterWarningPrint" pointcut-ref="pidOn"/>
            <!--配置最終通知-->
            <aop:after method="afterPrint" pointcut-ref="pidOn" />
        </aop:aspect>
    </aop:config>
外部使用 <aop:pointcut>標簽來完成切入點表達式的抽取
內部使用 pointcut-ref=""屬性來引用外部標簽的id

所有種寫法才是后期經常使用的,針對前面的繁瑣步驟只是來個鋪墊,大家只需要把后面的寫法記住即可!

5:XML環繞通知

它是 spring 框架為我們提供的一種可以在代碼中手動控制增強代碼什么時候執行的方式。大家可以聯想一下代理模式,要使用環繞模式,那么就要在通知類上面編寫一個環繞通知的方法。
#####在TransactionManage類中添加環繞通知方法

 /**
     * 環繞通知
     * @param pp spring 框架為我們提供了一個接口:ProceedingJoinPoint,它可以作為環繞通知的方法參數。
     *           在環繞通知執行時,spring 框架會為我們提供該接口的實現類對象,我們直接使用就行。
     */
    public Object aroundPrint(ProceedingJoinPoint pp) {
        //定義返回值
        Object obj = null;
        try {
            //獲取方法執行的參數
            Object[] args = pp.getArgs();
            beforePrint();      //調用前置通知
            //執行方法
            obj = pp.proceed(args);
            afterReturningPrint();    //調用后置運行通知
        } catch (Throwable e) {
            afterWarningPrint();     //調用異常通知
            //打印異常
            e.printStackTrace();
        } finally {
            afterPrint();     //調用最終通知
        }
        return obj;
    }
<!--配置AOP切面-->
    <aop:config>
        <!--切入點表達式抽取  配置在外邊表示全局的,可以供多個配置切面使用-->
        <aop:pointcut id="pidOn" expression="execution(* cn.xw.service.impl.*.*(..))"/>
        <!--配置切面-->
        <aop:aspect id="aspects" ref="manger">
           <!--調用環繞通知--> <aop:around method="aroundPrint" pointcut-ref="pidOn"></aop:around>
        </aop:aspect>
    </aop:config>

6:使用XML+注解實現AOP方式完成對學生轉賬功能(基於Spring+Mybatis)

<!--設置maven的代碼編譯環境 我設置JDK1.9-->
    <properties>
        <maven.compiler.source>1.9</maven.compiler.source>
        <maven.compiler.target>1.9</maven.compiler.target>
        <maven.compiler.compilerVersion>1.9</maven.compiler.compilerVersion>
    </properties>

    <dependencies>
        <!--mysql驅動坐標-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.32</version>
        </dependency>
        <!--c3p0連接池坐標-->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.2</version>
        </dependency>
        <!--junit單元測試坐標-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--Spring-context主要坐標-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--導入Spring的jdbc的操作 注集成mybatis要導入此包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring對mybatis的支持-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.1</version>
        </dependency>
        <!--導入mybatis坐標 注解sql語句需要-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--Spring-test測試坐標-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring對Aop支持-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--支持解析切入點表達式等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--支持AOP相關注解等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--JDK9廢棄了一些注解,使用此坐標導入-->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
pom.xml坐標
public class Student {
    private int sid;            //主鍵id
    private String sname;       //姓名
    private String ssex;        //性別
    private int sage;           //年齡
    private double scredit;     //學分
    private double smoney;      //零花錢
    private String saddress;    //住址
    private String senrol;      //入學時間
    //因為簡單的單表CRUD就不涉及到外鍵
    //private int fid;            //外鍵 連接家庭表信息學生對家庭,一對一
    //private int tid;            //外鍵 連接老師信息 學生對老師,一對一
    //創建構造器/get/set/toString就不展示了
}
POJO實體類Student
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 學生業務接口
 * @version: v1.0.0
 */
public interface StudentService {
    //轉賬
    void transfer(String currentName, String targetName, double money);
}
/***************************************************/

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 學生業務接口實現
 * @version: v1.0.0
 */
@Service("studentService")
public class StudentServiceImpl implements StudentService {

    //創建StudentDao變量並注入
    @Autowired
    private  StudentDao studentDao;
    
    /**
     * 轉賬
     * @param currentName 當前學生姓名
     * @param targetName  目標學生姓名
     * @param money       轉賬金額
     */
    public void transfer(String currentName, String targetName, double money) {
        /*為了演示 我就不做那么細致的數據判斷等等操作*/
        //①:查詢學生
        Student currentStudent = studentDao.findByName(currentName);
        Student targetStudent = studentDao.findByName(targetName);
        //打印最初結果
        System.out.println("轉賬前: 當前學生姓名和余額:" + currentStudent.getSname() + "  " + currentStudent.getSmoney());
        System.out.println("轉賬前: 目標學生姓名和余額:" + targetStudent.getSname() + "  " + targetStudent.getSmoney());
        //②:開始轉賬
        currentStudent.setSmoney(currentStudent.getSmoney() - money);
        targetStudent.setSmoney(targetStudent.getSmoney() + money);

        //③:調用更新
        studentDao.update(currentStudent);
        //int a=1/0; //開啟則拋異常
        studentDao.update(targetStudent);

        //打印最終結果
        Student currentStudentEnd = studentDao.findByName(currentName);
        Student targetStudentEnd = studentDao.findByName(targetName);
        System.out.println("轉賬后: 當前學生姓名和余額:" + currentStudentEnd.getSname() + "  " + currentStudentEnd.getSmoney());
        System.out.println("轉賬后: 目標學生姓名和余額:" + targetStudentEnd.getSname() + "  " + targetStudentEnd.getSmoney());
    }
}
/***************************************************/

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 學生dao
 * @version: v1.0.0
 */
public interface StudentDao {
    //根據姓名查詢
    @Select("select * from student where sname=#{name}")
    Student findByName(@Param("name") String name);

    //修改金額
    @Update("update student set smoney=#{stu.smoney} where sid=#{stu.sid}")
    void update(@Param("stu") Student student);
}
service/dao接口及實現類
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 配置自己創建的事務管理器bean 
 * @version: v1.0.0
 */
@Component("transactionManger")
public class TransactionManger {

    //自動注入dataSource數據源
    @Autowired
    private DataSource dataSource;

    /**
     * @PostConstruct 初始化事務管理器方法
     */
    @PostConstruct
    public void init() {
        //把Connection連接與當前線程綁定 底層是ThreadLocal
        //綁定是為了通知類的回滾或提交 與mybatis的Connection是一致的
        //簡單說當我們有業務需要在事務提交過后進行某一項或者某一系列的業務操作時候我們就可以使用
        TransactionSynchronizationManager.initSynchronization();
    }

    //前置通知
    public void beforeMessage() {
        System.out.println("前置通知  執行");
        try {
            //DataSourceUtils方式是Spring提供的,傳入DataSource返回Connection,
            //從這里面獲取Connection連接可以保證當前線程的Connection的統一
            DataSourceUtils.getConnection(dataSource).setAutoCommit(false);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }

    }

    //后置運行通知
    public void afterReturningMessage() {
        System.out.println("后置通知  執行");
        try {
            DataSourceUtils.getConnection(dataSource).commit();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    //后置運行異常通知
    public void afterThrowingMessage() {
        System.out.println("后置異常通知  執行");
        try {
            DataSourceUtils.getConnection(dataSource).rollback();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }

    }

    //最終通知 開始關閉資源的和還回連接
    public void afterMessage() {
        System.out.println("最終通知  執行");
        try {
            DataSourceUtils.getConnection(dataSource).close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    //環繞通知
    public void aroundMessage(ProceedingJoinPoint pp) {
        System.out.println("==》環繞通知開始《==");
        Object obj = null;
        try {
            Object[] args = pp.getArgs();
            beforeMessage();
            obj = pp.proceed(args);
            afterReturningMessage();
        } catch (Throwable e) {
            afterThrowingMessage();
            e.printStackTrace();
        } finally {
            afterMessage();
        }
    }
}
通知類用於配置切面通知TransactionManger
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/demo_school
jdbc.username=root
jdbc.password=123
數據庫連接信息jdbc.properties
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--開啟注解掃描-->
    <context:component-scan base-package="cn.xw"/>

    <!--導入jdbc數據庫連接必要數據-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置c3p0連接池 DataSource數據源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--設置Mybatis工廠類bean對象-->
    <bean id="factoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--設置mybatis數據源-->
        <property name="dataSource" ref="dataSource"/>
        <!--配置別名-->
        <property name="typeAliasesPackage" value="cn.xw.pojo"/>
        <!--配置mybatis配置文件,如果有別的配置文件可以使用此標簽導入-->
        <!--<property name="configuration" value="classpath:SqlMapConfig.xml"/>-->
    </bean>
    <!--配置mapper掃描映射類-->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.xw.dao"/>
    </bean>

    <!--編寫AOP切面配置-->
    <aop:config>
        <!--配置全局切入點表達式-->
        <aop:pointcut id="pt" expression="execution(* cn.xw.service.*.*(..))"/>
        <!--配置具體切面 並設置切面通知類是哪個 切面通知類使用注解注冊容器-->
        <aop:aspect id="aspect" ref="transactionManger">
            <!--四種通知-->
            <aop:before method="beforeMessage" pointcut-ref="pt"/>
            <aop:after-returning method="afterReturningMessage" pointcut-ref="pt"/>
            <aop:after-throwing method="afterThrowingMessage" pointcut-ref="pt"/>
            <aop:after method="afterMessage" pointcut-ref="pt"/>
            <!--環繞通知-->
            <!--<aop:around method="aroundMessage" pointcut-ref="pt"/>-->
        </aop:aspect>
    </aop:config>

</beans>
Spring配置文件applicationContext.xml
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5 15:35
 * @description: 測試類
 * @version: v1.0.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = "classpath:applicationContext.xml")
public class Client {
    @Autowired
    private StudentService studentService;
    @Test
    public void testTransfer() {
        studentService.transfer("王生安", "李鑫灝", 10);
    }
}
測試

7:Spring AOP基於注解操作介紹

  注解方式和xml方式大同小異,都差不多,但是使用注解方式的AOP最好使用環繞通知,后面會說明。我們要使用注解AOP通知就需要在配置文件添加一個基於注解的特有標簽

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      .....省略部分和此介紹沒關系的配置
    <!--去除其它手動設置AOP 加上下面標簽即可  我們把之前編寫的aop:config標簽刪除,不需要它了-->
    <!--配置開啟允許使用注解方式完成AOP-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

  我們對配置文件改造后,這時候我們就要改造一下自己定義的切面通知類即可

@Component("transactionManger")
@Aspect     //聲明當前類是一個切面通知類
public class TransactionManger {

    //配置切面表達式 注意下面的是方法不是屬性
    @Pointcut(value = "execution(* cn.xw.service.impl.*.*(..))")
    private void pointcut() { }

    //前置通知
    /*@Before(value = "pointcut()") *///和XML配置的aop:before一樣 注意這里是要導入annotation.Before;別和Junit混淆
    public void beforeMessage() {
        System.out.println("前置通知  執行");
    }

    //后置運行通知
    /*@AfterReturning("pointcut()")*/ //和XML配置的aop:after-returning一樣
    public void afterReturningMessage() {
        System.out.println("后置通知  執行");
    }

    //后置運行異常通知
    /*@AfterThrowing("pointcut()")*/ //和XML配置的aop:after-throwing一樣
    public void afterThrowingMessage() {
        System.out.println("后置異常通知  執行");
    }

    //最終通知 開始關閉資源的和還回連接
    /* @After("pointcut()")*/ //和XML配置的aop:after一樣 注意這里是要導入annotation.After;別和Junit混淆
    public void afterMessage() {
        System.out.println("最終通知  執行");
    }

    //環繞通知  使用注解方式AOP通知 就使用環繞通知吧
    //因為在注解中使用上面四個通知會出現本身意想不到的結果,還沒執行異常/正常通知就到最終通知了
    @Around("pointcut()") //和XML配置的aop:around一樣
    public Object aroundMessage(ProceedingJoinPoint pp) {
        System.out.println("==》環繞通知開始《==");
        Object obj = null;
        try {
            Object[] args = pp.getArgs();
            beforeMessage();
            obj = pp.proceed(args);
            afterReturningMessage();
        } catch (Throwable e) {
            afterThrowingMessage();
            e.printStackTrace();
        } finally {
            afterMessage();
        }
        return obj;
    }
}
使用注解來完成對配置AOP切面通知類改造

補充:如果連xml文件都沒有的話,開啟注解AOP的話需要在配置類上配置@EnableAspectJAutoProxy

@Configuration
@ComponentScan(basePackages="cn.xw")
@EnableAspectJAutoProxy public class SpringConfiguration {
}

8:使用純注解完成AOP方式操作學生轉賬(Spring+Mybatis)

   使用純注解方式完成轉賬並控制事務的操作,其實大部分代碼和我上面的第6小結寫的xml+注解完成事務操作差不多,我針對修改后的代碼展示,其它沒修改過的代碼則看上面的第6小節

/**
 * DataSourceConfig配置類  里面配置了spring集成mybatis和數據源啥的
 */
public class DataSourceConfig {
    /**
     * 配置數據源Bean對象放入Spring容器
     *
     * @return
     */
    @Bean("dataSource")
    public DataSource getDataSource() {
        //創建C3P0連接池對象
        ComboPooledDataSource dataSource = null;
        try {
            dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass("com.mysql.jdbc.Driver");
            dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo_school");
            dataSource.setUser("root");
            dataSource.setPassword("123");
        } catch (PropertyVetoException e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    /**
     * 保證Connection在當前線程統一
     * @param dataSource
     * @return
     */
    @Bean("connection")
    public Connection getConnection(@Qualifier("dataSource") DataSource dataSource) {
        //把Connection連接與當前線程綁定 底層是ThreadLocal
        //綁定是為了通知類的回滾或提交 與mybatis的Connection是一致的
        //簡單說當我們有業務需要在事務提交過后進行某一項或者某一系列的業務操作時候我們就可以使用
        TransactionSynchronizationManager.initSynchronization();
        return DataSourceUtils.getConnection(dataSource);
    }

    /**
     * 設置Mybatis工廠類bean對象
     * @param dataSource
     * @return
     */
    @Bean("factoryBean")
    public SqlSessionFactoryBean getSqlSessionFactoryBean(DataSource dataSource) {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);  //設置數據源
        factoryBean.setTypeAliasesPackage("cn.xw.pojo");    //設置別名
        return factoryBean;
    }

    /**
     * 配置mapper掃描映射類
     * @return
     */
    @Bean("mapperScannerConfigurer")
    public MapperScannerConfigurer getMapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("cn.xw.dao");
        return mapperScannerConfigurer;
    }
}
/************************************************** */

//主配置類  引用上面的DataSourceConfig類
@Configuration
@ComponentScan(value = "cn.xw")
@Import(value = {DataSourceConfig.class})
@EnableAspectJAutoProxy
public class SpringConfig {

}
配置類編寫SpringConfig和DataSourceConfig
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 配置自己創建的事務管理器bean
 * @version: v1.0.0
 */
@Component("transactionManger")
@Aspect
public class TransactionManger {

    //自動注入Connection數據源
    @Autowired
    private Connection connection;
    
    //配置切面表達式 注意下面的是方法不是屬性
    @Pointcut(value = "execution(* cn.xw.service.impl.*.*(..))")
    private void pointcut() { }

    //前置通知
    public void beforeMessage() {
        System.out.println("前置通知  執行");
        try {
            connection.setAutoCommit(false);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    //后置運行通知
    public void afterReturningMessage() {
        System.out.println("后置通知  執行");
        try {
            connection.commit();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    //后置運行異常通知
    public void afterThrowingMessage() {
        System.out.println("后置異常通知  執行");
        try {
            connection.rollback();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    //最終通知 開始關閉資源的和還回連接
    public void afterMessage() {
        System.out.println("最終通知  執行");
        try {
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    //環繞通知  使用注解方式AOP通知 就使用環繞通知吧
    @Around("pointcut()")
    public Object aroundMessage(ProceedingJoinPoint pp) {
        System.out.println("==》環繞通知開始《==");
        Object obj = null;
        try {
            Object[] args = pp.getArgs();
            beforeMessage();
            obj = pp.proceed(args);
            afterReturningMessage();
        } catch (Throwable e) {
            afterThrowingMessage();
            e.printStackTrace();
        } finally {
            afterMessage();
        }
        return obj;
    }
}
通知配置類改造TransactionManger
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class Client {
    @Autowired
    private StudentService studentService;
    @Test
    public void testTransfer() {
        studentService.transfer("王生安", "李鑫灝", 10);
    }
}
測試類

 .


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM