注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

被裁后的一天

距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生...
继续阅读 »

距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生不愉快的事情。我写这篇文章不是为了缅怀过去或者反思过去,而是记录我的一天,余下的内容没有华丽的词藻,也没有令人深思的道理,只是流水账。


今天我 7 点 20 就醒了,醒来的前一刻还在做梦,我梦到自己睡不着觉,可想而知那不是一个美梦。醒来的那一瞬间,我甚至没有分辨出是现实还是梦境,头昏昏沉沉的赖了一会儿床,在赖床期间我老公起床了,他洗漱结束的时候,我用手机放了一首歌,他走到床边对我巴拉巴拉说了些话,说的都是些大道理,大概是选择呀未来呀思考呀这一套,如果让我将他的话背下来,可真是在难为我。其实他一大早就讲大道理,我特别不想听,但我觉得他的话有些道理,于是赶紧起了床,洗漱完去跑步。


今天温度不高,早上很适合跑步,估摸着跑了 15 分钟,没有记录跑了多少公里,我打算明天记录一下跑步的公里数。跑完步给手机充了一会儿就出门吃早餐,需要坐两站地铁才能到那家包子铺,我住的地方距地铁站大概 1.5 公里,这段路可以坐小区的摆渡车,但我选择了走路。


那家包子铺叫什么名儿?不记得了,更准确的说,我从来没想过要记住它的名字。我买了两个豇豆包,一个卤鸡蛋还有一杯豆浆。为什么要买这些食物呢?不是没有缘由。昨天我只买了两个豇豆包和一杯豆浆,没有吃饱,于是今天加了一个卤鸡蛋。今天买的这些食物,还是没让我吃饱,明天我要再加一个豇豆包。


为什么要买豇豆包呢?因为我喜欢吃酸豇豆,但是这包子里包的不是酸豇豆,而是新鲜的豇豆,所以第一次吃它的时候它与我的预期不符合,尝了之后又发现新鲜的豇豆也好吃,现在我每次都买豇豆包。今天我看见豇豆包里包的不是豇豆,而是四季豆,四季豆也挺好吃。


这家包子店里能喝的食物除了有豆浆还有稀饭,它的稀饭不是很稀,我担心豇豆包就着稀饭吃不好下咽,于是买了豆浆。


吃了早餐我就回家了,到家的第一件事是联系物业师傅来疏通地漏,还让他们处理电动晾衣架不能升降的问题。关于电动晾衣架,我今天学到了一个新知识,有些人可能会认为那是小常识,不值一提,但我还是要写下来——电池放在遥控器里久了会腐蚀铜板,这将导致遥控器通不了电,遥控器就指挥不了晾衣架,我家的晾衣机不能升降就是这个原因的。


今天我写了两篇技术文章去参加掘金推出的金石计划征文活动,这可以瓜分奖金,这是我第 5 次参加金石计划,每一次都瓜分到了最高奖金,这次我瓜分不到最高奖金了,因为写不出 6 篇原创技术文章。


等物业师傅处理好地漏和晾衣架,发布了文章,我就出发去健身房游泳,健身房离我家有 4.8 公里左右,出发前我在想是开车还是走路,最终选了走路。


今后要做什么?在路上我不由得思考。


今年是我工作的第 6 年,6 年来我断断续续的有考研的想法。为什么想考研呢?在路上我想出了一个原因 —— 源自虚荣心。走着走着想上厕所,路过一家花店,进店问老板附近是否有厕所,他说马路对面有,到了马路对面又向烟酒铺的老板询问厕所的具体位置,如愿的上了厕所,一身轻松。


上完厕所路过一家包子铺,肚子饿了,于是进店买了 2 个包子,1 个鸡蛋还有 1 碗粥,其实我想喝带丝汤,但是卖完了,所以没喝成。


吃完午餐继续往健身房走。哦,我想起在包子铺吃午餐的时候看到了一个皮肤特别好的女生。走呀走呀,终于到了健身房,到那儿之后我没有立即去游泳池,而是去休闲区按摩,在按摩椅上躺着的时候,开始思考我是否想继续当程序员,如果当程序员,想做那方面的业务呢?


我喜欢写作。2021 年期间我写了一部小说,超过 10 万个字,2022 年写我的第一本技术书,上个月才交稿,出版社编辑说稿子问题不大,昨天我根据编辑老师的反馈做了修改,并补充了前言,现在稿件已进入 3 审。我还有一个微信公众号,每个月至少发一篇技术文章。


如果继续当程序员,我想做内容创作类的产品。


按摩完就去游泳,游泳池里只有两个人,除了我还有一个小男孩,他挺可爱的。游完 3 圈我离开游泳池去了桑拿室,那儿只有我一个人,当时我突然想到我还想当程序员,不禁眼睛里有了点泪花。


我很爱哭,被裁后我一个人在家哭了好几次。哭,不是因为被裁,因为以前没被裁的时候,我一个人在家也有哭的时候。桑拿室温度很高,泪花流出来就蒸发了。蒸完桑拿洗完澡打算回家,可是外面在下雨,于是又到按摩椅上躺了一会儿,寻思着等雨停了再回家。第二次按完摩,雨没有停的迹象,我决定淋着雨到外面去打车,16 点到的家,在家吃了一包薯片,看了一会莫言的小说《生死疲劳》,17 点的时候到床上睡觉,18 点的时候才醒,然后到小区门口买了做晚餐的蔬菜,今天晚上煮了面。


WechatIMG128.jpg


这篇文章写了 2 个小时。最后我想说今天还有一件想做的事情没有做成——开车到兴隆湖吹风。


作者:何遇er
来源:juejin.cn/post/7270831230069243961
收起阅读 »

程序员能有什么好出路?

我自己耶 从业10年了,经常在娱文中看到这种的文章,我怀疑是精准推送!! 30岁以上的程序员该何去何从? - 知乎 30岁: 程序员心中永远的痛? 过了30岁,程序员该怎么办? - 阿里云开发者社区 30岁转行程序员晚了吗?分享30岁转行的经历 - Segm...
继续阅读 »

我自己耶


从业10年了,经常在娱文中看到这种的文章,我怀疑是精准推送!!



  1. 30岁以上的程序员该何去何从? - 知乎

  2. 30岁: 程序员心中永远的痛?

  3. 过了30岁,程序员该怎么办? - 阿里云开发者社区

  4. 30岁转行程序员晚了吗?分享30岁转行的经历 - SegmentFault

  5. 30岁后“大龄程序员"应该何去何从? - 脉脉

  6. 程序员:伤不起的三十岁 - 菜鸟教程

  7. 程序员迷茫:30岁以上的“大龄程码农”出路在哪?java码 ... - 掘金

  8. 30岁老程序员迷茫| Laravel - LearnKu


关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。


如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的描述出来了:


30岁了,没啥竞争力,未来何去何从?

出路耶


我从事这个行业,其实是个人挺喜欢编程的,觉得编程是一件挺舒心的事情,所以没有考虑过换行。周围其实有一些同事,离开了这个行当,有一些赚了更多的钱,也有一些日子过的更不舒心,这里不予置评。


我简单的叙述一些可能的出路,这些出路没什么对错的区别,只是在我们人生抉择中,希望你能看到更多的选项。


技术深造


如果你在技术上有优势,这是一条可以走通的路子,未来的方向大致是“架构师”、“技术顾问”等等。这需要你有一些大型项目的经验,所以一些在大型公司就业的程序员,天然的拥有更多的机会。


通常技术深造主要是两部分:



  1. 技术视野,你需要一定的知识广度,对常用技术有深刻的理解,对部分不常用技术也要熟悉。

  2. 技术能力,有的时候,亲自动手能力、解决问题能力会很重要。


项目管理


很多程序员转行做了项目管理,其实在我们的日常工作中,项目管理一直伴随着我们,时长日久,我们对项目管理会变的更熟悉一些。这也造成了一些错觉,让我们觉得项目管理没那么难,“我去我也行”。


但是,项目管理从来不是一项普通的工作,相对于程序员,项目管理人员面临的环境会更加复杂。



  1. 面对客户。有时候,会遇见一些喜欢刁难我们的客户的。

  2. 面对团队。团队也可能不和谐。

  3. 计划乱了、工期排期、风险控制、质量管理、干系人管理等等专业知识。


自由职业


依赖于自己过硬的技术,可以承接一些外包的项目,成为一名自由的外包人员。



  1. 你的人际关系会很重要。周围有一些能打单的朋友,会让你工作的很舒服。

  2. 把事情做好,赢得信赖。

  3. 来自第三方平台的外包项目还是比较坑的,尽量做熟人生意。


跑单


当然,你在行业内可能会认识不少的朋友,他们的手里可能有些业务需要外包人员进行开发,那么拿下这些合同,找到自己朋友里面有时间做私活的人,然后我完成它。



  1. 你的人际关系更为重要。通常,这会给你带来财富。

  2. 做好自己的品牌,赢得认可,那么就有赢得钞票的机会。


插件独立开发者


一个人开发一个应用,然后上架,成功率是很低的。所以依托于平台,做一些平台内的插件,然后依托于平台推广,那么成功的几率会大一些。



  1. 你的技术能力很重要,毕竟没有专门的测试人员进行测试。

  2. 你选择的平台很重要,比如跨境电商、钉钉、微信、谷歌浏览器等等。

  3. 更加重要的是,你要对这个方向感兴趣。


独立开发者


如果你财富自由了,又喜欢编程,可以成为一名伟大的独立开发者,你脑海中的任何想法,都可以通过双手变为现实。



  1. 因为热爱,所以你会有更多的可能。

  2. 能力足够,可以参与开源的基金会,参与一些开源项目。

  3. 如果财富没自由,那也不影响我们在闲暇时间里追逐我们的梦想。


团购


IT行业是一个挺特殊的团体,他们的某些消费习惯趋于雷同,针对这些消费习惯和爱好,做一些团购,相信会赚到不少钱。



  1. 还是人际关系。

  2. 你喜欢做这些事情,从免费到收费循序渐进。

  3. 记住,双赢才能长久,IT行的聪明人是比较多的。


大公司养老团


找个大的,稳定的公司养老,但是也要留好退路,居安思危。


其他


比如炒股、搞理财的、做导游的、创业的……


每个人都会有自己的选择,有的人做好了准备,有的人还懵懵懂懂,2023年的行情如何还未可知,希望能长风破浪吧


作者:襄垣
来源:juejin.cn/post/7194295837265461305
收起阅读 »

教你面试就看出公司是否靠谱!

在⾯试过程中,应聘者可能想要从对公司环境的观察以及对⾯试官的提问当中获取公司相关的信息,以此来判断这家公司靠不靠谱,⾃⼰值不值得去。但这种信息可能会带有⼀定的⽚⾯性,毕竟 应聘者没有在公司实际体验过⼯作的内容,很难通过⼀两次⾯试就能看出公司的实际情况。这些细节...
继续阅读 »

在⾯试过程中,应聘者可能想要从对公司环境的观察以及对⾯试官的提问当中获取公司相关的信息,以此来判断这家公司靠不靠谱,⾃⼰值不值得去。但这种信息可能会带有⼀定的⽚⾯性,毕竟 应聘者没有在公司实际体验过⼯作的内容,很难通过⼀两次⾯试就能看出公司的实际情况。这些细节⼀般只能给予应聘者⼀点有效信息,虽然并不是绝对的准确,综合这些信息判断这家公司是否靠谱。


1 观⾯试流程


靠谱公司都有完善⾯试流程。公司发展到⼀定规模,不会随便找⾯试者过来沟通⼀下就决定要不要这个⾯试者。HR会主动跟应聘者 确定⾯试时间,通过招聘软件或正式邮件跟应聘者确定⾯试时间和⾯试准备内容,在⾯试当天主动跟进应聘者的到场情况,联系好⾯试官随时准备⾯试。如果你发现公司的⾯试流程⼗分混乱,HR 会突然更改⾯试时间,约好⾯试时间后⼜说⾯试官不在、让你第⼆天再过来,或者⾯试时连个会议 室都没有,说明公司招聘的HR⼀点都不专业。可能公司在招聘⼈才的能⼒和经验⼗分有限,或者公 司连⼀套完善的⾯试流程和制度都没有,这样的公司⼀般都是初创企业或⽐较⼩的公司。


2 察工作氛围


在⾯试当天,你可以提前到达公司,仔细稍微观察⼀下办公室⾥的⼯作氛围,看公司的氛围是否严 肃压抑,或者招聘信息 上写着不强制加班,实际个个⼯作压⼒都很⼤,每天被迫加班。如果⼤家 ⼯作的时候都是特别紧绷、各忙各的东⻄,员⼯或领导的神⾊表情都不是特别好,那这家公司有可 能是存在⼀定的内卷现象。加班可能特别严重。


如果不确定⾃⼰观察到的⼯作氛围是否就是真实情况,要如何去印证呢?


⼀般来说,⾯试官都会在⾯试环节询问⾯试者是否愿意接受加班。如果⾯试官主动问起你是否愿意 接受加班,你可以在回答中顺⼿推⾈反问⾯试官:


“⾸先我能理解公司在项⽬紧急的情况下是需要加班的,这个时候我肯定会⽀持公司的⼯作,以公司 的利益为先。(表示理解)我不是⼀个盲⽬加班的⼈,这样会让我的⽣活和⼯作⽐例失衡,反⽽会 影响⾃⼰的⼯作状态。(表明态度)如果是我⾃⼰⼯作效率低造成的加班,我肯定会先努⼒提升个 ⼈能⼒,改进⼯作⽅法,在规定的时间内完成⼯作任务。(提出解决⽅法)最后我想了解⼀下,贵 公司的加班频率是怎么样的,是否有对应的加班费呢?这是很多就业者都会关⼼的问题,希望你能 理解。(借机反问)”


如果⾯试官没有问起加班的问题,你就可以拿刚才在进⼊公司时观察到情况做⽂章,试探⼀下⾯试 官。例如:


(看到氛围差)刚才我进来到时候看到贵公司的同事⼯作都⼗分紧凑,⼯作时都是眉头紧锁、不说 ⼀句话,请问贵公司是对员⼯的⼯作效率有要求么?还是公司普遍存在⾼压或加班⽂化呢? (看到同事们⼯作时有说有笑)刚才进⼊公司的时候,我观察到贵公司的氛围⼗分轻松活跃,我⼗ 分喜欢贵公司的这种氛围,请问你们能做到这种氛围的原因是什么?是因为公司倡导⾼效率办公, 对员⼯是否加班没有硬性要求么? 当然,如果你时间⽐较充裕,或者⾯试的时间是在下午的时间,完全可以在公司附近停留⼀段时 间,等下班时间看看员⼯们是否有准时下班。如果下班时间过了半⼩时都没有员⼯⾛出来,那你对 这家公司的加班⽂化和⼯作压⼒就会有⼀定了解了。


3 ⾯试最后利用提问


套取信息。值不值得去⼀家公司,可以在⾯试的过程中询问公司发展的情况和业务发展阶段,以此判断你⼊职 后需要承担的⻆⾊和负责的⼯作内容。例如可以问⾯试官这些问题:




  • 公司/部⻔发展到⼀个什么样的阶段了?




  • ⽬前部⻔的业务是否有成熟的体系?




  • 这个岗位需要重点解决哪些问题?负责的业务有哪些?




  • 现在的部⻔/团队由哪些部分组成?具体是怎么分⼯的?




  • 岗位的转正/绩效考核是怎样的?有什么要求?




如果对⽅不能清晰介绍现在的业务或团队情况,或者跟你说现在的业务还处于起步阶段,需要你来 进⾏协助,那你就要警醒,这个岗位往往是需要你作为负责⼈或开荒⽜的⻆⾊,甚⾄要你帮公司重 新搭建业务体系,过程可能会⽐较⾟苦。如果薪酬也⽆法给到⽐较⾼的⽔平,说明这家公司可能也 是初创阶段,很多东⻄都不⼀定有保障,你就要慎重考虑这家公司值不值得去了。


4 爱压价?


在⾯试之前,HR⼀般会让⾯试者先填⼀张信息表,表格⾥往往有⼀个“期望薪资 ”这⼀栏。除⾮你 能⼒特别突出,否则公司⼀般会根据你期望薪资范围的下限来作为底薪的依据。假如你开出的底薪 条件并不算⾼,但公司还要继续压你的底薪下限,要么就是领导或⽼板格局太⼩,只想招⼀些廉价 劳动⼒ ,不是真的想吸引⼈才;要么是预算有限,你的要求不算⾼,但别⼈就是开不出这样的薪 酬条件。遇到那种跟你讨价还价的公司,就要学会擦亮双眼,这种公司⼊职后福利待遇多半不会太好。


5 查信息平台



  • 某眼查

  • 某查查

  • 某脉


6 总结


无论何时,求人不如求己,多谋求更多职业出路,才能无惧天下。


作者:JavaEdge在掘金
来源:juejin.cn/post/7292439225065160754
收起阅读 »

流金岁月

web
本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 小聚 “这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



小聚


“这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走了过去,在小白对面落了座。“少爷阔气,今天怎么请我来这里吃饭?”我问出心中疑虑,璇玑地中海自助旋转餐厅,位于广州塔106层,从窗户放眼望去,晚霞与珠江美景浑天然一色,无数高楼一览无遗,万家灯火如星光皆纳入眼前,最要命的是,大众点评人均525/人,还好不是我掏钱。


小白不以为然的笑了笑,“等到核污水传遍全球,你想吃都不敢吃了,人生短短几十年,要懂得及时行乐。而且,咱两的关系也非同一般啊~”。打从有记忆起,我和小白就认识了,年龄跟我差不多,性格跟我差不多,爱好也跟我差不多,好巧不巧,如今他跟我一样也是在广州做IT,所以我们经常联系,关系特别好。“还是你会享受,走吧,拿吃的。”随即我和小白分头寻宝,不一会儿的功夫,桌子上便摆满了芝士波士顿、芝士生蚝、海螺、北极贝、不知名海虾各种海鲜。


我抓起一个芝士生蚝就往里炫,甜中带嫩,入口即化,香味从味蕾刺激我的脑海,当我准备再抓一个,小白轻飘飘的说了句:“我破产了。”我顿时一僵,尴尬地把手收了回去,突然想到了什么,小心翼翼地问,“你不会找我借钱吧?我可没钱哦,这顿AA也行”。


互诉


“你想的倒是挺多,只是这芝士生蚝我才拿了两个,你吃了一个还想再拿,我只能技术性打断你了”,我沉默良久,直至小白把另一只芝士生蚝炫完,露出了他满足的嘴脸


04.png


我才询问道“是你那家自助预约舞蹈室吗?”



破产



小白点了点头,“自从前年疫情过后,收入就一直不太乐观了,且竞争愈来愈激烈,到了暑假最旺期间扣完水电和物业租金,我竟还要倒贴两百元。这样亏本坚持了大概8个月后,合伙人L总她终于决定要解散了。之前成立公司和几位合伙人签订的股份分配书感觉自己犹如走上人生巅峰,随着几家分店加盟,畅想无限美好未来,好日子越来越有,越来越甜。未曾想两年不到,昨日便签了注销公司的文件,现在搞一份副业,付出了大量心血,最后也未必落得一个美好的结局。”


“唉~”我长叹了一声,刚刚吃完这个芝士生蚝,嘴巴有点渴,顺手拿起手边的茶杯抿了一口,这铁观音茶,清淡清香、浓郁扑鼻,喝了一口后自有淡淡回甘。“大局已定,失败乃成功之母,不过这次你收获也不少吧?”之前我也听过小白的舞蹈室副业,记得他刚成立之初前几个月每月都有几万流水,扣除杂七杂八到手还能有个两三千块钱,没想到还是经受不住岁月的考验。


“的确有所收获,L总把舞蹈室的哈曼卡顿音响送我了,这音响听着贼带劲~”。或许是我安慰,小白皱着的双眉似乎舒展几分,“那你呢,你最近在搞什么?”


“我上王者了。”我漫不经心的回了一句。


“王者是谁?”,我白了他一眼,他笑了笑,“没想到你还在玩啊?”。“嗯,现在机制60星就能王者了,有时间便利用下elo机制完成下十年前的梦想,最近练了一手刀妹,真的万军丛中过,片叶不沾身,得心应手。”说罢,我便给他看了看我上王者的截图。


06.jpg


“羡慕你,你还是一如既往的追寻你想要的东西。”小白一边说一边用筷子把半截波士顿龙虾连根拔起,张开血盆大口,吞入嘴中。


“切,就算舞蹈室倒闭也不影响你现在的生活吧,你看你现在一样过的挺滋润啊。”我不屑的说到。


“本来还挺滋润的,但最近压力有点大了,现在每个月发工资后都往家里打几千。”我不解,问道“怎么了,是家里出什么事了吗?”



暴雷



“我家资产暴雷了,你之前也应该知道我家里主要收入吧”。我点了点头,“现在存放到那里的资产因为公司负责人喝酒脑溢血离世了,剩下一大堆债务没法处理,本来每月提供的利息也没有了,本金还被冻结住,现在暂时拿不回来,这些都是我妈跟我说的。”我听完,大吃一惊,不过这种依靠大资金存放赚取高额利息的盈利,本来就是极高风险的,正所谓高风险高回报。但我看着小白失落的神情,也只能安慰说,“有什么要帮忙的跟我说,还有这顿哥请你”。


就等你这句话”。小白乐呵呵的看着我,看着他这副表情,我硬了,拳头硬了。


“现在主要靠负责人的弟弟处理家中事务,他也承诺两年后慢慢还回本金,但是家里主要的收入来源没有了,我也不想家里人看不开,毕竟健康最重要。之前本来就有向家里人打钱,只不过现在翻了一倍,自己花钱也不能像之前大手大脚了,不过请你这顿饭倒是不成问题”。小白见我长舒一口气,好奇问我,“怎么请我吃顿饭好像要你命似的,最近手头很紧吗?”


“你小子,我听你这件事也不是死局,才放心下来,如果承诺两年之内将本金还清并落实到位,也已经算是万幸了。不过我最近手头的确不充裕”。“怎么?之前大礼包十几万全买皮肤去了?”。我摇了摇头,给他打开了我的小鹏APP,“我买了小鹏G9”。


05.jpg

小白看了看我的订单,便看向我,满是不解,“咦,之前你的梦中情车不是宝马5系,连在掘金写小说用的头图都是盗的百度,怎么买了小鹏G9啊,你不是看不上这种牌子的车吗?”


我笑了笑,说“所以这篇小说用的头图是小鹏G9”。“啊?”


“以前我总天真的以为,只要自己不断的存钱,总有一天能够实现自己买下宝马5系的梦想,但梦想终归是梦想,现实毕竟是现实,钱真的很难赚,我不想贷款,不靠父母支持,要自己一边打工一边存钱,凑够这五十万谈何容易。当然,毕竟我也是能力不行,能力配不上自己的野心,转眼之间便差不多到了而立之年,往后的日子还要准备结婚,生娃等等世俗制定好的人生阶梯,之前我老是看懂车帝推文,什么5系,E级,是普通人的天花板,那时候觉得天花板离我触手可及,而现在,我每天睁开眼都觉得天花板离我越来越远。慢慢的我也认清了自己,知道自己是个什么水平,也学会放下,但是,梦想永远会存在我的心中,不会灭去”。


“你爸妈不反对你?”。


“嗐,他们吵上天了,什么电车不安全呀,电车只能买特斯拉啊,小鹏都要倒闭之类的。但是又如何呢,毕竟钱是我的,他们做不了主。我也有试驾过宝马iX3,只是觉得当下,这台车更适合我”。小白听完我的赘述,点了点头,“嗯,我了解你这个人,一旦认定了某些事,别人很难去改变你的想法。”我嘿嘿一笑,“你不也是吗?”


边聊边吃时间总是过得特别快,不一会儿我们两人的桌碟上放满了残骸,堆积如山,刚好服务员经过帮我们更换了新碟子,我们异口同声的说了声谢谢。


进入了短暂的沉默,我率先问小白,“最近工作情况如何?”。


小白听完,擦了擦嘴,表情也正经了起来,“其实今天我主要目的就是来跟你分享一下我最近的经历的,看看你有啥想法。”。


“哦?细说。”



曙光



“前两周我之前的leader让我去他公司面试。”


“你去了?”


“别打岔”。小白没好气的看我一眼,继续说道“一开始我是拒绝的,你也知道,我最近都比较躺平了,拿着一份在广州过的不差的薪资,浑浑噩噩的过着日子。谁知道,那位大佬开口便以35 * 14邀约,我本是性情中人,路见不平便拔刀相助,朋友有困难,我都会鼎力相助,何况这是贵人”。随即,他给我看了看聊天内容。


02.jpg


01.jpg


03.jpg

“听到这个数,我真的是垂死病中惊坐起,手上的switch瞬间丢到一边,立刻屁颠屁颠准备简历了。他说我是人才,其实我知道,贵人的实力,才称得上真正的人才”


“因为base要去深圳,如果薪资涨幅不是太高的话,其实我心里还是不太乐意的,因为现在在公司其实也得到leader青睐了,而且临近年终了,说实话,工作了6年多,之前跳来跳去,为了眼前那点利益,年终奖都没拿过几次,实属心累。但是这次如果薪资到胃,能给到这么高,虽然脉脉都劝别人不要去,但是我还是想尝试一下,毕竟这是更大的平台,为自己以后职业规划着想,现在累点真不算啥。”


我表示理解,“毕竟脉脉都是问就是别去,劝就是不走”。


小白笑着点了点头,“所以我去了,一面二面都过了。”


“牛逼啊,面了什么,分享一下啊~”


“牛逼个啥啊,问的都比较浅,一面印象比较深主要下面几点”。



  • typescript中 interface 和 type 的区别


1. type类型使用范围更广, 接口类型只能用来声明对象
2. 在声明对象时, interface可以多次声明
3. 区别三 interface支持继承的,type不支持
4. interface可以被类实现


  • typescript中 字面量类型


这个我真忘了,回头你自己整理一下


  • redis中如何处理大key


这个我不会,面试官也很nice的讲解了一波


  • 服务器容灾方案


这个我不会,面试官也很nice的讲解了一波


  • service worker作用


这个我不会,面试官也很nice的讲解了一波

“不是,你咋啥都不会啊?”。我超大声的问。


“那你会吗?”小白反问我。


“我也不会”。


“那我就放心了,然后就是问项目细节,我稀里糊涂的答,他稀里糊涂的听,二面的话就是更深入的项目细节了。”


“然后呢然后呢?”


“然后便是昨天的HR终面,HR让我从春秋时期讲起,阐述我的工作经历,我也是乖巧的从百草园讲到三味书屋,其中声情并茂,对细节说到尽情之处,心中也不免感慨之前的辉煌。不过,往事已成云烟。待烟消云散,HR便说重点,问我期望薪资。因为期间我也听HR说现在节奏很紧,加班已是常态,10-10-6稀松平常,我见HR诚心待我,我便以真心待她”。


“你说了多少?”


“98K”


“她同意了?”


“她说你现在的薪资过低,申请不到这个数,如果降低你是否会考虑?”


“那你怎么回?”


“我怎么回,难道你不知道吗?”


“我怎么会知道?”


past lives couldn't ever hold me down, lost love is sweeter when it's finally


"i've got the strangest feelin, this isn't out first time around"


“怎么我的闹钟响了?”突然,周遭的一切突然模糊了起来,旋即进入黑暗。


梦醒


我肌肉记忆般的按停了手机的闹铃,这首歌是我最喜欢的歌,但是自从做了起床闹钟铃声后,我便没有再听过。


我机械般地刷牙洗脸漱口,坐在有点老旧的餐桌椅上,打开昨天晚上下班临时买的方块原味面包,吃了两片,肚子好受一些,看来平日加班还是要按时吃晚饭。


我出门走去,阳光落在我的脸上,小白是我,我亦是我。


作者:很饿的男朋友
来源:juejin.cn/post/7293786784127090715
收起阅读 »

一不小心真删库了

事情经过 就是今天看到web前端的页面因为某个字段是Null,就想给Prisma的某个字段加上一个默认值,就像这样: 感觉也比较常规,当我改完这个字段后,我想的就是直接跑prisma migrate然后去迁移数据库,然后自动把null全部填值了。 md,然后...
继续阅读 »

事情经过


就是今天看到web前端的页面因为某个字段是Null,就想给Prisma的某个字段加上一个默认值,就像这样:


image.png


感觉也比较常规,当我改完这个字段后,我想的就是直接跑prisma migrate然后去迁移数据库,然后自动把null全部填值了。


md,然后因为我觉得这个操作好像再正常不过了,所以我就没看prisma在说啥,直接一路y到底了。


image.png


然后我一看prisma的客户端,茫然->刷几下->看看链接->茫然->再刷几下->啊?->无语凝噎->亚历山大、血压升高、心跳加速。md我的数据那,我的表里怎么一条数据没了,几万条啊?????


所以这个事情就告诉了我们,在做一些操作的时候不要觉得烦就一路y到底,认真看看命令行的反馈,那tm红红的一段 All data will be lost.......


问题追溯


在问题追溯前我先简单介绍下Prisma是啥,以及db操作和migrate操作(就是如果完全不了解数据库Orm的已经可以直接跳过了)。


Prisma


简单的说就是Prisma通过自己创建的特定语言DSL,把自己DSL结构映射为表结构,然后可以通过一系列cli去生成client或者迁移洗数据推拉数据库之类的操作,去帮助开发者在 Node.jsTypeScript 项目中更轻松地处理数据库操作.


Prisma db


db操作pullpushseedexecute,这一系列命令用于将 Prisma schema 中的定义同步到数据库 schema 中。db push 会检查当前数据库并根据您的 Prisma schema 对其进行更改。这是一种直接将 Prisma schema 变更应用到数据库的方法,但不会创建或使用迁移文件。在使用 db类操作 时,不会记录更改的历史(也就是不会生成提交记录,记住这一点很重要)、而我理解迁移历史的意义就是在于回滚、备份、恢复和版本控制,因此适用于原型设计和开发过程中的快速迭代


它的工作流分解一下可以分为以下几步:



  1. 自检Prisma 工具检查当前连接的数据库,并收集有关现有表、列、索引和其他数据库对象的详细信息。

  2. 分析Prisma 分析收集到的数据库信息,并尝试推断当前数据库结构与 Prisma schema 之间的差异。结合推断出的信息,Prisma 将生成相应的数据库操作(例如创建、修改或删除表、列、索引等),执行后,当前已连接的数据库结构和 本地Prisma schema 应该保持一致。

  3. 生成触发器:默认情况下,在将更改应用到数据库架构后,Prisma 会触发生成器(例如,Prisma Client)。不需要手动调用 prisma generate


Prisma migrate


它帮助我们根据 Prisma schema 中的更改自动创建、处理和执行迁移,同时保持数据库 schema 与迁移历史记录同步。migrate dev 通过影子数据库(shadow database)来检测 schema 偏移和冲突,并在需要时提示我们进行数据库重置。也就是说它可以确保在团队中正确地记录和同步数据库更改。


它的工作流分解一下可以分为以下几步:



  1. 重播现有迁移历史:在影子数据库(shadow database)中重播现有迁移历史,以便检测 schema 偏移(编辑或删除的迁移文件,或者对数据库 schema 的手动更改)。

  2. 应用待执行的迁移:将待执行的迁移应用到影子数据库(例如,由同事创建的新迁移)。

  3. 从 Prisma schema 更改中生成新迁移:在运行 migrate dev 命令之前,从我们对 Prisma schema 所做的任何更改中生成新迁移。

  4. 将所有未应用的迁移应用到开发数据库并更新 _prisma_migrations:将所有未应用的迁移应用到开发数据库,并更新 _prisma_migrations 表以记录已执行的迁移。

  5. 生成触发器和迁移历史


Why?


刚刚提到了做db操作是不会生成提交记录的,那到底发生了什么呐,我旁边的老大哥给我复盘了一下,首先A同事对数据库进行了db push操作(因为他正在设计一张表),这时候数据库schema其实已经偏移了但这时候,我并不知道因为没有任何的留存,然后接下来我在没有进行db pull的情况下直接去做了migrate操作,其实在这个时候prisma通过生成的影子数据库已经检测到了有偏移了,但我并没有注意,直接y到底,导致直接进行了reset操作清空了数据。


反思



  • 在团队协作项目中优先使用数据库迁移工具(如 migrate dev)。

  • 在执行关键数据库操作之前,确保同步所有的更改并了解同事所做的修改。

  • 当迁移工具检测到偏移和冲突时,确保花时间了解问题并采取适当措施。

  • 这其实就是缺乏适当沟通和不遵循较好实践带来的后果。在执行关键操作前了解同事的更改和确认是非常重要的。遇到迁移工具检测到的偏移和冲突时,应确保彻底了解问题并采取适当措施解决,而不是忽略警告。


作者:溪饱鱼
来源:juejin.cn/post/7293765765126324259
收起阅读 »

不戴眼镜也可以看清啦!记录我的摘镜经历

大家好,我是 Gopal。好久不见,甚是想念! 本篇文章记录一下今年我做的一个比较大胆的决定——做近视手术。 首先声明一下,本篇文章不是广告,纯分享个人经历,看完这篇文章,至于要不要做视力矫正?怎么做?个人可以根据需要自行决定哈。 我为什么要摘镜 先说一下,我...
继续阅读 »

大家好,我是 Gopal。好久不见,甚是想念!


本篇文章记录一下今年我做的一个比较大胆的决定——做近视手术。


首先声明一下,本篇文章不是广告,纯分享个人经历,看完这篇文章,至于要不要做视力矫正?怎么做?个人可以根据需要自行决定哈。


我为什么要摘镜


先说一下,我的情况。我是初中毕业后开始慢慢近视的,至今的话,也有十来年了,眼镜都配了好多副。有近视的同学应该都知道,近视确确实实带给我们很多的烦恼,我举几个例子:



  1. 看 3D 电影,我得带两副眼镜,一副是自己的,一副是 3D 眼镜。

  2. 游泳的时候,脱掉眼镜,啥也看不到。

  3. 打篮球或者一些激烈的运动,眼镜经常脱落。

  4. 每天都得擦拭眼镜,对于我这种有点强迫症来讲的人,会是每天的工作。

  5. 早上起来第一件事情,找眼镜。

  6. ...


基于以上的种种,摘镜意愿,我个人是比较强烈的。当我听说现在有手术可以进行视力矫正时候,是非常兴奋的。我之后做了相关的调查,我们来看一下视力矫正有哪些方案吧。


视力矫正有哪些方案


这里我会介绍得比较粗糙一些,毕竟我不是专业的,知识都是我从网络中总结得来。


近视原因:我们大部分人的近视都是轴性近视,是由于眼球的前后长度(轴长)延长造成的,可以看以下的图片。在正常眼中,平行光线入眼后在视网膜上形成焦点。而在近视眼中,焦点位置落在视网膜之前,近视眼想看清,就得调整屈光度,使其聚焦在视网膜上。


image.png


眼镜通过镜片调节屈光度,而近视手术的大致有两种方式进行:



  • 第一种,通过角膜手术改变角膜的形态和屈光力,使物象成像到视网膜上。

  • 第二种,更换晶体或向眼内植入补偿晶体,从而达到类似的效果,这种类似于带隐形眼镜。


其中有一个特点就是晶体是有可逆性,激光手术的话,是不可逆的。以上两种并没有说哪种是最优解,需要通过医院检查才能决定。一般来说,角膜屈光手术建议近视度数小于 1000 度,散光小于 600 度。如果因为度数过高、或角膜太薄不能做角膜屈光手术,可以考虑做晶体植入手术。


现在主流的手术方案有以下四种(前面三种都属于角膜手术),具体的这里不展开了,大家有想法可以自行搜索哈。



  • Smart全激光

  • 半飞秒

  • 全飞秒Smile

  • ICL晶体植入术


而我通过检查之后,选择了全飞秒Smile。主要是我度数还好,角膜厚度也足够。


手术前中后


首先约了一家上市的机构做检查,看得出还是比较慎重,各个方面评估能不能做,以及有哪些方案,最后会有专门的人给我讲解。


做完检查之后,我当时就约了隔周去做,因为手术前几天是需要滴眼药水,而且不能佩戴隐形眼镜等。


手术当天,做术前检查,交代注意事项/术后用药、签署同意书等。说实话,我还是比较紧张的,毕竟把眼睛这么重要的部位交给医生。


然后就是手术了,真正的手术时间也就几分钟,当躺在手术室中,一开始我以为医生还在给我做前置检查,没想到几分钟后医生告诉我已经结束了,可以回家了。手术过程滴麻药,基本不会痛的。整个激光过程,眼睛是会有开睑器撑开固定,手术过程眼睛是同时睁开/闭上的。手术过程中,需要在打激光的过程中配合,眼睛保持不动不眨,听好医生指令


手术后,我和我对象就坐地铁回去了,回家的路上,我一直戴着墨镜,随着麻药的失效,会有一种较强的异物感。虽然我知道这是一个正常现象(医生提前已告知,手术后将会在4-6小时内会出现流泪、畏光、白雾感、异物感、酸胀感等刺激症状,其轻重因人而异),但是当时真的挺怕的,毕竟可不是小事。(所以最好手术当天最好还是有人一起去)。


当天下午,我就感觉好很多了。我做的事全飞秒,周六做的手术,周一的时候,我就可以正常上下班了,基本不会影响工作。唯一我感知到的一点点影响就是切换屏幕的时候,聚焦会需要一点点时间,很短,医生说是正常现象。不过中间需要注意用眼,然后注意按照医嘱滴眼药水等。这里我贴几个术后的注意事项,当然你应该根据你的主治医生来:



  1. 术后一周内清淡饮食,吃辛辣刺激食物。

  2. 术后两周内洗头洗脸时不宜将水溅入眼内,切忌揉眼。(手术室给的眼罩睡觉时带上)。

  3. 术后一个月内勿游泳,不要在眼部使用化妆品并避免异物进入眼内。

  4. 术后一个月在室外请戴太阳镜(全激光术后戴三个月),室内、晚上不需佩戴。

  5. 术后三个月内尽量避免剧烈运动,术后一周后可以适量健身运动,但须注意在活动中不要伤及手术眼。

  6. 术后视力恢复是逐步提高的过程,开始阶段看近会感到稍有模糊,雾感。此症状会逐渐消失,视力提高有快有慢,双眼视力恢复会有波动与近视度数及个人对治疗反应差异有关,六个月基本稳定。


在写本文的时候,已经有两个多月了,感觉日常生活没啥影响。在一个月复诊的时候,我的视力一边是 1.2,一边是 1.5。如果说有啥「副作用」的话,有两点。一个是晚上的视力(在光线比较差的地方)会稍微有点差,另外一个看一些光源(比如路灯),会有一点炫光。不过这些术前医生有给我说过,我是有做好心理准备的。


image.png


整体而言,目前为止,我对这次的近视手术还是很满意的。


手术价格以及副作用


这个我相信不少人比较关注,我做的全飞秒,全部下来大概 1.8w 左右(公司跟这家机构有合作,跟我说是打了折扣,我了解了一下,在深圳,这个价格其实差不多)。激光和半分秒会稍微便宜一些。晶体植入是最贵的(大概 3w 多)。具体的还需要根据不同的城市和机构看。据说虽然近视手术简单,但是它那个机器是需要给专利费用的,用一次给一次专利费用。整体上讲,这个还是可以接受的。另外,近视手术不能报医保。


比如价格,我觉得大家最关心的问题是副作用,或者说风险系数。任何手术都是有风险的,包括近视手术。网络上会有人说:



  • 近视手术这么好,为什么医生也还带眼镜?

  • 近视手术这么好,为什么马化腾还带眼镜?

  • ...


image.png


首先,先不否认大家的疑虑,毕竟眼睛这么重要的东西,要在上面动手术,想想都怕。但是,我们也需要辩证的看待这些问题。



  • 近视手术是需要满足一定条件的,不满足的话,医院是不敢给你做的。

  • 近视手术是不会导致你瞎的,从原理上来说,近视手术是眼睛前面部分的手术,不涉及眼部深层组织,比如晶状体、视网膜等,更加不可能致盲。

  • 近视手术存在发生并发症的可能性,可能性大小而已。(别人没有,并不代表自己没有)比如眼干等。虽然现在手术已经很成熟,但是在你决定要做之前,一定要先了解清楚,看你能不能承担这个风险。

  • 近视手术只是当前帮你调整好你的视力,假如你不注意用眼的话,是存在再次近视的风险的。所以一般建议成年之后,度数稳定之后再做这个手术。

  • ...


最后再强调一遍,近视手术是一种改善型的手术,不做其实本质上对于自身身体健康没有影响,如果摘镜意愿强烈的同学可以尝试去了解一下。


写在最后


目前为止,我感受到更多的是摘镜之后给我生活带来的便利,基本算是满足了我摘镜的预期!


我之前一直在想,假如哪天我落在一座荒岛上,估计是活不下去的。因为假如一旦我眼镜坏了,那么我就「看不清」这个世界了。


不过现在医学科技的进步解决了我的这个问题。


作者:Gopal
来源:juejin.cn/post/7293788137662038050
收起阅读 »

曹贼,莫要动‘我’网站 —— MutationObserver

web
前言 本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。 正文 话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子 这么好看的看的小乔,谁看谁不糊,更何...
继续阅读 »

前言


本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。


正文


话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子


image.png
这么好看的看的小乔,谁看谁不糊,更何况曹老板。这天,曹操在浏览网页的时候,无意间发现了周瑜的这个网站,看着美若天仙的小乔,曹操的眼泪止不住的从嘴角流了下来。赶紧将网站上的照片保存了下来。

这个消息最后传到了周瑜的耳朵里,他只是想展示小乔,可不是为了让别人下载的。于是在自己的网站上做了一些预防措施。

为了防止他人直接在网站上直接下载图片,周瑜将右键的默认事件给关闭了,并且为了防止有人打开控制台,并对图片保存,采取了以下方法:


禁用右键和F12键


//给整个document添加右击事件,并阻止默认行为
document.addEventListener("contextmenu", function (e) {
e.preventDefault();
return false;
});

//给整个页面禁用f12按键 keyCode即将被禁用 不再推荐使用 但仍可以使用
document.addEventListener("keydown", function (e) {
//当点了f3\f6\f10之后,即使禁用了f12键依旧可以打开控制台,所以一并禁用
if (
[115, 118, 121, 123].includes(e.keyCode) ||
["F3", "F6", "F10", "F12"].includes(e.key) ||
["F3", "F6", "F10", "F12"].includes(e.code) ||
//ctrl+f 效果和f3效果一样 点开搜索之后依旧可以点击f12 打开控制台 所以一并禁用
//缺点是此网站不再能够 **全局搜索**
(e.ctrlKey && (e.key == "f" || e.code == "KeyF" || e.keyCode == 70))||
//禁用专门用于打开控制台的组合键
(e.ctrlKey && e.shiftKey && (e.key == "i" || e.code == "KeyI" || e.keyCode == 73))
) {
e.preventDefault();
return false;
}
});

当曹操再次想保存小乔照片的时候,发现使用网页的另存了已经没用了。这能难倒曹老板吗,破解方法,在浏览器的右上角进行操作就可打开控制台,这个地方是浏览器自带的,没办法禁用


image.png
这番操作之后,曹操可以选择元素保存那个图片了。周瑜的得知了自己的禁用措施被破解后,赶忙连夜加班打补丁,于是又加了一些操作,禁止打开控制台后进行操作


禁用控制台


如何判定控制台被打开了,可以使用窗口大小来判定


function resize() {
var threshold = 100;
//窗口的外部减窗口内超过100就判定窗口被打开了
var widthThreshold = window.outerWidth - window.innerWidth > threshold;
var heightThreshold = window.outerHeight - window.innerHeight > threshold;
if (widthThreshold || heightThreshold) {
console.log("控制台打开了");
}
}
window.addEventListener("resize", resize);

但是也容易被破解,只要让控制台变成弹窗窗口就可以了


也可以使用定时器进行无限debugger,因为只有在控制台打开的时候debugger才会生效。关闭控制台的时候,并不会影响功能。当前网页内存占用比较大的时候,定时器的占用并不明显。在当前网页占用比较小的时候,一直开着定时器才会有较为明显的提升


  setInterval(() => {
(function () {})["constructor"]("debugger")();
}, 500);

破解方法一样有,在debugger的位置右键禁用调试就可以了。这样控制台就可以正常操作了


image.png
既然有方法破解,就还要做一层措施,既然是要保存图片,那就把img转成canvas,这样即使打开控制台也没办法进行对图片的保存


//获取dom
const img = document.querySelector(".img");
const canvas = document.querySelector("#canvas");
//img转成canvas
canvas.width = img.width;
canvas.height = img.height;
ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
document.body.removeChild(img);

经过一夜的努力,该加的措施都加上了。周瑜心想这下就没办法保存我的小乔了吧。

来到曹操这边,再次打开周瑜的小破站,还想故技重施时,发现已经有了各种显示,最后也没难倒曹操,那些阻碍也都被破解了。但是到保存图片的时候傻眼了,竟然已经不是图片格式了,那就没办法下载了呀。但是小乔真的很养神,曹操心有不甘,于是使用了最后一招,既然没办法下载那就截图,虽然有损画质,但是依旧能看。


得知如此情况的大都督周瑜不淡定了,从未见过如此厚颜无耻之人,竟然使用截图。


006APoFYly1g2qcclw1frg308w06ox2t.gif
话说魔高一尺,道高一丈,周瑜再次熬夜加班进行对网站的优化。于是使用了全屏水印+MutationObserver监听水印dom的方法。即使截图也让他看着不舒服。


MutationObserver


MutationObserver是一个构造函数,接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

它接收一个回调函数,每当监听的dom发生改变时,就会调用这个函数,函数传入一个参数,数组包对象的格式,里面记录着dom的变化以及dom的信息。


image.png
返回的实例是一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象。有三个方法observedisconnecttakeRecords



  • observe接收两个参数,第一个为要监听的dom元素,第二个则是一些配置对象,当调用 observe() 时,childListattributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。配置对象如下:

    • subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false

    • childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false

    • attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false

    • attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

    • attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false

    • characterDate:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

    • characterDateOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false



  • disconnect方法用来停止观察(当被观察dom节点被删除后,会自动停止对该dom的观察),不接受任何参数

  • takeRecords:方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。


该构造函数监听的dom即使在控制台中被更改属性或值,也会被监听到。


使用MutationObserver对水印dom进行监听,并限制更改。


<style>
//定义水印的样式
#watermark {
width: 100vw;
height: 100vh;
position: absolute;
left: 0;
top: 0;
font-size: 34px;
color: #32323238;
font-weight: 700;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-content: space-evenly;
z-index: 9999999;
}
#watermark span {
transform: rotate(45deg);
}
</style>

<script>
//获取水印dom
const watermark = document.querySelector("#watermark");
//克隆水印dom ,用作后备,永远不要改变
const _watermark = watermark.cloneNode(true);
//获取水印dom的父节点
const d = watermark.parentNode;
//获取水印dom的后一个节点
let referenceNode;
[...d.children].forEach((item, index) => {
if (item == watermark) referenceNode = d.children[index + 1];
});
//定义MutationObserver实例observe方法的配置对象
const prop = {
childList: true,//针对整个子树
attributes: true,//属性变化
characterData: true,//监听节点上字符变化
subtree: true,//监听以target为根节点的整个dom树
};
//定义MutationObserver
const observer = new MutationObserver(function (mutations) {
//在这里每次坚挺的dom发生改变时 都会运行,传入的参数为数组对象格式
mutations.forEach((item) => {
//这里可以只针对监听dom的样式来判断
if (item.attributeName === "style") {
//获取父节点的所有子节点,因为时伪数组,使用扩展运算符转以下
[...d.children].forEach((v) => {
//判断一下,是父节点里的那个节点被改变了,并且删除那个被改变的节点(也就是删除水印节点)
if (item.target.id && v == document.querySelector(`#${item.target.id}`)) {
v.remove();
}
});
//原水印节点被删除了,这里使用克隆的水印节点,再次克隆
const __watermark = _watermark.cloneNode(true);
//这里的this指向是MutationObserver的实例对象,所以同样可以使用observe监听dom
//监听第二次克隆的dom
this.observe(__watermark, prop);
//因为水印dom被删除了,再将克隆的水印dom添加到原来的位置 就是referenceNode节点的前面
d.insertBefore(__watermark, referenceNode);
}
});
});
在初始化的时候监听初始化的水印dom
observer.observe(watermark, prop);
</script>



这样,每当对水印dom进行更改样式的时候,就会删除该节点,并重新添加一个初始的水印dom,即使突破重重困难打开开控制台,用户也是无法对dom 进行操作。


视频转Gif_爱给网_aigei_com.gif


隔天曹操再次打开网页,发现网页上的水印,心里不足为惧,心想区区水印能难倒自己?操作到最后却发现,不论如何对水印dom进行操作,都无法改变样式。虽说只是为了保存图片,但是截图有着这样水印,任谁也不舒服呀。曹操大怒,刚吃了两口的饭啪的一下就盖在了桌子上......


20230508094549_33500.gif
然而曹操不知道的是,在控制台中,获取dom节点右键是可以只下载获取的那个节点的......


image.png


结尾


文章主要是以鬼畜恶搞的方式讲述了,如何禁止用户打开控制台(还有重写toSring,consloe.log等一些方法,但我并没有没有实现,所以这里并没有写上),并且如何使用MutationObserver构造函数来监听页面中的dom元素。其实大多情况下并没有这方面的项目需求,完全可以当扩展知识看了。


写的不好的地方可以提出意见,虚心请教!


作者:iceCode
来源:juejin.cn/post/7290862554657423396
收起阅读 »

在高德地图实现后期效果

web
介绍 最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图...
继续阅读 »

介绍


最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图层中。
Effect-PointsLayer2.gif


后期特效其实有点类似照片的后期滤镜处理,是对渲染结果的二次处理,可以实现发光、模糊、色调调整、镜头暗角、模拟环境光遮蔽等各种效果,为了方便理解,下面的讲解我将以辉光效果为例,学会了一种其他效果思路类似。


方案调研


Three官方提供了非常简单的方法实现后期特效,貌似仅需要完成以下两个步骤就可以完成我们想要的需求,代码也非常清晰简单:


import * as THREE from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing//UnrealBloomPass.js'
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'

...

// 1.在图层初始化完成后,创建效果合成器
onLayerInit(){
const { scene, camera, renderer } = this

const renderScene = new RenderPass(scene, camera)

// 后期泛光特效
bloomPass = new UnrealBloomPass(new THREE.Vector2(this.container.clientWidth, this.container.clientHeight), 1, 0, 0)
bloomPass.threshold = params.threshold
bloomPass.strength = params.strength
bloomPass.radius = params.radius

composer = new EffectComposer(renderer)
// 以下代码会遮盖地图
composer.addPass(renderScene)
composer.addPass(bloomPass)
}

// 2.更新合成器
onRender () {
if (composer) {
composer.render()
}
}

本以为这样做就可以开心收工了,燃鹅事情并没有那么简单,把这套方案移入高德的GLCustomLayer中,出现了这样的情况,后期效果直接把地图底图盖住了。
Untitled.png


出现这种情况的原因是实现辉光效果而编写的着色器,它会直接修改整个画面的alpha通道而导致透明效果丢失,因此需要单独修改UnrealBloomPass.js。


然而光是这样还不够,经过各种尝试,仍无法直接在GLCustomLayer上解决地图被遮盖的问题,后来咨询了高德地图开发团队的技术大佬,他给我的建议是后期效果层独立展示,于是就沿着这个思路进行了第二轮尝试。
Untitled 1.png


这里面有几个关键步骤是必须的:



  1. 修改UnrealBloomPass着色器代码

  2. 使用输出通道new OutputPass()置于特效通道的后面

  3. 在customLayer图层中,每次渲染就更新特效合成器EffectComposer


由于我这边是不希望之前开发的可视化图层做太多的修改去迁就这个后期效果的,也有对性能较差的终端机器优雅降级的考虑,索性把后期效果独立为EffectLayer层,以方便灵活地装载或剥离,最终实现了这个效果。


Effect-POI3dLayer1.gif


实现步骤




  1. 修改 UnrealBloomPass.js,由于这个文件在npm包中不能随意修改,我另外写了一个UnrealBloomPass1 继承并覆盖了UnrealBloomPass的方法


    import * as THREE from 'three'
    import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'

    class UnrealBloomPass1 extends UnrealBloomPass {
    constructor (resolution, strength, radius, threshold) {
    super(resolution, strength, radius, threshold)
    }

    getSeperableBlurMaterial (kernelRadius) {
    ...
    fragmentShader:
    `#include <common>
    varying vec2 vUv;
    uniform sampler2D colorTexture;
    uniform vec2 invSize;
    uniform vec2 direction;
    uniform float gaussianCoefficients[KERNEL_RADIUS];

    void main() {
    float weightSum = gaussianCoefficients[0];
    vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
    float alphaSum;
    for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
    float x = float(i);
    float w = gaussianCoefficients[i];
    vec2 uvOffset = direction * invSize * x;
    vec4 sample1 = texture2D( colorTexture, vUv + uvOffset );
    vec4 sample2 = texture2D( colorTexture, vUv - uvOffset );
    diffuseSum += (sample1.rgb + sample2.rgb) * w;
    alphaSum += (sample1.a + sample2.a) * w; //
    weightSum += 2.0 * w;
    }
    // gyrate: overwrite this line for alpha pass
    // gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
    gl_FragColor = vec4(diffuseSum/weightSum, alphaSum/weightSum);
    }`

    })
    }
    }

    export { UnrealBloomPass1 }



  2. 编写EffectLayer


    import * as THREE from 'three'
    import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
    import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
    import { UnrealBloomPass1 } from '../plugins/three/examples/jsm/postprocessing/UnrealBloomPass.js'
    import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'
    import _ from 'lodash'

    class EffectLayer {

    // 此处省去一些内部变量

    _style = {
    // 光照强度阈值
    threshold: 0.0,
    // 泛光强度
    strength: 1.0,
    // 泛光半径
    radius: 1.5
    }

    /**
    * 创建一个实例
    * @param {Object} config
    * @param {Layer} config.layer 目标图层,要求是Layer的相关子类
    * @param {Number} [config.zIndex=120] 图层的层级
    * @param {EffectStyle} [config.style] 后期特效的配置项
    */

    constructor (config) {
    const conf = _.merge(this._conf, config)
    this._style = _.merge(this._style, conf.style)

    if (!conf.layer.scene || !conf.layer.camera) {
    console.error('缺少场景和相机')
    return
    }
    this.init()
    }

    init () {
    this.createLayer()
    this.addEffect()
    }
    }



  3. 创建自定义图层customLayer


    createLayer () {
    const canvas = document.createElement('canvas')
    this._customLayer = new AMap.CustomLayer(canvas, {
    zooms: [3, 22],
    zIndex: this._conf.zIndex,
    alwaysRender: true
    })

    this._canvas = canvas
    }



  4. 创建特效合成器


    addEffect () {
    const { scene, camera, container, renderer, map } = this._conf.layer
    const { clientWidth, clientHeight } = container

    // 创建渲染器
    const effectRender = new THREE.WebGLRenderer({
    canvas: this._canvas,
    alpha: true,
    antialias: false,
    stencil: false,
    depth: false
    })
    // renderer.setClearColor(0xff0000);
    effectRender.autoClear = false
    effectRender.setSize(clientWidth, clientHeight)

    // 后期效果
    const renderScene = new RenderPass(scene, camera)

    // 后期辉光特效
    const bloomPass = new UnrealBloomPass1(new THREE.Vector2(clientWidth, clientHeight), 1, 0, 0)
    bloomPass.clear = false

    // 输出通道
    const outputPass = new OutputPass()
    outputPass.clear = false

    this.updatePass()

    const composer = new EffectComposer(effectRender)
    composer.addPass(renderScene)
    composer.addPass(bloomPass)
    composer.addPass(outputPass)

    this._composer = composer
    this._bloomPass = bloomPass

    this._customLayer.render = function () {
    if (composer) {
    // 每次渲染就更新特效合成器
    composer.render()
    }
    }

    map.add(this._customLayer)
    }

    updatePass() {
    const {_bloomPass} = this
    if (_bloomPass) {
    _bloomPass.threshold = this._style.threshold
    _bloomPass.strength = this._style.strength
    _bloomPass.radius = this._style.radius
    }
    // 添加其他特效通道...
    }



  5. 使用EffectLayer


    //之前编写的可视化图层
    const layer = new GLlayers.POI3dLayer({
    map: getMap(),
    zooms: [10, 22]
    })

    layer.on('complete', (layer) => {
    let effectLayer = new GLlayers.EffectLayer({
    layer: layer, //把图层传入effectLayer
    style:{
    threshold: 0.0,
    strength: 1.0,
    radius: 0.5,
    }
    })
    })




注意:以上方案three.js版本为0.157, 该版本对three/example/jsm/postprocessing目录中的后期效果通道相关文件做了较多调整,如果是用之前的three.js版本,修改内容可能有所不同。



至此我们就可以在之前的可视化图层基础上,加入几行代码实现辉光效果,以下是挑选一部分图层加上EffectLayer之后的效果,肉眼可见还是有很明显区别的。当然在使用过程中也发现了个别图层原有的问题需要做进一步优化。


Effect-BorderLayer1.gif


Effect-PointsLayer1.gif


Effect-SpriteLayer1.gif


Effect-cakeLayer1.gif


待解决问题


使用独立图层展示后期特效层有个明显缺点,无法关联默认基本图层的场景要素深度信息,最主要的影响是高德的建筑白模图层和自定义可视化图层的远近遮挡关系会丢失,导致可视化图层永远在最前面。比如下面这个城市主要道路的辉光效果,这个是需要后面花时间去解决的,写这篇文章的时候又找到几个方案,有时间再试一把,毕竟上面留给我的时间不多了。


Effect-FlowlineLayer2.gif


相关链接


three.js后期处理


three.js效果合成器文档和示例


实现模型材质局部辉光效果和解决辉光影响场景背景图显示的问题


Three.js带Depth实现分区辉光


作者:gyratesky
来源:juejin.cn/post/7293788726235365426
收起阅读 »

谢谢你,成都

成都,带不走的,只有你..... XXXX说,一千个人就有一千个成都 有人说成都是天府之国,有人说成都是西南柬埔寨。 我的成都,有爱有梦,谢谢你。 还记得5年前,2017年7月份,我从一个不知名的二本财经学校里的不知名专业毕业,凛冽的西北风最终没能把我留在兰州...
继续阅读 »

成都,带不走的,只有你.....


XXXX说,一千个人就有一千个成都


有人说成都是天府之国,有人说成都是西南柬埔寨。


我的成都,有爱有梦,谢谢你。


还记得5年前,2017年7月份,我从一个不知名的二本财经学校里的不知名专业毕业,凛冽的西北风最终没能把我留在兰州,大三一次偶然的寒假工,让我才意识到原来冬季并不都是白雪茫茫,原来冬季也可以绿意盎然,于是那年校招,我忽略了所有的北方校招,只参加了所有来自南方的企业。


在经历了当时看起来几次的高光时刻后,我最终通过校招加入了广东一家大型集团公司。


7月的兰州,需要穿件毛衣才可以让自己看起来非常从容,那时人生第一次坐飞机,原谅来自农村的孩子真的见识短浅,穿过白云的世界真的从来没有见识过,飞机上的2小时一刻也没有睡着,光顾着去拍下那高空中的世界。


下了飞机后,一股热气流扑面而来,让我一度怀疑广东人天天在蒸桑拿,走在路上没10分钟,我已经汗流浃背,非常羡慕走在我旁边的一个靓妹,小短袖,小短裤,甚是凉快。


IMG_1485.jpg


加入公司后,公司为每个新人分配了导师,导师带我们做常规开发任务,同时为我们能在这复杂的社会上立足而答疑解惑。


由于是传统企业,技术相对而言非常老旧,经常需要维护Jquery项目,唯独一个在当时看起来眼前一亮的项目是内部HR使用的考勤平台,在我们几个90后一再的说服下,领导同意使用React开发,但是我们必须保证进度.....


广东传统企业大部分都配备有员工餐厅,员工宿舍,因此毕业的那两年从来没有经历颠沛流离的生活,每天下班后会和当时校招进去的几个同期生先去糖水店吃一下,周末流窜于广州的大街小巷,上下九步行街.....,生活看起来无忧无虑。


直到2019年下半年,11月份的广州依然是那么炎热,桑拿生活还是一样在继续,周一正常打卡上班,上班没多久,领导给我发消息,“来下办公室呢”,进去后,领导还是和往常一样,在自己狭小略显简陋的办公室里沏了一壶茶,接下来的谈话让我一生难忘,因为这看似关心,实则裁员的谈话最终导致了我抑郁症的发作,久久没有平复.....


领导直言,公司在某个大的战略方向上已经折腾了3年,老板和投资人不愿意继续下去,开发人员只会留下部分,他可以替我申请,更换部门,去市场部就职....,一想起同期的一个市场部门同事说市场部会喝酒是一项工作技能,而我,生理本能排斥那样的生活.....


那天的广州,阳光非常热烈,但维护没有一丝丝光线照进我的工位,走出领导办公室的我,瘫坐在自己工位上,身体发软,毫无力气,喘气困难......


很多思绪开始在我脑海里出现,如果我当时选择了北方的校招,如果我曾经选择了跳槽,如果......随之而来的情绪将我击倒,没人给我说过我该怎样面对这样突如其来的变化.....


IMG_1487.jpg


之后的日子,总感觉广州的天没有了曾经那般蓝天白云的美丽,只觉得它刺眼且讨厌,每天总感觉身体加倍的疲惫,每一天都在思考的是到底做错了什么会导致现在的模样,当然也没有了当初那般谈笑风生的快乐.....,与同期生朋友的谈话聊天也变的心不在焉,乏味无趣,像那迷失的候鸟一般,找不到任何的归宿。


就这样持续了一个月,我还是鼓起勇气去了医院,果然是重度抑郁,医生建议回家修养,可我那从来走出过一亩三分地的父母又如何才能理解抑郁症的由来呢?


就这样我每天如同僵尸一样摇摇晃晃,来回走动.....


硬撑到年底,我向领导提了离职,也该离开我喜欢的南方城市了.....


回到北方,熟悉的父母,熟悉的冬日白雪,妹妹也在过年时嫁为人妻。


IMG_1484.jpg


之后的事情,想必大家都清楚,2019年年底,我们在新闻里看着新冠肺炎的消息,那么遥远,却没想到短短一个月,那么迅速就来到了我们身边。


我沉浸在父母和家乡的冬日里,实则是在逃避人生的下一段旅程,每当夜幕降临,我一遍又一遍打开Boss,脉脉,听闻着他人的故事,也探寻着自己的故事。


过年之后,到了4月,我知道,我得再次出发,就像当时刚毕业时那种对未来的美好期待一般。


最终我选择了离家不远,但又迎合了我的南方情结的成都。


在1个月内我找到了能够安身立命的前端开发工作,我开着我的小船,再次起航了......


我深深迷恋着夏日成都街头小巷的冰粉,也和很多成都朋友一样,一周必须吃一顿火锅来证明自己是个新成都人,饭桌上的折耳根永远是外地人百思不得其解的食物,川西的四季永远那么耀眼动人,令人向往。


IMG_1471.JPG


IMG_1472.JPG


IMG_1473.JPG


IMG_1474.JPG


2023年的5月,我也在这个巴适的城市和一位姑娘牵手走入了婚姻殿堂,我们都深爱着彼此,也深爱着我们几乎每周都要前往的川西。


如今的自己,也褪去了曾经的那份稚嫩,迷茫,渐渐在这片地方扎根,生长。


如今的工作也是朝九晚六,三天居家,二两办公室,wlb的日子才真正让人觉得人生值得走一遭。


平日里刷刷freecodecamp和油管上的技术视频,练习练习算法防止脑瓜壳生锈,和香港技术官探讨探讨技术,好让项目不至于那么难以维护,偶尔也用工作用的全栈技术(MERN)帮老婆写一两个小网站,谁让四川的男人都是耙耳朵呢。


作者:Aoda
来源:juejin.cn/post/7292576188017967116
收起阅读 »

有财务自由的思维,才能实现财务自由!

前两天在洋哥、竹子姐以及渡心总等大佬的带领下,第一次体验了穷爸爸富爸爸的作者研发的现金流游戏,收获颇丰! 游戏规则说明 心灵创富 现金流游戏分为三步: 一局游戏,时间两个小时;总结分享时刻;以及,最最重要的结合自己的生活,复盘自己关键时间点的选择,是否是符合财...
继续阅读 »

前两天在洋哥、竹子姐以及渡心总等大佬的带领下,第一次体验了穷爸爸富爸爸的作者研发的现金流游戏,收获颇丰!


游戏规则说明


心灵创富 现金流游戏分为三步:


一局游戏,时间两个小时;总结分享时刻;以及,最最重要的结合自己的生活,复盘自己关键时间点的选择,是否是符合财务自由的决策。


首先,说一下明面上的游戏规则:每个人都选择一张身份卡,这张身份卡决定了你的工资,还有每个月的现金流。你的身份可能是小学老师、飞机驾驶员、医生等等,他们月工资和现金流(每月结余)各不一样。


老鼠圈,所有玩家没实现财务自由之前都在老鼠圈。


游戏过程中,通过投掷骰子,可以有五种操作:



  1. 市场风云:变幻莫测的市场,之前不值钱的突然变得值钱,之前值钱的东西也会突然变得不值钱;还有金融政策可能会随时调整,借钱利率忽高忽低等。

  2. 小买卖:开小店挣小钱等。

  3. 大机会:买卖股票、房产等能带来大额现金流的操作。

  4. 意外支出:生孩子、买球拍、买游艇等。

  5. 领工资:领取一个月的现金流,比如:月工资:3300,月支出:2100,则每月现金流:3300-2100=1200。


整个游戏的目标分为两个阶段:第一个阶段,突破老鼠圈,实现财务自由;第二个阶段,实现梦想。


怎么实现财务自由?股票、房产等带来的非工作收入超过你每个月的支出,就算破圈了,实现了财务自由,游戏进入了下一个阶段。


第一次玩游戏,没有人完成第二阶段,就不说了。


二、游戏


带领银行家:竹子


玩家:海子、木川、天雨、Feli、YY、伍六七


身份:小学老师


月工资:3300 $


月支出:2190 $


月现金流:1110 $



一开始选择身份的时候,虽然想选一个工资高的,但是也没有那么强烈,只是不想选工程师了,毕竟现实中是程序员,游戏中想换个身份活一回。所以,也就比较随意选择了工资偏低的小学教师。


整场游戏,我的运气非常不好。本来月现金流就不高,结果第二轮就生了个娃,后面又陆续抽中了额外支出,让本就没钱的我雪上加霜。


唯一称得上的是机会的就是:可以以 5000 $ 购买 10 亩荒地。但是当时没有考虑到可以向银行贷款,也没有考虑到可以向其他玩家借款,最终没有购买。


没有考虑到向外部借钱的一部分原因是第一次玩这个游戏,不知道(忘记了)这个规则,另一个原因也是自己平时生活中也是这样一个人,除非迫不得已不向他人借钱。


第一次玩这个游戏,本着要恪守:10% - 20% 的钱投资高风险的产品(如股票),50% 的钱投资低风险的产品(当前最低价的股票、高收益的房产等)。


所以,我也做了两笔投资: 1000 $ 让木川代持的基金, 500 $ 让海子代持的股票。这让我在运气不好的一生中有一定的概率能破圈,虽然最后这两笔投资没有兑现,但是这两笔投资本身,我认为是没有问题的。


另外,太守规则。银行家一开始说的规则是不让给其他人提供决策建议,否则罚款。后来才知道,是可以向其他人提供付费资讯服务的,这种是不受惩罚的。


前两轮 FYY 想向我咨询决策建议,我直接就拒绝了。但是一个是给他人提供建议,可以给其他人好感,可以链接其他人。另外一个,就算有罚款,咨询的人也可以给予相应的补偿。这样就可以相互链接,相互成全。也应该思考,不破坏规则的情况下的有哪些选择。


整场游戏中,印象最深刻的一笔交易是:5000 $ 购买 10 亩荒地


我最终是没有购买,当时第一是看手里的现金,不满足购买要求。


第二是觉得这是一个可以搏一搏的机会,但是手里的现金不满足给自己制定的 10% -20% 投资高风险产品的 rule。


所以,问了在场的玩家,是否有需要这个机会的?拍卖 1000 $。最终,只有海子出价 500 $。当时,我认为这个机会价值还是挺大的,海子本质上是一个愿意花钱投资的人,所以拒绝了。


之后思考,海子是一个愿意投资机会的人,但是他当时手里有几笔房子的贷款,而且现金也不充足,所以没有购买这个机会。我只考虑了海子的性格,没有考虑他当时手里的资金,所以,错失了这笔交易。


游戏和自己现实生活中的关系


什么才算财务自由?现实中很多人说有多少多少百万,有说 500 W的,有说 1000 W的。但是这个游戏告诉我们:只要你的非工作收入超过了你的支出,你就实现了财务自由。跟你手里有多少现金是没有关系的。


唯一的目标就是,增加你的非工作收入,减少你的支出,让你的非工作收入超过你的支出。


这个游戏带给我的收获和启发


人这一生,有的人运气好,能碰到很多次大机会。有的人运气不好,可能一生也没什么机会。


不管如何,你需要在一开始就制定好你做决策的依据。这个依据就是你手里的现金流以及你能承受的风险。


你不应该拿手里的大部分现金去投资一个低收益率高风险的产品,但是也不应该守着一大笔资金不做任何投资。


这个决策依据能够让你在没机会的时候,不至于很快破产。也能让你在有机会的时候,能够快速收获第一桶金,实现财务自由。


对我现实的改变



  1. 正在整理自己的权益表和资产负债表,慢慢减少不必要的支出。

  2. 正在实践做自己的个人 IP,增加自己的非工作收入。

  3. 游戏中的小买卖、大机会,在现实中去寻找这样的信息。游戏中,大家都知道且能知道是小买卖还是大机会,但是现实中,你可能不知道什么是小买卖,什么是大机会。

  4. 在心里植入增加非工作收入,减少支出,实现财务自由这样的理念。


应该去践行的



  1. 与现实决策点结合

  2. 早日争取第一桶金

  3. 结果导向

  4. 自己反思,反思自己的不足

  5. 持续来,每次来会有不同的体验,牌面不同,选择不同

  6. 应该多做利他的事情,资源链接

  7. 玩家之间可以互相赋能

  8. 有资源的时候可以投资机会,购买机会

  9. 整理自己的资产负债表

  10. 多分享,清晰自己的认识

  11. 重复玩,玩到财务自由

  12. 映射现实中,改善自己的财务状态

  13. 运气不好,心态也要好,积极链接他人,才能保住底线的情况下,去创造更大的成功机会


作者:程序员伍六七
来源:juejin.cn/post/7293477092259201059
收起阅读 »

自研框架跻身全球 JS 框架榜单,排名紧随 React、Angular 之后!

web
前言 终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语...
继续阅读 »

前言


终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语法的全面支持,使得 Strve 在代码智能提示和代码格式化方面更加友好,进一步提高了开发效率。


介绍


相信有些小伙伴没有听说过 Strve 到底是什么,那我这里就大体介绍一下。


Strve 是一个可以将字符串转换为视图(用户界面)的 JavaScript 库。Strve 不仅易于使用,而且可以灵活地拆解不同的代码块。使用模板字符串开发用户界面,主要是利用 JavaScript 的能力,只关注 JavaScript 文件。Strve 又是一个易用性的 JavaScript 框架,它提供了很多实用的功能与生态工具。


我们可以通过一些简单的示例来了解 Strve 的使用方法。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Strve.js</title>
</head>

<body>
<script src="https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full.prod.js"></script>
<script>
const { html, setData, createApp } = Strve;
const state = {
count: 0,
};

function add() {
setData(() => {
state.count++;
});
}

function App() {
return html`<h1 onClick=${add}>${state.count}</h1>`;
}

const app = createApp(App);
app.mount('#app');
</script>
</body>
</html>

在上述代码中,我们通过引入 Strve 库,并使用 createApp 方法创建了一个 App 组件,然后通过 mount 方法挂载到页面上,这里的 App 组件就是通过模板字符串来定义的。这样就可以在 JS 代码中编写用户界面,是不是很方便呢?我们发现,在模板字符串中,我们使用 ${} 来引用数据,并且使用 onClick 方法来绑定事件。这样就可以实现一个计数器的功能。


除了这种简单的示例,Strve 还支持很多复杂的功能,我们可以使用 JSX 语法来编写组件,也可以使用函数式组件来编写组件,还可以使用组件来编写组件,甚至可以编写一些自定义的组件。


如果想了解更多关于 Strve 的信息,稍后可以到文章末尾处查阅官方文档。


性能评估


我们既然发布了 Strve,那么肯定需要对其性能进行评估,我们评估的工具就用js-framework-benchmarkjs-framework-benchmark 是什么?我们这里就简单介绍下 js-framework-benchmark,它是一个用于比较 JavaScript 框架性能的项目。它旨在通过执行一系列基准测试来评估不同框架在各种场景下的性能表现。这些基准测试包括渲染大量数据、更新数据、处理复杂的 UI 组件等。通过运行这些基准测试,可以比较不同框架在各种方面的性能优劣,并帮助开发人员选择最适合其需求的框架。js-framework-benchmark 项目提供了一个包含多个流行 JavaScript 框架的基准测试套件。这些框架包括 Angular、React、Vue 等。每个框架都会在相同的测试场景下运行,然后记录下执行时间和内存使用情况等性能指标。通过比较这些指标,可以得出不同框架的性能差异。这个项目的目标是帮助开发人员了解不同 JavaScript 框架的性能特点,以便在选择框架时能够做出更加明智的决策。同时,它也可以促进框架开发者之间的竞争,推动框架的不断改进和优化


在评估之前,我们必须要了解 js-framework-benchmark 中有两种模式。一种是 keyed,另一种是 non-keyed。在 js-framework-benchmark 中,"keyed" 模式是指通过给数据项分配一个唯一标识符作为 "key" 属性,从而实现数据项与 DOM 节点之间的一对一关系。当数据发生变化时,与之相关联的 DOM 节点也会相应更新。而 non-keyed 模式是指当数据项发生变化时,可能会修改之前与其他数据项关联的 DOM 节点。


因为 Strve 支持keyed模式,所以我们将使用此模式来评估 Strve 的性能。


对以下操作进行了基准测试:



  • 创建行:页面加载后创建 1,000 行的持续时间(无预热)。

  • 替换所有行:替换表中所有 1,000 行的持续时间(5 次预热迭代)。

  • 部分更新:对于具有 10,000 行的表,每 10 行更新一次文本(进行 5 次预热迭代)。

  • 选择行:响应单击该行而突出显示该行的持续时间。 (5 次预热迭代)。

  • 交换行:在包含 1,000 行的表中交换 2 行的时间。 (5 次预热迭代)。

  • 删除行:删除具有 1,000 行的表的行的持续时间。 (5 次预热迭代)。

  • 创建多行:创建 10,000 行的持续时间(无预热)

  • 将行追加到大型表:在包含 10,000 行的表中添加 1,000 行的持续时间(无预热)。

  • 清除行:清除填充有 10,000 行的表的持续时间。 (无热身)

  • 就绪内存:页面加载后的内存使用情况。

  • 运行内存:添加 1,000 行后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 替换内存:点击 5 次创建 1000 行后的内存使用情况。

  • 重复清除内存:创建并清除 1,000 行 5 次后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 启动时间:加载和解析 javascript 代码以及渲染页面的持续时间。

  • 持续交互:灯塔指标 TimeToConstantlyInteractive:悲观 TTI - 当 CPU 和网络都非常空闲时。 (不再有超过 50 毫秒的 CPU 任务)

  • 脚本启动时间:灯塔指标 ScriptBootUpTtime:解析/编译/评估所有页面脚本所需的总毫秒数

  • 主线程工作成本:灯塔指标 MainThreadWorkCost:在主线程上工作所花费的总时间包括样式/布局等。

  • 总字节权重:灯塔指标 TotalByteWeight:加载到页面中的所有资源的网络传输成本(压缩后)。


对于所有基准测试,都会测量持续时间,包括渲染时间。


因为js-framework-benchmark是一个自动化测试的工具,只需要符合标准的代码就可以进行测试。Strve 支持 JSX 语法,所以我们将使用 JSX 语法来编写测试代码。


import { setData, createApp } from 'strve-js';
import { buildData } from './data.js';

let selected;
let rows = [];

function setRows(update = rows.slice()) {
setData(
() => {
rows = update;
},
{
name: TbodyComponent,
}
);
}

function add() {
const data = rows.concat(buildData(1000));
setData(
() => {
rows = data;
},
{
name: TbodyComponent,
}
);
}

function remove(id) {
rows.splice(
rows.findIndex((d) => d.id === id),
1
);
setRows();
}

function select(id) {
setData(
() => {
selected = id;
},
{
name: TbodyComponent,
}
);
}

function run() {
setRows(buildData());
selected = undefined;
}

function update() {
for (let i = 0; i < rows.length; i += 10) {
rows[i].label += ' !!!';
}
setRows();
}

function runLots() {
setRows(buildData(10000));
selected = undefined;
}

function clear() {
setRows([]);
selected = undefined;
}

function swapRows() {
if (rows.length > 998) {
const d1 = rows[1];
const d998 = rows[998];
rows[1] = d998;
rows[998] = d1;
setRows();
}
}

function TbodyComponent() {
return (
<tbody>
{rows.map((item) => (
<tr class={item.id === selected ? 'danger' : ''} data-label={item.label} key={item.id}>
<td class='col-md-1'>{item.id}</td>
<td class='col-md-4'>
<a onClick={() => select(item.id)}>{item.label}</a>
</td>
<td class='col-md-1'>
<a onClick={() => remove(item.id)}>
<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>
</a>
</td>
<td class='col-md-6'></td>
</tr>
))}
</tbody>

);
}

function MainBody() {
return (
<fragment>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Strve-keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='run' onClick={run}>
Create 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='runlots'
onClick={runLots}
>

Create 10,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='add' onClick={add}>
Append 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='update'
onClick={update}
>

Update every 10th row
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='clear' onClick={clear}>
Clear
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='swaprows'
onClick={swapRows}
>

Swap Rows
</button>
</div>
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<component $name={TbodyComponent.name}>{TbodyComponent()}</component>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden='true'></span>
</fragment>

);
}

createApp(() => MainBody()).mount('#main');

以下页面就是将进行基准测试的页面:


01.png


我们大体看下测试过程,我们将使用动图来展示页面效果,这样会觉得更加直观。


02.gif


最终,Strve 通过了压力测试!


08.png


基准测试结果


既然我们通过测试,我们就需要提交到js-framework-benchmark官方项目中,进行综合评估,与全球其他框架进行比较。


我们提交的 PR 在 2023 年 9 月 18 号被作者合并了。


03.png


在接下来的时间里,作者进行了一系列的测试。最终,Chrome 118 版本于上周发布,并在 GitHub 上公布了官方的测试结果。


04.png


我们打开下面的网址,看下 Strve 的官方测试结果:


krausest.github.io/js-framewor…


经过查询,全球 JavaScript 框架榜单中共有 142 个框架。


性能测试基准分为三类:



  • 持续时间

  • 启动指标

  • 内存分配


【持续时间】


在此测试基准中,Strve 平均值 1.42,排名第 90 位。


React、Angular 和 Vue,平均值分别为1.401.381.20,分别排名第 85 位、第 83 位和第 51 位。



平均值越小,排名则越靠前。颜色越绿代表越优。



05.png


【启动指标】


在此测试基准中,Strve 平均值 1.07


React、Angular 和 Vue,平均值分别为 1.681.801.30



平均值越小,排名则越靠前。颜色越绿代表越优。



06.png


【内存分配】


在此测试基准中,Strve 平均值 1.33


React、Angular 和 Vue,平均值分别为 2.462.821.86



平均值越小,排名则越靠前。颜色越绿代表越优。



07.png


新特性


我们在上面的测试中,可以看到 Strve 性能表现非常不错。


这次我们发布的大版本号为 6.0.2,我们将这个具有里程碑意义的大版本命名为 Strve6,而 “Strve6,从芯出发!” 这个口号正是 Strve6 的核心理念。这一版本象征着我们从底层技术出发,致力于为用户提供更优质、更高效的开发体验。


此次版本我们在性能与体验之间做了权衡。在源码层面,我们将普通 Diff 算法升级为 双端 Diff 算法,大大提升了性能。另外,我们在用户体验层面也做了很大的改进。


这里,我们提到了双端 Diff 算法,我们在面试中经常提到这个概念,但是很少用到实际项目中去。那么,为了更好地理解双端 Diff 算法如何提高性能,我们来看一个关于 Strve 简单的示例。


我们来遍历一个数组,并且每次点击按钮,往数组头部中添加一个元素。


【普通 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,都会重新渲染整个列表。这样是肯定耗损浏览器性能的。


09.gif


【双端 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li key=${todo}>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,仅添加必要的元素,而不是重新渲染整个列表。这是因为我们在每个列表项中添加了 key 属性,并且这个 key 是唯一的。key 这个特殊的 attribute 主要作为 Strve 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。只要标签类型与 key 值都相等,就说明当前元素可以被复用。


10.gif


热门话题


文章接近尾声,让我们来回顾一下最近社区的几个热门话题。



  1. 为什么要开发这个框架?初衷是什么?


答:其实,我的动机特别简单,完全受 JSX 语法的影响。刚接触 JSX 语法的时候,就被它那种魔法深深地吸引住了,可以在 JS 中写 HTML。所以,我就想我自己可不可以也搞一个类似 JSX 语法的库或者框架呢!一方面可以锻炼自己的代码能力,另一方面体验开发框架的整个流程,也方便我以后更全面的学习其他框架(Vue.js、React.js 等)。


做自己喜欢的事情是特别有意义的!



  1. 为什么选择 Strve 作为框架的名字?


答:Strve 最初定位是可以将字符串转换为视图(用户界面)的 JavaScript 库,所以是由 StringView 两个单词缩减组成的新单词。



  1. 跟前端热门框架比较,是想超过它们吗?


答:不是,我主要是想学习一下前端热门框架的实现原理,然后自己实现一个框架。有一句话说得好:“只有站在巨人的肩膀上才能望得更远!”。



  1. 记得之前也写过登上框架榜单的文章,这次为什么还要写?


答:之前,Strve 测评的模式是使用"non-keyed"。现在,Strve 新的版本支持"keyed"模式,所以,我重新写了一篇文章,来介绍 Strve 的新特性。



  1. Strve 6.0.2 版本发布,普通 Diff 算法升级为双端 Diff 算法,可以简单讲下双端 Diff 算法的概念吗?


答:双端 diff 算法就是头尾指针向中间移动,分别判断头尾节点是否可以复用,如果没有找到可复用的节点再去遍历查找对应节点的下标,然后移动。全部处理完之后要对剩下的节点进行批量的新增和删除。



  1. Strve 是个 JavaScript 库还是 JavaScript 框架?


答:首先,我们来看下框架与库有什么区别?库更多是一个封装好的特定的集合,提供给开发者使用,而且是特定于某一方面的集合(方法和函数),库没有控制权,控制权在使用者手中,在库中查询需要的功能在自己的应用中使用,我们可以从封装的角度理解库;框架顾名思义就是一套架构,会基于自身的特点向用户提供一套相当于叫完整的解决方案,而且控制权的在框架本身,使用者要找框架所规定的某种规范进行开发。Strve 可以是框架,因为 Strve 提供了路由、插件等生态工具;Strve 也可以是库, 因为 Strve 可以单独作为一个渲染库。



  1. Strve 你还要继续维护下去吗?


答:是的,我还会继续维护下去,因为我也想学习下去,也希望能帮助到更多前端开发者。


关于


Strve 我是从 2021 年下半年开始开发,到现在也快两年了。在这两年中,从一个之前只会 调用 API 的码农,到现在可以独立开发一个框架,让我收获了很多。学习了如何去分析一个框架的实现原理,也学习了如何去设计一个框架。



Strve 源码仓库:github.com/maomincodin…


Strve 中文文档:maomincoding.gitee.io/strve-doc-z…



如果大家觉得 Strve 还不错,麻烦帮我点下 Star 吧,谢谢!


结语


感谢各位读者的阅读,希望本文能对你有所帮助,如果喜欢本文,欢迎点赞,欢迎关注!


最后,分享一段话给大家:



很多时候


不是有希望才去坚持


而是在坚持的过程中慢慢看到希望


我们都是在暗夜里赶路的人


纵使满身疲惫也不肯轻言放弃


愿你所坚持的东西


终有一天反过来拥抱你



作者:前端历劫之路
来源:juejin.cn/post/7293786784127025179
收起阅读 »

【JavaScript】【表达式和运算符】instanceof

web
前言 在JavaScript中,判断变量的类型,常常使用的是typeof运算符 typeof的痛点: 所有的引用类型结果都是 Object; 空值null的结果也是Object; 为此,引入 instanceof 一、instanceof 1.1 作用 ...
继续阅读 »

前言


在JavaScript中,判断变量的类型,常常使用的是typeof运算符


typeof的痛点



  • 所有的引用类型结果都是 Object

  • 空值null的结果也是Object


image.png


为此,引入 instanceof


一、instanceof


1.1 作用



  • 用于判断某个实例是否属于某构造函数

  • 在继承关系中,用来判断一个实例是否属于它的父类型或祖先类型的实例


1.2 使用




  • 语法object instanceof constructor




  • 参数



    • object:某个实例对象

    • constructor:某个构造函数




  • 示例:




// 类
class Maomi {} // 定义类
let fuLai = new Maomi() // fuLai是Maomi类的实例对象
fuLai instanceof Maomi // true

// 时间
new Date() instanceof Date // true

// 构造函数
function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi // true

// 函数
function getMaomi() {}
getMaomi instanceof Function // true

1.3 涉及的构造函数



  • 基础类型:String、Number、 Boolean、 Undefind、Null、Symbol

  • 引用类型:Object(Array、RegExp、Date、Function...)


1.3 实现原理


instanceof 的内部实现机制是:通过判断对象的原型链上是否能找到对象的 prototype,来确定 instanceof 返回值。


function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L)
return true;
L = L.__proto__;
}
}

代码释义

① L表示对象实例,R表示构造函数或者父类型实例

② 取R的显式原型,取L的隐式原型

③ 循环遍历,进行判断②中的两个值是否相等,相等返回true,不相等继续查找L的原型链


instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。



  • 示例:


function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi

观察fulai.__proto__SetMaomi.prototype的结构:

image.png


image.png



注意点fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 trueSetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false



二、instanceof产生继承关系


function Cat(name,age,type){
this.name = name;
this.age = age;
this.type = type;
}
function YingDuan(name,age,type,sex){
Cat.call(this,name,age,type);
this.sex = sex;
}
YingDuan.prototype = new Cat(); // 这里改变了原型指向,实现继承
var yd = new YingDuan("有鱼",2,"金渐层","男"); //创建了英短对象yd
console.log(yd instanceof YingDuan); // true
console.log(yd instanceof Cat); // true
console.log(yd instanceof Object); // true

下面为了直观的观察,我就不采用循环的方式,直接一个一个的打印出来:



  • yd instanceof YingDuan:

    image.png

  • yd instanceof Cat:

    image.png

  • yd instanceof Object:

    image.png


三、注意问题



  1. fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 true
    SetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false

  2. instanceof 用于判断对象类型,但以下情况的结果都为false,请注意。


console.log(Number instanceof Number)  // false
console.log(String instanceof String) // false
console.log(null instanceof Object) // false,null不具有任何对象的特性,也没有__proto__属性

参考



作者:旺仔小猪
来源:juejin.cn/post/7293348107517001739
收起阅读 »

解决Android卡顿性能瓶颈的深度探讨

在移动应用开发中,Android卡顿是一个常见但令人讨厌的问题,它可能导致用户体验下降,甚至失去用户。本文将深入探讨Android卡顿的原因,以及如何通过代码优化和性能监测来提高应用的性能。 卡顿现象 卡顿是指应用在运行时出现的明显延迟和不流畅的感觉。这可能包...
继续阅读 »

在移动应用开发中,Android卡顿是一个常见但令人讨厌的问题,它可能导致用户体验下降,甚至失去用户。本文将深入探讨Android卡顿的原因,以及如何通过代码优化和性能监测来提高应用的性能。


卡顿现象


卡顿是指应用在运行时出现的明显延迟和不流畅的感觉。这可能包括滑动不流畅、界面响应缓慢等问题。要解决卡顿问题,首先需要了解可能导致卡顿的原因。


卡顿原因


主线程阻塞


主线程负责处理用户界面操作,如果在主线程上执行耗时任务,会导致界面冻结。


public void doSomeWork() {
// 这里执行耗时操作
// ...
// 下面的代码会导致卡顿
updateUI();
}

内存泄漏


内存泄漏可能会导致内存消耗过多,最终导致应用变得缓慢。


public class MyActivity extends AppCompatActivity {
private static List<SomeObject> myList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
// 向myList添加数据,但没有清除
myList.add(new SomeObject());
}
}

过多的布局层次


复杂的布局层次会增加UI绘制的负担,导致卡顿。


<RelativeLayout>
<LinearLayout>
<ImageView />
<TextView />
<!-- 更多视图 -->
</LinearLayout>
</RelativeLayout>

大量内存分配


频繁的内存分配与回收,会导致性能下降,发生卡顿。


// 创建大量对象
List<Object> objects = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
objects.add(new Object());
}

优化策略


使用异步任务


避免在主线程上执行耗时操作,使用异步任务或线程池来处理它们。
协程提供了一种更清晰和顺序化的方式来执行异步任务,并且能够很容易地切换线程



// 创建一个协程作用域
val job = CoroutineScope(Dispatchers.IO).launch {
// 在后台线程执行后台任务
val result = performBackgroundTask()

// 切换到主线程更新UI
withContext(Dispatchers.Main) {
updateUI(result)
}
}

// 取消协程
fun cancelJob() {
job.cancel()
}

suspend fun performBackgroundTask(): String {
// 执行后台任务
return "Background task result"
}

fun updateUI(result: String) {
// 更新UI
}

在此示例中,我们首先创建一个协程作用域,并在后台线程(Dispatchers.IO)中启动一个协程(launch)。协程执行后台任务(performBackgroundTask),然后使用withContext函数切换到主线程(Dispatchers.Main)来更新UI。


内存管理


确保在不再需要的对象上及时释放引用,以避免内存泄漏。


public class MyActivity extends AppCompatActivity {
private List<SomeObject> myList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
myList.add(new SomeObject());
}

@Override
protected void onDestroy() {
super.onDestroy();
myList.clear(); // 清除引用
}
}

精简布局


减少不必要的布局嵌套,使用ConstraintLayout等优化性能的布局管理器。


<ConstraintLayout>
<ImageView />
<TextView />
<!-- 更少的视图层次 -->
</ConstraintLayout>

使用对象池


避免频繁的内存分配和回收。尽量重用对象,而不是频繁创建新对象。
使用对象池来缓存和重用对象,特别是对于复杂的数据结构。


// 使用对象池来重用对象
ObjectPool objectPool = new ObjectPool();
for (int i = 0; i < 10000; i++) {
Object obj = objectPool.acquireObject();
// 使用对象
objectPool.releaseObject(obj);
}

卡顿监测


Android提供了性能分析工具,如Android Profiler和Systrace,用于帮助您找到性能瓶颈并进行优化。


为了更深入地了解应用性能,您还可以监测主线程处理时间。通过解析Android系统内部的消息处理日志,您可以获取每条消息的实际处理时间,提供了高度准确的性能信息。


for (;;) {
Message msg = queue.next();

final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what)
;
}

msg.target.dispatchMessage(msg);

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}

当消息被取出并准备处理时,通过 logging.println(...) 记录了">>>>> Dispatching to" 日志,标志了消息的处理开始。同样,在消息处理完成后,记录了"<<<<< Finished to" 日志,标志了消息的处理结束。这些日志用于追踪消息的处理时间点。


这段代码对 Android 卡顿相关内容的分析非常重要。通过记录消息的处理起点和终点时间,开发者可以分析主线程消息处理的性能瓶颈。如果发现消息的处理时间过长,就可能导致卡顿,因为主线程被长时间占用,无法响应用户交互。


Looper.getMainLooper().setMessageLogging(new LogPrinter(new String("MyApp"), Log.DEBUG) {
@Override
public void println(String msg) {
if (msg.startsWith(">>>>> Dispatching to ")) {
// 记录消息开始处理时间
startTime = System.currentTimeMillis();
} else if (msg.startsWith("<<<<< Finished to ")) {
// 记录消息结束处理时间
long endTime = System.currentTimeMillis();
// 解析消息信息
String messageInfo = msg.substring("<<<<< Finished to ".length());
String[] parts = messageInfo.split(" ");
String handlerInfo = parts[0];
String messageInfo = parts[1];
// 计算消息处理时间
long executionTime = endTime - startTime;
// 记录消息处理时间
Log.d("DispatchTime", "Handler: " + handlerInfo + ", Message: " + messageInfo + ", Execution Time: " + executionTime + "ms");
}
}
});

这种方法适用于需要深入分析主线程性能的情况,但需要权衡性能开销和代码复杂性。


结语


Android卡顿问题可能是用户体验的重要破坏因素。通过了解卡顿的原因,采取相应的优化策略,利用性能分析工具和消息处理日志监测,您可以提高应用的性能,使用户体验更加流畅。卡顿问题的解决需要不断的监测、测试和优化,通过不断发现与解决卡顿问题,才能让应用更加流畅。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7293342627813425167
收起阅读 »

Shell 命令奇淫技巧,就是有点短

1、在任意目录之间快速移动 你发现自己要在两个或更多目录之间频繁移动,一会切换到这里,一会切换到那里,来回跳转。这些目录之间隔得还挺远,反复输入冗长的路径让人疲惫不堪。 使用内建命令 pushd 和 popd 来管理目录栈,轻松地在目录之间切换。下面是一个简单...
继续阅读 »

1、在任意目录之间快速移动


你发现自己要在两个或更多目录之间频繁移动,一会切换到这里,一会切换到那里,来回跳转。这些目录之间隔得还挺远,反复输入冗长的路径让人疲惫不堪。


使用内建命令 pushd 和 popd 来管理目录栈,轻松地在目录之间切换。下面是一个简单的示例:


$ cd /tmp/tank
$ pwd
/tmp/tank

$
pushd /var/log/cups
/var/log/cups /tmp/tank

$
pwd
/var/log/cups

$
ls
access_log error_log page_log

$
popd
/tmp/tank

$
ls
empty full

$
pushd /var/log/cups
/var/log/cups /tmp/tank

$
pushd
/tmp/tank /var/log/cups

$
pushd
/var/log/cups /tmp/tank

$
pushd
/tmp/tank /var/log/cups

$
dirs
/tmp/tank /var/log/cups

栈是一种后进先出的结构,这两个命令也正是这么做的。如果对一个新目录使用 pushd,那么它会将前一个目录压入栈中。当使用 popd时,它会弹出栈顶保存的当前位置,切换到新的栈顶目录。使用这些命令更改位置时,会从左到右输出目录栈中的值,对应于栈中自顶向下的顺序。


如果使用 pushd 时没有指定目录,那么它会交换栈顶的两个目录的位置,这样就可以重复使用 pushd 命令来实现两者之间的切换。cd命令也能够达到相同效果。


如果不记得目录栈中都有哪些目录,可以使用内建命令 dirs 按照从左到右的顺序显示。加上 -v 选项后,显示形式更形象。


$ dirs -v
0 /opt/yongheng
1 /opt/yongheng/Shell
$

数字可用来调整栈内目录的位置。pushd +1 会将编号为 1 的目录置为栈顶(并切换到该目录)并将其他目录下压。


$  pushd +1
/opt/yongheng/Shell /opt/yongheng

$
dirs -v
0 /opt/yongheng/Shell
1 /opt/yongheng

要想看到类似于栈的目录列表,但又不希望出现编号,可以使用 -p选项。


# dirs -p                                                                                              /opt/yongheng/Shell                                                                                    /opt/yongheng     

2、重复上一个命令


你刚刚输入了一个又长又麻烦的命令,其中包含了冗长的路径名和一堆复杂的参数。现在需要重新执行该命令。难道还得再输入一次?


这个问题有两种解决方法。第一种方法只需要在提示符下输入两个惊叹号,然后 bash 就会显示并重复执行上一个命令。例如:


$  cd /opt/                                                                                            $ !!                                                                                                   cd /opt/  

另一种(更现代的)方法是使用箭头键。按上箭头键会回滚到执行过的上一个命令。如果找到了需要的命令,按下 Enter 键就可以(再次)执行该命令。


3、参数重用


重用上一个命令很简单,使用 !! 就行了,但你需要的未必总是整个命令。如何只重用最后一个参数呢?


用 !$ 指明上一个命令中的最后一个参数。!:1 表示第一个参数,!:2 表示第二个参数,以此类推。


多个命令使用相同的文件名为参数是司空见惯的事情。最常见的场景之一就是程序员编辑源代码文件,然后编译、再编辑,再编译……有了 !$,事情就方便多了。如下:


$ vi /some/long/path/name/you/only/type/once
...
$ gcc !$
gcc /some/long/path/name/you/only/type/once
...
$ vi !$
vi /some/long/path/name/you/only/type/once
...
$ gcc !$
gcc /some/long/path/name/you/only/type/once
...
$

明白其中的意思了吗?这不仅省去了大量的键盘输入,还避免了错误。如果编译时输错文件名,那编译的可就不是刚刚编辑好的源代码文件了。有了 !$,就可以始终得到刚刚用过的文件名。要是想重用的参数位于命令行内部,可以使用带编号的 !: 命令来获取。


4、安全第一,命令测试


一不小心就会输错字符。 (不信你瞧!)即便是简单的 bash 命令,由此带来的后果也非常严重:你会移动错或删错文件。如果再加上模式匹配,结果更让人心跳,因为模式中的输入错误会导致南辕北辙的结果。小心谨慎的用户会怎么做?


可以使用命令历史特性和键盘便捷方式来重复参数,无须从头输入,因此能够减少输入错误。如果要用到棘手的模式来匹配文件,先用echo 测试一下模式能否正常匹配,然后再用 !$ 进行实际操作。例如:


$ ls
ab1.txt ac1.txt jb1.txt wc3.txt

$
echo *1.txt
ab1.txt ac1.txt jb1.txt

$
echo [aj]?1.txt
ab1.txt ac1.txt jb1.txt

$
echo ?b1.txt
ab1.txt jb1.txt

$
rm !$
rm ?b1.txt
$

echo 是检查模式匹配结果的一种方法。一旦确信结果符合预期,就可以将模式用于实际命令。这里我们要删除有特定名称的文件,没人愿意在这种事上犯错。


作者:博学谷_狂野架构师
来源:juejin.cn/post/7187268796753641531
收起阅读 »

压缩炸弹,Java怎么防止

一、什么是压缩炸弹,会有什么危害 1.1 什么是压缩炸弹 压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不...
继续阅读 »

一、什么是压缩炸弹,会有什么危害


1.1 什么是压缩炸弹


压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。


以下是安全测试几种经典的压缩炸弹


graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)

A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。


压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。


压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。



1.2 压缩炸弹会有什么危害


graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px

压缩炸弹可能对计算机系统造成以下具体的破坏:




  1. 资源耗尽:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。




  2. 磁盘空间耗尽:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。




  3. 系统崩溃:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。




  4. 拒绝服务攻击:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。




  5. 数据丢失:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。





重要提示:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。



二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹


2.1 个人有没有方法可以检测压缩炸弹?


有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:


graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)

A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px



  1. 安全软件和防病毒工具(推荐):使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。




  2. 文件大小限制:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。




  3. 文件类型过滤:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。




2.2 Java怎么防止压缩炸弹


在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:


graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px



  1. 解压缩算法的限制:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。




  2. 设置解压缩操作的资源限制:使用Java的java.util.zipjava.util.jar等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。




  3. 使用安全的解压缩库:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。




  4. 文件类型验证和过滤:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。




  5. 异步解压缩操作:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。




  6. 安全策略和权限控制:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。




2.2.1 使用解压算法的限制来实现防止压缩炸弹


在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制来实现防止压缩炸弹。


先来看看我们实现的思路


graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L

style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

实现流程说明如下:




  1. 首先,通过给定的 file 参数创建一个 ZipFile 对象,用于打开要解压缩的 ZIP 文件。




  2. zipFileSize 变量用于计算解压缩后的文件总大小。




  3. 使用 zipFile.entries() 方法获取 ZIP 文件中的所有条目,并通过 while 循环逐个处理每个条目。




  4. 对于每个条目,使用 entry.getSize() 获取条目的未压缩大小,并将其累加到 zipFileSize 变量中。




  5. 如果 zipFileSize 超过了给定的 size 参数,说明解压后的文件大小超过了限制,此时会调用 deleteDir() 方法删除已解压的文件夹,并抛出 IllegalArgumentException 异常,以防止压缩炸弹攻击。




  6. 创建一个 File 对象 unzipped,表示解压后的文件或目录在输出文件夹中的路径。




  7. 如果当前条目是一个目录,且 unzipped 不存在,则创建该目录。




  8. 如果当前条目不是一个目录,确保 unzipped 的父文件夹存在。




  9. 创建一个 FileOutputStream 对象 fos,用于将解压后的数据写入到 unzipped 文件中。




  10. 通过 zipFile.getInputStream(entry) 获取当前条目的输入流。




  11. 创建一个缓冲区 buffer,并使用循环从输入流中读取数据,并将其写入到 fos 中,直到读取完整个条目的数据。




  12. 最后,在 finally 块中关闭 fos 和 zipFile 对象,确保资源的释放。




实现代码工具类


import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class FileBombUtil {

/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/

public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;

/**
* 文件超限提示
*/

public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";

/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/

public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}

fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);

byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}

}

/**
* 递归删除目录文件
*
* @param dir 目录
*/

private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}

}

测试类


import java.io.File;

/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class Test {

public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}

}

三、总结


文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。


文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。


总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。


在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:





  1. 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。




  2. 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。




  3. 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。





作者:独爱竹子的功夫熊猫
来源:juejin.cn/post/7289667869557178404
收起阅读 »

那个热血澎湃的少年,他居然顶不住了!

感谢你阅读本文 那些我曾经觉得无比坚毅,勇敢,富有激情的朋友,几年后我从他们的言谈中看到了疲惫和妥协,不知是在城市的钢筋混泥土中穿梭太久而身心疲惫,还是在那午夜12点的霓虹灯下路过而感到失落。 昨天中午准备午休的时候,学弟发了个消息给我,说在深圳太累了,压力很...
继续阅读 »

感谢你阅读本文


那些我曾经觉得无比坚毅,勇敢,富有激情的朋友,几年后我从他们的言谈中看到了疲惫和妥协,不知是在城市的钢筋混泥土中穿梭太久而身心疲惫,还是在那午夜12点的霓虹灯下路过而感到失落。


昨天中午准备午休的时候,学弟发了个消息给我,说在深圳太累了,压力很大,想离开深圳了,我瞬间惊讶了,“这小伙曾经可是个卷王啊,在学校时在实验室可以从早上7点可以干到晚上12点呀,把我们这些老人卷回宿舍了他都还不回,现在怎么会累了?”



不过后面我仔细想了一想,怎么就不会累呢?


我们的同龄人中,不乏那种家境特别优越的人,有些人去上大学一个月家里可以给四五千的生活费,如果能谈上女朋友,额外还要加两千,毕业后也是一帆风顺的。


不过大部分人家境是十分普通甚至贫穷的,所以无论上学时期还是进入社会参加工作,都没有什么选择的余地,在没有资源,没有背景的情况下,生活都是过得比较艰难的。


就像网上很流行的那句话,“躺又躺不平,卷又卷不赢”。


特别是当下就业环境这么严峻,工作强度又挺强,工资还TM低,所以年轻人的积极性都被打消了,不过能有工作还好,主要是有很多人找不到工作。


可能你在网上看到统计毕业生分平均薪资过万,就业率90%等等类似的报道,但是这与实际情况大相径庭。


我在网络上也认识不少全国各地的高校学生,加上和现实中的很多朋友了解到,现在就业十分困难,无论什么专业,简历基本都石沉大海。


可能你会说,是他自己不努力,在学校没有好好学,所以出来找不到工作怪谁啊,我觉得这样想是不行的,不要以为自己所得到的都是自己努力换来的,比你努力的人多了去了,可能只是你运气稍微好了一点,站在是时代背景好一点,如果把你换到当前这个环境下,你可能还不如别人!


总是有很多成功的人说自己曾经多么难,但是自己依旧不放弃,最后走上了人生的金字塔尖,然后来呵斥现在年轻人怕吃苦,怕累!


但是实际上现在年轻人所经受的苦累并不是他们那时候所受的苦累能比拟的,身体的累尚能恢复,但是心理上的累却压得人喘不过气。


现在的年轻人毕业后好不容易进入一个公司,满怀激情去干,但是现实总是重拳出击,在繁华的都市拿着微薄的收入,加着加不完的班,熬着熬不完的夜,他们比谁都努力,比谁都上进,但是由于他们没有资源,没有阶梯,所以举步维艰。


在面对房价问题,工作压力,裁员,工资缩水等局面,那个坚毅,勇敢,富有激情的人,手心已经不知道攥出了多少汗!


现在的整个经济形势应该是最难的时刻了,那些你看似光鲜亮丽的人,实际上没有你想象的那么好,大家都比较难。


无论是大城市还是小城市,目前都是比较困难的,所以不要以为在大城市卷不动了回小城市就能轻轻松松,哪怕你回到村里都是很卷的。


在这样的局势下我们能苟住就先苟住,不要去触碰风险。


除了工作外我们应该还要注重生活,只有好好生活我们的身心才能更加健康开朗,工作永远是做不完的,而生活是越过越少的。


保持居安思危的意识,越是艰难的时候越要提起精神,积极探索新事物,别固步自封,因为可能现在看似没用的东西在关键时刻能曲线救国,实现人生的第二春!


今天的分享就到这里,感谢你的观看,我们下期见!


作者:刘牌
来源:juejin.cn/post/7293411651407904803
收起阅读 »

学弟说他面试时被问到了HashMap,差点就遭老罪了

面试官:小伙子,了解HashMap吗? 学弟:哎呦,你干嘛~ 真的问这个呀.... 面试官:呦,练习时长两年半?待会答不上来,你可就遭老罪喽! 那行吧,那开始吧...唱跳rap篮球🏀...... 一、HashMap的底层结构 说一下你理解的HashM...
继续阅读 »

面试官:小伙子,了解HashMap吗?


学弟:哎呦,你干嘛~ 真的问这个呀....


面试官:呦,练习时长两年半?待会答不上来,你可就遭老罪喽!



在这里插入图片描述



那行吧,那开始吧...唱跳rap篮球🏀......



一、HashMap的底层结构



说一下你理解的HashMap底层?



hashMap是由数值和链表组合而成的数据结构,存储为key Value形式。


在java7中叫entry,数据形式为数组+链表。java8中叫Node,数据形式为数组+链表+红黑树(当链表长度大于8时转为红黑树)。


每一个节点都会保存自身的hash、key、value、以及next属性指向下一个节点。


在这里插入图片描述


二、为什么使用数组+链表数据结构



你刚提到了使用数组+链表,可以讲讲为什么使用这个结构吗?



HashMap内部使用数组来存储键值对,这个数组就是 HashMap 的主体。


在这里插入图片描述


在数组中存储的每个位置上,可能会有多个键值对,这些键值对通过链表的形式链接在一起。


在这里插入图片描述


使用数组+链表的数据结构是为了解决散列表中的键冲突问题。在散列表中,每个键都会被映射到一个桶中,但是不同的键可能会被映射到同一个桶中,这种情况被称为键冲突。


为了解决键冲突问题,HashMap 采用了链表的形式将所有映射到同一个桶中的键值对链接在一起,这样就可以通过遍历链表来查找指定键的值当链表长度过长时,查找效率就会下降,因此在链表长度超过一定阈值(8)后,HashMap会将链表转换为红黑树,以提高查找效率


同时,数组的优势在于支持通过下标快速访问元素,因此HashMap可以将每个桶的位置映射到数组的一个元素上,通过下标访问该元素即可访问到对应的链表或红黑树


我们都知道:数组的查询效率很高,添加和删除的效率低。链表的查询效率很低,添加和删除的效率高。


因此:使用数组加链表形式,不仅可以解决散列表中的键冲突问题,且数组的查询效率高、链表的添加和删除效率高。结合在一起,增删查效率都很高


请添加图片描述



嗯,确实不错。不愧是练习时长两年半的程序员.....



三、数组+链表+红黑树



你刚说数组+链表+红黑树,什么情况下会转化红黑树?什么情况下转数组呢?



链表中元素过多,会影响查找效率,当其个数达到8的时候转换为红黑树。红黑树是平衡二叉树,在查找性能方面比链表要高


当红黑树的节点数小于等于6时,红黑树转换为链表,是为了减少内存开销


需要注意的是:将链表转换为红黑树、红黑树转换为链表的操作会影响HashMap的性能,因此需要尽可能避免这种情况的发生。同时,当HashMap中的元素数量较小时,不会出现链表转换为红黑树的情况,因此使用HashMap时,可以考虑在元素数量较少的情况下使用HashMap,以提高性能。


在这里插入图片描述


四、头插法和尾插法



说一下什么是头插法,什么是尾插法?



哇,这不是为难我胖虎吗?啥是头插法?啥是尾插法?


在这里插入图片描述


4.1、头插法


顾名思义,头插法就是新增元素时,放在最前面嘛。


举个栗子🌰,楼主画了一个简单的框框。用来表示原有存储顺序依次为1、2、3的数组。
在这里插入图片描述


假设现在加入了一个4,如果使用头插法,就会变为4123。


在这里插入图片描述


4.2、尾插法


同样道理,尾插法就是新增元素时,放在最后面。


还是原有存储顺序依次为1、2、3的数组。
在这里插入图片描述
假设现在加入了一个4,如果使用尾插法,就会变为1234。


在这里插入图片描述



头插法为什么要调整为尾插法呢?



为什么头插法要调整为尾插法?这是个好问题!!!
请添加图片描述


java7中使用头插法,新来的值会取代原有的值,原有的值就顺推到链表中。在这种情况下,引用关系可能会乱掉,严重会造成死循环。java8使用尾插法,把元素放到最后,就不会出现这种情况。


五、HashMap如何运算存储索引



向一个hashMap中存入数据时,如何知道数据放在哪个位置呢?



当向一个hashMap中存入数据时,会先根据key的哈希值决定放在数组中哪个索引位置。



Hash公式:index = HashCode(Key) & (Length - 1)



如果数组中该索引位置是空的,直接将元素放入,如果该索引位置已经存在元素了,就根据equals方法判断下已有的元素是否和我们新放入的元素是同一个,如果返回true是同一个,则覆盖掉。不是同一元素则在原有元素下面使用链表进行存储


每个元素都有一个next属性指向下一个节点(数组+链表)


    /**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
.........
}

六、HashMap初始化、扩容



嗯,你知道HashMap默认初始化大小是多少吗?还有它的扩容?



HashMap默认初始化容量大小是16,最大容量为2的30次方,负载因子是0.75


在这里插入图片描述


扩容时,会把原有数组中的值取出再次hash到新的数组中(长度扩大以后,Hash的规则也随之改变),因此性能消耗也相对较大。


当HashMap中的元素数量超过负载因子(默认为 0.75)乘以数组长度时,就会触发扩容操作,将数组长度增加一倍,并重新计算每个元素在新数组中的位置。


七、hash碰撞是什么



你听说过hash碰撞吗?



hash碰撞就是不同的Key,经过同一散列算法之后得到的hashCode值相同。


hashCode不同,key一定不同。hashCode相同,key却不一定相同。


当两个key的hashCode()返回值不同时,它们对应哈希表索引也一定不同。不同的key对象,即使它们包含相同的属性、值或状态,它们的hashCode()返回值也是不相同的。


在这里插入图片描述


当两个key的hashCode()返回值相同时,它们可能对应同一个哈希表索引,但它们并不一定相等。在哈希表中,不同的key可能会产生相同的哈希值(哈希碰撞)。


因此,当 key的hashCode相同时,还需要比较key的相等性。需要调用key的equals() 方法来判断它们是否相等。只有当hashCode相等,且equals方法返回true时。才可以认为这两个key相等


八、如何解决hash碰撞



解决hash碰撞的方法有哪些呢?



在哈希表中,哈希碰撞可能会导致性能下降或者安全问题。


常见的解决方法有:


1、开放地址法:在发生哈希碰撞时,通过一定的算法在哈希表中寻找一个空闲的位置,并将元素插入该位置。


2、链式哈希表:在每个哈希表的元素位置上,存储一个链表,哈希碰撞时,将元素插入到相应的链表中。


3、再哈希法:如果一个哈希函数产生的哈希值发生了碰撞,就再次使用另一个哈希函数计算哈希值。


4、负载因子调整:通过调整哈希表的容量、负载因子等参数,可以减少哈希碰撞的发生。


九、HashMap为什么线程不安全



HashMap线程安全吗?为什么?



HashMap是非线程安全的。在多线程环境下,如果多个线程同时修改HashMap中的数据,就可能会导致数据的不一致性。


说白了就是没加锁。


在这里插入图片描述


当多个线程同时调用HashMap的put()方法,一旦他们计算出的hash值相同,就会发生冲突,导致数据被覆盖。


所以,对于多线程并发访问的情况,建议使用线程安全的Map实现


例如ConcurrentHashMap,或者使用Collections.synchronizedMap()方法将HashMap包装成一个线程安全的Map


十、HashMap、HashTable、ConcurrentHashMap的区别



最后一个问题:说一下HashMap、HashTable、ConcurrentHashMap的区别?



麻了! 真的麻了....救救孩子吧....


在这里插入图片描述


HashMap、HashTable、ConcurrentHashMap都是Java中常用的哈希表实现。


区别主要在以下几个方面:


1、线程安全性:HashTable是线程安全的,HashMap是非线程安全的,ConcurrentHashMap通过分段锁的方式保证了线程安全。


2、是否可为空:HashTable不允许value为空,ConcurrentHashMap不允许null值作为key或value,而HashMap则允许null作为key或value。


3、迭代器:HashTable的迭代器是通过Enumeration实现的,而HashMap和ConcurrentHashMap使用的是Iterator实现的。


4、扩容:HashTable在扩容时,将容量扩大一倍加一,而HashMap和ConcurrentHashMap的扩容机制是将容量扩大一倍。


5、初始容量:HashTable的初始容量为11,而HashMap和ConcurrentHashMap的初始容量为16。


6、性能:HashMap通常比HashTable性能更好,因为它没加锁。所以弊端就是线程不安全。但后者加了锁,是线程安全的,缺点就是消耗性能。ConcurrentHashMap在多线程并发访问时,比HashTable和HashMap性能更好,因为它使用了分段锁来保证线程安全


所以,不建议使用HashTable。至于选择HashMap还是ConcurrentHashMap取决于并发访问量的大小,若并发访问量不高,则选用HashMap。若并发访问量较大,则选用ConcurrentHashMap。



ok,那今天先到这里吧。练习时长两年半的程序员.....唱跳rap篮球🏀....差点就遭老罪喽~



还有,别忘记给那个练习时长两年半的三婶儿也点个赞哈~她唱跳rap篮球也还行......


在这里插入图片描述


作者:三婶儿
来源:juejin.cn/post/7209826725365137465
收起阅读 »

领导派的活太难,除了跑路,还能怎么办?

人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中…… 这种事情一般有一些共同特点。 结果和目标极其模糊。 需要协调其他团队干活但是对方很不配合。 领导也...
继续阅读 »

人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中……


这种事情一般有一些共同特点。



  1. 结果和目标极其模糊。

  2. 需要协调其他团队干活但是对方很不配合。

  3. 领导也不知道怎么干


领导往往是拍脑袋提想法,他们也不知道具体如何执行。反过来说,如果领导明确知道怎么做,能亲自指导技术方案、亲自解决关键问题,那问题就好办了,只要跟着领导冲锋陷阵就好了,就不存在烦恼了。


遇到这种棘手的事情,如果自己被夹在中间,真的非常难受啊!


image.png


今天重点聊聊领导拍脑袋、心血来潮想做的那些大事 如果让你摊上了,你该怎么做!


1、提高警惕!逆风局翻盘难!


互联网行业目前处于稳定发展期,很少会出现突然迅猛增长的业务,也很少有公司能够迅速崛起。这是整个行业的大背景。因此,我们应该对任何不确定或模糊的目标表示怀疑,因为它们更有可能成为我们的绊脚石,而不是机遇。即使在王者荣耀这样的游戏里,要逆风翻盘也很困难,更何况在工作中呢。


当领导提出一个棘手的问题时,我们应立刻警惕,这可能不是一个好的机会,而是一个陷阱。我们不应该被领导画的饼所迷惑,而是要冷静客观地思考。哪些目标和结果是难以达到的,这些目标和结果就是领导给我们画的大饼!


领导给出任务后,我们就要努力完成。通常情况下,他们会给我们一大堆任务,需要我们确认各种事情。简而言之,他们只是有个想法,而调研报告和具体实施方案就需要我们去做。


如果领导是一位优秀而谦虚的人,通常在我们完成调研后,会根据调研结果来判断这个想法是否可行。如果不可行,他们会立即放弃,而我们也不会有什么损失。


但是,一旦领导有了一个想法,肯定是希望我们来完成的,即便我们在调研后认为不可行,大多数情况下,他们也不会接受我们的结论!因此,我们的调研工作必须极度认真,如果我们认为不可行,就要清楚地阐述不可行的理由,要非常充分。


这是我们第一次逃离的机会,我们必须重视这次机会,并抓住机会。


2、积极想办法退出


对于这种模糊不靠谱的事情,能避开就避开,不要犹豫。因为这种事情往往占用大量时间,但很难取得显著的成果。对于这种时间周期长、收益低、风险高的事情,最好保持距离。


你还需要忍受巨大的机会成本


在你长期投入这种事情的过程中,如果团队接到更好的项目和需求,那肯定不会考虑你。你只能羡慕别人的机会。


因此,如果可以撤退的话,最好离开这种费力不讨好的活远远的!



子曰:吾日三省吾身,这事能不能不干,这事能不能晚点干,这事能不能推给别人干。



如何摆脱这件事呢?


2.1 借助更高优事情插入,及时抽身


例如,突然出现了一件更为紧急的事情,这就是脱身的机会。与此同时,我们也可以为领导保留一些颜面,因为随着工作的进展,领导也会意识到这件事情的意义不大,很难取得实质成果。但是,如果我们一开始就表示不再继续做这件事,那么领导可能会觉得自己的判断出了问题,失去面子。所以,我们可以寻找一个时机,给领导下台阶。


或者,突然出现了一个需求,与我们目前的重构方案存在冲突。这是一个很好的借口。重构方案和未来产品规划产生了冲突,我们应优先满足产品规划和需求。重构方案需要后续再次评估,以找到更好的解决方案。


2.2 自己规划更重要的事情,并说服领导


当你对系统优化没有想法时,不要怪领导给你找事干。


如果领导有一个系统重构的计划和目标需要你执行,但是你不想干,或者你认为这件事不靠谱。那么你可以思考一个更可行、更有效、更能带来收益的重构方案,并与领导进行汇报。如果领导认为你的计划更加重要且更具可行性,那他可能会放弃自己的想法。


这就是主动转被动的策略。这时你的技术能力将接受考验,你能提出一个更优秀的系统重构方向吗?你能提出一个更佳的系统建设方向吗?


2.3 选择更好的时机做这件事


如果领导让你去做技术重构,而这件事的优先级不如产品需求高,上下游团队也不愿意配合你,而且领导给你的人力和时间资源也不够充裕,你应该怎么办呢?可以考虑与产品需求一起进行技术重构。也就是说,边开发需求,边进行技术重构。这样做有以下好处:可以借助于产品的力量,很自然地协调上下游团队与你一同进行重构。同时也能推动测试同事进行更全面的测试。在资源上遇到的问题,也可以让产品帮助解决。


所以,技术重构最好和产品需求结合起来进行。如果技术重构规模庞大,记得一定要分阶段进行,避免因技术重构导致产品需求延期哦。


2.4 坦诚自己能力不足,暂时无法完成这件事,以后再干行不行


可以考虑向领导坦然承认自己的能力还不足以立即执行这项任务,因此提出先缓一缓,先熟悉一下这个系统的建议。我可以多做一些需求,以此来熟悉系统,然后再进行重构。


我曾经接手一个系统,领导分配给我一个非常复杂的技术重构任务。当时我并没有足够聪明,没有拒绝,而是勉强去做,结果非常不理想,还导致了线上P0级别的事故发生!


新领导告诉我,"先想清楚如何实施,再去行动。盲目地勉强上阵只会带来糟糕的结果。当你对一个系统不熟悉的时候,绝对不能尝试对其进行重构。"


先熟悉系统至少三个月到半年。再谈重构系统!


2.5 拖字诀,拖到领导不想干这件事!


拖到领导不想干的时候,就万事大吉了。


注意这是最消极的策略,运气好,拖着拖着就不用干了。但如果运气不佳,拖延只会让任务在时间上更加紧迫,而且还会招致领导的不满。


使用拖延策略很可能得罪领导,给他们留下不良的印象。


因此,在使用此策略时应谨慎行事!


2.6 退出时毫不犹豫,不要惋惜沉默成本


如果有撤退的机会,一定不要犹豫,不要为自己付出的投入感到遗憾,不要勉强继续前进,也不必试图得到明确的结果。错误的决策只会带来错误的结果。一定要及时止损。


因为我曾经犯过类似的错误,本来有机会撤退,但是考虑到已经付出了很多,想要坚持下去。幸好有一位同事更加冷静,及时制止了我。事后我反思,庆幸及时撤退,否则后果真的不敢想象啊。


3、适当焦虑


每个人都喜欢做确定性的事情,面对不确定的事情每个人都会感到焦虑。为此可能你每天都很焦虑,甚至开始对工作和与领导见面感到厌恶。之所以这个事情让你感到不适,是因为它要求你跳出舒适区。


但是,请记住,适度的焦虑是正常的。告诉自己,这并没有什么大不了的。即使做得不好,顶多被领导责备一下而已。不值得让生活充满焦虑,最重要的是保持身心健康和快乐。


当你沉浸在焦虑中时,可能会对工作和领导感到厌烦。这样一来,你可能会对和领导沟通感到反感。这种情况是可怕的,因为你需要不断和领导沟通才能了解他真正的意图。如果失去了沟通,这个事情肯定不会有好的结果。


因此,一定要保持适度的焦虑。


3.1 沟通放在第一位


面对模糊的目标和结果,你需要反复和领导沟通,逐步确认他的意图。或者在沟通中,让领导他自己逐渐确定的自己的意图。在这方面有几个技巧~


3.2 直接去工位找他


如果在线上沟通,领导回复可能慢,可能沟通不通畅。单独约会议沟通,往往领导比较忙,没空参加。所以有问题可以直接去工位找他,随时找他沟通问题。提高效率


3.3 没听懂的话让领导说清楚


平常时候领导没说清楚,无所谓,影响不大。例如普通的产品需求,领导说的不清楚没关系,找产品问清楚就行。


面对目标不明确的项目,领导的意图就十分重要。因为你除了问领导,问其他人没用。领导就是需求的提出方,你不问领导你问谁。 在这种情况下,没听懂的事情必须要多问一嘴。把领导模糊的话问清楚。


不要怕啰嗦,也不要自己瞎揣摩领导的意图。每个人的想法都不同,瞎猜没用。


3.4 放低姿态


如果领导和你说这件事不用干了,你肯定拍手叫好。很多烦恼,领导一句话,就能帮你摆平!


放低姿态就是沟通时候,该叫苦叫苦,该求助就求助,别把自己当成超人,领导提啥要求都不打折扣的行为完全没必要。可以和领导叫叫苦,可以活跃气氛,让领导多给自己点资源,包括人和时间。


说白了,就是和 领导 “撒娇”。这方面女生比较有优势,男生可能拉不下脸。之前的公司,我真见识过,事情太多,干不完,希望领导给加人,但被领导拒绝。 然后她就哭了,最后还真管用!是个女同事。


男孩子想想其他办法撒娇吧。评论区留下你们的办法!


3.5 维护几个和领导的日常话题


平常如果有机会和领导闲聊天,一定不要社交恐惧啊! 闲聊天很能提升双方的信任关系,可以多想想几个话题。例如车、孩子、周末干啥、去哪旅游了等等。


提升了信任关系,容易在工作中和领导更加融洽。说白了就是等你需要帮忙的时候,领导会多卖你人情!


4 积极想替代方案————当领导提的想法不合理时


积极寻求替代方案,不要被领导的思路局限!引导众人朝着正确的方向前进!


不同领导的水平和对技术问题的认知不尽相同,他们注重整体大局,而员工更注重细节。这种差异导致了宏观和微观层面之间存在信息不对称,再加上个人经验、路径依赖导致的个人偏见,使得领导的想法不一定正确,也不一定能够顺利实施。


就我个人的经历来说,领导要求我进行一次技术重构。由于我对这个项目还不够熟悉,所以我完全按照领导的方案去操作,没有怀疑过。事后回顾,发现这个方案过于繁重,其实只需要调整前端接口就能解决问题,但最终我们却对底层数据库存储、业务代码和接口交互方式进行了全面改变。


最终收益并不高,反而导致了一个严重的故障。既没有获得功劳,也没有得到应有的认可。


事后反思,我意识到我不应该盲目按照领导的方案去执行,而是应该怀着质疑和批判的态度去思考他的方案。多寻求几个备选方案,进行横向比较,找到成本最低、实施最简单的方案。


4.1 汇报材料高大上,实现方案短平快


私底下,可以对老板坦诚这件事,就是没什么搞头。但是对外文章要写得高大上!


技术方案要高大上,实现方案要短平快。


面对不确定的目标、面对不好完成的任务,要适当吹牛逼和画饼。汇报文档可以和实现方案有出入。


模糊的目标,往往难以执行和完成,技术方案越复杂,越容易出问题。本来就没什么收益,还引出一堆线上问题,只能当项目失败的背锅侠,得不偿失。


一定要想办法,把实现方案做的简单。这样有3个好处;




  1. 降低实现难度,减少上线风险。




  2. 缩短开发周期,尽快摆脱这个项目。




  3. 把更多的时间放在汇报材料上。代码没人看!!!




程序员一般情况下习惯于实话实说,如果说假话,一定是被人逼得。


不会写文档?# 写文档不用发愁,1000个互联网常用词汇送给你


不会写技术方案?# 不会画图? 17 张图教你写好技术方案!


5、申请专门的团队攻克难关!


例如重构系统涉及到上下游系统,一个人搞不定的!要向领导寻求帮助,让上下游同事一起干这件事。


让熟悉系统的人跟自己一起做,拉更多的人入伙!多个人一起承担重任! 这种组织上的安排,只能由领导出面解决。


假如别的同事经常打扰你,总让你确认这件事,确认那件事,总让你帮忙梳理文档,你愿意配合吗? 每个人都很忙,没人愿意长期给你干活。


让领导帮忙成立重构小组!然后你可以给每个人都分派任务,比自己独自硬扛,成功概率大很多。


虽然重构的目标不明确,但你可以尝试明确每个人的责任,设置短期的里程碑。例如前三天梳理整理资料,每天开早会, push大家干活。(这样很招人恨!没办法,领导卷的)


5.1 寻求合作的最大公约数


重大项目往往需要多个团队同时配合,即便你申请了专门的小组跟进这件事,但是别人可能出工不出力!


他们不配合的原因在于:不光没有收益,付出还很多。成本和收益不对等,人家不愿意很正常。保持平常心!不要带着脾气看待这件事!


略微想一下就明白,既然你觉得这件事风险高、收益低,难道其他人看不出来吗?


作为项目的负责人推动事情更加困难。当别人不配合时,除了把矛盾上升到上层领导外,还有哪些更好的办法呢?




  1. 平时多和相关同学打好关系。平时奶茶咖啡多送点,吃别人嘴短,到时候求人时候很管事的。




  2. 调动对方的积极性!例如重构系统需要人家配合,但是这件事对他们又没有收益。可以和他们一起头脑风暴,想一下对方系统可以做哪些重构。当双方一拍即合,各取所需时,才能合作融洽。双赢的合作,才能顺利。




  3. 多作妥协。上下游系统的交互边界很难划分,如果交互存在争议,可以适当让步,换取对方的积极合作。完成胜于完美!




总之,涉及多个团队合作时,除了依靠上层领导的强硬干预之外,还要想一些合作共赢的方案!


6、争取更多的资源支持


没有完不成的事情,只要资源充裕,任何事情都是有希望的。当你面临棘手的问题时,除了打起12分的精气神,还要多想想和领导申请资源啊!


最重要的包括人力资源、时间资源。如果空口白牙就要人,可能比较困难。


这需要你在调研阶段深入思考,预想到系统的挑战点,把任务细分,越细越好,然后拿着排期表找领导,要人、要时间。


如果人和时间都不给!可以多试几次,软磨硬泡也是好办法!


此外还有别的办法,例如 ”偷工减料"。你可以和领导沟通,方案中哪些内容不重要,是否可以砍掉。”既然你不给人,砍掉不重要的部分,减少工作量,总可以吧"


除此之外,还可以考虑分期做。信用卡可以分期付款,技术重构当然也可以分期优化!


7、能分期就分期


对于技术重构类工作,一定要想办法分期重构,不要一次性只求大而全!




  1. 越复杂的技术方案越容易出问题!




  2. 越长的开发周期越容易出问题!




  3. 越想一次性完成,越容易忙中出错!




分期的好处自不必说,在设计方案时一定要想如何分期完成。


如果对一个系统不熟悉,建议分期方案 先易后难!先做简单的,逐渐地你对系统会有更深入的理解!


如果对一个系统很熟悉,可以考虑先难后易。先把最困难的完成!后面会轻松很多!


但是我还是建议庞大的重构工作,先易后难!先做简单的,拖着拖着,也许就不需要重构了呢!


8、即便没有功劳但是要收获苦劳


当一件事干成很难的时候,要想办法把损失降到最低。一定要想着先保护自己!别逞能!


工作几年的朋友应该知道,不是所有的项目都能成功!甚至大部分项目在商业上是失败的!做不成一件事很正常!


如果一件事很难办成,功劳就不要想了。但是可以赚一份苦劳。


这要求你能把自己的困难说给领导,例如其他团队不配合!你可以一直和领导反馈,并寻求领导的帮助。


日常工作的内容也要有文档留存。工作以周报形式单独和领导汇报!要让领导知道你每周的进展,向领导传递一个事实:“每一周你都努力地在做事,并且也都及时汇报了,日后干不成,可别只怪我一人啊!”


接到一个烫手山芋,处理起来很难~ 斗智斗勇,所以能躲开还是躲开啊!


9、转变观念:放弃责任心,领导关注的内容重点完成


出于责任心的角度,我们可能认为领导提出的方案并不正确,甚至认为领导给自己派的工作完全没有意义。


你可能认为领导的Idea 不切合实际!


出于责任心,你有你的想法,你有你的原则!你认为系统这样重构更适合!但那又怎样,除非你有足够的理由说服领导,否则改变不了什么。


站在更高的位置能看的更远,一般领导都会争取团队利益最大化。虽然看起来不切实际,但是努力拼一拼,也许能给团队带来更大的利益。这可能是领导的想法!说白了,就是领导想让团队多去冲锋陷阵,多把一些不可能变成可能!


和领导保持节奏,领导更关注哪件事,就尽力把这件事做好! 放弃自己所谓的“责任心”。


10、挑战、机遇、风险并存。


在互联网稳定期,各行各业都在内卷,公司内部更是在内卷!


在没有巨大增量的团队和公司里,靠内卷出成绩是很困难的事情。有时候真的很绝望,每一分钟都想躺平 。


像这种目标不明确、执行方案不明确、结果不明确、需要协调其他团队干活的难事越来越多!风险高、低收益的事情谁都不想干!


但是一旦能做成,对于个人也是极大地锻炼。所以大家不要一味地悲观,遇到这种棘手的事情,多和领导沟通,多想想更优的解决方案。也许能走出一条捷径,取得极大的成果~


作者:他是程序员
来源:juejin.cn/post/7290469741867565092
收起阅读 »

抓包调试工具的终极答案-whistle

web
前言 抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络...
继续阅读 »

前言


抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络数据流量的细节。


在计算机通信中,数据包是由发送端(如浏览器或应用程序)构建的,并通过互联网传输到接收端(如服务器或另一台计算机)。通常情况下,这些数据包由发送和接收的应用程序自行处理,用户往往无法直接观察这些数据包的内容。然而,抓包工具可以截取这些数据包,并将其内容以明文或加密的形式展示给用户。如果数据包是以明文形式发送的,或者我们可以推断出其加密方法,那么我们就可以对这些数据包进行深入的分析和解密。这样,我们便可以了解这些数据包的内容、用途和意义。


通过使用抓包工具,开发人员和系统管理员可以调试和分析网络应用程序和通信协议,以便更好地了解它们的性能、安全性和可靠性。此外,普通用户也可以利用抓包工具来了解他们正在使用的应用程序和网络服务的内部工作原理,并保障他们的网络安全和隐私。总之,抓包工具是计算机网络中不可或缺的一部分,它为我们提供了深入洞察和分析网络数据流量的能力。


抓包工具更多的分析网络流量以及数据包内容、检测网络问题、获取数据传输的详细信息、功能调试等。市面上常见的抓包工具有很多,比如说我们网页上常用到的浏览器开发者工具、移动端常用的vConsole以及市面上大家都在用的Charleswhistle,今天就简单给大家分享一下whistle的基本使用。


1. 简介


whistle主要是基于node来实现的一个跨平台web调试代理工具,whistle采用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能


2. 安装启动


wproxy.org/whistle/ins…


1. 环境


首先要使用whistle的话,必须要具备node环境(下载地址),下载成功后可以通过命令行来查看是否已安装


node -v // 查询当前node版本信息

2.安装whistle


后面也会提到桌面端应用LightProxy,也是基于Electron和whistle的桌面端代理软件。


npm install -g whistle // Windows
sudo npm install -g whistle // Mac(非root权限)

安装成功可以使用命令whistle help 或者w2 help查看whistle的相关帮助信息


3.启动whistle


最新版本的whistle支持三种等价的命令whistlew2wproxy


w2 start // 启动whistle
w2 restart // 重启whistle
w2 stop // 停止whistle
w2 run // 调试模式启动whistle

4.配置代理


这里就以Mac配置举例,可以在设置->Wi-Fi->详细信息->代理中选择网页代理(HTTP)填入对应的ip以及端口号;


移动端这里同样以IOS为例,在Wi-Fi->HTTP代理中打开配置代理为手动,同时填入对应ip以及端口号.


我们也可以通过chrome浏览器插件SwitchyOmega来进行网页代理



  1. 点击新建情景模式

  2. 选择选项代理服务器

  3. 配置代理协议、代理ip、代理端口

  4. 点击应用选项保存并切换到代理模式


switchyOmega


5.证书安装


最后我们只需要安装根证书即可。我们打开whistle生成的浏览器生成页,点开HTTPS选项,点击二维码下载证书,这里同样以MAC和IOS为例。



1.Mac我们打开钥匙串访问,这里要注意,当我们添加完成后依旧属于不被信任状态,我们需要双击证书,在信任的里面找到使用此证书时选中始终信任,配置证书完成后我们选中Capture TUNNEL CONNECTS即可代理成功,捕捉传输内容。


证书
2.IOS同样我们通过扫码打开证书,允许描述配置文件下载,在设置已下载描述文件中安装描述文件,安装完成后我们打开通用->关于本机->证书信任设置中选择对下载的whistle证书启用完全信任即可。



如果是windows系统出现证书无法下载的情况,进入系统设置 - 网络 - Windows防火墙 - 高级设置,设置入站规则Node.js开头的都允许连接,保存确定下载;


手机端偶尔可能会遇到无法找到证书的情况,可以连接同一个局域网,访问电脑ip代理对应ip地址,扫码HTTPS进行证书下载




3.使用


whistle官网这里详细介绍了whistle相关的命令行参数,这里我们就不过多赘述。我们只介绍几个常用的功能。


1.HTTPS请求抓取:


所有配置完成后,我们打开whistle页面,浏览器或手机发起HTTPS请求后即可看到.


image.png
那么问题来了,这么多请求同时包含了预检等众多请求,我们怎么快速找到我们需要看到的接口呢?
我们可以在下方的Type filter text来进行简单的搜索功能,默认是按照url来进行搜索的,我们也可以按照固定的分类规则来进行快速查询



  1. 默认搜索url

  2. h: {word}搜索头部

  3. c: {word}搜索内容

  4. p: {word}搜索请求协议

  5. o: {word}搜索ip

  6. m: {word}搜索方法

  7. s: {word}搜索状态码


如果我们依旧觉得不够清晰该怎么办呢,我们就可以用到whistle的Rules功能,Rules支持我们通过扩展系统host配置来进行所有操作。


// whistle将通过pattern匹配来完成对应uri的操作
pattern operatorURI

pattern的匹配规则支持域名、路径、正则、精准匹配和通配符匹配


api.juejin.cn style://color=@fff&fontStyle=italic&bgColor=red

这样我们就可以更清晰的来找到我们想捕捉的内容。


image.png


2.请求响应修改


我们可以通过固定的Rules配置来对请求或者返回来进行修改测试


{pattern} method://get 请求方式修改为get
{pattern} statusCode://500 请求状态码返回500
{pattern} log:// 日志打印
{pattern} resCors:// 跨域

以上提到的是我们部分的简单修改,如果我们需要修改请求的请求体以及相应内容,我们就需要用到whistle提供的Values板块来进行配置


{pattern} reqHeaders://filepath 修改请求头 //filepath: Values里面的key或者本地文件
// reqHeaders example
{pattern} reqHeaders://{testReqHeadersJSON}
// resBody example
{pattern} resBody://{testResBodyJSON}

Values模版中配置testReqHeadersJSON和testResBodyJSON


image.png


image.png


这样就可以添加或者修改请求头内容,修改或添加响应内容同理。


image.png


3.移动端调试


whistle不仅提供强大的web调试功能,对于移动端的调试也是十分友好的。


由于移动端需要适配众多不同的浏览器和设备,包括各种尺寸的屏幕、各种操作系统和不同的设备载体,因此相对于PC端的页面调试,移动端的调试难度更加复杂。在出现问题时,排查的过程也涉及更多因素,需要我们及时发现并修复问题。对于一个合格的开发人员来说,强大的开发能力是基础,同时还需要拥有快速解决问题的能力和精准定位问题的技能,这样才能够在面对不同的问题时应对自如、犹游刃有余。


像我们在测试环境常用到的vConsole一般是不会在生产环境以一般方式展现给用户的,但是whistle提供注入js的方法能够让我们通过js代码以及vConsole等工具来进行页面调试以及问题的快速排查。


1.接口调试

这里我们就简单的以掘金首页为例



  1. 我们在Values中配置好vConsole以及对应生成实例代码;

  2. 在Rules中通过jsPrepend进行js注入
    这样我们就可以成功生成vConsole来进行调试了



vConsole.min.js的源码我们可以去github上自行下载,或者也可以通过插件来解决。


// 集成 vConsole、eruda、mdebug 等调试H5页面工具的插件
sudo npm install -g whistle.inspect

{pattern} whistle.inspect://vConsole
{pattern} whistle.inspect://eruda
{pattern} whistle.inspect://mdebug

2. 元素样式调试

whistle同时内置了Weinre和Chii来帮助我们进行调试


{pattern} weinre://{yourName}

配置Rules后我们在Whistle下拉选项下选中对应name,重新打开页面后即可进行elment调试


image.png{pattern} 同样配置Rules后我们在Whistle选项下的Plugins选中对应Chii,点击打开后选择inspect来进行element调试


{pattern} whistle.chii://

image-20230921140713086.png


4.LightProxy


下载地址


LightProxy是基于whistle使用electron开发的一个桌面端代理软件,从操作以及证书代理配置上更加简单灵活,更好的满足开发者的需求,基本使用规则同whistle一致;同时也帮我们继承了常用的像inspectvase等插件,更加方便快捷。


5.总结


Whistle作为常用的的网络调试工具之一,它不仅具备常规的抓包功能,还在跨域代理等方面展现了多样化的应用。通过巧妙的配置和强大的功能,我们可以进行深度定制和扩展,以满足各种复杂的调试需求。


这个工具的应用场景非常广泛,从简单的HTTP/HTTPS请求拦截,到复杂的爬虫和自动化测试,都可以借助Whistle实现。同时,它还支持JavaScript、TypeScript等多种编程语言,以及各种浏览器和Node.js环境。


使用Whistle进行调试非常简单,只需要简单地设置和配置,就可以轻松地实现对网络请求的拦截和修改。无论是排查问题、测试接口还是调试前端代码,Whistle都能够帮助我们快速定位问题并解决问题。它的易用性和灵活性也使得它成为了前端开发人员的得力助手。


通过使用Whistle,我们可以更好地了解网络请求的细节,掌握API接口的调用和数据传输的规律。这有助于我们优化代码、提高程序的稳定性和性能。因此,无论是初学者还是经验丰富的开发者,都应该尝试使用Whistle来提升自己的调试技能和开发效率。


参考


1.whistle官网: wproxy.org/whistle/


作者:洞窝技术
来源:juejin.cn/post/7293180747400134706
收起阅读 »

《我当程序媛那些年(四)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!



接上文~我当程序媛那些年(三)


相遇


从H城市到S城市距离不远,高铁大概1个小时,我们相约在人广见面,虽然见面之前已经见过照片了,但没见过真人,怕有点脸盲一时半会儿认不出哈哈~



ps:等待的过程是有点期待和忐忑不安的,如果大家也有过这种经历,相信差不多都是这样的心情大差不差😂



经过十多分钟的等待,我们终于快要见面,因为是五一的原因,所以那天人广人很多,正准备上电梯扶手时,我恍然间好像感觉到有人拍了一下我的肩膀,回头侧身一看。没有看到熟悉的身影。


正当我以为是我的错觉,刚好到电梯口的时候,一个身影窜到我面前(那画面脑补一下有点搞笑😂),定睛注意一看,才发现眼前站了一个熟悉的人。背着书包,带着眼镜,斯斯文文,是典型的互联网宅男形象没跑了哈哈~(当然这里不是取笑之意,只是觉得这个形容词生动有趣😆)


因为是放假,所以我们前期就沟通好了去哪里玩。可能是因为初次见面,双方都有些拘谨,路上去聊到玩的地方话题才开始多了起来。



因为我本身就是性格外向,比较活泼的人。不熟悉我的人对我的第一印象可能是有点安静(实际正好相反哈哈~),熟悉了就是话匣子,虽然偶尔性格上会透露出女汉子的气质哈哈~



也许是有比较长的一段时间没有和身边熟悉的人聊过天,所以聊到有趣的话题时,嗯~我话会比较多,属于压根不用担心会冷场的那种🤣。


我们首先去了迪斯尼,晚上的迪斯尼烟花很美。那天虽然是尽兴而归,如果是约会或者和朋友一起的真的不建议节假日去玩哈哈~,人真的超多,排队排了一天只玩了差不多两三个项目额。。。。所以之后我们出去玩都再也不去人潮拥挤的地方(没出去玩过被坑一次就长记性了😂)。


后面两天为了缓解一下疲惫,没有去往那种要排很长的队伍景点了,陆续去参观了一下S市的动物园,以及H市的西湖等。


997898f8f3623ba14db306d04021647.jpg


7fab44935a6f99b7a4925ab96ea7a61.jpg


bd3dbd23a6626b32179791f9be57a2b.jpg


8a7388159570be6f3f53b031babc2d8.jpg


43e6f18afa89025160a70d77ac89c3d.jpg


bc099734c602575408e5f7ef198dff2.jpg


5c1ec5b2e90e2be09edbf7465bdbd9d.jpg


6ac2a47c7510db19645a79c685c1e3d.jpg


c05151ec9714b8e2abab63dbff2a8c0.jpg


a3965e9e150a42728c20bead3a15679.jpg


生病危机


继五一过后,开始回归日常的上班族生活,那次见面之后,加上假期几天的相处,之前也有过联系(虽然没见面),但是从谈吐、学识以及工作方面,我们都觉得互相很合拍,共同话题很多,所以我们后面不久就确认了关系,不过由于Z先生是在H市,所以我们前期算是属于异地恋。


上了一周班后,某天下班感觉腹部有点疼痛,一看伤口有些红肿,感觉就是被虫子咬了,至于是什么虫子,就不得而知了。于是我回家买了红霉素软膏涂了一下,感觉稍稍有些缓解。


我以为过几天就会好,当时也没多想,甚至没想着去医院看看,结果这一忽视,差点让我丢了小命`(>﹏<)′


过了几天,伤口越来越恶化,渐渐开始有脓水出现。我感觉情况有点不对,打算周末去医院看看。由于当时Z先生工作是在杭州,我们确认关系后约的都是周末见面,因此我当时就去了H市的医院。


令我没想到的是,这次H市的医院之旅,差点要了我的小命。起初找的也算是H市的三甲医院了,当时虽然就确认了是蜱虫叮咬,但是给的药物却并没有完全治好我的伤口,我不知道是不是我体质原因还是药物就没啥效果额,虽然最后花了一千多(⊙﹏⊙)。。。。


后面两天病情突然一下子就恶化的很快,腹部周围开始也全是脓,伤口开始剧烈疼痛,不能碰触的那种。


但是医生说脓水必须挤掉,我只记得当时算是痛彻心扉了,怎么形容呢,就似乎有人拿了把刀在你的腐肉上刮,还不打麻醉药的那种,挤脓的时候痛的都感觉有点意识模糊了。


当时Z先生看到治疗没效果,果断放弃继续治疗,请假带我直奔S市的专业皮肤科医院。当时伤口已经恶化到没有办法再继续上班,坐往前往S市的高铁一直到医院时,一边走伤口一边在流脓血,可想而知有多严重了。


后面到了S市的医院,医生简单的看了一下,我也把之前开的药给医生检查了一下,最终医生开了一套中药,直接热敷在伤口上,从看病->开药->敷药,整个时间大概持续了两三个小时。


在这里真的得感叹一句中药材的神奇,敷完药大概一个小时,伤口脓血已经止住,伤口疼痛也几乎缓解,红肿情况开始好转,感觉就好像在死亡的边缘跑了一趟突然能喘上气来的那种,算是毫不夸张了。


待我好些了之后,医生才开始跟我详细的说明了一下,他说我伤口之所以会恶化的这么严重,是因为蜱虫的尸体和毒素都留在了伤口里面,不清理出来只会越来越严重。


我问医生,我没有去蚊虫比较多的地方,为什么会无故染上这种虫子,医生问我是不是有去过公园或者草地呆过,像公园或者草地有的人会带猫猫狗狗啥的,动物身上最容易有沾染这种虫子,一旦沾到人体,后果不堪设想。


听到这里,我心里忽然一惊,想想放假的时候有在西湖边的草坪上坐过休息一会儿,那就有可能是那时沾染上的了。关键是这种虫子咬了还毫无感觉的那种,想想要多恐怖有多恐怖了😭。


随后医生还说,我这伤口算是恶化到最后严重的时期了,再晚来一周,小命估计都不保了。。。。。。



ps: 说到这里jym就得注意了,如果是女生或者有女朋友的男士们,千万千万去公园不要做草地,反正尽量多注意一下吧!!!!



经过这次,真是一朝被蛇咬,十年怕井绳,从此对虫子类心生恐惧,尤其是蜱虫、隐翅虫


至于隐翅虫为什么也很恐怖,额外说个小故事吧,算是亲眼见证的。


之前读初中的时候,由于是在农村,夏天晚上教室亮的时候会有很多隐翅虫飞进来,有的飞在灯泡上,有的飞在书桌上,当时班上一位女同学晚自习的时候,摁了一下隐翅虫,估计是没有洗手,然后摸了一下脸,然后就是没过两天,人就请假了,一周多才回来,整个脸脱了一层皮(一点都不带夸张,事实就是这样额)。。。。。



ps:也给各位jym提个醒,被虫子咬了如果不知道是什么虫子咬的,最好及时去医院看,千万不要拖!!!不然像我一样倒霉就得不偿失了😖,最后贴一下蜱虫和隐翅虫的照片,给大家提个醒。



image.png


image.png


image.png


文章以待后续。。。。如果觉得文章写的不错,那就给个赞或者关注一下吧,你的支持将是我写文最大的动力!


近期文章预览


我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)


作者:梦周十
来源:juejin.cn/post/7293122700687867931
收起阅读 »

《我当程序媛那些年(三)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!



接上文~我当程序媛那些年(二)


第一份工作日常


初入职B公司时,我的内心的激动而又忐忑的。毕竟是我的第一份工作,说不兴奋那都是虚言哈哈~,由于没有实际的工作经验,所以我也没有勇气报太高的薪资,我对自己的能力还是挺有自知之明的。同时心里其实也有一丢丢害怕,就算报了高薪资,最终还是会因为自己能力不足过不了试用期。


基于此原因,所以我是抱着一个谨慎和谦虚的学习态度去对待我的第一份工作,没有提太高的薪资,等到慢慢积攒经验在决定后面的出路。


先简单介绍一下我当时的B公司吧,B公司规模不大,大概20-99人左右的样子,公司大概就是做硬件机械行业的。当时进去的时候,人事部和技术部是分开的,总共两层(当然,是类似租的办公室的那种,并不是一整层的那个哈哈~),开发团队人不是很多,Java总共3个,前端2个,安卓1个,再加上还有做3D和UI的,加上项目经理和技术总监,整个团队差不多10人左右的样子。


公司整体工作环境只能算一般般吧,当时的项目经理、三个Java和1个安卓是坐在一个小办公室里,所以工作氛围不算是很活泼,整体气氛偏于沉闷。当然,由于后期生病的缘故(后续会说到~),所以第一家公司我也没有久待。


当时在公司做的项目算是接手别的公司项目,公司性质也不完全算是外包,毕竟有自营的硬件相关设施,估计只是外接一些项目多赚点钱吧。


由于那时算是互联网发展巅峰的初期,不像现在的技术栈种类繁多,当时用的是SSH(Spring+Structs2+Hibernate)框架,前端是用的Angular.js。偶尔也有Jquery的。



ps: 后面记得Structs2爆出了漏洞,所以后面改用了SpringMVC,还记得当年的Structs2可谓是风光无限,他与Spring和Hibernate堪称牛逼哄哄的Java Web三剑客,可惜一招从云端跌落,从此再也不负往日风光。还记得当时的招聘要求首要就是会SSH,后面的Structs2逐渐被SpringMVC/SpringBoot取代,Hibernate 也逐渐被 MyBatis/ Spring Data JPA 所取代,现在已经是SSM的天下了,说Struts2被淘汰一点也不为过。



因为刚入职不久,所以领导也没有给我派太多的活,先让我熟悉一下项目,当时项目管理软件用的是Redmine,前期我主要就是改改前端页面的bug,偶尔写写小功能,不是很复杂的CRUD,任务不重,工作算是比较轻松。


除此之外公司福利还算不错,记得当时五一劳动节发了三百,只是后续出去组织旅游,因为我是试用期,需要自费一半,当时手上发完工资之后,因为之前文章也提到过,借了同学的钱,所以一发工资我就把欠的钱立即还上了,再加上当时要还的助学贷款,还完房租,手上捉襟见肘,所以我当时也就没去了~


054aaf50b368fa5226a35a7845272e3.jpg


情绪转折点


虽然来上海加上面试和工作的时间才短短将近三个月,这段时间因为忙碌算是过的很充足。自从xyq离开上海之后,我的生活又恢复到了往日的安静。


我本身是一个喜欢热闹的人,太安静的环境下,我的那种焦虑感和孤独感在周末无人的环境中开始被无限放大,所以有的时候周末工作偶尔忙的时候我会去加加班,虽然加的次数不是很多。


有的时候忙碌也可能是好事,不空闲下来就不会胡思乱想那么多,工作渐渐稳定之后,我的情绪反而不像一开始找工作时的那种意气风发,整个人情绪开始断崖式跌落。


也不是工作不顺利吧,而是压力紧绷了太久,一时间突然放松下来,情绪便如洪水一样收也收不住。其实这个时候最好的方式就是出去走走,看一下新的环境或者认识新的人,转移一下注意力。


可惜当年的我没有想过这些,除了工作日上班就是周末在家里闷着,没有交际,整个人开始消极沉默寡言,至于为什么在家里闷着,主要原因是当时考虑到出去玩就得花钱,再加上刚上班不久,同事也不是特别熟的那种,也就没有想出去玩的欲望了,我也不会玩游戏,精神一下子放松下来那种糟糕的情绪就有点收不住了。


af93294cc90523736dc5d3faf16864f.jpg


与君初相识


我以为我会一直糟糕状态持续下去,到工作中期,已经有点开始影响到工作了。直至遇见那束救赎我的光——Z先生。


我们于17年相识,随后相知、相伴6年,在一起两千三百多天,直至今年国庆,我们相约结束爱情长跑,相守与共进入婚姻殿堂.


相伴期间,我们互相成长,事业上我们是互相的伙伴,生活中我们是互相的伴侣,我们算是共同进步,共同成长,因为都是做开发的,Z先生做前端,我做后端(后期才转的全栈),所以共同话题很多。


工作中我们遇到问题和挫折会相互分享,相互指导和建议,结合两人的共同想法选中最佳方案。生活中我们也会互相分享遇到的有趣的事情。



ps:虽然偶尔有点小摩擦(有时候也拌嘴吵架哈哈,但我通常都吵不过他~🤣),但都是小事,算是生活调剂吧,主要还是男女思维方式不一样,他比较偏理性,我有点偏感性😂



说起我和Z先生的缘分说着就有点绕了哈哈~,当时刚来上海找工作时,当时我和L小姐(我朋友,她当时跟我不在一个班)想找已经工作了的学长交流一下面试经验和技巧,通过老师介绍,就微信联系上了C君(Z先生他朋友),C君认识我和L小姐后,加上Z先生之前有跟C君说过和L小姐的亲戚关系(堂兄妹),得知L小姐还没有对象,所以动了追求的心思😂,因为我和L小姐是朋友,加上Z先生当时也是单身,所以C君私下就把我QQ推给了Z先生🤣。


加上QQ之后,前期我们基本一个月没有过沟通(主要还是因为没见过面不熟😂),后面因为L小姐打算去H城市(当时Z先生也在H城市),我们开始渐渐有了沟通,但也只是偶尔聊两句,前期联系比较少。


渐渐熟络起来源于Z先生带L小姐去游乐园玩发的一个QQ视频,我当时情绪正值低谷期,我自己算是感觉情绪很糟糕吧,偶然间看到了这个视频,感觉很欢乐,看完后我低落的情绪稍稍好了一些,我随即评论了一下,后面我们开始沟通真正多了起来。


我们QQ上熟络之后,对双方谈吐印象都很不错,所以我们后面准备开始见面了。由于Z先生在H城市,工作日互相没有时间,因此我们约定把时间订到了当时的五一。


af68df259a949ddd8b5a79a3ef10288.jpg


文章以待后续。。。。如果觉得文章写的不错,那就给个赞或者关注一下吧,你的支持将是我写文最大的动力!


近期文章预览


我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)


作者:梦周十
来源:juejin.cn/post/7292960995436527625
收起阅读 »

《我当程序媛那些年(二)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!



接上文~我当程序媛那些年(一)


搬家


选择了最终入职的公司之后,就开始准备搬家的事情了。由于公司距离当时住的地方比较远,当时刚来上海的时候也囊中羞涩,只能暂时住在公寓里面,公寓人很多,和学校的宿舍差不多,8张床,1张床800/月,也幸好没有久待,公寓里到处都是形形色色的人,没有自己的隐私空间。


找了距离公司附近地铁线的房子,公司在2号线徐泾东那边,早上上班坐大概6、7站,还算方便。房东也是一位老爷爷,人挺和善。新找的房子和暂住的公寓差不多的价格,房子在一楼,只有一个小窗户透气,空间狭小,仅有一张床、一个卫生间、一个可以放东西的桌子、衣柜是那种悬挂式的,整个房间大概就10-12平米左右的样子吧,虽然房间确实很小,但是我却很开心,因为我再也不需要和下班和别人挤着用卫生间,不用去公用的洗衣机间排队洗东西。


那是我从家里发生变故之后,到找到工作之前,精神一直紧绷着,我也不记得有多久没有那么发自内心开心过了,或许是因为难题都已被我自己慢慢解决,有了工作,我感觉我有了新的希望,对未来我抱有无限期许。


由于刚刚入职,年纪太小,也没有工作经验,最主要还是在试用期,想着表现好一点,所以当时没考虑请假。搬家就放在了周五下班后的时间,由于要先去住的地方拿东西(幸好东西也不是特别多,一趟能搞定),再到新的住址,当时也不知道有货拉拉这种app软件(都不知道货拉拉app啥时候发布的哈哈,只知道后面用到的时候已经有很多人在使用了~),所以到新住址整理下东西差不多就到凌晨了。


搬完家后,算是新生活正式开始了,前面有提到过是培训出来的,所以我自己独立搬出来后,就基本和原来培训同宿舍的没在怎么联系了,所谓道不同,不相为谋,加上本身我自己和她们家境差距甚大,在学校除了上课,周末就是在兼职,跟她们打交道不多,因此工作后也就跟他们基本断了联系了。


我对生活一直都算是属于那种积极向上的,属于那种性格坚韧的,虽然有的时候会经历一些坎坷,但只要咬咬牙能熬过,事后也只会赞叹自己一句我真棒勉励一下自己,瞬间觉得之前经历的事都不是事了。



ps: 只记得搬完家后的周末,那天阳光明媚,照在我的脸上,感觉恍若新生,我终于可以双手开始迎接我18岁之后崭新的人生。



69a6d3b6d5be4c867c9d7b24ec8d15e.jpg


48641b7e68e54476ec56d6dfee3ffd3.jpg


aca61df066cc55e73021ca96db0de5e.jpg


08560f208cf08a2e0547b4b0ab697dc.jpg


遇见老乡的意外惊喜


在上海偌大几千万人的人口城市,如果不是认识并且事先约定好见面的时间,遇见一个同省同城市同镇还是同村的老乡是觉得一件很让人觉得惊喜和开心的事情,就暂且用她名字的缩写吧,文中称她为xyq好了。


遇见她真的算是很有缘分,那天像往常一样准备上班,结果才出门不就,还未到地铁站,就下大雨,包里没带伞的我感觉有点手忙脚乱,周边也没有商店,纯纯的马路那种,没有遮挡物,我正准备想着索性一口气跑到地铁站算了,结果头上突然出现了一把伞,我回过头,旁边一个身高和我差不多、容貌清秀的小姐姐正举着伞打在我的头顶,我个子也不高,所以打伞还算不用过于费劲。


我先开了口谢谢她对我的帮助,她随后说看到下大雨了,我又是一个人,看着像没带伞的样子,索性她伞比较大,就一起去地铁站了。后面一路上由于不是很熟悉,路上气氛有点过于安静,我也想着跟小姐姐寒暄几句,缓解一下过于安静的气氛哈哈~,我问她是哪里人,她说也是湖南的,让我瞬间有点惊喜,后面再深入一点越聊越惊喜,结果发现是在不能近的同乡了,是我老家隔壁村的,距离我家就几百米远,由于我在家属于那种比较宅常年基本没事就不出门的,很多人都不认识,哈哈~


发现是同乡,我们话题瞬间就打开了,路上了解到小姐姐是做设计的,租的房子就离我不远,年纪虽然比我大两三岁,但我们俩却感觉很投缘。路上聊着聊着就到地铁站了,我们坐上地铁分别,那一整天心情都是开心愉悦的。


本来以为我们到这可能联系就不多了,没想到过了一周,某天下班在地铁站,看到地铁站里面旁边的娃娃机很多人在玩,感觉很有趣我停留了一会,突然听到有人叫我,回过头一看,竟然又遇见了她。我们都很开心,感觉缘分有的时候就是这么奇妙,不经意间就把两个毫无关系的人瞬间变成了朋友的缘分。


我们相约回家,之后我们联系也慢慢变多了起来,我们相互串门,做好吃的送给对方,后面我房租到期,我们索性就直接一起合租了一段时间,至于为什么是一段时间,是因为小姐姐后面工作变动,刚好也有朋友在广东那边,所以我们相处时间算是比较短暂,她后面就去广州了,虽然偶尔有联系,但终究由于工作繁忙还是聊的不是很多。


我和xyq的奇妙故事到这暂且就结束了,当然我们相遇的缘分并没有结束~,因为我去年回家过年去街上取钱遇见了她,不过此时的她已经结婚,我们又聊了很久,她已经生了一个,此时正身怀二胎,有一个体贴的丈夫,她从广州后面回到老家,在邮政工作,也算是编制人员了哈哈~,今年国庆我再次遇见了她,小孩长得玉雪可爱,灵动活泼,可爱极了。当然这些这算是后话了。。。。。


真的很高兴再次遇见她,看见她过的幸福开心我心里由衷的高兴。回想起和她的奇妙缘分,只感觉自己很幸运遇到了值得让我很惊喜的人和事。


d40567fa5a652796ef85d3b0ce15ab2.jpg


b9016b9dbfcc7d91142917b4c5fdda5.jpg


文章以待后续。。。。如果觉得文章写的不错,那就给个赞或者关注一下吧,你的支持将是我写文最大的动力!


近期文章预览

我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)


作者:梦周十
来源:juejin.cn/post/7291937010381684787
收起阅读 »

《我当程序媛那些年(一)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!


高考


高二时,由于我的理科实在是瘸脚的一比,因此文理分科时只能无奈选择了文科,选择文科的一大好处是,我再也不用看那种枯燥无味的物理公式,可以不在学习那些头疼的化学公式,可以逃避多了好几本选修的数学课本。


一直以来,数学是我的短板,从小数学及格的次数屈指可数,所以我一直都很羡慕那些数学逻辑思维特别好的人,哈哈,感觉他们都好聪明,数学不及格的我也注定了在高考上的失败。


高二会考过后,彻底进入最紧张的一年高三,由于数学太太太...差,差到什么程度呢?高考语文时记得当时好像是125左右吧,文综也才拿了230左右,但是数学才三十多分,英语90分,后面回想的时候就在想,要是我数学能多考几十分,也许也能和同龄人一样享受美好的大学生活了。


但也是由于数学实在太差的原因,当时甚至都想过去参加单招,都准备报名了,我妈专门赶到学校一再的阻止我,在我妈的一再劝说下,最终还是放弃单招了。


放弃单招之后,对于自己未来的出路想了很久,当时也觉得自己依照目前的数学成绩,评估了一下,考上三本啥的还有希望,二本估计够呛,所以早早的给自己规划出了明确的目标。由于家里有人接触过计算机这行业,虽然当时智能机刚普及不久,但隐约也感觉得到这是一个新兴崛起的行业,因此果断地将它纳入了我的未来规划中。


高考成绩出来后,果不其然在我的意料之后,填写完志愿,等待录取通知书,后面录取的是湖南株洲的一个铁道学院,当时高考志愿填了服从调剂,然后专业从计算机行业被调剂到电气化了,一方面是大专学校而且专业又调剂了,另一方面也考虑到家里的情况不足以支撑我能度过大学校园生活,当时想的是早点出来工作,所以后面也就没去了。


放弃去读大专的机会之后,趁着高考完那段时间,去北京打了一个月暑假工,逛了一下北京的颐和园,清华大学、北京三里屯,当时那天也赶巧,回来的时候看到有计算机的培训学校在招生,加上当时年龄也还未成年,想着培训个一年半差不多成年了就能出来工作了,因此果断选择入坑,也是从这个时候开始,算是正式踏入了互联网大门。



ps:这是我去北京当时看到的一些风景,去了北京的南锣鼓巷、清华园、水立方、鸟巢、奥林匹克公园,还看了70周年大阅兵,虽然未读大学,也算是人生一大遗憾吧,但如果重来,我还是不后悔会做同样的选择。



aa9866aafb2040e6bb67c42430751e4.jpg


6629b1df09d690515c76811722fcc34.jpg


4a0a7b7708e044fe7675c220f9191da.jpg


e26adcc29bcef9025c8f99261dc4d91.jpg


9d3209eaf6a7c3ba104114083017e57.jpg


ab0b88cad355b7ce25e689186e50e78.jpg


aac5cec72b9947cbaf17282e77728ed.jpg


ea58fd746cb6aadae270eb70d5023e1.jpg


变故


培训班的生活是漫长且枯燥无味的,总共是读三学期,刚开始去的第一学期,由于老师多教学水平也参差不齐,听的也是懵懵懂懂,加上当时也才高考完不久,因此也有点懈怠,学业上也没有那么用心,第一学期学的也是恍恍惚惚,主要学的是DIV、CSS、Jquery、JAVA基础啥的,会写一些简单的页面,第一学期结束时做了一个小项目算是对基础知识的掌握吧。


时间一晃而过,一眨眼就到了第二期下半年,也许人真的要经历磨砺才会有成长吧。天有不测风云,还记得那天下午,正是日落西山夕阳最美的时候,母亲突然打电话慌慌张张跟我说,家里出事了,具体什么事不好方便细说,只记得那一天记忆尤为深刻,那一天,我被迫成了独挡一面的大人。那一年,我18岁。


当时的我还未出去工作,还没有经济能力,当母亲跟我说家里出事,急需用到大笔钱,看着母亲为了解决家里的事情,低声下气受尽小辈言辞侮辱四处借钱的样子,尽管我还未出去工作,还是想办法问周边的同学看能不能借到点钱。


或许对人心太过敏感吧,我深知我最终的结果也不过是竹篮打水一场空,借不到什么,毕竟很少联系,突然借钱,人家也不会去借给你,很正常。但我当时已无路可走,还是硬着头皮去做了。最终借到了1800,至今为止我都很感激那给过我帮助的两位同学,我们到现在也还有联系,当然,这些都是后话了。。。。


也许看到这里会觉得疑惑,难道我身边没有什么朋友吗?其实有的,只是当时的年纪都在读书,手头基本都是父母给的,因此当时家里出事那一段时间,我第一想法没有想去找朋友,而是找已经有经济能力的同学看能不能帮助一下。


也许跟我本人性格也有点关系吧,我不大爱问别人借钱,不到万不得已借了也是想办法尽快还掉,总之,不喜欢欠钱的感觉。事后朋友得知这事说我为啥不找他们,能帮一点是一点,当时事情已经解决,也只是当闲谈后话了。


经历这一遭后,家里很长时间没有缓过来,母亲也跟我说家里没钱再供我继续读下去,但我还是不想放弃,那一刻我被迫长大,意识到很长时间内我的人生恐怕是一路艰辛了,我跟母亲让她别再操心我的事情,没有生活费的日子我周末就去做两天兼职,用于支撑下一周的生活费,依次反复,第三学期的学费后面跟学校老师沟通后,在百度申请了助学贷款,解决了学费的问题,虽然此后的两年内,一直在还着这个贷款了。。。。。。


没有钱足够支撑开支的日子分外难熬,周末两天的兼职有的时候并不足以支撑一周的生活费,为了顺利挨到周末,我只能减少吃饭的次数,只能买点便宜的零食和馒头度日,一个月下来,整个人迅速萧条,原本正常的体重也迅速掉秤到八十多斤,一个月没见到我的朋友都很惊讶,说怎么瘦的这么厉害,现在仔细想想,当时太年轻脑子也不灵活,傻的可以,也不知道去买箱泡面,至少也不至于去挨饿,当时性子也倔,吃不起饭也不想向别人求助,不过索性苦难都已过去,难熬的日子都熬过来了。


后面两学期的日子,我分外珍惜,我深知这是我最后一次机会,错过我将再也没有机会家里也没有能力在支撑我去学习的机会,那一年,埋头苦读,节假日也没有回家,一是没钱,二是也想多点时间学习。


功夫不负有心人,终于快熬到了第三学期快要结束的时候,由于出来工作需要买火车票、还有生活费,当时幸好学校有搞活动,靠着点赞群攒票的我拿到了票选的前三名,拿了最终的奖金,当时还是很开心的,因为终于体会到了第一次的得偿所愿,尽管艰辛困苦,但我永不言弃



ps: 只记得出事的那天,心情一下从清晨变天黑



216900704238292d795ff0bb62d665c.jpg


面试


最困难的事情解决后,在工作前过了一个最安稳的年,年后我怀着万分忐忑和期待离开了生活18年的小镇,终于踏上了前往上海的路程。带着对未来的无限期望,也抱着希望能快速找到工作解决身上所背负的贷款,我开始了疯狂投简历面试的过程。我深知我只有一个月的时间,也只有一次机会,只有紧紧抓住这次机会,我的未来才有无限可能。



庆幸时代的造就,也庆幸自己的直觉和眼光,庆幸一切的努力终究没有化成泡影.



17年当时的互联网还未完全发展到顶峰,不像如今很卷,那个时候互联网前景属于一片欣欣向荣的场景,很多独角兽公司处于初创阶段,还未完全崛起,面试机会很多,当时面试了大概二三十家吧,从刚开始面试紧张的磕磕巴巴到后面慢慢积攒经验,谈吐流利落落大方,最终收获了2个offer。后面选了回复最快的那家,薪资6.5K,顺利入职,我也终于不用在担心和无根的浮萍一样,最后只能落寞离去。


14f83f388ef385f2377d44fedf999ba.jpg


c94f9eaee75dc281404e15acccbdd28.jpg



ps: 当时收到入职offer,算是最开心的一天了



ff04aeb62f1cd78396cb7c8995b2cda.png


文章以待后续。。。。如果觉得文章写的不错,那就给个赞支持一下吧,你的支持将是我写文最大的动力!


最新文章预览

我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)

作者:梦周十
来源:juejin.cn/post/7291500185371623481
收起阅读 »

为啥一个 main 方法就能启动项目

在 Spring Boot 出现之前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat 或 Jetty),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。 在 IDE 中也需要对 Web 容器进行一...
继续阅读 »

在 Spring Boot 出现之前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat 或 Jetty),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。


在 IDE 中也需要对 Web 容器进行一些配置,才能够运行或者 Debug。而使用 Spring Boot 我们只需要像运行普通 JavaSE 程序一样,run 一下 main() 方法就可以启动一个 Web 应用了。这是怎么做到的呢?今天我们就一探究竟,分析一下 Spring Boot 的启动流程。


概览


回看我们写的第一个 Spring Boot 示例,我们发现,只需要下面几行代码我们就可以跑起一个 Web 服务器:


@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}


去掉类的声明和方法定义这些样板代码,核心代码就只有一个 @SpringBootApplication 注解和 SpringApplication.run(HelloApplication.class, args) 了。而我们知道注解相当于是一种配置,那么这个 run() 方法必然就是 Spring Boot 的启动入口了。


接下来,我们沿着 run() 方法来顺藤摸瓜。进入 SpringApplication 类,来看看 run() 方法的具体实现:


public class SpringApplication {
......
public ConfigurableApplicationContext run(String... args) {
// 1 应用启动计时开始
StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 2 声明上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;

// 3 设置 java.awt.headless 属性
configureHeadlessProperty();

// 4 启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 5 初始化默认应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

// 6 准备应用环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);

// 7 打印 Banner(Spring Boot 的 LOGO)
Banner printedBanner = printBanner(environment);

// 8 创建上下文实例
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);

// 9 构建上下文
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

// 10 刷新上下文
refreshContext(context);

// 11 刷新上下文后处理
afterRefresh(context, applicationArguments);

// 12 应用启动计时结束
stopWatch.stop();
if (this.logStartupInfo) {
// 13 打印启动时间日志
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

// 14 发布上下文启动完成事件
listeners.started(context);

// 15 调用 runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 16 应用启动发生异常后的处理
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
// 17 发布上下文就绪事件
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
......
}


Spring Boot 启动时做的所有操作都这这个方法里面,当然在调用上面这个 run() 方法之前,还创建了一个 SpringApplication 的实例对象。因为上面这个 run() 方法并不是一个静态方法,所以需要一个对象实例才能被调用。


可以看到,方法的返回值类型为 ConfigurableApplicationContext,这是一个接口,我们真正得到的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名我们可以知道,这是一个基于注解的 Servlet Web 应用上下文(我们知道上下文(context)是 Spring 中的核心概念)。


上面对于 run() 方法中的每一个步骤都做了简单的注释,接下来我们选择几个比较有代表性的来详细分析。


应用启动计时


在 Spring Boot 应用启动完成时,我们经常会看到类似下面内容的一条日志:


Started AopApplication in 2.732 seconds (JVM running for 3.734)

应用启动后,会将本次启动所花费的时间打印出来,让我们对于启动的速度有一个大致的了解,也方便我们对其进行优化。记录启动时间的工作是 run() 方法做的第一件事,在编号 1 的位置由 stopWatch.start() 开启时间统计,具体代码如下:


public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
}
// 记录启动时间
this.currentTaskName = taskName;
this.startTimeNanos = System.nanoTime();
}


然后到了 run() 方法的基本任务完成的时候,由 stopWatch.stop()(编号 12 的位置)对启动时间做了一个计算,源码也很简单:


public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {
throw new IllegalStateException("Can't stop StopWatch: it's not running");
}
// 计算启动时间
long lastTime = System.nanoTime() - this.startTimeNanos;
this.totalTimeNanos += lastTime;
......
}


最后,在 run() 中的编号 13 的位置将启动时间打印出来:


if (this.logStartupInfo) {
// 打印启动时间
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}


打印 Banner


Spring Boot 每次启动是还会打印一个自己的 LOGO,如图:


在这里插入图片描述


这种做法很常见,像 Redis、Docker 等都会在启动的时候将自己的 LOGO 打印出来。Spring Boot 默认情况下会打印那个标志性的“树叶”和 “Spring” 的字样,下面带着当前的版本。


在 run() 中编号 7 的位置调用打印 Banner 的逻辑,最终由 SpringBootBanner 类的 printBanner() 完成。这个图案定义在一个常量数组中,代码如下:


class SpringBootBanner implements Banner {

private static final String[] BANNER = {
"",
" . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\",
"( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )",
" ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/"
};
......

public void printBanner(Environment environment, Class sourceClass, PrintStream printStream) {
for (String line : BANNER) {
printStream.println(line);
}
......
}

}


手工格式化了一下 BANNER 的字符串,轮廓已经清晰可见了。真正打印的逻辑就是 printBanner() 方法里面的那个 for 循环。


记录启动时间和打印 Banner 代码都非常的简单,而且都有很明显的视觉反馈,可以清晰的看到结果。拿出来咱们做个热身,配合断点去 Debug 会有更加直观的感受,尤其是打印 Banner 的时候,可以看到整个内容被一行一行打印出来,让我想起了早些年用那些配置极低的电脑(还是 CRT 显示器)运行着 Win98,经常会看到屏幕内容一行一行加载显示。


创建上下文实例


下面我们来到 run() 方法中编号 8 的位置,这里调用了一个 createApplicationContext() 方法,该方法最终会调用 ApplicationContextFactory 接口的代码:


ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
};


这个方法就是根据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创建不同类型的应用上下文(context)。而属性 webApplicationType 的值是在前面执行构造方法的时候由 WebApplicationType.deduceFromClasspath() 获得的。通过方法名很容易看出来,就是根据 classpath 中的类来推断当前的应用类型。


我们这里是一个普通的 Web 应用,所以最终返回的类型为 SERVLET。所以会返回一个 AnnotationConfigServletWebServerApplicationContext 实例。


构建容器上下文


接着我们来到 run() 方法编号 9 的 prepareContext() 方法。通过方法名,我们也能猜到它是为 context 做上台前的准备工作的。


private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner)
{
......
// 加载资源
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}


在这个方法中,会做一些准备工作,包括初始化容器上下文、设置环境、加载资源等。


加载资源


上面的代码中,又调用了一个很关键的方法——load()。这个 load() 方法真正的作用是去调用 BeanDefinitionLoader 类的 load() 方法。源码如下:


class BeanDefinitionLoader {
......
void load() {
for (Object source : this.sources) {
load(source);
}
}

private void load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class) {
load((Class) source);
return;
}
if (source instanceof Resource) {
load((Resource) source);
return;
}
if (source instanceof Package) {
load((Package) source);
return;
}
if (source instanceof CharSequence) {
load((CharSequence) source);
return;
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
......
}


可以看到,load() 方法在加载 Spring 中各种资源。其中我们最熟悉的就是 load((Class) source) 和 load((Package) source) 了。一个用来加载类,一个用来加载扫描的包。


load((Class) source) 中会通过调用 isComponent() 方法来判断资源是否为 Spring 容器管理的组件。 isComponent() 方法通过资源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在内)来区分是否为 Spring 容器管理的组件。


而 load((Package) source) 方法则是用来加载 @ComponentScan 注解定义的包路径。


刷新上下文


run() 方法编号10 的 refreshContext() 方法是整个启动过程比较核心的地方。像我们熟悉的 BeanFactory 就是在这个阶段构建的,所有非懒加载的 Spring Bean(@Controller、@Service 等)也是在这个阶段被创建的,还有 Spring Boot 内嵌的 Web 容器要是在这个时候启动的。


跟踪源码你会发现内部调用的是 ConfigurableApplicationContext.refresh(),ConfigurableApplicationContext 是一个接口,真正实现这个方法的有三个类:AbstractApplicationContext、ReactiveWebServerApplicationContext 和 ServletWebServerApplicationContext。


AbstractApplicationContext 为后面两个的父类,两个子类的实现比较简单,主要是调用父类实现,比如 ServletWebServerApplicationContext 中的实现是这样的:


public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw ex;
}
}


主要的逻辑都在 AbstractApplicationContext 中:


@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

// 1 准备将要刷新的上下文
prepareRefresh();

// 2 (告诉子类,如:ServletWebServerApplicationContext)刷新内部 bean 工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 3 为上下文准备 bean 工厂
prepareBeanFactory(beanFactory);

try {
// 4 允许在子类中对 bean 工厂进行后处理
postProcessBeanFactory(beanFactory);

StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// 5 调用注册为 bean 的工厂处理器
invokeBeanFactoryPostProcessors(beanFactory);

// 6 注册拦截器创建的 bean 处理器
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();

// 7 初始化国际化相关资源
initMessageSource();

// 8 初始化事件广播器
initApplicationEventMulticaster();

// 9 为具体的上下文子类初始化特定的 bean
onRefresh();

// 10 注册监听器
registerListeners();

// 11 实例化所有非懒加载的单例 bean
finishBeanFactoryInitialization(beanFactory);

// 12 完成刷新发布相应的事件(Tomcat 就是在这里启动的)
finishRefresh();
}

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

// 遇到异常销毁已经创建的单例 bean
destroyBeans();

// 充值 active 标识
cancelRefresh(ex);

// 将异常向上抛出
throw ex;
} finally {
// 重置公共缓存,结束刷新
resetCommonCaches();
contextRefresh.end();
}
}
}


简单说一下编号 9 处的 onRefresh() 方法,该方法父类未给出具体实现,需要子类自己实现,ServletWebServerApplicationContext 中的实现如下:


protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

private void createWebServer() {
......
if (webServer == null && servletContext == null) {
......

// 根据配置获取一个 web server(Tomcat、Jetty 或 Undertow)
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
......
}
......
}


factory.getWebServer(getSelfInitializer()) 会根据项目配置得到一个 Web Server 实例,这里跟下一篇将要谈到的自动配置有点关系。


作者:刘水镜
来源:juejin.cn/post/7206749400172380219
收起阅读 »

对当前就业以及自身现状的一些思考

趁着今天是1024节,写一下离职程序员的感概。自从8月份离职后,到现在已经3个月了,期间碰到很多让我精神内耗以及思考人生的事,那个谁谁谁结婚了,那个谁谁谁买车了等等等等。反观自己,还是一个浑浑噩噩的事业完无成的单身的无业游民。年龄比我大的,比我小的感觉都比我优...
继续阅读 »

趁着今天是1024节,写一下离职程序员的感概。自从8月份离职后,到现在已经3个月了,期间碰到很多让我精神内耗以及思考人生的事,那个谁谁谁结婚了,那个谁谁谁买车了等等等等。反观自己,还是一个浑浑噩噩的事业完无成的单身的无业游民。年龄比我大的,比我小的感觉都比我优秀,而我只会写两行代码。


我不禁有时候会想,这个问题是由于什么原因引起的。试着从以下几个角度来分析一下自己失败的原因。


社会


在之前的那篇《关于我工作踩大坑的事》的文章里面有说过,上一份工作是真的被套路了,具体请看主页里面的文章。


我认为,在这个社会里面人与人之间的交流应该都是真诚的,而不是尔虞我诈连说句话都是套路。就上个工作来说,主管和我约谈的时候,我能感觉到他说话都在绕着圈子,变个法子和我说这个问题怎么怎么样。虽然我不是很懂这些管理话术,但是我还是能察觉到里面的一些端倪。


这个是基于我试用期超过了6个月而且没有签合同的情况下,我主动找上司约谈合同的情景下的对话


“如果你没有意见的话,那我们就继续保持这个关系”


话里意思就是“转正?转什么正,转正是不可能转正的,你爱干干,不干滚”


他问我有没有意见,那我肯定有意见的阿,社保不买,福利没有,合同没有,只有干活。到头来连个正式员工都不是了,不需要我的时候就爱干干,不干滚。不带这样玩的吧,入职之前说好的试用期三个月,三个月又三个月,最后连员工都不是。


image.png


“那边的部门刚刚走了个产品经理,你要是愿意的话我就推荐你去,然后那边会约你做一个面谈,如果可以的话那就可以正式签约”


虽然当时就觉得是大饼,但是我还是抱着一点点点点点希望等啊等,一个半月过去了,杳无音信。现在想想我真的很单纯。


下一份工作,入职不签合同的话我都要仔细考虑考虑要不要入职了,求职路上,全是套路!!


就业


自从去年12月底开始,整个互联网甚至全国经济都萎靡不振,各行各业都在裁员,包括阿里,微软这种互联网巨头。被裁的人分散到全国各地,竞争力一下子上来了,就比如我是个普通本科生,而我的竞争者都是985/211甚至哈佛等等这种世界一流名校毕业的。抢不过,根本抢不过,臣妾做不到啊。


image.png


然后吧,随着恒大的暴雷到许老板上头条,这些年楼市都一般般,没有之前那样的活跃了。


楼市的活跃度下降带来的资金资金问题影响着企业,而企业的资金问题就影响着企业的发展,谁也不想自己的钱就这么打水漂了。谨慎的投资带来了谨慎的招聘,前几年的金三银四,金九银十,在今年好像都没怎么听过了,而且各种电商平台的大活动也不公布订单数据了,可想而知。


image.png


这个薪资你认真的吗?租个房就没了还要倒贴。我不理解,但我大受震撼。


身边


打开朋友圈,那个小学同学生娃了,那个大学同学办婚礼了,隔壁屋买车了,反观自己,好像前面三样一个没有阿。这些人里面有年龄比我大的,也有比我小的,别人都家庭美满儿孙满堂了,而我只能在电脑桌面前写写文章。


想到这些我不禁在想,以前不是说读书越多就越成功吗?我寻思我也是个本科毕业吧,也没差到哪里去,为什么我三不沾呢(没车没孩子没女友),而我身边的早早就出来打拼现在幸福美满。


我依稀记得我妈2年前的一句话“哎,你们都出来赚大钱了,我儿子还背着个书包负资产”,现在想想也是,读那么多书,出来工作也是月薪3000,高中毕业也是月薪3000。那我多花的这几年时间到底是为了什么呢,可能是3000的岗位的舒适性吧大概。


可能有人会说,那些早早出来打拼的,别人家庭环境比我好,这个我不反驳,这也确实,谁不想有个富老爸老妈呢,谁不想做个富二代呢?


抛开别人富裕家庭不说,拼搏得来的幸福生活那是别人应得的,还有就是能抓住机遇被幸运女神眷顾的,运气也是实力的一部分,不是吗?我也想被幸运女神眷顾啊,我也想当一把幸运儿,上一份工作已经够倒霉了。


鸡汤


Dont worry, be happy! 反正开心不开心也要过一天,为什么不开开心心的过呢?俗话说,笑一笑十年少,人啊也就2w天,能过一天是一天,开心最重要。这段时间精神内耗很严重,要不是有个好基友,我可能就玉玉了。


不开心的时候看点开心的事物,比如猫猫!!这个世界真的不能没有猫猫吧。


image.pngimage.png


我想这段时间好好改改熬夜的坏习惯(最近11点睡觉,早起真的很爽),还有去锻炼一下瘦一下肚子,都快变成米其林先生了。当然了,自我增值也非常的重要,打算和朋友们讨论交流一下技术以及学习提升自己的技能,不然竞争者个个都是985/211真的抢不过啊。


欢迎大家在评论区留言,有不足的地方请指出。


作者:Avalon2353
来源:juejin.cn/post/7293304986867679283
收起阅读 »

手把手教你打造一个“蚊香”式加载

web
前言 这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。 效果预览 从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。 HTML布局 首先我们通过1...
继续阅读 »

前言


这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。


效果预览



从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。


HTML布局


首先我们通过15span子元素来实现金钟罩的每一层,用于创建基本结构。从专业术语上讲,每个span元素都代表加载动画中的一个旋转的小点。通过添加多个span元素,可以创建出一串连续旋转的小点,形成一个加载动画的效果。


<div class="loader">
<span></span>
// 以下省略15span元素
</div>

CSS设计


完成了基本的结构布局,接下来就是为它设计CSS样式了。我们一步一步来分析:


首先是类名为loaderCSS类,相关代码如下。


.loader{
position: relative;
width: 300px;
height: 300px;
transform-style: preserve-3d;
transform: perspective(500px) rotateX(60deg);
}

我们将元素的定位方式设置为相对定位,使其相对于其正常位置进行定位。然后定义好宽度和高度之后,设置元素的变换样式为preserve-3d,这样可以元素的子元素也会受到3D变换的影响。除此之外,还需要transform属性来设置元素的变换效果。这里的perspective(500px)表示以500像素的视角来观察元素,rotateX(60deg)则表示绕X轴顺时针旋转60度。


这样就将一个宽高都定义好的元素进行了透视效果的3D旋转,使其以60度角度绕X轴旋转。


loader类可以理解为父容器,接下来就是loader类中的子元素span


.loader span{
position: absolute;
display: block;
border: 5px solid #fff;
box-shadow: 0 5px 0 #ccc,
inset 0 5px 0 #ccc;
box-sizing: border-box;
border-radius: 50%;
animation: animate 3s ease-in-out infinite;
}

通过以上样式,我们可以创建一个圆形的动画效果,边框有阴影效果,并且以动画的方式不断旋转。关于CSS部分大部分都是一样的,这里主要介绍一下这里定义的动画效果。名称为animate,持续时间为3秒,缓动函数为ease-in-out,并且动画无限循环播放。


@keyframes animate {
0%,100%{
transform: translateZ(-100px);
}
50%{
transform: translateZ(100px);
}
}

这是一个关键帧动画。关键帧是指动画在不同时间点上的状态或样式。首先该动画名为animate,它包含了三个时间点的样式变化:


0%100% 的时间点,元素通过transform: translateZ(-100px)样式将在Z轴上向后移动100像素,这将使元素远离视图。


50% 的时间点,元素通过transform: translateZ(100px)样式将在Z轴上向前移动100像素。这将使元素靠近视图。


通过应用这个动画,span元素将在动画的持续时间内以一定的速率来回移动,从而产生一个视觉上的动态效果。


最后就是单独为每个子元素span赋予样式了。


.loader span:nth-child(1){
top: 0;
left: 0;
bottom: 0;
right: 0;
animation-delay: 1.4s;
}
.loader span:nth-child(2){
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
animation-delay: 1.3s;
}
......
以下省略到第15span元素

第一个span元素的样式设置了top、left、bottom和right属性为0,这意味着它会填充父元素的整个空间。它还设置了animation-delay属性为1.4秒,表示在加载动画开始之后1.4秒才开始播放动画。


后面14span元素都是按照这个道理,以此类推即可。通过给span元素的动画延迟属性的不同设置,可以实现加载动画的错落感和流畅的过渡效果。


总结


以上就是整个效果的实现过程了,通过设计的动画来实现这个蚊香式加载,整体还是比较简单的。大家可以去码上掘金看看完整代码,然后自己去尝试一下,如果有什么创新的地方或者遇到了什么问题欢迎在评论区告诉我~


作者:一条会coding的Shark
来源:juejin.cn/post/7291951762948259851
收起阅读 »

语雀,这波故障,放眼整个互联网也是炸裂般的存在。

你好呀,我是歪歪。 昨天语雀凉了一下午,哦,不对,下午一般是指 12 点到 18 点的这 6 个小时。 语雀是从下午 14 点到晚上 22 点多,凉了 8 小时有余。 这故障时长,放眼整个互联网也是炸裂般的存在。 我掐指一算,要是再晚个半小时修复,差点连 ...
继续阅读 »

你好呀,我是歪歪。




昨天语雀凉了一下午,哦,不对,下午一般是指 12 点到 18 点的这 6 个小时。


语雀是从下午 14 点到晚上 22 点多,凉了 8 小时有余。


这故障时长,放眼整个互联网也是炸裂般的存在。


我掐指一算,要是再晚个半小时修复,差点连 3 个 9 的(99.9%)可用性都保证不了。


如果你不知道语雀的话,我先用一句话给你铺垫一下:语雀是孵化自蚂蚁集团的。


这是它官网的自我介绍:



背靠蚂蚁,这样你再想想长达 8 小时的宕机,是不是就更加的有点匪夷所思了。


说好的高可用呢?八股文拿出来翻翻啊。


作为程序员,大家聊到这里的时候,一遍都会谈到高可用、容灾备份、两地三中心、异地多活、同城双活这些玩意..


这些东西大家聊起来都不算陌生,常常也出现于面试环节。


但是真的要做起来,是很困难的,是要以年度为时间单位进行推进的。


歪师傅没有搞过两地三中心,但是我见证过两地三中心从零开始搭建的全过程,可以说是举公司全体科技之力,耗费了巨大的人力物力,研发成本,燃烧了一个又一个老运维,才把这玩意推上去。


然而这玩意搭建起来之后,从来没有正式使用过。


没有使用,就是最好的结果。


虽然一次都没有正式使用,但是每年的灾备演练是必不可少的,演练一次,至少准备一个月,而且每年演练两次。


当场拔网线,模拟火灾报警,地震来袭,光纤挖断啥的,你以为这些是开玩笑的?


都是有预案的。宕机时间长了,


但是经过这次这个事情,我准备下次在演练的时候提一个意见:演练操作手册,电子版搞一份,打印版搞一份。万一真遇到的事情的时候,电子版打不开了,岂不是尴尬。



这件事儿也给大家提了个醒,自己写的文档,记得还是在本地留存一份。


本地化的文档,然后上传到各个云上,才是相对稳妥的方案。


比如歪师傅,写了这么多文档,在本地都有一份 md 格式的文件。我都是先用 markdown 格式在有道云上写好,没有花里胡哨的东西,写完之后只需要一键 CV 保存到本地:



不要太相信云端,我用有道云之前也丢过数据,写了一篇文章,莫名其妙的少了一大把,还找不回来。


你说气不气人嘛。


现在还能卡我脖子的就是腾讯云了,因为我的图床用的是腾讯云的图床:



要是腾讯云的对象存储挂了,那我文章中的图片也就挂了。


图片挂了,表情包也就没有了。


表情包都没有了,看文章还有啥意思啊。



这次事件之后,语雀如果不尽快给出补偿方案和离线功能,应该会流失一部分用户吧。


其他的各类笔记应用,也蠢蠢欲动,在相关的帖子下面进行宣传,想把这部分用户引流到自家产品之上。


笔记类软件现在是很卷的,除了大家耳熟能详的有道云、印象笔记、网易、为知,现在的 Notion、obsidian、Logseq......以及越来越多的本地软件,几乎可以说,每个大厂都有各自的笔记类产品,有的是偏向于在线 文档,比如金山文档、腾讯文档。有的是夹在办公软件里面,以协同为主,比如飞书文档。


这是语雀面临的外部竞争。


而语雀作为阿里系,内部还有一个钉钉文档与之赛马,而语雀的创始人玉伯于今年 4 月底离开蚂蚁,传言入职了飞书。


这就很巧了,飞书文档也很厉害。


语雀,这波属实焦灼,内忧外患啊。


虽然破局很难,但是互联网从来不缺少绝地反击,绝处逢生的故事。


语雀这次的事件确实处理的非常不好,但是这并不妨碍它是一个优秀的文档记录、协作类的产品。


然而互联网也是残酷和逐利的,当前的内忧外患之下,任何一个错误都有可能被放大,然后变成致命一击。


面对来势汹汹的这一招致命一击,语雀怎么去“接化发”。



接下来的故事,就看语雀怎么去写了。



我很期待,写出一个蜿蜒曲折、绝地反击的故事。


最后,我单方面的为“网络故障”发声,每次任何一个厂出任何一个问题要写对外公告的时候,第一个出来顶锅的,一定是“网络故障”。


惨,实在是太惨了。



作者:why技术
来源:juejin.cn/post/7293168604614377507
收起阅读 »

【Java集合】来了两个“插班生”如何打印花名册,以数组案例带你搞懂Collection集合概念

嗨~ 今天的你过得还好吗?到那时风变得软绵绵的🌞1.1 数组的特点步骤:有三个学生,放在一个长度为3的数组花名册打印学生突然来了两个插班生,请放在数组花名册中无法插入,通过重新定义一个新的数组,组成新的花名册下面我们来实现这个案例:2.输入...
继续阅读 »


嗨~ 今天的你过得还好吗?

每件事情都会好起来的

到那时风变得软绵绵的

阳光也会为你而温暖

🌞


这个系列是我在学习Java集合这部分时候,结合书籍和Java提供的api整理的部分知识,也参考了一些网络上的文章,如果错误,望大家指出。希望本系列文章对大家学习Java有所帮助,也可以回顾下这部分的基础知识,温故而知新。


集合概述

1.1 数组的特点

Java是一种面向对象语言,对一个事物的描述都是以对象的形式存在,为了方便操作这些对象,就需要把这些对象存储起来。为容纳一组对象,我们最适宜的选择就是Array数组;而且容纳一系列的基础数据类型的话,更是必须采用数组。


我们通过一个小案例来回顾一下之前的数组知识。数组不仅可以存放基本数据类型也可以容纳属于同一种类型的对象。


数组案例:一个小班有三个学生,请打印学生的姓名和年龄?

步骤:

  • 有三个学生,放在一个长度为3的数组花名册

  • 打印学生

  • 突然来了两个插班生,请放在数组花名册中

  • 无法插入,通过重新定义一个新的数组,组成新的花名册


下面我们来实现这个案例:

1.首页创建一个 javaee 的项目

Description


2.输入名称 collectPractice,选择对应的 JDK 版本 1.8

Description


3.新增 Class  student

Description

4.文件内容如下:

public class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里可以查看

5.在 Main.java 中,我们将要完成的需求写到注释中,逐行去打印结果

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

结果:

Description

6.来了两个新学生,也要加入到花名册中,直接使用数组添加,打印花名册,发现报错

import java.util.Arrays;

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

students[3] = student4;
students[4] = student5;

System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

打印结果

Description


7.所以在此时,我们需要重新 new 一个 长度为 5 的数组,重新设置新的花名册

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

// students[3] = student4;
// students[4] = student5;
//
// System.out.println("学生花名册---"+ Arrays.toString(students));

Student[] studentsNew = new Student[5];
for (int i = 0; i < students.length; i++) {
studentsNew[i] = students[i];
}
studentsNew[3] = student4;
studentsNew[4] = student5;


System.out.println("学生花名册---"+Arrays.toString(students));
System.out.println("学生新的花名册---"+Arrays.toString(studentsNew));
}
}

打印结果:

Description


分析结论

  • 数组长度在初始化后,就确定了,不能更改,不便于存储数量的扩展。比如我们再来了两个插班生,直接往元素组添加元素,会报错误信息。

  • 数组提供的属性和方法少,不便于操作。比如我们在打印花名册时,需要借助工具类的toString方法。

  • 存储的类型可以是基本类型,也可以是对象,但是必须是同一类型。


因为数组存在的这些缺点,Java语言又为我们提供了一种新的存储数据并且存储空间可变的容器,这就是我们Java集合的概念。它和数组一样,都是可以存储数据的容器,一种存储空间可变的存储模型,并且存储的数据容量可以随时发生改变。


1.2 集合的特点

最后我们来总结一下集合的特点:

  • 可以动态保存任意多的对象,使用方便;

  • 集合提供了一系列操作元素的方法,使集合元素的添加和修改等操作变得简单;

  • 集合还可以保存具有映射关系的关联数据;

  • 集合只能保存对象,实际上保存的是对象的引用地址。


文章就写到这里了,觉得不错的话点个赞支持一下吧!


收起阅读 »

他们在学校里不会教你的编程原则

前言 在大学的时候,学校一般只会教你你写编程语言,比如C、C++、JAVA等编程语言。但是当你离开大学进入这个行业开始工作时,才知道编程不只是知道编程语言、语法等,要想写好代码,必须还要了解一些编程原则才行。本文主要讨论KISS、DRY和SOLID这些常见的编...
继续阅读 »

前言


在大学的时候,学校一般只会教你你写编程语言,比如C、C++、JAVA等编程语言。但是当你离开大学进入这个行业开始工作时,才知道编程不只是知道编程语言、语法等,要想写好代码,必须还要了解一些编程原则才行。本文主要讨论KISSDRYSOLID这些常见的编程原则,而且你会发现随着工作时间越久,越能感受这些编程原则的精妙之处,历久弥香。


KISS原则



Keep It Simple, Stupid!



你是不是有过接手同事的代码感到十分头疼的经历,明明可以有更加简单、明白的写法,非要绕来绕去,看不明白?


其实,我们在写代码的时候应该要遵守KISS原则,核心思想就是尽量保持简单。代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。


我们写代码的的时候要站在别人的角度出发,就像马丁·福勒说的,我们写的代码不是给机器看的,而是给人看的。


“任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写出人类可以理解的代码。” — 马丁·福勒


那么如何才能写出满足KISS原则的代码呢?


如何写出KISS原则的代码?


我们直接上例子,下面的校验IP是否合法的3种实现方式,大家觉得哪个最KISS?



  1. 写法一




  1. 写法二




  1. 写法三




  • 写法一代码量最少,正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则的设计初衷上来讲,这种实现方式并不符合 KISS 原则。

  • 写法二使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP地址字符串,逻辑清晰,可读性好。

  • 写法三不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法,容易出bug,不好理解。


所以说,符合KISS原则的代码并不是代码越少越好,还要考虑代码是否逻辑清晰、是否容易理解、是否够稳定。


总结以下如何写出KISS原则的代码:



  1. 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。

  2. 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出bug 的概率会更高,维护的成本也比较高。

  3. 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

  4. 主观站在别人的角度上编写代码。你在编写代码的时候就要思考我这个同事看这段代码是不是很快就能够明白理解。


DRY原则



Don't Repeat Yourself



你是不是有过这样的经历,项目中很多重复逻辑的代码,然后修改一个地方,另外一个地方忘记修改,导致测试给你提了很多bug?


DRY原则,英文全称Don’t Repeat Yourself,直译过来就是不要重复你自己。这里的重复不仅仅是代码一模一样,还包括实现逻辑重复、功能语义重复、代码执行重复等。我们不要偷懒,有责任把这些存在重复的地方识别出来,然后优化它们。


如何写出DRY原则的代码呢?


我们直接上例子,代码重复的我就不讲了,很好理解,关于实现逻辑或者功能语义重复的我觉个例子。


还是上面校验IP的例子,团队中两个同事由于不知道就有了两种写法。



  • 同事A写法




  • 同事B写法



尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。不然哪天校验规则变了,很容易只改了其中一个,另外一个漏改,就会出现莫名其妙的bug


其他的比如逻辑重复的意思是虽然功能是不一致的,但是里面的逻辑都是一模一样的。举个例子,比如校验用户名和校验密码,虽然功能不一致,但是校验逻辑都是相似,判空、字符长度等等,这种情况我们就需要把相似的逻辑抽取到一个方法中,不然也是不符合DRY原则。


那么我们平时写代码注意些什么才是符合DRY原则呢?



  • 使用现成的轮子,不轻易造轮子


其实最关键的就是写代码带脑子,用到一个方法先看看有没有现成的,不要看看不看,就动手在那里造轮子。



  • 减少代码耦合


对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。



  • 满足单一职责原则


我们前面讲过,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。



  • 模块化


这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。



  • 业务与非业务逻辑分离


越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。



  • 通用代码下沉


从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。



  • 继承、多态、抽象、封装


在讲面向对象特性的时候,我们讲到,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。



  • 应用模板等设计模式


一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。


SOLID原则


SOLID原则不是一个单一的原则,而是对软件开发至关重要的 5 条原则,遵循这些原则有助于我们写出高内聚、低耦合、可扩展、可维护性好的代码。


S—单一职责原则



一个类应该有一个,而且只有一个改变它的理由。



单一职责原则在我看来是最容易理解也是最重要的一个原则。它的核心思想就是一个模块、类或者方法只做一件事,只有一个职责,千万不要越俎代庖。它可以带来下面的好处:



  • 可以让代码耦合度更低

  • 使代码更容易理解和维护

  • 使代码更易于测试和维护,使软件更易于实施,并有助于避免未来更改的意外副作用


举个例子,我们有两个类PersonAccount。 两者都负有存储其特定信息的单一责任。 如果要更改Person的状态,则无需修改类Account,反之亦然, 不要把账户的行为比如修改账户名changeAcctName写在Person类中。


    public class Person {
private Long personId;
private String firstName;
private String lastName;
private String age;
private List accounts;

// 错误做法
public void changeAcctName(Account account, String acctName) {
acccount.setAccountName(acctName);
// 更新到数据库
}
}

public class Account {
private Long guid;
private String accountNumber;
private String accountName;
private String status;
private String type;

}

所以大家在编写代码的时候,一定要停顿思考下这个段代码真的写在这里吗?另外很关键的一点是如果发现一个类或者一个方法十分庞大,那么很有可能已经违背单一职责原则了,后续维护可想而知十分痛苦。


O—开闭原则



软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。



对扩展开放,对修改关闭,什么意思?很简单,其实就是我们要尽量通过新增类实现功能,而不是修改原有的类或者逻辑。因为修改已有代码很有可能对已有功能引入bug。


让我们通过一个例子来理解这个原则,比如一个通知服务。


    public class NotificationService {
public void sendOTP(String medium) {
if (medium.equals("email")) {
//email 发送
} else if (medium.equals("mobile")) {
// 手机发送
}
}

现在需要新增微信的方式通知,你要怎么做呢? 是在加一个if else吗? 这样就不符合开闭原则了,我们看下开闭原则该怎么写。



  • 定义一个通知服务接口



public interface NotificationService {
public void sendOTP();
}


  • E-mail方式通知类EmailNotification



public class EmailNotification implements NotificationService{
public void sendOTP(){
// write Logic using JavaEmail api
}
}


  • 手机方式通知类MobileNotification



public class MobileNotification implements NotificationService{
public void sendOTP(){
// write Logic using Twilio SMS API
}
}


  • 同样可以添加微信通知服务的实现WechatNotification



public class WechatNotification implements NotificationService{
public void sendOTP(String medium){
// write Logic using wechat API
}
}

这样的方式就是遵循开闭原则的,你不用修改核心的业务逻辑,这样可能带来意向不到的后果,而是扩展实现方式,由调用方根据他们的实际情况调用。


是不是想到了设计模式中的策略模式,其实设计模式就是指导我们写出高内聚、低耦合的代码。


L—里氏替换原则



派生类或子类必须可替代其基类或父类



这个原则稍微有点难以理解,它的核心思想是每个子类或派生类都应该可以替代/等效于它们的基类或父类。这样有一个好处,就是无论子类是什么类型,客户端通过父类调用都不会产生意外的后果。


理解不了?那我我们通过一个例子来理解一下。


让我们考虑一下我有一个名为 SocialMedia 的抽象类,它支持所有社交媒体活动供用户娱乐,如下所示:


    package com.alvin.solid.lsp;

public abstract class SocialMedia {

public abstract void chatWithFriend();

public abstract void publishPost(Object post);

public abstract void sendPhotosAndVideos();

public abstract void groupVideoCall(String... users);
}

社交媒体可以有多个实现或可以有多个子类,如 FacebookWechatWeiboTwitter 等。


现在让我们假设 Facebook 想要使用这个特性或功能。


    package com.alvin.solid.lsp;

public class Wechat extends SocialMedia {

public void chatWithFriend() {
//logic
}

public void publishPost(Object post) {
//logic
}

public void sendPhotosAndVideos() {
//logic
}

public void groupVideoCall(String... users) {
//logic
}
}

我们都知道Facebook都提供了所有上述的功能,所以这里我们可以认为FacebookSocialMedia类的完全替代品,两者都可以无中断地替代。


现在让我们讨论 Weibo


    package com.alvin.solid.lsp;

public class Weibo extends SocialMedia {
public void chatWithFriend() {
//logic
}

public void publishPost(Object post) {
//logic
}

public void sendPhotosAndVideos() {
//logic
}

public void groupVideoCall(String... users) {
//不适用
}
}

我们都知道Weibo微博这个产品是没有群视频功能的,所以对于 groupVideoCall方法来说 Weibo 子类不能替代父类 SocialMedia。所以我们认为它是不符合里式替换原则。


如果强行这么做的话,会导致客户端用父类SocialMedia调用,但是实现类注入的可能是个Weibo的实现,调用groupVideoCall行为,产生意想不到的后果。


那有什么解决方案吗?


那就把功能拆开呗。


    public interface SocialMedia {   
public void chatWithFriend();
public void sendPhotosAndVideos()
}


public interface SocialPostAndMediaManager {
public void publishPost(Object post);
}



public interface VideoCallManager{
public void groupVideoCall(String... users);
}

现在,如果您观察到我们将特定功能隔离到单独的类以遵循LSP。


现在由实现类决定支持功能,根据他们所需的功能,他们可以使用各自的接口,例如 Weibo 不支持视频通话功能,因此 Weibo 实现可以设计成这样:


    public class Instagram implements SocialMedia,SocialPostAndMediaManager{
public void chatWithFriend(){
//logic
}
public void sendPhotosAndVideos(){
//logic
}
public void publishPost(Object post){
//logic
}
}

这样子就是符合里式替换原则LSP。


I—接口隔离原则



接口不应该强迫他们的客户依赖它不使用的方法。



大家可以看看自己的工程,是不是一个接口类中有很多很多的接口,每次调用API方法的时候IDE工具给你弹出一大堆,十分的"臃肿肥胖"。所以该原则的核心思想要将你的接口拆小,拆细,打破”胖接口“,不用强迫客户端实现他们不需要的接口。是不是和单一职责原则有点像?


例如,假设有一个名为 UPIPayment 的接口,如下所示


    public interface UPIPayments {

public void payMoney();

public void getScratchCard();

public void getCashBackAsCreditBalance();
}

现在让我们谈谈 UPIPayments 的一些实现,比如 Google PayAliPay


Google Pay 支持这些功能所以他可以直接实现这个 UPIPaymentsAliPay 不支持 getCashBackAsCreditBalance() 功能所以这里我们不应该强制客户端 AliPay 通过实现 UPIPayments 来覆盖这个方法。


我们需要根据客户需要分离接口,所以为了满足接口隔离原则,我们可以如下设计:



  • 创建一个单独的接口来处理现金返还。



public interface CashbackManager{
public void getCashBackAsCreditBalance();
}

现在我们可以从 UPIPayments 接口中删除getCashBackAsCreditBalanceAliPay也不需要实现getCashBackAsCreditBalance()这个它没有的方法了。


D—依赖倒置原则



高层模块不应该依赖低层模块,两者都应该依赖于抽象(接口)。抽象不应该依赖于细节(具体实现),细节应该取决于抽象。



这个原则我觉得也不是很好理解,所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。比如大家都知道的MVC模式,controller是调用service层接口这个抽象,而不是实现类。这也是我们经常说的要面向接口编程,而非细节或者具体实现,因为接口意味着契约,更加稳定。


我们通过一个例子加深一下理解。



  • 借记卡



public class DebitCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}


  • 信用卡



public class CreditCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}

现在用这两张卡你去购物中心购买了一些订单并决定使用信用卡支付


    public class ShoppingMall {
private DebitCard debitCard;
public ShoppingMall(DebitCard debitCard) {
this.debitCard = debitCard;
}
public void doPayment(Object order, int amount){
debitCard.doTransaction(amount);
}
public static void main(String[] args) {
DebitCard debitCard=new DebitCard();
ShoppingMall shoppingMall=new ShoppingMall(debitCard);
shoppingMall.doPayment("some order",5000);
}
}

上面的做法是一个错误的方式,因为 ShoppingMall 类与 DebitCard 紧密耦合。


现在你的借记卡余额不足,想使用信用卡,那么这是不可能的,因为 ShoppingMall 与借记卡紧密结合。


当然你也可以这样做,从构造函数中删除借记卡并注入信用卡。但这不是一个好的方式,它不符合依赖倒置原则。


那该如何正确设计呢?



  • 定义依赖的抽象接口BankCard



public interface BankCard {
public void doTransaction(int amount);
}


  • 现在 DebitCardCreditCard 都实现BankCard



public class CreditCard implements BankCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}


public class DebitCard implements BankCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}


  • 现在重新设计购物中心这个高级类,他也是去依赖这个抽象,而不是直接低级模块的实现类



public class ShoppingMall {
private BankCard bankCard;
public ShoppingMall(BankCard bankCard) {
this.bankCard = bankCard;
}
public void doPayment(Object order, int amount){
bankCard.doTransaction(amount);
}
public static void main(String[] args) {
BankCard bankCard=new CreditCard();
ShoppingMall shoppingMall1=new ShoppingMall(bankCard);
shoppingMall1.doPayment("do some order", 10000);
}
}

我们还可以拿 Tomcat这个 Servlet 容器作为例子来解释一下。


Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet规范。


总结


本文总结了软件编程中的黄金原则,KISS原则,DRY原则,SOLID原则。这些原则不仅仅适用于编程,也可以指导我们在架构设计上。虽然其中有些原则很抽象,但是大家多多实践和思考,会体会到这些原则的精妙。


作者:JAVA旭阳
来源:juejin.cn/post/7237037029570641979
收起阅读 »

🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

🎖️职业水平怎么样才算达到平均标准?我来告诉你 嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 毕业后进入社会,我像大家一样感到恐惧和不安。在这个新的阶段,我们都投入了大量时间和精力来从事各种社会劳动,同时也努力满足自己的经济需求。我们每个人在这个过程中都会去思考...
继续阅读 »

🎖️职业水平怎么样才算达到平均标准?我来告诉你


嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


毕业后进入社会,我像大家一样感到恐惧和不安。在这个新的阶段,我们都投入了大量时间和精力来从事各种社会劳动,同时也努力满足自己的经济需求。我们每个人在这个过程中都会去思考如何实现自己的人生价值,追求小时候美好的憧憬和期盼。💼


然而,在这个思考的过程中,没有人能为我提供确切答案。离开了学校的庇护和老师的指导,我感到比学校学习时更加困惑。未来的方向不太清晰,这使我在面对职业选择、个人发展和人生道路时遇到了许多挑战和困惑。🤔


有没有想过你职业生涯的下一步应该是什么呢?🤔


你怎么知道接下来要学习什么工具、原则或编程语言呢?📚


我想和大家分享一个超级简单的程序员分级定义思路,也许它可以帮助你这个处于职业生涯各个阶段的开发人员找到下一个目标并迈向更高的境界!✨


🚩声明:不一定正确,只是一组思路


以下的内容可能不一定正确,因为不同企业对员工能力的定义可能会有所不同。甚至每个人对这些级别的定义也会有很大的差异。🚫


然而,排除了内卷化的分级标准后,我接下来要介绍的每个阶段都代表了职业生涯中大多数人可能达到的“位置”。🎯


在每个等级之间,都存在一些过渡,可能需要在特定领域获得更多的知识和经验,也可能需要提升社交方面的知识和经验。🔀


每个等级都是在上一个等级的基础上进一步发展而设立的,我对此有着自己的职场经验启发。💡


然而,请注意:我所说的这些并不一定与你目前所处的职位相对应。 🚫


在某些公司,拥有“高级开发工程师”职称的人,实际上在技能和专业知识能力方面可能只是初级开发工程师!👨‍💻🏢


在职场中,许多人之所以被晋升,仅仅是因为他们在该领域(无论是前端、后端还是运维)有几年的经验,并非因为他们具备胜任所需的技能和知识。📚


同时,很多情况下,他们之所以成为公司中业务经验最丰富的“高级开发工程师”,仅仅是因为他们在同一家公司工作了很长时间,从而“熬掉”了许多老员工。⏳


这个世界并不公平,我相信大多数人都已经看到并经历了这种情况。🌍


当然,我还想补充一点,我所描述的这些等级并不是一成不变的标准。在你所在的领域中,有些地方对这些要求可能并不那么严格,所以你不需要过于关注我所提到的要求。🤔


以下内容仅供参考,希望能够帮助你更好地管理和掌握你未来的职业规划。说到底这仅仅是一种思路,我不是行业领袖,它仅仅是一组思路。🔍


1️⃣编程爱好者



“我有点不知道该怎么给这个阶段的 coder 定个级,算了,咱们姑且称他们为"编程爱好者"吧,但其实我还是觉得这个说法不太准确。😕”



我这里所指的“编程爱好者”是指广义上的 coder ,也就是那些会写代码或者热衷于写代码的人。💻


这些人可能有以下特征:



  1. 他们并非以“编程”为主业,而只是因为兴趣或者作为该专业的学生而加入到我们这个圈子中。对于那些以编程为职业的开发人员来说,他们算是“业余”的。🔍

  2. 这些开发爱好者了解编程语言的语法,并且能够熟练运用他们擅长的编程语言,甚至有时候比一些专业开发人员表现得更出色!📚

  3. 他们有能力独立开发一些小型项目,例如脚本、网页、游戏或应用程序。🚀

  4. 他们擅长使用搜索引擎自发解决问题。🔎

  5. 然而,在这个阶段,他们的编程能力并不能直接转化为经济利益,也就是说他们并不能通过技能获得收入。🚫


2️⃣初级开发工程师


"初级开发工程师"代表着那些已经以专业人士的身份进入IT领域的人,他们需要与其他专业人士合作,一起完成工作任务。👩‍💻


他们可能有以下特征:



  1. 他们是以编程为主要职业的专业人士,企业需要支付报酬雇佣他们加入生产。💼

  2. "初级开发工程师"会被分配到一个或多个项目中工作,但他们可能无法完全理解整个项目的结构,因为对于他们来说,项目可能还是“太大”了。🔨 在这个阶段,他们更多地承担一些被拆分成小模块的任务,对于项目的整体认识,他们并不清晰。🔎

  3. 他们可能只对自己专业领域有了解,在工作中需要继续学习前后端通信和数据库连接等跨系统的知识。📚

  4. 他们需要在中级开发工程师或高级开发工程师的指导下完成工作。🤝



“这些特征是一般情况下的描述,具体的职位要求和工作内容可能因公司和行业而异。📋💼”



3️⃣中级开发工程师


到了"中级开发工程师"阶段,他们已经适应了业内的整体开发节奏,成为了一名合格的开发团队成员和代码贡献者。🚀


在这个阶段,他们具备以下特征:



  1. 能够独立构建业务模块,并熟悉最佳实践。例如,在Web应用中开发单点登录模块。🏗️

  2. 开始了解项目的基本系统架构,对领域内的架构、性能和安全性有一定的了解。🏢

  3. 能够熟练使用专业工具来提高工作效率。🛠️

  4. 对设计模式和良好的编码习惯有基本的了解。🎨

  5. 能够在常规工作中独立操作,无需过多监督。💪

  6. 对于高级开发工程师来说,他们可能缺乏经验,需要经历几次完整的开发周期和遇到很多“坑”之后,才能学会如何在下次避免它们。🔍



“这个阶段的开发工程师最缺乏的就是项目实践经验。只要有不断地项目经历,通过实践和经验积累,他们就会不断成长。🌱”



4️⃣高级开发工程师


遗憾的是我们中大多数人在职业生涯中大部分时间都在面临从“中级开发工程师”到“高级开发工程师”的门槛。


有些“开发工程师”可能在整个职业生涯中一直停留在中级水平。


“高级开发工程师”之所以与众不同,是因为他们知道什么可以做,什么不可以做。这种洞察力是通过过去犯过的错误和经验教训获得的。


开发经验对于成为“高级开发工程师”至关重要。


根据我的理解,“高级开发工程师”应该具备以下特征:



  1. 精通团队所使用的核心技术,对其应用得非常熟练。💪

  2. 熟悉系统架构设计和设计模式,并能够在团队项目中应用这些概念,构建更复杂的系统。🏢

  3. 拥有构建“完整”解决方案的经验,能够考虑到项目的各个方面并提供全面的解决方案。🔍

  4. 在服务器部署和维护方面有一定的经验,了解负载平衡、连接池等跨领域知识。🖥️

  5. 作为团队的核心成员,能够担任导师的角色,积极指导中级和初级开发工程师。👥


其中最后一条是最最重要的。如果不能把你的经验、专业知识和知识传授给你的团队成员,我认为这就不是一个合格的“高级开发工程师”。


成为“高级开发工程师”的一个重要指标:一定是团队的其他成员经常向你寻求建议和帮助



“如果你还在沮丧为什么同事老是问我问题,也许现在可以改变一下想法了。💼


因为你是你们团队最重要的百科全书呢!也许现在是时候考虑向老板提出加薪的要求了呢?💰”



5️⃣开发领袖



这个阶段我也有点困惑,不知道要给他们这个等级取一个准确的称号。我想了两个名字:“高级架构师”和“团队领导者”,但是我又想,其实高级工程师也可以领导团队,也有架构能力啊。那就还是加“领袖”两个字,突出在技术领域的高级能力、团队领导能力和架构能力。这样看起来就更厉害了!👨‍💼



在这个阶段,程序员们已经不再仅仅为一个团队服务。他们可能同时为多个团队提供支持,并向下属团队提供更底层的指导,特别是在设计和早期产品开发阶段。💪


在国内,由于很难找到同时在业务领域和专业领域都深耕的人才,这类职位可能被企业分拆为不同的职能,更加注重管理能力而非专业能力。 🤔最终可能招聘了一个“高级监工”(毕竟,同时在业务领域和专业领域同时深耕的人真的少之又少,而且一般企业也不愿意花费与之对等的报酬)。


因此,大部分人可能会不同意我这个阶段的观点。 😕开发领袖的职能范围可能涵盖“敏捷教练(scrum master)”、“DevOps”、“项目经理(PM)”、“CTO”等管理职务。


因此,开发领袖最重要的特征是:



  1. 对业务领域有深刻的理解,能够消除开发团队与企业其他业务部门之间的沟通障碍。🌐

  2. 发挥"PM"职能: 协助规划产品开发和时间表,向营销或销售团队提供反馈。📈

  3. 发挥"CTO"职能: 协助高层管理,实现企业愿景,领导开发团队实现企业的业务目标。📊


因此,开发领袖必须对所处的业务领域(如医疗、金融、人力资源等)的产品有深入的了解。🏥 基于这些了解,他们能够理解软件所解决的业务问题,并且必须了解其他学科,如管理、产品开发、营销等,以消除各部门合作之间的沟通障碍。


简而言之,高级开发工程师和开发领袖的区别在于:



  1. 高级开发工程师也担任团队领导的角色,但主要面向开发团队的“内部”。👥

  2. 开发领袖则超越团队内部管理,他们的管理职能是面向“外部”的,致力于消除开发团队与公司其他部门之间的沟通障碍。🌍


因此,成为开发领袖需要具备高层领导的全局视野,并能够将业务术语和技术术语相互转化。🔑


如果你能够在公司内很好地与业务同事交流技术解决方案,并让其理解,那么你已经拥有了“开发领袖”其一的核心能力。💡


6️⃣领域专家


这个阶段的他们已经跳出了企业的限制,在一些特定领域也颇负盛名。他们的解决方案不再是只为一家企业服务,他们擅长的领域也不是一般的学科分类,而是一个非常有针对性地细分领域。🚀


可惜的是,一般的开发者们很难接触到这些领域,你想要了解他们的知识都不知道从哪儿下手,因为他们的知识分享大多是封闭的,只在内部共享,不对外传播。🔒



“可能你会觉得这与你对开源软件行业的理解不太一样,开源难道不是互联网发展的第一推动力吗?是啊,我同意你的观点,但你不了解不代表它不存在。其实大部分的技术分享都是在内部进行的,许多讲座和峰会也只限邀请制🔐。”



他们可能是某种编程语言的奠基人,可能是Web安全领域的重要任务驱动者,也可能是教导其他前端开发者如何使用React的大师,甚至还有那些在特定行业中扮演技术导师角色的人!👨‍💻


他们还可能是某个社区的建设者,在互联网和社会上有一群人将他们视为直接或间接的导师。🏢


他们也可能是支持特定事业或理念,并为之做出显著贡献的思想领袖。💡


他们会公开地讨论自己的专业领域和他们所推崇的理念。🗣️



“如果你也有自己的小圈子。比如在掘金社区;比如在GITHUB,拥有自己的互联网开源项目,并且有一大群粉丝用户支持和拥护你的产品和理念。那你也可以算是某一细分领域的专家了。👥”



总而言之,他们的一举一动都可能对互联网技术的发展产生重大影响。😄




🎉 你觉得怎么样?你认为自己处于哪个阶段?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


作者:道长王jj
来源:juejin.cn/post/7240838046789353530
收起阅读 »

消息推送的实现方式

短轮询(Long Polling) 网络资源:短轮询会产生大量的网络请求,尤其是当客户端轮询间隔很短时。这可能会导致大量的网络开销。 服务器处理:对于每个轮询请求,服务器需要处理该请求并发送响应,即使没有新的数据。这会导致服务器频繁地处理请求,可能增加CPU...
继续阅读 »

短轮询(Long Polling)



  1. 网络资源:短轮询会产生大量的网络请求,尤其是当客户端轮询间隔很短时。这可能会导致大量的网络开销。

  2. 服务器处理:对于每个轮询请求,服务器需要处理该请求并发送响应,即使没有新的数据。这会导致服务器频繁地处理请求,可能增加CPU和内存的使用。

  3. 总结:如果更新频率很低,但客户端仍然频繁地发送请求,短轮询可能会造成资源浪费,因为大多数响应可能只是告知“无新数据”


长轮询(Long Polling)



  • 客户端发送请求到服务器,服务器如果没有准备好的数据,就保持连接开放,直到有数据可以发送。一旦数据被发送,客户端处理数据后再次发送新的请求,如此循环。

  • 长轮询通常用于实时或近实时的通知和更新,比如活动通知。



  1. 网络资源:相比短轮询,长轮询减少了无效的网络请求。服务器只在有新数据时才发送响应,从而减少了网络流量。

  2. 服务器处理:长轮询可能导致服务器需要维护更多的打开连接,因为它会为每个客户端请求保持一个打开的连接,直到有新数据或超时。这可能会增加服务器的内存使用,并可能达到服务器的并发连接限制。

  3. 总结:长轮询在某些场景下可以提供更高效的资源使用,尤其是当数据更新不频繁但需要快速传递给客户端时。但如果有大量的客户端同时进行长轮询,服务器可能需要处理大量的并发打开连接。


WebSocket:



  • WebSocket提供了一个全双工通信通道,使得服务器和客户端可以在任何时刻发送数据给对方。这是一个非常实时且高效的解决方案。适合实时聊天



  1. 网络资源:WebSocket 在建立连接后只需要一个握手过程,之后数据可以在此连接上双向传输,不需要为每条消息进行新的请求和响应。这极大地减少了网络开销。

  2. 服务器处理:一旦 WebSocket 连接被建立,它将保持打开状态,直到客户端或服务器决定关闭它。这意味着服务器必须维护所有活动的 WebSocket 连接,这可能会消耗内存和其他资源。

  3. 总结:WebSocket 在数据频繁更新并且需要实时传递给客户端的场景中非常有效。尽管需要维护持久连接,但由于减少了网络开销,通常更为高效。


服务器发送事件(Server-Sent Events, SSE) :



  • 服务器发送事件是一种使服务器能够发送新数据到客户端的简单方法。它比WebSocket简单,但只允许服务器向客户端发送数据。活动通知和提醒



  1. 网络资源:与 WebSocket 类似,SSE 也只需要一次握手来建立持久连接。一旦连接建立,服务器可以持续地向客户端推送消息。

  2. 服务器处理:SSE 需要维护持久连接以发送数据,但与 WebSocket 相比,SSE 只是单向的。这意味着服务器不需要处理从客户端发来的消息。

  3. 总结:SSE 是一种高效的技术,适用于只需要服务器向客户端推送数据的场景,例如实时消息通知。


HTTP/2 Server Push:




  • HTTP/2协议支持服务器推送,允许服务器在客户端需要之前预先发送数据。这可以减少延迟,但通常只用于发送关联的资源,如CSS或JavaScript文件,而不是用于通用的消息推送。




  • 主要用于提前发送关联资源如CSS、JavaScript文件,以减少加载时间,提高网页性能。




  • 可以减少网页加载时间,提高用户体验。




  • 不适用于通用的消息推送,且需要HTTP/2协议支持,实现可能需要特定的服务器配置。




MQTT协议


MQTT 全称(Message Queue Telemetry Transport):一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。


该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的MQ有点类似。


TCP协议位于传输层,MQTT 协议位于应用层,MQTT 协议构建于TCP/IP协议上,也就是说只要支持TCP/IP协议栈的地方,都可以使用MQTT协议。


为什么要用 MQTT协议?


MQTT协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP协议呢?



  • 首先HTTP协议它是一种同步协议,客户端请求后需要等待服务器的响应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合IOT应用程序。

  • HTTP是单向的,如果要获取消息客户端必须发起连接,而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。

  • 通常需要将一条命令或者消息,发送到网络上的所有设备上。HTTP要实现这样的功能不但很困难,而且成本极高。


第三方推送服务:



  • 使用如Firebase Cloud Messaging (FCM), Apple Push Notification Service (APNs)等第三方推送服务来处理消息推送。


对比


WebSocket和Server-Sent Events提供了较低的延迟和较高的实时性,但可能需要更多的服务器资源。长轮询可能会有更高的延迟,并且可能不是最高效的解决方案。HTTP/2 Server Push和第三方推送服务可能更适合于不需要高度实时性的应用。消息队列和发布/订阅模型提供了一种解耦服务器和客户端的方式,但可能会增加系统的复杂性。


在选择实现方法时,需要考虑应用的具体需求,例如实时性的要求、服务器资源、网络条件以及开发和维护的复杂性。同时,也可以考虑将几种方法结合使用,以满足不同的需求。



  • 如果有大量的客户端并且数据更新不频繁,长轮询可能比短轮询更为有效,因为它减少了无效的网络请求。

  • 如果服务器有并发连接的限制或资源有限,大量的长轮询请求可能会耗尽资源,导致服务器不稳定。

  • 如果数据更新非常频繁,短轮询可能会比较合适,因为它可以更简单地处理频繁的请求。

  • WebSocket 通常在需要实时通信的应用中更为有效和资源高效。它减少了网络开销,并提供了持续的、低延迟的双向通信。

  • 短轮询长轮询 可能更适合不需要持续连接的场景或当 WebSocket 不可用或不适用时的备选方案。

  • WebSocket:提供双向通信,适用于需要实时双向交互的应用,如在线聊天。由于它是全双工的,可能需要更多的资源来处理双向的消息传输。

  • SSE:提供单向通信,适用于只需要服务器推送数据的应用,如股票行情更新。通常,SSE 比 WebSocket 更轻量,因为它只处理单向通信。

  • 短轮询:可能会产生大量网络开销,特别是在数据更新频繁的场景中。

  • 长轮询:减少了网络开销,但可能需要服务器维护大量的打开连接,直到有新数据或超时。


从资源消耗的角度看:



  • WebSocketSSE 都需要维护持久连接,但通常比短轮询和长轮询更高效,因为它们减少了网络开销。

  • SSE 可能比 WebSocket 更轻量,因为它是单向的。

  • 短轮询 可能是最耗资源的,尤其是在频繁请求且数据更新不频繁的场景中。

  • 长轮询 在某些情况下可能比短轮询更高效,但仍然不如 WebSocket 或 SSE。


作者:Pomelo_刘金
来源:juejin.cn/post/7291464815658172471
收起阅读 »

我本可以忍受黑暗,如果我未曾见过光明

【随想录】我本可以忍受黑暗,如果我未曾见过光明 随想录 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 老文章? 这篇文章大体结构早已在我语...
继续阅读 »

【随想录】我本可以忍受黑暗,如果我未曾见过光明



随想录


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



老文章?


这篇文章大体结构早已在我语雀里写完了很久很久~~~


假期就有构思了,现在埋坑


因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学一起上课,而且下一届还是我带的班级,想想那种感觉“咦,武哥你怎么在这上课”而我,内心qs:杀了我把,太羞辱了,脚指头已经扣除一套四合院了)


朋友问我成绩,当时孩子都傻了


所以这段时间我正在经历自我内耗,就向是欠了谁东西,到了deadline,到了审判的日子才能释怀!也至于最近心理一直在想着这个事情,导致最近焦虑的一批,最近几天自己都不正常了,但是终于结束了~~~(非常感谢老师)



言归正传


好了好了,又跑题了,书归正题,你可能会疑惑我为什么用这个标题,难道我经历了什么涩会黑暗,被潜规则,被PUA......(给你个大逼斗子,停止瞎想,继续向下看)



这篇文章灵感来源于我很喜欢的B站一位高中语文老师讲解《琵琶行》,突然我被这个短短 3分51秒的视频搞得愣住了,直接神游五行外,大脑开始快速的回顾自己最近的生活~~~(再次表白真的很爱这摸温柔的语文老师,他的课真的让我感觉到什么叫“腹有诗书气自华”)



视频链接:https://www.bilibili.com/video/BV1bW4y1j7Un/

最爱的语文老师


其实人生当中很残忍的一个事儿是什么呢?就是你一直以为未来有无限可能的时候,就像琵琶女觉得她能够过上那样的生活一直下去。一直被“五陵年少争缠头”,一直被簇拥着的时候,突然有一天你意识到好像这辈子就只能这样,就只能去来江头守空船,守着这一这艘空船,默默的度过慢慢的长夜。
就是如果如果你不曾体验过那样的生活,你会觉得好像“我”最终嫁给了一个商人,然后至少衣食不愁,至少也能活得下去,好像也还算幸福。但是如果我曾经经历过那样的生活,我此刻内心多多少少是有些不甘的。


很喜欢的一幅油画


亦或者是像白居易,如果他是从平民起身,然后一直一步一步做到了江州司马可能觉得也还是不错,但是你要知道他在起点就是在京城为官,所以这里其实是有很明显的,一种落差。那也同样,如果此刻你回到我们说所有的文学都是在读自己,你想想看你自己,此刻你可能没有这种感觉。


30公里鲜啤



哈哈哈,兄弟们不要emo啊,让我们珍惜当下,还是那句话,我们还年轻,谁都不怕。(但是遇到刀枪棍棒还是躲一躲呀,毕竟还是血肉之躯)



其实反思反思人生中最大的挑战,就是接受自己生来平凡。自己没有出色的外表,我也没有过人的才华,我可能也少了些许少年时的锐意。但是这个emo点我并不care,因为我还在拥有选择的阶段,我也在尝试探索不一样的人生,这也许就是喜欢记录生活和写下灵机一动时候想法的意义。但是也就向UP主@peach味的桃子记录自己第44次开学,也是最后一次开学表达自己点点滴滴,也同样是不同的感受;我们同样有应届生的迷茫,但是想想也没什么可怕,还在学习,还在向目标奔跑,也还在享受校园生活~~~


打卡老馆子-群乐饭店


啊呀,好像又唠跑偏了,就是说我对这个视频那么的不一样,尤其是这个主题,因为自己的寒假的实习给我带来了新的视野,哦不,应该是旷野,很有幸能去华为在我们省份的办事处,又被出差派往华为在一个某市分部工作了半个月。这短短的实习经历,让我在大三这个迷茫的时期多了份坚定,在这个期间和大佬们一起工作,真的看到了人家的企业文化和那种行动力,最主要被军团的大佬们很牛掰技术折服,在相处这段时间真的知道了什么是向往的生活,这个学历门槛迈过去,你将会迎来什么样的明天~~~


(谁说我去卖手机去了,我揍他啊[凶狠])


游客打卡照


所以我可能对之前年终总结看法有了些改变,我之前年终总结写到,薪资又不会增加多少,浪费三年那不纯属XX嘛,没错,今天我被打脸了,为我之前的幼稚想法感到可笑;写到这里脑子已经开始疼了,最近甲流,朋友圈注意身体,这个东西真的会影响我们的战斗力,好吧,这也只是一个随想录,留点内容给年中总结,要不到时候就词穷了,哈哈~~


很nice的江景房


近期反思


其实每个人的出发点不一样不能一概而论,就向我自己出发,一个来自十八线农村的孩子,父母通过自己一代人的努力从农村到乡镇,而我就通过自己的求学之路一直到,貌似能够在这个省份的省会立足,这也就是我能做的进步,不管怎么说,我们都是从自身出发,其实谈到这个问题,我自身也很矛盾,小城市就真的不好吗,人的一生除了衣食无忧,在向下追求的不就是快乐,如果真的能和一个爱的人,在做一些自己喜欢做的事情,难道不就是“人生赢家”,城市在这种维度下考虑貌似也不重要~~(如果你想diss这种想法,没有考虑子女的教育问题,其实我想到了,但是我目前的年龄和所处的位置吧,感觉很片面,所以就不对这个点展开讨论了)


过度劳累,小酌一杯


回复问题


有人怕别人看到自己以往的文章写的很幼稚,就不想写了,我有不同的看法,只有看到曾经的对事情的看法和处理方式幼稚了,才能证明自己的成长呀,谁能一下子从孩子成为一个大人!(但是某些时候谁还是不是一个孩子[挑眉])



作者:武师叔
来源:juejin.cn/post/7208476031136792631
收起阅读 »

"我的领导只任用关系户,我的团队里全是嫡系。"

说这句话的同学,跟我一起来认真的思考一下,你认为的那个关系户跟领导最初也是陌生人。既然关系户能跟领导搞好关系,你是不是也能跟领导搞好关系?现在给你个任务:跟领导搞好关系,请问你准备怎么做呢? 拍马屁吗?跟领导睡觉吗?你去试一试,看看管不管用。我相信你自己也知道...
继续阅读 »

说这句话的同学,跟我一起来认真的思考一下,你认为的那个关系户跟领导最初也是陌生人。既然关系户能跟领导搞好关系,你是不是也能跟领导搞好关系?现在给你个任务:跟领导搞好关系,请问你准备怎么做呢?


拍马屁吗?跟领导睡觉吗?你去试一试,看看管不管用。我相信你自己也知道,你每天吹捧领导"您真英明",领导只会觉得你是个傻逼。


一个部门有两个同事小A和小B,小A是一个身材曼妙的美女,谁见了都把持不住。但是业务能力一般,一个月只能给公司赚5万块;小B是书呆子理工男,业务能力比较好,一个月能给公司赚10万。


假设你是一个极端好色的男领导。


现在有两种情况,第1种情况,季度销售目标8个亿,两个月过去了,你只实现1个亿,再这样下去,你非得被开掉不可。这样你会把资源偏向哪个同事?


第2种情况,季度目标1个亿,你已经实现了8个亿,你会偏向哪个同事?


你跟漂亮女同事睡了觉,账上的钱会自动多出来吗?7个亿的销售额压力估计压得你都会不举吧?就算领导再好色,只要他心智还正常,肯定也会先偏向小B,把自己的位置稳固住,然后再用自己的位置去泡妞。


在巨大的压力下,人们会克制自己任性的偏好。反过来说,在巨大的压力下,人们做出的抉择,往往不是任性的偏好。


再来讲讲逻辑学


相关性不等于因果性, A事件和b事件总是同时发生,有可能是a事件导致了b事件,也有可能是b事件导致了a事件,也有可能是a和b由共同的一个看不见的c事件导致。


你的领导跟小a关系很好,小a晋升了,所以你得出一个结论。你的领导偏爱关系户。


有没有可能是另外一种情况?小a能力很强,表现很好,所以他晋升了。同时因为他表现好所以领导跟他关系很好?


请用严谨的逻辑来做一个排他性证明,证明我说的这种情况是错的。


还要说说利益


职场是赤裸裸的利益场,除了利益以外一无所有。


在职场里a跟b关系好,只有一种可能,就是b能满足a的利益;如果两个人互相关系好,那就是他们两个是利益共同体。


你去拍领导的马屁,领导完全不会屌你。如果你去分析领导正在焦虑的事情,跟他一起出谋划策,帮他解决,他肯定立刻跟你关系就上升了。不仅如此,如果他下次有了新的焦虑的事情,一筹莫展,做不了决定,他也会第一时间来问问你的意见。在这种时候,领导对你的需要比你对领导的需要更迫切,他就算不喜欢你,他也得硬着头皮跟你点头哈腰。


旁人看了就会觉得这个人是领导的嫡系,他俩关系真好。这种不合逻辑的论调,只不过是自己把握核心利益走向能力不足的无能狂怒,是吃不着葡萄说葡萄酸的低级防御机制。


这么多年来我在每个团队都很受领导器重,但我从来不点头哈腰,我从来都是该吵架就吵架,一点儿都不客气。因为我准确的知道他的利益点,也就可以准确的评估领导对我的需要程度,他不爽也得忍着。(他不需要我的时候我就乖乖的,大丈夫能屈能伸)。不懂门道的外人看来,我就是关系户。


"关系户"这个词特别好,道德上是不正当的,同时也不是谁能都能做到,所以它是一个非常完美的心理防御的盾牌。信奉"关系户"的人,往往既不会搞关系,也不会搞工作。认真搞一次关系就知道,搞关系比搞工作难多了。


要是你真的觉得你的团队任用关系户,那也请你认真的去搞关系。不过我猜到时候你会抱怨公司不好好"搞关系"净"搞工作"了,那些上位的人全都是"工作狂"。


拍马屁这种门槛这么低的事情,要是真的管用的话,竞争的激烈程度会超乎你的想象,你可能连领导的脚后跟都见不到。


职场是赤裸裸的利益场,除了利益以外一无所有。反过来说,那个你认为的"关系户",跟你的领导之间也是赤裸裸的利益关系。


你买东西的时候是希望卖东西的人越多越好还是越少越好?你卖东西的时候是希望买东西的人越多越好还是越少越好?所以你的领导是希望自己的团队里,牛逼的人越多越好还是越少越好?


所以哪个领导会任由自己团队的某个成员一家独大?


"我能力强,我的领导却不任用我"


人这种生物天生就有高看自己一眼的趋势的,人很难公平的对待自己和别人。所以当一个人跟我说他牛逼的时候,我心里第一时间的反应就是"你是真牛逼还是吹牛逼?"。


我平常在日常需求任务分配的时候,经常会遇到一个现象,一个PM提需求的时候指名道姓希望某研发同学来开发。此时另外一个需求的PM也立刻跳起来说他的需求也希望这个研发同学来开发。两个PM原地开始互相PK起了需求价值,我打圆场说团队还有别的空余人力,两个PM实在不好意思继续掰扯了才作罢。


两个PM不仅在人力的维度展开竞争,而且在某个具体的研发同学身上展开了竞争。



职场是赤裸裸的利益场,你的工作表现直接影响着别人的利益;反过来别人的表现也影响着你的利益。研发开发需求快质量高,可以直接的换算成需求在绩效周期内能给客户稳定使用的可能性,这个又可以直接换算成绩效,绩效又可以直接换算成年终奖。所以你的工作能力会被你的同事赤裸裸换算成真金白银,没有人会跟白花花的银子过不去,所以你真的优秀的话,别人是可以敏锐的感觉到的。


在高压的环境下,真实的你自己活在别人的眼睛里。


"我们团队确实是有嫡系的呀!"


团队里的成员加入团队的顺序有先有后,小A小B小C在团队一开始最艰难的时候跟领导一起搭建起了这条业务线。所以他们了解这个团队发展过程中的诸多细节,甚至可以说这个团队的文化是他们共同创造的。


你上半年刚加入团队,总共也没做几个需求。你要是领导,你更信任谁?有一个容易出成绩又很有挑战的项目,你更愿意交给谁?


你比别人晚来,不代表你能力比别人差,但你是不是一定就能力比别人好?还不确定。而前几个人的能力好坏领导经过多年磨合已经一清二楚了,对他来说,这个项目交给 ta 确定性就更大一些,风险更小一些。


容易出成绩的工作总是很稀缺的,所以这些工作一定要集中给更有希望晋升的人,领导的心态就是扶上去一个算一个。如果这样的工作分摊给好几个人,那好几个人都没有足够的业绩晋升,最后大家都不满意。


所有人都不满意和至少有一个人满意,是你的话你选哪一种?


这个内容甚至是写在教人当领导的书里,作为标准教程的。


再说说另外一种情况


公司要成立一个新业务线,需要迅速搭建一个20人的团队。老板认识一个别的公司的领导,那个领导表示要带着自己目前的团队一起来这个公司。老板听了大喜过望,恨不能跪在地上给对方磕俩响头。要知道想要搭建一个20人的团队,在招聘上要花费多少人力资源成本。而且招聘一个20人团队,至少需要半年的时间。人找齐了还不能立刻投入工作,彼此还得磨合。


现在好了,有人带着一个团队直接过来。成本的问题时间的问题磨合的问题一口气全部解决。


对方带了15个人过来,离20个人还差5个。于是小G被调配进入了这个团队。这个时候虽然名义上是对方几个新人加入了小G的公司,但实际上真实发生的事情是小G加入了对方的团队。所以贡献要沿着团队的历史记忆延续和排列。


小G可能会感觉到巨大的不公平,但是你尝试从老板的视角看,小G的这种"不公平"的感受,能值得起组建一个20人团队的费用吗?你的"公平"值几十万吗?


给大家几点建议。


第一,对于打工人来说,持续在同一个公司同一个团队长期工作才是最大化自己利益的最好方式,频繁跳槽的人是非常短视的。如果你加入了一个新团队,要保持耐心,耐心是一个良好的品格。


第二,机遇真的很重要,我相信这句话你已经听腻了。但是之前你听这句话只有一个宏观而含混的理解,我相信你读完这篇文章就对这句话有一个形象而具体的理解。


第三,这个世界不是公平的,如果你觉得世界是公平的,那只能说明你自己幼稚。你假设了一个"公平"的世界,但却发现真实的世界不是公平的,于是内心世界陷入了巨大的冲突,每日活在精神内耗里。请问谁许诺过你这个世界是公平的了吗?许诺给你的那个人有足够的权威保证世界是公平的吗?


要心平气和的接受世界不是公平的,然后在"世界不不公平"的基础上组织自己的想法和行动,以达到自己利益的最大化。这样就能避免很多无意义的内耗。


上面这段话好像我在PUA你,但是别忘了我只是在脉脉匿名圈顶着一个马甲的陌生人,我PUA你对我没有任何好处。然而我发自肺腑地说,我自己就是这么想的,从我是一个小兵开始就是这个想的,也是这么做的,我用这套想法获得了今天的一切,我善意的把这些想法分享给你,希望避免你的困扰。你爱信不信。


最后的最后想说一句,如果有一天你掌握了权力,我希望你能公平的对待他人,咱们就别再为这个世界的操蛋添砖加瓦了,这是一种修养。


作者:马可奥勒留
来源:juejin.cn/post/7288178532861378600
收起阅读 »

SQL为什么动不动就N百行以K计?

发明SQL的初衷之一显然是为了降低人们实施数据查询计算的难度。SQL中用了不少类英语的词汇和语法,这是希望非技术人员也能掌握。确实,简单的SQL可以当作英语阅读,即使没有程序设计经验的人也能运用。 然而,面对稍稍复杂的查询计算需求,SQL就会显得力不从心,经常...
继续阅读 »

发明SQL的初衷之一显然是为了降低人们实施数据查询计算的难度。SQL中用了不少类英语的词汇和语法,这是希望非技术人员也能掌握。确实,简单的SQL可以当作英语阅读,即使没有程序设计经验的人也能运用。


然而,面对稍稍复杂的查询计算需求,SQL就会显得力不从心,经常写出几百行有多层嵌套的语句。这种SQL,不要说非技术人员难以完成,即使对于专业程序员也不是件容易的事,常常成为很多软件企业应聘考试的重头戏。三行五行的SQL仅存在教科书和培训班,现实中用于报表查询的SQL通常是以“K”计的。


SQL困难的分析探讨


这是为什么呢?我们通过一个很简单的例子来考察SQL在计算方面的缺点。


设有一个由三个字段构成的销售业绩表(为了简化问题,省去日期信息):


sales_amount销售业绩表
sales销售员姓名,假定无重名
product销售的产品
amount该销售员在该产品上的销售额

现在我们想知道出空调和电视销售额都在前10名的销售员名单。


这个问题并不难,人们会很自然地设计出如下计算过程:


1. 按空调销售额排序,找出前10名;


2. 按电视销售额排序,找出前10名;


3. 对1、2的结果取交集,得到答案;


我们现在来用SQL做。


1. 找出空调销售额前10名,还算简单:


select top 10 sales from sales_amount where product='AC' order by amount desc

2. 找出电视销售额前10名。动作一样:


select top 10 sales from sales_amount where product='TV' order by amount desc

3. 求1、2的交集。这有点麻烦,SQL不支持步骤化,上两步的计算结果无法保存,只能再重抄一遍了:


select * from
( select top 10 sales from sales_amount where product='AC' order by amount desc )
intersect
( select top 10 sales from sales_amount where product='TV' order by amount desc )

一个只三步的简单计算用SQL要写成这样,而日常计算中多达十几步的比比皆是,这显然超出来许多人的可接受能力。


我们知道了SQL的第一个重要缺点:不支持步骤化。把复杂的计算分步可以在很大程度地降低问题的难度,反过来,把多步计算汇成一步则很大程度地提高了问题的难度。


可以想象,如果老师要求小学生做应用题时只能列一个算式完成,小朋友们会多么苦恼(当然,不乏一些聪明孩子搞得定)。


SQL查询不能分步,但用SQL写出的存储过程可以分步,那么用存储过程是否可以方便地解决这个问题呢?


暂先不管使用存储过程的技术环境有多麻烦和数据库的差异性造成的不兼容,我们只从理论上来看用分步SQL是否能让这个计算更简单捷些。


1. 计算空调销售额前10名。语句还是那样,但我们需要把结果存起来供第3步用,而SQL中只能用表存储集合数据,这样我们要建一个临时表:


create temporary table x1 as
select top 10 sales from sales_amount where product='AC' order by amount desc

2. 计算电视销售额前10名。类似地


create temporary table x2 as
select top 10 sales from sales_amount where product='TV' order by amount desc

3. 求交集,前面麻烦了,这步就简单些


select * from x1 intersect x2

分步后思路变清晰了,但临时表的使用仍然繁琐。在批量结构化数据计算中,作为中间结果的临时集合是相当普遍的,如果都建立临时表来存储,运算效率低,代码也不直观。


而且,SQL不允许某个字段取值是集合(即临时表),这样,有些计算即使容忍了繁琐也做不到。


如果我们把问题改为计算所有产品销售额都在前10名的销售员,试想一下应当如何计算,延用上述的思路很容易想到:


1. 将数据按产品分组,将每组排序,取出前10名;


2. 将所有的前10名取交集;


由于我们事先不知道会有多个产品,这样需要把分组结果也存储在一个临时表中,而这个表有个字段要存储对应的分组成员,这是SQL不支持的,办法就行不通了。


如果有窗口函数的支持,可以转换思路,按产品分组后,计算每个销售员在所有分组的前10名中出现的次数,若与产品总数相同,则表示该销售员在所有产品销售额中均在前10名内。


select sales
from ( select sales,
from ( select sales,
rank() over (partition by product order by amount desc ) ranking
from sales_amount)
where ranking <=10 )
group by sales
having count(*)=(select count(distinct product) from sales_amount)

这样的SQL,有多少人会写呢?


况且,窗口函数在有些数据库中还不支持。那么,就只能用存储过程写循环依次计算每个产品的前10名,与上一次结果做交集。这个过程比用高级语言编写程序并不简单多少,而且仍然要面对临时表的繁琐。


现在,我们知道了SQL的第二个重要缺点:集合化不彻底。虽然SQL有集合概念,但并未把集合作为一种基础数据类型提供,这使得大量集合运算在思维和书写时都需要绕路。


我们在上面的计算中使用了关键字top,事实上关系代数理论中没有这个东西(它可以被别的计算组合出来),这不是SQL的标准写法。


我们来看一下没有top时找前10名会有多困难?


大体思路是这样:找出比自己大的成员个数作为是名次,然后取出名次不超过10的成员,写出的SQL如下:


select sales
from ( select A.sales sales, A.product product,
(select count(*)+1 from sales_amount
where A.product=product AND A.amount<=amount) ranking
from sales_amount A )
where product='AC' AND ranking<=10


select sales
from ( select A.sales sales, A.product product, count(*)+1 ranking
from sales_amount A, sales_amount B
where A.sales=B.sales and A.product=B.product AND A.amount<=B.amount
group by A.sales,A.product )
where product='AC' AND ranking<=10

这样的SQL语句,专业程序员写出来也未必容易吧!而仅仅是计算了一个前10名。


退一步讲,即使有top,那也只是使取出前一部分轻松了。如果我们把问题改成取第6至10名,或者找比下一名销售额超过10%的销售员,困难仍然存在。


造成这个现象的原因就是SQL的第三个重要缺点:缺乏有序支持。SQL继承了数学上的无序集合,这直接导致与次序有关的计算相当困难,而可想而知,与次序有关的计算会有多么普遍(诸如比上月、比去年同期、前20%、排名等)。


SQL2003标准中增加的窗口函数提供了一些与次序有关的计算能力,这使得上述某些问题可以有较简单的解法,在一定程度上缓解SQL的这个问题。但窗口函数的使用经常伴随着子查询,而不能让用户直接使用次序访问集合成员,还是会有许多有序运算难以解决。


我们现在想关注一下上面计算出来的“好”销售员的性别比例,即男女各有多少。一般情况下,销售员的性别信息会记在花名册上而不是业绩表上,简化如下:


employee员工表
name员工姓名,假定无重名
gender员工性别

我们已经计算出“好”销售员的名单,比较自然的想法,是用名单到花名册时找出其性别,再计一下数。但在SQL中要跨表获得信息需要用表间连接,这样,接着最初的结果,SQL就会写成:


select employee.gender,count(*)
from employee,
( ( select top 10 sales from sales_amount where product='AC' order by amount desc )
intersect
( select top 10 sales from sales_amount where product='TV' order by amount desc ) ) A
where A.sales=employee.name
group by employee.gender

仅仅多了一个关联表就会导致如此繁琐,而现实中信息跨表存储的情况相当多,且经常有多层。比如销售员有所在部门,部门有经理,现在我们想知道“好”销售员归哪些经理管,那就要有三个表连接了,想把这个计算中的where和group写清楚实在不是个轻松的活儿了。


这就是我们要说的SQL的第四个重要困难:缺乏对象引用机制,关系代数中对象之间的关系完全靠相同的外键值来维持,这不仅在寻找时效率很低,而且无法将外键指向的记录成员直接当作本记录的属性对待,试想,上面的句子可否被写成这样:


select sales.gender,count(*)
from (…) // …是前面计算“好”销售员的SQL
group by sales.gender

显然,这个句子不仅更清晰,同时计算效率也会更高(没有连接计算)。


我们通过一个简单的例子分析了SQL的四个重要困难,这也是SQL难写或要写得很长的主要原因。基于一种计算体系解决业务问题的过程,也就是将业务问题的解法翻译成形式化计算语法的过程(类似小学生解应用题,将题目翻译成形式化的四则运算)。SQL的上述困难会造成问题解法翻译的极大障碍,极端情况就会发生这样一种怪现象:将问题解法形式化成计算语法的难度要远远大于解决问题本身


再打个程序员易于理解的比方,用SQL做数据计算,类似于用汇编语言完成四则运算。我们很容易写出3+5*7这样的算式,但如果用汇编语言(以X86为例),就要写成


    mov ax,3
mov bx,5
mul bx,7
add ax,bx

这样的代码无论书写还是阅读都远不如3+5*7了(要是碰到小数就更要命了)。虽然对于熟练的程序员也算不了太大的麻烦,但对于大多数人而言,这种写法还是过于晦涩难懂了,从这个意义上讲,FORTRAN确实是个伟大的发明。


为了理解方便,我们举的例子还是非常简单的任务。现实中的任务要远远比这些例子复杂,过程中会面临诸多大大小小的困难。这个问题多写几行,那个问题多写几行,一个稍复杂的任务写出几百行多层嵌套的SQL也就不奇怪了。而且这个几百行常常是一个语句,由于工程上的原因,SQL又很难调试,这又进一步加剧了复杂查询分析的难度。


更多例子


我们再举几个例子来分别说明这几个方面的问题。


为了让例子中的SQL尽量简捷,这里大量使用了窗口函数,故而采用了对窗口函数支持较好的ORACLE数据库语法,采用其它数据库的语法编写这些SQL一般将会更复杂。
这些问题本身应该也算不上很复杂,都是在日常数据分析中经常会出现的,但已经很难为SQL了。


计算不分步


把复杂的计算分步可以在很大程度地降低问题的难度,反过来,把多步计算汇成一步完成则会提高问题的复杂度。



任务1 销售部的人数,其中北京籍人数,再其中女员工人数?



销售部的人数


select count(*) from employee where department='sales'

其中北京籍的人数


select count(*) from employee where department='sales' and native_place='Beijing'

再其中的女员工人数


select count (*) from employee
where department='sales' and native_place='Beijing' and gender='female'

常规想法:选出销售部人员计数,再在其中找出其中北京籍人员计数,然后再递进地找出女员工计数。每次查询都基于上次已有的结果,不仅书写简单而且效率更高。


但是,SQL的计算不分步,回答下一个问题时无法引用前面的成果,只能把相应的查询条件再抄一遍。



任务2 每个部门挑选一对男女员工组成游戏小组



with A as
(select name, department,
row_number() over (partition by department order by 1) seq
from employee where gender=‘male’)
B as
(select name, department,
row_number() over(partition by department order by 1) seq
from employee where gender=‘female’)
select name, department from A
where department in ( select distinct department from B ) and seq=1
union all
select name, department from B
where department in (select distinct department from A ) and seq=1

计算不分步有时不仅造成书写麻烦和计算低效,甚至可能导致思路严重变形。


这个任务的直观想法:针对每个部门循环,如果该部门有男女员工则各取一名添进结果集中。但SQL不支持这种逐步完成结果集的写法(要用存储过程才能实现此方案),这时必须转变思路为:从每个部门中选出男员工,从每个部门选出女员工,对两个结果集分别选出部门出现在另一个结果集的成员,最后再做并集。


好在还有with子句和窗口函数,否则这个SQL语句简直无法看了。


集合无序


有序计算在批量数据计算中非常普遍(取前3名/第3名、比上期等),但SQL延用了数学上的无序集合概念,有序计算无法直接进行,只能调整思路变换方法。



任务3 公司中年龄居中的员工



select name, birthday
from (select name, birthday, row_number() over (order by birthday) ranking
from employee )
where ranking=(select floor((count(*)+1)/2) from employee)

中位数是个常见的计算,本来只要很简单地在排序后的集合中取出位置居中的成员。但SQL的无序集合机制不提供直接用位置访问成员的机制,必须人为造出一个序号字段,再用条件查询方法将其选出,导致必须采用子查询才能完成。



任务4 某支股票最长连续涨了多少交易日



select max (consecutive_day)
from (select count(*) (consecutive_day
from (select sum(rise_mark) over(order by trade_date) days_no_gain
from (select trade_date,
case when
closing_price>lag(closing_price) over(order by trade_date)
then 0 else 1 END rise_mark
from stock_price) )
group by days_no_gain)

无序的集合也会导致思路变形。


常规的计算连涨日数思路:设定一初始为0的临时变量记录连涨日期,然后和上一日比较,如果未涨则将其清0,涨了再加1,循环结束看该值出现的最大值。


使用SQL时无法描述此过程,需要转换思路,计算从初始日期到当日的累计不涨日数,不涨日数相同者即是连续上涨的交易日,针对其分组即可拆出连续上涨的区间,再求其最大计数。这句SQL读懂已经不易,写出来则更困难了。


集合化不彻底


毫无疑问,集合是批量数据计算的基础。SQL虽然有集合概念,但只限于描述简单的结果集,没有将集合作为一种基本的数据类型以扩大其应用范围。



任务5 公司中与其他人生日相同的员工



select * from employee
where to_char (birthday, ‘MMDD’) in
( select to_char(birthday, 'MMDD') from employee
group by to_char(birthday, 'MMDD')
having count(*)>1 )

分组的本意是将源集合分拆成的多个子集合,其返回值也应当是这些子集。但SQL无法表示这种“由集合构成的集合”,因而强迫进行下一步针对这些子集的汇总计算而形成常规的结果集。


但有时我们想得到的并非针对子集的汇总值而是子集本身。这时就必须从源集合中使用分组得到的条件再次查询,子查询又不可避免地出现。



任务6 找出各科成绩都在前10名的学生



select name
from (select name
from (select name,
rank() over(partition by subject order by score DESC) ranking
from score_table)
where ranking<=10)
group by name
having count(*)=(select count(distinct subject) from score_table)

用集合化的思路,针对科目分组后的子集进行排序和过滤选出各个科目的前10名,然后再将这些子集做交集即可完成任务。但SQL无法表达“集合的集合”,也没有针对不定数量集合的交运算,这时需要改变思路,利用窗口函数找出各科目前10名后再按学生分组找出出现次数等于科目数量的学生,造成理解困难。


缺乏对象引用


在SQL中,数据表之间的引用关系依靠同值外键来维系,无法将外键指向的记录直接用作本记录的属性,在查询时需要借助多表连接或子查询才能完成,不仅书写繁琐而且运算效率低下。



任务7 女经理的男员工们



用多表连接


select A.*
from employee A, department B, employee C
where A.department=B.department and B.manager=C.name and
A.gender='male' and C.gender='female'

用子查询


select * from employee
where gender='male' and department in
(select department from department
where manager in
(select name from employee where gender='female'
))

如果员工表中的部门字段是指向部门表中的记录,而部门表中的经理字段是指向员工表的记录,那么这个查询条件只要简单地写成这种直观高效的形式:


where gender='male' and department.manager.gender='female'

但在SQL中则只能使用多表连接或子查询,写出上面那两种明显晦涩的语句。



任务8 员工的首份工作公司



用多表连接


select name, company, first_company
from (select employee.name name, resume.company company,
row_number() over(partition by resume. name
order by resume.start_date) work_seq
from employee, resume where employee.name = resume.name)
where work_seq=1

用子查询


select name,
(select company from resume
where name=A.name and
start date=(select min(start_date) from resume
where name=A.name)) first_company
from employee A

没有对象引用机制和彻底集合化的SQL,也不能将子表作主表的属性(字段值)处理。针对子表的查询要么使用多表连接,增加语句的复杂度,还要将结果集用过滤或分组转成与主表记录一一对应的情况(连接后的记录与子表一一对应);要么采用子查询,每次临时计算出与主表记录相关的子表记录子集,增加整体计算量(子查询不能用with子句了)和书写繁琐度。


SPL的引入


问题说完,该说解决方案了。


其实在分析问题时也就一定程度地指明了解决方案,重新设计计算语言,克服掉SQL的这几个难点,问题也就解决了。


这就是发明SPL的初衷!


SPL是个开源的程序语言,其全名是Structured Process Language,和SQL只差一个词。目的在于更好的解决结构化数据的运算。SPL中强调了步骤化、支持有序集合和对象引用机制、从而得到彻底的集合化,这些都会大幅降低前面说的“解法翻译”难度。


这里的篇幅不合适详细介绍SPL了,我们只把上一节中的8个例子的SPL代码罗列出来感受一下:



任务1



AB
1=employee.select(department=="sales")=A1.len()
2=A1.select(native_place=="Beijing")=A2.len()
3=A2.select(gender=="female")=A3.len()

SPL可以保持记录集合用作中间变量,可逐步执行递进查询。



任务2



ABC
1for employee.group(department)=A1.group@1(gender)
2>if B1.len()>1=@|B1

有步骤和程序逻辑支持的SPL能很自然地逐步完成结果。



任务3



A
1=employee.sort(birthday)
2=A1((A1.len()+1)/2)

对于以有序集合为基础的SPL来说,按位置取值是个很简单的任务。



任务4



A
1=stock_price.sort(trade_date)
2=0
3=A1.max(A2=if(close_price>close_price[-1],A2+1,0))

SPL按自然的思路过程编写计算代码即可。



任务5



A
1=employee.group(month(birthday),day(birthday))
2=A1.select(~.len()>1).conj()

SPL可以保存分组结果集,继续处理就和常规集合一样。



任务6



A
1=score_table.group(subject)
2=A1.(~.rank(score).pselect@a(~<=10))
3=A1.(~(A2(#)).(name)).isect()

使用SPL只要按思路过程写出计算代码即可。



任务7



A
1=employee.select(gender=="male" && department.manager.gender=="female")

支持对象引用的SPL可以简单地将外键指向记录的字段当作自己的属性访问。



任务8



A
1=employee.new(name,resume.minp(start_date).company:first_company)

SPL支持将子表集合作为主表字段,就如同访问其它字段一样,子表无需重复计算。


SPL有直观的IDE,提供了方便的调试功能,可以单步跟踪代码,进一步降低代码的编写复杂度。


imagepng


对于应用程序中的计算,SPL提供了标准的JDBC驱动,可以像SQL一样集成到Java应用程序中:



Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
Statement st = connection.();
CallableStatement st = conn.prepareCall("{call xxxx(?,?)}");
st.setObject(1, 3000);
st.setObject(2, 5000);
ResultSet result=st.execute();
...


SPL资料



作者:苏三说技术
来源:juejin.cn/post/7189609501559881784
收起阅读 »

为了方便写文章,我开发了一个目录树🌲生成器

web
这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。 您可以通过以下链接访问:目录树生成器 - 在线使用 Next.js 是一个React全栈框架,它不仅可以用于...
继续阅读 »

这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。


image.png


您可以通过以下链接访问:目录树生成器 - 在线使用


Next.js


是一个React全栈框架,它不仅可以用于构建服务器端渲染(SSR),也支持支持静态渲染。


webkitdirectory



HTMLInputElement.webkitdirectory 是一个反应了 HTML 属性 webkitdirectory 的属性,其指示 <input> 元素应该让用户选择文件目录而非文件。在选择文件目录后,该目录及其整个内容层次结构将包含在所选项目集内。可以使用 webkitEntries (en-US) 属性获取选定的文件系统条目。

———————MDN



简而言之 利用这属性,我们可以在浏览器中上传文件夹,并获取到文件的目录结构。


可以看一个简单的栗子🌰


这个功能,也有一个兼容问题,具体参考这个:


image.png


有一些老版本的浏览器和安卓端火狐浏览器不支持的无法使用该功能。


数据转换


我们要将原数据转换一下


-   Java/main/main.java
- Java/main/main.class
- Java/hello/HelloWorld.class
- Java/hello/HelloWorld.java
- Java/OOP/xx.js
- Java/OOP/Person.class
- Java/OOP/oop.class
- Java/OOP/oop.java

转换为:


{
"name": "Java",
"type": "folder",
"contents": [
{
"name": "main",
"type": "folder",
"contents": [
{
"name": "main.java",
"type": "file"
},
{
"name": "main.class",
"type": "file"
}
]
},
{
"name": "hello",
"type": "folder",
"contents": [
{
"name": "HelloWorld.class",
"type": "file"
},
{
"name": "HelloWorld.java",
"type": "file"
}
]
},
{
"name": "OOP",
"type": "folder",
"contents": [
{
"name": "xx.js",
"type": "file"
},
{
"name": "Person.class",
"type": "file"
},
{
"name": "oop.class",
"type": "file"
},
{
"name": "oop.java",
"type": "file"
}
]
}
]
}

将路径结构转化为对象结构,方便我们的后续逻辑处理,转化方法是:



function convertToDirectoryStructure(fileList) {
const directory = {
name: "App",
type: "folder",
contents: [],
};

for (let i = 0; i < fileList.length; i++) {
const pathSegments = fileList[i].webkitRelativePath.split("/");
let currentDirectory = directory;

for (let j = 0; j < pathSegments.length; j++) {
const segment = pathSegments[j];
const isDirectory = j < pathSegments.length - 1;

let existingEntry = currentDirectory.contents.find((entry) => entry.name === segment);
if (!existingEntry) {
existingEntry = { name: segment };
if (isDirectory) {
existingEntry.type = "folder";
existingEntry.contents = [];
} else {
existingEntry.type = "file";
}
currentDirectory.contents.push(existingEntry);
}

currentDirectory = existingEntry;
}
}

return directory.contents[0];
}

最终效果


最后我们再加上一个一键复制的功能,就完成了。



最后我是将功能优化后部署到了GitHub Pagas上,如何将Next.js部署到GitHub Pages,可以看看我的这篇 如何将Next.js部署到Github Pages


最后希望大家多多使用,给个star,GitHub地址:dir-tree


作者:九旬
来源:juejin.cn/post/7292955000454692875
收起阅读 »

离职原因千万不要这样说!

HR 面作为面试的最后一关,非常重要,因为走到这一步,你已经和 Offer 只有一步之遥了。有人会认为:只要进入 HR 面就稳了,其实并不是! 在一个公司里,HR 拥有最终的人事任命权,部门主管只能提供用人建议,所以这一关千万不要大意,我每年都有学生挂在 HR...
继续阅读 »

HR 面作为面试的最后一关,非常重要,因为走到这一步,你已经和 Offer 只有一步之遥了。有人会认为:只要进入 HR 面就稳了,其实并不是!


在一个公司里,HR 拥有最终的人事任命权,部门主管只能提供用人建议,所以这一关千万不要大意,我每年都有学生挂在 HR 面。那么,今天我们就来聊聊“离职原因”这个话题。


1.同事或领导不行


虽然你在工作中,可能会遇到与同事或上司之间的不和谐关系,但直接将其作为离职原因,会对你的职业形象造成负面影响。


你应该这样回答



前公司的办公室政治比较严重,同事和领导之间相互推诿、扯皮的想象比较常见,工作效率也被无限拉低。我希望找一家能把精力放在高效工作的公司,同事之间分工明确、协同合作,同事之间氛围能好一点的公司,因为一个人的大部分时间都在公司,心情愉快的高效工作是非常重要的。



2.被裁员


这两年被裁员是比较常见的事,但直接说被裁了,可能会让 HR 觉得你的价值不高,因为那么多人,为什么偏偏把你裁了?是不是你的能力不如其他人?


你应该这样回答



原业务线被砍了,领导安排我转岗到其他业务线。但是新岗位和我的职业规划不相符,我还是想继续在XX方向深耕,所以我离职了。



3.学不到东西


如果直接说“学不到东西”,会让用人单位觉得你就是来学东西的,过段时间把东西学会了,也会从这离职的。


你应该这样回答



在原公司所负责的工作内容比较单一且重复性比较高,无法满足我个人的职业发展需求。我想找一个更有挑战性、并且更有成长空间的工作。



4.工资低


抱怨工资低,虽然工资低可能是客观原因,但是工资低也可能是因为你能力或经验的问题,所以因为工资低而离职,会让用人单位怀疑你的专业性和稳定性。


你应该这样回答



这几年我的技术以及业务能力已经得到了显著的提升,为公司创造XXX的业绩,领导对我也很认可,但是原公司的工资的涨幅非常有限,而现在需要用钱的地方很多,所以我想看看新机会。



5.加班多


互联网是典型的“薪资高、加班多”的公司,直接在离职原因中说加班多,可能会错失很多机会。


你应该这样回答



前公司常态化 996,但实际的工作量并不大,导致大部分人为了加班而加班,效率非常低,我个人并不反对加班,但这种低效的 996,我并不认可。



6.其他不能说的事



  1. 说上一家领导和公司的坏话,会给人留下一个不能融入环境或者缺乏团队意识的印象。

  2. 我男朋友/女朋友在这边工作,感情用事是职场大忌。

  3. 我爸妈不喜欢我原来的那份工作,没有主见也是职场大忌。


7.离职原因这样说


在原公司所负责的工作内容比较单一且重复性比较高,无法满足我个人的职业发展需求。我想找一个更有挑战性、并且有更大成长空间的工作。而贵公司的情况非常符合我的预期:



  • 首先,我很认可贵公司所推崇的人性化管理,非常符合我对工作环境的预期,我也相信在这样的环境中,我能发挥更大的主观能动性。

  • 并且我非常看好贵公司所处的行业和所做的事,如果我能有幸加入贵公司,相信一定能和贵公司一起发展、共同进步。


小结


HR 面作为面试的最后一关,非常重要,因为走到这一步,你已经和 Offer 只有一步之遥了。所以在回答离职原因时,不能太刻薄、不能太过于“现实”,要把刻薄的话委婉的说,要把个人的事儿往“大”的说。


作者:Java中文社群
来源:juejin.cn/post/7288238328381407251
收起阅读 »

Android:解放自己的双手,无需手动创建shape文件

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。 现在的移动应用中为了美化界面,会给各类视图增加一些圆角、描边、渐变等等效果。当然系统也提供了对应的功能,那就是创建shape标签的XML文件,例如下图就是创建一个圆角为10dp,填充...
继续阅读 »

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。



现在的移动应用中为了美化界面,会给各类视图增加一些圆角、描边、渐变等等效果。当然系统也提供了对应的功能,那就是创建shape标签的XML文件,例如下图就是创建一个圆角为10dp,填充是白色的shape文件。再把这个文件设置给目标视图作为背景,就达到了我们想要的圆角效果。


<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="#FFFFFF" />
</shape>

//圆角效果
android:background="@drawable/shape_white_r10"

但不是所有的圆角和颜色都一样,甚至还有四个角单独一个有圆角的情况,当然还有描边、虚线描边、渐变填充色等等各类情况。随着页面效果的多样和复杂性,我们添加的shape文件也是成倍增加。


这时候不少的技术大佬出现了,大佬们各显神通打造了许多自定义View。这样我们就可以使用三方库通过在目标视图外嵌套一层视图来达到原本的圆角等效果。不得不说,这确实能够大大减少我们手动创建各类shape的情况,使用起来也是得心应手,方便了不少。


问题:


简单的布局,嵌套层级较少的页面使用起来还好。但往往随着页面的复杂程度越高,嵌套层级也越来多,这个时候再使用三方库外层嵌套视图会越来越臃肿和复杂。那么有没有一种方式可以直接在XML中当前视图中增减圆角等效果呢?


还真有,使用DataBinding可以办到!


这里就不单独介绍DataBinding的基础配置,网上一搜到处都是。咱们直接进入正题,使用**@BindingAdapter** 注解,这是用来扩展布局XML属性行为的注解。


使用DataBinding实现圆角


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = ["shape_radius""shape_solidColor"])
fun View.setViewBackground(radius: Int = 0,solidColor: Int = Color.TRANSPARENT){
val drawable = GradientDrawable()
drawable.cornerRadius = context.dp2px(radius.toFloat()).toFloat()
drawable.setColor(solidColor)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_solidColor="@{@color/white}"

其实就是对当前视图的一个扩展,有点和kotlin的扩展函数类似。既然这样我们可以通过代码配置更多自定义的属性:


各方向圆角的实现:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = ["
"shape_solidColor",//填充颜色
"shape_tl_radius",//上左圆角
"shape_tr_radius",//上右圆角
"shape_bl_radius",//下左圆角
"shape_br_radius"//下右圆角
])
fun View.setViewBackground(radius: Int = 0,solidColor: Int = Color.TRANSPARENT){
val drawable = GradientDrawable()
drawable.setColor(solidColor)
drawable.cornerRadii = floatArrayOf(
context.dp2px(shape_tl_radius.toFloat()).toFloat(),
context.dp2px(shape_tl_radius.toFloat()).toFloat(),
context.dp2px(shape_tr_radius.toFloat()).toFloat(),
context.dp2px(shape_tr_radius.toFloat()).toFloat(),
context.dp2px(shape_br_radius.toFloat()).toFloat(),
context.dp2px(shape_br_radius.toFloat()).toFloat(),
context.dp2px(shape_bl_radius.toFloat()).toFloat(),
context.dp2px(shape_bl_radius.toFloat()).toFloat(),
)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_tl_radius="@{@color/white}"//左上角
shape_tr_radius="@{@color/white}"//右上角
shape_bl_radius="@{@color/white}"//左下角
shape_br_radius="@{@color/white}"//右下角

虚线描边:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = [
"shape_radius"
"shape_solidColor"
"shape_strokeWitdh",//描边宽度
"shape_dashWith",//描边虚线单个宽度
"shape_dashGap",//描边间隔宽度
])
fun View.setViewBackground(
radius: Int = 0,
solidColor: Int = Color.TRANSPARENT,
strokeWidth: Int = 0,
shape_dashWith: Int = 0,
shape_dashGap: Int = 0
){
val drawable = GradientDrawable()
drawable.setStroke(
context.dp2px(strokeWidth.toFloat()),
strokeColor,
shape_dashWith.toFloat(),
shape_dashGap.toFloat()
)
drawable.setColor(solidColor)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_solidColor="@{@color/white}"
strokeWidth="@{1}"
shape_dashWith="@{2}"
shape_dashGap="@{3}"

渐变色的使用:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = [
"shape_startColor",//渐变开始颜色
"shape_centerColor",//渐变中间颜色
"shape_endColor",//渐变结束颜色
"shape_gradualOrientation",//渐变角度
])
fun View.setViewBackground(
shape_startColor: Int = Color.TRANSPARENT,
shape_centerColor: Int = Color.TRANSPARENT,
shape_endColor: Int = Color.TRANSPARENT,
shape_gradualOrientation: Int = 1,//TOP_BOTTOM = 1 ,TR_BL = 2,RIGHT_LEFT = 3,BR_TL = 4,BOTTOM_TOP = 5,BL_TR = 6,LEFT_RIGHT = 7,TL_BR = 8
){
val drawable = GradientDrawable()
when (shape_gradualOrientation) {
1 -> drawable.orientation = GradientDrawable.Orientation.TOP_BOTTOM
2 -> drawable.orientation = GradientDrawable.Orientation.TR_BL
3 -> drawable.orientation = GradientDrawable.Orientation.RIGHT_LEFT
4 -> drawable.orientation = GradientDrawable.Orientation.BR_TL
5 -> drawable.orientation = GradientDrawable.Orientation.BOTTOM_TOP
6 -> drawable.orientation = GradientDrawable.Orientation.BL_TR
7 -> drawable.orientation = GradientDrawable.Orientation.LEFT_RIGHT
8 -> drawable.orientation = GradientDrawable.Orientation.TL_BR
}
drawable.gradientType = GradientDrawable.LINEAR_GRADIENT//线性
drawable.shape = GradientDrawable.RECTANGLE//矩形方正
drawable.colors = if (shape_centerColor != Color.TRANSPARENT) {//有中间色
intArrayOf(
shape_startColor,
shape_centerColor,
shape_endColor
)
} else {
intArrayOf(shape_startColor, shape_endColor)
}//渐变色
background = drawable
}

//xml文件中
shape_startColor="@{@color/cl_F1E6A0}"
shape_centerColor="@{@color/cl_F8F8F8}"
shape_endColor=@{@color/cl_3CB9FF}

不止设置shape功能,只要可以通过代码设置的功能一样可以在BindingAdapter注解中自定义,使用起来是不是更加方便了。


总结:



  • 注解BindingAdapter中value数组的自定义属性一样要和方法内的参数一一对应,否则会报错。

  • 布局中使用该自定义属性时需要将布局文件最外层修改为layout标签

  • XML中使用自定义属性时一定要添加@{}


好了,以上便是解放自己的双手,无需手动创建shape文件的全部内容,希望能给大家带来帮助!


作者:似曾相识2022
来源:juejin.cn/post/7278858311596359739
收起阅读 »

通过问题透析 IDEA Debug

引言 本来通过问题引入,一步一步对问题进行分析,重点学习 IDEA Debug 的能力解决问题。阅读本文可以学习如何通过 IDEA 的 Debug 功能解决实际问题。本文适合学生和刚工作的朋友,把 IDEA 作为开发工具,并且有 Spring 和 JPA 的使...
继续阅读 »

引言


本来通过问题引入,一步一步对问题进行分析,重点学习 IDEA Debug 的能力解决问题。阅读本文可以学习如何通过 IDEA 的 Debug 功能解决实际问题。本文适合学生和刚工作的朋友,把 IDEA 作为开发工具,并且有 Spring 和 JPA 的使用经验。


问题引入


最近看了 eclipse 开源的集合 Eclipse Collections,觉得它的使用相比 JDK 集合更加简洁,想在实际项目中使用。部分 API 对比如下。


JDK API


 //users is List<User> 
users.stream.map(user -> user.getCity()).collect(Collectors.toList());

Eclipse Collections API


 //users is MutableList<User>
users.collect(user -> user.getCity);

可以看到后者比前者要简洁不少。实际开发中集合数据大多还是来自数据库查询,使用 JPA 查询如下。


JDK API


List<User> findByCity(String city);

我想改成 Eclipse Collections API


MutableList<User> findByCity(String city);

然而报错了


org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.util.ArrayList<?>] to type [org.eclipse.collections.api.list.MutableList<?>] for value '[]'; nested exception is java.lang.IllegalArgumentException: Unsupported Collection interface: org.eclipse.collections.api.list.MutableList
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175)

如果不想看过过程,结论是改成如下代码或者升级 sping boot 到 2.7.0 及以上版本。


FastList<User> findByCity(String city);

Debug


对代码简单分析



  • 查看类名称,方法名称。 有 convert.ConversionFailedException/ConversionUtils.invokeConverter/convert.support.GenericConversionService.convert等等,关键词 convert。我们应该联想到这段代码的功能是把某一个类型 convert 到某一个类型。

  • 再看一眼报错信息,Failed to convert from type [java.util.ArrayList<?>] to type [org.eclipse.collections.api.list.MutableList<?>],无法将 ArrayList 转换成 MutableList

  • 再分析报错的那一行return converter.convert(source, sourceType, targetType),我们会更清晰一点。

    • result 是转换的结果,应该是 MutableList 的一个实例。

    • convert 方法是执行转换的核心逻辑,我们要的核心转换逻辑代码肯定在这里,如果你直接去看的话,它肯定是一个接口,这是面向接口编程。

    • sourceType 源类型,是 ArrayList 类型。

    • targetType 目标类型,是 MutableList 类型。




打断点


在 IDEA 控制台可以直接点击报错 class 定位到源文件,我们先点击 ConversionFailedException ,再点击 ConversionUtils.java:47,发现都是报错的异常,对我们没有帮助。最后我们点击 GenericConversionService.java:192,终于看到一行代码了。


Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);

断点分析


执行过程会停留在断点处,我们可以查看上下文变量类的实例。这里我们以 converter 为例。按照数字步骤点击,如下。


image.png


图中显示是 ConvertertoString 方法的结果。
可能的 converter 如下:


1. java.lang.String -> java.lang.Enum
2. NO_OP
3. java.lang.Boolean -> java.lang.String
// 等等。。。。。

由于是底层方法,被调用的次数很多,在这个断点停留的次数也很多。很多次不是我们想要的 Converter


条件断点


顾名思义 IDEA 会通过我们添加的条件来判断这个断点是否需要被处理。


我们想要的 Converter 是什么呢?回到代码分析阶段,我们想要的 ConvertersourceTypetargetType,通过上面分析 targetType 类型是 MutableList 类型。


下面添加条件断点:


image.png

完整的条件如下:


MutableList.class.isAssignableFrom(targetType.getType());

添加成功的标志如下,会在断点处显示问号。


image.png


单步调试


Debug 模式启动程序,可以看到 IDEA 停留在我们的条件断点上,并且targetType 的类型正是 MutableList


image.png


单步调试代码,来到 org.springframework.core.CollectionFactory#createCollection 方法。部分代码如下:


//省略的其他代码

// 判断集合类型是不是 ArrayList 或者 List,显然这里不是
else if (ArrayList.class == collectionType || List.class == collectionType) {
return new ArrayList<>(capacity);
}
//省略的其他代码

else {
//如果是集合类型的接口 或者 不是集合类型抛出异常
if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
}
try {
//如果是集合类型的类,直接通过反射实例化。
return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();
}
}

重回代码分析


上面的 collectionTypeMutableList,而 MutableList 是接口,走读代码可以发现最终会执行下面的代码,最终导致抛出异常。


if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
}

所以只需要我们的目标集合不是接口就行了,FastListMutableList 的实现类。 修改代码为如下:


FastList<User> findByCity(String city);

翻看控制台找到了下面的异常信息,这也侧面反映我们之前找的报错位置不是很精确。我们寻找异常时应该选择最原始的异常信息。


Caused by: java.lang.IllegalArgumentException: Unsupported Collection type: org.eclipse.collections.api.list.MutableList
at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:205)
at org.springframework.core.convert.support.CollectionToCollectionConverter.convert(CollectionToCollectionConverter.java:81)

继续分析源码可以发现,如果我们定义的类型不是接口,JPA 就会通过反射创建集合,即如下代码:


return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();

总结


本来通过解决实际问题介绍了 IDEA Debug 功能的使用。还有以下几点需要注意。



  • 查找异常时要定位到最初始的异常,这样往往能迅速处理问题。

  • 本文的问题只有在 sping boot 2.7.0 以下才会出现,高版本已经修复此问题。参见提交 spring data common

  • 使用非 Java 官方集合需要进行转换,有微小的性能损耗,对于常规内存操作来说影响很小,而且高版本中有优化。如果查询数据上千上万条时,应该避免转换,当然也要使用分页避免一次性查询成千上万的数据。


本文源码


作者:郁乎文
来源:juejin.cn/post/7185569129024192568
收起阅读 »

Redis只用来做缓存?来认识一下它其他强大的能力吧。

当今互联网应用中,随着业务的发展,数据量越来越大,查询效率越来越高,对于时序数据的存储、查询和分析需求也越来越强烈,这时候 Redis 就成为了首选的方案之一。 Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,每种数据结构都具备不同的...
继续阅读 »

当今互联网应用中,随着业务的发展,数据量越来越大,查询效率越来越高,对于时序数据的存储、查询和分析需求也越来越强烈,这时候 Redis 就成为了首选的方案之一。


Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,每种数据结构都具备不同的特性,可以满足不同的业务需求。其中,有序集合的 score 可以存储时间戳,非常适合用于存储时序数据,例如监控指标、日志、统计数据、报表等。下面举几个时序数据场景例子:



  1. 监控指标:


假设我们有一个服务,名为 my_service,需要监控它的请求响应时间。我们可以使用 Redis 有序集合来存储数据,每个请求的响应时间作为 value,请求的时间戳作为 score。示例如下:


> ZADD requests:my_service 1613115560 350
(integer) 1
> ZADD requests:my_service 1613115570 450
(integer) 1
> ZADD requests:my_service 1613115580 550
(integer) 1

这些命令向名为 requests:my_service 的有序集合中添加了 3 条数据,分别是 2021 年 2 月 12 日 10:19:20 的请求响应时间为 350ms,10:19:30 的请求响应时间为 450ms,10:19:40 的请求响应时间为 550ms。


接下来,我们来看一下如何使用 Redis 命令查询这些监控指标的数据。下面的命令会返回 requests:my_service 有序集合内所有数据:


> ZRANGE requests:my_service 0 -1 WITHSCORES
1) "350"
2) "1613115560"
3) "450"
4) "1613115570"
5) "550"
6) "1613115580"

命令执行结果表示,数据按照 score 排序,其中 score 是时间戳(单位为秒),value 是请求响应时间(单位为毫秒)。同时,使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的监控数据,例如:


> ZRANGEBYSCORE requests:my_service 1613115570 1613115580 WITHSCORES
1) "450"
2) "1613115570"
3) "550"
4) "1613115580"

这条命令返回了 requests:my_service 有序集合中在时间戳 1613115570 到 1613115580 之间的所有数据。



  1. 日志:


假设我们要存储的日志是一条指定格式的字符串,包含时间戳和日志内容。使用 Redis 列表存储日志数据,每次写入新日志时可以使用 Redis 列表的 rpush 命令将数据写入列表的尾部。示例如下:


> RPUSH logs:my_logs 2021-02-12 10:30:00 INFO message 1
(integer) 1
> RPUSH logs:my_logs 2021-02-12 10:30:01 ERROR message 2
(integer) 2
> RPUSH logs:my_logs 2021-02-12 10:30:02 WARN message 3
(integer) 3

这些命令向名为 logs:my_logs 的列表尾部添加 3 条数据,分别是 2021 年 2 月 12 日 10:30:00 的 INFO 级别消息,10:30:01 的 ERROR 级别消息和 10:30:02 的 WARN 级别消息。


接下来,我们来看一下如何使用 Redis 命令查询这些日志数据。下面的命令会返回 logs:my_logs 列表内所有数据:


> LRANGE logs:my_logs 0 -1
1) "2021-02-12 10:30:00 INFO message 1"
2) "2021-02-12 10:30:01 ERROR message 2"
3) "2021-02-12 10:30:02 WARN message 3"

命令执行结果表示,数据按照插入顺序排序,从列表头部开始遍历。使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的日志数据,例如:


> ZRANGEBYSCORE logs:my_logs 1613115570 1613115580
1) "2021-02-12 10:30:01 ERROR message 2"

这条命令返回了 logs:my_logs 列表中在时间戳 1613115570 到 1613115580 之间的日志数据,但因为日志数据并没有具体的 time stamp 做 score,所以这个例子只是演示这个命令的用法,实际上应该使用有序集合去查询时间区间内的日志数据。



  1. 统计数据:


假设我们要存储的统计数据是一些具体业务相关的计数器,例如每分钟用户访问量。我们可以使用 Redis 有序集合来存储统计数据,key 是计数器名称,score 是时间戳,value 是具体的计数值(例如访问次数)。示例如下:


> ZADD visits 1613167800 100
(integer) 1
> ZADD visits 1613167860 120
(integer) 1
> ZADD visits 1613167920 150
(integer) 1

这些命令向名为 visits 的有序集合中添加了 3 条数据,分别是 2021 年 2 月 12 日 23:30:00 的访问次数为 100,23:31:00 的访问次数为 120,23:32:00 的访问次数为 150。


接下来,我们来看一下如何使用 Redis 命令查询这些统计数据。下面的命令会返回 visits 有序集合内所有数据:


> ZRANGE visits 0 -1 WITHSCORES
1) "100"
2) "1613167800"
3) "120"
4) "1613167860"
5) "150"
6) "1613167920"

命令执行结果表示,数据按照 score 排序,其中 score 是时间戳(单位为秒),value 是访问次数。使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的统计数据,例如:


> ZRANGEBYSCORE visits 1613167860 1613167920 WITHSCORES
1) "120"
2) "1613167860"
3) "150"
4) "1613167920"

这条命令返回了 visits 有序集合中在时间戳 1613167860 到 1613167920 之间的所有数据。


使用 Redis 有序集合中的另一个常见场景是计算 TopN,例如找出访问次数最多的前 10 个计数器,可以使用命令 ZREVRANGE visits 0 9 WITHSCORES,它返回 visits 有序集合中前 10 个元素,按照 value 从大到小排列,并且返回每个元素的 score。


需求实践:


这是一个实时监控系统,主要用于记录和统计服务发生的错误情况,以便在错误数量超过预设阈值时发出警告信息。


系统每秒钟生成随机错误数据,并将它们存储到 Redis 数据库中。每隔 10 秒钟,系统会从 Redis 数据库中聚合最近一分钟内的错误数据,并按照服务名和错误类型进行统计计算。如果某个服务的错误数量超过预设阈值,系统会输出一条警告信息提示用户。


整个系统的目标是帮助用户及时了解每个服务的错误情况,以便及时采取相应的措施,保障服务的稳定性和可靠性。


代码示例:


模拟接口服务异常数据


package com.example.demo.redis;

import redis.clients.jedis.Jedis;
import java.util.*;

public class DataGenerator {
// 定义服务列表
private static final List SERVICES = Arrays.asList("service1", "service2", "service3");
// 定义错误列表
public static final List ERRORS = Arrays.asList("invalid_param", "timeout", "unknown_error");

/**
* 生成数据
*
*
@param total 数据总数
*
@param jedis Redis 客户端连接
*/

public static void generateData(int total, Jedis jedis) {
Random rand = new Random(); // 初始化随机数生成器
long currentTimestamp = System.currentTimeMillis() / 1000; // 获取当前时间戳,精确到秒
long startTimestamp = currentTimestamp - 60; // 计算起始时间戳,为当前时间戳减去 60 秒

for (int i = 0; i < total; i++) { // 循环 total 次,生成 total 条数据
String service = SERVICES.get(rand.nextInt(SERVICES.size())); // 随机选择一个服务
String error = ERRORS.get(rand.nextInt(ERRORS.size())); // 随机选择一个错误
long timestamp = startTimestamp + rand.nextInt(60); // 生成一个随机时间戳,精确到秒,范围为起始时间戳到当前时间戳
int count = 1;
String item = String.format("%s:%s:%d:%d", service, error, timestamp, count);
jedis.zadd("error_data", timestamp, item); // 将错误数据存储到 Redis 数据库中
}
}
}


聚合异常数据,达到阈值告警


package com.example.demo.redis;

import redis.clients.jedis.Jedis;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class DataAggregator {
private static final String REDIS_HOST = "localhost"; // Redis 主机名
private static final int REDIS_PORT = 6379; // Redis 端口号
private static final int THRESHOLD = 100; // 预设阈值,当错误数量超过该阈值时触发警告

public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // 创建一个只有一个线程的定时任务执行程序
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT); // 创建 Redis 客户端连接

scheduler.scheduleAtFixedRate(() -> {
// 并发情况下,线程会阻塞
synchronized (jedis) {
DataGenerator.generateData(20, jedis); // 生成随机错误数据,并将其存储到 Redis 数据库中
}
}, 0, 1, TimeUnit.SECONDS); // 定时任务间隔为 1 秒钟

scheduler.scheduleAtFixedRate(() -> { // 定时任务逻辑
synchronized (jedis) {
long currentTimestamp = System.currentTimeMillis() / 1000; // 获取当前时间戳,精确到秒
long startTimestamp = currentTimestamp - 60; // 计算起始时间戳,为当前时间戳减去 60 秒
Set data = jedis.zrangeByScore("error_data", startTimestamp, currentTimestamp); // 使用 zrange 命令获取指定时间范围内的数据

Map> countMap = new HashMap<>(); // 用于记录聚合后的服务和错误数量信息
for (String item : data) { // 遍历所有错误数据
String[] parts = item.split(":"); // 以冒号为分隔符,将错误数据分割为部分
String service = parts[0]; // 获取服务名
String error = parts[1]; // 获取错误类型
long timestamp = Long.parseLong(parts[2]); // 获取时间戳
int count = Integer.parseInt(parts[3]); // 获取错误数量

if (timestamp < startTimestamp) { // 如果时间戳早于起始时间戳,则跳过该数据
continue;
}

Map serviceCountMap = countMap.computeIfAbsent(service, k -> new HashMap<>()); // 获取指定服务的错误数量信息
serviceCountMap.put(error, serviceCountMap.getOrDefault(error, 0) + count); // 更新指定服务和错误类型的错误数量信息
}

List alerts = new ArrayList<>(); // 用于存储警告信息
for (String service : countMap.keySet()) { // 遍历服务名列表
Map serviceCountMap = countMap.get(service); // 获取服务和错误数量信息
int totalErrors = 0;
for (String error : serviceCountMap.keySet()) { // 遍历错误列表
int count = serviceCountMap.get(error); // 获取错误数量
totalErrors += count;
}
if (totalErrors > THRESHOLD) { // 如果错误数量超过预设阈值
alerts.add(service + " has too many errors: " + serviceCountMap.keySet() + ", count: " + totalErrors); // 将该服务名添加到警告信息列表中
}
}
if (!alerts.isEmpty()) { // 如果警告信息列表不为空
System.out.println(String.join("\n", alerts)); // 打印警告信息
}
}
}, 0, 10, TimeUnit.SECONDS); // 定时任务间隔为 10 秒

// 关闭 Redis 连接
jedis.close();
}
}


以上代码可正常运行,有疑问可以交流~~


作者:程序员的思考与落地
来源:juejin.cn/post/7219669309537484837
收起阅读 »

分库分表,真的有必要吗?

分库分表,真的有必要吗? 哈喽,大家好,我是janker。 关于数据库分库分表的面试题已经是烂大街了,面经小册各路神仙的经验分享也是不绝于耳。当然现有的技术解决方案已经是很成熟了。 但是想要使用的得心应手,首先应该搞清楚三个问题? 为什么使用分库分表? 何时...
继续阅读 »

分库分表,真的有必要吗?


哈喽,大家好,我是janker。


关于数据库分库分表的面试题已经是烂大街了,面经小册各路神仙的经验分享也是不绝于耳。当然现有的技术解决方案已经是很成熟了。


但是想要使用的得心应手,首先应该搞清楚三个问题?



  • 为什么使用分库分表?

  • 何时使用分库分表?

  • 如何分库分表?


为什么使用分库分表?


答案很简单:当数据库出现性能瓶颈。顾名思义就是数据库扛不住了。


数据库出现瓶颈,对外表现有以下几个方面?



  1. 高并发场景下,大量请求阻塞,大量请求都需要操作数据库,导致连接数不够了,请求处于阻塞状态。

  2. SQL操作变慢(慢SQL增多)如果数据库中存在一张上亿数据量的表,一条 SQL 没有命中索引会全表扫描,这个查询耗时会非常久。

  3. 随着业务流量变大存储出现问题,单库数据量越来越大,给存储造成巨大压力。


从机器角度,性能瓶颈不外乎就是CPU、磁盘、内存、网络这些,要解决性能瓶颈最简单粗暴的方式就是提升机器性能,但是通过这种方式投入产出比往往不高,也不划算,所以重点还是要从软件层面去解决问题。


数据库相关优化方案


数据库优化方案很多,主要分为两大类:软件层面、硬件层面。


软件层面包括:SQL 调优、表结构优化、读写分离、数据库集群、分库分表等;


硬件层面主要是增加机器性能。


分库分表其实不是数据库优化方案的最终解决办法,一般来说说能用优化SQL、表结构优化、读写分离等手段解决问题,就不要分库分表,因为分库分表会带来更多需要解决的问题,比如说分布式事务,查询难度增大等。


何时使用分库分表?


什么时候我们才会选择分库分表?前面已经说了,除了分库分表以外那些软件手段搞不定的时候,我们才会选择分库分表。


我们心中可能会有这些疑问?



  1. 使用分库分表,我们的评判依据是什么?

  2. 一张表存储了多少数据的时候,才需要考虑分库分表?

  3. 数据增长速度很快,每天产生多少数据,才需要考虑做分库分表?


阿里巴巴开发手册有推荐的思路:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。


注意:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。


如何分库分表?


当前针对分库分表有很多解决方案。这里分两个方面来展开说说:分库 和 分表。


分库


很多项目前期为了快速迭代,多个应用公用一套数据库,随着业务量增加,系统访问压力增大,系统拆分就势在必行。


为了保证业务平滑,系统架构重构也是分了几个阶段进行。


多应用单数据库


第一个阶段将商城系统单体架构按照功能模块拆分为子服务,比如:Portal 服务、用户服务、订单服务、库存服务等。


image-20230107232802486


多应用单数据库如上图,多个服务共享一个数据库,这样做的目的是底层数据库访问逻辑可以不用动,将影响降到最低。


多应用多数据库


随着业务推广力度加大,数据库终于成为了瓶颈,这个时候多个服务共享一个数据库基本不可行了。我们需要将每个服务相关的表拆出来单独建立一个数据库,这其实就是“分库”了。


单数据库的能够支撑的并发量是有限的,拆成多个库可以使服务间不用竞争,提升服务的性能。


image-20230107233441875


从一个大的数据中分出多个小的数据库,每个服务都对应一个数据库,这就是系统发展到一定阶段必要要做的“分库”操作。


分表


说完了分库,那什么时候才会分表呢?


如果系统处于高速发展阶段,拿商城系统来说,一天下单量可能几十万,那数据库中的订单表增长就特别快,增长到一定阶段数据库查询效率就会出现明显下降。


因此当表数据增长过快,根据阿里巴巴开发规范中超过500w的数据就要考虑分表了,当然这只是一个经验值,具体要不要分表还要看业务考虑未来三年的一个业务增量。


如何分表?


分表有几个维度,一是水平切分和垂直切分,二是单库内分表和多库内分表。


水平拆分和垂直拆分


拿商品表来说,表中分为几类属性:一类是基础属性,例如:商品名称、通用名,商品编码等信息。二类是规格属性:尺寸、型号等信息。三类是拓展属性:描述商品特征的一些属性。我们可以将其拆分为基础信息表、拓展属性表、属性信息表等。这几张表结构不同并且相互独立。但是从这个角度没有解决因为数据量过大而带来的性能问题,因此我们还需要继续做水平拆分。


image-20230108161956743


水平拆分表的方法很多种,比如说1w条数据,我们拆分为两个表,id 为基数的放在user1,id为偶数的放在user2中,这样的拆分方式就是水平拆分。


其他水平拆分的方式也很多,除了上面按照 id 来拆分,还可以按照时间维度拆分,比如订单表,可以按照每日、每月等进行拆分。



  • 每日表:只存储当天你的数据。

  • 每月表:可以起一个定时任务将前一天的数据全部迁移到当月表。

  • 历史表:同样可以用定时任务把时间超过 30 天的数据迁移到 history表。


总结一下水平拆分和垂直拆分的特点:



  • 垂直切分:基于表或者字段划分,表结构不同。

  • 水平拆分:基于数据划分,表结构相同,数据不同。根据表中字段的某一列特性,分而治之。


水平拆分也分两种拆分方式。单库内拆分和多库内拆分


单库内拆分和多库内拆分


拿针对用户表的拆分来举例,之前的单个用户表按照某种规则拆分为多个子表,多个子表存在于同一数据库中。比如下面用户表拆分为用户1表、用户2表。


image-20230108203705147


单库内拆分是在一个数据库中,将一张表拆分为多个子表,一定程度上可以解决单表查询的性能问题,但是也会遇到另外一个问题:但数据库的数据瓶颈。


所以在行业内更多的将子表拆分到多个数据库中,如下图,用户表拆分为4个子表,4个子表分两批存在两个不同的数据库中,根据一定的规则进行路由。


image-20230108204316330


多库拆分用一句话概括:主要是为了减少单张表数据量大小,解决单表数据量过大带来的性能问题。


但是分库分表也带来许多问题。


分库分表带来的问题


既然分库分表方案那么好,那我们是不是在项目初期就应该采用这种方案呢?莫慌,虽然分库分表解决了很多性能问题,但是同时也给系统带来了很多复杂性。下面我展开说说


1. 跨库关联查询


之前单体项目,我们想查询一些数据,无脑join就好了,只要数据模型设计没啥问题,关联查询起来其实还是很简单的。现在不一样了,分库分表后的数据可能不在一个数据库,那我们如何关联查询呢?


下面推荐几种方式去解决这个问题:



  1. 字段冗余:把需要关联的字段放到主表中,避免join操作,但是关联字段更新,也会引发冗余字段的更新;

  2. 数据抽象:通过ETL 等将数据汇总聚合,生成新的表;

  3. 全局表:一般是一些基础表可以在每个数据库都放一份;

  4. 应用层组装:将基础数据查出来,通过应用程序计算组装;

  5. 同特征的数据在一张表:举个例子:同一个用户的数据在同一个库中,比如说我们对订单按照用户id进行分表,订单主表、订单拓展信息表、跟订单有关联查询的表都按照用户id 进行分表,那么同一个用户的订单数据肯定在同一个数据库中,订单维度的关联查询就不存在跨库的问题了。


2. 分布式事务


单数据库我们可以用本地事务搞定,使用多数据库就只能通过分布式事务解决了。


常用的解决方案有:基于可靠消息(MQ)的最终一致性方案、二段式提交(XA)、柔性事务。


当然分布式事务相关开源项目推荐两个:SeataTX-LCN


比较推荐 Seata,阿里出品、大厂加持、如果需要企业级版本支持也是有的。


3. 排序、分表、函数计算问题


使用SQL 时,order bylimit 等关键字需要特殊处理,一般都是采用数据分片的思路:现在每个分片路由上执行函数、然后将每个分片路由的结果汇总再计算,然后得出最终结果。


开源的解决方案当然也有不少,比较推荐shardingsphere,无论是基于client 或者 基于数据库proxy的都有支持。


4. 分布式ID


既然分库分表了,主键id已经不能唯一确定我们的业务数据了,随之而来的就是分布式id,顾名思义就是在多个数据库多张表中唯一确定的ID。


常见的分布式Id 解决方案有:



  1. UUID

  2. 基于全局数据库自增的ID表

  3. 基于Redis缓存生成全局ID

  4. 雪花算法(Snowflake

  5. 百度uid-generator(雪花算法的变种)

  6. 美团Leaf(雪花算法的变种)

  7. 滴滴Tinyid


这些解决方案后面有专门的文章去介绍,这里不过多展开。


5. 多数据源


分库分表之后可能面临从多个数据库中获取数据,一般的解决方案有,基于 client 适配 和 基于 proxy 适配。


比较成熟并且常用的中间件有:



  • shardingsphere(apache顶级项目相当成熟,文档完善)

  • MyCat (社区不太活跃、不推荐)


总结


如果遇到数据库问题,建议不要着急分库分表。原则是:能不分库分表就不要做。先看下能否通过常规优化手段解决问题。


如上所述,引入分库分表虽然可以解决数据库瓶颈问题,但是也给系统带来巨大的复杂性,不是非必须不要使用。设计系统我们一向要本着高可拓展去设计,但是不要过度设计和超前设计。适合当前系统的设计才是最好的。


作者:爪哇干货分享
来源:juejin.cn/post/7186448714779590711
收起阅读 »

ElectronEgg 快速开发一个桌面应用

web
大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。 近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。 目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、E...
继续阅读 »

大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。


近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。


目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、ERP、娱乐、视频等领域客户端,请放心使用!


home.png


为什么使用


桌面软件(办公方向、 个人工具),仍然是未来十几年 PC 端需求之一,提高工作效率


electron 技术是流行趋势,QQ、百度翻译、阿里网盘、迅雷、有道云笔记 ......


开源


gitee:gitee.com/dromara/ele… 3900+


github:github.com/dromara/ele… 1200+


本次更新


3.8.0



  1. 【增加】新增 ee-bin exec 命令,支持自定义命令。

  2. 【增加】新增 ee-core jobs 配置,打开/关闭 messageLog。

  3. 【优化】优化 ee-core jsondb 异常处理。

  4. 【优化】优化 ee-core controller/services 异常捕获并写log。

  5. 【优化】优化 ee-bin loading 动画居中。

  6. 【优化】优化 electron-egg logo,优化mac图标,优化Linux系统图标。

  7. 【优化】优化 electron-egg loading 动画居中。

  8. 【升级】升级ee-core v2.6.0,升级ee-bin v1.3.0


下载


# gitee
git clone https://gitee.com/dromara/electron-egg.git

# github
git clone https://github.com/dromara/electron-egg.git

安装


# 设置国内镜像源(加速)
npm config set registry=https://registry.npmmirror.com

#如果下载electron慢,配置如下
npm config set electron_mirror=https://registry.npmmirror.com/-/binary/electron/

# 根目录,安装 electron 依赖
npm i

# 进入【前端目录】安装 frontend 依赖
cd frontend
npm i

运行项目


npm run start

example.png


用户案例


aw-3.png


p1.png


p3.png


更多


访问官网:electron-egg: 一个入门简单、跨平台、企业级桌面软件开发框架


作者:哆啦好梦
来源:juejin.cn/post/7292961931509186595
收起阅读 »

这个面试官真烦,问完合并又问拆分。

你好呀,我是歪歪。 这次来盘个小伙伴分享给我的一个面试题,他说面试的过程中面试官的问了一个比较开放的问题: 请谈谈你对于请求合并和分治的看法。 他觉得自己没有答的特别好,主要是没找到合适的角度来答题,跑来问我怎么看。 我能怎么看? 我也不知道面试官想问啥角...
继续阅读 »

你好呀,我是歪歪。


这次来盘个小伙伴分享给我的一个面试题,他说面试的过程中面试官的问了一个比较开放的问题:



请谈谈你对于请求合并和分治的看法。



他觉得自己没有答的特别好,主要是没找到合适的角度来答题,跑来问我怎么看。


我能怎么看?


我也不知道面试官想问啥角度啊。但是这种开放题,只要回答的不太离谱,应该都能混得过去。


比如,如果你回答一个:我认为合并和分治,这二者之间是辩证的关系,得用辩证的眼光看问题,它们是你中有我,我中有你~


那凉了,拿着简历回家吧。



我也想了一下,如果让我来回答这个问题,我就用这篇文章来回答一下。


有广义上的实际应用场景,也有狭义上的源代码体现对应的思想。


让面试官自己挑吧。



铺垫一下


首先回答之前肯定不能干聊,所以我们铺垫一下,先带入一个场景:热点账户。


什么是热点账户呢?


在第三方支付系统或者银行这类交易机构中,每产生一笔转入或者转出的交易,就需要对交易涉及的账户进行记账操作。


记账粗略的来说涉及到两个部分。



  • 交易系统记录这一笔交易的信息。

  • 账户系统需要增加或减少对应的账户余额。


如果对于某个账户操作非常的频繁,那么当我们对账户余额进行操作的时候,肯定就会涉及到并发处理的问题。


并发了怎么办?


我们可以对账户进行加锁处理嘛。但是这样一来,这个账户就涉及到频繁的加锁解锁操作。


虽然这样我们可以保证数据不出问题,但是随之带来的问题是随着并发的提高,账户系统性能下降。


极少数的账户在短时间内出现了极大量的余额更新请求,这类账户就是热点账户,就是性能瓶颈点。


热点账户是业界的一个非常常见的问题。


而且根据热点账户的特性,也可以分为不同的类型。


如果余额的变动是在频繁的增加,比如头部主播带货,只要一喊 321,上链接,那订单就排山倒海的来了,钱就一笔笔的打到账户里面去了。这种账户,就是非常多的人在给这个账户打款,频率非常高,账户余额一直在增加。


这种账户叫做“加余额频繁的热点账户”。


如果余额的变动是在频繁的减少,比如常见的某流量平台广告位曝光,这种属于扣费场景。


商家先充值一笔钱到平台上,然后平台给商家一顿咔咔曝光,接着账户上的钱就像是流水一样哗啦啦啦的就没了。


这种预充值,然后再扣减频繁的账户,这种账户叫做“减余额频繁的热点账户”。


还有一种,是加余额,减余额都很频繁的账户。


你细细的嗦一下,什么账户一边加一遍减呢,怎么感觉像是个二手贩子在左手倒右手呢?


这种账户一般不能细琢磨,琢磨多了,就有点灰色地带了,点到为止。



先说请求合并


针对“加余额频繁的热点账户”我们就可以采取请求合并的思路。


假设有个歪师傅是个正经的带货主播,在直播间穿着女装卖女装,我只要喊“321,上链接”姐妹们就开始冲了。



随着歪师傅生意越来越好,有的姐妹们就反馈下单越来越慢。


后来一分析,哦,原来是更新账户余额那个地方是个性能瓶颈,大量的单子都在这里排着队,等着更新余额。


怎么办呢?


针对这种情况,我们就可以把多笔调整账务余额的请求合并成一笔处理。



当记录进入缓冲流水记录表之后,我就可以告知姐妹下单成功了,虽然钱还没有真的到我的账户中来,但是你放心,有定时任务保证,钱一会就到账。


所以当姐妹们下单之后,我们只是先记录数据,并不去实际动账户。等着定时任务去触发记账,进行多笔合并一笔的操作。


比如下面的这个示意图:



对于歪师傅来说,实际有 6 个姐妹的支付记录,但是只有一次余额调整。


而我拿着这一次余额调整的账户流水,也是可以追溯到这 6 笔交易记录的详情。


这样的好处是吞吐量上来了,用户体验也好了。但是带来的弊端是余额并不是一个准确的值。


假设我们的定时任务是一小时汇总一次,那么歪师傅在后端看到的交易金额可能是一小时之前的数据。


但是歪师傅觉得没关系,总比姐妹们下不了单强。



如果我们把缓冲流水记录表看作是一个队列。那么这个方案抽象出来就是队列加上定时任务。


所以,请求合并的关键点也是队列加上定时任务。


除了我上面的例子外,比如还有 redis 里面的 mget,数据库里面的批量插入,这玩意不就是一个请求合并的真实场景吗?


比如 redis 把多个 get 合并起来,然后调用 mget。多次请求合并成一次请求,节约的是网络传输时间。


还有真实的案例是转账的场景,有的转账渠道是按次收费的,那么作为第三方公司,我们就可以把用户的请求先放到表里记录着,等一小时之后,一起汇总发起,假设这一小时内发生了 10 次转账,那么 10 次收费就变成了 1 次收费,虽然让客户等的稍微久了点,但还是在可以接受的范围内,这操作节约的就是真金白银了。


请求合并,说穿了,就这么一点事儿,一点也不复杂。


那么如果我在请求合并的前面,加上“高并发”这三个字...



首先不论是在请求合并的前面加上多么狂拽炫酷吊炸天的形容词,说的多么的天花乱坠,它也还是一个请求合并。


那么队列和定时任务的这个基础结构肯定是不会变的。


高并发的情况下,就是请求量非常的大嘛,那我们把定时任务的频率调高一点不就行了?


以前 100ms 内就会过来 50 笔请求,我每收到一笔就是立即处理了。


现在我们把请求先放到队列里面缓存着,然后每 100ms 就执行一次定时任务。


100ms 到了之后,就会有定时任务把这 100ms 内的所有请求取走,统一处理。


同时,我们还可以控制队列的长度,比如只要 50ms 队列的长度就达到了 50,这个时候我也进行合并处理。不需要等待到 100ms 之后。


其实写到这里,高并发的请求合并的答案已经出来了。


关键点就三个:



  • 一是需要借助队列加定时任务实现。

  • 二是控制定时任务的执行时间.

  • 三是控制缓冲队列的任务长度。


方案都想到了,把代码写出来岂不是很容易的事情。而且对于这种面试的场景图,一般都是讨论技术方案,而不太会去讨论具体的代码。


当讨论到具体的代码的时候,要么是对你的方案存疑,想具体的探讨一下落地的可行性。要么就是你答对了,他要准备从代码的交易开始衍生另外的面试题了。


总之,大部分情况下,不会在你给了一个面试官觉得错误的方案之后,他还和你讨论代码细节。你们都不在一个频道了,赶紧换题吧,还聊啥啊。


实在要往代码实现上聊,那么大概率他是在等着你说出一个框架:Hystrix。


其实这题,你要是知道 Hystrix,很容易就能给出一个比较完美的回答。


因为 Hystrix 就有请求合并的功能。


通过一个实际的例子,给大家演示一下。


假设我们有一个学生信息查询接口,调用频率非常的高。对于这个接口我们需要做请求合并处理。


做请求合并,我们至少对应着两个接口,一个是接收单个请求的接口,一个处理把单个请求汇总之后的请求接口。


所以我们需要先提供两个 service:



其中根据指定 id 查询的接口,对应的 Controller 是这样的:



服务启动起来后,我们用线程池结合 CountDownLatch 模拟 20 个并发请求:



从控制台可以看到,瞬间接受到了 20 个请求,执行了 20 次查询 sql:



很明显,这个时候我们就可以做请求合并。每收到 10 次请求,合并为一次处理,结合 Hystrix 代码就是这样的,为了代码的简洁性,我采用的是注解方式:



在上面的图片中,有两个方法,一个是 getUserId,直接返回的是null,因为这个方法体不重要,根本就不会执行。


在 @HystrixCollapser 里面可以看到有一个 batchMethod 的属性,其值是 getUserBatchById。


也就是说这个方法对应的批量处理方法就是 getUserBatchById。当我们请求 getUserById 方法的时候,Hystrix 会通过一定的逻辑,帮我们转发到 getUserBatchById 上。


所以我们调用的还是 getUserById 方法:



同样,我们用线程池结合 CountDownLatch 模拟 20 个并发请求,只是变换了请求地址:



调用之后,神奇的事情就出现了,我们看看日志:



同样是接受到了 20 个请求,但是每 10 个一批,只执行了两个sql语句。


从 20 个 sql 到 2 个 sql,这就是请求合并的威力。请求合并的处理速度甚至比单个处理还快,这也是性能的提升。


那假设我们只有 5 个请求过来,不满足 10 个这个条件呢?


别忘了,我们还有定时任务呢。


在 Hystrix 中,定时任务默认是每 10ms 执行一次:


同时我们可以看到,如果不设置 maxRequestsInBatch,那么默认是 Integer.MAX_VALUE。


也就是说,在 Hystrix 中做请求合并,它更加侧重的是时间方面。


功能演示,其实就这么简单,代码量也不多,有兴趣的朋友可以直接搭个 Demo 跑跑看。看看 Hystrix 的源码。


我这里只是给大家指几个关键点吧。


第一个肯定是我们需要找到方法入口。


你想,我们的 getUserById 方法的方法体里面直接是 return null,也就是说这个方法体是什么根本就不重要,因为不会去执行方法体中的代码。它只需要拦截到方法入参,并缓存起来,然后转发到批量方法中去即可。


然后方法体上面有一个 @HystrixCollapser 注解。


那么其对应的实现方式你能想到什么?


肯定是 AOP 了嘛。


所以,我们拿着这个注解的全路径,进行搜索,啪的一下,很快啊,就能找到方法的入口:



com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect#methodsAnnotatedWithHystrixCommand




在入口处打上断点,就可以开始调试了:



第二个我们看看定时任务是在哪儿进行注册的。


这个就很好找了。我们已经知道默认参数是 10ms 了,只需要顺着链路看一下,哪里的代码调用了其对应的 get 方法即可:



同时,我们可以看到,其定时功能是基于 scheduleAtFixedRate 实现的。


第三个我们看看是怎么控制超过指定数量后,就不等待定时任务执行,而是直接发起汇总操作的:



可以看到,在com.netflix.hystrix.collapser.RequestBatch#offer方法中,当 argumentMap 的 size 大于我们指定的 maxBatchSize 的时候返回了 null。


如果,返回为 null ,那么说明已经不能接受请求了,需要立即处理,代码里面的注释也说的很清楚了:



以上就是三个关键的地方,Hystrix 的源码读起来,需要下点功夫,大家自己研究的时候需要做好心理准备。


最后再贴一个官方的请求合并工作流程图:



再说请求分治


还是回到最开始我们提出的热点账户问题中的“减余额频繁的热点账户”。


请求分治和请求合并,就是针对“热点账户”这同一个问题的完全不同方向的两个回答。


分治,它的思想是拆分。


再说拆分之前,我们先聊一个更熟悉的东西:AtomicLong。


AtomicLong,这玩意是可以实现原子性的增减操作,但是当竞争非常大的时候,被操作的“值”就是一个热点数据,所有线程都要去对其进行争抢,导致并发修改时冲突很大。


那么 AtomicLong 是靠什么解决这个冲突的呢?


看一下它的 getAndAdd 方法:



可以看到这里面还是有一个 do-while 的循环:



里面调用了 compareAndSwapLong 方法。


do-while,就是自旋。


compareAndSwapLong,就是 CAS。


所以 AtomicLong 靠的是自旋 CAS 来解决竞争大的时候的这个冲突。


你看这个场景,是不是和我们开篇提到的热点账户有点类似?


热点账户,在并发大的时候我们可以对账户进行加锁操作,让其他请求进行排队。


而它这里用的是 CAS,一种乐观锁的机制。


但是也还是要排队,不够优雅。


什么是优雅的?


LongAdder 是优雅的。


有点小伙伴就有点疑问了:歪师傅,不是要讲热点账户吗,怎么扯到 LongAdder 上了呢?


闭嘴,往下看就行了。



首先,我们先看一下官网上的介绍:



上面的截图一共两段话,是对 LongAdder 的简介,我给大家翻译并解读一下。



首先第一段:当有多线程竞争的情况下,有个叫做变量集合(set of variables)的东西会动态的增加,以减少竞争。


sum 方法返回的是某个时刻的这些变量的总和。


所以,我们知道了它的返回值,不论是 sum 方法还是 longValue 方法,都是那个时刻的,不是一个准确的值。


意思就是你拿到这个值的那一刻,这个值其实已经变了。


这点是非常重要的,为什么会是这样呢?


我们对比一下 AtomicLong 和 LongAdder 的自增方法就可以知道了:



AtomicLong 的自增是有返回值的,就是一个这次调用之后的准确的值,这是一个原子性的操作。


LongAdder 的自增是没有返回值的,你要获取当前值的时候,只能调用 sum 方法。


你想这个操作:先自增,再获取值,这就不是原子操作了。


所以,当多线程并发调用的时候,sum 方法返回的值必定不是一个准确的值。除非你加锁。


该方法上的说明也是这样的:



至于为什么不能返回一个准确的值,这就是和它的设计相关了,这点放在后面去说。



然后第二段:当在多线程的情况下对一个共享数据进行更新(写)操作,比如实现一些统计信息类的需求,LongAdder 的表现比它的老大哥 AtomicLong 表现的更好。在并发不高的时候,两个类都差不多。但是高并发时 LongAdder 的吞吐量明显高一点,它也占用更多的空间。这是一种空间换时间的思想。


这段话其实是接着第一段话在进行描述的。


因为它在多线程并发情况下,没有一个准确的返回值,所以当你需要根据返回值去搞事情的时候,你就要仔细思考思考,这个返回值你是要精准的,还是大概的统计类的数据就行。


比如说,如果你是用来做序号生成器,所以你需要一个准确的返回值,那么还是用 AtomicLong 更加合适。


如果你是用来做计数器,这种写多读少的场景。比如接口访问次数的统计类需求,不需要时时刻刻的返回一个准确的值,那就上 LongAdder 吧。


总之,AtomicLong 是可以保证每次都有准确值,而 LongAdder 是可以保证最终数据是准确的。高并发的场景下 LongAdder 的写性能比 AtomicLong 高。


接下来探讨三个问题:



  • LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?

  • 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?

  • 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


先带你上个图片,看不懂没有关系,先有个大概的印象:



接下来我们就去探索源码,源码之下无秘密。


从源码我们可以看到 add 方法是关键:



里面有 cells 、base 这样的变量,所以在解释 add 方法之前,我们先看一下 这几个成员变量。


这几个变量是 Striped64 里面的。


LongAdder 是 Striped64 的子类:



其中的四个变量如下:




  • NCPU:cpu 的个数,用来决定 cells 数组的大小。

  • cells:一个数组,当不为 null 的时候大小是 2 的次幂。里面放的是 cell 对象。

  • base : 基数值,当没有竞争的时候直接把值累加到 base 里面。还有一个作用就是在 cells 初始化时,由于 cells 只能初始化一次,所以其他竞争初始化操作失败线程会把值累加到 base 里面。

  • cellsBusy:当 cells 在扩容或者初始化的时候的锁标识。


之前,文档里面说的 set of variables 就是这里的 cells。



好了,我们再回到 add 方法里面:



cells 没有被初始化过,说明是第一次调用或者竞争不大,导致 CAS 操作每次都是成功的。


casBase 方法就是进行 CAS 操作。


当由于竞争激烈导致 casBase 方法返回了 false 后,进入 if 分支判断。


这个 if 分子判断有 4 个条件,做了 3 种情况的判断




  • 标号为 ① 的地方是再次判断 cells 数组是否为 null 或者 size 为 0 。as 就是 cells 数组。

  • 标号为 ② 的地方是判断当前线程对 cells 数组大小取模后的值,在 cells 数组里面是否能取到 cell 对象。

  • 标号为 ③ 的地方是对取到的 cell 对象进行 CAS 操作是否能成功。


这三个操作的含义为:当 cells 数组里面有东西,并且通过 getProbe() & m算出来的值,在 cells 数组里面能取到东西(cell)时,就再次对取到的 cell 对象进行 CAS 操作。


如果不满足上面的条件,则进入 longAccumulate 函数。


这个方法主要是对 cells 数组进行操作,你想一个数组它可以有三个状态:未初始化、初始化中、已初始化,所以下面就是对这三种状态的分别处理:




  • 标号为 ① 的地方是 cells 已经初始化过了,那么这个里面可以进行在 cell 里面累加的操作,或者扩容的操作。

  • 标号为 ② 的地方是 cells 没有初始化,也还没有被加锁,那就对 cellsBusy 标识进行 CAS 操作,尝试加锁。加锁成功了就可以在这里面进行一些初始化的事情。

  • 标号为 ③ 的地方是 cells 正在进行初始化,这个时候就在 base 基数上进行 CAS 的累加操作。


上面三步是在一个死循环里面的。


所以如果 cells 还没有进行初始化,由于有锁的标志位,所以就算并发非常大的时候一定只有一个线程去做初始化 cells 的操作,然后对 cells 进行初始化或者扩容的时候,其他线程的值就在 base 上进行累加操作。


上面就是 sum 方法的工作过程。


感受到了吗,其实这就是一个分段操作的思想,不知道你有没有想到 ConcurrentHashMap,也不奇怪,毕竟这两个东西都是 Doug Lea 写的。


总的来说,就是当没有冲突的时候 LongAdder 表现的和 AtomicLong 一样。当有冲突的时候,才是 LongAdder 表现的时候,然后我们再回去看这个图,就能明白怎么回事了:



好了,现在我们回到前面提出的三个问题:



  • LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?

  • 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?

  • 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


它们其实是一个问题。


因为 LongAdder 把热点 value 拆分了,放到了各个 cell 里面去操作。这样就相当于把冲突分散到了 cell 里面。所以解决了并发修改冲突很大这个问题。


当发生冲突时 sum= base+cells。高并发的情况下当你获取 sum 的时候,cells 极有可能正在被其他的线程改变。一个在高并发场景下实时变化的值,你要它怎么给你个准确值?


当然,你也可以通过加锁操作拿到当前的一个准确值,但是这种场景你还用啥 LongAdder,是 AtomicLong 不香了吗?


为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


你发动你的小脑壳想一想,朋友。


AtomicLong 不管有没有冲突,它写的都是一个共享的 value,有冲突的时候它就在自旋。


LongAdder 没有冲突的时候表现的和 AtomicLong 一样,有冲突的时候就把冲突分散到各个 cell 里面了,冲突分散了,写的当然更快了。


我强调一次:有冲突的时候就把冲突分散到各个 cell 里面了,冲突分散了,写的当然更快了。


你注意这句话里面的“各个 cell”。


这是什么?


这个东西本质上就是 sum 值的一部分。


如果用热点账户去套的话,那么“各个 cell”就是热点账户下的影子账户。


热点账户说到底还是一个单点问题,那么对于单点问题,我们用微服务的思想去解决的话是什么方案?


就是拆分。


假设这个热点账户上有 100w,我设立 10 个影子账户,每个账户 10w ,那么是不是我们的流量就分散了?


从一个账户变成了 10 个账户,压力也就进行了分摊。


但是同时带来的问题也很明显。


比如,获取账户余额的时候需要把所有的影子账户进行汇总操作。但是每个影子账户上的余额是时刻在变化的,所以我们不能保证余额是一个实时准确的值。


但是相比于下单的量来说,大部分商户并不关心“账上的实时余额”这个点。


他只关心上日余额是准确的,每日对账都能对上就行了。


这就是分治。


其实我浅显的觉得分布式、高并发都是基于分治,或者拆分思想的。


本文的 LongAdder 就不说了。


微服务化、分库分表、读写分离......这些东西都是在分治,在拆分,把集中的压力分散开来。


这就算是我对于“请求合并和分治”的理解、


好了,到这里本文就算是结束了。


针对"热点账户"这同一个问题,细化了问题方向,定义了加余额频繁和减余额频繁的两种热点账户,然后给出了两个完全不同方向的回答。


这个时候,我就可以把文章开头的那句话拿出来说了:


综上,我认为合并和分治,这二者之间是辩证的关系,得用辩证的眼光看问题,它们是你中有我,我中有你~



作者:why技术
来源:juejin.cn/post/7292955463954563072
收起阅读 »

工作两年,本地git分支达到了惊人的361个,该怎么快速清理呢?

说在前面 不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。...
继续阅读 »

说在前面



不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。而我平时本地分支都不怎么清理,这就导致了我这两年来本地分支的数量达到了惊人的361个,所以便开始写了这个可以批量删除分支的命令行工具。



1697987090644.jpg


功能设计


我们希望可以通过命令行命令的方式来进行交互,快速获取本地分支列表及各分支的最后提交时间和合并状态,在控制台选择我们想要删除的分支。


功能实现


1、命令行交互获取相关参数


这里我们使用@jyeontu/j-inquirer模块来完成命令行交互功能,@jyeontu/j-inquirer模块除了支持inquirer模块的所有交互类型,还扩展了文件选择器文件夹选择器多级选择器交互类型,具体介绍可以查看文档:http://www.npmjs.com/package/@jy…


(1)获取操作分支类型


我们的分支分为本地分支和远程分支,这里我们可以选择我们需要操作的分支类型,选择列表为:"本地分支"、"远程分支"、"本地+远程"


(2)获取远程仓库名(remote)


我们可以输入自己git的远程仓库名,默认为origin


(3)获取生产分支名


我们需要判断各分支是否已经合并到生产分支,所以需要输入自己项目的生产分支名,默认为develop


相关代码


const branchListOptions = [
{
type: "list",
message: "请选择要操作的分支来源:",
name: "branchType",
choices: ["本地分支", "远程分支", "本地+远程"],
},
{
type: "input",
message: "请输入远程仓库名(默认为origin):",
name: "gitRemote",
default: "origin",
},
{
type: "input",
message: "请输入生产分支名(默认为develop):",
name: "devBranch",
default: "develop",
},
];
const res = await doInquirer(branchListOptions);

image.png


2、命令行输出进度条


在分支过多的时候,获取分支信息的时间也会较长,所以我们需要在控制台中打印相关进度,避免用户以为控制台卡死了,如下图:


image.png


image.png


3、git操作


(1)获取git本地分支列表


想要获取当前仓库的所有的本地分支,我们可以使用git branch命令来获取:


function getLocalBranchList() {
const command = "git branch";
const currentBranch = getCurrentBranch();
let branch = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branch = branch
.split("、")
.filter(
(item) => item !== "" && !item.includes("->") && item !== currentBranch
);
return branch;
}

(2)获取远程仓库分支列表


想要获取当前仓库的所有的远程分支,我们可以使用git ls-remote --heads origin命令来获取,git ls-remote --heads origin 命令将显示远程仓库 origin 中所有分支的引用信息。其中,每一行显示一个引用,包括提交哈希值和引用的全名(格式为 refs/heads/<branch_name>)。


示例输出可能如下所示:


Copy Code
<commit_hash> refs/heads/master
<commit_hash> refs/heads/develop
<commit_hash> refs/heads/feature/xyz

其中,<commit_hash> 是每个分支最新提交的哈希值。


function getRemoteList(gitRemote) {
const command = `git ls-remote --heads ${gitRemote}`;
let branchList = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branchList = branchList
.split("、")
.filter((item) => item.includes("refs/heads/"))
.map((branch) => {
return gitRemote + "/" + branch.split("refs/heads/")[1];
});
return branchList;
}

(3)获取各分支详细信息


我们想要在每个分支后面显示该分支最后提交时间和是否已经合并到生产分支,这两个信息可以作为我们判断该分支是否要删除的一个参考。



  • 获取分支最后提交时间
    git show -s --format=%ci <branchName> 命令用于查看 指定 分支最新提交的提交时间。其中,--format=%ci 用于指定输出格式为提交时间。


在 Git 中,git show 命令用于显示某次提交的详细信息,包括作者、提交时间、修改内容等。通过使用 -s 参数,我们只显示提交摘要信息,而不显示修改内容。


git show -s --format=%ci develop 命令将显示 develop 分支最新提交的提交时间。输出格式为 ISO 8601 标准的时间戳,例如 2023-10-22 16:41:47 +0800


function getBranchLastCommitTime(branchName) {
try {
const command = `git show -s --format=%ci ${branchName}`;
const result = child_process.execSync(command).toString();
const date = result.split(" ");
return date[0] + " " + date[1];
} catch (err) {
return "未获取到时间";
}
}


  • 判断分支是否合并到生产分支
    git branch --contains <branchName> 命令用于查找包含指定分支(<branchName>)的所有分支。


在 Git 中,git branch 命令用于管理分支。通过使用 --contains 参数,我们可以查找包含指定提交或分支的所有分支。


git branch --contains <branchName> 命令将列出包含 <branchName> 的所有分支。输出结果将显示每个分支的名称以及指定分支是否为当前分支。


示例输出可能如下所示:


Copy Code
develop
* feature/xyz
bugfix/123

其中,* 标记表示当前所在的分支,我们只需要判断输出的分支中是否存在生产分支即可:


function isMergedCheck(branch) {
try {
const command = `git branch --contains ${branch}`;
const result = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
const mergedList = result.split("、");
return mergedList.includes(gitInfoObj.devBranch)
? `已合并到${gitInfoObj.devBranch}`
: "";
} catch (err) {
return "未获取到合并状态";
}
}

(4)删除选中分支


选完分支后我们就该来删除分支了,删除分支的命令大家应该就比较熟悉了吧



  • git branch -D <branchName>


git branch -D <branchName> 命令用于强制删除指定的分支(<branchName>)。该命令会删除本地仓库中的指定分支,无法恢复已删除的分支。



  • git push <remote> :<branchName>


git push <remote> :<branchName> 命令用于删除远程仓库<remote>中的指定分支(<branchName>)。这个命令通过推送一个空分支到远程仓库的 <branchName> 分支来实现删除操作。


async function doDeleteBranch(branchList) {
const deleteBranchList = await getDeleteBranch(branchList);
if (!deleteBranchList) return;
console.log("正在删除分支");
progressBar.run(0);
deleteBranchList.forEach((branch, index) => {
let command = `git branch -D ${branch}`;
if (branch.includes("/")) {
const tmp = branch.split("/");
command = `git push ${tmp[0]} :${tmp[1]}`;
}
child_process.execSync(command);
progressBar.run(Math.floor(((index + 1) / deleteBranchList.length) * 100));
});
console.log("");
console.log("已删除分支:" + deleteBranchList);
}

image.png


1697995985140.png


1697996057503.jpg


可以看到我们的分支瞬间就清爽了很多。


使用


该工具已经发布到 npm 上,可以直接通过命令npm i -g jyeontu进行安装,安装完后在控制台中输入jyeontu git即可进行操作。


源码


该工具的源码也已经开源,有兴趣的同学可以到Gitee上查看:Gitee地址


说在后面



🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。



作者:JYeontu
来源:juejin.cn/post/7292635075304964123
收起阅读 »

去寺庙做义工,有益身心健康

《乔布斯传》中写到:乔布斯把对事物专注的能力和对简洁的热爱归功于他的禅修。他说:“禅修磨炼了他对直觉的欣赏能力,教他如何过滤掉任何能分散时间和精力的其它不必要的事情,在他的身上培养出了专注基于至简主义的审美观。” 如何在当今物欲横流的浮躁社会里不沦陷其中?如何...
继续阅读 »

《乔布斯传》中写到:乔布斯把对事物专注的能力和对简洁的热爱归功于他的禅修。他说:“禅修磨炼了他对直觉的欣赏能力,教他如何过滤掉任何能分散时间和精力的其它不必要的事情,在他的身上培养出了专注基于至简主义的审美观。”


如何在当今物欲横流的浮躁社会里不沦陷其中?如何在每天奔波忙碌之后却不内心疲惫、焦虑?如何在巨大的工作与生活压力下保持一颗平和的心?如何在经历感情、友情和亲情的起起落落后看破放下?如何改变透支健康和生命的人生模式?


程序员无疑是一个高压的职业,尤其是在头部公司工作的程序员们,工作压力更是大。并且在互联网行业,禅修并不是一件新鲜事。我们不一定要正儿八经地参加禅修活动,只是去寺庙走一走,呼吸一下新鲜空气,给寺庙干点活,对身心健康的帮助也会很大。


我与寺庙


我最早接触寺庙是在2011年上军校的时候,我的一个老师,作为大校,经常在课上分享他周末在南京附近寺庙的奇闻轶事,也会分享他自己的一些人生体验和感悟,勾起了我对寺庙生活的向往。


2013年,作为现役军人,我跑到了江西庐山的东林寺做了一个礼拜的义工,在那里,每天早上四点起床早上寺庙早课,负责三餐的行堂,也作为机动义工,干一些杂活,比如卸菜、组装床等,晚上有时也可以听寺庙的传统文化课。


2013年底,我申请退出现役,于是14年春就可以休假了,根据流程不定期去各部门办理手续即可,期间一个周末,我弟带我去凤凰岭玩,偶遇一个皈依法会,为了能看到传说中的北大数学天才,我填了一个义工表,参加了皈依仪式。


因为没有考虑政府安排的工作,所以打算考个研,期间不时会去凤凰岭的寺庙参加活动。考完研后,到18年春季,周末节假日,基本都会去这个寺庙做义工,累计得有200天以上。


期间,作为骨干义工,参与了该寺庙组织的第二至第四届的IT禅修营,负责行堂、住宿和辅导员等相关的工作。


很多人都听过这样一件往事:2010年,张小龙(微信之父)偶然入住一个寺院,当时正是微信研发的关键时刻,因为几个技术难题,张小龙连续几天彻夜难眠,终于一气之下把资料撕得粉碎。


没想到负责打扫卫生的僧人看到后,竟然帮他把资料重新粘贴了起来,还顺手写下了几条建议。张小龙非常惊讶,打听过后才知道这位扫地僧出家前曾混迹IT界,是个著名的极客。


经扫地僧点化,张小龙回到广州苦攻一年后,微信终于大成。这件事传的很广也很玄乎,可信度不会太高,不过故事中张小龙入住的寺院,就是我常去的寺庙。


至于在故事中懂得IT的扫地僧,在这里遇到其实不是什么奇怪的事,你还有可能遇到第47届国际数学奥赛金牌得主贤宇法师,他来自北大数学系;或者是禅兴法师,他是清华大学流体力学博士;又或者贤启法师,他是清华大学核能和热能物理博士。


“扫地只不过是我的表面工作,我真正的职业是一位研究僧。” 《少林足球》这句台词的背后,隐藏着关于这个寺庙“高知僧团”的一个段子。


因为各种不可描述的原因,18年9月之后,我就很少去这个寺庙了,但我知道我依然很向往寺庙的生活。于是22年春,我下定决心离开北京去深圳,其中就有考虑到深圳后,可以去弘法寺或弘源寺做义工。


去了一次弘法寺,感觉那边人太多,后面去了一次弘源寺后,感觉这里比较适合我,人少很安静,不堵车的话,开车只需20分钟到家。


目前,只要我有时间,我都会去弘源寺干一天临时义工,或者住上几天。


何为禅?


禅,是心智的高度成熟状态。直至印度词汇传入,汉语音译为“禅那”,后世简称为“禅”,汉译意思有“静虑”、“思维修”等。


禅修的方法就是禅法,禅法是心法,并不固着于某种具体形式,也不限于宗教派别。从泛义来说,任何一种方法,如果能够让你的心灵成熟,了解生命的本质,让心获得更高层次的证悟,并从而获得生命究竟意义的了悟。这样的方法就是禅修!


从狭义来说,在绵延传承数千年的漫长时空里,形成各种系统的修行方法,存在于各种教派中。现存主要有传承并盛行于南传佛教国家的原始佛教禅法与传承并盛行于中国汉传佛教的祖师禅。


如来禅是佛陀的原始教法,注重基础练习,强调修行止观。祖师禅是中国禅宗祖师的教法,强调悟性、觉性,推崇顿悟,以参话头为代表,以开悟、明心见性为目的。


我们普遍缺乏自我觉察,甚至误解了包括自由在内的生命状态真义。禅修中,会进入深刻自我觉察中,有机会与自己整合,从而开启真我。


近年来,禅修在西方非常流行,像美国的学校、医疗机构和高科技公司都广泛地在进行打坐、禅修。美国有些科学家曾做过一个实验,实验对象是长期禅修的修行人。在实验室中,实验者一边用脑电波图测量脑波的变化,一边用功能性核磁共振测量脑部活动的位置。


最后得出结论:通过禅修,不但能够短期改变脑部的活动,而且非常有可能促成脑部永久的变化。这就是说:通过禅定,可以有效断除人的焦虑、哀伤等很多负面情绪,创造出心灵的幸福感,甚至可以重塑大脑结构。


禅修能够修复心智、疗愈抑郁、提升智慧,让我们重获身心的全面健康!禅修让人的内心变得安静。在禅修时,人能放松下来,专注于呼吸,使内心归于平静,身体和心灵才有了真正的对话与接触。


“禅修是未来科技世界中的生存必需品”时代杂志曾在封面报道中这样写道。在硅谷,禅修被认为是新的咖啡因,一种能释放能量与创造力的全新“燃料”。


禅修也帮助过谷歌、Facebook、Twitter高管们走出困惑,国内比较知名则有搜狐的张朝阳和阿里的马云,还有微信之父张小龙的传说。


对于他们来说,商海的起伏伴随着心海的沉浮,庞大的财富、名声与地位带来的更多的不是快乐,但是禅修,却在一定程度上给他们指点迷津,带领他们脱离现代社会的痛苦、让内心更加平静!


乔布斯的禅修故事


乔布斯和禅修,一直有着很深的渊源。乔布斯是当世最伟大的企业家之一,同时也是一名虔诚的禅宗教徒。他少有慧根,17岁那年,他远赴印度寻找圣人寻求精神启蒙,18岁那年,他开始追随日本禅师乙川弘文学习曹洞宗的禅法。


年轻的时候,乔布斯去印度,在印度体验,呆了七个月。乔布斯在印度干了些什么,我们不得而知。不过据我推测,也就是四处逛一逛,看一看,可能会去一些寺庙,拜访一些僧人。


我从来不认为,他遇到了什么高人,或者在印度的小村庄一待,精神就受到了莫大的洗礼。变化永远都是从内在发生的,外在的不过是缘分,是过客,负责提供一个合适的环境,或者提供一些必要的刺激。


但我们知道,从此以后,乔布斯的人生,就开始变得不一样了。乔布斯的人生追求是“改变世界”,当年他劝说百事可乐总裁,来担任苹果CEO的时候所说的话:“你是愿意一辈子卖糖水,还是跟我一起改变这个世界?”激励了无数心怀梦想的朋友。


早在1973年乔布斯已经对禅有较深的领悟了。他这样说:“我对那些超越物质的形而上的学说极感兴趣,也开始注意到比知觉及意识更高的层次——直觉和顿悟。”


他还说:“因为时间有限,不要带着面具为别人而活,不要让别人的意见左右自己内心的想法,最重要的是要勇敢地忠于自己内心的直觉。”


乔布斯说:“你不能预先把点点滴滴串在一起;唯有未来回顾时,你才会明白那些点点滴滴是如何串在一起的。所以你得相信,你现在所体会的东西,将来多少会连接在一块。你得信任某个东西,直觉也好,命运也好,生命也好,或者业力。这种作法从来没让我失望,也让我的人生整个不同起来。”


他大学时学的书法,被他用来设计能够印刷出漂亮字体的计算机,尽管他在大学选修书法课时,完全不知道学这玩意能有什么用。他被自己创立的苹果公司开除,于是转行去做动画,结果在做动画的时候,遇到了自己未来的妻子。


人呐实在不知道,自己可不可以预料。你说我一个被自己创立的公司开除的失业狗,怎么就在第二份工作里遇到了一生的挚爱呢?若干年后,他回顾起自己的人生,他把这些点点滴滴串了起来,他发现,他所经历过的每一件事,都有着特殊的意义。


所以,无论面对怎样的困境,我们都不必悲观绝望,因为在剧本结束之前,你永远不知道,自己现在面对的这件事,到底是坏事还是好事。


所谓创新就是无中生有,包括思想、产品、艺术等,重大的创新我们称为颠覆。通过那则著名的广告《think different》他告诉世人:“因为只有那些疯狂到以為自己能够改变世界的人,才能真正地改变世界。”乔布斯确实改变了世界,而且不是一次,至少五次颠覆了这个世界:



  • 通过苹果电脑Apple-I,开启了个人电脑时代;

  • 通过皮克斯电脑动画公司,颠覆了整个动漫产业;

  • 通过iPod,颠覆了整个音乐产业;

  • 通过iPhone,颠覆了整个通讯产业;

  • 通过iPad,重新定义并颠覆了平板PC行业。


程序员与禅修


编程是一门需要高度专注和创造力的艺术,它要求程序员们在面对复杂问题和压力时,能够保持内心的安宁和平静。在这个快节奏、竞争激烈的行业中,如何修炼内心的禅意境界,成为程序员们更好地发挥潜力的关键。


在编程的世界里,专注是至关重要的品质。通过培养内在专注力,程序员能够集中精力去解决问题,避免被外界的干扰所困扰。以下是几种培养内在专注的方法:



  • 冥想和呼吸练习:  通过冥想和深呼吸来调整身心状态,让自己平静下来。坚持每天进行一段时间的冥想练习,可以提高专注力和注意力的稳定性。

  • 时间管理:  制定合理的工作计划和时间表,将任务分解为小的可管理的部分,避免心理上的压力。通过专注于每个小任务,逐步完成整个项目。

  • 限制干扰:  将手机静音、关闭社交媒体和聊天工具等干扰源,创造一个安静的工作环境。使用专注工作法(如番茄钟),集中精力在一项任务上,直到完成。


编程过程中会遇到各种问题和挑战,有时甚至会感到沮丧和失望。然而,保持平和的心态是非常重要的,它可以帮助程序员更好地应对压力和困难。以下是一些培养平和心态的技巧:



  • 接受不完美性:  程序永远不会是完美的,因为它们总是在不断发展和改进中。接受这一事实,并学会从错误中汲取教训。不要过于苛求自己,给自己一些宽容和理解。

  • 积极思考:  关注积极的方面,让自己的思维更加积极向上。遇到问题时,寻找解决方案而非抱怨。积极的心态能够帮助你更好地应对挑战和困难。

  • 放松和休息:  给自己合理的休息时间,让大脑得到充分的放松和恢复。休息和娱乐能够帮助你调整心态,保持平和的状态。


编程往往是一个团队合作的过程,与他人合作的能力对于一个程序员来说至关重要。以下是一些建立团队合作意识和促进内心安宁的方法:



  • 沟通与分享:  与团队成员保持良好的沟通,分享想法和问题。倾听他人的观点和建议,尊重不同的意见。积极参与和贡献团队,建立合作关系。

  • 友善和尊重:  培养友好、尊重和包容的态度。尊重他人的工作和努力,给予鼓励和支持。与团队成员建立良好的关系,创造和谐的工作环境。

  • 共享成功:  当团队取得成功时,与他人一起分享喜悦和成就感。相信团队的力量,相信集体的智慧和努力。


修炼内心安宁需要时间和长期的自我管理。通过培养专注力、平和心态、创造力和团队合作意识,程序员们可以在面对复杂的编程任务和挑战时保持内心的安宁和平静。


禅修有许多不同的境界,其中最典型的可能包括:



  • 懵懂:刚开始禅修时,可能会觉得茫然和困惑,不知该如何开始。

  • 困扰:在进行深度内省和冥想时,可能会遇到很多烦恼和难题,需耐心思考和解决。

  • 安和:通过不断地练习和开放自己的心灵,可能会进入一种更加平和和沉静的状态。

  • 祥和:当一些心理障碍得到解决,你会感受到一种更深层的平静和和谐。

  • 转化:通过不断的冥想与内省,你可以向内看到自己的内心,获得对自己和世界的新的认识和多样的观察角度。

  • 整体意识:通过冥想,您将能够超越个人的视野和言语本身,深入探究宇宙的内心,领悟更加深入和广泛的境界和意识。


程序员写代码的境界:



  • 懵懂:刚熟悉编程语言,不知做什么。

  • 困扰:可以实现需求,但仍然会被需求所困,需要耐心思考和解决。

  • 安和:通过不断练习已经可以轻易实现需求,更加平和沉静。

  • 祥和:全栈。

  • 转化:做自己的产品。

  • 整体意识:有自己的公司。


一个创业设想


打开小红书,与“疗愈”相关的笔记高达236万篇,禅修、瑜伽、颂钵等新兴疗愈方法层出不穷,无论是性价比还是高消费,总有一种疗愈方法适合你。


比起去网红景点打卡拍照卷构图卷妆造,越来越多的年轻人正在借助上香、拜神、颂钵、冥想等更为“佛系”的方式去追寻内心的宁静。放空大脑,呼吸之间天地的能量被尽数吸收体内,一切紧张、焦虑都被稀释,现实的残酷和精神的困顿,都在此间找到了出口。


在过去,简单的瑜伽和冥想就能达到这种目的,但伴随着疗愈文化的兴起与壮大,不断在传统方式之上叠加buff,才是新兴疗愈的终极奥义。


从目标人群来看,不同的禅修对应不同的人群。比如临平青龙寺即将在8月开启的禅修,就分为了企业禅修、教育禅修、功能禅修、共修禅、突破禅、网络共修等多种形式。但从禅修内容来看,各个寺庙的安排不尽相同,但基本上跳脱不出早晚功课、上殿过堂、出坡劳作、诵经礼忏、佛学讲座等环节。


艺术疗愈,是截然不同于起参禅悟道这种更亲近自然,还原本真的另一疗愈流派。具体可以细分为戏剧疗愈、绘画疗愈、音乐疗愈等多种形式。当理论逐渐趋向现代化,投入在此间的花费,也成正比增长。


绘画疗愈 ,顾名思义就是通过绘画的方式来表达自己内心的情绪。画幅的大小、用笔的轻重、空间的配置、色彩的使用,都在某种程度上反映着创作者潜意识的情感与冲突。


在绘画过程中,绘画者也同样会获得纾解和满足。也有一些课程会在绘画创作之外,添加绘画作品鉴赏的内容,通过一幅画去窥视作者的内心,寻求心灵上的共鸣,也是舒缓压力的一种渠道。


疗愈市场之所以能够发展,还是因为有越来越多人的负面情绪需要治愈。不论是工作压力还是亲密关系所带来的情绪内耗,总要有一个释放的出口。


当前,我正在尝试依托自营绘馆老师提供优质课件,打造艺培课件分享的平台或社区,做平台前期研发投入比较大,当前融资也比较困难,同时自己也需要疗愈。


所以,最近也在调研市场,评估是否可以依托自营的门店,组织绘画手工+寺庙行禅+技术专题分享的IT艺术禅修营活动,两天含住宿1999元,包括半天寺庙义工体验、半天禅修、半天绘画手工课和半天的技术专题分享。


不知道,这样的活动,大家会考虑参加吗?


总结


出家人抛弃尘世各种欲望出家修行值得尊重,但却不是修行的唯一方法,佛经里著名的维摩洁居士就是在家修行,也取得了非凡成就,六祖惠能就非常鼓励大家在世间修行,他说:“佛法在世间,不离世间觉,离世觅菩提,恰如求兔角”。


普通人的修行是在红尘欲望中的修行,和出家人截然不同,但无分高下,同样可以证悟,工作就是他们最好的修练道场。禅学的理论学习并不困难,但这只是万里长征的第一步,最重要的是,我们要在日常实践中证悟。


简单可能比复杂更难做到:你必须努力理清思路,从而使其变得简单。但最终这是值得的,因为一旦你做到了,便可以创造奇迹。”乔布斯所说的这种专注和简单是直接相关的,如果太复杂,心即散乱,就很难保持专注,只有简单,才能做到专注,只有专注,才能极致。


作者:三一习惯
来源:juejin.cn/post/7292781589477687350
收起阅读 »

成为务实的程序员

编程是一门技艺。简单地说,就是让计算机做你想让它做的事情(或是你的用户想让它做的事情)。作为一名程序员,你既在倾听,又在献策;既是传译,又行独裁;你试图捕获难以捉摸的需求,并找到一种表达它们的方式,以便仅靠一台机器就可以从容应付。你试着把工作记录成文档,以便他...
继续阅读 »

编程是一门技艺。简单地说,就是让计算机做你想让它做的事情(或是你的用户想让它做的事情)。作为一名程序员,你既在倾听,又在献策;既是传译,又行独裁;你试图捕获难以捉摸的需求,并找到一种表达它们的方式,以便仅靠一台机器就可以从容应付。你试着把工作记录成文档,以便他人理解;你试着将工作工程化,这样别人就能在其上有所建树;更重要的是,你试图在项目时钟的滴答声中完成所有这些工作。你每天都在创造小小的奇迹。



什么是“务实”?


务实(Pragmatic)这个词来自拉丁语 pragmaticus ——“精通业务”,该词又来源于希腊语 πραγματικός,意思是“适合使用”。


务实程序员特征



  • 早期的采纳者 / 快速的适配者:对技术和技巧有一种直觉,喜欢尝试。当接触到新东西时,你可以快速地掌握它们,并把它们与其他的知识结合起来。你的信心来自经验。

  • 好奇:倾向于问问题。热衷于收集各种细微的事实,坚信它们会影响自己多年后的决策。

  • 批判性的思考者:你在没有得到证实前很少接受既定事实。当同事们说“因为就该这么做”,或者供应商承诺会解决所有问题时,你会闻到挑战的味道。

  • 现实主义:你试图理解所面临的每个问题的本质。这种现实主义让你对事情有多困难、需要用多长时间有一个很好的感知。一个过程应该很难,或是需要点时间才能完成,对这些的深刻理解,给了你坚持下去的毅力。

  • 多面手:你努力熟悉各种技术和环境,并努力跟上最新的进展。虽然目前的工作可能要求你在某个专门领域成为行家,但你总是能够进入新的领域,迎接新的挑战。


务实的哲学


软件的熵



当软件中的无序化增加时,程序员会说“软件在腐烂”。有些人可能会用更乐观的术语来称呼它,即技术债。



有很多原因导致软件腐烂。最重要的一个似乎是项目工作中的心理状态,或者说文化。无视一个明显损坏的东西,会强化这样一种观念:看来没有什么是能修好的,也没人在乎,一切都命中注定了。所有的负面情绪会在团队成员间蔓延,变成恶性循环。



不要放任破窗,及时发现及时修复,漠视会加速腐烂的过程。



软件开发中应该遵循的方法:不要只是因为一些东西非常着急,就去造成附带伤害。破窗一扇都嫌太多


够好即可的软件



不要为了追求更好而损毁了原有已经够好的。



我们做的东西,从用户需求角度出发是否足够好?最好还是留给用户一个机会,让他们亲自参与评判。无视来自用户方面的需求,一味地向程序中堆砌功能,一次又一次地打磨代码,这是很不专业的表现。心浮气躁当然不值得提倡,比如承诺一个无法兑现的时间尺度,然后为了赶上截止日期删减必要的边角工程,这同样是不专业的做法。



如果早点给用户一点东西玩,他们的反馈常常引领你做出更好的最终方案。



知识组合



知识和经验是你最重要的专业资产。学习新事物的能力是你最重要的战略资产。



关于如何构建自己的知识组合,可以参考以下指导方针:



  • 定期投资:安排一个固定的时间和地点为你的知识组合投资。

  • 多样化:计算机技术变化迅猛——今天的技术热点可能到明天就接近无用(至少不那么受欢迎)。所以,熟悉的技能越多,越能适应变化。

  • 风险管理:分散风险,不要把所有的技术鸡蛋放在一个篮子里。

  • 低买高卖:在一项新技术变得流行之前就开始学习,可能和发现一只被低估的股票一样困难,但是所得到的收获会和此类股票的收益一样好。

  • 重新评估调整:这是一个充满活力的行业。你上个月开始研究的热门技术现在可能已经凉下来了。


对于那些已经构成知识组合的智力资产,获取它们的最佳途径可以参考如下建议:



  1. 每年学习一门新语言:不同语言以不同的方式解决相同的问题。多学习几种不同的解决方法,能帮助自己拓宽思维,避免陷入陈规。

  2. 每月读一本技术书:虽然网络上有大量的短文和偶尔可靠的答案,但深入理解还是需要去读长篇的书。当你掌握了当前正在使用的所有技术后,扩展你的领域,学习一些和你项目不相关的东西

  3. 还要读非技术书:不要忘记方程式中人的那一面,他需要完全不同的技能集。

  4. 上课:在本地大学或网上找一些有趣的课程,或许也能在下一场商业会展或技术会议上找到。

  5. 加入本地的用户组和交流群:不要只是去当听众,要主动参与。独来独往对你的职业生涯是致命的:了解一下公司之外的人都在做什么。

  6. 尝试不同的环境

  7. 与时俱进:关心一下和你当前项目不同的技术,阅读相关的新闻和技术贴。


批判性思维



批判性地思考读到的和听到的东西。



批判性地分析问题,先思考几个问题:



  • 问“五个为什么”:当有了答案后,还要追问至少五个为什么;

  • 谁从中受益:追踪钱的流动更容易理清脉络;

  • 有什么背景:每件事都发生在它自己的背景下,这也是为何“能解决所有问题”的方案通常不存在,而那些兜售“最佳实践”的书或文章实际上经不起推敲。

  • 什么时候在哪里可以工作起来:不要停留在一阶思维下(接下来会发生什么),要进行二阶思考:当它结束后还会发生什么?

  • 为什么是这个问题:是否存在一个基础模型?这个基础模型是怎么工作的?


务实的方法


ETC——优秀设计的精髓



优秀的设计比糟糕的设计更容易变更。



对代码而言,要顺应变化。因此要信奉 ETC(Easier To Change,更容易变更)原则。ETC 是一种价值观念,不是一条规则。关于培养 ETC 观念,有以下几点意见:



  • 不断强化意识,问自己这么做是否让系统更容易变更。

  • 假设不确定什么形式的改变会发生,你也总是可以回到终极的“容易变更”的道路上;试着让你写的东西可替换。

  • 在工程日志中记下你面临的处境:你有哪些选择,以及关于改变的一些猜测。


DRY——邪恶的重复


我们认为,想要可靠地开发软件,或让开发项目更容易理解和维护,唯一的方法就是遵循下面这条DRY(Don't repeat yourself,不要重复自己)原则:



在一个系统中,每一处知识都必须单一、明确、权威地表达。



与之相对的不同做法是在两个或更多地方表达相同的东西。如果变更其中一个,就必须记得变更其他的那些。


DRY 不限于编码,DRY 针对的是你对知识意图的复制。它强调的是,在两个地方表达的东西其实是相同的,只是表达的方式可能完全不同。



  • 代码中的重复

  • 文档中的重复:例如注释和代码表达完全相同的意思,但是未来代码变更可能不会同步注释的变更,或者用数据结构表达知识等。

  • 开发人员间的重复

    • 最难检测到且难处理的重复类型,可能发在同一个项目的不同开发人员之间。整块的动能集可能会在不经意间重复,而这种重复或者好多年都并未发现,最终导致了维护问题。

    • 我们认为解决这个问题最好的方法是鼓励开发人员间积极频繁的交流。

    • 指派团队中的一人作为项目只是管理员,他的工作就是促进知识的传播。在源码目录树中设置一个集中的位置,存放工具程序和脚本程序。

    • 你要努力的方向,应该是孕育出一个更容易找到和复用已有事务的环境而不是自己重新编写




正交性


定义:对于两个或多个事物,其中一个的改变不影响其他任何一个,则这些事物是正交的。


当系统的组件相互之间高度依赖时,就没有局部修理这回事儿。我们希望设计的组件自成一体:独立自主,有单一的清晰定义的意图。但凡编写正交的系统,就能获得两个主要的收益:提高生产力和降低风险


设计


可以用一个简单的方法可以测试设计的正交性。当你规划好组件后,问问自己:



  • 如果一个特别功能背后的需求发生显著改变,有多少模块会影响?

  • 你的设计与现实世界的变化有多大程度的解耦。 不要依赖那些你无法控制的东西。


编码


当你写下代码时,就有降低软件正交性的风险。你不仅需要盯着正在做的事情,还要监控软件的大环境。有几种技术可以用来保持正交性:



  • 保持代码解耦:编写害羞的代码——模块不会向其他模块透露任何不必要的信息,也不依赖于其他模块的实现。

  • 避免全局数据:只要代码引用全局数据,就会将自己绑定到共享该数据的其他组件上。即使只打算对全局数据进行读操作,也可能引发问题(例如突然需要将代码改为多线程的情形)。一般来说,如果总是显式地将任何需要上下文传递给模块,那么代码会更容易理解和维护。

  • 避免相似的数据:可以看看《设计模式》中的策略模式。


测试


修 Bug 也是评估整个系统正交性的好时机。遇到问题时,评估一下修复行为的局部化程度。只要变了一个模块,还是有很多处变更分散在整个系统里?当你修正了一个地方,是不是就修复了所有问题,还是会神秘地出现其他问题?


曳光弹



使用曳光弹找到目标。



对于我们来说,最初的曳光弹就是,创建一个简单的工程,加一行“hello world!”,并确保其能编译和运行。然后,我们再去找整个应用程序不确定的部分,添加上让它们跑起来的骨架。


曳光弹.png


使用曳光弹代码的优势



  • 用户可以更早地获得能工作的东西。

  • 开发者构造了一个可以在其中工作的框架。

  • 你有了一个集成平台。

  • 你有可以演示的东西。

  • 你对进度有更好的感觉。


务实的偏执


务实的程序员会为自己的错误建立防御机制。



  • 客户和供应商必须就权利和责任达成共识。

  • 我们想要确保在找出 Bug 的过程中不会造成破坏。

  • 为你所做的假设编写主动校验的代码。

  • 只要我们总是坚持走小步,就不会才能够悬崖边掉下去。


契约式设计(DBC)


与计算机系统打交道很难,与人打交道更是难上加难。而契约规定了你的权利和责任,同时也规定了对方的权利和责任。文档化及主张进行检验是契约式设计的核心。


在编写代码之前,简单列出输入域的范围、边界条件是什么、例程承诺要交付什么——或更重要的是,没有承诺要交付什么——这对编写更好的软件来说,是一个巨大的飞跃。


尽早崩溃


尽快检测问题的好处之一是,可以尽早崩溃,而崩溃通常是你能做的最好的事。一旦代码发现本来不可能发生的事情已经发生,程序就不再可靠。这一刻开始,它所做的任何事情都是可疑的,所以要尽快终止它。


一个死掉的程序,通常比瘫痪的程序,造成的损害更小。


使用断言编程


无论何时,你发现自己在想“当然这是不可能发生的”时,添加代码来检查这一点,最简单的方式就是使用断言。注意这里的断言检查的是不可能发生的事情,普通的错误处理不要使用断言。


保持资源平衡


大多数情况下,资源使用遵循一个可预测的模式:



  • 分配资源

  • 使用它

  • 然后释放它


不要超出控制范围


把反馈的频率当作速度限制,永远不要进行“太大”的步骤或任务。


当你不得不做下面的事情的时候,你可能陷入了占卜的境地:



  • 估计未来几个月之后的完成日期

  • 为将来的维护或可扩展性预设方案

  • 猜测用户将来的需求

  • 猜测将来有什么技术可用


当你编码时


传统观点认为,一旦项目到了编码阶段,就几乎只剩下一些机械工作:只是把设计翻译成可运行的代码段而已。我们认为这种态度是软件项目失败的最重要原因。编码不是机械工作。务实的程序员会对所有代码进行批判性思考,包括自己的代码。我们不断看到程序和设计的改进空间


听从直觉


倾听自己直觉的方法:




  • 首先,停止正在做的事情。给自己一点时间和空间,让大脑自我组织。远离键盘,停止对代码的思考,做一些暂时不需要动脑筋的事情——散步、吃午饭、和别人聊天,或是先睡一觉。让想法自己从大脑的各个层面渗透出来:对此不用很刻意。最终这些想法可能上升到有意识的水平,这样你就能抓住一个”啊哈“的时刻。




  • 如果这些不起作用,就试着把问题外化。把正在写的代码涂画到纸上,或者向你的同事(最好不是程序员)解释一下怎么回事儿,向橡皮鸭解释下也行。把问题暴露给不同部分的大脑,看看有没有一部分大脑能更好地处理困扰你的问题。




重构



代码需要演化:它不是一个静态的东西。



马丁.弗勒将重构定义为:重组现有代码实体、改变其内部结构而不改变其外部行为的规范式技术。



  • 这项活动是有规范的,不应随意为之。

  • 外部行为不变;现在不是添加功能的时候


何时重构


当你学到一些东西时,当你比去年、昨天甚至是十分钟前更了解某事时,你会重构。


无论问题是多是少,都有可能促使我们对代码进行重构:



  • 重复:当你发现一处违背DRY原则的地方。

  • 非正交设计

  • 过时的知识:事情变化来,需求偏移了,你对问题的了解更多了——代码也需要成长。

  • 使用:当系统在真实的环境中被真实的人使用时,你会意识到,与以前的认识相比,一些特性现在看来更为重要,反而 “必须拥有” 的特性可能并不重要。

  • 性能:你需要将功能从系统的一个区域移动到另一个区域以提高性能。

  • 通过了测试重构应该是一个小规模活动,需要良好的测试支持。如果你添加了少量代码,并且通过了一个额外的测试,现在就有了一个很好的机会,来深入研究并整理刚刚编写的代码。



尽早重构,经常重构。



如何重构


重构的核心是重新设计。你或团队中的其他人设计任何东西的时候,都可以根据新的事实、更深的理解、更改的需求等重新设计。但是,如果你执拗地非要将海量的代码统统撕毁,可能会发现,自己所处的境地,比开始时更加糟糕。


重构是一项需要慢慢地、有意地、仔细地进行的活动。马丁.弗勒提供了一些简单技巧,可以用来确保进行重构不至于弊大于利:



  • 不要试图让重构和添加功能同时进行

  • 在重构开始之前,确保有良好的测试

  • 采取简单而慎重的步骤:将字段从一个类移动到另一个类,拆分方法,重命名变量。重构通常设涉及对许多局部进行的修改,这些局部修改最终会导致更大范围的修改。如果保持小步骤,并在每个步骤之后进行测试,就能避免冗长的调试。


结尾


本文只是分享了书中的一部自己觉得比较好的、可以实际使用的观点,并非书中所有观点),强烈建议大家读一下本书,相信会有不错的收获。最后贴个大佬对本书的评价,我也是因为刷到这个所以才知道此书的。


38e638e5-520d-4505-ad79-73dfc98ac523.png


作者:raymond
来源:juejin.cn/post/7269063053877411896
收起阅读 »

菜鸟前端故事之翅膀硬了

web
2019的故事 最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。 话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。 在过年的时候,我人生...
继续阅读 »

2019的故事


最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。


话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。


在过年的时候,我人生第一次有了上万存款,发了不少红包给家里老人和晚辈,那种回报家人的感觉真的很好。
现在看来9k的薪资不算什么,但算是很快达到了老爸的预期。若不是计算机专业和互联网的崛起,我这破学校不可能刚毕业就有这种薪资,我庆幸生对了时代,做了对的选择。


加薪


小美妆没有年终奖,年末聚餐的时候发了800块红包,我收到很开心只觉得这是一笔意外的收入。当时也不懂什么13薪之类的,更不知道、也不敢想象在互联网还有四五个月的年终奖,觉得没有很正常。即使有听说腾讯给全体发iphone、游戏工作室数十个月年终,也觉得很遥远,那远不是我能触及的层次,又何苦去比较。


包括小美妆的976作息,因为自己的菜,便也觉得不合理的事也是合理。


可喜的是刚过完年不久,小张总就给我们加了工资,1000块,我开心了很久,不是仅仅是因为多了1000块,另一层含义是我的月薪上万了,传说中的月薪过万我终于达到了,我还记得我当时激动的跟我爸妈还有女友分享,爸妈直呼老板良心让我跟着好好干,在他们眼里能主动加薪还加这么多的太难得了,回想起来还是觉得那一刻很幸福。


躺不平


今年我们有个任务就是要把小美妆的网站做成一个小程序,一度令当时的我头疼,因为我只会jquery连vue都不会更别说小程序了。一开始甚至想直接用小程序包裹webview直接套壳做好的网站了,但是理智和直觉告诉我不能那样,可能会挖大坑。


没办法只有埋头苦学,没想到仅仅两天我就可以上手了,并且把网站最难的特效部分转为用小程序实现了,原来小程序也没那么难嘛,心想我还是有点天赋的。


到后来的其他网站我开始尝试用vue去做,发现跟小程序几乎都是一样,也就顺风顺水了。估计这个顺序跟大多数前端都是反着来的,但也总算是会了一些主流现代的技术。
可惜那时候我还是用的引入CDN的方式引入vue,没有尝试构建工具,这也让我在后面的职业经历中吃了瘪。


在那一年我们做了很多小程序,移动端H5,后台管理系统,内部外部用的都有,对移动端开发、兼容有了一定的技术积累,后面也做了支付宝的小程序。


其实大多都是重复的技术,除了熟练度没有什么提高,到年中的时候已经算是进入平稳开发期,没有什么水花,也没有了深入学习的心思,因为大部分需求已经不需要动脑子
就能实现。用现在流行的一个词来说就是:躺平了。


小张总讲情义,只要我认真完成任务他应该不会赶我走,小作坊朋友式的公司氛围舒服,薪水也不错,不禁开始妄想熬成老员工躺平一辈子多好。


我突然意识到,自己好像成了守哥,可这似乎也没错。


幸运的是接下来发现的一件事让我意识到技术必须不断进步,而不幸的是明白这个道理让我们付出了惨重的代价。


昂贵的教训


在我们来之前公司是有一台旧的云服务器,是某财务软件公司给我们部署财务系统用,使用windowsServer系统,里面存放了相当多的财务信息,以及我们的公众号服务端项目、数据库。


在一天早上我们突然接到用户反馈公众号报错服务故障了,起初我们还以为又是夜间突发流量导致宕机了,心想着重启一下就好。可进入服务器一看却傻了眼,各种文件都被清空,变成了加密文件,连数据库也被删得一干二净只留下大约这么一段话:


recovery data connect email xxx


我们才意识到服务器被攻击入侵了,黑客将我们的数据全都加密,并勒索要求支付比特币才能解锁。


我们的财务数据、公众号用户的充值信息全没了。我清晰的记得,那一刻我的心凉到了冰点,因为我们的数据库已近半年没有备份。即使宽宏大量的小张总知道后也有些生气了,是啊,这件事带来的后果太麻烦了,毫无疑问技术部门负全责。


面对小张总的追问,我异常艰难的开口告诉了实情,在写这段的时候我仍然感觉令人窒息。
报警?支付赎金?找专业人士解密?那段时间我忙得团团转,急得像热锅上的蚂蚁,只想能够挽回损失。比特币赎金的价格高达十几万,而且也非常冒险很可能被再次勒索或者解不开,咨询专业人士都说解不了,甚至还有人冒出来说知道谁干的,可以作为中间人担保帮我们付费解锁。


我才明白原来这种事早已经形成了一条完整的黑色产业链,同时也对黑客这个词不再敬畏,而是深恶痛绝。随意的一次攻击可能毁掉一家公司,让上百无辜的人失业。


还记得有个坊间传闻说阿里面试某低学历高手,高手当面破解阿里系统,被破格录取并重用。有人如法炮制面试鹅厂,刚破解完腾讯系统当场报警被抓。以此来传言阿里格局大,腾讯格局小。虽然这个流言多半是假的,但我的态度在经历被勒索之后也变成了支持腾讯的一方,违法的事就是违法,不因场合而改变性质,永远也不应鼓励这种行为。不然谁都去破解一下,总有出大事的一天。


后来经过深思熟虑小张总还是决定不屈服,而是在现在基础上补救、向用户致歉补偿等措施。而我也开始拼命恶补网络安全方面的知识,这种情况要是再出现一次,即使老板不开除我,我自己也没脸待下去了。后来我们将系统换成了linux,把不该打开的端口也都关闭了,设置了强密码,安排了各种云服务商的安全服务,最重要的是做了db的定时备份。从那以后我们没再出过安全上的问题。


这件事给我好好的上了一课,在解决问题的过程中,面对很多技术一抹黑的境况也让我觉得心里没底,同时给了我要不断学习深入研究的决心。


技术态度的转变


自那以后,我意识到了技术是片汪洋大海,而之前的我不过是在小水坑里扑腾。我开始学习很多工作中暂时用不到的技术,或者在工作中尽量使用新的技术,这也是小团队的好处,没有历史包袱想怎么搞就怎么搞。


我开始学了react,学了php做自己厌恶的服务端,学了linux买了自己的服务器做了自己的网站,也用RN开始写App,第一次了解到了算法题的存在并开始练习,我疯狂的想要提升自己,不允许存在任何技术上的短板。我给自己列了长长的一串学习清单,还因为有其他公司看中我们的小程序想嵌入,去跟上市公司谈合作,签合同。那是除了在小外包入职时期以外的另一段高速汲取知识并成长的时光。



如果有前端初学的朋友在看,我想顺便提一下,个人认为想成为一名优秀的软件工程师,服务端是你无法避开的一环,虽然不需要精通,但是也不要一味的躲避。



2019在经历波折后归于平淡,在平淡的年末却又爆发了疫情,平时总是和女友两个城市来回跑,因为疫情居家被关到了一起,她还担心总被封闭在一起两个人会矛盾显现,结果没想到住一起那么快乐,感情越来越好,給枯燥的广漂生活增加了很甜蜜的一段时光。哈哈抱歉秀了一段,那时候真是觉得糟了,我陷入爱河啦。


离开小美妆


没有不散的宴席。


偶尔会听到小张总吐槽我们部门一年成本几十万好肉疼,不管是不是玩笑话,我也理解这种体量的公司养几个全职的程序员是极为奢侈的。


后来的近一年内我的工资逐渐涨到了12k左右,那时候的我对市面上的行情开始有了些了解,我同时兼任产品、ui、前端、项目管理的工作量,这个水平还是偏低的。


再加上当时的我也开始为未来做打算,我想要的东西很多,我也想给我爱的人更多,因为后来我接过了武汉房子的房贷,当时一个月扣除花费后只能存下三千块,照这个水平我猴年马月能过上我想要的生活啊。而小张总的话也侧面反应了我的薪水已经不会再有什么增长空间。说得更严重一点,技术部门甚至已经成为了公司的负担,后面小张总已经打算接外面的项目来做了。


有了爱人,有了技术,随之而来的便是动力与野心。


虽然我理解守哥,但我却不再想成为守哥。


总结下离开的3大原因:



  • 翅膀硬了觉得没有成长空间了

  • 真心希望给公司减负

  • 不想接外包项目
    基于以上的因素,我决定去到深圳,开始面试,我想要进入更强大的公司。


面试吃瘪之旅


第一次打开了招聘软件参与社招(找实习只用了下实习僧),被hr打招呼的热情搞得受宠若惊,后来才知道全是外包.....


很快就又遇到了某软,没错就是那个实习要录用又不留用我的某软,上来就要我学位证,没有社招经验的我还以为都是这样,啥也没干就老老实实的交了资料。


那时候不觉得外包经历是扣分项,得意洋洋的把小外包的实习经历写在了简历上,充了充工作年限,还写了自己兼任各种岗位(现在看来无疑是扣分项),估计hr心想这人真是个二笔。


hr先问了我现在的薪资,听完说他们这边最多给我13k,他们是按经验算薪资,你再牛不好意思你练习时长都还不够两年半,13k封顶了。我还是试着面了一下非常容易就过了,但是没选择去,只是试了试面试的感觉,我还是不傻的,这个涨幅和公司不值得。后来又面了一些公司比如万科、宝能之类的大型企业,基本有一半的通过率,那个阶段真是处于互联网的黄金年代,岗位多经济好。


当时很喜欢万科,可惜面试官问我微服务我根本答不上来,遗憾挂掉。至于bat之流的公司,还是不敢想。后来还是忍不住试着投了下腾讯,居然拿到了面试机会,我望着印着腾讯logo的面试邀请邮件,充满了渴望。至于面试结果当然不必期待,被一番吊打后挂掉,因为差距太大我也就对互联网大厂死了心。不过当时面试的小鹅拼拼部门据说非常累,而且后面整体被裁撤了。


之后通过了宝能的面试,在当时的我眼里可是大公司,公积金顶格交满,还有双休和食堂,虽然把我薪资压到了13k但我还是兴致勃勃准备去,可是offer一直没审批下来,当时搞得我一度很失落。


后来才知道宝能当时处于内部动荡期,已经锁了hc。


涨幅70%


宝能一直没消息我只能继续面,后来有一家大小周跨境电商公司给我了16x13,还有一家双休金融公司给了18x14,果断选择了后者,这个涨幅令当时的我激动不已,在接到oc后差点没跳起来,也算是正式年薪突破二十万了。


而且还给我了高级前端工程师的title,虽然也是在后来的经历中才知道非名企的职级就是个鸡肋,但当时可以说满足至极。意外的是宝能offer卡了其实是一件大好事,也或许是命运的眷顾。


因为过后不久宝能hr说愿意立即给我发offer,但是当时我已经入职金融公司就拒绝了。再后来就是宝能公积金断缴的消息,hr自己也离职了,再到前两天还看到宝能的姚总拖欠薪资被打的新闻,没进去走了狗屎运。


在拿到offer当天我跟小张总提了离职,他很惊讶,挽留了我并且说愿意加薪,但是我知道不可能在小美妆发展了,离开公司对双方都是好事,小张总的挽留可能只是出于客套或者情义。


其他几个小伙伴也很震惊,不理解这么舒服的地方为什么要离开,去外面肯定受拘束还要加班。可我已执意离开。


因为不好说那些肉麻的话,我在微信给小张总发了长长的感谢的文字。离开那月的工资多了几千奖金,我明白那是小张总对我的肯定,离职后的一天我仍然去了公司,把一个新项目的需求框架谈好才走,免得小伙伴们一时没法适应要自己去处理需求的情况。


至此,我正式离开了小美妆,而跟我一起来广州的小伙伴现在2023年仍然在那里就职。
非常感谢小张总的信任和包容,愿意给一个不认识的菜鸟机会,让他去组建团队,管理项目,而我也回报了一个两年流水过亿的项目。


这个项目也成为了我简历上闪耀的一段背书,或许也是之前大厂愿意给我面试机会的原因。


巅峰跃升


写得有点多了,下次再继续写在金融公司呆了不久又离开的故事,以及我职业经历的的再一次巅峰跃升,欢迎各位小伙伴留言交流~


作者:鹅猴
来源:juejin.cn/post/7264383071318671421
收起阅读 »

关于25届二本通过某大厂实习面试后从兴奋到放弃

关于25届二本通过某大厂实习面试后从兴奋到放弃 背景 我是南方的一名25届前端程序员,找的是前端实习生的岗位,标题中的提到的大厂是北方的一家公司,就让我暂时把它和它所在的城市称为A厂和A市吧。(自身原因没法入职,我只是记录一下自己这段时间的心路历程,这家厂子的...
继续阅读 »

关于25届二本通过某大厂实习面试后从兴奋到放弃


背景


我是南方的一名25届前端程序员,找的是前端实习生的岗位,标题中的提到的大厂是北方的一家公司,就让我暂时把它和它所在的城市称为A厂和A市吧。(自身原因没法入职,我只是记录一下自己这段时间的心路历程,这家厂子的面试官真的很好,看到下面大家就知道了)


面试通过后的兴奋


2023年9月8号二面结束后,一面面试官给了我口头oc。我记得当时的我很恍惚,流程很快,从约面到口头oc也就两天,直到那个时候我还是有点不相信一个普通二本的学生能被A厂约面,不相信我能通过面试,毕竟现在的大环境大家或多或少都有亲身体验。然后我添加了未来ld的联系方式,好像一切都在往好的方向发展,一个大厂对于一个在校生的吸引是巨大的。当时的我觉得,我接下来应该就是买票、租房、入职了,我也这么以为。


觉得一切充满希望


我把这个好消息告诉了家里人,意料之中遭到了家里人的反对,我记得当时是:“你才大三,你不要那么急,附近找一个公司先过渡一下,不要一下子就跑那么远”。我理解他们的担心,但当时我的情绪也很上头,最后不欢而散。之后,我开始在网上加上一些在A厂实习的同学,加上A厂的实习同学之后,我跟他们聊了很多,就好像我已经可以成为他们的同事一样,在网上查阅一些资料,问好了租房,公司的制度,我觉得我还年轻,我可以克服很多困难,我觉得我不应该被局限,我想我做好准备了。但是,王子不一定能救出公主,走向未来的桥梁也不一定能筑起。


开始迷茫


理想和现实总是背道而驰,我知道了实习生第一个月的高额消费,还是在A市,对于我这个在校生是一个自己拿不出手的数额,我开始动摇,也是我自己对于这一方面没有做好准备,我开始咨询起之前在那边实习过的一些师兄师姐,跟他们取经,知道了去远的地方实习注定是哪里的挣钱哪里花,有的时候甚至是贴钱实习。我承认那会我有点退却了,想到一个人去一个那么远的地方,自己找房子,自己适应那边的生活,我开始觉得自己好像并不是那么勇敢。咨询了很多朋友,他们有的说:“你还年轻,这点距离不算什么,出去闯闯”。期间我也想过去借钱,去跟家里人商量一下,内心挣扎了很久,但是一直以来我都是那种不到迫不得已绝不会向朋友借钱,也不愿意让家里人去掏这个钱让我去镀金,也可能我当时对一切的未知充满了恐惧。


放弃前夕


d0b91d643a227ba0fec3b845e0419f8.jpg
这是我想清楚拒掉这个offer前一晚所写的,我想我并不是一个勇敢的人。


哭死,感谢面试官


2023年9月12日,一直在等我确认入职时间给我网申offer的"ld"却等来了我决定拒绝offer的消息,我把我的困难和拒绝offer的原因告诉了他,我本来以为这样子事情就已经告一段落了。可是后面他继续联系我,来了解我的难处,说如果我真的想去那边实习,他愿意为我提供帮助,让我安心去那边实习,其他的他来解决。他当时的原话是:“哈哈,没事,就看看你有啥困难,初期的困难是正常的,这个能帮到还是会帮到的”。我真的那会,我哭死,但是因为一些原因还是没能去那边实习,在这里真诚的感谢他。


好啦,翻篇啦,继续加油


作者:oversize的前端男孩
来源:juejin.cn/post/7277828214247768083
收起阅读 »

如何制作 GitHub 个人主页

web
原文链接:http://www.bengreenberg.dev/posts/2023-… 人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源...
继续阅读 »

原文链接:http://www.bengreenberg.dev/posts/2023-…


人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主页可能是人们为了了解你而去的第一个地方。


你希望你的GitHub个人主页说些什么?你希望如何以简明易读的方式向访客表达对你的重要性以及你是谁?无论他们是未来的雇主还是开源项目的潜在合作伙伴,你都必须拥有一个引人注目的个人主页。


使用GitHub Actions,你可以把一个静态的markdown文档变成一个动态的、保持对你最新信息更新的良好体验。那么如何做到这一点呢?


我将向你展示一个例子,告诉你如何在不费吹灰之力的情况下迅速做到这一点。在这个例子中,你将学习如何抓取一个网站并使用这些数据来动态更新你的GitHub个人主页。我们将在Ruby中展示这个例子,但你也可以用JavaScript、TypeScript、Python或其他语言来做。


GitHub个人主页如何运作


你的GitHub个人主页可以通过在网页浏览器中访问github.com/[你的用户名]找到。那么该页面的内容来自哪里?


它存在于你账户中一个特殊的仓库中,名称为你的账户用户名。如果你还没有这个仓库,当你访问github.com/[你的用户名]时,你不会看到任何特殊的内容,所以第一步是确保你已经创建了这个仓库,如果你还没有,就去创建它。


探索仓库中的文件


仓库中唯一需要的文件是README.md文件,它是你的个人主页页面的来源。


./
├── README.md

继续在这个文件中添加一些内容并保存,刷新你的用户名主页,你会看到这些内容反映在那里。


为动态内容添加正确的文件夹


在我们创建代码以使我们的个人主页动态化之前,让我们先添加文件夹结构。


在顶层添加一个名为.github的新文件夹,在.github内部添加两个新的子文件夹:scripts/workflows/


你的文件结构现在应该是这样的:


./
├── .github/
│ ├── scripts/
│ └── workflows/
└── README.md

制作一个动态个人主页


对于这个例子,我们需要做三件事:



  • README中定义一个放置动态内容的地方

  • scripts/中添加一个脚本,用来完成爬取工作

  • workflows/中为GitHub Actions添加一个工作流,按计划运行该脚本


现在让我们逐步实现。


更新README


我们需要在README中增加一个部分,可以用正则来抓取脚本进行修改。它可以是你的具体使用情况所需要的任何内容。在这个例子中,我们将在README中添加一个最近博客文章的部分。


在代码编辑器中打开README.md文件,添加以下内容:


### Recent blog posts

现在我们有了一个供脚本查找的区域。


创建脚本


我们正在构建的示例脚本是用Ruby编写的,使用GitHub gem octokit与你的仓库进行交互,使用nokogiri gem爬取网站,并使用httparty gem进行HTTP请求。


在下面这个例子中,要爬取的元素已经被确定了。在你自己的用例中,你需要明确你想爬取的网站上的元素的路径,毫无疑问它将不同于下面显示的在 posts 变量中定义的,以及每个post的每个titlelink


下面是示例代码,将其放在scripts/文件夹中:


require 'httparty'
require 'nokogiri'
require 'octokit'

# Scrape blog posts from the website
url = "<https://www.bengreenberg.dev/blog/>"
response = HTTParty.get(url)
parsed_page = Nokogiri::HTML(response.body)
posts = parsed_page.css('.flex.flex-col.rounded-lg.shadow-lg.overflow-hidden')

# Generate the updated blog posts list (top 5)
posts_list = ["\n### Recent Blog Posts\n\n"]
posts.first(5).each do |post|
title = post.css('p.text-xl.font-semibold.text-gray-900').text.strip
link = "<https://www.bengreenberg.dev#{post.at_css('a')[:href]}>"
posts_list << "* [#{title}](#{link})"
end

# Update the README.md file
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
repo = ENV['GITHUB_REPOSITORY']
readme = client.readme(repo)
readme_content = Base64.decode64(readme[:content]).force_encoding('UTF-8')

# Replace the existing blog posts section
posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m
updated_content = readme_content.sub(posts_regex, "#{posts_list.join("\n")}\n")

client.update_contents(repo, 'README.md', 'Update recent blog posts', readme[:sha], updated_content)

正如你所看到的,首先向网站发出一个HTTP请求,然后收集有博客文章的部分,并将数据分配给一个posts变量。然后,脚本在posts变量中遍历博客文章,并收集其中的前5个。你可能想根据自己的需要改变这个数字。每循环一次博文,就有一篇博文被添加到post_list的数组中,其中有该博文的标题和URL。


最后,README文件被更新,首先使用octokit gem找到它,然后在README中找到要更新的地方,并使用一些正则: posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m


这个脚本将完成工作,但实际上没有任何东西在调用这个脚本。它是如何被运行的呢?这就轮到GitHub Actions出场了!


创建Action工作流


现在我们已经有了脚本,我们需要一种方法来按计划自动运行它。GitHub Actions 提供了一种强大的方式来自动化各种任务,包括运行脚本。在这种情况下,我们将创建一个GitHub Actions工作流,每周在周日午夜运行一次该脚本。


工作流文件应该放在.github/workflows/目录下,可以命名为update_blog_posts.yml之类的。以下是工作流文件的内容:


name: Update Recent Blog Posts

on:
schedule:
- cron: '0 0 * * 0' # Run once a week at 00:00 (midnight) on Sunday
workflow_dispatch:

jobs:
update_posts:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Install dependencies
run: gem install httparty nokogiri octokit

- name: Scrape posts and update README
run: ruby ./.github/scripts/update_posts.rb
env:
GITHUB_TOKEN: $
GITHUB_REPOSITORY: $

这个工作流是根据cron语法定义的时间表触发的,该时间表指定它应该在每个星期天的00:00(午夜)运行。此外,还可以使用workflow_dispatch事件来手动触发该工作流。


update_posts工作由几个步骤组成:



  • 使用 actions/checkout@v2操作来签出仓库。

  • 使用 ruby/setup-ruby@v1 操作来设置 Ruby,指定的 Ruby 版本为 3.1。

  • 使用 gem install 命令安装所需的 Ruby 依赖(httpartynokogirioctokit)。

  • 运行位于.github/scripts/目录下的脚本 update_posts.rbGITHUB_TOKENGITHUB_REPOSITORY环境变量被提供给脚本,使其能够与仓库进行交互。


有了这个工作流程,你的脚本就会每周自动运行,抓取博客文章并更新README文件。GitHub Actions负责所有的调度和执行工作,使整个过程无缝且高效。


将所有的东西放在一起


如今,你的网络形象往往是人们与你联系的第一个接触点--无论他们是潜在的雇主、合作者,还是开源项目的贡献者。尤其是你的GitHub个人主页,是一个展示你的技能、项目和兴趣的宝贵平台。那么,如何确保你的GitHub个人主页是最新的、相关的,并能真正反映出你是谁?


通过利用 GitHub Actions 的力量,我们展示了如何将你的 GitHub 配置文件从一个静态的 Markdown 文档转变为一个动态的、不断变化关于你是谁的例子。通过本指南提供的例子,你已经学会了如何从网站上抓取数据,并利用它来动态更新你的 GitHub个人主页。虽然我们的例子是用Ruby实现的,但同样的原则也可以用JavaScript、TypeScript、Python或你选择的任何其他语言来应用。


回顾一下,我们完成了创建一个Ruby脚本的过程,该脚本可以从网站上抓取博客文章,提取相关信息,并更新你的README.md文件中的"最近博客文章"部分。然后,我们使用GitHub Actions设置了一个工作流,定期运行该脚本,确保你的个人主页中保持最新的内容。


但我们的旅程并没有就此结束。本指南中分享的技术和方法可以作为进一步探索和创造的基础。无论是从其他来源拉取数据,与API集成,还是尝试不同的内容格式,都有无限的可能性。


因此,行动起来让你的 GitHub 个人主页成为你自己的一个充满活力的扩展。让它讲述你的故事,突出你的成就,并邀请你与他人合作。


以上就是本文的全部内容,如果对你有所启发,欢迎点赞、收藏、转发~


作者:chuck
来源:juejin.cn/post/7251884086537650232
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpress/2…


作者:Mengke
来源:juejin.cn/post/7251884086536781880
收起阅读 »

前端地位Up!

web
背景 大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C... 然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种...
继续阅读 »

背景


大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C...


然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种小白进阶培训班、是业务形态的致命一击。最终是前端在技术圈子里不如狗的地位。


何至于此?


框架-砒霜or蜜糖?


说说框架


前段跟一个google的算法大佬聊天,他说你们前端很奇怪,现在浏览器已经非常快了,但你们非要搞这样那样的框架,然后去算这个算那个,搞些hooks虚拟dom,完全看不懂在干嘛。


也许我想他说的是对于web的性能层面是对的。


首先虚拟dom也就是中间层如果纯论性能在我看来其实是并不适合现在的时代的,它在这个时代的作用就是作为多端统一以及在真实dom操作前的数据缓冲/计算层...这可能也是这个时代出现了SvelteSolid为代表的此类框架的原因。我倒是希望现在web前端的方向能走向SolidSvelte这种框架的周边社区完善开发。


可是时代的潮流不会随着个人的意志改变,果然时代是分分合合吗,现在ssr为代表的next越来越火(可能有一部分vecel的商业原因),但更重要的一个原因是去改善开发人员的体验(在多端和最佳实践方面),也就是卷颗粒度


以next举例子:我对于next其实是这么理解的,粗颗粒度reactcsr已经进无可进了,改也不好改,那么转化一下方向把,在ssr的领域去降低粒度,就像流式渲染等。


说说人


啊真糟糕,怎么情不自禁就在说框架了,是不是发现我们前端情不自禁在开发or说某个技术的时候就会与框架挂钩。就像是面试我懂某个框架的原理、我学了几个框架诸如此类的,于是我们就从一个框架到另外个框架反反复复的在路上走、打包工具也是一样的(我真的需要这么快的打包工具吗?)。


于是我们就在框架中沉溺了,也许后面会出现一些5年vue工程师10年react工程师,我们整日沉浸在框架之中,日复一日,用着固定的写法(其实我在说Vue,React在这方面会好一些),做着相似的事情,技术的成长变为了我某个框架用得怎么样。


前端工程师or框架工程师


我想啊,前端的潮流很快(娱乐圈),但其实我们要明白一个道理,我用这个东西学这个东西对我有没有收益,对用户体验是不是有很大的提升,对团队开发有没有效率的进步。如果没有的话,不如搞浏览器,当然也可以学学当个PPT天才(纯褒义)或者业务、算法、其他语言等(好堕落啊现在不是PPT就是搞业务),着实没必要把绝大多数时间留着框架上,看多了就会觉得自己很牛逼,然后开摆。


毕竟我们不是框架开发工程师(也就是资源型工具人)、我们是前端开发工程师,我们是面向屏幕开发,我们是人机交互工程师,也就是现在的一个词终端工程师,如果你不能把你的应用在所有屏幕(安卓、Ios、桌面、PC、平板)跑那应该是不合格的。


业务形态


害,说这个之前闲聊一下,我们可以看到一些产品诸如语雀云凤蝶Antd等。蚂蚁体验技术部真的把前端的地位上拉了一截,他们真的很好,可能是未来5年在国内都不会再有这么好的标志性的前端产品,可惜没有一个闭环的商业业务形态,就类似next这种,我在这里不讨论具体的一些原因和后面发生的一些事情。


进入正文,产品和业务形态决定了前端的地位,后端开发通常被认为是应用程序的基础和核心。但其实怎么说那,有的时候其实是因为国内产品思维的局限于和上文提到的沉溺于框架和搜索工具


因为我做过不少国外的产品,有一些很有创造力和创新思维的产品会提出很多天马行空,极具艺术的产品交互效果和体验,在这类产品中其实前端的地位并不算低。


但是在国内就会有这种情况:



  1. 产品不会有这种想法,他的脑子里也是一些国内的那些很普通的竞品,和数据流转逻辑

  2. 前端自己拒绝,一般来说心路历程是这样的,我先看看能不能做 => 去百度掘金搜索 => 搜不到或者框架里没有,好感觉不好做,太复杂了 => 我们换一个普通一点的效果(理由五花八门)。

  3. 大家都是这么做的,那我们这个也这么做吧。


但其实我们自己作为科技触达用户的桥梁,是有能力去推动这个事情的,一个炫酷的配色、合理的交互效果、好看的页面,是可以去给产品去给设计说的,比如我自己有时候会figma或者lottie去自己画一些图和动画效果,去主动纠正设计的颜色和间距。难道产品会拒绝让产品变得更好?设计会拒绝更好看?


说白了,自己不想去做不想去推不会也不想学,觉得很复杂,当然如果实在没有这个土壤果断跑路。


Javascript本身的问题


Javascript吊打RustC估计我是这辈子都看不到了。


Javascript是解释性语言肯定没法跟一些编译语言竞天生就不行,再加上单线程即使有解决方案也就那样。这意味着前端掌握更多语言几乎是一个必要的事情,JavaSwift,Oc这些本来就会用到的不必多说,RustPython选一门掌握也很好。


会得越多你越强,当然我还是建议大家去当PPT天才


前端自信


以后,大伙自信一点,别觉得前端就不如其他技术岗位,地位都是自己争取的,前端优势很大,语言统一、前端立马可见的效果、前端基建相对较小、前端宿主环境统一Docker和容器配置相对统一等。主要是时间,有更多的时间意味着可以做更多的事学更多的东西更多的钱~。


总结


So,改变前端环境从你我做起,你卷一波我卷一波,前端的门槛就提起来了,以后面试的基本要求就是:前端要会Js、Ts、Java、Swift、混合框架、PWA,然后薪资30k起步。


作者:溪饱鱼
来源:juejin.cn/post/7283642910301192244
收起阅读 »