spring源码阅读(三)-Spring AOP用法和理解
AOP概念
面向切面编程,在不影响原有业务代码的基础上在原有方法执行前,方法返回后,方法出现异常时,进行拦截处理进行控制或增强
Spring AOP
Spring提供的AOP框架,使用了和AspectJ一样的注解,但是通过动态生成代理类的方式生成AOP代理类。
- 基于动态代理实现,默认如果使用接口,使用jdk动态代理。如果没有实现接口,则使用CGLIB实现 用法可参考
- Spring 3.2 以后,spring-core 直接就把 CGLIB 和 ASM 的源码包括进来了,这也是为什么我们不需要显式引入这两个依赖
- Spring AOP 需要依赖于 IOC 容器来管理。
- Spring AOP 只能基于容器中的Bean实现代理增强
- Spring 提供了 AspectJ 的支持 注:这里的支持只是规范的支持 底层还是spring
Aspectj
- AspectJ来自于Eclipse基金会
- 属于静态织入 注入时机可以是:
-
-
- Compile-time weaving:编译期织入,如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。
- Post-compile weaving:也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。
- Load-time weaving:指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法。1、自定义类加载器来干这个,这个应该是最容易想到的办法,在被织入类加载到 JVM 前去对它进行加载,这样就可以在加载的时候定义行为了。2、在 JVM 启动的时候指定 AspectJ 提供的 agent:
-javaagent:xxx/xxx/aspectjweaver.jar
。
-
3.AspectJ 能干很多 Spring AOP 干不了的事情,它是 AOP 编程的完全解决方案。
CGLib AOP
Spring AOP中AOP代理的另一种实现,使用CGLib动态生成AOP代理类,需要代理类为目标类的子类
AOP术语
Aspect
一个切面,可以理解为一个切面模块,将相关的增强内容写进同一个切面。例如:一个负责日志的切面。
Join Point
代表可以由AOP增强织入的程序执行节点。
Advice
所要做的增强处理
Pointcut
切入点,定义了将被Advice增强的一个或多个Join Point,可以使用正则表达式或模式匹配。
Target object
被增强的目标对象
Adivce的种类
Before
方法执行之前
After
方法执行之后
After-returning
方法成功执行完成之后
After-throwing
方法抛出异常之后
Around
环绕方法执行的整个周期
AOP 的核心模型
PointCut
即在哪个地方进行切入,它可以指定某一个点,也可以指定多个点。
比如类A的methord函数,当然一般的AOP与语言(AOL)会采用多用方式来定义PointCut,比如说利用正则表达式,可以同时指定多个类的多个函数。
Advice
在切入点干什么,指定在PointCut地方做什么事情(增强),打日志、执行缓存、处理异常等等。
Advisor/Aspect
PointCut + Advice 形成了切面Aspect,这个概念本身即代表切面的所有元素。但到这一地步并不是完整的,因为还不知道如何将切面植入到代码中,解决此问题的技术就是PROXY。
Proxy
Proxy 即代理,其不能算做AOP的家庭成员,更相当于一个管理部门,它管理 了AOP的如何融入OOP。之所以将其放在这里,是因为Aspect虽然是面向切面核心思想的重要组成部分,但其思想的践行者却是Proxy,也是实现AOP的难点与核心据在。
注:比如我们想实现一个aop封装,需要知道什么,哪些方法需要被代理(PointCut),代理执行的逻辑是啥(Advice),被代理对象,(Prox) 这三个的组成封装就是Advisor(切面)
这样在代理方法就可以遍历Advisor,通过pointCut判断是否需要执行增强逻辑,在啥时候执行。
spring也是同理,无论是注解配置还是xml配置,都是在构建Advisor对象
Spring AOP
Spring AOP沿用了AspectJ的相关概念,包括使用了 AspectJ 提供的 jar 包中的注解,但是不依赖于其实现功能。
如 @Aspect、@Pointcut、@Before、@After 等注解都是来自于 AspectJ,但是功能的实现是纯 Spring AOP 自己实现的。
目前 Spring AOP 一共有三种配置方式,Spring 做到了很好地向下兼容
- Spring 1.2 基于接口的配置:最早的 Spring AOP 是完全基于几个接口的
- Spring 2.0 schema-based 配置:Spring 2.0 以后使用 XML 的方式来配置,使用 命名空间
<aop />
- Spring 2.0 @AspectJ 配置:使用注解的方式来配置,这种方式感觉是最方便的,还有,这里虽然叫做
@AspectJ
,但是这个和 AspectJ 其实没啥关系。
Spring 1.2 中的配置
例子1
1.新增2个Advice
/** * @Project spring * @Description 记录方法入参 */ public class LogArgAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println(String.format("方法名字:%s,入参:%s",method.getName(), Arrays.toString(args))); } }
/** * @Description 方法返回拦截 */ public class ReturnAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println(String.format("方法:%s,返回值:%s",method.getName(),returnValue)); } }
2.配置被代理类
public interface StudentService { public Integer del(Long id); public boolean delAll(); }
public class StudentServiceImpl implements StudentService { @Override public Integer del(Long id) { System.out.println(String.format("执行删除id为:%s的数据",id)); return 1; } @Override public boolean delAll() { System.out.println("删除所有数据"); return true; } }
3.xml配置
核心是通能够给ProxyFactoryBean来创建对象 它实现了Spring的FactoryBean
<!--代理类--> <bean name="studentServiceImpl" class="org.springframework.lq.service.StudentServiceImpl"></bean> <!--定义2个advice--> <bean name="logArgAdvice" class="org.springframework.lq.aspect.LogArgAdvice"></bean> <bean name="returnAdvice" class="org.springframework.lq.aspect.ReturnAdvice"></bean> <!--配置实现studentServiceImpl的代理--> <bean name="studentServiceImplProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--代理接口--> <property name="proxyInterfaces"> <list> <value>org.springframework.lq.service.StudentService</value> </list> </property> <!--代理对象--> <property name="target" ref="studentServiceImpl" ></property> <!--配置拦截器 这里可以配置advice advisor interceptor--> <property name="interceptorNames"> <list> <value>logArgAdvice</value> <value>returnAdvice</value> </list> </property> </bean>
4.测试
@Test public void lqTEST() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( new String[] {LQCONTEXT}, getClass()); StudentService studentService=ctx.getBean("studentServiceImplProxy",StudentService.class); studentService.del(1L); studentService.delAll(); }
输出:
例子2
例子1有个缺点就是,粒度太大,所有方法都实现了拦截.如果只需要对delAll进行增强呢 使用Advisor
<!-- 自己书写的日志切面 --> <bean id="logArgAdvice" class="org.springframework.lq.aspect.LogArgAdvice" /> <!-- 使用JDK的正则切点~~~~~~ --> <bean id="regexPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"> <property name="patterns"> <list> <value>delAll</value><!-- 拦截所有方法名以delALL的方法 --> </list> </property> </bean> <!-- 切面+切点 组合成一个增强器即可~~~~~~ --> <aop:config> <aop:advisor advice-ref="logArgAdvice" pointcut-ref="regexPointcut"/> </aop:config>
其实Spring为我们提供了一个简便的Advisor定义,可以方便的让我们同时指定一个JdkRegexpMethodPointcut和其需要对应的Advice,它就是NameMatchMethodPointcutAdvisor,这样配置起来非常的方便
核心是通能够给ProxyFactoryBean来创建对象 它实现了Spring的FactoryBean
<!--代理类--> <bean name="studentServiceImpl" class="org.springframework.lq.service.StudentServiceImpl"></bean> <!--配置Advisor--> <bean name="logAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> <!--指定advice--> <property name="advice" ref="logArgAdvice"/> <!--需要拦截的方法 可以配置多个,号隔开--> <property name="mappedNames" value="delAll"/> </bean> <!--定义advice--> <bean name="logArgAdvice" class="org.springframework.lq.aspect.LogArgAdvice"></bean> <!--配置实现studentServiceImpl的代理--> <bean name="studentServiceImplProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--代理接口--> <property name="proxyInterfaces"> <list> <value>org.springframework.lq.service.StudentService</value> </list> </property> <!--代理对象--> <property name="target" ref="studentServiceImpl" ></property> <!--配置拦截器 这里可以配置advice advisor interceptor--> <property name="interceptorNames"> <list> <value>logAdvisor</value> </list> </property> </bean>
例子3
advisor和advice最终都是通过Interceptor来完成切入
1.定义一个拦截器
public class OperationInterceptor implements MethodInterceptor { /** * 拦截的方法 */ List<String> refuseMethod= Arrays.asList("delAll"); @Override public Object invoke(MethodInvocation invocation) throws Throwable { if(refuseMethod.contains(invocation.getMethod().getName())){ throw new Exception("无权访问"); } return invocation.proceed(); } }
2.定义xml
核心是通能够给ProxyFactoryBean来创建对象 它实现了Spring的FactoryBean
<!--代理类--> <bean name="studentServiceImpl" class="org.springframework.lq.service.StudentServiceImpl"></bean> <!--配置Interceptor--> <bean name="operationInterceptor" class="org.springframework.lq.aspect.OperationInterceptor"> </bean> <!--配置实现studentServiceImpl的代理--> <bean name="studentServiceImplProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--代理接口--> <property name="proxyInterfaces"> <list> <value>org.springframework.lq.service.StudentService</value> </list> </property> <!--代理对象--> <property name="target" ref="studentServiceImpl" ></property> <!--配置拦截器 这里可以配置advice advisor interceptor--> <property name="interceptorNames"> <list> <value>operationInterceptor</value> </list> </property> </bean>
3.测试
例子4
例子3的问题是,对于每个类型我们都要配置一个代理,如果我们有需求,对ServiceImpl结尾的类生成代理呢 之类我们使用BeanNameAutoProxyCreator
1.xml配置
beanNames支持正则表达式配置
核心是通过实现BeanPostProcessor的子接口SmartInstantiationAwareBeanPostProcessor 实现,通过利用spring创建对象的生命周期回调来实现代理
<!--代理类--> <bean name="studentServiceImpl" class="org.springframework.lq.service.StudentServiceImpl"></bean> <!--配置Interceptor--> <bean name="operationInterceptor" class="org.springframework.lq.aspect.OperationInterceptor"> </bean> <!--配置实现studentServiceImpl的代理--> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="*ServiceImpl"></property> <!--配置拦截器 这里可以配置advice advisor interceptor--> <property name="interceptorNames"> <list> <value>operationInterceptor</value> </list> </property> </bean>
2.测试
红色部分可以发现我们可以直接使用基础类型了
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( new String[] {LQCONTEXT}, getClass()); StudentService studentService=ctx.getBean(StudentService.class); studentService.del(1L); studentService.delAll();
例子5
如果我们需要让del开头的方法进行方法控制呢 使用RegexpMethodPointcutAdvisor
1.xml配置
<!--代理类--> <bean name="studentServiceImpl" class="org.springframework.lq.service.StudentServiceImpl"></bean> <!--配置Advisor--> <bean name="logAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <!--指定advice--> <property name="advice" ref="logArgAdvice"/> <!--需要拦截的方法 可以配置多个,号隔开 全名称配置 支持正则多个,号隔开--> <property name="pattern" value="org.springframework.lq.service.StudentService.del*"/> </bean> <!--定义advice--> <bean name="logArgAdvice" class="org.springframework.lq.aspect.LogArgAdvice"></bean> <!--配置实现studentServiceImpl的代理--> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="*ServiceImpl"></property> <!--配置拦截器 这里可以配置advice advisor interceptor--> <property name="interceptorNames"> <list> <value>logAdvisor</value> </list> </property> </bean>
例子6
是否觉得配置BeanNameAutoProxyCreator 很麻烦呢 可以使用DefaultAdvisorAutoProxyCreator
核心是通过实现BeanPostProcessor的子接口SmartInstantiationAwareBeanPostProcessor 实现,通过利用spring创建对象的生命周期回调来实现代理
<!--代理类--> <bean name="studentServiceImpl" class="org.springframework.lq.service.StudentServiceImpl"></bean> <!--配置Advisor--> <bean name="logAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <!--指定advice--> <property name="advice" ref="logArgAdvice"/> <!--需要拦截的方法 可以配置多个,号隔开 全名称配置 支持正则多个,号隔开--> <property name="pattern" value="org.springframework.lq.service.StudentService.del*"/> </bean> <!--定义advice--> <bean name="logArgAdvice" class="org.springframework.lq.aspect.LogArgAdvice"></bean> <!--配置自动代理--> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>
Spring 2.0 @AspectJ配置
1.引入包
注:虽然引入了Aspectj包,但是还是使用的是Spring API @Aspectj跟 Aspectj没有任何关系,只是spring AOP是根据Aspectj规则实现,用到了他的注解
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.11</version> </dependency>
如果是使用 Spring Boot 的话,添加以下依赖即可:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.xml配置
<aop:aspectj-autoproxy/>
也可以通过注解开启
@Configuration @EnableAspectJAutoProxy public class AppConfig { }
一旦开启了上面的配置,那么所有使用 @Aspect 注解的 bean 都会被 Spring 当做用来实现 AOP 的配置类,我们称之为一个 Aspect。
注意了,@Aspect 注解要作用在 bean 上面,不管是使用 @Component 等注解方式,还是在 xml 中配置 bean,首先它需要是一个 bean。
如:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"> <!-- configure properties of aspect here as normal --> </bean>
@Aspect public class NotVeryUsefulAspect { }
3.配置Pointcut 一般翻译为切点
@Pointcut("execution(* transfer(..))")// the pointcut expression private void anyOldTransfer() {}// the pointcut signature
@Pointcut 中使用了 execution 来正则匹配方法签名,这也是最常用的,除了 execution,我们再看看其他的几个比较常用的匹配方式:
-
within:指定所在类或所在包下面的方法(Spring AOP 独有)
如 @Pointcut("within(com.javadoop.springaoplearning.service..*)")
-
@annotation:方法上具有特定的注解,如 @Subscribe 用于订阅特定的事件。
如 @Pointcut("execution(.*(..)) && @annotation(com.javadoop.annotation.Subscribe)")
-
bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 独有)
如 @Pointcut("bean(*Service)")
上面匹配中,通常 "." 代表一个包名,".." 代表包及其子包,方法参数任意匹配使用两个点 ".."。
对于 web 开发者,Spring 有个很好的建议,就是定义一个 SystemArchitecture:
@Aspect public class SystemArchitecture { // web 层 @Pointcut("within(com.javadoop.web..*)") public void inWebLayer() {} // service 层 @Pointcut("within(com.javadoop.service..*)") public void inServiceLayer() {} // dao 层 @Pointcut("within(com.javadoop.dao..*)") public void inDataAccessLayer() {} // service 实现,注意这里指的是方法实现,其实通常也可以使用 bean(*ServiceImpl) @Pointcut("execution(* com.javadoop..service.*.*(..))") public void businessService() {} // dao 实现 @Pointcut("execution(* com.javadoop.dao.*.*(..))") public void dataAccessOperation() {} }
上面这个 SystemArchitecture 很好理解,该 Aspect 定义了一堆的 Pointcut,随后在任何需要 Pointcut 的地方都可以直接引用(如 xml 中的 pointcut-ref="")。
配置 pointcut 就是配置我们需要拦截哪些方法。
配置
Advice配置
@Aspect public class AdviceExample { // 这里会用到我们前面说的 SystemArchitecture // 下面方法就是写拦截 "dao层实现" @Before("com.javadoop.aop.SystemArchitecture.dataAccessOperation()") public void doAccessCheck() { // ... 实现代码 } // 当然,我们也可以直接"内联"Pointcut,直接在这里定义 Pointcut // 把 Advice 和 Pointcut 合在一起了,但是这两个概念我们还是要区分清楚的 @Before("execution(* com.javadoop.dao.*.*(..))") public void doAccessCheck() { // ... 实现代码 } @AfterReturning("com.javadoop.aop.SystemArchitecture.dataAccessOperation()") public void doAccessCheck() { // ... } @AfterReturning( pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // 这样,进来这个方法的处理时候,retVal 就是相应方法的返回值,是不是非常方便 // ... 实现代码 } // 异常返回 @AfterThrowing("com.javadoop.aop.SystemArchitecture.dataAccessOperation()") public void doRecoveryActions() { // ... 实现代码 } @AfterThrowing( pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... 实现代码 } // 注意理解它和 @AfterReturning 之间的区别,这里会拦截正常返回和异常的情况 @After("com.javadoop.aop.SystemArchitecture.dataAccessOperation()") public void doReleaseLock() { // 通常就像 finally 块一样使用,用来释放资源。 // 无论正常返回还是异常退出,都会被拦截到 } // 感觉这个很有用吧,既能做 @Before 的事情,也可以做 @AfterReturning 的事情 @Around("com.javadoop.aop.SystemArchitecture.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } } @Before("com.javadoop.springaoplearning.aop_spring_2_aspectj.SystemArchitecture.businessService()") public void logArgs(JoinPoint joinPoint) { System.out.println("方法执行前,打印入参:" + Arrays.toString(joinPoint.getArgs())); }
Spring 2.0 schema-based配置
解析<AOP/> 相关的xml在org.springframework.aop.config.AopNamespaceHandle 中
<aop:config>
<aop:aspect ref="logArgsAspect">
<aop:pointcut id="internalPointcut"
expression="com.javadoop.SystemArchitecture.businessService()" />
</aop:aspect>
</aop:config>