Spring原理初探----IOC、AOP

前言

说是Java web,Spring已经成为了事实标准,Spring原理的深入学习,无论是在工作中,还是在面试中,都尤为重要。

Spring的两个核心概念是IOC(控制反转)和AOP(面向切面编程)。想了解Spring的工作原理,毫无疑问,首先要从这两个概念的Spring实现入手。但是Spring源码浩如烟海,里面掺杂了太多的实现细节,入门可谓极其困难。当我正苦于难以入门时,好友介绍了tiny-spring这个开源项目,这个项目用了不到千行的代码,就将Spring的IOC、AOP的核心流程实现完毕,真是居家旅行、吹逼面试之必备呀!

废话少说,我们开始吧!

目录结构

在github上clone下项目来之后,我们关注src文件夹,其余的是一些爱好者提的注释PR,恰巧被作者merge了,不必理会。目录结构是这样的:


目录结构
  1. aop包,顾名思义,实现了Spring的AOP功能,可以通过bean的自动AOP切入,文件稍多,暂时先不展开。
  2. bean.factory包,通过BeanFactoryAbstractBeanFactoryAutowireCapableBeanFactory三个类,实现了BeanFactory的核心功能,详情稍后讲解。
  3. bean.io包定义了资源加载相关的抽象概念,这里的资源包括xml配置文件等。
  4. bean.xml包中只包含一个类:XmlBeanDefinitionReader,主要负责在xml配置文件中读取bean定义。
  5. bean包其他类,定义了BeanDefinition等核心概念,详情后讲。
  6. context包定义了ApplicationContext的核心概念。
  7. BeanReference指的是引用类型的Bean,而不是实体类。

IOC--浮沙筑台之根基

IOC(控制翻转)是一种编程范式,可以在一定程度上解决复杂系统对象耦合度太高的问题,并不是Spring的专利。IOC最常见的方式是DI(依赖注入),可以通过一个容器,将Bean维护起来,方便在其他地方直接使用,而不是重新new。可以说,IOC是Spring最基本的概念,没有IOC就没有Spring。

为什么DI可以起到解耦的作用?
一个软件系统包含了大量的对象,每个对象之间都有依赖关系,在普通的软件编写过程中,对象的定义分布在各个文件之中,对象之间的依赖,只能通过类的构造器传参,方法传参的形式来完成。当工程变大之后,复杂的逻辑会让对象之间的依赖梳理变得异常困难。
在Spring IOC中,一般情况,我们可以在XML文件之中,统一的编写bean的定义,bean与bean之间的依赖关系,从而增加逻辑的清晰度。而且,bean的创建是由Spring来完成的,不需要编程人员关心,编程人员只需要将精力放到业务的逻辑上面,减轻了思维的负担。

tiny-spring里面,整个beanscontext包都是用来实现IOC的。

beans包实现的核心关注点是BeanFactoryBeanFactory也叫作Bean容器,顾名思义,是用来盛放、管理bean的。
context包实现的核心关注是ApplicationContextApplicationContext也是用来获取Bean的,但是它更高层,它的面向用户是Spring的使用者,而BeanFactory面向的用户更多是Spring开发者。BeanFactory定义了Bean初始化的流程,ApplicationContext定义了从XML读取,到Bean初始化,再到使用的过程。

Bean在哪定义?

刚才有说到,Spring通常通过xml文件,来统一的描述bean,bean与bean的依赖关系。所以说,bean的定义表述,发生在xml配置文件之中。这个XML文件就是我们需要读取的资源文件。

因此,首要任务就是研究与读取XML资源文件相关的类。

bean.io中存放的是读取资源文件的抽象概念。其中包含了三个类或者接口:

  1. Resource接口,这个接口只有一个方法,InputStream getInputStream() throws IOException;。实现这个接口的类就是一个抽象的资源,可以获取这个资源的输入流,从而获取其中的内容。
  2. UrlResource类,这个类实现了Resource接口,通过构造器传入一个url地址,代表的是这个url所对应的文件。
  3. ResourceLoader类,只有一个方法,public Resource getResource(String location)。输入url的文件地址(并不是真正的URL格式地址),来获取Resource。

通过分析上面三个类、接口,我们知道,这个包完成了一个任务:通过ResourceLoader这个类,获取某一个地址的Resource,从而获取这个文件的输入流。因为使用了Resource概念,可以使用网络文件或者本地文件。

