开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

一位转行Java上岸4年到技术专家的经验分享

发表于 2024-01-15

自我介绍:

大家好, 我是你们的朋友, 晓龙。今天我想分享一下我自己艰难的Java转行经历。

为什么没有学历 , 因为大学上了一学期, 当时因为一些原因, 就辍学了。年少无知 , 发现到社会上找工作, 没有学历就没有敲门砖,最低要求也是大专。在2018年报考了大专的网络教育。同时, 自己也开始了金融学本科的自考。2018年还是比较的迷茫, 也不知道能靠什么挣钱。2019年, 开始在黑马进行Java的培训, 因为大概有4年没有学习了,加之因为0基础, 当时去的时候连Java能干什么也不知道, 所以学起来还是比较的吃力,尤其我记得当时对方法的定义, 是真的理解不了。

开局buffer就叠满了,运气一直比较的好。

工作经历:

2019年9月左右, 在武汉经过几次面似乎,找到了第一家公司, 规模比较的小, 0-20人, 当然对于当时的我来说, 能找到一份工作(上岸),也是非常开心的, 但是很遗憾的是, 这家公司我去只坚持1个月, 公司就倒闭了。

但是也没有急着找工作了,花了20天, 花了20天把自考的科目复习,在11月份继续在武汉找工作,经过面试,入职了一家比较小的公司, 后端就2人,都是培训出来的。当时我的想法很明确, 花时间补知识, 在培训班的6个月, 时间还是比较的紧的,很多东西只是知道, 都没有亲自动手实践过。公司好在不是很忙, 有一定的时间学习。当时上了2个多月,就回家过年,后面因为yq , 就没有去武汉了,这家公司也就上了3个月的班, 一直在家呆到2020年年7月,期间把慕课网的架构课程好好学习了。7月开始在重庆找工作 ,经过了一周多的努力, 找到了一家更加不规范的公司, 后端2个, 前端一个, 产品一个,其他就没有了,还在薪资给的还可以, 这个期间继续学习, 真正的代码都写得比较的少。很遗憾, 公司只坚持了6个月, 2021年1月, 公司又倒闭了。

我清晰的记得当时准备面试的那一周,是压力最大的一周。在经历了前面几家公司后, 我也知道, 如果还去一家不稳定的公司, 结果还是这样, 几个月就倒闭了, 而且整个公司没有研发体系, 自己就永远不能真正的上岸(野路子到正规军)。虽然前面1年多代码没有写多少, 但是还是有一定的知识积累。

自己也没有放弃, 我当时在BOSS上看准了一家公司, 然后就开始好好的准备, 不仅仅是准备技术方面的知识, 还去了解公司的背景, 当时觉得这家公司规模比较的大。当时面试完后, 顺利的拿到了offer , 期间我和面试官(也是我的领导)说, 我非常期待能来公司上班,即使不要钱,但是没办法, 我自己得生活。直到现在, 我还是非常的感谢这位领导, 也会在微信中和他交流近况。


是的 ,从某种意义上讲, 现在才是真正的上岸!

e3d060f3a7fbe92678d66225a7c841c.png

我在这家公司感受到什么是真正的做开发。公司有正规的研发流程, 完善的研发体系, 每一个同事都身怀绝技。在这家公司, 我给自己的定位是2年成长为高级开发工程师,期间也暴露出我的一些问题,比如代码逻辑写得不清楚(之前代码写得太少了),设计做得不好等。

我是渴望成长的, 所以我针对自己具体的问题, 在2年的时间里, 充分利用自己的时间做了这些工作:

第一阶段: 在试用期的前三个月,虽然完成了业务功能开发,但是意识到自己的代码量还是远远不够,在以最快的速度完成业务功能开发后,投入leetcode的算法练习,每一道题就是一个功能的缩影,在完成300+的练习后,业务代码就得心应手了。

第二阶段: 当时培训机构的架构课对我来说,是无法转化为我的能力的,我学习它无疑是浪费我的时间,所以我更多的选择了自己去寻找资料和书籍来学习,主要是针对这几个方面,操作系统,计算机网络,netty,JVM,并发编程,框架源码,此过程历经1年。

第三阶段: 系统设计,在经历前两个阶段后, 是时候提高我的系统设计,架构能力,这个东西学不来, 靠悟,也是最难受的地方!每一次的业务功能开发前,我都会阅读软件设计方面的资料(每阅读一次,都有不一样的收获)再对业务功能进行设计,非常的耗时,但也非常的值得,经历了一年,对模块和系统的设计都有了自己的理解。

第四阶段: 和产品的深度沟通能力,无论是在需求评审,还是自己负责的模块,系统,都要和产品深度的沟通,这个阶段经历了半年,完成了这4个阶段后,自己的能力得到了极大的提高。跳槽成功


50d3627eb62fe1ee94a94520b55feca.png

image.png

05de66c2fa3c081391f4154bb9701d8.png

大家会说, 现在的环境不好, 不好找工作。从我自己的经历来看, 转行培训过来的同学, 开局不管怎么样, 不管遇到多大的困难,不要放弃自己, 请坚持学习, 好好的积累3年,完善自己的各项能力, 那个时候才是真正的上岸,自己也不愁找不到工作,拿到自己满意的薪资。

目前也在写重写自己的系统框架(脚手架)fd-frameworkfd-framework, 今天为了解决springmvc对请求时间进行统一格式处理 , Long精度丢失问题处理问题, 仔细花了大概5小时阅读里面的源码,期间的实现方式改了4版, 每一次的深入,都是有新的实现想法, 真的感觉很开心,哈哈, 预祝各位转行的同学上岸。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

这年头,高低也要画几张架构图! 什么是架构图? 画什么架构图

发表于 2024-01-12

什么是架构图?

架构图的定义及作用

什么是架构图?维基百科、百度百科其实都没有关于它的直接定义。不过我们可以进行拆分理解:

  • 架构图=架构+图

这样问题就转化成,什么是架构,以及什么是图?

关于架构,百度百科上是这样定义的:

架构,又名软件架构,是有关软件整体结构与组件的抽象描述,于指导型软件系统各个方面的设计。

ISO/IEC 42010:20072 中对架构则有如下定义:

The fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution.(系统架构,体现在它的组成部分、它们之间的相互关系和环境中,以及控制其设计和演化的原则。)

也就是说,架构是由系统组件,以及组件间相互关系共同构成的集合体。

而架构图,则是用来表达这种集合的载体。

它的作用也很简单,两个:

  • 划分目标系统边界
  • 将目标系统的结构可视化

进而减少沟通障碍,提升协作效率。

画什么架构图?

架构的分类

架构大致可以分为4类:业务架构(产品架构)、应用架构、数据架构和技术架构,整体逻辑关系如下由上至下:

test8.jpg

业务架构:

使用一套方法论/逻辑对产品(项目)所涉及到的业务进行边界划分。所以熟悉业务是关键。

比如做一个电商网站,你需要把商品、购物车、订单、支付、退换货等进行清晰划分。业务架构不需要考虑诸如我用什么技术开发、我的并发大怎么办、我选择什么样的硬件机器等。

913c7e64-718a-408a-b21a-01f7dffc1133.jpeg

下图也是一个电商系统的产品架构图(也就是业务架构图),可作参考示意:
f662621f-d37a-4cb5-b98f-8b88c7d12806.jpeg

应用架构:

它是对整个系统实现的总体上的架构,需要指出系统的层次、系统开发的原则、系统各个层次的应用服务。

例如、下图将系统分为数据层、服务层、通讯层、展现层,并细分写明每个层次的应用服务。

下图是颗粒度更小的,针对某一小部分的应用架构:

1d84f9da-f18e-48b7-bab9-88bf50331141.jpeg

数据架构:

是一套对存储数据的架构逻辑,它会根据各个系统应用场景、不同时间段的应用场景 ,对数据进行诸如数据异构、读写分离、缓存使用、分布式数据策略等划分。

数据架构主要解决三个问题:

  1. 系统需要什么样的数据;
  2. 如何存储这些数据;
  3. 如何进行数据架构设计。

技术架构:

应用架构本身只关心需要哪些应用系统,哪些平台来满足业务目标的需求,而不会关心在整个构建过程中你需要使用哪些技术。技术架构则是应接应用架构的技术需求,并根据识别的技术需求,进行技术选型,把各个关键技术和技术之间的关系描述清楚。

技术架构解决的问题包括:纯技术层面的分层、开发框架的选择、开发语言的选择、涉及非功能性需求的技术选择。


简单介绍下软件架构中最经典的“4+1视图”

“4+1视图”,分别为场景视图、逻辑视图、物理视图、处理流程视图和开发视图。

逻辑视图

用于描述系统的功能需求,即系统给用户提供哪些服务;以及描述系统软件功能拆解后的组件关系、组件约束和边界,反映系统整体组成与系统如何构建的过程。比如UML中的类图。

下图是springcloud微服务的逻辑视图(仅部分),描述了springcloud中各个功能组件。从这个图中,基本可以对springcloud有一个大颗粒度的了解。

物理视图

开发出的软件系统,最终是要运行在物理或软件环境上。物理环境可能是服务器、PC机、移动终端等物理设备;软件环境可以是虚拟机、容器、进程或线程。部署视图就是对这个部署信息进行描述。在UML中通常由部署图表示。

185348f4-0365-4309-bdf5-5a2fd2ad336b.jpeg

处理视图

处理视图,又称过程视图、运行视图。用于描述系统软件组件之间的通信时序,数据的输入输出。在UML中通常由时序图和流程图表示,如下图所示:

开发视图

开发视图关注软件开发环境下实际模块的组织,反映系统开发实施过程。

一个设计良好的开发视图,应该能够满足以下要求:

  1. 通过逻辑架构元素,能够找到它所有代码和所有的二进制交付件
  2. 每一个代码源文件,都能够找到它所属的逻辑架构元素
  3. 每一个二进制交付件,都能够找到它集成了哪些逻辑架构元素

场景视图

场景视图,即4+1中的1。从前面的图可以看到,4+1中的4个视图都是围绕着场景视图为核心的。它用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计。在UML中通常由用例图表示:

以上5种架构视图,是从不同角度表示一个软件系统的不同特征


架构图和4+1视图是从两个不同的维度来说架构的,可以简单的认为(不过这样认为是有问题的) 应用架构图和业务架构图等等是针对不同的人员群体,比如管理人员或者决策者等一些不是太懂技术的都是看的业务架构图,技术架构是纯给技术人员看的,而应用架构主要给CTO等之类的看的(当然也看业务架构),区别在于主要的面向群体不同,有时候有些叫法可能不一样。 而且4+1视图与单独区分出来的架构图说法有时候是混在一起的,比如部署架构图,物理架构图等 就是基于4+1视图的物理视图出来的。当然,业务架构图、应用架构图、数据架构图、技术架构图等等合起来才是一个完整的架构蓝图。

如何画架构图?

画架构图可以分以下几步:

  1. 搞清楚要画的架构图的类型;
  2. 确认架构图中的关键要素(比如产品、技术、服务);
  3. 梳理关键要素之间的关联:包含、支撑、同级并列等;
  4. 输出关联关系清晰的架构图。

一、从具象到抽象

架构图表达的就是事物的结构,大多数领域都可以用具象的方式来呈现,比如下图中的人体器官系统图就显得一目了然。但是IT行业几乎没有具象表达的可能性。不像人体器官系统的直观视觉表达那么有效。所以,设计IT系统架构图,第一步就是要完成合理的“抽象”。

1、恰当的颗粒度

抽象最重要的目标就是决定用什么样的颗粒度来列出事物,而不是巨细无遗。这完全取决于架构图的受众和想要表达的系统的规模。越是非技术受众,越要降低颗粒度,越是大型系统,也要降低颗粒度。虽然我们可以在一个架构图中表达事物的层次,但是平面空间总是有限,就像人体器官分布图没办法呈现细胞组织一个道理。

2、通用和规范的命名

确定了抽象层级,在架构图上就要列出需要表达的事物对象。因为缺乏具象的参照,所有的对象表达唯一途径就是文字标签,最多加上符号图标。这就要求制作者必须使用规范或约定俗成的事物命名,否则读者将无法理解。比如,上例中的操作系统架构图,File System绝对不能写成Doc System,因为只有前者是一个规范名称。

很多IT产品设计中会有自己的独特命名组件,甚至这些命名没有任何的语义元素。这些命名在架构图中出现必须要附加实际含义的说明,否则没有读者会知道“海豚系统“是什么意思。当然IT行业中也会出现强势垄断产品,以自己的独特命名确定行业标准,以至于你不知道都不好意思,这种情况当然另当别论。

当使用图标和符号来表达事物对象时,要注意选择规范和识别性强的图形。在一些非常专业的领域,比如电子电气架构图,甚至有对各种组件规范的标准定义。即使有含义准确的符号图标,依然建议制作时附加上文本标签,可以让架构图的读者更明晰地理解。相反,没有图形,只有文字的架构图是完全可以接受的。

3、明确抽象对象之间的关系

