文章目录
1.事务问题
假如有如下持久层转账功能代码:
public void transfer(String sourceName, String targetName, float money) {
//根据名称查询转出账户
Account scourseAccount = accountDao.findAccountByName(sourceName);
//根据名称查询转入账户
Account targetAccount = accountDao.findAccountByName(targetName);
//转出账户减钱
scourseAccount.setMoney(scourseAccount.getMoney() - money);
//转入账户加钱
targetAccount.setMoney(targetAccount.getMoney() + money);
//更新转出账户
accountDao.updateAccount(scourseAccount);
//更新转入账户
accountDao.updateAccount(targetAccount);
}
看似没有问题,但加入上述的步骤任意位置出现错误,都有可能导致,一方转出钱,另一方未收到钱等各种各样的问题,这是因为上述操作不是一个事务,出现问题不会回滚到转账之前。
为了解决这个问题,需要让这些步骤都在同一个事务中,也就是用一个connection一次提交(上述步骤,用了四个connection提交了四次),要么一起成功,要么一起失败。
具体的做法是,使用ThreadLocal对象把Connection和当前线程绑定,从而使一个线程只有一个能控制事务的对象。
首先,创建一个工具类,给当前线程创建一个Connection。
/**
* 连接工具类,用于从数据源获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtil {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
//需要spring注入
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* @return
*/
public Connection getConnection(){
//线程ThreadLocal上获取
Connection conn = tl.get();
//判断当前线程上是否有连接
try {
if(conn == null){
//从数据源中获取一个连接,并且存入ThreadLocal中
conn = dataSource.getConnection();
//把conn存入ThreadLocal
tl.set(conn);
}
//返回当前线程上的连接
return conn;
} catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* 线程和连接解绑
*/
public void removeConnection(){
tl.remove();
}
}
tomcat拥有线程池,当一个线程使用完毕后,会还到线程池中。如果使用完一个连接就将它返还到连接池中,虽然连接还在线程中,但是连接已经不能用了,所以需要将线程和连接解绑。
事务操作工具类:
/**
* 和事务管理相关的工具类,包含了开启事务,提交事务,回滚事务,释放连接
*/
public class TransactionManager {
private ConnectionUtil connectionUtil;
/**
* 由spring注入数据
* @param connectionUtil
*/
public void setConnectionUtil(ConnectionUtil connectionUtil) {
this.connectionUtil = connectionUtil;
}
/**
* 开启事务
*/
public void beginTransaction(){
try {
//设置自动提交为false
connectionUtil.getConnection().setAutoCommit(false);
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit(){
try {
connectionUtil.getConnection().commit();
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback(){
try {
connectionUtil.getConnection().rollback();
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void release(){
try {
connectionUtil.getConnection().close();//还回池中
connectionUtil.removeConnection();//解绑
} catch (Exception e){
e.printStackTrace();
}
}
}
添加事务操作的代码:
public void transfer(String sourceName, String targetName, float money) {
try{
//开启事务
txManager.beginTransaction();
//执行操作
//根据名称查询转出账户
Account scourseAccount = accountDao.findAccountByName(sourceName);
//根据名称查询转入账户
Account targetAccount = accountDao.findAccountByName(targetName);
//转出账户减钱
scourseAccount.setMoney(scourseAccount.getMoney() - money);
//转入账户加钱
targetAccount.setMoney(targetAccount.getMoney() + money);
//更新转出账户
accountDao.updateAccount(scourseAccount);
//更新转入账户
accountDao.updateAccount(targetAccount);
//提交事务
txManager.commit();
}catch (Exception e){
e.printStackTrace();
//回滚操作
txManager.rollback();
}finally {
//释放连接
txManager.release();
}
}
在bean.xml中不需要配置QueryRunner的数据源,在业务层转账操作中使用我们的connectionUtil:
public Account findAccountByName(String accountName) {
try {
List<Account> accounts = runner.query(connectionUtil.getConnection(),"select * from account where name = ?", new BeanListHandler<Account>(Account.class),accountName);
if(accounts == null || accounts.size() == 0)
return null;
else if(accounts.size() > 1)
throw new RuntimeException("结果集不唯一");
else
return accounts.get(0);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
2.动态代理
上面的例子中,我们虽然解决了事务控制的问题,但是也因此多了很多的配置以及重复代码,那么如何解决这些问题?这就需要用到动态代理。
动态代理:
- 特点:字节码随用随创建,随用随加载。
- 作用:不修改源码的基础上对方法增强。
- 分类:基于接口;基于子类;
2.1 基于接口的动态代理
- 涉及的类:Proxy
- 提供者:JDK官方
如何创建代理对象:
- 使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
- 被代理类最少实现一个接口,如果没有则不能使用
newProxyInstance方法的参数:
参数 | 作用 |
---|---|
ClassLoader | 类加载器 用于加载代理对象字节码的,和被代理对象使用相同的类加载器。 固定写法 |
Class[] | 字节码数组 用于让代理对象和被代理对象有相同的方法 固定写法 |
InvocationHandler | 用于提供增强的代码 一般是写一个该接口的匿名实现类 谁用谁写 |
例子:
IActor proxyActor = (IActor) Proxy.newProxyInstance(
actor.getClass().getClassLoader(),
actor.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 执行被代理对象的任何方法,都会经过该方法。
* 此方法有拦截的功能。
*
* 参数:
* proxy:代理对象的引用。不一定每次都用得到
* method:当前执行的方法对象
* args:执行方法所需的参数
* 返回值:
* 当前执行方法的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args){
//获取执行方法的名字
String name = method.getName();
//后去方法的参数
Float money = (Float) args[0];
Object rtValue = null;
//根据不同的方法,执行不同的操作
if("basicAct".equals(name)){
if(money > 2000){
//看上去剧组是给了 8000,实际到演员手里只有 4000
//这就是我们没有修改原来 basicAct 方法源码,对方法进行了增强
rtValue = method.invoke(actor, money/2);
}
}
return rtValue;
}
});
//通过动态代理执行方法
proxyActor.basicAct(8000f);
2.2 基于子类的动态代理
需要第三方jar包支持:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
- 设计的类:Enhancer
- 提供者:第三方cglib库
如何创建代理对象:
- 使用Enhancer类中的create方法
创建代理对象的要求:
- 被代理类不能是最终类
create方法的参数:
参数 | 作用 |
---|---|
Class | 字节码 用于指定被代理对象的字节码 |
Callback | 用于提供增强的代码 一般写的是该接口的子接口实现类:MethodInterceptor |
例子:
IActor cglibActor = (IActor) Enhancer.create(
actor.getClass().getClassLoader(),
new MethodInterceptor() {
/**
* 执行被代理对象的任何方法,都会经过该方法。
* 此方法有拦截的功能。
*
* 参数:
* proxy:代理对象的引用。不一定每次都用得到
* method:当前执行的方法对象
* args:执行方法所需的参数
* MethodProxy:当前执行方法的代理对象,不常用
* 返回值:
* 当前执行方法的返回值
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args,MethodProxy methodProxy) throws Throwable{
//获取执行方法的名字
String name = method.getName();
//后去方法的参数
Float money = (Float) args[0];
Object rtValue = null;
//根据不同的方法,执行不同的操作
if("basicAct".equals(name)){
if(money > 2000){
//看上去剧组是给了 8000,实际到演员手里只有 4000
//这就是我们没有修改原来 basicAct 方法源码,对方法进行了增强
rtValue = method.invoke(actor, money/2);
}
}
return rtValue;
}
});
//通过动态代理执行方法
cglibActor.basicAct(8000f);
3 使用动态代理实现事务控制
- 创建代理对象工厂
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService iAccountService;
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public void setiAccountService(IAccountService iAccountService) {
this.iAccountService = iAccountService;
}
/**
* 获取Service的代理对象
* @return
*/
public IAccountService getiAccountService(){
return (IAccountService)Proxy.newProxyInstance(iAccountService.getClass().getClassLoader(),
iAccountService.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 添加事务的支持
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try{
//开启事务
txManager.beginTransaction();
//执行操作
rtValue = method.invoke(iAccountService,args);
//提交事务
txManager.commit();
}catch (Exception e){
e.printStackTrace();
//回滚操作
txManager.rollback();
throw new RuntimeException("事务出错");
}finally {
//释放连接
txManager.release();
}
return rtValue;
}
});
}
}
- 配置beanFactory、代理的Service
<!-- 配置代理的Service-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getiAccountService"></bean>
<!-- 配置beanFactory-->
<bean id="beanFactory" class="com.wxy.factory.BeanFactory">
<property name="iAccountService" ref="accountService"></property>
<property name="txManager" ref="txManager"></property>
</bean>
<!-- 配置Service -->
<bean id="accountService" class="com.wxy.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"></property>
<!-- 将AccountServiceImpl删除,也要将配置的txManager删除 -->
</bean>
- 测试
转账出错
没有出现错误修改:
4 AOP
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
作用:
- 在程序运行期间,不修改源码对已有方法进行增强。
优势:
- 减少重复代码
- 提高开发效率
- 维护方便
实现方式:
- 使用动态代理(上面的案例就是AOP的一个实现案例)
4.1 Spring 中的AOP
4.1.1 AOP相关术语
名称 | 解释 |
---|---|
Joinpoint(连接点) | 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。 也就是业务层所有的方法都是连接点 |
Pointcut(切入点) | 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。 也就是业务层中需要增强的方法 |
Advice(通知/增强) | 所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知(整个invoke方法执行就是环绕通知,其中有明确的切入点方法调用)。 |
Introduction(引介) | 引介是一种特殊的通知,在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。 |
Target(目标对象) | 代理的目标对象。 |
Weaving(织入 ) | 是指把增强应用到目标对象来创建新的代理对象的过程。 spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。 也就是增强方法的过程叫做织入。 |
Proxy(代理) | 一个类被 AOP 织入增强后,就产生一个结果代理类。 |
Aspect(切面) | 是切入点和通知(引介)的结合。 |
4.1.2 spring基于xml的AOP配置
编写通知:
/**
* 用于记录日志的工具类,提供了公共的代码
*/
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方法开始记录日志。。。");
}
}
在bean.xml中配置aop:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置spring的ioc,把Service配置进来-->
<bean id="accountService" class="com.wxy.service.impl.AccountSetviceImpl"></bean>
<!-- spring中基于xml的aop配置步骤-->
<!-- 1.把通知bean也交给spring管理-->
<bean id="logger" class="com.wxy.utils.Logger"></bean>
<!-- 2.使用aop:config标签表名开始配置aop-->
<aop:config>
<!-- 3.使用aop:aspect标签表名配置切面-->
<aop:aspect id="logAdivce" ref="logger">
<!-- 4.使用对应标签来配置通知的类型-->
<!-- 切入点表达式的写法:
关键词:execution(表达式)
表达式:访问修饰符 返回值 包名.包名...类名.方法名(参数列表)
public void com.wxy.service.impl.AccountServiceImpl.saveAccount()
-->
<!-- 配置切入点表达式,写在aop:aspect里面只能此切面使用,写在外面(切面之前)则所有切面可用-->
<aop:pointcut id="pt1" expression="execution(* com.wxy.service.impl.*.*(..))"/>
<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>
</aop:aspect>
</aop:config>
</beans>
切入点表达式的写法
实际开发中的通常写法:
切到业务层实现类下的所有方法
/ * com.wxy.service.impl.* . *(…)
环绕通知:
<!-- 配置环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
问题:
- 当配置了环绕通知,切入点方法没有执行,而通知方法执行了。
分析:
- 原因是配置的环绕通知没有明确的切入点方法调用
解决:
- Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。该接口可作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
- 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
/**
* 环绕通知
*/
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
Object rtValue = null;
//明确调用切入点方法
try {
Object args[] = proceedingJoinPoint.getArgs();
System.out.println("前置通知");
rtValue = proceedingJoinPoint.proceed(args);
System.out.println("后置通知");
return rtValue;
} catch (Throwable throwable){
System.out.println("异常通知");
throw new RuntimeException(throwable);
} finally {
System.out.println("最终通知");
}
}
4.1.3 spring基于注解的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
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">
<!-- 创建容器要扫描的包-->
<context:component-scan base-package="com.wxy"></context:component-scan>
<!-- 配置spring开启注解aop的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
通知类:
注意,前四种通知和环绕通知不能同时使用。
/**
* 用于记录日志的工具类,提供了公共的代码
*/
@Component("logger")
@Aspect//表示当前类是一个切面
public class Logger {
@Pointcut("execution(* com.wxy.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方法开始记录日志。。。");
}
/**
* 环绕通知
*/
//@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
Object rtValue = null;
//明确调用切入点方法
try {
Object args[] = proceedingJoinPoint.getArgs();
System.out.println("前置通知");
rtValue = proceedingJoinPoint.proceed(args);
System.out.println("后置通知");
return rtValue;
} catch (Throwable throwable){
System.out.println("异常通知");
throw new RuntimeException(throwable);
} finally {
System.out.println("最终通知");
}
}
}