JavaEE 企业级分布式高级架构师(三)Spring学习笔记(2)

本文深入解析Spring框架中的AOP原理与应用,探讨AOP术语、实现方式及动态代理技术,包括JDK动态代理与CGLib动态代理。同时,讲解Spring整合Junit进行单元测试,以及Spring框架下事务管理的多种方式,如基于AspectJ的纯注解事务管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

核心高级篇

Spring AOP原理分析

什么是AOP?

  • AOP:面向切面编程(Aspect Oriented Programming)
  • 作用:在不修改目标类代码的前提下,可以通过AOP技术去增强目标类的功能。通过【预编译方式】和【运行期动态代理】实现程序功能统一维护的一种技术。
  • AOP是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构。
  • AOP最早由AOP联盟的组织提出,制定了一套规范。Spring将AOP思想引入到框架中,必须遵守AOP联盟的规范。
  • AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
  • 利用AOP可以对业务代码中【业务逻辑】和【系统逻辑】进行隔离,从而使得【业务逻辑】和【系统逻辑】之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。。

为什么使用AOP?

  • 作用:AOP采用横向抽取机制,补充了传统纵向继承体系(OOP)无法解决的重复性代码优化(性能监视、事务管理、安全检查、缓存),将业务逻辑和系统处理的代码(关闭连接、事务管理、操作日志记录)解耦。
  • 优势:重复性代码被抽取出来,维护更加方便
  • 纵向继承体系:
    在这里插入图片描述
  • 横向抽取机制:

在这里插入图片描述

AOP相关术语介绍

术语解释
  • Joinpoint:连接点,指的是那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点
  • Pointcut:切入点,指的是要对哪些Joinpoint进行拦截的定义
  • Advice:通知/增强,通知指的是拦截到Jointpoint之后所要做的事情就是通知。通知分为:前置通知、后置通知、异常通知、最终通知和环绕通知
  • Introduction:引介,是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法和Field
  • Target:目标对象,即代理的目标对象
  • Weaving:织入,是指把增强应用到目标对象来创建新的代理对象的过程
  • Proxy:代理,一个类被AOP增强后,就产生一个结果代理类
  • Aspect:切面,是切入点和通知的结合,
  • Advisor:通知器/顾问,与Aspect相似

图示说明:
在这里插入图片描述

AOP实现之AspectJ

  • AspectJ是一个Java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)
  • 可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言。更幸运的是,AspectJ与Java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。
  • 了解AspectJ应用到java代码的过程(这个过程称为织入),对于织入这个概念,可以简单理解为Aspect(切面)应用到目标函数(类)的过程。
  • 对于织入这个过程,一般分为动态织入和静态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术。
  • AspectJ采用的就是静态织入方式。AspectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把Aspect类编译成class字节码后,在java目标类编译时织入,即先编译Aspect类再编译目标类。
    在这里插入图片描述

AOP实现之Spring AOP

  • Spring AOP是通过动态代理技术实现的
  • 而动态代理是基于反射设计的
  • 动态代理技术的实现方式有两种:基于接口的JDK动态代理和基于继承的CGLib动态代理
实现原理分析

在这里插入图片描述

JDK动态代理
  • 代理类需要实现InvocationHandler接口,并且实现该接口的 invoke 方法。
  • 目标对象必须实现接口,代理类通过Proxy.newInstance(classLoader, interface, InvocationHandler)创建,代理对象主要通过InvocationHandler 的实现类去实现代理功能的。
  • 定义目标对象的接口和实现类:
public interface UserService {
	void saveUser() ;
}
public class UserServiceImpl implements UserService {
	@Override
	public void saveUser() {
		System.out.println("添加用户");
	}
}
  • JDK动态代理写法一
/**
 * 代理工具类
 */
public class MyProxyUtils {
	/**
	 * 使用JDK的方式生成代理对象
	 */
	public static UserService getProxy(final UserService service) throws Exception{
		// 使用Proxy类生成代理对象
		UserService userService = (UserService) Proxy.newProxyInstance(
				service.getClass().getClassLoader(), 
				service.getClass().getInterfaces(), 
				(proxy, method, args) -> { // InvocationHandler
					if("saveUser".equals(method.getName())){
						System.out.println("记录日志...");
						// 开启事务
					}
					// 提交事务
					// 让service类的saveUser或其他方法正常执行
					return method.invoke(service, args);
				});
		// 返回代理对象
		return userService;
	}
}
/**
 * 测试代码
 * @throws Exception
 */
