spring揭秘10-aop04-基于AspectJ类库注解织入横切逻辑

文章目录

【README】

本文总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;



【1】基于AspectJ类库注解实现spring aop

1)概述:@AspectJ代表一种定义Aspect切面的风格,让我们能够以POJO形式定义Aspect,没有其他接口定义限制; 唯一需要的是使用相应的注解标注这些Aspect定义的POJO类; spring aop会根据注解扫描出Aspect类,然后织入横切逻辑;

  • 注意:spring aop使用AspectJ类库注解进行标注及注解解析, 但最终还是采用spring aop自身的代理模式实现横切逻辑的织入
  • 简单理解: spring aop使用了AspectJ类库注解,通过AspectJ类库的注解扫描与解析逻辑扫描得到Aspect切面,Advice通知,pointcut切点表达式;拿着 aspect,advice,pointcut,最终还是使用spring aop自身的代理模式(JDK动态代理或CGLIB动态代理)实现横切逻辑织入;


【1.1】使用AspectJ类库织入器实现aop

【1.1.1】基于AspectJ注解的硬编码织入

1)通过AspectJ类库注解重构切面(方法执行耗时统计):

【ManNoItfCallTaskAspectjAnnotationMain】基于AspectJ注解织入横切逻辑测试main

public class ManNoItfCallTaskAspectjAnnotationMain {
    public static void main(String[] args) {
        AspectJProxyFactory weaver = new AspectJProxyFactory();
        weaver.setProxyTargetClass(true);
        weaver.setTarget(new ManNoItfCallTask());
        weaver.addAspect(TimeCostAspectByAnnotation.class);

        // 获取代理对象
        Object proxy = weaver.getProxy();
        ((ManNoItfCallTask) proxy).call(BusiMessage.build("任务编号001", "您有待办任务需要处理"));
        System.out.println(proxy.getClass());
    }
}

【打印日志】

stopWatch.start()
人工拨打电话#call(): BusiMessage{msgId='任务编号001', msgText='您有待办任务需要处理'}
stopWatch.stop()
方法执行耗时2.0127219
class com.tom.springnote.common.aop.ManNoItfCallTask$$SpringCGLIB$$0  // 显然通过CGLIB代理

【TimeCostAspectByAnnotation】通过Aspect注解标注切面,包含pointcut注解标注切点表达式,around注解标注环绕通知(横切逻辑)

@Aspect
public class TimeCostAspectByAnnotation {

    @Pointcut("execution(public void *.call(..))")
    public void pointcutName() {
        // pointcut
    }

    @Around("pointcutName()")
    public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        try {
            System.out.println("stopWatch.start()");
            stopWatch.start();
            return joinPoint.proceed();
        } catch (Exception e) {
            System.out.println("抛出异常");
            e.printStackTrace();
        } finally {
            System.out.println("stopWatch.stop()");
            stopWatch.stop();
            System.out.printf("方法执行耗时%s\n", stopWatch.getTotalTime(TimeUnit.SECONDS));
        }
        return null;
    }
}

【ManNoItfCallTask】 目标对象POJO

public class ManNoItfCallTask {
    public void call(BusiMessage message) {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            System.out.println("抛出异常");
            throw new RuntimeException(e);
        }
        System.out.println("人工拨打电话#call(): " + message);
    }
}


【1.1.2】使用AspectJ注解与xml文件DTD配置实现自动代理织入

1)dtd:document type definition, 文档类型定义;用于描述xml文档结构,包括元素,属性名,属性值数据类型,嵌套元素等;

【AspectByAnnotationAndXmlDtdMain】

public class AspectByAnnotationAndXmlDtdMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/beans10aspectannotationdtd.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        ((ManNoItfCallTask) proxy).call(BusiMessage.build("任务编号001", "您有待办任务需要处理"));
        System.out.println(proxy.getClass());
    }
}

【打印日志】

stopWatch.start()
人工拨打电话#call(): BusiMessage{msgId='任务编号001', msgText='您有待办任务需要处理'}
stopWatch.stop()
方法执行耗时2.0144192
class com.tom.springnote.common.aop.ManNoItfCallTask$$SpringCGLIB$$0 

