注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

三十而立,我走过的路

你好,我是逗逗青,目前在某领先海外互联网公司担任技术leader,主要从事平台架构相关的研发和管理工作。 2023年即将进入尾声,我的职场生涯也走过了十年,在这篇文章里,我想聊聊我的过去,聊聊这些年我走过的一些弯路,以及得到的一些收获,希望对你能有一些启发。 ...
继续阅读 »

你好,我是逗逗青,目前在某领先海外互联网公司担任技术leader,主要从事平台架构相关的研发和管理工作。


2023年即将进入尾声,我的职场生涯也走过了十年,在这篇文章里,我想聊聊我的过去,聊聊这些年我走过的一些弯路,以及得到的一些收获,希望对你能有一些启发。


逗逗青的求学记


我来自广东一个沿海的农村,是家中长子,家族里学识最高的是我爸。


嗯,他初中学历。


我爸曾和我聊起他辍学的原因:害怕罚站。


他说:当年上学时,路比较远,家里只有一辆破烂的自行车,每次上学路上,这车常坏,经常变成走路推车上学。所以经常迟到被罚站,碍于面子就辍学花钱去拜师学做油漆工了。


我不知道我爸的选择是否正确,但我很庆幸,还好我爸习得了一门手艺,否则,我可能无法顺利上大学。


普通的小学


思绪回到1998年,那会我刚上小学,九年义务教育政策一直吊在我的尾巴:我一毕业,学弟学妹们就开始免收学费。


没赶上好的时代,我的求学生涯只能靠一担担的番薯喂养出来,至今我还记得很清楚,我一学期的学费大约等于一拖拉机的番薯。


这其中,还不包括将番薯从田地里一担一担来地来回走几百米挑到拖拉机上的人工费。


遇到收成不好的时节,我爸就得挨家挨户去给我借钱凑学费了。


因我爸老实本分,除了务农,也有油漆工这门手艺在手,在困难时,常有人能伸伸援手。


虽然家人很辛苦的供我上学,但在整个小学时期,我的成绩一直很普通,小学6年基本与各种奖状无缘。


一开始,我很羡慕那些在学期末可以拿着各种奖状,然后带着欢笑回家的同学。


到后来,我发现家里一到下雨天就会漏水,奖状拿回家贴墙上估计也很快受潮,所以也就释怀了。


逆袭的初中


2004年,进到初中时期,我的学习成绩发生了转变。


在那会,我们学校将班级划分为十几个普通班和四个重点班。


资质一般的我考进了普通班,但幸运的是,我们班级的隔壁就是重点班,更幸运的是,里边还有个我同村的亲戚。


因初中学校离家有些距离,我们一般会骑自行车上学,所以平时我们走得比较近,偶尔会聊聊学习方面的内容。


通过与这位亲戚的交流,我才开始意识到学习也是需要掌握一些技巧的,比如需要多刷题实战,特别是高频题。


慢慢地,我改掉了自己的一些学习陋习,比如埋头苦读背诵书本知识。


我的学习成绩也开始逐步提升。印象中,我当时最好的科目是数学,而且还摸清了数学的出题规律,每次考试,除了最后一道大题,基本都可以稳拿分数。


后来我的成绩排到了班里的第1名,并且升初二时,在班主任的帮助下调进了重点班。


到了重点班,我才明白重点班和普通班的本质区别。


这里,充满了竞争!


有的同学规划清晰,知道自己要去往何处,有的同学纯粹是年少轻狂,喜欢比拼成绩。


而我,则是盯上了那个为数不多的名额。


进了重点班后我才知道,学校和一所不错的高中学校有合作,优等生升学进这所高中可以免三年学费,但名额只有20个。


我不知道别人是否和我一样,也对那个名额感兴趣,但我能感受到,每次考试大家都会铆足了劲。


在那种环境之下,我的成绩也被逼着继续往前走,在后来的两年里,我的每次模拟考成绩基本都排在全校前60,但一直与那个名额无缘。


临近中考时,那所高校来人了,和20个优等生签订了协议,看着他们一个个拿着协议书从学校会议室走出来,那一刻,我竟然又有了“羡慕别人拿奖状”的感觉。


不过后来发生了一件趣事,让我从这次竞争失利中走了出来。


我爸平时忙着务工,早出晚归,基本没关心过我的学习成绩。


后来他听朋友聊起中考的事情,并听闻我们村附近某所初中学校在中考时,录取分数线可以比其他学校低(印象中和扶贫政策有关),比较容易升高中。


所以后面我爸就跟着朋友忙起了帮孩子转校的事情,最后在没和我商量的情况下,跑到我学校的校长面前,沟通起给我转校的事情。


我爸和我说这事时,我被逗笑了,他说校长被我爸气坏了,校长的原话是这样的:“我辛辛苦苦栽培3年的花朵,你现在就要摘走?”。


后来,校长在了解清楚情况并且知道我家的条件后,说服了我爸不给我转校。另外校长答应我爸,如果我的中考成绩能继续保持在学校前60,可以破格帮我申请免三年学费进到前面提到的那所高中。


中考很快结束,我的成绩很稳,还是和平时一样,保持在全校前60,但中考结束后的那段时间,出现了让我非常纠结的事情。


校长很守信,中考成绩出来后,他帮我申请到了入那所高中的名额,但因为是破格,只能免第1年学费,而第二和第三年学费需要由学校考核决定是否继续减免。


让我纠结的有3点,一是这个“破格录取”,在当时我感觉像是走后门,心理很不舒服。二是我的中考的分数可以进到县里一所不错的重点高中,而且听闻比这所破格进的学校要好。三是家里的经济条件。


最后,在我爸的支持之下,我选择了去自己考上的重点高中学校。


叛逆的高中


2007年,我顺利考进了县城里的重点高中,而且还是重点班,选学校那件事带来的郁闷感很快一扫而尽。


高中开始,我们从偏远农村来的就需要寄宿在学校了,原本被家人寄予厚望的我,却在高一时期发生了突变,是的,突变。


高一时,我开始接触到了网吧,从小最多只看过别人在电视机上玩小霸王游戏,而且在当时还没有QQ号的我,沦陷了。


高中那会,大部分人都有QQ,而我,只在初中毕业填写留言纪念簿时才知道有这东西,没办法,为了社交只好跟着同学去网吧学习怎么玩QQ。


一脚踏入网吧的我,立马被网络世界的各种缤纷色彩给吸住了。


到什么程度呢?


经常找同网吧坐我旁边机位的同学聊QQ,自学五笔还带出了几个徒弟,自习时还得意的带着他们唱"工戈草头右框七",还经常帮女同学整QQ空间,至于男同胞,则天天帮他们刷各种Q钻会员。


一直到后面开始接触网络游戏,就真的完全陷进去了,疯狂到原本为数不多的生活费都可以省出一大半用来上网。


高一的疯狂放纵,带来的结局是,在升高二时我被班主任调到了次重点班。


这场景是如此地熟悉,但体验感却是天差地别。


这对我打击很大,网瘾不降反增,上课睡觉,晚上通宵成了我的常态。


有一次周末我通完宵,白天在宿舍睡觉时被人叫醒,当时映入我眼帘的是我爸失望的表情,以及在一旁小声啜泣的母亲。


我荒废学业的事,被班主任通知到家中了。


这件事后,我才开始变得有所收敛,不过学业落下太多,后面基本无心学习。


就这样混混噩噩度过了三年高中,一直到教室黑板上的高考倒计时剩余30天时我才幡然醒悟。


不过为时已晚,最终,高考成绩公布后,我只考了498分,只到大专院校的录取分数线。


高考结果出来后,我开玩笑地对我爸说:"爸,您看我只花30天的时间,成绩就追上了专科的分数线,您让我复读吧,二本应该轻轻松松,一本还可以挑战挑战"。


我爸只回了句"滚",就没后文了。


浪子回头的我,在知道复读无望后,决定在大专的世界打下一片天下。


无知的大学


填报志愿时,我选择了软件技术专业。


也许是我爸感受到了我痛改前非的觉悟,也许是害怕我继续堕落。在知道我选择了计算机相关专业后,就忙着打听到这个专业的内容。


后来他了解到这个专业需要用到电脑,在一天中午,顶着大太阳,他把我拉上摩托车,先是带我去了他做碾米工作的朋友家借了1000块,然后再带着我去了他一个在做图文印刷店的朋友家,花了重金600块买下了对方的二手笔记本。


后来我很庆幸这台笔记本很垃圾,在别人玩游戏的时候我只能选择敲代码。


高考结束后的假期我没有和往常一样选择去打暑假工,而是选择呆在了家里。


通过上网了解到所报专业的相关课程内容后,还未开学,我就用笔记本下载了马士兵和韩顺平老师的教程视频,自学完了Java基础课程,还写了个坦克游戏天天在我弟面前显摆。


2010年,我开启了三年既装逼又无知的大专生涯。


由于开学前就把专业基础打好,在大一,当别人还在学基础课程时,我已经开始学做项目,偶尔也能和学校的师兄合作帮学校做做项目。


在平时,还会和去了重本大学的高中同学一起比赛刷杭电ACM,后来,还因此拿了个蓝桥杯省赛一等奖,不过正当我摩拳擦掌准备去北京参加全国决赛时,却被学校告知学校经费不足去不了。好吧,穷孩子的环境就是这么恶劣。


因为能力得到认可,和老师相处也融洽,一些专业课的老师们私下给我开了特权,允许我不用去教室上课,想忙啥就忙去。


所以我很听老师们的话,大二开始我开始忙着泡妞去了,并且把班里暗恋的女同学拐到手,嗯,她现在成为了我的妻子。


大三时期,某家培训机构来到了我们学校做宣传,我听完那些各式各样学员进名厂的案例,我心动了。以先学后付的方式进了培训机构。


大三时期的我一下进到了初中时期拼入选名额的状态,朝九晚十,日复一日的在广州某培训校区学习,一直幻想着凭自己的实力加上培训机构的联合企业资源,一举进到名厂。


但,现实却给了我一个大嘴巴子。


逗逗青的职场路


面试碰壁


2013年,大专即将毕业,培训机构的课程也已学完,我开始尝试自己出去找工作,原本以为就业会很顺利,但找工作却是磕磕碰碰。


小公司倒还好,基本有叫面试的都能拿到offer,但一到中大厂,就很难走到终面,而且大部分情况是简历直接被拒。


直到很多年后我才明白其根本原因:中大厂对于应届生的要求,是要挑选好苗子,学历对企业而言是一项减少选苗子出现差错的必要筛选项,而专业技能方面更看重的是通用基础技能,如操作系统、算法、常用框架原理等。


而在当时的我,是个偏实战的低学历选手,知识面学得很广但是不深,而且还有大专学历这个减分项,求职困难可想而知。


求职碰壁后,为应付学校的毕业实习要求,以及缓解经济上的压力,我选择了在一家做传统软件开发的中小企业过渡了一年时间。


因为不甘心,那段时间我一直在思考问题根因,后面大概摸到了一些门道,在还清学校助学贷款和培训机构的费用后,我决定跳槽。


跳槽再战


在当时,我隐约意识到求职的关键还是要对口,需要知道用人单位的需求。而我之前却有点炫技,简历上乱七八糟会的技术和项目全写上了。


决定跳槽后,我对市场上的招聘需求进行了归类,结合自己的兴趣和能力,我选择了主攻游戏行业(网瘾少年的后遗症~),并自学填补了游戏行业所需的关键技术,如网络、并发等。


终于在后面的面试中越挫越勇,顺利找到了一份上市公司的游戏服务端研发工作。


现在回过头想想,有点庆幸,还好那个时期的市场机会比较多,有很多机会去尝试,虽然求职路走了挺多弯路,但最终结果还是比较好的。


后来,在游戏研发岗位我做了2年多时间,主要做页游和手游。


在游戏行业里,企业内部一般是按工作室划分不同团队,比如“天美工作室”。


在游戏这个行业,如果想要赚得多,你得进到好团队。什么是好团队?能赚钱的团队就是好团队。


2年多时间的游戏工作经历,让我深刻理解了什么叫“选择比努力更重要”。


我庆幸自己进到了一个不错的游戏团队,虽然工作强度比较大,但由于项目运营收益高,经常可以参与奖金分红。


但同企业内的有些团队就另当别论了,强度比我们团队要来得更猛,但一年到头基本没什么奖金,而且还需要时常担忧工作室可能会解散的问题。


在游戏行业,除《王者荣耀》、《原神》等这种现象级产品,普遍项目周期比较短,一般两年左右就会进入衰退期,当项目不再产生正向收益且公司不再投入资金研发新项目时,团队就会面临解散的危险。


我所在团队的项目就属于后劲不足的产品。两年左右,团队业绩就开始出现下滑,最终团队还是解散了。


在游戏公司,团队解散时,一般会有其他团队过来挖人。当时我被其他团队挖了过去,而团队的其他大部分成员基本都办理了离职,包括我的leader,也就是团队里的主程。


跳槽转型


后来,我的leader在离职不到半年后联系上了我,并把我挖到他所在的一家互联网公司,也是从那一刻起,我开始转型做平台架构相关的研发工作。


当时这家公司比较吸引我的是:团队强、营收高。我当时是9月份过去,工作了4个月就到了发年终奖时间,当时我拿到了4个多月奖金。


这家公司钱给得大方,但工作强度贼猛,公司里加班文化比较重,工作节奏也非常快。


当时我所在的团队是公共部门,也就是所谓的中台。负责支持公司所有业务产品的基础能力,如订单、支付、履约等。


项目研发一周一个迭代,因为是公共部门,除了节奏要快,上线还要求要稳。一开始我很难适应,主要原因还是我的能力跟不上。


在那段时间,也是我买书最疯狂的时期。基本和工作相关的技术书籍我都买来啃了,加上经常能和其他大厂里的朋友交流技术,能力才开始慢慢追上。


成家,寻求变化


2017年,经过4年的积累,我和女友有了一些积蓄,爱情长跑也6年了,所以我们选择在广州安了个小家,把爱情的果结了。


2018年,我们的第1个宝宝出生,也是这一年,我选择了离职。


这5年里,我的工作强度一直比较大,经常处于on-call的状态,另外在业余时间我也比较卷,熬夜是我的常态。


宝宝的出生,加上近两次的体验报告出现了比较多预警信息,让我开始思考工作与生活的平衡。


所以我跳槽去了一家工作强度相对较好的企业,而且也选择了继续从事自己擅长的平台架构工作。


入职新公司后,由于懂得了一点分析上级期望值的技巧,在日常工作中常能做出符合领导心意的成绩,后来上级领导选择辞职创业后,我也顺势当上了团队leader,顺带拿到了一些期权。


所以现在,我平时除了写写代码,还需要规划团队的发展方向以及带带新人,平时也会作为面试官,参与公司的招聘,在这期间积累到了一些新的经验和感触,未来我会对这些经验做些分享,这里不作展开。


目前这家企业,我呆了有一段时间,近期感觉在发展上遇到了一些瓶颈,所以接下的路,要如何走,我还在持续思考与探索。


写在最后


近期,我计划在公众号上开始写作,所以也才有了这一篇文章。


工作了十年,我想找个地方将积累的内容沉淀下来,一是利他,二是自身也能受到一些益处,比如将来出本书,或者转行做教育工作者,都是不错的选择。


所以在接下来的三年,我计划通过写作的方式,持续分享一些高质量的技术干货,来链接一些技术同行,特别是一些职场新人。


我期望通过我的分享,能够帮到一些人少走些弯路,在职场的发展上能得到一些提速。


最后,关于“逗逗青”这个网名,是源于我的女儿。她近期在看一部名为《土豆逗严肃科普》的动画片,后来经常会给我讲土豆逗的故事,不听还不行~


所以我的网名就诞生了,虽有些稚气,但未来当我感到疲惫时,也许这个网名可以给我带来一些正能量。


作者:逗逗青
来源:juejin.cn/post/7317535572432584738
收起阅读 »

Android 0,1,2 个启动图标

最近改了个隐式app 拉起,启动图标也有不同的玩法 0 个启动图标 <intent-filter> <action android:name="android.intent.action.MAIN" /> <category an...
继续阅读 »

最近改了个隐式app 拉起,启动图标也有不同的玩法


0 个启动图标


<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="app"
android:path="/"
android:port="8080"
android:scheme="lb">
</data>
</intent-filter>

这里是对接受所有隐式拉起,这个是告诉系统app 启动不需要用户手动拉起,是为了被代码或者其他中转站调用,所以不需要用户手动拉起,自然就不用再显示图标


1个启动图标


<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

LAUNCHER 是决定是不是要显示在程序列表里,默认为主动唤起,也是Android 标准启动模式,会正常在手机的界面显示


2 个启动图标


<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<activity android:name="com.camera.demo.1Activity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="com.camera.demo.2Activity"
android:icon="@mipmap/ic_launcher" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

上面说了android.intent.category.LAUNCHER 为是否显示在应用列表内,所以我们配置多个LAUNCHER,就会有多个图标显示在手机列表内


intent-filter 相关说明


接受页面隐式跳转的过滤器,


action


必须的包含,定义一些操作.android.intent.action.MAIN/android.intent.action.WEB_SEARCH


image.png


category


一个字符串, 包含了处理该Intent的组件的种类信息, 起着对action的补充说明作用


image.png


data


要指定接受的 Intent 数据,Intent 过滤器既可以不声明任何 <data> 元素,也可以声明多个此类元素,如下例所示:


<intent-filter>
    <data android:mimeType="video/mpeg" android:scheme="http" ... />
    <data android:mimeType="audio/mpeg" android:scheme="http" ... />
    ...
</intent-filter>

每个 <data> 元素均可指定 URI 结构和数据类型(MIME 媒体类型)。URI 的每个部分都是一个单独的属性:schemehostport 和 path


<scheme>://<host>:<port>/<path>


下例所示为这些属性的可能值:


content://com.example.project:200/folder/subfolder/etc


在此 URI 中,架构是 content,主机是 com.example.project,端口是 200,路径是 folder/subfolder/etc
在 <data> 元素中,上述每个属性均为可选,但存在线性依赖关系:



  • 如果未指定架构,则会忽略主机。

  • 如果未指定主机,则会忽略端口。

  • 如果未指定架构和主机,则会忽略路径。


将 Intent 中的 URI 与过滤器中的 URI 规范进行比较时,它仅与过滤器中包含的部分 URI 进行比较。例如:



  • 如果过滤器仅指定架构,则具有该架构的所有 URI 均与该过滤器匹配。

  • 如果过滤器指定架构和权限,但未指定路径,则具有相同架构和权限的所有 URI 都会通过过滤器,无论其路径如何均是如此。

  • 如果过滤器指定架构、权限和路径,则仅具有相同架构、权限和路径的 URI 才会通过过滤器。


最后贴张LAUNCHER 的原理图


image.png


作者:libokaifa
来源:juejin.cn/post/7307471540715126795
收起阅读 »

18张图,详解SpringBoot解析yml全流程

前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的: switch: turnOn: on 程序中的代码也...
继续阅读 »



前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的:


switch:
turnOn: on

程序中的代码也很简单,大致的逻辑就是下面这样,如果取到的开关字段是on的话,那么就执行if判断中的代码,否则就不执行:


@Value("${switch.turnOn}")
private String on;

@GetMapping("testn")
public void test(){
if ("on".equals(on)){
//TODO
}
}

但是当代码实际跑起来,有意思的地方来了,我们发现判断中的代码一直不会被执行,直到debug一下,才发现这里的取到的值居然不是on而是true



看到这,是不是感觉有点意思,首先盲猜是在解析yml的过程中把on作为一个特殊的值进行了处理,于是我干脆再多测试了几个例子,把yml中的属性扩展到下面这些:


switch:
turnOn: on
turnOff: off
turnOn2: 'on'
turnOff2: 'off'

再执行一下代码,看一下映射后的值:



可以看到,yml中没有带引号的onoff被转换成了truefalse,带引号的则保持了原来的值不发生改变。


到这里,让我忍不住有点好奇,为什么会发生这种现象呢?于是强忍着困意翻了翻源码,硬磕了一下SpringBoot加载yml配置文件的过程,终于让我看出了点门道,下面我们一点一点细说!


因为配置文件的加载会涉及到一些SpringBoot启动的相关知识,所以如果对SpringBoot启动不是很熟悉的同学,可以先提前先看一下Hydra在古早时期写过一篇Spring Boot零配置启动原理预热一下。下面的介绍中,只会摘出一些对加载和解析配置文件比较重要的步骤进行分析,对其他无关部分进行了省略。


加载监听器


当我们启动一个SpringBoot程序,在执行SpringApplication.run()的时候,首先在初始化SpringApplication的过程中,加载了11个实现了ApplicationListener接口的拦截器。



这11个自动加载的ApplicationListener,是在spring.factories中定义并通过SPI扩展被加载的:



这里列出的10个是在spring-boot中加载的,还有剩余的1个是在spring-boot-autoconfigure中加载的。其中最关键的就是ConfigFileApplicationListener,它和后面要讲到的配置文件的加载相关。


执行run方法


在实例化完成SpringApplication后,会接着往下执行它的run方法。



可以看到,这里通过getRunListeners方法获取的SpringApplicationRunListeners中,EventPublishingRunListener绑定了我们前面加载的11个监听器。但是在执行starting方法时,根据类型进行了过滤,最终实际只执行了4个监听器的onApplicationEvent方法,并没有我们希望看到的ConfigFileApplicationListener,让我们接着往下看。



run方法执行到prepareEnvironment时,会创建一个ApplicationEnvironmentPreparedEvent类型的事件,并广播出去。这时所有的监听器中,有7个会监听到这个事件,之后会分别调用它们的onApplicationEvent方法,其中就有了我们心心念念的ConfigFileApplicationListener,接下来让我们看看它的onApplicationEvent方法中做了什么。



在方法的调用过程中,会加载系统自己的4个后置处理器以及ConfigFileApplicationListener自身,一共5个后置处理器,并执行他们的postProcessEnvironment方法,其他4个对我们不重要可以略过,最终比较关键的步骤是创建Loader实例并调用它的load方法。


加载配置文件


这里的LoaderConfigFileApplicationListener的一个内部类,看一下Loader对象实例化的过程:



在实例化Loader对象的过程中,再次通过SPI扩展的方式加载了两个属性文件加载器,其中的YamlPropertySourceLoader就和后面的yml文件的加载、解析密切关联,而另一个PropertiesPropertySourceLoader则负责properties文件的加载。创建完Loader实例后,接下来会调用它的load方法。



load方法中,会通过嵌套循环方式遍历默认配置文件存放路径,再加上默认的配置文件名称、以及不同配置文件加载器对应解析的后缀名,最终找到我们的yml配置文件。接下来,开始执行loadForFileExtension方法。



loadForFileExtension方法中,首先将classpath:/application.yml加载为Resource文件,接下来准备正式开始,调用了之前创建好的YamlPropertySourceLoader对象的load方法。


封装Node


load方法中,开始准备进行配置文件的解析与数据封装:



load方法中调用了OriginTrackedYmlLoader对象的load方法,从字面意思上我们也可以理解,它的用途是原始追踪yml的加载器。中间一连串的方法调用可以忽略,直接看最后也是最重要的是一步,调用OriginTrackingConstructor对象的getData接口,来解析yml并封装成对象。



在解析yml的过程中实际使用了Composer构建器来生成节点,在它的getNode方法中,通过解析器事件来创建节点。通常来说,它会将yml中的一组数据封装成一个MappingNode节点,它的内部实际上是一个NodeTuple组成的ListNodeTupleMap的结构类似,由一对对应的keyNodevalueNode构成,结构如下:



好了,让我们再回到上面的那张方法调用流程图,它是根据文章开头的yml文件中实际内容内容绘制的,如果内容不同调用流程会发生改变,大家只需要明白这个原理,下面我们具体分析。


首先,创建一个MappingNode节点,并将switch封装成keyNode,然后再创建一个MappingNode,作为外层MappingNodevalueNode,同时存储它下面的4组属性,这也是为什么上面会出现4次循环的原因。如果有点困惑也没关系,看一下下面的这张图,就能一目了然了解它的结构。



在上图中,又引入了一种新的ScalarNode节点,它的用途也比较简单,简单String类型的字符串用它来封装成节点就可以了。到这里,yml中的数据被解析完成并完成了初步的封装,可能眼尖的小伙伴要问了,上面这张图中为什么在ScalarNode中,除了value还有一个tag属性,这个属性是干什么的呢?


在介绍它的作用前,先说一下它是怎么被确定的。这一块的逻辑比较复杂,大家可以翻一下ScannerImplfetchMoreTokens方法的源码,这个方法会根据yml中每一个keyvalue是以什么开头,来决定以什么方式进行解析,其中就包括了{['%?等特殊符号的情况。以解析不带任何特殊字符的字符串为例,简要的流程如下,省略了一些不重要部分:



在这张图的中间步骤中,创建了两个比较重要的对象ScalarTokenScalarEvent,其中都有一个为trueplain属性,可以理解为这个属性是否需要解释,是后面获取Resolver的关键属性之一。


上图中的yamlImplicitResolvers其实是一个提前缓存好的HashMap,已经提前存储好了一些Char类型字符与ResolverTuple的对应关系:



当解析到属性on时,取出首字母o对应的ResolverTuple,其中的tag就是tag:yaml.org.2002:bool。当然了,这里也不是简单的取出就完事了,后续还会对属性进行正则表达式的匹配,看与regexp中的值是否能对的上,检查无误时才会返回这个tag


到这里,我们就解释清楚了ScalarNodetag属性究竟是怎么获取到的了,之后方法调用层层返回,返回到OriginTrackingConstructor父类BaseConstructorgetData方法中。接下来,继续执行constructDocument方法,完成对yml文档的解析。


调用构造器


constructDocument中,有两步比较重要,第一步是推断当前节点应该使用哪种类型的构造器,第二步是使用获得的构造器来重新对Node节点中的value进行赋值,简易流程如下,省去了循环遍历的部分:



推断构造器种类的过程也很简单,在父类BaseConstructor中,缓存了一个HashMap,存放了节点的tag类型到对应构造器的映射关系。在getConstructor方法中,就使用之前节点中存入的tag属性来获得具体要使用的构造器:



tagbool类型时,会找到SafeConstruct中的内部类 ConstructYamlBool作为构造器,并调用它的construct方法实例化一个对象,来作为ScalarNode节点的value的值:



construct方法中,取到的val就是之前的on,至于下面的这个BOOL_VALUES,也是提前初始化好的一个HashMap,里面提前存放了一些对应的映射关系,key是下面列出的这些关键字,value则是Boolean类型的truefalse



到这里,yml中的属性解析流程就基本完成了,我们也明白了为什么yml中的on会被转化为true的原理了。至于最后,Boolean类型的truefalse是如何被转化为的字符串,就是@Value注解去实现的了。


思考


那么,下一个问题来了,既然yml文件解析中会做这样的特殊处理,那么如果换成properties配置文件怎么样呢?


sw.turnOn=on
sw.turnOff=off

执行一下程序,看一下结果:



可以看到,使用properties配置文件能够正常读取结果,看来是在解析的过程中没有做特殊处理,至于解析的过程,有兴趣的小伙伴可以自己去阅读一下源码。


那么,今天就写到这里,我们下期见。


作者:码农参上
来源:juejin.cn/post/7054818269621911559
收起阅读 »

Java 中for循环和foreach循环哪个更快?

本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式 前言 在Java编程中,循环结构是程序员常用的控制流程,而for循环和foreach循环是其中比较常见的两种形式。关于它们哪一个更快的讨论一直存在。...
继续阅读 »

本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式



前言


在Java编程中,循环结构是程序员常用的控制流程,而for循环和foreach循环是其中比较常见的两种形式。关于它们哪一个更快的讨论一直存在。本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式。通过详细比较它们的遍历效率、数据结构适用性和编译器优化等因素,我们将为大家揭示它们的差异和适用场景,以便您能够做出更明智的编程决策。



for循环与foreach循环的比较


小编认为for和foreach 之间唯一的实际区别是,对于可索引对象,我们无权访问索引。


for(int i = 0; i < mylist.length; i++) {
if(i < 5) {
//do something
} else {
//do other stuff
}
}

但是,我们可以使用 foreach 创建一个单独的索引 int 变量。例如:


int index = -1;
for(int myint : mylist) {
index++;
if(index < 5) {
//do something
} else {
//do other stuff
}
}

现在写一个简单的类,其中有 foreachTest() 方法,该方法使用 forEach 迭代列表。


import java.util.List;

public class ForEachTest {
List intList;

public void foreachTest(){
for(Integer i : intList){

}
}
}

编译这个类时,编译器会在内部将这段代码转换为迭代器实现。小编通过执行 javap -verbose IterateListTest 反编译代码。


public void foreachTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: aload_0
1: getfield #19 // Field intList:Ljava/util/List;
4: invokeinterface #21, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
9: astore_2
10: goto 23
13: aload_2
14: invokeinterface #27, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
19: checkcast #33 // class java/lang/Integer
22: astore_1
23: aload_2
24: invokeinterface #35, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
29: ifne 13
32: return
LineNumberTable:
line 9: 0
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 this Lcom/greekykhs/springboot/ForEachTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class com/greekykhs/springboot/ForEachTest, top, class java/util/Iterator ]
stack = []
frame_type = 9 /* same */

从上面的字节码我们可以看到:


a). getfield命令用于获取变量整数。


b).调用List.iterator获取迭代器实例


c).调用iterator.hasNext,如果返回true,则调用iterator.next方法。


下边来做一下性能测试。在 IterateListTest 的主要方法中,创建了一个列表并使用 for 和 forEach 循环对其进行迭代。


import java.util.ArrayList;
import java.util.List;

public class IterateListTest {
public static void main(String[] args) {
List mylist = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
mylist.add(i);
}

long forLoopStartTime = System.currentTimeMillis();
for (int i = 0; i < mylist.size(); i++) {mylist.get(i);}

long forLoopTraversalCost =System.currentTimeMillis()-forLoopStartTime;
System.out.println("for loop traversal cost for ArrayList= "+ forLoopTraversalCost);

long forEachStartTime = System.currentTimeMillis();
for (Integer integer : mylist) {}

long forEachTraversalCost =System.currentTimeMillis()-forEachStartTime;
System.out.println("foreach traversal cost for ArrayList= "+ forEachTraversalCost);
}
}

结果如下:


总结


观察结果显示,for循环的性能优于for-each循环。然后再使用LinkedList比较它们的性能差异。对于 LinkedList 来说,for-each循环展现出更好的性能。ArrayList内部使用连续存储的数组,因此数据的检索时间复杂度为 O(1),通过索引可以直接访问数据。而 LinkedList 使用双向链表结构,当我们使用 for 循环进行遍历时,每次都需要从链表头节点开始,导致时间复杂度达到了 O(n*n),因此在这种情况下,for-each 循环更适合操作 LinkedList。


作者:葡萄城技术团队
来源:juejin.cn/post/7280050832950624314
收起阅读 »

拿开源套壳就是自主研发?事情没那么简单

去年8月国内科技圈出了一件非常丢人的事情,想必大家已经都知道了,某个号称自主研发的IDE完全使用开源的VSCode改名而来。没错,就是改名! 哦,对了,还加上了vip功能! 为什么这家公司可以如此堂而皇之地将VSCode改成自主研发?在该公司最新的道歉声明...
继续阅读 »

去年8月国内科技圈出了一件非常丢人的事情,想必大家已经都知道了,某个号称自主研发的IDE完全使用开源的VSCode改名而来。没错,就是改名!


图片


哦,对了,还加上了vip功能!


图片


为什么这家公司可以如此堂而皇之地将VSCode改成自主研发?在该公司最新的道歉声明中提到了缺失MIT协议:


图片


MIT协议到底是什么,里面有哪些要求,微软可以告他侵权吗?


在文章的最后我要聊一下软件的开源的意义在哪里,到底怎样才能真的叫“自主研发”。


1、MIT协议是什么


以GitHub为例,打开源代码的根目录,一般会有一个名为license的文件,这个license就是这套代码的许可证信息。


图片


可能这个license的文件内容很多,而且是英文的,不过不必担心,我给大家归纳总结一下就明白了。


直接看这张由阮一峰大佬总结的图,可以看到开源许可证主要限制的点就一目了然:


图片


正因为VSCode采用的是最宽松的MIT协议,它的MIT协议几乎没有什么约束。


整个协议非常的简短,不到两百个单词。我们直接看看VSCode的MIT协议全文长啥样:


图片


首先第一句,这个许可是免费的,任何人都可以拿到软件的副本以及附带的文档。


然后还能做啥呢?使用,复制,修改,合并,发布,分发,再许可/或出售该软件的副本。


也就是说你爱咋整都可以,拿来卖钱也可以,但只有一个要求,就是要把这个许可证放到软件的副本中!


所以大家看懂了上面这家公司的道歉声明了吗?他们道歉的点就是没有把MIT许可证放入其中,这也是MIT许可证唯一的要求。


后面据说他们也在GitHub上开源了CEC-IDE:


图片


不过被骂得太惨,最后还是消失了。


既然是套壳VSCode,微软能告他侵权吗?


答案是不太行。因为MIT许可证本身就是一个不起诉的承诺。


2、做CEC-IDE意义何在


为什么他们要做这个CEC-IDE呢?真的指望它vip能赚钱吗?


作为同样是程序员的我,其实对CEC-IDE的做法并不陌生。


例如我们公司也搞了一个开发平台,为了不惹麻烦了,我就不说是啥了,我就简单叫做by吧。


它其实就把springboot gitlab等等一些东西糅合在一起,然后把包名,比如spring替换成by:


图片


其实我觉得嘛,这玩意要是对内使用,作为公司统一开发的规范,除了包名被换了比较恶心外,问题不算大,反而这样还可以统一管理开发组建的版本。


而CEC-IDE最大的问题就是把这种本来应该内部使用的东西公开化,而且大肆炒作“自主研发”。


一般大企业内部都有研发立项资金,每年都有一定的申报额度,各个部门都会绞尽脑汁去做各种工作。


当然并不是做个ppt就完事了,上面人也不傻,现在一般大企业内部都很卷,为了拿到上面批下来的研发经费,无论如何都要造出点与众不同的地方。


毕竟kpi考核内部竞争也很激烈,所以大多数情况都会提前做一个“好看”的版本,配合一定的亮点宣传,“自主研发”显然是最契合的。


而MIT许可证规避了法律风险,确实是个“完美”的方案。


其实他们也是“聪明”的,只挑MIT许可证的,从他们的道歉声明可以看出,他们最初拿VSCode动手也是做了一定的功课的,错就错在太高调了!


3、软件开源的意义


为什么很多人和公司会选择把自己开发的软件开源?


开源不可避免会导致代码被其他人“拿来主义”,那么开源软件的意义在哪里?是因为他们太有钱做慈善吗?


首先要说明白一点,开源不代表与商业化冲突,反而优秀的开源软件能带来更多的商业化机会。


我举一个例子,假如我发明了一个人脸识别算法,这个算法有一个特别优势:可以在性能非常非常差的硬件上运行。但前提有一个条件,就是需要对指定硬件做适配,于是我把优化好的一个版本放在GitHub上,获得了很多人的关注,甚至也有很多人帮我改进代码中的一些bug。


有一天,一家大公司看中了我的代码,这时候会有两种情况:


第一种就是把我的代码“拿走”,用到自己的产品中,不给我一分钱!


第二种是把我“收编”了,或者给我一笔费用,让我为其提供有偿的技术支持,并能持续迭代适配这家公司的更多低端设备。


稍微有点远见的公司老板,肯定会选第二种。毕竟拿一段无人维护的陌生代码是有很大的风险的。有时候代价比自己做一套还要大。


对于企业来说,开源也不是做慈善,反而有战略作用。


例如代码开源,但你要获取的技术支持是付费的,这也是非常常见的盈利模式。


还有一个典型就是比如开放云服务形式,这也是AI领域常见的开源盈利模式。


在我看来,大家遵守游戏规则,尊重他人的劳动成果,软件开源肯定是有利于整个行业发展的。


4、怎么定义自主研发


自主研发严格定义应该是:企业主要依靠自己的资源,技术,人力,依据自己的意志,独立开发,并在研发项目的主要方面拥有完全独立的知识产权。


除此之外,自主研发还包含一层意思,自己做主,行使权利,而不受他人的控制或限制。


什么叫突破西方卡脖子?


去“突破”人家免费送的东西算哪门子自主!


我承认,做自主研发不可能完全从零开始,在别人的源代码基础上做衍生开发是再正常不过的事情。但起码要让人看到做这件事情的价值。


如此浮躁,急功近利,毫无底线,这件事无疑给国产化、信创行业更加蒙上一层阴影。


作者:程序员Winn
来源:juejin.cn/post/7319181255757185061
收起阅读 »

2023我的仲裁之旅, 谨以此献给需要仲裁的朋友

背景经历在我提出离职之前, 公司断断续续欠薪一年多的时间, 欠薪时长4.5个月(时间这里还搞了个小乌龙, 后面说下)。2023年5月底,我向领导提出辞职,离职时间确定为2023.6.6日。虽然公司欠薪, 但当时的我依旧心存幻想, 想着可以跟公司好聚好散, 毕竟...
继续阅读 »

背景经历

在我提出离职之前, 公司断断续续欠薪一年多的时间, 欠薪时长4.5个月(时间这里还搞了个小乌龙, 后面说下)。

2023年5月底,我向领导提出辞职,离职时间确定为2023.6.6日。

虽然公司欠薪, 但当时的我依旧心存幻想, 想着可以跟公司好聚好散, 毕竟在公司待三年多了&同事关系和谐&如果仲裁的话,了解到按我的情况会有n的补偿金( 以为公司不会冒仲裁风险(公司处于融资阶段),毕竟仲裁的话我这边赢下的概率比较大),期间我积极进行离职交接,又自以为是的以为公司会在我最后走的时候跟我约定好一个薪资发放的时间把欠我的工资补全(为啥说要约定好呢? 因为公司欠薪,薪资发放已经不是正常时间了,还有, 各位,记得不要自以为是!)。

时间转眼到了6.6号,期间人事在这之前一直没找我聊,我也没有找人事聊(人事是知道我提离职的, 上面说到我自以为公司这边会跟我约定好薪资发放时间, 这里为啥会有这种意识呢? 因为之前离职的同事工资都拿到了),这最后一天,我去找人事,人事那边拿出相关的材料让我填写离职申请, 我这边挨个找人签字,但是公司老总正好找各个部门领导开会,我这边还卡着最后一位签字,一时半会签不上, 然后我去找人事了。

我问:我这边工资什么时候能给我?

人事回复:我这边也不确定,需要请示下领导。

我问:领导那边回复要多久?

人事回复: 不确定, 领导现在还在开会,等开会完我确认下。

。。。

我回工位了。

等过了一会, 我看开会的人回来了,我又去找人事问。

我问: 能确定了吗?

人事回复:刚刚我去找领导了,领导不在,估计得明天了。(Fuck!)

然后我就去公司董事长办公室,想自己去聊,结果确实不在。

然后我对人事说:那明天你这边给我一个回复,离职材料我填好了,还差XXX签字,现在又去开会了, 找不到他。

下班时间到了,我走了。

因为没有敲定工资发放日期,有种预感公司准备使用拖字诀对付我了,所以我把一些仲裁材料整理了下,这个具体有什么材料后面再说一下。

第二天一早,我给人事和公司老板发消息,如下:

人事下午给我的回复:

而老板呢? 没理我, 么得回复。人事也是我中间又给发了次消息才回复的我。

到这呢, 我就明白了, 公司确实准备拖我了,可能是资金紧张, 也可能是看人下菜,总之,去仲裁吧。

一个简单的时间线

6.6号离职走人。

6.7号与HR和老板交涉,并开始做仲裁准备。

6.8号准备好仲裁材料

6.9号去仲裁

6.26号调解员开始调解

期间给了一些调解方案, 与调解员, HR交涉

7.19号我与公司均不接受互相给的方案, 我这边坚持开庭, 调解员让我联系仲裁委去那边办理开庭手续, 提交相关仲裁资料, 等待开庭。

8.7号调解员联系我, 说公司因为仲裁案频发, 被上级领导要求整改, 然后公司接受我之前一个降级的调解方案, 赔偿金额可以, 但给我的支付时间我没法接受, 我这边坚持当庭支付。

8.8号调解员做出保证: 可以当庭支付, 且如果公司那边没给当庭支付,那就不支付不给我结案, 我依旧可以继续把流程走下去,不必重新排队。

8.10号接受调解并签字, 公司当庭支付,至此我的仲裁到此结束。

仲裁资料准备

这是我的申请诉求和相关证据材料

  1. 拖欠薪资
  • 劳动合同
  • 我的银行流水
  1. 加班费用
  • 五一期间的打卡记录
  • 加班聊天记录
  1. (2021年的)年终奖金(应于2022.5月份左右发, 但一直拖欠)
  • 与人事沟通的会发年终奖的聊天记录
  • 上一年的工资流水
  1. 经济补偿金
  • n
  1. 误工费

这个其实是我想多看能否多拿一点, 因为在劳动仲裁过程中, 我跑了好几趟,都是请假去的, 但是好像不支持, 最后线下提交的时候没填上它

  1. 其他
  • 国家企业信用信息公示系统下载的企业报告(这个可以下载, 但是没用到, 去仲裁委提交材料的时候需要从仲裁委的企业信用打印机里打印, 用不到这个 )
  • 仲裁申请书, 这个是在劳动仲裁的官方小程序劳动争议调解智慧服务平台上填写并下载的, 现在基本都是线上填写了, 但是提交到仲裁委的时候需要下载下来打印

仲裁仲裁!

6.9号去的仲裁委, 上面的仲裁资料是我最终版的, 去仲裁委之前我只是在网上查的资料, 到了之后让扫个小程序码, 线上提交申请

之后会有调解员联系你(这个时间我等了大概半个月, 6.26才加上调解员的微信), 之后调解员开始调解, 调解员给出公司的方案: 只有正常工资, 加班费和赔偿别想了。这个方案我这边当然不同意, 之后就是各种PK, 我, 公司人事, 调解员三方互相PK, 在两个月之后, 8.11号, 我还是调解了, 拿到了差强人意的money,算是庭前调解, 最终还是没走上开庭。期间我算了下, 两个多月的时间, 大概请了3,4天假。

关于对公司拖欠工资时应对的方案

我想了很多方案, 有应用的也有没用上的, 大家可以看一下或者补充一下:

  • 劳动仲裁: 保存好证据,要有打持久战的准备, 我最后实践有效的就是这个渠道。
  • 公积金中心举报公司未按实际工资给所有员工缴纳公积金: 网上说只要公司没按你的实际工资缴纳公积金, 你就可以去投诉, 尽管这可能是伤敌一千,自损八百的渠道, 但当时我依旧去了。我准备好银行流水和劳动合同, 结果去了之后工作人员告诉我要等仲裁结果, 仲裁结果赢了之后才可以受理。当时还是冒雨去的,老远了, 呵呵呵。
  • 劳动大队投诉: 这个渠道呢, 如果只是想要要回工资不需要补偿, 可以先走这个渠道, 记住这个, 因为你打电话投诉公司的时候, 工作人员会问: 是否已经在仲裁委那边提交申请? 如果回答提交了劳动仲裁申请,那这个机构就不会管了。 说辞: 已经被仲裁委受理, 等待仲裁就好了。综合: 这个机构其实在要回工资的时候可能比较给力, 可以先给它打电话投诉公司, 让它给公司施压, 然后再去仲裁。
  • 个人所得税投诉: 这个渠道就是比较伤财务, 如果财务话语权比较大的话, 没准就能追回工资,因为财务不想折腾,但我这个公司, 财务没啥话语权,所以财务按税务局的要求折腾了一两天, 然后把我的个人所得税没发工资的那几个月给更新成功0了,然后会在收到工资后再给新的报税。
  • 掌上12333 国务院客户端小程序 =>更多 => 投诉 => 全国根治欠薪线索反映平台: 本来我以为这个渠道会比较强势, 但非常可惜, 啥用没有, 可能我傻的只写了自己的欠薪, 没写其他同事的, 数额较小, 没引起注意, 等到我仲裁都结束了好像半个月还是一个月还是更久? 才给我来了个电话。

抖音上看到有个能快速结束仲裁, 然后去法院起诉进行一审的方法: 在申请书上写: 申请枪毙无良老板! 然后这个会被仲裁委驳回, 然后请求仲裁委出具不予受理通知, 拿着这个就可以去起诉公司。这个的话大家纯粹当爽文看吧, 应该不太具有实施成功的可能性。

一点点走心之谈

  • 如果公司刚开始拖欠一个月两个月, 别想了, 赶紧去劳动仲裁, 只要申请的诉求中每个单项金额不超过当地月平均工资标准的12个月的金额, 那就可以触发一裁终审, 公司无权再上诉,所以这个只适用于拖欠前期和金额较小时。
  • 如果公司拖欠工资, 而你决定去仲裁了,那就别犹犹豫豫的了, 赶紧给公司发一个被动解除劳动通知书, 走邮政和单位邮箱, 这样可以申请到n的劳动补偿, 而我就是因为没有这一步, 没有听取辣条(热心群友)的建议, 导致这条申请诉求被仲裁委支持的概率比较小, 所以选择了庭前调解。诸君引以为鉴。
  • 坚持理性,不要感性。
  • 说一下最开始说的关于欠薪时长的乌龙, 这个问题其实就是, 我, 搞错了公司拖欠我薪资的时长!怎么说呢? 就是公司发放薪资是在本月的15-20号左右发放上个月的薪资, 也就是说2023.6.6号离职, 我这边的工资应该6.20号发放的5月份工资和7月份发放的6.1-6.6的工资, 而我当时算的是按每月收入算的, 也就是5.20号收到一个月的工资, 6.20号我这边收到的应该是只有6天的薪资, 我不知道当时我为啥这样想...,最后是怎么发现的呢? 是在调解后期人事给的调解方案里发现我少算了一个月的薪资。
  • 关于拖欠薪资的日期。所谓百足之虫死而不僵, 有的公司拖欠薪资不是拖欠后就一直不给了, 而是断断续续的给, 拖欠一个月, 给你半个月, 拖欠一个月, 给你一个月, 继续拖, 继续给点, 我前公司就是这样, 断断续续给点, 前前后后欠一年多的时间了总共欠了4.5个月。现在想想, 我是怎么撑下来的?!

相关资源, 以了解仲裁

  • 我有买一本书, 《劳动争议仲裁诉讼实战宝典》,但这本书我还没看完我就结束仲裁了, 但, 它确实不错, 至少可以让对仲裁不太了解的人有个大概的了解。
  • 现在有很多的AI应用, 大家可以尝试一下,这个我也不太了解, 大家可以补充一下。
  • 抖音或b站看劳动仲裁相关的视频, 我有看的是叫晨辉律师

致谢辣条

非常感谢神奇的程序员@大白群里的不正经网友->辣条, 之前群里聊的时候在我说了公司拖欠工资后他就让我走被动离职这条路, 但当时他给我的印象是不太正经的精神小伙, 所以我没听他的, 就正常离职, 自己写了离职申请, 这是让我后来在仲裁期间非常后悔的事。 仲裁的周期比较长, 我也比较迷茫, 他给了我很多帮助, 期间我很多次都想着放弃吧, 接受吧,能拿回基本工资就不错了, 是辣条帮我坚定了“道心”,在此, 真的非常感谢辣条佬。

2023, 拜拜~

写下这篇文章, 给2023画一个完美的句号。

2023.12.31


作者:掘金沸点顶流
来源:juejin.cn/post/7318446251631493171

收起阅读 »

我的天!多个知名组件库都出现了类似的bug!

web
前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design, 字节系:arco design 腾讯...
继续阅读 »

前言


首先声明,没有标题党哈!


以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


Affix组件是什么,以及bug复现


Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:


image.png


这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:


image.png


如何复现bug


你在这个button元素任意父元素上,加上以下任意style属性



  • will-change: transform;

  • will-change: filter;

  • will-change: perspective;

  • transform 不为none

  • perspective不为none

  • 非safari浏览器,filter属性不为none

  • 非safari浏览器,backdrop-filter属性不为none

  • 等等


都可以让这个固定组件失效,就是原本是距离顶部80px固定。


我的组件库没有这个bug,哈哈


mx-design


目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


bug原因


affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


真正的规则如下(以下说的包含块就是fixed布局的定位父元素):



  1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:



    1. transform 或 perspective 的值不是 none

    2. will-change 的值是 transform 或 perspective

    3. filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。

    4. contain 的值是 paint(例如:contain: paint;

    5. backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);




评论区有很多同学居然觉的这不是bug?


其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


解决方案



  • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值

  • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了


具体代码如下:



  • offsetParent固定元素的定位上下文,也就是相对定位的父元素

  • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定



affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

如何找出offsetParent,也就是定位上下文


export function getContainingBlock(element: Element) {
let currentNode = element.parentElement;
while (currentNode) {
if (isContainingBlock(currentNode)) return currentNode;
currentNode = currentNode.parentElement;
}
return null;
}

工具方法,isContainingBlock如下:


import { isSafari } from './isSafari';

export function isContainingBlock(element: Element): boolean {
const safari = isSafari();
const css = getComputedStyle(element);

// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
(!safari && (css.filter ? css.filter !== 'none' : false)) ||
['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
);
}


本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


作者:孟祥_成都
来源:juejin.cn/post/7265121637497733155
收起阅读 »

到了2038年时间戳溢出了怎么办?

计算机中的时间 看完这篇文章相信你会对计算机中的时间有更系统全面的认识。 我经常自嘲,自己写的程序运行不超过3年,因为大部分项目方就早早跑路了。大多数项目上线后,你跟这个项目就再无瓜葛,关于时间你只需要保证时区正确就不会有太大问题,哈哈。 但是今天我想认真对待...
继续阅读 »

计算机中的时间


看完这篇文章相信你会对计算机中的时间有更系统全面的认识。


我经常自嘲,自己写的程序运行不超过3年,因为大部分项目方就早早跑路了。大多数项目上线后,你跟这个项目就再无瓜葛,关于时间你只需要保证时区正确就不会有太大问题,哈哈。 但是今天我想认真对待时间这个问题,作为一个库作者或基础软件作者,就需要考虑下游项目万一因为你处理时间不当而造成困扰,影响范围就比较广了。


计算机中与时间有关的关键词:


时间类型
时间戳(timestamp
定时器(例如jssetInterval())
时间计算
时间段
超时(setTimeout())
时间片
GMT
UTC
Unix时间戳
ISO8601
CST
EST

看到这些你可能会疑惑,为何一个时间竟然如此复杂!!


如果下面的问题你都能答上来,那这篇文章对你的帮助微乎其微,不如做些更有意义的事情。



  • 常用的时间格式,他们都遵循哪些标准?

  • 什么是GMT?

  • 什么是UTC?

  • GMT UTC 和ISO8601有什么区别?

  • RFC5322是什么?

  • RFC5322 采用的是GMT还是UTC?

  • ISO8601 使用的是UTC还是GMT?

  • 在ISO8601中 Z可以使用+00:00表示吗?

  • UTC什么时候校准?

  • CST是东八区吗?

  • Z是ISO 8601规定的吗,为什么是Z?

  • 时区划分是哪个标准定义的?

  • 为什么是1970年1月1日呢?

  • 到了2038年时间戳溢出了怎么办?

  • 计算机中时间的本质是一个long类型吗?

  • WEB前后端用哪个格式传输好?

  • '2024-01-01T24:00:00' 等于 '2024-01-02T00:00:00' ??



正文开始


1. 两种时间标准


UTC和GMT都是时间标准,定义事件的精度。它们只表示 零时区 的时间,本地时间则需要与 时区 或偏移 结合后表示。这两个标准之间差距通常不会超过一秒。


UTC(协调世界时)


UTC,即协调世界时(Coordinated Universal Time),是一种基于原子钟的时间标准。它的校准是根据地球自转的变化而进行的,插入或删除闰秒的实际需求在短期内是难以预测的,因此这个决定通常是在需要校准的时候发布。 闰秒通常由国际电信联盟(ITU) 和国际度量衡局(BIPM) 等组织进行发布。由国际原子时(International Atomic Time,TAI) 通过闰秒 的调整来保持与地球自转的同步。


GMT(格林尼治标准时间)


以英国伦敦附近的格林尼治天文台(0度经线,本初子午线)的时间为基准。使用地球自转的平均速度来测量时间,是一种相对于太阳的平均时刻。尽管 GMT 仍然被广泛使用,但现代科学和国际标准更倾向于使用UTC。


2. 两种显示标准


上面我们讨论的时间标准主要保证的是时间的精度,时间显示标准指的是时间的字符串表示格式。我们熟知的有 RFC 5322 和 ISO 8601。


RFC 5322 电子邮件消息格式的规范


RFC 5322 的最新版本是在2008年10月在IETF发布的,你阅读时可能有了更新的版本。



RFC 5322 是一份由 Internet Engineering Task Force (IETF) 制定的标准,定义了 Internet 上的电子邮件消息的格式规范。该标准于2008年发布,是对之前的 RFC 2822 的更新和扩展。虽然 RFC 5322 主要关注电子邮件消息的格式,但其中的某些规范,比如日期时间格式,也被其他领域采纳,例如在 HTTP 协议中用作日期头部(Date Header)的表示。



格式通常如下:


Thu, 14 Dec 2023 05:36:56 GMT

时区部分为了可读可以如下表示:


Thu, 14 Dec 2023 05:36:56 CST
Thu, 14 Dec 2023 05:36:56 +0800
Thu, 14 Dec 2023 05:36:56 +0000
Thu, 14 Dec 2023 05:36:56 Z

但并不是所有程序都兼容这种时区格式,通常程序会忽略时区,在写程序时要做好测试。标准没有定义毫秒数如何显示。


需要注意的是,有时候我们会见到这种格式Tue Jan 19 2038 11:14:07 GMT+0800 (中国标准时间),这是js日期对象转字符串的格式,它与标准无关,千万不要混淆了。


ISO 8601


ISO 8601 最新版本是 ISO 8601:2019,发布日期为2019年11月15日,你阅读时可能有了更新的版本。


下面列举一些格式示例:


2004-05-03T17:30:08+08:00
2004-05-03T17:30:08+00:00
2004-05-03T17:30:08Z
2004-05-03T17:30:08.000+08:00

标准并没有定义小数位数,保险起见秒后面一般是3位小数用来表示毫秒数。 字母 "Z" 是 "zero"(零)的缩写,因此它被用来表示零时区,也可以使用+00:00,但Z更直观且简洁。



  1. 本标准提供两种方法来表示时间:一种是只有数字的基础格式;第二种是添加了分隔符的扩展格式,更易读。扩展格式使用连字符“-”来分隔日期,使用冒号“:”来分隔时间。比如2009年1月6日在扩展格式中可以写成"2009-01-06",在基本格式中可以简单地写成"20090106"而不会产生歧义。 若要表示前1年之前或9999年之后的年份,标准也允许有共识的双方扩展表达方式。双方应事先规定增加的位数,并且年份前必须有正号“+”或负号“-”而不使用“。依据标准,若年份带符号,则前1年为"+0000",前2年为"-0001",依此类推。

  2. 午夜,一日的开始:完全表示为000000或00:00:00;仅有小时和分表示为0000或00:00

  3. 午夜,一日的终止:完全表示为240000或24:00:00;仅有小时和分表示为2400或24:00

  4. 如果时间在零时区,并恰好与UTC相同,那么在时间最后加一个大写字母Z。Z是相对协调世界时时间0偏移的代号。 如下午2点30分5秒表示为14:30:05Z或143005Z;只表示小时和分,为1430Z或14:30Z;只表示小时,则为14Z或14Z。

  5. 其它时区用实际时间加时差表示,当时的UTC+8时间表示为22:30:05+08:00或223005+0800,也可以简化成223005+08。


日期与时间合并表示时,要在时间前面加一大写字母T,如要表示东八区时间2004年5月3日下午5点30分8秒,可以写成2004-05-03T17:30:08+08:00或20040503T173008+08。


在编写API时推荐使用ISO 8601标准接收参数或响应结果,并且做好时区测试,因为不同编程语言中实现可能有差异。


时区划分和偏移



全球被分为24个时区,每个时区对应一个小时的时间差。 时区划分由IANA维护和管理,其时区数据库被称为 TZ Database(或 Olson Database)。这个数据库包含了全球各个时区的信息,包括时区的名称、标识符、以及历史性的时区变更数据,例如夏令时的开始和结束时间等。在许多操作系统(如Linux、Unix、macOS等)和编程语言(如Java、Python等)中得到广泛应用。


TZ Database具体见我整理的表格,是从Postgresql中导出的一份Excel,关注公众号"程序饲养员",回复"tz"



时区标识符采用"洲名/城市名"的命名规范,例如:"America/New_York"或"Asia/Shanghai"。这种命名方式旨在更准确地反映时区的地理位置。时区的具体规定和管理可能因国家、地区、或国际组织而异。


有一些时区是按照半小时或15分钟的间隔进行偏移的,以适应地理和政治需求。在某些地区,特别是位于边界上的地区,也可能采用不同的时区规则。


EST,CST、GMT(另外一个含义是格林尼治标准时间)这些都是时区的缩写。


这种简写存在重复,如CST 可能有多种不同的含义,China Standard Time(中国标准时间),它对应于 UTC+8,即东八区。Central Standard Time(中部标准时间) 在美国中部标准时间的缩写中也有用。中部标准时间对应于 UTC-6,即西六区。因此在某些软件配置时不要使用简称,一定要使用全称,如”Asia/Shanghai“。


采用东八区的国家和地区有哪些



  • 中国: 中国标准时间(China Standard Time,CST)是东八区的时区,对应于UTC+8。

  • 中国香港: 中国香港也采用东八区的时区,对应于UTC+8。

  • 中国澳门: 澳门也在东八区,使用UTC+8。

  • 中国台湾: 台湾同样在东八区,使用UTC+8。

  • 新加坡: 新加坡位于东八区,使用UTC+8。

  • 马来西亚: 马来西亚的半岛部分和东马来西亚位于东八区,使用UTC+8。

  • 菲律宾: 菲律宾采用东八区的时区,对应于UTC+8。


计算机系统中的时间 —— Unix时间戳


Unix时间戳(Unix timestamp)定义为从1970年01月01日00时00分00秒(UTC)起至现在经过的总秒数(秒是毫秒、微妙、纳秒的总称)。


这个时间点通常被称为 "Epoch" 或 "Unix Epoch"。时间戳是一个整数,表示从 Epoch 开始经过的秒数。


一些关键概念:



  1. 起始时间点: Unix 时间戳的起始时间是 1970 年 1 月 1 日 00:00:00 UTC。在这一刻,Unix 时间戳为 0。

  2. 增量单位: Unix 时间戳以秒为单位递增。每过一秒,时间戳的值增加 1。

  3. 正负值: 时间戳可以是正值或负值。正值表示从 Epoch 开始经过的秒数,而负值表示 Epoch 之前的秒数。

  4. 精度: 通常情况下,Unix 时间戳以整数形式表示秒数。有时也会使用浮点数表示秒的小数部分,以提供更精细的时间分辨率。精确到秒是10位;有些编程语言精确到毫秒是13位,被称为毫秒时间戳。


为什么是1970年1月1日?


这个选择主要是出于历史和技术的考虑。


Unix 操作系统的设计者之一,肯·汤普森(Ken Thompson)和丹尼斯·里奇(Dennis Ritchie)在开发 Unix 操作系统时,需要选择一个固定的起始点来表示时间。1970-01-01 00:00:00 UTC 被选为起始时间。这个设计的简洁性和通用性使得 Unix 时间戳成为计算机系统中广泛使用的标准方式来表示和处理时间。


时间戳为什么只能表示到2038年01月19日03时14分07秒?


在许多系统中,结构体time_t 被定义为 long,具体实现取决于编译器和操作系统的架构。例如,在32位系统上,time_t 可能是32位的 long,而在64位系统上,它可能是64位的 long。 32位有符号long类型,实际表示整数只有31位,最大能表示十进制2147483647(01111111 11111111 11111111 11111111)。


> new Date(2147483647000)
< Tue Jan 19 2038 11:14:07 GMT+0800 (中国标准时间)

实际上到2038年01月19日03时14分07秒,便会到达最大时间,过了这个时间点,所有32位操作系统时间便会变为10000000 00000000 00000000 00000000。因具体实现不同,有可能会是1901年12月13日20时45分52秒,这样便会出现时间回归的现象,很多软件便会运行异常了。


至于时间回归的现象相信随着64为操作系统的产生逐渐得到解决,因为用64位操作系统可以表示到292,277,026,596年12月4日15时30分08秒。


另外,考虑时区因素,北京时间的时间戳的起始时间是1970-01-01T08:00:00+08:00。


好了,关于计算机中的时间就说完了,有疑问评论区相见 或 关注 程序饲养员 公号。



作者:程序饲养员
来源:juejin.cn/post/7312640704404111387
收起阅读 »

Arrays.asList() 隐藏的陷阱,你避开了吗?

[Arrays.asList()方法介绍] [Arrays.asList()方法的坑] [解决Arrays.asList()方法的坑] [总结] [Arrays.asList()方法介绍] [Arrays.asList()方法的坑] [解决Arrays.asL...
继续阅读 »

  • [Arrays.asList()方法介绍]

  • [Arrays.asList()方法的坑]

  • [解决Arrays.asList()方法的坑]

  • [总结]

  • [Arrays.asList()方法介绍]

  • [Arrays.asList()方法的坑]

  • [解决Arrays.asList()方法的坑]

  • [总结]




在Java中,我们经常需要将数组转换为List来方便地进行操作。Arrays.asList()方法是一种常见的方式,但是它存在一个不太常见但需要注意的坑。


本文将深入探讨Arrays.asList()的使用,揭示其中的陷阱,并提供解决方案。


[Arrays.asList()方法介绍]


Arrays.asList()方法是将数组转换为List的方法,它返回一个List对象,但这个List对象并不是java.util.ArrayList对象,而是Arrays内部的ArrayList对象。


Arrays.ArrayList类继承自AbstractList,实现了List接口。它重写了add()remove()等修改List结构的方法,并将它们直接抛出UnsupportedOperationException异常,从而禁止了对List结构的修改。


具体来说,Arrays.asList()方法返回的是Arrays类中的一个私有静态内部类ArrayList,它继承自AbstractList类,实现了List接口。


Arrays.asList()方法的使用非常简单,只需要将一个数组作为参数传递给该方法即可。例如:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);


基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能




[Arrays.asList()方法的坑]


尽管Arrays.asList()方法很方便,但也存在一些坑,其中最常见的一个是:在使用Arrays.asList()方法时,如果对返回的List对象进行修改(例如增加、删除元素),将会抛出"UnsupportedOperationException"异常。


为什么会出现这个异常呢?这是因为Arrays.asList()方法返回的List对象,是一个固定大小的List,不能进行结构上的修改,否则会抛出异常。


下面的代码演示了这个问题:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);
list.add("d"); // 抛出 UnsupportedOperationException 异常

上述代码中,我们尝试向List对象中添加一个新的元素"d",结果会抛出"UnsupportedOperationException"异常。



基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能




[解决Arrays.asList()方法的坑]


要解决Arrays.asList()方法的坑,我们需要将返回的List对象转换为一个可修改的List对象。有几种方法可以实现这个目标:


[方法一:使用java.util.ArrayList类]


我们可以使用java.util.ArrayList类,将Arrays.asList()方法返回的List对象转换为一个java.util.ArrayList对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
list.add("d"); // 正常运行

上述代码中,我们首先使用Arrays.asList()方法将一个数组转换为一个List对象,然后使用ArrayList的构造方法,将这个List对象转换为一个java.util.ArrayList对象,最后可以向这个ArrayList对象中添加元素。


[方法二:使用Collections类]


我们也可以使用Collections类提供的静态方法,将Arrays.asList()方法返回的List对象转换为一个可修改的List对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
Collections.addAll(list, "d"); // 正常运行

通过Collections.addAll()方法,我们可以将数组中的元素逐个添加到一个新的ArrayList对象中,从而实现了可修改性。


[总结]


在使用Arrays.asList()方法时,需要注意返回的List对象是一个固定大小的List,不支持结构上的修改操作。为了避免这个陷阱,我们可以使用java.util.ArrayList或Collections类提供的方法将返回的List对象转换为可修改的List。通过了解这个陷阱并采取相应的解决方案,我们可以安全地将数组转换为List,并避免潜在的异常情况。


不要让Arrays.asList()的陷阱坑了你的代码!


在Java中,我们经常需要将数组转换为List来方便地进行操作。Arrays.asList()方法是一种常见的方式,但是它存在一个不太常见但需要注意的坑。本文将深入探讨Arrays.asList()的使用,揭示其中的陷阱,并提供解决方案。


[Arrays.asList()方法介绍]


Arrays.asList()方法是将数组转换为List的方法,它返回一个List对象,但这个List对象并不是java.util.ArrayList对象,而是Arrays内部的ArrayList对象。


Arrays.ArrayList类继承自AbstractList,实现了List接口。它重写了add()remove()等修改List结构的方法,并将它们直接抛出UnsupportedOperationException异常,从而禁止了对List结构的修改。


具体来说,Arrays.asList()方法返回的是Arrays类中的一个私有静态内部类ArrayList,它继承自AbstractList类,实现了List接口。


Arrays.asList() 方法的使用非常简单,只需要将一个数组作为参数传递给该方法即可。例如:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);

[Arrays.asList()方法的坑]


尽管Arrays.asList()方法很方便,但也存在一些坑,其中最常见的一个是:在使用Arrays.asList()方法时,如果对返回的List对象进行修改(例如增加、删除元素),将会抛出"UnsupportedOperationException"异常。


为什么会出现这个异常呢?这是因为Arrays.asList()方法返回的List对象,是一个固定大小的List,不能进行结构上的修改,否则会抛出异常。


下面的代码演示了这个问题:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);
list.add("d"); // 抛出 UnsupportedOperationException 异常

上述代码中,我们尝试向List对象中添加一个新的元素"d",结果会抛出"UnsupportedOperationException"异常。


[解决Arrays.asList()方法的坑]


要解决Arrays.asList()方法的坑,我们需要将返回的List对象转换为一个可修改的List对象。有几种方法可以实现这个目标:


[方法一:使用java.util.ArrayList类]


我们可以使用java.util.ArrayList类,将Arrays.asList()方法返回的List对象转换为一个java.util.ArrayList对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
list.add("d"); // 正常运行

上述代码中,我们首先使用Arrays.asList()方法将一个数组转换为一个List对象,然后使用ArrayList的构造方法,将这个List对象转换为一个java.util.ArrayList对象,最后可以向这个ArrayList对象中添加元素。


[方法二:使用Collections类]


我们也可以使用Collections类提供的静态方法,将Arrays.asList()方法返回的List对象转换为一个可修改的List对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
Collections.addAll(list, "d"); // 正常运行

通过Collections.addAll()方法,我们可以将数组中的元素逐个添加到一个新的ArrayList对象中,从而实现了可修改性。


[总结]


在使用Arrays.asList()方法时,需要注意返回的List对象是一个固定大小的List,不支持结构上的修改操作。为了避免这个陷阱,我们可以使用java.util.ArrayList或Collections类提供的方法将返回的List对象转换为可修改的List。通过了解这个陷阱并采取相应的解决方案,我们可以安全地将数组转换为List,并避免潜在的异常情况。


不要让Arrays.asList()的陷阱坑了你的代码!


作者:智多星云
来源:juejin.cn/post/7258863572553302071
收起阅读 »

Android 粒子漩涡动画

前言 粒子动画经常用于大画幅的渲染效果,实际上难度并不高,但是在使用粒子动画时,必须要遵循的一些要素,主要是: 起点 矢量速度 符合运动学公式 在之前的文章中,如《烟花效果》和《粒子喷泉》是典型的粒子动画,起点之所以重要是因为其实位置决定粒子出现的位置,矢...
继续阅读 »

前言


粒子动画经常用于大画幅的渲染效果,实际上难度并不高,但是在使用粒子动画时,必须要遵循的一些要素,主要是:



  • 起点

  • 矢量速度

  • 符合运动学公式


在之前的文章中,如《烟花效果》和《粒子喷泉》是典型的粒子动画,起点之所以重要是因为其实位置决定粒子出现的位置,矢量速度则决定了快慢和方向,运动学公式属于粒子动画的一部分,当然不是物理性的,毕竟平面尺寸也就那么长,这里的物理学公式使得画面更加丝滑而无跳动感觉。


本篇将实现下面的效果


fire_90.gif
注意:gif图有些卡,实际上流畅很多


本篇效果实现


本篇效果是无数圆随机产生然后渐渐变大并外旋,另外也有雨滴,这里的雨滴相对简单一些。


首先定义粒子对象


定义粒子对象是非常重要的,绝大部分倾下粒子本身就是需要单独控制的,因为每个粒子的轨迹都是有所差别的。


定义圆圈粒子


private static class Circle {
float x;
float y;
int color;

float radius;

Circle(float x, float y, float radius) {
reset(x, y, radius);
}

private void reset(float x, float y, float radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = Color.rgb((int) (Math.random() * 256), (int) (Math.random() * 256), (int) (Math.random() * 256));
}
}

定义雨滴


private static class RainDrop {
float x;
float y;

RainDrop(float x, float y) {
this.x = x;
this.y = y;
}
}

定义粒子管理集合


private ArrayList mParticles;
private ArrayList mRainDrops;
private long mLastUpdateTime; //记录执行时间

生成粒子对象



  • 生成雨滴是从顶部屏幕意外开始,而y = -50f值是雨滴的高度决定。

  • 圆圈是随机产生,在中心位置圆圈内。


// 创建新的雨滴
if (mRainDrops.size() < 80) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding / 2f * Math.random();
RainDrop drop = new RainDrop((float) x, -50f);
mRainDrops.add(drop);
}

// 创建新的粒子
if (mParticles.size() < 100) {
float x = (float) (getWidth() / 2f - radius + 2*radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );

Circle particle = new Circle(x, y,5);
mParticles.add(particle);
}

绘制雨滴


雨滴的绘制非常简单,调用相应的canvas方法即可


// 绘制雨滴
mPaint.setColor(Color.WHITE);
for (RainDrop drop : mRainDrops) {
canvas.drawLine(drop.x, drop.y, drop.x, drop.y + 20, mPaint);
}

// 绘制粒子
for (Circle particle : mParticles) {
mPaint.setColor(particle.color);
canvas.drawCircle(particle.x, particle.y, particle.radius, mPaint);
}

更新粒子位置


雨滴的更新相对简单,但是圆圈的旋转是一个难点,一个重要的问题是如何旋转粒子的,其实有很多方法,其中最笨的方法是旋转Canvas坐标系,底层有很多矩阵计算,但是这个似乎使用Math.atan2(y,x)显然更加方便,我们只需要在当前角度加上偏移量就能旋转。


float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.65f;

下面是完整的更新逻辑


// 更新雨滴位置
Iterator rainIterator = mRainDrops.iterator();
while (rainIterator.hasNext()) {
RainDrop drop = rainIterator.next();
if (drop.y > getHeight() + 50) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding * Math.random();

drop.x = (float) (x);
drop.y = -50;
} else {
drop.y += 20;
}

}

// 更新粒子位置
long currentTime = System.currentTimeMillis();
float deltaTime = (currentTime - mLastUpdateTime) / 1000f;
mLastUpdateTime = currentTime;

float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;

Iterator iterator = mParticles.iterator();
while (iterator.hasNext()) {
Circle particle = iterator.next();
float dx = particle.x - centerX;
float dy = particle.y - centerY;
float distance = (float) Math.sqrt(dx * dx + dy * dy) + 4.5f;// 增加偏移
float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.5f;
particle.radius += 1f;

particle.x = centerX + (float) Math.cos(angle) * distance;
particle.y = centerY + (float) Math.sin(angle) * distance;

if (particle.radius > 10) {
int maxRadius = 100;
float fraction = (particle.radius - 10) / (maxRadius - 10);
if (fraction >= 1) {
fraction = 1;
}
particle.color = argb((int) (255 * (1 - fraction)), Color.red(particle.color), Color.green(particle.color), Color.blue(particle.color));
}
if (Color.alpha(particle.color) == 0) {

float x = (float) (getWidth() / 2f - radius + 2* radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );
particle.reset(x,y, 5);
}

}

粒子刷新


其实刷新机制我们以前经常使用,调用postInvalidate即可,本身就是View自身的方法。


总结


本篇主要内容总体上就是这些,下面是全部代码逻辑


public class VortexView extends View {

private Paint mPaint;
private ArrayList mParticles;
private ArrayList mRainDrops;
private long mLastUpdateTime;
private int padding = 20;

public VortexView(Context context) {
super(context);
mPaint = new Paint();
mParticles = new ArrayList<>();
mRainDrops = new ArrayList<>();
mLastUpdateTime = System.currentTimeMillis();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float radius = Math.min(getWidth(), getHeight()) / 3f;

// 创建新的雨滴
if (mRainDrops.size() < 80) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding / 2f * Math.random();
RainDrop drop = new RainDrop((float) x, -50f);
mRainDrops.add(drop);
}

// 创建新的粒子
if (mParticles.size() < 100) {
float x = (float) (getWidth() / 2f - radius + 2*radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );

Circle particle = new Circle(x, y,5);
mParticles.add(particle);
}

// 绘制雨滴
mPaint.setColor(Color.WHITE);
for (RainDrop drop : mRainDrops) {
canvas.drawLine(drop.x, drop.y, drop.x, drop.y + 20, mPaint);
}

// 绘制粒子
for (Circle particle : mParticles) {
mPaint.setColor(particle.color);
canvas.drawCircle(particle.x, particle.y, particle.radius, mPaint);
}

// 更新雨滴位置
Iterator rainIterator = mRainDrops.iterator();
while (rainIterator.hasNext()) {
RainDrop drop = rainIterator.next();
if (drop.y > getHeight() + 50) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding * Math.random();

drop.x = (float) (x);
drop.y = -50;
} else {
drop.y += 20;
}

}

// 更新粒子位置
long currentTime = System.currentTimeMillis();
float deltaTime = (currentTime - mLastUpdateTime) / 1000f;
mLastUpdateTime = currentTime;

float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;

Iterator iterator = mParticles.iterator();
while (iterator.hasNext()) {
Circle particle = iterator.next();
float dx = particle.x - centerX;
float dy = particle.y - centerY;
float distance = (float) Math.sqrt(dx * dx + dy * dy) + 3.5f;// 增加偏移
float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.65f;
particle.radius += 1f;

particle.x = centerX + (float) Math.cos(angle) * distance;
particle.y = centerY + (float) Math.sin(angle) * distance;

if (particle.radius > 10) {
int maxRadius = 100;
float fraction = (particle.radius - 10) / (maxRadius - 10);
if (fraction >= 1) {
fraction = 1;
}
particle.color = argb((int) (255 * (1 - fraction)), Color.red(particle.color), Color.green(particle.color), Color.blue(particle.color));
}
if (Color.alpha(particle.color) == 0) {

float x = (float) (getWidth() / 2f - radius + 2* radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );
particle.reset(x,y, 5);
}

}

Collections.sort(mParticles, comparator);

// 使view无效从而重新绘制,实现动画效果
invalidate();
}
Comparator comparator = new Comparator() {
@Override
public int compare(Circle left, Circle right) {
return (int) (left.radius - right.radius);
}
};

public static int argb(
int alpha,
int red,
int green,
int blue)
{
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}

private static class Circle {
float x;
float y;
int color;

float radius;

Circle(float x, float y, float radius) {
reset(x, y, radius);
}

private void reset(float x, float y, float radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = Color.rgb((int) (Math.random() * 256), (int) (Math.random() * 256), (int) (Math.random() * 256));
}
}

private static class RainDrop {
float x;
float y;

RainDrop(float x, float y) {
this.x = x;
this.y = y;
}
}
}

作者:时光少年
来源:juejin.cn/post/7317957339012202496
收起阅读 »

百度考题:反复横跳的个性签名

web
浅聊一下 在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下” 当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签...
继续阅读 »

浅聊一下


在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下”


image.png


当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签名组件

动手


我们要有JS组件思维,个性签名是很多地方都可以用到的,我们把它做成组件,用到的时候直接搬过去就好了。所以我们将使用原生JS将组件封装起来(大家也可以使用VUE)


EditInPlace 类


我们要使用这个JS组件,首先得将其方法和参数封装在一个类里,再通过类的实例化对象来展示。


function EditInPlace(id,parent,value){
this.id = id;
this.parent = parent;
this.value =value || "这个家伙很懒,什么都没有留下";
this.createElements()//动态装配html结点
this.attachEvents();
}


  1. 将传入的idparentvalue参数赋值给新创建的对象的对应属性。

  2. 如果没有提供value参数,则将默认字符串"这个家伙很懒,什么都没有留下"赋值给新对象的value属性。

  3. 调用createElements方法来动态装配HTML节点。

  4. 调用attachEvents方法来附加事件。


EditInPlace.prototype


在 EditInPlace 类中,我们调用了createElements() attachEvents()两个方法,所以我们得在原型上定义这两个方法


createElements


    createElements:function(){
this.containerElement = document.createElement('div');
this.containerElement.id= this.id;
//签名文字部分
this.staicElement = document.createElement('span');
this.staicElement.innerText = this.value
this.containerElement.appendChild(this.staicElement);
//输入框
this.fieldElement = document.createElement('input')
this.fieldElement.type = 'text'
this.fieldElement.value = this.value;
this.containerElement.appendChild(this.fieldElement);
//save 确认
this.saveButton = document.createElement('input');
this.saveButton.type = 'button'
this.saveButton.value = '保存'
this.containerElement.appendChild(this.saveButton)
//取消按钮
this.cancelButton = document.createElement('input')
this.cancelButton.type = 'button'
this.cancelButton.value = '取消'
this.containerElement.appendChild(this.cancelButton)


this.parent.appendChild(this.containerElement)
this.converToText();

}


  1. 创建一个<div>元素,并将其赋值给this.containerElement属性,并设置其id为传入的id

  2. 创建一个<span>元素,并将其赋值给this.staicElement属性,然后设置其文本内容为传入的value

  3. this.staicElement添加到this.containerElement中。

  4. 创建一个<input>元素,并将其赋值给this.fieldElement属性,设置其类型为"text",并将其值设置为传入的value

  5. this.fieldElement添加到this.containerElement中。

  6. 创建一个保存按钮(<input type="button">),并将其赋值给this.saveButton属性,并设置其值为"保存"。

  7. 将保存按钮添加到this.containerElement中。

  8. 创建一个取消按钮(<input type="button">),并将其赋值给this.cancelButton属性,并设置其值为"取消"。

  9. 将取消按钮添加到this.containerElement中。

  10. this.containerElement添加到指定的父元素this.parent中。

  11. 调用converToText方法。


这个方法主要是用于动态生成包含静态文本、输入框和按钮的编辑组件,并将其添加到指定的父元素中。也就是说我们在这里主要就是创建了一个div,div里面有一个text和一个input,还有保存和取消按钮


div和span的显示


我们怎么样实现点击一下,就让text不显示,input框显示呢?
就是在点击一下以后,让text的display为'none',让input框和按钮为 'inline'就ok了,同样的,在保存或取消时采用相反的方法就好


    converToText:function(){
this.staicElement.style.display = 'inline';
this.fieldElement.style.display = 'none'
this.saveButton.style.display = 'none'
this.cancelButton.style.display = 'none'
},
converToEdit:function(){
this.staicElement.style.display = 'none';
this.fieldElement.style.display = 'inline'
this.saveButton.style.display = 'inline'
this.cancelButton.style.display = 'inline'
}

attachEvents


当然,我们需要在text文本和按钮上添加点击事件


    attachEvents:function(){
var that = this
this.staicElement.addEventListener('click',this.converToEdit.bind(this))
this.cancelButton.addEventListener('click',this.converToText.bind(this))
this.saveButton.addEventListener('click',function(){
var value = that.fieldElement.value;
that.staicElement.innerText = value;
that.converToText();
})
}


  1. 通过var that = this将当前对象的引用保存在变量that中,以便在后续的事件监听器中使用。

  2. 使用addEventListener为静态元素(this.staicElement)添加了一个点击事件的监听器,当静态元素被点击时,会调用this.converToEdit.bind(this)方法,这样做可以确保在converToEdit方法中this指向当前对象。

  3. 为取消按钮(this.cancelButton)添加了一个点击事件的监听器,当取消按钮被点击时,会调用this.converToText.bind(this)方法,同样也是为了确保在converToText方法中this指向当前对象。

  4. 为保存按钮(this.saveButton)添加了一个点击事件的监听器,当保存按钮被点击时,会执行一个匿名函数,该函数首先获取输入框的值,然后将该值更新到静态元素的文本内容中,并最后调用converToText方法,同样使用了变量that来确保在匿名函数中this指向当前对象。


通过这些事件监听器的设置,实现了以下功能:



  • 点击静态元素时,将编辑组件转换为编辑状态。

  • 点击取消按钮时,将编辑组件转换为静态状态。

  • 点击保存按钮时,获取输入框的值,更新静态元素的文本内容,并将编辑组件转换为静态状态。


HTML


在html中通过new将组件挂载在root上


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>就地编辑-EditIntPlace</title>
</head>
<body>
<div id="root"></div>
<script src="./editor_in_place.js"></script>
<script>
// 为了收集签名,给个表单太重了 不好看
// 个性签名,就地编辑
// 面向对象 将整体开发流程封装 封装成一个组件
new EditInPlace('editor',document.getElementById('root'))//名字 挂载点
</script>
</body>
</html>

完整效果



结尾


写的不太美观,掘友们可以自己试试加上样式,去掉按钮,再加上一张绝美的背景图片...


作者:滚去睡觉
来源:juejin.cn/post/7315730050576367616
收起阅读 »

前端实习生如何提升自己

web
这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。 同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习...
继续阅读 »

这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。


同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习生。


正常来讲一个资深的研发,最多可以辅导两个实习生,但因为我们是初创阶段,为了降低成本,我们经常会出现需要同时辅导三个以上实习生的情况。


到网上搜了一圈,都没有系统讲解实习生如何提升自己的资料,本文将结合自己过去十年带人的经验,面向前端岗位,谈一谈前端实习生如何提升自己。


作者简介


先后就读于北京交大和某军校,毕业后任职于原北京军区38集团军,带过两次新兵连,最多时管理过120人左右的团队。


退役后,先进入外企GEMALTO做操作系统研发,后进入搜狐、易车和美团等公司做大前端开发和团队管理,最多时管理过30多人的大前端团队,负责过几百个产品的前端开发工作,做过十多个大型复杂前端项目的架构工作。


目前在艺培行业创业,通过AIGC技术提升艺培机构门店运营和招生推广的效率和降低成本。


如何选择实习?


我永远相信,选择比努力奋斗更重要。选择一家合适的实习单位或公司,可以达到事半功倍的效果。


大公司的优势


毫无疑问,很多大学生都会选择大公司作为自己的实习单位,尤其是一些上市公司,甚至为了求得一个实习机会,即使倒贴钱也愿意。


与小企业相比,大公司一般都会有相对完整的企业制度。从签实习协议开始,如薪资结算、工作安排等,完全按照相关的企业制度,以及法律法规进行,这样一定程度有利于保障实习生的劳动权益。


与此相比,一些小企业甚至连实习协议都不签,这对于实习生来说是非常不利的。


大公司一般都会有日历表和日常的作息时刻表。因此,在大公司工作一般都按照正常的作息时间上下班,不太会出现加班的情况。


而相对来说,小企业由于刚刚起步,工作量非常大,人手常常不够,因此加班加点是家常便饭了。


每个大公司都会具有良好的、独特的企业文化,从办公室的装饰、到公司员工午休时的娱乐活动,无不体现出自己的特性。因此,在大公司里实习,你能够亲身感受到大公司内部的企业文化。


而小企业,则不太可能会有自己的企业文化,即使有多数也是非常糟糕的。


小企业的特点


在大公司实习,由于大公司里本身就人员冗余,再加上对“实习生”的不信任,因此他们给实习生安排的工作常属于打杂性质的。可能你在大公司里呆了两个月,可是连它最基本的业务流程都没弄明白。


而在小企业里,由此大多处于起步,没有太多的职位分化,你可能一人需要兼多职,许多事情都得自己解决,能够提高你独立解决问题的能力。


大公司一般有完整的部门体制,上下级关系相对严格分明,因此,官僚作风比较明显,新人难有发言权。


而在小企业,这方面的阻力则小得多,使新人少受束缚,更自由地施展自己的能力、表达自己的想法。


小结


实习为了给将来就业铺路,因此,实习最重要的在于经验。当然,在大公司实习如果能在增加自己进该公司的概率那固然好,但如果只是为了在简历里加上“在某大公司里实习过”的虚名,那实在是没有必要。


结合未来职业选择,要实习的公司和岗位是和你的职业理想直接相关的,这个岗位的实习和这个公司的进入可以给你日后的职业发展加分。


选你能在实习期间出成果的项目。如果你选了个很大的项目,半年还没做完,这就说不清楚是你没做好,还是项目本身的问题了。


实习的时候要更深入的了解企业的实际情况和问题,结合理论和实际,要给企业解决实际问题,有一个具体的成果,让企业真正感觉到你的项目做好了


我个人建议,在正式工作前,大公司和小企业,最好都能争取去实习一段时间,但第一份实习最好去大公司。


通过实习,希望大家能够验证自己的职业抉择,了解目标工作内容,学习工作及企业标准,找到自身职业的差距。


如何做好实习


选择好合适的实习单位,只是走出了万里长征的第一步,要做好实习,大家可以关注以下四个方面:


端正态度


从实习生个人的角度来看,多数人的出发点都是学习提升,但如果我们只是抱着纯学习而不为企业创造价值的心态的话,实习工作就不容易长久。


我们可以从企业的角度来看:



  • 一是实习提供了观察一位潜在的长期员工工作情况的方法,为企业未来发展培养骨干技术力量与领导人;

  • 二手有利于廉价劳动力争夺人才,刚毕业的学员便于管理,这样不仅能降低成本,还能提高企业的知名度,有利于企业长远发展。


实习生只有表现出了企业看重的那些方面,企业才会投入时间和精力去好好培养他,教一些真本领。


不管是在美团,还是我自己创业的公司,我都是基于相同的原则,去评估是否留下某个实习,进行重点培养。


我个人看重以下几点:



  • 不挑活:即使不是自己喜欢或在工作范围的任务,也能积极去完成;

  • 爱思考:接到任务或经过指导后,能及时反馈存在的问题和提升改进方案;

  • 善沟通:遇到困难及时寻求帮助,工作不开心了不封闭自己、不拒绝沟通。


不管当年我去实习还是上班,我的心态都是先为企业创造价值,在此基础上再争取完成自己的个人目标。


注重习惯


有的人毕业10年都还没毕业3年的人混得好,不是因为能力不行,而是因为他没有养成良好的职场习惯,以至于总是吃亏。


开发人员培养好的习惯至关重要。编写好的代码,持续学习,逐步提高代码质量。


主动反馈和同步信息


这个习惯非常重要,可以避免大家做很多无用功,大大提升大家日常工作的效率,而且也特别容易获得大家的好感与信任。


经常冒烟自测


对于自己负责的功能或产品,不管是测试阶段还是正式发布,只要需要其他人使用,都最好冒烟自测一遍,避免低级错误,进而影响大家对你专业度的信任。


良好的时间管理


先每天花几分钟确定好我今天要办的最重要的三到六件事情,如此能帮助提高整体的效率;把今天要做的所有事情一一写下来,按轻重缓急排序,可以按照要事第一的原则;每做完一件事情就把它划掉,在今天工作结束之前检查我是不是把整个list的工作都完成了。


避免重复的错误


不要犯了错误之后就草草了事,花一点时间深度挖掘清楚我犯错的底层原因是什么,再给自己一些小提示,比如做一个便利贴做提醒,在电脑桌面上面放一个警告,避免相同的错误重复发生。


构建知识体系


在信息爆炸的年代,碎片化的知识很多,系统学习的时间越来越少,如果没有自己的知识体系,很容易被淹没在知识的海洋中,而且难以记忆。


100分程序员推荐的做法,通过Wiki或者其他知识管理工具构建一个知识框架,大的分类可以包括软技能、架构、语言、前端、后端等,小的分类可以更细化。


培养大局观


程序员比较容易陷入的困境是专注于自己的一亩三分地,不关心团队的进度和业绩,也不关心软件的整体架构和其他模块。


这种状态长期下去没有好处,特别是在大公司中,逐渐成长为一颗螺丝钉。100分程序员会在工作之余,多看看其他在做什么,看看团队的整体规划,看看软件系统的架构和说明文档。


对自己的工作更理解,而且知道为什么这个产品应该这样设计,为什么领导应该这样做规划,这种大局观非常有利于自己的职业生涯。


技能提升


通过实习阶段,你才会知道专业知识在工作中的具体运用,才能切身体会专业知识的重要性,这样也让自己在以后的学习中更加的努力和勤奋和有所侧重的安排各科目的学习时间。


对于前端实习生的技能学习,除了打牢HTML、CSS和JS这些基础的前端知识以及熟练掌握vue、react这些框架开发技术栈,建议大家还需要选中自己感兴趣的前端技术领域进行持续深入的学习实践,具体请看我之前总结的文章:前端学哪些技能饭碗越铁收入还高


关系处理


大学生在实习过程中最难适应的就是人际关系,同时还要了解企业组织运行的特点、规章制度,了解企业文化对员工的影响等等,知道职场中和领导及同事该如何沟通,企业对员工有着什么样的需求等。


这些也只有在实习时,让自己处于团队之中,才能切身的体会和参与,只有这样大学生才会对社会生活有深刻的理解,对将来就业才有益处。


不论是对同事,还是对客户,还是对合作伙伴,都不要吝啬你的赞美之词。善于夸赞他人的员工,更容易获得他人的好评,有时还能让你成功避免一些不必要的麻烦。


当然,赞美他人,不要过于浮夸,要能说出赞美的理由,以及哪些是你表示认同的地方。比如,我们在称赞他人方案做得好的时候,不要只是说:“这方案不错”。


可以这么说:“这方案在市场推广方面的内容写得很好,逻辑清晰,数据充分,还能有同行的案例借鉴,十分值得我学习。”


要让别人感受到你是真心的在夸奖他,而不是讨好他。


我们常常很难控制激动时的情绪,发怒时更是。往往这个时候表达出的观点都是偏激的,事后再后悔就来不及了。


如果你与同事之间发生争执而发怒,请在这个时间段保持沉默。给自己找个地方安静一会儿,等到情绪稳定以后,再向他人表达你的观点,慢慢地,你就会更好地控制自己的情绪。


在职场中切忌过于情绪化,学会管理好自己的情绪是一个职场人必备的技能之一。坏情绪并不会帮助你解决任何问题,过多的抱怨只会使你成为负能量的传播者。


协作在软件开发中至关重要。一个好的开发人员应该能够与他人很好地合作,并了解如何有效地协作。这意味着对反馈持开放态度,积极寻求团队成员的意见,并愿意作为团队的一部分从事项目。


协作会带来更好的沟通、更高效的工作流程和更好的产品。


总结


社会实践对大学生的就业有着很大的促进作用,是大学生成功就业的前提和基础。


实习的目的因人而异,可以千差万别,而你实习的真正目的也只有自己才最清楚。只有在开始实习之前首先明确自己的目的,后面的路才会变得清晰明了。


作者:文艺码农天文
来源:juejin.cn/post/7319181229520568371
收起阅读 »

前端学哪些技能饭碗越铁收入还高

web
随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。 但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深...
继续阅读 »

随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。


但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深耕。


本文,我们就聊聊,掌握了哪些技能,能让前端同学,收入高且稳定。


端智能


首推的是端智能,很多行业大咖都认为,随着ChatGPT的横空出世,开启了第四次工业革命,很多产品都可以用大模型重做一遍。当前,我创业的方向,也和大模型有关。


当前的大模型主要还跑在云端,但云端的成本高,大模型的未来在端智能,这也是小米创始人雷军在今年一次发布会上提出的观点。


在2023年8月14日的雷军年度演讲中,雷军宣布小米已经在手机端跑通13亿参数的大模型,部分场景效果媲美云端。


目前,端上大模型的可行性和前景已经得到了业内的普遍认可,国内外各个科技大厂在大模型的端侧部署领域均开始布局,目前大量工程已在PC端、手机端实现大模型的离线部署,更有部分App登陆应用商店,只需下载即可畅通无阻地对话。


我们相信,在不久的将来,端上大模型推理将会成为智能应用的重要组成部分,为用户带来更加便捷、智能的体验。


我在美团从零研发了web端智能推理引擎,当时立项时,就给老板画饼,美团每天的几百亿推理调用,如果有一半用端智能替代的话,每年能为公司节省上亿元。


要想掌握端智能,需要学习深度学习的基本知识,还要掌握图形学和C++编程,通过webgl或webassembly 技术实现在Web端执行深度学习算法。


图形学


前面提到的端智能,只是涉及到了图形学中的webgl计算,但图形学的未来在元宇宙,通过3D渲染,实现VR、AR、XR等各种R。


计算机图形学是一门快速发展的领域,涵盖了三维建模、渲染、动画、虚拟现实等众多技术和应用。在电影、广告、游戏等领域中,计算机图形学的应用已经非常广泛。


熟练使用threejs开发各种3D应用,只能算是入门。真正的图形学高手,不仅可以架构类似3D家装软件的大型应用,而且能掌握渲染管线的底层原理,熟练掌握各种模型格式和解决各种软件,进行模型转换遇到的各种兼容问题。


随着计算机硬件和算法的不断进步,计算机图形学正迎来新的发展趋势。


首先是实时渲染与逼真度提升



  • 实时渲染技术:随着游戏和虚拟现实的兴起,对实时渲染的需求越来越高。计算机图形学将继续致力于研发更高效的实时渲染算法和硬件加速技术,以实现更逼真、流畅的视觉效果。

  • 光线追踪与全局照明:传统的实时渲染技术在光照模拟方面存在挑战。计算机图形学将借助光线追踪等技术,实现更精确的全局照明效果,提升场景的真实感和细节表现。


其次是虚拟与增强现实的融合



  • 混合现实技术:计算机图形学将与传感器技术、机器视觉等相结合,推动虚拟现实与增强现实的融合发展。通过实时感知和交互,用户可以在真实世界中与虚拟对象进行互动,创造更沉浸式的体验。

  • 空间感知与虚拟对象定位:计算机图形学将致力于解决空间感知和虚拟对象定位的挑战。利用深度学习、摄像头阵列等技术,实现高精度的空间感知和虚实融合,为虚拟与增强现实应用带来更自然、精确的交互方式。


再次是计算机图形学与人工智能的融合



  • 生成对抗网络(GAN)在图形生成中的应用:GAN等人工智能技术为计算机图形学带来了新的创作手段。通过训练模型生成逼真的图像和场景,计算机图形学能够更便捷地创建大量内容,并提供个性化的用户体验。

  • 计算机图形学驱动的虚拟人物与角色生成:结合计算机图形学和人工智能技术,研究人员正在努力开发高度逼真的虚拟人物和角色生成方法。这将应用于游戏、影视等领域,带来更具情感表达和交互性的虚拟角色。


最后是可视化分析与科学研究。



  • 一是大数据可视化:随着大数据时代的到来,计算机图形学在可视化分析方面扮演着关键角色。通过创新的可视化方法和交互技术,研究人员能够更深入地理解和分析庞大而复杂的数据集,揭示潜在的模式和趋势。

  • 二是科学数据可视化:计算机图形学在科学研究中的应用也日益重要。通过将科学数据转化为可视化形式,研究人员能够更直观地理解复杂的数据模式和关系,加快对科学问题的洞察和发现。这种可视化分析有助于领域如天文学、生物学、气象学等的研究进展。


工程提效


其实,过去三年我在美团的工作,至少有一半的精力是做和工程提效相关的事情。当然,也做了降本的事情,从零搭建外包团队。


就像我之前总结的文章:我在美团三年的前端工程化实践回顾 中提到那样,前端工程提效,一般会按照工具化、标准化、平台化和体系化进行演进。


相比前面的端智能和图形学,除了建设低代码平台和WebIDE有点技术难度,其他更多需要的是沟通、整合资源的能力,既要有很强的项目管理能力,又要人产品思维。


前两个方向做好了我们一般称为技术专家,而工程提效则更偏管理者,未来可以成为高管或自己创业。


总结


端智能和图形学,我在美团都尝试过进行深耕,但自己性格外向,很难坐得住。工程提效做得也一般,主要是因为需要换城市而换了部门,没有机缘继续推进,在美团很难往上走,所以只能尝试自己创业。


当前我创业的公司,也需要用到很多端智能的技术,比如我们使用yolov8实现了在web端进行智能抠图(后面计划使用segment anything大模型实现);也需要用到很多图形学的技能,比如开发3D美术馆为艺培机构招生引流,通过3D展示火箭、太阳系、空间站等,提升教学效果。


未来,我们需要招聘很多端智能及图形学方向的技术人才,欢迎大家加入。


作者:三一习惯
来源:juejin.cn/post/7310143510103064585
收起阅读 »

从技术到生活,还是那个热烈的少年 | 2023年终总结

前言   2023最后一天了,围炉煮茶,余烟缭绕,似时间线一般回游。 工作上   今年工作上迎接了新老技术的更迭,项目的整体大版本重构,以及对整个前端的工程化梳理。 去年3.0版本才用的是Webpack4.0+Vue2.0+Element+Echarts构建的...
继续阅读 »

前言


  2023最后一天了,围炉煮茶,余烟缭绕,似时间线一般回游。


工作上


  今年工作上迎接了新老技术的更迭,项目的整体大版本重构,以及对整个前端的工程化梳理。
去年3.0版本才用的是Webpack4.0+Vue2.0+Element+Echarts构建的前端项目,今年已经采用Webpack5.0+Vue3.0+Antdesgin+AntV重构了整个项目,以及搭建起了完善的前端生态系统,全系采用V3的tsx(ts+jsx写法)开发。



  • 制定了前端开发模式、开发语言、风格一致化

  • 制定了git提交规范,代码错误提交自动校验

  • 开发了公司的npm库,统一nrm源

  • 开启了前端公共组件库

  • 微前端模式实践


不知道是不是年纪大了的缘故,感觉现在无论遇到什么问题,都感觉稀松平常、波澜不惊,以前的自己总想着又解决了一个难题,有很多想说的,想聊的。现在想去记录,反而感觉问题都太简单,似乎没什么值得记录的,可是遇到难题翻来覆去、左思右想确切经历过,只是现在云淡风轻了,可能是我变懒了,也有可能这就是“技术成长”变为“技术成熟”阶段了吧。


生活上


对于生活,我似乎想讲的更多了,我想去更多的地方、见不同的人、呼吸不同的空气、享受不一样的美食,儿时的那颗好奇心好像回来了


今年我去了很多地方,忽入人间仙境的的海外仙岛、不经人手的隐市山林、悠闲惬意的大城小镇、烟雨相陪的红色起源...下面简单的写几个近期去过的地方


第一站,苏州西山岛,国庆之后,已是深秋,虽已深秋但初入冬的西山岛仍是香气氤氲,让人惊叹不已,从未到过的人,是体会不到这里的冬天竟是那样的美。大大小小的岛屿群居在这片湖泊之上,弯曲蔓延的公路是彼此之间的纽带,一座座大大小小的桥梁是彼此紧握的友谊小手,交错分布的小镇是最美的点缀。



“舟行碧波上,人在画中游”
image.png




夕阳下的西山岛,竟给人一种长河落日圆的感觉,是如此的的凄美,让人心生惋惜,西山岛有两个世界,一个在天上一个在水里,海天之间或许就是“长久”
image.png



第二站,南通濠河风景区,这里给人的第一感觉就是大写的“惬意”,南通这座二线城市在“逃离北上广系列”肯定是有一席之地的



偷得浮生半日闲,心情半佛半神仙,
image.png




亭台楼榭,玉墙石瓦,双喜悬挂,原来树叶的绿黄之间还有红的渐变。
image.png




院内秋色关不住,枝头绿黄笑春风。原来现代的高楼大厦和古代的亭台楼阁也可以相得益彰
image.png



第三站,嘉兴南北湖&高阳山,未开发的山路格外陡峭,遍布泥沙,爬起来飞沙走石,中途休息了很多次,才抵达山顶。途中有几个十岁左右的小学生嘲笑了他姐姐一路,蹦蹦跳跳爬上爬下,好像毫不费力的样子。不禁想起了年少不知力竭,年长方知山高。



山顶的辽阔只有过往的年岁,充斥着汗流浃背的我们
image.png




下山之后的夕阳伫立在远方,云的尽头是海洋?
image.png




天高地迥,觉宇宙之无穷;兴尽悲来,识盈虚之有数。
image.png




青衫烟雨客,似是故人来
image.png



下一站,在路上......


写在最后


我是凉城a,一个前端,热爱技术也热爱生活。


与你相逢,我很开心。



作者:凉城a
来源:juejin.cn/post/7319181229520224307
收起阅读 »

告别StringUtil:使用Java 全新String API优化你的代码

前言   Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。 repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。 ...
继续阅读 »


前言


  Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。



  1. repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。

  2. isBlank():检查字符串是否为空白字符序列,即长度为 0 或仅包含空格字符的字符串。

  3. lines():返回一个流,该流由字符串按行分隔而成。

  4. strip():返回一个新的字符串,该字符串是原字符串去除前导空格和尾随空格后形成的。

  5. stripLeading():返回一个新的字符串,该字符串是原字符串去除前导空格后形成的。

  6. stripTrailing():返回一个新的字符串,该字符串是原字符串去除尾随空格后形成的。

  7. formatted(Object... args):使用指定的参数格式化字符串,并返回格式化后的字符串。

  8. translateEscapes():将 Java 转义序列转换为相应的字符,并返回转换后的字符串。

  9. transform() 方法:该方法可以将一个函数应用于字符串,并返回函数的结果。


示例


1. repeat(int count)


public class StringRepeatExample {
public static void main(String[] args) {
String str = "abc";
String repeatedStr = str.repeat(3);
System.out.println(repeatedStr);
}
}

输出结果:


abcabcabc

2. isBlank()


public class StringIsBlankExample {
public static void main(String[] args) {
String str1 = "";
String str2 = " ";
String str3 = " \t ";

System.out.println(str1.isBlank());
System.out.println(str2.isBlank());
System.out.println(str3.isBlank());
}
}

输出结果:


true
true
true

3. lines()


import java.util.stream.Stream;

public class StringLinesExample {
public static void main(String[] args) {
String str = "Hello\nWorld\nJava";
Stream<String> lines = str.lines();
lines.forEach(System.out::println);
}
}

输出结果:


Hello
World
Java

4. strip()


public class StringStripExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.strip());
System.out.println(str2.strip());
}
}

输出结果:


abc
def

5. stripLeading()


public class StringStripLeadingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripLeading());
System.out.println(str2.stripLeading());
}
}

输出结果:


abc
def

6. stripTrailing()


public class StringStripTrailingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripTrailing());
System.out.println(str2.stripTrailing());
}
}

输出结果:


abc
def

7. formatted(Object... args)


public class StringFormattedExample {
public static void main(String[] args) {
String str = "My name is %s, I'm %d years old.";
String formattedStr = str.formatted( "John", 25);
System.out.println(formattedStr);
}
}

输出结果:


My name is John, I'm 25 years old.

8. translateEscapes()


public class StringTranslateEscapesExample {
public static void main(String[] args) {
String str = "Hello\\nWorld\\tJava";
String translatedStr = str.translateEscapes();
System.out.println(translatedStr);
}
}

输出结果:


Hello
World Java

9. transform()


public class StringTransformExample {
public static void main(String[] args) {
String str = "hello world";
String result = str.transform(i -> i + "!");
System.out.println(result);
}
}

输出结果:


hello world!

结尾


  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。


  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!


作者:不一样的科技宅
来源:juejin.cn/post/7222996459833770021
收起阅读 »

不吹不黑,辩证看待开发者是否需要入坑鸿蒙

前言 自打华为2019年发布鸿蒙操作系统以来,网上各种声音百家争鸣。尤其是2023年发布会公布的鸿蒙4.0宣称不再支持Android,更激烈的讨论随之而来。 本文没有宏大的叙事,只有基于现实的考量。 通过本文,你将了解到: HarmonyOS与OpenHa...
继续阅读 »

前言


自打华为2019年发布鸿蒙操作系统以来,网上各种声音百家争鸣。尤其是2023年发布会公布的鸿蒙4.0宣称不再支持Android,更激烈的讨论随之而来。

本文没有宏大的叙事,只有基于现实的考量。

通过本文,你将了解到:




  1. HarmonyOS与OpenHarmony区别

  2. 华为手机的市场占有率

  3. HarmonyOS的市场占有率

  4. 移动开发现状

  5. 鸿蒙开发优劣势

  6. 到底需不需要入坑?



1. HarmonyOS与OpenHarmony区别


HarmonyOS


移动操作系统历史


当下移动端两大巨无霸操作系统瓜分了绝大部分市场:



image.png


iOS是闭源的,只有唯一的一家厂商:Apple。

Google开放了Android基础的能力,这些能力集构成了:Android Open Source Project(简称AOSP),这块是开源免费的,任何人/公司都可以基于此进行二次开发改动。

国内各大手机厂商基于此开发出自己的系统,大浪淘沙,目前主流市场上主要手机厂商及其操作系统如下:



image.png


以上系统均衍生自AOSP,在国内使用没什么问题,若要在国外使用则需要使用Google提供的一些基础服务:统称GMS,这是需要授权的。


HarmonyOS历史与现状


华为在2019年发布了HarmonyOS 1.0 ,彼时的该系统主要应用于智慧屏、手表等设备,在2021年发布的HarmonyOS 2.0 全面应用于Android手机。

也就是这个时候华为/荣耀(未分家前)手机设备都搭载了HarmonyOS,而我们知道换了手机系统但手机上的App并没有换,照样能够正常运行。

依照华为的说法,HarmonyOS兼容Android,而部分网友认为该兼容其实就是Android套壳。

这个时候开发者无需关心鸿蒙开发,因为即使开发了Android app也能够在搭载鸿蒙系统的设备上运行。

2023年华为宣布HarmonyOS Next不再支持Android,也就是说想要在HarmonyOS Next上安装Android app是不可能的事了。

那问题就来了,作为一名Android开发者,以前只需要一套代码就可以在华为/小米/荣耀/OPPO/VIVO上运行,现在不行了,需要单独针对搭载了HarmonyOS Next的华为手机开发一个App。

若当前的App是跨端开发,如使用RN、Flutter等,那么HarmonyOS的支持力度更不可知。


OpenHarmony


从上面的描述可知,只有华为一家主推HarmonyOS,相比整个市场还是太单薄,它需要更多的厂商共同使用、共同促进新系统的发展。

因此华为将HarmonyOS的基础能力剥离出来形成了:OpenAtom OpenHarmony(简称:OpenHarmony)。

OpenHarmony是开放原子开源基金会孵化及运营的开源项目。OpenHarmony由华为公司贡献主要代码、由多家单位共建,具备面向全场景、分布式等特点,是一款全领域、新一代、开源开放的智能终端操作系统。

OpenHarmony类似于Android领域的AOSP,而HarmonyOS则是华为基于OpenHarmony开发的商业版OS。

同样的,其它厂商也可以基于OpenHarmony做改动,发布属于自己的鸿蒙商业版。
通常说的鸿蒙生态是指OpenHarmony及其衍生的商业版鸿蒙系统。

OpenHarmony源码


2. 华为手机的市场占有率


全球手机出货量



image.png


可以看出Android(80%)和iOS(20%)瓜分了天下。

图上没有华为,它被归入了Others里。

点击查看数据来源


再看另一家的统计:



image.png


华为占用约为5%。

点击查看数据来源


第三家的统计:



image.png


点击查看数据来源


虽然各家统计的数据有差异,但可以看出华为在全球手机市场份额并不高。


国内手机市场占有率



image.png


点击查看数据来源


这么看,华为在国内的占有率达到了1/4。


3. HarmonyOS的市场占有率


全球市场系统占有率


手机市场占有率并不代表都搭载了鸿蒙操作系统。

来看看各大操作系统的占有率。



image.png


点击查看数据来源


可以看出,Android和iOS设备量很多,遥遥领先。


再细分移动端的市场占有:



image.png



image.png


点击查看数据来源
同样的Android遥遥领先,此时HarmonyOS占据了3%的份额。


美国市场占有率



image.png



image.png


可以看出,在美国,Android、iOS势均力敌,唯二的存在。


印度市场占有率


再看神秘的东方大国数据:



image.png



image.png


由此可见,在印度,Android才是和咖喱最配的存在,iOS还是太耗家底了。

怪不得小米等一众国内厂商去卷印度了,市场大大滴有,就看能不能躲过印度的罚款。。。


国内鸿蒙市场占有率



image.png



image.png


国内市场里,HarmonyOS占据高达13%,毕竟国内使用华为(荣耀)手机的存量还是蛮多的。


结论:



国内才是使用鸿蒙系统的大头市场



华为官方宣称的占有率



image.png


点击查看数据来源


这里说的设备不止是智能手机,还有平板、座舱、手表等嵌入式设备。


4. 移动开发现状


iOS开发现状


iOS最先火起来的,遥想十年前,随便一个iOS开发者都能找到工作。而现在存留的iOS开发者自嘲:"Dog都不学iOS"。

以前的开发者要么转行,要么继续用"最好"的编译器(xcode)写"最优秀"的语言(OC),当然也可以用Swift,但限于系统要求,SwiftUI也没有大规模普及。

现在很少见有新鲜的血液学习iOS(也有可能iOS装备比较贵吧)了,再加上各种跨平台的框架的投入使用,原生iOS开发者的生存空间越来越小了。


Android开发现状


无独有偶,移动端的难兄难弟怎么会缺少Android呢?

一开始Android使用Java,后面全面拥抱Kotlin。

一开始画画UI,写写逻辑就能找到一份糊口的工作,现在需要去卷各种框架的底层原理,为了KPI需要去研究各种奇淫技巧的性能优化。

跨平台的框架需要去卷,KMP(已稳定)+Compose你学会了吗?RN、Flutter、Uni-app你又懂了多少?

与iOS相比Android可选择的多一些,可以选择车载等其它嵌入式设备,但多不了多少,原生Android开发者的生存空间亦不容乐观。


跨平台的开发框架移动端原生开发者可以学,前端的同学也会过来学,比如RN,Uni-app优势在前端。



行业萎缩,通常不是技术的错,技术一直在,可惜市场需求变少了



5. 鸿蒙开发优劣势


是机会还是坑?


从国内各种新闻来看:



image.png



image.png


看起来是如火如荼。


从国际的新闻看:



image.png


翻看了前几页的新闻,讨论的热度并不高,大多是搬自国内的新闻。


再说说薪资:



image.png


一看就是有夸大的成分,可能真有人达到了,但人数可能是万里挑一,只讲个例不讲普遍性没有意义。


某Boss搜一下北京的岗位:



img_v3_026m_8d70f837-9ff5-4c81-a250-6b5cf7b3198g.jpg


北京的岗位也不多,而且招的都是比较资深的,北京如此,其它城市更不用说。


鸿蒙的基建



image.png


鸿蒙目前提供提供了方舟编译器,方舟语言、IDE、模拟器等一站式开发工具,开发者可以照着官方文档编写。


根据实操的结论:




  1. 各项更新比较快,导致官方的视频/ppt和实际的有些差异

  2. 模拟器有些卡顿,有点当时Android模拟器刚出来的既视感,真机买不起

  3. 排坑的文档不多,属于摸着官方教程过河



鸿蒙官网


鸿蒙入门的简易程度



  1. 基于TS,前端开发方式,语言并不难入手

  2. IDE和Android Studio同出一源,入手比较快

  3. 声明式UI,画UI快,没接触过的同学需要熟悉一下(现在无论是Swift还是Kotlin都支持声明式UI,前端老早就用得飞起了)

  4. 不用再被graddle各种莫名错误折磨了

  5. 中文文档,对英语不好的同学体验比较好


6. 到底需不需要入坑?


对于任何一个操作系统来说,生态是第一位,鸿蒙也不例外。

横亘于鸿蒙面前的难关:




  1. 主流App是否愿意适配鸿蒙系统?

  2. 其它Android厂商是否愿意接入鸿蒙系统?

  3. 鸿蒙对开发者的支持完善与否?

  4. 鸿蒙是否真如宣传般的优秀?



不论鸿蒙是否成功,它对开发者最大的意义在于:



开辟了新的领域,开发者有机会吃到可能的"红利"



而是否入坑,取决于个人的考量,以下仅供参考:




  1. 如果贵司需要适配鸿蒙,那么只能入坑

  2. 如果对鸿蒙兴趣不足,只是觉得最近的热点有点高,未雨绸缪,想试试水,那么可以照着官方文档试试Demo

  3. 如果押宝鸿蒙,则需要深入鸿蒙的各项开发,而不仅仅只是流于表面,当然此种方式下需要花费更多的时间、精力、头发去探索、排坑

  4. 如果认为鸿蒙没有前途,那么也没必要对此冷嘲热讽,静观其变即可



那么,2024年了,你如何选择呢?


作者:小鱼人爱编程
来源:juejin.cn/post/7318561797451481129
收起阅读 »

说一说css的font-size: 0?

web
平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白! 问题描述? 是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框) 是什么原因造成...
继续阅读 »

平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白



问题描述?


是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框)


image.png


是什么原因造成的呢?


大家都知道img是行内元素,比如当我们的标签换行的时候,回车符会解析一个空白符,所以这是造成会有间距的原因之一。


当然喽,不仅仅是img,包括其他的一些常见的行内元素,比如span👇标签回车换行的效果,同样也会间隙,当然如果是缩进、空格等字符同样也会产生空白间隙,导致元素间产生多余的间距


image.png


    <span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

如何解决呢?


那我们首先想到取消换行、空格...


既然是因为标签换行了引起的,那么我们就取消换行、空格等试一试。


image.png


<span>背景图</span><span>背景图</span><span>背景图</span><span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

证明方法还是有用的~ 那还有没有其他的方法解决呢,那这个时候可以借助font-size:0来用一用。


如何使用font-size: 0 解决呢?


利用font-size:0消除子行内元素间额外多余的空白,需要在父元素上添加font-size:0


image.png


是不是就解决了呀?


看一个完整的完整demo效果


image.png
当然需要注意一下



设置font-size: 0时,子元素必须指定一个font-size大小,否则文本内容不会显示哦



示例代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
<style>
/*************************css代码👇***********************/
ul {
margin: 20px;
display: flex;
gap: 20px;
}
.item {
width: 300px;
height: 200px;
padding: 20px;
border-radius: 10px;
background: #fff;
overflow: hidden;
font-size: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)
}
.item-img {
width: 100%;
height: 175px;
object-fit: cover;
border-radius: 5px;
}
.item-text {
color: #333;
font-size: 14px;
}
span {
background-color: red;
padding: 10px;
}
</style>
</head>
<body>


<ul>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
</ul>
</body>
</html>


作者:是小西瓜吖
来源:juejin.cn/post/7260752483055878204
收起阅读 »

Java中的一些编程经验

最近看公司项目,其中能学到很多的编程经验,正好总结学习一下 判空的处理 公司的判空处理,我看每个人都有每个人的喜好,这么多列出来,一看还真不知道这些的区别,正好今天总结学习一下😎: StrUtil.isBlank:这个一看就是处理字符串的,用于检查一个字符...
继续阅读 »

最近看公司项目,其中能学到很多的编程经验,正好总结学习一下



判空的处理


公司的判空处理,我看每个人都有每个人的喜好,这么多列出来,一看还真不知道这些的区别,正好今天总结学习一下😎:



  • StrUtil.isBlank:这个一看就是处理字符串的,用于检查一个字符串是否为 null、空字符串("")或者只包含空白字符(如空格、制表符、换行符等),注意只包含空格也会别认定为false

  • Objects.nonNull:它是 java.util.Objects 类的一部分。这个方法用于检查一个对象是否不为 null

  • ObjectUtil.isNull:这个也是检查对象是否为空的

  • CollUtil.isNotEmptyCollUtil.isNotEmpty 方法用于检查一个集合是否非空,即集合中至少包含一个元素,这个主要来检查集合的


这么总结一看,发现挺好区分的,字符串和集合都有对应的处理类,然后对象判空的话,两个都可以,看个人喜好了😁😁😁


异步的使用



看公司代码中调用异步任务的时候,使用了自己不熟悉的类,正好来学习总结一下



先学概念


CompletableFuture概念:是 Java 8 中引入的一个类,它是 java.util.concurrent 包的一部分,用于简化异步编程模型。CompletableFuture 提供了一种更加直观的方式来处理异步操作的结果,以及在异步操作完成后执行后续操作。


说人话😭:就是java.util.concurrent 包下的一个用来异步编程的一个类


核心知识



  • 链式调用:支持链式调用,这意味着你可以在异步任务完成后执行其他操作,如处理结果、执行新的异步任务等

  • 线程安全CompletableFuture 的操作是线程安全的,这意味着你可以在多线程环境中安全地使用它。

  • 异步回调CompletableFuture 可以通过 thenAcceptthenRun 方法来定义异步任务完成后的回调操作。


写个Demo


CompletableFuture 的使用示例:


创建一个异步任务:


 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
     // 异步执行的代码
     return "Hello, World!";
 });

分析:代码中创建一个异步任务,该任务会执行一个 Supplier 函数式接口的实现,这个实现返回一个字符串 "Hello, World!"。supplyAsync 方法会启动一个新的线程来执行这个任务,并且返回一个 CompletableFuture<String> 对象,这个对象代表了异步任务的执行结果。


OK,结束


别走😭,来都来了,多学点:


试试链式调用:


 future.thenApply(s -> s.toUpperCase())
      .thenAccept(System.out::println)
      .exceptionally(e -> {
           System.err.println("An error occurred: " + e.getMessage());
           return null;
      });

分析thenApply 方法用于指定一个函数,这个函数将异步任务的结果作为输入,并返回一个新的结果。在这个例子中,它将字符串转换为大写。


thenAccept 方法用于指定一个消费者函数,这个函数接受 thenApply 方法的结果,并执行某些操作(在这个例子中是打印字符串)。


exceptionally 方法用于指定一个异常处理器,如果前面的操作(thenApplythenAccept)抛出异常,这个处理器会被调用,打印错误信息。


调用结果


 try {
     String result = future.get();
     System.out.println("Result: " + result);
 } catch (InterruptedException | ExecutionException e) {
     e.printStackTrace();
 }

分析:future.get() 方法用于获取异步任务的结果。这个方法会阻塞当前线程,直到异步任务完成。


如果任务被中断或者执行过程中抛出异常,get 方法会抛出 InterruptedExceptionExecutionException


工程实践


学完了一些基本的,看一下公司代码是怎么写的😶‍🌫️:


         CompletableFuture.runAsync(() -> {
             Thread thread = new Thread(uuid) {
                 @Override
                 public void run() {
 ​
                     try {
                         taskContentInfoData(params, uuid, finalInputStream, insertPercent, flDtoList);
                    } catch (Exception exception) {
                         writeJobStatus(uuid, JobStatusEnum.FAIL.getStatus(), null);
                         log.info("错误信息{}", exception);
                         CommonConstants.threadPoolMap.remove(uuid);
                    }
                }
            };
             thread.start();
        });

也很简单,就是 CompletableFuture.runAsync来异步执行一个 Runnable 对象


分析:公司这里处理的也能达到异步的效果,这个实现的run方法里面,又开启了一个线程,主要是为了设置这个线程的唯一标识,所以有点绕。


顺便复习一下创建线程的几种方式:继承Thread类、实现Runnable接口,线程池创建


其中也可以直接创建Thread类来重写其run方法来创建🙌🙌🙌


作者:CoderLiz
来源:juejin.cn/post/7317325051476525093
收起阅读 »

如何写一个redis蜜罐

写在前面 蜜罐就是一种通过模拟真实环境来诱导入侵者的一种技术。通过它可以拖延黑客入侵时间,递给黑客用于取证假数据,溯源黑客等。通过控制平台去操作部署的仿真环境实现高效的诱捕。 之前写过一个简单的仿真redis蜜罐,简单介绍一下。 RESP 搭建这种组件的仿真环...
继续阅读 »

写在前面


蜜罐就是一种通过模拟真实环境来诱导入侵者的一种技术。通过它可以拖延黑客入侵时间,递给黑客用于取证假数据,溯源黑客等。通过控制平台去操作部署的仿真环境实现高效的诱捕。


之前写过一个简单的仿真redis蜜罐,简单介绍一下。


RESP


搭建这种组件的仿真环境,要么用真实的程序,要么就自己实现一套虚假的程序。假如自己实现的话,最关键的就是协议,对于redis来说,它的通信协议相对简单,就是resp,协议格式如下:



  • 单行字符串(Simple Strings): 响应的首字节是 "+"。例如:"+OK\r\n"。

  • 错误(Errors): 响应的首字节是 "-"。例如:"-ERROR message\r\n"。

  • 整型(Integers): 响应的首字节是 ":"。例如:":0\r\n"。

  • 多行字符串(Bulk Strings): 响应的首字节是"",后面跟字符长度,然后跟字符。例如:"",后面跟字符长度,然后跟字符。例如:"6\r\nfoobar\r\n"

  • 数组(Arrays): 响应的首字节是 "*"。例如:"*2\r\n3\nfoo˚\n˚3\r\nfoo\r\n3\r\nbar\r\n"


这就是它的通信协议,相当简单,比如我们想实现一个简单的get key操作,那么协议对应的字符格式为


*2\r\n$3\r\nget\r\n$3\r\nkey\r\n

然后传送给服务端就行。


Redis蜜罐


对于蜜罐,我们只需要实现服务端即可,客户端就用redis-cli这个工具就行。废话不多说,直接贴一下项目github.com/SSRemex/sil…



很简单,server.py实现了一个socket服务,同时加了日志收集功能。


而resp.py则实现了命令解析、命令执行、结果格式处理等操作


命令解析和结果格式主要是协议的解析和封装,这里主要想说一下两点,就是实现了一部分redis的命令


...
class RespHandler:
def __init__(self):
# 用来临时存储数据的字典
self.k_v_dict = {
"admin": "12345"
}

self.executable_command = {
"ping": (self.ping, True),
"get": (self.get, True),
"set": (self.set, True),
"keys": (self.keys, True),
"auth": (self.auth, True),
"del": (self.delete, True),
"exists": (self.exists, True),
"dbsize": (self.dbsize, True),
"config": (self.config, True)

}
self.unexecutable_command = [
"hget", "hset", "hdel", "hlen", "hexists", "hkeys", "hvals", "hgetall", "hincrby", "hincrbyfloat",
"hstrlen", "shutdown", "expire", "expireat", "pexpire", "pexpireat", "ttl", "type", "rename", "renamenx",
"randomkey", "move", "dump", "restore", "migrate", "scan", "select", "flushdb", "flushall", "mset", "mget",
"incr", "decr", "append", "strlen", "getset", "setrange", "getrange", "rpush", "lpush", "linsert", "lrange",
"lindex", "llen", "rpop", "lpop", "lrem", "lset", "blpop",

]
...

这里我内定了一些命令,并实现了他们的功能,让它像真的redis一样,你甚至可以进行kv操作,同时为了真实性,设定了一堆不可执行的命令,调用时会返回redis的报错,就像在配置文件里面禁用了这些命令一样。


演示


服务端执行,默认运行在3998端口



redis-cli连接



可以发现成功连接



此时服务端这边也接收到了,并且生成了日志



接下来,我们在redis-cli执行一些命令



很完美,甚至可以进行set get操作。


最后再次附上项目地址:github.com/SSRemex/sil…


作者:银空飞羽
来源:juejin.cn/post/7316783747491086371
收起阅读 »

90%的Java开发人员都会犯的5个错误

前言 作为一名java开发程序员,不知道大家有没有遇到过一些匪夷所思的bug。这些错误通常需要您几个小时才能解决。当你找到它们的时候,你可能会默默地骂自己是个傻瓜。是的,这些可笑的bug基本上都是你忽略了一些基础知识造成的。其实都是很低级的错误。今天,我总结一...
继续阅读 »



前言


作为一名java开发程序员,不知道大家有没有遇到过一些匪夷所思的bug。这些错误通常需要您几个小时才能解决。当你找到它们的时候,你可能会默默地骂自己是个傻瓜。是的,这些可笑的bug基本上都是你忽略了一些基础知识造成的。其实都是很低级的错误。今天,我总结一些常见的编码错误,然后给出解决方案。希望大家在日常编码中能够避免这样的问题。


1. 使用Objects.equals比较对象


这种方法相信大家并不陌生,甚至很多人都经常使用。是JDK7提供的一种方法,可以快速实现对象的比较,有效避免烦人的空指针检查。但是这种方法很容易用错,例如:


Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false

为什么替换==Objects.equals()会导致不同的结果?这是因为使用==编译器会得到封装类型对应的基本数据类型longValue,然后与这个基本数据类型进行比较,相当于编译器会自动将常量转换为比较基本数据类型, 而不是包装类型。


使用该Objects.equals()方法后,编译器默认常量的基本数据类型为int。下面是源码Objects.equals(),其中a.equals(b)使用的是Long.equals()会判断对象类型,因为编译器已经认为常量是int类型,所以比较结果一定是false


public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}

知道了原因,解决方法就很简单了。直接声明常量的数据类型,如Objects.equals(longValue,123L)。其实如果逻辑严密,就不会出现上面的问题。我们需要做的是保持良好的编码习惯。


2. 日期格式错误


在我们日常的开发中,经常需要对日期进行格式化,但是很多人使用的格式不对,导致出现意想不到的情况。请看下面的例子。


Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));//2022-12-31 08:00:00

以上用于YYYY-MM-dd格式化, 年从2021 变成了 2022。为什么?这是因为 javaDateTimeFormatter 模式YYYYyyyy之间存在细微的差异。它们都代表一年,但是yyyy代表日历年,而YYYY代表星期。这是一个细微的差异,仅会导致一年左右的变更问题,因此您的代码本可以一直正常运行,而仅在新的一年中引发问题。12月31日按周计算的年份是2022年,正确的方式应该是使用yyyy-MM-dd格式化日期。


这个bug特别隐蔽。这在平时不会有问题。它只会在新的一年到来时触发。我公司就因为这个bug造成了生产事故。


3. 在 ThreadPool 中使用 ThreadLocal


如果创建一个ThreadLocal 变量,访问该变量的线程将创建一个线程局部变量。合理使用ThreadLocal可以避免线程安全问题。


但是,如果在线程池中使用ThreadLocal ,就要小心了。您的代码可能会产生意想不到的结果。举个很简单的例子,假设我们有一个电商平台,用户购买商品后需要发邮件确认。


private ThreadLocal currentUser = ThreadLocal.withInitial(() -> null);

private ExecutorService executorService = Executors.newFixedThreadPool(4);

public void executor() {
executorService.submit(()->{
User user = currentUser.get();
Integer userId = user.getId();
sendEmail(userId);
});
}

如果我们使用ThreadLocal来保存用户信息,这里就会有一个隐藏的bug。因为使用了线程池,线程是可以复用的,所以在使用ThreadLocal获取用户信息的时候,很可能会误获取到别人的信息。您可以使用会话来解决这个问题。


4. 使用HashSet去除重复数据


在编码的时候,我们经常会有去重的需求。一想到去重,很多人首先想到的就是用HashSet去重。但是,不小心使用 HashSet 可能会导致去重失败。


User user1 = new User();
user1.setUsername("test");

User user2 = new User();
user2.setUsername("test");

List users = Arrays.asList(user1, user2);
HashSet sets = new HashSet<>(users);
System.out.println(sets.size());// the size is 2

细心的读者应该已经猜到失败的原因了。HashSet使用hashcode对哈希表进行寻址,使用equals方法判断对象是否相等。如果自定义对象没有重写hashcode方法和equals方法,则默认使用父对象的hashcode方法和equals方法。所以HashSet会认为这是两个不同的对象,所以导致去重失败。


5. 线程池中的异常被吃掉


ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
//do something
double result = 10/0;
});

上面的代码模拟了一个线程池抛出异常的场景。我们真正的业务代码要处理各种可能出现的情况,所以很有可能因为某些特定的原因而触发RuntimeException


但是如果没有特殊处理,这个异常就会被线程池吃掉。这样就会导出出现问题你都不知道,这是很严重的后果。因此,最好在线程池中try catch捕获异常。


总结


本文总结了在开发过程中很容易犯的5个错误,希望大家养成良好的编码习惯。


作者:JAVA旭阳
来源:juejin.cn/post/7182184496517611576
收起阅读 »

一个左侧导航栏的选中状态把我迷得颠三倒四

web
事情是这样的 👇 前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EAS...
继续阅读 »

事情是这样的 👇


前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EASY吗😎


我用的是vue版本的,说白了就一个知识点——路由和菜单。凭借我的聪明才智,肯定一看就会。


路由


首先,需要先了解一下路由表的配置。基本的路由配置请参阅 Vue-Router 官方文档


// 在本例子中,页面最终路径为 /dashboard/workplace
export default {
path: 'dashboard',
name: 'dashboard', // 路由名称
component: () => import('@/views/dashboard/index.vue'),
meta: {
locale: 'menu.dashboard',
requiresAuth: true,
icon: 'icon-dashboard',
},
children: [
{
path: 'workplace',
name: 'workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['admin'],
hideInMenu: false,
},
},
],
};

路由 Meta 元信息


参数名说明类型默认值
roles配置能访问该页面的角色,如果不匹配,则会被禁止访问该路由页面string[]-
requiresAuth是否需要登录鉴权booleanfalse
icon菜单配置iconstring-
locale一级菜单名(语言包键名)string-
hideInMenu是否在左侧菜单中隐藏该项boolean-
hideChildrenInMenu强制在左侧菜单中显示单项boolean-
activeMenu高亮设置的菜单项string-
order排序路由菜单项。如果设置该值,值越高,越靠前number-
noAffix如果设置为true,标签将不会添加到tab-bar中boolean-
ignoreCache如果设置为true页面将不会被缓存boolean-

hideInMenu 控制菜单显示, requiresAuth 控制是否需要登录,没错,以上知识点完全够我用了🤏🤏🤏🤏


三下五除二就开发完成了,正当我沉浸在成功的喜悦时,测试给我提了个bug。


说导航栏目切换,选中状态有俩个正常,俩个不正常,切换页面导航没选中,再点一次才会选中。我擦👏👏👏👏👏,我傻眼了😧😧😧


到底哪里出了问题,我哪里写错了呢,人家官方肯定没问题,于是我开始寻找。是路由名称重复了,还是那个组件写的有问题,还好有俩个正常的,我仔细比对一下不就好了吗,我可真是个小机灵鬼。


就这样,我又一次为我的自大付出了汗水,对比了一天,我感觉好像真不是我写的有问题,不过还是有收获的,我发现requiresAuth 设置为true的导航正常,requiresAuth设置为false的不正常。抱着怀疑的态度我开始找原来模板项目中处理requiresAuth的代码。最后在components》menu》index.vue文件下发现一个方法:


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

这... ... 我人麻了,这不是只有requiresAuth 为true的时候才会有效吗?他为啥这么写呢?还是我复制项目的时候复制错了?然后我改成了


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if ((requiresAuth === true || requiresAuth === false) && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

🙈🙈🙈🙈🙈看出来我改哪里了吗,考考你们的眼力,我改的方法是不是很nice。
反正好使了,下班!!!!


作者:一路向北京
来源:juejin.cn/post/7317277887567151145
收起阅读 »

【日常总结】解决el-select数据量过大的3种方法

web
背景 最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。 想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,...
继续阅读 »

背景


最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。


image.png


想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,前端搞也是可以的。


这个故事还有个后续


image.png


过了一周上线后,发现有一个下拉框的数据有30w+!!!加载都加载不出来,哈哈哈哈,接口直接超时报错了,所以又cuocuocuo改了一遍,最后改成了:



  1. 接口翻页请求

  2. 前端使用自定义指令实现上拉加载更多,搜索直接走的后端接口


方案


通过一顿搜索加联想总结了3种方法,以下方法都需要支持开启filterable支持搜索。


标题具体问题
方案1只展示前100条数据,这个的话配合filter-method每次只返回前100条数据。限制展示的条数可能不全,搜索需要多搜索点内容
方案2分页方式,通过指令实现上拉加载,不断上拉数据展示数据。仅过滤加载出来的数据,需要配合filterMethod过滤数据
方案3options列表采用虚拟列表实现。成本高,需要引入虚拟列表组件或者自己手写。经掘友指点,发现element-plus提供了对应的实现,如果是plus,则可以直接使用select-v2

方案一(青铜段位) filterMethod直接过滤数据量


<template>
<el-select
v-model="value"
clearable filterable
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options.slice(0, 100)"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>

</template>
export default {
name: 'Demo',
data() {
return {
options: [],
value: ''
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
for (let i = 0; i < 25000; i++) {
this.options.push({label: "选择"+i,value:"选择"+i});
}
},
filterMethod(val) {
console.log('filterMethod', val);
this.options = this.options.filter(item => item.value.indexOf(val) > -1).slice(0, 100);
},
visibleChange() {
console.log('visibleChange');
}
}
}

方案二(白银段位) 自定义滚动指令,实现翻页加载


写自定义滚动指令,options列表滚动到底部后,再加载下一页。但这时候筛选出来的是已经滚动出来的值。


这里如果直接使用filterable来搜索,搜索出来的内容是已经滑动出来的内容。如果想筛选全部的,就需要重写filterMethod方法来自定义过滤功能。可以根据情况选择是否要重写filterMethod。


image.png


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>
<script>
export default {
name: 'Demo',
data() {
return {
options: [],
value: '',
pageNo: 0
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
},
filterMethod(val) {
this.data = val ? this.allData.filter(item => item.label.indexOf(val) > -1) : this.allData;
this.getPageList();
}
},
directives:{
'el-select-loadmore':(el, binding) => {
// 获取element-ui定义好的scroll父元素
const wrapEl = el.querySelector(".el-select-dropdown .el-select-dropdown__wrap");
if(wrapEl){
wrapEl.addEventListener("scroll", function () {
/**
* scrollHeight 获取元素内容高度(只读)
* scrollTop 获取或者设置元素的偏移值,
* 常用于:计算滚动条的位置, 当一个元素的容器没有产生垂直方向的滚动条, 那它的scrollTop的值默认为0.
* clientHeight 读取元素的可见高度(只读)
* 如果元素滚动到底, 下面等式返回true, 没有则返回false:
* ele.scrollHeight - ele.scrollTop === ele.clientHeight;
*/

if (this.scrollTop + this.clientHeight >= this.scrollHeight) {
// binding的value就是绑定的loadmore函数
binding.value();
}
});
}
},
},
}
</script>

</script>

方案三(黄金段位) 虚拟列表


引入社区的vue-virtual-scroll-list 支持虚拟列表。但这里想的自己再实现一遍虚拟列表,后续再写吧。


另外,element-plus提供了对应的实现,如果是使用的是plus,则可以直接使用 select-v2组件


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable >

<virtual-list
class="list"
style="height: 360px; overflow-y: auto;"
:data-key="'value'"
:data-sources="data"
:data-component="item"
:estimate-size="50"
/>

</el-select>
</div>

</template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item';
export default {
name: 'Demo',
components: {VirtualList, Item},
data() {
return {
options: [],
data: [],
value: '',
pageNo: 0,
item: Item,
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
}
}
}
</script>


// item组件
<template>
<el-option :label="source.label" :value="source.value"></el-option>
</template>


<script>
export default {
name: 'item',
props: {
source: {
type: Object,
default() {
return {}
}
}
}
}
</script>


<style scoped>
</style>



总结


最后我们项目中使用的虚拟列表,为啥,因为忽然发现组件库支持select是虚拟列表,那就直接使用这个啦。


最后的最后


没有用虚拟列表,因为接口数据量过大(你见过返回30w+的接口吗🙄。。),后端接口改成分页,前端支持自定义指令上拉加载,引用的参数增加了remote、remote-method设置为远端的方法。


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
remote
:remote-method="remoteMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>

参考文章:



作者:searchop
来源:juejin.cn/post/7278238985448341544
收起阅读 »

为啥TextureView比SurfaceView表现还差呢?

从原理上面讲,我们大众的认知就是TextureView比SurfaceView的性能要好。硬的比软的好。但是其实这种是片面的。最近就遇到一个奇怪的现象:在3399上面通过ffmpeg拉rtsp流,然后通过mediacodec解码后渲染。渲染到TextureVi...
继续阅读 »

从原理上面讲,我们大众的认知就是TextureView比SurfaceView的性能要好。硬的比软的好。但是其实这种是片面的。最近就遇到一个奇怪的现象:在3399上面通过ffmpeg拉rtsp流,然后通过mediacodec解码后渲染。渲染到TextureView上会比较频繁的出现马赛克的现象。但是换用SurfaceView立马就变好了。


TextureView 和 SurfaceView 都有各自的优势和局限性,所以它们的性能表现也会因应用的具体需求和使用场景而异。
在某些情况下,TextureView 的性能可能会比 SurfaceView 差,原因可能有以下几点:



  1. 渲染管道的差异:TextureView 是基于 OpenGL ES 的,它使用图形渲染管道来渲染内容。而 SurfaceView 则使用传统的 Android 渲染管道,这与 Android 的视图系统更加紧密集成。在某些情况下,这可能会导致 SurfaceView 的性能更好。

  2. 线程管理:SurfaceView 使用一个独立的线程来渲染内容,这可以提供更平滑的渲染性能,尤其是在处理复杂动画或游戏时。而 TextureView 则在主线程上渲染内容,这可能会导致性能下降,尤其是在处理大量数据或复杂渲染时。

  3. 硬件加速:虽然 TextureView 支持硬件加速,但在某些情况下,硬件加速可能会导致性能问题,尤其是在低端设备上。SurfaceView 则更多地依赖于软件渲染,这可能在某些情况下会提供更稳定的性能。


需要注意的是,性能差异可能会因设备和应用而异,因此在实际开发中应该根据具体需求和性能测试结果来选择合适的视图。无论选择哪种视图,都应该优化代码以提高性能,并确保在不同设备上进行充分的测试。


于是,我针对上面的3点的结论做了一个实验,在3399上面ffmpeg硬解码居然比软解码帧率要低。看来3399的CPU性能比其他硬件确实要抢。这就证明了标题中的疑惑了。


下面贴出一段出马赛克的代码,换上SurfaceView就好了。


public class IPCameraPreviewFragment extends Fragment implements TextureView.SurfaceTextureListener{

public static final String TAG = "IPCameraPreviewFragment";
public static final boolean DEBUG = true;

private TextureView mPreview;
private SurfaceTexture mSurfaceTexture;
private Handler mUiHandler = new Handler();
private Runnable mRunnable = new Runnable() {

@Override
public void run() {
if(mPreview == null || mSurfaceTexture == null) return;
Play.getInstances().startPreivew(new Surface(mSurfaceTexture));
}
};
private IErrorCallback mErrorCallback = new IErrorCallback() {

@Override
public void onError(int error) {
Log.d(TAG, "onError = " + error);
if(null == mUiHandler || null == mRunnable) return;
mUiHandler.removeCallbacks(mRunnable);
mUiHandler.postDelayed(mRunnable, 5000);
}
};

public void setDataSource(String source){
Play.getInstances().setErrorCallback(mErrorCallback);
Play.getInstances().setDataSource(source);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGr0up container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.preview_fragment, container,false);
mPreview = (TextureView)view.findViewById(R.id.preview);
mPreview.setSurfaceTextureListener(this);
return view;
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
int height)
{
mSurfaceTexture = surface;
Play.getInstances().startPreivew(new Surface(surface));
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
int height)
{

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mSurfaceTexture = null;
Play.getInstances().releaseMediaPlay();
if(null == mUiHandler || null == mRunnable) return false;
mUiHandler.removeCallbacks(mRunnable);
mUiHandler = null;
mRunnable = null;
return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

}

作者:逗比先生
来源:juejin.cn/post/7316592817341218866
收起阅读 »

App防止恶意截屏功能的方法:iOS、Android和鸿蒙系统的实现方案

防止应用被截图是一个比较常见的需求,主要是出于安全考虑。下面将分别为iOS(苹果系统)、Android(安卓系统)及HarmonyOS(鸿蒙系统)提供防止截屏的方法和示例代码。在企业内部使用的应用中,防止员工恶意截屏是一个重要的安全需求。本文将详细介绍iOS、...
继续阅读 »

防止应用被截图是一个比较常见的需求,主要是出于安全考虑。下面将分别为iOS(苹果系统)、Android(安卓系统)及HarmonyOS(鸿蒙系统)提供防止截屏的方法和示例代码。

123456 (161).png

在企业内部使用的应用中,防止员工恶意截屏是一个重要的安全需求。本文将详细介绍iOS、Android和鸿蒙系统的防止截屏的方法,并提供相应的代码示例,以帮助代码初学者理解和实现该功能。

iOS系统防止截屏方法:

在iOS系统中,可以通过设置UIWindow的windowLevel为UIWindowLevelNormal + 1,使应用窗口覆盖在截屏窗口之上,从而阻止截屏。以下是Objective-C和Swift两种语言的代码示例:

  1. iOS系统防止截屏

在iOS中,可以使用UIScreen的isCaptured属性来检测屏幕是否被录制或截图。为了防止截屏,你可以监听UIScreenCapturedDidChange通知,当屏幕开始被捕获时,你可以做一些操作,比如模糊视图或显示一个全屏的安全警告。

swift

// 注册屏幕捕获变化通知
NotificationCenter.default.addObserver(
    self,
    selector: #selector(screenCaptureChanged),
    name: UIScreen.capturedDidChangeNotification,
    object: nil
)
@objc func screenCaptureChanged(notificationNSNotification) {
    if UIScreen.main.isCaptured {
        // 屏幕正在被捕获,可以在这里做一些隐藏内容的操作,比如
        // 显示一个覆盖所有内容的视图
    } else {
        // 屏幕没有被捕获,可以移除那个覆盖的视图
    }
}

但需要注意的是,iOS不允许应用程序完全禁止截屏。因为截图功能是系统级别的,而不是应用级别的,上述代码只能做到在截图时采取一定的响应措施,不能完全防止。

  1. Android系统防止截屏

在Android中,可以通过设置Window的属性来防止用户截图或录屏。这通过禁用FLAG_SECURE来实现。

java

// 在Activity中设置禁止截屏
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 在setContentView之前调用
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
                         WindowManager.LayoutParams.FLAG_SECURE);
    setContentView(R.layout.activity_main);
}

这样设置后,当前的Activity将无法被截屏或录屏。

  1. HarmonyOS(鸿蒙系统)防止截屏

HarmonyOS是华为开发的一个分布式操作系统,目前它在应用开发中有着与Android类似的API。因此可以使用与Android相同的方法进行禁止截屏。

java

// 在Ability(Activity)中设置禁止截屏
@Override
protected void onStart(Intent intent) {
    super.onStart(intent);
    // 在setUIContent之前调用
    getWindow().addFlags(WindowManager.LayoutConfig.FLAG_SECURE);
    setUIContent(ResourceTable.Layout_ability_main);
}

在HarmonyOS中,Ability相当于Android中的Activity。

请注意尽管上述方法能够有效地防止绝大多数截屏和录屏行为,但技术上并不是100%无法绕过的(例如某些root设备或具有特殊权限的应用可能可以绕过这些限制)。因此,在处理非常敏感的信息时,请综合其他安全措施一起使用,比如数据加密、用户行为分析等。


作者:咕噜分发企业签名梦奇
来源:juejin.cn/post/7317095140040376346
收起阅读 »

如何优雅的将MultipartFile和File互转

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。 前言 首先来区别一下MultipartFile和File: MultipartFile是 S...
继续阅读 »

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。


前言


首先来区别一下MultipartFile和File:



  • MultipartFile是 Spring 框架的一部分,File是 Java 标准库的一部分。

  • MultipartFile主要用于接收上传的文件,File主要用于操作系统文件。


MultipartFile转换为File


使用 transferTo


这是一种最简单的方法,使用MultipartFile自带的transferTo 方法将MultipartFile转换为File,这里通过上传表单文件,将MultipartFile转换为File格式,然后输出到特定的路径,具体写法如下。


transferto.png


使用 FileOutputStream


这是最常用的一种方法,使用 FileOutputStream 可以将字节写入文件。具体写法如下。


FileOutputStream.png


使用 Java NIO


Java NIO 提供了文件复制的方法。具体写法如下。


copy.png


File装换为MultipartFile


从File转换为MultipartFile 通常在测试或模拟场景中使用,生产环境一般不这么用,这里只介绍一种最常用的方法。


使用 MockMultipartFile


在转换之前先确保引入了spring-test 依赖(以Maven举例)


<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>version</version>
<scope>test</scope>
</dependency>

通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。


multi.png



更多文章干货,推荐公众号【程序员老J】



作者:程序员老J
来源:juejin.cn/post/7295559402475667492
收起阅读 »

从 Vue 2 迁移到 Svelte

web
大家好,这里是大家的林语冰。 本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。 在使用 Vue 2 作为我们的前端框架近 2 ...
继续阅读 »

大家好,这里是大家的林语冰。


本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。


在使用 Vue 2 作为我们的前端框架近 2 年后,我们宣布此支持将不再维护,因此我们决定迁移到新框架。但该选谁呢:Vue 3 还是 Svelte 呢


粉丝请注意,我们迁移后的目标也是改善 DX(开发体验),尤其是类型检查、性能和构建时间。我们没有考虑 React,因为它需要投资一大坨时间成本来学习,而且与 Vue 和 Svelte 不同,它没有提供开箱即用的解决方案。此外,后者共享相同的 SFC(单文件组件)概念:同一文件中的逻辑(JS)、结构(HTML)和样式(CSS)。


Svelte vs Vue 3


Svelte 的留存率更高。对于我们的新前端,我们必须从市场上可用的 2 个框架权衡,即 Svelte 和 Vue 3。下面是过去 5 年不同框架留存率的图示(留存率 = 会再次使用/(会再次使用 + 不会再次使用))。JS 现状调查汇编了该领域开发者的数据,如你所见,Svelte 排名第二,而 Vue 3 排名第四。


01-state.jpg


这启示我们,过去使用过 Svelte 的开发者愿意再次使用它的数量比不愿使用它的要多。


Svelte 的类型体验更棒


Vue 2Vue 3Svelte
组件级类型YesYesNo
跨组件类型NoYesYes
类型事件NoNoYes

Svelte 通过更简单的组件设计流程和内置类型事件提供了更好的类型体验,对我们而言十分用户友好。


全局变量访问限制。使用 Svelte,可以从其他文件导入枚举,并在模板中使用它们,而 Vue 3 则达咩。


02-benchmark.png


语法。就我个人而言,私以为 Svelte 语法比 Vue 更优雅和用户友好。您可以瞄一下下面的代码块,并亲自查看它们。


Svelte:


03-svelte.png


Vue:


04-vue.png


没有额外的 HTML div <template>。在 Svelte 中您可以直接编写自己的 HTML。


样式在 Svelte 中会自动确定作用域,这对于可维护性而言是一个优点,且有助于避免 CSS 副作用。每个组件的样式独立,能且仅能影响该组件,而不影响其父/子组件。


更新数据无需计算属性。在 Svelte 中,感觉更像在用纯 JS 撸码。您只需要专注于编写一个箭头函数:


const reset = () => {
firstName = ''
lastName = ''
}

Svelte 中只需单个括号:


//Svelte
{fullName}

//Vue
{{fullName}}

添加纯 JS 插件更简单。此乃使用 Svelte 和 Prism.js 的语法高亮集成用例,如下所示:


05-prism.png


无需虚拟 DOM 即可编译代码。Svelte 和 Vue 之间的主要区别是,减少了浏览器和 App 之间的层数,实现更优化、更快的任务成果。


自动更新。诉诸声明变量的辅助,Svelte 可以自动更新您的数据。这样,您就不必等待变更反映在虚拟结构中,获得更棒的 UX(用户体验)。


Svelte 也有短板


理所当然,Svelte 也有短板,比如社区相对较小,因为它是 2019 年才诞生的。但随着越来越多的开发者可能会认识到其质量和用户友好的内容,支持以及社区未来可能会不断发展壮大。


因此,在审查了此分析的结果后,尽管 SvelteKit 在迁移时仍处于积极开发阶段,我们决定使用 Svelte 和 Svelte Kit 砥砺前行。


06cons.png


如何处理迁移呢?


时间:我们选择在 8 月份处理迁移,当时该 App 用户较少。


时间长度:我们花了 2 周时间将所有文件从 Vue 迁移到 Svelte。


开发者数量:2 名前端开发者全职打工 2 周,另一名开发者全职打工 1 周,因此涉及 3 名开发人员。


工作流:首先,我们使用 Notion 工具将我们的凭据归属于团队的开发者。然后,我们开始在 Storybook 中创建新组件,最后,每个开发者都会奖励若干需要在 Svelte 中重写的页面。


作为一家初创公司,迁移更简单,因为我们没有 1_000 个文件需要重写,因此我们可以快速执行。虽然但是,当 SvelteKit 仍处于积极开发阶段时,我们就冒着风险开始迁移到 SvelteKit,这导致我们在迁移仅 1 个月后就不得不做出破坏性更新。但 SvelteKit 专业且博大精深的团队为我们提供了一个命令(npx svelte-migrate routes),以及一个解释清晰的迁移指南,真正帮助我们快速适应新的更新。


此外,9 月份,SvelteKit 团队宣布该框架终于进入候选版本阶段,这意味着,它的稳定性现在得到了保证!


文件和组件组织


SvelteKit 的“文件夹筑基路由”给我们带来了很多。我们可以将页面拆分为子页面,复用标准变量名,比如 loading/submit 等等。此外,布局直接集成到相关路由中,由于树内组织的增加,访问起来更简单。


那么我们得到了什么?


除了上述好处之外,还值得探讨其他某些关键因素:


性能提高且更流畅。编译完成后,我们可以体会到该 App 的轻量级。与其他框架相比,这提高了加载速度,其他框架在 App 的逻辑代码旁嵌入了“运行时”。


DX 更棒。SvelteKit 使用 Vite 打包器,此乃新一代 JS 构建工具,它利用浏览器中 ES 模块的可用性和编译为原生(compile-to-native)的打包器,为您带来最新 JS 技术的最佳 DX。


代码执行更快。它没有虚拟 DOM,因此在页面上变更时,需要执行的层数少了一层。


启动并运行 SSR(服务器端渲染)。如果最终用户没有良好的互联网连接或启用 JS,平台仍将在 SSR 的帮助下高效运行,因为用户仍能加载网页,同时失去交互性。


代码简洁易懂。Svelte 通过将逻辑(JS)、结构(HTML)和样式(CSS)分组到同一文件中,可以使用更具可读性和可维护性的面向组件的代码。黑科技在于所有这些元素都编译在 .svelte 文件中。


固定类型检查。自从我们迁移到 Svelte 以来,我们已经成功解决了类型检查的最初问题。事实上,我们以前必须处理周期性的通知,而如今时过境迁。不再出现头大的哨兵错误。(见下文)


07-error.jpg


粉丝请注意,此博客乃之前的迁移测评,其中某些基准测试见仁见智,尤大还亲自码字撰写博客布道分享,我们之后会继续翻译 Vue 官方博客详细说明。



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Migrating from Vue 2 to Svelte



您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎关注地球猫猫教。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7317222425384714294
收起阅读 »

让SQL起飞(优化)

最近博主看完了《SQL进阶教程》这本书,看完后给博主打开了SQL世界的新大门,对于 SQL 的理解不在局限于以前的常规用法。借用其他读者的评论, 读完醍醐灌顶,对SQL做到了知其然更能知其所以然。全书从头到尾强调了 SQL的内在逻辑是基于集合论和谓词逻辑,而...
继续阅读 »

最近博主看完了《SQL进阶教程》这本书,看完后给博主打开了SQL世界的新大门,对于 SQL 的理解不在局限于以前的常规用法。借用其他读者的评论,



读完醍醐灌顶,对SQL做到了知其然更能知其所以然。全书从头到尾强调了 SQL的内在逻辑是基于集合论和谓词逻辑,而这两条主线恰恰对使用SQL起到了至关重要的指导作用。



本文给大家总结如何让SQL起飞(优化)


一、SQL写法优化


在SQL中,很多时候不同的SQL代码能够得出相同结果。从理论上来说,我们认为得到相同结果的不同SQL之间应该有相同的性能,但遗憾的是,查询优化器生成的执行计划很大程度上受到SQL代码影响,有快有慢。因此如果想优化查询性能,我们必须知道如何写出更快的SQL,才能使优化器的执行效率更高。


1.1 子查询用EXISTS代替IN


当IN的参数是子查询时,数据库首先会执行子查询,然后将结果存储在一张临时的工作表里(内联视图),然后扫描整个视图。很多情况下这种做法都非常耗费资源。使用EXISTS的话,数据库不会生成临时的工作表。但是从代码的可读性上来看,IN要比EXISTS好。使用IN时的代码看起来更加一目了然,易于理解。因此,如果确信使用IN也能快速获取结果,就没有必要非得改成EXISTS了。


这里用Class_A表和Class_B举例,

我们试着从Class_A表中查出同时存在于Class_B表中的员工。下面两条SQL语句返回的结果是一样的,但是使用EXISTS的SQL语句更快一些。


--慢
SELECT *
FROM Class_A
WHERE id IN (SELECT id
FROM Class_B);

--快
SELECT *
FROM Class_A A
WHERE EXISTS
(SELECT *
FROM Class_B B
WHERE A.id = B.id);

使用EXISTS时更快的原因有以下两个。



  1. 如果连接列(id)上建立了索引,那么查询 tb_b 时不用查实际的表,只需查索引就可以了。(同样的IN也可以使用索引,这不是重要原因)

  2. 如果使用EXISTS,那么只要查到一行数据满足条件就会终止查询,不用像使用IN时一样扫描全表。在这一点上NOT EXISTS也一样。


实际上,大部分情况在子查询数量较小的场景下EXISTS和IN的查询性能不相上下,由EXISTS查询更快第二点可知,子查询数量较大时使用EXISTS才会有明显优势。


1.2 避免排序并添加索引


在SQL语言中,除了ORDER BY子句会进行显示排序外,还有很多操作默认也会在暗中进行排序,如果排序字段没有添加索引,会导致查询性能很慢。SQL中会进行排序的代表性的运算有下面这些。



  • GR0UP BY子句

  • ORDER BY子句

  • 聚合函数(SUM、COUNT、AVG、MAX、MIN)

  • DISTINCT

  • 集合运算符(UNION、INTERSECT、EXCEPT)

  • 窗口函数(RANK、ROW_NUMBER等)


如上列出的六种运算(除了集合运算符),它们后面跟随或者指定的字段都可以添加索引,这样可以加快排序。



实际上在DISTINCT关键字、GR0UP BY子句、ORDER BY子句、聚合函数跟随的字段都添加索引,不仅能加速查询,还能加速排序。



1.3 用EXISTS代替DISTINCT


为了排除重复数据,我们可能会使用DISTINCT关键字。如1.2中所说,默认情况下,它也会进行暗中排序。如果需要对两张表的连接结果进行去重,可以考虑使用EXISTS代替DISTINCT,以避免排序。这里用Items表和SalesHistory表举例:

我们思考一下如何从上面的商品表Items中找出同时存在于销售记录表SalesHistory中的商品。简而言之,就是找出有销售记录的商品。


在一(Items)对多(SalesHistory)的场景下,我们需要对item_no去重,使用DISTINCT去重,因此SQL如下:


SELECT DISTINCT I.item_no
FROM Items I INNER JOIN SalesHistory SH
ON I. item_no = SH. item_no;

item_no
-------
10
20
30

使用EXISTS代替DISTINCT去重,SQL如下:


SELECT item_no
FROM Items I
WHERE EXISTS
(SELECT
FROM SalesHistory SH
WHERE I.item_no = SH.item_no);
item_no
-------
10
20
30

这条语句在执行过程中不会进行排序。而且使用EXISTS和使用连接一样高效。


1.4 集合运算ALL可选项


SQL中有UNION、INTERSECT、EXCEPT三个集合运算符。在默认的使用方式下,这些运算符会为了排除掉重复数据而进行排序。



MySQL还没有实现INTERSECT和EXCEPT运算



如果不在乎结果中是否有重复数据,或者事先知道不会有重复数据,请使用UNION ALL代替UNION。这样就不会进行排序了。


1.5 WHERE条件不要写在HAVING字句


例如,这里继续用SalesHistory表举例,下面两条SQL语句返回的结果是一样的:


--聚合后使用HAVING子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
GR0UP BY sale_date
HAVING sale_date = '2007-10-01';

--聚合前使用WHERE子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
WHERE sale_date = '2007-10-01'
GR0UP BY sale_date;

但是从性能上来看,第二条语句写法效率更高。原因有两个:



  1. 使用GR0UP BY子句聚合时会进行排序,如果事先通过WHERE子句筛选出一部分行,就能够减轻排序的负担。

  2. 在WHERE子句的条件里可以使用索引。HAVING子句是针对聚合后生成的视图进行筛选的,但是很多时候聚合后的视图都没有继承原表的索引结构。


二、真的用到索引了吗


2.1 隐式的类型转换


如下,col_1字段是char类型:


-- 没走索引
SELECT * FROM SomeTable WHERE col_1 = 10;
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 ='10';
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = CAST(10, AS CHAR(2));

当查询条件左边和右边类型不一致时会导致索引失效。


2.2 在索引字段上进行运算


如下:


SELECT *
FROM SomeTable
WHERE col_1 * 1.1 > 100;

在索引字段col_1上进行运算会导致索引不生效,把运算的表达式放到查询条件的右侧,就能用到索引了,像下面这样写就OK了。


WHERE col_1 > 100 / 1.1

如果无法避免在左侧进行运算,那么使用函数索引也是一种办法,但是不太推荐随意这么做。使用索引时,条件表达式的左侧应该是原始字段请牢记,这一点是在优化索引时首要关注的地方。


2.3 使用否定形式


下面这几种否定形式不能用到索引。



  • <>

  • !=

  • NOT


这个是跟具体数据库的优化器有关,如果优化器觉得即使走了索引,还是需要扫描很多很多行的话,他可以选择直接不走索引。平时我们用!=、<>、not in的时候,要注意一下。


2.4 使用OR查询前后没有同时使用索引


例如下表:


CREATE TABLE test_tb ( 
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(55) NOT NULL
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用OR条件进行查询


SELECT * 
FROM test_tb
WHERE id = 1 OR name = 'tom'

这个SQL的执行条件下,很明显id字段查询会走索引,但是对于OR后面name字段的查询是需要进行全表扫描的。在这个场景下,优化器会选择直接进行一遍全表扫描。


2.5 使用联合索引时,列的顺序错误


使用联合索引需要满足最左匹配原则,即最左优先。如果你建立一个(col_1, col_2, col_3)的联合索引,相当于建立了 (col_1)、(col_1,col_2)、(col_1,col_2,col_3) 三个索引。如下例子:


-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 AND col_3 = 500;
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_3 = 500 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_3 = 500 ;
-- 走了索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_1 = 10 ;

联合索引中的第一列(col_1)必须写在查询条件的开头,而且索引中列的顺序不能颠倒。



可能需要说明的是最后一条SQL为什么会走索引,简单转化一下,col_2 = 100 AND col_1 = 10,
这个条件就相当于col_1 = 10 AND col_2 = 100,自然就可以走联合索引。



2.6 使用LIKE查询


并不是用了like通配符,索引一定会失效,而是like查询是以%开头,才会导致索引失效。


-- 没走索引
SELECT * FROM SomeTable WHERE col_1 LIKE'%a';
-- 没走索引
SELECT * FROM SomeTable WHERE col_1 LIKE'%a%';
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 LIKE'a%';

2.7 连接字段字符集编码不一致


如果两张表进行连接,关联字段编码不一致会导致关联字段上的索引失效,这是博主在线上经历一次SQL慢查询后的得到的结果,举例如下,有如下两表,它们的name字段都建有索引,但是编码不一致,user表的name字段编码是utf8mb4,user_job表的name字段编码是utf8,


CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER
SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`age` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `user_job` (
`id` int NOT NULL,
`userId` int NOT NULL,
`job` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

进行SQL查询如下:


EXPLAIN
SELECT *
from `user` u
join user_job j on u.name = j.name


由结果可知,user表查询走了索引,user_job表的查询没有走索引。想要user_job表也走索引,可以把user表的name字段编码改成utf8即可。


三、减少中间表


在SQL中,子查询的结果会被看成一张新表,这张新表与原始表一样,可以通过代码进行操作。这种高度的相似性使得SQL编程具有非常强的灵活性,但是如果不加限制地大量使用中间表,会导致查询性能下降。


频繁使用中间表会带来两个问题,一是展开数据需要耗费内存资源,二是原始表中的索引不容易使用到(特别是聚合时)。因此,尽量减少中间表的使用也是提升性能的一个重要方法。


3.1 使用HAVING子句


对聚合结果指定筛选条件时,使用HAVING子句是基本原则。不习惯使用HAVING子句的人可能会倾向于像下面这样先生成一张中间表,然后在WHERE子句中指定筛选条件。例如下面:


SELECT * 
FROM (
SELECT sale_date, MAX(quantity) max_qty
FROM SalesHistory
GR0UP BY sale_date
) tmp
WHERE max_qty >= 10

然而,对聚合结果指定筛选条件时不需要专门生成中间表,像下面这样使用HAVING子句就可以。


SELECT sale_date, MAX(quantity)
FROM SalesHistory
GR0UP BY sale_date
HAVING MAX(quantity) >= 10;

HAVING子句和聚合操作是同时执行的,所以比起生成中间表后再执行的WHERE子句,效率会更高一些,而且代码看起来也更简洁。


3.2 对多个字段使用IN


当我们需要对多个字段使用IN条件查询时,可以通过 || 操作将字段连接在一起变成一个字符串处理。


SELECT *
FROM Addresses1 A1
WHERE id || state || city
IN (SELECT id || state|| city
FROM Addresses2 A2);

这样一来,子查询不用考虑关联性,而且只执行一次就可以。


需要说明的MySql中,|| 操作符是代表或者也就是OR的意思。在Mysql中可以使用下面多种写法,如下:


-- 使用CONCAT(str1,str2,...)函数,将多列合并为一个字符串
SELECT *
FROM Addresses1 A1
WHERE CONCAT(id, state, city)
IN ('1湖北武汉', '2湖北黄冈');

-- 使用多列in查询
SELECT *
FROM Addresses1 A1
WHERE (id, state, city)
IN ((1, '湖北', '武汉'), (2, '湖北', '黄冈'));

使用多列in查询这个语法在实际执行中可以走索引,CONCAT(str1,str2,...) 函数不能。


3.3 先进行连接再进行聚合


连接和聚合同时使用时,先进行连接操作可以避免产生中间表。原因是,从集合运算的角度来看,连接做的是“乘法运算”。连接表双方是一对一、一对多的关系时,连接运算后数据的行数不会增加。而且,因为在很多设计中多对多的关系都可以分解成两个一对多的关系,因此这个技巧在大部分情况下都可以使用。


到此本文讲解完毕,感谢大家阅读,感兴趣的朋友可以点赞加关注,你的支持将是我更新动力😘。



作者:waynaqua
来源:juejin.cn/post/7221735480576245819
收起阅读 »

你的@Autowired被警告了吗

一个警告 近期组里来了新同学,依赖注入的时候习惯使用@Autowired,发现被idea黄色警告,跑过来问大家。由于我平时习惯使用@Resource,没太注意过这个问题,刚好趁着这个机会学习了一波。 首先看下问题的现象,使用@Autowired被idea警告,...
继续阅读 »

一个警告


近期组里来了新同学,依赖注入的时候习惯使用@Autowired,发现被idea黄色警告,跑过来问大家。由于我平时习惯使用@Resource,没太注意过这个问题,刚好趁着这个机会学习了一波。

首先看下问题的现象,使用@Autowired被idea警告,而使用@Resource则不会:


image.png


image.png


@Autowired和@Resource的差异


来源


  • @Resource是JSR 250中的内容,发布时间是2006年

  • @Autowired是Spring2.5中的内容,发布时间是2007年


@Resource是JSR的标准,Spring框架也提供了实现。既然@Autowired是在@Resource之后发布的,应该就有@Resource不能表达的含义或者不能实现的功能。


用法

注入方式

虽然我们开发中使用最多的方式是属性注入,但其实存在构造函数注意、set方法注入等方式。相比于@Resource支持的属性注入和set方法注入,@Autowired还能支持构造方法注入的形式,@Resource是不行的。


image.png


image.png


可指定属性

@Autowired支持required属性


image.png


@Resource支持7个其它属性


image.png


bean查找策略


  • @Autowired是类型优先,如果这个类型的bean有多个,再根据名称定位

  • @Resource是名称优先,如果不存在这个名称的bean,再去根据类型查找


查找过程


@Autowired

auto.png


@Resource

resource.png


思考


对于大多数开发同学来说,@Autowired 和 @Resource 差异是很小的,虽然 @Autowired 多支持构造器注入的形式,但是直接属性注入真的太灵活太香了。而@Autowired晚生于@Resource,既然已经有JSR标准的@Resource,还要增加1个特有Autowired,必然有Spring的考虑。

个人认为,构造器注入和支持属性不同这个理由是很弱的,这些特性完全可以在@Resource上实现,也不违反JSR的约束。比较可能的原因是,含义的不同,而最大的不同体现在bean查找策略上,@Autowired默认byType,@Resource默认byName,这个不同其实隐含了,@Resource注入的bean,更加有确定性,你都已经确定了这个bean的名称了,而类型在Java中编译过程本身是个强依赖,其实这里相当于指定了类型和名称,注入的是一个非常确定的资源。而@Autowired是类型优先,根据类型去查找,相比于@Resource,确定性更弱,我知道这里要注入bean的类型,但是我不确定这个bean的名称,也隐含体现了java多态的思想。


总结


回到开篇的idea的警告,网上有很多人都赞同的一种说法是,@Resource是JSR规范,@Autowired是Spring提供,不推荐使用绑定了Spring的@Autowired,因为@Resource在更换了框架后,依然可以使用。我不太赞同这种说法,因为idea的错误提示很明确,Field injection is not recommended,不推荐使用属性注入的方式,那换成@Resource,我理解并没有解决这个问题,虽然idea确实不警告了,可能有点掩耳盗铃的意思。

比较推荐的做法是,使用构造方法注入,虽然很多时候会有点麻烦,特别是增加依赖的时候,但是正是这种麻烦,会让你不再那么随意做属性注入,更能保持类的职责单一。然后配合lombok,也可以让你省去这种麻烦,不过希望还是能通过这个警告时刻提醒自己保持类的职责单一。


image.png


作者:podongfeng
来源:juejin.cn/post/7265926762729717819
收起阅读 »

什么?你现在还想用微信小程序做副业?

前言 相信各位程序猿/程序媛小朋友都有想创业赚外快的梦想,梦想哪一天自己能做出来个爆款产品,用户量蹭蹭的涨,真金白银哗哗的涌进来,挡都挡不住,从此当上总经理、出任CEO、赢取白富美、走上人生巅峰。或者退而求其次,产品虽然不那么火。但也可以让自己每天躺着也能挣钱...
继续阅读 »

前言


相信各位程序猿/程序媛小朋友都有想创业赚外快的梦想,梦想哪一天自己能做出来个爆款产品,用户量蹭蹭的涨,真金白银哗哗的涌进来,挡都挡不住,从此当上总经理、出任CEO、赢取白富美、走上人生巅峰。或者退而求其次,产品虽然不那么火。但也可以让自己每天躺着也能挣钱,副业的收入就足够日常开销,那个班随便上上就行了,还卷什么卷。


而做副业的各种途径中,做微信小程序无疑是门槛比较低的途径之一,微信国内几乎每个人都在用,那数量庞大的用户和流量无人能及,小程序天然跨平台,运行体验也比原生h5似乎流畅那么一点点。


最关键的是开发成本低啊,只要有一点点JS基础,分分钟就能搞出来个能跑的小程序,不会后端也没关系,有云开发,照着文档,基本的增删改查也是分分钟就能搞定。


注册一个账户很简单,做完的小程序上架也很简单。一切都很简单有木有。看到这里你是不是想立马手搓个小程序试试?


然而,时代已经变了,看似简单的小程序做起来已经不那么简单了。现在我已经不建议大家在做副业的时候选择微信小程序了。


回顾


在说明为什么不建议再做小程序之前,先来回顾一下个人做小程序之前为什么是可行的。


创业也好,做副业也好,首先得看大趋势,雷总不是有句名言嘛,“站在风口上,猪都能飞起来”,换句话说,赚钱要趁早,智能手机刚起步的时候去做app,微信刚推出小程序的时候你去做小程序,那赚钱的可能性都比较大,毕竟是蓝海,而现在,app,小程序啥的早就是一片红海了。有多么红海,你随便搜一下“小程序已死”就知道了,基本上有需求的地方都已经有小程序占好坑了。对个人做小程序来讲那时也是最好的时候,做成的概率也比较大。


当然,错过了风口也不是完全没机会,虽然小程序变红海已经好长时间了,但是架不住万一你突然发现了哪个小众需求,或者蹭到了哪个热点,比如前一段火的不行的短视频去水印,虽然靠这个没法财富自由,但赚个零花钱还是可以的。


做任何事情都是开始最困难,不是有句老话叫“万事开头难”么。而小程序曾经就可以把这个开头的难度降低很多,除了上面讲的开发难度低,另一个就是试错的成本也低。如果一个小程序做不起来可以迅速再搞一个其他小程序试试,甚至可以弄出来一堆小程序,哪个能活下来就重点运营,其他不行的可以放弃后再搞新的,基本上除了开发没有其他的成本。


最重要的一个点就是,虽然平台对个人主体仅开放了有限的类目,理论上讲,个人能做的小程序基本上被限制在工具类里面,比如都被做烂了的记账,备忘录啥的。我们都知道小程序的分享能力是非常重要的,可以分享才能裂变出新用户,新流量。


虽然原则上你的小程序是不能把用户创作的内容分享出去的,一旦涉及到分享,就被归为社交类目,而这个类目是不对个人开放的,举个例子,你搞了个制作头像的小程序,那用户制作好的头像是不能发给别人的,只能保存在本地。


但是呢,之前微信小程序平台对这些个规则执行的并不是十分严格。这就存在一些可操作的空间,让你上线一些只有公司才可以做的小程序。


所以,这里就有了一个可以低成本试错的路径,你可以先利用这个模糊空间,拍脑门也好,蹭热点也罢,先以个人名义上线小程序,如果不理想,就直接弃用或者换名字改成其他类型的小程序再试,扔掉的成本基本上就只是开发成本。如果不小心成了爆款,那恭喜你,可以去成立个公司啥的,再把小程序迁移过来,然后成为CEO,赢取白富美,走上人生巅峰。。。


dead.jpg
不幸的是,现在,这条路已经被堵死了


变化


为啥说路被堵死了,因为最近发生了一些变化,让个人开发者的试错成本直线上升,甚至可以说根本无路可走了。


创业或者搞副业无非就是要赚钱,收入减去成本就是赚到的钱。那就从成本,收入两个角度来分析一下发生了什么导致现在搞小程序很难赚到钱。


成本


先看成本,成本飙升到能把个人开发者的路都堵死,还得从微信这一年的一系列操作说起。这波操作总结起来就是一个字:“得加钱”


ac608998885aa3fee54ec48980361533.jpg


云开发,个人开发者用来快速整个后端,一年多前起步免费,现在起步价:优惠19.9/月,原价:39.9/月,起步价里面包含的资源也就够你开发调试用吧,上线以后超出部分另算。


个人小程序的自然流量基本上都依靠搜索,在We分析里可以看到用户通过搜索哪些关键词访问到小程序,开发者可以相应的做搜索词优化来引流。自12月18日起,这项功能也开始收费了,起步价388/年,这个起步价啥概念?你的小程序过去一段时间日打开次数得小于100次。。。


以上都是平a,下面这个才是大招,“必杀技:备案+认证无敌真伤斩杀组合拳”


先说认证,认证这事之前仅对企业等非个人主体开放的,300一个小程序,不管认证能不能通过,如果通过了就是永久有效。个人小程序无需认证。


然而,从下半年开始,个人小程序也需要认证了,目前优惠价30每个小程序,而且认证有效期只有一年了。不认证也可以,那你的小程序就别想被搜索到,也别想往外分享了。


你说你去认证吧,就会遇到另外一个问题,认证的时候要审核你的小程序的名字。做过小程序的都知道,名字可太重要了。直接决定了你的小程序能不能被搜索到。所以之前大家起名几乎都是在直接对标搜索热门词,什么“群签到助手”,什么“去水印神器”之类的。


然而现在在认证的时候这些名字统统不合格,名字不能太宽泛,必须含有个人特征。例如,张三做了个“记事本”小程序去认证,那名字几乎得叫“张三的记事本”之类才好通过。想蹭流量?门都没有。


再说下一个,备案。从9月4号开始,所有新小程序必须备案后才能上线,已上线的小程序在明年4月之前必须完成备案,否则就下架。


平台会对要备案的小程序做初审,这个初审就相当严格了。把个人小程序直接按死在那有限的几个类目里。基本上现在个人只能做记事本三件套了。前面说的模糊空间已经不存在了,路也就堵死了。


你要说能不能先通过备案,然后在新版本里添上新功能?不好意思,现在对版本的审核也很严格。以前几个小时就能审好,现在几天都是正常的,有超过资质范围的功能直接打回。


备案也会对小程序名字做审核,你要问啥样的名字能过审?别问我,我也不知道。


由于认证和备案不是同一拨人在搞,所以如果你的小程序不太幸运的话会遇到以下问题:


30认证 --> 认证不通过 --> 改名+30认证 --> 认证通过 --> 备案不通过 --> 改名+30认证。。。


就说为了个小程序,你能不能经得住这样的折腾吧。


至于看不懂的文档,日常的违规警告,无法找到的人工客服,随便废弃接口,隐私协议闹剧(具体可看《各位微信小程序的开发者们,你们还好吗?》)等等大家都早已经麻了。


成本讲完了,下面说说收入。


收入


什么?你还想着有收入?收入就是没有收入!(全文完)


------------------------- 分 ----- 割 ----- 线 ------------------------


说没有收入,是玩笑话,也不是玩笑话。小程序除非你另有门路,大部分个人开发者就只能靠当流量主接入平台的广告赚钱。然而,现在广告的ecpm虽然不能说聊胜于无,也可以说是惨不忍睹。并且这下降的趋势似乎还看不到头,简直和股市一个尿性。


就那点广告费收入,再减去上面说的成本,能不亏钱就不错了,至于收益,这么说吧,
去各大平台薅羊毛,每天在某东签个到,在某宝果园里种个树,农场里养个鸡,或者在某音某手放一个你好基友啪叽在冰面上摔个狗啃泥的短视频,获得的收益都可能比苦哈哈做小程序的收益多。


某多多除外,你永远都别想薅到某多多的羊毛。


总结


个人做小程序的成本和收益都写在上面了,干货满满。各位想着做副业的小朋友看完之后不妨自己先算算账,然后再决定要不要做小程序。另外大家对此还有什么想吐槽的,欢迎在讨论区一起聊聊。


作者:ad6623
来源:juejin.cn/post/7317104726768697398
收起阅读 »

降本增笑,领导要求程序员半年做出一个金蝶

关于降本增效的事,今天在网上看到一个帖子,虽然有点搞笑,但是对于打工人(特别是技术同学)来说,可挖掘的东西也不少,特别分享给大家。 真的是好多年没听说这样的事了,记得以前总有老板让做个淘宝、做个京东,然后预算只有几千块,最多几万块。 这些故事程序员谈起来往往...
继续阅读 »

关于降本增效的事,今天在网上看到一个帖子,虽然有点搞笑,但是对于打工人(特别是技术同学)来说,可挖掘的东西也不少,特别分享给大家。



真的是好多年没听说这样的事了,记得以前总有老板让做个淘宝、做个京东,然后预算只有几千块,最多几万块。


这些故事程序员谈起来往往都是哈哈一笑,并疯狂吐槽一番。



不过笑过之后,大家是否想过如何去解决问题?或者真的去评估下可行性,探索一下可能的实现路径。


找到问题


首先我们看下老板的问题。老板的根本问题并不是想要做金蝶,为什么这么说呢?


我们看看网友的描述就知道了:经济下行,领导不想出金蝶系统的维护费,不想为新功能花大价钱。这才是根本问题,用四个字来说就是:降低成本。


然后才是老板想到能不能用更少的钱达到金蝶系统的使用效果,再之后才是自己能不能做一个类似金蝶的系统,并思考了自己可以承担的成本:一个前端、一个后端,半年时间。


最后问题被抛到了这位网友的手里。可以看得出来这位网友也不太懂,还去咨询了朋友。不知道它有没有向朋友说清楚问题,还是只说了老板想自己做一个金蝶系统,结果是朋友们都说不可行。


遇到问题时,我们得把这个问题完完整整的捋一遍,找到最根本的问题,然后再说怎么解决问题,否则只是停留在表面,就可能事倍功半。在这个上下文中,根本的问题就是:降低成本。



解决问题


明确了老板的根本问题,我们就可以琢磨方案了。既然是降低财务系统的成本,可行的方案应该还是有几个的。


使用替代产品


假如公司只使用产品的部分功能,是不是可以选择金蝶的低版本?是不是可以降低一些人头费?


金蝶的服务贵,是不是可以选择一些小厂的产品?国内做财务系统的应该挺多的,小厂也更容易妥协。


或者选择SaaS服务,虽然SaaS用久了成本也不低,但是可以先撑过这几年,降低当前的支出。


当然替换财务系统也是有成本的,需要仔细评估。不过既然都想自己做了,这个成本应该能hold住。


找第三方维护


金蝶的服务贵,是不是可以找其它三方或者个人来维护修改?根据我之前的了解,金蝶这种公司有很多的实施工作是外包出去的,或者通过代理商来为客户服务,能不能找这些服务商来代替金蝶呢?或者去某些副业平台上应该也能找到熟悉金蝶系统的人。


当然这个还要看系统能不能顺利交接,金蝶有没有什么软硬件限制,第三方能不能接过来。


另外最重要的必须考虑系统和数据的安全性,不能因小失大。


自己开发


虽然自己开发的困难和成本都很高,但我仍旧认为这可能也是一个合适的解决方案。理由有下面两点。



  • 功能简单:如果公司的业务比较简单,使用的流程也简单,比如不使用涉及复杂的财务处理,那么捋一捋,能给开发人员讲清楚,也是有可能在短时间内完成的。

  • 迭代渐进:长城不是一天建成的,系统也都是逐渐迭代完善的。自己开发可以先从部分模块或者功能开始,然后逐步替换,比如前边的流程先在新系统中做,最后再导入金蝶。即使不能做到逐步替换,也可以控制系统的风险,发现搞不定时,及时止损。相信老板也能明白这个道理,如果不明白或者不接受,那确实搞不了。



当然我们也肯定不能忽视这其中的困难。我之前做过和金蝶系统的对接,订单的收付款在业务系统完成,然后业务系统生成凭证导入到金蝶K3。依稀记得业务也不算复杂,但是需求分析做了好几遍,我的代码也是改了又改,上线之后遇到各种问题,继续改,最终花了几个月才稳定下来。


事后分析原因,大概有这么几点:



  • 产品或者需求分析人员没接触过类似的业务,即使他对财务系统有一些经验,也不能准确的将客户的业务处理方式转换到产品设计中;

  • 财务人员说不明白,虽然他会使用金蝶系统,但是他不能一次性的把所有规则都讲出来,讲出来也很难让程序员在短时间内理解;

  • 程序员没做过财务系统,没接触过类似的业务,系统的设计可能要反复调整,比如业务模块的划分逻辑,金额用Long还是用BigDecimal,数据保留几位小数,这都会大幅延长开发周期,如果不及时调整就可能写成一锅粥,后期维护更困难。


这还只是和金蝶系统做一个简单的对接,如果要替代它,还要实现更多的功能,总结下,企业可能会面对下面这些困难:


业务复杂:财务规则一般都比较复杂,涉及到各种运算,各种数字、报表能把人搞晕。如果公司的业务也很复杂,比如有很多分支或者特殊情况,软件开发的难度也会很大,这种难度的变化不是线性增加的,很可能是指数级增长的,一个工作流的设计可能就把人搞死了。


懂业务的人:系统过于复杂时,可能没有一个人能把系统前前后后、左左右右的整明白。而要完成这样一个复杂的系统,必须有人能从高层次的抽象,到具体数字运算的细枝末节,完完全全的整理出来,逻辑自洽,不重不漏,并形成文档,还要能把程序员讲明白。


懂架构的人:这里说的是要有一个经验丰富的程序员,不能是普通的码农,最好是有财务系统开发经验的架构师。没走过的路,总是要踩坑的。有经验的开发人员可以少走很多弯路,极大降低系统的风险。这样的人才如果公司没有,外招的难度比较大,即使能找到,成本也不低。


灵活性问题:开发固定业务流程的系统一般不会太考虑灵活性的问题,如果业务需要调整,可能需要对系统进行大幅修改,甚至推倒重来。如果要让系统灵活些,必然对业务和技术人员都提出了更高的要求,也代表着更强的工作能力和更多的工作量。


和其它系统的对接:要不要和税务系统对接?要不要和客户管理系统对接?要不要和公司的OA对接?每一次对接都要反复调试,工作量肯定下不来。


总之,稍微涉及到财务处理的系统,都不是一个前端和一个后端能在短时间内完全搞出来的。


对程序开发的启示


搞清楚需求


日常开发过程中,大家应该都遇到过不少此类问题。领导说这里要加个功能,然后产品和开发就去吭哧吭哧做了,做完了给领导一看,不是想要的,然后返工反复修改。或者说用户提了一个需求,产品感觉自己懂了,然后就让开发这么做那样改,最后给用户一看,什么破玩意。这都是没有搞清楚真正的需求,没有触达那个根本问题。


虽然开发人员可以把这些问题全部甩给产品,自己只管实现,但这毕竟实实在在的消耗了程序员的时间,大量的时间成本和机会成本,去干点有意义的事情不好吗?所以为了不浪费时间,开发也要完整的了解用户需求。在一个团队中,至少影响产品落地的关键开发人员要搞懂用户的需求。


那么遇到这种问题,程序员是不是可以直接跑路呢?


也是一个选择, 不过对于一个有追求的程序员,肯定也是想把程序设计好、架构好的,能解决实际问题的,这也需要对用户需求的良好把控能力,比如我们要识别出哪些是系统的核心模块,哪些是可扩展能力,就像设计冯诺依曼计算机,你设计的时候会怎么处理CPU和输入输出设备之间的关系呢?


对于用户需求,产品想的是怎么从流程设计上去解决,开发需要考虑的是怎么从技术实现上去满足,两者相向而行,才能把系统做好。


当然准确把握用户的需求,很多时候并不是我说的这么容易,因为用户可能也说不清楚,我们可能需要不断的追问才能得到一些关键信息。比如这位网友去咨询朋友时,可能需求就变成了:我们要做一个财务系统,朋友如果不多问,也只能拿到这个需求,说不定这位朋友也有二次开发的能力,错失了一次挣钱的好机会。还有这位老板上边可能还有更大的老板,这位老板降低成本的需求也可能是想在大老板面前表现一下,那是不是还有其它降本增效的方法呢?比如简化流程、裁掉几个不关键的岗位(这个要得罪人了)。


我们要让程序始终保持在良好的状态,就要准确的把握用户需求,要搞懂用户需求,就需要保持谦逊求知的心态,充分理解用户的问题,这种能力不是朝夕之间就可以掌握的,是需要修炼的。


动起来


任何没有被满足的需求都是一次机会。


我经常会在技术社区看到一些同学分享自己业余时间做的独立产品,有做进销存的、客户管理的、在线客服的,还有解决问题的各种小工具,而且有的同学还挣到了钱。


我并不是想说让大家都去搞钱,而是说要善于发现问题、找到机会,然后动起来、去实践,实践的过程中我们可以发现更多的问题,然后持续解决问题,必然能让自己变得越来越强。在经济不太好的情况下,我们才有更强的生存能力。




啰里八嗦一大堆,希望能对你有所启发。


关注萤火架构,加速技术提升。


作者:萤火架构
来源:juejin.cn/post/7317704464999235593
收起阅读 »

2023市场需求最大的8种编程语言出炉!

众所周知,编程语言的种类实在是太多了。直到现在,经常还会看到关于编程语言选择和学习的讨论。 虽说编程语言有好几百种,但实际项目使用和就业要求的主流编程语言却没有那么多。 大家可能也会好奇:现如今就业市场上到底什么编程语言最受欢迎?或者说最需要的编程语言是什么?...
继续阅读 »

众所周知,编程语言的种类实在是太多了。直到现在,经常还会看到关于编程语言选择和学习的讨论。


虽说编程语言有好几百种,但实际项目使用和就业要求的主流编程语言却没有那么多。


大家可能也会好奇:现如今就业市场上到底什么编程语言最受欢迎?或者说最需要的编程语言是什么?


所以今天我们就结合Devjobsscanner之前发布的「Top 8 Most Demanded Programming Languages in 2023」编程语言清单来聊一聊这个问题。



虽说这个清单并不是完全针对我们本土开发者的调查,但还是能反映一些趋势的,大家顺带也可以参看一下各种编程语言的发展趋势和前景。


Devjobsscanner是一个综合性开发者求职/岗位信息聚合网站。



上面聚合展示了很多开发者求职岗位信息,并按多种维度进行分类,以便用户进行搜索。



该网站每年都会发布一些相关方面的调查总结报告,以反映开发者求职方面的趋势。


从2022年1月到2023年5月,这17个月的时间里,Devjobsscanner分析了超过1400万个开发岗位,并从中进行筛选和汇编,并总结出了一份「2023年需求量最大的8种编程语言」榜单。


所以下面我们就来一起看一看。


No.1 JavaScript/TypeScript


基本和大家所预想到的一样,Javascript今年继续蝉联,成为目前需求最大的编程语言。



当然这也不难理解,因为基本上有Web、有前端、有浏览器、有客户端的地方都有JavaScript的身影。


而且近几年TypeScript的流行程度和需求量都在大增,很多新的前端框架或者Web框架都是基于TypeScript编写的。


所以学习JavaScript/TypeScript作为自己的主语言是完全没有问题的。



  • 职位数量/占比变化趋势图



No.2 Python


榜单上排名第二的是Python编程语言。



众所周知,Python的应用范围非常广泛。


从后端开发到网络爬虫,从自动化运维到数据分析,另外最近这些年人工智能领域也持续爆火,而这恰恰也正是Python活跃和擅长的领域。


尤其最近几年,Python强势上扬,这主要和这几年的数据分析和挖掘、人工智能、机器学习等领域的繁荣有着不小的关系。



  • 职位数量/占比变化趋势图



No.3 Java


榜单中位于第三需求量的编程语言则是Java。



自1995年5月Java编程语言诞生以来,Java语言的流行程度和使用频率就一直居高不下,并且在就业市场上的“出镜率”很高。


所以每次调查结果出来,Java基本都榜上有名,而且基本长年都维持在前三。


Java可以说是构成当下互联网繁荣生态的重要功臣,无数的Web后端、互联网服务、移动端开发都是Java语言的领地。



  • 职位数量/占比变化趋势图



No.4 C#



看到C#在榜单上位列前四的那会,说实话还是挺意外的,毕竟自己周围的同学和同事做C#这块相对来说还是较少的。


但是C#作为一种通用、多范式、面向对象的编程语言,在很多领域其实应用得还是非常广泛的。


我们都知道,其实像.NET和Unity等框架在不少公司里都很流行的,而C#则会被大量用于像Unity等框架的项目编写。



  • 职位数量/占比变化趋势图



No.5 PHP



看到PHP在榜单上位列第五的时候,不禁令人又想起了那句梗:


不愧是最好的编程语言(手动doge)。


所以以后可不能再黑PHP了,看到没,这职位数量和占比还是非常高的。



  • 职位数量/占比变化趋势图



No.6 C/C++


C语言和C++可以说都是久经考验的编程语言了。



C语言于1972年诞生于贝尔实验室,距今已经有50多年了。


自诞生之日起,C语言就凭借其灵活性、细粒度和高性能等特性获得了无可替代的位置,而且随着如今的万物互联的物联网(IoT)时代的兴起,C语言地位依然很稳。


C语言和C++的应用领域都非常广泛,在一些涉及嵌入式、物联网、操作系统、以及各种和底层打交道的场景下都有着不可或缺的存在意义。



  • 职位数量/占比变化趋势图



No.7 Ruby


Ruby这门编程语言平时的出镜率虽然不像Java、Python那样高,但其实Ruby的应用领域还是挺广的,在包括Web开发、移动和桌面应用开发、自动化脚本、游戏开发等领域都有着广泛的应用。



Ruby在20世纪90年代初首发,并在2000年代初开始变得流行。


Ruby是一种动态且面向对象的编程语言,语法简单易学,使用也比较灵活,因此也吸引了一大批爱好者。



  • 职位数量/占比变化趋势图



No.8 GO


虽说Go语言是一个非常年轻的编程语言(由谷歌于2009年对外发布),不过Go语言最近这几年来的流行程度还是在肉眼可见地增加,国内外不少大厂都在投入使用。



众所周知,Go语言在编译、并发、性能、效率、易用性等方面都有着不错的表现,也因此吸引了一大批学习者和使用者。



  • 职位数量/占比变化趋势图



完整表单


最后我们再来全局看一看Devjobsscanner给出的编程语言完整表单和职位数量/占比的趋势图。




不难看出,JavaScript、Python和Java这三门语言在就业市场上的需求量和受欢迎程度都很大,另外像C语言、C#、Go语言的市场岗位需求也非常稳定。


总体来说,选择清单里的这些编程语言来作为自己的就业主语言进行学习和精进都是没有问题的。


说到底,编程语言没有所谓的好坏优劣,而最终选择什么,还是得看自己的学习兴趣以及使用的场景和需求。


作者:CodeSheep
来源:juejin.cn/post/7316968265057828874
收起阅读 »

防御性编程?这不就来了

最近程序员界又重新流行起来了防御性编程这个概念,早期嘞,这个概念代表是一种细致、谨慎的编程方法。 防御性编程的目的是为了开发可靠的软件,我们在设计系统中每个组件的时候,都需要使其尽可能的 "保护" 自己。 但是 2023 年以来,国内的互联网市场是什么行情,相...
继续阅读 »

最近程序员界又重新流行起来了防御性编程这个概念,早期嘞,这个概念代表是一种细致、谨慎的编程方法。


防御性编程的目的是为了开发可靠的软件,我们在设计系统中每个组件的时候,都需要使其尽可能的 "保护" 自己。


但是 2023 年以来,国内的互联网市场是什么行情,相信大家都清楚,整个市场环境都在强调降本增效、开猿节流。


因此为了体现程序员们在公司代码中的不可替代性?防止被裁。"防御性编程" 概念又重新流行了起来。


不过这次它可不再是保护程序了,而是保护广大程序员群体 😎。



所以我就给大家介绍一下,新时代背景下的 "防御性" 编程理念,如何实践 😜。


本文大纲如下,



代码书写


变量名称使用单一字符


Java 语言里变量名只能由 Unicode 字母、数字、下划线或美元符号组成,并且第一个字符不能是数字


那么对于单一字符的变量名称来说,26 个字母大写加 26 个字母小写加下划线以及美元符一共有 54 种变量名称,想一想难道这些还不够你在单个 Java 文件里给变量命名用吗?


兄弟这一般够用了。


使用中文命名


兄弟,大家都是中国人,肯定看得懂中文咯。



就问你,Idea 支不支持吧,有没有提示说你变量名不规范嘛!没提示就是规范。



还有一点,兄弟们,还记得上面 Java 语言里变量名组成规范吗?中文也在 Unicode 编码里面,所以其实我们还可以用中文作为变量名称。


我已经帮你查好了,Java 里常用的 utf-8 编码下,支持的中文字符有 20902 个,所以上面单一字符的变量名称还需要新增 20902 种 😃,简直完美。



使用多国语言命名



不多说,我就问你看不看得懂吧,看得懂算你厉害,看不懂算你技术不行。



你问我看不看得懂,我当然看的懂,我写的,我请百度翻译的 😝。





这些变量名称命名法则,不仅适用与 Java,也适用于 JavaScript,广大前端程序员也有福了。


CV 大法


不要抽象、不要封装、不要继承、不要组合,我只会 CV。


抽象



抽象:我可以让调用者只需要关心方法提供了哪些功能,而不需要知道这些功能是如何实现的。我的好处是可以减少信息的复杂度,提高代码的可读性和易用性,也方便了代码的修改和扩展,我厉害吧。


我:我只会 CV。


抽象:...



封装



封装:我可以把数据和基于数据的操作封装在一起,使其构成一个独立的实体,对外只暴露有限的访问接口,保护内部的数据不被外部随意访问和修改。我的好处是可以增强数据的安全性和一致性,减少代码的耦合性,也提高了类的易用性。看见没,我比抽象好懂吧。


我:我只会 CV。


封装:...



继承



继承:我可以让一个类继承另一个类的属性和方法,从而实现代码的复用和扩展。我可以表示类之间的 is-a 关系,体现了类的层次结构和分类。我的好处是可以避免代码的重复,简化类的定义,也增加了代码的维护性。我可是面向对象三大特征之一。


我:我只会 CV。


继承:...



组合



组合:我可以让一个类包含另一个类的对象作为自己的属性,从而实现代码的复用和扩展。我可以表示类之间的 has-a 关系,体现了类的关联和聚合。我的好处是可以增加类的灵活性和可变性,也降低了类之间的耦合性。不要用继承,我可是比继承更优秀的。


我:我只会 CV。


组合:...



不要问为什么我只会 CV,因为我的键盘只有 CV。



刚出道时我们嘲讽 CV,后来逐渐理解 CV,最后我们成为 CV。


CV 的越多,代码就越复杂,代码越复杂,同事就越难看懂,同事越难看懂,就越难接手你的代码,你的不可替代性就越来越强。


那么我们防御性编程的目的不久达到了嘛。


兄弟,听我说,给你的代码上防御,是为了你好!



产品开发


运营配置、开发配置、系统配置直接写死,用魔法值,没毛病。


产品每次提需求,代码实现一定要做到最小细粒度实现,做到需求里少一个字,我的代码里绝不会多一个词,注释也是不可能有的,我写的代码只有我看得懂不是防御性编程的基操吗?


我的代码我做主。


产品原型不提,我绝对不会问。要做到这系统有你才能每一次发版上线都是相安无事,一旦缺少了你,鬼知道会发生什么。


我们能做的就是牢牢把握项目中核心成员的位置。这个项目组少了你,绝对不行!



最后聊两句


2023 全年都在降本增效,节能开猿的浪潮下度过。


虽然本文是给大家讲防御性编程如何实践,但终究只是博君一笑,请勿当真。


这里我还是希望每一个互联网打工人都能平稳度过这波寒冬。


积蓄力量,多思考,多元发展。


在来年,春暖花开,金三银四之月,都能找到自己满意的工作,得到属于自己的果实。



关注公众号【waynblog】,每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力😘。



作者:waynaqua
来源:juejin.cn/post/7312376672665075722
收起阅读 »

2023年度总结 | 迷茫

一年一年又一年 一岁一岁又一岁 岁岁愁   都说35是程序猿的一道坎,30的我也开始慌,开始愁了,都说应该要有个副业,可又不知道做什么,要是以后失业了,也不知道能转行做啥,好愁~有没大佬带飞  (இωஇ)没钱 失业寻找工作   22年底公司项目砍了然后就是被...
继续阅读 »

一年一年又一年

一岁一岁又一岁

岁岁愁



  都说35是程序猿的一道坎,30的我也开始慌,开始愁了,都说应该要有个副业,可又不知道做什么,要是以后失业了,也不知道能转行做啥,好愁~有没大佬带飞  (இωஇ)没钱


失业寻找工作


  22年底公司项目砍了然后就是被离职了,当时疫情还没彻底解封,刚刚找了2个星期的工作,所在的小区就被封了。我是住在广州海珠区康乐村隔壁小区的,当时康乐村可谓闹得人人皆知,疫情的严重带,导致小区被封了1个月。之后在刚解封的时候,我又立刻中招了,发烧几天,咳嗽了也差不多一个月,期间也没怎么去找过工作,也接近过年了。

  虽然没了工作,但还是要开开心心的过个年的啦,工作什么的,年后再考虑了。

  过完年之后,疫情基本也全面放开了,不影响找工作了,就开始了每天疯狂的投投投,拉勾上看到一个符合的就投一个,保持在每天至少面一次的频率。奈何技术相对较差,又是工作了7年的老猿,面的都是小厂,但面试上还是有点不堪。面了两个星期,最终拿到两个offer,开出的条件几乎一样,都是单双休。虽然条件还是不太满意,但迫于已经几个月没工作了,每个月还有房贷在,还是有点压力的,就选择了各方面相对好一丢丢的一家,也就是目前在职的这一家。


入职搬砖


  二月底进行入职,入职之后先是熟悉环境,自己配置下电脑的开发环境,一天就过去了。第二天,就开始分配任务了,了解到我所在的项目是刚开的新项目,Android端一直没招到人开发,但服务端和iOS端已经是22年底就开始开发了的,已经领先了3个月。刚开始听到这个,感觉还是有压力的,相当于我需要赶进度去追这3个月的落差。所幸在我开始开发的时候,产品经理也跟我说了,按正常速度开发就行,不需要去加班加点赶。嗯,那还好,不然这3个月的差距,至少也得加班1-2个月才能赶上。


开始搬砖


  刚开始,还是有点懵的,产品经理说项目文档是别人出的,他也是上年底入职接手的,所以很多文档没看到,也可能是没出,就让我直接对着设计稿和iOS的做就可以了。之后我就拿了iOS的过来看,和设计稿进行了一次对比,又发现iOS和设计稿的有很多差别,iOS就跟我说他是最新的,有些细节和UI沟通过后,他没有进行修改设计稿,而是直接让iOS修改,所以让我直接照着iOS弄就可以了。emmmm....这给后面埋了个巨坑。

  由于Android这边有java和kotlin两种语言,于是问了下主管,主管让我用java去开发,也许考虑kotlin还不完善,怕项目出什么bug吧。拿着设计稿和iOS的app,就开始了疯狂敲代码模式。kotlin我是刚学了几个月,还不是太熟,但java可是用了6年多的了,那是嘎嘎香,拿着之前封装好的框架就是一顿套,什么网络请求框架、MVVM、图片加载Glide、BaseActivity等,套完之后,项目的雏形就完成了。不得不说,选择java也让我能直接使用已经封装的框架,要是kotlin,那就只能先慢慢搭建一套了,毕竟之前没怎么用过kotlin实际开发过项目,所以没什么存货。

  项目开始时,也了解到目前iOS端只是搭建了界面,服务端接口还未提供,数据模型等还没出,大概时服务端也是刚加入项目不久吧。所以iOS这期间基本都是对界面的修修补补,直到服务端出接口文档才能继续下一步,这刚好也能给我点追赶的时间。

  经过一顿嘎嘎猛敲,时间来到了4月中,期间服务端接口也出了,iOS也对接了,接口基本是没问题了,1个半月左右,我的界面也基本搭完了,就开始对接接口。刚开始还是有点难点,得按照服务端制定的规则进行加解密请求数据,编写的时候经常因为顺序和服务端的对不上导致加解密后数据是错的,调了整整2天才把加解密的调通。调通之后,其它也没啥了,正常的对接数据模型、对接接口、测试数据是否正确啥的。


搬砖完毕,进行检验


  5月初自测没问题之后进行提测了,iOS期间因为数据模型和他原本使用的模型对不上,说是改动很大,所以花了很多时间,最终和我同时提测的。

  重点来了,提测之后,测试部反馈了200+的bug数量...我当时就震惊了,我工作7年了,就没遇到能有200+bug的,这是什么神测试?结果...一看提的测试问题,一半以上是界面问题。emmmm...巨坑出来了,测试说和设计稿没对上,我说我是照着iOS做的,iOS说他是最新的,和UI对过没问题的。嗯,我以为是真的没问题,结果,UI设计师说他没说iOS那个是最新的,那些iOS虽然和他讨论过,但是他没确认可以,是iOS自己修改的。emmmmm...什么神操作,只是讨论过,没有设计师的确认,就自己修改了,还跟我说是最新的,和UI设计师确认过了的。emmmmm...能怎么办,改呗!!

  改了半个月,终于全部没问题可以上线了,真是艰难啊!!7年来,第一次遇到这种操作,真是糟心了!!期间iOS的其它问题,也不拿出来说了,评论别人不太合适。


收楼装修走起


  转眼来到6月份啦,房子终于建好收楼了,期间还是有点担心会烂尾的,毕竟恒大那么大的盘,说烂尾就烂尾,说破产就破产,要是变成烂尾楼,那简直就是天塌了,白花花的银子就全没了。

  收楼后就要开始搞装修的事啦~原本没打算今年装修的,还想再存点钱,毕竟装修要花一大笔,但父母说什么今年双春,明年没春,所以要今年入伙,就只有火急火燎的去看看装修了。考虑单双休,没时间去看,所以就找了装修全包的公司。找了1个多月,对比了5、6家公司,最终确定了一家,也差不多到10月初了,才进行签合同。期间又要物业审核什么的,拖了一个多月,11月中才开始装修。


第二个版本


  中间基本都是优化迭代,后面第二个大版本,从8月中开始,这次终于没有坑了,可以对着设计稿、交互文档来正常开发了。一开始主管要我们给个版本开发时间,因为内容有点多,加上有几个界面需要进行自定义的,我就预计了接近2个月。iOS看了就吐槽我说这么快,哪里弄的完,界面那么复杂...我也没法说啥,毕竟我预估的,已经很充足的了,再长就真的有点说不过去了。后面开发下来,也确实是差不多的,10月初就提测了,还不到两个月...然而...iOS到12月中才进行提测,这让我有点震惊。期间我都迭代了1个版本了,只能说主管貌似对进度不太关心。


学习鸿蒙


  由于iOS进度相对慢的有点多,所以我这边也自然多出了很多时间,刚好今年华为搞事情了,鸿蒙后续版本将不会再适配Android的apk了,需要进行单独开发,我刚好可以利用这段空闲的时间学学HarmonyOS,虽然感觉可以再缓缓,不用那么快学习,毕竟刚出,问题肯定相对较多,还需要一段时间让HarmonyOS的api完善完善,bug再修改修改。因为22年中下旬离职前,花了2、3个月的时间学习过JetpactCompose,所以学习HarmonyOS的ArkUI来说,相对轻松点,基本和JC有很多一样的用法。目前ArkUI也能做个简单的APP了,但还没开始看自定义视图,后面再学学,视图方面就基本可以了,主要还是各种优化和ArkTS的Api熟悉。


总结


  没想到一晃眼就一年过去了呢,今年除了一开始找工作、收楼找装修外,也没啥大事发生了,埋头敲敲代码就年底了,希望明年会越来越好!!


作者:ShrimpF
来源:juejin.cn/post/7317325043541131274
收起阅读 »

使用双异步后,从 191s 优化到 2s

大家好,我是哪吒。 在开发中,我们经常会遇到这样的需求,将Excel的数据导入数据库中。 一、一般我会这样做: 通过POI读取需要导入的Excel; 以文件名为表名、列头为列名、并将数据拼接成sql; 通过JDBC或mybatis插入数据库; 操作起来,...
继续阅读 »

大家好,我是哪吒。


在开发中,我们经常会遇到这样的需求,将Excel的数据导入数据库中。


一、一般我会这样做:



  1. 通过POI读取需要导入的Excel;

  2. 以文件名为表名、列头为列名、并将数据拼接成sql;

  3. 通过JDBC或mybatis插入数据库;



操作起来,如果文件比较多,数据量都很大的时候,会非常慢。


访问之后,感觉没什么反应,实际上已经在读取 + 入库了,只是比较慢而已。


读取一个10万行的Excel,居然用了191s,我还以为它卡死了呢!


private void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();

StringBuilder insertBuilder = new StringBuilder();

insertBuilder.append("insert int0 ").append(filename).append(" ( UUID,");

XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}

insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");

StringBuilder stringBuilder = new StringBuilder();
for (int i = 1; i <= maxRow; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}

boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}

List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
int sum = JdbcUtil.executeDML(collect);
}

private static boolean isExisted(String id, String name) {
String sql = "select count(1) as num from " + static_TABLE + " where ID = '" + id + "' and NAME = '" + name + "'";
String num = JdbcUtil.executeSelect(sql, "num");
return Integer.valueOf(num) > 0;
}

private static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}

二、谁写的?拖出去,斩了!


优化1:先查询全部数据,缓存到map中,插入前再进行判断,速度快了很多。


优化2:如果单个Excel文件过大,可以采用 异步 + 多线程 读取若干行,分批入库。



优化3:如果文件数量过多,可以采一个Excel一个异步,形成完美的双异步读取插入。



使用双异步后,从 191s 优化到 2s,你敢信?


下面贴出异步读取Excel文件、并分批读取大Excel文件的关键代码。


1、readExcelCacheAsync控制类


@RequestMapping(value = "/readExcelCacheAsync", method = RequestMethod.POST)
@ResponseBody
public String readExcelCacheAsync() {
String path = "G:\\测试\\data\\";
try {
// 在读取Excel之前,缓存所有数据
USER_INFO_SET = getUserInfo();

File file = new File(path);
String[] xlsxArr = file.list();
for (int i = 0; i < xlsxArr.length; i++) {
File fileTemp = new File(path + "\\" + xlsxArr[i]);
String filename = fileTemp.getName().replace(".xlsx", "");
readExcelCacheAsyncService.readXls(path + filename + ".xlsx", filename);
}
} catch (Exception e) {
logger.error("|#ReadDBCsv|#异常: ", e);
return "error";
}
return "success";
}

2、分批读取超大Excel文件


@Async("async-executor")
public void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();
logger.info(filename + ".xlsx,一共" + maxRow + "行数据!");
StringBuilder insertBuilder = new StringBuilder();

insertBuilder.append("insert int0 ").append(filename).append(" ( UUID,");

XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}

insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");

int times = maxRow / STEP + 1;
//logger.info("将" + maxRow + "行数据分" + times + "次插入数据库!");
for (int time = 0; time < times; time++) {
int start = STEP * time + 1;
int end = STEP * time + STEP;

if (time == times - 1) {
end = maxRow;
}

if(end + 1 - start > 0){
//logger.info("第" + (time + 1) + "次插入数据库!" + "准备插入" + (end + 1 - start) + "条数据!");
//readExcelDataAsyncService.readXlsCacheAsync(sheet, row, start, end, insertBuilder);
readExcelDataAsyncService.readXlsCacheAsyncMybatis(sheet, row, start, end, insertBuilder);
}
}
}

3、异步批量入库


@Async("async-executor")
public void readXlsCacheAsync(XSSFSheet sheet, XSSFRow row, int start, int end, StringBuilder insertBuilder) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = start; i <= end; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}

// 先在读取Excel之前,缓存所有数据,再做判断
boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}

List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
if (collect != null && collect.size() > 0) {
int sum = JdbcUtil.executeDML(collect);
}
}

private boolean isExisted(String id, String name) {
return ReadExcelCacheAsyncController.USER_INFO_SET.contains(id + "," + name);
}

4、异步线程池工具类


@Async的作用就是异步处理任务。



  1. 在方法上添加@Async,表示此方法是异步方法;

  2. 在类上添加@Async,表示类中的所有方法都是异步方法;

  3. 使用此注解的类,必须是Spring管理的类;

  4. 需要在启动类或配置类中加入@EnableAsync注解,@Async才会生效;


在使用@Async时,如果不指定线程池的名称,也就是不自定义线程池,@Async是有默认线程池的,使用的是Spring默认的线程池SimpleAsyncTaskExecutor。


默认线程池的默认配置如下:



  1. 默认核心线程数:8;

  2. 最大线程数:Integet.MAX_VALUE;

  3. 队列使用LinkedBlockingQueue;

  4. 容量是:Integet.MAX_VALUE;

  5. 空闲线程保留时间:60s;

  6. 线程池拒绝策略:AbortPolicy;


从最大线程数可以看出,在并发情况下,会无限制的创建线程,我勒个吗啊。


也可以通过yml重新配置:


spring:
task:
execution:
pool:
max-size: 10
core-size: 5
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: my-executor

也可以自定义线程池,下面通过简单的代码来实现以下@Async自定义线程池。


@EnableAsync// 支持异步操作
@Configuration
public class AsyncTaskConfig {

/**
* com.google.guava中的线程池
* @return
*/

@Bean("my-executor")
public Executor firstExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-executor").build();
// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(curSystemThreads, 100,
200, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), threadFactory);
threadPool.allowsCoreThreadTimeOut();
return threadPool;
}

/**
* Spring线程池
* @return
*/

@Bean("async-executor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心线程数
taskExecutor.setCorePoolSize(24);
// 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(200);
// 缓存队列
taskExecutor.setQueueCapacity(50);
// 空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
// 异步方法内部线程名称
taskExecutor.setThreadNamePrefix("async-");

/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/

taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}


5、异步失效的原因



  1. 注解@Async的方法不是public方法;

  2. 注解@Async的返回值只能为void或Future;

  3. 注解@Async方法使用static修饰也会失效;

  4. 没加@EnableAsync注解;

  5. 调用方和@Async不能在一个类中;

  6. 在Async方法上标注@Transactional是没用的,但在Async方法调用的方法上标注@Transcational是有效的;


三、线程池中的核心线程数设置问题


有一个问题,一直没时间摸索,线程池中的核心线程数CorePoolSize、最大线程数MaxPoolSize,设置成多少,最合适,效率最高。


借着这个机会,测试一下。


1、我记得有这样一个说法,CPU的处理器数量


将核心线程数CorePoolSize设置成CPU的处理器数量,是不是效率最高的?


// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;

Runtime.getRuntime().availableProcessors()获取的是CPU核心线程数,也就是计算资源。



  • CPU密集型,线程池大小设置为N,也就是和cpu的线程数相同,可以尽可能地避免线程间上下文切换,但在实际开发中,一般会设置为N+1,为了防止意外情况出现线程阻塞,如果出现阻塞,多出来的线程会继续执行任务,保证CPU的利用效率。

  • IO密集型,线程池大小设置为2N,这个数是根据业务压测出来的,如果不涉及业务就使用推荐。


在实际中,需要对具体的线程池大小进行调整,可以通过压测及机器设备现状,进行调整大小。


如果线程池太大,则会造成CPU不断的切换,对整个系统性能也不会有太大的提升,反而会导致系统缓慢。


我的电脑的CPU的处理器数量是24。


那么一次读取多少行最合适呢?


测试的Excel中含有10万条数据,10万/24 = 4166,那么我设置成4200,是不是效率最佳呢?


测试的过程中发现,好像真的是这样的。


2、我记得大家都习惯性的将核心线程数CorePoolSize和最大线程数MaxPoolSize设置成一样的,都爱设置成200。


是随便写的,还是经验而为之?


测试发现,当你将核心线程数CorePoolSize和最大线程数MaxPoolSize都设置为200的时候,第一次它会同时开启150个线程,来进行工作。


这个是为什么?


3、经过数十次的测试



  1. 发现核心线程数好像差别不大

  2. 每次读取和入库的数量是关键,不能太多,因为每次入库会变慢;

  3. 也不能太少,如果太少,超过了150个线程,就会造成线程阻塞,也会变慢;


四、通过EasyExcel读取并插入数据库


EasyExcel的方式,我就不写双异步优化了,大家切记陷入低水平勤奋的怪圈。


1、ReadEasyExcelController


@RequestMapping(value = "/readEasyExcel", method = RequestMethod.POST)
@ResponseBody
public String readEasyExcel() {
try {
String path = "G:\\测试\\data\\";
String[] xlsxArr = new File(path).list();
for (int i = 0; i < xlsxArr.length; i++) {
String filePath = path + xlsxArr[i];
File fileTemp = new File(path + xlsxArr[i]);
String fileName = fileTemp.getName().replace(".xlsx", "");
List<UserInfo> list = new ArrayList<>();
EasyExcel.read(filePath, UserInfo.class, new ReadEasyExeclAsyncListener(readEasyExeclService, fileName, batchCount, list)).sheet().doRead();
}
}catch (Exception e){
logger.error("readEasyExcel 异常:",e);
return "error";
}
return "suceess";
}

2、ReadEasyExeclAsyncListener


public ReadEasyExeclService readEasyExeclService;
// 表名
public String TABLE_NAME;
// 批量插入阈值
private int BATCH_COUNT;
// 数据集合
private List<UserInfo> LIST;

public ReadEasyExeclAsyncListener(ReadEasyExeclService readEasyExeclService, String tableName, int batchCount, List<UserInfo> list) {
this.readEasyExeclService = readEasyExeclService;
this.TABLE_NAME = tableName;
this.BATCH_COUNT = batchCount;
this.LIST = list;
}

@Override
public void invoke(UserInfo data, AnalysisContext analysisContext) {
data.setUuid(uuid());
data.setTableName(TABLE_NAME);
LIST.add(data);
if(LIST.size() >= BATCH_COUNT){
// 批量入库
readEasyExeclService.saveDataBatch(LIST);
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if(LIST.size() > 0){
// 最后一批入库
readEasyExeclService.saveDataBatch(LIST);
}
}

public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}

3、ReadEasyExeclServiceImpl


@Service
public class ReadEasyExeclServiceImpl implements ReadEasyExeclService {

@Resource
private ReadEasyExeclMapper readEasyExeclMapper;

@Override
public void saveDataBatch(List<UserInfo> list) {
// 通过mybatis入库
readEasyExeclMapper.saveDataBatch(list);
// 通过JDBC入库
// insertByJdbc(list);
list.clear();
}

private void insertByJdbc(List<UserInfo> list){
List<String> sqlList = new ArrayList<>();
for (UserInfo u : list){
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("insert int0 ").append(u.getTableName()).append(" ( UUID,ID,NAME,AGE,ADDRESS,PHONE,OP_TIME ) values ( ");
sqlBuilder.append("'").append(ReadEasyExeclAsyncListener.uuid()).append("',")
.append("'").append(u.getId()).append("',")
.append("'").append(u.getName()).append("',")
.append("'").append(u.getAge()).append("',")
.append("'").append(u.getAddress()).append("',")
.append("'").append(u.getPhone()).append("',")
.append("sysdate )");
sqlList.add(sqlBuilder.toString());
}

JdbcUtil.executeDML(sqlList);
}
}

4、UserInfo


@Data
public class UserInfo {

private String tableName;

private String uuid;

@ExcelProperty(value = "ID")
private String id;

@ExcelProperty(value = "NAME")
private String name;

@ExcelProperty(value = "AGE")
private String age;

@ExcelProperty(value = "ADDRESS")
private String address;

@ExcelProperty(value = "PHONE")
private String phone;
}

作者:哪吒编程
来源:juejin.cn/post/7315730050577694720
收起阅读 »

看我如何用JDBC数据库连接池,轻松解决大量并发请求问题!

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。首先,...
继续阅读 »

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。

但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。

首先,让我们来了解一下什么是数据库连接池。

一、数据库连接池简介

JDBC连接池,全称为Java多线程数据库连接池,是一种用于管理数据库连接的技术。其主要作用是减少每次请求时创建和释放数据库连接的开销,以此提高系统性能。

在应用程序和数据库之间,JDBC连接池会建立一个连接池,当需要访问数据库时,无需每次都重新创建连接,而是直接从池中获取已有的连接。

Description

总结一下就是:

  • 数据库连接池是个容器,负责分配、管理数据库连接(Connection)

  • 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。

  • 释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。

那么,为什么我们需要JDBC数据库连接池呢?

这主要有以下几个原因:

1.提高性能: 频繁地创建和销毁数据库连接会消耗大量的系统资源,而通过使用连接池,可以大大减少这部分开销,提高系统的性能。

2.提高稳定性: 在高并发的情况下,如果直接使用JDBC创建数据库连接,可能会出现系统无法创建更多的数据库连接的情况,导致系统崩溃。而通过使用连接池,可以有效地控制并发请求的数量,保证系统的稳定性。

3.提高数据库的响应速度: 通过使用连接池,可以减少等待数据库连接的时间,从而提高系统的响应速度。

之前我们代码中使用连接是没有使用都创建一个Connection对象,使用完毕就会将其销毁。这样重复创建销毁的过程是特别耗费计算机的性能的及消耗时间的。

而数据库使用了数据库连接池后,就能达到Connection对象的复用,如下图:

Description

  • 连接池是在一开始就创建好了一些连接(Connection)对象存储起来。用户需要连接数据库时,不需要自己创建连接;

  • 而只需要从连接池中获取一个连接进行使用,使用完毕后再将连接对象归还给连接池。

这样就可以起到资源重用,也节省了频繁创建连接销毁连接所花费的时间,从而提升了系统响应的速度。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


二、数据库连接池实现

1、标准接口:

javax.sql.DataSource。

官方(SUN公司)为数据库连接池提供了一套标准接口,由第三方组织实现此接口。

  • 核心方法:Connection getConnection(),获取连接。

Description

2、常见的数据库连接池:

JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由第三方来实现。

市面上有很多开源的JDBC数据库连接池,如C3P0、DBCP、Druid等,它们都有各自的特点和优势。

C3P0数据库连接池: 速度相对较慢(只是慢一丢丢),但是稳定性很好,Hibernate,Spring底层用的就是C3P0。

DBCP数据库连接池: 速度比C3P0快,但是稳定性差。

Proxool数据库连接池: 有监控连接池状态的功能,但稳定性仍然比C3P0差一些。

BoneCP数据库连接池: 速度较快。

Druid数据库连接池(德鲁伊连接池): 由阿里提供,集DBCP,Proxool,C3P0连接池的优点于一身,是日常项目开发中使用频率最高的数据库连接池。

三、Durid(德鲁伊连接池)的使用

Druid使用步骤:

  • 导入jar包 druid-1.1.12.jar。

  • 定义配置文件。

  • 加载配置文件。

  • 获取数据库连接池对象。

  • 获取连接。

druid.properties配置文件:

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_test?useSSL=false&useServerPrepStmts=true
username=root
password=123456
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000

代码示例:


package com.green.druid;


import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.Properties;

public class DruidDemo {

public static void main(String[] args) throws Exception {
//1、导入jar包


//2、定义配置文件


//3、加载配置文件
Properties prop = new Properties();
prop.load(new FileInputStream("jdbc-demo/src/druid.properties"));
//System.out.println(System.getProperty("user.dir")); //当前文件目录 D:\code\JDBC


//4、获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);


//5、获取数据库连接 Connection
Connection conn = dataSource.getConnection();


System.out.println(conn);


}
}

以上就是JDBC数据连接池的简介与常见连接池的基本使用,希望对你有所帮助。在未来的开发过程中,不妨尝试使用JDBC数据库连接池,让你的应用性能更上一层楼!

收起阅读 »

慎用,Mybatis-Plus这个方法可能导致死锁

1 场景还原 1.1 版本信息 MySQL版本:5.6.36-82.1-log  Mybatis-Plus的starter版本:3.3.2 存储引擎:InnoDB 1.2 死锁现象 A同学在生产环境使用了Mybatis-Plus提供的 com.b...
继续阅读 »

1 场景还原


1.1 版本信息


MySQL版本:5.6.36-82.1-log 
Mybatis-Plusstarter版本:3.3.2
存储引擎:InnoDB

1.2 死锁现象



A同学在生产环境使用了Mybatis-Plus提供的 com.baomidou.mybatisplus.extension.service.IService#saveOrUpdate(T, com.baomidou.mybatisplus.core.conditions.Wrapper) 方法(以下简称B方法),并发场景下,数据库报了如下错误





2 为什么是间隙锁死锁?



如上图示,数据库报了死锁,那死锁场景千万种,为什么确定B方法是由于间隙锁导致的死锁?



2.1 什么是死锁?


两个事务互相等待对方持有的锁,导致互相阻塞,从而导致死锁。


2.2 什么是间隙锁?



  • 间隙锁是MySQL行锁的一种,与Record lock不同的是间隙锁锁定的是一个间隙。

  • 锁定规则如下:


MySQL会向左找第一个比当前索引值小的值,向右找第一个比当前索引值大 的值(没有则为正无穷),将此区间锁住,从而阻止其他事务在此区间插入数据。


2.3 MySQL为什么要引入间隙锁?


与Record lock组合成Next-key lock,在可重复读这种隔离级别下一起工作避免幻读。


2.4 间隙锁死锁分析


理论上一款开源的框架,经过了多年打磨,提供的方法不应该造成如此严重的错误,但理论仅仅是理论上,事实就是发生了死锁,于是我们开始了一轮深度排查。首先我们从这个方法的源码入手,源码如下:


    default boolean saveOrUpdate(T entity, Wrapper updateWrapper) {
        return this.update(entity, updateWrapper) || this.saveOrUpdate(entity);
    }

从源码上看此方法就没有按套路出牌,正常逻辑应该是首先执行查询,存在则修改,不存在则新增,但此方法上来就执行了修改。我们就猜想是不是MySQL在修改时增加了什么锁导致了死锁,于是我们找到了DBA获取了最新的死锁日志,即执行show engine innodb status,我们发现了两项关键信息如下:


*** (1) TRANSACTION:
...省略日志
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71C lock_mode X locks gap before rec insert intention waiting
  
*** (2) TRANSACTION:
...省略日志
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71D lock_mode X locks gap before rec insert intention waiting

简单翻译一下,就是事务一在获取插入意向锁时,需要等待间隙锁(事务二添加)释放,同时事务二在获取插入意向锁时,也在等待间隙锁释放(事务一添加), (本文不讨论MySQL在修改与插入时添加的锁,我们把修改时添加间隙锁,插入时获取插入意向锁为已知条件) 那我们回到B方法,并发场景下,是不是就很大几率会满足事务一和事务二相互等待对方持有的间隙锁,从而导致死锁。




现在我们理论有了,我们现在用真实数据来验证此场景。


2.5 验证间隙锁死锁



  • 准备如下表结构(以下简称验证一)


create table t_gap_lock(
id int auto_increment primary key comment '主键ID',
name varchar(64not null comment '名称',
age int not null comment '年龄'
comment '间隙锁测试表';


  • 准备如下表数据


mysql> select * from t_gap_lock;
+----+------+-----+
| id | name | age |
+----+------+-----+
|  1 | 张三 |  18 |
|  5 | 李四 |  19 |
|  6 | 王五 |  20 |
|  9 | 赵六 |  21 |
| 12 | 孙七 |  22 |
+----+------+-----+


  • 我们开启事务一,并执行如下语句,注意这个时候我们还没有提交事务


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0


  • 同时我们开启事务二,并执行如下语句,事务二我们同样不提交事务


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 7;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0


  • 接下来我们在事务一中执行如下语句


mysqlinsert int0 t_gap_lock(id, name, agevalue (7,'间隙锁7',27);  


  • 我们会发现事务一被阻塞了,然后我们执行以下语句看下当前正在锁的事务。


mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS \G;
*************************** 1. row ***************************
    lock_id: 749:0:360:3
lock_
trx_id: 749
  lock_
mode: X,GAP
  lock_type: RECORD
 lock_
table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_
space: 0
  lock_page: 360
   lock_
rec: 3
  lock_data: 5
*************************** 2. row ***************************
    lock_
id: 74A:0:360:3
lock_trx_id: 74A
  lock_mode: X,GAP
  lock_
type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_
index: `PRIMARY`
 lock_space: 0
  lock_
page: 360
   lock_rec: 3
  lock_
data: 5
2 rows in set (0.00 sec)

根据lock_type和lock_mode我们可以很清晰的看到锁类型是行锁,锁模式是间隙锁。



  • 与此同时我们在事务二中执行如下语句


insert int0 t_gap_lock(idname, agevalue (4,'间隙锁4',24);


  • 一执行以上语句,数据库就立马报了死锁,并且回滚了事务二(可以在死锁日志中看到*** WE ROLL BACK TRANSACTION (2))


ERROR 1213 (40001): Deadlock found when trying to get locktry restarting transaction 



到这里,细心的同学就会发现,诶,你这上面故意造了一个间隙,并且让两个事务分别在对方的间隙中插入数据,太刻意了,生产环境基本上不会有这种场景,是的,生产环境怎么会有这种场景呢,上面的数据只是为了让大家直观的看到间隙锁的死锁过程,接下来那我们再来一组数据,我们简称验证二。



  • 我们还是以验证一的表结构与数据,我们来执行这样一个操作。首先我们开始开启事务一并且执行如下操作,依然不提交事务


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0 


  • 同时我们开启事务二,执行与事务一一样的操作,我们会惊奇的发现,竟然也成功了。


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0 


  • 于是乎我们在事务一执行如下操作,我们又惊奇的发现事务一被阻塞了。


insert int0 t_gap_lock(idname, agevalue (4,'间隙锁4',24);  


  • 在事务一被阻塞的同时,我们在事务二执行同样的语句,我们发现数据库立马就报了死锁。


insert int0 t_gap_lock(idname, agevalue (4,'间隙锁4',24);    
ERROR 1213 (40001): Deadlock found when trying to get locktry restarting transaction

验证二完整的复现了线上死锁的过程,也就是事务一先执行了更新语句,事务二在同一时刻也执行了更新语句,然后事务一发现没有更新到就去执行主键查询语句,发现确实没有,所以执行了插入语句,但是插入要先获取插入意向锁,在获取插入意向锁的时候发现这个间隙已经被事务二加锁了,所以事务一开始等待事务二释放间隙锁,同理,事务二也执行上述操作,最终导致事务一与事务二互相等待对方释放间隙锁,最终导致死锁。


验证二还说明了一个问题,就是间隙锁加锁是非互斥的,也就是事务一对间隙A加锁后,事务二依然可以给间隙A加锁。


3 如何解决?


3.1 关闭间隙锁(不推荐)



  • 降低隔离级别,例如降为提交读。

  • 直接修改my.cnf,将开关,innodb_locks_unsafe_for_binlog改为1,默认为0即开启


PS:以上方法仅适用于当前业务场景确实不关心幻读的问题。


3.2 自定义saveOrUpdate方法(推荐)


建议自己编写一个saveOrUpdate方法,当然也可以直接采用Mybatis-Plus提供的saveOrUpdate方法,但是根据源码发现,会有很多额外的反射操作,并且还添加了事务,大家都知道,MySQL单表操作完全不需要开事务,会增加额外的开销。


  @Transactional(
        rollbackFor = {Exception.class}
    )
    public boolean saveOrUpdate(T entity) {
        if (null == entity) {
            return false;
        } else {
            Class cls = entity.getClass();
            TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
            Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!"new Object[0]);
            String keyProperty = tableInfo.getKeyProperty();
            Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!"new Object[0]);
            Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());
            return !StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal)) ? this.updateById(entity) : this.save(entity);
        }
    }

4 拓展


4.1 如果两个事务修改是存在的行会发生什么?


在验证二中两个事务修改的都是不存在的行,都能加间隙锁成功,那如果两个事务修改的是存在的行,MySQL还会加间隙锁吗?或者说把间隙锁从锁间隙降为锁一行?带着疑问,我们执行以下数据验证,我们还是使用验证一的表和数据。



  • 首先我们开启事务一执行以下语句


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0


  • 我们再开启事务二,执行同样的语句,发现事务二已经被阻塞


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql
> update t_gap_lock t set t.age = 25 where t.id = 1;


  • 这个时候我们执行以下语句看下当前正在锁的事务。


mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS \G;
*************************** 1. row ***************************
    lock_id: 75C:0:360:2
lock_
trx_id: 75C
  lock_
mode: X
  lock_type: RECORD
 lock_
table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_
space: 0
  lock_page: 360
   lock_
rec: 2
  lock_data: 1
*************************** 2. row ***************************
    lock_
id: 75B:0:360:2
lock_trx_id: 75B
  lock_mode: X
  lock_
type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_
index: `PRIMARY`
 lock_space: 0
  lock_
page: 360
   lock_rec: 2
  lock_
data: 1
2 rows in set (0.00 sec)

根据lock_type和lock_mode我们看到事务一和二加的锁变成了Record Lock,并没有再添加间隙锁,根据以上数据验证MySQL在修改存在的数据时会给行加上Record Lock,与间隙锁不同的是该锁是互斥的,即不同的事务不能同时对同一行记录添加Record Lock。


5 结语


虽然Mybatis-Plus提供的这个方法可能会造成死锁,但是依然不可否认它是一款非常优秀的增强框架,其提供的lambda写法在日常工作中极大的提高了我们的开发效率,所以凡事都用两面性,我们应该秉承辩证的态度,熟悉的方法尝试用,陌生的方法谨慎用。


以上就是我们在生产环境间隙锁死锁分析的全过程,如果大家觉得本文让你对间隙锁,以及间隙锁死锁有一点的了解,别忘记一键三连,多多支持转转技术,转转技术在未来将会给大家带来更多的生产实践与探索。


作者:转转技术团队
来源:juejin.cn/post/7311880893841719330
收起阅读 »

js跨标签页通信

web
一、为什么要跨标签页通信 在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。 二、实现跨标签页通信的几种方式 2.1 localStorage 打...
继续阅读 »

一、为什么要跨标签页通信


在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。


二、实现跨标签页通信的几种方式


2.1 localStorage


image.png


打开A页面,可以看到localStorage和sessionStorage中都存储了testA:


image.png
image.png

B页面中,可以获取到localStorage,但是无法获取到sessionStorage:


image.png

2.2 BroadcastChannel


BroadcastChannel允许同源下浏览器不同窗口订阅它,postMessage方法用于发送消息,message事件用于接收消息。


A页面:


      const bc = new BroadcastChannel('test')

bc.postMessage('不去上班行吗?')

B页面:


      const bc = new BroadcastChannel('test')

bc.onmessage = (e) => {
console.log(e)
}

动画.gif


2.3 postMessage(跨源通信)


image.png


2.3.1 iframe跨域数据传递


parent.html


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1>主页面</h1>
<iframe id="child" src="http://10.7.9.69:8080"></iframe>
<div>
<h2>主页面跨域接收消息区域</h2>
<div id="message"></div>
</div>
</body>
<script>
// 传递数据到子页面
window.onload = function () {
// 第二个参数表示哪个窗口可以接收到消息
document.getElementById('child').contentWindow.postMessage('不上班行不行', 'http://10.7.9.69:8080')
}
// 接受子页面传递过来的数据
window.addEventListener('message', function (event) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
})
</script>
</html>


App.vue


<template>
<div class="app">
<div id="message"></div>
</div>

</template>
<script>
export default {
created() {
// 接收父页面传过来的数据
window.addEventListener('message', function (event) {
// 处理addEventListener执行两次的情况,避免获取不到data
// 因此判断接收的域是否是父页面
console.log('event', event)
if (event.origin.includes('http://127.0.0.1:5501')) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
// 把数据传递给父页面 window.parent === top
window.parent.postMessage('不上班你养我啊', 'http://127.0.0.1:5501')
}
})
},
}
</script>


image.png


注:



  1. http://127.0.0.1:5501/ 是使用 Open with Live Server打开后的地址

  2. http://10.7.9.69:8080/ 是启动vue后的地址


2.3.2 postMessage在window.open()中的使用


作者:蓝色海岛
来源:juejin.cn/post/7315354087829536803
收起阅读 »

【前端考古】没有await,如何处理“回调地狱”

web
太长不看 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层 利用函数提升。先使用后声明。 处理每一个异常 编写可以复用的函数,并把他们封装成一个模块 什么是“回调地狱”? 异步Javascript代码,或者说使用callback的Javascrip...
继续阅读 »

太长不看



  • 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层

  • 利用函数提升。先使用后声明。

  • 处理每一个异常

  • 编写可以复用的函数,并把他们封装成一个模块


什么是“回调地狱”?


异步Javascript代码,或者说使用callback的Javascript代码,很难符合我们的直观理解。很多代码最终会写成这样:


fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

看到上面金字塔形状的代码和那些末尾参差不齐的 }) 了吗?这就是广为人知的回调地狱了。

人们在编写JavaScript代码时,误认为代码是按照我们看到的代码顺序从上到下执行的,这就是造成回调地狱的原因。在其他语言中,例如C,Ruby或者Python,第一行代码执行结束后,才会开始执行第二行代码,按照这种模式一直到执行到当前文件中最后一行代码。随着你学习深入,你会发现JavaScript跟他们是不一样的。


什么是回调(callback)?


某种使用JavaScript函数的惯例用法的名字叫做回调。JavaScript语言中没有一个叫“回调”的东西,它仅仅是一个惯例用法的名字。大多数函数会立刻返回执行结果,使用回调的函数通常会经过一段时间后才输出结果。名词“异步”,简称“async”,只是意味着“这将花费一点时间”或者说“在将来某个时间发生而不是现在”。通常回调只使用在I/O操作中,例如下载文件,读取文件,连接数据库等等。


当你调用一个正常的函数时,你可以向下面的代码那样使用它的返回值:


var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 50 gets printed out

然而使用回调的异步函数不会立刻返回任何结果。


var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!

在这种情况下,上面那张gif图片可能需要很长的时间才能下载完成,但你不想你的程序在等待下载完成的过程中中止(也叫阻塞)。


于是你把需要下载完成后运行的代码存放到一个函数中(等待下载完成后再运行它)。这就是回调!你把回调传递给downloadPhoto函数,当下载结束,回调会被调用。如果下载成功,传入photo给回调;下载失败,传入error给回调。


downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)

function handlePhoto (error, photo) {
if (error) console.error('Download error!', error)
else console.log('Download finished', photo)
}

console.log('Download started')

人们理解回调的最大障碍在于理解一个程序的执行顺序。在上面的例子中,发生了三件事情。



  1. 声明handlePhoto函数

  2. downloadPhoto函数被调用并且传入了handlePhoto最为它的回调

  3. 打印出Download started


请大家注意,起初handlePhoto函数仅仅是被创建并被作为回调传递给了downloadPhoto,它还没有被调用。它会等待downloadPhoto函数完成了它的任务才会执行。这可能需要很长一段时间(取决于网速的快慢)。


这个例子意在阐明两个重要的概念:



  1. handlePhoto回调只是一个存放将来进行的操作的方式

  2. 事情发生的顺序并不是直观上看到的从上到下,它会当某些事情完成后再跳回来执行。


怎样解决“回调地狱”问题?


糟糕的编码习惯造成了回调地狱。幸运的是,编写优雅的代码不是那么难!


你只需要遵循三大原则


1. 减少嵌套层数(Keep your code shallow)


下面是一堆乱糟糟的代码,使用browser-request做AJAX请求。


**


var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

这段代码包含两个匿名函数,我们来给他们命名。


var form = document.querySelector('form')
form.onsubmit = function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

如你所见,给匿名函数一个名字是多么简单,而且好处立竿见影:



  • 起一个一望便知其函数功能的名字让代码更易读

  • 当抛出异常时,你可以在stacktrace里看到实际出异常的函数名字,而不是"anonymous"

  • 允许你合理安排函数的位置,并通过函数名字调用它


现在我们可以把这些函数放在我们程序的顶层。


document.querySelector('form').onsubmit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

请大家注意,函数声明在程序的底部,但是我们在函数声明之前就可以调用它。这是函数提升的作用。


2.模块化(Modularize)


任何人都有有能力创建模块,这点非常重要。写出一些小模块,每个模块只做一件事情,然后把他们组合起来放入其他的模块做一个复杂的事情。只要你不想陷入回调地狱,你就不会。让我们把上面的例子修改一下,改为一个模块。


下面是一个名为formuploader.js的新文件,包含了我们之前使用过的两个函数。


module.exports.submit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

module.exports是node.js模块化的用法。现在已经有了 formuploader.js 文件,我们只需要引入它并使用它。请看下面的代码:


var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

我们的应用只有两行代码并且还有以下好处:



  1. 方便新开发人员理解你的代码 -- 他们不需要费尽力气读完formuploader函数的全部代码

  2. formuploader可以在其他地方复用


3.处理每一个异常(Handle every single error)


有三种不同类型的异常:语法异常,运行时异常和平台异常。语法异常通常由开发人员在第一次解释代码时捕获,运行时异常通常在代码运行过程中因为bug触发,平台异常通常由于没有文件的权限,硬盘错误,无网络链接等问题造成。这一部分主要来处理最后一种异常:平台异常。


前两个大原则意在提高代码可读性,但是第三个原则意在提高代码的稳定性。在你与回调打交道的时候,你通常要处理发送请求,等待返回或者放弃请求等任务。任何有经验的开发人员都会告诉你,你从来不知道哪里回出现问题。所以你有必要提前准备好,异常总是会发生。


把回调函数的第一个参数设置为error对象,是Node.js中处理异常最流行的方式。


var fs = require('fs')

fs.readFile('/Does/not/exist', handleFile)

function handleFile (error, file) {
if (error) return console.error('Uhoh, there was an error', error)
// otherwise, continue on and use `file` in your code
}

把第一个参数设为error对象是一个约定俗成的惯例,提醒你记得去处理异常。如果它是第二个参数,你更容易把它忽略掉。


作者:Max力出奇迹
来源:juejin.cn/post/7294166986195533843
收起阅读 »

解决hutool图形验证码bug

从网上下载了一个开源的项目,发现登录界面的验证码无论怎么刷新,显示的都是同一个,即使换一个浏览器也是相同的图形验证码,说一下解决bug的过程 😶修改前的源代码如下(部分代码) import cn.hutool.captcha.*; import lombo...
继续阅读 »

从网上下载了一个开源的项目,发现登录界面的验证码无论怎么刷新,显示的都是同一个,即使换一个浏览器也是相同的图形验证码,说一下解决bug的过程



😶修改前的源代码如下(部分代码)



import cn.hutool.captcha.*;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

private final AbstractCaptcha abstractCaptcha;

@Override
public CaptchaResult getCaptcha() {
//获取验证码文本,例如 1+4=
String captchaCode = abstractCaptcha.getCode();
String imageBase64Data = abstractCaptcha.getImageBase64Data();

String captchaKey = IdUtil.fastSimpleUUID();
// 验证码文本缓存至Redis,用于登录校验
redisTemplate.opsForValue().set(CacheConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
//......
}
}

😐分析上面的代码,首先作者使用了Lombok提供的@RequiredArgsConstructor注解,它的作用是为下面被 final 修饰的变量生成构造方法,即利用构造方法将AbstractCaptcha对象注入到Spring容器中,但是通过这种方式注入,默认情况下Bean是单例的,即多次请求会复用同一个Bean 。


😐其次,阅读hutool有关abstractCaptcha.getCode()部分的代码,如下,可以看到,在第一次生成code时,就将生成的code赋值给了成员变量 code,再结合前面的单例Bean,真相大白。


//验证码
protected String code;

@Override
public String getCode() {
if (null == this.code) {
createCode();
}
return this.code;
}

@Override
public void createCode() {
generateCode();

final ByteArrayOutputStream out = new ByteArrayOutputStream();
ImgUtil.writePng(createImage(this.code), out);
this.imageBytes = out.toByteArray();
}

//生成验证码字符串
protected void generateCode() {
this.code = generator.generate();
}


😎最终修改业务代码如下



import cn.hutool.captcha.*;
import cn.hutool.captcha.generator.MathGenerator;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

//private final AbstractCaptcha abstractCaptcha;

@Override
public CaptchaResult getCaptcha() {
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 45, 4, 4);
// 自定义验证码内容为四则运算方式
captcha.setGenerator(new MathGenerator(1));
String captchaCode = captcha.getCode();
String captchaBase64 = captcha.getImageBase64Data();

String captchaKey = IdUtil.fastSimpleUUID();
// 验证码文本缓存至Redis,用于登录校验
redisTemplate.opsForValue().set(CacheConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
//......
}
}

😴阅读hutool关于createShearCaptcha()方法的源码,每次调用都会new一个新对象


/**
* 创建扭曲干扰的验证码,默认5位验证码
*
* @param width 图片宽
* @param height 图片高
* @param codeCount 字符个数
* @param thickness 干扰线宽度
* @return {@link ShearCaptcha}
* @since 3.3.0
*/

public static ShearCaptcha createShearCaptcha(int width, int height, int codeCount, int thickness) {
return new ShearCaptcha(width, height, codeCount, thickness);
}

作者:tomla
来源:juejin.cn/post/7316592830638800947
收起阅读 »

从零开始写一个web服务到底有多难?

背景 ​ 服务想必大家都有很多开发经验,但是从零开始搭建一个项目的经验,就少的多了。更不要说不使用任何框架的情况下从零开始搭建一个服务。那么这次就看看从零开始搭建一个好用好写web服务到底有多难? HelloWorld 官网给出的helloworld例子。ht...
继续阅读 »

背景



服务想必大家都有很多开发经验,但是从零开始搭建一个项目的经验,就少的多了。更不要说不使用任何框架的情况下从零开始搭建一个服务。那么这次就看看从零开始搭建一个好用好写web服务到底有多难?


HelloWorld


官网给出的helloworld例子。http标准库提供了两个方法,HandleFunc注册处理方法和ListenAndServe启动侦听接口。


请在此添加图片描述


请在此添加图片描述


假如业务更多


下面我们模拟一下接口增多的情况。可以看出有大量重复的部分。这样自然而然就产生了抽象服务的需求。


package main

import (
"fmt"
"net/http"
)

func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Greet!: %s\n", r.URL.Path)
}

func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello!: %s\n", r.URL.Path)
}

func notfound(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
}

func main() {
http.HandleFunc("/", notfound)
http.HandleFunc("/hello", hello)
http.HandleFunc("/greet", greet)
http.ListenAndServe(":80", nil)
}

我们想要一个服务,它代表的是对某个端口监听的实例,它可以根据访问的路径,调用对应的方法。在需要的时候,我可以生成多个服务实例,监听多个端口。那么我们的Server需要实现下面两个方法。


type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)

Start(address string) error
}

简单实现一下。


package server

import "net/http"

type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)
Start(address string) error
}

type httpServer struct {
Name string
}

func (s *httpServer) Route(pattern string, handlerFunc http.HandlerFunc) {
http.HandleFunc(pattern, handlerFunc)
}

func (s *httpServer) Start(address string) error {
return http.ListenAndServe(address, nil)
}

func NewHttpServer(name string) Server {
return &httpServer{
Name: name,
}
}

修改业务代码


func main() {
server := server.NewHttpServer("demo")
server.Route("/", notfound)
server.Route("/hello", hello)
server.Route("/greet", greet)
server.Start(":80")
}

格式化输入输出


在我们实际使用过程中,输入输出一般都是以json的格式。自然也需要通用的处理过程。


type Context struct {
W http.ResponseWriter
R *http.Request
}

func (c *Context) ReadJson(data interface{}) error {
body, err := io.ReadAll(c.R.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, data)
if err != nil {
return err
}
return nil
}

func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}

模拟了一个常见的业务代码。定义了入参和出参。


type helloReq struct {
Name string
Age string
}

type helloResp struct {
Data string
}

func hello(w http.ResponseWriter, r *http.Request) {
req := &helloReq{}
ctx := &server.Context{
W: w,
R: r,
}

err := ctx.ReadJson(req)

if err != nil {
fmt.Fprintf(w, "err:%v", err)
return
}

resp := &helloResp{
Data: req.Name + "_" + req.Age,
}

err = ctx.WriteJson(http.StatusOK, resp)
if err != nil {
fmt.Fprintf(w, "err:%v", err)
return
}

}

用postman试一下,是不是和我们平常开发的接口有一点像了。


请在此添加图片描述


由于200,404,500的返回结果实在是太普遍了,我们当然也可以进一步封装输出方法。但是我觉得没必要。


在我们设计的过程中,是否要提供辅助性的方法,还是只聚焦核心功能,是非常值得考虑的问题。


func (c *Context) SuccessJson(resp interface{}) error {
return c.WriteJson(http.StatusOK, resp)
}

func (c *Context) NotFoundJson(resp interface{}) error {
return c.WriteJson(http.StatusNotFound, resp)
}

func (c *Context) ServerErrorJson(resp interface{}) error {
return c.WriteJson(http.StatusInternalServerError, resp)
}


让框架来创建Context


观察下业务代码,还有个非常让人不舒服的地方。Context是框架内部使用的数据结构,居然要业务来创建!真的是太不合理了。


那么下面我们把Context移入框架内部创建,同时业务侧提供的handlefunction入参应该直接是由框架创建的Context。


首先修改我们的路由注册接口的定义。在实现中,我们注册了一个匿名函数,在其中构建了ctx的实例,并调用入参中业务的handlerFunc。


type Server interface {
Route(pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}

func (s *httpServer) Route(pattern string, handlerFunc func(ctx *Context)) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
ctx := NewContext(w, r)
handlerFunc(ctx)
})
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
W: w,
R: r,
}
}

这样修改之后我们的业务代码也显得更干净了。


func hello(ctx *server.Context) {
req := &helloReq{}
err := ctx.ReadJson(req)

if err != nil {
ctx.ServerErrorJson(err)
return
}

resp := &helloResp{
Data: req.Name + "_" + req.Age,
}

err = ctx.WriteJson(http.StatusOK, resp)
if err != nil {
ctx.ServerErrorJson(err)
return
}
}

RestFul API 实现


当然我们现在发现,不管用什么方法调用我们的接口,都可以正常返回。但是我们平常都习惯写restful风格的接口。


那么在注册路由时,自然需要加上一个method的参数。注册时候也加上一个GET的声明。


type Server interface {
Route(method string, pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}

server.Route(http.MethodGet, "/hello", hello)

那么我们自然可以这样写,当请求方法不等于我们注册方法时,返回error。


func (s *httpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
if r.Method != method {
w.Write([]byte("error"))
return
}
ctx := NewContext(w, r)
handlerFunc(ctx)
})
}

那么我们现在就有了一个非常简单的可以实现restful api的服务了。


但是距离一个好用好写的web服务还有很大的进步空间。


作者:4cos90
来源:juejin.cn/post/7314902560405684251
收起阅读 »

做了几年前端,别跟我说没配置过webpack

web
引言 webpack中文官网:webpack.docschina.org/concepts/ webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webp...
继续阅读 »

引言


webpack中文官网:webpack.docschina.org/concepts/


webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webpack的认知都停留在入口出口以及简单的loader和plugin配置上,对webpack的核心原理都一知半解。本文期望通过更深层的解读,让读者能更彻底地理解这个打包工具的来龙去脉。


为什么要用webpack


在webpack等打包工具出世之前,我们普通的H5项目是怎么处理错综复杂的脚本呢?
第一种方式:引用不同的脚本去使用不同的功能,但脚本太多的时候会导致网络瓶颈
第二种方式:使用一个大型js文件去引入所有代码,但这样会严重影响可读性,可维护性,作用域。


举个栗子:
由于浏览器不能直接解析less文件,我们可通过引入转换的插件(file watcher)把less实时转换为css并引入,但项目里面会多出一个map跟css文件,造成项目文件的臃肿。


官方文档的说法:


node.js诞生可以让Javasrcipt在浏览器环境之外使用,而webpack运行在node.js中。CommonJS的require机制允许在文件中引用某个模块,如此一来就可以解决作用域的问题。


    const HtmlWebpackPlugin = require('html-webpack-plugin')

webpack 关心性能和加载时间;它始终在改进或添加新功能,例如:异步地加载 chunk 和预取,以便为你的项目和用户提供最佳体验


核心概念


webpack有7个核心概念:



  1. 入口(entry)

  2. 输出(output)

  3. loader

  4. 插件(plugin)

  5. 模式(mode)

  6. 浏览器兼容性(brower compatibility)

  7. 环境(environment)


新建一个build文件夹,里面新建一个webpack.config.js


入口entry


这是打包的入口文件,所有的脚本将从这个入口文件开始


单入口


    const path = require('path')
module.exports = {
entry: path.resolve(__dirname, '../src/main.js')
}

多入口


使用对象语法配置,更好扩展,例如一个项目有前台网页跟后台管理两个项目可用多入口管理。


    entry: {
app: path.resolve(__dirname, '../src/main.js'),
admin: path.resolve(__dirname, '../src/admin.js'),
},

输出output


打包后输出的文件,[name]跟[hash:8]表示文件名跟入口的保持一致但后面加上了hash的后缀让每次生成的文件名是唯一的。


单入口


module.exports = {
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, '../dist'),
},
}

多入口


module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {
filename: '\[name].js',
path: \_\_dirname + '/dist',
},
};
// 写入到硬盘:./dist/app.js, ./dist/admin.js

loader转化器


用于模块的源码的转换,也是同学们在配置webpack的时候频繁接触的一个配置项。


举个例子,加载typescript跟css文件需要用到ts-loader跟css-loader、style-loader,
如果没有对应的loader,打包会直接error掉。


image.png


我们可以这么配置:先 npm i css-loader style-loader


module: {
rules: [
{
test: /\.css\$/,
use: ['style-loader','css-loader']
},
{
test: /\.ts\$/,
use: 'ts-loader'
}

必须留意的是,loader的执行是从右到左,就是css-loader执行完,再交给style-loader执行,


plugin插件


这是webpack配置的核心,有一些loader无法实现的功能,就通过plugin去扩展,建立一个规范的插件系统,能让你每次搭建项目的时候省去很多成本。


举个例子,我们会使用HtmlWebpackPlugin这个插件去生成一个html,其中会引入入口文件main.js。
假设不用这个插件,会发生什么?


当然是不会生成这个html,因此HtmlWebpackPlugin插件也是webpack的必备配置之一


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

模式mode


mode一共有production,development,node三种,如果没有设置,会默认为production


不同的mode对于默认优化的选项有所不同,环境变量也不同,具体需要了解每个插件的具体使用


选项描述
development会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development
production会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPlugin 和 TerserPlugin
none没优化选项

module.exports = {
mode: 'production'
}

source-map 的解读


Sourcemap 本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。 Sourcemap 解决了在打包过程中,代码经过压缩,去空格以及 babel 编译转化后,由于代码之间差异性过大,造成无法debug的问题


当mode为development时,devtool默认为‘eval’,当mode为production时,devtool默认为false。


sourceMap的分类



  • source-map:外部。可以查看错误代码准确信息和源代码的错误位置。

  • inline-source-map:内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置

  • hidden-source-map:外部用于生产环境。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置。

  • eval-source-map:内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息 和 源代码的错误位置。

  • nosources-source-map:外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。

  • cheap-source-map:外部。可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列。

  • cheap-module-source-map:外部用于生产环境。可以错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。

  • eval-cheap-module-source-map: 内联,用于开发环境,构建跟热更新比较快


内联和外部的区别: 外部生成了文件(.map),内联没有。内联构建速度更快。


笔者用的两种配置分为是


// webpack.dev.js
devtool: 'eval-cheap-module-source-map',

// webpack.prod.js
devtool: 'cheap-module-source-map'

浏览器兼容性 brower compatibility


Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill


环境 enviroment


本文使用的是webpack5 ,要求Node.js V在10.13.0+


Loader的汇总


笔者汇总了一部分常用的Loader以及其配置事项



  1. 浏览器兼容性:babel-loader

  2. css相关: css/style/less/postcss-loader

  3. vue: vue-loader



在配置loader前,先了解一下基本的配置



  • test: 匹配的文件,多用正则匹配

  • use: 使用loader,多用数组

  • exclude: 调整Loader解析的范围,包括某个路径下的文件,不如node_modules

  • include: 调整Loader解析的范围,包括某个路径下的文件


解决浏览器兼容性:babel


转义语法的babel-loader


譬如把const转为浏览器认识的var,虽然现在大部分主流浏览器都认识ES5之后的语法。


npm i babel-loader @babel/preset-env @babel/core

在rules配置:


{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
}

转义ES API的babel/polyfill


如果只有babel-loader,浏览器并不能识别出新的API(promise,proxy,includes),如图:


image.png


因此还需要配置一个babel/polyfill,在入口里面:


// npm i @babel/polyfill

entry: ["@babel/polyfill",path.resolve(__dirname, '../src/main.js')],

解析vue的vue-loader


vue-loader: 解析vue
vue-template-compiler: 编译vue模板


npm i vue-loader vue-template-compiler vue-style-loader
npm i vue

在rules跟plugins配置:


const { VueLoaderPlugin } = require('vue-loader') // vue3的引入跟vue2路径不同

rules:{
{
test: /\.vue$/,
use: ['vue-loader']
}
},
plugins:[
...
new VueLoaderPlugin()
...
]

配置完成后,vue文件就可以正常解析了


image.png


解析CSS文件


需要引入的Loader不止一个



  • 引入的基本Loader: style-loader,css-loader,如有less还需要less-loader

  • postcss-loader 添加不同浏览器的css前缀: 解决部分css语法在不同浏览器的写法不同的弊端


modules.exports = {
modules: {
rules: [{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env' // 解决css不同浏览器兼容性
],
],
},
}
}, 'less-loader'
]
},
}
}

拆分css


mini-css-extract-plugin: 把css拆分出来用外链的形式引入css文件,然后会在dist生成css文件,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件,
ps: 使用该插件不能重复使用style-loader


··· 
plugins: [
...
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}),
...
],
module:{
rules: [{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
}]
}

打包图片,字体,媒体等文件


file-loader: 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件url),并将文件移动到输出的目录中


url-loader 一般与file-loader搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中



{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(jpg|png|gif)$/i,
use: [{
loader: 'url-loader',
options: {
limit: 10240, // KB
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/icon')],
exclude: /node_modules/
},

使用多线程提升构建速度


js是单线程的工程,在构建工程的过程中,要消耗大量的时间在Loader的转换过程中,为了提升构建速度,这里使用了thread-loader将任务拆分为多个线程去处理。 其原理是把任务分配到各个worker线程中,之前多数人会使用happyPack,但webpack官方使用了thread-loader取代happypack。


...
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', ],
cacheDirectory: true,
}
},
{
loader: 'thread-loader', // 多线程解析模块
options: {
workers: 3, // 开启几个 worker 进程来处理打包,默认是 os.cpus().length - 1
}
}
],
exclude: /node_modules/
}
...

必须使用的插件Plugins


配置plugins必须注意的是,由于我们的模式(mode)区分为development跟production,因此plugins也需要按照实际需要,在config(公用),dev,prod三个配置文件分开加入。


首先先配置公用部分的plugins


公用plugins


清除打包残留文件


每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹


const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')



用外链的形式引入css


当一个html文件里面的css太多,全部把css添加到html中会显得很臃肿,那我们可以用mini-css-extract-plugin 把css拆分成外链引入,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件。


需要留意的是不能跟style-loader同时使用,下面用了hash


const MiniCssExtractPlugin = require("mini-css-extract-plugin");
....
plugins: [
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}, {
filename: devMode ? "[name].css" : "[name].[hash].less",
chunkFilename: devMode ? "[id].css" : "[id].[hash].less",
}),
]
....

module:{
rules:[{
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
]
},
}]
}
....

生产打包后的html


wepback必备的插件之一,上述举例也有提到。
主要是生产打包后的html, 同时由于main.js文件会随机生成新的hash名字,html在引入main.js文件时频繁改名字会很浪费时间,此插件会自动同步改文件名


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

开发环境Dev


热更新: webpack-dev-server


当我们修改文件的内容时,要重新build一次才能看到变化,这样对开发的效率不友好。


需要留意的是,webpack-dev-server只是在开发环境搭建一个服务帮助开发人员提高开发效率,实现了实时更新的功能,在生产环境并不会用到这一个插件.


同时注意在plugins中加入webpack自带的HotModuleReplacementPlugin。


webpack-dev-server这个插件功能十分强大,官方文档有详细的记录
(webpack.docschina.org/configurati…)


npm i webpack-dev-server --save-dev

module.exports = {
devServer: {
// 基本目录
static: {
directory: path.join(__dirname, 'dist'),
},
// 自动压缩代码
compress: true,
port: 9000,
// 自动打开浏览器
open: true,
// 热加载,默认是true
hot: true,
},
plugins: [
new Webpack.HotModuleReplacementPlugin()
]
}

生产环境Prod


由于生产环境对性能的要求跟开发不同,需要引入的插件比较丰富,也更需要对项目构建有更高的熟悉程度


压缩Js文件


webpack mode设置production的时候会自动压缩js代码。原则上不需要引入terser-webpack-plugin进行重复工作。但是optimize-css-assets-webpack-plugin压缩css的同时会破坏原有的js压缩,所以这里我们引入terser-webpack-plugin进行压缩


option很多,使用了dropconsole去除打印的内容


const TerserPlugin = require("terser-webpack-plugin");

...

optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


压缩CSS


前面有使用mini-css-extract-plugin的插件去拆分css,但这个插件并不能压缩CSS体积,
使用css-minimizer-webpack-plugin 可以压缩css的体积,但不同的是它是被加入到optimization的minimizer中,跟上述的js压缩插件共同作用


const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
...
optimization: {
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


抽离第三方模块


使用DllReferencePlugin把不需要经常变更的静态文件抽离出来,譬如element-ui组件,这样每次打包的时候就不会再去重新打包选中的静态文件了。


如此一来,当我们修改代码后,webpack只需要打包项目的代码而不需要重复去编译没有发生改变的第三方库。这样当我们没有升级第三方库时,webpack就不会再对这些库进行打包,从而提升项目构建的速度。


首先我们在同级目录下新建文件webpack.dll.config.js,在entry的vendor里面配置了vue跟element-ui。


// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
// 每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')

module.exports = {
mode: 'production',
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-plus']
},
output: {
path: path.resolve(__dirname, '../public/vendor'), // 打包后文件输出的位置,要在静态资源里面避免被打包转义
filename: '[name].dll.js',
library: 'vendor_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: 'vendor_library',
context: __dirname
})
]
};



同时在packake.json里面配置dll的命令


"scripts":{
"dll": "webpack --config build/webpack.dll.config.js",
}

最后在webpack.prod.js 加入配置项


···
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
···

随后执行命令npm run dll
在public/vendor会出现一个vendor.dll.js文件,我们需要在html文件引入这个文件.


<body>
<!-- dll插件的配置路径,注意是打包后的 -->
<script src="./vendor/vendor.dll.js"></script>
<!-- <img src="../assets/image/logo192.png" alt=""> -->
<!-- <img src="../assets/image/loginbg.png" alt=""> -->

<div id="app"></div>
</body>

配置完毕,这样我们在不需要更新第三方包的时候可以不用执行npm run dll,然后直接执行npm run build/dev的时候就会发现构建速度有所提高。


分析打包后的文件


使用webpack-bundle-analyzer,启动项目后会打开一个展示各个包的大小。从图中可以看出来,es6.promise.js这个包



  • stat size: webpack 从入口文件打包递归到的所有模块体积

  • parsed size: 解析与代码压缩后输出到dist目录的体积

  • gzipped size: 开启Gzip之后的体积


image.png


总结


webpack身为前端必备的一项技能,各位在学会基础的配置之后,千万别忘了因地制宜,看看哪些插件更适合自己的项目哦


作者:广州交租公
来源:juejin.cn/post/7277490138518159379
收起阅读 »

wordcloud,一个超酷的python库

微信公众号:愤怒的it男,超多Python技术干货文章。 一、简单介绍一下 词云图是文本挖掘中用来表征词频的数据可视化图像,通过它可以很直观地展现文本数据中地高频词,让读者能够从大量文本数据中快速抓住重点。如下图: wordcloud则是一个非常优秀的词云...
继续阅读 »

微信公众号:愤怒的it男,超多Python技术干货文章。



一、简单介绍一下


词云图是文本挖掘中用来表征词频的数据可视化图像,通过它可以很直观地展现文本数据中地高频词,让读者能够从大量文本数据中快速抓住重点。如下图:


图1.png


wordcloud则是一个非常优秀的词云展示python库,它支持自定义词云图的大小、颜色、字体等,甚至可以通过蒙版图片设置词云图的形状。因此,我们可以借助wordcloud轻松生成精美的词云图。


二、安装只需一行命令


pip install wordcloud

三、从一个简单例子开始


from wordcloud import WordCloud

text = "微信公众号:愤怒的it男"

wc = WordCloud(font_path='FZYTK.TTF', repeat=True)
wc.generate(text)
wc.to_file('wordcloud.png')

这里通过WordCloud类设置字体为方正姚体,背景颜色为白色,文本可以重复显示。生成WordCloud对象后,使用generate()方法将“微信公众号:愤怒的it男”生成词云图。最后,使用to_file()方法生成图片文件。


图2.png


四、细说wordcloud


WordCloud作为wordcloud库最核心的类,其主要参数及说明如下:


图3.PNG


这里以wordcloud库官方文档的constitution.txt文件作为数据,覆盖WordCloud类的各种参数设置用法,绘制出一张精美的词云图。


图4.PNG


首先,读入constitution.txt数据,并将数据清洗成空格分隔的长字符串。


import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

print(text[:500])

图5.PNG


然后,在默认参数设置下,使用WordCloud对象的generate()和to_file()方法生成一张简单的词云图。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud()
wc.generate(text)

wc.to_file('wordcloud.png')

图6.png


以上词云图是在默认参数下生成的,简单粗糙不好看。接下来我们将对WordCloud的各种参数调整设置,不断地对以上词云图进行升级改造。


1、设置图片属性


设置图片宽为600,高为300,放大1.5倍,色彩空间为RGBA,背景颜色为。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图7.png


2、设置文字布局


设置水平比例为1(即全部为水平文字),最多只显示100个词,停用词使用自带的词典(中文需要传入自定义的),相关一致性为0.3,文字布局为非随机,不允许重复词。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
prefer_horizontal=1,
max_words=400,
stopwords=,
relative_scaling=0.3,
random_state=4,
repeat=False,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图8.png


3、设置字体属性


设置字体为‘JOKERMAN.TTF’,最小字号为2,最大字号为150。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
prefer_horizontal=1,
max_words=400,
stopwords=,
relative_scaling=0.3,
random_state=4,
repeat=False,
font_path='JOKERMAN.TTF',
min_font_size=2,
max_font_size=150,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图9.png


4、设置蒙版


图10.PNG


设置微信公众号【愤怒的it男】头像的黑白图片为蒙版图片。


from PIL import Image
from wordcloud import WordCloud
import numpy as np
import re

mask_picture = np.array(Image.open('angry_it_man_mask.png'))

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
prefer_horizontal=1,
max_words=400,
stopwords=,
relative_scaling=0.3,
random_state=4,
repeat=False,
font_path='JOKERMAN.TTF',
min_font_size=2,
max_font_size=150,
mask=mask_picture,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图11.png



微信公众号:愤怒的it男,超多Python技术干货文章。



作者:愤怒的it男
来源:juejin.cn/post/7317214007572807731
收起阅读 »

亲测实战大屏项目的适配方法

web
背景 想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。 如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。 我这里倒是有一个亲测...
继续阅读 »

背景


想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。

如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。

我这里倒是有一个亲测且实践的一个特别简单的方法,不用换算,设计给多少px前端就写几px,照着UI设计无头脑的搬就可,而且在大屏、电脑pc端、手机移动端,一套代码,就可运行,保证界面不乱,不超出屏幕范围。

这个方法是在我第一次写大屏项目时,用上的,当时时间也是很紧迫,但是项目上线后,对甲方来说很完美无缺了,一次过。

最后发现它在手机端也能看。

如果好奇想知道究竟是什么方法可以这么的丝滑去兼容大屏乃至任何尺寸任何分辨率的设备的小伙伴们,不妨接着往下看。


常见兼容各尺寸的方法


css媒体查询


@media screen

eg:


@media screen and (max-width: 1700px){
//屏幕尺寸小于1700的样式
}

这种方式,太麻烦,如果要求高,就要分的越细,几乎每个尺寸都要兼顾,写进去,实在实在是太太麻烦了。


viewport


<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">

网页头部加入这段代码,网页宽度自动适应屏幕的宽度。

这种方式大屏中不太适合,这种更加适合移动端。

亲测,大屏不太适合。


rem


要换算,而且每次要根据屏幕尺寸的变化而去重新给一个新的根节点字体大小值。

还是麻烦,嗯,也是不能完美适合大屏。


vw和vh


把整个屏幕宽高分成一百分,一个vw就是1%的宽,一个vh就是10的高。

也是麻烦,而且也不能完美适配大屏。


百分百%


同vw,vh一样也不能完美适配大屏。


重点来了


最终我用了缩放,完美解决了大屏适配问题。

它不仅可以在任何尺寸任何分辨率下,去相对完美的展示,在PC甚至移动端也是可以看的。


如何?


jsx页面里:


 useEffect(() => {
//全屏模式
fullScreens();
//首次加载应用,设置一次
setScale();
//调试比例
window.addEventListener('resize', Listen(setScale, 100));
}, []);
//调试比例
// 监听手机窗口变化,重新设置
function Listen(fn, t) {
const delay = t || 100;
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
const context = this;
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, delay);
};
}

// 获取放大缩小比例
function getScale() {
const w = window.innerWidth / 1920;
const h = window.innerHeight / 1080;
return w < h ? w : h;
}

// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(-50%, -50%)');
}

css:


 width:1920px;
height:1080px;
transform-origin: 0 0;
position: absolute;
left: 50%;
top: 50%;

想要哪个页面去做这种兼容,就可在哪个页面去这样设置;

利用查找id的方式找到对应的页面元素(这里只要能查找到元素就行,怎么写随自己),然后给对应的元素添加样式 transform ,去设置比例


getScale方法里面:

先求一下宽高的比例,因为设计稿一般使用的分辨率是1920和1080,所以这里使用这个取比例;

最后返回比例的时候,取比例比较小的,这样可以保证都显示出来;

只不过相对小的尺寸会出现空白,大的尺寸那个占满全屏;

js这里的translate(-50%, -50%) 和css里面的position: absolute; left:50%;top: 50%;transform-origin:0 0可保证画面在最中间


注意点:transform-origin:0 0这个很重要哦,用来设置动画的基点,没有这个,怎么都不会居中正常显示的哦。


否则就会出现以下这几个情况:


image.png


image.png


而:


// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(0, 0)');
}

则会:


image.png
Listen方式,是专门设备窗口大小变化时,就会自动调用setScale方法,我设置的是每100秒监听一次,相当于做了一个自适应的处理;

setScale在页面加载时调用了一次,然后也加了一个resize方法;

宽度、长度也设置一样,我写的是1920 * 1080


出现问题,两边有空白?


屏幕没有铺满,左右有空白,这是因为设备上面有地址栏,而导致的;

因为地址栏占了一定的高度,但是设计稿把这块高度没有算进去,因此,实际的整体高度会比设计稿的大,而宽度是一样的,为了能够全部展示,就以大的为主,因此,宽度相对比就会出现空白啦;

image.png


解决


设置成全屏就好啦;


image.png

完美;

因为我的笔记本电脑分辨率高,所以,电脑默认缩放设置的是125%,而不是100%,默认设置为100%整体字体什么都都会变小,观看都不高,所以电脑默认设置为125%啦, 无论电脑默认是多少百分比,只要设置成全屏,都是全部铺满的。


image.png


设置全屏的代码


进入全屏、退出全屏、当前是否全屏模式的代码:

commonMethod.js:


//进入全屏
static fullScreen = (element) => {
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML W3C 提议
else if (element.requestFullScreen) {
element.requestFullScreen();
}
//IE11
else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
// setTimeout(()=>{
// console.log(isFullScreen());
// },100)
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
}
};
//退出全屏
static fullExit = () => {
//IE ActiveXObject
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML5 W3C 提议
else if (element.requestFullScreen) {
document.exitFullscreen();
}
//IE 11
else if (element.msRequestFullscreen) {
document.msExitFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
document.webkitCancelFullScreen();
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
document.mozCancelFullScreen();
}
};
//当前是否全屏模式
static isFullScreen = () =>{
return document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen ||
document.msFullscreenElement;
}

这是react项目页面里引用的封装好的方法:


 //全屏
const fullScreens = () => {
if (!CommonMethod.isFullScreen()) {
Modal.info({
content: '开启全屏模式,体验更佳',
okText: '确定',
maskClosable: true,
mask: false,
// centered:true,
width: '200px',
height: '100px',
className: 'fullScreenModal',
onOk() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
onCancel() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
});
}
};

然后大屏页面第一次加载时,调用一次全屏模式方法即可,fullScreens


useEffect(() => {
//全屏模式
fullScreens();
}


注意点


如果在使用transform,缩放的地方,就不能使用其他定位,你会发现使用其他定位(绝对定位、相对定位、固定定位等)会失效;

使用antd里的模态框,就会失效哦。


完结


设置缩放的方法可以试试,并且在不同设备尺寸和分辨率下,大屏、pc、手机端都可以试一波。

回头我把自己上面那个项目在不同设备下运行的截图整理一下,补发出来。

对移动端有点要求的其实还会需要重新写一套代码的,我上面这种方式移动端只是样式没有乱也没有超出屏幕而已,毕竟这个只是专门给大屏做的代码,那么大的尺寸怎么的在手机端看也不会很符合的


作者:浅唱_那一缕阳光
来源:juejin.cn/post/7232229178278903865
收起阅读 »

Python中级知识梳理

1. 文件操作 Python中的文件操作通常使用内置的open()函数来打开文件。以下是一个简单的示例: with open("file.txt", "r") as f: content = f.read() print(content) 在...
继续阅读 »

image.png


1. 文件操作


Python中的文件操作通常使用内置的open()函数来打开文件。以下是一个简单的示例:


with open("file.txt", "r") as f:
content = f.read()
print(content)

在这个示例中,我们打开了名为"file.txt"的文件,并将其读入变量content中,最后将其打印出来。


open()函数的第一个参数是文件名,第二个参数是打开文件的模式。以下是一些常用的模式:



  • "r":只读模式

  • "w":写入模式(会覆盖已有文件)

  • "a":追加模式(不会覆盖已有文件)


2. 正则表达式


正则表达式是一种强大的工具,可以帮助我们从文本中提取信息或进行文本替换。Python中可以使用内置的re模块来进行正则表达式操作。以下是一个示例:


import re

text = "The quick brown fox jumps over the lazy dog."
pattern = r"fox"
matches = re.findall(pattern, text)
print(matches)

在这个示例中,我们定义了一个正则表达式模式r"fox",然后使用re.findall()函数来查找匹配该模式的所有字符串。最后,我们将匹配的结果打印出来。


3. 异常处理


在编写程序时,经常需要处理可能出现的错误或异常情况。Python中可以使用tryexcept语句来实现异常处理。以下是一个简单的示例:


try:
x = 1 / 0
except ZeroDivisionError:
print("Error: division by zero")

在这个示例中,我们尝试计算1除以0,这将引发一个ZeroDivisionError异常。我们使用tryexcept语句来捕获该异常并打印出一条错误消息。


4. 面向对象编程(Object-Oriented Programming)


面向对象编程是一种重要的编程范式,Python是一种面向对象的语言。以下是一个简单的示例:


class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def say_hello(self):
print("Hello, my name is " + self.name)

person = Person("Alice", 25)
person.say_hello()

在这个示例中,我们定义了一个名为Person的类,并定义了一个构造函数__init__()来初始化对象的属性。我们还定义了一个名为say_hello()的方法,用于打印出问候语。最后,我们创建一个Person对象,并调用say_hello()方法。
Python是一种面向对象编程语言,它支持面向对象编程的三个基本概念:封装、继承和多态。面向对象编程的核心思想是将数据和操作数据的方法封装在一起,形成一个对象。


示例代码


class Animal:
def __init__(self, name, age):
self.name = name
self.age = age

def say_hello(self):
print(f'{self.name} is saying hello')

class Cat(Animal):
def __init__(self, name, age, color):
super().__init__(name, age)
self.color = color

def catch_mouse(self):
print(f'{self.name} is catching mouse')

cat = Cat('Tom', 2, 'White')
cat.say_hello()
cat.catch_mouse()

输出结果


Tom is saying hello
Tom is catching mouse

5. Python高级特性


5.1 生成器(Generator)


生成器是一种特殊的函数,可以在函数执行期间多次返回值,并且可以保留当前执行状态,等待下一次调用。它们是高效的迭代器,可以用于处理大量数据或无限流数据,同时也能节省内存。


示例代码


pythonCopy code
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b

f = fib()
for i in range(10):
print(next(f))

输出结果


Copy code
0
1
1
2
3
5
8
13
21
34

5.2 装饰器(Decorator)


装饰器是一种函数,用于修改其他函数的行为。它们提供了一种简单的方式来修改函数,而无需修改函数的原始定义。装饰器的常见用途包括添加日志、计时、缓存等功能。


示例代码


pythonCopy code
import time

def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} executed in {end - start} seconds')
return result
return wrapper

@timer
def calculate_sum(n):
return sum(range(n+1))

print(calculate_sum(100000000))

输出结果


Copy code
calculate_sum executed in 4.150076866149902 seconds
5000000050000000

作者:SandySY
来源:juejin.cn/post/7224335234010234935
收起阅读 »

分页合理化是什么?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 只要是干过后台系统的同学应该都做过分页查询吧,前端发送带有页码(pageNum)和每页显示数量(pa...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


只要是干过后台系统的同学应该都做过分页查询吧,前端发送带有页码(pageNum)和每页显示数量(pageSize)的请求,后端根据这些参数来提取并返回相应的数据集。在SpringBoot框架中,经常会使用Mybatis+PageHelper的方式实现这个功能。


但大家可能对分页合理化这个词有点儿陌生,不过应该都遇到过因为它产生的问题。这些问题不会触发明显的错误,所以大家一般都忽视了这个问题。那么啥是分页合理化,我来举几个例子:



它的定义:分页合理化通常是指后端在处理分页请求时会自动校正不合理的分页参数,以确保用户始终收到有效的数据响应。



1. 请求页码超出范围:



假设数据库中有100条记录,每页展示10条,那么就应该只有10页数据。如果用户请求第11页,不合理化处理可能会返回一个空的数据集,告诉用户没有更多数据。开启分页合理化后,系统可能会返回第10页的数据(即最后一页的数据),而不是一个空集。



2. 请求页码小于1:



用户请求的页码如果是0或负数,这在分页上下文中是没有意义的。开启分页合理化后,系统会将这种请求的页码调整为1,返回第一页的数据。



3. 请求的数据大小小于1:



如果用户请求的数据大小为0或负数,这也是无效的,因为它意味着用户不希望获取任何数据。开启分页合理化后,系统可能会设置一个默认的页面大小,比如每页显示10条数据。



4. 请求的数据大小不合理:



如果用户请求的数据大小非常大,比如一次请求1000条数据,这可能会给服务器带来不必要的压力。开启分页合理化后,系统可能会限制页面大小的上限,比如最多只允许每页显示100条数据。



二、为啥要设置分页合理化?


其实上面那些问题对于后端来讲很合理,页码和页大小设置不正确查询不出来值难道不合理吗?唯一的问题就是如果一次性查询太多条数据服务器压力确实大,但如果是产品要求的那也没办法呀!
真正让我不得不解决这个问题的原因是前端的一个BUG,这个BUG是啥样的呢?我来给大家描述一下。


1. BUG复现


我们先看看前端的分页组件



前端的这个分页组件大家应该很常见,它需要两个参数:总行数、每页行数。比如说现在总条数是6条,每页展示5条,那么会有2页,没啥问题对吧。



那么,现在我问一个问题:我们切换到第二页,把第二页仅剩的一条数据给删除掉,会出现什么情况?


理想情况:页码自动切换到第1页,并查询第一页的数据;
真实情况:页码切换到了第1页,但是查询不到数据,这明显就是一个BUG!


2. BUG分析


1. 用户切换到第二页,前端发起了请求,如:http://localhost:8080/user/pageQuery?pageNum=2&pageSize=5 ,此时第2页有一条数据;


2. 用户删除第2页的唯一数据后,前端发起查询请求,但还是第2页的查询,因为总数据的变化前端只能通过下一次的查询才能知道,但此时数据查询为空;


3. 虽然第二次查询的数据集为空,但是总条数已经变化了,只剩下5条,前端分页组件根据计算得出只剩下一页,所以自动切换到第1页;



可以看出这个BUG是分页查询的一个临界状态产生的,必现、中低频,属于必须修复的那一类。不过这个BUG想甩给前端,估计不行,因为总条数的变化只有后端知道,必须得后端修了。



三、设置分页合理化


咋一听这个BUG有点儿复杂,但如果你使用的是PageHelper框架,那么修复它非常简单,只需要两行配置
application.ymlapplication.properties中添加


pagehelper.helper-dialect=mysql
pagehelper.reasonable=true

只要加了这两行配置,这个BUG就能解决。因为配置是全局的,如果你只想对单个查询场景生效,那就在设置分页参数的时候,加一个参数,如下:


PageHelper.startPage(pageNumber, pageSize, true);

四、分页合理化配置的原理说明


这个BUG如果要自己解决的话,是不是感觉有点头痛了,但是人家PageHelper早就想到这个问题了,就像游戏开挂一样,一个配置就解决了这个麻烦的问题。
用的时候确实很爽,但是我却有点担心,这个配置现在解决了这个BUG,会不会导致新的BUG呢?如果真的出现了新BUG,我应该怎么做呢?所以我决定研究一下它的基础原理。


在com.github.pagehelper.Page类下,找到了这段核心源码,这段应该就是分页合理化的实现逻辑


// 省略其他代码
public Page<E> setReasonable(Boolean reasonable) {
if (reasonable == null) {
return this;
}
this.reasonable = reasonable;
//分页合理化,针对不合理的页码自动处理
if (this.reasonable && this.pageNum <= 0) {
this.pageNum = 1;
calculateStartAndEndRow();
}
return this;
}
// 省略其他代码

// 省略其他代码
/**
* 计算起止行号
*/

private void calculateStartAndEndRow() {
this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
}
// 省略其他代码

还有一些代码我没贴,比如PageInterceptor#intercept方法,这里我整理了一下它的执行流程图,如下:




看了图解,这套配置还挺清晰的,懂了怎么回事儿,用起来也就放心了。记得刚开始写代码时,啥都希望有人给弄好了,最好是拿来即用。但时间一长,自己修过一堆BUG,才发现只有自己弄明白的代码才靠谱,什么都想亲手来。等真正搞懂了一些底层的东西,才意识到要想造出好东西,得先学会站在巨人的肩膀上。学习嘛,没个头儿!



作者:summo
来源:juejin.cn/post/7316357622847995923
收起阅读 »

这下对阿里java这几条规范有更深理解了

背景 阿里java开发规范是阿里巴巴总结多年来的最佳编程实践,其中每一条规范都经过仔细打磨或踩坑而来,目的是为社区提供一份最佳编程规范,提升代码质量,减少bug。 这基本也是java业界都认可的开发规范,我们团队也是以此规范为基础,在结合实际情况,补充完善。最...
继续阅读 »

背景


阿里java开发规范是阿里巴巴总结多年来的最佳编程实践,其中每一条规范都经过仔细打磨或踩坑而来,目的是为社区提供一份最佳编程规范,提升代码质量,减少bug。

这基本也是java业界都认可的开发规范,我们团队也是以此规范为基础,在结合实际情况,补充完善。最近在团队遇到的几个问题,加深了我对这份开发规范中几个点的理解,下面就一一道来。


日志规约



这条规范说明了,在异常发送记录日志时,要记录案发现场信息和异常堆栈信息,不处理要往上throws,切勿吃掉异常。

堆栈信息比较好理解,就是把整个方法调用链打印出来,方便定位具体是哪个方法出错。而案发现场信息我认为至少要能说明:“谁发生了什么错误”。

例如,哪个uid下单报错了,哪个订单支付失败了,原因是什么。否则满屏打印:“user error”,看到你都无从下手。


在我们这次出现的问题就是有一个feign,调用外部接口报错了,降级打印了不规范日志,导致排查问题花了很多时间。伪代码如下:


	@Slf4j
@Component
class MyClientFallbackFactory implements FallbackFactory<MyClient> {

@Override
public MyClient create(Throwable cause) {
return new MyClient() {
@Override
public Result<DataInfoVo> findDataInfo(Long id) {
log.error("findDataInfo error");
return Result.error(SYS_ERROR);
}
};
}
}

发版后错误日志开始告警,打开kibana看到了满屏了:“findDataInfo error”,然后开始一顿盲查。

因为这个接口本次并没有修改,所以猜测是目标服务出问题,上服务器curl接口,发现调用是正常的。

接着猜测是不是熔断器有问题,熔断后没有恢复,但重启服务后,还是继续报错。开始各种排查,arthas跟踪,最后实在没办法了,还是老老实实把异常打印出来,走发版流程。


log.error("{} findDataInfo error", id, cause);

有了异常堆栈信息就很清晰了,原来是返回参数反序列失败了,接口提供方新增一个不兼容的参数导致反序列失败。(这点在下一个规范还会提到)

可见日志打印不清晰给排查问题带来多大的麻烦,记住:日志一定要打印关键信息,异常要打印堆栈。


二方库依赖



上面提到的返回参数反序列化失败就是枚举造成的,原因是这个接口返回新增一个枚举值,这个枚举值原本返回给前端使用的,没想到还有其它服务也调用了它,最终在反序列化时就报错了,找不到“xxx”枚举值。

比如如下接口,你提交一个不认得的黑色BLACK,就会报反序列错误:


	enum Color {
GREEN, RED
}

@Data
class Test {
private Color color;
}

@PostMapping(value = "/post/info")
public void info(@NotNull Test test) {

}

curl --location 'localhost/post/info' \
--header 'Content-Type: application/json' \
--data '{
"testEnum": "BLACK"
}'


关于这一点我们看下作者孤尽对它的阐述:


这就是我们出问题的场景,提供方新增了一个枚举值,而使用方没有升级,导致错误。可能有的同学说那通知使用方升级不就可以了?是的,但这出现了依赖问题,如果使用方有成百上千个,你会非常头痛。


那又为什么说不要使用枚举作为返回值,而可以作为输入参数呢?

我的理解是:作为枚举的提供者,不得随意新增/修改内容,或者说修改前要同步到所有枚举使用者,让大家知道,否则使用者就可能因为不认识这个枚举而报错,这是不可接受的。

但反过来,枚举提供者是可以将它作为输入参数的,如果调用者传了一个不存在的值就会报错,这是合理的,因为提供者并没有说支持这个值,调用者正常就不应该传递这个值,所以这种报错是合理的。


ORM映射



以下是规范里的说明:

1)增加查询分析器解析成本。

2)增减字段容易与 resultMap 配置不一致。

3)无用字段增加网络消耗,尤其是 text 类型的字段。


这都很好理解,就不过多说明。

在我们开发中,有的同学为了方便,还是使用了select *,一直以来也风平浪静,运行得好好的,直到有一天对该表加了个字段,代码没更新,报错了~,你没看错,代码没动,加个字段程序就报错了。

报错信息如下:



数组越界!问题可以在本地稳定复现,先把程序跑起来,执行 select * 的sql,再add column给表新增一个字段,再次执行相同的sql,报错。



具体原因是我们程序使用了sharding-jdbc做分表(5.1.2版本),它会在服务启动时,加载字段信息缓存,在查询后做字段匹配,出错就在匹配时。

具体代码位置在:com.mysql.cj.protocol.a.MergingColumnDefinitionFactory#createFromFields



这个缓存是跟数据库链接相关的,只有链接失效时,才会重新加载。主要有两个参数和它相关:

spring.shardingsphere.datasource.master.idle-timeout 默认10min

spring.shardingsphere.datasource.master.max-lifetime 默认30min


默认缓存时间都比较长,你只能赶紧重启服务解决,而如果服务数量非常多,又是一个生产事故。

我在sharding sphere github搜了一圈,没有好的处理方案,相关链接如:

github.com/apache/shar…

github.com/apache/shar…


大体意思是如果真想这么做,数据库ddl需要通过sharding proxy,它会负责刷新客户端的缓存,但我们使用的是sharding jdbc模式,那只能老老实实遵循规范,不要select * 了。如果select具体字段,那新增的字段也不会被select出来,和缓存的就能对应上。

那么以后面试除了上面规范说到的,把这一点亲身经历也摆出来,应该可以加分吧。


总结


每条开发规范都有其背后的含义,都是经验总结和踩坑教训,对于团队的开发规范我们都要仔细阅读,严格遵守。可以看到上面每个小问题都可能导致不小的生产事故,保持敬畏之心,大概就是这个意思了吧。


更多分享,欢迎关注我的github:github.com/jmilktea/jt…


作者:jtea
来源:juejin.cn/post/7308277343242944564
收起阅读 »

99年师弟,揭露华为工作的残酷真相

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。 师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研...
继续阅读 »

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。


师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研到本校,但拿到华为offer后,放弃了继续深造,选择直接就业。


聊到当今应届生求职现状时,感慨万千。


我的母校是一所普通211,部分专业比较有特色,在行业属于领先地位,但学校整体名气一般。


记得我们大四毕业时,就业行情还比较好,每个班都有好几个同学进入了华为。读研后,实验室的师兄师姐基本都拿到了华为offer,等我们开始找工作时,大部分同学也都拿了华为offer,华为基本属于大家的保底选择。


师弟去年毕业时,他是专业唯一拿到华为offer的学生。这几年的就业形势,本科生基本找不到什么好工作,除了继续读书深造,没有更好的选择。


短短的几年,社会瞬息万变,就像坐上了一辆过山车,身处其中的人,也只能仅仅抓住车身的杠子,在急速下坡时,不让自己摔下来。


图片


师弟进入华为,原本是一件很开心的事,短短一年,经历职场拷打,再也没有大学时代那份锐气。


华为属于员工人数最多的科技公司之一,每年吸收了大批校招生,但员工离职率也非常高。


师弟反馈,跟他同一批进去的人,很多干了几个月就离开了,他是那一批少数还留下来的人。


刚入职时,每个人都要参加新员工入职培训,那时候大家都很骄傲和自豪,也非常认同企业的价值观和文化。


但当真正的分到项目组开始工作时,发现大家实际工作情况,和价值观上说的,并不不太一样。在基层落地时,价值观更多是挂在墙上的标语。


领导也基本不太关心员工的成长和未来发展,更多是把每一个员工当作生产工具,哪一件工具用的顺手、更能出活儿,就会用谁;还没证明过自己的工具,或者经验匮乏的工具,很有可能会被无情抛弃。


华为的假期还可以,但大家有什么事,都不太敢请假。即便是生病,如果不是非住院不可,也不太会请假;公司支持请假,但大家还是轻伤不下火线,请假会影响出勤系数,各种与之相关的奖金,也会大打折扣。


……


师弟还分享了工作以来其他的一些心得体会,一句话总结:职场很残酷,相比起来,大学轻松太多了。


图片


在我看来,华为还是中国一流的科技公司。大部分人,还是要挤破脑袋才能进得去。


华为有很好的制度,比如员工转岗,门槛还不太高。华为支持不同工种互相转岗,比如,干技术的,有一天不想写代码了,可以转人力,或者转销售。开发太累,也可以转做测试。公司会提供基本的业务培训,还有三个月的考核期,只要达到一定的用人标准,就能转岗成功。


华为还有很好的奖励制度,只要能吃苦,且绩效不太差,能坚持下来,很多人最后都获得了丰厚的回报。比我高一两届的师兄师姐,还有跟我同一届的很多朋友,现在都还在华为,过得还不错。


华为还有比较完善的福利体系。在各地的办公区,远离市区,地价也很便宜,平时在公司会有夜宵、员工餐厅、下午茶和健身房各种福利,也有些地方,会有单身公寓。进入华为后,除了工作,也没什么社交,公司能满足日常的衣食住行。平时挣的钱,基本都能存下来,成家后,在华为附近买房,也基本衣食无忧了。


华为是一家跨国公司,如果是干技术支持或销售,还能享受去各国打卡的福利。在年轻的时候,能去更远的地方走一走,也是一件很宝贵的人生经历。


图片


华为是中国众多私企大厂的一个缩影,也是最早践行狼性文化的一家公司。


如今的字节、阿里等互联网企业,也在走华为的老路。


这些大厂,给了年轻人一个令人向往的机会,但并不适合每个人。如果我们能早些看清现实,自己只是大厂的一颗螺丝钉,需要做的是,尽快掌握螺丝钉需要具备的技能,与此同时,如果借助大厂的势能,多开眼界,多认识一些各个领域的前辈和牛人,把企业能提供的资源用到极致,就更了不起了。


至于在公司里寻找朋友、寻找归属感或者安全感,那并不是一个好选择。同事间的八卦,公司股价的涨跌,CEO的致辞,也并不是我们需要关心的事。


最重要的是,无论在大厂还是小厂,在国企还是私企,都要拥有良好的心态。最怕的是,在国企羡慕华为的工资;在大厂,又羡慕国企的朝九晚五。


人生在世,各有各的不易。每一种选择,都没有对错, 选择了其中一个,在另外一部分,就得有所取舍。


在任何时候,学会“自洽”,与生活和解,与自己和解很重要。


作者:工程师酷里
来源:juejin.cn/post/7316349124600168484
收起阅读 »

一个简单截图API,月收入2千美金

大家好,我是程序员凌览。又看到一个独立开发变现的故事,来分享下。 以下是变现分享内容: 大家好,我是Dmytro Krasun, screenshotone的创始人,这是一个简单的API,可以将任何网站的URL转换为网页截图。 人们经常惊讶于你可以通过自动截...
继续阅读 »

大家好,我是程序员凌览。又看到一个独立开发变现的故事,来分享下。


以下是变现分享内容:


大家好,我是Dmytro Krasun, screenshotone的创始人,这是一个简单的API,可以将任何网站的URL转换为网页截图。



人们经常惊讶于你可以通过自动截图网站来建立一个业务。如果你想在你的应用程序中显示任何网站的截图,那么就需要我的产品。


我才刚刚开始,但已经成功地将我的“小”产品发展到月收入2200美元。



你是如何想到这个主意的?


在开始我现在的产品之前,我是一个服务器端开发人员,工作了大约10年,薪水还不错。那时我写了15年的代码。

我有一些业余项目,我想尝试成为一名企业家。但在我工作的时候很难发布任何东西。我总是完全专注于我的日常工作,这是优先考虑的事情。这可能就是为什么我成长得很快,赚了不少钱。


一个机会几乎是“偶然”出现的。我家有了一个新宝宝,我休陪产假,我想这可能是一个好时机,我可以把时间花在抚养孩子、建立和启动一个项目上。这是一个艰难的决定。


最终,我决定毫无计划地跳进未知的世界。可能,因为我只是疯了,这“感觉”像一个有趣的冒险。


我是一名服务器端开发人员,在构建api方面有着丰富的经验。我坐下来写下我遇到的所有问题。我选择了一个随机截图API。我决定在运行中验证它,看看是否有需求。


然后我开始建造它。我谷歌了一下,发现有很多竞争对手,而且都是不错的。一开始我很失望。但后来我意识到,这意味着市场已经得到了验证,我只需要在细分市场打造出最好的产品。


请向我们介绍构建第一个版本的过程


在2022年1月5日买了一个域名,建立了一个简单的网站,开始写与我未来产品相关的内容。看看我的第一个丑陋版本的登陆页:



我写下了我在构建产品时遇到的任何问题,以及如何通过使用我的产品来更快更便宜地解决这些问题。


例如,我的截图API允许你在渲染截图时隐藏广告和cookie横幅,所以我写了如何自己免费做到这一点,并推荐我的产品作为替代方案。事实证明,这是一种推动转化率的有效方法。


seo优先的方法帮助了我,5个月后,当我发布产品时,我已经有了流量,我可以利用它来盈利。


我开始构建一个仪表盘应用程序,让潜在客户可以试用产品、查看使用历史、升级计划和配置通知。它是由Go渲染的纯HTML、CSS和JavaScript。我是一名服务器端开发人员,不知道如何使用现代JavaScript框架。


我将产品的第一个版本托管在Digital Ocean droplet上,然后当我开始增长时,我开始使用Render,然后需要更多的弹性服务器,并转移到谷歌云平台上。一周前,我启动了一个新的Kubernetes集群,以减少我在Google cloud Platform上的成本使用,它已经运行良好。


描述创业的过程


当我的产品的第一个版本准备好时,那是在2022年5月底,我已经从谷歌获得了一些相关流量,因为在发布之前创建了内容。但这还不足以促成销售。在Reddit, Indie Hackers,论坛,目录和Twitter上写文章。

最后,我总结了我在Indie Hackers的第一个营销月所做的一切。这是当月最热门的帖子,并迅速传播开来。


我几乎要放弃了,我累了。最终,在2022年7月4日,我迎来了第一位付费客户。我永远不会忘记这一点。我在Twitter上的朋友Jannis正在为创作者创建一个工具目录,并希望自动截图工具。


如果我今天重新开始,我不会从SEO开始。我会快速构建一个带有付费链接的原型,并尝试着将其展示给潜在的感兴趣的人。SEO是一个长期的游戏,它需要一个长期投入的工作。


是什么吸引并留住了客户?


根据我目前对市场营销的理解,没有什么灵丹妙药。你需要找到2-3个可以持续获得客户的渠道。除了实验,没有其他方法可以做到这一点,看看什么有效。


对我来说,搜索引擎优化、谷歌广告和推特都很管用。但我测试了Reddit、Indie Hackers、Twitter Ads、LinkedIn和其他平台。它们都是有效的,但问题是你可以反复从哪一个中获取客户?


我在Indie hacker上分享了我简单的SEO策略——我基本上是快速创建内容,获得流量,分析并更新内容。


对于X (Twitter),在早期,我积极推广我的产品,到处提到它。但感觉很尴尬,所以我就不再那样做了。开始专注于帮助人们,解决他们的问题,回答他们的问题。人们开始知道我在做什么,如何找到我。现在看到别人会主动去推荐我得产品。



我的主要流量来源是Twitter (X)和Google。我在Twitter上有1万名粉丝,这有助于推广我的产品。在搜索引擎优化上付出了很大的努力,现在我得到了回报。我的产品在细分市场中最具竞争力的关键词排名前5位,比如“screenshot API”之类的。


我认为没有捷径可走。无论如何,尝试每一种方法,找到最适合你的方法。


对其他创业者有什么建议吗?



先把你的野心放一边,试着一个SaaS软件或一个简单的应用程序来赚第1美元。一旦赚到钱,试着赚10美元,然后是100美元。不断重复,直到你确定创业真的是你想要的,并且你喜欢所发生的一切,不管结果有多难。


一旦你意识到这是你喜欢做的事情,不要放弃。如果你赚了1美元,10美元,然后100美元,你不放弃,那么一切皆有可能,只有天空才是你的极限。


别听任何人的建议,包括我的。只有你自己决定,如果你最终决定回到一份正常的工作,这是你的生活,你只能活一次,请保持自己的快乐。


"先把你的野心放一边,从赚第1美元开始"


作者:程序员凌览
来源:juejin.cn/post/7315586629308121140
收起阅读 »

Vue 2 最后之舞“天鹅挽歌”

web
大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本&nb...
继续阅读 »

大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。

圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本 Vue@2.7.16 正式发布,版本代号“Swan Song”(天鹅挽歌)。

01-swan.png

地球人都知道,去年 Vue 2 官宣了最后一个次版本 Vue@2.7.x,如今 Vue 2 官宣最后一个补丁版本 Vue@2.7.16,也算是为 Vue 2 的最后之舞画上惊叹号!此去经年,再无 Vue 2。

虽然但是,前端踏足之地,Vue 亦生生不息,此乃“Vue 之意志”。故本期《前端翻译计划》一起来重温 Vue@2.7 的官方博客,为 Vue 生态的未来规划未雨绸缪。

00-wall.png

今天我们十分鸡冻地官宣,Vue 2.7(版本代号“火影忍者”)正式发布!

尽管 Vue 3 现在已经是默认版本,但我们特别理解,由于依赖兼容性、浏览器支持的要求或带宽不足无法升级,仍然有一大坨用户被迫滞留在 Vue 2。在 Vue 2.7 中,我们向后移植了 Vue 3 中的某些特色功能,Vue 2 爱好者也可以有福同享。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Vue 2.7 "Naruto" Released

向后移植的功能

  • 组合式 API
  • SFC 
收起阅读 »

2023年终,找寻和回顾我在世界存活过的痕迹

有幸拜读了诸多掘友的年终总结,那些压力、纠结、卷、焦虑,我仅透过文字就感受到了,所以我又写回了上一年的主题了~ 我没再去写什么总结,反省啊,我只想轻松再带着些许俏皮的回顾一下2023年我在世界上存活过的痕迹,欢快些吧,生活总是要活下去的。 有时候,我们健康的活...
继续阅读 »

有幸拜读了诸多掘友的年终总结,那些压力、纠结、卷、焦虑,我仅透过文字就感受到了,所以我又写回了上一年的主题了~


我没再去写什么总结,反省啊,我只想轻松再带着些许俏皮的回顾一下2023年我在世界上存活过的痕迹,欢快些吧,生活总是要活下去的。


有时候,我们健康的活着,我们已经很棒啦


这篇文章我更多的抱着分享生活的想法去写的,不谈及那些重的话题,更像是活在当下的人吧


所以如果你是想要获取一些总结上的经验或者一些思考,可能我这篇年终文并不那么适合你。



博客较长,文字较多,希望朋友你能慢慢的读,犹如生活要慢慢的过


今年有幸在湘江边、珠江边和黄浦江边三地留下足迹,三处不同的场景,三种不同的生活状态,不一样人生阶段的结束和开始。



起伏跌宕的开始😀😟


和朋友一起在长沙度过了2022年最后一天,不知疲倦的在那个五一广场,一群人看着手机,数着秒表,大声呼喊着度过了2022年的最后一刻。


可时至今日,去年一起跨年的朋友,现在已经相隔千里了,而今年多半是在房间里面一个人度过这2023年的最后一刻啦


想到了去年写的那句“去年陪在你身边的人,如今还在你身旁吗?一年过完了,你的生活有变化吗?今年你过得开心吗?
人山人海,矮一丢丢,走进去就出不来啦。


人从众2023的新年时刻
image.pngimage.pngimage.png

回忆起那时的场景,调皮的人们在附近的高楼放起了鞭炮,气球在人们手上释放,一同奔向星空


(可惜这一幕我已经没有原视频啦,现在突然回忆起那个时刻,才发觉时间真的好快好快,一晃而过啊)


说起来,那还是我第一次在长沙看到那么那么多的人,也从没想过一个地方的退场,需要走半小时,打车更别说啦,都直接交通管制啦。


看起来这一切都像是美好的开始啊


但在那一晚的九点,我接到了来自家里的电话,让我抽空回家一趟,看一看最疼爱我的爷爷。


可殊不知,那是我和他的最后一次交谈了。


只愿世间永无病痛”。


长大后,那深夜里的电话🥺


长大后,才知道深夜里的电话,无一例外都是大事。


那是在某晚凌晨4点时接到我父亲打来的电话。


其实看清是我父亲打来时,我就清楚知道,小时候最疼爱我的爷爷可能去向了另外一个世界了。


当时的我真的算是非常平静,开始思考工作相关的事情,准备和领导请假,订高铁票,我以为我真的会非常平静的


stk


图一:等地铁


但当离家越来越近时,我才明白有些情绪是没有办法隐藏住的,有些崩溃就在某一瞬间


image.png

图二:回家的路


我看着我的奶奶流泪,我觉得她好无助啊,那一瞬间,我崩溃了。奶奶陪伴我爷爷一生,却剩她独自去过这余下的时光,(我小时候是和爷爷奶奶一起长大的,一直到我上小学之后,但那之后我每年的寒暑假也都会回到乡下去陪爷爷奶奶,只是不知道从什么时候开始,变得不再那么想要回去了)


奶奶很小就嫁过来了,可以说我爷爷就是她拥有的一切啦,儿女子孙大多在外拼搏,她自己也不愿意去麻烦儿女子孙了,所以说,这余下的日子大都是她一人度过了


有很多时刻,我都不愿去深想,那将是怎样的一个生活。在写这一段的时候,我非常想给她打一个电话,但是我的奶奶她已经非常听不清啦,只有偶尔父亲回去时,通过我父亲的传递才能更好的交流。


所幸在去年爷爷奶奶还算健康时,我回到家里陪他们。那一次我非常认真去和他们促膝交谈,安安静静的听他们说他们当年的故事给我听,而我也有幸做了一份录音,只是日后不知道有没有去倾听的勇气


今年回去我有一些想法了, 比如去给奶奶买一个助听器,再去装一个可以对话的摄像头,这些我终于都可以去付出实践啦,我现在有些期待过年啦


那时的我其实也写下了许多文字,关于病痛、选择、亲情、离世、我的父亲、我的奶奶、亲人、还有村里各位长辈,也有很多很多思考,但可能不那么适合写出来吧


只能说在有那么一刻,我深刻且贴切的感受到了亲情的冷暖和选择的无奈吧


也有那么一瞬间的快乐吧,看着五岁的侄女,开开心心的,不免也有些被感染,彷佛从她的身上,才能看到那些无忧无虑的快乐啊 image.png


小时候日日期盼的长大,长大后才知道长大是这一生最大的苦痛啊


短暂的相聚时间啊 😒


自从进入社会,开始成为一名社畜后,寒暑假的消失,让能待在家里的时间开始变得愈来愈短了。


好像每一年的开始,我们的时间就已经被定好啦


以前的过年,其实最好玩的是过年前的那段时间,家里开始筹备年货,准备各种各样的吃的,熬夜打游戏、看电视,一起嗑瓜子啊,和朋友们一起聚会啊,但现在已经变得越来越少啦。


回归中心,继续聊这个短暂的相聚时间吧,文字可不是拿来抱怨的呀~


今年回去,我也开始从我父亲那接过啦一些活,比如准备年货,包蛋饺等等,还有一些杂食,都交给我来做,在那么一刻感觉自己长大啦


另外每年大家来我家干饭的时候,总是要喝酒,我老爸又喝不了酒,就是我来陪酒,今年我几个弟弟,直接把我喝趴啦,麻啦麻啦


不过也只有在家里可以那么那么放松啦,出门在外,又能有几回这样的时刻呢


返回社畜岗位前,把自己的房间收拾了一番,晒给大伙看看吧


部分小玩偶房间里的书桌
image.pngimage.png

当时租房里面的小书桌,这个小桌子,也陪伴了我将近小一年,2022年的博客大部分是在这个小书桌上敲出来的,也是我看电影、刷剧的小书桌啦~


image.png

轻松愉快的时光 😊


生日、吃饭、按摩、记录生活、离开前的放松时光


清楚的记得,这是我今年第一篇日记朋友圈的开始,写的很长很长,还放了自己的自拍照


顺颂时宜,百事从欢


打游戏&按摩篇


身边有个社牛朋友,快乐有时候会变得很简单啊,笑点也会随之一降再降啊,哈哈。


我俩在游戏城的时候,他拿全部彩-票换了把M24,他说:“这是他绝地求生,丢了所有装备都要捡的枪”,他拿到枪后的第一时间就是看看哪里有小孩子,说这必须得让他们看看,男人至死是少年,哈哈哈。


image.png

观影篇


夜晚一起去按摩的时候,看了电影《人生大事》,借用一句豆瓣的影评:“人生除死无大事,天上的每一颗星星,都是爱过我们的人”。 在今年的年初,我也亲眼看到了一颗星星缓缓上升啊,请各位要早点爱自己所爱的人,人生总是在不经意间就会留下遗憾但是我们就是不要……不要留下太多遗憾啊。 在一切都还来的及的时候,去爱你身边的一切,去尝试生活,去绽放自己的光芒,那就是属于你的人生大事啦


后面还去看啦流浪地球,我能说的是“十分震撼人心,这是一部值得我推荐的电影,如果有可能的话,一定要选择 IMAX 影院,观影体验会更佳,也更能展现这部影片的大制作”。这部真是为数不多让我在朋友圈中写上这么主观的电影观后感的片子啦。


image.png

脱口秀篇


第一次去观看了线下的脱口秀表演,那时候我才知道,原来脱口秀演员,大都数是兼职的。


观看体验的话,具体得看演员玩的是什么样的梗,有优雅的,也有不太优雅的,还有一些内容是16岁以上的朋友们才适合明白的,遇上一个能活跃气氛并且能和观众们互动的表演者的话,体验就会非常好,如果平平淡淡的话,就会显得有点枯燥啦。如果要想深度体验,就要坐第一排,那样乐趣会多很多,不过前提是需要能够接受调侃


(补充:一定要遇到开合适段子的脱口秀表演的演员,有些重口味的,是有点难受的)


哦对啦,还遇到了一位杭州的朋友赠送了一份属于ta的运气王的票和夜宵套餐


image.png

干饭篇 🍻


干饭篇(一),长沙的《一盏灯》,主打一个辣,不过味道还是蛮不错的,哈哈哈


image.png

干饭篇(二)名称忘记啦


这是第二天我生日时,拉着朋友一起去了另外的苍蝇馆子


那天聚会的餐桌旁,有一位同年同月同日生日的伙伴,我的社牛朋友就拉着隔壁桌的朋友一起给我唱生日快乐歌,让我也给人家唱生日歌,我只能说当时我的脚趾在扣地啦


不过也算是有缘认识到新的朋友


image.png

即使许久之后再看到那天的视频和图片,也仍然觉得是有趣和有缘


干饭篇(三)北二楼大排档


接脱口秀篇的夜宵套餐,这家店味道是真不错,值得我推荐。


哈哈哈,来长沙可以去试一下,味道和价格都可以,唯一的缺点就是排队排的太久啦,当时我们两个好像从夜晚八点排到了夜晚十一点,中间去了那个HIB HUB公社酒吧溜达,我们两个去看男孩子跳舞,哈哈哈哈


我是真佩服那会的我们,吃的是真多啊,这我们两个人的吃的量...


image.png

干饭篇(四)烤肉店,店名忘记啦


我们一伙人在一起,主打一个笑点低,啥事都能笑半天,我这种高冷boy直接被同化成搞笑boy


image.png

干饭篇(五)喝酒&夜宵


这个忘记是哪一天啦,只隐约记得,凌晨三点多,一起喝完酒,和朋友一起在街边买烤串和麻辣烫,那时候是真放纵自己啊,现在是吃了晚饭,就坚决不吃东西啦,我要减肥啊啊啊啊


清吧喝酒街边小摊的夜宵
image.pngimage.png

美术馆&看展 🖼


在长沙的时候,一个人去了长沙美术馆—齐白石先生的画展,在馆内看到了曾经语文教科书上那画得惟妙惟肖的“


我本没有艺术天赋,去看展大都是静心养气,瞎逛闲逛,但有些美,它就是美得非常直接,直击你的心灵。


有些画本身就真的非常美,你一眼看上去,就会立马喜欢上,换我也想收藏这些画😃


云海夕阳蚂蚱独身一人
image.pngimage.pngimage.pngimage.png

总的说来,这可能是我这一年来最放松的一个月啦吧,这么吃喝玩乐的原因也是朋友准备离开长沙啦,就聚一聚,在长沙逛一逛,吃一吃。我也非常开心,看到自己有留下这么多的瞬间和记录。




在此时此刻写下这些文字时,我才感觉自己有真实的在这个世界上活过一样。


裸辞 | 年少轻狂或是年少无知吧 🤡


在去年我就有考虑离职,但一直没有行动,主要一方面是能力不足,第二个是没有足够的存款,无法承担裸辞后的风险。


但直到我正式提出离职,其实我也还没有准备好,只是觉得拖得太久啦,不能再内耗下去啦


离开


我最终还是决定离开了我职业生涯中的第一家公司。


image.png

总的来说,其实这家公司各方面还蛮不错的吧,位于长沙、双休、不加班、偶尔也有个下午茶。当然这都是不谈薪资的情况下,哈哈哈。




只言片语聊一聊离开的原因吧。


关于离开,这个想法其实在进入这家公司数周后就有啦。熟悉一家公司所要花费的时间,真的没有想象中那么长,几周到一月,大致就可以把所属团队的技术栈,技术水平、团队氛围、领导性格、团队主要工作方向都了解的差不多。而通过这几点,大致就可以决定或影响你在这个团队中所待的时间啦


对于我这种年少无知的年轻人,那个时候我最看重的是技术水平,如果让我觉得没有技术可以继续学下去啦,就会下意识认为继续待着是没有意义的啦。


但是换到现在来说的话,有几人又能够一直做技术基建工作呢?在小公司又能做什么样的基建呢?另外IT人这么多,大家都卷技术,又能卷赢谁呢?无疑都是为了生存下来罢啦


另外很大程度上是业务推动技术发展,你如果没有十万级的数据,坦白说你日常开发中,真的不会老是去想怎么优化SQL,改数据库配置之类的,没有百万千万级,很少有机会去思考去实践分库分表这些操作,不得不说,平台真的非常重要的。


我现在还记得去年我写到一篇文章,聊到过“团队>领导>个人努力”,优秀的团队,放手让你干的领导和努力的自己




说回这小节的中心吧,我跳出属于自己的那个舒适圈,有以下几点原因吧:


1、年轻气盛。想的是年轻再不出来看看,以后混不下去了,退路都没有啦


2、学习技术。想要学习更多技术相关的知识,薪资可以少,但吃饭的技术不能停滞不前。(不过现在看来,还是蛮天真的,现在是有个班上就行,哈哈哈哈)


3、完整的软件开发流程。想要经历从零到一的项目,项目需求、文档、画图等等,都想要去了解一遍,而不仅仅是开发代码(事实证明,浪浪山外还是浪浪山,只是山大了一点,爬山的人多了一些。)


4、薪资问题。(这个对于当时的我算是问题也不算是关键问题吧,当时真觉得是只要能获得成长,我就觉得非常OK,现在让我选的话,哪家给的多就去哪家啦😂)


心路历程


团队内交流。最开始对职场是陌生的,这个要离职的想法并没有太藏着,团队的老哥都知道我这个想法,当时都挺照顾我,也没谁介意这个问题,后面在真的到离开的时候,老哥们提醒我,下次如果再离职,千万不要声张,悄无声息的就好。后面仔细想想确实是这样,你大大咧咧的说,比较影响公司团队氛围。


与人事交谈。最开始正式知道的我想要离职的第一个人是人事,因为人事和我谈岗位调整的问题,需要增加一个运维的工作给我,附带调整薪资。但那个时候我,我已经想要提离职了,就不想在交接工作时,再附加其他问题。我就提出了近期可能会离职的说法,提了之后,我的身份就变得十分敏感啦。那会人事有事没事就问我,打算什么时候离职啊之类的。现在回想起来是真不应该啊,说了之后导致该调的薪资也没了,也被大领导关注到我要离职的事情啦。


与大领导谈话。人事知道我的想法后,就向上汇报啦(与大领导认识,他是想让我之后接实施的活,后续与他和项目经理一起去到项目上)。他了解完我离职的想法,先是肯定,后谈薪资,谈未来工作,定军心,提出挽留;不过最后我还是婉拒啦,抱着那一丝对大城市的向往跑出来啦吧


与直属领导交谈。最后和直属领导了提出了正式离职的想法,与大领导类似的,也是支持和提出挽留,我都一一拒绝啦,最后只有支持和祝福啦。


正式离职。交接工作、清理账号权限、格式化文件、领导签字、人事签字,拿到离职证明,和团队中的各位伙伴说再见,也是下次再见。


坦白说,当走出公司门,还是有那么一些不舍吧,就感觉以后不会再踏及这个地方,只能是一段回忆的感觉啦。


短暂的轻松快乐刚离职的时候还能的听《蓝莲花》,还能打一打王者。后面真的是...


出远门前,还回了一趟家,和老爸老妈待了几天。


无意中聊起年龄、结婚和生孩子的问题,才发现我家老爸老妈转眼间也老了,白发也开始显眼起来了。上次聊到这个话题还是在朋友家听她谈到相亲的话题,说她的父母五十来岁,年龄再大些,站到台上就不那么美丽啦。


心情也莫名沉重了几分~


那时候想到今年的Flag之一是带着父母出去外面按按摩,洗个脚,在那一刻想的却是今年要多挣点钱,过年回去的时候,陪他们一起去医院好好做个完整的体检,好让他们在之后的岁月中可以平安健康的享享清福。


不过现在看来是都容易啊。


最后的最后也只是回家简单吃了两顿饭,然后就踏上了去远方的旅程啊。


也许我们回家和离家的路总是开心的,可父母都是站着门前望着,一个是望着回来,一个是目送着你的离开。


羊城飘荡 🛫


到啦这边之后,投了差不多两三周之后,情绪心态已经完全不一样啦


那份来时的轻松自在已不再了,虽不至于坐立难安,但也是焦虑丛生啦。


不过认真说起来,在羊城的那段找工作的时间,虽然没找到工作,但说真的其实开心还占了多数,可能还是和那群一起爱搞事的兄弟们。


image.png

坦白说,写出来,我感觉有些小伙伴都不太会相信,在一室一厅的房间里面,最多的时候一起住了五个大兄弟,哈哈哈哈哈哈。


当时都是碰巧辞职或者是想在羊城或者鹏城找个工作,就从最开始的两个人,一步一步的凑到了五个人。


那会可以说整个房间可以说是能利用的都利用上啦,房间里的地板、木板床、沙发都睡着人。


突然想到啦那句“三块是面子,四块是生活。不是小瓶买不起,而是大瓶更有性价比。”


我们则是”不是房租出不起,而是一起住更有性价比“,也感受了一番城中村的生活


image.png

在那间房里我还有另外一个身份,”大厨“,那时候也靠这件事情转移我的注意力,同时也算是造福几个哥们,给他们做了一段时间的饭菜。也只有在做饭的时候,我不用去想找不到工作的焦虑


落地羊城后的第一顿饭我第一次做九转大肠
image.pngimage.png

生活也蛮有意思的,凌晨夜话;失眠拉着大家一起发疯;翻来覆去睡不着时,就要问一嘴谁还没睡,起来聊会天;一起出去玩,逛动物园、博物馆、广州塔、大佛寺、相亲角等;还有夜晚出去散步一起喝蜜雪冰城,哈哈哈哈


逛相亲角还蛮有意思的,以前都是在网上看,真来到线下,看到那一张张如同商品挂在上面的个人介绍,压力顿感庞大,优秀的人真滴是太多啦。


夜晚的小蛮腰大厂
image.pngimage.png

广东省博物馆动物园
image.pngimage.png

明年有机会应该去看一看上海市博物馆。


另外还凑了一波雪王的不倒翁,来到魔都也想继续凑的,但是这边没有卖,遗憾。


image.png

非常感谢有他们,我才能够较为开心的度过了那段十分焦虑的求职时期吧


后来的后来,我因为身边其他兄弟引荐,机缘巧合下来到了魔都;一个则找工作去了新疆;还有一个也留在羊城了,不过已经不再从事编程这一行业啦


真正意义的相隔千里了,只有在年前年尾才能短暂的再相聚了呀




在羊城的时候,机缘巧合下,博客有帮助到一名网友,碰巧也在广州,就线下一起面基啦。


当时他还在中国建设银行实习,一起吃了个便饭,聊了很多,溜达了一圈,看了广州塔,我也有不少收获。


关于这一点我多补充一下,多和行业内的小伙伴们交流,我个人觉得真的是有意义的,大家可能都是做开发,但业务可能不同,技术栈可能不同,背景不同,很多想法也是不一样的,每次交流可以收获一些以往不知道的知识吧。


离开后,已许久未曾联系啦,希望一切安好和顺利!祝福


人这一生就是在不断的遇见和离开,珍惜每一次的相遇,或许就是我们能做到的最棒的事情啦


辗转流离 🚄


为啥说是辗转流离勒,还要听我细细道来啊。


长沙裸辞


从长沙裸辞,我就回了趟家,看啦看我的老爸老妈,在家真的只能待三天。再久,那个经就会念起来啦,哈哈哈。所以三天一到,我就踏上了去往羊城的道路啦。


当时是还没被社会毒打,心情还不错,后面真的是心态一崩再崩😐


现在想一想,我都觉得当时的自己是真勇敢,换现在的我,真的不敢裸辞啦,手上没余钱和找不到工作的焦虑真的太折磨人啦。


羊城飘荡


还是多亏有兄弟照顾,来到这边,有个住的地方,没有太多的经济压力,只要认真投简历,找工作就好。


即使后面投简历投到心态爆炸,也因为有兄弟们一起陪着,让那些焦虑生了又灭了。


如果心里有事情,是可以找一找身边愿意倾听的朋友说一说的,当然不是长期输出负面情绪,而是适当的说一说,憋在心里容易出问题


鹏城溜达


要离开前,去鹏城溜达了一遍,和我的表哥表姐吃了一顿饭。因为离开广深地区,下次再相聚也就只能是过年啦啊。


我姐的小女儿,超级可爱的小侄女~


写到这里的时候,我还想起了当时答应她,下次去要给她带玩具,我还写了备忘录,过年要准备准备啦


image.png

image.png


和我老哥晚上吃夜宵,但没想到的是,这顿生蚝将是我今年吃的最爽的一次🤡


(xxxx,上海夜宵店上的生蚝,真不知道是个什么超级刺客价格,真的该死,我这还是在深圳商场里的夜宵店里面吃的,又大,而且比起上海便宜好多,我真的气死啦)


烧烤鲜甜的生蚝
image.pngimage.png

转战魔都


转战魔都前,我又跑回家躺了两天,你没看错,我又回家躺了两天,其实也是感觉如果来了魔都,可能要等到过年才有机会回去啦【事后确实如此】


然后从湖南出发去魔都~


昂贵的高铁费上海虹桥站来接我的科科ikun之家
image.pngimage.pngimage.pngimage.png

我这个小黑子成功加入ikun之家,开启啦在ikun之家的生活~


来到上海后,重新整备后的书桌:


image.png

工作 | 新的启程 🐱‍💻


转变


来到魔都后,工作方向略有改变,不再是单纯的Java开发吧,没法单纯的说是什么样的工作吧,定位也不够准确。


另外也是目前没什么大的成就,就不太想谈论工作这个话题,明年慢慢去更新更多的文章,会一步一步谈论到现在的工作吧


不过我还是活跃在一线开发岗位上的,最让我苦恼的是垂直领域深度不够,非常烦啊。


但这边的整体团队氛围、管理风格都让人比较放松,工作也相比以前开心许多,有一段时间让我一度找回了开发的乐趣。


来到这边后,有几个方向的提升吧。


1、公司提供了更大的平台,接触到了更多的新事物;


2、负责的事情要比之前多;


3、技术的横向扩展拉得比较广。就是有点大网捞鱼,鱼全部漏掉了的感觉,头大。


4、扁平化的管理,让我更大胆的去做事


总的来说,是非常棒的,我也在努力的将手上的事情去做好,也在寻找工作上的乐趣,而不是麻木的工作


image.png

下班等电梯时的随手拍,剪刀手boy~


因为目前正在岗位上,有些内容是没有办法写出来的~


补充


浅浅的谈论一下求职这个问题


我在去年和今年年初都有求职的经历,只差一年,但感受相差甚远。


求职


去年的情况还只是有些糟糕,但还能约到面试,到处也还在招人。


今年离职出来的时候,可以说是寒气逼人,先说我自己,我是陆陆续续投了一个月,投了1.5k左右,能约到的面试少之又少,贴几个朋友的求职经历:


朋友1,求职前端开发,base:深圳、广州、厦门,boss投递3k+,4个月还没找到份前端开发的工作,最后转行啦;


朋友2,求职Java开发岗位,base:深圳、广州,boss投递1k+,最后找了个外包去了新疆;


朋友3,求职前端开发,base:深圳,找了近3个月,最后转回安卓,上个月才收到一个安卓岗位的offer;


朋友4,求职Java开发,base:成都,有美团短期实习经历、银行6个月的实习经历,在成都找了3个月,上个月才入职新的公司,转向 kotlin 开发啦


上述都是求职初中级开发,年限1-3年




收简历


这边公司有开放过岗位,因此有过收简历的经历,浅谈一下,当时两周左右,看了将近300份的简历,本科生研究生的简历都不少,就我个人看简历的心态变化大致是以下几个阶段:


最开始:每一份简历都看的非常认真。看技术栈、项目经历,比较需求匹配度,后面再看学校如何,判断是否进一步交流;


中间阶段:每天都收,真的太多啦,有时候看都看不完(有工作,可能是每天晚上回到家才看),开始转变成,没有get到需求可能就忽略了


后期:只要基本满足条件,学历优秀的优先谈。不行就下一个。主要会去关注下面几个问题,基础条件是不是满足;相关的技术栈的熟练程度;进行前期沟通,了解性格,初步判断能否融入当前团队;入职后,能不能发挥他应有能力;稳定性等方面的问题;


小小的思考
今年我求过职,同时又收过不少简历,后面就补了这么一个小节。


找工作难,招人也难


说实话,如果没有实际工作经历,看十份简历,有六份简历中的内容基本上大差不差的。


你说怎么选。


以前是想着跳槽涨工资,或者是想找个好工作,今年的想法是能有个b班上就不错啦。


魔都生活 | 出游 | 干饭


在魔都的日子,出门时间其实不算多,工作日的三点一线,休息日的步数100,总的来说,不是在干饭的路上,就是在干饭的桌上


外出篇


大魔都的标志性建筑物--东方明珠塔。


美是真的美啊,也是来到了外滩才深刻感受到金融到底多么挣钱,外滩这边一条街,全部都是xxx银行,感觉没有一栋楼,都不好意思说自己是个大银行啦。


白昼时的东方明珠傍晚时的东方明珠
image.pngimage.png

夜景更美一些,要不是上塔顶的票太贵,我感觉我已经冲啦


豫园


国庆假期时过去溜达的,当时好好的感受了一下上海的City Walk


挹秀楼阁楼
image.pngimage.png

有没有觉得第一张照片中阁楼里的两个闪光灯非常有趣呀~


广富林遗址和醉白池公园


天高云淡时,和室友一起外出骑行,去看看外面的风景~


广富林遗址醉白池含苞待放
image.pngimage.pngimage.png

游泳


和两个小伙伴一起去游泳。不对,他们两才是游泳的,我是去喝盐水,是的,我这个小菜鸡还是不会游泳,哭死。


image.png

不过我对于魔都这座城市知道的还是太少,那么多馆,那么多展,那么多景点,但我是动都不想动啊。


希望明年可以去做更多探索吧,如果有小伙伴一起的话,那就更好啦。


干饭篇 😲


在写这一小节的时候,我想我终于找到我自己反向减肥十斤的原因啦🤡


十天的照片里,八天的照片都是关于吃的… 我自己都不敢相信


同事聚餐、在家做饭、周末小聚、生日聚会、夜宵、外出游玩干饭、疯狂星期四,各种各样的干饭,上演各种碳水炸弹


可是今年原来的的目标要减肥的啊啊啊啊啊啊啊🥺


偶尔周末给自己和室友加餐的日子


干饭加餐篇
image.pngimage.png
image.pngimage.png

与团队中的小伙伴一起在住房里面小聚一下,再加上一点小酒,不免也是一个快乐时光


热时多数是炒菜,转凉后多数是吃火锅,夏天小龙虾和啤酒更配哦,冬天则是牛羊肉更暖身啦


聚会篇繁忙工作里的畅谈和轻松
image.pngimage.png
image.pngimage.png

大声呼喊着减肥,每天和室友一起吃荞麦面,还一起跳绳,但。。。


实际情况是工作日每天吃荞麦面,一到周末就是夜宵、KFC、聚餐,妥妥的反向减肥


image.png

夏天时,自制青柠和百香果的饮料果汁喝,非常爽哦


青柠百香果
image.pngimage.png

(别看我说的这么好,实际上我把所有装备买齐,就做了三次,哈哈哈哈)


外出干饭,聚会~


补充:黄酒初喝不上头,喝多啦该醉还是醉,下次我要坐小孩子那桌去


温暖你的猪小餐馆淞沪名灶和记小菜
image.pngimage.pngimage.pngimage.png

安徽


今年有幸到过两次安徽,一次是去了黄山,一次是去了合肥,尝试了安徽的菜,我觉得也蛮好吃的,可能也是偏向于辣和咸,挺合适我这个湖南人滴


总的来说,感觉内陆省份的菜品都偏向于重油、重盐和重辣,而沿海省份,感觉就是鲜、淡、甜,吃食材的本味~


忘记名称的店脑海中只记得吃啦地锅鸡
image.pngimage.pngimage.png

我们这群人属于是,遇上开心的事情,烧烤配啤酒;碰上不开心的事情,也是烧烤配啤酒!!


开心烧烤配啤酒不开心也是烧烤配啤酒
image.pngimage.png

疯狂星期四


以往都是说段子,后面真是每周都过疯狂星期四,碳水炸弹


image.png

细细想来,我的长胖好像也是有迹可循的,没写出来前,我日常还觉得我自己吃的不多😂


总的来说,在上海的周末做的最多的事情就是干饭啦。


室友篇


从个人生活状态转变到合租生活状态,其实有不少改变的。


不过非常幸运的是,在这里遇到的每一位室友都非常棒,也分别给我带来的不一样的影响


首先第一点,室友们都是大厨,这才有了上述那种每天吃吃喝喝的生活。


如果要是我一个人独居的话,吃饭在我看来更可能一种任务,随便应付一下就算是完事啦,根本不会去想着一荤一素,更不会想着好好做个菜啦,哈哈哈




keke哥哥最开始来到魔都时,我没有另外再出去租房啦,就是和keke哥哥一起睡。因为他有考虑后面要搬出去和女朋友一起住,所以最开始我们就是睡一起的;坦白说,如果他女朋友没来,他最后没搬出去住,我感觉我们两个男生才像谈恋爱的感觉【捂脸】


生活非常同频。今天我买菜做饭,他就洗碗;隔天他买菜做饭,我就洗碗;作息时间同频;有问题会直接说;一起电动车上下班;周末一起去游泳;一起吃KFC;半夜睡不着一起骑电动去吃夜宵;一起养猫;一起打游戏,cf、王者五排;安静互相不打扰,多数时候是一起同行的。有那么片刻,恍惚间有感觉像是在谈恋爱的。


这种生活一直持续到他女朋友到来,他搬出去之后才结束,说起来还是有点怀念啊。


要是谈恋爱的时候也能是这种感觉,可能我现在也不会一直单身着啦吧




wancheng哥哥。这个叫法是因为我某一天打王者,听到一个男生在用那种夹子音叫哥哥,我一下就学会啦,然后就整天在他们的名后面加个哥哥,哈哈哈,最终的结果就是整个房间里,所有人都被我带偏啦~


wancheng哥哥是这里的大厨,要是每次有个什么聚会,肯定就是我们两个搭配掌勺,不过我们两单独某一个人都懒,但要是一起合作去做某件事情,就会非常认真的去做,想要去做好,而不是偷懒。


还有他今年经常带我一起消费,笑麻了。他买机械臂,就会非常真诚的向我安利它,陈述它的一些优点,把我说的心动,然后就是买买买;冬天来啦,他买暖风机,也会非常真诚的说这款产品的优缺点等等,最后我又忍不住去剁手;还有其他的,我自己都被自己笑麻啦。另外大家都偏向于及时享乐,我也有被影响吧,多了很多消费,也比以前果断许多啦。


对啦,最最最重要的一点,他现在不减肥啦。然后他现在是直接往住房里疯狂买零食,放在客厅的桌子上,他知道我一定会去吃,我真的哭死,他真是生怕我瘦下来啦。周末吃完饭还要再点个烤串,减肥减肥,越减越肥~


不过也有带动我共同提高。因为他老是来查我房,来查我在干嘛,是不是在打游戏;如果我是在卷的话,他也会立马回到房间去卷。当然我也会去反向查他的房,他如果不在下棋,我也会立马回到房间卷,坦白说,是有点小孩子行为的。


补充:要非常感谢wancheng哥哥,还有未曾蒙面的阿姨,才让我们在魔都有吃不完的湖南特产,剁辣椒、腊肉、腊牛肉、腊肠、萝卜条、豆鼓,对啦还有扣肉等等,真的太棒啦




另外还有一个我此前并非那么了解的室友-sian哥哥,在今年给过我很多帮助。在和他的诸多交谈中,我是真的受益匪浅,也有重塑他在我心里的印象


在这里暂且称呼他是个游戏佬好啦。在我以前十分局限且带有偏见的认知中,对游戏佬的印象都比较一般。人的专注是有限的,沉迷于某一个事物当中,那么自然对其他事物的关注肯定不会那么高啦。


很长一段时间里,我认为阅读是最容易带给人深度思考的,但是在和他交流的过程中,我发现,其实我们专注做的每件事情,都是值得思考的。


比如他热爱建城类的游戏(请原谅我已经忘记游戏名称了),可能部分玩家是去追求那张掌控城市的爽感的,他在我看来,则是那种享受自己从零建造一切的成就感的。在那么一刻我突然意识到,在游戏中完成一个从零到一的任务,换成我们编程的说法,也就是从零到一完成了一个项目啊。仔细停下来想一想,是不是这样


他做的更好,他将这种从零到一的思维方式,从游戏带到了现实生活中,在和他共事的过程中,我发现他的思考和规划大多是从全局到局部的。


这点我是非常想要学习的,因为我个人的观念,老是非常局限,不管做什么,我第一个想到的,总是站在自己的位置上去思考问题,跳不出去,无法抛开自己的固有思维去思考问题犹如井底之蛙。


另外他是我身边朋友圈里,和我一样,不刷抖音的人,都是b站的深度用户,喜爱那种能够引导人进行深度思考的长视频。


不过话说回来,我感觉我今年比起往年心浮气躁了许多,以前还能心平气和的看完长视频,今年已经要加速啦。


朋友篇


今年有幸在魔都认识到一位非常真诚的朋友,见证过ta的勇敢,也见识到的ta的真诚,也有被ta写的一段话所惊艳到。


“我一生都在探索人性,我知道现实残酷人性凉薄,所以我任愿抱以真诚去对待所有人、事。


我所付出的那些金钱和精力,又比如你说我以后会吃的亏,在我看来远不及一次灵魂碰撞来的重要。


同样真诚的人难能可贵。


这条路艰难,我必定会历经诸多碰壁。


我可以被拒绝可以被推开,我都不在意,但唯独不能不真诚。”


生活状态的转变 🏄‍♂️🤼‍♀️


写完上面那部分内容,我在思考一个问题,今年的生活状态和去年还是有很多不一样的。


我在湖南的时候,多数周末还是会出去走一走;但是在上海,外出的欲望是真的低啊,直接进化成宅男啦


思考了几个原因:


1、交通不便。目前在魔都比较偏的地方吧,附近没有地铁口,只能坐金山铁路


2、外出通勤时间太长。因为没有地铁,出去一次,动辄一个小时、一个半小时,出行欲望极低。来上海近半年,准确来说我只坐过一天地铁,那时候好像还是是火山引擎的一个活动,在市中心那边的一个酒店,那天我一个人过去的。其他时候好像完全没坐过地铁。捂脸,甚至于有时候同事聊到上海几号线几号线,我完全不知道在哪。


其他时候,和室友们一起出去玩的话,要么打车,要么踩单车,要么找公司财务借车。公共交通除了当时坐轨道交通去游泳,好像其他真没坐过啥啦。回想起来,我自己也觉得有点离谱啦


3、从个人居住状态转变到合租状态。开始习惯和与人同行,而非个人独行,以往自己要干嘛,我可能自己想啦就去坐了,当习惯有人同行后,我会下意识问ta愿意一起吗,如果他不愿意,我的意愿会降低。


4、脑子暂停转动😵。一个人住的时候,要做什么,想去干嘛,都是自己在思考,与室友一起住后,思想开始懒惰啦,比如吃饭,总想问一句他想要吃什么,不想自己去动脑子思考啦。


类比到我自己打王者也是一样,一个人单排,我会去想整体局势如何,该怎么打,人在哪,和朋友一起打,我就直接开摆,成为机器人,哈哈哈哈。


我仔细想了想,我并没有因为没有减少外出溜达,而变得不快乐,也没有讨厌宅在家里的自己,我觉得这种生活状态是适应当下的我。




但是我觉得我还是要做出一些适当的改变,如果长期不动自己的脑子去做决定的话,我感觉我会丧失掉自己的主动意愿,之后就更不愿意去思考啦,有些事情,想做就做啦~


《杂乱无章》 🖋


在今年有能力的情况下,这次终于可以好好的支持喜欢了很久的《杂乱无章》啦


购买了一些周边,其实不太贵,就正常的衣物、香水、护手霜、鞋等等。


不过我老是给身边朋友安利,感觉自己已经很久很久没有做过这样的事情啦,其实就是非常单纯的想要分享自己所喜欢的宝藏吧。


一直在等正常装的香水,可惜一直没有货一个人也要穿情侣装
image.pngimage.png

新品护手霜,味道非常好闻的 image.png


我关注《杂乱》,没记错应该是在两年前啦,现在回想起来我已经不记得我为什么会关注到它,但是我记得当时看完第一篇文章就点了关注,然后一口气把当时的历史文章全部看了,就是非常喜欢他们输出来的文字。


喜欢文字的朋友,可能也会喜欢上他们吧。


另外今年,他们在广州开展啦,可惜的是,不在江浙沪地区,不可惜的是,我让一个朋友去逛了这个展。


下面是展馆里的个别片段,希望来年能够来到魔都,不行的话,江浙沪地区就好,哈哈哈


文字
我们都需要适度抽离,谈恋爱要一直要有这个状态才行,偶尔的距离感,会让人舒适许多。image.png
希望你开心,无论和谁,不管在哪image.png
成年人除了容易胖,还容易加班,容易熬夜,还容易在杂乱无章里找到同感,真的太对啦image.png
给自己的生活留一些属于自己的片段image.png
《心情》“过年了!” “又过年了。“image.png

不知道,你有没有在在上述的几幅图中呢?你又是哪种情绪呢?


猫猫相伴的时光 🐱🐱‍👓


养猫,真的是一个极费照片的事情,哈哈哈


我在这之前,很长一段时间都没拍过啥照片啦,后面和室友一起养之后,可以说是,照片以一个非正常量级的速度往上递增,哈哈哈哈


image.png

它的存在,我相信是真的能治愈人的,上班一整天,回到家,看到它,那些愁绪会消散一大半


七月刚回来时,那时候还是娇小可爱勒 image.png


image.png

现在在室友饲养下,已经是胖嘟嘟啦,变得更可爱。 image.png
虽然最后,是让它和室友一起和室友女朋友出去住啦


但最后的效果是好的,它已经成功的胖嘟嘟啦,很开心遇见和陪伴,现在偶尔也能去看一看,也算是非常快乐啦


掘金瞬间


八月Docker技术征文一等奖


不知不觉中在掘金也待了两年多啦,时间真的很快啊,同时也代表着我写网络博客也近两三年啦


用心对待的博客,被掘友们用心认可,真的非常快乐,非常感谢每位阅读过的朋友


让我记录一下这个高光时刻吧~


有在多个平台写过博客,但最让我喜欢的,还是掘金


很多原因吧,有掘金的大方,也有因为掘金而相识的朋友,大家不仅是在掘金上有交流,也是彼此的微信好友,接触的越多,就越习惯这里吧,如果有一天不再玩掘金,那么可能就是这些朋友也在逐步退圈啦吧


image.png

图一:Docker技术专题的实体奖牌


也非常感谢斗金小姐姐第一个来告诉我这个好消息,不知道斗金小姐姐能注意到这句话吗,哈哈


要是其他运营小伙伴看到啦,劳请转达一下下啦,还有记得给来个推荐,哈哈哈


image.png

图二:在奖金的基础上加了些给自己淘来的4k屏幕


后续也有多个活动想参加的,有可能是懒吧,也有可能是心浮气躁,总之今年的博客数量对比起2022、2021年,差之甚远


2023年统计2022年统计2021年统计
image.pngimage.pngimage.png

真的是每年比折半还折半,明年我一定要努力克服🤡🥺


与友人见面 | 人间有味是清欢


因博文相识的朋友,时隔许久,终于有幸在线下见面啦


开始,可能还是要提起掘金,哈哈哈,那会加上好友,还是因为同时在掘金的“神转折滴评审团”中~


image.png

在这之后,其实成为了互相的读者和粉丝,ta的文章内容简短,偏向于日常思考、感悟和读书笔记。


话说ta的日更是我当时早上的摸鱼读物,哈哈哈哈


我个人也偏好去阅读其他人的想法,想了解其他人对于某件事情是如何看待的。


ta的文章也让我在寻找自信以及与自己和解等方面,荣获不少的成长。



去年写到”我们是彼此的正向反馈”,那么今年就是”幸会“。



“杭城,幸会”


人间之幸事,遇良师益友,品人间有味,游山河大地


在这月准备写年终博文时,在回忆在思考,今年有哪件事是想做而未做的,最后才有啦这次的见面和交流~ 很感谢那晚的思考和感性的自己,才有今天关于杭城的人和事的记忆。 或许这趟出行,是略有唐突的,(因为我的到来,让朋友有不小的心理负担,特意规划了去哪里,从那里走到那里,非常细心),辛苦啦,庆幸的是,最后都收获满满~


幸会”,有预想过双方的见面,虽然都感受对方都是极易相处的人,但是也不免有一丝不安吧


但真实情况远比起预想中要好,可能是我因为成功的把我那份社牛属性释放出来啦吧。


当文中那个积极向上、热爱生活的形象真实立体出现在眼前时,有那么片刻的不真实吧,同时也非常激动;另外所期待的聊天,也贯穿于整个同行的过程,十分放松和愉快的一天,不用去这想那的。


期待下一次吧~


回忆起整个交流过程中有些问题我还存惑,不过我已经想好要再次拾起阅读啦


人间有味。一方面是尝了那份老杭州的美食味道,另一方面是品鉴了那份属于杭城的老味道。日常总是忙于工作,已经很少有过沉下心来去感受那些明明离我们很近,但又久久未曾感受的地方啦。


山河大地自然是杭州西湖啦。不过那时未曾记起苏轼先生的诗句,一开口便只能是,wc,好美,哈哈哈(已然决定要去b站学习一下关于东坡居士的历史啦),下一次再踏足时,希望自己能欣赏到另一种更深刻的美。


补充:中间也有过许多搞笑瞬间和尴尬的瞬间 比如下面👇的照片,已然是我摆的比较好的姿势了;不熟悉杭州路况,我老是走神开错道;还有最后找停车的地方,下到车库,都没想起自己车停在哪里,还信誓旦旦走错啦,回忆起来,我真的要被自己的信誓旦旦给整麻啦,现在想想都蠢死啦,还不相信你,麻啦;表情管理更不用说了,直接全放开,现在回头看照片,我真的😁😂🙄😵🤡


标题
image.pngimage.png

西湖边上拿着旁边的小朋友的饼干喂鸳鸯,哈哈


image.png

在我很喜欢的一个公众号《杂乱无章》的一篇推文中,看到了下面的这段话,我觉得是非常契合这次的见面


image.png


如果说见完面之后的想法是什么的话,一方面可能是想要继续去拾起阅读吧,觉得自己言语谈吐仍然有需要进步的地方;另外一方面就是想要改变下自己的仪态吧(在相机中的自己,实在是称的上难看啦,哈哈哈哈)


不管是和哪位好友的见面,我想我们都会永远期待下一次的见面吧


记录生活 | 观影 | 阅读 | 消费


今年总的来说,比起以往,是多了很多记录的,也是想用数字去量化,看看自己的变化。


比如关于看电影、剧,我今年线上线下观影和追剧总的来说是45部吧。有2/3我是写下了观后感的,还有三分之一要么是看完太晚,懒得写,要么纯粹就是懒得写啦。我看的大部分都集中于评分高的经典电影和突然刷到感兴趣的。




关于阅读的话,我今年阅读量降低了很多很多,只有寥寥几本😂


image.png

补充:不过这几本书,我个人觉得还是非常值得阅读的。《认知觉醒》有谈及到很多日常行为,阅读这本书的同时,会下意识的反省自己的行为;《娱乐至死》是新闻专业的考研读物,里面谈到的诸多观点,都非常有意思,而且十分深刻,但是有些话不适合写出来,只能自己知道;我还记得我当时读完这本书,就感觉是读书越多的人,并不是多么厉害,而是对这个世界背后的真实规则了解的越多,这本书,让我对宣传、新闻、娱乐了解的更深刻些了,也明白某些新闻背后的意图吧。新闻真真假假,但如今信三分都算多




关于消费。今年的消费直接上了一个量级,把自己也给吓到了。工作真就是给老板和房东挣钱,钱包该是多空还是多空。不过今年还是有几个大额消费的,主要是下面几个:


1、房租费用。一年下来真的心痛的要死;


2、双十一购物。有不少花销,反正花钱如流水的感觉...


3、一台二手台式电脑+一块4K屏幕


4、给家里打钱,给了妹妹一些生活费


5、个人消费也是真的高;


6、软件费用也贵。总的一算,都1k+啦,麻啦


白吃白喝魔都过一年


补充:详细的消费记录没有写到这里,不然大伙都该知道春春我还差一周,就真是一年穷到头啦。


立下来又总挂掉的Flag


照例回忆下去年年终写下的Flag,可以说是完成的非常糟糕啦


image.png
image.png

很坦白的说,完成度非常低,低到我自己都不愿意去翻看这个事情。


最主要很多不是无法完成,而是我没有去努力完成,这是我最无法接受的事情。


回头一看2023年,真的十分模糊,不知道自己到底做了些什么,恍惚间就又要到2024年啦


今年是这种感觉最强烈的一年,因为真的有过迷茫,找不到努力的方向,也浪费了许许多多时间。


我现在还记得我大学室友在今年12月问我的一句话,“二哥,今年我看你文章也没写多少,朋友圈也没咋更新,你今年到底在忙啥啊?”


我也不知道我忙了啥啊


关于明年


下面是我2024年的一些目标和想法,能不能完成,能完成多少,我心里也没底,反正都想冲一冲,试一试吗,先想啦,才能更好的去做,难道不是吗?


image.png


总的来说,主要是围绕生活和工作方面,其他的我没有再给自己立明确的需求啦,还有一些则是去年就立过啦,我到目前还没有完成,那些想法今年也会接着去做。


对于明年,我对自己只有一个要求,如果一件事情如果我能做好,那就希望自己能尽力去完成它,而不是因为懒惰而一拖再拖。一定一定一定要有执行力。


那么正在读这篇文章的你呢?今年的你想要做什么呢?你有什么样的想法呢?你会去尽力实现它吗?


在写完之后,我就已经开始着手去开始,有些事情想做就去做吧,没有必要一定要等到某个特殊的时间点之后,时间是在自己手上,而不是在日历上


长路修远,吾与子之所共适也


每每想到自己已将要岁至二十余四,不免有些恍惚,有些时候,在我自己看来,感觉自己还非常小,完全没长大,很多事情甚至都还没有去考虑过。


但看到身边的朋友,谈恋爱的谈恋爱,买车的买车,结婚的结婚,另外偶尔再加上家里边的催促,焦虑一下就放大了,莫名坐立难安


但其实大家都一样的,别慌,我先来自黑一下。


我呢,一年工资那么点,给房东一大把,自己吃喝玩乐一大把,购物一大把;约等于白吃白喝在魔都过一年呢~


技术呢,说啥都会点也是,但要说会多少,也就是啥都会一点,问多一句,就直接是啥都不会啦。


该努力就努力,该生活就生活,做好自己该做的事情;那么多的信息流,分辨不了,就少看点;你就是你,想那么多干嘛,想那么多也得烦恼今天晚上吃什么


无论如何,人生这条道路,仍然漫长而修远,我希望我们能一同享受这短短数十载。


那么这篇文章就写到这里啦。


如果你读到这里,如果你也喜欢的话,请说说你的感受吧,我真的非常想要收到来自于你的反馈,无论如何我都会认真的一一回复的,有问题也可以提出来,我也会一一回答~



2023年,我是宁在春,我在上海


那2024年,平安喜乐,万事胜意,祝你,祝我,祝我们



作者:宁在春
来源:juejin.cn/post/7317091895171153920
收起阅读 »