目录
一、大名鼎鼎的Spring框架
1.1 为什么需要Spring?
谈及为什么需要Spring,就要搞清楚,我们的需求是什么。
1.1.1 敏捷开发模型的起源
在20 世纪60年代以前,实现一个系统通过人机交互的模式,前台在交互时调用后台的程序集合去响应交互,而后台开发者,往往是一个接口写到底:
这样的编写方式在软件工程方法学之前十分广泛,但久而久之,随着系统的功能膨胀,程序员们突然发现修改功能的代价越来越沉重,比如数据库准备操作,一旦有所改动,n个功能就需要改n处,并且出现漏改错改需求无法得到满足的情况。于是,20世纪60年代中期,爆发了众所周知的软件危机。
为了解决问题,在1968、1969年连续召开两次著名的NATO会议,并同时提出软件工程的概念。
随着软件工程方法学的不断完善,总结了一个六字真言:高内聚、低耦合。
模块化逐渐深入人心。众多模块化思想,经历的各大项目的实践,MVC突出重围逐渐成为主流,那之后程序员们设计一个系统不约而同的会分为三个模块:
这一的模式实现了控制、业务、视图的逻辑解耦,以封装好的数据集作为数据流传输。这样控制了软件危机的扩散,极大地提高了系统的生命周期。
后续的程序员不断实践MVC设计模型,总结了一套敏捷开发的体系:前后台分离,后台Controller-Service-Dao(Entity)三层分离,Entity作为数据流传递,业务层间彼此隔离。自此出现了前台(View层)与后台(控制与业务)的解耦。
1.1.2 选择Spring的原因
为了实现敏捷开发体系:需要Controller依赖Service,Service依赖Dao,这样就会出现一个现象:
程序员为了响应前台界面的所有指令,只能在Controller1中,将Service1、Service2、Service3缓存到内存中待命响应,同理Service1需要Dao1缓存到内存中待命响应,其他同理。
我们不难从此发现2个问题:
- 如果用户在某段时间只发出有关Service1的指令,那么Service2、Service3缓存到内存中在某段时间就是内存泄漏(因为在这段时间,满足需求的所需内存小于实际使用内存)
- Service1需要Dao1缓存到内存中,Service2、Service3也需要Dao1缓存到内存中,即使三类业务调用均匀,整体上看起来没有内存泄露,但Dao1的数据在内存上重复缓存了2次。这是内存浪费行为。
随着系统功能的不断增加,内存泄漏与内存浪费问题会让本就不富裕的内存空间有效利用率逐渐降低。需求便应运而生:如何避免内存泄漏与内存浪费?
我们肯定希望内存在需要时被占用,在不需要时被释放。通过控制对象的生命周期:我们可以在程序执行到到对应对象时,将对象初始化,使用结束后释放内存上的对象实例。
忘记初始化怎么办、使用完毕忘记回收怎么办······由于人的不稳定因素,类似这样流程化的环节就应该被机器工程化,需求便由此产生:开发者需要委托中间件控制对象的实例化!
Rod Johnson是第一个高度重视以配置文件来管理Java实例的协作关系的人,他给这种方式起了一个名字:控制反转(Inverse of Control,IoC)
基于以上想法,Spring便应运而生。
我们希望将对象的生命周期的实例化委托给 Spring 去控制,
以上便解释了为什么需要Spring:为了避免内存泄漏与内存浪费,需要控制对象生命周期的实例化,人是马虎的,这个过程不如交给中间件处理。
1.2 如何实现控制反转(Ioc)?
Spring中,对于 IoC 来说,最重要的就是容器。
容器管理着 对象 的实例化。
Q:Spring 如何设计容器?
我们先谈一下自己的实现思路:可以把未来可能使用到的类放入一个集合中,在程序执行到对应对象时,遍历这个集合看看是否有对应的类,如果有说明,程序员委托中间件来控制对象的生命周期,那就将这个对象自动地实例化,并在使用结束后GC掉。(就好比,我们在程序里合适的位置撒播种子(Bean),时机来到时,Spring便使Bean成长为一个对象。)
我们通过一个项目的生命周期来看看Spring作为中间件在做什么。
项目启动前,
->通过引入Spring框架的Jar包,“安装”Spring中间件。
->通过xml配置一个静态映射关系(Bean->Entity),我们要“告诉“Spring,哪些是需要委托给它来控制的(方式1)
<bean id="myClass" class="org.mypackage.MyClass"/>
->然后为Spring指路,告诉它哪里可以找到映射关系的配置文件(方式1)
public class Main{
public static void main(String args[]) throws Exception{
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext("/application-context.xml");
}
}
->通过注解一个类配置静态映射关系(Bean->Entity),我们要“告诉“Spring,哪些是需要委托给它来控制的,这种方式不需要配置文件 (方式2)
@Bean
Class MyClass{
}
下面会逐步介绍方式1、2的不同之处。
准备完毕,项目启动,
执行程序,Spring通过ApplicationContext启动。
1.2.1 初始化容器
Spring首先会加载BeanFactory实体类DefaultListableBeanFactory(定义的 BeanDefinition 的IoC容器,以下简称BeanFactory),容器由诸多ConcurrentHashMap实现,参见DefaultListableBeanFactory:
/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
对ConcurrentHashMap不熟悉的读者,可阅读:Java设计思想深究----多线程与并发(图文)_kevinmeanscool的博客-CSDN博客
1.2.2 准备Bean
注册的过程为:
->在 BeanFactory 中,打算registerBeanDefinition(),委托BeanDefinitionReader呈递BeanDefinition给工厂。
->收到工作的BeanDefinitionReader需要阅读BeanDefinition的Document,并将准备文档(Document)的工作委托给BeanDefinitionDocumentReader。
->收到工作的BeanDefinitionDocumentReader需要Context才能制作Document,定义了一个程序需要的资源集合Resources(如 xml、注解等),委托原生I/O框架将静态资源的Context加载到集合Resources;
->Resources整理完成后,BeanDefinitionDocumentReader以Resources中的Context制作Document,呈递给BeanDefinitionReader;
->BeanDefinitionReader接收到文档(Document)后,解析DOM对象为BeanDefinition呈递给BeanFactory;
->BeanFactory接收到BeanDefinition并注册BeanDefinition到Ioc容器中。
上文描述了静态资源为xml、注解等。
如果静态资源为XML,那么就是XMLBeanFactory+XMLBeanDefinitionReader+XMLBeanDefinitionDocumentReader组合
原理:利用I/O将指定路径的XML解析为Resource->Document->BeanDefinition->Ioc容器注册
如果静态资源为注解,那么就是AnnotationBeanFactory+AnnotationBeanDefinitionReader+AnnotationBeanDefinitionDocumentReader组合
原理:利用I/O将项目所有Java类文件名递归遍历缓存集合A->遍历集合A利用反射获取Class,利用Class获取Annotations,将被@Bean注解的类缓存集合B->Resource->Document->BeanDefinition->Ioc容器注册
实现了工厂模式的同时,大程度解耦。
1.2.3 注册Bean到Ioc容器
我们上文知道了准备好的BeanDefinition都是用于Ioc容器注册的,当然Ioc容器注册不是简单的map.put(BeanDefinition)。下面我们深究一下registerBeanDefinition():
->首先,会进行准入校验,这是开发规范
->是否单例模式,这个是可以通过字段判断,初始化时默认为单例模式:
allowBeanDefinitionOverriding = true;
实例冲突,通过条件判断,决定是否抛出异常
->实例不冲突的情况下,便put操作。但特别注意,这里使用了synchronized关键字,也就是对Map对象施加重量级锁,这是因为Concurrent在写操作下不是线程安全的,详情此文有详细解释:
Java设计思想深究----多线程与并发(图文)_kevinmeanscool的博客-CSDN博客
->因此我们可以总结,Ioc容器默认单例,并可以设置为多例。
1.2.4 实例化Bean
Ioc容器注册完成后,是否决定实例化Bean与BeanFactory类型、是否单例、是否懒加载有关。
懒加载就是在Spring启动时,对于lazy-init(默认为false),为true的Bean不进行实例化,在第一次使用时实例化,注解@Lazy与@Bean等搭配使用。
如果BeanFactory类型为ApplicationContext、Spring单例模式下,并且lazy-init为false(默认是false,所以可以不用设置),则启动的时候就实例化该Bean,并且将实例化的Bean放在一个ConcurrentHashMap中,下次再使用该Bean的时候,直接从这个集合中取。
除此情况外,均在第一个使用Bean时实例化。
1.2.5 使用Bean
Spring提供了2种对象关联实例化Bean的方法:DL与DI
-
依赖查找(DL Dependence Lookup)
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/application-context.xml");
MyBean bean = applicationContext.getBean("myBean")
依赖项查找是指 对象本身试图查找依赖项。即静态指定对象试图依赖的Bean。实现原理很简单:1.2.3里我们知道了Ioc容器默认单例,bean引用实际上是指针变量 bean*,bean* = getBean(),即让指针变量的指向Ioc容器中的对象实例。
-
依赖注入(DI Dependence Injection)
@Autowired
MyBean bean;
依赖注入是指 在初始化实例时属性自动绑定。即动态指定对象试图依赖的Bean。实现原理也不难:1.2.2中我们知道了注解实现Ioc注册的原理。在完成Ioc注册后,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,利用反射机制,这个处理器会扫描Ioc容器中的Bean的字段Fields,通过判断Field.Annotations里是否有@Autowired、@Resource或@Inject等注解来判断是否需要自动注入。如果符合自动注入,则先判断Ioc容器中时候存在Bean,存在则直接注入(Field.set)。如果不存在Bean,则抛出异常。
还有一种方式是将注入注解加在setter器上,称作setter注入,即扫描到注解的对象是Method时,后置处理器会直接执行这个方法,读者可以试试:
@Autowired
public void test(){
System.out.println("====hello");
}
到此为止,对象的加载-连接-初始化都已经由Spring替开发者完成。
Spirng有2个明显的优势:
1.提供了懒加载模式的选择,最大可能的避免了内存泄漏的问题;
2.默认单例模式下,不同场景所需相同实例所需内存与实际使用内存相等,避免了内存浪费的问题。
1.2.6 Bean的回收
我们使用Spring管理对象的生命周期的实例化阶段。
Spring单例模式时,Ioc容器中的Bean都是强引用,不会被回收的,大部分情况下会进入老年代。
其他情况时(多例),卸载阶段,Spring也要依赖JVM的GC机制:Java设计思想深究----JVM垃圾回收(GC GarbageCollection)(图文)_kevinmeanscool的博客-CSDN博客
我们不难发现,Bean的生命周期处理,与JVM对于类的生命周期处理十分相似:
二、为什么需要SpringMVC?
一如既往,我们要先知道我们的需求是什么。
2.1 Servlet技术解放了B/S模式的生产力
1.1与1.2中介绍了MVC敏捷开发模型和Spring容器原理,对于本地服务的系统已经足够。
但随着21世纪后互联网的不断普及,B/S模型系统的需求(Browser/Server 浏览器/服务端)日益增长。1个B/S模型的流程大致为:
->Web客户端与服务端约定通过HTTP协议通信
->浏览器封装请求的http报文发送给服务端
->http报文通过网络路由到指定的服务器
->Web容器接收到http报文后根据HTTP协议解析报文,封装为一个request对象
->服务端根据request对象中的信息处理业务
->处理业务完成后,将结果信息封装为response对象
->根据HTTP协议封装response对象为http报文
->http报文通过网络路由到指定的Web客户端
->浏览器解析http报文,回显到页面上
我们可以发现,这是一个闭环的通用模型,如果开发者每次处理一个请求,就需要完整的编写一次上述逻辑,重复的过程就是方法。于是,将每个步骤做成接口,对应层的开发人员调用对应层的接口即可。于是,逐渐分离出前后端开发。
前端专注于Web端的View逻辑:
在完全分离的前后端项目中,前端还会承担路由选择的任务:
不难发现,前端实际上并不需要JVM的管理,逐渐前端被脚本语言所占领市场。
后端就负责服务端:
不难发现,后端依旧需要掌握HTTP协议、报文格式转换(报文是纯文本)、处理请求、实现业务逻辑等。
对此,不同的框架逐渐解决后端的重复工作的烦恼:
- 需要掌握HTTP协议?
Java提供了原生的Servlet接口。servlet是一个运行在Web服务器中的小型Java程序,servlet接收并响应来自Web客户端的请求,开发者无需再受到HTTP协议的烦恼;
- 报文格式转换?
Json等框架提供了报文与Java对象间的转换接口,中间的解析过程无需开发者担心;
自此,后端开发者将专注于请求处理与业务逻辑。
(ps.但并不代表后端开发不需要了解上述技术,学无止境~)
请求处理,主要是要将请求与处理逻辑准确对应起来,我们自然地可以想到一种映射实现方案:
->前端发送的请求信息中封装需要的逻辑,比如 <localhost?api='addItem'>;
->后端利用ServletAPI,接收到request对象后,判断api字段,调用对应的业务接口;
->然后就是业务处理后利用ServletAPI返回response对象。
实现的方法,用if或switch都可以实现。
但是,这样业务代码间,将具有很强的耦合。
Spring对映射的处理方法就很巧妙,利用配置文件或注解。所以我们便有了一个需求:委托中间件管理请求,实现http报文自动封装requst,并关联映射接口并执行,返回的结果自动封装response,通过映射原路路由。
2.2 SpringMVC如何设计
于是Spring便基于Servlet技术,将Spring的管理范围扩大,最大可能的减少内存泄漏与内存浪费问题。并将映射实现方案利用利用配置文件或注解实现。也就是说,后端开发者甚至只需要配置映射关系,即可让请求报文通过Spring精准关联到后端业务接口,连Servlet都不用了!SpringMVC便应运而生。
如果读者掌握了1.2的有关Spring的内存,将十分容易理解SpringMVC的设计思想:
->启动项目时,将静态依赖通过BeanFactory注册到Ioc容器中:
(Bean容器、urlHandlerMap容器:扫描@Controller等控制层下RequestMapping注释注解的方法,如果方法所在的类没有url,则直接存入方法url,如果有url,遍历前在方法url左侧拼接类的url。出现重复的url会采取异常抛出)
->设置Dispatcher,拦截http请求报文,并利用Servlet API对报文进行拆(request)封(response)
->封装好request对象后,遍历urlHandlerMap的keySet,匹配url(先遍历优先算法:),获得url->Method映射,填充参数,反射执行。
->返回结果,Dispatcher获取后封装为response,利用ServletAPI封装为http响应报文,发送
//urlMap是LinkedHashMap,保留了存入顺序
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
private final Map<String, Object> urlMap = new LinkedHashMap<>();
}
以上结束了对SpringMVC实现原理的解释,通俗而言SpringMVC属于整个Spring框架的SpringWeb部分内容,因此属于Spring。
使用 SpringMVC有如下好处:
1.获得了SpringIoC的2个好处(见1.2.5)
2.降低了开发人员的研发成本,业务与请求控制解耦,更专注于业务的研究。
当然,dispatcher存在着高强度并发访问的场景,因此如果将dispatcher进一步委托给线程池,那么将有效的应对并发场景,这样观察时,线程池就是一个http报文容器,称作Web容器。
SpringMVC+Web容器就组成了:
常见的Web容器:Tomcat(免费开源)、 weblogic等。
三、SpringBoot:前人植树,后人乘凉
(待更新)