《Spring实战》学习笔记(4)——面向切面的Spring

这章学习Spring第二个重要的知识点:AOP。


本章知识点:

  • 面向切面编程的基本原理
  • 通过POJO创建切面
  • 使用@AspectJ注解
  • 为AspectJ切面注入依赖
一、什么是面向切面编程

  切面能帮助我们模块化横切关注点(影响应用的多处功能)。使用面向切面编程时,可以通过声明的方式定义通过功能以何种方式,在何处调用,且无需修改受影响的类。

1. AOP术语
  • 横切关注点:影响应用的多处功能。
  • 切面:横切关注点可以被模块化为特殊的类,这些类叫切面。切面是通知和切点的集合,定义了切面它是什么,何时,何处完成其功能。
  • 通知:切面的工作。
  • 连接点:Spring应用中有许多时机去使用通知,这些时机成为连接点。连接点是应用执行过程中能插入切面的一个点。
  • 切点:影响应用的多处功能。
  • 引入:向现有的类添加新方法或属性。
  • 织入:把切面应用到目标对象并创建新的代理对象的过程。

切面可以使用五种类型的通知:

  • 前置通知(Before):在目标方法被调用前调用通知。
  • 后置通知(After):在目标方法被调用后调用通知。
  • 返回通知(After-returning):在目标方法成功执行后调用通知。
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹被通知的方法,在被通知的方法调用前和调用后执行自定义行为。
2. Spring对AOP的支持

  Spring提供了4中类型的AOP支持:

  • 基于代理的经典Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面

关于AOP框架的一些知识:

Spring通知是Java编写的。

Spring在运行时通知对象。

通过在代理类中包裹切面,Spring在运行期间把切面织入到Spring管理的bean中。

Spring只支持方法级别的连接点。

二、通过切点来选择连接点

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

1. 编写切点

  我们依旧使用之前的例子,借口Hero,killEnemy方法在英雄击杀敌人时调用。

public interface Hero {
    void killEnemy();
}

下面为一个切点表达式

execution(* chapterfour.hero.Hero.killEnemy(..))
  • execution():在方法执行时触发
  • *:返回任意类型
  • chapterfour.hero.Hero:方法所属的类
  • killEnemy:触发通知的方法
  • (..)中的‘..’:使用任意参数

如果我们想配置的切点仅匹配chapterfour包,可用within()指示器限制匹配:

execution(* chapterfour.hero.Hero.killEnemy(..)) && within(chapterfour.*)

类似的,我们也可以用“||”,“!” 这样的逻辑运算符。

2. 在切点中选择bean
execution(* chapterfour.hero.Hero.killEnemy(..)) && bean('adHero')

我们使用bean('beanID')这种方式来限制切点只匹配特定的bean。

三、使用注解创建切面
1. 定义切面

我们定义一场比赛新闻的切面:

@Aspect
public class GameBroadcast {
    @Before("execution(* chapterfour.hero.Hero.killEnemy(..))")
    public void lockEnemy(){
        System.out.println("英雄锁定目标敌人");
    }

    @Before("execution(* chapterfour.hero.Hero.killEnemy(..))")
    public void beforeKill(){
        System.out.println("英雄击杀敌人前");
    }

    @AfterReturning("execution(* chapterfour.hero.Hero.killEnemy(..))")
    public void afterKill(){
        System.out.println("英雄击杀敌人后");
    }

    @AfterThrowing("execution(* chapterfour.hero.Hero.killEnemy(..))")
    public void heroDead(){
        System.out.println("英雄阵亡");
    }
}

使用@Aspect注解,表明该类是一个切面。

有5个注解来定义通知:

我们可以看到,上面例子中,同样的切点表达式,我们重复了四次,有没有办法只定义一次然后重复利用呢?

@Aspect
public class GameBroadcast {
    @Pointcut("execution(* chapterfour.hero.Hero.killEnemy(..))")
    public void broadcast() {
        
    }
    
    @Before("broadcast()")
    public void lockEnemy() {
        System.out.println("英雄锁定目标敌人");
    }

    @Before("broadcast()")
    public void beforeKill() {
        System.out.println("英雄击杀敌人前");
    }

    @AfterReturning("broadcast()")
    public void afterKill() {
        System.out.println("英雄击杀敌人后");
    }

    @AfterThrowing("broadcast()")
    public void heroDead() {
        System.out.println("英雄阵亡");
    }
}

在方法上使用@Pointcut注解,参数为切点表达式。之后便可以将相同的切点表达式替换为方法名。broadcast()方法本身内容应该为空,因为它的内容不重要,只是一个标识而已。

GameBroadcast类是一个切面,而且他依旧是一个POJO,可以想其他Java类一样,调用它的方法,也可以作为bean进行装配。

上面例子只是定义了切面,但就像之前学习自动扫描与自动装配时,@Component与@ComponentScan要搭配使用一样。在这里,只有@Aspect注解,Spring是不会认为这是切面的,还需要我们去启动切面自动代理。

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class HeroConfig {
   @Bean
    public GameBroadcast gameBroadcast(){
        return new GameBroadcast();
    }
}