【beans10aspectannotationdtd.xml】 ( 虽然只注册了切面和目标对象,我们没有手工织入,但 AnnotationAwareAspectJAutoProxyCreator 实现了自动织入

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator">
        <property name="proxyTargetClass" value="true" />
    </bean>

    <bean id="timeCostAspectByAnnotation" class="com.tom.springnote.chapter10.aspectjannotation.TimeCostAspectByAnnotation" />
    <bean id="target" class="com.tom.springnote.common.aop.ManNoItfCallTask"/>
</beans>


【1.1.2】使用AspectJ注解与xml文件XSD配置实现自动代理织入

1)XSD: xml schema definition, xml文件模式定义;与dtd类似, 用于描述xml文档结构,如文档中出现的元素、文档中出现的属性、子元素、子元素的数量、子元素的顺序、元素是否为空、元素和属性的数据类型、元素或属性的默认和固定值; (用于替换dtd)

  • 基于xml的自动代理实现自动织入的配置方式
    • DTD格式: 注册 AnnotationAwareAspectJAutoProxyCreator,AspectJ注解装配自动代理创建者bean
    • XSD元素格式:使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器;

【AspectByAnnotationAndXmlXsdMain】使用AspectJ注解与xml文件XSD配置实现自动代理织入测试main

public class AspectByAnnotationAndXmlXsdMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/beans10aspectannotationxsd.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        ((ManNoItfCallTask) proxy).call(BusiMessage.build("任务编号001", "您有待办任务需要处理"));
        System.out.println(proxy.getClass());
    }
}

【打印日志】

stopWatch.start()
人工拨打电话#call(): BusiMessage{msgId='任务编号001', msgText='您有待办任务需要处理'}
stopWatch.stop()
方法执行耗时2.0148879
class com.tom.springnote.common.aop.ManNoItfCallTask$$SpringCGLIB$$0

【beans10aspectannotationxsd.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 --> 
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean id="timeCostAspectByAnnotation" class="com.tom.springnote.chapter10.aspectjannotation.TimeCostAspectByAnnotation" />
    <bean id="target" class="com.tom.springnote.common.aop.ManNoItfCallTask"/>
</beans>

2)aop:aspectj-autoproxy 元素定义:( 底层还是使用 AnnotationAwareAspectJAutoProxyCreator自动代理类实现自动织入

<xsd:element name="aspectj-autoproxy">
    <xsd:annotation>
       <xsd:documentation source="java:org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"><![CDATA[
Enables the use of the @AspectJ style of Spring AOP.

See org.springframework.context.annotation.EnableAspectJAutoProxy Javadoc
for information on code-based alternatives to this XML element.
       ]]></xsd:documentation>
    </xsd:annotation>
    <xsd:complexType>
       <xsd:sequence>
          <xsd:element name="include" type="includeType" minOccurs="0" maxOccurs="unbounded">
             <xsd:annotation>
                <xsd:documentation><![CDATA[
Indicates that only @AspectJ beans with names matched by the (regex)
pattern will be considered as defining aspects to use for Spring autoproxying.
                ]]></xsd:documentation>
             </xsd:annotation>
          </xsd:element>
       </xsd:sequence>
       <xsd:attribute name="proxy-target-class" type="xsd:boolean" default="false">
          <xsd:annotation>
             <xsd:documentation><![CDATA[
Are class-based (CGLIB) proxies to be created? By default, standard
Java interface-based proxies are created.
             ]]></xsd:documentation>
          </xsd:annotation>
       </xsd:attribute>
       <xsd:attribute name="expose-proxy" type="xsd:boolean" default="false">
          <xsd:annotation>
             <xsd:documentation><![CDATA[
Indicate that the proxy should be exposed by the AOP framework as a
ThreadLocal for retrieval via the AopContext class. Off by default,
i.e. no guarantees that AopContext access will work.
             ]]></xsd:documentation>
          </xsd:annotation>
       </xsd:attribute>
    </xsd:complexType>
</xsd:element>

【补充】推荐使用spring容器内的自动代理;



【2】 基于AspectJ注解标注pointcut

【2.1】AspectJ类库Pointcut注解的使用

1)@Pointcu注解,用于标注 被@Aspect注解标注的类的方法; 用于描述切点位置表达式;

2)@Pointcut注解中可以引入当前Aspect或者外部Aspect的pointcut ;且可以做逻辑运算;

【AspectJPointcutAnnotation】AspectJ类库的Pointcut注解测试

@Aspect
public class AspectJPointcutAnnotation { 
    // @Pointcut注解的逻辑运算
    @Pointcut("execution(public void *.call(..)) || execution(public void *.call2(..))") // pointcut表达式
    private void callPointcut() {  // pointcut签名
        // pointcut
    }

    // 引用Aspect内部pointcut
    @Pointcut("callPointcut()")
    public void referInnerPointcut() {
        // pointcut
    }

    // 引用Aspect外部pointcut
    @Pointcut("com.tom.springnote.chapter10.aspectjannotation.TimeCostAspectByAnnotation.pointcutName()")
    public void referOuterPointcut() {
        // pointcut
    }

