1. 创建切面
增强提供了连接点方位信息,如织入到方法前面,后面等,而切点进一步描述了织入哪些类的哪些方法上。
Spring通过 org.springframework.aop.Pointcut
接口描述切点,Pointcut由ClassFilter和MethodMatcher构成,通过ClassFilter定位到特定类上,通过MethodMatcher定位到某些特定方法上,这样Pointcut就拥有了描述某些类的某些特定方法的能力。
注: ClassFilter只定义了方法matches(Class clazz),参数代表一个被检测类,该方法判断被检测的类是否匹配过滤条件
Spring支持两种方法匹配器(MethodMatcher):静态方法匹配器和动态方法匹配器。
- 静态方法匹配器: 仅对方法名签名(包括方法名和入参类型及顺序)进行匹配。仅判别一次
- 动态方法匹配器: 会在运行期检查方法入参的值。每次调用方法都必须判断(因为每次调用方法的入参可能不一样),所以动态匹配对性能的影响很大。一般,动态匹配不常使用。
- 方法匹配器的类型由
isRuntime()
方法的返回值决定,返回false表示的是静态方法匹配器,返回true表示是动态方法匹配器
1. 切点类型
2. 切面类型
Spring使用
org.springframework.aop.Advisor
接口表示切面的概念,一个切面同时包含横切代码和连接点信息
切面的分类:
- ① Advisor(一般切面): 仅包含一个
Advice
。因为Advice包含了横切代码和连接点信息,所以Advice(增强)就是一个简单的切面,只不过它代表的横切的连接点是目标类的所有方法,因为这个横切面太宽泛,一般不会直接使用 - ② PointcutAdvisor(切点切面): 包含
Advice
和Pointcut
两个类,可以通过类,方法名及方法方位等信息灵活地定义切面的连接点,提供更具适用性的切面。 - ③ IntroductionAdvisor(引介切面): 它对应引介增强的特殊的切面,应用于类层面上,所以引介切点用
ClassFilter
进行定义
- ① Advisor(一般切面): 仅包含一个
PointcutAdvisor的主要实现类体系:
- DefaultPointcutAdvisor: 最常用切面类型,可以通过任意Pointcut和Advice定义一个切面,唯一不支持的是引介的切面类型,一般可以通过扩展该类实现自定义的切面
- NameMatchMethodPointcutAdvisor: 通过该类可以定义按方法名定义切点的切面
- RegexpMethodPointcutAdvisor: 对于按正则表达式匹配方法名进行切点定义的切面,可以通过扩展实现类进行操作。允许用户以正则表达式模式串定义方法匹配的切点,其内部通过JdkRegexpMethodPointcut构造出正则表达式方法名切点
- StaticMethodMatcherPointcutAdvisor: 静态方法匹配器切点定义的切面,默认情况下匹配所有的目标类
- AspectJExpressionPointcAdvisor: 用于AspectJ切点表达式定义切点的切面
- AspectJPointcutAdvisor: 用于AspectJ语法定义切点的切面
3. 静态普通方法名匹配切面
StaticMethodMatcherPointcutAdvisor
代表一个静态方法匹配切面,通过StaticMethodMatcherPointcut
来定义切点,并通过类过滤和方法名来匹配定义的切点
希望通过StaticMethodMatcherPointcutAdvisor定义一个切面,在Waiter#greetTo()方法调用前织入一个增强,即连接点为Waiter#greetTo()方法调用前的位置:
StaticMethodMatcherPointcutAdvisor抽象类唯一需要定义的是matches()方法,在默认情况下,该切面匹配所有的类,这里通过覆盖getClassFilter()方法,让它仅匹配Waiter类及其子类
Advisor还需要一个增强类的配置,为Advisor定义一个前置增强
可以直接通过ProxyFactory,通过手工编码的方式织入切面生成代理类
如下通过Spring配置来定义切面:
注: 在①处,将greetAdvice增强配置到greetingAdvisor切面中。
StaticMethodMatcherPointcutAdvisor
的属性:
- advice属性
- classFilter: 类匹配过滤器,在GreetingAdvisor中用编码的方式设定了classFilter
- order: 切面织入时的顺序,该属性用于定义Ordered接口表示的顺序
由于需要分别为waiter和seller两个Bean定义代理器,而且两者有很多公共的配置信息,所以使用一个父<bean>
简化配置,在③和④,通过引用父<bean>
定义了两个织入切面的代理:
输出:
4. 静态正则表达式方法匹配切面
实例:
测试代码:
输出:
注:- patterns:
- advice-ref:
- pattern: 如果只有一个匹配模式串,则可以使用该属性进行配置。patterns属性用于定义多个匹配模式串,这些匹配模式串之间是“或”的关系
- order: 切面在织入时对应的顺序
- 正则表达式的语法 :
5. 动态切面
- 使用DefaultPointcutAdvisor和DynamicMethodMatcherPointcut可以创建动态切面。
- DynamicMethodMatcherPointcut是一个抽象类,将isRuntime()标识为final且返回true,这样子类就一定是一个动态切点
- 该抽象类默认匹配所有的类和方法,因此需要通过扩展该类编写复合要求的动态切点
注: spring2.0 中已经过期
注:
- 由于动态切点检查对性能会造成很大的影响,所以应当尽量避免在运行时每次都对目标类的每个方法进行动态检查
- 在动态切点类中定义静态切点检查的方法可以避免不必要的动态检查操作,从而极大地提高运行效率
Spring采用这样的机制:
- 在创建代理时对目标类的每个连接点使用静态切点检查
- 如果仅通过静态切点检查就可以知道连接点不匹配,则在运行时不再进行动态检查
- 如果静态切点检查是匹配的,则在运行时才进行动态切点检查
注: 使用DefaultPointcutAdvisor定义切面,使用内部Bean方式注入动态切点GreetingDynamicPointcut,用内部Bean方式注入增强GreetingBeforeAdvice。此外DefaultPointcutAdvisor还有一个order属性,用于定义切面的织入顺序
输出:
注:
- Spring在创建代理织入切面时,对目标类中的所有方法都进行静态切点检查
- 在生成织入切面的代理对象后,第一次调用代理类的每一个方法进行一次静态切点检查,如果本次检查就能从候选者队列中将该方法排除,则以后对该方法的调用就不再执行静态切点检查
- 对于那些在静态切点匹配时匹配的方法,在后续调用该方法时,将执行动态切点检查
每次调用代理对象的任何一个方法,都会执行动态切点检查,这将导致很大的性能问题,所以在定义动态切点时,不要忘记同时覆盖
getClassFilter(0和matches(Method method,Class clazz)
方法,通过静态切点检查排除大部分方法
6. 流程切面
Spring的流程切面由
DefaultPointcutAdvisor
和ControlFlowPointcut
实现。流程切点代表由某个方法直接或间接发起调用的其他方法
如果希望所有由WaiterDelegate#service()方法发起调用的其他方法都织入GreetingBeforeAdvice增强,就必须使用流程切面来完成目标。使用DefaultPointcutAdvisor配置一个流程切面来完成这一需求:
注: 在这里指定WaiterDelegate#service()方法作为切点,表示所有通过该方法直接或间接调用发起的调用匹配切点
ControlFlowPointcut有两个构造函数:
ControlFlowPointcut(Class clazz)
:指定一个类作为流程切点ControlFlowPointcut(Class clazz,String methodName)
:指定一个类和某个方法作为流程切点
输出:
注: 流程切面和动态切面从某种程度可以算是一类切面,因为二者都需要在运行期判断动态的环境。 对于流程切面来说,代理对象在每次调用目标类方法时,都需要判断方法调用堆栈中是否有满足流程切点要求的方法,和动态切面一样,流程切面对性能的影响也很大。
7. 复合切点切面
ComposablePointcut
本身也是一个切点,实现了Pointcut接口,可以将多个切点以并集和交集的方式组合起来,提供了切点之间复合运算的功能
ComposablePointcut的构造函数:
ComposablePointcut()
构造一个匹配所有类所有方法的复合切点ComposablePointcut(ClassFilter classFilter)
构造一个匹配特定类所有方法的复合切点ComposablePointcut(MethodMatcher methodMatcher)
构造一个匹配所有类特定方法的复合切点ComposablePointcut(ClassFilter classFilter,MethodMatcher methodMatcher)
构造一个匹配特定类特定方法的复合切点
ComposablePointcut提供了三个交集运算的方法:
ComposablePointcut intersection(ClassFilter classFilter)
将复合切点和一个ClassFilter对象进行交集运算,得到一个结果复合切点ComposablePointcut intersection(MethodMatcher mm)
将复合切点和一个MethodMatcher对象进行交集运算,得到一个结果复合切点ComposablePointcut intersection(Pointcut other)
将复合切点和一个切点对象进行交集运算,得到一个结果复合切点
ComposablePointcut提供了两个并集运算的方法:
ComposablePointcut union(ClassFilter filter)
将复合切点和一个ClassFilter对象进行并集运算,得到一个结果复合切点ComposablePointcut union(MethodMatcher mm)
将复合切点和一个MethodMatcher对象进行并集运算,得到一个结果复合切点
ComposablePointcut没有提供直接对两个切点进行并集元素的方法,可以使用Spring提供的org.springframework.aop.support.Pointcuts工具类,它有两个很好用的静态方法:
Pointcut intersection(Pointcut a.Pointcut b)
对两个切点进行交集运算,返回一个结果切点,该切点即ComposablePointcut对象的实例Pointcut union(Pointcut a,Pointcut b)
对两个切点进行并集运算,返回一个结果切点,该切点即ComposablePointcut对象的实例
通过ComposablePointcut创建一个流程切点和方法切点的相交切点:
配置复合切点切面的配置 :
测试:
输出:
8. 引介切面
引介切面是引介增强的封装器,通过引介切面,可以更容易地为现有对象添加任何接口的实现
IntroductionAdvisor
同时继承Advisor和IntroductionInfo接口,IntroductionInfo接口描述了目标类需要实现的新接口。IntroductionAdvisor和PointcutAdvisor接口不同,它只有一个类过滤器器ClassFilter
而没有MethodMatcher,这是因为引介切面的切点是类级别的,而Pointcut的切点是方法级别的。
IntroductionAdvisor有两个实现类:
- DefaultIntroductionAdvisor:引介切面最常用的实现类
- DeclareParentsAdvisor: 用于实现AspectJ语言的DeclareParent注解表示的引介切面
DefaultIntroductionAdvisor拥有三个构造函数 :
DefaultIntroductionAdvisor(Advice advice)
通过一个增强创建的引介切面,引介切面讲为目标对象新增增强对象中所有接口的实现DefaultIntroductionAdvisor(DynamicIntroductionAdvice advice,Class clazz)
通过一个增强和一个指定的接口类创建引介切面,仅为目标对象新增clazz接口的实现DefaultIntroductionAdvisor(Advice advice,IntroductionInfo introductionInfo)
通过一个增强和一个IntroductionInfo创建引介切面,目标对象需要实现哪些接口由introductionInfo对象的getInterfaces()方法表示
2. 自动创建代理
Spring提供了自动代理机制,让容器自动生成代理,Spring使用BeanPostProcessor自动完成这项工作
1. 实现类介绍
基于 BeanPostProcessor
的自动代理创建器的实现类,将根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理实例,这些代理创建器可以分为3类:
- 基于Bean配置名规则的自动代理创建器: 允许为一组特定配置名的Bean自动创建代理实例的代理创建器,实现类为BeanNameAutoProxyCreator
- 基于Advisor匹配机制的自动代理创建器: 它会对容器中的所有Advisor进行扫描,自动将这些切面应用到匹配的Bean中(为目标Bean创建代理实例),实现类为DefaultAdvisorAutoProxyCreator
- 基于Bean中AspectJ注解标签的自动代理创建器: 为包含AspectJ注解的Bean自动创建代理实例,实现类为AnnotationAwareAspectJAutoProxyCreator
2. BeanNameAutoProxyCreator
代码清单7-29中通过配置两个ProxyFactoryBean分别为waiter和seller的Bean创建代理对象。
下面通过BeanNameAutoProxyCreator完成相同的功能:
BeanNameAutoProxyCreator有一个
beanNames
属性,允许用户指定一组需要自动代理的Bean名称,Bean名称可以用*通配符。
一般不会为FactoryBean的Bean创建代理,如果有需求,则需要在beanNames中指定添加$的Bean属性,如<property name=”beanNames vlaue=”$waiter” />
等
BeanNameAutoProxyCreator的interceptorNames属性指定一个或多个增强Bean的名称。还有一个常用的optimize
属性,如果设置为true,将强制使用CGLib动态代理技术。
通过这样的配置,容器在创建waiter和seller Bean的实例时,就会自动为它们创建代理对象
测试:
输出:
3. DefaultAdvisorAutoProxyCreator
DefaultAdvisorAutoProxyCreator能扫描容器中的Advisor并将Advisor自动织入匹配目标的Bean中,即为匹配的目标Bean自动创建代理
注: 用DefaultAdvisorAutoProxyCreator定义了一个Bean,负责Advisor织入匹配的目标Bean中
测试:
输出:
注: Waiter#serveTo()方法没有被织入增强,而Waiter和Seller中的greetTo()方法都被织入了增强,增强被正确地织入匹配的连接点中
4. AOP无法增强疑难问题剖析
在方法内部之间调用的时候,不会使用被增强的代理类,而是直接使用未被增强原类的方法
想解决这个问题,就是在内部方法调用时,让其通过代理类调用其内部方法,所以,需要让原来的Waiter实现一个可注入自身代理类的接口BeanSelfProxyAware:
public interface BeanSelfProxyAware {
void setSelfProxy(Object object); //织入自身代理类接口
}
需要实对所有实现了BeanSelfPorxyAware的Bean执行自身代理Bean的注入,设计一个可复用的注入装配器BeanSelfProxyAwareMounter:
@Component
public class BeanSelfProxyAwareMounter implements SystemBootAddon, ApplicationContextAware {
private Logger logger= LoggerFactory.getLogger(this.getClass());
private ApplicationContext applicationContext;
//①注入Spirng容器
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException{
this.applicationContext = applicationContext;
}
//②实现系统启动器接口中的装配就绪方法
public void onReady(){
//从容器中获取所有自动注入的自动代理Bean
Map<String,BeanSelfProxyAware> proxyAwareMap=
applicationContext.getBeansOfType(BeanSelfProxyAware.class);
if(proxyAwareMap!=null){
for(BeanSelfProxyAware beanSelfProxyAware:proxyAwareMap.values()){
beanSelfProxyAware.setSelfProxy(beanSelfProxyAware);
if(logger.isDebugEnabled()){
logger.debug("{}注册自身被代理的实例");
}
}
}
}
public int getOrder(){
return Ordered.HIGHEST_PRECEDENCE;
}
}
- 在①通过实现ApplicationContextAware#setApplicationContext()接口方法来注入Spring容器上下文。
- 在②实现了系统启动器SystemBootAddon#onReady()接口方法。
- 这个接口在系统所有组件都装载完成后,准备就绪前调用的插件接口,用于Spring容器启动完成后触发调用注入装配器BeanSelfProxyAwareMounter
public interface SystemBootAddon extends Ordered {
//在系统就绪后调用的方法
void onReady();
}
在②中从Spring 容器中获取所有实现自身代理织入接口BeanSelfProxyAware的Bean,循环迭代遍历这些Bean,并调用setSelfProxy()方法将自身代理类注入自身
同时实现了Bean加载顺序接口Ordered,在插件中可以实现Ordered#getOrder()方法,返回一个整形数字来指定插件的执行顺序,值越小优先被加载处理。
最后需要设置一个启动管理器,告诉Spring什么时候触发BeanSelfProxyAwareMounter装配器
@Component
public class SystemBootManager implements ApplicationListener<ContextRefreshedEvent> {
private Logger logger= LoggerFactory.getLogger(this.getClass());
private List<SystemBootAddon> systemBootAddons= Collections.EMPTY_LIST;
private boolean hasRunOnce=false;
//①注入所有SystemBootAddon插件
@Autowired(required = false) //找不到也不报错
public void setSystemBootAddons(List<SystemBootAddon> systemBootAddons){
Assert.notEmpty(systemBootAddons);
OrderComparator.sort(systemBootAddons);
this.systemBootAddons=systemBootAddons;
}
//②触发所有插件
public void onApplicationEvent(ContextRefreshedEvent event){
if(!hasRunOnce){
for(SystemBootAddon systemBootAddon:systemBootAddons){
systemBootAddon.onReady();
if(logger.isDebugEnabled()){
logger.debug("执行插件:{}",systemBootAddon.getClass().getCanonicalName());
}
}
hasRunOnce=true;
}else{
if (logger.isDebugEnabled()) {
logger.debug("已执行过容器启动插件集,本次忽略之.");
}
}
}
}
在①通过自动注入方法注入所有实现SystemBootAddon接口的插件
在②通过监听Spring容器的ContextRefreshedEvent时间调用容器中所有已注册的SystemRootAddon插件。
在Waiter类中让其实现BeanSelfProxyAware接口