1.AOP概述
1.1.AOP到底是什么
AOP只适合那些具有横切面逻辑的应用场合,如性能监测,访问控制,事务管理及日志记录
1.2.AOP术语
- 连接点(JoinPoint):特定点是程序执行某个特定位置,如初始化开始前,初始化开始后等等。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就被称为“连接点”。Spring仅支持方法的连接点,仅能在方法调用前、方法调用后、方法抛出异常时及方法调用前后这些程序执行点织入增强。
- 切点(Pointcut):每个程序类都有多个连接点,就好像每个类有多个方法一样,那么如何定位感兴趣的连接点呢?AOP通过"切点"来定位特定的连接点。在Spring中,切点通过Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。
- 增强(Advice):增强是织入目标类连接点上的一段程序代码,换句话说增强就是添加到目标连接点上的一段程序逻辑。并且Spring所提供的增强接口都是带方位名的,如BeforeAdvice等等。只有结合切点和增强,才能确定特定的连接点并实施增强逻辑。
- 目标对象(Target):增强逻辑织入目标类,如果没有AOP,那么目标业务类要自己实现所有的逻辑。在AOP的帮助下,程序只需要实现非横切逻辑的代码部分,而性能监视和事务管理等这些横切逻辑则可以使用AOP动态织入特定的连接点上。
- 引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
- 织入(Weaving):织入是将增强添加到目标类的具体连接点上的过程,将目标类、增强或者引介天衣无缝地编织到一起。AOP的3种织入方式:
- 编译期织入:要求使用特殊的Java编译器
- 类装载期织入:要求使用特殊的类装载器
- 动态代理织入:在运行期为目标类添加增强生成子类的方式
- 代理(Proxy):一个类被AOP织入增强之后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以可以采用与调用原类相同的方式调用代理类。
- 切面(Aspect):切面由切点和增强(引介)组成,它既包括横切逻辑的定义,也包括连接点的定义。AOP的工作重心在于如何将增强应用于目标对象的连接点上,包括以下两点
- 通过切点和增强定位到连接点上
- 在增强中编写切面的代码
1.3.AOP的实现者
- AspectJ
- AspectWerkz
- JBossAOP
- SpringAOP
2.基础知识
Spring AOP使用动态代理技术在运行时织入增强代码,Spring AOP使用两种代理机制:一种
基于JDK的动态代理,另一种是基于CGLib的动态代理。之所以需要两种代理机制,是因为很大程度上JDK本身只提供接口的代理,而不支持类的代理。
所有实例的代码地址
Aop实例的github地址
2.1.带有横切面逻辑的实例
以下是通过代码实现,当某个方法需要进行性能监视时,必须要调整方法代码,在方法体前后分别添加开启性能监视和结束性能监视的代码。
public interface ForumService {
public void removeTopic(int topicID);
public void removeForum(int topicID);
}
package com.aop.impl;
public class ForumServiceImpl implements ForumService {
public void removeTopic(int topicID){
//开始对该方法进行性能监视
PerformanceMonitor.begin("removeTopic");
System.out.println("模拟删除论坛topic记录:"+topicID);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//结束对该方法的性能监视
PerformanceMonitor.end();
}
public void removeForum(int topicID){
//开始对该方法进行性能监视
PerformanceMonitor.begin("removeForum");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("模拟删除Forum记录:"+topicID);
//结束对该方法的性能监视
PerformanceMonitor.end();
}
}
public class MethodPerformance {
private long begin;
private long end;
private String serviceMethod;
public MethodPerformance(String serviceMethod) {
this.serviceMethod = serviceMethod;
this.begin = System.currentTimeMillis();//记录目标类方法开始执行点的开始时间
}
public void printPerformance() {
end = System.currentTimeMillis();//获取目标类方法执行完成后的系统时间,进而计算出目标类方法的执行时间
long elapse = end - begin;
System.out.println(serviceMethod+"花费"+elapse+"毫秒");
}
}
public class PerformanceMonitor {
//通过一个ThreadLocal保存与调用线程相关的性能监视信息
private static ThreadLocal<MethodPerformance> performanceRecord = new ThreadLocal<>();
//启动对某一目标方法的性能监视
public static void begin(String method){
System.out.println("begin monitor....");
MethodPerformance mp=new MethodPerformance(method);
performanceRecord.set(mp);
}
public static void end(){
System.out.println("end monitor");
MethodPerformance mp=performanceRecord.get();
//打印出方法性能监视的结果信息
mp.printPerformance();
}
}
public class TestForumService {
public static void main(String[] args) {
ForumService forumService=new ForumServiceImpl();
forumService.removeForum(10);
forumService.removeTopic(20);
}
}
2.2.JDK动态代理
public class PerformanceHandler implements InvocationHandler {
private Object target;
public PerformanceHandler(Object target) {//target为目标业务类
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
PerformanceMonitor.begin(target.getClass().getName()+"."+method.getName());
Object obj=method.invoke(target,args);//通过反射调用业务类的目标方法
PerformanceMonitor.end();
return obj;
}
}
public class TestForumService {
public static void main(String[] args) {
ForumService target = new ForumServiceImpl();
PerformanceHandler handler = new PerformanceHandler(target);
ForumService proxy= (ForumService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),handler);
proxy.removeForum(10);
proxy.removeTopic(1020);
}
}
2.3.CGLib动态代理
由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final或private方法进行代理
public class CGLibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz);//设置需要创建子类的类
enhancer.setCallback(this);
return enhancer.create();//通过字节码技术动态创建子类实例
}
@Override
public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {//拦截父类所有方法
PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName());
Object result=methodProxy.invokeSuper(obj,objects);//通过反射调用业务类的目标方法
PerformanceMonitor.end();
return result;
}
}
public class TestForumService {
public static void main(String[] args) {
CGLibProxy proxy=new CGLibProxy();
ForumServiceImpl forumService= (ForumServiceImpl) proxy.getProxy(ForumServiceImpl.class);//通过动态生成子类的方式创建 代理类
forumService.removeTopic(10);
forumService.removeForum(1024);
}
}
2.4.AOP联盟
2.5.代理知识小结
虽然通过上面的PerformanceHandler或CGLibProxy实现了性能监视横切逻辑的动态织入,但是这种实现方式存在3个明显需要改进的地方
- 目标类的所有方法都添加了性能监视横切逻辑,而有时这并不是我们所期望的,我们可能只希望对业务中的某些特定方法添加横切面逻辑
- 通过硬编码的方式指定了织入横切逻辑的织入点,即在目标类业务方法的开始和结束前织入代码
- 手工编写代理实例的创建过程,在为不同的类创建代理时,需要分别编写相应的创建代码,无法做到通用
JDK动态代理所创建的代理对象,在Java 1.3下,性能差强人意,研究表明CGLib所创
建的动态代理对象的性能依旧比JDK所创建的动态代理对象的性能高不少(大概10倍),但是CGLib在创建代理对象时所花费的时间却比JDK动态代理多(大概8倍)。对于singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理技术,反之则适合采用JDK动态代理技术
3.创建增强类
Spring使用增强类定义了横切面逻辑,同时由于Spring只支持方法连接点,增强还包括方法在哪一点加入横切代码的方位信息,所以增强既包含横切逻辑,又包含部分连接点的信息
3.1.增强类型
- 前置增强:在目标方法前实施增强
- 后置增强:在目标方法后实施增强
- 环绕增强:在目标方法前后实施增强
- 异常抛出增强:表示在目标方法抛出异常后实施增强
- 引介增强:在目标类中添加一些新的属性和方法
3.2.前置增强
通过实现MethodBeforeAdvice接口
public interface Waiter {
public void greetTo(String name);
public void serveTo(String name);
}
public class NativeWaiter implements Waiter {
@Override
public void greetTo(String name) {
System.out.println("greet to "+name+"...");
}
@Override
public void serveTo(String name) {
System.out.println("serving to "+name+"...");
}
}
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object o) throws Throwable {//在目标方法调用前使用
String clientName= (String) args[0];
System.out.println("How are you! Mr."+clientName);
}
}
public class BeforeAdviceTest {
@Test
public void before() {
System.out.println("测试代码");
Waiter target = new NativeWaiter();
BeforeAdvice advice=new GreetingBeforeAdvice();
//spring提供代理工厂
ProxyFactory pf=new ProxyFactory();
//设置代理目标
pf.setTarget(target);
//为代理目标添加增强
pf.addAdvice(advice);
//生成代理实例
Waiter proxy= (Waiter) pf.getProxy();
proxy.greetTo("John");
proxy.serveTo("Tom");
}
}
上面代码中使用ProxyFactory代理,ProxyFactory内部使用的就是JDK或者是CGLib动态代理技术将增强应用到目标类中的。如果通过ProxyFactory的setInTerfaces方法指定目标接口进行代理,则使用JDK代理,如果是针对类代理,则使用CGLib代理。此外还可以通过setOptimize方法让ProxyFactory启动优化代理方式。
3.3.后置增强
后置增强和前置增强的代码类似,前置增强要实现接口MethodBeforeAdvice,后置增强则要实现接口AfterReturningAdvice
public class GreetingAfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("enjoy yourself!");
}
}
3.4.环绕增强
环绕增强综合实现了前置、后置增强的功能
public class GreetingInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {//截获目标类方法的执行,并在前后添加横切逻辑
Object[] args = methodInvocation.getArguments();
String clientName = (String) args[0];
System.out.println("How are you !Mr."+clientName+".");//在目标方法前执行
Object obj=methodInvocation.proceed();//通过反射机制调用目标方法
System.out.println("Please enjoy yourself!");
return obj;
}
}
3.5.异常抛出异常
异常抛出异常最适合的应用场景是事务管理,当参与事务的某个DAO发生异常时,事务管理器就必须回滚事务
ThrowAdvice异常抛出增强接口没有定义任何方法,它是一个标签接口,在运行期Spring使用反射机制自行判断,必须采用以下签名形式定义异常抛出的增强方法。
public class TransactionManager implements ThrowsAdvice {
//定义增强逻辑
public void afterThrowing(Method method,Object[] args,Object target,Exception ex) throws Throwable{
System.out.println("--------------");
System.out.println("method:"+method.getName());
System.out.println("抛出异常:"+ex.getMessage());
System.out.println("成功回滚事务!");
}
}
3.6.引介增强
引介增强是为目标类创建新的方法和属性,所以引介增强的连接点是类级别的,而非方法级别的,通过引介类增强,可以为目标类添加一个接口的实现,即原来目标类未实现某个方法接口,通过引介增强可以为目标类创建实现某接口的代理。
public class ControllablePerformanceMonitor extends DelegatingIntroductionInterceptor implements Monitorable {
private ThreadLocal<Boolean> MonitorStatusMap = new ThreadLocal<>();
@Override
public void setMonitorActive(boolean active) {
MonitorStatusMap.set(active);
}
//拦截方法
public Object invoke(MethodInvocation mi) throws Throwable {
Object obj = null;
//对于性能监视可控代理,通过判断其状态决定是否开启性能监控功能
if(MonitorStatusMap.get()!=null&&MonitorStatusMap.get()){
PerformanceMonitor.begin(mi.getClass().getName()+"."+mi.getMethod().getName());
obj=super.invoke(mi);
PerformanceMonitor.end();
}else {
obj=super.invoke(mi);
}
return obj;
}
}
4.创建切面
增强被织入目标类的所有方法中,假设我们希望有选择地织入目标类的某些特定的方法中,就需要使用切点进行目标连接点的定位。
Spring通过Pointcut接口来描述切点,Pointcut由ClassFilter和MethodMatcher构成,它通过ClassFilter定位到某些特定类上,通过MethodMatcher定位到某些特定方法上,这样Pointcut就拥有了描述某些类的某些特定方法的能力。
4.1.切点类型
- 静态方法切点:匹配所有的类
- 动态方法切点:匹配所有的类
- 注解切点:表示注解切点
- 表达式切点:支持AspectJ切点表达式语法而定义的接口
- 流程切点:根据程序执行堆栈的信息查看目标方法是否由某一个方法直接或间接发起调用,以此判断是否为匹配的连接点
- 复合切点:使用链接表达式对切点进行操作
4.2.切面类型
- Advisor:代表一般切面,仅包含一个Advice
- PointcutAdvisor:代表具有切点的切面,包含Advice和Pointcut两个类
- IntroductionAdvisor:代表引介切面
4.3.静态普通方法名匹配切面
public class GreetingBeforeAdvisor extends StaticMethodMatcherPointcutAdvisor {
@Override
public boolean matches(Method method, Class<?> clazz) {
return "greetTo".equals(method.getName());
}
public ClassFilter getClassFilter(){
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return Waiter.class.isAssignableFrom(clazz);
}
};
}
}
4.4.静态正则表达式方法切面
<!--静态正则表达式方法匹配切面-->
<bean id="regexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
p:advice-ref="greetingBefore">
<property name="patterns">
<list>
<value>.*greet.*</value><!--定义可匹配模式串,“.*greet.*”-->
</list>
</property>
</bean>
<bean id="waiter1"class="org.springframework.aop.framework.ProxyFactoryBean"
p:proxyTargetClass="true"
p:interceptorNames="regexpAdvisor"
p:target-ref="waiterTarget"
/>
4.5.动态切面
Spring会在创建动态代理织入切面时,对目标类中所有方法进行静态切点检查;在生成织入切面的代理对象后,第一次调用代理类的每一个方法时都会进行一次静态切点检查,如果本次检查就能从候选者列表中将该方法排除,则以后对该方法的调用就不再执行静态切点检查;对于那些在静态切点检查时匹配的方法,在后续调用该方法时,将执行动态切点检查
public class GreetingDynamicPointcut extends DynamicMethodMatcherPointcut {
private static List<String> specialClientList=new ArrayList<>();
static{
specialClientList.add("Jhon");
specialClientList.add("Tom");
}
public ClassFilter getClassFilter(){
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
System.out.println("调用getClassFilter()对"+clazz.getName()+"静态检查.");
return Waiter.class.isAssignableFrom(clazz);
}
};
}
@Override
public boolean matches(Method method, Class<?> clazz, Object... objects) {//对方法进行动态切点检查
System.out.println("调用动态检查方法对"+clazz.getName()+"类的"+method+"动态检查.");
String clientName= (String) objects[0];
return specialClientList.contains(clientName);
}
@Override
public boolean matches(Method method, Class<?> clazz) {//对方法进行静态切点检查
System.out.println("调用静态检查方法对"+clazz.getName()+"类的"+method+"静态检查.");
return "greetTo".equals(method.getName());
}
}
<!--动态切面-->
<bean id="dynamicAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut">
<bean class="cn.com.dynamicPoint.GreetingDynamicPointcut"/>
</property>
<property name="advice">
<bean class="cn.com.beforeEnhancer.GreetingBeforeAdvice"/>
</property>
</bean>
<bean id="waiter2" class="org.springframework.aop.framework.ProxyFactoryBean"
p:interceptorNames="dynamicAdvisor"
p:target-ref="waiterTarget"
p:proxyTargetClass="true"
/>
每次调用代理对象的任何一个方法,都会执行动态切点检查,这将导致很大的性能问题,所以,在定义动态切点时,切勿忘记同时覆盖getClassFilter()matches(Metho method,Class clazz)方法,通过静态切点检查排除大部分方法
4.6.流程切面
流程切点代表由某个方法直接或间接发起调用的其他方法。流程切面和动态切面从某种程度上来说可以算是一类切面,因为二者都需要在运行期判断动态的环境。对于流程切面来说,代理对象在每次调用目标类方法时,都需要判断方法调用堆栈中是否有满足流程切点要求的方法,因此,和动态切面一样,流程切面对性能的影响也很大。
public class WaiterDelegate {
private Waiter waiter;
public void service(String clientName){//waiter的方法通过该方法发起调用
waiter.serveTo(clientName);
waiter.greetTo(clientName);
}
public void setWaiter(Waiter waiter) {
this.waiter = waiter;
}
}
<!--流程切面-->
<bean id="controlFlowPointcut" class="org.springframework.aop.support.ControlFlowPointcut">
<constructor-arg type="java.lang.Class">
<value>cn.com.flowPoint.WaiterDelegate</value>
</constructor-arg>
<constructor-arg type="java.lang.String">
<value>service</value>
</constructor-arg>
</bean>
<bean id="controlFlowAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
p:pointcut-ref="controlFlowPointcut"
p:advice-ref="greetingBefore"
/>
<bean id="waiter3" class="org.springframework.aop.framework.ProxyFactoryBean"
p:interceptorNames="controlFlowAdvisor"
p:target-ref="waiterTarget"
p:proxyTargetClass="true"
/>
4.7.符合切点切面
有时,一个切点可能难以描述目标连接点的信息,比如在前面的例子中,假设我们希望WaiterDelegate#service发起调用且被调用的方法是Waiter#greetTo时才织入增强,这个切点就是符合切点。
public class GreetingComposablePointcut {
public Pointcut getInTersectionPointcut(){
ComposablePointcut cp=new ComposablePointcut();//创建一个复合切点
Pointcut pt1=new ControlFlowPointcut(WaiterDelegate.class,"service");//创建一个流程切点
NameMatchMethodPointcut pt2=new NameMatchMethodPointcut();//创建方法名切点
pt2.addMethodName("greetTo");
return cp.intersection(pt1).intersection((Pointcut) pt2);
}
}
<!--复合切点切面-->
<bean id="gcp" class="cn.com.composablePointcut.GreetingComposablePointcut"></bean>
<bean id="composableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
p:pointcut="#{gcp.inTersectionPointcut}"
p:advice-ref="greetingBefore"
/>
<!--引用gcp.inTersectionPointcut方法返回的复合切点-->
<bean id="waiter4" class="org.springframework.aop.framework.ProxyFactoryBean"
p:interceptorNames="composableAdvisor"
p:target-ref="waiterTarget"
p:proxyTargetClass="true"
/><!--使用复合切点-->
4.8.引介切面
5.自动创建代理
在前面的例子中,都通过ProxyFactoryBean创建织入切面的代理,每个需要被代理的Bean都需要使用一个ProxyFactoryBean进行配置,虽然可以使用父子<bean>进行改造,但还是很麻烦,Spring提供了自动代理机制,让容器自动生成代理,可以从繁琐的配置中解放出来。Spring使用BeanPostProcessor自动完成这项工作
5.1.实现类介绍
这些基于BeanPostProcessor的自动代理创建器的实现类,将根据一些规则自动在容器实例化Bean时为匹配的Bean生成自动代理实例。
- 基于Bean配置名规则的自动代理创建器:允许为一组特定配置名的Bean自动创建代理实例的代理创建器,实现类为BeanNameAutoProxyCreator
- 基于Advisor匹配机制的自动代理创建器:它会对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中(为目标Bean创建带实例),实现类为DefaultAdvisorAutoProxyCreator
- 基于Bean中AspectJ注解标签的自动代理创建器:为包含AspectJ注解的Bean自动创建代理实例,实现类为AnnotationAwareAspectJAutoProxyCreator
5.2.BeanNameAutoProxyCreator
<!--自动代理BeanNameAutoProxyCreator-->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"
p:beanNames="*er"
p:interceptorNames="greetingBefore"
p:optimize="true"
/>
5.3.DefaultAdvisorAutoProxyCreator
DefaultAdvisorAutoProxyCreator能够扫描容器中的Advisor,并将Advisor自动织入匹配的目标Bean中,即为匹配的目标Bean自动创建代理
<bean id="regexpAdvisor1" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
p:patterns=".*greet.*"
p:advice-ref="greetingBefore"
/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>
5.4.AOP无法增强疑难问题剖析
因为AOP底层的实现原理基于JDK和CGLib动态代理,在JDK动态代理中通过接口来实现方法拦截,所以必须保证要拦截的目标方法在接口中有定义,在CGLib动态代理中通过动态生成代理子类来实现方法拦截,所以必须要确保拦截的目标方法可被子类访问,也就是目标方法必须定义为非final,即非私有实例方法
<bean id="regexpAdvisor1" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
p:pattern=".*To.*"
p:advice-ref="greetingBefore"
/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>
<!--修改一下自动代理的正则表达式,所有方法中包含To的都要织入增强,-->
public class NativeWaiter implements Waiter {
@Override
public void greetTo(String name) {
//throw new RuntimeException("运行异常");
System.out.println("greet to "+name+"...");
}
@Override
public void serveTo(String name) {
greetTo(name);
System.out.println("serving to "+name+"...");
}
//只调用serveTo方法,serverTo和greetTo方法能否可以同时增强
/**
console打印的日志如下
How are you! Mr.Jhon
greet to Jhon...
serving to Jhon...
表明被调用的方法没有织入增强
**/
}
>在调用内部方法时,让其通过代理类调用内部的方法来解决内部方法不能被代理的情况,因此,需要让原来的Waiter实现一个可注入自身代理类的接口
注:通过配置代理类来调用内部方法还是不能够给方法织入增强,关于这点如果有大神知道,请告知,我后面也会持续研究这个问题,如果有进展会第一时间更新文章
网友评论