    @Around("referInnerPointcut()")
    public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        try {
            System.out.println("stopWatch.start()");
            stopWatch.start();
            return joinPoint.proceed();
        } catch (Exception e) {
            System.out.println("抛出异常");
            e.printStackTrace();
        } finally {
            System.out.println("stopWatch.stop()");
            stopWatch.stop();
            System.out.printf("方法执行耗时%s\n", stopWatch.getTotalTime(TimeUnit.SECONDS));
        }
        return null;
    }
}

3)@Pointcu注解有2部分组成:

  • pointcut表达式:描述切点位置的表达式;
    • pointcut标志符;如 execution ;within;
    • pointcut表达式匹配模式;如 public void *.call(…)
  • pointcut签名: 声明pointcut名字,以便其他pointcut注解可以引用;


【2.2】AspectJ类库pointcut标志符

1)pointcut表达式的标志符:

  • execution: 匹配指定方法签名的切点;(静态匹配)
  • within: 匹配指定类型所有切点;
  • this和target: this表示匹配目标对象的代理对象; target表示匹配目标对象; 可以使用this(ObjectType)或target(ObjectType),分别匹配类型为ObjectType的目标对象的代理对象或目标对象下所有切点;
  • args: 匹配指定参数类型,指定参数数量的方法级切点; (运行时动态匹配)
  • @within:匹配被指定注解类型标注的类下的所有切点;(静态匹配)
  • @target:匹配被指定注解类型标注的类下的所有切点;(运行时动态匹配)
  • @args:匹配方法参数类型被指定注解类型标注的方法;
  • @annotation:匹配被指定注解类型标注的所有方法切点;(如应用于基于注解的事务管理)

补充: spring aop在原来表达式基础上新增了一个标志符,如 bean(…)



【2.2.1】pointcut表达式标志符-execution

1)execution: 匹配指定方法签名的切点;可以使用通配符 * 与 … ;

2)execution格式: execution([访问类型] 返回类型 [声明类型模式] 名称模式(参数类型模式) [抛出异常模式]) ;其中 方法返回类型, 方法名及参数部分的匹配模式必须指定 ,其他非必输;

// 目标类及方法定义 
public class Foo {
    public void doSomething(String arg) {
        // do nothing.
    }
}

使用pointcut的execution指定匹配表达式,如下:

execution(public void Foo.doSomething(String))

可以简化如下(省略访问类型public, 类型名Foo, 抛出异常模式):

execution(void doSomething(String))

3)使用通配符 * 指定execution标志符:* 可以用于任何部分的匹配模式中; 可以匹配相邻的多个字符,即一个word;使用* 之后,以上 execution可以简化为:(使用* 替代方法方法类型void, 方法名doSomething)

execution(* *(String))

继续简化使用* 替代 方法参数类型String:

execution(* *(*))

4)使用通配符 … (2点)指定execution标志符: 可以在声明类型模式使用,及在方法参数模式使用;如果替换类型模式,可以指定多个层次的类型声明;

execution(void com.tom.spring.*.doSomething(*)) // 匹配 com.tom.spring 包下所有类型的 doSomething方法;(无法匹配com.tom.spring下的子包)
execution(void com.tom.spring..*.doSomething(*)) // 匹配  com.tom.spring 包及其子包下所有类型的 doSomething方法 
execution(void *.doSomething(..)) // .. 用于匹配方法参数,可以匹配0个到多个参数,类型不限制; 

补充:* 用于匹配方法参数,仅可以匹配1个参数;



【2.2.2】pointcut表达式标志符-within

1)within: 匹配指定类型所有切点 ;

within(com.tom.spring.Foo) // 匹配类型
within(com.tom.spring.*) //  匹配com.tom.spring包下所有类型内部的方法级别切点(不匹配com.tom.spring包下的子包)
within(com.tom.spring..*) // 匹配com.tom.spring包及其子包下所有类型内部的方法级别切点 

【2.2.3】pointcut表达式标志符-this和target

1)this和target: this表示匹配目标对象的代理对象; target表示匹配目标对象; 可以使用this(ObjectType)或target(ObjectType),分别匹配类型为ObjectType的目标对象的代理对象或目标对象下所有切点;(一般而言,目标对象与目标对象的代理对象,两者的类型是相同的,因为目标对象与其代理对象实现相同接口,即便使用CGLIB动态代理,代理对象是目标对象的子类;即使用this或者target做匹配,效果是差不多的)

  • 通过 标志符this与target 通常与其他标志符结合使用(通过逻辑运算)

【例】

