Spring 学习其二:AOP

一、AOP

Spring 的两大特性,IOC 在上一章,本篇讨论另一大特性,AOP(面向切面)。

何为面向切面,

动态代理,可以绑定一个接口和一个它的实现,并且代理这个实现类,所以我们可以在代理里写进一些自己的操作,甚至可以不执行实现类的方法。

原来的代码:

这是接口:

public interface ProxyService {
    void HelloWorld();

}

这是它的实现类:

import static MyTools.PrintTools.*;
public class ProxyServiceImpl implements ProxyService {

    @Override
    public void HelloWorld() {
        println("Hello World");
    }

}

然后是动态代理的实现过程:

public class ProxyJdkExample implements InvocationHandler{

    private Object target = null;
    
    public Object bind(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        println("proxy method");
        println("before proxy");
        //Object obj = method.invoke(target, args);
        println("after proxy");
        return null;
    }

}

 二、在 xml 里定义 AOP

现在,我们再来一个例子,这个例子会贯穿整篇完整。

有一个叫做演员接口,它有一个方法叫做表演:

public interface Performer {

    void Perform();
}

然后有一个实现他的类:

public class PerformerImpl implements Performer{

    @Override
    public void Perform() {
        System.out.println("Performing....");
    }

}

去调用这个方法很容易,但是现在我希望,在表演前,能够让观众关掉手机。我们可以利用动态代理把关手机的过程放进去:

public class PerformProxy implements InvocationHandler {

    private Object target = null;
    
    public Object bind(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("close your phone");
        Object obj = method.invoke(target, args);
        return obj;
    }

测试:

public class PerformaceTest {
    public static void main(String[] args) {
        PerformProxy performProxy = new PerformProxy();
        Performer performerImpl = new PerformerImpl();
        Performer performer =(Performer)performProxy.bind(performerImpl);
        performer.Perform();
    }

}
/*output:
close your phone...
Performing....*/

现在回到 AOP,AOP 让我们能够更方便的去实现上述的功能。在上面的示例中,我们完成在表演前让观众关闭手机,但是还不够,我们希望能够在完成表演后观众可以鼓掌,如果表演失败,观众会要求退票。现在我们用 spring 来实现。

首先我们把上述添加的功能作为方法统一放到一个观众类里:

public class Audience {

    public void before() {
        System.out.println("close your phone....");
    }
    public void after() {
        System.out.println("clapping ....");
    }
    public void afterThrowning() {
        System.out.println("take my money back ....");
    }
}

在进行面向切面设计之前,先来简单介绍 AOP 的几个术语:

  1. 切面(Aspect):就是指 Audience ,它是一个方法的集合,可以把这些方法切入到别的地方;
  2. 通知(Advice):我们之前实现的关手机操作,是在表演开始前的,“表演开始前”就是一个通知,Spring 提供的通知有:前置通知(before),后置通知(after)返回通知(afterReturning),异常通知(afterThrowing),环绕通知(around),每个通知的具体位置(在方法进行到哪一步执行)会在后面示例中体现
  3. 切点(Pointcut):我们想要切入的点,上面的示例,切点就是 perform() 方法。
  4. 连接点(join point):连接点对应的事具体需要拦截的东西,比如通过切点的正则表达式去判断哪些方法是连接点。
  5. 织入(Weaving):织入是一个生成代理对象并将切面内容放入指定流程中的过程。

现在我们来通过 xml 来定义 AOP:

 

<bean id = "performAspect" class = "SpringTest.performance.aspect.Audience"/>
    <bean id = "performer" class = "SpringTest.performance.service.impl.PerformerImpl">
    </bean>
    <aop:config>
      <aop:aspect ref = "performAspect">
        <aop:pointcut expression="execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..))" id="performPoint"/>
        <aop:before method="before" pointcut-ref = "performPoint"/>
        <aop:after method="after" pointcut-ref = "performPoint"/>
        <aop:after-throwing method="afterThrowning" pointcut-ref = "performPoint"/>
      </aop:aspect>
    </aop:config>

注意 performer 也必须定义,只有将它放入到 IOC 容器里,AOP 才能生效。

测试:

public class PerformaceTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = 
                new ClassPathXmlApplicationContext("spring-cfg.xml");    
        Performer performer =(Performer)ctx.getBean("performer");
        performer.perform();
    }

}
/*output:
close your phone....
Performing....
clapping ....
*/