@Test
public void testJDKDynamicProxy() throws Exception{
	UserService service = new UserServiceImpl();
	service.saveUser();
	System.out.println("================");
	service = MyProxyUtils.getProxy(service);
	service.saveUser();
}
// 测试结果输出:
// 添加用户
// ================
// 记录日志...
// 添加用户
  • JDK动态代理写法二
/**
 * 主要作用就是生成代理类 使用JDK的动态代理实现 它是基于接口实现的
 */
public class JDKProxyFactory implements InvocationHandler{
	// 目标对象的引用
	private Object target;
	// 通过构造方法将目标对象注入到代理对象中
	public JDKProxyFactory(Object target){
		super();
		this.target = target;
	}
	/**
	 * 获取代理对象
	 */
	public Object getProxy(){
		// 如何生成一个代理类呢?
		// 1、编写源文件(java文件)----目标类接口interface实现类(调用了目标对象的方法)
		// 2、编译源文件为class文件
		// 3、将class文件加载到JVM中(ClassLoader)
		// 4、将class文件对应的对象进行实例化(反射)
		
		// Proxy是JDK中的API类
		Object object = Proxy.newProxyInstance(
			target.getClass().getClassLoader(),  // 第一个参数:目标对象的类加载器
			target.getClass().getInterfaces(),   // 第二个参数:目标对象的接口
			this);								 // 第三个参数:代理对象的执行处理器
		return object;
	}
	/**
	 * 代理对象会执行的方法
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
		Method method2 = target.getClass().getMethod("saveUser", null);
		Method method3 = Class.forName("com.sun.proxy.$Proxy2").getMethod("saveUser", null);
		System.out.println("目标对象的方法:" + method2.toString());
		System.out.println("目标接口的方法:" + method.toString());
		System.out.println("代理对象的方法:" + method3.toString());
		System.out.println("这是jdk的代理方法");
		// 下面的代码,是反射中的API用法
		// 该行代码,实际调用的是[目标对象]的方法
		// 利用反射,调用[目标对象]的方法
		Object returnValue = method.invoke(target, args);
		
		//增强的部分
		// ...
		return returnValue;
	}
}
/**
 * 测试
 */
public class DynamicProxyTest {
	@Test
	public void testJDKProxy(){
		// 1、创建目标对象
		UserService service = new UserServiceImpl();
		// 2、生成代理对象
		JDKProxyFactory proxyFactory = new JDKProxyFactory(service);
		// 得到代理对象
		UserService proxy = (UserService) proxyFactory.getProxy();
		// 生成class文件
		generatorClass(proxy);
		
		// 3、调用目标对象的方法
		service.saveUser();
		System.out.println("===============");
		// 4、调用代理对象的方法
		proxy.saveUser();
	}
	private void generatorClass(Object proxy) {
		FileOutputStream out = null;
		try {
			// byte[] generateProxyClass =
			// ProxyGenerator.generateProxyClass(proxy.getClass().getName(), new Class[]
			// {proxy.getClass()});
			byte[] generateProxyClass = ProxyGenerator.generateProxyClass(proxy.getClass().getSimpleName(),
					new Class[] { proxy.getClass() });
			out = new FileOutputStream(proxy.getClass().getSimpleName() + ".class");
			out.write(generateProxyClass);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
				}
			}
		}
	}
}
// 测试结果输出:
// 添加用户
// ===============
// 目标对象的方法:public void com.yw.spring.dynamic_proxy.taregt.UserServiceImpl.saveUser()
// 目标接口的方法:public abstract void com.yw.spring.dynamic_proxy.taregt.UserService.saveUser()
// 代理对象的方法:public final void com.sun.proxy.$Proxy2.saveUser()
// 是jdk的代理方法
// 添加用户
CGLib动态代理
  • 目标对象不需要实现接口,底层是通过继承目标对象产生代理子对象(代理子对象中继承了目标对象的方法,并可以对该方法进行增强)。代理对象是子类,目标对象是父类。
  • CGLib动态代理的功能,主要需要实现MethodInterceptor接口,并实现该接口的 intercept 方法。
  • 写法一:
/**
 * 使用CGLIB的方式生成代理对象
 */