this(com.tom.spring.Foo) && target(com.tom.spring.Foo) && execution(void doSomething(String))

【2.2.4】pointcut表达式标志符-args

1) args: 匹配指定参数类型,指定参数数量的方法级切点;

2)目标对象类

public class ManNoItfCallTask {
    public void call(BusiMessage message) {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            System.out.println("抛出异常");
            throw new RuntimeException(e);
        }
        System.out.println("人工拨打电话#call(): " + message);
    }
}

3)通过@Pointcut的args指定切点位置

args(com.tom.springnote.common.aop.BusiMessage)

// 使用execution在指定切点位置,可以达到相同效果
execution(* *(com.tom.springnote.common.aop.BusiMessage))   

4)execution与args区别:方法签名为 call(Object object) ,调用方法为 call(BusiMessage msg) ;

  • execution:属于静态pointcut(编译时匹配) ;无法匹配方法调用call(BusiMessage msg) ; 因为方法声明是call(Object object) ;
  • args: 属于动态pointcut(运行时匹配) ; 可以匹配所有参数类型为BusiMessage的方法调用;


【2.2.5】pointcut表达式标志符-@within

1)@within:匹配被指定注解类型标注的类下的所有切点;(静态匹配)

【CustomJoinpointAnnotation】自定义 joinpoint注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CustomJoinpointAnnotation {
}

使用注解@CustomJoinpointAnnotation标注目标类

@CustomJoinpointAnnotation
public class Foo {
    public void doSomething(String arg) {
        // do nothing.
    }
}

@pointcut指定的表达式

@within(CustomJoinpointAnnotation)


【2.2.6】pointcut表达式标志符-@target

1)匹配被指定注解类型标注的类下的所有切点;(运行时动态匹配)

参考 @within ;

【2.2.7】pointcut表达式标志符-@args

1)@args:匹配方法参数类型被指定注解类型标注的方法;

参考 @within ;

【2.2.8】pointcut表达式标志符-@annotation

1)@annotation:匹配被指定注解类型标注的所有方法切点;(如应用于基于注解的事务管理)

参考 @within ;



【2.3】AspectJ类库的Pointcut注解底层原理

1)Aspect类库的Pointcut注解声明后, spring aop内部会通过解析,转化为具体的Pointcut对象,即 AspectJExpressionPointcut 对象,类图如下;

AspectJExpressionPointcut 内部还是通过 ClassFilter 和 MethodMatcher 匹配切点;
在这里插入图片描述



【3】AspectJ类库的Advice通知注解

【3.1】AspectJ的Advice注解

1)背景: 可以通过实现接口,如BeforeAdvice,MethodInterceptor 定义通知类; 为了简化代码,通过通知注解来标注通知对应的方法;

2)AspectJ类库通知注解列表:

  • @Before: 前置通知注解; (标注方法)
  • @AfterReturning: 方法正常返回后置通知注解;(标注方法)
  • @AfterThrowing: 方法异常返回后置通知注解;(标注方法)
  • @After:方法finally后置通知注解(无论方法正常或异常返回,都会执行);(标注方法)
  • @Around: 环绕通知注解;(标注方法)
  • @DeclareParents: 用于Introduction引入型通知的注解; (标注属性)

环绕通知代码示例参见: AspectJPointcutAnnotation ;



【3.1.1】前置与后置通知注解代码示例

【AspectJAdviceAnnotationMain】通知注解实现织入main测试

public class AspectJAdviceAnnotationMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/advice/beans10AspectjAdviceAnnotationXsd.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        // 调用方法 
        BusiMessage busiMessage = ((MessageDAO) proxy).qryMsg("task001");
        System.out.println("main: " + busiMessage);
    }
}

【打印日志】

[@Before]方法执行开始,方法参数=[task001]
[@Before]方法执行开始,方法参数=task001
MessageDAO#qryMsg() 被调用
[@AfterReturning]方法执行结束(成功返回): 方法执行返回值=BusiMessage{msgId='task001', msgText='您有待办任务需要处理'}
[@After]方法执行结束afterFinally
main: BusiMessage{msgId='task001', msgText='您有待办任务需要处理'}

【beans10AspectjAdviceAnnotationXsd.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
	
	<!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter10.aspectjadviceannotation.MethodCallCallLogAdviceAnnotation" />
    <bean id="target" class="com.tom.springnote.chapter10.target.MessageDAO"/>
</beans>

【MethodCallCallLogAdviceAnnotation】 方法调用日志通知注解

@Aspect
public class MethodCallCallLogAdviceAnnotation {

