Spring-AOP编程(JavaConfig)

本文是Spring AOP官方文档部分内容的简记。

1,Introduction

Spring IoC容器不依赖于AOP,但是作为IoC容器的补充,AOP提供了非常强大的中间件解决方案。

AOP在Spring框架中的用途:

  • 提供声明式企业级服务,特别是作为EJB声明式服务的替代品,典型的应用就是Spring声明式事务管理。
  • 允许用户自定义切面

1.1,AOP概念

  • 切面(Aspect):切面是模块化的关注点,这些关注点会横切多个类。在Spring AOP中,切面通过普通java类(基于原型—schema-based的方法)或者基于注解@Aspect实现。切面=切点+通知
  • 连接点(Joint Point):程序执行期间的某个运行点,如方法的执行或异常处理。在Spring AOP中,连接点总是表示方法的执行。
  • 通知(Advice):在特定的连接点上由切面执行的动作。通知分为不同的类型:around, before 和 after,后面会详细讨论。在 Spring 中,通知是一个拦截器(interceptor)模型,Spring在连接点前后维护一个拦截器链。
  • 切点(Pointcut):匹配连接点的断言。切面总是和切点表达式相关,并在任何匹配的连接点执行通知。Spring默认使用AspectJ切点表达式语言。切点决定了在何处执行通知。
  • 引入(Introduction):向现有的类添加额外的属性或方法。
  • 目标对象(Target object):被一个或多个切面通知的对象。由于Spring AOP使用运行时代理实现,目标对象总是被代理的对象—参考Spring AOP的实现。
  • AOP代理(AOP proxy):有AOP框架创建的用来实现切面协议的对象。Spring框架通过JDK动态代理或者CGLIB代理实现AOP。
  • 织入(Weaving):将切面和应用类型或对象连接起来创建advised对象的过程。织入过程能发生在编译时期,加载时期以及运行时。Spring AOP在运行时执行织入。

通知类型:

  • 前置通知(Before advice):执行在连接点之前的通知。除非抛出异常,前置通知无法阻止程序执行到连接点。
  • 后置返回通知(After returning advice):连接点正常返回后的通知。
  • 后置异常通知(After throwing advice):连接点抛出异常时的通知。
  • 后置通知(After finally advice):不管连接点执行成功与否都会执行的通知,类似try-catch中的finally。
  • 环绕通知(Around advice):包裹连接点的通知。环绕通知能运行到连接点也可以直接返回或抛出异常防止程序运行到连接点。

1.2,Spring AOP的能力和目标

Spring AOP由纯Java实现,不需要控制类加载器,适用于Servlet容器或应用服务器。

Spring AOP的实现和大多数其它的AOP框架不同,Spring的目标不是提供完整的AOP实现,而是在AOP实现和IoC容器提供一个集成方案,以此解决企业级y应用中的一些常见问题。因此,Spring AOP的功能总是和IoC容器结合在一起,切面通过常规的bean definition语法配置。

1.3,AOP代理

Spring AOP默认使用标准的JDK动态代理实现。Spring AOP也能使用CGLIB代理,这在代理类而不是接口的时候是必要的。当业务对象没有实现任何接口的时候,CGLIB默认被使用。

2,@AspectJ注解支持

Spring利用AspectJ中的切点解析和匹配库来解析AspectJ中的注解,但是AOP运行时仍旧是纯Spring AOP,它并不依赖于AspectJ编译器或织入器。

2.1,启动@AspectJ支持

可以通过Java配置使能@AspectJ注解支持(XML配置不在此记录)

@Configuration
@EnableAspectJAutoProxy
public class AppConfig{
  // TODO
}

2.2,声明一个切面

任何定义在上下文中并被@Aspect注解的bean都可以被Spring探测到并用于配置Spring AOP。例如:

package com.example;
import org.aspectj.lang.annotation.Aspect

@Aspect
public class NotVeryUsefulAspect {
    // TODO
}

可以通过xml配置将切面类注册成切面bean,也可以通过组件扫描自动探测它们。自动扫描需要配合Spring 原型注解(例如@Component)使用。

2.3,声明切点

切点的声明包括两部分:1,由名称和任意多个参数构成的签名;2,切点表达式。在@AspectJ注解风格的AOP中,切点签名由一个常规的方法定义表示,切点表达式由@Pointcut注解表示。注意,作为切点签名的方法的返回类型只能是void.

示例代码:

@Pointcut("execution(* transfer(...))") //切点表达式
private void anyOldTransfer() {} // 切点签名

上述切点匹配任何名为transfer的方法调用。关于切点语言,参考Aspect Programming Guide. Spring AOP支持的切点标识符(AspectJ pointcut Designators)如下:

  • execution - for matching method execution join points, this is the primary pointcut designator you will use when working with Spring AOP
  • within - limits matching to join points within certain types (simply the execution of a method declared within a matching type when using Spring AOP)
  • this - limits matching to join points (the execution of methods when using Spring AOP) where the bean reference (Spring AOP proxy) is an instance of the given type
  • target - limits matching to join points (the execution of methods when using Spring AOP) where the target object (application object being proxied) is an instance of the given type
  • args - limits matching to join points (the execution of methods when using Spring AOP) where the arguments are instances of the given types
  • @target - limits matching to join points (the execution of methods when using Spring AOP) where the class of the executing object has an annotation of the given type
  • @args - limits matching to join points (the execution of methods when using Spring AOP) where the runtime type of the actual arguments passed have annotations of the given type(s)
  • @within - limits matching to join points within types that have the given annotation (the execution of methods declared in types with the given annotation when using Spring AOP)
  • @annotation - limits matching to join points where the subject of the join point (method being executed in Spring AOP) has the given annotation

