目录
什么是AOP
面向切面编程是一种思想,其编程思想就是把散布于不同业务但功能相同的代码从业务逻辑中抽取出来,封装成独立的模块,这些独立的模块被称为切面,比如权限认证、日志、事务、上下文处理、异常处理、懒加载、缓冲。
面向切面的目的就是解耦合,分离系统的各种关注点,将业务处理主流程(核心关注点)和与主流程关系不大的部分(横切关注点)分离,做到职责单一,专人做专事。
为什么实现AOP
我们知道日志的实现是基于AOP。对访问的方法进行日志记录,其实方法很多,那么为什么最后选择了AOP方式呢?我们一步一步瞅瞅
硬编码
硬编码的方式看是解决了日志记录的问题,但是处理代码相同,严重耦合,一旦需要修改,而且方法成千上万,那这将是一次浩大的工程,工程师一定是忍受不了的,那么接下来就用OOP的方式改进一下。
OOP
这是OOP的两种实现方式,OOP的核心思想就是封装,继承。
第一种方式将日志处理代码抽取层单独的类。这样当修改时,只需调整单独类中的代码,避免了硬编码导致的随处挖坟问题,大大降低了维护的复杂度。
第二种方式是通过继承来解决的方式,只需把抽取的相同代码放到一个类,其他类(子类)通过继承获取相同的代码。
通过上面的两种方法,代码冗余得到了解决。但随着软件系统开发的越来越复杂,代码量越来越多,我们的愿景是,核心业务代码就是核心业务代码,但是以上两种方式,往往导致核心业务代码中掺杂着一些不相关的特殊业务,比如日志记录、事务控制、错误信息监测等等。这样就会造成代码混乱、分散、冗余等问题。那么如何实现我们的愿景,将核心业务代码和其他外围操作的代码再进行分离,将这些外围模块可以实现热插拔特性而且无需入侵到核心模块中呢?那就是AOP了。
AOP
我们的实现目标就是将外围业务看做单独的关注点,在需要他们的时候可以及时的运用而且无需提前整合到核心模块中。每个关注点与核心业务模块分离,作为单独的功能,横切几个核心业务模块。这种抽象级别的技术就叫做AOP面向切面编程。而单独的这些关注点,叫做横切关注点。
AOP的实现技术有多种,AspectJ可以与Java无缝对接。
AspectJ
aspectJ是一个java实现的AOP框架,和spring AOP实现原理不一样,但功能上是相似的。开发中我们直接使用的是spring AOP,了解aspectJ可以有助于理解spring AOP。
1、使用aspectJ技术实现AOP的时候,aspect是声明切面类的关键字,而且文件后缀是.aj,含义与.class类似。
2、在切面内部使用了pointcut定义了切点,切点就是那些需要引用切面的方法,需要引用切面的方法也称为目标方法。
3、使用before()、after()、after returning()、after throwing()、around()定义通知,通知就是那些需要在目标方法前后执行的函数。
4、所以,切面就是切点和通知的组合体,组成一个单独的结构工后续使用。
5、把切面引用到目标方法的过程称为织入,也就是说这些方法执行前后都可以切入通知的代码,这些目标方法统称为连接点,切点正是连接点的集合。
aspectJ的使用如下:
/**
* 切面类
*/
public aspect MyAspectJDemo {
/**
* 定义切点,日志记录切点
*/
pointcut recordLog():call(* HelloWord.sayHello(..));
/**
* 定义切点,权限验证(实际开发中日志和权限一般会放在不同的切面中,这里仅为方便演示)
*/
pointcut authCheck():call(* HelloWord.sayHello(..));
/**
* 定义前置通知!
*/
before():authCheck(){
System.out.println("sayHello方法执行前验证权限");
}
/**
* 定义后置通知
*/
after():recordLog(){
System.out.println("sayHello方法执行后记录日志");
}
}
织入
上面提到AspectJ和spring AOP的实现原理不一样,正是体现在织入中。织入表示切面应用到目标函数的过程。对于这个过程,一般分为动态织入和静态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术,这点后面会分析。
这里主要重点分析一下静态织入,ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
关于ajc编译器,是一种能够识别aspect语法的编译器,它是采用java语言编写的,由于javac并不能识别aspect语法,便有了ajc编译器,注意ajc编译器也可编译java文件。
spring AOP
AspectJ在AOP的实现方式上依赖于特殊编译器(ajc编译器),实现编译器织入。Spring为了回避这点,转向采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入),这是与AspectJ(静态织入)最根本的区别。在AspectJ 1.5后,引入@Aspect形式的注解风格的开发,Spring也非常快地跟进了这种方式,引入spring-aspects jar包,可以使用AspectJ的注解。
spring AOP的概念和上面介绍的aspectJ的概念是一样的,如切点(pointcut)定义需要应用通知的目标函数,通知则是那些需要应用到目标函数而编写的函数体,切面(Aspect)则是通知与切点的结合。织入(weaving),将aspect类应用到目标函数(类)的过程,只不过Spring AOP底层是通过动态代理技术实现罢了。
基于注解的spring aop开发
定义切面类
/**
* 切面类
* @author lfy
*
* @Aspect: 告诉Spring当前类是一个切面类
*
*/
@Aspect
public class LogAspects {
//抽取公共的切入点表达式
//1、本类引用
//2、其他的切面引用
@Pointcut("execution(public int com.atguigu.aop.MathCalculator.*(..))")
public void pointCut(){};
//@Before在目标方法之前切入;切入点表达式(指定在哪个方法切入)
//@Before("public int com.atguigu.aop.MathCalculator.*(..))
//抽取公共切入点表达式后
@Before("pointCut()")
//JoinPoint 获取方法名、参数、返回值、异常信息
public void logStart(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
System.out.println(""+joinPoint.getSignature().getName()+"运行。。。@Before:参数列表是:{"+Arrays.asList(args)+"}");
}
@After("com.atguigu.aop.LogAspects.pointCut()")
public void logEnd(JoinPoint joinPoint){
System.out.println(""+joinPoint.getSignature().getName()+"结束。。。@After");
}
//JoinPoint一定要出现在参数表的第一位
@AfterReturning(value="pointCut()",returning="result")
public void logReturn(JoinPoint joinPoint,Object result){
System.out.println(""+joinPoint.getSignature().getName()+"正常返回。。。@AfterReturning:运行结果:{"+result+"}");
}
@AfterThrowing(value="pointCut()",throwing="exception")
public void logException(JoinPoint joinPoint,Exception exception){
System.out.println(""+joinPoint.getSignature().getName()+"异常。。。异常信息:{"+exception+"}");
}
}
1、@Aspect注解定义了切面类
2、@Pointcut定义了切入点函数,通知类型注解也可以直接定义切入点函数,如@Before("public int com.atguigu.aop.MathCalculator.*(..))
3、@Before、@After、@AfterReturning、@AfterThrowing、@Around定义了五种通知类型。
4、execution是切入点指示符,切入点指示符标识通知应用到哪些目标方法上。
4.1、execution表示方法签名表达式:如果想根据方法签名进行过滤,关键字execution可以帮到我们,语法格式如下:
//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))
除了execution,还有其他切入点指示符
4.2、类型签名表达式:为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字,语法格式如下:
within(<type name>)
//匹配com.zejian.dao包及其子包中所有类中的所有方法
@Pointcut("within(com.zejian.dao..*)")
//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl)")
//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl+)")
4.3、通配符
.. :匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包
+ :匹配给定类的任意子类
* :匹配任意数量的字符
4.4、@annotation(com.zejian.spring.MarkerMethodAnnotation) : 根据所应用的注解进行方法过滤
//匹配使用了MarkerAnnotation注解的方法(注意是方法)
@Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)")
private void myPointcut5(){}
定义业务逻辑类
public class MathCalculator {
public int div(int i,int j){
System.out.println("MathCalculator...div...");
return i/j;
}
}
开启AOP并将切面类和业务逻辑类加入到容器
@EnableAspectJAutoProxy
@Configuration
public class MainConfigOfAOP {
//业务逻辑类加入容器中
@Bean
public MathCalculator calculator(){
return new MathCalculator();
}
//切面类加入到容器中
@Bean
public LogAspects logAspects(){
return new LogAspects();
}
}
@EnableAspectJAutoProxy注解表示开启AOP,开启后spring容器会尝试自动识别带@Aspect的Bean
@Configuration+@Bean 表示将Bean加入到容器
测试类
@Test
public void test01(){
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);
//1、不要自己创建对象
// MathCalculator mathCalculator = new MathCalculator();
// mathCalculator.div(1, 1);
//在创建Bean的过程中,如果判断到该Bean需要增强,就会创建代理对象
MathCalculator mathCalculator = applicationContext.getBean(MathCalculator.class);
//此处的mathCalculator对象已经是代理对象
mathCalculator.div(1, 0);
applicationContext.close();
}
基于XMl的开发
前面分析完基于注解支持的开发是日常应用中最常见的,即使如此我们还是有必要了解一下基于xml形式的Spring AOP开发,也需要定义如上的一个切面类,然后启动切面功能,将切面类配置为Bean、定义通知等都需要在xml文件中配置。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--<context:component-scan base-package=""-->
<!-- 启动@aspectj的自动代理支持-->
<aop:aspectj-autoproxy />
<!-- 定义目标对象 -->
<bean name="productDao" class="com.zejian.spring.springAop.dao.daoimp.ProductDaoImpl" />
<!-- 定义切面 -->
<bean name="myAspectXML" class="com.zejian.spring.springAop.AspectJ.MyAspectXML" />
<!-- 配置AOP 切面 -->
<aop:config>
<!-- 定义切点函数 -->
<aop:pointcut id="pointcut" expression="execution(* com.zejian.spring.springAop.dao.ProductDao.add(..))" />
<!-- 定义其他切点函数 -->
<aop:pointcut id="delPointcut" expression="execution(* com.zejian.spring.springAop.dao.ProductDao.delete(..))" />
<!-- 定义通知 order 定义优先级,值越小优先级越大-->
<aop:aspect ref="myAspectXML" order="0">
<!-- 定义通知
method 指定通知方法名,必须与MyAspectXML中的相同
pointcut 指定切点函数
-->
<aop:before method="before" pointcut-ref="pointcut" />
<!-- 后置通知 returning="returnVal" 定义返回值 必须与类中声明的名称一样-->
<aop:after-returning method="afterReturn" pointcut-ref="pointcut" returning="returnVal" />
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pointcut" />
<!--异常通知 throwing="throwable" 指定异常通知错误信息变量,必须与类中声明的名称一样-->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="throwable"/>
<!--
method : 通知的方法(最终通知)
pointcut-ref : 通知应用到的切点方法
-->
<aop:after method="after" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
Spring AOP的实现过程
- 不管是注解实现方式还是xml实现方式,开启AOP功能是我们的入口点,比如注解方式,@EnableAspectJAutoProxy 开启AOP功能
-
@EnableAspectJAutoProxy 会给容器中注册一个组件 AnnotationAwareAspectJAutoProxyCreator,是一个后置处理器,而这个后置处理器的作用首要是创建代理对象
- IOC容器创建过程
-
registerBeanPostProcessors()注册后置处理器;创建AnnotationAwareAspectJAutoProxyCreator对象
-
finishBeanFactoryInitialization()初始化剩下的单实例bean
-
创建业务逻辑组件和切面组件
-
AnnotationAwareAspectJAutoProxyCreator拦截组件的创建过程
-
组件创建完之后,判断组件是否需要增强
-
是:切面的通知方法,包装成增强器(Advisor);给业务逻辑组件创建一个代理对象(cglib)
-
-
-
-
执行目标方法:
-
代理对象执行目标方法
-
CglibAopProxy.intercept()进行拦截 ;
-
得到目标方法的拦截器链(增强器包装成拦截器MethodInterceptor)
-
利用拦截器的链式机制,依次进入每一个拦截器进行执行;
-
效果:
-
正常执行:前置通知-》目标方法-》后置通知-》返回通知
-
出现异常:前置通知-》目标方法-》后置通知-》异常通知
-
-
-
AOP实现原理
spring AOP的实现原理前面的分析中,我们谈到Spring AOP的实现原理是基于动态织入的动态代理技术,而AspectJ则是静态织入,而动态代理技术又分为Java JDK动态代理和CGLIB动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现。
在上边的过程中,我么可以发现,在判断当前bean是否增强的时候,如果需要增强,就会创建代理对象,创建的时候如果当前bean实现了接口,会使用jdk动态里,如果没有实现接口,就会使用cglib动态代理。
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
//使用jdk动态代理实现
return new JdkDynamicAopProxy(config);
} else {
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.");
} else {
//使用cglib动态代理实现
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
}
JDK动态代理
jkd的实现逻辑以及代理类的创建过程可以参考我之前写的博客:jkd动态代理
AOP创建代理对象的逻辑判断是:如果被代理类有实现接口,才会使用jdk动态代理。所以jdk的动态代理的先决条件是目标对象必须带接口。原因是在从容器获取bean时,如果判断到该bean需要增强,就会通过反射获取到目标对象的接口,动态代理技术便可以创建与目标对象类型相同的代理对象。得到代理对象后,便可以由代理对象来执行目标方法。
CGLib动态代理
cglib动态代理的实现过程也可以参考我之前写的博客:cglib动态代理
AOP创建代理对象的逻辑判断是:如果被代理类没有实现接口,就会使用cglib动态代理。所以可以看到一点就是,cglib动态代理不需要和jdk动态代理一样,先决条件必须实现接口。
cglib动态代理和JDk动态代理的不同之处在于cglib动态代理是通过继承被代理类,生成的动态代理类是被代理类的子类,然后通过重写业务方法来实现代理。
虽然被代理的类不需要实现接口即可实现动态代理,但是CGLibProxy代理类需要实现一个方法拦截器接口MethodInterceptor并重写intercept方法,类似JDK动态代理的InvocationHandler接口,也是理解为回调函数,同理每次调用代理对象的方法时,intercept方法都会被调用,利用该方法便可以在运行时对方法执行前后进行动态增强。
spring AOP的实现到此就结束了。以上有不正确的地方,感谢大家指正。