Bean如何定义?

  1. BeanDefinition是Bean定义的核心概念,BeanDefinition包含了:bean的对象、bean的类类型、bean的名字,bean的所有属性。这个类对bean的基本信息做好一个包装。
  2. BeanDefinitionReader接口,只有一个方法:void loadBeanDefinitions(String location) throws Exception;,实现这个接口的类,具有将某一个文件中的所有bean定义载入的功能。所以BeanDefinitionReader定义了,在哪载入bean定义,至于载入到哪里、如何载入,稍后看具体实现。
  3. AbstractBeanDefinitionReader抽象类,上面刚说了实现了BeanDefinitionReader接口的类,具有将某一个文件中描述的bean定义载入的功能,AbstractBeanDefinitionReader就实现了这样一个抽象功能。它的作用就是定义,载入到哪和如何载入的问题。在这个类里面,有两个属性:Map<String,BeanDefinition> registry;ResourceLoader resourceLoader;registry是一个注册表,他保存的就是所有的Bean定义,Map结构,key是bean的名字,value就是BeanDefinition。resourceLoader描述了如何载入。
  4. XmlBeanDefinitionReader这是beans.xml包里面的唯一一个方法,也是最重要的方法之一。它继承了AbstractBeanDefinitionReader,实现了所有方法,解决了bean定义中:在哪载入、如何载入、载入到哪的三个大问题。这个类面向用户的方法有两个,一个是loadBeanDefinitions,毫无疑问,这个是必须的。另一个是getRegistry用来获取bean注册表,得到所有bean的信息,registry是bean们在内存中实际的家。但是这个getRegistry方法并不是面向用户的,而是面向ApplicationContext的。
  5. PropertyValuePropertyValue代表一种抽象概念,在xml中,bean的属性包括属性名和属性对象,PropertyValue就是这么一个实体。
  6. BeanReference代表的是Bean的属性不是真实对象,而是另一个bean的引用。

Bean的组装全过程

上面两部分是铺垫,而BeanFactory才是重点对象。beans.factory包中有三个类用来定义BeanFactory相关的概念。

  1. BeanFactory接口,只有一个方法:Object getBean(String name) throws Exception;,实现这个接口的类,就具有了得到一个bean的能力。
  2. AbstractBeanFactory类,较为复杂。详情后讲。
  3. AutowireCapableBeanFactory继承了AbstractBeanFactory,实现了applyPropertyValues方法,通过反射,将bean的所有属性,通过set方法注入进去。

AbstractBeanFactory有三大属性:

  • beanDefinitionMap,类似于registry,但是他是BeanFactory里面私有的,代表的是这个BeanFactory里面暂时有哪些bean定义。
  • beanDefinitionNames代表里面,这个BeanFactory里面有哪些bean(名字)。
  • beanPostProcessors,代理处理器,AOP会用到,详情后讲。

AbstractBeanFactory实现了几大功能:

  • getBean,这是主要功能,可以获取一个Bean对象。
  • registerBeanDefinition,面向ApplicationContext,用来将XML配置文件里面的bean定义注册到BeanFactory里面。
  • preInstantiateSingletons,面向ApplicationContext,用来在开始的时候,将所有的bean都初始化完毕,避免懒加载。
  • addBeanPostProcessor添加代理处理器。
  • getBeansForType,在BeanFactory里面,获取某一个类型的所有bean。

经过上面的分析,我们可以知道BeanFactory完成了Bean初始化的整个流程。BeanFactory的工作流程如下:

  1. getBean, 在beanDefinitionMap里面得到bean,如果没有的话,先初始化。(为什么会没有,因为ApplicationContext读取xml文件时候,只是给BeanDefinition服了类类型,并没有赋值对象,这个对象还是需要BeanFactory通过反射生成的)。
  2. createBeanInstance,通过反射,根据BeanDefinition的类对象新建实体对象->将得到的bean对象赋值给beandefinition,然后将BeanDefinition里面的属性都注入到Bean里面,这就完成了doCreateBean。
  3. initializeBean就是调用BeanPostProcessor的postProcessBeforeInitilizztion方法和postProcessAfterIntilizatin方法,获取新的bean,这里会在aop中用到。

好了,到这BeanFactory就讲完了,下面是更重要的ApplicationContext。

ApplicationContext-用户与BeanFactory之间的桥梁

beans.context包有三个类、接口,完成了ApplicationContext的基本功能。

  1. ApplicationContext接口,没有任何方法,只是继承了BeanFactory接口,暗示ApplicationContext与BeanFactory都是获取Bean的地方。
  2. AbstractApplicationContext抽象类,首先,它的构造函数接收入参BeanFactory,所以说ApplicationContext内部具有一个BeanFactory。类似于一种装饰器模式,但不是装饰器模式,类似于代理模式,但也不是代理模式。fresh方法分为三个步骤:1.loadBeanDefinitions,这个是一个模板方法,需要子类实现,它的作用就是从某一个地方读取BeanDefinition,然后写入到ApplicationContext自己的BeanFactory里面,这就是ApplicationContext与BeanFactory之间的联系,也就是ApplicationContext还负责了读取定义。2. registerBeanPostProcessors,这个就是在BeanFactory里面找到BeanPostProcessor,然后将他们放到BeanFactory的beanPostProcessors容器里面,方便BeanFactory初始化使用。3. onRefresh初始化一遍所有的bean。
  3. ClassPathXmlApplicationContext实现了loadBeanDefinitions的方法,将xml文件和BeanFactory结合在一起。

总结-ApplicationContext初始化流程

ApplicationContext初始化流程

总结-ApplicationContext获取bean流程

ApplicationContext getBean

AOP--移花接木之魔法

