抛开技术细节,分享五年职场的个人感悟及道理总结

个人简介

1. 过程

我从毕业至今五年一直在目前的公司工作着,从部门最开始的十几人团队发展到现在的将近两百号人,几年了没换工作不是因为习惯舒适区,相反这一路过来都是不断的突破,因为团队在快速壮大,面临的问题也在不断升级,根本没有太多的时间可以休息整顿。当然,上百人的增速那也是部门而已,我的团队人数增速并不大,只是从两三人到目前的二十人,不过项目金额却是从最开始的十万级到目前的千万级,项目难度升级倒是挺大的。

2. 自我评价

优点

  1. 能快速准确的理解需求
  2. 对技术方案有较好的判断及把控
  3. 性格不是主动外交型,面对外部沟通内心会有一定抵触,但还是能较好的完成大部分沟通协调工作
  4. 相对比较容易接受变化,不会太死板

缺点

  1. 技术不够扎实。太多协调处理的事消耗精力,难以全身心投入到技术沉淀
  2. 一直待在一家公司里,少接触其他公司的文化及流程

其他

  1. 一直有做产品的心,奈何团队的几个产品都没能成功落地,工作的方向还是项目型
  2. 内心向往互联网方向,希望最后还是能进入互联网行业
  3. 以架构师、CTO的目标前进,一方面永不放弃技术,另一方面在面对管理协调等“杂事”时,也会尽量调整心态去接受。

3. 当前项目

一直都在想,如果以当前能力再去做小项目简直是游刃有余,能做得漂漂亮亮,但现实是一直以来都被推着往上走,根本不能回头,不过也正因如此才有不断的进步。目前在做的是一个省级项目,虽然概括起来项目都是增删查改,但背后要处理的问题复杂度与普通项目对比绝对不是一个量级的。
在这个项目过程中,我要面对的相关方很多:

  1. 客户——需求沟通,进度反馈(虽说有项目经理,但也难避免要到一线直接沟通)
  2. 厂商——方案沟通,进度跟进
  3. 机房资源——确认使用方式,明确责任边界
  4. 内部团队——带领冲刺、安抚情绪、技术方案确认
  5. 外部团队——项目很难保证开局就有充足的资源投入的,这就避免不了去外部协调资源解决燃眉之急

这个项目过程一直保持高强度工作,可以说是每天被榨干榨尽,关键是现在项目还没结束。。。

个人感悟及道理总结

1. 工作的本质就是解决问题

工作的本质就是解决问题,而你的位置决定了你面临的问题复杂度。按照事情发展的规律,一件复杂的事情总会出现大大小小的状况外的问题,没问题才是不合常规,当出现问题的时候尽量保持淡定和积极的心态,不必过分焦虑和惊慌,因为你工作的意义就是来解决问题的。当你能坦然面对时,这个问题的难度已经减半。

2. 最难的是从混沌到有序

事情最难的是从混沌到有序的过程,你看到的成功产品看似简单,但每一个功能、每一个细节都是由一个个大大小小的决策构成,每个决策的背后都会对应大量的分析、讨论、协调与纠结的过程。每一个能带来价值的产品或项目都是伟大的,背后都是一帮人努力的结晶。

3. 要做成一件事不是那么简单的

要做成一件事,就需要去促使这件事的往前推进,这个过程会有很多的阻力,需要协调各方各面。有时候当事情完满结束时,回过头想好像也并不是那么难,但如果在过程中把每个细节都记下来,那肯定是每天都面对着各种奇奇怪怪的问题。

4. 有人的地方就有江湖

有人的地方就有江湖,有江湖的地方就有是非。曾经我们还是个小团队的时候,大家都很善良淳朴,但是当团队壮大后,一切就都不那么简单了。一来是人际关系变得错综复杂,二来是做的事情大了,与外界的接触也越来越多,不能再待在象牙塔里,不得不面对社会的复杂性。

4.1 没有密不透风的墙

在办公室里,感受最深的就是没有密不透风的墙。工作中总会听到一些小道消息,有公司的经营状况,有高层的动向,有同事间的八卦,有某人对某人的吐槽,真真假假,难辨是非。这也是告诫自己,不要过多的吐槽别人,因为你真的不知道这个吐槽是怎么去到你吐槽对象的耳边的。。。同时当你听闻某人对自己的吐槽时,也没必要过于执着,一来不知真假,二来江湖难免冲突,对于吐槽的内容有则改之即可。

4.2 做事留痕

另外,在这个江湖中同样重要的是要学会保护自己。做的事情大了,就肯定要跟外部打交道,这个外部包括外部团队、外部企业等,你可以保持真诚合作,但也要注意做事过程中保留痕迹,因为你不能保证当发生利益冲突时,是否会导致合作关系破裂,当你没有证据时,很容易有理说不清。
我认为在团队内部应尽量保持真诚,不要过多计较。但是当与团队边界外的相关方合作时,要有一定的戒备,关系越远戒备等级也越高,关系由近致远为:跨团队 -> 跨部门 -> 跨企业。

5. 没有绝对的对错,只有不同的立场

是个简单的道理,不用细说了,遇事能保持客观就好。

6. 做过的事总会有意义的

多做点总是有好处的,现在认为没用的事情,但是这份经验总会在某一天派上用场的。但也要注意分清主次,不要在分支上越走越远了,有些事情点到为止拿到经验值即可。

7. 站在用户角度思考问题

一定要站在用户的角度思考问题,如果不考虑用户的实际使用场景,容易简单认为用户都是无理取闹,武断的否定需求,最终只从技术的简易性去设计方案。有时候一个小细节的实现能对用户工作效率带来非常大的提升,一个技术方案的确认并不那么简单,要结合用户的使用场景、技术的简洁合理、投入成本等多因素综合决策。

8. 合理上升问题

从两方面说为什么要上升问题:

1. 信息断层很致命

项目经理最害怕的是任务下发后,成员埋头苦干,遇到问题不敢问,遇到风险不敢说,到了节点交付日期才说问题解决不了。有些问题,只要及早的向上反馈,解决办法是有多种的:可能可以请教高人指点,可能可以跟客户提前交底降低心理预期,可能可以通过方案的变更降低难度。但如果不把问题的风险尽早暴露,到了最后节点再爆发,可能问题就是一发不可收拾了。

2. 会哭的孩子有奶吃

领导每天要处理的事情都很多,你不出声代表你没问题,那领导也肯定不会主动去找麻烦。所以当问题真的超出自己能力范围后,就应该考虑是否要烦扰领导,把问题上升了。而且有些问题在自己的层面是个大问题,但是在领导的眼里根本不是问题。要学会在必要时候合理的主动上升问题,不要什么都自己藏着掖着。当然,也要注意把握这个度,不要事事都麻烦别人。

关于团队管理

1. 交付的信息越多越有利于做出正确判断

通常我们做技术的,很少会出于私心而对一些信息的交付有所保留,但很多时候是因为我们太忙,在分发任务时容易缺乏耐心。但是对于复杂的任务,要保证成员交付的产物与你的期望一致,应该交付尽可能多的信息,如需求的背景、你希望的实现效果及实现方式、包含的一些隐性需求(如性能、可扩展性)、潜在风险等,只有当团队成员掌握足够多的信息,在遇到问题的歧义时,他才能做出正确的判断,避免任务的返工。

2. 梯度的搭建很重要

你可以保持扁平化的氛围,但是工作任务还是要尽可能的搭建梯度,不然什么事情都会上升到你这里,你会被各种琐事打断你的思路,消耗你的精力。只有当你手上的事情能分发出去,你才能去做更重要的事情。

3. 团队情绪很重要

当开始带领团队后,你就不能再只关注自身,和你一起做事的兄弟,他们不是没有感情的劳动机器,当他们的状态不佳时,你应该更主动的去关心他们,可能是因为一些私事,可能是因为薪酬福利,可能是因为工作强度,这些可能性是需要了解的,而不是一味的吐槽。只有保证团队的情绪是健康的,才能保持高昂的士气,确保团队有足够的战斗力。

