写点什么

《Spring 实战》读书笔记 - 第 4 章 面向切面的 Spring

作者:Java高工P7
  • 2021 年 11 月 11 日
  • 本文字数:7532 字

    阅读完需:约 25 分钟

| execution() | 用于匹配是连接点的执行方法 |


| this() | 限制连接点匹配 AOP 代理的 bean 引用为指定类型的类 |


| target | 限制连接点匹配目标对象为指定类型的类 |


| @target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |


| within() | 限制连接点匹配指定的类型 |


| @within() | 限制连接点匹配指定注解所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里) |


| @annotation | 限制匹配带有指定注解的连接点 |


在 Spring 中尝试使用 AspectJ 其他指示器时,将会抛出 IllegalArgument-Exception 异常。


注意:当我们查看如上所展示的这些 Spring 支持的指示器时,只有 execution 指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。


编写切点


为了阐述 Spring 中的切面,我们定义一个 Performance 接口:


package concert;


public interface Performance {


public void perform() ;


}


假设我们想编写 Performance 的 perform()方法触发的通知,下面展示了切点表达式的写法



使用 AspectJ 切点表达式来选择 Performance 的 perform()方法


现在假设我们需要配置的切点仅匹配 concert 包。我们可以使用 within()指示器来限制匹配,如下图



使用 within 指示器限制切点范围


注意:我们使用了“&&”操作符把 execution()和 within()指示器连接在一起形成与(可以用 and 代替)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||操作符”来标识或(可以用 or 代替)关系,而使用“!”操作符来标识非(可以用 not 代替)操作。


在切点中选择 bean


除了表所列的指示器外,Spring 还引入了一个新的 bean()指示器,它允许我们在切点表达式中使用 bean 的 ID 来标识 bean。bean()使用 beanID 或 bean 名称作为参数限制切点只匹配特定的 bean。


例如,考虑如下的切点:


execution(* concert.Performance.perform())


and bean('woodstock')


还可以使用非操作为除了特定 ID 以外的其他 bean 应用通知:


execution(* concert.Performance.perform())


and !bean('woodstock')


4.3 使用注解创建切面




我们已经定义了 Performance 接口,它是切面中切点的目标对象。现在,让我们使用 AspectJ 注解来定义切面


AspectJ 提供了五个注解来定义通知,如下表所示。


| 注  解 | 通  知 |


| --- | --- |


| @After | 通知方法会在目标方法返回或抛出异常后调用 |


| @AfterReturning | 通知方法会在目标方法返回后调用 |


| @AfterThrowing | 通知方法会在目标方法抛出异常后调用 |


| @Around | 通知方法会将目标方法封装起来 |


| @Before | 通知方法会在目标方法调用之前执行 |


定义切面


我们将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。


下面为 Audience 类的代码


package com.springinaction.perf;


import org.aspectj.lang.ProceedingJoinPoint;


import org.aspectj.lang.annotation.*;


//切面 POJO


@Aspect


public class Audience {


//定义命名的切点


@Pointcut("execution(** com.springinaction.perf.Performance.perform(..))")


public void performance(){


}


//定义通知


@Before("performance()") // 表演之前


public void silenceCellPhones(){


System.out.println("Silencing cell phones");


}


@Before("performance()") // 表演之前


public void takeSeats(){


System.out.println("Taking seats");


}


@AfterReturning("performance()") // 表演之后


public void applause(){


System.out.println("CLAP CLAP CLAP");


}


@AfterThrowing("performance()") // 表演失败之后


public void demandRefund(){


System.out.println("Demanding a refund");


}


@Around("performance()") // 环绕通知方法


public void watchPerformance(ProceedingJoinPoint jp){


try {


System.out.println("Silencing cell phones Again");


System.out.println("Taking seats Again");


jp.proceed();


System.out.println("CLAP CLAP CLAP Again");


}


catch (Throwable e){


System.out.println("Demanding a refund Again");


}


}


}


关于环绕通知,我们首先注意到它接受 ProceedingJoinPoint 作为参数。这个对象是必须要有的,因为要在通知中通过它来调用被通知的方法。


需要注意的是,一般情况下,别忘记调用 proceed()方法。如果不调用,那么通知实际上会阻塞对被通知方法的调用,也许这是所期望的效果。当然,也可以多次调用,比如要实现一个场景是实现重试逻辑。


除了注解和没有实际操作的 performance()方法,Audience 类依然是一个 POJO,可以装配为 Spring 中的 bean


@Bean


public Audience audience(){ // 声明 Audience


return new Audience();


}


除了定义切面外,还需要启动自动代理,才能使这些注解解析。


如果使用 JavaConfig 的话,需要如下配置


package com.springinaction.perf;


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 AppConfig {


@Bean


public Audience audience(){ // 声明 Audience


return new Audience();


}


}


假如在 Spring 中要使用 XML 来装配 bean 的话,那么需要使用 Spring aop 命名空间中的<aop:aspectj-autoproxy>元素。


<?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"


xmlns:context="http://www.springframework.org/schema/context"