    @Pointcut("execution(* *.qryMsg(..)) && args(String)")
    public void pointcut() {
        // pointcut
    }

    // 带上JoinPoint类型参数,获取方法参数列表(joinPoint必须作为第一个参数)
    // 通过 args(msgId) 把声明pointcut的参数名称绑定到方法调用的参数上
    @Before(value = "args(msgId)")
    public void printLogBefore(JoinPoint joinPoint, String msgId) {
        System.out.println("[@Before]方法执行开始,方法参数=" + Arrays.toString(joinPoint.getArgs()));
        System.out.println("[@Before]方法执行开始,方法参数=" + msgId);
    }

    // 引用定义的pointcut
    // 异常处理通知,获取异常对象e
    @AfterThrowing(pointcut = "pointcut()", throwing = "e")
    public void printLogAfterFinally(RuntimeException e) {
        System.out.println("[@AfterThrowing]异常处理开始");
        e.printStackTrace();
        System.out.println("[@AfterThrowing]异常处理结束");
    } 

    // 通过 returning 属性绑定方法调用返回值
    @AfterReturning(value = "pointcut()", returning = "resultValue")
    public void printLogAfterReturning(BusiMessage resultValue) {
        System.out.printf("[@AfterReturning]方法执行结束(成功返回): 方法执行返回值=%s\n", resultValue);
    }

    // 1. 不使用定义的pointcut,通知注解直接绑定pointcut表达式
    @After("execution(* *.qryMsg(..)) && args(String)")
    public void printLogAfterFinally() {
        System.out.println("[@After]方法执行结束afterFinally");
    }
}

【MessageDAO】目标类

public class MessageDAO {
    public BusiMessage qryMsg(String msgId) {
        System.out.println("MessageDAO#qryMsg() 被调用");
        return BusiMessage.build(msgId, "您有待办任务需要处理");
    }
} 

【小结】 ( 非常重要,这些小技巧

  • 1)如何在通知方法中获取目标方法调用的入参

    • 方法1: 通过 JoinPoint (注意是 org.aspectj.lang.JoinPoint )
    • 方法2:通过 args(argName) 把声明pointcut的参数名称绑定到方法调用的参数上 ; ;
    • 此外: this, target, @within, @target, @annotation, @args 也具备类似功能
  • 2)如何获取目标方法执行返回值:通过 returning把返回值绑定到方法调用参数上;

  • 3)如何获取目标方法抛出的异常: 通过 throwing 绑定

【补充】 @After(finally)注解通常用于处理网络连接的释放,数据库资源的释放



【3.1.2】环绕通知注解代码示例

1)环绕通知的通知或横切逻辑方法的第一个参数类型是 ProceedingJoinPoint ;

  • 环绕通知具体方法内部可以实现偷梁换柱的效果

2)测试场景:为方法调用上下文新增日志;

【AspectJAroundAdviceAnnotationMain】环绕通知测试main

public class AspectJAroundAdviceAnnotationMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/advice/beans10AspectjAroundAdviceAnnotationXsd.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        // 根据任务编号查询待发送消息
        BusiMessage busiMessage = ((MessageDAO) proxy).qryMsg("task001");
        System.out.println("main: " + busiMessage);
    }
}

【打印日志】

[@Around]方法调用开始
[@Around]参数值 = task001
MessageDAO#qryMsg() 被调用
[@Around]方法调用结束
main: BusiMessage{msgId='task001', msgText='您有待办任务需要处理'}

【beans10AspectjAroundAdviceAnnotationXsd.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter10.aspectjadviceannotation.MethodCallCallLogAroundAdviceAnnotation" />
    <bean id="target" class="com.tom.springnote.chapter10.target.MessageDAO"/>
</beans>

【MethodCallCallLogAroundAdviceAnnotation】 环绕通知 (环绕通知对应方法的第一个参数类型是 ProceedingJoinPoint

@Aspect
public class MethodCallCallLogAroundAdviceAnnotation {

    // 环绕通知对应方法的第一个参数类型是 ProceedingJoinPoint
    @Around("execution(* *.qryMsg(..)) && args(msgId)")
    public Object printLogAroundAdvice(ProceedingJoinPoint joinPoint, String msgId) {
        System.out.println("[@Around]方法调用开始");
        System.out.println("[@Around]参数值 = " + msgId);
        try {
//            joinPoint.proceed();  // 可以无参调用
            return joinPoint.proceed(new Object[]{msgId}); // [有参调用] 显然这里可以偷梁换柱
        } catch (Throwable e) {
            throw new RuntimeException(e);
        } finally {
            System.out.println("[@Around]方法调用结束");
        }
    }
}

【注意】joinPoint.proceed(args)有参调用,可以实现偷梁换柱



【3.1.3】 Introduction引入型通知注解代码示例

1)引入型通知使用注解织入:通过 @DeclareParents 注解 标注实例变量;(非引入型通知如Before使用 @Before注解标注方法)

  • 引入型通知回顾: 引入型通知是给目标对象织入新方法,不会修改目标对象已有方法; 织入对象是目标对象,不是目标类;即需要指定被织入的目标对象;
    • 此外:引入型通知(横切逻辑)必须实现接口;

