在上一篇文章中传送门,我们做了一个账户的 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);
}
}
由于执行有异常,转账失败。但是因为每次执行持久层方法都是独立事务,导致无法实现事务控制 ( 不符合事务的一致性)
为了解决这个问题,我们可以让业务层来控制事务的提交和回滚。
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);
}
}
}
<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">
<bean id="accountService" class="com.cz.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao">property>
<property name="tsManager" ref="tsManager"/>
bean>
<bean id="accountDao" class="com.cz.dao.impl.AccountDaoImpl">
<property name="runner" ref="runner">property>
<property name="connectionUtils" ref="connectionUtils"/>
bean>
<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&characterEncoding=utf-8"/>
<property name="user" value="root"/>
<property name="password" value="7107883"/>
bean>
<bean id="connectionUtils" class="com.cz.utils.ConnectionUtils">
<property name="dataSource" ref="dataSource"/>
bean>
<bean id="tsManager" class="com.cz.utils.TransactionManager">
<property name="connectionUtils" ref="connectionUtils"/>
bean>
beans>
通过对业务层加上事务控制,此时的转账方法已经可以正常运行,但是由于我们的业务层每个方法都加上了事务控制,所以显得业务层的方法特别臃肿,业务核心代码可能就 accountDao.removeAccount(accountId);
这么一句,其余的都是和事务有关,这样也就造成了业务层方法的耦合度很高。
如果此时我们的 TransactionManager
类中的方法名进行了更改,那么我们需要在每处调用的地方都进行修改,这样不利于我们后期的开发。
那么如何解决呢?这里我们可以使用动态代理,把事务的控制交给代理对象,所以下面我们先来看看关于动态代理的基本使用。
之前在讲mybatis的入门案例的时候也给大家贴过两篇其他大佬写的动态代理,下面我们简单的了解一下,想深入的朋友可以去看看那两篇文章。
什么是动态代理:
简单来说就是使用反射动态创建代理对象,使用代理对象来代替目标对象,因此达到增强目标对象的功能
动态代理的特点
字节码随用随创建,随用随加载。它与静态代理的区别也在于此,因为静态代理是字节码一上来就创建好,并完成加载。装饰者模式就是静态代理的一种体现。
动态代理的常用两种方式
基于接口的动态代理
基于子类的动态代理
提供者:JDK 官方的 Proxy 类。
要求:被代理类最少实现一个接口。
在这里我们使用厂家的例子:以前厂家不仅要对产品进行售后处理,还得负责销售的环节,把产品卖给客户。但是随着时间的推移,出现了一个新的角色:代理商,这时候厂家只需要负责对产品的售后处理,销售的事情交给代理商即可。如果客户需要售后维修,那么只需要将产品交给代理商,由代理商送往厂家进行维修。如下图:
定义接口,表示厂家需要具备的功能(销售以及售后)
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);
}
}
使用
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>cglibgroupId>
<artifactId>cglibartifactId>
<version>2.1_3version>
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
:当前执行方法的代理对象- 返回值类型与被代理对象方法一致
动态代理的基本使用就讲到这了,接下来我们回归正题。如何解决问题:
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();
}
}
});
}
}
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService">bean>
<bean id="beanFactory" class="com.cz.factory.BeanFactory">
<property name="accountService" ref="accountService"/>
<property name="tsManager" ref="tsManager"/>
bean>
<bean id="accountService" class="com.cz.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao">property>
bean>
到这里的时候,改造已经完成,通过代理对象一样可以控制住事务,同时业务层不再需要编写一堆的重复事务控制代码。但是如果每次管理事务的时候,我们都要像这样自己创建代理对象,那不是也挺麻烦的吗?有没有更好的方法呢?这时候就需要 AOP 的出场了。
使用动态代理的技术
,在不修改源码的基础上,对我们的已有方法进行增强。在程序运行期间,不修改源码对已有方法进行增强
。Joinpoint (连接点)
被拦截到的方法
,在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点Pointcut (切入点)
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义,简单来说就是被增强的方法
比如说,我们在业务层新增了一个方法test()
,但是在代理工厂中,我们不对该方法进行增强,而是直接放行,那么此时的test()
就不是切入点,仅仅是一个连接点,而其他的方法都被事务管理,也就是切入点
Advice (通知/ 增强)
Introduction (引介)
Target (目标对象)
Weaving (织入)
Proxy (代理)
Aspect (切面)
这篇文章就到这了,有点长,能看完的都很了不起。下一篇文章将讲解Spring 中基于注解的 AOP 配置