4. 对团队成员要有要求

不能对团队成员过于仁慈,一定要对成员提出要求。千万不要因为成员的能力水平不足,或是担心任务超出他们的能力时会有抵触情绪,于是一直只安排简单的任务给到他们。你的仁慈会阻碍成员的成长,只有安排超出他们一定能力范围的工作时,在一定的压力情况下才会有快速的成长和突破,这样你的成员能够更多的为你分担工作,达到双赢的局面。

5. 不患寡而患不均

公平是很难做到的,但也要尽量保证。在工作量分配上,要避免能力强的人承担过多,而能力差的人反而轻轻松松过日子。在奖金福利的分配上更是要小心,相当容易造成不满情绪,别把好事变成坏事。

6. 做好决策

团队管理者就是要做大量的决策,把握方向。做技术的都希望可以玩新技术的,团队成员由于不用担责通常都是激进派,但作为决策者必须衡量其利弊,不能过于保守,也不能过于激进。当然说起来简单,要做好是需要大量的经验积累及学习的。

总结

时间匆匆,一不小心就已经五年工作经验,这一路也是跌跌撞撞的过来,以上都是个人的感悟总结,当然以上也有很多是“道理都懂”,自己也没能做好的。要说对这几年的工作生涯很满意那是不可能的,不过也不至于充满遗憾,毕竟成长嘛,还是有的。希望以后的道路更精彩,未来可期。

如果希望再多的交流,可以关注我的公众号换取微信号,乐意解答相关问题。


【软考】系统架构设计师(高级)考试经验回顾分享

前言

全文以过程回顾为主,跳转到“备考攻略”小节可成功闪避唠叨攻击

早在2013年还在大三的时候便随大众考了「软件设计师(中级)」证书,时隔多年在2019年11月9日再次踏入软考的考场参加「系统架构设计师(高级)」的考试,最终结果是侥幸的以49/50/46成绩低分飘过。

由于当时备考时也没看见多少关于系统架构设计师考试的文章,所以既然难得通过了,那也顺手记录一下这个过程做个分享吧。考试过后一直忙于工作没有闲暇进行回顾总结,现在已经有点记忆模糊,只能尽力回忆了。

考「系统架构设计师」的念头是在2019年初时萌发的,当时感到工作上一直在往前走,但没什么里程碑节点,于是定下一个目标,不管是否有用,要在2019年尝试考「系统架构设计师」证书,一来是藉此系统性的梳理系统建设的相关知识,二来也看这个证书能否换取一点补贴,毕竟现在的中级证书竟然在当前带有一点国企性质的公司中,在半年前突然开始有一丁点薪酬补贴。

备考过程

「系统架构设计师」一年只有一次报名机会,与中级证书的一年两次不一样,所以一定不要错过报考时间,2019年是11月份考试,而报考时间是在8月中旬,当时我早早写在了我的GTD列表中。

我的备考时间也算是挺早的,5月中旬我已经开始购买备考书籍,当时先买的是《系统架构设计师考试全程指导(第2版)》,考虑到对应的题册最新只到2016年,所以打算迟一点再买看是否会更新,等到8月底感觉光看知识点已经不行了于是才买的《系统架构设计师2009至2016年试题分析与解答》。

虽然很早开始买书,但是由于疯狂加班,加上一定的懒惰性,其实真实花在备考的时间不太多。5月底到6月中旬的时候是比较用心的在看,7月份想着还有比较多时间所以投入的精力比较少,8、9月份碰上工作上的一个突发项目,连续没日没夜没周末的加班,直接损失了两个月的复习时间,剩下的10月到考前11月都是工作上比较忙碌,处于只能挤时间复习的状态。

考试的题型主要分三场,早上选择题、下午是案例分析、论文(我之前也没特意了解,一直以为只是选择题+论文,没想到原来总共是有三场考试的)。我到10月前基本都还是在通读教材并做章节后的练习题,10月份才开始做真题,10月中下旬才开始练习论文,直到考前大概也只做了两套多的题目,还是没做全的。

备考感受

最开始报考「系统架构设计师」的目的是希望通过对这个教材的学习把相关的技术知识梳理一遍,因为觉得工作至今的技能增长点还是比较散,没有一个系统性的学习。但是当我翻阅教材开始学习时还是比较失望的,书本的内容过于空洞,给人的感觉更像是凑字数,仿佛是定好章节大纲后相关人员往对应的章节填充足够的资料、素材就了事了,阅读过程非常枯燥无味,知识内容也是大多脱离实际。

当然,教材的内容也不是一无是处的,哪怕像是阅读百度百科,也是能了解了一下对应的知识点会有哪些内容。但是,如果真的只是为了梳理知识点的话,其实只看教材的目录便可,根据目录的框架作为知识学习的方向,相关的内容自己带着疑问去搜索相关资料文章的效果会更好。考这个证书是有用的,但如果是抱着通过考证的学习过程去提升梳理自己的知识网络,我觉得大概率是会失望的

另外,让我在学习过程受挫的一点是,很多题目的答案其实在教材中是无法找到一一对应的知识点讲解的。例如像部分嵌入式题目提及的一些专有名词,对于做应用开发的我是完全没接触过的,而教材中无论怎么翻阅也是找不到有提及过的描述,所以还是非常考验实际的技术积累与阅历的,并不像以往的考试答案都在教材中。

考试过程

直到考前其实也没太大信心,尤其是选择题最没底,就如之前说的,选择题部分很考验知识面,不懂的领域挠破头也没用。但也是因为本来就没抱太大期望,反而心态比较放松,内心是希望可以考过,但如果不通过也不会太失望。

早上按时进入考场,万万没想到课室竟然是没挂钟的,而我手上也是没钟表,考试的时间控制只能靠监考老师的人工报时了,所以备考还是要带齐文具、准考证、钟表这些。。。

早上考的是选择题,考试时间是9:00-11:30。试卷发下来,大概翻阅了一下,考点基本上是按教材提纲的顺序制定的,并不意外。通常前边几道题会有涉及到内存分页相关的计算题,难度会较大,但是这次的并没太大难度,而继续往下做时,一些超出技术领域的题目,也是相对容易能够判断出明显的正确项,所以总体做下来比想象中要好。而且仔细计算一下,总共75道题,及格线45分,那总共可以错30道题,这样一想,感觉通过的可能性其实也挺大的。秉承着改多错多的原则,在10点钟做完所有题目后,我检查了一次答题卡没有填涂错误后便开始睡觉了,这一睡就是一个钟。。。

由于感觉最难的选择题仿佛也有挺大希望及格,所以到了下午的考试我就开始认真对待了。

13:30-15:00的考试是案例分析。案例分析是可选题目的形式,其中有一道题我做到一半觉得还是把握不大,所以换去了另外一道虽然也不确定但是稍微好一点的题目。

15:20-17:20是论文。在几道题目中我选了最有把握的“负载均衡的三种实现方式”这题目,只要没理解错题目,那这个基本就是一般面试过程都会问到的题目,作答还是比较容易的,只是有可能因为本来题目就简单,所以要拿高分也不容易。定下题目后短时间内列好提纲,然后开始写摘要,摘要部分稍微超出了建议的字数,不知最后分数是否因为这里有影响。然后就开始奋笔疾书的按之前准备好的模板套内容,连续不停的写下2000多字,具体多少字不记得,反正是达标但也没超多少,因为真的憋不出了。整个过程时间还是非常紧迫的,几乎一刻都不能停,手再酸都还是要继续,到后来只剩半小时的时候紧张起来提了一下速,字迹也开始变得潦草,最终写完也只剩出十来分钟。

