Spring中的面向切面编程。
事务控制
1 | public void transfer(String sourceName, String targetName, Float money) { |
多次连接如果中间出现一个异常,前面执行后面没有执行,且无法回滚。需要使用ThreadLocal
对象把Connection合当前线程绑定,从而使一个线程中只有一个能控制事务的对象。
示例:
1 | public class AccountDaoImpl_OLD implements IAccountDao { |
1 | public class AccountServiceImpl implements IAccountService { |
事务控制应该在业务层,转移。
整合示例:
1 | public class AccountDaoImpl implements IAccountDao { |
1 | public class ConnectionUtils { |
1 | public class TransactionManager { |
1 | public class AccountServiceImpl_OLD implements IAccountService { |
1 | <!-- 业务层 --> |
动态代理
字节码随用随创建,随用随加载。在不修改源码的基础上对方法增强。
如何创建代理对象
基于接口的动态代理
使用Proxy
类中的newProxyInstance
方法,参数:
ClassLoader
:类加载器,用于加载代理对象字节码,和被代理对象使用相同类加载器。固定写法。Class[]
:字节码数组,用于让代理对象和被代理对象有相同方法。固定写法。InvocationHandler
:用于提供增强的代码。让我们写如何代理。一般都是写一个该接口的实现类,通常情况下是匿名内部类,但不是必须的。- 被代理类最少实现一个接口,如果没有则不能使用。
1 | final Producer producer=null;//final且要求Producer实现接口 |
基于子类的动态代理
使用cglib
提供的Enhancer
类中的create
方法,参数:
Class
:字节码,用于指定被代理对象的字节码。Callback
:用于提供增强的代码。一般写的都是该接口的子接口的实现类MethodIntercepter
。- 要求被代理类不能是最终类,即不能是被
final
修饰的类。
-
配置
pom.xml
坐标1
2
3
4
5<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency> -
代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17final Producer producer =new Producer();
Producer cglibProducer= (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
//执行被代理对象的任何方法都会经过该方法,前三个参数和函数返回值与接口代理invoke一致
//methodProxy:当前执行方法的代理对象
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object retuernValue=null;
//1.获取方法执行的参数
Float money = (Float) args[0];
//2.判断当前的方法是不是销售
if ("saleProduct".equals(method.getName())){
retuernValue= method.invoke(producer,money*0.8f);
}
return retuernValue;
}
});
cglibProducer.saleProduct(1000f);
事务动态代理
使用动态代理将事务管理引入到增强代码。
1 | public class BeanFactory { |
1 | //测试代码 |
1 | <!--配置代理的service对象--> |
Spring中的AOP
Aspect Oriented Programming,面向切面编程。在不修改源码的基础上对方法增强。减少重复代码,提高开发效率,维护方便。
相关术语
Jointpoint(连接点)
:指那些被拦截的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点。即业务层所有方法。Pointcut(切入点)
:指我们要对哪些Jointpoint
进行拦截定义。被增强的方法。Advice(通知/增强)
:指拦截到Jointpoint
之后所要做的事情就是通知。类型:前置通知(invoke之前),后置通知(invoke之后),异常通知(catch里),最终通知(finally里),环绕通知(整个invoke方法在执行就是环绕通知)。Introduction(引介)
:一种特殊的通知,在不修改类的前提下,可以在运行期为类动态地添加一些方法或Field。Target(对象)
:代理地目标对象。Weaving(织入)
:把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。Proxy(代理)
:一个类被AOP
织入增强后,就产生一个结果代理类。Aspcet(切面)
:是切入点和通知(引介)的结合。
分工
-
开发阶段(程序员)
- 编写核心业务代码(开发主线)
- 把公共代码抽取出来,制作成通知,开发阶段最后再做。
- 在配置文件中,声明切入点与通知间的关系,即切面。
-
运行阶段(Spring)
Spring框架监控切入点的执行。一旦监控到切入点方法被执行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
基于XML的AOP
-
pom.xml的配置
1
2
3
4
5
6
7
8
9
10
11
12
13<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<!--用于解析切入点表达式-->
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies> -
类设计
目标:在每个方法执行前,执行日志的打印函数。
1
2
3
4
5
6
7
8
9
10
11
12public class AccountService implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
}
public void updateAccount(int i) {
System.out.println("执行了更新");
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}1
2
3
4
5
6public class Logger {
//用于打印日志,计划让其在切入点方法执行前执行,切入点方法就是业务层方法
public void printLog(){
System.out.println("Logger中的print开始记录日志");
}
} -
配置
bean.xml
- 必要配置
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: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">
</beans>- 配置Spring的IOC,把service对象配置进去。
1
<bean id="accountService" class="com.ztygalaxy.service.impl.AccountService"></bean>
-
Spring中基于XML的AOP配置步骤
-
把通知Bean也交给Spring来管理
1
<bean id="logger" class="com.ztygalaxy.utils.Logger"></bean>
-
使用
aop:config
标签表明开始AOP的配置 -
使用
aop:aspect
标签表明开始配置切面id
:给切面提供一个唯一标识ref
:指定通知类bean的id
-
在
aop:aspect
标签的内部使用对应标签来配置通知类型aop:config
:表示配置前置通知-
method:用于指定Logger类中的哪个方法是前置通知
-
pointcut:用于指定切入点表达式,该表达式的含义是指对业务层的哪些方法增强
-
-
切入点表达式的写法:
关键字:execution(表达式)
表达式:访问修饰符 返回值 包名.包名.类名.方法名(参数列表)
标准的表达式写法:
1
2 > execution(public void com.ztygalaxy.service.impl.AccountServiceImpl.saveAccount())
>
访问修饰符可以省略:
1
2 > execution(void com.ztygalaxy.service.impl.AccountServiceImpl.saveAccount())
>
返回值可以使用通配符,表示任意返回值:
1
2 > execution(* com.ztygalaxy.service.impl.AccountServiceImpl.saveAccount())
>
包名可以使用通配符,表示任意包,但是有几级包就用几个
*.
:
1
2 > execution(* *.*.*.*.AccountServiceImpl.saveAccount())
>
包名可以用
..
表示当前包及其子包:
1
2 > execution(* *..AccountServiceImpl.saveAccount())
>
类名和方法名都可以使用
*
来进行通配:
1
2 > execution(* *..*.*())
>
参数列表可以直接写数据类型,基本类型直接写名称,引用类型写包名.类名的方式。也可以使用通配符但必须有参数。可以使用
..
表示有无参数都可,有参数为任意类型。
1
2
3 > execution(* *..*.*(int))
> //=execution(* *..*.*(*)),只执行有参数的
>
全通配写法:
1
2 > execution(* *..*.*(..))
>
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
1
2 > * com.ztygalaxy.service.impl.*.*(..)
>
1 | <aop:config> |
1 | //测试函数 |
四种常用通知类型
1 | <aop:config> |
前置通知
:在切入点方法执行之前执行后置通知
:在切入点方法正常执行之后执行异常通知
:在切入点方法执行产生异常后执行最终通知
:无论切入点方法是否正常执行,他都会在其后面执行
后置通知和异常通知只能执行一个,即四个方法中一次最多三个。
1 | <aop:aspect id="logAdvice" ref="logger"> |
环绕通知
1 | <aop:around method="aroundPrinting" pointcut-ref="pt1"></aop:around> |
1 | //Logger中增加环绕通知 |
1 | as.saveAccount(); |
- 问题:配置好了环绕通知后,切入点方法并没有执行,而环绕通知的方法执行了。
- 分析:通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明显的切入点方法调用,而我们没有。
- 解决:Spring框架为我们提供了一个接口,ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,Spring会为我们提供该接口的实现类供我们使用。
它是Spring框架为我们提供的一种可以在代码中控制增强代码何时执行的通知。
1 | //修改环绕通知 |
1 | //测试 |
基于注解的AOP
-
pom.xml
的配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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">
<!--配置spring创建时要扫描的包-->
<context:component-scan base-package="com.ztygalaxy"></context:component-scan>
<!--配置Spring开启注解AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
-
业务层配置
1
2"accountService") (
public class AccountServiceImpl implements IAccountService { -
切面层配置
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"logger") (
//表示当前类是一个切面类
public class Logger {
//指定切入点表达式
"execution(* com.ztygalaxy.service.impl.*.*(..))") (
private void pt1(){
}
//前置通知
"pt1()") (
public void beforePrintLog(){
System.out.println("前置通知Logger中的beforeprintLog开始记录日志");
}
//后置通知
"pt1()") (
public void afterReturningPrintLog(){
System.out.println("后置通知Logger中的afterReturningPrintLog开始记录日志");
}
//异常通知
"pt1()") (
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger中的afterThrowingPrintLog开始记录日志");
}
//最终通知
"pt1()") (
public void afterPrintLog(){
System.out.println("最终通知Logger中的afterPrintLog开始记录日志");
}
//环绕通知
"pt1()") (
public void aroundPrinting(ProceedingJoinPoint pjp){
System.out.println("aroundPrinting");
}
}
一些问题
使用xml可以让通知执行顺序可靠,配置文件的最终通知和后置通知在事务控制上可能会错乱,因此可以使用环绕通知。
1 | "pt1()") ( |