AOP,即Aspect Oriented Programming,面向切片的编程,是通过动态代理技术,在功能程序在不修改功能代码情况下,实现功能的扩展的能力。
和代理功能一样,其实看内在实现,AOP就是dynamic proxy的封装,AOP内部通过Java或者cglib实现代理的生产,通过继承InvocationHandler()接口或者MethodInterceptor()接口,并在实现接口的方法中调用Method.invoke()实现目标方法的调用,并在目标方法前和后添加扩展功能。
AOP是OOP的功能补充。OOP可以看作是纵向的功能实现,关注主要的业务逻辑,AOP是横向的功能补充,比如处理业务逻辑间的日志记录、参数校验等,所以AOP的操作,也是横切操作,在横向补充业务点(根据切点确定)的功能。
一、了解AOP的相关概念
1、Aspect(切面)
一个关注点的模块化,这个关注点可能会横切多个对象。在Spring AOP中,切面可在xml配置文件中通过<aop:config>的<aop:aspect>配置切面或者基于@Aspect注解。
2、JoinPoint(连接点)
在程序执行过程中某个特定的点,比如某方法调用之前、之后或者处理异常的时候。
3、Advice(通知/增强)
在特定的连接点,AOP框架执行的的动作。类型包括:
- 前置通知(before advice):目标方法执行之前执行
- 返回后通知(after returning advice): 方法正常执行结束后的通知,可以访问到方法的返回值
- 抛出异常后的通知(after throwing advice): 方法抛出异常时执行
- 后置通知(after(finally) advice): 在方法执行后执行,无论方法是否发生异常。
- 环绕通知(around advice): 可以实现上述 所有功能。
4、Pointcut(切入点)
指定一个通知将被引发的一系列连接点的集合,使用正则表达式表示一系列接口。Spring 定义的Poingcut包含两个部分,即MethodMatcher和ClassFilter,可以通过参数名字很清楚的理解,MethodMatcher是用来检查目标类的方法是否可以被应用此通知,ClassFilter过滤Pointcut应用的目标类。
5、Introduction(引入)
添加方法或字段到被通知的类。
6、Target Object(目标对象)
包含连接点的对象。也称作被通知或者被代理的对象。
7、AOP Proxy(AOP代理)
AOP框架创建的对象,包含通知。AOP通过jdk或者cglib动态生产目标对象的代理。具体选择jdk还是cglib来生成代理,需要根据目标对象来确定。如果目标对象继承接口,则使用jdk,如果没有,则使用cglib。
8、Weaving(织入)
组装切面创建一个被通知对象。
二、切点表达式
2.1 Spring AOP支持的AspectJ切入点指示符
execution:用于匹配符合的方法;
within:用于匹配指定的类及其子类中的所有方法。
this:匹配可以向上转型为this指定的类型的代理对象中的所有方法。
target:匹配可以向上转型为target指定的类型的目标对象中的所有方法。
args:用于匹配运行时传入的参数列表的类型为指定的参数列表类型的方法;
@annotation:用于匹配持有指定注解的方法;
@within:用于匹配持有指定注解的类的所有方法;
@target:用于匹配持有指定注解的目标对象的所有方法;
@args:用于匹配运行时 传入的参数列表的类型持有 注解列表对应的注解 的方法;
2.2 切入点使用示例
1、execution: 使用"execution(方法表达式)"匹配方法执行
模式 | 描述 |
public * *(..) | 任何公共方法的执行 |
* cn.javass..IPointcutService.*() | cn.javass包及所有子包下IPointcutService接口中的任何无参方法 |
* cn.javass..*.*(..) | cn.javass包及所有子包下任何类的任何方法 |
* cn.javass..IPointcutService.*(*) | cn.javass包及所有子包下IPointcutService接口的任何只有一个参数方法 |
* (!cn.javass..IPointcutService+).*(..) | 非“cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法 |
* cn.javass..IPointcutService+.*() | cn.javass包及所有子包下IPointcutService接口及子类型的的任何无参方法 |
* cn.javass..IPointcut*.test*(java.util.Date) | cn.javass包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的,,args表示则与之相反。 如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的; |
* cn.javass..IPointcut*.test*(..) throws IllegalArgumentException, ArrayIndexOutOfBoundsException | cn.javass包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException异常 |
* (cn.javass..IPointcutService+ && java.io.Serializable+).*(..) | 任何实现了cn.javass包及所有子包下IPointcutService接口和java.io.Serializable接口的类型的任何方法 |
@java.lang.Deprecated * *(..) | 任何持有@java.lang.Deprecated注解的方法 |
@java.lang.Deprecated @cn.javass..Secure * *(..) | 任何持有@java.lang.Deprecated和@cn.javass..Secure注解的方法 |
@(java.lang.Deprecated || cn.javass..Secure) * *(..) | 任何持有@java.lang.Deprecated或@ cn.javass..Secure注解的方法 |
(@cn.javass..Secure *) *(..) | 任何返回值类型持有@cn.javass..Secure的方法 |
* (@cn.javass..Secure *).*(..) | 任何定义方法的类型持有@cn.javass..Secure的方法 |
* *(@cn.javass..Secure (*) , @cn.javass..Secure (*)) | 任何签名带有两个参数的方法,且这个两个参数都被@ Secure标记了, 如public void test(@Secure String str1, @Secure String str1); |
* *((@ cn.javass..Secure *))或 * *(@ cn.javass..Secure *) | 任何带有一个参数的方法,且该参数类型持有@ cn.javass..Secure; 如public void test(Model model);且Model类上持有@Secure注解 |
* *( @cn.javass..Secure (@cn.javass..Secure *) , @ cn.javass..Secure (@cn.javass..Secure *)) | 任何带有两个参数的方法,且这两个参数都被@ cn.javass..Secure标记了;且这两个参数的类型上都持有@ cn.javass..Secure; |
* *( java.util.Map<cn.javass..Model, cn.javass..Model> , ..) | 任何带有一个java.util.Map参数的方法,且该参数类型是以< cn.javass..Model, cn.javass..Model >为泛型参数;注意只匹配第一个参数为java.util.Map,不包括子类型; 如public void test(HashMap<Model, Model> map, String str);将不匹配,必须使用“* *( java.util.HashMap<cn.javass..Model,cn.javass..Model> , ..)”进行匹配; 而public void test(Map map, int i);也将不匹配,因为泛型参数不匹配 |
* *(java.util.Collection<@cn.javass..Secure *>) | 任何带有一个参数(类型为java.util.Collection)的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass..Secure注解; 如public void test(Collection<Model> collection);Model类型上持有@cn.javass..Secure |
* *(java.util.Set<? extends HashMap>) | 任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型继承与HashMap; Spring AOP目前测试不能正常工作 |
* *(java.util.List<? super HashMap>) | 任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型是HashMap的基类型;如public voi test(Map map); Spring AOP目前测试不能正常工作 |
* *(*<@cn.javass..Secure *>) | 任何带有一个参数的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass..Secure注解; Spring AOP目前测试不能正常工作 |
2、within:使用“within(类型表达式)”用于匹配指定的类的任何方法;
注意:within只能指定类,然后该类内的所有方法都将被匹配
模式 | 描述 |
within(cn.javass..*) | cn.javass包及子包下的任何类的任何方法执行 |
within(cn.javass..IPointcutService+) | cn.javass包或所有子包下IPointcutService类型及子类型的任何方法 |
within(@cn.javass..Secure *) | 持有cn.javass..Secure注解的任何类型的任何方法 必须是在目标对象上声明这个注解,在接口上声明的对它不起作用 |
3、this:使用“this(type)。
this表示匹配type指定的代理对象的所有方法。this的Pointcut表达式的语法是this(type),当生成的代理对象可以转换为type指定的类型时则表示匹配,也就是说type类型只能是代理类的父类或者父接口(即目标类或者目标类的父类、父接口),
还有,基于JDK接口的代理和基于CGLIB的代理生成的代理对象是不一样的。
基于CGLIB生成的代理对象,type类型可以为目标类及目标类的父类(or父接口)..因为cglib代理所生成的代理类是目标类的子类,所以 代理对象是可以转换为目标类及目标类的父类(父接口)的。
基于jdk的代理,type为目标类的父接口时,可以匹配到该目标类的代理对象。原因:
如果type是一个类类型,那么,此时该切入点表达式无法匹配到任何代理对象,因为在jdk代理中,由于目标类与代理类是兄弟关系,代理对象是不可能转换为目标类型的,............正确的是 type只能是接口,并且 type必须是目标类所实现的接口(任意一个),这样的话,代理对象才可以转换为这个接口类型,此时才能匹配成功,也就是说,type为目标类的父接口时,可以匹配到该目标类的代理对象。
注意:(jdk代理这一段话未经测试,仅仅只是我的推导)。
综上所述,type最好就是代理类所实现的接口。。。这样不论JDK还是CGLIB都一样。
模式 | 描述 |
this(cn.javass.spring.chapter6.service.IPointcutService) | 当前AOP对象实现了 IPointcutService接口的任何方法 |
this(cn.javass.spring.chapter6.service.IIntroductionService) | 当前AOP对象实现了 IIntroductionService接口的任何方法 也可能是引入接口 |
4、target:使用“target(type)
type指的是一个类或者接口的完整包路径
功能:匹配type类型的目标对象的所有方法。即目标对象可以向上转型为type类型就算是匹配成功
模式 | 描述 |
target(cn.javass.spring.chapter6.service.IPointcutService) | 当前目标对象(非AOP代理对象)实现了 IPointcutService接口的任何方法 |
target(cn.javass.spring.chapter6.service.IIntroductionService) | 当前目标对象(非AOP代理对象) 实现了IIntroductionService 接口的任何方法 不可能是引入接口 |
5、args:使用“args(参数类型列表)”
匹配当前执行的方法传入时的参数类型为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
args(java.util.Date)
如定义方法:public void test(Object obj);执行时传入java.util.Date,则匹配,传入的如果不是java.util.Date,则不匹配
args匹配的是传入时的的参数类型是否是指定类型
模式 | 描述 |
args (java.io.Serializable,..) | 任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数的方法执行,args指定的参数类型是在运行时动态匹配的 |
6、@within:使用“@within(注解类型)”
匹配所以持有指定注解类型内的方法;注解类型也必须是全限定类型名;
模式 | 描述 |
@within (cn.javass.spring.chapter6.Secure) | 任何目标对象对应的类型持有Secure注解的类方法; 必须是在目标对象上声明这个注解,在接口上声明的对它不起作用 |
7、@target:使用“@target(注解类型)”
匹配持有指定注解的类型的目标对象;注解类型也必须是全限定类型名;
模式 | 描述 |
@target (cn.javass.spring.chapter6.Secure) | 任何目标对象持有Secure注解的类方法; 必须是在目标对象上声明这个注解,在接口上声明的对它不起作用 |
8、@args:使用“@args(注解列表)”
匹配运行时传入的参数的类型持有指定注解的方法,并且args括号内可以指定多个arg;注解类型也必须是全限定类型名;
模式 | 描述 |
@args (cn.javass.spring.chapter6.Secure) | 任何一个只接受一个参数的方法,且方法运行时传入的参数类型持有cn.javass.spring.chapter6.Secure注解 |
9、@annotation:使用“@annotation(注解类型)”
匹配持有指定注解的方法;注解类型也必须是全限定类型名;
模式 | 描述 |
@annotation(cn.javass.spring.chapter6.Secure ) | 方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配 |
10、bean:使用“bean(Bean id或名字通配符)”
匹配特定名称的Bean对象的执行方法;Spring ASP扩展的,在AspectJ中无相应概念;
模式 | 描述 |
bean(*Service) | 匹配所有以Service命名(id或name)结尾的Bean |
三、使用样例
3.1 基于xml配置方式
beans.xml配置aop如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="cn.com.demo.service.UserService"/>
<bean id="logAopDemo" class="cn.com.demo.aop.LogAop"/>
<!-- 面向切面编程 -->
<aop:config>
<aop:aspect ref="logAopDemo">
<!-- 定义切点 -->
<aop:pointcut expression="execution(* *..*(..))" id="pointCut"/>
<!-- 声明前置通知 (在切点方法被执行前调用)-->
<aop:before method="beforeAdvice" pointcut-ref="pointCut"/>
<!-- 声明后置通知 (在切点方法被执行后调用)-->
<aop:after method="afterAdvice" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
</beans>
日志类LogAop.jar
package cn.com.demo.aop;
public class LogAop {
public void beforeAdvice() {
System.out.println("beforeAdvice");
}
public void afterAdvice() {
System.out.println("afterAdvice");
}
}
目标类UserService.java
package cn.com.demo.service;
public class UserService {
public void insertUser() {
System.out.println("插入用户成功");
}
public boolean updateUser() {
System.out.println("更新用户成功");
return true;
}
}
测试一下
package cn.com.demo.test;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import cn.com.demo.service.UserService;
public class TestAop {
private ClassPathXmlApplicationContext ac;
@Before
public void before() {
ac = new ClassPathXmlApplicationContext("*/applicationContext.xml");
}
@Test
public void test() {
try {
UserService userService = (UserService) ac.getBean("userService");
userService.insertUser();
userService.updateUser();
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果:
beforeAdvice
插入用户成功
afterAdvice
beforeAdvice
更新用户成功
afterAdvice
3.2 基于注解配置方式
首先是beans.xml的配置,开启AOP注解方式。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="cn.com.demo.service.UserService"/>
<!-- 启用注释驱动自动注入 -->
<!-- <context:annotation-config/> -->
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="cn.com.demo"></context:component-scan>
<!-- 开启aop注解方式,此步骤不能少,这样java类中的aop注解才会生效 -->
<aop:aspectj-autoproxy/>
</beans>
注:<aop:aspectj-autoproxy/>为开启aop注解方式,必须。
日志类LogAop
package com.cc.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LogAop {
@Pointcut("execution(* cn.com.demo.service.UserService.insertUser(..))")
public void ponitCut() {
}
@Before("ponitCut()")
public void beforeAdvice() {
System.out.println("beforeAdvice");
}
@After("ponitCut()")
public void afterAdvice() {
System.out.println("afterAdvice");
}
//环绕通知。注意要有ProceedingJoinPoint参数传入
@Around("ponitCut()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("注解类型环绕通知..环绕前");
pjp.proceed();//执行方法
System.out.println("注解类型环绕通知..环绕后");
}
}
目标类UserService.java
package com.cc.demo.service;
public class UserService {
public void insertUser() {
System.out.println("插入用户成功");
}
public boolean updateUser() {
System.out.println("更新用户成功");
return true;
}
}
测试下
package com.cc.demo.test;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import cn.com.demo.service.UserService;
public class TestAop {
private ClassPathXmlApplicationContext ac;
@Before
public void before() {
ac = new ClassPathXmlApplicationContext("*/applicationContext.xml");
}
@Test
public void test() {
try {
UserService userService = (UserService) ac.getBean("userService");
userService.insertUser();
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果
注解类型环绕通知..环绕前
beforeAdvice
插入用户成功
注解类型环绕通知..环绕后
afterAdvice