什么是AOP
Aspect Oriented Programming,面向切面/方面编程
即针对一批组件的通用业务逻辑进行编程,将通用的功能与业务模块分离,通用的功能就是可以复用的代码段
AOP中的一些术语
- 切面:封装通用业务逻辑的组件,作用到其他组件上,要求:
- 必须是被Spring管理的bean组件中的方法
- 无返回值(void)
- 参数类型与通知类型一致
- 切入点:指定哪些组件的哪些方法使用切面程序,spring提供表达式来实现指定,要求:
- 必须是spring 管理的bean组件的方法
- 不限定返回值
- 不限定参数类型
- 通知:切面程序插入到切入点程序的某个位置,根据位置的不同,分为:
- 前置通知 : 切面程序在目标方法执行前执行
- 后置通知 : 目标组件(切入点)正常执行并返回参数后执行
- 异常通知 : 目标组件抛出异常后执行
- 最终通知 : 目标组件正常执行后、或者异常通知前 执行,和finally 中的内容相似,最终通知中切面中的代码不管异常否,均会执行
- 环绕通知 : 可以取代之前的集中通知类型,具体见下面代码
切入点表达式
- 按函数匹配 ,形如:execution(* com.lc.test.Boy.aspectTest(..)),注意的几点:
- 第一个 * 表示任意修饰符及任意返回类型,不能省略; 如:写成 execution(public * com.lc.test.Boy.*(..))表示Boy类中用public修饰的方法、任意返回类型、任意参数的方法为切入点
- * 表示匹配一个,如果要两个参数,就要这样写:execution(* com.lc.test.Boy.aspectTest(*,*))
- 两个点 .. 表示匹配任意参数(0个或多个): execution(* com.lc.test.Boy.aspectTest(..))
- com..*.*(..)) : 指定com下的所有类的任意参数的任意方法为切入点
- 按类匹配, 形如:within(com.lc.test.Boy) :指定这个类中所有方法 均为切入点
- 按bean组件的id匹配:bean(boy) :bean的id为boy的类 中所有方法 均为切入点,可用通配符 ,形如:bean(b*)
环境搭建
- spring 核心jar : core
- spring-aop, spring-aspects (spring-aop..., spring-aspects..., aopalliance..., aspectjweaver...,)
Demo部分代码及过程中遇到的问题:
前置通知:
// 切入点程序
public class Boy {
public void aspectTest(String param1, String param2){
System.out.println("测试AOP");
}
}
// 切面程序
// JoinPoint jp 参数并不是必须的,这个参数可以获取目标组件的细节,如类名,方法名等
public class AspectDemo{
public void doBefore(JoinPoint jp){
System.out.println("前置通知");
System.out.println(jp.getTarget().getClass().getName());
}
}
// applicationComtext.xml配置文件
<!-- 将切入点和切面程序声明为bean组件 -->
<bean id="boy" class="com.lc.test.Boy"></bean>
<bean id="beforeAspect" class="com.lc.aspect.AspectDemo"></bean>
<!-- 配置通知 -->
<aop:config>
<!-- 配置切入点 expression="within(com.lc.test.Boy) 或者expression=bean(boy)"" " -->
<aop:pointcut id="targetPointcut" expression="execution(* com.lc.test.Boy.aspectTest(..))" />
<!-- 配置切面,ref:切面对象 -->
<aop:aspect ref="beforeAspect">
<!-- 前置通知 method:切面方法, pointcut-ref:指定切面方法作用的切入点 -->
<aop:before method="doBefore" pointcut-ref="targetPointcut"/>
</aop:aspect>
</aop:config>
后置通知:
// 切入点程序和前置通知一样
// 切面程序
public void doAfterReturning(JoinPoint jp, Object result){
System.out.println("目标组件的返回值:" + result);
System.out.println("后置通知");
}
// 配置
<!-- 将切入点和切面程序声明为bean组件 -->
<bean id="boy" class="com.lc.test.Boy"></bean>
<bean id="beforeAspect" class="com.lc.aspect.AspectDemo"></bean>
<!-- 配置通知 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="targetPointcut" expression="bean(boy)" />
<!-- 配置切面,ref:切面对象 -->
<aop:aspect ref="beforeAspect">
<!--前置通知 method:切面方法, pointcut-ref:指定切面方法作用的切入点 -->
<aop:before method="doBefore" pointcut-ref="targetPointcut"/>
<!-- 后置通知 returnint:切入点程序执行完成的返回值,void为null -->
<aop:after-returning method="doAfterReturning" pointcut-ref="targetPointcut" returning="result"/>
</aop:aspect>
</aop:config>
异常通知:
// 切入点程序
public void aspectTest(){
System.out.println("测试AOP");
// 创造一个抛出异常的点
System.out.println(3/0);
}
// 切面程序
public void doThrowable(JoinPoint jp, Throwable t){
System.out.println("异常信息:"+ t.getMessage());
System.out.println("异常通知");
}
//配置
<!-- 将切入点和切面程序声明为bean组件 -->
<bean id="boy" class="com.lc.test.Boy"></bean>
<bean id="beforeAspect" class="com.lc.aspect.AspectDemo"></bean>
<!-- 配置通知 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="targetPointcut" expression="bean(boy)" />
<!-- 配置切面,ref:切面对象 -->
<aop:aspect ref="beforeAspect">
<!--前置通知 method:切面方法, pointcut-ref:指定切面方法作用的切入点 -->
<aop:before method="doBefore" pointcut-ref="targetPointcut"/>
<!-- 后置通知 returnint:切入点程序执行完成的返回值,void为null -->
<aop:after-returning method="doAfterReturning" pointcut-ref="targetPointcut" returning="result"/>
<!-- 异常通知 throwing:切入点程序抛出的异常信息 -->
<aop:after-throwing method="doThrowable" pointcut-ref="targetPointcut" throwing="t" />
</aop:aspect>
</aop:config>
最终通知(暂时没搞清和后置通知有啥区别,但是有一点,后置通知可以获取到目标组件的返回值):
// 切面程序
public void doAfter(JoinPoint jp){
System.out.println("最终通知");
}
// 配置
<!-- 将切入点和切面程序声明为bean组件 -->
<bean id="boy" class="com.lc.test.Boy"></bean>
<bean id="beforeAspect" class="com.lc.aspect.AspectDemo"></bean>
<!-- 配置通知 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="targetPointcut" expression="bean(boy)" />
<!-- 配置切面,ref:切面对象 -->
<aop:aspect ref="beforeAspect">
<!--前置通知 method:切面方法, pointcut-ref:指定切面方法作用的切入点 -->
<aop:before method="doBefore" pointcut-ref="targetPointcut"/>
<!-- 后置通知 returnint:切入点程序执行完成的返回值,void为null -->
<aop:after-returning method="doAfterReturning" pointcut-ref="targetPointcut" returning="result"/>
<!-- 异常通知 throwing:切入点程序抛出的异常信息 -->
<aop:after-throwing method="doThrowable" pointcut-ref="targetPointcut" throwing="t" />
<!-- 最终通知 -->
<aop:after method="doAfter" pointcut-ref="targetPointcut"/>
</aop:aspect>
</aop:config>
环绕通知:参数为org.aspectj.lang.ProceedingJoinPoin(pjp), 是JoinPoint的子类,提供proceed()方法;
执行过程:proceed方法调用目标组件(切入点)方法,并返回其返回值,Object result = pjp.proceed();
取代前几种通知类型的方式:
proceed方法之前的代码 | 前置通知 |
proceed方法之后的代码 | 后置通知 |
异常中的代码 | 异常通知 |
finally中的代码 | 最终通知 |
// 切面程序
public void doArround(ProceedingJoinPoint pjp){
try {
System.out.println("前置通知");
Object result = pjp.proceed();
System.out.println("后置通知:" + result );
} catch (Throwable t) {
System.out.println("异常通知:" + t.getMessage());
}finally{
System.out.println("最终通知");
}
}
// 切入点程序
public String aspectTest(){
System.out.println("测试AOP");
//System.out.println(3/0);
return "success";
}
// 配置
<!-- 将切入点和切面程序声明为bean组件 -->
<bean id="boy" class="com.lc.test.Boy"></bean>
<bean id="beforeAspect" class="com.lc.aspect.AspectDemo"></bean>
<!-- 配置通知 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="targetPointcut" expression="bean(boy)" />
<!-- 配置切面,ref:切面对象 -->
<aop:aspect ref="beforeAspect">
<!-- 环绕通知 -->
<aop:around method="doArround" pointcut-ref="targetPointcut"/>
</aop:aspect>
</aop:config>
用配置文件配置,配置比较繁琐,下面用注解配置一下:
前提条件:
- 引入了aspectj包,因为要使用包中的@Aspect注解标注组件为切面(这个Demo中是com.lc.aspect.AspectDemo)
- 开启自动扫描 :<context:component-scan base-package="com.lc"></context:component-scan>
- 开启aspectj的自动代理模式 <aop:aspectj-autoproxy><aop:aspectj-autoproxy/>
前置通知:
// 切入点程序
public String aspectTest(){
System.out.println("测试AOP");
//System.out.println(3/0);
return "success";
}
// 切面程序
@Aspect
public class AspectDemo {
// 注意这里是execution,不是配置文件中的expression
// 另外:bean(boy) , within(com.lc.test.Boy) 也可以
@Before(value="execution(* com.lc.test.Boy.aspectTest())")
public void doBefore(JoinPoint jp){
System.out.println("前置通知");
System.out.println(jp.getTarget().getClass().getName());
}
}
// 因为这个没有注解类为bean组件,在配置文件里面配置
<!-- 开启注解扫描 -->
<context:component-scan base-package="com.lc"></context:component-scan>
<!--如果要使用@Aspect注解, 得开启aspectj的自动代理模式 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- 将切入点和切面程序声明为bean组件 -->
<bean id="boy" class="com.lc.test.Boy"></bean>
<bean id="beforeAspect" class="com.lc.aspect.AspectDemo"></bean>
后置通知、异常通知、最终通知、环绕通知:
@Aspect
public class AspectDemo {
@Before(value="execution(* com.lc.test.Boy.*())")
// 注意这里是execution,不是配置文件中的expression
// 另外:bean(boy) , within(com.lc.test.Boy) 也可以
public void doBefore(JoinPoint jp){
System.out.println("前置通知");
System.out.println(jp.getTarget().getClass().getName());
}
@AfterReturning(pointcut="execution(* com.lc.test.Boy.*())", returning="result")
public void doAfterReturning(Object result){
System.out.println("目标组件的返回值:" + result);
System.out.println("后置通知");
}
@AfterThrowing(pointcut="execution(* com.lc.test.Boy.*())",throwing="t")
public void doThrowable(JoinPoint jp, Throwable t){
System.out.println("异常信息:"+ t.getMessage());
System.out.println("异常通知");
}
@After(value="execution(* com.lc.test.Boy.*())")
public void doAfter(JoinPoint jp){
System.out.println("最终通知");
}
@Around(value="execution(* com.lc.test.Boy.*())")
public void doArround(ProceedingJoinPoint pjp){
try {
System.out.println("前置通知");
Object result = pjp.proceed();
System.out.println("后置通知:" + result );
} catch (Throwable t) {
System.out.println("异常通知:" + t.getMessage());
}finally{
System.out.println("最终通知");
}
}
}
像上面的注解配置中,对于切入点的配置,其实不管是
@AfterReturning(pointcut="execution(* com.lc.test.Boy.*())", returning="result") 中的 pointcut 取值, 还是其他通知,比如:
@After(value="execution(* com.lc.test.Boy.*())") 中的 value 取值
它们都是同一个取值,此时,为了简化配置,我们可以这样写(定义一个公共的切入点):
@Aspect
public class AspectDemo {
// 定义切入点
@Pointcut(value="execution(* com.lc.test.Boy.aspectTest())")
public void pointcutName(){}
@Before("pointcutName()")
// 注意这里是execution,不是配置文件中的expression
// 另外:bean(boy) , within(com.lc.test.Boy) 也可以
public void doBefore(JoinPoint jp){
System.out.println("前置通知");
System.out.println(jp.getTarget().getClass().getName());
}
@AfterReturning(pointcut="pointcutName()", returning="result")
public void doAfterReturning(Object result){
System.out.println("目标组件的返回值:" + result);
System.out.println("后置通知");
}
@AfterThrowing(pointcut="pointcutName()",throwing="t")
public void doThrowable(JoinPoint jp, Throwable t){
System.out.println("异常信息:"+ t.getMessage());
System.out.println("异常通知");
}
@After(value="pointcutName()")
public void doAfter(JoinPoint jp){
System.out.println("最终通知");
}
@Around(value="pointcutName()")
public void doArround(ProceedingJoinPoint pjp){
try {
System.out.println("前置通知");
Object result = pjp.proceed();
System.out.println("后置通知:" + result );
} catch (Throwable t) {
System.out.println("异常通知:" + t.getMessage());
}finally{
System.out.println("最终通知");
}
}
}
总结:aop可以通过通知来实现对一些通用业务逻辑进行编程(代码复用): 日志打印、事务控制、权限控制......