aop小结

aop 相关理论知识

一直再用aop,一直都没时间整理一下,得空整理一下,备忘!

基本概念

  1. 通知(Advice):
    通知定义了切面是什么以及何时使用。描述了切面要完成的工作和何时需要执行这个工作。
  2. 连接点(Joinpoint):
    程序能够应用通知的一个“时机”,这些“时机”就是连接点,例如方法被调用时、异常被抛出时等等。(@After,@Around,@Before.@AfterThrowing.)
  3. 切入点(Pointcut)
    通知定义了切面要发生的“故事”和时间,那么切入点就定义了“故事”发生的地点,例如某个类或方法的名称,spring中允许我们方便的用正则表达式来指定(@After 中的 execution )
  4. 切面(Aspect)
    通知和切入点共同组成了切面:时间、地点和要发生的“故事”
  5. 引入(Introduction)
    引入允许我们向现有的类添加新的方法和属性(@DeclareParents,可以理解成 Adaptor模式)
  6. 目标(Target)
    即被Advice的对象,如果没有AOP,那么它的逻辑将要交叉别的事务逻辑,有了AOP之后它可以只关注自己要做的事(AOP 让他关注主业务, 日志,性能统计等可以交给Advice 来做)
  7. 代理(proxy)
    调用Advice的对象,详细内容参见设计模式里面的代理模式
  8. 织入(Weaving)
    把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
    (1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器
    (2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码(很少用到,也是一个实现的思路)
    (3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理基于JDK的动态代理技术

pointcut 切入点 格式

Pointcut可以有下列方式来定义或者通过&& || 和!的方式进行组合.

  • args()
  • @args()
  • execution()
  • this()
  • target()
  • @target()
  • within()
  • @within()
  • @annotation

execution

模板

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

demo

任意公共方法的执行:
execution(public * *(..))
任何一个以“set”开始的方法的执行:
execution(* set*(..))
AccountService 接口的任意方法的执行:
execution(* com.xyz.service.AccountService.*(..))
定义在service包里的任意方法的执行:
execution(* com.xyz.service.*.*(..))
定义在service包和所有子包里的任意类的任意方法的执行:
execution(* com.xyz.service..*.*(..))
定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:
execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")

@within和@target针对类的注解,@annotation是针对方法的注解,@arg 针对参数(必须是在目标对象上声明注解,在接口上声明的不起作用)

带有@Transactional标注的所有类的任意方法.
@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)(注解类型必须是全限定类型名)

带有@Transactional标注的任意方法.
@annotation(org.springframework.transaction.annotation.Transactional)

参数带有@Transactional标注的方法.
@args(org.springframework.transaction.annotation.Transactional)

within,target,this

1. 当前 AOP 对象实现了IPointcutService 接口的任何方法(this 中
使用的表达式必须是类型全限定名,不支持通配符)
    this(cn.javass.spring.chapter6.service.IIntroductionService)
2. cn.javass 包及子包下的任何方法执行
    within(cn.javass..*) 
3. target :使用 “target( 类型全限定名 )” 匹配当前目标对象类型的执行方法;注意
是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意 target 中使用的表
达式必须是类型全限定名,不支持通配符;

args

args表达式 有如下两个作用:
① 提供了一种简单的方式来访问目标方法的参数。
② 可用于对切入表达式增加额外的限制。

demo

 @AfterReturning(returning="retVal", pointcut="execution(* com.chapa.annotation.service.*.*(..)) && args(var,word)")
    public void access(String var, String word, Object retVal){
      //todo
    }

    @AfterReturning(returning="retVal", pointcut="execution(* com.chapa.annotation.service.*.*(..)) && args(String,String)")
    public void access( Object retVal){
        //todo
    }

aop 的两种织入实现方式

spring aop 运行时通过动态代理织入

aop 通过spring容器,基于代理类运行时实现aop功能,注意所有的相关类必须是基于spring容器初始化,否则无法实现aop功能

  • jdk dynamic proxy
  • 基于cglib的 动态代理
    cglib 能基于具体实现类实现动态代理,默认的jdk动态代理的不能对具体类实现代理,只能基于接口.

spring aop 的两种实现

基于配置文件
<aop:config proxy-target-class="false">
        <aop:aspect id="TestAspect" ref="aspectAdvice" order="1">
            
            <aop:pointcut id="businessService" expression="execution(* com.chapa.service.*.*(..))" />
            <aop:before pointcut-ref="businessService" method="doBefore"/>
            <aop:after pointcut-ref="businessService" method="doAfter"/>
            <aop:around pointcut-ref="businessService" method="doAround"/>
            <aop:after-throwing pointcut-ref="businessService" method="doThrowing" throwing="ex"/>
        aop:aspect>

        <aop:aspect id="secondAspect" ref="secondAdvice" order="2">
            
            <aop:pointcut id="businessService" expression="execution(* com.chapa.service.*.*(..))" />
            <aop:before pointcut-ref="businessService" method="doBefore"/>
            <aop:after pointcut-ref="businessService" method="doAfter"/>
            <aop:around pointcut-ref="businessService" method="doAround"/>
            <aop:after-throwing pointcut-ref="businessService" method="doThrowing" throwing="ex"/>
        aop:aspect>

    aop:config>
基于注解
 <context:component-scan base-package="com.chapa.annotation.*"/>
    <context:annotation-config/>
    <aop:aspectj-autoproxy proxy-target-class="true"/>
  • @Aspect 织入点class 注解
  • @After , @Around,@Before@AfterThrowing 对应上文配置文件中的织入方式, method注解

基于aspectj 编译时weaver

aspectj实现,在编译期已经完成了相关aop功能的织入.基于 aspectj 实现时,需要用ajc 自己的编译器,
同时因为aspectj实现,实际上是已经在编译期把对应aop代码织入到目标对象,所以不需要通过spring容器初始化对象,实际上aspectj实现已经和spring没有关系了.

ajc maven编译插件

<plugin>
   <groupId>org.codehaus.mojogroupId>
   <artifactId>aspectj-maven-pluginartifactId>
   <configuration>
       <complianceLevel>1.8complianceLevel>
   configuration>
   <executions>
       <execution>
           <goals>
               <goal>compilegoal>
               <goal>test-compilegoal>
           goals>
       execution>
   executions>
plugin>

编译器设置ajc编译,idea参见下文,eclipse需要安装插件,自行百度

aop 中的几点关键申明

1. scoped-proxy

coped-proxy 指定由ScopedProxyFactoryBean创建bean,ScopedProxyFactory为这个Bean增加了getTargetObject的方法(使用Introduction),因此所有带上了这个标签的Bean,也就默认实现了ScopedObject的接口,可以调用getTargetObject方法。这个方法的意义在于,因为代理Bean的scope是默认singleton的,这也就意味着,我们每次调用applicationContext.getBean方法,总是返回同一个代理bean,如果我们想要获得scope下真正的bean的话,就实际上调用getTargetObject方法了。
不加scoped-proxy,引用该对象时,只有在最开始注入的时候,获得该对象的代理,也就是说实际上这个引用指向spring初始话的唯一代理对象,无法实现真正意义上的这个对象对因的scope周期.(该注解 合适使用在httpsession 场景,session Scope是web环境下的,有兴趣的可以自行验证)

<aop:aspect>
   <aop:declare-parents
            types-matching="com.chapa.service.TestServiceImpl"
            implement-interface="com.chapa.service.IIntroduce"
            default-impl="com.chapa.service.IntroduceImpl"/>
aop:aspect>
<bean id="testService" class="com.chapa.service.TestServiceImpl" scope="prototype">
   <aop:scoped-proxy/>
bean>
<bean class="com.chapa.service.TestServiceWithoutInterface">bean>
<bean id="singletonBean" class="com.chapa.service.SingletonBean" >
    <property name="testService">
        <ref bean="testService" />
    property>
bean>

注:coped-proxy 和 Introduction (aop:declare-parents) 共同使用的时候不能设置对像scope为prototype,会导致注入的类强制转换失败(参见demo,具体原因,有兴趣的可以研读一下代码,坐等大神解惑)

2. order && @DeclarePrecedence

这两个都是用来 指定织入顺序,有些业务逻辑必须指定织入顺序否则会导致结果不一致.aop的织入顺序类似 serverlet的 filter ,先进后出,环形嵌套织入的规则. @Order(1) 值越小优先级越大, 只对spring aop 有效,对aspectj 编译实现的aop无效.@DeclarePrecedence ,aspectj 专用的指定顺序的注解,使用改注解必须使用 ajc 编译,使用javac编译会报错.

3. declare-parents

允许我们向现有的类添加新的方法和属性,类似Adatper模式. (@DeclareParents 注解方式,config方式参见上文). value表示目标对象,defaultImpl 待引用对象的实现类.

 @DeclareParents(value = "com.chapa.annotation.service.TestServiceImpl",defaultImpl =com.chapa.annotation.service.IntroduceImpl.class)
    IIntroduce testService;

4.expose-proxy

aop 默认无法对 对象调用自己的方法实现增强,必须基于expose-proxy,实现AOP代理对象的ThreadLocal支持.

<aop:aspectj-autoproxy expose-proxy="true"/><!—注解风格支持-->
<aop:config expose-proxy="true"><!—xml风格支持--> 

设置expose-proxy后,按下面的方式调用当前类的方法,就能实现aop

 public void hhh() {
        System.out.println("TestServiceImpl with interface");
        ((ITestService) AopContext.currentProxy()).hprintTime();
    }

demo 验证

环境准备

  • jdk8
  • idea16
  • spring 4.1.6

配置idea 使用ajc 编译

aop小结_第1张图片

关键代码分析

初始化Spring代理对象 (基于 Spring 2.5.6 早期版本 )

 public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        // isProxyTargetClass()  通过spring 设置 是否强制 使用 cglib    默认 false
        if(!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
            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 if(targetClass.isInterface()) {
                return new JdkDynamicAopProxy(config);
            } else if(!cglibAvailable) {
                throw new AopConfigException("Cannot proxy target class because CGLIB2 is not available. Add CGLIB to the class path or specify proxy interfaces.");
            } else {
                return DefaultAopProxyFactory.CglibProxyFactory.createCglibProxy(config);
            }
        }
    }
   //通过类加载器  判断 当前环境是否加载了 cglib 相关类
static {
        cglibAvailable = ClassUtils.isPresent("net.sf.cglib.proxy.Enhancer", DefaultAopProxyFactory.class.getClassLoader());
 }

注:早期版本项目必须依赖cglib 否则 对无接口类的 代理实现 会报错

 <dependency>
            <groupId>cglibgroupId>
            <artifactId>cglibartifactId>
            <version>2.2.2version>
        dependency>

注:从 spring 3.2.2 版本开始 spring 提供了 cglib 的实现 org.springframework.cglib.proxy.Enhancer,不用在强制依赖cglib

scope-proxy 实现 源码分析

aop Advice demo

@Aspect
@Component // @Repository,@Service,@Controller  表识 需要spring 加载到容器中 ,本质上目前没发现 他们四个有任何区别,不过语义上最好用在对应的层级上
@Scope(value=ConfigurableBeanFactory.SCOPE_SINGLETON,proxyMode= ScopedProxyMode.DEFAULT) //指定 spring容器生成改对像的方式(单列.原型) spring 生成代理的方式(基于interface的jdk dynamic proxy,基于cglib)
@Order(3) // 指定3spring aop 的植入顺序 ,必须基于代理的模式 才有效
@DeclarePrecedence("TestAdvice,SecondAdvice")  //指定aop植入顺序 (ajc注解  需要指定使用ajc编译代码)
public class TestAdvice {
    @After("execution(* com.chapa.annotation.service.*.*(..))")
    public void doAfter(JoinPoint jp) {
        System.out.println("TestAdvice annotation log Ending method: " + jp.getTarget().getClass().getName() + "." + jp.getSignature().getName());
    }

    @Around("execution(* com.chapa.annotation.service.*.*(..))")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        long time = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        time = System.currentTimeMillis() - time;
        System.out.println("TestAdvice annotation process time: " + time + " ms");
        return retVal;
    }

    @Before("execution(* com.chapa.annotation.service.*.*(..))")
    public void doBefore(JoinPoint jp) {
        System.out.println("TestAdvice annotation log Begining method: " + jp.getTarget().getClass().getName() + "." + jp.getSignature().getName());
    }

    @AfterThrowing(throwing="ex",pointcut="execution(* com.chapa.annotation.service.*.*(..))")
    public void doThrowing(JoinPoint jp, Throwable ex) {
        System.out.println("TestAdvice annotation method " + jp.getTarget().getClass().getName() + "." + jp.getSignature().getName() + " throw exception");
        System.out.println(ex.getMessage());
    }
}

gitHub demo

gitHub 地址

https://github.com/chen8238065/aop-study

简介

  • mvn -f pom-ajc.xml clean compiler (使用ajc编译项目),观察编译后的class,你会发现是有config的aop编译期都没有生成对应功能的字节码,只有是有aspectj注解的才有生成相应的字节码.
    aop小结_第2张图片
  • test aspectj编译期织入 效果
    aop小结_第3张图片

目录结构

aop小结_第4张图片

相关文献

你可能感兴趣的:(JAVA)