Spring03:案例转账功能(事务问题)、动态代理解决、AOP

2023-02-15,,,,

今日内容--核心2AOP

完善Account案例
分析案例中的问题
回顾之前讲过的技术--动态代理
动态代理的另一种实现方式
解决案例中的问题
AOP的概念
Spring中的AOP相关术语
Spring中基于xml和注解的AOP配置※
一、案例完善

1、案例中添加转账方法并演示事务问题

<?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="com.itheima.service.impl.AccountServiceImpl">
<!-- 注入dao对象 -->
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置dao对象-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<!--注入QueryRunner-->
<property name="runner" ref="runner"></property>
</bean>
<!--配置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/eesy02"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
</beans>
package com.itheima.test;
import com.itheima.domain.Account;
import com.itheima.service.IAccountService;
import com.itheima.service.impl.AccountServiceImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.List;
/**
* 使用Junit单元测试:测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/bean.xml"})
public class AccountServiceTest {
@Autowired
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}
    @Override
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);//相当于return
}
} @Override
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);
//int i = 1 / 0;
//5.更新转出账户
accountDao.updateAccount(source);
//6.更新转入账户
accountDao.updateAccount(target);
}

存在问题:
出现异常,结果不同步
2、分析事务的问题并编写ConnectionUtils
猜想:没有事务造成的

原因:每个事务都会提交,已经提交的正常结束,没有提交的报错

如何使所有事务使用同一个Connection?
需要使用ThreadLocal对象把Connection和当前线程绑定,从而使一个线程中只有一个能控制事务的对象

package com.itheima.utils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException; /**
* 连接的工具类,用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
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 (conn == null){
//3.从数据源中获取一个连接,并且和线程绑定,存入ThreadLocal中
conn = dataSource.getConnection();
tl.set(conn);
}
//4.返回当前线程上的连接
return conn;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

3、编写事务管理工具类并分析连接和线程解绑

package com.itheima.utils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException; /**
* 连接的工具类,用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
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 (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();
}
}
package com.itheima.utils;
import java.sql.SQLException;
/**
* 和事务管理相关的工具类,包含开启事务、提交事务、回滚事务和释放连接
*/
public class TransactionManager {
//获取当前线程上的Connection
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 开启事务
*/
public void beginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(true);
} 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();
}
}
}

4、编写业务层和持久层事务控制代码并配置spring的ioc

package com.itheima.utils;

import java.sql.SQLException;

/**
* 和事务管理相关的工具类,包含开启事务、提交事务、回滚事务和释放连接
*/
public class TransactionManager {
//获取当前线程上的Connection
private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
} /**
* 开启事务
*/
public void beginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(true);
} 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();
}
}
}
package com.itheima.service.impl;

import com.itheima.dao.IAccountDao;
import com.itheima.domain.Account;
import com.itheima.service.IAccountService;
import com.itheima.utils.TransactionManager; import java.util.List; /**
* 账户的业务层实现类
* 事务控制应当在业务层
*/
public class AccountServiceImpl 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) {
//回滚操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//释放连接
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) {
//回滚操作
txManager.rollback();
}finally {
//释放连接
txManager.release();
}
return accountDao.findAccountById(accountId);
} @Override
public void saveAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.saveAccount(account);
//3.提交事务
txManager.commit();
//4.返回结果 } catch (Exception e) {
//回滚操作
txManager.rollback();
}finally {
//释放连接
txManager.release();
} } @Override
public void updateAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.updateAccount(account);
//3.提交事务
txManager.commit();
//4.返回结果 } catch (Exception e) {
//回滚操作
txManager.rollback();
}finally {
//释放连接
txManager.release();
}
} @Override
public void deleteAccount(Integer accountId) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.deleteAccount(accountId);
//3.提交事务
txManager.commit();
//4.返回结果 } catch (Exception e) {
//回滚操作
txManager.rollback();
}finally {
//释放连接
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);
//int i = 1 / 0;
//2.5.更新转出账户
accountDao.updateAccount(source);
//2.6.更新转入账户
accountDao.updateAccount(target);
//3.提交事务
txManager.commit();
//4.返回结果
} catch (Exception e) {
//回滚操作
txManager.rollback();
} finally {
//释放连接
txManager.release();
}
}
}
<?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="com.itheima.service.impl.AccountServiceImpl">
<!-- 注入dao对象 -->
<property name="accountDao" ref="accountDao"></property>
<!--注入事务管理器-->
<property name="txManager" ref="txManager"></property>
</bean>
<!--配置dao对象-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<!--注入QueryRunner-->
<property name="runner" ref="runner"></property>
<!--注入ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置QueryRunner对象-->
<!--不再提供Connection对象,没有数据源,不会从数据源中获取连接-->
<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/eesy02"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!--配置Connection的工具类-ConnectionUtils-->
<bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
<!--注入数据源的配置-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理器-->
<bean id="txManager" class="com.itheima.utils.TransactionManager">
<!--注入ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
</beans>

