本篇博客主要是根据《Spring揭秘》所做的读书笔记。这一部分主要是讲述IoC和AOP的部分。
第一章
介绍了spring框架的历史和包含的东西,自己看即可。
第二章
简单介绍了一下,什么是IoC,以及几种注入的方法。
什么是IoC
IoC的功能,简单来讲就是我原先想要做一道番茄炒蛋,那么我必须去菜市场自己买来番茄和鸡蛋,然后炒着吃。但是这里就有一个问题,我必须依赖于番茄和鸡蛋,没有它们我做不了菜。而IoC的职能就是,在我开始做菜之前,它会帮我送来番茄和鸡蛋,这样我就能安心炒菜了。之后如果我要炒其他菜,我只需要通知IoC,让它为我准备好材料,而不需要自己亲自准备材料了。也就是原先我自己掌握原材料,现在是IoC来帮我管理,这里就发生了控制反转。这里的原材料,对应的就是一个个Java对象。
IoC的注入方法
当然IoC需要知道怎么构造这些对象,不然它怎么送来给我们使用呢?那就有以下这些依赖注入的方法:
- 通过构造方法注入。构造方法本来就是用来创造对象的,所以只需要准备好构造方法的参数列表中的对象,自然而然就可以创造出一个对象。
- 通过setter方法注入。setter方法也可以为对象添加(更改)值,所以也可以用setter方法,而且setter方法可以被继承、可以设置默认值、不像构造方法一旦执行就马上创建出了新对象等,所以比较推荐使用。
- 通过接口注入。几乎完全看不到使用了,所以不做介绍。
第三章
IoC容器的职能
说白了它的职能其实非常简单:创建出对象,解决对象之间的依赖关系(依赖绑定)。
创建对象上面已经讲了,这里聊一聊解决对象之间的依赖关系的几种方法:
- 直接写代码告知。这种方法非常简单,我直接告诉一个IoC容器,将
A.class与new a()这个方法绑定,当我们需要这个特定的对象的时候,请把容器中对应的那个实例返还给我们。一个简单的hashmap就能实现这个功能。 - 配置文件。配置文件其实就是spring最广为使用的方法之一,随便找个spring的配置bean的文件看一眼就懂了。简单来说就是将目前的这个类所依赖的类全部写清楚具体类的信息,然后容器就能把类给注入进来。
- 使用元数据(注解)。同样是spring最广为使用的方法之一。
第四章
从这一章开始,我们就开始深入了解spring里面的IoC容器——BeanFactory和ApplicationContext(见第五章)
spring的IoC容器除了能够提供上面说的创建对象和依赖注入这两种职能外,还兼顾AOP支持,生命周期管理等功能。
- BeanFactory,采用懒加载的模式,所以它启动速度快,所需资源少。
- ApplicationContext,它在BeanFactory之上构建(间接继承),且构建之后会完成初始化并且绑定,所以会稍微慢点并且消耗比较多的资源,但是它功能齐全。

