《Spring实战》学习笔记-------AOP切面

AOP的实现

AOP实现有两种方式:JDKProxy和CGlib(Code generate library)

  • 如果目标类是接口则使用JDKproxy,否则用后者
  • JDKProxy的核心:InvocationHandler接口和Proxy类。使用java反射机制实现
  • Cglib:以继承的方式动态生成目标类的代理。

1. AOP术语

通知(advice)

通知:即切面所要完成的工作,和什么时候执行这个工作

Spring切面有5种类型的通知:

  • 前置通知:在目标方法被调用前调用通知功能
  • 后置通知:在目标方法完成后调用通知功能
  • 返回通知:在目标方法成功执行后调用通知
  • 异常通知:在目标方法抛出异常后调用通知功能
  • 环绕通知:通知包裹了被通知的方法,在被通知的方法调用前和调用后执行自定义的行为。

连接点

应用有很多时机应用通知,这些时机被称为连接点。连接点是应用执行过程种插入切面的一个点。这个点可以是调用方法时、抛出异常时、或者修改一个字段时。切面代码利用这些点插入到应用的流程中去。(感觉和通知的when差不多)

切点

通知定义了切面的“什么”和“何时”,切点定义了“何处”。切点会匹配所要织入的一个或多个连接点。

切面

通知和切点定义了切面的全部内容。

织入

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。

在目标的生命周期中有多个点可以进行织入:

  • 编译期:切面在目标编译时被织入。需要特殊的编译器。AspectJ的编译器是以这种方式。
  • 类加载期:切面在目标类加载到JVM时被织入。需要特殊的类加载器。AspectJ5的加载时织入支持。
  • 运行期:切面在应用运行的某个时刻被织入。Spring AOP是以这种方式。

2. Spring对AOP的支持

Spring提供4种类型的AOP支持:

  • 基于代理的经典SpringAOP(被淘汰)
  • 纯POJO切面(需要使用XML)
  • @AspectJ注解驱动的切面(不需要使用XML)
  • 注入式AspectJ切面(适用于Spring各版本)

前三种是Spring AOP实现的变体,Spring AOP建立在动态代理的基础之上,所以Spring对AOP的支持局限于方法拦截。

Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑(并不代表是前置通知)。
在这里插入图片描述

3. 编写切点

在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。Spring支持的AspectJ切点指示器有:

  • arg() : 限制连接点匹配参数为指定类型
  • @args() : 限制连接点匹配参数由指定注解标注
  • execution() : 用于匹配连接点
  • this() :限制连接点匹配AOP代理的bean引用为指定类型
  • target:限制连接点匹配目标对象为指定类型
  • @target():限制连接点匹配特定的注解的执行对象
  • within():限制连接点匹配指定的类型
  • @within():限制连接点匹配指定注解所标注的类型
  • @annotation:限制匹配带有指定注解的连接点

假设有一个Performance接口:

package concert;

public interface Performance{
	public void perform();
}

现在要编写Performance的perform()方法触发的通知:

execution(* concert.Performance.perform(..))

使用execution()指示器选择方法。“*”代表返回任意类型都可以,然后指定全限定类名方法名,对于参数列表使用(…)表示切点要选择任意的perform()方法,无论该方法入参是什么。

可以进一步限制匹配,配置的切点仅匹配concert包时:

execution(* concert.Performance.perform(..)) && within(concert.*)

还可以限制切点只匹配特定的bean时起作用,这需要指明bean的id:

execution(* concert.Performance.perform(..)) and bean("beanID")
execution(* concert.Performance.perform(..)) and !bean("beanID") //切面通知织入到所有id不为beanID的bean中

4. 编写切面

使用Aspect注解来定义切面:

package concert

@Aspect
public class Audience{
	
	@Before("execution(** concert.Performance.perform(..))")//表演之前
	public void silenceCellPhone(){...}//关机

	@Before("execution(** concert.Performance.perform(..))")//表演之前
	public void takeSeats(){...}//落座

	@AfterReturning("execution(** concert.Performance.perform(..))")//表演之后
	public void applause(){...}//鼓掌

	@AfterThrowing("execution(** concert.Performance.perform(..))")//表演失败之后
	public void demandRefund(){...}//退票
}
  • @Aspect:这是一个切面
  • @Before:通知在方法调用前执行
  • @After:通知在方法调用后执行
  • @AfterReturning:通知在方法返回后执行
  • @AfterThrowing:通知在方法抛出异常后执行

可以对上面的代码进行优化:

package concert

@Aspect
public class Audience{
	
	@PointCut("execution(** concert.Performance.perform(..))")//定义命名的切点
	public void performance(){}//这个方法只是一个标识。
	
	@Before("performance()")//表演之前
	public void silenceCellPhone(){...}//关机

	@Before("performance()")//表演之前
	public void takeSeats(){...}//落座

	@AfterReturning("performance()")//表演之后
	public void applause(){...}//鼓掌