5、测试转账并分析案例中的问题

package com.itheima.service.impl;

import com.itheima.dao.IAccountDao;
import com.itheima.domain.Account;
import com.itheima.service.IAccountService;
import com.itheima.utils.TransactionManager; import java.util.List; /**
* 账户的业务层实现类
* 事务控制应当在业务层
*/
public class AccountServiceImpl 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() {
return accountDao.findAllAccount();
} @Override
public Account findAccountById(Integer accountId) {
//2.执行操作
return accountDao.findAccountById(accountId); } @Override
public void saveAccount(Account account) {
accountDao.saveAccount(account);
} @Override
public void updateAccount(Account account) {
accountDao.updateAccount(account);
} @Override
public void deleteAccount(Integer accountId) {
accountDao.deleteAccount(accountId);
} @Override
public void transfer(String sourceName, String targetName, Float money) {
//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);
//int i = 1 / 0;
//2.5.更新转出账户
accountDao.updateAccount(source);
//2.6.更新转入账户
accountDao.updateAccount(target);
//3.提交事务
txManager.commit();
}
}
package com.itheima.utils;

import java.sql.SQLException;

/**
* 和事务管理相关的工具类,包含开启事务、提交事务、回滚事务和释放连接
*/
public class TransactionManager {
//获取当前线程上的Connection
private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
} /**
* 开启事务
*/
public void beginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(true);
} 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();
}
}
}

问题:方法名一改其他地方也需要改
二、动态代理
1、代理的分析

2、基于接口的动态代理回顾

package com.itheima.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) {
Producer producer = new Producer();
/**
* 动态代理
* 特点:字节码随用随创建,随用随加载
* 作用:在不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 设计的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类至少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* Classloader:类加载器
* 用于加载代理对象字节码,和被代理对象使用相同的类加载器
* Class[]:字节码数组
* 用于让代理对象和被代理对象有相同的方法
* InvocationHandler:用于提供增强的代码
* 让我们写如何代理,一般情况下写该接口的实现类,通常情况下是匿名内部类
* 此接口的实现类都是谁用谁写
*/
//不实现任何接口时,无法正常使用
IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法,都会经过该方法
* 方法参数
* @param proxy:代理对象的引用
* @param method:当前执行的方法
* @param args:当前执行方法所需的参数
* @return:和被代理对象有相同的返回值
* @throws Throwable
*/
@Override
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);
}
}
package com.itheima.proxy;
/**
* 一个生产者
* 生产厂家需要有标准-销售和售后(接口)
*/
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
//@Override
public void saleProduct(float money){
System.out.println("销售产品,并拿到"+money+"元钱");
} /**
* 售后
* @param money
*/
//@Override
public void afterService(float money){
System.out.println("提供售后服务,并拿到"+money+"元钱");
}
}

3、基于子类的动态代理
要求有第三方jar包的支持--导入 jar包

    <dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
</dependencies>
package com.itheima.cglib;

import com.itheima.proxy.IProducer;
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) {
Producer producer = new Producer();
/**
* 动态代理
* 特点:字节码随用随创建,随用随加载
* 作用:在不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 设计的类:Proxy
* 提供者:第三方cglib库
* 如何创建代理对象:
* 使用Ehancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
* create方法的参数:
* Class:字节码
* 用于指定被代理对象的字节码,producer.getClass()
* Callback:用于提供增强的代码
* 让我们写如何代理,一般情况下写该接口的实现类,通常情况下是匿名内部类
* 此接口的实现类都是谁用谁写
* 一般写的都是该接口的子接口实现类:MethodIntercepter
*/
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数相同
* @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())) {
returnValue = method.invoke(producer, money * 0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(10000f);
}
}

4、使用动态代理实现事务控制

package com.itheima.factory;