public static UserService getProxy(){
	// 创建CGLIB的核心类
	Enhancer enhancer = new Enhancer();
	// 设置父类
	enhancer.setSuperclass(UserServiceImpl.class);
	// 设置回调函数
	enhancer.setCallback(new MethodInterceptor() {
		@Override
		public Object intercept(Object obj, Method method, Object[] args,
				MethodProxy methodProxy) throws Throwable {
			if("saveUser".equals(method.getName())){
				System.out.println("记录日志...");
			}
			// 因为代理对象是目标对象的子类,该行代码实际调用的是父类目标代码的方法(通过子类【代理类】的invokeSuper方法去实际调用父类【目标对象】的方法)
			return methodProxy.invokeSuper(obj, args);
			// method.invoke(target, args); --> 调用的是目标对象的方法,问题就是target对象需要传过来
			// methodProxy.invoke(proxy, args); --> 调用的是代理对象的方法,此方法不可取
			// methodProxy.invokeSuper(proxy, args); --> 调用的是代理对象的父类【目标类】的方法,invokeSuper方法中会调用目标类的invoke方法完成目标对象的调用
		}
	});
	// 生成代理对象
	UserService userService = (UserService) enhancer.create();
	return userService;
}
/**
 * 测试CGLIB动态代理
 */
@Test
public void testCGLIBDynamicProxy() throws Exception{
	UserService service = new UserServiceImpl();
	service.saveUser();
	System.out.println("================");
	service = MyProxyUtils.getProxy();
	service.saveUser();
}
  • 写法二:
/**
 * 基于CGLIB实现动态代理
 */
public class CgLibProxyFactory implements MethodInterceptor {
	public Object getProxy(Class clazz){
		// 创建增强器
		Enhancer enhancer = new Enhancer();
		// 设置需要增强的类的类对象
		enhancer.setSuperclass(clazz);
		// 设置回调函数
		enhancer.setCallback(this);
		// 获取增强之后的代理对象
		return enhancer.create();
	}
	@Override
	public Object intercept(Object obj, Method method, Object[] args,
			MethodProxy proxy) throws Throwable {
		System.out.println("这是 CGLib 的动态代理方式");
		Object returnValue = proxy.invokeSuper(obj, args);
		return returnValue;
	}
}
@Test
public void testCgLibProxy() {
	// 创建目标对象
	UserService service = new UserServiceImpl();

	// 生成代理对象
	CgLibProxyFactory proxyFactory = new CgLibProxyFactory();
	UserService proxy = (UserService) proxyFactory.getProxy(service.getClass());

	// 调用目标对象的方法
	service.saveUser();
	System.out.println("===============");
	// 调用代理对象的方法
	proxy.saveUser();
}
笔记小结
* 什么是代理?
	简单点讲,代理就是帮[目标对象]去完成它应该做,但是不想或者不擅长做的事情。
	例子:
	媒婆:帮你找对象。
	黄牛:帮你去挂号。
	社保代办:帮你缴纳社保。
* 对于动态代理的学习目标
	* 了解代理对象是如何产生的?牵扯到Classloader的理解
	* 会编写JDK和cglib的动态代理代码
	* 了解代理对象和InvocationHandler实现类直接的关系(调用关系)
* 代理模式分为静态代理和动态代理
	* 静态代理:就是编写一个代理类,去代理[目标对象]
	* 动态代理:在运行期间,通过反射,对[目标对象]产生一个[代理对象]
* 动态代理的实现方式分为两种,下面两种方式生成的代理类都是继承了Proxy:
	* JDK实现的:基于接口的,也就是说[目标对象]必须实现一个接口,[目标对象]和[代理对象]都实现该接口
		* 目录类必须实现接口
		* 编写代理工厂类 implements InvocationHandler{
			getProxy(target){return Proxy.newInstance(Classloader,interface,this)}
			invoke(proxy,method,args)方法
		}
		* 生成的代理对象
		public final void saveUser()
		    throws 
		  {
		    try
		    {
		      this.h.invoke(this, m3, null); // h 就是上面的代理工厂类
		      return;
		    }
		    catch (Error|RuntimeException localError)
		    {
		      throw localError;
		    }
		    catch (Throwable localThrowable)
		    {
		      throw new UndeclaredThrowableException(localThrowable);
		    }
		  }
	* cglib实现的	:可以被继承的普通类,它是通过继承的方式,产生一个子类,这个子类就是代理类
* 为什么使用动态代理呢?
	增强对象的功能。这种增强,是符合开闭原则的,不会对目标对象进行修改,只需要扩展就可以实现增强
总结

Spring中的AOP实现,都是采用以下使用顺序

  • 只要有接口,就使用 JDK 动态代理
  • 没有接口,才使用 CGLib 的动态代理

