理解面向切面编程
- 横切关注点:散布在应用中多处的功能(各种方法)
切面能够模块化横切关注点,横切关注点可以被描述为影响应用多处的功能。例如安全就是一个横切关注点,应用中的许多方法都会涉及到安全规则,下图为横切关注点的概念
常用的重用功能的方法是继承和委托,继承会导致对象体系脆弱,委托可能会导致复杂的调用。将横切关注点模块为特殊的类(切面)可以使每个关注点集中到一个地方,并且可以使服务模块更加简洁。
AOP术语
通知
主要是定义切面以及切面何时使用,Spring切面有五种类型的通知
1、前置通知:在目标方法被调用之前调用通知功能
2、后置通知:在目标方法被调用之后调用通知功能,不关心方法的输出是什么
3、返回通知:在目标方法成功执行之后调用通知
4、异常通知:在目标方法抛出异常后调用通知
5、环绕通知:通知包裹了被通知的的方法,在被通知的方法调用之前和调用之后执行自定义的行为
连接点
表示能够插入切面的一个点
切点
切面插入的一个具体的位置,通知主要是定义了切面的“什么”和“何时”,而切点则定义了“何处”
切面
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能
引入
引入允许我梦向现有类添加新方法或属性
织入
把切面应用到目标对象并创建新的代理对象的过程。切脉你在指定的连接点被织入到目标对象中。目标对象的生命周期里有多个点可以进行织入:
1、编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ的织入编译器就是以种种方式织入切面的
2、类加载期:切面在目标类加载到JVM时被织入,这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入就支持这种方式织入切面
3、运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象,Spring AOP就是这种方式织入切面的
Spring对AOP的支持
创建切点来定义切面所织入的连接点是AOP的基本功能,Spring提供了如下四种类型的AOP支持
- 基于代理的经典Spring AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring任何版本)
通过切点来选择连接点
切点用于准确定位应该在什么地方应用切面的通知,Spring仅支持AspectJ切点指示器的一个子集,以下是Spring AOP支持的AspectJ切点指示器。
-
arg()
:限制连接点匹配参数为指点类型的执行方法 -
@args()
:限制连接点匹配参数有指定注解标注的执行方法 -
execution()
:用于匹配时连接点的执行方法 -
this()
:限制连接点匹配AOP代理的bean引用为指定类型的类 -
target
:限制连接点匹配目标对象为指定类型的类 -
@target()
:限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 -
within()
:限制连接点匹配指定的类型 -
@within()
:限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) -
@annotation
:限定匹配带有指定注解的连接点
编写切点
设置配置的切点仅仅匹配concert包
只匹配特定的bean
不匹配特定的bean
使用注解创建切面
如果一场演出没有观众,则不能称为演出。从演出的角度来看,观众非常重要,但是对于演出本身的功能来讲,它并不是核心,这是一个单独的关注点,因此可以将观众定义为一个切面,并将其应用到演出上
定义演出接口
package com.ruian;
// 演出的一个接口
public interface Performance {
public void perform();
}
//创建实现类
package com.ruian;
import org.springframework.stereotype.Component;
@Component//定义该类为一个bean
public class Performanceimpl implements Performance {
@Override
public void perform() {
System.out.println("演出进行中");
}
}
将观众类定义为一个切面
package com.ruian;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
//将观众类定义为一个切面
@Aspect
public class Audience {
@Before("execution(* com.ruian.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("手机静音");
}
@Before("execution(* com.ruian.Performance.perform(..))")
public void takeSeats(){
System.out.println("坐下");
}
@AfterReturning("execution(* com.ruian.Performance.perform(..))")
public void applause(){
System.out.println("鼓掌");
}
@AfterThrowing("execution(* com.ruian.Performanceimpl.perform(..))")
public void demandRefund(){
System.out.println("退票");
}
}
创建Spring配置类
package com.ruian;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan
@EnableAspectJAutoProxy//启用AspectJ自动代理
public class JavaConfig {
//将切面注册为bean
@Bean
public Audience audience(){
return new Audience();
}
}
在xml文件中需要使用<aop:aspectj-autoproxy>
启用AspectJ自动代理。
进行测试
package com.ruian;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class SpringTest {
@Autowired
Performance performance;
@Test
public void test(){
performance.perform();
}
}
使用@Pointcut可以在@AspectJ切面中定义一个可重用的切点
package com.ruian;
import org.aspectj.lang.annotation.*;
//将观众类定义为一个切面
@Aspect
public class Audience {
//定义一个可重用的切点
@Pointcut("execution(* com.ruian.Performance.perform(..))")
public void performance(){ };
@Before("performance()")
public void silenceCellPhones(){
System.out.println("手机静音");
}
@Before("performance()")
public void takeSeats(){
System.out.println("坐下");
}
}
Spring中由五个注解用来定义通知
-
@After
:通知方法会在目标方法返回或抛出异常后调用 -
@AfterReturning
:通知方法会在目标方法返回后调用 -
@AfterThrowing
:通知方法会在目标方法抛出异常后调用 -
@Around
:通知方法会将目标方法封装 -
@Before
:通知方法会在目标方法调用之前执行
创建环绕通知
package com.ruian;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
//将观众类定义为一个切面
@Aspect
public class Audience {
@Pointcut("execution(* com.ruian.Performance.perform(..))")
public void performance() {
}
//创建环绕通知
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("手机静音");
System.out.println("坐下");
jp.proceed();//需要捕获异常
System.out.println("鼓掌");
} catch (Throwable e) {
System.out.println("退票");
}
}
}
其中jp.proceed()用来将控制权交给被通知的方法,然后调用被通知的方法,如果不使用jp.proceed()方法会阻塞被通知方法的调用
处理通知中的参数
package com.ruian.soundsystem;
public interface CompactDisc {
void play();
void playTrack(int trackNumber);
}
//实现类
package com.ruian.soundsystem;
import java.util.List;
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public BlankDisc(String title, String artist){
this.title = title;
this.artist = artist;
}
public void setTracks(List<String> tracks){
this.tracks = tracks;
}
@Override
public void play() {
System.out.println("Playing " + title + " by " + artist);
}
@Override
public void playTrack(int trackNumber) {
System.out.println("trackNumber =" + trackNumber);
}
}
TrackCounter类
package com.ruian.soundsystem;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.util.HashMap;
import java.util.Map;
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<>();
//args(tracknumber)限定符,传递给playTrack()的参数也会传递给通知方法
@Pointcut("execution(* com.ruian.soundsystem.CompactDisc.playTrack(int))" + "&& args(tracknumber)")
public void trackPlayer(int tracknumber){}
@Before("trackPlayer(trackNumber)")
public void countTrank(int trackNumber){
int currentCount = getPlayCount(trackNumber);
}
public int getPlayCount(int trackNumber) {
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
配置类
package com.ruian.soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class JavaConfig {
@Bean
public CompactDisc sgtPeppers() {
String title = "Sgt. Pepper's Lonely Hearts Club Band";
String artist = "The Beatles";
BlankDisc cd = new BlankDisc(title, artist);
List<String> tracks = new ArrayList<String>();
tracks.add("Sgt");
tracks.add("With");
tracks.add("Lucy");
tracks.add("Getting");
tracks.add("Fixing");
cd.setTracks(tracks);
return cd;
}
@Bean
public TrackCounter trackCounter() {
return new TrackCounter();
}
}
测试类
package com.ruian.soundsystem;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class SpringTest {
@Rule
public final StandardOutputStreamLog log = new StandardOutputStreamLog();
@Autowired
private CompactDisc cd;
@Autowired
// TrackCounter类型的属性,带有@Autowired,以便于将TrackCounter bean注入到测试代码的counter成员变量之中
private TrackCounter counter;
@Test
public void testTrackCounter() {
// 播放一些磁道
cd.playTrack(1);
cd.playTrack(2);
cd.playTrack(2);
// 断言期望的数量
assertEquals(1, counter.getPlayCount(1));
assertEquals(2, counter.getPlayCount(2));
}
}
同注解引入新功能
利用AOP为已有对象添加新的方法
package com.ruian;
import com.ruian.Encoreable;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value = "com.ruian.Performanceimpl + ",defaultImpl =DefaultEncoreable.class)
public static Encoreable encoreable;
}
利用XML配置文件声明切面
Spring中xml中常用的AOP 元素
-
<aop:advisor>
:定义AOP通知器 -
<aop:after>
:定义AOP后置通知(不管被通知的方法是否执行成功) -
<aop:after-returning>
:定义AOP返回通知 -
<aop:after-throwing>
:定义AOP异常通知 -
<aop:around>
:定义AOP环绕通知 -
<aop:aspect>
:定义一个切面 -
<aop:aspect-autoproxy>
:启用@AspectJ注解驱动的切面 -
<aop:before>
:定义一个AOP前置通知 -
<aop:config>
:顶层的AOP配置元素。大多数的<aop:*>
元素必须包含在<aop:config>
元素内 -
<aop:declare-parents>
:以透明的方式为被通知的对象引入新的接口 -
<aop:pointcut>
:定义一个切点
<?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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="audience" class="com.ruian.Audience"/>
<bean id = "performacn" class="com.ruian.Performanceimpl"/>
<aop:config>
<!-- 定义切面 -->
<aop:aspect ref="audience">
<!-- 配置通知 -->
<aop:before pointcut="execution(* com.ruian.Performance.perform(..))" method="silenceCellPhones"/>
<aop:after pointcut="execution(* com.ruian.Performance.perform(..))" method="applause"/>
</aop:aspect>
</aop:config>
<!-- 定义可重用的切点-->
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expression="execution(* com.ruian.Performance.perform(..))"/>
<aop:before method="takeSeats" pointcut-ref="performance"/>
</aop:aspect>
</aop:config>
<!-- 环绕通知 -->
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expression="execution(* com.ruian.Performance.perform(..))"/>
<aop:around method="watchPerformance" pointcut-ref="performance"/>
</aop:aspect>
</aop:config>
<!--引入新功能-->
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="com.ruian.Performance+" implement-interface="com.ruian.Encoreable"
default-impl="com.ruian.DefaultEncoreable"/>
</aop:aspect>
</aop:config>
</beans>