备考攻略

  1. 开始学习教材前,建议是先浏览一遍真题,知道大概有哪些题型,会考哪些知识点。
  2. 多看目录架构,了解总体的知识点方向,做到脉络清晰,不要陷在某个知识点不能自拔,影响总体进度。
  3. 学习每一章节前建议先分析课后习题,带着疑问去学习每一章节(我开始时打算学完一个章节后再做对应的课后习题,想通过这样的方式检验自己的学习过程是否到位,后来发现这样效率非常低,一来学习过程容易分神,做题还是要回去再看一遍知识点;二来这些题目真的不一定在课本中有的,反复定位查找答案位置的过程很耗时)。
  4. 案例分析的题目基本是靠做题目去实践,认真弄懂几套真题的案例分析基本就上手了。
  5. 论文一定要套模板,考试时没有太多时间让你慢慢构思,要在限时内写出足够多的字数已经是非常大的挑战了,2500字写下来绝对会手酸的。备考过程找几篇示例文章,分析一下文章结构,作者是如何开始自我介绍和项目介绍,如何引出考题内容,如何展开分析,从哪些方面可以加入凑字数的内容,如何进行总结,把这些结构甚至内容梳理好,最后形成自己的模板,到时论文就按这个模板进行填充内容即可,最好是把模板的内容背一背、抄一抄,做到考试时不在一些凑字数的语句上卡壳。如果有条件,最好是练上一遍两遍提前感受一下2500字的难度,如果时间不允许,那也要多找几个主题,每个主题大概花半小时练习提纲的构思,思考如何行文。
  6. 通读教材是最花时间的,而教材的内容基本上是面向早上的选择题,至于案例分析、论文这些基本都是从真题分析中便可掌握,所以千万不要把所有时间投入到性价比较低的通读教材,一定要保证有一定的时间练习案例分析和论文。

总结

这次考试难度相对偏低所以侥幸过关,以上只是对自己整个过程的回顾记录,仅供参考。虽然备考的过程比较粗糙,但也算是完成了年初定下的一个目标,算是对自己的一个交代。当然,要认清的是,虽然这个证书的名字是「系统架构设计师」,但显然考过了并不意味着自己就是架构师,这只是一个微薄的起点,后续还需要多鞭策自己,以这个知识架构为基础多积累、多实践,朝着真正的架构师的方向去努力。


HTTPS 原理分析——带着疑问层层深入

HTTPS

随着 HTTPS 建站的成本下降,现在大部分的网站都已经开始用上 HTTPS 协议。大家都知道 HTTPS 比 HTTP 安全,也听说过与 HTTPS 协议相关的概念有 SSL 、非对称加密、 CA证书等,但对于以下灵魂三拷问可能就答不上了:

  1. 为什么用了 HTTPS 就是安全的?
  2. HTTPS 的底层原理如何实现?
  3. 用了 HTTPS 就一定安全吗?

本文将层层深入,从原理上把 HTTPS 的安全性讲透。

HTTPS 的实现原理

大家可能都听说过 HTTPS 协议之所以是安全的是因为 HTTPS 协议会对传输的数据进行加密,而加密过程是使用了非对称加密实现。但其实,HTTPS 在内容传输的加密上使用的是对称加密,非对称加密只作用在证书验证阶段。

HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过程如下:

WX20191127-133805@2x.png

① 证书验证阶段

  1. 浏览器发起 HTTPS 请求
  2. 服务端返回 HTTPS 证书
  3. 客户端验证证书是否合法,如果不合法则提示告警

② 数据传输阶段

  1. 当证书验证合法后,在本地生成随机数
  2. 通过公钥加密随机数,并把加密后的随机数传输到服务端
  3. 服务端通过私钥对随机数进行解密
  4. 服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输

为什么数据传输是用对称加密?

首先,非对称加密的加解密效率是非常低的,而 http 的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的;

另外,在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。

为什么需要 CA 认证机构颁发证书?

HTTP 协议被认为不安全是因为传输过程容易被监听者勾线监听、伪造服务器,而 HTTPS 协议主要解决的便是网络传输的安全性问题。

首先我们假设不存在认证机构,任何人都可以制作证书,这带来的安全风险便是经典的“中间人攻击”问题。
“中间人攻击”的具体过程如下:

WX20191126-212406@2x.png

过程原理:

  1. 本地请求被劫持(如DNS劫持等),所有请求均发送到中间人的服务器
  2. 中间人服务器返回中间人自己的证书
  3. 客户端创建随机数,通过中间人证书的公钥对随机数加密后传送给中间人,然后凭随机数构造对称加密对传输内容进行加密传输
  4. 中间人因为拥有客户端的随机数,可以通过对称加密算法进行内容解密
  5. 中间人以客户端的请求内容再向正规网站发起请求
  6. 因为中间人与服务器的通信过程是合法的,正规网站通过建立的安全通道返回加密后的数据
  7. 中间人凭借与正规网站建立的对称加密算法对内容进行解密
  8. 中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输
  9. 客户端通过与中间人建立的对称加密算法对返回结果数据进行解密

由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。

浏览器是如何确保 CA 证书的合法性?

1. 证书包含什么信息?

  • 颁发机构信息
  • 公钥
  • 公司信息
  • 域名
  • 有效期
  • 指纹
  • ……

2. 证书的合法性依据是什么?

首先,权威机构是要有认证的,不是随便一个机构都有资格颁发证书,不然也不叫做权威机构。另外,证书的可信性基于信任制,权威机构需要对其颁发的证书进行信用背书,只要是权威机构生成的证书,我们就认为是合法的。所以权威机构会对申请者的信息进行审核,不同等级的权威机构对审核的要求也不一样,于是证书也分为免费的、便宜的和贵的。

3. 浏览器如何验证证书的合法性?

浏览器发起 HTTPS 请求时,服务器会返回网站的 SSL 证书,浏览器需要对证书做以下验证:

  1. 验证域名、有效期等信息是否正确。证书上都有包含这些信息,比较容易完成验证;
  2. 判断证书来源是否合法。每份签发证书都可以根据验证链查找到对应的根证书,操作系统、浏览器会在本地存储权威机构的根证书,利用本地根证书可以对对应机构签发证书完成来源验证;
    WX20191127-084216@2x.png
  3. 判断证书是否被篡改。需要与 CA 服务器进行校验;
  4. 判断证书是否已吊销。通过CRL(Certificate Revocation List 证书注销列表)和 OCSP(Online Certificate Status Protocol 在线证书状态协议)实现,其中 OCSP 可用于第3步中以减少与 CA 服务器的交互,提高验证效率

以上任意一步都满足的情况下浏览器才认为证书是合法的。

这里插一个我想了很久的但其实答案很简单的问题:
既然证书是公开的,如果要发起中间人攻击,我在官网上下载一份证书作为我的服务器证书,那客户端肯定会认同这个证书是合法的,如何避免这种证书冒用的情况?
其实这就是非加密对称中公私钥的用处,虽然中间人可以得到证书,但私钥是无法获取的,一份公钥是不可能推算出其对应的私钥,中间人即使拿到证书也无法伪装成合法服务端,因为无法对客户端传入的加密数据进行解密。

4. 只有认证机构可以生成证书吗?

如果需要浏览器不提示安全风险,那只能使用认证机构签发的证书。但浏览器通常只是提示安全风险,并不限制网站不能访问,所以从技术上谁都可以生成证书,只要有证书就可以完成网站的 HTTPS 传输。例如早期的 12306 采用的便是手动安装私有证书的形式实现 HTTPS 访问。
WX20191127-130501@2x.png

本地随机数被窃取怎么办?

证书验证是采用非对称加密实现,但是传输过程是采用对称加密,而其中对称加密算法中重要的随机数是由本地生成并且存储于本地的,HTTPS 如何保证随机数不会被窃取?

其实 HTTPS 并不包含对随机数的安全保证,HTTPS 保证的只是传输过程安全,而随机数存储于本地,本地的安全属于另一安全范畴,应对的措施有安装杀毒软件、反木马、浏览器升级修复漏洞等。