在抽象系统架构中,各种对象之间的关系只有三种:包含、并列和联结。比如操作系统包含用户模式和内核模式,用户模式和内核模式是并列的,客户端和服务器是通过TCP/IP协议连接(联结)的。无论系统多么庞大和复杂,对象和对象之间的关系就这么几种。而且,在平面空间中,无论怎么演绎,也只能有效地表达这些有限的关系类型。

二、确定架构图形态

1、三种基本形态

鉴于抽象对象之间有限的关系种类,架构图无非有三种形态:

  • 分叉模式(Branching Model),表达事物的分支
  • 网络模式(Network Model),表达事物的联结
  • 通过空间组合,组合以上两种模式

所以,不用纠结要表达的系统有多么复杂。只要认知到系统对象之间的内在关系性质,就不难选择一个基本形态来表达。如果空间允许,也完全可以综合运用分叉和网络一体化表达更完整的系统。

2、确定边界

架构图既然表达的是一个系统,那么这个系统必然有一个明确的边界。计算机网络的边界是与上一级网络的连接网关,一个SaaS软件的边界是主体应用、其依赖的开源组件和网络服务。确定边界后的系统才能盘点内部所包含的组件对象层次和数量。一般而言,一个系统架构图不宜超过30-40个对象,超出这个限制则可能要考虑提高抽象层次;反过来,如果一个架构图只包含寥寥数个对象,那也犯不着劳累追求架构图的高要求,转而用一些简单的框图就能够搞定,这时候,我们应该降低抽象层次,以反映出更具体和详细的架构信息。

超出这个既定边界的对象理论上无需绘制,但有时候我们为了表达与外部系统的连接关系,会用次要色彩在边界线外绘制部分系统边界外的对象。这有点像某个行政区的地图中会把连接其他区域的交通线上标上“通往…”。

3、确定主次

假设一个系统包含10个子系统,如果往下钻取一级,就有可能要表达上百个对象。这时候如果轻重不分,则可能让最终的产出过于繁复。我们可以依据策略的需要进行选择,只分支展开系统的一部分,而让其他部分粗略表达。不必要的细节展现过多,反而让看的人无法识别出重点。因为每一张架构图都有明确的沟通目标和上下文,这样主次有别的表达不仅是合理的,而且很多时候是必要的。

4、确定空间位置

空间位置是架构图设计的关键。我们要按照易于理解的逻辑关系,把抽象对象放到平面上的合理位置,人的视觉焦点就可以被空间关系有效引导,从而更直观地理解系统结构和原理。

在确定空间位置时,根据不同的架构图基本形态,有几个重要准则:

(1)分叉模式下,要注意均衡利用空间,避免画面一部分过度稀疏,一部分过度稠密。为了实现这一点,需要借助矩形框、连接分叉线的帮助。如果遇到了实在窘迫的空间限制,也可以利用Call-out的局部放大设计来利用独立的空间详解一个特定模块。

(2)分叉模式下,依然要关注逻辑次序。在树状分叉下,虽然每个分支的地位都是均等的,但是并不意味着可以随便决定次序。设计者依然可以根据同级别对象的主次关系、先后关系来决定排序。

(3)网络模式下,根据被联结组件的性质决定南北向。在IT架构设计中,一般把底层的基础技术组件定义为北向(North Bound),放在架构图的下方,把高层级的应用组件定义为南向(South Bound),放在架构图的上方。把中间的各个技术组件依照高低位性质依序排列联结。这就是我们经常看到的分层技术架构图。

典型的分层技术架构图

注意:南北向也不是绝对的上下结构。只要条理清晰,左右结构也同样可以表达这些递进的层次。例如下图:

(4)网络模式下,可以将具备联结关系的对象放在彼此邻近的位置上,以方便创建清晰的连接线。比如下图,在同一级别的层次上,可能存在彼此需要联结的组件。这时候,是放在左边,中间,还是右边就有一定的讲究了,否则就无法绘制出简洁的连线。

d00a9b03-adb6-4d8e-a9da-4fe6c72a7e1a.jpeg
三、视觉设计


可以用一些视觉设计技巧

1、制定统一的设计范式

在抽象架构图中,会充满组件框和连接线。无论使用何种绘图工具,先要定义出标准的图形样式、色彩、文本标签的字体、字号、连接线样式等。在真个架构图中,应该依据这些定义好的基本样式来重复对象,而不是想到哪里,画到哪里。很多架构图看起来充满了完全不同的组件样式,看起来凌乱不堪,就是因为缺乏了这个范式的约束。

架构图上的同性质组件只能使用单一的图形样式,字体、字号和粗细都要统一,影响观感一致性的还包括字距、文本框边距(margin)、文本框间距(padding)、文本对齐模式等细节。这些样式属性,稍微有一些不一样,放到一张架构图中就会一眼看出来,非常不舒服。

原则上,我们应该最少的样式差异,不要使用过多数量的色彩,不要为了区别事物,把什么属性都重新定义一边。两类事物之间,靠色彩能区别,靠标签字号也能区别,甚至仅仅靠色彩明度就能区别,不需要同时变化两个属性。

2、使用图形符号辅助表达

利用计算机领域约定俗成的符号、专有图标或三维图形来增强架构图设计,可以让看的人更轻松地阅读架构图。而且,精致的画面可以给读者传递系统的精密度和高质量感受。

  • 用具象物缩略图来表达系统组件
  • 用三维图形来表达组件层次

3、使用附注、旁注来平衡空间

在技术性较强的架构图中,有时候我们必须提供一些重要的技术细节,但是架构图的空间可能不允许我们这么做。此时,可以使用编号附注或者Call-out连线的旁注来实现。

  • 使用附注来说明系统组件,一般在有具象特征的架构图中使用
1
2
3
ruby复制代码有部分内容参考下文(感谢):
https://www.zhihu.com/question/30106392/answer/2538839287
https://www.zhihu.com/org/yi-tu-tu-shi

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一个看似很简单的redis需求,我问了农行和腾讯大佬也没想出

发表于 2024-01-12

欢迎关注,微信公众号/知乎/稀土掘金/小红书都叫 ——【浣熊say】

专注输出国央企招聘、Java开发、物联网和数字孪生相关优质内容,关注公众号有小福利哦~

一个看似很简单的redis需求,我问了农行和腾讯大佬也没想出更优秀的解决方案!

来自领导的需求?

最近小浣熊领导又作妖了,给了我一个乍一听很简单,但是我想了半个小时也没想出更好解决方案的需求。

背景是这样的,我们有一个MQTT集群,其中有一个topic是某类IOT数据,也就是某个设备的“胎压”,这个数据需要实时通过websock推送到前端,并且有个参数需要计算两次传递值的差值。

本来这块儿当时是让外包兄弟做的,领导也没把需求定义清楚,外包兄弟很直接,直接给我弄了上一秒的“胎压”和这一秒的“胎压”存在redis里面,算这个差值的时候直接取上一秒的“胎压”和本次的“胎压”一算就完事儿了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码    private void redisCacheTirePressure(String data) {

try {
// generate cache key
String redisKey = RopewayUtils.GetRopewayCaheKey(MongoConstants.TIRE_PRESSURE_LIST_CACHEKEY);
// put new value
redisTemplate.opsForList().rightPush(redisKey, data);

// get new total size
Long size = redisTemplate.opsForList().size(redisKey);

if (size.intValue() > 2) {
// remove other record(s), just keep two records
redisTemplate.opsForList().trim(redisKey, size - 2, size - 1);
}

} catch (Exception e) {
// e.printStackTrace();
}

}

好家伙,我真想反手给外包兄弟一个赞👍,代码注释还是英文,很有水平。但是,领导最近看了看,发现不对啊,为啥这个“胎压”的差值一直没变化 啊,半个月过去了气都漏完了,胎压差值还是0,肯定是小浣熊又写bug了。

我特么是诚惶诚恐,立马停止摸鱼,开始想起了解决方案。

农行、腾讯大佬怎么说?

想了半天,小浣熊想了个效率不怎么高的方案,就是把半个小时的历史数据全部缓存在redis里面,然后当这一秒的数据到了,我就去redis里面拿半小时前的数据,然后计算这个差值,返回给前端。

但是,这样的设计毕竟很暴力,也很不优雅,因为我们的IOT数据是1s一次的,对于一个IOT场景来说redis需要存储的数据就是30x60=1800条数据。虽然看起来还好,但是未来IOT场景增大的话,这段代码就可能存在性能问题,并且如果需求改了,计算1个小时或者24个小时甚至一个月的差值,那这个方案又当如何应对?所以小浣熊得想办法优化,就是有没有那种只存2条数据的办法?

于是自己想不出方案就找外援吧,这是就想起了自己还在腾讯呆着的研究生室友,先问问他知道怎么做不?首先咱们把需求给他描述清楚咯,入下:

image.png

腾讯大佬怎么说

image.png
我和我的室友合计了一下,好像也没想出啥好的方案~ 都在感叹自己是菜鸡,当然我更菜一点儿。

农行大佬怎么说

image.png
农行大佬的角度比较清奇,不然不能改变方案,那就改变领导,不得不说也是一个好的思路~

小浣熊的菜鸡解决方案

没办法,只有蛮干了,那我们只有在redis当中把前半个小时的历史数据缓存下来,实时删除多余的部分数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
scss复制代码    private void redisCacheTirePressure(String data,int cacheTimeMinutes) {
try {

String redisKey = RopewayUtils.GetRopewayCaheKey(MongoConstants.TIRE_PRESSURE_ZSET_CACHEKEY);

long currentTime = System.currentTimeMillis();

//存储当前的数据到redis的zset当中,score为当前时间,zset默认排序是按照升序排列的
redisTemplate.opsForZSet().add(redisKey,data,currentTime);

//从前到后遍历zset,找到和currentTime差值为cacheTimeMinutes的节点
Set<Object> expiredMembers = redisTemplate.opsForZSet().rangeByScore(redisKey, Double.NEGATIVE_INFINITY, currentTime - cacheTimeMinutes * 60 * 1000);

//删除超过cacheTimeMinutes的节点,保证在zset的首尾节点时差为30分钟
for (Object expiredMember : expiredMembers) {

redisTemplate.opsForZSet().remove(redisKey, expiredMember);

}

} catch (Exception e) {

logger.error(e.getMessage());

}
}

计算实时差值数据的时候,只需要取redis中zset的第一个和最后一个元素进行计算就可以咯,需求就这样完成了!

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码        
String redisKey = RopewayUtils.GetRopewayCaheKey(MongoConstants.TIRE_PRESSURE_ZSET_CACHEKEY);

Set<String> records = redisTemplate.opsForZSet().range(redisKey,0,-1);

List<String> recordsList = new ArrayList<>(records);

if (recordsList != null && records.size() > 1) {
String prev = recordsList.get(0);
String next = recordsList.get(recordsList.size() - 1);
//其它的操作
}

最后

当然,我这个方案漏洞百出,首先当项目规模扩大之后,就算一个IOT设备那么数据也有30x60=1800条,如果设备数据不断扩张,极有可能干爆redis。并且,假设当领导改成计算一个月的差值的话,那么数据量将会达到60x60x24x30 = 259万条的zset数据,redis爆之无疑。

不知道掘金的各位大佬有没有其它的好方案,帮帮我这个菜鸡想出更好的方案,适应当项目规模扩大的时候或者领导改需求时的场景,感激不尽~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在Java中一种更快的反射调用方式

发表于 2024-01-12

背景

在使用Java进行开发时,我们会不可避免的使用到大量的反射操作,比如Spring Boot会在接收到HTTP请求时,利用反射Controller调用接口中的对应方法,或是Jackson框架使用反射来解析json中的数据给对应字段进行赋值,我们可以编写一个简单的JMH测试来评估一下通过反射调用来创建对象的性能,与直接调用对象构造方法之间的差距:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
java复制代码
@BenchmarkMode(value = Mode.AverageTime)
@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public abstract class JmhTest {
public static void runTest(Class<?> launchClass) throws RunnerException {
Options options = new OptionsBuilder().include(launchClass.getSimpleName()).build();
new Runner(options).run();
}
}