import com.itheima.dao.IAccountDao;
import com.itheima.domain.Account;
import com.itheima.service.IAccountService;
import com.itheima.utils.TransactionManager; import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy; /**
* 用于创建service代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
/**
* 获取service的代理对象
*
* @return
*/
public IAccountService getAccountService() {
Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 添加事务的支持
*
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
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) {
//回滚操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//释放连接
txManager.release();
}
}
});
return accountService;
}
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
}
<?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="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<!--配置beanfactory-->
<bean id="beanFactory" class="com.itheima.factory.BeanFactory">
<!--注入Service-->
<property name="accountService" ref="accountService"></property>
<!--注入事务管理器-->
<property name="txManager" ref="txManager"></property>
</bean>
<!-- 配置Service -->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<!-- 注入dao对象 -->
<property name="accountDao" ref="accountDao"></property> </bean>
</beans>
package com.itheima.test;
import com.itheima.domain.Account;
import com.itheima.service.IAccountService;
import com.itheima.service.impl.AccountServiceImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.List;
/**
* 使用Junit单元测试:测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/bean.xml"})
public class AccountServiceTest {
@Autowired
@Qualifier("proxyAccountService")
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}

三、AOP
1、AOP的概念
AOP:全称是 Aspect Oriented Programming 即:面向切面编程。

简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的

基础上,对我们的已有方法进行增强。

 spring 的 aop,就是通过配置的方式

2、spring基于XML的AOP-编写必要的代码

<?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>com.itheima</groupId>
<artifactId>day03_eesy_003springaop</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging> <properties>
<maven.compiler.source>9</maven.compiler.source>
<maven.compiler.target>9</maven.compiler.target>
</properties>
<dependencies>
<!--spring的aop会用到-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.3.RELEASE</version>
</dependency>
<!--用于解析切入点表达式-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
</dependencies> </project>
package com.itheima.service;

/**
* 账户的业务层接口
*/
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount(); /**
* 模拟更新账户
* @param i
*/
void updateAccount(int i); /**
* 模拟删除账户
* @return
*/
int deleteAccount();
}
package com.itheima.service.impl;

import com.itheima.service.IAccountService;

/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
} @Override
public void updateAccount(int i) {
System.out.println("执行了更新"+i);
} @Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
package com.itheima.utils;

/**
* 用具记录日志的工具类,内部提供公共代码
*/
public class Logger {
/**
* 向控制台打印日志:计划在其切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的printLog方法开始记录日志了");
}
}

3、spring基于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">
<!--先配置Spring的IOC,把Service对象配置进来【控制反转】-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--Spring中基于xml的aop配置步骤【面向切面编程,动态代理】
1、把通知bean(logger类)也交给Spring管理
2、使用aop:config标签表明aop的配置
3、使用aop:aspect标签表明开始配置切面
id属性用于给切面一个唯一标识
ref属性指定通知类bean的id
4、在aop:aspect标签的内部,使用对应的标签来配置通知的类型
当前示例是让printLog方法在切入点之前执行,所以是前置通知
aop:before表示配置前置通知
method属性:表示哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中的哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.类名.方法名 参数列表
标准写法:
public void com.itheima.service.impl.AccountService.saveAccount()
5、
-->
<!--配置logger类-->
<bean id="Logger" class="com.itheima.utils.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="Logger">
<!--配置通知的类型,并建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(public void com.itheima.service.impl.AccountService.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>

5、测试类及切入点表达式的写法

package com.itheima.test;

import com.itheima.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext; /**
* 测试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();//查看是否实现了记录日志/日志的打印
as.updateAccount(1);
as.deleteAccount();
}
}
<?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">
<!--先配置Spring的IOC,把Service对象配置进来【控制反转】-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--Spring中基于xml的aop配置步骤【面向切面编程,动态代理】
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.类名.方法名 参数列表
标准写法:
public void com.itheima.service.impl.AccountService.saveAccount()
1.访问修饰符可以省略 void com.itheima.service.impl.AccountService.saveAccount()
2.返回值可以使用通配符表示任意返回值 * com.itheima.service.impl.AccountService.saveAccount()
3.包名可以使用通配符表示任意包,但是有几级包,就需要写几个*.
3.1包名可以使用*..表示当前包及子包 * *..
4.类名和方法名都可以使用*实现通配 * *..*(*)
4.1参数列表:
可以直接写数据类型
基本类型直接写名称 int * *..*(int)
引用类型写报名.类名的方式 java.lang.String
类型可以使用通配符*表示任意类型,但必须有参数int * *..*(*)
可以使用..表示有无参数均可,有参数时表示任意类型 * *..*(..)
全通配写法:
* *..*.#(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl.*.*(..)
aspectj坐标表示切入点表达式的语言联盟
-->
<!--配置logger类-->
<bean id="Logger" class="com.itheima.utils.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="Logger">
<!--配置通知的类型,并建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* *..*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>

4、四种常用通知类型

<?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">
<!--先配置Spring的IOC,把Service对象配置进来【控制反转】-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--配置logger类-->
<bean id="Logger" class="com.itheima.utils.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="Logger">
<!--配置通知的类型,并建立通知方法和切入点方法的关联-->
<!--事务要么try提交,要么catch回滚-->
<!--配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut="execution(* *..*.*(..))"></aop:before>
<!--配置后置通知:在切入点方法正常执行之后执行,它和异常通知永远只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* *..*.*(..))"></aop:after-returning>
<!--配置异常通知:在切入点方法执行产生异常之后执行,它和后置通知永远只能执行一个-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* *..*.*(..))"></aop:after-throwing>
<!--配置最终通知:无论切入点方法是否正常执行,它都会在其后面执行-->
<aop:after method="afterPrintLog" pointcut="execution(* *..*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
</beans>
package com.itheima.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方法开始记录日志了");
}
}

5、通用化切入点表达式

<?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">
<!--先配置Spring的IOC,把Service对象配置进来【控制反转】-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--配置logger类-->
<bean id="Logger" class="com.itheima.utils.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
-->
<!--配置切面-->
<aop:aspect id="logAdvice" ref="Logger">
<!--配置通知的类型,并建立通知方法和切入点方法的关联-->
<!--事务要么try提交,要么catch回滚-->
<!--配置前置通知:在切入点方法执行之前执行-->
<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标签外部(必须写在aspect标签之前),可以在所有切面使用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
</aop:aspect>
</aop:config>
</beans>

6、spring中的环绕通知

<?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">
<!--先配置Spring的IOC,把Service对象配置进来【控制反转】-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--配置logger类-->
<bean id="Logger" class="com.itheima.utils.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
--> <!--配置切面-->
<aop:aspect id="logAdvice" ref="Logger">
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
<!--配置环绕通知 详细的注释请看logger类中-->
<aop:around method="aroundAroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
package com.itheima.utils;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 用具记录日志的工具类,内部提供公共代码
*/
public class Logger {
/**
* 环绕通知
* 问题:
* 当配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
* 分析:
* 通过对比动态代理中的环绕代理通知代码,发现动态代理的环绕通知有明确的切入点方法调用
*
* 动态代理的环绕通知-有明确的切入点调用
* 我们没有切入点方法
* 解决:
* Spring框架提供了一个接口:ProceedingJoinPoint,该接口有一个方法proceed,此方法就相当于明确调用切入点方法
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用
* Spring中的环绕通知:
* 是Spring框架为我们提供的一种可以在代码中手动控制增强方法的实现方式
*/
public Object aroundAroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("前置通知开始记录日志了");
rtValue = pjp.proceed(args);//明确调用业务层/切入点方法
System.out.println("后置通知开始记录日志了");
return rtValue;
} catch (Throwable throwable) {
System.out.println("异常通知开始记录日志了");
throw new RuntimeException(throwable);
}finally {
System.out.println("最终通知开始记录日志了");
return rtValue;
}
}
}