上图是它们之间的关系。
BeanFactory源代码
1 | public interface BeanFactory { |
从源码中我们可以看到,最基本的就是能够根据id来获取bean对象,确认这个bean对象是否包含在IoC容器中以及确认这个bean对象是单例还是多例。
BeanFactory的依赖注入
既然BeanFactory是一个IoC的容器,那么它必然需要知道如何来进行依赖注入,令人兴奋的是,它支持第三章所讲述的全部三种的依赖注入方法。
直接编码
spring又抽象了BeanDefinationRegistry这个接口,用这个接口来注册管理bean。而有一个实现类叫DefaultListableBeanFactory则是同时实现了这两个接口(也就是它既可以注册,也可以作为一个BeanFactory),所以用它就可以完成直接编码的任务。下面是代码:
1 | public class Demo01 { |
虽然我们一直说的都是你和BeanFactory进行打交道,来获取对象,但是其实真正的“对象”是BeanDefinition;每一个对象,都在BeanFactory中有一个BeanDefinition来对应,它包含了对象的一切信息。而RootBeanDefinition和ChildBeanDefinition则是它的两个实现类。(PS:BeanDefinition是被AbstractBeanDefinition所继承的,所以代码里并不是直接用的BeanDefinition)。
通过配置文件
主要是properties和xml,当然你如果愿意也可以用自己的文件格式(但是解析你得自己来了)。由于目前properties用的比较少了,所以下面我只介绍一下xml格式的。
其核心思想很简单,就是通过BeanDefinitionRead这个类来加载这些配置文件的信息,然后映射到BeanDefinition中,把BeanDefinition注册到BeanDefinitionRegistry中,然后由BeanDefinitionRegistry完成注册和加载。本质上其实和直接编码完全一致,但是完成了解耦,所以非常爽。
首先我们需要提供一个bean.xml文件:
1 | <bean id="person" class="cn.chenlangping.Person"> |
然后就更加简单了:
1 | public class Demo02 { |
直接读取并生成即可。
进一步深入解析xml配置文件
最顶层的标签,<beans>,是所有的最高层,下面是它的一些属性(这些属性对其下所有bean都生效)
- default-lazy-init,是否进行延迟初始化,默认false
- default-autowise,自动绑定的方式,默认no
- default-dependency-check,做依赖检查,默认no
- default-init-method,指定初始化方法名字
- default-destroy-method,指定对象销毁方法。
上面的这些属性,是对其内部所有的bean都生效,所以可以理解为就是简化操作而已。
之后就是四种并列的标签了,分别是<bean> <description> <import> 和<alias>,下面我们了解下几乎不怎么用到的三个:
<description>:添加写描述性的信息….真没人用<import>:看一下使用方法:<import resource="xx.xml"/>,看上去还算有用,但是实际中我们的容器本来就可以增加多个配置,所以xml中的这个功能倒是略显鸡肋了。<alias>:为bean起外号,比如某个bean的id超级超级长,可以用<alias name="longlonglongname" alias="short"/>来替换。
接下来就是我们的重头戏,<bean>这个标签了
id:就像身份证一样唯一来标示这个bean,但是并不是必须的。
class:用以指定具体的类型,基本上是必须的(除非你用bean来作为模板)。
然后分别是用构造器和使用setter方法注入的示例:
1 | <bean id="person" class="cn.chenlangping.Person"> |
1 | <bean id="person" class="cn.chenlangping.Person"> |
值得注意的是,如果使用setter进行注入,请确保有一个无参的构造器,因为setter的依赖注入必须你先有对象才可以呀;而且这两个(setter注入)可以同时使用。
接下来是property和constructor-arg都通用的一些配置项:
- value:我们一般将其作为bean标签内部的一个属性,但是它其实也可以单独作为一个标签,上面构造器注入就是。
- ref:就是引用别的bean,和value一样,我们也是基本把它当成一个属性来用。
- 内部bean:就是只给当前的对象所用,这个bean就可以没有id(本来也不想给别人用),当然你要加上id也是没有问题的。
- list:就是Java中的list子类。
- set:同list
- map:
<entry key="key1" value="value"> <null/>:这个值得特别讲一讲,在使用String的时候,如果你没写这个标签而是留空了,那么默认这个String是"",而只有这个标签才代表了是Null。
如果某些类需要依赖另外一些类,而你又没有写ref,那么就需要depends-on来显式指定。
在bean中,我们可以通过autowire来明确指定bean之间的依赖关系,有下面几种:
- no:默认值。依赖你手工配置。
- byName:如果你的类中定义了某个属性,然后如果bean中的name和你类中的属性(变量)名字一样,那么就会自动注入。
- byType:如果没找到对应的type,那么不做设置;如果有多个type,那么就会出现问题。
- contructor:这个用来匹配构造方法的参数类型从而完成自动绑定。
推荐还是不要使用上面的这些属性来完成自动配置,现在idea能够自动提示,我个人是完全不在意来多敲几个字符的。而且难道@Autowired它不香吗?
注解
目前使用最多的一个方法。上面的直接编码比较麻烦,而通过XML的方式则有些格格不入(我个人认为,一个java项目,能用代码解决的地方,少用别的格式)
一个@Autowired注解,就告知了spring,我们目前需要这个注解下面的对象,请把它注入进来。相应的一个@Component则是告知spring这个类是用来生成所需要的对象的。即@Component用来构造我们需要的类,而@Autowired则是用来解决我们的依赖关系。
bean的作用域scope
面试常考这个,这里打算彻底理解一下这个。scope一共有五种类型,分别是最开始的两种:singleton和prototype,随后加入了request,session和global session(spring5中已经去掉了它)。
- singleton是当容器启动的时候,因为第一次请求而被初始化,随着容器退出才消亡。而且保证整个容器过程中只有一个实例。tips:从生命周期和单例这两方面回答面试官。还有一点是不要和设计模式中的单例混淆,容器中的这个是在同一个容器中保证只有一个实例,而设计模式中的是同一个类加载器下,只有一个实例。
- prototype是每次容器接到对应的请求就生成一个对象,只要容器把对象发出去了,那么容器就不再持有对这个对象的引用了,接下来就是由接收这个对象的人来进行这个对象的生命管理了。
- request只对web应用有效。每个请求进来的时候,容器都会生成一个全新的实例来使用。所以你可以把它看成是prototype的一种特例。
- session同样只对web应用有效。其实就是更长的存活时间(还不一定)。
- global session只对portlet应用才有效。在平时(基于servlet的应用),就是一个普通的session。
替换
这部分主要讲述了方法的注入、实现BeanFactoryAware或者直接通过方法替换来实现方法返回值的变动。
BeanFactory背后的流程
我们可以将spring的IoC容器实现其功能的过程,分成两个部分:容器的启动和bean的实例化。
容器的启动过程
最开始的阶段,毫无疑问需要读取我们之前的xml配置文件,然后经过一通解析,就可以封装出一个个的BeanDefinition,这些BeanDefinition会注册到BeanDefinitionRegistry,然后准备过程就完成啦。
bean的实例化
通过上面的这些阶段,我们需要的信息已经准备完毕,当调用容器的getBean方法的时候,或者隐式调用getBean的时候,容器就会实例化对象了。
所以简单概括下:一个阶段画图纸(构造BeanDefinition),另外一个阶段根据图纸做成品(Object)。
当然,spring这么优秀的框架,提供了一套完美的机制让我们来插手容器和bean创建的每一个过程。这个设计思路,也完全可以用到自己设计开发的框架上去。
插手容器的启动
容器的启动本质上就是准备好BeanDefinition的过程,spring则是提供了一种叫BeanFactoryPostProcessor的接口来让我们对BeanDefinition来做一些操作。而如果我们的这些操作还是有顺序的,则还需要实现核心包下面的Ordered接口。当然spring为我们已经实现了两个不错的实现类,分别是PropertyPlaceholderConfigurer和PropertyOverrideConfigurer。
由于BeanFactory比较落后…所以需要我们手动创建这些类,然后进行注册。而ApplicationContext则会自动识别并使用。下面详细来看看这两个实现类好了。
PropertyPlaceholderConfigurer
我们可以在我们的配置文件中使用占位符,比如<value>${jdbc.password}</value>,然后在另外一个文件中指定:jdbc.password=123456这样子的。所以如果不用这个类,那么最后在BeanDefinition中的将会是${jdbc.password},所以需要这个类来完成最后的替换工作。
PropertyOverrideConfigurer
Emmm 这个和上面的是一样的,只不过上面的是“显式”的,而这个比较隐蔽。就是你偷偷摸摸在文件中写了一些配置文件,则这些配置文件会覆盖掉之前的BeanDefinition。如果存在多个,那么最后一个会生效。
CustomEditorConfigurer
上面两个都是对BeanDefinition做了改动,而这个类则不会对BeanDefinition进行改动。在开始这个自定义的BeanFactoryPostProcessor之前,我们先需要了解一下PropertyEditor。
PropertyEditor
我们在xml中配置的所有信息,本质上都是字符串,比如<value>2019/01/01</value>,那我们就需要把这个字符串转换成Date对象,但是spring并不知道要怎么转换呀,所以需要我们通过使用一种叫PropertyEditor的类来辅助。当然spring自然也是提供了一些PropertyEditor的。
我们如果要自己实现,要么实现这个接口,要么继承PropertyEditorSupport来少写点代码。
OK了解了这个,我们的CustomEditorConfigurer就是用来注册这些的PropertyEditor。
现在我们可以通过这三个BeanFactoryPostProcesser的实现类来干预BeanDefinition(其实是修改BeanDefinition),接下来就是进入到BeanFactory的实例化Bean的过程了。
了解Bean的一生
上面我们已经对如何插手容器的启动有了了解,接下来就是对bean的一生来进行剖析了。
- 实例化bean对象。
- 设置对象的属性。
- 检查Aware接口并且设置相关的依赖。
- BeanPostProcessor进行前置处理
- 看看是否有InitializingBean,执行afterPropertiesSet方法进一步处理。
- 检查是否有配置init-method方法
- BeanPostProcessor进行后置处理(此时如果是prototype的,就交给调用者,接下来的就不归到容器管辖了)
- 注册Destruction相关的回调接口(singleton专属)
- 正式使用。
- 判断是否实现了DispossableBean接口
- 检查是否配置了destory方法。
下面是对这些步骤的详细解释。
在容器的内部,采用“策略模式”来决定采用什么方法来初始化bean,一般通过反射或者cglib(默认)来生成。
我们说的创建bean,其实在内部创建的是BeanWrapper,也就是对bean进行了包装,之前有提到过,spring其实内部的配置文件是string类型的,我们需要使用PropertyEditor来告知如何将String转化为指定的对象,被告知的就是这个BeanWrapper。这样就可以设置对象的属性了。
到了这一步,对象实例化已经完成,且已经设置好了属性(和依赖),现在就会去检查目前的对象,它有没有实现一系列的以Aware。如果有实现,则根据对应的接口来给bean注入对应的依赖,有些bean在使用的时候可能会需要自己的beanName(id),或者需要BeanFactory,就是在这一步完成的。
ApplicationContext靠的是BeanPostProcessor来处理Aware接口的,不过由于它和第三步紧紧挨着的,所以其实把第三步和第四步看成一块的也无妨。这里特别注意:BeanFactoryPostProcessor和BeanPostProcessor的区别,带factory的,是在第一阶段起作用,用来修改BeanDefinition的,而这个BeanPostProcessor则声明了两个方法,分别是postProcessBeforeInitialization和postProcessAfterInitialization,即上面的第四步和第七步所对应的“前置”和“后置”处理。同时这里也是我们使用AOP来进行对象代理的关键所在。其实还有一个特例,即InstantiationAwareBeanPostProcessor,它的位置其实在第0步,容器会首先检查有没有这个类型,有的话会让它来实例化对象。
```java
public interface InitializingBean {void afterPropertiesSet() throws Exception;}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
显然就是在第三步第四步之后,如果还需要设置,可以实现这个类。但是并不推荐使用这个,因为我们有更好的init-method,而不需要拘泥于afterPropertiesSet()这个写死的方法名。同理对应DispossableBean接口和destory方法。
10. 之前讲到过,单例创建的bean对象,会随着spring容器的关闭才消亡,但是spring它不会聪明到去自动调用这些回调方法,所以需要我们手动编写代码告知。
## 第五章
我很好奇为什么书里不把它放到第四章,和BeanFactory肩并肩的。本章说实话不涉及到核心,所以时间紧张的话可以跳过。
首先, ApplicationContext支持所有BeanFactory所支持的功能,而且还支持别的一些东西,要讲清楚这些“别的东西”,需要首先介绍下其他的一些接口。
### 资源策略
我们在使用spring框架的时候,肯定会使用到各种各样的资源,于是spring就为我们抽象出了一个`Resource`接口,然后一堆实现类用来针对各种各样的资源。
接着,又抽象出ResourceLoader这个接口来抽象如何定位资源的,里面有一个叫getResource方法来返回Resource。提供了两种实现类,分别是DefaultResourceLoader和FileSystemResourceLoader。
最后,还提供了一个叫ResourcePatternResolver的玩意儿来批量查找资源,本质上就是继承了ResourceLoader,并且新增了一个叫getResources的方法来返回资源数组。
最后的最后,我们的ApplicationContext其实继承了ResourcePatternResolver,所以它完全可以实现配置文件的加载。如果你的bean需要来加载资源,那么完全可以用之前提到的Aware系列接口,把ApplicationContext注入到bean里面,就可以实现资源的加载了。
### 国际化支持
Java中使用了Locale来代表不同的国家和地区,ResourceBundle则被用来保存特定于某个Locale的信息,可以通过getBundle (String baseName, Locale locale)方法取得不同Locale对应的ResourceBundle,然后根据资源的键取得相应Locale的资源条目内容。
而spring则是更近一步,抽象了MessageSource这个接口,我们只需要传入对应的Locale、资源的键和相应的参数,就可以获得对应的信息;而这在Java中通常是根据Locale获得ResourceBundle,在去ResourceBundle中查询信息。
ApplicationContext则是继承了这个MessageSource接口,所以它完全可以用来当做获取信息的类。如果你的bean需要实现国际化支持,那么就给它声明一个MessageSource,然后注入messageSource即可。
### 事件发布
Java自带一个类和一个接口,分别是EventObject类和EventListener接口。一个是事件,另外一个是监听器。监听器本质上是一个空接口,你在内部实现自己的方法,然后把事件传入进来,这样就能对事件进行操作了。最后就是发布事件,一般会定义一个事件发布者。
在spring中,ApplicationEvent 定义事件,ApplicationListener来监听容器内发布的所有事件。
ApplicationEvent有下面三个实现:
- ContextClosedEvent:ApplicationContext容器在即将关闭的时候发布的事件类型。
- ContextRefreshedEvent:ApplicationContext容器在初始化或者刷新的时候发布的事件类
型。
- RequestHandledEvent:Web请求处理后发布的事件,其有一子类ServletRequestHandledEvent提供特定于Java EE的Servlet相关事件。
而我们的ApplicationContext则继承了ApplicationEventPublisher接口,但是它其实是通过ApplicationEventMulticaster来实现的,所以在容器启动的时候就会检查是否存在名字为applicationEventMulticaster的bean,如果没有就创建一个。
### 多配置
emmm就是ApplicationContext支持多个配置文件....
## 第六章
这是IoC容器的最后一部分内容,主要讲了自动配置和基于注解的依赖注入。由于日常中用的非常多,所以本章非常重要。
### 自动依赖注入
#### @Autowired
之前讲到可以在xml文件中配置`autowire=byType`来进行依赖的注入,而`@Autowired`则是注解版本的依赖注入,只不过它要比byType更加灵活,也更加强大。
它的原理就是通过BeanPostProcessor(具体的实现类是AutowiredAnnotationBeanPostProcessor),在实例化对象的时候检查一下有没有`@Autowired`注解,有的话就需要进行处理。所以我们需要在配置文件中导入这个组件:`<bean class="org.springframeword.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>`,当然在实际中我们并不是这么写的,而是通过`<context:Component-scan base-package=".."`来做的。
#### @Qualifier
因为我们可以通过类型同时找到不止一种的bean,这个时候就需要这个注解来直接点名自己需要的是哪一个了。不过实际中写web的时候基本没用到过这个注释。
### JSR250注解
注意这个是Java实现的。分别是@Resource和@PostConstruct以及@PreDestroy.
@Resource并不是通过类型,而是通过bean的名字来找bean的,另外两个注解分别对应`init-method`和`destory`。
当然,JSR250也是通过`CommonAnnotationBeanPostProcessor`这个后置的处理器来完成的。我们在使用的时候同样通过bean的导入来实现的。
到了这里,其实我们可以自己推断出,`<context:component-scan base-package=".."`本质上就是为我们添加了这些注解所需要的BeanPostProcessor而已。
至此,IoC部分结束。接下来是AOP部分。
## 前六章的小小总结
在这前六章,作者向我们介绍了IoC的概念,并且介绍了spring框架中有名的BeanFactory和ApplicationContext。它们最为核心的就是解决两点:创建对象和解决对象之间的依赖关系。同时引出了容器的创建以及相对应的bean的生命流程。
## 第七章
AOP的作用这里就不多说了,网上到处都是。实现了AOP的我们给他们取了一个名字,叫AOL,即Aspect-Oriented Language。当然AOL可以和实现普通类的一致,也可以用单独的一种语言,比如AspectJ就是扩展自java的一种AOL,它和Java是两种语言。用AOL实现AOP组件,最后还是要集成到OOP实体中,这个过程即成为织入(Weave)。
### 静态AOP
静态AOP,第一代的AOP,AspectJ就是其代表。静态表现在,实现了相关的切入点之后,会通过编译器直接就织入到静态类中了。比如AspectJ就是用了ajc编译器,以字节码的形式编译到系统的各个功能模块里面,这样Java虚拟机只需要正常运行就可以了。
### 动态AOP
静态AOP的明显缺点是,如果我需要修改我织入的位置,那代码得重写,而且还得重新编译。于是就有了动态AOP,比如Spring AOP框架。同时,AspectJ融合了别的框架,也支持了动态织入,所以它既支持动态又支持静态。优点就是它是在系统启动后织入的,所以很灵活,易用性很高,缺点就是动态织入性能损失比较大,当然我觉得这个性能完全可以忍受。
#### 动态代理
定义了接口之后,我们可以动态生成代理对象,所以我们只需要实现动态代理的接口——InvocationHandler,然后指明要织入的位置。缺点就是必须要实现接口。
#### 动态字节码增强
我们的Java虚拟机实际上执行的class文件,而只要class文件符合标准,程序就能运行。所以我们可以对我们需要增强的类生成对应的子类,然后把我们的逻辑放到子类中,让程序在运行的时候执行的是这些被我们动态生成的子类。所以缺点就是,这些类不能是final的,否则无法继承了。
#### 自定义类加载器
类加载器的作用就是把class文件交给虚拟机进行执行,那这不就是刚好符合我们的需要么,我们自定义一个类加载器,并且在提交给虚拟机的时候“偷偷”做些手脚,这样就完成了AOP。
#### AOL扩展
AOP有各种概念,在这里就有一一对应的实体,然后我们就可以随心所欲了。缺点就是:AOL是一门新的语言,你确定要付出代价来学习吗?
### AOP的术语
JoinPoint:系统需要在哪些位置织入新的“功能”。基本上任意位置都可以作为joinpoint。
Pointcut:规定了JoinPoint的表达方式。一般会使用正则表达式来进行制定。
Advice:织入到JoinPoint的一些逻辑。这里其实可以和spring中的`@Before`这些注解对应起来。其中比较在意的就是Around Advice,它会对JoinPoint进行包裹,所以你可以在之前和之后都进行处理。
Aspect:AOP实体的代表,内部可以包含多个PointCut和Advice。
Weaver:把新增的逻辑切入到现有类中的,它可以是类加载器,也可以是编译器,而在Spring AOP中,最通用的则是ProxyFactory。
Target Object:就是原有的类,需要被增强的对象。
## 第八章
接下来就开始Spring AOP的旅程了。
首先需要了解设计模式中的代理模式,这部分就不写进来了。
### 静态AOP的囧境
要写的代码太多了,确实可以用设计模式来解决,但是如果有成百上千个对象,那么写代码会烦死人的。
### 动态代理
动态代理主要有2个类:java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler接口。具体代码就略了,比较简单。
我们这里的InvocationHandler,其实就是AOP里面的Advice。本质上,不同的Advice就是通过实现了不同逻辑的InvocationHandler来实现的。
### CGLIB
需要被扩展的类实现MethodInterceptor接口,然后重写intercept方法,思路和动态代理很像,只不过底层的实现不同。
## 第九章
AOP在Spring中目前是第二代,但是由于第一代AOP和第二代其本质上是一致的,所以先从这第一代开始吧。
### JoinPoint
spring只支持方法执行的JoinPoint,满足了二八法则。通过对方法的拦截,我们变相也可以实现对属性的拦截(getter和setter)。如果还是不够,那么可以通过AspectJ来提供帮助。
### Pointcut
```java
public interface Pointcut {
Pointcut TRUE = TruePointcut.INSTANCE;
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
其中的ClassFilter对应被增强的对象(通过match方法),而MethodMatcher则是相应的方法。
MethodMatcher有两个直接抽象类,分别是DynamicMethodMatcher(基本不用)和StaticMethodMatcher。
当然实际中肯定需要了解几种Pointcut的实现类。
- NameMatchMethodPointcut:只要函数名字(注意是函数名,不是函数的签名)一致,就认为匹配。
- JdkRegexpMethodPointcut和Perl5RegexpMethodPointcut,这两个都是通过正则表达式来处理的,且必须匹配整个方法签名。
- AnnotationMatchingPointcut:明显就是通过特定的注解来匹配的。
- ComposablePointcut:这个Pointcut可以支持与、并等操作。
- ControlFlowPointcut:最特殊的一个,它不仅能够指定被拦截的方法,还能指定拦截者是谁。当然性能比较差,只有真正需要的时候才用它。
以上是spring已经实现的Pointcut实现类,当然我们还需要学会自定义,无非是继承一下StaticMethodMatcher或者是DynamicMethodMatcher,重写一下它们的matches方法即可。
Advice
spring根据能否在目标对象类中的所有实例中共享这一原则,分成了两类Advice,per-class的和per-instance的。前者是对一个类的所有对象(中的方法)都生效,后者是对指定对象生效。
Before Advice
在相应的JoinPoint执行之前执行。一般它不打断正常的逻辑,除非加入了异常判断。只需要实现MethodBeforeAdvice这个接口,重写它的before方法即可。
Throws Advice
这个ThrowsAdvice是一个空接口,继承自AfterAdvice。
AroundAdvice
真正的类其实叫MethodInterceptor,它能实现各种advice所实现的功能。简单来说就是通过一个链把这些所有的切面连了起来,然后在invoke里面对其进行增强。
Introduction
上面的各种advice是在类的每个实例之间进行共享的,而这里的这个则是仅仅是对某个对象生效。
在spring中,靠的就是IntroductionInterceptor这个接口。实际中,一般会使用DelegatingIntroductionInterceptor实现类,只需要定义好新的接口以及该新接口的实现类,然后进行织入即可。
Aspect
在spring中,使用的是Advisor代表了Aspect,但是Advisor本身有点特殊,通常只持有一个Pointcut和一个Advice。主要有两大家族:PointcutAdvisor和IntroductionAdvisor。
PointcutAdvisor
DefaultPointAdvisor就是它的实现类,除了Introduction之外的advice,其余都可以通过它来使用。使用方法也很简单,创建好Pointcut和Advice之后,在构造方法里面丢入或者直接用set方法即可。
NameMatchMethodPointcutAdvisor则是加了限定,从名字中就看出来是对pointcut进行了限制,只允许NameMatchMethod;同理还有RegexpMethodPointcutAdvisor。
所以,默认情况下,用DefaultPointAdvisor就足够啦。
IntroductionAdvisor
IntroductionAdvisor只能对应于类级别的拦截,且advice类型只能使用Introduction的。默认也就一个DefaultIntroductionAdvisor。
Ordered
有些时候,我们需要让Aspect(Advisor)有顺序。默认情况下,先声明的bean顺序号小,优先级最高,越先拦截。所以我们需要在bean注入的时候,为order注入序号。
AOP织入
到目前为止,所有的组件都已经完全准备好了,这个时候只需要用“胶水”把它们粘合在一起即可。spring使用的是ProxyFactory。
1 | ProxyFactory factory = new ProxyFactory(proxyInterfaces.class); |
第一行指定了你需要增强的类名,第二个就是设置好Aspect(Advisor),然后织入即可,就是那么简单。
只要目标类实现了一个接口,那么ProxyFactory默认就是通过接口(也就是JDK的方法)进行织入的。这里需要重点注意的是,代理类它本质上是一个实现了被代理的接口的一个Proxy类,所以代理类和被代理类之间,不能进行强制类型转换。
如果目标类没实现接口,那么默认情况下就使用cglib进行类的增强。当然就算实现了某个接口,仍然可以指定其使用cglib,由于cglib是通过生成子类来完成代理功能的,所以是可以进行类型转换的。
ProxyFactory的本质
首先先从最基础的接口开始揭秘:
1 | public interface AopProxy { |
也就是通过这个接口,我们可以获得我们的代理对象,以此来增强原来的方法。
接下来spring中还有一个AopProxyFactory的接口,看名字就知道了就是用来生成AopProxy的,我们这里贴出它的具体实现类——DefaultAopProxyFactory的具体实现代码:
1 | public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { |
可以从上面的代码看出,根据不同的config信息,判断应该使用JDK的动态代理呢,还是cglib的动态代理。所以秘密还是出在了AdvisedSupport这个类中。我们不难猜测,这个类其实就是封装了生成代理对象需要的一些信息,然后就是借着这些信息来进行判断的。
然后回到我们要讲述的主角——ProxyFactory身上来,它其实就是间接继承了AdvisedSupport,所以它可以保存生成代理对象的所有需要的信息。同时它也拥有getProxy方法,所以它可以生成最终的代理对象。
ProxyFactoryBean
上面的这些代理对象,其实是完全独立于IoC容器的,我们有相关的类就可以,完全不需要容器的插手。
ProxyFactoryBean拆分的话,应该是Proxy+FactoryBean。而FactoryBean是一种特殊的bean,它不是返回自己,而是返回某个方法的返回值。
有了这个,再把PointCut,Advisor也加入到容器,这样就可以全部交给容器管理了。
自动代理
BeanPostProcessor可以在对bean进行实例化的时候进行干预,那我们为什么不设计这么一个BeanPostProcessor,让它在对象实例化的时候,为对象生成代理对象,返回这个代理对象,以此达成偷天换日的目的呢?核心的逻辑就是判断一下bean是否满足拦截的条件,满足就返回一个代理对象,不满足就老老实实生成一个对象。
也就是现在核心点又落到了,我们怎么让BeanPostProcessor知道拦截条件是什么呢?Spring AOP实现了两个类,分别是BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator。
BeanNameAutoProxyCreator就是当你的bean的名字满足要求的时候,就拦截下来,也就是你只需要告知这个类你要拦截的目标和你的拦截器,剩下的事情它会帮你搞定。而DefaultAdvisorAutoProxyCreator则更为全自动,你只需要把它加入到容器,它会自动去找到所有的Advisor,然后根据Advisor的信息来自己全自动完成。
还记得bean的生命周期么?之前提到过一个叫InstantiationAwareBeanPostProcessor,它才是真正的第一步,直接通过它来构造对象并且直接返回,短路掉后面的所有步骤,现在知道它用在哪里了吧?
第十章
spring2.0在第九章的基础上,进行了一些增强:增强了pointcut表达式的方式,现在不仅仅局限于正则表达式了;可以通过注解来简化开发了。但是本质上还是1.0的内容。
我们可以通过一个@Aspect的注解标注在一个类上,用来说明这个类就是用来声明AOP的。然后可以写一个方法,这个方法里面对应的内容就是你希望加入到被代理类中的,然后用对应的注解(比如@Around)标记这个方法。最后还需要指定这个对应的逻辑应该被加到哪一个类中去,同样使用注解,只不过这里的注解稍微比较复杂。
完成这些工作之后,我们需要让容器帮我们完成接下来的工作,可以通过编码,当然也可以往容器中注入一个AnnotationAwareAspectJAutoProxyCreator,然后它会自动收集容器中被标注了@Aspect的类,然后进行相应的处理。
复杂的pointcut
pointcut存在就是为了能够找到我们要找的切入点,它必须被标注在一个方法之上,且该方法的返回值必须是空。你可能会疑惑为什么要在方法上标注呢,这个跟方法似乎也没什么关联呀?其实你可以理解为方法名就是这个pointcut的变量名,用方法名就可以直接引用这个pointcut,而函数的public/private则相当于声明了该pointcut表达式的范围。pointcut的表达式则是重中之重,因为就是靠着表达式我们才能够精准找到对应的类。
- execution,最常用的标识符,比如可以写
execution(public void ClassName.MethodName(String))来匹配某个类下的方法,且该方法只有一个String参数。 - within,括号内写好类名,代表匹配该类下的所有方法。
- this和target,它们俩是一对,分别代表了代理对象和目标对象。一般配合上面的execution同时使用来加强判断。
- args,顾名思义,如果方法的参数满足了这个条件,那么就成功匹配。这里注意的是,这个是动态的检查参数的类型的,而execution则是静态检查的。
- @within,这个举个例子就明白了。假设我有一个注解是@clp,然后我只要使用
@within(clp),这样任何被我用@clp标注的方法和类都会匹配上。 - @target,就是@within的动态版本。
- @args,如果方法的参数是被对应的注解标注的,那么就匹配了。
- @annotation,我个人觉得和@within是一模一样的。
底层实现和前一章介绍的其实是一样的,只不过解析这一部分交给AspectJ这个第三方类库来处理,然后处理完了之后就可以知道是否匹配。
相对简单的Advice
通过在不同方法上标注不同的advice注解,我们就可以轻松实现织入逻辑的编写。
@Before,后面的value中加入PointCut表达式;或者如果你已经写好了PointCut表达式,那么也可以直接用方法名来代替。在某些情况下我们可能需要目标对象方法处的参数,那么我们可以在方法的参数中加入JoinPoint,通过它可以获取到原始方法的参数。@AfterThrowing,这个可以用在当异常发生时,比如需要写到某个日志中的时候使用。可以通过这个注解的throwing参数指定异常对象,然后写入到方法的参数中。如果你除了异常信息,还想获取原始方法的信息,那么还是可以用JointPoint参数。@AfterReturning,这个注解也有一个参数是returning,它可以把原方法的返回值给你绑定到一个参数中,这样你就可以直接使用这个返回值了。@After,这个就类似于try/catch/finally中的finally,确保一定会执行,我们一般可以在这里执行系统资源的释放等。@Around,前面的几个JointPoint参数都是可选的,而这个因为没有指定切入逻辑的位置,所以必须要有ProceedingJoinPoint,通过调用它的proceed方法,就可以执行原方法了。
剩下还有一种基于Schema的配置方式,我这边打算是跳过了。
第十一章
最佳实践的部分,不能仅仅知其然,知其所以然,还需要在实际中能够使用它。
异常处理
在写web程序的时候,遇到某些错误的时候,我之前的做法是搞一个enum的类,然后把所有的错误信息和错误提示代码都写到里面,最后需要的时候就直接使用它。但是其实我们可以把所有的异常信息也放到一起,利用AOP来更加简单快捷的实现。这里说的异常是unchecked exception,即程序无法处理的异常,我们把这些异常放在一起写入日志,通知管理员。
安全检查
在web应用领域中,filter其实就是相对应的AOP,而且spring家族中也有spring security这一套成熟的框架可以用。
缓存
缓存本身也可以作为aop来加入到整个系统中,简单的伪代码见下:
1 |
|
第十二章
还有一个问题,我们需要解决一下。如果一个类,它的方法调用了自己的方法,如下图所示,那么会出现什么问题,我们应该怎么解决呢?
假设下面是我们的业务类:
1 | public class MyTask { |
可以看到很简单,就是两个方法,其中方法1调用了方法2。然后我们又声明了一个Aspect:
1 |
|
也是非常简单,就是在方法执行之前加入一句日志开始,方法结束之后加入一句日志结束,且可以看到上面匹配的时候用到了通配符,所以两个方法其实都会匹配到,然后执行一下。
1 | public class Main { |
这里我们只执行了方法1,结果是
日志开始
do something in method2
do something in method1
日志结束
这个和我们预估的不一致,因为我们希望不论是method1还是2,在调用的时候都应该加上日志开始。稍微分析一下就知道原因,是因为我们调用方法2的时候,用的其实是目标对象本身,而没有用到代理对象,自然不会有相应的逻辑了。spring aop提供了一个AopContext来获得代理对象,所以只需要修改一下即可:((MyTask) AopContext.currentProxy()).method2();,最后还需要设置一下ProxyFactory,设置暴露代理为true即可。factory.setExposeProxy(true);这样就能按照我们的思路进行下去了。
总结
到此,书中讲述IoC和AOP的部分就结束了,我觉得有必要稍微总结一下这部分学到了什么。
首先,自然是IoC部分,在这一部分,我们需要知道,什么是IoC容器,它解决了什么问题。其次我们需要知道依赖注入的两种方法,分别是构造器注入和setter方法注入(接口注入当它不存在好了),然后是解决依赖注入的具体实现,这里可以有三种,分别是直接编码、xml文件(最强大)还有注解的方法(最方便),通过它们我们完成了依赖的注入。
接下来就是两种Spring的容器,BeanFactory和ApplicationContext,BeanFactory这部分学习了容器是如何创建,以及bean的生命周期;在ApplicationContext这部分扩展了资源的引入、国际化的支持等(个人觉得没什么用)。
接下来就是第二部分,AOP了。AOP中最为重要的两个概念应该是Advice和PointCut,分别代表了程序的逻辑和这部分的逻辑应该加入到哪个方法中。我们在这一部分主要就是理清楚概念,然后也是三种方式来具体实现,分别是通过直接编码,通过注解和通过Schema(更为先进的xml)来解决,最后还是倾向于注解。