Spring AOP还支持一个名为bean的PCD。这个PCD允许限制匹配连接点到给定名字的Spring bean或者bean集合(通配符)

联合切点表达式

切点表达式能通过“&&”, “||”, “!“ 进行联合。也可以通过名字引用切点表达式。例如:

@Pointcut("execution(* *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.example.someapp.trading..*)")
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

共享公共切点定义

package com.example.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

  //定义一个web层的连接点,方法在某个包或其任意的子包下
  @Pointcut("within(com.example.someapp.web..*)")
  public void inWebLayer() {}

  //定义一个服务层的连接点,。。。。。。。
  @Pointcut("within(com.example.someapp.service..*)")
  public void inServiceLayer() {}

  //定义一个数据访问层连接点,。。。。。。。。
  @Pointcut("within(com.example.someapp.dao..*)")
  public void inDaoLayer() {}

  //业务服务是定义在服务接口中的任意方法的执行。
  @Pointcut("execution(* com.example.someapp..service.*.*(..))")
  public void businessService() {}

  //数据访问层中任意方法的调用
  @Pointcut("execution(* com.example.someapp.dao.*.*(..))")
  public void dataAccessOperation() {}
}

2.4,声明通知

通知总是和切点表达式相关,它运行在和切点匹配的方法调用之前、之后或者环绕这个方法。

前置通知

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

  @Before("com.example.SystemArchitecture.dataAccessOperation")
  public void doAccessCheck() {
    // TODO
  }
}

后置通知

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class BeforeExample {

  @AfterReturning("com.example.SystemArchitecture.dataAccessOperation")
  public void doAccessCheck() {
    // TODO
  }
}

后置通知获取方法返回值

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // TODO
    }
}

环绕通知

环绕通知由@Around注解声明,通知方法的第一个参数必须是ProceedingJoinPoint类型。在方法内部,调用ProceedingJoinPoint对象的proceed()方法执行要调用的方法。proceed()方法还能传入一个Object[]参数。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}

通知参数

可以在通知签名中声明自己需要的参数。

访问当前连接点

任何通知方法都能将当前连接点声明为它的d一个参数(org.aspectj.lang.JoinPoint类型),JointPoint接口提供了很多有用的方法,例如:getArgs()获取方法参数,getThis()获取代理对象,getTarget()获取目标对象等。

传递参数给通知方法

为了能使参数值可被通知体访问,可以使用args绑定形式。如果参数名被用于代替参数表达式的类型名,那么当通知被调用时,参数的值被作为参数值传递给通知。例如:

@Before("com.example.myapp.SystemArchitecture.dataAccessOperation() && args(account, ..)")
public void validateAccount(Account account) {
  // ...
}

切点表达式的args(account, ..)部分有两个目的:1,限制匹配到那些至少需要一个参数的方法,而且参数必须是Account类型的实例;2,使得实际的Account对象在通知体中也能访问。

通知参数和泛型

假设有以下泛型类型:

public interface Sample<T> {
  void sampleGenericMethod(T param);
  void sampleGenericCollectionMethod(Collection param);
}

可以限制拦截的方法类型为确定的参数类型,只需要将通知参数写成实际需要的类型即可。

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
  // Advice implemention
}

确定参数名称

绑定到通知调用的参数依赖于切点表达式中匹配的名字。参数名字无法通过java反射技术获取,Spring AOP通过以下策略获取参数名称:

  • 如果参数名称已经有用户显示指定,那么就使用指定的参数名,通知和切点注解都有一个可选的属性”argNames“用于指定注解方法的参数名,例如:

    @Before(value="com.example.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames="bean, auditable")
    public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ...use code, bean and jp....
    }
  • 查看debug信息,尝试从局部变量表中确定参数名。

  • 如果代码编译时没有必须的debug信息,Spring AOP将会尝试绑定变量到参数

  • 如果上述所有策略失效,抛出IllegalArgumentException异常

处理参数

@Around("execution(List find*(..)) && " + "com.example.myapp.SystemArchitecture.inDataAccessLayer() && " + "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern) throws Throwable {
  String newPattern = preProcess(accountHolderNamePattern);
  return pjp.proceed(new Object[] {newPattern});
}

3,引入

引入使得切面能声明被通知的对象实现了给定的接口,并代表那些对象实现接口。

引入通过@DeclareParents注解实现。给定一个接口UsageTracked和这个接口的实现DefaultUsageTracked,下面的例子表明所有实现了service接口的类都实现了UsageTracked接口:

@Aspect
public class UsageTracking {
  @DeclareParents(value="com.example.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
  public static UsageTracked mixin;

  @Before("com.example.myapp.SystemArchitecture.businessService() && this(usageTracked)")
  public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
  }
}

4,切面实例模型

默认情况下,应用上下文中的每个切面都是单例的。这是AOP的一个高级主题,暂时略过。

你可能感兴趣的:(Spring,&,SpringBoot)