package cn.zorcc.common.jmh;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.RunnerException;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectionTest extends JmhTest {
@Param({"10", "100", "1000", "10000"})
private int size;

static class Test {
private int integer;

public int getInteger() {
return integer;
}

public void setInteger(int integer) {
this.integer = integer;
}
}

@Benchmark
public void testDirectCall(Blackhole bh) {
for(int i = 0; i < size; i++) {
Test test = new Test();
bh.consume(test);
test.setInteger(i);
bh.consume(test.getInteger());
}
}

@Benchmark
public void testNormalReflection(Blackhole bh) {
try{
Constructor<Test> constructor = Test.class.getDeclaredConstructor();
Method setter = Test.class.getDeclaredMethod("setInteger", int.class);
Method getter = Test.class.getDeclaredMethod("getInteger");
for(int i = 0; i < size; i++) {
Test test = constructor.newInstance();
bh.consume(test);
setter.invoke(test, i);
int integer = (int) getter.invoke(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}


public static void main(String[] args) throws RunnerException {
runTest(ReflectionTest.class);
}
}

在Test类中,具有一个简单的int类型的变量,我们分别测试直接调用构造方法,赋值然后取值,以及使用Constructor和Method进行普通反射调用之间的性能对比,注意一定要将构造出来的对象使用Blackhole.consume()方法给吃掉,这样JVM才不会把没有使用到的变量给直接的优化掉,得出错误的测试结果,以上代码在笔者的机器上运行的结果如下:

1
2
3
4
5
6
7
8
9
10
plaintext复制代码
Benchmark (size) Mode Cnt Score Error Units
ReflectionTest.testDirectCall 10 avgt 50 10.584 ± 0.141 ns/op
ReflectionTest.testDirectCall 100 avgt 50 108.301 ± 1.129 ns/op
ReflectionTest.testDirectCall 1000 avgt 50 1068.026 ± 12.312 ns/op
ReflectionTest.testDirectCall 10000 avgt 50 10660.596 ± 148.673 ns/op
ReflectionTest.testNormalReflection 10 avgt 50 145.483 ± 1.300 ns/op
ReflectionTest.testNormalReflection 100 avgt 50 1131.994 ± 19.586 ns/op
ReflectionTest.testNormalReflection 1000 avgt 50 13461.067 ± 130.624 ns/op
ReflectionTest.testNormalReflection 10000 avgt 50 148811.318 ± 5766.679 ns/op

可以看到,使用反射的性能比起直接调用来讲有非常大的差距,尤其是在这种极其简单的对象创建场景中,但是使用反射是很多情况下我们不得不采用的一个做法,那么我们有没有什么办法来尽可能优化一下反射调用的性能呢?

先让我们试一下MethodHandle提供的方法调用模型,MethodHandle是自JDK7版本后开始推出的,用于替换旧反射调用的新方式,相比起原有的反射调用,提供了更多的交互方式,并且具备对Java方法调用和Native方法调用一致的模型,我们可以简单的创建一个用例进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
@Benchmark
public void testMethodHandleReflection(Blackhole bh) {
try{
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType constructorType = MethodType.methodType(void.class);
MethodHandle constructorHandle = lookup.findConstructor(Test.class, constructorType);
MethodHandle iSetter = lookup.findSetter(Test.class, "integer", int.class);
MethodHandle iGetter = lookup.findGetter(Test.class, "integer", int.class);
for(int i = 0; i < size; i++) {
Test test = (Test) constructorHandle.invokeExact();
bh.consume(test);
iSetter.invokeExact(test, i);
int integer = (int) iGetter.invokeExact(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}

实测的结果则更加的不尽人意:

1
2
3
4
5
plaintext复制代码
ReflectionTest.testMethodHandleReflection 10 avgt 50 1346.515 ± 17.347 ns/op
ReflectionTest.testMethodHandleReflection 100 avgt 50 2355.083 ± 37.358 ns/op
ReflectionTest.testMethodHandleReflection 1000 avgt 50 456694.572 ± 31415.118 ns/op
ReflectionTest.testMethodHandleReflection 10000 avgt 50 982008.110 ± 46807.572 ns/op

可以看到,使用MethodHandle与使用普通反射之间的性能差距,就和普通反射与直接调用之间的差距一样大,事实上在JDK18以后,根据# JEP 416: Reimplement Core Reflection with Method Handles 使用java.lang.reflect和java.lang.invoke的相关API已经进行了相应的底层重构,转而使用MethodHandle进行实现,很明显,在使用java.lang.reflect和java.lang.invoke中的方法时,与直接使用MethodHandle相比,具备了更多的优化工作,根据官方的说法,在使用MethodHandle时因将字段尽可能定义为static final,这样JVM可以将其进行常量折叠,从而实现巨大的性能提升,让我们修改一下以上的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码
private static final MethodHandle constructorHandle;
private static final MethodHandle iSetter;
private static final MethodHandle iGetter;
static {
try{
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType constructorType = MethodType.methodType(void.class);
constructorHandle = lookup.findConstructor(Test.class, constructorType);
iSetter = lookup.findSetter(Test.class, "integer", int.class);
iGetter = lookup.findGetter(Test.class, "integer", int.class);
}catch (Throwable e) {
throw new UnknownError();
}
}

@Benchmark
public void testMethodHandleReflection(Blackhole bh) {
try{
for(int i = 0; i < size; i++) {
Test test = (Test) constructorHandle.invokeExact();
bh.consume(test);
iSetter.invokeExact(test, i);
int integer = (int) iGetter.invokeExact(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}

得到了如下的数据:

1
2
3
4
5
plaintext复制代码
ReflectionTest.testMethodHandleReflection 10 avgt 50 9.825 ± 0.084 ns/op
ReflectionTest.testMethodHandleReflection 100 avgt 50 99.174 ± 1.128 ns/op
ReflectionTest.testMethodHandleReflection 1000 avgt 50 997.094 ± 11.961 ns/op
ReflectionTest.testMethodHandleReflection 10000 avgt 50 10212.014 ± 215.662 ns/op

突然之间,我们的反射调用和直接调用的性能已经完全一致了,那么这是不是意味着,我们想要的功能已经完全实现了呢?事实上并未如此,如果我们必须在static final中指定需要使用到的反射字段,那么就相当于损失了绝大多数的灵活性,在实际操作中可行性并不高。

同样的,我们可以试一试,将直接使用java.lang.reflect和java.lang.invoke的函数所需的对象先构建并缓存在本地,再测试一下其对应的性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码
private Constructor<Test> c;
private Method setter;
private Method getter;

@Setup
public void setup() {
try{
this.c = Test.class.getDeclaredConstructor();
this.setter = Test.class.getDeclaredMethod("setInteger", int.class);
this.getter = Test.class.getDeclaredMethod("getInteger");
}catch (Throwable e) {
throw new UnknownError();
}
}

@Benchmark
public void testNormalReflection(Blackhole bh) {
try{
for(int i = 0; i < size; i++) {
Test test = c.newInstance();
bh.consume(test);
setter.invoke(test, i);
int integer = (int) getter.invoke(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}

与在测试MethodHandle时我们将需要初始化的变量定义为static final不同,此处我们直接将其定义为private变量,在JMH框架中提供的@Setup函数中进行初始化,更贴合的模拟我们在运行时进行创建的行为,测试得到的结果如下:

1
2
3
4
5
plaintext复制代码
ReflectionTest.testNormalReflection 10 avgt 50 152.242 ± 5.625 ns/op
ReflectionTest.testNormalReflection 100 avgt 50 1495.302 ± 21.467 ns/op
ReflectionTest.testNormalReflection 1000 avgt 50 16917.774 ± 420.810 ns/op
ReflectionTest.testNormalReflection 10000 avgt 50 143252.377 ± 2150.908 ns/op

可以看到,使用普通反射的方式,无论是每次都获取新的Constructor或Method对象进行创建,还是通过提前缓存的形式进行加载,性能表现是相似的,这也使得通用的反射调用方式在各类通用场景下都能够具备比较不错的表现。

鉴于我们之前的这些测试结果,如果想要进一步的提升反射的性能,只能考虑使用类生成的方式,在编译期创建出MethodHandle的静态变量,让JVM帮我们去自动内联,当然,类生成的方式一定可以拥有非常不错的性能,但是使用ByteBuddy或Asm框架进行类生成的代码相对而言过于繁琐,目前[# JEP 457: Class-File API (Preview)].(openjdk.org/jeps/457) 特性正处于preview阶段,可以帮助我们更加简化的在JVM中进行类生成,但是目前我们还无法对其进行使用。

解决方案

Lambda表达式贯穿了我们日常的开发中的所有角落,且Lambda表达式本身的性能不会差,否则JDK内部绝对不会如此大量的使用它,Lambda表达式的生成方式也并不复杂,其背后的核心方法是通过LambdaMetafactory.metafactory()方法生成对应的方法调用,我们可是实现以下的代码来完成对应构造函数,getter方法和setter方法向Lambda函数的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码
private Supplier<Test> constructor;
private BiConsumer<Object, Object> setConsumer;
private Function<Test, Integer> getFunction;

@Setup
public void setup() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(ReflectionTest.class, MethodHandles.lookup());
this.constructor = lambdaGenerateConstructor(lookup);
this.setConsumer = lambdaGenerateSetter(lookup);
this.getFunction = lambdaGenerateGetter(lookup);
}

@SuppressWarnings("unchecked")
private Supplier<Test> lambdaGenerateConstructor(MethodHandles.Lookup lookup) throws Throwable {
MethodHandle cmh = lookup.findConstructor(Test.class, MethodType.methodType(void.class));
CallSite c1 = LambdaMetafactory.metafactory(lookup,
"get",
MethodType.methodType(Supplier.class),
MethodType.methodType(Object.class), cmh, MethodType.methodType(Test.class));
return (Supplier<Test>) c1.getTarget().invokeExact();
}

@SuppressWarnings("unchecked")
private BiConsumer<Object, Object> lambdaGenerateSetter(MethodHandles.Lookup lookup) throws Throwable {
MethodHandle setHandle = lookup.findVirtual(Test.class, "setInteger", MethodType.methodType(void.class, int.class));
CallSite callSite = LambdaMetafactory.metafactory(lookup,
"accept",
MethodType.methodType(BiConsumer.class),
MethodType.methodType(void.class, Object.class, Object.class),
setHandle,
MethodType.methodType(void.class, Test.class, Integer.class));
return (BiConsumer<Object, Object>) callSite.getTarget().invokeExact();
}

@SuppressWarnings("unchecked")
private Function<Test, Integer> lambdaGenerateGetter(MethodHandles.Lookup lookup) throws Throwable {
MethodHandle getHandle = lookup.findVirtual(Test.class, "getInteger", MethodType.methodType(int.class));
CallSite getSite = LambdaMetafactory.metafactory(
lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
getHandle,
MethodType.methodType(Integer.class, Test.class)
);
return (Function<Test, Integer>) getSite.getTarget().invokeExact();
}

@Benchmark
public void testLambda(Blackhole bh) {
for(int i = 0; i < size; i++) {
Test test = constructor.get();
bh.consume(test);
setConsumer.accept(test, i);
int integer = getFunction.apply(test);
bh.consume(integer);
}
}

@Benchmark
public void testLambdaGeneration(Blackhole bh) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(ReflectionTest.class, MethodHandles.lookup());
bh.consume(lambdaGenerateConstructor(lookup));
bh.consume(lambdaGenerateSetter(lookup));
bh.consume(lambdaGenerateGetter(lookup));
}

测试分为两个步骤,一个是测试Lambda表达式的生成性能,一个是测试Lambda表达式的运行性能,这两个指标对我们来说都非常的重要,得到的结果如下:

1
2
plaintext复制代码
ReflectionTest.testLambdaGeneration 10000 avgt 50 92486.909 ± 62638.147 ns/op
1
2
3
4
5
plaintext复制代码Benchmark                  (size)  Mode  Cnt      Score     Error  Units
ReflectionTest.testLambda 10 avgt 50 10.720 ± 0.087 ns/op
ReflectionTest.testLambda 100 avgt 50 105.001 ± 1.312 ns/op
ReflectionTest.testLambda 1000 avgt 50 1020.406 ± 9.990 ns/op
ReflectionTest.testLambda 10000 avgt 50 10198.842 ± 143.259 ns/op

可以看到,通过模拟Lambda表达式生成的方式,调用构造函数以及get和set方法的性能,与直接调用是几乎完全一致的,这也就达成了我们想要的效果,但是Lambda生成的性能非常不容乐观,与直接使用箭头函数进行生成的性能有着天壤之别,好在如果Lambda表达式没有捕获任何的外部变量,比如我们在示例中调用的get和set方法,那么生成的方法是可以被缓存起来重复使用的,如果使用的基数本身比较大,在多次调用的开销权衡中,初始化的开销就可以被忽略不计。

小结

本文介绍了一种在Java中的新的反射调用方式,即使用类似于Lambda表达式的生成的方式进行反射,可以将一些简单的方法,例如get和set方法,直接转化为相应的Lambda表达式来调用,虽然可以做到和直接调用一致的性能,但是该方法的生成开销比较大,需要在频繁调用的场景中进行缓存,才能起到比较好的效果。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nacos 高级详解:提升你的开发和部署效率 Nacos 高

发表于 2024-01-12

Nacos 高级

前言

欢迎来到本篇文章,今天我们将探索 Nacos 高级版,这是一个旨在提升开发和部署效率的强大工具。作为 Nacos 的高级版本,它提供了更多功能和特性,帮助开发人员和运维团队更好地管理和部署他们的应用程序和服务。无论你是一名开发人员、系统管理员还是 DevOps 工程师,Nacos 高级版都将成为你的得力助手。让我们深入了解一下它的优势和特点吧。

正文

一 、服务集群

1 需求

  • 服务提供者搭建集群

1655452364194.png

  • 服务调用者,依次显示集群中各服务的信息

image-20210415083941141.png

image-20210415083949074.png

2 搭建

  • 1)修改服务提供方的controller,打印服务端端口号

1655221836544.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.czxy.controller;

import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
* @author 桐叔
* @email liangtong@itcast.cn
*/
@RestController
public class EchoController {

@Resource
private HttpServletRequest request;

@RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
public String echo(@PathVariable String string) {
int serverPort = request.getServerPort();
return "Hello Nacos Discovery " + string + ":" + serverPort;
}
}
  • 2)编写yml配置

1655221950052.png

1
2
3
4
5
6
7
8
9
10
11
yml复制代码#端口号
server:
port: 8170

spring:
application:
name: service-provider #服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos服务地址
1
2
3
4
5
6
7
8
9
10
11
yml复制代码#端口号
server:
port: 8270

spring:
application:
name: service-provider #服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos服务地址
  • 3)配置idea启动项
1
ini复制代码-Dspring.profiles.active=8170

image-20210415084820667.png

image-20210415084839900.png

3 测试

  • 启动3个服务(2个服务提供,1个服务消费)

image-20210415085003936.png

  • 查看nacos控制台

1655222274072.png

二、 加载配置文件顺序2

  • 对 3.4.5章节/第4步内容进行详解
  • 加载配置文件的顺序(第4步详解)

1 nacos 配置 DataId 介绍

  • nacos 提供了3种方式,配置dataId的加载顺序

A: 共享配置:(过时),使用 shared-configs 替代

spring.cloud.nacos.config.shared-dataids

spring.cloud.nacos.config.refreshable-dataids

B: 加载多配置:(过时),使用 extension-configs 替代

spring.cloud.nacos.config.ext-config[n]
C: 内部规则拼接:

spring.cloud.nacos.config.prefix

spring.cloud.nacos.config.file-extension

spring.cloud.nacos.config.group

2 配置 yml 文件中的 DataId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
yml复制代码spring:
application:
name: config-service # 服务名
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # nacos 服务地址
# shared-dataids: test1.yml # 4.1 共享配置 (已过时)
# refreshable-dataids: test1.yml
shared-configs: # 4.1 共享配置【最新】
- data-id: test1-1.yml
group: DEFAULT_GROUP
refresh: true
- data-id: test1-2.yml
group: DEFAULT_GROUP
refresh: true
# ext-config: # 4.2 配置多个 (已过时)
# - data-id: test2-1.yml
# group: DEFAULT_GROUP
# refresh: true
# - data-id: test2-2.yml
# group: DEFAULT_GROUP
# refresh: true
extension-configs: # 4.2 配置多个 【最新】
- data-id: test2-1.yml
group: DEFAULT_GROUP
refresh: true
- data-id: test2-2.yml
group: DEFAULT_GROUP
refresh: true
prefix: ${spring.application.name} # 4.3 data ID的前缀,默认服务名
file-extension: yaml # data ID的后缀:config-service.yaml
group: DEFAULT_GROUP # 组名
discovery:
server-addr: 127.0.0.1:8848 #nacos服务地址

3 配置 console中的DataId

  • nacos控制台配置

image-20210415104915218.png

4 测试

  • 后面加载的dataId将覆盖前面加载的dataId设置的内容
  • 查看日志

image-20210415105052039.png

1
2
bash复制代码Located property source: [
BootstrapPropertySource {name='bootstrapProperties-test3-demo.yaml'}, BootstrapPropertySource {name='bootstrapProperties-test3.yaml'}, BootstrapPropertySource {name='bootstrapProperties-test2-2.yml'}, BootstrapPropertySource {name='bootstrapProperties-test2-1.yml'}, BootstrapPropertySource {name='bootstrapProperties-test1.yml'}]

三、 多环境配置

1 介绍

  • 在Nacos为不同的环境(开发、测试、生产等)中,提供了多个不同管理级别的概念,包括:Data ID、Group、Namespace。
概念 描述
Data ID 数据唯一标识,可理解为Spring Cloud应用的配置文件名
Group 用来对Data ID做集合管理,相当于小分类
Namespace 用于进行租户粒度的配置隔离。相当于大分类

2 配置介绍

  • 组group配置
1
yml复制代码spring.cloud.nacos.config.group=				#组名称
  • 命名空间 namespace配置 ==注意:namespace的ID==
1
yml复制代码spring.cloud.nacos.config.namespace=			#namespace的ID

1655435343460.png

3 配置内容

  • 在nacos 控制台配置namespace

1655435426654.png

  • 在nacos控制台显示namespace

1655435409120.png

四、 数据持久化

  • 在单机模式时nacos默认使用嵌入式数据库实现数据存储,0.7版本后增加了mysql存储数据。

1 初始化数据库

  • 在conf目录下,提供了nacos-mysql.sql SQL语句,进行数据库的初始化
+ 要求:5.6+ mysql
+ 注意:如果使用mysql 5.5,需要修改sql语句


![1655436482630.png](https://gitee.com/songjianzaina/juejin_p2/raw/master/img/324cef9fae964bad617696c80a664ce48b5a47b2226ead8f05d4997cfcd29517)
  • 提供的SQL语句没有创建database,手动创建nacos_config

image-20210415162504004

2 开启mysql存储

  • conf目录下,提供了application.properties可以修改数据库配置信息

image-20210415162504004.png

1
2
3
4
5
6
7
8
9
10
properties复制代码### If use MySQL as datasource:
spring.datasource.platform=mysql

### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config_2_1?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=1234
  • 配合完成后,重启nacos

3 测试

  • 添加配置信息

image-20210415164446314.png

  • 检查数据库存储

image-20210415164511747.png

五、 Nacos集群搭建

1 概述

  • 3个或3个以上Nacos节点才能构成集群
  • 配置数据源
+ 使用内置数据源



1
cmd复制代码startup.cmd -p embedded
+ 使用外置数据源(MySQL,参考4.4.2)
  • 在一台主机配置多个节点的端口号==不能连续==。
+ 例如:8841/8842/8843 不可用
+ 例如:8841/8843/8845 可用

2 配置步骤

1655309574502.png

  • 节点1:配置Nacos8841
    1. 配置数据源
    2. 修改端口号:8841
    3. 配置集群配置文件
    4. 启动服务:startup.cmd
  • 节点2:复制Nacos8843
    • 修改端口号:8843
    • 启动服务:startup.cmd
  • 节点3:复制Nacos8845
    • 修改端口号:8845
    • 启动服务:startup.cmd

3 配置详情

1)配置节点1

  1. 拷贝nacos,并重命名 nacos-2.1.0-8841
  2. 配置数据源

1655309637618.png
3. 修改端口号:8841

1655309662031.png
4. 配置集群配置文件:拷贝conf/cluster.conf.example,重名为cluster.conf

1655309699463.png
5. 启动服务:startup.cmd

1655309748459.png
6. 成功启动

1655309809762.png

2)配置节点2

  • 复制节点nacos-2.1.0-8841,并重命名nacos-2.1.0-8843
  • 修改端口号

1655309928803.png

  • 启动服务

1655309958005.png

3)配置节点3

  • 复制节点nacos-2.1.0-8841,并重命名nacos-2.1.0-8845
  • 修改端口号

1655310040083.png

  • 启动服务

1655310010135.png

4)配置成功

1655310082877.png

4 常见错误

1)db.num is null

  • 错误提示:db.num is null
  • 原因:没有配置数据库

1655224799207.png

2) unable to start embedded tomcat

  • 错误提示:unable to start embedded tomcat
  • 原因1:没有编写集群配置文件

1655224825852.png

  • 原因2:安装目录有中文

3)内存不足

  • 提示信息:
  • 原因:内存不足,修改分配内存大小

1655224914532.png

4) Cannot determine JNI library name for ARCH=’x86’ OS=’windows 10’ name=’rocksdb’

  • 提示信息:Cannot determine JNI library name for ARCH=’x86’ OS=’windows 10’ name=’rocksdb’
  • 原因:nacos与jdk 系统位数(64位和32位,)不一致

5) JNI相关错误

  • 提示信息: C:\Users\Administrator\AppData\Local\Temp\/librocksdbjni1411968517689619912.dll: Can't find dependent libraries

