存在问题的转账案例
环境的搭建和测试
pom.xml中的依赖jar包坐标
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
</dependencies>
账户持久层的接口和实现类
//账户持久层接口
public interface IAccountDao {
//查询所有
List<Account> findAll();
//查询一个
Account findAccountById(Integer id);
//保存
void saveAccount(Account account);
//更新
void updateAccount(Account account);
//删除
void deleteAccount(Integer id);
//根据名称查找唯一一个账户
Account findAccountByName(String name);
}
//账户持久层实现类
public class AccountDaoImpl implements IAccountDao {
private QueryRunner queryRunner;
public void setQueryRunner(QueryRunner queryRunner) {
this.queryRunner = queryRunner;
}
public List<Account> findAll() {
try {
return queryRunner.query("select * from account",new BeanListHandler<Account>(Account.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public Account findAccountById(Integer id) {
try {
return queryRunner.query("select * from account where id = ?",new BeanHandler<Account>(Account.class),id);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void saveAccount(Account account) {
try {
queryRunner.update("insert into account(name,money) values (?,?)",account.getName(),account.getMoney());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void updateAccount(Account account) {
try {
queryRunner.update("update account set name=?,money=? where id =?",account.getName(),account.getMoney(),account.getId());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void deleteAccount(Integer id) {
try {
queryRunner.update("delete from account where id =?",id);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Account findAccountByName(String name) {
try {
List<Account> accounts = queryRunner.query("select * from account where name = ?", new BeanListHandler<>(Account.class), name);
//查找结果为空
if(accounts==null||accounts.size()==0)
return null;
//查找结果不唯一
if(accounts.size()>1)
throw new RuntimeException("结果集不唯一");
return accounts.get(0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
账户业务层的接口和实现类
//业务层接口
public interface IAccountService {
//查询所有
List<Account> findAll();
//查询一个
Account findAccountById(Integer id);
//保存
void saveAccount(Account account);
//更新
void updateAccount(Account account);
//删除
void deleteAccount(Integer id);
//交易
void transfer(String sourceName,String targetName,Float money);
}
//账户的业务层实现类
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public List<Account> findAll() {
return accountDao.findAll();
}
public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}
public void saveAccount(Account account) {
accountDao.saveAccount(account);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void deleteAccount(Integer id) {
accountDao.deleteAccount(id);
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
//根据名称查出转出账户
Account source = accountDao.findAccountByName(sourceName);
//根据名称查出转入账户
Account target = accountDao.findAccountByName(targetName);
//转出账户减钱
source.setMoney(source.getMoney()-money);
//转入账户加钱
target.setMoney(target.getMoney()+money);
//转出账户更新
accountDao.updateAccount(source);
//转入账户更新
accountDao.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">
<!--把对象的创建交给spring -->
<bean id="accountService" class="service.impl.AccountServiceImpl">
<!-- 注入dao对象 -->
<property name="accountDao" ref="accountDao"/>
</bean>
<!-- 配置dao -->
<bean id="accountDao" class="dao.impl.AccountDaoImpl">
<!-- 注入queryrunner -->
<property name="queryRunner" ref="query"/>
</bean>
<!-- 配置query runner 使用多例-->
<bean id="query" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入数据源 -->
<constructor-arg name="ds" ref="dataSource"/>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 连接数据库的信息 -->
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql:///test?serverTimezone=UTC"/>
<property name="user" value="root"/>
<property name="password" value="sjh2019"/>
</bean>
</beans>
测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountTest {
@Autowired
IAccountService accountService;
@Test
public void transfer(){
accountService.transfer("aaa","bbb", (float) 100);
}
}
此时运行测试,结果正常(初始aaa和bbb账户均为1000元,建表sql语句见上篇)
但如果业务层实现类存在错误,转账会存在问题
此时运行的结果:
我们看到发生错误后,转出成功了,但转入失败了,这显然不是我们想要的结果。
问题原因分析
在业务层实现类的方法中,调用了多次和数据库交互的方法,每次都使用的是新的数据库连接,因此当出现错误时,前面的事务已经提交了,而最后一次并不能执行到。
因此我们需要将所有连接绑定为同一个,方法是使用thread local,将数据库连接和当前线程绑定,从而使一个线程中只有一个能控制事务的对象。
解决问题
首先编写一个数据库连接的工具类ConnectionUtils
//连接的工具类,用于从数据源获取一个连接,并实现和线程的绑定
public class ConnectionUtils {
private ThreadLocal<Connection> threadLocal=new ThreadLocal<>();
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
//提供set方法等待spring的注入
this.dataSource = dataSource;
}
//获取当前线程上的连接
public Connection getThreadConnection(){
//1.从threadlocal获取
Connection connection = threadLocal.get();
try {
//2.判断当前线程上是否有连接
if(connection==null){
//3.从数据源获取一个连接并和线程绑定
connection=dataSource.getConnection();
threadLocal.set(connection);
}
//4.返回当前线程上的连接
return connection;
}catch (Exception e){
throw new RuntimeException();
}
}
//解绑连接和线程
public void remove(){
threadLocal.remove();
}
}
再编写一个进行事务管理的工具类TransactionManager
//和事务管理相关的工具类,包含了事务的开启,提交,回滚,释放连接
public class TransactionManager {
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
//提供set方法等待spring的注入
this.connectionUtils = connectionUtils;
}
//开启事务
public void beginTran(){
try {
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
//提交事务
public void commit(){
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
//回滚事务
public void rollback(){
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
//释放连接
public void release(){
try {
//把连接关闭,返回池中
connectionUtils.getThreadConnection().close();
//如果不解绑连接,下次获取时仍有连接,但已经不可用了
connectionUtils.remove();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
在业务层的实现类中通过事务类实现事务的控制
//账户的业务层实现类
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
private TransactionManager transactionManager;
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public List<Account> findAll() {
try{
transactionManager.beginTran();
List<Account> accounts = accountDao.findAll();
transactionManager.commit();
return accounts;
}catch (Exception e){
transactionManager.rollback();
throw new RuntimeException(e);
}finally {
transactionManager.release();
}
}
public Account findAccountById(Integer id) {
try{
transactionManager.beginTran();
Account accountById = accountDao.findAccountById(id);
transactionManager.commit();
return accountById;
}catch (Exception e){
transactionManager.rollback();
throw new RuntimeException(e);
}finally {
transactionManager.release();
}
}
public void saveAccount(Account account) {
try{
transactionManager.beginTran();
accountDao.saveAccount(account);
transactionManager.commit();
}catch (Exception e){
transactionManager.rollback();
}finally {
transactionManager.release();
}
}
public void updateAccount(Account account) {
try{
transactionManager.beginTran();
accountDao.updateAccount(account);
transactionManager.commit();
}catch (Exception e){
transactionManager.rollback();
}finally {
transactionManager.release();
}
}
public void deleteAccount(Integer id) {
try{
transactionManager.beginTran();
accountDao.deleteAccount(id);
transactionManager.commit();
}catch (Exception e){
transactionManager.rollback();
}finally {
transactionManager.release();
}
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
try{
transactionManager.beginTran();
//根据名称查出转出账户
Account source = accountDao.findAccountByName(sourceName);
//根据名称查出转入账户
Account target = accountDao.findAccountByName(targetName);
//转出账户减钱
source.setMoney(source.getMoney()-money);
//转入账户加钱
target.setMoney(target.getMoney()+money);
//转出账户更新
accountDao.updateAccount(source);
//模拟错误
int i=1/0;
//转入账户更新
accountDao.updateAccount(target);
transactionManager.commit();
}catch (Exception e){
transactionManager.rollback();
e.printStackTrace();
}finally {
transactionManager.release();
}
}
}
由于我们希望获取的连接是同一个,因此还要修改持久层的实现类,将连接加入查询方法的第一个参数,使连接为同一个
//账户持久层实现类
public class AccountDaoImpl implements IAccountDao {
private QueryRunner queryRunner;
private ConnectionUtils connectionUtils;
public void setQueryRunner(QueryRunner queryRunner) {
this.queryRunner = queryRunner;
}
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
public List<Account> findAll() {
try {
return queryRunner.query(connectionUtils.getThreadConnection(),"select * from account",new BeanListHandler<Account>(Account.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public Account findAccountById(Integer id) {
try {
return queryRunner.query(connectionUtils.getThreadConnection(),"select * from account where id = ?",new BeanHandler<Account>(Account.class),id);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void saveAccount(Account account) {
try {
queryRunner.update(connectionUtils.getThreadConnection(),"insert into account(name,money) values (?,?)",account.getName(),account.getMoney());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void updateAccount(Account account) {
try {
queryRunner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id =?",account.getName(),account.getMoney(),account.getId());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void deleteAccount(Integer id) {
try {
queryRunner.update(connectionUtils.getThreadConnection(),"delete from account where id =?",id);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Account findAccountByName(String name) {
try {
List<Account> accounts = queryRunner.query(connectionUtils.getThreadConnection(),"select * from account where name = ?", new BeanListHandler<>(Account.class), name);
//查找结果为空
if(accounts==null||accounts.size()==0)
return null;
//查找结果不唯一
if(accounts.size()>1)
throw new RuntimeException("结果集不唯一");
return accounts.get(0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
最后,由于要使连接为同一个,我们要取消对queryRunner的数据源注入,将数据源注入到连接的工具类中,这样确保使用的连接每次都是同一个,并配置事务工具类
<?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">
<!--把对象的创建交给spring -->
<bean id="accountService" class="service.impl.AccountServiceImpl">
<!-- 注入dao对象 -->
<property name="accountDao" ref="accountDao"/>
<!-- 注入事务管理 -->
<property name="transactionManager" ref="transactionManager"/>
</bean>
<!-- 配置dao -->
<bean id="accountDao" class="dao.impl.AccountDaoImpl">
<!-- 注入queryrunner -->
<property name="queryRunner" ref="query"/>
<!-- 注入工具类 -->
<property name="connectionUtils" ref="connectionUtils"/>
</bean>
<!-- 配置query runner 使用多例-->
<bean id="query" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!-- 配置connection工具类 -->
<bean id="connectionUtils" class="utils.ConnectionUtils">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 连接数据库的信息 -->
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql:///test?serverTimezone=UTC"/>
<property name="user" value="root"/>
<property name="password" value="sjh2019"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="utils.TransactionManager">
<!-- 注入工具类 -->
<property name="connectionUtils" ref="connectionUtils"/>
</bean>
</beans>
此时我们再对交易方法进行测试,测试是成功的,打印出了错误信息(除数为0),并且事务成功回滚,没有出现支出账户钱减少的现象。
但此时我们的代码、配置十分复杂和臃肿,接下来将解决此问题。
动态代理的引入
基于接口的动态代理回顾
动态代理:
- 特点:字节码随用随创建,随用随加载
- 作用:不修改源码的基础上对方法增强
- 分类:
基于接口的动态代理,基于子类的动态代理
基于接口的动态代理:设计的类:Proxy 提供者:jdk官方- 如何创建:使用proxy类的newProxyInstance方法
- 创建要求:被代理类最少实现一个接口,如果没有则不能使用
- newProxyInstance参数:
class loader:类加载器,加载代理对象字节码,和被代理对象使用相同的类加载器,固定写法
class[]:字节码数组 用于让代理对象和被代理对象有相同方法 固定写法
InvocationHandler:用于增强代码的方法,一般都是写一个该接口的实现类,通常是匿名内部类
一个销售接口和实现类
//对生产厂家要求的接口
public interface IProducer {
//销售
public void sale(float money);
}
//生产者
public class Producer implements IProducer{
//销售
public void sale(float money){
System.out.println("销售产品,拿到钱: "+money);
}
}
测试类
//模拟消费者
public class Customer {
public static void main(String[] args) {
Producer producer = new Producer();
IProducer proxyProduer= (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("sale".equals(method.getName())){
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
//不使用代理
producer.sale(10000);
//使用代理
proxyProduer.sale(10000);
}
}
运行结果
但使用接口的动态代理存在一定的问题:如果一个类不实现接口的话,就不能被代理,所以接下来学习基于子类的动态代理。
基于子类的动态代理
首页需要添加一个新的依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1.3</version>
</dependency>
基于子类的动态代理
- 提供者:第三方的 CGLib,如果报 asmxxxx 异常,需要导入 asm.jar。
- 要求:被代理类不能用 final 修饰的类(最终类)。
- 用到的类: Enhancer
- 用到的方法: create(Class, Callback)
- 方法的参数:
- Class:被代理对象的字节码
- Callback:如何代理
去掉生产者实现类的接口,使用基于子类的动态代理
public class Customer {
public static void main(String[] args) {
Producer producer = new Producer();
Producer cglibProducer= (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法,都会经过该方法。在此方法内部就可以对被代理对象的任何
方法进行增强。
*
* 参数:
* 前三个和基于接口的动态代理是一样的。
* MethodProxy:当前执行方法的代理对象。
* 返回值:
* 当前执行方法的返回值
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//提供增强逻辑f
Object returnValue=null;
//1.获取方法执行参数
Float money = (Float) objects[0];
//2.判断方法是不是销售
if("sale".equals(method.getName())){
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
//不使用代理
producer.sale(10000);//10000
//使用代理
cglibProducer.sale(10000);//8000
}
}
结果和之前一样。
使用动态代理实现事务控制
简化之前臃肿的业务层实现类
//账户的业务层实现类
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public List<Account> findAll() {
return accountDao.findAll();
}
public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}
public void saveAccount(Account account) {
accountDao.saveAccount(account);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void deleteAccount(Integer id) {
accountDao.deleteAccount(id);
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
//根据名称查出转出账户
Account source = accountDao.findAccountByName(sourceName);
//根据名称查出转入账户
Account target = accountDao.findAccountByName(targetName);
//转出账户减钱
source.setMoney(source.getMoney()-money);
//转入账户加钱
target.setMoney(target.getMoney()+money);
//转出账户更新
accountDao.updateAccount(source);
//模拟错误
int i=1/0;
//转入账户更新
accountDao.updateAccount(target);
}
}
创建一个动态代理类来对业务层实现类提供事务支持
//用于创建service代理对象的工厂
public class BeanFactory {
private IAccountService accountService;
public void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
private TransactionManager transactionManager;
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
//获取service的代理对象
public IAccountService getAccountService() {
IAccountService proxyService= (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue;
try{
transactionManager.beginTran();
returnValue = method.invoke(accountService, args);
transactionManager.commit();
return returnValue;
}catch (Exception e){
transactionManager.rollback();
throw new RuntimeException(e);
}finally {
transactionManager.release();
}
}
});
return proxyService;
}
}
修改bean.xml配置
<!-- 配置service工厂 -->
<bean id="beanFactory" class="factory.BeanFactory">
<!--注入业务层实现类 -->
<property name="accountService" ref="accountService"/>
<!--注入事务控制类 -->
<property name="transactionManager" ref="transactionManager"/>
</bean>
<!-- 配置代理类service-->
<bean id="proxyService" factory-bean="beanFactory" factory-method="getAccountService"/>
测试类
使用@Qualifier("proxyService")
指明要使用代理的service
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountTest {
@Autowired@Qualifier("proxyService")
IAccountService accountService;
@Test
public void findAll(){
List<Account> accounts = accountService.findAll();
accounts.forEach(System.out::println);
}
@Test
public void transfer(){
accountService.transfer("aaa","bbb", (float) 100);
}
}
初始时将aaa和bbb账户都置为1000,使用错误的转账方法,报错,事务回滚成功,双方金额并未改变。
将错误语句注释掉,可以成功运行。
Aop
AOP 的相关概念
AOP:
全称是 Aspect Oriented Programming 即:面向切面编程。
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。
作用:
在程序运行期间,不修改源码对已有方法进行增强。
优势:
减少重复代码
提高开发效率
维护方便
实现方式:
使用动态代理技术
Spring中的AOP
代理方式
spring中会判断类是否实现了接口决定采用哪种动态代理方式。
相关术语
- Joinpoint(连接点):
所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
例如业务层实现类中的方法都是连接点。 - Pointcut(切入点):
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
例如业务层实现类中被增强的方法都是切入点,切入点一定是连接点,但连接点不一定是切入点。 - Advice(通知/增强):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。 - Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。 - Target(目标对象):
代理的目标对象。
例如前面被代理的accountService。 - Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。 - Proxy(代理):
一个类被 AOP 织入增强后,就产生一个结果代理类。
例如前例的代理对象proxyService。 - Aspect(切面):
是切入点和通知(引介)的结合。
基于XML的AOP实现
导入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
业务层接口和实现类
//业务层接口
public interface IAccountService {
//模拟保存
void saveAccount();
//模拟更新
void updateAccount(int i);
//模拟删除
int deleteAccount();
}
//账户的业务层实现类
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("save");
}
@Override
public void updateAccount(int i) {
System.out.println("update");
}
@Override
public int deleteAccount() {
System.out.println("delete");
return 0;
}
}
通知类
//用于记录日志的工具类,提供公共代码
public class Logger {
public void beforePrintLog(){
System.out.println("前置通知");
}
public void afterPrintLog(){
System.out.println("后置通知");
}
public void errorPrintLog(){
System.out.println("异常通知");
}
public void finalPrintLog(){
System.out.println("最终通知");
}
}
bean.xml配置
aop:config
:
作用:用于声明开始 aop 的配置aop:aspect
:
作用: 用于配置切面。
属性:id
:给切面提供一个唯一标识。ref
:引用配置好的通知类 bean 的 id。aop:pointcut
:
作用: 用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
属性:expression
:用于定义切入点表达式。id
:用于给切入点表达式提供一个唯一标识
aop:before
作用: 用于配置前置通知。指定增强的方法在切入点方法之前执行
执行时间点: 切入点方法执行之前执行aop:after-returning
作用: 用于配置后置通知
执行时间点: 切入点方法正常执行之后。它和异常通知只能有一个执行aop:after-throwing
作用: 用于配置异常通知
执行时间点: 切入点方法执行产生异常后执行。它和后置通知只能执行一个aop:after
作用: 用于配置最终通知
执行时间点: 无论切入点方法执行时是否有异常,它都会在其后面执行。
切入点表达式说明
execution:匹配方法的执行(常用)
execution(表达式)
表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
写法说明:
全匹配方式:
public void service.impl.AccountServiceImpl.saveAccount(domain.Account)
访问修饰符可以省略:
void service.impl.AccountServiceImpl.saveAccount(domain.Account)
返回值可以使用*号,表示任意返回值
* service.impl.AccountServiceImpl.saveAccount(domain.Account)
包名可以使用*
号,表示任意包,但是有几级包,需要写几个*
* *.*.AccountServiceImpl.saveAccount(domain.Account)
使用..
来表示当前包,及其子包
* *..AccountServiceImpl.saveAccount(domain.Account)
类名可以使用*
号,表示任意类
* *..*.saveAccount(.domain.Account)
方法名可以使用 *
号,表示任意方法
* *..*.*( domain.Account)
参数列表可以使用*
,表示参数可以是任意数据类型,但是必须有参数
* *..*.*(*)
参数列表可以使用…表示有无参数均可,有参数可以是任意类型
* *..*.*(..)
全通配方式:
* *..*.*(..)
<?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">
<bean id="accountService" class="service.impl.AccountServiceImpl"/>
<bean id="logger" class="utils.Logger"/>
<!-- 配置aop -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知 -->
<aop:before method="beforePrintLog" pointcut="execution(* *..AccountServiceImpl.*(..))"/>
<!-- 配置后置通知 -->
<aop:after-returning method="afterPrintLog" pointcut="execution(* *..AccountServiceImpl.*(..))"/>
<!-- 配置异常通知 -->
<aop:after-throwing method="errorPrintLog" pointcut="execution(* *..AccountServiceImpl.*(..))"/>
<!-- 配置最终通知 -->
<aop:after method="finalPrintLog" pointcut="execution(* *..AccountServiceImpl.*(..))"/>
</aop:aspect>
</aop:config>
</beans>
测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountTest {
@Autowired
IAccountService accountService;
@Test
public void test(){
accountService.saveAccount();
}
}
结果
前置通知
save
后置通知
最终通知
优化切入点表达式的配置
<!-- 配置aop -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知 -->
<aop:before method="beforePrintLog" pointcut-ref="pt"/>
<!-- 配置后置通知 -->
<aop:after-returning method="afterPrintLog" pointcut-ref="pt"/>
<!-- 配置异常通知 -->
<aop:after-throwing method="errorPrintLog" pointcut-ref="pt"/>
<!-- 配置最终通知 -->
<aop:after method="finalPrintLog" pointcut-ref="pt"/>
<!-- 配置切入点表达式 写在aspect标签内 只能在当前切面使用 -->
<aop:pointcut id="pt" expression="execution(* *..AccountServiceImpl.*(..))"/>
</aop:aspect>
</aop:config>
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点表达式 写在aspect标签外 可在所有切面使用-->
<aop:pointcut id="pt" expression="execution(* *..AccountServiceImpl.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知 -->
<aop:before method="beforePrintLog" pointcut-ref="pt"/>
<!-- 配置后置通知 -->
<aop:after-returning method="afterPrintLog" pointcut-ref="pt"/>
<!-- 配置异常通知 -->
<aop:after-throwing method="errorPrintLog" pointcut-ref="pt"/>
<!-- 配置最终通知 -->
<aop:after method="finalPrintLog" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
环绕通知的配置
aop:around:
作用:
用于配置环绕通知- 属性:
method
:指定通知中方法的名称。
pointct
:定义切入点表达式
pointcut-ref
:指定切入点表达式的引用 - 说明:
它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
注意:通常情况下,环绕通知都是独立使用的
在Logger类中添加环绕通知方法,相当于之前的动态代理
/**
* 环绕通知
* @param point
* spring 框架为我们提供了一个接口:ProceedingJoinPoint,它可以作为环绕通知的方法参数。
* 在环绕通知执行时,spring 框架会为我们提供该接口的实现类对象,我们直接使用就行。
* @return
*/
public Object aroundLog(ProceedingJoinPoint point){
Object val;
try {
Object[] args = point.getArgs();
System.out.println("前置通知");
val=point.proceed(args);
System.out.println("后置通知");
return val;
}catch (Throwable e){
System.out.println("异常通知");
throw new RuntimeException(e);
}finally {
System.out.println("最终通知");
}
}
修改bean.xml的aop配置
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="pt" expression="execution(* *..AccountServiceImpl.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置环绕置通知 -->
<aop:around method="aroundLog" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
测试运行
使用注解的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">
要扫描的包:
(给业务层实现类加上@Sevice注解,此处省略)
<context:component-scan base-package="service"/>
<context:component-scan base-package="utils"/>
开启注解aop支持:
<aop:aspectj-autoproxy/>
</beans>
修改后的Logger类
//用于记录日志的工具类,提供公共代码
@Component("logger")
@Aspect//指明是切面类
public class Logger {
//配置切入点表达式
@Pointcut("execution(* *..*.AccountServiceImpl.*(..))")
private void pt(){};
@Before("pt()")
public void beforePrintLog(){
System.out.println("前置通知");
}
@AfterReturning("pt()")
public void afterPrintLog(){
System.out.println("后置通知");
}
@AfterThrowing("pt()")
public void errorPrintLog(){
System.out.println("异常通知");
}
@After("pt()")
public void finalPrintLog(){
System.out.println("最终通知");
}
@Around("pt()")
public Object aroundLog(ProceedingJoinPoint point){
Object val;
try {
Object[] args = point.getArgs();
System.out.println("前置通知");
val=point.proceed(args);
System.out.println("后置通知");
return val;
}catch (Throwable e){
System.out.println("异常通知");
throw new RuntimeException(e);
}finally {
System.out.println("最终通知");
}
}
}
测试运行
我们发现最终通知出现在后置通知前,这是注解配置aop的问题,一般我们使用环绕通知解决。我们给除了环绕通知的其他注解都注释掉,发现可以正常执行。
纯注解配置
去掉bean.xml,创建一个配置类
@Configuration//配置类
@ComponentScan(basePackages={"service","utils"})//要扫描的包
@EnableAspectJAutoProxy//开启aop支持
public class SpringConfig {
}
测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})//引入配置类
public class AccountTest {
@Autowired
IAccountService accountService;
@Test
public void test(){
accountService.saveAccount();
}
}