之所以要了解动态代理,是因为Spring的AOP底层就是使用动态代理技术实现的。

基于AspectJ的AOP使用

  • 其实就是指的 Spring + AspectJ 整合,不过 Spring 已经将 AspectJ 收录到自己的框架中,并且底层织入依然是采取的动态织入的方式。

添加依赖

 <!-- 基于 AspectJ的AOP依赖 -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
	<groupId>aopalliance</groupId>
	<artifactId>aopalliance</artifactId>
	<version>1.0</version>
</dependency>

编写目标类和目标方法

  • 编写接口和实现类(目标对象)
// UserService接口
public interface UserService {
	void saveUser();
	void saveUser(String name);
	void updateUser();
}
// UserServiceImpl实现类
@Service
public class UserServiceImpl implements UserService {
	@Override
	public void saveUser() {
		System.out.println("添加用户");
	}
	@Override
	public void saveUser(String name) {
		System.out.println("添加用户name:" + name);
	}
	@Override
	public void updateUser() {
		System.out.println("修改用户");		
	}
}
  • 配置目标类,将目标类交给Spring IoC容器管理
<context:component-scan base-package="sourcecode.ioc">

使用XML实现

实现步骤
  • 编写通知(增强类,一个普通的类)
public class MyAdvice {
	// 演示前置通知
	public void before(){
		System.out.println("前置通知...");
	}
	// 演示后置通知
	public void afterreturing(){
		System.out.println("后置通知...");
	}
	// 演示最终通知
	public void after(){
		System.out.println("最终通知...");
	}
	// 演示环绕通知
	public void around(ProceedingJoinPoint joinPoint){
		System.out.println("环绕通知---前置通知");
		try {
			// 调用目标对象的方法
			joinPoint.proceed();
			System.out.println("环绕通知---后置通知");
		} catch (Throwable e) { // 相当于实现异常通知
			System.out.println("环绕通知---异常抛出配置");
			e.printStackTrace();
		} finally {
			System.out.println("环绕通知---最终通知");
		}
	}
	// 演示异常抛出通知
	public void afterThrowing(){
		System.out.println("异常抛出通知...");
	}
}
  • 配置目标对象
<!-- 配置目标对象 -->
<bean class="com.yw.spring.example.aop.target.UserServiceImpl"></bean>
  • 配置通知,将通知类交给Spring IoC容器管理
<!-- 配置通知类 -->
<bean id="myAdvice" class="com.yw.spring.example.aop.advice.MyAdvice"></bean>
  • 配置AOP切面
<!-- AOP 配置-->
<aop:config>
	<!-- 这是使用Spring AOP实现 -->
	<!-- <aop:advisor advice-ref="" pointcut=""/> -->
	
	<!-- 
		这是使用 Spring+AspectJ 的AOP实现
		配置AOP切面,切面是由通知和切入点组成的,aspect是切面,它有advice和pointcut组成
		ref指的是advice
	 -->
	 <aop:aspect ref="myAdvice">
	 	<!-- 
	 		对哪些切入点进行功能增强是由pointcut属性指定的
	 		增强哪些功能是由method属性指定的,它指定的方法是advice类中的方法
	 		什么时候织入,通过 <aop:before> 等标签来指定
	 	 -->
	 	 <aop:before method="before" pointcut="execution(* *..*.*ServiceImpl.*(..))"/>
	 	 
	 	 <!-- 
	 	 	对应 AspectJAfterAdvice 通知类
	 	 	AspectJAfterAdvice构造参数:
	 	 		第一个构造参数:method属性指定的方法的Method对象
	 	 		第二个构造参数:pointcut属性对应的切入点对象
	 	 		第三个构造参数:产生myAdvice类的工厂对象
	 	  -->
	 	 
	 	 <!-- 
	 	 	第二个参数的作用:确定哪些类运行时,调用AspectJAfterAdvice的after方法
	 	 	第一个参数和第三个参数的作用:一旦调用after方法的时候,就会调用method.invoke(obj,args)
	 	  -->
	 	 
	 	 <!-- 
	 	 	method属性:对应myAdvice类的after方法
	 	 	pointcut属性:指定切入点类AspectJExpressionPointcut
	 	  -->
	 	 <aop:after method="after" pointcut="execution(* *..*.*ServiceImpl.*(..))"/>
	 </aop:aspect>
</aop:config>
  • 测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:spring/spring-aop.xml")
