Spring框架 - AOP&事务
AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中
AOP前奏
提出问题
情景:数学计算器
要求
- 执行加减乘除运算
- 日志:在程序执行期间追踪正在发生的活动
- 验证:希望计算器只能处理正数的运算
常规实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
public int add(int i, int j) {
System.out.println("日志记录==> The method add begin with ["+i+","+j+"]");
int result = i + j ;
System.out.println("日志记录==> The method add ends with : " + result );
return result ;
}
public int sub(int i, int j) {
System.out.println("日志记录==> The method sub begin with ["+i+","+j+"]");
int result = i - j ;
System.out.println("日志记录==> The method sub ends with : " + result );
return result ;
}
public int mul(int i, int j) {
System.out.println("日志记录==> The method mul begin with ["+i+","+j+"]");
int result = i * j ;
System.out.println("日志记录==> The method mul ends with : " + result );
return result ;
}
public int div(int i, int j) {
System.out.println("日志记录==> The method div begin with ["+i+","+j+"]");
int result = i / j ;
System.out.println("日志记录==> The method div ends with : " + result );
return result ;
}
}问题
代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
代码分散: 以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。
动态代理
动态代理的原理
代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
动态代理的方式
基于接口实现动态代理: JDK动态代理
基于继承实现动态代理: Cglib、Javassist动态代理
数学计算器的改进
JDK动态代理
Proxy : 是所有动态代理类的父类, 专门用户生成代理类或者是代理对象
1
2
3
4
5
6
7
8
9//用于生成代理类的Class对象.
public static Class<?> getProxyClass(ClassLoader loader,
Class<?>... interfaces)
// 用于生成代理对象
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)InvocationHandler :完成动态代理的整个过程.
1
2public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;日志代理类-newProxyInstance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60public class ArithmeticCalculatorProxy {
//动态代理: 目标对象 如何获取代理对象 代理要做什么
//目标对象
private ArithmeticCalculator target ;
public ArithmeticCalculatorProxy(ArithmeticCalculator target) {
this.target = target ;
}
//获取代理对象的方法
public Object getProxy() {
//代理对象
Object proxy ;
/**
* loader: ClassLoader对象。 类加载器对象. 帮我们加载动态生成的代理类。
*
* interfaces: 接口们. 提供目标对象的所有的接口. 目的是让代理对象保证与目标对象都有接口中想同的方法.
*
* h: InvocationHandler类型的对象.
*/
ClassLoader loader = target.getClass().getClassLoader();
Class [] interfaces = target.getClass().getInterfaces();
proxy = Proxy.newProxyInstance(loader, interfaces, new InvocationHandler() {
/**
* invoke: 代理对象调用代理方法, 会回来调用invoke方法。
*
* proxy: 代理对象 , 在invoke方法中一般不会使用.
*
* method: 正在被调用的方法对象.
*
* args: 正在被调用的方法的参数.
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//将方法的调用转回到目标对象上.
//获取方法的名字
String methodName = method.getName();
//记录日志
System.out.println("LoggingProxy==> The method " + methodName+" begin with "+ Arrays.asList(args));
Object result = method.invoke(target, args); // 目标对象执行目标方法. 相当于执行ArithmeticCalculatorImpl中的+ - * /
//记录日志
System.out.println("LoggingProxy==> The method " + methodName +" ends with :" +result );
return result ;
}
});
return proxy ;
}
}日志代理类-getProxyClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50public class ArithmeticCalculatorProxy2 {
//动态代理: 目标对象 如何获取代理对象 代理要做什么
//目标对象
private ArithmeticCalculator target ;
public ArithmeticCalculatorProxy2(ArithmeticCalculator target) {
this.target = target ;
}
//获取代理对象的方法
public Object getProxy() throws Exception {
//代理对象
Object proxy ;
ClassLoader loader = target.getClass().getClassLoader();
Class [] interfaces = target.getClass().getInterfaces();
Class proxyClass = Proxy.getProxyClass(loader, interfaces);
//Class创建对象? 通过获取构造器然后 newInstance()
Constructor con =
proxyClass.getDeclaredConstructor(InvocationHandler.class);
proxy = con.newInstance(new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//将方法的调用转回到目标对象上.
//获取方法的名字
String methodName = method.getName();
//记录日志
System.out.println("LoggingProxy2==> The method " + methodName+" begin with "+ Arrays.asList(args));
Object result = method.invoke(target, args); // 目标对象执行目标方法. 相当于执行ArithmeticCalculatorImpl中的+ - * /
//记录日志
System.out.println("LoggingProxy2==> The method " + methodName +" ends with :" +result );
return result ;
}
});
return proxy ;
}
}测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public static void main(String[] args) throws Exception{
//将动态生成的代理类保存下来
Properties properties = System.getProperties();
properties.put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//目标对象
ArithmeticCalculator target = new ArithmeticCalculatorImpl();
//获取代理对象
Object obj = new ArithmeticCalculatorProxy(target).getProxy();
// 转回具体的类型.
ArithmeticCalculator proxy = (ArithmeticCalculator) obj ;
System.out.println(proxy.getClass().getName());
int result = proxy.add(1, 1);
System.out.println("Main Result : " + result );
/**
* 问题:
* 1. 代理对象能否转换成目标对象的类型?
不能,可以将代理理解为继承了接口的一个类,和目标对象是二个类,通过接口知道方法。
* 2. 代理对象调用代理方法,为什么会执行 InvocationHandler中的invoke 方法
newProxyInstance的第三个参数 InvocationHandle 会传到代理类中,代理类在生成后,调用代理方法,
实际上是调用的是InvocationHandle的invoke方法。
*/
底层生成的动态代理类
1 | package top.fulsun.spring.aop.proxy; |
基于继承的方式
- 如果算数运算的实现类中除了加减乘除外还有其他方法,代理类只能根据接口得到目标对象的方法,这是后根据接口的动态代理就不行了,需要使用继承的方式的动态代理。
AOP概述
AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
AOP编程操作的主要对象是切面(aspect),而切面模块化横切关注点。
在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
AOP的好处:
每个事物逻辑位于一个位置,代码不分散,便于维护和升级
业务模块更简洁,只包含核心业务代码
AOP图解
AOP术语
横切关注点
从每个方法中抽取出来的同一类非核心业务。
切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。
通知(Advice)
切面必须要完成的各个具体工作
目标(Target)
被通知的对象
代理(Proxy)
向目标对象应用通知之后创建的代理对象
连接点(Joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
在应用程序中可以使用横纵两个坐标来定位一个具体的连接点:(简单记忆就是模块中的方法,)
切入点(pointcut):
定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
图解
最大的圆是我们的目标,里面有四个方法,这四个方法就是连接点,这里有个切面(日志切面),切面里面要完成的操作就是通知,通知需要作用到方法(连接点)上,需要使用切面表达式找到方法,这时候通知就可以作用到连接点上,这个时候连接点也就变成了一个切入点。
AspectJ
简介
AspectJ:Java社区里最完整最流行的AOP框架。
在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。
启用AspectJ注解支持
导入JAR包
- com.springsource.net.sf.cglib-2.2.0.jar
- com.springsource.org.aopalliance-1.0.0.jar
- com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
- spring-aop-4.0.0.RELEASE.jar
- spring-aspects-4.0.0.RELEASE.jar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59<properties>
<spring.version>5.2.2.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aopalliance/com.springsource.org.aopalliance -->
<dependency>
<groupId>org.aopalliance</groupId>
<artifactId>com.springsource.org.aopalliance</artifactId>
<version>1.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>引入aop名称空间
1
2
3
4
5
6
7
8
9
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
</beans>配置
1
2
3
4
5<!-- 组件扫描 -->
<context:component-scan base-package="top.fulsun.spring.aspectJ.annotation"></context:component-scan>
<!-- 基于注解使用AspectJ: 主要的作用是为切面中通知能作用到的目标类生成代理. -->
<aop:aspectj-autoproxy/>
当Spring IOC容器侦测到bean配置文件中的<aop:aspectj-autoproxy>
元素时,会自动为与AspectJ切面匹配的bean创建代理
用AspectJ注解声明切面
要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。
当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。
在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。
通知是标注有某种注解的简单的Java方法。
AspectJ支持5种类型的通知注解:
@Before
:前置通知,在方法执行之前执行@After
:后置通知,在方法执行之后执行@AfterRunning
:返回通知,在方法返回结果之后执行@AfterThrowing
:异常通知,在方法抛出异常之后执行@Around
:环绕通知,围绕着方法执行
例子
计算器接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package top.fulsun.spring.aspectJ.annotation;
/**
* 算数计算器
*
*/
public interface ArithmeticCalculator {
public int add(int i ,int j );
public int sub(int i, int j );
public int mul(int i ,int j );
public int div(int i, int j );
}实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36package top.fulsun.spring.aspectJ.annotation;
import org.springframework.stereotype.Component;
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
public int add(int i, int j) {
int result = i + j ;
return result ;
}
public int sub(int i, int j) {
int result = i - j ;
return result ;
}
public int mul(int i, int j) {
int result = i * j ;
return result ;
}
public int div(int i, int j) {
int result = i / j ;
return result ;
}
//public int aaa() {}
}日志切面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108package top.fulsun.spring.aspectJ.annotation;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 日志切面
*/
//标识为一个组件
//标识为一个切面
public class LoggingAspect {
/**
* 前置通知: 在目标方法(连接点)执行之前执行.
*/
public void beforeMethod(JoinPoint joinPoint) {
//获取方法的参数
Object [] args = joinPoint.getArgs();
//方法的名字
String methodName = joinPoint.getSignature().getName();
System.out.println("LoggingAspect==> The method "+methodName+" begin with "+ Arrays.asList(args));
}
/**
* 后置通知: 在目标方法执行之后执行, 不管目标方法有没有抛出异常. 不能获取方法的结果
* * top.fulsun.spring.aspectJ.annotation.*.*(..)
* * : 任意修饰符 任意返回值
* * : 任意类
* * : 任意方法
* ..: 任意参数列表
*
* 连接点对象: JoinPoint
*/
public void afterMethod(JoinPoint joinPoint) {
//方法的名字
String methodName = joinPoint.getSignature().getName();
System.out.println("LoggingAspect==> The method " + methodName +" ends .");
}
/**
* 返回通知: 在目标方法正常执行结束后执行. 可以获取到方法的返回值.
*
* 获取方法的返回值: 通过returning 来指定一个名字, 必须要与方法的一个形参名一致.
*/
public void afterReturningMethod(JoinPoint joinPoint,Object result ) {
//方法的名字
String methodName = joinPoint.getSignature().getName();
System.out.println("LoggingAspect==> The method " + methodName + " end with :" + result );
}
/**
* 异常通知: 在目标方法抛出异常后执行.
*
* 获取方法的异常: 通过throwing来指定一个名字, 必须要与方法的一个形参名一致.
*
* 可以通过形参中异常的类型 来设置抛出指定异常才会执行异常通知.
*
*/
public void afterThrowingMethod(JoinPoint joinPoint,ArithmeticException ex ) {
//方法的名字
String methodName = joinPoint.getSignature().getName();
System.out.println("LoggingAspect==> Thew method " + methodName + " occurs Exception: " +ex );
}
/**
* 环绕通知: 环绕着目标方法执行. 可以理解是 前置 后置 返回 异常 通知的结合体,更像是动态代理的整个过程.
*/
public Object aroundMethod(ProceedingJoinPoint pjp) {
//执行目标方法
try {
//前置
Object result = pjp.proceed();
//返回
return result ;
} catch (Throwable e) {
//异常通知
e.printStackTrace();
}finally {
// 后置
}
return null;
}
}
AOP细节
切入点表达式
作用
通过表达式的方式定位一个或多个具体的连接点。
语法细节
切入点表达式的语法格式
execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19execution(* top.fulsun.spring.ArithmeticCalculator.*(..))
# ArithmeticCalculator接口中声明的所有方法。
# 第一个“*”代表任意修饰符及任意返回值。
# 第二个“*”代表任意方法。
# “..”匹配任意数量、任意类型的参数。
# 若目标类、接口与该切面类在同一个包中可以省略包名。
execution(public * ArithmeticCalculator.*(..))
# ArithmeticCalculator接口的所有公有方法
execution(public double ArithmeticCalculator.*(..))
# ArithmeticCalculator接口中返回double类型数值的方法
execution(public double ArithmeticCalculator.*(double, ..))
# 第一个参数为double类型且返回double类型数值的方法。
# “..” 匹配任意数量、任意类型的参数。
execution(public double ArithmeticCalculator.*(double, double))
# 参数类型为double,double类型的方法在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
1
2
3
4
5execution (* *.add(int,..)) || execution(* *.sub(int,..))
# 任意类中第一个参数为int类型的add方法或sub方法
!execution (* *.add(int,..))
# 匹配不是任意类中第一个参数为int类型的add方法切入点表达式应用到实际的切面类中
当前连接点细节
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint
接口的实例对象中。
1 |
|
通知
在具体的连接点上要执行的操作。
一个切面可以包括一个或者多个通知。
通知所使用的注解的值往往是切入点表达式。
前置通知
- 前置通知:在方法执行之前执行的通知
- 使用@Before注解
后置通知
- 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候
- 无论连接点是正常返回还是抛出异常,后置通知都会执行,不能获取方法的结果。
- 使用@After注解
返回通知
返回通知:如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
使用@AfterReturning注解,
在返回通知中访问连接点的返回值
- 在返回通知中,只要将
returning
属性添加到@AfterReturning
注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
- 在返回通知中,只要将
必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
- 原始的切点表达式需要出现在pointcut属性中
1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 返回通知: 在目标方法正常执行结束后执行. 可以获取到方法的返回值.
*
* 获取方法的返回值: 通过returning 来指定一个名字, 必须要与方法的一个形参名一致.
*/
public void afterReturningMethod(JoinPoint joinPoint,Object result ) {
//方法的名字
String methodName = joinPoint.getSignature().getName();
System.out.println("LoggingAspect==> The method " + methodName + " end with :" + result );
}
异常通知
异常通知:只在连接点抛出异常时才执行异常通知
将
throwing
属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 异常通知: 在目标方法抛出异常后执行.
*
* 获取方法的异常: 通过throwing来指定一个名字, 必须要与方法的一个形参名一致.
*
* 可以通过形参中异常的类型 来设置抛出指定异常才会执行异常通知.
*
* 设置只有出现ArithmeticException才执行
*/
public void afterThrowingMethod(JoinPoint joinPoint,ArithmeticException ex ) {
//方法的名字
String methodName = joinPoint.getSignature().getName();
System.out.println("LoggingAspect==> Thew method " + methodName + " occurs Exception: " +ex );
}
环绕通知
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 环绕通知: 环绕着目标方法执行. 可以理解是 前置 后置 返回 异常 通知的结合体,更像是动态代理的整个过程.
*/
public Object aroundMethod(ProceedingJoinPoint pjp) {
//执行目标方法
try {
//前置
Object result = pjp.proceed();
//返回
return result ;
} catch (Throwable e) {
//异常通知
e.printStackTrace();
}finally {
// 后置
}
return null;
}
重用切入点定义
- 在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
- 在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
- 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
- 其他通知可以通过方法名称引入该切入点
1 | /** |
指定切面的优先级
- 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
- 切面的优先级可以通过实现Ordered接口或利用
@Order
注解指定。 - 实现Ordered接口,getOrder()方法的返回值越小,优先级越高。默认2147483647(0x7fffffff)
- 若使用@Order注解,序号出现在注解中
1 | /** |
以XML方式配置切面
概述
除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
配置细节
在bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>
元素内部。对于每个切面而言,都要创建一个<aop:aspect>
元素来为具体的切面实现引用后端bean实例。
切面bean必须有一个标识符,供<aop:aspect>
元素引用。
声明切入点
切入点使用
<aop:pointcut>
元素声明。切入点必须定义在
<aop:aspect>
元素下,或者直接定义在<aop:config>
元素下。- 定义在
<aop:aspect>
元素下:只对当前切面有效 - 定义在
<aop:config>
元素下:对所有切面都有效
- 定义在
基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。
声明通知
- 在aop名称空间中,每种通知类型都对应一个特定的XML元素。
- 通知元素需要使用
<pointcut-ref>
来引用切入点,或用<pointcut>
直接嵌入切入点表达式。 - method属性指定切面类中通知方法的名称
例子
1 |
|
JdbcTemplate
概述
- 为了使JDBC更加易于使用,Spring在JDBC API上定义了一个抽象层,以此建立一个JDBC存取框架。
- 作为Spring JDBC框架的核心,JDBC模板的设计目的是为不同类型的JDBC操作提供模板方法,通过这种方式,可以在尽可能保留灵活性的情况下,将数据库存取的工作量降到最低。
- 可以将Spring的JdbcTemplate看作是一个小型的轻量级持久化层框架,和我们之前使用过的DBUtils风格非常接近。
环境准备
导入IOC容器所需要的JAR包
- commons-logging-1.1.1.jar
- spring-beans-4.0.0.RELEASE.jar
- spring-context-4.0.0.RELEASE.jar
- spring-core-4.0.0.RELEASE.jar
- spring-expression-4.0.0.RELEASE.jar
JdbcTemplate所需要的JAR包
spring-jdbc-4.0.0.RELEASE.jar
spring-orm-4.0.0.RELEASE.jar
spring-tx-4.0.0.RELEASE.jar
数据库驱动和数据源
- c3p0-0.9.1.2.jar
- mysql-connector-java-5.1.7-bin.jar
Maven方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!-- JdbcTemplate所需要的JAR包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!--数据库驱动和数据源-->
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--<version>5.1.47</version>-->
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>创建连接数据库基本信息属性文件
1
2
3
4
5
6
7
8
9
10
11user=root
password=root
jdbcUrl=jdbc:mysql:///query_data
driverClass=com.mysql.jdbc.Driver
initialPoolSize=30
minPoolSize=10
maxPoolSize=100
acquireIncrement=5
maxStatements=1000
maxStatementsPerConnection=10
XML配置
Spring配置文件中配置相关的bean
数据源对象
1
2
3
4
5
6
7
8<!-- 数据源 -->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
JdbcTemplate对象
1
2
3
4<!-- JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
Java配置
提供一个配置类,在配置类中配置 JdbcTemplate:
1 | /** |
持久化操作
增删改
JdbcTemplate.update(String, Object...)
批量增删改
JdbcTemplate.batchUpdate(String, List<Object[]>)
Object[]
封装了SQL语句每一次执行时所需要的参数List集合封装了SQL语句多次执行时的所有参数
查询单行
JdbcTemplate.queryForObject(String, RowMapper<Department>, Object...)
查询多行
JdbcTemplate.query(String, RowMapper<Department>, Object...)
RowMapper
对象依然可以使用BeanPropertyRowMapper
查询单一值
JdbcTemplate.queryForObject(String, Class, Object...)
1 | import java.util.ArrayList; |
使用具名参数的JdbcTemplate
关于具名参数
在Hibernate的HQL查询中我们体验过具名参数的使用,相对于基于位置的参数,具名参数具有更好的可维护性,在SQL语句中参数较多时可以考虑使用具名参数。
在Spring中可以通过
NamedParameterJdbcTemplate
类的对象使用带有具名参数的SQL语句。通过IOC容器创建NamedParameterJdbcTemplate对象
1
2
3
4
5<!-- 配置可以使用具名参数的JDBCTemplate类对象 -->
<!-- NamedParameterJdbcTemplate -->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>具名参数
SQL语句中的格式
INSERT INTO depts (dept_name) VALUES (:deptName)
通过
Map对象
传入NamedParameterJdbcTemplate.update(String sql, Map<String, ?> map)
Map的键是参数名,值是参数值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 测试具名参数模板类
*/
public void testNpjt() {
String sql = "insert into tbl_employee(last_name,email,gender) values(:ln,:em,:ge)";
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("ln", "Jerry");
paramMap.put("em", "jerry@sina.com");
paramMap.put("ge", 0);
//Map对象传入
npjt.update(sql, paramMap);
}通过
SqlParameterSource
对象传入,这里的具名参数要和对象的属性名一致1
2
3
4
5
6
7
8
9
10
11
12
13
public void testNpjtObject() {
//模拟Service层 直接传递给Dao层一个具体的 对象
Employee employee = new Employee(null, "张无忌", "zwj@sina.com", 1);
//在dao的插入方法中:
String sql ="insert into tbl_employee(last_name,email,gender) values(:lastName,:email,:gender)";
SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee) ;
npjt.update(sql, paramSource);
}
使用JdbcTemplate实现Dao
- 通过IOC容器自动注入
JdbcTemplate类是线程安全的,所以可以在IOC容器中声明它的单个实例,并将这个实例注入到所有的Dao实例中。
1 |
|
声明式事务管理
事务概述
在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
事务的四个关键属性(ACID)
原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
Spring事务管理
编程式事务管理
使用原生的JDBC API进行事务管理
- 获取数据库连接Connection对象
- 取消事务的自动提交
- 执行操作
- 正常完成操作时手动提交事务
- 执行失败时回滚事务
- 关闭相关资源
评价
- 使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。
- 编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。
- 在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。
- 相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
声明式事务管理
- 大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
- 事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
- Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
- Spring既支持编程式事务管理,也支持声明式的事务管理。
Spring提供的事务管理器
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是
PlatformTransactionManager
。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
事务管理器的主要实现
DataSourceTransactionManager
:在应用程序中只需要处理一个数据源,而且通过JDBC存取。JtaTransactionManager
:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理HibernateTransactionManager
:用Hibernate框架存取数据库
简单案例
需求
书店买书的流程,查询价格,扣款,减库存。
1 | public interface BookShopService { |
数据库表
1 | CREATE TABLE book ( |
Java 注解配置
配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30<!-- 数据源 -->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- NamedParameterJdbcTemplate -->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<!-- 事务管理器 -->
<bean id="dataSourceTransactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启事务注解
transaction-manager 用来指定事务管理器, 如果事务管理器的id值 是 transactionManager,
可以省略不进行指定。
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
事务控制
在需要进行事务控制的方法上加注解 @Transactional
1 | //对当前类中所有的方法都起作用 |
事务的传播行为
简介
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。
设置
事务传播属性可以在@Transactional
注解的propagation
属性中定义。
在Spring 2.x事务通知中,可以像下面这样在<tx:method>
元素中设定传播事务属性。
测试
添加一个结账的方法checkout,买多本书
1 | public interface Cashier { |
REQUIRED传播行为
当bookService的purchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书都买不了。
1 | /** 事务属性: |
REQUIRES_NEW传播行为
表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。
1 | /** 事务属性: |
事务的隔离级别
数据库事务并发问题
假设现在有两个事务:Transaction01和Transaction02并发执行。
脏读
- Transaction01将某条记录的AGE值从20修改为30。
- Transaction02读取了Transaction01更新后的值:30。
- Transaction01回滚,AGE值恢复到了20。
- Transaction02读取到的30就是一个无效的值。
不可重复读(针对的是修改操作)
- Transaction01读取了AGE值为20。
- Transaction02将AGE值修改为30。
- Transaction01再次读取AGE值为30,和第一次读取不一致。
幻读(针对的是插入操作)
- Transaction01读取了STUDENT表中的一部分数据。
- Transaction02向STUDENT表中插入了新的行。
- Transaction01读取了STUDENT表时,多出了一些行。
隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。
- 可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
- 串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
- 各个隔离级别解决并发问题的能力见下表
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
- 各种数据库产品对事务隔离级别的支持程度
Oracle | MySQL | |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
指定事务隔离级别
XML
在Spring 2.x事务通知中,可以在
<tx:method>
元素中指定隔离级别注解
用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别
1
2
3
4
5
6
7
public void buyBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username, price);
}
事务回滚
默认情况
捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。
xml
在Spring 2.x事务通知中,可以在
<tx:method>
元素中指定回滚规则。如果有不止一种异常则用逗号分隔。设置注解@Transactional
rollbackFor
属性:指定遇到时必须进行回滚的异常类型,可以为多个rollbackForClassName
:参数为字符串数组noRollbackFor
属性:指定遇到时不回滚的异常类型,可以为多个noRollbackForClassName
:参数为字符串数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 当余额不足的时候不会回滚:没有买成功但库存会减少
public void buyBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username, price);
}
//==================分割符===================================
public class BookShopDaoImpl implements BookShopDao {
public void updateUserAccount(String username, Integer price) {
//判断余额是否足够
String sql ="select balance from account where username = ? ";
Integer balance = jdbcTemplate.queryForObject(sql, Integer.class,username);
if(balance < price) {
throw new UserAccountException("余额不足......");
}
sql = "update account set balance = balance - ? where username = ? ";
jdbcTemplate.update(sql, price,username);
}
}
//==================分割符===================================
public class UserAccountException extends RuntimeException {
//调用父类的方法
}
事务的超时和只读属性
由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。
如果一个事务只读取数据但不做修改,数据库引擎可以对这个事务进行优化。
超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
设置
XML
在Spring 2.x事务通知中,超时和只读属性可以在
<tx:method>
元素中进行指定@Transaction注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void buyBook(String username, String isbn) {
//验证超时:永远不会买成功
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username, price);
}
XML声明式事务配置
1 | <!-- 事务管理器 --> |