上一节,讲完了Spring IOC的整个流程,也就是bean从定义获取,到得到bean之间的整个流程。本节,我们接触一下Spring另一个重要概念,AOP。AOP用途十分广泛,其中Spring内部的声明式事务和拦截器都是利用了AOP的强大威力,才得以优雅的实现。
AOP是什么呢,简单来说,它可以让编程人员在不修改对象代码的情况下,为这个对象添加额外的功能或者限制。
很熟悉吧,这就是代理模式!

Java中存在两种代理模式:
一种叫静态代理,就是通过接口继承复用的方式来完成的, 代理类与被代理对象实现相同的接口,然后代理类里面会拥有一个被代理对象,代理类与被代理对象相同的方法,活调用被代理对象的方法,不过中间会加以限制,您翻开任何一本设计模式相关的书,翻到代理模式这一节,讲的就是它了。

另一种叫做动态代理,动态代理就是允许我们在程序运行过程中,为动态生成的对象,动态的生成代理。显然,这比静态代理灵活太多了。
Java默认提供了动态代理的实现方式,但是有限制,它要求被代理对象必须实现某一个接口。为了突破这一限制,为普通类也可以提供代理,CGLib这个库横空出世。

因为AOP涉及的知识较为复杂,所以我先将背景知识介绍一下。

  1. Java动态代理,就是Java本身提供的代理实现,要求对象必须实现某一个接口。
  2. CGLib库,为Java提供了,为普通类提供代理的功能。
  3. aopalliance,aop联盟包,统一类aop编程的一些概念,这个包里没有具体的类实现,而是定义了几个重要的概念接口,具体的aop实现,要遵从这些接口编程,才能达到一定的通用性。
  4. aspectj包,实现了,通过一种固定的编程语言,通过这种简单的编程语言,我们可以定位到被代理的类,自动完成代理。

在aopallicance里面,定义了几个核心概念:

  1. Advice增强,说明这是一个,实现这个接口,说明这个类负责某一种形式的增强。
  2. Joinpoint连接点,表示切点与目标方法连接处的信息。
  3. MethodInterceptor继承了Interceptor接口,而Interceptor继承了Advice接口,是一种Advice,但是有一个方法invoke。这个方法需要一个参数MethodInvocation。
  4. MethodInvocation表示的是连接点的信息以及连接点函数的调用。
    结合上面的信息,我们发现,其实MethodInterceptor的invoke方法,调用的就是MethodInvocation的proceed方法,而这个proceed方法呢,应该调用的肯定是Method.invoke方法。所以,这是一种变相调用method.invoke的方式。为什么这样做呢,猜一猜的话,肯定是为了代码的复用,哈哈哈,这是废话。
    在Spring中,还定义了几个核心概念:
  5. Pointcut,切点,可以定位类以及方法
  6. Advisor,可以获取一个增强。
  7. PointcutAdvisor,定义了哪些方法,具有什么类型的增强。
  8. MethodMatcher表示某一个方法是否符合条件
  9. ClassFilter 定义了某个类是否符合条件。
  10. TargetSource被代理的对象,包括对象本身,类类型、所有接口
  11. AdvisedSupport代理相关的元数据,包括被代理的对象,增强等。
  12. ProxyFactory,代理工厂,可以获得一个代理对象,同时具有AdvisedSupport的所有特性。
  13. Cglib2AopProxy,使用cglib实现的动态代理类,继承了AbstractAopProxy抽象类,这个类的主要方法就是getProxy,通过什么呢,通过AdvisorSupport。
  14. ReflectiveMethodInvocation可以获取连接点的信息,代理的对象等。
  15. JdkDynamicAopProxy,和Cglib2AopProxy类一个作用,通过AdvisorSupport来getProxy,不过是使用Java自带的动态代理实现的。
    其中,ProxyFactory是获取一个代理对象的直接工厂,而这个代理对象,可以通过Cglib2AopProxy产生,也可以通过JdkDynamicAopProxy产生。

Spring AOP之所以能够为动态生成的Bean提供代理,得益于PostProcessor接口。我们会议IOC初始化流程中,最后一部,就是得到BeanFactory之中所有继承了PostProcessor接口的bean,调用它们的postProcessBeforeInitilization、postProcessAfterInitilization方法,来代理bean,生成新的bean。

基于这个突破口,我们只需要在xml配置文件中,放入PostProcessor对象,Spring就会自动的用这写对象,来代理真正的对象。


aop目录

在这里,我们的对象是AspectJAwareAdvisorAutoProxyCreator。
在这个对象的方法中,逻辑是这样的,找到xml里面所有切面bean,然后在这些bean里面,找到符合被代理类的切面bean,找到切面bean之后,就可以获得增强,切点等,于是可有构造一个AdvisorSupport,知道了AdvisorSupport,我们就能够通过proxyFactory来获取代理了。

至于如何这个类切面是用来切入代理类的,这个就要交给PointCut来实现了,pointcut有很多实现方式,这里我们用的是aspectj。具体这个类我就不细讲了。

到目前位置,我自己已经将整个AOP的流程搞清楚了,下面通过流程图的形式展示出来:


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

推荐阅读更多精彩内容