用了 HTTPS 会被抓包吗?

HTTPS 的数据是加密的,常规下抓包工具代理请求后抓到的包内容是加密状态,无法直接查看。

但是,正如前文所说,浏览器只会提示安全风险,如果用户授权仍然可以继续访问网站,完成请求。因此,只要客户端是我们自己的终端,我们授权的情况下,便可以组建中间人网络,而抓包工具便是作为中间人的代理。通常 HTTPS 抓包工具的使用方法是会生成一个证书,用户需要手动把证书安装到客户端中,然后终端发起的所有请求通过该证书完成与抓包工具的交互,然后抓包工具再转发请求到服务器,最后把服务器返回的结果在控制台输出后再返回给终端,从而完成整个请求的闭环。

既然 HTTPS 不能防抓包,那 HTTPS 有什么意义?
HTTPS 可以防止用户在不知情的情况下通信链路被监听,对于主动授信的抓包操作是不提供防护的,因为这个场景用户是已经对风险知情。要防止被抓包,需要采用应用级的安全防护,例如采用私有的对称加密,同时做好移动端的防反编译加固,防止本地算法被破解。

总结

以下用简短的Q&A形式进行全文总结:

Q: HTTPS 为什么安全?
A: 因为 HTTPS 保证了传输安全,防止传输过程被监听、防止数据被窃取,可以确认网站的真实性。

Q: HTTPS 的传输过程是怎样的?
A: 客户端发起 HTTPS 请求,服务端返回证书,客户端对证书进行验证,验证通过后本地生成用于改造对称加密算法的随机数,通过证书中的公钥对随机数进行加密传输到服务端,服务端接收后通过私钥解密得到随机数,之后的数据交互通过对称加密算法进行加解密。

Q: 为什么需要证书?
A: 防止”中间人“攻击,同时可以为网站提供身份证明。

Q: 使用 HTTPS 会被抓包吗?
A: 会被抓包,HTTPS 只防止用户在不知情的情况下通信被监听,如果用户主动授信,是可以构建“中间人”网络,代理软件可以对传输内容进行解密。

顺手 po 一张学习的过程图,高清大图点这里☞HTTPS学习草稿图.jpg


学习容易写文难,转载请注明出处~ 如有错漏恳请指出。



Spring源码分析专题 —— IOC容器启动过程(下篇)

声明
1.建议先阅读《Spring源码分析专题 —— 阅读指引》
2.强烈建议阅读过程中要参照调用过程图,每篇都有对应的调用过程图
3.写文不易,转载请标明出处

前言

Spring IOC 容器的启动过程是「 定位 -> 加载 -> 注册 -> 实例化 」,前边已经讲解了最重要的「加载」与「注册」过程(「实例化」的内容将在依赖注入的章节讲解),本篇的主题是对「上篇」「中篇」的补充,定位的详细过程会在本篇中讲解,之后如果有必要还会在本篇中补充其他的一些细节内容。

定位

调用过程图↓↓↓
SpringIOC源码 - 定位.jpg

关于前边的调用过程我们略过,直接来到图中的入口: ContextLoader 的 configureAndRefreshWebApplicationContext 方法。

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
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}

wac.setServletContext(sc);
/**
* [note-by-leapmie]
* CONFIG_LOCATION_PARAM = "contextConfigLocation"
* 对应的含义是读取web.xml中contextConfigLocation的值
*/
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}

// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

customizeContext(sc, wac);
/** [note-by-leapmie] 调用容器的refresh()方法,此处wac对应的类是XmlWebApplicationContext **/
wac.refresh();
}

在「上篇」 中我们有看过这个方法,当时我们关心的是最后一行 wac.refresh() ,这次我们关心的是wac.setConfigLocation(configLocationParam);。configLocationParam 的值是调用 sc.getInitParameter(CONFIG_LOCATION_PARAM); 获取的,CONFIG_LOCATION_PARAM的值是 "contextConfigLocation" ,对于 "contextConfigLocation" 有印象吗?回顾我们的 web.xml 配置文件中可以看到,我们配置了一个 <context-param> 上下文参数,这个参数名便是 contextConfigLocation , 而其值是便是我们自定义的 Spring 配置文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<web-app>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>

<!-- ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- DispatcherServlet -->
...
</web-app>

如果有兴趣还可以继续深入查看 setConfigLocation 的处理逻辑,其实现过程是在 AbstractRefreshableConfigApplicationContext 类中,通过代码可知道配置文件支持的分隔符有,; \t\n,这里我们不再占用篇幅深入讲解了。

接下来我们直接分析 XmlWebApplicationContext 的 loadBeanDefinitions(XmlBeanDefinitionReader) 方法(忘记调用路线的可以看回「上篇」中的调用过程图复习一下)

1
2
3
4
5
6
7
8
9
10
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
/** [note-by-leapmie] 获取配置文件位置*/
String[] configLocations = getConfigLocations();
if (configLocations != null) {
for (String configLocation : configLocations) {
/** [note-by-leapmie] 实际是调用XmlBeanDefinitionReader的loadBeanDefinitions方法 **/
reader.loadBeanDefinitions(configLocation);
}
}
}

这个方法我们在前边也是看过的,当时我们只关注 reader.loadBeanDefinitions(configLocation); 这一行,这次我们关注第一行 String[] configLocations = getConfigLocations(); ,从方法的字面意思就很容易理解它的作用——获取配置位置,很明显这就是我们的「定位」过程。
getConfigLocations 的实现方法是在其父类 AbstractRefreshableConfigApplicationContext 中

1
2
3
protected String[] getConfigLocations() {
return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations());
}

这个方法的意思是,当容器中的 configLocations 变量为空时则调用 getDefaultConfigLocations, 当不为空时直接返回容器中的 configLocations 。我们先看一看 getDefaultConfigLocations 方法

1
2
3
4
5
6
7
8
protected String[] getDefaultConfigLocations() {
if (getNamespace() != null) {
return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
}
else {
return new String[] {DEFAULT_CONFIG_LOCATION};
}
}

DEFAULT_CONFIG_LOCATION 的定义是

1
2
/** Default config location for the root context. */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";

/WEB-INF/applicationContext.xml 便是我们再熟悉不过的 Spring 默认的配置文件路径与文件名。
而当我们再 web.xml 配置了 contextConfigLocation ,Spring则会读取我们的自定义 Spring 配置文件。至此,关于「定位」的过程已经完整讲解完毕。


[目录]
[上一篇]Spring源码分析专题 —— IOC容器启动过程(中篇)
[[下一篇]Spring源码分析专题 —— IOC容器依赖注入]



Spring源码分析专题 —— IOC容器启动过程(中篇)

声明
1.建议先阅读《Spring源码分析专题 —— 阅读指引》
2.强烈建议阅读过程中要参照调用过程图,每篇都有对应的调用过程图
3.写文不易,转载请标明出处

前言

在上文《Spring源码分析专题 —— IOC容器启动过程(上篇)》中我们已经寻找到 Spring IOC 启动过程的核心方法 refresh() ,本篇我们将详细讲解启动流程 「定位 -> 加载 -> 注册 -> 实例化」 中的加载与注册环节。
(定位的作用是获取到配置文件,通常我们spring的配置文件是 application.xml 或自定义的 spring-xxx.xml ,定位过程的细节不少,而对我们的主流程影响不大,所以关于定位过程将放在下篇补充讲解)

本篇继续使用上篇中的调用过程图☞ IOC容器启动调用过程图.jpg

加载与注册

我们再看一眼 refresh() 方法

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
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
/**
* obtainFreshBeanFactory方法中会调用loadBeanDefinition方法,用于加载bean的定义
*/
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
/** 初始化所有非lazy-init的bean **/
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

在这个 refresh() 方法中,我们首先关注的是 obtainFreshBeanFactory()

