面向切面的Spring

2019-08-09 00:00:00 spring 切面 面向

本篇内容说说Spring对切面的支持,如何把普通类声明为一个切面,以及如何使用注解创建切面,主要有以下几点内容:

  • 什么是面向切面编程
  • 选择连接点
  • 使用注解创建切面
  • 在XML中声明切面

什么是面向切面编程

切面能帮助模块化横切关注点,横切关注点可以被描述为影响应用多处的功能。如图,直观呈现横切关注点的概念:

                              《面向切面的Spring》  

 

 

上图展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全、事务管理、监控日志等。

使用面向切面编程时,在一个地方定义通用功能,可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面。这样做有两个好处:首先,每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点的代码,而次要关注点的代码被转移到切面中。

定义AOP术语

描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point),这几个概念之间的关联如图所示:

                                    《面向切面的Spring》

 

通知(Advice)

 通知定义了切面是什么以及何时使用。Spring切面可应用的通知有5种类型:

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

连接点(Join point)

连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

切点(Pointcut)

切点的定义会匹配通知所要织入的一个或多个连接点。通常是使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

切面(Aspect)

切面是通知和切点的结合,通知和切点共同定义了切面的全部内容 —— 是什么,在何时、何处完成其功能。

引入(Introduction)

引入允许我们向现有的类添加新方法和属性。

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

Spring对AOP的支持

不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分,有些允许在字段修饰符级别应用通知,有些只支持方法调用相关的连接点。它们织入切面的方式和时机也有所不同。总的来说,创建切点来定义切面所织入的连接点是AOP框架的基本功能。

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

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

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

 Spring的经典AOP编程模型曾经非常棒,但现在Spring提供了更简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP,Spring经典的AOP看超来显得非常笨重和过于复杂,使用ProxyFactory Bean会让人感到繁琐。所以这里只写简单的声明式AOP和基于注解的AOP。

借助Spring的aop命名空间,可以将纯POJO转换为切面。这些POJO只是提供了满足切点条件时所要调用的方法,它需要XML配置。

Spring借鉴了AspectJ的切面,提供注解驱动的AOP。它不需要XML配置,它虽然是基于代理的AOP,但编程模型与AspectJ注解切面几乎一致。

选择连接点

Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。Spring AOP支持的AspectJ切点指示器如下表:

AspectJ指示器

描述

arg()

限制连接点匹配参数为指定类型的执行方法

@args()

限制连接点匹配参数由指定注解标注的执行方法

execution()

用于匹配是连接点的执行方法

this()

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

target()

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

@target()

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

within()

限制连接点匹配指定的类型

@within()

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

@annotation

限定匹配带有指定注解的连接点

只有execution指示器是实际执行匹配的,而其它的指示器都是用来限制匹配的。execution指示器是编写切点定义时使用最多的指示器。

编写切点

定义Performance接口作为切面的切点:

package concert;

public interface Performance {

     public void perform();

}

现在展示一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用:

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

使用execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明不关心方法返回值的类型。还指定了全限定类名和方法名,对于方法参数列表,使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。

假设需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配:

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

在切点中选择bean

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

execution(* concert.Performance.perform(..)) and bean(‘dancer’)

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

execution(* concert.Performance.perform(..)) and !bean(‘dancer’)

 

使用注解创建切面

Aspect提供了五个注解来定义通知:

  • @Before: 通知方法会在目标方法调用之前执行
  • @After: 通知方法会在目标方法返回或抛出异常后调用
  • @AfterReturning: 通知方法会在目标方法返回后调用
  • @Around: 通知方法会将目标方法封装起来
  • @AfterThrowing: 通知方法会在目标方法抛出异常后调用

示例代码

package asp;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class RecordAspect {

    @Pointcut("execution(*  asp.P30Phone.call(..))")
    public void excute() {
    }

    @Before("excute()")
    public void before(JoinPoint joinPoint) {
        // 打印请求入参
        System.out.println("parameter:" + JSONObject.toJSONString(joinPoint.getArgs()));
    }

    @Around("excute()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        long endTime = System.currentTimeMillis();

        System.out.println("result:"+ result+", execute times:" + (endTime - beginTime) + " ms");

        return result;
    }

    @After("excute()")
    public void after(JoinPoint joinPoint) throws Throwable {
        System.out.println("after:" + joinPoint.toString());
    }
}
@Aspect定义一个切面,@Pointcut 定义命名的切点

package asp;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AspConfig {

}
@EnableAspectJAutoProxy 启动AspectJ自动代理

package asp;

public interface HWPhone {
    String call(String user);
}

 

package asp;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component("p30Phone")
@Qualifier("p30")
public class P30Phone implements HWPhone {
    @Override
    public String call(String user) {
        System.out.println("使用 P30 手机呼叫..."+user);
        return "p30";
    }
}

 

package asp;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspConfig.class);
        HWPhone hwPhone = (HWPhone) context.getBean("p30Phone");
        hwPhone.call("xiaoming");
    }
}

运行结果:

parameter:["xiaoming"]
使用 P30 手机呼叫...xiaoming
result:p30, execute times:51 ms
after:execution(String asp.HWPhone.call(String))

 

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

定义一个切点

 

示例代码
<?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-4.3.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <bean id="p30Phone" class="asp.P30Phone"/>
    <bean id="recordAspect" class="asp.RecordAspect" />

    <aop:config>
        <aop:aspect ref="recordAspect">
            <aop:pointcut id="execute" expression="execution(*  asp.P30Phone.call(..))"/>
            <aop:before pointcut-ref="execute" method="before" />
            <aop:after pointcut-ref="execute" method="after" />
            <aop:around pointcut-ref="execute" method="around" />
        </aop:aspect>
    </aop:config>

</beans>

 

package asp;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class RecordAspect {

    public void before(JoinPoint joinPoint) {
        // 打印请求入参
        System.out.println("parameter:" + JSONObject.toJSONString(joinPoint.getArgs()));
    }

    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        long endTime = System.currentTimeMillis();

        System.out.println("result:"+ result+", execute times:" + (endTime - beginTime) + " ms");

        return result;
    }

    public void after(JoinPoint joinPoint) throws Throwable {
        System.out.println("after:" + joinPoint.toString());
    }
}

 

package asp;

public interface HWPhone {
    String call(String user);
}

 

package asp;

public class P30Phone implements HWPhone {
    @Override
    public String call(String user) {
        System.out.println("使用 P30 手机呼叫..."+user);
        return "p30";
    }
}

 

package asp;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("asp/application.xml");
        HWPhone hwPhone = (HWPhone) context.getBean("p30Phone");
        hwPhone.call("xiaoming");
    }
}

 

运行结果:

parameter:["xiaoming"]
使用 P30 手机呼叫...xiaoming
result:p30, execute times:0 ms
after:execution(String asp.HWPhone.call(String))

 

参考《Spring实战(第4版)》

 

相关文章