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?: 可选的修饰符模式,如
public
、protected
、private
或*
(任意)。 - ret-type-pattern: 返回类型模式,如
int
、java.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()
方法,下面是几个切入点表达式的例子:
- 匹配所有公共方法:
execution(public * com.example.BusinessService.*(..))
- 匹配
BusinessService
类中的所有doSomething()
方法:
execution(* com.example.BusinessService.doSomething(..))
- 匹配所有
doSomething()
方法,无论在哪个类中:
execution(* *.doSomething(..))
- 匹配所有返回类型为
void
的方法:
execution(void *.*(..))
- 匹配所有带有两个参数的方法:
execution(* *.*(?, ?))
- 匹配所有带有两个字符串参数的方法:
execution(* *.*(java.lang.String, java.lang.String))
- 匹配所有带有
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
接口。