xsi:schemaLocation="http://www.springframework.org/schema/aop


http://www.springframework.org/schema/aop/spring-aop.xsd


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">


<context:component-scan base-package="com.springinaction.perf" />


<aop:aspectj-autoproxy />


<bean id="audience" class="com.springinaction.perf.Audience" />


</beans>


无论使用 JavaConfig 还是 XML,Aspect 自动代理都会使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。这种情况下,将会为 Concert 的 bean 创建一个代理,Audience 类中的通知方法将会在 perform()调用前后执行。


我们需要记住的是,Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。本质上,它依然是 Spring 基于代理的切面。


处理通知中的参数


目前为止,除了环绕通知,其他通知都没有参数。如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?


为了阐述这个问题,我们来重新看一下 BlankDisc 样例。假设你想记录每个磁道被播放的次数。为了记录次数,我们创建了 TrackCounter 类,它是通知 playTrack()方法的一个切面。


package com.springinaction.disc;


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<>();


@Pointcut("execution(* com.springinaction.disc.CompactDisc.playTrack(int))" +


"&& args(trackNumber)") // 通知 playTrack()方法


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 类型参数也会传递到通知中去。trackNumber 也与切点方法签名中的参数相匹配。切点定义中的参数与切点方法中的参数名称是一样的。


下面我们启动 AspectJ 自动代理以及定义 bean


package com.springinaction.disc;


import org.springframework.context.annotation.Bean;


import org.springframework.context.annotation.Configuration;


import org.springframework.context.annotation.EnableAspectJAutoProxy;


import java.util.ArrayList;


import java.util.List;


@Configuration


@Enabl


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


eAspectJAutoProxy


public class TrackCounterConfig {


@Bean


public CompactDisc sgtPeppers(){


BlankDisc cd = new BlankDisc();


cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");


cd.setArtist("The Beatles");


List<String> tracks = new ArrayList<>();


tracks.add("Sgt. Pepper's Lonely Hearts Club Band");


tracks.add("With a Little Help from My Friends");


tracks.add("Luck in the Sky with Diamonds");


tracks.add("Getting Better");


tracks.add("Fixing a Hole");


tracks.add("Feel My Heart");


tracks.add("L O V E");


cd.setTracks(tracks);


return cd;


}


@Bean


public TrackCounter trackCounter(){


return new TrackCounter();


}


}


最后的简单测试


package com.springinaction;


import static org.junit.Assert.*;


import com.springinaction.disc.CompactDisc;


import com.springinaction.disc.TrackCounter;


import com.springinaction.disc.TrackCounterConfig;


import org.junit.Rule;


import org.junit.Test;


import org.junit.runner.RunWith;


import org.junit.contrib.java.lang.system.StandardOutputStreamLog;


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 = TrackCounterConfig.class)


public class TrackCounterTest {


@Rule


public final StandardOutputStreamLog log = new StandardOutputStreamLog();


@Autowired


private CompactDisc cd;


@Autowired


private TrackCounter counter;


@Test


public void testTrackCounter(){


cd.playTrack(1);


cd.playTrack(2);


cd.playTrack(3);


cd.playTrack(3);


cd.playTrack(3);


cd.playTrack(3);


cd.playTrack(7);


cd.playTrack(7);


assertEquals(1,counter.getPlayCount(1));


assertEquals(1,counter.getPlayCount(2));


assertEquals(4,counter.getPlayCount(3));


assertEquals(0,counter.getPlayCount(4));


assertEquals(0,counter.getPlayCount(5));


assertEquals(0,counter.getPlayCount(6));


assertEquals(2,counter.getPlayCount(7));


}


}


通过注解引入新功能


我们除了给已有的方法添加新功能外,还可以添加一些额外的功能。


回顾一下,在 Spring 中,切面只是实现了它们所包装 bean 相同的接口代理。如果除了实现这些接口,代理也能暴露新接口。即便底层实现类并没有实现这些接口,切面所通知的 bean 也能实现新的接口。下图展示了它们是如何工作的。



使用 Spring AOP,我们可以为 bean 引入新的方法。代理拦截调用并委托给实现该方法的其他对象


需要注意的是,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个 bean 的实现被拆分到了多个类中。


为了验证能行得通,我们为所有的 Performance 实现引入 Encoreable 接口


package com.springinaction.perf;


public interface Encoreable {


void performEncore();


}


借助于 AOP,我们创建一个新的切面


package com.springinaction.perf;


import org.aspectj.lang.annotation.Aspect;


import org.aspectj.lang.annotation.DeclareParents;


@Aspect


public class EncoreableIntroducer { // 需要给 Performance 和其实现类额外添加方法的实现


@DeclareParents(value = "com.springinaction.perf.Performance+",


defaultImpl = DefaultEncoreable.class)


public static Encoreable encoreable;


}


其中 @DeclareParents 注解,将 Encoreable 接口引入到 Performance bean 中。


@DeclareParents 注解有三个部门组成:


  • value 属性指定了哪种类型的 bean 要引入该接口。(本例中,就是 Performance,加号表示 Performance 的所有子类型)

  • defaultImpl 属性指定了为引入功能提供实现的类。

  • @DeclareParents 注解所标注的静态属性指明了要引入的接口。