如果,方法执行过程中抛出异常,就会调用 afterThrowning()(这里不演示)

接下来是环绕通知,环绕通知的功能很强,甚至能够实现后置和前置通知,为何?先看代码:

public void around(ProceedingJoinPoint jp) {
        System.out.println("keep quite...");
        try {
            jp.proceed();
        } catch (Throwable e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("ending...");
        
    }

这是环绕通知的实现方式,把它放在 Audience 类里即可,可以发现,它是带有一个参数的,这个参数可以反射连接点的方法,也就是说环绕通知可以自定义什么时候执行原方法,甚至不执行。

现在我们再 xml 里注册它:

<aop:around method="around" pointcut-ref = "performPoint"/>

测试后的输出为:

close your phone....
keep quite...
Performing....
clapping ....
ending...

从输出顺序可以看出环绕通知执行的方式,clapping 在 ending 前面,说明什么?

说明传递给 around 的参数已经和 after 先绑定在一起了。所有在执行 try 内代码的时候,clapping 就执行了。

那么 close your phone...为何在keep quit 之前?难道 before 就没有和切点方法绑定?为了得到解释,我们只能通过 debug 一步步看它怎么执行的。

这里直接给出,我看源码的结果:

首先这些通知是按照一定的顺序放置在一个拦截器数组数组里的,第一个被执行的是 Before 所以第一个输出 close your phone;

第二个执行的是 Around,一路执行到输出 keep quite。到了这里,我们会进入到 jp.proceed() 该方法会进入到拦截器数组.

拦截器数组第三个应该是 afterThrowning 但是不满足执行条件,所以执行第4个 after。然后输出 clapping...

jp.proceed() 执行完后,继续执行 Around 的输出 ending。

整个过程其实是一个递归过程,当执行 Around 时,应为 proceed() 的关系,进入了更深层次的递归,所以导致,ending 的输出在 clapping 后面。

  三、给通知传递参数

表演者往往都有自己的粉丝,而粉丝只想给自己的偶像鼓掌怎么办?能不能再通知里加入一个参数能够判别是哪个表演者在表演?在动态代理里,代理方法 invoke 是能够获取到被代理方法的参数的,所以在 spring 里也是可行的。

首先,先在被代理的方法 perform 里加上演员名称的参数:

public interface Performer {
    void perform(String performer);
}
public class PerformerImpl implements Performer{

    @Override
    public void perform(String performer) {
        System.out.println(performer + " is Performing....");
    }

}

之后,修改切点的定义,添加参数进去:

<aop:pointcut expression="execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..)) and args(performer)" id="performPoint"/>

在对应的通知方法里加上参数:

public class Audience {

    public void before(String performer) {
        System.out.println("close your phone....");
        System.out.println("welcome " + performer);
    }
    public void after(String performer) {
        System.out.println("clapping for " + performer);
        
    }
    public void afterThrowning(String performer) {
        System.out.println("take my money back ....");
    }
    public void around(ProceedingJoinPoint jp,String performer) {
        System.out.println(performer+" is singing");
        System.out.println("ending...");
        
    }
}

需要注意的是 Around 通知的参数必须放在 jp 后面,放在前面会抛出异常。

四:引入

引入说的简单点就是引入其他接口的实现,比如 PerformImpl 实现的事 Perform 这个接口,通过引入可以让它实现另一个接口,并且能为它指定具体的实现方法。

现在,我们就为 Perform 添加一个接口,这个接口的方法用来判断票卖出了多少,如果少于100张,就取消表演:

public interface TicketCheck {
    boolean ticketIsEnough(int number);
}
public class TickCheckImpl implements TicketCheck {

    @Override
    public boolean ticketIsEnough(int number) {
        return number > 100 ? true:false;
    }

}

然后再 xml 里把 TicketChenk 引入到 Perform 里:

<aop:config>
      <aop:aspect ref = "performAspect">
        <aop:pointcut expression="execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..)) and args(performer)" id="performPoint"/>
        <aop:after method="after" pointcut-ref = "performPoint"/>
        <aop:after-throwing method="afterThrowning" pointcut-ref = "performPoint"/>
        <aop:before method="before" pointcut-ref = "performPoint"/>
        <aop:around method="around" pointcut-ref = "performPoint"/>
        <aop:declare-parents types-matching="SpringTest.performance.service.impl.PerformerImpl+" 
implement-interface
="SpringTest.performance.service.TicketCheck" default-impl = "SpringTest.performance.service.impl.TicketCheckImpl"/> </aop:aspect> </aop:config>

来分析下关于引入的定义:

<aop:declare-parents types-matching="SpringTest.performance.service.impl.PerformerImpl+" 
                             implement-interface="SpringTest.performance.service.TicketCheck"
                             default-impl = "SpringTest.performance.service.impl.TicketCheckImpl"/>

types-matching:表示切点的类型,这里是 PerformerImpl,后面的 + 表示我们想要为它添加一个新的接口,接口的名称在 implement-interface 声明,然后默认的实现类在 default-impl 里声明,这样处理之后,我们能干吗?我们可以把 PerformerImpl 显性的转换为 TicketCheck :

public class PerformaceTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = 
                new ClassPathXmlApplicationContext("spring-cfg.xml");    
        Performer performer =(Performer)ctx.getBean("performer");
        TicketCheck ticketCheck = (TicketCheck) performer;
        if(ticketCheck.ticketIsEnough(101))
        performer.perform("Luhan");
    }

}

