如何使用Spring AOP及基本原理

阅读本文,你将了解到如何使用Spring AOP及AOP的基本原理,文末还与大家分享AOP的使用情景

在面向对象编程中(OOP)中,我们编程的关注点在于某个对象实体有哪些具体功能及其子类功能实现的不同。不同于OOP,面向切面编程(AOP)更多关注的业务流程。在不侵入业务代码的前提下,我们可以通过AOP编程,为业务流程某个具体环节(连接点)增加业务逻辑(通知),这些业务逻辑可能是打印日志、安全控制、事务控制等等。
使用AOP前必须理解清楚AOP相关的几个概念:通知(Advice)、切点(pointcut)、切面(aspect)、。

  • 通知(Advice):通知要解决的是通知什么、什么时候通知的问题。通知什么指的我们增加的功能,比如日志打印、事务控制等。什么时候通知指的是我们在什么时候调用我们增加的功能。我们可以在方法调用前调用通知(Before)、方法调用后调用通知(After)、方法调用成功后调用通知(After-returning)、方法调用异常后调用通知(After-throwing)、在方法调用前和调用后调用通知(Around)
  • 切点(Piontcut):切点主要定义的是在什么位置上应用通知,SpringAOP仅支持方法级别的切面编程(这和其应用动态代理实现有关)。一般我们会指定某个类的某个方法为切点,或者匹配某一通配符的一个或多个方法为切点,还可以指定由某一注解修饰的方法为切点等等。
  • 切面(aspect):切面是通知和切点定义的结合,切面定义了在什么时候、什么位置执行什么操作(何时何地执行何种操作)

通过理解这几个概念,面向切面编程(AOP)就是要解决何时何地执行何种操作的问题。
除了以上的三个概念,AOP还有其他的概念,在这里也简单说明一下:

  • 连接点:目标类中某个具体的方法(待增强);
  • 织入: 织入是将切面加入的目标类的过程。在Spring AOP中,织入指的是将切面逻辑应用到目标类中并生成代理类的过程。
1.使用示例

理解清楚AOP相关的几个概念后,我们可以看一个AOP的使用示例。
创建切面,其中注解@Pointcut定义了切点信息,@Before("log")和logPrint方法定义了通知信息。

//logAOP.java
@Component
@Aspect
public class LogAOP {
    //切点信息
    @Pointcut("execution(* cn.test.pro.project.GsProjectService.*(..))")
    public void log(){

    }
    //前置增强
    @Before("log()")
    public void logPrint(JoinPoint joinPoint){
        System.out.println("---------logPrint();-------"+joinPoint.getTarget().getClass());
    }

}

目标类的信息

//GsProjectService.java
@Service
public class GsProjectService {
    @Autowired
    private GsProjectMapper gsProjectMapper;

    public String getById(String id){
        return "admin";
    }
}

配置文件spring-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
         http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context-4.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    <!-- 扫描注解 -->
    <context:component-scan base-package="cn.test.pro">
        <context:exclude-filter type="annotation"       
           expression="org.springframework.stereotype.Controller" />
    </context:component-scan>
    <!--基于aspectj的注解驱动-->
    <aop:aspectj-autoproxy/>
</beans>

启动类相关信息

//Main.java
public class Main {
    public static void main(String[] args){
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
        GsProjectService gsProjectService = (GsProjectService) ac.getBean("gsProjectService");
        System.out.println(gsProjectService.getById("1"));
    }
}

输出结果:

---------logPrint();-------class cn.test.pro.project.GsProjectService
cn.test.pro.project.GsProject@3370f42
2. 基本原理

说明:本文提到的AOP的基本原理是主要说明使用注解的AOP,基于XML配置的AOP类似。看本节时建议先阅读Java 动态代理机制解析

说起Spring AOP的基本原理,我们要从配置文件中配置说起:

<!--基于aspectj的注解驱动-->
    <aop:aspectj-autoproxy/>