使用@EnableAspectJAutoProxy注解启用AspecJ自动代理。

在XML中的写法:

<context:component-scan base-package="chapterfour" />
<aop:aspectJ-autoproxy />
<bean class="chapterfour.hero.GameBroadcast" />

AspectJ自动代理会为使用@Aspect注解的bean创建一个代理。

测试一下:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(value="chapterfour")
public class HeroConfig {
    @Bean
    public GameBroadcast gameBroadcast(){
        return new GameBroadcast();
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HeroConfig.class)
public class HeroTest {
    @Autowired
    private Hero adHero;

    @Autowired
    private GameBroadcast gameBroadcast;

    @Test
    public void testAOP() {
        adHero.killEnemy();
    }
}

其中,GameBroadcast 也可以使用自动装配的方式,将HeroConfig改为:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(value="chapterfour")
public class HeroConfig {
}

然后在切面上加上@Component注解。两种装配方式都可以,但一定要为切面创建bean,否则切面则不会生效。

输出结果:

英雄击杀敌人前

英雄锁定目标敌人

炮车兵被击杀

英雄击杀敌人后

2. 使用环绕通知

我们将上一小节的例子改为环绕通知:

@Aspect
public class GameBroadcast {
    @Pointcut("execution(* chapterfour.hero.Hero.killEnemy(..))")
    public void broadcast() {

    }

    @Around("broadcast()")
    public void aroundGame(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("英雄击杀敌人前");
            System.out.println("英雄锁定目标敌人");
            joinPoint.proceed();
            System.out.println("英雄击杀敌人后");
        } catch (Throwable e) {
            System.out.println("英雄阵亡");
        }
    }
}

@Around注解表明aroundGame方法会作为broadcast()切点的环绕通知。

我们在aroundGame中接收了ProceedingJoinPoint类型的参数,这个对象是必须的,因为我们需要在环绕通知中去调用被通知的方法。利用环绕通知,我们将之前四个通知合成了一个。

注意:如果我们不调用proceed()方法的话,那么这个通知就会阻塞被通知的方法的调用。

3. 处理通知中的参数

  前几个例子中的切面都很简单,没有参数。如果切面通知的方法中有参数怎么办呢?

我们为Hero接口增加一个购买装备的方法:

public interface Hero {
    void killEnemy();

    void buy(String equip);
}

ADHero中实现buy()方法:

@Component
public class ADHero implements Hero {
    @Autowired
    private Enemy enemy;

    private List<String> equipList = new ArrayList<>();

    public void killEnemy() {
        enemy.dead();
    }

    @Override
    public void buy(String equip) {
        this.equipList.add(equip);
    }
}

接着我们修改切面:

@Aspect
public class GameBroadcast {

    @Pointcut("execution(* chapterfour.hero.Hero.buy(String)) && args(equip)")
    public void broadcast(String equip) {

    }

    @AfterReturning("broadcast(equip)")
    public void afterBuy(String equip) {
        System.out.println("英雄购买了装备:"+ equip);
    }
}

测试一下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HeroConfig.class)
public class HeroTest {
    @Autowired
    private Hero adHero;

    @Autowired
    private GameBroadcast gameBroadcast;

    @Test
    public void testAOP() {
        adHero.buy("无尽之刃");
    }
}

输出:

英雄购买了装备:无尽之刃

分析一下表达式:

execution(* chapterfour.hero.Hero.buy(String)) && args(equip)
  • *:返回任意类型
  • chapterfour.hero.Hero:方法所在类
  • buy:方法
  • String:方法接收String类型的参数
  • args(equip):指定参数,与buy方法中的参数相匹配
4. 通过注解引入新功能

我们增加一个技能接口,接口中有回城方法:

public interface Skill {
    void backCity();
}

它的实现类如下:

public class SkillImpl implements Skill{
    @Override
    public void backCity() {
        System.out.println("英雄回城了");
    }
}

我们想要Hero有回城的能力,但又不想让Hero实现Skill接口,有没有其他方法呢?我们可以使用AOP去为bean引入新的方法。

创建一个新的切面:

@Aspect
public class SkillIntorducer {
    @DeclareParents(value = "chapterfour.hero.Hero+",defaultImpl = SkillImpl.class)
    public static Skill skill;
}

这个切面不同于之前的切面,有各种通知,这个切面的作用是将Skill接口引入到Hero的bean中。

分析一下 @DeclareParents(value = "chapterfour.hero.Hero+",defaultImpl = SkillImpl.class):

  • value:指定为哪种类型的bean引入新接口。“+”代表是Hero的所有子类型,而不是Hero本身。
  • defaultImpl:指定需要引入的新接口的实现类。
  • @DeclareParents:该注解标注的静态属性就是要引入的接口。

