什么是面向切面编程
在软件开发中,散布于应用中多处的功能(比如日志、声明式事务、安全和缓存)被称为横切关注点(crosscutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
下图直观的呈现横切关注点的概念:
上图展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全和事务管理。
如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。
切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。
这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。
AOP术语
描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。下图展示了这些概念是如何关联在一起的。
通知(Advice)
在AOP术语中,切面的工作被称为通知。
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?
Spring切面可以应用5种类型的通知:
前置通知(Before):在目标方法被调用之前调用通知功能;
后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
返回通知(After-returning):在目标方法成功执行之后调用通知;
异常通知(After-throwing):在目标方法抛出异常后调用通知;
环绕通知(Around):通知包裹了被通知的方法,在被通知的方
法调用之前和调用之后执行自定义的行为。
连接点(Join point)
程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为“连接点”。Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入通知。连接点由两个信息确定:第一是用方法表示的程序执行点;第二是用相对点表示的方位。
切点(Poincut)
如果说通知定义了切面的“what”和“when”的话,那么切点就定义了“where”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。如果把连接点当做数据库中的记录,那么切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。
切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。
引入(Introduction)
引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简单,只需一个方法,setLastModified(Date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入:
编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-timeweaving,LTW)就支持以这种方式织入切面。
运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring对AOP的支持
并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能。
Spring和AspectJ项目之间有大量的协作,而且Spring对AOP的支持在很多方面借鉴了AspectJ项目。
Spring提供了4种类型的AOP支持:
基于代理的经典Spring AOP;
纯POJO切面;
@AspectJ注解驱动的切面;
注入式AspectJ切面(适用于Spring各版本)
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种AOP风格的好处在于能够不使用XML来完成功能。
如果你的AOP需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用AspectJ来实现切面。在这种情况下,上文所示的第四种类型能够帮助你将值注入到AspectJ驱动的切面中。
Spring通知是Java编写的
Spring所创建的通知都是用标准的Java类编写的。这样的话,我们就可以使用与普通Java开发一样的集成开发环境(IDE)来开发切面。而且,定义通知所应用的切点通常会使用注解或在Spring配置文件里采用XML来编写,这两种语法对于Java开发者来说都是相当熟悉的。
AspectJ与之相反。虽然AspectJ现在支持基于注解的切面,但AspectJ最初是以Java语言扩展的方式实现的。这种方式有优点也有缺点。通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集,但是我们需要额外学习新的工具和语法。
Spring在运行时通知对象
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如下图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
Spring只支持方法级别的连接点
通过使用各种AOP方案可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。
Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。
编写切点
在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。
Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。下表列出了Spring AOP所支持的AspectJ切点指示器。
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。
当我们查看如上所展示的这些Spring支持的指示器时,注意只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。
为了阐述Spring中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个Performance接口:
package com.angeilz.aop.concert;
public interface Performance {
void perform();
}
编写切点表达式:
execution(* com.angeilz.aop.concert.Performance.perform(..))
execution()指示器选择Performance的perform()方法。
方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。
然后指定了全限定类名和方法名。
对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。
within()指示器来限制匹配:
execution(* com.angeilz.aop.concert.Performance.perform(..))
&& within(com.angeilz.aop.concert.*)
该表达式所配置的切点仅匹配concert包。
请注意我们使用了“&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。
因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代替“||”和“!”。
在切点中选择bean
我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为audience。
execution(* com.angeilz.aop.concert.Performance.perform(..))
&& bean(audience)
在某些场景下,限定切点为指定的bean或许很有意义,但我们还可以使用非操作为除了特定ID以外的其他bean应用通知:
execution(* com.angeilz.aop.concert.Performance.perform(..))
&& !bean(audience)
定义切面
定义一个观看演出的切面
package com.angeilz.aop.concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect//所标注的类是一个切面
public class Audience {
//@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点
@Pointcut("execution(* com.angeilz.aop.concert.Performance.perform(..))")
public void perform() {
}
//定义前置通知
@Before("perform()")
public void silenceCellPhone() {
System.out.println("silence cell phone");
}
//定义前置通知
@Before("perform()")
public void takeSeats() {
System.out.println("Taking seats");
}
//定义后置通知(目标方法返回后调用)
@AfterReturning("perform()")//
public void applause() {
System.out.println("CLAP! CLAP! CLAP! CLAP! ");
}
//定义后置通知(目标方法抛出异常后调用)
@AfterThrowing("perform()")
public void demandRefund() {
System.out.println("Demanding a refund");
}
}
Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。
AspectJ提供了五个注解来定义通知:
注解 | 通知 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
需要注意的是,除了注解和没有实际操作的performance()方法,Audience类依然是一个POJO。我们能够像使用其他的Java类那样调用它的方法,它的方法也能够独立地进行单元测试,这与其他的Java类并没有什么区别。Audience只是一个Java类,只不过它通过注解表明会作为切面使用而已。
像其他的Java类一样,它可以装配为Spring中的bean:
@Bean()
public Audience audience() {
return new Audience();
}
在JavaConfig中启用AspectJ注解的自动代理
package com.angeilz.aop.concert;
import org.springframework.context.annotation.*;
@Configuration
@EnableAspectJAutoProxy//启用AspectJ自动代理
@ComponentScan
public class ConcertConfig {
//Audience bean
@Bean()
public Audience audience() {
return new Audience();
}
}
编写测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ConcertConfig.class})
public class ConcertTest {
@Autowired
Performance performance;
@Test
public void perform(){
performance.perform();
}
}
Performance 的实现类Movie
@Component
public class Movie implements Performance {
@Override
public void perform() {
System.out.println("show a movie");
}
}
运行结果:
silence cell phone
Taking seats
show a movie
CLAP! CLAP! CLAP! CLAP!
在XML中,通过Spring的aop命名空间启用AspectJ自动代理:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<!--启用AspectJ 自动代理-->
<aop:aspectj-autoproxy/>
<!--声明Audience bean-->
<bean id="audience" class="com.angeilz.aop.concert.Audience"/>
<!--构建切面-->
<aop:config>
<aop:aspect ref="audience">
<!--定义一个切点-->
<aop:pointcut id="perform" expression="execution(* com.angeilz.aop.concert.Performance.perform())"/>
<!--定义前置通知-->
<aop:before method="silenceCellPhone" pointcut-ref="perform"/>
<!--定义前置通知-->
<aop:before method="takeSeats" pointcut-ref="perform"/>
<!--定义一个后置通知(方法返回后调用)-->
<aop:after-returning method="applause" pointcut-ref="perform"/>
<!--定义一个后置通知(方法抛异常后调用)-->
<aop:after-throwing method="demandRefund" pointcut-ref="perform"/>
<!--定义一个环绕通知-->
<!--<aop:around method="watchPerformance" pointcut-ref="perform"/>-->
</aop:aspect>
</aop:config>
</beans>
创建环绕通知
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
使用环绕通知重新实现Audience切面:
package com.angeilz.aop.concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class Audience {
@Pointcut("execution(* com.angeilz.aop.concert.Performance.perform(..))")
public void perform() {
}
//定义环绕通知
@Around("perform()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("silence cell phone");
System.out.println("Taking seats");
jp.proceed();
System.out.println("CLAP! CLAP! CLAP! CLAP! ");
} catch (Throwable throwable) {
System.out.println("Demanding a refund");
}
}
}
几点说明:
这个通知所达到的效果与之前的前置通知和后置通知是一样的。
注意ProceedingJoinPoint参数,这个对象是必须要有的,应为你必须通过调用ProceedingJoinPoint的proceed()方法来调用目标方法。
别忘记调用proceed()方法。如果不调这个方法的话,目标方法得不到调用。
你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。
在XML中声明环绕通知
<!--构建切面-->
<aop:config>
<aop:aspect ref="audience">
<!--定义一个切点-->
<aop:pointcut id="perform" expression="execution(* com.angeilz.aop.concert.Performance.perform())"/>
<!--定义一个环绕通知-->
<!--<aop:around method="watchPerformance" pointcut-ref="perform"/>-->
</aop:aspect>
</aop:config>
处理通知中的参数
如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?为了阐述这问题,我构建了下面这个例子:
MusicBox 目标类,有一个play方法,用来打印成员变量songList中的歌曲名。
package com.angeilz.aop.musicsystem;
import java.util.List;
public class MusicBox {
//歌曲名称列表
private List<String> songList;
public MusicBox(List<String> songList) {
this.songList = songList;
}
public void play(int index) {
System.out.print("歌曲:[" + songList.get(index) + "] ");
}
}
SongCounter类,用来定义切面,count方法用来声明后置通知,打印歌曲序号与播放次数;
@Aspect
@Component
public class SongCounter {
private Map<Integer, Integer> counter = new HashMap<>();
@Pointcut("execution(* com.angeilz.aop.musicsystem.MusicBox.play(int)) && args(index)")
public void play(int index) {
}
@After("play(index)")
public void count(int index) {
counter.put(index, counter.get(index) == null ? 1 : counter.get(index) + 1);
System.out.println("序号" + index + ",播放次数" + counter.get(index));
}
}
需要关注的是切点表达式中的args(int)限定符。它表明传递给playTrack()方法的int类型参数也会传递到通知中去。参数的名称index也与切点方法签名中的参数相匹配。
定义配置类MusicConfig,并在配置类中注入MusicBox bean
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class MusicConfig {
//注入MusicBox bean 并初始化
@Bean
public MusicBox musicBox() {
String[] songs = {"你给我听好", "晴天", "千里之外", "童话镇", "岁月神偷"};
List<String> list = new ArrayList<>();
Collections.addAll(list, songs);
return new MusicBox(list);
}
}
编写测试类MusicTest,playTest方法用来随机播放musicBox中的歌曲;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MusicConfig.class)
public class MusicTest {
@Autowired
MusicBox box;
@Test
public void playTest(){
Random random=new Random(System.nanoTime());
for (int i = 0; i < 10; i++) {
box.play(random.nextInt(5));
}
}
}
运行结果
歌曲:[千里之外] 序号2,播放次数1
歌曲:[晴天] 序号1,播放次数1
歌曲:[晴天] 序号1,播放次数2
歌曲:[你给我听好] 序号0,播放次数1
歌曲:[童话镇] 序号3,播放次数1
歌曲:[千里之外] 序号2,播放次数2
歌曲:[岁月神偷] 序号4,播放次数1
歌曲:[你给我听好] 序号0,播放次数2
歌曲:[你给我听好] 序号0,播放次数3
歌曲:[童话镇] 序号3,播放次数2
XMl中配置参数化的切面
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<!--声明SongCounter bean-->
<bean id="songCounter" class="com.angeilz.aop.musicsystem.SongCounter"/>
<!--声明MusicBox bean-->
<bean id="musicBox" class="com.angeilz.aop.musicsystem.MusicBox">
<!--通过构造器注入值-->
<constructor-arg name="songList">
<list>
<value>千里之外</value>
<value>晴天</value>
<value>岁月神偷</value>
<value>通话镇</value>
<value>你给我听好</value>
</list>
</constructor-arg>
</bean>
<!--启动AspectJ自动代理-->
<aop:aspectj-autoproxy/>
<aop:config>
<!--定义切面-->
<aop:aspect ref="songCounter">
<!--定义切点-->
<aop:pointcut id="play"
expression="execution(* com.angeilz.aop.musicsystem.MusicBox.play(int)) and args(index)"/>
<!--声明后置通知-->
<aop:after method="count" pointcut-ref="play"/>
</aop:aspect>
</aop:config>
</beans>
可以看到,我们使用了和前面相同的aop命名空间XML元素,它们会将POJO声明为切面。唯一明显的差别在于切点表达式中包含了一个参数,这个参数会传递到通知方法中。如果你将这个表达式与程序清单4.6中的表达式进行对比会发现它们几乎是相同的。唯一的差别在于这里使用and关键字而不是“&&”(因为在XML中,“&”符号会被解析为实体的开始)。
通过切面引入新功能
一些编程语言,例如Ruby和Groovy,有开放类的理念。它们可以不用直接修改对象或类的定义就能够为对象或类增加新的方法。不过,Java并不是动态语言。一旦类编译完成了,我们就很难再为该类添加新的功能了。
实际上,利用被称为引入的AOP概念,切面可以为Spring bean添加新方法。
看下面的例子
禽类接口,有个fly方法
public interface Bird {
void fly();
}
燕子类实现Bird接口,重写fly方法
@Component
public class Swallow implements Bird {
@Override
public void fly() {
System.out.println("swallow can fly");
}
}
人类接口,有个walk方法
public interface Human {
void walk();
}
男人实现人类接口,重写walk方法
@Component
public class Man implements Human {
@Override
public void walk() {
System.out.println("man can walk");
}
}
定义一个切面
@Aspect
@Component
public class BirdIntroducer {
@DeclareParents(value = "com.angeilz.aop.animal.Human+",
defaultImpl = Swallow.class)
public Bird bird;
}
可以看到,BirdIntroducer切面。与我们之前所创建的切面不同,它并没有提供前置、后置或环绕通知,而是通过@DeclareParents注解,将Bird 接口引入到Human bean中。
@DeclareParents注解由三部分组成:
value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Human的类型。(标记符后面的加号表示是Human的所有子类型,而不是Human本身。)
defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的是Swallow提供实现。
@DeclareParents注解所标注的成员变量指明了要引入了接口。在这里,我们所引入的是Bird 接口。
构建配置类AnimalConfig
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AnimalConfig {}
测试类AnimalTest
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AnimalConfig.class)
public class AnimalTest {
@Autowired
Human human;
@Test
public void fly(){
Bird bird=(Bird)human;
bird.fly();
}
}
运行结果
swallow can fly
如何工作的
在Spring中,切面只是实现了它们所包装bean相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?那样的话,切面所通知的bean看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。下图展示了它们是如何工作的。
我们需要注意的是,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个bean的实现被拆分到了多个类中。
XML的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="com.angeilz.aop.animal.Human+"
implement-interface="com.angeilz.aop.animal.Bird"
default-impl="com.angeilz.aop.animal.Swallow"/>
</aop:aspect>
</aop:config>
</beans>