Bootstrap

Spring AOP:面向切面编程的最佳实践 ( 四 )

6.切点表达式

Spring AOP(面向切面编程)使用切入点表达式来定义哪些连接点(join points)应该应用切面逻辑。

切入点表达式是一种灵活的方式来指定哪些方法调用、构造函数调用或其他类型的连接点应该触发通知(advice)的执行。

Spring AOP 支持多种类型的切入点表达式,最常用的是基于方法调用的表达式。

6.1.基本语法

Spring AOP 的切入点表达式通常使用 execution 函数来定义方法调用的切入点。基本形式如下:

execution(modifiers-pattern? ret-type-pattern declaring-class-pattern? name-pattern(param-pattern) throws-pattern?)

这里的各个部分解释如下:

  • modifiers-pattern?: 可选的修饰符模式,如 publicprotectedprivate*(任意)。
  • ret-type-pattern: 返回类型模式,如 intjava.lang.String*(任意)。
  • declaring-class-pattern?: 可选的声明类模式,如 com.example.*(包下的任何类)或 *(任意)。
  • name-pattern: 方法名模式,如 doSomething*(任意)。
  • param-pattern: 参数列表模式,如 (int, java.lang.String)*(..)(任意数量和类型的参数)。
  • throws-pattern?: 可选的异常模式,如 throws java.lang.Exception*(任意)。

6.2.示例

假设你有一个 BusinessService 类,其中包含 doSomething() 方法,下面是几个切入点表达式的例子:

  1. 匹配所有公共方法:
execution(public * com.example.BusinessService.*(..))
  1. 匹配 BusinessService 类中的所有 doSomething() 方法:
execution(* com.example.BusinessService.doSomething(..))
  1. 匹配所有 doSomething() 方法,无论在哪个类中:
execution(* *.doSomething(..))
  1. 匹配所有返回类型为 void 的方法:
execution(void *.*(..))
  1. 匹配所有带有两个参数的方法:
execution(* *.*(?, ?))
  1. 匹配所有带有两个字符串参数的方法:
execution(* *.*(java.lang.String, java.lang.String))
  1. 匹配所有带有 Exception 异常的方法:
execution(* *.*(..) throws java.lang.Exception)

6.3.其他类型的切入点

除了 execution 函数之外,Spring AOP 还支持以下类型的切入点表达式:

  • within: 匹配特定类的所有方法。

    within(com.example.BusinessService+)
    
  • this: 匹配代理对象的类型。

    this(com.example.BusinessService)
    
  • target: 匹配目标对象的类型。

    target(com.example.BusinessService)
    
  • args: 匹配方法调用时的参数类型。

    args(java.lang.String, ..)
    
  • @target: 匹配目标对象上的注解。

    @target(com.example.MyAnnotation)
    
  • @within: 匹配类上的注解。

    @within(com.example.MyAnnotation)
    
  • @args: 匹配方法参数上的注解。

    @args(com.example.MyAnnotation)
    

6.4.组合多个切入点

可以组合多个切入点表达式来创建更复杂的规则。例如,使用 &&(交集)、||(并集)和 !(否定)运算符:

execution(* com.example.BusinessService.*(..)) && this(com.example.BusinessService)

上述表达式匹配所有在 BusinessService 类中的方法调用,并且这些方法的调用发生在 BusinessService 类型的代理对象上。

6.5.应用位置

在 Spring AOP 中,切入点表达式可以以多种方式使用。以下是几种常见的使用方式:

1. 直接写在通知上

你可以直接在通知(Advice)的方法上定义切入点表达式。这种方式适用于只需要在一个地方使用该切入点的情况。

假设你有一个 BusinessService 类,其中包含 doSomething() 方法,你可以直接在通知上使用切入点表达式:

@Aspect
public class LoggingAspect {

    @Before("execution(* com.example.BusinessService.doSomething(..))")
    public void logBefore(JoinPoint joinPoint) {
        //通知内容
    }
}
2. 写在一个声明的方法上

另一种方式是将切入点表达式定义在一个单独的方法上,并使用这个方法作为通知中的切入点。这种方法的好处是可以重用相同的切入点表达式。

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

@Aspect
public class LoggingAspect {

    @Pointcut("execution(* com.example.BusinessService.doSomething(..))")
    public void businessServiceDoSomething() {}

    @Before("businessServiceDoSomething()")
    public void logBefore(JoinPoint joinPoint) {
        // 通知内容
    }

    @After("businessServiceDoSomething()")
    public void logAfter(JoinPoint joinPoint) {
        // 通知内容
    }
}

在上面的例子中,我们定义了一个名为 businessServiceDoSomething 的切入点方法,它指定了 BusinessService 类中的 doSomething() 方法作为切入点。然后我们在通知方法中使用这个切入点名称来引用这个切入点。

3. 组合多个切入点

你可以组合多个切入点表达式来创建更复杂的规则。例如,使用 &&(交集)、||(并集)和 !(否定)运算符。