结语

原因:JAVA_HOME配置的jdk安装目录,而不是jre安装目录
在本文中,我们深入探讨了 Nacos 高级版的功能和特性,展示了它如何提升开发和部署效率,为开发人员和运维团队带来更好的体验。无论是在微服务架构中使用、进行多环境部署还是进行灰度发布,Nacos 高级版都是一个强大而可靠的选择。如果你希望提升你的应用程序和服务的管理水平,不妨考虑尝试 Nacos 高级版吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

开发同学如何学习Kubernetes? 想清楚为什么要学 用

发表于 2024-01-11

学习前首先需要有一个自己的环境

自己搭整个集群进行学习确实会经常遇到资源不足的情况,我几年前最开始学的时候也是被搭环境的第一步。不过后来出了minikube ,我自己的机器是 16g的2016版本的mbp,虽然可以跑起来,但是相当的费劲,后来在公司白嫖了测试机器,如果你是在职开发的话可以看看有没有可以白嫖的物理机,狠狠的当一把工贼。

我不太建议直接买云产商的k8s,这样就少了自己搭建 k8s集群的过程,如果已经搭建了我也觉得可以在自己搭建最的k8s去玩,这样能接触到更多原生的概念,而云厂商会对一些概念进行封装,这样其实不太利于你的学习。

学习相关的资料和实操,下面是我总结出来的一些经验:

想清楚为什么要学

首先kubernetes技术解决什么问题?为什么别的同类技术做不到?为什么是这样解决的? 有没有更好的方式?可以通过 了解k8s 是什么:kubernetes.io/zh-cn/docs/…

技术付费点基本体现在两个地方,一个是,能帮别人“挣钱”的地方;另一个是,能帮别人“省钱”的地方。

在学k8s之前我们最主要的肯定是要了解他为我们解决了什么问题,这是在开始前必须要阅读的。

最主要的解决了服务自动部署回滚、自我修复、水平扩展、服务发现和负载均衡的问题,这里也是挑出来了我们平时研发过程中最频繁使用的几个功能。

部署演进过程.png

原先如果没有 k8s 帮我们解决服务部署的话,那我们需要通过脚本去重启服务并且kill掉原有的服务,并且在流量转发上也需要我们自己通过Nginx或者其他网关来实现,如果要进行水平扩展的话也需要脚本来实现,在线上操作这些内容对于运维人员和开发人员的要求来说都是非常高的,而且相应的我们也需要补充开发许多脚本来实现我们部署回滚、自我修复、水平扩展等功能。

所以 k8s 在很大程度是上为我们去省钱,省下了开发和运维人员的时间,让我们有更多精力去建设业务系统的功能和可用性;并且由于资源的虚拟化,我们可以在相同的物理机上部署更多的服务,从而节省了很大一笔购买服务器的资金。

至于k8s 是否能帮我们挣钱,我觉得也是可以的,由于我们引入了k8s 的自动部署回滚,增加了系统稳定性和可用性,因为有80%的问题是由于变更出现的,所以当出现发布时出问题的时候,流量不会被转发到不健康的服务,并且我们可以通过k8s实现了快速回滚,也让用户更加愿意使用我们的系统,从而提升了业务系统的收入,也是间接帮助我们赚了钱。

同类技术能否做的到的问题,答案同类替代品目前没有k8s完善,而且需要组合其他多种工具才能实现相同的功能,自己也需要编写较多的脚本来辅助实现,所以并没有k8s解决的更直接。

用好官方文档

通过官方中英文档学习是学习新技术难以绕开的一环,中文搜索和gpt 都有比较多的纰漏,所以建议以官方文档的内容为准去进行学习和实践,这里因为官方文档内容较多,我挑出来我学习过程中看的关键的几个文档,然后再遇到费解的地方之后,通过搜索引擎和GPT 辅助去理解。这样也能在自己有一定识别能力的时候去借助三方工具理解,避免了被误导的情况。

k8s的官方文档有多种语言的翻译,如果觉得中文文档理解起来比较费劲,可以切换到英文的文档,这样专业名词看起来也会衔接的更加好

初识 K8s

理解k8s能帮我们创建什么对象来进行部署:kubernetes.io/zh-cn/docs/…

这里只需要对yaml有个大概的认知,明白我们的对象都是通过yaml来描述,然后k8s根据我们的yaml文件进行资源创建,并且创建出来的资源有相应的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # 告知 Deployment 运行 2 个与该模板匹配的 Pod
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

这里不需要过度深入每一个字段的内容和含义,先建立起一个大概的认知,后续在实际操作中创建具体的资源时,我们再通过参考手册去查询。

这个yaml描述了我们需要创建一个 Deployment 来维护副本为2的 Nginx状态,并且容器暴露的端口为80。

开始动手搭建k8s的环境,这样方便我们展开实操:kubernetes.io/zh-cn/docs/…

搭建好k8s环境后 ,参考这个文档 github.com/bmuschko/ck…

