目录
1.案例分析
原始表
Account.java
package cn.cqu.domain;
public class Account {
private Integer id;
private String name;
private float money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getMoney() {
return money;
}
public void setMoney(float money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
IAccountDao.java
package cn.cqu.dao;
import cn.cqu.domain.Account;
import java.util.List;
/**
* 账户的持久层接口
*/
public interface IAccountDao {
/**
* 查询所有
* @return
*/
List<Account> findAllAccount();
/**
* 查询一个
* @param accountId
* @return
*/
Account findById(Integer accountId);
/**
* 插入
* @param account
*/
void saveAccount(Account account);
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 删除
* @param accountId
*/
void deleteAccount(Integer accountId);
/**
* 根据名称查询账户
* @param accountName
* @return 如果有唯一的结果就返回,如果没有结果就返回null
* 如果结果集超过一个就抛出异常
*/
Account findAccountByName(String accountName);
}
AccountDaoImpl.java
package cn.cqu.dao.impl;
import cn.cqu.dao.IAccountDao;
import cn.cqu.domain.Account;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private QueryRunner runner;
public List<Account> findAllAccount() {
try {
return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Account findById(Integer accountId) {
try {
return runner.query("select * from account where id = ?",new BeanHandler<Account>(Account.class),accountId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void saveAccount(Account account) {
try {
runner.update("insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void updateAccount(Account account) {
try {
runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void deleteAccount(Integer accountId) {
try {
runner.update("delete from account where id=?",accountId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
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("结果集不唯一,数据有问题");
}
return accounts.get(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
IAccoutService.java
package cn.cqu.service;
import cn.cqu.domain.Account;
import java.util.List;
public interface IAccoutService {
/**
* 查询所有
* @return
*/
List<Account> findAllAccount();
/**
* 查询一个
* @param accountId
* @return
*/
Account findById(Integer accountId);
/**
* 插入
* @param account
*/
void saveAccount(Account account);
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 删除
* @param accountId
*/
void deleteAccount(Integer accountId);
/**
* 转账
* @param sourceName 转出账户
* @param targetName 转入账户
* @param money 转账金额
*/
void transfer(String sourceName,String targetName,float money);
}
AccountServiceImpl.java
package cn.cqu.service.impl;
import cn.cqu.domain.Account;
import cn.cqu.service.IAccoutService;
import cn.cqu.dao.IAccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("accountService")
public class AccountServiceImpl implements IAccoutService {
@Autowired
private IAccountDao dao;
public List<Account> findAllAccount() {
return dao.findAllAccount();
}
public Account findById(Integer accountId) {
return dao.findById(accountId);
}
public void saveAccount(Account account) {
dao.saveAccount(account);
}
public void updateAccount(Account account) {
dao.updateAccount(account);
}
public void deleteAccount(Integer accountId) {
dao.deleteAccount(accountId);
}
public void transfer(String sourceName,String targetName,float money)
{
//1.根据名称查询转出账户
Account source = dao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = dao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney()+money);
//5.更新转出账户
dao.updateAccount(source);
int i=1/0;
//6.更新转入账户
dao.updateAccount(target);
}
}
bean.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: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">
<!--告知spring要在创建容器时要扫描的包-->
<context:component-scan base-package="cn.cqu"></context:component-scan>
<!-- 配置QueryRunner对象 -->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 使用构造方法注入数据源 -->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
<!-- 配置数据源对象 -->
<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/mydb"></property>
<property name="user" value="root"></property>
<property name="password" value=""></property>
</bean>
</beans>
AccountServiceTest.java
package cn.cqu.test;
import cn.cqu.service.IAccoutService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 使用Junit单元测试:测试我们的配置
*/
public class AccountServiceTest {
@Test
public void testTransfer()
{
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccoutService as = ac.getBean("accountService", IAccoutService.class);
as.transfer("aaa","bbb",500f);
}
}
可以发现在转账中途出异常后,转账结果最终不满足事务的一致性
上述代码出现问题的原因分析:
解决办法:
- 使用Threadlocal对象把Connection和当前线程绑定,从而使一个线程中只有一个能控制事务的对象
- 注意:事务的控制应该全部在业务层
对上述代码做如下修改:
代码目录结构:
添加了ConnectionUtil.java
package cn.cqu.utils;
import javax.sql.DataSource;
import java.sql.Connection;
/**
* 连接的工具类
* 它用于从数据源获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtil {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
*/
public Connection getThreadConnection()
{
try {
//1.先从ThreadLocal中获取
Connection conn = tl.get();
//2.判断当前线程上是否有连接
if (null == conn) {
//3.从数据源中获取一个连接,和线程绑定,存入ThreadLocal中
conn = dataSource.getConnection();
tl.set(conn);
}
//4.返回当前线程上的连接
return conn;
}catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection()
{
tl.remove();
}
}
TransactionManager.java
package cn.cqu.utils;
import java.sql.SQLException;
/**
* 和事务管理相关的工具类
*/
public class TransactionManager {
private ConnectionUtil connectionUtil;
public void setConnectionUtil(ConnectionUtil connectionUtil) {
this.connectionUtil = connectionUtil;
}
/**
* 开启事务
*/
public void startTransaction()
{
try {
connectionUtil.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit()
{
try {
connectionUtil.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback()
{
try {
connectionUtil.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void releaseConnection()
{
try {
//还回连接池中
//如果只写如下关闭时,只是把连接还回连接池中,线程上还是绑着该连接
//下次判断时,连接还在,但是不能使用
connectionUtil.getThreadConnection().close();
//我们需要如下解绑线程和连接
connectionUtil.removeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
修改AccountDaoImpl.java
package cn.cqu.dao.impl;
import cn.cqu.dao.IAccountDao;
import cn.cqu.domain.Account;
import cn.cqu.utils.ConnectionUtil;
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 IAccountDao {
private QueryRunner runner;
private ConnectionUtil connectionUtil;
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
public void setConnectionUtil(ConnectionUtil connectionUtil) {
this.connectionUtil = connectionUtil;
}
public List<Account> findAllAccount() {
try {
return runner.query(connectionUtil.getThreadConnection(),"select * from account",new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Account findById(Integer accountId) {
try {
return runner.query(connectionUtil.getThreadConnection(),"select * from account where id = ?",new BeanHandler<Account>(Account.class),accountId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void saveAccount(Account account) {
try {
runner.update(connectionUtil.getThreadConnection(),"insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void updateAccount(Account account) {
try {
runner.update(connectionUtil.getThreadConnection(),"update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void deleteAccount(Integer accountId) {
try {
runner.update(connectionUtil.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(connectionUtil.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("结果集不唯一,数据有问题");
}
return accounts.get(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
AccountServiceImpl.java
package cn.cqu.service.impl;
import cn.cqu.domain.Account;
import cn.cqu.service.IAccoutService;
import cn.cqu.dao.IAccountDao;
import cn.cqu.utils.TransactionManager;
import java.util.List;
public class AccountServiceImpl implements IAccoutService {
private IAccountDao dao;
private TransactionManager transactionManager;
public void setDao(IAccountDao dao) {
this.dao = dao;
}
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public List<Account> findAllAccount() {
try {
//1.开启事务
transactionManager.startTransaction();
//2.执行操作
List<Account> accounts = dao.findAllAccount();
//3.提交事务
transactionManager.commit();
//4.返回结果
return accounts;
}catch (Exception e){
//5.回滚事务
transactionManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放资源
transactionManager.releaseConnection();
}
}
public Account findById(Integer accountId) {
try {
//1.开启事务
transactionManager.startTransaction();
//2.执行操作
Account account = dao.findById(accountId);
//3.提交事务
transactionManager.commit();
//4.返回结果
return account;
}catch (Exception e){
//5.回滚事务
transactionManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放资源
transactionManager.releaseConnection();
}
}
public void saveAccount(Account account) {
try {
//1.开启事务
transactionManager.startTransaction();
//2.执行操作
dao.saveAccount(account);
//3.提交事务
transactionManager.commit();
}catch (Exception e){
//4.回滚事务
transactionManager.rollback();
}finally {
//5.释放资源
transactionManager.releaseConnection();
}
}
public void updateAccount(Account account) {
try {
//1.开启事务
transactionManager.startTransaction();
//2.执行操作
dao.updateAccount(account);
//3.提交事务
transactionManager.commit();
}catch (Exception e){
//4.回滚事务
transactionManager.rollback();
}finally {
//5.释放资源
transactionManager.releaseConnection();
}
}
public void deleteAccount(Integer accountId) {
try {
//1.开启事务
transactionManager.startTransaction();
//2.执行操作
dao.deleteAccount(accountId);
//3.提交事务
transactionManager.commit();
}catch (Exception e){
//4.回滚事务
transactionManager.rollback();
}finally {
//5.释放资源
transactionManager.releaseConnection();
}
}
public void transfer(String sourceName,String targetName,float money)
{
try {
//1.开启事务
transactionManager.startTransaction();
//2.执行操作
//2.1.根据名称查询转出账户
Account source = dao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = dao.findAccountByName(targetName);
//2.3.转出账户减钱
source.setMoney(source.getMoney() - money);
//2.4.转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5.更新转出账户
dao.updateAccount(source);
int i=1/0;
//2.6.更新转入账户
dao.updateAccount(target);
//3.提交事务
transactionManager.commit();
}catch (Exception e){
//4.回滚事务
transactionManager.rollback();
e.printStackTrace();
}finally {
//5.释放资源
transactionManager.releaseConnection();
}
}
}
配置bean.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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置Service -->
<bean id="accountService" class="cn.cqu.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"></property>
<!-- 注入事务管理器 -->
<property name="transactionManager" ref="txManager"></property>
</bean>
<!--配置Dao-->
<bean id="accountDao" class="cn.cqu.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="runner"></property>
<!--注入ConnectionUtil-->
<property name="connectionUtil" ref="connectionUtil"></property>
</bean>
<!-- 配置QueryRunner对象 -->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!-- 配置数据源对象 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 注入连接数据库的必备信息 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb"></property>
<property name="user" value="root"></property>
<property name="password" value=""></property>
</bean>
<!--配置ConnectionUtil的工具类-->
<bean id="connectionUtil" class="cn.cqu.utils.ConnectionUtil">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理器-->
<bean id="txManager" class="cn.cqu.utils.TransactionManager">
<!--注入ConnectionUtil-->
<property name="connectionUtil" ref="connectionUtil"></property>
</bean>
</beans>
其他均同上
运行结果:
虽然这种写法可以保证事务的一致性,但是存在如下问题:
- 1.配置变得比较复杂,AccountServiceImpl中重复代码多
- 2.方法之间的依赖,AccountServiceImpl中的方法依赖于ConnectUtil中的方法,比如,将startTransaction方法的方法名更换,AccountServiceImpl类就不能通过编译
对于上述问题,在下文解决
2.动态代理
特点:
- 字节码随用随创建,随用随加载
作用:
- 不修改源码的基础上对源码进行增强
分类:
- 1.基于接口的动态代理
- 2.基于子类的动态代理
2.1 基于接口的动态代理
涉及到的类:Proxy
提供方:JDK官方
如何创建代理对象:使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
- 被代理对象最少实现一个接口,如果没有则不能使用
newProxyInstance的参数:
- ClassLoader:类加载器
- 它是用来加载代理对象的字节码的,和被代理对象使用相同的类加载器
- 固定写法:被代理对象.getClass().getClassLoader();如:
- Class[]:
- 它是让代理对象和被代理对象有相同的方法
- 固定写法:被代理对象.getClass().getInterfaces();如:
- InvocationHandler:用于提供增强的代码
- 它是让我们写如何代理,我们一般都是写一个接口的实现类,通常情况下是匿名内部类,但不是必须的
- 此接口的实现类都是谁用谁写
代码示例:
IProducer.java
package cn.cqu.proxy;
public interface IProducer {
/**
* 销售
*
* @param money
*/
public void saleProduct(float money);
/**
* 售后
*
* @param money
*/
public void afterService(float money);
}
Producer.java
package cn.cqu.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);
}
}
Client.java
package cn.cqu.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()))
{
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
基于接口的动态代理的缺点:
- 当我们的类没有实现接口的时候,它是不能使用的
而我们可以使用下面的基于子类的动态代理来代理一个普通的没有实现接口的类
2.2 基于子类的动态代理
这种动态代理必须有第三方jar包的支持
涉及到的类:Enhancer
提供方:第三方cglib库
如何创建代理对象:使用Enhancer类中的create方法
创建代理对象的要求:
- 被代理类不能是最终类
create方法的参数:
- Class:字节码
- 它用于指定被代理对象的字节码
- 固定格式:被代理对象.getClass()
- Callback:用于提供增强的代码
- 我们一般写的都是该接口的子接口的实现类,MethodInterceptor
示例代码:
Producer.java
package cn.cqu.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);
}
}
Client.java
package cn.cqu.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 模拟一个消费者
*/
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
* 以上三个参数和基于接口的动态代理的invoke方法中的参数是一样的
* @param methodProxy 当前执行方法的代理对象
* @return
* @throws Throwable
*/
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()))
{
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000);
}
}
这两种动态代理的共同点是:都可以对我们已经写好的方法进行增强,在增强的过程中实现我们需要的一些功能
好处举例:
- 比如上述连接池中close方法在关闭时不能真正地关闭,而是还回池中,可以用动态代理的方式对Connection的close方法进行增强,
2.3 使用动态代理实现事务控制
接下来使用动态代理改进案例1中的代码:
添加BeanFactory.java用于创建Service的代理对象
package cn.cqu.factory;
import cn.cqu.service.IAccoutService;
import cn.cqu.utils.TransactionManager;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 用于创建service代理对象的工厂
*/
public class BeanFactory {
private IAccoutService accountService;
private TransactionManager transactionManager;
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public final void setAccountService(IAccoutService accoutService) {
this.accountService = accoutService;
}
/**
* 获取service的代理对象
* @return
*/
public IAccoutService getAccountService()
{
IAccoutService proxyAccountservice = (IAccoutService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue =null;
try {
//1.开启事务
transactionManager.startTransaction();
//2.执行操作
returnValue = method.invoke(accountService,args);
//3.提交事务
transactionManager.commit();
//4.返回结果
return returnValue;
}catch (Exception e){
//4.回滚事务
System.out.println("操作异常,进行回滚");
transactionManager.rollback();
throw new RuntimeException(e);
}finally {
//5.释放资源
transactionManager.release();
}
}
});
return proxyAccountservice;
}
}
AccountServiceImpl.java简化变更如下:
package cn.cqu.service.impl;
import cn.cqu.domain.Account;
import cn.cqu.service.IAccoutService;
import cn.cqu.dao.IAccountDao;
import java.util.List;
public class AccountServiceImpl implements IAccoutService {
private IAccountDao dao;
public void setDao(IAccountDao dao) {
this.dao = dao;
}
public List<Account> findAllAccount() {
return dao.findAllAccount();
}
public Account findById(Integer accountId) {
return dao.findById(accountId);
}
public void saveAccount(Account account) {
dao.saveAccount(account);
}
public void updateAccount(Account account) {
dao.updateAccount(account);
}
public void deleteAccount(Integer accountId) {
dao.deleteAccount(accountId);
}
public void transfer(String sourceName,String targetName,float money)
{
System.out.println("transfer方法执行了。。。");
//1.根据名称查询转出账户
Account source = dao.findAccountByName(sourceName);
//2根据名称查询转入账户
Account target = dao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney()+money);
//5.更新转出账户
dao.updateAccount(source);
int i=1/0;
//6.更新转入账户
dao.updateAccount(target);
}
}
修改配置文件bean.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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置proxyAccountService -->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<!-- 配置beanFactory -->
<bean id="beanFactory" class="cn.cqu.factory.BeanFactory">
<property name="accountService" ref="accountService"></property>
<!-- 注入事务管理器 -->
<property name="transactionManager" ref="txManager"></property>
</bean>
<!-- 配置Service -->
<bean id="accountService" class="cn.cqu.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"></property>
</bean>
<!--配置Dao-->
<bean id="accountDao" class="cn.cqu.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="runner"></property>
<!--注入ConnectionUtil-->
<property name="connectionUtil" ref="connectionUtil"></property>
</bean>
<!-- 配置QueryRunner对象 -->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!-- 配置数据源对象 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 注入连接数据库的必备信息 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb"></property>
<property name="user" value="root"></property>
<property name="password" value=""></property>
</bean>
<!--配置ConnectionUtil的工具类-->
<bean id="connectionUtil" class="cn.cqu.utils.ConnectionUtil">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理器-->
<bean id="txManager" class="cn.cqu.utils.TransactionManager" >
<!--注入ConnectionUtil-->
<property name="connectionUtil" ref="connectionUtil"></property>
</bean>
</beans>
AccountServiceTest.java
package cn.cqu.test;
import cn.cqu.service.IAccoutService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 使用Junit单元测试:测试我们的配置
*/
public class AccountServiceTest {
@Test
public void testTransfer()
{
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccoutService as = ac.getBean("proxyAccountService", IAccoutService.class);
as.transfer("aaa","bbb",500f);
}
}
通过代理对象实现了对tranfer方法的增强,同时也解耦了AccountService类中transfer方法中对TrasactionManager中方法的依赖,即便TransactionManager中的方法名做了更改,编译时,AccountService类也能独立完整编译,而在运行时才会报错,而如果没有动态代理,我们凡是调用了TransactionManager中修改了的方法的地方都要进行修改,有了动态代理我们只需要修改代理类
上述思想并没有完全消除依赖,它将AccountService中的依赖转移到代理类BeanFactory类中,降低了业务类本身的耦合性
通过上述案例再去理解接下来的AOP就通俗易懂了
3.Spring中的AOP
3.1 Spring的AOP介绍
(1)什么是AOP
- Aspect Oriented Programming:面向切面编程
- 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
- AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
- 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
(2)作用与优势
作用:
- 在不修改源码的情况下,对已有方法进行增强
优势:
- 减少重复代码
- 提高开发效率
- 降低了耦合性,方便维护
(3)Spring中的AOP
Spring中的AOP是通过配置的方式实现2中动态代理的内容
Spring中AOP的细节
Spring中可以手动地选择是使用基于接口的动态代理还是基于子类的动态代理
Spring中AOP的相关术语
- 1.Joinpoint(连接点)
- 所谓连接点是指哪些被拦截到的点,在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
- 比如上述的IAccountService中所有的方法都是连接点,连接业务和增强方法中的那个点,我们要把增强的代码加到业务中来,只能通过这些方法,这些方法可以加上代理的方法即事务的支持
- 2.Pointcut(切入点)
- 所谓切入点是我们要对哪些Joinpoint进行拦截的定义,切入点指的是被增强的方法
-
package cn.cqu.service; import cn.cqu.domain.Account; import java.util.List; public interface IAccoutService { ...... void test(); //假设为IAccoutService中添加此函数 }
并且动态代理中的方法做如下修改:
-
所有切入点都是连接点,但连接点不一定是切入点,即不一定被方法增强
-
3.Advice(通知/增强)
-
指拦截到Joinpoint之后要做的事情
-
通知类型:前置通知、后置通知、异常通知、最终通知、环绕通知
-
-
4.Target(被代理对象):
-
如上述的accountService
-
-
5.Weaving(织入):
-
把增强应用到被代理对象来创建新的代理对象的过程称为织入
-
spring中采用动态代理织入,AspectJ采用编译器织入和类装载期注入
-
-
-
6.Proxy(代理):
-
一个类被增强织入以后,就会产生一个结果代理类
-
-
7.Aspect(切面):
-
建立切入点方法和通知方法之间执行调用的对应关系
-
如下这段代码就是在建立切面
-
-
(4)学习Spring我们要明确的事
- a.开发阶段(我们做的)
- 1.编写核心业务代码
- 2.把公用代码抽取出来,制作完成通知
- 3.在配置文件中,声明切入点和通知间的关系,即切面
- b.运行阶段(Spring框架完成的)
- Spring框架监控切入点方法的执行,一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类型,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行
3.2 基于XML的AOP配置
示例:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.cqu</groupId>
<artifactId>xmlAop</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
</project>
IAccountService.java
package cn.cqu.service;
/**
* 账户的业务层接口
*/
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
/**
* 模拟更新账户
*/
void updateAccount(int i);
/**
* 模拟删除账户
*/
int deleteAccount();
}
AccountServiceImpl.java
package cn.cqu.service.impl;
import cn.cqu.service.IAccountService;
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
}
public void updateAccount(int i) {
System.out.println("执行了更新:"+i);
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
Logger.java
package cn.cqu.utils;
public class Logger {
/**
* 用于打印日志,计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog()
{
System.out.println("Logger类中的printLog方法开始记录日志了。。。");
}
}
bean.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" 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">
<!--配置spring中的IOC,将service配置进来-->
<bean id="accountService" class="cn.cqu.service.impl.AccountServiceImpl" ></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="cn.cqu.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,并且建立通知方法和切入点的关联-->
<aop:before method="printLog" pointcut="execution(* cn.cqu.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
TestAOP.java
package cn.cqu.test;
import cn.cqu.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAOP {
public static void main(String[] args) {
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//3.执行方法
as.saveAccount();
as.updateAccount(1);
as.deleteAccount();
}
}
配置步骤:
- 1.把通知的bean也交给spring来管理
- 2.使用aop:config标签表明开始AOP的配置
- 3.使用aop:aspect标签表明配置切面
- id属性:给切面提供唯一标志
- ref属性:指定通知类bean的id
- 4.在aop:aspect标签内部使用对应标签来配置通知的类型,并使用切入点表达式建立通知方法和切入点的关联
- 我们现在上述的示例是让printLog方法在切入点方法执行之前,所以是前置通知
- apo:before表示配置前置通知
- method属性:用于指定Logger类中哪个方法是前置通知
- pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中的哪些方法增强
切入点表达式的写法:
- 关键字:execution(表达式)
- 表达式:访问修饰符 返回值 包名.包名....类名.方法名(参数列表)
- 标准表达式示例:
- 简化规则:
- 访问修饰符可以省略
- 返回值可以使用通配符,表示任意返回值
- 包名可以使用通配符,表示任意包,但是有几级包就需要写几个*
- 包名可以使用..当前包及其子包
- 类名和方法名都可以使用方法名实现通配
- 参数列表:
- 可以直接写数据类型:
- 基本类型直接写名称 如int
- 引用类型写包名.类名的方式 如java.lang.String
- 可以使用通配符表示任意类型,但是必须有参数
- 可以使用..表示有无参数均可,有参数可以是任意类型
- 可以直接写数据类型:
- 通过上述规则得到最终统配写法:
- 实际开发中切入点表达式的通常写法
- 切到业务层实现类下的所有方法
四种常用通知类型:(直接使用代码演示,除过以下,同上述代码,测试类中只调用saveAccount方法演示)
更改上述代码中Logger.java
package cn.cqu.utils;
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog()
{
System.out.println("Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
public void afterReturningPrintLog()
{
System.out.println("Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
public void afterThrowingprintLog()
{
System.out.println("Logger类中的afterThrowingprintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
public void afterPrintLog()
{
System.out.println("Logger类中的afterPrintLog方法开始记录日志了。。。");
}
}
bean.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" 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">
<!--配置spring-->
<bean id="accountService" class="cn.cqu.service.impl.AccountServiceImpl" ></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="cn.cqu.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,并且建立通知方法和切入点的关联-->
<!--前置通知-->
<aop:before method="beforePrintLog" pointcut="execution(* cn.cqu.service.impl.*.*(..))">
</aop:before>
<!--后置通知-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* cn.cqu.service.impl.*.*(..))">
</aop:after-returning>
<!--异常通知-->
<aop:after-throwing method="afterThrowingprintLog" pointcut="execution(* cn.cqu.service.impl.*.*(..))">
</aop:after-throwing>
<!--最终通知-->
<aop:after method="afterPrintLog" pointcut="execution(* cn.cqu.service.impl.*.*(..))">
</aop:after>
</aop:aspect>
</aop:config>
</beans>
正常情况下:
异常情况下:
将saveAccount方法写成如下时:
简化切入点表达式的配置:
bean.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" 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">
<!--配置spring-->
<bean id="accountService" class="cn.cqu.service.impl.AccountServiceImpl" ></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="cn.cqu.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切入点表达式,
id属性用于指定表达式的唯一标识,
expression用于指定表达式的内容
此标签写在aop:aspect内部,只能当前切面使用
它也可以写在aop:aspect外部,所有切面都可以使用,由于导入的约束的要求,表达式必须写在所有切面的前面
-->
<aop:pointcut id="pt1" expression="execution(* cn.cqu.service.impl.*.*(..))"/>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,并且建立通知方法和切入点的关联-->
<!--前置通知-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<!--后置通知-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!--异常通知-->
<aop:after-throwing method="afterThrowingprintLog" pointcut-ref="pt1"></aop:after-throwing>
<!--最终通知-->
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
</beans>
配置环绕通知:
bean.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" 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">
<!--配置spring-->
<bean id="accountService" class="cn.cqu.service.impl.AccountServiceImpl" ></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="cn.cqu.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* cn.cqu.service.impl.*.*(..))"/>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,并且建立通知方法和切入点的关联-->
<!--环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
Logger.java
package cn.cqu.utils;
import org.aspectj.lang.ProceedingJoinPoint;
public class Logger {
/**
* 环绕通知
*
* public void aroundPrintLog()
* {
* System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。");
* }
*
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而环绕通知方法执行了
* 分析:
* 通过对比动态代理中环绕通知的代码,发现动态代理中的环绕通知有明确的切入点方法调用,而我们的代码中没有
* 解决:
* Spring框架为我们提供了一个接口——ProceedingJoinPoint,该接口有一个方法,此方法相当于明确调用切入点方法
* 该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架为我们提供该接口的实现类供我们使用
*/
public Object aroundPrintLog(ProceedingJoinPoint pJP)
{
Object returnValue=null;
try {
//得到方法执行所需的参数
Object[] args = pJP.getArgs();
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。前置通知");
returnValue = pJP.proceed(args);
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。后置通知");
return returnValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。异常通知");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。最终通知");
}
}
/**
* 也就是我们既可以通过配置的方式实现四种常用通知,也可以通过自己编码的方式来实现
*/
}
我们既可以通过配置的方式实现四种常用通知,也可以通过自己编码的方式来实现
3.3 基于注解的AOP配置
配置步骤:
- 1.第一步:把通知类也使用注解配置
- 2.第二步:在通知类上使用@Aspect 注解声明为切面
- 作用:把当前类声明为切面类。
- 3.第三步:在增强的方法上使用注解配置通知
- 4.在 spring 配置文件中开启 spring 对注解 AOP 的支持
- 5.环绕通知注解配置
- 6.切入点表达式注解
示例代码:
bean.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">
<!--配置spring创建容器时要扫描的包-->
<context:component-scan base-package="cn.cqu"></context:component-scan>
<!--配置spring开启注解Aop的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
AccountServiceImpl.java
package cn.cqu.service.impl;
import cn.cqu.service.IAccountService;
import org.springframework.stereotype.Service;
/**
* 账户的业务层实现类
*/
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
}
public void updateAccount(int i) {
System.out.println("执行了更新:"+i);
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
Logger.java
package cn.cqu.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* cn.cqu.service.impl.*.*(..))")
private void pt1(){}
// /**
// * 前置通知
// */
// @Before("pt1()")
// public void beforePrintLog()
// {
// System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
// }
//
// /**
// * 后置通知
// */
// @AfterReturning("pt1()")
// public void afterReturningPrintLog()
// {
// System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
// }
//
// /**
// * 异常通知
// */
// @AfterThrowing("pt1()")
// public void afterThrowingprintLog()
// {
// System.out.println("异常通知Logger类中的afterThrowingprintLog方法开始记录日志了。。。");
// }
//
// /**
// * 最终通知
// */
// @After("pt1()")
// public void afterPrintLog()
// {
// System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
// }
/**
* 环绕通知
*
*/
@Around("pt1()") //注意传入的切入点表达式中的()一定要写上,不然会出现异常
public Object aroundPrintLog(ProceedingJoinPoint pJP)
{
Object returnValue=null;
try {
//得到方法执行所需的参数
Object[] args = pJP.getArgs();
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。前置通知");
returnValue = pJP.proceed(args);
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。后置通知");
return returnValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。异常通知");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。最终通知");
}
}
}
异常情况下也是如此
Spring基于注解的AOP中这四种通知类型确实有顺序调用的问题
Spring基于注解的AOP中使用环绕通知的情况下就不会发生上述情况
所以如果需要用注解的话,建议使用环绕通知