2)引入型通知注解代码示例

【MethodCallLogIntroductionAdviceAnnotationMain】引入型通知测试main

public class MethodCallLogIntroductionAdviceAnnotationMain {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/advice/beans10AspectjIntroductionAdviceAnnotationXsd.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        // 根据任务编号查询待发送消息
        ((MessageDAO) proxy).qryMsg("task001");

        // 转为Introduction通知接口,调用横切逻辑方法
        ((IAccessLog) proxy).sendAccessLog();
    }
}

【打印日志】

MessageDAO#qryMsg() 被调用
AccessLogImpl#sendAccessLog(): 发送访问日志

【beans10AspectjIntroductionAdviceAnnotationXsd.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 --> 
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter10.aspectjadviceannotation.MethodCallLogIntroductionAdviceAnnotation" />
    <bean id="target" class="com.tom.springnote.chapter10.target.MessageDAO"/>
</beans>

【MethodCallLogIntroductionAdviceAnnotation】基于注解@DeclareParents 定义引入型通知

@Aspect
public class MethodCallLogIntroductionAdviceAnnotation {

    @DeclareParents(value="com.tom.springnote.chapter10.target.MessageDAO", defaultImpl = AccessLogImpl.class)
    public IAccessLog accessLog;
}

【IAccessLog】 引入型通知接口

public interface IAccessLog {

    void sendAccessLog();
}

【AccessLogImpl】引入型通知接口实现类

public class AccessLogImpl implements IAccessLog {
    @Override
    public void sendAccessLog() {
        System.out.println("AccessLogImpl#sendAccessLog(): 发送访问日志");
    }
}

3)DeclareParents注解定义

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface DeclareParents {
    String value();  // 目标类全限定类名 (可以通过通配符*指定一批目标对象,如com.tom.spring.target.*) 

    Class defaultImpl() default DeclareParents.class; // 指定织入的横切逻辑接口的实现类class
}


【4】AspectJ类库中Aspect其他方面

【4.1】 advice执行顺序

1) 问题: 一个切点可能匹配多个 advice, 这些advice执行 顺序是什么?

2)解决方法:

  • 如果 Advice定义在同一个Aspect, 根据其定义顺序决定其执行顺序;
  • 如果Advice定义在不同Aspect,这时可以通过实现 Ordered接口; 值越小,优先级越高;越先执行;

3)多个切面实现 Ordered实现有序调用

【MultiAspectOrderMain】 测试main

public class MultiAspectOrderMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/advice/beans10AspectjMultiAspectAnnotationXsd.xml");

        Object proxy = container.getBean("target");
        ((MessageDAO) proxy).qryMsg("task001");
    }
}

【打印日志】

SecondMethodCallLogAspect#requestLog(): 方法被调用
FirstMethodCallLogAspect#requestLog(): 方法被调用
MessageDAO#qryMsg() 被调用

【beans10AspectjMultiAspectAnnotationXsd.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter10.multiaspect.FirstMethodCallLogAspect" />
    <bean class="com.tom.springnote.chapter10.multiaspect.SecondMethodCallLogAspect" />
    <bean id="target" class="com.tom.springnote.chapter10.target.MessageDAO"/>
</beans>

【FirstMethodCallLogAspect】第1个切面 (order为100,值越大 优先级越小,越后执行 )

@Aspect
public class FirstMethodCallLogAspect implements Ordered {

    @Before("execution(* *.qryMsg(..))")
    public void requestLog() {
        System.out.println("FirstMethodCallLogAspect#requestLog(): 方法被调用");
    }

    @Override
    public int getOrder() {
        return 100;
    }
}

【SecondMethodCallLogAspect】第2个切面 (order为0,值越小, 优先级越高,越先执行 )

@Aspect
public class SecondMethodCallLogAspect implements Ordered {

    @Before("execution(* *.qryMsg(..))")
    public void requestLog() {
        System.out.println("SecondMethodCallLogAspect#requestLog(): 方法被调用");
    }

    @Override
    public int getOrder() {
        return 0;
    }
}


【4.2】Aspect实例化模式