这里面的练习会首先提出问题,然后让我们一步一步操作,并且给出对应的答案,从而帮助我们快速的学会如何操作 kubernetes 集群和如何排查问题等,里面需要创建的内容也会给出相应的 yaml,当我们对字段不清楚是什么意思的时候,可以去官方文档中查询,避免上来就对着一大堆字段和概念懵逼。

通过第一个实操训练,能对核心的资源进行操作,然后部署起来一个nginx 服务,暴露出端口进行使用,也对k8s的操作有了初步的认识。

由于实际生产情况中我们是直接写了yaml文件进行资源描述,所以这里我提供了一个yaml和部署过程。

通过下面的yaml我们描述了一个Nginx资源并且暴露出 Service可以让我们进行访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
---

apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: NodePort
selector:
app: nginx
ports:
- name: http
port: 8080
nodePort: 48080 # 对外暴露的pod
targetPort: 80

然后通过以下的命令来一步一步创建我们的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
shell复制代码$ kubectl create namespace ckad-prep # 创建namespace

$ kubectl apply -f nginx-test.yaml -n ckad-prep # 创建资源
...
deployment.apps/nginx-deployment created
service/nginx created

$ curl localhost:48080
...
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="<http://nginx.org/>">nginx.org</a>.<br/>
Commercial support is available at
<a href="<http://nginx.com/>">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

完成实验后清理资源

1
2
3
4
5
6
shell复制代码$ kubectl delete -f nginx-test.yaml -n ckad-prep
deployment.apps "nginx-deployment" deleted
service "nginx" deleted

$ kubectl delete namespace ckad-prep
namespace "ckad-prep" deleted

通过上面的操作我们已经对 k8s 有了初步的认识和了解,也能对 k8s 进行使用,接下来就要开始攻克各种资源的知识,让我们能更加理解 k8s 。

更深入的学习各个组件

我们最开始接触到的容器是Docker run 运行一个镜像后,就会产生一个容器,并且通过 docker swarm 也可以单容器有调度、负载均衡等能力,但是存在一个致命的问题是单容器的这种模式难以描述真实世界中复杂的应用架构。

比如一个后端应用需要对日志进行收集,把日志转存到elasticSearch 上,如果是用Docker 进行实现的话,我们需要通过Docker-Compose 启动三个容器,分别是后端服务、日志转发收集服务和日志存储服务(elasticSearch) ,这种实现方式会存在一个问题,后端服务和日志转发服务有可能会被调度到不同的集群上,这个时候就需要通过网络来收集日志文件,我们更加直观的理解是虽然将这两个服务打包成一个容器,这个时候就是kubernetes 中抽象出来的Pod。

image.png
通过yaml我们描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3 # 可以根据需要进行调整
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: backend
image: your-backend-image
# 其他后端应用配置...
- name: log-collection
image: log-collection-image
# 其他 配置...

这里就将后端服务和日志收集打包在了一个Pod中,通过 Deployment 来管理我们的Pod,然后处理完后存储下来,这个模式就是容器设计模式里面最常用的Sidecar。

image.png

这里我们接触了最核心的Pod的概念

有了Pod后,Pod的生命周期管理及调度的问题,需要有另外的抽象来负责,这个抽象就是Deployment ,它用来管理Pod的对象,比如扩缩容,维持pod数量等。

image.png

如果节点不正常 Deployment可以快速将Pod调度到其他的节点上,并且可以通过Deployment快速的扩展Pod的数量来提升系统的响应能力。

在上面Yaml文件中我们创建出来的为Deployment而不是直接创建Pod,也是这个原因。

image.png

学习完上面的内容后作为研发经常碰到的内容已经解决了很大一部分,剩下的内容其实就根据自己感兴趣的和工作实际用到的去探索。

我自己的话因为对网络感兴趣,所以会深入的去学习 Service 相关的内容,比如它是如何维护变更的,如何通过 Iptables和 Ipvs去完成负载均衡,在通过源码看kubeproxy如何实现负载均衡的规则维护、长连接在k8s中是怎么样的形态出现。

通过一个感兴趣的点去带出面,不要盲目的东看一块西看一块。

我通过网络也从k8s的应用层学习到了传输层和网络层的协议以及操作系统的功能是如何被k8s使用,这样当K8s底层要更换底层实现的时候,其实我们只要去学习相应的内核功能就可以,这样也不仅仅局限在k8s的学习,更能串联计算机的所有相关知识。

计算机是人造科学,在学的时候一定有遵循一定人的逻辑,而且大神们的逻辑思路也相对清晰,思考大神们是如何思考的也能得到很多启发。

写在最后

感谢你读到这里,如果想要看更多 Kubernetes 的文章可以订阅我的专栏: juejin.cn/column/7321… 。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你真的理解分布式理论吗?

发表于 2024-01-11

分布式系统

随着时代的发展,传统的单台机器可能无法完成我们期待的任务,所以发展出了分布式系统这么一个概念,目的是为了提高系统的性能、可伸缩性、可用性、容错性。其本质就是堆机子,通过增加机器数量,完成之前单台机器难以完成的任务。

分布式系统:多台独立的计算机组成的系统,我们将每一台计算机视为一个结点,这些节点通过网络互相通信和协作。

但是引入了分布式系统之后,随之而来的是单体架构不会出现的问题。

比如

  • 通信和网络问题:在单体架构中,只有一台机器,无需和其他机器进行通信。而在分布式系统中,节点之间通过网络进行通信,这就导致可能会出现消息延迟、丢包、网络不稳定等问题造成的系统稳定性下降
  • 一致性和可用性的权衡:在单体架构中,一台机器接受请求之后进行处理,处理完成即可响应。但是在分布式系统中,在出现网络分区时,你可以选择一致性(先同步数据、再进行响应),也可以选择可用性(先进行响应、再进行数据同步)。
  • 数据一致性问题:多节点环境下,确保数据的一致性变得更为复杂,而且无法做到数据的实时强一致性,只能保证系统在一定时间的稳定运行之后,各节点的数据趋于一致。

为了解决这些问题,业界提出了一些分布式基石级别的理论以及落地方案。

本文会向大家介绍分布式的一些基础理论,以及流行的分布式算法。

分布式基础理论

CAP理论

CAP是分布式系统方向中的一个非常重要的理论,可以粗略的将它看成是分布式系统的起点,CAP分别代表的是分布式系统中的三种性质,分别是Consistency(可用性)、Availability(一致性)、Partition tolerance(网络分区容忍性),它们的第一个字母分别是C A P,于是这个理论被称为CAP理论。

  • 一致性(Consistency):所有节点在同一时间看到的数据是一致的。在分布式系统中,一致性要求所有节点对于某个操作的执行都具有相同的视图。
  • 可用性(Availability):系统在有限时间内能够为用户提供满足要求的响应,即系统对于请求的响应不能无期限的延迟或失败。
  • 分区容忍性(Partition Tolerance):成熟的分布式系统必须满足的性质,系统能够在节点之间发生网络分区的请款修改继续工作。

理论上来说,CAP三者同时最多满足两者,但是并不是必须满足两个,许多系统最多只能同时满足0、1个

为什么CAP最多只能满足两个呢?

我们可以以电商系统来当做例子,这个电商系统有两台服务器,彼此之间使用网络进行通信。

网络正常的时候,可以同步数据到另一台机器之后,再进行返回,或者返回之后再进行数据的同步。但是一旦出现了网络隔离,那么就可以有两个选择,即先同步数据还是先返回响应。

如下图所示,假设当一个请求打到了Server2这里

C: 追求的是数据一致性 当有一个请求来了之后 它会等待网络隔离的情况结束之后 向另一个机器进行数据的同步

首先,他会在本地处理好请求,这个请求常常会伴随着某些数据的变化,比如缓存内容的变化,程序内部某些共享变量的变化等等。

之后,它会等待网络恢复,即能够和Server1进行通信。

网络恢复后,Server1和Server2处于同一个网络中,彼此可以通信,这时进行数据的同步。

完成数据同步之后,返回响应。

A: 追求的是可用性 也就是尽可能提供有效服务 当一个请求来了之后 它会立即返回 哪怕数据是陈旧的 也得优先提供服务,其他分区的节点返回的结果(数据)可能是不一样,图也是反过来的。

注意:这里的AC不可同时满足指的是当整个分布式系统中出现网络隔离的时候,我们不能既想着保证数据的实时强一致性,又去追求服务的可用性。

但是当没有网络隔离的时候,其实这两个性质是可以同时满足的,因为『同步数据』和『返回结果』这两个操作都是在同一个网络中,只有先后关系,不会因为某个操作导致另一个操作的『死等』。

在分布式系统中,P是会必然发生的,造成P的原因可能是网络隔离,也可能是节点宕机。

我们无法保证分布式系统每一时刻都不出现网络隔离,如果不满足P的特性,一旦发生分区错误,那么分布式系统就无法工作,这显然违背了分布式的理念,连最基本的分布式系统条件都没有满足

典型的CP和AP的产品

CP:Zookeeper 当系统在发生分区故障之后 客户端的所有请求都会被卡死或者超时 但是系统总会返回一致的数据

AP:Eureka 分区发生故障之后 客户端依然可以访问系统 但是获取的数据有的是新数据 有的是老数据

当然 ,CAP这几个特性不是BOOL类型的,而是一个范围类型,完全是看系统具体需要什么样的要求。

比如分区容错,有的系统一台机器出错,系统会认为不影响业务的话,认为分区不存在。只有多台机器都出问题了,系统受到严重影响才认为出现分区

PACELC理论

PACELC理论是对CAP理论的扩展,在维基百科上的定义是

It states that in case of network partitioning (P) in a distributed computer system, one has to choose between availability (A) and consistency (C) (as per the CAP theorem), but else (E), even when the system is running normally in the absence of partitions, one has to choose between latency (L) and consistency (C).

翻译:如果有分区(P),那么系统就必须在可用性(A)和一致性(C)之间取得平衡,否则(E),当系统运行在无分区的情况下,系统需要在延迟(L)和一致性(C)之间取得平衡。

它相比于CAP,多引入了一个延迟Latency的概念,在出现分区错误的时候,取前半部分PAC,理论和CAP的内容一致。没有出现分区错误的时候取LC,也就是Latency与Consistency。

当前分布式系统指导理论更替代CAP理论,理由如下

  • PACELC更能满足实际操作中分布式系统的工作场景,是更好的工程实现策略
  • 当P存在的场景下,需要在A C之间做取舍,但是实际上分布式系统大部分时间里P是不存在的,那么在L和C之间做取舍是一个更好的选择
  • PACELC可以在latency与consistency之间获得平衡

要保证系统的高可用,那么就得采用冗余的思想,我的其他博文有提到4个9的异地多活策略,也是采用的数据冗余思想,而一旦涉及到了复制数据,在分布式中就一定会在Consistency和Latency之间做一个取舍

举个例子

在强一致性的场景下,需要三个从节点都落盘数据,才能给客户端返回OK,这个时候当master向slave同步数据的时候,超过20ms触发超时了,整个系统还是会不断的重试这个过程,这显然造成了系统的可用性比较低

所以我们一般都会在数据一致性和请求时延之间做一个balance

例如:当同步超过五次之后,认为这个节点故障,选择直接返回,可以消除写时的长尾抖动,同时给节点打上故障标签,进行后续的处理

BASE模型

base模型是Basically Avaliable(基本可用)、Soft State(软状态)、Eventually Consistent(最终一致性)三个短语的缩写,核心思想如下

即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。对应到CAP中的概念,就是牺牲C,来保证AP的满足,这是对传统ACID模型的取舍,适用于大规模的分布式系统,尤其是与存储相关的分布式组件,例如分布式缓存、分布式存储等等。

BA:基本可用指的是当系统出现了不可预知的故障,系统依旧可用,不过可用度也许会降低,比如响应时间上出现损失,功能上只能满足基本功能等等

S:基于原子性而言的话,当要求多个节点数据一致时,我们认为这是一种『硬』状态,而允许系统中的数据存在中间状态,并认为其不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据时延

E:最终一致性,系统不可能一直都处于一个软状态中,必须有个时间期限。在期限过后,应该保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于时延、负载、方案等等

在工程实践中,有这么几种最终一致性的实现策略,通常都是多种策略混合实现

  • 因果一致性:如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。与此同时,与节点A无因果关系的节点C的数据访问没有这样的限制
  • 读已知所写:节点A更新一个数据之后,自身总是能访问到更新过的最新值,而不会访问旧值
  • 会话一致性:将对系统数据的访问过程框定在了一个会话当中,系统能保证同一个有效的会话中实现客户端在一个会话中读取到该数据项永远是最新值
  • 单调读一致性:如果一个节点从系统中读取出一个数据项的某个值之后,那么系统对于该节点后续的任何数据访问都不该返回更旧的值
  • 单调写一致性:一个系统要能够保证来自同一个节点的写操作被顺序的执行。

NWR多数派理论

NWR多数派理论是分布式系统共识和分布式一致性算法的基础,只有理解了NWR才能理解Raft、Paxos、Gossip这些分布式一致性算法。这个理论是分布式系统中一种常见的一致性模型,被广泛应用于保证数据的一致性和可靠性,以及系统的可用性。

