文章目录
Spring学习第三天,AOP
在实际的开发过程中经常会使用到事务,比如模拟一个银行转账系统,从a账户转账到b账户,如果在转账过程中出现问题则回滚,如果想使用这个功能则需要先创建一个事务类
然后给我们的业务层代码添加逻辑
在每个业务层方法执行前开启一个事务,执行完成之后提交事务,出现异常则回滚,这里只是对保存功能添加了事务功能,但是如果还有一个删除功能,还有一个修改功能,难道我们要一遍又一遍重复写这些代码吗?这里有一个非常严重的问题
动态代理回顾
java实现动态代理有两种方式:
1.基于接口
提供者:JDK 官方的 Proxy 类。
要求:被代理类最少实现一个接口。
2.基于子类
提供者:第三方的 CGLib,如果报 asmxxxx 异常,需要导入 asm.jar。
要求:被代理类不能用 final 修饰的类(最终类)。
基于接口
这里使用一个销售商的例子
注意这里调用被代理对象方法的时候也就是method.invoke后面money乘上0.8的时候后面要加上f否则系统无法识别出来这是float变量。
运行结果:
这里可以看到我们模拟了一个生产厂商,一个消费者,我们生产厂商拿到钱之后就会给一台电脑,消费者后来只能从销售商处拿电脑,我们对生产者的方法做了增强,如果买电脑就会通过销售商处拿,给销售商1000元,这样销售商就会抽取掉百分之20,生产厂商只能拿到800元,这里使用的是java自带的proxy类的newProxyInstance()方法去获取一个销售商,销售商就是实现了生产商接口的一个实现类,这里前面两个参数是传入被代理对象的字节码和字节码数组,有固定的写法,最后一个参数则是主体,是我们提供增强代码的地方,通常是使用匿名内部类,其中的invoke方法中proxy代表对象本体,不常用,Method代表当前调用的类,args代表方法中的参数,可以看到增强方法中我们将参数取出来之后进行一定的改进之后传入被代理对象之中,然后将增强过后的对象返回。
基于子类
基于子类的动态代理首先要导入一个第三方库:cglib。
可以看到基本上和接口用法一模一样,只不过不再需要producer接口,同时被代理的对象不能是最终类即可。
利用动态代理解决之前的银行转账事务问题
service
dao
测试代码
可以看到这时候在service中使用int i=1/0;代表在转账过程中出现了错误。这时候运行会发现,因为没有事务的支持,在第一个账户减少钱之后,第二个账户却没有增加钱,这是非常严重的错误。
因此使用动态代理对整个方法进行增强使得其在银行转账的时候先开启一个事务。完整通过之后再提交事务,否则回滚.
将代码增强:
import Util.TransactionManager;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.IUserService;
import service.impl.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class test {
private TransactionManager transactionManager;
private IUserService service;
private ApplicationContext context;
@Before
public void init() {
context = new ClassPathXmlApplicationContext("bean.xml");
service = (IUserService) context.getBean("userServiceImpl");
transactionManager = (TransactionManager) context.getBean("transactionManager");
}
@Test
public void test1() {
service = (IUserService) Proxy.newProxyInstance(service.getClass().getClassLoader(), service.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtObject = null;
try {
transactionManager.beginTransaction();
method.invoke(service, args);
transactionManager.commit();
} catch (Exception e) {
transactionManager.rollback();
} finally {
transactionManager.release();
}
return rtObject;
}
});
service.ZhuanZhang(1, 2, 100);
}
}
aop
实际上aop就是将我们需要使用的重复代码注入到我们想要注入的位置,比如此处的事务处理,我们就可以利用aop在我们所有的数据库交互的代码前开启事务,在没有问题之后提交事务,出现异常后回滚,通过aop我们就不用手动重复写代码了。
aop中的相关术语
连接点时被代理对象中所有的方法都叫连接点
切入点指的是被增强的方法,如果有方法没被增强,则它是连接点,但不是切入点。
通知就是我们要注入的代码,上述银行转账例子中就是开启事务,提交事务,回滚事务等代码
引介现阶段使用不到
目标对象就是被代理的对象
织入是指给原本没有增强功能的对象进行增强的过程叫做织入
代理就是经过增强之后返回的对象
切面就是建立切入点和增强方法之间关系的这个过程。
利用aop解决银行转账问题
准备工具类
分别准备三个工具类:
ConnectionUtils
TransactionManager
BeanFactory
这个类主要用于将当前线程和一个数据库连接绑定
package Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
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();
}
}
这个类主要作用就是封装出我们需要的开启线程,提交事务,回滚事务,以及释放连接等操作,值得注意的是因为后期我们开发是基于tomcat的,在tomcat中一个线程close之后,线程并非真的消失了,而是归还给线程池,数据库连接也是类似的,因此我们在关闭连接的时候,连接也只是归还给线程池,并没有和线程解绑,相同的线程可能被再次利用,但是下次这个线程中可以拿到一个连接,但是这个连接已经不能使用了,所以在关闭连接的时候要调用remove方法,将连接和线程解绑。
package Util;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
public class TransactionManager {
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 开启事务
*/
public void beginTransaction() {
try {
System.out.println(connectionUtils);
connectionUtils.getThreadConnection().setAutoCommit(false);
System.out.println("前置通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit() {
try {
connectionUtils.getThreadConnection().commit();
System.out.println("后置通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback() {
try {
connectionUtils.getThreadConnection().rollback();
System.out.println("异常通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void release() {
try {
connectionUtils.getThreadConnection().close();//还回连接池中
connectionUtils.removeConnection();
System.out.println("最终通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
}
package Util;
import service.IUserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class BeanFactory {
static TransactionManager transactionManager;
static IUserService userService;
public TransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(TransactionManager transactionManager) {
BeanFactory.transactionManager = transactionManager;
}
public void setUserService(IUserService UserService) {
BeanFactory.userService = UserService;
}
这个类主要用于返回一个增强过后的代理对象,我们将增强的代码写在这个类的方法中。
/**
* 创建账户业务层实现类的代理对象
* @return
*/
public static IUserService getUserService() {
IUserService proxyUserService = (IUserService)
Proxy.newProxyInstance(userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),new
InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
Object rtValue = null;
try {
//开启事务
transactionManager.beginTransaction();
//执行业务层方法
rtValue = method.invoke(userService, args);
//提交事务
transactionManager.commit();
}catch(Exception e) {
//回滚事务
transactionManager.rollback();
e.printStackTrace();
}finally {
//释放资源
transactionManager.release();
}
return rtValue; }
});
return proxyUserService; } }
准备Dao service test Utils 代码
这部分沿用之前的即可
导入依赖
这两个是属于aop的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
给配置文件导入约束
<?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">
</beans>
先配置ioc
<!--配置业务层-->
<bean id="userService" class="service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<!--配置持久层-->
<bean id="userDao" class="dao.impl.UserDaoImpl">
<property name="queryRunner" ref="queryRunner"></property>
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置queryRunner-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
<constructor-arg ref="datasource"></constructor-arg>
</bean>
<!--配置datasource-->
<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"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!--配置connectionUtils-->
<bean id="connectionUtils" class="Util.ConnectionUtils">
<property name="dataSource" ref="datasource"></property>
</bean>
配置aop
<!--配置transactionManager-->
<bean id="tsManager" class="Util.TransactionManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置aop-->
<aop:config >
<aop:pointcut id="p1" expression="execution(* service.impl.*.*(..))"/>
<aop:aspect ref="tsManager" id="tsAdvice">
<aop:before method="beginTransaction" pointcut-ref="p1"></aop:before>
<aop:after-returning method="commit" pointcut-ref="p1"></aop:after-returning>
<aop:after-throwing method="rollback" pointcut-ref="p1"></aop:after-throwing>
<aop:after method="release" pointcut-ref="p1"></aop:after>
</aop:aspect>
</aop:config>
这里的transactionManager是抽取出来的公共代码通过该类的方法去对事务进行操作
aop:config是用来表示下面的配置是aop
aop:aspect是用来配置切面的,应该先在外部配置好有公共代码的类,然后引用进来
aop:pointcut表示切入点的表达式,应该写在所有的切面配置之前,另外这里的表达式有相应的写法,后面给出详细内容
aop:before是前置通知,会在切入点之前执行
aop:after-returning是后置通知,会在切入点之后执行
aop:after-throwing是异常通知,会在切入点方法发生异常之后执行
aop:after是最终通知,会在一切执行完毕之后执行
切入点表达式说明
环绕通知
环绕通知说白了就是自己手写出来各个通知的位置,然后调用,这里就不是用method.invoke去调用被代理对象的方法了,而是使用spring为我们提供的对象的方法,proceed去调用被代理对象的方法,参数测试使用getArgs()方法去获取,另外环绕通知经常用于注解开发aop的时候,因为注解开发aop的时候通知执行的顺序有错误。需要自己手动写,这个环绕通知的方法应该写在增强类的类体中
基于注解的aop
配置pom文件
和之前使用xml配置相同,也要配置这两个aop的基本依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
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
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">
<!-- 添加扫描的包-->
<context:component-scan base-package="Util"></context:component-scan>
<context:component-scan base-package="service"></context:component-scan>
<context:component-scan base-package="dao"></context:component-scan>
<!--配置queryRunner-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
<constructor-arg ref="datasource" name="ds"></constructor-arg>
</bean>
<!--配置datasource-->
<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"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
</beans>
只需要配置好需要扫描的包以及queryRunner以及dataSource即可,剩下的就按照注解开发进行。
dao,service,connectionUtils
在持久层和业务层以及工具类上添加相应的注入注解
事务管理类代码
可以看到我们只需要将对应的通知方法前加上相应的通知属性注解即可
这里有两点需要注意:
1.要在这个类之前加上@Aspect让系统清楚这是一个切面类
2.@Pointcut利用该注解去设置我们要增强的方法。其他通知方法就可以使用该简写方法去指定要增强的方法,记住一定要加上()
3.利用注解方法去设置通知方法会有异常,通知的顺序会出错,出现异常会先执行最终通知再去执行异常通知,因此如果使用注解方法,对于上述的事务处理实际上是无法完成的,因此如果非要使用注解开发,建议使用环绕通知,让代码的执行顺序可控
package Util;
import org.apache.commons.dbutils.QueryRunner;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("txManager")
@Aspect
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
@Pointcut("execution(* service.impl.*.*(..))")
private void pt(){}
/**
* 开启事务
*/
@Before("pt()")
public void beginTransaction() {
try {
System.out.println(connectionUtils);
connectionUtils.getThreadConnection().setAutoCommit(false);
System.out.println("前置通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
@AfterReturning("pt()")
public void commit() {
try {
connectionUtils.getThreadConnection().commit();
System.out.println("后置通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
@AfterThrowing("pt()")
public void rollback() {
try {
connectionUtils.getThreadConnection().rollback();
System.out.println("异常通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
@After("pt()")
public void release() {
try {
connectionUtils.getThreadConnection().close();//还回连接池中
connectionUtils.removeConnection();
System.out.println("最终通知已经开启");
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用环绕通知的方法
在事务管理类中加入如下代码
使用@Around()注解说明该方法时环绕通知的方法,然后将想要的代码顺序写在其中。此时我们的银行转账问题可以正常解决了。
@Around("pt()")
public Object transactionAround(ProceedingJoinPoint pt){
Object rtObject=null;
try {
Object[] args=pt.getArgs();
beginTransaction();
pt.proceed(args);
commit();
} catch (Throwable throwable) {
rollback();
throwable.printStackTrace();
}
finally {
release();
}
return rtObject;
}