public class XmlAopTest {
	@Autowired
	private UserService userService;
	@Test
	public void test(){
		userService.saveUser();
		System.out.println("===============");
		userService.saveUser("balala");
		System.out.println("===============");
		userService.updateUser();
	}
}
  • 测试结果输出:
    在这里插入图片描述
切入点表达式
  • 切入点表达式的格式
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
  • 表达式格式说明:
execution:必须要
修饰符:可省略
返回值类型:必须要,但是可以使用*通配符
包名:多级包之间使用.分割;包名可以使用*代替,多级包名可以使用多个*代替;如果想省略中间的包名可以使用 ..
类名:可以用*代替;也可以写成*DaoImpl
方法名:可以用*代替;也可以写成add*
参数:参数使用*代替;如果有多个参数,可以使用..代替
通知类型

通知类型有5种:前置通知、后置通知、最终通知、环绕通知和异常抛出通知

  • 前置通知
执行时机:目标对象方法之前执行通知
配置文件:<aop:before method="before" pointcut-ref="myPointcut"/>
应用场景:方法开始时可以进行校验
  • 后置通知
执行时机:目标对象方法之后执行通知,有异常则不执行
配置文件:<aop:after-returning method="afterReturning" pointcut-ref="myPointcut">
应用场景:可以修改方法的返回值
  • 最终通知
执行时机:目标对象方法之后执行通知,有没有异常都会执行
配置文件:<aop:after method="after" pointcut-ref="myPointcut">
应用场景:例如像释放资源
  • 环绕通知
执行时机:目标对象方法之前和之后执行通知,有没有异常都会执行
配置文件:<aop:around method="around" pointcut-ref="myPointcut">
应用场景:事务,统计代码执行时间
  • 异常抛出通知
执行时机:在抛出异常后通知
配置文件:<aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut">
应用场景:包装异常

使用注解实现

实现步骤
  • 编写切面类(注意不是通知类,因为该类中可以指定切入点)
@Component("myAspect")
@Aspect // 标记该类是一个切面类
public class MyAspect {
	private static final String pointcut = "execution(* *..*.*ServiceImpl.*(..))";
	// @Beafore:标记该方法是一个前置通知
	// value:切入点表达式
	@Before(value="MyAspect.fn()")
	public void before(){
		System.out.println("这是注解方式的前置通知");
	}
	@AfterReturning(pointcut)
	public void after(){
		System.out.println("这是注解方式的后置通知");
	}
	@Pointcut("execution(* *..*.*ServiceImpl.*(..))")
	public void fn(){}
}
  • 配置扫描切面类和目标类
<context:component-scan base-package="com.yw.spring.example.aop"></context:component-scan>
  • 开启aspectj的自动代理,用于AOP的注解方式
<aop:aspectj-autoproxy />
  • 测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:spring/spring-aop-anno.xml")
public class AnnotationAopTest {
	@Autowired
	private UserService userService;
	@Test
	public void test(){
		userService.saveUser();
		System.out.println("===============");
		userService.saveUser("ababc");
		System.out.println("===============");
		userService.updateUser();
	}
}
  • 测试结果输出:
    在这里插入图片描述
环绕通知注解配置
  • @Around:作用是把当前方法看成是环绕通知。有一个属性 value,用于指定切入点表达式,还可以指定切入点表达式的引用。
@Around(value="execution(* *..*.*ServiceImpl.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint){
	Object ret = null;
	try {
		// 获取方法执行所需的参数
		Object[] args = joinPoint.getArgs();
		// 前置通知:开启事务 beginTransaction();
		// 执行方法
		ret = joinPoint.proceed(args);
		// 后置通知:提交事务commit
	} catch (Throwable e) {
		// 异常通知:回滚事务rollback();
	} finally {
		// 最终通知:释放资源release();
	}
	return ret;
}
定义通用切入点
  • 使用 @Pointcut 注解在切面类中定义一个通用的切入点,其他通知可以引用该切入点

在这里插入图片描述

纯注解方式

@Configuration
@ComponentScan(basePackages="com.yw.spring.example")
@EnableAspectJAutoProxy
public class SpringConfiguration{}

总结

1. AOP是什么?主要用来做什么?
2. AOP的实现者都有哪些?AspectJ、Spring AOP、Spring+AspectJ整合的方式
3. AOP的术语有哪些?主要需要掌握:
	* 切入点(point cut)
	* 通知(advice)
	* 切面(aspect)