1
2
3
4
5
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
/** 实际是调用子类AbstractRefreshableApplicationContext中的refreshBeanFactory方法 **/
refreshBeanFactory();
return getBeanFactory();
}

obtainFreshBeanFactory() 方法中调用了 refreshBeanFactory() ,而这个方法是在 AbstractApplicationContext 的子类 AbstractRefreshableApplicationContext 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
/** [note-by-leapmie] 调用子类XmlWebApplicationContext的loadBeanDefinitions方法 **/
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

refreshBeanFactory方法中调用了 loadBeanDefinitions 方法,路线又回到了 XmlWebApplicationContext 容器,loadBeanDefinitions 方法是在 XmlWebApplicationContext 类中实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
/** [note-by-leapmie] 注意传入的beanDefinitionReader是XmlBeanDefinitionReader **/
loadBeanDefinitions(beanDefinitionReader);
}

方法最后是调用了重载的 loadBeanDefinitions 方法,传入的参数是 XmlBeanDefinitionReader 的对象,我们先看一看重载的 loadBeanDefinitions 方法

1
2
3
4
5
6
7
8
9
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
String[] configLocations = getConfigLocations();
if (configLocations != null) {
for (String configLocation : configLocations) {
/** 实际是调用XmlBeanDefinitionReader的loadBeanDefinitions方法 **/
reader.loadBeanDefinitions(configLocation);
}
}
}

可以看到,其实最后是调用 reader 的 loadBeanDefinitions 方法,此处 reader 的类型是 XmlBeanDefinitionReader ,所以我们查看 XmlBeanDefinitionReader 中的 loadBeanDefinitions 方法

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
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
.
.
.
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
/** do开头的方法表示是正真执行处理操作的方法 **/
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}

此处我们关注的是 doLoadBeanDefinitions 方法,有个技巧,在Spring中,以do开头的方法都是最终实际执行逻辑处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {

try {
/** [note-by-leapmie] 读取加载配置文件 **/
Document doc = doLoadDocument(inputSource, resource);
/** [note-by-leapmie] 注册Bean **/
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
.
.
.
}

在这里有两个方法值得我们关注,一个是 doLoadDocument ,负责把配置文件读取为 Document 对象,这个方法中包含了「定位」过程的处理逻辑,关于定位过程我们在下篇再详细分析;第二个是 registerBeanDefinitions 方法,这个方法包含了「加载」和「注册」逻辑。

1
2
3
4
5
6
7
8
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
/** [note-by-leapmie] 获取BeanDefinitionDocumentReader,此处获得的对象实际类型为DefaultBeanDefinitionDocumentReader **/
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
/** [note-by-leapmie] 调用Reader的registerBeanDefinitions方法 */
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}

这里关键有两步,第一步是获取 documentReader ,第二步是调用 documentReader 的 registerBeanDefinitions 方法。稍微跟踪一下可知 documentReader 的实际类型是 DefaultBeanDefinitionDocumentReader,所以我们进入到 DefaultBeanDefinitionDocumentReader 的 registerBeanDefinitions 方法

1
2
3
4
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}

按照惯例,干活的是do开头的方法

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
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);
/** 解析转换为BeanDefinitions **/
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

然后是 parseBeanDefinitions 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
/** [note-by-leapmie] 判断元素是否属于默认的Namespace(当标签为<beans>时判断条件为真) **/
if (delegate.isDefaultNamespace(ele)) {
/** [note-by-leapmie] 处理默认的Element **/
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

isDefaultNamespaces 方法是判断元素是否属于默认的 Namespace ,通过跟踪可知,这个默认的 Namespace 是指 <beans> 标签, 我们知道在spring的配置文件中,对bean的定义是放在 <beans> 标签里边的,所以接下来的 parseDefaultElement 方法则是用于解析 bean 定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
/** 处理<import>标签 **/
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
/** 处理<alias>标签 **/
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
/** 处理<bean>标签 **/
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
/** 处理嵌套的<beans>标签 **/
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

到这里我们就一目了然了,这很显然就是针对 <beans> 标签中的各种元素进行解析,对于其他标签我们不深究,直接看处理 <bean> 标签的 processBeanDefinition 方法

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
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
/**
* [note-by-leapmie]
* 调用BeanDefinitionParserDelegate的parseBeanDefinitionElement方法
* 返回一个包含BeanDefinition信息的BeanDefinitionHolder实例
* **/
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
/**
* [note-by-leapmie]
* 注册BeanDefinition
* 传入的参数是刚刚获取到的BeanDefinitionHolder对象,再加上DefaultListableBeanFactory对象
* DefaultListableBeanFactory对象的由来需要追溯到AbstractRefreshableApplicationContext的refreshBeanFactory()方法中
* **/
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}

这里有两个非常关键的方法:

  • 一个是 parseBeanDefinitionElement ,这个方法最终会返回一个持有 BeanDefinition 的 BeanDefinitionHolder 实例,我们在上篇开头的结论中已经说了,加载的过程其实就是把bean的定义转换成一个 BeanDefinition 对象,所以 parseBeanDefinitionElement 对应的便是 「加载」 过程;
  • 另一个则是 registerBeanDefinition ,这个方法对应的便是「注册」过程。

接下来我们将分两部分分别讲解 parseBeanDefinitionElement 和 registerBeanDefinition 的内容

1. 加载 (parseBeanDefinitionElement)

bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); 中的 delegate 是 BeanDefinitionParserDelegate 的实例,我们查看 BeanDefinitionParserDelegate 中的 decorateBeanDefinitionIfRequired 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {

.
.
.

/** [note-by-leapmie] 此处调用的parseBeanDefinitionElement方法返回的是BeanDefinition **/
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
.
.
.
/** [note-by-leapmie] 把beanDefinition注入BeanDefinitionHolder中 **/
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}

return null;
}

这个方法先调用另一个重载的 parseBeanDefinitionElement 方法,然后把获得 beanDefinition 传入 BeanDefinitionHolder 。我们看一看重载的 parseBeanDefinitionElement 方法

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
public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, @Nullable BeanDefinition containingBean) {

.
.
.

try {
AbstractBeanDefinition bd = createBeanDefinition(className, parent);

/** [note-by-leapmie] 解析bean定义的属性 **/
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));

parseMetaElements(ele, bd);
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

parseConstructorArgElements(ele, bd);
parsePropertyElements(ele, bd);
parseQualifierElements(ele, bd);

bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));

return bd;
}
.
.
.
return null;
}

这里的parse开头的方法都是对bean定义的属性标签进行解析,例如「name」、「singleton」、「lazy-init」等,大家可以自行深入了解每一个parse方法是如何解析各个属性的,在本文中就不再占用篇幅逐一讲解了。至此我们已经获取到了BeanDefinition的信息,下一步就到「注册」了。

2. 注册 (registerBeanDefinition)

所谓的注册,其实就是把BeanDefintion存储到IOC容器中,我们进入到 registerBeanDefinition 中看一看是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {

// Register bean definition under primary name.
String beanName = definitionHolder.getBeanName();
/** [note-by-leapmie] 此处实际是调用DefaultListableBeanFactory的registerBeanDefinition方法 **/
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

// Register aliases for bean name, if any.
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}

这里最终调用的是 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()) 其中 registry 是 DefaultListableBeanFactory 的实例。

(为什么是 DefaultListableBeanFactory ?感兴趣的可以去追溯一下,给出一点提示,在 AbstractRefreshableApplicationContext 的 refreshBeanFactory() 方法中会创建DefaultListableBeanFactory的实例,并在之后的所有关键方法中都会作为参数传入该实例,保证后续的调用流程中都能获取到该实例)

