Spring入门学习二
一、AOP的引入
1.转账问题
情景
在不开启事务的情况下,直接使用如下的转账方法,如果遇到转账中途出现错误可能会导致一个账号上余额减少但另一个账号余额并未增加的情况。
public int transfer(String sourceName, String targetName, Double money) {
try{
Account a1=accountDao.getAccountByName(sourceName);
Account a2=accountDao.getAccountByName(targetName);
int res=0;
if(a1!=null&&a2!=null){
a1.setMoney(a1.getMoney()-money);
a2.setMoney(a2.getMoney()+money);
res+=accountDao.updateAccount(a1);
//int i=1/0;
res+=accountDao.updateAccount(a2);
}
return res;
}catch (Exception e){
e.printStackTrace();
return 0;
}finally {
}
}
解决方案一
直接对该方法进行修改,引入事务。每次转账操作都会拥有一个独立的线程,因此我们可以使用ThreadLocal
对象存储当前使用的连接,以便开启该连接的事务。这里我创建了一个ConnectionUtils工具类负责存储连接,TxManager类负责对连接进行关闭自动提交、提交事务、回顾事务和关闭连接的操作。
package com.xxbb.util;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 链接的工具类,用于从数据源获取一个链接,并实现和线程的绑定
* @author xxbb
*/
@Component("connectionUtils")
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<>();
@Resource(name = "dataSource")
private DataSource dataSource;
public Connection getThreadConnection(){
Connection conn=tl.get();
if(conn==null){
try {
conn=dataSource.getConnection();
tl.set(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
return conn;
}
/**
* 释放当前线程的连接
*/
public void removeConnection(){
tl.remove();
}
}
package com.xxbb.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 事务工具类 包含了,开启事务,提交事务,回滚事务,释放连接
* @author xxbb
*/
@Component("txManager")
@Aspect
public class TxManager {
@Resource(name="connectionUtils")
private ConnectionUtils connectionUtils;
/**
* 开启事务
*/
public void beginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit(){
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
// @AfterThrowing("pt1()")
public void rollback(){
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 释放事务
*/
// @After("pt1()")
public void release(){
try {
connectionUtils.getThreadConnection().close();
connectionUtils.removeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public int transfer(String sourceName, String targetName, Double money) {
try{
//1.开启事务
transactionManager.beginTransaction();
//2.执行操作
Account a1=accountDao.getAccountByName(sourceName);
Account a2=accountDao.getAccountByName(targetName);
int res=0;
if(a1!=null&&a2!=null){
a1.setMoney(a1.getMoney()-money);
a2.setMoney(a2.getMoney()+money);
res+=accountDao.updateAccount(a1);
//int i=1/0;
res+=accountDao.updateAccount(a2);
}
//3.提交事务
transactionManager.commit();
//4.返回结果
return res;
}catch (Exception e){
e.printStackTrace();
//5.回滚操作
transactionManager.rollback();
return 0;
}finally {
//6.释放资源
transactionManager.release();
}
}
当转账业务出现了错误能够回滚,业务执行完成后连接也会关闭,没有错误则正常执行转账。我们可以将此种方式应用在所有与数据库有关的业务中,但是TxManager类中的方法一旦发生了改变,涉及到其中方法的所有类都需要进行调整,同时代码也存在很多冗余。 为了解决这种问题,我们希望能够将这些事务控制的代码抽取出来,这里我们可以采用动态代理设计模式对代码进行优化。
解决方案二
1.基于接口的动态代理:
- 特点:字节码随用随创建,随用随加载。
- 涉及的类:Proxy
- 提供者:JDK官方
- 创建代理对象:使用Proxy类中的newProxyInstance方法
- 创建对象要求:被代理对象至少实现了一个接口类
- newProxyInstance方法的参数:
- ClassLoader:类加载器。加载代理对象类的字节码,和被代理的对象类使用相同的类加载器
- Class[]:字节码数组。用于让代理对象和被代理对象有相同的方法
- InvocationHandler:提供增强代码。代理的内容,一般是写一个接口的匿名内部类
示例:
public static void main(String[] args) {
AccountService accountService=new AccountServiceImpl();
AccountService proxyAccountService=(AccountService) Proxy.newProxyInstance(AccountService.class.getClassLoader(),AccountService.class.getInterfaces(),
/**
* 方法参数含义:
* proxy:代理对象的引用
* method:当前执行的方法
* args 当前执行方法所需要的参数
* return: 和代理对象的返回值相同
*/
(proxy, method, arg)->{
//任何被代理对象的方法都会经过该方法
//提供增强方法
Object returnValue=null;
try {
//1.开启事务
transactionManager.beginTransaction();
returnValue=method.invoke(accountService,arg);
//3.提交事务
transactionManager.commit();
return returnValue;
}catch (Exception e){
e.printStackTrace();
//5.回滚操作
transactionManager.rollback();
return returnValue;
}finally {
//6.释放资源
transactionManager.release();
}
});
}
在进行业务操作时我们实际使用的是AccountService的代理对象proxyAccountService,AccountService对象的所有方法都会通过代理而获得事务控制的功能
2.基于子类的动态代理
- 特点:字节码随用随创建,随用随加载
- 作用:不修改源码的基础上对方法增强
- 涉及的类:Enhance
- 提供者:cglib库
- 创建代理对象:使用Enhance的creat方法
- 创建对象要求:被代理类不可为final
- create方法的参数:
- Class:指定被代理对象的字节码文件
- callback:回调接口。这里一般使用他的实现类,MethodInterceptor
示例:
public static void main(String[] args) {
AccountService accountService=new AccountServiceImpl();
AccountService proxyAccountService=(AccountServiceImpl) Enhancer.create(accountService.getClass(), new MethodInterceptor() {
/**
*
* @param o 被代理对象的引用
* @param method 当前执行的方法
* @param objects 当前方法需要的参数
* @param methodProxy 当前执行方法的代理对象
* @return 和代理对象的返回值相同
* @throws Throwable s
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//任何被代理对象的方法都会经过该方法
//提供增强方法
Object returnValue=null;
try {
//1.开启事务
transactionManager.beginTransaction();
returnValue=method.invoke(accountService,objects);
//3.提交事务
transactionManager.commit();
return returnValue;
}catch (Exception e){
e.printStackTrace();
//5.回滚操作
transactionManager.rollback();
return returnValue;
}finally {
//6.释放资源
transactionManager.release();
}
});
}
2.转账问题的解决方案,SpringAOP的引入
除了使用上述动态代理的方法对原有方法进行增强外,Spring也为我们提供了对方法进行增强的功能——SpringAOP
1.AOP概念
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2.AOP相关名词
- Join point(连接点) :所谓连接点是指那些被拦截到的点。在spring中, 这些点指的是方法, 因为spring只支持方法类型的连接点。
- Pointe ut(切入点) :所谓切入点是指我们要对哪些Join point进行拦截的定义。
- Advice(通知/增强) :所谓通知是指拦截到Join point之后所要做的事情就是通知。
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。 - Introduction(引介) :引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。
- Target(目标对象) :代理的目标对象。
- Weaving(织入) :是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入, 而AspectJ采用编译期织入和类装载期织入。
- Proxy(代理) :一个类被AOP织入增强后, 就产生一个结果代理类。
- Aspect(切面) :是切入点和通知(引介)的结合。
前置通知、后置通知、异常通知、最终通知:
3.基于XML的AOP配置
准备工作:
- 1、导入aspectjweaver.jar、spring-aop.jar这两个jar包。
- 2、在bean.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">
</bean>
配置步骤:
- 1、把通知Bean也交给spring来管理
- 2、使用aop:config标签表明开始AOP的配置
- 3、使用aop:aspect标签表明配置切面
- id属性:是给切面提供唯一标识
- ref属性:是指定通知类bean的Id。
- 4、在
aop:aspect id="" ref=""></aop:aspect>
标签的内部使用对应标签来配置通知的类型 。<aop:before method="" pointcut=""/>
前置通知<aop:after-returning method="" pointcut=""/>
后置通知<aop:after-throwing method="" pointcut=""/>
异常通知<aop:after method="" pointcut=""/>
最终通知通知<aop:around method="" pointcut=""/>
最终通知通知<aop:pointcut id="" expression=""/>
切入点设置 ,id表示一个唯一标识,expression内填写切入点表达式
- 5、内部配置的method属性表示通知bean对象的对应方法,pointcout属性表示切入点表达式
- 6、切入点表达式写法:
- 关键字:execution
- 表达式:访问修饰符 返回值 包名.类名.方法名(参数)
- 全通配写法:访问修饰符可以省略(切入点无法拦截私有方法),其余都可以用*表示。参数列表可以直接写基本数据类型(如int)或者引用类型使用包名.类名的方式(java.lang.String/com.xxbb.po.Account),也可以直接使用. .代替。
全通配写法: * *..*.*(..)
在实际开发中通常是要精确到业务层的实现类: * com.xxbb.service.AccountServiceImpl.*(..)
实际应用:
创建一个Logger类为作为业务层的通知
package com.xxbb.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author xxbb
*/
@Component("logger")
public class Logger {
public void pt1(){}
/**
* 计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
* 前置通知
*/
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方法开始记录日志了");
}
/**
* 环绕通知
* @return
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object returnValue=null;
try {
//得到方法执行需要的参数
Object[] args=pjp.getArgs();
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了---前置");
//明确业务方法的切入带你
returnValue=pjp.proceed(args);
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了--后置");
return returnValue;
} catch (Throwable throwable) {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了--异常");
throw new RuntimeException(throwable);
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了--最终");
}
}
}
配置bean.xml
<context:component-scan base-package="com.xxbb"/>
<aop:aspectj-autoproxy/>
<aop:config >
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="beforePrintLog"
pointcut="execution(* com.xxbb.service.impl.*.*(..))"/>
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* com.xxbb.service.impl.*.*(..))"/>
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* com.xxbb.service.impl.*.*(..))"/>
<aop:after method="afterPrintLog" pointcut="execution(* com.xxbb.service.impl.*.*(..))"/>
<aop:around method="aroundPrintLog" pointcut-ref="logAdv"/>
<aop:pointcut id="logAdv" expression="execution(* *com.xxbb.service.impl.*.*(..))"/>
</aop:aspect>
</aop:config>
测试类
@Autowired
private IProducer producer;
@Test
public void test(){
producer.saleProduct(1000.0);
}
结果:
由于我这里通过时配置了环绕通知,所以会有两次日志答打印
4.基于注解的AOP配置
1.在bean.xml开启对aop注解的扫描,这里Spring会自动使用之前导入aspectjweaver.jar中的注解功能扫描类中的@Aspect
注解
<aop:aspectj-autoproxy/>
2.在我们需要使用的通知类上添加@Aspect
,在类中方法添加对应标签,示例如下
package com.xxbb.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author xxbb
*/
@Component("logger")
@Aspect
public class Logger {
//可以先配置一个切入点,
@Pointcut("execution(* com.xxbb.service.impl.*.*(..))")
public 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方法开始记录日志了");
}
/**
* 环绕通知,不适用已设置的切入点表达式,自己设置切入点表达式
* @return
*/
@Around("execution(* com.xxbb.service.impl.*.*(..))")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object returnValue=null;
try {
//得到方法执行需要的参数
Object[] args=pjp.getArgs();
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了---前置");
//明确业务方法的切入带你
returnValue=pjp.proceed(args);
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了--后置");
return returnValue;
} catch (Throwable throwable) {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了--异常");
throw new RuntimeException(throwable);
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了--最终");
}
}
}
在使用注解方式配置通知后,执行代码会出现执行完前置通知后先执行最终通知,再执行后置通知的情况。
因此使用注解配置最好使用环绕通知,环绕通知的实现方法类似于之前提到的动态代理,在环绕通知方法中传入了一个ProceedingJoinPoint 的接口引用,该接口有一个方法proceed
,作用可以参照在动态代理中的method.invoke
方法。
注意:1.只有被Spring管理的bean对象才会被通知增强
2.使用自动注入时,被注入的接口实现类对象的类型一定要是接口类型,不然会报错,如下
@Resource(name="accountService")
private AccountServiceImpl as;
org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘com.xxbb.test.aopTest’: Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘accountService’ is expected to be of type ‘com.xxbb.service.impl.AccountServiceImpl’ but was actually of type ‘com.sun.proxy.$Proxy37’
5.全注解配置
创建一个配置类,注解可参照IoC的全注解配置,添加@EnableAspectJAutoProxy
注解,其余注解如上述。
二、JDBC Tamplate
使用Spring的事务控制的最基本的JDBC模板,需要导入spring-jdbc包。使用方式和dbutils基本相同。
query("select * from t_account", new BeanPropertyRowMapper<>(Account.class));
update("update t_account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
结果集处理的的接口实现类为BeanPropertyRowMapper
,我们也可以继承RowMapper自定义结果集的处理
三、事务控制
1.Spring的事务控制
JavaEE体系进行分层开发,事务处理位于业务层,Spring提供了分层设计业务层的事务处理解决方案。Spring的事务控制都是基于AOP的,既可以通过配置方式实现,也可以通过编程方式实现。
PlatformTransactionManager
是Spring的事务控制器接口,提供了操作事务的方法
- TransactionStatus getTransaction(TransacionDefinition definition)——获取事务的状态信息
- void commit(TransactionStatus status)——提交事务
- void rollback(TransactionStatus status)——回滚事务
在实际开发中一般是使用它的实现类
- org.springframework.jdbc.datasource.DataSource TransactionManager——使用SpringJDBC或iBatis进行持久化数据时使用
- org.springframework.or m.hibernate 5.Hibernate TransactionManager——使用Hibernate版本进行持久化数据时使用
TransactionDefinition
定义事务的信息对象,有如下方法
- String getName()——获取事务对象名称
- int getlsolationLevel()——获取事务隔离级别
- int getPropagationBehavior()——获取事务传播行为
- int getTimeout()——获取事务超时时间
- boolean isReadOnly()——获取事务是否只读
TransactionStatus
描述某个时间点上事务对象的状态信息,包含六个集体的操作
- void flush()——刷新事务
- boolean has Savepoint()——获取是否是否存在存储点
- boolean is Completed()——获取事务是否完成
- boolean is NewTransaction()——获取事务是否为新的事务
- boolean is Rollback Only()——获取事务是否回滚设置事务回滚
2.事务的隔离级别
事务隔离级反映事务提交并发访问时的处理态度,默认使用数据的隔离级别。
- ISOLATION_DEFAULT———默认级别,归属下列某一种
- ISOLATION_READ_UNCOMMITTED——可以读取未提交数据
- ISOLATION_READ_COMMITTED——只能读取已提交数据, 解决脏读问题(Oracle默认级别)
- ISOLATION_REPEATABLE_READ——是否读取其他事务提交修改后的数据,解决不可重复读问题(MySQL默认级别)
- ISOLATION_SERIALIZABLE——是否读取其他事务提交添加后的数据,解决幻影读问题
3.事务的传播行为
- REQUIRED:如果当前没有事务, 就新建一个事务, 如果已经存在一个事务中, 加入到这个事务中。一般的选
择(默认值) - SUPPORTS:支持当前事务, 如果当前没有事务, 就以非事务方式执行(没有事务)
- MANDATORY:使用当前的事务, 如果当前没有事务, 就抛出异常
- REQUE RS NEW:新建事务, 如果当前在事务中, 把当前事务挂起。
- NOT_SUPPORTED:以非事务方式执行操作, 如果当前存在事务, 就把当前事务挂起
- NEVER:以非事务方式运行, 如果当前存在事务, 抛出异常
- NESTED:如果当前存在事务, 则在嵌套事务内执行。如果当前没有事务, 则执行REQUIRED类似的操作。
4.基于xml配置事务
配置步骤
- 1.配置事务管理管理器
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 2.配置事务的通知
- 导入约束
xmlns:tx="http://www.springframework.org/schema/tx"
配置标签:
- 3.配置
< tx:advice >
标签
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--配置事务的属性-->
<tx:attributes>
<!--配置需要拦截的方法
属性:
issolation="" 用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的哦默认隔离级别,即可重复度
propagation="" 用于指定事务的传播行为。默认值是REUQIRED
read-only="" 用于指定事务是否只读,默认false,表示读写
rollback-for="" 用于指定一个异常,当产生改异常,事务回滚,产生其他异常,事务不会滚。没有默认值时,表示任何异常都回滚
no-rollback-for="" 用于指定一个异常,当产生改异常,事务不回滚,产生其他异常,事务回滚。没有默认值时,表示任何异常都回滚
timeout="" 用于指定事务的超时时间,默认值是-1,表示永不超时。指定数值时单位为:秒
-->
<tx:method name="*" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>
- 4.建立AOP中的通用切入段表达式
- 5.建立事务通知和切入点表达式的对应关系
<!--配置AOP-->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.xxbb.service.impl.OldAccountServiceImpl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/>
</aop:config>
由于这里使用的的是DataSourceTransactionManager
,所以支持对SpringJDBC和MyBatis的事务管理。
5.基于注解配置事务
在需要进行事务控制的类或者方法上添加@Transactional
注解,注解内可和标签< tx:method >
一样配置属性。
6.全注解配置事务
在配置类上添加@EnableTransactionManagement
注解。
7.编程式事务控制
类似于模板方法,Spring提供了一个已经完成了事务控制的方法,需要我们填写具体的业务操作。
首先在bean.xml或配置类中声明一个org.springframework.transaction.support.TransactionTemplate
对象,然后在具体的业务中使用该对象的execute方法。
@Override
public int transferTemplate (String sourceName, String targetName, Double money){
return transactionTemplate.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus transactionStatus) {
try {
System.out.println("transferTemplate模板方法执行!!!");
Account a1 = accountDao.getAccountByName(sourceName);
Account a2 = accountDao.getAccountByName(targetName);
int res = 0;
if (a1 != null && a2 != null) {
a1.setMoney(a1.getMoney() - money);
a2.setMoney(a2.getMoney() + money);
res += accountDao.updateAccount(a1);
//int i=1/0;
//int i=1/0; 测试事务
res += accountDao.updateAccount(a2);
}
return res;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
我们所填写的代码即为重写doInTransaction()方法
8.注意
- private、final、static 方法无法添加事务管理
- 当绕过代理对象,直接调用添加事务管理的方法时,事务管理将无法生效