目录
- 完善我们的account案例
- 分析案例中的问题
- 回顾动态代理
- 动态代理的另一种实现方式
- 解决案例中的问题
- AOP中的概念
- spring中AOP的相关术语
- spring中基于XML和注解的AOP配置
1 完善account案例
IAccountService.java业务层接口上添加转账方法
public interface IAccountService {
.....................
/**
* 转账
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
void transfer(String sourceName, String targetName, Float money);
}
在IAccountDao.java持久层接口上添加查询账户的方法
/**
* 账户的持久层接口
*/
public interface IAccountDao {
........................
/**
* 根据名称查询账户
* @param accountName
* @return 如果有唯一的一个结果就返回,如果没有结果就返回null
* 如果结果集超过一个就抛异常
*/
Account findAccountByName(String accountName);
}
在持久层实现类上实现添加根据名称查找账户的方法
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
.............................................
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("结果集不唯一,数据有问题");
}
return accounts.get(0);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
在业务层实现类上实现转账方法
/**
* 账户的业务层实现类
*
* 事务控制应该都是在业务层
*/
public class AccountServiceImpl implements IAccountService{
...................................
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer....");
//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);
//2.6更新转入账户
accountDao.updateAccount(target);
}
}
测试
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
2 存在问题:
若在业务层实现类上添加异常,转账方法上添加异常
//2.5更新转出账户
accountDao.updateAccount(source);
int i=1/0;
//2.6更新转入账户
accountDao.updateAccount(target);
则会违背一致性,账户转出钱,但未转入别的账户
分析事务的问题并编写ConnectionUtils
目前QueryRunner的配置
每次操作数据库都会创建一个新的QueryRunner,并会从数据源中拿出一个连接
QueryRunner图
事务控制
每一次操作数据库,都会获得一个新的QueryRunner,操作一个新的连接
所以报错之后,后面的数据库连接的事务没法提交,但前面的连接的事务已经提交
改进目标:使这些数据库操作属于同一个连接
事务控制应该都在业务层
新建utils.ConnectionUtils类
连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
//不能自己new创建,必须等着spring来注入,提供一个set方法
private DataSource dataSource;
//实现spring的注入
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* @return
*/
public Connection getThreadConnection() {
try{
//1.先从ThreadLocal上获取
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
包含了开启事务,提交事务,回滚事务和释放连接
操作之前必须有线程上的connection,从ConnetionUtils对象中获取
/**
* 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
//提供一个set方法,等spring注入
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();
}
}
}
编写业务层和持久层的代码控制,并配置spring的IOC
业务层
改造业务层的实现类
需要用到TransactionManager类的方法
添加一个成员变量
private TransactionManager txManager;
不能自己new,等着spring的依赖注入添加一个set方法
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
在每个方法上加上事务控制
try{
//1开启事务
txManager.beginTransaction();
//2执行操作
不同操作不同
//3提交事务
txManager.commit();
//4返回结果
不同操作不同
}catch(Exception e){
//5 回滚操作,抛出异常
txManager.rollback();
throw new RuntimeException(e);
}finally{
//6 释放连接
txManager.release();
}
/**
* 账户的业务层实现类
*
* 事务控制应该都是在业务层
*/
public class AccountServiceImpl_OLD implements IAccountService{
private IAccountDao accountDao;
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public List<Account> findAllAccount() {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
List<Account> accounts = accountDao.findAllAccount();
//3.提交事务
txManager.commit();
//4.返回结果
return accounts;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
}
@Override
public Account findAccountById(Integer accountId) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
Account account = accountDao.findAccountById(accountId);
//3.提交事务
txManager.commit();
//4.返回结果
return account;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
}
@Override
public void saveAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.saveAccount(account);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}
}
@Override
public void updateAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.updateAccount(account);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}
}
@Override
public void deleteAccount(Integer acccountId) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.deleteAccount(acccountId);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.开启事务
txManager.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);
//2.6更新转入账户
accountDao.updateAccount(target);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
e.printStackTrace();
}finally {
//5.释放连接
txManager.release();
}
}
}
持久层
Dao实现类中执行方法时,给QueryRunner注入了connection之后,就会从连接取一个
问题
我不希望QueryRunner中从连接里去取,不再注入connection
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
但不提供connction对象的时候,Dao实现类里的操作将没有Connection
在Dao实现类里加一个新的成员变量
private ConnectionUtils connectionUtils;
并且提供set方法,让spring可以注入
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
在Dao实现类操作中选择第一个参数是带有连接对象的方法
eg:
runner.query(connectionUtils.getThreadConnection(),"select * from account",new BeanListHandler<Account>(Account.class));
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
private QueryRunner runner;
private ConnectionUtils connectionUtils;
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
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 void saveAccount(Account account) {
try{
runner.update(connectionUtils.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(connectionUtils.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(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("结果集不唯一,数据有问题");
}
return accounts.get(0);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
最后把新建的依赖注入好
首先配置Connection的工具类,注入数据源
<!-- 配置数据源 -->
<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/springtest"></property>
<property name="user" value="root"></property>
<property name="password" value="xgh961120"></property>
</bean>
<!-- 配置Connection的工具类 ConnectionUtils -->
<bean id="connectionUtils" class="com.tju.utils.ConnectionUtils">
<!-- 注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
再配置Dao实现类,注入Connection的工具类
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
<!--配置Dao对象-->
<bean id="accountDao" class="com.tju.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="runner"></property>
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
最后配置业务层实现类业务层还需要事务管理器TransactionManager
1配置事务管理器,并注入成员变量ConnectionUtils
<!-- 配置事务管理器-->
<bean id="txManager" class="com.tju.utils.TransactionManager">
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
2 给service实现类注入事务管理器
<!-- 配置Service -->
<bean id="accountService" class="com.tju.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"></property>
<!-- 注入事务管理器 -->
<property name="txManager" ref="txManage"></property>
</bean>
测试转账并分析案例中的问题
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
测试成功,满足事务的一致性
对service实现类进行进一步改造
原来的service实现类对TransactionManager类有方法依赖,TransactionManager里的方法变化,service实现类的调用也必须改变
在删去冗余后
/**
* 账户的业务层实现类
*
* 事务控制应该都是在业务层
*/
public class AccountServiceImpl implements IAccountService{
private IAccountDao accountDao;
public void setAccountDao(IAccountDao 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) {
accountDao.saveAccount(account);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void deleteAccount(Integer acccountId) {
accountDao.deleteAccount(acccountId);
}
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer....");
//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 代理分析
动态代理解决了什么问题?
举一个例子
原来我们买电脑,直接去生产厂家买电脑,生产厂家提供销售和售后
现在我们买电脑,去经销商那里买电脑,经销商就是生产厂家的一个代理
经销商对原来生产厂家的销售和售后进行改造,原来生产厂家卖4000,经销商卖7000
代理
IProducer.java接口
/**
* 对生产厂家要求的接口(代理商的要求,要求生产厂家必须提供销售和售后)
*/
public interface IProducer {
/**
* 销售
* @param money
*/
void saleProduct(float money);
/**
* 售后
* @param money
*/
void afterSerivce(float money);
}
Producer实现类
/**
* 一个生产者(生产厂家)
*/
public class Producer implements IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,工厂并拿到钱"+money);
}
/**
* 售后
* @param money
*/
public void afterSerivce(float money){
System.out.println("提供售后服务,工厂并拿到钱"+money);
}
}
Client.java(消费者)
//原来的模式
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
//原来的模式直接去生产厂商去买
//客户就看中这个生产厂商的东西
Producer producer = new Producer();
//客户交钱拿货
producer.saleProduct(10000f);
}
}
结果:
销售产品,工厂并拿到钱10000.0
动态代理:
- 特点:字节码随用随创建,随用随加载
- 作用:不修改源码的基础上对方法增强(一会儿对Producer.java中的代码增强)
- 分类:
基于接口的动态代理
基于子类的动态代理
基于接口的动态代理
- 设计的类:Proxy
- 提供者:JDK官方
- 如何创建代理对象
- 使用Proxy类中的newProxyInstance方法
- 创建代理对象的要求:
- 被代理类最少实现一个接口,如果没有则不能使用
newProxyInstance方法
- newProxyInstance方法的参数
- ClassLoader:类加载器
- 它是用于加载代理对象字节码的,写的是被代理对象类的加载器(和被代理对象使用相同的类加载器)
- 固定写法
代理谁,就写谁的.getClass().getClassLoader()
- Class[]:字节码数组
- 它是用于让代理对象和被代理对象有相同的方法(只要两个都实现同一个接口,两个对象都会有接口的方法)
- 固定写法
代理谁,就写谁的.getclass().getInterfaces()
- InvocationHandler:用于提供增强的代码
- 它是让我们写如何代理,我们一般是写一个该接口的实现类,通常情况下都是匿名内部类(但不是必须的)
- 此接口的实现类都是谁用谁写
- ClassLoader:类加载器
实现InvocationHandler接口需要重写一个方法
public Object invoke(Object proxy, Method method, Object[] args)
- 作用:执行被代理对象的任何接口方法都会经过该方法(被代理对象producer中的IProducer接口的方法,都会经过该方法)
- 该方法就会有拦截的功能
- 参数介绍:
@param proxy 指的是代理对象的引用
@param method 当前执行的方法
@param args 当前执行方法所需的参数
@return 和被代理对象方法有相同的返回值
再重写此方法时通常会用到反射中的invoke方法
- invoke(Object obj, Object… args)
- Object obj 指的是谁的方法,被代理对象(的方法):producer
- Object… args 方法的参数
- 匿名内部类访问外部成员变量的时候,用final修饰
- 返回值是Object类型
- eg: method.invoke(producer,args);
模拟动态模式的消费者
/**
* 模拟一个消费者
*/
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() {
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())){
//经销商进行代理,拿走2000
/**
* invoke(Object obj, Object... args)
* Object obj 指的是谁的方法,被代理对象(的方法):producer
* 匿名内部类访问外部成员变量的时候,用final修饰
*Object... args
* 方法的参数
* 返回值是Object类型
* method.invoke(producer,args);
*/
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
结果
销售产品,工厂并拿到钱8000.0
经过经销商的二次加工 ,也就是代理对象对被代理对象的方法进行了改造
问题:
限制当我们的被代理对象不实现任何接口的时候会发生异常
Producer implements IProducer才会成功
想要代理一个普通的Java类
一个生产者(生产厂家)没有实现接口
public class Producer {
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,工厂并拿到钱"+money);
}
/**
* 售后
* @param money
*/
public void afterSerivce(float money){
System.out.println("提供售后服务,并拿到钱"+money);
}
}
解决:基于子类的动态代理
在pom.xml中导入依赖
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
</dependencies>
基于子类的动态代理
- 设计的类:Enhancer
- 提供者:第三方cglib库
- 如何创建代理对象
- 使用Enhancer类中的create方法
- 创建代理对象的要求:
- 被代理类不能是最终类
- create方法的参数
- class:用于指定一个字节码
- 它使用于指定被代理对象的字节码
- 固定写法
代理谁,就写谁的.getClass()
- Callback:用于提供增强的代码
- 它是让我们写如何代理,我们一般是写一个该接口的实现类,通常情况下都是匿名内部类(但不是必须的)
- 此接口的实现类都是谁用谁写
- 我们一般写的都是该接口的子接口的实现类:MethodInterceptor(子接口,方法拦截) public interface MethodInterceptor extends Callback
- 实现子接口的话要重写一个方法
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable-
执行被代理对象的任何方法都会经过该方法
前三个参数跟刚才基于接口动态代理的参数一样 -
@param proxy:指的是代理对象的引用
-
@param method:当前执行的方法
-
@param args:当前执行方法所需的参数
-
以上三个参数和基于接口的动态代理中invoke方法的参数是一致的
-
@param methodProxy:当前执行方法的代理对象
-
@return Object 和被代理对象执行方法中的返回值是一样的
-
- class:用于指定一个字节码
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 基于子类的动态代理
* 设计的类:Enhancer
* 提供者:第三方cglib库
* 如何创建代理对象
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
* create方法的参数
* class:用于指定一个字节码
* 它使用于指定被代理对象的字节码
* 固定写法
* 代理谁,就写谁的.getClass()
* Callback:用于提供增强的代码
* 它是让我们写如何代理,我们一般是写一个该接口的实现类,通常情况下都是匿名内部类(但不是必须的)
* 此接口的实现类都是谁用谁写
* 我们一般写的都是该接口的子接口的实现类:MethodInterceptor(子接口,方法拦截)
* public interface MethodInterceptor extends Callback
*/
Producer cglibproducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过改方法
* 前三个参数跟刚才基于接口动态代理的参数一样
* @param proxy:指的是代理对象的引用
* @param method:当前执行的方法
* @param args:当前执行方法所需的参数
*以上三个参数和基于接口的动态代理中invoke方法的参数是一致的
* @param methodProxy:当前执行方法的代理对象
* @return Object 和被代理对象执行方法中的返回值是一样的
* @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())) {
//经销商进行代理,拿走2000
/**
* invoke(Object obj, Object... args)
* Object obj 指的是谁的方法,被代理对象(的方法):producer
* 匿名内部类访问外部成员变量的时候,用final修饰
*Object... args
* 方法的参数
* 返回值是Object类型
* method.invoke(producer,args);
*/
returnValue = method.invoke(producer, money * 0.8f);
}
return returnValue;
}
});
cglibproducer.saleProduct(10000f);
}
}
结果
销售产品,工厂并拿到钱8000.0
使用cglib代理了没有接口实现的类
4 使用动态代理实现事务控制
我们希望用我们改造的简洁service 并实现事务控制
public class AccountServiceImpl implements IAccountService{
private IAccountDao accountDao;
public void setAccountDao(IAccountDao 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) {
accountDao.saveAccount(account);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void deleteAccount(Integer acccountId) {
accountDao.deleteAccount(acccountId);
}
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer....");
//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);
}
}
新建一个类叫做BeanFactory
用于创建Service代理对象的工厂
原来事务的代码改成代理对象重写匿名内部对象new InvocationHandler()中
public Object invoke(Object proxy, Method method, Object[] args)的方法
原来事务的代码
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
Account account = accountDao.findAccountById(accountId);
//3.提交事务
txManager.commit();
//4.返回结果
return account;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
改造成利用反射的方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue =null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
//不能直接写对象调用Account account = accountDao.findAccountById(accountId);
//参数 被代理对象,被代理对象的方法
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commit();
//4.返回结果
return account;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
public class BeanFactory {
//被代理对象
private IAccountService accountService;
//重新service方法中用到此对象
private TransactionManager txManager;
//通过spring注入的方式,获取成员变量的值
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
//需要通过spring注入的方式,获取成员变量的值
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
/**
* 获取Service代理对象
* @return
*/
public IAccountService getAccountService() {
return (IAccountService)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.开启事务
txManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e) {
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6.释放连接
txManager.release();
}
}
});
}
}
事务控制和业务层的方法实现了分离
在beans.xml
先配置beanfactory
<!--配置beanfactory-->
<bean id="beanFactory" class="com.tju.factory.BeanFactory">
<!-- 注入service -->
<property name="accountService" ref="accountService"></property>
<!-- 注入事务管理器 -->
<property name="txManager" ref="txManager"></property>
</bean>
配置代理的service对象,有事务支持的
<!--配置代理的service-->
<!--对象创建实例工厂factory-bean,创建方法是factory-method-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
有两个service对象,一个原本的sevice实现类,一个用动态代理实现+事务控制的service对象,@Qualifier("proxyAccountService")
区分
/**
* 使用Junit单元测试:测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
@Autowired
@Qualifier("proxyAccountService")
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}
虽然通过动态代理的方式
实现了service对象事务控制,和业务的分离
但是配置复杂
如何改进?
5 AOP 面向切面编程
spring中的AOP
spring选择AOP有一个准则
根据是否实现了接口,来决定是基于接口的动态代理,还是基于子类的动态代理
AOP相关概念
Joinpoint (连接点)
所谓连接点是只那些被拦截的点,在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
类比刚才的案例
打开业务层接口,这些接口的方法,可以通过动态代理的方法增强代码加上事务的逻辑
public interface IAccountService {
/**
* 查询所有
* @return
*/
List<Account> findAllAccount();
/**
* 查询一个
* @return
*/
Account findAccountById(Integer accountId);
/**
* 保存
* @param account
*/
void saveAccount(Account account);
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 删除
* @param acccountId
*/
void deleteAccount(Integer acccountId);
/**
* 转账
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
void transfer(String sourceName, String targetName, Float money);
}
Pointcut(切入点)
所谓切入点是指我们要对哪些Jointpoint进行拦截的定义,切入点是指被增强的方法,有的方法可以不被增强则称为连接点
比如在业务层接口添加
public interface IAccountService {
..........................
void test();//它只是连接点,但不是切入点,因为没有被增强
}
在代理对象工厂增强代码加上判断方法是不是test,是的话才增强
public IAccountService getAccountService() {
return (IAccountService)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 {
//如果是接口方法test,则不增强,此时test只是连接点,不是切入点
if("test".equals(method.getName())){
return method.invoke(accountService,args);
}
Object rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
.......
}}
Advice(通知/增强)
所谓通知是指拦截到Jointpoint之后所要做的通知
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知‘
一般动态代理中的invoke方法就是对接口方法进行增强的
通知类型图
Target(目标对象)
代理的目标对象(也就是被代理的对象)
Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程
原有的accountService对象没法实现事务,通过动态代理的方式返回加入事务管理的动态代码
整个过程叫做织入
Proxy(代理)
一个类被AOP织入增强后,就产生一个结果代理类
Aspect(切面)
是指切入点和通知(引介 )的结合
比如切入点是指IAcountService接口哪些方法被增强过
通知是提供公共代码的TransactionManager类,这些代码何时去执行呢?
建立切入点和通知方法的对应关系就是切面
Spring基于XML的AOP编写必要代码
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
IAccountService接口的三类方法
IAccountService接口
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
/**
* 模拟更新用户
* @param i
*/
void updateAccount(int i);
/**
* 删除账户
* @return
*/
int deleteAccount();
}
接口的实现类
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
}
public void updateAccount(int i) {
System.out.println("执行了更新");
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
公共代码部分
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 用于打入日志,计划让其在切入点方法方法执行之前执行(切入点方法就是业务层方法)
*/
public void printlog(){
System.out.println("Logger类中的printlog方法开始记录日志了。。。");
}
}
spring基于XML的AOP-配置步骤
创建bean.xml
导入aop约束
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
有一个service实现类对象需要加上增强,利用springIOC加载进来
有一个日志工具类(公共代码部分)(其实就是通知),它里面的方法可以加上日志增强,利用springIOC加载进来
配置AOP
首先配置切面,切面就是引用通知(日志工具类)
通知中的方法(printLog)是准备在切入点之前先执行,指明切入点方法的具体位置(切入点表达式)
思路
spring中基于XML的AOP配置步骤
1.把通知类的Bean也交给spring来管理
2.使用aop:config标签表明开始AOP的配置
3.使用aop:aspect标签表面配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的id
4.在aop:aspect标签内部使用对应标签来配置通知的类型
我们现在的示例是让printLog方法在切入点方法执行之前:所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
标准的表达式写法
public void cn.tju.service.impl.AccountServiceImpl.saveAccount();
<!--配置spring的IOC,把service对象配置进来-->
<bean id="accountService" class="cn.tju.service.impl.AccountServiceImpl"></bean>
<!--spring中基于XML的AOP配置步骤
1.把通知类的Bean也交给spring来管理
2.使用aop:config标签表明开始AOP的配置
3.使用aop:aspect标签表面配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的id
4.在aop:aspect标签内部使用对应标签来配置通知的类型
我们现在的示例是让printLog方法在切入点方法执行之前:所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名....类名.方法名(参数列表)
标准的表达式写法
public void cn.tju.service.impl.AccountServiceImpl.saveAccount();
-->
<!--配置logger类-->
<bean id="logger" class="cn.tju.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(public void cn.tju.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
测试
/**
* 测试AOP的测试
*/
public class AOPTest {
public static void main(String[] args) {
//1获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2获取对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//3执行方法
as.saveAccount();
}
}
结果
Logger类中的printlog方法开始记录日志了。。。
执行了保存
难道要对每一个业务层的接口方法都配一遍切入点表达式?
切入点表达式的写法
切点全通配写法:* *..*.*(..)
分析有标准的表达式如何到全通配写法
标准
public void cn.tju.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void cn.tju.service.impl.AccountServiceImpl.saveAccount();
返回值可以使用通配符表示任意返回值
* cn.tju.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符表示任意包,但是有几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount()
包名可以使用 ..
表示当前包及其子包,*..
代表任意包下的AccountServiceImp类都会被增强
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*
来实现通配
先改类名
* *..*.saveAccount()
再改方法名
* *..*.*()
此时接口中没有参数的方法都会被增强,void updateAccount(int i);不会
void saveAccount();
void updateAccount(int i);
int deleteAccount();
参数列表:可以直接写数据类型:
基本数据类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
类型可以使用通配符*
表示任意类型,但必须有参数
可以使用 ..
表示有无参数均可,有参数可以是任意类型
示例1
* *..*.*(int)
只有参数是int的方法被增强
void updateAccount(int i);
示例2
参数任意类型都可以,但只针对有参数
* *..*.*(*)
void updateAccount(int i);被增强
示例3
* *..*.*(..)
参数有无,有任意类型均可以,
三个方法全部增强
实际开发中切入点表达式的通常写法
切到业务层实现类下的所有方法
标准:
void cn.tju.service.impl.AccountServiceImpl.saveAccount();
(任意返回值 cn.tju.service.impl这个包下的任意类,任意方法名称,方法参数任意可有可无)
* cn.tju.service.impl.*.*(..)
修改beans.xml一个位置
<aop:before method="printLog" pointcut="execution(* *..*.*(..))"></aop:before>
测试结果
public class AOPTest {
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(0);
as.deleteAccount();
}
}
结果
Logger类中的printlog方法开始记录日志了。。。
执行了保存
Logger类中的printlog方法开始记录日志了。。。
执行了更新
Logger类中的printlog方法开始记录日志了。。。
执行了删除
四种常用的通知类型
修改通知类Logger
配置通知类中的四种通知方法
public class Logger {
/**
* 用于打入日志,计划让其在切入点方法方法执行之前执行(切入点方法就是业务层方法)
* 前置通知
*/
public void beforePrintLog(){
System.out.println("Logger类中前置通知的printlog方法开始记录日志了。。。");
}
/**
*
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("Logger类中后置通知的printlog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("Logger类中异常通知的printlog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("Logger类中最终通知的printlog方法开始记录日志了。。。");
}
}
在beans.xml中配置四种通知
<!--配置logger类-->
<bean id="logger" class="cn.tju.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型,并且建立通知方法和切入点方法的关联-->
<!--配置前置通知,在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut="execution(* cn.tju.service.impl.*.*(..))"></aop:before>
<!--配置后置通知,在切入点方法正式常执行之后执行,后置通知和异常通知永远只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* cn.tju.service.impl.*.*(..))"></aop:after-returning>
<!--配置异常通知,再切入点方法执行产生异常之后执行-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* cn.tju.service.impl.*.*(..))"></aop:after-throwing>
<!--配置最终通知,无论切入点是否正常执行它都会在其后面执行-->
<aop:after method="afterPrintLog" pointcut="execution(* cn.tju.service.impl.*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
注意:
后置通知和异常通知永远只能执行一个
测试
//1获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2获取对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//3执行方法
as.saveAccount();
结果
Logger类中前置通知的printlog方法开始记录日志了。。。
执行了保存
Logger类中后置通知的printlog方法开始记录日志了。。。
Logger类中最终通知的printlog方法开始记录日志了。。。
通用化切入点表达式
切入点表达式设置太繁琐了
引入新的标签 <aop:pointcut id="pt1" expression="execution(* cn.tju.service.impl.*.*(..))"></aop:pointcut>
配置切入点表达式
- id属性用于指定表达式的唯一标识
- expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用,它还可以写在aop:aspect外面,此时就变成了所有切面可用
当写在aop:aspect标签内部时
<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>
<!--配置切入点表达式,id属性用于指定表达式的唯一标识,expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用-->
<aop:pointcut id="pt1" expression="execution(* cn.tju.service.impl.*.*(..))"></aop:pointcut>
</aop:aspect>
当写在aop:aspect标签外部时:
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:pointcut id="pt1" expression="execution(* cn.tju.service.impl.*.*(..))"></aop:pointcut>
<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>
<!--配置切入点表达式,id属性用于指定表达式的唯一标识,expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用-->
</aop:aspect>
</aop:config>
spring中的环绕通知
在切面中配置环绕通知
<!--配置logger类-->
<bean id="logger" class="cn.tju.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:pointcut id="pt1" expression="execution(* cn.tju.service.impl.*.*(..))"></aop:pointcut>
<aop:aspect id="logAdvice" ref="logger">
<!--配置环绕通知-->
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
在日志工具类中配置环绕通知方法
public class Logger {
public void aroundPringLog(){
System.out.println("Logger类中最终通知的aroundPringLog方法开始记录日志了。。。");
}
}
测试
//1获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2获取对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//3执行方法
as.saveAccount();
结果
Logger类中最终通知的aroundPringLog方法开始记录日志了。。。
当我们配置了环绕通知的时候,切入点方法没有执行,而通知方法执行了
分析:回顾动态代理中的环绕通知为整个方法是环绕通知,在环绕通知中有明确的切入点调用(绿色)
通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知有明确的切入点方法调用,而我们的代码中没有
解决:
- spring框架为我们提供了一个接口:ProceedingJoinPoint,该接口有一个方法proceed(),此方法相当于明确调用切入点方法
- 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用
spring中的环绕通知
它是spring框架为我们提供的一种方式,一种可以在代码中手动控制增强方法何时执行的方式
public class Logger {
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args =pjp.getArgs();//得到方法执行所需要的参数
System.out.println("Logger类中通知的aroundPringLog方法前置开始记录日志了。。。"); //此时相当于前置方法
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中通知的aroundPringLog方法后置开始记录日志了。。。"); //此时相当于后置方法
return rtValue;
} catch (Throwable throwable) {
System.out.println("Logger类中通知的aroundPringLog方法异常开始记录日志了。。。"); //此时相当异常方法
throw new RuntimeException(throwable);
}finally {
System.out.println("Logger类中通知的aroundPringLog方法最终开始记录日志了。。。"); //此时相当最终方法
}
}
}
结果等效于XML中配置了切面方法
Logger类中最终通知的aroundPringLog方法前置开始记录日志了。。。
执行了保存
Logger类中最终通知的aroundPringLog方法后置开始记录日志了。。。
Logger类中最终通知的aroundPringLog方法最终开始记录日志了。。。
spring基于注解AOP配置
首先更改bean.xml中的约束
配置spring创建容器需要扫描的包
service实现类对象不用配了直接加上注解
@Service("accountService")
public class AccountServiceImpl implements IAccountService{
Logger类也不用配了,直接加上注解,还得加上注解说明这是个切面类
前置通知就在方法上@Before(“pt1()”)
后置通知就在方法上@AfterReturning(“pt1()”)
异常通知就在方法上@AfterThrowing(“pt1()”)
最终通知就在方法上@After(“pt1()”)
环绕通知就在方法上@Around(“pt1()”)
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
//定义切入点方法,生成切入点表达式
@Pointcut("execution(* com.tju.service.impl.*.*(..))")
private void pt1(){}
最后配置开启注解AOP的支持
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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.tju"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>