4. AOP织入的方式:静态织入和动态织入
5. 动态织入其实就是使用动态代理方式
6. 动态代理方式主要分为JDK动态代理方式和CGLib动态代理方式
7. JDK动态代理方式需要实现 InvocationHandler接口,实现invoke方法
	* 需要目标类实现接口
	* 代理类是通过 Proxy.newInstance(ClassLoader, interfaces, InvocationHandler)创建的
	* 代理对象主要是通过invocationHandler的实现类去实现代理功能的
	* invoke方法:一参是代理对象,二参是目标类接口的方法对象,三参是方法参数。method.invoke(目标对象, args);
	* 注意事项:第一个参数没有使用
8. CGLib动态代理方式需要实现 MethodInterceptor接口,实现intercept方法
9. 通知的分类:前置通知、后置通知、最终通知、环绕通知和异常抛出通知
10.编写AOP代码的思路
	* 编写目标类
	* 编写通知类(增强类) -- 不需要实现任何接口和继承任何类
	* 编写配置(可以是XML、也可以是注解),作用是绑定通知类和目标类

组件支持篇

Spring整合Junit

单元测试问题

  • 在测试类中, 每个测试方法都有以下两行代码
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
UserService service = context.getBean(UserService.class);

解决思路

  • 针对上述问题,我们需要的是程序能自动帮我们创建容器。一旦程序能自动为我们创建 Spring 容器,我们就无须手动创建了,问题就解决了。
  • 但紧接着的问题是 junit 它本身不认识 Spring,更无法帮助创建Spring容器,不过好在 Junit 给我们暴露了一个注解(@RunWith),可以让我们替换掉它的运行器。
  • 这时,我们需要依靠 Spring 框架,因为它提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件在哪里就行了。

具体实现

  • 第一步:添加 spring-test 依赖包即可。
  • 第二步:通过 @RunWith 注解,指定 Spring 的运行器,Spring 的运行器是 SpringJunit4ClassRunner。
  • 第三步:通过 @ContextConfiguration 注解,指定 Spring 运行器需要的配置文件路径
  • 第四步:通过 @Autowired 注解给测试类中的变量注入数据
  • 示例代码:
@RunWith(SpringJunit4ClassRunner.class)
@ContextConfiguration(location = "classpath:applicationContext.xml")
public class SpringJunitTest {
	@Autowired
	private UserService service;
	@Test
	public void saveUser(){
		service.saveUser();
	}
}

事务支持

  • 底层是通过 Spring 的 AOP 去实现的

回顾事务

  • 事务最终都是交给数据库来实现的,但是 jdbc 和 spring 都可以对事务进行管理。管理的前提:事务是手动提交的。因为自动提交的事务,我们管不了。

事务介绍

  • 事务:指的是逻辑上的一组操作,组成这个事务的各个执行单元,要么一起成功,要么一起失败。
  • 事务的特性(ACID)
    • 原子性(Atomicity):是指事务包含的所有操作要么全部成功,要么全部失败回滚。
    • 一致性(Consistency):是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
    • 隔离性(Isolation):是指当多个用户并发访问数据库时,比如操作同一张表,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
    • 持久性(Durability):是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

事务并发问题(隔离性导致)

在事务的并发操作中可能会出现一些问题:

  • 脏读:一个事务读取到另一个事务未提交的数据。
  • 不可重复读:一个事务因读取到另一个事务【Update操作】已提交的数据,导致对同一条记录读取两次以上的结果不一致。
  • 幻读:一个事务因读取到另一个事务【Insert/Delete操作】已提交的数据,导致对同一张表读取两次以上的结果不一致。

事务隔离级别

  • 为了避免上面出现的几种情况,在标准SQL规范中,定义了4种事务隔离级别,不同的隔离级别对事务的处理不同。四种隔离级别,由低到高分别是:
    • Read uncommitted(读未提交):最低级别,任何情况都无法保证;
    • Read committed(读已提交):可避免脏读的发生;
    • Repeatable read(可重复读):可避免脏读,不可重复读的发生;
    • Serializable(串行化):可避免脏读、不可重复读、幻读的发生。
  • 默认隔离级别:大多数数据库的默认隔离级别是Read committed(RC),比如:Oracle、DB2等;MySQL数据库的默认隔离级别是Repeatable read(RR)。

注意事项:隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

