奉纸填词说明文档

# fill poem 奉旨填词 一款填词app,基于React Native开发的移动端app,选择词牌名并让这款APP帮助你开始创作 ![yoyo project](./fill_poem/yoyo.png) ## 下载 [通过github release下载](https://github.com/charlesix59/fill_poem/releases) [通过百度云盘下载](https://pan.baidu.com/s/18kJGedL4la8FpQW1myi0ig?pwd=vine) 提取码:vine 除此之外我还在寻找其他简单快速(~~并且免费~~)的下载服务 ## 为什么选择奉纸填词 我们有一个功能非常强大的网站:搜韵! 但是这个网站对移动端的适配并不算太友好,因此我做出了这个app并且加入了一些独特的功能。 以下是该app的优势: - **易用**:您可以非常轻松的上手本应用并使用它辅助您的创作 - **强大**:拥有特色的填词方式以及相当完备的功能 - **干净**:没有广告的困扰,页面干净不混乱 - **美观**:简约与直观的形状搭配合适的色彩 - **安全**:您的数据全部保存在本地,不用担心有人会剽窃您的内容 - **开源**:该APP以MIT LICENSE在Github开源 - **免费**:免费是理所应当的 - **持续维护**:该APP持续更新功能,修改bug - **可自定义**:拥有丰富的自定义功能,打造适合你的app ![intro1](./fill_poem/intro1.jpg) ![intro2](./fill_poem/intro2.jpg) ![intro3](./fill_poem/intro3.jpg) ![intro4](./fill_poem/intro4.jpg) ![intro5](./fill_poem/intro5.jpg) ## 如何使用 1. 将下载好的APK安装到手机 ### 我写好了词想进行韵律的检查 - 您可以在创作标签页点击韵律检查按钮 - 输入您创作的词牌名(请您自行保证输入的词牌名与词谱中的词牌名一致) - 输入您创作的作品内容,作品内容请用【,】或【。】隔开 - 点击检查,您将会看到匹配情况 ### 我想对照词谱进行填词 - 您可以在词谱界面选择自己希望填写的词谱,然后在随后的界面中选择相应的词格 - 点击对应的词格将进入填词界面 - 每个输入框<mark>可以并且仅可以</mark>输入<mark>一个汉字</mark> - 您将可以直观的看到输入的汉字是否与输入框上部的平仄相符,若相符则输入框为绿色,否则为红色。若系统不能确定,则为蓝色,需要您手动确认 - 您输入的第一个韵字将会作为整首词的韵律(已知问题:变韵词,如菩萨蛮,将会被错误识别) - 如果您已经填词结束或者希望暂存内容,可以点击下方的<mark>保存草稿</mark>按钮,然后您将可以在创作界面的草稿箱中找到对应的草稿 - 您可以在草稿箱中点击【操作-编辑】继续您的编辑并进行暂存 - 您可以在草稿箱中点击【操作-预览】来预览您的作品,在此界面可以将您的作品复制或者保存为图片 ### 我想在填词的时候查找字典 - 您可以在填词时随意切换到字典标签,这并不会导致您正在进行的动作被覆盖 - 选择平水韵或者词林正韵,并在其中选择或搜索自己需要的内容 ### 设置中的选项都有什么作用 ![setting](./fill_poem/setting_explain.png) ## 贡献 ### 反馈与意见 您的反馈是一种非常重要的贡献! 在您反馈时您最好在此Github仓库页面创建issue,并详细阐述您的问题,这样会方便我定位与解决问题。 除此之外您也可以通过软件内的【意见与反馈】功能来联系我,或者直接发送邮件到我的邮箱`CharlesMin2001@outlook.com` ### PR 非常欢迎大家修改我的代码,并提交Pull Request,我会尽快审核并通过您的代码。 具体开发有关的内容可以查看下面的【开发指南】条目 ### 我需要帮助! 如果你觉得这个项目还不错,希望能够参与其中并且您拥有以下的技能之一,那么我非常希望您能与我联系!(联系方式见上文) 技能树: - IOS开发(如果您拥有苹果的开发环境或苹果开发者账号就更好了🫶) - 前端开发(React Native) - 平面设计(救救孩子的图标吧! T^T) - UI设计(呜呜) - 古文顾问(在文字释义、诗词韵律等方面给孩子一点帮助) ## 开发指南 > 确保你拥有RN的开发环境 > PS:欢迎你把项目用到期末项目之类的地方(或许……只要不在课堂之外的地方分发无LICIENSE副本都是允许的……) ### 安装依赖 `npm install` ### 运行 运行**Metro**: `npm run start` 启动应用(安卓): `npm run android` 启动应用(ios): `npm run ios` ### ⚠️:此项目依赖均未加入IOS pod 本项目依赖的部分库如下: - ant-design/react-native - react-native-camera-roll - react-native-clipboard - react-navigation/bottom-tabs - react-navigation - realm 部分库需要下载native文件,如果上述依赖出现问题还请您检查依赖是否正确下载、安装与链接

关于这个博客网站

# 关于这个博客网站 这是我的博客网站,我会在这里定下我的规矩 ## 关于网站 如你所见,这个网站的域名属于GitHub,也就是说并非是国内的网站,而是属于我的网站。 我拥有腾讯云服务器与工信部备案的域名,但是我不使用速度更快的腾讯云,有两方面考虑 - 第一:我所在的地区不允许建立博客类网站,是白纸黑字的禁止,这当然是正常又直接的原因 - 第二:就算我将网站部署在了国内,也不能保证他能活下来。虽然我不打算说什么反动的内容,但是我希望直言不讳 ## 关于博客 我的博客主要是有两块内容,一块是分享技术,其实这个扔在CSDN也能做,没什么特别的东西,只不过因为写博客比较方便,以后各种文章就会在博客上首发,其他平台就先往后稍稍 另一块就是分享一些文学类的文章,包括但不限于:古体诗、近体诗、词、曲、骈文、散文(古文)、小说、散文(现代)、剧本。我文笔飘忽不定,诗歌可能非常浪漫,小说散文可能更偏真实,但不绝对。不考虑写连续的长篇小说,感觉没有那么多精力。会挑选一部分觉得能过审的扔在公众号 ## 关于评论 一般来说,我的所有文章都允许评论,但是不排除以后出现不允许评论的情况(我暂时想不到是什么情况,反正没人管我) 评论的内容你们随意写 *注意:我使用的是国外的服务器,所以评论在中国是打不开的* ## 关于我 本人,姓名字号不方便透露,别开我盒。 性别,你觉得是啥就是啥,我不介意。 年龄,每年长一岁,这是静态博客(其实可以动态响应这个年龄) 身高体重,你随便想,想啥是啥 网络上的人设是文学院学生,国学天才,上知天文,下知地理;才高八斗,学富五车;前知五千,后推五十;无所不知,无所不晓,普天之下无出其右之高士。故乡浙江衢州,目前在某北方港口城市 现实中是新时代农民工,卑微的打工人 ## 使用的插件 - 主题:fluid - 评论插件:waline - 文件压缩:hexo-all-minifier - GitHub热力图:Github Calendar

奉纸填词企划书

## 技术选型: 底层框架:React Native 依赖管理:NPM UI:~~React Native Element~~ Ant Design Mobile 路由:react navigation (👍!) 持久化:realm ## 平台 Android IOS(暂不考虑) ## 技术难点 - [x] 异步加载渲染,跳转之后先显示骨架 - [x] 多重路由 解决方案:react navigation - [x] CSS问题 - [x] Tab keepAlive 问题 解决方案:react navigation bottom tab - [x] 中文行显示不全缺失问题 - [x] 自动换格子功能 讲一下实现 - [x] 词谱错了,重新扒一下 - [x] 数据持久化能力 如何使用realm - [x] input如果有焦点,期间无法正常处理点击事件(在手机端是点击收起键盘,正常) - [x] switch动画卡顿问题 根据状态刷新 ## 版本内容 ### later - [ ] <u>*字体大小设置*</u> - [ ] 优化分享样式 - [ ] 优化自定义hooks - [ ] UI美化专项 ### v0.1.0 - [x] 重建保存草稿功能 - [x] 草稿箱功能交互更加现代(点击直接进入预览,然后再操作) ### v0.0.5 - [x] 更新清除缓存的步骤(输入密码) - [x] 修复部分bug <details> <ul><li>UI边框留白</li></ul> </details> ### v0.0.4 - [x] 启动时更新检查 - [x] 文档与注释 ### v0.0.3 - [x] 显示版本号以及正确检查更新 - [x] 更新路由标题栏 - [x] 添加中华新韵 - [x] 修复部分bug <details> <ul><li>无法正确回退</li></ul> <ul><li>保存的文本退格异常</li></ul> </details> ### v0.0.2 内容 - [x] 词韵律检查 - [x] 预览界面 - [x] 导出图片 - [x] 图片署名与签名 - [x] 复制文字 - [x] 草稿编辑 - [x] 自定韵律 - [x] 自定韵律编辑 - [x] 填词页面样式 - [x] 填词界面阻止退出 - [x] 自定义颜色 - [x] 调节一下颜色的样式 - [x] 清除缓存功能 - [x] 意见与反馈 - [x] 检查更新 - [x] 显示版本号 - [x] 关于页面 - [x] Github跳转 - [x] 修复词谱问题 - [x] 词谱显示 - [x] 预览显示 - [x] 修复词律为中的问题 - [x] 修复词韵不为句或韵的问题 - [x] 修复删除韵律后韵律识别不正确问题 - [x] 设置预览界面编辑样式 - [x] 阻止退出 - [ ] <u>*字体大小设置*</u>

小圆季节灏文档|小圆季节灏信息码获取

# 小圆季节灏 GitHub地址:https://github.com/charlesix59/QDU_AFL_V3 ## 获取信息代码 其实原理非常简单,只需要您填写一个json文件,格式如下: ```json { "name":"你的名字", "number":"你的学号", "sclass":"你的班级", "collage":"你的学院", "tname":"你导员的大号", "tel":"你的电话", "address":"你要去的地方", "dormitory":"你的住处" } ``` 复制一份到任意文本编辑器,修改各个值为你需要的值,然后复制,粘贴到小程序中的信息码文本框即可 ## 更新感言 腾讯真的恶心,直接填个人信息无法过审,所以我就只能曲线救国了。 信息代码的获取方法在操作指南中 乞望见谅 2022.9.3 <hr> 有些事说是小心也好,说是形式也罢。 但是体现在个人身上,是痛苦与气愤。 我对如是行径感到恶心。 虽然大多是为了自己,如果下次又有魑魅魍魉在人间横行,我依然会用自己的方式祛魅。 2022.9.2 <hr> 某大学终于打开了虚妄的门扉,推倒了染血的藩篱。这是一场胜利,或许是。 有幸看到<b>**小圆**</b>在此刻退休,目前仍未更新的版本也不会再继续更新了。 对于小圆的退休我并不感到遗憾 > 但愿世间人无病,何妨架上药生尘。 我们还有别的战场要奔赴,哪里有压迫,哪里就有反抗。不合理仍然存在,以真理之名,我仍要与这些恶魔战斗。 无论如何,很高兴看到这一片光辉闪烁着,我们可以暂时唱着欢歌,相拥而眠;不必枕戈待旦,横刀冷对了。 今天,它退休了,但当黑暗再次笼罩之时,我仍将燃起炬火,予你微弱的光芒。 5.26 <hr> 最近校园集结号迁移到微信小程序了,于是我又赶出来一个。 因为是微信小程序,大家能在微信中直接搜索到我就不上传gitee了 从网页到mui到微信小程序这都第三版了,考虑要不要直接uniapp……(那我就快要成为真正的集结号了) ### 早期版本 V2版本:[校园集结号自主请假: 勾勾啊哈哈 (gitee.com)](https://gitee.com/charles-min/QDU_ASLv2) V1版本:[charlesix59/QDU_self_ASF_web (github.com)](https://github.com/charlesix59/QDU_self_ASF_web) ### 一些废话 > 沉默呵,沉默呵!不在沉默中爆发,就在沉默中灭亡——鲁迅 > 哪里有压迫哪里就有反抗——毛泽东

札记1

# 20240328 委托同学帮我把相机寄到北京,今天已经寄到了。 回家打开自己很久没有摸到的相机,回忆起之前问过同学一个问题。 ”你觉得那种状态才是真正的我?“ 与我要好的两个同学的回答都是:”穿的板正的,带着相机到处溜达的状态,是真正的你。“ 这样的说辞实在令人欣慰,但是我却知道,那可能不是真正的我。打扮整齐,带着相机,像一个文青,我也喜欢这样的印象。他们认为我是中产的,城市的,精英的,是带着精致人间潇洒。 但是没有人比我更明白自己的窘迫。当年我在武汉实习,冒着雨回家。口袋里的钱只够吃饭,没有钱去打车。每天都靠疯狂的摄入碳水来维持自己的心情。在被雨水淋透的清冷中,我写道”年少窘迫复几时,旗湿猎猎晚风遒。“当年为了多赚些钱,为了和早点和前女友结婚,自己嫌弃自己的落魄。分居两地,天天为了哄她开心而花心思。 但是最终啊,她还是离开了我。在我实习结束之前的几天。于是在实习结束之前,我迷茫的问自己,我是为了回去陪她才选择不续实习合同,离开这里。但是现在分手了,我还该回去吗?可惜木已成舟,不容更改。于是在离开之前,我吃下了这次在武汉的最后一碗热干面,回到了家里。专心准备了一个月,终于找到了一份合适的工作。这一个月待在家里,无聊,难熬,但是幸好有老妈耐心、细心又小心的照顾。虽然失恋了,但是并不糟糕。 当我拿到offer之后,实习公司也给开出工资来了。几千块钱,对我来说已经很富裕了。于是我果断的去买了一台入门级的相机,然后去了一趟自己梦想了四年的南京。那一趟南京之行很快乐,自己一个人吃,一个人走。当我走上石头城的城墙,仿佛走进了自己四年前的梦里。我终于完成了自己梦想了四年的旅行。 直到那天,我才短暂的体验过几天”中产“的生活。但是我心里知道,我离中产还差着远呢。或许我这一生都达不到所谓中产的标准,但是我知道了,花钱去满足自己的小梦想,是很快乐的一件事情。 人生一世,如此的短暂。时运不齐,那么自己最宏大的理想必然无法完成。而这样的大运,谁又能碰上呢?虽然自己的大梦已经无法完成,但是自己的小梦,此时我已有能力去满足。也许不需要那么大的压力,虽然我现在孤身一人,时常寂寞,而且还很穷。但是能够努力去满足自己的小梦想,好像人生也不枉受那么多辛苦。

窗边

外面下起了雨,不大,不小。 大到刚好让人不能在街上停留,小到刚好没有使得雨声成为噪音。 天很暗,乌云几乎要压下来,看起来这场雨还能下好久。对面的那栋建筑零星的灯火,让黑更加的黑。 公寓的窗前有一块很宽阔的窗台,恰好能坐在上面看这幅雨夜。 我爱下雨的天气,她能让我睡个好觉,还能让我像现在这样,看着窗外,为我的生活添加一点生趣。雨滴蜿蜒在窗户上,正如我的血液流淌在血管里,我的心脏,将血液泵到全身各处,此刻它就在我的胸腔中跳动着,但我却感受不到心跳的感觉。 我望着玻璃,上面是漆黑的墨色,下面是橘黄的灯色,中间半遮半掩的,映出了我白皙的脸,我望着那双眼睛,它让我想起那格索斯,他为了触摸自己而溺死,我不会那么做,因为那样会摸到冰冷的玻璃。 于是我放弃了与自己对视,用胳膊轻轻的抱住自己的膝盖,把头埋下去,我看到了自己新做的指甲:主色调是接近指甲本身的浅杏色,在灯光下能看到其中闪烁蓝色与绿色的小亮片,我很喜欢,但是又遗憾没有人能够与我共享它的美丽。于是我把脸别开,望着宽阔的房间——其实只有几十平米,但是对于一个人还是太大了——一股冷冷的感觉从膈肌扩散到喉咙。 我将手掌抵在自己的胸前,深吸一口气,那格索斯其实也不是那么傻吧,这样想着,随手整理了一下鬓前的一缕头发,将脸贴到了玻璃上,视线轻轻的穿过雨幕飞到窗外的雨中。 “嗯,凉凉的。” 这样想着,又一滴水从玻璃上淌下,比较不同的是,它流在窗户的内侧。

临别

“这就要走了吗?”女生牵着他的两只手,仰起头来,眼泪汪汪地看着他。他感到肚子里酸酸的,没有说话,点了点头。 “那你下次回来会是什么时候啊?”女生继续追问。他低下头,“可能得元旦了吧……”。这是他不愿意说出口的日期。 “那不是还有……”女生不知道是在心中算着日子,还是不愿意说出那个显而易见的答案:“……三个月?” 女生的失落已经跃然脸上了,她不甘又委屈地问:“中间不回来了嘛?” “元旦是最近的假期了,如果我周末回来,路上来回就要花20多个小时,陪你一晚上,第二天一早就要走了……”男生用尽量和缓的语气对她说,这是事实,尽管是如此的残酷,没有一点儿浪漫。男生希望用事实来让她明白他的无奈,希望她不要那么伤心,以至于寸断柔肠。 “你就是个坏蛋!”女生娇嗔道,"说好了陪我考试的!"她甩着男生的手,却没有甩开。 男生将她拥在怀里,在她的耳畔轻声地说:”我也想陪你考试,但是没想到推迟到这个时候了。“ 女生轻轻的推开了他,“我知道啦,你该走了宝贝,”她轻轻帮他整理好衣领,“过去那边要照顾好自己哦,要记得想我。”话音未落,泪水先从下巴上滴了下来,砸到他的心里,泛起了滔天骇浪。 “好哦……”男生答应了他的爱人。他该上路了。他们依依惜别,三步一回首,当进行到第四次的时候,男生回头望过去,那一抹熟悉的影子已经不见了,从他的视线中消失了。他回过头,望着拥挤的人群,泪水溢满了眼眶……一滴、两滴、四滴,再到抑制不住的泪如雨下。 女生从火车站离开,她该回家了。一路上她是呆滞的,仿佛不适应生活又将她心爱的人从她身边抽走了。她在地铁站台上等车,思绪不知飘到了何方。 突然她被人抱住了,突如其来的惊吓让她猛地回过头去,看清了抱住她的那个人,她从惊讶变为了惊喜。 “你怎么在这?”,她几近啜泣的问。 “不走了。我可以在这边再找一份新的工作,对我来说不成问题。我陪着你考试。”他用手轻轻抚摸着她的脑袋。 “嗯!”。她欢畅的笑着,泪水一滴滴都落在男生的肩膀上。

海之血

我的姐姐要成亲了,丈夫是海宁府林太守的公子。 为了筹备这一桩婚事,林太守派了几十艘大船,将这一带的海贼逐出了几十里,这才让公子带着几大箱金银绸缎带着五十个好手乘船而来。为了筹备这一桩亲事,全家上下都要忙疯了。爹整日东跑西跑,把周围十几个岛的亲戚都请了一边,三个叔叔和两个姑姑在家里忙里忙外的拾掇,需要准备几百号人的柴米酒肴。我刚刚成年,作为家里新晋的大人,除了照顾我的小弟弟,也忙着准备婚宴。 姐姐要出嫁,我是极开心的,因为这桩婚事实在是天公作美,林太子的家境自不用说,他自己本身也是个人物,对待别人如此谦逊,又是行船打仗的好把式,而且还读过书,整个海宁郡真是再找不出如此的人才。 但是这几天的忙碌却让我有事顾虑,我怀念我的屿姑娘。隔壁家的姑娘石屿,与我是青梅竹马,她身世凄惨,五岁的时候父母出海,便再没回来,她奶奶年迈昏聩,十几年来一直是我家帮忙照顾她。她和其他本地姑娘不同,她害怕大海,从不出去,靠纺纱织布赚点小钱,常常给我们家做衣服作为回报,姐姐的嫁衣就是出自她手,真是精巧璀璨。因此她的皮肤是白皙的,眼睛不像我们出海的人那么锐利,有着别的姑娘不曾有的柔情,我说不准是从啥时候喜欢上她的,刚开始的时候我待她如同我的妹妹,后来我们就不自觉的越走越近了。 姐姐的出嫁,让我突然发觉,如果我也要娶一个姑娘,那除了我的屿姑娘,还有谁会能让我接受呢? 昨天我在礁石上抱着她,摸着她的头发,告诉她,等姐姐的婚礼完毕,就轮到咱们了。她揽住我的脖子,对我说,让我专心准备婚礼,她等我娶她。 所以今天我的心情兴奋到了极点,不仅仅为了姐姐嫁给林公子。林公子的船下午到了,一时间锣鼓喧天,红花纷飞,村里的老人说自己活了一辈子,没见过这么有排场这么热闹的婚礼。我家里坐满了宾客,笙歌宴乐,觥筹交错。林太守的五十个小伙子站在我家屋前,形貌昂然,一看就是出海多次的硬汉子。叔叔劝他们酒,他们却不为所动,真真令人折服的纪律。 这次宴会如此热闹,不觉夜色已深,按照习俗,林公子今晚赴完宴,明天一早就要带姐姐回府上了。爹劝那群汉子吃肉喝酒,林公子觉得夜色已深,便让他们放开吃喝去了。等外宾都走的差不多了,姐姐喊来我和爹,从颈上取下我们家传的项链,海之泪,一颗映射着红色光芒的珍珠。在火光之下,它显得愈发鲜红,真如一滴浑圆的血。姐将这串项链交给我,她从此是林家的人,这串项链理应在我家继续流传。姐对我说:”早些娶个妻子,别让这串项链闲着“。林公子也在旁边打趣,问我可有意中人。我握住了拳头,深吸了一口气,说出:”石屿“。 老爹点点头,姐姐也微笑,林公子打听出这个石浪就是隔壁姑娘时,握住了我的手,”弟弟你如何不早点告诉我,请她一同赴宴。今日天色虽晚,想必石姑娘未曾睡,不如请她来吃点喜酒,你们早做打算!“姐对这个说法很满意,爹也没有反对,于是林公子便点上几个随从,我们便即兴出发去浪姑娘家。 当往她家走时,我的心跳的好快,有些心慌。大概是是因为见到之后便可以定下终生大事,因而激动吧。但当我们走到她家门口时,却听到一群男人大声的谈笑,刚才在家里太嘈杂,不曾听到这边的声音。我胸口堵得慌,大步走过去,推开门,引入眼前的红色的血迹,一只白皙的断手停在织布机上。我握紧随身携带的匕首,撞进卧房,几个海贼居然在糟踏我的姑娘,蹂躏她的遗体,我来不及想,下一秒刀尖就已经插进了行暴的混蛋的身体,将它撞到墙上。这几个贼人愣了一下,纷纷拿起了自己的武器。我眼前黑了一下,再睁开时,几个贼人已经被林公子的人劈死。我手上依然握住祖传的海之血,猛然瘫软了下去。 ——这篇文章,是我几天前做的一个梦,我在醒后趁着记忆犹新,大概梳理了一下梦中的内容,今天凭借所剩无多的回忆将其补充为一篇短文。这场梦着实荒诞,但是却又如此真实,令人后怕。惟愿天下人皆有机会保护所爱免受灾厄。

海外诸国志·卷三·第二篇——骡

> 騾:騾馬也——《廣韻》 大明共和国东北部,有一座叫做青州的城市。青州沃野千里,都种麦子,一到冬春,绿油油的一片。北方人擅长养马养驴,当然,也擅养骡子,用来做农活。 青州的每个村子都有一个农业大队,里面会养一些骡子。**这些骡子虽然放在生产大队养着,但是几乎各家各户都认为其中有几头是自家的——曾经是自己的骡子,自家负责喂养,到时候需要用也是自家牵去用,只是养在农业大队的大院中而已。** 不过既然名义上是农业大队的骡子,大队的领导自然也会负责管理。麦坡农业大队的房队丞曾经是县里的太医署医助教,不知何故被贬到了这个地方。但是他以前只是管给人治病的小吏,现在虽然不入九品之列,但是却是一村之主,无人能管。只是村子实在没有是么事情,房队丞又是一个励精图治的人,不愿游手好闲,于是便折腾起村里的骡子来。 刚开始,**房队丞嫌这些骡子一年就干农忙几个月,平时也吃草料,只吃不干,也忒浪费。于是他召集百姓,让他们只要报告大队,就算不在农忙时节,也可以随时可以拉着骡子去干活。** 这条规定一出,村中老少都很欢喜,于是大家都把驴子喂的饱饱的,秋天收完麦子带骡子去驮果子,冬天又驮白菜,但凡有需要人力搬运的东西,都爱寻骡子来,大家都驮些东西去贩卖,甚至都让集市热闹起来了。 于是大家都觉得骡子多了好,于是疯狂的买起小骡子来。自古有得必有失,畜生吃得多,干的多,屙的也多,更何况现在牲口多了,马棚大小却没变。以前还算干净的马棚此刻飘荡着一股浓烈的味道,人闻都不忍,怎么能忍得住去看呢?而且骡子身上也乱糟糟的,大家也为此颇有怨言。 房队丞自然要提大家解难,于是他规定,不许让骡子到处乱屙,猫狗尚且能教会,骡子如何就教不会,**于是房队丞择了一个良辰吉日对着骡子高喊,不许随地大小便,不然就抽鞭子**!自然,这群畜生没少被抽。起初大家还觉得,抽的好,这群骡子浑身弄得这副鬼样,一定是吃好了犯贱,四出糟蹋,教训的对。 但是当大家看到自己的骡子被抽打的皮开肉绽的时候,不免又动了恻隐之心,可惜对于畜生,大家又有多么上心呢?挨了许多日的毒打,骡子到也变得安分起来了,这群畜生习惯去指定的地方排泄了,但是凡有随地排泄的骡子,反而会收到更严重的鞭笞,整个村子的上空都盘旋着骡子被抽打时痛苦的嘶鸣。 正巧,这年又是一个大丰年,大家都拉着自己的骡子去田里干活,**让骡子驮着累累若小山的袋子,或者拉着轮子深深陷入地面的车,行走在阡陌之上,昼夜不停,全然不在乎这群畜生身上满是被抽出来的伤口**。 经过这一番折腾之后,村中的牲口官一清点骡子的数量,发现甚至比房队丞来时更少。房队丞一下子焦躁起来,一下子死了这么多畜生,似乎略显得自己的不称职。**为了显示出自己敬业,他又呕心沥血想出了一个十分`科学`的方法,为什么不让这些骡子交配产子呢?于是,一场轰轰烈烈的催产运动开启了** 骡子不愿意交配?那就将公骡子与母骡子关在一个小围栏中。骡子生不出来?那就抛开肚子给接出来。骡子要是不吃软的,那就来硬的。总而言之,就是必须让骡子多多生产。这种做法,却不是人人都能理解的,有些人理解了房队丞的良苦用心,有些人却心疼骡子仍是生灵,不忍残害。 只恨苍天何薄,由于房队丞将两头骡子扔在一小块圈里,骡子屎尿无处,最后一种恶疾在骡子之中流行开来。得了这种病,骡子虽然不会一命呜呼,也不会变得形销骨立,但是却会变得真真切切的完全无法受孕。 正在催产的房队丞怎么能容忍这种事情?**于是房队丞这次召集了乡亲们到农业大队的大院中,不知道是对人喊还是对骡子喊:“为了防控时疫,保护更多的骡子免受其害,凡是染病的骡子,一律杀死!”** 但是对于百姓来说,骡子本就无法生育,而就因为一种让骡子无法生育的病,就把骡子杀死,这也太糟蹋了。于是房队丞又搬出什么流疫或许还有其他病状,流疫或许会让人染上之类的理论,封住了乡亲们的嘴。但是随着栏中的骡子越来越少,骡子被管制的原来越重,很快,农民和骡子居然站到了同一条船上。 **终于,在一个<font color=red>伸手不见五指的满月之夜</font>,每天吃着苦药的浑身裹着白布的三月没有出过马棚的骡子们,在<font color=red>皎洁的月光下</font>将房队丞踏到坚硬的蹄子之下**

海外诸国志·卷三·第一篇——铎

# 海外诸国志·卷三·第一篇——铎 > 鐸:古者將有新令,必奮木鐸以警衆——《周禮·天官·小宰》之《注》 莲花县是大明共和国的沿海县城,县令姓李。 李县令似乎励精图治,大明律中规定,各地官员办案之日,一侯不少于一旬[^1],而直隶总督规定直隶各地官员,五日一休,到了东海郡,郡守要求十日一休。至于李县令,他令自己每日办案,从无休假。 李县令也非常民主。大明律规定,每个公民都有权参与国家事务。而直隶总督规定,各级官员升堂断案都需要有民众监督。到了东海郡,郡守要求各县必须抽取一部分人参与升堂。至于李县长,他令莲花县已冠未知天命[^2]的所有人都必须每日到县衙参与升堂。 所以每日县衙周围都人头攒动,县尉每日都领着几千军士维护秩序。于是莲花县不仅能够聚集数万市民参与县政,还能让其秩序井然,县令真真的是清官明吏。 于是莲花县的一天从县令的升堂开始。 ## “升堂!” 升堂的时间,自然是在日出[^3],若是夏季还好,但此刻是冬季,天气寒冷,甭说日出,都没有平旦破晓的迹象。甲兵举着火,催促百姓来到县衙,似乎是有些不好受。不过百姓到这里是行使自己的政治权利,似乎也值得。大家受了一早上的冻,回到家中,燃起将熄的炉子,钻进被衾中继续睡,毕竟冬天没法种地。 ## “升堂!” 升堂的时间若是在冬季,似乎不影响农务,但是如果在春季,百姓每日需要耕地除草灌溉播种施肥除虫,每日忙不迭,而县令此时似乎需要更长的时间断案。 百姓自在县衙外议论。 “这么长时间了,这又是啥案子?” “听说是张三家的牛被李嗣抢了。” “这还用断?俺亲眼看见李嗣那天牵走了张三家的牛。而且李嗣本来就不种地,哪来的耕牛啊?” “哎,不能这么说,李嗣毕竟是县令的侄子,而且我听说昨天晚上李嗣就去县令家……” 这时在这群人旁边的军士抖了抖自己身上的盔甲,将长枪往地上一戳,“保持安静,不许多嘴!” 堂中县令开始断案了,“堂下何人,有何冤情?” 粗布短褐的人首先开口:“青天大老爷呀,青天大老爷,小人张三,家里有一头耕地的牛,昨天本来打算牵着牛去耕地,刚出家门忘了拿中午的干粮,于是回去拿,就这一刻的工夫,李嗣就牵着俺家的牛要走,俺上前阻拦,反被他家丁一顿毒打,小民实在委屈,请老爷明断!” 李县令转头看向李嗣,厉声道:”李嗣,你可知罪?“ 李嗣磕了一个头,”老爷,冤枉啊老爷,这实在是我家的牛,是张三诬告!“ 李县令举起惊堂木,狠狠地拍到桌上,”你们二人,各执一词,不必多说。本官自有办法查明。左右,牵牛来!“ 很快一头健壮的黄牛就被牵到了县衙的院子中。 县令问李嗣:”你说这是你的牛,你可说明这头牛的牛左后腿是什么颜色?“ ”回大人,是褐色!“ 这时堂外的官吏对堂高喊:”大人,是褐色!“ 县令转头问张三,”你说这是你的牛,你可说明这头牛共有多少根毛?“ 张三抬起头,左右张望了一番,望着太守,欲哭无泪,“这……这……小人着实不知啊!” 县令又猛地摔了一下惊堂木,“大胆张三!胆敢诬告他人!来人,给我拉下去痛打五十大板!” 在张三的一片喊冤与哀嚎声中,李嗣悄悄地凑近李县令,”叔,说好的左后腿,今日就给您送到府上。“ ## ”升堂!“ 如果是在春天,在太阳下站一会似乎还是一件挺舒服的事情,而现在正当盛夏,太阳洒下毒辣辣的光,这样站着人汗流如雨,实在有点不体面。县令依然在堂中办案,大家每天都在乞求快点结束。 但是今天并不太炎热,因为天空乌云如墨,似乎要下大雨了。 虽然不甚炎热,但是农民却更希望快点结束今日。因为麦子都已经熟了,如果不赶紧收割贮存,一旦淋雨,一年的收成就这样毁于一旦了。 但是太守讲的正酣,此时大堂带上了一个衣衫褴褛的人,身上处处结痂流血,体无完肤,脸上乌黑,浑身上下只有一双眼睛还是白色的,死死的盯着前方。 太守从堂中走出,面向诸位百姓,指着这个血淋淋的人,大声喊道:”乡亲们,看看这个人,昨日升堂议事,他竟然敢自行逃走!我大明素来以民为贵,尊重各地百姓的意见,这是多么宝贵的机会!此人,非但不珍惜,反而逃走,实在是有违大明律令,不尊重我们的朝廷!可耻可恨!大家看好了,这就是对抗官府的下场!我李某自从去年当此县令,素来勤政爱民,我今日饶他一命,让他回去,接受教育,往后爱国为民,为大明贡献自己!“ 太守慷慨的讲了一刻钟,没有要停止的意思。但是天公不作美,此刻突然下了倾盆大雨。太守忙回到大堂,而一群百姓虽然心急如焚,却不敢走动。兵士各个严阵以待,守在四周。 这是主簿在县衙的偏房喊:”乡亲们,大家来这份赞颂李县令的表上印上手印再回去,大家不要着急,排成一队,都按完再走!“ ”第几次了?“人群中一个人悄悄地问。 ”谁知道呢?起码七八次了吧“ ”我的麦子,麦子,你们还关心这个!“ ”行了,别再说了,看见那个人没有,血还没干呢!“ ## ”升堂!“ 若是夏天,飞禽走兽草木果实还有许多,大家搜罗搜罗还能果腹。但是如今已是深秋,各家早无余粮。不说余粮,就是树皮野菜,此时也都没有了。此时大家面黄肌瘦,一脸的饿相。路上已经有几具饿殍了。今天似乎没有太多事务,李县令笑眯眯的接受了钱老板送的”爱民如子“的匾额,大家在莲花县人民丰足的奏章中按下手印,今天就退堂了。 **今天没有升堂。** **今天没有升堂。** **今天没有升堂。** **今天没有升堂。** **今天没有升堂。** [^1]: 一侯15日,一旬10日 [^2]: 人20而冠,五十而知天命 [^3]: 日出是指五点

海外诸国志·卷三·第三篇——印

# 海外诸国志·卷三·第三篇——印 > 印:執政所持信也——《说文解字》 承殷民是一直在京畿港口沽口做买卖的布商,细数起来他家的承实布坊的历史甚至要比大明共和国更长一些。他家多世经营,虽然历经风雨,家业已大不如从前,但是所幸如今布店的生意非常红火,现在的布店掌柜承殷民也是诚实君子,沽口市民都愿意光顾他的店铺。 直到有一天,两个官差登门,神态凶狠,挤开看布匹的百姓,随手抓起两块布检视一番,又扔了回去。其中一个高个子的官差喊,“这里的掌柜呢?赶紧出来?” 承殷民急匆匆的从后院过来,做了个揖,“二位官员,来小店何事呀?” “营业许可有没有?”矮胖的官差问。 承殷民抬起头来看了看,犹豫了以下,”这……小民小本经营,素来没有听说过什么经营许可呀。“ 高官差做出一副愤怒的神情,”什么?没有经营许可你也敢做生意?“ 承殷民吓得连连摆手后退,这时一个老太太拿着一块布过来,”官爷您看,这承老板一向是童叟无欺的,布质量好,价格也公道,您行个好,啊。“ 高官差一把夺过布,扔到老太太的脸上,导致老太太一个踉跄,差点摔倒,所幸有人在后面扶了一把。 矮胖官差对承殷民说:”得了,你赶紧去府伊处办一张经营许可,办不出来,你这店就甭开了!“ 于是,店中顾客便全被赶出去,店铺大门也被”砰“的关上了。 承殷民万分无奈,只得去府伊处办理经营许可。承殷民不敢怠慢,星夜进京,第二天一早便去拜见府伊,府伊听明来由,笑了笑,**“我素来不执管经营,此事属盐运使司大人分管,你可去问他。”** 于是承殷民急忙又去寻找盐运使司,使司大人看到承殷民,厌烦的说:**“你从哪里来,回哪里去,让你们那边的盐运司库大使给你文书之后再来找我。”** 承殷民四处碰壁,只得怏怏回家。但是他又焦急难眠,打定主意,明天天一亮就去寻司库大人。第二天才刚鸡鸣,承殷民就急匆匆的去往盐运司库大使处。不想到了之后,司库大使一脸不爽的问:**“你有没有食品质量证明?食品安全证明?合法经营保证书?合理竞争保证书?反垄断证明?……”** 一串词汇听的承殷民头晕目眩,他都不知道自己当天是如何退出司库衙门,又是如何回到家中,他脑子里全都是布匹怎么样才能过视频安全检验,怎么样才能让竞争对手为自己签合理经营证明…… --- 夫人见他如此魂不守舍、闷闷不乐,就问“相公可有什么烦恼,能否说出来让我替你分忧?” 承殷民就将今日之事和盘托出,捂着额头恸哭,捶胸顿足,”我承家几代家业,毁于一旦呀!“。夫人听了,灵机一动,对承殷民说:“之所以如此,不过是他们这些狗官相互推卸,我听说当今县丞的家父与相公的祖上曾是至交,不如找他说情,让司库大使速速办出文书,或许可以成事呀。” 承殷民此时犹如醍醐灌顶,激动的抓住夫人的手,高兴的说:“还是夫人冰雪聪慧,好,就依夫人,就依夫人!” 第二天,承殷民又是早早的去找县令,得到应允的信件,又兴奋的跑去盐库司处,希望赶紧办妥此时。司库见到县令来信,倒也未曾耽搁,**速速拿出了一份《百货经营证明书》,交给承殷民,”你先去找府伊与使司大人签字,若是他们签字,我自当签字。“** 于是承殷民又星夜前往京城,他在客栈里辗转难寐,自己这些天四处奔波,着实辛苦,眼看胜利就在眼前,怎能不兴奋呢?”我终于要拿到许可了,这样也儿孙也无愧祖宗吧!“ 于是他先去访问府伊,在府外炎热的天气下苦苦等待了五个小时,终于得见府伊,可喜府伊为人正直,虽然对此事感到不悦,却也依然给签了字。于是承殷民又兴冲冲的到使司大人处。 使司大人虽然公务不算繁忙,很快就能拜见,但是使司却不太愿意签字,至少不太愿意无偿签字。**”本官一向为官清廉,不取百姓分毫。无奈这府中公事繁忙,一年到头损耗颇多,府中连一竿好使的笔都没有,这签字呢,又多费笔墨,哎,这让本官非常头疼呀。你可知道本官的良苦用心?“** 承殷民忙说:”明白明白!“随即从怀中掏出一个系的紧紧的包袱,一层层剥开里面装着一大包白花花的银子,双手奉上。 使司笑逐颜开,让官吏速速将银子取来,咧着嘴说:“哎呀,我作为共和国的官员,理应不辜负政府的信任,怎么能受贿呢?可是念在你一片好心,愿意资助政府,我就领下你的好意了,哈哈哈哈。“紧接着,低头提笔,潇洒的签下了他的大名。 虽然损失了不少钱财,但是承殷民想到自己的店铺即将开张,心里还是兴奋的。于是他又连夜赶回了沽口,第二天一早,就去找到司库大使,高兴的将证明交给司库。**可惜司库不在,库吏让承殷民先将文书留下,等司库回来,确认无误就将经营许可送到府上。** 于是承殷民非常喜悦的离开了。 他前脚刚刚跨过库衙的门槛,库吏就慌忙报告司库,”大人,他走了“。 司库这是弹弹衣服,阔步走到府上,问,”让你们办的事办好了吗?“ ”办好了“ ”哈哈哈哈哈,好!知道我为什么不给他“布匹经营证明”而给他“百货经营证明”吗?百货证明难办,而布匹经营证明容易,我拖延他时间的功夫,已经派人把沽口百货店最后一个名额占住了,哈哈哈哈哈!“ 库吏一揖,”司库大人神算!“ --- 正当承殷民与夫人把酒言欢之时,库吏登门,承殷民惊喜迎接:”上官亲来辛苦,如蒙不弃,先来寒舍小酌两杯淡酒。“ 库吏没什么表情,”不用了,我奉司库大人之命,特来告知,百货店数量已满,所以你的店铺经营许可不予发放。“说完,转身便走了。 承殷民听说这话,紧紧的握着手中的酒杯,额头上的青筋都可以清晰的看见了,他头脑中再没有其他想法,只能感受到手中的酒杯。陶瓷的杯子,怎么能被人握碎?但是这个杯子,却被这个孱弱的商人,握成了齑粉。 这天晚上,月亮缺了一块,在月光中,一个人将自己的脖子放到了挂在房梁的绳子上。<font color=red>巨大的声响惊醒了床上的另一个人,她慌忙割断绳子,扶起自缢者,两个人相拥痛哭起来。</font>

海外诸国志·卷三·第五篇——锥

# 海外诸国志·卷三·第五篇——锥 > 夫贤士之处世也,譬若锥之处囊中……使遂蚤得处囊中,乃颖脱而出,非特其末见而已!——史记·平原君虞卿列传 ## 壹 马车的声音震落了清晨的露珠,拉车的马是两头结实的好马,而车却是粗糙的木板车。驾车的人是一个身材与五官都很标准的男子,车上装着几个箱子、许多捆起来的布匹与一个神情黯然的姑娘。 马车驶进一个村子后慢了下来,最终停到了一户人家面前。男人走下来,拆开篱笆上小门的门栓,同时,一个老汉从堂屋中探出来,男人望见他,热情的喊了一声: “爹!” 老汉的神情顿时变得放松,朝屋里叫唤,“他娘,儿回来了!”。老汉先出门迎接,一会一个老妇人也趋庭而来。 “哎呦,槛儿,还有翠云,今个怎么回来了?快进屋坐,坐!”,边说边挺身扶车上的女子下车。男子从车上拿下几匹布,“掌柜这次差我去运送布匹,正好路过咱们这边,我寻思顺便来看看您俩,顺便捎几匹布回来”……一家人相处的时光应当是欢乐的。 可茶水没喝几杯,男子便起身去,托词道:“刘构上次托我给他带点东西,我先去给他送去,一会回来。”说罢,便匆匆去找他这位铁匠朋友。 ## 贰 村子里这位铁匠是周围几个村有名的巧手,继承了他爹的手艺,打的农具结实又顺手。这天他依然是奋力挥锤打铁。“狗子!”熟悉的声音呼唤着他的诨名,他抬起头,看见刘槛站在他的面前。“哎,刘大,你咋来了?去后面一坐,我给你倒杯水,整好我也歇一歇。”说罢,他停下了手头的动作,擦了擦汗,完成了这块锄头的收尾工作,然后坐到了刘槛的旁边。 “兄弟,你啥都能打吗?“ “当然,哥是谁啊!” 刘槛看了看四周,握住他肩膀,轻声说,”你会铸剑吗?” 他听了这话,短暂的怔了一会,轻声回答:”我还真没做过这玩意,再说,我也没有证啊,官府不让造的。“ 刘槛听罢思索了一会,继续问到:“狗子,那你肯定会造锥子吧?你就帮我造一把更细更长的锥子,不需要多耐用,但是一定要锋利,最好两边能开刃。”说完拿出来一块银子,推到他的手心里,“这些够吗?”。 他颠了颠手上的钱,沉甸甸的,他小心的问,“哥们,这是咋了?这么多钱,都够去城里打一把了吧?这是有啥想不开的?”。 刘槛松开他的肩膀,“兄弟你有所不知,这次掌柜让我去送货,之前押货的伙计伤着了。我听说最近这一路上不太太平,我走的急,没东西防身,这不就想……”刘槛盯着他,“我明天就该赶路了,这事还挺急的,你看到晚上能不能打出来?” 他对望着一直以来的好友,又捏了捏手上的银子,“兄弟你放心,天黑你来取就行了。但是这钱太多了,我不能要。”说罢手摊开伸了出去。 刘槛连忙握住他的手给他推了回去,“狗子,钱少了我不放心。掌柜的说,事成之后给大钱呢!你就按这些钱的质量打就行!” 刘槛跟狗子吩咐完,又赶回家去。而他则停下手头的农具,专心打造自己出产的第一把“剑”,最终他望着这个长3尺余,尖锐非常,一侧开刃并且还带着一个木制剑柄的三角形“锥子”,擦了一把头上的汗。 ## 叁 刘槛第二天带着剑、布匹、箱子和女子,从家里出发,绕了一条路又返回了城里。将车上的东西打点好,还了马车、马匹、箱子和布匹,带着剑和女子回到自己的家中。 没错,他对所有人都撒了谎,除了自己和眼前的这位女子,谁都不知道事情的始末。 他将剑藏在卧室的柜子顶上,用麻布盖住。那个女子还是坐在床上看着他做完这一切。 ”夫君,我们为什么还要回来?“听到妻子的呼唤,他转过身来,牵住妻子的手,”翠云,我的工作与家业都在城里,若会到村里,又要到哪去找这样赚钱的工作呢?更何况回去之后,你就不仅得收拾家内,还得照顾公婆、纺纱织布,必然会委屈劳累许多。更何况你已经有了身孕,留在城中,也更好找人给你接生。“说罢,他用手轻轻抚摸着妻子的肚子。显然她还在妊娠的初期,小腹只是微微的隆起。 她将丈夫的手贴到自己的脸上,轻轻的抚摸,一颗泪滴到了这只手上。”夫君,他们可是官府的人,上次就对妾身百般侮辱,若不是妾身哄骗吓唬他们,恐怕已经被他们……“,她抱住他的胳膊,啜泣起来。他抚摸着妻子的头发,安慰道:”不要担心,翠云,我这次跟狗子要了一把宝剑,若是他们敢来,我一定跟他们豁出去,要他们有来无回!这些狗腿子,谅他们也不敢这么欺负人!“她听到丈夫的坚决,用胳膊搂住丈夫的脖子,将其拥入怀中。刘槛感受到妻子的泪水流到了自己的脖子上。他心疼的抱住她,全神的感受着自己最珍重之物在自己怀中这温暖的感觉。 ## 肆 刘槛在妻子身旁守了几天,已过接近一旬,他和妻子都懈怠起来。于是,他回归自己工作了。日子一天天过去,翠云也渐渐的认为风波已平,展眼过去半月有余了,她胆子也大了起来,也敢自己去上街买菜了。 这种专横擅权鱼肉百姓的官吏都该死绝,也一定会死绝,不是吗?也许他们已经伏罪受诛了呢? 善良的人总会这么想。这天翠云买了丈夫爱吃的一家大饼,又买了一些猪肉,准备给辛苦一天的丈夫做爱吃的饭菜。她走进家门,正准备将门锁上,却发现门被人拽住,她丢下篮子,用尽全身力气想去关上房门,却无奈一个瘦弱女子怎么比得过彪形大汉的力气。门被无情的拽开,一胖一瘦两个官吏走了进来。 ## 伍 刘槛觉得今天心神不宁,他做完工作就匆匆赶回家里。他也不知道为何他会如此心慌,胸口好像被大石压住,沉闷喘不过气来。他急匆匆的走,额头上冒出了细密的汗珠。他走到了他家的那条街,心更是怦怦直跳,他深深地吸了一口气,继续朝着家的方向走,快到家了,他告诉自己。但是他好像听到了女人的尖叫,他心里更慌乱、脚步更快了。他越来越近,声音却更清晰了。他的感到有点眩晕,太阳穴附近的青筋凸现出来,额头上豆大的汗珠正往下流淌。 “肏!” 他爆发出了一声呐喊,双腿如同生风,疯一般的重进自己的家门,他看到地上散落的物品,听到自己妻子痛苦的呼喊。 砰!他推开了寝室的房门,那个瘦的官吏一把拽住他的衣服,将他甩到了墙上。 砰!他后脑勺撞到了墙上,他感觉有点眩晕,睁开眼睛,一把明晃晃的大刀在以及的眼前晃悠。“小六啊,把这个王八蛋子给我看好了!”胖子回头对着瘦子说,随后继续将自己的阳具捅入翠云的身体。 砰!瘦子拽着他将他推到柜子与墙形成的角落,摁住他的身体。柜子摇晃了一下。他努力将脸别过去,他望到了一头肥猪,骑在自己心爱的女人身上!他继续往下看,他从下看,翠花的下面白色与红色的液体交杂着,身上有好几处伤痕,他看向她的脸,泪水与口水、汗水交杂着。他从未见过她这么狼狈的模样,她还想呼喊,她张着嘴,但是已经喊不出什么来了。她的头发凌乱,有一部分被沾湿了。他望着她的眼睛,已经没有一点神采的眼睛,这双眼睛忽然注意到了自己,于是它终于像活着一样,转动了一下,望着他,一点点光芒闪耀着,两颗泪水淌了下来。 砰!他的脑袋仿佛被人用万斤的石头砸中了一样,耳鸣在他的颅骨内轰炸。他想要移动以及的胳膊,但是却被紧紧的钳制住。“老曹,你快点!”瘦子显然已经快要没有了耐心,他回过头去,催促老曹。刘槛觉得自己胸口的压力变小了,他活动着胳膊,反手摸到了柜子上的那柄剑。刷啦,剑被抽出来了,瘦子感觉到自己的胳膊受到了一股力量的冲撞,他赶紧转过身去,映入他眼帘的是一个眼眶通红的男人与一柄造型奇特的剑,他被撞开了,紧接着是腹部感到一股清凉,胖子听到异响转过身去,一柄串着瘦子的剑正在向自己刺过来。 砰!刘槛应该是用尽了此生全部的力气,瘦子和胖子两个人被一柄剑串了起来,撞在了一起。血液从胖子的嘴角流出来,他动了动手指,想要说什么。身上已经沾满了鲜血的刘槛愤怒的咬紧了牙,使劲将这把剑往开刃的那个方向拽。那个胖子露出了痛苦的神情,从肚子中流出来了各种各样颜色的东西,随后两个人都咽气了。 翠云从胖子的身下奋力爬了出来,握住剑柄的刘槛还在喘着粗气,他悲痛的看向自己的妻子,伸出了自己的手。她握住了。两个人的手上都沾满了鲜血。她艰难的直起身子,眼泪不住的往下流。 “去洗个澡换衣服,翠云,我们一块逃走!” 她摇了摇头,咬了咬自己的嘴唇,忍住眼泪,挤出了一个微笑。她紧紧的握住他的手,向前奋力的移动着自己的身体。 “翠云?你想干嘛?翠云!” 她猛地用力往前一探,锥子穿过了她的身体。

海外诸国志·卷三·第四篇——朱

# 海外诸国志·卷三·第四篇——朱 朱教谕是前朝著名理学家朱苳之后。一方面,他因自己祖上是朱子而感到自豪,另一方,又对自己才当一个小小的教谕感到不满。他觉得自己好歹也得当一个国子监祭酒——虽然他或许不关心和他同辈的朱子之后已经有几百人——如今的祭酒出身庶民,他十几年的寒窗苦读怎么能赶得上我作为朱子之后的无上荣光? 朱教谕最常想的一件事就是我们今天的教育出了什么问题。他是一个愤世嫉俗的人,常觉得自己怀才不遇。我们今天的教育,一定有很大的问题——随着今年秋试的开始,他更加肯定了他的这套理论。 朱教谕是当地秋试的考官,大明共和国的秋试只考作文,当年的题目是: > 我国近年来推行‘民主监察’制度,国家坚持人民至上、民主至上,大力推进监察错失,全国人民积极参与。 > 人们来到衙门,进行听证和监察;茶楼酒肆兴隆,远行商旅暂停,春耕秋实有序推迟。监察听证使人们变得更集中。 > 县令乡甲,自愿接受听证;军旅接受调度,守卫百姓安全;衙门断案办事,坚持依法而行;大小人事财政,一律公开透明;四方人民百姓,大力赞美政策。‘民主监察制度’使人们变得更自由。 > 请综合以上材料,以“民主检查的集中与自由”为题,写一篇文章。 朱教谕看着这个题目,心情大好:这样中庸的题目,真的是有圣人的才能才能想的出来。我祖上朱子,大力推行圣贤之道,最终总结出了天下都接受的唯一的解释。就因为这样,国家才有机会推行八股文这种完美的体裁。八股,多么美丽的数字,从头到尾,层层递进,前呼后应,详略得当,多么的整齐,多么的符合中庸之道。而这个题目,完美的符合了八股文的行文思路,考生一定得写出完美的八股文才能对得起这个题目。 看完题目,朱教谕开始阅卷,朱教谕阅卷有自己的一套规范——虽然大明共和国大多数考官都是这样做的——从头到尾看下去,八股之中,一股都不能出纰漏,如果出现纰漏,直接判为不合格,不再往下看了。 有一些朱教谕之看了破题就受不了了,什么“监察听证是有利于人民的好制度”,这哪有点破题目的集中与自由;什么“监察听证有许多的好处,但是也导致……”,居然敢妄评国政;甚至还有“民主检查的集中是”,直接进入承题,根本无法接受。到了承题,朱教谕还是继续上火,凡是不讨论“集中是为了自由”这一观点,自然都被朱教谕朱笔一挥,扫出孙山。 自然,后面几股,不合朱教谕意思的考卷,自然都被淘汰。不过只是八股写的不标准,还是小事,更令朱教谕恼火的是居然有人胆敢写八股之外的文体,凡是不和八股的,朱教谕甚至懒得过目,随后放到一边。而“之乎者也”之类的散文,朱教谕看不懂这是怎么意思,也懒得看这是什么意思,愤愤的讲它扔到一边,而写诗文的,则直接被扔进垃圾桶,还要吐上一口唾沫,表达朱教谕对这般风雅的嫉恨。 当然,这些问题也不算最严重的,最严重的当然要属原则上的错误。原则上的错误,最基础的就是评论时政。八股文虽然要有政治,但是政治不是去议论的,而是去论述与赞美的,一定程度上,作文就是看赞美的水平,如果赞美的不够优雅与真挚,就是没有将自己的怨恨埋在心底,口是心非。更严重一级的呢,当然是与主流价值观不符,俗称就是腿是歪的。就拿民主听证制度来说,如果这两年来赞美,当然就是符合主流价值,如果前些年说呢,自然就是不符合主流价值。毕竟价值是流动的嘛,变一变也不奇怪。更进一步的问题是对政府的批评,如果胆敢批评政府,那么一定会被揭开密封性,禁考三年。<mark>比这更严重的是对目前当政的湘派的冒犯</mark>,至今已经掌权近百年了,如果有什么含沙射影的文字,必然会被拘禁教育。<mark>最最严重的错误呢,当然是对当今总统不敬</mark>。当今总统名叫董远凹,总统自然是人民选出来的总统四方天下的人物,姓名的自然要有所避讳。故而三字都是讳,若是谁胆敢冲撞了总统的名讳,免不了牢狱之灾。 虽然没有明文规定,但是在朱教谕心目中,自己的祖上朱子也是不可冒犯的,如果冒犯了朱子的名讳,自然也和上述错误一等,直接除去名字,如果别挑出刺来,还要被禁考三年,当然,还要忍受朱教谕将唾液吐到自己卷子上的刑法——虽然在多数情况下考生并不知情。 不过令朱教谕感到比较欣慰的事情是有十几篇文章写的非常得体,文字流畅,八股明确,点抹有道,深深的博得了朱教谕的欢心。虽然这十几篇文章大同小异,如同出自一人之手,但是朱教谕丝毫不在意。毕竟是得到了圣人真传,深喑中庸之道,既然是出自朱子的标准的大道,千人一面并不为奇。于是,这十几个人都被朱教谕赋予了优异的成绩。 正当朱教谕看过一篇“符合大道”的文章,暗自欣喜的时候,下一篇文章却出现的非常不合时宜。朱教谕一看,符合八股之道,往下看去,却是“监察制度”如何劳民伤众,天理不容。朱教谕看到这种文章,心底生气一股怒火,他说民无暇春耕,苦不堪言,我为何不知道?一定是一派胡言!而看到后面,这篇大逆不道的文章居然敢写下:<mark>“之前的被人们称为圣人的朱苳,实际上没有一点真才实学,之是蝇营狗苟之辈,凭借钻盈的功夫,留下了毒害后世的学问。他的不肖子孙,如今依然为虎作伥。”</mark>朱教谕看到这一行字,感到自己的权威被极大的挑战了,他怒火中烧,一把撕开密封线,看到考试的名字前面一行画了一个🐷,他愤怒的攥紧了这张答卷,嘴里叽里咕噜的说起什么,突然站起来,使劲锤了一下桌子,嘴中吐出一口黑色的血,倒到了地上。

核酸上课论

# 核酸上课论 夫核酸、上课者,大学之道也,不可以不审慎也。今日之势,欲核酸并上课。孟子云:“鱼我所欲也,熊掌我所欲也,二者不可得兼。”[^1]今核酸上课犹如二虎[^2],二强相争不得俱全。 依余愚见,当舍上课而取核酸者也。夫上课者,桃李之所硕者也[^3];夫核酸者,印佩[^4]之所全也。若舍上课,虽不成蹊[^5],可保五斗[^6];若舍核酸而取上课,则桃之夭夭[^7]徒做金衣[^8]而已。核酸者,天下之神器[^9]也,死生利害,不可以不深思而慎取之也! 吾尝闻,夫上善若水[^10],水之型也,避上而趋下[^11]。今上催之甚急,而下则多受其惠,盍罢黜百科,独尊核酸。核酸一出,则万民归顺,百邦来朝,上颜大悦,岂不早当封侯之位,列三公之重。 余虽卑鄙,躬耕阳郡[^12],苟全性命于乱世。今蒙重恩,得效犬马之劳,不敢不殚精竭虑以事明主。愿明公以核酸为重,上承皇恩,下顺民意,若有不服管束贻误核酸者,当重斥赋黄;而意在鸿鹄[^13]、常念斩蛇者[^14],立斩不赦。如是,则天下幸甚,社稷幸甚,明公亦可高卧安眠,指日高升[^15]也。 诗云:战战兢兢,如临深渊,如履薄冰[^16],此余所以事明主也。愿明公听之任之,早做决断,若迁延时日,恐生不测。余伏惟圣鉴,再拜以闻。 [^1]: 出自《孟子注疏》卷十一下〈告子章句上〉 [^2]:《礼记·檀弓下》:“夫子曰:‘小子识之:苛政猛于虎也。’ [^3]:《韩诗外传》卷七:夫春树桃李,夏得阴其下,秋得食其实。学生之谓也 [^4]: 苏轼《渔家傲》:腰跨金鱼旌旆拥。将何用。只堪妆点浮生梦。泛指官位 [^5]: 《史记》卷一百九〈李将军列传〉:桃李本不能言,但以华实感物,故人不期而往,其下自成蹊径也。 [^6]: 《宋书》卷九十三〈隐逸列传·陶潜〉:郡遣督邮至,县吏白应束带见之,潜叹曰:「我不能为五斗米折腰向乡里小人。」即日解印绶去职。赋归去来。泛指官位 [^7]: 《诗·周南·桃夭》:“桃之夭夭,灼灼其华。”喻事物的繁荣兴盛 [^8]: 秦韬玉《贫女》:苦恨年年压金线,为他人作嫁衣裳。 [^9]: 《老子》:“将欲取天下而为之,吾见其不得已。天下神器,不可为也。为者败之。”此指政治 [^10]: 《老子》:“上善若水“ [^11]: 《孙子兵法·虚实篇》:兵形象水,水之形,避高而趋下 [^12]: 诸葛亮《出师表》:臣本布衣,躬耕于南阳,苟全性命于乱世。孔明阳郡人也 [^13]: 《史记》卷四十八〈陈涉世家〉:陈涉太息曰:「嗟乎,燕雀安知鸿鹄之志哉!」 [^14]: 《史记》卷八〈高祖本纪〉:」高祖醉,曰:「壮士行,何畏!」乃前,拔剑击斩蛇。蛇遂分为两,径开。 [^15]: 《梦笔生花·杭州俗语杂对》:“望风下拜,指日高升。” [^16]: 《小雅·节南山之什·小旻》:战战兢兢,如临深渊,如履薄冰。指面对政局谨慎

摆烂进入共产主义——我读《资本论》

# 内卷与共产主义 在马克思的《资本论》中有这么一句话: > 在平等的权利之间,团结就是力量,力量将会起到决定作用。在资本主义生产的历史上,工作日的正常化过程表现为规定一定工作日界限的斗争从14世纪中叶至十七世纪末关于延长工作日的强制性法律到资本家阶级与工人阶级的斗争 资本家需要更多劳动力来产生更多剩余价值,而无产阶级则需要合适的工作时间来保证自己有充足的时间进行休息、娱乐等活动。所以,无产阶级需要阶级斗争来为自己争取合适的工作时间。而内卷是一种自愿通过延长工作时间或减少工资等方式来获得资本家青睐的行为。 根据剩余价值理论,商品价值=工资+生产资料+剩余价值 内卷行为无疑会增加剩余价值,其结果便是资本家的资本增加。 所以,内卷是天生与共产主义相悖的。想要实现共产主义,必然要让工人的工作时间与工资都处于合适的位置。而内卷显然与这种理念不和。内卷的本质,是在高度资本化的当下,人们为了竞争某一利益而出现的无效竞争。在短期看来,通过内卷,在其中获胜的人会得到阶级跃升的机会;而从长期看来,内卷现象必然导致无产阶级的待遇变差,无产阶级必然遭到更多的压迫。人们为了阶级跃升而进行的内部斗争,最终会导致资本家得利。 **简而言之,内卷是一种无产阶级的内部斗争行为,是一种内耗,会使无产阶级的处境更为困难。** 而当内卷达到一种程度时,必然会导致无产阶级的反抗, 比如现在的摆烂。关于摆烂——与内卷相对,是一种无产阶级表达自己对薪资或工作不满的方式。 因此,在面对资本家及其爪牙对无产阶级进行·压迫时,**拒绝内卷选择躺平无疑是一种推动共产主义建设的合理方式。**

更伟大的人——我读《沉默的大多数》

# 更伟大的人 ## 为什么要读王小波 我没有特别喜欢的作者,让我有兴趣读完他所有的著作。我属于那种雨露均沾的类型,所有的作者,但凡我感兴趣的,我都要沾一沾。我开始读王小波是因为我每次看到他时他都被人赋予了极高的评价。但是之所以拖到现在才看是因为我很少看到他。 现在想来很少看到他或许不是他的错。**毕竟他虽然是中国人但却不是中华人民共和国人——这是当今政府最深恶痛绝的类型**。他能够在国内被看到就已经是一个奇迹了,怎么能奢望他有更多的曝光呢?因此,我一开始讲他当成一个媚俗的小说家。直到看了《黄金时代》。 虽然《黄金时代》中有很多大众喜闻乐见的内容,但是这些内容只是一种粉饰,它粉饰的是王小波的智慧与他锐利的观点。**有些人喜欢显露自己的智慧,让所有人都知道自己多么的伟大,而有些人喜欢把自己的智慧藏起来,让能看透表面的人看到,然后博得读者的莞尔一笑。前一种大概智慧有限,所以常常被人评价为装逼**。言归正传,读完《黄金时代》,我就觉得王小波是一个聪明人,他应该不只是专注于女人的胸脯与男人的下体。 于是我开始读《沉默的大多数》 ## 值得一读的书 如果让我一句话来评价《沉默的大多数》,我会说,你最好读一下。 **如果一定要将书分个好坏,我会说,读完让人变得智慧的是好的,读完让人变得愚蠢的是坏的**。《沉默的大多数》属于前一种。当然并不是所有人都觉得智慧是好的,但是一定是所有人都渴望自己变得智慧。 但是这本书也并不是适合所有人读,这本书适合有一定智慧的人读。如果你读这本书觉得被刺痛被冒犯,读的火冒三丈,觉得书中文字都是一派胡言,说明你的智慧还不足以读这本书。但是如果你能坚持读完然后时不时反刍一下,你的智慧肯定会得到质的提升。我前面就已经说过,要读懂王小波,需要有一定的聪明。说的简单一点,就是要有独立思考的能力。如果周围在欢呼万岁,你也欢呼万岁,周围在高呼打倒,你也高呼打倒,别人说你要努力,你就会努力,那我觉得你暂时读不懂王小波。你读的时候会生气,那就不如不读。 但是读完这本书会收获智慧,这一点是毋庸置疑的。 ## 谁更伟大? 前面评价了一下作者与书,这部分开始正式的说感想了。 《沉默的大多数》,王小波说,这个世界上的人,大多是没有话语权的。于是,他要为沉默的人发声。这句话的意思,其实很好理解。_你可以自己想他的意思,我觉得我没必要明说。_ 我要说的是,真理往往掌握在沉默的人嘴里。但是大家就是不愿意说。这种不愿意是多发性的,你不能说不愿意说就是因为坏。不愿意说恰恰不是因为他们坏,不愿意说出真理。而是他们太好了,所以说不出真理。王小波的这本书,依我来看,要从两个方面打破沉默,第一是他帮沉默的人说出一部分真相,但这不是主要目的;第二是让沉默的人不再沉默,这才是他的主要目的。我不方便说的太具体,因为我也刚刚走出沉默,你们若是有兴趣不妨去读一读这本书。 这里我又说到了人都好与坏,可能有人会质问我,“你怎么证明沉默的好,不沉默的坏呢?”。当然,我这句话说的不绝对,应该说沉默的大都好,不沉默的大都坏。而且这句话也有时空上的局限性,他只对某段时间的某个地方有用。这句话拿到美国,就不如在中国的时候好用了,这是肯定的。 那让我来证明一下这句话,一般我们要证明一件事情之前,我们通常要下一个定义。我可以粗略的规定一下,让其他人幸福的人是好的,让其他人不幸的人是坏的。接下来,我们可以举几个例子。曾经有一段时间:农民,让大家吃饱,是好人,学者科学家,为人类带来智慧,是好人,他们失声了。而打人的恶霸,以及指示别人打人的土皇帝却声音很大,一边高呼着万岁,一边下达最高指示,好不乐乎。只是苦了挨饿的农民与挨打的学者。又有一段时间,建房子造东西种粮食做美食搞发明的好人又失声了,而打人的人与他们的土皇帝又提高了嗓门。**他们一边喊着万岁,一边说着“安全”、“清零”、“坚持”,好不乐乎,只是苦了不愿吃人的人**。 就我来看,王小波与鲁迅是一种,他们都是希望人们追求智慧的,追寻真理的。**人独立的思考,即使沉默不发声,也不会变为坏人的唇舌**。这样看来,王小波是让人变成好人的人。 如此这般,我们不妨将这种让人变成好人的人成为伟大的人。**毋容置疑,鲁迅比袁世凯伟大,同理,王小波比毛泽东伟大**。但是何必要抬出来【伟大】这个词呢?仅仅规定好人比坏人伟大。**对比那个十里山路不换肩的人,我们这些所谓的“屁民”已经足够伟大了**。

AI思考小记

## 我们在哪里? 如果要我将AI发展至今的阶段分一个类的话,我会这样分: 1. 前AI阶段: 2020.6 - 2023.3    从openAI发布的ChatGPT3进入大众视野开始,到ChatGPT4刷新了人们的认知,这一段时间内,AI的理论基础已经打好了,但是人们对AI能做到什么程度还是抱有疑问的。直到GPT4的发布,让人们猛地意识到,AI居然已经悄悄发展到这种程度了。 2. AI阶段:2023.3 - 2025.1    在这一阶段,GPT 4o,GPT o1不断刷新大模型的智能程度,copilot、cursor、 midjourney、sora等应用的出现,也表示AI在某些专业领域取得了长足的进步。我将这个阶段的结束定为deepseek R1问世的那一天,这不仅仅是因为R1是一个国产的大模型,更是因为R1的出现,标识着一个足够智能的模型不再是一个稀缺的资源了。 3. 后AI阶段:2025.1 - ?    从25年开始,我们进入了后AI阶段。这个阶段的特征可能是:大语言模型能力的进步将不再是最受人瞩目、最主流的科技发展了;取而代之的将是越来也智能、越来越“人体工学”的AI应用。manus的火爆充分的说明了这一点。但是很多人不知道的是,在manus之前,openAI就已经推出了一款个人助理:Operator。可以预见,越来越多的AI应用将会被创造出来,供人使用。     我们现在所处的这个时代,是AI时代的晚期,而一个时代的晚期,往往是这个时代最辉煌的时候。AI发展了这么久,可以说,它不够惠民,没有大幅的提高生产力。我们这个阶段,将是AI转化为生产力的阶段。后AI时代,将会在AGI被研发出来的时候结束。到那时,人类的历史将会进入一个新的时代。 ## AI的未来 AI的未来是AGI,也就是通用人工智能。但是AGI出现后,人类的未来是怎样的,我想就算是最有智慧的人也难以给出一个确定的答案。 但是在AGI出现之前,AI的发展在一定程度上还是可以被预测的。比如我可以断言,在未来,AI会继续在各种垂直领域大放异彩,成为越来越好用的生产力工具;同时,在通用的领域,可能有许多的智能助手会出现。 各个手机厂商、互联网公司都在摩拳擦掌,准备用自己的智能助手带给用户新的体验,谷歌的Chrome提供了ai接口,微软在自家产品的方方面面植入copilot……之前被许多人奉为笑话的一句话将会成为事实:ai助手将会成为人们选择一款手机时,最重要的理由。而如果这个命题成为现实,我们可能不得不面对一个超级手机公司——一个占领80%以上市场份额的庞然巨兽。 可以预见,在不久的将来,许多AI软件将会如雨后春笋般出现,但是一将功成万骨枯,最终只会有几款产品成为“吃鸡”的人,马太效应将会在这次革命上表现的淋漓尽致。 以及,我个人最期盼的,是AI真正带来生产力的飞跃,就像蒸汽机、内燃机与计算机在第一次、第二次与第三次工业革命发挥的作用一般。我相信这一天并不遥远。而且工业革命的周期一次比一次要短暂,甚至有可能AI带来的工业革命还未结束,由AGI带来的工业革命又将瞬间引爆全球。 ## 人类的未来 这是一个美好的时代吗?是的。 AI是目前的一个风口吗?是的。 但是这个时代,这个风口,与大多数人都没有关系。如果我们说20世纪是人才的世纪,那么21世纪就是天才的世纪。虽然开发LLM可能并不如独立研究出微积分更加困难,可能奥特曼并不比牛顿更加聪明。但是我能够知道,一万张英伟达A100显卡一定比一叠草稿纸要贵的多。 如果未来有人格的话,他只会偏爱两种人,一种是拥有资源的人,另一种是超人。拥有资源的人,尚可通过通过投资超人去化身超人毛发上、肠胃中的寄生虫,在超人一飞冲天之时,跟着鸡犬升天。而普通人可能就只能呼吸着超人留下的污浊的尾气了。 AI的时代,将是一个深沟高垒的时代。技术的壁垒、资本的壁垒将会把人与人划分,如果说还有底层人想要改变自己的命运的话,可能这就是最后的窗口期了。也正是因为在这个时代,突破自己阶级的限制变得愈发困难,大家才会一致的摆烂,放弃疯狂而无意义的内卷。 二八定律将会失效,未来可能是一九九定律。这百分之一的人,未必掌握了百分之九十九的财富,但是可能会掌握百分之九十九的未来。如果说这就是我们印象中的那个“乌托邦”的话,那么他就快要来了。 ## 未来看我 如果未来有人格的话,他肯定会偏爱上面说的那些,有资本或者有技术的人。 而像我一样的大多数人,既不是天才,更不可能成为寄生虫,因此只能被未来所抛弃。 但是被未来所抛弃就一定意味着悲惨的结局吗?可能未必。勇立潮头的弄潮儿,是时代的风云人物,但是在风波诡谲之处行动,最为费劲,也最为危险。而我们这些站在岸上的人,虽然不被潮水青睐,但潮落之后,兴许能捡到几枚贝壳。 AI带来的未来不一定能照亮所有人,但是他的光源一定会比过去更加明亮。或许我们每个人,都能受到它的普照,而不是在他的影中。这就取决于那些在大公司、AI创业公司和政府部门工作的弄潮儿们,如何驱赶海浪了。 ## 我看未来 我认为我并不是只在被未来选择着,我也可以选择未来。 《大明王朝1566》中有这么一幕:吕公公教育误杀周云逸的冯保,要思危、思退、思变。 如今,我在感受到AI带给社会的变化之后,开始了思危:目前AI带来了生产力的发展,虽然没有让AI取代某些人,但是却让一部分会用AI的人驱逐了一部分不会用AI的人。如果我不去卷AI,那么就可能会被那些会用AI的人卷死。但是就算在互卷的过程中暂时的残存下来,如果生产模式没有变化,AI + 人工的组织方式也只会让公司的员工越来越少。这是思危。 然后我便开始思退:我不愿意让别人把我卷死,但是我又不愿意违心的内卷,以及卷死别人。用一种抬高自己的说法,我是情愿给别人留一条活路的。比起在时代的浪潮中挣扎,我更愿意急流勇退,到自己的舒适区重新思考。 等到了我的舒适区之后,我再开始思变。不过如何变,我也大概有一些想法。AI给了我们很多机会,很多一人成军的机会。如果我们愿意,我们完全可以只对自己负责,我 + AI,或许是一个不错的抉择。 ## 后话 虽然已经决定要退了,但是后退并不是一溃千里,而是朝另一个方向前进。在后疫情时代,后AI世代,后移动互联网时代,一个人在家能有什么活法呢?我正在积极探索之中。 在做好准备之前,我不会轻易的遁走。其中一个准备,就是准备100个的资产,预期四年完成。目前我的进度是: *8/100* 当我已经完成了撤退之后,或者是在四年之后,我可能会转过身来回味这篇文章,看我言中了多少,又做了哪些事情。 *感谢chatGPT与豆包为我整理资料。*

Hadoop 知识整理

# hadoop知识点整理 ## 第一章 大数据的概念 大数据是指无法用现有的软件工具提取、存储、搜索、共享、分析和处理的海量的复杂的数据图集。 ### 特征 **4个V**: - Volume:数据体量巨大 - Variety:数据种类繁多 - Value:数据价值密度低 - Velocity:处理速度快 ### hadoop生态圈 大数据工具主要包括:Hadoop、Hbase、ZooKeeper、Hive、Mahout、Sqoop、Storm等 #### Hadoop **Doug Cutting**开发,受到**Map/Reduce**启发,核心是**MapReduce编程模型和HDFS分布式文件系统**。 采用分而治之的思想,Map用来切分大的数据,Reduce用来合并Map计算的结果。 HDFS 分布式文件系统,为海量数据提供存储服务,将大文件拆分为块,多节点存放,具有高吞吐量、高容错性的特点。 #### HBASE HBASE是Apache开源的KV型数据库,是建立在HDFS之上,提供高可靠性、高性能、列存储、可伸缩、实时读写的数据库系统。 仅支持单行事务。 主要用来存储非结构化和半结构化的松散数据。 #### Hive Apache Hive数据仓库软件提供对存储在分布式中的大型数据集的查询和管理,它本事是建立在Hadoop之上的 #### Storm Apache Storm是一个免费、开源的分布式实时计算机系统,简化了数据流的可靠处理。 #### ZooKeeper zooKeeper是一个高性能、分布式的开源分布式应用协调服务,他是storm、hbase的重要组件,它是一个为分布式应用提供一致性服务的软件。 服务端跑在JAVA上 ZooKeeper有两个角色,一个是leader,负责写服务和数据同步,剩下的是follower,提供读服务。 **特点**: - 顺序一致性:按照客户端发送请求的顺序更新数据 - 原子性 - 单一性:无论客户端连接哪个server都看到同一个视图 - 可靠性:一旦数据更新成功将一直保持,直到新的更新 - 及时性:客户会在一个确定的时间内得到最新的数据 **运用场景**: - 数据发布订阅 - 名空间服务 - 分布式通知 - 分布式锁 - 集群管理 #### sqoop sqoop是Apache顶级项目,允许用户将数据从关系型数据库中抽取数据到Hadoop中 #### mahout mahout是一个强大的数据挖掘工具,是一个分布式机器学习算法的集合,包括分布式协同过滤的实现、分类、聚类等 ### Hadoop历史和版本 历史: - 2011年12月,Apache基金会发布了Apache Hadoop 版本1.0 - 2013年8月,版本2.0.6可用 - 2017年12月发布Apache Hadoop3 发行版: Hadoop有许多变体: - Cloudera Hadoop分布:是Coludera Enterprise的核心,包括Apache Hadoop、Apache Spark,Apache Kafka 以及十多个其他紧密继承的领先开源项目 - Hortonworks Hadoop分布:是基于YARN的安全性强、企业就绪的开源版本 - MapR Hadoop分布:是Hadoop的完成整企业级发行版 - PivotalHD:是领先的基于标准的Hadoop该发行版,为Business Data Lake架构奠定了基础 优势: - 高可靠性 - 高拓展性 - 高效性 - 高容错性 ## 第二章 Hadoop 组成与结构 Hadoop1的三大核心模块: - Common模块:支持其他模块的工具模块 - HDFS模块:一个高可靠、高吞吐量的分布式文件系统 - MapReduce模块:一个分布式的资源调度和离线并行计算系统 Hadoop2的组成: MapReduce模块仅作为分布式计算框架存在,资源调度功能交给YARN来调度处理 ### HDFS 一个分布式文件系统。 HDF的设计适合一次写入多次读出的场景且不支持文件修改。适合用来做数据分析,并不适合做网盘使用。 Master-Slave结构,Master是NameNode,Slave是DataNode client职责如下: - 文件切分 - 与NameNode交互获取文件的位置信息 - 与DataNode交互读取或写入数据 - 提供一些明恋来管理HDFS,比如启动或者关闭HDFS - 可以通过一些命令来访问HDFS NameNode职责如下: - 配置副本策略 - 处理client读写请求 - 管理block(数据块)映射信息,以元数据的形式存储在Fsimage镜像文件中 - 管理HDFS命名空间 DataNode的职责: - 执行实际的数据块 - 执行数据块的读写操作 SecondaryNameNode,第二名称节点,并非名称节点的热备,晋档NameNode重启或者热备NameNode激活时将宕机前所保留集群的快照发送给NameNode以恢复此前集群的状态。具体功能为: - 存辅NameNode,分担其工作量 - 定期合并Fsimage和Edits,并推送给NameNode - 在紧急情况下可辅助恢复NameNode 优点: - 高容错性 - 适合大数据处理 - 支持流式数据访问 - 可构建在廉价机器上 缺点: - 不适合低延时数据访问 - 无法高效的对大量小文件进行存储 - 不支持并发写入文件和随机修改 ### YARN架构 MRv1的局限: - 扩展性差 - 可靠性差 - 资源利用率低 - 无法支持多种计算机框架 YARN是一个弹性计算平台,他的目标已经不局限于支持MapReduce一种计算框架,而是朝着对多种框架的统一管理前进 优点: - 资源利用率高 - 运维成本低 - 数据共享 对比: | | V1 | V2 | | ----------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | | 基本框架 | JobTracker由资源管理和作业控制两部分组成 | 将JobTracker的两个功能拆分成两个独立的进程,资源管理进程负责整个集群的资源,而作业控制则是直接与应用程序相关的模块,每个进程只负责一个作业 | | 编程模型与数据处理引擎 | | MRv2重用了v1中的编程模型与数据处理引擎 | | 运行时环境 | 由JobTracker和TaskTracker两类服务组成,JT负责资源和任务的管理与调度,TT负责单个节点的资源管理和任务进行 | 将资源部管理与应用程序管理分开,分别又YARN和ApplicationMaster负责 | #### YARN基本架构 总体上仍然是Master/Slave架构 YARN的组成成分如下: - ResourceManager:一个全局的资源管理器,负责整个系统的资源管理与分配。它由两个组件构成: - 调度器(Scheduler):根据容量、队列等限制条件将资源分配给各个正在运行的应用程序 - 应用程序管理器(Application Manager ASM):负责整个系统中所有应用程序 - ApplicationMaster(AM)的主要功能有: - 与RM调度器协商以获取资源(Container) - 将得到的任务进一步分给内部任务 - 与NM通信以启动/停止任务 - 监控所有任务运行状态 - NodeManager:是每个节点上资源和任务管理器 - Container:是YARN山中的资源抽象,它封装了某个节点上的多维度资源 ## 第三章 Hadoop运行模式与大数据技术框架 Hadoop的运行模式主要有四种: - 本地模式 - 伪分布式 - 全分布式 - 高可用模式 ### 伪分布式模式 Hadoop可以运行在单个节点上,其中每一个Hadoop守护进程运行在单独的Java进程中,这个模式称之为伪分布式模式。Hadoop所有进程都运行在一台服务器以模拟全分布式模式,常用于学习阶段。 后台的五个进程为: - NameNode - DataNode - SecondaryNameNode - ResourceManager - NodeManager ### 高可用模式 Hadoop是一种主从式架构,这样就会有**单点故障**的问题 ## HDFS - 数据块(block) - HDFS默认的最基本的存储单位是128MB的数据块 - 128M为一块 - 一个文件如果小于一个数据块的大小,并不占用整个数据块的空间 - 存放策略(3副本) - 第一个和client同node - 第二个放在与第一个节点的不同机架中的随机的一个node - 第三个放在与第一个节点不同的机架中与第二个不同的随机node中 - NameNode 和DataNode - HDFS体系结构中有两类节点,一类是NameNode ( Master) ,又叫"元数据节点";另一类是DataNode (Slave) ,又叫"数据 节点”。 - 元数据节点用来管理文件系统的命名空间,作用如下: - 其将所有的文件和文件夹的元数据保存在一个文件 系统树中 - 这些信息也会在硬盘上保存成以下文件:命名空间镜像(namespace image)及修改日志(edit log) - 还保存了一个文件包括哪些数据块,分布在哪些数据节点上,然而这些信息并不存储在硬盘上,而是在系统启动的时候从数据节点收集而成的 - 数据节点是文件系统中真正存储数据的地方,作用如下: - 客户端(clien)或者 元数据信息(namenode)可以向数据节点请求写入或者读出数据块 - 周期性的向元数据节点回报其存储的数据块信息 - `hadoop.tmp.dir`,临时目录,其他临时目录的父目录,默认 `/tmp/hadoop-${user.name}`,在`core-site.xml`中配置 - 元数据节点目录结构,在`hdfs-site.xml`中配置`dfs.name.dir`参数,以`,`分隔,默认在`{hadoop.tmp.dir}/dir/name` - 数据节点目录结构 - 在`hdfs-site.xml`中配置参数`dfs.data.dir`,以`,`分隔 - HDFS通信协议 - 所有HDFS通信协议都是构建在TCP/IP协议上的 - HDFS安全模式 - Namenode启动后会进入一种称为安全模式的特殊状态。处于安全模式的Namenode是不会进行数据块的复制的。Namenode从所有的DataNode接受心跳信号和块状态报告 **Name Node、DataNode 和Client** - Namencodte 是分布式文件素统中的管理者, 主要负责管理 文件系统的命名空间、集群配置信息和存储块的复制等。NameNode 会将文件系统的Meta-data 存储在内存中,这些信息主要包括了文件信息,每个文件对应的文件块的信息和每个 文件块DataNode的信息等。 - DataNode是文件存储的基本单元, 它将Block 存储在本地文件系统中,保存了Block 的meta-data,同时 周期性地将所有存在的Block信息发送给NameNode. - Client 就是需要获取分布式文件系统文件的应用程序。 - Client读取文件信息 ### Hadoop Shell命令 实际上是属性,命令为:`hadoop fs -xx` - `cat`: - `chgrp`: change group - `chmod`: - `chown` - `copyFromLocal` - `copyToLocal` - `cp`: copy - `du`:显示目录中所有文件的大小 - `dus`:显示单个文件大小 - `expunge`:清空回收站 - `get`:复制到本地 - `getmerge`:将 source dir 中的文件链接成 local target dir - ls - lsr:递归ls - mkdir - movefromLocal - mv - put:本地到远程 - rm - rmr:递归rm - setrep:改变副本数 - stat:返回指定路径的统计信息 - tail:将尾部1kb的字节输出到stdout - test:检测文件是否存在 - text:将源文件输出为文本格式 - touchz:新建一个0自己的文件 Hadoop管理命令: - distcp:分布式拷贝(集群之间) - fsck:检查整个文件系统的健康情况 - jar:运行java文件 - job:用于和MapReduce交互 - balancer:运行集群平衡工具 - dfsadmin:运行一个dfs admin客户端 - namenode: 运行namenode ### java接口 Hadoop中关于文件操作类基本都在`org.apache.hadoop.fs`包中 Hadoop类库中最终面向用户提供接口是`FileSystem` ```java // 获取FileSystem具体类 static FileSystem get(Configuration conf); // 写文件 public FSDataOutputStream create(Path f) throws IOException; //读文件 // 上传文件到HDFS public void copyFileLocalFile(Path src,Path dist); // 重命名文件 public abstract boolean rename(Path src, Path dist); // 删除文件&目录 public abstract boolean delete(Path f, boolean recursive) throws IOException; // 创建目录 public boolean mkdirs(Path f) throws IOException; // 遍历目录 public abstract FileStatus[] listStatus(Path f) throws FileNotFoundException, IOException ``` ## MapReduce ```java public void map(Object Key,Text value,Context context) throws IOExcetion, InterruptedException{ } ``` ### MapReduce工作原理 MapReduce框架的运作完全基于“键值对”,即数据的输入是一批“键值对” (key-value) ,生成的结果也是批“键值对”,只是有时候它们的类型不一样而已。Key和value的类由于需要支持被序列化 (Serealire) 操作,所以它们必须要实现`Writable` 接口,而且key的类还必须实现`WirtableComparable`接口,使得可以让框架对数据集的执行排序操作,MapRedtre运行机制,按照时间顺序包括:输入分片(input split)、map 阶段、combiner 阶段、shuffle阶段和reduce阶段。 在进行map计算之前,MapReduce会根据输入文件计算输入分片 ### YARN运行流程 1. JobClient 向YARY中提交应用程序,其中 包括ApplicationMaster 程序、启动ApplicationMaster的命令、用户程序、环境变量、作业信息、文件位置信息等 2. RecourseManager为该应用程序分配第一个 Container. 并与对应的 Node-Manager 通信(通过心跳方式),更求它在这个Container中启动应用程序的ApplicationMaster 3. ApplicationMaster首先向ReoourceManager注册,这样用 户可以直接通过ResourceManager查看应用程序的运行状态。然后它将为各个任务申请资源,并监控它的运行状,直到运行结束 4. ApplicationMaster 采用轮询的方式通过RPC协议向ResourceManager申请和领取资源 5. 一旦ApplicationMaster申请到资源后,便与对应的NodeManager通信,要求它启动任务 6. NodeManager为任务设置好运行环境(包括环境变量、JAR包、二进制程序等)后,将任务启动命令写到一个脚本中, 并通过运行该脚本启动任务。 7. 各个任务通过某个RPC协议向AplcationMaster汇报自己的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。 8. 应用程序运行完成后, ApplicationMaster向ResourceManager注销并关闭自己 ### 作业关键过程详解 **map**:map任务最终是交给Map任务执行器 **Reduce**:从所有map节点取到属于自己的map输出 **Partitioner**:当Mapper处理好数据后,需要使用Partitioner(分区器)确定怎样合理地将Mapper输出分配到Reduce上 **Combiner**:相当于一个本地的Reduce,主要是对Mapper输出的大量本地文件进行一次合并。Combiner函数执行时机可能是在map的merge操作完成之前 ### MapReduce各种输入输出 **InputFormat**:负责处理MR的输入部分,来决定Map的数量,InputFormat **FileInputFormat**:是所有以文件作为数据源的InputFormat实现的基类,FileInputFormat保存作为job输入的所有文件 ## MapReduce 设计模式 - **过滤器模式**:设定某种条件,当负责条件时保留数据,不符合条件时丢弃数据 - **Top N**:根据数据集的排名,获取前N条记录 - **去重模式**:去重 - **数据重组**:按照一定的规划整理数据。数据重组要求划分的分区数量已经确定,划分分区的条件已经确定 -

SpringCloud 知识整理

# SpringCloud 知识点梳理 ## 第二章 构建spring cloud ### SpringBoot目录结构 - `src/main/java`:主程序入口 - `src/main/resources`:配置目录 - `src/test`:单元测试目录 ### Springboot依赖 Starter POMs 是一系列轻便的依赖包,是一套一站式的Spring相关技术的解决方案。 SpringBoot的StarterPOMs采用 `spring-boot-starter-*`命名,`*`表示一个特别的功能模块。 我们用到的POMs有: - `spring-boot-starter-web`:全栈Web开发模块 - `spring-boot-starter-test`:通用测试模块 ### 配置详解 springboot 的默认配置文件位置为 `src/main/resources/application.properties` #### 自定义参数 在application.yaml中 ```yaml book: name: springboot author: charles ``` 在java中 ```java @Component @Data public class Book{ @Value("${book.name}) private String name; } ``` `@value` 属性在加载配置时支持两种表达式: - PlaceHolder方式,格式如上,为`${*}` - SpEL表达式,格式为`#{*}` #### 多环境配置 多环境配置的文件名需要满足 `application-{profiles}.yaml`的格式,其中profile对应你的环境表示: - dev:开发 - test:测试 - prod:生产 加载那个环境需要在`application.yaml`中设置`spring.profile.active=*`属性 #### 加载顺序 springboot属性加载顺序如下: 1. 在命令行中传入的参数。 2. SPRING APPLICATION JSON中的属性。SPRING APPIATION JSON是以JSON格式配置在系统环境变量中的内容 3. Java.comp/env中的JNDI属性 4. Java的系统属性,可以通过`System.getProperties ()`获得的内容 5. 操作系统的环境变量 6. 通过`random.*`配置的随机属性 7. 位于当前应用 jar包之外,针对不同{profile}环境的配置文件内容,例如`application- {profile} .properties`或是YAML定义的配置文件。 8. 位于当前应用jar包之内,针对不同{profile}环境的配置文件内容,例如`aplication- {profilel} .properties` 或是YAML定义的配置文件。 9. 位于当前应用jar包之外的`application.properties`和YAML配置内容 10. 位于当前应用jar包之内的`application. properties`和YAML配置内容。 11. 在`@Configuration `注解修改的类中,通过`@PropertySource` 注解定义的属性。 12. 应用默认属性,使用`SpringApplication setDefautProperties`定义的内容 优先级按上面的顺序由高到低 ### actuator 要想使用actuator需要添加依赖:`spring-boot-starter-actuator`,然后在application添加如下配置以暴露全部端点: ```yaml management: endpotints: web: exposure: include: '*' ``` actuator的原生断点分为三类: - 应用配置类 - `/beans`:获取上下文所有的Bean,每个Bean都包含以下信息 - scope:作用域 - type:java类型 - resource:class文件的具体路径 - dependencies:依赖的bean的名称 - `/configprops`:配置属性 - profix:属性前缀 - properties:各个属性的名称和值 - `/env`:用来获取应用所有可用的环境属性报告 - `/mapping`:用来返回所有springMVC控制器的映射关系 - 度量指标类 - `/metrics`:返回当前应用的各种重要指标 - `/health`:用来获取应用的各类健康指标信息,当项目较为简单时,只标识出应用的状态 - UNKNOWN:未知状态,503 - UP:正常,200 - DOWN:失败,503 - OUT_OF_SERVICE:不提供对外服务,200 - 操作控制类 - `/shutdown`:只提供了这一个,不支持get请求 ## 第三章 服务治理 Alibaba Nacos ### 常见方案 `Spring Cloud Eureka`:既包含了服务端组件,又包含了客户端组件,并且服务端与客户端均通过java编写 `zookeeper`:一个开源的分布式应用程序协调服务,是Chubby的一个开源实现,是Hadoop和Hbase的重要组件 `Consul`:所以一个服务网络的解决方案,他是一个分布式的高可用的系统,而且开发使用都很简便。它提供了一个功能齐全的控制平面,它的主要特点是:服务发现、健康检查、键值存储、安全服务通信、多数据中心 `Nacos`:帮助发现、配置和管理微服务 ### Nacos 修改端口:默认端口8848,可以进入nacos/conf目录修改`application.propertoes`的`server.port`属性 启动:运行`startup.cmd -m standalone`(单例模式运行) 访问:默认账号密码都是nacos #### 注册服务提供者 ```yml spring: application: name: #要注册到注册中心的服务名称 cloud: nacos: server-addr: localhost:8848 #服务端地址 ``` #### 服务发现与消费 ```java @Bean @LoadBalanced public RestTemplete restTemplete(){ // ... } ``` ## 第四章 负载均衡 Ribbon SpringCloud Ribbon是一个基于HTTP和TCP的**客户端**负载均衡工具,他基于Netflix Ribbon实现。 虽然它只是一个工具类框架,不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在在每一个SpringCloud构建的微服务和基础设施中。 客户端负载均衡与服务端负载均衡最大的不同点在于服务清单所存储的位置。 - 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心 - 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用 ### 分析 负载均衡器应该具备这样几种能力: - `ServiceInstance choose(String serviceId)`:根据传入的服务名从负载均衡器中挑选一个对应服务的实例 - `T excute(String serviceId, LoadBalancerRequest request)`:使用从负载均衡器中挑选出的服务实例来执行请求内容 - `URI reconstuctURI(ServiceInstance instance, URI origianl)`:为系统构建合适的hostzport形式的URI ### 负载均衡器 - AbstractLoadBalancer 是 ILoadBanlancer 接口的抽象实现,在该抽象类中定义了一个关于服务实例的分组枚举类ServerGroup - BaseLoadBalancer 是 Ribbon负载均衡器的基础实现类 - DynamicServerListLoadBalancer类是继承于BaseLoadBalancer类,他是对基础负载均衡器的扩展 - ZoneAwareLoadBalancer 负载均衡器是对 DynamicServerListLoadBalancer的扩展 ### 负载均衡策略 - AbstractLoadBalancerRule,负载均衡器策略抽象类,在该抽象类中定义了负载均衡器ILoadBalancer对象 - ClientConfigEnabledRoundRobinRule,该策略较为特殊,一般不直接使用它,因为它本身并没有实现什么特殊的处理逻辑,他在内部定义了一个RoundRobinRule策略 - PredicateBaseRule,是一个抽象策略,继承了ClientConfigEnabledRoundRobinRule,从其命名中可以看出来这个一个基于Predicate实现的策略 | 内置负载均衡规则类 | 规则描述 | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | RoundRobinRule | 简单轮询服务列表来选择服务器。它是Rbbon默认的负载均衡规则。 | | AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1) 在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。(2) 并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabiliyFilteringRule规则的客户端也会将其忽略。 | | WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。 服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 | | ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zome可以理解为个机房、一个机架等。而后再对Zome内的多个服务做轮询 | | BestvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 | | RandomRule | 随机选择一个可用的服务器 | | RetryRule | 重试机制的选择逻辑 | | NacosRule | 支持优先调用同个集群实例的 rbbon负载均衡规则 | ### 配置详解 #### 自动化配置 Ribbon能够自动配置下面这些接口的实现 - IClientConfig: Ribon的客户端配置, 默认采用com.netflix.client.config.DefaultlCientCnfigimpl实现 - IRule: Ribbon的负载均衡策略,默认采用com.netflix.loadbalancer,ZoneAvoidanceRule实现,该策略能够在多区域环境下选出最佳区域的实例进行访问。 - IPing: Ribbon的实例检查策略,默认采用com.netfix.loadbalancer.DummyPing实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回true,默认认为所有服上面务实例都是可用的。 - ServerList: 服务实例清单的维护机制,默认采用com.netfix.loadbalancer.ConfigurationBasedServerList实现。 - ServerListFilter: 服务实例清单过滤机制,默认采用org.springrameworkcloud.netflix.ribbon.ZomeAwareKiadBakabcer实现,该策略能够优先过滤出与请求调用方处于同区域的服务实例。 - lLoadBalancer: 负载均衡器,默认采用com.netflix.loadbalancer.ZoneAwareloadBalancer实现,它具备了区域感知的能力。 针对一些个性化需求可以在springboot中创建对应的实现实例即可覆盖默认配置,比如 ```java @Bean public Irule rule(){ return new NacosRule(); } ``` #### 参数配置 对于RIbbon的配置有两种: - 全局配置:使用`ribbon.<key> = <value>`格式进行配置 - 指定客户端配置:`<client>.ribbon.<key>=<value>`的格式进行配置 **IRule配置**: ```yaml userservice: ribbon: NFLoadBalancerRuleClassName: <value> # 负载均衡规则类全名 ``` **饥饿加载**: ```yml ribbon: eager-load: enabled:true client: - userservice ``` **与Nacos结合** ```yaml spring: application: name: userservice #服务名称 cloud: nacos: server-addr: <value> #服务端地址 discovery: cluster-name: <value> # 区域集群名称 ``` **重试机制** ```yaml userservice: ribbon: ConnectTimeout: 200 #链接超时时间 ReadTimeout: 200 #请求超时时间 OkToRetryOnAllOperations: true #是否对所有操作都允许重试 MaxAutoRetriesNextServer: 2 # 切换实例的重试次数 MaxAutoRetries: 1 # 当前实例的重试次数 ``` ## 第五章 服务容错保护 Hystrix 快速入门: ```java @EnableCircuitBreaker //启动断路器 ``` ### 分析 #### 命令执行 - `execute()` 同步执行,从依赖的服务返回个单一的结果对象,或是在发生错误的时候抛出异常 - `queue()` 异步执行, 直接返回一个Future对象,其中包含了服务执行结束时要返回单一结果对象。 - `observe()` 返回来自依赖项的响应的Observable对象,它代表了操作的多个结果,它是一个hot Observable - `toObservable()` 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个cold Observable。 #### 断路器是否打开 - 如果断路器是打开的,那么Hystrix不会执行命令,而是转到fallback处理逻辑 - 如果断路器关闭,则会检测是否有可用资源执行命令 #### 请求方法 - `HystrixCommand.run()` 返回一个单一的结果或抛出异常 - `HystrixObservableCommand,constuct()` 返回一个Observable 对象来发射多个结果或通过 onError 发送错误通知 #### 计算断路器健康度 Hystrix会将 成功 、 失败 、 拒绝 、 超时等信息报告给断路器 #### fallback处理 - `execute()` : 抛出异常。 - `queue()`: 正常返回Future对象,但是当调用get 0来获取结果的时候会抛出异常。 - `observe()`: 正常返回Observable 对象,当订阅它的时候,将立即通过调用订阅者的onError方法来通知中止请求。 - `tobservable()`: 正常返回Obsevable对象,当订阅它的时候, 将通过调用订阅者的onError方法来通知中止请求。 #### 成功的响应 - `exeoute()` 以与queue 相同的方式获取一个Future,然后在这个Future 上调用get()获取由Observable发出的单个值 - `queue()` 将Obervable转换为一个BlockingObservable,以便它可以被转换为一个Future然后返回这个Future - `observe ()` 在toObservable()产生原始Observable 之后立即订阅它,让命令能够马上开始异步执行,井返回一个 Observable对象,当调用它的subscribe时,将重新产生结果和通知给订阅者。 - `toObservable()` 返回最原始的Observable,必须通过订阅它才会真正触发命令的执行流程。 ### 断路器 三个抽象方法: - `allowRequest()`:每个Hystrix的命令的请求都通过他去判断是否被执行 - `isOpen()`:返回当前断路器是否打开 - `markSuccess()`:闭合断路器 #### 创建请求命令 ```java @GetMapping("/{param}") @HystrixCommand(fallbackMethod = "showAppInfoFallback") public String showAppInfo(@PathVariable("param") String params){ String url = "..."; return restTemplate.getForObject(url,String.class); } ``` #### 定义服务降级 ```java public String showAppInfoFallback(String param){ return "busy"; } ``` ```java /** * 服务降级方法 */ @HystrixCommand(fallbackMethod = "showAppInfoCallback") public String ShowAppInfoFallback(String param){ ... } ``` #### 异常获取 注解配置方式的实现非常简单,只需要fallback实现方法的参数中增加Throwable对象的定义,这样在方法内部就可以获取除法服务降级的具体异常内容了 ```java public String showAppInfoFallBack(String param,Throwable exception){ // ...something } ``` #### @Hystrix注解 ```java @HystrixCommand(fallbackMethod = "..." ignoreException = {Exception.class} commandKey = "..." groupKey = "..." threadPoolKey = "..."                 ) ``` #### 请求缓存 | 注解 | 描述 | 属性 | | ------------ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------- | | @CacheResult | 该注解用来标记请求命令返回的结果cacheKeyMethod应该被缓存,它必须与@HystrixCommand注解结合使用 | cacheKeyMethod | | @CacheRemove | 该注解用来让请求命令的缓存失效,失效的缓存根据定义的Key决定 | commandKey,cacheKeyMethod | | @CacheKey | 该注解用来在请求命令的参数上标记,使其作为缓存的Key值,如果没有标注则会使用所有参数。如果同时还使用了@CacheResult和@CacheRemove注解的cacheKeyMethod方法指定缓存Key的生成,那么该注解将不会起作用 | value | #### 设置请求缓存 ```java @CacheResult ``` 定义一个filter ```java HystrixRequestContext context = HystrixRequestContext.initializeContext(); ``` #### 定义缓存key ```java @HystrixCommand(fallbackMethod = "showAppInfoCallback") @CacheResult(CacheKeyMethod = "cacheKey") public User getUserInfo(@PathVariable("id") @CacheKey String id){ // something } public String cacheKey(String id){ return id; } ``` #### 缓存清理 `@CacheRemove`注解的`commandKey`属性是必须要指定的,它用来指明需要使用请求缓存的请求命令,因为只有通过该属性的配置,Hystrix才能知道正确的请求命令缓存位置、 #### 注解请求合并 `@HystrixCollapser` #### 请求合并的额外开销 - 请求命令本身的延迟 - 延迟时间窗内的并发量 ### 属性详解 4个不同优先级别的配置(优先级由低到高): - 全局数认值:如果没有设置下面三个级别的属性,那么这个属性就是默认值。由于该属性通过代码定义,所以对于这个级别,我们主要关注它在代码中定义的默认值即可。 - 全同配置属性:通过在配置文件中定义全局属性值,在应用启动时或在与SpringCloud Comfig和Spring Cloud Bus实现的动态刷新配置功能配合下,可以实现对“全局默认值”的覆盖以及在运行期对“全局默认值”的动态调整 - 实例默认值。通过代码为实例定义的默认值。通过代码的方式为实例设置属性值来覆盖默认的全同配置。 - 实例配置属性。通过配置文件来为指定的实例进行属性配置,以覆盖前面的三个默认值 **Command属性** | 属性级别 | 默认值、配置方式、配置属性 | | ------ | -------------------------------------------------------------------------- | | 全局默认值 | THRAD | | 全局配置属性 | Hystrix.command.default.execution.isolation.strategy | | 实例默认值 | 可通过@HystrixProperty(name = "excution.isolation.strategy" value = "THREAD") | | 实例配置属性 | hystrix.command.HystrixCommandKey.execution.isolation.strategy | ### Hystrix 仪表盘 为启动类加上@EnableHystrixDashboard, 启用Hystrix DashBoard功能 Hystrix DashBoard 支持三种不同的监控方式,分别是: - 默认的集群监控,通过URL `http://hostname:port/turbine.stream`开启 - 指定的集群监控,通过URL `http://hostname:port/turbine.stream?cluster=[clusterName]`开启 - 单体应用的监控,通过URL `http://hostname:port/hystrix.stream`开启 监控界面各项元素的意义: 实心圆:颜色表示健康程度,大小表示请求流量 曲线:用来记录两分钟内流量的相对变化 ### Turbine集群监控 通过启动类加`@EnableTurbine`注解开启 ## 第六章 声明式服务调用 Feign 启用Feign:在启动类添加`@EnableFeignClients`注解 定义服务接口,定义一个接口(Interface),通过`@FeignClient`注解指定服务名来绑定服务,然后再使用Spring MVC注解来绑定具体该服务提供的REST接口(这里服务名不区分大小写) ### 参数绑定 ```java // 获取参数 public String hello(@RequestParam("name") String name){} // 获取路径变量 public String hello(@PathVariable("id") String name){} // 获取header public String hello(@RequestHeader String name){} // 获取body public String hello(String name){@RequestBody User user} ``` ### 配置 #### Ribbon 全局配置 ```yaml ribbon: NFLoadBalancerRuleClassName: * #负载均衡规则全类名 ConnectTimeout: 200 #链接超时时间 ReadTimeout: 200 #请求超时时间 OkToRetryOnAllOperations: true #是否对所有操作都允许重试 MaxAutoRetriesNextServer: 2 # 切换实例的重试次数 MaxAutoRetries: 1 # 当前实例的重试次数 ``` 指定服务配置 ```yaml userservice: ribbon: NFLoadBalancerRuleClassName: * #负载均衡规则全类名 ConnectTimeout: 200 #链接超时时间 ReadTimeout: 200 #请求超时时间 OkToRetryOnAllOperations: true #是否对所有操作都允许重试 MaxAutoRetriesNextServer: 2 # 切换实例的重试次数 MaxAutoRetries: 1 # 当前实例的重试次数 ``` #### Hystrix 全局配置 ```yaml hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 5000 ``` **禁用Hystrix** 通过`feign.hystrix.enable = false` 来关闭Hystrix或 `hystrix.command.default.execution.timeout.enabled = false`来关闭熔断功能 如果要局部禁用,需要使用`@Scope("protoype")`注解来指定配置实例 **服务降级配置** ```java @FeignClient(value = "userservice", fallback= UserClientFallback.class) public interface UserClient{ ... } ``` #### 请求压缩 ```yaml feign: compression: request: enable: true response: enable: false ``` 可以针对请求的数据类型以及触发压缩的大小下限进行设置 ```yaml feign: compression: request: enable: true mime-types: text/html, application/xml. application/json min-request-size: 1024 # 设置触发压缩的大小下限 ``` #### 日志配置 Feign的Logger级别有下面四种: - NONE:不记录任何信息 - BASIC:金鸡路请求方法、URL以及响应码和执行时间 - HEADERS:除了BASIC之外还记录请求和响应头 - FULL:记录所有请求与响应的明细 全局配置 ```yaml feign: client: config: metaDataClient: connect-timeout: 3000 read-timeout: 3000 default: loggerLevel: FULL ``` 局部配置 ```yaml feign: client: config: metaDataClient: connect-timeout: 3000 read-timeout: 3000 userservice: # 服务名称 loggerLevel: BASIC ``` Java配置类 ```java public class UserClientConfig(){ @Bean public Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; } } ```

Android 60问60答 (一篇复习整个Android)

# Android 60问60答 (一篇复习整个Android) ## 有哪些移动端平台? 厂商开发平台: - los - 黑莓(blackBerry) 第三方私有平台:第三方开发供移动设备厂商使用 - WindowsMobile 免费开源平台: - Android - Symbian ## Android中Linux内核的作用 - 充当用户和设备之间的接口。 - 管理所用活动内存和资源共享 - 充当设备上所安装的应用的主机 - 常由硬件制造商使用,因为它提供一个硬件抽象层,可在硬件发生变化时,确保上层保持不变 ## Android使用的数据库是? SQLite,提供可用于所有应用的功能强大的轻量级关系数据库 ## Android有哪些构建块? - 活动:专为一个清晰的目的提供独特的可视化UI - 服务:始终在后台,完成特定的任务 - 内容提供者:存储和检索存储在文件、SQLite、web或任何其他持久化存储位置的数据 - 广播接收器:相应系统范围内广播通知的应用的组成部分 ## Android项目的目录结构 - `/src/main/java`:项目使用的java源文件 - `/build`:编译后生成文件 - `libs`:专有库 - `/src/main/resourse`:应用资源文件,有 - Drawable:位图文件或者绘制对象资源类型的xml文件 - Mipmap:使用于不同启动器图标密度的可会址对象文件 - Layout:用于定义用户界面布局的XML文件 - Menu:定义应用菜单的XML文件 - Raw:需要以原始形式保存的任意文件 - Values:包括字符串、整数、颜色等简单值的xml文件 ## 小部件的常用属性 | 属性名称 | 关联方法 | 描述 | | ---------------------- | --------------- | ------------------------------ | | android:layout_gravity | | 定义如何在布局容器内对齐小部件 | | android_gravity | setGravity(int) | 设置对象在容器中的放置 | | android:layout_weight | | 指定小部件的大小比例。如果不拉伸则设置为0,否则根据权重拉伸 | ### Android颜色表示 AARRGGBB 透明度|红|绿|蓝 ## ListView小部件的作用 此小部件向用户显示一个列表,它与Java中的列表框组件类似,ListView默认支持垂直滚动。要想使用小部件,如下所示: ```xml <ListView android:id="@id/listview" android:layout_width="match_parent" android:layoutheight="warp_content" > </ListView> ``` ## ImageView小部件 用于显示图像,可以从其他资源(例如drawable目录、因特网、内容提供者)加载图像,它根据源图像的大小调整自己的尺寸。要想使用该小部件,如下: ```xml <ImageView android:id="@id/imageview" android:layout_width="match_parent" android:layoutheight="warp_content" android:src="@drawable/map" > </IamgeView> ``` ## WebView小组件 用于显示网页,使用如下 ```xml <WebView android:id="@id/webview" android:layout_width="match_parent" android:layoutheight="warp_content" > </WebView> ``` 要想加载网页需要在Java中调用: ```java WebView view = (WebView)findViewById(R.id.webview); view.loadUrl("https://google.com"); ``` ## Android布局有那些? ### 表格布局 此布局以行和列的形式排列其子视图。表格航对象创建可容纳小部件的行,表格布局的最大列数取决于用最大列数分割的行。 使用表格布局如下: ```xml <TableLayout xmlns:android="略" android:layout_height="fill_parent" android:layout_height="fill_parent" android:stretchColumns="1" > <TableRow> <TextView /> <TextView /> <TextView /> <!-- 这里有三个元素,故是三列 --> </TableRow> </TableLayout> ``` ### 框架布局 以堆栈格式从上往下添加视图,项位置用`android:gravity`属性设置,布局的大小取决于最大视图的大小。 使用框架布局如下: ```xml <FrameLayout xmlns:android:"略" android:layout_height="fill_parent" android:layout_height="fill_parent" > <ImageView /> <ImageView /> <ImageView /> <!-- 这里有三个元素,故是三行 --> </FrameLayout> ``` ## 使用那个方法来关联活动? `setContentView()`方法用于将UI与活动关联,用法如下: ```java public void onCreate(Bundle saveInstanceState){ super.onCreate(sabeInstanceState); setContentView(R.layout.main); } ``` ## 如何注册活动为主活动? 只有在`manifest.xml`中注册过的活动才能被系统访问,要注册活动如下: ```xml <manifest ...> <application ...> <activity android:name="..." /> </application> </manifest> ``` activity标签中也可以添加一些属性,比如可以指定某活动为主活动: ```xml <activity android:name=".mainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <categort android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> ``` ## 活动生命周期? - 正在运行:活动对于用户是在屏幕上可见的 - 已暂停:焦点在另一个活动上,但是此活动依然可见 - 已停止;活动被另一个活动安全覆盖,且现处于后台中。停止的活动也是活动的,其对象依然保留在内存中 ### 活动生命周期方法 - **onResume**:此方法在活动开始与用户交互之前调用。当活动位于栈顶且准备好接收用户输入就会调用。 ![在这里插入图片描述](../../../assets/default.png) ## Intent的作用 意图(intent)是用于激活应用组件(活动、服务和广播接收器)的消息 ## 如何使用显式intent启动活动? 显式Intent指定目标组件的名称以激活组件。 ```java Intent intent = new Intent(MyFirstActivity.this,MySecondActicity.class); startActivity(intent); ``` ## 如何使用隐式intent启动活动? 隐式intent用于激活其他应用中的组件,不通过名称指定目标组件。要使得隐式的intent生效,**需要为被调用的组件设置intent过滤器,`intent-filter`在`manifest.xml`中设置**。之后可以使用隐式intent表述action,Android会自动与现有组件的intent过滤器比较,找到合适的组件 ```java Intent intent = new Intent(); intent.setAction("com.anna.chapter6.a6"); intent.addCategory(Intent.CATEGORY_DEAFULT); startActivity(intent); ``` ## 如何使用Intent在组件之间传递数据 ### 使用Intent 写入方: ```java intent.putExtra("name","Nicole") Intent intent = new Intent(MainActivity.this,OtherActivity.class); intent.putExtra("name", "Nicole"); intent.putExtra("age", 25); intent.putExtra("address", "Shenzhen"); ``` 读取方: ```java Intent intent = getIntent(); String nameString = intent.getStringExtra("name"); int age = intent.getIntExtra("age",0); String addressString = intent.getStringExtra("address"); ``` ### 使用Bundle 写入方: ```java Intent intent = new Intent(MainActivity.this,OtherActivity.class); Bundle bundle = new Bundle(); bundle.putString("name", "Ben"); bundle.putInt("age", 28); bundle.putString("address", "China"); intent.putExtras(bundle); //将bundle传入intent中。 ``` 读取方: ```java Intent intent = getIntent(); Bundle bundle = intent.getExtras(); String nameString = bundle.getString("name"); int age = bundle.getInt("age"); String addressString = bundle.getString("address"); ``` ## ADB是什么 Android Debug Bridge(ADB)充当开发硬件与设备/仿真器之间的通信媒介 ## ADB有哪些常用命令? - device:生成已连接的设备列表 - pull:将指定文件从设备复制到计算机 - push:将指定文件从计算机复制到设备 - `install<path>`:在设备上安装应用(指定路径) ## Logcat有哪些日志级别? - 错误,使用Log.e()记录 - 警告,使用Log.w()记录 - 信息,使用Log.i()记录 - 调试,使用Log.d()记录 - 详细信息,使用Log.v()记录 ## 内部存储区与外部存储器的概念 设备内部的储存,以文件存储,属于应用私有 外部存储器存放在SD卡,所有应用与用户都可以访问 ## 共享首选项是什么? 共享首选项是轻量级机制,用于存储基本数据类型的键值对,是快速存储默认值、类实例变量、用户界面状态以及用户偏好的理想方式 ## 一个程序的数据库在哪? `/data/data/<package name>/databases` ## 如何以编程的方式创建数据库? 在Android中,可以使用`android.database.sqlite.SQLiteDatabase`以编程方式创建数据库。SQLiteDatabase类公开各种方法来管理SQLite数据库。 ## 操作数据库的方法有哪些? - `openOrCreateDatabase(String path,SQLiteDatabase.CursorFactory factory,DatabaseErrorHandler errorHandler)` - `updata(String table,ContentValue values,String whereClause,String[] whereArgs)` - `query(SQLiteDatabase db,String[] columns, String selection, Srtring[] selections,String groupby,String having,String sortOrder)` - `getColumnIndex(String ColumnName)` 返回给的列名的基于0的索引,或1 - `getColumnName(int ColumnIndex)` - `excute()` 如果不是SELECT、INSERT、DELETE或UPDATE,则执行SQL语句 ## 如何检索受SQL语句影响的行数? 使用`excuteUpadataDelete()`或`excuteInsert()`方法 ## 如何访问内容提供者公开的数据? 使用`android.content.ContentResolver`类 ```java ContentResolver resolver = getContentResolver(); ``` ## 如何读写内部存储区 **写文件**: 1. 使用`android.content.Context`类的`openFileOutput()`方法打开或创建文件 2. 使用`java.io.FileOutputStream`类的`write()`方法将数据写入文件 3. 使用`java.io.FileOutputStream`类的`close()`方法关闭文件 **读文件** 1. 使用`android.content.Context`类的`openFileOutput()`方法打开或创建文件 2. 使用`java.io.FileOutputStream`类的`read()`方法从文件读取数据 3. 使用`java.io.FileOutputStream`类的`close()`方法关闭文件 ## getFIleDir与getDir的作用 - `getFileDir()`:获取用于保存的内部文件的文件系统目录的绝对路径 - `getDIr(String dirname,int node)`:在您的内部存储空间创建或打开一个现有目录,接收以下参数: - dirname:要检索的文件夹名称 - mode:文件的创建模式 ## 外部环境Enviroment的有哪些状态? | 常量 | 描述 | | ----------------------- | ------------ | | MEDID_MOUNTED | 已经在加载,可以读写访问 | | MEDID_REMOVED | 不存在 | | MEDID_UNMOUNTED | 已存在但是没有加载 | | MEDID_MOUNTED_READ_ONLY | 已存在但只读 | ## `getExternalStorageDirectory(String type)`:检索外部存储目录 `getExternalStorageState()`:检索外部存储设备当前的状态,返回Environment中的某个常量 ## 如何读写外部设备 需要使用`getExternalStorageDirectory`方法获取到外部存储的路径,然后读写过程与内部存储的读写方式相同 ⚠️:写入之前需要在manifest文件中指定所需的权限 ```xml <user-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"> </user-permission> ``` ## 如何检索公共首选项 需要使用`SharedPerferences`接口提供的方法 - `contains(String key)`:检查首选项是否包含特定首选项,参数指定要检查的key - `getXXX(String key,XXX defValue)`:它从首选项中检索指定数据类型的值 ## 如何获取网络权限 ```xml <user-permission android:name="android.permission.INTERNET"> </user-permission> ``` ## 如何启动一个服务 可以使用`startService)`方法从引用组建启动服务 ```java Intent intent = new Intent(this,myService.class); startService(intent); ``` ## 启动的服务与绑定的服务的区别 ### 启动的服务 启动的服务**生命周期**如下: `onCreate()`--->`onStartCommand()`(`onStart()`方法已过时) ---> `onDestory()` **特点**: 一旦服务开启跟调用者(开启者)就没有任何关系了。 开启者退出了,开启者挂了,服务还在后台长期的运行。 开启者**不能调用**服务里面的方法。 ### 绑定的服务 绑定的服务生命周期如下: `onCreate()` --->`onBind()`--->`onunbind()`--->`onDestory()` **注意**:绑定服务不会调用`onstart()`或者`onstartcommand()`方法 **特点**: bind的方式开启服务,绑定服务,调用者挂了,服务也会跟着挂掉。 绑定者可以调用服务里面的方法。 ## 什么是广播接收器? 广播以intent的形式传达。要接收和响应广播,应用要包括一个称为广播接收器的组件。广播接收器不提供UI。不过,它可以创建状态栏通知,以在收到播时向用户发出提示 ## 创建广播接收器要调用哪个方法? - 创建一个类来拓展BroadcastReceiver类 - 在这个派生类中重写onReceive()方法 ## 如何注册广播接收器? 以下两种方法都可以: **在订单文件中配置** ```xml <application ...> <receiver android:name="./myReceiver"> <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> </intent-filter> </receiver> <user-permission android:name="android.permisson.ACCESSS_NETWORK_STATE" /> </application> ``` **以动态的方式** 使用与主应用线程中的过滤器匹配的广播intent调用`registerReceiver`方法,这个方法的签名是:`public abstract Intent registerReceiver(BroadcastReceiver reveiver, IntentFilter filter)` ## 创建一个菜单资源要用哪些元素? - `<menu>`:菜单资源的根结点 - `<item>`:用于创建菜单项 - `<group>`:是一个可选元素,允许对菜单项进行分类,从而允许共享属性 ## 创建菜单要使用哪两个方法? - 要重写`onCreateIotinsMenu()`方法 - 在这个方法中创建`android.view.MenuInflator`类的`expand`方法来拓展菜单 ## ViewPage是什么? 可以使用AndroidX的ViewPage小部件创建滑动视窗 ## 如何将主题引用到整个引用? 在manifest中将`android:thene`属性添加到application标签 ## 自定义控件要拓展哪个类? View ## setGravity怎么使用? 可以使用`setGravity(gavity,xOffset,yOffset)`方法将非常灵活地将消息条放在屏幕的任何位置 ```java Toast toast = Toast,makeText(...); toast.setGravity(Gravity.BOTTOM|Gravity.LEFT,0,0); toast.show(); ``` ## 设置闹铃到几种方法的区别? - set:设置闹铃 - setRepeating:设置重复闹铃 - setInexactRepeating:按照定义的时间间隔重复闹铃,但如果设备处于休眠状态则不会唤醒,这样更省电,即不准确的重复闹铃。 ## 常见的定位技术有哪四种? - GPS - WiFi定位 - IP地址定位 - 三角测量法 ## 如何侦听传入的SMS ```java Bundle bundle = intent.getExtras(); if(buddle!=null){ Object[] pdus = (Object[]) bundle.get("pdus); SmsMessage[] messages = new SmsMessage[pdus.length]; for(int i=0;i<pdus.length;i++){ messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]) } } ``` ## 如何监听电话状态? ```java String nameService = Content.TELEPHONE_SERVICE; TelephonyManager teleManager = (TelephoneManager)getSystemService(nameService); ``` ## Canvas与Paint的区别 Canvas(画布)表示可以绘制图形和图像的表面。提供了多种方法来绘制简单的图形、路径、位图和文本。 Paint(画笔)提供绘制富豪、文本和图形的样式和颜色的相关信息。 ## 如何画一个圆? ```java cavas.onDraw(){ // drawCircle(float cx,float cy,float radius,Paint paint); cavas.drawCircle(50,50,30,paint); } ``` ## MediaPlayer的getCurrentPosition与setOnPerparedListener方法的作用 - `getCurrentPosition()`:返回当前播放的位置,单位毫秒 - `setOnPerparedListener(MediaPlayer.onPerparedListener listener)`:注册一个回调函数,当媒体源准备好播放时,可调用该函数 ## 在哪里声明权限? AndroidManifest.xml ## 如何运行时申请权限? 调用`checkSelfPermisson(String perm)`来检查权限,以保证用户没有把权限撤销 使用`requestPermissions(String permissions,int requestCode)`来请求权限,参数一是请求的权限,参数二随便给一个数字 ## Android打包成什么? .APK

我的大学

# 我的大学 自己的大学生活居然已经结束了,从此以后,便不再是学生了。自己做学生,已经做了近20年,没想到这居然也有结束的一天,而且这一天来地如此漫不经心,一如平日。没想到埋葬我学生时代的日子,居然是那么普通,那么平淡,那么不动人心弦。以至于在那一天过去好长一段时间之后,我才猛地反应过来:哦。原来已经不再是学生了。 虽然已经到了这个地步了,已经结束了一段时间了,已经晚了。但是我还是想在这里,回忆一下我的那一段,没有任何值得说道,但又值得一说的大学生活。毕竟再晚的话,就全都忘光了。 ## Ⅰ 不算好的起点 经过三年地狱般的高中生活,我,一个非常刻苦但又并不十分努力的学生,以一个非常不理想又不算低的成绩,在一个不算好也不算坏的城市,上了一个在双非中顶尖的学校。 哈,真实矛盾又老套。 在高一的时候,我常常幻想自己即是天选,时不时做着不知所谓的清北梦。到了高二,见识过强者,认清了现实的自己把目标定为了北邮——一所CS非常好的学校。而在高三的时候,心灰意冷的我将目标定为了北交——至少是一所位于北京的211。 但是很遗憾,正所谓求之上者得之中,求之中者得之下。而像我这样的求之下者,最终当然是什么都没有得到。 *最低だ* 想想高中时候的自己,真是一个无耻的可怜虫。这些年来,我一直在说自己的注意力是多么的不集中,因此多么不擅长解决复杂的数学和理科问题。而那一年的物理和数学又是多么困难,因此我的发挥是如何如何的不好,最终成绩是多么多么的不理想云云。 但是真的只是如此吗?我把一切都归结于自己的性格是多么的不适合应付数学和物理的考试。但是我却从来没有说我细腻的感情和广阔的思维在英语和语文这两门科目上给予了我多大的优势。性格与运气只是一部分原因,我频繁的提及它们只是想掩饰另一部分原因。 我在高中的时候情不自禁的喜欢上了一个人。当然,她并不喜欢我。要否认这件事对学习没有影响,即使是对我这样的人来说也还是太难了。但是喜欢上这样一个人我并没有任何怨言。 而让我感到不安的事情是,因为被她拒绝,我神使鬼差的接受了另一个女生的示好,并与她开始了恋爱。就在高中快要结束的最后一段时间。当然,那时候的我脆弱、哀伤,没有谈过恋爱,并且处于青春期。但是这都不该成为借口,不该成为爱上一个自己本不喜欢的人的借口。 当然,在高考结束后,这段恋爱也顺延到了我的大学。现在想想,那一段时间是对自己的不负责任,但更多的是对另一个人的不负责任。她是如此的敏感、多疑、软弱、暴躁。但是这都不应该是我伤害她的理由。许多人的第一段感情都是模糊地就结束了,但是这也不能将我对她的伤害正当化。我对她的伤害不需要刻意,接受她的示好那一刻,就已经在开始了。 当然,她也在伤害着我,但是这不是我应该反思的事情,而是她应该反思的事情。直到现在,我还是讨厌谈及这段感情,但是至少经历过一段感情之后,总不至于像一只饿狗一样对待感情,这也算是少有的一点好处吧。 在大学的上半年,带着刚刚走出高中的稚嫩,我进入了大学,充满迷茫。那个时候我想要考研,目标是北邮,准备一雪前耻。而20年年底,疫情正浓。那个时候,想出校门几乎是不可能的。尽管如此,我依然绞尽脑汁通过大学的围墙。我是一个多么爱好自由的人,那个时候我觉得:墙的那边,是自由。 对于大学,至少有一件事我做对了,我选择了一个适合我的专业。当开始上C语言这门课的时候,我是多么的兴奋,我觉得自己很幸福。摸索着写下第一段`hello world`,我仿佛也感悟到了世界的美好。大一上半年让我印象最深的,无疑就是C语言这门课了。感谢教我这门课的吴老师,她对我有充分的信任。信任!对我来说,信任这种东西,是师生之间最难得、最美好、最珍贵的羁绊。她解答我奇诡的问题,放任我自由的探索自己的兴趣,以及用超出常规的手段解决她的问题。 我像一匹脱缰的野马奔驰在知识的草原上,我踏过每一寸草地,嫌弃后面的驽马跑的太慢。我迈开马蹄,远远的甩开了同伴,却也不担心迷路。在学期的一半,我就已经自学完了整本教材(按照课表,这本教材要在这个学期学完大部分,留下几章到下学期学,以及几章不需要学习),并且自己看完了《C与指针》,以及在假期看完了《C++ primer plus》。《C与指针》是一本很难理解的书,但当我在未来解决关于C语言中的指针问题时,脑海中总会突然浮现出书中内容,并在那一刻理解那段文字的意义。 另一个让我记忆颇深的是王老师的选修课:中国古典诗词鉴赏。这又是一个我十分擅长的领域,我在课堂上尽情的出风头,回答老师的问题,与他交换对诗词的理解以及交换双方的诗作,最后在下课后,八点五十分,我和他一起走出教学楼,在美丽的剑湖前散散步,然后目送他开车离开,我去711买一颗饭团。他是一个高材生,也是一个好老师。 虽然有一些美好,但是在半个学期结束之后,我还是对大学大失所望。我以为大学应该是自由的地方,学生与老师自由的交换的自己的想法,大家都有一些崇高的理想,在努力做一些很酷的事情。但是到了大学之后,我发现我能看见的只有没有什么学术氛围的教室与图书馆,没有什么理想的同学和老师,以及令人作呕的官僚又僵化的管理模式。我承认,青大的管理已经算是国内众多大学中的佼佼者了。但是即便如此,我也必须得挑明,这与我的理想存在太大的差距。这让我相信,研究生的生活大概也不会好太多,学校其实并不适合我,因此,我选择了放弃读研。 在大学的上半年结束后,我终于和她分手了。在那之后,我用了一个星期在W3CSchool学完了HTML、CSS以及JavaScript,然后大病了一场。 痊愈之后,我的大学生活仿佛正式开始了。 ## Ⅱ 最牛逼的大艺团 但是回想我的大一生活,我也还是做出了一些正确的决定的。比如,加入了大艺团。当初想要加入大艺团仅仅是因为我觉得人在大学理当加入一些社团。当大艺团到操场上进行军训慰问的时候,我被他们吸引了。 真有活力啊,如果我加入他们,我也能变得这么有活力吗? 这么想着,我递交了申请表,最终成功加入了大艺团的新媒体中心和微电影剧组。最开始接到的任务都来自微电影剧组的lt姐和yw姐(因为平时都是称呼名字,这种敏感信息就不盒了,如果不知道外号的就用首字母拼音代替了,实在抱歉)。她们两个是相当美丽、温柔又可靠的学姐。 但是刚开始接到任务的时候,我的想法却很单纯,因为当时在东校区,而大艺团的活动大多在中心校区,所以每次去参加活动大概都需要开具请假条——找到了一个通往自由的好办法!通过P请假条,我获得了自由出入学校的特权。在疫情期间,这是多么伟大的权力。 此时我还不知道,在我大一的下学期,当我开始认真的对待自己的大学生活时,我将会在这里遇到足以让我怀念一生的好朋友:小黄、聪姐、万妈妈、yn、kt、一滑、csj、hyj、wjy以及令人尊敬的童哥。 在那半年,发生了很多很有趣的事情。+7姐带我们去city walk,吃海鲜,zy姐在清明带我们去中山公园拍樱花,wjy借给我一部相机,这是我的摄影启蒙。那一天的zy姐和yn,是比樱花更美的模特。在那之后,我带着借来的相机参与了一场又一场的拍摄,从每一张都是糊照的丑照,到渐渐有几张能够被选上推文,那段时间摄影技术也算有些进步呢。 而在一场场的活动中,我与这些伙伴们的羁绊也在一点点的加深着。zy姐、+7姐和xy姐都是很好的学姐,能够很好的引领我们。而和wjy,我们两个男生私下里有很好交情,时不时约着来一场city walk。至于其他人,我们大都在大二的时候选择留在团里当部长,继续配合着工作。 小黄和聪姐,我们是经典的《我们仨》组合。当有人说”不相信男女之间有纯洁的友谊“时,我们三个人一块站出来,抛出另外两人的名字以作反驳。我们一起约了很多顿饭,一起配合着完成了很多工作,一起在去轰趴,一起喝过好几次酒。我仍然记得那年冬天雪后,我们一起去石老人海滩,在地铁站对着玻璃比划着身高,一起在积雪未消的沙滩上散步,那天的云粉粉的、蓝蓝的,那么洁白。 yn,是我喜欢过的女生。在大一的末尾,我和她曾是那么默契。我们在图书馆、在食堂那么碰巧的遇到。我曾经说过,她是那么像我曾经喜欢的那个女生。给yn看过她的照片,她也惊叹彼此的相像。但当最终被她察觉到我的心意的时候,她也像她一样,拒绝了我。理由是:“我觉得你喜欢我是因为我像她,而不是真的喜欢我。”其实啊,我自己又何曾了解过她呢?她就像我心中的一抹魅影,是我心目中最美好的伴侣的投射。我喜欢她只是因为她符合这个投射,因而她也就代表了这一抹投射。而我呢,喜欢的大概真的是你。不过呢,被拒绝了也算是毫不意外,**自己最想要的永远也得不到,这件事在过去、在未来,在我身上已经也还将无数次的发生**。不过幸而这件事没有影响我们之间的关系,我们最后还是成为了朋友,如此自然,犹如当初。后来,我们还成了搭档,而且意外的还挺默契的呢。 万妈妈,虽然和我们同级,但是她如此成熟可靠,像是靠谱的“大人”。csj呢,虽然也曾是新媒体中心的伙伴,但是她果然还是更适合声乐部。当她站在舞台中央的那一刻,整个人的风貌与气质都得到了升华。有些人就是为了站到舞台中央而生的。hyj,中途加入的超级E人,帅气又有趣,有他在饭桌上从来不会无聊,舞蹈部的“青大黄子韬”,我的同乡,也是同专业的同学,是让我爹感叹“很少有跟我儿子那么帅的人”,我知道我爹之美我者,私我也,我自己其实很羡慕你的外貌、身材、性格、智商……你只是缺了一点运气罢了,总有一天会混的比我好的。以及我们的童哥,作为常务副团长,真的是台前幕后样样精通,唱过歌、跳过舞、写过稿、拍过照、剪过片、画过图……从一个外行人到十项全能,童哥是真正的热爱并努力。 当然,还有很多人,此刻不能一一列出来回忆了,真的很感谢大家给我留下如此美好的记忆。 我还记得在大一的时候,毕业歌会结束后,和毕业的学长一起去吃饭,我和大艺团的伙伴们纵情畅饮,找自己最喜欢的伙伴们一次又一次的干杯,仿佛怎么也喝不醉。 到了大二的时候,又是毕业结束后,这次一别,可能大多数人都不会有太多的交集了。大家开始忙于考研、忙于自己的事情,只有少数人会去竞选团长的职位。而我,忙于各种各样的事务,在那个时候居然感到松了一口气。那天晚上,大家依旧推杯换盏。有了更深的羁绊的大家,想着即将到来的别离,只有将杯中酒一杯杯的饮尽,一次又一次热烈的拥抱,伤感的流泪。那时候我面对这个场景,没有落下一滴泪。只有肩上的担子卸下时的放松。 我真傻,真的。如果那时我知道那是我最后一次参加毕业歌会,我一定再喝一打酒,痛哭流涕着和每一个小伙伴都热烈的拥抱一番。可惜,我那时什么也不知道。 如你所见,我没有参加自己的毕业歌会。那一天,我在外面实习,没有办法赶回学校。就这样,错过了。我这一生,很少会为自己的决定感到后悔,但是想到这件事,只能感叹天命无常,痛苦不已。 “大艺团,牛逼!” 我多想再和大家一块喊出这句口号。但是啊……渠会永无期。 ## Ⅲ 我的舍友们 在软工系,最不缺的就是有个性的人。但是即使是在这群有个性的人当中,我也是最有个性、最乖张的那位。 我很感谢我的舍友,他们让我在宿舍里过的很不错。那些在网上见过的对舍友的吐槽没有出现在我身上。 我记得大一刚到宿舍的时候,最开始是与mc君相遇。当时RE:0正在连载,我发现我们都在追番。于是,我们成为了好朋友——短暂的好朋友。虽然有相同的兴趣,而且我也相当喜欢他,但是我们却无法相处的很融洽。 归根结底,是因为他是一个阳光、外向、开朗、兴趣广泛又正常的人。与我这种阴暗逼合不来大概也正常。但他却与宿舍另外两个人玩的很好,一个是cl君,他也与我大不相同,开朗、温柔、外向又正常,喜欢健身和运动,身上充满肌肉,大概是大部分女生都会喜欢的类型。他们两个经常约着一起健身。说实话他们两个实在是很适合做朋友。另一个人是我们的班长,说实话,我讨厌他,因为他太阳光、太开朗、太正确、太正常,简直是我这种人的反面。所以,我讨厌他。 我是一个怪胎,所以跟怪胎更合得来一些。比如宿舍里面有一个老哥,几乎不学习呢,整天要么在宿舍里躺着,要么就是在玩游戏。但是我觉得他这样挺好的,这才是大学生该有的样子。与他相近的是朋哥,他虽然也天天不是躺在宿舍就是在和我们打游戏。但是他却极其聪明,每次考试前稍加用功便能学会一学期的知识,在考试时取得高分。而且游戏还打的非常好。真是令人羡慕啊。此外就是呆哥,呆哥确实是呆呆的,而且总是在省吃俭用。但是他却也是一个温柔善良的人,虽然是一个怪胎,但是却又更像是一个正常人。 当然,宿舍更是有几个重量级的怪胎。 一位是学霸哥,人如其名,是个学霸,宿舍里的成绩担当。是唯一一个学识不在我之下的人。但是我们还是有些区别的,我只在自己认定的事情上努力,剩下的时间全部都在玩了。而他呢,大部分时间都在努力。我其实是真心的佩服他。但是我又是如此好胜,所以我们有时会莫名其妙的针锋相对,谁也不服谁。不过幸好最后我们没有选择同一条跑道,我选择了就业,他选择了读研。不再相互较劲之后,我们的关系好了很多很多。幸好在前进的路上没有他,不知道他是不是也为前进的路上没有我而长舒了一口气呢? 一位是仙不觉,或者叫秦无衣,但是他本人其实姓杨。这就已经够奇怪了吧?他是一个相当聪明而且富有见识的人,本来呢也是能成为一个正确的人的。但他偏偏没有那么做。他貌似是觉得保持正确太费劲了。于是他每天都和我们一起做着错误的事情。我们一起打游戏一起追番,一起闲的没事出去吃点好的。居然让他跟着我们学坏了呢。他每天都会说一些正确的事情,但是从来做不到。虽然每天不努力,但是凭借自己的聪明智商与神级运气,居然每次都能卡在及格线上,从来没挂科。他本人其实是那么高情商,大方,能体谅人而富有原则,却甘愿与我们做朋友一起当个怪胎,真够奇怪的呀。但是,能和他成为朋友,真不错呢。 另一位是siri哥。这个哥们,是第一个让我觉得不爽的人。刚到宿舍就开始四处找茬,这人真是一个倔脾气,谁劝也不听,只爱找茬,在宿舍里操着一口即墨话跟朋友大声的开黑。什么人啊这是。但是呢,随着日子一天天过去,我却发现,这个人在和大家开黑的时候喜欢主动承担所有人都不想玩的辅助位,会在不管是谁向他寻求帮助的时候尽力帮忙,会以一种及其拧巴的方式听取别人的建议,以及以和正常人不同的方式,思考和批判自己的人生。随着和他进一步相处,感觉他的脾气也没那么糟糕,再往后呢,我才发觉:什么嘛,这个家伙,原来和我一样,都是一个怪胎嘛。就是这个惹人讨厌的家伙,大概就是我的舍友和同学中,和我关系最要好的一位。 上面这两位,是入选我论文致谢内容的角色。 后来呢,我们专业分流,换了校区,也换了宿舍。 我和siri哥、呆哥以及仙不觉选了相同的专业,故而分在了同一个宿舍。这个新的宿舍还有六个人,分别来自三个不同的班级。真是奇怪啊。但是由于不是同一个专业,加上我的大二大三大都泡在实验室,大三大四大都在外实习。因此我对宿舍的大家也不算太熟悉。只知道其中有一个是整天说着粗鄙之语的粗鄙之人,一位退役的武警,一个帅气的令人羡慕的但是却及其擅长模仿老八的声音的家伙,还有一个努力、上进、阳光、外向又温柔的人,虽然听起来这人也是一个正常人,但我总觉得他也和我一样,是一个思考方式与正常人不同的怪胎。 还有另外两个人,我是十分确定他们就是怪胎的。 一个是振桑,振桑喜欢游戏、喜欢动漫、喜欢特摄。他喜欢这些不是因为这些东西受人欢迎也不是因为这是一种社交货币,他喜欢这些是因为他真的喜欢。和我一样。他不是一个温柔的人,而且对感情很笨拙,一副应付不了爱情和亲情的样子。但是啊,他是一个真诚的人,是一个赤诚的人。他有自己纯洁的理想,虽然他笨拙,虽然他不怎么努力,虽然他自己说自己又懒又犟。但是我知道,他有一颗那么纯洁那么真诚的心。上苍总有一天会对这样的人降下馈赠。而且,就算上苍不馈赠他,总有一天,他也能自己创造出给自己的馈赠吧。 另一个是康子,康子是一条狗,他自以为自己是一条狗,而其他人也认为他是一条狗。但是他其实是一个人。可能他知道狗山上灵活,是狗之乐也。我非康,故不知康知不知狗之乐,但是当一只狗,大概是挺快乐的。因此他选择了快乐的生活方式。所谓不知康子之梦狗与,狗之梦庄子与?康子与狗,此必有分也。此之谓物化。而这样的狗人,自然是一个不折不扣的怪胎,每天从狗嘴里面吐出莫名其妙的狗叫,但是这些狗叫传到我的耳朵里时,我却觉得蛮有道理的。我们能相互理解,大概是因为我们也是同一类人吧! siri和康,大概就是我的舍友中最奇怪的二人,也是我最好的朋友。现在总是盼望着能够回到青岛,如果是冬天,就一块煮一顿火锅;如果是夏天,就一起吃一顿烧烤。真是的,让人又多了一个怀念青岛的理由啊。 ## Ⅳ 追梦还是一起开心? 对于一个有理想的CS人,参加算法比赛几乎是一个绕不过去的话题。我还记得我第一次知道数据结构与算法还是从学霸哥哪里。有一次学霸哥出去自习完完回到宿舍,打了一天游戏的我阴阳怪气的问他这一天是去卷了高数还是C语言,还是……(巴拉巴拉列了一堆编程语言) 但是学霸哥却一脸不屑的告诉我:说了那么多其实都是一种东西,自己是去学了数据结构和算法,这是所有语言通用的。 极具虚荣心的我不甘落后,随后也去刷完了[B站王卓老师的数据结构网课](https://www.bilibili.com/video/BV1nJ411V7bd/),随后又去买了一本北大出版社的数据结构看完了,最后又去买了一本邮电出版社的《ALgorithm》,仔细的看了一遍。 当然,光说不练假把式,在看网课看书的同时也需要去刷题练手。恰好在大一的时候学校组织了一次PTA钻石级考试。虽然叫钻石级,但是钻石实际上是非常低的等级。那个时候还没有开始学习数据结构,靠自己仅有的努力考了七十多分,但是还是比学霸哥低一点。但最令我感到惊讶的是,我两个小时没有做完的题目,旁边的那个同学居然花了十几分钟就交卷了,而且考了满分。 后来我才知道,这是大名鼎鼎的wz哥,是转专业留级的同学。虽然wz哥有着很强的实力,但是他的大名鼎鼎可有很大一部分不是好名声,而这不是好名声的部分却又不是来自与wz哥。个中缘由,我也是后来才从要好的学长那里听说。 后来,就开始自己在PAT上刷题,刷完乙级刷甲级,连做带抄,总算刷完了甲级。但是甲级题对我来说,其实已经有点捉襟见肘了。毕竟自己没有受到来者的辅导,全靠自己摸索,既没有科学的方法,又没有领路的明灯(哪怕我早点发现[oi-wiki](https://github.com/OI-wiki/OI-wiki)这个仓库)。更不要提做算法题这件事和我最不擅长的算法题有多么相似了。 我做题,每次不是因为过度优化、想的解决方案太复杂而写不出来,就是因为考虑不全面而<font color=green>WA</font>,很少有能够一发<font color=red>AC</font>的时候。对我而言,做算法题常常是一种折磨。但是如同之前的很多次那样,虽然折磨,虽然不擅长,但是我一直在坚持。 后来,也跟着学校的zzm老师,加入了一个叫做软件梦工厂的群。这是一个学校里专注于算法比赛的群,里面有好几百号人,其中有很多都是往届和我目前的学长,看起来充满底蕴。里面不乏算法与工程领域的大佬。 但是这个群却死气沉沉,每次群里发点什么东西,都有一群人冷嘲热讽,阴阳怪气,语气中充满着火药味,言语中满是虚伪、谎言、算计、拉踩。在这里我仿佛看到了比我更恶毒更低俗的存在,我承认,当时我确实为这种氛围感到恶心。 本来就不算擅长算法的我,由是拒绝了软件梦工厂的一切集训,断绝了除算法比赛外一切与这个群的联系。不过在刚开始懵懂的时候,我也曾在群里问过学长们两个问题。其中有一个问题得到了一个学长热情的解答。这让曾经一无所知的我深受感动。 后来,当我得知zzm老师是如何如何的以权谋私、不劳而获、恫吓同学、鱼肉队员、排挤同僚,我深深的为自己的决定感到正确。如果当初真的一门心思搞竞赛,我一定会过一个不快乐的大学。 后来我还听说,我们学校原本除了软件梦工厂,还有一个叫一起开心的算法团队,是我们学院的周院长领导。那个学长就曾经是一起开心的成员。因为周院长采取放养的态度,大家都是老人带新人,真正的贯彻了"一起开心"这个主题。但是很可惜,当我了解算法比赛,准备进入算法团队的时候,一起开心就已经被软件梦工厂吞并了。鉴于zzm老师如此让人恶心,所以才有那么多人在群里与她暗戳戳的针锋相对。 我没去过一起开心,但是我的朋友中有一起开心的队员,却没有梦工厂的队员。因此,我主观的相信,一起开心一定是一个能够让大家一起开心的地方。 但是啊,已经没有一起开心了。 如果不能一起开心的话,那么也就没有必要一个人不开心的造梦了。于是,我的算法生涯就这样还没有开始就结束了。 ## Ⅴ 未来,309实验室 放弃了加入算法集训集训队,那么就得找点别的事情做。我那时是这么想的,因为我太想进步了。后来听说有一个叫未来研究院的地方正在招人,而且这个地方离我的宿舍很近,于是我就联系了赵叔(当时还得叫赵学长),想要加入研究院。 ![](../../../../assets/default.png) 我还记得刚见到赵叔的时候,是在21年11月月末,他穿了一件黄色的外套,手上端着一杯瑞幸,让人一看就感觉是个大厂程序员的感觉。后来他领着我在研究院逛了逛,领着我去了308实验室(刚开始的时候我们在308,后来到了309实验室,并且在那里一呆好久),这是我们办公的地方。随后他问了我几个问题,在发现我有前端基础之后决定让我跟着另一位学长搞前端。 这位学长就是ALGarth了。我记得那天下午第一次与ALGarth见面的时候,他正吃着一份看起来很不错的外卖。在跟我介绍目前正在做的项目之前,他先跟我介绍了实验室点外卖的地址,介绍了自己放在实验室的充气床,以及刚买的香烟平替——一大袋子阿尔卑斯棒棒糖。并且随手递给我一根,表示自己找个打火机点上(?)。 就这样,我的实验室生活开始了。在这里,我认识了赵叔、ALGarth、互为、银龙、大大大墨水、厨酱、金酱、光源等等很多和我一样不太正常的人,他们都成为让我受益一生的朋友。 在我刚来研究院的时候,在做的是一个面瘫线上诊疗的横向项目。这个项目说实话,写的很烂,但是让我很受益。这是一个使用Uni-APP开发的项目,通过包装一个webkit浏览器,让我们得以用web技术栈开发手机APP。在algarth(区分大小写好麻烦,后面都是小写了)的帮助下,我学会了使用git,对于前端的工程化初窥门径,以及最终于的,让我了解了前端的技术栈以及坚定了自己成为一个前端开发者的愿望。 后来赵叔和algarth给我们几个新人报名了一个学校JAVA社组织的比赛。这时是我和互为、银龙第一次合作开发。我们都没有经验,开发的及其艰难,天天在熬夜攻关。最终,因为我们的经验不足,只做出了一个半成品,与名次失之交臂。 但是赵叔和algarth的小组却拿了二等奖,几个学弟组成的小组拿了三等奖(赵叔给他们找了个开源项目,让他们包装包装,拿奖全靠口才)。对于这样的结果,我这种阴暗又好胜的人自然是及其不爽。本来已经阴沉着脸回宿舍了,但是ALgarth却给我发消息,说周末一起出去拿奖金吃个饭,剩下的钱就大家一起分了。我极力推辞,说自己没有拿到名次,哪里好意思分奖金呢?赵叔却回复到:“说什么呢,你们三个人这几天的努力,大家都看在眼里。这次就是为了试试你们的实力,后面还有大钱要赚呢!” 后来啊,做了面瘫诊疗与好停车这两个还算成功的项目,以及一个目前来看确实失败的项目,自己确确实实赚到了一笔钱。正是靠着这一笔钱,让我顺利开启了自己的实习之旅。 虽然当时将做项目和赚钱看的很重。但是如今回忆起来,才发现这在我的记忆中占比是多么的轻,寥寥数笔便写完了。原来这段记忆中,最重要的,还是和大家留下的美好回忆。 我记得我们一起在309实验室里面当作会议室的小屋里吃过好几次饭,有几次是外卖,有几次是火锅,还有一次,是本来准备去实验室的天台烧烤,结果因为买到了劣质的碳,点不着,最终烧烤变成了用平底锅炒肉。这一次让人印象很深刻,因为那时疫情正严重,学校封闭。我们不能出去吃饭,只好去美团买菜,让骑手买完之后,把食材绑到我们从楼上扔出去的绳子上,我们再将东西拽上来,因为过程艰难,所以极其难忘。虽然每次在那里吃饭都有些不同,但是每次我们都会放上一集让子弹飞,喊着已经滚瓜烂熟的台词,一起“吃着火锅唱着歌”。电影放完,饭也吃完了。只是里面这个屋子不通风,饭菜的香味经常许多天无法散去。 以及在这个实验室,我产出了两个还算有趣的开源项目。一个是输入信息一键生成电子请假条的小程序,在能够用电子假条的时候我们用这个小程序出入自如。另一个是输入信息一键生成核算预约码的项目。靠这个网站我们不用去抢核算预约时段的名额,在三天两头做核酸的时代做到了想什么时候去做核酸就什么时候去做核酸。 但是要说最有意思的还得是那个项目!那一次因为疫情封校太无聊,我们决定整个活。于是我们用一些嘉然、溜溜梅、美羊羊啥的抽象图,下面配一个二维码,打印了六七张,贴到学校的各个地方。其中有一张贴到了餐厅门口的显眼处,作为第一张。扫描这一张图片上的二维码,将会得到一个数字和一个提示,暗示下一张图片贴在哪里。就这样一张一张找下去,最后将会找到贴在未来研究院院子门前的一张图片。扫描之后会得到最后一个数字,以及一个输入框,将之前得到的数字输入经过计算后,会得到一串特别臭的数字:“114514”。这就是最终的答案,届时会告诉得到答案的人未来研究院309实验室的地址,凭最终答案可以得到我们提供的奖品:两颗溜溜梅以及一颗印有IFF(institute for future)的钥匙扣。 除此之外,我们还一起做了很多很多有趣的事情,一起去餐厅吃饭,深渊二楼的大阪烧是我们的最爱;一起打原神的深渊,比比谁先满星;被带入坑明日方舟,天天缠着Algarth给我挂助战;一起用实验室72寸的大屏幕玩“开火车”;一起看ASOUL直播和原神前瞻……原来,我有一段时间觉得在学校的时光那么开心是因为有你们啊。 当然,让我们能够拥有空间和时间去做这些事情的人,刘老师,当然值得特别的感谢。作为研究院的院长,刘老师是一个温柔又负责,知识渊博又高瞻远瞩的老师。非常感谢刘老师对我们的信任,才让我留下一段如此充实又难忘的时光。希望刘老师能够继续坚持这样,给后面的学生带来美好的回忆。 但是,最终还是要离开啊。在论文答辩的时候,我问互为:这里还是能称为家的地方吗?“互为回答我说:”不能了吧“。我其实早就知道答案如是,只是不愿承认罢了。物是,人非。曾经在实验室的那些小伙伴们,我们的last Dance已经结束了。接下来,是什么时候再见呢? ## Ⅵ ALgarth,我的挚友 algarth,就是上文提到的那个,是我的学长、良师也是我的挚友。为什么说他是我的良师呢?因为他给我解释过好多有关js的问题,其中他给我解释事件循环(event loop)时的情形,我现在还记得。此外,他也给了我许多的学习资料,以及给我推荐了许多好用的工具,这些也让我相当受益。同时,他也教给我很多人生的道理,虽然他这个人活得也就没什么道理的样子……以及前文提到的,在梦工厂的群里回答我问题的学长,也是他。 我还记得当赵叔把algarth的微信推给我的时候,我看到他的头像时候的惊讶。因为这个头像和之前帮助过我的xxx学长的QQ头像是一样的。随后他发消息进行自我介绍:“你好,我是xxx。“ 就这样,无巧不成书,缘分使我们两个本该再无联系的人相遇了。 我还记得第一次见到algarth的时候,他外面穿着那身经典灰绿色冲锋衣,里面是经典米色法兰绒衬衫。那个时候他对我说:”哎呀,学弟,没想到这样还能续上啊!“ 刚见到他的时候,我还以为他是一个靠谱的学长。因为他用很严肃的神情,配合着不协调的滑稽语气给我讲项目背景,讲git使用,讲技术栈选型,讲实验室的一些大大小小的门道。虽然他说的话有些乐子人特有的夸张、扭曲与抽象,但是大体上是正确而精准的。这些知识让我受益匪浅,我当时对他还是心生佩服的。 但是随着相处日久,我渐渐发现,这个人就是一个纯纯的啥呗二次元、互联网乐子人、抽象怪、ACG爱好者、死宅……会因为压力大又不想抽烟而买棒棒糖抽;会追一些只有到了一定浓度才会看的新番;会去知乎刷各种奇奇怪怪的问题;会去搞一些神奇小软件让自己打明日方舟更加顺利;会闲的没事的时候托着一个手柄搓尼尔·机械纪元……总之就是一个和世界格格不入的怪胎。简直是和我一摸一样,不,是比我更是一个怪胎。至少我都不会天天看ASOUL的直播。 正因如此,我和他的关系更好了。于是,在写代码之余,我们开始交流哪些番剧值得补,哪些游戏值得玩,哪些漫画值得看,哪些外设值得买……他拉着我看嘉然的直播,虽然直到现在我也还是觉得V圈没啥意思。他慷慨的把手柄借给我让我去玩NFS,帮我改进我的键盘,让它听起来声音更好听,帮我找入手G502的最低价格,一起挑战Ghost runner的结局,一块研究原神的配队和圣遗物的词条,一块打水月肉鸽,一块研究哪家外卖、哪个窗口便宜又好吃……后来他还送了我一块泥岩的音律通行证。我也曾买给他一块通行证盲盒,结果他开出来一块斥罪。最后我们一人送了对方一个不屈者的通行证呢。 algarth是那么热心,以至于每次考试我都会找他问问往年的题型,每次要买电子产品我都找他帮忙看看参数,每次遇到学校的事件都找他问问往年的情况……如同形成了路径依赖。 后来,我在外实习,他也毕业了。但是我依然在依赖着他,感谢他陪我打apex,跟我在结束游戏后一起聊一会天,陪我度过独自在外的无聊时光。 再后来,我们又一次相见,是他到北京来找我。那一次我们一块逛了漫展,逛了谷子店,看了一场演出,吃了几家餐馆。我们聊了很多话。他说:”互联网上是无法发展感情的,大家都在互相攻击。人与人的羁绊只能在线下建立。在实验室那两年,就像在玩P5,把大伙的高感度都刷满了啊!“ 等他离开的时候,我竟然有些伤感,为我这次手头拮据未能尽地主之谊,也为此次一别不知何时再见。但是我相信,只要怀着想要相见的心情,我们再见面应该也不难吧? ## Ⅶ 一段正常的恋爱 在大三的时候,我也开始了一场恋爱。持续了大概一年多。如果从外人的角度来看,这场恋爱应该是失败的。大概就是一个普通的男大和一个普通的女大谈了一场普普通通的恋爱最后因为各种磕磕绊绊而最终分手的故事。没错,只有在谈到恋爱的时候,我才会承认自己是一个普通人,是一个正常人。 但是就我自己的感觉来说,这只是一场结束了的恋爱,而不是一场失败了的恋爱。至于为何我这么讲呢?且听我下面分析。 这场恋爱是普通的开始,普通的发展,普通的高潮,普通的结束,可能与他人的恋爱别无二致,因此就没有必要详细的讲述我的恋爱故事了。 我只想谈谈我的感觉。这次恋爱,自然是有喜有悲。当得到了出乎意料的小惊喜,当携手走在海边,当遇到挫折时受到对方好心的安慰,那自然是其喜洋洋者也。当受到蛮横无理的指责、当受到莫名其妙的冷落,当看到自己仅有的几件因为没钱添置穿了好几年的衣服时,那自然是感极而悲者也。有喜有悲,人生才会有趣,这是我觉得这场恋爱成功的第一个点。 此外,恋爱也是一次很好的体验,如果不是一起逛过那么多次小吃街,我不会知道台东的松河路一号、金丝牛肉饼,李村的花溪牛肉粉、茶油臭豆腐、车轮饼、鸡蛋汉堡、熏肉大饼、拌鸡架以及鲍师傅的肉松小贝的美味,如果不是一起喝过那么多次奶茶,我就不会知道喜茶的多肉葡萄,茶百道的生椰西瓜,一点点的冰激凌红茶、茶话弄的桂花引是我喜欢的滋味,如果不是逛过这么多次街,我不会喜欢逛KKV、名创这样的精品店,如果不是出去吃过那么多顿饭,我不会知道祥子家和厨大匠这两家好吃的小店……或许这些都是一些消费的诱惑,但是能够知道如何取悦自己也是重要的一课。能够了解更多符合自己喜好的东西,这是我认为这场恋爱成功的第二点。 以及啊,通过一次恋爱,让自己知道如何正确处理和亲密之人的关系,如何让亲密之人开心,又如何让自己开心。自己喜欢什么样的人,又适合什么样的人……如何和喜欢的人相处,又如何和自己相处。这一次之后,大概对自己也有了更加深入的了解吧。 但是一场恋爱,何必总结出个一二三四的功过得失? 我们分手的时候,是一个工作日的早上。我知道这一消息之后,如常去公司工作。过了一个小时之后,我觉得自己必须做些什么,才能章明自己已经是孤身寡人了这回事。于是我从公司后门出去,面对浩浩汤汤的汤逊湖,我摘下手上的戒指,说着希望湖女祝福她,随后将戒指扔入水中,可惜只激起了一点点的浪花。有诗道: ***誓言银戒指,一点落水声*** 随后我就回去继续写代码了,一如既往。 大学谈一场回忆起来没有无尽的懊悔,没有滔天的怨恨,分手时平平淡淡的恋爱,不就已经算成功的了吗?难道还要奢求什么吗? ## Ⅷ 大学的结束从实习开始 因为没有要考研的计划,所以我在大三下学期一开学就开始准备找实习,为后面正式工作打基础了。 现在回想起来,自己的大学生活,似乎不是一个瞬间猝死,而是缓慢的凋亡。这个凋亡的过程,从我找到去实习的时候开始。 在23年的二三月份,新年伊始,疫情结束。大家带着刚刚从阳性恢复过来的躯体投入正常的工作和学习。我也终于可以,也必须找实习了。因为这是最好,也是最后的机会。我本以为凭借自己这些年在课上积累的知识,在算法网站刷的题目,在实验室积累的经验,能够让轻松找到一份合适的实习。但是疫情之后春天却是一片凛冬。 *winter is coming.* 在那个春天,我每天都在优化简历,看机会,投简历,准备项目,背八股文,复盘面试,刷leetcode,看面试题……每天都是匆匆忙忙,慌慌张张,心里七上八下不知所措,对自己对未来充满失望。而同时,和前女友之间的裂隙越来越大,我承受的经济压力也越来也大。那一段日子真是难熬啊。 但是啊,在4.18号,我在OneNote上写下了这么一句话: *今天闻到了许久不曾闻到过得花香。* 那是在从研究院到食堂的路上,一株西府海棠开得正盛。花朵如此洁白,闪着粉色的红晕,如此的健硕,如此的娇嫩,如此的完整,如此的鲜艳。我仔细观赏了这一片美丽的脸庞,心情仿佛好了一些。我这才意识到自己是多么迟钝,是怎么样的做了一件蠢事,原来”春色如此“! ![](../../../../assets/default.png) 那个时候,还有另外一件让我开心的事情,有一个我还算满意的公司让我进入了第二轮面试。不久之后,我就收到了他们的口头offer。好事居然真的发生了!我凭借自己这些年在课上积累的知识,在算法网站刷的题目,在实验室积累的经验,在这春日的凛冬中也如同这株西府海棠一般盛开了! 五月十四号,我兴冲冲从学校请假,准备出发前往实习。随后的一年里,我辗转各地,实习、旅行、在家休息,仿佛自己已经不属于学校了。我与学校的联系,我与学生这个身份的联系,正在逐渐的衰弱、枯萎、凋零。那时候我还不知道,此刻到我毕业,我还能在学校里正儿八经的呆着的日子,还有50天。 ## Ⅸ 在校倒计时 去年五月,我从没想过,自己这一年能呆在学校的日子还有50天,更没有想过,当有一天从这里离开的时候,会觉得有一丝丝遗憾,有一丝丝不舍,有一丝丝怀念。那个时候我一心只想赶紧毕业,摆脱学校的束缚,开始工作生活。 所以当我实习结束的时候,我先回家呆了一个多月。这一个多月里,我把找实习的时候走过的心路又走了一遍。从踌躇满志,到心灰意冷,再到豁然开朗,柳暗花明。这个时候真的很感谢我的妈妈一直陪在我的身边,每天照顾我的饮食起居,跟重要的是,每天都顺着我的心情,不断给我打气加油。也幸好自己一直坚持,不曾放弃。 拿到offer之后,我给自己买了一台相机,随后,在家里和亲人们过了一个快乐的中秋节和国庆节。随后,回到了学校。随后,我给自己安排了一场旅行,一个人去了已经想去了四年的南京。这一次旅行是我玩的最开心的一次。随后,为了赚够来年三月到北京实习的房租和生活费,我又开始去刘老师的公司实习。在那里和互为与银龙一起度过了一小段快乐但又辛苦的时光。从我五月出去实习到从刘老师的公司辞职,我呆在学校的时间只有六月回学校考试、过端午节,以及十一结束之后。这一共只有9天的时间。 随后,12月来了。这一个月,是我四年来在学校里最开心的一个月。12月的学校,是很舒服的,因为夏天没有空调,而冬天的暖气却很足。这个月,因为没有课程安排,没有考试,所以我是如此的自由。每天睡到八九点自然醒,然后去食堂美美吃过早饭,然后开始写[奉纸填词](https://github.com/charlesix59/fill_poem)这个开源项目。这个项目完全按照我自己的想法去进行,我自己进行技术选型,自己去做产品设计,自己做UI设计,自己做方案设计,自己开发前端,自己写了一个简单的后端,自己去做数据爬虫和数据处理……没有预期目标,没有DDL,但是我做的那么开心。每天看着自己写出的一行行代码忍俊不禁。 代码经常就是一写就到中午,随后草草吃个午饭,然后下午写累了就出去散散步。我从学校为中心,沿着青岛扭曲嶙峋的路网向四面八方探索,我向北上山,向南看海,向西进入繁华的闹市,向东进入安静的富人区。我总是娴静的漫步,一路上或者想想项目的设计,或者构思自己想写的小说的剧情,神游八方,意趣盎然。 这其中我最常走的路线,还是香港东路、麦岛路、东海东路、海游路这一圈。因为可以走到小麦岛和海之恋公园,海岸线的风景非常美。虽然小麦岛这一年都在封路修地铁,但是我还是忍不住一次又一次的去到小麦岛,感受那悬崖边上的海风。 到了下午,自然是要吃点好的。这一餐,一般都是经过我的精心构思的,要知道自己想吃什么,需要经过深思熟虑!但是晚上这一顿,我偏向去吃炸串卷饼或者麻辣烫,也很爱披萨、炸鸡、方便面炒鸡与塔斯汀与KFC这种昂贵却又便宜的快餐。但是最开心的还是要说那几次! 有一次和siri哥去台东,金丝牛肉饼溅出的热油洒了他一身,另一次和siri哥去李村,我推荐的低价美味收到了他的大力好评,还有冬至的时候和仙不觉去吃好吃的水饺,还有那天为了让振桑和不觉见识到撒了siri哥一身的牛肉饼,我特地跑了一趟台东给他们买了回来。以及我想一次又一次重复的,我们四个人的自助小火锅。提到美食,总是有满满的回忆啊。 至于晚上,那自然是宿舍一起开黑,用变形的操作和妙语连珠的狡辩引起一阵阵笑声的美好时刻。呆哥每每被我们逗笑,都会评价道:”今天太开心了!“ 是啊,今天太开心了。那一个月,每一天,都太开心了。 到了1月,我回到家里,处理了一些事情,做了个小手术,生了一场病。又是麻烦妈妈在一旁悉心的照顾,只要有她在,我要考虑的事情总是很少很少。随后,和家人们过了一个开心的大年。到了正月十八回学校的时候,我不知道我呆在学校的日子还剩下最后十天。 其中有三天是在去3月1号去实习之前,那段日子在忙于处理各种学校的事情,准备外出实习,时间被肆意的浪费了。当我再回到这座城,这个学校的时候,已经是毕业答辩的时候了。 答辩前一天我才回到学校,处理各种各样的事务,修改论文,打印论文,准备稿子,完善ppt,收拾宿舍的铺盖,和宿舍和实验室的朋友打了个招呼……第二天是论文答辩,我被分到了下午场,于是中午的时候,我背着电脑,来到了海之恋park,坐到面朝大海的长椅上。那天是个晴天,但是海雾弥漫,遮天蔽日。坐在海边,如漫步雨中,我就这样打开电脑,修改和记诵稿子。时不时看看面前的碧海,自己的紧张和疑虑就这么被消除了。 当晚上得知自己顺利通过了答辩时,又免不了和舍友一起去吃饭庆祝。第二天又与实验室的大家吃饭唱K,那一晚我罕见的亢奋,久违的喝了酒,而且又是那种怎么也喝不醉的感觉,因为我知道,在他们面前,不需要隐藏自己的内心,不需要故作矜持。我跟银龙碰杯,跟光源碰杯,跟algarth碰杯,跟厨酱碰杯,跟大大大墨水碰杯,跟赵叔碰杯……晚上我们一起唱着伍佰的last dance,为我们这一群即将各奔东西的朋友们奏响骊歌。 而这几天过去,回到北京之后,我的大学余额就仅剩四天了。再回来,就已经是毕业前夕了。这几天,我急匆匆的从一个地方赶到另一个地方,以为用眼睛扫描过,用脚步丈量过,就可以将记忆中一切打包带走。但是后来我知道,我错了。记忆中的一切,我带不走,却又逃不开。那天晚上,我一个人走上小麦岛,望着漆黑的天空中挂着一轮明月,无尽的黑暗的深渊上闪烁着明亮的波光,海风和涛声将我包围,我喝光了那一罐可乐,忽然想明白了。 那一天,我写下了这样的一篇散文: ![](../../../../assets/default.png) 随后,在毕业的那天,我和siri哥、康桑、振桑以及学霸哥一起吃了一顿晚饭,看着他们,留在那座充满海雾的城市,在浮山后租了一个宽敞的房子住在了一起,我既羡慕,又欣慰。在那之后,我要去北京,学霸哥要去杭州。我和我的舍友们,也离分了。我与这座学校,也离分了。我与这座城市,也离分了。 但是啊,你记住,你们记住,有些联系不会那么容易切断。我会顺着这份羁绊找到你们,一次又一次。 ## Ⅹ 我的大学 这篇文章,从突发奇想开始构思到写完,历时五天,凡一万四千余字。虽然文中有些地方我记不清楚,只取大意。但总体上,我是诚实的。我诚实的回顾了我的大学生活,遍历着这四年的的人生轨迹。 虽然我的大学没教给我多少知识,也没让我有多少收获。但是我还是要感谢它,如果不是选择了这个学校,我也不会遇到那么多有趣的人,度过这四年有趣的时间。 在大学时,我曾无数次憧憬着毕业之后的日子。但是当他真正到来的时候,有那么一瞬间,我居然想要逃避,我居然感到不舍,我居然想要想要抓住时间的衣角,让他慢下来,等我一下。 但是就像我说的,“明日亦有明日未竟之美。”正是这份对未来的憧憬,一直支持着我一步步走到现在,也将继续支持我一步步走向未来。所以,虽然不舍,我也不会停下脚步,我会继续往前,不断往前。 *進め,進め!* 只有这样,我才能拥有与过去重逢的勇气,只有这样,我才能再回望过去的时候感慨:“看吧,虽然受了那么多苦,但是我的选择果然是值得的。”

社会观察——性别对立

# 社会观察 第一篇 男女对立 现在男女对立的现象已经不仅存在于网络之中,更顺着网线蔓延到了现实世界…… ## 经济,此间万物的根本 我们回顾世界各地的女权运动,可以发现,**女性获取政治地位通常发生在女性获得工作的权利之后**。女性广泛的获得工作条件从家庭主妇的身份中脱离出来是在第一次工业革命之后。当社会生产的中心从农村、农业转换到城镇、工业之后,越来越多的人从农村搬迁到城市,进入工厂工作。 当人们的工作性质从繁重的农业劳动变为枯燥的流水线之后,男性与女性在体力上的差距逐渐消弭。越来越多的工作,女性与男性的工作效率相差无几,甚至高于男性。随着城镇化的深入,更多的第三产业的发展,越来越多的女性赚到了更多的钱,在经济上,他们不必依靠男性,可以维持自己的生活。 至此,女性开始尝试从传统的地位中脱离出来,但是她们却四处碰壁。由于一些传统的价值观,女性在政治和经济上受到诸多的限制。于是,**女性开始追求自己正当的权利,包括:选举与被选举权,工作权,同工同酬,生育与性的权利。** 我们必须说,这些权利是十分正当的,它们需要,而且必须,得到或逐渐得到实现。我们观察世界各地世俗国家,不难发现上述的权利大都逐渐的都实现。 以选举权为例,它们实现的时间集中在20世纪20年代左右。一次世界大战结束后,大量的男性死亡,而经济则继续保持发展。于是更多的工作机会对女性敞开,得到更多话语权的女性,追求自己的政治权利是理所应当的。 而发展中国家的男女平等呢,则在20世纪50年代左右兴起。因为大量的国家脱离殖民统治,获取独立。大量的民主或共产政府为他们国家的女性提供了相应政治权利。随着经济的发展,工业化的推进,这些国家的女性权利也逐渐得到完善。 > 在1920年批准《[第19条修正案](https://zh.wikipedia.org/wiki/%E7%BE%8E%E5%9C%8B%E6%86%B2%E6%B3%95%E7%AC%AC%E5%8D%81%E4%B9%9D%E4%BF%AE%E6%AD%A3%E6%A1%88 "美国宪法第十九修正案")》之前,各个州已经通过了立法,允许女性在不同类型的选举中投票;有的只允许女性在学校或市政选举中投票,有的则要求女性拥有自己的财产才能投票,有些领土则向女性提供充分的选举权,只是在成为州之后就将其剥夺。[[53]](https://zh.wikipedia.org/wiki/%E5%A5%B3%E6%80%A7%E5%8F%83%E6%94%BF%E6%AC%8A#cite_note-53)尽管具有合法选举权,但1965年之前,黑人女性的这项权利在南部许多州实际上都被剥夺了。 > > —— wiki 美国女性的政治权利 我们说回到中国,为什么中国如今的男女对立如此严重呢?难道是中国的女性权利得不到保障吗?我认为中国政府在这一点做的相对较好。男女对立如此严重,并非主要是社会制度问题。 男女对立的出现,是近些年的事情。女权的出现,最早也是改革开放之后出现的。为什么1945年之前不会有男女对立呢?因为那时女性是男性的附庸,不存在所谓的对立。而改革开始之前呢?大家一穷二白,忙于社会主义建设与阶级斗争,恐怕无暇与性别的对立。 在改革开放的初期,男女对立仍然极不明显。甚至在进入新世纪之前,女性主义在中国都完全不流行。那时大量男性吃到了改革开放的红利,而大量女性呢,吃到了男性吃到了改革开放的红利的红利。 **到13年之前,虽然女性主义在中国已经有了自己的受众与市场,但是由于经济的快速发展,人们仍致力于勤劳与务实的致富。如果经济增长的势头能够持续下去,那么中国应该也能够形成类似欧美的两性关系**。可惜的是,自13年以来,经济增长速度的放缓、贫富差距的增大与社会阶级的固化都冲击着每一个普通人。 面对令人窒息的生活压力,普通人与富人的子嗣面对的挑战完全不同。富有的人,可以完全不必担心男女对立的问题,而普通人则无法回避,无从选择。 对于普通的女性,**她们无法得到适宜的工作**。那些印象中更适合女性的工作竞争激烈,更因这些岗位男女比例的失衡,使得雇主更愿意吸纳男性加入。我必须在这进行说明,这种行为是正常的也是自然的。首先,对于普通的岗位,有一定量的男性是必要的,因为一些体力活人们更倾向于让男性去做。并且,如果男女比例过大,会使数量少的群体被特殊化或边缘化,是不利于行业的生态的。这样,会**使一部分女生产生女性被社会鄙视的感觉,或者会认为男性抢走了她们的工作,从而对男性产生愤怒**。 对于普通男性,他们无法得到适宜的工作,得不到足够多的薪资,那用于追求女性的部分自然就会减少。而追求异性的代价却并未减少,甚至是逐渐增加的。于是**男性会认为女性比以往更加拜金,更加贪婪,于是对女性产生愤怒**。 不仅是工作的压力增加,**女性通过婚假来达到阶级跃升的可能性更小了**。在经济快速增长的时期,社会上充满了各种各样的机会。如果希望通过特殊的途径来增加自己的财富是相对较为简单的,也较为体面。当经济放缓时,希望通过婚假的方式实现阶级跃升,不管对于男性或女性,都会遇到更激烈的竞争,即使达到自己的目的,也会承受更多的非议。 同时,两人搭伙过日子的难度也有增无减,昂贵的房价,愈演愈烈的消费与难以维系的生育投入,都使得**男女双方对待婚姻更加的谨慎**。这两个条件叠加起来,使得两性在面对结婚这个话题时都格外的挑挑拣拣,疑神疑鬼。这样的行为无疑在消磨双方的精神耐心与爱。**对爱人与婚姻的失望可能会被放大到整个异性群体。** 不仅如此,消费主义的盛行无疑也在将男性与女性的距离拉开。**对于消费来说,女性的价值大于儿童大于男性**。我们从商超的布局就能看出来,第一层多是珠宝化妆品奢侈品这种高溢价的商品,再往上可能是女装或快时尚的衣物更多,再往上是儿童乐园与童装、儿童兴趣班与育婴商品之类。男装要么在更上层,要么散布在角落里。最上层是吃喝玩乐这种功能性与目的性更强的消费作为一次逛街的终点。**消费主义的盛行,对于男性与女性都意味着更大的经济压力**。女性需要买更多的化妆品与潮流的服饰,以实现自身对于美丽的追求,表达自身个性价值以及追去与世界和潮流的同步,同时男性也将一定程度上为这种花费买单。 **消费主义并非不好,而当经济下行与消费主义的盛行相遇时,无数的矛盾就会迸发,男女对立只是其中的一种。** 对于商人,他们需要拆解的传统的家庭,因为单身的男女或者恋爱中的男女能够有更多的消费能力。当男性与女性结婚成家时,他们花费在餐馆、娱乐场所与商场上的价值不出意外的将会减少。但当他们决心结婚来应对消费主义时,高额的房价又将榨干他们最后的一滴血。我们不得不承认,**年轻人已经进退维谷了**。 总结一下上述内容,**当经济经过一段时间的快速发展之后陷入低迷,而又有着巨大的贫富差距和较低的社会福利时,青壮年人就会感到理想与现实的落差。当这个落差不能被抹平时,通常就会爆发社会冲突**。目前不仅是男女对立,意识形态的对立、阶级的对立与地域的对立甚至民族的对立都同样的明显。 ***在迷茫的绝境中,人们本能的将责任甩给另一群人,只需要一点小小的引导,就能引发两派的冲突***。 ## 舆论,推波助澜的帮凶 微博、贴吧等社交平台不仅仅是男女对立的主战场,也是这一现象产生的引导者。在经济如是的背景下,他们引导了男女的双方的对立。 他们成为主战场的原因容易理解,而引导对立的原因却有点曲折。**以这两个平台为例,他们引导男女对立恐怕并非平台刻意为之,但是却是必然现象。** 这一切的根本原因便是受众的不同。 几年前的贴吧还不是男厕所,而是一个供大量同好交流经验与技术的平台。大量游戏、电影、动漫、小说的爱好者聚集在一起相互讨论与分享。几年前的微博也不是女厕所,而是相当多的明星与机构与普通人交流与分享的平台。人们聚集在一起通常是为了追星。他们二者的功能决定了他们的受众构成——贴吧的使用者大都是男性,而微博的使用者更多的是女性。 当然,只有受众并不构成充分条件,**另一重要的部分是平台对负面的、对立的、劣质的内容缺少正面的管理措施**。微博在这个方面已经是积重难返了,在成为女厕所之前它已经引起了数次追星族对其他人的攻击与诽谤。它是一个管理极度缺失的平台,除了某些敏感的言论,你几乎可以在上面说任何话,肆意侵犯他人的权利,攻击侮辱诽谤他人而几乎不需要付出任何代价。而平台的管理者在很多时候是起反作用的,他们不仅允许使用金钱燃起或消降热度,更默认购买使用机器人水军的行为。 贴吧在几年前还存在一个十分强力的民间自治组织,叫做吧务组。如今,这个制度恐怕濒临分崩离析了,为了热度与讨论度而牺牲专业性与自治性,导致如今的贴吧在言论的令人不适程度与思想的扭曲程度恐怕无人能敌。 在微博上的某些妖魔鬼怪在胡作非为的同时,其他人群在各种平台进行了不同程度的回击,其中以贴吧的回击在声量上最大,态度也十分激烈。微博上某明星的支持者与贴吧上的非议者相互攻击能够被看做是当下男女对立的雏形。 到此为止,**两个平台都完成朝向男女对立的首要战场的演化:某个性别占主导的受众构成、不受限制的劣质与负面内容,以及对冲突与话题的追求。** 虽然这两个平台依然承担着自己曾今的任务,但是最主要的街面上,已经充满着硝烟与恶臭。 不过还有一个问题,对立内容的生产者从何而来呢?我们绝对不能否认,一些生产者只是对自己的遭遇感到愤怒与不满,而在网络上宣泄自己的负面情绪,人自有恻隐之心,对这些人应该援以善意的问候,而非精心的引诱与利用。除此之外,绝对是有大部分人是出于某种利益而进行如是行为。一个原因是境外势力的煽动,我认为这种猜想可以被忽略,即使真的有境外势力,他们也没有如此大的能量与体量。**最主要的原因还是对流量的追求或者输出的观点能够为自己带来好处。** 前者容易理解,比如咪蒙,依靠热点话题带来的流量吸金。后者其实也常见,从男女对立上受益的势力有很多,比如部分情趣用品、某些单身经济的实体行业,以及很多受众性别单一的商品都可能从中谋取消费者的关注。 恰好,此时有两个广阔的舞台仿佛特意为他们搭建,能够让他们尽情演出。**在他们巧妙设计的剧情下,女性与男性之间的关系形同水火,势不相容。他们扭曲事实、断章取义,试图用一面之词、只言片语去评价一个人**——多么可鄙的一种行为。 当社会中男女对立被挑起时,其势如燎原烈火,恐怕很难再被阻挡了。 而如何男女双方如同上好的柴火如此容易引燃如同积怨已久? ***看过当下,或许我们还应该回顾从前。*** ## 历史,物是人非的困境 上文已经说过,在47年之前,女性一定程度上是男性的附庸。不只是中国,目前世界上所有的主流国家,在进行工业化之前,男性的地位都要高于女性。工业化之前,劳动主要以体力劳动为主,男性创造的价值要高于女性。况且女性要承担相当繁重的生育与养育的任务,所以主导了生产力的男性,也就主导了社会的结构。 古代中国的男女关系又有自己的特点,现在主流的学者喜欢叫民国之前的中国“封建中国”,但是很奇怪,中国从秦朝以来就已经废除了封建制度,改为郡县制。那为什么我们叫古代中国封建呢?我认为封建的是国家的cell——家庭。 我们中国人十分重视宗族,孰为宗,孰为祖,素以为重。在一个家庭中,最年长的家长一般占到绝对的话语权——即使她不掌握最多的话语权。什么意思呢?就像红楼梦中的贾府,贾府中表面上的话事人应该是贾赦,而实际上贾政却拥有更多的权威,而贾府的大小事物却由王熙凤统管,而贾府中言语最有重量最不容反驳的却是贾母。古代中国的家庭地位就是这样:**嫡长子优先,能者为大,长者为重**。嫡长子虽然是理论上的话事人,但是有能力的人通常说话会更有分量,而一家中最年长的人通常说的话是最重的。 这样就形成了一种层层剥削的封建制度。所以封建制度对人的压迫不限于男女,只是**同阶层的男女,女性的地位通常比男性低一层**。这一层的差距是血淋淋真是存在的,但却并非不可消弭。 我们印象中古代有权势的男性都是三妻四妾,花天酒地。而现实是,按照传统,古人是坚定的一夫一妻制的支持者。林语堂认为,读书人、传统世家大族的后代通常都是坚持一夫一妻的。嫁到这种家庭的女性,通常拥有较高的家庭地位。可能在明面上只负责相夫教子,而关起门来就能当起一家之主。不过古人的一夫一妻制有些特殊,它应该被描述成,一夫n妾(n>=0)。古人纳妾,通常需要征得妻子的同意,甚至有些女性为了减轻自己打理家庭、生养孩子的压力会主动让丈夫纳妾。妻与妾的地位有如云泥,发妻决不能被轻易抛弃,更不允许被其他人欺侮,不然男子会受到长年的羞辱与非议;而妾则是可以随意赠送的东西。**这对女性不公吗?当然不公,极其不公,但是要知道,在那个时代,妾并不是唯一可以被随意赠送与抛弃的东西**。 说了这么多,只是想说明一点:在古代,<u>同阶级</u>的男女组成家庭,家庭内部是有消弭男尊女卑的可能的。 而说明这一点,只是想让大家认识到,古代的男女不平等,并非如大众所想,更多的存在于家庭之间。**真正体现重男轻女的是二者的社会地位,而非家庭地位**。 古代的社会对女性的发展的可能性的限制是十分严重的。比如女子无法参加科举考试,鲜能从政,不可从军,更枉谈成为军官,不可参与宗族的祭奠、没有孩子的冠姓权,甚至被认为是没有传宗接代的能力……**除了对女性地位的限制,对女性人格的限制也同样甚至更为严重。女性被认为是弱势群体,甚至被强制变为弱势群体**。最著名的是在宋末之后,裹脚的习俗在女性之间流行,这种行为是及其血腥与野蛮的,是优势群体对劣势群体的强制臆想与人格压迫。 不过我们同时需要说明一点,**当女性被打压成为弱势群体,失去一定的权利的同时,她们有被赋予了一些新的权利**,比如不需要被强制征兵、不需要强制服徭役、可以较少的从事劳动生产等……同时,男女对立中很重要的一种东西——彩礼,这在这种背景下出现了。关于彩礼,不管是给到女方还是给到女方的父母,或是为了从女方手中“买下”闺女,或是给女方作为生活的保障。不管系何种情况,女方的父母都有义务给予自己的闺女一些嫁妆来保证自己的闺女在过门之后的生活。 我个人认为,**这种在权利被剥夺时被赋予的权利,应当具有原子性**。与其在彩礼的问题上挣个高下,不如保障女性在性与生育方面的决定权,当这个社会把被剥夺的权利返还给女性时,那么女性被额外赋予的权利也应该并且应当被女性自然的放弃。 不过历史中相当有趣的一点是,我们观察各个朝代的社会状况时,有时会觉得社会在倒退。比如木兰诗,是北朝民歌,那时巾帼英雄还被津津乐道的传颂;到了唐朝,女性依然有骑马打猎种地劳作的权利;到了宋朝,还有许多女性的文学方面展露头角,引领风骚。**这一情况的改变是在宋末,至于明朝,对女性的压迫就已到达极致……而清承明制,现代中国又一定情况承清制。** 在改革开放的初期,我们一度看到许多乱象。我们必须承认,重男轻女、男尊女卑的观念一直存在流传到如今。随着中国经济与产业的发展,女性的社会地位逐渐提高,那套从明清继承而来的封建糟粕已经不适于当今社会了。这便是这节标题说的物是人非——**因为历史的惯性,男女不平等的现象一定程度存在,而女性已经不是之前的女性了**,她们有着自己独立的思想,有着自己独立的经济能力。社会经济的结构变革,社会制度也必须要变革,**当女性不再在经济上附庸男性时,男女的平等便势在必行,而这一变革已经取得相当大的成效了,并且应当持续推进**。 不过坏消息是,这种变革的推进似乎遇到了阻碍:**当社会经济发展停滞时,这种变革就一定程度上会受到阻碍**。要完全把男尊女卑、男强女弱的思维从中国——尤其是中国的广大农村——消除看来还有一段路要走。 **而单单依靠的经济的力量是不稳定,我们同样需要法律与制度的力量**。 ## 法律,主观善意的错误 我们现在使用的刑法是在1980的颁布,在那个时代,女性的社会地位还不如如今这样平等,大量胚胎鉴定、拐卖妇女、嫖娼强暴的事情发生,所以那一版刑法有专门注意这一点并且制定了许多保护妇女儿童权益的法条。 但是随着思想的开放,有人意识到**对弱者的特别保护本身也会引发对弱者的歧视**。比如刑法的如下条款: > 第二百三十六条 以暴力、胁迫或者其他手段强奸**妇女**的,处三年以上十年以下有期徒刑。 > > 第二百三十七条  以暴力、胁迫或者其他方法强制猥亵他人或者侮辱**妇女**的,处五年以下有期徒刑或者拘役。 > > 第二百四十条  拐卖**妇女、儿童**的,处五年以上十年以下有期徒刑,并处罚金;有下列情形之一的,处十年以上有期徒刑或者无期徒刑,并处罚金或者没收财产;情节特别严重的,处死刑,并处没收财产 > > ——《中华人民共和国刑法》 这些法条的出发点是好的,也是正确的,但却并不完美。除了上文所说的原因之外,它在一定程度上忽视了男性也有可能遭遇强奸、强制侮辱与拐卖。 这些法令存在着一种性别上的刻板印象,并且将少数人的情况置于不顾。所以**当我们将其中的弱势群体改为全体大众时,不仅能够部分消除人们对于弱势群体的歧视、进一步促进男女平等,保护更多人的权利,而且也没有违背这些法律的本性,没有减少对弱者的保护**,何乐而不为呢? 如果排除立法者因为修改法条太麻烦,因为他们太忙而没时间去落实的原因,那就可能是当今的立法者并不认为男女的对立是一种严重的社会现象,并没有改变对两性的刻板印象。当然,我们无须对此苛责。法条的修改本身就是一件严肃的事情,不可一蹴而就。 不过我们的法律在一定程度上更能体现肉食者的意图,他们又是如何在高高在上的位置看待男女对立的现象呢?**也许,他们并不在乎**。 ## 政治,错失机会的手段 上一节我说他们并不在乎,并非空穴来风妄加揣测。中国是一个很大的国家,内政外交事物繁多,像男女对立这样的问题,一非政治危机,二非社会危机,三非外交危机,很难让人重视。况且以他们的年龄,这样的事情在他们看来不过是年轻男女的气盛之缘。毕竟在他们这个年代,男女小的时候也是对立仇视的,而到了青春期之后,却都互生情愫、求之若渴。他们无视了这个问题,以他们的角度,实在是正常的。 而在我的角度看,在几年前,我们完全能把这种现象扼杀在萌芽中,而且方法还有很多。这里有上中下三策: **上策,加快产业转型,降低房价,提高城镇化水平,完善医疗体系,保证社会福利,进一步开放国门,加强国际合作、加强科技投入、改善教育制度,赚更多的钱。** **中策,以法律的形式保证男女同工同酬、强迫男性与女性休同样时间的产假、承认LGBT群体的利益、强制取缔彩礼、保证女性的生育权、大力增加生育的补贴,保证男女的平等。** **下策,督促各个社交平台删除性别对立的言论,对部分以此为话题的博主进行封杀。** 这三个对策应该都是有效的,只不过**上策伤筋动骨,中策伤及皮肉,下策易于饮水**。不过,目前并没有专门针对性别对立这个现象做什么实质性的动作。不但如此,还有一些政策进一步加剧了男女之间的对立。 试问,中国目前最被重视的问题是什么? 中美关系?中台关系?中日关系?新冠疫情?芯片短期?经济复苏?生育率低?其实这些都算不上最主要的问题,**最主要的问题是,房地产**。房地产是中国的立国之本,是政府收入的主要来源,是大多数家庭的主要财富。所以一定要保证房地产的价格不跌。那是什么决定商品的价格呢?剖除政府的控制不谈,那供需关系一定是最主要的因素。当一件商品因为价格过高在市场上的流通率降低时,那它一定会逐渐降价。但是房地产并不是普通的商品,他还是理财产品——当然,对于大多数人来说,房子可能只是寓所。 **但是对于一些人来说房价就是财富,所以房价是万万不能跌的**。如果房价不跌,那么购房的人就会减少,如此房产的积压就会使很多房地产公司亏损、资金链断裂、无法偿还债务,以至破产。这对整个政府来说都是难以接受的损失。 所以政府要做的第一件事,就是保证房地产市场能够正常运转,保持房市的正常运转就是保证政府的财政,就是保护房企,就是保护已经购房者的利益,**而房地产正常运转的方案只能是保证更多的人购房**。所以这种博弈呢,至少要一个人受损失,要么是政府,要么是房企,要么是已经是购房者,要么是未购房者。**前面三种势力的能量都是极大的,所以必须要让还没有买房的人承受损失**。那么如何让这种剥削能够持续呢?回忆我们古代的家庭是如何为续的呢?莫不是长辈剥削晚辈,这样往下,只要子孙不绝,剥削便不停止。 **只要大家能够源源不断的结婚生子,那么新生儿就需要买房——毕竟现在婚房已经快要成为结婚的决定条件了——这样,这个游戏就能维持下去了**。所以为了房产,政府必须不断的促进人民结婚生育,只有这样,房地产的游戏才能继续。而青年男女结婚的压力却又如此巨大,房、车、生育,一座座大山压在他们的头上。 结婚,有山大的压力,而不结婚呢?父母的催婚,政府的催婚,单位的催婚……纷至沓来的又是各种压力。又是这样的进退维谷,毫无办法。真是,结婚,青年苦,不婚,青年苦。**如此的矛盾与高压,在不能冲击触及其他阶层的情况下,青年男女除了相互攻击,还有什么缓解压力的做法**?难道靠做爱吗? ## 女权,莫衷一是的组织 为何在此只说女权不说男权呢?应为上文已经提到过,现在主流的国家曾今都是男性占主导的社会,所以只会有女权组织,不会有男权组织。不过我在此提到这个组织并没有任何的不敬,我只是想要说明一个事实:**在中国,从来没出现过一个团结的、目标一致的、大规模的、具有里程碑意义的女权组织**。 而这其中最重要的是,没有一个一致的目标。而任何一次运动,没有统一的纲领很难成功的。而在中国,**想要为女权运动定一个统一的纲领是相当困难的。** 女权运动想要什么?男女平等?这个纲领太笼统了,他们会说,新中国成立以来,男女就已经平等了。选举权?在中国,没有选举权的可不只是女性。同工同酬?这个似乎在中国并不是一个很大的问题。取消彩礼,取消对弱势群体的特殊保护?这可能是大多数女性不想看到的。 实际上,**在中国,男女平等在社会层面实际上已经做的相当好了,问题出在个人身上,出现在思想上**。有许多人在思想上觉得男女不平等,并且以自己个人的身份作出重男轻女或者重女轻男的事情。对于这种人,我们必然不能将他们统统判为死刑,这似乎有些太不人道了。由于历史有一定的惯性,随着社会发展,这种情况一定会逐渐消失,我们需要做的只是等待。 对于现在,社会运动可能对个人的思想不起作用,我们能做的只有坚持自己男女平等的思想,然后原理那些用重男轻女或重女轻男的思想压迫我们的人。 这就要求我们自己有能够独立生活的能力,**能够做到经济独立,才能最低限度的让自己实现男女的平等**。所以在中国,女权运动这种方式并不符合我们的国情,而在网络上站队,互相诋毁也不能改变自己的处境。**与其诋毁他人,不如提升自己**。 ## 题外话:田园女权与直男癌 几年前,有两个词语比较流行,一个叫做“田园女权”,一个叫做“直男癌”。 > 田园女权,[网络流行词](https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E6%B5%81%E8%A1%8C%E8%AF%8D/4604547?fromModule=lemma_inlink),“中华田园女权”的简称,指要求[男女平等](https://baike.baidu.com/item/%E7%94%B7%E5%A5%B3%E5%B9%B3%E7%AD%89/2939686?fromModule=lemma_inlink)却要男性承担主要[责任](https://baike.baidu.com/item/%E8%B4%A3%E4%BB%BB/32951?fromModule=lemma_inlink),以女权为借口追求女性收益最大化的群体。 > 直男癌一词源于网友对活在自己的世界观、价值观、[审美观](https://baike.baidu.com/item/%E5%AE%A1%E7%BE%8E%E8%A7%82?fromModule=lemma_inlink)里,时时向别人流露出对对方的不顺眼及不满,并略带[大男子主义](https://baike.baidu.com/item/%E5%A4%A7%E7%94%B7%E5%AD%90%E4%B8%BB%E4%B9%89?fromModule=lemma_inlink)的人的一种蔑称或调侃。 当时性别对立还不强烈,大部分人对于田园女权和直男癌都是持批评态度的。 但是如今这两个词几乎已经消失在网络历史的黄沙当中了。究其原因,无非是现在的网友已经完成了女权向田园女权、直男向直男癌的退化了。当他们成为主流,也就无须背负他们的前缀与后缀了。这样的结果是令人遗憾的。 笔者所在的地方依然有着一定程度的男女不平等现象,包括但不限于女性不能在男性喝酒时上桌、不能参与祭祀等,我也曾汲汲与男女的平权,不曾想时过境迁,风向的变化居然能如此之快。 支持男女平等的,从来不只是女性,青年的男性也深受男女不平等之苦。青年的男女应该是最团结的盟友,而他们的敌人永远是冰冷的现实。当青年男女开始相互攻击的时候,他们只能多了一个敌人,而少了一个朋友。 几乎所有正常的人,都能看到当下这种处处对立的环境的低劣之处,但人们并没有选择去结束或遏制这种糟糕的舆论环境。 曾认为这种处处的对立,不过是在网络中才会出现的现象。但当它开始侵蚀我们的现实时,我们还有能力去改变现状吗? 而这种侵蚀,却早已开始了。

SSM & SpringBoot 138问138答

# SSM 73问 ## mybaits 21问 ### Mybaits与Hibernate的异同? Hibernate与MyBatis都是ORM框架,都有相应的代码生成工具,可以生成简单基本的DAO层方法。 Mybaits是半ORM框架,Hibernate是全ORM框架 Mybaits需要手动写SQL语句,Hibernate不需要 ### 什么是ORM? **对象关系映射**(英语:**Object Relational Mapping**,简称**ORM**,或**O/RM**,或**O/R mapping**),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。 目的是**使用面向对象的方法操纵数据库** ### mybatis怎么配置环境信息? ```xml <environments default="development"> <!-- default:默认的环境 ID--> <environment id="development"> <!-- 事务管理器的配置 如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器 - JDBC – 这个配置就是直接使用了 JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务范围。 - MANAGED – 这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期 --> <transactionManager type="JDBC"> <property name="..." value="..."/> </transactionManager> <!-- 数据源的配置--> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> ``` ### mybaits的setting有什么作用? 这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。下表描述了设置中各项的意图、默认值等。 | 设置参数 | 描述 | 有效值 | 默认值 | |------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------| | cacheEnabled | 该配置影响的所有映射器中配置的缓存的全局开关。 | true,false | true | | lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置`fetchType`属性来覆盖该项的开关状态。 | true,false | false | | aggressiveLazyLoading | 当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载。 | true,false | false (在 3.4.1 及之前的版本中默认为 true) | | multipleResultSetsEnabled | 是否允许单一语句返回多结果集(需要兼容驱动)。 | true,false | true | | useColumnLabel | 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。 | true,false | true | | useGeneratedKeys | 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 | true,false | False | | autoMappingBehavior | 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 | NONE, PARTIAL, FULL | PARTIAL | | defaultExecutorType | 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE | | defaultStatementTimeout | 设置超时时间,它决定驱动等待数据库响应的秒数。 | Any positive integer | Not Set (null) | | safeRowBoundsEnabled | 允许在嵌套语句中使用分页(RowBounds)。 | true,false | False | | mapUnderscoreToCamelCase | 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 | true, false | False | | localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 | SESSION,STATEMENT | SESSION | | jdbcTypeForNull | 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER | | lazyLoadTriggerMethods | 指定哪个对象的方法触发一次延迟加载。 | A method name list separated by commas | equals,clone,hashCode,toString | | defaultScriptingLanguage | 指定动态 SQL 生成的默认语言。 | A type alias or fully qualified class name. | org.<br/>apache<br/>.ibatis.<br/>scripting.<br/>xmltags.<br/>XMLDynamicLanguageDriver | | callSettersOnNulls | 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。注意基本类型(int、boolean等)是不能设置成 null 的。 | true,false | false | | logPrefix | 指定 MyBatis 增加到日志名称的前缀。 | Any String | Not set | | logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J<br/>,LOG4J<br/>,LOG4J2,<br/>JDK_LOGGING,<br/>COMMONS_LOGGING,<br/>STDOUT_LOGGING,<br/>NO_LOGGING | Not set | | proxyFactory<br><br> | 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具。 | CGLIB JAVASSIST | CGLIB<br><br> | | vfslmpl<br><br> | 指定 VFS 的实现 | 自定义 VFS 的实现的类全限定名,以逗号分隔。 | no set<br><br> | | useActualParamName<br><br> | 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 `-parameters` 选项。(新增于 3.4.1) | true \\ | false | | configurationFactory<br><br> | 指定一个提供 `Configuration` 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为`static Configuration getConfiguration()` 的方法。(新增于 3.2.3) | 类型别名或者全类名. | no set | 一个配置完整的 settings 元素的示例如下: ```xml <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="multipleResultSetsEnabled" value="true"/> <setting name="useColumnLabel" value="true"/> <setting name="useGeneratedKeys" value="false"/> <setting name="autoMappingBehavior" value="PARTIAL"/> <setting name="defaultExecutorType" value="SIMPLE"/> <setting name="defaultStatementTimeout" value="25"/> <setting name="safeRowBoundsEnabled" value="false"/> <setting name="mapUnderscoreToCamelCase" value="false"/> <setting name="localCacheScope" value="SESSION"/> <setting name="jdbcTypeForNull" value="OTHER"/> <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> </settings> ``` ### 如何配置类型别名? 类型别名是为 Java 类型设置一个短的名字,存在的意义仅在于用来减少类完全限定名的冗余。例如: ```xml <typeAliases> <typeAlias alias="Author" type="domain.blog.Author"/> <typeAlias alias="Blog" type="domain.blog.Blog"/> <typeAlias alias="Comment" type="domain.blog.Comment"/> <typeAlias alias="Post" type="domain.blog.Post"/> <typeAlias alias="Section" type="domain.blog.Section"/> <typeAlias alias="Tag" type="domain.blog.Tag"/> </typeAliases> ``` 当这样配置时,`Blog`可以用在任何使用`domain.blog.Blog`的地方。 也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如: ```xml <typeAliases> <package name="domain.blog"/> </typeAliases> ``` 每一个在包 `domain.blog` 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 `domain.blog.Author` 的别名为 `author`;若有注解,则别名为其注解值。看下面的例子: ```java @Alias("author") public class Author { ... } ``` ### 三种数据源类型的区别? **UNPOOLED**– 这个数据源的实现只是每次被请求时打开和关闭连接。虽然一点慢,它对在及时可用连接方面没有性能要求的简单应用程序是一个很好的选择。 **POOLED**– 这种数据源的实现利用"池"的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这是一种使得并发 Web 应用快速响应请求的流行处理方式。 **JNDI**– 这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。 ### 如何指定映射文件? 最佳的方式是告诉 MyBatis 到哪里去找映射文件。你可以使用相对于类路径的资源引用, 或完全限定资源定位符(包括 `file:///` 的 URL),或类名和包名等。例如: ```xml <!-- 使用相对类路径定义资源 --> <mappers> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> </mappers> <!-- 使用完全限定资源定位符 --> <mappers> <mapper url="file:///var/mappers/AuthorMapper.xml"/> </mappers> <!-- 使用包名+类名 --> <mappers> <mapper class="org.mybatis.builder.AuthorMapper"/> </mappers> <!-- 注册包内所有mapper --> <mappers> <package name="org.mybatis.builder"/> </mappers> ``` ### select与属性的用法? 简单查询的 select 元素是非常简单的。比如: ```xml <select id="selectPerson" parameterType="int" resultType="hashmap"> SELECT * FROM PERSON WHERE ID = #{id} </select> <!-- id:查询的名称 parameterType:参数类型 resultType:返回值类型 --> ``` select 元素有很多属性允许你配置,来决定每条语句的作用细节。 ```xml <select id="selectPerson" parameterType="int" parameterMap="deprecated" resultType="hashmap" resultMap="personResultMap" flushCache="false" useCache="true" timeout="10000" fetchSize="256" statementType="PREPARED" resultSetType="FORWARD_ONLY"> ``` 属性的含义: | 属性 | 描述 | |---------------|------------------------------------------------------------------------------------------------------------------------------| | id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 | | parameterType | 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。 | | resultType | 从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用 resultType 或 resultMap,但不能同时使用。 | | resultMap | 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用 resultMap 或 resultType,但不能同时使用。 | | flushCache | 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。 | | useCache | 将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true。 | | timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。 | | fetchSize | 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)。 | | statementType | STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 | | resultSetType | FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)。 | | databaseId | 如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。 | | resultOrdered | 这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。 | | resultSets | 这个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称是逗号分隔的。 | ### insert、update、delete的用法? 数据变更语句 insert,update 和 delete 的实现非常接近: ```xml <insert id="insertAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" keyProperty="" keyColumn="" useGeneratedKeys="" timeout="20"> <update id="updateAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" timeout="20"> <delete id="deleteAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" timeout="20"> ``` Insert, Update 和 Delete 的属性 | 属性 | 描述 | |------------------|---------------------------------------------------------------------------------------------------------------------------------------------| | id | 命名空间中的唯一标识符,可被用来代表这条语句。 | | parameterType | 将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。 | | flushCache | 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true(对应插入、更新和删除语句)。 | | timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。 | | statementType | STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 | | useGeneratedKeys | (仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。 | | keyProperty | (仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 | | keyColumn | (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 | | databaseId | 如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。 | 下面就是 insert,update 和 delete 语句的示例: ```xml <insert id="insertAuthor"> insert into Author (id,username,password,email,bio) values (#{id},#{username},#{password},#{email},#{bio}) </insert> <update id="updateAuthor"> update Author set username = #{username}, password = #{password}, email = #{email}, bio = #{bio} where id = #{id} </update> <delete id="deleteAuthor"> delete from Author where id = #{id} </delete> ``` ### 两种字符串替换的区别? | #{} | ${} | |--------------------------------|--------------------------------| | 参数占位符,即预编译 | 字符串替换符,即SQL拼接 | | 很大程度上能防止sql 注入 | 不能防止sql 注入 | | 将传入的数据都当成一个字符串,会对传入的变量自动加一个单引号 | 将传入的参数直接显示生成在sql中,且不加任何引号 | | | 排序时使用order by 动态参数时需要注意,用$而不是# | ### 三种自动映射等级? ```xml <setting name="autoMappingBehavior" value="PARTIAL"/> ``` NONE 表示取消自动映射 PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集 FULL 会自动映射任意复杂的结果集(无论是否嵌套) ### if标签的作用? ```xml <select id="findActiveBlogWithTitleLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> <!-- 如果test为true则语句包含if内的内容--> AND title like #{title} </if> </select> ``` ### foreach标签的用法? ```xml <select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </select> <!-- foreach 允许你指定一个集合,声明可以用在元素体内的集合项和索引变量--> <!-- 也允许你指定开闭匹配的字符串以及在迭代中间放置分隔符。--> ``` ### 怎么配置日志级别? ```properties # 全局日志等级 log4j.rootLogger=ERROR, stdout # mabatis mapper日志等级 log4j.logger.org.mybatis.example.BlogMapper=TRACE ``` ### 一级缓存和二级缓存的定义与区别? - 一级缓存 - 定义 - 一级缓存作用域是sqlsession级别的,同一个sqlsession中执行相同的sql查询(相同的sql和参数),第一次会去查询数据库并写到缓存中,第二次从一级缓存中取。 - 一级缓存是基于 PerpetualCache 的 HashMap 本地缓存,默认打开一级缓存。 - 清空一级缓存 - 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 - 一级缓存时执行commit,close,增删改等操作,就会清空当前的一级缓存;当对SqlSession执行更新操作(update、delete、insert)后并执行commit时,不仅清空其自身的一级缓存(执行更新操作的效果),也清空二级缓存(执行commit()的效果)。 - 一级缓存无过期时间,只有生命周期 - 二级缓存 - 简介 - 它指的是Mybatis中SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。 - 二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。 、 - 何时存入 - 在关闭sqlsession后(close),才会把该sqlsession一级缓存中的数据添加到namespace的二级缓存中。 - 开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中。 - 二级缓存有过期时间,但没有后台线程进行检测 - 需要注意的是,并不是key-value的过期时间,而是这个cache的过期时间,是flushInterval,意味着整个清空缓存cache,所以不需要后台线程去定时检测。 - 每当存取数据的时候,都要检测一下cache的生命时间,默认是1小时,如果这个cache存活了一个小时,那么将整个清空一下。 - 当 Mybatis 调用 Dao 层查询数据库时,先查询二级缓存,二级缓存中无对应数据,再去查询一级缓存,一级缓存中也没有,最后去数据库查找。 ### useCache与flushCache的作用? ```xml <select flushCache="false" useCache="true" > <!--useCache:将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true。--> <!--flushCache:将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。--> ``` ### Redis的概念? Redis 是一个高性能的key-value数据库。经常用作缓存 ### 那个注解是对dao组件的修饰? ```java @Mapper //Mybatis 需要找到对应的 mapper,在编译的时候动态生成代理类 @Repository //用于标注数据访问组件,即DAO组件 ``` ### JavaModelGenerator配置、sqlMapperGenerator配置与JavaClientGenerator配置的作用? - JavaModelGenerator配置生成的实体类的存放的位置(entity层的java文件) - sqlMapperGenerator配置生成的映射文件的位置(resources/dao中的xml文件) - JavaClientGenerator配置生成的mapper接口文件的位置(dao层中的java文件) ### 如何实现分页? 导入依赖包 ```xml <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.3</version> </dependency> ``` 配置properties ```properties #pagehelper配置 pagehelper.helper-dialect=mysql pagehelper.reasonable=true pagehelper.support-methods-arguments=true pagehelper.params=count=countSql ``` 使用 ```java @Transactional(readOnly = true) @Override public PageInfo<Student> getStudentListByBatchName(String batchName, Integer pageNo) { //spring boot程序中,添加了PageHelper的启动依赖后,直接调用对应方法分页查询即可 PageHelper.startPage(pageNo, 5); List<Student> list = studentMapper.getListByBatchName(batchName); //传入查询的列表,创建一个PageInfo对象,用于包含分页的所有信息 //还可以传入第二个参数,表示导航页码的数量 PageInfo<Student> pageInfo=new PageInfo<>(list,5); return pageInfo; } ``` ### 分页最后封装成什么数据? `PageInfo<T>` ## spring Framework 40问 ### 依赖注入的概念? Spring 通过 IoC 容器来管理所有 Java 对象的实例化和初始化,控制对象与对象之间的依赖关系. 控制反转核心思想就是由 Spring 负责对象的创建。DI是IOC的一种。在对象创建过程中,Spring 会自动根据依赖关系,将它依赖的对象注入到当前对象中,这就是所谓的“依赖注入”。 ### springCoreContainer是什么? ![img.png](./ssm/core_container.png) - Core:核心工具包,包括字节码操作cglib、asm,资源的抽象Resource,对象实例化化工具等等。 - Beans:Bean 的定义、Bean 的创建以及对 Bean 的解析。 - Context:Context模块建立在Core和Beans模块之上,是Bean运行环境(即保存维护Bean的状态、数据,Bean之间的关系),又称之为Ioc容器。 - SpEL:提供了一个强大的表达式语言,可以在运行时查询和操作对象。 ### springIoC容器的类型? #### spring BeanFactory 容器 - 是Spring bean容器的根接口,提供获取bean,是否包含bean,是否单例与原型,获取bean类型,bean 别名的方法 。 - BeanFactory不支持国际化功能 - 不支持事件机制 - 没有扩展ResourceLoader,只能加载一个Resource - BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化 - BeanFactory需要手动注册 #### Spring ApplicationContext 容器 - ApplicationContext继承了BeanFactory - 扩展了MessageResource接口,因而具有消息处理的能力(i18N) - 通过ApplicationEvent和ApplicationListener这两个接口实现事件机制 - 扩展了ResourceLoader(资源加载器)接口,从而可以用来加载多个Resource - 在容器启动时,一次性创建了所有的Bean - 而ApplicationContext则是自动注册 ### ApplicationContext容器的实现有哪些? ApplicationContext 有两个直接子接口:WebApplicationContext 和 ConfigurableApplicationContext。 ConfigurableApplicationContext:扩展于ApplicationContext, 新增加两个主要方法。refresh()和close(),让ApplicationContext具有启动、刷新和关闭上下文的能力。ApplicationContext在初始化上下文时就实例化所有的单例Bean. WebApplicationContext:WebApplicationContext是专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作。 最常用的两个实现类: ClassPathXmlApplicationContext : 从类路径下加载配置文件。 FileSystemXmlApplicationContext : 从文件系统中加载配置文件。 两个都是继承ConfigurableApplicationContext ### bean标记的使用? ```xml <bean id="Bean 唯一标志符" class="包名+类名" p:普通属性="普通属性值" p:对象属性-ref="对象的引用"> ``` Spring 框架提供了 2 种短命名空间,可以简化 Spring 的 XML 配置,如下表。 | 短命名空间 | 简化的 XML 配置 | 说明 | |--------|--------------------------------|--------------------------| | p 命名空间 | <bean> 元素中嵌套的 <property> 元素 | 是 setter 方式属性注入的一种快捷实现方式 | | c 命名空间 | <bean> 元素中嵌套的 <constructor> 元素 | 是构造函数属性注入的一种快捷实现方式 | ### bean的范围有哪些? Spring 5 共提供了 6 种 scope 作用域,如下表。 | 作用范围 | 描述 | |-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | singleton | 默认值,单例模式,表示在 Spring 容器中只有一个 Bean 实例 | | prototype | 原型模式,表示每次通过 Spring 容器获取 Bean 时,容器都会创建一个新的 Bean 实例。 | | request | 每次 HTTP 请求,容器都会创建一个 Bean 实例。该作用域只在当前 HTTP Request 内有效。 | | session | 同一个 HTTP Session 共享一个 Bean 实例,不同的 Session 使用不同的 Bean 实例。该作用域仅在当前 HTTP Session 内有效。 | | application | 同一个 Web 应用共享一个 Bean 实例,该作用域在当前 ServletContext 内有效。 与 singleton 类似,但 singleton 表示每个 IoC 容器中仅有一个 Bean 实例,而一个 Web 应用中可能会存在多个 IoC 容器,但一个 Web 应用只会有一个 ServletContext,也可以说 application 才是 Web 应用中货真价实的单例模式。 | | websocket | websocket 的作用域是 WebSocket ,即在整个 WebSocket 中有效。 | ### 如何获取bean? 1. 在初始化时保存ApplicationContext对象 2. 通过Spring提供的utils类获取ApplicationContext对象 3. 继承自抽象类ApplicationObjectSupport 4. 继承自抽象类WebApplicationObjectSupport 5. 实现接口ApplicationContextAware 6. 通过Spring提供的ContextLoader ### bean的生命周期和生命周期方法? 在传统的 Java 应用中,Bean 的生命周期很简单,使用 Java 关键字 new 进行 Bean 的实例化后,这个 Bean 就可以使用了。一旦这个 Bean 长期不被使用,Java 自动进行垃圾回收。 相比之下,Spring 中 Bean 的生命周期较复杂,大致可以分为以下 5 个阶段: 1. Bean 的实例化 2. Bean 属性赋值 3. Bean 的初始化 4. Bean 的使用 5. Bean 的销毁 Spring 根据 Bean 的作用域来选择 Bean 的管理方式, - 对于 singleton 作用域的 Bean 来说,Spring IoC 容器能够精确地控制 Bean 何时被创建、何时初始化完成以及何时被销毁; - 对于 prototype 作用域的 Bean 来说,Spring IoC 容器只负责创建,然后就将 Bean 的实例交给客户端代码管理,Spring IoC 容器将不再跟踪其生命周期。 ![Spring 生命周期流程](./ssm/1F32KG1-0.png) Bean 的生命周期回调方法主要有两种: - 初始化回调方法:在 Spring Bean 被初始化后调用,执行一些自定义的回调操作。 - 销毁回调方法:在 Spring Bean 被销毁前调用,执行一些自定义的回调操作。 **通过接口实现** ```java //in bean class @Override public void afterPropertiesSet() throws Exception { System.out .println("【InitializingBean接口】调用InitializingBean.afterPropertiesSet()"); } // 这是DiposibleBean接口方法 @Override public void destroy() throws Exception { System.out.println("【DiposibleBean接口】调用DiposibleBean.destory()"); } ``` **通过XML配置实现** ```java //in bean class public void myInit() { System.out.println("【init-method】调用<bean>的init-method属性指定的初始化方法"); } public void myDestory() { System.out.println("【destroy-method】调用<bean>的destroy-method属性指定的初始化方法"); } ``` ```xml <bean id="person" class="springBeanTest.Person" init-method="myInit" destroy-method="myDestory" scope="singleton" p:name="张三" p:address="广州" p:phone="15900000000" /> ``` **通过注解实现** | 注解 | 描述 | | -------------- | --------------------------------------------------- | | @PostConstruct | 指定初始化回调方法,这个方法会在 Spring Bean 被初始化后被调用,执行一些自定义的回调操作。 | | @PreDestroy | 指定销毁回调方法,这个方法会在 Spring Bean 被销毁前被调用,执行一些自定义的回调操作。 | **通过后置处理器实现** ```java public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; } ``` ### 两种显式装配? **构造函数注入**</b> 使用构造函数实现属性注入大致步骤如下: 1. 在 Bean 中添加一个有参构造函数,构造函数内的每一个参数代表一个需要注入的属性; 2. 在 Spring 的 XML 配置文件中,通过 `<beans>` 及其子元素 `<bean>` 对 Bean 进行定义; 3. 在 `<bean>` 元素内使用 `<constructor-arg>`元素,对构造函数内的属性进行赋值,Bean 的构造函数内有多少参数,就需要使用多少个 `<constructor-arg>` 元素。 <b>**setter注入**</b> 使用 setter 注入的方式进行属性注入,大致步骤如下: 1. 在 Bean 中提供一个默认的无参构造函数(在没有其他带参构造函数的情况下,可省略),并为所有需要注入的属性提供一个 setXxx() 方法; 2. 在 Spring 的 XML 配置文件中,使用 `<beans>` 及其子元素 `<bean>`对 Bean 进行定义; 3. 在 `<bean>`元素内使用 `<property>` 元素对各个属性进行赋值。 ### value和ref的区别? value用于注入字面量属性,ref用于注入引用属性 ### 如何注入集合? 标签说明<list>用于注入 list 类型的值,允许重复<set>用于注入 set 类型的值,不允许重复<map>用于注入 key-value 的集合,其中 key 和 value 都可以是任意类型<props>用于注入 key-value 的集合,其中 key 和 value 都是字符串类型 | 标签 | 说明 | |-----------|--------------------------------------------| | `<list>` | 用于注入 list 类型的值,允许重复 | | `<set>` | 用于注入 set 类型的值,不允许重复 | | `<map>` | 用于注入 key-value 的集合,其中 key 和 value 都可以是任意类型 | | `<props>` | 用于注入 key-value 的集合,其中 key 和 value 都是字符串类型 | ### 构造函数注入? ```xml <bean id="bean名" class="类"> <constructor-arg name="属性名" value="值"/> <constructor-arg name="属性名" ref="引用"/> </bean> ``` 或使用c:命名空间 ### parent属性的作用? 在 Spring XML 配置中,我们通过子 Bean 的 parent 属性来指定需要继承的父 Bean,配置格式如下。 ```XML <!--父Bean--> <bean id="parentBean" class="xxx.xxxx.xxx.ParentBean" > <property name="xxx" value="xxx"/> <property name="xxx" value="xxx"/> </bean> <!--子Bean--> <bean id="childBean" class="xxx.xxx.xxx.ChildBean" parent="parentBean"/> ``` ### 如何使用工厂方法实现DI? ```java public class AnimalFactory { public static Animal getAnimal1() { System.out.println("调用AnimalFactory类的静态工厂方法getAnimal1()................."); return new Dog(); } public Animal getAnimal2() { System.out.println("调用AnimalFactory类的实例工厂方法getAnimal2()................."); return new Pig(); } private static Map<Integer,Animal> map; static { map=new HashMap<>(); map.put(1,new Dog()); map.put(2,new Pig()); map.put(3,new Sheep()); } public static Animal getAnimal3(int type) { System.out.println("调用AnimalFactory类的静态工厂方法getAnimal3()................."); return map.get(type); } } ``` 使用静态工厂方法返回自身实例 ```xml <!-- 如果想通过一个工厂类的静态工厂方法创建bean 可通过factory-method属性指定创建实例的方法的名称 class指定工厂类的类型 --> <bean class="com.qdu.bean.A" factory-method="getA" /> ``` 使用静态工厂方法返回其他类实例 ```xml <!-- 如果希望调用A类的静态方法来获取B类的实例 --> <!-- 这种情况下,class指定的应该是工厂方法所在的类的名称 --> <!-- factory-method属性指定创建实例的方法的名称 --> <!-- 实际创建的对象不是AnimalFactory类型,而是Dog类型 --> <bean id="animal" class="com.qdu.bean.AnimalFactory" factory-method="getAnimal1" /> <!-- 希望通过AnimalFactory这个工厂类的静态方法getAnimal3(int type) --> <!-- 来创建要托给spring管理的Dog或Pig或Sheep的实例 --> <bean id="animal" class="com.qdu.bean.AnimalFactory" factory-method="getAnimal3"> <!-- constructor-arg在构造函数注入的时候是构造函数参数的名称 --> <!-- 但是对于工厂方法,就是指定一个普通方法参数的信息 --> <constructor-arg name="type" value="2" /> </bean> ``` 使用非静态工厂方法返回其他类实例(不考) ```xml <!-- 如果希望通过工厂类A的非静态方法来实例化B --> <!-- 必须先有一个A的对象才可以 --> <bean id="animalFactory" class="com.qdu.bean.AnimalFactory" /> <!-- 如果希望通过工厂类A的非静态方法来实例化B 这时候不再使用class属性来指定生成的bean的类型 factory-bean属性指定创建bean实例(如这里的Pig)的工厂对象是哪个,指定其id或name factory-method属性指定创建bean实例的工厂方法的名称 id为animal的bean实际是一个Pig实例,也即是getAnimal2()方法返回的实例 --> <bean id="animal" factory-bean="animalFactory" factory-method="getAnimal2" /> ``` 在java中获取bean ```java public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("config/beans1.xml"); A a = ctx.getBean(A.class); a.methodOfA(); } ``` ### 自动装配的模式有哪些? Spring 共提供了 5 中自动装配规则,它们分别与 autowire 属性的 5 个取值对应,具体说明如下表。 | 属性值 | 说明 | |-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| | byName | 按名称自动装配。 Spring 会根据的 Java 类中对象属性的名称,在整个应用的上下文 ApplicationContext(IoC 容器)中查找。若某个 Bean 的 id 或 name 属性值与这个对象属性的名称相同,则获取这个 Bean,并与当前的 Java 类 Bean 建立关联关系。 | | byType | 按类型自动装配。 Spring 会根据 Java 类中的对象属性的类型,在整个应用的上下文 ApplicationContext(IoC 容器)中查找。若某个 Bean 的 class 属性值与这个对象属性的类型相匹配,则获取这个 Bean,并与当前的 Java 类的 Bean 建立关联关系。 | | constructor | 与 byType 模式相似,不同之处在与它应用于构造器参数(依赖项),如果在容器中没有找到与构造器参数类型一致的 Bean,那么将抛出异常。 其实就是根据构造器参数的数据类型,进行 byType 模式的自动装配。 | | default | 表示默认采用上一级元素 <beans> 设置的自动装配规则(default-autowire)进行装配。 | | no | 默认值,表示不使用自动装配,Bean 的依赖关系必须通过 <constructor-arg>和 <property> 元素的 ref 属性来定义。 | **基于注解的自动装配** ```xml <!--开启组件扫描--> <context:component-scan base-package=""/> ``` ```java @Autowired ``` ### 显式装配与自动装配的关系? 显式装配是在 XML 配置中通过 <constructor-arg>和 <property> 中的 ref 属性,手动维护 Bean 与 Bean 之间的依赖关系的。 Spring 的自动装配功能可以让 Spring 容器依据某种规则(自动装配的规则,有五种),为指定的 Bean 从应用的上下文(AppplicationContext 容器)中查找它所依赖的 Bean,并自动建立 Bean 之间的依赖关系。而这一过程是在完全不使用任何 <constructor-arg>和 <property> 元素 ref 属性的情况下进行的。 ### 依赖注入使用哪些注解? Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。 | 注解 | 说明 | |-------------|-------------------------------------------------------------------------------------------------------------| | @Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 | | @Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 | | @Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 | | @Controller | 该注解通常作用在控制层(如 Struts2 的 Action、SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 | 我们可以通过以下注解将定义好 Bean 装配到其它的 Bean 中。 | 注解 | 说明 | |------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | @Autowired | 可以应用到 Bean 的属性变量、setter 方法、非 setter 方法及构造函数等,默认按照 Bean 的类型进行装配。 @Autowired 注解默认按照 Bean 的类型进行装配,默认情况下它要求依赖对象必须存在,如果允许 null 值,可以设置它的 required 属性为 false。如果我们想使用按照名称(byName)来装配,可以结合 @Qualifier 注解一起使用 | | @Resource | 作用与 Autowired 相同,区别在于 @Autowired 默认按照 Bean 类型装配,而 @Resource 默认按照 Bean 的名称进行装配。 @Resource 中有两个重要属性:name 和 type。 Spring 将 name 属性解析为 Bean 的实例名称,type 属性解析为 Bean 的实例类型。如果指定 name 属性,则按实例名称进行装配;如果指定 type 属性,则按 Bean 类型进行装配;如果都不指定,则先按 Bean 实例名称装配,如果不能匹配,则再按照 Bean 类型进行装配;如果都无法匹配,则抛出 NoSuchBeanDefinitionException 异常。 | | @Qualifier | 与 @Autowired 注解配合使用,会将默认的按 Bean 类型装配修改为按 Bean 的实例名称装配,Bean 的实例名称由 @Qualifier 注解的参数指定。 | ### @Configuration和@Bean注解的作用 ```java /* @Configration 注解作用在类、接口(包含注解)上 @Configuration 用于定义配置类,可替换 xml 配置文件 @Configration 注解类中可以声明一个或多个 @Bean 方法 */ @Configuration public class MyConfig { /* @Bean 注解作用在方法上 @Bean 指示一个方法返回一个 Spring 容器管理的 Bean,也就是说方法返回值就是给Springr容器装配的bean @Bean 一般和 @Component 或者 @Configuration 一起使用,也可以在 @Service 里使用,没有特定要求,主要看项目的需求。 @Bean 注解默认作用域为单例 singleton 作用域,可通过 @Scope(“prototype”) 设置为原型作用域 */ @Bean public MyBean myBean() { return new MyBean(); } @Bean public MyBean myBean1() { return new MyBean(); } } ``` ### 什么是AOP? AOP 的全称是“Aspect Oriented Programming”,译为“面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想。 ### 通知的概念? AOP的一套术语 | 名称 | 说明 | |----------------|------------------------------------------------------------------------------------| | Joinpoint(连接点) | AOP 的核心概念,指的是程序执行期间明确定义的一个点,例如方法的调用、类初始化、对象实例化等。 在 Spring 中,连接点则指可以被动态代理拦截目标类的方法。 | | Pointcut(切入点) | 又称切点,指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。 | | Advice(通知) | 指拦截到 Joinpoint 之后要执行的代码,即对切入点增强的内容。 | | Target(目标) | 指代理的目标对象,通常也被称为被通知(advised)对象。 | | Weaving(织入) | 指把增强代码应用到目标对象上,生成代理对象的过程。 | | Proxy(代理) | 指生成的代理对象。 | | Aspect(切面) | 切面是切入点(Pointcut)和通知(Advice)的结合。 | ### 连接点的概念? 见上条 ### 通知的类型(位置)? 共有5种: | 通知 | 说明 | |------------------------|-------------------| | before(前置通知) | 通知方法在目标方法调用之前执行 | | after(后置通知) | 通知方法在目标方法返回或异常后调用 | | after-returning(返回后通知) | 通知方法会在目标方法返回后调用 | | after-throwing(抛出异常通知) | 通知方法会在目标方法抛出异常后调用 | | around(环绕通知) | 通知方法会将目标方法封装起来 | ### 用来创建通知的注解有哪些? | 名称 | 说明 | |-----------------|----------------------------------------------| | @Aspect | 用于定义一个切面。 | | @Pointcut | 用于定义一个切入点。 | | @Before | 用于定义前置通知,相当于 BeforeAdvice。 | | @AfterReturning | 用于定义后置通知,相当于 AfterReturningAdvice。 | | @Around | 用于定义环绕通知,相当于 MethodInterceptor。 | | @AfterThrowing | 用于定义抛出通知,相当于 ThrowAdvice。 | | @After | 用于定义最终通知,不管是否异常,该通知都会执行。 | | @DeclareParents | 用于定义引介通知,相当于 IntroductionInterceptor(不要求掌握)。 | ### 如何声明切入点? ```java //Pointcut表示式 //我除了可以通过切入点表达式(execution)直接对切点进行定义外 //还可以通过切入点方法的名称来引用其他的切入点 //在使用方法名引用其他切入点时,还可以使用“&&”、“||”和“!”等表示“与”、“或”、“非”的含义 @Pointcut("execution(* com.savage.aop.MessageSender.*(..))") //定义为切点的方法,它的返回值类型必须为 void private void log(){} ``` ### 在返回后通知中如何获取目标方法的返回值? ```java @AfterReturning(value = "pt()",returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..."+ret); } ``` ### XML如何配置AOP? 在 Spring 的 XML 配置文件中,添加以下内容启用 @AspectJ 注解支持。 ```xml <!-- 开启注解扫描 --> <context:component-scan base-package=""/> <!--开启AspectJ 自动代理--> <aop:aspectj-autoproxy/> ``` ```xml <!-- 默认是不启用@AspectJ的支持,也就是默认情况下,@Aspect等注解是不起作用的,启用AspectJ支持后才会起作用 --> <!-- 该注解的作用是启用@AspectJ风格的Spring AOP --> <!-- 如果是基于xml schema的aop是否还需要使用该标记: 不用 --> <!-- <aop:aspectj-autoproxy /> --> <!-- 方面对应的类需要注册为spring管理的bean,才能将方面切入到需要的地方 --> <bean id="logAspect1" class="com.qdu.aop.LogAspect1" /> <bean id="logAspect2" class="com.qdu.aop.LogAspect2" /> <bean class="com.qdu.service.impl.StudentServiceImpl" /> <bean class="com.qdu.service.impl.TeacherServiceImpl" /> <bean class="com.qdu.service.impl.MathServiceImpl" /> <!-- execution(* com.qdu.service.StudentService.*(..)) --> <!-- execution(* com.qdu.service.MathService.add(..)) --> <!-- execution(* com.qdu.service.MathService.divide(..)) --> <!-- execution(* com.qdu.service.MathService.multiply(..)) --> <!-- aop:config用于以xml格式配置aop --> <aop:config> <!-- aop:config标记中可以定义切入点,这样的切入点可以在多个aop:aspect标记中使用 --> <aop:pointcut expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" /> <!-- aop:aspect用于配置一个方面对应的类 --> <!-- ref指定方面类bean的id或name --> <!-- order用于控制通知的执行顺序,值越小,该方面类中对应的通知就会先执行 --> <aop:aspect ref="logAspect1" order="2"> <!-- 在aop:aspect标记内定义的切入点只能在该aop:aspect标记中使用 --> <aop:pointcut expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" /> <aop:pointcut expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" /> <aop:pointcut expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" /> <!-- aop:before用于配置前置通知,method指定作为前置通知的方法的名称 --> <!-- pointcut属性用于指定切入点表达式,pointcut-ref用于指定引用的切入点的id --> <aop:before method="before1" pointcut-ref="pt1" /> <aop:before method="before2" pointcut-ref="pt1" /> <!-- aop:after-returning用于配置返回后通知,returning用于指定一个参数名(可随便起,尽量有意义) --> <!-- 这样可以在返回后通知对应的方法上添加一个该名称的参数,用于接收目标方法的返回值 --> <aop:after-returning method="afterReturning" pointcut-ref="pt2" returning="returnValue" /> <!-- aop:after-throwing用于配置抛出后通知,throwing属性指定一个参数名(可随便起,尽量有意义) --> <!-- 这样可以抛出后通知对应的方法上添加一个该名称的参数,用于接收抛出的异常对象 --> <aop:after-throwing method="afterThrowing" pointcut-ref="pt3" throwing="ex" /> <!-- aop:after用于配置最终通知/后置通知 --> <aop:after method="after" pointcut-ref="pt3"/> <!-- aop:around用于配置环绕通知 --> <aop:around method="around" pointcut-ref="pt4"/> </aop:aspect> <aop:aspect ref="logAspect2" order="1"> <aop:before method="before3" pointcut-ref="pt1" /> <aop:before method="before4" pointcut-ref="pt1" /> </aop:aspect> </aop:config> ``` ### JDBC Template的CRUD操作有哪些? | 方法 | 说明 | |-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | public int update(String sql) | 用于执行新增、更新、删除等语句;sql:需要执行的 SQL 语句;args 表示需要传入到 SQL 语句中的参数。 | | public int update(String sql,Object... args) | | | public void execute(String sql) | 可以执行任意 SQL,一般用于执行 DDL 语句; sql:需要执行的 SQL 语句;action 表示执行完 SQL 语句后,要调用的函数。 | | public T execute(String sql, PreparedStatementCallback action) | | | public <T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args) | 用于执行查询语句;sql:需要执行的 SQL 语句;rowMapper:用于确定返回的集合(List)的类型;args:表示需要传入到 SQL 语句的参数。 | | public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) | | | public int[] batchUpdate(String sql, List<Object[]> batchArgs, final int[] argTypes) | 用于批量执行新增、更新、删除等语句; sql:需要执行的 SQL 语句;argTypes:需要注入的 SQL 参数的 JDBC 类型;batchArgs:表示需要传入到 SQL 语句的参数。 | ### 如何创建处理全局异常? 局部异常 ```java //可以在控制器类中添加方法,用于处理异常,这个方法就叫做局部异常处理程序 //这样该方法可以处理这个控制器类里发生的异常 //异常处理程序使用@ExceptionHandler注解修饰,说明这个方法用于处理异常 //value属性用于指定处理的异常类型,多个类型使用一个数组给出 //在异常处理程序中,可以跳转到某个错误页面,返回一个字符串指定视图名称即可 //在异常处理程序中,可以使用Model等对象返回一些数据给页面 //如果希望获取异常信息,可以在方法上添加一个对应异常类型(如ArithmeticException)的参数 //也可使用Throwable来接收所有类型的异常对象 @ExceptionHandler({ArithmeticException.class,NumberFormatException.class}) public String handleException(Model model, Throwable ex) { //可以考虑使用Log4J2等日志框架将异常记录到日志 logger.error("发生异常,异常消息: "+ex.getMessage()); //如果需要返回一些数据显示在页面,可以添加到Model对象中 model.addAttribute("msg", "局部异常处理程序,异常消息: "+ex.getMessage()); return "error"; //指定要跳转的视图的名称 } ``` 全局异常 ```java //全局异常处理程序所在的类也要成为spring mvc容器管理的bean //@Controller、@Service、@Repository、@Component //@RestController、@Configuration、@ControllerAdvice //@ControllerAdvice也会将一个类注册为spring管理的bean //@ControllerAdvice作用很多,其中一个作用是修饰包含全局异常处理程序类 //这样,需要开启对这个类所在的包的扫描,但是这里我们直接将类放到了com.qdu.controller的子包 //com.qdu.controller.advice包下,所以不需要额外开启包扫描 @ControllerAdvice public class GlobalExceptionHandler { private static Logger logger=LoggerFactory.getLogger(GlobalExceptionHandler.class); //添加一个方法,作为异常处理程序 //方法需要使用@ExceptionHandler注解进行修饰 //可以考虑处理异常后,跳转到一个友好的错误页面,如果需要,也可在错误页面显示一些信息 //如果同时定义了全局异常处理和局部异常处理程序,那么会使用局部异常处理程序 @ExceptionHandler({ArithmeticException.class}) public String handleException(Model model, ArithmeticException e) { logger.error("全局异常处理程序:程序发生异常,异常消息-"+e.getMessage()); model.addAttribute("msg", "全局异常处理程序,异常消息: "+e.getMessage()); return "error"; } //异常处理程序前也可使用@ResponseBody注解,让返回的内容成为响应正文内容显示 //而不是要跳转的页面的名称 @ExceptionHandler({IOException.class,ArrayIndexOutOfBoundsException.class}) @ResponseBody public String exceptionHandler2(Throwable ex) { return "Exception occurred, exception message: "+ex.getMessage(); } } ``` ### JDBC Template 和 NamedParameterJDBCTemplate的区别 JDBC Template:最基本的JDBC模板,支持基于索引参数(`?`)的查询 NamedParameterJdbcTemplate:使用该模板类执行查询的时候使用命名参数的方式绑定到SQL而不是索引参数 ### 事务的特征? 事务具有 4 个特性:原子性、一致性、隔离性和持久性,简称为 ACID 特性。 - 原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的动作要么都做要么都不做。 - 一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态,一致性和原子性是密切相关的。 - 隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。 - 持久性(Durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。 ### 事务的传播行为? 事务传播行为(propagation behavior)指的是,当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。 Spring 提供了以下 7 种不同的事务传播行为。 | 名称 | 说明 | |---------------------------|--------------------------------------------------------------| | PROPAGATION_MANDATORY | 支持当前事务,如果不存在当前事务,则引发异常。 | | PROPAGATION_NESTED | 如果当前事务存在,则在嵌套事务中执行。 | | PROPAGATION_NEVER | 不支持当前事务,如果当前事务存在,则引发异常。 | | PROPAGATION_NOT_SUPPORTED | 不支持当前事务,始终以非事务方式执行。 | | PROPAGATION_REQUIRED | 默认传播行为,如果存在当前事务,则当前方法就在当前事务中运行,如果不存在,则创建一个新的事务,并在这个新建的事务中运行。 | | PROPAGATION_REQUIRES_NEW | 创建新事务,如果已经存在事务则暂停当前事务。 | | PROPAGATION_SUPPORTS | 支持当前事务,如果不存在事务,则以非事务方式执行。 | ### 事务并发可能导致的问题? 事务的隔离级别定义了一个事务可能受其他并发事务影响的程度。 在实际应用中,经常会出现多个事务同时对同一数据执行不同操作,来实现各自的任务的情况。此时就有可能导致脏读、幻读以及不可重复读等问题的出现。 在理想情况下,事务之间是完全隔离的,这自然不会出现上述问题。但完全的事务隔离会导致性能问题,而且并不是所有的应用都需要事务的完全隔离,因此有时应用程序在事务隔离上也有一定的灵活性。 Spring 中提供了以下隔离级别,我们可以根据自身的需求自行选择合适的隔离级别。 | 方法 | 说明 | |----------------------------|----------------------------------------------| | ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | | ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的更改,可能导致脏读、幻读和不可重复读 | | ISOLATION_READ_COMMITTED | Oracle 默认级别,允许读取已提交的并发事务,防止脏读,可能出现幻读和不可重复读 | | ISOLATION_REPEATABLE_READ | MySQL 默认级别,多次读取相同字段的结果是一致的,防止脏读和不可重复读,可能出现幻读 | | ISOLATION_SERIALIZABLE | 完全服从 ACID 的隔离级别,防止脏读、不可重复读和幻读 | ### PlatformTransactionManager接口的作用? Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。 在 Spring 中提供了一个 org.springframework.transaction.PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下。 ```java public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; } ``` 该接口中各方法说明如下: | 名称 | 说明 | |--------------------------------------------------------------------|-------------| | TransactionStatus getTransaction(TransactionDefinition definition) | 用于获取事务的状态信息 | | void commit(TransactionStatus status) | 用于提交事务 | | void rollback(TransactionStatus status) | 用于回滚事务 | Spring 为不同的持久化框架或平台(例如 JDBC、Hibernate、JPA 以及 JTA 等)提供了不同的 PlatformTransactionManager 接口实现,这些实现类被称为事务管理器实现。 | 实现类 | 说明 | |----------------------------------------------------------------------------|---------------------------------------------| | org.springframework.<br/>jdbc.datasource.<br/>DataSourceTransactionManager | 使用 Spring JDBC 或 iBatis 进行持久化数据时使用。 | | org.springframework.<br/>orm.hibernate3.<br/>HibernateTransactionManager | 使用 Hibernate 3.0 及以上版本进行持久化数据时使用。 | | org.springframework<br/>.orm.jpa.<br/>JpaTransactionManager | 使用 JPA 进行持久化时使用。 | | org.springframework<br/>.jdo.<br/>JdoTransactionManager | 当持久化机制是 Jdo 时使用。 | | org.springframework.<br/>transaction.<br/>jta.JtaTransactionManager | 使用 JTA 来实现事务管理,在一个事务跨越多个不同的资源(即分布式事务)使用该实现。 | 这些事务管理器的使用方式十分简单,我们只要根据持久化框架(或平台)选用相应的事务管理器实现,即可实现对事物的管理,而不必关心实际事务实现到底是什么。 ### 如何使用XML配置事务? #### 1. 引入 tx 命名空间 Spring 提供了一个 tx 命名空间,借助它可以极大地简化 Spring 中的声明式事务的配置。 想要使用 tx 命名空间,第一步就是要在 XML 配置文件中添加 tx 命名空间的约束。 ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> ``` > **注意:**由于 Spring 提供的声明式事务管理是依赖于 Spring AOP 实现的,因此我们在 XML 配置文件中还应该添加与 aop 命名空间相关的配置。 #### 2. 配置事务管理器 接下来,我们就需要借助数据源配置,定义相应的事务管理器实现(PlatformTransactionManager 接口的实现类)的 Bean,配置内容如下。 ```xml <!--配置数据源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <!--数据库连接地址--> <property name="url" value="xxx"/> <!--数据库的用户名--> <property name="username" value="xxx"/> <!--数据库的密码--> <property name="password" value="xxx"/> <!--数据库驱动--> <property name="driverClassName" value="xxx"/> </bean> <!--配置事务管理器,以 JDBC 为例--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> ``` 在以上配置中,配置的事务管理器实现为 DataSourceTransactionManager,即为 JDBC 和 iBatis 提供的 PlatformTransactionManager 接口实现。 #### 3. 配置事务通知 在 Spring 的 XML 配置文件中配置事务通知,指定事务作用的方法以及所需的事务属性。 ```xml <!--配置通知--> <tx:advice id="tx-advice" transaction-manager="transactionManager"> <!--配置事务参数--> <tx:attributes> <tx:method name="create*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/> </tx:attributes> </tx:advice> ``` ##### 事务管理器配置 当我们使用 <tx:advice> 来声明事务时,需要通过 transaction-manager 参数来定义一个事务管理器,这个参数的取值默认为 transactionManager。 如果我们自己设置的事务管理器(第 2 步中设置的事务管理器 id)恰好与默认值相同,则可以省略对改参数的配置。 ```xml <tx:advice id="tx-advice" > <!--配置事务参数--> <tx:attributes> <tx:method name="create*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/> </tx:attributes> </tx:advice> ``` 但如果我们自己设置的事务管理器 id 与默认值不同,则必须手动在 <tx:advice> 元素中通过 transaction-manager 参数指定。 ##### 事务属性配置 对于<tx:advice> 来说,事务属性是被定义在<tx:attributes> 中的,该元素可以包含一个或多个 <tx:method> 元素。 <tx:method> 元素包含多个属性参数,可以为某个或某些指定的方法(name 属性定义的方法)定义事务属性,如下表所示。 | 事务属性 | 说明 | |-----------------|-------------------------------------------------------------| | propagation | 指定事务的传播行为。 | | isolation | 指定事务的隔离级别。 | | read-only | 指定是否为只读事务。 | | timeout | 表示超时时间,单位为“秒”;声明的事务在指定的超时时间后,自动回滚,避免事务长时间不提交会回滚导致的数据库资源的占用。 | | rollback-for | 指定事务对于那些类型的异常应当回滚,而不提交。 | | no-rollback-for | 指定事务对于那些异常应当继续运行,而不回滚。 | ### 4. 配置切点切面 <tx:advice> 元素只是定义了一个 AOP 通知,它并不是一个完整的事务性切面。我们在 <tx:advice> 元素中并没有定义哪些 Bean 应该被通知,因此我们需要一个切点来做这件事。 在 Spring 的 XML 配置中,我们可以利用 Spring AOP 技术将事务通知(tx-advice)和切点配置到切面中,配置内容如下。 ```xml <!--配置切点和切面--> <aop:config> <!--配置切点--> <aop:pointcut id="tx-pt" expression="execution(* net.biancheng.c.service.impl.OrderServiceImpl.*(..))"/> <!--配置切面--> <aop:advisor advice-ref="tx-advice" pointcut-ref="tx-pt"></aop:advisor> </aop:config> ``` ### 如何启用注解驱动的事务编程模型? #### 1. 开启注解事务 tx 命名空间提供了一个 <tx:annotation-driven> 元素,用来开启注解事务,简化 Spring 声明式事务的 XML 配置。 <tx:annotation-driven> 元素的使用方式也十分的简单,我们只要在 Spring 的 XML 配置中添加这样一行配置即可。 ```xml <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven> ``` 与 <tx:advice> 元素一样,<tx:annotation-driven> 也需要通过 transaction-manager 属性来定义一个事务管理器,这个参数的取值默认为 transactionManager。如果我们使用的事务管理器的 id 与默认值相同,则可以省略对该属性的配置,形式如下。 ```xml <tx:annotation-driven/> ``` 通过 <tx:annotation-driven> 元素开启注解事务后,Spring 会自动对容器中的 Bean 进行检查,找到使用 @Transactional 注解的 Bean,并为其提供事务支持。 #### 2. 使用 @Transactional 注解 @Transactional 注解是 Spring 声明式事务编程的核心注解,该注解既可以在类上使用,也可以在方法上使用。 ```java @Transactional public class XXX { @Transactional public void A(Order order) { …… } public void B(Order order) { …… } } ``` 若 @Transactional 注解在类上使用,则表示类中的所有方法都支持事务;若 @Transactional 注解在方法上使用,则表示当前方法支持事务。 Spring 在容器中查找所有使用了 @Transactional 注解的 Bean,并自动为它们添加事务通知,通知的事务属性则是通过 @Transactional 注解的属性来定义的。 @Transactional 注解包含多个属性,其中常用属性如下表。 | 事务属性 | 说明 | |-----------------|-------------------------------------------------------------| | propagation | 指定事务的传播行为。 | | isolation | 指定事务的隔离级别。 | | read-only | 指定是否为只读事务。 | | timeout | 表示超时时间,单位为“秒”;声明的事务在指定的超时时间后,自动回滚,避免事务长时间不提交会回滚导致的数据库资源的占用。 | | rollback-for | 指定事务对于那些类型的异常应当回滚,而不提交。 | | no-rollback-for | 指定事务对于那些异常应当继续运行,而不回滚。 | ### SQLSessionFactory的创建与使用? ```java public class MybatisUtil { private static SqlSessionFactory sqlSessionFactory; static { try { InputStream is = Resources.getResourceAsStream("config/mybatis-config.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); sqlSessionFactory = builder.build(is); } catch (IOException ex) { ex.printStackTrace(); } } public static SqlSessionFactory getSqlSessionFactory(){ return sqlSessionFactory; } public static SqlSession openSession(){ return sqlSessionFactory.openSession(); } } ``` ### 如何使用Java类替换web.xml? web.xml对应的配置 ```java public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { // spring的配置 @Override protected Class<?>[] getRootConfigClasses() { return new Class[] { SpringConfig.class }; } // springMVC的配置 @Override protected Class<?>[] getServletConfigClasses() { return new Class[] { SpringMvcConfig.class }; } // servlet配置 @Override protected String[] getServletMappings() { return new String[] { "/" }; } // servlet过滤器 @Override protected Filter[] getServletFilters() { return new Filter[] { new CharacterEncodingFilter("utf-8",true,true) }; } } ``` springApplication.xml相应的配置 ```java @Configuration //开启对service组件所在包的扫描 @ComponentScan(basePackages= {"com.qdu.service"}) //指定要使用的属性文件的位置,src目录下的文件要添加classpath:作为前缀 @PropertySource({"classpath:config/jdbc.properties"}) //开启对Mapper接口所在包的扫描,这样mybatis-spring可以帮你发现映射器 //帮你创建Mapper接口的对象,也就是映射器实例 @MapperScan(basePackages= {"com.qdu.mapper"}) //启用注解驱动的编程模型,这样可以通过使用@Transactional注解来实现事务 @EnableTransactionManagement public class SpringConfig { //注入一个Environment对象,用于读取属性文件中的属性 @Autowired Environment env; //1. 配置数据源 @Bean public DruidDataSource dataSource() { //属性文件中属性的值都是字符串,所以如果需要转换类型,需要手动转换 DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(env.getProperty("jdbc.driver")); dataSource.setUrl(env.getProperty("jdbc.url")); dataSource.setUsername(env.getProperty("jdbc.username")); dataSource.setPassword(env.getProperty("jdbc.password")); dataSource.setInitialSize(Integer.parseInt(env.getProperty("initialSize"))); dataSource.setMaxActive(Integer.parseInt(env.getProperty("maxActive"))); dataSource.setMaxWait(Integer.parseInt(env.getProperty("maxWait"))); dataSource.setMinIdle(Integer.parseInt(env.getProperty("minIdle"))); dataSource.setTimeBetweenEvictionRunsMillis(Long.parseLong(env.getProperty("timeBetweenEvictionRunsMillis"))); dataSource.setMinEvictableIdleTimeMillis(Long.parseLong(env.getProperty("minEvictableIdleTimeMillis"))); dataSource.setValidationQuery(env.getProperty("validationQuery")); dataSource.setTestWhileIdle(Boolean.parseBoolean(env.getProperty("minEvictableIdleTimeMillis"))); dataSource.setTestOnBorrow(Boolean.parseBoolean(env.getProperty("testOnBorrow"))); dataSource.setTestOnReturn(Boolean.parseBoolean(env.getProperty("testOnReturn"))); return dataSource; } //2. 配置SqlSessionFactory @Bean public SqlSessionFactory sqlSessionFactory(DataSource ds) throws Exception { //通过SqlSessionFactoryBean来创建SqlSessionFactory对象 SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean(); //用传入的DataSource对象设置为依赖的数据源 factoryBean.setDataSource(ds); //指定要使用别名的类型所在的包,这样在mybatis映射文件等文件中可以使用类型的别名 factoryBean.setTypeAliasesPackage("com.qdu.entity"); //返回创建的SqlSessionFactory对象,成为spring容器管理的bean return factoryBean.getObject(); } //3. 配置事务管理器 @Bean public DataSourceTransactionManager txManager(DataSource ds) { return new DataSourceTransactionManager(ds); } } ``` springMVC.xml对应的配置 ```java @Configuration @ComponentScan(basePackages = {"com.qdu.controller"}) @EnableWebMvc public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("/static/"); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index").setViewName("index"); } @Bean public SpringResourceTemplateResolver templateResolver() { SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html"); templateResolver.setTemplateMode(TemplateMode.HTML); templateResolver.setCacheable(false); templateResolver.setCharacterEncoding("UTF-8"); return templateResolver; } @Bean public SpringTemplateEngine templateEngine() { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); templateEngine.setEnableSpringELCompiler(true); return templateEngine; } @Bean public ThymeleafViewResolver viewResolver() { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine()); viewResolver.setCharacterEncoding("UTF-8"); return viewResolver; } } ``` ### 如何集成SSM? #### java配置类配置ssm 见上条 #### xml配置ssm web.xml ```xml <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:config/spring-config.xml</param-value> </context-param> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:config/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <session-config> <session-timeout> 30 </session-timeout> </session-config> </web-app> ``` spring-config ```xml <beans 略> <!-- 1. 开启包扫描 --> <!-- 因为后面使用MapperScannerConfigurer这个类的配置开启了对Mapper接口所在包的扫描 --> <!-- 而且我们已经没有Mapper的实现类了,所以这里不需要扫描mapper包了 --> <context:component-scan base-package="com.qdu.service" /> <!-- 2. 指定要加载的属性文件的位置,可以指定多个,逗号隔开 --> <context:property-placeholder location="classpath:config/jdbc.properties" /> <!-- 3. 配置数据源,这里使用alibaba的DruidDataSource --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClass}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="initialSize" value="${initialSize}" /> <property name="maxActive" value="${maxActive}" /> <property name="maxWait" value="${maxWait}" /> <property name="minIdle" value="${minIdle}" /> <property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" /> <property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" /> <property name="validationQuery" value="${validationQuery}" /> <property name="testWhileIdle" value="${testWhileIdle}" /> <property name="testOnBorrow" value="${testOnBorrow}" /> <property name="testOnReturn" value="${testOnReturn}" /> </bean> <!-- 4. 配置SqlSessionFactoryBean,告知spring如何创建SqlSessionFactory对象 --> <!-- 并将创建的SqlSessionFactory交给spring容器管理,然后可以在需要的地方依赖注入 --> <!-- 这里class指定的SqlSessionFactoryBean,这是一个工厂bean,负责生产某种类型的对象 --> <!-- 它负责创建SqlSessionFactory对象并交给spring管理该对象 --> <!-- 这里的id是它创建的SqlSessionFactory这个bean的id,而不是SqlSessionFactoryBean的id --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!-- 如果还需要使用mybatis配置文件,可以使用configLocation属性指定其位置 --> <!-- <property name="configLocation" value="classpath:mybatis-config.xml" /> --> <!-- typeAliasesPackage属性用于指定实体类所在的包,这样在mybatis映射文件中可以使用别名来使用实体类 --> <property name="typeAliasesPackage" value="com.qdu.entity" /> <!-- mapperLocations属性用于指定mybatis映射文件的位置 --> <!-- <property name="mapperLocations" value="classpath:com/qdu/mapper/**/*.xml" /> --> </bean> <!-- 5. 配置MapperScannerConfigurer,扫描Mapper接口所在的包,这样可以发现映射器 --> <!-- 可以不用一个个注册映射器,而是直接指定Mapper接口所在的包 --> <!-- 让spring扫描Mapper接口所在的包,根据每个Mapper接口创建对应的Mapper对象,也就是映射器实例 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 需要使用basePackage属性指定Mapper接口所在的包 这样映射器会自动被创建,在需要的地方直接注入映射器(Mapper实例)即可--> <property name="basePackage" value="com.qdu.mapper" /> </bean> <!-- 6. spring集成mybatis应该将事务交给spring管理 --> <!-- 6.1 配置事务管理器,事务管理器提供方法操作事务 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 事务管理器和SqlSessionFactoryBean使用的数据源必须是同一个 --> <property name="dataSource" ref="dataSource" /> </bean> <!-- 6.2 配置事务通知 --> <!-- 配置事务通知其实就是配置通知的bean,但是不使用bean标记 --> <!-- 而是使用tx:advice标记简化事务配置,需要指定通知bean的id和依赖的事务管理器--> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <!-- tx:method标记用于指定每个要加事务的方法如何应用事务 --> <!-- 查询操作可加事务,可不加事务,但是如果查询操作可能受并发事务影响 --> <!-- 那就要考虑加事务 --> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="update*" /> <tx:method name="delete*" /> <tx:method name="transfer*" /> <!-- 如果查询加了事务,最好使用只读事务,方便优化查询 --> <tx:method name="get*" read-only="true" /> </tx:attributes> </tx:advice> <!-- 6.3 配置事务通知器,指定事务通知应用到什么切入点 --> <aop:config> <aop:pointcut expression="within(com.qdu.service..*)" id="pt"/> <!-- 配置通知器,指定哪个通知应用到哪个切入点 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt" /> </aop:config> </beans> ``` spring-mvc ```xml <?xml version="1.0" encoding="UTF-8"?> <beans 略> <!--1. 开启包扫描,扫描控制器所在包 --> <context:component-scan base-package="com.qdu.controller" /> <!--2. 启用注解驱动的控制器编程模型,也就是启用Web MVC配置 --> <mvc:annotation-driven /> <!--3. 配置Thymeleaf相关的bean --> <!-- 配置模板解析器 --> <bean id="templateResolver" class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"> <property name="prefix" value="/WEB-INF/templates/" /> <property name="suffix" value=".html" /> <!-- templateMode指定Thymeleaf处理的模板类型 --> <property name="templateMode" value="HTML" /> <!-- cacheable指定是否可以缓存模板页面内容 开发的时候设置为false,实际发布程序可以设置为true 设置true的话,页面被缓存,如果更改了页面内容,刷新页面不会改变 设置false的话,如果更改了页面内容,刷新页面内容会更改 --> <property name="cacheable" value="false" /> <property name="characterEncoding" value="UTF-8" /> </bean> <!-- 配置模板引擎 --> <bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine"> <property name="templateResolver" ref="templateResolver" /> <!-- enableSpringELCompiler属性设置为true是为了提高性能 --> <!-- 只有考虑向后兼容的时候才会设置为false --> <property name="enableSpringELCompiler" value="true" /> </bean> <!-- 配置Thymeleaf视图解析器 --> <bean class="org.thymeleaf.spring5.view.ThymeleafViewResolver"> <property name="templateEngine" ref="templateEngine" /> <property name="characterEncoding" value="UTF-8" /> </bean> <!-- 4. 处理静态资源 --> <mvc:resources mapping="/static/**" location="/static/" /> <!-- 5. 配置跳转到首页 --> <mvc:view-controller path="/" view-name="index" /> <mvc:view-controller path="/index" view-name="index" /> </beans> ``` ### 如何配置Spring Security? 1.添加启动依赖 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` 2.配置spring security ```java @Configuration @EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //注入要使用的UserDetailsService对象 @Autowired private CustomUserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //指定要使用UserDetailsService对象,从而实现从数据库加载用户信息(用户名、密码和角色/授权) //并指定要使用的密码加密器 auth .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/img/**"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/index", "/toLogin", "/login_failed").permitAll() .antMatchers("/user/**").hasRole("user") .antMatchers("/admin/**").hasAnyRole("admin", "sadmin") .antMatchers("/sadmin/**").hasRole("sadmin") .anyRequest().authenticated() .and() .formLogin() .loginPage("/toLogin") .failureUrl("/login_failed") .and() .rememberMe() .rememberMeCookieName("remember") .rememberMeParameter("rememberMe") .tokenValiditySeconds(7 * 24 * 60 * 60) .and() .httpBasic() .and() .csrf().disable(); // http.httpBasic(); // http.csrf().disable(); } } ``` 3.自定义security功能 ```java //如果使用spring security验证用户,用户的信息来自数据库 //可以创建一个UserDetailsService类来指定如何加载用户信息 //需要将其注册为spring管理的bean,在需要的地方依赖注入即可 @Component public class CustomUserDetailsService implements UserDetailsService{ @Autowired private UserInfoMapper mapper; //loadUserByUsername()方法中查询用户信息,封装成验证要用的用户信息 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //如果需要spring security进行用户的验证和授权 //spring security需要知道用户的用户名、密码和角色 //这里根据传入的用户名(直接使用即可,会帮你传入输入的用户名) //根据用户名查询用户的信息,主要是为了获取密码 UserInfo userinfo=mapper.selectByPrimaryKey(username); //如果查不到该用户,抛出一个用户名找不到异常 if(null==userinfo) throw new UsernameNotFoundException("无此用户"); //根据用户名查询用户所属的角色列表 List<String> roles=mapper.selectRoleNamesById(username); //用户角色列表需要封装成一个List<GrantedAuthority>列表 List<GrantedAuthority> authorities=new ArrayList<>(); //遍历每个角色名称,将每个角色名称封装成一个GrantedAuthority对象 //添加到列表中,表示对用户的一项授权,如果多个角色,则有多项授权 for(String role:roles) { //角色名必须要添加ROLE_前缀,这是spring security的工作机制决定的 authorities.add(new SimpleGrantedAuthority("ROLE_"+role)); } //最终要返回一个UserDetails对象,封装了验证和授权用的用户名、密码和授权信息 //UserDetails是一个接口,可以使用其实现类User return new User(userinfo.getUid(),userinfo.getUpassword(),authorities); } } ``` ## spring MVC 12问 ### MVC模式的概念? MVC 设计模式一般指 MVC 框架,M(Model)指数据模型层,V(View)指视图层,C(Controller)指控制层。使用 MVC 的目的是将 M 和 V 的实现代码分离,使同一个程序可以有不同的表现形式。其中,View 的定义比较清晰,就是用户界面。 ### Spring MVC包含的组件及其作用? #### 1)DispatcherServlet DispatcherServlet 是前端控制器,从图 1 可以看出,Spring MVC 的所有请求都要经过 DispatcherServlet 来统一分发。DispatcherServlet 相当于一个转发器或中央处理器,控制整个流程的执行,对各个组件进行统一调度,以降低组件之间的耦合性,有利于组件之间的拓展。 #### 2)HandlerMapping HandlerMapping 是处理器映射器,其作用是根据请求的 URL 路径,通过注解或者 XML 配置,寻找匹配的处理器(Handler)信息。 #### 3)HandlerAdapter HandlerAdapter 是处理器适配器,其作用是根据映射器找到的处理器(Handler)信息,按照特定规则执行相关的处理器(Handler)。 #### 4)Handler Handler 是处理器,和 Java Servlet 扮演的角色一致。其作用是执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装至 ModelAndView 对象中。 #### 5)View Resolver View Resolver 是视图解析器,其作用是进行解析操作,通过 ModelAndView 对象中的 View 信息将逻辑视图名解析成真正的视图 View(如通过一个 JSP 路径返回一个真正的 JSP 页面)。 #### 6)View View 是视图,其本身是一个接口,实现类支持不同的 View 类型(JSP、FreeMarker、Excel 等)。 以上组件中,需要开发人员进行开发的是处理器(Handler,常称Controller)和视图(View)。通俗的说,要开发处理该请求的具体代码逻辑,以及最终展示给用户的界面。 ### Spring MVC处理请求的过程? ![Spring MVC执行流程](./ssm/1139441444-0.png) SpringMVC 的执行流程如下。 1. 用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器); 2. 由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。 3. DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器); 4. HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller); 5. Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息); 6. HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ; 7. DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析; 8. ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet; 9. DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图); 10. 视图负责将结果显示到浏览器(客户端)。 ### servlet合法的URL模式? servlet的url模式合法的写法有: - 精准匹配 - ./index.jsp - 扩展名匹配 - `*.xxx` 匹配以xxx结尾的请求url - 路径匹配 - `/xxx/*` 用于匹配路径包含xxx的所有请求,xxx可以是一级或者多级 - `/*` 匹配以/开头,任意结尾的请求url。 - 缺省匹配 - `/` 匹配除JSP之外的所有请求 ### 如何配置DispatcherServlet? xml配置 ```xml <!--web.xml--> <web-app> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 如果不指定<context-param>, 将会默认加载/WEB-INF/applicationContext.xml文件.--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/app-context.xml</param-value> </context-param> <servlet> <servlet-name>app</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value></param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>app</servlet-name> <url-pattern>/app/*</url-pattern> </servlet-mapping> </web-app> ``` Java配置:实现WebApplicationInitializer接口 ```java public class MyWebApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletCxt) { // 加载spring 容器 AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext(); ac.register(AppConfig.class); ac.refresh(); // 创建并注册DispatcherServlet DispatcherServlet servlet = new DispatcherServlet(ac); ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet); registration.setLoadOnStartup(1); registration.addMapping("/app/*"); } } ``` ### 如何配置Spring MVC配置文件的位置? Spring MVC 配置:在 web.xml 中配置 Servlet,创建 Spring MVC 的配置文件 ```xml <!-- 配置 SpringMVC 的前端控制器,对浏览器发送的请求统一进行处理 --> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--配置 DispatcherServlet 的一个初始化参数:spring mvc 配置文件按的位置和名称--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMVC.xml</param-value> </init-param> <!--作为框架的核心组件,在启动过程中有大量的初始化操作要做 而这些操作放在第一次请求时才执行会严重影响访问速度 因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时--> <load-on-startup>1</load-on-startup> </servlet> ``` ### Spring和Spring MVC配置文件的默认名称? springMVC配置文件默认名称:`<servlet-name>-servlet.xml` servlet-name是在web.xml`<servlet>`标签中定义的 spring配置文件名称:`applicationContext.xml` ### 如何配置视图解析器? in *-.servlet ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!--开启组件扫描--> <context:component-scan base-package=""/> <!-- 配置 Thymeleaf 视图解析器 --> <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"> <property name="order" value="1"/> <property name="characterEncoding" value="UTF-8"/> <property name="templateEngine"> <bean class="org.thymeleaf.spring5.SpringTemplateEngine"> <property name="templateResolver"> <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"> <!-- 视图前缀 --> <property name="prefix" value="/WEB-INF/templates/"/> <!-- 视图后缀 --> <property name="suffix" value=".html"/> <property name="templateMode" value="HTML5"/> <property name="characterEncoding" value="UTF-8"/> </bean> </property> </bean> </property> </bean> </beans> ``` 视图解析器(ViewResolver)是 Spring MVC 的重要组成部分,负责将逻辑视图名解析为具体的视图对象。 Spring MVC 提供了很多视图解析类,其中每一项都对应 Java Web 应用中特定的某些视图技术。下面介绍一些常用的视图解析类。 #### URLBasedViewResolver UrlBasedViewResolver 是对 ViewResolver 的一种简单实现,主要提供了一种拼接 URL 的方式来解析视图。 UrlBasedViewResolver 通过 prefix 属性指定前缀,suffix 属性指定后缀。当 ModelAndView 对象返回具体的 View 名称时,它会将前缀 prefix 和后缀 suffix 与具体的视图名称拼接,得到一个视图资源文件的具体加载路径,从而加载真正的视图文件并反馈给用户。 使用 UrlBasedViewResolver 除了要配置前缀和后缀属性之外,还需要配置“viewClass”,表示解析成哪种视图。示例代码如下 ```xml <bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceViewResolver"/> <!--不能省略--> <!--前缀--> <property name="prefix" value="/WEB-INF/jsp/"/> <!--后缀--> <property name="suffix" value=".jsp"/> </bean> ``` #### InternalResourceViewResolver InternalResourceViewResolver 为“内部资源视图解析器”,是日常开发中最常用的视图解析器类型。它是 URLBasedViewResolver 的子类,拥有 URLBasedViewResolver 的一切特性。 InternalResourceViewResolver 能自动将返回的视图名称解析为 InternalResourceView 类型的对象。InternalResourceView 会把 Controller 处理器方法返回的模型属性都存放到对应的 request 属性中,然后通过 RequestDispatcher 在服务器端把请求 forword 重定向到目标 URL。也就是说,使用 InternalResourceViewResolver 视图解析时,无需再单独指定 viewClass 属性。示例代码如下。 ```xml <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceViewResolver"/> <!--可以省略--> <!--前缀--> <property name="prefix" value="/WEB-INF/jsp/"/> <!--后缀--> <property name="suffix" value=".jsp"/> </bean> ``` ### 创建和使用控制器? @Controller 注解用于声明某类的实例是一个控制器。例如,创建控制器类 IndexController,示例代码如下 ```java @Controller public class IndexController { // 处理请求的方法 } ``` Spring MVC 使用扫描机制找到应用中所有基于注解的控制器类,所以,为了让控制器类被 Spring MVC 框架扫描到,需要在配置文件中声明 spring-context,并使用 `<context:component-scan/>` 元素指定控制器类的基本包(请确保所有控制器类都在基本包及其子包下)。 例如,在 springmvcDemo 应用的配置文件 springmvc-servlet.xml 中添加以下代码: ```xml <!-- 使用扫描机制扫描控制器类,控制器类都在net.biancheng.controller包及其子包下 --> <context:component-scan base-package="com.example.controller" /> ``` ### 如何映射不同的请求? 在基于注解的控制器类中可以为每个请求编写对应的处理方法。使用 @RequestMapping 注解将请求与处理方法一 一对应即可。 @RequestMapping 注解可用于类或方法上。用于类上,表示类中的所有响应请求的方法都以该地址作为父路径。 @RequestMapping 注解常用属性如下。 #### 1. value 属性 value 属性是 @RequestMapping 注解的默认属性,因此如果只有 value 属性时,可以省略该属性名,如果有其它属性,则必须写上 value 属性名称。如下。 ```java @RequestMapping(value="toUser") //or @RequestMapping("toUser") ``` value 属性支持通配符匹配,如 `@RequestMapping(value="toUser/*")` 表示 http://localhost:8080/toUser/1 或 http://localhost:8080/toUser/hahaha 都能够正常访问。 #### 2. path属性 path 属性和 value 属性都用来作为映射使用。即 `@RequestMapping(value="toUser")` 和 `@RequestMapping(path="toUser")` 都能访问 toUser() 方法。 path 属性支持通配符匹配,如 `@RequestMapping(path="toUser/*")` 表示 http://localhost:8080/toUser/1 或 http://localhost:8080/toUser/hahaha 都能够正常访问。 #### 3. name属性 name属性相当于方法的注释,使方法更易理解。如 `@RequestMapping(value = "toUser",name = "获取用户信息")`。 #### 4. method属性 method 属性用于表示该方法支持哪些 HTTP 请求。如果省略 method 属性,则说明该方法支持全部的 HTTP 请求。 `@RequestMapping(value = "toUser",method = RequestMethod.GET)`表示该方法只支持 GET 请求。 也可指定多个 HTTP 请求,如 `@RequestMapping(value = "toUser",method = {RequestMethod.GET,RequestMethod.POST})`,说明该方法同时支持 GET 和 POST 请求。 #### 5. params属性 params 属性用于指定请求中规定的参数,代码如下。 ```java @RequestMapping(value = "toUser",params = "type") public String toUser() { return "showUser"; } ``` 以上代码表示请求中必须包含 type 参数时才能执行该请求。即 http://localhost:8080/toUser?type=xxx 能够正常访问 toUser() 方法,而 http://localhost:8080/toUser 则不能正常访问 toUser() 方法。 ```java @RequestMapping(value = "toUser",params = "type=1") public String toUser() { return "showUser"; } ``` 以上代码表示请求中必须包含 type 参数,且 type 参数为 1 时才能够执行该请求。即 http://localhost:8080/toUser?type=1 能够正常访问 toUser() 方法,而 http://localhost:8080/toUser?type=2 则不能正常访问 toUser() 方法。 #### 6. header属性 header 属性表示请求中必须包含某些指定的 header 值。 `@RequestMapping(value = "toUser",headers = "Referer=http://www.xxx.com")` 表示请求的 header 中必须包含了指定的“Referer”请求头,以及值为“http://www.xxx.com”时,才能执行该请求。 #### 7. consumers属性 consumers 属性用于指定处理请求的提交内容类型(Content-Type),例如:application/json、text/html。如 `@RequestMapping(value = "toUser",consumes = "application/json")`。 #### 8. produces属性 produces 属性用于指定返回的内容类型,返回的内容类型必须是 request 请求头(Accept)中所包含的类型。如 @RequestMapping(value = "toUser",produces = "application/json")。 除此之外,produces 属性还可以指定返回值的编码。如 `@RequestMapping(value = "toUser",produces = "application/json,charset=utf-8")`,表示返回 utf-8 编码。 使用 @RequestMapping 来完成映射,具体包括 4 个方面的信息项:请求 URL、请求参数、请求方法和请求头。 ### 如何配置静态资源? ```xml <mvc:resources mapping="/js/**" location="/js/"></mvc:resources> <mvc:resources mapping="/css/**" location="/css/"></mvc:resources> <mvc:resources mapping="/img/**" location="/img/"></mvc:resources> <!-- 在配置了mvc:resources标签之后必须配置mvc:annotation-driven标签静态资源 才可以访问,否则不仅静态资源不能访问,其他的所有请求也都无法正常处理了 --> <mvc:annotation-driven/> ``` ### 如何构建Restful API? REST(Representational State Transfer)即表述性转移,是目前最流行的一种软件架构风格。它结构清晰、易于理解、有较好的扩展性。 Spring REST 风格可以简单理解为:使用 URL 表示资源时,每个资源都用一个独一无二的 URL 来表示,并使用 HTTP 方法表示操作,即准确描述服务器对资源的处理动作(GET、POST、PUT、DELETE),实现资源的增删改查。 - GET:表示获取资源 - POST:表示新建资源 - PUT:表示更新资源 - DELETE:表示删除资源 # Spring Boot 65问 ## 框架22问 ### 启动依赖是什么? Spring Boot就可以指定基于功能依赖。Spring Boot通过起步依赖为项目的依赖管理提供帮助。如果应用程序是Web应用程序(功能),不需要向项目pom.xml文件中添加一堆单独的依赖,可以直接向项目中添加Web起步依赖。如果应用程序需要用到JPA持久化(功能),加入jpa起步依赖;如果需要安全功能(功能),就加入security起步依赖。添加依赖时不需要指定依赖的版本号,依赖的版本号由当前是使用的Spring Boot版本号来决定。 起步依赖就是特殊的Maven依赖,利用了传递依赖解析,把常用库聚合在一起,组成几个为特定功能而定制的依赖。Spring Boot通过起步依赖:直接引入相关起步依赖就行,我们不需要考虑支持某种功能需要什么库, 减少了依赖数量,而且不需要考虑这些库的那些版本。如果我们需要什么功能,就往项目中加入该功能的起步依赖就好了。 ### Actuator的作用? Actuator用于监视和管理应用程序 监控的内容: - Spring应用程序上下文中配置的Bean - Spring Boot的自动配置做的决策 - 应用程序可用的环境变量、系统属性、配置属性和命令行参数等 - 应用程序里线程的当前状态 - 应用程序最近处理的HTTP请求的追踪情况 - 各种和内存使用、垃圾回收、Web请求等相关的指标 ### Spring Boot 配置依赖? ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> ``` ### 如何修饰启动类? ```xml <!--排除依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> </exclusion> </exclusions> </dependency> ``` ```xml <!--用指定依赖版本 覆盖 传递依赖--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.4.3</version> </dependency> ``` ### @SpringBootApplication是哪三合一? @SpringBootApplication = (默认属性)@Configuration + @EnableAutoConfiguration + @ComponentScan。 @Configuration:建一个简单的spring配置类,可以用来替代相应的xml配置文件,见词条 @Configuration和@Bean注解的作用 @EnableAutoConfiguration:能够自动配置spring的上下文,试图猜测和配置你想要的bean类,通常会自动根据你的类路径和你的bean定义自动配置 @ComponentScan:会自动扫描指定包下的全部标有@Component的类,并注册成bean,当然包括@Component下的子注解@Service,@Repository,@Controller ### 如何排除指定的自动配置? ```java @SpringBootApplication(exclude={RedisAutoConfiguration.class}) ``` ### 一个启动类能否同时作为控制器类? 能 ### 如何自定义banner? 将`banner.txt`(或`banner,jpg`)放入`src/main/resources` ### SpringBoot如何实现自动配置? 加载spring.factories ### SpringBoot如何配置视图解析器? **配置文件** ```properties spring.mvc.view.prefix=/pages/ spring.mvc.view.suffix=.html ``` 或 ```yaml spring: mvc: view: prefix: /pages/ suffix: .html ``` **配置类** ```java @Configuration public class MvcConfig implements WebMvcConfigurer { @Bean public InternalResourceViewResolver configureInternalResourceViewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/pages/"); resolver.setSuffix(".html"); return resolver; } } ``` ### 如何配置端口号? ```properties server.port=8081 ``` 或 ```yaml server: port: 8081 ``` ### properties与yml的异同? <mark>properties的优先级会高于yml</mark> yml采用树形结构,更有层次感,可读性很强;相反,properties 则更为直接 properties 的基本语法格式是“key=value”的形式;yml 的基本语法格式是“key: value”的形式,冒号后面需要加空格 ### 静态资源默认位置在哪? classpath:/META-INF/resources/ classpath:/resources/ classpath:/static/ classpath:/public/ ### 如何自定义静态资源的位置? **在配置文件中配置** ```properties #静态资源访问路径 spring.mvc.static-path-pattern=/upload/** #静态资源映射路径 spring.resources.static-locations=classpath:/upload/ ``` **配置类** ```java @Configuration public class MvcConfig extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) {     // 这里之所以多了一"/",是为了解决打war时访问不到问题 registry.addResourceHandler("/**").addResourceLocations("/","classpath:/"); } } ``` ### 有哪些常用的JSON解析器? 1)json-lib json-lib 最早也是应用广泛的 JSON 解析工具,缺点是依赖很多的第三方包 对于复杂类型的转换,json-lib 在将 JSON 转换成 Bean 时还有缺陷,比如一个类里包含另一个类的 List 或者 Map 集合,json-lib 从 JSON 到 Bean 的转换就会出现问题。 #### 2)开源的Jackson 开源的 Jackson 是 Spring MVC 内置的 JSON 转换工具。 但是 Jackson 对于复杂类型的 JSON 转换 Bean 会出现问题 #### 3)Google的Gson Gson 是目前功能最全的 JSON 解析神器 Gson 完全可以将复杂类型的 JSON 到 Bean 或 Bean 到 JSON 的转换,是 JSON 解析的神器。Gson 在功能上面无可挑剔,但性能比 FastJson 有所差距。 #### 4)阿里巴巴的FastJson FastJson 是用 Java 语言编写的高性能 JSON 处理器,由阿里巴巴公司开发。 FastJson 在复杂类型的 Bean 转换 JSON 上会出现一些问题,可能会出现引用的类型,导致 JSON 转换出错,需要制定引用。 ### 为哪些JSON解析器提供了自动配置? Gson 、Jackson 、JSON-B ### 如何使用fastjson? 导入依赖 ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.15</version> </dependency> ``` 在springMVC的配置类中配置JSON解析器为fastJSON ```java //通过配置HttpMessageConverter来配置使用的JSON处理器 //每个JSON处理器都有对应的消息转换器实现类,用于实现java对象和json对象的转换 @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter converter=new FastJsonHttpMessageConverter(); //设置转换器的字符集编码,解决中文乱码 converter.setDefaultCharset(Charset.forName("UTF-8")); //因为spring boot没有提供fastjson的集成和自动配置,所以不能在application.properties //中配置fastjson的属性 //创建一个FastJsonConfig对象,定制fastjson的配置 FastJsonConfig config=new FastJsonConfig(); config.setDateFormat("yyyy-MM-dd"); //setSerializerFeatures()用于指定序列化后json数据的特征 //WriteMapNullValue: 是否输出值为null的字段,默认为false //WriteNullStringAsEmpty: null字符串是否显示为空字符串,而不是null //WriteNullNumberAsZero: 是否将null的数值显示为0,而不是null //WriteNullListAsEmpty: 是否将null的List显示为[],而不是null //DisableCircularReferenceDetect: 禁用对同一个对象的循环引用,如果使用Hibernate等框架映射了关系,可能会出现循环引用 config.setSerializerFeatures( SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullNumberAsZero, SerializerFeature.WriteNullListAsEmpty, SerializerFeature.DisableCircularReferenceDetect ); //应用fastjson配置 converter.setFastJsonConfig(config); converters.add(converter); for(HttpMessageConverter c:converters) System.out.println(c.getClass()); //打印一下转换器的类型 } ``` 直接用。。。 ### 如何自定义错误界面的位置? **静态** 在 resource/static 目录下创建 error 目录,然后在 error 目录中创建错误展示页面。错误展示页面命名规则有如下两种: 一种是直接使用具体的响应码命名文件,比如:404.html、405.html、500.html 另一种可以使用 4xx.html、5xx.html 这样的命名 **动态** 在 resource/templates 目录下创建 error 目录,然后在 error 目录中创建错误展示页面。 ### 错误界面使用的顺序如何? 优先级: 动态错误码>静态错误码>动态模糊错误码>静态模糊错误码 找到一个后便不往下寻找 ### 如何定义局部异常handler? ```java @ExceptionHandler(value = {java.lang.ArithmeticException.class}) public ModelAndView arithmeticExceptionHandler(Exception e){ ModelAndView mv = new ModelAndView(); mv.addObject("errorMsg",e); mv.setViewName("error"); return mv; } ``` ### 如何定义全局异常handler? ``` //创建一个全局异常处理程序 //全局异常处理程序所在的类使用@ControllerAdvice或@RestControllerAdvice注解修饰 //@ControllerAdvice注解作用很多,其中一个作用是修饰全局异常处理程序所在的类 @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ ArithmeticException.class }) public String handleException(Exception ex, Model model) { // 在此收集异常信息,比如将异常信息写入日志等 model.addAttribute("msg", "全局异常处理程序,异常消息:"+ex.getMessage()); return "err"; // 处理完异常,可以跳转到指定的页面/视图 } //异常处理程序对应的方法前可以使用@ResponseBody注解,将返回值作为响应内容返回 @ExceptionHandler({ArrayIndexOutOfBoundsException.class}) @ResponseBody public Map<String,Object> handleException2(Exception e) { Map<String,Object> map=new HashMap<>(); map.put("status", 500); map.put("msg", "数组索引出界,索引: "+e.getMessage()); map.put("reason", "索引值超出数组长度"); return map; } } ``` ### 如何使用RestController? 如果在类上加上@RestController,该类中的所有SpringMVCUrl接口映射都是返回json格式 ```java @RestController public class hello{ //something } ``` ## Thymeleaf 11问 ### Thymeleaf如何使用变量表达式页面获取不同范围的属性? ```html 变量表达式${.....} <p> <!-- 能够直接获取model的属性值--> user1属性值: <span th:text="${user1.id}"></span> | <span th:text="${user1.password}" ></span> | <span th:text="${user1.gender}" ></span> </p> <p> user2属性值: <!-- 会话范围的属性需要添加session的使用 --> <span th:text="${session.user2.id}"></span> | <span th:text="${session.user2.password}" ></span> | <span th:text="${session.user2.gender}" ></span> </p> ``` ### Thymeleaf的五种表达式? - 变量表达式${.....} - 选择变量表达式*{.....} - 选择变量表达式主要是为了简化书写 - th:object属性用于绑定一个对象,这样在当前元素(p元素)内可以直接使用该对象的属性 - 选择变量表达式用于使用外围绑定的对象的属性,以简化书写 - 消息表达式#{.....} - 消息表达式用于显示消息,主要是来自属性文件的属性,可用于实现国际化等功能 - {属性名}来显示来自属性文件的一个消息 - URL链接变量表达式@{.....} - 用于指定一个链接,指定一个资源的url - 片段(fragment)表达式~{.....} - 页面名称::片段名称 ### 如何使用选择变量表达式? ```html <p th:object="${user1}"> user1属性值: <span th:text="*{id}"></span> | <span th:text="*{password}" ></span> | <span th:text="*{gender}" ></span> </p> ``` ### 链接表达式的四种写法? ```html <div class="content"> <a th:href="@{https://www.baidu.com}" class="btn btn-sm btn-danger">测试1-绝对路径</a> &nbsp; <!-- 请求/页面相对路径:直接写资源的路径 --> <!-- 使用相对路径,要注意404问题 --> <a th:href="@{static/img/lxh01.gif}" class="btn btn-sm btn-warning">测试2-页面/请求相对路径</a> &nbsp; <!-- 上下文相对路径,以/开头,表示相对程序的上下文路径 --> <a th:href="@{/static/img/lxh02.gif}" class="btn btn-sm btn-success">测试3-上下文相对路径</a> &nbsp; <!-- 服务器相对路径,以~/开头,表示相对服务器的路径 --> <a th:href="@{~/static/img/lxh03.gif}" class="btn btn-sm btn-primary">测试4-服务器相对路径</a> &nbsp; <!-- 协议相对路径: 以//开头,表示相对协议的路径 --> <a th:href="@{//static/img/lxh04.gif}" class="btn btn-sm btn-info">测试5-协议相对路径</a> &nbsp; </div> ``` ### 怎么定义和引用片段? **定义** ```html <div th:fragment="f1" id="frag1" class="bg-primary padding10 margin10"> 这是模板片段1内容~~~~~~~~~~~~~~~~~~~<br> 这是模板片段1内容~~~~~~~~~~~~~~~~~~~<br> 这是模板片段1内容~~~~~~~~~~~~~~~~~~~<br> </div> ``` **引用** ```html <div class="content"> <!-- th:insert用于插入一个模板片段,比如下面的代码表示将f1片段内容插入div1 --> <!-- 页面名称::片段名称 --> <div id="div1" th:insert="~{footer::f1}"></div> <!-- th:replace用于使用指定的模板片段替换原有内容,比如下面的代码表示使用f1的片段内容替换div2 --> <div id="div2" th:replace="~{footer::f1}"></div> <!-- 也可通过页面名称::#id通过元素的id来使用一个片段 --> <div id="div3" th:insert="~{footer::#frag2}"></div> <div id="div4" th:insert="~{footer}"></div> <!-- 使用当前页面定义的片段,可不加页面名称或使用this --> <div id="div5" th:insert="~{::frag3}"></div> <div id="div5" th:insert="~{this::#fragment3}"></div> </div> ``` ### 哪些字面标记不合法? 字面标记值类似文本字面值: 区别是不使用单引号括起来值, 只能允许字母、数字、下划线、中划线、点号、中括号, 不能包含逗号、空格以及其他特殊字符 ```html 字面标记:<span th:text="Anna"></span><br> 字面标记:<span th:text="Annabelle_Lee"></span><br> 字面标记:<span th:text="www.baidu.com"></span><br> 字面标记:<span th:text="a-b_c.d"></span><br> <!-- 空格不允许 --> <!-- 字面标记:<span th:text="[kite runner]"></span><br> --> <!-- 逗号不允许 --> <!-- 字面标记:<span th:text="a,b"></span><br> ``` ### th:*属性的作用? th:* 修改单个属性的值 ```html <!-- 绝大部分html属性都有对应的th:*属性,在th:*属性中才能使用Thymeleaf的语法 --> <div class="content"> <p> <!--显示user5的头像,鼠标悬浮显示其id--> <img th:src="'static/img/'+${user5.img}" th:title="${user5.id}" src="static/img/lxh01.gif" title="小胖" width="60" height="60"> <img th:src="@{'/static/img/'+${user5.img}}" th:title="${user5.id}" src="static/img/lxh01.gif" title="小胖" width="60" height="60"> </p> </div> ``` th:attr 修改一个标签多个属性的值 ```html <div class="content"> <p> <!--显示user5的头像,鼠标悬浮显示其id--> <img th:attr="src=@{'/static/img/'+${user5.img}},title=${user5.id}" src="static/img/lxh01.gif" title="小胖" width="60" height="60"> </p> </div> ``` th:object 绑定一个对象,方便在子标记中使用选择变量表达式 ```html <div class="content"> <p th:object="${user5}"> user5属性值: <span th:text="*{id}"></span> | <span th:text="*{password}"></span> | <span th:text="*{gender}"></span> | <span th:text="*{img}"></span> </p> </div> ``` th:if 判断一个条件,条件成立,显示标记内容 ```html <div class="content"> <p th:if="${age}>=18">恭喜你,成为大人了!!!</p> </div> ``` th:switch+th:case 类似switch-case结构作用的属性 ```html <div class="content"> <!-- th:switch通常用于判断一个表达式的值 --> <div th:switch="${type}"> <p th:case="1">冰箱</p> <p th:case="2">空调</p> <p th:case="3">电视</p> <!-- *表示其他情况 --> <p th:case="*">其他</p> </div> </div> ``` th:each 类似foreach循环 ```html <div class="content"> <table class="table table-striped table-hover text-center"> <tr> <th>学号</th> <th>姓名</th> <th>性别</th> </tr> <!--1)遍历列表--> <tr th:each="s:${studentList}"> <!-- th:属性名写法 --> <td th:text="${s.sid}"></td> <!-- data-th-属性名写法 --> <td data-th-text="${s.sname}"></td> <!-- 内联表达式写法 --> <td>[[${s.sgender}]]</td> </tr> </table> <!--2)获取状态信息--> <!-- s是一个变量名,可以随意命名,表示当前遍历的元素 --> <!-- stat是一个变量名,可以随意命名,用于获取当前遍历的元素的状态信息 --> <div th:each="s,stat:${studentList}" th:class="${stat.odd}?'bg-danger':'bg-success'"> [[${s.sid}]] | [[${s.sname}]] | <!-- index:获取当前元素的索引,从0开始 --> [[${stat.index}]] | <!-- count: 获取当前元素的序号,也就是第几个元素 --> [[${stat.count}]] | <!-- size: 获取集合大小 --> [[${stat.size}]] | <!-- first: 返回布尔值,表示是否是第一项元素 --> [[${stat.first}]] | <!-- last: 返回布尔值,表示是否是最后一项元素 --> [[${stat.last}]] | <!-- odd: 返回布尔值,表示是否是奇数项元素 --> [[${stat.odd}]] | <!-- even: 返回布尔值,表示是否是偶数项元素 --> [[${stat.even}]] | </div> <!--3)遍历Map集合--> <hr> <!-- 遍历Map集合,通过key获取键,通过value获取值 --> <div th:each="entry,status:${map1}"> [[${entry.key}]] | [[${entry.value}]] | [[${status.index}]] </div> <!-- 遍历map2集合,打印键和值的各项信息 --> <!-- value如果不是简单类型,可继续获取具体属性 --> </div> ``` th:text和th:utext ```html <div class="content"> <!-- th:text如果内容是html内容,则不会解析html内容,作为文本显示 --> <p th:text="${content1}"></p> <!--th:text等效写法:内联表达式写法--> [[${content1}]] <!-- th:utext如果内容是html内容,会解析html内容,显示解析后的内容 --> <p th:utext="${content2}"></p> <!--th:utext等效写法:内联表达式写法--> [(${content2})] </div> ``` th:with 用于定义局部变量 ```html <!-- th:with用于定义局部变量,可以定义一个或多个局部变量 --> <!-- 定义的变量只能在标记内使用 --> <div th:with="firstStudent=${studentList[0]}"> [[${firstStudent.sid}]] | [[${firstStudent.sname}]] | [[${firstStudent.sgender}]] </div> <!--定义多个变量--> <div th:with="a=10,b=20,c=30"> [[${a+b+c}]] </div> ``` th:inline 启用和禁用内联表达式 ```html <div th:inline="none"> [[1+2+3]] <br> [(1+2+3)] </div> <div> <!-- 如果需要在js代码中使用内联表达式,可将inline属性设置为javascript --> <script th:inline="javascript"> document.write([[${user5.id}]]); document.write("<br>"); document.write([[${age}]]); </script> </div> <div> <!-- 默认,可以在css样式中使用内联表达式 --> <!-- 如果设置值是none,css中的内联表达式不会被解析 --> <style th:text="css"> .bg-red{ color: [[${color}]]; } </style> <p class="bg-red">测试样式</p> </div> ``` ### Thymeleaf的名称空间? Thymeleaf提供一些名称空间,用于获取对应的信息。每个名称空间都对应一个Map集合,存储了对应的属性、参数或其他信息 ```html <div class="content"> <!-- 不使用任何名称空间就是请求范围的属性 --> <!--获取请求范围属性--> <p th:text="${num1}"></p> <p th:text="${num2}"></p> <!--获取会话范围属性--> <p th:text="${session.num3}"></p> <!--获取应用程序范围属性--> <p th:text="${application.num4}"></p> <!--获取一个请求参数的单个值--> <p th:text="${param.name}"></p> <!--获取一个请求参数的多个值--> <p th:text="${param.hobbies}"></p> <!--获取一个请求参数的多个值中的某个值,加上数组索引即可--> <p th:text="${param.hobbies[0]}"></p> </div> ``` ### 有哪些基本对象? Thymeleaf 中常用的内置基本对象如下: - ctx :上下文对象 - vars :上下文变量 - locale:上下文的语言环境 - request:HttpServletRequest 对象(仅在 Web 应用中可用) - response:HttpServletResponse 对象(仅在 Web 应用中可用) - session:HttpSession 对象(仅在 Web 应用中可用) - servletContext:ServletContext 对象(仅在 Web 应用中可用) ### 表达式工具对象如何使用? #### numbers:数字工具对象 ```html formatDecimal()方法: 用于格式化小数数值 <div class="content"> <!-- 这里的三个参数分别表示:要格式化的值,至少保留几位整数,保留几位小数 --> <p th:text="${#numbers.formatDecimal(num1,1,3)}"></p> </div> ``` formatCurrency()方法:用于格式化金额,会加货币符号 ```html <p th:text="${#numbers.formatCurrency(num1)}"></p> ``` sequence()方法: 用于产生一个整数序列/数组, 可以通过产生一个整数序列实现普通循环,如打印分页的页码 ```html <!-- 可以指定两个参数,表示from和to,最小值和最大值 --> <!-- 可以指定三个参数,表示from、to和step,最小值、最大值和步长 --> <span th:each="i:${#numbers.sequence(1,10)}"> | <a th:href="'findByPageNo?pageNo='+${i}">[[${i}]]</a> | </span> ``` #### strings:字符串工具对象 ```html <div class="content"> <!-- 检查一个字符串内容是否为空 --> <p th:text="${#strings.isEmpty('')}"></p> <p th:text="${#strings.isEmpty('anna')}"></p> <!-- 获取字符串长度 --> <p th:text="${#strings.length(bookName)}"></p> <!-- 截取子字符串 从索引为0的字符开始,截取5-0个字符--> <p th:text="${#strings.substring(bookName,0,5)}"></p> <!-- 检查字符串是否包含指定内容 --> <p th:text="${#strings.contains(bookName,'kite')}"></p> </div> ``` #### dates:日期工具对象 ```html <!-- 格式化日期时间 --> <p th:text="${#dates.format(now)}"></p> <!-- 格式化的时候可指定显示格式 --> <p th:text="${#dates.format(now,'yyyy-MM-dd HH:mm:ss D E')}"></p> <!-- 获取日期时间的指定部分, 如年份、月份、日、月份名称、星期几等 --> <p th:text="${#dates.year(now)}"></p> <p th:text="${#dates.month(now)}"></p> <p th:text="${#dates.day(now)}"></p> <p th:text="${#dates.monthName(now)}"></p> <p th:text="${#dates.dayOfWeek(now)}"></p> <p th:text="${#dates.hour(now)}"></p> ``` #### lists/sets:List/Set 集合工具对象 ```html <div class="content"> <!-- #lists对象用于操作List类型的数据,比如sort()方法可用于对集合排序--> <span th:each="name:${#lists.sort(nameList)}"> [[${name}]] | </span> <p th:text="${#lists.sort(scoreList)}"></p> </div> ``` #### aggregates ```html <!-- #aggregates用于对数组或集合进行汇总运算,如求和或求平均 --> <div class="content"> <p th:text="${#aggregates.avg(scoreList)}"></p> <p th:text="${#aggregates.sum(scoreList)}"></p> </div> ``` #### maps:Map 集合工具对象 常用的方法有:size、isEmpty、containsKey 和 containsValue 等 ## Spring Boot JPA 13问 ### SpringBoot支持的三种数据源是什么? HikariCP:默认内置数据源对象 Tomcat提供DataSource:HikariCP不可用的情况下,在web环境中,将tomcat服务器配置的数据源对象。 Commons DBCP:HikariCP不可用,tomcat数据源也不可用,将使用dbcp数据源。 ### 如何配置数据源? 导入依赖项 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> ``` ```yaml spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf-8 username: root password: root jpa: hibernate: ddl-auto: update show-sql: true ``` ### SpringDataJPA是什么? spring data jpa是spring提供的一套简化JPA开发的框架,按照约定好的【方法命名规则】写dao层接口,就可以在不写接口实现的情况下,实现对数据库的访问和操作。同时提供了很多除了CRUD之外的功能,如分页、排序、复杂查询等等。 ### SpringData JPA 需不需要JPA提供程序? 需要 Spring Data JPA 可以理解为 JPA 规范的再次封装抽象,底层还是使用了 Hibernate 的 JPA 技术实现。 ### JPA个各个属性都有什么作用? ```properties # 指定是否在控制台打印生成的sql语句,以方便跟踪程序执行和调试程序,默认值为false spring.jpa.show-sql=true # 指定是否格式化在控制台打印的sq|语句 spring.jpa.properties.hibernate.format_sql=true # 指定使用的方言,是一个类的名称,根据操作的数据库选择 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57InnoDBDialect # 指定程序运行时是否自动生成、更新或删除数据库表 spring.jpa.hibernate.ddl-auto=update ``` ddl-auto的有效值有: - create: 每次加载hibernate时,先删除已存在的数据库表结构再重新生成 - create-drop:每次加载hibernate时,先删除已存在的数据库表结构再重新生成,并且当sessionFactory关闭时自动删除生成的数据库表结构: - update: 只在第一次加载hibernate时自动生成数据库表结构,以后再次加载hibernate时根据 model类自动更新表结构 - validate:每次加载hibermate时,验证数据库表结构,仅仅和数据库中的表进行比较,不会 创建新表,但是会插入新值 - none:禁用DDL操作 ### 有哪些常用的注解? - @Entity定义对象将会成为被JPA管理的实体,将映射到指定的数据库表。 - @Table指定数据库的表名。 - @Id定义属性为数据库的主键,一个实体里面必须有一个。 - @IdClass利用外部类的联合主键。 - @GeneratedValue为主键生成策略 - @Basic表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为@Basic。 - @Transient表示该属性并非一个到数据库表的字段的映射,表示非持久化属性,与@Basic作用相反。JPA映射数据库的时候忽略它。 - @Column定义该属性对应数据库中的列名。 - @Temporal用来设置Date类型的属性映射到对应精度的字段。 - @Lob 将属性映射成数据库支持的大对象类型,支持以下两种数据库类型的字段。 关联关系注解 - @JoinColumn定义外键关联的字段名称 - @OneToOne关联关系 - @OneToMany与@ManyToOne可以相对存在,也可只存在一方。 - @ManyToMany表示多对多,和@OneToOne、@ManyToOne一样也有单向、双向之分。单向双向和注解没有关系,只看实体类之间是否相互引用。 ### Repository接口和他的派生类型? **Repository**: 是 spring Data 的一个核心接口,它不提供任何方法,开发者需要在自己定义的接口中声明需要的方法 仅仅是一个标识,表明任何继承它的均为仓库接口类,方便Spring自动扫描识别 **CrudRepository**: 继承 Repository,实现了一组 CRUD 相关的方法 **PagingAndSortingRepository**: 继承 CrudRepository,实现了一组分页排序相关的方法 **JpaRepository**: 继承 PagingAndSortingRepository,实现一组 JPA 规范相关的方法 ### 如何构建简单的条件查询? ```java //resposity List<Student> findByUserDepUuid(String uuid); //框架在解析该方法时,流程如下: /* 首先剔除 findBy,然后对剩下的属性进行解析,假设查询实体为Doc 先判断 userDepUuid(根据 POJO 规范,首字母变为小写)是否为查询实体的一个属性, 如果是,则表示根据该属性进行查询;如果没有该属性,继续往下走 从右往左截取第一个大写字母开头的字符串(此处为Uuid), 然后检查剩下的字符串是否为查询实体的一个属性,如果是, 则表示根据该属性进行查询;如果没有该属性,则重复这一步,继续从右往左截取; 最后假设 user 为查询实体的一个属性 接着处理剩下部分(DepUuid), 先判断 user 所对应的类型是否有depUuid属性, 如果有,则表示该方法最终是根据 "Doc.user.depUuid" 的取值进行查询;否则继续按照步骤3的规则从右往左截取, 最终表示根据 "Doc.user.dep.uuid" 的值进行查询。 */ //以下是条件查询中可用的关键字 //5. 构建条件查询-根据性别和班级查询学生列表 //如果多个参数,则参数顺序和方法名称要对应起来,比如这里第一个参数必须是班级 List<Student> findBySbatchAndSgender(String batchName, String gender); //6. 构建条件查询-根据学号或姓名查询学生 List<Student> findBySidOrSname(String id, String name); //7. 构建条件查询-根据两个学号查询学号介于该区间的学生的列表 List<Student> findBySidBetween(String start, String end); //8. 构建条件查询-查询学号小于指定学号的学生列表 List<Student> findBySidLessThan(String id); //9. 构建条件查询-查询学号小于等于指定学号的学生列表 List<Student> findBySidLessThanEqual(String id); //10. 构建条件查询-查询学号大于指定学号的学生列表 List<Student> findBySidGreaterThan(String id); //11. 构建条件查询-查询学号大于等于指定学号的学生列表 List<Student> findBySidGreaterThanEqual(String id); //12. 构建条件查询-查询班级为null的学生的列表 List<Student> findBySbatchNull(); //13. 构建条件查询-查询学号包含指定关键字的学生列表 List<Student> findBySidLike(String id); //14. 构建条件查询-查询名字包含指定关键字的学生列表 List<Student> findBySnameLike(String name); //15. 构建条件查询-查询名字以指定字符串结尾的学生列表 List<Student> findBySnameEndingWith(String name); ``` ```java //server //根据主键查找查找时 public Product get(Long id) { //findById()返回的是一个Optional对象,需要进一步调用get()返回实际数据对象 return repo.findById(id).get(); } @Override public List<Student> getStudentListByUuid(String Uuid) { return studentRepository.findByUuid(Uuid); } ``` ### 如何使用@Query查询? ```java // 1. 查询id最大的员工的信息 // 使用@Query注解定义JPQL查询 // JPQL(Java Persistence Querying Language-Java持久化语言)或者叫做JPA Querying // Language-操作的是对象和对象的属性 // Native Query-本地/原生SQL语句-操作的是表和表中的列 // select emp相当于查询员工对象的所有属性,emp.属性名 表示查询对应的属性 @Query("select emp from Employee emp where id=(select max(id) from Employee)") Employee query1(); // 2. 根据性别和部门编号查询员工列表 // 使用占位符参数 ?X 表示查询参数 // Employee类中通过dept属性映射了关系,但是Employee类中没有deptId属性 // 所以如果需要使用deptId属性,需要写dept.deptId @Query("select emp from Employee emp where gender=?1 and dept.deptId=?2") List<Employee> query2(String gender, Integer deptId); // 3. 根据性别和部门编号查询员工列表 // 使用命名参数 :参数名 // 可通过@Param注解指定查询参数的名称 @Query("select emp from Employee emp where gender=:gen and dept.deptId=:did") List<Employee> query3(@Param("gen") String gender, @Param("did") Integer deptId); // 4. 查询姓名包含指定关键字和指定性别的员工 // Spring Data支持在占位符参数两端添加%的使用 @Query("select emp from Employee emp where name like %?1% and gender=?2") List<Employee> query4(String name, String gender); // 5. 查询姓名包含指定关键字和指定性别的员工 // Spring Data支持在命名参数两端添加%的使用 @Query("select emp from Employee emp where name like %:name% and gender=:gender") List<Employee> query5(@Param("name") String name, @Param("gender") String gender); // 6. 查询所有员工 // JPQL: 操作的是对象和对象的属性 // 原生SQL: 操作的是表和表中的列 // 使用本地查询/原生SQL语句,需要设置nativeQuery属性为true // Spring Data JPA不仅仅支持JPQL语句,也支持原生SQL语句 @Query(value = "select * from emp", nativeQuery = true) List<Employee> query6(); // 7. 联接表查询指定列 // 查询指定列,可以考虑将每行数据封装到一个Object[]数组 @Query("select e.id,e.name,e.gender,d.deptId,d.deptName from Employee e, Department d where e.dept.deptId=d.deptId") List<Object[]> query7(); ``` ### 如何进行增删改? 直接调用JPA给定的 CRUD 方法 ```java @Override public void addStudent(Student student) { studentRepository.save(student); } @Override public void updateStudent(Student student) { studentRepository.save(student); } @Override public void deleteStudent(String id) { studentRepository.deleteById(id); } ``` 或者在repository层中定义Query ```java // 8. 修改指定员工的姓名 // 需要使用@Modifying注解和@Transactional注解 // @Query("select emp from Employee emp") // 如果执行的是一个增删改操作,则必须添加@Modifying和@Transactional注解的使用 // @Transactional注解建议加到Service层的方法上,有时候一个Service功能可能涉及多个Dao的调用,需要作为一个事务执行 @Modifying @Query("update Employee set name=:name where id=:id") void update1(@Param("id") Integer id, @Param("name") String name); // 9. 添加一个新员工 // 需要使用@Modifying注解和@Transactional注解 // JPQL不支持insert语句 @Modifying @Query(value = "insert into emp(name,gender,dept_id) values(:name,:gender,:deptId)", nativeQuery = true) void insert(@Param("name") String name, @Param("gender") String gender, @Param("deptId") Integer deptId); // 10. 刪除一个员工 // 需要使用@Modifying注解和@Transactional注解 @Modifying @Query("delete Employee where id=?1") void delete(Integer id); ``` ### 如何声明事务? ```java //例 @Transactional(propagation=Propagation.SUPPORTS, readOnly=true) public List<PayInfo> findAll() { return payInfoDao.findAll(); } ``` | 属性 | 类型 | 描述 | |-------------------------|------------------------------|----------------------| | value | String | 可选的限定描述符,指定使用的事务管理器 | | propagation | enum: Propagation | 可选的事务传播行为设置 | | isolation | enum: Isolation | 可选的事务隔离级别设置 | | readOnly | boolean | 读写或只读事务,默认读写 | | timeout | int (in seconds granularity) | 事务超时时间设置 | | rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 | | rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 | | noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 | | noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 | ### 如何配置mapper映射文件的位置? 在配置文件中 ```properties mybatis.mapper-locations=classpath:com/qdu/mapper/**/*.xml ``` ### 如何开启对mapper包的扫描? 在启动类中 ```java @MapperScan("com.qdu.mapper") ``` ## 消息传递 10问 ### 消息传递的概念? 消息传递是一个或多个实体之间进行通信的一-种方式,它无处不在。自计算机发明以来,以一种或 另一种形式进行的计算机消息传递就已经存在。它被定义为硬件和/或软件组件或应用程序之间的通 信方法。总会有一个发送者和一个或多个接收者。消息传递可以是同步和异步,发布/订阅, RPC 基于企业的消息传递,ESB (企业服务总线),MOM (面向消息的中间件)等等。 ### JMS的概念? JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。 ### 点对点模型有什么特点? 1. 一个消息由一个<b>消息生产者(producer)</b>传送给一个<b>消息使用者(consumer)</b>。</a></li> 2. 消息由生产者发送到<b>队列(Queue)</b>目的地,然后传送给在该队列上注册了的<b>消息使用者之一 </b>。</a></li> 3. 任意多数量的消息生产者都可以发送消息同一个队列,每条消息都可以确保被传送成功,且<b>每条消息仅由一个消息使用者收到和使用</b>。</a></li> 4. 如果没有消息使用者注册接受队列中的消息,则<b>队列保留该消息</b>,直到有使用者读取该消息,一旦读取,该消息便不在队列中,不可再有其他使用者读取。</a></li> ### 发布订阅模型的特点? 1. 一个消息由一个<b>消息生产者(Producer/Publisher)</b>传送给<b>任意数量的消息使用者(Consumer/Subscriber)</b>。</a></li> 2. 消息由生产者发送到<b>主题(Topic)</b>目的地, 然后由<b>订阅了该主题的活跃消息使用者读取和使用</b>。</a></li> 3. 任意数量的消息生产者可以发送消息到<b>主题</b>目的地,<b>每条消息</b>传送给<b>任意数量订阅了该主题的消息使用者</b>。</a></li> 4. 如果没有订阅该主题的消息使用者,则Topic目的地不会保留该消息(除非有非活跃使用者进行了持久性订阅),后续订阅该主题的消息使用者也不会再收到使用者之前发的消息。</a></li> 5. 一个持久性订阅表示注册了该主题的使用者可以在生产者发送消息的时候处于非活跃状态,这样,使用者可以在变成活跃状态时收到之前发送的消息。</a></li> ### ActiveMQ是什么? ActiveMQ是一种开源的基于JMS(Java Message Servie)规范的一种消息中间件的实现,ActiveMQ的设计目标是提供标准的、面向消息的、能够跨越多语言和多系统的应用集成消息通信中间件。 它为企业应用中消息传递提供高可用、出色性能、可扩展、稳定和安全保障。 ActiveMQ实现JMS规范并在此之上提供大量额外的特性。ActiveMQ支持队列和订阅两种模式的消息发送。 ### 如何启动ActiveMQ? ```shell cd <activeMQ_dir>/bin ./activemq start ``` ### JMS创建什么链接实例? `javax.jms.Connection` (由于使用SpringBoot,此处不再赘述) ### 如何配置ActiveMQ? 首先配置properties文件 ```properties #activemq通讯地址 spring.activemq.broker-url=tcp://localhost:61616 #用户名 spring.activemq.user=admin #密码 spring.activemq.password=admin #是否启用内存模型(就是不安装mq,项目启动时同时启动一个mq实例) spring.activemq.in-memory=false #信任所有包 spring.activemq.packages.trust-all=true #是否替换默认的连接池,使用activemq的连接池需引入依赖 spring.activemq.pool.enabled=false ``` 然后配置activeMQ ```java @Configuration @EnableJms public class ActiveMQConfig { @Bean public Queue queue() { return new ActiveMQQueue("springboot.queue") ; } //springboot默认只配置queue类型消息,如果要使用topic类型的消息,则需要配置该bean @Bean public JmsListenerContainerFactory jmsTopicListenerContainerFactory(ConnectionFactory connectionFactory){ DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); //这里必须设置为true,false则表示是queue类型 factory.setPubSubDomain(true); return factory; } @Bean public Topic topic() { return new ActiveMQTopic("springboot.topic") ; } } ``` 或者在properties中配置 ```propeties # 使用点对点模型 spring.jms.pub-sub-domain=false ``` 配置文件的其他配置: ```properties # 是否信任所有包 spring.activemq.packages.trust-all= # 要信任的特定包的逗号分隔列表(当不信任所有包时) spring.activemq.packages.trusted= # 当连接请求和池满时是否阻塞。设置false会抛“JMSException异常”。 spring.activemq.pool.block-if-full=true # 如果池仍然满,则在抛出异常前阻塞时间。 spring.activemq.pool.block-if-full-timeout=-1ms # 是否在启动时创建连接。可以在启动时用于加热池。 spring.activemq.pool.create-connection-on-startup=true # 是否用Pooledconnectionfactory代替普通的ConnectionFactory。 spring.activemq.pool.enabled=false # 连接过期超时。 spring.activemq.pool.expiry-timeout=0ms # 连接空闲超时 spring.activemq.pool.idle-timeout=30s # 连接池最大连接数 spring.activemq.pool.max-connections=1 # 每个连接的有效会话的最大数目。 spring.activemq.pool.maximum-active-session-per-connection=500 # 当有"JMSException"时尝试重新连接 spring.activemq.pool.reconnect-on-exception=true # 在空闲连接清除线程之间运行的时间。当为负数时,没有空闲连接驱逐线程运行。 spring.activemq.pool.time-between-expiration-check=-1ms # 是否只使用一个MessageProducer spring.activemq.pool.use-anonymous-producers=true ``` ### @JMSListener有什么用? ```java //该注解用于将一个方法设置为监听端点,用于接收消息 //destination用于指定接收消息的目的地,也就是消息来自哪个目的地,给出其名称 @JmsListener(destination="queue2") public void receiveMsg1(String msg) { System.out.println("监听端点1收到消息,消息内容:"+msg); } ``` ### 如何使用发布订阅模型? ```properties # 设置为ture使用发布订阅模型, i.e spring.jms.pub-sub-domain=true ``` ## 测试 9问 ### 单元测试与集成测试? 见[软件工程第十一章单元测试与集成测试部分](https://charlesix59.github.io/2023/02/07/subject/software_project/chapter11/#%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95) ### Junit5的三个组件是什么? JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage - JUnit Platform: 是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。 - JUnit Jupiter: 提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。 - JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,其提供了兼容JUnit4.x,Junit3.x的测试引擎。 ### Junit的常用注解? @Test :表示方法是测试方法 ```java @Test void contextLoads() { } ``` @DisplayName :为测试类或者测试方法设置展示名称 ```java @DisplayName("测试displayname注解") @Test void testDisplayName() { System.out.println(1); } ``` @BeforeEach:表示在每个单元测试之前执行 @AfterEach:表示在每个单元测试之后执行 @BeforeAll:表示在所有单元测试之前执行 @AfterAll:表示在所有单元测试之后执行 ```java @BeforeEach void testBeforeEach() { System.out.println("测试就要开始。。。"); } @AfterEach void testAfterEach() { System.out.println("测试就要结束。。。"); } @BeforeAll static void testBeforeAll() { System.out.println("所有测试就要开始。。。"); } @AfterAll static void testAfterAll() { System.out.println("所有测试已经结束。。。"); } ``` @Disabled :表示测试类或测试方法不执行 ```java @Disabled @Test void test2() { System.out.println(2); System.out.println(jdbcTemplate.getClass()); } ``` @Timeout :表示测试方法运行如果超过了指定时间将会返回错误 ```java @Test @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) void testTimeOut() throws InterruptedException { Thread.sleep(520); } ``` @RepeatedTest :表示方法可重复执行 ```java @RepeatedTest(5) //重复测试5次 @DisplayName("测试3") @Test void test3() { System.out.println(3); System.out.println(jdbcTemplate.getClass()); } ``` @ParameterizedTest :表示方法是参数化测试 @Tag :表示单元测试类别 @ExtendWith :为测试类或测试方法提供扩展类引用 ### Junit的测试方法与注意事项? 就这么测。。。 注意注意就行了。。。 ### Test依赖包含的库有哪些? ![img.png](./ssm/dependencies.png) ### 如何测试Spring Boot程序? @SpringBootTest注解:添加在需要依赖spring boot框架的测试类上, 不然不能使用Spring boot的相关开发功能 @Test注解:添加在测试方法上 ```java @SpringBootTest class Boot05WebAdminApplicationTests { @Test void contextLoads() { } } ``` ### MockMvc是什么? MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。 ### @WebMvcTest有什么用? ```java //该注解的使用实现针对spring mvc组件的切片测试,这样只加载mvc相关的配置的即可 @WebMvcTest(controllers= {HelloController.class}) ``` 如果使用@SpringBootTest来修饰一个测试类, 则会加载所有配置,并注册相关的bean成为spring管理的bean, 包括service组件、mapper组件、数据源等 但是如果测试只涉及部分组件,如控制器,不需要使用service或mapper等组件, 可以使用切片测试对应的注解来实施测试,这样只加载需要的配置即可 比如使用@WebMvcTest,实现对spring mvc组件的切片测试, 这样就不能注入service等组件,因为不会加载service等的配置

操作系统复习 第一章

# 第一章 引论 **操作系统:** - 目标: - 方便性 - 有效性 - 提高系统资源利用率 - 提高系统吞吐量 - 可拓展性 - 开放性 - 作用: - 作为用户与计算机硬件系统之间的接口 - 程序接口 - 命令接口 - GUI - 作为计算机系统资源的管理者 - 处理机管理 - 存储器管理 - I/O设备管理 - 文件管理 - 实现了对计算机资源的抽象 - 将具体的计算机硬件资源抽象为软件资源,方便用户使用和拓展 - 开放了简单的访问方式,隐藏了实现细节 - 发展动力 - 不断提高计算机资源利用率 - 方便用户 - 器件的不断更新迭代 - 计算机体系结构的不断发展 - 不断提出新的应用需求 ## 计算机的发展历程 ### 人工操作方式 - 优点 - 用户独占全机 - CPU等待人工操作 - 缺点 - 计算机资源浪费 - 效率低 - CPU与I/O设备之间速度不匹配 ### 脱机输入输出方式 - 优点 - 解决了人际矛盾 - 减少了CPU的空闲时间 - 提高了I/O速度 - 缺点 - 一次只能执行一个程序 ### 单道批处理系统 批处理是指计算机系统对一批作业自动进行处理的一种技术。 为实现对作业的连续处理,需要先把一批作业以脱机方式输入到磁带上,并在系统中配上监督程序(Monitor) ,在它的控制下,使这批作业能一个接一地连续处理 - 优点 - 自动性 - 顺序性 - 单道性 - 缺点 - 内存中只有一道程序 - CPU需要等待I/O完成 ### 多道批处理系统 在多道批处理系统中,用户所提交的作业都先存放在外存上并排成一个队列,称为“后备队列”; 然后,由作业调度程序按一定的算法从后备队列中选择若干个作业调入内存,使它们共享CPU和系统中的各种资源。 - 优点 - 提高CPU的利用率 - 可提高内存I/O设备利用率 - 增加系统吞吐量 - 缺点 - 平均周转周期长 - 无人机交互 ### 分时系统 采用时间片轮转的方法,同时为许多终端用户服务,对每个用户能保证足够快的响应时间,并提供交互会话的功能。 时间片:将CPU的时间划分成若干个片段,称为时间片,操作系统以时间片为单位,轮流为每个终端用户服务关键问题 - 特征 - 多路性:时间片轮转机制 - 独立性:用户彼此独立 - 及时性:用户能在短时间内获得响应 - 交互性:用户可以请求多种服务 #### 实时任务 - 按照是否呈现周期性划分 - 周期性实时任务 - 非周期性实时任务 - 开始截止时间 - 完成截止时间 - 对截止时间的要求的划分 - 硬实时任务 - 软实时任务 ### 微机操作系统 微型计算机操作系统 微型计算机操作系统简称微机操作系统,常用的有Windows、Mac OS、Linux。 ## 基本特征 ### <mark>并发性</mark> **并行性**是指两个或多个事件在同一时间发生 **并发性**是指两个或多个事件在同一事件间隔发生 - 宏观上,处理机同时处理多道程序 - 微观上,处理机在多道程序间高速切换 **进程**( process )是指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。 **线程**( threads )通常在一个进程中可以包含若干个线程, 一般把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。 ### <mark>共享性</mark> 资源共享,即系统中的资源多个“并发执行”的应用程序共同使用 - 互斥共享方式:多个程序在同一个共享资源上独立而互不干扰的工作 - 又叫临界资源/独占资源 - 同时访问方式:同一时段允许多个程序同时访问共享资源 并发和共享互为存在条件 - 共享性要求OS中同时运行着多道程序若只有单道程序正在运行,则不存在共享的可能 - 并发性难以避免的导致多道程序同时访问同一个资源,若多道程序无法共享部分资源(比如磁盘) ,则无法并发 ### 虚拟性 使用某种技术把一个物理尸体变成多个逻辑上的对应物 - 时分复用技术( TDM , Time Division Multiplexing ) - 虚拟处理机技术:利用多道程序设计技术,为每道程序建立一个进程,让多道程序并发执行,以此来分时使用一台处理机。 - 虚拟设备技术:将一台物理I/O设备虚拟为多台逻辑上的I/O设备,并允许每个用户占用一台逻辑上的I/O设备。 - 空分复用技术( SDM , Space Division Multiplexing ) - 虚拟存储器技术 ### 异步性 - 多道程序环境下,允许多个程序并发执行;单处理机环境下,多个程序分时交替执行; - 程序执行的不可预知性 - 获得运行的时机 - 因何暂停 - 每道程序需要多少时间 - 不同程序的性能,比如计算多少, I/O多少 - 宏观上“一气呵成”,微观上“走走停停” - 每个程序在何时执行,多个程序间的执行顺序以及完成每道程序所需的时间都是不确 定和不可预知的。进程是以人们不可预知的速度向前推进,此即进程的异步性。 ## 主要功能 ### 处理机管理 - **进程控制**:当用户作业要运行时,应为之建立一个或多个进程,并为它分配除处理机以外的所有资源,将它放入进程就绪队列。当进程运行完成时,立即撤消该进程,以便及时释放其所占有的资源。进程控制的基本功能就是创建和撤消进程以及控制进程的状态转换。 - **进程同步**:所谓进程同步是指系统对并发执行的进程进行协调。 - 进程互斥方式,是使诸进程以互斥方式访问临界资源。 - 进程同步方式,对于彼此相互合作去完成共同任务的诸进程,则应由系统对它们的运行次序加以协调。 - **进程通信**:对于相互合作的进程,在它们运行时,相互之间往往要交换一定的信息,这种进程间所进行的信息交换称为进程通信。 - **调度**:当一个正在执行的进程已经完成,或因某事件而无法继续执行时,系统应进行进程调度,重新分配处理机。 - 作业调度是从后备队列中按照一定的算法,选择出若干个作业,为它们 分配运行所需的资源。 - 进程调度是指按一定算法,从进程就绪队列中选出一进程,把处理机分 配给它,为该进程设置运行现场。并使之投入运行。 ### 存储器管理 - 内存分配:主要任务是: - 为每道程序分配内存空间,使它们"各得其所"。 - 提高存储器的利用率,尽量减少不可用的内存空间(碎片)。 - 允许正在运行的程序申请附加的内存空间,以适应程序和数据动态增长的需要。 - 内存分配方式 - 静态分配 - 动态分配 - 内存保护:为保证各道程序都能在自己的内存空间运行而互不干扰,要求每道程序在执行时能随时检查对内存的所有访问是否合法。必须防止因一道程序的错误而扰乱了其它程序,尤其应防止用户程序侵犯操作系统的内存区。 - 内存保护机制:设置两个寄存器,存放正在执行程序的上下界。 - 地址映射: -个应用程序(源程序)经编译后,通常会形成若干个目标程序;这些目标程序再经过链接便形成了可装入程序。这些程序的地址都是从"0"开始的,程序中的其它地址都是相对于起始地址计算的; - 在多道程序环境下,每道程序不可能都从"0"地址开始装入(内存),这就致使地址空间内的逻辑地址和内存空间中的物理地址不相一-致。 - 使程序能正确运行,存储器管理必须提供地址映射功能,以将地址空间中的逻辑地址转换为内存空间中与之对应的物理地址。 - 内存扩充:并非是去扩大物理内存的容量,而是借助于虚拟存储技术,从逻辑上去扩充内存容量,使用户所感觉到的内存容量比实际内存容量大得多;或者是让更多的用户程序能并发运行。 - 请求调入功能。 - 置换功能。 - 缓冲管理:几乎所有的外围设备于处理机交换信息时,都要利用缓冲来缓和CPU和I/O设备间速度不匹配的矛盾,和提高CPU与设备、设备与设备间操作的并行程度,以提高CPU和I/O设备的利用率。 - 最常见的缓冲区机制有单缓冲机制、能实现双向同时传送数据的双缓冲机制,以及能供多个设备同时使用的公用缓冲池机制。 - 设备分配:系统根据用户所请求的设备类型和所采用的分配算法对设备进行分配,并将未获得所需设备的进程放进相应设备的等待队列。 - 设备处理程序又称为设备驱动程序。其基本任务是用于实现CPU和设备控制器之间的通信,即由CPU向设备控制器发出I/O命令,要求它完成指定的I/0操作;反之由CPU接收从控制器发来的中断请求,并给予迅速的响应和相应的处理。 ### 设备管理 - 设备管理的主要任务: - 为用户程序分配I/0设备;完成用户程序请求的I/O操作; - 提高CPU和I/O设备的利用率;方便用户使用。 ### 文件管理 - 文件存储空间的管理 - 目录管理 - 文件读写管理和保护 ### 操作系统与用户之间的接口 - 用户接口 - 联机用户接口 - 为联机用户提供的,它由一组键盘操作命令及命令解释程序所组成。用户可通过先后键入不同的命令方式来实现对作业的控制。 - 脱机用户接口 - 该接口是为批处理作业的用户提供的,故也称为批处理用户接口。由一组作业控制语言组成。用户用JCL把需要对作业进行的控制和干预事先写在作业说明书上,然后将作业连同作业说明书一起提供给系统。 - 图形用户接口 - 采用图形化的操作界面,用各种图表将系统的各项功能、文件等,直观的表现出来。用户直接用鼠标对应用程序和文件进行操作。 - 程序接口 - 由一组系统调用组成,每一个系统调用都是一个能完成特定功能的子程序,每当应用程序要求OS提供某种服务时,便调用具有相应功能的系统调用。 ## 运行环境 <mark>内核态(管态)和用户态(目态)将内核程序和用户程序隔离</mark> - 内核态 - 操作系统的程序执行 - 使用全部指令 - 使用全部系统资源 - 用户态 - 用户程序执行 - 禁止使用特权指令 - 只允许用户程序访问自己的存储区域 特权指令和非特权指令 - 特权指令 - 设计外部设备的输入/输出指令 - 存取用于内存保护的寄存器 - 内存清零 - 置时钟 - 允许/禁用中断 <mark>中断指令</mark>是用户程序发起的调用内核代码的唯一方式 - 中断机制 - 提高多道程序环境下CPU利用率 - 外中断:中断信号来源于外部设备 - 内中断:中断信号来源于当前指令内 - 内中断的三种情况 - 陷阱/陷入:由应用程序主动引发 - 故障:由错误条件引发 - 终止:由致命错误引发 - 系统调用的核心 - 用户程序中包含一段含有int指令的代码 - 操作系统写中断处理,获取想调用程序的编号 - int指令将使CPL改成0 ,“进入内核” - 操作系统根据编号执行相应代码 -

操作系统复习 第二章

# 第二章 进程的描述与控制 ## 程序的执行顺序 ### 程序的顺序执行 一个具有独立功能的程序独占处理机,直到得到最终结果的过程 #### 前驱图 一个有向无环图(DAG),用于描述进程间执行的前后的关系 - 节点:表示一个程序段或进程,或一条语句 - 有向边:节点之间的偏序或前驱关系 - 初始节点:没有前驱关系的点 - 终止节点:没有后继的节点 程序顺序执行的特性: - 顺序性:处理机的操作严格按照顺序进行 - 封闭性:程序一旦开始执行,结果不受外因影响 - 可再现性:程序结果与执行速度无关,只与初始条件有关 ### 程序的并发执行 内存中同时装入多道程序,他们共享系统资源,并发执行 特性: - 间断性:程序并发执行时,由于它们共享资源或程序之间相互合作完成-项共同任务,因而使程序之间相互制约。相互制约导致并发程序具有"执行一暂停-执行”这种间断性的活动规律。 - 失去封闭性: 程序在并发执行时,多个程序共享资源,因而资源的状态将由多个程序来改变,致使程序的运行失去了封闭性。 - 不可再现性:程序在并发执行时,多次运行初始条件相同的同-一程序会得出不同的运行结果。 ## 进程 - 定义: - 程序段、数据段、PCB三部分构成了进程实体,简称"进程”。 - 注意: - 进程是[程序J的「一次执行J - 进程是一个程序及其数据在处理机上顺序执行时所发生的「活动」 - 进程是程序在一个「数据集合J」运行的过程 - 进程是系统进行「资源分配和调度J的- -个「独立」单位(或者说基本单位) - 特征 - 动态性:进程的实质是程序的一次执行过程 - 进程是动态产生,动态消亡的,进程在其生命周期内,在3种基本状态之间转换 - 并发性:任何进程都可以同其他进程一起向前推进 - 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位 - 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进 - 三种状态 - 就绪状态(Ready) - 进程已获得除CPU之外的所有必需的资源,一旦得到CPU控制权,立即可以运行 - 运行状态(Running) - 进程已获得运行所必需的资源,它正在处理机上执行。 - 阻塞状态(Blocked) - 正在执行的进程由于发生某事件而暂时无法执行时,便放弃处理机而处于暂停状态,称该进程处于阻塞状态或等待状态。 - 其他状态 - 创建(New) - 为了保证进程的调度必须在创建工作完成后进行,同时为了增加管理的灵活性, OS可以根据系统性能或主存容量的限制,推迟创建状态进程的提交,产生进程的创建状态。 - 终止( Terminated ) - 进程到达自然结束点、出现无法克服的错误、被操作系统所终结 - 等待操作系统善后处理,将其PCB清零,将PCB空间返还系统 - 挂起 - 有的系统有时希望能人为地把进程挂起,使之处于静止状态,以便研究其执行情况或对它进行修改。 - 挂起:把进程从内存转向外存 - 引入挂起操作的原因 - 终端用户的请求。 - 父进程请求。 - 负荷调节的需要。 - 操作系统的需要。 - 激活 - 对应挂起 - ![](../../../../assets/default.png) ### 进程管理中的数据结构 - 操作系统中,对每个资源和每个进程都设置了-个数据结构,用于表征其实体,称之为:资源信息表和进程信息表。 - OS管理的这些数据结构分为四类:内存表、设备表、文件表、进程表。 ### 进程控制块 PCB PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据) ,成为一个能独立运行的基本单位, 一个能与其他进程并发执行的进程 <mark>PCB是进程存在的唯一标志</mark> - 作用 - 作为独立运行基本单位的标志。 - 能实现间断性运行方式。 - 提供进程管理所需要的信息。 - 提供进程调度所需要的信息。 - 实现与其它进程的同步与通信。 - 内容 - | 类型 | 内容 | 作用 | | ------------ | --------------------------------------------------------- | ---------------- | | 标识信息 | 1 )外部标识为方便用户<br/>2 )内部标识为方便系统 | 标识一个进程 | | 处理机状态(现场信息 ) | 1 ) CPU通用/指令寄存器<br/>2 ) CPU程序状态字PSW<br/>3 )用户栈指针 | 记录处理机现场信息,以备恢复之用 | | 调度信息 | 1 )进程状态<br/>2 )进程优先级<br/>3 )调度所需信息<br/>4)事件 | 用户进程的调度管理 | | 控制信息 | 1 )程序和数据地址<br/>2 )进程同步和通信机制<br/>3 )资源清单<br/>4 )指向下一个进程的指针 | 用于进程的控制管理 | - 组织方式 - 线性方式:即将系统中所有的PCB都组织在一张线性表中,将该表的首址存放在内存的一个专用区域中。该方式实现简单、开销小,但每次查找时都需要扫描整张表,因此适合进程数目不多的系统 - 链接方式:把具有同一状态的PCB用其中的链接字链接成一个队列。 - 就绪队列 - 若干个堵塞队列 - 索引方式:系统根据所有进程的状态建立几张索引表,把各表的内存首地址记录在内存的专用单元中。索引表的表目中记录了相应状态的某个PCB在PCB表中的地址。 ## 进程控制 进程控制是对系统中所有进程从产生、存在到消亡的全过程实行有效的管理和控制。 进程控制一般是由操作系统的内核来实现,内核在执行操作时,往往是通过执行各种原语操作来实现的。 ### 操作系统内核 - 功能 - 支撑功能 - 中断管理 - 时钟管理 - 计时 - 时钟中断 - 原语操作 - 由若干条指令组成 - 用来完成某个特定功能 - 执行过程不会中断 - 资源管理功能 - 进程管理 - 存储器管理 - 设备管理 - 进程控制 - 原语种类 - 进程创建原语 - 进程撤销原语 - 阻塞原语 - 唤醒原语 - 挂起原语 - 激活原语 - 控制过程 - 更新PCB中的信息(如修改进程状态标识、将运行环境保存到PCB、从 PCB恢复运行环境) ➢所有的进程控制原语一定会修改进程状态标志 ➢剥夺当前运行进程的CPU使用权必然需要保存其运行环境(玩游戏的存档) ➢某进程开始运行钱必然要恢复运行环境(玩游戏的读档) - 将PCB插入合适的队列 - 分配/回收资源 ### 进程的创建 - 层次结构 - 在OS中允许一个进程创建另-个进程,通常把创建进程的进程称为父进程,而把被创建的进程称为子进程。 - 在UNIX中子进程可继续创建更多的孙进程,由此便形成了- -个进程的层次 结构。 - 子进程可以继承父进程所拥有的资源, - 当子进程被撤消时,应将其从父进程那里获得的资源归还给父进程。 - 在撤消父进程时,也必须同时撤消其所有的子进程。 - Windows中不存在任何进程层次结构的概念 - 引起创建进程的事件 - 用户登录 - 作业调度 - 提供服务 - 应用请求 - 创建过程 - 申请进程标识,即申请空白PCB - 为新进程分配内存和其它资源 - 初始化进程控制块 - 将创建的进程置于就绪队列 ### 进程的终止 - 引起进程终止的事件 - 正常: - 任务完成=> halt/ logs off指令=>中断 - 异常: - 访问控制(存储越界、资源访问越界、指令越界) - 运行超时、等待超时 - 被禁止的运算 - I/O错误等 - 外界干预: - OS或用户干预 - 父进程请求 - 父进程终止 - 终止过程 - 根据被终止进程的标识符,从PCB集合中检索出该进程的PCB ,从中读出该进程的状态 - 若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度; - 若该进程还有子孙进程,还应将其所有子孙进程也都予以终止,以防它们成为不可控的进程; - 将被终止进程所拥有的全部资源或者归还给其父进程,或者归还给系统; - 将被终止进程( PCB )从所在队列(或链表)中移出,等待其它程序来搜集信息。 ### 进程的阻塞与唤醒 - 引起的事件 - 请求资源失败(如临界资源被占用,属于内部同步) - 等待某种操作完成(如磁盘l/O操作,属于内部同步) - 等待新数据的到来(如多进程协作时,属于外部同步) - 等待新任务(如Deamon服务进程,属于外部同步) - 进程的阻塞( block原语) - 进程停止执行 - 状态改为“阻塞” - 进程被插入阻塞队列 - 进程的唤醒( wakeup原语) - 状态改为“就绪” - 进程被插入就绪队列 ### 进程的挂起与激活 #### 挂起 当出现了引起进程挂起的事件时,比如,用户进程请求将自己挂起,或父进程请求将自己的某个子进程挂起,系统将利用挂起原语suspend( )将指定进程或处于阻塞状态的进程挂起。 挂起原语的执行过程: - 检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪;对于活动阻塞状态的进程,则将之改为静止阻塞。 - 把该进程的PCB插入相应的挂起队列上,将程序段、数据段移除内存。最后,若被挂起的进程正在执行,则转向调度程序重新调度。 ### 激活 - 当发生激活进程的事件时,若该进程驻留在外存而内存中已有足够的空间时,则可将在外存上处于静止就绪状态的进程换入内存。 - 系统将利用激活原语active( )将指定进程激活。 - 激活过程: - 先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞便将之改为活动阻塞。 - 假如采用的是抢占调度策略,则每当有新进程进入就绪队列时,应检查是否要进行重新调度,即由调度程序将被激活进程与当前进程进行优先级的比较,如果被激活进程的优先级更低,就不必重新调度;否则,立即剥夺当前进程的运行,把处理机分配给刚被激活的进程。 ## 进程同步 **引入进程的好处**:支持多道程序并发,提高资源利用率 **引入进程的缺点**:系统更复杂,破坏了程序运行的封闭性和不可再现性 **引入进程同步**:对多个进程在执行次序上进行协调,使并发执行的进程之间能按照一定的规则共享系统资源,并能相互协作,使得程序的执行具有可再现性 - 两种形式的制约关系 - 间接相互制约:多个进程只能互斥执行 - 互斥:对某个系统资源,多个进程不能同时使用 - 临界资源:一段时间内只允许一个进程访问的资源 - 问题 - 死锁 - 直接相互制约:多个进程之间相互合作共同完成一件事 - 临界区:进程中访问临界资源的那段代码 - 访问临界区的程序设计为: - 对欲访问的临界资源进行检查, - 若此刻未被访问,设正在访问的标志——进入区 - 访问临界资源——临界区 - 将正在访问的标志恢复为未被访问的标志——退出区 - 其余部分——剩余区 - 同步机制应遵循的规则 - 空闲让进:当无进程在临界区时,任何有权使用临界区的进程可进入。 - 忙则等待:不允许两个以上的进程同时进入临界区。 - 有限等待:任何进入临界区的要求应在有限的时间内得到满足。 - 让权等待:处于等待状态的进程应放弃占用CPU ,以使其他进程有机会得到CPU的使用权。 ### 硬件同步机制 - 关中断法(开关中断指令)也称为"硬件锁”, 是实现互斥最简单的方法。 - 做法:在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断。这样,进程在临界区执行期间,计算机系统不响应中断,从而不会引发调度,也就不会发生进程或线程切换。由此,保证了对锁的测试和关锁操作的连续性和完整性,有效地保证了互斥。 - 缺点: - 滥用关中断权力可能导致严重后果; - 关中断时间过长,会影响系统效率,限制了处理器交叉执行程序的能力; - 关中断方法也不适用于多CPU系统,因为在一个处理器上关中断并不能防止进程在其它处理器上执行相同的临界段代码。 - 利用Test and Set指令实现互斥 - 做法:这是一种借助一条硬件指令一-‘ 测试并建立"指令TS(Test- and-Set)以实 现互斥的方法。在许多计算机中都提供了这种指令。 - 缺点: - 不符合“让权等待”原则 - 利用swap指令实现线程互斥 - 缺点: - 不符合”让权等待“原则 ### 信号量机制 1965年荷兰Dijkstra提出的信号量( Semaphores )是-种卓有成效的进程同步工具,在长期的应用中,得到了很大的发展,从整型信号量经过记录型信号量,进而发展为"信号量集”机制。 - 优点 - 信号量就是OS提供的管理公有资源的有效手段。 - 信号量代表可用资源实体的数量。 #### 整型信号量 - 定义:把整型信号量定义为-个用于表示资源数目的整型量S ,除初始化外仅能通过两个原子操作wait(S),signal(S)来访问 - 原子操作P : - 荷兰语"proberen"一-“检测” 之意。意味着请求分配一个单位资源 - wait(S) - 原子操作V : - 荷兰语“verhogen"一- "增量”之意,意味着释放一个单位资源 - signal(S) #### 记录型信号量 - 在信号量机制中,除了需要一个用于代表资源数目的整型变量value外 ,还应增加一个进程链表L,用于链接上述的所有等待进程。 - 记录型信号量是由于它采用”了记录型的数据结构而得名的。 #### AND型信号量 - AND同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。 - 只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源,也不分配给他。 - 在wait操作中,增加了一个"AND"条件,故称为AND同步,或称为同时wait操作。 #### 信号量集 - 一次申请多个单位的临界资源 - 资源数量低于预设下限值时不予分配 - ![](../../../../assets/default.png) #### 应用 - 利用信号量实现进程互斥 - 为使多个进程互斥的访问某临界资源,须为该资源设置一互斥信号量mutex ,并设其初始值为1 ,然后将各进程访问资源的临界区CS置于wait(mutex)和signal(mutex)之间即可。 - 利用信号量实现前驱关系 - 希望S1→S2 ,只需使进程P1和P2共享一个公用信号量S=0 ,将signal(S)放在语句S1后,将wait(S)放在语句S2前。 ### 管程 - 信号量的缺点 - 同步操作分散:信号量机制中,同步操作分散在各个进程中,使用不当就可能导致各进程死锁(如P、V操作的次序错误、重复或遗漏) ; - 易读性差:要了解对于一组共享变量及信号量的操作是否正确,必须通读整个系统 - 管程的优点 - 把分散在各进程中的临界区集中起来进行管理,并把系统中的共享资源用数据结构抽象地表示出来。由于临界区是访问共享资源的代码段,建立一个“秘书”程序管理来到的访问。"秘书” 每次仅让一个进程来访,这样既便于对共享资源的管理,又实现了互斥访问。 管程是由若干个公共变量和所有访问这些变量的过程所组成的一个特殊的模块或软件包 - 基本思想: - 集中管理各进程中的临界区:管程把分散在各个进程中互斥地访问公共变量的那些临界区集中起来管理。 - 特点 - 管程的局部变量只能由该管程的过程存取; - 系统保证进程只能互斥地调用管程中的过程。 - 条件变量 - 管程中对每个条件变量,都须予以说明, 其形式为: condition x, y。该变 量应置于wait和signal之前,即可表示为X.wait和X.signal。 - 特征 - 模块化: 一个管程是一个基本程序单位,可以单独编译; - 抽象数据类型:管程是一种特殊的数据类型,其中不仅有数据,而且有对数据进行操作的代码,是对数据和操作的封装。 - 信息掩蔽:管程如何实现其功能相对于外部程序是半透明的。 - 优点 - 安全性:共享变量外部不可见,只能由管程中的操作存取; - 互斥性:管程在任何一个时刻只能有一个进程进入; - 等待机制:设置有等待队列及相应的操作,对资源进行管理。 - 管程和进程的区别 - 设置目的不同:管程是对共享资源进行管理,进程是资源分配和执行的基本单位。 - 数据结构不同:管程定义公用数据结构,进程定义私有数据结构PCB。 - 存在方式不同:进程有生命周期,管程是操作系统固有的部分,没有生命周期。 - 执行方式不同:管程被进程调用,没有并发性,进程具有并发执行性。 ## 进程通信 ### 共享存储器系统 - 模式: - 共享数据结构 - 进程公用某些数据结构,借以实现诸进程间的信息交换。 - 实现:公用数据结构的设置及对进程间同步的处理,都是程序员的职责。操作系统提供共享存储器 - 特点:低效。只适合传递相对少量的数据。 - 共享存储区 - 在存储器中划出一块共享存储区,诸进程可通过对共享存储区中数据的读或写 来实现通信。 ### 管道通信系统 - 管道:指用于连接-个读进程和一个写进程以实现他们之间通信的一个打开的共享文件,又名pipe文件。 - 管道只能采取半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道各个进程要互斥的访问管道 - 数据以字节流的形式写入管道,当管道写满时,写进程的write()系统调用将会被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程的read()系统调用将会被阻塞 ### 消息传递系统 - 用格式化消息封装所需传输的数据,消息长度可以固定,也可以变化。 - 直接利用系统提供的一组通信命令(原语)进行通信。 - 操作系统隐藏了通信的实现细节,大大减化了通信程序编制的复杂性 - 应用广泛:微内核、多处理机系统、分布式系统、计算机网络等 - 消息传递系统的通信方式属于高级通信方式。又因其实现方式的不同而进一步分成 - 直接通信方式 - 发送进程利用OS提供的发送命令,直接把消息发送给目标进程。发送进程和接收进程都以显式方式提供对方的标识符。 - 通信原语: - Send(Receiver, message);发送一个消息给接收进程 - Receive(Sender, message);接收Sender发来的消息 - 间接通信方式。 - 信箱用来暂存发送进程发送给目标进程的消息,接收进程则从信箱中取出发送给自己的消息。 - 消息在信箱中可安全保存,只允许核准的目标用户随时读取 - 利用信箱通信方式,既可实时通信,又可非实时通信。 - 通信原语: - Send (MailBox, Message) ; - Receive (MailBox, Message) ; - 信箱可由操作系统创建,也可由用户进程创建,创建者是信箱的拥有者。 - 信箱分类: - 私用信箱 - 公用信箱 - 共享信箱 ### 客户-服务器系统 ## 线程 ### 线程的引入 - 进程的两个基本属性: - 进程是一个可拥有资源的独立单位 - 进程同时又是一个可独立调度和分派的基本单位。 - 为使程序能并发执行,系统还必须进行以下的一系列操作。 - 创建进程 - 撤消进程 - 进程切换 - 这样系统必须为之付出较大的时空开销。因此应分开进程的两个属性。即对于作为调度和分派的基本单位,不同时作为拥有资源的单位,以"轻装上阵”,反之亦然。 - 进程切换过程 - 切换页目录以使用新的地址空间 - 切换内核栈和硬件上下文 ### 线程 线程(thread)是一个可执行的实体单元。<mark>它代替以往的进程,成为现代操作系统中处理机调度的基本单位。</mark> - 特性 - 调度的基本单位 - 同一进程中的线程切换不会引起进程切换 - 不同进程中的线程切换必然引起进程切换 - 并发性 - 拥有资源 - 独立性 - 系统开销 - 支持多处理机系统 - 线程运行的三个状态 - 执行状态,表示线程正获得处理机而运行; - 就绪状态,指线程已具备了各种执行条件, 一旦获得CPU便可执行的状态; - 阻塞状态,指线程在执行中因某事件而受阻,处于暂停执行时的状态。 - 线程控制块TCB - 线程标识符; - 组寄存器,它包括程序计数器PC、状态寄存器和通用寄存器; - 线程运行状态,用于描述线程正处于何种运行状态; - 优先级,描述线程执行的优先程度; - 线程专有存储区,用于线程切换时存放现场保护信息,和相关统计信息; - 信号屏蔽,即对某些信号加以屏蔽。 - 堆栈,用来保存局部变量和返回地址。 - 用户栈和核心栈 - 用户级线程 - 定义 - 用户级线程仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。 - 对于用户级线程的切换,通常是发生在一个应用进程的诸多线程之间,无须内核的支持。 - 切换的规则远比进程调度和切换的规则简单 - 实现 - 用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、线程同步和通信的函数以及实现线程调度的函数等 ➢pthread_ creat , pthread_ exit , pthread_ join , pthread_ yield - 运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。 - 优点 - 线程切换不需要转换到内核空间 - 调度算法可以是进程专用的 - 用户级线程的实现与OS平台无关 - 用户线程不占用内核内存,本身的内核空间开销也更小一些,可以节约资源 - 缺点 - 系统调用的阻塞问题。当线程执行一个系统调用时,不仅该线程被阻塞,而且,进程内的所有线程会被阻塞。而在内核支持线程方式中则进程中的其它线程仍然可以运行。 - 在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理的优点,内核每次分配给一个进程的仅有一个CPU ,因此,进程中仅有一个线程能执行,在该线程放弃CPU之前,其它线程只能等待。 - 内核支持线程KST - 定义 - 在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他,们的创建、撤消和切换等,也是依靠内核实现的。 - 在内核空间还为每一个内核支持线程设置了一 个线程控制块 ,内核是根据该控制块而感知某线程的存在的,并对其加以控制。 - 实现 - 在支持线程的OS中,系统在创建进程时,便为他分配一个任务数据区,包括若干线程控制块空间。 - 优点 - 在多处理器系统中,内核能够同时调度同-进程的多个线程并行执行。 - 一个线程被阻塞,内核可以调度该进程的其它线程运行,也可以运行其它进程中的线程。 - 线程切换比较快,开销小。 - 内核本身也可以采用多线程技术,提高执行速度和效率 - 缺点 - 在同一进程间的线程控制权转移时,用户级与核心级的切换开销很大。 - 组合方式 - 内核支持多线程的建立、调度和管理,同时,也允许用户应用程序建立、调度和管理用户级线程。 - 这种方式能够结合两者的优点,克服其各自的不足。

操作系统复习 第三章

# 第三章 处理机调度与死锁 ## 处理机调度的层次 - 进程:是一个程序对某个数据集的执行过程,是分配资源的基本单位。 - 作业:是用户需要计算机完成的某项任务,是要求计算机所做工作的集合 - 一个作业通常包括几个进程,几个进程共同完成一个任务,即作业。 - 用户提交作业以后,当作业被调度,系统会为作业创建进程, 一个进程无法完成时,系统会为这个进程创建子进程。 - 作业的概念更多地用在批处理系统中。 - <mark>进程的概念几乎可以用在所有的多道程序系统中</mark>。 <mark>调度</mark>是多道程序的关键 **调度算法的目标:** - 提高资源利用率 - **公平性**。公平性是指应使诸进程都获得合理的CPU时间,不会发生进程 饥饿现象。 - **平衡性**。由于在系统中可能具有多种类型的进程,有的属于计算型作业, 有的属于I/O型。为使系统中的CPU和各种外部设备都能经常处于忙碌状态, 调度算法应尽可能保持系统资源使用的平衡性。 - **策略强制执行**。对所制订的策略其中包括安全策略,只要需要,就必须 予以准确地执行,即使会造成某些工作的延迟也要执行。 **批处理系统的目标**: - 平均周转周期更短 - 周转时间=作业完成时间-作业到达时间 - 平均周转时间=作业周转总时间/进程个数 - 带权周转时间:即作业的周转时间T与系统为它提供服务的时间T,之比,即.W = T/Ts。 - 可进一步反映调度的性能,更清晰地描述各进程在其周转时间中,等待和执行时间的具体分配状况. - 系统吞吐量高 - 由于吞吐量是指在单位时间内系统所完成的作业数 - 它与批处理作业的平均长度有关 - 如果单纯是为了获得高的系统吞吐量,就应尽量多地选择短作业运行。 - 处理机利用率高 - 如果单纯是为使处理机利用率高,应尽量多地选择计算量大的作业运行 **分时系统的目标**: - 响应时间快 - 响应时间指用户提交请求到系统首次响应为止的时间。 - 均衡性。 - 系统响应时间的快慢应与用户所请求的复杂性相适应。 **实时系统的目标**: - 截止时间的保证 - 开始执行的最迟时间 - 必须完成的最迟时间。 - 可预测性 ## 作业与作业调度 ### 作业 - 作业(Job) - 用户提交给计算机系统的任务。 - 由程序、数据、作业说明书组成。 - 作业步(Job Step) - 作业执行期间所经历在加工步骤。 - 典型的作业控制过程分:“编译” " 链接装配“ ”运行“ - 作业控制块(Job Control Block , JCB) - 作业控制块是批处理作业存在的标志,保存有系统对于作业进行管理所需要的全部信息,位于磁盘区域中 - 作业开始,系统输入程序为其建立一个作业控制块,进行初始化,大部分信息取自作业说明书。 - 系统输入程序、作业调度程序、作业控制程序、系统输出程序等需要访问作业控制块。 - 作业完成后,其作业控制块由系统输出程序撤消。 - ![](../../../../assets/default.png) ### 作业调度 - 先来先服务算法(First Come First Serve) - FCFS是最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。 - 基本原则是按照作业到达系统的先后次序来选择,或者说它是优先考虑在系统中等待时间最长的作业,而不管该作业所需执行时间的长短。 - 短作业优先(Short Job First)调度算法 - 对短作业或短进程优先调度的算法。可以分别用于作业调度和进程调度。 - SJF算法是以作业的长短来计算优先级,作业越短,其优先级越高。 - SJF调度算法:从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。 - 缺点 - (1)必须预知作业的运行时间。 - (2)对长作业非常不利,长作业的周转时间会明显地增长,更严重的是,该算法 完全忽视作业的等待时间,可能使作业等待时间过长,出现饥饿现象。 - (3)在采用SJF算法时,人一机无法实现交互。 - (4)该调度算法完全未考虑作业的紧迫程度,故不能保证紧迫性作业能得到及时处理。 - 优先级调度算法(Priority-Scheduling Algorithm) - 反应作业的紧迫性 - 高响应比优先调度算法 - 高响应比优先调度算法则是既考虑了作业的等待时间,又考虑作业运行时间的调度算法,因此既照顾了短作业,又不致使长作业的等待时间过长,从而改善了处理机调度的性能 - 为每个作业引入一个动态优先级,即优先级是可以改变的,令它随等待时间延长而增加,这将使长作业的优先级在等待期间不断地增加,等到足够的时间后,必然有机会获得处理机。该优先级的变化规律可描述为: $$ 优先权=\frac{等待时间+要求服务时间}{要求服务时间} $$ - 由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先级又相当于响应比RP。据此,优先又可表示为: $$ R_p=\frac{等待时间+要求服务时间}{要求服务时间}=\frac{响应时间}{要求服务时间} $$ - 特点 - (1)如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高, 因而该算法有利于短作业。 - (2)当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时 间愈长,其优先权愈高,因而它实现的是先来先服务。 - (3)对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待 时间足够长时,其优先级便可升到很高,从而也可获得处理机。 ## 进程调度 - 任务 - 保存处理机的现场信息。 - 按某种算法选取进程。 - 把处理器分配给进程。 - 组成部分 - 排队器 - 分派器 - 上下文切换器 - 调度方式 - 非抢占方式: - 处理机分配给某进程后,就一直让它运行下去,决不会因为时钟中断或任何其它原因去抢占当前正在运行进程的处理机,直至该进程完成,或发生某事件而被阻塞时,才把处理机分配给其它进程。 - 评价 - 实现简单、系统开销小 - 适用于大多数的批处理OS ,但在分时系统和要求比较严格的实时系统中,不宜采用这种调度方式 - 抢占方式 - 允许调度程序根据某种原则,去暂停某个正在执行的进程,将处理机重 新分配给另一进程。 - 在现代OS中广泛采用抢占方式,这是因为: - 对于批处理机系统,可以防止一-个长进程长时间地占用处理机,以确保处理机能为所有进程提供更为公平的服务。 - 在分时系统中,只有采用抢占方式才有可能实现人一机交互 - 在实时系统中,抢占方式能满足实时任务的需求。 - 抢占原则 - 优先权原则:优先权高的可以抢占优先权低的进程的处理机。 - 短作业(进程)优先原则:短作业(进程)可以抢占长作业(进程)的处理机。 - 时间片原则:各进程按时间片运行,一个时间片用完时,停止该进程执行重新进行调度。 ### 调度算法 - 轮转调度算法 - 在分时系统中,为保证能及时响应用户的请求,必须采用基于时间片轮转式进程调度算法。 - 在早期,分时系统中采用的是简单的时间片轮转法 - 系统将所有的就绪进程按FCFS策略排成一个就绪队列 - 系统可设置每隔一定时间(如30 ms)便产生一次中断,去激活进程调度程序进行调度,把CPU分配给队首进程,并令其执行一个时间片。 - 当它运行完毕后,又把处理机分配给就绪队列中新的队首进程,也让它执行一个时间片。 - 这样,就可以保证就绪队列中的所有进程在确定的时间段内,都能获得一个时间片的处理机时间。 - 在RR调度算法中,应在何时进行进程的切换,可分为两种情况: - 若一个时间片尚未用完,正在运行的进程便已经完成,就立即激活调度程序,将它从就绪队列中删除,再调度就绪队列中队首的进程运行,并启动一个新的时间片。 - 在一个时间片用完时,计时器中断处理程序被激活。如果进程尚未运行完毕,调度程序将把它送往就绪队列的末尾。 - 时间片大小: - 如果太小利于短作业,但是会频繁中断,进程.上下文切换,增加系统开销; - 如果太大则每个进程都能在时间片内完成,则退化为FCFS算法,无法满足交互式用户的需求。 - 一个比较可取的大小是,时间片略大于一次典型的交互所需要的时间。 - 优先级调度算法 - 优先级进程调度算法,是把处理机分配给就绪队列中优先级最高的进程。这时,又可进一步把该算法分成如下两种 - 非抢占式优先级调度算法。 - 抢占式优先级调度算法。 - 优先级类型 - 静态优先级是在创建进程时确定的,在进程的整个运行期间保持不变。优先级是利用某一范围内的一个整数来表示的,例如0 ~ 255中的某一整数,又把该整数称为优先数。确定进程优先级大小的依据有如下三个: - 进程类型。 - 进程对资源的需求。 - 用户要求。 - 动态优先级 - 动态优先级是指在创建进程之初,先赋予其-个优先级,然后其值随进程的推进或等待时间的增加而改变,以便获得更好的调度性能。 - 若所有进程都具有相同的优先级初值,则最先进入就绪队列的进程会因其优先级变得最高,从而得到cpu ,等于FCFS。 - 若所有就绪进程具有各不相同的优先级初值,那么对于优先级初值低的进程,在等待了足够的时间后,也可以获得处理机。 - 也可以规定,当运行的进程随着运行时间的推移动态降低其优先级,防止一个进程长期垄断cpu。 - 多队列调度算法 - 在多处理机系统中可以为每个处理机设置一个单独的就绪队列 - 多级反馈队列调度算法 - 是时间片轮转算法和优先级调度算法的综合和发展,通过动态调整进程优先级和时间片大小,不必事先估计进程的执行时间。 - <mark>FCFS+优先级+RR+抢占</mark> - 多级反馈队列可兼顾多方面的系统目标,是目前公认的一种<mark>较好</mark>的进程调度算法 - 调度机制 - 设置多个就绪队列并为各个队列赋予不同的优先级,第一个最高,依次降低。各个队列中进程执行时间片的大小设置为:优先权越高,时间片越短 - 每个队列都采用FCFS算法。当新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可撤离系统。否则,即它在一个时间片结束时尚未完成,调度程序将其转入第二队列的末尾等待调度;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列, ..... 依此类推。当进程最后被降到第n队列后,在第n队列中便采取按RR方式运行。 - 按队列优先级调度。调度程序首先调度最高优先级队列中的诸进程运行,仅当第一队列空闲时才调度第二队列中的进程运行;换言之,仅当第1~ (i-1)所有队列均空时,才会调度第队列中的进程运行。如果处理机正在第队列中为某进程服务时又有新进程进入任-优先级较高的队列,此时须立即把正在运行的进程放回到第队列的末尾,而把处理机分配给新到的高优先级进程。 - 注意 - 当现行进程正在执行它的C P U周期时,如果发生了时间片中断或有进程进入更高级的就绪队列时将引起剥夺,对前一-种情况,现行进程将进入下一级队列,对后一种情况,现行进程则进入本级队列末尾。 - 当一进程被唤醒时,它进入的是原先离开的那个队列,即与其当前优先级对 应的就绪队列。 - 一个进程的优先级被降低,仅发生在因时间片中断而被剥夺的时候。 - ![](../../../../assets/default.png) - 多级反馈队列调度算法具有较好的性能,能较好的满足各种类用户的需要。 - 终端型作业用户。大多属于较小的交互性作业,只要能使作业在第一队列的时间片内完成,便可令用户满意。 - 短批处理作业用户。周转时间仍然较短,至多在第二到三队列即可完成。 - 长批处理作业用户。将依次在1 ~ n级队列中轮转执行,不必担心作业长期得不到处理。 - 保证调度算法 - 在系统中有n个相同类型的进程同时运行,为公平起见,须保证每个进程都获得相同的处理机时间1/n。 - 跟踪计算每个进程自创建以来已经执行的处理时间。 - 计算每个进程应获得的处理机时间,即自创建以来的时间除以n。 - 计算进程获得处理机时间的比率,即进程实际执行的处理时间和应获得的处理机时间之比。 - 比较各进程获得处理机时间的比率。如进程A的比率最低,为0.5 ,而进程B的比率为0.8 ,进程C的比率为1.2等。 - 调度程序应选择比率最小的进程将处理机分配给它,并让该进程一-直运行,直到超过最接近它的进程比率为止。 - 公平共享调度算法 - 分配给每个进程相同的处理机时间,显然,这对诸进程而言,是体现了一定程度的公平,但如果各个用户所拥有的进程数不同,就会发生对用户的不公平问题。 - 用户1有4个进程A、B、C、D ,用户2只有一-个进程E。强制调度序列为: A E B E C E D E A E B E C E D E - 如果希望用户1所获得的处理机时间是用户2的2倍,则强制调度序列为: A B E C D E A B E C D E ## 实时调度 ### 实时调度的条件 - 实时系统 - 两种任务 - 硬实时任务( HRT )指必须满足最后期限的限制,否则会给系统带来不可接受的破坏或者致命错误。 - 软实时任务( SRT)也有一个与之关联的最后期限,并希望能满足这个期限的要求,但这并不是强制的,即使超过了最后期限,调度和完成这个任务仍然是有意义的。 - 提供必要的信息 - 就绪时间。 - 开始截止时间和完成截止时间。 - 处理时间。 - 资源要求。 - 优先级。 - 系统处理能力强 - 单处理机实时调度条件 - $$ \sum_{i+1}^{n}\frac{C_i}{P_i}\leq1 $$ - 其中C表示处理时间,P表示周期时间 - 提高处理能力的途径 - 采用单处理机系统,增强处理能力,减少每个任务处理时间; - 采用多处理机调度 - $$ \sum_{i+1}^{n}\frac{C_i}{P_i}\leq N $$ - N表示处理机个数 - 采用抢占式调度机制 - 在含有硬实时任务的实时系统中,广泛采用抢占机制。 - 当一个优先权更高的任务到达时,允许将当前任务暂时挂起,令高优先权任务立即投入运行,这样可满足该硬实时任务对截止时间的要求。但此种机制较复杂。 - 对于一些小的实时系统,如果能预知任务的开始截止时间,则对实时任务的调度可采用非抢占调度机制,以简化调度程序和对任务调度时所花费的系统开销。 - 具有快速切换机制 - 为保证要求较高的硬实时任务能及时运行,在实时系统中还应具有快速切换机制,以保证任务的快速切换。需要以下两种能力: - 对外部中断的快速响应能力。要求系统具有快速硬件中断机构,使可在紧迫的外部事件请求中断时及时响应。 - 快速的任务分派能力。在完成任务调度后,便应进行任务切换,为提高速度,应使系统中的运行功能单位适当的小,以减少任务切换的时间开销。 ### 实时调度算法 - 分类 - 根据实时任务性质,可将实时调度的算法分为: - 硬实时调度算法 - 软实时调度算法 - 按调度方式,则可分为: - 非抢占调度算法 - 非抢占式轮转调度算法。 - 非抢占式优先调度算法。 - 抢占调度算法 - 基于时钟中断的抢占式优先级调度算法。 - 立即抢占(Immediate Preemption)的优先级调度算法。 - ![](../../../../assets/default.png) - 最早截止时间优先(Earliest Deadline First)算法 - 根据任务的截止时间来确定任务的优先级。截止时间越早,其优先级越高。 - 该算法要求在系统中保持一个实时任务就绪队列,该队列按各任务截止时间的早晚排序,调度程序在选择任务时总是选择就绪队列中的第一个任务,为之分配处理机,使之投入运行。 - EDF算法既可以用于抢占式调度,也可用于非抢占式调度。 - 最低松弛度优先(Least Lexity First)算法 - 该算法是根据任务紧急(或松弛)的程度,来确定任务的优先级。任务的紧急程度越高,为之赋予的优先级就越高。 - 例如,任务A在200ms时必须完成,本身运行时间100ms ,则必须在100ms之前调度执行, A任务的紧急(松弛)程度为100ms ,又如任务B在400ms是必须完成,需运行150ms ,其松弛程度为250ms. - 该算法主要用于抢占调度方式中。 - 优先级倒置(Priority Inversion Problem) - 形成 - 当前OS广泛采用优先级调度算法和抢占方式,然而在系统中存在着影响进程运行的资源而可能产生"优先级倒置”的现象,即高优先级进程(或线程)被低优先级进程(或线程)延迟或阻塞 ## 死锁 ### 死锁概述 死锁( Deadlock ) 是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种状态时,若无外力作用,它们都将无法再向前推进。 | | 共同点 | 区别 | | --- | -------------- | ----------------------------------------------------------------------------------------------------- | | 死锁 | 都是进程无法向前推进的现象。 | 1.一定是循环等待对方手里的资源导致的 <br/>2.至少有2个或2个以上进程同时发生 <br/>3.进程处于阻塞态<br/>4.操作系统分配资源的策略不合理导致<br/>5.是管理者(操作系统)的问题 | | 饥饿 | | 1.只能由一个进程发生饥饿<br/>2.可能在阻塞态,也可能在就绪态<br/>3.操作系统分配资源的策略不合理导致<br/>4.是管理者(操作系统)的问题 | | 死循环 | | 1.可能只有一个<br/>2.可以是运行态<br/>3.由代码逻辑错误导致的<br/>4.是被管理者的问题 | - 资源 - 可重用性资源 - 每一个可重用性资源中的单元只能分配给一个进程使用,不允许多个进程共享。进程在使用可重用性资源时,须按照这样的顺序: - 请求资源 - 使用资源。 - 释放资源。 - 系统中每一类可重用性资源中的单元数目是相对固定的,进程在运行期间既不能创建也不能删除它。 - 可消耗性资源 - 可消耗性资源又称为临时性资源,它是在进程运行期间,由进程动态地创建和消耗的,它具有如下性质: - 每一类可消耗性资源的单元数目在进程运行期间是可以不断变化的,有时它可以有许多,有时可能为0 ; - 进程在运行过程中,可以不断地创造可消耗性资源的单元,将它们放入该资源类的缓冲区中,以增加该资源类的单元数目。 - 进程在运行过程中,可以请求若干个可消耗性资源单元,用于进程自己 的消耗,不再将它们返回给该资源类中。 - 可抢占性资源(CPU、内存等) - 某进程在获得这类资源后,该资源可以再被其它进程或系统抢占。 - 不可抢占性资源(打印机、磁带机等) - 一旦系统把某资源分配给该进程后,就不能将它强行收回,只能在进程用完后自行释放。 - 死锁原因 - 竞争不可抢占性资源 - 竞争可消耗资源 - 进程推进顺序不当 - 产生死锁的必要条件 - 互斥条件 - 请求和保持条件 - 不可抢占条件 - 循环等待条件 ### 预防死锁 预防死锁的方法是使四个必要条件中的第2、3、4条件之一不能成立来避免发生死锁。 必要条件1 ,因为它是由设备的固有条件所决定的,不仅不能改变,还应加以保证。、 - 破坏请求和保持条件 - 第一种协议 - 系统规定所有进程在开始运行之前,都必须-次性的申请其在整个运行过程所需的全部资源。 - 优点:算法简单、易于实现且很安全。 - 缺点:资源浪费严重和使进程经常会发生饥饿现象。 - 第二种协议 - 进程提出申请资源前必须释放已占有的一切资源。 - 当一个已经保持了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着进程已占有的资源会被暂时地释放,或者说是被抢占了,从而破坏了”不可抢占”条件。 - 缺点:实现起来比较复杂且付出很大代价。可能会前功尽弃,反复申请和释放等情况,延长了周转时间,增加系统开销。 - 破坏循环等待条件 - 采用资源顺序分配法,可以破坏循环等待条件 - 采用资源顺序分配法,系统不会出现循环等待。因为在任何时刻,总有一个进程占有较高序号的资源,该进程继续请示的资源必然是空闲的。故该进程可一直向前推进。 - 优点: - 有序资源分配法提高了资源利用率 - 缺点: - 不方便增加新的设备,因为可能需要重新分配所有编号 - 进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费; - 必须按规定申请资源,用户编程麻烦, ### 避免死锁 - 系统安全状态 - 该方法允许进程动态地申请资源,系统在进行资源分配之前,先计算资源分配的安全性。若此次分配不会导致系统从安全状态向不安全状态转换,便可将资源分配给进程;否则不分配资源,进程必须阻塞等待。从而避免发生死锁。 - 安全状态:在此状态系统能按某种顺序P1, P2... Pn来为各个进程分配其所需资源,使每个进程都可顺序地一个个地完成。这个序列{P1,P....Pn}称为安全序列。 - 不安全状态:系统不存在任何一个安全序列 - 银行家算法 - 银行家算法的数据结构 - 可利用资源向量Available [m] - m为系统中资源种类数, Available[j] =k表示系统中第j类资源数为k个。 - 最大需求矩阵Max[n][m] - n为系统中进程数, Max[i][j] =k表示进程对j类资源的最大需求数为k。 - 分配矩阵Allocation[n][m] - Allocation[i][j]=k表示进程i记分得j类资源的数目为k个。 - 需求矩阵Need[n][m] - Need[i][j]=k表示进程i还需要j类资源k个。 - Need[i][j] =Max[i][j]-Allocation[i][j] - 内容 - 设Request;[j]=k,表示进程P:需要k个R;类型的资源 - 如果Request;[j] <= Need[i,j],便转向步骤2 ;否则认为出错,因为它所请求的资源数已超过它所需要的最大值。 - 如果Request;[j] <= Available[j],便转向步骤3 ;否则,表示尚无足够资源,P需等待 - 系统试探着把资源分配给进程P,并修改下面数据结构中数值 - Available[j] = Available[j]- Request;[j]; - Allocation[ij] = Allocation[;j]+ Request;[j]; - Need[ij] =Need[ij]-Request;[j]; - 安全性 - (1 )设置两个向量: - 工作向量work :表示系统可提供给进程继续运行所需的各类资源数目,含有m个元素的一维数组,初始时, work =Available; - Finish:含n个元素的一维数组,表示系统是否有足够的资源分配给n个进程,使之运行完成。开始时先令Finish[i] =false (i=1..n);当有足够资源分配给进程时,再令Finish[i]=true。 - ( 2 )从进程集合中找到一个能满足下述条件的进程: - Finish[i]=false; - Need[ij] < =work[j]; - 若找到,执行步骤( 3),否则执行步骤( 4)。 - ( 3 )当进程Pi获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行: - work[j]= work[j]+ Allocation[ij] ; - Finish[i]=true; - go tostep (2); - ( 4 )如果所有进程的Finish[i] =true都满足,则表示系统处于安全状态;否则,系统处于不安全状态。 ### 检查死锁 - 为了能对系统中是否已发生了死锁进行检测,在系统中必须 - ①保存有关资源的请求和分配信息; - ②提供一种算法,它利用这些信息来检测系统是否已进入死锁状态。 - ![](../../../../assets/default.png) - 死锁定理 - 如果资源分配图中没有环路,则系统中没有死锁; - 如果图中存在环路则系统中可能存在死锁 - 如果每个资源类中只包含一个资源实例,则环路是死锁存在的充分必要条件 - 如果每个资源类中资源实例个数不全为1 , 则环路是死锁存在的必要条件 - 死锁定理2 - 资源分配图化简 - 找到资源分配图中一个不孤立、不阻塞的进程节点,消去请求边与分配边,使之成为孤立点。 - 孤立点的资源释放后,可以分给其它进程,即将某进程的申请边变为分配边。 - 重复上述过程,当资源分配图中所有进程都成为孤立点时,称该资源分配图是可以完全简化的,否则称该资源分配图是不可完全简化的。 - **不孤立**:是指该进程存在有与之相连的有向边; - **不阻塞**:是指该进程除了已经分配的资源外,对它尚需要的资源,系统都能够满足,因此该进程不会被阻塞。 - **孤立点**:是指该进程既无请求边,也无分配边,即没有与之相连的有向边。 - <mark>一种资源分配状态为死锁状态的充要条件是资源分配图是不可完全简化的。</mark> ### 解除死锁 - 当发现进程死锁时,便应立即把它们从死锁状态中解脱出来。常采用的方法是: - 抢占资源:从其他进程剥夺足够数量的资源给死锁进程以解除死锁状态。 - 终止进程:最简单的是让全部进程都死掉;温和一点的是按照某种顺序逐个撤销进程,直至有足够的资源可用,使死锁状态消除为止。 - 终止所有死锁进程 - 这是一种最简单的方法,即是终止所有的死锁进程,死锁自然也就解除了,但所付出的代价可能会很大。因为其中有些进程可能已经运行了很长时间,已接近结束,一旦被终止真可谓“功亏一篑”,以后还得从头再来。 - 逐个终止进程 - 按照某种顺序,逐个地终止进程,直至有足够的资源,以打破循环等待,把系统从死锁状态解脱出来为止。 - 每终止一个进程,都需要用死锁检测算法确定系统死锁是否已经被解除,若末解除还需再终止另一个进程。 - 在采取逐个终止进程策略时,还涉及到应采用什么策略选择一个要终止的进程

操作系统复习 第四章

# 第四章 存储器管理 存储器是计算机系统的重要组成部分之一。 对存储器加以有效管理,不仅直接影响存储器的利用率,而且对系统性能有重大影响。存储器管理的主要对象是内存,对外存的管理在文件管理中。 ## 存储器的层次结构 - 存储器的多层结构 - 理想的存储器: - 速度快 - 容量大 - 价格低 - 现代计算机系统的存储部件实际上采用了层次结构,组成了一-个速度由快到慢,容量由小到大,价格由高到低的存储装置层次。 - 可执行存储器 - 寄存器和主存储器为可执行存储器。 - 进程访问可执行存储器:使用load或者store指令便可访问 - 进程访问辅存:通过I/O设备实现,在访问中将涉及到中断、设备驱动程序以及物理设备的运行。 - 层次结构 - 寄存器 - 寄存器访问速度最快,完全能与CPU协调工作,但价格昂贵,因此容量不可能做得很大。寄存器的长度一般以字为单位。 - 高速缓存 - 是现代计算机结构中的一个重要部件,其容量大于或远大于寄存器,速度快于主存储器。 - 根据局部性原理,将主存储器中一些经常访问的信息存放在高速缓存中,减少访问主存储器的次数,提高程序执行速度。 - 主存储器 - 简称内存或主存,用于保存进程运行时的程序和数据,也成为可执行存储器。 - CPU的控制部件只能从主存储器中取得指令和数据,数据能够从主存储器读取并将他们装入到寄存器中,或者从寄存器存入到主存储器中。 - 外存 - 由于I/O速度远低于对主存的访问速度,因此加了磁盘缓存,利用它将使用频繁的部分磁盘数据和信息,暂时存放在磁盘缓存中,用以减少磁盘访问次数。 - 它利用主存空间来暂存从磁盘中读出的信息。 ## 程序的连接与装入 在多道程序环境下,要使程序运行,必须为之。先建立进程。创建进程的第一件事是将程序和数据装入内存。 将用户源程序变为可在内存中执行的程序的步骤: - 编译:由编译程序将用户源代码编译成若干个目标模块 - 链接:由链接程序将编译后形成的一组目标模块,以及它们所需要的库函数链接在一起,形成一个完整的装入模块 - 装入:由装入程序将装入模块装入内存 ### 程序的装入 - 绝对装入方式 - 在编译时,如果知道程序驻留在内存的什么位置,那么编译程序将产生绝对地址的目标代码。 - 装入模块装入内存后,程序中的逻辑地址与实际内存地址完全相同,不须对程序和数据的地址进行修改。 - 程序中所使用的绝对地址,可在编译或汇编时给出,也可由程序员赋予。 - 只适合于单道程序环境 - 可重定位装入方式(Relocation Loading Mode) - 在多道程序环境下,可重定位装入方式,根据内存的当前情况,将装入模块装入到内存的适当位置。 - 注意:在采用可重定位装入方式将装入模块装入内存后,会使装入模块中的所有逻辑地址与实际装入内存的物理地址不同。 - 在装入时对目标程序中指令和数据的修改过程称为重定位。地址变换在装入时一次完成,以后不再改变,称为静态重定位。 - 动态运行时的装入程序 - 在把装入模块装入内存后,并不立即把装入模块中的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正要执行时(访问内存之前)才进行。 - 装入内存后的所有地址都仍是相对地址。 - 为使地址转换不影响指令的执行速度,应设置一个重定位寄存器。 - 重定位寄存器:存放装入模块的起始位置 ### 程序的链接 - 静态链接方式 - 在程序运行前,将目标模块及所需的库函数链接成一个完整的装配模块 ,以后不再拆开。 - 将目标模块装配成装入模块时需解决的两个问题: - 对相对地址进行修改 - 变换外部调用符号 - 库有两种:静态库和动态库。windows. 上对应的是.lib .dll、 linux. 上对应的是.a .so - 静态库 - :在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。 - 静态库对函数库的链接是放在编译时期完成的程序在运行时与函数库再无瓜葛,移植方便。 - 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件 - 动态库 - 动态库在程序编译时并不会被连接到目标;代码中,而是在程序运行时才被载入。 - 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。 - 装入时动态链接 - 用户源程序经编译后所得的目标模块,是在装入内存时,边装入边链接的,即在装入一个目标模块时,若发生一个外部模块调用事件,将引起装入程序去找出相应的外部目标模块,并将它装入内存,还要修改目标模块中的相对地址 - 优点: - 便于修改和更新 - 便于实现对目标模块的共享。 - 运行时动态链接 - 运行时动态链接是将对某些模块的链接推迟到执行时才执行,即在执行过程中,当发现一个被调用模块尚未装入内存时,立即由OS去找到该模块并将之装入内存,把它链接到调用者模块上。 - 凡执行过程中未被用到的目标模块,不会调入内存和链接,这样不仅加快程序的装入过程。而月节省大量的内存空间. ## 连续分配存储方式 连续分配方式:为一个用户程序分配一个连续的存储空间。 ### 单一连续分配 - 最简单,只适用于单用户、单任务系统,内存只有一道用户程序 - 内存分为两部分: - 系统区 - 内存的低址部分,仅供OS使用 - 不一定需要内存保护 - 用户区 - 系统区以外的全部内存空间,供用户程序使用,存储器利用率低 ### 固定分区分配 将内存用户空间划分为若干个固定大小的区域,在每个分区中只装入一道作业,便可以有多道作业并发执行。 当有一空闲分区时,便可以再从外存的后备作业队列中,选择一个适当大小的作业装入该分区,当该作业结束时,可再从后备作业队列中找出另一作业调入该分区。 - 划分分区 - 分区大小相等 - 适用于控制多个相同对象的场合 - 缺乏灵活性,小程序浪费空间,大程序无法装入 - 分区大小不等 - 解决了灵活性问题,将内存分为多个大小不等的分区:小分区(较多) ,中等分区(适量) ,大分区(少量) - 内存分配 - 分区说明表 - 用于管理和分配内存的数据结构。 - 每个表项对应一个分区,记载着这个分区的序号、空间大小、起始地址和使用状况。 - 分配过程 - 当有一用户程序要装入时,由内存分配程序检索该表,从中找出一-个能满足要求的、尚未分配的分区,将之分配给该程序,然后将该表项中的状态置为“已分配”; - 若未找到大小足够的分区,则拒绝为该用户程序分配内存。 ### 动态分区分配 动态分区法在作业执行前并不建立分区,分区的建立是在作业的处理过程中进行的,且其大小可随作业或进程对内存的要求而改变。 这就改变了固定分区法中那种即使是小作业也要占据大分区的浪费现象,从而提高了内存的利用率。 采用动态分区法,在系统初启时,除了操作系统中常驻内存部分之外,只有一-个空闲分区。随后,分配程序将该区依次划分给调度选中的作业或进程。 - 在实现过程中涉及如下问题: - 分区分配中的数据结构 - 空闲分区表 - 在系统中设置-一张空闲区表,每个表目记录-个空闲区,主要参数包括分区号、长度和起始地址。 - 空闲分区链 - 在每个分区的起始部分,设置一些用于控制分区分配的信息,以及用于链接各分区所用的前向指针;在分区尾部则设置一后向指针,将所有的空闲分区链接成一个双向链。 - 分区分配算法 - 把一个新作业装入内存时,须按照一 定的动态分区分配算法,从空闲分区表(或空闲分区链)中选出一个分区分配给该作业。由于分配算法算法对系统性能有很大的影响,因此人们对它进行了广泛的研究。 - 基于顺序搜索的动态分区分配算法 - 首次适应算法(First Fit) - 要求按地址递增的次序组织空闲区表(链)。 - 申请和分配:从低地址找起,直至找到-个能可满足要求的空闲分区,根据作业大小划出一-块给申请者,剩余空间仍留在空闲链中,成为一个小的空闲分区。 - 优点: - 优先使用内存中低址部分的小空闲区,从而保留了高址部分的大空闲区,有利于大作业。 - 缺点: - 低址部分不断被划分,形成许多难以利用的小空闲分区(碎片) ,造成间浪费 - 碎片聚集在低址区域,每次又从低址开始查找,增加时间开销 - 循环首次适应算法(Next Fit) - 是FF算法的演变和改进,每次分配不再从空闲分区链(表)的开始找起,而是从上次找到的空闲分区的下一个找起,找到一个能满足要求的空闲区。 - 需增设一个起始查寻指针,指示下一次查找从那个空闲分区开始。 - 优点:使空闲分区分布均匀,减少查找开销 - 缺点:缺乏大空闲分区,不利于大作业 - 最佳适应算法(Best Fit) - '最佳”的含义:把能满足要求的最小空闲分区分配给作业,避免“大材小 用” - 空闲分区链组织方式:按容量从小到大 - 优点:尽量使用小空闲区,保留大空闲区 - 孤立地看,对每个程序,分区大小是最合适的,浪费最小 - 宏观上看,每次分配切割下来的剩余部分最小,因此会形成许多难以利用的碎片 - 最坏适应算法(Worst Fit) - 总是挑选最大的空闲分区分割给作业使用 - 空闲分区链组织方式:按容量从大到小 - 优点: - 防止产生碎片,空间浪费最少有利于中、小作业 - 查找效率很高(只需查找第一块) - 缺点 - 缺乏大的空闲分区 - 基于索引搜索的动态分区分配算法 - 快速适应算法 - 也称分类搜索法 - 将空闲分区按大小分类,每类具有相同容量,放入一个空闲分区链表 - 增设一张管理索引表,每个表项对应一个分类,并记录相应链表的表头指针 - 即:按分区容量大小,分类索引、搜索 - 优点: - 查找效率高,仅需要根据进程的长度,寻找到能容纳它的最小空闲区链表,并取下第一块进行分配即可。 - 缺点: - 在分区归还主存时算法复杂,系统开销较大 - 浪费空间,空闲分区划分越细,浪费则越严重。 - 伙伴系统 - 无论已分配分区或空闲分区,分区大小必须是2的k次幂,k为整数, I≤k≤m - 2<sup>m</sup>表示最大分区大小,通常是整个可分配内存的大小 - 每次切分必须对半分(分开的两半称之为一对伙伴) - 每次合并必须与其伙伴合并 - 分配过程: - 首先计算一个i值 ,使2<sup>i-1</sup><n<=2<sup>i</sup> - 在分区大小为2<sup>i</sup>的空闲分区链表中查找,找到则分配,否则转3 - 查找分区大小为2i+1的空闲分区链表,若存在空闲分区,则将其划分为两个大小相同(2<sup>i</sup>)的分区(一对伙伴) , 一个用于分配,一个加入到分区大小为2的空闲分区链表。若仍然找不到, 转4 - 继续查找大小为2<sup>i+2</sup>的空闲分区链表,此时需要进行两次划分 - 以此类推 - 回收 - 回收时,与其伙伴分区合并一次回收可能进行多次合并 - 性能 - 时间开销:查找、分割、合并空闲分区 - 哈希算法 - 哈希算法就是利用哈希快速查找优点,以及空闲分区在可利用空间表中的分布规律,建立哈希函数,构造一张以空闲分区大小为关键字的哈希表,该表的每一个表项记录了一个对应的空闲分区链表表头指针。 - 当进行空闲分区分配时,根据所需要的分区大小,通过哈希函数计算,即得到在哈希表中的位置,从中得到相应的空闲分区链表,实现最佳分配策略。 - 分区分配及回收操作 - **分配**:利用某种分配算法,从空闲分区链(表)中找到所需大小的分区。设请求的分区大小为u.size ,表中每个空闲分区的大小表示为m.size ,若m.size- u.sizessize(规定的不再切割的分区大小, 将整个分区分配给请求者,否则从分区中按请求的大小划出一块内存空间分配出去,余下部分留在空闲链中,将分配区首址返回给调用者。 - **回收**:当某一个用户作业完成释放所占分区时,系统应进行回收。在可变式分区 中,应该检查回收区与内存中前后空闲区是否相邻 - 若相邻,则应进行合并,形成一个较大的空闲区,并对相应的链表指针进行修改 - 若不相邻,应将空闲区插入到空闲区链表的适当位置。 ### 动态重定位分区分配 动态分区分配方式,要求把一个系统程序或用户程序装入一个连续的内存空间中。 随着各进程不断申请和释放内存,导致在内存中出现大量分散的小空闲区。 内存中这种容量太小、无法利用的小分区称做<mark>(外部)“碎片"</mark>或“零头” **碎片**: - 内部碎片:指已分配给作业的存储空间中未被利用的部分。如固定分区中存在的碎片。 - 外部碎片:指系统中无法利用的小空闲分区。如动态分区中存在的碎片。 **动态重定位分区分配** - 将内存中所有作业移到内存一端(作业在内存中的位置发生了变化,这就必须对其地址加以修改或变换,即重定位) , 使本来分散的多个小空闲分区连成一个大的空闲区 - 这种通过移动作业从把多个分散的小分区拼接成一个大分区的方法称为拼接或紧凑。 - 拼接时机: - 分区回收时;当找不到足够大的空闲分区,且总空闲分区容量可以满足作业要求时。 - 定时。 - 实现 - 作业被装入内存后所有地址仍然是相对地址,将相对地址转换为物理地址的工作,被推迟到程序指令真正执行时。 - 需要硬件地址变换机构的支持 - 重定位寄存器,保存作业在内存中的起始地址 - “紧凑”之后,程序移动了位置,只需更改重定位寄存器的内容为新的起始地址 ![](../../../../assets/default.png) ## 对换 - 问题 - 内存中被阻塞的进程占用大量空间,外存,上却有许多作业因为内存不足而等待。 - 对换 - 把内存中暂时不能运行的进程或暂时不用的程序和数据调出到外存上,以便腾出足够的内存空间 - 类型 - 进程 - “整体对换”, "进程对换”,常用于中级调度、分时系统 - “页”或"段” - "页面对换”或"分段对换”,统称“部分对换”,用于支持虚拟存储系统 - 空间管理 - 一般从磁盘上划出一块空间作为对换区使用 - 在系统中设置相应的数据结构以记录外存的使用情况 - 对换空间的分配与回收,与动态分区方式时的内存分配与回收雷同。 - 进程的换入与换出 - 进程的换出 - 选择换出进程的优先次序 - 阻塞>睡眠>优先级&驻留时间>就绪进程 - 换出过程 - 申请对换空间(外存) - 启动磁盘并传送数据 - 回收内存空间 - 修改数据结构 - 进程的换入 - 选择 - “就绪”已换出>换出最久 - 换入过程 - 申请内存 - 申请成功,直接调入 - 申请失败,先换出,再换入 - 对换伴随着较大的系统开销 - CPU时间 - I/O开销 - 不必要的对换可能导致系统性能下降 - 常用方案:正常时并不启用对换,内存紧张时,启用对换,把部分进程调出,缓解紧张状况后,暂停对换程序 ## 分页存储 - 连续分配方式的缺点: - 会形成许多“碎片” - “紧凑”产生较大开销 - 离散分配方式 - 允许将一个进程分散地装入多个不相邻的分区,从而无需“紧凑”。 - 可按离散分配的基本单位,分为两种方式: - 分页存储管理方式 - 分段存储管理方式 - 段页式存储管理方式 **页面** - 把进程的逻辑地址空间划分成若干大小相等的区域,每个区域称作**页面**或**页**。每个页都有一个编号,叫做页号。页号一般从0开始编号,如0 , 1, 2, ...等。 - 把内存空间划分成若3 F和页大小相同的物理块,这些物理块叫**页框**(frame)或(物理)块。同样,每个物理块也有一个编号,块号从0开始依次顺序排列。 - 以**页**为单位进行内存分配,并按进程的页数多少来分配。逻辑上相邻的页,物理上不- 定相邻。 - **“页内碎片”**:最后一页装不满而形成的碎片,不可利用。 **页面大小** - 页太大,页表短,管理开销小,内碎片大,内存利用率低 - 页太小:内碎片小,内存利用率高,但页面数目多,使页表过长,占大量内存,管理开销大 - 页面大小由硬件地址结构决定,机器确定,页面大小就确定了 - 一般来说,页面大小为2的若干次幂,根据计算机结构的不同,其大小从1 KB到8KB不等。 **地址结构**: - 分页地址中的地址结构如下: - ![](../../../../assets/default.png) - 地址长度32位: - 0~11位为位移量(页内地址),即每页的大小为4KB - 12-31位为页号,地址空间最多允许有1 M页 - 对于某特定的机器,其地址结构是一定的。 - 如果给定的逻辑地址是A ,页面的大小为L ,则: - <mark>页号P = INT[A/L]</mark> - <mark>页内地址d = [A] MOD L</mark> **页表** - 进程的每一页离散地存储在内存的任一存储块中,为方便查找,系统为每一进程建立一张页面映像表,简称页表。 - 页表实现了从页号到物理块号的地址映射。 **地址变化机构** - 由于页内地址和(块内)物理地址是一-对应的 ,地址变换机构的任务实际上是将逻辑地址中的页号转换为内存中的物理块号。 - 基本的地址变换机构 - 页表寄存器PTR - 每个进程对应一个页表,页表驻留在内存中,页表始址和长度存放在PCB中,进程被调度时,这两个数据被装入页表寄存器 - 地址变换处理 - 得到页号:自动将逻辑地址分为页号和页内地址 - 用页号查页表,得到块号 - 将块号与页内地址拼接,即得物理地址 - 越界保护 - 查页表前,将页号与PTR中的页表长度比较,超出(> =)则越界 - 具有快表的地址变换机构 - 对于基本的地址变换机构(无快表) ,每存取一个数据,需要访问两次内存 - 访问页表=>数据所在的块号,形成物理地址 - 访问数据所在的物理地址=>数据 - 增设一个联想寄存器(快表TLB ) - 具有并行查询能力的高速缓冲寄存器 - 存放当前访问的那些页表项 - 每次地址变换时,先在快表中查找页号 - 快表中找不到,再访问内存中的页表,并将页表项存入快表,若快表已满,应换出最老的页表项 - 命中率:第3步时,在快表中查找成功的概率。 **局部性原理** - 时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环) - 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的) **访问内存的有效时间:** - 进程发出指定逻辑地址的访问请求,到在内存中找到对应的实际物理地址单元并取出数据,所需要花费的总时间,为内存的有效访问时间(EAT)。 - 假设访问一次内存的时间为t ,在基本分页存储管理方式中,有效访问时间为 - EAT=t+t=2t - 在引入快表的分页存储管理方式中,有效访问时间的计算公式即为: - EAT =axλ+ (t+λ)(1-a)+t=2t+λ-txa - λ表示查找快表所需要的时间, a表示命中率, t表示访问一次内存所需要的时间。 **两级和多级页表** - 现代计算机系统都支持非常大的逻辑地址空间(232~264) ,页表就非常大,需占用较大的地址空间。 - 解决方法: - 采用离散方式存储 - 只将当前所需页表项调入内存 - 二级页表 - 将页表分页,并离散地将各个页面分别存放在不同的物理块中,同时为离散分配的页表再建立一张页表,称为外层页表,其每个页表项记录了页表页面的物理块号。 - ![](../../../../assets/default.png) - 上述方法用离散分配空间解决了大页表无需大片存储空间的问题,但并未减少页表所占的内存空间。 - 解决方法: - 把当前需要的一批页表项调入内存,以后再根据需要陆续调入。在采用两级页表结构的情况下,对正在运行的进程,必须将其外层页表调入内存,而对页表则只需调入一页或几页。 - 这就需要在外层页表项中增设一个状态位S ,用于表示该页表是否内存调入内存。 - 多级页表 - 二级页表的扩展,对外层页表再分页 **反置页表** - 反置页表是为每一个物理块设置一个页表项并将按物理块号排序,其中的内容则是页号及其隶属进程的标志符。 - 所有进程共同使用一张页表 - ![](../../../../assets/default.png) ## 分段存储 - 引入 - 分页从根本.上克服了外部碎片(地址空间、物理空间都分割)。内存利用率提高。 - 缺点 - 无论信息内容如何,按页长分割,分割后装入内存,有可能使逻辑完整的信息分到不同的页面, 执行速度降低 - 所以考虑以逻辑单位分配内存。 - 分页主要是为了提高系统资源利用率 - 分段主要是为了满足用户(程序员)的需求 - 方便编程. - 信息共享 - 信息保护 - 动态增长或者动态链接 - 分段 - 进程的地址空间:按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),通常用段号代替段名,每段从0开始编址,段内地址是连续的。 - 段长由逻辑信息组的长度决定,逻辑地址由段号(段名)和段内地址所组成。 - 段表 - 分段式存储管理:以段为单位分配内存,每一个段在内存中占据连续空间,各段之间可以不连续存放。 - 为使程序正常运行,须在系统中为每个进程建立一张段映射表,简称"段表”。每个段在表中占有一个表项。 - 段表结构:段号;段在内存中的起始地址(基址) ;段长。 - 段表可以存放在寄存器中,但更多的是存放在内存中。 - 段表用于实现从逻辑段到物理内存区的映射。 - 分页与分段 - 相似点 - 采用离散分配方式,通过地址映射机构实现地址变换 - 不同点 - 页是信息的物理单位,分页是为了满足系统的需要;段是信息的逻辑单位,含有意义相对完整的信息,是为了满足用户的需要。 - 页的大小固定且由系统确定,由系统把逻辑地址分为页号和页内地址,由机器硬件实现;段的长度不固定,取决于用户程序,编译程序对源程序编译时根据信息的性质划分。 - 分页的作业地址空间是一维的;分段的作业地址空间是二维的。 - 信息共享 - 分段系统的一个突出优点是易于实现段的共享和保护,允许若干个进程共享一个或多个分段,且对段的保护十分简单易行。 - 分页系统中虽然也能实现程序和数据的共享,但远不如分段系统方便。 - 分页系统的信息共享 - 共享页面时只需要在物理内存中保存一个编辑器的拷贝。 - 每个用户的页表映射到编辑器的同一物理拷贝,而数据页映射到不同的块。 - 分段系统的信息共享 - 在分段系统中,实现共享十分容易,只需在每个进程的段表中为共享程序设置一个段表项。 ### 段页式存储 - 作业地址空间进行段式管理。( 面向用户 ) - 每段内再分成若干大小固定的页,每段都从零开始为自己的各页依次编写连续的页号。 - 对内存空间的管理仍然和分页存储管理一样,将其分成若干个和页面大小相同的物理块。(面向机器 ) - 作业的逻辑地址包括3个部分:段号、页号和页内位移。 - 为实现地址变换,段页式系统设立了段表和页表。 ![](../../../../assets/default.png)

操作系统复习 第五章

# 第五章 虚拟存储器 虚拟存储器:是指具有请求调入功能和置换功能,能从逻辑.上对内存容量加以扩充的一种存储器系统。其逻辑容量由内存容量和外存容量之和所决定,其运行速度接近于内存速度,而每位的成本却接近于外存。 ## 概述 - 常规处理器管理方式的特征 - 一次性 - 作业必须一次性全部装入内存后才能开始运行。 - 这会造成两个问题: - 作业很大时,不能全部装入内存,导致大作业无法运行 - 当大量作业要求运行时,由于内存无法容纳所有作业,因此只有少量作业能运行,导致多道程序并发度下降。 - 驻留性: - 一旦作业被装入内存,就会一直驻留在内存中,直至作业运行结束。 - 事实上,在一个时间段内,只需要访问作业的一小部分数据即可正常运行,这就导致了内存中会驻留大量的、暂时用不到的数据,浪费了宝贵的内存资源。 - 局部性 - 基于局部性原理,在程序装入时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存,就可以让程序开始执行。 - 在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。 - 若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。在操作系统的管理下在用户看来似乎有一个比实际内存大得多的内存,这就是虚拟内存 虚拟内存的最大容量是计算机的地址结构, CPU寻址范围决定的。 <mark>虚拟内存的实际容量是内存与外存之和, CPU寻址范围,两者的最小值</mark> - 虚拟存储器的特征 - 多次性 - 一个作业被分成多次调入内存运行。多次性是虚拟存储器最重要的特征,与常规存储器管理的一次性相对应。 - 对换性 - 系统允许作业在运行过程中进行换进、换出操作。换进和换出能有效地提高内存利用率。 - 虚拟性 - 虚拟性是指从逻辑上扩充内存容量,并非实际存在。用户感觉到的很大的虚拟存储容量实际上是一种“假象” <mark>虚拟存储器的实现都是建立在离散分配的存储管理方式的基础上。</mark> ## 请求分页系统 - 在分页系统的基础上,增加了请求调页功能和页面置换功能所形成的页式虚拟存储系统。置换时以页面为单位。 - 为实现请求调页和置换功能,系统必须提供必要的支持: - 硬件支持: - 请求分页的页表机制 - 在请求分页系统中所需要的主要数据结构是页表。其基本作用仍然是将用户地址空间中的逻辑地址变换为内存空间中的物理地址。 - 由于只将应用程序的一部分调入内存,还有一部分仍在盘上,故需在页表中再增加若干项,供程序(数据)在换进、换出时参考。 - ![](../../../../assets/default.png) - 缺页中断机构 - 在请求分页系统中,每当要访问的页面不在内存时,便产生一缺页中断,请求OS将所缺之页调入内存。 - 缺页中断作为中断,同样需要经历诸如保护CPU环境、分析中断原因、转入缺页中断处理程序进行处理、恢复CPU环境等几个步骤。 - 但缺页中断是一种特殊的中断,与一般中断有明显区别: - 缺页中断在指令执行期间产生和处理中断信号,而一般中断在一条指令执行完后检查和处理中断信号。 - 缺页中断返回到该指令的开始重新执行该指令,而一般中断返回到该指令的下一条指令执行。 - 一条指令在执行期间,可能产生多次缺页中断。 - 地址变换机构 - 请求分页系统中的地址变换机构,是在分页系统地址变换机构的基础上,再为实现虚拟存储器而增加了某些功能而形成的,如产生和处理缺页中断,以及从内存中换出一页的功能等。 - 软件支持: - 实现请求调页的软件 - 实现页面置换的软件 - 请求分页中的内存分配 - 最小物理块数的确定 - 最小物理块数,指能保证进程正常运行所需的最小物理块数。当系统为进程分配的物理块数少于此值时,进程将无法运行。 - 进程应获得的最小物理块数与计算机的硬件结构有关,取决于指令的格式、功能和寻址方式。 - 物理块的分配策略 - 在请求分页系统中,可采取两种内存分配策略: - 固定分配策略:为每个进程分配一定数目的物理块,在整个运行期间不再改变 - 可变分配策略:先为每个进程分配一定数量的物理块,在整个运行期间可适当增多或减少。 - 在进行置换时,也可采取两种策略 - 全局置换:发生缺页时,只选进程自己的物理块置换 - 局部置换:可以将操作系统保留的空闲物理快分配给进程,也可以将别的进程持有的物理块置换到外存,再分配给缺页进程。 - 组合出的三种策略: - 固定分配局部置换(Fixed Allocation , Local Replacement) - 为每个进程分配一定数目的物理块,在整个运行期间不再改变。 - 采用该策略时,如果进程在运行中发现缺页,只能从该进程在内存中的n个页面中选出一页换出,然后再调入一页。 - 困难:应为每个进程分配多少个物理块难以确定。 - 可变分配全局置换(Variable Allocation , Global Replacement) - 在采用这种策略时,先为系统中的每个进程分配一定数目的物理块,而OS自身也保持一个空闲的物理块队列。 - 如果某进程发生缺页时,由系统从空闲的物理块队列中,取出一个物理块分配给该进程,并将欲调入的页装入其中。 - 当空闲物理块队列中的物理块用完后, OS才能从系统中的任一进程中选择一页调出。 - 可变分配局部置换(Variable Allocation , Local Replacement) - 为每个进程分配一定数目的物理块,如果某进程发生缺页时,只允许从该进程在内存的页面中选出一页换出,不会影响其他进程执行。 - 如果进程在运行中频繁发生缺页中断,则系统再为进程分配若干物理块 - 如果进程在运行中缺页率特别低,则适当减少分配给该进程的物理块。 - 物理块的分配算法 - 在采用固定分配策略时,如何将系统中可供分配的物理块分配给各个进程,可采用以下几种算法: - 平均分配算法:将系统中所有可供分配的物理块,平均分配给各个进程。 - 按比例分配算法:根据进程的大小按比例分配物理块。 - 设系统中共有n个进程,每个进程的页面数为Si ,则系统中各进程页面数的总和为: $$ S=\sum_{i+1}^{n}S^i $$ - 又假定系统中可用的物理块总数为m ,则每个进程所能分到的物理块数为bi ,将有 $$ b_i=\frac{S_i}{S}*m $$ - bi应该取整,必须大于最小物理块数。 - 考虑优先权的分配算法:在实际应用中,为了照顾重要的、急迫的作业尽快完成,为它分配较多的内存空间 - 方法: - 把内存中可供分配的物理块分为两部分: - 一部分按比例分配给各进程; - 一部分则根据各进程的优先权,适当地增加其相应份额,分配给各进程。 - 页面调入策略 - 何时调入页面 - 预调页策略 - 采用以预测为基础的预调页策略,将那些预计在不久之后便会被访问的页面,预先调入内存。 - 主要用于进程的首次调入时 ,由程序员指出应该先调入哪些页。 - 请求调页策略 - 当程序在运行中需要访问某部分程序和数据时,若发现其所在的页面不在内存,便立即提出请求,由OS将其所需要的页面调入内存。 - 优点:由请求调页策略所确定调入的页, 一定会被访问;请求调页策略比较容易实现。 - 缺点:每次仅调入一页,需花费较大的系统开销,增加了磁盘I/O的启动频率。 - 从何处调入页面 - 系统拥有足够的对换区空间,这时可以全部从对换区调入所需页面,以提高调页速度。 - 系统缺少足够的对换区空间,则将不会被修改的文件直接从文件区调入,将可能会被修改调到对换区。 - 如何进行调入 - 每当程序所要访问的页面未在内存时,便向CPU发出一缺页中断,中断处理程序首先保护CPU环境,分析中断原因后,转入缺页中断处理程序。 - 程序通过查找页表,得到该页在外存上的物理块后,如果此时内存能容纳新页则启动磁盘I/O将所缺之页调入内存,然后修改页表。 - 如果内存已满,则需按照某种置换算法从内存中选出一页准备换出;如果该页未被修改过,可不必写回磁盘;但如果此页已被修改,则必须将它写回磁盘,然后把所缺的页调入内存,并修改页表中的相应表项,置其存在位为“1" , 并将此页表项写入快表。 - 在缺页调入内存后,利用修改后的页表,形成所要访问的物理地址,再去访问内存数据。整个页面调入过程对用户是透明的 - 缺页率 - 假设一个进程的逻辑空间为n页,系统为其分配的内存物理块数为m(m≤n)。 - 如果在进程的运行过程中,访问页面成功(即所访问页面在内存中)的次数为S ,访问页面失败(即所访问页面不在内存中,需要从外存调入)的次数为F ,则该进程总的页面访问次数为A= S+ F - 那么该进程在其运行过程中的缺页率即为 $$ 缺页率=\frac{缺页次数}{访问总次数} $$ - 影响缺页率的因素: - 页面大小 - 进程所分配页框的数目 - 页面置换算法 - 程序固有属性 - 请求分段系统 - 在分段系统的基础.上,增加了请求调段功能和分段置换功能所形成的段式虚拟存储系统。置换时以段为单位。 - 为实现请求调段和置换功能,系统必须提供必要的支持: - 硬件支持: - 请求分段的段表机制 - 缺段中断机构 - 地址变换机构 - 软件支持: - 实现请求调段的软件 - 实现段的置换的软件 ## 页面置换算法 在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。 若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存用页面置换算法决定应该换出哪个页面 页面的换入、换出需要磁盘I/O ,会有较大的开销,因此好的页面置换算法应该追求更少的缺页率 - 最佳置换算法 - 其所选择的被淘汰页面,将是以后永不再用的,或许是在最长(未来)时间内不再被访问的页面。 - 优点:保证获得最低的缺页率 - 缺点:无法预知一-个进程在内存的若干个页面,哪个在未来最长时间内不再被访问。 - 算法无法实现,但可评价其他算法。 - 先入先出页面(FIFO)置换算法 - 算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。 - 算法实现简单,只需把一个进程已调入内存的页面,按先后次序链接成一个队列,并设置一个指针(替换指针) ,使它总是指向最老的页面。 - 算法与进程的实际运行规律不相适应,因为进程中的某些页面经常被访问,但先进先出置换算法不能保证这些页面不被淘汰。 - **Belady现象** - 采用FIFO算法时,如果对一个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。 - Belady现象的原因是FIFO算法的置换特征与进程访问内存的动态特征是矛盾的,即被置换的页面并不是进程不会访问的,因而FIFO并不是- -个好的置换算法。 - 最近最久未使用(LeastRecently Used)算法 - 算法根据页面调入内存后的使用情况进行决策。由于无法预测各页面将来的使用情况,只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU置换算法是选择最近最久未使用的页面予以淘汰。 - 该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t ,当需淘汰一个页面时,选择现有页面中其t值最大的,即最近最久未使用的页面予以淘汰。 - 需有以下两类硬件之一的支持: - 寄存器 - 为每个在内存中的页面配置一个移位寄存器,用来记录某进程在内存中各页的使用情况。移位寄存器表示为 - $$ R= R_{n-1}R_{n-2}R_{n-3}\cdots R_2R_1R_0 $$ - 当进程访问某物理块时,要将相应寄存器的R.1位置成1。此时,定时信号将每隔一定时间将寄存器右移一位。如果把n位寄存器的数看作一个整数,那么具有最小数值的寄存器所对应的页面,就是最近最久未使用的页面。 - 栈 - 利用一个特殊的栈来保存当前使用的各个页面的页面号 - 每当进程访问某页面时,**便将该页面的页面号从栈中移出,将它压入栈顶**。 - 栈顶始终是最新被访问页面的编号,而栈底则是最近最久未使用页面的页面号。 - 最少使用置换(Least Frequently Used)算法 - 在采用LFU算法时,应为在内存中的每个页面设置一个移位寄存器,用来记录该页面被访问的频率。该置换算法选择在最近时期使用最少的页面作为淘汰页。 - 简单的Clock置换算法 - 当采用简单Clock算法时,只需为每页设置一位访问位,再将内存中的所有页面都通过链接指针链接成一个循环队列。 - 当某页被访问时,其访问位被置1。 - 置换算法在选择一页淘汰时,只需检查页的访问位。如果是0 ,就选择该页换出;若为1 ,则重新将它置0 ,暂不换出,而给该页第二次驻留内存的机会 - 再按照FIFO算法检查下一个页面。 - 当检查到队列中的最后一个页面时,若其访问位仍为1 ,则再返回到队首去检查第一个页面。 - 因此简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描 - 改进型CLock算法 - 在将一个页面换出时,如果该页已被修改过,便须将该页重新写回到磁盘上;但如果该页未被修改过,则不必将它拷回磁盘。 - 在改进型Clock算法中,除须考虑页面的使用情况外,还须再增加一个因素,即置换代价,这样,选择页面换出时,既要是未使用过的页面,又要是未被修改过的页面。 - 把同时满足这两个条件的页面作为首选淘汰的页面。 - 由访问位A和修改位M可以组合成下面四种类型的页面: - 1类(A=0 , M=0) :表示该页最近既未被访问,又未被修改,是最佳淘汰页。 - 2类(A=0 , M=1) :表示该页最近未被访问,但已被修改,并不是很好的淘汰页。 - 3类(A=1 , M=0) :表示该页最近已被访问,但未被修改,该页有可能再被访问。 - 4类(A=1 , M=1) :表示该页最近已被访问且被修改,该页可能再被访问。 - (1)从指针所指示的当前位置开始,扫描循环队列,寻找A=0且M=0的第一类页面,将所遇到的第一个页面作为所选中的淘汰页。在第一次扫描期间不改变访问位A。 - (2)如果第一步失败,则开始第二轮扫描,寻找A=0且M= 1的第二类页面,将所遇到的第一个这类页面作为淘汰页。在第二轮扫描期间,将所有扫描过的页面的访问位都置0。 - (3)如果第二步也失败,则将指针返回到开始的位置,然后回到第一步重新开始,一定能找到被淘汰的页。 - 该算法与简单Clock算法比较,可减少磁盘的I/O操作次数。但为了找到一个可置换的页,可能须经过几轮扫描。换言之,实现该算法的开销有所增加。 - ![](../../../../assets/default.png) - 页面缓冲算法(Page Buffering Algorithm) - 影响效率的因素 - 页面置换算法。 - 写回磁盘的频率。 - 建立了一个已修改换出页面的链表,则对每一个要被换出的页面(已修改) ,系统可暂不把它们写回磁盘,而是将它们挂在已修改换出页面的链表.上,仅当被换出页面数目达到一定值时,例如64个页面,再将它们一起写回磁盘上 - 读入内存的频率。 - 如果有进程在这批数据还未写回磁盘时需要再次访问这些页面时,就不从外存上调入,而直接从已修改换出页面链表中获取,这样也可以减少将页面从磁盘读入内存的频率,减少页面换进的开销。 - 特点 - 显著地降低了页面换进、换出的频率,使磁盘I/O的操作次数大为减少,因而减少了页面换进、换出的开销; - 正是由于换入换出的开销大幅度减小,才能使其采用一种较简单的置换策略,如先进先出(FIFO)算法,它不需要特殊硬件的支持,实现起来非常简单。 - 空闲页面链表 - 该链表是一个空闲物理块链表。是系统掌握的空闲物理块,当进程需要读入一个页面时,便可利用空闲物理块来装入该页。 - 当有一个未被修改的页面要换出时,实际上并不将它换回到外存,而是把它们所在的物理块挂在空闲链表的末尾。这些挂在空闲链表中的物理块是有数据的,如果以后某个进程需要访问这些页面时,可以直接从空闲链表中取下,免除了从磁盘读入数据的操作,减少了页面换进的开销。 - 修改页面链表 - 该链表是由已修改的页面所形成的链表。设置该链表的目的是减少修改页面换出的次数。当进程需要将一个已经修改的页面换出时,系统并不立即将它换出到磁盘上,而是将它所在的物理块挂在修改页面链表的末尾。 - 当已修改页面的链表上的页面个数达到一定程度时,会-起将这些页面写回到磁盘。如果这些被换出的页面在写回磁盘之前,有进程需要访问其中的页面,那么可以直接从已修改页面链表.上获取该页面。所以可见,减少了磁盘I/O操作,并且减少了内存写入频率。 ### 访问内存的有效时间 - 查找快表时间( λ ) - 访问实际物理地址时间( t ) - 缺页中断处理时间( ε ) - 命中率(a) - 缺页率(f ) - 在内存,在快表 - EAT =快表时间+内存时间=λ+t - 在内存,不在快表 - EAT =快表时间+内存时间+修改快表时间+内存时间=λ+t+λ+t - 不在内存 - EAT =快表时间+内存时间+缺页中断时间+修改快表时间+内存时间=λ+t+ε+λ+t - 考虑命中率与缺页率 - EAT =aX(+t)+(1-a)X [fX(+t++s+i+t)+(1-f)x(2+t+i+t)]=λ+ aXt+(1-a) X [t+f X (ε+ λ+t)+(1-f) X (1+ t)] - 不考虑命中率,仅考虑缺页率, - EAT=f X (t+ε+t)+ (1-f) X(t+t)=t+f X (ε+ t)+(1-f) X t ## 抖动与工作集 ### 抖动 由于请求分页式虚拟存储器系统的性能优越,在正常运行情况下,它能有效地减少内存碎片,提高处理机的利用率和吞吐量,故是目前最常用的一种系统。 但如果在系统中运行的进程太多,进程在运行中会频繁地发生缺页情况,这又会对系统的性能产生很大的影响,故还须对请求分页系统的性能做简单的分析。 ![](../../../../assets/default.png) **刚被淘汰的页面又马.上被调回内存,调回内存不久后又被淘汰出去,如此频繁进行,这种现象称为抖动(或称颠簸)**。它使得系统中页面调度非常频繁,以致CPU大部分时间都花费在内存和外存之间的调入调出上。 发生"抖动”的根本原因是,同时在系统中运行的进程太多,由此分配给每一个进程的物理块太少,不能满足进程正常运行的基本要求。抖动是在进程运行中出现的严重问题,必须采取相应的措施解决它。 ### 工作集 ![](../../../../assets/default.png) 如果将缺页率控制在上界与下界(比如0.1%~1%) 之间,那么缺页率达到0.5%时的驻留集尺寸W将是比较适宜的。 我们无法事先预知程序在不同时刻将访问哪些页面,故仍只有像置换算法那样,用程序的过去某段时间内的行为作为程序在将来某段时间内行为的近似。 **所谓工作集,是指在某段时间间隔△里,进程实际所要访问页面的集合。** 把进程在某段时间间隔O里,在时间t的工作集记为w(t,A) ,变量△称为工作集“窗口尺寸” 对于给定的页面走向,如果△= 10次存储访问,在t1时刻的工作集是W(t1,10)=(1,2,5,6,7) ,在t2时刻,工作集是W(t2,10)=(3,4) ## 抖动的预防 - 采取局部置换策略 - 即使该进程发生了"抖动”, 也不会对其它进程产生影响,于是可把该进程”抖动”所造成的影响限制在较小的范围内 - 但在某进程发生"抖动”后,它还会长期处在磁盘I/O的等待队列中,使队列的长度增加,这会延长其它进程缺页中断的处理时间。 - 把工作集算法融入到处理机调度中 - 当调度程序发现处理机利用率低下时,它将试图从外存调入一个新作业进入内存,在从外存调入作业之前,必须检查每个进程在内存的驻留页面是否足够多。 - 如果都足够多,此时便可以从外存调入新的作业,不会因新作业的调入而导致缺页率的增加; - 反之,如果有些进程的内存页面不足,则应首先为那些缺页率居高的作业增加新的物理块,此时将不再调入新的作业。 - 利用"L=S"准则调节缺页率 - L是缺页之间的平均时间, S是平均缺页服务时间,即用于置换一个页面所需的时间。 - 如果是L远比S大,说明很少发生缺页,磁盘的能力尚未得到充分的利用; - 反之,如果是L比S小,则说明频繁发生缺页,缺页的速度已超过磁盘的处理能力。 - 只有当L与S接近时,磁盘和处理机都可达到它们的最大利用率。 - 理论和实践都已证明,利用"L=S"准则,对于调节缺页率是十分有效的。 - 选择暂停的进程 - 当多道程序度偏高时,已影响到处理机的利用率,为了防止发生“抖动”,系统必须减少多道程序的数目,以便腾出内存空间分配给那些缺页率偏高的进程。

操作系统复习 第八章

# 磁盘存储器的管理 ## 外存的组织方式 - 磁盘的物理地址(盘块号)是(柱面号,盘面号,扇区号) - 磁盘存储器中用t表示每个柱面上的磁道数(盘面数), - 用s表示每个磁道.上的扇区数, - 根据块号可以确定该块在磁盘上的位置 - 每个柱面.上有: sxt个磁盘块 - 计算第p块在磁盘上的位置,可以令d=sxt,则有: - i柱面(磁道)号=[p/d] - j盘面号=[(p mod d)/s] - k扇区号=(pmodd)mods - 文件的物理结构直接与外存的组织方式有关。对于不同的外存组织方式,将形成不同的文件物理结构。目前常用的外存组织方式有: - 顺序组织方式:早期文件系统使用,现今仅在磁盘文件对换区的使用上还能看到 其影子。 - 连续分配方式要求每个文件在磁盘上占有一-组连续的块。 - (逻辑块号,块内地址) →(物理块号,块内地址)。只需转换块号就行,块内地址保持不变 - 优点: - 顺序访问容易,支持定长记录文件随机存取; - 顺序访问速度快。 - 缺点: - 要求为一个文件分配连续的存储空间,产生外部碎片。 - 必须事先知道文件的长度。 - 不能灵活地删除和插入记录。 - 文件的动态增长困难。 - 链接组织方式:分为隐式链接和显式链接两类。FAT12、 FAT16、FAT32文件系统使用的就是显式链接方式 - 采用链接分配方式时,可通过在每个盘块.上的链接指针,将同属于一个文件的多个离散的盘块链接成一个链表, 把这样形成的文件称为链接文件。 - 链接组织方式的主要优点: - 消除了磁盘的外部碎片,提高了外存的利用率。 - 对插入、删除和修改记录都非常容易。 - 能适应文件的动态增长,无需事先知道文件的大小。 - 链接方式又可分为两种形式: - 隐式链接 - 隐式链接分配方式的主要问题: - 它只适合于顺序访问,对随机访问是及其低效的 - 可靠性差 - 为了提高检索速度和减小指针所占用的存储空间,可以将几个盘块组成一个簇(cluster)。在进行盘块分配时,是以簇为单位进行的。同样,链接文件也是以簇为单位的。 - 显式链接 - 把用于链接文件各物理块的指针显式地存放在一-张表中。 即文件分配表(FATFile Allocation Table) - 注意:一个磁盘仅设置一张FAT - 开机时,将FAT读入内存,并常驻内存 - 逻辑块号转换成物理块号的过程不需要读磁盘操作 - 文件分配表可同时管理磁盘的空闲盘块 - FAT技术 - FAT12:每个FAT表项为12位, - 每个FAT表最多允许4096 (212) 个表项,如果每个盘块大小为512 (29)B,则每个磁盘分区容量最大为2MB。 - 以簇为分配的基本单位,如果一簇包含8个扇区, 则每个磁盘分区容量为1 6MB,但是簇内碎片也会增加。 - FAT16:每个FAT表项为1 6位 - 最大表项数增值65536 (216) 个,每个簇中有最多有64个扇区,最大分区空间为216x64x 512=2048MB - FAT32 - 最大支持磁盘分区为2TB - 链接分配方式虽然解决了连续分配方式所存在的问题,但又出现了另外两个问题: - 不能支持高效的直接存取。要对一个文件进行直接存取,需首先在FAT中顺序的查找许多盘块号。 - FAT需占用较大的内存空间。当磁盘容量较大时,FAT可能要占用数MB以上的内存空间。这是令人难以忍受的。 - 索引组织方式: ext2、ext3、 ext4等UNIX文件系统采用该方式。 - 单级索引 - 索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表 - 索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表)。 - 索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块。 - 优点: - 能顺序存取,又能直接存取 - 满足了文件动态增长、插入删除的要求 - 没有外碎片,外存空间利用率较高 - 缺点 - 索引表本身需要存储空间 - 文件比较小时,索引表利用率低 - 多级索引 - 将一个大文件的所有索引表(二级索引)的地址放在另一个索引表(一级索引)中(原理类似于多级页表)。如果文件非常大,还可用三级、四级。 - 访问目标数据块,需要多次磁盘I/O - 对与中小文件,访问磁盘次数过多 - 增量式索引 - 多种索引分配方式的结合。例如,一个文件的索引结点中,既包含直接地址(直接指向数据块),又包含一次间接地址(指向单层索引表)、还包含多次间接地址(指向两层索引表) - 在UNIX System V的索引结点中设有13个地址项,即i.addr(0) ~i.addr(12) ## 文件存储空间管理 - 磁盘管理要解决的重要问题之一是如何为新创建的文件分配存储空间。 其解决方法与内存的分配情况有许多相似之处 - 连续分配方式 - 空闲表法 - 与内存的动态分配方式雷同,它为每个文件分配一块连续的存储空间。系统为外存上的所有空闲区建立一张空闲表, 每个空闲区对应于一个空闲表项,再将所有空闲区按其起始盘块号递增的次序排列 - 离散分配方式 - 空闲盘块链 - 这是将磁盘上的所有空闲空间以盘块为单位拉成一条链,其中的每一个盘块都有指向后继盘块的指针。 - 空闲盘区链 - 这是将磁盘上的所有空闲盘区拉成一条链。 在每个盘区上除含有用于指示下一个空闲盘区的指针外,还应有能指明本盘区大小(盘块数)的信息。 - 位示图法(连续离散都适用) - 利用二进制的一位来表示磁盘中一个盘块的使用情况。当其值为0时表示对 应的盘块空闲,为1时表示已分配(Linux)。 有的系统则相反。 - 磁盘上的所有盘块都有一个二进制位与之对应,这样由所有盘块所对应的位构成一个集合,称为位示图。 - 通常可用`m*n`个位数来构成位示图,并使`m*n`等磁盘的总块数。 - 存储空间的基本分配单位都是磁盘块而非字节。 - 为了实现存储空间的分配,首先必须记住空闲存储空间的使用情况。 - 数据结构 - 分配和回收功能 - 根据位示图进行盘块分配时,可分三步进行: - 顺序扫描位示图,从中找出一个或一组其值为 "0”的二进制位( "0"表示空闲时) - 将所找到的一个或-组二进制位转换成与之相应的盘块号。假定找到的其值为"0"的二进制位位于位示图的第i行、第j列,则其相应的盘块号应按下式计算:`b=n(i-1) +j`式中,n代表每行的位数 - 修改位示图,令`map[i, j] = 1` - 盘块的回收分两步: - 将回收盘块的盘块号转换成位示图中的行号和列号。转换公式为: `i= (b-1)DIV n+ 1` `j=(b-1)MODn+1` - 修改位示图。令`map[i,j] = 0`

操作系统复习 第六章

# 第六章 输入输出设备 ## I/O系统 - I/O系统管理的主要对象是I/O设备和相应的设备控制器。 - 其最主要的任务是 - 完成用户提出的I/O请求 - 提高I/O速率 - 提高设备的利用率 - 为更高层的进程方便地使用这些设备提供手段 - 基本功能 - 隐藏物理设备的细节 - 与设备的无关性 - 提高处理机和I/O设备的利用率 - 对I/O设备进行控制 - 采用轮询的可编程1/O方式 - 采用中断的可编程I/O方式 - 直接存储器访问方式 - I/O通道方式 - 确保对设备的正确共享 - 独占设备,进程应互斥地访问这类设备,即系统一旦把这类设备分配给 了某进程后,便由该进程独占,直至用完释放。典型的独占设备有打印机、磁带机等。 - 共享设备,是指在一段时间内允许多个进程同时访问的设备。典型的共享设备是磁盘,当有多个进程需对磁盘执行读、写操作时,可以交叉进行,不会影响到读、写的正确性。 - 错误处理 ![](../../../../assets/default.png) ### I/O软件 - 中断处理程序 - 当I/O任务完成时,I/O控制器会发送一个中断信号,系统会根据中断信号类型找到相应的中断处理程序并执行。 - 设备驱动程序 - 驱动程序一般会以一个独立进程的方式存在。 - 主要负责对硬件设备的具体控制,将上层发出的一系列命令(如read/write) 转化成特定设备“能听得懂”的一系列操作。包括设置设备寄存器;检查设备状态等 - 不同的I/O设备有不同的硬件特性,具体细节只有设备的厂家才知道。因此厂家需要根据设备的硬件特性设计并提供相应的驱动程序。 - 设备独立性软件 - 与设备的硬件特性无关的功能几乎都在这层实现。 - 他的主要功能有: - 向上层提供统一的调用接口,如read/write系统调用 - 设备的分配与回收 - 差错处理 - 数据缓冲区管理 - 建立逻辑设备名到物理设备名的映射关系:根据设备类型选择调用相应的驱动程序 - 用户层软件 - 实现了与用户交互的接口,用户可直接使用该层提供的、与I/O操作相关的库函数对设备进行操作。 ### 系统接口 - 块设备接口 - 块设备:用于存储信息。对于信息的存取总是以数据块为单位。典型例子是磁盘。该类设备基本特征是传输速率较高,另一特征是可寻址。工作方式常采用DMA方式。 - 隐藏了磁盘的二维结构。 - 将抽象命令映射为低层操作。 - 流设备接口 - 字符设备:用于数据的输入和输出。基本单位是字符。如交互式终端、打印机等。 - 其基本特征是传输速率较低,另一特征是不可寻址。 - 工作方式常采用中断方式。 - get和put操作。 - in-control指令。 - 网络通信接口 - 通过某种方式把计算机连接到网络上。同时操作系统也必须提供相应的网络软件和网络通信接口,使计算机能通过网络与网络.上的其它计算机进行通信或上网浏览。 ## I/O设备 - 部分 - I/O设备的机械部件主要用来执行具体I /O操作。如我们看得见摸得着的鼠标/键盘的按钮;显示器的LED屏;移动硬盘的磁臂、磁盘盘面。 - I/ O设备的电子部件通常是一块插入主板扩充槽的印刷电路板 - 分类 - 按使用特性分类 - 存储设备,也称外存或后备存储器、辅助存储器。 - 输入/输出设备 - 输入设备,如键盘、鼠标、扫描仪、视频摄像、各类传感器等。 - 输出设备,如打印机、绘图仪、显示器、音箱等。 - 交互式设备,集成上述两类设备,利用输入设备接收用户命令信息,并通过输出设备同步显示用户命令以及命令执行的结果。 - 按传输速率分类 - 低速设备,每秒钟几个字节至数百个字节 - 键盘、鼠标器、语音的输入和输出设备 - 中速设备,每秒钟数千个字节至数万个字节。 - 行式打印机、激光打印机 - 高速设备,数十万个字节至数千字节。 - 磁盘机、光盘机 ### 设备控制器 - I/O设备与CPU之间通过设备控制器通信 - 三种信号线 - 数据信号线 - 控制信号线 - 状态信号线 - ![](../../../../assets/default.png) - 设备控制器的基本功能 - 接收和识别命令 - 数据交换 - 标识和报告设备的状态 - 地址识别 - 数据缓冲区 - 差错控制 - 组成部分 - 设备控制器与处理机的接口 - 设备控制器与设备的接口 - I/O逻辑 ### 内存映像I/O ## 设备驱动程序 - 设备处理程序通常又称为设备驱动程序,它是I/O系统的高层与设备控制器之间的通信程序 - 其主要任务是接收上层软件发来的抽象I/O要求,如read或write命令,再;把它转换为具体要求后,发送给设备控制器,启动设备去执行; - 反之,它也将由设备控制器发来的信号传送给上层软件。 - 由于驱动程序与硬件密切相关,故通常应为每一类设备配置一种驱动程序。例如,打印机和显示器需要不同的驱动程序。 - 设备驱动程序的功能 - 接收由与设备无关的软件发来的命令和参数,并将命令中的抽象要求转换为与设备相关的低层操作序列。 - 检查用户I/O请求的合法性,了解I/O设备的工作状态,传递与I/O设备操作有关的参数,设置设备的工作方式。 - 发出I/O命令,如果设备空闲,便立即启动I/O设备,完成指定的I/O操作;如果设备忙碌,则将请求者的请求块挂在设备队列上等待。 - 及时响应由设备控制器发来的中断请求,并根据其中断类型,调用相应的中断处理程序进行处理。 - 处理过程 - 将抽象要求转换为具体要求 - 检查I/O请求的合法性 - 读出和检查设备的状态 - 传送必要的参数 - 启动I/O设备 - 控制方式 - 宗旨:尽量减少主机对I/O控制的干预,把主机从繁杂的IO控制事务中解脱出来 - 忙——等待方式 - CPU向控制器发出I/O指令,启动输入设备。 - 状态寄存器忙/闲标志busy置1。 - 循环测试busy, busy= 1输入未完成。 - busy=0输入完成,将数据从控制器的数据寄存器读到内存 - 由于CPU高速,I/O设备低速致使CPU极大浪费。 - 使用中断的可编程I/O方式 - CPU向控制器发出I/O指令,CPU返回继续原来的工作。 - 设备控制器控制I/O设备。CPU与I/O并行工作。 - 数据输入寄存器,控制器向CPU发出中断。 - CPU检查数据正确性,数据写入内存。 - 优点 - CPU与I/O并行工作, 提高了资源利用率和吞吐量。 - 缺点 - CPU每次处理的数据量少(通常不超过几个字节),只适于传输率较低的设备 - 直接存储器访问(Direct Memory Access) - 特点 - 数据传输的基本单位是数据块。 - 数据从设备直接送入内存,或者相反。 - 仅在传送一个或多个数据块的开始和结束时,才需CPU干预,整块数据的传送是在控制器的控制下完成的。 - DMA控制器 - 三部分 - 主机与DMA控制器的接口 - DMA控制器与块设备的接口 - I/O控制逻辑 - 四类寄存器 - 数据寄存器DR - 命令/状态寄存器CR - 内存地址寄存器MAR - 数据计数器DC - DMA工作过程 - CPU发出指令,存入CR - 内存起始目标地址送入MAR。 - 读取字节数送DC。 - 源地址送DMA的I/O控制逻辑。 - 启动DMA控制器,CPU处理其他任务。 - DMA控制读入一个字(节)到DR。 - 将该字(节)送入MAR指向的内存单元。 - MAR加1,DC减1。 - DC<> 0继续传输,DC=0发出中断。 - 中断方式是在数据缓冲寄存区满后,发中断请求,CPU进行中断处理。 - DMA方式则是在所要求传送的数据块全部传送结束时要求CPU进行中断处理,大大减少了CPU进行中断处理的次数。 - 中断方式的数据传送是由CPU控制完成的,而DMA方式则是在DMA控制器的控制下不经过CPU控制完成的。 - I/O通道控制方式 - 目的:CPU的I/O任务由通道来承担。 - 一种特殊的处理机,属于硬件技术。它具有执行I/O指令的能力,并通过执行通道程序来控制I/O操作。 - 指令类型单一、 即由于通道硬件比较简单,其所能执行的指令,主要为与I/O有关的指令 - 通道没有自己的内存,与CPU共享内存 - CPU一次读(或写)多个数据块。 - 多个数据块送入不同内存区域。 - CPU、通道和I/O设备三者的并行操作。 - 工作过程: - CPU向通道发送一条I/O指令。 - 给出通道程序首址和要访问的I/O设备。 - 通过执行通道程序完成I/O任务。 - 通道程序由一系列通道指令(通道命令)构成。 - 每条通道指令包含的信息: - 操作码 - 内存地址 - 计数 - 通道程序结束位P (P= 1表示程序结束) - 记录结束标志R (R=0表示与下一条指令处理的数据属于同一记录;R= 1表示某记录的最后一条指令) - 字节多路通道(Byte Multiplexor Channel) - 字节多路通道以字节为单位传输信息 - 含有许多非分配型子通道 - 子通道按时间片轮转方式共享通道 - 只要扫描每个子通道的速度足够快,而连接到子同上的设备的速率较小的时,不丢数据 - 主要连接以字节为单位的低速I/O设备,如:打印机、终端 ## 与设备无关的I/O软件 设备独立性又叫设备无关性,应用程序中所使用的设备,不局限于使用某个具体的物理设备 应用程序中使用逻辑设备名称来请求使用某类设备;系统将其转换为物理设备名称。 - 好处 - 设备分配时的灵活性 - 易于实现I/O重定向 - 设备独立性的实现 - 系统有专门从逻辑设备到物理设备的转换机制,类似于存储器管理中所介绍的逻辑地址和物理地址的概念 - 功能 - 执行所有设备的公有操作。 - 设备驱动程序的统一接口, 对设备进行保护,禁止用户直接访问设备 - 缓冲管理 - 差错控制 - 对独立设备的分配与回收 - 独立于设备的逻辑数据块 - 将逻辑设备名映射为物理设备名,找到相应物理设备的驱动程序 ### 设备分配 一个通道可控制多个设备控制器每个设备控制器可控制多个设备。 - 设备分配中的数据结构 - 设备控制表DCT - 每个物理设备有一张,反映设备特性、设备和I/O控制器的连接情况,在设备和系统连接时创建,动态修改 - ![](../../../../assets/default.png) - 设备分配时考虑的因素 - 设备的固有属性 - 独享设备 - 共享设备 - 虚拟设备 - 设备分配算法 - 先来先服务 - 优先级高者优先 - 设备分配中的安全性 - 安全分配方式 - 不安全分配方式 - 独占设备的分配程序 - 分配设备 - 分配控制器 - 分配通道 - 增加设备的独立性 - 使用逻辑设备名 - 逻辑设备表(LUT) - 逻辑设备表(LUT)建立了逻辑设备名与物理设备名之间的映射关系。 - 某用户进程第一次使用设备时使用逻辑设备名向操作系统发出请求 操作系统根据用户进程指定的设备类型(逻辑设备名)查找系统设备表,找到一个空闲设备分配给进程,并在LUT中增加相应表项。 - 如果之后用户进程再次通过相同的逻辑设备名请求使用设备,则操作系统通过LUT表即可知道用户进程实际要使用的是哪个物理设备了,并且也能知道该设备的驱动程序入口地址 ## 用户层I/O软件 - 系统调用 - 系统调用,应用程序可以通过它间接调用OS中的I/0过程,对I/O设备进行操作。 - 库函数 ### 假脱机系统(spooling) - 假脱机技术 - 在多道程序技术下,专门利用一道程序来模拟脱机输入操作,把低速I/O设备上的数据传送到高速磁盘上 - 再用另一道程序来模拟脱机输出操作,把数据从磁盘传送到输出设备上。 - 此时I/O设备与CPU并行工作。 - 组成 - 输入井和输出井。是磁盘上开辟的两个大存储空间。 - 输入缓冲区和输出缓冲区。在内存中开辟两个缓冲区,输入缓冲区暂存由输入设送来的数据,后送输入井 - 输出缓冲区暂存从输出井送来的数据,后送输出设备。 - 输入进程和输出进程。利用两个进程模拟脱机I/O时的外围处理机。 - 假脱机打印机 - 打印机是经常用到的输出设备,属于独占设备。、 - 利用假脱机技术可将它改造为一台可供多个用户共享的打印设备 - 假脱机打印系统主要有以下3三部分 - 磁盘缓冲区。 - 打印缓冲区。 - 假脱机管理进程和假脱机打印进程 - 特点 - 提高了I/O的速度 - 将独占设备改造为共享设备 - 实现了虚拟设备功能 ## 缓冲区管理 - 缓冲的引入 - 缓和CPU与I/O设备间速度不匹配的矛盾。 - 减少对CPU的中断频率,放宽对CPU中断响应时间的限制。 - 解决数据粒度不匹配的问题 - 提高CPU和1/O设备之间的并行性。 - 单缓冲区(Single Buffer) - 进程发出一个I/O请求时,操作系统便在主存中为之分配一缓冲区。 - ![](../../../../assets/default.png) - 双缓冲区 - 在设备输入时,先将数据送入第一缓冲区,装满后便转向第二缓冲区。此时OS可 以从第二缓冲区中移出数据,并送入用户进程。接着由CPU对数据进行计算。 - 为了实现两台机器间的双向数据传输,必须在两台机器中都设置两个缓冲区,一个用作发送缓冲区,另外一个用作接收缓冲区。 - 环形缓冲区 - CPU和外设的处理速度可能相差较大。 - 在主存中分配一组大小相等的缓冲区, 并用指针将这些缓冲区-个循环链表。 - 多个缓冲区:空缓冲区R;满缓冲区G;工作缓冲区C。 - 多个指针:下一个可用缓冲区G的指针Nextg;下一个输入缓冲区R的指针Nexti;当前计算进程正在使用的缓冲区C的指针Current。 ## 磁盘 - 结构 - 磁盘设备可包括一个或多个物理盘片,每个磁盘片分一个或两个存储面(Surface) - 每个盘面上有若干个磁道(Track),磁道之间留有必要的间隙(Gap)。为使处理简单起见,在每条磁道上可存储相同数目的二进制位。 - 磁盘密度即每英寸所存储的位数 - 每条此道从逻辑上划分位若干扇区 - 磁盘格式化 - 磁盘低级格式化(温切斯特盘) - 将空白的磁盘划分出柱面和磁道,再将磁道划分为若干个扇区, - 每个扇区又划分出标识部分ID,间隔区GAP和数据区DATA等。 - 磁盘分区 - 每个分区是一个独立的逻辑磁盘分区情况记录在磁盘的主引导扇区中的分区表(MBR)中 - 高级格式化 - 设置引导块,空闲和已分配空间表,构件文件系统,在磁盘上初始化文件系统数据结构,根目录等 - 类型 - 硬盘、软盘 - 单片盘、多片盘 - 固定磁头盘、移动磁头盘 - 磁盘访问时间 - 寻道时间T<sub>s</sub> :在读/写数据前,将磁头移动到指定磁道所花的时间。 - 启动磁头臂是需要时间的。假设耗时为s ; - 移动磁头也是需要时间的。假设磁头匀速移动,每跨越一个磁道耗时为m ,总共需要跨越n条磁道。则: - $$ T_s=s+m*n $$ - 延迟时间Tt : - 通过旋转磁盘,使磁头定位到目标扇区所需要的时间。设磁盘转速为r (单位:转/秒,或转/分),则平均所需的延迟时间 - $$ T_t=\frac{1}{2}*\frac{1}{r}=\frac{1}{2r} $$ - 传输时间Tt - 从磁盘读出或向磁盘写入数据所经历的时间,假设磁盘转速为r,此次读/写的字节数为b,每个磁道上的字节数为N - $$ T_t=\frac{b}{rN} $$ - 总的平均存取时间 - $$ T_a=T_s+\frac{1}{2r}+\frac{b}{rN} $$ ### 磁盘调度算法 - 先来先服务 - 根据进程请求访问磁盘的先后顺序进行调度。 - 优点 - 公平,简单。 - I/O负载较轻且每次读写多个连续扇区时,性能较好。 - 适用于l/O进程较少的场合。 - 最短寻道优先算法 - SSTF算法会优先处理的磁道是与当前磁头最近的磁道。 - 可以保证每次的寻道时间最短,但是并不能保证总的寻道时间最短。(贪心算法) - 可能会有进程处于“饥饿”状态。 - 扫描算法 - 只有磁头移动到最外侧磁道的时候才能往内移动,移动到最内侧磁道的时候才能往外移动。这就是扫描算法(SCAN)的思想。由于磁头移动的方式很像电梯,因此也叫电梯算法。 - 性能较好,平均寻道时间较短 - 防止“饥饿”现象。 - 被广泛应用。 - SCAN算法对于各个位置磁道的响应频率不平均 - 循环扫描算法 - 磁头单向移动,磁头朝某个特定方向移动时才处理磁道访问请求,而返回时直接快速移动至起始端而不处理任何请求。 - 比起SCAN来,对于各个位置磁道的响应频率很平均。 - 比起SCAN算法来,平均寻道时间更长。 - NStepSCAN算法 - 在高密度磁盘.上容易出现“磁臂粘着”情况。 - 将磁盘请求队列分成若干个长度为N的子队列。 - 队列之间使用FCFS算法。 - 队列内部使用SCAN算法。 - 新的I/O请求,放入其他队列,避免粘着现象。 - 当N值很大时,性能接近于SCAN算法。 - 当N=1时,蜕化为FCFS算法。 - FSCAN算法 - 是N步SCAN算法的简化。 - 磁盘请求队列分成两个子队列。 - 一个是由当前所有请求磁盘I/O的进程形成的队列,按SCAN算法进行处理。 - 新出现的所有请求磁盘I/O的进程,放入另- -个等待处理的请求队列。

操作系统复习 第七章

# 第七章 文件管理 ## 文件和文件系统 现代OS中是通过文件系统来组织和管理计算机中存储的数据 文件则是指具有文件名的若干相关元素的集合 基于文件系统的概念,**可以把数据组成分为数据项、记录和文件三级** - 文件名 - 文件名 - 扩展名 - 文件类型 - 按用途分类:系统文件、用户文件和库文件 - 按文件中数据的形式分类:源文件、目标文件和可执行文件 - 按存取控制属性分类:只执行文件、只读文件和读写文件 - 组织形式和处理方式分类:普通文件、目录文件、和特殊文件 - 文件系统模型 - 对象及其属性。文件管理系统的对象有:文件、目录和磁盘存储空间。 - 对对象操纵和管理的软件集合。是文件管理的核心部分。实现了文件系统的大部分功能——对文件存储空间的管理、对文件目录的管理、将文件的地址转换机制、对文件读写管理以及对文件的共享和保护。 - 文件系统的接口。命令接口(用户与文件系统)和程序接口(用户程序和文件系统)。 - 文件操作 - 用户通过文件系统所提供的系统调用实施对文件的操作。最基本的文件操作有:创建文件、删除文件、读文件、写文件和设置文件的读/写位置。 - 但对于一个实际的OS,为了方便用户使用文件而提供了更多地对文件的操作,如打开和关闭一一个文件及改变文件名等操作。 ## 文件的逻辑结构 - 类型 - 按文件是否有结构分类 - 有结构文件:是指由-一个以上的记录构成的文件,又把它称为记录式文件; - 无结构文件,这是指由字符流构成的文件,故又称为是流式文件。 - 根据文件的组织方式,可把有结构文件分为三类: - 顺序文件。由一系列记录按某种顺序排列所形成的文件。通常是定长记录 - 逻辑记录的排序 - 串结构:各记录之间的顺序与关键字无关。通常由时间来决定(从头开 始逐个查找) - 顺序结构:文件中的所有记录按关键字排列。可以按关键字的长短或英文字母书写排序。顺序结构的检索效率更高(可利用折半查找、插值查找法等) - 优点: - 顺序文件的最佳应用场合是在对文件中的记录进行批量存取时(即每次要读或写一大批记录)。 - 对于顺序存储设备(如磁带),也只有顺序文件才能被存储并能有效地工作。 - 缺点: - 查找和增删记录比较困难 - 顺序寻址 - 隐性寻址 - 确定下一个记录的逻辑地址 - 显性寻址 - 可对定长记录实现直接或随机访问 - 通过文件中记录的位置 - 定长记录文件,可实现随机存储 - 变长记录文件,无法实现随机存储 - 通过关键字 - 索引文件。当记录可变长时,通常为之建立一张索引表,并为每个记录设置一个表项以加快对记录检索的速度。 - 按关键字建立索引 - 索引表本身是定长记录的顺序文件。因此可以快速找到第i个记录对应的索引项。 - 可将关键字作为索引号内容若按关键字顺序排列,则还可以支持按照关键字折半查找。 - 每当要增加/删除一个记录时,需要对索引表进行修改。 - 具有多个索引表的索引文件 - 可以用不同的数据项建立多个索引表 - 索引顺序文件。上述两种方式的结合。为文件建立一张索引表,为每一组记录中的第一个记录设置一个表项。 - 将变长记录顺序文件中的所有记录分为若干个组,如50个记录为一个组。然后为顺序文件建立一张索引表,并为每组中的第一个记录在索引表中建立一个索引项,其中含有该记录的关键字和指向该记录的指针。 - 若一个顺序文件有10000个记录,则根据关键字检索文件,只能从头开始顺序 查找(这里指的并不是定长记录),平均须查找5000(N/2)个记录。 - 若采用索引顺序文件结构,可把10000个记录分为100组,每组100个记录。则需要先顺序查找索引表找到分组(共1 00个分组,因此索引表长度为100,平均需要查50次),找到分组后,再在分组中顺序查找记录(每个分组100个记录,因此平均需要查50次)。可见,采用索引顺序文件结构后,平均查找次数减少为50+50= 100次(根号N) - 二级索引顺序文件 - 为了进一步提高检索效率,可以为顺序文件建立多级索引表。例如,对于一个含10<sup>6</sup>个记录的文件,可先为该文件建立- -张低级索引表,每 100个记录为一组,故低级索引表中共有10000个表项(即10000个定长记录),再把这10000个定长记录分组,每组100个,为其建立顶级索引表,故顶级索引表中共有100个表项。 - 直接文件 - 可根据给定的关键字直接获得指定记录的物理地址。换而言之, - 关键字本身就决定了记录的物理地址。 - 哈希(Hash)文件 - 这是目前应用最为广泛的一种直接文件。 - 它利用Hash函数(或称散列函数)可将关键字转换为相应记录的地址。 ## 文件目录 通常在现代计算机系统中,都要存储大量的文件。为了能对这些文件实施有 效的管理,必须对它们加以妥善组织 - 实现"按名存取” - 提高对目录的检索速度 - 文件共享 - 允许文件重名 - 文件控制块 - 为了能对一个文件进行正确的存取,必须为文件设置用于描述和控制文件的数据结构,称之为"文件控制块(FCB) - FCB通常含有三类信息: - 基本信息类。包括:文件名,文件物理位置,文件逻辑结构,文件的物理结构 - 存取控制信息类。包括:文件主的存取权限,核准用户的存取权限和一般用户的存取权限 - 使用信息类。包括:文件的建立日期和时间、文件上次修改的日期和时间及当前使用信息。 - 文件目录 - 把所有的FCB组织在一-起,就构成了文件目录,即文件控制块的有序集合 - 目录文件 - 为了实现对文件目录的管理,通常将文件目录以文件的形式保存在外存,这个文件就叫目录文件 - 索引节点 - 文件目录通常是存放在磁盘上的,当文件很多时,文件目录要占用大量的盘块。在检索目录文件的时候,需要将目录调入内存后比较文件名,但是只用到文件名,而不需要其它那些对文件的描述信息。所以便把文件名与文件信息分开,使文件描述信息单独形成-个索引结点。 - 磁盘索引结点 - 存放在磁盘上的索引结点。每个文件有唯一-的一 个磁盘索引结点,主要包括以下内容: - 文件主标识符 - 文件类型 - 文件存取权限 - 文件物理地址 - 文件长度 - 文件链接计数 - 文件存取时间 - 内存索引结点、 - 放在内存中的索引结点。当文件被打开后,将磁盘索引结点拷贝到内存索引 结点中以便使用。 - 比磁盘索引结点增加了以下内容: - 索引结点编号 - 状态 - 访问计数; - 文件所属文件系统的逻辑设备号 - 链接指针。 - 读取文件的流程 - 根据文件名找到其所在目录中的目录项,获取该目录的对应文件的inode - 根据文件inode号,找到inode在磁盘的位置 - 根据inode中的对应关系,找到对应文件的盘块block; - 读取文件 - 文件目录 - 目录结构的组织,关系到文件系统的存取速度,也关系到文件的共享性和安全性。因此,组织好文件的目录是设计好文件系统的重要环节。 - 单级目录 - 最简单的目录结构。整个文件系统中只建立一张目录表, 每个文件一个目 录项,目录项含有文件相关信息。状态位表明每个目录项是否空闲。 - 优点 - 简单且能实现目录管理的基本功能 - 实现了"按名存取" - 缺点 - 查找速度慢 - 不允许重名。多用户环境下重名难以避免 - 不便于实现文件共享 - 两级目录 - 为了克服单级目录所存在的缺点,可以为每个用户建立一个单独的用户文件目录UFD。由该用户所有文件的文件控制块组成。 - 在系统中再建立一个主文件目录MFD。在主文件目录中每个用户目录文件都占有一个目录项,其中包括用户名和指向该用户文件的指针。 - 基本克服了单级目录的缺点,并具有以下优点: - 提高了检索目录的速度。 - 在不同的目录中可以有相同的文件名。 - 不同用户还可以使用不同的文件名来访问系统中的同一个共享文件。 - 用户不能对自己的文件进行分类 - 多级目录 - 现代操作系统通常采用三级或三级以上的目录结构,以提高目录的检索速度和文件系统的性能。多级目录结构又称为树型目录结构,主目录称为根目录,数据文件为树叶,其它目录为结点。 - 路径名 - 用户(或用户进程)要访问某个文件时要用文件路径名标识文件,文件路径名是个字符串。各级目录之间用"/"隔开。从根目录出发的路径称为绝对路径 - 当前目录 - 很多时候,用户会连续访问同一目录内的多个文件。显然,每次都从根目录开始查找是很低效的因此可以设置一个"当前目录"。 当用户想要访问某个文件时,可以使用从当前目录出发的相对路径 - 目录操作 - 创建目录 - 删除目录 - 不删除非空目录 - 可删除非空目录 - 改变目录 - 移动目录 - 链接(Link)操作 - 查找 - 目录查询技术 - 当用户要访问一个已存文件时,系统首先利用用户提供的文件名对目录进行查询,找出该文件控制块或对应索引结点; - 然后根据FCB或索引结点中所记录的文件物理地址,换算出文件在磁盘上的物理位置; - 最后通过磁盘驱动程序,将所需文件读入内存 - 线性检索法 - Hash方法 - 建立了一张Hash索引文件目录,利用Hash方法进行查询,即系统利用用户提供的文件名并将它变换为文件目录的索引值,再利用该索引值到目录中去查找,显著地提高检索速度。 - 文件打开过程 - 用户访问某文件时,调用open(),系统的工作过程: - 查找目录,找到指定的文件目录; - 核对访问权限; - 将文件目录项复制到“打开文件表(open-file table) "中 - “打开文件表”是open()系统调用关联的主要数据结构,用于记录系统中所有已打开的文件的信息。当用户再次需要访问文件时,不需要重复查找目录,而是通过返回的文件指针,直接找到“打开文件表”对应表项,进而得到文件在外存的地址。这样大大的减少了检索目录所带来的开销,提高了系统工作效率。 - 返回用户该文件在“打开文件表”中的指针fd。 - 文件关闭过程 - 当一个文件暂不使用时,就应撤消该文件在“已打开表”中的相应表目,以释放所占内存空间,这就是文件的关闭。及时关闭不用的文件,一方面节约了内存的开支, 另一方面把FCB的内容及时写回外存, 以免被意外情况破坏。 - 当用户调用close系统调用时,系统的工作过程: - 核对用户的使用权限,一般只有文件的建立者和打开者才有权关闭文件; - 根据fd查找“用户打开文件表”,释放该文件占用的表目; - 检查读入内存的文件目录和文件索引节点是否已被修改,若已被修改,则需将该文件的文件目录项和文件索引节点重新写回外存,以保存做过的修改。 ## 文件共享 操作系统为用户提供文件共享功能,可以让多个用户共享地使用同一个文件 注意:多个用户共享同一个文件,意味着系统中只有"一份"文件数据。并且只要某个用户修改了该文件的数据,其他用户也可以看到文件数据的变化。 如果是多个用户都"复制"了同一个文件,那么系统中会有"好几份"文件数据。 其中一个用户修改了自己的那份文件数据,对其他用户的文件数据并没有影响。 - 基于有向无环图的文件共享 - 有多个属于不同用户的多个目录,同时指向同-个文件,这样虽会破坏树的特性,但这些用户可用对称的方式实现文件共享,而不必再通过其属主目录来访问。 - 在文件目录中只设置文件名及指向相应索引结点的指针 - 索引结点中设置一个链接计数变量count ,用于表示链接到本索引结点上的用户目录项数。 - 利用符号链接实现文件共享 - 利用符号链接实现文件共享的基本思想,是允许一个文件或子目录有多个父目录,但其中仅有一个作为主(属主)父目录,其它的几个父目录都是通过符号链接方式与之相链接的(简称链接父目录)。 - 为使链接父目录D5能共享文件F8,可以由系统创建一个LINK类型的新文件, 也取名为F8,并将F8写入链接父目录D5中,以实现D5与文件F8的链接。在新文件F中只包含被链接文件F8的路径名。这样的链接方法被称为符号链接。新文件F中的路径名则只被看做是符号链。 - 优点 - 在利用符号链方式实现文件共享时,只是文件主才拥有指向其索引结点的 指针;而共享该文件的其他用户则只有该文件的路径名,不会出现指针悬 空的情况。 - 问题 - 当其他用户去读共享文件时,系统是根据给定的文件路径名逐个分量(名)地去查找目录,直至找到该文件的索引结点。因此,在每次访问共享文件时,都可能要多次地读盘。这使每次访问文件的开销甚大,且增加了启动磁盘的频率。 - 要为每个共享用户建立一条符号链,而由于链本身实际上是一个文件,尽管该文件非常简单,却仍要为它配置一个索引结点,这也要耗费一定的磁盘空间。 - ## 文件保护 - 访问权 - 我们把一个进程能对某对象执行操作的权力,称为访问权(Access right)。 - 保护域 - "域”是进程对一组对象访问权的集合,进程只能在指定域内执行操作 - “域”规定了进程所能访问的对象和能执行的操作 - 进程和域间的静态联系 - 在进程和域之间可以一对应,即一个进程只联系着一个域。这意味着,在进程的整个生命期中,其可用资源是固定的,我们把这种域称为'静态域” - 进程和域间的动态联系方式 - 在进程和域之间,也可以是一对多的关系,即一个进程可以联系着多个域 - 可将进程的运行分为若干个阶段,其每个阶段联系着一个域,这样便可根据运行的实际需要来规定在进程运行的每个阶段中所能访问的对象。 - 基本的访问矩阵 - 访问矩阵中的行代表域,列代表对象,矩阵中的每一项是由一组访问权组成的。 - 访问控制表(Access Control List) - 对访问矩阵按列(对象)划分,为每一列建立一张访问控制表ACL - 在该表中,已把矩阵中属于该列的所有空项删除,此时的访问控制表是由一有序对(域,权集)所组成的。 - 使用访问控制表可以显著地减少所占用的存储空间,并能提高查找速度。 - 在每个文件的FCB (或索引结点)中增加一个访问控制列表(Access-Control List, ACL)该表中记录了各个用户可以对该文件执行哪些操作。 - 精简的访问列表:以“组”为单位,标记各“组”用户可以对文件执行哪些操作。 - 如果把访问矩阵按行(即域)划分,便可由每一行构成一张访问权限表。换言之,这是由一个域对每一个对象可以执行的一组操作所构成的表。

软件工程复习 第十章

# 第十章 软件实现 ## 编程语言 在软件设计阶段,得到了实现目标系统的解决方案,并用模型图、伪代码等设计语言表述出来。编码的过程就是把软件设计阶段得到的解决方案转化为可以在计算机上运行的软件产品的过程。 选择合适的编程语言是编码过程的关键。可以说,编程语言是人与计算机交互的基本工具,它定义了计算机的一组语法规则,通过这些语法规则可以把人的意图、思想等转化为计算机可以理解的指令,进而让计算机帮助人类完成某些任务。软件开发人员使用编程语言来实现目标系统的功能。 ### 编程语言的发展与分类 - 机器语言 - 汇编语言 - 高级语言 - 超高级语言 ### 选择编程语言需要考虑的因素 - 待开发系统的应用领域,即项目的应用范围 - 用户的要求 - 将使用何种工具进行软件开发 - 软件开发人员的喜好和能力 - 软件的可移植性要求 - 算法和数据结构的复杂性 - 平台支持 ## 编程风格 编程风格是指源程序的书写习惯,比如变量的命名规则、代码的注释方法、缩进等。具有良好编程风格的源程序具有较强的可读性、可维护性,还能提高团队开发的效率。 良好的个人编程风格是优秀程序员素质的一部分,项目内部相对统一的编程风格也使得该项目的版本管理、代码评审等软件工程相关工作更容易实现。在大型软件开发项目中,为了控制软件开发的质量,保证软件开发的一致性,遵循一定的编程风格尤为重要。 ### 如何做到良好的编程风格 - 版权和版本声明应该在每个代码文件的开头声明代码的版权和版本,主要内容如下: - 版权信息 - 文件名称、标识符、摘要 - 当前版本号、作者/修改者、完成日期 - 版本历史信息。 - 在程序编写过程中应该注意代码的版式,使代码更加清晰易读。对空行、空格的使用及对代码缩进的控制与程序的视觉效果密切相关。编程人员基本积累了一些程序版式规则 - 在每个类声明之后、每个函数定义结束之后都要加空行 - 在一个函数体内,逻辑上密切相关的语句之间不加空行,其他地方应加空行分隔 - 一行代码只做一件事情,如只定义一个变量,或只写一条语句 - if、for、while、do等语句独占一行,执行语句不得紧跟其后,不论执行语句有多少都要加{} - 尽可能在定义变量的同时初始化该变量 - 关键字之后要留空格,函数名之后不要留空格,“,”之后要留空格 - 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符等二元操作符的前后应当加空格,一元操作符前后不加空格 - 程序的分界符“{”和“}”应独占一行并且位于同一列,同时与引用它们的语句左对齐 - 代码行最大长度宜控制在70~80个字符 - 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首 - 注释注释阐述了程序的细节,是软件开发人员之间以及开发人员和用户之间交流的重要途径。做好注释工作有利于日后的软件维护。注释也需要遵循一定的规则,比如注释需要提供哪些方面的信息、注释的格式、注释的位置等。 - 注释的分类 - 序言注释位于模块的起始部分,说明模块的详细信息,如模块的用途、模块的参数描述、模块的返回值描述、模块内捕获的异常类型、实现该模块的软件开发人员及实现时间、对该模块做过修改的开发人员及修改日期等 - 行内注释位于模块内部,用于解释较难理解、逻辑性强或比较重要的代码,提高代码的可理解性。 - 注释的作用: - ①版本、版权声明 - ②函数接口说明 - ③重要的代码行或段落提示。 - 使用注释的基本规则 - 注释是对代码的“提示”,而不是文档,注释的花样要尽量少 - 注释应当准确、易懂,防止注释有二义性 - 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方 - 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,以便于阅读。 - 命名规则 - 按照标识符的实际意义命名,使其名称具有直观性,能够体现标识符的语义。这样可以帮助开发人员理解和记忆标识符 - 标识符的长度应当符合“最小长度与最大信息量”原则 - 命名规则尽量与采用的操作系统或开发工具的风格一致,如缩写的使用、字母大小写的选择、对常量和变量命名的区分等 - 变量名不要过于相似,这样容易引起误解 - 在定义变量时,最好注释其含义和用途 - 程序中不要出现仅靠大小写区分的相似的标识符 - 尽量避免名称中出现数字编号,除非逻辑上的确需要编号 - 数据说明 - 在数据说明时应该遵循一定的次序 - 当在同一语句中说明相同数据类型的多个变量时,变量一般按照字母顺序排列 - 对于复杂数据结构的说明,为了容易理解,需要添加必要的注释 - 语句构造 - 不要为了节省空间而把多条语句写在一行 - 合理利用缩进来体现语句的不同层次结构 - 在含有多个条件语句的算术表达式或逻辑表达式中使用括号来清晰地表达运算顺序 - 将经常使用并且具有一定独立功能的代码封装为一个函数或公共过程 - 避免使用goto语句 - 避免使用多层嵌套语句 - 避免使用复杂的判定条件。 ## 面向对象实现 面向对象实现主要是指把面向对象设计的结果翻译成用某种程序语言书写的面向对象程序。 采用面向对象方法开发软件的基本目的和主要优点是通过重用提高软件的生产率。因此,应该优先选用能够最完善、最准确地表达问题域语义的面向对象语言。

软件工程复习 第一章

# 第一章 软件与软件工程 ## 软件 软件包括程序、程序的处理对象——数据,以及与程序开发、维护和使用有关的图文资料(文档) - 特点 - (1)软件是一种逻辑实体,而不是具体的物理实体,因而它具有抽象性。 - (2)软件没有明显的制造过程 - (3)在软件的运行和使用期间,不会出现硬件中出现的机械磨损、老化问题,然而它存在退化问题 - (4)计算机的开发与运行对计算机系统有着不同程度的依赖性。 - (5)软件开发至今尚未完全摆脱人工的开发方式。 - (6)软件本身是复杂的。 - (7)软件成本相当昂贵。 - (8)相当多的软件工作涉及社会因素。 - 分类 - ![](../../../../assets/default.png) ## 软件危机 软件危机是指计算机软件在开发和维护过程中遇到的一系列严重问题 - 表现 - (1)开发出来的软件产品不能满足用户的需求 - (2)相比越来越廉价的硬件,软件代价过高。 - (3)软件质量难以得到保证,且难以发挥硬件潜能。 - (4)难以准确估计软件开发、维护的费用以及开发周期 - (5)难以控制开发风险,开发速度赶不上市场变化。 - (6)软件产品修改维护困难,集成遗留系统更困难。 - (7)软件文档不完备,并且存在文档内容与软件产品不符的情况。 - 出现的原因 - (1)忽视软件开发前期的需求分析。 - (2)开发过程缺乏统一的、规范化的方法论的指导。 - (3)文档资料不齐全或不准确。 - (4)忽视与用户之间、开发组成员之间的交流。 - (5)忽视测试的重要性。 - (6)不重视维护或由于上述原因造成维护工作困难。 - (7)从事软件开发的专业人员对这个产业认识不充分,缺乏经验 - (8)没有完善的质量保证体系 - 启示:使我们更加深刻地认识到软件的特性以及软件产品开发的内在规律。 - (1)软件产品是复杂的人造系统,具有复杂性、不可见性和易变性,难以处理。 - (2)个人或小组在开发小型软件时使用到的非常有效的编程技术和过程,在开发大型、复杂系统时难以发挥同样的作用。 - (3)从本质上讲,软件开发的创造性成分很大,发挥的余地也很大,很接近于艺术。它介于艺术与工程之间,并逐步向工程一段漂移,但很难发展到完全的工程。 - (4)计算机和软件技术的快速发展,提高了用户对软件的期望,促进了软件产品的演化,为软件产品提出了新的、更多的需求,难以在可接受的开发进度内保证软件的质量。 - (5)几乎所有的软件项目都是新的,而且是不断变化的。项目需求在开发过程中会发生变化,而且很多原来预想不到的问题会出现,适当调整设计和实现手段是不可避免的。 - (6)“人月神化”现象——生产力与人数并不成正比。 ## 软件工程 1968年,在北大西洋公约组织举行的一次学术会议上,将其定义为“为了经济地获得可靠的和能在实际机器上高效运行的软件,而建立和使用的健全的工程规则”。 IEEE(Institute of Electrical and Electronics Engineers,电气和电子工程师协会)对软件工程的定义为: (1)将系统化、严格约束的、可量化的方法应用于软件的开发、运行和维护,即将工程化应用于软件。 (2)对(1)中所述方法的研究。 过程、方法和工具是软件工程的3个要素 - 软件工程研究的内容 - (1)软件开发技术。主要研究软件开发方法、软件开发过程、软件开发工具和环境。 - (2)软件开发过程管理。主要研究软件工程经济学和软件管理学。 - 目标和原则 - (1)达到要求的软件功能。 - (2)取得较好的软件性能。 - (3)开发出高质量的软件。 - (4)付出较低的开发成本。 - (5)需要较低的维护费用。 - (6)能按时完成开发工作,及时交付使用。 - 7条基本原则 - 用分阶段的生命周期计划严格管理 - 坚持进行阶段评审 - 实行严格的产品控制 - 采用现代程序设计技术 - 软件工程结果应能清楚地审查 - 开发小组的人员应该少而精 - 承认不断改进软件工程实践的必要性 - 15个知识领域 - 软件需求 - 软件设计 - 软件构建 - 软件测试 - 软件维护 - 软件配置管理 - 软件工程管理 - 软件工程过程 - 软件工程模型和方法 - 软件质量 - 软件工程职业实践 - 软件工程经济学 - 计算基础 - 数学基础 - 工程基础 ## 软件开发方法 - 结构化方法 - 面向数据结构方法 - 面向对象方法 - 形式化方法 ## 软件工程工具 - (1)按照功能划分:功能是对软件进行分类最常用的标准,按照功能划分,软件工程工具可分为可视化建模工具、程序开发工具、自动化测试工具、文档编辑工具、配置管理工具、项目管理工具等。 - (2)按照支持的过程划分:软件工程工具可分为设计工具、编程工具、维护工具等 - (3)按照支持的范围划分:软件工程工具可以分为窄支持、较宽支持和一般支持工具。窄支持工具支持软件工程过程中的特定任务,一般将称之为工具;较宽支持工具支持特定的过程阶段,一般由多个工具集合而成,称之为工作台;一般支持工具支持覆盖软件过程的全部或大部分阶段,包含多个不同的工作台,称之为环境。

操作系统复习 重点提纲

# 操作系统关键部分摘要 ## 引论 ### 操作系统的作用 - 作为用户与计算机硬件系统之间的接口 - 作为计算机系统资源的管理者 - 实现了对计算机资源的抽象 ### 操作系统的种类 - 单道批处理系统: - 批处理是指计算机系统对一批作业自动进行处理的一种技术。 - 为实现对作业的连续处理,需要先把一批作业以脱机方式输入到磁带上,并在系统中配上监督程序(Monitor) ,在它的控制下,使这批作业能一个接一地连续处理 - 多道批处理系统 - 在多道批处理系统中,用户所提交的作业都先存放在外存上并排成一个队列,称为“后备队列”; 然后,由作业调度程序按一定的算法从后备队列中选择若干个作业调入内存,使它们共享CPU和系统中的各种资源。 - 分时系统 - 采用时间片轮转的方法,同时为许多终端用户服务,对每个用户能保证足够快的响应时间,并提供交互会话的功能。 - 时间片:将CPU的时间划分成若干个片段,称为时间片,操作系统以时间片为单位,轮流为每个终端用户服务关键问题 - 微机操作系统 - 微型计算机操作系统 微型计算机操作系统简称微机操作系统,常用的有Windows、Mac OS、Linux。 ### 现代操作系统的特性 <mark>并发性</mark>、<mark>共享性</mark>、虚拟性、异步性 ### 操作系统程序接口 由一组系统调用组成,每一个系统调用都是一个能完成特定功能的子程序,每当应用程序要求OS提供某种服务时,便调用具有相应功能的系统调用。 ### 操作系统内核态与用户态 <mark>内核态(管态)和用户态(目态)将内核程序和用户程序隔离</mark> - <font color=red>特权指令</font> - 涉及外部设备的输入/输出指令 - 存取用于内存保护的寄存器 - 内存清零 - 置时钟 - 允许/禁用中断 <mark>中断指令</mark>是用户程序发起的调用内核代码的唯一方式 - 中断机制 - 提高多道程序环境下CPU利用率 - 外中断:中断信号来源于外部设备 - 内中断:中断信号来源于当前指令内 - 内中断的三种情况 - 陷阱/陷入:由应用程序主动引发 - 故障:由错误条件引发 - 终止:由致命错误引发 - 系统调用的核心 - 用户程序中包含一段含有int指令的代码 - 操作系统写中断处理,获取想调用程序的编号 - int指令将使CPL改成0 ,“进入内核” - 操作系统根据编号执行相应代码 ## 进程控制 ### 进程的概念 程序段、数据段、**PCB**三部分构成了进程实体,简称"进程”。 ### 进程的三种状态 - 就绪状态(Ready) - 进程已获得除CPU之外的所有必需的资源,一旦得到CPU控制权,立即可以运行 - 运行状态(Running) - 进程已获得运行所必需的资源,它正在处理机上执行。 - 阻塞状态(Blocked) - 正在执行的进程由于发生某事件而暂时无法执行时,便放弃处理机而处于暂停状态,称该进程处于阻塞状态或等待状态。 ### PCB的定义 PCB是内存中的一种数据结构,PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据) ,成为一个能独立运行的基本单位, 一个能与其他进程并发执行的进程 <mark>PCB是进程存在的唯一标志</mark> ### PCB的内容 | 类型 | 内容 | 作用 | |--------------|-----------------------------------------------------------|------------------| | 标识信息 | 1 )外部标识为方便用户<br/>2 )内部标识为方便系统 | 标识一个进程 | | 处理机状态(现场信息 ) | 1 ) CPU通用/指令寄存器<br/>2 ) CPU程序状态字PSW<br/>3 )用户栈指针 | 记录处理机现场信息,以备恢复之用 | | 调度信息 | 1 )进程状态<br/>2 )进程优先级<br/>3 )调度所需信息<br/>4)事件 | 用户进程的调度管理 | | 控制信息 | 1 )程序和数据地址<br/>2 )进程同步和通信机制<br/>3 )资源清单<br/>4 )指向下一个进程的指针 | 用于进程的控制管理 | ### 进程的创建 - 引起创建进程的事件 - 用户登录 - 作业调度 - 提供服务 - 应用请求 - 创建过程 - 申请进程标识,即申请空白PCB - 为新进程分配内存和其它资源 - 初始化进程控制块 - 将创建的进程置于就绪队列 ### 进程控制的原语 - 进程创建原语 - 进程撤销原语 - 阻塞原语 - 唤醒原语 - 挂起原语 - 激活原语 ### 临界区的定义 临界区:进程中访问临界资源的那段代码 ### <font color=red>信号量</font> - 定义:把整型信号量定义为一个用于表示资源数目的整型量S ,除初始化外仅能通过两个原子操作wait(S),signal(S)(低级原语)来访问 - 原子操作P : - 分配一个单位资源 - wait(S) - 原子操作V : - 释放一个单位资源 当信号量sign 大于0时,表示该资源该有sign个。当sign小于0时,表示有sign个进程正在等待 ### 管程 管程是由若干个公共变量和所有访问这些变量的过程所组成的一个特殊的模块或软件包 ### 其他进程同步机制(有各种各样的问题) - 关中断法(开关中断指令)也称为"硬件锁”,在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断。 - 利用Test and Set指令实现互斥:这是一种借助一条硬件指令一“测试并建立”指令TS(Test- and-Set)以实 现互斥的方法。在许多计算机中都提供了这种指令。 - 利用swap指令实现线程互斥 ### 管道的定义 - 管道:指用于连接一个读进程和一个写进程以实现他们之间通信的一个打开的共享文件,又名pipe文件。 - 管道只能采取半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道各个进程要互斥的访问管道 - 数据以字节流的形式写入管道,当管道写满时,写进程的write()系统调用将会被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程的read()系统调用将会被阻塞 ### 线程 线程(thread)是一个可执行的实体单元。<mark>它代替以往的进程,成为现代操作系统中处理机调度的基本单位。 **调度的基本单位** - 同一进程中的线程切换不会引起进程切换 - 不同进程中的线程切换必然引起进程切换 **用户级线程** - 用户级线程仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。 - 对于用户级线程的切换,通常是发生在一个应用进程的诸多线程之间,无须内核的支持。 - 切换的规则远比进程调度和切换的规则简单 **内核支持线程KST** - 在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他,们的创建、撤消和切换等,也是依靠内核实现的。 - **在内核空间还为每一个内核支持线程设置了一个线程控制块** ,内核是根据该控制块而感知某线程的存在的,并对其加以控制。 线程控制块TCB - 线程标识符; - 组寄存器,它包括程序计数器PC、状态寄存器和通用寄存器; - 线程运行状态,用于描述线程正处于何种运行状态; - 优先级,描述线程执行的优先程度; - 线程专有存储区,用于线程切换时存放现场保护信息,和相关统计信息; - 信号屏蔽,即对某些信号加以屏蔽。 - 堆栈,用来保存局部变量和返回地址。 - 用户栈和核心栈 ## 调度机制与死锁 ### 作业调度——先来先服务算法(First Come First Serve) 基本原则是按照作业到达系统的先后次序来选择,或者说它是优先考虑在系统中等待时间最长的作业,而不管该作业所需执行时间的长短。 ### 作业调度——<font color=red>短作业优先(Short Job First)调度算法</font> SJF算法是以作业的长短来计算优先级,作业越短,其优先级越高。 ### 作业调度——高响应比优先调度算法 - 为每个作业引入一个动态优先级,即优先级是可以改变的,令它随等待时间延长而增加,这将使长作业的优先级在等待期间不断地增加,等到足够的时间后,必然有机会获得处理机。该优先级的变化规律可描述为: $$ R_p=\frac{等待时间+要求服务时间}{要求服务时间}=\frac{响应时间}{要求服务时间} $$ ### 响应比 (等待时间+执行时间)/执行时间 ### [进程调度算法](https://charlesix59.github.io/2023/02/07/subject/operate_system/chapter3/#%E8%B0%83%E5%BA%A6%E7%AE%97%E6%B3%95) 考得不大具体,了解即可 ### 死锁的概念 死锁( Deadlock ) 是指多个进程在运行过程中因争夺资源而造成的一种僵局 **死锁原因** - 竞争不可抢占性资源 - 竞争可消耗资源 - 进程推进顺序不当 **产生死锁的必要条件** - 互斥条件 - 请求和保持条件 - 不可抢占条件 - 循环等待条件 #### [死锁的预防、避免、检查与解除](https://charlesix59.github.io/2023/02/07/subject/operate_system/chapter3/#%E9%A2%84%E9%98%B2%E6%AD%BB%E9%94%81) 这一块内容很多还挺重要的,慢慢看吧 ## 存储器管理 ### 目的 存储器管理的目的:提高内存利用率、方便用户 ### 程序的装入 **重定位**: 在装入时对目标程序中指令和数据的修改过程称为重定位。 **绝对装入方式**: 在编译时,如果知道程序驻留在内存的什么位置,那么编译程序将产生绝对地址的目标代码。 **可重定位装入方式(Relocation Loading Mode)**: 在多道程序环境下,可重定位装入方式,根据内存的当前情况,将装入模块装入到内存的适当位置。 **动态重定位**: - 在把装入模块装入内存后,并不立即把装入模块中的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正要执行时(访问内存之前)才进行。 ### 连续分配存储方式 - 单一连续分配:系统区加用户区,连续的分配 - 固定分区分配:将内存用户空间划分为若干个固定大小的区域,在每个分区中只装入一道作业,便可以有多道作业并发执行。 - 动态分区分配:动态分区法在作业执行前并不建立分区,分区的建立是在作业的处理过程中进行的,且其大小可随作业或进程对内存的要求而改变。<mark>分区分配算法有</mark>: - 首次适应算法:要求按地址递增的次序组织空闲区表(链)。 - 循环首次适应算法(Next Fit):每次分配不再从空闲分区链(表)的开始找起,而是从上次找到的空闲分区的下一个找起,找到一个能满足要求的空闲区。 - <font color=red>最佳适应算法(Best Fit)</font>:把能满足要求的最小空闲分区分配给作业,避免“大材小用” - 最坏适应算法(Worst Fit):总是挑选最大的空闲分区分割给作业使用 - 快速适应算法:将空闲分区按大小分类,每类具有相同容量,放入一个空闲分区链表 - 伙伴系统:无论已分配分区或空闲分区,分区大小必须是2的k次幂,k为整数, I≤k≤m - 哈希算法:哈希算法就是利用哈希快速查找优点,以及空闲分区在可利用空间表中的分布规律,建立哈希函数,构造一张以空闲分区大小为关键字的哈希表,该表的每一个表项记录了一个对应的空闲分区链表表头指针。 ### 分页存储 **页面** - 把进程的逻辑地址空间划分成若干大小相等的区域,每个区域称作**页面**或**页**。每个页都有一个编号,叫做页号。页号一般从0开始编号,如0 , 1, 2, ...等。 - 把内存空间划分成若干和页大小相同的物理块,这些物理块叫**页框**(frame)或(物理)块。同样,每个物理块也有一个编号,块号从0开始依次顺序排列。 - 以**页**为单位进行内存分配,并按进程的页数多少来分配。逻辑上相邻的页,物理上不一定相邻。 ## 虚拟存储器 虚拟存储器:是指具有请求调入功能和置换功能,能从逻辑.上对内存容量加以扩充的一种存储器系统。 <mark>虚拟内存的实际容量 = min(内存与外存之和, CPU寻址范围)</mark> <mark>虚拟存储器的实现都是建立在离散分配的存储管理方式的基础上。</mark> ### 请求分页系统 在分页系统的基础上,增加了请求调页功能和页面置换功能所形成的页式虚拟存储系统。置换时以页面为单位。 硬件支持: - 请求分页的页表机制 - 缺页中断机构 - 地址变换机构 ### 物理块分配策略 在请求分页系统中,可采取两种内存分配策略: - 固定分配策略:为每个进程分配一定数目的物理块,在整个运行期间不再改变 - 可变分配策略:先为每个进程分配一定数量的物理块,在整个运行期间可适当增多或减少。 在进行置换时,也可采取两种策略 - 全局置换:可以将操作系统保留的空闲物理快分配给进程,也可以将别的进程持有的物理块置换到外存,再分配给缺页进程。 - 局部置换:发生缺页时,只选进程自己的物理块置换 组合出的三种策略:(不能采用固定分配全局置换) - 固定分配局部置换(Fixed Allocation , Local Replacement) - 为每个进程分配一定数目的物理块,在整个运行期间不再改变。 - 采用该策略时,如果进程在运行中发现缺页,只能从该进程在内存中的n个页面中选出一页换出,然后再调入一页。 - 困难:应为每个进程分配多少个物理块难以确定。 - 可变分配全局置换(Variable Allocation , Global Replacement) - 在采用这种策略时,先为系统中的每个进程分配一定数目的物理块,而OS自身也保持一个空闲的物理块队列。 - 如果某进程发生缺页时,由系统从空闲的物理块队列中,取出一个物理块分配给该进程,并将欲调入的页装入其中。 - 当空闲物理块队列中的物理块用完后, OS才能从系统中的任一进程中选择一页调出。 - 可变分配局部置换(Variable Allocation , Local Replacement) - 为每个进程分配一定数目的物理块,如果某进程发生缺页时,只允许从该进程在内存的页面中选出一页换出,不会影响其他进程执行。 - 如果进程在运行中频繁发生缺页中断,则系统再为进程分配若干物理块 - 如果进程在运行中缺页率特别低,则适当减少分配给该进程的物理块 **交换(swap)分区**: Swap分区在系统的物理内存不够用的时候,把硬盘内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap分区中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。 ### 页面置换算法 - 最佳置换算法 - 算法无法实现,但可评价其他算法。 - 先入先出页面(FIFO)置换算法 - 算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。 - **Belady现象** - 采用FIFO算法时,如果对一个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。 - <font color=red>最近最久未使用(LeastRecently Used)算法</font> - 算法根据页面调入内存后的使用情况进行决策。由于无法预测各页面将来的使用情况,只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU置换算法是选择最近最久未使用的页面予以淘汰。 - 该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t ,当需淘汰一个页面时,选择现有页面中其t值最大的,即最近最久未使用的页面予以淘汰。 - 需有以下两类硬件之一的支持: - 寄存器 - 栈 - 最少使用置换(Least Frequently Used)算法 - 在采用LFU算法时,应为在内存中的每个页面设置一个移位寄存器,用来记录该页面被访问的频率。该置换算法选择在最近时期使用最少的页面作为淘汰页。 - 简单的Clock置换算法 - 当采用简单Clock算法时,只需为每页设置一位访问位,再将内存中的所有页面都通过链接指针链接成一个循环队列。 - 当某页被访问时,其访问位被置1。 - 置换算法在选择一页淘汰时,只需检查页的访问位。如果是0 ,就选择该页换出;若为1 ,则重新将它置0 ,暂不换出,而给该页第二次驻留内存的机会 - 再按照FIFO算法检查下一个页面。 - 当检查到队列中的最后一个页面时,若其访问位仍为1 ,则再返回到队首去检查第一个页面。 - 因此简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描 - <font color=red>改进型CLock算法</font> - 在将一个页面换出时,如果该页已被修改过,便须将该页重新写回到磁盘上;但如果该页未被修改过,则不必将它拷回磁盘。 - 在改进型Clock算法中,除须考虑页面的使用情况外,还须再增加一个因素,即置换代价,这样,选择页面换出时,既要是未使用过的页面,又要是未被修改过的页面。 - 把同时满足这两个条件的页面作为首选淘汰的页面。 - 由访问位A和修改位M可以组合成下面四种类型的页面: - 1类(A=0 , M=0) :表示该页最近既未被访问,又未被修改,是最佳淘汰页。 - 2类(A=0 , M=1) :表示该页最近未被访问,但已被修改,并不是很好的淘汰页。 - 3类(A=1 , M=0) :表示该页最近已被访问,但未被修改,该页有可能再被访问。 - 4类(A=1 , M=1) :表示该页最近已被访问且被修改,该页可能再被访问。 - (1)从指针所指示的当前位置开始,扫描循环队列,寻找A=0且M=0的第一类页面,将所遇到的第一个页面作为所选中的淘汰页。在第一次扫描期间不改变访问位A。 - (2)如果第一步失败,则开始第二轮扫描,寻找A=0且M= 1的第二类页面,将所遇到的第一个这类页面作为淘汰页。在第二轮扫描期间,将所有扫描过的页面的访问位都置0。 - (3)如果第二步也失败,则将指针返回到开始的位置,然后回到第一步重新开始,一定能找到被淘汰的页。 - 该算法与简单Clock算法比较,可减少磁盘的I/O操作次数。但为了找到一个可置换的页,可能须经过几轮扫描。换言之,实现该算法的开销有所增加。 ### 预防抖动 <mark>刚被淘汰的页面又马上被调回内存,调回内存不久后又被淘汰出去,如此频繁进行,这种现象称为抖动(或称颠簸)</mark> **所谓工作集,是指在某段时间间隔△里,进程实际所要访问页面的集合。** <font color=red>预防抖动的策略</font> - 采取局部置换策略 - 把工作集算法融入到处理机调度中 - 利用"L=S"准则调节缺页率 - 选择暂停的进程 - 当多道程序度偏高时,已影响到处理机的利用率,为了防止发生“抖动”,系统必须减少多道程序的数目,以便腾出内存空间分配给那些缺页率偏高的进程。 ## 输入输出设备 ### I/O 系统 (四个层级从低到高) - 中断处理程序 - 当I/O任务完成时,I/O控制器会发送一个中断信号,系统会根据中断信号类型找到相应的中断处理程序并执行。 - 设备驱动程序 - 驱动程序一般会以一个独立进程的方式存在。 - 主要负责对硬件设备的具体控制,将上层发出的一系列命令(如read/write) 转化成特定设备“能听得懂”的一系列操作。包括设置设备寄存器;检查设备状态等 - 设备独立性软件 - 他的主要功能有: - 向上层提供统一的调用接口,如read/write系统调用 - 设备的分配与回收 - 差错处理 - 数据缓冲区管理 - 建立逻辑设备名到物理设备名的映射关系:根据设备类型选择调用相应的驱动程序 - 用户层软件 - 实现了与用户交互的接口,用户可直接使用该层提供的、与I/O操作相关的库函数对设备进行操作。 ### 设备驱动设备的控制方式 控制方式 - 宗旨:尽量减少主机对I/O控制的干预,把主机从繁杂的IO控制事务中解脱出来 - 忙——等待方式 - CPU向控制器发出I/O指令,启动输入设备。 - 状态寄存器忙/闲标志busy置1。 - 循环测试busy, busy= 1输入未完成。 - busy=0输入完成,将数据从控制器的数据寄存器读到内存 - 由于CPU高速,I/O设备低速致使CPU极大浪费。 - 使用中断的可编程I/O方式 - CPU向控制器发出I/O指令,CPU返回继续原来的工作。 - 设备控制器控制I/O设备。CPU与I/O并行工作。 - 数据输入寄存器,控制器向CPU发出中断。 - CPU检查数据正确性,数据写入内存。 - <font color=red>直接存储器访问(Direct Memory Access)</font> - 特点 - 数据传输的基本单位是数据块。 - 数据从设备直接送入内存,或者相反。 - 仅在传送一个或多个数据块的开始和结束时,才需CPU干预,整块数据的传送是在控制器的控制下完成的。 - DMA控制器 - 三部分 - 主机与DMA控制器的接口 - DMA控制器与块设备的接口 - I/O控制逻辑 - 四类寄存器 - 数据寄存器DR - 命令/状态寄存器CR - 内存地址寄存器MAR - 数据计数器DC - DMA工作过程 - CPU发出指令,存入CR - 内存起始目标地址送入MAR。 - 读取字节数送DC。 - 源地址送DMA的I/O控制逻辑。 - 启动DMA控制器,CPU处理其他任务。 - DMA控制读入一个字(节)到DR。 - 将该字(节)送入MAR指向的内存单元。 - MAR加1,DC减1。 - DC<> 0继续传输,DC=0发出中断。 - 中断方式是在数据缓冲寄存区满后,发中断请求,CPU进行中断处理。 - DMA方式则是在所要求传送的数据块全部传送结束时要求CPU进行中断处理,大大减少了CPU进行中断处理的次数。 - 中断方式的数据传送是由CPU控制完成的,而DMA方式则是在DMA控制器的控制下不经过CPU控制完成的。 - I/O通道控制方式 - 目的:CPU的I/O任务由通道来承担。 - 一种特殊的处理机,属于硬件技术。它具有执行I/O指令的能力,并通过执行通道程序来控制I/O操作。 - 指令类型单一、 即由于通道硬件比较简单,其所能执行的指令,主要为与I/O有关的指令 - 通道没有自己的内存,与CPU共享内存 - CPU一次读(或写)多个数据块。 - 多个数据块送入不同内存区域。 - CPU、通道和I/O设备三者的并行操作。 - 工作过程: - CPU向通道发送一条I/O指令。 - 给出通道程序首址和要访问的I/O设备。 - 通过执行通道程序完成I/O任务。 - 通道程序由一系列通道指令(通道命令)构成。 - 每条通道指令包含的信息: - 操作码 - 内存地址 - 计数 - 通道程序结束位P (P= 1表示程序结束) - 记录结束标志R (R=0表示与下一条指令处理的数据属于同一记录;R= 1表示某记录的最后一条指令) - 字节多路通道(Byte Multiplexor Channel) - 字节多路通道以字节为单位传输信息 - 含有许多非分配型子通道 - 子通道按时间片轮转方式共享通道 - 只要扫描每个子通道的速度足够快,而连接到子同上的设备的速率较小的时,不丢数据 - 主要连接以字节为单位的低速I/O设备,如:打印机、终端 ### 设备独立性 <font color=red>用户编程时使用的硬件名称与实际使用的设备名称不同</font> ### 假脱机系统(spooling) 在多道程序技术下,专门利用一道程序来模拟脱机输入操作,<font color=red>把低速I/O设备上的数据传送到高速磁盘上</font>, 再用另一道程序来模拟脱机输出操作,把数据从磁盘传送到输出设备上。 - 此时I/O设备与CPU并行工作。 ### 缓冲区管理 单缓冲区(Single Buffer) - 进程发出一个I/O请求时,操作系统便在主存中为之分配一缓冲区。 双缓冲区 - 在设备输入时,先将数据送入第一缓冲区,装满后便转向第二缓冲区。此时OS可以从第二缓冲区中移出数据,并送入用户进程。接着由CPU对数据进行计算。 - 为了实现两台机器间的双向数据传输,必须在两台机器中都设置两个缓冲区,一个用作发送缓冲区,另外一个用作接收缓冲区。 ## 文件管理 文件逻辑结构是为了方便用户而设计的 ### 文件的逻辑结构 顺序文件:由一系列记录按某种顺序排列所形成的文件。通常是定长记录 索引文件:当记录可变长时,通常为之建立一张索引表,并为每个记录设置一个表项以加快对记录检索的速度。 索引顺序文件:上述两种方式的结合。为文件建立一张索引表,为每一组记录中的第一个记录设置一个表项。 二级索引顺序文件:为了进一步提高检索效率,可以为顺序文件建立多级索引表。 ### 文件控制块 - 为了能对一个文件进行正确的存取,必须为文件设置用于描述和控制文件的数据结构,称之为"文件控制块(FCB) - FCB通常含有三类信息: - 基本信息类。包括:文件名,文件物理位置,文件逻辑结构,文件的物理结构 - 存取控制信息类。包括:文件主的存取权限,核准用户的存取权限和一般用户的存取权限 - 使用信息类。包括:文件的建立日期和时间、文件上次修改的日期和时间及当前使用信息。 ### 文件目录 文件目录通常是存放在磁盘上的,当文件很多时,文件目录要占用大量的盘块。在检索目录文件的时候,需要将目录调入内存后比较文件名,但是只用到文件名,而不需要其它那些对文件的描述信息。所以便把文件名与文件信息分开,使文件描述信息单独形成一个索引结点。 ### 软连接与硬链接 硬链接 - 软链接相当于Windows的快捷方式 - 软链接里面存放的是源文件的路径,指向源文件 - 删除源文件,软链接文件依然存在,但是无法通过软链接访问源文件,已经失效,并且白字红底闪烁 - 软链接和源文件是不通的文件,iNode号不同,文件类型也不同 - 所有连接文件的权限都是777,而实际权限是由链接指向的源文件权限决定的 软连接 - 具有相同iNode节点号的多个文件,互为硬链接文件 - 删除硬链接文件或者源文件任意之一,文件实体并未被删除,只有删除了所有硬链接文件和源文件,文件实体才被删除 - 硬链接文件只是文件的另一个入口 - 链接文件和源文件属性相同 - 不能跨分区,不能对目录使用 ## 硬盘存储器管理 ### 外存组织方式(文件的物理结构) - 顺序组织方式 - 每个文件在磁盘上占有一组连续的块 - 链接组织方式 - 可通过在每个盘块上的链接指针,将同属于一个文件的多个离散的盘块链接成一个链表 - 显式链接:把用于链接文件各物理块的指针显式地存放在一张表中。 即文件分配表(FATFile Allocation Table) - 索引组织方式 - 索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表 - 索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表)。 - 索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块。 ### 文件存储空间管理 存储空间的管理实际上是对外存未占用空间的管理 - 连续分配方式 - 空闲表法:为每个文件分配一块连续的存储空间。系统为外存上的所有空闲区建立一张空闲表, 每个空闲区对应于一个空闲表项,再将所有空闲区按其起始盘块号递增的次序排列 - 离散分配方式 - 空闲盘块链:磁盘上的所有空闲空间以盘块为单位拉成一条链,其中的每一个盘块都有指向后继盘块的指针。 - 空闲盘区链:将磁盘上的所有空闲盘区拉成一条链。 在每个盘区上除含有用于指示下一个空闲盘区的指针外,还应有能指明本盘区大小(盘块数)的信息。 - <font color=red>位示图法(连续离散都适用)</font> - 利用二进制的一位来表示磁盘中一个盘块的使用情况。当其值为0时表示对 应的盘块空闲,为1时表示已分配(Linux)。 有的系统则相反。 - 磁盘上的所有盘块都有一个二进制位与之对应,这样由所有盘块所对应的位构成一个集合,称为位示图。 - 通常可用`m*n`个位数来构成位示图,并使`m*n`等磁盘的总块数。

软件工程复习 第十一章

# 第十一章 软件测试 **软件测试**是发现软件中错误和缺陷的主要手段。为了保证软件产品的质量,软件开发人员通过软件测试发现产品中存在的问题,并及时修改 **软件缺陷**是指软件产品中存在的问题,具体表现为用户所需的功能没有实现,无法满足用户的需求。 在软件开发过程的任何阶段都可能引入缺陷。缺陷被引入的阶段越早,在软件开发后期修复这些缺陷造成的成本损失就越大。 **软件测试工作应该贯穿于整个开发过程。** - 原则 - 完全测试是不可能的 - 测试中存在风险 - 软件测试只能表明缺陷存在,而不能证明软件产品已经没有缺陷 - 软件产品中潜在的错误数与已发现的错误数成正比 - 让不同的测试人员参与到测试工作中 - 让开发小组和测试小组分立,开发工作和测试工作不能由同一部分人来完成 - 尽早并不断地进行测试,使测试工作贯穿于整个软件开发的过程中 - 在设计测试用例时,应包括输入数据和预期的输出结果两个部分,并且输入数据不仅包括合法的情况,还应该包括非法的输入情况 - 要集中测试容易出错或错误较多的模块 - 应该长期保留所有的测试用例 - 软件测试模型 - 要素 - 测试的时间 - 测试的步骤 - 如何计划测试 - 不同阶段的测试中应该关注哪些测试对象 - 测试过程中应该考虑哪些问题 - 测试需要达到的目标等。 - 模型 - V - W - H - 按照质量因素划分的软件测试 - 功能测试。,检验最终的软件产品是否实现了需求规格说明书中的所有功能需求 - 可靠性测试。关注于程序输出结果的准确性 - 可用性测试。用来衡量处理服务请求时,应用程序的可用频率 - 性能测试 - 安全性测试 - 配置测试。考察软件系统是否能在多种硬件平台上正常运行 - 兼容性测试.主要关注软件的运行平台和应用系统的版本、标准和规范、数据的共享性 - 安装测试 - 文档测试 - 软件国际化测试和本地化测试 - α测试和β测试。它们都属于验收测试的范畴,是在系统测试之后,产品发布之前进行的测试过程的最后一个阶段。 ## 测试用例 为达到最佳的测试效果或高效地揭露隐藏的错误而精心设计并执行的少量测试数据,称为测试用例。 我们不可能进行穷举测试,为了节省时间和资源,提高测试效率,必须从数量极大的可用测试数据中精心挑选出具有代表性或特殊性的测试数据来进行测试。 测试用例=测试数据+预期测试结果( +测试环境) 测试结果=测试数据+期望结果+实际结果 <mark>一个好的测试用例是在于它能发现至今未发现的错误。</mark> ## 软件测试方法 ### 静态审查 - 自查 - 会审 - 走查 ### 动态审查 #### 黑盒测试 - 定义 - 在黑盒测试中,测试人员把被测试的软件系统看成是一个黑盒子,不需要关心盒子的内部结构和内部特性,只关注软件产品的输入数据和输出结果,从而检查软件产品是否符合它的功能说明。 - 等价类划分法 - 把被测对象的输入域划分为有限个等价区段——“等价类”,以有针对性的等价类少量测试,代替漫无边际的、数量较大的“穷尽”测试或随机测试。 - 分类 - 有效等价类是指对程序的规格说明是有意义的、由合理的输入数据构成的集合 - 无效等价类是指对程序的规格说明是无意义的、由不合理的输入数据构成的集合 - 原则 - 如果输入条件规定了取值范围或个数,则可确定一个有效等价类和两个无效等价类 - 如果输入条件规定了输入值的集合或是规定了“必须如何”的条件,则可确定一个有效等价类和一个无效等价类 - 如果输入条件是布尔表达式,则可以分为一个有效等价类和一个无效等价类 - 如果输入条件是一组值,且程序对不同的值有不同的处理方式,则每个允许的输入值对应一个有效等价类,所有不允许的输入值的集合为一个无效等价类 - 如果规定了输入数据必须遵循的规则,则可以划分出一个有效的等价类(符合规则)和若干无效的等价类(从不同的角度违反规则) - 如已划分的等价类各元素在程序中的处理方式不同,则应将此等价类进一步划分成更小的等价类 - 边界值分析法 - 定义 - 经验表明,处理边界情况时程序最容易发生错误 - 边界类型:下标、数据结构、循环、选择等的边界附近 - 通常选用等价类边界值作为边界值测试的数据 - 一般边界值分析法作为等价类划分法的补充与细化。 - 原则 - 以输入条件的边界的值作为测试用例 - 若规定了值的数量的边界作为测试用例 - 针对每个输出条件 - 输入或输出范围是有序的集合,应注意选取有序集的第一个和最后一个元素作为测试用例 - 分析规格说明,找出其他的可能边界条件 - 错误推测法 - 错误推测法在很大程度上靠直觉和经验进行。它的基本想法是列举出程序中可能有的错误和容易发生错误的特殊情况,并且根据它们选择测试方案。 - 因果图法 - 定义 - 等价类划分法和边界值分析法都主要考虑输入条件,而没有考虑输入条件的各种组合以及各个输入条件之间的相互制约关系。因此,必须考虑描述多种条件的组合,相应地产生多个动作的形式来考虑设计测试用例。这就需要利用因果图法。 - 因果图法是一种黑盒测试方法,它从用自然语言书写的程序规格说明书中寻找因果关系,即利用输入条件与输出和程序状态的改变,由因果图产生判定表。它能够帮助人们按照一定的步骤高效地选择测试用例,还能指出程序规格说明书中存在的问题。 - 使用 - 用C表示原因,E表示结果 - 各节点表示状态 - 取值0表示某状态不出现,取值1表示某状态出现 - 四种关系符号 - ![](../../../../assets/default.png) - 表示约束的符号 - ![](../../../../assets/default.png) - E约束(互斥):表示a和b两个原因不会同时成立,最多有一个可以成立 - I约束(包含):表示a和b两个原因至少有一个必须成立 - O约束(唯一):表示a和b两个条件必须有且仅有一个成立 - R约束(要求):表示a出现时,b也必须出现。 - M约束(强制)表示a是1时,b必须为0。 - 步骤 - 分析程序规格说明书的描述中,哪些是原因,哪些是结果 - 分析程序规格说明书中描述的语义内容,并将其表示成连接各个原因与各个结果的因果图 - 有些原因和结果的组合情况是不可能出现的,为表明这些特定的情况,在因果图上使用若干特殊的符号标明约束条件 - 把因果图转化为决策表 - 为决策表中每一列表示的情况设计测试用例 - 决策表法 - 定义 - 在一些数据处理问题中,某些操作是否实施依赖于多个逻辑条件的取值。在由这些逻辑条件取值的组合构成的多种情况下,分别执行不同的操作。处理这类问题的一个非常有力的工具就是决策表 - 决策表(也称判定表)是分析和表达多逻辑条件下执行不同操作的情况的工具,可以比较明确地表达复杂的逻辑关系和多种条件组合的情况 - 组成 - 条件桩:列出问题的所有条件 - 条件项:列出所列条件下的取值,以及在所有可能情况下的真假值 - 动作桩:列出问题规定可能采取的动作 - 动作项:列出在条件项的各种取值情况下应采取的动作 - 在简化并得到最终决策表后,只要选择适当的输入,满足决策表每一列的输入条件,即可生成测试用例。 - 场景法 - 定义 - 现在很多软件都是用事件触发来控制流程,事件触发时的情形变形成场景,而同一事件不同的触发顺序和处理结果就形成了事件流。这种在软件设计中的思想也可以应用到软件测试中,可生动地描绘出事件触发时的情形,有利于测试者执行测试用例,也更容易理解和执行测试用例。 - 组成 - 基本流:采用黑直线表示,是经过用例的最简单路径,表示无任何差错,程序从开始执行到结束 - 采用不同颜色表示,一个备选流可以从基本流开始,在某个特定条件下执行,然后重新加入基本流中,也可以起源于另一个备选流,或终止用例,不再加入基本流中。 - 步骤 - 根据规格说明,描述出程序的基本流和各个备选流 - 根据基本流和各个备选流生成不同的场景 - 对每一个场景生成相应的测试用例 - 复审生成的所有测试用例,去掉多余的测试用例,确定每一个测试用例的测试数据。 - 选择 - 在任何情况下都必须选择边界值分析方法。经验表明用这种方法设计出的测试用例发现程序错误的能力最强 - 必要时用等价类划分法补充-些测试用例 - 用错误推测法再追加一些测试用例 - 如果程序的功能说明中含有输入条件的组合情况,则可选用因果图法和决策表法 #### 白盒测试 白盒测试有时也被称为玻璃盒测试,它关注软件产品的内部细节和逻辑结构,即把被测的程序看成是一个透明的盒子。 白盒测试利用构建层设计的一部分而描述控制结果来生成测试用例。白盒测试需要清楚了解系统内部结构和工作原理。 - 逻辑覆盖法 - 逻辑覆盖法以程序内在的逻辑结构为基础,根据程序的流程图设计测试用例 - 测试步骤 - 选择逻辑覆盖标准 - 按照覆盖标准列出所有情况 - 选择确定测试用例 - 验证分析运行结果与预期结果 - 语句覆盖:指选择足够的测试用例,使被测语句的每个语句至少执行一次 - 判定覆盖:指选择足够的测试用例,使每个判定的所有可能结果至少出现一次。 - 条件覆盖:指选择足够的测试用例,使判定中的每个条件的所有可能结果至少出现一次 - 判定/条件覆盖:指选择足够的测试用例,使判定中的每个条件的所有可能结果至少出现一次,并且每个判定中条件结果的所有可能组合也至少出现一次。 - 条件组合覆盖:指选择足够的测试用例,使每个判定中条件结果的所有可能组合至少出现一次。 - 路径覆盖:指选择足够的测试用例,使流程图中的每条路径至少经过一次。 - 基本路径法 - 基本路径法是在程序控制流图的基础上,通过分析控制构造的环路复杂性,导出基本可执行的路径集合,从而设计测试用例的方法。**使用基本路径法设计出的测试用例要保证在测试中程序的每条可执行语句至少执行一次。** - 步骤 - 画出控制流图 - 计算环路复杂度 - 导出测试数据 - 准备测试用例 - 圈复杂度的计算 - 给定流图G的圈复杂度=流图中边的数量,-流图中结点的数量 - 给定流图G的圈复杂度=流图G中判定结点的数量+1 - 白盒方法的选择 - 白盒测试还有静态质量度量、域测试、Z路径覆盖等方法 - 选择方法的几条经验 - 在测试中,可采取先静态再动态的组合方式,先进行代码检查和静态结构分析,再进行覆盖测试 - 将静态分析的结果作为引导,通过代码检查和动态测试的方式进一步确认静态分析的结果 - 覆盖测试是白盒测试的重点,一般可使用基本路径法达到语句覆盖标准,对于软件的重点模块,应使用多种覆盖标准衡量测试的覆盖率 - 不同测试阶段的测试重点不同,在单元测试阶段,以代码检查、覆盖测试为主,在集成测试阶段,需要增加静态结构分析等,在系统测试阶段,应根据黑盒测试的结果,采用相应的白盒测试方法。 - 白盒与黑盒的比较 - | 白盒测试 | 黑盒测试 | | --- | --- | | 考察程序逻辑结构 | 不涉及程序结构 | | 用程序结构信息生成测试用例 | 用软件规格说明书生成测试用例 | | 主要适用于单元测试和集成测试 | 可适用于从单元测试到系统验收测试 | | 测试所有逻辑路径 | 某些代码段得不到测试 | #### 灰盒测试 灰盒测试是介于白盒测试和黑盒测试之间的测试方法,它关注输出对于输入的正确性,也关注内部表现,但是不像白盒测试那样详细、完整,只是通过一些表征性的现象、事件、标志来判断内部的运行状态。有时候输出是正确的,但是程序内部已经是错误的,这种情况非常多,如果每次都通过白盒测试来操作,效率会很低,因此可采取灰盒测试这种方法。 灰盒测试结合了白盒测试和黑盒测试的要素,考虑了用户端、特定的系统知识和操作环境。它在系统组件的协同性环境中评价应用软件的设计。可以认为,集成测试就是一类灰盒测试。 ## 测试的步骤 ### 单元测试 在进行单元测试时,被测试的单元本身不是独立的程序,需要为其开发驱动模块和桩模块。 **驱动模块**是用来模拟待测试模块的上级模块。驱动模块在集成测试中接受测试数据,将相关的数据传送给待测模块,启动待测模块,并打印出相应的结果 **桩模块**也称为存根程序,用以模拟待测模块工作过程中调用的模块。 桩模块由待测模块调用,它们一般只进行很少的数据处理,例如打印入口和返回,以便于检验**待测模块与下级模块**的接口。 ### 集成测试 - 组装测试也称集成测试,是在单元测试的基础上,将所有模块按照软件设计要求组装成执行子系统、功能子系统直至应用系统并进行测试的过程。 - 测试内容:主要是模块间的结构和通信,发现软件设计阶段产生的错误 - 测试方法:黑盒测试 - 注意问题 - 接口数据丢失:连接各模块时,穿越模块接口的数据是否会丢失 - 模块功能干扰:一个模块的功能是否对其他模块的功能产生不利影响。 - 组合成功:各个子功能组合起来,能否达到预期的父功能。 - 整体数据结构:全局数据结构是否有问题 - 误差积累:单个模块的误差是否会累积、放大 - 影响数据库:单个模块的错误是否会导致数据库错误 - ~~非增量组装测试方式~~ - 一次组装,然后测试 - 不使用 - 增量组装测试方式 - 增量式集成测试中单元的集成是逐步实现的,集成测试也是逐步完成的 - 一边测试一边组装 - 自顶向下增量式集成测试 - 这种组装方式是将模块按系统程序结构,沿控制层次自项向下进行组装。 - 这种方式在测试过程中较早地验证了主要的控制点和判断点。在一个功能划分合理的程序结构中,判断常出现在较高的层次,因而能较早地遇到。如果主要控制有问题,尽早发现能够大大减少以后的返工。 - 优点:能够尽早发现系统主控方面的问题,驱动真实,上层测试充分。 - 缺点:需要设计大量桩模块、进行大里回归测试,测试用例设计麻烦。 - 自底向上增量式集成测试 - 这种组装方式从程序结构的最底层模块开始组装和测试。 - 由于模块是由底向.上进行组装的,对于一个给定层次的模块,它的子模块(包括子模块的所有下属模块)已经组装并测试完成,以不再要桩模块。在模块的测试过程中需要从子模块得到的信息可以直接得到。 - 基本增殖步骤 - 编写驱动模块,把最底层的模块组合成子功能模块族。 - 用实际模块替换驱动模块,把子功能族组合起来形成更大的功能族。 - 为子系统编写驱动模块,进行新的测试过程。 - 重复步骤②、③,直到完成所有的模块组装测试。 - 优点:桩模块真实,驱动模块和测试用例容易编写;能够尽早查出底层涉及较复杂的算法和实际的I/0模块中的错误。 缺点:系统结构建立晚,系统协调、功能接口问题发现的晚。 - 深度优先与宽度优先 - 无论是自顶向下还是由底向上的方式,都可以选择深度优先或者宽度优先增量方式 - 混合方式(实际使用) ### 确认测试 - 确进一步验证软件的有效性,即验证软件的功能、性能及其他特性是否与用户的要求一致。 - 测试依据:需求规格说明书 - 测试人员:专门测试部门、专业用户、典型用户、专家 - 有效性测试:制定测试计划,运用黑盒法验证软件特性是否与需求符合。 - 软件配置复查:软件配置指软件工程过程中所产生的所有信息项一一文档、报告、程序、表格、数据。随着软件工程过程的进展,软件配置项(SCI software Configuration Item)快速增加和变化,应复查SCI是否齐全。 - 确认测试结果 - 在全部确认测试的测试用例运行完后,所有的测试结果可以分为两类: - 测试结果与预期的结果相符-一这说明软件的这部分功能或性能特征与需求说明书相符合,从而这部分程序可以接受。 - 测试结果与预期的结果不符一-这说明软件的这部分功能或性能特征与需求说明书不一致,因此需要开列一张软件各项缺陷表或软件问题报告,通过与用户的交流,解决所发现的缺陷和错误。 - 测试类型 - 功能测试:根据需求规格说明书和测试需求列表,验证产品的功能是否符合需求规格 - 性能测试:用来测试软件系统在实际的集成系统中的运行性能 - 安装测试:用来确保软件在正常情况和异常情况的不同条件下都不丢失数据或者功能,具体测试活动包括首次安装、升级、完整安装、自定义安装、卸载等 - 可用性测试:检验其是否达到可用性标准 - 压力测试:不是在常规条件下运行手动或自动测试,而是长时间或超大负荷地运行测试软件来测试被测系统的性能、可靠性、稳定性等 - 容量测试:目的是通过测试预先分析出反映软件系统应用特征的某项指标的极限值(如最大并发用户数、数据库记录数等),系统在该极限值下没有出现任何软件故障或还能保持主要功能正常运行 - 安全性测试:目的是验证系统的保护机制是否能够在实际的环境中抵御非法入侵、恶意攻击等非法行为 - 健壮性测试:指在故障存在的情况下,软件还能正常运行的能力 - 图形用户界面测试:包含两方面内容,一是界面实现与界面设计是否吻合,二是界面功能是否正确 - 文档测试:文档包括开发文档、管理文档和用户文档3种 ### 验收测试 验收测试是在系统测试之后进行的测试,目的是验证新建系统产品是否能够满足用户的需要,产品通过验收测试工作才能最终结束。具体说来,验收测试就是根据各自需求说明书的标准,利用工具进行的一项检查工作,其中包括对进程的验收、进程质量是否达到需求说明书的要求,以及是否符合工程的设计要求等,验收测试可分为前阶段验收和竣工验收两个阶段。 验收测试是依据软件开发商和用户之间的合同、软件需求说明书以及相关行业标准、国家标准、法律法规等的要求对软件的功能、性能、可靠性、易用性、可维护性、可移植性等特性进行严格的测试,验证软件的功能、性能及其他特性是否与用户需求一致。 α测试:是用户在开发环境下的测试,或者是开发公司组织内部人员模拟各类用户行为,对即将面市的软件产品进行的测试,它是由开发人员或测试人员进行的测试。在α测试中,主要是确认使用的功能和任务,测试的内容由用户需求说明书决定。α测试是试图发现软件产品的错误的测试,它的关键在于尽可能逼真地模拟实际运行环境和用户对软件产品的操作,并尽最大努力涵盖所有可能的用户操作方式。 β测试:β测试由最终用户实施,通常开发(或其他非最终用户)组织对其的管理很少或不管理。β测试是所有验收测试策略中最主观的:测试员负责创建自己的环境、选择数据,并决定要研究的功能、特性或任务,采用的方法完全由测试员决定。 ### 回归测试 回归测试是指软件系统被修改或扩充后重新进行的测试,回归测试是为了保证软件修改后,没有引入新的错误而重复进行的测试 ## 面向对象的软件测试 在基于面向对象思想的软件开发中,由于面向对象的软件工程方法与传统的软件工程方法有诸多不同,所以传统的软件测试模型对面向对象的软件系统已经不再适用。 在面向对象的软件开发中,人们已经抛弃了传统的测试模型。针对面向对象的开发模型中的面向对象分析(OOA)、面向对象设计(OOD)、面向对象实现(OOP)3个阶段 ### 面向对象分析的测试 结构化需求分析把目标系统看成是一个由若干功能模块组成的集合,而面向对象需求分析以现实世界中的概念为模型结构。前者关注系统的行为,即功能结构,后者更关注于系统的逻辑结构。对面向对象需求分析的测试,要考虑以下方面: - 对认定的对象或类的测试 - 对定义的属性和操作的测试 - 对类之间层次关系的测试 - 对对象之间交互行为的测试 - 对系统逻辑模型的测试等 ### 面向对象设计的测试 与传统的软件工程方法不同的是,面向对象分析和面向对象设计之间并没有严格的界限。实际上,面向对象设计是对面向对象分析结果的进一步细化、纠正和完善。对面向对象设计的测试涉及了面向对象分析的测试内容,但是会更加关注对类及其类之间关系的测试和对类库支持情况的测试。 ### 面向对象实现的测试 面向对象的程序具有封装、继承和多态的特性。测试多态的特性时要尤为注意,因为它使得同一段代码的行为复杂化,测试时需要考虑不同的执行情况和行为。由于系统功能的实现分布在类中,所以本阶段的测试中还要重点评判类是否实现了要求的功能。 ### 面向对象的单元测试 面向对象的单元测试以类或对象为单位。由于类包含一组不同的操作,并且某些特殊的操作可能被多个类共享,因此单元测试不能孤立地测试某个操作,而是将操作作为类的一部分。 ### 面向对象的集成测试 面向对象的集成测试采用基于线程或者基于使用的测试方法。基于线程的测试是指把回应系统外界输入的一组相关的类集成起来,对线程进行集成并测试。基于使用的测试方法按照类对服务器的依赖以及对其他类的依赖程度,把类划分为独立类和依赖类。 独立类是指那些几乎不使用服务器的类。在进行基于使用的测试时,先测试独立类。 依赖类是使用独立类的类,即它们对独立类存在某种程度的依赖。 在测试完独立类后,就可以测试依赖类了。依赖类可能还划分为多个层次,测试时按照逐层向下的顺序,直到测试完整个系统。 ### 面向对象的系统测试及验收测试 在系统测试的过程中,软件开发人员要尽量搭建与用户的实际使用环境相同的平台,检测和评估目标系统是否能作为一个整体,满足用户在性能、功能、安全性、可靠性等各个方面的要求。面向对象的系统测试要以面向对象需求分析的结果为依据,验证需求分析中描述的对象模型、交互模型等各种分析模型。 ## 软件调试 调试(也称为纠错)是在测试发现错误之后排除错误的过程。 虽然调试可以而且应该是一个有序的过程,但是在很大程度上它仍然是一项技巧。软件工程师在评估测试结果时,往往仅面对软件问题的症状,也就是说,错误的外部表现和它的内在原因之间可能并没有明显的联系。调试就是把症状和原因联系起来的尚未被人很好理解的智力过程。 - 强行排错 - 适合于结构比较简单的程序。是使用较多,效率较低的方法。 - 在程序中插入打印语句 - 将内存和寄存器的内容打印或显示出来,然后从中找出错误的原因 - 屏蔽部分程序 - 把不需要执行的语句段加上注释符号,使其不再运行 - 在不需要执行的语句段前加上判断值为假的if语句,使其不再运行 - 调试完成后再恢复 - 借助于调试工具 - 回溯排错 - 演绎排错

软件工程复习 第三章

# 第三章 可行性研究与软件开发计划 ## 项目立项 任何一个完整的软件工程项目都是从项目立项开始的。 - 项目立项包括项目发起、项目论证、项目审核和项目立项4个过程 - 在发起一个项目时,<mark>项目发起</mark>人或单位为寻求他人的支持,要以书面材料的形式递交给项目的支持者和领导,使其明白项目的必要性和可行性。 <mark>项目论证</mark>过程,也就是可行性研究过程。可行性研究就是指在项目进行开发之前,根据项目发起文件和实际情况,对该项目是否能在特定的资源、时间等制约条件下完成做出评估,并且确定它是否值得去开发。 项目经过可行性研究并且认为可行后,还需要报告主管领导或单位,以获得<mark>项目</mark>的进一步<mark>审核</mark>,并得到他们的支持。 项目通过可行性研究和主管部门的批准后,将其列入项目计划的过程,叫做<mark>项目立项</mark>。 ## 可行性研究的任务 可行性研究需要从多个方面进行评估,主要包括:战略可行性、操作可行性、计划可行性、技术可行性、社会可行性、市场可行性、经济可行性和风险可行性等。 - (1)战略可行性研究主要从整体的角度考虑项目是否可行,如提出的系统对组织目标具有怎样的贡献、新系统对目前的部门和组织结构有何影响、系统将以何种方式影响人力水平和现存雇员的技术、它对组织整个人员开发策略有何影响,等等。 - (2)操作可行性研究主要考虑系统是否能够真正解决问题;系统一旦安装后,是否有足够的人力资源来运行系统;用户对新系统具有抵触情绪是否可能使操作不可行;人员的可行性等问题。 - (3)计划可行性研究主要估计项目完成所需的时间并评估项目的时间是否足够。 - (4)技术可行性研究主要考虑项目使用技术的成熟程度;与竞争者的技术相比,所采用技术的优势及缺陷;技术转换成本;技术发展趋势及所采用技术的发展前景;技术选择的制约条件等。 - (5)社会可行性研究主要考虑项目是否满足所有项目涉及者的利益;是否满足法律或合同的要求等。 - (6)市场可行性研究主要包括研究市场发展历史与发展趋势,说明本产品处于市场的什么发展阶段;本产品和同类产品的价格分析;统计当前市场的总额、竞争对手所占的份额,分析本产品能占多少份额;分析产品消费群体的特征、消费方式以及影响市场的因素;分析竞争对手的市场状况;分析竞争对手在研发、销售、资金、品牌等方面的实力;分析自己的实力等。 - (7)经济可行性研究主要是研究系统开发和运行需要的成本与得到的效益,分析成本效益。 - (8)风险可行性研究主要是考虑项目在实施过程中可能遇到的各种风险因素,以及每种风险因素可能出现的概率和出现后的影响程度。 ## 技术可行性 技术可行性主要研究待开发系统的功能、性能和限制条件,确定现有技术能否实现有关的解决方案,在现有的资源条件下实现新系统的技术风险有多大。这里的资源条件是指已有的或可以得到的软硬件资源、现有开发项目人员的技术水平和已有的工作基础。 在评估技术可行性时,需要考虑以下情况:了解当前最先进的技术,分析相关技术的发展是否支持新系统;确定资源的有效性,如新系统的软硬件资源是否具备,开发项目的人员在技术和时间上是否可行等;分析项目开发的技术风险,即能在给定的资源和时间等条件下,设计并实现系统的功能和性能等。 <mark>技术可行性研究往往是系统开发过程中难度最大的工作,是可行性研究的关键。</mark> ## 操作可行性 - 操作可行性是对开发系统在一个给定的工作环境中能否运行或运行好坏程度的衡量。操作可行性研究决定在当前的政治意识形态、法律法规、社会道德、民族意识以及系统运行的组织机构或人员等环境下,系统的操作是否可行。 - 操作可行性往往最容易被忽视或被低估,或者认为它一定是可行的。 ## 经济可行性 成本-效益分析是可行性研究的重要内容,它用于评估基于项目的经济合理性,给出项目开发的成本论证,并将估算的成本与预期的利润进行对比。 一般说来,基于项目的成本由4个部分组成:购置并安装软硬件及有关设备的费用,项目开发费用,软硬件系统安装、运行和维护费用,人员的培训费用。在项目的分析和设计阶段只能得到上述费用的预算,即估算成本。在项目开发完毕并将系统交付用户运行后,上述费用的统计结果就是实际成本。 项目开发效益包括经济效益和社会效益两部分。 - 经济效益是指所使用系统为用户增加的收入,可以通过直接的或统计的方法估算。 - 社会效益只能用定性的方法估算。 - 成本估算 - 代码行技术 - 代码行技术是比较简单的定量估算方法,它将开发每个软件功能的成本和实现这个功能需要使用的源代码行数联系起来。通常根据经验和历史数据估算实现一个功能所需的源代码行数。一旦估算出源代码行数后,用每行代码的平均成本乘以行数即可确定软件的成本。每行代码的平均成本主要取决于软件的复杂程度和薪资水平。 - 任务分解技术 - 首先将开发项目分解为若干相对独立的任务,再分别估算每个任务单独开发的成本,最后累加起来就可得出开发项目的总成本。 - 如果项目比较复杂,如由若干子系统组成,则可以将若干子系统再按开发阶段进一步划分成更小的任务。 - ![](../../../../assets/default.png) - 成本——效益分析 - 分析 - 开发成本:用代码行技术或任务分解技术进行估算。 - 运行费用:取决于系统操作的费用(操作员人数、工作时间和消耗物资等),以及维护费用。 - 系统的经济效益:为使用新系统而增加的收入加上使用新系统可以节省的运行费用。 - 四个考虑方面 - 货币的时间价值 - 投资回收期:累计的经济效益等于最初投资所需要的时间,也就是达到估计开发总成本加_上运行维护费用所需要的时间。 - 纯收入=累计经济效益(折合成现在值)一投资额 - 投资回收率=年经营净现金流量或年均经营净现金流量/原始投资额 - 货币的时间价值 - 通常使用利率的形式表示货币的时间价值。假设年利率为i,如果现在存入P元,则n年后可以得到的价值为: $$ F=P(1+i)^n $$             F就是P元在n年后的价值。反之,如果n年后能收入F元,那么这些钱的现在价值就是: $$ P=\frac{F}{(1+i)^n} $$ - 投资回报期 - 投资回收期是衡量项目价值的常用方法。投资回收期就是使累计的经济效益等于最初投资需要的时间。很明显,投资回收期越短,获得利润的速度就越快,该项目就越值得开发。 - 纯收入 - 纯收入是衡量项目价值的另一项经济指标。纯收入就是在软件生命周期中软件系统的累计经济效益(折合成现在值)与投资之差。 ## 可行性研究步骤 - 步骤 - 明确系统的目标在这一步,可行性分析人员要访问相关人员,阅读分析可以掌握的材料,确认用户需要解决问题的实质,进而明确系统的目标以及为了达到这些目标系统所需的各种资源。 - 分析研究现行系统现行系统是新系统重要的信息来源。新系统应该完成现行系统的基本功能,并在此基础上对现行系统中存在的问题进行改善或修复。可以从3个方面分析现有系统:系统组织结构定义、系统处理流程分析和系统数据流分析。 - 设计新系统的高层逻辑模型这一步从较高层次设想新系统的逻辑模型,概括地描述开发人员对新系统的理解和设想。 - 获得并比较可行的方案开发人员可根据新系统的高层逻辑模型提出实现此模型的不同方案。在设计方案的过程中要从技术、经济等角度考虑各方案的可行性,然后从多个方案中选择出最合适的方案。 - 撰写可行性研究报告可行性研究的最后一步就是撰写可行性研究报告。此报告包括项目简介、可行性分析过程和结论等内容。 - 结论 - (1)可以按计划进行软件项目的开发。 - (2)需要解决某些存在的问题(如资金短缺、设备陈旧和开发人员短缺等)或者需要对现有的解决方案进行一些调整或改善后才能进行软件项目开发。 - (3)一旦待开发的软件项目不具有可行性,就立即停止该软件项目。 ## 制定开发计划 - 项目概述。说明项目的各项主要工作;说明软件的功能和性能;为完成项目应具备的条件;甲方和乙方应承担的工作、完成期限和其他限制条件;应交付软件的名称,所使用的开发语言及存储形式;应交付的文档等。 - 实施计划。说明任务的划分,各项任务的责任人;说明项目开发进度,按阶段应完成的任务,用图表说明每项任务的开始时间和完成时间;说明项目的预算,各阶段的费用支出预算等。 - 人员组织及分工。说明开发该项目所需人员的类型、组成结构和数量等。 - 交付期限。说明项目应交付的日期等。

软件工程复习 第二章

# 第二章 软件工程 ## 软件过程 软件的诞生和生命周期是一个过程,我们总体上称这个过程为软件过程 ## 软件生命周期 软件产品的生命周期是指从设计该产品的构想开始,到软件需求的确定、软件设计、软件实现、产品测试与验收、投入使用以及产品版本的不断更新,到最终该产品被市场淘汰的全过程。 - 生命周期的划分原则 - 各阶段的任务应尽可能相对独立; - 同一阶段各项任务的性质尽可能相同。 - 划分生命周期的优点 - 有利于软件开发工程的组织和管理 - 降低了整个软件开发过程的困难程度 - 对每个阶段都可选用最优的管理方法 - 保证软件质量、提高生产效率. - 划分 - 软件定义 - 问题的定义与可行性研究 - 需求分析 - 软件开发 - 软件设计 - 程序编码 - 软件测试 - 软件维护 ## 软件过程模型 人们通过建立抽象的软件开发模型(也称软件过程模型或软件生命周期模型),把软件生命周期中的各个活动或步骤安排到一个框架中,将软件开发的全过程清晰、直观地表达出来 软件开发模型的内在特征有以下4点: - (1)软件开发模型描述了主要的开发阶段 - (2)软件开发模型定义了每个阶段要完成的主要任务和活动 - (3)软件开发模型规范了每个阶段的输入和输出 - (4)软件开发模型提供了一个框架,把必要的活动映射到这个框架中 ### 瀑布模型 - ![](../../../../assets/default.png) - 优点 - 过程模型简单,执行容易 - 缺点 - 无法适应变更。 - 适用情况 - (1)在软件开发的过程中,需求不发生或很少发生变化,并且开发人员可以一次性获取到全部需求 - (2)软件开发人员具有丰富的经验,对软件应用领域很熟悉。 - (3)软件项目的风险较低。瀑布模型不具有完善的风险控制机制。 - 变体 - v模型 - ![](../../../../assets/default.png) ### 快速原型模型 快速原型的基本思想是快速建立一个能反映用户主要需求的原型系统,让用户在计算机上试用它,通过实践来了解目标系统的概貌。 - ![](../../../../assets/default.png) - 优点 - 不带反馈环 - 适用情况 - (1)已有产品或产品的原型(样品),只需客户化的工程项目。 - (2)简单而熟悉的行业或领域。 - (3)有快速原型开发工具。 - (4)进行产品移植或升级。 ### 增量模型 增量模型是把待开发的软件系统模块化,将每个模块作为一个增量组件,从而分批次地分析、设计、编码和测试这些增量组件 - ![](../../../../assets/default.png) - 优点 - (1)将待开发的软件系统模块化,可以分批次提交软件产品,使用户可以及时了解软件项目的进展。 - (2)以组件为单位进行开发降低了软件开发的风险。一个开发周期内的错误不会影响到整个软件系统。 - (3)开发顺序灵活。开发人员可以对构件的实现顺序进行优先级排序,先完成需求稳定的核心组件。当组件的优先级发生变化时,还能及时调整实现顺序。 - 缺点 - 要求待开发的软件系统可以被模块化。 - 适用情况 - (1)软件产品可以分批次地交付。 - (2)待开发的软件系统能够被模块化。 - (3)软件开发人员对应用领域不熟悉,难以一次性地开发系统。 - (4)项目管理人员把握全局的水平较高。 ### 螺旋模型 螺旋模型是一种用于开发风险较大的大型软件项目的开发模型。该模型将瀑布模型与快速原型模型结合起来,并且加入了这两种模型忽略了的风险分析。 ![](../../../../assets/default.png) - 优点 - 将风险分析扩展到各个阶段中,大幅度降低了软件开发的风险。 - 缺点 - 控制和管理较为复杂 - 可操作性不强 - 对项目管理人员的要求较高。 ### 喷泉模型 在分析阶段,定义类和对象之间的关系,建立对象-关系和对象-行为模型。在设计阶段,从实现的角度对分析阶段模型进行修改或扩展。在编码阶段,使用面向对象的编程语言和方法实现设计模型。在面向对象的方法中,分析模型和设计模型采用相同的符号标示体系,各阶段之间没有明显的界限,而且常常重复、迭代地进行。 ![](../../../../assets/default.png) - 适用情况 - 喷泉模型主要用于面向对象的软件项目 - 软件的某个部分通常被重复多次 - 相关对象在每次迭代中随之加入渐进的软件成分。 ### 基于组件的开发模型 基于组件的开发模型使用现有的组件以及系统框架进行产品开发,由于现有组件大多已经历实际应用的反复检验,因此其可靠性相对新研发组件高出很多。 ![](../../../../assets/default.png) - 优点 - 极大地提高了产品开发效率 - 质量也得到了提高 ### 统一软件开发过程模型 统一软件开发过程(Rational Unified Process,RUP)模型是基于UML(统一建模语言)的一种面向对象软件开发模型。采用迭代和增量递进的开发策略,并以用例驱动为特点,集中了多个软件开发模型的优点。RUP模型是迭代模型的一种。 ![](../../../../assets/default.png) ### 敏捷过程与极限编程 敏捷方法是一种轻量级的软件工程方法,相对于传统的软件工程方法,它更强调软件开发过程中各种变化的必然性,通过团队成员之间充分的交流与沟通以及合理的机制来有效地响应变化。 - 四个价值观 - 个体与交互高于过程和工具 - 可运行软件高于详尽的文档 - 与客户协作高于合同(契约)谈判 - 对变更及时响应高于遵循计划 - 12条原则 - (1)首先要做的是通过尽早和持续交付有价值的软件来让客户满意。 - (2)需求变更可以发生在整个软件的开发过程中,即使在开发后期,我们也欢迎客户对于需求的变更。敏捷过程利用变更为客户创造竞争优势。 - (3)经常交付可工作的软件。交付的时间间隔越短越好,最好2~3周一次。 - (4)在整个的软件开发周期中,业务人员和开发人员应该天天在一起工作。 - (5)围绕受激励的个人构建项目,给他们提供所需的环境和支持,并且信任他们能够完成工作。 - (6)在团队的内部,最有效果和效率的信息传递方法是面对面交谈。 - (7)可工作的软件是进度的首要度量标准。 - (8)敏捷过程提倡可持续的开发速度。责任人、开发人员和用户应该能够保持一种长期稳定的开发速度。 - (9)不断地关注优秀的技能和好的设计会增强敏捷能力。 - (10)尽量使工作简单化。 - (11)好的架构、需求和设计来源于自组织团队。 - (12)每隔一定时间,团队应该反省如何才能有效地工作,并相应调整自己的行为。 - 实践方式 - 极限编程(eXtreme Programming,XP) - 自适应软件开发(Adaptive Software Development,ASD) - 动态系统开发方法(Dynamic System Development Method,DSDM) - ScrumCrystal和特征驱动开发(Feature Driven Development,FDD) - 极限编程 - 极限编程是一种实践性较强的规范化的软件开发方法,它强调用户需求和团队工作。 - 适用情况 - 软件需求模糊且容易改变 - 开发团队少于10人 - 开发地点集中 - ![](../../../../assets/default.png) - ![](../../../../assets/default.png) ### 选择软件开发模型 - (1)符合软件自身的特性,如规模、成本和复杂性等 - (2)满足软件开发进度的要求 - (3)对软件开发的风险进行预防和控制 - (4)具有计算机辅助工具的支持。 - (5)与用户和软件开发人员的知识和技能相匹配 - (6)有利于软件开发的管理和控制

软件工程复习 第四章

# 第四章 结构化分析 ## 需求分析 - 作用 - 为了开发出真正满足用户需要的软件产品,明确了解用户需求是关键。 - 需求分析就是要回答“系统必须做什么” - 在需求中会存在大量的错误,这些错误若未及时发现和更正,就会造成软件开发费用增加、软件质量降低,严重时,会造成软件开发失败 - 需求分析是非常重要的过程,它完成的好坏直接影响后续软件开发的质量。 - 方面 - **确定系统的运行环境要求系统运行时的环境要求包括** - 硬件环境要求,如对计算机的CPU、内存、存储器、输入/输出方式 - 通信接口和外围设备等的要求 - 软件环境要求,如操作系统、数据库管理系统和编程语言等的要求。 - **确定系统的功能性需求和非功能性需求** - 功能需求是软件系统最基本的需求表述,包括对系统应该提供的服务,如何对输入做出反应以及系统在特定条件下的行为描述。 - 非功能性需求包括对系统提出的性能需求、可靠性和可用性需求、系统安全以及系统对开发过程、时间、资源等方面的约束和标准等。 - 进行有效的需求分析 - 原则 - 需求分析是一个过程,它应该贯穿于系统的整个生命周期中,而不是仅仅属于软件生命周期早期的一项工作。 - 需求分析应该是一个迭代的过程。通常情况下,需求是随着项目的深入而不断变化的。 - 为了方便评审和后续的设计,需求的表述应该具体、清晰,并且是可测量的、可实现的。最好能够对需求进行适当的量化 - 两个任务 - 需求分析的建模阶段,即在充分了解需求的基础上,建立起系统的分析模型。 - 需求分析的描述阶段,就是把需求文档化,用软件需求规格说明书的方式把需求表达出来。 - 软件需求规格说明书 - 软件需求规格说明书是需求分析阶段的输出,它全面、清晰地描述了用户需求,因此是开发人员进行后续软件设计的重要依据。软件需求规格说明书应该具有清晰性、无二义性、一致性和准确性等特点。同时,它还需通过严格的需求验证、反复修改的过程才能最终确定。 - 步骤 - **需求获取** - 观察 - 体验 - 问卷调查 - 访谈 - 单据分析 - 报表分析 - 需求调研会 - 分析建模 - 获取需求后,下一步就应该对开发的系统建立分析模型了。模型就是为了理解事物而对事物做出的一种抽象,通常由一组符号和组织这些符号的规则组成。对待开发系统建立各种角度的模型有助于人们更好地理解问题 - 需求描述 - 需求描述就是指编制需求分析阶段的文档。一般情况下,复杂的软件系统在需求阶段会产生3个文档:**系统定义文档(用户需求报告)、系统需求文档(系统需求规格说明书)、软件需求文档(软件需求规格说明书)**。而对简单的软件系统而言,需求阶段只需要输出<mark>软件需求文档</mark>就可以了。 - 需求验证 - 需求分析阶段的工作成果是后续软件开发的重要基础,为了提高软件开发的质量,降低软件开发的成本,必须对需求的正确性进行严格的验证,确保需求的一致性、完整性、现实性、有效性,确保设计与实现过程中的需求可回溯性,并进行需求变更管理 - 需求管理 - 为了更好地进行需求分析并记录需求结果,需要进行需求管理。 - 需求管理是一种用于查找、记录、组织和跟踪系统需求变更的系统化方法。可用于: - (1)获取、组织和记录系统需求。 - (2)使客户和项目团队在系统变更需求上达成并保持一致。 - 有效需求管理的关键在于维护需求的阐述足够明确、每种需求类型适用的属性,以及与其他需求和其他项目工件之间的可追踪性。 - 需求管理实际上是项目管理的一部分,它涉及以下3个主要问题 - (1)识别、分类、组织需求,并为需求建立文档。 - (2)需求变化,即带有建立对需求不可避免的变化是如何提出、如何协商、如何验证以及如何形成文档的过程。 - (3)需求的可跟踪性,即带有维护需求之间以及与系统的其他制品之间依赖关系的过程。 - 需求分析的常用方法 - 功能分解方法 - 功能分解方法是将一个系统看成是由若干功能模块组成的,每个功能又可分解为若干子功能及接口,子功能再继续分解,即功能、子功能和功能接口为功能分解方法的3个要素。功能分解方法采用自顶向下、逐步求精的理念。 - 结构化分析方法 - 结构化分析方法是一种从问题空间到某种表示的映射方法,其逻辑模型由数据流图和数据词典构成并表示。它是一种面向数据流的需求分析方法 - 信息建模方法 - 模型是用某种媒介对相同媒介或其他媒介中的一些事物进行模拟表现的形式。从一个建模角度出发,模型就是要抓住事物最重要的方面而简化或忽略其他方面。简而言之,模型就是对现实的简化。建立模型的过程称为建模。 - 建模可以帮助理解正在开发的系统,这是需要建模的一个基本理由,并且,人对复杂问题的理解能力是有限的。建模可以帮助开发者缩小问题的范围,每次着重研究一个方面,进而对整个系统产生更加深刻的理解。可以明确地说,越大、越复杂的系统,建模就越重要。 - 面向对象的分析方法 - 面向对象的分析方法的关键是识别问题域内的对象,分析它们之间的关系,并建立3类模型。 - 它们分别是 - 描述系统静态结构的对象模型 - 描述系统控制结构的动态模型 - 描述系统计算结构的功能模型 - 其中,对象模型是最基本、最核心、最重要的。面向对象主要考虑类或对象、结构与连接、继承和封装、消息通信,只表示面向对象分析中几项最重要的特征。类的对象是对问题域中事物的完整映射,包括事物的数据特征(即属性)和行为特征(即服务) ## 结构化分析概述 一种考虑数据和处理的需求分析方法被称为结构化分析(Structured Analysis,SA)方法,它是20世纪70年代由Yourdon Constaintine及DeMarco等人提出和发展,并得到广泛应用的。它基于“分解”和“抽象”的基本思想,逐步建立目标系统的逻辑模型,进而描绘出满足用户要求的软件系统。 结构化分析的具体步骤如下。 - 建立当前系统的“具体模型”。 - 抽象出当前系统的逻辑模型。 - 建立目标系统的逻辑模型。 - 为了完整地描述目标系统,还需要考虑人机界面和其他一些问题。 ## 结构化分析方法 ![](../../../../assets/default.png) - 此模型的核心是“数据字典”,它描述软件使用或产生的所有数据对象。 - 围绕这个核心有3种图 - “数据流图”指出当数据在软件系统中移动时怎样变换,以及描绘变换数据流的功能和子功能,用于功能建模 - “实体-关系图”(E-R图)描绘数据对象之间的关系,用于数据建模 - “状态转换图”指明作为外部事件结果的系统行为,用于行为建模。 - 结构化分析方法必须遵守下述准则。 - (1)必须定义软件应完成的功能,这条准则要求建立功能模型。 - (2)必须理解和表示问题的信息域,根据这条准则建立数据模型。 - (3)必须表示作为外部事件结果的软件行为,这条准则要求建立行为模型。 - (4)必须对描述功能、信息和行为的模型进行分解,用层次的方式展示细节。 - (5)分析过程应该从要素信息移向实现细节。 - 不同的模型往往表述系统需求的某一方面,而模型之间又相互关联,相互补充。 ### 功能建模 功能建模的思想就是用抽象模型的概念,按照软件内部数据传递和变换的关系,自顶向下逐层分解,直到找到满足功能要求的可实现的软件为止。<mark>功能模型用数据流图来描述。</mark> 数据流图(简称DFD图)就是采用图形方式来表达系统的逻辑功能、数据在系统内部的逻辑流向和逻辑变换过程,是结构化系统分析方法的主要表达工具及用于表示软件模型的一种图示方法。 - 数据流图 - 4种符号 - (1)外部实体:表示数据的源点或终点,它是系统之外的实体,可以是人、物或者其他系统。 - (2)数据流:表示数据流的流动方向。数据流可以从加工流向加工、从加工流向文件、从文件流向加工。 - (3)数据变换:表示对数据进行加工或处理,比如对数据的算法分析和科学计算。 - (4)数据存储:表示输入或输出文件。这些文件可以是计算机系统中的外部或者内部文件,也可以是表、账单等。 - Yourdon表示法 - (1)矩形表示数据的外部实体。 - (2)圆形泡泡表示变换数据的处理逻辑。 - (3)两条平行线表示数据的存储。 - (4)箭头表示数据流。 - 环境图 - 环境图也称为系统顶层数据流图(或0层数据流图),它仅包括一个数据处理过程,也就是要开发的目标系统。 - 环境图的作用是确定系统在其环境中的位置,通过确定系统的输入和输出与外部实体的关系确定其边界。 - 根据结构化需求分析采用的“自顶向下,由外到内,逐层分解”的思想,开发人员要先画出系统顶层的数据流图,然后再逐层画出低层的数据流图。顶层的数据流图要定义系统范围,并描述系统与外界的数据联系,它是对系统架构的高度概括和抽象。底层的数据流图是对系统某个部分的精细描述。 - 遵守的原则 - (1)第0层的数据流图应将软件描述为一个泡泡。 - (2)主要的输入和输出应该仔细标记。 - (3)通过分离在下一层表示的候选处理过程、数据对象和数据存储,开始求精过程。 - (4)应使用有意义的名称标记所有的箭头和泡泡。 - (5)当从一个层转移到另一个层时要保持信息流的连续性。 - (6)一次精化一个泡泡。 - 数据流图的分层 - 对于稍微复杂一些的实际问题,在数据流图上常常出现十几个甚至几十.个加工,这样的数据流图看起来不直观,不易理解,分层的数据流图能很好地解决这一问题。 - 按照系统的层次结构进行逐步分解,并以分层的数据流图反映这种结构关系,能清楚地表达和容易理解整个系统。 - 原则 - 数据守恒与数据封闭原则 - 所谓数据守恒是指加工的输入输出数据流是否匹配,即每一-个加工既有输入数据流又有输出数据流。或者说一个加工至少有一 个输入数据流,一个输出数据流。 - 分解加工的原则 - 自然性:概念上合理、清晰; - 均匀性:理想的分解是将一个问题分解成大小均匀的几个部分; - 分解度:每个加工每次分解一般不超过7士2个子加工;分解到基本加工为止。 - 子图与父图的“平衡” - 父图中某个加工的输入输出数据流应该同相应的子图的输入输出相同(或相对应),分层数据流图的这种特点称为子图与父图“平衡” - 合理使用文件 - 当文件作为某些加工之间的交界面时,文件必须画出来,一旦文件作为数据流图中的一个独立成份画出来了,那么同其他成份之间的联系也应同时表达出来。 ### 数据建模 - 数据模型中包含3种相互关联的信息 - 数据对象 - 数据对象是对软件必须理解的复合信息的抽象。数据对象可以是外部实体、事物、行为、事件、角色、单位、地点或结构等。总之,可以由一组属性来定义的实体都可以被认为是数据对象 - 数据对象的属性 - 属性定义了数据对象的性质 - 必须把一个或多个属性定义为“标识符”,也就是说,当人们希望找到数据对象的一个实例时,用标识符属性作为“关键字”(通常简称为“键”) - 数据对象彼此间相互连接的关系 - 客观世界中的事物彼此间往往是有联系的 - 数据对象彼此之间相互连接的方式称为联系,也称为关系 - 关系用菱形表示,并用无向边分别与有关实体连接起来,以此描述实体之间的关系 - 关联数量的表示 - ![](../../../../assets/default.png) - 关系本身也可能有属性, - 关系属性的表示 - 在表示关系的无向边上再加一个菱形框,并在菱形框中标明关系的名字,关系的属性同样用椭圆形或圆角矩形表示,并用无向边将关系与其属性连接起来。 ### 行为建模 状态转化图 - 状态转换图是一种描述系统对内部或外部事件响应的行为模型。它描述系统状态和事件,事件引发系统在状态间的转换,而不是描述系统中数据的流动。这种模型尤其适合用来描述实时系统,因为这类系统多是由外部环境的激励而驱动的。 - 优点 - 状态之间的关系能够直观地捕捉到 - 由于状态转换图的单纯性,能够机械地分析许多情况,可以很容易地建立分析工具。 - 状态转换图能够很方便地对应状态转换表等其他描述工具 - 并不是所有的系统都需要画状态转换图,有时系统中的某些数据对象在不同状态下会呈现不同的行为方式,此时应分析数据对象的状态,画出状态转换图,才可正确认识数据对象的行为,并定义其行为。对这些行为规则较复杂的数据对象需要进行如下分析。 - 找出数据对象的所有状态 - 分析在不同的状态下,数据对象的行为规则是否不同,若无不同,则可将其合并成一种状态 - 分析从一种状态可以转换成哪几种状态,是数据对象的什么行为导致这种状态的转换。 - 状态 - 状态是任何可以被观察到的系统行为模式,一个状态代表系统的一种行为模式 - 在状态转换图中定义的状态主要有 - 初态(即初始状态) - 终态(即最终状态) - 中间状态 - **一张状态图中只能有一个初态,而终态可以没有,也可以有多个** - 状态中的活动表的语法格式如下 - 事件名(参数表)/动作 - 表达式其中,“事件名”可以是任何事件的名称。 - 事件 - 事件是在某个特定时刻发生的事情,它是对引起系统做动作或(和)从一个状态转换到另一个状态的外界事件的抽象。 - 状态变迁通常是由事件触发的,在这种情况下,应在表示状态转换的箭头线上标出触发转换的事件表达式。 - 事件表达式的语法如下 - `事件说明[守卫条件]/动作表达式` - 其中,事件说明的语法为:`事件名(参数表)` - 守卫条件是一个布尔表达式。如果同时使用事件说明和守卫条件,则当且仅当事件发生且布尔表达式为真时,状态转换才发生。如果只有守卫条件没有事件说明,则只要守卫条件为真,状态转换就发生。 - 动作表达式是一个过程表达式,当状态转换开始时执行该表达式。 ### 数据字典 - 数据字典是关于数据的信息的集合,也就是对数据流图中包含的所有元素的定义的集合。 - 数据字典可以把不同的需求文档和分析模型紧密结合在一-起,如果所有的开发人员在数据字典上取得一致意见, 那么就可以缓和集成性问题。为了避免冗余和不一致性,应该在项目中创建一个独立的数据字典,而并不是在每个需求出现的地方定义每一个数据项。 - 六类条目 - 数据项 - 数据结构 - 数据流 - 数据存储 - 加工逻辑 - 外部实体 ### 加工规格说明 - 过程设计语言 - 过程设计语言(Procedure Design Language,PDL),也称为伪代码,在某些情况下,在加工规格说明中会用到。但一般说来,最好将用PDL来描述加工规格说明的工作推迟到过程设计阶段进行比较好。 - 判定表 - 在某些数据处理中,某个数据处理(即加工)的执行可能需要依赖于多个逻辑条件的取值,此时可用判定表。判定表能够清晰地表示复杂的条件组合与应做的动作之间的对应关系。 - 判定树 - 判定树是判定表的变种,也能清晰地表示复杂的条件组合与应做的动作之间的对应关系。判定树也是用来表述加工规格说明的一种工具。判定树的优点在于,它的形式简单到不需要任何说明,一眼就可以看出其含义,因此易于掌握和使用 ## 结构化分析的图形工具 - 层次方框图 - 层次方框图由树形结构的一系列多层次的矩形组成,用来描述数据的层次结构。 - 方框之间的关系是组成关系,而不是调用关系。 - Warnier图 - 与层次方框图相似,也用树形结构来描绘数据结构,但提供了更详细的描绘手段 - 能指出某一类数据或某一数据元素重复出现的次数,并能指明某一特定数据在某一类数据中是否是有条件地出现。 - IPO图 - IPO图是输入-处理-输出(Input-Process-Output)图的简称。 - 能够方便地描绘输入数据、对数据的处理和输出数据之间的关系。

软件工程复习 第五章

# 第五章 结构化设计 - 目标 - 回答系统应该“怎么做”这个问题。 - 意义 - 软件设计在软件开发过程中处于核心地位,它是保证质量的关键步骤。 - 设计为我们提供了可以用于质量评估的软件表示、 - 设计是我们能够将用户需求准确地转化为软件产品或系统的唯一方法 - 软件设计是所有软件工程活动和随后的软件支持活动的基础。 - 软件设计是一个迭代的过程,通过设计过程,需求被变换为用于构建软件的“蓝图”。 - 良好设计演化的三个特征 - 设计必须实现所有包含在分析模型中的明确需求,而且必须满足用户期望的所有隐含需求。 - 对于程序员、测试人员和维护人员而言,设计必须是可读的、可理解的指南。 - 设计必须提供软件的全貌,从实现的角度说明数据域、功能域和行为域。 - 以上每一个特征实际上都是设计过程应该达到的目标。 - 原则 - 模块化 - 模块是数据说明、可执行语句等程序对象的集合,是构成程序的基本构件,可以被单独命名并通过名字来访问。在面向过程的设计中,过程、函数、子程序、宏都可以作为模块;在面向对象的设计中,对象是模块,对象中的方法也是模块。 - 模块化就是把系统或程序划分为独立命名并且可以独立访问的模块,每个模块完成一个特定的子功能。模块集成起来可以构成-一个整体,完成特定的功能,进而满足用户需求。 - 模块的公共属性: - 接口:指模块的输入、输出。 - 功能:指模块的逻辑功能。 - 状态:指模块的运行环境,即模块的调用和被调用关系 - 逻辑:描述内部如何实现要求的功能及所需的数据。 ### 内聚的分类 内聚的种类由紧到松(越紧越好)依次为: 1. **功能内聚**:指模块内的所有元素共同作用完成一个功能,缺一不可。 2. **顺序内聚**:指一个模块中的各个处理元素都密切相关于同一功能且必须顺序执行,前一功能元素的输出就是下一个功能元素的输入。 3. **通信内聚**:指模块内所有处理元素都在同一个数据结构上。 4. **过程内聚**:指一个模块完成多个任务,这些任务必须按指定的过程执行。 5. **瞬时内聚**:把需要同时执行的任务或动作组合在一起(如初始化模块)。 6. **逻辑内聚**:模块完成逻辑上相关的一组任务。 7. **偶然内聚**:指一个模块内的各处理元素之间没有任何联系或有松散的联系。 ### 耦合的分类 耦合的种类从高到低(越低越好)依次为: 1. **内容耦合**:一个模块直接使用另一个模块的内部数据,或通过非正常入口转入另一个模块内部时,这种耦合关系就是内容耦合。 2. **公共耦合**:指一组模块访问一个公共数据环境,如全局数据结构。 3. **外部耦合**:指一组模块访问一个公共变量,这里指基本数据类型而不是数据结构(或者说对象)。 4. **控制耦合**:指一个模块调用另一个模块时,传递的是控制变量,被调用模块通过该控制变量的值选择执行模块内某一功能。那么也就是说,被调用的模块应具有多个功能。 5. **标记耦合**:耦合模块之间以数据结构传递(比如在 java 程序中,传递的就是一个对象)。 6. **数据耦合**:耦合模块之间有调用关系,传递的是简单数据类型的值(比如在 java 程序中,传递的就是一个基本数据类型的值)。 7. **无直接耦合**:指两个模块之间没有直接的关系,它们分别从属于不同模块的控制与调用,它们之间不传递任何信息。

软件工程复习 第六章

# 面向对象方法与UML ## 面向对象的软件工程方法 - 基本观点 - 客观世界是由对象组成的,任何客观的事物或实体都是对象,复杂的对象可以由简单的对象组成。 - 具有相同数据和相同操作的对象可以归并为一个类,对象是对象类的一个实例。 - 类可以派生出子类,子类继承父类的全部特性(数据和操作) ,又可以有自己的新特性。子类父类形成类的层次结构。 - 对象之间通过消息传递相互联系。类具有封装性,其数据和操作等对外界是不可见的,外界只能通过消息请求进行某些操作,提供所需要的服务。 - 基本概念 - 面向对象:按人们认识客观世界的系统思维方式,采用基于对象的概念建立模型,模拟客观世界分析、设计、实现软件的办法。通过面向对象的理念使计算机软件系统能与现实世界中的系统一一对应。 - 对象:即指现实世界中各种各样的实体。它可以指具体的事物,也可以指抽象的事物。在面向对象概念中我们把对象的内部状态称为属性,把运动规律称为方法或事件。 - 类:类是具有相似内部状态和运动规律的实体的集合。对于一个具体的类,它有许多具体的个体,我们就管这些个体叫作“对象”。类的内部状态是指类集合中对象的共同状态;类的运动规律是指类集合中对象的共同运动规律。 - 消息:消息是指对象间相互联系和相互作用的方式。一个消息主要由5部分组成:发送消息的对象、接收消息的对象、消息传递办法、消息内容、反馈。 - 包:现实世界中不同对象间的相互联系和相互作用构成了各种不同的系统,不同系统间的相互联系和相互作用构成了更庞大的系统,进而构成了整个世界。在面向对象概念中把这些系统称为包 - 接口:在系统间相互作用时为了蕴藏系统内部的具体实现,系统通过设立接口界面类或对象来与其他系统进行交互;让其他系统只看到这个接口界面类或对象,这个类在面向对象中称为接口类。 - 类的特性 - 抽象:类的定义中明确指出类是一组具有内部状态和运动规律对象的抽象,抽象是一种从一般的观点看待事物的方法,它要求我们集中于事物的本质特征,而非具体细节或具体实现。 - ②继承:继承是类不同抽象级别之间的关系。在计算机软件开发中采用继承性,提供了类的规范的等级结构;通过类的继承关系,使公共的特性能够共享,提高了软件的重用性 - ③封装:对象间的相互联系和相互作用过程主要通过消息机制得以实现。对象之间并不需要过多地了解对方内部的具体状态或运动规律。面向对象的类是封装良好的模块,类定义将其说明与实现显式地分开,其内部实现按其具体定义的作用域提供保护。类是封装的最基本单位。封装防止了程序相互依赖而带来的变动影响。在类中定义的接收对方消息的方法称为类的接口。 - 多态:多态是指同名的方法可在不同的类中具有不同的运动规律。在父类演绎为子类时,类的运动规律也同样可以演绎,演绎使子类的同名运动规律或运动形式更具体,甚至子类可以有不同于父类的运动规律或运动形式。不同的子类可以演绎出不同的运动规律 - 重载:重载指类的同名方法在给其传递不同的参数时可以有不同的运动规律。在对象间相互作用时,即使接收消息对象采用相同的接收办法,但消息内容的详细程度不同,接收消息对象内部的运动规律也可能不同。 - 特征 - 把数据和操作封装在一起,形成对象。对象是构成软件系统的基本构件 - 把特征相似的对象抽象为类 - 类之间可以存在继承或被继承的关系,形成软件系统的层次结构 - 对象之间通过发送消息进行通信 - 将对象的私有信息封装起来。外界不能直接访问对象的内部信息,而必须是发送相应的消息后,通过有限的接口来访问。 - 优势 - 符合人类的思维习惯 - 稳定性好 - 可复用性好 - 可维护性好 - 实施步骤 - 面向对象分析:从问题陈述入手,分析和构造所关心的现实世界问题域的模型,并用相应的符号系统表示。 - 确定问题域,包括定义论域、选择论域、根据需要细化和增加论域 - 区分类和对象,包括定义对象、定义类、命名 - 区分整体对象以及组成部分,确定类的关系以及结构 - 定义属性,包括确定属性、安排属性 - 定义服务,包括确定对象状态、确定所需服务、确定消息联结 - 确定附加的系统约束 - 面向对象设计 - 应用面向对象分析,改善和完善用其他方法得到的系统分析结果 - 设计交互过程和用户接口 - 设计任务管理,根据前一步骤确定是否需要多重任务,确定并发性,确定以何种方式驱动任务,设计子系统以及任务之间的协调与通信方式,确定优先级 - 设计全局资源,确定边界条件,确定任务或子系统的软、硬件分配 - 面向对象实现:使用面向对象语言实现面向对象的设计相对比较容易。用非面向对象语言实现面向对象的设计时,特别需要注意和保留程序的面向对象结构 - 面向对象测试:对面向对象实现的程序进行测试,包括模型测试、类测试、交互测试、系统(子系统)测试、验收测试等 ## 统一建模语言(Unify Modeling Language) 统一建模语言(Unified Modeling Language,UML)是一种通用的可视化建模语言,可以用来描述、可视化、构造和文档化软件密集型系统的各种工件。它是由信息系统和面向对象领域的3位著名的方法学家GradyBooch、James Rumbaugh和Ivar Jacobson提出的。它记录了与被构建系统有关的决策和理解,可用于对系统的理解、设计、浏览、配置、维护以及控制系统的信息。这种建模语言已经得到了广泛的支持和应用,并且已被ISO组织发布为国际标准。 - UML是一种标准的图形化建模语言,它是面向对象分析与设计的一种标准表示 - 它不是一种可视化的程序设计语言,而是一种可视化的建模语言 - 它不是工具或知识库的规格说明,而是一种建模语言规格说明,是一种表示的标准 - 它不是过程,也不是方法,但允许任何一种过程和方法使用它。 - 特点 - 统一标准 - 面向对象 - 可视化,表达能力强大 - 独立于过程 - 容易掌握使用 - 与编程语言的关系 - 应用范围 - UML以面向对象的方式来描述系统。UML最广泛的应用是对软件系统进行建模,但它同样适用于许多非软件系统领域的系统。从理论上来说,任何具有静态结构和动态行为的系统都可以使用UML建模。当UML应用于大多数软件系统的开发过程时,它从需求分析阶段到系统完成后的测试阶段都能起到重要作用。 - 组成 - UIL符号为开发者或开发工具使用这些图形符号和文本语法为系统建模提供了标准 - 这些图形符号和文字所表达的是应用级的模型,在语义上它是U0元模型的实例 - UIL模型由事物、关系和图组成 - [UML概述_w3cschool](https://www.w3cschool.cn/uml_tutorial/uml_tutorial-c1gf28pd.html) ## 静态建模机制 任何建模语言都以静态建模机制为基础,UML也不例外。<mark>UML的静态建模机制包括用例图、类图、对象图、包图等。</mark> ### 用例图 用例图是从用户的角度描述系统的功能,由用例(Use Case)、参与者(Actor)以及它们的关系连线组成。 用例的特征用例是从用户角度描述系统的行为,它将系统的一个功能描述成一系列的事件,这些事件最终对操作者产生有价值的观测结果。参与者(也称为操作者或执行者)是与系统交互的外部实体,可能是使用者,也可能是与系统交互的外部系统、基础设备等。用例是一个类,它代表一类功能而不是使用该功能的某一具体实例。 - 包含关系 - 如果系统用例较多,不同的用例之间存在共同行为,可以将这些共同行为提取出来,单独组成一个用例。当其他用例使用这个用例时,它们就构成了包含关系。 - 扩展关系 - 在用例的执行过程中,可能出现一些异常行为,也可能会在不同的分支行为中选择执行,这时可将异常行为与可选分支抽象成一个单独的扩展用例,这样扩展用例与主用例之间就构成了扩展关系。一个用例常常有多个扩展用例。 - 泛化关系 - 用例之间的泛化关系描述用例的一般与特殊关系,不同的子用例代表了父用例的不同实现。 ### 类图和对象图 类图使用类和对象描述系统的结构,展示了系统中类的静态结构,即类与类之间的相互关系。类之间有多种联系方式,如关联(相互连接)、依赖(一个类依赖于或使用另一个类)、泛化(一个类是另一个类的特殊情况)。一个系统有多幅类图,一个类也可以出现在几幅类图中。 对象图是类图的实例,它展示了系统在某一时刻的快照。对象图使用与类图相同的符号,只是在对象名下面加上下画线。 ### 包图 包是一种对元素进行分组的机制。如果系统非常复杂,常常包含大量的模型,为了便于理解以及将模型独立出来用于复用,对这些元素进行分组组织,从而作为一个个集合进行整体命名和处理。 ## 动态建模机制 系统中的对象在执行期间的不同时间点如何通信以及通信的结果如何,就是系统的动态行为,也就是说,对象通过通信相互协作的方式以及系统中的对象在系统生命期中改变状态的方式,是系统的动态行为。<mark>UML的动态建模机制包括顺序图、协作图、状态图和活动图。</mark> ### 顺序图 顺序图描述了一组对象的交互方式,它表示完成某项行为的对象和这些对象之间传递消息的时间顺序。顺序图由对象(参与者的实例也是对象)、生命线、控制焦点、消息等组成。生命线是一条垂直的虚线,表示对象的存在时间;控制焦点是一个细长的矩形,表示对象执行一个操作经历的时间段;消息是作用于控制焦点上的一条水平带箭头的实现,表示消息的传递。 - 绘制顺序图的步骤如下 - 列出启动该用例的参与者 - 列出启动用例时参与者使用的边界对象 - 列出管理该用例的控制对象 - 根据用例描述的所有流程,按时间顺序列出分析对象之间进行消息传递的序列 - 绘制顺序图需要注意以下问题。 - 如果用例的事件流包含基本流和若干备选流,则应当对基本流和备选流分别绘制顺序图 - 如果备选流比较简单,可以合并到基本流中 - 如果事件流比较复杂,可以在时间方向上将其分成多个顺序图 - 实体对象一般不会访问边界对象和控制对象。 ### 协作图 协作图显示多个对象及它们之间的关系,对象间的箭头显示消息的流向。消息上也可以附带标签,表示消息的其他信息,如发送顺序、显示条件、迭代和返回值等。开发人员熟识消息标签的语法之后,就可以读懂对象之间的通信,并跟踪标准执行流程和消息交换顺序。但是,如果不知道消息的发送顺序,就不能使用协作图来表示对象关系。图6-21为某个系统的“登录”交互过程的简要协作图。 ### 状态图 状态图由状态机扩展而来,用来描述对象对外部对象响应的历史状态序列,即描述对象所有可能的状态,以及哪些事件将导致状态改变。状态图包括对象在各个不同状态间的跳转以及这些跳转的外部触发事件,即从状态到状态的控制流。状态图侧重于描述某个对象的动态行为,是对象的生命周期模型。并不是所有的类都需要画状态图,只有明确意义的状态、在不同状态下行为有所不同的类,才需要画状态图。状态图在4.3.3节中已经介绍,这里不再赘述。 ### 活动图 活动图是展示整个计算步骤的控制流(及其操作数)的节点和流的图。执行的步骤可以是并发的或顺序的。 ## 描述物理架构的机制 系统架构分为逻辑架构和物理架构两大类。逻辑架构完整地描述系统的功能,把功能分配到系统的各个部分,详细说明它们是如何工作的。物理架构详细描述系统的软件和硬件,描述软件和硬件的分解。在UML中,用于描述逻辑架构的图有:用例图、类图、对象图、状态图、活动图、协作图和顺序图;<mark>用于描述物理架构的图有:构件图、部署图。</mark> ### 构件图 构件图根据系统的代码构件显示系统代码的物理结构。 ### 部署图

软件工程复习 第八章

# 第八章 软件体系结构 体系结构是研究系统各部分组成及相互关系的技术学科。它包括以下几个部分: - 软件的组成元素(组件) - 这些(组件)元素的外部可见特性 - 这些元素(组件)之间的相互关系。 ## 体系结构模式 ### 分层模式 将软件系统按照抽象级别逐层递增或递减的顺序划分为若干层次,每层由一-些抽象级别相同的构件组成。 每层的构件仅为其上的层次提供服务,并且它们仅使用其下层提供的服务。 一般而言, 顶层直接面向用户提供软件系统的交互界面,底层则负责提供基础性、公共性的技术服务,比较接近于硬件计算环境、操作系统或数据库管理系统。 - 层次之间的连接有两种形态: - 高层构件向低层构件发出服务请求,低层构件在计算完成后向请求者发送服务应答。 - 低层构件主动探测或被动获知计算环境的变化事件后通知高层构件。 - 优势 - 松耦合。通过软件层次的划分和层间接口的规整,有效降低耦合度,系统各构件之间依赖关系的局部化。 - 可替换性。一个层次可被替换。 - 可复用性。具有良好定义的抽象级别和对外服务接口。 - 劣势 - 高层功能需要逐层调用下层服务,返回值、报错信息又需逐层上传,耗时 - 若低层服务还完成了最初的服务请求者不需要的冗余功能,性能损耗更严重。 ### 管道过滤器模式 将软件系统的功能实现为一-系列的处理步骤,每个步骤封装在一个过滤器构件中,相邻过滤器之间由管道连接。一个过滤器的输入数据借助管道流向后续过滤器,作为其输入数据。 输入由数据源提供,它通过管道与过滤器相连。最终输出由源自某个过滤器的管道流向数据汇。数据源和汇通常为数据库、文件、其他软件系统和物理设备。 采用管道过滤器模式,可以通过升级、更换部分过滤器构件以及处理步骤的充足来实现软件系统的扩展和进化。此模式适合于采用批处理方式的软件系统,不适合与交互式、事件驱动的系统 ### 黑板模式 - 将软件系统划分为黑板、知识源和控制器3类构件 - 黑板负责保存问题求解过程中的状态数据,并提供这些数据的读写服务 - 知识源负责根据黑板中存储的问题求解状态评价其自身的可应用性,进行部分问题求解工作,并将此工作的结果数据写入黑板 - 控制器负责监视黑板中不断更新的状态数据,安排知识源的活动 - 适合于没有确定的求解方法的复杂问题 - 优势: - 知识源和控制器可灵活更换、升级。 - 知识源之间没有交互,知识源与控制器和黑板之间均通过接口交互,知识源可复用性好。 - 知识源的求解动作是探索性的,允许失败和试错。 - 劣势 - 问题求解性能较低,有时甚至无法预测求解时间。 - 不能确保得到最优解。 - 问题求解路径不确定,造成软件测试较为困难。 - 知识源和控制器两种构件的开发相当困难。 ### 分布式体系结构 - 优势 - 资源共享。分布式系统允许硬件、软件等资源共享使用 - 经济性。 - 性能与可扩展性。 - 固有分布性。 - 健壮性。 - 多处理机体系结构 - 分布式系统的一个最简单的模型是多处理器系统,系统由许多进程组成,这些进程可以在不同的处理器上并行运行,可以极大地提高系统的性能。 - 由于大型实时系统对响应时间要求较高,这种模型在大型实时系统中比较常见。大型实时系统需要实时采集信息,并利用采集到的信息进行决策,然后发送信号给执行机构。虽然,信息采集、决策制定和执行控制这些进程可以在同一台处理器上统一 调度执行,但使用多处理器能够提高系统性能。 - 客户/服务器模式 - 客户机/服务器( client/server , C/S )体系结构是基于资源不对等且为实现共享而提出来的,由服务器、客户机和网络三部分组成。 - 在C/S体系结构中,客户机可以通过远程调用来获取服务器提供的服务,因此,客户机必须知道可用的服务器的名字及它们所提供的服务,而服务器不需要知道客户机的身份,也不需要知道有多少台服务器在运行。 - 瘦客户机模型 - 在瘦客户机模型中,数据管理部分和应用逻辑都在服务器上执行,客户机只负责表示部分。 - 瘦客户机模型的主要缺点: - 它将繁重的处理负荷都放在了服务器和网络上,服务器负责所有的计算,这将增加客户机和服务器之间的网络流量。 - 目前个人计算机所具有的处理能力在瘦客户机模型中用不上。 - 胖客户机模型 - 在这种模型中,服务器只负责对数据的管理。客户机_上的软件实现应用逻辑和与系统用户的交互。 - 胖客户机模型能够利用客户机的处理能力,比瘦客户机模型在分布处理上更有效。 - 缺点: - 开发成本较高 - 用户界面风格不一,使用繁杂,不利于推广使用 - 软件移植困难 - 软件维护和升级困难 - 三层C/S体系 - 为了解决以上问题,三层C/S体系结构应运而生。三层C/S体系结构中增加了应用服务器。可以将整个应用逻辑驻留在应用服务器上,而只有表示层存在于客户机上。 - B/S体系 - 浏览器/服务器( browser/server , B/S )风格是三层体系结构的一种实现方式,其具体结构为浏览器/Web服务器/数据库服务器。B/S体系结构如下图所示。 - B/S体系结构主要是利用不断成熟的WWW浏览器技术,结合浏览器的多种脚本语言,用通用浏览器就实现了原来需要复杂的专用软件才能实现的强大功能,并节约了开发成本。从某种程度上来说, B/S结构是一种全新的软件体系结构。 - B/S体系结构具有以下优点: - 基于B/S体系结构的软件,系统安装、修改和维护全在服务器端解决。 - B/S体系结构还提供了异种机、异种网、异种应用服务的联机、联网和统一服务的最现实的开放性基础。 -

软考复习之信号量

# 信号量 信号量(Semaphore),有时被称为信号灯,是在[多线程](https://baike.baidu.com/item/%E5%A4%9A%E7%BA%BF%E7%A8%8B/1190404?fromModule=lemma_inlink)环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被[并发](https://baike.baidu.com/item/%E5%B9%B6%E5%8F%91/11024806?fromModule=lemma_inlink)调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。 ## 信号量值的含义 - n>0:当前有可用资源,可用资源数量为n - n=0:资源都被占用,可用资源数量为0 - n<0:资源都被占用,并且还有n个进程正在排队 ## PV操作 PV操作是一种实现进程互斥与同步的有效方法。 PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思。 **注意:** 1. P(S)和V(S)都是在同一个信号量S上操作 2. 约定P(S)和V(S)必须是两个不可被中断的过程 3. PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用 **P操作的主要动作是:** ①S减1; ②若S减1后仍大于或等于0,则进程继续执行; ③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。 **V操作的主要动作是:** ①S加1; ②若相加后结果大于0,则进程继续执行; ③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。 ~~如果再考到其他的点我再加吧~~

软考复习之编译原理

# 软考复习之编译原理 ## 语言处理程序 *语言处理程序是将高级语言或汇编语言编写的程序**翻译**成某种机器语言* ### 汇编语言 *汇编语言是为特定计算机设计的面向机器的符号化的设计语言* 汇编语言源程序是指用汇编语言编写的程序 #### 语句 ##### 指令语句 *指令语句又叫机器指令语句* 将其汇编后能产生相应的机器代码 基本指令有`ADD`.`SUB`,`AND`等 ##### 伪指令语句 *伪指令语句指示汇编程序在汇编源程序时完成的工作* 汇编后不产生相应的机器码 ##### 宏指令语句 *宏指令语句就是宏的引用* 宏的定义必须按照相应的规定进行,每个宏都有相应的宏名 #### 汇编程序 *汇编程序的功能是将用汇编语言写的源程序翻译成机器指令程序* 汇编程序一般需要两次扫描源程序才能完成翻译过程 **第一次扫描** - 创建记录汇编时遇到的符号的值的符号表ST - 一个固定的表MOT1记录每条机器指令的记忆码和指令长度 - 设立一个位置计数器或单元地址计数器LC来计算各汇编语句标点的地址,其初值一般是0 第二次扫描 - 使用符号指令表MOT2 - 设立一个伪指令表POT2 第二次扫描中,可执行的汇编语句应被翻译成对应的二进制代码机器指令 ### 编译程序 *编译程序的功能是把某高级语言书写的源程序翻译成与之等价的目标程序* #### 词法分析 *对源程序从前到后、从左到右的逐个字符扫描,从中识别出一个个“单词”符号* 词法规则用3型号文法或正规表达式描述,它产生的集合是语言基本字符集字母表上的字符串的一个子集叫做正规集 ##### 正规表达式与正规集 ![](../../../../assets/default.png) ![](../../../../assets/default.png) ##### 有限状态机 **确定有限状态机(DFA)** ![](../../../../assets/default.png) **不确定的有限状态机(NFA)** ![](../../../../assets/default.png) ##### NFA到DFA的转换 [NFA到DFA的转化_暗夜绿的博客](https://blog.csdn.net/weixin_48627356/article/details/121321357) ~~感觉不考~~ ##### DFA的最小化 [【编译原理】最小化 DFA_零碎@流年絮语的博客](https://blog.csdn.net/qq_44824148/article/details/115477583) ~~感觉也必考~~ ##### 正规式与有限自动机之间的转换 ~~感觉还是不考~~ ##### 词法分析器构造 1. 用正规表达式描述语言中的单词构成规则 2. 为每个正规式构造一个NFA,它识别正规式所表示的正规集 3. 将构造出的NFA转换成等价的DFA 4. 对DFA进行最小化处理 5. 从DFA构造词法分析器 #### 语法分析 *根据语言的语法规则将单词符号序列分解成各类语法单元,如果没有语法错误,语法分析后就能正确的构造出语法树,否则指出语法错误。* 语法分析的任务是根据语言的规则分析单词串中是否构成短语和句子 - ##### 文法和语言分析 *描述语言语法结构的规则成为<b>文法</b>* [【编译原理】文法的分类_Tuuua_的博客](https://blog.csdn.net/qq_42933533/article/details/109681369) [语法分析_Shanhj的博客](https://blog.csdn.net/Shanhj/article/details/124771451) #### 语义分析 *分析各语法结构的含义,检查源程序是否包含静态语义错误,并收集类型信息。* 只有语法检查与语义检查都正确的源程序才能被翻译成正确的目标代码 #### 中间代码生成 *中间代码生成阶段的工作是根据语义分析的输出生成中间代码* - 中间代码是一种简单且含义明确的记号系统 - 可以有多种形式 - 特征是与具体机器无关 - 最常用的是三地址码,实现方式是四元式 **常用的中间代码形式:** - **后缀式**:[逆波兰式_百度百科 (baidu.com)](https://baike.baidu.com/item/%E9%80%86%E6%B3%A2%E5%85%B0%E5%BC%8F/128437?fromtitle=%E5%90%8E%E7%BC%80%E8%A1%A8%E8%BE%BE%E5%BC%8F&fromid=6160580&fr=aladdin) - 树形表示 - 三元表示 - 四元表示 **常用语法结构翻译** 布尔表达式和控制语句:[【编译原理系列】布尔表达式及控制语句翻译_槑!的博客](https://blog.csdn.net/weixin_43934607/article/details/113068312) 算数表达式和赋值语句:[【编译原理系列】算术表达式与数组元素翻译_槑!的博客](https://blog.csdn.net/weixin_43934607/article/details/113077357?ops_request_misc=&request_id=&biz_id=102&utm_term=%E7%AE%97%E6%95%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%BF%BB%E8%AF%91&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-113077357.142^v51^new_blog_pos_by_title,201^v3^add_ask&spm=1018.2226.3001.4187) **动态存储分配和过程调用的翻译** ~~没见考过~~ #### 代码优化 *代码优化生成高效的目标代码* 可以在中间代码生成阶段进行也可以在目标代码生成阶段进行 一般建立在对程序的控制流和数据流分析的基础上 #### 目标代码生成 *把中间代码变换成特定机器上的绝对指令代码、可重定向的指令代码或汇编指令代码* 和机器密切相关 是编译器工作的最后一个阶段 **代码生成所需要考虑的主要问题** - 中间代码形式 - 目标代码形式 - 寄存器的分配 - 计算时序的选择 #### 符号表管理 *记录源程序中各个符号的必要信息,以辅助语义的正确性检查和代码生成* #### 出错处理 *<b>动态错误</b>又成为语义错误,发生在程序运行时* *<b>静态错误</b>时指编译状态发现的程序错误* 编译时发现错误后,应该采用适当的策略修复他们,使得分析过程能够继续下去,以便能够在一次编译过程中尽可能多的找出程序中的错误 ### 解释程序 *解释程序与编译程序在词法、语法、语义分析方面与编译程序的工作原理基本相同,但是在运行用户程序时,它直接执行源程序或源程序的中间表达式* #### 基本结构 ##### 分析部分 - 词法分析 - 语法分析 - 语义分析 ##### 解释部分 ### 编译与解释的比较 - 效率:编译方式可能取得更高的效率 - 灵活性:解释方式更灵活 - 可移植性:解释方法更好

软考复习之设计模式

# 设计模式 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 ## 设计模式的六大原则 **1、开闭原则(Open Close Principle)** 开闭原则的意思是:**对扩展开放,对修改关闭**。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 **2、里氏代换原则(Liskov Substitution Principle)** 里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 **3、依赖倒转原则(Dependence Inversion Principle)** 这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。 **4、接口隔离原则(Interface Segregation Principle)** 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。 **5、迪米特法则,又称最少知道原则(Demeter Principle)** 最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。 **6、合成复用原则(Composite Reuse Principle)** 合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。 ## 分类 共有23种设计模式分为3大类 - 创建型模式 - 工厂模式(Factory Pattern) - 抽象工厂模式(Abstract Factory Pattern) - 单例模式(Singleton Pattern) - 建造者模式(Builder Pattern) - 原型模式(Prototype Pattern) - 结构型模式 - 适配器模式(Adapter Pattern) - 桥接模式(Bridge Pattern) - 过滤器模式(Filter、Criteria Pattern) - 组合模式(Composite Pattern) - 装饰器模式(Decorator Pattern) - 外观模式(Facade Pattern) - 享元模式(Flyweight Pattern) - 代理模式(Proxy Pattern) - 行为型模式 - 责任链模式(Chain of Responsibility Pattern) - 命令模式(Command Pattern) - 解释器模式(Interpreter Pattern) - 迭代器模式(Iterator Pattern) - 中介者模式(Mediator Pattern) - 备忘录模式(Memento Pattern) - 观察者模式(Observer Pattern) - 状态模式(State Pattern) - 空对象模式(Null Object Pattern) - 策略模式(Strategy Pattern) - 模板模式(Template Pattern) - 访问者模式(Visitor Pattern) ![](cp/img1.jpg) ## 详细信息 [设计模式 | 菜鸟教程 (runoob.com)](https://www.runoob.com/design-pattern/design-pattern-tutorial.html)

软考复习之机械硬盘

# 机械硬盘管理 ## 硬盘结构 硬盘内部是由许许多多的圆形盘片、机械手臂、 磁头与主轴马达所组成的 实际的数据都是写在具有磁性物质的盘片上头,而读写主要是通过在机械手臂上的磁头(head)来达成。 实际运行时, 主轴马达让盘片转动,然后机械手臂可伸展让磁头在盘片上头进行读写的动作。 另外,由于单一盘片的容量有限,因此**有的硬盘内部会有两个以上的盘片** ![](disk/Disktructure.jpg) 磁盘上的数据都存放于磁道上。<mark>磁道就是磁盘上的一组同心圆</mark>,**其宽度与磁头的宽度相同**。为了避免减小干扰,**磁道与磁道之间要保持一定的间隔**(inter-track gap),<mark>沿磁盘半径方向,单位长度内磁道的数目称之为道密度</mark>(道/英寸,TPI),最外层为0道。 <mark>沿磁道方向,单位长度内存储二进制信息的个数叫位密度</mark>。为了简化电路设计,**每个磁道存储的位数都是相同的,所以其位密度也随着从外向内而增加**。磁盘的数据传输是以块为单位的,所以<mark>磁盘上的数据也以块的形式进行存放。这些块就称为扇区</mark>(sector),**每个磁道通常包括10~100个扇区。同样为了避免干扰,扇区之间也相互留有空隙**(inter–sector gap)。柱面是若干个磁盘组成的磁盘组,<mark>所有盘面上相同位置的磁道组称为一个柱面</mark>(每个柱面有n个磁道);若每个磁盘有m个磁道,则该磁盘组共有m个柱面。 通常数据的读写会由外圈开始往内写 ## 磁盘容量 **磁盘的非格式化容量为<mark>Cn=w×3.14×d×m×n</mark>,其中w为位密度,d为最内圈直径,m为记录面数,n为每面磁道数。** 磁盘格式化后能够存储有用信息的总量。**<mark>存储容量=n×t×s×b</mark>,其中:n为保存数据的总盘面 数;t为每面磁道数;s为每道的扇区数;b为每个扇区存储的字节数。** ## 磁盘读取时间 磁盘的存取时间包括寻道时间和等待时间。<mark>寻道时间(查找时间,Seek Time)为磁头移动到目标磁道所需的时间</mark>(movabe–head disk),对于固定磁头磁盘而言,无须移动磁头,只需选择目标磁道对应的磁头即可。<mark>等待时间为等待读写的扇区旋转到磁头下方所用的时间</mark>。一般选用磁道旋转一周所用时间的一半作为平均等待时间。寻道时间由磁盘机的性能决定 <mark>磁盘的数据传输速率是指磁头找到地址后,单位时间写入或读出的字节数</mark>。<mark>R=TB/T</mark>,其中:TB为一个磁道上记录的字节数,T为磁盘每转一圈所需的时间,R为数据传输速率。

软考复习之内存管理

## 内存管理 ## 存储管理的概念 存储管理主要是指对内存储器的管理,负责对内存的分配和回收、内存的保护和内存的扩充。存储管理的目的是尽量提高内存的使用效率。 ## 单一连续区管理 在单道程序系统中,内存区域的用户空间全部为一个作业或进程占用。单一连续分配方法主要用于早期单道批处理系统。单一连续分配方法主要采用静态分配方法,为降低成本和减少复杂度,通常不对内存进行保护,因而会引起冲突使系统瘫痪。 ## 分区存储管理 分区存储管理包括固定分区、可变分区,其基本思想是把内存划分成若干个连续区域,每个分区装入一个作业运行。要求作业一次性装入内存,且分区内部地址必须连续。 ### 固定分区存储管理 固定分区分配方法是把内存空间固定地划分为若干个大小不等的区域,划分的原则由系统决 定。系统使用分区表描述分区情况。分区一旦划分结束,在整个执行过程中每个分区的长度和内存的总分区个数保持不变。 ### 可变分区存储管理 可变分区分配方法是把内存空间按用户要求动态地划分成若干个分区。随着进程的执行,剩余的自由区域会变得更小,这时需要合并自由区和存储拼接技术。合并自由区是将相邻自由存储区合并为单一自由区的方法;存储拼接技术也称碎片收集,包括移动存储器的所有被占用区域到主存的某一端。可变分区克服了固定分区分配方法中的小作业占据大分区后产生碎片的浪费问题。 ### 存储分配算法 常使用的4种存储分配算法介绍如下。 **首次适应算法:** 把内存中的可用分区单独组成可用分区表或可用分区自由链,按起始地址递增的次序排列。每次按递增次序向后找,<mark>一旦找到大于或等于所要求内存长度的分区</mark>,则结束探索,从找到的分区中找出所要求的内存长度分配给用户,并把剩余的部分进行合并。 **循环适应算法:** 上述首次适应法经常利用的是低地址空间,后面经常是较大的空白区,为使内存所有线性地址空间尽可能轮流使用到,<mark>每重新分配一次,都在当前之后寻找。</mark> **最佳适应算法:** 最佳适应算法是<mark>将输入作业放入主存中与它所需大小最接近的空白区中</mark>,使剩下的未用空间最小,该法要求空白区大小按从小到大次序组成空白区可用表或自由链。在进行分配时总是从最小的一个开始查询,因而找到的一个能满足要求的空白区便是最佳的一个。 **最差适应算法:** 分配时把一个作业程序放入主存中最不适合它的空白区,即<mark>最大的空白区(空闲区)内</mark>。 ### 交换与覆盖技术 覆盖与交换技术是在多道程序环境下用来扩充内存的两种方法。覆盖技术主要用在早期的操作系统中,而交换技术则在现代操作系统中得到了进一步发展。 覆盖技术是一种解决小内存运行大作业的方法。-个作业中若干程序段和数据段可以不同时使 用,这样它们就可以共享内存的某个区域,再根据需要分别调入该区域,这个区域就称为覆盖区。将程序执行时并不要求同时装入主存的覆盖组成一组,并称其为覆盖段,这个覆盖段分配到同一个覆盖区。交换技术可以将暂不需要的作业移到外存,让出内存空间以调入其他作业,交换到外存的作业也可以被再次调入。交换技术与覆盖技术相比不要求给出程序段之间的覆盖结构。交换主要是在作业之间进行的,而覆盖则主要是在同一个作业内进行的。 ## 页式存储管理 分页的基本思想是把程序的逻辑空间和内存的物理空间按照同样的大小划分成若干页面,以页面为单位进行分配。在页式存储管理中,系统中虚地址是一个有序对(页号,位移)。系统为每一个进程建立一个页表,其内容包括进程的逻辑页号与物理页号的对应关系、状态等。 ## 段式存储管理 段式存储管理与页式存储管理相似。分段的基本思想是把用户作业按逻辑意义上有完整意义的段来划分,以段为单位作为内、外存交换的空间尺度。一个作业是由若干个具有逻辑意义的段(如主程序、子程序、数据段等)组成的。在分段系统中,容许程序(作业)占据内存中许多分离的分区。每个分区存储一个程序分段。这样,每个作业需要几对界限地址寄存器,判定访问地址是否越界也困难了。在分段存储系统中常常利用存储保护键实现存储保护。分段系统中虚地址是一个有序对(段号,位移)。系统为每个作业建立一个段表,其内容包括段号、段长、内存起始地址和状态等。状态指出这个段是否已调入内存,即内存起始地址指出这个段,状态指出这个段的访问权限。 ## 段页式存储管理 段页式管理是段式和页式两种管理方法结合的产物,综合了段式组织与页式组织的特点,根据程序模块分段,段内再分页,内存被划分成定长的页。段页式系统中虚地址形式是(段号、页号、位移)。系统为每个进程建立一个段,为每个段建立一个页表。段页式管理采用段式分配、页式使用的方法,便于动态连接和存储的动态分配。这种存储管理能提高内存空间的利用率。段页式虚拟存储管理结合了段式和页式的优点,但增加了设置表格(段表、页表)和查表等开销,段页式虚拟存储器一般只在大型计算机系统中使用。 ## 页面调度 如果选择的页面被频繁地装入和调出,这种现象称为"抖动",应减少和避免抖动现象。常用的页面调度算法有以下几种。 **最优(OPT)算法**。<mark>选择不再使用或最远的将来才被使用的页</mark>,难以实现,常用于淘汰算法的 比较。 **随机(RAND)算法**。<mark>随机地选择被淘汰的页</mark>,开销小,但是可以选中立即就要访问的页。 先进先出(First in First out,FIFO)算法,又称轮转法(RR)。选择在内存驻留时间最长的 页,似乎合理,但可能淘汰掉频繁使用的页。另外,使用FIFO算法时,在未给予进程分配足够的页面数时,有时会出现给予进程的页面数增多,缺页次数反而增加的异常现象。FIFO算法简单,可采用队列实现。 **最近最少使用(Least Recently Used缩写为LRU)算法**。<mark>选择离当前时间最近的一段时间内使用得最少的页</mark>。这个算法的主要出发点是,如果某个页被访问了,则它可能马上就要被访问;反之,如果某个页长时间未被访问,则它在最近一段时间也不会被访问。 另外,还有最不经常使用的页面先淘汰(LFU,least frequent used)、最近没有使用的页面先淘汰(NUR)、最优淘汰算法(OPT,optimal replacement algorithm)等。

软考复习之其他知识点

# 其他 ## 数据结构 ### 强连通分量 有向非强连通图的<mark>极大</mark>强连通子图,称为强连通分量 ### 霍夫曼编码 [霍夫曼(Huffman)压缩(文件压缩机制)](https://blog.csdn.net/Charlesix59/article/details/119975011) ## 计算机组成原理 - 立即寻址:指令的地址字段指出的不是操作数的地址,而是操作数本身 - 直接寻址:指令中的形式地址部分即为有效地址 - 间接寻址:指令中的形式地址不是操作数的地址,而是 “操作数地址的地址” - 隐含寻址:指令中不直接给出操作数地址,操作数地址通常隐含在操作码或某个(约定)寄存器中 - 寄存器寻址:指令中的形式地址直接指出寄存器的编号,操作数存储于寄存器中 - 寄存器间接寻址:指令中的形式地址为寄存器的编号,寄存器的内容是操作数的有效地址 - 基址寻址:指令中的形式地址与基址寄存器内容之和为有效地址。 - 变址寻址:指令中的形式地址与变址寄存器内容之和为有效地址。 - 相对寻址:有效地址为程序计数器*PC*的值与形式地址之和。 - 堆栈寻址: - [寻址方式_百度百科 (baidu.com)](https://baike.baidu.com/item/%E5%AF%BB%E5%9D%80%E6%96%B9%E5%BC%8F/3210621) ## 软件工程 ### McCabe算法 `V(G)=m−n+2p` 其中,V(G) 是有向图 G 中的环路数,m 是图 G 中弧的个数,n 是图 G 中的结点数,p 是图 G 中的强连通分量个数 ### SCI EMM - L1:CMMI一级,完成级。在完成级水平上,企业对项目的 目标与要做的努力很清晰。项目的目标得以实现。因此,任务是完成了。 但是由于任务的完成带有很大的偶然性,企业无法保证在实施同类项目的时候仍然能够完成任务。企业在一级上的项目实施对实施人员有很大的依赖性。 - L2:CMMI二级,管理级。在管理级水平上,企业在项目实施上能够遵守既定的计划与流程,有资源准备,权责到人,对相关的项目实施人员有相应的培训,对 整个流程有监测与控制,并与上级单位对项目与流程进行审查。企业在二级水平上体现了对项目的一系列的管理程序。这一系列的管理手段排除了企业在一级时完成 任务的随机性,保证了企业的所有项目实施都会得到成功。 - L3:CMMI三级,定义级。在定义级水平上,企业不仅仅能够对项目的实施有一整套的管理措施,并保障项目的完成;而且,企业能够根据自身的特殊情况以及 自己的标准流程,将这套管理体系与流程予以制度化。这样,企业不仅能够在同类的项目上得到成功的实施,在不同类的项目上一样能够得到成功的实施。科学的管 理成为企业的一种文化,企业的组织财富。 - L4:CMMI四级,量化管理级。在量化管理级水平上,企业的项目管理不仅仅形成了一种制度, 而且要实现数字化的管理。对管理流程要做到量化与数字化。通过量化技术来实现流程的稳定性,实现管理的精度,降低项目实施再质量上的波动。 - L5:CMMI五级,优化级。在优化级水品上, 企业的项目管理达到了最高的境界。企业仅仅能够通过信息手段与数字数手段来实现对项目的管理, 而且能够充分利用信息资料,对企业在项目实施的过程中可能出现的次品予以预防。能够主动地改善流程,运用新技术,实现流程的优化 CMMI是英文Capacity Maturity Model Integrated的简称。 中文的译意是能力成熟度集成模型。CMMI是CMM模型的最新版本。早期的能力成熟度模型是一种单一的模型其英文缩写为CMM,较多地用于软件工程。 ### 数据流图 [数据流图(DFD)_溢出的vector的博客-CSDN博客_数据流图](https://blog.csdn.net/weixin_46694417/article/details/120588235) #### 数据流图平衡 [【软件工程】数据流图 ( 数据字典 | 数据流图平衡原则 | 父图与子图平衡 | 子图内平衡 | 数据流图绘制原则 )_韩曙亮的博客](https://blog.csdn.net/shulianghan/article/details/109276722) ### 统一过程 (RUP) 统一过程有四个阶段,每个阶段又有多个任务。 ### 软件专利 这篇博客讲的还挺简单、详细的 [软考中级软件设计师---知识产权(自用)_嘟嘟的程序员铲屎官的博客](https://blog.csdn.net/weixin_42753193/article/details/124991506) ### 风险管理 **风险识别** 可以用不同的方法对风险进行分类。从宏观上来看,风险可以分为项目风险、技术风险和商业风险。<mark>项目风险识别潜在的预算、进度、个人、资源、用户和需求</mark>方面的问题。<mark>技术风包括识别潜在的设计、实现、接口、检验和维护</mark>方面的问题。而商业风险则主要来源于市场。 风险识别的重要工作就是将潜在的风险找到,文档化。 **风险估计** 风险估计使用两种方法来估计每一种风险。**一种方法是估计其发生的可能性;另一种方法是估计它可能带来的破坏性**。然后根据这样的结果对其进行排列优先级,对于那种可能性大、破坏力也大的风险就应该更加重视,拟定相应的解决方案才能够有效地防范。 **风险驾驭** 风险驾驭是指利用某种技术,如原型化、软件自动化、软件心理学、可靠性工程学,以及某些项目管理方法等设法避开或转移风险。 ### 内聚的分类 内聚的种类由紧到松(越紧越好)依次为: 1. **功能内聚**:指模块内的所有元素共同作用完成一个功能,缺一不可。 2. **顺序内聚**:指一个模块中的各个处理元素都密切相关于同一功能且必须顺序执行,前一功能元素的输出就是下一个功能元素的输入。 3. **通信内聚**:指模块内所有处理元素都在同一个数据结构上。 4. **过程内聚**:指一个模块完成多个任务,这些任务必须按指定的过程执行。 5. **瞬时内聚**:把需要同时执行的任务或动作组合在一起(如初始化模块)。 6. **逻辑内聚**:模块完成逻辑上相关的一组任务。 7. **偶然内聚**:指一个模块内的各处理元素之间没有任何联系或有松散的联系。 ### 耦合的分类 耦合的种类从高到低(越低越好)依次为: 1. **内容耦合**:一个模块直接使用另一个模块的内部数据,或通过非正常入口转入另一个模块内部时,这种耦合关系就是内容耦合。 2. **公共耦合**:指一组模块访问一个公共数据环境,如全局数据结构。 3. **外部耦合**:指一组模块访问一个公共变量,这里指基本数据类型而不是数据结构(或者说对象)。 4. **控制耦合**:指一个模块调用另一个模块时,传递的是控制变量,被调用模块通过该控制变量的值选择执行模块内某一功能。那么也就是说,被调用的模块应具有多个功能。 5. **标记耦合**:耦合模块之间以数据结构传递(比如在 java 程序中,传递的就是一个对象)。 6. **数据耦合**:耦合模块之间有调用关系,传递的是简单数据类型的值(比如在 java 程序中,传递的就是一个基本数据类型的值)。 7. **无直接耦合**:指两个模块之间没有直接的关系,它们分别从属于不同模块的控制与调用,它们之间不传递任何信息。 ### 软件体系结构风格 ~~考得不多,但不是不考~~ [13种常见软件体系结构风格定义分析、结构图、优缺点_Jayphone17的博客](https://blog.csdn.net/Jayphone17/article/details/103651076) ## 操作系统 ### 进程分配图 [ 资源分配图化简法_coding1994的博客-CSDN博客](https://blog.csdn.net/coding1994/article/details/52474731) ### 硬盘位视图 [位示图_百度百科 (baidu.com)](https://baike.baidu.com/item/%E4%BD%8D%E7%A4%BA%E5%9B%BE/2475925?fr=aladdin) 一bit代表一个硬盘块 ### Flynn算法 [Flynn分类法 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/413778318) ### 实时系统 实时操作系统是保证在一定时间限制内完成特定功能的操作系统。 #### 分类 实时操作系统有硬实时和软实时之分,**硬实时要求在规定的时间内必须完成操作**,这是在操作系统设计时保证的;**软实时则只要按照任务的优先级,尽可能快地完成操作即可**。我们通常使用的操作系统在经过一定改变之后就可以变成实时操作系统。 #### 要求 - 多任务 - 处理能被区分优先次序的进程线 - 一个中断水平的充分数量 #### 特征 - 高精度计时系统 - 多级中断机制 - 实时调度机制 ## 计网 ### 实现IPv4到IPv6的通信 #### 双栈协议 在IPv6实现之前,使一部分主机装有双协议栈:一个IPv4和一个IPv6。经过IPv4网络时将IPv6报文头转化为IPv4报文头 #### 隧道技术 在IPv6报文将要进入IPv4网络的时候将IPv6数据报封装在IPv4数据报里面

软考复习之安全性措施

# 安全性措施 ## 加密算法 ### 术语 - [明文](https://baike.baidu.com/item/%E6%98%8E%E6%96%87?fromModule=lemma_inlink),即原始的或未加密的[数据](https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE?fromModule=lemma_inlink)。通过[加密算法](https://baike.baidu.com/item/%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95?fromModule=lemma_inlink)对其进行加密 - 密文,明文加密后的格式,是加密算法的输出信息。密文不应为无[密钥](https://baike.baidu.com/item/%E5%AF%86%E9%92%A5?fromModule=lemma_inlink)的用户理解,用于数据的存储以及传输; - 加密,把明文转换为密文的过程; - 加密算法,加密所采用的变换方法,[加密算法](https://baike.baidu.com/item/%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95?fromModule=lemma_inlink)的输入信息为明文和[密钥](https://baike.baidu.com/item/%E5%AF%86%E9%92%A5?fromModule=lemma_inlink); - 密钥,是由数字、字母或特殊符号组成的字符串,用它控制数据加密、解密的过程;加密算法是公开的,[密钥](https://baike.baidu.com/item/%E5%AF%86%E9%92%A5?fromModule=lemma_inlink)则是不公开的 - 解密,对密文实施与加密相逆的变换,从而获得明文的过程; - 解密算法,解密所采用的变换方法。 ### 简介 数据加密是对明文按照某种加密算法进行处理,形成密文。这样一来,密文即使被截获,截获方也无法或难以解码,从而防止泄露信息。 - 秘密密钥加密体制K1=K2:加密和解密采用相同的密钥,因而又称为对称密码体制。因为加密速度快,通常用来加密大批量的数据。 - 公开密钥加密体制K1≠K2:又称不对称密码体制,其加密和解密使用不同的密钥;其中一个密钥是公开的,另一个密钥则是保密的。由于加密速度较慢,所以往往用在数据量较小的通信业务中。 ### 目的 - 提供高质量的数据保护,防止数据未经授权的泄露和未被察觉的修改; - 应具有相当高的复杂性,使得破译的开销超过可能获得的利益,同时又要便于理解和掌握; - 密码体制的安全性应该不依赖于算法的保密,其安全性仅以加密密钥的保密为基础; - 实现经济,运行有效,并且适用于多种完全不同的应用。 ### 具体算法 #### DES算法 **简介**: DES全称为Data Encryption Standard,即数据加密标准,是一种使用[密钥加密](https://baike.baidu.com/item/%E5%AF%86%E9%92%A5%E5%8A%A0%E5%AF%86/5928903?fromModule=lemma_inlink)的块算法 **参数:** DES算法的入口参数有三个:**Key、Data、Mode**。其中Key为7个字节共56位,是DES算法的工作密钥;Data为8个字节64位,是要被加密或被解密的数据;Mode为DES的工作方式,有两种:加密或解密。 **历史沿革**: 一般DES算法的密钥长度为56位,为了加速DES算法和RSA算法的执行过程,可以用硬件电路来实现加密和解密。针对DES密钥短的问题,科学家又研制了80位的密钥,以及在DES的基础上采用三重DES和双密钥加密的方法。即用两个56位的密钥K1、K2,发送方用K1加密,K2解密,再使用K1加密。接收方则使用K1解密,K2加密,再使用K1解密,其效果相当于将密钥长度加倍。 #### RSA算法 **简介** 在公开密钥密码体制中,**加密密钥(即公开密钥)PK是公开信息,而解密密钥(即秘密密钥)SK是需要保密的**。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的,但却不能根据PK计算出SK **过程** 先生成一对RSA密钥,**其中之一是保密密钥,由用户保存**;另一个为公开密钥,可对外公开,甚至可在网络服务器中注册。为提高保密强度,RSA密钥至少为500位长。这就使加密的计算量很大。为减少计算量,在传送信息时,常采用传统加密方法与[公开密钥加密](https://baike.baidu.com/item/%E5%85%AC%E5%BC%80%E5%AF%86%E9%92%A5%E5%8A%A0%E5%AF%86/8090774?fromModule=lemma_inlink)方法相结合的方式,即信息采用改进的DES或IDEA对话密钥加密,然后使用RSA密钥加密对话密钥和信息摘要。对方收到信息后,用不同的密钥解密并可核对信息摘要 **算法** 略,可参考[RSA算法_百度百科](https://baike.baidu.com/item/RSA%E7%AE%97%E6%B3%95) #### 其他算法 - 国际数据加密算法(IDEA)在1990年正式公布。这种算法是在DES算法的基础上发展起来的,类似于三重DES.发展IDEA也是因为感到DES具有密钥太短等缺点,IDEA的密钥为128位,这么长的密钥在今后若干年内应该是安全的。 - 1993年4月16日,美国政府推出了cipper密码芯片,该芯片采用美国国家安全局设计的Skipjack加密算法。采用Cipper的加密体制能为信息传输提供高等级的安全和保密,该体制是以防篡改硬件器件(Cipper芯片)和密钥Escrow(第三方托管)系统为基础的。 - 1994年2月14日,美国政府宣布了Escrow加密标准,其加密算法使用Skipjack.该算法采用80位密钥和合法强制访问字段(aw Enforcement Access Fied,EAF),以便在防篡改芯片和硬件上实现。由于使用了80位的密钥,Skipjack算法具有较高的强度。 | 名称 | 对称性 | 特点 | 密钥长度(通常) | |------|-----|--------------|----------| | DES | 对称 | 不够安全 | 56 | | RSA | 不对称 | 安全性高,速度慢 | 512 | | IDEA | 对称 | 速度快,密钥管理复杂困难 | 128 | ## 身份认证技术 数字签名用来保证信息传输过程中信息的完整和提供信息发送者的身份认证和不可抵赖性,该技术利用公开密钥算法对于电子信息进行数学变换,通过这一过程,数字签名存在于文档之中,不能被复制 ### 哈希签名 Hash签名不属于强计算密集型算法,应用较广泛。Hash的主要局限是接收方必须持有用户密钥的副本以检验签名,因为双方都知道生成签名的密钥,较容易攻破,存在伪造签名的可能。 #### MD5 MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的[密码散列函数](https://baike.baidu.com/item/%E5%AF%86%E7%A0%81%E6%95%A3%E5%88%97%E5%87%BD%E6%95%B0/14937715?fromModule=lemma_inlink),**可以产生出一个128位(16[字节](https://baike.baidu.com/item/%E5%AD%97%E8%8A%82/1096318?fromModule=lemma_inlink))的散列值(hash value)**,用于确保信息传输完整一致。 ### RSA签名 RSA既可以用来加密数据,也可以用于身份认证。 RSA算法中数字签名技术实际上是通过一个Hash函数来实现的。数字签名的特点是它代表了文件的特征,文件如果发生改变,数字签名的值也将发生变化。不同的文件将得到不同的数字签名。 ### DSS算法 对数字签名和公开密钥加密技术来说,都会面临公开密钥的分发问题,即如何把一个用户的公钥以一种安全可靠的方式发送给需要的另一方。这就要求管理这些公钥的系统必须是值得信赖的。 所以,必须有一项技术来解决公钥与合法拥有者身份的绑定问题。假设有一个人自称某一个公钥是自己的,必须有一定的措施和技术来对其进行验证。 **数字证书**是解决这一问题的有效方法。它通常是一个签名文档,标记特定对象的公开密钥。**数字证书由一个认证中心(CA)签发**,认证中心类似于现实生活中公证人的角色,它具有权威性,是一个普遍可信的第三方。当通信双方都信任同一个CA时,两者就可以得到对方的公开密钥,从而能进行秘密通信、签名和检验。 #### 数字证书的基本工作原理: 第一,发送方在发送信息前,需先与接收方联系,同时利用公钥加密信息,信息在进行传输的过程当中一直是处于[密文](https://baike.baidu.com/item/%E5%AF%86%E6%96%87/9684333?fromModule=lemma_inlink)状态,包括接收方接收后也是加密的,确保了信息传输的[单一性](https://baike.baidu.com/item/%E5%8D%95%E4%B8%80%E6%80%A7/6153534?fromModule=lemma_inlink),若信息被窃取或截取,也必须利用接收方的[私钥](https://baike.baidu.com/item/%E7%A7%81%E9%92%A5/8973452?fromModule=lemma_inlink)才可解读数据,而无法更改数据,这也有利保障信息的完整性和安全性。 [3] 第二,数字证书的数据[签名](https://baike.baidu.com/item/%E7%AD%BE%E5%90%8D/2890277?fromModule=lemma_inlink)类似于[加密](https://baike.baidu.com/item/%E5%8A%A0%E5%AF%86/752748?fromModule=lemma_inlink)过程,数据在实施加密后,只有接收方才可打开或更改数据信息,并加上自己的[签名](https://baike.baidu.com/item/%E7%AD%BE%E5%90%8D/2890277?fromModule=lemma_inlink)后再传输至发送方,而接收方的[私钥](https://baike.baidu.com/item/%E7%A7%81%E9%92%A5/8973452?fromModule=lemma_inlink)具唯一性和[私密性](https://baike.baidu.com/item/%E7%A7%81%E5%AF%86%E6%80%A7/7896067?fromModule=lemma_inlink),这也保证了签名的[真实性](https://baike.baidu.com/item/%E7%9C%9F%E5%AE%9E%E6%80%A7/6345696?fromModule=lemma_inlink)和[可靠性](https://baike.baidu.com/item/%E5%8F%AF%E9%9D%A0%E6%80%A7/512935?fromModule=lemma_inlink),进而保障信息的安全性。 简单来说,发送方用**公钥**加密信息,接收方收到后用**接收方的私钥**解密

软考复习之软件需求

# 软件需求 软件需求是 (1)用户解决问题或达到目标所需条件或权能(Capability)。 (2)系统或系统部件要满足合同、标准、规范或其它正式规定文档所需具有的条件或权能。 (3)一种反映上面(1)或(2)所述条件或权能的文档说明。 它包括功能性需求及非功能性需求,非功能性需求对设计和实现提出了限制,比如性能要求,质量标准,或者设计限制。 ## 类型 - 业务需求(Business Requirments):组织或者客户高层次的目标,从宏观上描述开发系统的必要性、意义和目标,具有以业务为想到、可度量、合理、可行的特点。BR的核心部分是业务建模,对当前企业当前业务流程进行评估,并对新开发系统的业务处理流程进行展望。 - 用户需求(User Requirements):用户要求系统必须要完成的任务,即用户要系统做什么,产生什么业务价值。 - 系统需求(System Requirments):整个系统的顶级需求,由系统分析人员对UR进行分析、提炼、整理,从而生成指导开发的、更准确地软件需求。完整的表达了软件项目的预期特征,为接下来的软件设计和测试提供了依据和基础。 - 功能需求(Functional Requirements):规定开发人员必须在产品中要实现的软件功能 ### 非功能性需求 - 产品必须遵从的规范、标准和合约 - 外部界面的具体细节 - 性能需求 - 设计或实现的约束条件及质量属性 ## 过程标准 清楚(Clear)、完整(Complete)、一致(Consistent)、可测试(Testable)。 此外还有其他的概念,如可跟踪的、可修改的等等

修复fluid的tags中的词云tag跳转异常问题

# 修复fluid的tags中的词云tag跳转异常问题 ## 前言 最近在尝试搭建自己的blog,再再三考量之下选择了**hexo**,不得不说这个blog框架还是很香的,配合github的托管能够快速搭建一个界面美观的博客。 但是在使用的过程中我发现了一个小问题,就是博客的tags并不能正常工作。 当我们从首页中的tag跳转时,可以跳转到正常的界面,如图: ![](../../essay/prose/imperial_edict/img1.png) <font color=red>但是当我从tags界面的词云中跳转时就会出现错误</font> ![](fluid-tags-bug-fix/img2.png) 错误界面如图: ![](fluid-tags-bug-fix/img3.png) <!-- more --> ## 解决方法 <font color=red>我们可以很明显的观测到问题——url地址错误,重复了一次'tags/'目录</font> 我们首先说解决方案: <font color=red>找到目录`<your hexo dirctory>\node_modules\hexo\lib\plugins\helper`中的tagcloud.js文件,将文件第71行改为:</font> ```javascript `<a href="${url_for.call(this, tag.path).substring(5)}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}</a>` //原代码为: //`<a href="${url_for.call(this, tag.path))}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}</a>` //加上了一个子字符串分割 ``` ***注意!这个方法只适合跟我描述的错误相同的朋友,如果问题不同请不要随意更改!*** ## 详细解释 这一部分讲述我怎么分析并解决问题的,如果不感兴趣可以直接关掉网页了 首先发现到这一点后,我企图弄清楚fluid是如何生成tags这个html文件的。但是我观察文件夹时并没有和明显的察觉到这一点。于是我找到`public`文件夹,**这个文件夹存放的是通过hexo生成的html文件。** 然后我们找到`tags`文件夹下的`index.html`   文件,随着翻阅html文件,我发现了这样的一块代码: ```html <div class="text-center tagcloud"> <a href="tags/node-js/" style="font-size: 15px; color: #bbe">node.js</a> <a href="tags/前端/" style="font-size: 15px; color: #bbe">前端</a> </div> ``` 毫无疑问,这就是生成词云的代码,但是我已经生成为html的文件我们并不能直接改,因为如果要这样解决问题,每次生成新的html之后我们都要手动修改一次,这是很没有效率的。于是我们顺藤摸瓜继续寻找问题。 于是我在webstrom使用<kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>r</kbd>全局查找tagcloud,发现在fluid的layout中有一个名为`tags.ejs`文件,这显然是生成tags的文件之一。其中有这样的一块代码: ```ejs <div class="text-center tagcloud"> <%- tagcloud({ min_font: min_font, max_font: max_font, amount: 999, unit: unit, color: true, start_color, end_color }) %> </div> ``` 很明显,这是生成上面那个html的映射文件,但是这其中依然没有修改herf的方法。于是我们继续向源头寻找。 然后我们发现tagcloud包是被引用进来的,而他的源码就在我们面提到过的文件夹中。 ```javascript `<a href="${url_for.call(this, tag.path)}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}</a>` ``` 这一行很明显就是生成每个小tag的代码。虽然我们看不太懂`${url_for.call(this, tag.path)}`这行代码的意思,但是我们知道最终他都会被解析为字符串。如果你熟悉js,就会知道js拥有将所有东西转化为字符串的能力😂因此我们直接使用substring方法来更改这一行代码就可以解决问题了

修復hexo fluid归档、分类、标签界面查看文章404错误

# 修復fluid归档、分类、标签界面查看文章404错误 ## 前言 上次遇到了[tag跳转的问题](https://charlesix59.github.io/2022/09/01/fluid-tags-bug-fix/)之后,我又遇到了这个奇怪的问题。按照按图索骥的原则,我又用那篇文章中提到过的方法进行了纠错,最终也是顺利的解决了这个bug。 错误如图: ![](../../essay/prose/imperial_edict/img1.png) <!-- more --> ## 解决方案 <font color=red>还是和以前一样,先说结论</font> <font color=orange>警告⚠:如果你的错误和我的不同,请不要随意修改</font> 首先找到fluid主题的目录中的`<your fluid dir>/layout/_partials` 打开`category-list.ejs`文件 修改第**45行**为 ```html <a href="../<%= url_for(cat.path) %>" style="text-align: center" class="list-group-item list-group-item-action"> ``` 修改第**50**行为 ```html <a href="../<%= url_for(post.path) %>" title="<%= post.title %>" ``` 然后打开`archieve-list.ejs` 修改第10行为 ```html <a href="../<%= url_for(post.path) %>" class="list-group-item list-group-item-action"> ``` 然后**复制**整个文档,**新建**一个名为`sub-archive-list.ejs`(名字可以自定)的文件,将复制的内容**粘贴**过去 并修改第10行为 ```html <a href="../../<%= url_for(post.path) %>" class="list-group-item list-group-item-action"> ``` 然后回到上一级目录,找到`tag.ejs`,修改第12行引用的文件为你新建的那个文件: ```ejs <%- partial('_partials/sub-archive-list.ejs', { params: { key: page.layout, postTotal: tag ? tag.posts.length : 0 } }) %> ``` 同时如上修改`category.ejs`中的第12行为: ```ejs <%- partial('_partials/sub-archive-list.ejs', { params: { key: page.layout, postTotal: cat ? cat.posts.length : 0 } }) %> ``` ## 其他 我不知道是不是因为我配置不完全或者安装出现问题才导致有这么多的bug,但是随着我的重装这个问题也没有得到很好的解决。 如果你也遇到了相似的问题可以尝试用这种方法解决 如果你对详细的解决思路感兴趣,你可以查看[前言](#前言)中提到的文章

认识与修改hexo的paginator(分页)系统

# 认识与修改hexo的pagintor(分页)系统 ## 前言 为啥我会出这篇博客呢?不出意外的当然是又出意外了。我的分页系统出现了一点问题。 当然,它可能从最开始就有一点问题,只不过是随着页面的增加我才渐渐注意到而已。 问题就是它会跳到当前页面+当前页面+分页,比如在archives界面点击第二页就会跳转到 `domain/archives/archives/page/2` 这个界面。这很明显是错误的 为了修正这个问题我又是对着各种源码一顿查找。 <!-- more --> ## 修改方案 老规矩,先贴出来修改方案。如果问题和我不同的就不要跟着做了。 首先找到我们需要修改的文件夹 `<your hexo directory>/node_modules/hexo/lib/plugins/helper/pagintor` 将第八行更改为 ```javascript return i => '/' + url_for.call(ctx, i === 1 ? base : base + format.replace('%d', i)); ``` 同时要保证在hexo目录中的配置文件`_config.yml`中主目录的地址路由为'' 即`index_generator`中的`path`属性的值为`''` ```yaml index_generator: path: '' per_page: 10 order_by: -date ``` ## 详解paginator 我觉得我直接贴注释就好 ```javascript 'use strict'; const { htmlTag, url_for } = require('hexo-util'); //这个函数就是用来创建链接的,它决定了路径的内容 const createLink = (options, ctx) => { const { base, format } = options; //加一个'/'是为了能够返回主目录 return i => '/' + url_for.call(ctx, i === 1 ? base : base + format.replace('%d', i)); }; //这个函数生成html界面的内容,具体说就是界面的按钮(数字加链接) const createPageTag = (options, ctx) => { const link = createLink(options, ctx); const { current, escape, transform } = options; return i => { if (i === current) { return htmlTag('span', { class: 'page-number current' }, transform ? transform(i) : i, escape); } return htmlTag('a', { class: 'page-number', href: link(i) }, transform ? transform(i) : i, escape); }; }; //为生成的所有的按钮进行排序与包装 const showAll = (tags, options, ctx) => { const { total } = options; const pageLink = createPageTag(options, ctx); for (let i = 1; i <= total; i++) { tags.push(pageLink(i)); } }; const pagenasionPartShow = (tags, options, ctx) => { const { current, total, space, end_size: endSize, mid_size: midSize } = options; const leftEnd = Math.min(endSize, current - 1); const rightEnd = Math.max(total - endSize + 1, current + 1); const leftMid = Math.max(leftEnd + 1, current - midSize); const rightMid = Math.min(rightEnd - 1, current + midSize); const spaceHtml = htmlTag('span', { class: 'space' }, space, false); const pageTag = createPageTag(options, ctx); // Display pages on the left edge for (let i = 1; i <= leftEnd; i++) { tags.push(pageTag(i)); } // Display spaces between edges and middle pages if (space && leftMid - leftEnd > 1) { tags.push(spaceHtml); } // Display left middle pages for (let i = leftMid; i < current; i++) { tags.push(pageTag(i)); } // Display the current page tags.push(pageTag(current)); // Display right middle pages for (let i = current + 1; i <= rightMid; i++) { tags.push(pageTag(i)); } // Display spaces between edges and middle pages if (space && rightEnd - rightMid > 1) { tags.push(spaceHtml); } // Display pages on the right edge for (let i = rightEnd; i <= total; i++) { tags.push(pageTag(i)); } }; //这是函数的入口,它接受option参数 function paginatorHelper(options = {}) { options = Object.assign({ base: this.page.base || '', //文章的路径 current: this.page.current || 0, //当前页 format: `${this.config.pagination_dir}/%d/`, //待格式化的page路径 total: this.page.total || 1, //总页面 end_size: 1, mid_size: 2, space: '…', next_text: 'Next', //下一个界面的信息 prev_text: 'Prev', //上一个界面的信息 prev_next: true, escape: true }, options); const { current, total, prev_text: prevText, next_text: nextText, prev_next: prevNext, escape } = options; if (!current) return ''; const link = createLink(options, this); const tags = []; // Display the link to the previous page if (prevNext && current > 1) { tags.push(htmlTag('a', { class: 'extend prev', rel: 'prev', href: link(current - 1)}, prevText, escape)); } if (options.show_all) { showAll(tags, options, this); } else { pagenasionPartShow(tags, options, this); } // Display the link to the next page if (prevNext && current < total) { tags.push(htmlTag('a', { class: 'extend next', rel: 'next', href: link(current + 1) }, nextText, escape)); } return tags.join(''); } module.exports = paginatorHelper; ``` ## 碎碎念 本来以为分页应该也是在ejs中生成的,我只需要修改ejs文件即可。 可是万万没想到hexo的分页是直接通过一个插件的函数生成html内容然后显示上的 所以这个改的还挺艰难的,差点给我愁死😭

IndexDB游标的正确打开方式

# IndexDB游标的正确打开方式 ## 引言 我最近在维护我的玩具项目`react-door`,当我试图使用IndexDB的游标遍历某个表时,我遇到了一些让人十分困惑的问题,下面我将带大家来复现一下事故的现场。 ## 复现 首先,在我的印象中,游标类似迭代器,所以刚开始我试图拿到游标传递出去然后在dao层调用: ```js // indexDB.js // 获取cursor const getCursor = () => { if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); return new Promise((resolve, reject) => { request.onsuccess = (e) => { const cursor = e.target["result"] resolve(cursor) } request.onerror = (e) => { reject(e) } }) } // in dao.js async function someFunc() { const cursor = await getCursor(); if (cursor) { // do something cursor.continue() } else { console.log("no more data") } } ``` 一开始我是这么设想的,但是我发现了一个令人特别疑惑的问题:为什么在MDN的文档中,使用`if...else...`来遍历这个cursor呢? ```js if (cursor) { var listItem = document.createElement("li"); listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year; list.appendChild(listItem); cursor.continue(); } else { console.log("Entries all displayed."); } // 来自MDN,可以看到使用 if else 进行遍历 ``` 当时我也是借鉴MDN,使用`if...else...`来遍历,但是我发现我的代码只能运行一次,这肯定不叫遍历啊。**谁家好人用`if...else`做遍历啊???** 年轻人不气盛能叫年轻人吗?这肯定是MDN写错了!根据我的经验,立刻给他改写成了这样: ```js async function someFunc() { const cursor = await getCursor(); // 从 if 语句变为 while 语句 while (cursor) { // 猜猜会输出什么? console.log(cursor.key); // do something cursor.continue() } console.log("no more data") } ``` 看起来没什问题,非常正确! **但是** ,在我实际运行这段代码的时候,并不能正常运行。我们希望它的输出是`1 2 3 4 5 ....`,而实际的输出结果却是`1 1 1 1 1 ...`。为啥会出现这种结果呢?😥本来我以为是`cursor.continue()`方法是异步的,但是结果这是一个完完全全的同步函数。怎么回事呢? 如果说`continue`方法不起作用的话,但是它确实能够帮我们将cursor向前移位,并在cursor结束时结束循环(虽然报了一个info),但是如果说它起作用的,我们的cursor每次都指向第一条数据。这是怎么回事呢??? 难道IndexDB这个方法还有bug?不可能吧?*难道真的是使用`if...else`遍历*?于是我再次查阅MDN,现在我为大家贴上MDN完整的代码段: ```js function displayData() { var transaction = db.transaction(["rushAlbumList"], "readonly"); var objectStore = transaction.objectStore("rushAlbumList"); objectStore.openCursor().onsuccess = function (event) { var cursor = event.target.result; if (cursor) { var listItem = document.createElement("li"); listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year; list.appendChild(listItem); cursor.continue(); } else { console.log("Entries all displayed."); } }; } ``` 经过我的观察,难道...`cursor只能在openCursor的回调中使用??` 带着这个猜想,我将我的代码改为这样: ```js if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); return new Promise((resolve, reject) => { request.onsuccess = (e) => { const res = [] const cursor = e.target["result"] if (cursor) { res.push(cursor.value) cursor.continue() } else { resolve(res) } } request.onerror = (e) => { reject(e) } }) ``` 嗯,果然不出所料,现在我们已经可以正确的遍历cursor了!但是又有一个问题随之而来,为什么每次我的res只有一个数据😅?经过我的一番思考,一个很恐怖的想法闪现出来,沃日,indexDB你不会....**每次游标移位都要调用onSuccess回调吧**? 于是我们再次改写代码,写成这样: ```js if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); // 将res放到回调函数之外 const res = [] return new Promise((resolve, reject) => { request.onsuccess = (e) => { const cursor = e.target["result"] if (cursor) { res.push(cursor.value) cursor.continue() } else { resolve(res) } } request.onerror = (e) => { reject(e) } }) ``` 这时,我们终于如愿以偿的拿到了我们需要的数据(真是辛苦啊😭) ## 总结 现在我们来总结一下cursor的坑: - cursor必须配合`oepnCursor`函数的回调函数一起使用 - 每次调用`cursor.continue()`并不是简单的将cursor移位,而是请求IDB的API更新cursor并再次触发`openCursor`的回调函数 所以上面那一版代码就是使用cursor遍历的标准模板,因为这个过程是类似递归的过程,所以我们的cursor是真的使用`if...else...`遍历代码😥,没毛病哦兄弟们

使用node.js创建一个todo列表——node.js服务器搭建、json读写以及使用pm2保持服务运行

# 使用node.js创建一个todo列表——node.js服务器搭建以及json读写 ## 前言 前些日子学习了 nodejs ,顺理成章的想要找点东西练练手。恰好最近需要一个简介的todo list网页,因为之前用的一些todo list应用都被墙了,访问速度感人,于是就想自己搞一个todo。 而恰好,我要搞的todo是一个非常简单的程序,非常适合用nodejs这样的解释型脚本语言来实现,比起java这样光搭框架就要半天的专注大型项目的语言,nodejs 的优势就是非明显了。 <!-- more --> nodejs搭建服务器只需要去nodejs官网复制这样的一块代码: ```javascript const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hi!') }) app.listen(3000, () => console.log('Server ready')) ``` 所以我们可以把更多的经历放在我们的业务实现上。 ## 准备工作 ### 需求分析 在开始动工之前,要先列出需求。当时我的需求如下: - 添加需要做的事情 - 展示需要做的事情 - 完成需要做的事情 - 查看曾经完成的事情 ### 解决方案分析 对于上述需求,我打算这样实现: - 使用json文件系统实现持久化操作 - 使用nodejs来获取数据 - 使用nodejs来处理数据并储存数据 这样的话。明确了需求与实现方式,我们就可以动工了。 ## 实现 ### json读写 nodejs提供了一套文件的读写的模块:`fs`。使用fs可以很简单的实现文本文件的读写。但是为了我们在开发的时候更加方便,我又将他封装成了一个专注json读写的方法: ```javascript //in jsonHandler.js const fs = require("fs"); function test(){ console.log("Hello world!") } function readJson(name){ let jsonFile = fs.readFileSync("./"+name); let toDoList = JSON.parse(jsonFile); //解析json,并直接返回json对象 return toDoList; } function writeJson(name,data){ fs.writeFileSync("./"+name,JSON.stringify(data)); } module.exports = { readJson, writeJson } ``` ### 服务器搭建 还记得我们的服务器的格式吗?我们先定义一下我们的服务器接口: ```javascript //in index.js const jh = require("./jsonHandler") const http = require('http'); const url = require('url'); const util = require('util'); let workList = jh.readJson("list.json"); const port = 3000 const server = http.createServer((req, res) => { //设置响应头与请求编码 res.statusCode = 200 //设置字节流编码为utf-8 res.setHeader('Content-Type', 'text/plain;charset=utf8') //设置允许跨域 res.setHeader('Access-Control-Allow-Origin','*') req.setEncoding('utf8'); //write your code here let ret=""; //这是需要返回的数据 res.end(ret) }) server.listen(port, () => { }) ``` 然后根据我们的需求分析,先来确定以下API接口: - `addWork()`:用来添加一个任务并将数据保存到json - `deleteWord(res)`:用来将一个任务删除并将数据保存到json - `writeHistory(work)`:将完成的任务添加到历史记录中 - `getAllWork()`:获取目前正在运行中的任务 首先我们先开始addWork()的编写,要添加的话,首先要接受并解析客户端发送的请求,然后再储存到json中,代码如下: ```javascript //用来增加一个任务 function addWork(){ let work=""; req.on('data', chunk => { //接受客户端发送的请求流 //这里要取子串的原因是请求会带key,我这里是work=xxx,所以要将'work='去掉 work=chunk.toString().substring(5); //设置编码,不然中文会乱码 work = decodeURIComponent(work); //解析worklist let jsonObj=eval(workList) jsonObj.push({"work":work}) //将数据写回 jh.writeJson("list.json",jsonObj) }) } ``` 任务历史与添加任务几乎同理,只不过这个方法获取的数据是deleteWork提供的,我们不需要再去监控客户端的请求。代码如下: ```javascript //任务历史 function writeHistory(work) { let jsonStr = jh.readJson("history.json"); let jsonObj = eval(jsonStr); let event = {} event.work = work; event.finishTime =new Date(); jsonObj.push(event); jh.writeJson("history.json",jsonObj) } ``` 删除任务也是同理,我们要在前端给每个任务隐性的标上index,然后就可以用来删除了: ```javascript //删除任务 function deleteWork(res){ req.on('data', chunk => { console.log(`可用的数据块: ${chunk}`) //获取点击事件的位置 let index=chunk.toString().substring(3); let jsonObj=eval(workList) //添加历史 let work = jsonObj[index].work writeHistory(work) //删除 jsonObj.splice(index,1); // console.log(jsonObj) jh.writeJson("list.json",jsonObj) }) } ``` 获取任务就非常简单了,我们只需要从json中拿到数据并转发给客户端就可以了: ```javascript //获取所有任务 function getAllWork(){ workList=jh.readJson("list.json"); ret=JSON.stringify(workList); } ``` 最后呢,我们需要写一个路由解析,因为js非常小,所以这些东西都写在一个文件里面就可以: ```javascript /解析路由 let pathname = url.parse(req.url).pathname; //路由配置 if (pathname==="/getAllWork"){ getAllWork() } else if(pathname==="/addWork"){ addWork() } else if(pathname==="/deleteWork"){ deleteWork() } ``` 随下贴出前端源码: ```html <-- in index.html --> <!DOCTYPE html> <html lang="ch"> <head> <link rel="stylesheet" type="text/css" href="https://cdn.simplecss.org/simple.css"> <link rel="stylesheet" type="text/css" href="technological.css"> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> </head> <body class="tech-background" style="text-align: center"> <p> wish you a substantial day</p> <p>to do list</p> <label> <input name="add-work" id="add-work" class="tech-input" style="width: 70%"> <button onclick='addWork()' class="tech-btn" style="display: inline;">增加</button> </label> <div id="container"></div> <script> 'use strict' $.ajax({ url:"http://localhost:3000/getAllWork", method:"GET", crossDomain: true, success:(res)=>{ let con=document.getElementById("container"); let workList=JSON.parse(res) console.log(workList) workList=eval(workList) for (let i=0;i<workList.length;i++){ let list=document.createElement("label"); list.innerHTML="<input name=\"work-list\" id=\"work"+i+"\" value='"+workList[i].work+"' class='tech-input' style=\"width: 70%\"> " + "<button onclick='deleteWork(this)' id='"+i+"' class='tech-btn' style=\"display: inline;\">完成</button>"; con.appendChild(list); } } }) function addWork(){ let work=document.getElementById("add-work").value; $.ajax({ url: "http://localhost:3000/addWork", method: "POST", crossDomain: true, data:{ "work":work }, success:(res)=>{ // alert("success"); location.reload(); } }) } function deleteWork(res){ let index=res.id; $.ajax({ url: "http://localhost:3000/deleteWork", method: "POST", crossDomain: true, data:{ "id":index }, success:(res)=>{ // alert("success"); location.reload(); } }) } </script> </body> </html> ``` 这个项目的源码我也会放在GitHub(因为项目实在太小我甚至都没有建一个文件夹) [项目地址](https://github.com/charlesix59/todo_list_by_nodeJS) ## 保持代码运行 我们服务器代码当然要有一个服务器去运行,而这个迷你的项目显然不值得让我们去分配他一个云服务器加一个域名,那么我们是否可以把这个服务运行在本地呢? 可是运行在本地就需要频繁的重启服务,每次开机都要启动一次,这未免也太麻烦了。于是我们就可以使用**pm2**来使我们的项目保持运行 关于pm2我在此不多赘述了,大家有兴趣的可以自行百度搜索,我只给出部署的命令: ```shell npm install pm2 -g #全局安装pm2 cd <项目目录> pm2 start index.js --watch pm2 save pm2 list ``` 如果你部署成功,那么当你运行`pm2 list`之后你就可以看到你的项目了

flex-direction为row时主动换行

# flex-direction为row时主动换行(React native主动换行) 我今天在写`react-native`时候,遇到了很不舒服的一件事情。因为RN默认是`flex`布局,所以如果我想要实现`inline`模式就只能设置父元素的`flex-direction`的value为`row`。这样如果我们如果想要换行需要怎么处理呢? 要知道,在RN中是没有`<br>`标签的,所以要想换行需要采取一些非常的手段。 那有没有什么好办法能够做到主动换行呢? 在正式开始之前我先上一个例子: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row' }}> {arr.map((item, index) => ( <View> <Text key={index}>{item}</Text> </View> ))} </View> </SafeAreaView> ); } ``` 很明显,上述的代码会生成一个纵向排列的`123456`,如果我们希望在**每三个断一下**,即将纵向的`123456`改成横向的`123 <br> 456`应该如何做呢? 刚开始的时候我问了ChatGPT,他给出的答案是使用伪类撑开元素: ```html <div class="flex-container"> <div class="flex-item">1</div> <div class="flex-item break-after">2</div> <!-- 想要在这个元素后换行 --> <div class="flex-item">3</div> <div class="flex-item">4</div> </div> ``` ```css .flex-container { display: flex; flex-wrap: wrap; flex-direction: row; } .flex-item { width: 100px; margin: 5px; } .break-after::after { content: ""; flex-basis: 100%; } ``` 经过我的尝试,这种方法很遗憾的不起任何作用,而且在RN中使用CSS class并不是很容易。但是他给出的代码确实给了我一些启发,经过一些摸索,我发现**如果一次渲染两个元素,其中一个是正常元素而另一个负责撑开元素**,那么就可以实现换行,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {arr.map((item, index) => { if (item % 3 !== 0) { return <Text key={index}>{item}</Text>; } else { return ( <> {[ <Text key={index}>{item}</Text>, <View style={{ width: '100%' }}></View>, ]} </> ); } })} </View> </SafeAreaView> ); } ``` 但是很抱歉,这样依旧有缺陷,因为reactNative中`<></>`实际上会影响继承,即Text和View将会被看作一个整体,而不会被解析成两个单独的元素,**并且`return`中的`jsx fragment`必须有一个根节点**,也就说说这种方法与顶上charGPT给出的方法并没有区别,都无法将一个元素完美换行。这只会让我们需要换行的元素变为单独的一行而已。 那么,还有什么方法呢? ## 完美的换行 这时候我们或许需要更新一下思路,如果在渲染过程中无法做到完美的换行,那么我们为什么不能在数据上动一点手脚呢?经过一系列思路的转换,我突然想到我们完全可以**进行一个数组分割+元素嵌套的方式实现换行**,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { const [arrState, setArrState] = useState([]); useEffect(() => { const result = []; let tempArr = []; arr.forEach((item) => { tempArr.push(item); if (item % 3 === 0) { result.push(tempArr); tempArr = []; } }); setArrState(result); }, []); return ( <SafeAreaView style={styles.container}> {arrState.map((subArr, index) => ( <View key={index} style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {subArr.map((item, subIndex) => { return <Text key={subIndex}>{item}</Text>; })} </View> ))} </SafeAreaView> ); } ``` 终于,我们实现了完美的换行! so,如果你也想要在flex布局中尝试主动换行,那么希望这篇文章能够解答你的疑惑。 当然,如果你有更好的方法,非常希望能在下方评论区和我分享你的好主意🤗

如何在 react 表达式中传入 react node[] (节点数组)

# 如何在 react 表达式中传入 react node[] (节点数组) 我们知道 jsx 表达式允许插入`reactNode`以及`ReactNode[]`,当但我们传入一个类似的东西的时候他却会报错: ```jsx <div className='App'> { <p>aaa</p> <p>bbb</p> // ❌ JSX expressions must have one parent element } </div> ``` 但是我们明明可以使用 map 函数传入数组呀! ```jsx <div className="App"> {arr.map((num) => ( <p key={num}>{num}</p> ))} </div> ``` 如果我们希望手动在表达式中传入一个节点数组我们应该怎么做呢?当我遇到这种奇葩问题的时候我也是愣了一会的,经过我对`Array.map()`方法的观察,我突然顿悟,如果想要手动传入一个节点数组应该这么写: ```jsx <div className="App">{[<p>aaa</p>, <p>bbb</p>]}</div> ``` 但是回头想想这个问题奇怪的很,我们明明可以写成: ```jsx <div className="App"> <p>aaa</p> <p>bbb</p> </div> ``` 但是其实有时候我们真的会需要手动的传入一个`node[]`,比如有些组件可能只接受一个节点数组而你又只有有限个节点不希望使用`map()`,或者是调试时候使用,再或者是希望在渲染列表中加入几个与之前的节点结构不同的节点,比如以下这个实例: ```jsx <div className="App"> {arr.map((num) => <p key={num}>{num}</p>) .concat([<p>aaa</p>, <p>bbb</p>])} </div> ``` 以上就是如何使用`reactNode[]`的一点个人心得咯

全面理解useEffect——useEffect用法解析与使用技巧

# 全面理解useEffect `useEffect`可以说是react的hook中最强大的一个,他能够以一种极为优雅的方式模拟我们的生命周期。但是优雅的代价就是这个hook极其的晦涩,不容易被理解与掌握。下面我们就来全方位的探讨一下,`useEffect`到底怎么用 ## useEffect的结构 在上一篇关于react的文章我提到过,函数式组件每次`reRender`都会重新执行一遍,如果我们需要生命周期的效果,那我们就需要`useEffect`函数。我们先来看一下useEffect的结构: ```js useEffect(setup, dependencies?) ``` - 参数 - `setup`:一个实现了你需要的副作用逻辑的函数,这个函数将会在组件首次被绑定以及每次**重新渲染导致`dependencies`更新时**调用。同时他还可以`return`一个清除函数,这个函数将会在函数卸载以及每次**重新渲染导致`dependencies`更新之前**调用 - `dependencies`:在`setup`中引用的所有`reactive values`(state、参数以及在组件内部定义的变量和函数)或更新凭据组成的数组。这个数组必须是有限的并且偏平的,每次更新react将使用`Object.is()`比较每个依赖项是否与之前相同,如果不同则更新。 - return - `undefinded` (无返回值) 中文博客中经常会漏掉`setup`函数的返回值,但是它的返回值是这个函数的半壁江山,我们决不能将其忽略不提,如果忽略掉的话我们就有一半的效果无法实现了。 ❗:`useEffect`也是一个hook,请在组件或者自定义hook的最上层使用,不要在循环或分支结构中定义hook ## 模拟生命周期 react生命周期大概分为三种,`mount`、`update`、`unmount`,当然在类式组件,我们已经可以规定是在mount之前,mount时还是mount之后调用,在函数式组件我们就不分的这么清楚了,因为在函数式组件我们的生命周期其实是独立的,是面向状态的。 下面我们就使用useEffect简单模拟一下类式组件的生命周期: **只在初始化时执行(`DidMounted`)** : ```js // 不依赖任何变量,则传入一个空数组 useEffect(() => { console.log("init"); return () => {}; }, []); ``` **每次更新时执行(`render`)** : ```js // 依赖所有变量,则不传入 useEffect(() => { console.log("rerender"); }); ``` **每次卸载时执行(`beforeUnmount`)** : ```js // 不依赖任何变量,return的函数将会在卸载之前被调用 useEffect(() => { return () => {console.log("unmount");}; }, []); ``` 实际上,我们经常说`useEffect`的作用是模拟声明周期,而这似乎违背了函数式组件的初衷,也违背了`useEffect`的初衷。我们应该多从**状态**的角度去思考,`useEffect`并非是什么生命周期函数,而是**状态更新**函数。 ## 控制更新 如果某个变量在每次组件更新的时候都需要更新,那我们完全没有必要使用`useEffect`,我们使用副作用钩子一定是因为我们需要控制某个变量的更新。 我们可以使用`useEffect`将**组件内部的状态与组件外部系统同步**: ```jsx // 比如说,我们可以在父组件更新props的时候调用useEffect const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildContext(count); }, [count]); return <p>this is a child, count = {childContext}</p>; }; ``` 或者我们可以使用`useEffect`将**组件组件外部系统与内部的状态同步**: ```jsx import "./styles.css"; import { useEffect, useState } from "react"; const fetch = async (count) => { // 假设这是一个网络请求 // 当然,这个外部状态也可以是一个外部组件,外部变量,jsAPI等等 await new Promise(() => { setTimeout(() => { console.log("fetch date"); }, count); }); }; export default function App() { const [count, setCount] = useState(0); const onClick = () => { setCount(count + 1); }; // 每次修改count都会将更改同步外部系统 useEffect(() => { fetch(count); }, [count]); return ( <div className="App"> <button onClick={onClick}>Change state in father</button> <p>count is {count}</p> </div> ); } ``` 其实生命周期也不过是对状态的统一更改,react现在只是将生命周期将颗粒度从组件降低到了状态。 ## 一些tips ### 在useEffect中使用state 有时候我们可能会需要在`useEffect`中使用和修改`state`,但是这是极端危险的行为,因为当我们修改`state`时会触发页面的`rerender`,然后因为`useEffect`依赖了`state`,这将会导致`state`被再次更改继而引起死循环…… ```jsx const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildConte(childContext); }, [count, childContext]); // !! 千万不要这么做,会引发死循环 return <p>this is a child, count = {childContext}</p>; }; ``` 如果我们在`useEffect`中根据之前的state更新state,我们可以这么写: ```jsx useEffect(() => { setChildContext(c => c + 1); }, [count]); // 不需要依赖childContext,安全 ``` 如果你真的既要在Effect中读取state,又要修改state,那么我给出的建议是,:**使用两个依赖同一个更新凭证`useEffect`** 千万别这么做: ```jsx useEffect(() => { setChildContext(Math.random()) console.log(childContext) }, [count, childContext]) // 死循环!!😭 ``` 正确的做法如下: ```jsx useEffect(() => { setChildContext(Math.random()) }, [count]); useEffect(() => { console.log(childContext) }, [count, childContext]) // 安全😊 ``` ### 消除不必要的对象与函数依赖 如果我们在组件内定义了一个函数或对象,我们在上一篇文章详细讲过,它每次都会被初始化。就像这样: ```jsx const childContext = { count: 1 } useEffect(() => { childContext.count = count; console.log("run effect") // 不要这样,每次Render都会触发effect😔 }, [count, childContext]); ``` 取而代之的是,如果你的对象或者函数只在`useEffect`中使用,则写在`setup`内部 ```jsx const Child = ({count}) => { useEffect(() => { const childContext = { count: 1 } childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` 如果你是希望有一个在整个组件都可以被访问的静态变量,那么可以将对象写在组件外部: ```jsx const childContext = { count: 1 } const Child = ({count}) => { useEffect(() => { childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` ## useLayoutEffect 我们在使用`useEffect`的时候有时会出现一种问题:我们的组件会发生闪烁或突然的变化。 这时因为`useEffect`是在`mount`之后执行的,因此第一次渲染出的值是我们定义的初始值,如果更新的性能比较差或者更新的范围比较大,可能会导致我们的`view`发生闪烁的现象。 这个时候我们就可以使用`useLayoutEffect`,他和`useEffect`的最大区别就是他是在`mount`之前执行的,所以不会导致`view`的闪烁。 但是如果我们没有很明显的ui闪烁问题,我们应该尽可能的使用`useEffect`,因为它的运行效率更好。

应该在哪里定义react函数——关于react开发规范的一些理解

# 应该在哪里定义react函数——关于react开发规范的一些理解 ## 前言 我之前有一个同事,从vue技术栈转入react技术栈,对于函数式编程还有一些不太熟悉。 最近他问了我一个问题,**react函数应该定义在什么地方**?他发现有些函数写在react组件之内,有些却写在react函数之外。他想知道哪种做法是最合适的。这是一个很好的问题,因为这体现了对于代码规范的追求。现在我希望在此妥善的梳理一下这个问题。 ## 看法 先开门见山的说一下我的看法,我的看法就是<mark>应外尽外</mark>,意思就是**能够**放到组件外部的函数都应该放到外部。但是需要注意的是,只有满足一定条件的函数才能够放到组件外部。在具体的讨论这些规则之前,我们应该先来看一下函数写在组件内与组件外部的<mark>区别</mark>。 ## 区别 其实两者的区别十分显著,我们在编写`jsx`文件的时候一般每个文件会暴露一个文件入口,也就是我们的组件函数。当我们在父组件使用我们的子组件的时候,实际上就相当于`new`出了一个新的组件实例。而在组件外的函数或者变量,则与组件形成了闭包。(不太了解闭包的可以理解为组件内的东西是原型模式,而组件外的东西是单例模式) 我们来看一个例子: 首先我们有一个Test组件: ```jsx let a = "123"; const Test = (props) => { return( <> <button onClick={()=>{console.log(a);a = props.value;}}>change</button> </> ) } export default Test; ``` 然后我们在App.js中调用这个组件: ```js function App() { return ( <div className="App"> <Test value={666}></Test> <Test value={777}></Test> </div> ); } ``` 可以看到我们在这里调用了两次Test组件,页面上应当有两个按钮。 这时我们应该想一下,当我们依次点击这两个按钮的时候输出的是 > 123 123 还是 > 123 666 呢? 答案显而易见,是`123 666`,各位可以自己去尝试一下。导致这种现象出现的原因其实不是react的组件,而是我们的`esm`。我们应该都对闭包有一定的概念,其实我们的模块化正是使用了闭包这一概念。当我们像上文那样编写Test组件的时候,其实闭包就已经形成了。 下面我们来梳理一下哪些函数是需要放到组件内的以及这其中的原因。 ## 需要在组件内的函数 ### Hook函数 一个hook函数只能在以下两个位置定义: 1. 自定义hook函数内部 2. React组件内部 但是其实我们的自定义hook最终也是在React组件内部调用,所以这就相当于每一个Hook函数最终都必须在React组件内部环境中执行。 ### 依赖Hook函数的函数 上文说过,hook函数只能在React组件内部调用,那么调用hook函数的函数也理所应当的需要写在组件内部。 ### 依赖State的函数 如果我们的函数需要直接使用State的值,那么我们需要将这个函数放置在组件内部,因为state是每一个组件实例私有的,在外部的函数无法读取到这个值。但是对于可能会复用的函数我们还是更推荐大家使用参数传递的方式传递state的值然后再使用setState的方式设置返回值。 不推荐: ```jsx // :( not recommend // a.jsx const [count,setCount] = useState(1); function a(){ setCount(count+2); } // b.jsx const [count,setCount] = useState(2); function b(){ setCount(count+2); } ``` 推荐 ```jsx // :) recommend //calcCount.js function calcCount(count){ return count+2; } //a.jsx const [count,setCount] = useState(1); setCount(calcCount(count)); // b.jsx const [count,setCount] = useState(2); setCount(calcCount(count)); ``` ### 依赖props的函数 依赖props的函数当然也不能将其写在组件外面。我们很大程度上是通过props去区别某一个组件函数的不同实例的,所以props也是组件实例私有的,需要写在外面。 ### 事件处理函数 虽然事件处理函数从理论上来说完全可以放置到组件外部(只要不依赖上面所说的),但是我们会认为事件处理函数是比较私人的东西,从习惯上来说我们还是更习惯将其放到组件的内部而不是抽出到一个util文件中,而且对于不同组件的事件处理函数,我们能抽象的部分也优先。 而且比起将事件处理函数抽象出来,我们更推荐将事件处理函数中处理逻辑的部分代码抽取出来,类似我们在MVC框架中所作的那样,model与controller的分离是值得推崇的设计思想。 ### 其他依赖组件实例的方法、变量的函数 其实我们可以看到,凡是我们的函数依赖了我们组件实例的私有变量、方法,我们都需要放在组件内部。 ## 为啥应外尽外? 以上我们整理出很多需要放到组件内部的情况,那为啥还推荐放到函数外部呢?直接全部放到函数内部不是会简单很多吗? 尽量将函数放到外面其实是一种函数式与模块化的设计思想,他驱使我们尽可能去复用我们的函数以及使用纯函数,综合来说,将函数与变量放到外部有如下几个好处: - 减少内存占用:减少组件内部的函数与变量可以减少组件实例中的内容,减少对内存的占用 - 重用代码:我们完全可以将一些负责逻辑、可以重用的函数归类并且放置到方法类(utils.js之类)中,减少我们的代码量 - 模块化:我们将逻辑与视图抽离完全可以减少我们组件的耦合度,并且可以降低模块体积,提高我们模块化的“效率”。而且能够使我们尽量去使用纯函数编写更优雅、更容易维护的代码 - 共享信息:我们可以刻意的通过将数据放置到组件外让这个组件的不同实例去共享信息。不过需要注意的是,我们放到组件外的数据肯定不是`React State`,这就意味着它们不是响应式的(一般不会有人这么做吧?) 综上所述,对于能够放置到组件函数外的应该尽量放置到函数外,对于能够复用的代码片,最好是能够抽取成全局的函数,这样才更符合模块化的要求。

软考复习之KMP算法

# KMP算法 KMP算法我曾今写过一篇[博客](https://blog.csdn.net/Charlesix59/article/details/119875803)讲过,但是我当时觉得这个算法好烂,就没细讲,后来发现他们还挺喜欢考这个算法的,于是我再开帖讲一下 ## 自然语言描述 自然语言描述是我根据《algorithm》的内容以及我自己的理解写出来的,,一般来说是正确的计算next[]与解释KMP的算法,这个方法比较考验悟性,但是原理确实是这样😂 ![](kmp/1.png) ## 考研教材解释 我们还是拿`pat=abcac`举例子,我们从a开始取字串,然后找前缀与后缀的最长相等长度 - ”a“最长相等长度为0 - "ab"最长相等长度0 - "abc"最长相等长度0 - "abca"最长相等长度1 - "abcac"最长相等长度0 所以我们可以得到PM={0,0,0,1,0} 但是我们有公式`右移位数=已匹配的字符数-对应的部分匹配值` 然后 <mark>对应的部分匹配值 是 j 前一个值</mark>,于是为了方便我们把PM整体右移一位,并用-1作为next[0]的值,得到`next[]={-1,0,0,0,1}` 所以我们有公式: `Move=(j-1)-next[j]` 这就相当于我们将J回退到: `j回退到的位置=j-Move=j-((j-1)-next[j])=next[j]+1` 所以我们就可以都得到next[]的新的形式: `next[]={0,1,1,1,2}` <mark>这里next[]的含义是在j处匹配失败后,j回退到的位置</mark> ## 拓展:更底层的匹配方法 实际上,KMP算法应该生成一个有限状态自动机(DFA),然后通过DFA去匹配字串 继续按照`pat=abcac`为例,画出的DFA如下: ![](kmp/2.jpg) 生成DFA的算法如下 ```cpp /*KMP字符串查找*/ class KMP { private: std::string pat; const static int r = 256; int *dfa[r]; //建立一个二维数组 public: KMP(std::string pat) { this->pat = pat; int m = pat.length(); for (int i = 0; i < r; i++) { dfa[i] = new int[m]; std::fill(dfa[i], dfa[i] + m, 0); } dfa[pat[0]][0] = 1; for (int x = 0, j = 1; j < m; j++) { for (int c = 0; c < r; c++) { dfa[c][j] = dfa[c][x]; //复制匹配失败情况下的值 } dfa[pat[j]][j] = j + 1; //设置匹配成功情况下的值 x = dfa[pat[j]][x]; //更新重启状态 } } int seatch(std::string txt) { int i, j, n = txt.length(), m = pat.length(); for (i = 0, j = 0; i < n&&j < m; i++) { j = dfa[txt[i]][j]; } if (j == m) return i - m; //找到匹配 else return n; //未找到匹配 } }; ```

为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别

# 为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别 ## 前言 今天在写代码的时候遇到一个问题,我的一个组件需要维护一个不需要在页面中渲染的变量。那么我想,既然它不需要渲染,我直接用`let`变量新建一个不就可以了吗? 但是当我真正用这个`let`创建的变量时,才意识到大事不好,原来这么使用变量有一个大问题! ## 为什么要有hook 要说明问题是什么,我们需要先弄明白一件事情,**我们使用函数式组件时我们怎么去`render`** ?在我们使用类式组件时,`rerender`可能是简单的调用一次`render`方法,那对于函数式组件呢?函数式组件的`render`写在`return`语句中,很明显我们需要重新执行整个函数。 那么我在前言部分提到的问题就呼之欲出了:组件每次渲染更新都会使得变量变为初始值。也就是说我在组件的一个函数中修改变量并更新组件,想在另一个函数中使用时我们获取到的就只会是初始值了。 可以看到,函数式组件是没有状态的!而react引入`hook`的原因就是补全函数式组件在这方面的缺陷。比如useState,当函数重新渲染时,state能够从状态池中获取到上次render的状态,像useEffect能根据变量来确定是否需要渲染,从而起到生命周期函数的作用。 也就是说,如果我们希望有一个能在每次渲染的时候不被重新初始化的变量,那么我们就需要借助hook了。 那么问题又来了,我们应该用哪一个`hook`呢?这里我推荐一个:`useRef`。 ## useRef 我们使用`useRef()`创建的变量不会每次都被重新渲染 ```js let count = 0 // 每次render都会init let count = useRef(0) // rerender时不会init ``` `useRef`的特性如下: - ref的值通过`current`属性获取 - ref是可以更改的 - 更改ref的值**不会**引起`rerender` - 如果ref的值被用于渲染,那么则不可更改 - 不建议在组件渲染中**读**写ref的`current`,推荐在`useEffect`与`event handler`中使用使用`ref.current` 可以看到,ref就是为我们保存**不用每次初始化**而且**不必渲染**的变量而生的。 ### useRef与useState 那么为什么我们要用ref而不去使用state呢?这两个hook有两个非常大的区别: - **ref不会引起rerender,而state会**。当我们调用setState时,会触发rerender,而在使用ref就不必担心这个,你可以随意的使用 - **ref是同步的,state是异步的**。当我们同步的使用useState更新数据之后,我们会获取不到最新的值,因为他是异步更新的。而ref则不会有这样的问题,你可以在更新之后立刻获取到最新的值。 所以对我现在的需求来说,`useRef`要比`useState`合适的多。 ### 使用useRef操作dom useRef的另一个重要的作用就是操作dom,我们在这边也顺带提一下吧。 我们可以这样使用ref: ```jsx const domRef = useRef(null) function focusInput(){ domRef.current.focus(); } return ( <> <input ref={domRef}/> <button onClick={focusInput}>click to focus<button> </> ) ``` 我们通过jsx中的`VNODE`的ref属性属性传递我们的ref,这样react会在渲染的时候自动将`current`的值设置为node。然后我们就可以访问到这个node了。 当这个node被从页面中移除时,`ref.current`也会被设置为`null` ## 总结 如果你希望在react函数式组件中保存一个不会每次渲染时更新并且无须被渲染的值,那么`useRef`是你的不二之选

解决插件使用GitHub calendar 插件无响应问题

最近开始发掘好用的hexo插件,先瞄准的是很久之前就看见过的小冰的GitHub Calendar [教程:hexo-githubcalendar 插件 1.0 | 小冰博客 (zfe.space)](https://zfe.space/post/hexo-githubcalendar.html) 但是按照他描述的方法配置完成之后,发现并没有生成Calendar,于是我就开始了漫长的排查。 <!--more--> ## 问题与解决方案 我这里先说我的问题,因为我是用了fluid主题,所以我先找到了小冰给出的fluid配置参数,实际上只需要将配置中的最后一行改为: ```yaml plus_style: "#github_container > .position-relative > .border{border:0!important}#github-calendar{position: relative;margin-top: -2rem;background-color: var(--board-bg-color);transition: background-color 0.2s ease-in-out;border-radius: 0.5rem;z-index: 3;-webkit-box-shadow: 0 12px 15px 0 rgb(0 0 0 / 24%), 0 17px 50px 0 rgb(0 0 0 / 19%);box-shadow: 0 12px 15px 0 rgb(0 0 0 / 24%), 0 17px 50px 0 rgb(0 0 0 / 19%);}" ``` 但是随着我按照教程完整的配置完全,发现并没有用,在对应插件的没有出现在它应该出现的位置。后来,觉得fluid有很多配置都是直接配置在`_config.fluid.yaml`中,我就尝试着将配置文件复制到这个文件一份,结果阴差阳错的成功了。 后来我又删除了hexo自带的config中的配置,结果又刷不出来了。 **所以,如果你是使用了某个主题,并且刷不出这个插件,<font color=red>你可以将主题和hexo的配置都加入插件配置</font>** ## 其他可能的问题 **这个插件想要使用应该不是只配置一个`_config.yaml`就可以了** ,你还需要在对应的界面(enable_page属性中对应的)加入相应的元素,如果是layout.type是id,那么就要插入: ```html <div id="这里替换为layout.name"> </div> ``` 如果你的layout.type是class,那么你要保证你那个界面又n-1个name为`layout.name`的代码块

React Native 移动光标,都什么年代还在用传统setNativeProps

# React Native 移动光标,都什么年代还在用传统setNativeProps? 对我的故事不感兴趣的直接跳转正文部分。 ## 念叨 今天遇到一个bug,要解决这个bug呢就需要手动移动input的光标。然后我发现之前我在~~抄~~借人家的代码的时候已经借过移动光标的代码了: ```typescript inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); ``` 于是我先去搜索了一下`setNativeProps`的用法,在最新版本的RN文档上明确的给出了这个方法的使用示例,但是却没有API文档。。。😓 也就是说,理论上,这个方法是可以使用的,于是我又搜索了一下如何使用这个方法实现移动光标,结果我发现**上述写法是完全正确的,但是完全没用!** 我还发现这些文章还说有一个`_lastNativeSelection`属性可供读取,但是我只读到了`undefined`。于是乎,我大胆的怀疑,这个方法是不是已经寄了😰 然后我打印了一下`inputRef.current`发现有这么一个属性:`"setSelection": [Function setSelection],`,于是乎我大胆尝试,请看下文…… ## 正文 不要再使用`setNativeProps`方法了,这个方法疑似不再受到支持了。如果你需要操作input的光标,请直接使用`setSelection`。示例如下: ```tsx inputRef.current.setSelection(1, 1); ``` 函数签名(<u>我猜的</u>): `setSelection(selectionStart,selectionEnd)` - `selectionStart` - 被选中的第一个字符的位置索引,从 0 开始。如果这个值比元素的 `value` 长度还大,则会被看作 `value` 最后一个位置的索引。 - `selectionEnd` - : 被选中的最后一个字符的 *下一个* 位置索引。如果这个值比元素的 value 长度还大,则会被看作 value 最后一个位置的索引。

react native 如何正确使用realm

# react native 如何正确使用realm Realm是一个支持云同步的快速的新型的数据库,是SQLite的替代品。Realm的官网实际上对Realm的使用有相当详细的讲解,但是对于我们实际开发中可能出现的一些问题官方文档可能会有没有提到的地方,这篇博客旨在记录笔者在使用realm时踩到的一些坑 ## 安装 安装过程官方说的很明确,但是要注意我们需要安装两个依赖: ```shell # 安装realm库 npm install realm # 安装一些操作realm的hooks等 npm install @realm/react ``` 在使用过程中也需要注意,我们引入依赖的位置。 ## 使用 目前的realm支持两种写法,一种是直接引入的方法: ```tsx import React from 'react'; import {RealmProvider} from '@realm/react'; // Import your models import {Profile} from '../../../models'; export const AppWrapper = () => { return ( <RealmProvider schema={[Profile]}> <RestOfApp /> </RealmProvider> ); }; ``` 在你需要的时候仅需要: ```ts import {useQuery} from '@realm/react'; const profiles = useQuery(Profile); ``` 如果你有多个schema,或者说多个表,直接在`RealmProvider`中的`schema`属性中添加即可,因为这里它接受一个**数组**嘛。 ## 更好的使用 但是我认为上述方法并不是最好的使用方法,我这里还是推荐直接使用`createRealmContext`的方式,这样能够让我们更灵活的使用`realm config`。 ```tsx // 配置realm const config: Realm.Configuration = { schema: [Settings, DarftSchema], schemaVersion: 1, onMigration: (oldRealm, newRealm) => { newRealm.deleteAll(); }, }; // 创建Provider const {RealmProvider, useRealm, useObject, useQuery} = createRealmContext(config); // 包裹App剩余部分 function App(): React.JSX.Element { return ( <RealmProvider> <RealmContext.Provider value={{useRealm, useObject, useQuery}}> <LayoutWarp /> </RealmContext.Provider> </RealmProvider> ); } ``` 但是由于我们使用了这种方式之后,就不能使用直接引用的全局api了,所以我们必须想办法把api传递给后代组件。我的建议是:`useContext` ```ts export const RealmContext = createContext({ useRealm, useObject, useQuery, }); ``` 在需要使用这些api的子组件调用: ```ts const {useRealm, useObject} = useContext(RealmContext); const realm = useRealm(); ``` 这样就不会报错了。

基于命令模式使用 React 实现中文自动前进输入框(OTP)

# 基于命令模式使用 React 实现中文自动前进输入框(OTP) 最近写的项目中有一个需要自动换格子、自动聚焦并且让父组件获取输入值的需求。这个需求让我很头大,因为在 RN 中想要控制 DOM 绝非一件容易的事情。 ## 难点分析 - RN 控制 dom 相对麻烦 - 输入的是中文,需要处理输入法或复制粘贴的问题 - 内容需要和父子组件同步 - 格子数量不定长 - 每个 input 相对复杂 ## 解决方案 React 的单向数据流与组件化的模式一定程度上可以提高我们系统的可维护性,而另一方面这种设计也会导致我们的父子组件通信相对麻烦。在此处如果我们想要使用`ref`来维护一个 input 数组这显然是不现实的,因为我们**不能在 for 循环中定义`react hooks`**。而且我的每个 Input 都相对比较复杂,所以我选择每个 input 封装一个组件,然后在组件中使用`ref`来控制`vDom`。那么我们如何通信呢?自然就是**传参**咯。那么子组件如何优雅的通知父组件自身输入的改变呢?经过思索我想到了一种设计模式:<mark>命令模式</mark>。**通过命令模式+状态函数我们可以优雅的将子组件的变化通知父组件**。下面我们先来尝试实现一下最基本的功能,自动跳转与聚焦。 ### 自动聚焦 首先我们需要先思考,一个输入框可能有几种状态,其实无非两种`输入`和`删除`。但是我们是一组输入框,在其中来回跳,所以我们还需要一个`回退`状态。 ```ts // 定义命令格式 interface BaseCommand { name: string; value?: unknown; } interface CheckInputCommand extends BaseCommand { value?: string | undefined; additionalValue?: string; callarIndex: number; } ``` ```tsx // 父组件中定义命令状态 const [command, setCommand] = useState<CheckInputCommand>(); ``` 这样,确定了状态,我们还需要确定一件事情,我们的子组件要接受哪些参数呢? 首先,设置命令的状态函数一定是必须的,另外我们还需要传递一个下标来让组件知道它的序号以及一个`boolean`来表示是否聚焦,代码如下: ```tsx // 定义子组件 type propsType = { setCommand: Dispatch<SetStateAction<CheckInputCommand | undefined>>; index: number; focus: boolean; }; function InputCheck({ setCommand, index, focus, }: propsType): React.JSX.Element {} ``` 并且我们在父组件中应该如是调用子组件: ```tsx // 父组件 // 聚焦元素的下标,默认第一个元素聚焦 const [foucsElement, setFoucsElement] = useState(0); return ( // 此处省略n个字 <View> {arr.map((item, keyIndex) => { return ( <InputCheck key={`${index}-${keyIndex}`} setCommand={setCommand} index={keyIndex} // 下标与聚焦元素的下标相同则聚焦 focus={foucsElement === keyIndex} /> ); })} </View> ); ``` 通过上述的代码,我们已经能够得知我们如何来让一个元素聚焦了:**如果一个元素的下标与我们希望聚焦的元素下标相同则让他聚焦**。我们知道在 react 中,**如果 state 发生了变化则会引起发生变化的节点`rerender`,并且如果组件的 props 发生了变化会引起`reaction`的,这一系列反应让我们可以通过父节点的`state`来控制子组件。** 那么在子组件我们应该如何处理这种变化呢? 那自然是`useEffect`啊! ```jsx // 子组件 const [char, setChar] = useState(""); const inputRef = useRef<any>(); // 处理聚焦 useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); } }, [focus, setInputValue]); return ( <TextInput value={char} onChange={e => { TextInputHandler(e.nativeEvent.text); }} ref={inputRef} /> ); ``` 好的,这样我们就可以处理父组件传递的`focus`参数并且自动聚焦了。而且通过上面的代码我们也能发现我们返回的`TextInput`是一个<mark>受控组件</mark>哎~那我们又是如何处理输入的呢? 详情还请看代码: ```jsx /** 发送指令的函数 */ const sendCommand = useCallback( (name: string, commandValue?: string, additionalValue?: string) => { const command: CheckInputCommand = { name: name, value: commandValue, callarIndex: index, additionalValue: additionalValue, }; setCommand(command); }, [index, setCommand] ); /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand] ); /** 处理输入 */ const TextInputHandler = (inputChar: string) => { setInputValue(inputChar); }; ``` 这样当我们输入的时候,子组件就已经能够发送命令通知父组件自己的变化了,这时我们就还需要父组件去监听子组件以及做出相应的反应咯: ```jsx // 父组件 /** 监听command变化 */ useEffect(() => { /** 接收子组件发送的命令 */ if (command && command.name === "input") { if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } }, [command, format.tunes, format.tunes.length]); ``` 这样我们就可以自动的前进并聚焦啦! ### 处理中文 但是考虑到我们输入的是中文,所以我们必须要对输入进行一定的处理。首先是中文有输入法的问题,**这样我们就需要判断是否是中文,然后只对中文做出相应**。 ```ts // 中文匹配正则 const reg = /^[\u4E00-\u9FA5]+$/; /** 检查是否是中文 */ const verifyCharIsChinese = (char: string) => { if (reg.test(char)) { return true; } return false; }; ``` ```jsx // 子组件 /** 真正处理输入的逻辑 */ const setInputValue = useCallback({ if (!verifyCharIsChinese(str)) { return; } // 截取中文 let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand], ); ``` 另外因为中文会存在一次性输入多个字符的特点,所以我们就需要将输入的字符传递给父组件的问题。像上面的代码,我们已经看到我们已经截取了输入,然后发送到父组件,我们又该如何让父组件将多出的字符传递给剩下的子组件呢?**我想到的方法是:`useContext`!** ```tsx // 父组件 export const StrContext: Context<string> = createContext(""); fn(){ //省略函数定义 const [content, setContent] = useState<Array<string>>([]); // 保存真正的内容 if (command && command.name === "input") { useEffect(() => { if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } } return( // ...此处省略n-m个字 <StrContext.Provider value={chars}> <View> {arr.map((item, keyIndex) => { return ( <InputCheck key={`${index}-${keyIndex}`} setCommand={setCommand} index={keyIndex} // 下标与聚焦元素的下标相同则聚焦 focus={foucsElement === keyIndex} /> ); })} </View> </StrContext> ) } ``` 同时我们**需要让得到焦点的子组件去处理我们的上下文**,即输入与重新发送指令: ```jsx const value = useContext(StrContext); /** 自动移动字符 */ useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({ selection: { start: 1, end: 1 } }); // 在上文的处理聚焦的函数中添加判断是否有全局字符串的方法 if (value) { setInputValue(value); } } }, [focus, setInputValue, value]); ``` ### 处理删除与后退 如果光前进却不能后退那不是没过河的<font color="green">卒</font>子嘛。看到如何处理前进那么如何处理后退应该也很明确了,但是需要注意,删除与后退并不一样, - **删除**:输入为空 - **后退**:输入已经为空并且继续删除 我们先来实现删除,删除非常简单,在处理输入的时候判断是否为空即可: ```jsx // 子组件 /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { // 输入为空则发送删除命令 if (!str) { setChar(""); sendCommand("delete"); return; } if (!verifyCharIsChinese(str)) { return; } let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand] ); ``` <u>_父组件该如何处理我这里就掠过咯_</u>~ 处理后退要稍微复杂一些,首先我们需要在子组件中新增一个监听,监听我们的键盘输入: ```tsx // 子组件 /** 监听键盘事件,处理退格事件 */ const backSpaceHandler = (keyName: string) => { if (keyName === "Backspace" && !char) { sendCommand("back"); } }; return ( <TextInput style={TuneStyle} value={char} onChange={(e) => { TextInputHandler(e.nativeEvent.text); }} onKeyPress={(e) => { backSpaceHandler(e.nativeEvent.key); }} ref={inputRef} /> ); ``` 然后父组件也需要处理退格事件: ```jsx // 父组件 else if (command && command.name === "back") { if (command.callarIndex === 0) { return; } setFoucsElement(command.callarIndex - 1); } ``` ### 内容同步 哎呀,内容同步这种事情读者朋友们自己个性化发挥就好,毕竟我在命令模式中携带了挺多参数呢,请大家根据各自的需求随意发挥就好。 ## 完整代码 父组件: ```jsx export const StrContext: Context<string> = createContext(""); export default function fn(): React.JSX.Element { const [command, setCommand] = useState<CheckInputCommand>(); const [foucsElement, setFoucsElement] = useState(0); const [chars, setChars] = useState(""); // 多出的文字,自动填充到下一个block const [content, setContent] = useState<Array<string>>([]); // 保存真正的内容 /** 监听command变化 */ useEffect(() => { /** 接收子组件发送的命令 */ if (command && command.name === "input") { // 添加到内容 if (command.callarIndex >= 0) { setContent(e => { e[command.callarIndex] = addContentChar( command.additionalValue || "", command.callarIndex, ); return [...e]; }); } if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } else if (command && command.name === "delete") { setContent(e => { e[command.callarIndex] = ""; return [...e]; }); } else if (command && command.name === "back") { if (command.callarIndex === 0) { return; } setFoucsElement(command.callarIndex - 1); } }, [command]); return ( <StrContext.Provider key={index} value={chars}> <View style={fillPoemStyle.inlineContainer} key={index}> {arr.map((item, keyIndex) => { return ( <InputCheck tune={item.tune} rhythm={item.rhythm} key={`${index}-${keyIndex}`} setCommand={setCommand} index={item.index as number} focus={foucsElement === item.index} rhymeWord={rhymeWord} /> ); })} </View> </StrContext.Provider> } ``` 子组件 ```jsx type propsType = { setCommand: Dispatch<SetStateAction<CheckInputCommand | undefined>>; index: number; focus: boolean; }; function InputCheck({ setCommand, index, focus, }: propsType): React.JSX.Element { const [char, setChar] = useState(""); const inputRef = useRef<any>(); const value = useContext(StrContext); const sendCommand = useCallback( (name: string, commandValue?: string, additionalValue?: string) => { const command: CheckInputCommand = { name: name, value: commandValue, callarIndex: index, additionalValue: additionalValue, }; setCommand(command); }, [index, setCommand], ); /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { if (!str) { setChar(""); sendCommand("delete"); return; } if (!verifyCharIsChinese(str)) { return; } let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand], ); /** 自动移动字符 */ useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); if (value) { setInputValue(value); } } }, [focus, setInputValue, value]); /** 处理输入 */ const TextInputHandler = (inputChar: string) => { setInputValue(inputChar); }; /** 监听键盘事件,处理退格事件 */ const backSpaceHandler = (keyName: string) => { if (keyName === "Backspace" && !char) { sendCommand("back"); } }; return ( <TextInput style={TuneStyle} value={char} onChange={e => { TextInputHandler(e.nativeEvent.text); }} onKeyPress={e => { backSpaceHandler(e.nativeEvent.key); }} ref={inputRef} /> ); } export default InputCheck; ``` types ```ts // 定义命令格式 interface BaseCommand { name: string; value?: unknown; } interface CheckInputCommand extends BaseCommand { value?: string | undefined; additionalValue?: string; callarIndex: number; } ```