在xml配置文件中增加如上配置后,就开启了基于注解的AOP功能。我们知道Spring 启动时会读取配置文件,并对文件中的配置项进行解析。

  1. 当Spring读取到该配置项后,会根据该行的命名空间AOP,查找对应的命名空间处理器AOPNamespaceHandler;

2.在AOPNamespaceHandler中,我们看到如下的代码:

public class AopNamespaceHandler extends NamespaceHandlerSupport {
    public AopNamespaceHandler() {
    }

    public void init() {
        this.registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
        this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
        this.registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
        this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
    }
}

在方法init()中,我们看到"aspectj-autoproxy"配置信息的解析交给了类 AspectJAutoProxyBeanDefinition进行解析。

  1. 现在Spring知道要使用类AspectJAutoProxyBeanDefinition进行配置解析,类AspectJAutoProxyBeanDefinition是接口BeanDefinitionParser的实现类,接着Spring调用该类的parse方法进行解析;
//AspectJAutoProxyBeanDefinition.java
public BeanDefinition parse(Element element, ParserContext parserContext) {
 AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
        this.extendBeanDefinition(element, parserContext);
        return null;
    }
  1. 我们特别注意parse方法中的
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);

介绍这个方法前,我们还必须要知道AOP是通过动态代理机制实现的,而类AspectJAnnotationAutoProxyCreator正是完成由目标类(target Object)到代理类的转换,可以说该类是AOP实现的核心类。
我们接着看方法registerAspectJAnnotationAutoProxyCreatorIfNecessary的功能,从方法名上我们可以看出该方法主要完成的是将AspectJAnnotationAutoProxyCreator注册到Spring容器中的功能。这样在合适的时机,Spring就可以使用该类根据目标类动态生成代理类了。

  1. 什么是合适的时机呢?根据动态代理机制原理(可参考Java 动态代理机制解析)的介绍,生成代理类必须需要一个实例化的目标类。
    为了知道什么是合适的时机,我们还要看一下AspectJAnnotationAutoProxyCreator的类结构图,我们看到该类是接口BeanPostProcessor的实现类。

    AspectJAnnotationAutoProxyCreator的类结构图.png

    BeanPostProcessor是一种非常重要的接口,在创建Bean的过程中会调用BeanPostProcessor的postProcessAfterInitialization方法。spring的开发者也可以使用该接口的特性扩展bean的功能。而代理类的生成也正式在此处。

  2. 现在我们看一下AspectJAnnotationAutoProxyCreator的postProcessAfterInitialization方法

//方法的实现在AbstractAutoProxyCreator.java
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if(bean != null) {
            //如果已经生成过代理,则直接从缓存中获取
            Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
            if(!this.earlyProxyReferences.contains(cacheKey)) {
                //生成代理对象
                return this.wrapIfNecessary(bean, beanName, cacheKey);
            }
        }

        return bean;
    }

我们再看一下方法wrapIfNecessary的实现

//方法的实现在AbstractAutoProxyCreator.java
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
         //如果当前已经被代理过,则直接返回;
        if(beanName != null && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        } else if(Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        } else if(!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {
            //获取切面的所有信息(包含通知和切点信息)
            Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);
            if(specificInterceptors != DO_NOT_PROXY) {
                this.advisedBeans.put(cacheKey, Boolean.TRUE);
                //根据切面信息和具体bean,创建该bean的代理类
                Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
                this.proxyTypes.put(cacheKey, proxy.getClass());
                return proxy;
            } else {
                this.advisedBeans.put(cacheKey, Boolean.FALSE);
                return bean;
            }
        } else {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }
    }

wrapIfNecessary方法主要分为两个步骤:首先找到所有切面的信息,然后根据切面信息生成代理类。

  1. 我们再详细看一下Spring是如何创建代理类的?
