介绍
在软件开发中,散布于应用中多处的功能被称为横切关注点(crosscutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。
advisedSupport.isOptimize()与advisedSupport.isProxyTargetClass()默认返回都是false,所以在默认情况下目标对象有没有实现接口决定着Spring采取的策略,当然可以设置advisedSupport.isOptimize()或者advisedSupport.isProxyTargetClass()返回为true,这样无论目标对象有没有实现接口Spring都会选择使用CGLIB代理
Spring在运行时通知对象
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
Spring只支持方法级别的连接点
通过使用各种AOP方案可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么可以利用Aspect来补充Spring AOP的功能。
通过切点来选择连接点
关于Spring AOP的AspectJ切点,Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。下表列出了Spring AOP所支持的AspectJ切点指示器:
AspectJ指示器 | 描 述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。
注:只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。说明execution指示器是在编写切点定义时最主要使用的指示器。
编写切点
package concert;
public interface Performance
{
public void perform();
}
Performance可以代表任何类型的现场表演,如舞台剧、电影或音乐会。设想编写Performance的perform()方法触发的通知。下图展现了一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。
使用AspectJ切点表达式来选择Performance的perform()方法:
使用execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明了不关心方法返回值的类型。然后指定全限定类名和方法名。对于方法参数列表,使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。
设现需配置的切点仅匹配concert包。在此场景下,可以使用within()指示器限制切点范围:
and代替“&&”,or代替“||”,not代替“!”
在切点中选择bean
Spring引入新的bean()指示器,它允许在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。
执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock:
execution(* concert.Performance.perform()) and bean('woodstock')
使用非操作为除了特定ID以外的其他bean应用通知:
execution(* concert.Performance.perform()) and !bean('woodstock')
使用注解创建切面
定义切面
//Audience类:观看演出的切面
package concert;
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(** concert.Performance.perform(..))") // 表演之前
public void silenceCellPhones()
{
System.out.println("Silencing cell phones");
}
@Before("execution(** concert.Performance.perform(..))") // 表演之前
public void takeSeats()
{
System.out.println("Taking seats");
}
@AfterReturning("execution(** concert.Performance.perform(..))") // 表演之后
public void applause()
{
System.out.println("CLAP CLAP CLAP!!!");
}
@AfterThrowing("execution(** concert.Performance.perform(..))") // 表演失败之后
public void demandRefound()
{
System.out.println("Demanding a refund");
}
}
Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。
Spring使用AspectJ注解来声明通知方法:
注 解 | 描 述 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
为@Pointcut注解设置的值是一个切点表达式,就像之前在通知注解上所设置的那样。通过在performance()方法上添加@Pointcut注解,实际上扩展了切点表达式语言,这样就可以在任何的切点表达式中使用performance()了,如果不这样做的话,需要在这些地方使用那个更长的切点表达式
现在把所有通知注解中的长表达式都替换成了performance(),该方法的实际内容并不重要,在这里实际上是空的。其实该方法本身只是一个标识,供@Pointcut注解依附
// 通过@Pointcut注解声明频繁使用的切点表达式
package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Audience
{
@Pointcut("execution(** concert.Performance.perform(..))") //定义命名的切点
public void performance(){}
@Before("performance()")
public void silenceCellPhones()
{
System.out.println("Silencing cell phones");
}
@Before("execution(** concert.Performance.perform(..))") // 表演之前
public void takeSeats()
{
System.out.println("Taking seats");
}
@AfterReturning("performance()") // 表演之后
public void applause()
{
System.out.println("CLAP CLAP CLAP!!!");
}
@AfterThrowing("performance()") // 表演失败之后
public void demandRefound()
{
System.out.println("Demanding a refund");
}
}
像其他的Java类一样,它可以装配为Spring中的bean:
@Bean
public Audience audience()
{
return new Audience();
}
通过上述操作,Audience只会是Spring容器中的一个bean。即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理
使用JavaConfig可以在配置类的类级别上通过使用EnableAspectJ-AutoProxy注解启用自动代理功能:
package concert;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
// 启动AspectJ自动代理
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig
{
@Bean
public Audience audience()
{
return new Audience();
}
}
创建环绕通知
环绕通知是最为强大的通知类型。能够让所编写的逻辑被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知
// 使用环绕通知重新实现Audience切面
package concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Audience
{
@Pointcut("execution(** concert.Performance.perform(..))") //定义命名的切点
public void performance(){}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp)
{
try{
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
jp.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch(Throwable e){
System.out.println("Demanding a refund");
}
}
}
@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知。首先接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为在通知中需要通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法
注:调用proceed()方法。如不调该方法,那么通知实际上会阻塞对被通知方法的调用;若不调用proceed()方法,会阻塞对被通知方法的访问,与之类似,也可以在通知中对它进行多次调用。
Spring Aop中的常用对象
JoinPoint 对象
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
方法名 | 描 述 |
---|---|
Signature getSignature() | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs() | 获取传入目标方法的参数对象 |
Object getTarget() | 获取被代理的对象 |
Object getThis() | 获取代理对象 |
ProceedingJoinPoint对象
ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中:
- Object proceed() throws Throwable //执行目标方法
- Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
例子:
@Around("declareJoinPointerExpression()")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;
try {
//前置通知
System.out.println("目标方法执行前...");
//执行目标方法
//result = pjd.proeed();
//用新的参数值执行目标方法
result = pjd.proceed(new Object[]{"newSpring","newAop"});
//返回通知
System.out.println("目标方法返回结果后...");
} catch (Throwable e) {
//异常通知
System.out.println("执行目标方法异常后...");
throw new RuntimeException(e);
}
//后置通知
System.out.println("目标方法执行后...");
return result;
}
Spring Aop 增强(Advice)
增强(advice)主要包括如下五种类型:
- 前置增强(BeforeAdvice):在目标方法执行前实施增强
- 后置增强(AfterReturningAdvice):在目标方法执行后实施增强
- 环绕增强(MrthodInterceptor):在目标方法执行前后实施增强
- 异常抛出增强(ThrowsAdvice):在目标方法抛出异常后实施增强
- 引介增强(IntroductionIntercrptor):在目标类中添加一些新的方法和属性
例子:
定义代理接口
public interface ITarget {
String speak(String name);
}
定义被代理对象
//被代理对象
public class Target implements ITarget{
private static final String name = "zenghao";
@Override
public String speak(Integer age){
System.out.println("hello I'm " + age + " years old");
return "I'm return value";
}
public static String getName() {
return name;
}
}
定义增强
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class BeforeAdvice implements MethodBeforeAdvice {
/**
* @param method:目标类的方法
* args: 目标类的方法入参
* obj:目标类实例
*
*/
@Override
public void before(Method method, Object[] args, Object target)
throws Throwable {
if(target instanceof Target){
System.out.println("前置日志记录: " + ((Target)target).getName() + "调用了" + method.getName() + "方法,传入参数为:" + args[0] );
}
}
}
/*------------------分割线---------------------*/
package test.aop;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
public class AfterAdvice implements AfterReturningAdvice {
/**
* @param
* returnValue 返回值
* method:目标类的方法
* args: 目标类的方法入参
* obj:目标类实例
*
*/
@Override
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
if(target instanceof Target){
System.out.println("后置日志记录: " + ((Target)target).getName() + "调用了" + method.getName() + "方法,返回值为:" + returnValue );
}
}
}
配置代理对象ProxyFactoryBean
ProxyFactoryBean是FactoryBean的实现类,我们知道FactoryBean负责初始化Bean,而ProxyFactoryBean则负责为其他Bean创建代理实例,通过在xml中配置后注入使用
<!-- 配置被代理对象 -->
<bean id="mytarget" class="test.aop.Target" />
<!-- 配置前置增强 -->
<bean id="myBeforeAdvice" class="test.aop.BeforeAdvice" />
<!-- 配置后置增强 -->
<bean id="myAfterReturnAdvice" class="test.aop.AfterAdvice" />
<!-- 配置代理对象 -->
<bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean" >
<!-- 配置代理接口集 -->
<property name="proxyInterfaces" value="test.aop.ITarget" />
<!-- 代理目标对象需要实现的接口,可以通过<list>标签设置多个 -->
<!-- 把通知织入代理对象 -->
<property name="interceptorNames" >
<list>
<idref bean="myBeforeAdvice"/>
<idref bean="myAfterReturnAdvice"/>
</list>
</property><!-- 配置实现了Advice增强接口的Bean,以bean名字进行指定 -->
<property name="targetName" value="mytarget"></property><!-- 代理的目标对象 -->
</bean>
测试
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAOP {
private ApplicationContext ac;
@Before
public void setup(){
ac = new ClassPathXmlApplicationContext("classpath:test/aop/aop.xml");
}
@Test
public void test(){
ITarget iTarget = (ITarget) ac.getBean("proxyFactoryBean");
iTarget.speak(21);
}
}
Spring aop实现
- Pointcut(切点):过滤条件,指定在那些类的那些方法上织入横切逻辑;
- Advice(增强):用于描述横切逻辑和方法的具体织入点;
- Advisor(通知器):将Pointcut和Advice两者组装起来;
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
}
}
@Configuration
public class LogConfiguration extends AbstractPointcutAdvisor {
private Pointcut pointcut;
private Advice advice;
@Autowired
private LogProperties logProperties;
@PostConstruct
public void init() {
this.pointcut = new AnnotationMatchingPointcut(null,Log.class);
this.advice = new LogMethodInterceptor();
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this.advice;
}
}