DefaultListableBeanFactory 的 registerBeanDefinition 方法如下

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
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {

.
.
.
if (existingDefinition != null) {
.
.
.
}
else {
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
if (this.manualSingletonNames.contains(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);
updatedSingletons.remove(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
}
else {
// Still in startup registration phase
/**
* 把BeanDefinitionc添加到beanDefinitionMap中
* Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
* 由此可知,IOC的启动过程是先把Bean的定义解析转换为BeanDefiniton,
* 最后存储于IOC容器(DefaultListableBeanFactory是一个IOC容器)的一个Map变量中。
* */
this.beanDefinitionMap.put(beanName, beanDefinition);
/**
* 把所有的Bean名存储于beanDefinitionNames列表中
*/
this.beanDefinitionNames.add(beanName);
this.manualSingletonNames.remove(beanName);
}
this.frozenBeanDefinitionNames = null;
}

if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
}

this.beanDefinitionMap.put(beanName, beanDefinition); 这一行是重点,字面意思已经很明显,就是把 beanName 和 beanDefinition 以 key-value 的形式存储于 beanDefinitionMap 中, beanDefinitionMap 的定义如下。

1
2
/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

跟踪到这一步我们便可以得出结论:「 BeanDefinition 是存储在 DefaultListableBeanFactory 的一个 Map 数据结构中 」


[目录]
[上一篇]Spring源码分析专题 —— IOC容器启动过程(上篇)
[下一篇]Spring源码分析专题 —— IOC容器启动过程(下篇)



Spring源码分析专题 —— IOC容器启动过程(上篇)

声明
1.建议先阅读《Spring源码分析专题 —— 阅读指引》
2.强烈建议阅读过程中要参照调用过程图,每篇都有对应的调用过程图
3.写文不易,转载请标明出处

前言

关于 IOC 容器启动的内容很多,我将分上中下三篇讲解,其中上篇相对简单,中篇最为复杂,请大家耐心阅读。

  • 上篇 - 主要是相关基础说明和找到分析入口
  • 中篇 - 讲解定位、加载、注册的过程(实例化在依赖注入的章节再讲)
  • 下篇 - 细节补充

调用过程图

由于篇幅问题,此处我只放个缩略图,高清大图请点击链接☞ IOC容器启动调用过程图.jpg
请务必一边对照图片一边阅读文章。

先放结论

此处先放结论,大家稍微记一记,后边将展开详解

  • Spring 的启动流程主要是定位 -> 加载 -> 注册 -> 实例化
    • 定位 - 获取配置文件路径
    • 加载 - 把配置文件读取成 BeanDefinition
    • 注册 - 存储 BeanDefinition
    • 实例化 - 根据 BeanDefinition 创建实例
  • 所谓的IOC容器其实就是 BeanFactory , BeanFactory 是一个接口,有很多对应的实现类
  • IOC容器的关键入口方法是 refresh()
  • Web 应用中使用的容器是 XmlWebApplicationContext ,其类图如下,可以看出最终是一个实现了 BeanFactory 的类

IOC容器源码的入口

我们知道 Spring 框架不仅仅是面向 Web 应用,所以 Spring 中对应不同场景有许多 IOC 容器的实现类,其中有简单的也有复杂的,在此我们跳过简单容器的讲解,直接以我们最熟悉、也是最感兴趣的 Java Web 项目下手,寻找其对应的 IOC 容器实现类,同时一口气寻找到 IOC 容器的关键入口方法 refresh() 。

1. 寻找IOC容器实现类

以下是我们熟知的 SpringMVC 项目中 web.xml 的基础配置,其关键是要配置一个 ContextLoaderListener 和一个 DispatcherServlet

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
<web-app>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>

<!-- ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- DispatcherServlet -->
<servlet>
<description>spring mvc servlet</description>
<servlet-name>springMvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<description>spring mvc</description>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>

我们知道在 Java Web 容器中相关组件的启动顺序是 ServletContext -> listener -> filter -> servlet , listener 是优于 servlet 启动的,所以我们先看一看 ContextLoaderListener 的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}

public ContextLoaderListener(WebApplicationContext context) {
super(context);
}

public void contextInitialized(ServletContextEvent event) {
this.initWebApplicationContext(event.getServletContext());
}

public void contextDestroyed(ServletContextEvent event) {
this.closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}

根据 Java Web 容器的规范可知,当 Listener 启动时会调用 contextInitialized 方法,而 ContextLoaderListener 中该方法的内容是继续调用 initWebApplicationContext 方法,于是我们再跟踪 initWebApplicationContext
( ContextLoaderListener 是 ContextLoader 的子类,所以其实是调用了父类的 initWebApplicationContext 方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already " +
"a root application context present - check whether " +
"you have multiple ContextLoader* definitions in your web.xml!");
} else {
Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}

long startTime = System.currentTimeMillis();

try {
if (this.context == null) {
this.context = this.createWebApplicationContext(servletContext);
}
.
.
.
}

此处我们关心的是 createWebApplicationContext 方法

1
2
3
4
5
6
7
8
9
10
11
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {

/** [note-by-leapmie] determineContextClass方法中获取contextClass **/
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
/** [note-by-leapmie] 根据contextClass返回实例 */
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

从代码可知,方法中的逻辑主要是调用 determineContextClass 获取 contextClass ,然后根据 contextClass 创建 IOC 容器实例。所以, contextClass 的值将是关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected Class<?> determineContextClass(ServletContext servletContext) {
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
.
.
.
}
else {
/**
* [note-by-leapmie]
* defaultStrategies的值是在本类中的static方法中注入的
* 即该类加载过程中defaultStrategies已经被赋值
* 本类的开始部分有static代码块
* **/
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}

可以看到, contextClassName 是从 defaultStrategies 中获取的,而关于 defaultStrategies 的赋值需要追溯到 ContextLoader 类中的静态代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
static {
try {
/**
* [note-by-leapmie]
* DEFAULT_STRATEGIES_PATH的值是ContextLoader.properties
*/
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}

defaultStrategies 是从 resource 中获取的参数,而 resource 又是从 DEFAULT_STRATEGIES_PATH 中获取,查看可知 DEFAULT_STRATEGIES_PATH 的值是 ContextLoader.properties ,通过全局查找到ContextLoader.properties文件,其中内容如下

1
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

由此可知, SpringMVC 项目中使用到的 IOC 容器类型是 XmlWebApplicationContext。

2. 寻找关键入口方法refresh()

我们回到 ContextLoader 的 initWebApplicationContext 方法,前边我们说到调用 createWebApplicationContext 方法创建容器,容器创建后我们关注的下一个方法是 configureAndRefreshWebApplicationContext

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
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
.
.
.
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
/** [note-by-leapmie] 获取SpringIOC容器类型 **/
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
/** [note-by-leapmie] 配置和刷新容器 **/
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
.
.
.
}

configureAndRefreshWebApplicationContext的代码如下

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
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}

wac.setServletContext(sc);
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}

// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

customizeContext(sc, wac);
/** [note-by-leapmie] 调用容器的refresh()方法,此处wac对应的类是XmlWebApplicationContext **/
wac.refresh();
}

在这里我们要关注的是最后一行 wac.refresh() ,意思是调用容器的 refresh() 方法,此处我们的容器是XmlWebApplicationContext,对应的 refresh() 在其父类 AbstractApplicationContext

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
@Override
/** 核心过程 **/
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
/**
* obtainFreshBeanFactory方法中会调用loadBeanDefinition方法,用于加载bean的定义
*/
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
/** 初始化所有非lazy-init的bean **/
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

至此我们已经找到了关键的入口 refresh() ,我们看一下在调用过程图中我们所处的位置

refresh 方法是 Spring IOC 容器启动过程的核心方法,方法中按顺序调用了好几个命名清晰的方法,其对应的都是 IOC 容器启动过程的关键步骤,更多的细节我们将在下一节继续讲解。