1)Aspect类:被Aspect注解标注的类;

2)spring会扫描Aspect类并进行实例化; 与pojo实例化模式有Singleton与prototype类似, aspect类的实例化也有模式(默认 singleton),aspect实例化模式清单如下:perthis, pertarget, percflow, percflowbelow, perwithin ;



【5】基于schema模式的aop

1)背景:spring提倡的容器xml配置方式从 基于 DTD 转向 基于Schema; 因为基于Schema的xml,功能更加丰富与灵活;

【5.1】基于schema的aop配置概览

1)基于schema的aop配置,使用 <aop:config> 元素配置, <aop:config>元素包含 Pointcut, Advisor 及 Aspect共3个子元素;

2)使用 <aop:config> 元素实现aop

【BasedSchemaXmlConfAopMain】基于schema的aop

public class BasedSchemaXmlConfAopMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/basedSchema/beans10AspectjBasedSchema.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        ((MessageDAO) proxy).qryMsg("task001");
    }
}

【打印日志】

MethodLogAroundMethodInterceptorImpl#invoke(): 方法调用前
MessageDAO#qryMsg() 被调用
MethodLogAroundMethodInterceptorImpl#invoke(): 方法调用后

【beans10AspectjBasedSchema.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config proxy-target-class="true">
        <aop:pointcut id="pointcut" expression="execution(* *.qryMsg(..))"/>
        <aop:advisor id="advisor" pointcut-ref="pointcut" advice-ref="methodLogAroundMethodInterceptorImpl" order="1" />
    </aop:config>

    <bean id="methodLogAroundMethodInterceptorImpl"
          class="com.tom.springnote.chapter10.basedschema.advice.MethodLogAroundMethodInterceptorImpl" />
    <bean id="target" class="com.tom.springnote.chapter10.target.MessageDAO"/>

</beans>

【MethodLogAroundMethodInterceptorImpl】环绕通知

public class MethodLogAroundMethodInterceptorImpl implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("MethodLogAroundMethodInterceptorImpl#invoke(): 方法调用前");
        try {
            return invocation.proceed();
        } finally {
            System.out.println("MethodLogAroundMethodInterceptorImpl#invoke(): 方法调用后");
        }
    }
}


【5.2】基于AspectJ注解的aop迁移到基于Schema的aop

1)aspectj通知注解,包括 @Before, @After(finally), @AfterReturning, @AfterThrowing , @Around, @DeclareParents ;

  • 这些Aspectj通知注解,也可以通过基于schema的xml元素来实现;

【5.2.1】前置与后置通知注解迁移到基于schema的通知配置

1)AspectJ通知注解代码,参见 AspectJAdviceAnnotationMain ;

2)基于schema通知配置的aop

【AdviceBasedSchemaXmlConfAopMain02】

public class AdviceBasedSchemaXmlConfAopMain02 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/basedSchema/beans10AspectjAdviceBasedSchema.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        ((MessageDAO) proxy).qryMsg("task001");
    }
}

【打印日志】

[@Before]方法执行开始,方法参数=[task001]
[@Before]方法执行开始,方法参数=task001
[@Around]方法调用开始
[@Around]参数值 = task001
MessageDAO#qryMsg() 被调用
[@Around]方法调用结束
[@After]方法执行结束afterFinally
[@AfterReturning]方法执行结束(成功返回)

【beans10AspectjAdviceBasedSchema.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config proxy-target-class="true">
        <aop:pointcut id="pointcut" expression="execution(* *.qryMsg(..)) and args(String)"/>
        <aop:pointcut id="printLogBefore" expression="args(msgId)"/>
        <aop:aspect id="aspect01" ref="basedSchemaMethodLogAdvice" order="100">
            <aop:before pointcut-ref="pointcut" method="printLogBefore" />
            <aop:after-returning pointcut-ref="pointcut" method="printLogAfterReturning" />
            <aop:after pointcut-ref="pointcut" method="printLogAfterFinally" />
            <aop:after-throwing pointcut-ref="pointcut" method="printLogAfterThrowing" throwing="e" />
            <aop:around pointcut-ref="pointcut" method="printLogAroundAdvice" /> <!-- 配置环绕通知 --> 
        </aop:aspect>
    </aop:config>

    <bean id="target" class="com.tom.springnote.chapter10.target.MessageDAO"/>
    <bean id="basedSchemaMethodLogAdvice" class="com.tom.springnote.chapter10.basedschema.advice.BasedSchemaMethodLogAdvice" />
</beans>

【BasedSchemaMethodLogAdvice】基于schema配置的通知定义POJO

public class BasedSchemaMethodLogAdvice {