它指的是:在多数副本的一致性模型中,只有大多数副本确认了某个操作,才认为这个操作已经完成。

NWR中N代表的是副本数量,W代表写入的副本数量,R则为读取的副本数量。在多数的一致性模型中,一般要求W+R>N,以保证读写操作的一致性。同样的,NWR也有着三个阶段。

  • Negotiation(协商):所有节点通过相互通信来达成一致性决策,在协商阶段,节点之间需要同步信息,最后达成一致。
  • Write(写入):决策被转化为实际的操作,节点进行写入操作。
  • Read(读取):节点读取其他节点的值,并使用协商阶段的决策和写入阶段的结果,也就是说读一定是读的最新值。

在写入操作的时候,只有W个副本被成功写入才返回成功,而在读取操作时,只有R个副本成功返回相同的数据才返回成功。这样,只要大多数副本成功确认了操作,就可以认为这个操作已经完成。

NWR在现有组件的应用还是很广泛的,比如Raft选主判断逻辑为投票数量>=n/2则成功选主,比如Redis的哨兵机制,有哨兵标记下线则为主观下线,>=n/2标记下线则为客观下线。

分布式一致性算法

分布式一致性算法用于确保在分布式系统中不同节点之间达成一致决策的算法。这些算法致力于解决由于节点故障、网络分区或并发操作等原因导致的数据不一致的问题。

常见的分布式一致性算法有Raft、Paxos、Gossip等等,在分布式一致性算法中,每台计算机都视为等同的节点,这和分布式事务完全相反。

分布式事务和分布式一致性算法的目的是一样的,本质都是将一个任务放在分布式的环境下处理和解决,但是实现则不一样。

  • 在分布式事务中,不同机器的作用是不一样的,一个任务会被拆解成多个步骤,分别交给不同的角色进行处理,全部成功则提交,一旦有失败则回滚,也就是不同机器各司其职。
  • 在分布式算法中,每台机器的作用都是一样的,每一个节点都能处理请求,它的作用是进行各个节点之间的同步,并解决可能出现的问题,比如数据竞态问题(data racing)等等。

具体算法的实现细节我们另开一篇文章来阐述。

总结

本文向大家介绍了常见的分布式基础理论,后面还会向大家介绍Raft、Paxos、Gossip等分布式算法。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:01+02等于多少?我不假思索03,结果直接回

发表于 2024-01-10

面试官:0.1+0.2等于多少?我不假思索0.3,结果直接回家等通知

关于0.1+0.2,这是一个非常经典的问题

众所周知数学上是0.1+0.2是等于0.3的,然而在大部分编程语言中却不等于0.3

我第一次知道也是难以置信,于是就写了下图的java程序来验证
0.1+0.2!=0.3


相信大部分小伙伴第一次知道这个事实后都会怀疑人生

难道这个世界是假的?我们身处在楚门的世界中?显然不是这样😂

这是为什么,又该如何解决呢?(本文以JAVA面试为例)

PS:这个问题是一个比较有名的前端面试题,但实际上不止是JavaScript中,

后端的JAVA以及大部分别的语言都有这个问题

(题外话:

我对象也学计算机,大二的时候我和她打赌java里面0.1+0.2不等于0.3,她不信结果赌输了亲了我一下😁😁😁)

面试官:0.1+0.2等于多少?我:0.3000000000000004

面试官:为什么不是0.3?我:不知道…… 结果直接回家等通知

许多开发者知道0.1+0.2==0.3000000000000004

却是知其然不知其所以然,这里我简单说说为什么0.1+0.2不等于0.3

众所周知计算机使用的是二进制

十进制小数转成二进制,一般采用”乘2取整,顺序排列”方法,如0.625转成二进制的表示为0.101。
但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数。所以,计算机是没办法用二进制精确的表示0.1的

人们想出了一种采用一定的精度,使用近似值表示一个小数的办法。这就是IEEE 754

IEEE754中,一个浮点数由符号位、尾数和阶码组成。符号位用于表示正数或负数,尾数是有效数字的部分,而阶码用于表示指数
image.png
十进制数经IEEE754实际转换得到的二进制数是一个近似值

而Double类型只存储8字节,即64位,如下图所示

0.1转换为

1
js复制代码0 01111111011 1001100110011001100110011001100110011001100110011010

0.2转换为

1
js复制代码0 01111111100 1001100110011001100110011001100110011001100110011010

相加得

1
js复制代码0 01111111101 0011001100110011001100110011001100110011001100111010

故转换成十进制就得0.3000000000000004

面试官:0.1+0.2等于多少?我:0.3000000000000004

面试官:为什么不是0.3?我:因为采用了IEEE754码制,十进制浮点数无法完全精确转换为二进制浮点数

面试官:那你能实现0.1+0.2==0.3吗 我:我不会欸.. 结果直接回家等通知

面试官:0.1+0.2等于多少?我:0.3000000000000004

面试官:为什么不是0.3?我:因为采用了IEEE754码制,十进制浮点数无法精确转换为二进制浮点数

面试官:那你能否实现0.1+0.2==0.3

我:(0.1* 10 + 0.2 *10)/10==0.3

其实想要使0.1+0.2等于0.3也是可以实现的
最简单的方法就是

(0.1* 10 + 0.2 *10)/10==0.3

直接乘10再除以10 就好了,因为乘以10以后就是整数运算了,就是精确值了

但在实际的业务场景中却并不是一个很好的办法

面试官:你还会别的实现方法吗

我:使用BigDecimal类存储0.1和0.2,然后再用add方法相加!

面试官:下周来入职🤝🤝🤝!

针对0.1+0.2问题,java中常用的解决方法BigDecimal

BigDecimal 是一个可以实现对浮点数的运算的类,而且不会造成精度丢失

1
2
3
4
5
6
7
8
9
java复制代码 import java.math.BigDecimal;
public class Main{
public static void main(String[] args) {
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = BigDecimal.valueOf(0.2);
BigDecimal c = a.add(b);
System.out.println(c);
}
}

(终于等于0.3了,呼~)



需要注意的是BigDecimal的用法,以上文字引用自《阿里巴巴开发手册》

有的人看完了可能会说,差这么一点点有什么关系呢

实际上如果涉及到高频金钱交易的话,这一点点的差距也可以造成致命的损失

比如某金融系统可能1天交易100万次,1次损失1分钱,一个月下来损失就有30万了!

希望兄弟们遇到类似的场景不要忘记BigDecimal的用法,记得不要直接传参数

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

就是高效,EasyExcel这样写合并单元格 一、引入 二、

发表于 2024-01-10

一、引入

通过阅读本文,你将了解到:

  • 使用EasyExcel写出Excel时,如何一步步提升性能;
  • 写出有合并单元格的页签时,如何在更短的时间内写出更多的数据。

如果你第一次接触EasyExcel,可以先访问官网,按照指引掌握EasyExcel读写操作,然后再阅读本文。

如果你已经掌握了使用EasyExcel读写Excel,那么当你需要写出有合并单元格的大页签时,你会如何实现?

github地址:github.com/alibaba/eas…

官方网站: easyexcel.opensource.alibaba.com/

语雀文档:www.yuque.com/easyexcel/d…

1
2
3
4
5
6
java复制代码<!-- 本文引用的版本 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>

假设有个需求

假设你所在的公司需要开发一个功能:将数据库中票据表写出到Excel中,而且想在尽可能短的时间内(如30秒)写出几个月甚至一年内的数据(可能有几十万、上百万条记录),你会如何实现?

我们先来看看票据的一个简单模型:由一个头信息区、多条明细两部分组成,写出到Excel时样式如下。
image.png

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.math.BigDecimal;
import java.util.Date;

/**
* 一张票据下有多项费用科目
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class BillExpenseDetail {
@ExcelProperty("票据编号")
private String number;
@ExcelProperty("创建时间")
private Date createDate;
@ExcelProperty("收支方向")
private String direction ;
@ExcelProperty("总金额")
private BigDecimal totalAmount;

@ExcelProperty("名称规格")
private String subject;
@ExcelProperty("单价")
private String price;
@ExcelProperty("数量")
private String quantity;
@ExcelProperty("单位")
private String unit;
@ExcelProperty("金额")
private BigDecimal amount;
}

二、无合并单元格时

2.1 一次性查询写出

不考虑单元格合并时,你可能会这样实现:一次性查询所有数据,然后一次性写出。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码  // 查询所有数据
private static List<BillExpenseDetail> queryAll() {
return new ArrayList<>();
}

public static void simpleWrite() {
String fileName = "/bill/simpleWrite.xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName, BillExpenseDetail.class).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("票据").build();
excelWriter.write(queryAll(), writeSheet);
}
}

这样实现有问题吗?数据量较少时没问题。

可是,当一次需要写出的数据有数万条甚至更多时,将所有数据一次性查询到内存中,当所有数据写出后,才能释放内存。这样可能导致很大的内存压力,甚至服务OOM。

有什么更好的办法吗?有。

2.2 分页查询写出

EasyExcel支持重复多次写单个或者多个Sheet页,我们可以多次分页查库获取数据,循环写入到一个Excel页签中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码  // 查询数据总量
private int count() {
// 假设为100万
return 1000000;
}

// 分页查询
private List<BillExpenseDetail> pageQuery(int startIndex, int limit) {
return new ArrayList<>();
}

public void repeatedWrite() {
int count = count();
int pageSize = 1000;
int pageCount = count / pageSize;
pageCount = pageCount * pageSize < count ? pageCount + 1 : pageCount;

String fileName = "/bill/repeatedWrite.xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName, BillExpenseDetail.class).build()) {
// 写出到一个sheet页中,因此在for外面创建WriteSheet
WriteSheet writeSheet = EasyExcel.writerSheet("票据").build();
// 逐页查询,追加写出
for (int i = 0; i < pageCount; i++) {
List<BillExpenseDetail> detailList = pageQuery(i * pageSize, pageSize);
excelWriter.write(detailList, writeSheet);
// help gc
detailList.clear();
}
}
}

现在,查询数据时内存压力减小了。可是等上线后,发现导出一个月的数据可能需要10秒,导出半年内数据时可能需要50秒或更长时间。如果是离线导出,耗时久点也能接受;如果是在线导出,可能接口响应超时。

有更高效的办法吗?

2.3 并发查询依次写出

查询一页数据写出后,再查询下一页,读与写是串行的。而且分页查询时页码越大,一次查询耗时越久。那么我们能否并发查询同时写出呢?当然可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码  public static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(4);

public void repeatedWrite() {
// 并发分页查询数据
int count = count();
int pageSize = 1000;
int pageCount = count / pageSize;
pageCount = pageCount * pageSize < count ? pageCount + 1 : pageCount;
List<Future<List<BillExpenseDetail>>> futureList = new ArrayList<>(pageCount);
for (int i = 0; i < pageCount; i++) {
int index = i;
Future<List<BillExpenseDetail>> submit = EXECUTOR_SERVICE.submit(
() -> pageQuery(index * pageSize, pageSize));
futureList.add(submit);
}

String fileName = "/bill/repeatedWrite.xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName, BillExpenseDetail.class).build()) {
// 写出到一个sheet页中
WriteSheet writeSheet = EasyExcel.writerSheet("票据").build();
// 追加写
for (Future<List<BillExpenseDetail>> future : futureList) {
try {
List<BillExpenseDetail> detailList = future.get();
excelWriter.write(detailList, writeSheet);
// help gc
detailList.clear();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
}

三、合并单元格写出Excel

现在我们考虑如何实现合并单元格。因为一个票据有多个明细行(数量不确定),导出Excel时要将“票据编号”、“创建时间”等列跨行合并。该如何实现呢?

3.1 EasyExcel中实现

EasyExcel提供了两个创建合并单元格的注解,以及与注解等效的WriteHandler接口实现。定义下面的数据类,我们来试用一下。

1
2
3
4
5
6
7
8
9
10
java复制代码@Getter
@Setter
public class DemoMergeData {
@ExcelProperty("字符串")
private String string;
@ExcelProperty("日期")
private Date date;
@ExcelProperty("数字")
private Double doubleData;
}

3.1.1 @ContentLoopMerge

image.png

先不使用@ContentLoopMerge,生成的Excel如下:

image.png

对第一列使用@ContentLoopMerge后,生成的Excel如下:

1
2
3
4
java复制代码    // 每两行合并一次,跨两列
@ContentLoopMerge(eachRow = 2, columnExtend = 2)
@ExcelProperty("字符串")
private String string;

image.png

3.1.2 @OnceAbsoluteMerge

该注解通过指定合并区域行列索引,用来创建一个合并区域(不是循环创建);单元格值取左上角单元格的。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Getter
@Setter
// 将第2-6行的2-3列合并
@OnceAbsoluteMerge(firstRowIndex =1, lastRowIndex = 5, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
@ExcelProperty("字符串")
private String string;
@ExcelProperty("日期")
private Date date;
@ExcelProperty("数字")
private Double doubleData;
}

效果如下:

image.png

3.1.3 WriteHandler实现

EasyExcel提供了与上面两个注解等效的WriteHandler实现,分别是OnceAbsoluteMergeStrategy和LoopMergeStrategy。使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public static void mergeWrite() {
String fileName = "/excel/mergeWrite.xlsx";
// 对第一列每隔2行合并一次,不跨列(第二个参数)
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 1, 0);
// 创建合并区:将第2-6行的2-3列合并
OnceAbsoluteMergeStrategy absoluteMergeStrategy = new OnceAbsoluteMergeStrategy(1, 5, 1, 2);
EasyExcel.write(fileName, DemoMergeData.class)
.registerWriteHandler(loopMergeStrategy)
.registerWriteHandler(absoluteMergeStrategy)
.sheet("模板")
.doWrite(data());
}

3.2 自定义合并策略

3.2.1 网络上的常规实现

票据导出时因为每个票据的明细行数量不定,@ContentLoopMerge就不适用了。此时,我们自然想到去网上找找方案。
比如,我找到了这篇博客《EasyExcel导出自定义合并单元格策略》https://cloud.tencent.com/developer/article/1671316。
它的实现方式如下,核心逻辑为:
实现CellWriteHandler接口,在Cell层面,每写一行数据,将合并列的单元格数据,与上一行的单元格数据比较。如果数据相同,就将当前行与上一行合并;如果上一行已被合并,则将当前行加入到合并区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
java复制代码import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import java.util.List;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

public class ExcelFillCellMergeStrategy implements CellWriteHandler {

// 需要创建合并区的列
private int[] mergeColumnIndex;
// 从第几行后开始合并,取列头行
private int mergeRowIndex;

public ExcelFillCellMergeStrategy() {
}

public ExcelFillCellMergeStrategy(int mergeRowIndex, int[] mergeColumnIndex) {
this.mergeRowIndex = mergeRowIndex;
this.mergeColumnIndex = mergeColumnIndex;
}

@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex,
Integer relativeRowIndex, Boolean isHead) {

}

@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex,
Boolean isHead) {

}

@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> list, Cell cell,
Head head,
Integer integer, Boolean aBoolean) {
int curRowIndex = cell.getRowIndex();
int curColIndex = cell.getColumnIndex();
if (curRowIndex > mergeRowIndex) {
for (int i = 0; i < mergeColumnIndex.length; i++) {
// 需合并的列
if (curColIndex == mergeColumnIndex[i]) {
mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex);
break;
}
}
}
}

/**
* 当前单元格向上合并
*
* @param writeSheetHolder
* @param cell 当前单元格
* @param curRowIndex 当前行
* @param curColIndex 当前列
*/
private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) {
Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
// 将当前单元格数据与上一个单元格数据比较
Boolean dataBool = preData.equals(curData);
//此处需要注意:因为我是按照序号确定是否需要合并的,所以获取每一行第一列数据和上一行第一列数据进行比较,如果相等合并
Boolean bool = cell.getRow().getCell(0).getNumericCellValue() == cell.getSheet().getRow(curRowIndex - 1).getCell(0).getNumericCellValue();
if (dataBool && bool) {
Sheet sheet = writeSheetHolder.getSheet();
List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();
boolean isMerged = false;
for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {
CellRangeAddress cellRangeAddr = mergeRegions.get(i);
// 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
sheet.removeMergedRegion(i);
cellRangeAddr.setLastRow(curRowIndex);
sheet.addMergedRegion(cellRangeAddr);
isMerged = true;
}
}
// 若上一个单元格未被合并,则新增合并单元
if (!isMerged) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
sheet.addMergedRegion(cellRangeAddress);
}
}
}
}

