Spring_AOP
Account案例中的转账
持久层接口IAccountDao.java,新增按用户名查询。
public interface IAccountDao {
...
/**
* 按用户名查询账户
* @param accountName
* @return 如果有唯一的结果就返回,如果没有结果就返回null,如果结果集超过一个,抛出异常。
*/
Account findAccountByName(String accountName);
}
持久层接口实现类AccountDaoImpl.java,实现按名成查询。
public class AccountDaoImpl implements IAccountDao {
private QueryRunner queryRunner;
...
public Account findAccountByName(String accountName) {
try {
List<Account> accounts = queryRunner.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 (SQLException e) {
throw new RuntimeException(e);
}
}}
业务层接口IAccountService.java新增转账方法
public interface IAccountService {
...
/**
* 转账
* @param sourceName 转出账户
* @param targetName 转入账户
* @param money 金额
*/
void transfer(String sourceName, String targetName, Float money);
}
业务层接口实现类AccountServiceImpl.java
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
...
public void transfer(String sourceName, String targetName, Float money) {
//得到转出账户
Account sourceAccount = accountDao.findAccountByName(sourceName);
//得到转入账户
Account targetAccount = accountDao.findAccountByName(targetName);
//转出账户减少
sourceAccount.setMoney(sourceAccount.getMoney() - money);
//转入账户增加
targetAccount.setMoney(targetAccount.getMoney() + money);
//更新转出账户
accountDao.updateAccount(sourceAccount);
int i = 1/0;
//更新转入账户
accountDao.updateAccount(targetAccount);
}
}
xml配置文件中数据源的配置
<!-- 配置queryRunner对象 -->
<bean id="queryRunner" 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/spring?useSSL=false"></property>
<property name="user" value="root"></property>
<property name="password" value="welcome1"></property>
</bean>
存在的问题
在上述案例中,总共和数据库交互了四次,每次和数据交互都会从连接池中获取一个对象,并执行对应的SQL。前面三条可以正常执行,但是第四次由于被零除的异常,未能执行。导致转账操作出错。
原因:每次操作都使用的是不同connection,未成功提交的无法对已经提交成功的sql进行回滚,导致数据库信息出错。
解决:让整个转账过程的所有操作使用一个connection。使用ThreadLocal对象吧Connection和当前现场绑定,从而使一个线程中只有一个能控制事务的对象。
工具类
/**
* 连接工具类
* 从数据源中获取一个连接,并且实现和线程绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
*
* @return
*/
public Connection getConnection() {
try {
Connection connection = threadLocal.get();//从ThreadLocal上获取连接
if (connection == null) {//当前线程是否有连接
//从数据源中获取连接,并且和当前线程绑定
connection = dataSource.getConnection();
threadLocal.set(connection);
}
//返回当前线程上的连接
return connection;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection(){
threadLocal.remove();
}
}
/********************************************************************************************************************************************************************************************/
/**
* 事务管理相关的工具类(开启事务,提交事务,回滚事务,释放连接)
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 开启事务
*/
public void beginTransaction(){
try {
connectionUtils.getConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit(){
try {
connectionUtils.getConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback(){
try {
connectionUtils.getConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void release(){
try {
connectionUtils.getConnection().close();
connectionUtils.removeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
修改持久层实现类
/**
* 持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
private QueryRunner queryRunner;
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
public void setQueryRunner(QueryRunner queryRunner) {
this.queryRunner = queryRunner;
}
public List<Account> findAllAccount() {
try {
return queryRunner.query(connectionUtils.getConnection(), "select * from account", new BeanListHandler<Account>(Account.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
......
}
修改业务层实现类
/**
* 账户的业务层实现类
*/
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> findAllAccount() {
try {
//开启事务
transactionManager.beginTransaction();
//执行操作
List<Account> accounts = accountDao.findAllAccount();
//提交事务
transactionManager.commit();
//返回结果
return accounts;
} catch (Exception e) {
//回滚事务
transactionManager.rollback();
throw new RuntimeException(e);
} finally {
//释放连接
transactionManager.release();
}
}
......
public void transfer(String sourceName, String targetName, Float money) {
try {
//开启事务
transactionManager.beginTransaction();
//执行操作
//得到转出账户
Account sourceAccount = accountDao.findAccountByName(sourceName);
//得到转入账户
Account targetAccount = accountDao.findAccountByName(targetName);
//转出账户减少
sourceAccount.setMoney(sourceAccount.getMoney() - money);
//转入账户增加
targetAccount.setMoney(targetAccount.getMoney() + money);
//更新转出账户
accountDao.updateAccount(sourceAccount);
//更新转入账户
accountDao.updateAccount(targetAccount);
//提交事务
transactionManager.commit();
} catch (Exception e) {
//回滚事务
transactionManager.rollback();
throw new RuntimeException(e);
} finally {
//释放连接
transactionManager.release();
}
}
}
修改配置文件,重新配置依赖关系
<!-- 业务层Service对象 -->
<bean id="accountService" class="com.xijianlv.service.impl.AccountServiceImpl">
<!-- 注入Dao对象 -->
<property name="accountDao" ref="accountDao"></property>
<!-- 注入事务管理器 -->
<property name="transactionManager" ref="transactionManager"></property>
</bean>
<!-- 配置Dao对象 -->
<bean id="accountDao" class="com.xijianlv.dao.impl.AccountDaoImpl">
<!-- 注入queryRunner -->
<property name="queryRunner" ref="queryRunner"></property>
<!-- 注入connectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!-- 配置queryRunner对象 -->
<bean id="queryRunner" 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/spring?useSSL=false"></property>
<property name="user" value="root"></property>
<property name="password" value="welcome1"></property>
</bean>
<bean id="connectionUtils" class="com.xijianlv.utils.ConnectionUtils">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="com.xijianlv.utils.TransactionManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
经过新增连接工具类和事务管理相关的工具类,转账的过程中不再使用多个连接,事务可以正常控制。但是,从目前的业务层代码来看,有很多的冗余代码;而且还增强了代码将的耦合(事务管理工具和业务层的方法耦合)。
动态代理
基于接口的动态代理
/**
* 模拟消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
producer.saleProduct(10000f);
/**
* 动态代理:字节码随用随创建,随用随加载。在不修改源码的基础上对方法进行增强。
* 分类:
* 基于接口的动态代理
* 涉及的类:Proxy
* 提供方:JDK
* 基于子类的动态代理
*
* 基于接口的动态代理
* 涉及的类:Proxy
* 提供方:JDK
* 如何创建代理对象:使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:被代理类最少实现一个接口,否则不能使用
* newProxyInstance方法参数:
* ClassLoader loader:类加载器,用于加载代理对象字节码。和被代理对象使用相同的类加载起。
* Class<?>[] interfaces:字节码数组,用于让代理对象和被代理对象有相同的方法。
* InvocationHandler h:用于提供增强的代码,实现如何代理。一般情况是接口的实现类,通常是匿名内部类,但不是必须的。
*
*/
IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
/**
* 执行被代理对象的任何接口方法都会经过该方法。
*
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行的方法所需的参数
* @return 和被代理对象有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
Float money = (Float) args[0];
if("saleProduct".equals(method.getName())){
result = method.invoke(producer,args);
}
return result;
}
});
proxyProducer.saleProduct(10000f);
}
}
基于子类的动态代理
/**
* 模拟消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
producer.saleProduct(10000f);
/**
* 动态代理:字节码随用随创建,随用随加载。在不修改源码的基础上对方法进行增强。
* 分类:
* 基于接口的动态代理
*
* 基于子类的动态代理
* 涉及的类:Enhancer
* 提供方:第三方cglib库
* 如何创建代理对象:使用Enhancer类中的create方法
* 创建代理对象的要求:被代理类不能是最终类
* Object create(Class type, Callback callback)
* 参数Class type:字节码,用于指定被代理对象的字节码。
* 参数Callback callback:用于提供增强的代码。一般写的为该接口的子接口实现类:MethodInterceptor
*/
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过此方法
* @param o
* @param method
* @param objects
* 前三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy 当前执行方法的代理对象
* @return 和被代理对象的返回值一样
* @throws Throwable
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object result = null;
Float money = (Float)objects[0];
if("saleProduct".equals(method.getName())){
result = method.invoke(producer,money*0.8f);
}
return result;
}
});
cglibProducer.saleProduct(11000f);
}
}
解决案例中的问题
使用动态代理实现事务控制。
/**
* 用于创建Service的代理对象的工厂
*/
public class BenaFactory {
private IAccountService accountService;
private TransactionManager transactionManager;
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
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 result = null;
try {
//开启事务
transactionManager.beginTransaction();
//执行操作
result = method.invoke(accountService, args);
//提交事务
transactionManager.commit();
return result;
} catch (Exception e) {
transactionManager.rollback();
throw new RuntimeException(e);
} finally {
transactionManager.release();
}
}
});
}
}
在xml配置文件中,配置代理的service。
<!-- 配置代理的 service-->
<bean id="proxyAccountService" factory-bean="benaFactory" factory-method="getAccountService"></bean>
<!-- 配置benaFactory -->
<bean id="benaFactory" class="com.xijianlv.factory.BenaFactory">
<property name="accountService" ref="accountService"></property>
<property name="transactionManager" ref="transactionManager"></property>
</bean>
<!-- 业务层Service对象 -->
<bean id="accountService" class="com.xijianlv.service.impl.AccountServiceImpl_old">
<!-- 注入Dao对象 -->
<property name="accountDao" ref="accountDao"></property>
</bean>
AOP
AOP为面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各部分进行隔离,从而使得业务逻辑各部分间的耦合度降低,提供程序的可重用性,同时提高开发效率。
作用:在代码运行期间,不修改源码可对已存在的方法进行增强。
优势:维护方便、较少重复代码,提供开发效率。
Spring中Aop的相关术语
Joinpoint(连接点):指的是被连接到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
Pointcut(切入点):指的是对哪些Joinpoint进行拦截。即对哪些方法进行了增强。
可知,所有的切入点都是连接点,而连接点并不一定是切入点。比如,在增强点代码,对方法进行筛选增强,只增强一部分方法。此时,所有的方法都是连接点,而只有被增强点方法才是切入点。
Advice(通知/增强):所谓通知指的是拦截到Joinpoint之后所要做的事情就是通知。通知的类型:前置通知、后置通知、异常通知、最终通知、环绕通知。
Introduction(引介):引介是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期间为类动态的添加一些方法或Field。
Target(目标对象):代理的目标对象,即被代理对象。
Weaving(织入):是指把增强应用到目标对象来创建新代理对象的过程。spring采用动态代理织入。
Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类。
Aspect(切面):切入点和通知的结合。
Spring中基于XML的AOP配置
xml中的相关配置
-
把通知bean叫个Spring来管理。
-
使用aop:config标签表示开始aop的配置。
-
使用aop:aspect标签表明配置切面。
属性:
- id:给切面提供一个唯一标识
- ref:指定通知类bean的id
-
在aop:aspect标签内部使用对应的标签来配置通知的类型。
aop:before 标签:表示前置通知
属性:
- method:指定Logger类中的哪个方法是前置通知
- pointcut:指定切入点表达式,该表达式的含义指的是对业务层中的哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:访问修饰符 返回值 包名.包名…类名.方法名(参数列表)
public void com.xijianlv.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略:void com.xijianlv.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符:* com.xijianlv.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。:* *.*.*.*.AccountServiceImpl.saveAccount()
包名可以使用…表示当包及其子包:* *…AccountServiceImpl.saveAccount()
类名和方法名也可以用*实现通配:* *…*.*()
参数类型可以使用通配符,但必须有参数:* *…*.*(*)
使用…表示有无参数均可,参数类型任意:* *…*.*(…) 通常是业务层是类型中的所有方法:* com.xijianlv.service.impl.*.*(…)
<!-- 配置IoC -->
<!-- 配置service对象 -->
<bean id="accountServiceImpl" class="com.xijianlv.service.impl.AccountServiceImpl"></bean>
<!-- 基于xml的AOP配置 -->
<!-- 1.把通知bean叫个Spring来管理 -->
<bean id="logger" class="com.xijianlv.utils.Logger"></bean>
<!-- 2.使用aop:config标签表示开始aop的配置 -->
<aop:config>
<!-- 3.使用aop:aspect标签表明配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!-- 4.在aop:aspect标签内部使用对应的标签来配置通知的类型-->
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联 -->
<aop:before method="printLog"
pointcut="execution(* com.xijianlv.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
通知的类型
<aop:config>
<!-- 配置切入点表达式,id属性用于指定表达式的唯一标识。expreesion属性用于指定表达式内容
此标签写在 aop:aspect标签内部只能是当前切面使用。
它可以写在标签aop:aspect外面,此时所有切面均可使用。
-->
<aop:pointcut id="pointcup1" expression="execution(* com.xijianlv.service.impl.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联 -->
<!-- 前置通知:切入点方法执行之前执行 -->
<aop:before method="beforePrintLog"
pointcut-ref="pointcup1"></aop:before>
<!-- 后置通知:切入点方法正常执行之后执行 -->
<aop:after-returning method="afterReturningPrintLog"
pointcut-ref="pointcup1"></aop:after-returning>
<!-- 异常通知:切入点方法执行产生异常执行之后执行 -->
<aop:after-throwing method="afterThrowingPrintLog"
pointcut-ref="pointcup1"></aop:after-throwing>
<!-- 最终通知:无论切入点方法的执行结果,他都会在其后执行 -->
<aop:after method="afterPrintLog" pointcut-ref="pointcup1"></aop:after>
</aop:aspect>
</aop:config>
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方法记录日志。。。");
}
}
aop:pointcut标签:配置切入点表达式。
属性:
- id:用于指定表达式的唯一标识。
- expreesion:用于指定表达式内容。
此标签写在 aop:aspect标签内部只能是当前切面使用。
它可以写在标签aop:aspect外面,此时所有切面均可使用。
环绕通知
xml中的配置:
<aop:config>
<aop:pointcut id="pointcup1" expression="execution(* com.xijianlv.service.impl.*.*(..))"/>
<aop:aspect id="logAdvice" ref="logger">
<aop:around method="aroundPrintLog" pointcut-ref="pointcup1"></aop:around>
</aop:aspect>
</aop:config>
环绕通知需要明确调用切入点方法,即业务层的方法。spring框架提供了ProceedingJoinPoint接口,该结构有个一proceed()方法,此方法相当于明确调用切入点方法。该接口可以作为环绕通知当方法参数,在程序执行时spring框架会提供该接口当实现类供开发者使用。
它是spring框架提供当一种可以在代码中手动控制增强方法何时执行当方式。
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null;
try {
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在proceed()方法之前,相当于前置通知");
Object[] args = proceedingJoinPoint.getArgs();//得到方法执行所需参数
result = proceedingJoinPoint.proceed(args);//明确调用业务层方法,也叫切入点方法。
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在proceed()方法之后,相当于后置通知");
return result;
} catch (Throwable throwable) {
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在catch中,相当于异常通知");
throw new RuntimeException(throwable);
} finally {
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在finally中,相当于最终通知");
}
}
Spring中基于注解的AOP配置
xml配置文件中开启注解aop的支持
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- spring创建容器要扫描的包 -->
<context:component-scan base-package="com.xijianlv"></context:component-scan>
<!-- 配置spring开启注解aop的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
对应的切面类
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.xijianlv.service.impl.*.*(..))")//配置切入点表达式
private void pointcup1(){}
/**
* 前置通知
*/
//@Before("pointcup1()")
public void beforePrintLog() {
System.out.println("前置通知:Logger类中beforePrintLog方法记录日志。。。");
}
/**
* 后置通知
*/
//@AfterReturning("pointcup1()")
public void afterReturningPrintLog() {
System.out.println("后置通知:Logger类中afterReturningPrintLog方法记录日志。。。");
}
/**
* 异常通知
*/
//@AfterThrowing("pointcup1()")
public void afterThrowingPrintLog() {
System.out.println("异常通知:Logger类中afterThrowingPrintLog方法记录日志。。。");
}
/**
* 最终通知
*/
//@After("pointcup1()")
public void afterPrintLog() {
System.out.println("最终通知:Logger类中afterPrintLog方法记录日志。。。");
}
/**
* 环绕通知
*/
@Around("pointcup1()")
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null;
try {
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在proceed()方法之前,相当于前置通知");
Object[] args = proceedingJoinPoint.getArgs();//得到方法执行所需参数
result = proceedingJoinPoint.proceed(args);//明确调用业务层方法,也叫切入点方法。
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在proceed()方法之后,相当于后置通知");
return result;
} catch (Throwable throwable) {
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在catch中,相当于异常通知");
throw new RuntimeException(throwable);
} finally {
System.out.println("Logger类中aroundPrintLog方法记录日志。。。放在finally中,相当于最终通知");
}
}
}
Spring在使用注解配置Aop的时候,@After注解的最终通知会在@AfterThrowing异常通知和@AfterReturning后置通知之前执行。所以在选择使用注解配置的时候需注意,环绕通知则不存在此问题。