文章目录
【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,代码比较分散,比较复杂)