事务的传播级别

  • 事务隔离级别定义的是事务在数据库读写方面的控制范围,而传播级别定义的是事务的控制范围。
  • 以下是事务的7种传播级别:
    • PROPAGATION_REQUIRED:默认的 Spring 事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
    • PROPAGATION_SUPPORTS:字面意思就知道、supports 支持。该传播级别的特点是,如果上下文存在事务,则支持事务加入事务;如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在 transactionTemplate.execute 中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
    • PROPAGATION_MANDATORY:该级别的事务要求上下文中必须要存在事务,否则就会抛出异常。配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
    • PROPAGATION_REQUIRES_NEW:从字面即可知道,new 每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
      • 这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100 个红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。
    • PROPAGATION_NOT_SUPPORTED:这个也可以从字面得知,not supported 不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
      • 这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板包起来就可以了。
    • PROPAGATION_NEVER:该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出 runtime 异常,强制停止执行!这个级别上辈子跟事务有仇。
    • PROPAGATION_NESTED:字面也可知道,nested 嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

Spring框架事务管理相关接口

  • Spring并不直接管理事务,而是提供了事务管理接口——PlatformTransactionManager,通过这个接口,Spring为各个平台,如:JDBC、Hibernate 等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

在这里插入图片描述

  • PlatformTransactionManager接口:平台事务管理器(真正管理事务的类)。该接口有具体的实现类,根据不同的持久层框架,需要选择不同的实现类。
1. 接口的实现类
	1)如果使用的是Spring的JDBC模板或MyBatis(IBatis)框架,需要选择DataSourceTransactionManager实现类;
	2)如果使用的是Hibernate的框架,需要选择HibernateTransactionManager实现类
2. 该接口的常用方法
	1)void commit(TransactionStatus status);
	2)TransactionStatus getTransaction(TransactionDefinition definition);
	3)void rollback(TransactionStatus status);
  • TransactionDefinition接口:事务定义信息(事务的隔离级别,传播行为,超时,只读)。
1. 事务隔离级别的常量
	* static int ISOLATION_DEFAULT	-- 采用数据的默认隔离级别
	* static int ISOLATION_READ_UNCOMMITIED
	* static int ISOLATION_READ_COMMITIED
	* static int ISOLATION_REPEATABLE_READ
	* static int ISOLATION_SERIALIZABLE
2. 事务的传播行为常量(不用设置,使用默认值) 
	* 先解释什么是事务的传播行为:解决的是业务层之间的方法调用
	* PROPAGATION_REQUIRED(默认值) 	-- A中有事务,使用A中的事务;如果没有,B就会开启一个新的事务,将A包含进来(保证A,B在同一个事务中),默认值。
	* PROPAGATION_SUPPORTS			-- A中有事务,使用A中的事务;如果没有,那么B中也使用事务。
	* PROPAGATION_MANDATORY			-- A中有事务,使用A中的事务;如果没有,抛出异常。
	* PROPAGATION_REQUIRES_NEW		-- A中有事务,将A中的事务挂起;B创建一个新的事务(保证A,B没有在一个事务中)。
	* PROPAGATION_NOT_SUPPORTED		-- A中有事务,将A中的事务挂起。
	* PROPAGATION_NEVER				-- A中有事务,抛出异常。
	* PROPAGATION_NESTED			-- 嵌套事务,当A执行之后,就会在这个位置设置一个保存点;如果B没有问题,执行通过;如果B出现异常,运行客户根据需求回滚(选择回滚到保存点或是最初始状态)
  • TransactionStatus接口:事务的状态(是否新事务、是否已提交、是否有保存点、是否回滚)。
  • **小结:**上述对象之间的关系:平台事务管理器是真正管理事务的对象,它根据事务定义的信息TransactionDefinition对象进行事务管理,在管理事务中产生的一些状态记录到TransactionStatus中。

Spring框架事务管理的分类

编程式事务管理(了解)
  • 通过手动编写代码的方式完成事务的管理(不推荐使用)
  • Spring为了简化事务管理的代码,提供了模板类 TransactionTemplate,所以手动编程的方式来管理事务,只需要使用该模板类即可。具体步骤如下:
  • ① 配置一个事务管理器,Spring 使用 PlatformTransactionManager 接口来管理事务,所以我们需要使用到它的实现类
<!-- 配置事务管理器 -->
<bean id="transationManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource"></property>
</bean>
  • ② 配置事务管理的模板
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
	<property name="transactionManager" ref="transactionManager"/>
</bean>
  • ③ 在需要进行事务管理的类中,注入事务管理的模板
<bean id="accountService" class="com.yw.spring.example.tx.service.AccountServiceImpl">
	<property name="accountDao" ref="accountDao"/>
	<property name="transactionTemplate" ref="transactionTemplate"/>
</bean>
  • ④ 在业务层使用模板管理事务
