1. 程式人生 > >從零開始認識並操縱Spring Aop事務

從零開始認識並操縱Spring Aop事務

目錄

宣告:從零開始,並不代表你對java Spring一點都不懂的程度哈,本例項只是過一個概貌,詳細內容會分多篇文章拆解

業務介紹

要想要設計一個和事務操作相關的業務,我們以最經典的轉賬的例子來描述

轉賬,即兩個使用者之間錢的關係變動,但要求總和不變

版本宣告

本案例使用的Web 版本2.5,Spring 版本為4.3

開發工具為eclipse

操作步驟

導包

Spring 各版本官方包下載

下載的包包含Spring包以及第三方工具整合Spring的所有包,但是我們不需要全部匯入,只需根據專案需求匯入即可

Spring 核心包 + apache logging包

  • Spring-core
  • Spring-bean
  • Spring-express
  • Spring-context

  • apache.commons.logging
  • apache.log4j

Spring 測試包

  • Spring-test

Spring Aop 事務包

  • Spring-aop
  • Spring-aspect
  • com.springsource.org.aopalliance
  • com.springsource.org.aspectj.weaver
  • Spring-jdbc
  • Spring-tx

其他包

  • c3p0連線池
  • JDBC取得包

以上一共15個包

注意的是:每一類Spring 包分為 RELEASE 和 javadoc和source包,匯入只需用RELEASE包

準備資料庫

這裡準備的資料庫就簡單一個表,主要包含使用者名稱和賬戶即可

image.png

而我實際運用的表會多幾個欄位,但這並不影響

編寫javaBean

編寫User 的Bean類,注意的是屬性名和表字段名一樣即可,順便加個toString和構造方法,一定要有空參構造哦。

書寫Dao實現介面

為了完整起見,我們把Dao類的增刪改查也順便實現以下

public interface AccountDao {
    void save(User u);
    void delete(Integer id);
    void update(User u);
    User getById(Integer id);
    int getTotalCount();
    List<User> getAll();
    void increaseAccount(Integer id, Double money);
    void decreaseAccount(Integer id, Double money);
}

實現Dao類,這裡我們不整合其他ORM框架,採用的是原生的JDBC + Spring技術

在寫實現類的時候,需要繼承一個JdbcDaoSupport來為我們生成JdbcTemplate模板

這裡就順帶體驗一下使用JdbcDaoSupport增刪改查的操作,如果已經知道的可以略過

public class UserDaoImpl extends JdbcDaoSupport implements UserDao {

    @Override
    public void save(User u) {
        String sql = "insert into sys_user values(null, ?,?,?,?,?)";
        super.getJdbcTemplate().update(sql, u.getUser_code(), u.getUser_name(),
                            u.getUser_password(), u.getUser_state(), u.getAccount());
    }

    @Override
    public void delete(Integer id) {
        String sql = "delete from sys_user where user_id=?";
        super.getJdbcTemplate().update(sql, id);
    }

    @Override
    public void update(User u) {
        String sql = "update sys_user set user_name=? where user_id=?";
        super.getJdbcTemplate().update(sql, u.getUser_name(), u.getUser_id());
    }

    @Override
    public User getById(Integer id) {
        String sql = "select * from sys_user where user_id = ?";
        return super.getJdbcTemplate().queryForObject(sql, new RowMapper<User>() {

            @Override
            public User mapRow(ResultSet rs, int index) throws SQLException {
                User u = new User(rs.getInt("user_id"), rs.getString("user_code"),
                        rs.getString("user_name"), rs.getString("user_password"),
                        rs.getString("user_state").toCharArray()[0], rs.getDouble("account"));
                return u;
            }
            
        }, id);
    }

    @Override
    public int getTotalCount() {
        String sql = "select count(*) from sys_user";
        Integer count = super.getJdbcTemplate().queryForObject(sql, Integer.class);
        return count;
    }

    @Override
    public List<User> getAll() {
        String sql = "select * from sys_user";
        return super.getJdbcTemplate().query(sql, new RowMapper<User>() {

            @Override
            public User mapRow(ResultSet rs, int index) throws SQLException {
                User u = new User(rs.getInt("user_id"), rs.getString("user_code"),
                        rs.getString("user_name"), rs.getString("user_password"),
                        rs.getString("user_state").toCharArray()[0], rs.getDouble("account"));
                return u;
            }
            
        });
    }

    @Override
    public void increaseAccount(Integer id, Double money) {
        String sql = "update sys_user set account=account+? where user_id=?";
        super.getJdbcTemplate().update(sql, money, id);
    }

    @Override
    public void decreaseAccount(Integer id, Double money) {
        String sql = "update sys_user set account=account-? where user_id=?";
        super.getJdbcTemplate().update(sql, money, id);
    }
}

Spring 配置

在Spring 中,只要把業務寫好了,剩下的就全靠配置了

資料庫連線配置