能实现我们导出票据的需求吗?能。但是试用后将会发现,这个实现性能不佳:

  • 每写入一个单元格,都需要读取上一行,一边写入一边读取;
  • 当上一行已经合并过了,本次写入需要修改合并区域,而且会反复修改;
  • 比如,写出下图中第一个票据,写出3行,将读取3次,修改合并区域两次。
    image.png
    此外,网上还有一些基于RowWriteHandler接口的实现,也存在上面指出的性能问题。

3.2.2 我的实现

当我们分页查询票据记录后,可以按照合并自动进行分组,每组数量就是合并区域大小,合并区域位置可以通过行数累加来定位。因此。写出Excel前就可以预知那些合并区域。如果在创建sheet页时就将这些区域一并创建,写出时就不用关注单元格合并了。岂不美哉!

预创建合并区:实现SheetWriteHandler接口,重写afterSheetCreate(),将合并区域加入到sheet中。
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import java.util.Collections;
import java.util.List;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

/**
* 添加合并区Handler
*/
public class AddCellRangeWriteHandler implements SheetWriteHandler {

private final List<CellRangeAddress> rangeCellList;

public AddCellRangeWriteHandler(List<CellRangeAddress> rangeCellList) {
this.rangeCellList = (rangeCellList == null) ? Collections.emptyList() : rangeCellList;
}

public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Sheet sheet = writeSheetHolder.getSheet();
for (CellRangeAddress cellRangeAddress : this.rangeCellList) {
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码  public static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(4);

public void repeatedWrite() {
// 并发分页查询数据
int count = count();
int pageSize = 1000;
int pageCount = count / pageSize;
pageCount = pageCount * pageSize < count ? pageCount + 1 : pageCount;
List<Future<List<BillExpenseDetail>>> futureList = new ArrayList<>(pageCount);
for (int i = 0; i < pageCount; i++) {
int index = i;
Future<List<BillExpenseDetail>> submit = EXECUTOR_SERVICE.submit(
() -> pageQuery(index * pageSize, pageSize));
futureList.add(submit);
}

// 追加写
String fileName = "/bill/repeatedWrite.xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName, BillExpenseDetail.class).build()) {
// 行计数,初始值取列头行数
int lineCount = 1;
// sheet中需要合并的列的索引
final int[] mergeColumnIndex = {0, 1, 2, 3};
WriteSheet writeSheet;
for (Future<List<BillExpenseDetail>> future : futureList) {
try {
List<BillExpenseDetail> detailList = future.get();
List<CellRangeAddress> rangeCellList = createCellRange(detailList, mergeColumnIndex, lineCount);
lineCount += detailList.size();
// 写出到一个sheet页中,sheetName固定
writeSheet = EasyExcel.writerSheet("票据").registerWriteHandler(new AddCellRangeWriteHandler(rangeCellList)).build();
excelWriter.write(detailList, writeSheet);
// 及时释放内存
detailList.clear();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
}

/**
* 生成合并区
*
* @param detailList 票据
* @param mergeColumnIndex sheet 中需要合并的列的索引
* @param lineCount 行计数(包括列头行)
* @return 合并区
*/
private List<CellRangeAddress> createCellRange(List<BillExpenseDetail> detailList, int[] mergeColumnIndex, int lineCount) {
if (detailList.isEmpty()) {
return Collections.emptyList();
}

List<CellRangeAddress> rangeCellList = new ArrayList<>();
Map<String, Long> groupMap = detailList.stream().collect(Collectors.groupingBy(BillExpenseDetail::getNumber, Collectors.counting()));
for (Map.Entry<String, Long> entry : groupMap.entrySet()) {
int count = entry.getValue().intValue();
int startRowIndex = lineCount;
// 如合并第2到4行,共3行,行索引从1到3
int endRowIndex = lineCount + count - 1;
for (int columnIndex : mergeColumnIndex) {
rangeCellList.add(new CellRangeAddress(startRowIndex, endRowIndex, columnIndex, columnIndex));
}
lineCount += count;
}
return rangeCellList;
}

该方式我已在工作中使用,性能确实有较大提升。感兴趣的小伙伴,也不妨一试。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Jetpack Compose -> 声明式UI & Mod

发表于 2024-01-08

前言

本章主要介绍下 Compose 的声明式 UI 以及初级写法;

什么是声明式UI

传统UI

传统 UI 方式来声明UI

1
2
3
4
5
6
7
8
9
ini复制代码<androidx.appcompat.widget.LinearLayoutCompat    
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="match_parent"
android:scaleType="centerCrop"
android:layout_height="300dp" />
</androidx.appcompat.widget.LinearLayoutCompat>

是通过 xml 来进行显示的,显示文字的方式是使用 TextView,它内部显示文字的方式有两种,一种是在 xml 中直接设置,通过下面这种方式设置

1
ini复制代码android:text="@string/app_name"

这种方式是通过初始值在 xml 中进行预设置的;

还有一种是在代码中直接调用 setText 进行设置

1
2
ini复制代码TextView textView = findViewById(R.id.text);
textView.setText("xxxx");

代码中是通过 setText 后续对值进行手动更新的;

这种需要手动对界面更新的方式 就不是声明式,属于传统式

声明式UI

指的是:在写界面的时候,只需要对界面进行一次性的声明,而不用在各种条件对界面元素进行跟条件有关的更新;

声明式 UI 自动更新界面,不需要手动更新,而传统式需要手动更新界面;

例如我们写一个 Compose 的 Text

1
2
3
4
ini复制代码Text(    
text = "Hello $name!",
modifier = modifier
)

我们只需要把 name 作为参数传给这个 Text ,这个文字控件就会显示 name 的值;而声明式 UI 它不仅会用 name 来初始化显示 name 的值,当 name 的值发生变化的时候,Text 所显示的内容也会跟着发生改变,比如 name 的初始值是 Mars,界面第一时间显示的就是 Mars,过了一会这个 name 的值变成了 老A 了,那么界面就会跟着变成 老A,而不需要再次调用 setText 进行更新;这就是声明式 UI;并不是说界面是用声明式写出来的,而是说只需要声明就够了,不需要任何的手动更新;

传统式再次调用 setText 才能更新,这个调用不能省略,否则不会发生更新;

Compose 怎么写?

一种是直接创建一个 Compose 项目,一种是项目中增加配置,让它支持 Compose;

我的 Android Stuido 版本,可以直接创建 Compose 项目,并且这个版本的 AS 已经默认创建的就是 Compose 项目了;

点击 Next 创建完成;

完成之后,我们在 MainActivity 中可以看到一个大概的构建逻辑:

区别与传统的方式,我们通过一个 setContent 函数开启了我们的 Compose 之旅;

文字 Text 的声明

1
2
3
scss复制代码setContent {    
Text("老A")
}

运行我们的程序,然后就可以在界面上看到 老A

Text 函数还有很多其他的用法,大家根据 API 直接去探索使用即可

图片 Image 的声明

传统 ImageView 显示图片的方式有两种,一种是位图,也就是 Bitmap;一种是矢量图,也就是 VectorDrawable;

但是在 Compose 中位图提供了一个新的 API,ImageBitmap;矢量图也提供了一个新的 API,ImageVector;

到这里的时候,可能很多人就会有疑问了,为什么要区别开来?

因为 Compose 的初衷是独立于 Android 平台,这个 [独立于平台] 说的就是不依赖于最新的 Android 系统才能使用;

例如我们熟悉的 RecyclerView,ViewPager2,AppCompat,协调者布局,ViewModel 等等一系列 Jetpack 组件,任何一个库出了新版本就能直接发布,开发者就可以直接使用,不需要等升级到最新的 Android 系统版本之后才能使用;

Compose 不仅独立与最新版本的 Android ,还独立于 Android 平台;Compose 提供的所有 API 全都不带有 Android 的痕迹;

虽然 Compose 不带有 Android 痕迹了,但是它底层还是使用的 Android 的 drawText()、drawTextRun()(可以看下这个链接,针对这两个 api 的介绍)、Canvas 来进行的绘制;

更精确的说:是上层暴露给开发者的 API 是独立于 Android 平台的;

声明一个图片 使用 Image,如果想使用 drawable 文件夹下的图片,使用 painterResource 来传递一个资源 id;

1
2
3
4
5
ini复制代码Image(
painterResource(
id = R.drawable.ic_launcher_foreground),
contentDescription = resources.getString(R.string.app_name)
)

painterResource 这个函数内部会创建一个 ImageBitmap 或者 ImageVector 对象,取决于传递进去的时候位图还是矢量图,不过这就跟外部调用的没有关系了;painter 跟 Android 中的 drawable 比较类似,我们也可以不使用 painterResource 直接传递一个 ImageBitmap、ImageVector 也是可以的;

运行效果如上;

网络图片的加载,就需要依赖第三方库了,这里推荐一个图片库 Coil (Android 官方推荐 Coroutine Image Loader)这个库是面向 kotlin 和协程的,同时它还不是面向 View 系统的,

使用的话,直接引入依赖即可

1
scss复制代码implementation("io.coil-kt:coil-compose:2.5.0")

然后还是使用 Image 函数

1
2
3
4
5
less复制代码Image(    
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp)
)

只不过 painterResource 替换成了 rememberAsyncImagePainter,就可以传递一个网络图片url,并进行加载;

设置图片尺寸,使用 Modifier 关键字;

别忘了添加网络权限

1
ini复制代码<uses-permission android:name="android.permission.INTERNET"/>

运行效果如下:

早期的 Coil 并没有支持 Jetpack Compose,如果你使用的 Coil 版本比较低,可以使用另外一个库 Accompanist

Image 的底层也是最终调用的 Android 的原生 API drawBitmap,如果是纯色,就是 Canvas 的 drawColor 等 API;

图标 Icon 的声明

在 Compose 中,图标的声明使用 Icon 函数;

1
2
3
4
5
ini复制代码Column {
Icon(
imageVector = Icons.Filled.Favorite, contentDescription = ""
)
}

运行效果如下:

Icon 的用法基本上和 Image 上一样;

按钮 Button 的声明

在Compose当中,Button 和 Text 之间并没有什么关系。它们是两个独立的控件,并且通常它们还需要配合在一起使用才行;

先来看下不配合 Text 的效果

1
ini复制代码Button(onClick = {}) {    }

运行效果如下:

按钮是没有文案的,需要给 Button 设置一个文案

1
2
3
ini复制代码Button(onClick = {}) { 
Text(text = "我是老A")
}

运行效果如下:

点击事件的话,就直接在 onClick = { } 中处理即可

1
2
3
4
5
ini复制代码Button(onClick = {    
Toast.makeText(LocalContext.current as Context, "", Toast.LENGTH_LONG).show()
}) {
Text(text = "我是老A")
}

输入框 TextFiled 的声明

在 Compose 中,使用 TextFiled 代替了 EditText

1
2
3
4
5
6
7
ini复制代码var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = {
text = it
},
label = {Text(text = "用户名")})

运行效果如下:

可以看到,实现了 EditeText 的效果,还是 Material 风格的;

TextField 函数还有很多其他的用法,大家根据 API 直接去探索使用即可;

使用 Layout 的 Compose 平替

Android 中常用的布局有 FrameLayout、 LinearLayout、RelativeLayout、ConstraintLayout、ScrollView、RecyclerView、ViewPager 等等

Compose 中的平替控件

FrameLayout -> Box()

1
2
3
4
5
6
7
8
9
ini复制代码Box {
Text(
text = "老A"
color = Color(0XFF886600)
)
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp)
)
}