//方法的实现在AbstractAutoProxyCreator.java
protected Object createProxy(Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {
        if(this.beanFactory instanceof ConfigurableListableBeanFactory) {
            AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)this.beanFactory, beanName, beanClass);
        }

        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.copyFrom(this);
        if(!proxyFactory.isProxyTargetClass()) {
            if(this.shouldProxyTargetClass(beanClass, beanName)) {
                proxyFactory.setProxyTargetClass(true);
            } else {
                this.evaluateProxyInterfaces(beanClass, proxyFactory);
            }
        }

        Advisor[] advisors = this.buildAdvisors(beanName, specificInterceptors);
        Advisor[] var7 = advisors;
        int var8 = advisors.length;

        for(int var9 = 0; var9 < var8; ++var9) {
            Advisor advisor = var7[var9];
            proxyFactory.addAdvisor(advisor);
        }

        proxyFactory.setTargetSource(targetSource);
        this.customizeProxyFactory(proxyFactory);
        proxyFactory.setFrozen(this.freezeProxy);
        if(this.advisorsPreFiltered()) {
            proxyFactory.setPreFiltered(true);
        }

        return proxyFactory.getProxy(this.getProxyClassLoader());
    }

//方法实现在ProxyFactory.java
public Object getProxy(ClassLoader classLoader) {
        return this.createAopProxy().getProxy(classLoader);
    }
//方法实现在ProxyCreatorSupport.java中
protected final synchronized AopProxy createAopProxy() {
        if(!this.active) {
            this.activate();
        }

        return this.getAopProxyFactory().createAopProxy(this);
    }
//方法实现在DefaultAopProxyFactory中
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if(!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
            return new JdkDynamicAopProxy(config);
        } else {
            Class<?> targetClass = config.getTargetClass();
            if(targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
            } else {
                return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass)?new ObjenesisCglibAopProxy(config):new JdkDynamicAopProxy(config));
            }
        }
    }

经历了多个方法间的调用,我们终于看到了关注了代码。在DefaultAopProxyFactory的方法createAopProxy中,我们看到了Spring 是如何选择JDK和CGLIB两种动态代理机制的:

  • 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP;
  • 如果目标对象实现了接口,可以强制使用CGLIB实现AOP(proxy-target-class为true 或 Optimize为true即可,optimize是CGLIB中的独有配置项),但是需要保证targetClass不是接口,并且targetClass不是jdk动态代理生成的类;
  • 如果目标对象没有实现接口,必须采用CGLIB库;
    默认情况下,Spring会使用JDK动态代理,但是也会根据实际情况在两者之间切换。
    确定使用哪种动态机制后,就可以创建目标类的代理了。至此 Spring AOP的基本原理就介绍完毕了。
3. 使用场景

了解了Spring AOP的使用示例及基本原理后,我们一块看两种Spring AOP的应用场景。

(1)增加统一日志

在第一节使用示例中,为我们展示在方法调用前增加日志打印。在Web开发中,我们可以实现Controller层或者Service统一日志打印,避免重复性日志打印代码。

(2)动态切换数据源

Spring对于多数据源有很好的支持。在Spring中,我们可以通过继承AbstractRoutingDataSource实现在程序运行时动态选择数据源。具体实现方案可以查看spring 动态切换数据源 多数据库

参考:
《Spring实战》 第三版
《Spring源码深度解析》
https://docs.spring.io/spring-framework/docs/current/javadoc-api/overview-summary.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 12,274评论 6 86
  • 本章内容: 面向切面编程的基本原理 通过POJO创建切面 使用@AspectJ注解 为AspectJ切面注入依赖 ...
    谢随安阅读 3,122评论 0 9
  • 一、AOP的基础 1.1、AOP是什么??? 考虑这样一个问题:需要对系统中的某些业务做日志记录,比如支付系统中的...
    聂叼叼阅读 2,105评论 2 17
  • 不要做坏事,你以为谁都伤害可以伤害你吧?你吃的像乌龟里面的嫩肉一样,你缺少一个壳,你连乌龟都不是 是这样吗?蜗牛之...
    不像话的故事阅读 66评论 0 0
  • 我是日记星球125号星宝宝吴翊灵,正在参加孙老师的日记星球21天蜕变之旅和21天亲子英语启蒙的学习,这是我的第篇5...
    wu溧蕙阅读 250评论 0 2