松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot Vue 微人事视频教程
原文地址:https://www.iteye.com/blogs/subjects/springaop
- 1 Aop简介
- 1.1 基本原理
- 1.2 基本概念
- 2 基于Aspectj注解的Spring Aop简单实现
- 2.1 启用对Aspectj注解的支持
- 2.2 定义切面类
- 2.3 定义Pointcut
- 2.4 定义Advice
- 3 Pointcut表达式介绍
- 3.1 表达式类型
- 3.2 使用示例
- 3.3 表达式组合
- 3.4 基于Aspectj注解的Pointcut表达式应用
- 4 基于Aspectj注解的Advice介绍
- 4.1 @Before
- 4.2 @AfterReturning
- 4.3 @AfterThrowing
- 4.4 @After
- 4.5 @Around
- 4.6 Advice执行顺序
- 5 给Advice传递参数
- 5.1 获取切入点方法参数
- 5.2 argNames参数
- 5.3 获取this对象
- 5.4 混合使用
- 5.5 获取target对象
- 5.6 获取注解对象
- 5.7 泛型参数
- 6 @DeclareParents介绍
- 7 基于XML配置的Spring AOP
- 7.1 配置切面
- 7.2 配置切入点
- 7.3 配置Advice
- 7.4 declare-parents
- 7.5 启用CGLIB代理类
- 8 advisor标签
- 8.1 before Advice
- 8.2 around Advice
- 8.3 afterReturning Advice
- 8.4 afterThrowing Advice
- 8.5 after Advice
- 9 基于正则表达式的Pointcut
- 10 编程式的Pointcut
- 10.1 自定义Pointcut
- 10.2 继承自现有的Pointcut
- 11 编程式的创建Aop代理之ProxyFactory
- 11.1 指定被代理对象
- 11.2 指定Advisor
- 11.3 指定Advice
- 11.4 指定是否需要发布代理对象
- 12 编程式的创建Aop代理之AspectjProxyFactory
- 13. ProxyFactoryBean创建代理对象
- 14 Aop自动创建代理对象的原理
- 14.1 BeanNameAutoProxyCreator
- 14.2 DefaultAdvisorAutoProxyCreator
- 15 Spring Aop原理之Advised接口
- 16 编程式的自定义Advisor
- 16.1 概述
- 16.2 实现自定义的Advisor
- 16.3 配置使用自定义的Advisor
1 Aop简介
AOP的全称是Aspect Oriented Programming,翻译成中文是面向切面编程。它的主要思想是在程序正常执行的某一个点切进去加入特定的逻辑。AOP框架中对AOP支持最完整的是Aspectj,Spring Aop是基于Aspectj实现的专门针对于Spring自身支持的Aop,它的功能没有Aspectj那么完整,它只作用于Spring bean容器中bean对象的某个方法的执行。正如Spring官方文档所描述的那样,Spring Aop与Aspectj不是竞争关系,而是相互补充、相互完善的这么一个关系。
1.1 基本原理
AOP框架的基本原理基本上都是通过代理的方式对目标对象达到切入式的切面编程的效果,Spring Aop也不例外。Spring Aop只能对它自身bean容器中定义的bean对象进行代理,这算是Spring Aop的一个限制,如果你的项目中不使用Spring的IOC,使用Spring的Aop显然是有点不那么合适的。Spring Aop中使用的代理有两种方式,一种是Jdk的动态代理,另一种是基于CGLIB实现的代理,当我们的bean对象实现了某一接口后,Spring默认将采用Jdk动态代理,当我们的bean对象没有实现接口时,默认将采用CGLIB代理,Spring也支持我们在bean对象实现了接口时也强制的使用CGLIB代理。Spring的Bean容器在初始化bean对象的时候就会判断对应的bean是否需要进行切面编程,即是否需要对其进行代理,如果需要,则初始化的时候就会把它初始化为一个代理对象。下面基于Jdk来看一个简单的示例,假设我们定义了如下这样一个代理工厂,其可以将一个普通的对象基于其实现的接口利用Jdk动态代理机制生成对应的代理对象。具体代码和说明请看如下代码,其中我们可以在目标对象执行特定的方法时加入一些特定的处理逻辑。
代码语言:javascript复制import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory {
private static ProxyFactory instance = new ProxyFactory();
private ProxyFactory() {}
public static ProxyFactory getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T create(final T t) {
return (T) Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), new InvocationHandler() {
/**
* 当使用创建的代理对象执行其中的方法时,都会转换为调用与代理对象绑定的InvocationHandler对象的invoke方法,
* 这样我们就可以在这个方法里面对调用情况进行一些特定的处理逻辑
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("正在调用的方法是:" method);
//1、加入对调用方法前的处理逻辑
//...
Object result = null;
try {
//2、正常的调用目标对象的目标方法
result = method.invoke(t, args);
//3、可加入正常调用后的处理逻辑
//...
} catch (Exception e) {
//4、可加入目标对象的方法调用抛出异常后的处理逻辑
//..
} finally {
//5、可加入目标对象的方法执行完成后的处理逻辑,此逻辑不论是否抛出异常都将执行
}
return result;
}
});
}
}
以下是基于上述代码进行的一个简单示例,具体如下,有兴趣的朋友也可以自己试一试。
代码语言:javascript复制@Test
public void test1() {
ProxyFactory proxyFactory = ProxyFactory.getInstance();
//创建一个实现了UserService接口的对象
UserService userService = new UserServiceImpl();
//创建一个基于userService对象的代理对象
UserService proxy = proxyFactory.create(userService);
//调用代理对象的某个方法
User user = proxy.findById(1);
System.out.println(user);
}
上述只是一个简单的示例,只是为了说明Spring Aop的大体原理,实际上Spring Aop的代理逻辑比这个要复杂很多,在初始化bean后它需要判断该bean是否需要创建代理对象,这通常都是BeanPostProcessor的功能。有兴趣的读者可以参考一下DefaultListableBeanFactory的preInstantiateSingletons方法,了解一下Spring bean的初始化过程,更详细的内容请参考AbstractApplicationContext.refresh方法。
1.2 基本概念
在了解Spring Aop的用法前,我们需要先了解一下Spring Aop中的一些重要概念,这些概念的英文名称摘自Spring的官方文档,这些术语在本系列文章中出现时可能会以原始英文的形式出现。
- Aspect:切面类,是面向切面编程的主体类,用以定义Pointcut和Advice这样的对应关系。
- Join Point:切入点,程序运行的某一个点,比如执行某个方法,在Spring AOP中Join Point总是表示一个方法的执行。
- Advice:切面类Aspect需要在Join Point处执行的操作。Advice的类型主要包括Before、After和Around三种。包括Spring在类的很多AOP框架都会把Advice以类似于Interceptor(拦截器)的形式进行封装,然后在每个Join Point的前后都可以包含一个Interceptor链,即可以进行多个操作处理。
- Pointcut:用来定义匹配的Join Point的,它是一个表达式。它往往会跟Advice绑定在一起,用以指定需要在Pointcut表达式匹配的Join Point执行的操作。采用Pointcut表达式来匹配Join Point是AOP中一个非常重要的概念,它能够使得我们的Advice能够比较独立,即一个Advice可以同时服务于多个JoinPoint。Spring AOP默认采用Aspectj(AOP的始祖)的Pointcut表达式。
- Introduction:用来声明额外的方法和属性。可以给目标对象引入新的接口及其实现。例如可以使用Introduction让一个bean实现isModified接口。
- Target Object:目标对象,表示Aspect正在处理的对象。因为Spring AOP是基于运行时代理实现的,所以这个对象永远都是一个代理对象。
- Aop Proxy:由AOP框架创建的一个代理对象。在Spring AOP中这个代理对象将由JDK代理(基于接口)或CGLIB代理(基于Class)生成。
- Weaving:表示编织的意思。用以将切面类Aspect与目标对象联系在一起的这么一个动作,所形成的结果就是在Pointcut所指定的Join Point执行时由Aspect对目标对象执行特定的Advice。AOP框架中的Weaving动作可以发生在编译时、类装载时和运行时,Spring AOP的Weaving动作是发生在运行时。
1.2.1 Advice类型
Advice的类型主要有Before、After和Around三种,Before作用于JoinPoint执行前,After作用于JoinPoint执行后(After类型还可以细分),Around则可作用于JoinPoint执行前后,且JoinPoint的执行需要在Around类型的Advice中进行调用,具体如下:
- Before:Before类型的Advice将在Join Point执行前运行,除非在运行时抛出一个异常,否则Before类型的Advice不会阻止Join Point的运行。
- After Return:After Return类型的Advice将在Join Point正常执行完成(return)后运行,即Join Point的运行没有抛出对外的异常后返回的。
- After Throwing:After Throwing类型的Advice将在Join Point抛出对外的异常后运行。
- After (finally):After类型的Advice不论Join Point的执行结果如何都将运行。
- Around:Around类型的Advice将围绕一个Join Point执行,它既可以在Join Point执行前执行特定的逻辑,也可以在Join Point执行后执行特定的逻辑,还可以控制Join Point是否执行、抛出异常、修改返回值等。
Around Advice的功能是最强大的,所有其它Advice能够满足的需求使用Around Advice也都能够满足。但是Spring官方并不推荐我们大量的使用Around Advice,而是使用最简单最能满足我们需要的那个Advice。比如如果你只是想简单的在Join Point执行完成后根据返回值来更新缓存,那你使用After Return Advice将比使用Around Advice更合适。这不但能够使你的程序更加的简单,也能减少你出错的机会(使用Around Advice时需要用户自己调用JoinPoint的proceed方法,让JoinPoint继续运行),更能减少程序运行的复杂度。
Spring AOP目前只支持对方法执行这样的JoinPoint进行特定的Advice处理,更确切的来说是只支持对Spring Bean容器里面的bean定义的方法执行进行切入特定的处理逻辑。如果你需要对属性的访问也进行拦截,也执行特定的Advice,那么你可以考虑使用Aspectj。还有一点需要注意的是切面类不会被自动代理,不能作为其它切面类作用的目标类,即使你配置的Poincut目标对象能包含对应的Aspect也不行。
2 基于Aspectj注解的Spring Aop简单实现
Spring Aop是基于Aop框架Aspectj实现的,它不是完完全全的对Aspectj框架进行扩展和改造,而是利用Aspectj里面的一些功能来实现自己的Aop框架,其中就包括对Aspectj提供的注解的解析。之前已经提过Spring Aop和Aspectj实现的Aop之间的差别,这里就不再赘述。本文主要描述的是如何利用Aspectj提供的注解来实现Spring Aop功能,旨在让大家对Spring Aop、对使用Aspectj注解开发Spring Aop有一个初步印象。
2.1 启用对Aspectj注解的支持
使用Aspectj注解来实现Spring Aop时我们首先需要启用Spring对Aspectj注解支持的功能,这是通过配置来进行的。当我们的Spring配置是以配置文件为主时,我们可以通过在Spring的配置文件中引入aop相关的schema,然后通过aop:aspectj-autoproxy/来启用对Aspectj注解的支持。
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<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.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.elim.test"/>
<!-- 启用对Aspectj注解的支持 -->
<aop:aspectj-autoproxy/>
</beans>
当我们的配置是通过使用@Configuration标注的配置类来进行的时候,我们就可以通过在该配置类上使用@EnableAspectJAutoProxy进行标注来启用对Aspectj注解的支持。
代码语言:javascript复制import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
2.2 定义切面类
定义切面类比较简单,只需要定义一个简单的使用@Aspect进行标注的类即可。@Aspect是Spring Aop识别切面类的一个标记,但是光使用@Aspect对切面类进行标注还不行。因为Spring Aop只能对定义在bean容器中的bean发生作用,对应的切面类也必须是定义在bean容器中的bean对象时其才能发现。@Aspect不具备默认让Spring扫描到它,把对应的类实例化为Spring bean的功能,所以我们必须要在bean容器中定义该bean。可以通过配置文件定义,也可以通过使用@Component进行标注等。
代码语言:javascript复制@Aspect
@Component
public class MyAspect {
}
使用@Aspect标注的切面类也可以像普通类一样定义普通的属性和方法,如果有需要也可以把它当做一个普通的bean使用。
2.3 定义Pointcut
Pointcut是用来定义切面类需要作用的JoinPoint的,在Spring Aop中这些JoinPoint其实就是一些我们需要进行切入的方法执行,因为之前我们说过Spring Aop只支持对bean方法执行的切入。基于Aspect注解形式定义的Pointcut的核心是@Pointcut注解,我们需要在Aspect类中定义一个没有返回值的方法,方法类型可任意,然后在该方法上使用@Pointcut进行标注,表示其是一个Pointcut定义,对应的方法名即表示该Pointcut的名称。@Pointcut有一个value属性,其通常是一个表达式,通过它可以指定当前Pointcut需要作用的JoinPoint。表达式可以有很多种写法,这个在后续会专门讲解,通常用的最多的就是execution,表示方法的执行。如我们想定义一个Pointcut的JoinPoint为所有add方法的执行,那么我们可以如下定义。
代码语言:javascript复制@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* add(..))")
private void pointcut() {}
}
2.4 定义Advice
Advice需要与Pointcut绑定在一起,用以定义需要在指定的Pointcut匹配的JoinPoint处执行的操作。Advice主要有三种类型,before、after和around,Aspectj对它们都有对应的注解进行支持。基于Aspectj注解的advice定义是通过对应的注解来指定的,我们需要在切面类中定义一个方法,然后在该方法上使用对应的注解进行标注。对应的advice注解都有一个value属性,我们需要通过它来指定与之绑定的Pointcut,对应的Pointcut需要通过Pointcut定义的类全名称.方法名()来指定,如果是在当前切面类中定义的Pointcut则可以省略对应的类名称。这里主要拿before来做一个示例,如下,我们在切面类中定义了一个方法before,并用@Before注解标注了该方法,同时指定了其所绑定的Pointcut为同一个切面类中定义的pointcut。
代码语言:javascript复制@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* add(..))")
private void pointcut() {}
@Before("com.elim.test.spring.aop.MyAspect.pointcut()")
private void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getTarget() "----------------------Before---------------------");
}
}
至此,当我们在访问Spring bean容器中任意bean对象的add方法前就会调用MyAspect切面类中定义的before方法了。
代码语言:javascript复制@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {
@Autowired
private UserService userService;
@Test
public void test1() {
User user = new User(1, "ZhangSan");
userService.add(user);
}
}
3 Pointcut表达式介绍
3.1 表达式类型
标准的Aspectj Aop的pointcut的表达式类型是很丰富的,但是Spring Aop只支持其中的9种,外加Spring Aop自己扩充的一种一共是10种类型的表达式,分别如下。
- execution:一般用于指定方法的执行,用的最多。
- within:指定某些类型的全部方法执行,也可用来指定一个包。
- this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
- target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
- args:当执行的方法的参数是指定类型时生效。
- @target:当代理的目标对象上拥有指定的注解时生效。
- @args:当执行的方法参数类型上拥有指定的注解时生效。
- @within:与@target类似,看官方文档和网上的说法都是@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。而根据笔者的测试这两者都是只要目标类或父类上有指定的注解即可。
- @annotation:当执行的方法上拥有指定的注解时生效。
- bean:当调用的方法是指定的bean的方法时生效。
3.2 使用示例
3.2.1 execution
execution是使用的最多的一种Pointcut表达式,表示某个方法的执行,其标准语法如下。
代码语言:javascript复制execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?
name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern表示方法的访问类型,public 等;
- ret-type-pattern表示方法的返回值类型,如String表示返回类型是String,“
*
”表示所有的返回类型; - declaring-type-pattern表示方法的声明类,如“com.elim..
*
”表示com.elim包及其子包下面的所有类型; - name-pattern表示方法的名称,如“add
*
”表示所有以add开头的方法名; - param-pattern表示方法参数的类型,name-pattern(param-pattern)其实是一起的表示的方法集对应的参数类型,如“add()”表示不带参数的add方法,“add(
*
)”表示带一个任意类型的参数的add方法,“add(*
,String)”则表示带两个参数,且第二个参数是String类型的add方法; - throws-pattern表示异常类型;
- 其中以问号结束的部分都是可以省略的。
- “execution(* add())”匹配所有的不带参数的add()方法。
- “execution(public * com.elim..
*
.add*(..))”匹配所有com.elim包及其子包下所有类的以add开头的所有public方法。 - “execution(*
*
(..) throws Exception)”匹配所有抛出Exception的方法。
3.2.2 within
within 是用来指定类型的,指定类型中的所有方法将被拦截。
1、“within(com.elim.spring.aop.service.UserServiceImpl)”匹配UserServiceImpl类对应对象的所有方法外部调用,而且这个对象只能是UserServiceImpl类型,不能是其子类型。2、“within(com.elim..*
)”匹配com.elim包及其子包下面所有的类的所有方法的外部调用。
3.2.3 this
Spring Aop是基于代理的,this就表示代理对象。this类型的Pointcut表达式的语法是this(type),当生成的代理对象可以转换为type指定的类型时则表示匹配。基于JDK接口的代理和基于CGLIB的代理生成的代理对象是不一样的。
- “this(com.elim.spring.aop.service.IUserService)”匹配生成的代理对象是IUserService类型的所有方法的外部调用。
3.2.4 target
Spring Aop是基于代理的,target则表示被代理的目标对象。当被代理的目标对象可以被转换为指定的类型时则表示匹配。
- “target(com.elim.spring.aop.service.IUserService)”则匹配所有被代理的目标对象能够转换为IUserService类型的所有方法的外部调用。
3.2.5 args
args用来匹配方法参数的。
- “args()”匹配任何不带参数的方法。
- “args(java.lang.String)”匹配任何只带一个参数,而且这个参数的类型是String的方法。
- “args(..)”带任意参数的方法。
- “args(java.lang.String,..)”匹配带任意个参数,但是第一个参数的类型是String的方法。
- “args(..,java.lang.String)”匹配带任意个参数,但是最后一个参数的类型是String的方法。
3.2.6 @target
@target 匹配当被代理的目标对象对应的类型及其父类型上拥有指定的注解时。
1. “@target(com.elim.spring.support.MyAnnotation)”匹配被代理的目标对象对应的类型上拥有MyAnnotation注解时。
3.2.7 @args
@args匹配被调用的方法上含有参数,且对应的参数类型上拥有指定的注解的情况。
1、“@args(com.elim.spring.support.MyAnnotation)”匹配方法参数类型上拥有MyAnnotation注解的方法调用。如我们有一个方法add(MyParam param)接收一个MyParam类型的参数,而MyParam这个类是拥有注解MyAnnotation的,则它可以被Pointcut表达式“@args(com.elim.spring.support.MyAnnotation)”匹配上。
3.2.8 @within
@within用于匹配被代理的目标对象对应的类型或其父类型拥有指定的注解的情况,但只有在调用拥有指定注解的类上的方法时才匹配。
1、“@within(com.elim.spring.support.MyAnnotation)”匹配被调用的方法声明的类上拥有MyAnnotation注解的情况。比如有一个ClassA上使用了注解MyAnnotation标注,并且定义了一个方法a(),那么在调用ClassA.a()方法时将匹配该Pointcut;如果有一个ClassB上没有MyAnnotation注解,但是它继承自ClassA,同时它上面定义了一个方法b(),那么在调用ClassB().b()方法时不会匹配该Pointcut,但是在调用ClassB().a()时将匹配该方法调用,因为a()是定义在父类型ClassA上的,且ClassA上使用了MyAnnotation注解。但是如果子类ClassB覆写了父类ClassA的a()方法,则调用ClassB.a()方法时也不匹配该Pointcut。
3.2.9 @annotation
@annotation用于匹配方法上拥有指定注解的情况。
1、“@annotation(com.elim.spring.support.MyAnnotation)”匹配所有的方法上拥有MyAnnotation注解的方法外部调用。
3.2.10 bean
bean用于匹配当调用的是指定的Spring的某个bean的方法时。
1、“bean(abc)”匹配Spring Bean容器中id或name为abc的bean的方法调用。2、“bean(user*)”匹配所有id或name为以user开头的bean的方法调用。
3.3 表达式组合
表达式的组合其实就是对应的表达式的逻辑运算,与、或、非。可以通过它们把多个表达式组合在一起。
1、“bean(userService) && args()”匹配id或name为userService的bean的所有无参方法。2、“bean(userService) || @annotation(MyAnnotation)”匹配id或name为userService的bean的方法调用,或者是方法上使用了MyAnnotation注解的方法调用。3、“bean(userService) && !args()”匹配id或name为userService的bean的所有有参方法调用。
3.4 基于Aspectj注解的Pointcut表达式应用
在使用基于Aspectj注解的Spring Aop时,我们可以把通过@Pointcut注解定义Pointcut,指定其表达式,然后在需要使用Pointcut表达式的时候直接指定Pointcut。
代码语言:javascript复制@Component
@Aspect
public class MyAspect {
@Pointcut("execution(* add(..))")
private void beforeAdd() {}
@Before("beforeAdd()")
public void before() {
System.out.println("-----------before-----------");
}
}
上面的代码中我们就是在@Before()中直接指定使用当前类定义的beforeAdd()方法对应的Pointcut的表达式,如果我们需要指定的Pointcut定义不是在当前类中的,我们需要加上类名称,如下面这个示例中引用的就是定义在MyService中的add()方法上的Pointcut的表达式。
代码语言:javascript复制@Before("com.elim.spring.aop.service.MyService.add()")
public void before2() {
System.out.println("-----------before2-----------");
}
当然了,除了通过引用Pointcut定义间接的引用其对应的Pointcut表达式外,我们也可以直接使用Pointcut表达式的,如下面这个示例就直接在@Before中使用了Pointcut表达式。
代码语言:javascript复制/**
* 所有的add方法的外部执行时
*/
@Before("execution(* add())")
public void beforeExecution() {
System.out.println("-------------before execution---------------");
}
4 基于Aspectj注解的Advice介绍
之前介绍过,Advice一共有五种类型,分别是before、after return、after throwing、after(finally)和around。在使用注解的时候,它们对应的注解分别是@Before、@AfterReturning、@AfterThrowing、@After和@Around。这几个注解都是在org.aspectj.lang.annotation包中。
4.1 @Before
Before Advice将在目标方法执行前执行Advice逻辑,通过它我们可以在指定的切入点方法执行前加入特定的逻辑。如下是定义的一个Before Advice,通过其value属性(注解中只指定value属性时属性名是可以省略的)指定其需要拦截的切入点id或name为userService的bean的方法执行,然后拦截后我们只是简单的打印一条语句,在实际应用中这里应该加上我们特定的逻辑。
代码语言:javascript复制@Before("bean(userService)")
public void before() {
System.out.println("-----before with pointcut expression: bean(userService)------");
}
注:
1、@Before除了可以通过value属性指定需要拦截的切入点外,还可以指定一个argNames属性,这个是用于方便我们在Advice中访问切入点方法参数的,这个在后续会专门用一篇文章来讲如何在Advice中使用切入点方法参数。2、argNames这个属性不仅在@Before上有,在其它的Advice注解上也有。3、除非抛出异常,否则Before Advice是没法阻止程序继续往下执行的。所有的Advice方法都可以接收一个JoinPoint参数,而且这个参数必须是Advice方法的第一个参数,通过这个参数我们可以获取到目标方法的一些信息,比如当前方法调用传递的参数信息、目标对象等。而如果是Around类型的Advice则必须接受一个ProceedingJoinPoint类型的参数,ProceedingJoinPoint是JoinPoint的子类。
代码语言:javascript复制@Before("bean(userService)")
public void before(JoinPoint joinPoint) {
System.out.println("-----before with pointcut expression: bean(userService)------");
joinPoint.getArgs();//获取当前目标方法调用传递的参数
joinPoint.getSignature();//获取当前目标方法的签名,通过它可以获取到目标方法名
joinPoint.getThis();//获取AOP生成的代理对象
joinPoint.getTarget();//获取被代理对象,即目标对象
System.out.println(joinPoint.getArgs());
System.out.println(joinPoint.getSignature().getName());
System.out.println(joinPoint.getThis().getClass());
System.out.println(joinPoint.getTarget().getClass());
System.out.println(joinPoint.toString());
}
4.2 @AfterReturning
AfterReturning Advice对应的是切入点方法正常执行完的拦截,即切入点方法执行时没有对外抛出异常,包括在目标方法被Around类型的Advice处理时没有抛出异常,如果目标方法在被Around类型的Advice处理时也抛出了异常,则同样会被认为目标方法是执行异常的,因为Around Advice是最先处理的,AfterReturning Advice会在Around Advice处理结束后才被触发的。如果我们希望在AfterReturning Advice中根据目标方法的返回结果做特定的业务逻辑,那么我们可以给AfterReturning Advice处理方法加一个参数,参数类型可以是你能确定的目标方法返回类型或用通用的Object,然后需要在@AfterReturning上通过returning属性指定目标方法的返回值需要赋值给AfterReturning Advice处理方法的哪个参数。如下示例中就在Advice处理方法上加入了一个通用类型的Object类型的returnValue参数,然后指定@AfterReturning的returning属性为“returnValue”。如果我们确定目标方法的返回结果一定是一个User类型的,那么我们也可以指定下面的方法参数类型是User类型。
代码语言:javascript复制@AfterReturning(value="bean(userService)", returning="returnValue")
public void afterReturning(Object returnValue) {
System.out.println("-----after returning with pointcut expression: bean(userService)------");
System.out.println("-----return value is: " returnValue);
}
4.3 @AfterThrowing
AfterThrowing Advice对应的是切入点方法执行对外抛出异常的拦截。因为当一个切入点方法可以同时被Around Advice和AfterThrowing Advice拦截时,实际上AfterThrowing Advice拦截的是Around Advice处理后的结果,所以这种情况下最终AfterThrowing Advice是否能被触发,还要看Around Advice自身是否对外抛出异常,即算是目标方法对外抛出了异常,但是被Around Advice处理了又没有向外抛出异常的时候AfterThrowing Advice也不会被触发的。如果希望在AfterThrowing Advice处理方法中获取到被抛出的异常,可以给对应的Advice处理方法加一个Exception或其子类型(能确定抛出的异常类型)的方法参数,然后通过@AfterThrowing的throwing属性指定拦截到的异常对象对应的Advice处理方法的哪个参数。如下就指定了拦截到的异常对象将传递给Advice处理方法的ex参数。
代码语言:javascript复制@AfterThrowing(value="bean(userService)", throwing="ex")
public void afterThrowing(Exception ex) {
System.out.println("-----after throwing with pointcut expression: bean(userService)------" ex);
}
AfterThrowing是用于在切入点方法抛出异常时进行某些特殊的处理,但是它不会阻止方法调用者看到异常结果。
4.4 @After
After Advice就相当于try…catch…finally语句里面的finally的角色,即无论被拦截的切入点方法是成功执行完成还是对外抛出了异常,对应的Advice处理方法都将会执行。
代码语言:javascript复制@After("bean(userService)")
public void after() {
System.out.println("-----after with pointcut expression: bean(userService)------");
}
4.5 @Around
Around Advice必须接收一个ProceedingJoinPoint类型的方法参数,然后在方法体中选择一个合适的时机来调用ProceedingJoinPoint的proceed方法以触发对目标方法的调用,然后Around Advice处理方法的返回值会被当做是目标方法调用的返回值。所以通过Around Advice我们可以在通过ProceedingJoinPoint调用目标方法的前后加上特定的逻辑,包括使用try…catch…finally等,所以Around Advice是功能最强大的一个Advice,前面的任何一种Advice在应用的时候都可以被Around Advice替换。
代码语言:javascript复制@Around("bean(userService)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("-----around with pointcut expression: bean(userService)------");
System.out.println("---------------------调用前---------------------");
Object result = pjp.proceed();
System.out.println("---------------------调用后---------------------");
return result;
}
在上面的示例中我们就通过Around Advice拦截了id或name为userService的bean的所有方法调用,把真实的目标方法的返回结果返回去了。而实际上我们这里还可以修改目标方法的返回结果,比如常用的就是Spring的缓存会通过Around Advice在调用目标方法前先从缓存中获取结果,如果获取到了则直接返回。这也是Around Advice跟AfterReturning Advice一个比较大的差别,AfterReturning Advice是不能改变返回对象的引用的,但是它可以改变返回对象的个别属性。在使用Around Advice时也可以改变目标方法调用时传递的参数,这个时候要用到ProceedingJoinPoint 的带参数的proceed(Object[] args)方法了。如下示例中我们就在Around Advice中把调用目标方法的参数替换为15了。
代码语言:javascript复制@Around("bean(userService)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("-----around with pointcut expression: bean(userService)------");
System.out.println("---------------------调用前---------------------");
Object[] params = new Object[]{15};
Object result = pjp.proceed(params);//可以调整目标方法调用时传递的参数
System.out.println("---------------------调用后---------------------");
return result;
}
4.6 Advice执行顺序
官方的说法是在进入切入点前优先级越高的越先执行,而在从切入点出去时优先级越高的会越后执行。当一个切面类中定义了多个Advice需要作用于同一个切入点时它们的执行顺序是不确定的,理由是无法通过反射获取到这些Advice在编译好的字节码中的声明顺序,这种情况下官方建议将多种切面逻辑整合到一个Advice中处理,以免造成错误。当两个定义在不同的切面中的Advice需要作用在同一个切入点时,除非你在切面类上使用@Order注解指定了顺序,数字越小表示优先级越高,或者是使切面类实现Ordered接口。
5 给Advice传递参数
Advice除了可以接收JoinPoint(非Around Advice)或ProceedingJoinPoint(Around Advice)参数外,还可以直接接收与切入点方法执行有关的对象,比如切入点方法参数、切入点目标对象(target)、切入点代理对象(this)等。
5.1 获取切入点方法参数
假设我们现在有一个id为userService的bean中定义了一个findById(int id)方法,我们希望定义一个Advice来拦截这个方法,并且把findById()的参数作为Advice处理方法的参数,即每次调用findById()传递的参数都将传递到Advice处理方法,那么我们可以如下这样定义。
代码语言:javascript复制@Before(value="bean(userService) && execution(* findById(java.lang.Integer)) && args(id)", argNames="id")
public void beforeWithParam(JoinPoint joinPoint, Integer id) {
System.out.println(this.getClass().getName() " ID is : " id);
}
上面这种定义是非常精确的定义,我们通过表达式“bean(userService) && execution(* findById(java.lang.Integer))”就已经明确的指定了我们需要拦截的是id或name为userService的findById(Integer)方法,后面又加了一个args(id)是干什么用的呢?它的作用跟findById(Integer)是类似的,它表示我们的切入点方法必须只接收一个参数,而且这个参数的类型是和当前定义的Advice处理方法的参数id是相同类型的,在上面的示例中其实就是要求是Integer类型的;另外它还有一个非常重要的作用,通过这种指定后对应的Advice处理方法在执行时将接收到与之对应的切入点方法参数的值。在上面的示例中笔者特意给Advice处理方法加了一个JoinPoint参数是为了说明JoinPoint、ProceedingJoinPoint参数是可以直接定义在Advice方法的第一个参数,并且是可以与其它接收的参数共存的。其实如果我们不只是需要拦截findById(Integer)方法,而是需要拦截id为userService的bean中所有接收一个int/Integer参数的方法,那么我们可以把上面的配置简化为如下这样。
代码语言:javascript复制@Before(value="bean(userService) && args(id)", argNames="id")
public void beforeWithParam2(int id) {
System.out.println(this.getClass().getName() " ID is : " id);
}
如果我们需要拦截的方法可能是有多个参数的,但我们只关注第一个参数,那我们可以把表达式调整为如下这样,只关注第一个参数为int/Integer类型的,并且在Advice方法中接收这个方法参数进行相应的处理。
代码语言:javascript复制@Before(value="bean(userService) && args(id,..)", argNames="id")
public void beforeWithParam2(int id) {
System.out.println(this.getClass().getName() " ID is : " id);
}
5.2 argNames参数
我们可以看到在上述例子中我们都指定了@Before的argNames属性的值为id,那么这个argNames属性有什么作用呢?argNames属性是用于指定在表达式中应用的参数名与Advice方法参数是如何对应的,argNames中指定的参数名必须与表达式中的一致,可以与Advice方法参数名不一致;当表达式中使用了多个参数时,argNames中需要指定多个参数,多个参数之间以英文逗号分隔,这些参数的顺序必须与对应的Advice方法定义的参数顺序是一致的。比如下面这个示例中,我们在Pointcut表达式中使用了name和sex两个参数,我们的Advice处理方法接收两个参数,分别是sex1和name1,我们希望Pointcut表达式中的name参数是对应的Advice处理方法的第二个参数,即name1,希望Pointcut表达式中的sex参数是对应的Advice处理方法的第一个参数,即sex1,那么我们在指定@Before注解的argNames参数时必须定义name和sex参数与Advice处理方法参数的关系,且顺序要求与对应的处理方法的参数顺序一致,即哪个参数是需要与Advice处理方法的第一个参数匹配则把哪个参数放第一位,与第二个参数匹配的则放第二位,在我们的这个示例中就应该是sex放第一位,name放第二位。
代码语言:javascript复制@Before(value="bean(userService) && args(name, sex)", argNames="sex, name")
public void beforeWithParam3(int sex1, String name1) {
System.out.println("sex is : " sex1);
System.out.println("name is : " name1);
}
@Before注解的argNames参数不是必须的,它只有在我们编译的字节码中不含DEBUG信息或Pointcut表达式中使用的参数名与Advice处理方法的参数名不一致时才需要。所以在编译的字节码中包含DEBUG信息且Advice参数名与Pointcut表达式中使用的参数名一致时,我们完全可以把argNames参数省略。如果表达式里面使用了多个参数,那么这些参数在表达式中的顺序可以与Advice方法对应参数的顺序不一致,例如下面这个样子。
代码语言:javascript复制@Before(value="bean(userService) && args(id)")
public void beforeWithParam2(int id) {
System.out.println(this.getClass().getName() " ID is : " id);
}
5.3 获取this对象
this对象就是Spring生成的bean的那个代理对象。如下示例就是Advice方法接收this对象,我们给Advice方法指定一个需要拦截的this对象类型的参数,然后在表达式中使用this类型的表达式定义,表达式中定义的对应类型指定为Advice方法参数。
代码语言:javascript复制@Before("this(userService)")
public void beforeWithParam4(IUserService userService) {
//this对象应该是一个代理对象
System.out.println(this.getClass().getName() "==============传递this对象: " userService.getClass());
}
5.4 混合使用
我们的Advice方法可以同时接收多个目标方法参数,与此同时它也可以接收this等对象,即它们是可以混合使用的。下面这个示例中我们就同时接收了this对象和目标方法int/Interger类型的参数。
代码语言:javascript复制@Before("this(userService) && args(id)")
public void beforeWithParam5(IUserService userService, int id) {
System.out.println(this.getClass().getName() "===========" id "==============" userService.getClass());
}
5.5 获取target对象
获取target对象也比较简单,只需要把表达式改为target类型的表达式即可。
代码语言:javascript复制@Before("target(userService)")
public void beforeWithParam6(IUserService userService) {
System.out.println(this.getClass().getName() "==============传递target对象: " userService.getClass());
}
5.6 获取注解对象
当我们的Pointcut表达式类型是通过注解匹配时,我们也可以在Advice处理方法中获取匹配的注解对象,如下面这个示例,其它如使用@target等是类似的。
代码语言:javascript复制@Before("@annotation(annotation)")
public void beforeWithParam7(MyAnnotation annotation) {
System.out.println(this.getClass().getName() "==============传递标注在方法上的annotation: " annotation.annotationType().getName());
}
5.7 泛型参数
有的时候我们的Advice方法需要接收的切入点方法参数定义的不是具体的类型,而是一个泛型,这种情况下怎么办呢?可能你会想那我就把对应的Advice方法参数定义为Object类型就好了,反正所有的类型都可以转换为Object类型。对的,这样是没有错的,但是说如果你只想拦截某种具体类型的参数调用时就可以不用把Advice方法参数类型定义为Object了,这样还得在方法体里面进行判断,我们可以直接把Advice方法参数类型定义为我们想拦截的方法参数类型。比如我们有下面这样一个使用了泛型的方法定义,我们希望只有在调用testParam方法时传递的参数类型是Integer类型时才进行拦截。
代码语言:javascript复制 public <T> void testParam(T param);
那这个时候我们就可以把我们的Advice的表达式定义为如下这样,前者精确定义接收方法名为testParam,返回类型为void,后者精确定义方法参数为一个Integer类型的参数,其实前者也可以定义为“execution(void testParam(Integer))”。看到这你可能会想,为什么不直接把表达式定义为“execution(void testParam(param))”呢?因为execution是不支持Advice方法参数绑定的,基本上支持Advice参数绑定的就只有this、target、args以及对应的注解形式加@annotation。
代码语言:javascript复制@Before("execution(void testParam(..)) && args(param)")
public void beforeWithParam8(Integer param) {
System.out.println("pointcut expression[args(param)]--------------param:" param);
}
以上就是常用的传递参数给Advice处理方法的方式,有一些示例可能没有讲到,比如@target这种,这些其实都是类似的。包括上面我们都是以@Before这种Advice来讲的,其实其它的Advice在接收参数的时候也是类似的。
6 @DeclareParents介绍
@DeclareParents注解也是Aspectj提供的,在使用基于Aspectj注解的Spring Aop时,我们可以在切面中通过@DeclareParents指定满足指定表达式的类将自动实现某些接口。这个只是在运行时会将生成的代理类实现指定的接口。有接口就会有实现,对应的实现类也需要我们在@DeclareParents声明自动实现的接口时声明。现假设我们有一个接口叫CommonParent,其实现类叫CommonParentImpl,代码如下。
代码语言:javascript复制public interface CommonParent {
public void doSomething();
}
public class CommonParentImpl implements CommonParent {
public void doSomething() {
System.out.println("-----------do something------------");
}
}
然后我们希望我们的所有的Service实现类都可以在运行时自动实现CommonParent接口,即所有的Service实现类在运行时都可以被当做CommonParent来使用。那我们可以定义如下这样一个切面类和对应的Advice。
代码语言:javascript复制@Component
@Aspect
public class DeclareParentsAspect {
@DeclareParents(value="com.elim.spring.aop.service..*", defaultImpl=CommonParentImpl.class)
private CommonParent commonParent;
@Before("bean(userService) && this(commonParent)")
public void beforeUserService(CommonParent commonParent) {
commonParent.doSomething();
}
}
如上,我们先在切面类中声明了一个CommonParent类型的属性,然后在上面使用了@DeclareParents注解表示我们需要把CommonParent声明为某些指定类型的父接口,然后通过@DeclareParents的value属性指定需要作用的类的形式,其语法和Pointcut表达式类似。通过defaultImpl属性指定默认CommonParent接口的实现类是CommonParentImpl。然后我们声明了一个before类型的Advice,在其中直接把我们的bean当做CommonParent类型的对象使用。
整个过程就是这样的,非常简单。但是笔者暂时还没有发现这个在实际应用中可以应用于哪些场景,欢迎交流。
7 基于XML配置的Spring AOP
基于XML配置的Spring AOP需要引入AOP配置的Schema,然后我们就可以使用AOP Schema下定义的config、aspect、pointcut等标签进行Spring AOP配置了。
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p" 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-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">
</beans>
基于XML配置的Spring AOP的核心配置是config元素,其它的诸如切面配置、切入点配置等都是配置在它的下面的,所以我们需要先定义一个config元素。
7.1 配置切面
切面的配置是通过aspect元素配置的,我们需要通过其ref属性指定切面类对应的Spring bean的id或name,还可以通过其order属性指定切面类的在拦截的时候的优先级。需要说明的是在使用Spring时基于XML的配置和基于注解的配置往往是可以混用的,所以在下面示例中我们使用的切面类引用不一定非得在Spring的bean配置文件中进行配置。
代码语言:javascript复制<bean id="schemaBasedAspect" class="com.elim.spring.aop.aspect.SchemaBasedAspect"/>
<!-- 基于XML的AOP配置是基于config元素开始的 -->
<aop:config>
<!-- 定义切面 -->
<aop:aspect id="aspect1" ref="schemaBasedAspect" order="1">
</aop:aspect>
</aop:config>
7.2 配置切入点
切入点可以是直接配置在config元素下的,意为顶级切入点,也可以是配置在指定的aspect下面的。切入点的配置是通过pointcut元素来配置的,在配置的时候我们需要指定id和expression属性,expression属性就对应的是切入点的表达式,其配置规则跟基于Aspectj注解配置时的规则是一样的。如下示例中就定义了两个pointcut,一个是顶级的,一个是在aspectj1中定义的。需要注意的是顶级的Pointcut必须定义在aspect之前。
代码语言:javascript复制<aop:config>
<aop:pointcut expression="bean(userService)" id="userServicePointCut2"/>
<!-- 定义切面 -->
<aop:aspect id="aspect1" ref="schemaBasedAspect" order="1">
<aop:pointcut expression="bean(userService)" id="userServicePointCut"/>
</aop:aspect>
</aop:config>
如果我们的切入点是以Aspectj注解的形式定义在类里面的,我们也可以像在前面介绍的基于Aspectj注解配置那样进行引用,如在下面这个配置就是使用的在SchemaBasedAspect类的pointcut方法上通过@Pointcut指定的表达式。
代码语言:javascript复制<aop:pointcut expression="com.elim.spring.aop.aspect.SchemaBasedAspect.pointcut()" id="pointcut"/>
7.3 配置Advice
切面的最终目的是为了拦截指定切入点的方法执行,然后加入自己特定逻辑的,有了切面定义和切入点的定义后,我们需要定义在哪些切入点上需要使用切面的哪些Advice,即执行切面类的哪些方法。我们知道Advice有before、after、after returning、after throwing和around五种,对应的分别是before、after、after-returning、after-throwing和around标签,它们都是定义在aspect标签下的。这五类Advice在定义时的用法基本上是类似的,通过method方法指定Advice对应的是aspect类的哪个方法,然后通过pointcut指定切入点的表达式,或者通过pointcut-ref指定需要引用的是哪个切入点。如下示例中我们就定义了两个Advice,before Advice指定了对应的是切面类的doBefore方法,然后通过pointcut-ref指定需要使用的切入点是id为userServicePointCut的切入点;around Advice指定了对应的是切面类的doAround方法,然后通过pointcut直接指定了需要作用的切入点表达式。
代码语言:javascript复制<aop:aspect id="aspect1" ref="schemaBasedAspect" order="1">
<!-- 定义一个Around Advice -->
<aop:before method="doBefore" pointcut-ref="userServicePointCut" />
<aop:around method="doAround" pointcut="bean(userService)"/>
<aop:pointcut expression="bean(userService)" id="userServicePointCut"/>
</aop:aspect>
和基于注解的配置类似,如果我们需要在afterReturning Advice中访问返回值,我们也可以给对应的处理方法一个返回值对应类型(或Object类型)的参数,然后通过after-returning元素的returning属性指定返回值需要赋予给Advice处理方法的哪个参数,参数名需要一致。同样afterThrowing类型的Advice需要在处理方法中接收抛出的异常时,也可以给定对应的处理方法一个Exception类型的参数,然后通过after-throwing元素的throwing属性指定抛出的异常需要赋值给Advice处理方法的哪个参数。
代码语言:javascript复制<aop:after-returning method="doAfterReturning" pointcut-ref="pointcut" returning="returnValue"/>
<aop:after-throwing method="doAfterThrowing" pointcut-ref="pointcut" throwing="ex"/>
参数传递也是类似的,在Advice处理方法上给定指定的参数,然后在定义Pointcut表达式的时候加上对应的参数传递限制,如“args(id)”、“this(userService)”等,在需要指定参数的绑定顺序时,可以通过before、around等Advice标签的arg-names属性指定。如下示例中我们就要求id为userServicePointCut的Pointcut需要接收一个参数,这个参数是需要跟对应的Advice处理方法绑定的,而before Advice是使用该Pointcut的,所以就要求id与对应的处理方法参数绑定,而且对应的方法参数名需要是id。
代码语言:javascript复制<aop:aspect id="aspect1" ref="schemaBasedAspect" order="1">
<aop:before method="doBefore" pointcut-ref="userServicePointCut"/>
<aop:around method="doAround" pointcut="bean(userService)" />
<aop:pointcut expression="bean(userService) && args(id)" id="userServicePointCut"/>
</aop:aspect>
我们对应的doBefore处理方法是这样的。
代码语言:javascript复制public void doBefore(int id) {
System.out.println("======================doBefore======================" id);
}
在上面的示例中在定义id为userServicePointcut的表达式的时候用到了“&&”,这是因为在XML中&是特殊字符,我们必须对它进行转义。其实在XML中配置Pointcut表达式时我们可以使用“and”、“or”和“not”替代“&&”、“||”和“!”。
7.4 declare-parents
在基于注解的形式定义AOP时,我们有一个@DeclareParents注解可以给所有匹配的bean生成的代理默认加上指定的接口实现,并使用默认的实现。如果是基于XML配置时如果想达到对应的效果我们可以通过declare-parents元素来达到对应的效果。示例如下,切面类SchemaBasedAspect有一个doBefore方法接收一个CommonParent参数,我们在配置切面时配置了一个before Advice将调用切面类的doBefore方法;通过declare-parents元素指定service包下面所有的类都实现CommonParent接口,底层在调用的时候将会调用CommonParentImpl的实现。CommonParent接口的源码没有提供,其只定义了一个返回类型为void的doSomething()方法。然后我们的before Advice将拦截id为userService的bean的所有方法调用,注意因为我们声明了所有的Service都将实现CommonParent接口,所以生成的id为userService的bean其实也是实现了CommonParent接口的,在调用的doBefore方法时传递的就是id为userService的bean,我们在doBefore方法中又继续调用CommonParent的doSomething方法就相当于重新调用了id为userService的bean的方法,会造成before Advice循环拦截和调用。很显然此种情况下,我们是不希望CommonParent的方法调用还被拦截的,所以我们在对应的Pointcut表达式上把CommonParent的doSomething方法排除了。在应用注解的时候不用排除也不会循环调用,但是使用XML配置时必须排除。
代码语言:javascript复制public void doBefore(CommonParent commonParent) {
System.out.println("======================doBefore======================");
commonParent.doSomething();
}
代码语言:javascript复制<bean id="schemaBasedAspect" class="com.elim.spring.aop.aspect.SchemaBasedAspect" />
<bean id="commonParent" class="com.elim.spring.aop.service.CommonParentImpl" />
<!-- 基于XML的AOP配置是基于config元素开始的 -->
<aop:config>
<aop:aspect id="aspect1" ref="schemaBasedAspect" order="1">
<aop:before method="doBefore" pointcut-ref="userServicePointCut" />
<aop:pointcut expression="bean(userService) and this(commonParent) and !execution(void com.elim.spring.aop.service.CommonParent.doSomething())"
id="userServicePointCut" />
<!-- 加上通用的父类 -->
<aop:declare-parents types-matching="com.elim.spring.aop.service..*"
implement-interface="com.elim.spring.aop.service.CommonParent"
delegate-ref="commonParent"/>
</aop:aspect>
</aop:config>
7.5 启用CGLIB代理类
默认情况下当bean实现了接口时Spring AOP是基于JDK的动态代理的,也就是说我们生成的bean只能调用接口中定义的方法,如果我们的bean是没有实现接口的,则会采用CGLIB代理。如果我们希望不管bean是否实现了接口都采用CGLIB代理,则在基于Aspectj注解进行AOP配置时,我们可以通过aspectj-autoproxy元素的proxy-target-class=”true”启用CGLIB代理。
代码语言:javascript复制<aop:aspectj-autoproxy proxy-target-class="true" />
如果是基于XML的配置时,我们也希望强制使用CGLIB代理时怎么办呢?我们可以在config元素上配置proxy-target-class=”true”。
代码语言:javascript复制<aop:config proxy-target-class="true">
</aop:config>
8 advisor标签
advisor标签是需要定义在aspect标签里面的,其作用与aspect类似,可以简单的把它理解为一个特殊的切面,用于把一个Advice和一个Pointcut组合起来。一个advisor标签对应的就是一个Advisor接口的实现类,默认是DefaultBeanFactoryPointcutAdvisor实现。其使用的基本语法类似如下这样。
代码语言:javascript复制<aop:config>
<aop:advisor advice-ref="" pointcut-ref=""/>
</aop:config>
上面的advice-ref属性用于指定一个org.aopalliance.aop.Advice实现,该接口没有任何内容,只是起到标记作用,用于标记某个类是Advice。pointcut-ref用于指定一个通过已经存在的Pointcut定义,当然也可以直接通过pointcut属性指定对应的Pointcut表达式。如果在一个config元素下既定义了aspect,又定义了advisor,那advisor必须定义在aspect之前。接下来看一下如何通过advisor标签应用常用的5种Advice,本文旨在介绍advisor是如何用的,以及如何使用5种Advice,至于每种Advice的功能、区别啥的已经在之前的文章中已经介绍过了,就不再赘述了,有兴趣的读者请参考以前的博文。
8.1 before Advice
对应于切入点方法执行前的拦截的Advice接口是BeforeAdvice接口,这个接口也是一个空接口没有实现,我们在自定义自己的BeforeAdvice实现时不直接实现BeforeAdvice接口,而是实现MethodBeforeAdvice接口。这是Spring为了将来可以支持对类的成员变量的访问进行拦截而预留的定义,也就是说将来BeforeAdvice还会有一个基于对类的成员变量访问的拦截的子接口定义。MethodBeforeAdvice接口中定义了一个before方法,在调入目标方法前就会调用before方法。如下就是一个MethodBeforeAdvice的实现示例。
代码语言:javascript复制public class LogBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("===============before advice start==============");
System.out.println("method: " method);
System.out.println("args: " args);
System.out.println("target: " target);
System.out.println("===============before advice end================");
}
}
定义了Advice之后,我们就可以通过advisor标签来把它和指定的PointCut绑定了。
代码语言:javascript复制<aop:config>
<aop:pointcut expression="bean(userService)" id="userServicePointcut"/>
<aop:advisor advice-ref="logBeforeAdvice" order="1" pointcut-ref="userServicePointcut"/>
</aop:config>
<bean id="logBeforeAdvice" class="com.elim.spring.aop.advice.LogBeforeAdvice" />
8.2 around Advice
around Advice的实现需要实现org.aopalliance.intercept.MethodInterceptor接口,该接口定义了一个接收MethodInvacation类型的参数的invoke方法。通过MethodInvocation对象可以获取到目标方法、方法参数等信息,然后还可以通过调用其proceed方法来调用对应的目标方法,所以我们可以根据需要来判断是否需要调用目标方法。invoke方法的返回值将作为目标方法的调用者接收到的返回值,所以我们也可以在invoke方法中根据需要判断需要给目标方法调用者返回什么样的结果。以下是一个MethodInterceptor接口实现示例。
代码语言:javascript复制public class LogAroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("=============================方法调用开始===" invocation.getMethod());
try {
Object result = invocation.proceed();
System.out.println("=============================方法调用正常结束===" invocation.getMethod());
return result;
} catch (Exception e) {
System.out.println("=============================方法调用异常===" invocation.getMethod());
throw e;
}
}
}
之前介绍基于XML配置和基于Aspectj注解的配置进行Around Advice配置时都可以在运行时根据条件来改变实际调用目标方法时传递的参数,那么如果我们直接实现MethodInterceptor接口是否又可以呢?答案是肯定的,MethodInterceptor的invoke方法参数MethodInvocation中已经封装了或者目标方法和参数的信息,如果需要改变传递的参数,我们可以不调用MethodInvocation的proceed方法,而是选择获取当前Method,然后直接调用Method的invoke方法传递自己所需要的参数。
代码语言:javascript复制public class LogAroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("=============================方法调用开始===" invocation.getMethod());
try {
Object result = invocation.getMethod().invoke(invocation.getThis(), 1);
System.out.println("=============================方法调用正常结束===" invocation.getMethod());
return result;
} catch (Exception e) {
System.out.println("=============================方法调用异常===" invocation.getMethod());
throw e;
}
}
}
其配置与before advice的配置是类似的。
代码语言:javascript复制<aop:config>
<aop:pointcut expression="bean(userService)" id="userServicePointcut"/>
<aop:advisor advice-ref="logAroundAdvice" pointcut-ref="userServicePointcut"/>
</aop:config>
<bean id="logAroundAdvice" class="com.elim.spring.aop.advice.LogAroundAdvice"/>
8.3 afterReturning Advice
AfterReturning Advice将在目标方法正常返回时触发,对应的是AfterReturningAdvice接口,其定义如下,第一个参数是目标方法的返回值。
代码语言:javascript复制public interface AfterReturningAdvice extends AfterAdvice {
void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable;
}
以下是笔者的一个测试实现。
代码语言:javascript复制public class LogAfterReturningAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("==============调用成功返回,返回值是:" returnValue);
System.out.println("Method: " method);
if (returnValue instanceof User) {
//不能修改返回值,但可以修改返回值的某些属性,因为是对象引用
((User) returnValue).setName("modifyedName");
}
}
}
8.4 afterThrowing Advice
afterThrowing Advice对应的Advice接口子类是ThrowsAdvice,该接口也是一个空接口,也是用于标记作用的,但是不同于BeforeAdvice定义了可供用户实现的包含方法定义的子接口MethodBeforeAdvice,ThrowingAdvice没有这样的子接口。这是因为用户可能需要同时对多种异常进行处理,如果把接口方法定义好了,那用户只能在方法体中判断当前捕获的异常类型了。没有方法定义时用户就可以在实现类中定义很多的异常处理方法了,但是这些方法也不是随便定义的,它们必须满足以下形式。其中的方法名必须为afterThrowing,方法参数只有最后一个subclassOfThrowable是必须的。
afterThrowing([Method, args, target], subclassOfThrowable)以下是一个实现示例,在示例中我们一共实现了三个处理Exception的方法,前两个处理方法用于处理特定的异常类型,而且只接收一个异常类型参数,最后一个处理方法接收所有的参数,处理除前两者以外的其它异常。
代码语言:javascript复制public class LogThrowsAdvice implements ThrowsAdvice {
/**
* 处理IllegalArgumentException
* @param e
*/
public void afterThrowing(IllegalArgumentException e) {
System.out.println("=====================方法调用异常,抛出了IllegalArgumentException");
}
/**
* 处理NumberFormatException
* @param e
*/
public void afterThrowing(NumberFormatException e) {
System.out.println("=====================方法调用异常,抛出了NumberFormatException");
}
/**
* 处理其它所有的异常
* @param method
* @param args
* @param target
* @param e
*/
public void afterThrowing(Method method, Object[] args, Object target, Exception e) {
System.out.println("=====================方法调用异常了," e);
System.out.println("Method: " method);
System.out.println("Args: " args);
System.out.println("Target: " target);
}
}
8.5 after Advice
对于After Advice类型的Advice没有特定的接口供我们实现,如果需要自己实现一个Advice可以达到after Advice那样的效果,即无论切入点方法是否抛出异常都执行某些逻辑时,可以使用MethodInterceptor代替,在方法实现中使用try…finally形式即可。
上面配置的完整配置如下。
代码语言:javascript复制<aop:config>
<aop:pointcut expression="bean(userService)" id="userServicePointcut"/>
<aop:advisor advice-ref="logBeforeAdvice" order="1" pointcut-ref="userServicePointcut"/>
<aop:advisor advice-ref="logThrowsAdvice" order="2" pointcut-ref="userServicePointcut" />
<aop:advisor advice-ref="logAfterReturningAdvice" order="3" pointcut-ref="userServicePointcut"/>
<aop:advisor advice-ref="logAroundAdvice" pointcut-ref="userServicePointcut"/>
</aop:config>
<bean id="logBeforeAdvice" class="com.elim.spring.aop.advice.LogBeforeAdvice" />
<bean id="logThrowsAdvice" class="com.elim.spring.aop.advice.LogThrowsAdvice" />
<bean id="logAfterReturningAdvice" class="com.elim.spring.aop.advice.LogAfterReturningAdvice" />
<bean id="logAroundAdvice" class="com.elim.spring.aop.advice.LogAroundAdvice"/>
9 基于正则表达式的Pointcut
JdkRegexpMethodPointcutSpring官方为我们提供了一个基于正则表达式来匹配方法名的Pointcut,JdkRegexpMethodPointcut。该Pointcut是继承自StaticMethodMatcherPointcut的。我们在定义JdkRegexpMethodPointcut时可以通过patterns和excludedPatterns来注入需要满足和排除的正则表达式,它们对应的都是一个String[]。比如我们想匹配所有的方法名以find开头的方法,我们可以如下定义:
代码语言:javascript复制 <bean id="regexPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>find.*</value><!-- 所有方法名以find开始的方法 -->
</list>
</property>
</bean>
如果我们需要匹配或需要排除的正则表达式只是单一的一个正则表达式,那么我们也可以通过pattern和excludedPattern来指定单一的需要匹配和排除的正则表达式。需要注意的是patterns和pattern不能同时使用,excludedPattern和excludedPatterns也是一样的。当我们同时指定了patterns和excludedPatterns时,该Pointcut将先匹配patterns,对于能够匹配patterns的将再判断其是否在excludedPatterns中,如果存在也将不匹配。以下是该匹配逻辑的核心代码。
代码语言:javascript复制 protected boolean matchesPattern(String signatureString) {
for (int i = 0; i < this.patterns.length; i ) {
boolean matched = matches(signatureString, i);
if (matched) {
for (int j = 0; j < this.excludedPatterns.length; j ) {
boolean excluded = matchesExclusion(signatureString, j);
if (excluded) {
return false;
}
}
return true;
}
}
return false;
}
需要说明的是在上面的匹配逻辑中传递的参数signatureString是对应方法的全路径名称,即包含该方法的类的全路径及该方法的名称,如“org.springframework.aop.support.JdkRegexpMethodPointcut.matches”这种,所以如果我们需要在使用正则表达式定义Pointcut时,也可以匹配某某类的某某方法这种形式。
RegexpMethodPointcutAdvisor使用了JdkRegexpMethodPointcut后,我们在使用的时候通常会进行如下配置。
代码语言:javascript复制 <aop:config>
<aop:advisor advice-ref="logBeforeAdvice" pointcut-ref="regexPointcut"/>
</aop:config>
<bean id="logBeforeAdvice" class="com.elim.learn.spring.aop.advice.LogBeforeAdvice" />
<bean id="regexPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>find.*</value><!-- 所有方法名以find开始的方法 -->
</list>
</property>
</bean>
其实针对于JdkRegexpMethodPointcut,Spring为我们提供了一个简便的Advisor定义,可以让我们同时指定一个JdkRegexpMethodPointcut和其需要对应的Advice,那就是RegexpMethodPointcutAdvisor,我们可以给它注入一个Advice和对应需要匹配的正则表达式(pattern或patterns注入)。
代码语言:javascript复制 <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="logBeforeAdvice"/>
<property name="pattern" value="find.*"/>
</bean>
需要注意的是RegexpMethodPointcutAdvisor没有提供不匹配的正则表达式注入方法,即没有excludedPattern和excludedPatterns注入,如果需要该功能请还是使用JdkRegexpMethodPointcut。
10 编程式的Pointcut
除了可以通过注解和Xml配置定义Pointcut之外,其实我们还可以通过程序来定义Pointcut。Spring Aop的切入点(Pointcut)对应于它的一个Pointcut接口,全称是org.springframework.aop.Pointcut。该接口的定义如下:
代码语言:javascript复制public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
该接口一共定义了两个核心方法,一个用于获取该Pointcut对应的过滤Class的ClassFilter对象,一个用于获取过滤Method的MethodMatcher对象。ClassFilter接口的定义如下:
代码语言:javascript复制public interface ClassFilter {
boolean matches(Class<?> clazz);
ClassFilter TRUE = TrueClassFilter.INSTANCE;
}
该接口只定义了一个matches方法,用于判断指定的Class对象是否匹配当前的过滤规则。MethodMatcher接口定义如下:
代码语言:javascript复制public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method method, Class<?> targetClass, Object[] args);
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}
该接口中一共定义了三个方法,两个matches方法,一个包含方法参数一个不包含。不包含方法参数的matches方法用于判断非运行时的方法匹配,比如只需要匹配方法名、方法参数定义的;包含方法参数值的matches方法用于运行时判断方法是否匹配,应用于需要根据方法传参来判断是否匹配的情况,但是该方法一般会在不包含方法参数的matches方法返回true和isRuntime()方法true的情形下才会调用。isRuntime()方法用于指定该Pointcut是否需要在运行时才能判断对应的方法是否匹配。
10.1 自定义Pointcut
以下是一个自定义Pointcut的代码,其将匹配所有的名称Service结尾的Class对应的名称以find开始的方法调用:
代码语言:javascript复制import java.lang.reflect.Method;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
/**
* 自定义Pointcut
* @author Elim
* 2017年5月8日
*/
public class MyCustomPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return new MyCustomClassFilter();
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyCustomMethodMatcher();
}
private static class MyCustomClassFilter implements ClassFilter {
@Override
public boolean matches(Class<?> clazz) {
//实现自己的判断逻辑,这里简单的判断对应Class的名称是以Service结尾的就表示匹配
return clazz.getName().endsWith("Service");
}
}
private static class MyCustomMethodMatcher implements MethodMatcher {
@Override
public boolean matches(Method method, Class<?> targetClass) {
//实现方法匹配逻辑
return method.getName().startsWith("find");
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object[] args) {
return false;
}
}
}
然后我们可以定义该自定义Pointcut对应的bean,再定义一个Advisor将使用该Pointcut。如下示例中我们就指定了将在MyCustomPointcut对应的切入点处采用LogAroundAdvice。
代码语言:javascript复制 <aop:config>
<aop:advisor advice-ref="logAroundAdvice" pointcut-ref="myCustomPointcut"/>
</aop:config>
<bean id="logAroundAdvice" class="com.elim.learn.spring.aop.advice.LogAroundAdvice"/>
<bean id="myCustomPointcut" class="com.elim.learn.spring.aop.pointcut.MyCustomPointcut"/>
10.2 继承自现有的Pointcut
除了可以完全实现Pointcut接口外,我们还可以直接使用Spring自带的Pointcut。比如基于固定方法的StaticMethodMatcherPointcut。该Pointcut是一个抽象类,在使用该Pointcut时只需要实现一个抽象方法matches(Method method, Class<?> targetClass),以下是一个继承自StaticMethodMatcherPointcut的示例类定义,该Pointcut将匹配所有Class中定义的方法名以find开头的方法。
代码语言:javascript复制public class FindMethodMatcherPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return method.getName().startsWith("find");
}
}
关于更多Spring官方已经提供的其它Pointcut定义请参考Spring的API文档。
11 编程式的创建Aop代理之ProxyFactory
Spring Aop是基于代理的,ProxyFactory是Spring Aop内部用来创建Proxy对象的一个工厂类。如果我们需要在程序运行时来动态的应用Spring Aop,则我们可以考虑使用ProxyFactory。使用ProxyFactory时,我们需要为它指定我们需要代理的目标对象、代理时我们需要使用的Advisor或Advice。如下示例就是一个简单的使用ProxyFactory创建MyService对象的代理,同时对其应用了一个MethodBeforeAdvice,即每次调用代理对象的方法时都将先调用MethodBeforeAdvice的before方法。
代码语言:javascript复制 @Test
public void testProxyFactory() {
MyService myService = new MyService();
ProxyFactory proxyFactory = new ProxyFactory(myService);
proxyFactory.addAdvice(new MethodBeforeAdvice() {
@Override
public void before(Method method, Object[] args,
Object target) throws Throwable {
System.out.println("执行目标方法调用之前的逻辑");
//不需要手动去调用目标方法,
//Spring内置逻辑里面会调用目标方法
}
});;
MyService proxy = (MyService) proxyFactory.getProxy();
proxy.add();
}
11.1 指定被代理对象
ProxyFactory有多个重载的构造函数,上面示例中笔者用的是指定被代理对象的构造函数,如果我们应用的是其它构造函数,则可以通过ProxyFactory的setTarget(Object)方法来指定被代理对象。如果我们没有指定被代理对象的Class,那么默认创建出来的代理对象是我们传递的被代理对象的类型,即获取的是targetObject.getClass()类型。如果我们的被代理对象的类型是包含多个接口实现或父类型的,而我们只希望代理其中的某一个类型时,我们可以通过ProxyFactory的setTargetClass(Class)来指定创建的代理对象是基于哪个Class的。默认情况下,ProxyFactory会根据实际情况选择创建的代理对象是基于JDK代理的还是基于CBLIB代理的,即目标对象拥有接口实现且没有设置proxyTargetClass="true"或者指定的targetClass是一个接口的时候将采用JDK代理,否则将采用CGLIB代理。也就是说即算是你通过ProxyFactory.setProxyTargetClass(true)指定了将会建立基于Class的CGLIB代理,最终也不一定是CGLIB代理,因为这种情况下如果targetClass是一个接口也将建立JDK代理。这块的逻辑是由DefaultAopProxyFactory的createAopProxy()方法实现的,其源码如下。
代码语言:javascript复制 public AopProxy createAopProxy(AdvisedSupport config)
throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass()
|| hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: "
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface()) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
ProxyFactory底层在创建代理对象的时候实际上是会委托给AopProxyFactory对象的,AopProxyFactory是一个接口,其只定义了一个createAopProxy()方法,Spring提供了一个默认实现,DefaultAopProxyFactory。ProxyFactory中使用的就是DefaultAopProxyFactory,有兴趣的朋友可以参考一下ProxyFactory的源代码。
11.2 指定Advisor
使用Aop时我们是需要对拦截的方法做一些处理的,对于Spring Aop来讲,需要对哪些方法调用做什么样的处理是通过Advisor来定义的,通常是一个PointcutAdvisor。PointcutAdvisor接口中包含主要有两个接口方法,一个用来获取Pointcut,一个用来获取Advice对象,它俩的组合就构成了需要在哪个Pointcut应用哪个Advice。所以有需要的时候我们也可以实现自己的Advisor实现。
代码语言:javascript复制/**
* 简单的实现自己的PointcutAdvisor
* @author Elim 2017年5月9日
*/
public class MyAdvisor implements PointcutAdvisor {
@Override
public Advice getAdvice() {
return new MethodBeforeAdvice() {
@Override
public void before(Method method, Object[] args,
Object target) throws Throwable {
System.out.println("BeforeAdvice实现,在目标方法被调用前调用,目标方法是:"
method.getDeclaringClass().getName() "."
method.getName());
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
@Override
public Pointcut getPointcut() {
//匹配所有的方法调用
return Pointcut.TRUE;
}
}
@Test
public void testProxyFactory2() {
MyService myService = new MyService();
ProxyFactory proxyFactory = new ProxyFactory(myService);
proxyFactory.addAdvisor(new MyAdvisor());
MyService proxy = (MyService) proxyFactory.getProxy();
proxy.add();
}
上述示例就是一个指定代理对象对应的Advisor的示例。其实一个代理对象可以同时绑定多个Advisor对象,ProxyFactory的addAdvisor()方法可多次被调用,且该方法还有一些重载的方法定义,可以参数Spring的API文档。
11.3 指定Advice
我们的第一个示例指定的代理对象绑定的是一个Advice,而第二个示例指定的Advisor,对此你会不会有什么疑问呢?依据我们对Spring Aop的了解,Spring的Aop代理对象绑定的就一定是一个Advisor,而且通常是一个PointcutAdvisor,通过它我们可以知道我们的Advice究竟是要应用到哪个Pointcut(哪个方法调用)?当我们通过ProxyFactory在创建代理对象时绑定的是一个Advice对象时,实际上ProxyFactory内部还是为我们转换为了一个Advisor对象的,只是该Advisor对象对应的Pointcut是一个匹配所有方法调用的Pointcut实例。
11.4 指定是否需要发布代理对象
在调用Aop代理对象的方法时,默认情况下我们是不能访问到当前的代理对象的,如果我们指定了创建的代理对象需要对外发布代理对象,那么在调用代理对象的方法时Spring会把当前的代理对象存入AopContext中,我们就可以在目标对象的方法中通过AopContext中获取到当前的代理对象了。这是通过exposeProxy属性来指定的,如果我们希望对外发布代理对象,我们可以通过exposeProxy的set方法来指定该属性的值为true。如:
代码语言:javascript复制 @Test
public void testProxyFactory2() {
MyService myService = new MyService();
ProxyFactory proxyFactory = new ProxyFactory(myService);
proxyFactory.setExposeProxy(true);//指定对外发布代理对象,即在目标对象方法中可以通过AopContext.currentProxy()访问当前代理对象。
proxyFactory.addAdvisor(new MyAdvisor());
proxyFactory.addAdvisor(new MyAdvisor());//多次指定Advisor将同时应用多个Advisor
MyService proxy = (MyService) proxyFactory.getProxy();
proxy.add();
}
除了上述配置信息以外,ProxyFactory其实还可以配置很多其它的信息,更多的配置信息项请参考ProxyFactory的源代码或参考Spring API文档。
12 编程式的创建Aop代理之AspectjProxyFactory
之前已经介绍了一款编程式的创建Aop代理的工厂——ProxyFactory,其实ProxyFactory拥有的功能AspectjProxyFactory都有。它们虽然没有直接的继承关系,但是它们都继承自ProxyCreatorSupport,而创建代理对象的核心逻辑都是在ProxyCreatorSupport中实现的。所以说ProxyFactory拥有的功能AspectjProxyFactory都有。那么AspectjProxyFactory与ProxyFactory相比有什么不同呢?AspectjProxyFactory的特殊之处就在于其可以直接指定需要创建的代理对象需要绑定的切面。在使用ProxyFactory时,我们能够绑定的是Advisor或Advice,但是如果我们的程序中已经有了现成的切面类定义且能够为我们新创建的代理类使用时,我们还要为了ProxyFactory建立代理对象的需要创建对应的Advisor类、Advice类和Pointcut类定义,这无疑是非常麻烦的。AspectjProxyFactory通常用于创建基于Aspectj风格的Aop代理对象。现假设我们有如下这样一个切面类定义。
代码语言:javascript复制@Aspect
public class MyAspect {
@Pointcut("execution(* add(..))")
private void beforeAdd() {}
@Before("beforeAdd()")
public void before1() {
System.out.println("-----------before-----------");
}
}
在上述切面类定义中我们定义了一个Advisor,其对应了一个BeforeAdvice,实际上是一个AspectJMethodBeforeAdvice,该Advice对应的是上面的before1()方法,还对应了一个Pointcut,实际上是一个AspectJExpressionPointcut。该Advisor的语义就是调用所有的方法名为“add”的方法时都将在调用前调用MyAspect.before1()方法。如果我们现在需要创建一个代理对象,其需要绑定的Advisor逻辑跟上面定义的切面类中定义的Advisor类似。则我们可以进行如下编程。
代码语言:javascript复制@Test
public void testAspectJProxyFactory() {
MyService myService = new MyService();
AspectJProxyFactory proxyFactory = new AspectJProxyFactory(myService);
proxyFactory.addAspect(MyAspect.class);
proxyFactory.setProxyTargetClass(true);//是否需要使用CGLIB代理
MyService proxy = proxyFactory.getProxy();
proxy.add();
}
在上述代码中我们AspectjProxyFactory在创建代理对象时需要使用的切面类(其实addAspect还有一个重载的方法可以指定一个切面类的对象),其实在AspectjProxyFactory内部还是解析了该切面类中包含的所有的Advisor,然后把能够匹配当前代理对象类的Advisor与创建的代理对象绑定了。有兴趣的读者可以查看一下AspectjProxyFactory的源码,以下是部分核心代码。
代码语言:javascript复制public void addAspect(Class<?> aspectClass) {
String aspectName = aspectClass.getName();
AspectMetadata am = createAspectMetadata(aspectClass, aspectName);
MetadataAwareAspectInstanceFactory instanceFactory =
createAspectInstanceFactory(am, aspectClass, aspectName);
addAdvisorsFromAspectInstanceFactory(instanceFactory);
}
private void addAdvisorsFromAspectInstanceFactory(
MetadataAwareAspectInstanceFactory instanceFactory) {
List<Advisor> advisors = this.aspectFactory.getAdvisors(instanceFactory);
advisors = AopUtils.findAdvisorsThatCanApply(advisors, getTargetClass());
AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(advisors);
OrderComparator.sort(advisors);
addAdvisors(advisors);
}
需要注意的是在使用AspectjProxyFactory基于切面类创建代理对象时,我们指定的切面类上必须包含@Aspect注解。
另外需要注意的是虽然我们自己通过编程的方式可以通过AspectjProxyFactory创建基于@Aspect标注的切面类的代理,但是通过配置aop:aspectj-autoproxy/使用基于注解的Aspectj风格的Aop时,Spring内部不是通过AspectjProxyFactory创建的代理对象,而是通过ProxyFactory。有兴趣的朋友可以查看一下AnnotationAwareAspectjAutoProxyCreator的源代码。
13. ProxyFactoryBean创建代理对象
ProxyFactoryBean实现了Spring的FactoryBean接口,所以它跟Spring中的其它FactoryBean一样,都是基于工厂模式来获取一个bean的。ProxyFactoryBean就是用来获取一个对象的代理对象的FactoryBean。它也是继承自ProxyCreatorSupport类的,所以它的功能基本跟ProxyFactory差不多,只是ProxyFactory是用于编程式的创建代理对象。而ProxyFactoryBean用于在Spring的bean容器中创建基于bean的代理对象。通常一个简单的ProxyFactoryBean配置大概会是如下这样。
代码语言:javascript复制<bean id="proxyFactoryBeanTestService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target"><!-- 指定被代理的对象 -->
<bean class="com.elim.learn.spring.aop.service.ProxyFactoryBeanTestService"/>
</property>
<property name="proxyTargetClass" value="true"/><!-- 指定启用基于Class的代理 -->
<!-- 指定生成的代理对象需要绑定的Advice或Advisor在bean容器中的名称 -->
<property name="interceptorNames">
<list>
<value>logAroundAdvice</value>
</list>
</property>
</bean>
在上面的示例中我们被代理的对象对应的Class是com.elim.learn.spring.aop.service.ProxyFactoryBeanTestService,其是一个没有实现任何接口的Class,所以我们生成的代理对象最终会是基于CGLIB的代理。我们需要指定proxyTargetClass="true",以表示我们是倾向于使用CGLIB代理的,对于上面的配置实际上就是告诉Spring我们要使用CGLIB代理。虽然这里我们不指定proxyTargetClass="true"时,Spring可能也会给我们使用CGLIB代理,为什么这里说是可能呢?因为ProxyFactoryBean默认生成的bean都是单例、且在生成bean时会自动检测被代理对象实现的接口,而且proxyTargetClass默认是false,这种情况下ProxyFactoryBean就会自动检测被代理对象的实现的接口。按理来说我们的bean是一个没有实现接口的bean,Spring给我们去找它实现的接口是找不出来的,但是我们知道Spring的Aop是会自动为我们的对象实现一些接口的。简单的说如果我们的bean容器中配置了其它的Advisor,那么我们指定的target对象有可能就不是一个原始的bean,而是一个已经被Aop代理过的bean对象,这种bean对象,Spring Aop默认会为其实现一个Advised接口。所以使用ProxyFactoryBean时,如果我们的代理对象类是没有实现接口的,或者我们期望生成代理对象时是基于Class的,而不是基于Interface的,我们最好明确的指定proxyTargetClass="true",而不是寄希望于Spring的自动决定机制。指定被代理对象时,除了可以直接指定target外,我们还可以通过targetName指定被代理对象在bean容器中的bean名称。在上面的示例中我们通过interceptorNames属性指定了生成的代理对象需要应用的Advisor/Advice对应于bean容器中的bean的名称。跟ProxyFactory一样,如果我们指定的是Advice对象,则其会转换为一个匹配所有方法调用的Advisor与代理对象绑定。在指定intercepterNames时我们也可以通过“*
”指定所有beanName以XX开始的Advisor/Advice,如我们的bean容器中同时拥有“abc1Advisor”、“abc2Advisor”两个Advisor,我们期望创建的ProxyFactoryBean同时应用这两个Advisor,那我们可以不用单独指定两次,而是一次性把interceptorNames指定的一个beanName为“abc*
”。需要注意的是“*
”只能定义在beanName的末端。
<bean id="proxyFactoryBeanTestService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target"><!-- 指定被代理的对象 -->
<bean class=" com.elim.learn.spring.aop.service.ProxyFactoryBeanTestService"/>
</property>
<property name="proxyTargetClass" value="true"/><!-- 指定启用基于Class的代理 -->
<!-- 指定生成的代理对象需要绑定的Advice或Advisor在bean容器中的名称 -->
<property name="interceptorNames">
<list>
<value>abc*</value>
</list>
</property>
</bean>
其它配置信息
- exposeProxy:属于从ProxyCreatorSupport继承过来的属性,用于定义是否需要在调用代理对象时把代理对象发布到AopContext,默认是false。
- singleton:用来指定ProxyFactoryBean生成的bean是否是单例的,默认是true。该值对应于FactoryBean的isSingleton()接口方法的返回值。
- frozen:属于从ProxyCreatorSupport继承过来的属性,用于指定代理对象被创建后是否还允许更改代理配置,通过Advised接口更改。true表示不允许,默认是false。
- autodetectInterfaces:表示是否在生成代理对象时需要启用自动检测被代理对象实现的接口,默认是true。
- proxyInterfaces:基于接口的代理时指定需要代理的接口。
- interfaces:基于接口的代理时指定需要代理的接口,属于从ProxyCreatorSupport继承过来的。关于ProxyFactoryBean的更多配置项信息可以参考Spring的API文档或ProxyFactoryBean的源代码。
14 Aop自动创建代理对象的原理
我们在使用Spring Aop时,通常Spring会自动为我们创建目标bean的代理对象,以使用对应的Advisor。前提是我们在使用Spring Aop时是使用的aop:config/或aop:aspectj-autoproxy/,这是因为当我们在applicationContext.xml文件中通过aop:config/的形式定义需要使用Aop的场景时,Spring会自动为我们添加AspectjAwareAdvisorAutoProxyCreator类型的bean;而我们定义了aop:aspectj-autoproxy/时,Spring会默认为我们添加AnnotationAwareAspectjAutoProxyCreator类型的bean。
Spring中在bean实例化后能够对bean对象进行包装的是BeanPostProcessor,AspectjAwareAdvisorAutoProxyCreator和AnnotationAwareAspectjAutoProxyCreator都是实现了BeanPostProcessor接口的。
AnnotationAwareAspectjAutoProxyCreator的父类是AspectjAwareAdvisorAutoProxyCreator,而AspectjAwareAdvisorAutoProxyCreator的父类是AbstractAdvisorAutoProxyCreator,AbstractAdvisorAutoProxyCreator的父类是实现了BeanPostProcessor接口的AbstractAutoProxyCreator。
它们的核心逻辑都是在bean初始化后找出bean容器中所有的能够匹配当前bean的Advisor,找到了则将找到的Advisor通过ProxyFactory创建该bean的代理对象返回。AspectjAwareAdvisorAutoProxyCreator在寻找候选的Advisor时会找到bean容器中所有的实现了Advisor接口的bean,而AnnotationAwareAspectjAutoProxyCreator则在AspectjAwareAdvisorAutoProxyCreator的基础上增加了对标注了@Aspect的bean的处理,会附加上通过@Aspect标注的bean中隐式定义的Advisor。所以这也是为什么我们在使用@Aspect标注形式的Spring Aop时需要在applicationContext.xml文件中添加aop:aspectj-autoproxy/。
既然AspectjAwareAdvisorAutoProxyCreator和AnnotationAwareAspectjAutoProxyCreator都会自动扫描bean容器中的Advisor,所以当我们使用了aop:config/或aop:aspectj-autoproxy/形式的Aop定义时,如果因为某些原因光通过配置满足不了你Aop的需求,而需要自己实现Advisor接口时(一般是实现PointcutAdvisor接口),那这时候你只需要把自己的Advisor实现类,定义为Spring的一个bean即可。如果你在applicationContext.xml中没有定义aop:config/或aop:aspectj-autoproxy/,那你也可以直接在applicationContext.xml中直接定义AspectjAwareAdvisorAutoProxyCreator或AnnotationAwareAspectjAutoProxyCreator类型的bean,效果也是一样的。其实为了能够在创建目标bean的时候能够自动创建基于我们自定义的Advisor实现类的代理对象,我们的bean容器中只要有AbstractAutoProxyCreator类型的bean定义即可,当然了你实现自己的BeanPostProcessor,在其postProcessAfterInitialization方法中创建自己的代理对象也是可以的。
但是本着不重复发明轮子的原则,我们尽量使用官方已经提供好的实现即可。AbstractAutoProxyCreator是继承自ProxyConfig的,所以我们在定义AbstractAutoProxyCreator子类的bean时,我们也可以手动的定义一些ProxyConfig中包含的属性,比如proxyTargetClass、exposeProxy等。AbstractAutoProxyCreator的子类除了AspectjAwareAdvisorAutoProxyCreator和AnnotationAwareAspectjAutoProxyCreator外,我们可用的还有BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator。
14.1 BeanNameAutoProxyCreator
BeanNameAutoProxyCreator可以用来定义哪些bean可与哪些Advisor/Advice绑定,以生成对应的代理对象。需要绑定的bean是通过beanNames属性来指定的,对应的是bean的名称,其中可以包含“”号,表示任意字符,比如“abc”则匹配任意名称以“abc”开始的bean;需要绑定的Advisor/Advice是通过interceptorNames来指定的,如果指定的是Advisor,那么是否可生成基于该Advisor的代理需要对应的bean能够匹配对应Advisor(PointcutAdvisor类型)的Pointcut;如果指定的是Advice,则该Advice会被包含在Pointcut恒匹配的Advisor中,即能够与所有的bean绑定生成对应的代理,且会对所有的方法调用起作用。指定interceptorNames时是不能使用通配符的,只能精确的指定需要应用的Advisor/Advice对应的bean名称。
代码语言:javascript复制<bean id="userService" class="com.elim.learn.spring.aop.service.UserServiceImpl"/>
<bean id="myService" class="com.elim.learn.spring.aop.service.MyService"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<!-- 匹配userService和所有名称以my开头的bean -->
<property name="beanNames" value="userService, my*"/>
<!-- interceptorNames中不能使用通配符,只能是精确匹配,
即精确指定Advisor/Advice的bean名称 -->
<property name="interceptorNames" value="logBeforeAdvice, myAdvisor "/>
</bean>
<bean id="logBeforeAdvice"
class="com.elim.learn.spring.aop.advice.LogBeforeAdvice" />
<bean id="myAdvisor" class="com.elim.learn.spring.aop.advisor.MyAdvisor"/>
如上就是一个使用BeanNameAutoProxyCreator建立指定的bean基于指定的Advisor/Advice的代理对象的示例。示例中我们指定interceptorNames时特意应用了一个Advisor实现和一个Advice实现。Advice会应用于所有绑定的bean的所有方法调用,而Advisor只会应用于其中的Pointcut能够匹配的方法调用。这里的源码我就不提供了,有兴趣的朋友可以自己试试。
14.2 DefaultAdvisorAutoProxyCreator
DefaultAdvisorAutoProxyCreator的父类也是AbstractAdvisorAutoProxyCreator。DefaultAdvisorAutoProxyCreator的作用是会默认将bean容器中所有的Advisor都取到,如果有能够匹配某一个bean的Advisor存在,则会基于能够匹配该bean的所有Advisor创建对应的代理对象。需要注意的是DefaultAdvisorAutoProxyCreator在创建bean的代理对象时是不会考虑Advice的,只是Advisor。如上面的示例中,如果我们希望所有的bean都能够自动的与匹配的Advisor进行绑定生成对应的代理对象,那么我们可以调整配置如下。
代码语言:javascript复制<bean id="userService" class="com.elim.learn.spring.aop.service.UserServiceImpl"/>
<bean id="myService" class="com.elim.learn.spring.aop.service.MyService"/>
<bean
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean id="myAdvisor" class="com.elim.learn.spring.aop.advisor.MyAdvisor"/>
使用DefaultAdvisorAutoProxyCreator时可能我们并不希望为所有的bean定义都自动应用bean容器中的所有Advisor,而只是希望自动创建基于部分Advisor的代理对象。这个时候如果我们期望应用自动代理的Advisor的bean定义的名称都是拥有固定的前缀时,则我们可以应用DefaultAdvisorAutoProxyCreator的setAdvisorBeanNamePrefix(String)指定需要应用的Advisor的bean名称的前缀,同时需要通过setUsePrefix(boolean)指定需要应用这种前缀匹配机制。如我们的bean容器中有两个Advisor定义,一个bean名称是“myAdvisor”,一个bean名称是“youAdvisor”,如果只期望自动创建基于bean名称以“my”开始的Advisor的代理,则可以进行如下配置。
代码语言:javascript复制<bean
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
<property name="usePrefix" value="true" />
<!-- 匹配所有bean名称以my开始的Advisor -->
<property name="advisorBeanNamePrefix" value="my" />
</bean>
15 Spring Aop原理之Advised接口
通过之前我们介绍的ProxyFactory我们知道,Spring Aop是通过ProxyFactory来创建代理对象的。ProxyFactory在创建代理对象时会委托给DefaultAopProxyFactory.createAopProxy(AdvisedSupport config),DefaultAopProxyFactory内部会分情况返回基于JDK的JdkDynamicAopProxy或基于CGLIB的ObjenesisCglibAopProxy,它俩都实现了Spring的AopProxy接口。AopProxy接口中只定义了一个方法,getProxy()方法,Spring Aop创建的代理对象也就是该接口方法的返回结果。
我们先来看一下基于JDK代理的JdkDynamicAopProxy的getProxy()的逻辑。
代码语言:javascript复制@Override
public Object getProxy() {
return getProxy(ClassUtils.getDefaultClassLoader());
}
@Override
public Object getProxy(ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is "
this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils
.completeProxiedInterfaces(this.advised);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
我们可以看到它最终是通过JDK的Proxy来创建的代理,使用的InvocationHandler实现类是它本身,而使用的接口是AopProxyUtils.completeProxiedInterfaces(this.advised)的返回结果。而这个this.advised对象是AdvisedSupport类型,它是ProxyFactory的父类(间接通过ProxyCreatorSupport继承,ProxyFactory的直接父类是ProxyCreatorSupport,ProxyCreatorSupport的父类是AdvisedSupport),AdvisedSupport的父类是ProxyConfig,ProxyConfig中包含创建代理对象时的一些配置项信息。以下是AopProxyUtils.completeProxiedInterfaces(this.advised)的内部逻辑。
代码语言:javascript复制public static Class<?>[] completeProxiedInterfaces(AdvisedSupport advised) {
Class<?>[] specifiedInterfaces = advised.getProxiedInterfaces();
if (specifiedInterfaces.length == 0) {
// No user-specified interfaces:
//check whether target class is an interface.
Class<?> targetClass = advised.getTargetClass();
if (targetClass != null && targetClass.isInterface()) {
specifiedInterfaces = new Class<?>[] {targetClass};
}
}
boolean addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class);
boolean addAdvised = !advised.isOpaque()
&& !advised.isInterfaceProxied(Advised.class);
int nonUserIfcCount = 0;
if (addSpringProxy) {
nonUserIfcCount ;
}
if (addAdvised) {
nonUserIfcCount ;
}
Class<?>[] proxiedInterfaces = new Class<?>[specifiedInterfaces.length
nonUserIfcCount];
System.arraycopy(specifiedInterfaces, 0,
proxiedInterfaces, 0, specifiedInterfaces.length);
if (addSpringProxy) {
proxiedInterfaces[specifiedInterfaces.length]
= SpringProxy.class;
}
if (addAdvised) {
proxiedInterfaces[proxiedInterfaces.length - 1] = Advised.class;
}
return proxiedInterfaces;
}
我们可以看到其会在!advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)返回true的情况下加上本文的主角Advised接口。isOpaque()是ProxyConfig中的一个方法,对应的是opaque属性,表示是否禁止将代理对象转换为Advised对象,默认是false。!advised.isInterfaceProxied(Advised.class)表示将要代理的目标对象类没有实现Advised接口,对于我们自己应用的Class来说,一般都不会自己去实现Advised接口的,所以这个通常也是返回true,所以通常创建Aop代理对象时是会创建包含Advised接口的代理对象的,即上述的proxiedInterfaces[proxiedInterfaces.length - 1] = Advised.class会被执行。前面我们已经提到,JdkDynamicAopProxy创建代理对象应用的InvocationHandler是其自身,所以我们在调用JdkDynamicAopProxy创建的代理对象的任何方法时都将调用JdkDynamicAopProxy实现的InvocationHandler接口的invoke(Object proxy, Method method, Object[] args)方法。该方法实现如下:
代码语言:javascript复制public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Class<?> targetClass = null;
Object target = null;
try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement
// the equals(Object) method itself.
return equals(args[0]);
}
if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode()
// method itself.
return hashCode();
}
if (!this.advised.opaque
&& method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(
this.advised, method, args);
}
Object retVal;
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// May be null. Get as late as possible to
// minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
if (target != null) {
targetClass = target.getClass();
}
// Get the interception chain for this method.
List<Object> chain = this.advised
.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// Check whether we have any advice. If we don't,
// we can fallback on direct
// reflective invocation of the target,
// and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation:
//just invoke the target directly
// Note that the final invoker must be an
// InvokerInterceptor so we know it does
// nothing but a reflective operation on the target,
// and no hot swapping or fancy proxying.
retVal = AopUtils.invokeJoinpointUsingReflection(target,
method, args);
} else {
// We need to create a method invocation...
invocation = new ReflectiveMethodInvocation(proxy,
target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}
// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType.isInstance(proxy) &&
!RawTargetAccess.class
.isAssignableFrom(method.getDeclaringClass())) {
// Special case: it returned "this" and
// the return type of the method
// is type-compatible. Note that we can't help
// if the target sets
// a reference to itself in another returned object.
retVal = proxy;
} else if (retVal == null && returnType != Void.TYPE
&& returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: "
method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
其中关于Advised接口方法调用最核心的一句是如下这句。我们可以看到,当我们调用的目标方法是定义自Advised接口时,对应方法的调用将委托给AopUtils.invokeJoinpointUsingReflection(this.advised, method, args),invokeJoinpointUsingReflection方法的逻辑比较简单,是通过Java反射来调用目标方法。在这里invokeJoinpointUsingReflection传递的目标对象正是AdvisedSupport类型的this.advised对象。
代码语言:javascript复制if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}
AdvisedSupport类是实现了Advised接口的,所以Spring Aop创建了基于Advised接口的代理对象后在调用Advised接口方法时可以把它委托给AdvisedSupport。而我们知道Spring Aop代理对象的创建正是基于AdvisedSupport的配置进行的(配置项主要都定义在AdvisedSupport的父类ProxyConfig类中)。创建代理对象时应用AdvisedSupport,调用Advised接口方法也用同一个实现了Advised接口的AdvisedSupport对象,所以这个过程在Spring Aop内部就可以很好的衔接。接着我们来看一下Advised接口的定义。
代码语言:javascript复制public interface Advised extends TargetClassAware {
boolean isFrozen();
boolean isProxyTargetClass();
Class<?>[] getProxiedInterfaces();
boolean isInterfaceProxied(Class<?> intf);
void setTargetSource(TargetSource targetSource);
TargetSource getTargetSource();
void setExposeProxy(boolean exposeProxy);
boolean isExposeProxy();
void setPreFiltered(boolean preFiltered);
boolean isPreFiltered();
Advisor[] getAdvisors();
void addAdvisor(Advisor advisor) throws AopConfigException;
void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
boolean removeAdvisor(Advisor advisor);
void removeAdvisor(int index) throws AopConfigException;
int indexOf(Advisor advisor);
boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
void addAdvice(Advice advice) throws AopConfigException;
void addAdvice(int pos, Advice advice) throws AopConfigException;
boolean removeAdvice(Advice advice);
int indexOf(Advice advice);
String toProxyConfigString();
}
Advised接口中定义的方法还是非常多的,通过它我们可以在运行时了解我们的代理对象是基于CGLIB的还是基于JDK代理的;可以了解我们的代理对应应用了哪些Advisor;也可以在运行时给我们的代理对象添加和删除Advisor/Advise。本文旨在描述Spring Aop在创建代理对象时是如何基于Advised接口创建代理的,以及我们能够应用Advised接口做哪些事。文中应用的是Spring创建基于JDK代理对象的过程为示例讲解的,其实基于CGLIB的代理也是一样的。关于CGLIB的代理过程、本文中描述的一些核心类以及本文的核心——Advised接口的接口方法说明等请有兴趣的朋友参考Spring的API文档和相关的源代码。
16 编程式的自定义Advisor
16.1 概述
大多数情况下,我们的Aop应用都可以通过Spring的Aop配置来进行(不管是基于注解的,还是基于XML配置的)。Spring Aop的核心就是Advisor,Advisor接口中暂时有用的就是getAdvice()方法,而isPerInstance()方法官方说暂时还没有应用到,生成的Advisor是单例还是多例不由isPerInstance()的返回结果决定,而由自己在定义bean的时候控制。
代码语言:javascript复制public interface Advisor {
Advice getAdvice();
boolean isPerInstance();
}
我们在使用Advisor时不会直接实现Advisor的接口,而是实现Advisor接口的子接口,PointcutAdvisor或IntroductionAdvisor。IntroductionAdvisor个人感觉用处不大,我们之前介绍的@DeclareParents和aop:declare-parents/就属于IntroductionAdvisor使用,它们对应的是DeclareParentsAdvisor。剩下的大部分应用的都是PointcutAdvisor。PointcutAdvisor接口的定义如下。
代码语言:javascript复制public interface PointcutAdvisor extends Advisor {
Pointcut getPointcut();
}
我们可以看到它在Advisor接口的基础上新增了一个getPointcut()方法,用以指定我们的Advisor需要应用到哪些Pointcut,即哪些方法调用。编程式的Pointcut定义之前已经介绍过了,它不属于本文介绍的范畴,这里就不再赘述了,对这块不是很了解的读者建议从头看起,笔者的博文是系列博文,当然了也可以暂时先略过,直接看笔者下文的示例。
16.2 实现自定义的Advisor
以下是笔者实现的一个自定义的Advisor,是实现的PointcutAdvisor接口。应用的Advice是MethodBeforeAdvice的实现;应用的Pointcut简单匹配所有类的方法名为find的方法调用。
代码语言:javascript复制public class MyAdvisor implements PointcutAdvisor {
@Override
public Advice getAdvice() {
return new MethodBeforeAdvice() {
@Override
public void before(Method method,
Object[] args, Object target) throws Throwable {
System.out.println("BeforeAdvice实现,在目标方法被调用前调用,目标方法是:"
method.getDeclaringClass().getName() "."
method.getName());
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
@Override
public Pointcut getPointcut() {
/**
* 简单的Pointcut定义,匹配所有类的find方法调用。
*/
return new Pointcut() {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MethodMatcher() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
String methodName = method.getName();
if ("find".equals(methodName)) {
return true;
}
return false;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass,
Object[] args) {
return false;
}
};
}
};
}
}
16.3 配置使用自定义的Advisor
有了自定义的Advisor后我们应该如何来应用它呢?这又区分好几种情况。
如果是自己通过编程应用ProxyFactory,或者说是应用ProxyCreatorSupport来创建代理对象,那么我们通过AdvisedSupport.addAdvisor(Advisor advisor)来应用我们自定义的Advisor。AdvisedSupport的子类中有ProxyCreatorSupport。如果我们的项目中已经应用了aop:aspectj-autoproxy/或aop:config,那么我们定义在bean容器中的Advisor bean会自动应用到匹配的bean上。这个在《Spring Aop原理之自动创建代理对象》一文中有详细介绍。如果项目中没有应用aop:aspectj-autoproxy/或aop:config,我们就需要自己定义BeanNameAutoProxyCreator、DefaultAdvisorAutoProxyCreator等AbstractAdvisorAutoProxyCreator类型的bean了。或者是定义AnnotationAwareAspectjAutoProxyCreator或AspectJAwareAdvisorAutoProxyCreator类型的bean,其实aop:aspectj-autoproxy/就是自动定义了AnnotationAwareAspectjAutoProxyCreator类型的bean,aop:config就是自动定义了AspectJAwareAdvisorAutoProxyCreator类型的bean。这样在创建bean后都会寻找匹配的Advisor建立对应的代理对象。