运行效果如下:

可以看到,和上面截图排列一样,也就是说,不使用布局控件,默认就是 FrameLayout排列;

LinearLayout -> Column()、Row() 表示纵向和横向;

1
2
3
4
5
6
7
8
9
ini复制代码Column {
Text(
text = "老A"
color = Color(0XFF886600)
)
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp)
)
}

运行效果如下:

1
2
3
4
5
6
7
8
9
ini复制代码Row {
Text(
text = "老A"
color = Color.Blue
)
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp)
)
}

运行效果如下:

RelativeLayout -> Box()

RelativeLayout 也是使用 Box 平替,但是通过 Modifier 来修改相对位置;

1
2
3
4
5
6
7
8
9
10
11
scss复制代码Box(modifier = Modifier.border(2.dp, Color.Black).size(200.dp)){    
Text(
text= "老A",
color = Color.Blue
)
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp).align(Alignment.CenterEnd)
)
}

运行效果如下:

ConstraintLayout -> ConstraintLayout for Compose

因为约束布局非常好用,所以官方为我们迁移到了 Compose 平台,我们可以直接使用这个控件;这里并不是套壳支持,而是把它们的逻辑移植到了 Compose 里面;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
scss复制代码implementation("androidx.constraintlayout:constraintlayout-compose:+")

ConstraintLayout(modifier = Modifier
.border(2.dp, Color.Black)
.size(400.dp)
.padding(10.dp)) {
val (portraitImageRef, usernameTextRef, desTextRef) = remember { createRefs() }
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = "",
modifier = Modifier.size(150.dp).constrainAs(portraitImageRef){
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
})
Text(
text = "我是老A",
fontSize = 16.sp,
maxLines = 1,
textAlign = TextAlign.Left,
modifier = Modifier.constrainAs(usernameTextRef) {
top.linkTo(portraitImageRef.top)
start.linkTo(portraitImageRef.end, 10.dp)
})
Text(
text = "这是我的个人描述",
fontSize = 12.sp,
maxLines = 1,
color = Color.Red,
fontWeight = FontWeight.Light,
modifier = Modifier.constrainAs(desTextRef) {
top.linkTo(usernameTextRef.bottom, 5.dp)
start.linkTo(portraitImageRef.end, 10.dp)
})
}

运行效果如下:

使用 RecyclerView(竖向) 的 Compose 平替 -> LazyColumn

1
2
3
4
5
6
ini复制代码val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn { // 没有 adapter,没有 ViewHolder
items(names.size) {
Text(text = names[it])
}
}

运行效果如下:

items 是遍历,item 是设置单个列表项,而且可以使用 item 来添加header、footer,也可以设置不同的 itemType,也就是不同的 ViewHolder;

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn {
item {
Text(text = "header")
}
items(names.size) {
Text(text = names[it])
}
item {
Text(text = "footer")
}
}

运行效果如下:

设置不同的 itemType,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vbnet复制代码val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn {
item {
Text(text = "header")
}
items(names.size) {
Text(text = names[it])
}
item {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp)
)
}
items(names.size) {
Text(text = names[it])
}
item {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp)
)
}
}

运行效果如下:

可以看到,直接就可以滑动了,也就是实现了 Rechyclerview 的效果;

使用 RecyclerView(横向) 的 Compose 平替 -> LazyRow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vbnet复制代码val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul")
LazyRow {
item {
Text(text = "header")
}
items(names.size) {
Text(text = names[it])
}
item {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp)
)
}
items(names.size) {
Text(text = names[it])
}
item {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp)
)
}
}

运行效果如下,横向滑动的列表:

使用 ScrollView(竖向) 的 Compose 平替 -> Modifier.verticalScroll

Android 中还有一个滑动组件:ScrollView,它的内部不是动态的,它内部的内容都是预先编排好的,而不是让它自己算的,它内部的布局也是在第一时间就加载好的,而不是滑动到什么地方才加载,如果 ScrollView 加载一个很长很长的布局,有可能会内存溢出的;

而在 Compose 中的平替是没有 ScrollView 的概念的,你只需要给你需要滑动的组件加上 Modifier.verticalScroll() 函数就可以实现竖向滑动;

1
2
3
4
5
6
7
8
9
10
scss复制代码Column(Modifier.requiredSize(200.dp)    
.background(Color.Blue)
.verticalScroll(rememberScrollState())) {
repeat(20) {
Text(
text= "老A",
color = Color.Red
)
}
}

运行效果如下:

使用 ScrollView(横向) 的 Compose 平替 -> Modifier.horizontalScroll

1
2
3
4
5
6
7
8
9
scss复制代码Row(Modifier.requiredSize(200.dp)    
.background(Color.Blue)
.horizontalScroll(rememberScrollState())) { repeat(20) {
Text(
text= "老A",
color = Color.Red
)
}
}

运行效果如下:

使用 ViewPager(竖向) 的 Compose 平替 -> VerticalPager

1
2
3
4
5
6
ini复制代码VerticalPager(10) { page ->     
Text(
text = "Page: $page",
modifier = Modifier.fillMaxWidth()
)
}

运行效果如下:

使用 ViewPager(横向) 的 Compose 平替 -> HorizontalPager

1
2
3
4
5
6
ini复制代码HorizontalPager(10) { page ->     
Text(
text = "Page: $page",
modifier = Modifier.fillMaxWidth()
)
}

这里就不运行显示了,大家可以自己运行看下效果

Modifier

padding & margin

现在我们知道怎么去写布局了,其中线性布局是我们开法中用的最多的布局了,但是我们一般并不只是在那放一个线性布局就OK了,而是我们需要做很多很多的细节设置,比如我们需要去设置一些边距,margin、padding 等等,而在 Compose 中需要使用另外一种方式来设置边距,它就是 Modifier;

Modifier 是 Compose 中的一个很重要的角色,UI 的很多设置都是通过 Modifier 来完成的,通过官方文档,我们可以知道,Modifier 有如下四个作用:

借助修饰符(Modifier),您可以修饰或扩充可组合项。您可以使用修饰符(Modifier)来执行以下操作:

  • 更改可组合项的大小、布局、行为和外观;
  • 添加信息,如无障碍标签;
  • 处理用户输入;
  • 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放;

比如,我们想给一个 Row 设置一个 10dp 的 padding,我们可以这么实现

1
2
3
scss复制代码Row(Modifier.padding(8.dp)) {

}

但是 Compose 中的 Modifier 是没有 margin 的概念的,我们在 Andriod 中使用 padding 和 margin 来设置内边距和外边距,通常是因为背景色的原因,margin 是外边距,它是不包含在背景之内的,而 padding 它是内边距;

我们在 Compose 中设置背景色的时候,也是通过 Modifier 来执行的,Modifier.background(),但是 Modifier 对调用顺序是有先后要求的,我们可以看下下面的这个例子,以及我们如果和通过 padding 来实现 margin 的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码Row(    
Modifier
.background(Color.Red)
.padding(10.dp)) {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp)
)

Text(
text = "我是老A",
Modifier.background(Color.Green).padding(10.dp)
)

Text(
text = "老A是我",
Modifier.padding(10.dp).background(Color.Green)
)
}

运行效果:

可以看到 我是老A 是把背景色包含进来了,而 老A是我 是不包含的,也是 Modifier 的一个特点,设置有先后;

这样,我们就只需要 padding 这一个函数就可以了;

background

我们看到,background 其实是有两个参数的,一个设置背景色,一个是 Shape,也就是形状,也就是说,我们可以给 background 设置一个形状,我们来看下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码Row(    
Modifier
.background(Color.Red, RoundedCornerShape(10.dp)) // 给背景设置一个圆角
.padding(10.dp)) {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.size(150.dp)
)

Text(
text = "我是老A",
Modifier.background(Color.Green).padding(10.dp)
)

Text(
text = "老A是我",
Modifier.padding(10.dp).background(Color.Green)
)
}

运行效果如下:

我们的 CardView,可以通过 Modifier 来实现;

clip(shape: Shape)

Modifier 还提供了一个切割函数 clip,可以切各种东西,例如可以切图片,我们看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码Row(    
Modifier
.background(Color.Red, RoundedCornerShape(10.dp)) // 给背景设置一个圆角
.padding(10.dp)) {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
// 将图片切成圆的
modifier = Modifier.clip(CircleShape).size(150.dp)
)

Text(
text = "我是老A",
Modifier.background(Color.Green).padding(10.dp)
)

Text(
text = "老A是我",
Modifier.padding(10.dp).background(Color.Green)
)
}

运行效果如下:

因为图片的左右是透明的,所以看不出来效果;已经把图片切割成圆的了;

layout_width & layout_height 的 Compose 平替 size

layout_width 可以用 width 来替换,layout_height 可以用 height 来替换,如果宽高一样的话,可以直接使用 size

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码Row(    
Modifier
.background(Color.Red, RoundedCornerShape(10.dp))
.width(400.dp) // 设置宽度
.height(800.dp) // 设置高度
.padding(10.dp)) {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.clip(CircleShape).size(150.dp)
)
Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp))
Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}

在 Android 中,我们需要强制给每个控件声明 width 和 height,但是在 Compose 中,这个并不是强制的了,因为每个组件都有一个默认规则,默认规则就是,如果你不写,那么它就相当于是传统 View 的 wrap_content 的了;

如果想使用类似 match_parent 的,则使用 Modifier.fillMaxWidth/ fillMaxHeight/ fillMaxSize 函数;

给 Text 设置颜色和大小

如果我们想给 Text 设置颜色和大小,可能第一时间我们就想到了使用 Modifier,但是,可能没有你想的那么简单,Modifier 并没有提供相关API,我们可以往上看 Text 的基础使用,发现其实 Text 的 API 提供了相关设置;

我们在声明 Text 的时候,直接使用 color 和 fontSize 来设置颜色和大小,而不用通过 Modifier,到这里的时候,可能大家就比较迷糊了,为啥还不统一呢,是不是 Jetpack Compose 的作者脑子没想好呢?然而并不是,因为 Compose 对于 Android 团队来说,是一次重要的革命,Android 团队其实在 Compose 上下了很大的功夫的;判断是 Modifier 还是函数参数,其实很简单:

**通用的设置方式,使用 Modifier;**专项的设置,使用函数参数;

给控件设置点击监听

Android 中其实任何控件都可以设置监听的,这其实可以看做通用的设置方式,也就是说可以通过 Modifier 来给任意的控件设置一个监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码Row(    
Modifier
.background(Color.Red, RoundedCornerShape(10.dp))
.width(400.dp)
.height(800.dp)
.padding(10.dp)
// 给 Row 设置一个监听
.clickable {
Toast.makeText(context, "我是 Row,我被点击了", Toast.LENGTH_LONG).show()
}) {
Image(
rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),
contentDescription = resources.getString(R.string.app_name),
modifier = Modifier.clip(CircleShape).size(150.dp)
)
Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp))
Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}

运行效果如下:

可以看到,点击事件被执行了;

好了,Compose 的初探,就先到这里吧~~

下一节预告

状态订阅和自动更新;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…636465…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%