话痨一下
大家可能会觉得,在源码分析过程中一个方法中调用了很多方法,例如先执行方法 a() ,再执行方法 b() ,为什么我们直接看方法 b() 而跳过了方法 a() ?
在这里我想说的是,Spring的源码量很庞大,如果每个细节都去了解可能一年过去了都看不完,我们应该先关注大流程,其他的细枝末节可以在了解了大流程后再慢慢深入了解。
至于为什么是看方法 b() 而跳过方法 a() ,这些都是前人总结的经验与心血,在学习过程中我也是跟着别人的步伐在源码中探索,中间有些缺失的路线我也花费大量时间去踩坑,最后绘制了每一份调用过程图。在本专题中我能确保的是,只要跟着我的步伐,你们不会在源码分析的路上迷路。


[目录]
[上一篇]Spring源码分析专题 —— 阅读指引
[下一篇]Spring源码分析专题 —— IOC容器启动过程(中篇)



Spring源码分析专题 —— 阅读指引

声明:本博客中除声明转载的,其余都是原创文章。本人宗旨是不抄袭,不生产垃圾网文,写下的都是个人经验与心血总结,如转载请注明出处。若文中有任何纰漏错误,希望大家直接指出,以免个人愚见误导他人,感激!

阅读源码的意义

  1. 更深入理解框架原理,印象更深刻
  2. 学习优秀的编程风格、编程技巧、设计思想
  3. 解决实际问题,如修复框架中的bug,或是参考框架源码,结合实际业务需求编写一个独有的框架

阅读源码的方法

  1. 首先是要有一定的编程经验。如果连业务代码都写得不流畅那是不建议阅读源码的,因为基础不好的情况下一是阅读困难,二是无法静下心理解,每看两行就会纠结花大量时间在源码上是否值得,感觉不如写多两行业务代码来得有价值。
  2. 要有耐心。一篇关于源码的文章可能需要阅读两三小时以上才能读完,如果没有这个觉悟,可能看到一半就转头去干别的事了。
  3. 先读文档,理解概念,而不是一上来就扎在代码中。要从代码中反推出设计逻辑是非常痛苦的,也是最没意义的,除非实在是没有相关文档,例如研究竞争对手的源码。(文档可以是官方文档也可以是网上的优秀博客文章,主要用于理解设计概念)

本专题阅读指引

  • 本专题涵盖Spring IOC容器、SpringMVC、AOP、事务四大方面,内容较多,要较长时间消化
  • 可下载带有我注释笔记的Spring源码,Spring版本是5.1.2.BUILD-SNAPSHOT,下载地址https://github.com/leapmie/spring-framework-master-note
  • 每个专题都会配有我整理的调用过程图,可以理解为源码追踪地图,建议是一边看调用过程图一边跟踪源码,因为Spring的源码非常复杂,调用层次很深,非常容易在源码中迷路,看到一半就分不清自己身在何处了。

[目录]
[下一篇]Spring源码分析专题 —— IOC容器启动过程(上篇)



Spring源码分析专题——目录

EJB到底是什么

声明:本文转载自https://www.cnblogs.com/strugglion/p/6027318.html

EJB到底是什么?

1. 我们不禁要问,什么是”服务集群”?什么是”企业级开发”?

既然说了EJB 是为了”服务集群”和”企业级开发”,那么,总得说说什么是所谓的”服务集群”和”企业级开发”吧!这个问题其实挺关键的,因为J2EE 中并没有说明白,也没有具体的指标或者事例告诉广大程序员什么时候用EJB 什么时候不用。于是大家都产生一些联想,认为EJB”分布式运算”指得是”负载均衡”提高系统的运行效率。然而,估计很多人都搞错了,这个”服务群集”和”分布式运算”并没有根本解决运行负载的问题,尤其是针对数据库的应用系统。
为什么?

我们先把EJB 打回原形给大家来慢慢分析。

2. 把EJB 掰开了揉碎了

我们把EJB 的概念好好的分析一下,看看能发现些什么蛛丝马迹。

3.1 EJB 概念的剖析

我们先看一下,EJB 的官方解释:
商务软件的核心部分是它的业务逻辑。业务逻辑抽象了整个商务过程的流程,并使用计算机语言将他们实现。

J2EE 对于这个问题的处理方法是将业务逻辑从客户端软件中抽取出来,封装在一个组件中。这个组件运行在一个独立的服务器上,客户端软件通过网络调用组件提供的服务以实现业务逻辑,而客户端软件的功能单纯到只负责发送调用请求和显示处理结果。在J2EE 中,这个运行在一个独立的服务器上,并封装了业务逻辑的组件就是EJB(Enterprise Java Bean)组件。这其中我们主要关注这么几点,我们来逐条剖析:

剖析1:所谓:”业务逻辑”
我们注意到在EJB 的概念中主要提到的就是”业务逻辑”的封装,而这个业务逻辑到底是什么?说的那么悬乎,其实这个所谓的”业务逻辑”我们完全可以理解成执行特定任务的”类”。

剖析2:所谓:”将业务逻辑从客户端软件中抽取出来,封装在组件中……运行在一个服务器上”
既然我们知道了”业务逻辑”的概念就是执行特定任务的”类”,那么,什么叫”从客户端软件中抽取出来”?其实,这个就是把原来放到客户端的”类”,拿出来不放到客户端了,放到一个组件中,并将这个组件放到一个服务器上去运行。

3.2 把EJB 这个概念变成大白话

变成大白话就是,”把你编写的软件中那些需要执行制定的任务的类,不放到客户端软件上了,而是给他打成包放到一个服务器上了”。

3.3 发现问题了

不管是用”八股文”说,还是用大白话说这个EJB 概念都提到了一个词–”客户端软件”。
“客户端软件”?难道EJB 的概念中说的是C/S 软件?
是的,没错!
EJB 就是将那些”类”放到一个服务器上,用C/S 形式的软件客户端对服务器上的”类”进行调用。
快崩溃了吧!
EJB 和JSP 有什么关系?EJB 和JSP 有关系,但是关系还真不怎么大,至多是在JSP 的服务器端调用远端服务上的EJB 类,仅此而已。

4.1 EJB 的最底层究竟是什么

我们揭开了EJB”八股”概念的真谛,那么,再来分析EJB 的底层实现技术,通过底层实
现技术来分析EJB 的工作方式。

4.2 EJB 的实现技术

EJB 是运行在独立服务器上的组件,客户端是通过网络对EJB 对象进行调用的。在Java中,能够实现远程对象调用的技术是RMI,而EJB 技术基础正是RMI。通过RMI 技术,J2EE将EJB 组件创建为远程对象,客户端就可以通过网络调用EJB 对象了。

4.3 看看RMI 是什么东东

在说RMI 之前,需要理解两个名词:
对象的序列化
分布式计算与RPC

名词1:对象的序列化
对象的序列化概念:对象的序列化过程就是将对象状态转换成字节流和从字节流恢复对象。将对象状态转换成字节流之后,可以用java.io 包中的各种字节流类将其保存到文件中,或者通过网络连接将对象数据发送到另一个主机。
上面的说法有点”八股”,我们不妨再用白话解释一下:对象的序列化就是将你程序中实例化的某个类的对象,比如,你自定一个类MyClass,或者任何一个类的对象,将它转换成字节数组,也就是说可以放到一个byte 数组中,这时候,你既然已经把一个对象放到了byte数组中,那么你当然就可以随便处置了它了,用得最多的就是把他发送到网络上远程的计算机上了。如图2 11所示。


名词2:分布式计算与RPC
RPC 并不是一个纯粹的Java 概念,因为在Java 诞生之前就已经有了RPC 的这个概念,RPC是”Remote Procedure Call”的缩写,也就是”远程过程调用”。在Java 之前的大多数编程语言,如,Fortran、C、COBOL 等等,都是过程性的语言,而不是面向对象的。所以,这些编程语言很自然地用过程表示工作,如,函数或子程序,让其在网络上另一台机器上执行。说白了,就是本地计算机调用远程计算机上的一个函数。如图2 12所示。


