前言
琢磨了一段时间整合Spring Security ,及Spring Security的使用,处于能搭出来却毫无安全感的情况。归根到底还是对spring 框架的核心思想缺乏体系化的理解。在豆瓣书籍中找到了评分高达9.1的《spring揭秘》,这本书09年出版,现已绝版,用到的spring 版本最多仅到2.5,虽然时间久远,但是Spring Security彼时已经在spring大家族中。如果用发展的眼光理解框架,我认为无论是spring boot的自动装配还是spring cloud的整合,了解spring的核心架构思想让我更有安全感。以下使用笔记的方式加以自己的思考,回顾关于IOC
的内容。另外,以下内容可能需要了解爬虫业务。文章以回答以下疑问作为思考方向进行展开:
- 控制反转,控制的是什么?反转的是什么?
- 控制反转有什么好处?
- spring 如何介入控制的?
1. 控制反转 Inversion of Control (IOC)
都说控制反转,那反转的是什么呢?
先得走近一个简单的业务场景:爬取多家新闻社的新闻,近乎实时展示给客户。
/**
* 新闻的生产者
*/
public class FXNewsProvider {
// 新闻爬虫逻辑, 接口类型的声明
private IFXNewsListener newsListener;
// 持久化操作逻辑, 接口类型的声明
private IFXNewsPersister newsPersister;
public FXNewsProvider(IFXNewsListener newsListener, IFXNewsPersister newsPersister) {
this.newsListener = newsListener;
this.newsPersister = newsPersister;
}
public void getAndPersistNews() {
// 爬取新闻列表
String[] newsIds = newsListener.getAvailableNewsIds();
for (String newsId : newsIds) {
// 爬取具体新闻
FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
// 持久化操作,可以是存进数据库
newsPersister.persistNews(newsBean);
// 爬虫行为的后续处理
newsListener.postProcessIfNecessary(newsId);
}
}
}
// 应用层代码
FXNewsProvider p = new FXNewsProvider(new DowJonesNewsListener(), new DowJonesNewsPersister());
p.getAndPersistNews();
1.1 书中没提到的 —— 面向对象的编程习惯
以上代码 getAndPersistNews
抽象了业务过程,用面向对象的角度来看由以下解释:
现在关心的是以下语句:
// 定义:新闻爬虫逻辑
private IFXNewsListener newsListener;
// 定义:持久化操作逻辑
private IFXNewsPersister newsPersister;
---------------------------------------------------------
// 应用层代码指定具体的实现
FXNewsProvider p = new FXNewsProvider(new DowJonesNewsListener(), new DowJonesNewsPersister());
p.getAndPersistNews();
生产者定义的时候,将依赖的爬虫逻辑和持久化逻辑指定为接口,把以上两个依赖剥离出应用层代码。为什么这么做?—— 多态
书中并没有提到,这是一种多态的代码实现,在spring框架出现前就已经很流行了。
因为需要爬取的新闻门户,不止DowJones(道琼斯)一家。反之,如果明确只针对道琼斯新闻的爬取,并不需要引入多态,直接写进应用层代码:
public void main() {
DowJonesNewsListener newsListener = new DowJonesNewsListener()
DowJonesNewsPersister newsPersister = new DowJonesNewsPersister();
// 爬取新闻列表
String[] newsIds = newsListener.getAvailableNewsIds();
for (String newsId : newsIds) {
// 爬取具体新闻
FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
// 持久化操作,可以是存进数据库
newsPersister.persistNews(newsBean);
// 爬虫行为的后续处理
newsListener.postProcessIfNecessary(newsId);
}
}
多态 的引入增加了代码量,与此同时,究竟带来了什么好处呢?
1.1.1 多态 —— 拓展
业务逻辑中出现的对象能够在不改以上一套业务逻辑的前提下,得到拓展
// 定义:新闻爬虫逻辑
private IFXNewsListener newsListener;
// 定义:持久化操作逻辑
private IFXNewsPersister newsPersister;
---------------------------------------------------------
// 应用层代码指定具体的实现
FXNewsProvider p = new FXNewsProvider(new DowJonesNewsListener(), new DowJonesNewsPersister());
p.getAndPersistNews();
现在如果要新增实现:纳斯达克的新闻爬虫,应用层只需要加两行代码:
FXNewsProvider n = new FXNewsProvider(new NasNewsListener(), new NasNewsPersister());
p.getAndPersistNews();
1.1.2 多态 —— 解耦
在对爬虫业务流程进行整体升级的时候,共用一套业务逻辑就可以只改一个代码块,比如先看看有没有3分钟前的缓存,有的话就不实际访问门户网站。
// 不确定newIds从哪里,进行延时赋值
String[] newsIds = null;
// 查看当前爬虫是否有缓存的新闻
if (newsListener.hasCache()) {
newsIds = newsListener.getCacheAvailableNewsIds();
log.("缓存命中,newIds: {}", newsIds);
} else {
// 爬取新闻列表
newsIds = newsListener.getAvailableNewsIds();
// 爬取新闻
FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
// 持久化操作,可以是存进数据库
newsPersister.persistNews(newsBean);
// 爬虫行为的后续处理(加入缓存)
newsListener.postProcessIfNecessary(newsId);
}
- 以上代码,就将所有参与到爬虫的业务逻辑都升级成了缓存支持。
- 显而易见的好处是,newsListener的行为被定义成不常修改的规范时(爬取可用新闻列表 -> 爬取具体新闻 -> 对新闻持久化操作 -> 后续处理),一旦升级流程(如查询缓存),业务逻辑还是清晰的,具体实现交给实现类去写。这样就将可重用的业务逻辑和具体实现细节解耦了。被解耦的实现对象,还可以被容器持久化——后文将提到——IOC容器。
1.2 反转的是什么?
1.2.1 阐述
产生:是引入了面向对象编程后产生的变化。
定义:后续补充。
- 面向过程:
- (查看是否有内存缓存 -> 爬取道琼斯的可用新闻列表 -> 爬取道琼斯具体新闻 -> 将道琼斯新闻存入MySQL数据库 -> 释放MySQL数据库连接)
- (查看是否有Redis缓存 -> 爬取纳斯达克的可用新闻列表 -> 爬取纳斯达克具体新闻 -> 将纳斯达克新闻存入Redis数据库 -> 释放Redis数据库连接)
- 面向对象:
- 定义标准:查缓存-> 爬取可用新闻列表 -> 爬取具体新闻 -> 对新闻持久化操作 -> 后续处理
- 定制化道琼斯、纳斯达克在以上标准中的实现。
- 差异:
面向过程main方法需要很清晰得把所有步骤写在一段代码块上。
面向对象main方法,使用多态,解耦后把不同的步骤分别写在不同的代码块上。体现的是一个依赖关系,剥离了具体实现
1.2.2 回答问题
面向对象把控制具体实现的权利给到了抽象接口的实现类,而不是自己。本来应该自己做的事,交给了抽象接口的实现类去做,这种权利的反转就叫做控制反转。大白话就是把实现外包出去了,每个外包团队是可以不知道整个业务链条即可进行开发的。
1.3 IOC 控制反转带来的好处
- “外包”出去的代码是可以被容器持久化的!
假设你希望,“外包”出去的代码被调用时早已准备就绪的,也就是处理业务的速度需要提升。IOC容器的引入就再适合不过了。具体的调用过程
- 我要爬取道琼斯新闻列表缺少套件,告诉IOC容器:给我一个爬虫 + 一个数据持久化方案
- IOC容器看了看自己有的爬取器,给了道琼斯的爬虫器和道琼斯定制的持久化方案。
- 我用IOC给的套件完成了爬取工作。
如果一天执行10000次调用过程,IOC容器可以把持久化的道琼斯的爬虫器和道琼斯定制的持久化方案持续交给代码执行,而不用每次都从头执行一遍代码。
- 大大节省了9999次内存空间的开辟及销毁的开销,牺牲的只是O(1)复杂度的持久化空间
2. Dependency Injection 是时候谈到依赖注入
- 重复上面提到的:使用多态,解耦后把不同的步骤分别写在不同的代码块上。体现的是一个依赖关系,剥离了具体实现
- 现在面临的问题:具体实现怎么进入控制反转的过程
- 解决办法:控制反转的时候提供入口,IOC容器借助入口注入逻辑 —— 这就是依赖注入
2.1 依赖注入的方式
根据提供被反转的具体实现的注入入口的不同,分成了以下三种注入方式
2.1.1 构造器注入
FXNewsProvider p = new FXNewsProvider(new DowJonesNewsListener(), new DowJonesNewsPersister());
//更恰当的语义 new FXNewsProvider(IOC.getBean(DowJonesNewsListener.class), IOC.getBean(DowJonesNewsPersister.class))
2.1.2 setter注入
FXNewsProvider p = new FXNewsProvider();
p.setListener(new DowJonesNewsListener());
p.setPersister(new DowJonesNewsPersister());
// 更恰当的语义 p.setXXX(IOC.getBean(XXX.class))
2.1.3 接口注入
涉及到设计模式的知识,较前两种复杂,并且书的作者不提倡使用(开发者注入自己的Bean)。换言之,其他用法:网上有工具类用到了这种用法(注入Spring的Bean)
打开此连接并拉直文末
3. 工厂模式
3.1 工厂模式的必要性
- 不使用工厂模式的应用层代码 (A)
// 没有用工厂模式
JavaVideo javaVideo = new JavaVideo();
javaVideo.produce();
- 使用工厂模式的应用层代码(B)
接口:VideoFactory
, 实现类:PythonVideoFactory
接口:Video
,实现类:PythonVideo
// 用了工厂模式
VideoFactory factory = new PythonVideoFactory();
// 抽象了返回产品
Video pythonVideo = factory.getVideo();
pythonVideo.produce();
A代码比B代码简单,原则上,用A的代码就能覆盖业务实现,就不需要使用B代码。B代码增加了代码的复杂度,又带来了什么好处呢?—— 抽象 —— 对象用接口作为返回值时,下游使用该对象就能进行很好的拓展。来看下面的场景:
- 没有工厂模式下,增加下游方法(C)
// 没有用工厂模式
JavaVideo javaVideo = new JavaVideo();
javaVideo.produce();
nextStepForJavaVideo(javaVideo);
public static void nextStepForJavaVideo(JavaVideo video) {
System.out.println("nextStepForJava... ");
}
- 使用工厂模式,增加下游方法(D)
// 用了工厂模式
VideoFactory factory = new PythonVideoFactory();
// 抽象了返回产品
Video pythonVideo = factory.getVideo();
pythonVideo.produce();
nextStepForAnyVideo(pythonVideo);
public static void nextStepForAnyVideo(Video video) {
System.out.println("nextStepForAnyVideo... ");
}
工厂模式下,应用层调用者需要关心使用哪一个工厂的实现,工厂也是一个接口。在Spring的框架下改写上面的代码:
- Spring 使用工厂模式改写代码 (E)
// IOC容器依赖注入
@Component
public class Test {
@Autowired
VideoFactory factory;
public void test() {
// 抽象了返回产品
Video video= factory.getVideo();
video.produce();
nextStepForAnyVideo(pythonVideo);
}
public static void nextStepForAnyVideo(Video video) {
System.out.println("nextStepForAnyVideo... ");
}
}
此时,业务上需要什么类型的Factory就不需要显式的new出来了,把工厂交给Spring注入
3.3 Spring
后记
- IOC的思想是建立在面向对象的编程模式下的。IOC思想提倡我们做两件事:
- 提供业务流程规范:展示业务流程中实体的依赖关系,剥离具体实现,代码层面上解耦。
- 提供具体实现的入口。利用IOC容器持久化具体实现,被解耦的具体实现对象在代码运行时注入
- 为IOC提供具体实现,除了依赖注入,还有依赖查找,目前先不展开