// 注入事务模板对象
private TransactionTemplate transactionTemplate;
public void setTransactionTemplate(TransactionTemplate transactionTemplate){
	this.transactionTemplate = transactionTemplate;
}
public void pay(final String out, final String in, final double money){
	transactionTemplate.execute(new TransactionCallbackWithoutResult(){
		protected void doInTransactionWithoutResult(TransactionStatus status){
			// 扣钱
			accountDao.outMoney(out, money);
			int a = 10/0;
			// 加钱
			accountDao.inMoney(in, money);
		}
	})
}
声明式事务管理(重点)
  • 通过一段配置的方式完成事务的管理(底层采用AOP的技术),事务管理的目标类一般都是业务层代码。声明式事务管理又分为三种方式:
    • 基于AspectJ的XML方式(重点掌握)
    • 基于AspectJ的注解+XML混合方式(重点掌握)
    • 基于AspectJ的纯注解方式(重点掌握)

事务管理开发方式

事务管理之XML方式
  • 定义业务层接口和实现类:
public interface AccountService {
	void transfer(String from , String to,double money);
}
//@Transactional:标记该类的所有方法都已经被事务进行管理了,至于管理属性,不设置的话,都采取默认值
@Transactional
@Service
public class AccountServiceImpl implements AccountService {
	@Resource
	private AccountDao accountDao;
	
	@Override
	public void transfer(String from, String to, double money) {
		// 先查询from账户的钱
		double fromMoney = accountDao.queryMoney(from);
		// 对from账户进行扣钱操作
		accountDao.update(from, fromMoney - money);

		// 手动制造异常
		// System.out.println(1/0);
		// 先查询from账户的钱
		double toMoney = accountDao.queryMoney(to);
		// 对to账户进行加钱操作
		accountDao.update(to, toMoney + money);
	}
}
  • 定义持久层接口和实现类:
public interface AccountDao {
	void update(String name,double money);
	double queryMoney(String name);
}
/**
 * JdbcDaoSupport内部封装了JdbcTemplate
 */
@Component
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
    @Autowired
    public AccountDaoImpl(DataSource dataSource) {
        setDataSource(dataSource);
    }

    @Override
    public void update(String name, double money) {
        Object[] args = {money, name};
        this.getJdbcTemplate().update("update account set money = ? where name = ?", args);
    }

    @Override
    public double queryMoney(String name) {
        return this.getJdbcTemplate().queryForObject(
                "select money from account where name = ?",
                new DoubleMapper(), name);
    }

    // 结果映射器
    class DoubleMapper implements RowMapper<Double> {
        @Override
        public Double mapRow(ResultSet rs, int rowNum) throws SQLException {
            return rs.getDouble("money");
        }
    }
}
  • Spring配置:
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://192.168.254.128:3306/ssm"/>
  <property name="username" value="root"/>
  <property name="password" value="123456"/>
</bean>

<!-- 扫描AccountDao和AccountService -->
<context:component-scan base-package="com.yw.spring.example.tx" />
  • 配置事务管理的AOP:使用平台事务管理器 DataSourceTransactionManager
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource" />
</bean>
  • 配置事务通知:TransactionInterceptor
<tx:advice id="txAdvice" transaction-manager="transactionManager">
	<tx:attributes>
		<tx:method name="save*" propagation="REQUIRED" />
		<tx:method name="update*" propagation="REQUIRED" />
		<tx:method name="transfer*" propagation="REQUIRED" />
		<tx:method name="query*" read-only="true" />
	</tx:attributes>
</tx:advice>
  • AOP配置:
<!-- 配置aop切面类或者通知器类 -->
<aop:config>
	<!-- 
		这使用的是Spring AOP的实现
		advice-ref:指定advice增强类
	 -->
	<aop:advisor advice-ref="txAdvice"
		pointcut="execution(* *..*.*ServiceImpl.*(..))" />
</aop:config>
  • 单元测试代码:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:spring/spring-tx.xml")
public class XmlTxTest {
	@Autowired
	private AccountService service;
	@Test
	public void testTransfer() {
		service.transfer("老公", "老婆", 100);
	}
}
事务管理之混合方式
  • service类上或者方法上加注解:
    • 类上加 @Transactional:表示该类中所有的方法都被事务管理;
    • 方法上加 @Transactional:表示只有该方法被事务管理
  • 开启事务注解:
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>

<!-- 配置事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
事务管理之基于AspectJ的纯注解方式
@EnableTransactionManagement
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值