生产故障处理SOP分享

一、背景 在日常的需求变更和技术变更中,测试用例覆盖率很难达到100%,再加上变更过程中的各种原因,可能会导致生产环境出现故障。 针对生产故障处理,每位开发同学可能都会有不同的处理方式,如果处理方式得当,故障能够快速止血顺利恢复,反之可能会错上加错!!! 基于以往经验,在这里推荐一套通用的生产故障处理SOP,他可能无法帮你快速定位问题,但是可以尽可能缩短恢复时间,降低故障影响! 二、参考流程 阶段一:快速处理(故障发现–>故障止血)【1-5-15】 步骤内容说明故障发现时间注意点 步骤1拉故障处理群、拉电话会议先拉自己的直属Leader、运维人员1min内【第一时间执行!】,避免信息差! 步骤2识别故障类型、影响范围执行预案、快速恢复(兜底、降级、灾备等等)5min内【无法快速定位时】,直接进行步骤3 步骤3梳理相关变更项(自身+上游)执行变更回滚、故障止血恢复10min内【无法恢复故障时】,直接进行步骤4 步骤4联系业务、运营进行业务恢复通过产品侧、运营侧的业务手段止血15min内 阶段二:排查恢复(根因定位–>故障恢复) 步骤内容说明故障发现时间注意点 步骤5定位问题,根因排查切勿在【阶段一】埋头查原因 步骤6修复验证,故障恢复做好验证再执行 阶段三:总结复盘(故障总结–>故障复盘) 步骤内容说明故障发现时间注意点 步骤7梳理时间线、原因和处理方案等 步骤8故障复盘,总结经验教训 三、总结 以上是基于 快速恢复快速止血 为宗旨的故障处理SOP,不同的业务和团队可参考其中的处理流程,但需要注意以下几点: 信息同步:第一时间拉群同步信息,避免信息差 很多时候,很多简单的问题都是因为信息差,开发盲目排查,导致故障升级,酿成大错!!! 快速止血:率先考虑应急预案、降级、开关逻辑等止血操作,切记勿要闷头排查!!! 开发的通病:遇到问题总是想要定位问题,此时一定要转变思想,应该第一时间进行止血操作,减少资损 这里也需要加强日常开发中的稳定性意识,没有稳定性预案,可能当故障发生时只能通过回滚来恢复 故障复盘:故障无法100%避免,所以一定要做好故障复盘,避免同类问题二次踩坑! 吃一堑长一智,不怕犯错,但是不能犯同样的错 最后提一嘴,生产故障无法预知,无法避免,所以作为开发同学一定要 敬畏生产敬畏生产敬畏生产!!!

2022/9/8
articleCard.readMore

系统稳定性建设实践总结【转载】

本文转载自 架构精进之路 的博客:《系统稳定性建设实践总结》 2020年,注定是个不平凡的一年。疫情的蔓延打乱了大家既定的原有的计划,同时也催生了一些在线业务办理能力的应用诉求,作为技术同学,需要在短时间内快速支持建设系统能力并保障其运行系统稳定性。恰逢年终月份,正好梳理总结下自己的系统稳定性建设经验和思考。 开篇 在开始介绍服务稳定性之前,我们先聊一下SLA。SLA(service-level agreement,即 服务级别协议)也称服务等级协议,经常被用来衡量服务稳定性指标。通常被称作“几个9”,9越多代表服务全年可用时间越长服务也就越可靠,即停机时间越短。通常作为服务提供商与受服务用户之间具体达成承诺的服务指标——质量、可用性,责任。 3个9,即99.9%,全年可停服务时间:365 * 24 * 60 *(1-99.9%)= 525.6min 4个9,即99.99%,全年可停服务时间:365 * 24 * 60 *(1-99.99%)= 52.56min 5个9,即99.999%,全年可停服务时间:365 * 24 * 60 *(1-99.999%)= 5.256min 在严苛的服务级别协议背后,其实是一些列规范要求来进行保障。 一、系统稳定性建设是指什么? 关于系统稳定性是指什么这一问题,相信好多开发同学都会有自己的理解和认知,但可能会存在是否理解片面或者是否标准的疑惑,那到底有什么判定标准和划分边界呢? 我们不妨看下来自于维基百科的解释: 稳定性是数学或工程上的用语,判别一系统在有界的输入是否也产生有界的输出。 若是,称系统为稳定;若否,则称系统为不稳定。 简单理解,系统稳定性****本质上是系统的确定性应答****。 从另一个角度解释,服务稳定性建设就是如何保障系统能够满足SLA所要求的服务等级协议。 二、为什么需要系统稳定性建设? 可以确定的一点,服务稳定性建设是非常必要的,不管是满足日常系统正常运行还是重大节庆活动的稳定有序运营。 我们来看几个由于服务稳定性故障造成影响的案例: 1)2020年国庆前一天,受“2020年最难打车日”的需求影响,滴滴平台和嘀嗒平台相继出现宕机故障; 2)2018年亚马逊prime day:亚马逊会员日故障(顾客无法将商品添加到购物车结账),导致公司损失高达9900万美元。 3)2015年由于中国工商银行部分地区因计算机系统升级,造成柜面和电子渠道业务办理缓慢,甚至不能受理业务; 4)2012年12306铁路订票网站因机房空调系统故障,导致暂停互联网售票、退票、改签业务。 服务稳定性对于企业来说非常重要,不仅仅会对企业带来直接的经济损失,甚至会对行业、人们的生活造成非常严重的影响。所以说服务稳定性建设的意义非常重大。 三、系统稳定性建设为什么难? 关于稳定性以及如何提升稳定性指标,我们可以想到很多的优化项: 1 eg. 加服务器、扩容、超时重试、服务降级、资源隔离&备份、代码逻辑优化、异步事件化... 那系统稳定性建设的主要难点是什么呢? 3.1 面对的挑战比较大 流量未知 尤其对于一个新改革上线的新业务而言,系统稳定性建设主要是流量洪峰的是个未知数,由于没有经验可以参考,我不确定是百万级别还是千万级别,还是更高级别? 改动量大 往往这种系统稳定性建设需要考虑需求主要是短时间内支持XX能力的上线,这其中往往涉及系统层面从下到上的多处变更,包括底层数据结构调整、业务逻辑改造以及用户交互方式的优化等等。时间短,改动大,质量难以保证。 不确定性 软件工程往往被用来描述“研究用工程化方法构建和维护有效的、实用的和高质量的软件”。其包括软件建设的方方面面,凡事事无巨细,任何细微的疏忽都可能造成全盘故障问题,不确定性问题尤其严重。 3.2 系统稳定性建设是一个系统性的大工程 多环节分工精细复杂,不容一点疏忽。 从系统构成来看,可以区分为单服务系统稳定性和多服务集群稳定性。 单服务稳定性 主要包括:功能配置可控、缓存加速(利器) 、服务隔离(第三方)、场景异常兜底方案、服务监控与及时响应等等 集群稳定性 主要包括:合理的系统架构、优秀的集群部署、科学的熔断限流、压测机制、精细的监控体系等等 四、系统稳定性建设如何入手? 4.1 系统稳定性建设前提 在提出系统稳定性建设解决方案之前,我们需要明确一下前提条件: 业务熟悉 需要对业务全貌流程熟悉,具备较强的掌控力; 架构明确 需要对系统技术架构熟知并具有一定的实操经验。 只有这样,对业务、架构都具备掌控能力之后,才谈得上去做稳定性建设的拆解和优化,才有基本的保障。 4.2 流程划分 一般情况下,我们提到系统稳定性建设,更像将系统稳定性作为一个专项Topic来搞,从其运行流程来看,主要存在以下几个方面: 前提 目标明确(基准) 事前 请求链路优化、服务性能优化&压测、应急预案制定、故障演练 事中 故障监控、定位问题、故障止损、问题修复 事后 故障复盘、整改优化、经验总结沉淀 服务稳定性建设其实是一个系统性的大工程,包括了方方面面。 五、系统稳定性建设的关键动作 从上一Part工作拆解来看,稳定性建设囊括的点比较多,而且杂。更多情况下,我们会做服务稳定性专项,针对某些特定场景下的特定问题而梳理出对应的方案。 那我们可以以小见大,从单服务系统本身出发,提炼看看存在哪些稳定性建设的关键点。其实只有每个单服务环节都稳定可靠,那集群系统乃至整个工程系统的稳定性才有保障。 假如系统面对突增的请求流量情况下,如何做好服务稳定性建设呢? 稳定性建设关键动作拆分如下几类: 5.1 削峰限流 例如,经典的秒杀场景,春节的火车票抢购、电商平台的双11秒杀等等,都是短时间上亿的用户涌入,瞬间流量巨大(高并发)。 不管前期对服务器资源做了如何的扩容,都会存在一个处理上限,所以一定要进行必要的削峰限流策略,类似于城市早晚高峰错峰限行的解决方案。同样,秒杀场景也需要类似的解决方案。 那具体如何来实现呢? 利用消息队列来削峰 消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。 消息队列就像“水库”一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。 利用挡板过滤无效请求 流量挡板过滤,主要是建立一种验证机制过滤掉无效请求,保障核心服务避免受更多外界无效请求的影响。比较常用的方案就是“布隆过滤器”。 产品策略的调整 产品策略调整是一种特别有效的手段,效果甚至会优于技术层面的改进优化。 例如:利用排队策略,有效打散高并发请求;调整活动宣传时间分散点,避免同一时刻出现高并发请求… 5.2 缓存加速 缓存是解决并发的利器,可以有效的提高系统的吞吐量。按照业务以及技术的纬度必要时可以增加多级缓存来保证其命中率。 主要应用思路:在数据库与服务端之间利用 Redis 做缓存服务,减少请求直接冲击到数据库。 5.3 异步化处理 与异步对应的就是同步,即所有事情排队一件件的有序进行,等上件事情完成后才会去做下一件事情。有点像一根签子串起来的糖葫芦。需要实时处理并响应,一旦超过时间会结束会话,在该过程中调用方一直在等待响应方处理完成并返回。 异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程。 需要强调一点:异步是一种设计理念,异步操作不等于多线程,常见的消息中间件、发布订阅的广播模式等,都可以实现异步处理的方式。 六、稳定性建设过程中的一些经验 6.1 做好压测 提前做好系统压测,做到心中有数,防患于未然,压力预估要切合实际,不要盲目过大。对于性能瓶颈点,尽量提前做好改进优化或者重点关注布防 6.2 应急预案必备 应急预案一定要有,研发人员往往比较自信,这是好事也是坏事,我们需要做最坏的打算。因为经验再丰富的工程师,也无法穷举未来可能发生的意外事件,而故障往往出现在预案之外的地方(墨菲定律)。 6.3 完善监控体系 建立完善的监控、告警机制,尽量让我们第一时间发现问题点,保障报错及时感知。在监控点的设置上,主要原则是:所有的依赖都是不可信的! 6.4 快速响应能力 类似于在行驶的飞机上换引擎,过程中无论发生什么样的故障,立即要动用一切力量“快速”止损。服务要有等级划分,保障抓大放小,保护核心服务原则,如确实存在不能快速定位问题时,可逐层降级。主要目标:防止问题扩大,故障止损,快速恢复。 总结 稳定性建设关键点 削峰限流 面对资源上限,做技术、业务层面的处理,达到流量削峰保障服务稳定性; 缓存加速 利用缓存解决并发,有效提升系统的吞吐量,同时需注意避免热Key、大Key问题; 异步化处理(同步->异步),有效提升响应效率,保障数据的最终一致性。 技术服务于业务 技术还是要解决实际问题来落地。应用场景很关键,所有的优化工作不要单纯为了技术而技术,技术归根结底还是为应用场景和产业落地服务。 可以尝试将业务视角目标做为最终目标,通过一切技术手段来保障目标的达成,从而实现技术价值最大化。 不拘泥于形式,灵活运用 稳定性方案需要视场景而灵活调整应用,切忌生搬硬套。在具体实现过程中,关键要把控主要行动路径,多条路径情况下选取投入产出比最高的那一条。推进一个行动路径:问题驱动(问题感知->问题分析->问题控制->问题解决)。

2022/8/1
articleCard.readMore

valine访问leancloud国际版异常,评论失效修复

起因 太久没维护博客了,最近发现Valine评论都展示不出来,看了下console发现是leancloud访问出了问题 查了下前因后果,大概就是LeanCloud对部分域名不再进行维护了,如果继续使用老的域名去拉取评论数据必然失败。 这里和大家同步下我的环境 leancloud国际版 报错域名:us-api.leancloud.cn 调整方案如下 获取新域名 登录leancloud后台 查询自己的APPID 替换https://你的appid前8位.api.lncldglobal.com获得新域名 修改valine代码 主题配置文件中的valine配置增加配置: severURLs(私有leancloud域名) 修改主题中valine对应的js源码:加载私有域名 更新av-min.js文件:确保私有域名可生效 示例 不同的主题可能涉及到的代码位置不同,但是调整思路类似,这里我贴下我的主题配置和涉及到调整的代码片段 主题配置文件config.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 valine: enable: true # if you want use valine,please set this value is ture appId: 12345678 # leancloud application app id appKey: 1234123123123 # leancloud application app key notify: false # valine mail notify (true/false) https://github.com/xCss/Valine/wiki verify: false # valine verify code (true/false) pageSize: 10 # comment list page size avatar: monsterid # gravatar style https://valine.js.org/#/avatar lang: zh-cn # i18n: zh-cn/en/tw placeholder: 📢📢📢留下邮箱可以收到回复提醒哦~ guest_info: nick,mail,link #valine comment header inf serverURLs: https://12345678.api.lncldglobal.com #替换为你的私有域名 valine对应的js源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 //更新av-min.js <script src="//code.bdstatic.com/npm/leancloud-storage@4.12.0/dist/av-min.js"></script> <script src="//unpkg.com/valine/dist/Valine.min.js"></script> <script> var GUEST_INFO = ['nick','mail','link']; var guest_info = '<%= theme.valine.guest_info %>'.split(',').filter(function(item){ return GUEST_INFO.indexOf(item) > -1 }); var notify = '<%= theme.valine.notify %>' == true; var verify = '<%= theme.valine.verify %>' == true; var valine = new Valine(); valine.init({ el: '#vcomment', notify: notify, verify: verify, appId: "<%= theme.valine.appId %>", appKey: "<%= theme.valine.appKey %>", placeholder: "<%= theme.valine.placeholder %>", pageSize:'<%= theme.valine.pageSize %>', avatar:'<%= theme.valine.avatar %>', lang:'<%= theme.valine.lang %>', //增加serverURLs serverURLs:'<%= theme.valine.serverURLs %>' }) </script> 测试 本地构建启动之后可能会因为不在leancloud白名单内,返回403,不过不要紧说明已经生效 直接hexo d发布就能生效了

2022/1/11
articleCard.readMore

匆匆忙忙的2021

碎碎念 感觉像被按下了快进键一样,2021年无论身边的人或事都转瞬即逝… 不知不觉又过了一年,老了一岁,自己也逐渐从一个刚毕业的懵懂少年,变成了现在职场上的老油条~ 时间真的是个奇妙的东西,时间是毒药也是解药、时间是让人猝不及防的,2021年的时间现在回想就是像是突然给丢了一样,一起丢的还有很多老朋友、很多自己以前的想法… 记忆中的2021就好像只有前半年,后半年基本上都是一个基调。话虽如此但这一年还是学到了很多新的东西,遇到了很多值得的人。 可能我写的像是流水账 年度关键字 心态 得失 遗憾 贵人 匆匆忙忙 新年伊始 牛年是第一次在外过年,那时候疫情紧张,杭州提倡就地过年,很多同事都响应政府号召,当然我也不例外。因为就地过年政府给发红包呀! 欢欢喜喜过大年的同时当然也在面试看机会,种种原因吧,拿到了叮咚的offer之后便决定过去了,于是开始了2021年的第一次搬家 上海 杭州是个很不错的城市,在杭州呆了三年,突然要离开,追求新生活新工作的我当时其实并没有什么感觉,于是开始了浩浩荡荡的跨省搬家操作。 有时候也挺佩服自己,在杭州有一起拼搏(摸鱼)三载的小伙伴们,而上海…那也没关系,谁还没年轻过,没必要和钱过意不去吧,舒适圈呆久了,就想去经历经历互联网的毒、打体验体验奋斗B的生活。也可能从那开始我就自动离队了吧。 围城 每个人都会经历这个阶段,看见一座山,就想知道山后面是什么。我很想告诉他,可能翻过去山后面,你会发觉没有什么特别,回头看会觉得这边更好。但是他不会相信,以他的性格,自己不试试是不会甘心 其实现在对这句话略有体会,当然人都是有好奇心的,也只有经历过这个阶段才会有不一样的体验 之前听到过一句话说的是进了大厂基本上就是失踪人口了,新公司对比我上一家公司可以算是大厂了,当然不能和BAT对比。但是失踪人口是我本人了。 新工作带给我更多的是心态的变化,从一开始的斗志昂扬,伴随着高强度的工作整个人已经疲惫不堪、工作和生活的节奏也彻底混乱,说实话那段时间天天都在离职的边缘徘徊,工作和生活无法平衡让我不得已要在二者之间做出选择。得失得失,有失才有得,你想要拿高薪总得拿点东西来换。 强行被投喂了大量工作的同时疯狂吸收了大量的新知识,也在那段时间遇到了让自己受益良多的职场贵人。 稳定性 记得刚入职时,每个月基本都会听到有某某同学某某团队出现了P级故障,作为新人那时候还没什么感觉。但是故障的频发本身就不正常。 在当时,团队服务的稳定性预案基本聊聊无几,可以说稍有不慎就喜提大礼包。 在BOSS的牵头下,开始着手稳定性建设,当然我也是新兵上阵,头一次干这个,但起码没吃过猪肉见过猪跑。基于团队服务的特殊性,截止目前下游依赖至少40+,主要从几个方面入手 监控大盘 故障告警 灾备数据 超时控制 降级方案 故障演练 目前基本可以做到弱依赖故障无需人工干预,降级预案覆盖90%场景,截止12.31号,没有喜提P级故障,当然这也是一直抓稳定性的一部分成果 提升 大公司就是如此,不像小公司一个人就可以接触到整个流程,你可以有精力去钻研你感兴趣的内容。大公司好比一个精密机械,它可以被拆分到很小的模块,而每一个人在里面都只是不知疲倦的一颗齿轮。 这一年技术能力上基本上原地踏步,更多的是软技能的提升。 所在团队的特殊性,向上与用户直接对接,向下需要统筹所有依赖方。需要强沟通能力。所以日常的工作基本上就是沟通、会议、方案、业务…真正落地开发其实很少,更多的是系统稳定性方案和保障上。 小半年下来若说提升可能就在四方面 系统稳定性 团队沟通 项目管理 方案设计 现在一想技术提升基本为0,当然整年投入精力最多的就是在稳定性上 博客 失踪人口今年博客的产出为0,这个羞耻的成绩实在是难以启齿 工作之外的flag 保持身体健康 继续技术提升 稳定博客分享 2022 希望疫情早日结束,希望自己能够不忘初心,希望远方的朋友都能心想事成,希望梦想成真~加油!

2022/1/2
articleCard.readMore

聊一下换工作

近半年博客都没怎么更新和维护,一方面确实是忙,另一方面就是一直在为找工作奔波。 终于工作也尘埃落定,马上也要入职,最近在处理工作交接的事情,就写一篇文章来记录下人生中第一次换工作的经历吧。 首先这次工作是从杭州换到了上海,新工作解决了一些个人问题,薪资也达到了预期,新的开始祝自己一切顺利! 我是从大三校招就进了老东家开始程序生涯、毕业就直接拿到了提前转正,说实话,老东家确实挺好的,无论是工作氛围、领导、同事都是无可挑剔的,我在这里生活了三年,和大家都很熟,这里就好像是我的舒适区,拿着够花的工资,过着朝九晚五的生活,周末和同事朋友约饭、游山玩水。在杭州这样的城市真的可以说是美滋滋,当然了前提是你没有外部压力(诸如房子、车子、等等)。 老东家是杭州的一家物联网公司,如果有需要内推的可直接发我邮箱。 跳槽、换工作在互联网公司实在是太普遍了,三年间送走了一批又一批,我所在的小组从我入职到现在,除了我之前的人已经全换了一批。以前都是我受邀参加同事的散伙饭,终于今天也到老同事们被我邀请,轮到他们送我,一伙人坐到桌前,仿佛有种不真实的感觉,一起聊着这几年的事,就仿佛都还是昨天… 我差不多是从去年12月份开始陆陆续续投起了简历,然后截止到年前2月初陆陆续续面了大小共6家公司 其中有运气也有自己的因素,拿到了5份offer,最终在年后开工后确定了入职公司。 这也算是参加工作后的第一次换工作,一路上磕磕绊绊总算有了定论。 扯了这么多,还是和大家分享下找工作需要的注意事项 时间 选择一个适合的时间段来执行你的计划是非常重要,都说金三银四、金九银十是跳槽的最佳时间,还是有一定道理的,每年三月份左右企业都过完新年刚开工,年前制定的招聘计划正是开展的时候,我就是在年底这个尴尬的时间点开始的,春招吧有点早,秋招吧有点晚,但是如果你准备好了,其实什么时候找工作都可以,如果刚好赶上金三银四、金九银十岗位的选择机会会更多一些。毕竟开发面试还是得看技术。 渠道 既然要找工作了,渠道很重要,如何从岗位海洋里找到和你契合度高的岗位,并且如何高效的送达简历,其实都是至关重要。 招聘App:我主要是在BOSS直聘和前程无忧两个APP上,其他的没有使用过也就不做评论 内推:确定了目标公司或者意向岗位,先发动下你的小伙伴们,看能不能内推,如果不行可以发动互联网资源,像牛客、知乎、Ve2x,甚至github也有一些内推渠道 猎头:寻找一名优秀的猎头,提出你的需求,交给他来帮你物色,但前提是你们俩要相互信任、并且信息对等且真实,不然工作谈好了最终因为你提供的相关信息与实际不符(比如学历、当前薪资情况等等)导致翻车。 准备 简历投出去了,就预示着你随时会收到面试邀请,可万万不能等收到面试再准备复习,到那个时候只能是临时抱佛脚,很可能被佛踢一脚!! 夯实基础:基础不牢,地动山摇。面试过程中基础知识的考察还是占一定比重的,很多一面基本都是基础考察,所以基础是你能否二面的关键 梳理项目:根据你的简历,梳理你的项目,主要从项目架构(为什么这样设计)、核心功能逻辑(流程熟悉)、遇到的困难这几块来准备 技能自查:简历中一般都会列举自己掌握的技术能力,从熟悉到了解,既然你写上去了,那就要做到完全的准备,随时迎接面试官的连环炮 时间管理:工作、复习、面试是一个漫长的过程,三者之间还是需要一个比较好的时间安排,本职工作还是需要同样重视,毕竟你还没离职。 面试 基本上现在的互联网面试方式就三种:电话面试、视频面试、现场面试 论效率的话现场面试效率最高,电话、视频面一般都只是一面、二面简单了解下。我因为是异地面试的原因,通常都会和对方商量,一共几面,可否当天全部安排。一站式的面试很考验人的精神状态。 面试礼仪:毕竟是面试,打工人骨子里的修养和礼貌还是要有的,电话、视频的沟通方式,需要的注意事项都要提前准备 了解面试:一定要了解下面试的整体流程,会有几面、大概多久会出结果。一方面做到心中有数,同时也能合理的安排其他时间 自信谦虚:去面试一定要自信,既然他已经通知你面试了,说明你还是很优秀的,但切记不能自信过头转而极度自负,还是要保持谦虚,切记不能夸夸其谈 其实面试就像是平时的技术分享一样,把你掌握的一些骚操作、知识点分享给面试官,在我个人的体会下,一场成功的面试就是两个技术人的经验交流,面试者发挥出了自己的所学也看到了自己的短板,面试官测出了对方的深度也发现了对方的闪光点。 复盘 第一场面试结束后,大概率你的心态已经发生了一丝变化,要么信心满满要么就是可能被虐了一顿,但是不论如何,面试后的复盘是尤为重要,技术面试中被问到的问题,哪些是你非常熟悉的,哪些是你印象模糊含含糊糊的,哪些又是你从来没接触过的。这些都需要进行复盘总结。 通过面试后的复盘,来查漏补缺,花时间补一补自己的薄弱点,用每一场面试来磨砺自己,直到你可以在面试中游刃有余,那说明你已经来感觉了。这也代表着你面试大概率要通过了。 抉择 无论你的预期是什么,当你在有可以选择的情况下一定要多方面多角度考虑和抉择 薪资待遇:出来打工为的就是赚钱,所以薪资待遇也是最关注的问题,是否达到预期,是否可以接受,五险一金缴纳细则、 技术氛围:对方的技术氛围如何,是不是让你去开荒(比如全公司就你一个写Java的),技术栈是否与当前的你匹配,如果就职对你的技术实力是否有所提升 个人发展:就职后对个人的发展如何,是否是高危暴雷行业,晋升规则方式如何 最后关于薪资多说两嘴: 时薪时薪时薪!!!!重要的事情说三遍! 有的朋友觉得加班无所谓只要钱管够、有的朋友觉得绝对不加班,加班的我就不去 但是无论加班还是不加班,我都建议你先计算一下时薪,福报型企业加班多自然到手的也多一些,正常型企业不加班但是薪资可能稍微低一点 但是并不是薪资低就不考虑,这个时候建议你算一下时薪,如果不加班的工作可以拿到和加班工作相近的时薪,那还真的需要你好好斟酌,毕竟双休、朝九晚五的生活也是很美的。 最后 唠唠叨叨扯了这么多,也是经历这次换工作后,把自己遇到的一些坑点和经验分享给大家。还是那句话,换工作可以,但是不要盲目的换。 你为什么换工作?你的新工作是否解决了你的困惑,达到了你的预期? 最后,还是祝自己也祝大家工作顺利~

2021/3/13
articleCard.readMore

IoT系列(2):WIFI设备常见配网方案介绍

前言 本文讨论目前市面上基于WIFI智能设备的配网方案,结合自身开发案例,对不同的配网方案进行对比介绍。 阅读本文你可以了解到如下几种配网方案: 一键配网 设备热点配网 零配 手机热点配网 设备配网说明 提到设备配网这一流程,通俗的理解就是让设备连上网,本文主要就WIFI智能设备的配网展开讨论,目前市面上常见的配网方案都绕不开以下几个步骤: WIFI设备拿到某一wifi的SSID和Password APP拿到WIFI设备的唯一编号 APP用户发起设备绑定请求 WIFI设备发起入网请求 下面我们针对不同的配网方案来注意分析器配网流程 一键配网 如果你近几年购买过一些智能灯具、智能插座等等WIFI设备,那么大概率他的配网方式就是一键配网 因为一键配网方案,用户操作简单,只需要录入wifi的ssid和password,即可等待设备完成配网。 正如此一键配网几乎是智能设备的通用标准,但是它最大的痛点就是成功率低,特别低!!! 下面一起来看下一键配网的实现原理: 手机提前连接至路由器wifi APP中输入ssid和密码点击配网,开始进行广播 WIFI智能设备抓取广播包,拿到wifi信息,连接至路由器 WIFI设备连接至路由器后,将自身唯一编号MAC进行局域网广播 手机APP收到设备广播的MAC编号,向服务器发起设备绑定 从步骤上来看,没有任何毛病,但是在实际的用户配网过程中会出现各种各样的问题,导致用户体验极差,配网成功率极低 路由器兼容性:部分型号的路由器不支持或者禁止发送广播包,直接导致配网永远无法成功,并且用户无法排查 手机兼容性:WIFI设备连接的频段和手机连接的频段不同,导致双方无法收发广播包,例如5G和2.4G频段 wifi同名:如果设备附近有多个同名的ssid信号,极有可能设备会无法连接到正确的路由器 等等一些稀奇古怪的问题 看似用户操作方便,并且使用率极高的配网方式,实际操作中有很苛刻的配网条件,这也是一键配网让人又爱又恨的地方 如果有新的WIFI智能设备项目,不建议选用一键配网方案! 设备热点配网 既然一键配网成功率这么低,那有没有成功率高的方案呢,当然是有的:设备热点配网 由于它出众的配网成功率,很快成为wifi设备配网的新宠,像米家的摄像头就采用的这种配网方式 一起来看看他的实现原理: WIFI设备进入AP模式,对外提供一个wifi热点 用户手机连接此wifi,然后通过APP将路由器的SSID和密码发送给WIFI设备 WIFI设备收到SSID信息后将唯一编号MAC发送给APP 手机APP收到MAC编号,向服务器发起设备绑定【预绑定】 设备连接路由器联网,向服务器发起入网【激活绑定】 设备热点配网时首先由设备AP模式,手机STA模式,去连接到设备热点上,进行数据传输 整个过程不需要通过路由器广播数据,所以不存在路由器兼容性,也不存在信号频段问题 唯一的风险点就是用户通过APP输入SSID和密码错误,导致设备无法联网。 针对这一风险点,在绑定流程上设计了预绑定和激活绑定: app携带用户id和设备mac发起预绑定,如果设备正常联网上线,那么绑定生效,设备激活;如果设备拿到了错误的ssid信息一定时间内没有上线,那么清除预绑定记录。 设备热点配网相对于一键配网几乎没有任何额外的成本增加,在尽量不增加用户操作复杂度的前提下,极大的提高了配网成功率,这也是当下新的WIFI设备配网首选方案。 零配 零配,我最早在天猫精灵系列设备的配网方案中遇到过,这是一种特定场景的配网方案,大致思路是通过已经配网成功的设备(智能音箱)给新的设备进行配网,实现真正意义上的零配置配网。 现在大部分的智能音箱联动场景中都支持零配方案。 先看一下的的实现步骤: 前提:通过其他方式已经完成配网的智能设备(天猫精灵),与服务器连接正常,并存有路由器SSID信息 手动触发WIFI设备将自己MAC信息通过Sniffer报文发送到天猫精灵 天猫精灵收到设备MAC信息后,将本地保存的路由器SSID信息发送给WIFI设备 天猫精灵向服务器发起该设备的预绑定请求 WIFI设备连接路由器联网,并向服务器发起激活绑定请求 该方案需要有一台已经联网的智能设备,并且该设备保存了用户信息和路由器SSID信息,优化掉了用户手动输入SSID和密码的步骤,进一步简化了用户配网操作。 在实际使用中,用户开启WIFI设备后,只需要对天猫精灵说一句“找队友”即可完成配网,可以说用户的配网体验感很好。 手机热点配网 这种方案和设备热点配网方案比较相似,从名字能看出来,这种方案的热点是由手机提供。同样都是为了解决路由器兼容性而提出的解决方案。 这种方案在阿里IoT中被作为一键配网失败后的补救措施。当一键配网失败后,用户可以通过手机设置特定的wifi热点,设备连接到手机热点上后进行信息交互。 原理图如下: 流程基本上和设备热点方案类似,区别就是提供热点的是手机端 不过在实际应用中,使用率不是很高,一方面用户操作复杂度过高,可能用户完全不知道如何开启手机热点。另一方面能想到手机热点配网方案,肯定会采用设备热点配网方案了。 所以总的来说,该方案成功率相对较高,但是用户操作复杂度也随之增大,可以作为其他方案失败后的备选方案,但并不推荐使用,毕竟用户体验是第一位 总结 总结一下上面提到的四种方案的特点: 方案使用率成功率用户体验路由器兼容性频段兼容性手机兼容性使用场景 一键配网高低优差(不支持广播)差(2.4G/5G)差不推荐使用 设备热点配网高高优优优优WIFI配网首选方案 零配中高优(免输入SSID信息)优优优音箱联动场景推荐 手机热点配网低高差(手动开启热点)优优良不推荐使用 以上四种配网方案也是我目前工作中接触到的一些常用方案,为了方便理解,简化了各种方案的细节,实际通讯和交互流程会更为复杂。 当然除了这些,也有一些其他方案比如路由器热点配网方案、WEB配网方案等等,这些方案都因为需要特定场景和复杂流程等因素逐渐不被经常使用。

2021/1/15
articleCard.readMore

IoT系列(1):什么是物联网

前言 本文主要讨论物联网的相关概念知识,阅读本文你会有如下几点了解: 物联网概念引入 物联网与互联网的区别与联系 什么是物联网 物联网在我们生活中有哪些应用 物联网引子 如果一把伞可以感知当地天气并提醒主人今天是否应该带伞 如果某种可穿戴设备能够监测病人的健康状况并预测病情是否恶化以便及时准确地通知医生 如果汽车上的计算和预测分析系统能够提醒用户保养计划以避免突如其来的部件故障,我们的生活将会如何? 如今的物联网解决方案已经能够轻松实现上面的设想。我们的生活也在逐渐走向万物互联。 先说说当你听到物联网(Internet of Things),你想到了什么?有没有下面这些: 互联网、IoT、传感器、智能家居、智能空调、智能手机、智能酒店、车联网… 物联网从字面看蕴含着物物相联的意思,从我们身边的物联网产品来看,它具备着将设备与设备相互连接,人与设备连接的能力。 物联网与互联网 物联网和互联网在很多人的理解中可能觉得都差不多、都可以上网之类的。但实际上他们两者可以说是完全不同的两个场景。 在互联网时代,最初是PC电脑实现人与人之间的沟通变得越来越简单,而后手机作为一个媒介打开了移动互联网的热潮。无论手机还是电脑,都是为了实现人与人之间高效连接 其中,人是消费者也是生产者,手机或者电脑是作为传输媒介进行信息传输 上面是在互联网中的模型,而在物联网中则是另外一中场景,举个栗子: 你购买了一个智能灯,智能灯可以通过手机app进行wifi配网后连接到网络,用户可以通过app控制设备,设备的状态会实时的通知到app,用户可以通过app检查设备是否正常。 以上是一个典型的物联网设备使用场景,在这个场景中涉及到了3个设备:智能灯、手机、路由器。他们搭配在一起,实现了一个设备与设备连接,设备与人连接的场景 与互联网中不同,设备的参与度更高,设备不仅仅进行消息的传输,他也是消息的生产者和消费者。 通过以上的对比,有一个最显著的感受就是物联网中,设备的参与度更高,更倾向于设备与设备的连接互通。互联网中更着重人与人之间的互联。 当然物联网与互联网也不是完全分割的,可以理解为随着互联网的发展物联网应运而生,物联网是互联网的增强和延伸。 什么是物联网? 上面引入了物联网,也将其与互联网做了对比,那么到底什么是物联网呢? 物联网(IoT,Internet of Things)在互联网的基础上,将用户端延伸和扩展到物与物、物与人的连接。物联网模式中,所有物品都可以与网络连接,并进行通信和场景联动。 物联网是互联网的延伸。互联网通过电脑、移动终端等设备将参与者联系起来,形成的一种全新的信息互换方式。而物联网则是通过传感器、芯片、无线模组使设备联网,进而进行信息互换,实现物物人相联。 物联网三层架构 物联网从整个体系结构来看,可以分为三个层面: 设备层(Device):负责数据采集的各种智能硬件设备,比如传感器设备,控制器等。 网络层(Connect):负责可靠传递,通过将物体接入网络,依托通信技术和通信协议,实现可信的信息交互和共享。通信技术例如NB、LoRa、WIFI,通信协议例如HTTP、TCP、UDP、MQTT、AMQP等 应用层(Manage):负责智能处理,分析和处理海量的感知数据和信息,实现智能化的决策和控制。就是实现具体业务逻辑的地方。 此处引用一张《物联网开发实战》中的图例: 物联网在我们生活中的应用 目前我们普通人对于物联网接触最多的应该就是智能家居了,像家里的空调、冰箱、窗帘、灯具等等 但这仅仅是物联网在智能家居板块的体现,如果按行业划分,主要体现在如下几块 智慧物流:例如菜鸟物流实验室智能搬运、分拣机器人,顺丰的数据灯塔让物流过程可视化。 智能交通:比如电动车厂商推进车联网、美团的共享自行车、共享电动车,gofun的共享汽车等 精准农业:通过物联网相关技术进行农作物长势、自然条件的检测,比如电信推出的山洪预警系统,还有像最近比较火的智慧养猪等等项目 智慧医疗:比如像今年的健康码,比如通过可穿戴设备检测人体器官信息 智慧家居:像小米生态链、智能酒店、智慧安防等等。 总结 如今在通信、互联网、嵌入式等技术的推动下,物联网正在逐渐走进我们的生活、互联网时代下,人与人的距离变小了,而继互联网之后物联网时代则是缩短物与物、物与人之间的距离。

2021/1/14
articleCard.readMore

Java8特性2 - StreamApi

Stream Stream API 关注对数据的运算,属于CPU密集型 Collections 关注对数据的存储,属于IO密集型 Stream 自己本身不存储元素 Stream 不会改变元对象,但是他会返回一个持有结果的新Stream Stream 操作是延时执行的,意味着需要结果时才执行 Stream执行流程 1 执行流程: 实例化 ==> 中间操作 ==> 终止操作 中间操作往往是一个操作链 一旦终止操作,就开始执行中间操作链,并产生结果。【延时执行,终止操作触发执行】 准备数据 为了测试方便,这里写一个学生工具类StudentData.java,用来获取测试数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 public class StudentData { public static List<Student> getList(){ List<Student> list = new ArrayList<>(); list.add(new Student(1001,"张三",34,50000)); list.add(new Student(1002,"李四",19,3000)); list.add(new Student(1003,"王五",14,600)); list.add(new Student(1004,"赵六",42,30000)); list.add(new Student(1005,"李明",22,6000)); list.add(new Student(1006,"张华",32,40000)); list.add(new Student(1007,"李华",30,9000)); list.add(new Student(1008,"王二",28,12000)); return list; } } class Student{ private Integer id; private String name; private Integer age; private float salary; public Student(Integer id, String name, Integer age, float salary) { this.id = id; this.name = name; this.age = age; this.salary = salary; } public Integer getId() { return id; } public Student setId(Integer id) { this.id = id; return this; } public String getName() { return name; } public Student setName(String name) { this.name = name; return this; } public Integer getAge() { return age; } public Student setAge(Integer age) { this.age = age; return this; } public float getSalary() { return salary; } public Student setSalary(float salary) { this.salary = salary; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return Float.compare(student.salary, salary) == 0 && Objects.equals(id, student.id) && Objects.equals(name, student.name) && Objects.equals(age, student.age); } @Override public int hashCode() { return Objects.hash(id, name, age, salary); } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", salary=" + salary + '}'; } } Stream实例化 通过集合 通过数组 Stream.of() Stream.iterate() 迭代创建 Stream.generate() 生成创建 通过集合创建流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 List<Student> list = StudentData.getList(); /** * 创建一个顺序流(按流的顺序进行中间操作) * default Stream<E> stream() * {@link Collection#stream()} */ Stream<Student> stream = list.stream(); /** * 创建一个并行流(并行进行中间操作,无顺序) * default Stream<E> parallelStream() * {@link Collection#parallelStream()} */ Stream<Student> studentStream = list.parallelStream(); 通过数组创建流 1 2 3 4 5 int[] arr = new int[]{1,2,3,4,5}; IntStream stream = Arrays.stream(arr); Student[] students = {new Student(1,"zhang3",15,2000),new Student(2,"li4",25,3000)}; Stream<Student> stream1 = Arrays.stream(students); 通过Stream.of(T t)创建流 1 Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5); 通过Stream.generate()生成流 1 2 //生成十个随机数 Stream.generate(Math::random).limit(10).forEach(System.out::println); 为了展示效果,这里用到了终止操作forEach(Consumer c),来打印生成的流,输出如下: 1 2 3 4 5 6 7 8 9 10 0.4110376914730558 0.3859646598602653 0.6615549365050744 0.5086477303367989 0.2614939389108638 0.4766495481509283 0.4378851389809656 0.018579677210072254 0.5217833438932207 0.44390638190496046 通过Stream.iterate()创建流 1 2 //创建前10个偶数 Stream.iterate(0,t->t+2).limit(10).forEach(System.out::println); 输出如下: 1 2 3 4 5 6 7 8 9 10 0 2 4 6 8 10 12 14 16 18 Stream中间操作 1、筛选与切片 filter、limit、skip、distinct 2、映射 map、mapToDouble、mapToInt、mapToLong、flatMap 3、排序 sorted()、sorted(Comparator c) 筛选与切片 筛选流filter 从流中筛选需要的元素 1 2 3 4 List<Student> list = StudentData.getList(); Stream<Student> stream = list.stream(); //filter 筛选出年龄大于40的学生 stream.filter(s -> s.getAge() > 40).forEach(System.out::println); 输出结果如下: 1 Student{id=1004, name='赵六', age=42, salary=30000.0} 截断流limit 从stream中获取指定大小的stream,可以类比sql中的LIMIT 1 2 //limit 截断流 list.stream().limit(4).forEach(System.out::println); 输出如下: 1 2 3 Student{id=1001, name='张三', age=34, salary=50000.0} Student{id=1002, name='李四', age=19, salary=3000.0} Student{id=1003, name='王五', age=14, salary=600.0} 跳过元素skip 从stream中跳过指定个数后获取stream 1 2 //skip 跳过2个元素后截取1个元素 list.stream().skip(2).limit(1).forEach(System.out::println); 输出结果如下: 1 Student{id=1003, name='王五', age=14, salary=600.0} **注意:**当跳过的个数超过stream中元素个数,返回空流 1 2 /*此时的list.stream()中只有8个元素,直接跳过30个元素*/ list.stream().skip(30).forEach(System.out::println); 执行此代码无任何输出,因为此时生成的流为空 去重distinct 去重,根据stream中元素自己的hashcode()和equals()进行判断,效果可以类比sql中的DISTINCT 1 2 3 4 //distinct 可以看到list中有两个Tony老师,出现重复 list.add(new Student(1009, "Tony", 18, 50000)); list.add(new Student(1009, "Tony", 18, 50000)); list.stream().distinct().forEach(System.out::println); 输出结果 1 2 3 4 5 6 7 8 9 Student{id=1001, name='张三', age=34, salary=50000.0} Student{id=1002, name='李四', age=19, salary=3000.0} Student{id=1003, name='王五', age=14, salary=600.0} Student{id=1004, name='赵六', age=42, salary=30000.0} Student{id=1005, name='李明', age=22, salary=6000.0} Student{id=1006, name='张华', age=32, salary=40000.0} Student{id=1007, name='李华', age=30, salary=9000.0} Student{id=1008, name='王二', age=28, salary=12000.0} Student{id=1009, name='Tony', age=18, salary=50000.0} 去重后仅保留一个Tony对象 映射 映射就是 a -> b 的过程,比如把水放进冰箱一段时间就会变成冰块,把水果放进榨汁机榨汁就会变成果汁等等,这些都是映射,只不过他们的映射规则不同。 在Stream中映射有两种 map(Function f) flatMap(Function f) map(Function f) 1 2 将一个流中元素转换成其他形式,或者提取其中信息,最终产生一个新的流 其中这个Function就是映射规则,该函数会被应用到流中每一个元素上,并将其映射成一个新的元素 举个例子说明下: **信息提取:**提取流中前3个元素的姓名属性, 映射成新的元素,最终生成一个新的流, 为了好理解,这里用了终止操作forEach(),并将Stream实例化、中间操作、终止操作分开写。 1 2 3 4 //Stream<Student> --------> Stream<String> Stream<Student> stream = list.stream(); Stream<String> limit = stream.map(Student::getName).limit(3); limit.forEach(System.out::println); 输出信息为: 1 2 3 张三 李四 王五 可以看到经过了map,原本的Student流最终映射为String流 再举个例子: **格式转换:**截取流中前三个元素的姓名和年龄,产生一个新的字符串,格式为姓名:年龄,以次产生一个新的流 1 2 3 4 //Stream<Student> --------> Stream<String> Stream<Student> stream1 = list.stream(); Stream<String> limit1 = stream1.map(student -> student.getName() + " : " + student.getAge()).limit(3); limit1.forEach(System.out::println); 打印结果如下: 1 2 3 张三 : 34 李四 : 19 王五 : 14 可以看到了,通过map,将Student流映射成指定格式的String流 FlatMap(Function f) 1 接收一个函数作为映射规则,该函数把流中的每个元素都转换成一个新的流,最后再把这些流连接成一个流 说人话就是,flatmap会把每一个元素都映射成一个流,最终把多个流整合成一个流 举个例子 将这个字符串数组创建的Stream中的每个元素都用,分割后生成一个流,最终整合为一个完整的流 1 2 String[] strings = {"a","b,c,d","A,B"}; Stream.of(strings).flatMap((s)->Stream.of(s.split(","))).forEach(System.out::println); 输出结果: 1 2 3 4 5 6 a b c d A B map与flatmap的区别 可能你觉得map与flatmap好像没啥区别,都是在映射,都是把一个流变成另一个流 但其实大有不同!!! 注意他们的参数Function map是对每个元素进行映射,把所有映射后元素转为一个新的流 flatmap是对每个元素进行映射后,每个元素都转变成一个流,最终把产生的多个流整合为一个流 先看两个Api的参数 1 2 3 4 5 //map <R> Stream<R> map(Function<? super T, ? extends R> mapper); //flatMap <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); 很明显flatMap的返回值R是Stream类型,这正对应了之前说的flatMap会把每个元素转换成Stream 如果把上一个例子中的flatmap换成map,试试会如何 1 2 String[] strings = {"a","b,c,d","A,B"}; Stream.of(strings).map((s)->Stream.of(s.split(","))).forEach(System.out::println); 输出结果: 1 2 3 java.util.stream.ReferencePipeline$Head@1963006a java.util.stream.ReferencePipeline$Head@7fbe847c java.util.stream.ReferencePipeline$Head@41975e01 打印了3个对象,说明使用了map之后,最终生成的流中的3个元素都是流,并没有像flatmap进行整合操作 简单的总结下就如下: 1 2 map: 1个流 ----> 1个流 flatmap: 1个流 ----> n个流 ----> 1个流 排序 Stream中间操作中有两种排序 sorted() sorted(Comparator c) sorted()自然排序 1 2 3 //sorted() 自然排序 IntStream sorted = Arrays.stream(new int[]{1, 20, 3, 99,11}).sorted(); sorted.forEach(System.out::println); 输出结果 1 2 3 4 5 1 3 11 20 99 sorted(Comparator c) 自定义排序,参数即为排序规则 1 2 3 4 5 //根据对象中的年龄属性排序 List<Student> list = StudentData.getList(); list.stream() .sorted((s1,s2)>Integer.compare(s1.getAge(),s2.getAge())) .limit(3).forEach(System.out::println); 输出结果 1 2 3 Student{id=1003, name='王五', age=14, salary=600.0} Student{id=1002, name='李四', age=19, salary=3000.0} Student{id=1005, name='李明', age=22, salary=6000.0}

2020/11/15
articleCard.readMore

Java8特性1 - lambda表达式&函数式接口

引子 要求创建一个线程,线程中输出hello world 没学Lambda前的画风: 1 2 3 4 5 6 7 8 //写法1 class myThread implements Runnable{ @Override public void run() { System.out.println("hello world!"); } } new Thread(new myThread()).start(); 1 2 3 4 5 6 7 //写法2 new Thread(new Runnable() { @Override public void run() { System.out.println("hello world!"); } }).start(); 当你学会Lambda表达式之后,画风是这样的: 1 new Thread(() -> System.out.println("hello world!")).start(); 是不是有种打开新世界大门的感觉,一起来看下Java8的新特性之一Lambda表达式吧 函数式接口:接口中只有一个抽象方法的接口 这种接口都可以用lambda表达式来实现 JDK内置四大基础函数式接口 Consumer#accept(Object) void accept(T t); 消费型接口,接受一个参数,无返回 Supplier#get() T get(); 供给型接口,无参数,但有返回值 Function#apply(Object) R apply(T t); 函数型接口,接收参数T,返回一个R Predicate#test(Object) boolean test(T t); 断定型接口,接收参数T,判断其是否满足某一约束,返回一个boolean值 从源码中可以看到这四个基础接口都有注解@FunctionalInterface,他们都有一个特点: 有且仅有一个抽象方法 下面通过以前的写法、Java8的lambda写法进行对比学习 Consumer 消费型接口,接收一个参数,但无返回值 其接口核心源码如下: 1 2 3 4 5 6 7 8 9 /** * 接受单个输入参数且不返回结果的操作 * @param <T>输入参数的类型 * @since 1.8 */ @FunctionalInterface public interface Consumer<T> { void accept(T t); } 写一个小示例: 1 2 3 4 5 6 7 8 9 10 11 12 //Java8以前的写法 Consumer<String> consumer1 = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }; consumer1.accept("接收1个参数,无返回值的函数式接口实现"); //Lambda表达式写法 Consumer<String> consumer2 = s -> System.out.println(s); consumer2.accept("接收1个参数,无返回值的lambda表达式"); Supplier 供给型接口,无参数,但有返回值 其接口核心源码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * 代表了结果的提供者 * @param <T> 该接口对外提供的返回结果类型 * @since 1.8 */ @FunctionalInterface public interface Supplier<T> { /** * 获取一个结果 * @return 返回结果 */ T get(); } 写一个小示例: 1 2 3 4 5 6 7 8 9 10 11 12 Supplier<Integer> supplier1 = new Supplier<Integer>() { @Override public Integer get() { return new Random().nextInt(100); } }; System.out.println("supplier接口原始写法获取随机数:" +supplier1.get()); System.out.println("----------------------"); Supplier<Integer> supplier2 = () -> new Random().nextInt(100); System.out.println("supplier接口Lambda表达式写法获取随机数:" +supplier2.get()); Function 函数型接口,接收参数T,返回一个R 其核心源码如下: 1 2 3 4 5 6 7 8 9 10 /** * 接收一个参数并返回一个结果的函数 * @param <T> 函数(方法)的参数类型 * @param <R> 函数(方法)的返回值类型 * @since 1.8 */ @FunctionalInterface public interface Function<T, R> { R apply(T t); } 新老用法对比例子 1 2 3 4 5 6 7 8 9 10 11 12 Function<Integer, String> function1 = new Function<Integer, String>() { @Override public String apply(Integer integer) { return "接收整型参数:" + integer; } }; System.out.println(function1.apply(100)); System.out.println("----------------------"); Function<Integer, String> function2 = i -> "接收整型参数:" + i; System.out.println(function2.apply(1000)); Predicate 断定型接口,接收参数T,判断其是否满足某一约束,返回一个boolean值 核心源码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * 接收参数T,判断其是否满足某一约束,返回一个boolean值 * @param <T> 方法参数类型 * @since 1.8 */ @FunctionalInterface public interface Predicate<T> { /** * 根据给定的参数进行自定义实现的判断,返回一个布尔值 * * @param t 输入参数 * @return {@code true} 判断通过,返回true * otherwise {@code false} 判断不通过,返回false */ boolean test(T t); 1 2 3 4 5 6 7 8 9 10 11 12 Predicate<Integer> predicate1 = new Predicate<Integer>() { @Override public boolean test(Integer integer) { return integer > 100; } }; System.out.println("大于100 ? " + predicate1.test(101)); System.out.println("----------------------"); Predicate<Integer> predicate2 = integer -> integer>100; System.out.println("大于100 ? " + predicate2.test(99));

2020/11/10
articleCard.readMore

设计模式(5)-适配器模式

定义 适配器模式:将一个类的接口转换为调用方希望的另一个接口,使得原本不兼容的接口变得可兼容共同工作 举一个生活中的例子来解释适配器模式如下: typeC的充电线不能给普通安卓机充电,因为接口不兼容,此时需要一个转接头适配器,typeC转安卓,即可实现给安卓手机充电 用直流电的电子设备在使用中都需要一个电源适配器将插座上的交流电转变为直流电 角色和分类 适配器模式种分为3大角色 目标接口:当前系统业务所期待的接口,抽象类或者接口 适配者类:要被适配的类,原本不兼容的类 适配器类:一个转换器,通过继承或引用适配者对象,把适配者接口转换成目标接口,使得客户按照目标接口的格式访问适配者 适配器模式分为3种: 类适配器模式 对象适配器模式 接口适配器模式 下面分别对他们进行介绍 类适配器模式 以手机充电器为例来介绍类适配器模式:充电器将220V交流电转换为5V直流电这一过程。其中的角色如下 输出5v电压:目标接口,兼容手机充电 电源插座:适配者类,要被适配,不适合手机直接充电 手机充电器:适配器类,将220V不可用的电压转换为手机可用的充电电压 简易的类图结构如下: 代码实现 目标接口,定义一个将220转换为5v的接口,作为一个标准,提供给各个厂商的电源适配器使用 1 2 3 public interface IOutput5V { int output5v(); } 被适配类,电源插座,提供220v的直流电,不能被手机直接使用 1 2 3 4 5 6 7 public class Output220V { public int output220(){ int src = 220; System.out.println("电源输出电压:220V"); return src; } } 适配器类,继承了被适配类,实现目标接口具体的适配转换逻辑 1 2 3 4 5 6 7 8 9 10 public class Adapter220To5 extends Output220V implements IOutput5V { @Override public int output5v() { int src = output220(); //适配电压 int dts = src / 44; System.out.println("充电器适配后电压:" + dts); return dts; } } 以上便完成了主要的角色实现,编写手机的充电方法进行测试 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Phone { public void charging(IOutput5V iOutput5V){ if (iOutput5V.output5v()==5){ System.out.println("电压5v,开始充电"); }else { System.err.println("电压不符,无法充电"); } } public static void main(String[] args) { new Phone().charging(new Adapter220To5()); } } 运行后输出结果如下: 1 2 3 电源输出电压:220V 充电器适配后电压:5V 电压5v,开始充电 类适配器模式说明 适配器的实现过程中是继承了被适配类同时实现目标接口的方式,这样的原因是受Java单继承的限制,所以在类适配器模式下算是一个小小的缺点,使用继承大大的增加了适配器的复杂度。 对象适配器模式 基本思路和类的适配器模式相同,只是将Adapter类作修改,不是继承待适配类,而是持有待适配类的实例,以解决兼容性的问题。 即:持有待适配类,实现目标接口,完成兼容性适配 依然按照上文的场景和角色只有适配器类的变动,类图关系如下 从原本的继承被适配类转变为持有被适配类的实例。涉及到代码修改的只有适配器类:Adapter220To5.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Adapter220To5 implements IOutput5V { private Output220V output220; public Adapter220To5 setOutput220(Output220V output220) { this.output220 = output220; return this; } @Override public int output5v() { int src = output220.output220(); //适配电压 int dts = src / 44; System.out.println("充电器适配后电压:" + dts); return dts; } } 适配器类在需要持有被适配类的实例,所以在Phone充电方法中传入被适配类的对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Phone { public void charging(IOutput5V iOutput5V){ if (iOutput5V.output5v()==5){ System.out.println("电压5v,开始充电"); }else { System.err.println("电压不符,无法充电"); } } public static void main(String[] args) { //传入持有被适配类实例的适配器类 new Phone().charging(new Adapter220To5().setOutput220(new Output220V())); } } 对象适配器模式说明 对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。 根据合成复用原则,使用组合替代继承,所以它解决了类适配器必须继承被适配类的局限性问题,也不再要求目标接口角色必须是接口。 接口适配器模式 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求 接口适配器模式适用于一个接口不想使用其所有的方法的情况 简单的类图结构如下: 代码实现 假定现在的目标接口定义了输出5v、输出20v、输出60v的方法 1 2 3 4 5 6 7 8 public interface IOutput { //转换为5v输出 int output5(); //转换为20v输出 int output20(); //转换为60v输出 int output60(); } 定义抽象类默认实现目标接口,并持有待适配的对象实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public abstract class AbsAdapter implements IOutput{ protected Output220V output220; public AbsAdapter(Output220V output220V) { this.output220 = output220V; } @Override public int output5() { return 0; } @Override public int output20() { return 0; } @Override public int output60() { return 0; } } 当需要使用到输出5v的转换时,或者需要使用输出10v转换时,使用匿名内部类的方式实现内部适配细节 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Phone { public void charging(IOutput iOutput){ if (iOutput.output5()==5){ System.out.println("电压5v,开始充电"); }else { System.err.println("电压不符,无法充电"); } } public static void main(String[] args) { AbsAdapter absAdapter = new AbsAdapter(new Output220V()) { @Override public int output5() { int src = output220.output220(); //适配电压 int dts = src / 44; System.out.println("充电器适配后电压:" + dts); return dts; } }; new Phone().charging(absAdapter); } } 输出结果: 1 2 3 电源输出电压:220V 充电器适配后电压:5 电压5v,开始充电 适配器模式总结 主要优点: 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。 适用场景: 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。 适配器模式在源码中的使用 SpringMvc中的HandlerAdapter就是典型的对象适配器模式 spring AOP中的适配器模式

2020/10/11
articleCard.readMore

设计模式(4)-建造者模式

概念 软件开发过程中,复杂对象的创建步骤繁杂,这些产品都是由多个部件构成的,各个部件可以灵活选择,但其创建步骤都大同小异。 复杂对象比如电脑、汽车、飞机、手机、冰箱… 这类产品的创建无法用前面介绍的工厂模式描述,只有建造者模式可以很好地描述该类产品的创建。 将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,被称为建造者模式 建造者模式又叫生成器模式,是一种对象构建模式。 模拟场景 组装台式电脑,不同的人选择的配置和价位都不同,而且组装电脑需要的零件很多过程十分复杂。 用代码实现不同用户组装不同价位和配置的电脑 传统实现方式 根据上述场景,主要有以下几个类 抽象电脑类 具体电脑类A、B 客户类 AbstractComputer.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public abstract class AbstractComputer { /**安装cpu*/ public abstract void cpu(); /**安装内存*/ public abstract void ram(); /**安装硬盘*/ public abstract void disk(); /**安装显卡*/ public abstract void graphics(); /**安装电源*/ public abstract void power(); public void build(){ this.cpu(); this.ram(); this.disk(); this.graphics(); this.power(); } } 普通配置的电脑: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class ComputerType1 extends AbstractComputer implements Serializable { @Override public void cpu() { System.out.println("安装普通cpu"); } @Override public void ram() { System.out.println("安装8g内存"); } @Override public void disk() { System.out.println("安装500g机械硬盘"); } @Override public void graphics() { System.out.println("安装低配显卡"); } @Override public void power() { System.out.println("安装普通电源"); } } 客户类需要该配置电脑时进行构建 1 2 3 4 5 6 public class Client { public static void main(String[] args) { ComputerType1 computerType1 = new ComputerType1(); computerType1.build(); } } 传统方式来实现电脑的组装流程是比较好理解的,但是电脑作为产品,组装过程与产品并没有完全解耦。 在设计模式中,有一种专门用于将产品与产品创建过程分析的方式,也叫做建造者模式 建造者模式 又叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。 他具有四个角色,分别为: Product(产品角色): 一个具体的产品对象。 Builder(抽象建造者): 创建一个Product对象的各个部件指定的 接口/抽象类。 ConcreteBuilder(具体建造者): 实现接口,构建和装配各个部件。 Director(指挥者): 构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。 它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。 模拟场景 电脑城买组装台式机。 从选机到下单到装机到提货流程繁多步骤复杂, 实际的流程是客户提出机器需求,装机店老板给出配置A和B两个套餐和价位,客户下单,老板根据配置单A/B指挥装机员A/B进行装机,装机员装好机器之后,由装机店老板转交客户 根据场景分析可得如下几个角色: 电脑:具体产品 装机店:抽象建造者 装机员A、B:具体建造者 装机店老板:指挥者 代码实现 产品角色Computer定义如下 1 2 3 4 5 6 7 8 9 10 public class Computer { private String cpu; private String ram; private String disk; private String graphics; private String power; //get、set、toString省略 } 抽象建造者,定义所有建造者的基础方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public abstract class ComputerBuilder { Computer computer = new Computer(); /**安装cpu*/ public abstract void cpu(); /**安装内存*/ public abstract void ram(); /**安装硬盘*/ public abstract void disk(); /**安装显卡*/ public abstract void graphics(); /**安装电源*/ public abstract void power(); public Computer build(){ this.cpu(); this.ram(); this.disk(); this.graphics(); this.power(); return computer; } } 具体的建造者角色,装机员A、B,此处以A为例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ComputerInstallerA extends ComputerBuilder { @Override public void cpu() {computer.setCpu("普通cpu");} @Override public void ram() {computer.setRam("4G内存");} @Override public void disk() {computer.setDisk("500G机械硬盘");} @Override public void graphics() {computer.setGraphics("集成显卡");} @Override public void power() {computer.setPower("普通电源");} } 指挥者角色,装机店老板,指挥装机员进行某一配置的装机 1 2 3 4 5 6 7 8 9 10 11 12 13 public class StoreBoss { ComputerBuilder builder = null; public StoreBoss setBuilder(ComputerBuilder builder) { this.builder = builder; return this; } public void builder(){ Computer computer = builder.build(); System.out.println(computer.toString()); } } 以上便是建造者模式的基础角色,编写客户类进行测试 1 2 3 4 5 6 7 8 9 10 11 public class Client { public static void main(String[] args) { StoreBoss boss = new StoreBoss(); //客户A需要一台价格便宜的电脑 System.out.println("客户A的电脑配置:"); boss.setBuilder(new ComputerInstallerA()).builder(); //客户B需要一台高配高性能电脑 System.out.println("客户B的电脑配置:"); boss.setBuilder(new ComputerInstallerB()).builder(); } } 客户只需要联系装机店老板,并告诉他自己的需求,即可进行按需装机 执行结果如下: 1 2 3 4 客户A的电脑配置: Computer{cpu='普通cpu', ram='4G内存', disk='500G机械硬盘', graphics='集成显卡', power='普通电源'} 客户B的电脑配置: Computer{cpu='高端cpu', ram='16G内存', disk='500G固态硬盘', graphics='GTX2080Ti显卡', power='金标550W电源'} 建造者模式与抽象工厂模式的比较 与抽象工厂模式相比,建造者模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族 。 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中,客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象 。 如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车

2020/9/29
articleCard.readMore

设计模式(3)-原型模式与浅拷贝和深拷贝

概念 在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象,会比较复杂且耗时耗资源, 用原型模式生成对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出很多孙悟空一样简单。 原型模式定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象 模拟需求 现在有一辆车,他的名字叫做哈啰单车,它的价格是2元/1小时,请编写程序创建多辆哈啰单车 简单分析后涉及到以下几个类: 车辆类 Vehicle.java 测试类 Client.java 传统方式 先来看下最容易理解的方式: 1 2 3 4 5 6 7 public class Vehicle { private String name; private double price; /**getter&setter&toString.......*/ } 1 2 3 4 5 6 7 8 9 10 public class Client { public static void main(String[] args) { Vehicle vehicle = new Vehicle("哈啰单车", 2.0); Vehicle vehicle1 = new Vehicle(vehicle.getName(),vehicle.getPrice()); Vehicle vehicle2 = new Vehicle(vehicle.getName(),vehicle.getPrice()); System.out.println(vehicle.hashCode()); System.out.println(vehicle1.hashCode()); System.out.println(vehicle2.hashCode()); } } 执行测试类代码可以看到创建了另外3个属性相同但引用完全不同的哈啰单车 执行结果如下: 1 2 3 1836019240 325040804 1173230247 原型模式 上面的对象复制方式是比较容易理解的,但是如果要复制很多对象时,每次都要get/set ,工作量必然很大 那有没有其他的复制方式吗?当然有了,设计模式中有一种原型模式的设计理念 原型模式的概念我们上文也有提到:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象 原型模式是一种创建型设计模式,创建方无需了解创建的细节,原型模式所涉及到的角色和类图如下 抽象原型类Prototype:抽象原型类,声明一个克隆自己的方法 具体的原型实现类ConcretePrototype:具体的原型类,实现克隆自己的方法 客户类Client:客户调用方,克隆对象 原型模式示例 对于模式场景中,要求复制多个不同的对象的需求,使用原型模式则有了新的解决方案如下 Java中Object类提供一个clone方法。该方法可以将一个Java对象复制一份。 如果某一个要使用clone方法,必须先实现Cloneable接口,Cloneable接口表示该类能够复制并且具有复制能力 涉及到的类和角色如下: 抽象原型类:Cloneable接口,声明了clone方法 具体原型类:Vehicle类,有自己的对象属性,并且实现了clone方法 Client:调用测试 只需要将上面的代码加以修改即可: 1 2 3 4 5 6 7 8 9 10 11 12 public class Vehicle implements Cloneable { private String name; private double price; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } /**getter&setter&toString.......*/ } 1 2 3 4 5 6 7 8 9 10 11 public class Client { public static void main(String[] args) throws CloneNotSupportedException { Vehicle vehicle = new Vehicle("哈啰单车", 2.0); //使用clone方法进行对象复制 Vehicle vehicle1 = (Vehicle) vehicle.clone(); Vehicle vehicle2 = (Vehicle) vehicle.clone(); System.out.println(vehicle.hashCode()); System.out.println(vehicle1.hashCode()); System.out.println(vehicle2.hashCode()); } } 依旧执行测试方法,来看看通过clone方法是否复制出了不同的对象 执行结果: 1 2 3 1836019240 325040804 1173230247 通过上面的一个小场景,对原型模式进行了简单的演示。但是上面的原型模式在一些特殊情况下可能就会出现问题 模拟需求2 现在有一车辆类,他有名称、单价、所属公司三个属性;所属公司对象包含了公司名称属性,请编写程序创建多辆美团单车,单价为2元/小时,所属公司为美团点评 简单分析后涉及到以下几个类: 车辆类 Vehicle.java 公司类 Company.java 测试类 Client.java 在上文原型模式的代码中加上Company类的代码和Vehicle类的公司属性后如下 1 2 3 4 5 public class Company { private String name; /**getter&setter&toString.......*/ } 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Vehicle implements Cloneable { private String name; private double price; private Company company; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } /**getter&setter&toString.......*/ } Client代码如下 1 2 3 4 5 6 7 8 9 10 11 public class Client { public static void main(String[] args) throws CloneNotSupportedException { Vehicle vehicle = new Vehicle("美团单车", 2.0, new Company("美团点评")); Vehicle vehicle1 = (Vehicle) vehicle.clone(); Vehicle vehicle2 = (Vehicle) vehicle.clone(); System.out.println("vehicle.hashCode(): "+vehicle.hashCode()+" company.hashCode(): "+ vehicle.getCompany().hashCode()); System.out.println("vehicle1.hashCode(): "+vehicle1.hashCode()+" company.hashCode(): "+ vehicle1.getCompany().hashCode()); System.out.println("vehicle2.hashCode(): "+vehicle2.hashCode()+" company.hashCode(): "+ vehicle2.getCompany().hashCode()); } } 观察克隆对象后的输出结果,你就会发现问题所在 1 2 3 vehicle.hashCode(): 1836019240 company.hashCode(): 325040804 vehicle1.hashCode(): 1173230247 company.hashCode(): 325040804 vehicle2.hashCode(): 856419764 company.hashCode(): 325040804 三个车辆对象的hashCode都不相同,说明有被成功克隆,但是其中的公司属性(对象类型)的hashCode并没有被同步克隆,内容中只有一份Company对象 相当于这次的克隆,内存中创建了三个不同车辆(Vehicle)对象,但是公司(Company)对象只有一个,被三个车辆对象所引用。 理论上,在创建了第一个车辆对象后,连续克隆两次后,内存中应该有3个车辆对象和3个公司对象。 这里就出现了原型模式中会存在的 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 浅拷贝&深拷贝 关于浅拷贝的描述: - 数据类型为基本类型的成员变量,在调用默认clone方法后,会进行浅拷贝,即将该属性的值复制一份给新的对象 - 数据类型为引用类型的成员变量,比如一个数组、一个对象,在调用默认的clone方法后,只会将成员变量的引用地址指向新的对象,而不会克隆新的成员变量对象 这种现象即为`浅拷贝`,上面的几个例子严格来说都属于浅拷贝,因为都没有去考虑成员变量为引用类型时的对象克隆 `深拷贝`自然是解决了浅拷贝的缺陷,对整个对象进行完全深度的对象复制,包括对象的引用类型和基本类型成员变量 # 深拷贝应用 针对模拟需求2,使用深拷贝的方式进行代码实现 实现思路:Company和Vehicle都实现Cloneable接口,重写Vehicle的clone方法 ```java public class Company implements Cloneable { private String name; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } /**getter&setter&toString.......*/ } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Vehicle implements Cloneable { private String name; private double price; private Company company; @Override protected Object clone() throws CloneNotSupportedException { Vehicle vehicle = (Vehicle) super.clone(); vehicle.setCompany((Company) this.company.clone()); return vehicle; } /**getter&setter&toString.......*/ } Client代码无需变动,执行Client进行测试,结果如下: 1 2 3 vehicle.hashCode(): 1836019240 company.hashCode(): 325040804 vehicle1.hashCode(): 1173230247 company.hashCode(): 856419764 vehicle2.hashCode(): 621009875 company.hashCode(): 1265094477 可以看到三个对象的对象成员属性明显都是不同的,说明做到了深拷贝 常见的原型模式的运用 Spring中配置bean的时候,scope属性可以配置一个prototype值,该值指定该bean的创建是使用原型模式 1 2 //示例: <bean id="userDaoImpl" scope="prototype" class="com.larscheng.www.dao.impl.UserDaoImpl"/> 当通过getBean方法获取bean时,可以看到源码中对于scope属性进行了处理 结语 当我们要创建新的对象过于复杂时,可以考虑使用原型模式来进行创建 使用原型模式时,需要考虑到浅拷贝和深拷贝

2020/9/5
articleCard.readMore

设计模式(2)-工厂模式图文介绍

工厂模式 工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。 模拟需求① 假设现有一个口罩生产工厂,可以生产防霾口罩、医用一次性口罩、N95口罩 客户可以通过口罩直营店根据自己的需求下单购买口罩 使用代码实现这一流程 传统实现方式 根据给出的需求,结合面向对象思想,大概有以下几个类 BaseMask 抽象口罩类 HazeMask 防霾口罩类 MedicalMask 医用口罩类 N95Mask N95口罩类 MaskStore 直营店类 Client 客户类 简单类图如下: 实现代码 HazeMask、MedicalMask、N95Mask继承自BaseMask,分别实现prepare方法,并调用setName方法设置name属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public abstract class BaseMask { protected String name; public abstract void prepare(); public void processing(){ System.out.println(name+"开始加工..."); } public void bale(){ System.out.println(name+"打包完成..."); } public void setName(String name) { this.name = name; } } MaskStore类,实现了口罩直营店根据用户需求进行下单的流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class MaskStore { public void order() { BaseMask mask = null; int maskType; do { maskType = getType(); if (1 == maskType){ mask = new HazeMask(); }else if (2 == maskType){ mask = new MedicalMask(); }else if (3 == maskType){ mask = new N95Mask(); }else { System.out.println("不支持的产品类型"); break; } mask.prepare(); mask.processing(); mask.bale(); } while (true); } /**接收用户要下单的产品类型 * 1:防霾口罩 * 2:医用口罩 * 3:n95口罩 * */ private int getType() { try { BufferedReader typeReader = new BufferedReader(new InputStreamReader(System.in)); System.out.println("输入需要下单的类型: "); return Integer.parseInt(typeReader.readLine()); } catch (Exception e) { e.printStackTrace(); } return 0; } } Client的实现就相对简单,模拟用户下单操作,直接调用直营店暴露的下单order方法 优缺点分析 根据场景需求我们有了如上的代码方案,其中涉及到的类和方法都比较好理解,核心主要是通过用户需要下单的type来进行产品的创建,但优缺点需要细细捋一捋 优点:思路清晰,便于理解 缺点:违反开闭原则,也就是扩展性差,如果添加一个新的口罩类型,涉及到的修改点过多 举个栗子: 如果这时候添加一个新的口罩类型,那所有的口罩直营店类中的代码都需要同步修改 这时候有一种解决方案:将根据类型创建产品的方法单独封装起来,当有新产品加入时,只需要修改单独封装过的这部分代码,而调用方可以做到无感知接入,这种方式也叫做简单工厂模式。但他并不属于23种设计模式,简单工厂仅仅指一种创建类的解决方案 简单工厂模式 相对于传统方案中多出一个简单工厂类SimpleMaskFactory,同时对MaskStore进行了重构,简单类图如下: 代码实现 与传统方案不同的是,之前的口罩产品创建是在MaskStore中,使用简单工厂模式后,将创建口罩产品的工作封装到了SimpleMaskFactory中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class SimpleMaskFactory { public BaseMask createMask(int maskType) { BaseMask mask = null; if (1 == maskType) { mask = new HazeMask(); } else if (2 == maskType) { mask = new MedicalMask(); } else if (3 == maskType) { mask = new N95Mask(); } return mask; } } MaskStore只需要持有工厂类和需要下单的产品类型,发起下单操作即可 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class MaskStore { private SimpleMaskFactory factory; public MaskStore(SimpleMaskFactory factory) { this.factory = factory; } public void order() { BaseMask mask = null; int maskType; do { maskType = getType(); mask = factory.createMask(maskType); if (!Objects.isNull(mask)){ mask.prepare(); mask.processing(); mask.bale(); }else { System.out.println("不支持的产品类型..."); break; } } while (true); } private int getType() { try { BufferedReader typeReader = new BufferedReader(new InputStreamReader(System.in)); System.out.println("----------------"); System.out.println("输入需要下单的类型: "); return Integer.parseInt(typeReader.readLine()); } catch (Exception e) { e.printStackTrace(); } return 0; } } Client客户端的调用也更加方便 1 2 3 4 5 public class Client { public static void main(String[] args) { new MaskStore(new SimpleMaskFactory()).order(); } } 模拟需求② 假设现有多个口罩生产工厂,大致分为杭州制造和上海制造,可以生产防霾口罩、医用一次性口罩, 客户可以通过自己的需求下单购买某个地址制造的某一种口罩 使用代码实现这一流程 此时的需求不仅有地域区分,同时还有种类区分,这种场景该如何处理呢? 方案1 使用简单工厂模式,根据地域创建不同的工厂类,通过不同的工厂类来进行不同的产品创建 扩展性差,可维护性差 方案2 使用工厂方法模式,将创建产品的方法抽象化,创建对象的操作交给子类自己来完成,即将对象实例化推迟到子类 工厂方法模式 与简单工厂模式所不同,工厂方法模式将定义一个创建对象的抽象方法,根据实际需求整理到所涉及的类有 BaseMask 抽象的口罩类 HangzhouHazeMask 杭州制造-防霾口罩 HangzhouMedicalMask 杭州制造-医用口罩 ShanghaiHazeMask 上海制造-防霾口罩 ShanghaiMedicalMask 上海制造-医用口罩 BaseMaskFactory 抽象口罩工厂类,定义了一个创建对象的抽象方法,将对象创建延缓到子类进行 HangzhouMaskFactory 杭州制造工厂类 ShanghaiMaskFactory 上海制造工厂类 Client 客户类 简单的类图如下: 代码实现 HangzhouHazeMask、HangzhouMedicalMask、ShanghaiHazeMask、ShanghaiMedicalMask继承自BaseMask,分别实现prepare方法,并调用setName方法设置name属性 HangzhouMaskFactory、ShanghaiMaskFactory继承自BaseMaskFactory类,重写了抽象方法createMask方法实现自己的对象创建逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public abstract class BaseMaskFactory { //抽象方法,子类自己实现对象的创建 abstract BaseMask createMask(int maskType); public BaseMaskFactory() { BaseMask mask = null; int maskType; do { //1:防霾口罩 2:医用口罩 maskType = getType(); mask = createMask(maskType); if (!Objects.isNull(mask)) { mask.prepare(); mask.processing(); mask.bale(); }else { System.out.println("不支持的产品类型..."); break; } } while (true); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 public class HangzhouMaskFactory extends BaseMaskFactory{ @Override BaseMask createMask(int maskType) { BaseMask mask = null; if (1==maskType){ mask = new HangzhouHazeMask(); }else if (2==maskType){ mask = new HangzhouMedicalMask(); } return mask; } } 此时的客户调用,可以有选择性的指定某一地区来进行下单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Client { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请选择要购买的产品产地,1:杭州,2:上海"); int location = Integer.parseInt(scanner.nextLine()); if (1 == location) { new HangzhouMaskFactory(); } else if (2 == location) { new ShanghaiMaskFactory(); } else { System.out.println("暂无该地区产品"); } } } 模拟需求③ 假设现有两种产品要进行生产:口罩和酒精,并且有杭州和上海两个工厂都可以生产这两种产品 客户可以通过自己的需求下单购买某个地址制造的某一种产品 使用代码实现这一流程 这次的需求不同以往,产品类型出现了多种,即一个工厂可以生产多种不同类型的产品,这种涉及到多个产品簇,比较推荐使用抽象工厂模式 抽象工厂模式 抽象工厂模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口, 且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。 抽象工厂模式是工厂方法模式的升级版本,工厂方法模式中一个工厂只生产一种产品,而在抽象工厂模式中,一个工厂生产多种产品,并且存在多个工厂 抽象工厂模式中有这两个概念 产品等级:产品等级可以理解为同一类产品属于一个等级,比如防霾口罩、与医用外科口罩都属于口罩类,属于一个产品等级,但口罩和酒精明显不是一个产品等级 产品族:同一个具体工厂所生产的位于不同产品等级的所有产品称作一个产品族。比如杭州工厂生产的杭州口罩和酒精就属于一个产品族 上面的需求用抽象工厂模式的思路得到的简单类图如下: 代码实现 其中HangzhouMask、ShanghaiMask都继承自BaseMask,HangzhouAlcohol、ShanghaiAlcohol继承自BaseAlcohol 通过定义抽象工厂接口AbstractMaskFactory,定义创建产品的方法,交由子类工厂进行实现。这里的产品创建方法可以覆盖到所有的产品等级 1 2 3 4 5 public interface AbstractFactory { BaseMask createMask(); BaseAlcohol createAlcohol(); } 1 2 3 4 5 6 7 8 9 10 11 12 public class HangzhouFactory implements AbstractFactory{ @Override public BaseMask createMask() { return new HangzhouHazeMask(); } @Override public BaseAlcohol createAlcohol() { return new HangzhouAlcohol(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 public class ShanghaiFactory implements AbstractFactory{ @Override public BaseMask createMask() { return new ShanghaiHazeMask(); } @Override public BaseAlcohol createAlcohol() { return new ShanghaiAlcohol(); } } 创建了工厂类后,客户可以通过某一工厂进行指定产品的下单操作,这些逻辑封装在了Store类中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Store { private AbstractFactory factory; public Store(AbstractFactory factory) { this.factory = factory; } public void orderMask() { BaseMask mask = null; mask = factory.createMask(); if (!Objects.isNull(mask)) { mask.prepare(); mask.processing(); mask.bale(); } else { System.out.println("不支持的产品类型..."); } } public void orderAlcohol() { BaseAlcohol alcohol = null; alcohol = factory.createAlcohol(); if (!Objects.isNull(alcohol)) { alcohol.prepare(); alcohol.processing(); alcohol.bale(); } else { System.out.println("不支持的产品类型..."); } } } 3种工厂模式的总结 本文一共提到了三种工厂模式,简单工厂模式、工厂方法模式、抽象工厂模式,也根据模拟场景对其进行了简单的说明 从上面的介绍中可以简单做下总结 简单工厂模式 实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责 工厂类不够灵活,增加新的具体产品需要修改工厂类的判断逻辑代码 而且产品较多时,工厂方法代码逻辑将会非常复杂 工厂方法模式 定义一个抽象的核心工厂类,并定义创建产品对象的接口,创建具体产品实例的工作延迟到其工厂子类去完成 系统需要新增一个产品是,无需修改现有系统代码,只需要添加一个具体产品类和其对应的工厂子类 系统的扩展性变得很好,符合面向对象编程的开闭原则 抽象工厂模式 工厂模式的升级版,工厂方法模式中一个工厂负责生产一类产品,而抽象工厂模式中一个工厂可以生产多种产品 扩展性更强,无论是增加工厂,还是增加产品,抽象工厂模式都比工厂方法模式更为便捷 关于工厂方法模式和抽象工厂模式的几点区别如下: 工厂方法模式利用继承,抽象工厂模式利用组合 工厂方法模式产生一个对象,抽象工厂模式产生一族对象 工厂方法模式利用子类创造对象,抽象工厂模式利用接口的实现创造对象 常见的工厂模式的运用 JDK中Calendar的getlnstance方法 JDBC中的Connection对象的获取 Spring中IOC容器创建管理bean对象 反射中Class对象的newlnstance方法

2020/8/30
articleCard.readMore

设计模式(1)-带你了解3类8种单例模式

单例模式的分类 饿汉式 静态常量 静态代码块 懒汉式 线程不安全 线程安全,同步方法 线程安全,同步代码块 双重检查锁 静态内部类 枚举 饿汉式 饿汉式,单例模式的一种类型,对于这个名字可以假想成: 有一天小明买了菜回到家,由于他特别饿,于是就把所有菜都用掉做了满满一桌子菜,而直到最后吃饱,仍然有一些菜从来没尝过,而且由于做的菜太多导致的燃气也用完了。 这里的菜就是我们要使用的对象,而小明就是单例类,燃气就是系统内存。在调用方准备使用对象前,就把所有的对象都实例化好,以供随时调用,但如果实例化工作量过大可能导致内存浪费 饿汉式-静态常量(⭐慎用) 这是最简单的单例模式,主要有以下几点核心思路 私有构造方法 私有静态常量,类加载时初始化常量对象 公有对象获取方法 示例代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class SingletonType01 { public static void main(String[] args) { Singleton01 instance1 = Singleton01.getInstance(); Singleton01 instance2 = Singleton01.getInstance(); System.out.println("instance1 == instance2 "+(instance1==instance2)); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Singleton01 { /** * 构造方法私有,防止外部实例化 */ private Singleton01() { } /** * 在类加载时创建私有的静态变量 */ private final static Singleton01 INSTANCE = new Singleton01(); /** * 对外提供获取对象的静态方法, * 外部调用,类名.方法名 Singleton.getInstance() * @return 返回单例对象 */ public static Singleton01 getInstance() { return INSTANCE; } } 示例代码本机执行结果: 1 2 3 instance1 == instance2 true 491044090 491044090 主方法中对于两次获取到的对象进行了对比,可以看到两者为同一对象,且hashcode相同 优点: 写法简单,在类装载的时候完成实例化,避免线程同步问题 缺点: 在类装载时就实例化,那可能这个对象从始至终都没有被用到,无形中造成资源浪费,没有懒加载效果 这种单例模式,可以使用,并且无需考虑多线程问题,但是存在内存浪费问题 饿汉式-静态代码块(⭐慎用) 饿汉式静态代码块的实现与静态常量基本类似,唯一不同就是对象的实例化从静态变量转移到了静态代码块中,但其都是在类加载是执行的 代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 /** * * @author larsCheng */ public class SingletonType02 { public static void main(String[] args) { Singleton02 instance1 = Singleton02.getInstance(); Singleton02 instance2 = Singleton02.getInstance(); System.out.println("instance1 == instance2 : "+(instance1==instance2)); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Singleton02 { /** * 构造方法私有,防止外部实例化 */ private Singleton02() { } /** * 静态私有变量 */ private static Singleton02 INSTANCE; /** * 将对象的实例化放在了静态代码块中,同样也是类加载时被执行 */ static { INSTANCE = new Singleton02(); } /** * 对外提供获取对象的静态方法, * 外部调用,类名.方法名 Singleton.getInstance() * @return 返回单例对象 */ public static Singleton02 getInstance() { return INSTANCE; } } 示例代码本机执行结果: 1 2 3 instance1 == instance2 : true 491044090 491044090 可以看出同样是单例对象的效果,所有与饿汉式静态常量写法相比较,其优缺点也一样,都会造成内存浪费 懒汉式 前面提到的两种单例模式都是饿汉式,即无论用不用这个对象,他对会被实例化。 这里要提到的是另一种单例模式-懒汉式,即对象只有在需要使用的时候才进行实例化,同样可以想象成一个小场景 有一天小李特别饿,但是他很懒,不想做饭就到餐馆吃饭,看了菜单从里面选择点了一份牛肉拉面,后厨师傅马上给他做好,小李吃饱后就开心的回家了 虽然描述的比较抽象,小李是是对象使用方,菜单上的每一个菜是一个单例类,后厨师傅是JVM。 当你选定一个对象了之后才会为你立即创建,而不是提前把所有的对象都实例化好。这样实现了懒加载的效果 懒汉式-线程不安全(👎👎👎不可使用) 懒汉式的简易版本,这一实现方式虽然做到了懒加载,但是存在线程安全问题 示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class SingletonType03 { public static void main(String[] args) { Singleton03 instance1 = Singleton03.getInstance(); Singleton03 instance2 = Singleton03.getInstance(); System.out.println("instance1 == instance2 : " + (instance1 == instance2)); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Singleton03 { /** * 构造方法私有,防止外部实例化 */ private Singleton03() { } /** * 静态私有变量 */ private static Singleton03 INSTANCE; /** * 对外提供获取对象的静态方法,此处存在线程安全问题 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton03 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton03(); } return INSTANCE; } } 示例代码本机执行结果: 1 2 3 instance1 == instance2 : true 491044090 491044090 简单的执行测试结果看似乎并无问题,做到了延迟加载(懒加载),并且实现了单例模式 但是!!!这一切都是单线程的前提下,一旦为多线程环境,在getInstance方法中会有严重的线程安全问题 分析: 假设有两个线程A、B A线程先到,判断INSTANCE为空,进入if内,准备进行对象初始化 此时B线程也到达if判断,发现INSTANCE仍为空(A还未完成对象实例化),B也进入if内。 这种情况下,待A、B执行完后,得到的将是两个对象。这就完全违背了单例模式的初衷!! 所以通常情况下,不推荐使用这种懒汉式的单例模式。因为绝大多数的应用场景都为多线程环境。 而在多线程环境下,这种实现方式完全不算单例模式的范畴,因为它会产生多个对象实例 懒汉式 - 同步方法(👎不推荐) 针对于线程不安全问题,对应则有线程安全的解决方案 即在getInstance方法上加入synchronized关键字,将其改造成同步方法,解决在多线程环境下的线程不安全问题 示例代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Singleton04 { /** * 构造方法私有,防止外部实例化 */ private Singleton04() { } /** * 静态私有变量 */ private static Singleton04 INSTANCE; /** * 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static synchronized Singleton04 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton04(); } return INSTANCE; } } 如上,虽然解决了线程不安全问题,但是随之而来的是效率问题 分析: 每次调用getInstance方法都需要进行线程同步 实际上造成多个对象被实例化的仅仅只是方法中代码片段 所以总的来说,虽然解决的线程安全问题,但是由于效率不加,且有优化方案,故此种方式也不建议使用 针对同步方法带来的效率问题,有改进方案,但有一种错误的改进方案这里有必要提一下 将同步方法改造为同步代码块,尝试减少同步的代码,来提高效率,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Singleton04ErrorSolution { /** * 构造方法私有,防止外部实例化 */ private Singleton04ErrorSolution() { } /** * 静态私有变量 */ private static Singleton04ErrorSolution INSTANCE; /** * 对外提供获取对象的静态方法,对造成线程安全问题的代码块进行同步 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton04ErrorSolution getInstance() { if (INSTANCE == null) { synchronized (Singleton04ErrorSolution.class) { INSTANCE = new Singleton04ErrorSolution(); } } return INSTANCE; } } 如上代码的本意是将同步方法细化到同步代码块,来进行效率优化,但是这样的改动起到了相反的效果 分析: 对实例化对象的代码片段进行同步,假设A、B两线程执行getInstance方法 A线程判断INSTANCE为空后进入if内,准备执行同步代码块,此时B线程也判断INSTANCE为空,也进入了if内部,等待A线程执行完毕 A线程执行完同步代码块后,实例化了一个对象,此时B线程开始执行,也创建了一个对象 从上面的分析可以看出,这种改进方案,属于想法正确,但是操作错误,导致不但没有解决效率问题,同时造成线程安全问题,是一定要避免的错误!! 懒汉式-同步代码块(👎不推荐) 基于上文提到的优化思路:将同步方法细化到同步代码块,那正确的改进方案可能会有下面这种写法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Singleton05 { /** * 构造方法私有,防止外部实例化 */ private Singleton05() { } /** * 静态私有变量 */ private static Singleton05 INSTANCE; /** * 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton05 getInstance() { synchronized (Singleton05.class) { if (INSTANCE == null) { INSTANCE = new Singleton05(); } } return INSTANCE; } } 从getInstance方法可以看到,使用了同步代码块的方式,并且同步的是if判断和实例化部分的代码 虽然达到了线程安全,但是基本上和同步方法的效率没什么区别,依旧每个线程进来后,都需要等待执行同步代码块。 这种方案只是为了和上面的错误同步代码块方式进行对比。真实业务中也不推荐使用这种方式!!! 双重检查锁(👍推荐使用) 想要实现懒加载,同时保证线程安全,同时提高效率。那么一起来看看双重检查锁的实现方式: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Singleton06 { /** * 构造方法私有,防止外部实例化 */ private Singleton06() { } /** * 静态私有变量 * 声明volatile,防止指令重排,导致的空对象异常 */ private static volatile Singleton06 INSTANCE; /** * 对外提供获取对象的静态方法,使用双重检查锁机制,保证同步代码块中的实例化代码只会被执行一次 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton06 getInstance() { if (INSTANCE == null) { synchronized (Singleton06.class) { if (INSTANCE == null) { INSTANCE = new Singleton06(); } } } return INSTANCE; } } 首先先来看看该方案于前几种的不同点 使用synchronized关键字实现同步代码块 同步前同步后两次判断 使用了volatile关键字 分析 在getInstance方法中使用了Double-Check概念,配合同步代码块,保证线程安全。简单分析下其流程 A、B、C 3个线程执行getInstance方法 A、B线程都通过了第一个if判断,A线程抢到了锁,开始执行同步代码块中的逻辑,B等待 A通过了第二个if判断,进行了INSTANCE的实例化操作,A完成操作,释放锁 B开始执行同步代码块内容,B未通过第二个if(此时的INSTANCE不为空),直接返回INSTANCE对象,B释放锁 此时C开始执行getInstance方法,C未通过第一个if,直接返回INSTANCE对象 从上面分析过程中可以看到,无论有多少个线程,实例化代码只会被执行一次,意味着只会创建一个对象。 volatile 但是在整个流程中有一个小小的隐患 INSTANCE = new Singleton06();它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事: ①第一步:给 INSTANCE 分配内存空间; ②第二步:调用 Singleton06 的构造函数等,来初始化 INSTANCE; ③第三步:将 Singleton06 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)。 这里的理想执行顺序是 1->2->3,实际在Jvm中执行顺序有可能是1->3->2,也有可能是 1->2->3。 这种现象被称作指令重排也就是说第 2 步和第 3 步的顺序是不能保证的,这就导致了隐患的产生。 在线程A执行INSTANCE = new Singleton06();是,JVM中的执行顺序是1->3->2,先进行分配内存再初始化INSTANCE,若在刚完成内存分配时,线程C开始执行第一个if判断,发现INSTANCE不为空,直接返回INSTANCE对象,此时的INSTANCE明显会出现问题。 在Java内存模型中,volatile 关键字作用可以是保证可见性且禁止指令重排。从而避免由于指令重排导致的异常隐患。 关于 volatile关键字和指令重排相关 可以参考此处 总结 双重检测锁的单例实现方案,可以实现延迟加载,同时线程安全并且效率高,在实际场景中是推荐使用的! 静态内部类(👍推荐使用) 除了双重检查锁被推荐使用外,静态内部类实现单例模式也是被推荐使用的一种 示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Singleton07 { /** * 构造方法私有,防止外部实例化 */ private Singleton07() { } /** * 提供一个静态内部类,类中声明一个类型为 Singleton07 的静态属性 INSTANCE */ private static class SingletonInstance { private static final Singleton07 INSTANCE = new Singleton07(); } /** * 对外提供获取对象的静态方法, * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回静态内部类的静态属性 */ public static Singleton07 getInstance() { return SingletonInstance.INSTANCE; } } 分析 该方案采用了类装载机制来保证初始化实例时只有一个线程,从而保证了线程安全 单例类Singleton07被装载时,静态内部类SingletonInstance是不会实例化的,只有调用getInstance方法时才会触发静态内部类SingletonInstance的装载,从而执行实例化代码 并且静态内部类的静态属性只会在第一次加载类的时候被初始化,所以做到了懒加载 结论 保证了线程安全,使用静态内部类的特点实现懒加载,并且有较高效率,推荐使用 枚举(👍推荐使用) 那么这么多的实现方案,Java中有没有一个公认的最佳枚举实现方案呢,当然有啊,通过枚举来实现 代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class SingletonType08 { public static void main(String[] args) { String connection1 = Singleton08.INSTANCE.getConnection(); String connection2 = Singleton08.INSTANCE.getConnection(); System.out.println("connection1 == connection2 : " + (connection1 == connection2)); System.out.println(connection2.hashCode()); System.out.println(connection2.hashCode()); } } enum Singleton08 { /***/ INSTANCE; /**资源对象,此处以字符串示例*/ private String connection = null; /** * 在私有构造中实例化单例对象 */ Singleton08() { //模拟实例化过程 this.connection = "127.0.0.1"; } /** * 对外提供获取资源对象的静态方法 */ public String getConnection() { return connection; } } 如上代码是通过枚举来实现单例对象的创建 enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。 枚举类型是线程安全的,并且只会装载一次。 枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,它保证线程安全,并防止外部反序列化的破坏。

2020/8/29
articleCard.readMore

Java时间处理5---Java8中时区相关类库介绍

前言 在Java8以前,我们对于时区的处理通常是为时间转换类设置指定TimeZone,然后进行时区时间转换。 而在Java8中不仅对时间日期进行了细粒度处理,有无时区,时区处理也进行了更加细粒度的优化。 在之前我们介绍的新类库中基本都是无时区概念的。本文将引入时区概念。 概念 介绍时区相关类库前,先来认识下Java中常见的几种时间格式 1.世界标准时间(UTC时间),其中T表示时分秒的开始,结尾的Z表示这是一个世界标准时间 2020-07-06T11:24:37.081Z 2.本地时间(不含时区信息的时间),结尾无时区信息 2020-07-06T19:24:37.156 3.含有时区信息的时间,+08:00表示该时间是由UTC时间加上8小时得到的,[Asia/Shanghai]表示该时间的时区信息 2020-07-06T19:24:37.156+08:00[Asia/Shanghai] ZoneId和ZoneOffSet ZoneId表示一个时区实例,他的内部定义了一个地区的时区规则集,例如Europe/Paris ZoneOffSet表示与UTC时间的偏移时间,格式为+08:00、-04:00 创建ZoneId 1 2 3 4 5 6 7 //获取系统默认时区 System.out.println(ZoneId.systemDefault()); //4种常用方式创建ZoneId System.out.println(ZoneId.of("+01:00")); System.out.println(ZoneId.of("UTC+01:00")); System.out.println(ZoneId.of("America/Chicago")); System.out.println(ZoneId.ofOffset("UTC", ZoneOffset.of("+01:00"))); 输入结果: 1 2 3 4 5 Asia/Shanghai +01:00 UTC+01:00 America/Chicago UTC+01:00 创建ZoneOffSet 1 2 3 System.out.println(ZoneOffset.ofHours(3)); System.out.println(ZoneOffset.ofHoursMinutesSeconds(1, 2, 3)); System.out.println(ZoneOffset.of("+01:00")); 输出结果: 1 2 3 +03:00 +01:02:03 +01:00 单独看ZoneId和ZoneOffSet可能还不能完全看出使用效果,下面看看带时区的日期时间 ZoneDateTime 表示ISO-8601日历系统中具有时区的日期时间,此类存储所有日期和时间字段,精度为纳秒,时区为区域偏移量,用于处理模糊的本地日期时间。 例如:2020-07-06T19:24:37.156+08:00[Asia/Shanghai] ZonedDateTime相当于拥有三个独立对象,一个本地日期时间LocalDateTime ,一个时区IDZoneId和时间偏移量ZoneOffset。 偏移量和本地日期时间用于在必要时定义一个瞬时时间。 时区ID用于获取偏移量的具体规则。(因为在部分区域夏时令时的偏移量与平常不同) 来看看ZoneDateTime的常用方法 初始化 1 2 3 4 5 6 7 8 9 10 //默认系统时区 System.out.println(ZonedDateTime.now()); //指定一个时区的时间 System.out.println(ZonedDateTime.now(Clock.system(ZoneId.of("Europe/Paris")))); //指定一个偏移量的时间 System.out.println(ZonedDateTime.now(Clock.system(ZoneOffset.of("+04:00")))); //根据本地日期时间和系统时区组合日期时间 System.out.println(ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault())); //根据年月日时分秒毫秒纳秒时区id构建 System.out.println(ZonedDateTime.of(2020, 1, 1, 1, 1, 1, 111, ZoneId.of("Europe/Paris"))); 输出结果: 1 2 3 4 5 2020-07-10T11:44:15.651+08:00[Asia/Shanghai] 2020-07-10T05:44:15.653+02:00[Europe/Paris] 2020-07-10T07:44:15.668+04:00 2020-07-10T11:44:15.668+08:00[Asia/Shanghai] 2020-01-01T01:01:01.000000111+01:00[Europe/Paris] 其他方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ZonedDateTime z = ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault()); System.out.println(z.getZone());//获取时区信息 System.out.println(z.getOffset());//获取时间偏移量 System.out.println(z.getDayOfMonth());//获取当月第几天 System.out.println(z.getDayOfWeek());//获取本周星期几 System.out.println(z.getDayOfYear());//获取本年第几天 //获取时间信息 System.out.println(z.getYear()+"/"+z.getMonthValue()+"/"+z.getDayOfMonth()+" "+ z.getHour()+":"+z.getMinute()+":"+z.getSecond()+"."+z.getNano()); //加减时间 System.out.println(z.plusHours(3)); System.out.println(z.minusHours(3)); //修改时间 System.out.println(z.withHour(20)); 输出结果 1 2 3 4 5 6 7 8 9 Asia/Shanghai +08:00 10 FRIDAY 192 2020/7/10 13:37:19.37000000 2020-07-10T16:37:19.037+08:00[Asia/Shanghai] 2020-07-10T10:37:19.037+08:00[Asia/Shanghai] 2020-07-10T20:37:19.037+08:00[Asia/Shanghai] 时区与偏移量 本文开始的时候介绍了ZoneId和ZoneOffSet,在Java8中这两个类都可以对日期时间进行时区的转换,但是我更推荐使用时区信息(ZoneId),而不是时间偏移量(ZoneOffset) 首先需要重温一下概念 ZoneId表示一个时区实例,他的内部定义了一个地区的时区规则集,例如Europe/Paris ZoneOffSet表示与UTC时间的偏移时间,格式为+08:00、-04:00 这里我们以亚洲上海时间(北京时间)—>法国巴黎时间为例,对三月份的时间和六月份两个时间进行转换 1 2 3 4 5 6 7 ZoneId zoneId = ZoneId.of("Europe/Paris"); ZonedDateTime now = ZonedDateTime.now().withMonth(6); System.out.println("6月的此时北京时间:"+now); System.out.println("6月的此时巴黎时间:"+now.withZoneSameInstant(zoneId)); ZonedDateTime newTime = now.withMonth(3); System.out.println("3月的此时北京时间:"+newTime); System.out.println("3月的此时巴黎时间:"+newTime.withZoneSameInstant(zoneId)); 输出结果: 1 2 3 4 6月的此时北京时间:2020-06-10T14:23:48.756+08:00[Asia/Shanghai] 6月的此时巴黎时间:2020-06-10T08:23:48.756+02:00[Europe/Paris] 3月的此时北京时间:2020-03-10T14:23:48.756+08:00[Asia/Shanghai] 3月的此时巴黎时间:2020-03-10T07:23:48.756+01:00[Europe/Paris] 有没有发现什么异样? 两个同一时刻不同月份的时间转换了时区后第一次偏移量为2小时,第二次为1小时 这是因为部分国家存在夏时令这种骚操作,一年中不同的月份有着不同的时间偏移量。 如果我们使用ZoneOffset,假设你知道目标时区的多种偏移时间,那么可以进行代码判断处理,但是如果要转换的时区很多,或者完全没有考虑夏时令问题时,那么转换出来的时间将会超乎你的想象!!

2020/7/10
articleCard.readMore

Java时间处理4---Java8中LocalDate、LocalTime、LocalDateTime介绍

前言 在Java8中,对于日期、时间、时间日期有不同的对象来表示,分别就是LocalDate、LocalTime、LocalDateTime 他们都位于java.time包下,并且他们都仅单纯的表示一个不可变的时间对象,无时区等附加信息的出现 LocalDate 一个不可变的时间对象用来表示一个日期(年月日),仅包含日期,例如2020-06-06 它的可表示范围为-999999999-01-01到+999999999-12-31 LocalTime 一个不可变的时间对象,用来表示一个时间(时分秒),最高精度为纳秒,例如11:11:11 它的表示范围为:00:00到23:59:59.999999999 LocalDateTime 一个不可变的时间对象,用来表示一个日期时间(年月日时分秒),最高精度为纳秒,例如2020-06-06T13:45:30.123456789 -999999999-01-01T00:00:00到+999999999-12-31T23:59:59.999999999 LocalDate常用方法示例 LocalDate、LocalTime、LocalDateTime三者的使用方式基本相同,APi设计也类似,可以说掌握其中一个明白其他两个的用法了 此处以LocalDate的常用方法进行介绍: 初始化一个LocalDate now(…)系列: 获取当前日期 of(…)系列 : 获取指定年月日日期 parse(…)系列: 将日期字符串转换为LocalDate日期 from(…)系列:将TemporalAccessor类型对象转换为LocalDate日期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /*now()*/ System.out.println(LocalDate.now());//获取当前日期 System.out.println(LocalDate.now(ZoneId.systemDefault()));//获取系统时区当前日期 System.out.println(LocalDate.now(Clock.systemDefaultZone()));//获取系统时钟当前日期 /*of()*/ System.out.println(LocalDate.of(2020, 6, 6));//获取指定年月日日期 System.out.println(LocalDate.of(2020, Month.JUNE, 6));//获取指定年月日日期 System.out.println(LocalDate.ofEpochDay(10));//获取大于1970-1-1多少天的日期 System.out.println(LocalDate.ofYearDay(2020, 200));//获取指定年份中的第几天的日期 /*parse()*/ System.out.println(LocalDate.parse("2017-05-05"));//DateTimeFormatter.ISO_LOCAL_DATE System.out.println(LocalDate.parse("2017-05-05", DateTimeFormatter.ISO_DATE)); System.out.println(LocalDate.parse("20170505", DateTimeFormatter.BASIC_ISO_DATE)); System.out.println(LocalDate.parse("2017-05-05+01:00", DateTimeFormatter.ISO_OFFSET_DATE)); /*from()*/ LocalDate.from(LocalDate.parse("2017-05-05"));//将TemporalAccessor类型对象转换为LocalDate日期 获取LocalDate中的信息 从已有的LocalDate中获取年、月、日等具体信息 get…()系列 其他 1 2 3 4 5 6 7 8 9 10 11 LocalDate localDate = LocalDate.now(); System.out.println(localDate.getYear());//获取年 System.out.println(localDate.getMonthValue());//月 System.out.println(localDate.getDayOfMonth());//日 System.out.println(localDate.getMonth());//月份 System.out.println(localDate.getDayOfWeek());//星期 System.out.println(localDate.getDayOfYear());//本年中的第几天 System.out.println("是否闰年:"+localDate.isLeapYear()); System.out.println("本月天数:"+localDate.lengthOfMonth()); System.out.println("本年天数:"+localDate.lengthOfYear()); 修改LcoalDate的内容 可以对现有的LcoalDate对象进行修改 plusxxx()系列 minusxxx()系列 withxxx()系列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 LocalDate localDate = LocalDate.now(); System.out.println("当前日期:"+localDate); System.out.println("3天后:"+localDate.plusDays(3)); System.out.println("1月后:"+localDate.plusMonths(1)); System.out.println("1年后:"+localDate.plusYears(1)); System.out.println("1周后:"+localDate.plusWeeks(1)); System.out.println("3天前:"+localDate.minusDays(3)); System.out.println("1月前:"+localDate.minusMonths(1)); System.out.println("1年前:"+localDate.minusYears(1)); System.out.println("1周前:"+localDate.minusWeeks(1)); System.out.println(localDate.withYear(2012));//指定某一年的今天 System.out.println(localDate.withMonth(12));//指定某月的今天 System.out.println(localDate.withDayOfMonth(1));//指定本月的第几天 System.out.println(localDate.withDayOfYear(1));//指定今年的第几天 三者之间的转化关系 既然上面的三个类是分别代表时间、日期、日期时间,那他们必定存在相互转化的关系 就这个关系,我们进行代码示例如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 LocalDate localDate = LocalDate.of(2020,1,1); LocalTime localTime = LocalTime.now(); LocalDateTime localDateTime = LocalDateTime.now(); LocalDate ldt2ld = localDateTime.toLocalDate(); LocalTime ldt2lt = localDateTime.toLocalTime(); //localDateTime--->LocalDate System.out.println(localDateTime+"---> localDateTime.toLocalDate() --->"+ldt2ld); //localDateTime--->LocalTime System.out.println(localDateTime+"---> localDateTime.toLocalTime() --->"+ldt2lt); LocalDateTime atDate = localTime.atDate(localDate); LocalDateTime atTime = localDate.atTime(localTime); LocalDateTime dateTime = LocalDateTime.of(localDate, localTime); //localDate+localTime--->LocalDateTime System.out.println("["+localDate+" + "+localTime+"] localDate.atTime(localTime) --->"+atTime); System.out.println("["+localDate+" + "+localTime+"] localTime.atDate(localDate) --->"+atDate); System.out.println("["+localDate+" + "+localTime+"] LocalDateTime.of(localDate, localTime) --->"+dateTime); 输出结果 1 2 3 4 5 2020-07-10T17:02:47.711---> localDateTime.toLocalDate() --->2020-07-10 2020-07-10T17:02:47.711---> localDateTime.toLocalTime() --->17:02:47.711 [2020-01-01 + 17:02:47.710] localDate.atTime(localTime) --->2020-01-01T17:02:47.710 [2020-01-01 + 17:02:47.710] localTime.atDate(localDate) --->2020-01-01T17:02:47.710 [2020-01-01 + 17:02:47.710] LocalDateTime.of(localDate, localTime) --->2020-01-01T17:02:47.710 总结 本文介绍的三个类,属于java时间库中较为常用的三个类,它代替了java8以前常用的Date和Calendar类,转变为以更加细粒度的时间、日期、时间日期概念进行时间处理。

2020/7/10
articleCard.readMore

Java时间处理3---Java8中Instant、Duration、Period、Clock介绍

前言 前面文章对Java中的Date和Calendar类进行了介绍,在Java8以前,Javaer处理时间基本都是使用这两个类。 然鹅在使用过程中一个很尴尬的场景就是Date大部分方法废弃,Calendar又有很多不太友好的设计(月份从0开始) 终于,Java8中提供了一套全新的时间处理库,源码中的目录为java.time,该包中的类都是不可变且线程安全。 看上图感觉新的time包下好像有很多都是新的类,感觉看着很头大啊,不过不用担心新提供的处理类中方法设计具有规律性,并且模块清晰,上手较快。 下面对比较常用的类库进行介绍。 本文主要对Instant、Duration、Period、Clock这四个类进行介绍 Instant:时间线上的某一时间点 Duration:两个时间之间的持续时间,存储秒和纳秒 Period:两个日期之间的持续时间,存储年,月和日 Clock:表示真实世界的时钟,可通过时钟访问的当前日期和时间 Instant Instant用于记录时间线上某一瞬间的时间点,顾名思义就是时间戳,但它不同于System.currentTimeMillis();精度为秒 Instant可以精确到纳秒,它的取值范围为:-1000000000-01-01T00:00Z到1000000000-12-31T23:59:59.999999999Z 下面看下他的常用方法示例: now(): 获取基于UTC时间的Instant ofEpochMilli(long milli):根据时间戳(毫秒)创建一个Instant实例 ofEpochSecond(long second): 根据时间戳(秒)创建一个Instant实例 parse(): 根据时间字符串转换为Instant实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //UTC System.out.println(Instant.now()); //系统时区 System.out.println(Instant.now(Clock.systemDefaultZone())); //根据时间字符串转换为Instant实例 System.out.println(Instant.parse("2020-06-06T12:12:12Z")); Instant instant =Instant.parse("2020-06-06T12:12:12Z"); long milli = instant.toEpochMilli(); long second = instant.getEpochSecond(); //给定时间戳转换为Instant实例 System.out.println(Instant.ofEpochMilli(milli)); //给定时间戳转换为Instant实例 System.out.println(Instant.ofEpochSecond(second)); //给定时间戳和纳秒值转换为Instant实例 System.out.println(Instant.ofEpochSecond(second, 111)); 输出结果: 1 2 3 4 5 6 2020-07-10T08:37:52.299Z 2020-07-10T08:37:52.380Z 2020-06-06T12:12:12Z 2020-06-06T12:12:12Z 2020-06-06T12:12:12Z 2020-06-06T12:12:12.000000111Z Duration Duration通常用秒或者纳秒相结合来表示一个时间量,最高精度为纳秒 通常用作表示两个时间之间的间隔,也称作持续时间,例如1s持续时间表示为PT1S 创建一个Duration实例 ofXXX()系列方法: 根据纳秒、毫秒、秒、分、时、天等时间来构造持续时间 from(TemporalAmount amount):根据TemporalAmount实例创建Duration对象 parse(CharSequence text):根据ISO-8601持续时间格式字符串创建Duration对象 between(Temporal startInclusive, Temporal endExclusive):获取两个时间对象之间的持续时间 1 2 3 4 5 6 7 8 9 10 11 System.out.println(Duration.ofNanos(1000)); System.out.println(Duration.ofMillis(1000)); System.out.println(Duration.ofSeconds(30)); System.out.println(Duration.ofSeconds(30,12345)); System.out.println(Duration.ofMinutes(1)); System.out.println(Duration.ofHours(1)); System.out.println(Duration.ofDays(1)); System.out.println(Duration.of(1000, ChronoUnit.MILLIS)); System.out.println(Duration.from(ChronoUnit.MINUTES.getDuration())); System.out.println(Duration.parse("PT20.345S")); System.out.println(Duration.between(Instant.parse("2020-06-23T10:15:30.00Z"), Instant.now())); 输出结果 1 2 3 4 5 6 7 8 9 10 11 PT0.000001S PT1S PT30S PT30.000012345S PT1M PT1H PT24H PT1S PT1M PT20.345S PT406H26M35.814S Duration常用方法 getXXX(): 获取持续时间对象具体的秒数或者毫秒数 plusXXX(): 给Duration对象加上指定精度的值 minusXXX(): 给Duration对象减去指定精度的值 withXXX(): 修改Duration对象的秒数or毫秒数 其他方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Duration d = Duration.parse("PT20.345S"); System.out.println(d.getSeconds()); System.out.println(d.getNano()); System.out.println(d.withNanos(3456789));//修改纳秒值,返回一个新的Duration System.out.println(d.withSeconds(22));//修改秒值,返回一个新的Duration System.out.println(d.plusNanos(1));//加1纳秒,返回一个新的Duration System.out.println(d.plusMillis(100));//加100毫秒,返回一个新的Duration System.out.println(d.plusSeconds(1)); System.out.println(d.minusNanos(1));//减去1纳秒,返回一个新的Duration System.out.println(d.minusMillis(100));//减去10毫秒,返回一个新的Duration System.out.println(d.minusSeconds(1)); System.out.println(d.isZero());//是否为0 System.out.println(Duration.ZERO.isZero());//是否为0 System.out.println(d.isNegative());//是否为负 System.out.println(d.negated());//求负 System.out.println(d.negated().abs());//求绝对值 输出结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 20 345000000 PT20.003456789S PT22.345S PT20.345000001S PT20.445S PT21.345S PT20.344999999S PT20.245S PT19.345S false true false PT-20.345S PT20.345S Period 与Duration类似都是用来表示持续时间 但是Period是由年月日为单位的时间量,例如1年2个月3天 与Duration相比,Period的用法与之基本相同 初始化Period ofXXX()系列方法: 根据年月日来构造持续时间 from(TemporalAmount amount):根据TemporalAmount实例创建Period对象 parse(CharSequence text):根据ISO-8601持续时间格式字符串创建Period对象 between(LocalDate startDateInclusive, LocalDate endDateExclusive):获取两个日期对象之间的持续时间 1 2 3 4 5 6 7 8 9 System.out.println(Period.of(1, 2, 3));//根据年月日构造Period System.out.println(Period.ofDays(1)); System.out.println(Period.ofMonths(2)); System.out.println(Period.ofWeeks(3));//根据周数构造 System.out.println(Period.ofYears(1)); System.out.println(Period.from(Period.ofMonths(1))); System.out.println(Period.parse("P20Y10M5D"));//根据ISO-8601时间格式字符串进行构造 //计算两个日期对象之间的持续时间 System.out.println(Period.between(LocalDate.now().minusYears(1).minusDays(1),LocalDate.now() )); 输出结果 1 2 3 4 5 6 7 8 P1Y2M3D P1D P2M P21D P1Y P1M P20Y10M5D P1Y1D Period常用方法 常用方法的使用方式与Duration也基本类似 getXXX(): 获取持续时间对象具体的年、月、日 plusXXX(): 给Period对象加上指定精度的值 minusXXX(): 给Period对象减去指定精度的值 withXXX(): 修改Period对象的某一精度值 其他方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Period p = Period.of(1, 2, 3); //获取年月日 System.out.println(p.getYears()+"年"+p.getMonths()+"月"+p.getDays()+"日"); //重设Period的年月日 System.out.println(p.withYears(3).withMonths(2).withDays(1)); //加上1天 System.out.println(p.plusDays(1)); //减去1天 System.out.println(p.minusDays(1)); //判断是否为0 System.out.println(p.isZero()); //判断是否为负 System.out.println(p.isNegative()); //取负 System.out.println(p.negated()); 输出结果 1 2 3 4 5 6 7 1年2月3日 P3Y2M1D P1Y2M4D P1Y2M2D false false P-1Y-2M-3D Clock Clock表示一个时钟,Clock的实例用于查找当前时刻,可以使用存储的时区来解释当前时刻以查找当前日期和时间。某种程度上可以使用时钟代替System.currentTimeMillis()和TimeZone.getDefault()。 我们可以自定义创建一个指定滴答间隔的时钟,用来获取需要的时间日期 钟表的滴答间隔(tickDuration):规定了提供下一个读数的时间间隔。比如,滴答间隔为 1 秒的钟表,读数的分辨率就到 1 秒。滴答间隔为 5 秒的钟表,读数的"分辨率" 就到 5 秒。这里,5 秒的"分辨率"是指,当实际时间数据是 0 或 1、2、3、4 秒时,从它那里得到的读数都是 0 秒。当实际时间数据是 5 或 6、7、8、9 秒时,从它那里得到的读数都是 5 秒。 Clock的初始化 1 2 3 4 Clock clock = Clock.systemUTC(); System.out.println(clock.millis());//打印时钟当前毫秒值 System.out.println(System.currentTimeMillis());//打印当前毫秒值 System.out.println(clock.instant().toEpochMilli());//时钟转换为Instant实例并获取时间戳毫秒值 输出结果 1 2 3 1594371253772 1594371253772 1594371253773 自定义Clock的创建 使用tick()方法创建一个滴答间隔为3s的时钟,每1s钟查看一下它的时间 1 2 3 4 5 6 7 8 9 10 //系统默认时区时钟 Clock clock = Clock.systemDefaultZone(); //滴答时间间隔为3秒的时钟 //当实际时间数据是 0 或 1、2秒时,从它那里得到的读数都是 0 秒。当实际时间数据是 3或 4、5秒时,从它那里得到的读数都是 3 秒。 Clock tick = Clock.tick(clock, Duration.ofSeconds(3)); for (int i = 0; i < 10; i++) { TimeUnit.SECONDS.sleep(1); System.out.println(clock.instant()+"---> "+tick.instant()); } 输出结果如下,可以看到两个时钟每秒钟的计数是不同的: 1 2 3 4 5 6 7 8 9 10 2020-07-10T08:55:35.182Z---> 2020-07-10T08:55:33Z 2020-07-10T08:55:36.195Z---> 2020-07-10T08:55:36Z 2020-07-10T08:55:37.195Z---> 2020-07-10T08:55:36Z 2020-07-10T08:55:38.196Z---> 2020-07-10T08:55:36Z 2020-07-10T08:55:39.197Z---> 2020-07-10T08:55:39Z 2020-07-10T08:55:40.198Z---> 2020-07-10T08:55:39Z 2020-07-10T08:55:41.198Z---> 2020-07-10T08:55:39Z 2020-07-10T08:55:42.199Z---> 2020-07-10T08:55:42Z 2020-07-10T08:55:43.199Z---> 2020-07-10T08:55:42Z 2020-07-10T08:55:44.200Z---> 2020-07-10T08:55:42Z 使用tickSeconds()和tickMinutes()创建时钟 tickSeconds(ZoneId zone) : 创建一个滴答间隔为1秒的时钟 tickMinutes(ZoneId zone) :创建一个滴答间隔为1分钟的时钟 1 2 3 4 5 6 7 8 9 10 11 //系统默认时区时钟 Clock clock = Clock.systemDefaultZone(); //获取滴答间隔为1秒的钟表 Clock clock1 = Clock.tickSeconds(ZoneId.systemDefault()); //获取滴答间隔为1分钟的钟表 Clock clock2 = Clock.tickMinutes(ZoneId.systemDefault()); for (int i = 0; i < 10; i++) { TimeUnit.SECONDS.sleep(1); System.out.println(clock.instant()+"---> "+clock1.instant()+"---> "+clock2.instant()); } 输出结果,从左到右依次为,系统默认时钟—>滴答间隔1秒的时钟---->滴答间隔1分钟的时钟 1 2 3 4 5 6 7 8 9 10 2020-07-10T08:58:58.001Z---> 2020-07-10T08:58:58Z---> 2020-07-10T08:58:00Z 2020-07-10T08:58:59.001Z---> 2020-07-10T08:58:59Z---> 2020-07-10T08:58:00Z 2020-07-10T08:59:00.002Z---> 2020-07-10T08:59:00Z---> 2020-07-10T08:59:00Z 2020-07-10T08:59:01.002Z---> 2020-07-10T08:59:01Z---> 2020-07-10T08:59:00Z 2020-07-10T08:59:02.002Z---> 2020-07-10T08:59:02Z---> 2020-07-10T08:59:00Z 2020-07-10T08:59:03.003Z---> 2020-07-10T08:59:03Z---> 2020-07-10T08:59:00Z 2020-07-10T08:59:04.004Z---> 2020-07-10T08:59:04Z---> 2020-07-10T08:59:00Z 2020-07-10T08:59:05.005Z---> 2020-07-10T08:59:05Z---> 2020-07-10T08:59:00Z 2020-07-10T08:59:06.005Z---> 2020-07-10T08:59:06Z---> 2020-07-10T08:59:00Z 2020-07-10T08:59:07.006Z---> 2020-07-10T08:59:07Z---> 2020-07-10T08:59:00Z 总结 以上是Java8中针对瞬时时间、持续时间、时钟加入的新工具类,可以看到对于时间的概念区分更加细化、这四个基础的时间概念也是Java8中时间处理比较常用的模块,大家不妨上手敲几段代码试试。

2020/6/10
articleCard.readMore

一些有意思的问答

一些有趣的问答,做答时间(03.26) 1. 来做个自我介绍吧,以及有几道快问快答等你接招。 大家好,我是LarsCheng,一枚程序猿,来自古城西安,目前在杭州一家物联网科技公司从事服务端开发工作。平时喜欢电影和文字,热衷于尝试新的事物,从毕业开始坚持通过博客来分享并记录生活,努力让每一天都过得精彩和不同。 2. 你最近都在忙些什么? 除了工作外,最近在捣鼓如何通过容器技术(docker)实现cs游戏服务器的一键部署,这种高龄经典游戏与当下主流技术的碰撞简直不要太刺激。 3. 如果可以在世界上所有人中任意选择,你想邀请谁共进晚餐? 三年前的自己 虽然这世界很大有很多人和事还在等着我,但是有那么一个人在我失落时默默替我承受,在我高兴时只有他感受最深,陪伴我二十多年却很少为他着想,在上个月我收到了一封来自三年前的邮件,信中的字里行间满是稚嫩,如果真的有这个机会,我想与曾经的自己坐下来,一起和这位老朋友聊聊这几年的人和事。 4. 你最喜欢/最常用的微信表情(系统自带的不算)。 5. 你最近一次给/被别人成功安利了啥? 被别人安利:韩剧《请回答1988》 你能想象一个大直男看这部剧处处被戳中泪点嘛?这是我第一次看完一整部韩剧,也是我看过的最温暖的一部剧,它涵盖了所有人的青春,所有人的记忆,整部剧充满了对生活热爱和期待,我也十分愿意把他安利给其他人,这是一部你不舍的快进的优秀影视剧!!! 给别人安利:国产linux系统deepin 如果你厌烦了windows的广告弹窗,如果你像我一样是一个喜欢瞎捣鼓的人,不妨试试国产深度系统Deepin,Deepin是一款专注于日常办公和生活娱乐等基于linux内核,以桌面应用为主的开源GUV/linux的操作系统,支持笔记本电脑,台式机和一体机。使用Deepin已经快要半年了,感觉已经离windows越来越远。 6. 现在还有哪些内容是你上网时一定会点进去看的? 行业内新的技术框架案例分析,广受好评的的生产力工具推荐等 7. 说一个你最近常在思考的问题吧。 如何将每一天过的精彩且不同? 人的一生大概有80年,那就是29200天,如果现在是25岁,那已经过去了31%的时间,你的人生还剩下69%,然而实际上,每一天都是在倒计时,你也无法预测未来还有多少时间。最近常常有人说:明天和意外永远也不知道哪一个先到,在这个充满躁动的大环境中,唯有把握自己的每一天才是最真实的。如何将往后的每一天过得精彩和不同,这是一个十分值得尝试的人生计划。 8. 都说科技改变生活。那你的生活呢,有什么“于是再也回不去了”的改变吗?无论正反,具体聊聊呗。 移动支付 这无疑是近十年来国内最大的技术革新没有之一,时间回到2010年,那个时候过节送礼还很流行送钱包,然而现在呢,基本上没有使用钱包的习惯了,纸币已经到了没有机会花的时代,自从教会了父母使用扫一扫付款,他们也表示再也不用为找零钱而烦恼了。 9. 你工作,你生活,你也慢慢攒下了一批物件,试审视:它们中的哪些成了你的必须品(也就是,哪怕你换工作、换城市,你都打算一直带着的那些器物。)可按重要性排序,最重要的两件不妨展开聊聊。 票 作为一名断舍离晚期患者,一直秉承着用不到的就没用的原则,却偏偏对于票类有着极度的收藏癖,可能这与科技无关,但是确实是我辗转几个城市都一直不曾舍弃的藏品。大概从大学开始到现在的所有的车票、机票、电影票、景区票都保留至今,这些每一张票背后都是一段青春的记忆。可能慢慢往后就得变成电子票收藏了,这也是科技在进步的体现吧。 10.你还用过什么好物(软件/硬件/服务)是 少有人知道的?求分享![“少有人知道的”划重点] PicGo 如果你长期写博客,那么你一定为图床而烦恼过,pic-go就是一款专为文字创作着提供的图床神器,一键自动上传图片并返回Markdown图片地址,再搭配github作为图片仓库,使用体验总结为两个字:真香! Teambition 属于阿里旗下的团队协作工具, 如果你使用过一定会被他的ui所吸引。 但它吸引我的是:个人计划安排、日程功能。你可以通过它对你的工作、学习、生活进行规划,把你想做的事提上日程,画上截止日期。 11. 最近又有什么科技产品是你心痒痒想入的?一句话说明你为它找的理由。 Macbookpro 2020 当我听说mbp要出14寸并且采用剪刀键盘,那是真的爱了爱了 ! 12. 跟上面相反,有什么是你想强烈吐槽、狠狠劝退的么? 思前想后目前用过的产品里面都还算中规中矩,没有让我忍不住要去吐槽的,比较佛系。 13. 工欲善其事,必先利其器。无论是工作还是生活,有什么特好的“磨刀法子”是你巴不得大家都知道的? 合理规划、阶段总结 无论工作还是生活,我的建议就是规划和总结,想清楚自己需要什么,并为之制定计划,通过每个阶段的总结和复盘来审视自己,长此以往你会发现,虽然你只是每天完成了一小部分,但是却离自己的目标越来越近。 14. 科技生活离不开内容,它们可能是书、是游戏、是电影、是音乐,甚至是App。来,请分享你的最爱吧。 书籍类: 《代码大全》:程序员工程师必读 《三体》:世界是偶然的,也是必然的 影视类: 《粉雄救兵》(Queer Eye For The Straight Guy) :通过另一种方式帮助你重塑信心,保持自信 《请回答1988》: 直男力荐,一定会温暖到你的一部优秀作品 APP类: picsew :IOS下的长截屏工具 365 Dots:一款时间规划工具 15. You are what you eat. 你在网上fo过什么有意思的账号?求分享!(可按 有趣—>严肃 排序;平台不限于YouTube/Twitter/B站/微博/播客/公众号) github仓: 清华大学计算机系课程攻略:https://github.com/PKUanonym/REKCARC-TSC-UHT 仓库中涵盖了清华大学计算机系大一到大四的基础课程的电子版教材、课后习题、历年试卷、复习资料等,对于计算机相关基础知识的温习和回顾或者有考研意向的同学十分有帮助,同样类似的仓库还有:浙江大学课程攻略共享计划 以社会学年鉴模式体例规范地统编自2019年12月起武汉新冠肺炎疫情进展的时间线:https://github.com/Pratitya/wuhan2020-timeline 这一仓库收录了2020年新型冠状病毒肺炎相关的媒体报道和亲历者个人叙述,通过时间线的形式详细记录了COVID-19疫情进展的时间线。虽然只是事件记录型的仓库,但是贵在真实全面! 博客: 阮一峰的网络日志:http://www.ruanyifeng.com/blog/ 阮老师的科技爱好者周刊已经属于每周五必追的网络博客,通过每一期去了解海内外热点事件、最前沿的技术,以及最受欢迎的生产力工具。 我的博客小站:https://www.larscheng.com/ 通过文字记录和分享自己的生活,希望自己能一直坚持博客输出。 16. 如果要给别人一些美好生活的建议,你会说啥? 与人分享:我从毕业开始搭建了自己的博客小站,并坚持通过它来分享我的工作和生活,在这个过程中不仅加强了自己对所掌握知识的理解,同时也提高了自身的语言组织能力。 投资自己:自身的健康永远是第一位,保持强健的体魄是对未来的自己最好的投资! 17. 如果给你一张白板,可以在上面写上你想抗议的内容,你会写什么? 佛系少年暂时没有要抗议的,如果有那就去来一顿火锅解决问题。 18. #所有人问所有人# 轮到你问问题了!问一个你想问其他人的问题? 你是否想过与多年后的自己进行一次对话?

2020/4/20
articleCard.readMore

Nacos系列博客说明

前言 最近回顾之前写的Nacos系列博客和源码,其中关于Nacos的基础使用教程在现在的版本虽然依旧适用,但是当时使用的还是1.0.1版本的Nacos-Server,如今Nacos已经迭代到了1.2.0。而且当时的SpringCloud-Alibaba还处于孵化期,SCA毕业后整个的依赖都有了一些变化。再加上如今的Nacos也修复了许多问题增加了权限功能等。 之前的文章加源码可能给上手就是V1.2.0,或者更新版本的同学带来很多迷惑性,所以… 之前的源码项目仍旧保留,我又新开了一坑,用作记录你从来没有玩过的船新版本Naocs系列博客源码。 名称目录源码 基于Nacos:V1.0.1的九篇Nacos入门到避坑系列文章点击查看github v1.0.1分支 基于Nacos:V1.2.0的Nacos系列文章暂时还没写github master分支 源码项目:nacos-learning-samples 分支说明: v1.0.1:基于Nacos:V1.0.1的九篇博客源码(优化了依赖关系,以前是多级Maven父子工程,比较乱) master:基于v1.0.1源码进行了nacos依赖升级,使用最新版本的Nacos,以及后续文章源码都存放在此 后续文章 Nacos系列文章已经写了9篇啦,基本把Nacos如何使用已经介绍的七七八八,随着Nacos的迭代,以及一些老哥的留言,后期会陆陆续续更新Nacos-docker集群部署、Nacos使用过程中遇到的一些坑点 、Nacos新特性等。

2020/3/25
articleCard.readMore

菜鸡程序员的2019年度总结

碎碎念 写这篇总结时已经是2020年的第一天了。不得不感慨时间过得太快,记忆中的2019仿佛一片空白,但是细细回味之后,这一年许多人许多事都还历历在目。 2019年过去,2020的到来。第一批90后已经迈三奔四,同样作为90后的我不由得意识到自己的年龄,自己确实已经不小了。但一想到自己的现状,只能说:革命尚未成功,我辈仍需继续努力!!! 灵魂拷问: 时间回到2019年年初,你当时立下的flag,现在又完成了多少呢?(反正我是惨不忍睹…) 2019年是我毕业后的第一年,可能18年还能厚脸皮说自己还是个学生,但是19年已经正式从学生到求职者的角色中转变过来,但实际上论参加工作时间的话,到目前为止已经过去2年1个月了(2017.11),如果要是算年头的话,2020年已经是我成为程序员的第4个年头了,看着这些数据真的是细思极恐,再看看自己这个技术水平,哇不忍直视! 标题虽然写的是2019年度总结,但这也是我第一次写年度总结,我更想借此对自己从参加工作到现在的生活和工作进行梳理。整理过去的人和事,确定今后的方向(立flag),轻装上阵。 那闲话不多说,既然是总结,那就先对这过去的3个年头做一个简单的回溯吧 2017 网易云热评上看到过这样一句话: 跨越数千公里,只身来到一个陌生的城市,火车站凌晨昏黄的灯光照在脸上时大概是一个人最孤独的时候吧。 然鹅生活中谁又何尝不是负重前行,忍受孤独呢?我们都是平凡的普通人,所以你只管努力,其他的交给时间就好。 年度关键字 离家 萌新 新起点 努力 入职 2017.11那时候也算是初生牛犊不怕虎,10月底拿到offer,签三方、学校请假、买车票一套操作带走后,直接闪现杭州。凌晨4点下火车时才有种如梦初醒的感觉(我是谁?我在哪?我怎么跑这来了???),在工作人员的引导下坐上了第一班公交车,晃悠晃悠到了现在的公司(给工作人员点赞昂) 入职后来到一个新的环境,新的事物新的朋友。公司安排了员工宿舍,分配了导师。一周的接触后,现在想想真的很温馨,大家对我都很关照。记得那时上手的技术大部分都没用过。自己当时除了有点Java基础,框架懂点SSM,其他的就是一片空白,当时导师给我介绍公司的技术栈时真的有种听天书的感觉,有些又仿佛听过,当时的真实问答现场是这样的: 导师:springboot用过么? 我:…没…没用过,用过spring,springMvc(内心:面试咋没问到这个,这几个是不是有啥关系,名字这么像) 导师:那mybatisplus接触过吗? 我:…也没…,就用过mybatis 导师:没事两个很像,好上手 … 成长 11月就是一个接收新事物的一个月,学习到新的技术,认识到新的朋友。那段时间也许是出于自身的焦虑,也或许是作为实习生应该要有一个努力上进的态度,每晚都在办公室学习,造轮子很久,庆幸自己遇到了一个很好的导师,导师每天下班前都会习惯性的问我今天遇到了哪些问题,我也是厚脸皮将问题一一提出,导师则细心解答。 时间过得很快,每天过着充实的学习生活,当时就像一片海绵,疯狂的吸取自己缺少的那些水分。记得当时已经开始帮同事解决禅道上的一些小bug(当时还用的禅道haha),直到年底,12月底,项目组接到了一个新的项目,我也有幸成为了项目的一员。当时我也不会想到这个项目一作就做到了现在,而且做到现在最初的那些开发人员也就只剩下了我和导师,那些熟悉的面孔逐渐变成陌生又慢慢变为熟悉。 2018 一辈子有很多夏天,但没有任何一个夏天像今夏 2018年见了太多的别离,2018年也是毕业那年,记得那年夏天,记得那些人,四年美好的回忆 年度关键字 毕设 旅行 毕业 博客 项目 读书学习分享 springboot、springcloud linux、docker mongodb、redis、rabbitmq ci/cd 2018算是自己人生中历史性的一年吧,这一年正式毕业摇身一变成为一名社会银,这一年完成了自己的毕业作品,这一年和她一起去走过了毕业旅行,这一年正真开始了自己的程序生涯。 300块 毕业前夕伴随着是劳神的毕业设计,从2017年底,确定毕设题目后(《毕业设计选题系统》),以自己最熟悉的springboot+mybatisplus+mysql+redis等技术,利用周末和返校的时间独自开发完成,并将该系统接入院系内开始内部测试使用,将毕业生从选题到结题的所有流程实现自动化。 就这样折腾下来,自己有幸还被评选为优秀毕业生,最后还发了300块钱(虽然有点少,是真的少...哈哈哈嗝~) 后来据指导老师的反馈说,系统已经正式在全院开始使用。 成都 毕业旅行应该是许多同学都必须要实现的愿望之一,有幸、碰巧我也实现了这一愿望,闪现到了成都耍了一圈。 记得那年有首《成都》火遍了大街小巷,当时决定去成都前还特意自己规划了一晚上的游玩路线,实际后来没能用得上哈哈。 其实无论是学习还是工作,如果条件允许的话一定要多出去走走,看看这大好河山,尝尝各地美食。让自己有更好的精神面貌来迎接工作和生活 博客、分享 回到公司,我开始迷上了一个东西:博客 最初对于博客一直有敬畏之心,觉着能发表博客的都是大佬,都是业界大牛,这种想法直到我参加了一次小组的技术分享后发生了转变。 以前在学校虽然也组织一些活动,参加一些活动,但是那些都是在自己熟悉的同学老师面前,做一些自己擅长的事情。而作为一个职场新人技术小白组内的技术分享对我来说就像是关公面前耍大刀(这个比拟比较夸大哈哈哈~),记得当时分享的是quartz,当时做了很多准备,结合自己的使用经验,以及实践社区大佬的使用经验总结成书面的技术分享文章,后来分享效果还挺不错,起码把自己懂得,理解到的,用过的都分享了出来并做了总结。 通过那次的分享,逐渐明白完全掌握一个新知识新技术,并不是你会用就可以了,而是你能够将这个知识讲出来,教会别人,并能为其答疑解惑。 自此,博客成了我记录学习,总结知识的方法和途径。 程序生涯开启 自7月份后,程序生涯正式开启,进入打怪升级的模式,我是一个莫得感情的编码机器。 后半年陆续开始参与两个项目的开发工作,在有了之前的适应期,慢慢开始接触和掌握项目中的相关技术栈。 同时作为一个不安分的开发,在工作之余开始折腾持续集成持续部署,开始学习linux、docker、jenkins,好像走向了半道子运维的路线。 Java后端的世界广袤无垠,我也仅仅探索了冰山一角,每一个程序员都有相同的特点,比如都喜欢打篮球,比如都要实现架构师之梦,我也一样,心怀梦想在技术之路上不断向前。 2019 拒绝拖延症,拒绝拖延症,拒绝拖延症 2019年是生活重心都在工作的一年,这一年也是五味杂陈的一年,这一年暴露出了自身的很多潜藏问题和缺点。 年度关键字 整合 规范 基础 参与开源 Nacos 博客 以考代学 Mycat、DRDS influxDB 优秀员工 这一年的项目情况就不多作介绍,主要对自己项目工作以外的一些事件进行总结 五味杂陈 说到五味杂陈,离不开情之一字,哈哈哈,这种事其实不应该出现在程序员的年度总结中,但是键盘敲到这里,思绪划过2019一整年,真的是五味杂陈,异地恋的痛点也算是这一年不可缺的的一部分,经历了很多的争吵,产生过很多的矛盾。异地恋考验着双方的同时又需要两个人的坚持,好在争吵平息,矛盾化解。愿这些烦恼都随2019一起远去。 提高效率 经过1年的打怪升级,项目组内的开发套路和基本架构都已经熟悉,在对目前项目中在使用的延时任务进行了整合,并以微服务形式提供接口支持。防止出现重复造轮子的现象,提高开发效率。 随着业务的细分,项目中的微服务越来越多,原本的手动配置管理已经愈加繁杂,在调研了市面上的微服务配置管理解决方案后,选用了Nacos作为配置中心和注册中心。并接入正在开发中的项目。目前Nacos已经落地公司多个项目中。在多个微服务的配置管理上大大提高了效率 调研了部门中各个项目组的代码提交习惯后,发现一个问题:提交记录没有任何规范可言,于是乎,不安分的我开始率先在小组内推广规范式提交,规范小组内的提交格式,并且向整个部门安利。 返璞归真 刚入行的时候就有朋友给提点过:做这一行,到了最后拼的都是内功,话俗理不俗! 一门语言,语言的基础就像武侠小说中的内功心法,而那些框架,中间件等等就好比武功招式绝学。那些武林大侠个个都是内功深厚,就好比张无忌武功初上光明顶时武功平平,但他却因习得九阳神功这一绝世无双的内功心法而能力战群雄。 Java开发同样如此,其实最核心的还是Java基础,这一年基本上停止了新框架的学习,重心放在了基础知识的复习和深入。 考证 在9月份时报考了软件设计师考试,抱着以考代学的心态(因为自己基础太差),稳扎稳打,踏实复习,最终结果还算令人满意成功通过了考试。 这门考试最佳的报考时间应该是大三大四,我到现在才考完全是因为自己的拖延症,不过现在回头去复习,很多基础的知识点都有一种恍然大悟的感觉。也算是恶补了一波基础知识。 虽然目前大众对这个证书不是太感冒,但是存在即合理,通过复习确实可以梳理计算机相关基础知识,同时可以系统的了解计算机相关知识点。如果你有兴趣不妨一试哟。 博客 博客输出已经成为一种习惯,虽然我的更新频率十分不规律,而且技术深度也一般,但是相信自己只要坚持输出,终会有所收获。 写博客已经不单单是记录和总结了,就像能用嘴巴说出来的东西,不一定能用文字清楚的表达。而写博客更多的是分享和文档撰写、信息归纳汇总的锻炼,就像锻炼一个人的文字表达能力一样。 今年最满意的是将自己在公司使用Nacos的历程记录成系列博客,虽然只是如何使用如何接入的基础性文章,但是也收到很多热心读者的信息反馈。 目前博客的访问人数也刚刚突破6000+,双十一的时候趁着有活动一次性给自己的域名续费了5年,这一年我也在不断的调整自己的写作风格,力求能够用简单朴实的文字将技术点清晰明了的传递给每一位读者。 架构升级 随着业务需求的升级,目前在用的部分架构和数据库解决方案已经不足以支撑后续业务,有幸参与到目前项目组的数据库解决方案升级计划中,开始负责TSDB和DRDS的调研和落地使用。 目前已经基本完成了TSDB的接入工作,后续会重点关注DRDS的相关调研和接入工作。 暴露出的问题 前面都是在总结一些自己的阶段性成果和大事件,但对于过程中暴露出额自身问题却只字未提,这里想把他们单拎出来,警示自己: 未能时常关心到家人 -----> 做到一周最少一次联系(特别好奇是不是男生都和家里联系少?还是就我是这样?总之这样是不对的) 重度拖延症 -----> 为自己制定计划表,和奖惩措施 负能量多于正能量 -----> 调整心态,微笑面对生活 读书太少 -----> 提升内涵,多读书,读好书 运动太少 -----> 加强锻炼,控制体重(毕业到现在已经从120斤胖到140斤......惨不忍睹) 熬夜太多 -----> 早睡早起,狗命重要!!! 典型肥宅 -----> 重拾兴趣爱好,充实业务生活 多的flag着实不想立了,不想来年打脸,但以上这些已经列出来的问题点,是必须优先解决的。 2020 逆水行舟不进则退,长期处于舒适区只会习惯于停留在原地 最后的这一节,就像小时候写新学期打算一样,要求新的一年做出展望和规划,也是必不可少的。 新的一年当但是希望自己测试通过,全年无bug啦,哈哈哈 新的一年也算是一个新的起点,希望自己初心不改,保持现在的节奏稳扎稳打,努力提升自己,变得更优秀。 新的一年像上面提到的,要警惕舒适区,时时刻刻,必须要提醒自己,不能懈怠,懈怠你就破功了老弟!

2020/1/1
articleCard.readMore

Java中“附近的人”实现方案讨论及代码实现

前言 在我们平时使用的许多app中有附近的人这一功能,像微信、qq附近的人,哈罗、街兔附近的车辆。这些功能就在我们日常生活中出现。 像类似于附近的人这一类业务,在Java中是如何实现的呢? 本文就简单介绍下目前的几种解决方案,并提供简单的示例代码 注: 本文仅涉及附近的人这一业务场景的解决方案讨论,并未涉及到相关的技术细节和方案优化,各位看官可以放心阅读。 基本套路和方案 目前业内的解决方案大都依据geoHash展开,考虑到不同的数据量以及不同的业务场景,本文主要讨论以下3种方案 Mysql+外接正方形 Mysql+geohash Redis+geohash Mysql+外接正方形 外接矩形的实现方式是相对较为简单的一种方式。 假设给定某用户的位置坐标, 求在该用户指定范围内的其他用户信息 此时可以将位置信息和距离范围简化成平面几何题来求解 实现思路 以当前用户为圆心,以给定距离为半径画圆,那么在这个圆内的所有用户信息就是符合结果的信息,直接检索圆内的用户坐标难以实现,我们可以通过获取这个圆的外接正方形。 通过外接正方形,获取经度和纬度的最大最小值,根据最大最小值可以将坐标在正方形内的用户信息搜索出来。 此时在外接正方形中不属于圆形区域的部分就属于多余的部分,这部分用户信息距离当前用户(圆心)的距离必定是大于给定半径的,故可以将其剔除,最终获得指定范围内的附近的人 代码实现 这里只贴出部分核心代码,详细的代码可见源码:NearBySearch 在实现附近的人搜索中,需要根据位置经纬度点,进行一些距离和范围的计算,比如求球面外接正方形的坐标点,球面两坐标点的距离等,可以引入Spatial4j库。 1 2 3 4 5 <dependency> <groupId>com.spatial4j</groupId> <artifactId>spatial4j</artifactId> <version>0.5</version> </dependency> 首先创建一张数据表user 1 2 3 4 5 6 7 8 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '名称', `longitude` double DEFAULT NULL COMMENT '经度', `latitude` double DEFAULT NULL COMMENT '纬度', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 假设已插入足够的测试数据,只要我们获取到外接正方形的四个关键点,就可以直接直接查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private SpatialContext spatialContext = SpatialContext.GEO; /** * 获取附近x米的人 * * @param distance 距离范围 单位km * @param userLng 当前经度 * @param userLat 当前纬度 * @return json */ @GetMapping("/nearby") public String nearBySearch(@RequestParam("distance") double distance, @RequestParam("userLng") double userLng, @RequestParam("userLat") double userLat) { //1.获取外接正方形 Rectangle rectangle = getRectangle(distance, userLng, userLat); //2.获取位置在正方形内的所有用户 List<User> users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY()); //3.剔除半径超过指定距离的多余用户 users = users.stream() .filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance) .collect(Collectors.toList()); return JSON.toJSONString(users); } private Rectangle getRectangle(double distance, double userLng, double userLat) { return spatialContext.getDistCalc() .calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat), distance * DistanceUtils.KM_TO_DEG, spatialContext, null); } 这里给出查询的sql 1 2 3 4 5 6 <select id="selectUser" resultMap="BaseResultMap"> SELECT * FROM user WHERE 1=1 and (longitude BETWEEN ${minlng} AND ${maxlng}) and (latitude BETWEEN ${minlat} AND ${maxlat}) </select> Mysql+geohash 前面介绍了通过Mysql存储用户的信息和gps坐标,通过计算外接正方形的坐标点来粗略筛选结果集,最终剔除超过范围的用户。 而现在要提到的Mysql+geohash方案,同样是以Mysql为基础,只不过引入了geohash算法,同时在查询上借助索引。 geohash被广泛应用于位置搜索类的业务中,本文不对它进行展开说明,有兴趣的同学可以看一下这篇博客:[GeoHash核心原理解析],这里简单对它做一个描述: GeoHash算法将经纬度坐标点编码成一个字符串,距离越近的坐标,转换后的geohash字符串越相似,例如下表数据: 用户经纬度Geohash字符串 小明116.402843,39.999375wx4g8c9v 小华116.3967,39.99932wx4g89tk 小张116.40382,39.918118wx4g0ffe 其中根据经纬度计算得到的geohash字符串,不同精度(字符串长度)代表了不同的距离误差。具体的不同精度的距离误差可参考下表: geohash码长度宽度高度 15,009.4km4,992.6km 21,252.3km624.1km 3156.5km156km 439.1km19.5km 54.9km4.9km 61.2km609.4m 7152.9m152.4m 838.2m19m 94.8m4.8m 101.2m59.5cm 1114.9cm14.9cm 123.7cm1.9cm 实现思路 使用Mysql存储用户信息,其中包括用户的经纬度信息和geohash字符串。 添加新用户时计算该用户的geohash字符串,并存储到用户表中 当要查询某一gps附近指定距离的用户信息时,通过比对geohash误差表确定需要的geohash字符串精度 计算获得某一精度的当前坐标的geohash字符串,通过WHERE geohash Like 'geohashcode%'来查询数据集 如果geohash字符串的精度远大于给定的距离范围时,查询出的结果集中必然存在在范围之外的数据 计算两点之间距离,对于超出距离的数据进行剔除。 代码实现 这里只贴出部分核心代码,详细的代码可见源码:NearBySearch 同样的要涉及到坐标点的计算和geohash的计算,开始之前先导入spatial4j 创建数据表user_geohash,给geohash码添加索引 1 2 3 4 5 6 7 8 9 10 CREATE TABLE `user_geohash` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '名称', `longitude` double DEFAULT NULL COMMENT '经度', `latitude` double DEFAULT NULL COMMENT '纬度', `geo_code` varchar(64) DEFAULT NULL COMMENT '经纬度所计算的geohash码', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `index_geo_hash` (`geo_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 添加用户信息和范围搜索逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 private SpatialContext spatialContext = SpatialContext.GEO; /*** * 添加用户 * @return */ @PostMapping("/addUser") public boolean add(@RequestBody UserGeohash user) { //默认精度12位 String geoHashCode = GeohashUtils.encodeLatLon(user.getLatitude(),user.getLongitude()); return userGeohashService.save(user.setGeoCode(geoHashCode).setCreateTime(LocalDateTime.now())); } /** * 获取附近指定范围的人 * * @param distance 距离范围 单位km * @param len geoHash的精度 * @param userLng 当前经度 * @param userLat 当前纬度 * @return json */ @GetMapping("/nearby") public String nearBySearch(@RequestParam("distance") double distance, @RequestParam("len") int len, @RequestParam("userLng") double userLng, @RequestParam("userLat") double userLat) { //1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码 String geoHashCode = GeohashUtils.encodeLatLon(userLat, userLng, len); QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>() .likeRight("geo_code",geoHashCode); //2.匹配指定精度的geoHash码 List<UserGeohash> users = userGeohashService.list(queryWrapper); //3.过滤超出距离的 users = users.stream() .filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance) .collect(Collectors.toList()); return JSON.toJSONString(users); } /*** * 球面中,两点间的距离 * @param longitude 经度1 * @param latitude 纬度1 * @param userLng 经度2 * @param userLat 纬度2 * @return 返回距离,单位km */ private double getDistance(Double longitude, Double latitude, double userLng, double userLat) { return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat), spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM; } 通过上面几步,就可以实现这一业务场景,不仅提高了查询效率,并且保护了用户的隐私,不对外暴露坐标位置。并且对于同一位置的频繁请求,如果是同一个geohash字符串,可以加上缓存,减缓数据库的压力。 边界问题优化 geohash算法将地图分为一个个矩形,对每个矩形进行编码,得到geohash码,但是当前点与待搜索点距离很近但是恰好在两个区域,用上面的方法则就不适用了。 解决这一问题的办法:获取当前点所在区域附近的8个区域的geohash码,一并进行筛选。 如何求解附近的8个区域的geohash码可参考Geohash求当前区域周围8个区域编码的一种思路 了解了思路,这里我们可以使用第三方开源库ch.hsr.geohash来计算,通过maven引入 1 2 3 4 5 <dependency> <groupId>ch.hsr</groupId> <artifactId>geohash</artifactId> <version>1.0.10</version> </dependency> 对上一章节的nearBySearch方法进行修改如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /** * 获取附近指定范围的人 * * @param distance 距离范围 单位km * @param len geoHash的精度 * @param userLng 当前经度 * @param userLat 当前纬度 * @return json */ @GetMapping("/nearby") public String nearBySearch(@RequestParam("distance") double distance, @RequestParam("len") int len, @RequestParam("userLng") double userLng, @RequestParam("userLat") double userLat) { //1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码 GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len); //2.获取到用户周边8个方位的geoHash码 GeoHash[] adjacent = geoHash.getAdjacent(); QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>() .likeRight("geo_code",geoHash.toBase32()); Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("geo_code",a.toBase32())); //3.匹配指定精度的geoHash码 List<UserGeohash> users = userGeohashService.list(queryWrapper); //4.过滤超出距离的 users = users.stream() .filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance) .collect(Collectors.toList()); return JSON.toJSONString(users); } Redis+GeoHash 基于前两种方案,我们可以发现gps这类数据属于读多写少的情况,如果使用redis来实现附近的人,想必效率会大大提高。 自Redis 3.2开始,Redis基于geohash和有序集合Zset提供了地理位置相关功能 Redis提供6条命令,来帮助我们我完成大部分业务的需求,关于Redis提供的geohash操作命令介绍可阅读博客:Redis 到底是怎么实现“附近的人”这个功能的呢? 本文主要介绍下,我们示例代码中用到的两个命令: GEOADD key longitude latitude member:将给定的空间元素(纬度、经度、名字)添加到指定的键里面 例如添加小明的经纬度信息:GEOADD location 119.9886618073271630.27465803229662 小明 GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]: 根据给定地理位置坐标获取指定范围内的地理位置集合(附近的人) 例如查询某gps附近500m的用户坐标:GEORADIUS location 119.9886618073271630.27465803229662 500 m WITHCOORD 实现思路 添加用户坐标信息到redis(GEOADD),redis会将经纬度参数值转换为52位的geohash码, Redis以geohash码为score,将其他信息以Zset有序集合存入key中 通过调用GEORADIUS命令,获取指定坐标点某一范围内的数据 因geohash存在精度误差,剔除超过指定距离的数据 实现代码 这里只贴出部分核心代码,详细的代码可见源码:NearBySearch 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Autowired private RedisTemplate<String, Object> redisTemplate; //GEO相关命令用到的KEY private final static String KEY = "user_info"; public boolean save(User user) { Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>( user.getName(), new Point(user.getLongitude(), user.getLatitude())) ); return flag != null && flag > 0; } /** * 根据当前位置获取附近指定范围内的用户 * @param distance 指定范围 单位km ,可根据{@link org.springframework.data.geo.Metrics} 进行设置 * @param userLng 用户经度 * @param userLat 用户纬度 * @return */ public String nearBySearch(double distance, double userLng, double userLat) { List<User> users = new ArrayList<>(); // 1.GEORADIUS获取附近范围内的信息 GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut = redisTemplate.opsForGeo().radius(KEY, new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)), RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs() .includeDistance() .includeCoordinates().sortAscending()); //2.收集信息,存入list List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent(); //3.过滤掉超过距离的数据 content.forEach(a-> users.add( new User().setDistance(a.getDistance().getValue()) .setLatitude(a.getContent().getPoint().getX()) .setLongitude(a.getContent().getPoint().getY()))); return JSON.toJSONString(users); } 方案总结 方案优势缺点 Mysql外接正方形逻辑清晰,实现简单,支持多条件筛选效率较低,不适合大数据量,不支持按距离排序 Mysql+Geohash借助索引有效提高效率,支持多条件筛选不支持按距离排序,存在数据库瓶颈 Redis+Geohash效率高,集成便捷,支持距离排序不适合复杂对象存储,不支持多条件查询 总结以上三种方案,各有优劣,在不同的业务场景下,可选择不同的方案来实现。 当然目前附近的人的解决方案并不仅仅这三种,以上权当是这一功能的入门引子,希望对大家有所帮助。 本文的三种方案均有源码提供,源码地址 参考文章 Redis 到底是怎么实现“附近的人”这个功能的呢? Geohash求当前区域周围8个区域编码的一种思路 GeoHash核心原理解析

2019/12/18
articleCard.readMore

Java时间处理2----时区TimeZone类方法探究(Java8以前)

本文转载于CSDN博主「Gene Xu」 原文链接:https://blog.csdn.net/goodbye_youth/article/details/81807273 一、TimeZone 类的定义 TimeZone 类位于 java.util 包中,是一个抽象类,主要包含了对于时区的各种操作,可以进行计算时间偏移量或夏令时等操作 二、TimeZone 类常用方法 getAvailableIDs() 获取Java支持的所有时区 ID 1 2 3 System.out.println(Arrays.toString(TimeZone.getAvailableIDs())); // Asia/Shanghai, Asia/Chongqing, Asia/Hong_Kong, Asia/Macao, ... getAvailableIDs(int rawOffset) 根据 时间偏移量 来获取时区 ID 1 2 3 4 5 6 7 8 // 东八区时间,与标准时间相差8小时 System.out.println(Arrays.toString(TimeZone.getAvailableIDs(8*60*60*1000))); // [Asia/Brunei, Asia/Choibalsan, Asia/Chongqing, Asia/Chungking, Asia/Harbin, // Asia/Hong_Kong, Asia/Irkutsk, Asia/Kuala_Lumpur, Asia/Kuching, Asia/Macao, // Asia/Macau, Asia/Makassar, Asia/Manila, Asia/Shanghai, Asia/Singapore, // Asia/Taipei, Asia/Ujung_Pandang, Asia/Ulaanbaatar, Asia/Ulan_Bator, // Australia/Perth, Australia/West, CTT, Etc/GMT-8, Hongkong, PRC, Singapore] getDefault() 获取当前系统的默认时区,中国默认为东八区 1 2 3 4 System.out.println(TimeZone.getDefault()); // sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000, // dstSavings=0,useDaylight=false,transitions=0,lastRule=null] setDefault(TimeZone zone) 设置当前系统的默认时区 1 2 3 4 5 TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); System.out.println(TimeZone.getDefault()); // sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000, // dstSavings=0,useDaylight=false,transitions=19,lastRule=null] getTimeZone(String ID) 根据时区 ID 来获取其对应的时区 1 2 3 4 System.out.println(TimeZone.getTimeZone("GMT+08:00")); // sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000, // dstSavings=0,useDaylight=false,transitions=0,lastRule=null] getTimeZone(ZoneId zoneId) 根据 ZoneId 对象来获取其对应的时区 1 2 3 4 System.out.println(TimeZone.getTimeZone(ZoneId.of("GMT+08:00"))); // sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000, // dstSavings=0,useDaylight=false,transitions=0,lastRule=null] getDisplayName() 获取该 TimeZone 对象的时区名称 1 2 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.getDisplayName()); // 中国标准时间 getDisplayName(Locale locale) 获取该 TimeZone 对象的时区名称,并根据 Locale 对象进行国际化 1 2 3 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.getDisplayName()); // 中国标准时间 System.out.println(timeZone.getDisplayName(Locale.ENGLISH)); // China Standard Time getDisplayName(boolean daylight, int style) 获取该 TimeZone 对象的时区名称 daylight true:指定夏令时名称 false:指定标准时间名称 style TimeZone.LONG:显示全称 TimeZone.SHORT:显示简称 1 2 3 4 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.getDisplayName()); // 中国标准时间 System.out.println(timeZone.getDisplayName(false, TimeZone.LONG)); // 中国标准时间 System.out.println(timeZone.getDisplayName(false, TimeZone.SHORT)); // CST (China Standard Time) getDisplayName(boolean daylight, int style, Locale locale) 获取该 TimeZone 对象的时区名称,并根据 Locale 对象进行国际化 daylight true:指定夏令时名称 false:指定标准时间名称 style TimeZone.LONG:显示全称 TimeZone.SHORT:显示简称 1 2 3 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.getDisplayName()); // 中国标准时间 System.out.println(timeZone.getDisplayName(false, TimeZone.LONG, Locale.ENGLISH)); // China Standard Time getID() 获取该 TimeZone 对象的时区 ID 1 2 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.getID()); // Asia/Shanghai setID(String ID) 设置该 TimeZone 对象的时区 ID 1 2 3 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); timeZone.setID("Asia/Chongqing"); System.out.println(timeZone.getID()); // Asia/Chongqing getOffset(long date) 获取该时间所在时区的时间偏移量 1 2 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.getOffset(System.currentTimeMillis())); // 28800000 getDSTSavings() 在夏令时规则生效时,返回相对于标准时间提前的毫秒数 如果此时区不实施夏令时,则为 0 1 2 3 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); // 中国没有夏令时,故为0 System.out.println(timeZone.getDSTSavings()); // 0 getRawOffset() 获取时间原始偏移量,该值不受夏令时的影响,故称为时间原始偏移量 1 2 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.getRawOffset()); // 28800000 setRawOffset(int offsetMillis) 设置时间原始偏移量 1 2 3 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); timeZone.setRawOffset(25200000); System.out.println(timeZone.getRawOffset()); // 25200000 toZoneId() 将 TimeZone 对象转换为 ZoneId 对象 1 2 3 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); ZoneId zoneId = timeZone.toZoneId(); System.out.println(zoneId); // Asia/Shanghai useDaylightTime() 查询此时区是否使用夏令时 1 2 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.useDaylightTime()); // false inDaylightTime(Date date) 查询给定的日期是否在此时区的夏令时中 1 2 TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai"); System.out.println(timeZone.inDaylightTime(new Date())); // false hasSameRules(TimeZone other) 如果两时区仅时区 ID 不同,但具有相同的规则和时间偏移量,则返回 true 如果另一个时区为空,则返回 false 1 2 3 TimeZone timeZone1 = TimeZone.getTimeZone("Asia/Shanghai"); TimeZone timeZone2 = TimeZone.getTimeZone("Asia/Chongqing"); System.out.println(timeZone1.hasSameRules(timeZone2)); // true

2019/11/22
articleCard.readMore

Java时间处理1----Date和Calendar方法探究(Java8以前)

前言 时间的处理在Java中会经常用到,Java中常用的时间处理类有如下两种: Date: 日期类 用来表示瞬时时间,精确到毫秒,他表示的是某一刹那的时间。Date不存在时区概念。 由于Date类不易于实现国际化,所以逐渐不被推荐使用,并且废弃了大部分方法。 Calendar: 日历类,可以理解为计算机历 一方面是为了弥补Date的国际化缺陷,另一方面是将时间以日历的形式来表示和处理 Calendar包含时区和语言的概念 Calendar将时间细分成了年月日时分秒毫秒上午下午夏时令等概念,并提供这些概念的相关操作方法。 下面我们来康康这两个工具类的一些常用方法。 Date 📅 Date类位于java.util包下,是JDK中最早的时间处理类,但由于Date类不易于实现国际化,后不被推荐使用。 Date源自于JDK1.0,到如今Date类中的大部分构造函数和方法都已经废弃不推荐使用(Deprecated) 目前还在使用的方法如下: 方法 : 返回值参数说明功能 Date()构造器当前时间的Date对象 Date(long date)构造器,date:与GMT1970年1月1日00:00:00之间的时间差(ms)创建指定时间的Date对象 after(Date when):booleanwhen:待判断的Date对象某一Date是否在when之后 before(Date when):booleanwhen:待判断的Date对象某一Date是否在when之前 compareTo(Date anotherDate):intanotherDate:待比较的Date对象Date对象与参数中的Date进行比较 , a.compareTo(b):a>b:正数, a=b:0,a<b:负数 getTime():long返回Date对象的时间戳 setTime(long time):voidtime:时间戳设置Date对象的时间 Date对象的使用示例如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.Date; /** * 描述: * Date的常用方式 * * @author larscheng * @date 2019/11/21 13:46 */ public class DateTest { public static void main(String[] args) { Date date1 = new Date(); Date date2 = new Date(System.currentTimeMillis()+1000); System.out.println("date1: "+date1);//date1: Thu Nov 21 13:54:52 CST 2019 System.out.println("date2: "+date2);//date2: Thu Nov 21 13:54:53 CST 2019 System.out.println("date1.compareTo(date2): "+date1.compareTo(date2));//-1 System.out.println("date1.after(date2): "+date1.after(date2));//false System.out.println("date1.before(date2): "+date1.before(date2));//true System.out.println("date1.getTime(): "+date1.getTime());//1574315692607 date1.setTime(System.currentTimeMillis()); System.out.println("setTime() ==> date1.getTime(): "+date1.getTime());//1574315692628 } } Calendar 📆 在JDK1.1后,处理时间时,推荐使用Calendar类,Calendar包含Date中所有的功能,并且比Date更复杂更强大。 Calendar是一个抽象类,不可以直接实例化它,但可以通过他提供的getInstance方法进行创建 Calendar类在Date原有功能基础上,加强了对时间的处理和自定义等。其常用方法如下: 方法 : 返回值参数说明功能 getInstance():Calendar对外提供Calendar的创建入口 (该方法还有两个重载方法, 主要用于自定义时区,语言环境) after(Object when):booleanwhen:待判断的Calendar对象某一Date是否在when之后 before(Object when):booleanwhen:待判断的Calendar对象某一Date是否在when之前 compareTo(Calendar a):inta:待比较的Calendar对象Date对象与参数中的Date进行比较 , a.compareTo(b):a>b:正数, a=b:0,a<b:负数 getTime():Date将Calendar日期对象转换为Date对象 setTime(Date date):voiddate:Date对象将Date对象表示的时间值设置给Calendar日期对象 getTimeInMillis():long返回Calendar日期对象的时间戳 setTimeInMillis(long millis):voidmillis:时间戳用给定long值设置日历的当前时间 get(int field):intfield:日历字段(Calendar.YEAR)获取指定日历字段的值 set(int field, int val):voidfield:日历字段,val:具体值设置日历字段的值 add(int field, int amount):voidfield:日历字段,amount:加减值基于日历的规则实现日期加减。amout可为负数 roll(int field, boolean up):voidfield:日历字段,up:T加/F减在指定日历字段上添加或减去1个时间单元 roll(int field, int amount):voidfield:日历字段,amount:加减值在指定日历字段上添加或减去指定的时间单元 以上仅仅是Calendar类中的一部分常用方法,更多详细的方法大家可以参考源码 Calendar类是基于日历的时间处理类,在使用他之前需要注意下面一些小细节: Calendar.MONTH: 初始值为0,最大值11,所以在格式化时间时记得+1 在使用set方法时,Calendar.HOUR为12小时制,Calendar.HOUR_OF_DAY为24小时制 add与roll都可以实现指定字段的加减,两者的区别在于add会进位而roll不会,例子如下: 1 2 3 4 5 6 7 8 //add与roll的区别 SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Calendar ca1 = Calendar.getInstance();//2019-11-21 16:24:31 Calendar ca2 = Calendar.getInstance();//2019-11-21 16:24:31 ca1.add(Calendar.MONTH,3);//add加3个月 ca2.roll(Calendar.MONTH,3);//roll加3个月 System.out.println(sf.format(ca1.getTime()));//进位:2020-02-21 16:24:31 System.out.println(sf.format(ca2.getTime()));//不进位:2019-02-21 16:24:31 Calendar类的常用方法使用示例如下: add 1 2 3 4 5 6 7 8 9 //add:基于日历规则实现日期加减 System.out.println("#################### add #####################"); Calendar cal = Calendar.getInstance(); System.out.println("当前月份:"+(cal.get(Calendar.MONTH)+1)); cal.add(Calendar.MONTH,2); System.out.println("加上2个月:"+(cal.get(Calendar.MONTH)+1)); cal.add(Calendar.MONTH,-2); System.out.println("再加上-2个月:"+(cal.get(Calendar.MONTH)+1)); set 1 2 3 4 5 6 7 //set:设置日历字段的值 System.out.println("#################### set #####################"); Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR,2018); System.out.println("修改年份后:"+calendar.get(Calendar.YEAR)); calendar.set(2018, Calendar.MAY,13,15,1,11); System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime())); get 1 2 3 4 5 6 7 8 //get:返回指定日历字段的值 System.out.println("#################### get #####################"); System.out.println("当前日期:"+calendar.get(Calendar.YEAR)+"-"+calendar.get(Calendar.MONTH) +"-"+calendar.get(Calendar.DATE)); System.out.println("当前时间:"+calendar.get(Calendar.HOUR_OF_DAY)+":"+calendar.get(Calendar.MINUTE) +":"+calendar.get(Calendar.SECOND)+":"+calendar.get(Calendar.MILLISECOND)); System.out.println("12小时制:"+calendar.get(Calendar.HOUR)); System.out.println("24小时制:"+calendar.get(Calendar.HOUR_OF_DAY)); 总结 本文主要对时间处理类进行了方法梳理和说明,实际的使用中还会掺杂着时区和时间格式化等操作。 下篇文章我们接着来了解下Java中时区类TimeZone的基本使用。

2019/11/21
articleCard.readMore

FastJson中JSONString、JavaBean、JSONObject、JSONArray的转换关系及API示例

前言 JSON作为一种轻量级的数据交换格式,在我们日常的开发中使用十分广泛,就Java后端的开发工作中,JSON字符串与Java对象之间相互转换是常常遇到的操作。 虽然平时用到的挺多的,但是因为用于JSON处理的包有很多种,每种工具集的功能和使用方式也都不同,很容易在使用时造成混乱。 本文就结合FastJson部分源码,简单整理了下常用的API及其使用示例 本文FastJson版本:1.2.54 转换图 根据FastJson源码大致整理出了这么一张转换图: 可以看到参与转换的对象主要有图中五种: JSONString:json字符串 JSONObject:json对象 JSONArray:json对象数组 JavaBean:java对象 List:java对象集合 转化中用到的方法的方法名有如下几种: parse: JSONString ==> JSONObject/JSONArray parseObject: JSONString ==> JSONObject/JavaBean pareseArray: JSONString ==> JSONObject/List<JavaBean> toJSONString: JavaBean/JSONObject ==> JSONString toJSON: JavaBean ==> JSONObject toJavaObject:JSONObject ==> JavaBean 常用API 本文种仅列举平常使用频率较高的API,其他的重载方法可参考源码,大都是对序列化/反序列化过程进行定制化。 toJSONString 实现了json对象(JSONObject)>json字符串(JSONString),和Java对象(JavaBean)>json字符串(JSONString)的转化 从源码中可以看到这一方法被重载了多个,我们日常会用到的有如下几个: 方法 : 返回值参数说明功能 toJSONString(Object object):Stringobject: 需要进行序列化的对象javaBean或者JSONObject将对象序列化为json字符串 toJSONString(Object object, boolean prettyFormat):StringprettyFormat:是否格式化输出json字符串格式化输出json字符串 toJSONString(Object object, SerializerFeature… features):Stringfeatures:序列化额外属性配置,非必填根据指定属性进行序列化 toJSONStringWithDateFormat(Object object, String dateFormat, SerializerFeature… features):StringdateFormat:日期格式(yyyy-MM-dd)序列化时格式化日期 这些方法中最常用的即为:toJSONString(Object object) parse 实现了json字符串(JSONString)>json对象(JSONObject),和json字符串(JSONString)>json对象数组(JSONArray)的转化 方法 : 返回值参数说明功能 parse(String text):Objecttext:json字符串反序列化json字符串 parseObject 实现了json字符串(JSONString)>json对象(JSONObject),和json字符串(JSONString)>Java对象(JavaBean)的转化 方法 : 返回值参数说明功能 parseObject(String text):JSONObjecttext:json字符串反序列化json字符串为Json对象 parseObject(String text, Class clazz):Tclazz:指定反序列化后的类json字符串转java对象 parseObject(String text, TypeReference type, Feature… features):Ttype:构造转化类型,features:反序列化额外属性json字符串转java对象 parseArray 实现了json字符串(JSONString)==>json对象数组(JSONArray),和json字符串(JSONString)==>Java对象集合(List`)的转化 方法 : 返回值参数说明功能 parseArray(String text) :JSONArraytext:json字符串将json字符串反序列化为JSON数组对象 parseArray(String text, Class clazz):Listclazz:指定转化后的类将json字符串反序列化为java对象集合 toJSON/toJavaObject toJSON()实现了Java对象(JavaBean)==>Json对象(JSONObject)的转换 toJavaObject()实现了Json对象(JSONObject)==>Java对象(JavaBean)的转换 方法 : 返回值参数说明功能 toJSON(Object javaObject):ObjectjavaObject:java对象java对象转化为Json对象 toJavaObject(JSON json, Class clazz):Tjson:json对象,clazz:要转化的类型json对象转化为java对象 代码示例 Student学生类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.larscheng.www.jsontest; import lombok.AllArgsConstructor; import lombok.Data; import java.util.Date; /** * 描述: * 学生类 * * @author larscheng * @date 2019/11/19 19:33 */ @Data @AllArgsConstructor public class Student { private String name; private int age; private Date birthday; } 测试类FastJsonTest.java代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 package com.larscheng.www.jsontest.fastJson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; import com.alibaba.fastjson.serializer.SerializerFeature; import com.larscheng.www.jsontest.Course; import com.larscheng.www.jsontest.Student; import java.util.Arrays; import java.util.Date; import java.util.List; /** * 描述: * fastJson的api示例 * * @author larscheng * @date 2019/11/19 19:37 */ public class FastJsonTest { private final static Student LIMING = new Student("liming", 20, new Date()); private final static String LIMING_STR = "{'age':20,'birthday':1574163958480,'name':'liming'}"; private final static Course MATH = new Course("数学课", "高等代数"); private final static Course CHINESE = new Course("语文课", "大学语文"); private final static List<Course> COURSES = Arrays.asList(MATH, CHINESE); private final static String COURSES_STR = "[{'desc':'高等代数','name':'数学课'},{'desc':'大学语文','name':'语文课'}]"; private final static JSONObject LIMING_MAP = new JSONObject(); static { LIMING_MAP.put("name", "liming"); LIMING_MAP.put("age", 20); LIMING_MAP.put("birthday", new Date()); } public static void main(String[] args) { //############ toJSONString ############### /*JavaBean--->JSONString*/ System.err.println("JavaBean--->JSONString(默认无格式):"); System.out.println(JSON.toJSONString(LIMING)); System.err.println("JavaBean--->JSONString(带格式):"); System.out.println(JSON.toJSONString(LIMING, true)); System.err.println("JavaBean--->JSONString(日期格式化):"); System.out.println(JSON.toJSONStringWithDateFormat(LIMING, "yyyy-MM-dd") + "\n"); /*JSONObject--->JSONString*/ System.err.println("JSONObject--->JSONString(带格式):"); System.out.println(JSON.toJSONString(LIMING_MAP, true) + "\n"); /*List<JavaBean>--->JSONString*/ System.err.println("List<JavaBean>--->JSONString(默认双引号):"); System.out.println(JSON.toJSONString(COURSES)); System.err.println("List<JavaBean>--->JSONString(单引号):"); System.out.println(JSON.toJSONString(COURSES, SerializerFeature.UseSingleQuotes)); System.err.println("List<JavaBean>--->JSONString(单引号+带格式):"); System.out.println(JSON.toJSONString(COURSES, SerializerFeature.UseSingleQuotes,SerializerFeature.PrettyFormat) + "\n"); //########## parse/parseObject ################### /*JSONString--->JSONObject*/ System.err.println("JSONString--->JSONObject(parse):"); JSONObject jsonObject1 = (JSONObject) JSON.parse(LIMING_STR); System.out.println(jsonObject1.toString()); System.err.println("JSONString--->JSONObject(parseObject):"); System.out.println(JSON.parseObject(LIMING_STR).toString() + "\n"); System.err.println("JSONString--->JavaBean:"); Student student1 = JSON.parseObject(LIMING_STR,Student.class); System.out.println(student1.hashCode()+"\t"+student1.toString()); System.err.println("JSONString--->JavaBean:"); Student student2 = JSON.parseObject(LIMING_STR,new TypeReference<Student>(){}); System.out.println(student2.hashCode()+"\t"+student2.toString()); //########### parse/parseArray ################ /*JSONString--->JSONArray*/ System.err.println("JSONString--->JSONArray(parse):"); JSONArray jsonArray1 = (JSONArray) JSON.parse(COURSES_STR); System.out.println(jsonArray1.toString()); System.err.println("JSONString--->JSONArray(parseArray):"); System.out.println(JSON.parseArray(COURSES_STR).toString()); System.err.println("JSONString--->List<JavaBean>:"); List<Course> courses1 = JSON.parseArray(COURSES_STR,Course.class); System.out.println(courses1.hashCode()+"\t"+courses1.toString()+"\n"); //######### toJSON/toJavaObject ################ System.err.println("JavaBean--->JSONObject:"); System.out.println(JSON.toJSON(LIMING)); System.err.println("JSONObject--->JavaBean:"); System.out.println(JSON.toJavaObject(LIMING_MAP,Student.class)); System.out.println(LIMING_MAP.toJavaObject(Student.class)); System.out.println((Student)LIMING_MAP.toJavaObject(new TypeReference<Student>(){})); System.out.println(LIMING_MAP.toJavaObject(new TypeReference<Student>(){}.getType())+"\n"); } } 总结 基本常用的方法都进行了代码测试,使用过程中可能会出现混淆的情况,但是只要记住了文中的转换图,相信应该会加深印象。

2019/11/20
articleCard.readMore

2019.11软考软件设计师归来心得体会及复习备考指南

前言 本文文字量比较大,废话比较多😂。如果感到不适,建议直接阅读各段标题即可 干货和建议都在标题栏,📢文末有我整理的2019软件设计师考点思维脑图,以及备考资料大全分享 2019.12.18更新:昨天得知成绩已出,没想到过了,此处省略1W个哈哈哈😁…(58/62,同志们我们高级见~) 上周六(11.09)参加了软考软件设计师的考试,考试分上午场和下午场。总的来说题型比较常规,但是作为战五渣的我还是很担心能否过线😭。(个人感觉这次是下午简单上午难) 不过不管结果如何,经过这段时间的备考,已经把自己大学落下的基础知识都从头补了一遍,如今回过头复习数据结构、组成原理、软件工程真的就有一种恍然大悟、原来如此的感觉。复习下来,收获还是很多的。 话不多说,总结下考试心得和备考指南,希望自己的闲言碎语能给你的复习备考带来帮助 考试介绍 考试全称:全国计算机技术与软件工程专业技术资格考试,分为初、中、高级,详细介绍可以 百度了解一下 软件设计师考试属于软考中的中级考试,一年两次,报名费140元,考试时间一般在5月份和11月份。 软件设计师考试分上午场和下午场考试,其中上午场: 考点:综合知识(详细可参考《软件设计师考试冲刺(习题与解答)》) 题型:75道单项选择,每题1分(其中70~75题为英语完形填空) 考试时间:9:00-11:30 及格分数:45及以上 下午场: 考点:数据流图、UML、ER模型关系模式、算法应用、面向对象编程(包括但不仅限于这5个大类) 题型:问答题、填空题、代码填空题等,一共6道大题,每题15分,其中第5、6题为选答题,任选其一即可 考试时间:14:00-16:30 及格分数:45及以上 考试心得 说是心得其实就是些爬坑经验,如何准备考试,如何面对考试想必大家比我擅长,随便一位都是身经百战,这里我有几个在复习中发现的问题,作为建议讲给大家。 1.能早考就早考!最好不要拖!最佳备考时间 “大三~大四” 出名儿要趁早,软考也得趁早,最佳的复习考试时间就是大三到大四,因为软考初级、中级、高级所涉及到的知识点都是大学期间学过的知识。知识点覆盖面广,知识点多而杂,只有在大三这段时间所有的知识你都刚学过,考试大大减少复习的难度。 就拿中级软件设计师考试内容为例,备考教材中的12个章节的考点正好为为一个计算机科学与技术专业的学生大一到大四所有学过的专业课。 如果你不幸错过了大三、大四的最佳备考期,别怕!你不是一个人,因为我也是,然后才发现毕业后备考有多痛苦,不说了,说多了都是泪😭😭 2.制定复习计划,切勿朝三暮四 软考的考点基本都一个特点:特别多、特别杂、特别容易忘 我是八月中旬(11.9考试)开始复习的,因为白天要上班,所以每天就晚上复习。才开始是下班在家里复习,然后发现效率极差,因为我几乎是复习5分钟玩耍2小时。没办法,第二周开始我就下班后在公司复习(我司几乎没人加班,所以也没人管我)。 开始在决定在公司复习后,我就详细制定了一套复习计划,把复习分成三轮来 🎈 一轮:基础知识点复习,覆盖面广,知识点杂乱 🎈 二轮:刷题、通过刷题填补知识漏洞,同时回顾一轮(当时的感觉几乎就是前学后忘) 🎈 三轮:冲刺 我因为基础比较差,做计划时一轮复习计划用时45天左右,二轮用一个月,三轮两个礼拜。 其实当时也只是为了有个复习计划,但是现在回首,没想到自己居然能坚持下来,也是厉害了,这再没考过真实丢人丢到姥姥家😥。 说这么多,其实就是想说,既然是备考,就一定得要有复习计划,我的计划仅仅是一种参考,每个人的复习习惯、学习方式都不同,但是一定要给自己定一个复习计划来约束自己、提醒自己。 3.最好找个队友 一个人注定是寂寞的,你想想每天就你一个人深夜复习,多无聊。于是我就怂恿了两个憨憨同事和我一起备考。👯‍👯‍ 其实找个队友,最大的好处就是让你有一个比较好的学习氛围,产生一种竞争,复习竞争。 当然个体差异,每个人学习习惯不同,此条建议非必选 备考指南 如果你已经准备好了复习计划、带上了复习的队友,那么我就把我复习时用到的干货全部交予你。 1.考什么? 无论你开始复习与否,首先要弄清楚的就是你要考的科目他考什么?下面两个点是最重要的: 🎈 考试大纲 🎈 考点分布 考试大纲直接在网上都可以搜到,一般来说参考上一年的考试大纲就可以,复习前仔细阅读考试大纲是非常有必要的。 至于各个章节的考点就要通过官方教材作为参考了。 2.教程+考点+真题足以 我用的复习材料就是这三样: 《软件设计师教程》-第五版(所有知识点,详细!!!多!!!) 《软件设计师考试冲刺(习题与解答)》(各章节考点梳理及30道历年真题) 真题套卷(买你喜欢的就好) 其实买这两本书就够了,其他的书都没必要,第五版教程讲的很多很多、很杂,而《软件设计师考试冲刺(习题与解答)》讲的很精简,他每一章分两部分本章考点归纳和30道真题。 所以这两本书可以很好的结合:根据《软件设计师考试冲刺(习题与解答)》中的考点去阅读《软件设计师教程》,考点复习完后,回头来完成30道真题 基本我每一章节都是这么个循环过来的 请记住:《软件设计师教程》你把他当作新华词典就好,千万不要抱着他从头到尾的读,那样是很耗费时间的。 根据考试的考点有针对性的去看这本书。快速建立知识骨架 3.建立各章节知识体系骨架 在我复习的过程中,经常出现一个现象就是前学后忘,一个月前刚过的点,一个国庆节回来忘的一干二净,为此我不免要继续回顾一遍。 但是这里不是要强调回顾复习,而是要强调建立知识体系骨架,在你一轮复习时,脑海中对于每一个章节建立一个知识体系的简图(你可以像我一样直接画在Process中)。 一旦你建立了体系骨架后,任何一个知识点遗忘或者做题不确定答案时,你可以第一时间知道这个考点是那个章节,那个部分的知识点。 就在这个回顾、二次复习的过程中逐渐完善自己的知识体系骨架图 通过这种方式,多而杂的知识点不再是一盘散沙,在你的脑海中他们是分门别类,各有归属的图形。 4.一定要刷真题 说实话,我一轮复习过完脑袋里还是一片混乱,加上过了一个国庆节,回来之后那些知识点犹如水和面粉在脑袋里一样。 就酱紫,连续刷了2018-2015的8套真题后,大概到10月底的状态如下: 🎈 有了一个大体上的考点知识骨架图在脑海里 🎈 知道常考的题型,和一些惯用套路 🎈 知道自己已经掌握的部分和未掌握的地方 效果还是很明显的,做题可以帮你整合知识点,让你查漏补缺。 总结 以上就是我个人在备考中的一些经历和建议。希望能对您有所帮助。 下面分享一些总结的知识和用到的一些资源: 自己总结的2019软件设计师考点体系图 自己总结的8种排序算法的分析和代码演示 自己总结的23种设计模式分类和特性介绍 B站视频教程 软件设计师资料包, 提取码:v57u : 内含视频、讲义、教程、真题、模拟题…等等等 哔哔赖赖了半天,还是祝大家备考顺利,一次通过!

2019/11/12
articleCard.readMore

你还没用过“约定式提交”吗?那你赶紧来补补知识吧

前言 本文为介绍约定式提交,主要从以下几点展开: 现状分析 约定式提交 优点 规范 commitizen standard-version 本文对应的github项目地址:https://github.com/larscheng/Conventional-Commits-Demo 现状分析 目前我们的项目在commit时基本上五花八门,各领风骚。虽然不如网上的那些恶搞commit记录,但是这一现象严重影响我们在阅读记录和查找bug原因时的效率。 我们可以感受下: 可以对比看看同样按照规范式提交的项目的commit记录 两种commit message的对比很明显说明了情况,统一的提交信息,不仅看起来舒服,而且读起来更舒服 其实已经越来越多的人开始意识到规范化提交的重要性,据我在公司实地采访了一圈,前端团队早已经开始约定式提交,这也可能是因为目前社区中主流的提交规范都是由Angular提交准则形成。 为了提高开发效率,减少在处理问题时耗费的时间,推荐大家在写完代码,提交时能够使用以下规范: 规范化提交(不一定是文中提到的方式,但无论哪种方式,要做到统一、简明) 一处变更一次commit(谨防多处、多次修改堆积成一次commit提交,这对后期bug分析将是灾难) 约定式提交 约定式提交:每次使用git commit 的时候都需要写commit message,如果message的 style是按照固定的模版格式书写,对于后期的维护和编写changelog都有巨大的好处。 而且现在的很多自动生成changelog的工具,都是建立在约定式提交的基础之上。 优点 可读性好,清晰,不必深入看代码即可了解当前commit的作用。 为 Code Reviewing做准备 方便跟踪工程历史 让其他的开发者在运行 git blame 的时候想跪谢 提高项目的整体质量,提高个人工程素质 约定式提交规范 约定式提交规范是基于Angular提交准则形成,提交说明的结构如下: 1 2 3 4 5 <类型>([可选的作用域]): <描述> // 空一行 [可选的正文] // 空一行 [可选的脚注] 其中,<类型>是为了向类库使用者表明其意图,其可选值为: feat: 表示新增了一个功能 fix: 表示修复了一个 bug docs: 表示只修改了文档 style: 表示修改格式、书写错误、空格等不影响代码逻辑的操作 refactor: 表示修改的代码不是新增功能也不是修改 bug,比如代码重构 perf: 表示修改了提升性能的代码 test: 表示修改了测试代码 build: 表示修改了编译配置文件 chore: 无 src 或 test 的操作 revert: 回滚操作 [可选的作用域]: 是为了描述 此次 commit 影响的范围,比如: route, component, utils, build, api, website, docs <描述>: 此次提交的简短描述 [可选的正文]: 此次提交的详细描述,描述为什么修改,做了什么样的修改,以及开发的思路等等,输入 \n 换行 [可选的页脚]: 主要写下面2种 Breaking changes: 在可选的正文或脚注的起始位置带有 BREAKING CHANGE: 的提交,表示引入了破坏性变更(这和语义化版本中的 MAJOR 相对应)。 Closed issues: 罗列此次提交修复的 bug,如 fixes issue #110 Commitizen Commitizen是一个撰写合格 Commit message 的工具。 安装 安装命令如下:任选其一 1 2 3 > $ npm install -g commitizen (全局安装) > $ npm install -d commitizen (项目安装) > 然后,在项目目录里,运行下面的命令,使其支持 Angular 的 Commit message 格式。 1 2 > $ commitizen init cz-conventional-changelog --save --save-exact > ps: 对于非Node项目(java、php…)在执行上一条命令前,需要手动创建package.json文件 1 2 > $ npm init --yes > 通过如上命令生成package.json文件基本格式如下: 1 2 3 4 5 6 7 8 { "name": "demo", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" } } 以后,凡是用到git commit命令,一律改为使用git cz。这时,就会出现选项,用来生成符合格式的 Commit message。如图: standard-version 如果你的所有 Commit 都符合 Angular 格式,那么发布新版本时, Change log 就可以用脚本自动生成 standard-version就是生成 Change log 的工具 安装使用 安装命令如下:任选其一 1 2 3 > $ npm i -g standard-version (全局安装) > $ npm i -S standard-version (项目安装) > 生成CHANGELOG: 在package.json中的script中 加入配置: “scirpt”:{“release”:“standard-version”} 直接执行,即可生成CHANGELOG文件 1 2 > $ npm run release > 备注: 生成CHANGELOG的工具很多,conventional-changelog-cli也可以用来生成CHANGELOG,安装使用方法和standard-version类似 本项目的CHANGELOG生成实例:CHANGELOG查看

2019/11/11
articleCard.readMore

教你如何看懂UML中的类图及类图中的关系

前言 本文作为设计模式系列的第零篇文章,其主要时教大家如何去看懂设计模式中常见的类图,以及类间的关系。因为无论你从哪里学习设计模式,都避免不了类图的阅读和理解。 本文主要从以下几个方面介绍: UML介绍 类图介绍 类与类之间的关系 UML介绍 统一建模语言(Unified Modeling Language,UML)是用来设计软件蓝图的可视化建模语言,1997 年被国际对象管理组织(OMG)采纳为面向对象的建模语言的国际标准。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。 UML集成了Booch,OMT和面向对象程序设计的概念,将这些方法融合为单一的,通用的,并且可以广泛使用的建模语言。UML打算成为可以对并发和分布式系统的标准建模语言。 UML发展至今在UML2.2种已经定义了14种图示,本文不对UML展开说明,感兴趣的同学可以翻一下大学课本《软件工程》 本文我们重点讲解在设计模式中用到最多的类图。 类图介绍 类图(Class Diagram)展现了一组对象、接口、协作和它们之间的关系 类图的组成 类图中通常由 类、接口、协作、关系组成 类 类:是对对象的抽象,具有相似结构、行为和关系的一组对象的描述符,用来描述系统的静态部分。类的图示如下 最上面是类名称 中间部分包含类的属性([可见性]属性名:类型[=默认值] —> + age:Integer = 1) 底部部分包含类的方法([可见性]名称(参数列表)[:返回类型] —> + eat(String food):String) 其中的可见性表示该属性对类外的元素是否可见 包括公有(Public)、私有(Private)、受保护(Protected)和默认(Default)4 种 在类图中分别用符号+、-、#、~表示。 接口 接口(Interface)是一种特殊的类,它具有类的结构但不可被实例化,只可以被子类实现。它包含抽象操作,但不包含属性。它描述了类或组件对外可见的动作。在 UML 中,接口有两种表示方法,如下图所示: 关系 这里以图形接口、长方形类、圆形类的类图为例,长方形和圆形都实现了图形接口,他们之间存在实现关系,类图如下: 从图中可以发现,长方形类和圆形类都通过虚线空心箭头指向图形接口,这在类图中表示他们存在实现关系。 类与类之间的关系 根据类与类之间的耦合度从弱到强排列,UML 中的类图有以下几种关系: 依赖关系、关联关系、聚合关系、组合关系、泛化关系和实现关系。 依赖关系 对象之间最弱的一种关联方式,是临时性的关联。代码中一般指由局部变量、函数参数、返回值建立的对于其他对象的调用关系以及对静态方法的调用。一个类调用被依赖类中的某些方法而得以完成这个类的一些职责。在类图使用带箭头的虚线表示依赖,箭头从使用类指向被依赖的类。 如图所示,程序员与电脑的类图中,程序员想要写代码,需要用到电脑,程序员与电脑之间存在依赖关系 关联关系 对象之间一种引用关系,比如客户类与订单类之间的关系。这种关系通常使用类的属性表达。 关联又分为一般关联、聚合关联与组合关联。 一般关联 一般关联在类图使用带箭头或者没有箭头的实线表示,箭头从使用类指向被关联的类。可以是单向和双向,也可以没有箭头。 如图所示,单向关联中,可以看到Student类有home属性,且Student类引用了Home类 在双向关联中,不难发现Student类引用了Teacher类,Teacher类也相应的引用了Student类 聚合关联 聚合关系是一种特殊的关联关系,表示has-a的关系,是一种不稳定的包含关系,聚合关系强调的是整体和部分的关系,其中部分可以脱离整体而存在。 例如学校有老师,而老师脱离学校后仍可以教授学生。 在UML类图中聚合用带空心菱形的直线表示,其中菱形指向整体,学校与老师的类图如下: 组合关联 组合关系也是一种特殊的关联关系,表示contains-a的关系,它与聚合关系很像,也是强调整体与部分的关系,不同的是部分无法脱离整体存在。 比如我们的身体有心脏、大脑、四肢等重要器官,对于一个健康的身体而言,任何一个器官都不能少 在UML类图中聚合用带实心菱形的直线表示,其中菱形指向整体,身体与心脏的类图如下: 泛化关系 泛化关系在Java中也叫作继承关系,表示is-a的关系,是对象之间耦合度最大的一种关系,子类继承父类的所有细节。 在UML中我们用带空心三角形的直线来表示,其中空心三角指向父类,如图所示: 实现关系 接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。 实现关系上文也有讲到,在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。 总结 UML作为一个严谨的软件建模语言,经过20多年的发展已经成为业界的标准建模语言,大家接触它最多的时候应该时大学中,反倒工作中很少用到。 但是关于UML的基本常识和基本使用是作为一个开发人员的必修课,就像在学习设计模式时,你首先就得学会读懂类图,能够画出类图。 相信读完本文,类图已经难不倒你了。

2019/10/29
articleCard.readMore

设计模式总览

前言 前段时间,跟着Gang of Four学习了设计模式,虽然23种设计模式都过了一遍,好像懂了但是好像又有些不明白。刚好在软考备考时设计模式也是考点。故此,通过这一次的再复习索性整理出设计模式的一套学习笔记,笔记中会通过生活中的例子详细讲解各种常用模式,并利用Java代码实现。 本文主要通过以下几点来对设计模式进行总结: 设计模式的背景 设计模式的概念 设计模式的的7大原则 设计模式的的4大要素 常见的23种设计模式 设计模式的背景 设计模式这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中。 直到1990年,软件工程界才开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。 在 1994年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns - Elements of Reusable Object-Oriented Software) 一书,该书首次提到了软件开发中设计模式的概念,并收录了23种设计模式。 设计模式的概念 首先初次接触设计模式的同学一定要牢记 设计模式不是某种语言,不是某种框架,更不是什么架构模式,它只是前辈们爬了无数坑、秃了头总结出来的代码设计经验!!! 其概念如下: 软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。其目的是为了提高代码的可重用性、代码的可读性和代码的可靠性。 简单概括可得出如下要点: 代码设计经验的总结 通用解决方案 解决问题的套路 反复使用 提高重用性、可读性、可靠性 常见的23种设计模式 根据各个模式的作用和目的可划分为创建型模式、结构型模式和行为型模式 3 种。 模式类型模式 创建型模式(5) 对象怎么来:用于创建对象● 工厂模式(Factory Pattern) ● 抽象工厂模式(Abstract Factory Pattern) ● 单例模式(Singleton Pattern) ● 建造者模式(Builder Pattern) ● 原型模式(Prototype Pattern) 结构型模式(7) 对象和谁有关:用于处理类或对象的组合● 适配器模式(Adapter Pattern) ● 桥接模式(Bridge Pattern) ● xxx过滤器模式(Filter、Criteria Pattern) ● 组合模式(Composite Pattern) ● 装饰器模式(Decorator Pattern) ● 外观模式(Facade Pattern) ● 享元模式(Flyweight Pattern) ● 代理模式(Proxy Pattern) 行为型模式(11) 对象与对象在干嘛:用于描述类、对象相互交互的责任分配● 责任链模式(Chain of Responsibility Pattern) ● 命令模式(Command Pattern) ● 解释器模式(Interpreter Pattern) ● 迭代器模式(Iterator Pattern) ● 中介者模式(Mediator Pattern) ● 备忘录模式(Memento Pattern) ● 观察者模式(Observer Pattern) ● 状态模式(State Pattern) ● xxx空对象模式(Null Object Pattern) ● 策略模式(Strategy Pattern) ● 模板模式(Template Pattern) ● 访问者模式(Visitor Pattern) 根据模式的操作对象来分(类或对象),可分为类模式和对象模式2种。 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。 工厂方法、(类)适配器、模板方法、解释器属于类模式。 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。 除了以上 4 种类模式,其他的都是对象模式。 设计模式的的7大原则 你可能没有听过设计模式的原则,但是想必你一定知道面向对象设计的原则。如果都是第一次听说,那么接着往下看 在设计模式中,所有的模式都是基于这些设计原则来解决问题 依赖倒转原则(Dependence Inversion Principle) 高层模块(稳定)不应该依赖低层模块(变化),二者都依赖抽象(稳定)。抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定) 开闭原则(Open Close Principle) 充分发挥面向对象,对现实事物进行抽象化,实现对扩展开放,对修改关闭,程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果 单一职责原则(Single responsibility principle) 一个类因其他变化的原因只有一个职责。多个职责会引起耦合,牵一发动全身,此原则的核心是解耦和增强内聚性。 里氏代换原则(Liskov Substitution Principle) 实现抽象化的具体步骤规范,子类可以当作父类用,并拥有自身独有的行为 接口隔离原则(Interface Segregation Principle) 使用多个隔离的接口优于单个重度耦合接口,降低依赖,降低耦合 迪米特法则,又称最少知道原则(Demeter Principle) 一个实体应当尽可能少的与其他实体产生联系,防止过度耦合 合成复用原则(Composite Reuse Principle) 尽量使用合成/聚合的方式,而不是使用继承。 设计模式的的四大要素 软件设计模式使人们可以更加简单方便地复用成功的设计和体系结构,它通常包含以下几个基本要素: 模式名称、别名、动机、问题、解决方案、效果、结构、模式角色、合作关系、实现方法、适用性、已知应用、例程、模式扩展和相关模式等 其中最关键的元素包括以下 4 个主要部分。 模式名称(PatternName) 对于一个模式进行命名,模式名称有助于我们理解和记忆该模式计。 问题(Problem) 问题描述了该模式的应用环境。它解释了设计问题和问题存在的前因后果,以及必须满足的一系列先决条件。 解决方案(Solution) 针对问题的解决方案,其内容给出了设计的各个组成部分,它们之间的关系、职责划分和协作方式 效果(Consequence) 描述了模式的应用效果以及使用该模式应该权衡的问题,即模式的优缺点。 最后 本文是根据Gang of Four书中的23中设计模式展开讨论的,实际上,经过了近25年的发展,设计模式已经不仅仅局限于这23种。 设计模式是前辈们总结的优秀代码设计经验,是无法依次完全掌握的,其中很多都是需要结合实际场景的,需要我们先学习这种思想,学习前辈们优秀的案例,逐渐培养自己对代码的思维模式,当你遇到相应的场景时,能够知道通过何种设计模式进行优化,通过这种理解加应用的过程,就是对设计模式的逐渐掌握。 学习新知识都是需要循序渐进,当你了解了这23种典型的设计模式,并对它有一定理解后,相信无论是在日常开发,还是CodeReview时都可以设计和写出优秀的代码。

2019/10/29
articleCard.readMore

萌新入门Github请看这里,学不会远程教

一些废话 本文的主旨是为初次接触Github的同学提供一个入门的演示,如果你已经是Github老鸟,可以忽略本文哦,另外本文只是抛砖引玉,其实最好的教程是官方文档!!! Github官网 Github使用指南-Web页面 Github官方中文版文档-ALL Github目前虽然十分火热,但是对于第一次接触这一类产品的同学来说,上手还是有一定难度的 本文就聊一聊如何使用Github,相信读完本文,你一定可以掌握以下几点: Github是什么 Github可以做什么 Git与Github的关系 本地项目上传至Github 克隆远程仓库修改后提交 举一反三Github、Gitee、Coding Github是什么 全球最大的同性交友平台—>全球最大的程序猿交流平台—>一个神奇的地方 Github是目前最火的网站之一,在这里很多的用户都是从事程序相关工作,又被戏称为全球最大的程序猿交流平台, 从最初仅活跃在程序员圈子中发展到现在各行各业的使用,经常用Github你就会发现,这真的是个神奇的地方,这里有很多五花八门,有趣的仓库。 那么Github到底是什么呢? Wiki 百科上是这么说的 GitHub 是一个共享虚拟主机服务,用于存放使用Git版本控制的软件代码和内容项目。它由GitHub公司(曾称Logical Awesome)的开发者Chris Wanstrath、PJ Hyett和Tom Preston-Werner 使用Ruby on Rails编写而成。 官方是这么说的 GitHub is a code hosting platform for version control and collaboration. It lets you and others work together on projects from anywhere. 用我的工地英语翻译过来意思就是 GitHub是用于版本控制和协作的代码托管平台。它使您和其他人可以在任何地方共同处理项目。 虽然官方概括的很精简,但是过于官方,以至于我也一脸懵*,不过我们脑海里可以对她有以下几点印象: 存放代码、存放项目 版本控制 托管平台 共同处理项目 Github可以做什么 在了解了Github是什么后,如果你以为github就是个存代码的地方那你就太年轻了,你完全想不到全球网民的脑洞有多强大,Github都快玩出了花。 除了最基础的可用来存储和管理项目工程外,Github还可以做很多其他事情: 网站 个人博客 说明文档 管理配置文件 收集资料 面试简历 托管编程环境 写书、写小说 资源共享 招聘信息发布 老师布置作业,学生交作业 公益活动 等等等…五花八门千奇百怪 Git与Github的关系 从GitHub的介绍上我们不难看出,GitHub托管的是Git代码仓库。 这时候可能又有同学想问“这个Git又是什么鬼?他和Github又有什么关系” Git是一个分布式的版本控制系统,可以对项目进行版本管理。而GitHub可以托管各种git库,并提供一个web界面 Git只是一个源代码管理工具(管理代码的版本) 并不能实现代码的共享。 而我们将本地的Git库(使用Git版本控制的软件代码和内容项目)上传到Github上,通过GitHub实现共享,达到不同之间的协同工作 所以在正式使用Github之前,需要先学会使用Git管理项目工程,即学会版本控制 Git的命令大全和原理性知识本文不做拓展,大家如有兴趣可以去Git官方文档翻阅学习哦 本地项目上传至Github 在开始本节介绍前呢需要大家准备以下环境和账号: Github账号 本地电脑安装Git 在Github中创建新的仓库 直接进入创建仓库页面:https://github.com/new 或者通过按钮进入创建页面: 具体的创建步骤如下图所示: 创建成功后会跳转到如下页面: 仔细看图上,不难发现,他已经告诉你接下来该怎么做,如何创建一个新的存储库(create a new repository on the command line) 下面我们按照他给的方式,来试一下看看效果~ 配置本地Git环境 如果是第一次使用 Git,你需要设置署名和邮箱 1 2 $ git config --global user.name "用户名" $ git config --global user.email "电子邮箱" 设置成功后在Github提交记录中就会显示本次提交人的信息 创建本地项目 创建一个用于托管在Github中的项目工程 创建本地文件夹 创建一个名为 GithubStudy 的文件夹 本地git仓库初始化 进入 GithubStudy 文件夹 右键打开git bash here,执行如下命令,进行本地git仓库初始化 1 git init 完成后在当前项目里面会有一个.git文件夹 是用来记录当前本地仓库(如果看不到需要勾上显示隐藏文件) 创建文件 在文件夹下创建一个README.md,其内容为: 1 ### 1.学习使用Github:本地项目上传至Github 添加到暂存区 此时把将项目的修改全部添加到暂存区 ,意思就是保存此次的所有修改 在GithubStudy 文件夹 右键打开git bash here,执行如下命令git add .(注意有点…) ps: git add .:添加全部文件到暂存区,git add README.md:指定文件添加到暂存区 提交到本地仓库 把当前暂存区添加的文件提交到本地仓库(提交后就表示创建了一个代码的版本) 在GithubStudy 文件夹 右键打开git bash here,执行如下命令 git commit -m '提交日志' 提交完成后就会把代码提交到.git文件夹(本地git仓库) 并且会自动创建一个代码的版本 指定远程服务器仓库 继续在git bash here中执行命令:git remote add origin {项目仓库地址} 注意:这里的项目仓库地址是你在github创建完新仓库时自动生成的,比如我刚刚创建后生成的是:https://github.com/larscheng/GithubStudy.git 执行完上述命令后,远程仓库的地址就指定好了,我们随时都可以推送项目 推送至Github 所有准备工作都已完成,现在就需要我们将项目推送至GitHub,你也可以理解为上传。 在GithubStudy 文件夹下右键打开git bash here,执行如下命令 git push -u origin master 注:初次提交时,会需要验证你的Github用户名和密码,根据命令提示输入后即可完成推送 此时你再进入Github刚刚创建的仓库中,刷新页面后,看到的应该是如下的样子: 在线修改项目并提交 这里,我在项目中创建了一个新的文件夹名叫update.txt 修改操作可以参考如下动态图 拉取最新版本 此时我们本地的项目已经不是最新的了,如何进行版本同步,获取最新的代码呢,执行如下命令即可 1 git pull 执行成功后,本地代码即为最新咯 克隆远程仓库修改后提交 这一节,主要演示的是:当你已有一个github项目,如何将它克隆至本地,修改后推送至github 这里还是以GithubStudy项目为演示,在Github下该仓库的首页中复制仓库地址: 克隆 新建文件夹GithubStudyClone 在GithubStudyClone 文件夹下右键打开git bash here,执行如下命令 git clone {项目仓库地址} 注意:这里的项目仓库地址是你在github创建完新仓库时自动生成的,比如我创建后生成的是:https://github.com/larscheng/GithubStudy.git,每个人的地址都会有所不同,记得替换哦 执行结果如下: 修改 修改文件README.md,在原有内容中加入如下信息: 1 ### 2.学习使用Github:克隆远程仓库修改后提交 添加+提交+推送 修改完成后,执行如下三条命令,即可推送至GitHub 在GithubStudyClone/GithubStudy 目录下右键打开git bash here,依次执行如下命令 1 2 3 4 5 git add . git commit -m '修改README' git push -u origin master 此时进入GitHub中查看GithubStudy,你会发现我们刚刚的修改已经生效 对比 上面对于Github的入门使用已经基本介绍完了,Github目前以被微软收入囊中,对于国内用户来说由于网络的原因访问Github时往往不是那么的顺畅。 那么国内是否有Github同类型的产品呢?答案当然是有的 码云(gitee.com)是 OSCHINA.NET 推出的代码托管平台,支持 Git 和 SVN,提供免费的私有仓库托管。 Coding 是一个面向开发者的云端开发平台,目前提供代码托管,运行空间,质量控制,项目管理等功能。 这两款产品作为代码托管平台来说与Github几乎一样,都是优秀的托管平台。 如果你烦恼于GitHub的访问限制,那完全可以考虑使用Gitee、或者Coding 所以问题来了,产品都换了,那怎么用呢? GitHub我都是才学会,又要学Coding、Gitee 不用担心,操作一摸一样,你完全可以用上面Github讲解的操作流程来使用Gitee、Coding。 总结 本文不是Git的安装教程、也不是原理分析文章,本文旨在为初次接触Github的同学提供一个容易上手的中文演示 本文中也存在一些难点和坑点,例如 Git命令的使用 Github身份验证 Github的其他功能使用 等等… 其实大家不用害怕,东西虽然多,但是都很好理解,当你遇到问题时要学会运用搜索引擎、学会阅读官方文档。 当然如果有问题也可以留言或者邮件联系我哦 如果阅读完本文后,你已经基本掌握了Github的使用,这里为大家提供一些高阶操作的文章: Git原理入门-阮一峰 Git常用命令详解 Github官方中文版文档-ALL 基于hexo和coding免费搭建个人博客网站-larscheng

2019/10/14
articleCard.readMore

Hexo的工作原理探究

原文作者:赵彪 原文地址:《hexo是怎么工作的》 你可能用过hexo(或者jekyll)来搭建自己的博客网站。通常我们在安装、配置完成hexo之后,借助hexo,一般通过以下步骤,就可以完成一篇博客的编写及发布,真是方便极了: 1 2 3 4 5 $ hexo init // 创建一个新的hexo项目 $ hexo new mynewblog // 新建一篇标题为mynewblog的文章 $ hexo server // 为hexo在本地起一个http server, 然后通过浏览器访问博客 $ hexo generate // 生成将要发布的博客网站包含html在内的静态资源 $ hexo deploy // 将generate的结果发布到_config.yml中指定的仓库 可是,从hexo init到hexo deploy中间发生了什么呢?为了搞清楚这一过程、理解hexo的工作原理,本文将试着回答以下问题: 命令行中的hexo是什么 hexo是怎么将我们写好的markdown转换成html的 hexo插件是如何工作的 本地的hexo项目和git page有什么关系 本文不是: hexo的安装、使用教程 git page的使用教程 命令行中的hexo是什么? hexo项目在github上已经有超过17k的star了,但是你知道吗,日常我们在命令行”操作”hexo时所输入的hexo(例如hexo init)并不是这个17k个star的项目! 是的,我们在命令行中所输入的”hexo”实际是hexo-cli项目,该项目在github上的star还不足50个。 hexo可以粗略分为三个子项目,分别是: hexo-cli hexo (下文中用hexo core来指代) hexo plugins 其中hexo plugins不是指某一个单独的项目,而是泛指所有的hexo plugin项目。 请看下图: 让我们结合这张图来大致看看这三个项目的作用(下面的链接均是指向Github中相关的源码): hexo-clihexo命令行项目,作用是: 启动hexo命令(进程),及其参数解析机制。每次我们输入’hexo xxx’命令后,都会通过node调用hexo-cli中的entry函数(比如,可以把’hexo init’视为’node hexo-cli/entry.js init’) 实现hexo命令的三个初始参数(功能): init / version / help 加载hexo核心模块,并初始化 hexo corehexo核心,他的主要作用如下: 实现了hexo功能扩展对象 实现了hexo核心功能, 如new, publish, generate等(其实是一些hexo插件,下文中会详细分析) hexo plugins指一些能够扩展hexo的插件。插件可以按功能分成两类: 扩展hexo命令的参数,如hexo-server(安装这个插件以后才能使用hexo server命令) 扩展hexo解析文件的”能力”,如增加jade模版解析功能的hexo-render-jade插件 从markdown到html的旅程 简单来说,hexo中,从markdown到html的generate过程中做了两件事: 模板渲染 模板渲染 是的,就是这样,就是两次模板渲染。只不过两次渲染的输入、渲染模板的引擎、输出不一样。此处应该有一个表格: 还得有一张图: 对上面表格和图的说明: hexo core在generate的过程中会产生一个对象,我们在这里把这个对象称为article。第一次渲染的主要目的就是给这个对象添加title,content等属性。其中: article.title, article.date, article.tags, article.categories等属性来自yml front的部分 article.content是markdown文章解析后的html片段 hexo项目目录下包含三个子目录, source目录,写博客的主要工作目录。这个目录下存放的是我们的markdown文章以及js, images, css themes目录,主题目录,定义了即将生成的html的layout, 和html中需要加载的css, js, images public目录, hexo generate的最终输出目录。里面包含了整个博客网站的html, css, js, images 第二次渲染,需要引入对应模板文件格式的插件,如.ejs文件就需要使用hexo-render-ejs插件,.jade文件需要使用hexo-render-jade插件,而.sass文件则需要hexo-render-sass插件来转换成css文件。hexo的这一设计有点类似webpack中的loader。 hexo插件是如何工作的 hexo和webpack还有一点类似的地方就是插件驱动理念。即hexo(和webpack)是先实现一套(插件)扩展系统,然后再往扩展系统中添加插件来实现自身的功能。即我们日常使用的hexo init, hexo new,hexo generate等等功能都是通过一个个插件(其实就是一个个function)实现的。 具体来讲就是: hexo.extend这个对象的每个属性都是一个用来绑定(特定)插件的对象。(所谓”绑定”,其实就是对象的register方法) hexo初始化过程中先加载内部插件,再加载外部插件 而这些插件的功能分为两大类: 命令行插件和generate过程相关功能,例如: 命令行插件, hexo new, 是在hexo.extend.console对象上绑定的一个插件 generate过程相关的插件,如上文提到的往article对象添加title,content等属性的功能,是通过往hexo.extend.processer对象上绑定post插件来实现的 所以,当我们想自己动手写插件时,就是像hexo官网给出的这样,调用某个对象的register方法,如hexo.extend.console.register。 hexo和git page 如上图,(用户通过浏览器访问到的)git page上的博客网站其实是hexo generate之后生成的public目录下的内容。 所以,一个hexo博客项目应该有两个仓库: (基于hexo init结果的)博客编写仓库。可以把这个项目看成一个代码库,用来”开发”博客网站(包含写博客,生成博客等任务) 存放(hexo generate结果的)public目录仓库。这个项目是”只读”的,我们不会直接修改这个仓库的内容,我们也不会对这个仓库直接进行git pull、git commit、git push等常规操作。这个仓库的内容就是public目下的内容,即是通过hexo generate产生、hexo deploy提交的。 总结 hexo简洁、强大的功能来自于自身优雅的系统设计: hexo进程启动、hexo核心对象封装、插件系统分别独立 自身采用插件驱动,生来就具备高可扩展性 希望读完这篇文章你能对hexo本身有更深入的理解,也能通过hexo的代码设计,对自己以后写出更优雅的代码有所启发。

2019/9/26
articleCard.readMore

Hexo-theme-butterfly修改调整记录教程

首先感谢Butterfly作者jerryc,提供了一个非常优秀的博客主题 2020.3.27 更新: 我目前已经更换了博客主题,butterfly也更新了许多新功能,本文的修改仅在我当时使用的版本中测试过,如果你使用的是最新版的butterfly,建议本文的diy修改仅作参考哦 GL&HF 前言 在阅读本文之前,确保您已经了解并使用过hexo-theme-butterfly主题。 如果你还没有使用过,那推荐你一款美观大气的hexo主题:hexo-theme-butterfly 其实在主题的使用过程中也是新语言的学习过程。 首页背景图片屏占比 1.在主题配置文件themes/Butterfly/_config.yml最后边添加如下配置 1 2 3 # 首页背景图片屏占比,1:100%,2:50%,1.5:75% homepage: ratio: 2 2.修改themes/Butterfly/layout/includes/additional-js.pug该文件内代码 1 2 3 4 5 ....省略.... function alignContent() { for (var n = $(window).height()/!{theme.homepage.ratio}, e = document.querySelectorAll(".full_page"), i = 0; i < e.length; i++) ....省略.... } 此处将homepage.ratio属性设置为2。首页效果如下: 首页设置多个子标题 1.修改主题配置文件themes/Butterfly/_config.yml的subtitle属性,例如修改前为: 1 2 3 4 subtitle: enable: true sub1: 所有的不平凡都来自平凡! sub2: All the extraordinary from ordinary! 修改后: 1 2 3 4 5 6 subtitle: enable: true sub: - 所有的不平凡都来自平凡 - 分享Java技术记录点滴生活... - 今天最好的表现是明天最低的要求 2.修改themes/Butterfly/layout/includes/additional-js.pug该文件内代码 1 2 3 4 5 6 7 8 script. var typed = new Typed(".subtitle", { strings: '!{theme.subtitle.sub}'.split(','),//修改这里 startDelay: 300, typeSpeed: 100, loop: true, backSpeed: 50 }); 将之前的单个读取,改为直接读取数组再拆分 这里有一个瑕疵,它使用逗号split(",")进行拆分,所以当sub中有逗号的话,用转义符代替 ICP备案国徽图标显示 1.修改主题配置文件themes/Butterfly/_config.yml的ICP属性,为其添加国徽图标的路径pic: 1 2 3 4 5 ICP: enable: ture pic: /img/beian.png #备案国徽小图路径 url: http://www.beian.gov.cn text: 浙ICP备12345678号 2.修改themes/Butterfly/layout/includes/footer.pug该文件内代码 1 2 3 4 5 if theme.ICP.enable .icp a(href=theme.ICP.url target="_blank") img.lozad(data-src=`${theme.ICP.pic}` onerror=`onerror=null;src='${theme.ICP.pic}'` style="padding:0px;vertical-align: text-bottom;") span=theme.ICP.text 修改后效果如下 添加友情链接侧边栏 1.修改主题配置文件themes/Butterfly/_config.yml的aside属性,添加侧边栏友链属性 1 2 3 4 5 6 7 8 9 10 11 12 aside: card_author: true card_announcement: true card_recent_post: true card_categories: true card_tags: true card_archives: true card_webinfo: true card_links: enable: true # 是否开启友链侧边栏显示 num: 6 # 显示个数 flink: http://www.larscheng.com/friends/ # 友链页面地址 1.在目录themes/Butterfly/layout/includes/widget下新建文件card_links.pug 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .card_widget.card-links .card-content .item_headline i.fa.fa-handshake-o(aria-hidden="true") span= _p('aside.card_links') each i in site.data.link if(i.class_name === "友情链接") ul.aside_category_item each item,index in i.link_list if(index <= theme.aside.card_links.num) li.aside_category_list a(href=item.link title=item.name+" : "+item.descr target="_blank") img.lozad(data-src=item.avatar alt=item.name onerror=`onerror=null;src='${theme.lodding_bg.flink}'` height="35px") item=item.name li.aside_category_list.find_more a(href=theme.aside.card_links.flink target="_blank") 查看更多 其中_p('aside.card_links')是根据语言环境获取侧边栏标题:友情链接。可以到themes/Butterfly/languages下进行添加 需要注意的一点是if(i.class_name === "友情链接")这个判断这里是用来判断需要显示的数据 Butterfly 的友情链接模板中存在多个class,通常我们只需要显示友情链接这一部分而已,所以会有这一判断 此处应该有更优雅的处理方式,但是限于水平有限,先暂且这样实现 link.yml示例文件如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class: class_name: 友情鏈接 link_list: 1: name: xxx link: https://blog.xxx.com avatar: https://cdn.xxxxx.top/avatar.png descr: xxxxxxx 2: name: xxxxxx link: https://www.xxxxxxcn/ avatar: https://xxxxx/avatar.png descr: xxxxxxx class2: class_name: 鏈接無效 link_list: 1: name: 夢xxx link: https://blog.xxx.com avatar: https://xxxx/avatar.png descr: xxxx 2: name: xx link: https://www.axxxx.cn/ avatar: https://x descr: xx 3.修改themes/Butterfly/layout/includes/aside.pug文件,添加如下代码 1 2 if theme.aside.card_links.enable include widget/card_links.pug 需要注意改代码片段添加的先后位置,决定了它在页面上被渲染的顺序 修改完毕后,显示的基本效果如下: 其他小改动 这部分的修改适合强迫症晚期患者… 文章列表图片文字比例调整 这个属于css样式的调整,你可以直接在浏览器调整为你想要的样式属性后 到themes/Butterfly/source/css/_layout/z-other.styl内进行修改 文章列表的图片样式:#recent-posts .post_cover 文章列表的内容预览样式:.recent-post-info 大小、比列都由你定。 归档侧边栏查看更多按钮 查看themes/Butterfly/layout/includes/widget/card_archives.pug发现它直接调用了list_archives方法 emmmmmm,能力有限我又去暴力改代码了…找到/node_modules/hexo/lib/plugins/helper/list_archives.js 修改if (style === 'list') {·······}内的内容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 if (style === 'list') { result += `<ul class="${className}-list">`; for (i = 0, len = data.length; i < 6; i++) { item = data[i]; result += `<li class="${className}-list-item">`; result += `<a class="${className}-list-link" href="${link(item)}">`; result += transform ? transform(item.name) : item.name; result += '</a>'; if (showCount) { result += `<span class="${className}-list-count">${item.count}</span>`; } result += '</li>'; } result += `<li class="${className}-list-item">`; result += `<a style="text-align: center;font-size: 13px" class="${className}-list-link" href="${link()}">`; result += '查看更多'; result += '</a>'; result += '</li>'; result += '</ul>'; 然后你看到的效果大概是这样的: 最后 以上就是我在hexo-theme-butterfly的使用过程中,个人修改的一些记录,如果你也钟意这些修改,不妨试试吧😁 GL&HF…😜

2019/9/19
articleCard.readMore

排序8:基数排序

原文作者:Mr.Seven 原文地址:八大排序算法总结与java实现 ❤查看排序算法动态演示❤查看排序算法动态演示❤查看排序算法动态演示 基数排序 (Radix Sort) 基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine),排序器每次只能看到一个列。 它是基于元素值的每个位上的字符来排序的。 对于数字而言就是分别基于个位,十位,百位或千位等等数字来排序。 基数排序是一种非比较型整数排序算法,其原理是将整数按位数(个十百千...)切割成不同的数字,然后每个位数分别比较。 由于整数也可以是表达字符串和特定格式的浮点数,所以基数排序也不是只能用于整数。 基本思想 将所有待比较数值统一为同样的数位长度,数位较短的数前面补0,然后,从最低位开始依次进行一次排序。这样从最低位排序一直到最高位完成以后,数列就变成一个有序数列 基数排序按照优先从高位或低位来排序有两种实现方案: MSD(Most significant digital) 从最左侧高位开始进行排序。先按k1排序分组, 同一组中记录, 关键码k1相等, 再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来, 便得到一个有序序列。MSD方式适用于位数多的序列。 LSD (Least significant digital)从最右侧低位开始进行排序。先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。LSD方式适用于位数少的序列。 动态示意图如下: 算法分析 以LSD为例,从最低位开始,具体算法描述如下: 取得数组中的最大数,并取得位数; arr为原始数组,从最低位开始取每个位组成radix数组; 对radix进行计数排序(利用计数排序适用于小范围数的特点); Java代码 基数排序:通过序列中各个元素的值,对排序的N个元素进行若干趟的“分配”与“收集”来实现排序。 分配:我们将L[i]中的元素取出,首先确定其个位上的数字,根据该数字分配到与之序号相同的桶中 收集:当序列中所有的元素都分配到对应的桶中,再按照顺序依次将桶中的元素收集形成新的一个待排序列L[]。对新形成的序列L[]重复执行分配和收集元素中的十位、百位…直到分配完该序列中的最高位,则排序结束 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 /** * 描述: * 基数排序 * * @author lars * @date 2019/9/12 13:35 */ public class RadixSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 26, 13, 27, 49, 55, 4}; System.out.println("排序前:" + Arrays.toString(arr)); radixsort(Arrays.copyOf(arr, arr.length)); } /** * 基数排序(LSD 从低位开始) * @param arr 待排序数组 */ private static void radixsort(int[] arr) { if (arr.length <= 1) { return; } //待排元素最大值 int max = Arrays.stream(arr).max().getAsInt(); int maxDight = String.valueOf(max).length(); //申请一个桶空间 int[][] buckets = new int[10][arr.length]; //从个位开始 int base = 10; for (int i = 0; i < maxDight; i++) { //各个桶中元素个数 int[] temp = new int[10]; //1.分配:将所有元素分配到桶中 for (int j = 0; j < arr.length; j++) { //确定当前位的数字 int num = (arr[j] % base) / (base / 10); buckets[num][temp[num]] = arr[j]; //num桶中元素个数+1 temp[num]++; } //收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备, // 由于靠近桶底的元素排名靠前,因此从桶底先捞 int k = 0; for (int b = 0; b < buckets.length; b++) { //第b个桶中共有temp[b]个元素 for (int p = 0; p < temp[b]; p++) { arr[k++] = buckets[b][p]; } } //本轮结束,开始下一位的分配收集 base *= 10; } System.out.println("排序后:" + Arrays.toString(arr)); } } 复杂度 以下是基数排序算法复杂度,其中k为最大数的位数: 平均时间复杂度最好情况最坏情况空间复杂度 O(d*(n+r))O(d*(n+r))O(d*(n+r))O(n+r) 其中,d 为位数,r为基数,n 为原数组个数。 在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的,均为O(d*(n + r))。 基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。 适用场景 数据范围较小,建议在小于1000 每个数值都要大于等于0 基数排序更适合用于对时间, 字符串等这些整体权值未知的数据进行排序。 桶概念算法对比 基数排序、计数排序、桶排序三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异: 基数排序:根据键值的每位数字来分配桶 计数排序:每个桶只存储单一键值 桶排序:每个桶存储一定范围的数值

2019/9/12
articleCard.readMore

排序7:归并排序

原文作者:Mr.Seven 原文地址:八大排序算法总结与java实现 ❤查看排序算法动态演示❤查看排序算法动态演示❤查看排序算法动态演示 归并排序 (Merging Sort) 归并排序是建立在归并操作上的一种有效的排序算法。 归并算法用到了分治法的思想,且各层分治递归可以同时进行。(快速排序也引入了分治思想) 基本思想 归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。 动态示意图如下: 算法描述 归并排序可通过两种方式实现: 自上而下的递归 自下而上的迭代 递归法(假设共有n个元素) 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素; 重复步骤2,直到所有的元素排序完毕 迭代法 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 设定两个指针,最初位置分别为两个已经排序序列的起始位置 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 重复步骤③直到某一指针到达序列尾 将另一序列剩下的所有元素直接复制到合并序列尾 Java代码实现 归并排序其实要做两件事: 分解:将序列每次折半拆分 合并:将划分后的序列段两两排序合并 因此,归并排序实际上就是两个操作,拆分+合并 如何合并? L[first…mid]为第一段,L[mid+1…last]为第二段,并且两端已经有序,现在我们要将两端合成达到L[first…last]并且也有序。 首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[] 重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[] 此时将temp[]中的元素复制给L[],则得到的L[first…last]有序 如何分解? 在这里,我们采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列分组;直到分组后组内只有一个元素,此时我们认为组内所有元素有序,则分组结束。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 /** * 描述: * 归并排序 * * @author lars * @date 2019/9/11 11:46 */ public class MergingSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 26, 13, 27, 49, 55, 4}; System.out.println("排序前:" + Arrays.toString(arr)); System.out.println(Arrays.toString(mergingSort(Arrays.copyOf(arr,arr.length)))); } private static int[] mergingSort(int[] arr) { if (arr.length <= 1) { return arr; } int num = arr.length >> 1; int[] leftArr = Arrays.copyOfRange(arr, 0, num); int[] rightArr = Arrays.copyOfRange(arr, num, arr.length); System.out.println("split two array: " + Arrays.toString(leftArr) + " And " + Arrays.toString(rightArr)); //递归:将序列拆分为若干个最小单元后进行合并 return mergeTwoArray(mergingSort(leftArr), mergingSort(rightArr)); } private static int[] mergeTwoArray(int[] arr1, int[] arr2) { int i = 0, j = 0, k = 0; //申请额外的空间存储合并之后的数组 int[] result = new int[arr1.length + arr2.length]; //选取两个序列中的较小值放入新数组 while (i < arr1.length && j < arr2.length) { if (arr1[i] <= arr2[j]) { result[k++] = arr1[i++]; } else { result[k++] = arr2[j++]; } } //序列1中多余的元素移入新数组 while (i<arr1.length){ result[k++]=arr1[i++]; } //序列2中多余的元素移入新数组 while (j<arr2.length){ result[k++]=arr2[j++]; } // System.out.println("Merging: " + Arrays.toString(result)); return result; } } 由上, 长度为n的数组, 最终会调用mergeSort函数2n-1次。通过自上而下的递归实现的归并排序, 将存在堆栈溢出的风险。 复杂度 从效率上看,归并排序可算是排序算法中的”佼佼者”,假设数组长度为n,那么拆分数组共需logn,又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlogn)。 另一方面, 归并排序多次递归过程中拆分的子数组需要保存在内存空间, 其空间复杂度为O(n)。 平均时间复杂度最好情况最坏情况空间复杂度 O(nlog2n)O(nlog2n)O(nlog2n)O(n) 交换元素时,可以在相等的情况下做出不移动的限制,所以归并排序是可以稳定的; 和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。 适用场景 若n较大,则应采用时间复杂度为O(nlog2(n))的排序方法:快速排序、堆排序或归并排序 但是如果对算法的稳定性有所要求的话,即相同元素的顺序不被改变,那这时就可以使用归并排序。 因为通过之前文章的分析可以知道快速排序、堆排序都是不稳定的排序算法。

2019/9/11
articleCard.readMore

排序6:快速排序

原文作者:Mr.Seven 原文地址:八大排序算法总结与java实现 ❤查看排序算法动态演示❤查看排序算法动态演示❤查看排序算法动态演示 快速排序 (Quick Sort) 快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高! 快速排序是一种分而治之思想在排序算法上的典型应用。快速排序是在冒泡排序的基础上进行的改良,快排在面试中也是常客。 基本思想 快速排序的基本思想:挖坑填数+分治法。 先选择一个元素作为基准,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。 动态演示如图,其中第一个基准元素位最后一个元素 算法描述 快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为: 从数列中挑出一个元素,称为”基准”(pivot)。 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。 递归到最底部时,数列的大小是0或1,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。 如图所示:数组[7,5,6,3,5,1,2,9,5,8,4],以第一个元素7为基准 首次分区后可以得到[5,6,3,5,1,2,4,5]和[7,8,9]两组数据,继续以第一个元素为基准进行两个数组的分区 如此循环往复,即可得到有序序列 Java实现 用伪代码描述如下: i = L; j = R; 将基准数挖出形成第一个坑a[i]。(L:左侧下标起点,R:右侧下标起点) j–,由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。 i++,由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。 再重复执行2,3二步,直到i==j,将基准数填入a[i]中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class QuickSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 26, 13, 27, 49, 55, 4}; System.out.println("排序前:" + Arrays.toString(arr)); int[] a = Arrays.copyOf(arr, arr.length); quicksort1(a, 0, a.length - 1); } private static void quicksort1(int[] arr, int low, int high) { if (arr.length <= 0 || low >= high) { return; } int leftIndex = low; int rightIndex = high; //挖坑1:保存基准的值 int temp = arr[leftIndex]; while (leftIndex < rightIndex) { //坑2:从后向前找到比基准小的元素,插入到基准位置坑1中 while (leftIndex < rightIndex && arr[rightIndex] >= temp) { //不做交换 rightIndex--; } //右侧数据小于基准,进行交换 arr[leftIndex] = arr[rightIndex]; //坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中 while (leftIndex < rightIndex && arr[leftIndex] <= temp) { leftIndex++; } //左侧数据大于基准,进行交换 arr[rightIndex] = arr[leftIndex]; } //基准值填补到坑3中,准备分治递归快排 arr[leftIndex] = temp; quicksort1(arr, low, leftIndex - 1); quicksort1(arr, leftIndex + 1, high); } } 上面是递归版的快速排序:通过把基准temp插入到合适的位置来实现分治,并递归地对分治后的两个划分继续快排。 那么非递归版的快排如何实现呢? 因为递归的本质是栈,所以我们非递归实现的过程中,可以借助栈来保存中间变量就可以实现非递归了。在这里中间变量也就是通过partition函数划分区间之后分成左右两部分的首尾指针,只需要保存这两部分的首尾指针即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 /** * 快速排序(非递归) * * ①. 从数列中挑出一个元素,称为"基准"(pivot)。 * ②. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。 * ③. 把分区之后两个区间的边界(low和high)压入栈保存,并循环①、②步骤 * @param arr 待排序数组 */ public class QuickSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 26, 13, 27, 49, 55, 4}; System.out.println("排序前:" + Arrays.toString(arr)); quickSortByStack(Arrays.copyOf(arr, arr.length)); } private static void quickSortByStack(int[] arr) { if (arr.length <= 0) { return; } Stack<Integer> stack = new Stack<Integer>(); stack.push(0); stack.push(arr.length - 1); while (!stack.empty()) { int high = stack.pop(); int low = stack.pop(); int pivot = partition(arr, low, high); if (pivot > low) { stack.push(low); stack.push(pivot - 1); } if (pivot < high && pivot >= 0) { stack.push(pivot + 1); stack.push(high); } } System.out.println("排序后:" + Arrays.toString(arr)); } private static int partition(int[] arr, int low, int high) { if (arr.length <= 0 || low >= high) { return -1; } int leftIndex = low; int rightIndex = high; //挖坑1:保存基准的值 int pivot = arr[leftIndex]; while (leftIndex < rightIndex) { //坑2:从后向前找到比基准小的元素,插入到基准位置坑1中 while (leftIndex < rightIndex && arr[rightIndex] >= pivot) { //不做交换 rightIndex--; } //右侧数据小于基准,进行交换 arr[leftIndex] = arr[rightIndex]; //坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中 while (leftIndex < rightIndex && arr[leftIndex] <= pivot) { leftIndex++; } //左侧数据大于基准,进行交换 arr[rightIndex] = arr[leftIndex]; } //基准值填补到坑3中,准备分治递归快排 arr[leftIndex] = pivot; return leftIndex; } } 复杂度 快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。 但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。 为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。 平均时间复杂度最好情况最坏情况空间复杂度 O(nlog2n)O(nlog2n)O(n²)O(1) 快速排序排序效率非常高。 虽然它运行最糟糕时将达到O(n²)的时间复杂度, 但通常平均来看, 它的时间复杂为O(nlogn), 比同样为O(nlogn)时间复杂度的归并排序还要快. 适用场景 快速排序似乎更偏爱乱序的数列, 越是乱序的数列, 它相比其他排序而言, 相对效率更高.

2019/9/10
articleCard.readMore

排序5:冒泡排序

本文参考于:八大排序算法总结与java实现 ❤查看排序算法动态演示❤查看排序算法动态演示❤查看排序算法动态演示 冒泡排序 (Bubble Sort) 冒泡应该是大部分同学第一个接触到的排序算法,冒泡在面试中也有很高的出现频率。所以务必要将其掌握。 基本思想 冒泡排序依次遍历要排序的元素序列,依次比较两个相邻的元素,如果他们的顺序错误就进行交换。如此往复,知道待排序列中没有相邻元素要交换时排序完成。 其动态演示如图: 从其动态图可以看出,冒泡排序法在每轮遍历后都会将最大或者最小的元素慢慢的浮到顶端,这种下现象就像气泡上浮一般,所以算法命名冒泡排序 算法描述 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 JAVA实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /** * 描述: * 冒泡排序 * * @author lars * @date 2019/9/9 15:41 */ public class BubbleSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 26, 13, 27, 49, 55, 4}; System.out.println("排序前:" + Arrays.toString(arr)); Bubble(Arrays.copyOf(arr, arr.length)); } /*** * 若序列原本有序,会有多余的遍历和比较 * @param arr */ private static void Bubble(int[] arr) { //控制外层比较轮数 for (int i = 1; i < arr.length; i++) { //控制内层需要参与比较的序列大小 for (int j = 0; j < arr.length - i; j++) { //相邻元素比较 if (arr[j] > arr[j + 1]) { //交换 int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } System.out.println("排序后:" + Arrays.toString(arr)); } } 冒泡排序还有一种优化算法,就是建立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序,即可结束遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /*** * 优化版本,若序列有序,1次遍历后直接结束 * @param arr */ private static void BubbleDemo(int[] arr) { for (int i = 1; i < arr.length; i++) { // 设定一个标记 boolean flag = true; for (int j = 0; j < arr.length - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; flag = false; } } if (flag) { //若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。 break; } } } 复杂度 最好情况:序列原本有序(一轮遍历)时间复杂度为O(n) 最差情况:序列为反序,时间复杂度为O(n²) 平均来讲, 时间复杂度为O(n²), 冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1). Tips: 由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法. 平均时间复杂度最好情况最坏情况空间复杂度 O(n²)O(n)O(n²)O(1) 适用场景 主要看数据的顺序情况,如果数据本身已经是离最终排序结果不远的,通过加个交换标识,冒泡排序可能是更快的。 所以所有排序算法的试用性都是分场景来看的,但是不得不承认冒泡排序在性能要求高的场景下,通用性不高

2019/9/9
articleCard.readMore

排序4:堆排序

本文参考于:八大排序算法总结与java实现 堆排序 (Head Sort) 1991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同发明了著名的堆排序算法(Heap Sort). 堆的定义如下:n个元素的序列 {k1,k2,⋅⋅⋅,kn} 当且仅当满足下关系时,称之为堆。 小顶堆:{ki≤k2iki≤k2i+1大顶堆:{ki≥k2iki≥k2i+1(i=1,2,...,⌊n2⌋)小顶堆:\begin{cases}k_i \leq k_{2i}\\\\k_i \leq k_{2i+1}\end{cases} 大顶堆:\begin{cases}k_i \geq k_{2i}\\\\k_i \geq k_{2i+1}\end{cases} (i=1,2,...,\left \lfloor \frac{n}{2} \right \rfloor)小顶堆:⎩⎪⎨⎪⎧​ki​≤k2i​ki​≤k2i+1​​大顶堆:⎩⎪⎨⎪⎧​ki​≥k2i​ki​≥k2i+1​​(i=1,2,...,⌊2n​⌋) 其中的两种情况又分为小顶堆和大顶堆 把此序列对应的二维数组看成一个完全二叉树。 那么堆的含义就是:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左,右孩子节点的值。 由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。 因此我们可使用大顶堆进行升序排序, 使用小顶堆进行降序排序。 基本思想 先将序列建立堆,然后输出堆顶元素,然后再将剩下的序列在建立新的堆,在输出堆顶元素,以此类推,直到所有元素均输出为止,此时输出的元素序列即为有序序列 动态演示如下图所示: 算法描述 先将初始序列K[1…n]建成一个大顶堆, 那么此时第一个元素K1最大, 此堆为初始的无序区 再将关键字最大的记录K1 (即堆顶, 第一个元素)和无序区的最后一个记录 Kn 交换,并将Kn取出, 由此得到新的无序区K[1…n-1]和有序区K[n] 交换K1 和 Kn 后, K1在堆顶可能违反堆性质, 因此需将K[1…n-1]调整为堆. 然后重复步骤2, 直到无序区只有一个元素时停止. 简单总结可以分为三个部分操作 建立堆(大顶堆or小顶堆) 取出堆顶元素,将最后一个元素放在堆顶 重建堆,重复步骤2 Java实现 对于堆节点的访问: 父节点i的左子节点在位置:(2*i+1); 父节点i的右子节点在位置:(2*i+2); 子节点i的父节点在位置:floor((i-1)/2); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 /** * 描述: * 堆排序 * * @author zhengql * @date 2019/9/8 15:48 */ public class HeadSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 26, 13, 27, 49, 55, 4}; System.out.println("排序前:" + Arrays.toString(arr)); HeadSort1(Arrays.copyOf(arr, arr.length)); HeadSort2(Arrays.copyOf(arr, arr.length)); } private static void HeadSort2(int[] arr) { int len = arr.length; while (len > 0) { //1.建堆 buidHead(arr, len); //2.取出堆顶(把堆顶与数组最后一个元素进行交换) int temp = arr[0]; arr[0] = arr[len - 1]; arr[len - 1] = temp; //3.重新建堆 len = len - 1; } System.out.println("排序后:" + Arrays.toString(arr)); } private static void buidHead(int[] arr, int len) { for (int i = len / 2; i >= 0; i--) { int leftIndex = 2 * i + 1; int rightIndex = leftIndex + 1; //默认父节点最大 int maxIndex = i; if (leftIndex < len && arr[leftIndex] > arr[maxIndex]) { maxIndex = leftIndex; } if (rightIndex < len && arr[rightIndex] > arr[maxIndex]) { maxIndex = rightIndex; } //最大值确认,如果父节点不是最大则交换 if (maxIndex != i) { int temp = arr[i]; arr[i] = arr[maxIndex]; arr[maxIndex] = temp; } } } private static void HeadSort1(int[] arr) { //每次循环数组长度缩小1,相当于取出堆顶元素 for (int i = arr.length; i > 0; i--) { //建堆 buildMaxHead(arr, i); //堆顶元素与Kn交换,下次循环开始前取出Kn(数组长度减1) int temp = arr[0]; arr[0] = arr[i - 1]; arr[i - 1] = temp; } System.out.println("排序后:" + Arrays.toString(arr)); } private static void buildMaxHead(int[] arr, int limit) { if (arr.length <= 0 || arr.length < limit) { return; } //从最后一个非叶子节点开始比较,进行建堆 int parentIdx = limit / 2; for (; parentIdx >= 0; parentIdx--) { if (parentIdx * 2 + 1 >= limit) { continue; } //左子节点位置 int left = parentIdx * 2 + 1; //右子节点位置,如果没有右节点,默认为左节点位置 int right = (left + 1) >= limit ? left : (left + 1); int maxChildId = arr[left] >= arr[right] ? left : right; //交换父节点与左右子节点中的最大值 if (arr[maxChildId] > arr[parentIdx]) { int temp = arr[parentIdx]; arr[parentIdx] = arr[maxChildId]; arr[maxChildId] = temp; } } } } 复杂度 以上, 建立堆的过程, 从length/2 一直处理到0, 时间复杂度为O(n); 调整堆的过程是沿着堆的父子节点进行调整, 执行次数为堆的深度, 时间复杂度为O(lgn); 堆排序的过程由n次第2步完成, 时间复杂度为O(nlgn). 平均时间复杂度最好情况最坏情况空间复杂度 O(nlog2n)O(nlog2n)O(nlog2n)O(1) Tips: 由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序. 适用场景 由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列. 在堆排序算法分析过程中可以发现,堆排序通过构建堆,率先将最大或者最小的元素找出来。 所以,堆排序往往适用于,不需要对序列整体排序,只需要找到最大或者最小元素的场景

2019/9/8
articleCard.readMore

排序3:选择排序

原文作者:Mr.Seven 原文地址:八大排序算法总结与java实现 ❤查看排序算法动态演示❤查看排序算法动态演示❤查看排序算法动态演示 选择排序(Selection Sort) 从算法逻辑上看,选择排序是一种简单直观的排序算法,在简单选择排序过程中,所需移动记录的次数比较少。 基本思想 选择排序的基本思想:比较 + 交换。 在未排序序列中找到最小(大)元素,存放到未排序序列的起始位置。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。 算法描述 从待排序序列中,找到关键字最小的元素; 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换; 从余下的 N - 1 个元素中,找出关键字最小的元素,重复1、2步,直到排序结束。 JAVA代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 /** * 描述: * 选择排序 * * @author lars * @date 2019/9/7 17:37 */ public class SelectionSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 76, 13, 27, 49, 55, 4}; System.out.println("排序前:"+Arrays.toString(arr)); selectSort1(Arrays.copyOf(arr,arr.length)); } /** * 选择排序 * * 1. 从待排序序列中,找到关键字最小的元素; * 2. 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换; * 3. 从余下的 N - 1 个元素中,找出关键字最小的元素,重复①、②步,直到排序结束。 * 仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 * @param arr 待排序数组 */ private static void selectSort1(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { //待排序列第一个默认为最小 int min = i; for (int j = i + 1; j < arr.length; j++) { //依次与其他元素比较找出最小元素的下标 if (arr[j] < arr[min]) { min = j; } } //最小元素下标不是i,则将i与最小下标所在元素交换位置 if (min!=i){ int temp = arr[min]; arr[min] = arr[i]; arr[i]=temp; } } System.out.println("排序后:"+Arrays.toString(arr)); } } 复杂度 平均时间复杂度最好情况最坏情况空间复杂度 O(n²)O(n²)O(n²)O(1) 选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”, 无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。 即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。 适应场景 当数据量不大,且对稳定性没有要求的时候,适用于选择排序。

2019/9/7
articleCard.readMore

排序2:希尔排序

❤查看排序算法动态演示❤查看排序算法动态演示❤查看排序算法动态演示 希尔排序 (Shell Sort) 希尔排序 也称做递减增量排序算法,1959年Shell发明,是插入排序的一种高速而稳定的改进版本 基本思想 希尔排序是先将整个待排序的记录序列分割成若干个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,在对全体记录进行依次直接排序 例如上图中的待排序数组:[49,38,65,97,76,13,27,49,55,4] 将数组按5个间隔为一组划分成5组子序列,每个子序列进行插入排序后,各个子序列就变成了有序的了(整体不一定有序) 将上一步得到的数组按2个间隔为一组划分成3组子序列,各个子序列进行插入排序 将上一步得到的数组按正常插入排序,此时序列基本有序,所以效率较高 上面提到的间隔可以称作增量, 一般初始增量取数组的一半长度, 每轮排序后,增量减半,直至增量为1(存在多种增量序列) 算法描述 选择一个增量序列t1,t2,…,tk,其中t1>t2,tk=1;(一般初次取数组半长,之后每次再减半,直到增量为1) 按增量序列个数k,对序列进行k 趟排序; 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 如下图,其中H表示增量 Java代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import java.util.Arrays; /** * 描述: * * @author zhengql * @date 2019/9/6 11:35 */ public class ShellSort { public static void main(String[] args) { int[] arr = {49, 38, 65, 97, 76, 13, 27, 49, 55, 4}; System.out.println("排序前:"+Arrays.toString(arr)); int[] a = Arrays.copyOf(arr,arr.length); shellsort1(a); int[] b = Arrays.copyOf(arr,arr.length); shellsort2(arr); } /** * 希尔排序(Wiki官方版) * * 1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;(注意此算法的gap取值) * 2. 按增量序列个数k,对序列进行k 趟排序; * 3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。 * 仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 * @param arr 待排序数组 */ private static void shellsort2(int[] arr) { int gap = 1, i, j, len = arr.length; int temp; while (gap < len / 3){ // <O(n^(3/2)) by Knuth,1973>: 1, 4, 13, 40, 121, ... gap = gap * 3 + 1; } for (; gap > 0; gap /= 3) { for (i = gap; i < len; i++) { temp = arr[i]; for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap){ arr[j + gap] = arr[j]; } arr[j + gap] = temp; } } System.out.println("排序后:"+Arrays.toString(arr)); } /*** * 个人实现 * @param arr */ private static void shellsort1(int[] arr) { //根据增量g进行分组,g初始状态为数组长度一半 for (int g = arr.length / 2; g > 0; g /= 2) { for (int i = g; i < arr.length; i++) { //待插入数为arr[i] int inserted = arr[i]; int j; //待插入数,与当前组内的序列进行依次比较 for (j = i - g; j >= 0 && inserted < arr[j]; j -= g) { //待插入数小于他前面的数,进行交换 arr[j + g] = arr[j]; } arr[j + g] = inserted; } } System.out.println("排序后:"+Arrays.toString(arr)); } } 复杂度 希尔排序的复杂度与增量有关,不同的增量会产生不同的复杂度 像我们思路分析中的数组对半取值为增量5,直至为1,其实并不是最优增量序列。 平均时间复杂度最好情况最坏情况空间复杂度 O(n^1.25)O(n)O(n²)O(1) 适用场景 希尔排序时直接插入排序的优化版,解决了直接插入排序在面对大量数据时的效率低问题。 希尔排序适用于大规模无序数组的排序,且相对于直接插入排序数组越大优势越大

2019/9/6
articleCard.readMore

排序1:直接插入排序

原文作者:Mr.Seven 原文地址:八大排序算法总结与java实现 ❤查看排序算法动态演示❤查看排序算法动态演示❤查看排序算法动态演示 直接插入排序(Insertion Sort) 插入排序的设计初衷是往有序的数组中快速插入一个新的元素。它的算法思想是:把要排序的数组分为了两个部分, 一部分是数组的全部元素(除去待插入的元素), 另一部分是待插入的元素; 先将第一部分排序完成, 然后再插入这个元素. 其中第一部分的排序也是通过再次拆分为两部分来进行的. 插入排序由于操作不尽相同, 可分为 直接插入排序 , 折半插入排序(又称二分插入排序), 链表插入排序 , 希尔排序 。我们先来看下直接插入排序。 基本思想 将数组中所有元素依次和之前已经排序好的元素序列相比较,如果选择的元素比已排序的元素小,则进行交换,直到所有元素都比较过为止 动态示意图如下: 算法描述 一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下: 从第一个元素开始,该元素可以认为已经被排序 取出下一个元素,在已经排序的元素序列中从后向前扫描 如果该元素(已排序)大于新元素,将该元素移到下一位置 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置 将新元素插入到该位置后 重复步骤②~⑤ 如下图所示: 算法实现中比较有意思的一点是,在每次比较操作发现取出来的新元素小于等于已排序的元素时,可以将已排序的元素移到下一位置, 然后将取出来的新元素插入该位置(即相邻位置对调),接着再与前面的已排序的元素进行比较,如上图所示,这样做缺点是交换操作代价比较大。 另一种做法是:将新元素取出(挖坑),从左到右依次与已排序的元素比较,如果已排序的元素大于取出的新元素,那么将该元素移动到下一个位置(填坑), 接着再与前面的已排序的元素比较,直到找到已排序的元素小于等于新元素的位置,这时再将新元素插入进去。就像基本思想中的动图演示的那样。 如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。可以认为是插入排序的一个变种,称为二分查找插入排序。 Java代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import java.util.Arrays; /** * 描述: * 直接插入排序 * * @author lars * @date 2019/9/5 16:30 */ public class InsertionSort { public static void main(String[] args) { int[] a = {1, 4, 8, 2, 5}; insertSort1(a); System.out.println(Arrays.toString(a)); int[] aa = {1, 4, 8, 2, 5}; insertSort2(aa); System.out.println(Arrays.toString(aa)); } /*** * 交换次数较多实现 * @param a */ private static void insertSort2(int[] a) { for (int i =0 ; i<a.length-1;i++){ for(int j=i+1;j>0;j--){ if (a[j-1]>a[j]){ //j为待排序元素,j-1为前一位元素,j-1>j交换 int temp = a[j]; a[j]=a[j-1]; a[j-1]=temp; }else { //待排序元素大于他前1位元素,位置不变,循环结束 break; } } } } /*** * 交换次数较少 * @param a */ private static void insertSort1(int[] a) { for (int i = 1; i < a.length; i++) { //直接取出第二个元素开始比较,第一个元素默认已完成排序 int temp = a[i]; // temp与他前面的有序元素依次比较,找到自己的位置 for (int j = i; j >= 0; j--) { //temp小于他前一位的元素,交换位置, // 继续循环比较(j>0如果不成立,说明已经比较到0位置,说明temp属于当前最小,直接放当前位置) if (j > 0 && a[j - 1] > temp) { a[j] = a[j - 1]; //相互交换(可以先不进行移动) //a[j-1] = temp; } else { //temp大于他前面的元素,temp就放置在当前位置 a[j] = temp; //该元素位置确定,结束循环,到下一个 break; } } } } } 复杂度 直接插入排序复杂度如下: 最好情况下,排序前对象已经按照要求的有序。比较次数(KCN):n−1;移动次数(RMN)为0。则对应的时间复杂度为O(n)。 最坏情况下,排序前对象为要求的顺序的反序。第i趟时第i个对象必须与前面i个对象都做排序码比较,并且每做1次比较就要做1次数据移动(从上面给出的代码中看出)。比较次数(KCN):n²/2 ; 移动次数(RMN)为:n²/2。则对应的时间复杂度为O(n²)。 如果排序记录是随机的,那么根据概率相同的原则,在平均情况下的排序码比较次数和对象移动次数约为n²/2,因此,直接插入排序的平均时间复杂度为O(n²)。 平均时间复杂度最好情况最坏情况空间复杂度 O(n²)O(n)O(n²)O(1) Tips: 由于直接插入排序每次只移动一个元素的位, 并不会改变值相同的元素之间的排序, 因此它是一种稳定排序。

2019/9/5
articleCard.readMore

推荐一款博客一文多发的良心工具OpenWrite

前言 许多网友想看一文多发的OpenWrite,今天,它来了!别问落地价,因为内测无价! 这款实用工具,可支持十大博客平台一键发布,是博主们的发文神器 你看它多种平台、一键管理、后台界面优雅、还有签到计划 怎么样,还不来体验一下(编不下去了😂) Open Write官网:https://www.openwrite.cn 题外话 我也是去年刚刚开始写博客的小菜鸡,虽然不会有人来爬我写的小白文(有我也不知道哇😂) 但是出于技术博主的惯例,大家都希望自己的文章可以被更多人看到,于是写完一篇博客之后会有以下操作: 思否发布->简书发布->CSDN发布->博客园发布->掘金发布->… 呕心沥血创作一篇文章已经掉了很多头发了,没想到还要再重复发布操作N次,每次写完文章要发布时都头皮发麻 但是、But…救星来了,OpenWrite,一款让你专注文章创作,而不用为文章发布而烦恼的平台工具。 目前支持的技术平台 思否 CSDN 掘金 博客园 简书 知乎 开源中国 Spring4All 技术社区 今日头条 据群内大佬们讨论,后期可能会加入其他渠道(个人博客等) 目前已经支持微信公众号一键复制功能,大大节省了公众号文章的维护和处理。 特色功能 OpenWrite除了核心功能一文多发外,还有两个特色功能。 签到计划 签到计划是我们为每位博主提供的粘性工具,读者通过二维码扫描参与签到计划,而二维码可以由博主放置在任意平台的博文下,参考示例如自律到极致-人生才精致:第12期 点击链接查看 赞助平台 如果说签到计划是博主与读者的互动,那么赞助平台就是博主与赞助商的互动。 举个栗子: 我是一个写博客的,我有很多读者很多阅读量。 XXX是一个出版社的,需要为某本或者某批书做宣传。 这里两个角色就达成了供求关系,博主就是流量主,出版社的就是赞助商。 详细使用介绍可以参考OpenWrite 赞助平台全流程说明 使用体验 目前OpenWrite还属于内测期间,功能也都在完善中,如果你也有一文多发的烦恼不妨来体验下 我已经用了两个星期了,贼好用😁😁😁 Open Write官网:https://www.openwrite.cn Open Write内测入口: 感谢 最后还是要感谢各位OpenWrite的开发大佬 程序猿DD、泥瓦匠,等等其他大佬们。

2019/8/28
articleCard.readMore

近期学习计划

前几天报名了软考(软件设计师),本来应该大学就应该拿下的证,当时没有重视,现在重新开始备考 备考前给自己规划了如下计划,希望在复习备考的同时可以巩固计算机相关的基础知识 这里给自己立个🏳‍🌈flag 认真复习,一次通过 复习资料 《软件设计师教程》-第五版 《软件设计师考试冲刺(习题与解答)》 《软件设计师真题精析与命题密卷》 B站视频教程 软件设计师资料包, 提取码:gnuc : 内含视频(2018年)、讲义、教程、真题、模拟题…等等等 备考安排如下: step1😄: 考试大纲,考试内容、重难点梳理(8.18前结束) step2😋: 一轮复习+刷题(9.29前结束) 根据《考试冲刺》中的考点提炼去《软件设计师教程》中复习知识,复习完后回来做《考试冲刺》中的习题,并纠错记录 step3😘: 二轮复习+刷题(10.27前结束) 刷历年真题卷,刷题后回《软件设计师教程》复习总结 step4😎: 最后冲刺汇总知识点(11.9结束) 刷冲刺题、模拟题。 打卡计划: 每天至少2个小时复习时间 每天记录当日学习内容(打卡记录不定时更新) 每周总结复习进度

2019/8/27
articleCard.readMore

Nacos(九):Nacos集群部署和遇到的问题

前言 前面的系列文章已经介绍了Nacos的如何接入SpringCloud,以及Nacos的基本使用方式 之前的文章中都是基于单机模式部署进行讲解的,本文对Nacos的集群部署方式进行说明 环境准备 JDK8 Centos7.5(ip:10.1.8.27) MySQL 5.6.5+ Nacos-server:1.0.1 请提前下载Nacos-server:1.0.1压缩包并解压至相应目录 本次的Nacos-server在linux服务器上进行启动。 集群模式部署 Nacos文档中提供了三种集群部署方案 http://ip1:port/openAPI 直连ip模式: ip+端口进行部署,客户端直接连接Nacos的ip http://Vip:port/openAPI 挂载虚拟IP模式: 配合KeepAlive,Nacos真实ip都挂载虚拟Ip下 客户端访问Vip发起请求 当主Nacos宕机后,备用Nacos接管,实现高可用, http://www.nacostest.com:port/openAPI 挂载虚拟IP+域名模式: 为虚拟ip绑定一个域名,当Nacos集群迁移时,客户端配置无需修改。 这三种方案都是为了尽可能实现高可用,后两种方案除了基本的部署流程外更多侧重于实现高可用的工作上 本文以第一种ip+端口的方式为大家介绍集群部署方式 当然ip+端口也有多种部署方式 1ip+n端口+Nginx:普通玩家部署方式,没有过多服务器,单台服务器启动多个nacos实例,仅适合测试使用 nip+n端口+Nginx:RMB玩家部署方式,服务器资源充足,组建完美集群,实现更好的容灾与隔离 无论怎么部署,部署方式都是一样的,这里我以1ip+3端口+Nginx的方式进行集群搭建 修改配置 1、修改Nacos-server目录conf/下的application.properties文件,添加mysql数据源 2、修改集群配置 ip和端口的规划如下: ip端口 10.1.8.278849 10.1.8.278850 10.1.8.278851 修改conf/下的cluster.conf.example文件,将其命名为cluster.conf,内容如下 1 2 3 4 # ip:port 10.1.8.27:8849 10.1.8.27:8850 10.1.8.27:8851 注:一定要记得将配置文件重命名为cluster.conf, 最好用实际ip,而非127.0.0.1,否则会出现问题 3、修改启动脚本 我们要在单台服务器上启动多个Nacos实例,要保证三个实例为不同的端口,这里我们可以修改启动脚本 定位到export FUNCTION_MODE="all"这一行,修改脚本内容,使其支持以-p传入端口参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export MODE="cluster" export FUNCTION_MODE="all" # 新加 export SERVER_PORT="8848" while getopts ":m:f:p:" opt do case $opt in m) MODE=$OPTARG;; f) FUNCTION_MODE=$OPTARG;; # 新加 p) SERVER_PORT=$OPTARG;; ?) echo "Unknown parameter" exit 1;; esac done # 新加 JAVA_OPT="${JAVA_OPT} -Dserver.port=${SERVER_PORT}" 相应的,修改shutdown脚本,使其可接收参数 1 2 3 4 5 6 7 8 9 10 11 # 新加内容 PORT=$1 if [ ! $PORT ]; then echo "please select stop port!" >&2 exit 1 fi # 添加PORT过滤 pid=`ps ax | grep -i 'nacos.nacos' |grep java |grep ${PORT} | grep -v grep | awk '{print $1}'` # 后边省略... 启动Nacos 执行如下三条命令 bash startup.sh -p 8849 bash startup.sh -p 8850 bash startup.sh -p 8851 **如果你的机器不能同时启动3个实例,检查是否内存不够了,可以适当调整JVM参数 ** 调整启动脚本中JAVA_OPT="${JAVA_OPT} -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m中的-Xms -Xmx -Xmn 启动成功后会打印如下一句话: nacos is starting,you can check the /usr/local/nacos/nacos/logs/start.out 查看该目录下的start.out文件,可以看到如下日志 此时,多节点的集群雏形已经搭建好了,可以试着访问Nacos后台 http://ip:8849/nacos/index.html http://ip:8850/nacos/index.html http://ip:8851/nacos/index.html 不出意外,可以正常访问到Nacos控制台 从图上可以看到集群节点共有三个,其中端口8850为leader 配置Nginx 完成上面的配置后,已经基本完成集群搭建的90%了 这里我们可以通过Nginx配置,为Nacos提供统一的入口,来实现一个简单的负载均衡 Nginx配置如下 1 2 3 4 5 6 7 8 9 10 11 12 13 upstream nacos-server { server 127.0.0.1:8849; server 127.0.0.1:8850; server 127.0.0.1:8851; } server { listen 8848; server_name localhost; location /nacos/ { proxy_pass http://nacos-server/nacos/; } } 执行命令 sudo nginx启动nginx 通过8848端口访问Nacos后台,此时Nginx会将请求分发至nacos-server下的地址中,这里默认的分发策略是线性轮询 客户端测试 这里继续使用之前的Demo项目(Nacos(四)) 1、修改下项目配置文件bootstarp.yml 1 2 3 4 5 6 7 8 9 10 11 spring: application: name: nacos-config cloud: nacos: discovery: server-addr: 10.1.8.27:8848 config: server-addr: 10.1.8.27:8848 prefix: ${spring.application.name} file-extension: yml 注:主要是修改注册中心和配置中新的地址,记得替换成你的服务器地址哦 2、启动前确保已经向Nacos中添加配置文件,如果已经添加可以忽略 在公共空间(public)中新建一个配置文件DataID: nacos-config.yml, 配置内容如下: 1 2 3 4 server: port: 9989 nacos: config: 配置文件已持久化到数据库中... 3、启动Nacos-config项目 启动成功后检查日志和Nacos控制台 注册成功的日志如下: 09:37:19.366 [main] [nacos] [64] INFO o.s.c.a.n.r.NacosServiceRegistry - nacos registry, nacos-config 10.1.8.71:9989 register finished 控制台中可以看到服务已经注册 浏览器请求nacos-config中的接口,查看是否可以读取配置 访问:http://127.0.0.1:9989/getValue 返回:配置文件和服务信息已持久化到数据库中… 集群的部署方式就介绍完了,文中的集群部署方式不是最优的方式 实际生产环境下,至少应该保证各个Nacos节点位于不同服务器,以实现更好的隔离和容灾 出现的问题 这里将我在配置集群时出现的一些问题和解决方式进行说明 1、在集群搭建完成后,启动nacos客户端进行服务注册时报错了,提示我服务端没有启动,稍后再试,如下: code:503 msg: server is STARTING now, please try again later! 一脸懵B,Nacos后台都可以访问的鸭,为啥说我没启动。翻阅Nacos-server的源码后 后台可访问只能说明nacos-consloe模块启动成功 无法注册服务nacos-naming模块可能启动失败了 参考了github中的#770-issues 必须保证`InetAddress.getLocalHost().getHostAddress()`或者`hostname -i`打印的结果是 192.xxx.xxx.xxx而不是`127.0.0.1`才行 解决办法如下: 修改hosts,在hosts文件中添加yourip hostname,例如:10.1.8.27 lars 修改cluster.conf,修改集群配置文件,全部用实际ip+端口的方式,而非127.0.0.1 总结 Nacos的集群部署基本就介绍完了,官方推荐的三种方式,他们的基本部署思路和方式都大同小异,只不过在高可用上有所不同,挑选你适合的方式动手搭建集群试试吧。 参考 Nacos支持三种部署模式 社区issues

2019/8/16
articleCard.readMore

Nacos(八):Nacos持久化

前言 前景回顾: Nacos(七):Nacos共享配置 Nacos(六):多环境下如何“管理”及“隔离”配置和服务 Nacos(五):多环境下如何“读取”Nacos中相应环境的配置 Nacos(四):SpringCloud项目中接入Nacos作为配置中心 前面的七篇文章,从Nacos介绍,到Nacos做注册中心、做配置中心,一直都没有提及持久化的问题。 我们服务的信息、配置的信息都放在哪的? 当我们使用默认配置启动Nacos时,所有配置文件都被Nacos保存在了内置的数据库中。 在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力 如果使用内嵌数据库,注定会有存储上限,本文带大家一起将Nacos中的数据实现持久化 我的环境 Windows10 JDK8 SpringCloud:Finchley.RELEASE SpringBoot:2.0.4.RELEASE spring-cloud-alibaba-dependencies:0.2.2.RELEASE Nacos-server:1.0.1 本文的项目Demo继续沿用之前文章中的聚合工程Nacos,若小伙伴还没有之前的环境,可至源码地址中下载 本文的演示环境为:windows平台 项目准备 本文使用Nacos(四)项目代码作为演示 项目内容不做修改 安装数据库 目前Nacos仅支持Mysql数据库,且版本要求:5.6.5+ 初始化数据库 Nacos的数据库脚本文件在我们下载Nacos-server时的压缩包中就有 进入nacos-server-1.0.1\nacos\conf目录,初始化文件:nacos-mysql.sql 此处我创建一个名为 mynacos 的数据库,然后执行初始化脚本,成功后会生成 11 张表 修改配置文件 这里是需要修改Nacos-server的配置文件 Nacos-server其实就是一个Java工程或者说是一个Springboot项目,他的配置文件在nacos-server-1.0.1\nacos\conf目录下,名为 application.properties,在文件底部添加数据源配置: 1 2 3 4 5 6 spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/mynacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=123456 启动Nacos-server和Nacos-config 先启动Nacos-server,启动方式不变,直接双击执行nacos-server-1.0.1\nacos\bin下的startup.cmd文件 启动成功后进入Nacos控制台,此时的Nacos控制台中焕然一新,之前的数据都不见了 因为加入了新的数据源,Nacos从mysql中读取所有的配置文件,而我们刚刚初始化的数据库是干干净净的,自然不会有什么数据和信息显示。 在公共空间(public)中新建一个配置文件DataID: nacos-config.yml, 配置内容如下: 1 2 3 4 server: port: 9989 nacos: config: 配置文件已持久化到数据库中... 再启动Nacos(四)中的demo项目。服务启动成功后,观察Nacos控制台如下 验证是否持久化到数据库中 观察数据库mynacos中的数据库表 config_info , 如下 请求一下接口 http://127.0.0.1:9989/getValue 返回结果: 配置文件已持久化到数据库中… 总结 Nacos通过集中式存储来保证数据的持久化,同时也为Nacos集群部署奠定了基础 试想一下,如果我们以之前的方式启动Nacos,如果想组建Nacos集群,那各个节点中的数据唯一性就是最大的问题 Nacos采用了单一数据源,直接解决了分布式和集群部署中的一致性问题。 参考和感谢 Spring Cloud Alibaba基础教程:Nacos的数据持久化

2019/8/12
articleCard.readMore

Nacos(七):Nacos共享配置

前言 前景回顾: Nacos(六):多环境下如何“管理”及“隔离”配置和服务 Nacos(五):多环境下如何“读取”Nacos中相应环境的配置 Nacos(四):SpringCloud项目中接入Nacos作为配置中心 前几章已经基本介绍了springcloud项目结合Nacos的大部分用法,本文介绍一下Nacos作为配置中心时,如何读取共享配置 我的环境 Windows10 JDK8 SpringCloud:Finchley.RELEASE SpringBoot:2.0.4.RELEASE spring-cloud-alibaba-dependencies:0.2.2.RELEASE Nacos-server:1.0.1 本文的项目Demo继续沿用之前文章中的聚合工程Nacos,若小伙伴还没有之前的环境,可至源码地址中下载 场景描述 一个项目中服务数量增加后,配置文件相应增加,多个配置文件中会存在相同的配置,那么我们可以将相同的配置独立出来,作为该项目中各个服务的共享配置文件,每个服务都可以通过Nacos进行共享配置的读取 下面用一个demo演示下,是否可行 demo工程:nacos-config-share 配置文件:nacos-config-share.yml 共享配置文件:shareconfig1.yml,shareconfig2.yml 创建项目 一如往常,还是在聚合工程Nacos下创建名为nacos-config-share的子工程,其pom.xml文件依赖与之前的项目都一致,如果您没有之前的项目可参考源码地址 1、修改springboot启动类NacosConfigShareApplication.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @SpringBootApplication @EnableDiscoveryClient @RestController @RefreshScope public class NacosConfigShareApplication { public static void main(String[] args) { SpringApplication.run(NacosConfigShareApplication.class, args); } @Value("${nacos.share}") private String share; @Value("${share.config1}") private String shareConfig1; @Value("${share.config2}") private String shareConfig2; @RequestMapping("/getValue") public String getValue() { return share; } @RequestMapping("/getShare1") public String getShare1() { return shareConfig1; } @RequestMapping("/getShare2") public String getShare2() { return shareConfig2; } } 2、修改该项目的配置文件bootstrap.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 spring: application: name: nacos-config-share cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml shared-dataids: shareconfig1.yml,shareconfig2.yml refreshable-dataids: shareconfig1.yml,shareconfig2.yml 从配置文件可以看出,通过shared-dataids属性来指定要读取共享配置文件的DataID,多个文件用,分隔 使用refreshable-dataids指定共享配置文件支持自动刷新 新建配置文件 这里我们作为演示,暂不加入Namespace,直接在公共空间中创建及测试 创建配置文件nacos-config-share.yml,详细如下: DataId:nacos-config-share.yml 配置格式:YAML 配置内容: 1 2 3 4 server: port: 9984 nacos: share: nacos-config-share 创建共享配置文件1shareconfig1.yml,详细如下: DataId:shareconfig1.yml 配置格式:YAML 配置内容: 1 2 share: config1: 这里是共享配置文件1 创建共享配置文件1shareconfig2.yml,详细如下: DataId:shareconfig2.yml 配置格式:YAML 配置内容: 1 2 share: config2: 这里是共享配置文件2 创建成功后,配置列表如下图: 启动测试 直接启动项目,如果启动成功。可以看到日志中如下信息: 访问启动类中提供的接口,测试下能否获取到共享配置文件中的值 1 2 3 访问127.0.0.1:9984/getValue,返回:nacos-config-share 访问127.0.0.1:9984/getShare1,返回:这里是共享配置文件1 访问127.0.0.1:9984/getShare2,返回:这里是共享配置文件2 再测试下refreshable-dataids配置的自动刷新是否生效 在Nacos控制台中修改共享配置文件shareconfig2.yml的值为:这里是共享配置文件2这里是共享配置文件2 编辑保存后,重新请求 127.0.0.1:9984/getShare2 ,观察返回结果如下: 这里是共享配置文件2这里是共享配置文件2 以上返回结果说明通过在配置文件中指定shared-dataids和refreshable-dataids是可以实现共享配置文件的读取和自动刷新的。 需求变更 假设现在要读取shareconfig3.yml和shareconfig4.yml文件但是它的Group为SHARE3_GROUP和SHARE4_GROUP, 即共享配置文件与项目自身配置文件不在同一Group中(上边的例子是全都在DEFAULT_GROUP分组) 那如果继续用上边的方法,就无法读取共享配置文件 这时可以使用另一个配置ext-config,它可以由用户自定义指定需要加载的配置DataID、Group以及是否自动刷新 并且ext-config是一个集合(List),支持多个配置文件的指定。 新建共享配置文件 先创建配置配置文件shareconfig3.yml和shareconfig4.yml,注意他们的Group属性 DataId:shareconfig3.yml Group:SHARE3_GROUP 配置格式:YAML 配置内容: 1 2 share: config3: 这里是共享配置文件3,Group:SHARE3_GROUP DataId:shareconfig4.yml Group:SHARE4_GROUP 配置格式:YAML 配置内容: 1 2 share: config4: 这里是共享配置文件4,Group:SHARE4_GROUP 创建成功页面如下: 修改项目代码 1、在启动类NacosConfigShareApplication.java中新增如下代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Value("${share.config3}") private String shareConfig3; @Value("${share.config4}") private String shareConfig4; @RequestMapping("/getShare3") public String getShare3() { return shareConfig3; } @RequestMapping("/getShare4") public String getShare4() { return shareConfig4; } 2、修改项目配置文件bootstrap.yml,增加ext-config配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 spring: application: name: nacos-config-share cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml shared-dataids: shareconfig1.yml,shareconfig2.yml refreshable-dataids: shareconfig1.yml,shareconfig2.yml ext-config: - data-id: shareconfig3.yml group: SHARE3_GROUP refresh: true - data-id: shareconfig4.yml group: SHARE4_GROUP refresh: true 启动进行测试 项目经过修改后,可以看到 项目自身的nacos配置文件属于DEFAULT_GROUP下,默认读取 shareconfig1.yml,shareconfig2.yml 都属于DEFAULT_GROUP下,通过shared-dataids指定进行读取 shareconfig3.yml,shareconfig4.yml 都属于非DEFAULT_GROUP下,通过ext-config配置属性进行自定义读取 启动项目,测试所有的配置文件是否可以正常读取 1 2 3 4 5 访问127.0.0.1:9984/getValue,返回:nacos-config-share 访问127.0.0.1:9984/getShare1,返回:这里是共享配置文件1 访问127.0.0.1:9984/getShare2,返回:这里是共享配置文件2这里是共享配置文件2 访问127.0.0.1:9984/getShare3,返回:这里是共享配置文件3,Group:SHARE3_GROUP 访问127.0.0.1:9984/getShare4,返回:这里是共享配置文件4,Group:SHARE4_GROUP 修改shareconfig4.yml的配置内容为:这里是共享配置文件4,Group:SHARE4_GROUP,支持自动刷新,保存后,再次调用127.0.0.1:9984/getShare4,返回如下: 这里是共享配置文件4,Group:SHARE4_GROUP,支持自动刷新 调用接口后发现,两种共享配置的加载方式都可以正常读取,并且可以一起使用。ext-config的方式实现了用户自定义配置共享配置文件。 总结 上面的demo已经演示Nacos共享配置的两种实现方式,两种方式针对不同的场景,总结如下: shared-dataids方式: 适合于共享配置文件与项目默认配置文件处于相同Group时,直接两条命令就可以搞定 优点:配置方便 缺点:只能在同一Group中 ext-config方式: 它可以由开发者自定义要读取的共享配置文件的DataId、Group、refresh属性,这样刚好解决了shared-dataids存在的局限性。 优点:可以与shared-dataids方案结合使用,用户自定义配置。灵活性强 缺点:配置容易出错,要熟悉YAML语法 可见两种方式各有长处,所以如果在开发中需要使用共享配置,大家可以是具体情况而定选择自己最合适的方案。 本文源码:https://github.com/larscheng/larscheng-learning-demo/tree/master/Nacos 参考 SpringCloud Alibaba - Nacos Config 自定义共享配置

2019/8/7
articleCard.readMore

Nacos(六):多环境下如何“管理”及“隔离”配置和服务

前言 前景回顾: Nacos(五):多环境下如何“读取”Nacos中相应环境的配置 Nacos(四):SpringCloud项目中接入Nacos作为配置中心 现如今,在微服务体系中,一个系统往往被拆分为多个服务,每个服务都有自己的配置文件,然后每个系统往往还会准备开发环境、测试环境、正式环境 我们来说算一算,假设某系统有10个微服务,那么至少有10个配置文件吧,三个环境(dev\test\prod),那就有30个配置文件需要进行管理。 这么多的配置文件,要修改一个或者多个的时候,稍有不慎可能就会出现改错了、不生效…等等问题。 那么如果引入Nacos作为配置中心后,如何有效的进行配置文件的管理和不同环境间的隔离区分呢? 别担心,Namespace可以帮助我们进行多环境下的管理和隔离 有了上一篇文章的介绍,本文主要从以下几个方面介绍: Namespace是什么 Namespace如何进行配置和服务的管理、隔离 创建和获取NamespaceID Namespace实施方案1 Namespace实施方案2 Namespace Nacos引入了命名空间(Namespace)的概念来进行多环境配置和服务的管理及隔离 Namespace也是官方推荐的多环境支持方案。 如何进行配置和服务的管理、隔离 当我们的服务达到一定的数量,集中式的管理许多服务会十分不便, 那我们可以将这些具有相同特征或属性的服务进行分组管理,服务对应的配置也进行分组隔离 这里的分组就是Namespace的概念,将服务和配置纳入相同的Namespace进行管理 不同Namespace下的服务和配置之间就隔离开来 创建和获取NamespaceID NamespaceId值是在配置文件配置时必须要填入的配置项,所以需要我们先创建Namespace和Id,步骤如下: nacos 的控制台左边功能栏看到有一个命名空间的功能,点击就可以看到新建命名空间 的按钮 新建成功后,可以在命名空间列表中查看到你所创建的Namespace和他生成的ID值 这里只是讲解创建步骤,本文继续延用Nacos(五)中创建的DEV、TEST Namespace实施方案1 Nacos给出了两种Namespace的实践方案 面向一个租户 面向多个租户 方案1主要说明一下面向一个租户 从一个租户(用户)的角度来看,如果有多套不同的环境,那么这个时候可以根据指定的环境来创建不同的 namespce,以此来实现多环境的隔离。 例如,你可能有dev,test和prod三个不同的环境,那么使用一套 nacos 集群可以分别建以下三个不同的 namespace。如下图所示: 这里的单租户同样也适于小型项目,或者是项目不太多时的实施方案 通过定义不同的环境,不同环境的项目在不同的Namespace下进行管理,不同环境之间通过Namespace进行隔离 当多个项目同时使用该Nacos集群时,还可以通过Group进行Namespace内的细化分组 这里以Namespace:dev为例,在Namespace中通过不同Group进行同一环境中不同项目的再分类 有了以上思路,我们通过代码来实践一下 Namespace下新建配置文件 启动Nacos-Server,进入Nacos控制台,切换到Namespace:dev界面,新建配置文件 DataId:nacos-namespace-one-dev.yml Group:namespace-one 配置格式:YAML 配置内容: 1 2 nacos: config: 项目:nacos-namespace-one,Namespace:dev 继续新建配置文件 DataId:nacos-namespace-two-dev.yml Group:namespace-two 配置格式:YAML 配置内容: 1 2 nacos: config: 项目:nacos-namespace-two,Namespace:dev 切换到Namespace:test环境,按照dev中的创建方式,分别创建nacos-namespace-one-test.yml和nacos-namespace-two-test.yml 注意检查DataId是否正确、group、配置内容与环境是否匹配 创建项目 在聚合工程Nacos下创建名为nacos-namespace-one的子项目,该工程的依赖文件和启动类的代码与Nacos(四)完全一致。 以下NamespaceId均来自创建Namespace时生成的Id,在控制台命名空间页面中可以查看 创建dev环境配置文件bootstrap-dev.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 server: port: 9911 spring: application: name: nacos-namespace-one profiles: active: dev cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: edbd013b-b178-44f7-8caa-e73071e49c4d group: namespace-one config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml namespace: edbd013b-b178-44f7-8caa-e73071e49c4d group: namespace-one 创建test环境配置文件bootstrap-dev.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 server: port: 9912 spring: application: name: nacos-namespace-one profiles: active: test cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: 0133bd1e-25c3-4985-96ed-a4e34efdea2e group: namespace-one config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml namespace: 0133bd1e-25c3-4985-96ed-a4e34efdea2e group: namespace-one 重复以上操作,再创建一个名为nacos-namespace-two的子项目 nacos-namespace-two项目的dev和test启动端口分别设置为9921和9922,group为:namespace-two 记得修改spring.application.name、namespace和group 启动工程 分别启动两个项目的两个环境(四个启动类),如下图 现在我们有2个项目:nacos-namespace-one和nacos-namespace-two 2个项目分别有两个不同的环境dev和test 此时观察Nacos-Server控制台如下: 尝试访问接口来获取配置信息,验证是否可以读取相应环境配置 1 2 3 4 访问127.0.0.1:9911/getValue,返回:项目:nacos-namespace-one,Namespace:dev 访问127.0.0.1:9912/getValue,返回:项目:nacos-namespace-one,Namespace:test 访问127.0.0.1:9921/getValue,返回:项目:nacos-namespace-two,Namespace:dev 访问127.0.0.1:9922/getValue,返回:项目:nacos-namespace-two,Namespace:test 通过以上实验,方案1可以达到多环境多项目下的服务、配置管理的目标 方案1通过Namespace来隔离不同的环境(dev\test),在具体的环境Namespace中通过Group来管理不同的项目 Namespace实施方案2 了解了单租户的方案1,再来看看Nacos推荐的面向多租户的方案2 从多个租户(用户)的角度来看,每个租户(用户)可能会有自己的 namespace,每个租户(用户)的配置数据以及注册的服务数据都会归属到自己的 namespace 下,以此来实现多租户间的数据隔离。 例如超级管理员分配了三个租户,分别为张三、李四和王五。张三负责A项目,李四负责B项目,王五负责C项目 分配好了之后,各租户用自己的账户名和密码登录后,创建自己的命名空间。如下图所示: 方案2通过Namespace来隔离多租户之间的服务和配置,但不仅于此,他有很好的扩展性 在该方案中,Group同样也有用武之地。 需求改变下,公司发展迅速业务调整,张三负责A项目、B项目、C项目,李四负责D项目、E项目、F项目,王五负责G项目、H项目、I项目, 而每个项目又分了dev、test、prod三个环境,继续沿用之前的Namespace隔离租户方案,显得有些管理不便,这时候可以在NameSpace中加入Group进行项目环境分组,如图: 但是当业务规模更大的时候(不考虑Nacos集群能否支持的因素),张三、李四、王五每人都负责10多个项目时,即项目数>环境数时,可以通过Group进行项目分组,如下图: 通过上面的理论分析,可以看出方案二有很好的扩展性 依旧如上,我们通过代码来实践一下方案2(Namespace隔离租户 + group环境分组) 场景描述 依旧使用上面的两个项目,假设现在有两个租户,张三、李四 张三负责项目:nacos-namespace-one, 李四负责项目:nacos-namespace-two,项目分别有dev和test环境 新建Namespace和配置文件 新建两个Namespace来隔离租户,分别为zhangsan、lisi 在Namespace:zhangsan 下创建配置文件 DataId:nacos-namespace-one-dev.yml Group:namespace-one-dev 配置格式:YAML 配置内容: 1 2 nacos: config: 项目:nacos-namespace-one,Namespace:张三,环境:dev 继续创建test环境配置文件 DataId:nacos-namespace-one-test.yml Group:namespace-one-test 配置格式:YAML 配置内容: 1 2 nacos: config: 项目:nacos-namespace-one,Namespace:张三,环境:test 参照以上操作,在Namespace:lisi命名空间中创建配置文件nacos-namespace-two-dev.yml和nacos-namespace-two-test.yml 注意核对DataId、Group、和配置内容 修改项目的配置文件bootstrap.yml 修改项目nacos-namespace-one的dev配置文件bootstrap-dev.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 server: port: 9911 spring: application: name: nacos-namespace-one profiles: active: dev cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # 方案2:NamespaceID\Group namespace: e0d75068-a12c-4314-9296-3f396139d5b3 group: namespace-one-dev config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml # 方案2:NamespaceID\Group namespace: e0d75068-a12c-4314-9296-3f396139d5b3 group: namespace-one-dev 修改test配置文件bootstrap-test.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 server: port: 9912 spring: application: name: nacos-namespace-one profiles: active: test cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # 方案2:NamespaceID\Group namespace: e0d75068-a12c-4314-9296-3f396139d5b3 group: namespace-one-test config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml # 方案2:NamespaceID\Group namespace: e0d75068-a12c-4314-9296-3f396139d5b3 group: namespace-one-test 重复以上操作相应的修改项目nacos-namespace-two的dev和test配置文件 主要修改namespace和group属性,与命名空间lisi的ID和其下配置文件的Group对应 启动项目 分别启动两个项目的两个环境(四个启动类),启动成功如下图 此时两个项目分别启动两个环境后,注册到Nacos上不同的Namespace下,并读取相应环境的配置,具体如下: nacos-namespace-one dev: 注册到Namespace:zhangsan,读取Namespace:zhangsan下Group:namespace-one-dev的配置 test: 注册到Namespace:zhangsan,读取Namespace:zhangsan下Group:namespace-one-test的配置 nacos-namespace-two dev: 注册到Namespace:lisi,读取Namespace:lisi下Group:namespace-two-dev的配置 test: 注册到Namespace:lisi,读取Namespace:lisi下Group:namespace-two-test的配置 此时Nacos控制台如下图: ok我们来测试下各个环境的服务能否访问到对应的配置 1 2 3 4 访问127.0.0.1:9911/getValue,返回:项目:nacos-namespace-one,Namespace:张三,环境:dev 访问127.0.0.1:9912/getValue,返回:项目:nacos-namespace-one,Namespace:张三,环境:test 访问127.0.0.1:9921/getValue,返回:项目:nacos-namespace-two,Namespace:李四,环境:dev 访问127.0.0.1:9922/getValue,返回:项目:nacos-namespace-two,Namespace:李四,环境:test 通过访问服务的接口,各个服务都可以准确的读取到各自环境下的配置文件 方案二可以看到同样支持服务和配置的隔离分组,同时支持业务的扩展,有较好的扩展性 问题描述 但是相信大家已经发现了一个问题,当使用的Group来进行分组后,配置文件相互之间可以实现不同环境与不同项目之间的分组隔离 但是服务注册后,虽然可以通过Namespace隔离,但指定的Group分组却并没有生效,依然是DEFAULT_GROUP 比如方案1 所有项目启动后Nacos服务列表页如下图 这里本应该是我们自定义的分组namespace-one和namespace-two却没有生效 由此发现,配置之间是达到了相互分组隔离名但服务列表暂时并不支持。 但是不要担心,Nacos的社区极度活跃,社区的大佬们也发现了这一情况,并且在Nacos-client的源码中可以看到NameingService在加载配置文件时是有预留Group这一属性字段的。 所以既然Nacos提供了这一实践方案,正常使用只不过是时间问题。 总结 以上分析了Nacos对于Namespace提供的两种实践方案,同时进行了代码实验,均达到了预期的要求。 现对两种方案进行一个总结 单租户方案(方案1):适合小型项目,服务数量不多时,方案一完全够用 多租户方案(方案2):适合项目量多,有一定的团队规模,且服务数量较多时,可以相对条理清晰的管理和隔离配置及服务。 本文源码:https://github.com/larscheng/larscheng-learning-demo/tree/master/Nacos 参考与感谢 Namespace最佳实践

2019/8/6
articleCard.readMore

Nacos(五):多环境下如何“读取”Nacos中相应的配置

前言 前景回顾: Nacos(四):SpringCloud项目中接入Nacos作为配置中心 Nacos(三):Nacos与OpenFeign的对接使用 Nacos(二):SpringCloud项目中接入Nacos作为注册中心 通过前面几篇介绍,已经基本了解了Nacos做为注册中心和配置中心的基本用法。 在实际开发中,通常一个系统会准备开发环境、测试环境、预发环境、正式环境 那么如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢 本文主要对Nacos作为配置中心时,如何对多环境配置文件进行读取进行论述。 我的环境 Windows10 JDK8 SpringCloud:Finchley.RELEASE SpringBoot:2.0.4.RELEASE spring-cloud-alibaba-dependencies:0.2.2.RELEASE Nacos-server:1.0.1 本文的项目Demo继续沿用 Nacos(四)中的工程nacos-config,若小伙伴还没有之前的环境,可至源码地址中下载 Data ID方案 在上一篇文章中有对Data ID进行过介绍,它的命名规则为:${prefix}-${spring.profile.active}.${file-extension} 通过其中的spring.profile.active属性即可进行多环境下配置文件的读取 一起来试一下吧~ 新建配置 1、启动Nacos-Server后,创建配置文件Data ID为:nacos-config-dev.yml, 其配置如下: 1 2 3 4 server: port: 9980 nacos: config: 这里是dev环境 2、继续创建配置文件Data ID为:nacos-config-test.yml, 其配置如下: 1 2 3 4 server: port: 9981 nacos: config: 这里是test环境 多环境测试 通过Idea启动nacos-config项目,并指定spring.profiles.active,通过不同的环境进行启动 通过上面的配置,将项目分为dev、test两个环境启动后,进行测试 访问 http://127.0.0.1:9980/getValue 返回:这里是dev环境 访问 http://127.0.0.1:9981/getValue 返回:这里是test环境 可以看到,分别以dev、test启动后相应的读取到不同的配置,dev环境读取到启动端口为9980,test读取到启动端口9981 Group方案 上面介绍了通过指定spring.profile.active和配置文件的DataID来使不同环境下读取不同的配置 这里也可以不用DataID,直接通过Group实现环境区分 注:这种方式不太推荐,切换不灵活,需要切换环境时要改Gruop配置 新建配置 1、创建配置文件Data ID为:nacos-config.yml, Group为:DEV_GROUP, 其配置如下: 1 2 3 4 server: port: 9980 nacos: config: 这里是dev环境 2、继续创建配置文件Data ID为:nacos-config.yml, Group为:TEST_GROUP, 其配置如下: 1 2 3 4 server: port: 9981 nacos: config: 这里是test环境 这里的两个配置文件他们的DataID相同但是Group不同 修改项目中的配置文件bootstrap.yml 在config下增加一条group的配置,指定配置文件所在的group,可配置为DEV_GROUP或TEST_GROUP 1 2 3 4 5 6 7 8 9 10 11 12 spring: application: name: nacos-config cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml group: DEV_GROUP 启动测试 将group配置为DEV_GROUP启动进行测试 访问 http://127.0.0.1:9980/getValue 返回:这里是dev环境 将group配置为TEST_GROUP启动进行测试 访问 http://127.0.0.1:9981/getValue 返回:这里是test环境 通过指定group的方式启动,DEV_GROUP读取到启动端口为9980,TEST_GROUP读取到启动端口9981 说明 只通过Group来进行多环境的区分的方式我不推荐使用,因为涉及到了多环境自然就会改变spring.profile.active,而profile一旦生效,配置文件就会依据DataID的规则进行查找。所以Group的方式仅作参考。 Group的合理用法应该是配合namespace进行服务列表和配置列表的隔离和管理 Namespace方案 Namespace命名空间进行环境隔离也是官方推荐的一种方式。Namespace的常用场景之一是不同环境的配置的区分隔离,例如:开发测试环境和生产环境的资源(如配置、服务)隔离等。 创建命名空间 创建命名空间DEV和TEST,不同的命名空间会生成相应的UUID,如下图 新建配置文件 1、在命名空间DEV下创建DataID为:nacos-config.yml,Group为默认值的配置,配置如下: 1 2 3 4 server: port: 9980 nacos: config: 这里是DEV命名空间 2、在命名空间TEST下创建DataID为:nacos-config.yml,Group为默认值的配置,配置如下: 1 2 3 4 server: port: 9981 nacos: config: 这里是TEST命名空间 修改项目中的配置文件bootstrap.yml 在config下增加一条namespace的配置,指定当前配置所在的命名空间ID。注意是命名空间ID!!!配置如下 1 2 3 4 5 6 7 8 9 10 11 12 spring: application: name: nacos-config cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 prefix: ${spring.application.name} file-extension: yml namespace: edbd013b-b178-44f7-8caa-e73071e49c4d 启动测试 将namespace配置为DEV的ID:edbd013b-b178-44f7-8caa-e73071e49c4d,启动进行测试 访问 http://127.0.0.1:9980/getValue 返回:这里是DEV命名空间 将namespace配置为TEST的ID:0133bd1e-25c3-4985-96ed-a4e34efdea2e,启动进行测试 访问 http://127.0.0.1:9981/getValue 返回:这里是TEST命名空间 通过指定namespace的方式启动,均可读取到对应的启动端口和相关配置 说明 Namespace是官方推荐的环境隔离方案,确实有他的独到之处,使用namespace这种方案,同时可以与DataID+profile的方式结合 同时释放Group的限制,大大提高多环境配置管理的灵活性。 总结 通过上面三种方案的介绍,想必大家对于多环境下的配置读取方式应该有所选择 DataID: 适用于项目不多,服务量少的情况。 Group:实现方式简单,但是容易与DataID方案发生冲突,仅适合于本地调试 Namespace:实现方式简单,配置管理简单灵活,同时可以结合DataID共同使用,推荐这种方案 参考感谢 Nacos如何支持多环境 Nacos配置的多环境管理

2019/7/23
articleCard.readMore

Nacos(四):SpringCloud项目中接入Nacos作为配置中心

前言 通过前两篇文章: Nacos(二):Nacos与OpenFeign的对接使用 Nacos(三):SpringCloud项目中接入Nacos作为注册中心 相信大家已经对Nacos作为注册中心的基本使用已经有了一定的了解。 然而,Nacos远远不止于此,本文我们来看一下Nacos作为配置中心在SpringCloud中的使用 我的环境 Windows10 JDK8 SpringCloud:Finchley.RELEASE SpringBoot:2.0.4.RELEASE spring-cloud-alibaba-dependencies:0.2.2.RELEASE Nacos-server:1.0.1 本文的项目Demo继续沿用之前文章中的聚合工程Nacos,若小伙伴还没有之前的环境,可至源码地址中下载 启动Nacos-Server 进入bin文件夹(目录:nacos-server-1.0.1\nacos\bin),直接双击执行startup.cmd文件,这里具体的启动细节就不再说明 新建配置 在Nacos-Server中新建配置,其中Data ID它的定义规则是:${prefix}-${spring.profile.active}.${file-extension} prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置。 spring.profile.active 即为当前环境对应的 profile,可以通过配置项 spring.profile.active 来配置。 file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。 注意:当 spring.profile.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 prefix.{prefix}.prefix.{file-extension} 这里我创建Data Id 为nacos-config.yml的配置文件,其中Group为默认的DEFAULT_GROUP,配置文件的格式也相应的选择yaml,其内添加配置nacos.config=hello_nacos,如图所示 创建应用 1、在聚合工程Nacos下新建Module,创建一个名为nacos-config的子工程,其pom.xml文件内容如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>Nacos</artifactId> <groupId>com.study.www</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.larscheng.www</groupId> <artifactId>nacos-config</artifactId> <version>0.0.1-SNAPSHOT</version> <name>nacos-config</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 2、创建配置文件名为bootstrap.yml,注意是bootstrap.xxx,而不是application或者其他。原因如下 Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application 这里的配置文件类型可以根据个人习惯选择,我这里用的时yml类型,配置内容如下 1 2 3 4 5 6 7 8 9 10 11 spring: application: name: nacos-config cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 file-extension: yml 可以看到必须可少的配置项spring.application.name spring.cloud.nacos.discovery.server-addr指定注册中心的地址,如果你不需要注册该服务,也可以去掉该项,并删除discovery依赖 spring.cloud.nacos.config.server-addr指定配置中心的地址 file-extension指定配置中心中配置文件的格式 上面的配置是为了保证服务的正常注册和配置获取,以及配置DataID的正确性 3、创建对外接口来从nacos中读取配置,NacosConfigApplication.java修改如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootApplication @EnableDiscoveryClient @RestController @RefreshScope public class NacosConfigApplication { public static void main(String[] args) { SpringApplication.run(NacosConfigApplication.class, args); } @Value("${nacos.config}") private String config; @RequestMapping("/getValue") public String getValue() { return config; } } 其中通过@Value注解,去读取key为nacosconfig的配置的值,并通过/getValue接口返回。 加入@RefreshScope注解,可以使当前类下的配置支持动态更新。 到此代码部分的工作已经完成 启动测试 保证Nacos-Server已经启动 检查配置文件是否已经添加 启动nacos-config项目 启动成功后在Nacos控制台中可以看到我们注册的服务 此时调用接口进行测试 http://127.0.0.1:8080/getValue ,可以看到返回结果 hello_nacos 此时说明已经成功读取到配置,下面我将Nacos-Server上的配置修改为hello_lars,看看能否动态更新 调用接口进行测试 http://127.0.0.1:8080/getValue ,返回结果如下 hello_lars 可以看到我通过Nacos-server的控制台进行配置的修改,客户端服务nacos-config也相应的进行热更新。 总结 通过上面的测试,可见Nacos做配置中心,在SpringCloud项目中,也可以做到无缝衔接,切换到Nacos可以说知识修改配置的问题,成本也很低 项目的其他配置不变,只需要指定配置中心地址,同时将配置文件外部管理。 本文源码:https://github.com/larscheng/larscheng-learning-demo/tree/master/Nacos 参考 Nacos Spring Cloud

2019/7/23
articleCard.readMore

Nacos(三):Nacos与OpenFeign的对接使用

前言 上篇文章中,简单介绍了如何在SpringCloud项目中接入Nacos作为注册中心,其中服务消费者是通过RestTemplate+Ribbon的方式来进行服务调用的。 实际上在日常项目中服务间调用大都用的是OpenFeign, OpenFeign自身整合了Ribbon和Hystrix,为服务调用提供了更优雅的方式 那么接入了Nacos之后,服务调用还能用这一套吗? 通过我在公司项目上的试水,这个大胆的设想是完全没问题的 本文在上一篇文章中的项目工程基础上,进行测试和演示,文章地址:在SpringCloud项目中接入Nacos作为注册中心 创建项目 打开之前创建的工程Nacos,目前已经有两个子工程: nacos-provide:服务提供者 nacos-consumer:服务消费者(RestTemplate+Ribbon服务调用) 同样的操作,在Nacos项目下继续创建一个Springboot项目名为nacos-feign,创建时添加OpenFeign的依赖,如图: nacos-fegin的pom.xml文件如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>Nacos</artifactId> <groupId>com.study.www</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.larscheng.www</groupId> <artifactId>nacos-fegin</artifactId> <version>0.0.1-SNAPSHOT</version> <name>nacos-fegin</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 定义远程接口 创建RemoteClient接口,来定义OpenFeign要调用的远程服务接口。 同时通过@FeginClient注解指定被调用方的服务名,通过fallback属性指定RemoteHystrix类,来进行远程调用的熔断和降级处理。 RemoteClient.java代码如下 1 2 3 4 5 6 @FeignClient(name = "nacos-provide",fallback = RemoteHystrix.class) public interface RemoteClient { @GetMapping("/helloNacos") String helloNacos(); } RemoteHystrix.java代码如下 1 2 3 4 5 6 7 @Component public class RemoteHystrix implements RemoteClient { @Override public String helloNacos() { return "请求超时了"; } } 通过OpenFeign调用远程服务 在启动类NacosFeignApplication.java中添加注解@EnableDiscoveryClient开启服务注册、添加注解@EnableFeignClients开启OpenFeign,启动类通过OpenFeign调用服务代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootApplication @RestController @EnableDiscoveryClient @EnableFeignClients public class NacosFeignApplication { public static void main(String[] args) { SpringApplication.run(NacosFeginApplication.class, args); } @Autowired private RemoteClient remoteClient; @GetMapping("/feign") public String test() { return remoteClient.helloNacos(); } } 添加项目配置文件 在resourse目录下,添加application.yml配置 1 2 3 4 5 6 7 8 9 10 server: port: 9529 spring: application: name: nacos-feign cloud: nacos: discovery: server-addr: 127.0.0.1:8848 启动测试 启动Nacos-server 启动项目nacos-provide 启动项目nacos-feign 完成以上三步后,访问Nacos控制台,检查服务注册情况,如果启动都成功,你看到的应该是如下图: 浏览器访问 http://127.0.0.1:9529/feign, 可以看到返回结果与RestTemplate结果无异,但对于编码和操作方式都更加优雅。 访问nacos-feign的接口 http://127.0.0.1:9529/feign, 可以通过OpenFeign远程调用nacos-provide的接口,返回结果: 你好,nacos! 总结 OpenFegin整合Ribbon和Hystrix,为微服务中远程调用提供了一种更优雅的调用方式,它支持负载均衡和容错熔断机制。通过上面的例子,在SpringCloud中接入Nacos做注册中心后,并不会影响我们继续使用其他SpringCloud组件。 本文源码:https://github.com/larscheng/larscheng-learning-demo/tree/master/Nacos

2019/7/10
articleCard.readMore

Nacos(二):SpringCloud项目中接入Nacos作为注册中心

前言 通过上一篇文章:Nacos介绍 简单了解了Nacos的发展历程和现状,本文我们开始Nacos试水的第一步: 使用Nacos做注册中心 上周末(7.6)Nacos发布了V1.1.0版本,这次更新支持灰度配置、地址服务器模式、配置文件导入导出等其他功能。感觉社区的老哥们都很高产呐… 本文主要通过两个项目来完成演示: nacos-provide:服务提供者 nacos-consumer:服务消费者 将nacos-provide和nacos-consumer注册到Nacos-server,服务消费者nacos-consumer通过主动轮询获取他所订阅消费的服务信息列表nacos-consumer根据获取到的服务信息列表,进行服务调用。 熟悉SpringCloud+Eureka的同学阅读完本文后可以无缝切换Nacos做注册中心 我的环境 Windows10 JDK8 SpringCloud:Finchley.RELEASE SpringBoot:2.0.4.RELEASE spring-cloud-alibaba-dependencies:0.2.2.RELEASE Nacos-server:1.0.1 注:Nacos针对不同版本的SpingCloud提供不同的依赖,各个版本的对应关系请参考官方文档给出的说明:版本说明 启动Nacos-server Nacos-server可以直接从github上下载安装包,当然你也可以拉取代码后自行打包 本文我直接从官网下载Nacos-server:V1.0.1(为避免新版本V1.1.0有其他问题,我这里还是用V1.0.1) 下载地址: https://github.com/alibaba/nacos/releases 下载解压后进入bin文件夹(目录:nacos-server-1.0.1\nacos\bin),直接双击执行startup.cmd文件,启动成功如下图: 启动成功后,此时Nacos控制台就可以访问了,浏览器访问:http://127.0.0.1:8848/nacos/index.html ,默认的账号密码为nacos/nacos,控制台页面如下: 创建服务提供者 IDEA中创建聚合工程Nacos作为父工程,其pom.xml如下(重点关注dependencyManagement配置): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>larscheng-learning-demo</artifactId> <groupId>com.study.www</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <version>0.0.1-SNAPSHOT</version> <modelVersion>4.0.0</modelVersion> <modules> <module>nacos-provide</module> </modules> <artifactId>Nacos</artifactId> <properties> <java.version>1.8</java.version> <spring-boot.version>2.0.4.RELEASE</spring-boot.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> <nacos.version>0.2.2.RELEASE</nacos.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${nacos.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project> 在父工程Nacos下创建springboot子工程nacos-provide,其pom.xml文件为: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>Nacos</artifactId> <groupId>com.study.www</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.larscheng.www</groupId> <artifactId>nacos-provide</artifactId> <version>0.0.1-SNAPSHOT</version> <name>nacos-provide</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 在NacosProvideApplication.java中提供一个对外接口,并添加注解@EnableDiscoveryClient 开启服务注册发现功能: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @EnableDiscoveryClient @SpringBootApplication public class NacosProvideApplication { public static void main(String[] args) { SpringApplication.run(NacosProvideApplication.class, args); } @GetMapping("/helloNacos") public String helloNacos(){ return "你好,nacos!"; } } 配置文件application.yml进行如下配置 1 2 3 4 5 6 7 8 9 server: port: 9527 spring: application: name: nacos-provide cloud: nacos: discovery: server-addr: 127.0.0.1:8848 ok,服务提供者的创建和配置已经完成 创建服务消费者 仍然在Nacos工程下创建一个SpringBoot项目子工程命名为nacos-consumer,其pom文件与nacos-provide相同。 同样为nacos-consumer增加配置文件,内容如下 1 2 3 4 5 6 7 8 9 server: port: 9528 spring: application: name: nacos-consumer cloud: nacos: discovery: server-addr: 127.0.0.1:8848 服务消费者这里按照官方文档中的方式通过 RestTemplate+Ribbon进行服务调用, NacosConsumerApplication.java代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @SpringBootApplication @EnableDiscoveryClient @RestController public class NacosConsumerApplication { public static void main(String[] args) { SpringApplication.run(NacosConsumerApplication.class, args); } @Autowired private RestTemplate restTemplate; @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } @GetMapping("/consumer") public String test1() { String result = restTemplate.getForObject("http://nacos-provide/helloNacos",String.class); return "Return : " + result; } } ok,服务消费者的创建工作也已完成,下面启动两个项目,进行测试 调用测试 启动完成后,在日志中应该可以看到如下两条信息 o.s.c.a.n.registry.NacosServiceRegistry : nacos registry, nacos-provide 192.168.200.1:9527 register finished o.s.c.a.n.registry.NacosServiceRegistry : nacos registry, nacos-consumer 192.168.200.1:9528 register finished 现在登录Nacos控制台,你会发现服务列表中,已经显示了我们刚才创建的两个项目,并可以对其进行简单的监控和管理。 浏览器中访问服务消费者的接口 http://127.0.0.1:9528/consumer, 可以看到成功返回结果 Return : 你好,nacos! 总结 完成上面的服务发现和注册的测试后,我的第一感觉是,好像只用修改配置就可以替换Eureka,好像无缝支持SpringCloud , 带着这个内心的冲击感,我到公司的项目中简单试了下水,居然直接注册成功,并且各个服务之间正常使用,虽然只是单独的服务注册发现功能。但这能够说明Nacos天生就无缝衔接SpringCloud生态(当然他也有很多坑) 看Nacos控制台中的几大分类,明显Nacos的能力绝不仅仅是注册中心这么简单,更多Nacos的使用姿势和坑点,我们未完待续~ 本文源码:https://github.com/larscheng/larscheng-learning-demo/tree/master/Nacos 参考感谢 Nacos官方手册

2019/7/9
articleCard.readMore

Nacos(一):Nacos介绍

前言 6月份阿里开源的Nacos出了1.0.1版本,从去年7月份第一个release版本到现在一直在默默关注 官方的版本规划为:Nacos从0.8.0开始支持生产可用,1.0版本可大规模生产可用,2.0版本接入k8s、SpringCloud、ServiceMesh、ServerLess 公司目前的项目都是Springcloud,由于eureka2.X的断更、以及Nacos面世,所以自然而然最近就进行了一次试水爬坑,虽然过程艰苦,但是最终效果似乎还不错。 本文主要从以下几点来带大家熟悉下Nacos Nacos是什么 Nacos的发展历程 Nacos能做什么 Nacos无缝接入各大生态 Nacos是什么 Nacos是什么?好像没听过,不要紧。那Eureka听说过吧,在SpringCloud中做服务注册中心组件,类似的还有Zookeeper、Consul。 所以Nacos也是一个注册中心组件咯,当然是,不过它不仅仅是注册中心。 Nacos也是一个配置中心,比如SpringCloud中的Config,将配置文件版本化管理。 那么Nacos到底是什么呢, 总结为官网一句话就是: Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos的发展历程 首先要说Nacos的发展历程就要从阿里巴巴的内部产品ConfigServer说起了,因为Nacos是ConfigServer 的开源实现 早在2008年阿里就开始服务化的进程(那个时候我好像还在上初中啊),在那个时候阿里内部自研的服务发现解决方案就叫做ConfigServer ConfigServer经历了十年的发展从V1.0的单机版演变为目前对外公布的V4.0集群版。 2018年7月阿里巴巴高级技术专家许真恩(慕义)发布了Nacos首个开源版本V0.1.0,Nacos作为ConfigServer的开源实现截止目前已经更新到了V1.0.1的大版本,并且支持大规模生产版本。 Nacos能做什么 虽然官方文档也有介绍,但是语言比较官方,我就用大白话谈一点自己的使用感受。 服务注册发现和服务健康检测 Nacos支持基于DNS和基于RPC的服务发现,服务端可以通过SDK或者Api进行服务注册,相应的服务消费者可以使用DNS或者Http查找的方式获取服务列表。Nacos同时提供对服务的实时健康检查,阻止想不健康的主机或服务发送请求,与Eureka类似Nacos也有友好的控制台界面。 动态配置服务 接触过SpringCloud应该对config有所了解,那么配置中心也就很好理解,Nacos支持动态的配置管理,将服务的配置信息分环境分类别外部管理,并且支持热更新。不过与Config不同Nacos的配置信息存储与数据库中,支持配置信息的监听和版本回滚。 动态DNS服务 支持权重路由,更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。不过这个特性目前版本还不支持 服务及元数据管理 Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。 Nacos无缝接入各大生态 首先先上一张官方的生态图 除了对于阿里开源生态体系如 Dubbo 等自身的支持,也非常强调融入其它的开源生态,这里就包括 Java 的微服务生态体系 Spring Cloud,Kubernetes/CNCF 云原生生态体系。 Nacos 无缝支持 Spring Cloud,为 Spring Cloud 用户其提供更简便的配置中心和注册中心的解决方案。 Nacos支持目前几乎所有主流的微服务生态体系。 总结 Nacos从官方的介绍上看,就像是SpringCloud中Eureka+Config+Bus+Git+MQ的一个结合体,当然也不能完全这么理解。Nacos是脱胎于阿里内部的ConfigServer,而ConfigServer早在3.0版本就解决了Eureka在1.0版本留下的隐患,所以从技术的更新和迭代角度来看,稳定版本的Nacos将更适合做为微服务体系中的服务注册发现组件,当然了他也不单单只是注册和发现。更多的特性和功能,不如一起搭建试试吧。 参考与感谢 Nacos官方手册 来看看阿里自研服务注册中心产品ConfigServer

2019/7/9
articleCard.readMore

20190719小组分享

本次分享内容 技术分享:Nacos 非技术分享:实用工具站点推荐 ========== 技术内容Nacos Naming-Config-Service ========== SpringCloud生态注册中心现状 (CAP 原则?) Spring Cloud Eureka:内存、轮询、集群、2.0断更 Spring Cloud Zookeeper:内存,瘫痪 Spring Cloud Consul:生态、社区 == SpringCloud生态配置中心现状 Spring Cloud Config:git+Bus Spring Cloud Zookeeper:网络,cp模型 Spring Cloud Consul:生态 == Nacos现状 当前版本:1.1.0 Spring、Spring Boot、Spring Cloud、 Docker、Dubbo、{Kubernetes} roadmap == 未来趋势 总览 Spring Cloud Alibaba SCN与SCA SCA的宗旨,就是要替代SCN,成为Spring Cloud规范的默认实现方案 ========== 非技术内容 程序员的乐趣 ========== 实用工具篇 == 图床工具 PicGo == Tampermonkey 免费观看 百度网盘 过滤百度推广 Tampermonkey == 密码太多记不住 LastPass 临时邮箱 ChaCuo SMTP Server 填写邮箱时候,不想使用您的真实邮箱?那就使用临时邮箱 == git学习 Git闯关学习 == github树形结构 Octotree == Quickey Launcher 键盘书签 == 静态博客搭建 Hexo 10分钟搭建属于你自己的个人博客 推荐主题:Next、Melody… == Markdown写书 安装node.js npm install -g gitbook-cli gitbook init gitbook serve ok了 == 发现有趣项目 分享 GitHub 上有趣、入门级的开源项目 ========== GitHub篇 盘点那些神奇的GitHub仓 == 996.ICU 996.ICU == Chinese sticker pack Chinese sticker pack == hangzhou_house_knowledge 买房经历总结 == 网易云第三方 Listen 1 ieaseMusic == 全网视频站 i视频 == 百度网盘 百度网盘不限速客户端 == 福利 new-pac == 钢琴 键盘钢琴 ========== 番外篇 如何科学上网😏😏😏 == 访问不了的原因 dns污染 封ip 封代理 封vpn 其他… == 基本操作 第三方免费VPN(赛风、蓝灯) 公司VPN 直翻通道 免费SSR 自建 搭建教程 == 分享结束,感谢聆听…

2019/6/19
articleCard.readMore

Java中equals和HashCode方法的分析

前言 上一篇文章简单分析了equals()与==的关系,本文我们再来看看equals()与hashcode()的关系。hashcode的使用还是有很多坑的,一起看看吧~ 本文主要有以下几点来分析: hashCode使用中产生的问题 equals/hashcode的渊源 产生问题的原因 正确的使用姿势 hashCode使用中产生的问题 注:HashSet是一个无序、不可重复的集合,我们做一个小测试运行如下代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class HashEqualsDemo { static class Person { private String age; Person(String age) { this.age = age; } @Override public String toString() { return "Person{" + "age='" + age + '\'' + '}'; } } } public static void main(String[] args) { HashSet set1 = new HashSet(); set1.add("1"); set1.add("1"); for (Object a : set1) { System.out.println(a); } HashSet set2 = new HashSet(); Person p1 = new Person("1"); Person p2 = new Person("1"); set2.add(p1); set2.add(p2); for (Object a : set2) { System.out.println(a); } } } 由于HashSet是不可重复的集合,所以输出的结果中set1和set2中都应该只有一个元素,那么执行结果是什么呢?如下 1Person{age='1'}Person{age='1'} 好吧,又双叒叕和我想象的不一样,set1不重复,set2明显发生了重复现象,这是为什么呢? 这是因为equals、hashCode使用不规范导致的,问题且放在这,我们先看看equals和hashCode的关系 equals/hashcode的渊源 同为Object类中的方法 public boolean equals(Object obj) public int hashCode() equals(): 用来判断两个对象是否相同,再Object类中是通过判断对象间的内存地址来决定是否相同 hashCode(): 获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。 由于同为Object类中的方法,所以基本上所有Java类都会继承这两个方法,所以通过阅读hashCode方法的注释发现了: 概括为以下几点: 该方法返回对象的哈希码,支持该方法是为哈希表提供一些优点,例如,HashMap 提供的哈希表。 同一个对象未发生改变时多次调用hashCode()返回值必须相同, 两个对象equals不相等,那么两对象的hashCode()返回必定不同(此处可用来提高哈希表性能) 两个对象的hashCode()返回值相同,两对象不一定相同,还需要通过equals()再次判断 当equals方法被重写时,通常有必要重写 hashCode 方法 通过第1点其实可以看出,hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置,当对象不会用来创建像hashMap、hashSet等散列表时,hashCode()实际上用不上。 产生问题的原因 了解了两者的关系,我们在回过头来看看产生问题的原因 分析原因前需要了解哈希表的底层实现,hashCode在哈希表中充当的作用: 举一个栗子说明下: 假设内存中有0 1 2 3 4 5 6 7 8这8个位置,如果我有个字段叫做ID,那么我要把这个字段存放在以上8个位置之一,如果不用HashCode而任意存放,那么当查找时就需要到8个位置中去挨个查找 使用HashCode则效率会快很多,把ID的HashCode%8,然后把ID存放在取得余数的那个位置,然后每次查找该类的时候都可以通过ID的HashCode%8求余数直接找到存放的位置了 如果ID的HashCode%8算出来的位置上本身已经有数据了怎么办?这就取决于算法的实现了,比如ThreadLocal中的做法就是从算出来的位置向后查找第一个为空的位置,放置数据;HashMap的做法就是通过链式结构连起来。反正,只要保证放的时候和取的时候的算法一致就行了。 如果ID的HashCode%8相等怎么办(这种对应的是第三点说的链式结构的场景)?这时候就需要定义equals了。先通过HashCode%8来判断类在哪一个位置,再通过equals来在这个位置上寻找需要的类。对比两个类的时候也差不多,先通过HashCode比较,假如HashCode相等再判断equals。如果两个类的HashCode都不相同,那么这两个类必定是不同的。 其实在HashSet就是采用的这种存储和获取方式,通过HashCode和equals组合的方式来保证集合无重复。也说明了HashCode()在散列表中是发挥作用的 ok,我们分析下最开始的代码,找一下输出结果重复的原因(代码片段): 1 2 3 4 5 6 7 8 9 10 HashSet set1 = new HashSet(); set1.add("1"); set1.add("1"); HashSet set2 = new HashSet(); Person p1 = new Person("1"); Person p2 = new Person("1"); set2.add(p1); set2.add(p2); set1.add(“1”);:set1集合为空,找到hashCode对应在哈希表中的存储区,直接存入字符串1 set1.add(“1”);:首先判断该字符串1的hashCode值对应哈希表中所在的存储区域是否有相同的hashCode,此处调用String类中的hashCode(),显然两次返回了相同的hashCode,接着进行equals()方法的比较,此处调用String类中的equals(),由于两个字符串指向的常量池中的同一个字符串1,所以两个String对象相同,字符串1重复,不进行存储。 set2.add(p1);:set2集合为空,找到对象p1的hashCode对应在哈希表中的存储区,直接存入对象p1 set2.add(p2);:首先判断该对象p2的hashCode值对应哈希表中所在的存储区域是否有相同的hashCode,Person中未重写hashCode()此处调用Object类中的hashCode(),所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为p1、p2为不同对象,地址值不同,所以这里不存在与p2相同hashCode值的对象,直接存入对象p2 看到这里已经知道Set集合中出现重复的原因了。都是因为hashCode、equals的不规范使用。 正确的使用姿势 从Jdk源码的注释中可以看出,hashCode() 在散列表中才会发挥作用,当对象无需创建像HashMap、HashSet等集合时,可以不用重写hashCode方法,但是如果有使用到对象的哈希集合等操作时,必须重写hashCode()和equals()。 修改最初的代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class HashEqualsDemo { static class Person { private String age; Person(String age) { this.age = age; } //重写equals() @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof Person)) { return false; } //地址相同必相等 if (obj == this) { return true; } Person person = (Person) obj; //地址不同比较值是否相同 return person.age.equals(this.age); } //重写hashCode() @Override public int hashCode() { return Objects.hash(age); } @Override public String toString() { return "Person{" + "age='" + age + '\'' + '}'; } } } public static void main(String[] args) { HashSet set1 = new HashSet(); set1.add("1"); set1.add("1"); for (Object a : set1) { System.out.println(a); } HashSet set2 = new HashSet(); Person p1 = new Person("1"); Person p2 = new Person("1"); set2.add(p1); set2.add(p2); for (Object a : set2) { System.out.println(a); } } } 重写了equals和hashCode方法之后,执行结果就恢复正常了: 1Person{age='1'} 总结 hashCode主要用于提升查询效率提高哈希表性能,来确定在散列结构中对象的存储地址 重写equals()必须重写hashCode() 哈希存储结构中,添加元素重复性校验的标准就是先检查hashCode值,后判断equals() 两个对象equals()相等,hashcode()必定相等 两个对象hashcode()不等,equals()必定也不等 两个对象hashcode()相等,对象不一定相等,需要通过equals()进一步判断。 参考和感谢 哈希存储结构中添加元素的逻辑:https://blog.csdn.net/lijiecao0226/article/details/24609559 hashcode详解:https://www.cnblogs.com/whgk/p/6071617.html

2019/6/17
articleCard.readMore

Java中==和equals方法的分析

前言 == 和 equals是经久不衰的面试题,记得刚毕业的时候我也被问到过很多次,从最开始的一脸懵逼到最后的从容回答,本文我们就来分析下这两者之间的区别和联系。 为避免阅读疲劳,我这里先放上结论: 联系: 两者都被用来进行比较操作 当equals()未被重写时,两者的用途和比较的内容相同,即都是比较对象的引用地址是否相同 区别: ==既可以比较基本数据类型,亦可用在对象之间。equals()只能比较对象间的关系  基本数据类型对象类型 ==比较值是否相同比较引用地址是否相同 equals-equals()未被重写时比较对象的引用地址是否相同 equals()被重写后根据equals()实现逻辑而定 下面我们对以上的结论进行验证. 两者的联系 ==:关系操作符,计算两个操作数之间的关系,返回一个boolean类型的结果 equals:Object类的一个方法,用来比较两个对象之间的关系,返回一个boolean类型的结果 从Object类中的equals()实现来看他们两个都是用来进行==的逻辑比较,并且都返回一个boolean值 但是仔细分析, ==的操作数是有类型区分的(基本数据类型,对象类型),所以不同的操作数会有不同的计算逻辑。 而equals()是Object一个方法,既然是基类方法那么就可以被子类重写,所以实际的比较逻辑还是要根据重写内容来判断 栗子:Date类的equals()被重写,实际判断的是时间戳的值是否相等 ==的使用 == 是一个关系操作符,他有两个操作数,操作数则分为两个大类:基本数据类型、引用数据类型。 直接上代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Demo { public static void main(String[] args) { int a = 10; int b = 10; System.out.println("a==b:"+(a==b)); String s1 = "A";//栈内存中对象引用变量s1指向常量池中的A String s2 = "A";//栈内存中对象引用s2指向常量池中的A String s3 = new String("A");//栈内存中对象引用s3指向堆内存中的A对象 String s4 = new String("A");//栈内存中对象引用s4指向堆内存中的另一个A对象 System.out.println("s1==s2:"+(s1==s2)); System.out.println("s1==s3:"+(s1==s3)); System.out.println("s3==s4:"+(s3==s4)); } } 运行结果如下: 1 2 3 4 a==b:true s1==s2:true s1==s3:false s3==s4:false 具体分析下输出的结果 a==b:操作数a、b是基本数据类型,使用==直接比较ab在栈内存中的值是否相等,故结果为true s1==s2:操作数s1、s2为对象类型,String s1 = "A"执行时,堆内存的常量池中会开辟空间存放A对象,栈内存中的引用变量s1会指向该对象的内存地址,s2创建时同样会指向常量池中的A,s1和s2指向的是同一个对象所以结果为true s1==s3:s2是通过new()来创建对象,堆内存中会开辟空间存放对象,显然s1和s3的内存地址是不同的,s1指向常量池中的"A",s2指向堆内存中的new String(“A”),所以结果为false s3==s4:s3、s4是通过new()的方式创建的两个不同的对象,他们的内存地址不同,结果必然为false 总结: ==作为关系操作符,当操作数为基本数据类型时,直接判断值是否相同, 当操作数为对象类型时,判断两对象的内存地址是否相同 equals() equals()方法时Object类的方法之一,这意味着所有Java类都继承了这一方法,并可以对他进行重写,比如String、Date、Integer… 在上文我们通过Object类中equals()方法的源码可知,在未被重写时,equals()内部其实是调用了==进行判断。 下面我们看下String类对equals()的实现: 可见,String类的equals方法中,先判断两个对象是否内存地址相同,如果内存地址不同,则判断值是否相同 修改之前的代码测试如下: 1 2 3 4 5 6 7 8 9 10 11 12 public class Demo { public static void main(String[] args) { String s1 = "A";//栈内存中对象引用变量s1指向常量池中的A String s2 = "A";//栈内存中对象引用s2指向常量池中的A String s3 = new String("A");//栈内存中对象引用s3指向堆内存中的A对象 String s4 = new String("A");//栈内存中对象引用s4指向堆内存中的另一个A对象 System.out.println("s1.equals(s2):"+(s1.equals(s2))); System.out.println("s1.equals(s3):"+(s1.equals(s3))); System.out.println("s3.equals(s4):"+(s3.equals(s4))); } } 运行结果如下: 1 2 3 s1.equals(s2):true s1.equals(s3):true s3.equals(s4):true 具体分析下输出的结果 s1.equals(s2):相同的内存地址直接返回true s1.equals(s3):内存地址不同,开始判断值是否相同,值都为"A",返回true s3.equals(s4):内存地址不同,开始判断值是否相同,值都为"A",返回true 通过上面的栗子,发现了一种现象:内存地址相同的对象其值必定相同,而内存地址不同的对象,其值关系不确定 总结: equals()方法在Object类中作用于==相同,但是大部分的类都对equals()进行了重写,所以要找到equals真正的判断逻辑就得看他的方法实现。同样的我们自己创建的实体类或者其他对象都可以自定义equals()方法。

2019/6/17
articleCard.readMore

Java中的自动拆装箱、装箱缓存

前言 Java 是一种面向对象的编程语言,Java 中的类把方法与数据类型连接在一起,构成了自包含式的处理单元。但在 Java 中不能定义基本类型对象,为了能将基本类型视为对象处理,并能连接相关方法,Java 为每个基本数据类型都提供了包装类,如 int 型数值的包装类 Integer,boolean 型数值的包装类 Boolean 等。这样便可以把这些基本类型转换为对象来处理了。 在Java中包含了8种基本数据类型,与之相对应的还有8种包装类,他们之间的对应关系如下: 基本数据类型包装类 byteByte shortShort intInteger longLong floatFloat doubleDouble booleanBoolean charCharacter 什么是自动拆装箱 Java中不能定义基本数据类型的对象,因此我们可以使用包装类,每种基本数据类型都有自己对应的包装类。 基本数据类型与包装类之间的转换过程就涉及到了自动拆装箱。 基本数据类型转换为包装类的过程称作自动装箱 包装类转换为基本数据类型的过程称作自动拆箱 自动拆装箱的实现原理 举一个栗子: 1 2 3 4 5 6 7 8 9 public class AutoBoxing { public static void main(String[] args) { int i = 10; //装箱 Integer ii = i; //拆箱 int iii = ii; } } 上面的代码实际上就是Java中的语法糖,通过对.class文件进行反编译之后就可以看到代码的真面目: 1 2 3 4 5 6 7 public class AutoBoxing { public static void main(String[] arrstring) { int n = 10; Integer n2 = Integer.valueOf(n); int n3 = n2.intValue(); } } 从反编译后的代码可以看到,int类型到Integer的装箱过程是通过Integer.valueOf()实现,Integer到int的拆箱过程是通过intValue()实现。 刚好我们测试下其他七种数据类型的拆装箱过程是怎么样的,代码如下AutoBox.java: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class AutoBox { public static void main(String[] args) { Integer aa = 10; int aaa = aa; Byte bb = 20; byte bbb = bb; Short cc = 30; short ccc = cc; Long d = 40L; long dd = d; Float e = 50f; float ee = e; Double f = 60d; double ff = f; Character g = 'a'; char gg = g; Boolean h = true; boolean hh = h; } } 直接对AutoBox.java文件进行编译后,对AutoBox.class文件反编译分析,命令如下 //编译javac AutoBox.java//反编译分析javap -c AutoBox.class 结果如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 Compiled from "AutoBox.java" public class com.zhengql.practice.autoBox.AutoBox { public com.zhengql.practice.autoBox.AutoBox(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: aload_1 7: invokevirtual #3 // Method java/lang/Integer.intValue:()I 10: istore_2 11: bipush 20 13: invokestatic #4 // Method java/lang/Byte.valueOf:(B)Ljava/lang/Byte; 16: astore_3 17: aload_3 18: invokevirtual #5 // Method java/lang/Byte.byteValue:()B 21: istore 4 23: bipush 30 25: invokestatic #6 // Method java/lang/Short.valueOf:(S)Ljava/lang/Short; 28: astore 5 30: aload 5 32: invokevirtual #7 // Method java/lang/Short.shortValue:()S 35: istore 6 37: ldc2_w #8 // long 40l 40: invokestatic #10 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 43: astore 7 45: aload 7 47: invokevirtual #11 // Method java/lang/Long.longValue:()J 50: lstore 8 52: ldc #12 // float 50.0f 54: invokestatic #13 // Method java/lang/Float.valueOf:(F)Ljava/lang/Float; 57: astore 10 59: aload 10 61: invokevirtual #14 // Method java/lang/Float.floatValue:()F 64: fstore 11 66: ldc2_w #15 // double 60.0d 69: invokestatic #17 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 72: astore 12 74: aload 12 76: invokevirtual #18 // Method java/lang/Double.doubleValue:()D 79: dstore 13 81: bipush 97 83: invokestatic #19 // Method java/lang/Character.valueOf:(C)Ljava/lang/Character; 86: astore 15 88: aload 15 90: invokevirtual #20 // Method java/lang/Character.charValue:()C 93: istore 16 95: iconst_1 96: invokestatic #21 // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean; 99: astore 17 101: aload 17 103: invokevirtual #22 // Method java/lang/Boolean.booleanValue:()Z 106: istore 18 108: return } 经过测试,其他7种基本数据类型到包装类的装箱拆箱原理都与int/Integer相同,自动装箱都是通过包装类的valueOf()方法来实现的,自动拆箱都是通过包装类对象的xxxValue()来实现的 什么时候用到自动拆装箱 赋值操作时 1 Integer a = 1;//Integer a = Integer.valueOf(1);//自动装箱 包装类之间运算时(±*/) 1 2 3 Integer a = 1; Integer b = 2; int c = a + b;//int c = a.intValue() + b.intValue();//自动拆箱 比较运算时 1 2 Integer a=1; boolean b = a==1;//boolean b = a.intValue()==1;自动拆箱 向集合中添加基本数据类型时 1 2 3 4 List<Integer> list = new ArrayList<>(); for (int i = 1; i < 10; i ++){ list.add(i);//list.add(Integer.valueOf(i));自动装箱 } 方法调用、参数返回时 1 2 3 4 5 6 7 8 9 10 11 public class AutoBox { private static int test(int i){ return i + 1; } public static void main(String[] args) { Integer i = 1; int a = test(i);//int a = test(i.intvalue());自动拆箱 Integer b = test(1);//Integer b = Integer.valueOf(test(1));//自动装箱 } } 装箱缓存 其实,在自动装箱过程中还存在一种缓存的操作,且看下面一道题: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class AutoBoxTest { public static void main(String[] args) { Integer a = 30; Integer b = 30; if (a==b){ System.out.println("a、b:内存地址相同"); }else { System.out.println("a、b:不同的两个对象"); } Integer c = 300; Integer d = 300; if (c==d){ System.out.println("c、d:内存地址相同"); }else { System.out.println("c、d:不同的两个对象"); } } } 这道题乍一看是不是觉得匪夷所思,怎么会有这种沙雕题目,两个对象类型用等号判断大小,很明显都是new出来的对象,肯定指向不同的内存地址啊,肯定不相等了。然鹅运行的结果如下: 1 2 a、b:内存地址相同 c、d:不同的两个对象 可以看到为什么同样的操作,c和d就符合判断逻辑,而a和b就偏偏指向同一个对象呢? 这是因为在自动装箱过程中,Integer对象通过使用相同的对象引用实现对象的缓存和重用。 那么问题又来了,既然有缓存操作,那为什么a、b有,c、d却没有呢? 来看一下Integer自动装箱的源码: 1 2 3 4 5 public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } 首先判断入参i是否处于[IntegerCache.low,IntegerCache.high]区间内,如果i值在区间内,则从缓存IntegerCache.cache中读取某一个值返回,反之直接new一个Integer对象,这说明触发缓存操作是根据i值的范围决定的。 那这个范围又是多少呢?阅读该方法的注释: 1 2 This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range. 此方法默认缓存[-128,127]范围内的值,但也可以缓存范围外的其他值,这里是因为区间右侧的IntegerCache.high是可配置的。 看到这里,终于明白,最开始的那道题目,为什么ab和cd的结果会完全不一样,是因为a、b的值在[-128,127]区间内,而c、d的值不在此范围内。 那么,既然Integer有缓存这个骚操作,那其他的包装类是不是也有呢?直接去看每个包装类的valueOf方法就可以知道了。 这里我就不贴源码了,查看后的结论是,其他的7种包装类中,所有的整数类型的类,在自动装箱时都有类似于Integer的这种缓存操作,只不过他们各自的触发情况不同,结果整理如下: 包装类缓存机制触发条件备注 ByteByteCache[-128,127] ShortShortCache[-128,127] IntegerIntegerCache[-128,127]最大值可配置 LongLongCache[-128,127] Float-- Double-- Boolean-- CharacterCharacterCache[0,127] 总结 自动装箱和拆箱方便了我们开发人员,但是在使用自动拆装箱时也有很多翻车现场,最容易出现的就是空指针,所以在使用自动拆装箱时一定要防止空指针。 自动装箱过程中涉及到对象的创建等操作,如果在循环体中大量的拆装箱操作,势必会浪费资源,所以何时使用合理的使用自动拆装箱是尤为重要。 参考和感谢 Java中整型的缓存机制:https://www.hollischuang.com/archives/1174

2019/6/17
articleCard.readMore

About-blog

LarsCheng’s Blog 的前世今生 === 2018.07.24 有一天lars觉得应该持续积累知识,记录学习历程,so…,基于hexo搭建了第一版博客 === 2019.03.09 强迫症的驱使下更换了博客主题为Next,并且完善了博客各项功能 === 2019.07.12 更换博客主题为Melody 你现在看到的幻灯片也是Melody的功能之一 这里十分感谢Melody的作者Molunerfinn 主题地址:传送门 === 2019.07.24 更换博客主题为Butterfly, 她是在Melody基础上进行再开发的一款非常棒的主题 再次感谢作者Jerryc,主题地址:传送门 === now… 博客持续更新,如果喜欢记得收藏哦~~~ === 博客主旨 分享技术,记录生活 === thx…

2019/6/12
articleCard.readMore

Java中的编译、反编译和反编译工具全家桶分享

前言 本文介绍到的反编译工具下载地址:反编译工具全家桶 , 提取码:oxor 编程语言 编程语言分为低级语言和高级语言 1 2 3 4 5 6 graph LR 编程语言-->低级语言 编程语言-->高级语言 低级语言-->机器语言 低级语言-->汇编语言 高级语言-->C/C++/Java/Python.... 最早的程序员都是用机器语言在写代码,那时候应该不叫代码,叫写十六进制串,这样的编程十分复杂不方便而且出错率高 后来有一个偷懒的程序员把机器语言中一组一组通用的十六进制用助记符来代替,这种通过助记符的方式被称作汇编语言,用助记符和部分机器语编写程序,最终执行前让汇编器将助记符转换成机器语言。 然鹅,无论是机器语言还是汇编语言,后来的程序员觉得还是很麻烦,于是又有几个偷懒的程序员他们先后创造了c、c++、java、python… 编译 程序员编程是通过高级语言,而计算机执行程序只认识机器语言,那么将高级语言翻译成机器语言的过程就叫做编译。负责执行这一过程的工具叫做编译器 举个例子: Java语言属于高级语言,计算机不认识 .class文件属于编译后的Java虚拟机认识的字节码文件 Java文件通过Java语言中的编译器javac编译后生成.class字节码文件 说明:字节码并不是机器语言,要想让机器能够执行,还需要把字节码翻译成机器指令。这个过程是Java虚拟机做的,这个过程也叫编译。是更深层次的编译。 反编译 反编译当然就是编译的逆向操作了,将机器认识的机器语言转换成程序员认识的高级语言。 举个例子: Java种将.class文件转换成Java文件,这一过程就是反编译。 Java中常用的反编译工具 javap javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。 用法:javap <options> <classes> javap命令算是java自己原装的反编译工具,但是他反编译后的代码阅读性不太好,我们也可以用其他方式进行反编译。 JD-GUI JD-GUI是一个独立的图形实用程序,支持Windows、Linux和苹果Mac Os三个平台,可对“.class”文件进行反编译。可以使用JD-GUI浏览重建的源代码,以便即时访问方法和字段。 JD-GUI在1.4.0版本后停止更新了很久,当时的版本对于Java1.7以后的代码兼容性很差,不过最近JD-GUI的恢复更新,并兼容Java9 JD-GUI现在最新的版本是1.5.1,下载jd-gui-1.5.1.jar直接双击执行即可。 github地址 CFR 在JD-GUI断更期间,CFR就被大家广泛使用了。 CFR will decompile modern Java features - up to and including much of Java 9, 10, 12 and beyond, but is written entirely in Java 6, so will work anywhere! 作者表示,CFR可以反编译目前Java 9,10,12及更高版本的大部分功能,但完全用Java 6编写,因此可以在任何地方使用! CFR的使用也是十分方便,直接下载jar包,通过java -jar执行即可,如下: java -jar cfr-0.144.jar Demo.class 最新jar包下载和其他参数使用可参考CFR官网 Procyon Procyon就很有趣了,它的作者和CFR作者就职同一公司,他们俩在进行一场友谊赛,看看谁能提供更好的反编译器。 Procyon-Decompiler支持JDK1.8类的反编译,并且支持:字符串的Switch、枚举声明方面、注解方面、匿名类、内部类、Java8新接口规范、Java8 Lambda表达式、Java8 方法传递等。 Procyon和CFR的运行方式相同,如下: java -jar procyon-decompiler-0.5.30.jar Demo.class 最新jar包下载和其他参数使用可参考Procyon地址 Procyon拥有一款第三方的GUI:luyten 如果你习惯像JD-GUI那样操作方便的GUI的话,你可以选择使用Luyten,它是基于Procyon的一款反编译工具,推荐使用哦~ Luyten官方网址 Fernflower Fernflower相信经常使用IDEA的同学应该很眼熟叭,他是IDEA自带的反编译工具,我们通过IDEA看到的.class文件内容都是通过Fernflower反编译后的。IDEA中效果如下: Fernflower.jar下载地址,也可以拉取官方的项目自己在本地进行gradle构建 下好fernflower.jar后也准备好需要编译的jar包。 使用命令行 java -jar fernflower.jar <目标>.jar <文件夹名>/ java -jar fernflower.jar Demo.jar demo/ 编译成功后demo文件夹下是一个<文件名>.jar。 可以看到比刚才的目标jar包小一些,解压此jar包就可以查看源码了! Fernflower github地址 bytecode-viewer(逆向必备!!!) bytecode-viewer(字节码查看器)一款轻量级的Java字节码查看器,它可以反编译jar包,.class文件、Android APK,并且支持多种反编译器 你可以用CFR、Procyon、Fernflower等同时编译同一个.class文件,并进行结果查看和对比。如下: 是不是很方便呐,github地址 快去下载试试吧。 在线反编译 Java decompiler online JAVA反向工程网 ps: 部分官网和仓库需要翻墙才可以下载,我这边已经为大家准备好了反编译工具全家桶 , 提取码:oxor 总结 了解编译和反编译的基本概念后,其实反编译可以使我们更好的了解Java代码真实的面目,Java中有很多的语法糖,通过反编译可以很好的了解和学习这些语法糖的实现原理。

2019/5/30
articleCard.readMore

finalize()的生命周期(执行过程)

说明 本文转载自 Smina俊 的博客:《java finalize方法总结、GC执行finalize的过程》 博文中关于对象复活的示例和生命周期的过程极为精辟,分享给大家。 本文的目的并不是鼓励使用finalize方法,而是大致理清其作用、问题以及GC执行finalize的过程。 finalize的作用 finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。 finalize()与C++ 中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性不建议用finalize方法完成“非内存资源”的清理工作。 但建议用于: ① 清理本地对象(通过JNI创建的对象); ② 作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法。 其原因可见下文[finalize的问题] finalize的问题 一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法 System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不可盲目依赖它们 Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行 finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行 对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的 finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为) finalize的执行过程(生命周期) (1) 首先,大致描述一下finalize流程: 当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。 (2) 具体的finalize流程: 对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义如下: unfinalized: 新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的 finalizable: 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行 finalized: 表示GC已经对该对象执行过finalize方法 reachable: 表示GC Roots引用可达 finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达 unreachable:对象不可通过上面两种途径可达 变迁说明: 新建对象首先处于[reachable, unfinalized]状态(A) 随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态 若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)。 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N) 处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable。这也是图中只有八个状态点的原因 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为 若JVM检测到finalized状态的对象变成unreachable,回收其内存(I) 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O) 注:System.runFinalizersOnExit()等方法可以使对象即使处于reachable状态,JVM仍对其执行finalize方法 对象复活代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class Demo extends Object{ public static Demo SAVE_HOOK = null; public static void main(String[] args) throws InterruptedException { // 新建对象,因为SAVE_HOOK指向这个对象,对象此时的状态是(reachable,unfinalized) SAVE_HOOK = new Demo(); //将SAVE_HOOK设置成null,此时刚才创建的对象就不可达了,因为没有句柄再指向它了,对象此时状态是(unreachable,unfinalized) SAVE_HOOK = null; //强制系统执行垃圾回收,系统发现刚才创建的对象处于unreachable状态,并检测到这个对象的类覆盖了finalize方法,因此把这个对象放入F-Queue队列, //由低优先级线程执行它的finalize方法,此时对象的状态变成(unreachable, finalizable)或者是(finalizer-reachable,finalizable) System.gc(); // sleep,目的是给低优先级线程从F-Queue队列取出对象并执行其finalize方法提供机会。在执行完对象的finalize方法中的super.finalize()时, // 对象的状态变成(unreachable,finalized)状态,但接下来在finalize方法中又执行了SAVE_HOOK = this;这句话,又有句柄指向这个对象了,对象又可达了。 // 因此对象的状态又变成了(reachable, finalized)状态。 Thread.sleep(500); // 这里对象处于(reachable,finalized)状态。对象的finalized方法被执行了,因此是finalized状态。又因为在finalize方法是执行了SAVE_HOOK=this这句话, // 本来是unreachable的对象,又变成reachable了。 if (null != SAVE_HOOK) { //此时对象应该处于(reachable, finalized)状态 // 这句话会输出,注意对象由unreachable,经过finalize复活了。 System.out.println("Yes , I am still alive"); } else { System.out.println("No , I am dead"); } // 再一次将SAVE_HOOK放空,此时刚才复活的对象,状态变成(unreachable,finalized) SAVE_HOOK = null; // 再一次强制系统回收垃圾,此时系统发现对象不可达,虽然覆盖了finalize方法,但已经执行过了,因此直接回收。 System.gc(); // 为系统回收垃圾提供机会 Thread.sleep(500); if (null != SAVE_HOOK) { // 这句话不会输出,因为对象已经彻底消失了。 System.out.println("Yes , I am still alive"); } else { System.out.println("No , I am dead"); } } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("execute method finalize()"); // 这句话让对象的状态由unreachable变成reachable,就是对象复活 SAVE_HOOK = this; } }

2019/5/24
articleCard.readMore

Java关键字之final、finally与finalize方法

Java中有很多关键字,这些关键字中的final、finally和finalize()方法长相十分相似,其实他们仨并没什么特殊的联系,只是单纯的像,本文就简单介绍下他们仨各自的用途。 final final关键字可用于非抽象类、非抽象类的成员方法(构造方法除外)、非抽象类中的变量、参数 用于类:表示该类不可被继承,类中的方法默认都是被final修饰的方法(例如String类) 用于方法:表示该方法不可被子类重写(例如Object.getClass()方法) 用于变量:表示常量,只能被赋值一次不可改变 用于参数:该参数在方法中只可以被读取不可被修改 注:final修饰变量时,被修饰的变量是常量,该变量名全部大写;可以先声明不进行赋值值,这种叫做final空白。但是使用前必须被初始化。一旦被赋值,将不能再修改 修饰基本类型变量和引用类型变量 修饰基本类型变量时:不能对基本类型重新赋值。 修饰引用型变量时:它仅仅保存的是一个引用,final保证的是这个引用类型的变量所引用的地址不会变。即一直引用同一个对象,但是被引用对象的值可以改变。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 /** * 描述: final修饰变量示例 * * @author zhengql */ public class Demo { public static void main(String[] args) { final int num = 10; //编译报错,无法为最终变量num分配值 //num+=1; System.out.println(num); final Person person = new Person("张三",20); System.out.println(person.toString()); person.setName("李四"); System.out.println(person.toString()); } static class Person{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public Person setName(String name) { this.name = name; return this; } public int getAge() { return age; } public Person setAge(int age) { this.age = age; return this; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } } 运行示意图如下: finally try-catch想必大家都用过,finally必定不会陌生,finally只有在出现try-catch的地方才会用到,而且不一定会用到。我们一般用到它的时候应该是这样: 1 2 3 4 5 6 7 8 try { //...... } catch (Exception e) { e.printStackTrace(); } finally { //....... } 理解finally记住下面这就话就够了: try-catch中无论是否发生异常,finally中的逻辑都会执行。finally可有可无。但是必须与try-catch成对出现。 finalize 首先需要说明的是:finalize()方法本身存在一定的缺陷性,实际使用中也不推荐finalize方法,在Java9中finalize已经被废弃 finalize()方法是在Object类中定义的,Java中所有类都从Object类中继承finalize()方法。垃圾回收器准备释放对象占用的内存时,首先调用对象的finalize()方法 finalize()与C++ 中的析构函数是不一样的。C++中的析构函数调用的时机是确定的(对象离开作用域或调用delete),但Java由于gc的执行时间不确定导致finalize的调用具有不确定性 Java有垃圾回收器(GC)负责回收无用对象占据的内存空间。但也有特殊情况:假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收期只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。所以Java的设计者准备了finalize()方法来解决这个问题,但是finalize也带来了一些隐患 finalize存在的问题 不可靠:只有当垃圾回收器(GC)释放该对象时才会调用finalize方法,然而GC并不是想执行就执行的(根据程序当前是否内存不足),而且即使调用了finalize方法也不一定回收成功 阻碍GC的快速回收:在进行垃圾回收时会启动一个finalizethread,当遇到有重写了finalize方法的对象时,会将对象放入finalizethread的中,并形成一个队列,暂时挂起,且运行时间并不确定,这就导致了对象回收的缓慢,如果队列中存在重写的finalize方法有死锁问题则会导致后面的方法都无法执行 会发生对象复活现象:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的 关于finalize的生命周期和代码示例,此处推荐一篇很详细的文章:finalize的执行过程(生命周期) 参考 Java中finalize()详解和Java9中的垃圾回收:https://blog.csdn.net/u011695358/article/details/78860410 finalize()的使用场景:https://segmentfault.com/q/1010000000094660

2019/5/24
articleCard.readMore

Java中重写、重载

重写 子类继承父类,拥有父类的方法和属性后,子类在保证继承的方法在方法名和参数列不变的情况下,对方法的内部实现进行重新定义,这种现象就是方法重写 重写比较侧重于父子差异 示例 父亲(Father类)名下有一套三层楼房,后来由儿子(Son类)继承,继承之后碰巧拆迁,三层楼房在儿子这里变成了拆迁款,这套房子(house方法)的价值转换就属于重写 1 2 3 4 5 6 7 8 9 10 11 class Father{ public void house(){ System.out.printhl("三层楼房"); } } class Son extends Father{ public void house(){ System.out.printhl("拆迁款"); } } 重载 在同一个类中,有多个同名、但不同参数列表(相同参数不同顺序)的方法存在现象就是方法重载 最常见的重载应该就是构造方法的重载 示例 我掌握了烧菜的技能,当家里没菜的时候我只能做泡面(cook()方法),有一天买了猪肉,于是我做了一份红烧肉(cook(int a)方法),第二天新买了土豆和牛肉,于是麻溜的做了一份土豆炖牛肉(cook(int a,String s)),后台有一天家里来客人,他们带来了鸡肉和蘑菇于是我就做了小鸡炖蘑菇(cook(String s,int a)) 可以看出同样是烧菜,但根据不同的配菜可以用同样的手法做出不同的菜品。这种因菜而变的烧菜方法也是重载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class Cooking { //1:原方法 public int cook(){ System.out.println("泡面"); return 1; } //2:同名、不同参数、无返回类型(与1对比) public void cook(int a){ System.out.println("红烧肉"); } //3:同名、不同参数、不同返回类型(与1对比) public String cook(int a,String s){ return "土豆炖牛肉"; } //4:同名、相同返回结果、不同参数顺序(与3对比) public String cook(String s,int a){ return "小鸡炖蘑菇"; } public static void main(String[] args){ Cooking o = new Cooking(); //没菜 o.cook(); //买了肉 o.cook(0); //买了土豆和牛肉 System.out.println(o.cook(1,"a")); //买了鸡肉和蘑菇 System.out.println(o.cook("b",2)); } } 重写与重载之间的区别 要点重写重载 方法名不可修改不可修改 参数列表不可修改必须修改 参数顺序不可修改可以修改 返回类型不可修改可以修改 异常只可降级或删除可以修改 访问权限只可降级可以修改

2019/5/23
articleCard.readMore

Java中面向对象的三大特征:继承、封装、多态

面向对象的三大特征 继承、封装、多态 接口(Interface)是用来定义行为的! 抽象类(Abstract Class)是用来实现行为的! 具体类(Class)是用来执行行为的! 使用了对象就叫基于对象,基于对象的基础上增加了继承从而变成了面向对象。 封装 是对象和类概念的主要特征 封装:即把客观事物封装成抽象的类,并且类可以对自己的数据和方法进行权限控制, 封装就是指利用抽象数据类型将数据和基于数据的操作封装在一起,数据被保护在抽象类型的内部,系统的其他部分只有通过包裹在数据外面的被授权的操作,才能够与这个抽象数据类型交流与交互! 封装的优点 将变化隔离 便于使用 提高安全性 提高重用性 总结 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。 继承 继承: 继承是使用已存在的类的定义作为基础建立新的类,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。 继承的特点: 子类拥有父类非private的属性和方法。 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 子类可以用自己的方式实现父类的方法。(重写)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 Father父类 class Father { public String name; //父类的私有属性 private int money; public Father() { this.name = "李明"; this.money = 1000; } public String getName() { return name; } public Father setName(String name) { this.name = name; return this; } public int getMoney() { return money; } public Father setMoney(int money) { this.money = money; return this; } public void say(){ System.out.println("我是父亲"); } public void speak(){ System.out.println("父亲是我"); } } 子类Son继承父类Father class Son extends Father{ //子类自己的属性 public String sonName; public Son() { this.sonName = "李小明"; } //子类重写父类的方法 @Override public void say(){ //子列拥有父类的非private属性 System.out.println("我的父亲是:"+name+"他的存款我不知道..."); //子类拥有父类非private的方法访问权限 System.out.println("我的父亲是:"+getName()+"但他的存折显示存款为:"+getMoney()); } //子类重载父类的方法 public void speak(int age){ System.out.println("我的名字是:"+sonName+",年龄:"+age); } } public class Test{ public static void main(String[] args){ Son s = new Son(); s.say(); s.speak(20); } } 执行结果: 1 2 3 我的父亲是:李明,他的存款我不知道... 我的父亲是:李明,但他的存折显示存款为:1000 我的名字是:李小明,年龄:20 栗子分析: 父类Father有自己的公有属性name,私有属性money,同时对外提供了所有属性的公有方法(get/set) 子类Son继承父类Father后,拥有父类所有非private的属性和方法的访问权限,子类Son同时还可以有自己的属性(sonName),儿子知道父亲的名字却不知道父亲具体的存款,但是可以通过从父类继承的getMoney()获取存折中的存款信息。 子类可以重写父类的方法,Son类对父类的say方法在保持方法名、参数列表、返回值不变的情况下,对具体的实现进行重写。 父类引用指向子类对象 保持上边代码中Father、Son的内容不变,修改Test类的内容如下: 1 2 3 4 5 6 7 8 public class Test{ public static void main(String[] args){ //父类引用指向子类对象 Father s = new Son(); s.say(); s.speak(20);//此处会报错 } } 首先来分析下代码 Father s = new Son();: 是向上转型即父类引用指向子类对象,它是对Father的对象的方法的扩充,即Father的对象可访问Son从Father中继承来的和Son复写Father的方法,其它的方法都不能访问,包括Father中的私有成员方法。 s.speak(20);:此处报错,正是因为speak(int age)方法属于子类自己的方法而不是从父类继承而来,在向上转型过程中,父类对象s会遗弃子类中的该方法 总结 继承实际上是存在于面向对象程序中的两个类之间的关系。当一个类拥有另一个类的所有数据和操作时,就称这两个类之间具有继承关系! 多态 多态即同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果 多态分为编译时多态和运行时多态 编译时多态也称为静态多态,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编译之后会变成两个不同的函数,在运行时谈不上多态。 运行时多态称作动态多态,它是通过动态绑定来实现的,也就是我们所说的多态性,只有在运行期才知道是调用的那个类的方法。 多态实现形式 基于继承的多态,基于接口的多态,下面分别给出栗子: 基于继承的多态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Father { public void say(){ System.out.println("我是父亲"); } } class Son extends Father{ //子类重写父类的方法 @Override public void say(){ System.out.println("我是儿子"); } } class Daughter extends Father{ //子类重写父类的方法 @Override public void say(){ System.out.println("我是女儿"); } } public class Test{ public static void main(String[] args){ Father f = new Father(); Son s = new Son(); Daughter d = new Daughter(); f.say(); s.say(); d.say(); } } 执行结果: 1 2 3 我是父亲 我是儿子 我是女儿 通过基于继承来实现多态其实是子类对父类的方法进行了重写,以至于在程序执行时不同的子类对同一个方法会有不同的执行结果 2.基于接口的多态: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public interface Animal { public void say(); } public class Dog implements Animal { @Override public void say() { System.out.println("狗:汪汪汪"); } } public class Cat implements Animal { @Override public void say() { System.out.println("猫:喵喵喵"); } } public class Test{ public static void main(String[] args){ Dog dog = new Dog(); Cat cat = new Cat(); dog.say(); cat.say(); } } 执行结果: 1 2 狗:汪汪汪 猫:喵喵喵 通过实现Animal接口,不同的对象会有不同的实现,在程序执行时同样也会有不一样的执行结果。 多态迷魂阵 继承和多态关系密接,多态是基于继承,继承和多态的理解每个人都各有不同,下面可以看下一个非常有趣的题目,被称为多态迷魂阵 出自博客: https://blog.csdn.net/thinkGhoster/article/details/2307001 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 优先级由高到低依次为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。 public class A { public String show(D obj) { return ("A and D"); } public String show(A obj) { return ("A and A"); } } public class B extends A{ public String show(B obj){ return ("B and B"); } public String show(A obj){ return ("B and A"); } } public class C extends B{ } public class D extends B{ } public class Test { public static void main(String[] args) { A a1 = new A(); A a2 = new B(); B b = new B(); C c = new C(); D d = new D(); System.out.println("1--" + a1.show(b)); System.out.println("2--" + a1.show(c)); System.out.println("3--" + a1.show(d)); System.out.println("4--" + a2.show(b)); System.out.println("5--" + a2.show(c)); System.out.println("6--" + a2.show(d)); System.out.println("7--" + b.show(b)); System.out.println("8--" + b.show(c)); System.out.println("9--" + b.show(d)); } } 执行结果: System.out.println("1--" + a1.show(b)); A and A System.out.println("2--" + a1.show(c));A and A System.out.println("3--" + a1.show(d));A and D System.out.println("4--" + a2.show(b));B and A System.out.println("5--" + a2.show(c));B and A System.out.println("6--" + a2.show(d));A and D System.out.println("7--" + b.show(b));B and B System.out.println("8--" + b.show(c));B and A System.out.println("9--" + b.show(d)); A and D 参考感谢 面向对象的本质是什么?封装,继承,多态是到底用来做什么的:https://bbs.csdn.net/topics/50019051 多态迷魂阵:https://blog.csdn.net/thinkGhoster/article/details/2307001

2019/5/23
articleCard.readMore

DockerFile介绍

前言 DockerFile文件通常用来构建自定义镜像,举个不恰当的栗子: 1 2 3 4 5 6 7 8 比如果现在有一个纯净版的windows10镜像,使用这个镜像我安装了一个纯净版的win10到我的电脑, 接着我安装了杀毒软件、IDEA、JDK、mysql、Redis、MongoDb、qq、微信等一些基础应用, 后来我的系统损坏了(无法修复),需要重装系统, 于是我又用纯净版的win10镜像重新装一次系统, 然后又重新安装一次杀毒软件、IDEA、JDK、mysql、Redis、MongoDb、qq、微信... 那我能不能自己创建一个自定义的系统镜像,就以win10纯净版为基础, 预装上IDEA、JDK、Mysql、Redis这些应用,下次重装系统时,安装完系统这些软件就装好了,答案是当然可以啊。 那么docker中同理,我想以jdk为基础镜像创建一个自定义镜像,把自定义创建过程写成一个脚本,通过这个脚本来一键生成自定义镜像,这个脚本就是DockerFile,调用这个脚本的Docker命令就是Docker build DockerFile语法 FROM <镜像名> 所有的 Dockerfile 都用该以 FROM 开头,FROM 命令指明 Dockerfile 所创建的镜像文件以什么镜像为基础,FROM 以后的所有指令都会在 FROM 的基础上进行创建镜像;可以在同一个 Dockerfile 中多次使用 FROM 命令用于创建多个镜像。 FROM 10.200.0.230:5000/openjdk:8-jreFROM ubuntu MAINTAINER <你的名字邮箱> 用于指定镜像创建者和联系方式。 MAINTAINER zhengql zhengql@test.comMAINTAINER lky6666 lky6666@test.com RUN <命令> 用于容器内部执行命令。每个 RUN 命令相当于在原有的镜像基础上添加了一个改动层,原有的镜像不会有变化。 RUN echo zql666 ADD <原始位置><目标位置> 用于从将原始位置的文件复制到目标位置文件:原始位置 是相对被构建的源目录的相对路径,可以是文件或目录的路径,也可以是一个远程的文件 url,目标位置 是容器中的绝对路径。 COPY target/myDemo-1.0.0.jar /usr/src/ CMD CMD 命令有三种格式: CMD [“executable”,“param1”,“param2”]:推荐使用的 exec 形式。 CMD [“param1”,“param2”]:无可执行程序形式 CMD command param1 param2:shell 形式。 CMD 命令用于启动容器时默认执行的命令,CMD 命令可以包含可执行文件,也可以不包含可执行文件:不包含可执行文件的情况下就要用 ENTRYPOINT 指定一个,然后 CMD 命令的参数就会作为ENTRYPOINT的参数。 一个 Dockerfile 中只能有一个CMD,如果有多个,则最后一个生效。 CMD 的 shell 形式默认调用 /bin/sh -c 执行命令。 CMD命令会被 Docker 命令行传入的参数覆盖: docker run busybox /bin/echo Hello Docker 会把 CMD 里的命令覆盖。 ENTRYPOINT ENTRYPOINT 命令的字面意思是进入点,而功能也恰如其意:他可以让你的容器表现得像一个可执行程序一样。 ENTRYPOINT 命令也有两种格式: ENTRYPOINT [“executable”, “param1”, “param2”] :推荐使用的 exec 形式 ENTRYPOINT command param1 param2 :shell 形式 一个 Dockerfile 中只能有一个 ENTRYPOINT,如果有多个,则最后一个生效。 EXPOSE 用来指定对外开放的端口。 EXPOSE 80 3306 WORKDIR WORKDIR /path/to/work/dir 配合 RUN,CMD,ENTRYPOINT 命令设置当前工作路径。 可以设置多次,如果是相对路径,则相对前一个 WORKDIR 命令。默认路径为/。 1 2 3 4 5 6 FROM ubuntu WORKDIR /etc WORKDIR .. WORKDIR usr WORKDIR lib ENTRYPOINT pwd 启动容器后得到/usr/lib USER <UID/用户名> 容器内指定 CMD RUN ENTRYPOINT 命令运行时的用户名或UID VLOUME 允许容器之间互相访问目录,VOLUME [’/data’] 允许其他容器访问当前容器的目录。 ENV 指定环境变量,会被RUN指令使用,并在容器运行时保存 ENV LC_ALL en_US.UTF-8 栗子分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #基于私有仓库中openjdk为基础镜像 FROM 10.200.0.230:5000/openjdk:8-jre #设置工作路径 WORKDIR /usr/src/ #将jar包复制到指定目录 COPY target/myDemo-1.0.0.jar /usr/src/ #设置容器启动时要执行的命令 CMD ["java", "-Duser.timezone=GMT+08", "-jar", "myDemo-1.0.0.jar", "--spring.profiles.active=dev"] #设置镜像所运行的时区 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime #设置环境变量 ENV LANG C.UTF-8 #暴露端口 EXPOSE 9041 参考 dockerFile官网手册

2019/4/30
articleCard.readMore

Docker环境下安装Gitlab

前言 在Docker中我们同样可以部署自己专属的代码仓库GitLab,下面简单介绍一下安装流程。 安装前提 内存大于 2G 的linux虚拟机或者云服务器皆可(我用的虚拟机) 已安装Docker服务,若未安装可参考博客: Centos7下两种方式安装Docker-CE 说明:我的虚拟机ip:10.200.0.230,安装过程中涉及到的ip配置可替换成你的ip地址 安装步骤 拉取GitLab镜像 GitLab的镜像有很多,官方镜像:gitlab/gitlab-ce,网友汉化版镜像:twang2218/gitlab-ce-zh,此处以官方镜像进行安装介绍,下面拉取镜像到本地。 docker pull gitlab/gitlab-ce 启动gitlab容器 docker run -d \-p 9443:443 \-p 9080:80 \-p 222:22 \--name gitlab \--restart always \--hostname 10.200.0.230 \-v /home/gitlab/config:/etc/gitlab \-v /home/gitlab/logs:/var/log/gitlab \-v /home/gitlab/data:/var/opt/gitlab \gitlab/gitlab-ce 说明: -d 后台启动,打印容器id -p 9080:80 将宿主机的端口映射至容器暴露的端口 –name gitlab 设置容器名称为gitlab –restart always 设置容器重启策略:在容器退出时总是重启容器 –hostname 10.200.0.230 设置容器主机名为10.200.0.230(此处修改为你相应的虚拟机ip) -v 挂载目录至宿主机,方便备份 出现如下图所示时表示启动成功 配置gitlab服务器的访问地址 按照上面的方式,让gitlab容器运行起来是没有问题的,但是当在gitlab上创建项目的时候,生成项目的URL访问地址是按容器的hostname来生成的,即容器的id。作为gitlab服务器,当然是需要一个固定的URL访问地址,于是需要配置gitlab.rb(宿主机上的路径为:/home/gitlab/config/gitlab.rb)配置文件里面的参数。执行如下命令: vi /home/gitlab/config/gitlab.rb 可以看到,文件中的配置默认都是注释掉的,为修改方便,直接添加三条配置即可 # 配置http协议所使用的访问地址external_url 'http://10.200.0.230'# 配置ssh协议所使用的访问地址和端口gitlab_rails['gitlab_ssh_host'] = '10.200.0.230'gitlab_rails['gitlab_shell_ssh_port'] = 222 保存文件后重启容器 docker restart gitlab gitlab启动较慢,可以查看容器的状态来判断是否启动成功 starting:表示正在启动中 healthy:表示已经启动成功 登录gitlab的web界面 浏览器访问 http://10.200.0.230:9080 ,可以看到第一次登录需要设置一个密码,账号默认是root,设置完后直接登录即可 登录成功后进入管理页面,我们通过Create a project创建一个项目先。 项目内容如下: 创建成功,查看克隆地址,仔细观察http的url还是有问题http://10.200.0.230/root/testdemo.git,此处仍使用的默认80端口,所以还是得接着修改配置。 修改gitlab.yml(位于宿主机/home/gitlab/data/gitlab-rails/etc/) vi /home/gitlab/data/gitlab-rails/etc/gitlab.yml 修改GitLab settings下的port为9080,如下: 保存修改后重启容器,等待容器启动成功,进入web页面查看,可以看到此时克隆地址已经ok了 Git拉取提交测试 通过Http拉取项目 本地电脑上通过git bash 来拉取刚才创建的项目 git clone http://10.200.0.230:9080/root/testdemo.git(此处可替换为你的url地址) 如下图所示,即为克隆成功,首次拉取需要验证账号密码(gitlab的账号密码,root+设置的密码) 通过SSH拉取项目 生成私钥和公钥(若已有可跳过) git bash中查看是否有已生成的私钥公钥 cd ~/.ssh 如图所示表示已有私钥和公钥文件 当然你可以重新生成,步骤如下: 设置Git的user name和email: git config --global user.name "zhengql"git config --global user.email "zhengql@test.com" 生成私钥和公钥(需要你设置一个密码,此处建议不进行设置,直接三次回车) ssh-keygen -t rsa -C "zhengql@test.com" 下图为生成步骤图解 成功之后。查看生成的文件夹内有两个文件,查看公钥文件内容 在gitlab中配置公钥 直接进入gitlab的web页面,复制我们刚刚生成的公钥内容添加至gitlab,操作如下 通过git bash 克隆项目至本地 git clone ssh://git@10.200.0.230:222/root/testdemo.git(此处可以替换为你的ssh地址) 成功图示如下: 总结 至此,Docker中搭建GitLab流程和两种方式克隆项目的测试就介绍完毕了。 其实在搭建过程中有很多配置上的小坑,例如在修改80端口的映射后external_url是否需要加上端口 测试发现如果在external_url设置为ip+端口,会导致无法访问gitlab的web页面,所以目前我的解决方法是手动修改启动后生成的gitlab.yml中的配置,然后让容器重新读取配置,这样的弊端就是当通过docker restart gitlab重启容器后,修改的gitlab.yml会被覆盖。 参考 利用GitLab Docker images安装GitLab(填坑)

2019/4/30
articleCard.readMore

Docker中私有仓库的搭建流程

前言 docker中的仓库就像是我们平时用的git一样,git用来存放管理项目代码,而docker仓库则是专门用来存放管理docker镜像,既然git有公有、私有仓库之分,docker仓库同样不例外,本文介绍如何在docker中安装你专属的私有仓库。 环境说明 Centos7-64位虚拟机环境 Docker-CE-18.03.1-ce 虚拟机ip地址:10.200.0.204 私有仓库搭建 拉取镜像 docker pull registry 启动仓库镜像 docker run -d \--name myRegistry \-p 5000:5000 \-v /myRegistry:/var/lib/registry \registry 上面的命令大意是指,通过registry镜像来启动一个容器,并命名为myRegistry,开放容器端口5000并映射到宿主机5000端口,将容器中的/var/lib/registry目录挂载至宿主机/myRegistry目录下 查看容器运行结果 docker ps -a 此时,我们已经在docker中搭建好了一个专属的私有仓库,仓库地址为10.200.0.204:5000 下面测试提交一个镜像到私有仓库,此处以hello-world镜像为例 如果docker中暂无该镜像,可通过docker pull hello-world拉取 修改hello-world镜像的tag,标记一个修改后的版本 docker tag <image_name/image_id> <registry_ip>:5000/<image_name>:<version> docker tag hello-world 10.200.0.204:5000/hello-world:v1.0.1 将tag为v1.0.1的hello-world镜像提交到私有仓库 docker push <registry_ip>:5000/<image_name>:<version> docker push 10.200.0.204:5000/hello-world:v1.0.1 此时回提交失败出现如下错误 http: server gave HTTP response to HTTPS client: 出现这问题的原因是:Docker自从1.3.X之后docker registry交互默认使用的是HTTPS,但是我们搭建的私有仓库在进行交互时默认使用的是HTTP服务, 由于仓库与客户端的https问题,需要修改/usr/lib/systemd/system/docker.service文件, vi /usr/lib/systemd/system/docker.serviceExecStart=/usr/bin/dockerd --insecure-registry 192.168.211.153:5000 重新启动docker systemctl daemon-reload systemctl restart docker 如果是虚拟机,需重启虚拟机。 重新提交hello-world镜像到私有仓库 docker push 10.200.0.204:5000/hello-world:v1.0.1 查看仓库中的镜像列表 curl -X GET http://10.200.0.204:5000/v2/_catalog 查看仓库中某镜像的详细信息 curl -X GET http://10.200.0.204:5000/v2/hello-world/tags/list 从私有仓库pull镜像 docker pull <registry_ip>:5000/<image_name>:<version> docker pull 10.200.0.204:5000/hello-world:v1.0.1 总结 至此,docker中私有仓库的安装,和基本使用就介绍完了,不妨在自己的linux机器中实践一下吧。

2019/4/25
articleCard.readMore

Centos7下两种方式安装Docker-CE

前言 Docker 提供了两个版本:社区版 (CE) 和企业版 (EE)。 Docker 社区版 (CE) 是开发人员和小型团队开始使用 Docker 并尝试使用基于容器的应用的理想之选。 本文介绍下Docker 社区版 (CE) 在CentOS 7系统中的里两种安装方式 安装前提 64 位版本的 CentOS 7 CentOS 系统的内核版本高于 3.10(uname -r命令可查看系统内核版本) 安装 Docker CE 设置 Docker 的镜像仓库并从中进行安装 下载 RPM 软件包并手动进行安装(例如,在不能访问互联网的隔离系统中安装 Docker) 从镜像仓库安装 确保 yum 包更新到最新: sudo yum update 安装一些必要的系统工具: yum-utils 提供了 yum-config-manager 实用程序,用于设置yum源 devicemapper 存储驱动需要 device-mapper-persistent-data 和 lvm2 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 设置镜像仓库: 阿里云仓库地址(推荐) sudo yum-config-manager \ --add-repo \ http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 官方仓库地址 sudo yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo 更新 yum 软件包索引。 sudo yum makecache fast 查看仓库中所有docker版本 sudo yum list docker-ce --showduplicates | sort -r 安装 Docker CE 最新版本:sudo yum install docker-ce sudo yum install docker-ce 安装 Docker-ce 指定版本:sudo yum install docker-ce-版本号 sudo yum -y install docker-ce-18.03.1.ce-1.el7.centos 启动 Docker sudo systemctl start docker 启动sudo systemctl restart docker 重启sudo systemctl enable docker 加入开机启动 Docker 版本信息查看 sudo docker version 测试运行 hello-world sudo docker run hello-world docekr拉取hello-world镜像并启动,后打印出Hello from Docker! 卸载 Docker CE sudo yum remove docker-ce 删除所有镜像、容器和存储卷 sudo rm -rf /var/lib/docker RPM软件包进行安装 wget安装 yum -y install wget 下载rpm安装文件 官方镜像(国内下载较慢) wget -P /tmp https://download.docker.com/linux/centos/7/x86_64/stable/Packages/docker-ce-18.03.1.ce-1.el7.centos.x86_64.rpm 阿里云镜像(推荐) wget -P /tmp https://mirrors.aliyun.com/docker-ce/linux/centos/7/x86_64/stable/Packages/docker-ce-18.03.1.ce-1.el7.centos.x86_64.rpm 进入rpm文件目录,安装docker cd tmp/yum install docker-ce-18.03.1.ce-1.el7.centos.x86_64.rpm 启动docker sudo systemctl start docker 启动sudo systemctl restart docker 重启sudo systemctl enable docker 加入开机启动 至此,docker-ce在Centos7下的安装介绍已经完成,目前docker不仅限于Linux系列,它还支持Windows、Mac以及AWS和Azure等平台的使用。更多的用法和介绍有兴趣的同学可以在官网进一步了解。 参考 Docker官网文档

2019/4/25
articleCard.readMore

Vert.x创建一个Http服务

本文是我在学习Vert.x过程中的一些笔记,作为记录。因为是初学,对Vert.x的理解还不够透彻,如有错误之处我们可以在评论中一起讨论呦。 环境准备 JDK8+ Maven IDE Vert.x 3.6.3 本文将会建立一个基本的HTTP服务器,并监听指定端口,任何发往该服务器以及端口的请求,服务器会返回一个字符串。 该项目使用Maven构建,当然也可以选择Gradle(参考vertx.io) pom.xml文件 pom.xml中加入相关的依赖和插件,如下所示: 注:此处需要注意Vert.x的版本,以及main.class属性的值是对应着Verticle类的路径,maven打包后启动时会以次类为启动类。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.zhengql</groupId> <artifactId>vertx</artifactId> <packaging>pom</packaging> <version>0.0.1-SNAPSHOT</version> <modules> </modules> <name>vertx</name> <description>Demo project for vertx</description> <properties> <vertx.version>3.6.3</vertx.version> <main.class>com.zhengql.vertx.Main</main.class> </properties> <dependencies> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>${vertx.version}</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-unit</artifactId> <version>${vertx.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.2</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Main-Class>${main.class}</Main-Class> </manifestEntries> </transformer> <!--多语言支持在打包时需加入以下转换器--> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/services/io.vertx.core.spi.VerticleFactory</resource> </transformer> </transformers> <artifactSet /> <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-prod.jar</outputFile> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> 创建MyFirstVerticle类和Main类 MyFirstVerticle.java,继承AbstractVerticle类,创建一个http服务端并监听指定端口,异步处理该端口的请求。代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.zhengql.vertx; import io.vertx.core.AbstractVerticle; import io.vertx.core.http.HttpServer; /** * 描述:Verticle类,创建一个http服务端,监听10802端口,当有请求进来时返回结果 * * @author zhengql * @date 2019/3/13 10:25 */ public class MyFirstVerticle extends AbstractVerticle { @Override public void start() throws Exception { vertx.createHttpServer().requestHandler(req->{ req.response().end("bababababalalallala"); }).listen(10802); } Main.java代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.zhengql.vertx; import io.vertx.core.Vertx; public class Main { public static void main(String[] args) { Vertx vertx = Vertx.vertx(); vertx.deployVerticle(MyFirstVerticle.class.getName(),r->{ if (r.succeeded()){ System.out.println("http server start succeeded....."); }else { System.out.println("http server start error....."); } }); } } 启动Vert.x应用 在IDE中启动,直接运行Main文件中的main方法,部署verticle,若启动成功,控制台中会打印http server start succeeded..... 在浏览器中请求 http://localhost:10802/ ,可以看到我们刚才创建的httpserver对发出的请求监听成功并作出了响应: 当然,我们也可以通过打包的方式来部署,使用Maven的mvn package命令打包,随后在src的同级目录下会出现target目录会生成jar包,java -jar vertx 0.0.1-SNAPSHOT-prod.jar执行,控制台中同样会打印http server start succeeded.....,浏览器中请求 http://localhost:10802/,可以看到我们编写的请求响应。 通过jar包执行部署,是因为我们在pom中指定了程序的主类,也就是启动入口为Main.java,当我们java -jar的时候就启动了Main.java类,等同于在IDE中启动。 Launcher启动 另一种启动方式,也是官方推荐的启动方式 在pom.xml中加入main.verticle属性,并将该属性值设置为maven-shade-plugin插件的manifestEntries的Main-Verticle对应的值,最后修改main.class为io.vertx.core.Launcher,修改后的pom.xml如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.zhengql</groupId> <artifactId>vertx</artifactId> <packaging>pom</packaging> <version>0.0.1-SNAPSHOT</version> <modules> </modules> <name>vertx</name> <description>Demo project for vertx</description> <properties> <vertx.version>3.6.3</vertx.version> <!--此处修改--> <main.class>io.vertx.core.Launcher</main.class> <!--此处修改--> <main.verticle>com.zhengql.vertx.MainVerticle</main.verticle> </properties> <dependencies> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>${vertx.version}</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-unit</artifactId> <version>${vertx.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.2</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Main-Class>${main.class}</Main-Class> <!--此处修改--> <Main-Verticle>${main.verticle}</Main-Verticle> </manifestEntries> </transformer> <!--多语言支持在打包时需加入以下转换器--> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/services/io.vertx.core.spi.VerticleFactory</resource> </transformer> </transformers> <artifactSet /> <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-prod.jar</outputFile> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> 至此,我们还缺少main-verticle属性中的MainVerticle.java,创建该类,代码如下: 1 2 3 4 5 6 7 8 9 10 package com.zhengql.vertx; import io.vertx.core.AbstractVerticle; public class MainVerticle extends AbstractVerticle { @Override public void start() { vertx.deployVerticle(MyFirstVerticle.class.getName()); } } 然后重新打包后执行,控制台中同样会打印http server start succeeded.....,浏览器中请求 http://localhost:10802/ ,可以看到我们编写的请求响应。 总结 如此,便创建一个http服务端,是不是很方便呢,首先最大的感觉就是Vert.x应用他的启动没有依赖任何容器,直接启动,完全是运行在jvm上,没有像springmvc那样,需要依靠tomcat或者其他容器,而且vertx是异步编程,HTTP服务端创建逻辑,就是一个观察者模式,创建http服务端,异步处理该端口中的请求。

2019/4/3
articleCard.readMore

Vert.x创建TCP服务端及客户端

本文是我在学习Vert.x过程中的一些笔记,作为记录。因为是初学,对Vert.x的理解还不够透彻,如有错误之处我们可以在评论中一起讨论呦。 环境准备 JDK8+ Maven IDE Vert.x 3.6.3 我们通常用到的最多的应该是Http服务,创建一个Http服务端似乎web开发中常有的事情。但是如果有一个需要Tcp服务的场景,这时候我们会想到Socket编程,基于Socket实现一个Tcp服务的过程是及其考验编程水平的,需要手动处理网络和线程问题。于是乎我们又想到了Netty,用Netty来实现Tcp服务似乎也不错啊,他简化了传统的Nio操作,但是如果没有接触过Netty则需要从头学习,学习成本较高。其实我们可以使用Vertx来创建Tcp服务,因为Vert.x本来就是基于Netty的,而且通过Vertx创建Tcp服务非常方便。 vertx中创建Tcp服务端 默认的创建方式如下: NetServer netServer = vertx.createNetServer(); 默认创建的Tcp服务端实际上是初始化了一个默认的NetServerOptions实例,Tcp服务端会随机选择一个本地未被占用的端口进行监听 当然我们也可以通过配置自定义属性来创建: NetServer netServer = vertx.createNetServer(new NetServerOptions().setPort(9981));//可以获取监听的端口netServer.actualPort(); 当创建Tcp服务端监听某一端口时我们注册一个处理器,当创建成功并开始监听时触发: 1 2 3 4 5 6 7 8 //监听指定主机和端口,并且在监听开始时触发通知 vertx.createNetServer().listen(9983, "localhost", res -> { if (res.succeeded()) { System.out.println("Tcp服务端启动成功"); } else { System.err.println("Tcp服务端启动失败"); } }); 当服务端创建成功后,有客户端请求进来,会触发相应的处理器,可以通过connectHandler方法绑定处理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //绑定处理器,当有请求进入时触发 NetServer netServer = vertx.createNetServer().connectHandler(netSocket -> { //得到NetSocket实例 netSocket.handler(buffer -> { //读取数据 System.out.println("读取到数据:" + buffer.toString() + " 长度为: " + buffer.length()); }); netSocket.write(Buffer.buffer("数据已接收......"), ar -> { if (ar.succeeded()) { System.out.println("写入数据成功!"); } else { System.err.println("写入数据失败!"); } }); netSocket.closeHandler(ar -> { System.out.println("客户端退出连接"); }); }).listen(9984, "localhost"); Tcp客户端创建 客户端的创建方式与服务端类似,也有默认的创建方法和自定义的属性配置 1 2 3 4 5 //默认客户端创建 NetClient netClient1 = vertx.createNetClient(); //自定义属性创建 NetClient netClient2 = vertx.createNetClient(new NetClientOptions().setConnectTimeout(10000)); 创建Tcp客户端需要使用connect方法连接到服务端后,才可以进行数据的收发 1 2 3 4 5 6 7 8 NetClient client = vertx.createNetClient(new NetClientOptions().setConnectTimeout(10000)); client.connect(9984, "localhost", res -> { if (res.succeeded()) { System.out.println("连接成功!"); } else { System.out.println("连接失败: " + res.cause().getMessage()); } }); 关闭Tcp连接 当请求结束时,可以调用close关闭服务端或者客户端 1 2 3 4 5 6 7 8 9 10 11 12 //1.直接关闭 netServer.close(); //2.关闭结果打印通知 netServer.close(res -> { if (res.succeeded()) { System.out.println("关闭成功!"); } else { System.err.println("关闭失败!"); } }); 项目创建 在IDEA中创建一个Maven工程,pom文件引入Vertx-core的依赖 1 2 3 4 5 6 <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>3.6.3</version> </dependency> 创建NetServerDemo.java文件,创建并部署Tcp服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.zhengql.www; import io.vertx.core.AbstractVerticle; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetServer; import io.vertx.core.net.NetServerOptions; /** * 描述: tcp * * @author zhengql * @date 2019/4/2 14:57 */ public class NetServerDemo extends AbstractVerticle { public static void main(String[] args) { Vertx.vertx().deployVerticle(new NetServerDemo()); } @Override public void start() throws Exception { //绑定处理器,当有请求进入时触发 NetServer netServer = vertx.createNetServer().connectHandler(netSocket -> { //得到NetSocket实例 netSocket.handler(buffer -> { //读取数据 System.out.println("读取到数据:" + buffer.toString() + " 长度为: " + buffer.length()); }); netSocket.write(Buffer.buffer("数据已接收......"), ar -> { if (ar.succeeded()) { System.out.println("写入数据成功!"); } else { System.err.println("写入数据失败!"); } }); netSocket.closeHandler(ar -> { System.out.println("客户端退出连接"); }); }).listen(9984, "localhost", res -> { if (res.succeeded()) { System.out.println("Tcp服务端启动成功"); } else { System.err.println("Tcp服务端启动失败"); } }); } } 创建NetClientDemo.java文件,创建并部署Tcp客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.zhengql.www; import io.vertx.core.AbstractVerticle; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.NetSocket; /** * 描述: * Tcp客户端 * * @author zhengql * @date 2019/4/2 15:39 */ public class NetClientDemo extends AbstractVerticle { public static void main(String[] args) { Vertx.vertx().deployVerticle(new NetClientDemo()); } @Override public void start() throws Exception { //创建连接到指定主机和端口的客户端,并绑定创建结果的处理器 NetClient netClient3 = vertx.createNetClient(new NetClientOptions().setConnectTimeout(10000)) .connect(9984, "localhost", res -> { if (res.succeeded()) { System.out.println("连接成功!"); NetSocket socket = res.result(); //向服务器写入数据 socket.write(Buffer.buffer("发送数据......"), ar -> { if (ar.succeeded()) { System.out.println("数据发送成功!"); } else { System.err.println("数据发送失败!"); } }); //读取服务端返回的数据 socket.handler(buffer -> { System.out.println("读取到数据:" + buffer.toString() + " 长度为: " + buffer.length()); }); socket.closeHandler(ar -> { System.out.println("客户端断开连接"); }); } else { System.out.println("连接失败!: " + res.cause().getMessage()); } }); } } 到此,服务端和客户端的代码已经编写完成 启动NetServerDemo,可以看到控制台中的日志打印如下: 在启动NetClientDemo,控制台打印如下: 客户端启动成功后,此时服务端的日志如下: 上面的代码创建了一个Tcp服务端和Tcp客户端,服务端监听本地的9984端口,客户端与本地的9984端口的Tcp服务端建立连接后发送数据, 当接收到客户端的请求时打印其传来的消息“发送数据......”,并回复“数据已接收......” 至此,一个基于Vert.x的Tcp服务端、客户端创建demo就完成了,是不是比Socket编程要简单很多呢? 资料参考 https://vertx.io/docs/ https://vertx.io/docs/vertx-core/java/

2019/4/2
articleCard.readMore

Vert.x Core(二)- Event Bus(事件总线)

本文是我在学习Vert.x过程中的一些笔记,作为记录。因为是初学,对Vert.x的理解还不够透彻,如有错误之处我们可以在评论中一起讨论呦。 概述 The event bus is the nervous system of Vert.x EnventBus是Vert.x的神经系统,EventBus为Verticle之间提供通讯和信息传递的基础。这种方式提供了一个简单但有效的解耦。 如上图,EventBus为多个Verticle实例传递消息,而在Vertx中每一个Verticle都对应着一个或者多个处理器(handler),我们将部署两个Verticle(H1和H2)来处理HTTP请求,一个Verticle(D1)封装数据库持久化。由此产生的Verticle将没有相互的直接引用,它们将只商定事件总线中的目的地名称以及消息格式。假设H1接收到查询请求,H1会将查询的消息发送到EventBus上,此时注册在该地址上的D1接收到了消息,执行查询任务将返回结果以JSON形式原路返回,这整个过程都是异步进行。 发送到事件总线的消息将解码为JSON。虽然Vert.x的事件总线支持灵活的串行化方案用于高要求或者高度定制的上下文,但是使用JSON数据通常是明智的选择。使用JSON的另一个优势是它是一种语言无关的格式。由于Vert.x是支持多语言的,对于使用不同语言编写的Verticle之间的通讯,JSON是非常理想的。 地址 Verticle之间的消息被EventBus发送到一个约定的地址(Address),消息的提供者和消费者通过地址来实现消息的生产和消费。 处理器 这里的消费者自然就是不同的处理器Handler,处理器之间可以根据不通的消息通信方式实现不同的功能。 EventBus事件总线中支持的消息通信方式有如下三种:、 点对点 : 消息指发送给一个监听这个地址上的 消费者(consumer) 。 发布/订阅 : 消息会被所有监听在这个地址上的所有 消费者(consumer) 收到。 请求/应答 : 消息回发送给一个 消费者(consumer) , 它 应答 这个消息并且把另外一个 消息 发送回初始的发送者。 消息类型 通常的消息格式无非字符串、整数、Json等,但因为Vert.x多语言的特点,JSON则是他最常用的消息类型,JSON在Vertx支持的所有语言都是非常容易创建、读取和解析的,因此它已经成为了Vert.x中的通用语。这就给程序员提供很大的发挥空间,你可以自定义一个专属的消息传递对象,通过JSON形式进行传递。 EventBus的使用 1.获取EventBus对象 1 EventBus eventBus = vertx.eventBus(); 2.EventBus对外提供的api 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 @Fluent EventBus send(String var1, Object var2); @Fluent <T> EventBus send(String var1, Object var2, Handler<AsyncResult<Message<T>>> var3); @Fluent EventBus send(String var1, Object var2, DeliveryOptions var3); @Fluent <T> EventBus send(String var1, Object var2, DeliveryOptions var3, Handler<AsyncResult<Message<T>>> var4); @Fluent EventBus publish(String var1, Object var2); @Fluent EventBus publish(String var1, Object var2, DeliveryOptions var3); <T> MessageConsumer<T> consumer(String var1); <T> MessageConsumer<T> consumer(String var1, Handler<Message<T>> var2); <T> MessageConsumer<T> localConsumer(String var1); <T> MessageConsumer<T> localConsumer(String var1, Handler<Message<T>> var2); <T> MessageProducer<T> sender(String var1); <T> MessageProducer<T> sender(String var1, DeliveryOptions var2); <T> MessageProducer<T> publisher(String var1); <T> MessageProducer<T> publisher(String var1, DeliveryOptions var2); @GenIgnore({"permitted-type"}) EventBus registerCodec(MessageCodec var1); @GenIgnore({"permitted-type"}) EventBus unregisterCodec(String var1); @GenIgnore <T> EventBus registerDefaultCodec(Class<T> var1, MessageCodec<T, ?> var2); @GenIgnore EventBus unregisterDefaultCodec(Class var1); @GenIgnore void start(Handler<AsyncResult<Void>> var1); @GenIgnore void close(Handler<AsyncResult<Void>> var1); @Fluent <T> EventBus addOutboundInterceptor(Handler<DeliveryContext<T>> var1); @Fluent <T> EventBus removeOutboundInterceptor(Handler<DeliveryContext<T>> var1); @Fluent <T> EventBus addInboundInterceptor(Handler<DeliveryContext<T>> var1); @Fluent <T> EventBus removeInboundInterceptor(Handler<DeliveryContext<T>> var1); 可以看到提供了很多的接口,我们大致对他们的功能进行分类如下: 发布消息publish 发布消息到指定地址可以使用publish方法 1 2 3 4 @Fluent EventBus publish(String var1, Object var2); @Fluent EventBus publish(String var1, Object var2, DeliveryOptions var3); eventBus.publish("hello.world", "发布一条消息...."); 通过publish发布的消息将会传递给所有在地址 hello.world 上注册过的处理器。这就涉及到EventBus的发布订阅 在发布的时候我们还有一个参数DeliveryOptions,其实可以理解为获取Vertx对象时的Vertxoptions一样,可以额外添加一些配置,进行消息的发布。 发送消息send 发送消息到指定地址可以使用send方法,他与发布消息最大的区别就是,send方法只会发送消息到指定地址上的一个处理器,正好对应点对点的信息通信方式 1 2 3 4 5 6 7 8 @Fluent EventBus send(String var1, Object var2); @Fluent <T> EventBus send(String var1, Object var2, Handler<AsyncResult<Message<T>>> var3); @Fluent EventBus send(String var1, Object var2, DeliveryOptions var3); @Fluent <T> EventBus send(String var1, Object var2, DeliveryOptions var3, Handler<AsyncResult<Message<T>>> var4); eventBus.send("hello.world", "发送一条消息...."); 除了单纯的发送消息外,EventBus还提供了用户自定义属性配置(DeliveryOptions),进行消息发送的接口 1 2 3 DeliveryOptions options = new DeliveryOptions(); options.addHeader("token", "aaaaaa"); eventBus.send("hello.world", "发送一条消息....", options); 当我们发送一条消息到某一地址后,消息被某一处理器接收并处理,这时我们需要知道消息是何时被消费的,我们可以通过send方法的另一种方式配合consumer方法来实现请求应答的消息通信方式。 1 2 3 4 5 6 7 8 9 10 11 eventBus.send("hello.world", "发送一条消息....", ar -> { if (ar.succeeded()) { System.out.println("结果: " + ar.result().body()); } }); MessageConsumer<String> consumer = eventBus.consumer("hello.world"); consumer.handler(message -> { System.out.println("处理器收到一条消息: " + message.body()); message.reply("消费成功!"); }); 注册处理器consumer 1 2 <T> MessageConsumer<T> consumer(String var1); <T> MessageConsumer<T> consumer(String var1, Handler<Message<T>> var2); consumer方法可以将处理器注册到指定的地址,第一个参数String var1就是address,EventBus提供了两个注册处理器的方法,一种是直接在参数中指定,另一种是通过comsumer方法返回的MessageConsumer对象进行注册设置。 1 2 3 4 5 6 7 8 9 10 EventBus eb = vertx.eventBus(); eb.consumer("hello.world", message -> { System.out.println("处理器收到一条消息: " + message.body()); }); MessageConsumer<String> msgcomsumer = eb.consumer("hello.world"); msgcomsumer.handler(message -> { System.out.println("处理器收到一条消息: " + message.body()); }); 如果你想知道他什么时候注册成功,那么可以为msgcomsumer绑定一个完成时的处理器 1 2 3 4 5 6 7 msgcomsumer.completionHandler(res -> { if (res.succeeded()) { System.out.println("处理器注册成功"); } else { System.out.println("处理器注册失败"); } });

2019/4/2
articleCard.readMore

Vert.x-Core(一)- 基础篇

本文是我在学习Vert.x过程中的一些笔记,作为记录。因为是初学,对Vert.x的理解还不够透彻,如有错误之处我们可以在评论中一起讨论呦。 Vert.x core模块是vertx的根基 是基于netty的一个工具包,提供tcp、http、websocket、dns、eventbus等基础功能封装 Vertx对象 Vertx对象是Vert.x的控制中心,是做一切事情的基础 直接创建该对象:Vertx vertx = Vertx.vertx(); 或者在创建时使用相关配置属性:Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40)); VertxOptions的具体属性参数参考 在Vertx中我们需要时刻保持eventLoop的畅通,当创建集群模式下的Vertx对象时,就不能用单机模式的方式了,因为让不同的 Vert.x 实例组成一个集群需要一些时间(也许是几秒钟)。在这段时间内,我们不想去阻塞调用线程,所以我们通过异步的方式来获取Vertx对象。 1 2 3 4 5 6 7 8 Vertx.clusteredVertx(new VertxOptions(), res -> { if (res.succeeded()) { Vertx vertx = res.result(); // 获取到了集群模式下的 Vertx 对象 // ..... } else { // 获取失败,可能是集群管理器出现了问题 } }); Vertx是事件驱动 当Vertx有一个事件要传递给某一个Hander去处理时,他会异步的去调用这个Hander。 Vertx中的大部分api都是不会阻塞线程的 传统的阻塞式的api,例如spring开发中,往往会有以下场景 线程a调用线程b,线程b执行逻辑,执行完毕后返回结果到线程a,线程a处理返回结果,线程a执行完毕 在这种场景下,线程a调用了线程b后就一直处于阻塞状态,如果此时有大量请求涌入,很可能造成灾难性的后果。 而如果使用Vertx来处理这种场景,则变成了如下的逻辑 线程a调用线程b,并告知线程b执行完毕后的通知线程c,线程a执行完毕。线程b开始执行执行完毕后通知线程c,线程c处理返回结果。 因为Vert.x API不会阻塞线程,所以通过Vert.x您可以只使用少量的线程来处理大量的并发。 EventLoop Vert.x的api保证无阻塞的情况下,Vert.x使用 Event Loop 来调用您的处理器。Event Loop 可以在事件到达时快速地分发到不同的处理器中。由于没有阻塞,Event Loop 可在短时间内分发大量的事件。例如,一个单独的 Event Loop 可以非常迅速地处理数千个 HTTP 请求。这种方式被称为反应器(Reactor)模式,所以呢,在Vertx中有一条黄金法则:不要阻塞EventLoop 处理阻塞式代码 虽然Vertx的大部分api是无阻塞的,但仍然存在一些阻塞式的代码。比如数据库操作,如果这种方法或者线程运行在EventLoop上,势必会造成阻塞,这种情况下内,Vertx提供了专门为阻塞式代码执行和处理回调的方法。 executeBlocking 1 2 3 4 5 6 7 8 vertx.executeBlocking(future -> { // 调用一些需要耗费很长时间返回结果的阻塞式API String result = someAPI.blockingMethod("hello"); future.complete(result); }, res -> { //处理结果 System.out.println("The result is: " + res.result()); }); 默认情况下,如果 executeBlocking 在同一个上下文环境中(如:同一个 Verticle 实例)被调用了多次,那么这些不同的 executeBlocking 代码块会 顺序执行(一个接一个)。若您不需要关心您调用 executeBlocking 的顺序,可以将 ordered 参数的值设为 false。这样任何 executeBlocking 都会在 Worker Pool 中并行执行。 Worker Verticle 1 2 3 4 5 6 7 8 WorkerExecutor executor = vertx.createSharedWorkerExecutor("my-worker-pool"); executor.executeBlocking(future -> { // 调用一些需要耗费显著执行时间返回结果的阻塞式API String result = someAPI.blockingMethod("hello"); future.complete(result); }, res -> { System.out.println("The result is: " + res.result()); }); Worker Executor 在不需要的时候必须被关闭: executor.close(); 异步协调 在Vertx中,Future可以用来协调多个异步线程的操作结果,Future支持两种组合方式:并发组合、顺序组合 并发组合 static <T1,T2> CompositeFuture all(Future<T1> f1,Future<T2> f2) static CompositeFuture all(List<Future> futures) 该方法接受多个 Future 对象作为参数(最多6个,或者传入 List)。当所有的 Future 都成功完成,该方法将返回一个 成功的 Future;当任一个 Future 执行失败,则返回一个 失败的 Future,例如: 1 2 3 4 5 6 7 8 9 10 11 12 13 Future<HttpServer> httpServerFuture = Future.future(); Future<NetServer> netServerFuture = Future.future(); httpServer.listen(httpServerFuture.completer()); netServer.listen(netServerFuture.completer()); CompositeFuture.all(httpServerFuture, netServerFuture).setHandler(ar -> { if (ar.succeeded()) { // 所有服务器启动完成 } else { // 有一个服务器启动失败 } }); 当组合的处理操作完成时,该方法返回的 Future 上绑定的处理器(Handler)会被调用。当一个操作失败(其中的某一个 Future 的状态被标记成失败),则返回的 Future 会被标记为失败。当所有的操作都成功时,返回的 Future 将会成功完成。 static <T1,T2> CompositeFuture any(Future<T1> f1,Future<T2> f2) static CompositeFuture any(List<Future> futures) 该方法的合并会等待第一个成功执行的Future。CompositeFuture.any 方法接受多个 Future 作为参数(最多6个,或传入 List)。当任意一个 Future 成功得到结果,则该 Future 成功;当所有的 Future 都执行失败,则该 Future 失败。 1 2 3 4 5 6 7 CompositeFuture.any(future1, future2).setHandler(ar -> { if (ar.succeeded()) { // 至少一个成功 } else { // 所有的都失败 } }); static <T1,T2> CompositeFuture join(Future<T1> f1,Future<T2> f2) static CompositeFuture join(List<Future> futures) join方法的合并会等待所有的 Future 完成,无论成败。CompositeFuture.join 方法接受多个 Future 作为参数(最多6个),并将结果归并成一个 Future 。当全部 Future 成功执行完成,得到的 Future 是成功状态的;当至少一个 Future 执行失败时,得到的 Future 是失败状态的。 1 2 3 4 5 6 7 CompositeFuture.join(future1, future2, future3).setHandler(ar -> { if (ar.succeeded()) { // 所有都成功 } else { // 至少一个失败 } }); 顺序合并 和 all 、join以及 any 实现的并发组合不同,compose 方法作用于顺序组合 Future。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Future<Void> startFuture = Future.future(); Future<Void> fut1 = Future.future(); FileSystem fs = vertx.fileSystem(); fs.createFile("/foo", fut1.completer()); fut1.compose(v -> { // fut1中文件创建完成后执行 Future<Void> fut2 = Future.future(); fs.writeFile("/foo", Buffer.buffer(), fut2.completer()); return fut2; }).compose(v -> { // fut2文件写入完成后执行 System.out.println("--------------------"); fs.copy("/foo", "/foo", startFuture.completer()); }, // 如果任何一步失败,将startFuture标记成failed startFuture) .setHandler(a -> { if (startFuture.succeeded()) { System.out.println("success..."); } else { System.out.println("error..."); } }); 这里例子中,有三个操作被串起来了: 一个文件被创建(fut1) 一些东西被写入到文件(fut2) 文件被移走(startFuture) 如果这三个步骤全部成功,则最终的 Future(startFuture)会是成功的;其中任何一步失败,则最终 Future 就是失败的。 Verticle Verticle 是由 Vert.x 部署和运行的代码块。一个应用程序通常是由在同一个 Vert.x 实例中同时运行的许多 Verticle 实例组合而成。不同的 Verticle 实例通过向 Event Bus 上发送消息来相互通信。 Verticle 的实现类必须实现 Verticle 接口。 如果您喜欢的话,可以直接实现该接口,但是通常直接从抽象类 AbstractVerticle 继承更简单。 1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyVerticle extends AbstractVerticle { // Called when verticle is deployed // Verticle部署时调用 public void start() { } // Optional - called when verticle is undeployed // 可选 - Verticle撤销时调用 public void stop() { } } Verticle 种类 Stardand Verticle:这是最常用的一类 Verticle —— 它们永远运行在 Event Loop 线程上。 当 Standard Verticle 被创建时,它会被分派给一个 Event Loop 线程,并在这个 Event Loop 中执行它的 start 方法。当您在一个 Event Loop 上调用了 Core API 中的方法并传入了处理器时,Vert.x 将保证用与调用该方法时相同的 Event Loop 来执行这些处理器。 这意味着我们可以保证您的 Verticle 实例中 所有的代码都是在相同Event Loop中执行(只要您不创建自己的线程并调用它!) 同样意味着您可以将您的应用中的所有代码用单线程方式编写,让 Vert.x 去考虑线程和扩展问题。您不用再考虑 synchronized 和 volatile 的问题,也可以避免传统的多线程应用经常会遇到的竞态条件和死锁的问题。 Worker Verticle:这类 Verticle 会运行在 Worker Pool 中的线程上。一个实例绝对不会被多个线程同时执行。 不是由一个 Event Loop 来执行,而是由Vert.x中的 Worker Pool 中的线程执行。 Worker Verticle 被设计来调用阻塞式代码,它不会阻塞任何 Event Loop。 将 Verticle 部署成一个 Worker Verticle,通过 如下方法来设置: 1 2 DeploymentOptions options = new DeploymentOptions().setWorker(true); vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options); Multi-Threaded Worker Verticle:这类 Verticle 也会运行在 Worker Pool 中的线程上。一个实例可以由多个线程同时执行(因此需要开发者自己确保线程安全)。 Verticle部署 deployVerticle方法可用来部署Verticle,具体怎么部署可以看看源码中提供的方法: package io.vertx.core.Vertx; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @GenIgnore({"permitted-type"}) void deployVerticle(Verticle var1); @GenIgnore({"permitted-type"}) void deployVerticle(Verticle var1, Handler<AsyncResult<String>> var2); @GenIgnore({"permitted-type"}) void deployVerticle(Verticle var1, DeploymentOptions var2); @GenIgnore({"permitted-type"}) void deployVerticle(Verticle var1, DeploymentOptions var2, Handler<AsyncResult<String>> var3); @GenIgnore void deployVerticle(Class<? extends Verticle> var1, DeploymentOptions var2); @GenIgnore void deployVerticle(Class<? extends Verticle> var1, DeploymentOptions var2, Handler<AsyncResult<String>> var3); @GenIgnore({"permitted-type"}) void deployVerticle(Supplier<Verticle> var1, DeploymentOptions var2); @GenIgnore({"permitted-type"}) void deployVerticle(Supplier<Verticle> var1, DeploymentOptions var2, Handler<AsyncResult<String>> var3); void deployVerticle(String var1); void deployVerticle(String var1, Handler<AsyncResult<String>> var2); void deployVerticle(String var1, DeploymentOptions var2); void deployVerticle(String var1, DeploymentOptions var2, Handler<AsyncResult<String>> var3); 阅读源码可以看出,部署方式大概有两类 实例部署 vertx.deployVerticle(new MyFirstVerticle()); vertx.deployVerticle(MyFirstVerticle.class,new DeploymentOptions()); vertx.deployVerticle(MyFirstVerticle::new,new DeploymentOptions()); 类名部署 vertx.deployVerticle(“com.zhengql.vertx.MyFirstVerticle”); Verticle的部署是异步的,当我们调用deployVerticle方法后,部署结果不是立即返回的,我们可以同步绑定处理异步返回结果的处理器: void deployVerticle(String var1, Handler<AsyncResult<String>> var2); 1 2 3 4 5 6 7 8 vertx.deployVerticle("com.zhengql.vertx.MyFirstVerticle", res -> { if (res.succeeded()) { //如果部署成功,这个完成处理器的结果中将会包含部署ID的字符串。这个部署 ID可以在之后您想要撤销它时使用。 System.out.println("Deployment id is: " + res.result()); } else { System.out.println("Deployment failed!"); } }); 撤销Verticle 我们可以通过 undeploy 方法来撤销部署好的 Verticle。 撤销操作也是异步的,因此若您想要在撤销完成过后收到通知则可以指定另一个完成处理器: 1 2 3 4 5 6 7 vertx.undeploy(deploymentID, res -> { if (res.succeeded()) { System.out.println("Undeployed ok"); } else { System.out.println("Undeploy failed!"); } }); DeploymentOptions 在上边的部署方法api中有一个参数是DeploymentOptions ,可以通过配置自定义的配置来部署Verticle。 指定Verticle的实例数量。 1 2 DeploymentOptions options = new DeploymentOptions().setInstances(2); vertx.deployVerticle("com.zhengql.vertx.MyFirstVerticle", options); 部署时传给 Verticle 一个 JSON 格式的配置,该配置中的值可以在Verticle的start()中通过config().getString()方法来获取。 1 2 3 JsonObject config = new JsonObject().put("name", "zhengql").put("age", 18); DeploymentOptions options = new DeploymentOptions().setConfig(config); vertx.deployVerticle("com.zhengql.vertx.MyFirstVerticle", options); 定时任务 在Vertx中我们要求了Verticle不可以阻塞EventLoop,所以我们不能在Verticle中使用线程调度方法sleep、wait等,好在Vert.x为我们提供了专用的定时器 一次性定时器 setTimer 一次性计时器会在一定延迟后调用 Event Handler如下 延迟5s,打印字符串 1 2 3 4 //5000代表延迟时间,单位毫秒 long timerID = vertx.setTimer(5000, id -> { System.out.println("hello xiaogege"); }); 周期性定时器 setPeriodic 周期性触发的定时器setPeriodic,在任务第一次触发前也是需要延时的,demo如下 每5s打印一次字符串,注:第一次不是立即触发,时间单位毫秒 1 2 3 long timerID = vertx.setPeriodic(5000, id -> { System.out.println("hello xiaojiejie"); }); 取消定时任务 cancelTimer 首先先来看看定时任务的api long setTimer(long var1, Handler<Long> var3);long setPeriodic(long var1, Handler<Long> var3);boolean cancelTimer(long var1); 从api中可以看出,设置定时任务都有一个long型的返回值,取消定时任务需要一个long型的参数,可能你已经猜到了两者之间的关系,那么这个long型的返回值是什么呢? 1 2 3 4 5 6 7 8 9 setTimer long setTimer(long delay, Handler<Long> handler) Set a one-shot timer to fire after delay milliseconds, at which point handler will be called with the id of the timer. Parameters: delay - the delay in milliseconds, after which the timer will fire handler - the handler that will be called with the timer ID when the timer fires Returns: the unique ID of the timer 这个返回值是定时器的唯一id,当定时器触发调用处理器也是通过这个唯一id。 撤销定时器直接调用cancelTimer即可 资料参考 https://vertx.io/docs/ https://vertx.io/docs/vertx-core/java/

2019/4/1
articleCard.readMore

SpringBoot项目中实现国际化

前言 什么是国际化呢?国际惯例,来时来一段官方介绍: 国际化(internationalization)是设计和制造容易适应不同区域要求的产品的一种方式。它要求从产品中抽离所有地域语言,国家/地区和文化相关的元素。换言之,应用程序的功能和代码设计考虑在不同地区运行的需要,其代码简化了不同本地版本的生产。开发这样的程序的过程,就称为国际化。 在我们实际开发中,一个web应用可能要在多个地区使用,面对不同地区的不同语言,为了适应不同的用户,我们可以尝试在前端页面实现多语言的支持,那么同样对于后端返回的一些提示信息,异常信息等,我们后端也可以根据不同的语言环境来进行国际化处理,返回相应的信息。 开发工具 IDEA、Maven、SpringBoot2.0.5、Jdk1.8、google浏览器 SpringBoot中的国际化 原理: 想要使应用支持国际化,首先需要知道用户的语言环境,即用户想要看到的语言,我们设想在用户每次请求时告诉服务器自己的语言环境,服务器收到请求后,根据不同的语言环境返回不同的信息来实现国际化。在spring应用中,用户的语言环境是通过区域解析器来识别的,而区域解析器有分为好几种(后面详细说),在我们不做配置修改时,spring使用AcceptHeaderLocaleResolver作为默认的区域解析器,它是根据HTTP请求 Header中的Accept-language的值来解析,当然区域解析器我们也可以自定义配置。 springboot默认就支持国际化。我们只需要只需要作相应的配置即可。 1.首先你需要一个springboot项目。IDEA中分分钟创建好一个项目。 2.在resources下定义国际化配置文件,注意名称必须以messages开始。(在springboot中,当我们不修改配置时默认去解析名称以message开始的properties文件) messages.properties (默认环境,无法确定语言环境时,解析该文件中的相应信息) messages_zh_CN.properties(中文语言环境时,解析该文件中的相应信息) messages_en_US.properties(英文语言环境时,解析该文件中的相应信息) 在三个配置文件中分别以Key = Value形式存储如下三条信息,如下: 1 welcome = 这是一个支持国际化的项目。 1 welcome = 这是一个支持国际化的项目。 1 welcome = This is a project supporting internationalization. 3.创建thymeleaf页面 加入thymeleaf依赖 1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> 在resources/templates目录下创建hello.html页面: 尝试在不同的语言环境下,通过#{welcome}获取信息 1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>demo</title> </head> <body> <p><label th:text="#{welcome}"></label></p> </body> </html> 4.创建访问页面的controller 注意这里controller的注解时@Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * 描述: * * @author zhengql * @date 2018/9/25 19:28 */ @Controller public class BaseController { @RequestMapping("/hi") public String hello() { return "/hello"; } } 测试国际化效果 这里使用google浏览器进行测试,测试之前需要安装插件Language Switcher Language Switcher : 可以改变当前请求的语言环境(根据自己的选择) 启动我们的springboot项目,google浏览器访问 http://127.0.0.1:8080/hi ,可以看到如下页面: 通过Language Switcher切换语言环境为English - United States,重新访问 http://127.0.0.1:8080/hi ,可以看到如下页面: ok,大功告成,到此一个简单的国际化项目就完成了。 扩展国际化 通过上面的小栗子,我们可以看到一个简单的国际化使用,但是在开发中中还需要我们进行一定的配置,来满足我们不同情况下的使用。 在返回结果中获取国际化信息 很多时候,后端接收到一个请求后,需要返回一个提示信息,而此时我们可以使这个返回信息支持国际化 这里就用到了org.springframework.context.MessageSource接口,MessageSource提供了三个方法 @Nullable//参数字段可为空String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4);String getMessage(String var1, @Nullable Object[] var2, Locale var3) throws NoSuchMessageException;String getMessage(MessageSourceResolvable var1, Locale var2) throws NoSuchMessageException; String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4):用来从MessageSource获取消息的基本方法。如果在指定的locale中没有找到消息,则使用默认的消息。var2中的参数将使用标准类库中的MessageFormat来作消息中替换值。 String getMessage(String code, Object[] args, Locale loc):本质上和上一个方法相同,其区别在:没有指定默认值,如果没找到消息,会抛出一个NoSuchMessageException异常。 String getMessage(MessageSourceResolvable resolvable, Locale locale):上面方法中所使用的属性都封装到一个MessageSourceResolvable实现中,而本方法可以指定MessageSourceResolvable实现。 下面我们实践一下: 1.创建一个以json返回格式的controller,注入MessageSource,注意controller的注解为@RestController 在这里首先我们需要获取到当前请求的Locale,有两种方法: 1 2 Locale locale = LocaleContextHolder.getLocale(); Locale locale = RequestContextUtils.getLocale(request); 两种方式根据情况选择使用,下面是controller代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.example.i18n.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 描述: * * @author zhengql * @date 2018/9/21 10:54 */ @RestController public class JsonController { @Autowired private MessageSource messageSource; @RequestMapping("/ha") public String ha() { return messageSource.getMessage("welcome", null, LocaleContextHolder.getLocale()); } } 2.启动项目访问 http://127.0.0.1:8080/ha ,可以看到相应语言环境的返回信息 通过Language Switcher切换语言环境为English - United States,重新访问http://127.0.0.1:8080/ha,可以看到如下页面: 支持占位符国际化信息返回 我们经常会遇到这样一个场景,登录账号需要验证码,填写完手机号获取验证码后会收到一条类似于尊敬的用户13099999999您好,您的验证码是6666,这种信息,其实就是一个模板,通过改变参数,重复使用。我们通过国际化资源文件中的占位符,配合MessageSource提供的api也可以实现。 资源文件中加入如下属性: messages.properties,messages_zh_CN.properties 1 hello=你好:{0} , 你的验证码为 :{1} messages_en_US.properties 1 hello=Hello: {0}, your verification code is: {1} 我们在JsonController中,创建一个测试接口 1 2 3 4 @RequestMapping("/haha") public String haha() { return messageSource.getMessage("hello", new Object[]{"zhangsan","123456"}, LocaleContextHolder.getLocale()); } 启动项目直接,访问 http://127.0.0.1:8080/haha 可以看到相应语言环境的返回信息 通过Language Switcher切换语言环境为English - United States,重新访问 http://127.0.0.1:8080/haha ,可以看到如下页面: 支持国际化的枚举类 既然返回信息可以实现国际化,那我们的枚举类同样也可以实现国际化咯 创建一个枚举类EnumSuccessOrError.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 /** * 描述:枚举类举例 * * @author zhengql * @date 2018/9/26 20:52 */ public enum EnumSuccessOrError { SUCCESS(0, "操作成功"), ERROR(1, "操作失败"); /** * 返回状态码 */ private int statusCode; /** * 返回状态信息 */ private String statusMsg; EnumSuccessOrError(int statusCode, String statusMsg) { this.statusCode = statusCode; this.statusMsg = statusMsg; } /** * @return the statusCode */ public int getStatusCode() { return statusCode; } /** * @return the statusMsg */ public String getStatusMsg() { return statusMsg; } } 如上,刚刚创建的枚举类是不支持国际化的,我们呢需要改造他,当调用getStatusMsg方法时根据语言环境返回相应的国际化字符串。可以从如下两个点着手: getStatusMsg方法改造 资源文件中添加不同语言环境对应的返回值 先在三个资源文件中加入不同环境的返回值: messages.properties,messages_zh_CN.properties 1 2 SUCCESS = 操作成功 ERROR = 操作失败 messages_en_US.properties 1 2 SUCCESS=success ERROR=error 改造后的枚举如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public enum EnumSuccessOrError { SUCCESS(0, "SUCCESS"), ERROR(1, "ERROR"); /** * 返回状态码 */ private int statusCode; /** * 返回状态信息 */ private String statusMsg; EnumSuccessOrError(int statusCode, String statusMsg) { this.statusCode = statusCode; this.statusMsg = statusMsg; } private MessageSource messageSource; public EnumSuccessOrError setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; return this; } //通过静态内部类的方式注入bean,并赋值到枚举中 @Component public static class ReportTypeServiceInjector { @Autowired private MessageSource messageSource; @PostConstruct public void postConstruct() { for (EnumSuccessOrError rt : EnumSet.allOf(EnumSuccessOrError.class)) rt.setMessageSource(messageSource); } } /** * @return the statusCode */ public int getStatusCode() { return statusCode; } /** * @return the statusMsg,根据语言环境返回国际化字符串 */ public String getStatusMsg() { return messageSource.getMessage(statusMsg,null,statusMsg, LocaleContextHolder.getLocale()); } 此时我们在JsonController中,再创建一个测试接口 1 2 3 4 @RequestMapping("/enumDemo") public String enumDemo() { return EnumSuccessOrError.SUCCESS.getStatusMsg(); } 启动项目直接,访问 http://127.0.0.1:8080/enumDemo 可以看到相应语言环境的返回信息 通过Language Switcher切换语言环境为English - United States,重新访问 http://127.0.0.1:8080/enumDemo ,可以看到如下页面: 参考和感谢 https://blog.csdn.net/linxingliang/article/details/52350238 https://blog.csdn.net/flowingflying/article/details/76358970

2019/4/1
articleCard.readMore

Vert.x介绍

1 Vert.x不是一个框架,而是一个工具包 Vert.x是基于Netty项目——一个基于JVM的高性能异步网络库,它的核心库定义了编写异步网络应用的基本API,你可以为应用程序选择有用的模块(如数据库链接、监控、认证、日志、服务发现、集群支持等),同时它也可以内嵌到现有的应用中(如springboot项目中)既能体验Vert.x的高效又兼顾spring的泛用性 vertx并不是针对特定应用的框架,它其实很大程度上就是netty的一个最佳实践的封装。在java上实现了类似于node.js的异步处理框架。 2 Vert.x是多语言的 Vert.x运行在Java虚拟机上,支持多种编程语言,Vert.x是高度模块化的,同一个应用,你可以选择多种编程语言同时开发。它支持广泛的流行的JVM语言:Java、Groovy、Scala、Kotlin、JavaScript、Ruby及Ceylon。 3 完善的生态体系 与我们熟悉的spring类似,Vert.x拥有完善的生态,使用vert.x可以完成一个应用的所有开发工作,可以实现一个完全异步处理的应用。它拥有类似于spring全家桶的生态体系,具体如下: 3.1 核心模块:Vert.x-core Vert.x核心模块包含一些基础的功能,如HTTP,TCP,文件系统访问,EventBus、WebSocket、延时与重复执行、缓存等其他基础的功能,你可以在你自己的应用程序中直接使用。可以通过vertx-core模块引用即可。 3.2 支持web开发:Vert.x-Web Vert.x Core 提供了一系列底层的功能用于操作 HTTP,对于一部分应用来是足够的。 Vert.x Web 基于 Vert.x Core,提供了一系列更丰富的功能以便更容易地开发实际的 Web 应用。 3.3 Vert.x提供多种数据访问的Api封装 Vert.x提供了对关系型数据库、NoSQL、消息中间件的支持,传统的客户端因为是阻塞的,会严重影响系统的性能,因此Vert.x提供了对以上客户端的异步支持。具体支持的数据访问如下: MongoDB client JDBC client SQL common Redis client MySQL/PostgreSQLclient 3.5 微服务的支持:服务发现、熔断器 Vert.x Service Discovery:一个服务发现的基础组件,用来发布和发现各种类型的资源 Vert.x Circuit Breaker:是熔断器模式的Vert.x实现。可与springcloud种的Hystrix对比 Vert.x Config:提供了一种配置 Vert.x 应用的方式。 3.6 Vert.x整合了常用的消息驱动:Vert.x Integration Vert.x Mail Client:提供了一简单STMP邮件客户端,所以你可以在应用程序中发送电子邮件 Vert.x STOMP Client & Server:提供了STOMP协议的实现包括客户端与服务端。 Vert.x JCA Adaptor:提供了Java连接器架构适配器,这允许同任意JavaEE应用服务器进行互操作。 Vert.x RabbitMQ Client:消息队里的客户端支持 Vert.x Kafka Client:消息队里的客户端支持 Vert.x Consul Client:google开源的一个使用go语言开发的服务发现、配置管理中心服务。内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案。 3.7 身份验证和授权: Vert.x提供了简单API用于在应用中提供认证和授权。 Auth common:通用的认证API,可以通过重写AuthProvider类来实现自己的认证 JDBC auth: 后台为JDBC的认证实现 JWT auth: 用JSON Web tokens认证实现 Shiro auth: 使用Apache Shiro认证实现 MongoDB auth: MongoDB认证实现 OAuth 2: Oauth2协义认证实现 htdigest auth: 这个是新增一种认证的支持 5 Vert.x、lambda、JDK8 Vert.x异步也带来了编码上的复杂性,想要编写优美的异步代码,就需要对lambda表达式、函数式编程、Reactive等技术非常熟悉才行,否则很容易导致你的代码一团糟,完全没有可读性。 在JDK8中引入了lambda表达式后,使用Java开发Vert.x应用就变得十分顺畅。 6 Vert.x核心概念 Verticle Vert.x中的部署单元称为Verticle。Verticle是Vert.x中的一个核心概念。如果说Vertx是“应用”,那么Verticle就是应用中的一个服务。另一个更形象一点的比喻,如果说Vertx是一个机架,那么Veticle就是机架上的服务器。Verticle可以被传递一些配置信息(如证书、网络地址等),而且Verticle可以被多次部署,Verticle可以部署其它Verticle verticle分两种,一种是基于EventLoop的适合I/O密集型的,还有一种是适合CPU密集型的worker verticle EvenLoop 事件循环(EventLoop)是异步编程模型中是特有的,一个Verticle通过一个事件循环(EventLoop)处理接收到的事件,这些事件可以是任何事情,如接收网络缓冲、调度事件或由其它Verticle发送的消息,事件循环(EventLoop)中执行时,不能进行线程阻塞操作 EvenBus 事件总线(EvenBus)是在不同Verticle之间通过异步消息传递进行通讯的主要工具 其他介绍 Vert.x 的定义是 “用来在JVM上构建反应式(reactive)应用程序的工具箱”。 Vert.x并不是一个应用服务器,一个容器或者一个框架。 它也并不是一个JavaScript开发库。Vert.x是一个朴素的老的 jar文件,所以一个Vert.x应用程序实际上是一个使用这个 jar 文件的程序。 Vert.x并不强制一个打包的方式。所有Vert.x 模块(components) 都是朴素 平淡 的 jar 文件。 这将怎样影响你的应用程序呢?让我们想象你在使用一个项目构建工具,比方说Maven或者Gradle, 去建立你的应用,一个 Vert.x 应用程序,其实就是吧 vertx-core 加入到依赖项里。 你想使用其他的 Vert.x 组件吗?请把它+到你的依赖项里。这很简单,毫无负担,不是吗。 启动这个程序就是启动一个简单实现了 public static void main(String[] args) 的类。我们不需要任何特殊的IDE或者插件去安装和开始使用 Vert.x。 反应式、响应式编程、响应式系统 Vert.x 是 反应式 。它就是要用来建立反应式应用程序,或者更贴切的说法是反应式系统 再次,Vert.x 是一个事件驱动和非阻塞的。事件被投递到一个永不阻塞的事件循环(EventLoop) 里。Vert.x只使用非常少的线程。 有一些线程是事件循环, 它们在处理器(Handlers) 之间派发事件。如果你把某个线程阻塞了,事件将不能继续派发。这个执行模式将影响你如何写代码,不同于传统的阻塞代码,你的代码将是异步的和非阻塞的。 举一个例子,如果你要得到一个基于URL的资源,你需要这么做: 1 2 3 4 5 6 7 8 URL site = new URL("http://vertx.io/"); BufferedReader in = new BufferedReader(new InputStreamReader(site.openStream())); String inputLine; while ((inputLine = in.readLine()) != null) { System.out.println(inputLine); } in.close(); 但是用 Vert.x 的话,就很简单: 1 2 3 vertx.createHttpClient().getNow(80, "vertx.io", "", response -> { response.bodyHandler(System.out::println); }); 对于异步编程的理解,比如ajax与Vert.x 1 2 3 4 5 6 7 8 9 10 11 12 13 //ajax代码 console.log("1"); $.ajax({ "url" : "/hello", "type" : "post", "dataType" : "json", "success" : function(val) { console.log("2"); } }); console.log("3"); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //vert.x代码 System.out.println("1") WebClient .create(vertx) .postAbs(REQUEST_URL) // 这里指定的是请求的地址 .sendBuffer(buffer, res -> { // buffer是请求的数据 if (res.succeeded()) { // 请求远程服务成功 System.out.println("2") } else { // 请求失败 resultHandler.handle(Future.failedFuture("请求服务器失败...")); } }); System.out.println("3") 学习资料 Vert.x 官方文档中文翻译 A gentle guide to asynchronous programming with Eclipse Vert.x for Java developers中文翻译版 Vert.x CSDN教程专栏 Vert.x 蓝图 - Micro-Shop 微服务实战

2019/4/1
articleCard.readMore

毕设选题项目本地运行环境搭建教程

Please enter the password to read the blog. Incorrect Password! No content to display! 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

2018/10/17
articleCard.readMore

Jupyter Notebooks的安装和使用介绍

最近又开始重新学习Python,学习中使用到了一款编辑器Jupyter Notebooks ,非常想安利给初学python的同学。 注:本文内容仅针对windows环境下安装和配置Jupyter Notebooks 。 1.Jupyter Notebooks 简介 国际惯例还是来一段官方的介绍: Jupyter Notebook是一个Web应用程序,允许您创建和共享包含实时代码,方程,可视化和说明文本的文档。 用途包括:数据清理和转换,数值模拟,统计建模,机器学习等等。 Notebooks其实就像是你的python笔记本一样,不仅可以运行书写的python代码,同时还支持markdown格式的文本显示。 在Notebooks中不仅可以运行python,它还支持R、Julia 和 JavaScript等其他40余种语言。 2.安装Jupyter Notebooks 安装方式大致分为两种: pip 方法 Anaconda 方法 pip 方法 使用pip命令安装之前需要我们先安装Python。此处以python3.6为例 1.安装Python 可以在python下载处,选则对应的系统版本,我这里选择Windows x86-64 executable installer下载安装。 具体的安装就不再赘述,主要说明一下几点: 需要注意的是安装时记得勾选Add Python 3.6 to PATH,然后选择Customize installation。 添加 Path,是为了以后可以在任何目录下使用 cmd 运行 Python,跟 Java 的 path一样。如果安装过程中没有添加 Path,也可以以后再添加。 自定义安装可以选择安装路径,这里我的安装路径为E:\MyTools\Python\Python36 2.升级pip到最新版本 安装python3.6的同时会安装pip,但此时需要升级pip到最新版 打开命令提示窗,切换到python3.6的安装目录下的Scripts文件夹。 执行如下命令: 1 pip install --upgrade pip 3.安装Jupyter Notebooks 打开命令提示窗,切换到python3.6的安装目录下的Scripts文件夹。执行如下命令 1 pip install jupyter 安装完成Scripts文件夹如下图 4.启动 Jupyter Notebooks 打开命令提示窗,切换到python3.6的安装目录下的Scripts文件夹。执行如下命令 1 jupyter notebook 出现如下提示,启动成功,并且浏览器自动打开notebook窗口。此时显示的是Script文件夹下的文件目录。 Anaconda 方法 对与初学者而言,还是推荐该使用 Anaconda 来安装 Python 和 Jupyter Notebooks。 在安装 Anaconda 的同时会安装Python 和 Jupyter Notebooks这两个工具,并且还包含相当多数据科学和机器学习社区常用的软件包。 可以在Anaconda官网下载页来下载安装包,它提供了python3.6和python2.7两个版本,可以根据自己的需要来下载对应系统的安装文件。具体的安装步骤可以参考Windows系统安装Anaconda 修改jupyter notebook工作空间 在我们第一次启动Notebooks时,默认显示的是Script文件夹下的文件目录。因为此时notebooks默认的工作空间是安装目录。 当然了,你也可以自定义一个专属的工作空间,操作如下: 1.创建一个文件夹,此处我创建了一个jupyter-notebook文件夹,他的目录是E:\MyTools\Python\jupyter-notebook 2.获取jupyter notebook的配置文件 打开命令提示窗口,执行如下命令: 1 jupyter notebook --generate-config 此处需要注意的是,如果你已经配置过notebooks的相关信息,执行此命令会提示你是否覆盖原有配置。如果是首次执行此命令,则生成配置到相应目录。如下图所示,输入y直接覆盖 3.修改配置文件 打开生成的配置文件,修改#c.NotebookApp.notebook_dir = ''此条配置,在单引号中填入我们刚才创建的专属工作空间,此处我这里是E:\MyTools\Python\jupyter-notebook,此条配置默认是注释掉的,所以我们需要删除第一个#,ok,保存配置文件。 好了,现在打开命令提示窗口,执行jupyter notebook重新启动notebooks,浏览器相应会打开notebooks主页,主页中相应会显示工作空间中的文件目录。 注意:启动notebooks之后,不要不要不要关闭该命令提示窗口,因为一旦关闭该窗口就会与本地服务器断开连接 jupyter notebook 基本使用 如果按照上面的操作进行配置后,启动notebooks后的首页应该是这个样子的 下面对首页上的功能按钮进行基本说明: 第一部分介绍: Files:列出所有文件 Running:展示你当前打开的终端和笔记本 Clusters:由 IPython 并行提供的(emmmmm,暂时也没使用过) 第二部分介绍: 点击右侧的New按钮可展开如图的下拉列表按钮,其内包括了可创建的四种工作环境: Python3:创建一个可以执行python代码的文件(后面详细介绍) Text File:创建文本类型的文件,后缀名为.txt Folder:创建一个文件夹 Teminal:在浏览器中打开一的命令窗口 第三部分介绍: 这里的按钮其实就是对当前工作空间内的文件进行复制、重命名等的一系列操作: Duplicate:复制文件 Rename:重命名 Move:移动文件 Download:下载文件 View:在浏览器中预览文件内容 Edit:编辑文件 Delete(小图标):删除选中的文件 jupyter notebook 中编写并执行python代码 在首页右侧点击New,选择点击Python3,页面即跳转到一个新的窗口,此时已经创建了一个新的文件,红色区域为该文件的名称(默认为Untitled),点击即可修改文件名,此处我们命名为test,如下所示, 在In [ ] :后面的输入框中我们可以输入一段python代码进行测试,点击Run按钮执行,也可以快捷键Ctrl+Enter执行代码,结果如下 Jupyter Notebooks 的强大之处在于除了能够输入代码之外,你还可以用 Markdown 添加叙述性和解释性文本。比如我想添加一个文字说明,在代码上面添加了一个单元格,并以 Markdown 输入了一个文本。按下Ctrl+Enter,效果如下: jupyter notebook 中的快捷键介绍 当你熟练使用notebooks的基本功能后,掌握他的快捷键是十分必要的,这样可以大大提高你的工作效率。下面是一些比较常用的快捷键: 编辑模式:点击单元格按下Enter 命令模式(退出编辑模式):Esc 进入命令模式之后(此时你没有活跃单元),有以下快捷键: A:在所选单元之上插入一个新的单元 B:在所选单元之下插入一个新的单元 D:连续按两次删除所选的单元 Z:撤销被删除的单元 Y:将当前选中的单元变成一个代码单元 F:查找和替换 Shift +上或下箭头:可选择多个单元。 Shift + M:在多选模式时,可合并你的选择。 处于编辑模式时(在命令模式时按 Enter 会进入编辑模式),下列快捷键很有用: Ctrl + Home :到达单元起始位置 Ctrl + S :保存进度 Ctrl + Enter :会运行你的整个单元块 Alt + Enter :不止会运行你的单元块,还会在下面添加一个新单元 Ctrl + Shift + F :打开命令面板 可在命令模式按 H 或进入Help > Keyboard Shortcuts。可以查看键盘快捷键完整列表。如下: 总结 关于notebooks的安装和基本用法就先介绍到这里了,有兴趣的朋友不妨动手安装一个试试。 参考和感谢 https://baijiahao.baidu.com/s?id=1601883438842526311&wfr=spider&for=pc http://www.cnblogs.com/zlslch/p/6984403.html

2018/10/17
articleCard.readMore

算法笔试题:1元,5元,10元,20元,50元、100元面值人民币组合给定x元的问题

最近有一道笔试题引起了小伙伴们的激烈讨论。 参考博客 作为算法菜鸟非常感谢大神的分析和举例。博客地址 问题描述 目前市面上的纸币主要有1元,5元,10元,20元,50元、100元六种,如果要买一件商品x元,有多少种货币组成方式? 思路一 现有6种面额的纸币用来组合成给定的x元金额。那么可以大致推出这个等式 sum 表示给定的金额 {x1, x2, x3, x4, x5, x6}分别表示1元,5元,10元,20元,50元、100元的张数 1 sum = x1 * 1 + x2 * 5 + x3 * 10 + x4 * 20 + x5 * 50 + x6 * 100 如此看来其实就是求解满足这个等式的 {x1, x2, x3, x4, x5, x6} 的所有可能的个数。 可以通过循环来依次确定每种面额的纸币有多少张,最终来判断,不同张数的组合最终是否等于x元。 于是有了如下代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class Demo1 { /** * @param x 商品金额 */ public static void test1(int x){ int sum = 0; //符合条件的组合次数 int count = 0; //循环次数 int times = 0; //硬币面额 int[] a = {1, 5, 10, 20, 50, 100}; for (int i = 0; i <= x / a[5]; i++) { //100元可能出现的张数 for (int j = 0; j <= x / a[4]; j++) { //50元可能出现的张数 for (int k = 0; k <= x / a[3]; k++) { //20元可能出现的张数 for (int l = 0; l <= x / a[2]; l++) { //10元可能出现的张数 for (int m = 0; m <= x / a[1]; m++) { //5元可能出现的张数 //for(int n=0;n<x/1;n++){//这步循环可省略 int n = x - (i * a[5] + j * a[4] + k * a[3] + l * a[2] + m * a[1]); sum = i * a[5] + j * a[4] + k * a[3] + l * a[2] + m * a[1] + n * a[0]; times++; if (sum == x && n >= 0) { count++; } //} } } } } } System.out.println("循环次数:" + times); System.out.println("组合数:" + count); } public static void main(String[] args) { long startTime = System.currentTimeMillis(); //指定200元的金额 test1(200); long endTime = System.currentTimeMillis(); System.out.println("执行时间:" + (endTime - startTime) + "ms"); } } 执行结果如下: 1 2 3 循环次数:142065 组合数:3274 执行时间:13ms 结果分析 这种解决方式虽然可以得到正确的结果,但是计算量很大,循环次数随着指定的金额增大会越来越高。性能也就非常差,基本上数字超过1000,就是无脑循环了。所以这并不是最优解。 思路二 从上面的分析中我们也可以这么考虑,我们希望用m种纸币构成sum元。 1 sum = x1 * V1 + x2 * V2 + ... + xm * Vm 根据最后一个面额Vm的系数的取值为无非有这么几种情况,xm分别取{0, 1, 2, …, sum/Vm},换句话说,上面分析中的等式和下面的几个等式的联合是等价的。 1 2 3 4 5 6 7 8 9 sum = x1 * V1 + x2 * V2 + ... + 0 * Vm sum = x1 * V1 + x2 * V2 + ... + 1 * Vm sum = x1 * V1 + x2 * V2 + ... + 2 * Vm ... sum = x1 * V1 + x2 * V2 + ... + K * Vm 其中K是该xm能取的最大数值K = sum / Vm。可是这又有什么用呢?不要急,我们先进行如下变量的定义: dp[i][sum] = 用前i种硬币构成sum 的所有组合数。 那么题目的问题实际上就是求dp[m][sum],即用前m种纸币(所有纸币)构成sum的所有组合数。 在上面的联合等式中: 当xm=0时,有多少种组合呢? 实际上就是前i-1种纸币组合sum,有dp[i-1][sum]种! xm = 1 时呢,有多少种组合? 实际上是用前i-1种纸币组合成(sum - Vm)的组合数,有dp[i-1][sum -Vm]种; xm =2呢, dp[i-1][sum - 2 * Vm]种,等等。 所有的这些情况加起来就是我们的dp[i][sum]。所以: 1 dp[i][sum] = dp[i-1][sum - 0*Vm] + dp[i-1][sum - 1*Vm] + dp[i-1][sum - 2*Vm] + ... + dp[i-1][sum - K*Vm]; 其中K = sum / Vm 换一种更抽象的数学描述就是: 1 dp[i][sum] = \sum_{k=0}^{sum/vm} dp[i-1][sum - K*Vm] 通过此公式,我们可以看到问题被一步步缩小,那么初始情况是什么呢?如果sum=0,那么无论有前多少种来组合0,只有一种可能,就是各个系数都等于0; dp[i][0] = 1 // i = 0, 1, 2, … , m 如果我们用二位数组表示dp[i][sum], 我们发现第i行的值全部依赖与i-1行的值,所以我们可以逐行求解该数组。如果前0种硬币要组成sum,我们规定为dp[0][sum] = 0. 第二种代码实现方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Demo1 { /** * @param x 商品金额 */ public static void test2(int n){ //硬币面额 int money[]={1,5,10,20,50,100}; int dp[] = new int[n+1]; dp[n] = 0; dp[0] = 1; for(int i = 0;i < 6;++i){ for(int j = money[i];j <= n;++j){ dp[j] =(dp[j]+dp[j-money[i]]); } } System.out.println(dp[n]); } public static void main(String[] args) { long startTime = System.currentTimeMillis(); //指定200元的金额 test2(200); long endTime = System.currentTimeMillis(); System.out.println("执行时间:" + (endTime - startTime) + "ms"); } } 执行结果如下 1 2 3274 执行时间:0ms 分析 这种思路属于算法中的动态规划。也是动态规划的经典题目。很明显,大大优化了思路一的性能问题。

2018/9/17
articleCard.readMore

Quartz学习总结

Quartz学习总结 常规需求 每天早上的闹钟 定时统计数据 spring自带支持定时器的任务实现。其可通过简单配置来实现定时任务。 1 2 3 4 5 6 7 8 9 10 @Component @Configurable @EnableScheduling public class ScheduledTasks{ @Scheduled(cron = "0 */1 * * * * ") public void reportCurrentTimeByCorn(){ System.out.println("Cron北京时间:"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date())); } } 但是当我们的业务需求发生变动,比如使用springboot自带的定时器发布的定时任务,在运行了一段时间后,我们想要修改他的执行时间,但又不能关闭项目,只能动态修改。怎么办呢??? Quartz任务调度框架,就可以解决这种烦恼。让你随意的修改和添加定时任务。 复杂需求 暂停 修改 删除 管理 当你在设计定时任务时,遇到了以上几种业务场景时,那么你就可以考虑使用quartz来解决,那么到底什么才是quartz呢? quartz框架 完全由java开发的开源的任务日程管理系统,即在一个预先确定的时间到达时,负责执行或者通知其他软件组件的系统 springboot集成quartz小例子 我们可以先来看一个springboot中通过quartz实现定时任务的小栗子。 maven文件 注:springboot2.0以后就已经提供了quartz的依赖支持:spring-boot-starter-quartz,此处我们还是使用官方提供的依赖来举例。 1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.3.0</version> </dependency> 创建一个Job 即定义一个任务类,实现Job接口,告诉quartz当任务任务的执行具体内容,创建SimpleJob类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.example.quartz.Job; import org.quartz.*; import java.util.Date; /** * 描述: * * @author zhengql * @date 2018/8/21 14:37 */ public class SimpleJob implements Job { public SimpleJob(){ } @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("开始执行简单任务"+new Date()); } } 接着创建一个最基础的定时任务套路 QuartzDemo类,简单概括为5步,注意看代码注释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.example.quartz.demo; import com.example.quartz.Job.SimpleJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; import static org.quartz.SimpleScheduleBuilder.simpleSchedule; /** * 描述: * * @author zhengql * @date 2018/8/28 09:41 */ public class QuartzDemo { public static void main(String[] args) { try { //获取调度器 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); //启动调度器:等待执行 scheduler.start(); //创建任务详情 JobDetail job = JobBuilder.newJob(SimpleJob.class) .withIdentity("job1", "group1") .build(); //创建触发器:触发事件,触发条件等 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .withSchedule(simpleSchedule() .withIntervalInSeconds(6)//每6秒执行一次 .withRepeatCount(10))//执行10次(实际是11次) .build(); //将任务详情+触发器绑定并交给调度器来管理 scheduler.scheduleJob(job, trigger); } catch (SchedulerException e) { e.printStackTrace(); } } } 运行QuartzDemo,观察控制台,你会发现一个简单的定时任务已经跑起来了! Quartz中的要素:scheduler任务调度、Job任务、JobDetail任务详情、Trigger触发器 Job:任务的逻辑。通过实现Job接口,定义任务的执行内容,简单说就是定义“做什么?” JobDetail:任务的定义,通过newJob()绑定Job类。描述自定义的Job实现类,比如任务的名字。另一方面也是为了防止并发问题,简单说就是定义“谁要做?” Trigger:定时器,配置定时器的名称,配置定时器的类型触发方式等,简单说就是定义“什么时候做?” Scheduler:调度器。接受一组JobDetail+Trigger即可安排一个任务,所有的调度由他控制 quartz的调度思路: 创建一个具体的任务(Job) 配置任务的触发时间等(Trigger) 配置任务的具体内容(JobDetail) 调度器Scheduled根据JobDetail+Trigger安排此任务去执行 用一幅图简单形容一下quartz的原理 触发器种类 刚才的小例子中的触发器是以秒为时间间隔来定时调度,Quartz中有多种不同类型的触发器: SimpleTrigger:用来存储方法只需用执行一次,或者时给定触发事件并重复执行N次并且每次执行延迟一定时间的任务。 CronTirgger:按照日历出发,例如每周五,每月十号十点钟,适合于更复杂的任务 DateIntervalTrigger:指定每天的某个时间段内,以一定的时间间隔执行任务。并且它可以支持指定星期。 CalendarIntervalTrigger:类似于SimpleTrigger,支持的间隔单位有秒,分钟,小时,天,月,年,星期。 quartz持久化方式 Quartz提供两种基本作业存储类型 第一种类型叫做RAMJobStore 最佳的性能,因为内存中数据访问最快 不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失 第二种类型叫做JDBC作业存储 通过调整其quartz.properties属性文件,持久化任务调度信息 使用数据库保存任务调度信息后,即使系统崩溃后重新启动,任务的调度信息将得到恢复 quartz中其他知识点 通过阅读quartz的源码,以及官方的文档发现,quartz的开发者很喜欢用建造者模式。其实这种方式可以使复杂的对象更加清晰,阅读和使用中也更加清楚明了。 本文其实只是quartz的基础知识的学习和使用入门,springboot2.0之后也对quartz进行了封装,关于quartz更加优雅的使用方式和技巧也会在后面的博文中记录。

2018/8/31
articleCard.readMore

SpringBoot2.x集成Redis

Redis介绍 Redis数据库是一个完全开源免费的高性能Key-Value数据库。它支持存储的value类型有五种,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型) Redis非常快,每秒可执行大约110000次的设置(SET)操作,每秒大约可执行81000次的读取/获取(GET)操作。 开发工具 IDEA、Maven、SpringBoot2.0.4、Jdk1.8、Redis3.2.100、PostMan 配置开始 说明 spring 封装了两种不同的对象来进行对redis的各种操作,分别是StringTemplate与redisTemplate。 两者的关系是StringRedisTemplate继承RedisTemplate。 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。 SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。 StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。 RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。 其实通俗的来讲: 当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可。 但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是 更好的选择。 StringTemple Maven配置 1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> yml配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 server: port: 8031 spring: application: name: spirng-boot-redis redis: host: 127.0.0.1 timeout: 3000 password: port: 6379 database: 5 jedis: pool: max-active: 8 # 连接池最大连接数(使用负值表示没有限制) max-idle: 8 # 连接池中的最大空闲连接 max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制) min-idle: 0 # 连接池中的最小空闲连接 Service业务实现 创建StringRedisService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; /** * 描述: * redis业务 * * @author zhengql * @date 2018/8/10 14:29 */ @Service public class StringRedisService { private static final Logger logger = LoggerFactory.getLogger(StringRedisService.class); @Autowired private StringRedisTemplate stringRedisTemplate; public void setString(String key, String value){ logger.info("--------------------->[Redis set start]"); stringRedisTemplate.opsForValue().set(key,value); } public String getString(String key){ logger.info("--------------------->[Redis get start]"); return stringRedisTemplate.opsForValue().get(key); } } 测试 测试代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class StringRedisServiceTest { @Autowired StringRedisService stringRedisService; @Test public void set() { stringRedisService.setString("name","张三"); } @Test public void get(){ System.out.println(stringRedisService.getString("name")); } } 使用Junit测试存入一条数据到redis中,测试效果如下: 可以看到我们已经成功存进去啦,下面测试从redis中去取出刚才存入的数据。 RedisTemple 当我们的数据是复杂的对象类型,那么可以采用RedisTemple 手动配置 首先我们需要手动创建Redis的配置类,来自定义序列化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 /** * 描述: * * @author zhengql * @date 2018/8/10 16:00 */ @Configuration @EnableCaching public class RedisConfiguration extends CachingConfigurerSupport { private static final Logger logger = LoggerFactory.getLogger(RedisConfiguration.class); /** * redis模板,存储关键字是字符串,值jackson2JsonRedisSerializer是序列化后的值 * * @param * @return org.springframework.data.redis.core.RedisTemplate * @author zhengql * @date 2018/8/10 16:03 */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); //使用StringRedisSerializer来序列化和反序列化redis的key值 RedisSerializer redisSerializer = new StringRedisSerializer(); //key redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setHashKeySerializer(redisSerializer); //value redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } } 定义User实体类 注意:这里必须实现序列化接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import java.io.Serializable; /** * 描述: * * @author zhengql * @date 2018/8/10 15:29 */ public class User implements Serializable { private static final long serialVersionUID = -8289770787953160443L; private String userName; private Integer age; public User(String userName, Integer age) { this.userName = userName; this.age = age; } public User() { } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "User{" + "userName='" + userName + '\'' + ", age=" + age + '}'; } } Service方法 RedisTemplate中定义了对5种数据结构操作: redisTemplate.opsForValue() :操作字符串 redisTemplate.opsForHash() :操作hash redisTemplate.opsForList() :操作list redisTemplate.opsForSet() :操作set redisTemplate.opsForZSet() :操作有序set 创建RedisService类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; /** * 描述: * redisTemple 的Service类 * * @author zhengql * @date 2018/8/10 16:22 */ @Service public class RedisService { @Autowired private RedisTemplate<String,Object> redisTemplate; public void setObj(String key,User value) { redisTemplate.opsForValue().set(key,value); } public User getObj(String key) { return (User)redisTemplate.opsForValue().get(key); } } 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * @auther: zhengql * @date: 2018/8/11 10:25 * @description: */ @RunWith(SpringRunner.class) @SpringBootTest public class RedisServiceTest { @Autowired private RedisService redisService; @Test public void setObj() { redisService.setObj("user",new User("小明",22)); User user = redisService.getObj("user"); System.out.println(user.toString()); } @Test public void getObj() { User user = redisService.getObj("user"); System.out.println(user.toString()); } } 执行上述setObj()方法,可以看到我们存入redis中的对象 总结 完成以上配置,我们Springboot集成Redis的基本环境就搭建完成了。redis的功能其实很多,redis可以解决很多关系型数据库所不能解决的问题,它可以实现缓存,可以实现持久化、可以做分布式锁等等,如此强大的redis,需要我们不断的学习和实践理解他的功能和原理。

2018/8/13
articleCard.readMore

SpringBoot2.x集成MongoDB

MongoDB MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是可以应用于各种规模的企业、各个行业以及各类应用程序的开源数据库。基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个高性能,开源,无模式的文档型数据库,是当前NoSql数据库中比较热门的一种。 MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bjson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 传统的关系数据库一般由数据库(database)、表(table)、记录(record)三个层次概念组成,MongoDB是由数据库(database)、集合(collection)、文档对象(document)三个层次组成。MongoDB对于关系型数据库里的表,但是集合中没有列、行和关系概念,这体现了模式自由的特点。 MySqlMongoDB 数据库数据库 表集合 行文档 记录字段 开发环境 IDEA、Maven、SpringBoot2.0.4、Jdk1.8、MongoDB4.0、MongoDB Compass Community、PostMan 配置开始 Maven 相关配置 在pox.xml文件中添加spring-boot-starter-data-mongodb引用 1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> 资源文件yml配置 笔者这里使用yml配置方式,配置时要注意缩进!!!! 1 2 3 4 5 6 7 8 9 10 11 server: port: 8031 spring: application: name: spirng-boot-mongodb data: mongodb: host: localhost #同127.0.0.1 port: 27017 database: test #指定操作的数据库 创建实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import org.springframework.data.annotation.Id; import java.util.Date; /** * 描述:图书实体类 * * @author zhengql * @date 2018/8/9 10:28 */ public class Book { @Id private String id; //价格 private Integer price; //书名 private String name; //简介 private String info; //出版社 private String publish; //创建时间 private Date createTime; //修改时间 private Date updateTime; //Getter、Setter省略.... 创建service类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; /** * 描述: * mongo * * @author zhengql * @date 2018/8/9 10:24 */ @Service public class MongoDbService { private static final Logger logger = LoggerFactory.getLogger(MongoDbService.class); @Autowired private MongoTemplate mongoTemplate; /** * 保存对象 * * @param book * @return */ public String saveObj(Book book) { logger.info("--------------------->[MongoDB save start]"); book.setCreateTime(new Date()); book.setUpdateTime(new Date()); mongoTemplate.save(book); return "添加成功"; } /** * 查询所有 * * @return */ public List<Book> findAll() { logger.info("--------------------->[MongoDB find start]"); return mongoTemplate.findAll(Book.class); } /*** * 根据id查询 * @param id * @return */ public Book getBookById(String id) { logger.info("--------------------->[MongoDB find start]"); Query query = new Query(Criteria.where("_id").is(id)); return mongoTemplate.findOne(query, Book.class); } /** * 根据名称查询 * * @param name * @return */ public Book getBookByName(String name) { logger.info("--------------------->[MongoDB find start]"); Query query = new Query(Criteria.where("name").is(name)); return mongoTemplate.findOne(query, Book.class); } /** * 更新对象 * * @param book * @return */ public String updateBook(Book book) { logger.info("--------------------->[MongoDB update start]"); Query query = new Query(Criteria.where("_id").is(book.getId())); Update update = new Update().set("publish", book.getPublish()) .set("info", book.getInfo()) .set("updateTime", new Date()); //updateFirst 更新查询返回结果集的第一条 mongoTemplate.updateFirst(query, update, Book.class); //updateMulti 更新查询返回结果集的全部 // mongoTemplate.updateMulti(query,update,Book.class); //upsert 更新对象不存在则去添加 // mongoTemplate.upsert(query,update,Book.class); return "success"; } /*** * 删除对象 * @param book * @return */ public String deleteBook(Book book) { logger.info("--------------------->[MongoDB delete start]"); mongoTemplate.remove(book); return "success"; } /** * 根据id删除 * * @param id * @return */ public String deleteBookById(String id) { logger.info("--------------------->[MongoDB delete start]"); //findOne Book book = getBookById(id); //delete deleteBook(book); return "success"; } } controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import com.example.mqdemo.mongo.Book; import com.example.mqdemo.mongo.MongoDbService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /*** * @author zhengql * @date 2018/8/9 10:38 */ @RestController public class BaseController { @Autowired private MongoDbService mongoDbService; @PostMapping("/mongo/save") public String saveObj(@RequestBody Book book) {return mongoDbService.saveObj(book);} @GetMapping("/mongo/findAll") public List<Book> findAll() {return mongoDbService.findAll();} @GetMapping("/mongo/findOne") public Book findOne(@RequestParam String id) {return mongoDbService.getBookById(id);} @GetMapping("/mongo/findOneByName") public Book findOneByName(@RequestParam String name) {return mongoDbService.getBookByName(name);} @PostMapping("/mongo/update") public String update(@RequestBody Book book) {return mongoDbService.updateBook(book);} @PostMapping("/mongo/delOne") public String delOne(@RequestBody Book book) {return mongoDbService.deleteBook(book);} @GetMapping("/mongo/delById") public String delById(@RequestParam String id) {return mongoDbService.deleteBookById(id);} } 运行测试 启动项目,打开postman开始接口调试,可以看到成功添加book对象。返回添加成功。 打开MongoDB Compass Community,连接本地MongoDB,可以看到刚才添加的信息。 其他接口这里就不一一测试了。 优化使用 完成以上配置,我们springBoot集成MongoDB环境基本已经搭建好了。 但是在使用中会发现一个问题,假如要对数据库操作多个对象,那岂不是每一个对象Service都需要写一套增删查改的方法。 为了解决这一问题我们可以封装一个通用的操作类来提高效率。 创建MongoDbDao类如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; /** * 描述: * mongoDB基础方法封装 * * @author zhengql * @date 2018/8/9 14:01 */ public abstract class MongoDbDao<T> { protected Logger logger = LoggerFactory.getLogger(MongoDbDao.class); /** * 反射获取泛型类型 * * @return */ protected abstract Class<T> getEntityClass(); @Autowired private MongoTemplate mongoTemplate; /*** * 保存一个对象 * @param t */ public void save(T t) { logger.info("-------------->MongoDB save start"); this.mongoTemplate.save(t); } /*** * 根据id从几何中查询对象 * @param id * @return */ public T queryById(Integer id) { Query query = new Query(Criteria.where("_id").is(id)); logger.info("-------------->MongoDB find start"); return this.mongoTemplate.findOne(query, this.getEntityClass()); } /** * 根据条件查询集合 * * @param object * @return */ public List<T> queryList(T object) { Query query = getQueryByObject(object); logger.info("-------------->MongoDB find start"); return mongoTemplate.find(query, this.getEntityClass()); } /** * 根据条件查询只返回一个文档 * * @param object * @return */ public T queryOne(T object) { Query query = getQueryByObject(object); logger.info("-------------->MongoDB find start"); return mongoTemplate.findOne(query, this.getEntityClass()); } /*** * 根据条件分页查询 * @param object * @param start 查询起始值 * @param size 查询大小 * @return */ public List<T> getPage(T object, int start, int size) { Query query = getQueryByObject(object); query.skip(start); query.limit(size); logger.info("-------------->MongoDB queryPage start"); return this.mongoTemplate.find(query, this.getEntityClass()); } /*** * 根据条件查询库中符合条件的记录数量 * @param object * @return */ public Long getCount(T object) { Query query = getQueryByObject(object); logger.info("-------------->MongoDB Count start"); return this.mongoTemplate.count(query, this.getEntityClass()); } /*** * 删除对象 * @param t * @return */ public int delete(T t) { logger.info("-------------->MongoDB delete start"); return (int) this.mongoTemplate.remove(t).getDeletedCount(); } /** * 根据id删除 * * @param id */ public void deleteById(Integer id) { Criteria criteria = Criteria.where("_id").is(id); if (null != criteria) { Query query = new Query(criteria); T obj = this.mongoTemplate.findOne(query, this.getEntityClass()); logger.info("-------------->MongoDB deleteById start"); if (obj != null) { this.delete(obj); } } } /*MongoDB中更新操作分为三种 * 1:updateFirst 修改第一条 * 2:updateMulti 修改所有匹配的记录 * 3:upsert 修改时如果不存在则进行添加操作 * */ /** * 修改匹配到的第一条记录 * @param srcObj * @param targetObj */ public void updateFirst(T srcObj, T targetObj){ Query query = getQueryByObject(srcObj); Update update = getUpdateByObject(targetObj); logger.info("-------------->MongoDB updateFirst start"); this.mongoTemplate.updateFirst(query,update,this.getEntityClass()); } /*** * 修改匹配到的所有记录 * @param srcObj * @param targetObj */ public void updateMulti(T srcObj, T targetObj){ Query query = getQueryByObject(srcObj); Update update = getUpdateByObject(targetObj); logger.info("-------------->MongoDB updateFirst start"); this.mongoTemplate.updateMulti(query,update,this.getEntityClass()); } /*** * 修改匹配到的记录,若不存在该记录则进行添加 * @param srcObj * @param targetObj */ public void updateInsert(T srcObj, T targetObj){ Query query = getQueryByObject(srcObj); Update update = getUpdateByObject(targetObj); logger.info("-------------->MongoDB updateInsert start"); this.mongoTemplate.upsert(query,update,this.getEntityClass()); } /** * 将查询条件对象转换为query * * @param object * @return * @author Jason */ private Query getQueryByObject(T object) { Query query = new Query(); String[] fileds = getFiledName(object); Criteria criteria = new Criteria(); for (int i = 0; i < fileds.length; i++) { String filedName = (String) fileds[i]; Object filedValue = getFieldValueByName(filedName, object); if (filedValue != null) { criteria.and(filedName).is(filedValue); } } query.addCriteria(criteria); return query; } /** * 将查询条件对象转换为update * * @param object * @return * @author Jason */ private Update getUpdateByObject(T object) { Update update = new Update(); String[] fileds = getFiledName(object); for (int i = 0; i < fileds.length; i++) { String filedName = (String) fileds[i]; Object filedValue =getFieldValueByName(filedName, object); if (filedValue != null) { update.set(filedName, filedValue); } } return update; } /*** * 获取对象属性返回字符串数组 * @param o * @return */ private static String[] getFiledName(Object o) { Field[] fields = o.getClass().getDeclaredFields(); String[] fieldNames = new String[fields.length]; for (int i = 0; i < fields.length; ++i) { fieldNames[i] = fields[i].getName(); } return fieldNames; } /*** * 根据属性获取对象属性值 * @param fieldName * @param o * @return */ private static Object getFieldValueByName(String fieldName, Object o) { try { String e = fieldName.substring(0, 1).toUpperCase(); String getter = "get" + e + fieldName.substring(1); Method method = o.getClass().getMethod(getter, new Class[0]); return method.invoke(o, new Object[0]); } catch (Exception var6) { return null; } } } 我们将mongoDB常用的CURD操作封装为通用的父类,然后在不同的业务场景下继承该类,通过泛型和反射获取到正在操作的实体类。 比如我们可以将之前的Book实体类的CURD类进行改造 创建BookMongoDbDao类继承MongoDbDao 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.springframework.stereotype.Repository; /** * 描述: * * @author zhengql * @date 2018/8/9 20:46 */ @Repository public class BookMongoDbDao extends MongoDbDao<Book> { @Override protected Class<Book> getEntityClass() { return Book.class; } } 接下来我们可以改造Book的Service类 原始版本: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Service public class BookMongoDbService { private static final Logger logger = LoggerFactory.getLogger(BookMongoDbService.class); @Autowired private MongoTemplate mongoTemplate; /** * 保存对象 * * @param book * @return */ public String saveObj(Book book) { logger.info("--------------------->[MongoDB save start]"); book.setCreateTime(new Date()); book.setUpdateTime(new Date()); mongoTemplate.save(book); return "添加成功"; } //其他操作方法...... } 改造后 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Service public class BookMongoDbService { private static final Logger logger = LoggerFactory.getLogger(BookMongoDbService.class); @Autowired private MongoTemplate mongoTemplate; @Autowired private BookMongoDbDao bookMongoDbDao; /** * 保存对象 * * @param book * @return */ public String saveObj(Book book) { logger.info("--------------------->[MongoDB save start]"); book.setCreateTime(new Date()); book.setUpdateTime(new Date()); mongoTemplate.save(book); return "添加成功"; } public String saveObj2(Book book) { book.setCreateTime(new Date()); book.setUpdateTime(new Date()); bookMongoDbDao.save(book); return "添加成功"; } } 改造后的saveObj2方法的效果与以前的一致,但是大大的提高了开发效率。不需要重复的编写CURD的方法。 总结 通过以上的配置已经完成springboot与mongoDB集成环境的初步搭建,当然了MongoDB在springboot中的使用不仅于此,还有更多的功能和更优雅的使用方式等待着我们去发掘。

2018/8/9
articleCard.readMore

[SpringCloud学习] - 浅谈微服务架构

前言 目前越来越多的话题都围绕着微服务,许多公司也在使用微服务架构。笔者也刚刚接触微服务不久,也算是微服务架构的初学者,谨以本文来记录学习过程中对微服务架构的一些理解。好啦,废话不多说,我们往下看。 微服务是什么? 微服务,英文名MicroService,他是一种架构风格一种架构设计模式,通常表现为一个庞大而复杂的应用其背后是由数个职责分明的服务组成,这些服务他们各自分工明确,可以独立部署同时也可以根据需求进行扩展,各个服务之间松耦合并且可相互通信。 结合我们生活来说,一个公司内部组织架构也算是一种微服务的表现,公司内部按不同职能划分了许多部门,人事部门、财务部门、开发部门、测试部门、运维部门等等这些部门都是一个个的微服务,各个部门之间相互独立办公同时也相互协同办公。这些所有的部门组成了公司的整体。 微服务的概念出自于马丁·福勒(Martin fowler),他对微服务的定义如下: 微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相协作(通常是基于 HTTP 协议的 RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,对具体的服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建 。(摘自王磊先生的《微服务架构与实践》) 为什么要用微服务? 微服务从最初的无人问津,到现在大红大紫,被大家广泛使用。那么问题来了,为什么要用微服务架构?为什么就不用以前的架构了?我们先来了解一下传统的架构方式。 单体架构 应用程序作为单体进行打包和部署,称之为单体应用,例如基于SpringMVC+Mybatis+Spring开发的许多Java项目最终被打包成一个war格式的文件部署在Tomcat或者Jetty服务器上。而这种单体应用的架构理论就称之为单体架构。 单体应用的局限性 一个单体应用他可能内部也区分了业务逻辑模块,但最终都打包为一个单体,随着时间的推移,单体式应用的不足就暴露出来了。 复杂度高难以理解 随着时间推移,业务需求的升级,代码量越来越大,项目内部逻辑变得越来越复杂,各个模块之间区别模糊,逻辑混乱,开发人员对于代码的理解难度加大。 代码维护难度升级 时间线拉长后,一个项目可能会有许多程序员接手,代码复杂度增大之后,前人留下的坑后人来填,刚上手的程序员可能会面对一个又一个问题。 部署速度之间变慢 单体架构的应用内部业务模块众多,每次功能的变更都需要重新部署整个应用,项目的启动时间可能从最初的一分钟演变为最终的十分钟,这种情况乱其实很多。 可靠性稳定性直线下降 由于整个项目是部署在一个实例中,一个小小的bug可能就会导致整个应用的崩溃。 技术创新难以实现 受项目本身限制,团队成员必须使用一种框架和语言,模块无法明确清晰的拆分,升级框架和使用新技术的风险和成本很高。 资源需求冲突难以解决 不同的业务对物理资源的需求是不同,比如处理图片音乐视频的模块是CPU密集型的模块,而像订单、日志等是属于IO密集型模块,当需要提升IO密集模块性能时,但由于我们的应用是单体架构,所有模块都在一个架构下,所以我们想要对某一模块进行升级扩展不得不考虑其他模块。随着需求进一步变更,资源需求冲突会成为整个应用最大的痛点。 单体应用在面对这写日益严峻的问题时,微服务架构则从根本上杜绝了这些隐患的产生。 微服务能用在哪? 微服务架构往往用于解决复杂问题,他适合将复杂庞大的问题拆分为相互独立又相互联系的小个体。相比于单体架构,微服务架构是构建业务复杂度高,规模大,需要长期持续迭代这一类应用时更好的选择。 现在已经有很多公司采用微服务架构来解决单体式架构可能会造成的隐患,笔者所在的团队就选用了基于SpringBoot的SpringCloud,如此一来能够大大提高开发效率的同时降低项目的维护难度,将项目分解为多个微服务组件,各个相对独立的同时又相互协作。不用再构建并且维护一个臃肿又令人头疼的单体应用。 主流的微服务框架介绍 Spring Boot Spring Cloud Dubbo Dropwizard Akka Vert.x、Lagom、ReactiveX、Spring 5 微服务的优点 说了那么多,那在使用微服务之后到底有哪些优势呢? 应用复杂度降低,代码可读性高,易于开发。 由于微服务单个模块就相当于一个项目,开发这个模块我们就只需关心这个模块的逻辑即可,代码量和逻辑复杂度都会降低,从而易于开发和维护。 容错率更高 由于各服务相互独立,当某一模块出现bug,只是针对与某一个服务组件出现故障而已,不会影响其他模块的使用,同时开发人员可以快速的解决问题。 技术选型不受影响 各个服务独立,完全可以使用不同的语言来实现其内部业务。 资源冲突问题顺利解决 在单体应用中存在的资源冲突问题,在微服务中,我们完全可以根据服务本身的特性对性能进行升级。 微服务的缺点 任何架构都是在实际开发中慢慢演化出来的,是为更好地适应开发者们的需求。所以微服务也存在着自身的不足之处。 对开发者要求更高 各个服务根据不同业务,使用到的语言、数据库、技术都存在差异,这对开发者本身就是一个挑战。 运维难度提升 微服务架构有许多服务组件,而部署一个微服务应用也是十分复杂的过程,单体架构中只需要维护一个应用的正常运行,但是在微服务中,但是一种服务可就就有很多实例,可能需要维护数十个服务,所以自动化部署也是应用成功运行的基础。 微服务自身的复杂性 为服务应用本身就是一个分布式系统,从整体上来说它也十分复杂。 总结 没有哪一个好的架构是被设计出来的,也没有哪一个架构可以解决所有的问题,每一个好的架构都是在不断适应业务需求的过程中不断被演化出来的。所以每种架构方式都有各自的优势和缺陷,没有最好,只有最合适! 参考文章 如何通俗易懂的解释微服务:http://www.cnblogs.com/hang520/p/9239071.html 微服务从涉及到部署:https://github.com/DocsHome/microservices

2018/7/26
articleCard.readMore

基于hexo和coding免费搭建个人博客网站

前言 现如今有很多人每天在看博客,也有很多人在各大平台发表博客,今天我们通过hexo博客框架来自己动手搭建一个免费并且完全DIY的个人博客。Hexo的优势在于方便快捷并且提供的主题丰富,本文是笔者在搭建博客期间的一些经验分享。还是先来看一下搭建完成后的效果吧—我的博客。 Hexo介绍 Hexo是由台湾在校大学生tommy351(twitter名)设计的一款基于node的静态网站生成器,它使用MarkDown语法解析文章,能够几秒内生成静态网页。使用hexo搭建博客你会发现页面浏览十分流畅,这就是静态网页部署的优势所在。 正文 搭建前提 在开始搭建博客网站之前需要以下准备: 安装 Node.js 安装 Git 注册 Coding 账号 下载并安装Node 进入Node下载页面,此处笔者下载当前稳定版本8.11.3,安装过程比较简单此处不再赘述。安装完成后在命令提示符下使用以下命令检测是否安装成功,如果显示安装的版本号即表示安装成功。 1 2 $ npm -v v8.11.3 下载并安装Git 进入Git下载页面,选择对应的系统版本和git版本号进行下载,安装过程此处不再赘述。安装完成后在桌面右键可以看到git bash或者开始菜单中也可以找到。本文后续大部分操作也都在git bash中完成。 安装完成Git后,需要对Git进行用户名和密码的配置。 1 2 $ git config --global user.name "Your Name" $ git config --global user.email "email@example.com" 因为Git是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和Email地址。你也许会担心,如果有人故意冒充别人怎么办?这个不必担心,首先我们相信大家都是善良无知的群众,其次,真的有冒充的也是有办法可查的。----廖雪峰Git教程 Hexo安装 首先我们先新建一个文件夹作为个人博客的文件目录,此处笔者创建blog文件夹。进入该文件夹后打开Git bash,开始安装hexo 1 2 3 $ npm install -g hexo-cli #安装Hexo脚手架 $ hexo init #Hexo自动在当前文件夹下下载搭建网站所需的所有文件 $ npm install #安装依赖包 此时在blog文件夹中我们可以看到已经生成了搭建网站所需要的所有文件了,其目录如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 │ .gitignore │ package.json │ _config.yml #博客网站的配置信息,核心配置文件 │ ├─node_modules │ ├─scaffolds #模版文件夹。当您新建文章时,Hexo 会根据 scaffold 来建立文件。 │ draft.md │ page.md │ post.md │ ├─source #资源文件夹是存放用户资源的地方,我们新建的md格式的博客就在此处 │ └─_posts │ hello-world.md #hexo为我们创建的hello-world样例文件 │ └─themes #主题文件夹。Hexo会根据主题来生成静态页面。 └─landscape #默认使用的主题 现在一个简单的个人博客已经创建完成,我们可以使用hexo generate命令来生成静态网页。然后通过hexo server命令启动本地服务器来预览一下hexo为我们提供的最原始博客页面。当然这两个命令也可以简写如下。 1 2 hexo g hexo s 浏览器访问http://localhost:4000/ ,此时就可以看见一个简单的博客页面。该页面正是source目录下hello-world.md文件生成的静态网页。 部署博客到coding Coding与Github相似,都是基于Git的代码托管平台,GitHub大家应该都很熟悉,相对与Coding来说,GitHub面向英语系国家程序员,Coding面向中国程序猿,而且Coding的私有库可免费使用这是最大的区别。两者的用法和操作其实大同小异。笔者也是第一次使用Coding,所以大家共同学习。本文就使用了Coding来作为博客项目的托管平台。 新建仓库 1.打开coding主页,创建新项目 新建一个名为yourname.coding.me的项目,其中这里的yourname最好是coding注册时的username,等项目部署成功后它就是你博客的访问地址。如下所示coding可以创建私有仓库,GitHub中是要收费的。 2.开启Pages服务,开启后我们就可以通过 用户名+网站后缀来访问博客,而且还可以绑定域名通过固定域名来访问。进入项目->代码->Pages服务,具体操作如下: 连接到Coding 在管理Git项目时,无论是GitHub还是Coding我们可以通过SSH、HTTPS两种方式来clone或者push代码,当使用HTTPS的方式来管理代码,每次操作时都需要验证用户名和密码;而使用SSH方式Push代码之前需要配置个人的SSH key,这样就可以省掉繁琐的验证步骤。 当然,我们也可以选择通过HTTPS方式来进行项目的管理,那么请跳过SSH key的配置步骤,直接阅读项目部署步骤的内容。 配置SSH key 以下命令皆在Git bash下执行 1.检查你的电脑上是否已经生成了SSH Key 1 2 $ cd ~/.ssh $ ls 如果该目录下存在id_rsa.pub或id_dsa.pub文件,那么直接进行第三步 2.创建一个SSH Key 执行创建命令 1 ssh-keygen -t rsa -C "username" #username为git config --global user.name,即git中所配置的用户名 代码参数含义: 参数描述默认值 -t指定密钥类型,默认是rsa,可以省略。rsa -C设置注释文字,比如用户名。 -f-f指定密钥文件存储文件名。id_rsa 以上命令省略了-f参数,因此,运行上面那条命令后会让你输入一个文件名,用于保存刚才生成的SSH key代码,如: 1 2 Generating public/private rsa key pair. Enter file in which to save the key (/c/Users/you/.ssh/id_rsa): [Press enter] 此处我们直接按下回车使用默认文件名创建,那么就会生成id_rsa和id_rsa.pub两个秘钥文件。 接着又会提示你输入两次密码(该密码是你push文件的时候要输入的密码,而不是Coding管理者的密码),当然,你也可以不输入密码,直接按回车。那么push的时候就不需要输入密码,直接提交到Git服务器上了,如: 1 2 Enter passphrase (empty for no passphrase): Enter same passphrase again: 接下来,就会显示如下一些提示,如: 1 2 Your identification has been saved in /c/Users/you/.ssh/id_rsa. Your public key has been saved in /c/Users/you/.ssh/id_rsa.pub. 3.在Coding中配置SSH Key 在刚才生成的文件夹中找到id_rsa.pub,用记事本打开后全选复制。 打开浏览器,进入我们Coding的工作台,账户->SSH公钥,按如下操作,将生成的SSH key添加。 部署发布项目 在我们创建的博客项目工作空间中找到_config.yml文件,打开后在文件的最后方,可以看到如下配置: 1 2 deploy: type: 对该配置进行修改如下(注意缩进): 1 2 3 4 deploy: type: git repository: git@git.coding.net:yourname/yourname.coding.me.git #发布到Coding(SSH方式) branch: master 该配置指定了Coding仓库的地址,并通过SSH方式进行连接。同样我们也可以通过HTTPS方式。repository写法如下: repository: https://git.coding.net/yourname/yourname.coding.me.git 保存后关闭,在项目当前目录打开Git Bash ,因为项目的部署会用到hexo-deployer-git,所以先安装该插件: 1 $ npm install hexo-deployer-git --save 安装完成后,开始项目的部署操作,每次的部署操作可以分三步来,分别如下: 1 2 3 $ hexo clean #清除缓存文件和已生成的静态文件。更换主题后一定要用 $ hexo g #完整命令为hexo generate ,生成静态文件。 $ hexo d #完整命令为hexo deploy ,部署网站。 如果你使用的是SSH方式的话,应该可以看到commit和push成功的记录,说明已经部署成功。如果使用的是HTTPS方式,会弹出输入用户名密码的输入框,键入你的coding用户名密码即可。 此时打开浏览器访问 https://yourname.coding.me 主题更换 我们刚才搭建的博客使用的是默认的landscape主题。我们也可以在hexo提供的主题页面中选择自己喜欢的进行配置。在博客文件夹下的themes文件夹,该目录下存放的就是所有的主题资源。此处笔者以Next主题的配置为例。 首先第一步下载主题,下载命令格式为git clone {主题链接} themes/{主题名称}。 1 git clone https://github.com/iissnan/hexo-theme-next themes/next 下载完成后,在themes文件夹下就可以看到Next主题的相关文件。 2. 下载完成,接着就是启用主题。 在博客文件根目录中找到_config.yml文件,并用记事本打开,对theme属性进行修改 1 theme: next #原本为lanscape主题,此处直接修改为我们下载的主题名 完成以上配置后,我们既定要记得清楚缓存并重新部署项目,操作如下: 1 2 3 $ hexo clean #清除缓存 $ hexo g #生成静态文件 $ hexo d #部署博客网站 此时,我们的博客主题已经修改成功。 总结 搭建个人博客网站,并且坚持在网站上更发表自己的一些知识积累是很有意义的,这个过程不但让你融会贯通而且完全转化为自己的理解。笔者在搭建过程中也遇到许多坑,多亏有网上的众多大牛前辈。先回过头来看其实还是很有成就感的,当然当前进度的博客网站只能算是刚刚搭建好,hexo本来就是完全DIY的,主题和页面都需要更多的优化,最重要的是内容不能停,既然搭建好了,就坚持写下去吧。 大家如果有问题或者笔者博客内容有误可以留言交流,共同学习。 参考的文章 hexo开发手册可以初步了解hexo: https://hexo.io/zh-cn/docs/ 博客搭建的基础教程写得非常详细: https://www.jianshu.com/p/eb002d35436c

2018/7/24
articleCard.readMore

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick Start Create a new post 1 $ hexo new "My New Post" More info: Writing Run server 1 $ hexo server More info: Server Generate static files 1 $ hexo generate More info: Generating Deploy to remote sites 1 $ hexo deploy 按时大苏打实打实大苏打啊实打实打算 More info: Deployment RESTful API概述 RESTful API是什么 RESTful是Representational State Transfer的缩写,代表着表征状态转移。REST拥有一组架构约束条件和原则,只要符合这一套约束原则的架构,就是RESTful架构。 需要注意的是,REST并没有提供新的组件、技术,也并不是专门为HTTP提供规范,而是通过约束和原则去合理使用Web的现有特征和能力(是的,REST受到Web现有特征的影响还是比较深的)。RESTful API 是一种围绕 资源(resource)展开的无状态传输的API设计方案。所有的HTTP Action,都应该是在相应resource上可以被操作和处理的,而API就是对资源的管理操作,而这个具体操作是由 HTTP Action 指定的。 RESTful API在功能上更像是隔离层,要访问服务器资源,就必须找到API入口。如果这个入口的规则遵循REST风格,那就是RESTful设计框架。 RESTful API产生的意义 随着移动互联网的发展,各类Client层出不穷,RESTful可以通过一套统一的接口为Web、ios和Android提供服务。对于广大平台来说,例如微信公共平台,它们不需要有显示的前段,只需要一套提供服务的接口,RESTful正好是最佳选择。 规定的资源格式 资源的标识URL 资源是一个数据单元,这个单元可大可小,根据业务规模自主定制。要准确识别一个资源,需要有一个唯一标识,在Web中这个唯一标识就是URL(Uniform Resource Identifier)。 URL的设计应该具有自适性、可寻址、直观性的原则。用/来表示层级,用_或-来分割单词,用?来过滤资源。 HTTP协议语义支持 GET:从服务器取出资源或资源列表 POST:在服务器新建一个资源 PUT:客户端提供数据,以整体的方式更新服务器资源 PATCH:只更新服务器一个资源的一个属性 DELETE:从服务器删除资源 HEAD:从服务器获取报头信息(不是资源) OPTIONS:获取客户端能对资源做什么操作的信息 除了POST不是幂等的,其他几个都是幂等的。 HTTP的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性的一个实例:在网速不够快的条件下,客户端发送第一个请求后不能立即得到响应,由于不能确定是否请求是否被成功提交,所以它有可能会再次发送另一个相同的请求,幂等性决定了第二个请求是否有效。幂等情况下,第一次请求成功实现了事务操作,第二次请求就一定不能再次操作事务。 媒体类型 客户端与服务端进行交互式,需要规定双方能够接受的媒体表现形式。常见的媒体格式类型有: application/json:JSON数据格式 application/xhtml+xml:XHTML数据格式 application/xml:XML数据格式 application/atom+xml:ATOM XML聚合格式 在设计RESTful API的时候,要规定端端之间具有统一的数据传输格式,目前JSON数据格式使用范围比较广。 好的API是什么样的 举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则他的路径应该设计成以下的样子。 1 2 3 + https://api.example.com/v1/zoos + https://api.example.com/v1/animals + https://api.example.com/v1/employees 博客魔改记录 归档侧边栏显示查看更多,要修改blog/node_modules/hexo/lib/plugins/helper/list_archives.js内容 新增友情链接侧边栏 page页显示字数、阅读量、等等 添加page页类型others 导航渐变色 首页不显示全图 二级导航栏 文章h1\h2\h3。。。显示优化 自由调整top_img的布局 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if (style === 'list') { result += `<ul class="${className}-list">`; for (i = 0, len = data.length; i < 6; i++) { item = data[i]; result += `<li class="${className}-list-item">`; result += `<a class="${className}-list-link" href="${link(item)}">`; result += transform ? transform(item.name) : item.name; result += '</a>'; if (showCount) { result += `<span class="${className}-list-count">${item.count}</span>`; } result += '</li>'; } result += `<li class="${className}-list-item">`; result += `<a style="text-align: center;font-size: 13px" class="${className}-list-link" href="${link()}">`; result += '查看更多'; result += '</a>'; result += '</li>'; result += '</ul>';

2018/7/23
articleCard.readMore