Spring Aop

介绍


在软件开发中,散布于应用中多处的功能被称为横切关注点(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;
    }

}

参考:https://segmentfault.com/a/1190000010175112

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值