首页 > 技术文章 > Spring框架学习(十):探索Spring AOP原理及基于注解的AOP

chenyulin 2019-07-24 13:48 原文

在Spring框架学习系列的第二篇文章中,我简单的使用了基于xml文件配置的AOP,那篇文章没有告诉大家为什么要使用AOP以及Spring AOP的原理,在这篇文章我们就深入学习面向切面编程。

1、为什么要引入AOP思想?

最最重要的一点就是我们希望业务关注业务逻辑自身,从代码层面避免引入太多与业务无关的逻辑,比如:数据库事务管理、日志管理、异常处理等等。我们都不希望在业务逻辑操作数据库完成数据持久化的时候要考虑使用额外的代码逻辑来开启事务支持,可是开启事务又是必需的,怎么办?AOP很好的解决了这个问题。我们把开启数据库事务管理的操作封装到一个类(这个类在AOP中叫做:切面)中,然后通过在服务类或者其他业务相关的类的方法上面使用一个自定义注解(也可以是其他的方式)来引入切面中的事务管理操作,这样是不是就能减小非逻辑代码的侵入性了?有点不明白我在说什么?看看下面这个例子。

@Transactional
public void dbOperation(){}

仅仅是在方法上引入一个@Transactional注解就能完成事务的管理,是不是很炫酷?当然了,声明式的事务管理只是Spring AOP应用的一个简单例子,在开发中我们可以根据自己的需求定义我们的AOP。不过你首先要明确什么时候需要AOP:当通用服务在多处被调用造成大量的代码冗余时。这个只是我的简单的概括.....你也可以有自己的理解。

2、Spring AOP的原理是什么?

不知道你是否了解Java的动态代理机制,Spring AOP的实现就是依赖了这个机制,为目标bean动态创建一个代理bean用来增强目标bean的功能。

 在编写代码时你只引入了一个注解,Spring却为你创建了一个代理类,于是在程序执行过程中,只要方法上加了@Transactional注解,就会自动进行事务管理,我上面这个图只是一个代理类的抽象表示。

 

那么除了这种用注解触发切面逻辑的方式之外还有没有别的方式呢?答案是:有。Spring AOP的粒度虽只精确到方法上,但是你也可以玩出很多花样,比如:根据方法上是否有某个指定注解触发切面逻辑、根据方法名字触发逻辑、触发指定具有指定参数的方法的切面逻辑等等。

 

我还要告诉你一个更令人震惊的功能:Spring AOP可以实现让某个对象动态地实现某个接口。What!!!!。你没有听错。一会儿给你来个例子。现在我们就开始编写代码实现自己的AOP,采用Java配置方式。

 

情景是这样的:有一个Performance接口,有一个Dance实现类,我要实现当你调用Dance对象的performance()方法时,执行切面中keepSilence()方法。以下是各个类的代码:

 

package demo4;

public interface Performance {
    
    void performance();
}

 

package demo4;

import org.springframework.stereotype.Component;

@Component
public class Dance implements Performance{

    public void performance() {
        System.out.println("跳舞表演");
    }
}
package demo4;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {

    @Pointcut("execution(* demo4.Performance.performance(..))")
    public void performanceCut(){}


    @Before("performanceCut()")
    public void keepSilence(){
        System.out.println("表演之前,请保持安静...");
    }
}
package demo4;

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

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackages = "demo4")
public class MyConfig {

}
package demo4;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class App {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(demo4.MyConfig.class);
        Dance dance = context.getBean(Dance.class);
        dance.performance();
        context.close();
    }
}

先看一下执行结果:

如果之前你没有一点AOP基础,看起来肯定会有点蒙,没关系,我来帮你梳理梳理。Dance和Performance类不需要介绍了,我们从App即主程序开始分析。由于我采用的是Java注解配置方式,所以我的上下文context也使用相应的类,当上下文对象创建建时,它会去找我指定的配置类:MyConfig。看看MyConfig怎么编写的:@Configuration表明这是一个配置类,没问题吧?@ComponentScan()扫描指定路径的组件,即指定路径下的带@Component注解的类,带@Component注解的类会被Spring容器管理,也就是创建一个对应的bean到容器中。然后是@EnableAspectJAutoProxy(proxyTargetClass = true),还记得刚说过的吧?Spring AOP是基于动态代理机制的,因此我们要使用这个注解开启,它有个参数,代理目标类=true。如果不设置的话,AOP默认是为bean实现的接口代理。

 最关键的地方来了,看一下MyAspect类,一共有两个方法,其中一个是空的方法。第一个方法上使用了@PointCut()注解,表明这个方法是切点,注解里面还有一个叫做切点表达式的参数。《Spring实战第四版》一书中对这个表达式是这么解释的:

我想你应该明白了切点表达式的含义了。被@PointCut标注的函数不需要参数也不需要方法体,这个方法的方法名表示一个切点名,仅此而已。另外一个函数由@Before()注解标识,函数体就是我们要实现的逻辑,我们想在目标方法执行前(@Before()表明在执行前)切入自定义逻辑。程序运行结果你也看到了,在调用dance.performance()之后打印的结果,就是这个keepSilence()方法得到调用。在这个切面里面,你可以定义多个切点,除了@Before还有@After、@AfterReturning、@AfterThrowing、@Around等等。具体的注解以及使用方式你可以查阅资料。这里提一下,切点表达式除了写成执行方法触发之外还可以写成遇到注解触发,就像这样:@Pointcut("@annotation(package.yourAnnotation)"),我想,@Transactional注解就是这么实现的。你编写的切面也需要由Spring容器管理,需要用@Bean或者@Compnent注解标识它。

 

Spring AOP更强的功能:让bean动态实现指定接口

在没告诉你这个功能之前,你肯定会想:怎么可能?impossible!废话不多说。看例子。在上面的基础上,我加入两个类(一个接口一个实现类),Back4Performance、Back4PerformanceImpl,不要纠结于这个类名(back for performance:返场表演),我想让容器中的dance 这个bean实现我定义的这个接口,并且可以调用接口的方法。看看怎么写:

 

package demo4;

public interface Back4Performance {

    void back4performance();
}
View Code

 

package demo4;

public class Back4PerformanceImpl implements Back4Performance {
    public void back4performance() {
        System.out.println("开始返场表演");
    }
}
View Code

重新写一个切面,代码如下:

package demo4;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect2 {

    @DeclareParents(value = "demo4.Performance+",defaultImpl = Back4PerformanceImpl.class)
    public static Back4Performance back4Performance;

}

由于事实上你在定义Dance类的时候没有实现Back4Performance接口,所以你从容器中取得的对象是不能直接调用这个函数的,你可以通过Java的反射机制来进行调用。代码如下:

package demo4;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class App {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(demo4.MyConfig.class);
        Dance dance = context.getBean(Dance.class);
        dance.performance();
        Class<Dance> clazz = (Class<Dance>) dance.getClass();
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if(method.getName().equals("back4performance")){
                try {
                    method.invoke(dance);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }    
        context.close();
    }
}
View Code

运行结果:

是不是很强?简直逆天!关于Spring AOP我就介绍那么多,感兴趣的朋友可以自行阅读《Spring实战第四版》,链接:https://pan.baidu.com/s/1P9mV1PdHC1BhfHY9Dj3ngw 提取码:f9jo。Spring框架学习系列暂时就告一段落了,Spring MVC和SpringBoot的底层还是DI和AOP,因此学习Spring MVC和SpringBoot可以从Spring核心开始。如果这篇文章有帮到你,欢迎点赞、收藏、关注、转发...

 

推荐阅读