假设你有两个切入点,一个匹配 BusinessService 类的所有方法,另一个匹配所有公共方法。你可以组合这两个切入点来定义一个新的切入点表达式:

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

@Aspect
public class LoggingAspect {

    @Pointcut("execution(* com.example.BusinessService.*(..))")
    public void businessServiceMethods() {}

    @Pointcut("execution(public * *(..))")
    public void allPublicMethods() {}

    @Pointcut("businessServiceMethods() && allPublicMethods()")
    public void businessServicePublicMethods() {}

    @Before("businessServicePublicMethods()")
    public void logBefore(JoinPoint joinPoint) {
        // 通知内容
    }
}

在上面的例子中,我们定义了三个切入点:businessServiceMethods 匹配 BusinessService 类的所有方法,allPublicMethods 匹配所有公共方法,businessServicePublicMethods 是前两者的交集。然后我们在通知方法中使用 businessServicePublicMethods 来指定只对 BusinessService 类中的公共方法执行前置通知。

4. 使用 AspectJ 注解

如果你使用 AspectJ 的编译时织入(compile-time weaving),你还可以在普通的 Java 类中使用 AspectJ 注解来定义切入点和通知。

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

@Aspect
public class LoggingAspect {

    @Pointcut("execution(* com.example.BusinessService.doSomething(..))")
    public void businessServiceDoSomething() {}

    @Before("businessServiceDoSomething()")
    public void logBefore(JoinPoint joinPoint) {
  		// 通知内容
    }
}

请注意,在使用 AspectJ 的编译时织入时,你需要使用 AspectJ 的编译器来编译你的源代码,并且通常需要在项目构建过程中集成 AspectJ 编译器。

5. 使用表达式变量

你可以在切入点表达式中使用变量来存储常用的表达式,这样可以在多个通知中重用相同的表达式。这有助于减少重复代码并提高可读性和可维护性。

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

@Aspect
public class LoggingAspect {

    private String businessServiceMethods = "execution(* com.example.BusinessService.*(..))";

    @Pointcut(businessServiceMethods)
    public void businessServiceDoSomething() {}

    @Before("businessServiceDoSomething()")
    public void logBefore(JoinPoint joinPoint) {
  		// 通知内容
    }

    @After("businessServiceDoSomething()")
    public void logAfter(JoinPoint joinPoint) {
  		// 通知内容
    }
}

在这个例子中,我们将常用的切入点表达式存储在 businessServiceMethods 字符串变量中,并在 @Pointcut 注解中引用这个变量。这种方式对于较长或复杂的表达式特别有用。

6. 使用配置文件定义切入点

你也可以在 Spring 的 XML 配置文件中定义切入点表达式,然后在通知中引用这些表达式。

假设你有一个 XML 配置文件 applicationContext.xml,你可以像这样定义切入点:

<bean id="loggingAspect" class="com.example.LoggingAspect">
    <property name="pointcuts">
        <map>
            <entry key="businessServiceDoSomething">
                <value>execution(* com.example.BusinessService.*(..))</value>
            </entry>
        </map>
    </property>
</bean>

<aop:config>
    <aop:pointcut id="businessServiceDoSomething" expression="execution(* com.example.BusinessService.*(..))"/>
    <aop:advisor advice-ref="loggingAspect" pointcut-ref="businessServiceDoSomething"/>
</aop:config>

LoggingAspect 类中,你可以使用注入的 Pointcut 对象:

import org.springframework.beans.factory.annotation.Autowired;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Pointcut;

@Aspect
public class LoggingAspect {

    private Pointcut businessServiceDoSomething;

    @Autowired
    public void setBusinessServiceDoSomething(Pointcut businessServiceDoSomething) {
        this.businessServiceDoSomething = businessServiceDoSomething;
    }

    @Around("businessServiceDoSomething()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before doSomething: " + joinPoint.getSignature());
        Object result = joinPoint.proceed();
        System.out.println("After doSomething: " + joinPoint.getSignature());
        return result;
    }
}
7. 使用 AspectJ 的 @DeclareParents 注解

@DeclareParents 注解可以用来创建引入(Introductions),即向现有类添加新接口的实现。你可以使用切入点表达式来指定哪些类应该引入新接口。

假设你有一个接口 Loggable 和一个类 BusinessService,你可以在 AspectJ 注解中使用 @DeclareParents 来指定哪些类应该实现 Loggable 接口:

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

@Aspect
public class LoggingAspect {

    @DeclareParents(value = "com.example.BusinessService+", defaultImpl = LoggableImpl.class)
    public static Loggable loggable;

    public static class LoggableImpl implements Loggable {
        @Override
        public void log(String message) {
            System.out.println("Logging: " + message);
        }
    }
}

在这个例子中,@DeclareParents 注解中的表达式 "com.example.BusinessService+" 指定 BusinessService 类及其子类都应该实现 Loggable 接口。

;