目录
一、AOP基本介绍
AOP(Aspect Orented Programming,面向切面编程)是在OOP(Object Orented Programming,面向对象编程)之上延伸出的一种编程思想。OOP通过 封装、继承和多态的概念构建类的层级结构,完美地解决纵向共用性,提高了代码地重用性和可维护性。但OOP编程存在着两个不足:对于没有继承关系类的横向共用性无法处理,也无法动态增加类的功能。AOP是在不修改源码的前提下添加功能的一种编程思想,其目的是降低系统业务逻辑之间的耦合性,提高代码的重用性和开发的效率性。AOP和OOP并不冲突,AOP是OOP的补充,两者的角度不同,OOP是纵向结构的类层级,AOP是横向结构的方法层级,两者的关系如下图:
1.应用场景
AOP可以扩展横向共用性,会使得逻辑代码变得干净整洁,它适合的应用场景有很多,常用的如下:
- 日志记录:不需要在业务代码中混杂日志代码,可以在项目后期织入日志功能。
- 安全控制:对系统的访问进行登录验证,对请求进行权限验证。
- 参数检验:对请求的参数进行统一转换或验证。
- 事务处理:统一处理数据库事务的提交和回滚,比如在方法执行完成后自动提交事务,出现异常时自动回滚。
- 统一异常处理:不需要在每个方法中使用try catch捕捉异常,使用AOP集中处理。
- 缓存:使用AOP实现缓存存取和清除。
- 统一发信、通知,对某些方法执行或者异常拦截发送通知。
2.基本术语解释
- Aspect:切面,由切点和增强组成,如在Java代码中对应一个切面类。
- JointPoint:连接点。程序执行的某个位置,如函数调用、函数执行、构造函数调用、获取或设置变量、类初始化等。
- Pointcut:切入点,也就是特定的连接点。一个程序有多个连接点,但并不是所有连接点都是需要关心的,找到合适的连接点进行切入。切入点就是选取的合适连接点的描述。
- Advice:增强。织入到目标连接点的代码,也就是额外功能的代码。
- Weaving:织入。将增强添加到目标连接点的过程。
- Before(前置)、After(后置)和Around(环绕):织入方位可前、可后也可以是两者的合集。
二、Java AOP框架---AspectJ
JDK的反射与代理特性及第三方动态代理框架提供了AOP实现的基础,但是与实际的AOP开发需要的功能还相差甚远。在Java代理技术的基础上,出现了一些AOP框架,包括Aspect、Javassist及Spring AOP等。其中,AspectJ是目前使用较为广泛的Java AOP框架,Spring AOP框架与其有一定的关系。使用AspectJ开发需要下载AspectJ运行环境和AspectJ依赖包,在基于Eclipse IDE的开发中,安装AJDT(AspectJ Development Tools)插件可以简化AspectJ AOP开发。
1.Eclipse IDE中安装AJDT的步骤:
- 打开AJDT下载页,找到对应版本的AJDThttps://www.eclipse.org/ajdt/downloads/
- 打开Eclipse,操作如下:
- 接着将第一步中的URL直接粘贴到下图中的输入框,进行如下几步:
完成之后Eclipse会提示你重启,重启之后查看Windows--Preferences是否安装成功,如下出现AspectJ Compiler 即为安装成功:
2.使用Eclipse AJDT 开发AspectJ实例
- 在Eclipse中建立AspectJ类型项目,New--->Other---->AspectJ---->AspectJ Project;
- 定义被代理的类,此处为UserService类
public class UserService { public void doSomthing() { System.out.println("-----执行doSomthing方法-----"); } }
- 创建AspectJ的切面文件UserServiceAspect.aj,内容如下:
public aspect UserServiceAspect { //定义切面 //定义切入点 pointcut UserServicePointCut() : execution(* com.mec..*.doSomthing(..)); //定义前置增强 before(): UserServicePointCut(){ System.out.println("前置增强"); } //定义后置增强 after(): UserServicePointCut(){ System.out.println("后置增强"); } }
使用aspect关键字定义切面,pointcut关键字定义切入点,execution()用于定义切入点表达式,before()和after()定义前置和后置增强。
-
测试UserService的doSomthing()方法:Run As--->AspectJ/Java Application命令运行,输出如下:
3.AspectJ 切入点执行表达式
AspectJ提供了一套切点表达式,该表达式也被Spring所沿用,表达式语法如下图所示:
- 位置1:限定符,代表方法权限修饰符,此处匹配public;
- 位置2:方法返回类型,*号代表所有类型;
- 位置3:拦截的类所在的包名,..表示当前包及其所有子包;
- 位置4:类名,*号代表所有类;
- 位置5:方法名;
- 位置6:该方法的参数。..代表所有参数,不限个数,不限类型。
常见的AspectJ切点表达式示例如下:
- execution(public * *(..)):所有public的方法;(第一个*号为匹配所有返回类型,第二个*号匹配所有方法)。
- execution(* *Somthing(..)):所有add后缀的方法;(第一个*匹配所有返回类型,*Somthing匹配所有以Somthing为后缀的方法)。
- execution(* com..*Service.do*(..)):匹配com包及其子包下所有以Service为后缀的类中的所有以do为前缀的方法。
- execution(* doSomthing(String,*)):匹配有两个参数的doSomthing方法,第一个参数为String类型,第二个参数为任意类型。
4.AspectJ注解开发
除了使用.aj文件进行切面的定义,AspectJ从1.5开始提供了注解的开发方式,使用在Bean类和方法中。注解包括:
- @Apsect:注解切面类;
- @Pointcut:切点及表达式定义;
- @Before:前置增强;
- @After:后置增强;
- @Around:环绕增强;
- @AfterReturning:方法返回结果之后执行;
- @AfterThrowing:方法抛出异常之后执行。
@Aspect
public class UserServiceAspectAnno {
@Pointcut("execution(* com.mec..*.doSomthing(..))")
public void userServicePointCut() {
}
// @Before("userServicePointCut()")
// public void beforeAdvice() throws Throwable {
// System.out.println("[UserServiceAspectAnno][beforeAdvice]前置增强");
// }
//目标方法之后执行,不论执行成功或者失败(抛出异常)都执行
@After("userServicePointCut()")
public void afterAdvice() throws Throwable {
System.out.println("[UserServiceAspectAnno][afterAdvice]后置增强");
}
@Around("userServicePointCut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[UserServiceAspectAnno][aroundAdvice]环绕增强,方法执行前");
// 这里利用joinPoint执行原始方法
Object result = joinPoint.proceed();
System.out.println("[UserServiceAspectAnno][aroundAdvice]环绕增强,方法执行后");
//这里如果不能顺利执行return result(抛出异常),那么也算方法没有返回结果,就不能触发afterRunningAdvice()
return result;
}
@AfterReturning("userServicePointCut()")
public void afterRunningAdvice() throws Throwable {
System.out.println("[UserServiceAspectAnno][afterRunningAdvice]返回结果之后");
}
//通过throwing属性可以访问抛出的异常对象
@AfterThrowing(pointcut = "userServicePointCut()", throwing = "a")
public void afterThrowingAdvice(Throwable a) throws Throwable {
System.out.println("[UserServiceAspectAnno][afterThrowingAdvice]抛出异常之后");
a.printStackTrace();
}
}
由于@Before与@Around不能同时使用,会编译错误,所以将@Before注释起来。
三、Spring AOP
AOP和IOC是Spring框架的两个核心编程思想。Spring IOC容器将对象间的依赖从对象本身转移到外部容器进行控制,Spring AOP则将非业务代码从业务代码中分离出来,进行统一处理。使用Apring IOC容器,AOP不是必要的,但Spring自身的很多特性与功能是基于AOP的方式实现,比如声明式事务管理。
Spring AOP同时支持基于接口的JDK代理和基于类的CGLIB代理,这两种方式各有优劣。CGLIB创建的动态代理的对象性能要比JDK创建的好,但CGLIB创建代理对象所花费的时间比JDK动态代理要多,所以CGLIB适合单例对象或者池对象的代理,因为不需要频繁创建新实例,反之则适用于JDK动态代理技术。
1.Spring AOP框架的主要接口和类
如果使用Maven管理依赖,spring-aop模块会在导入spring-context模块时一并导入。Spring AOP模块主要包括三部分内容:
- Spring AOP框架核心:包含org.springframework.aop目录下的接口类及除aspectj子目录下的其他接口和类。
- AspectJ的支持:包括AspectJ类型的代理对象、切点和增强等支持。此外,Spring还提供了单独的AspectJ的集成模块spring-aspects。
- aopalliance接口的包装:aopallicance是AOP联盟为了规范AOP开发而定义的规范接口,Spring AOP框架基于该规范接口提供实现。
aopalliance定义的主要接口包括:
- org.aopalliance.aop.Advice:增强接口。
- org.aopalliance.intercept.Interceptor:拦截器接口。该接口继承自Advice。拦截器是增强的一个子集,关注的是某些特定的连接点(比如属性访问、构造器调用以及方法调用)时的动作。
- org.aopalliance.intercept.Joinpoint:连接点接口
- org.aopalliance.intercept.Invocation:调用接口。继承自Joinpoint接口,Invocation是可以被拦截器拦截的Joinpoint。
Spring AOP基于Advice、Interceptor和Joinpoint等接口提供了子接口及实现类,通过动态代理实现AOP功能,AopProxy是代理对象接口,ProxyFactoryBean用于对单个目标对象代理。
Spring继承aopalliance的Advice和Interceptor,定义了一系列增强和拦截器子接口。应用程序可实现增强接口完成自身的增强逻辑类的定义,常用的增强接口如下:
- MethodBeforeAdvice接口:目标方法执行前增强。实现类完成before()方法。
- AfterReturningAdvice接口:目标方法执行后增强(无异常状况下)。实现类完成afterReturning()方法。
- ThrowsAdvice接口:抛出异常时增强。
- IntroductionAdvice接口:引介增强,在不改变目标类的状况下,动态增加新的属性和方法。
- MethodInterceptor接口:方法拦截器,相当于环绕增强,在方法执行前后执行增强。
- ConstructorInterceptor接口:构造器拦截器。
aopalliance定义了连接点接口(Joinpoint),但没有提供切点(Pointcut)接口,于是Spring定义了该类型的接口,如下:
public interface Pointcut {
ClassFilter getClassFilter(); //对应类层级的匹配
MethodMatcher getMethodMatcher(); //对应方法层级的匹配
Pointcut TRUE = TruePointcut.INSTANCE;
}
切点Bean的配置可以使用ComposablePointcut或NameMatchMethodPointcut等,但使用AspectJ表达式定义切点的方式更加简单。
Spring AOP还提供了用于获取代理对象的接口AopProxy,该接口定义了获取代理对象的方法getProxy()。框架默认提供的实现类包括JdkDynamicAopProxy和CglibAopProxy及其子类ObjenesisCglibAopProxy,分别对应JDK和CGLIB的动态代理。在Spring AOP中,类的代理对象不会直接使用JdkDynamicAopProxy或ObjenesisCglibAopProxy创建,而是统一通过代理工厂Bean(ProxyFactoryBean)创建。
public class ProxyFactoryBean extends ProxyCreatorSupport
implements FactoryBean<Object>, BeanClassLoaderAware, BeanFactoryAware
- 实现FactoryBean接口,具备工厂Bean特性,通过getObject()方法得到实际对象。工厂Bean可以更加灵活地扩展该类地功能。
- 实现BeanClassLoaderAware接口:可以使用类加载器地功能。
- 实现BeanFactoryAware接口,具备Spring容器对象(BeanFactory)的功能。
- ProxyCreatorSupport类实现了代理创建、增强等功能,在该类中使用DefaultAopProxyFactory的createAopProxy()创建代理对象。默认使用JDK代理,也可以指定使用CGLIB代理。
2.ProxyFactoryBean的使用
ProxyFactoryBean可以实现单个Bean的代理并织入增强功能,通过在配置文件中 配置代理Bean的目标对象及拦截器即可。目标对象类可以继承接口,也可以不继承接口,框架可以自动选择代理实现方式。
2.1.基于接口的代理
定义IUserService接口及其实现类UserService,如下:
public interface IUserService {
public void doSomthing();
}
public class UserService implements IUserService {
@Override
public void doSomthing() {
System.out.println("----执行doSomthing方法----");
}
}
方法增强实现类MyMethodBeforeAdvice,如下:
public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("前置增强");
}
}
相关的Bean配置:
<bean id="userService" class="com.mec.spring.aop.UserService" />
<bean id="myBeforeAdvice" class="com.mec.spring.aop.MyMethodBeforeAdvice" />
<!--userServiceProxy即为代理对象Bean,该Bean中的方法是增强后的方法-->
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--代理的接口,基于JDK接口的代理时配置,不配置容器也可以自行处理。-->
<property name="interfaces" value="com.mec.spring.aop.IUserService" />
<property name="target">
<!--委托类的Bean-->
<ref bean="userService"/>
</property>
<property name="interceptorNames">
<list>
<!--代理对象绑定的增强或拦截器的Bean-->
<value>myBeforeAdvice</value>
</list>
</property>
</bean>
2.2.基于类的代理
如果目标对象没有实现任何接口,ProxyFactoryBean会自动使用CGLIB代理,也可以设置proxyTargetClass属性值为true的方式强制指定。
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref bean="userService"/>
</property>
<property name="interceptorNames" value="myBeforeAdvice" />
<property name="proxyTargetClass" value="true"></property>
</bean>
ProxyFactoryBean还可以配置的属性有:
- singleton:生成的代理Bean是否单例,默认是单例;
- autodetectInterfaces:自动侦测接口,默认值为true,上面基于接口的代理中不强制要求配置接口的原因就是如此。
2.3.使用拦截器替代增强
MethodInterceptor类似于环绕增强,如下:
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("前置拦截");
Object res = invocation.proceed();
System.out.println("后置拦截");
return res;
}
}
在配置中,将代理Bean的interceptorNames配置为拦截器的Bean的id即可。
3.增强器(Advisor)
上面的ProxyFactoryBean代理中没有任何过滤条件,所有方法执行都会调用配置的增强功能。interceptorNames除了可以指定Advice类型的Bean,还可以指定Advisor类型的Bean。Advisor是切点(Pointcut)和增强(Advice)的适配器,即Advisor用来指定满足切点定义的方法增强。修改配置如下:
<bean id="userService" class="com.mec.spring.aop.UserService" />
<bean id="myMethodInterceptor" class="com.mec.spring.aop.MyMethodInterceptor"></bean>
<!-- 配置切点类型的Bean,该类会匹配方法名。以直接使用NameMatchMethodPointcut,也可以继承该类实现自定义的匹配逻辑 -->
<bean id="namePointCut" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedNames">
<list>
<!-- 匹配以do为前缀的方法名 -->
<value>do*</value>
</list>
</property>
</bean>
<bean id="defaultAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut"> <!-- 切点 -->
<ref bean="namePointCut" />
</property>
<property name="advice"><!-- 增强 -->
<ref bean="myMethodInterceptor" />
</property>
</bean>
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref bean="userService"/>
</property>
<property name="interceptorNames" value="defaultAdvisor" />
<property name="proxyTargetClass" value="true"></property>
</bean>
通过以上配置,在执行增强代码之前,都会首先判断是否应该拦截该方法,只对拦截的方法织入增强。
Advisor是增强器的接口,对应两个子接口IntroductionAdvisor(引介增强器)和PointcutAdvisor(切入点增强器)。Spring AOP提供了许多接口的实现类,其中常用的如下:
- DefaultPointcutAdvisor:默认的切点增强器,该类型实现切点Bean配置拦截规则,开发较为繁琐,但比较灵活。
- RegexpMethodPointcutAdvisor:方法名正则表达式切点增强器,使用patterns属性配置正则表达式匹配拦截的方法。
- NameMatchMethodPointcutAdvisor:方法名匹配切点增强器,直接通过mappedNames属性设置需要拦截的方法名匹配,也可以通过*通配符。
4.基于XML的AOP配置
Spring AOP的实现虽然与AspectJ无关,但使用了aspectjweaver的一些功能,所以Spring AOP需要导入aspectjweaver依赖包。aspectjweaver主要提供类加载期织入切面的功能。Spring提供了AOP的相关标签来简化开发,导入Spring AOP和aspectjweaver包并在配置文件的根节点加入aop命名空间之后,就可以在配置文件中使用<aop:config>标签进行AOP配置。
4.1.<aop:aspect>切面标签
如下通过一个示例展示AOP的配置方式,先定义切面类,如下:
//切面类不需要继承特定的接口或类,唯一要求是增强方法必须要包含一个org.aspectj.lang.JoinPoint类型的参数
public class MyAspect {
// JoinPoint用于获取被切入点传入的参数,任何切入方法的第一个参数都可以是JoinPoint
// 该参数对象可以获取目标对象和方法名等信息
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("前置增强");
}
}
配置如下:
<!--委托类Bean(目标Bean)-->
<bean id="userService" class="com.mec.spring.aop.UserService" />
<!--切面Bean-->
<bean id="myAspect" class="com.mec.spring.aop.MyAspect" />
<!-- AOP配置根标签 -->
<aop:config>
<!-- 切面Bean的id -->
<aop:aspect ref="myAspect">
<!-- 定义切点 -->
<aop:pointcut id="myPointcut" expression="execution(* com.mec.spring.aop.UserService.*(..))"/>
<!--
引用上面切点配置,method指定切面类中的方法名
同样的,还有
1.<aop:after>(配置后置增强的标签)
2.<aop:around>(配置环绕增强的标签)
3.<aop:after-returning>(方法返回之后的增强)
4.<aop:after-throwing>(抛出异常之后的增强)
-->
<!--这里可以通过pointcut-ref引用上面的切点配置,也可以使用pointcut属性直接配置切点表达式-->
<aop:before pointcut-ref="myPointcut" method="beforeAdvice"/>
</aop:aspect>
</aop:config>
通过以上配置,获取userService的Bean,调用其中方法就是增强之后的方法。
4.2.<aop:advisor>增强器标签
增强器(Advisor)包括切点和增强两部分,所以在<aop:advisor>标签中需要使用advice-ref和pointcut-ref(或pointcut)属性配置增强Bean和切点。<aop:advisor>可以看成是<aop:aspect>一种特殊类型,同样配置在<aop:config>标签下。
<bean id="userService" class="com.mec.spring.aop.UserService"></bean>
<bean id="myBeforeAdvice" class="com.mec.spring.aop.MyMethodBeforeAdvice"></bean>
<!-- AOP配置根标签 -->
<aop:config>
<aop:pointcut expression="execution(* com.mec.spring.aop.UserService.*(..))" id="myPointcut"/>
<aop:advisor advice-ref="myBeforeAdvice" pointcut-ref="myPointcut"/>
</aop:config>
4.3.<aop:aspect>和<aop:advisor>比较
- <aop:aspect>标签的ref属性指定切面类的Bean名称,该切面类没有特殊要求;但<aop:advisor>的advice-ref属性的Bean就需要实现Advice接口。
- 若<aop:config>下同时定义了<aop:aspect>和<aop:advisor>,那么<aop:advisor>需要放在前面。
- <aop:advisor>一般用在事务处理中,而<aop:aspect>一般用在缓存和日志等功能中。如下为事务处理的AOP配置:
<!-- 事务增强 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 所有get前缀的方法开启事务 -->
<tx:method name="get*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 切面定义 -->
<aop:config>
<aop:pointcut id="interceptorPointCuts" expression="execution(* com.mec.spring.db.UserDaoImpl.getUser(int))"/>
<!-- 织入增强 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="interceptorPointCuts"/>
</aop:config>
5.基于注解的AOP配置
在使用组件扫描的组件注解开发方式下,使用AspectJ提供的注解可以完全替代<aop:aspect>和<aop:advisor>等标签配置。结合组件扫描的配置,只需要在配置文件中加上如下配置即可:
<context:component-scan base-package="com.mec.spring.aop" />
<!-- proxy-target-class设置为true即表示强制使用CGLIB代理,若不设置,则框架可以根据该类是否继承接口自动选择代理方式 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
对应AOP标签配置元素,Spring AOP支持的AspectJ注解包括:
- @Aspect:切面类注解,使用在类上,相当于<aop:config>;
- @Pointcut:注解在方法上,使该方法成为一个切入点,对应<aop:pointcut>;
- @Before:前置增强注解;
- @After:后置增强注解,目标方法之后执行,不论执行成功或者失败(抛出异常)都执行;
- @AfterReturning:成功执行后置注解,目标方法执行成功后执行,异常不执行;
- @Around:环绕增强注解,执行前、执行后都可以执行;
- @AfterThrowing:异常增强注解,发生异常执行。
@Aspect仅仅是切面注解,不会自动注册成Bean,需要结合@Component及其子注解使用。这些注解的使用方法与前面AspectJ介绍中的一致。需要注意的是:增强方法中需要包含一个JoinPoint类型的参数,如果一个类被@Aspect标注,则这个类就不能作为目标类进行增强,使用@Aspect注解后,这个类就会被排除在auto-proxying机制之外。
在完全注解的开发中,使用@EnableAspectJAutoProxy注解在配置类上,可替代<aop:aspectj-autoproxy>标签。