一、概述
- 問題
- 完善user案例
- 分析案例中的問題
- 動態代理技術
- 動態代理的另一種實現方式
- 解決案例中的問題
- AOP概念
- spring中AOP的相關術語
- spring中基於XML和註解的AOP配置
一、問題
1.1 業務場景
在轉賬的業務中,需要保證轉出方扣錢成功,同時要保證轉入方收到相同金額的錢,這就要求整個轉賬的過程在同一個事務中
- 業務層代碼
public class UserServiceImpl implements UserService {
private UserDao userDao;
private TransactionManager txManager;
// 使用set注入dao
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
@Override
public int addOne(User user) {
try {
// 1. 開啓事務
txManager.start();
// 2. 執行操作
int count = userDao.addOne(user);
// 3. 提交事務
txManager.commit();
// 4. 返回數據
return count;
} catch (Exception e) {
// 5. 異常回滾
txManager.rollBack();
} finally {
// 6. 結束事務
txManager.release();
}
return 0;
}
@Override
public int deleteOne(int id) {
try {
// 1. 開啓事務
txManager.start();
// 2. 執行操作
int count = userDao.deleteOne(id);
// 3. 提交事務
txManager.commit();
// 4. 返回數據
return count;
} catch (Exception e) {
// 5. 異常回滾
txManager.rollBack();
} finally {
// 6. 結束事務
txManager.release();
}
return 0;
}
@Override
public int updateOne(User user) {
try {
// 1. 開啓事務
txManager.start();
// 2. 執行操作
int count = userDao.updateOne(user);
// 3. 提交事務
txManager.commit();
// 4. 返回數據
return count;
} catch (Exception e) {
// 5. 異常回滾
txManager.rollBack();
} finally {
// 6. 結束事務
txManager.release();
}
return 0;
}
@Override
public User findOne(int id) {
try {
// 1. 開啓事務
txManager.start();
// 2. 執行操作
User user = userDao.findOne(id);
// 3. 提交事務
txManager.commit();
// 4. 返回結果
return user;
} catch (Exception e) {
// 5. 異常回滾
txManager.rollBack();
} finally {
// 6. 結束事務
txManager.release();
}
return null;
}
@Override
public List<User> findAll() {
try {
// 1. 開啓事務
txManager.start();
// 2. 執行操作
List<User> list = userDao.findAll();
// 3. 提交事務
txManager.commit();
// 4. 返回結果
return list;
} catch (Exception e) {
// 5. 異常回滾
txManager.rollBack();
} finally {
// 6. 結束事務
txManager.release();
}
return null;
}
/**
* 轉賬操作
* @author: [email protected]
* @date: 2020/3/20 11:36 下午
* @param originAccount
* @param targetAccount
* @param money
* @return int
*/
@Override
public int transferMoney(Integer originAccount, Integer targetAccount, Double money) {
try {
// 1. 開啓事務
txManager.start();
// 2. 執行操作
// 2.1 查出轉出賬戶
User originUser = userDao.findOne(originAccount);
// 2.2 查出站如賬戶
User targetUser = userDao.findOne(targetAccount);
// 2.3 轉出賬戶減少金額
double originMoney = originUser.getMoney() - money;
originUser.setMoney(originMoney);
// 2.4 轉入賬戶增加金額
double targetMoney = targetUser.getMoney() + money;
targetUser.setMoney(targetMoney);
// 2.5 更新轉出賬戶
userDao.updateOne(originUser);
int num = 1/0;
// 2.6 更新轉入賬戶
userDao.updateOne(targetUser);
// 3. 提交事務
txManager.commit();
// 4. 返回結果
return 1;
} catch (Exception e) {
// 5. 異常回滾
txManager.rollBack();
e.printStackTrace();
} finally {
// 6. 結束事務
txManager.release();
}
return 0;
}
}
- 問:事務控制爲什麼需要加到業務層?
- 答:通常一個業務需要連續多次操作數據庫,如果中間某一次操作數據庫的時候發生了異常,整個業務沒有回滾就提交了部分操作,就會發生異常;比如最常見的轉賬業務,如果轉出方轉出成功,而轉入的時候發生了異常,就會造成轉入方沒有收到轉賬,從而造成金額損失
- beans.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置service -->
<bean id="userService" class="com.lizza.service.impl.UserServiceImpl">
<!-- 注入Dao -->
<property name="userDao" ref="userDao"></property>
<property name="txManager" ref="txManager"></property>
</bean>
<!-- 配置事務管理器 -->
<bean name="txManager" class="com.lizza.util.TransactionManager">
<property name="jdbcUtil" ref="jdbcUtil"></property>
</bean>
<!-- 配置dao -->
<bean id="userDao" class="com.lizza.dao.impl.UserDaoImpl">
<!-- 注入QueryRunner -->
<property name="queryRunner" ref="queryRunner"></property>
<property name="jdbcUtil" ref="jdbcUtil"></property>
</bean>
<!-- 配置JdbcUtil -->
<bean id="jdbcUtil" class="com.lizza.util.JdbcUtil">
<!-- 注入數據源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置QueryRunner;QueryRunner需要配置成多例模式,避免發生線程安全問題 -->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
<!-- 配置數據源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3360/spring"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean>
</beans>
1.2 存在問題
- 代碼大量重複:每一個涉及到事務的控制都需要增加事務控制的代碼
- 代碼重複導致耦合度增大,開發維護成本升高:新的人接手代碼時,需要更多的時間去了解與業務無關的事務控制的代碼
1.3 解決方案
靜態代理,動態代理(本案例選擇動態代理)
二、動態代理
2.1 概述
動態的在運行期創建某個類或者接口的代理類
2.2 分類
- JDK動態代理
- cglib動態代理
類型 | 特點 | 使用方式 |
---|---|---|
JDK動態代理 | 基於接口的動態代理 | 使用Proxy的newProxyInstance()方法創建代理對象 |
cglib動態代理 | 基於類的動態代理 | 使用Enhancer類中的create()方法創建代理對象 |
2.3 JDK動態代理示例
public class Client {
/**
* JDK動態代理
* 定義:可以在運行期動態創建某個interface的實例
* 作用:在不修改源碼的基礎上實現對方法的增強
* 特點:需要接口,並且實現類的實例
* 分類:
* 1. 基於接口的動態代理
* 2. 基於子類的動態代理
* 基於接口的動態代理
* 涉及的類:Proxy
* 提供者:JDK官方
* 創建動態代理的要求
* 被代理的類最少實現一個接口,如果沒有則不能使用
* newProxyInstance方法的參數
* ClassLoader:類加載器
* 用於加載代理對象的字節碼;與被代理對象使用相同的類加載器;固定寫法
* Class<?>[]:字節碼數組
*
* InvocationHandler:用於提供增強的代碼
* 用於實現增強的代碼;通常情況下是匿名內部類;非必需
*/
public static void main(String[] args){
Producer producer = new Producer();
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:執行被代理對象的任何接口方法時都會執行該方法
* @author: [email protected]
* @date: 2020/3/23 9:19 下午
* @param proxy 代理對象的引用
* @param method 當前執行的方法
* @param args 當前執行方法所需的參數
* @return 和被代理對象具有相同的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 獲取傳入的參數
if("sale".equals(method.getName())) {
return method.invoke(producer, (float) args[0] * 0.8f);
}
return method.invoke(producer, args);
}
}
);
proxyProducer.sale(10000);
}
}
2.4 cglib動態代理示例
public class Client {
/**
* cglib動態代理
* 定義:可以在運行期動態創建某個class的實例
* 作用:在不修改源碼的基礎上實現對方法的增強
* 特點:對指定類進行代理
* 分類:
* 1. 基於接口的動態代理
* 2. 基於子類的動態代理
* 基於接口的動態代理
* 涉及的類:Proxy
* 提供者:第三方cglib庫
* 創建動態代理的要求
* 被代理的類不能用final修飾
* 如何創建代理對象
* 使用Enhancer類中的create方法
* create方法的參數
* ClassLoader:類加載器
* 用於加載代理對象的字節碼;與被代理對象使用相同的類加載器;固定寫法
* Class<?>[]:字節碼數組
*
* Callback:用於提供增強的代碼
* 用於實現增強的代碼;通常情況下是匿名內部類;非必需
* 一般創建MethodInterceptor
*/
public static void main(String[] args){
Producer producer = new Producer();
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), producer.getClass().getInterfaces(), new MethodInterceptor() {
/**
* 執行被代理對象的任何方法都會經過該方法
* @author: [email protected]
* @date: 2020/3/25 8:21 下午
* @param proxy
* @param method
* @param args
* @param methodProxy
* @return java.lang.Object
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 1. 獲取傳入的參數
if("sale".equals(method.getName())) {
return method.invoke(producer, (float) args[0] * 0.8f);
}
return method.invoke(producer, args);
}
});
cglibProducer.sale(12000);
}
}
三、用動態代理解決問題
3.1 代碼示例
- 創建動態代理對象並增加事務控制
public class BeanFactory {
private UserService userService;
private TransactionManager txManager;
public UserService getUserService() {
return (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
txManager.start();
result = method.invoke(userService, args);
txManager.commit();
} catch (Exception e) {
txManager.rollBack();
e.printStackTrace();
} finally {
txManager.release();
}
return result;
}
});
}
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
}
- 配置beans.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置帶事務控制的service -->
<bean id="beanFactory" class="com.lizza.factory.BeanFactory">
<property name="txManager" ref="txManager"></property>
<property name="userService" ref="userService"></property>
</bean>
<bean id="proxyUserService" factory-bean="beanFactory" factory-method="getUserService"></bean>
<!-- 配置service -->
<bean id="userService" class="com.lizza.service.impl.UserServiceImpl">
<!-- 注入Dao -->
<property name="userDao" ref="userDao"></property>
</bean>
<!-- 配置事務管理器 -->
<bean name="txManager" class="com.lizza.util.TransactionManager">
<property name="jdbcUtil" ref="jdbcUtil"></property>
</bean>
<!-- 配置dao -->
<bean id="userDao" class="com.lizza.dao.impl.UserDaoImpl">
<!-- 注入QueryRunner -->
<property name="queryRunner" ref="queryRunner"></property>
<property name="jdbcUtil" ref="jdbcUtil"></property>
</bean>
<!-- 配置JdbcUtil -->
<bean id="jdbcUtil" class="com.lizza.util.JdbcUtil">
<!-- 注入數據源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置QueryRunner;QueryRunner需要配置成多例模式,避免發生線程安全問題 -->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
<!-- 配置數據源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean>
</beans>
- 進行測試
public class UserServiceTest {
@Test
public void addOne() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean("userService", UserService.class);
User user = new User();
user.setId(4);
user.setName("test");
user.setAge(19);
Assert.assertEquals(1, userService.addOne(user));
}
@Test
public void deleteOne() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean("userService", UserService.class);
Assert.assertEquals(1, userService.deleteOne(4));
}
@Test
public void updateOne() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean("userService", UserService.class);
User user = new User();
user.setId(4);
user.setName("test");
user.setAge(0);
Assert.assertEquals(0, userService.updateOne(user));
}
@Test
public void findOne() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean("userService", UserService.class);
User user = userService.findOne(1);
Assert.assertEquals(1, user.getId());
}
@Test
public void findAll() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean("userService", UserService.class);
List<User> list = userService.findAll();
System.out.println(list);
Assert.assertNotNull(list);
}
@Test
public void transferMoney() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 獲取代理對象
UserService userService = context.getBean("proxyUserService", UserService.class);
userService.transferMoney(1, 2, 100.0);
List<User> list = userService.findAll();
System.out.println(list);
}
}
3.2 總結
- 可以看到動態代理在不改變源碼的基礎上實現了事務的控制
- 事務的控制類獨立於業務代碼,降低了耦合性
四、Spring AOP
4.1 AOP概述
- 定義
AOP(Aspect Oriented Programming)面向切面編程,通過預編譯和運行期動態代理的方式,實現了程序各層級業務邏輯的隔離,降低了程序的耦合性,提高了程序開發的效率
- 作用
在程序運行期間,通過動態代理的方式不改變源碼實現對方法的增強
- 優勢
- 降低了代碼的耦合性
- 提高了開發效率
- 方便維護
五、Spring AOP
5.1 相關術語
- 連接點 Joinpoint
被攔截到的點;spring中指方法,因爲spring只支持方法的攔截
- 切入點 Pointcut
進行了增強的連接點
- 通知 Advice
在切入點進行的所有增強的操作
通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知
- 切面 Aspect
切入點和通知的結合
- 目標對象 Target
被代理的對象
- 代理對象 Proxy
增強後的對象
- 織入 Weaving
把切面應用到目標對象產生增強後的代理對象的過程
注意:spring採用動態代理織入,而AspectJ採用編譯器織入和類裝載期織入
- 引入
5.2 xml配置實現spring aop
- xml配置步驟
- 使用aop:config標籤開始spring aop的配置
- 使用aop:aspect標籤開始配置切面
- id: 切面ID
- ref: 切面處理Bean
- 使用aop:aspect等子標籤配置通知類型
- method: 通知處理的具體方法
- pointcut: 切入點表達式,用於描述哪些連接點可以作爲切入點
- 切入點表達式
- 關鍵字:execution
- 表達式:訪問修飾符 返回值 類限定名.方法名(參數列表)
- 標準寫法:public void com.lizza.service.UserService.update(int)
- 訪問修飾符可以省略:void com.lizza.service.UserService.update(int)
- 返回值可以使用通配符:* com.lizza.service.UserService.update(int)
- 包名可以使用通配符,有幾級包寫幾個*:* ..*.UserService.update(int)
- 包名使用…表示當前包及其子包:* com.lizza…update(int)
- 類名和方法名都可以使用通配符:* com.lizza…*()
- 參數列表:* com.lizza…*(…)
- 基本類型:直接寫名稱,如int,char,double
- 應用類型:全限定名.類名,如:java.lang.String
- *:表示必須有參數
- …:表示有無參數均可
- 全通配形式:* ….*(…)
- beans.xml
<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"
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">
<bean id="userService" class="com.lizza.service.UserService"></bean>
<bean id="log" class="com.lizza.util.Log"></bean>
<!--
Spring中配置AOP的步驟
1. 使用<aop:config>標籤開始spring aop的配置
2. 使用<aop:aspect>標籤開始配置切面
id: 切面ID
ref: 切面處理Bean
3. 使用<aop:aspect>等子標籤配置通知類型
method: 通知處理的具體方法
pointcut: 切入點表達式,用於描述哪些連接點可以作爲切入點
4. 切入點表達式
關鍵字:execution
表達式:訪問修飾符 返回值 類限定名.方法名(參數列表)
標準寫法:public void com.lizza.service.UserService.update(int)
訪問修飾符可以省略:void com.lizza.service.UserService.update(int)
返回值可以使用通配符:* com.lizza.service.UserService.update(int)
包名可以使用通配符,有幾級包寫幾個*:* *.*.*.UserService.update(int)
包名使用..表示當前包及其子包:* com.lizza..update(int)
類名和方法名都可以使用通配符:* com.lizza..*()
參數列表:* com.lizza..*(..)
基本類型:直接寫名稱,如int,char,double
應用類型:全限定名.類名,如:java.lang.String
*:表示必須有參數
..:表示有無參數均可
全通配形式:* *..*.*(..)
-->
<aop:config>
<aop:aspect id="logAdvice" ref="log">
<aop:before method="log" pointcut="execution(* com.lizza..*())"></aop:before>
</aop:aspect>
</aop:config>
</beans>