与之前定义切面一样,我们也需要为SkillIntorducer这个切面声明为一个bean:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(value="chapterfour")
public class HeroConfig {
    @Bean
    public GameBroadcast gameBroadcast(){
        return new GameBroadcast();
    }

    @Bean
    public SkillIntorducer skillIntorducer(){
        return new SkillIntorducer();
    }
}

测试一下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HeroConfig.class)
public class HeroTest {
    @Autowired
    private Hero adHero;

    @Autowired
    private GameBroadcast gameBroadcast;

    @Test
    public void testAOP() {
        adHero.buy("无尽之刃");
        Skill adHero = (Skill) this.adHero;
        adHero.backCity();
    }
}

输出结果:

英雄购买了装备:无尽之刃

英雄回城了

注意:我们虽然为Hero引入了接口Skill,但我们调用Skill接口的backCity方法前,需要将adHero转换为Skill类型以绕过语法检查。

四、在XML中声明切面

  我们的原则是基于注解的配置优于基于Java的配置,基于Java的配置优于基于XML的配置。但有时不能为通知添加注解,就只能使用XML配置了。

1. 声明前置通知和后置通知

我们将最开始的切面例子上的注解全部拿掉:

public class GameBroadcast {

    public void lockEnemy() {
        System.out.println("英雄锁定目标敌人");
    }

    
    public void beforeKill() {
        System.out.println("英雄击杀敌人前");
    }

   
    public void afterKill() {
        System.out.println("英雄击杀敌人后");
    }

    
    public void heroDead() {
        System.out.println("英雄阵亡");
    }

}

现在它变成了一个简单的Java类,但是它依然具有成为AOP通知的潜质,只需要在XML中进行一些配置。

<bean id="gameBroadcast" class="chapterfour.hero.GameBroadcast"/>

    <aop:config>
        <!--   引用gameBroadcast bean     -->
        <aop:aspect ref="gameBroadcast">
            <aop:before pointcut="execution(* chapterfour.hero.Hero.killEnemy(..))" method="beforeKill"/>
            <aop:before pointcut="execution(* chapterfour.hero.Hero.killEnemy(..))" method="lockEnemy"/>
            <aop:after-returning pointcut="execution(* chapterfour.hero.Hero.killEnemy(..))" method="afterKill"/>
            <aop:after-throwing pointcut="execution(* chapterfour.hero.Hero.killEnemy(..))" method="heroDead"/>
        </aop:aspect>
    </aop:config>

这段代码和使用注解方式实现的功能是一样的,看元素的名称就可以理解各个通知的功能,不再加以解释。

2. 声明环绕通知
public class GameBroadcast {
    public void aroundGame(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("英雄击杀敌人前");
            System.out.println("英雄锁定目标敌人");
            joinPoint.proceed();
            System.out.println("英雄击杀敌人后");
        } catch (Throwable e) {
            System.out.println("英雄阵亡");
        }
    }
}
 <bean id="gameBroadcast" class="chapterfour.hero.GameBroadcast"/>

    <aop:config>
        <!--   引用gameBroadcast bean     -->
        <aop:aspect ref="gameBroadcast">
            <aop:pointcut id="killPoint" expression="execution(* chapterfour.hero.Hero.killEnemy(..))"/>
            <aop:around pointcut-ref="killPoint" method="aroundGame"/>
        </aop:aspect>
    </aop:config>
3. 为通知传递参数
public class GameBroadcast {
    public void afterBuy(String equip) {
        System.out.println("英雄购买了装备:"+ equip);
    }
}
<bean id="gameBroadcast" class="chapterfour.hero.GameBroadcast"/>

    <aop:config>
        <!--   引用gameBroadcast bean     -->
        <aop:aspect ref="gameBroadcast">
            <aop:pointcut id="broadcast" expression="execution(* chapterfour.hero.Hero.buy(String)) and args(equip)"/>
            <aop:after-returning pointcut-ref="broadcast" method="afterBuy"/>
        </aop:aspect>
    </aop:config>
4. 通过切面引入新的功能
 <bean id="gameBroadcast" class="chapterfour.hero.GameBroadcast"/>
 <bean id="skillIntorducer" class="chapterfour.hero.SkillIntorducer"/>
   
    <aop:config>
        <aop:aspect>
            <aop:declare-parents types-matching="chapterfour.hero.Hero+" implement-interface="chapterfour.hero.Skill" default-impl="chapterfour.hero.SkillImpl" />
        </aop:aspect>
    </aop:config>
五、注入AspectJ切面

  创建一个评论员切面(创建Aspect文件):

public aspect Commentator {

    pointcut commentKill(): execution(* chapterfour.hero.Hero.killEnemy(..));

    after() returning: commentKill(){
        System.out.println("一次完美的击杀");
    }
}

通过aspectOf()工厂方法获得切面的引用:

<bean class="chapterfour.hero.Commentator" factory-method="aspectOf" />
posted @ 2020-08-30 17:28  当代艺术家  阅读(173)  评论(0)    收藏  举报