理解面向切面编程

  • 横切关注点:散布在应用中多处的功能(各种方法)

切面能够模块化横切关注点,横切关注点可以被描述为影响应用多处的功能。例如安全就是一个横切关注点,应用中的许多方法都会涉及到安全规则,下图为横切关注点的概念

spring 如何配置两个切面 spring切面使用_spring

常用的重用功能的方法是继承和委托,继承会导致对象体系脆弱,委托可能会导致复杂的调用。将横切关注点模块为特殊的类(切面)可以使每个关注点集中到一个地方,并且可以使服务模块更加简洁。

AOP术语

spring 如何配置两个切面 spring切面使用_spring 如何配置两个切面_02

通知
主要是定义切面以及切面何时使用,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:限定匹配带有指定注解的连接点

编写切点

spring 如何配置两个切面 spring切面使用_spring 如何配置两个切面_03

设置配置的切点仅仅匹配concert包

spring 如何配置两个切面 spring切面使用_AOP_04

只匹配特定的bean

spring 如何配置两个切面 spring切面使用_连接点_05

不匹配特定的bean

spring 如何配置两个切面 spring切面使用_AOP_06

使用注解创建切面

如果一场演出没有观众,则不能称为演出。从演出的角度来看,观众非常重要,但是对于演出本身的功能来讲,它并不是核心,这是一个单独的关注点,因此可以将观众定义为一个切面,并将其应用到演出上
定义演出接口

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为已有对象添加新的方法

spring 如何配置两个切面 spring切面使用_连接点_07

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>