什么是AOP
AOP用于实现应用中的横切关注点(crosscutting concerns) 。
横切关注点是指与正常业务逻辑紧耦合的其它逻辑,例如日志、权限等等。
通过AOP,可以将不同的关注点模块化,然后应用到不同的业务逻辑中,达到复用和解耦的目的。
AOP是OOP的补充,并不能取代OOP。
AOP术语
连接点(Joinpoint):程序运行时的某个点,如调用某个方法。连接点定义了可以插入AOP逻辑的程序单元。Spring AOP中,只有方法可以定义为连接点。
建议(Advice):AOP要在连接点插入的额外逻辑。例如建议方法(连接点)执行之前先执行什么(MethodBeforeAdvice)。Spring AOP中,用Advice接口表示建议。
切点(Pointcut):连接点的集合,方便统一建议多个连接点。例如,建议某个包下的所有方法都打印访问日志。Spring AOP中用Pointcut接口表示切点。
切面(Aspect):建议+切点形成一个切面。完整地实现了“在什么地方(切点)加入什么逻辑(建议)”的AOP目标。Spring AOP中,用Advisor接口表示切面。
织入(Weaving):将切面加入到应用程序的过程。有多种织入时机,编译时、运行时、加载时(AspectJ)
目标(Target):织入AOP的目标对象,也叫被建议的对象。
引入(Introduction):给一个对象引入额外的方法或字段从而改变该对象的结构。引入可以使某个对象实现特定接口的方法,即使该对象对应的类定义时并没有implements该接口。
静态AOP和动态AOP
静态AOP,在应用构建时织入,通过修改字节码实现。静态AOP,性能更好,但每次修改都要重新编译。AspectJ的编译时织入就是一种静态AOP。
Spring AOP是动态AOP,在运行时织入,通过动态代理实现。性能比静态AOP差点,但不需要重新编译。
Spring AOP核心使用方便,又提供了诸如事务管理等基于AOP的服务,所以Spring AOP是首选。AspectJ性能更好,AOP特性支持更全面,真有这方面需求,可用AspectJ。
Spring AOP基本架构
Spring AOP的核心是代理,通过ProxyFactory控制织入和代理创建的过程。
在运行时,Spring分析IOC容器中所有bean的横切关注点(连接点),然后动态创建代理bean。调用方依赖注入时,注入的是代理bean,而不是目标的bean。然后代理bean会根据运行条件织入相应的建议。
Spring支持两种代理实现:JDK动态代理和CGLIB代理。对于实现特定接口的目标对象,使用JDK动态代理,否则使用CGLIB代理。
Spirng AOP中,Advisor接口表示切面(切点+建议),Advisor又有两个子接口: PointcutAdvisor和IntroductionAdvisor,分别用于实现基于切点和引入的AOP逻辑。
示例:通过AOP,改变特工(Agent)的说话方式(speak方法)
package demo.aop;
public class Agent {
//连接点
public void speak() {
System.out.print("Bond");
}
}
//MethodInterceptor是一个典型的建议
public class AgentDecorator implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.print("James ");//方法执行前干嘛
Object object=invocation.proceed();
System.out.println("!");//方法执行后干嘛
return object;
}
}
package demo.aop;
import org.springframework.aop.framework.ProxyFactory;
public class AgentAOPDemo {
public static void main(String[] args) {
Agent target=new Agent();//目标对象
ProxyFactory proxyFactory=new ProxyFactory();//代理工厂,Spring AOP核心类
proxyFactory.addAdvice(new AgentDecorator());//添加建议
proxyFactory.setTarget(target);//添加目标
Agent proxy= (Agent) proxyFactory.getProxy();//创建代理
target.speak();
System.out.println();
proxy.speak();//调用代理的方法,目标对象的方法逻辑被改变
}
}
ProxyFactory 用于编程方式的AOP织入和代理创建。
ProxyFactory的addAdvice方法添加建议,实际上是添加切面。通过判断参数类型,创建相应的Advisor实现。这里实际上是addAdvisor(pos, new DefaultPointcutAdvisor(advice))。
DefaultPointcutAdvisor属于PointcutAdvisor一种实现,适用于任意切点和建议,所以会对目标对象的所有连接点(方法)执行建议。
ProxyFactory的setTarget方法添加目标对象,实际是在父类AdvisedSupport中实现的。通过 new SingletonTargetSource(target),这是一个静态的TargetSource,所以每次都是直接返回target。
ProxyFactory的getProxy()方法创建代理。实际上是通过父类ProxyCreatorSupport创建默认的AOP代理工厂DefaultAopProxyFactory。
DefaultAopProxyFactory是AopProxyFactory接口的默认实现类,用于创建JDK动态代理或CGCLIB代理。当isOptimize、isProxyTargetClass、 hasNoUserSuppliedProxyInterfaces条件之一满足时,使用CGLIB代理,否则使用JDK动态代理。
所以,这里使用的是CGLIB代理。return new ObjenesisCglibAopProxy(config); 它调用父类CglibAopProxy构造函数,通过AdvisedDispatcher将配置分发到目标对象,Dispatcher for any methods declared on the Advised class.
Spring AOP中的建议
Spring AOP中用Advice接口表示建议。它是一个标记接口,没有任何方法。这意味着建议可用不同的方式实现,例如拦截器(Interceptors)。
Interceptor接口也没有任何方法,要实现某种建议,就要使用其对应的子接口,如MethodInterceptor。
建议 | 接口 | 功能 |
---|---|---|
Before | MethodBeforeAdvice | 方法执行前的建议,如认证和权限管理 |
After-Returning | AfterReturningAdvice | 方法正常返回之后的建议 |
After(finally) | AfterAdvice | 方法返回之后的建议,包括异常之后 |
Around | MethodInterceptor | 适用于各种情况 |
Throws | ThrowsAdvice | 方法抛出异常之后的建议,例如错误日志集中处理 |
Introduction | IntroductionInterceptor | 引入额外的方法 |
Spring AOP中的切点
Spring AOP中用Pointcut接口表示切点。
public interface Pointcut {
ClassFilter getClassFilter ();
MethodMatcher getMethodMatcher();
}
Spring分析一个Pointcut是否应用于某个方法时,先用该类过滤器ClassFilter来匹配目标方法所在类。
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
类匹配成功后,再匹配方法:
public interface MethodMatcher {
boolean matches(Method m, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method m, Class<?> targetClass, Object[] args);
}
isRuntime()表示静态匹配还是动态匹配。
如果是静态匹配,第一次检查时调用matches(Method m, Class<?> targetClass)方法,缓存结果,之后再检查时直接返回结果。
如果是动态匹配,matches(Method m, Class<?> targetClass)方法返回true后,再进一步调用matches(Method m, Class<?> targetClass, Object[] args)方法,该方法可以对调用时的参数进行匹配。
所以静态匹配性能好,动态匹配精度高。
Spring 提供的切点实现
Spring已经提供了很多切点实现类:
类名 | 功能 |
---|---|
DynamicMethodMatcherPointcut | 便于实现动态匹配的基类 |
StaticMethodMatcherPointcut | 便于实现静态匹配的基类 |
AnnotationMatchingPointcut | 基于Java注解匹配类或方法 |
AspectJExpressionPointcut | 基于AspectJ表达式匹配 |
JdkRegexpMethodPointcut | 基于正则表达式匹配 |
NameMatchMethodPointcut | 基于方法名匹配 |
ComposablePointcut | 用于组合多个切点,union,intersection |
ControlFlowPointcut | 特殊的切点,用于匹配定义在某个方法内的所有方法 |
示例:使用AspectJ表达式定义切点
首先需要添加AspecJ相关依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
package demo.aop;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class AgentAOPDemo {
public static void main(String[] args) {
//使用AspectJ表达式定义的切点
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
pc.setExpression("execution(* speak(..))");//所有speak方法
//使用DefaultPointcutAdvisor
Advisor advisor = new DefaultPointcutAdvisor(pc, new AgentDecorator());
ProxyFactory proxyFactory=new ProxyFactory();//代理工厂,Spring AOP核心类
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(new Agent());//添加目标
Agent proxy= (Agent) proxyFactory.getProxy();//创建代理
proxy.speak();//调用代理的方法,目标对象的方法逻辑被改变
}
}
Spring AOP中的Introduction
关于Introduction怎么翻译,我的理解是“引入”,即为目标对象引入新的功能。
使用Spring Introduction可以为目标对象动态引入任意接口的方法。
Spring把Introduction看作一种特殊的around建议(继承了MethodInterceptor),由IntroductionInterceptor接口表示。
Spring提供了DelegatingIntroductionInterceptor类,方便我们实现IntroductionInterceptor接口。所以,要构建一个Introduction,只要继承DelegatingIntroductionInterceptor类,同时实现我们要引入的方法所在的接口。
与PointcutAdvisor相对应,基于引入的AOP切面用IntroductionAdvisor接口表示,Spring也提供了默认实现类DefaultIntroductionAdvisor。
Introduction和Advice的区别
首先Introduction也是一种建议,它本质上是一种around advice。
使用基于Advice的建议时,一般可以把同一个建议实例应用到多个对象,这种方式叫:per-class life cycle。
而使用基于Introduction的建议时,建议成了目标对象的一部分,所以必须为每个目标对象使用唯一的建议实例。这种方式叫:per-instance life cycle。
示例:检查对象是否被修改
检查对象是否被修改,可以定义IsModified接口,然后构建一个实现该接口的Introduction建议,当把改Introduction建议应用到目标对象时,就具备检查对象是否被修改过的能力了。
IsModified接口:判断对象是否被修改
package demo.aop;
public interface IsModified {
boolean isModified();
}
IsModifiedMixin类:定义一个引入了IsModified接口的Introduction建议。
package demo.aop;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class IsModifiedMixin extends DelegatingIntroductionInterceptor implements IsModified {
private boolean isModified = false;//对象被修改标志
//缓存setter-getter方法对,避免每次都通过反射获取getter方法
private Map<Method, Method> methodCache = new HashMap<>();
@Override
public boolean isModified() {
return isModified;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (!isModified) {
//setter方法被调用时,对象可能被修改
if ((invocation.getMethod().getName().startsWith("set"))
&& (invocation.getArguments().length == 1)) {
Method getter = getGetter(invocation.getMethod());
if (getter != null) {
Object newVal = invocation.getArguments()[0];
Object oldVal = getter.invoke(invocation.getThis(), null);
if ((newVal == null) && (oldVal == null)) {
isModified = false;
} else if ((newVal == null) && (oldVal != null)) {
isModified = true;
} else if ((newVal != null) && (oldVal == null)) {
isModified = true;
} else {
isModified = !newVal.equals(oldVal);
}
}
}
}
//一定要调用父类的invoke方法,因为是由DelegatingIntroductionInterceptor负责分发该方法调用的
return super.invoke(invocation);
}
/**
* 通过setter方法获取getter方法,使用反射
* @param setter setter方法
* @return 对应的getter方法
*/
private Method getGetter(Method setter) {
Method getter = methodCache.get(setter);
if (getter != null) {
return getter;
}
String getterName = setter.getName().replaceFirst("set", "get");
try {
getter = setter.getDeclaringClass().getMethod(getterName, null);
synchronized (methodCache) {
methodCache.put(setter, getter);
}
return getter;
} catch (NoSuchMethodException ex) {
return null;
}
}
}
封装成一个基于Introduction的切面:
package demo.aop;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
public class IsModifiedAdvisor extends DefaultIntroductionAdvisor {
public IsModifiedAdvisor() {
super(new IsModifiedMixin());
}
}
目标对象所在类:
package demo.aop;
public class Contact {
private String name;
private String phoneNumber;
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
演示类:
package demo.aop;
import org.springframework.aop.IntroductionAdvisor;
import org.springframework.aop.framework.ProxyFactory;
public class IntroductionDemo {
public static void main(String[] args) {
Contact target = new Contact();
target.setName("John Mayer");
IntroductionAdvisor advisor = new IsModifiedAdvisor();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
pf.setOptimize(true);//强制使用CGLIB代理
Contact proxy = (Contact) pf.getProxy();//如果是JDK代理,这里会报错,因为proxy并没有实现Contact接口
IsModified proxyInterface = (IsModified)proxy;
System.out.println("Is Contact?: " + (proxy instanceof Contact));
System.out.println("Is IsModified?: " + (proxy instanceof IsModified));
System.out.println("Has been modified?: " + proxyInterface.isModified());
proxy.setName("John Mayer");
System.out.println("Has been modified?: " + proxyInterface.isModified());
proxy.setName("Eric Clapton");
System.out.println("Has been modified?: " + proxyInterface.isModified());
}
}
声明式AOP配置
ProxyFactory用于编程式AOP实现,实际开发中一般不直接使用,而是使用声明式AOP配置。
Spring支持三种声明式AOP配置:
- ProxyFactoryBean:实现了FactoryBean,告诉SpringApplicationContext通过一个bean去创建代理,一般不使用。
- Spring aop namespace:在基于xml风格的SpringApplicationContext配置中使用
- @AspectJ-style annotations:在基于Java注解风格的SpringApplicationContext配置中使用。
示例:使用AspectJ风格注解配置AOP
package demo.aop;
import org.springframework.stereotype.Component;
@Component
public class Agent {
//连接点
public void speak() {
System.out.print("Bond");
}
}
package demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AgentDecorator {
@Pointcut("execution(* speak(..))")
public void speakPointCut() {
}
@Around("speakPointCut()")
public Object speakAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.print("James ");
Object val= proceedingJoinPoint.proceed();
System.out.println("!");
return val;
}
}
package demo.aop;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.GenericApplicationContext;
@Configuration
@ComponentScan("demo.aop")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AgentAOPDemo {
public static void main(String[] args) {
GenericApplicationContext context=new AnnotationConfigApplicationContext(AgentAOPDemo.class);
Agent agent= (Agent) context.getBean("agent");//spring 会获取代理,而不是原来的bean
agent.speak();
}
}
总结
- AOP作为OOP的补充,用于解决应用中的横切问题。横切问题就是遍布在各个对象正常业务逻辑中的系统关注点,例如日志记录、权限控制等等。
- AOP两个核心概念:建议和切点。建议是横切问题的抽象,即我们要AOP额外干什么。切点即切入点,也就是把建议用到什么地方。两者结合就是“在什么地方再干点什么”。
- Spring 提供了两类建议:Advice和Introduction。Introduction是一种特殊的建议,可以为目标对象引入任意接口的方法。
- Spring AOP的核心是代理,通过创建代理对象,将实际方法调用转发到代理对象,从而实现AOP织入。Spring提供两种代理对象:JDK动态代理和CGLIB代理。
- ProxyFactory用于编程式AOP实现,实际开发中一般不直接使用,而是用基于AOP的服务,如声明式事务管理。