這裡我們採用通用的一種,使用properties檔案配置資料庫引數

在 src 目錄下的db.properties中

jdbc.jdbcUrl=jdbc:mysql:///test
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=******

建立Spring配置檔案

原則上Spring配置檔案可以放置在任何位置,起任意名字,但是一般都會被統一化

統一是src 目錄下的 applicationContext.xml檔案

匯入約束

在這裡匯入約束的操作可以直接使用eclipse完成,Spring 約束檔案都在Spring包中的schema目錄下,

image.png

裡邊有很多,這裡需要匯入的只需要

  • beans (物件注入)
  • context (讀取properties配置)
  • aop (aop配置)
  • tx (Spring事務)

裡邊找到最高版本的xsd約束檔案即可

使用eclipse 匯入約束的方法:

windows -> preferences 裡搜尋xml Catalog

image.png

裡邊找到剛才介紹的其中一個約束檔案,比如這裡使用context

注意將key type 修改為 Schema location,並在後面追加檔名

image.png

接下來是需要在配置檔案標籤中引入這種新版的xsd配置,這裡使用eclipse操作

開啟applicationContext.xml檔案,輸入beans標籤,進入設計模式

image.png

對beans新增名稱空間

image.png

首先先載入xsi

image.png

然後再載入剛剛在xml Catalog匯入的xsd檔案
如果剛才加入的是beans就先找spring-beans

image.png

這裡讓beans 使用無字首,但是所有的約束當中只能存在一個xsi約束無字首

image.png

比如下一個context就需要帶字首了

image.png

對於每一個約束,都要進行以上操作哦,也就是說,重複4次

其實最終的效果就是生成這樣的配置

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd ">

</beans>

配置資料庫連線池

使用連線池進行資料庫連線,連線引數讀取properties檔案

配置如下:

<!-- 讀取db.properties配置 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 將連線池放入spring容器 -->
<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
    <property name="driverClass" value="${jdbc.driverClass}"></property>
    <property name="user" value="${jdbc.user}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

注入連線池到UserDao

接下來我們要分析依賴關係,在UserDao中繼承了JDBCDaoSupport類

這個類中可以獲得JDBC模板,這個模板的建立需要依賴連線池

因此首先要先建立連線池物件(上面已經完成),再將連線池注入到UserDao類中去

<!-- 將UserDao放入到Spring容器中 -->
<bean name="UserDao" class="edu.scnu.dao.UserDaoImpl">
    <property name="dataSource" ref="dataSource"></property>
</bean>

使用Junit和Spring整合測試

到了這個階段,有必要進行一波連線測試了,

這裡測試我們顯然需要使用JUnit,但是我們發現寫每個測試方法都要加入這麼一句Spring的Context物件建立的宣告

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        
UserDao userDao = (UserDao) ac.getBean("userDao");

Spring 給了我們貼心的簡化測試類書寫的操作,就是使用註解

  • RunWith
  • ContextConfiguration
  • 這些都在Spring-test包下
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestJdbc {
    @Resource(name="userDao")
    private UserDao ud;
    
    @Test
    public void testSave() {
        User u = new User();
        u.setUser_name("測試使用者");
        u.setUser_state('0');
        u.setAccount(1000d);
        ud.save(u);
    }
    
    @Test
    public void testUpdate() {
        User u = new User();
        u.setUser_name("測試使用者2");
        ud.update(u);
    }
    
    @Test
    public void testDelete() {
        ud.delete(7);
    }
    
    @Test
    public void testFind() {
        System.out.println(ud.getById(7));
        System.out.println(ud.getAll());
    }
}

一波測試後確實發現很多問題... 這裡都修改完成了。

AOP 事務操作的實現方式

一些相關概念回顧

資料庫事務

Spring Aop

編碼式

書寫轉賬的業務介面和實現類

public class UserServiceImpl implements UserService {
    
    private UserDao ud;

    @Override
    public void transfer(Integer from, Integer to, Double money) {
        ud.decreaseAccount(from, money);
        
        ud.increaseAccount(to, money);
    }

    public void setUd(UserDao ud) {
        this.ud = ud;
    }

}

將核心事務管理器配置到spring容器

<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>

配置TransactionTemplate模板

<bean name="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager"></property>
</bean>

將事務模板注入Service

<bean name="userService" class="edu.scnu.service.UserServiceImpl">
    <property name="ud" ref="userDao"></property>
</bean>

在Service中使用事務模板

public class UserServiceImpl implements UserService {
    
    private UserDao ud;
    
    private TransactionTemplate txtemplate;

    @Override
    // 注意在老版本的java中,內部函式訪問不了外部引數的時候,需要把外部引數型別加上final修飾
    public void transfer(Integer from, Integer to, Double money) {
        
        txtemplate.execute(new TransactionCallbackWithoutResult() {
            
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus arg0) {
                
                ud.decreaseAccount(from, money);
                
                ud.increaseAccount(to, money);
            }
        });

    }

    public void setUd(UserDao ud) {
        this.ud = ud;
    }

}