然后需要在配置中声明 EncoreableIntroducer 的 bean


@Bean


public EncoreableIntroducer encoreableIntroducer(){


return new EncoreableIntroducer();


}


当调用委托给被代理的 bean 或被引入的实现,取决于调用的方法属性被代理的 bean 还是属性被引入的接口。


在 Spring 中,注解和自动代理提供了一种很便利的方式来创建切面。但有一个劣势:必须能够为通知类添加注解,要有源码。


如果没有源码或者不想注解到你的代码中,能可选择 Spring XML 配置文件中声明切面。


4.4 在 XML 中声明切面




如果声明切面,但不能为通知类添加注解时,需要转向 XML 配置了。


在 Spring 的 aop 命名空间中,提供了多个元素用来在 XML 中声明切面,如下表所示


| AOP 配置元素 | 用途 |


| --- | --- |


| <aop:advisor> | 定义 AOP 通知器 |


| <aop:after> | 定义 AOP 后置通知(不管被通知的方法是否执行成功) |


| <aop:after-returning> | 定义 AOP 返回通知 |


| <aop:after-throwing> | 定义 AOP 异常通知 |


| <aop:around> | 定义 AOP 环绕通知 |


| <aop:aspect> | 定义一个切面 |


| <aop:aspectj-autoproxy> | 启用 @AspectJ 注解驱动的切面 |


| <aop:before> | 定义 AOP 前置通知 |


| <aop:config> | 顶层的 AOP 配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内 |


| <aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |


| <aop:pointcut> | 定义一个切点 |


我们重新看一下 Audience 类,这一次我们将它所有的 AspectJ 注解全部移除掉:


package com.springinaction.perf;


public class Audience {


public void silenceCellPhones(){


System.out.println("Silencing cell phones");


}


public void takeSeats(){


System.out.println("Taking seats");


}


public void applause(){


System.out.println("CLAP CLAP CLAP");


}


public void demandRefund(){


System.out.println("Demanding a refund");


}


public void watchPerformance(ProceedingJoinPoint jp){


try {


System.out.println("Silencing cell phones Again");


System.out.println("Taking seats Again");


jp.proceed();


System.out.println("CLAP CLAP CLAP Again");


}


catch (Throwable e){


System.out.println("Demanding a refund Again");


}


}


}


声明前置、后置以及环绕通知


下面展示了所需要的 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:aop="http://www.springframework.org/schema/aop"


xsi:schemaLocation="http://www.springframework.org/schema/aop


http://www.springframework.org/schema/aop/spring-aop-3.2.xsd


http://www.springframework.org/schema/beans


http://www.springframework.org/schema/beans/spring-beans.xsd">


<bean id="audience" class="com.springinaction.perf.Audience" />


<bean id="performance" class="com.springinaction.perf.Concert"/>


aop:config


<aop:aspect ref="audience">


<aop:pointcut id="perf" expression="execution(* com.springinaction.perf.Performance.perform(..))" />


<aop:before pointcut-ref="perf" method="silenceCellPhones" />


<aop:before pointcut-ref="perf" method="takeSeats" />


<aop:after-returning pointcut-ref="perf" method="applause" />


<aop:after-throwing pointcut-ref="perf" method="demandRefund"/>


<aop:around pointcut-ref="perf" method="watchPerformance"/>


</aop:aspect>


</aop:config>


</beans>


为通知传递参数


我们使用 XML 来配置 BlankDisc。


首先,移除掉 TrackCounter 上所有的 @AspectJ 注解。


package com.springinaction.disc;


import java.util.HashMap;


import java.util.Map;


public class TrackCounter {


private Map<Integer, Integer> trackCounts = new HashMap<>();


// 在播放前,为该磁道计数


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 配置为参数化的切面


<?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/aop


http://www.springframework.org/schema/aop/spring-aop-3.2.xsd


http://www.springframework.org/schema/beans


http://www.springframework.org/schema/beans/spring-beans.xsd">


<bean id="trackCounter" class="com.springinaction.disc.TrackCounter" />


<bean id="cd" class="com.springinaction.disc.BlankDisc" >


<property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />


<property name="artist" value="The Beatles" />


<property name="tracks">


<list>


<value>Sgt. Pepper's Lonely Hearts Club Band</value>


<value>With a Little Help from My Friends</value>


<value>Lucy in the Sky with Diamonds</value>


<value>Getting Better</value>


<value>Fixing a Hole</value>


<value>Feel My Heart</value>


<value>L O V E</value>


</list>


</property>


</bean>


aop:config


<aop:aspect ref="trackCounter">


<aop:pointcut id="trackPlayed" expression="


execution(* com.springinaction.disc.CompactDisc.playTrack(int))


and args(trackNumber)" />


<aop:before pointcut-ref="trackPlayed" method="countTrack"/>


</aop:aspect>


</aop:config>


</beans>

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
《Spring实战》读书笔记-第4章 面向切面的Spring