五:注解实现 AOP

注解实现的解释说明和 xml 很接近,这里直接上代码做参照即可

Audience:

 

package SpringTest.performance.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.DeclareParents;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import SpringTest.performance.service.TicketCheck;
import SpringTest.performance.service.impl.TicketCheckImpl;

@Aspect
@Component(value="performAspect")
public class Audience {
    @Pointcut("execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..))" + " && args(performer)")
    public void performPoint(String performer) {
        
    }
    @DeclareParents(value="SpringTest.performance.service.impl.PerformerImpl+",defaultImpl = TicketCheckImpl.class)
    public TicketCheck ticketService;
    
    
    @Before("performPoint(performer)")
    public void before(String performer) {
        System.out.println("close your phone....");
        System.out.println("welcome " + performer);
    }
    @After("performPoint(performer)")
    public void after(String performer) {
        System.out.println("clapping for " + performer);
        
    }
    @AfterThrowing("performPoint(performer)")
    public void afterThrowning(String performer) {
        System.out.println("take my money back ....");
    }
    @Around("performPoint(performer)")
    public void around(ProceedingJoinPoint jp,String performer) {
        System.out.println(performer+" is singing");
        System.out.println("ending...");
        
    }
}

performPoint() 方法只是为了给上面注解的 Pointcut 创建一个 id。

此处有很多需要注意得点,不然很容易报错:

  1. 在类上面添加 @Aspect 这样才能被当做切面处理
  2. 在类上面添加 @Component 这样才能被装入 IOC 容器
  3. 定义切点的时候注意格式:* 和 S 之间的空格必须存在
    @Pointcut("execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..))" + " && args(performer)")
  4. @Pointcut 下面的方法参数必须和注解里的参数保持一致
  5. 注意通知里的切点的格式,必须有双引号,参数必须和切点保持一致

以上有几点关于格式的错误,Spring 的报错很不详细的,所以必须在键入代码时,就注意格式问题。

完成之后,就可以把 xml 里关于 AOP 的声明都删除了:

为了注解的内容能被扫描到,我们还需要定义一个扫描器:

 

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import SpringTest.performance.aspect.Audience;
import SpringTest.performance.service.impl.PerformerImpl;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages= {"SptingTest.performance"},
               basePackageClasses= {PerformerImpl.class,Audience.class})
public class PerformConfig {

}

注意我用的注解,一个都不能少。。。

然后就可以运行测试了:

public class PerformaceTest {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(PerformConfig.class);    
        Performer performer =(Performer) ctx.getBean("performer");
        TicketCheck ticketCheck = (TicketCheck) performer;
        if(ticketCheck.ticketIsEnough(101))
        performer.perform("Luhan");
    }

}

六、关于多重切面

Spring 可以针对一个切点,定义多个切面和多个通知,需要注意的是,想要控制好它的顺序,可在 @Aspect 注解下面跟随 @Order({数字}) 比如 @Order(1),表是它的顺位是第一个,但是记住,这里 1 表达的是,该切面是在所有切面的最外围,2 被包裹在 1 里面,3 又会被包裹在 2 里面。其输出顺序 1 的 before 在最前面调用,After 在后面调用。

至于更为复杂的 Around 通知的顺序,我会在后面针对 通知的执行顺序 源码说明里提到(挖个坑)。

posted @ 2018-10-02 21:33  crazy_runcheng  阅读(237)  评论(0编辑  收藏  举报