	@AfterThrowing("performance()")//表演失败之后
	public void demandRefund(){...}//退票
}

Audience类同时也是一个Bean,在配置类中启用自动代理功能以启用这个切面:

@Configuration
@EnableAspectJAutoProxy//启用AspectJ自动代理
@ComponentScan
public class ConcertConfig{

	@Bean
	public Audience audience(){
		return new Audience();
	}
}

XML中:

<context:component-scan base-package="concert" />
<aop:aspectj-autoproxy /> //启用代理
<bean class="concert.Audience" /> //声明bean

5. 创建环绕通知

package concert

@Aspect
public class Audience{
	
	@PointCut("execution(** concert.Performance.perform(..))")
	public void performance(){}

	@Around("performance()")//环绕通知方法
	public void watchPerformance(ProceedingJoinPoint jp){
		try{
			System.out.println("开始进行目标方法执行前的操作");
			jp.proceed();//被通知方法执行
			System.out.println("开始进行目标方法执行后的操作");
		} catch (Throwable e){
			System.out.println("目标方法抛出异常时执行的操作");
		}
	}
}
  • 在环绕通知中 ProceedingJoinPoint 必须作为参数传入
  • ProceedingJoinPoint 的 proceed()方法调用被通知方法
  • 如果不执行 proceed() 方法会对被通知方法进行阻塞
  • 可以多次执行 proceed() 方法来多次调用目标方法

6. 处理通知中的参数

这段代码的上下文背景是:soundsystem包下CompactDisc类里有一个tracks数组,playTrack(int count) 方法会执行对应数组下标的track。现在的需求是:记录每个track被调用的次数

@Aspect
public class TrackCounter{
	
	private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();

	@PointCut("execution(** soundsystem.CompactDisc.playTrack(int))" + "&& args(trackNumber)")
	public void trackPlayed(int trackNumber){}

	@Before("trackPlayed(trackNumber)")//播放前计数加一
	public void countTrack(int trackNumber){
		int currentCount = getPlayCount(trackNumber);
		trackCounts.put(trackNumber, currentCount+1);
	}

	public int getPlayCount(int trackNumber){
		return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
	}
}

这个切点声明了要给通知方法的参数,args(trackNumber)表明传递给playTrack()方法的int类型参数也会传递到通知方法中去。countTrack(int trackNumber)与args中的参数名称相匹配。这样就完成了从命名切点到通知方法的参数转移。

7. 通过注解引入新功能

在Spring中,切面只是实现了它所包装的bean相同接口的代理。同时它也可以暴露新的接口,即使底层被代理的bean没有实现这个接口。

例如:为Peformance引入Encore接口以及它的实现类:

package concert;

public interface Encore(){
	public void performEncore();
}

实现类:
package concert;
@Component
public class DefaultEncore implements Encore(){
	@Overwrite
	public void performEncore(){
		System.out.println("默认返场");
	}
}

创建一个新的切面:

package concert;

@Aspect
@Component
public class EncoreIntroducer{
	@DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncore.class)
	public static Encore encore;
}

@DeclareParents注解由三部分组成:

  • value 属性指定了哪种类型的bean要引入接口。本例中代表所有实现了Performance的类型(加号表示performance所有的子类)
  • defaultImpl 属性指定了为引入功能提供实现的类。你想添加的功能的实现。
  • @DeclareParents 标注的静态属性指明了要引入的接口,你想添加的功能。在这里引入的是Encore接口。

将bean注入Spring容器并开启代理:

package concert;

@Configuration
@ComponentScan
@EnableAspectAutoProxy
public class AnnotationConfig{}

当Spring发现一个bean使用了@Aspect注解,Spring就会创建一个代理,然后调用委托给被代理的bean或被引入的实现。

面向注解的切面声明有一个弊端:必须能够为通知类添加注解,必须要有通知类的源码。

8. 在XML中声明切面

如果需要声明切面,但又不能为通知类添加注解,那么就要使用XML配置了。

AOP配置元素用途
aop:config顶层的AOP配置元素
aop:advisor定义AOP通知器
aop:before定义AOP前置通知
aop:after定义AOP后置通知
aop:after-returning定义AOP返回通知
aop:after-throwing定义AOP异常通知
aop:around定义AOP环绕通知
aop:aspect定义一个切面
aop:aspectj-autoproxy启用@AspectJ注解驱动的切面
aop:declare-parents以透明的方式为被通知的对象引入额外的接口
aop:pointcut定义一个切点

将之前Audience类的注解全部拿掉:

package concert

public class Audience{
	
	public void silenceCellPhone(){...}//关机

	public void takeSeats(){...}//落座

	public void applause(){...}//鼓掌

	public void demandRefund(){...}//退票
}

8.1 声明前置和后置通知