7、spring基于注解的AOP配置
@EnableAspectJAutoProxy可以不使用xml注解

package com.itheima.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component; /**
* 用具记录日志的工具类,内部提供公共代码
*/
@Component("logger")
@Aspect //表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.itheima.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方法开始记录日志了");
} /**
* 环绕通知
* 问题:
* 当配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
* 分析:
* 通过对比动态代理中的环绕代理通知代码,发现动态代理的环绕通知有明确的切入点方法调用
*
* 动态代理的环绕通知-有明确的切入点调用
* 我们没有切入点方法
* 解决:
* Spring框架提供了一个接口:ProceedingJoinPoint,该接口有一个方法proceed,此方法就相当于明确调用切入点方法
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用
* Spring中的环绕通知:
* 是Spring框架为我们提供的一种可以在代码中手动控制增强方法的实现方式
*/
//@Around("pt1()")
public Object aroundAroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("前置通知开始记录日志了");
rtValue = pjp.proceed(args);//明确调用业务层/切入点方法
System.out.println("后置通知开始记录日志了");
return rtValue;
} catch (Throwable throwable) {
System.out.println("异常通知开始记录日志了");
throw new RuntimeException(throwable);
}finally {
System.out.println("最终通知开始记录日志了");
return rtValue;
} }
}
<?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.itheima"></context:component-scan>
<!--不再需要配置service-->
<!--把通知类交给Spring管理-->
<!--配置Spring开启注解AOP注解的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!--基于注解的aop有顺序的调用过程,而环绕通知没有调用顺序的问题※-->
</beans>

总结和作业

转账案例中事务问题的切入
记录日志
重复问题,判断用户是否登录,判断用户是否有权限
解决方式:
提取出来重复代码
通过动态代理(在不改变源码的情况下将方法增强)将重复代码加入到方法中
AOP
增强的代码
执行的时机、类型与客体

来自为知笔记(Wiz)

Spring03:案例转账功能(事务问题)、动态代理解决、AOP的相关教程结束。

《Spring03:案例转账功能(事务问题)、动态代理解决、AOP.doc》

下载本文的Word格式文档,以方便收藏与打印。