配置式

使用程式設計式的方法,其實本質上還是要寫事務的程式碼到每個業務身上,只不過它內部封裝的一下,讓我們不用寫這麼多,但說白了還是要寫

那麼通過配置的方式,我們可以徹底在業務層上移除事務處理的程式碼

那這個怎麼來配置呢?在Spring Aop中,對於這類問題的抽象,分為了三個核心概念:通知和切點以及他兩構成的切面

我們需要配的就是通知和織入配置這兩個部分了:

配置事務通知

首先配置通知,這個通知其實是用於完成事務操作,如果要自己寫的話,我們肯定會選用環繞通知,異常包裹,但是貼心的Spring給我們簡化了這步操作,我們只需要配置事務管理器注入即可。

配置事務的一些屬性,分別註明了被通知的方法名,隔離級別,傳播級別以及只讀限制,這些內容會在後續補充哦!

    <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="dataSource" ></property>
    </bean>
    
    <!-- 配置事務通知 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="save*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="update*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
            <tx:method name="find*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
            <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        </tx:attributes>
    </tx:advice>

配置織入————切面

首先需要的是切點,切點要求用spring提供的表示式來匹配實際需要被通知的類方法名

其次是切面,切面可以說就是切點和通知的交際了!

<!-- 配置織入 -->
<aop:config>
    <!-- 配置切點 -->
    <aop:pointcut expression="execution(* edu.scnu.service.UserServiceImpl.*(..))" id="txPc"/>
    <!-- 配置切面: 通知+切點 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPc"/>
</aop:config>

<!-- 將UserDao放入到Spring容器中 -->
<bean name="userService" class="edu.scnu.service.UserServiceImpl">
    <property name="ud" ref="userDao"></property>
</bean>

到這裡,配置就算完成了!

簡化的Service

通過通知和切點的配置後,Service方法不再需要顯示顯明事務操作了,直接專注於業務操作即可,也就是回到遠古時代,那個時候你不知道事務,不知道什麼異常,你還是可以完成安全無誤的程式碼

@Override
public void transfer(Integer from, Integer to, Double money) {
    ud.decreaseAccount(from, money);

    ud.increaseAccount(to, money);
}

測試方法

對於測試方法,需要建立UserService物件呼叫transfer方法即可,這裡UserService物件我們也是採用@Resource註解引入,這項配置在剛才 將UserDao放入到Spring容器中 已經完成過

@Resource(name="userService")
private UserService us;

@Test
public void testTransfer() {
    us.transfer(3, 1, 100d);
}

對於異常測試,我們只需要嘗試在業務方法,轉賬過程中間插入異常測試即可

@Override
public void transfer(Integer from, Integer to, Double money) {
    ud.decreaseAccount(from, money);
    int i = 1 / 0; // 引入異常測試
    ud.increaseAccount(to, money);
}

註解式

註解配置可以說是上述xml配置的一種簡化版,因為上面的方法好是好,只不過配置檔案的內容畢竟太多,而且很多時候還需要對照著來看

因此JDK1.6註解的引入,大大簡化了大多數框架的xml配置

開啟使用註解管理aop事務

<!-- 開啟使用註解管理aop事務 -->
    <tx:annotation-driven/>

在需要使用事務的Service方法中新增如下註解

@Override
@Transactional(isolation=Isolation.REPEATABLE_READ, propagation=Propagation.REQUIRED,readOnly=false)
public void transfer(Integer from, Integer to, Double money) {
    ud.decreaseAccount(from, money);

    ud.increaseAccount(to, money);
}

注意,在eclipse中,修改了配置檔案,最好要clean一下project否則容易報配置檔案失效是產生的錯誤

java.lang.Error: Unresolved compilation problems:

也可以將該註解配置到類宣告之前,讓所有方法都有該事務通知

@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=false)
public class UserServiceImpl implements UserService {
    
    private UserDao ud;

    @Override
    public void transfer(Integer from, Integer to, Double money) {
        ud.decreaseAccount(from, money);

        ud.increaseAccount(to, money);
    }

    @Override
    public void save(User u) {
        ud.save(u);
    }

    @Override
    public User find(Integer id) {
        return ud.getById(id);
    }
    
// ...

}

當然,如果有某些方法需要特殊配置,那麼只需要專門針對這個方法添加註解即可

@Override
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=true)
public User find(Integer id) {
    return ud.getById(id);
}

@Override
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=true)
public List<User> getAll() {
    return ud.getAll();
}

好了,整個流程到這裡就算完成了,整個流程的詳細專案程式碼可以踩我的github結合觀光:https://github.com/Autom-liu/SpringAopLearn,整個流程涉及到諸多知識細節需要慢慢琢磨,這裡只給大家展示整個概貌,具體細節會在後續的文章中慢慢解釋,期待大家的支援和star哦~~~