<aop:config>
	<aop:aspect ref="audience"  <!-- 引用audience Bean作为切面--!>
		<aop:before    
			pointcut="execution(** concert.Performance.perform(..))"
			method="silenceCellPhone" />    <!-- perform方法调用前执行--!>
		<aop:before
			pointcut="execution(** concert.Performance.perform(..))"
			method="takeSeats" />
		<aop:after-returning
			pointcut="execution(** concert.Performance.perform(..))"
			method="applause" />
		<aop:after-throwing
			pointcut="execution(** concert.Performance.perform(..))"
			method="demandRefund" />
	</aop:aspect>
</aop:config>

使用aop:pointcut标签:

<aop:config>
	<aop:aspect ref="audience"  <!-- 引用audience Bean作为切面--!>
		<aop:pointcut id="performance" 
					  expression="execution(** concert.Performance.perform(..))" />
		<aop:before    
			pointcut-ref="performance"   
			method="silenceCellPhone" />    <!-- perform方法调用前执行--!>
		<aop:before
			pointcut-ref="performance" 
			method="takeSeats" />
		<aop:after-returning
			pointcut-ref="performance" 
			method="applause" />
		<aop:after-throwing
			pointcut-ref="performance" 
			method="demandRefund" />
	</aop:aspect>
</aop:config>

aop:pointcut标签在 aop:aspect 标签内外均可,只是作用域的不同。

8.2 声明环绕通知

Audience类:

package concert;
public class Audience{
	public void watchPerformance(ProceedingJoinPoint jp){
		try{
			System.out.println("开始进行目标方法执行前的操作");
			jp.proceed();//被通知方法执行
			System.out.println("开始进行目标方法执行后的操作");
		} catch (Throwable e){
			System.out.println("目标方法抛出异常时执行的操作");
		}
	}
}

在XML中调用aop:around元素

<aop:config>
	<aop:aspect ref="audience">
		<aop:pointcut id="performance"
					  expression="execution(** concert.Performance.perform(..))" />
		<aop:around 
			pointcut-ref="performance"
			method="watchPerformance" />
	</aop:aspect>
</aop:config>

8.3 为通知传递参数

移除TrackCounter上的@AspectJ注解

package soundsystem;
public class TrackCounter{
	
	private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();

	public void countTrack(int trackNumber){
		int currentCount = getPlayCount(trackNumber);
		trackCounts.put(trackNumber, currentCount+1);
	}

	public int getPlayCount(int trackNumber){
		return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
	}
}

XML中将TrackCounter配置为切面:

<beans>
	<bean id="trackCounter" class="soundsystem.TrackCounter" />

	<aop:config>
		<aop:aspect ref="trackCounter">
			<aop:pointcut id="trackPlayed"
						  expression="execution(** soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)" /> <!--切点为另一个bean中的playTrack(trackNumber)方法 -->
			<aop:before
				pointcut-ref="trackPlayed"
				method="countTrack" />
		</aop:aspect>
	</aop:config>
</beans>

XML中&符号会被解析为实体的开始,这里使用and替代&&。

8.4 通过切面引入新功能

<aop:aspect ref="beanId">
	<aop:declare-parents
		types-matching="concert.Performance+"	<!-- 类型匹配为Performance接口的bean -->
		implement-interface="concert.Encore"	<!-- 要增加的接口 -->
		default-impl="concert.DefaultEncore" />  <!-- 加入的接口的方法实现 -->
</aop:aspect>

除了使用default-impl还可以使用delegate-ref来指明具体的实现:

<aop:declare-parents
		types-matching="concert.Performance+"
		implement-interface="concert.Encore"	
		delegate-ref="encoreDelegate" /> 
这种方法需要存在一个id为encoreDelegate的bean
<bean id="encoreDelegate" class="concert.DefaultEncore" />

这种方法的好处是这个bean本身可以被注入、通知。

9. 注入AspectJ切面

AspectJ切面与Spring是相互独立的,它可以织入到任意的Java应用中。同时也可以借助Spring的依赖注入把bean装配到Aspect切面中。需要单独学习AspectJ语法。

例如:

package concert;
public aspect CriticAspect{
	public CriticAspect(){}

	pointcut performance() : execution(** perform(..));

	afterReturning() : performance(){
		Systemout.println("执行目标方法结束后的操作");
	}

	private CriticismEngine criticismEngine;//这是一个接口,要对CriticismEngine进行注入

	public void setCriticismEngine(CriticismEngine criticismEngine){ 
		this.criticismEngine = criticismEngine;
	}
}

为AspectJ切面使用Spring依赖注入时,要先将这个切面设置为bean。然后再将 CriticismEngineImpl注入到切面当中:

<bean class="concert.CriticAspect" factory-method="aspectOf" >
	  <property name="criticismEngineImpl" ref="criticismEngineImpl" /> <!-- 假设criticismEngineImpl已经设置为bean -->
</bean>

这里要使用factory-method属性调用aspectOf方法,因为AspectJ切面是在AspectJ运行期创建的,等到Spring给CriticAspect注入CriticismEngine时,CriticAspect已经被实例化了。要先通过asspectOf()方法获取切面的引用,再执行依赖注入。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值