名词3:二者结合就是RMI
RMI 英文全称是”Remote Method Invocation”,它的中文名称是”远程方法调用”,它就是利用Java 对象序列化的机制实现分布式计算,实现远程类对象的实例化以及调用的方法。说的更清楚些,就是利用对象序列化来实现远程调用,也就是上面两个概念的结合体,利用这个方法来调用远程的类的时候,就不需要编写Socket 程序了,也不需要把对象进行序列化操作,直接调用就行了非常方便。
远程方法调用是一种计算机之间对象互相调用对方函数,启动对方进程的一种机制,使用这种机制,某一台计算机上的对象在调用另外一台计算机上的方法时,使用的程序语法规则和在本地机上对象间的方法调用的语法规则一样。如图2 13所示。

4.4 优点

这种机制给分布计算的系统设计、编程都带来了极大的方便。只要按照RMI 规则设计程序,可以不必再过问在RMI 之下的网络细节了,如:TCP 和Socket 等等。任意两台计算机之间的通讯完全由RMI 负责。调用远程计算机上的对象就像本地对象一样方便。RMI 可将完整的对象作为参数和返回值进行传递,而不仅仅是预定义的数据类型。也就是说,可以将类似Java 哈西表这样的复杂类型作为一个参数进行传递。

4.5 缺点

如果是较为简单的方法调用,其执行效率也许会比本地执行慢很多,即使和远程Socket机制的简单数据返回的应用相比,也会慢一些,原因是,其在网络间需要传递的信息不仅仅包含该函数的返回值信息,还会包含该对象序列化后的字节内容。

4.6 EJB 是以RMI 为基础的

通过RMI 技术,J2EE 将EJB 组件创建为远程对象,EJB 虽然用了RMI 技术,但是却只需要定义远程接口而无需生成他们的实现类,这样就将RMI 技术中的一些细节问题屏蔽了。但不管怎么说,EJB 的基础仍然是RMI,所以,如果你想了解EJB 的原理,只要把RMI的原理搞清楚就行了。你也就弄清楚了什么时候用EJB 什么时候不需要用EJB 了。

5. EJB 中所谓的”服务群集”

既然已经知道了,RMI 是将各种任务与功能的类放到不同的服务器上,然后通过各个服务器间建立的调用规则实现分布式的运算,也就明白EJB 所谓的”服务群集”的概念。就是将原来在一个计算机上运算的几个类,分别放到其他计算机上去运行,以便分担运行这几个类所需要占用的CPU 和内存资源。同时,也可以将不同的软件功能模块放到不同的服务器上,当需要修改某些功能的时候直接修改这些服务器上的类就行了,修改以后所有客户端的软件都被修改了。如图2 14所示。

6. 这种部署难道是无懈可击

图2 14所示的这个”服务群集”看似”无懈可击”,其实是它这个图没有画完整,我们来把这个图画完整,再来看看有什么问题没有。

6.1 瓶颈在数据库端

仔细观察之后,发现这种配置是有瓶颈的,如图2 15所示。


我们看看图2 15的结构图,现在如果想实现各个服务器针对同一个数据库的查询,那么,不管你部署多少个功能服务器,都需要针对一个数据库服务器进行查询操作。也就是说,不管你的”计算”有多么”分布”也同样需要从一台服务器中取得数据。虽然,看起来将各个功能模块分布在不同的服务器上从而分担了各个主计算机的CPU 资源,然而,真正的瓶颈并不在这里,而是,数据库服务器那里。数据库服务器都会非常忙的应付各个服务器的查询及操作请求。
因此,通过这个结构图使我们了解到了EJB 根本不能完全解决负载的问题,因为,瓶颈并不在功能模块的所在位置,而是在数据库服务器这里。

6.2 假如分开数据库,数据共享怎么办

有的读者一定会想到下面的这个应用结构,如图2 16所示。


就是把每一个功能服务器后面都部署一个数据库,这样不就解决了上节所说的问题了吗?是的解决了数据库查询负载的问题,然而又出现了新的问题,就是”数据共享”的问题就又不容易解决了。

6.3 网络面临较大压力,让你的应用慢如老牛

我们再向前翻看看如图2 15所示的这种架构中存在两个网络,一个是”A 网”一个是”B网”,这两个网络是不同的。”B 网”往往是局域网,一般带宽是10M/100M,速度较快,因此到还好说,然而,”A 网”往往是互联网或者是利用电信网络互联VPN 网或称广域网。”A 网”的特点是带宽一般较窄,如ADSL 的网络仅仅有512K-2M 的带宽,由于广域网互联的成本较高,所以一般不会有较高的带宽。而在这个网络上恰恰跑的是功能模块和客户端软件之间交换的数据,而这部分数据恰恰优势非常占用带宽的。因此,这个应用架构其运行速度可以想见是多么的慢了。说句不夸张的话,有点想老牛拉破车一样的慢。一个如老牛的系统:目前在中国互联网做运营商网络管理系统的一个大公司,它的一个早期的网管软件就是采用了这种架构来做的C/S 结构的应用系统。有一次,我作为评估者来对其应用系统进行评估,将其部署到一个非运营商大型的网络中的时候,便出现了我们上述描述的情况,速度已经到了难以忍受的地步,打开一个流量图,有时候需要用15分钟的时间才能呈现完整。然而,该系统在开发阶段并没有发现这个问题,为什么呢?因为,他们没有考虑到应用的实际用户连接网络的复杂性,从而给该公司造成较大损失,以至于,这个开发架构被最终遗弃。

7. EJB 活学活用,J2EE 不是必须使用EJB

通过上面小节的讲解似乎好像EJB 和开发Web 应用的B/S 结构的系统关系并不大,其实倒也不然。我们如果把”客户端程序”理解成某一台服务器,这样也是可以被应用的,而且,如果是服务器互相之间做EJB 的调用的话,也就不存在广域网带宽限制的问题了。
但是,如下情况尽量就不要使用EJB 了:
1、较为简单的纯Web 应用开发,不需要用EJB。
2、需要与其他服务程序配合使用的应用,但调用或返回的自定义的网络协议可以解决的应用程序,不需要使用EJB。
3、较多人并发访问的C/S 结构的应用程序,尽量不要使用EJB。

总结

a.EJB实现原理: 就是把原来放到客户端实现的代码放到服务器端,并依靠RMI进行通信。

b.RMI实现原理 :就是通过Java对象可序列化机制实现分布计算。

c.服务器集群: 就是通过RMI的通信,连接不同功能模块的服务器,以实现一个完整的功能。


再次声明:本文转载自https://www.cnblogs.com/strugglion/p/6027318.html


个人见解

技术总是在一个轮回中不断行进,现金的EJB被大家贴上的标签就是已经淘汰的落后技术,只有老旧的银行、企业系统仍在使用,但所谓落后的EJB只是变成了现在流行的所谓微服务。技术会不断的迭代升华,名字会不断的变更,但核心技术是永久留存的,如RMI的概念在分布式互联网应用占有重要地位,我们应该踏踏实实把底层技术学好。


IDEA设置注释模板最佳实践

效果

在方法上输入/**,然后按tab键,生成的效果如下
1536055768001.jpg

配置步骤

1. 在Live Templates中添加模板组,命名随意,主要是为了存放自定义的模板,方便管理

tapd_20988451_base64_1536053182_18.png

2. 在模板组下添加一个模板,具体配置如下

1. Abbreviation 填写 *

(注:曾经我填写的是/** ,发现拦截/**会导致某些情况下无法获取对应参数的)
1536054663460.jpg

2. Template text 填写

1
2
3
4
5
6
7
* 
*
*
$param$
* @author XxxYyyZzz
* @date $date$
*/

3. 点击Edit variables,date字段选择date(),param中填写以下内容

1
groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+='* @param ' + params[i] + ((i < params.size() - 1) ? '\\n ' : '')};return result", methodParameters())

1536054939594.jpg

4. 在Expand with中选择tab,然后点击Apply即可

(也可选择其他按键,表示触发的按键,个人比较喜欢用tab,避免与原有按键冲突)
1536055074276.jpg