    public void printLogBefore(JoinPoint joinPoint, String msgId) {
        System.out.println("[@Before]方法执行开始,方法参数=" + Arrays.toString(joinPoint.getArgs()));
        System.out.println("[@Before]方法执行开始,方法参数=" + msgId);
    }

    public void printLogAfterThrowing(RuntimeException e) {
        System.out.println("[@AfterThrowing]异常处理开始");
        e.printStackTrace();
        System.out.println("[@AfterThrowing]异常处理结束");
    }

    public void printLogAfterReturning() {
        System.out.printf("[@AfterReturning]方法执行结束(成功返回)\n");
    }

    public void printLogAfterFinally() {
        System.out.println("[@After]方法执行结束afterFinally");
    }

    public Object printLogAroundAdvice(ProceedingJoinPoint joinPoint, String msgId) {
        System.out.println("[@Around]方法调用开始");
        System.out.println("[@Around]参数值 = " + msgId);
        try {
//            joinPoint.proceed();  // 可以无参调用
            return joinPoint.proceed(new Object[]{msgId}); // [有参调用] 显然这里可以偷梁换柱
        } catch (Throwable e) {
            throw new RuntimeException(e);
        } finally {
            System.out.println("[@Around]方法调用结束");
        }
    }
}


【5.2.2】环绕通知注解迁移到基于Schema的通知配置

参见 【beans10AspectjAdviceBasedSchema.xml】 ;



【5.2.3】引入型通知迁移到基于Schema的通知配置

【IntroductionAdviceBasedSchemaXmlConfAopMain03】

public class IntroductionAdviceBasedSchemaXmlConfAopMain03 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter10/basedSchema/beans10AspectjIntroductionAdviceBasedSchema.xml");

        // 获取代理对象
        Object proxy = container.getBean("target");
        ((MessageDAO) proxy).qryMsg("task001");

        // 调用引入型通知接口代理对象的方法
        ((IAccessLog) proxy).sendAccessLog();
    }
}

【打印日志】

[@Around]方法调用开始
[@Around]参数值 = task001
MessageDAO#qryMsg() 被调用
[@Around]方法调用结束
AccessLogImpl#sendAccessLog(): 发送访问日志

【beans10AspectjIntroductionAdviceBasedSchema.xml】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config proxy-target-class="true">
        <aop:pointcut id="pointcut" expression="execution(* *.qryMsg(..)) and args(String)"/>
        <!-- 第1个切面 -->
        <aop:aspect id="aspect01" ref="basedSchemaMethodLogAdvice" order="100">
            <aop:around pointcut-ref="pointcut" method="printLogAroundAdvice" />
        </aop:aspect>

        <!-- 第2个切面 -->
        <aop:aspect id="aspect02" ref="basedSchemaMethodLogAdvice" order="1">
            <aop:declare-parents types-matching="com.tom.springnote.chapter10.target.MessageDAO"
                                 implement-interface="com.tom.springnote.chapter10.introductionadviceannotation.IAccessLog"
                                 default-impl="com.tom.springnote.chapter10.introductionadviceannotation.AccessLogImpl" />
        </aop:aspect>
    </aop:config>

    <bean id="target" class="com.tom.springnote.chapter10.target.MessageDAO"/>
    <bean id="basedSchemaMethodLogAdvice" class="com.tom.springnote.chapter10.basedschema.advice.BasedSchemaMethodLogAdvice" />

</beans>

【IAccessLog】引入型通知接口

public interface IAccessLog {

    void sendAccessLog();
}

【AccessLogImpl】 引入型通知接口实现类

public class AccessLogImpl implements IAccessLog {
    @Override
    public void sendAccessLog() {
        System.out.println("AccessLogImpl#sendAccessLog(): 发送访问日志");
    }
}


【6】spring aop织入横切逻辑总结

1)spring aop的3种实现方式:

  • 方式1(spring 1.0版本):基于接口声明Advice通知: 即自定义通知实现接口,如前置通知实现BeforeAdvice接口,环绕通知实现 MethodInterceptor接口; (硬编码实现)
  • 方式2(spring 2.0版本):基于AspectJ类库注解声明Aspect切面与Advice通知;以POJO声明切面与通知,并通过注解标注相应类与方法; ( 墙裂推荐,使用注解简单
  • 方式3(spring 2.0可选版本):在方式2的基础上,基于Schema的xml配置方式说明切面与通知(把通过注解的方式,转为通过schema xml元素配置方式); (不推荐,因为切面,通知,pointcut配置在xml文件,而其具体实现是POJO,代码比较分散,比较复杂)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值