关于源码分析:海外交友源码平台搭建基础功能的实现一

作为一名软件开发师,我深知源码平台的技术性能的重要性,明天我要分享的性能是利用海内交友源码去实现,这两个性能并不会引起咱们的特地关注,然而,当咱们在应用海内交友源码平台时,它们却时时刻刻陪伴着咱们。当咱们在应用海内交友源码平台去看视频或是直播时,有没有感觉过画面含糊,而如果想要去扭转画面含糊的状况,就须要去变换画面的品质,像是480p、720p、1080p、4k等就是常见的画面质量选项,没错,画面质量性能就是咱们第一个要讲的性能;第二个性能技术比画面质量性能还要离咱们更近,这个性能不只是海内交友源码平台领有,在各大程序APP中都会体现,在海内交友源码平台中,他经常会呈现在短视频评论区、直播交友互动区等,它以字符的模式让用户去相互传递信息,这个性能就是文字聊天性能。可能有些人就要问了,为什么海内交友源码平台要去实现这两个性能那?首先说画面质量性能,这个性能关系着海内源码平台的画面清晰与否,古代社会,画面质量始终在一直地翻新,让咱们在应用各种电子设备观看内容的时候都很清晰,甚至很渺小的细节都能看清,这也让大家曾经适应了高质量的画面,如果海内交友源码平台的画面质量很低,很含糊,会让大家不适应,不喜爱,这也就没人来应用,很快就会被市场所淘汰;再说文字聊天性能网络时代的倒退使文字聊天沟通能成为大家日常聊天的次要形式,不论是微信还是qq,文字聊天都是大部分人的抉择,还有对于直播来说,如果没有文字聊天性能,只有语音或者视频,那整个直播看直播的人数达到肯定的值的时候,就会特地的乱,不晓得去看谁,也不晓得谁在谈话,直播间可能就会解体,超负荷,而直播源码技术文字聊天性能就能缓解这一问题。讲完海内直播源码为什么要有画面质量和文字聊天性能,接下来,咱们讲这两个性能的实现:(局部代码)画面质量性能实现  文字聊天性能实现  总之,咱们在开发海内直播源码平台的时候,肯定要跟紧时代的潮流,要晓得市场的需要,源码技术性能还有许许多多,前面我会持续为大家分享,如果大家还有什么不懂能够问我。

June 19, 2023 · 1 min · jiezi

关于源码分析:成败关键一对一直播源码平台搭建需要的条件

网络时代的后退,人们对直播也有了新的要求,对于观众们来说,大多数观众更喜爱只让本人和主播进行交换,只有不仅仅能减少私密性,而且还能和本人喜爱的主播更加亲热实在,像是面对面一样;而对于主播而言,大部分主播都想让本人轻松许多,并且收益更高。而随着直播源码平台的倒退,一对一直播源码平台横空出世,它可能满足主播和观众的这些需要,所以一对一直播源码平台日渐火爆,很多人或者公司都想去开发一对一直播源码平台,要想去开发一对一直播源码平台,有很多重要的常识,像是我后面讲过的一对一直播源码技术性能常识,它就是开发一对一直播源码平台的重要组成部分,明天咱们来讲另一个重要组成部分:一对一直播源码平台搭建须要的条件! 首先一对一直播源码平台在开发过程中要留神的两点。第一点为模块,模块的回声打消,噪声克制,自动增益,丢帧弥补,前向纠错与网络抖动是在开发一对一直播源码要害的问题,所以在做这些时肯定要审慎。第二点为终端的兼容性:安卓端要想全面兼容就要去进行机型适配工作,而其中,最麻烦的就属摄像头适配,所以在做一对一直播源码开发时要有肯定的急躁。其次一对一直播源码要兼容Android和iOS两个终端。Android端:Java语言,应用Android Studio开发,IOS端:采纳OC语言 ,应用Xcode 工具开发,这些和一对多直播开发没有不同;通过流媒体服务器(CDN)实现内容散发,用户在进行拉流,通过设施对音视频流解码进行观看。一对一社交直播零碎的重点在于主播开播前的设置,即咱们该如何以最佳的形式实现一对一视频直播。 一对一直播源码平台的开发建设须要有足够的常识储备量,不仅仅要明确一对一直播源码的技术性能常识,其余常识也同样重要,就像是今台南我所讲到的一对一直播源码平台搭建所须要的条件,当然,我都会为大家分享这些常识,如果大家还有什么不懂的能够问我。

June 13, 2023 · 1 min · jiezi

关于源码分析:社交app源码技术屏幕的两大实用功能

在这个大部分人都是独生子的时代,很多人都会因为没有敌人或是在当地、亲人不在身边而孤单,这时候,很多人就会去抉择去社交app软件,这也促使了社交app源码搭建平台的火爆,然而要想搭建出一个令用户称心的社交app平台,就要去理解用户须要什么样的社交app源码技术性能,明天我要讲的也是用户须要的,对于屏幕的两大实用功能:屏幕共享与屏幕录制!上面就进入咱们明天的次要内容。首先咱们要去理解这两大性能为什么是用户须要的。在用户在社交app平台中意识到敌人时,可能会因为只能聊天,不能一起看电视剧、看电影而苦恼,这时候社交app源码技术屏幕共享性能就派上了用场,它能够将本人的屏幕上的内容实时投到敌人的屏幕上,这样两个人就能够一起看电影,看电视剧等等。当用户和敌人用社交app源码看电影或是电视剧看到精彩的中央的时候,可能想记录下来,这时候社交源码技术屏幕录制性能也派上了用场,它能够将屏幕中的内容实时录制并保留到本人的电子设备里,等到看完屏幕共享完结就能够发给对方,让彼此再次产生话题。综合来看,社交app源码技术屏幕共享与屏幕录制性能有利于用户社交,让用户和敌人更亲切,更有话题,这样也会吸引来更多的用户去应用本人开发的社交app源码平台。说完社交app源码技术屏幕共享与屏幕录制性能的用处,我再为大家去教大家如何去实现社交app源码技术屏幕共享与屏幕录制的实现:(局部代码)社交app源码屏幕共享技术实现,如下 社交app源码屏幕录制的实现,如下  这样,咱们就实现了社交app源码技术的屏幕共享与屏幕录制性能,他是社交app源码技术性能的重要组成部分,当然,社交app源码平台开发还有许许多多的技术性能,我会为大家一一分享,如果大家还有什么不懂的能够问我。

June 12, 2023 · 1 min · jiezi

关于源码分析:限速神器RateLimiter源码解析-京东云技术团队

作者:京东科技 李玉亮 目录指引 限流场景软件系统中个别有两种场景会用到限流: •场景一、高并发的用户端场景。 尤其是C端系统,常常面对海量用户申请,如不做限流,遇到霎时高并发的场景,则可能压垮零碎。 •场景二、外部交易解决场景。 如某类交易工作解决时有速率要求,再如上下游调用时上游对上游有速率要求。 •无论哪种场景,都须要对申请解决的速率进行限度,或者单个申请解决的速率绝对固定,或者批量申请的解决速率绝对固定,见下图: 罕用的限流算法有如下几种: •算法一、信号量算法。 保护最大的并发申请数(如连接数),当并发申请数达到阈值时报错或期待,如线程池。 •算法二、漏桶算法。 模仿一个按固定速率漏出的桶,当流入的申请量大于桶的容量时溢出。 •算法三、令牌桶算法。 以固定速率向桶内发放令牌。申请解决时,先从桶里获取令牌,只服务有令牌的申请。 本次要介绍的RateLimiter应用的是令牌桶算法。RateLimiter是google的guava包中的一个笨重限流组件,它次要有两个java类文件,RateLimiter.java和SmoothRateLimiter.java。两个类文件共有java代码301行、正文420行,正文比java代码还要多,写的十分具体,前面的介绍也有相干内容是翻译自其正文,有些形容英文原版更加精确清晰,有趣味的也能够联合原版正文进行更具体的理解。 应用介绍RateLimiter应用时只需引入guava jar便可,最新的版本是31.1-jre, 本文介绍的源码也是此版本。 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version> </dependency>源码中提供了两个直观的应用示例。 示例一、有一系列工作列表要提交执行,管制提交速率不超过每秒2个。 final RateLimiter rateLimiter = RateLimiter.create(2.0); // 创立一个每秒2个许可的RateLimiter对象. void submitTasks(List<Runnable> tasks, Executor executor) { for (Runnable task : tasks) { rateLimiter.acquire(); // 此处可能有期待 executor.execute(task); } }示例二、以不超过5kb/s的速率产生数据流。 final RateLimiter rateLimiter = RateLimiter.create(5000.0); // 创立一个每秒5k个许可的RateLimiter对象 void submitPacket(byte[] packet) { rateLimiter.acquire(packet.length); networkService.send(packet); }能够看出RateLimiter的应用非常简单,只须要结构限速器,调用获取许可办法便可,不须要开释许可. 算法介绍在介绍之前,先说一下RateLimiter中的几个名词: ...

May 16, 2023 · 5 min · jiezi

关于源码分析:H2存储内核分析一

开篇阐明当初做数据库个别都才有 C/C++ 获取其它编译型的语言,为什么会抉择 h2 这种基于 java 的语言?会不会影响效率?其实答复这个问题很简略,无论是用什么语言来实现数据库,其实都是在调用操作系统 IO 的函数。因而仅仅是作为存储的话差异其实是不大的。当初大多数,波及到存储内核的文章或者讲义,要么是一堆原理,要么就是玩具版本例子,根本无法利用到理论的工程下面去,就像马保国的闪电五连鞭一样。咱们抉择 h2 的一个重要起因就是,学习完后,能够间接利用到工程上。行不行间接在擂台上比一下就晓得了。MVStore 的根底办法1、创立一个 MVStore 的对象时,如果 fileName 设置为空示意纯内存模式。也就是说 MVStore 能够作为 redis 应用,当然性能会比 redis 还弱小。1.1、纯内存模式// 创立一个纯内存的 storeMVStore store = MVStore.open(null);1.2、磁盘模式// 文件存储地位String fileName = "/Users/chenfei/temp/my_store.db";// 创立一个 storeMVStore store = new MVStore.Builder().fileName(fileName).pageSplitSize(1000).open();1.3、应用 MVStore.Builder() 生成 MVStoreMVStore.Builder() 罕用办法 生成纯内存 store MVStore.Builder builder = new MVStore.Builder(); MVStore store = builder.open();fileName(String fileName):设置存储MVStore数据的文件名 String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); builder.fileName(fileName); MVStore store = builder.open();encryptionKey(char[] key):设置加密密钥,用于对MVStore的数据进行加密。如果不设置,则不进行加密。 String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); builder.encryptionKey("my_h2".toCharArray()); builder.fileName(fileName); MVStore store = builder.open();compress():开启压缩选项,用于将MVStore的数据进行压缩,以减小存储空间。默认不开启。 String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); builder.encryptionKey("my_h2".toCharArray()); /** * 应用 LZF 算法在写入之前压缩数据。这将节俭 * 大概 50% 的磁盘空间,但会减慢读写速度操作轻微 * */ builder.compress(); builder.fileName(fileName); MVStore store = builder.open();禁用主动提交事务,须要手动提交。默认开启主动提交事务。 String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); builder.encryptionKey("my_h2".toCharArray()); /** * 应用 LZF 算法在写入之前压缩数据。这将节俭 * 大概 50% 的磁盘空间,但会减慢读写速度操作轻微 * */ builder.compress(); // 禁用主动提交事务,须要手动提交。 builder.autoCommitDisabled(); builder.fileName(fileName); MVStore store = builder.open();设置MVStore为只读模式,不能进行写操作。 String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); builder.encryptionKey("my_h2".toCharArray()); /** * 应用 LZF 算法在写入之前压缩数据。这将节俭 * 大概 50% 的磁盘空间,但会减慢读写速度操作轻微 * */ builder.compress(); // 禁用主动提交事务,须要手动提交。 builder.autoCommitDisabled(); // 设置MVStore为只读模式,不能进行写操作。 builder.readOnly(); builder.fileName(fileName); MVStore store = builder.open();设置MVStore的缓存大小,单位为MB,默认为16MB。 String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); builder.encryptionKey("my_h2".toCharArray()); /** * 应用 LZF 算法在写入之前压缩数据。这将节俭 * 大概 50% 的磁盘空间,但会减慢读写速度操作轻微 * */ builder.compress(); // 禁用主动提交事务,须要手动提交。 builder.autoCommitDisabled(); // 设置MVStore为只读模式,不能进行写操作。 // builder.readOnly(); // 设置MVStore的缓存为 8MB,默认为16MB builder.cacheSize(8); builder.fileName(fileName); MVStore store = builder.open();pageSplitSize(int pageSplitSize):数据页的大小是通过pageSplitSize办法进行设置的,默认值为4KB。**MVStore应用了数据页的概念来治理存储的数据,将较大的数据文件拆分成多个小的数据页,以进步性能。每个数据页的大小是通过pageSplitSize办法进行设置的,默认值为4KB。当MVStore在写入数据时,首先会将数据写入内存缓存中,当缓存中的数据达到肯定大小后,会将数据刷新到磁盘上,并拆分成多个数据页。如果数据大小超过了pageSplitSize的设置值,则会拆分成多个数据页。因而,pageSplitSize的设置值会影响数据拆分的粒度,进而影响MVStore的性能。通常状况下,pageSplitSize的默认值能够满足大部分利用的须要。如果须要调整MVStore的性能,能够依据理论状况适当调整pageSplitSize的值。须要留神的是,pageSplitSize的值必须是2的幂次方。** String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); builder.encryptionKey("my_h2".toCharArray()); /** * 应用 LZF 算法在写入之前压缩数据。这将节俭 * 大概 50% 的磁盘空间,但会减慢读写速度操作轻微 * */ builder.compress(); // 禁用主动提交事务,须要手动提交。 builder.autoCommitDisabled(); // 设置MVStore为只读模式,不能进行写操作。 // builder.readOnly(); builder.pageSplitSize(500); builder.fileName(fileName); MVStore store = builder.open();open():应用builder中的配置选项创立MVStore实例。 String fileName = "/Users/chenfei/temp/my_store.db"; MVStore.Builder builder = new MVStore.Builder(); MVStore store = builder.open();生成MVStore实例的过程如果是纯内存模式,它的 file header 就为空。只有是磁盘模式的时候才有 file header。 ...

March 27, 2023 · 2 min · jiezi

关于源码分析:TiKV-源码阅读三部曲一重要模块

作者简介:谭新宇,清华大学软件学院研三在读,Apache IoTDB committer,Talent Plan Community mentor。 TiKV 是一个反对事务的分布式 Key-Value 数据库,目前曾经是 CNCF 基金会 的顶级我的项目。 作为一个新同学,须要肯定的后期筹备才可能有能力参加 TiKV 社区的代码开发,包含但不限于学习 Rust 语言,了解 TiKV 的原理和在前两者的根底上理解相熟 TiKV 的源码。 TiKV 官网源码解析文档 具体地介绍了 TiKV 3.x 版本重要模块的设计要点,次要流程和相应代码片段,是学习 TiKV 源码必读的学习材料。以后 TiKV 曾经迭代到了 6.x 版本,不仅引入了很多新的性能和优化,而且对源码也进行了屡次重构,因此一些官网源码解析文档中的代码片段曾经不复存在,这使得读者在浏览源码解析文档时无奈对照最新源码加深了解;此外只管 TiKV 官网源码解析文档系统地介绍了若干重要模块的工作,但并没有将读写流程全链路串起来去介绍通过的模块和对应的代码片段,实际上尽快地相熟读写流程全链路会更利于新同学从全局角度了解代码。 基于以上存在的问题,笔者将基于 6.1 版本的源码撰写三篇博客,别离介绍以下三个方面: TiKV 源码浏览三部曲(一)重要模块:TiKV 的基本概念,TiKV 读写门路上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的计划TiKV 源码浏览三部曲(二)读流程:TiKV 中一条读申请的全链路流程TiKV 源码浏览三部曲(三)写流程:TiKV 中一条写申请的全链路流程心愿此三篇博客可能帮忙对 TiKV 开发感兴趣的新同学尽快理解 TiKV 的 codebase。 本文为第一篇博客,将次要介绍 TiKV 的基本概念,TiKV 读写门路上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的计划。 基本概念TiKV 的架构简介能够查看 官网文档。总体来看,TiKV 是一个通过 Multi-Raft 实现的分布式 KV 数据库。 TiKV 的每个过程领有一个 store,store 中领有若干 region。每个 region 是一个 raft 组,会存在于正本数个 store 上治理一段 KV 区间的数据。 ...

October 18, 2022 · 16 min · jiezi

关于源码分析:Databend-源码阅读系列一-开篇

前言Databend 在 2021 年开源后,陆续受到了很多社区同学的关注。Databend 应用了 Rust 编程语言。为了吸引更多的开发者,特地是没有 Rust 开发教训的新同志,咱们设计了 Rust 相干课程,同时建设了多个 Rust 兴趣小组。 Databend 在 issue 中还引入了“Good First issue”的 label 来疏导社区新同学参加第一次奉献,目共有超过一百多位 contributors,算是一个不错的成绩。但 Databend 也在过来的一年中经验了数次迭代,代码日渐简单。目前代码骨干分支有 26 w 行 rust 代码,46 个 crate,对于新接触 Databend 的技术爱好者来说,奉献门槛越来越高。即便是相熟 rust 的同学,clone 代码后,面对着茫茫码海,竟不知如何读起。在多个社区群中,也有敌人数次提到什么时候能有一个 Databend 源码浏览系列文章,帮忙大家更快相熟 Databend 代码。 因而,咱们接下来会发展“Databend 源码浏览”系列文章,次要受众是社区技术开发者,心愿通过源码浏览,来增强和社区的技术交换,引发更多思维碰撞。Databend 的故事 Databend 的故事很多同学都问过咱们一个问题:为什么你们要用 Rust 从零构建一个数据库?其实这个问题能够分为两个子问题: 1.为什么抉择的是 Rust? 答:咱们晚期的成员大多是 ClickHouse、tidb 、tokudb 等出名数据库的贡献者,从技术栈来说更相熟的是 C++ 和 Go。虎哥@bohutang 在疫情期间也应用 Go 实现了一个小的数据库原型 vectorsql有同学示意 vectorsql 的架构十分优雅,值得学习借鉴。 语言本没有孰劣之分,要从面向的场景来聊聊。目前大多的 DMBS 应用的是 C++/Java,新型的 NewSQL 更多应用的是 Go。在以往的开发教训来看,C/C++ 曾经是高性能的代名词,开发者更容易写出高运行效率的代码,但 C++ 的开发效率切实不忍直视,工具链不是很欠缺,开发者很难一次性写出内存平安,并发平安的代码。而 Go 可能是另外一个极其,大道至简,工具链欠缺,开发效率十分高,不足之处在于泛型的进度太慢了,在 DB 零碎上内存不能很灵便的管制,且难于达到前者的运行性能,尤其应用 SIMD 指令还须要和汇编代码交互等。咱们须要的是兼具 开发效率(内存平安,并发平安,工具链欠缺)& 运行效率 的语言,过后看来,Rust 可能是咱们惟一的抉择了,历经尝试后,咱们也发现,Rust 不仅能满足咱们的需要,而且很酷! ...

August 4, 2022 · 2 min · jiezi

关于源码分析:OneFlow源码解析OpKernel与解释器

撰文|郑建华更新|赵露阳 1 Op与Kernel的注册持续追踪执行流程会发现,ReluFunctor在结构UserOpExpr时会用到UserOpRegistryMgr治理的Op与Kernel。Op示意算子的形容信息,Kernel在不同设施上实现计算。 注册信息保留在公有的map变量中。UserOpRegistryMgr的头文件(https://github.com/Oneflow-In...)中定义了3个宏,REGISTER_USER_OP、REGISTER_USER_OP_GRAD、REGISTER_USER_KERNEL别离用于注册op、grad_op、kernel。 1.1 ReluOp的注册 REGISTER_USER_OP负责UserOp的注册。通过检索代码能够找到这个宏的应用场景。ReluOp相干的源代码在这3个文件中: class定义:build/oneflow/core/framework/op_generated.h注册op、op的局部实现:build/oneflow/core/framework/op_generated.cpp次要实现:oneflow/oneflow/user/ops/relu_op.cppREGISTER_USER_OP宏在op_generated.cpp中开展后代码如下: static UserOpRegisterTrigger<OpRegistry> g_register_trigger715 = ::oneflow::user_op::UserOpRegistryMgr::Get() .CheckAndGetOpRegistry("relu") .Input("x") .Output("y") .SetGetSbpFn(&ReluOp::GetSbp) .SetLogicalTensorDescInferFn(&ReluOp::InferLogicalTensorDesc) .SetPhysicalTensorDescInferFn(&ReluOp::InferPhysicalTensorDesc) .SetDataTypeInferFn(&ReluOp::InferDataType);调用流程如下:  CheckAndGetOpRegistry(https://github.com/Oneflow-In...)会创立一个OpRegistry(https://github.com/Oneflow-In...)对象,这个类和UserOpRegisterTrigger(https://github.com/Oneflow-In...)类一样,只是为结构OpRegistryResult(https://github.com/Oneflow-In...)用的两头类型。 OpRegistry会暂存两头后果并在Finish中设置一些默认推导逻辑。UserOpRegisterTrigger的构造函数会调用注册逻辑。动态变量就是为了触发构造函数从而调用注册逻辑,将结构好的OpRegistryResult保留到UserOpRegistryMgr(https://github.com/Oneflow-In...)(key是op_type,如relu)。 ReluOp示意一个具体的op_type,负责为OpRegistryResult提供Op特有的办法。 OpRegistryResult把不同的Op形象为一个通用的构造(便于对立注册治理),次要蕴含形容信息,保留了op的输入输出形容,以及数据类型、sbp等的推导逻辑函数。对于relu来说,次要是记录了几个推导函数要调用ReluOp的静态方法;op_def次要蕴含input/output的名字。 1.2 ReluKernel的注册 ReluKernel在relu_kernel.cpp中注册,过程和Op的注册相似。REGISTER_USER_KERNEL宏产开后如下所示: static UserOpRegisterTrigger<OpKernelRegistry> g_register_trigger0 = UserOpRegistryMgr::Get(). CheckAndGetOpKernelRegistry("relu"). .SetCreateFn(...) .SetIsMatchedHob(UnaryPrimitiveExists(ep::primitive::UnaryOp::kRelu, "y", "x")) .SetInplaceProposalFn([](const user_op::InferContext&, const user_op::AddInplaceArgPair& AddInplaceArgPairFn) -> Maybe<void> { OF_RETURN_IF_ERROR(AddInplaceArgPairFn("y", 0, "x", 0, true)); return Maybe<void>::Ok(); });留神SetCreateFn只是把一个如下的lambda表达式赋值给result_.create_fn,这个字段很重要,后续执行就是通过它获取kernel。 []() { return user_op::NewOpKernel<UnaryPrimitiveKernel>( "y", "x", [](user_op::KernelComputeContext* ctx) { const user_op::TensorDesc* src = ctx->TensorDesc4ArgNameAndIndex("x", 0); const user_op::TensorDesc* dst = ctx->TensorDesc4ArgNameAndIndex("y", 0); return ep::primitive::NewPrimitive<ep::primitive::ElementwiseUnaryFactory>( ctx->device_type(), ep::primitive::UnaryOp::kRelu, src->data_type(), dst->data_type()); });}对于relu来说,NewOpKernel就是new一个UnaryPrimitiveKernel对象并返回函数指针。最终注册的后果,会把OpKernelRegistryResult保留到UserOpRegistryMgr(key是op_type_name,如"relu")。 1.3 Op和Kernel注册相干的类关系图 ...

August 1, 2022 · 3 min · jiezi

关于源码分析:悬赏任务源码开源威客系统网站源码部署教程

 威客悬赏工作公布零碎源码是用来进行日常在线工作接单解决的威客零碎。零碎能够用来公布或解决悬赏工作,甚至能够晓得一个帐户的信息,如工作类型和解决状态等,它们很不便,易于应用,它容许雇主和威客执行疾速自助交易。 残缺源码:wk.wxlbyx.icu 在本文中,咱们将探讨用c++编写的开源威客平台零碎,它是一个为用户提供理论工作公布零碎源码所应该具备的各个方面的利用接口。它是一个菜单驱动的架构,包含: 1、威客注册登录页面; 2、显示正在进行交易悬赏工作; 3、雇主账户管理系统; 4、充值和体现零碎; 5、工作公布和接单解决零碎; 6、接单工作投诉和反馈解决零碎。 办法:这个源码应用了类的基本概念,PHP中的Access Modifiers、数据类型、变量和Switch Case等。以下是将要实现的性能: ●setvalue():这个函数在这里应用c++中的根本输出和输入办法来设置数据,即cout和cin语句,它们别离显示和承受来自键盘的输出,即来自用户的输出。 ●showvalue():用于打印数据。 ●deposit():这个函数帮忙将钱存入特定的账户。 ●showbal():该函数显示贷款后可用的总余额。 ●withdrawl(): 这个性能有助于从帐户中提款。 ●main():这个函数在有限while循环中有一个简略的切换状况(做出抉择),这样每次用户都能够抉择选项。 上面是应用上述办法的PHP程序: // Management System #include <iostream> #include <stdlib.h> #include <string.h> using namespace std; class Bank { // Private variables used inside class private: string name; int accnumber; char type[10]; int amount = 0; int tot = 0; // Public variables public: // Function to set the person's data void setvalue() { cout << "Enter name\n"; cin.ignore(); // To use space in string getline(cin, name); cout << "Enter Account number\n"; cin >> accnumber; cout << "Enter Account type\n"; cin >> type; cout << "Enter Balance\n"; cin >> tot; } // Function to display the required data void showdata() { cout << "Name:" << name << endl; cout << "Account No:" << accnumber << endl; cout << "Account type:" << type << endl; cout << "Balance:" << tot << endl; } // Function to deposit the amount in ATM void deposit() { cout << "\nEnter amount to be Deposited\n"; cin >> amount; } // Function to show the balance amount void showbal() { tot = tot + amount; cout << "\nTotal balance is: " << tot; } // Function to withdraw the amount in ATM void withdrawl() { int a, avai_balance; cout << "Enter amount to withdraw\n"; cin >> a; avai_balance = tot - a; cout << "Available Balance is" << avai_balance; } }; // Driver Code int main() { // Object of class Bank b; int choice; // Infinite while loop to choose // options everytime while (1) { cout << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~" << "~~~~~~~~~~~~~~~~~~~~~~~~~~~~" << "~~~WELCOME~~~~~~~~~~~~~~~~~~" << "~~~~~~~~~~~~~~~~~~~~~~~~~~~~" << "~~~~~~~~~\n\n"; cout << "Enter Your Choice\n"; cout << "\t1. Enter name, Account " << "number, Account type\n"; cout << "\t2. Balance Enquiry\n"; cout << "\t3. Deposit Money\n"; cout << "\t4. Show Total balance\n"; cout << "\t5. Withdraw Money\n"; cout << "\t6. Cancel\n"; cin >> choice; // Choices to select from switch (choice) { case 1: b.setvalue(); break; case 2: b.showdata(); break; case 3: b.deposit(); break; case 4: b.showbal(); break; case 5: b.withdrawl(); break; case 6: exit(1); break; default: cout << "\nInvalid choice\n"; } } } 输入: 显示威客工作抉择: ...

May 11, 2022 · 2 min · jiezi

关于源码分析:SOFARegistry-源码|数据分片之核心路由表-SlotTable-剖析

文|程征征(花名:泽睿 ) 高德软件开发工程师 负责高德新场景业务摸索开发与保护 对畛域驱动、网络通讯、数据一致性有肯定的钻研与实际 本文 23009字 浏览约 25 分钟 第一次关注 SOFA 社区是在开发一个故障剔除组件时,发现 SOFARPC 中也有相似的组件。在 SOFARPC 的设计中,入口采纳了一种无缝插入的设计形式,使得在不毁坏凋谢关闭准则前提下,引入单机故障剔除能力。并且是基于内核设计和总线设计,做到可插拔、零侵入,整个故障剔除模块是通过 SPI 动静加载的。统计信息的收集也是通过事件驱动的形式,在 RPC 同步或异步调用实现后,会向事件总线 EventBus 发送对应事件。事件总线接管到对应的事件,以执行后续的故障剔除逻辑。 基于以上优良的设计,我也将其纳为己用,也因而开启了在 SOFA 社区的开源摸索之路。陆续钻研了 SOFABoot、SOFARPC 以及 MOSN 等,自我感觉每一个我的项目的代码程度都很高,对我本人的代码晋升有很大的帮忙。 SOFARegistry 是一个开源的注册核心提供了服务的公布注册订阅等性能,反对海量的服务注册订阅申请。作为一个名源码爱好者,尽管看过 SOFA 的架构文章大抵理解其中的设计哲学,然而因为没有从代码中理解过细节,实际上也是只知其一;不知其二。恰好借助 SOFARegistry 开拓的源码剖析流动,基于本人的趣味抉择了 SlotTable 这个工作。 SOFARegistry 对于服务数据是分片进行存储的,因而每一个 data server 只会承当一部分的服务数据,具体哪份数据存储在哪个 data server 是有一个称为 SlotTable 的路由表提供的,session 能够通过 SlotTable 对对应的 data derver 进行读写服务数据, slot 对应的 data follower 能够通过 SlotTable 寻址 leader 进行数据同步。 保护 SlotTable 是由 Meta 的 leader 负责的,Meta 会保护 data 的列表,会利用这份列表以及 data 上报的监控数据创立 SlotTable,后续 data 的高低线会触发 Meta 批改 SlotTable, SlotTable 会通过心跳分发给集群中各个节点。 ...

April 19, 2022 · 11 min · jiezi

关于源码分析:HAVE-FUN|Layotto-源码解析

对于 Layotto 源码解析系列Layotto 源码解析流动是由 SOFAStack 团队主办的开源流动,咱们心愿打造一个人人皆可参加,基于 GitHub 合作的踊跃通明的开源流动。 本次流动旨在加强大家对 Layotto 的理解与认知,促成开源社区的交换,让大家更好的理解、学习和应用开源我的项目,是大家学习和应用 Layotto,与 Layotto 的外围开发者间接交换的一个良好契机。 本次流动所产出的文章将首先发表在 Layotto 我的项目主页上,同时也将会进行线上全渠道的推广,经整顿后的局部内容会作为 SOFAStack 官网博客中,并会在全渠道进行公布。 流动角色划分发起人: 负责经营合作 参与者: 所有对社区我的项目感兴趣的开发者。 Reviewer: 我的项目外围开发者,在源码解析中给予领导和倡议。 seefloodwenxuwanzhenjunmaMoonShiningstulzqZLBerReviewer 既是审稿人也负责 mentor 的角色,是 Layotto 的外围开发者。流动流程流动在 GitHub 上进行合作。流程图如下: 参与者登陆本人的 GitHub 账号,在源码解析流动的 GitHub 页面回复【/assign】认领 issue。Reviewer 指派 issue 给对应的参与者。参与者在认领 issue 胜利后在规定工夫内提交 PR。Reviewer 对提交的 PR 进行 Review。PR 审核通过后,由 Reviewer 进行公布在我的项目主页中。参与者敞开 issue。规定阐明一人一 issue每位参与者一次最多只能够认领一个 issue,如错领 issue 等,需先敞开已领 issue 再进行从新认领。一人可认领实现屡次。 工作分级本次工作难度分为 3 个等级 、的工作为初阶 的星为中阶 、 的星为高阶。 ...

March 30, 2022 · 1 min · jiezi

关于源码分析:Spring的bean加载流程

Spring bean的加载Spring的bean加载Spring的容器架构[Spring容器架构]finishBeanFactoryInitialization(),正文下面写着 **Instantiate all remaining (non-lazy-init) singletons**,意味着非提早加载的类,将在这一步实例化,实现类的加载。 加载流程: 从开始的getbean入口进行剖析 ApplicationContext context = new ClassPathXmlApplicationContext("配置文件xml"); context.getBean("bean名字");1、先获取bean,调用AbstractBeanFactory的doGetBeandoGetBean有四个参数: name:bean的名称 requiredType: 返回的类型 args: 传递的结构参数 typeCheckOnly: 查看类型 protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { //获取beanName,这边有三种模式,一个是原始的beanName,一个是加了&的,一个是别名 final String beanName = transformedBeanName(name); Object bean; // Eagerly check singleton cache for manually registered singletons. // 是否曾经创立了 Object sharedInstance = getSingleton(beanName); //曾经创立了,且没有结构参数,进入这个办法,如果有结构参数,往else走,也就是说不从获取bean,而间接创立bean if (sharedInstance != null && args == null) { if (logger.isTraceEnabled()) { if (isSingletonCurrentlyInCreation(beanName)) { logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); } else { logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); } } // 如果是一般bean,间接返回,是FactoryBean,返回他的getObject bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { // Fail if we're already creating this bean instance: // We're assumably within a circular reference. // 没创立过bean或者是多例的状况或者有参数的状况 // 创立过Prototype的bean,会循环援用,抛出异样,单例才尝试解决循环依赖的问题 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } // Check if bean definition exists in this factory. BeanFactory parentBeanFactory = getParentBeanFactory(); // 父容器存在,本地没有以后beanName,从父容器取 if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { // Not found -> check parent. // 解决后,如果是加&,就补上& String nameToLookup = originalBeanName(name); if (parentBeanFactory instanceof AbstractBeanFactory) { return ((AbstractBeanFactory) parentBeanFactory).doGetBean( nameToLookup, requiredType, args, typeCheckOnly); } else if (args != null) { // Delegation to parent with explicit args. return (T) parentBeanFactory.getBean(nameToLookup, args); } else if (requiredType != null) { // No args -> delegate to standard getBean method. return parentBeanFactory.getBean(nameToLookup, requiredType); } else { return (T) parentBeanFactory.getBean(nameToLookup); } } if (!typeCheckOnly) { // typeCheckOnly为false,将beanName放入alreadyCreated中 markBeanAsCreated(beanName); } try { // 获取BeanDefinition final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); // 抽象类查看 checkMergedBeanDefinition(mbd, beanName, args); // Guarantee initialization of beans that the current bean depends on. // 如果有依赖的状况,先初始化依赖的bean String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dep : dependsOn) { // 查看是否循环依赖,a依赖b,b依赖a。包含传递的依赖,比方a依赖b,b依赖c,c依赖a if (isDependent(beanName, dep)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); } // 注册依赖关系 registerDependentBean(dep, beanName); try { // 初始化依赖的bean getBean(dep); } catch (NoSuchBeanDefinitionException ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", ex); } } } // Create bean instance. // 如果是单例 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { // 创立bean return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } }); // 如果是一般bean,间接返回,是FactoryBean,返回他的getObject bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } else if (mbd.isPrototype()) { // It's a prototype -> create a new instance. Object prototypeInstance = null; try { // 退出prototypesCurrentlyInCreation,阐明正在创立 beforePrototypeCreation(beanName); //创立bean prototypeInstance = createBean(beanName, mbd, args); } finally { // 移除prototypesCurrentlyInCreation,阐明曾经创立完结 afterPrototypeCreation(beanName); } // 如果是一般bean,间接返回,是FactoryBean,返回他的getObject bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } else { String scopeName = mbd.getScope(); final Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); } try { Object scopedInstance = scope.get(beanName, () -> { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider " + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex); } } } catch (BeansException ex) { cleanupAfterBeanCreationFailure(beanName); throw ex; } } // Check if required type matches the type of the actual bean instance. if (requiredType != null && !requiredType.isInstance(bean)) { try { T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); if (convertedBean == null) { throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } return convertedBean; } catch (TypeMismatchException ex) { if (logger.isTraceEnabled()) { logger.trace("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", ex); } throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } } return (T) bean;}2、获取beanName而后转换名称解析完配置后创立的 Map,应用的是 beanName 作为 key。见 DefaultListableBeanFactory: ...

March 15, 2022 · 14 min · jiezi

关于源码分析:HAVE-FUN-SOFARegistry-源码解析

对于 SOFARegistry 源码解析系列SOFARegistry 源码解析流动是由 SOFAStack 团队主办的开源流动,咱们心愿打造一个人人皆可参加,基于 GitHub 合作的踊跃通明的开源流动。 本次流动旨在加强大家对 SOFARegistry 的理解与认知,促成开源社区的交换,让大家更好的理解、学习和应用开源我的项目,是大家学习和应用 SOFARegistry,与 SOFARegistry 的外围开发者间接交换的一个良好契机。 本次流动所产出的文章将首先发表在 SOFARegistry 我的项目主页上,同时也将会进行线上全渠道的推广,经整顿后的局部内容会作为 SOFAStack 官网博客中,并会在全渠道进行公布。 流动角色划分发起人:负责经营合作参与者:所有对社区我的项目感兴趣的开发者。Reviewer:dzdx,我的项目外围开发者,在源码解析中给予领导和倡议。Reviewer 既是审稿人也负责 mentor 的角色,是 SOFARegistry 的外围开发者。流动流程流动在 GitHub 上进行合作。流程图如下: 参与者登陆本人的 GitHub 账号,在源码解析流动的 GitHub 页面回复【/assign】认领 issue。Reviewer 指派 issue 给对应的参与者。参与者在认领 issue 胜利后在规定工夫内提交 PR。Reviewer 对提交的 PR 进行 Review。PR 审核通过后,由 Reviewer 进行公布在我的项目主页中。参与者敞开 issue。规定阐明一人一 issue每位参与者一次最多只能够认领一个 issue,如错领 issue 等,需先敞开已领 issue 再进行从新认领。一人可认领实现屡次。 工作分级本次工作难度分为 3 个等级 、的工作为初阶 的星为中阶 、 的星为高阶。 issue 提交期限初阶 issue 认领 7 天内提交中阶 issue 认领 15 天内提交高阶 issue 认领 20 天内提交如过期未提交将视为放弃该 issue,issue 将会从新进行调配认领。 ...

March 9, 2022 · 1 min · jiezi

关于源码分析:mybatis框架下一二级缓存

上篇文章提到查问时会用到缓存,其内置的两级缓存如下: // 一级缓存,在executor中,与sqlsession绑定// org.apache.ibatis.executor.BaseExecutor#localCache// 指向org.apache.ibatis.cache.impl.PerpetualCache#cacheprivate Map<Object, Object> cache = new HashMap<>();// 二级缓存,在MappedStatement中(对应mapper.xml中的一个crud办法),周期与SqlSessionFactory统一org.apache.ibatis.mapping.MappedStatement#cache// 最终也指向了org.apache.ibatis.cache.impl.PerpetualCache#cacheprivate Map<Object, Object> cache = new HashMap<>();一、二级缓存都是查问缓存,select写入,insert、update、delete则革除一、二级缓存均指向org.apache.ibatis.cache.impl.PerpetualCache#cache,实质是一个HashMap一、二级缓存Key的计算形式统一,均指向org.apache.ibatis.executor.BaseExecutor#createCacheKey,Key的实质:statement的id + offset + limit + sql + param参数一级缓存生命周期和SqlSession统一,默认开启;二级缓存申明周期和SqlSessionFactory统一,需手动开启雷同namespace应用同一个二级缓存;二级缓存和事务关联,事务提交数据才会写入缓存,事务回滚则不会写入接下来通过源码别离来看一下。 一级缓存一级缓存的生命周期是sqlSession;在同一sqlSession中,用雷同sql和查问条件屡次查问DB状况,非首次查问会命中一级缓存。 一级缓存默认是开启的,如果想敞开须要减少配置 // == 如果不设置,默认是SESSION(后续的源码剖析会波及这里)<setting name="localCacheScope" value="STATEMENT"/>以查询方法作为入口 org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // == 计算CacheKey CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // == 查问中应用缓存 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}CacheKey计算org.apache.ibatis.executor.BaseExecutor#createCacheKeyCacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { CacheKey cacheKey = new CacheKey(); // == 调用update办法批改cache cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); // value是参数 cacheKey.update(value); return cacheKey;}从这里就能够猜测到,CacheKey和statement的id、offset、limit、sql、param参数无关。 ...

March 1, 2022 · 4 min · jiezi

关于源码分析:OKio源码分析

本篇文章次要剖析Okio读写流程以及超时检测机制。首先会介绍Okio中几个重要的类,而后提供一段用Okio api 实现读写文件代码,依据这段代码进行整体读写流程剖析,以及剖析Okio为什么比间接应用Java io 高效,最初介绍了在读写时Okio如何进行超时检测。1.OKio介绍Okio作为Okhttp底层io库,它补充了java.io和java.nio的有余,使拜访、存储和解决数据更加容易。Okio中几个重要的类介绍 <font color='red'> ByteString </font> 是不可变的字节序列。对于字符数据,最根本的就是String。而ByteString就像是String的兄弟个别,它使得将二进制数据作为一个变量值变得容易。这个类很聪慧:它晓得如何将本人编码和解码为十六进制、base64和utf-8。<font color='red'> Segment </font> Segment在Okio中作为数据缓冲的载体,一个Segment的数据缓冲大小为8192,即8k。每一个Segment都有前驱和后继结点,也就是说Sement是一个双向链表链表,精确的来说是一个双向循环链表。读取数据从Segment头结点读取写数据从Segment尾结点写。Okio中引入池的概念也就是源码中SegmentPool的实现。SegmentPool负责Segment创立和销毁,SegmentPool最大能够缓存8个Segment。<font color='red'> Buffer </font> 是一个可变的字节序列。像Arraylist一样。得益于它的底层由Segment实现因而你不须要事后设置缓冲区的大小,当你将数据从一个缓冲区挪动到另一个缓冲区时,它会重新分配Segment的持有关系,而不是跨Segment复制数据。其中Buffer实现了BufferedSource和BufferedSink,同时具读写性能。<font color='red'> Sources </font> 相似于java中的InputStream,Source作为Okio中读取数据的顶层接口只提供了简略的api long read(Buffer sink, long byteCount) throws IOException;Timeout timeout();void close() throws IOException;更多读取api由它的子接口BufferedSource提供,实现类为RealBufferdSource,底层InputStream->Buffer,而后基于Buffer的读取。 <font color='red'> Sink </font> 相似于java中的OutPutStream,Sink作为Okio中写入数据的顶层接口也只提供了简略的api void write(Buffer source, long byteCount) throws IOException;void flush() throws IOException;Timeout timeout();void close() throws IOException;更多写入api由它的子接口BufferedSink提供,实现类为RealBufferedSink,底层将数据写入到Buffer,再由Buffer写入到OutPutStream中。 这里省略了GzipSource,GzipSink,HashingSink,HashingSource...等其余实现Source和Sink的类,只关注主流程。 依据后面介绍和UML图得悉,数据的读写在RealBufferedSource和RealBufferedSink中实现 2.Okio读写流程作为一个简略切入点,这里提供一段Okio实现的输出流写入到指定文件的代码。 /*** * 将字节输出流写入到指定文件中 * @return true 写入胜利,false 写入失败 */ fun copy(inputStream: InputStream, dest: File): Boolean { val source = Okio.buffer(Okio.source(inputStream)) val sink = Okio.buffer(Okio.sink(dest)) val buffer = Buffer() return try { var length = source.read(buffer, 8192L) while (-1L != length) { sink.write(buffer, length) sink.flush() length = source.read(buffer, 8192L) } true } catch (e: Exception) { e.printStackTrace() false } finally { source.close() sink.close() } }Okio.source(inputStream)实现了对InputStream的包装,将InputStream包装在Source对象中并返回。 ...

February 28, 2022 · 7 min · jiezi

关于源码分析:JUC一图看懂ReentrantLock加解锁逻辑

应用样例 ThreadA、ThreadB、ThreadC拜访如下逻辑ReentrantLock lock = new ReentrantLock();// == 1.加锁lock.lock();...省略业务解决...// == 2.开释lock.unlock();非偏心加锁过程偏心形式,无ThreadD局部逻辑,会间接入队 后续都在具体解释这张图一、非偏心加锁1.状态批改// ## 状态:拜访线程会采纳cas的形式批改state的值,加锁过程0->1private volatile int state;// ## 持有线程:state批改胜利的线程,将被记录。比方,exclusiveOwnerThread=ThreadAprivate transient Thread exclusiveOwnerThread;# NonfairSync 非偏心实现final void lock() { // == 1.cas 批改state状态 0->1(插队1) if (compareAndSetState(0, 1)) // state批改胜利批改持有线程 exclusiveOwnerThread = ThreadA setExclusiveOwnerThread(Thread.currentThread()); else // == 2.构建队列,并阻塞线程 acquire(1);}2.队列构建### public final void acquire(int arg) { // a-尝试获取,尝试批改state状态(未获取胜利持续后续逻辑) if (!tryAcquire(arg) // b2-排队获取 && acquireQueued( // b1-新增期待节点,构建“独占”模式队列 addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}a-尝试获取(可能插队的地位)java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquirejava.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquirefinal boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // ## 插队地位 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // ## 重入,state++ else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;}b1-新增期待节点,构建“独占”模式队列class Node { /** 独占 */ static final Node EXCLUSIVE = null; // 指向线程 volatile Thread thread; volatile Node prev; volatile Node next; static final int SIGNAL = -1;java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiterprivate Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // == 2.队列不为空,节点尾插 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // == 1.队列初始构建 enq(node); return node; // 返回尾节点}//== 1.队列初始构建java.util.concurrent.locks.AbstractQueuedSynchronizer#enqprivate Node enq(final Node node) { for (;;) { Node t = tail; // -- A、初始化构建,头尾指针指向空Node if (t == null) { if (compareAndSetHead(new Node())) tail = head; } // -- B、尾插 else { node.prev = t; // cas 批改尾节点指向 if (compareAndSetTail(t, node)) { t.next = node; return t; // 返回头节点 } } }}b2-排队获取java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueuedfinal boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 循环中 for (;;) { final Node p = node.predecessor(); // ### 前置节点是头节点,有机会尝试获取 //(联合下一个if判断,会自旋两次,也就是说有两次尝试获取机会) if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } // ### 第1次将waitstatus设置成signal返回false // ### 第2次判断waitstatus==signal返回true if (shouldParkAfterFailedAcquire(p, node) // === 线程阻塞(将来唤醒时,从此处继续执行) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}###private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // -- 第二次调用 if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } // -- 第一次调用 else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}===private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 以后线程是否被中断 return Thread.interrupted();}二、开释java.util.concurrent.locks.ReentrantLock#unlockjava.util.concurrent.locks.AbstractQueuedSynchronizer#release{ // == 1.state还原,exclusiveOwnerThread清空 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // == 2.“解除阻塞”执行胜利的节点 unparkSuccessor(h); return true; } return false;}1.state还原,exclusiveOwnerThread清空protected final boolean tryRelease(int releases) { // 加锁时线程重入,state++。因而解锁时,state-- int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // state归0时,开释线程援用 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free;}2.“解除阻塞”执行胜利的节点private void unparkSuccessor(Node node) { int ws = node.waitStatus; // 加锁时,ws=SIGNAL,也就是-1。当初改成0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } // ## 开释s节点,也就是head的下一个节点 if (s != null) LockSupport.unpark(s.thread);}三、偏心加锁差异一# FairSync 偏心实现final void lock() { // 无插队操作,间接构建队列 acquire(1);}再比照下刚刚的非偏心实现,只有else局部 ...

January 26, 2022 · 4 min · jiezi

关于源码分析:万字整理MyBatis源码

MyBatis差不多在我刚学编程就始终在用,始终没有去看它源码,这次,正好钻研到了,把源码看了一遍,还是有不小的播种的,特意整顿了下,如果有任何问题,欢送指出 概述MyBatis这个orm框架其实就是把JDBC给封装了一层,用过的敌人必定晓得,创立一个mybatis_config.xml配置文件,创立一个mapper接口,创立一个mapper.xml文件,而后在service层中调用了。暂且先不剖析源码,如果假如本人开发这么个orm框架,性能齐全喝MyBatis一样,那摆在本人背后的问题总共有如下3个 怎么把配置封装起来(数据库链接地址,用户名,明码),达成只注册一次,后续就不须要管这个怎么绑定mapper接口和mapper.xml文件如何生成一个代理对象,让接口中的办法,找到对应的mapper语句,而后把参数带进去执行带着这几个问题,一步一步的比拟好学习源码,当然,光凭这几个问题是无奈齐全开发进去的,这里我会尽可能带着讲一下,如果有些比拟冷门的配置,可能就要本人去深入研究下了。JDBC&原生MyBatis调用回顾首先,MyBatis是对传统jdbc的一层封装,首先咱们先来回顾一下传统的jdbc JDBCpublic class User { //user表的id private Integer id; //用户名 private String username; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username == null ? null : username.trim(); } @Override public String toString() { return "User [id=" + id + ", username=" + username ; }}public class JDBCDemo { //创立一个数据库的连贯 private static Connection getConnection() { Connection connection = null; try { //加载用户驱动 Class.forName("com.mysql.cj.jdbc.Driver"); //连贯数据库的地址 String url = "jdbc:mysql://127.0.0.1:3306/test1"; //数据库的用户名 String user = "root"; //数据库的明码 String password = "12345678"; //失去一个数据库的连贯 connection = DriverManager.getConnection(url, user, password); } catch (ClassNotFoundException e) { System.out.println(JDBCDemo.class.getName() + "数据库驱动包未找到!"); return null; } catch (SQLException e) { System.out.println(JDBCDemo.class.getName() + "SQL语句有问题,无奈查问胜利!"); return null; } return connection;//返回该连贯 } public User getUser(int id) { //失去该数据库的连贯 Connection connection = getConnection(); //申明一个null的预处理的Statement PreparedStatement ps = null; //申明一个后果集,用来寄存SQL的查问后的后果 ResultSet rs = null; try { //对查问的User表的SQL进行预处理编译 ps = connection.prepareStatement("select * from user where id=?"); //把参数Id设值到数据的条件中 ps.setInt(1, id); //执行查问语句。把后果返回到ResultSet后果集中 rs = ps.executeQuery(); //遍历从后果集中取数 while (rs.next()) { //取出Statement的用户id int user_id = rs.getInt("id"); //取出Statement的用户名 String username = rs.getString("username"); User user = new User(); //寄存在user对象中 user.setId(user_id); user.setUsername(username); return user; } } catch (SQLException e) { e.printStackTrace(); } finally { this.close(rs, ps, connection); } return null; } /** * 判断数据库是否敞开 * @param rs 查看后果集是滞敞开 * @param stmt 预处理SQL是否敞开 * @param conn 数据库连贯是否敞开 */ private void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs != null) { rs.close(); } } catch (SQLException e) { System.out.println(JDBCDemo.class.getName() + "ResultSet 敞开失败!"); } try { if (stmt != null) { stmt.close(); } } catch (SQLException e) { System.out.println(JDBCDemo.class.getName() + "Statement 敞开失败!"); } try { if (conn != null) { conn.close(); } } catch (SQLException e) { System.out.println(JDBCDemo.class.getName() + "Connection 敞开失败!"); } } public static void main(String[] args) { //咱们查问用户的id 为 1 用户 User user = new JDBCDemo().getUser(1); //打印输出查问进去的数据 System.out.println(user); }}这里就简略的介绍下3个次要的类,前面会对介绍如何封装的 ...

December 14, 2021 · 18 min · jiezi

关于源码分析:alertmanager-源码分析一

监控告警个别是作为一个整体,包含从采集数据、存储、展现、规定计算、告警音讯解决等等。 Alertmanager(以下简称 am 了) 是一个告警音讯治理组件,包含音讯路由、静默、克制、去重等性能,总之其它负责规定计算的组件能够把音讯无脑发给 am, 由它来对音讯进行解决, 尽可能收回高质量的告警音讯。 先看个概览图,这个是我基于原开源库外面的架构图画的,原仓库中的架构图有很多跟理论源码出入的中央,所以这个图比原来的更丰盛,更精确。 这篇先说第一局部:告警的写入 告警的写入到最终被解决能够形象成生产-生产模型,生产侧就是 api 接管告警,生产侧就是图中的 dispatcher,两头的 provider.Alerts 作为缓冲区。 上面是写入时的逻辑,次要就是判断告警状态,am 的告警状态是由 alert.StartsAt 和 alert.EndsAt 来判断,而后续还有很多须要这个属性的逻辑,所以这个地位须要把起止工夫确认。 func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*types.Alert) { now := time.Now() api.mtx.RLock() resolveTimeout := time.Duration(api.config.Global.ResolveTimeout) api.mtx.RUnlock() // 确定一个告警音讯的起止工夫 // 须要依据起止工夫来定义告警的状态, 如果止工夫在以后之前就是 Resolved for _, alert := range alerts { // 新收到的告警标记接管工夫, 这样如果有两个告警 label 统一, 能够判断出哪个是最新收到的 alert.UpdatedAt = now // Ensure StartsAt is set. if alert.StartsAt.IsZero() { if alert.EndsAt.IsZero() { alert.StartsAt = now } else { alert.StartsAt = alert.EndsAt } } // 止工夫如果没有就须要应用 resolveTimeout 计算一个 if alert.EndsAt.IsZero() { alert.Timeout = true alert.EndsAt = now.Add(resolveTimeout) } if alert.EndsAt.After(time.Now()) { api.m.Firing().Inc() } else { api.m.Resolved().Inc() } } // Make a best effort to insert all alerts that are valid. var ( validAlerts = make([]*types.Alert, 0, len(alerts)) validationErrs = &types.MultiError{} ) // 校验alert, 比方清理空值的 label, 起止工夫, 至多一个label, label中的kv命名规定 等等 for _, a := range alerts { removeEmptyLabels(a.Labels) if err := a.Validate(); err != nil { validationErrs.Add(err) api.m.Invalid().Inc() continue } validAlerts = append(validAlerts, a) } // 写入 alertsProvider, 这一端相当于生产者 if err := api.alerts.Put(validAlerts...); err != nil { api.respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return }}而 provider.Alerts 是一个interface ...

October 30, 2021 · 4 min · jiezi

关于源码分析:我终于学会了黑客帝国中的矩阵雨

置信大家都对黑客帝国电影里的矩阵雨印象十分粗浅,就是上面这个成果。 成果十分酷炫,我看了一下相干实现库的代码,也非常简单,外围就是用好命令行的控制字符,这里分享一下。 在 matrix-rain 的源代码中,总共只有两个文件,ansi.js 和 index.js,十分玲珑。 控制字符和管制序列ansi.js 中定义了一些命令行的操作方法,也就是对控制字符做了一些办法封装,代码如下: const ctlEsc = `\x1b[`;const ansi = { reset: () => `${ctlEsc}c`, clearScreen: () => `${ctlEsc}2J`, cursorHome: () => `${ctlEsc}H`, cursorPos: (row, col) => `${ctlEsc}${row};${col}H`, cursorVisible: () => `${ctlEsc}?25h`, cursorInvisible: () => `${ctlEsc}?25l`, useAltBuffer: () => `${ctlEsc}?47h`, useNormalBuffer: () => `${ctlEsc}?47l`, underline: () => `${ctlEsc}4m`, off: () => `${ctlEsc}0m`, bold: () => `${ctlEsc}1m`, color: c => `${ctlEsc}${c};1m`, colors: { fgRgb: (r, g, b) => `${ctlEsc}38;2;${r};${g};${b}m`, bgRgb: (r, g, b) => `${ctlEsc}48;2;${r};${g};${b}m`, fgBlack: () => ansi.color(`30`), fgRed: () => ansi.color(`31`), fgGreen: () => ansi.color(`32`), fgYellow: () => ansi.color(`33`), fgBlue: () => ansi.color(`34`), fgMagenta: () => ansi.color(`35`), fgCyan: () => ansi.color(`36`), fgWhite: () => ansi.color(`37`), bgBlack: () => ansi.color(`40`), bgRed: () => ansi.color(`41`), bgGreen: () => ansi.color(`42`), bgYellow: () => ansi.color(`43`), bgBlue: () => ansi.color(`44`), bgMagenta: () => ansi.color(`45`), bgCyan: () => ansi.color(`46`), bgWhite: () => ansi.color(`47`), },};module.exports = ansi;这外面 ansi 对象上的每一个办法不做过多解释了。咱们看到,每个办法都是返回一个奇怪的字符串,通过这些字符串能够扭转命令行的显示成果。 ...

September 15, 2021 · 5 min · jiezi

关于源码分析:04篇-Nacos-Client服务订阅机制之核心流程

学习不必那么功利,二师兄带你从更高维度轻松浏览源码~说起Nacos的服务订阅机制,对此不理解的敌人,可能感觉十分神秘,这篇文章就大家深入浅出的理解一下Nacos 2.0客户端的订阅实现。因为波及到的内容比拟多,就分几篇来讲,本篇为第一篇。 Nacos订阅概述Nacos的订阅机制,如果用一句话来形容就是:Nacos客户端通过一个定时工作,每6秒从注册核心获取实例列表,当发现实例发生变化时,公布变更事件,订阅者进行业务解决。该更新实例的更新实例,该更新本地缓存的更新本地缓存。 上图画出了订阅办法的主线流程,波及的内容较多,解决细节简单。这里只用把握住外围局部即可。上面就通过代码和流程图来逐渐剖析上述过程。 从订阅到定时工作开启咱们这里聊的订阅机制,其实实质上就是服务发现的准实时感知。下面曾经看到了当执行订阅办法时,会触发定时工作,定时去拉服务器端的数据。所以,实质上,订阅机制就是实现服务发现的一种形式,对照的形式就是间接查问接口了。 NacosNamingService中裸露的许多重载的subscribe,重载的目标就是让大家少写一些参数,这些参数呢,Nacos给默认解决了。最终这些重载办法都会调用到上面这个办法: // NacosNamingServicepublic void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener) throws NacosException { if (null == listener) { return; } String clusterString = StringUtils.join(clusters, ","); changeNotifier.registerListener(groupName, serviceName, clusterString, listener); clientProxy.subscribe(serviceName, groupName, clusterString);}办法中的事件监听咱们临时不聊,间接看subscribe办法,这里clientProxy类型为NamingClientProxyDelegate。实例化NacosNamingService时该类被实例化,后面章节中曾经讲到,不再赘述。 而clientProxy.subscribe办法在NamingClientProxyDelegate中实现: // NamingClientProxyDelegate@Overridepublic ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException { String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName); String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters); // 获取缓存中的ServiceInfo ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey); if (null == result) { // 如果为null,则进行订阅逻辑解决,基于gRPC协定 result = grpcClientProxy.subscribe(serviceName, groupName, clusters); } // 定时调度UpdateTask serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters); // ServiceInfo本地缓存解决 serviceInfoHolder.processServiceInfo(result); return result;}这段办法是不是眼生啊?对的,在后面剖析《Nacos Client服务发现》时咱们曾经讲过了。看来必由之路,查问服务列表和订阅最终都调用了同一个办法。 ...

August 12, 2021 · 2 min · jiezi

关于源码分析:LnkedList源码

概述LinkedList 继承自 AbstrackSequentialList 并实现了 List 接口以及 Deque 双向队列接口,因而 LinkedList 岂但领有 List 相干的操作方法,也有队列的相干操作方法。LinkedList 和 ArrayList 一样实现了序列化接口 Serializable 和 Cloneable 接口使其领有了序列化和克隆的个性。继承了AbstractSequentialList抽象类,在遍历的时候,举荐应用迭代器进行遍历。然而只反对浅克隆,在LinkedList类中,其中的外部类Node并没有被克隆,只是调用了Object类中的clone办法进行可克隆。 LinkedList 双向链表实现及成员变量外围组成:用来存储数据的结点,在LinkedList中设计成了外部类。 private static class Node<E> { // 以后节点的元素值 E item; // 下一个节点的索引 Node<E> next; // 上一个节点的索引 Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }}LinkedList 次要成员变量有下边三个: //LinkedList 中的节点个数transient int size = 0;//LinkedList 链表的第一个节点transient Node<E> first;//LinkedList 链表的最初一个节点transient Node<E> last;构造方法LinkedList的构造方法只提供了两个: /** * 空参数的结构因为生成一个空链表 first = last = null */ public LinkedList() { }/** * 传入一个汇合类,来结构一个具备肯定元素的 LinkedList 汇合 * @param c 其外部的元素将按程序作为 LinkedList 节点 * @throws NullPointerException 如果 参数 collection 为空将抛出空指针异样 */public LinkedList(Collection<? extends E> c) { this(); addAll(c);}带参数的构造方法,调用 addAll(c) 这个办法, ...

August 3, 2021 · 8 min · jiezi

关于源码分析:react16-版本源码-简单分析

在本文之前说了 react15的毛病,这里来说下16版本是怎么修复的。我之前的一篇文章 写过mini-react-fiber版本,那是一个简易版。 咱们这里剖析先做总结,而后依据总结的流程来看源码,这里次要就是串通主流程。 大架构:首先从react源码层面执行,宏观角度来看,它实际上分为了两局部。 render阶段:次要就是用来生成新的fiber树并diff出有变动的节点。commit阶段:获取到render阶段中diff进去的产生了变动的节点的fiber,通过原生api更新页面。也就是说,在render阶段中,只会做一些简单的运算,并不会真正的操作页面(在内存中做 新旧 fiber对象比对,找出更新的fiber节点,或者是首次加载时 生成组装html片段),这一阶段是能够被打断的(初始渲染时不会被打断,因为要让用户尽快看到界面),也就是说在react里的工夫分片的概念,分的就是简单运算的局部也就是这里,在这里这个render阶段也就是说可能会被高优先级的工作(例如界面事件)打断。 直到commit阶段才会去通过js的原生api去批改dom,commit阶段是不能够被打断的,因为commit阶段是渲染阶段,它如果也是能够被打断的话,不一次性更新进去的话,就会呈现 相似网速过慢时 图片 迟缓加载的成果,和咱们的要求不符,所以commit阶段是同步的。 本质上我原来那篇mini-react-fiber文章也是遵循了react的两大阶段。 小架构:当初咱们来认真说下react外部的架构划分:react外部架构理论能够分成三层: 调度层Scheduler:调度工作的优先级,高优工作优先进入协调器协调层Reconciler:构建 Fiber 数据结构,比对 Fiber 对象找出差别, 记录 Fiber 对象要进行的 DOM 操作(初始加载的时候,负责组装html片段)渲染层Renderer:负责将发生变化的局部渲染到页面上Scheduler我后面比照15-16的时候讲过 16为了解决 vnode过大stack递归堆栈问题。引入了工作优先级 和 工作可中断的概念。 我那个繁难版本是通过window 对象中 requestIdleCallback API实现的,它能够利用浏览器的闲暇工夫执行工作,然而它本身也存在一些问题,比如说并不是所有的浏览器都反对它,而且它的触发频率也不是很稳固,所以 React 最终放弃了 requestIdleCallback 的应用。 在 React 中,官网实现了本人的任务调度库,这个库就叫做 Scheduler。它也能够实现在浏览器闲暇时执行工作,而且还能够设置工作的优先级,高优先级工作先执行,低优先级工作后执行。 Scheduler 存储在它源码的 packages/scheduler 文件夹中。 Reconciler在 React 15 的版本中,协调器和渲染器交替执行,即找到了差别就间接更新差别。在 React 16 的版本中,这种状况产生了变动,协调器和渲染器不再交替执行。协调器负责找出差别,在所有差别找出之后,对立交给渲染器进行 DOM 的更新。也就是说协调器的次要工作就是找出差别局部,并为差别打上标记。 Renderer渲染器依据协调器为 Fiber 节点打的标记,同步执行对应的DOM操作。 既然比对的过程从递归变成了能够中断的循环,那么 React 是如何解决中断更新时 DOM 渲染不齐全的问题呢? 其实基本就不存在这个问题,因为在整个过程中,调度器和协调器的工作是在内存中实现的是能够被打断的,渲染器的工作被设定成不能够被打断,所以不存在DOM 渲染不齐全的问题。 这样和咱们后面说的两个大阶段比照的话,Scheduler和Reconciler都能够归属到后面的render阶段,Scheduler负责工作优先级调度 Reconciler负责依据进入的工作来组装比照fiber构造,这个过程里高优先级能够打断低优先级,协调器 生产比照fiber也是能够被打断的, 也就验证了render能够被打断这一说法。 Renderer阶段对应大阶段就是咱们说的commit阶段,这个阶段渲染器 工作是不能够被打断的,它负责渲染更新界面。 ...

April 22, 2021 · 1 min · jiezi

关于vue.js:Vue中是如何防御XSS注入攻击的

XSS 简略来说就是 非法脚本 存储在了服务端,并输入到了用户客户端,脚本执行后就会读取cookie等隐衷数据 并发送信息给攻击者 模仿一段攻打文本 let xssText = '<script> console.log( 'cookie数据为', document.cookie ) </script>'如果将这段文本间接写在html标签外面,那么它会间接执行(如 innerHtml操作 )这个时候就是十分不平安的,那么怎么做能力防止这种景象产生呢?有两种办法1.innerText 办法2.createTextNode 创立文本节点 vue中是如何操作的呢 咱们来看一段模板代码 <template>   <div>{{ xssText  }}<div></template>这种操作无害吗?no,齐全有害,我来剖析一下 下面一段模板代码生成的render函数相似于 createElement( 'div', {}, xxsText ) // 创立vnodevue 在 patchVnode( 虚构dom 生成 实在dom )有如下代码 解决子节点红框局部意思是 如果 vnode 子节点为 根本类型 如字符串,那么该文本会通过createTextNode办法 生成 文本节点,而后插入父节点 所以 很显著  xssText 被 createTextNode 解决成了纯字符串了,变成有害的了,so easy

March 25, 2021 · 1 min · jiezi

关于源码分析:minireact新版本fiber架构

之前写了一篇stack版的mini-react实现,这里再写一篇fiber版的实现。这里如果不晓得两者的区别的话,举荐先看看我这一篇文章:stack和fiber架构的区别 从我下面连贯这篇文章咱们能够晓得:React 16 之前的版本比对更新 VirtualDOM 的过程是采纳循环加递归实现的,这种比对形式有一个问题,就是一旦工作开始进行就无奈中断,如果利用中组件数量宏大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新实现之后主线程能力被开释,主线程能力执行其余工作。这就会导致一些用户交互,动画等工作无奈立刻失去执行,页面就会产生卡顿, 十分的影响用户体验。 其次要问题是:递归无奈中断,执行重工作耗时长。 JavaScript 又是单线程,无奈同时执行其余工作,导致工作提早页面卡顿,用户体验差。 咱们得解决方案是: 利用浏览器闲暇工夫执行工作,回绝长时间占用主线程放弃递归只采纳循环,因为循环能够被中断工作拆分,将工作拆分成一个个的小工作基于以上几点,在这里咱们先理解下requestIdleCallback这个api 外围 API 性能介绍:利用浏览器的空余工夫执行工作,如果有更高优先级的工作要执行时,以后执行的工作能够被终止,优先执行高级别工作。 requestIdleCallback(function(deadline) { // deadline.timeRemaining() 获取浏览器的空余工夫})这里咱们理解下什么是浏览器空余工夫:页面是一帧一帧绘制进去的,当每秒绘制的帧数达到 60 时,页面是晦涩的,小于这个值时, 用户会感觉到卡顿,1s 60帧,每一帧分到的工夫是 1000/60 ≈ 16 ms,如果每一帧执行的工夫小于16ms,就阐明浏览器有空余工夫。 如果工作在残余的工夫内没有实现则会进行工作执行,持续优先执行主工作,也就是说 requestIdleCallback 总是利用浏览器的空余工夫执行工作。 咱们先用这个api做个例子,来看:html <div class="playground" id="play">playground</div><button id="work">start work</button><button id="interaction">handle some user interaction</button>css <style> .playground { background: palevioletred; padding: 20px; margin-bottom: 10px; }</style>js var play = document.getElementById("play")var workBtn = document.getElementById("work")var interactionBtn = document.getElementById("interaction")var iterationCount = 100000000var value = 0var expensiveCalculation = function (IdleDeadline) { while (iterationCount > 0 && IdleDeadline.timeRemaining() > 1) { value = Math.random() < 0.5 ? value + Math.random() : value + Math.random() iterationCount = iterationCount - 1 } requestIdleCallback(expensiveCalculation)}workBtn.addEventListener("click", function () { requestIdleCallback(expensiveCalculation)})interactionBtn.addEventListener("click", function () { play.style.background = "palegreen"})从这个示例中咱们晓得了,这个api该如何应用,该如何中断工作。 ...

February 16, 2021 · 2 min · jiezi

关于源码分析:react为何采用fiber架构

这里要比照一下stack和fiber架构的不同以及react在fiber架构做了那些更改这里说到了react16应用了fiber,那咱们看下16之前输出stack架构的实现的问题,说起React算法架构避不开“Reconciliaton”。 ReconciliationReact 官网外围算法名称是 Reconciliation , 中文翻译是“协调”![React diff 算法的实现就与之相干。略微理解浏览器加载页面原理的前端同学都晓得网页性能问题大都呈现在DOM节点频繁操作上;而React通过“虚构DOM” + React Diff算法保障了前端性能 传统Diff算法通过循环递归对节点进行顺次比照,算法复杂度达到 O(n^3) ,n是树的节点数,这个有多可怕呢?——如果要展现1000个节点,得执行上亿次比拟。。即使是CPU快能执行30亿条命令,也很难在一秒内计算出差别。 React Diff算法将Virtual DOM树转换成actual DOM树的起码操作的过程 称为 协调(Reconciliaton)。React Diff三大策略 : tree diffcomponent diffelement diff在V16版本之前 协调机制 是 Stack reconciler, V16版本公布Fiber 架构后是 Fiber reconciler。 咱们先说Stack reconciler存在的问题:在setState后,react会立刻开始reconciliation过程,从父节点(Virtual DOM)开始递归遍历,以找出不同。将所有的Virtual DOM遍历实现后,reconciler能力给出以后须要批改实在DOM的信息,并传递给renderer,进行渲染,而后屏幕上才会显示此次更新内容。 对于特地宏大的DOM树来说,reconciliation过程会很长(x00ms),在这期间,主线程是被js占用的,因而任何交互、布局、渲染都会进行,给用户的感觉就是页面被卡住了。 在这里咱们想解决这个问题的话,来引入一个概念,就是工作可中断,以及工作优先级,也就是说咱们的reconciliation的过程中会生成一些工作和子工作,用户的操作的工作优先级是要高于reconciliation产生的工作的,也就是说用户操作的工作是能够打断reconciliation中产生得工作的,它会优先执行. Fiber reconciler原来的React更新工作是采纳递归模式,那么当初如果工作想中断, 在递归中是很难解决, 所以React改成了大循环模式,批改了生命周期也是因为工作可中断。 Fiber reconciler 应用了scheduling(调度)这一过程, 每次只做一个很小的工作,做完后可能“喘口气儿”,回到主线程看下有没有什么更高优先级的工作须要解决,如果有则先解决更高优先级的工作,没有则继续执行(cooperative scheduling 单干式调度)。 所以Fiber 架构就是用 异步的形式解决旧版本 同步递归导致的性能问题。 本文借鉴于https://segmentfault.com/a/11...

February 15, 2021 · 1 min · jiezi

关于源码分析:Vite-依赖预编译缩短数倍的冷启动时间

前言前段时间,Vite 做了一个优化依赖预编译(Dependency Pre-Bundling)。简而言之,它指的是 Vite 会在 DevServer 启动前对须要预编译的依赖进行编译,而后在剖析模块的导入(import)时会动静地利用编译过的依赖。 这么一说,我想大家可能立马会抛出一个疑难:Vite 不是 No Bundle 吗?的确 Vite 是 No Bundle,然而依赖预编译并不是意味着 Vite 要走向 Bundle,咱们不要急着下定义,因为它的存在必然是有着其理论的价值。 那么,明天本文将会围绕以下 3 点来和大家一起从疑难点登程,深入浅出一番 Vite 的依赖预编译过程: 什么是依赖预编译依赖预编译的作用依赖预编译的实现(源码剖析)一、什么是依赖预编译当你在我的项目中援用了 vue 和 lodash-es,那么你在启动 Vite 的时候,你会在终端看到这样的输入内容: 而这示意 Vite 将你在我的项目中引入的 vue 和 lodash-es 进行了依赖预编译!这里,咱们通过大白话认识一下 Vite 的依赖预编译: 默认状况下,Vite 会将 package.json 中生产依赖 dependencies 的局部启用依赖预编译,即会先对该依赖进行编译,而后将编译后的文件缓存在内存中(node_modules/.vite 文件下),在启动 DevServer 时间接申请该缓存内容。在 vite.config.js 文件中配置 optimizeDeps 选项能够抉择须要或不须要进行预编译的依赖的名称,Vite 则会依据该选项来确定是否对该依赖进行预编译。在启动时增加 --force options,能够用来强制从新进行依赖预编译。须要留神,强制从新依赖预编译指的是疏忽之前已编译的文件,间接从新编译。 所以,回到文章开始所说的疑难,这里咱们能够这样了解依赖预编译,它的呈现是一种优化,即没有它其实 No Bundle 也能够,有它更好(xiang)! 而且,依赖预编译并非无米之炊,Vite 也是受 Snowpack 的启发才提出的。 那么,上面咱们就来理解一下依赖预编译的作用是什么,即优化的意义~ 二、依赖预编译的作用对于依赖预编译的作用,Vite 官网也做了具体的介绍。那么,这里咱们通过联合图例的形式来认识一下,具体会是两点: 1. 兼容 CommonJS 和 AMD 模块的依赖 ...

February 14, 2021 · 4 min · jiezi

关于源码分析:Vue-3-中-vif-和-vshow-指令实现的原理源码分析

前言又回到了经典的一句话:“知其然,而后使其然”。置信大家对 Vue 提供 v-if 和 v-show 指令的应用以及对应场景应该都滚瓜烂熟了。然而,我想依然会有很多同学对于 v-if 和 v-show 指令实现的原理存在常识空白。 所以,明天就让咱们来一起理解一番 v-if 和 v-show 指令实现的原理~ v-if在之前 【Vue3 源码解读】从编译过程,了解动态节点晋升 一文中,我给大家介绍了 Vue 3 的编译过程,即一个模版会经验 baseParse、transform、generate 这三个过程,最初由 generate 生成能够执行的代码(render 函数)。 这里,咱们就不从编译过程开始解说 v-if 指令的 render 函数生成过程了,有趣味理解这个过程的同学,能够看我之前的文章从编译过程,了解动态节点晋升咱们能够间接在 Vue3 Template Explore 输出一个应用 v-if 指令的栗子: <div v-if="visible"></div>而后,由它编译生成的 render 函数会是这样: render(_ctx, _cache, $props, $setup, $data, $options) { return (_ctx.visible) ? (_openBlock(), _createBlock("div", { key: 0 })) : _createCommentVNode("v-if", true)}能够看到,一个简略的应用 v-if 指令的模版编译生成的 render 函数最终会返回一个三目运算表达式。首先,让咱们先来认识一下其中几个变量和函数的意义: _ctx 以后组件实例的上下文,即 this_openBlock() 和 _createBlock() 用于结构 Block Tree 和 Block VNode,它们次要用于靶向更新过程_createCommentVNode() 创立正文节点的函数,通常用于占位显然,如果当 visible 为 false 的时候,会在以后模版中创立一个正文节点(也可称为占位节点),反之则创立一个实在节点(即它本人)。例如当 visible 为 false 时渲染到页面上会是这样: ...

January 17, 2021 · 5 min · jiezi

关于源码分析:TiKV-源码解析系列文章二十一Region-Merge-源码解析

Region Merge 是 Range 相邻的两个的 Region 合并的过程,咱们把一个 Region 称为 Source Region,另一个称为 Target Region,在 Merge 过程完结后,Target Region 治理的 Range 会扩充到 Source Region 的局部,Source Region 则被删除。 在上一篇 Region Split 源码解析 的结尾,咱们提到了与其绝对的 Region Merge 的复杂性。 因为两个 Region 属于不同的 Raft group,与 Region Split,Raft Snapshot 的相互作用,再加上网络隔离带来的影响,无疑有更大的复杂度。本文接下来将会解开 Region Merge 的神秘面纱。 Merge 的设计需要咱们心愿 Merge 的设计可能满足以下几个需要: 不依附 PD 在 Merge 期间不发动其余的调度满足正确性不会因为网络隔离或者宕机引发正确性问题只有参加 Merge 的 TiKV 中 Majority 存活且能相互通信,Merge 能够持续或者回滚,不会被阻塞住(跟 Raft 保障可用性的要求统一)不对 Split/Conf Change 加额定条件限度(出于性能思考)尽量减少搬迁数据的开销尽量减少 Merge 期间服务不可用的工夫接下来咱们来看一下 TiKV 的 Merge 是怎么一一满足这些需要的。 ...

December 17, 2020 · 6 min · jiezi

七星排列网站系统平台结算功能代码解析

这个是前端工夫帮敌人做的一个我的项目,海南那边的七星网站零碎 盘口开奖算法实现,当初分享进去给大家钻研钻研,心愿能够帮到一些敌人,有问题的能够加我q:3523-657133 function bet3add($bet5){ $sql = "UPDATE jz\_bet3 SET BetAmount = CASE id ";     $sql1= "Odds = CASE id ";      foreach ($bet5 as $key => $value) {         if($value['id']){             $idss\[\]=$value['id'];             $sql.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['BetAmount']);             $sql1.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['Odds']);               $t=1;                }else{             $bet3add\[\]=$value;         }     }     $ids = implode(',',$idss);     $sql .= "END,".$sql1."END WHERE id IN ($ids)";      if(isset($bet3add)){         M('bet3')->addAll($bet3add);     }       if($t==1){         M()->execute($sql);     } } function bet4add($bet6){ $sql = "UPDATE jz\_bet4 SET BetAmount = CASE id ";     $sql1= "lhmoney = CASE id ";      foreach ($bet6 as $key => $value) {         if($value['id']){             $idss\[\]=$value['id'];             $sql.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['BetAmount']);             $sql1.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['lhmoney']);             //$sql1.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['Odds']);               $t=1;                }else{             $bet4add\[\]=$value;         }     }     $ids = implode(',',$idss);     $sql .= "END,".$sql1."END WHERE id IN ($ids)";      if(isset($bet4add)){         M('bet4')->addAll($bet4add);     }       if($t==1){         M()->execute($sql);     } } function bet5add($bet7){ $sql = "UPDATE jz\_bet5 SET money = CASE id ";     //$sql1= "money = CASE id ";      foreach ($bet7 as $key => $value) {         if($value['id']){             $idss\[\]=$value['id'];             $sql.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['money']);             //$sql1.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['money']);             //$sql1.=sprintf("WHEN %d THEN %2\\$.2f ",$value\['id'\],$value['Odds']);               $t=1;                }else{             $bet5add\[\]=$value;         }     }     $ids = implode(',',$idss);     $sql .="END WHERE id IN ($ids)";      if(isset($bet5add)){         M('bet5')->addAll($bet5add);     }       if($t==1){         M()->execute($sql);     } } ...

July 14, 2020 · 1 min · jiezi

spring学习之源码分析FactoryBeanRegistrySupport

FactoryBeanRegistrySupportFactoryBeanRegistrySupport抽象类继承了DefaultSingletonBeanRegistry类,增加了对FactoryBean的处理。 类结构 常量// 缓存factoryBean的对应关系private final Map<String, Object> factoryBeanObjectCache = new ConcurrentHashMap<>(16);方法解析getTypeForFactoryBean调用factoryBean的getObjectType方法返回class类型 protected Class<?> getTypeForFactoryBean(final FactoryBean<?> factoryBean) { try { if (System.getSecurityManager() != null) { return AccessController.doPrivileged((PrivilegedAction<Class<?>>) factoryBean::getObjectType, getAccessControlContext()); } else { return factoryBean.getObjectType();//调用factoryBean的getObjectType方法返回class类型。 } } catch (Throwable ex) { // Thrown from the FactoryBean's getObjectType implementation. logger.info("FactoryBean threw exception from getObjectType, despite the contract saying " + "that it should return null if the type of its object cannot be determined yet", ex); return null; }}getCachedObjectForFactoryBean从缓存中通过制定的beanName获取FactoryBean ...

November 4, 2019 · 3 min · jiezi

spring学习之源码分析DefaultSingletonBeanRegistry

DefaultSingletonBeanRegistryDefaultSingletonBeanRegistry类继承了SimpleAliasRegistry以及实现了SingletonBeanRegistry的接口。处理Bean的注册,销毁,以及依赖关系的注册和销毁。 类结构截取部分 常量// 单例对象的缓存:从beanname到bean实例private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);// 单例工厂的缓存:从beanname到ObjectFactoryprivate final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);// 早期单例对象的缓存:从beanname到bean实例private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);// 一组已注册的单例,包含按注册顺序排列的beannameprivate final Set<String> registeredSingletons = new LinkedHashSet<>(256);// 正在创建的单例的beanName的集合private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));// 当前不检查的bean的集合private final Set<String> inCreationCheckExclusions = Collections.newSetFromMap(new ConcurrentHashMap<>(16));// 异常集合private Set<Exception> suppressedExceptions;// 当前是否在销毁bean中private boolean singletonsCurrentlyInDestruction = false;// 一次性bean实例private final Map<String, Object> disposableBeans = new LinkedHashMap<>();// 内部bean和外部bean之间关系private final Map<String, Set<String>> containedBeanMap = new ConcurrentHashMap<>(16);// 指定bean与依赖指定bean的集合,比如bcd依赖a,那么就是key为a,bcd为valueprivate final Map<String, Set<String>> dependentBeanMap = new ConcurrentHashMap<>(64);// 指定bean与指定bean依赖的集合,比如a依赖bcd,那么就是key为a,bcd为valueprivate final Map<String, Set<String>> dependenciesForBeanMap = new ConcurrentHashMap<>(64);方法解析registerSingleton通过bean的名称和对象进行注册。 ...

November 4, 2019 · 6 min · jiezi

spring学习之源码分析ConfigurableListableBeanFactory

ConfigurableListableBeanFactoryConfigurableListableBeanFactory继承了ListableBeanFactory, AutowireCapableBeanFactory, ConfigurableBeanFactory。在ConfigurableBeanFactory的基础上,它还提供了分析和修改bean定义以及预实例化单例的工具 类结构 方法解析忽略自动装配 // 在装配的时候忽略指定的依赖类型void ignoreDependencyType(Class<?> type);// 在装配的时候忽略指定的接口void ignoreDependencyInterface(Class<?> ifc);依赖 // 注册可解析的依赖void registerResolvableDependency(Class<?> dependencyType, @Nullable Object autowiredValue);// 指定的bean是否可以作为自动选派的候选,boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) throws NoSuchBeanDefinitionException;BeanDefinition // 根据bean名称获取BeanDefinitionBeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;/ /获取bean名称的IteratorIterator<String> getBeanNamesIterator()bean的元数据缓存 void clearMetadataCache();冻结bean的配置 // 冻结所有bean定义,表明注册的bean定义将不再修改或后期处理。void freezeConfiguration();// bean的定义是否被冻结boolean isConfigurationFrozen();lazy-init相关 // 非延迟加载的bean都实例化void preInstantiateSingletons() throws BeansException;

November 4, 2019 · 1 min · jiezi

spring学习之源码分析ConfigurableBeanFactory

ConfigurableBeanFactoryConfigurableBeanFactory继承了HierarchicalBeanFactory, SingletonBeanRegistry两个接口。这个接口将被大多数bean工厂实现。 类结构这个方法,比之前的都多,没有截全。 常量String SCOPE_SINGLETON = "singleton";//单例String SCOPE_PROTOTYPE = "prototype";//多例方法解析设置父类容器 void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;类加载器 // 设置类加载器void setBeanClassLoader(@Nullable ClassLoader beanClassLoader);// 获取类加载器ClassLoader getBeanClassLoader();// 设置临时加载器,如果涉及到加载时编织,通常只指定一个临时类装入器,以确保实际的bean类被尽可能延迟地装入void setTempClassLoader(@Nullable ClassLoader tempClassLoader);// 获取临时加载器ClassLoader getTempClassLoader();bean的元数据缓存,默认为true。如果为false,每次创建bean都要从类加载器获取信息。 // 设置是否缓存void setCacheBeanMetadata(boolean cacheBeanMetadata);// 获取是否缓存boolean isCacheBeanMetadata();bean的表达式解析器 // 设置表达式解析器void setBeanExpressionResolver(@Nullable BeanExpressionResolver resolver);// 获取表达式解析器BeanExpressionResolver getBeanExpressionResolver();类型转换器 // 设置类型转换器void setConversionService(@Nullable ConversionService conversionService);// 获取类型转换器ConversionService getConversionService();属性编辑器 // 添加属性编辑器void addPropertyEditorRegistrar(PropertyEditorRegistrar registrar);// 注册给定类型的属性编辑器void registerCustomEditor(Class<?> requiredType, Class<? extends PropertyEditor> propertyEditorClass);// 使用在这个BeanFactory中注册的自定义编辑器初始化给定的PropertyEditorRegistryvoid copyRegisteredEditorsTo(PropertyEditorRegistry registry);类型转换器 // 设置类型转换器void setTypeConverter(TypeConverter typeConverter);// 获取类型转换器TypeConverter getTypeConverter();为嵌入的值(如注释属性)添加字符串解析器 ...

November 4, 2019 · 1 min · jiezi

JDK源码那些事儿之LinkedBlockingDeque

阻塞队列中目前还剩下一个比较特殊的队列实现,相比较前面讲解过的队列,本文中要讲的LinkedBlockingDeque比较容易理解了,但是与之前讲解过的阻塞队列又有些不同,从命名上你应该能看出一些端倪,接下来就一起看看这个特殊的阻塞队列 前言JDK版本号:1.8.0_171LinkedBlockingDeque在结构上有别于之前讲解过的阻塞队列,它不是Queue而是Deque,中文翻译成双端队列,双端队列指可以从任意一端入队或者出队元素的队列,实现了在队列头和队列尾的高效插入和移除 LinkedBlockingDeque是链表实现的线程安全的无界的同时支持FIFO、LIFO的双端阻塞队列,可以回顾下之前的LinkedBlockingQueue阻塞队列特点,本质上是类似的,但是又有些不同: 内部是通过Node节点组成的链表来实现的,当然为了支持双端操作,结点结构不同LinkedBlockingQueue通过两个ReentrantLock锁保护竞争资源,实现了多线程对竞争资源的互斥访问,入队和出队互不影响,可同时操作,然而LinkedBlockingDeque只设置了一个全局ReentrantLock锁,两个条件对象实现互斥访问,性能上要比LinkedBlockingQueue差一些无界,默认链表长度为Integer.MAX_VALUE,本质上还是有界阻塞队列,是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其它要获取该资源的线程需要阻塞等待Queue和Deque的关系有点类似于单链表和双向链表,LinkedBlockingQueue和LinkedBlockingDeque的内部结点实现就是单链表和双向链表的区别,具体可参考源码 在第二点中可能有些人有些疑问,两个互斥锁和一个互斥锁的区别在哪里?我们可以考虑以下场景: A线程先进行入队操作,B线程随后进行出队操作,如果是LinkedBlockingQueue,A线程入队过程还未结束(已获得锁还未释放),B线程出队操作不会被阻塞等待(锁不同),如果是LinkedBlockingDeque则B线程会被阻塞等待(同一把锁)A线程完成操作才继续执行 LinkedBlockingQueue一般的操作是获取一把锁就可以,但有些操作例如remove操作,则需要同时获取两把锁,之前的LinkedBlockingQueue讲解曾经说明过,这里就不详细讲解了 类定义实现BlockingDeque接口,其中定义了双端队列应该实现的方法,具体方法不说明了,主要是每个方法都分为了First和Last两种方式,从头部或者尾部进行队列操作 public class LinkedBlockingDeque<E> extends AbstractQueue<E> implements BlockingDeque<E>, java.io.Serializable 常量/变量 /** * 头结点 */ transient Node<E> first; /** * 尾结点 */ transient Node<E> last; /** 双端队列实际结点个数 */ private transient int count; /** 双端队列容量 */ private final int capacity; /** 互斥重入锁 */ final ReentrantLock lock = new ReentrantLock(); /** 非空条件对象 */ private final Condition notEmpty = lock.newCondition(); /** 非满条件对象 */ private final Condition notFull = lock.newCondition();内部类为了实现双端队列,内部使用了双向链表,不像LinkedBlockingQueue使用的是单链表,前驱和后继指针的特殊情况需要注意 ...

November 2, 2019 · 6 min · jiezi

styleloader源码解析

首先打开style-loader的package.json,找到main,可以看到它的入口文件即为:dist/index.js,内容如下:` var _path = _interopRequireDefault(require("path"));var _loaderUtils = _interopRequireDefault(require("loader-utils"));var _schemaUtils = _interopRequireDefault(require("schema-utils"));var _options = _interopRequireDefault(require("./options.json"));function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }module.exports = () => {};module.exports.pitch = function loader(request) { // ...}`其中_interopRequireDefault的作用是:如果引入的是 es6 模块,直接返回,如果是 commonjs 模块,则将引入的内容放在一个对象的 default 属性上,然后返回这个对象。我首先来看pitch函数,它的内容如下:` // 获取webpack配置的optionsconst options = _loaderUtils.default.getOptions(this) || {};// (0, func)(),运用逗号操作符,将func的this指向了windows,详情请查看:https://www.jianshu.com/p/cd188bda72df// 调用_schemaUtils是为了校验options,知道其作用就行,这里就不讨论了(0, _schemaUtils.default)(_options.default, options, { name: 'Style Loader', baseDataPath: 'options'});// 定义了两个变量,**insert**、**injectType**,不难看出insert的默认值为head,injectType默认值为styleTagconst insert = typeof options.insert === 'undefined' ? '"head"' : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString();const injectType = options.injectType || 'styleTag';switch(injectType){ case 'linkTag': { // ... } case 'lazyStyleTag': case 'lazySingletonStyleTag': { // ... } case 'styleTag': case 'singletonStyleTag': default: { // ... }}`在这里,我们就看默认的就好了,即insert=head,injectType=styleTag` ...

October 15, 2019 · 4 min · jiezi

Go-map原理剖析

在使用map的过程中,有两个问题是经常会遇到的:读写冲突和遍历无序性。为什么会这样呢,底层是怎么实现的呢?带着这两个问题,我简单的了解了一下map的增删改查及遍历的实现。 结构hmaptype hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // 有效数据的长度# live cells == size of map. Must be first (used by len() builtin) flags uint8 // 用于记录hashmap的状态 B uint8 // 2^B = buckets的数量log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // 随机的hash种子 buckets unsafe.Pointer // buckets数组array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // 老的buctedts数据,map增长的时候会用到 nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) extra *mapextra // 额外的bmap数组optional fields}mapextra type mapextra struct { // If both key and value do not contain pointers and are inline, then we mark bucket // type as containing no pointers. This avoids scanning such maps. // However, bmap.overflow is a pointer. In order to keep overflow buckets // alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow. // overflow and oldoverflow are only used if key and value do not contain pointers. // overflow contains overflow buckets for hmap.buckets. // oldoverflow contains overflow buckets for hmap.oldbuckets. // The indirection allows to store a pointer to the slice in hiter. overflow *[]*bmap oldoverflow *[]*bmap // nextOverflow holds a pointer to a free overflow bucket. nextOverflow *bmap}bmaptype bmap struct { // tophash generally contains the top byte of the hash value // for each key in this bucket. If tophash[0] < minTopHash, // tophash[0] is a bucket evacuation state instead. tophash [bucketCnt]uint8 // Followed by bucketCnt keys and then bucketCnt values. // NOTE: packing all the keys together and then all the values together makes the // code a bit more complicated than alternating key/value/key/value/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8. // Followed by an overflow pointer.}stringStructtype stringStruct struct { str unsafe.Pointer len int}hitermap遍历时用到的结构,startBucket+offset设定了开始遍历的地址,保证map遍历的无序性 ...

October 8, 2019 · 17 min · jiezi

Spring-security-一架构框架ComponentServiceFilter分析

想要深入spring security的authentication (身份验证)和access-control(访问权限控制)工作流程,必须清楚spring security的主要技术点包括关键接口、类以及抽象类如何协同工作进行authentication 和access-control的实现。 1.spring security 认证和授权流程常见认证和授权流程可以分成: A user is prompted to log in with a username and password (用户用账密码登录)The system (successfully) verifies that the password is correct for the username(校验密码正确性)The context information for that user is obtained (their list of roles and so on).(获取用户信息context,如权限)A security context is established for the user(为用户创建security context)The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.(访问权限控制,是否具有访问权限)1.1 spring security 认证上述前三点为spring security认证验证环节: ...

October 7, 2019 · 4 min · jiezi

React-源码阅读1027

React 源码阅读1Fork最新版的 React 源码地址 React 入口 import ReactVersion from 'shared/ReactVersion';import { REACT_FRAGMENT_TYPE, REACT_PROFILER_TYPE, REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_TYPE, REACT_SUSPENSE_LIST_TYPE,} from 'shared/ReactSymbols';import {Component, PureComponent} from './ReactBaseClasses';import {createRef} from './ReactCreateRef';import {forEach, map, count, toArray, only} from './ReactChildren';import { createElement, createFactory, cloneElement, isValidElement, jsx,} from './ReactElement';import {createContext} from './ReactContext';import {lazy} from './ReactLazy';import forwardRef from './forwardRef';import memo from './memo';import { useCallback, useContext, useEffect, useImperativeHandle, useDebugValue, useLayoutEffect, useMemo, useReducer, useRef, useState, useResponder,} from './ReactHooks';import {withSuspenseConfig} from './ReactBatchConfig';import { createElementWithValidation, createFactoryWithValidation, cloneElementWithValidation, jsxWithValidation, jsxWithValidationStatic, jsxWithValidationDynamic,} from './ReactElementValidator';import ReactSharedInternals from './ReactSharedInternals';import createFundamental from 'shared/createFundamentalComponent';import createResponder from 'shared/createEventResponder';import createScope from 'shared/createScope';import { enableJSXTransformAPI, enableFlareAPI, enableFundamentalAPI, enableScopeAPI,} from 'shared/ReactFeatureFlags';const React = { Children: { map, forEach, count, toArray, only, }, createRef, Component, PureComponent, createContext, forwardRef, lazy, memo, useCallback, useContext, useEffect, useImperativeHandle, useDebugValue, useLayoutEffect, useMemo, useReducer, useRef, useState, Fragment: REACT_FRAGMENT_TYPE, Profiler: REACT_PROFILER_TYPE, StrictMode: REACT_STRICT_MODE_TYPE, Suspense: REACT_SUSPENSE_TYPE, unstable_SuspenseList: REACT_SUSPENSE_LIST_TYPE, createElement: __DEV__ ? createElementWithValidation : createElement, cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, createFactory: __DEV__ ? createFactoryWithValidation : createFactory, isValidElement: isValidElement, version: ReactVersion, unstable_withSuspenseConfig: withSuspenseConfig, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,};if (enableFlareAPI) { React.unstable_useResponder = useResponder; React.unstable_createResponder = createResponder;}if (enableFundamentalAPI) { React.unstable_createFundamental = createFundamental;}if (enableScopeAPI) { React.unstable_createScope = createScope;}// Note: some APIs are added with feature flags.// Make sure that stable builds for open source// don't modify the React object to avoid deopts.// Also let's not expose their names in stable builds.if (enableJSXTransformAPI) { if (__DEV__) { React.jsxDEV = jsxWithValidation; React.jsx = jsxWithValidationDynamic; React.jsxs = jsxWithValidationStatic; } else { React.jsx = jsx; // we may want to special case jsxs internally to take advantage of static children. // for now we can ship identical prod functions React.jsxs = jsx; }}export default React;按照顺序一一对 React 进行解读: ...

September 20, 2019 · 2 min · jiezi

学习-lodash-源码整体架构打造属于自己的函数式编程类库

前言这是学习源码整体架构系列第三篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。 上上篇文章写了jQuery源码整体架构,学习 underscore 源码整体架构,打造属于自己的函数式编程类库 上一篇文章写了underscore源码整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库 感兴趣的读者可以点击阅读。 underscore源码分析的文章比较多,而lodash源码分析的文章比较少。原因之一可能是由于lodash源码行数太多。注释加起来一万多行。 分析lodash整体代码结构的文章比较少,笔者利用谷歌、必应、github等搜索都没有找到,可能是找的方式不对。于是打算自己写一篇。平常开发大多数人都会使用lodash,而且都或多或少知道,lodash比underscore性能好,性能好的主要原因是使用了惰性求值这一特性。 本文章学习的lodash的版本是:v4.17.15。unpkg.com地址 https://unpkg.com/lodash@4.17... 文章篇幅可能比较长,可以先收藏再看,所以笔者使用了展开收缩的形式。 导读: 文章主要学习了runInContext() 导出_ lodash函数使用baseCreate方法原型继承LodashWrapper和LazyWrapper,mixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)和Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。匿名函数执行;(function() {}.call(this));暴露 lodash var _ = runInContext();runInContext 函数这里的简版源码,只关注函数入口和返回值。 var runInContext = (function runInContext(context) { // 浏览器中处理context为window // ... function lodash(value) {}{ // ... return new LodashWrapper(value); } // ... return lodash;});可以看到申明了一个runInContext函数。里面有一个lodash函数,最后处理返回这个lodash函数。 再看lodash函数中的返回值 new LodashWrapper(value)。 LodashWrapper 函数function LodashWrapper(value, chainAll) { this.__wrapped__ = value; this.__actions__ = []; this.__chain__ = !!chainAll; this.__index__ = 0; this.__values__ = undefined;}设置了这些属性: ...

September 10, 2019 · 10 min · jiezi

Spring-IOC过程源码解析

废话不多说,我们先做一个傻瓜版的IOC demo作为例子自定义的Bean定义 class MyBeanDefinition{ public String id; public String className; public String value; public MyBeanDefinition(String id, String className, String value) { this.id = id; this.className = className; this.value = value; }}自定义的Bean工厂 class MyBeanFactory { Map<String, Object> beanMap = new HashMap<>(); public MyBeanFactory(MyBeanDefinition beanDefinition) throws ClassNotFoundException, IllegalAccessException, InstantiationException { Class<?> beanClass = Class.forName(beanDefinition.className); Object bean = beanClass.newInstance(); ((UserService) bean).setName(beanDefinition.value); beanMap.put(beanDefinition.id, bean); } public Object getBean(String id) { return beanMap.get(id); }}测试傻瓜版IOC容器 ...

September 10, 2019 · 9 min · jiezi

深入理解GoruntimeSetFinalizer原理剖析

finalizer是与对象关联的一个函数,通过runtime.SetFinalizer 来设置,它在对象被GC的时候,这个finalizer会被调用,以完成对象生命中最后一程。由于finalizer的存在,导致了对象在三色标记中,不可能被标为白色对象,也就是垃圾,所以,这个对象的生命也会得以延续一个GC周期。正如defer一样,我们也可以通过 Finalizer 完成一些类似于资源释放的操作 1. 结构概览1.1. heaptype mspan struct { // 当前span上所有对象的special串成链表 // special中有个offset,就是数据对象在span上的offset,通过offset,将数据对象和special关联起来 specials *special // linked list of special records sorted by offset.}1.2. specialtype special struct { next *special // linked list in span // 数据对象在span上的offset offset uint16 // span offset of object kind byte // kind of special}1.3. specialfinalizertype specialfinalizer struct { special special fn *funcval // May be a heap pointer. // return的数据的大小 nret uintptr // 第一个参数的类型 fint *_type // May be a heap pointer, but always live. // 与finalizer关联的数据对象的指针类型 ot *ptrtype // May be a heap pointer, but always live.}1.4. finalizertype finalizer struct { fn *funcval // function to call (may be a heap pointer) arg unsafe.Pointer // ptr to object (may be a heap pointer) nret uintptr // bytes of return values from fn fint *_type // type of first argument of fn ot *ptrtype // type of ptr to object (may be a heap pointer)}1.5. 全局变量var finlock mutex // protects the following variables// 运行finalizer的g,只有一个g,不用的时候休眠,需要的时候再唤醒var fing *g // goroutine that runs finalizers// finalizer的全局队列,这里是已经设置的finalizer串成的链表var finq *finblock // list of finalizers that are to be executed// 已经释放的finblock的链表,用finc缓存起来,以后需要使用的时候可以直接取走,避免再走一遍内存分配了var finc *finblock // cache of free blocksvar finptrmask [_FinBlockSize / sys.PtrSize / 8]bytevar fingwait bool // fing的标志位,通过 fingwait和fingwake,来确定是否需要唤醒fingvar fingwake bool// 所有的blocks串成的链表var allfin *finblock // list of all blocks2. 源码分析2.1. 创建finalizer2.1.1. mainfunc main() { // i 就是后面说的 数据对象 var i = 3 // 这里的func 就是后面一直说的 finalizer runtime.SetFinalizer(&i, func(i *int) { fmt.Println(i, *i, "set finalizer") }) time.Sleep(time.Second * 5)}2.1.2. SetFinalizer根据 数据对象 ,生成一个special对象,并绑定到 数据对象 所在的span,串联到span.specials上,并且确保fing的存在 ...

September 8, 2019 · 8 min · jiezi

JDK源码那些事儿之SynchronousQueue上篇

今天继续来讲解阻塞队列,一个比较特殊的阻塞队列SynchronousQueue,通过Executors框架提供的线程池cachedThreadPool中我们可以看到其被使用作为可缓存线程池的队列实现,下面通过源码来了解其内部实现,便于后面帮助我们更好的使用线程池 前言JDK版本号:1.8.0_171synchronousQueue是一个没有数据缓冲的阻塞队列,生产者线程的插入操作put()必须等待消费者的删除操作take(),反过来也一样。当然,也可以不进行等待直接返回,例如poll和offer 在使用上很好理解,每次操作都需要找到对应的匹配操作,如A线程通过put插入操作填入值1,如果无其他线程操作则需要阻塞等待一个线程执行take操作A线程才能继续,反过来同样道理,这样看似乎synchronousQueue是没有队列进行保存数据的,每次操作都在等待其互补操作一起执行 这里和其他阻塞队列不同之处在于,内部类将入队出队操作统一封装成了一个接口实现,内部类数据保存的是每个操作动作,比如put操作,保存插入的值,并根据标识来判断是入队还是出队操作,如果是take操作,则值为null,通过标识符能判断出来是出队操作 多思考下,我们需要找到互补的操作必然需要一个公共的区域来判断已经发生的所有操作,内部类就是用来进行这些操作的,SynchronousQueue分为公平策略(FIFO)和非公平策略(LIFO),两种策略分别对应其两个内部类实现,公平策略使用队列结构实现,非公平策略使用栈结构实现 由于篇幅过长,本篇先说明SynchronousQueue相关知识和公平策略下的实现类TransferQueue,下篇将说明非公平策略下的实现类TransferStack和其他知识 类定义public class SynchronousQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable 常量/变量 /** The number of CPUs, for spin control */ // cpu数量,会在自旋控制时使用 static final int NCPUS = Runtime.getRuntime().availableProcessors(); // 自旋次数,指定了超时时间时使用,这个常量配合CAS操作使用,相当于循环次数 // 如果CAS操作失败,则根据这个参数判断继续循环 static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32; // 自旋次数,未指定超时时间时使用 static final int maxUntimedSpins = maxTimedSpins * 16; /** * The number of nanoseconds for which it is faster to spin * rather than to use timed park. A rough estimate suffices. */ // 自旋超时时间阈值,在设置的时间超过这个时间时以这个时间为准,单位,纳秒 static final long spinForTimeoutThreshold = 1000L; // 后进先出队列和先进先出队列 @SuppressWarnings("serial") static class WaitQueue implements java.io.Serializable { } static class LifoWaitQueue extends WaitQueue { private static final long serialVersionUID = -3633113410248163686L; } static class FifoWaitQueue extends WaitQueue { private static final long serialVersionUID = -3623113410248163686L; } // 序列化操作使用 private ReentrantLock qlock; private WaitQueue waitingProducers; private WaitQueue waitingConsumers; /** * The transferer. Set only in constructor, but cannot be declared * as final without further complicating serialization. Since * this is accessed only at most once per public method, there * isn't a noticeable performance penalty for using volatile * instead of final here. */ // 所有的队列操作都通过transferer来执行,统一方法执行 // 初始化时会根据所选的策略实例化对应的内部实现类 private transient volatile Transferer<E> transferer;从上边也能看出没有设置变量来保存入队出队操作的数据,统一操作方法都放置到了Transferer中 ...

September 7, 2019 · 9 min · jiezi

antd源码分析之折叠面板collapse

官方文档 https://ant.design/components... 目录一、antd中的collapse 代码目录 1、组件结构图(♦♦♦重要) 2、源码节选:antd/components/collapse/collapse.tsx 3、源码节选:antd/components/collapse/CollapsePanel.tsx 二、RcCollapse 代码目录 1、组件内部属性结构及方法调用关系图(♦♦♦重要) 2、组件应用的设计模式(♦♦♦重要) 3、源码节选:rc-collapse/Collapse.jsx 4、源码节选:rc-collapse/panel.jsx 一、antd中的collapseantd组件中有些使用了React 底层基础组件(查看具体列表点这里),collapse就是这种类型的组件 antd中collapse主要源码及组成结构如下,其中红色标注的Rc开头的组件是React底层基础组件 代码目录 1、组件结构图: 2、antd/components/collapse/collapse.tsxexport default class Collapse extends React.Component<CollapseProps, any> { static Panel = CollapsePanel; static defaultProps = { prefixCls: 'ant-collapse', bordered: true, openAnimation: { ...animation, appear() { } }, }; renderExpandIcon = () => { return ( <Icon type="right" className={`arrow`} /> ); } render() { const { prefixCls, className = '', bordered } = this.props; const collapseClassName = classNames({ [`${prefixCls}-borderless`]: !bordered, }, className); return ( <RcCollapse {...this.props} className={collapseClassName} expandIcon={this.renderExpandIcon} /> ); }}3、antd/components/collapse/CollapsePanel.tsxexport default class CollapsePanel extends React.Component<CollapsePanelProps, {}> { render() { const { prefixCls, className = '', showArrow = true } = this.props; const collapsePanelClassName = classNames({ [`${prefixCls}-no-arrow`]: !showArrow, }, className); return <RcCollapse.Panel {...this.props} className={collapsePanelClassName} />; }}二、RcCollapse由上述Collapse源码不难看出,折叠面板组件的实现逻辑主要在RcCollapse中,下面是核心代码、组件内部属性结构及方法调用关系图 ...

August 20, 2019 · 3 min · jiezi

Java容器List容器使用方法及源码分析

List容器ArrayList:使用动态数组保存元素,支持随机访问。Vector:与ArrayList类似,但是它是线程安全的。LinkedList:使用双向链表保存元素,只能顺序访问,此外可以用作为栈、队列和双向队列。1 ArrayList1.1 简介基于动态数组实现了List接口。除了List接口的所有方法之外,还提供了调整内部数组大小的方法。该类与Vector类大致相同,区别在于ArrayList是不支持同步的。 size,isEmpty,get,set,iterator和listIterator方法都只需时间复杂度为O(1)。其他的操作时间复杂度为O(n)。且常数因子与LinkedList类相比更低。 每个ArrayList实例都有一个capacity,描述了该列表中实际用于存储元素的数组的大小。当向列表中添加元素时,capacity会自动增大。使用ensureCapacity方法,可以在向列表中添加大量元素之前先使数组扩容,避免列表自身多次自动扩容。 1.2 存储结构ArrayList内部使用一个数组来保存元素,ArrayList的容量就表示该数组的大小, transient Object[] elementData; // non-private to simplify nested class access任何空的ArrayList中的elementData数组用一个默认的空数组表示, private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};当向空ArrayList中添加第一个元素时,会扩容到DEFAULT_CAPACITY大小, private static final int DEFAULT_CAPACITY = 10;此外,ArrayList中还使用一个size记录当前保存元素的个数, private int size;1.3 添加元素add(E e)方法可以向ArrayList的末尾添加一个元素, public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }其中会先调用ensureCapacityInternal(int minCapacity)方法对elementData数组的大小进行检查与扩容, private void ensureCapacityInternal(int minCapacity) { // ensureCapacity方法返回数组所需要的大小 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }然后调用ensureExplicitCapacity(int minCapacity)方法, ...

August 19, 2019 · 3 min · jiezi

Go-gcache-源码分析图解

概述gcache是一个用go实现的并发安全的本地缓存库。他可以实现如下功能: 指定缓存的的大小,初始化之时为cache设置size大小。支持多种缓存的策略:Simple、LRU、LFU、ARCSimple:最普通的缓存策略,根据先存入的先淘汰。LUR:Least Recently Used,意思是最近最少使用。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。LFU:Least Frequently Used ,意思是最近最不常用。LFU Cache先淘汰一定时间内被访问次数最少的页面。ARC:Adaptive Replacement Cache,ARC介于 LRU 和 LFU 之间。支持多个回调函数LoaderExpireFunc:过期回调函数EvictedFunc:淘汰回调函数PurgeVisitorFunc:清除所有key回调函数AddedFunc:新怎key回调函数SerializeFunc:对value序列化回调函数DeserializeFunc:对value反序列化回调函数支持计数事件HitCount:命中次数MissCount:没有命中的次数LookupCount:查找次数HitRate:命中率使用singleflight机制,多个人请求一个key,保证只有一个真正获取数据其余等待结果。简单使用其实github上已经有了很详细的例子,其中有简单key/value、设置超时时间、设置淘汰策略、设置回调函数等各种例子。这里简单摘抄一些简单的例子: 简单key/value 设置package mainimport ( "github.com/bluele/gcache" "fmt")func main() { gc := gcache.New(20). LRU(). Build() gc.Set("key", "ok") value, err := gc.Get("key") if err != nil { panic(err) } fmt.Println("Get:", value)}Get: ok设置过期时间package mainimport ( "github.com/bluele/gcache" "fmt" "time")func main() { gc := gcache.New(20). LRU(). Build() gc.SetWithExpire("key", "ok", time.Second*10) value, _ := gc.Get("key") fmt.Println("Get:", value) // Wait for value to expire time.Sleep(time.Second*10) value, err = gc.Get("key") if err != nil { panic(err) } fmt.Println("Get:", value)}Get: ok// 10 seconds later, new attempt:panic: ErrKeyNotFound使用load回调函数package mainimport ( "github.com/bluele/gcache" "fmt")func main() { gc := gcache.New(20). LRU(). LoaderFunc(func(key interface{}) (interface{}, error) { return "ok", nil }). Build() value, err := gc.Get("key") if err != nil { panic(err) } fmt.Println("Get:", value)}Get: ok源码分析实体和初始化builder类// 缓存builder对象,存放时间、大小和各种回调函数type CacheBuilder struct { clock Clock tp string size int loaderExpireFunc LoaderExpireFunc evictedFunc EvictedFunc purgeVisitorFunc PurgeVisitorFunc addedFunc AddedFunc expiration *time.Duration deserializeFunc DeserializeFunc serializeFunc SerializeFunc}设置过期时间、策略、回调函数// 设置策略 设置CacheBuilder的回调函数属性func (cb *CacheBuilder) LRU() *CacheBuilder { return cb.EvictType(TYPE_LRU)}// 设置过期时间 设置CacheBuilder的Expiration属性func (cb *CacheBuilder) Expiration(expiration time.Duration) *CacheBuilder { cb.expiration = &expiration return cb}// 设置驱除回调函数func (cb *CacheBuilder) EvictedFunc(evictedFunc EvictedFunc) *CacheBuilder { cb.evictedFunc = evictedFunc return cb}build 输出cache对象// 判断size和类型func (cb *CacheBuilder) Build() Cache { if cb.size <= 0 && cb.tp != TYPE_SIMPLE { panic("gcache: Cache size <= 0") } return cb.build()}// 根据type来新建相对应的cache对象func (cb *CacheBuilder) build() Cache { switch cb.tp { case TYPE_SIMPLE: return newSimpleCache(cb) case TYPE_LRU: return newLRUCache(cb) case TYPE_LFU: return newLFUCache(cb) case TYPE_ARC: return newARC(cb) default: panic("gcache: Unknown type " + cb.tp) }}// 举例一个SimpleCache func newSimpleCache(cb *CacheBuilder) *SimpleCache { c := &SimpleCache{} buildCache(&c.baseCache, cb) c.init() c.loadGroup.cache = c return c}// init 初始化simple 中的mapfunc (c *SimpleCache) init() { if c.size <= 0 { c.items = make(map[interface{}]*simpleItem) } else { c.items = make(map[interface{}]*simpleItem, c.size) }}// 初始化回调函数func buildCache(c *baseCache, cb *CacheBuilder) { c.clock = cb.clock c.size = cb.size c.loaderExpireFunc = cb.loaderExpireFunc c.expiration = cb.expiration c.addedFunc = cb.addedFunc c.deserializeFunc = cb.deserializeFunc c.serializeFunc = cb.serializeFunc c.evictedFunc = cb.evictedFunc c.purgeVisitorFunc = cb.purgeVisitorFunc c.stats = &stats{}}接口和总体流程type Cache interface { Set(key, value interface{}) error SetWithExpire(key, value interface{}, expiration time.Duration) error Get(key interface{}) (interface{}, error) GetIFPresent(key interface{}) (interface{}, error) GetALL(checkExpired bool) map[interface{}]interface{} get(key interface{}, onLoad bool) (interface{}, error) Remove(key interface{}) bool Purge() Keys(checkExpired bool) []interface{} Len(checkExpired bool) int Has(key interface{}) bool statsAccessor}type statsAccessor interface { HitCount() uint64 MissCount() uint64 LookupCount() uint64 HitRate() float64}type baseCache struct { clock Clock size int loaderExpireFunc LoaderExpireFunc evictedFunc EvictedFunc purgeVisitorFunc PurgeVisitorFunc addedFunc AddedFunc deserializeFunc DeserializeFunc serializeFunc SerializeFunc expiration *time.Duration mu sync.RWMutex loadGroup Group *stats}SimpleCacheSimpleCache是gcache中最简单的一种,其中比较重要的函数就是Get,Set。在SimpleCache结构体中items保存这simpleItem。simpleItem结构体中保存具体值和过期时间。Get,Set函数就是通过操作items属性来保存和获取缓存中的值的。下面我们详细看一下代码: ...

August 8, 2019 · 8 min · jiezi

java并发编程学习之Condition分析二

ConditionObjectCondition在ReentrantLock中,实际上是创建AQS的ConditionObject对象,主要的成员变量有Node类型的firstWaiter和lastWaiter,作为头节点和尾节点,是单向链表。当调用await时,加入队列,signal时,加入到AQS的阻塞队列。 await方法把节点移到Condition队列后挂起 public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter();//添加一个节点成为尾节点 int savedState = fullyRelease(node);//释放所有持有的锁 int interruptMode = 0; while (!isOnSyncQueue(node)) {//要么中断,要么进入阻塞队列,退出while循环 LockSupport.park(this);//挂起 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//中断过,就跳出循环 break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE)//是否被中断。acquireQueued之前讲过 interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled如果尾节点不为空 unlinkCancelledWaiters();//将不是CONDITION状态的移除出去 if (interruptMode != 0) reportInterruptAfterWait(interruptMode);//重新中断}addConditionWaiter,如果尾节点不在队列里,先移除已取消的节点,添加一个节点成为尾节点 private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out.如果尾节点不为空,但是状态不是CONDITION,说明已取消,不想在Condition的队列里,就移除 if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters();//将不是CONDITION状态的移除出去 t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION);//创建状态是CONDITION的节点 if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node;//加入到尾节点 return node;}fullyRelease方法 ...

July 26, 2019 · 2 min · jiezi

Go-Micro-Broker-源码分析

概述在第一篇概述文章中已经提到了在Micro中 Broker的作用,Go Micro 总体设计。我们也知道Micro是一个可插拔的分布式框架,我们可以使用kafka,rabbitmq,cache,redis,nats等各种实现具体可以在git上的插件库中找到go-plugins我们再来看一下接口: type Broker interface { Init(...Option) error Options() Options Address() string Connect() error Disconnect() error Publish(topic string, m *Message, opts ...PublishOption) error Subscribe(topic string, h Handler, opts ...SubscribeOption) (Subscriber, error) String() string}Connect 开启brokerSubsribe 注册对某个topic的监听Publish 发布某个topic的消息Disconnect 关闭broker 简单使用本片文章使用的是默认broker实现httpbroker分析,是一个比较简单和容易理解的方式,代码地址。 PubTopic函数为发布函数SubTopic函数为订阅函数下面的例子其实不需要很多注释,就是简单的使用一个发布一个订阅函数来实现。package mainimport ( "fmt" "github.com/micro/go-micro/broker" "log" "time")func main() { if err := broker.Connect(); err != nil { fmt.Println("Broker Connect error: %v", err) } go PubTopic("SubServerName") go SubTopic("SubServerName") time.Sleep(time.Second * 10)}func PubTopic(topic string) { tick := time.NewTicker(time.Second) i := 0 for _ = range tick.C { msg := &broker.Message{ Header: map[string]string{ "id": fmt.Sprintf("%d", i), }, Body: []byte(fmt.Sprintf("%d: %s", i, time.Now().String())), } if err := broker.Publish(topic, msg); err != nil { log.Printf("[pub] failed: %v", err) } else { fmt.Println("[pub] pubbed message:", string(msg.Body)) } i++ }}func SubTopic(topic string) { _, err := broker.Subscribe(topic, func(p broker.Event) error { fmt.Println("[sub] received message:", string(p.Message().Body), "header", p.Message().Header) return nil }) if err != nil { fmt.Println(err) }}结果[pub] pubbed message: 0: 2019-07-15 15:48:00.377247298 +0800 CST m=+1.005764860[sub] received message: 0: 2019-07-15 15:48:00.377247298 +0800 CST m=+1.005764860 header map[id:0][pub] pubbed message: 1: 2019-07-15 15:48:01.376383294 +0800 CST m=+2.004891632[sub] received message: 1: 2019-07-15 15:48:01.376383294 +0800 CST m=+2.004891632 header map[id:1][pub] pubbed message: 2: 2019-07-15 15:48:02.377595797 +0800 CST m=+3.006094892[sub] received message: 2: 2019-07-15 15:48:02.377595797 +0800 CST m=+3.006094892 header map[id:2][pub] pubbed message: 3: 2019-07-15 15:48:03.376685455 +0800 CST m=+4.005175327[sub] received message: 3: 2019-07-15 15:48:03.376685455 +0800 CST m=+4.005175327 header map[id:3][pub] pubbed message: 4: 2019-07-15 15:48:04.377715895 +0800 CST m=+5.006196526[sub] received message: 4: 2019-07-15 15:48:04.377715895 +0800 CST m=+5.006196526 header map[id:4]源码分析Connect开启tcp监听启一个goroutine,在registerInterval间隔对subscriber就行注册,类似心跳设置服务发现注册服务设置缓存对象设置running = truefunc (h *httpBroker) Connect() error { h.RLock() if h.running { h.RUnlock() return nil } h.RUnlock() h.Lock() defer h.Unlock() var l net.Listener var err error // 创建监听函数 判断是否有配置 if h.opts.Secure || h.opts.TLSConfig != nil { config := h.opts.TLSConfig fn := func(addr string) (net.Listener, error) { if config == nil { hosts := []string{addr} // check if its a valid host:port if host, _, err := net.SplitHostPort(addr); err == nil { if len(host) == 0 { hosts = maddr.IPs() } else { hosts = []string{host} } } // generate a certificate cert, err := mls.Certificate(hosts...) if err != nil { return nil, err } config = &tls.Config{Certificates: []tls.Certificate{cert}} } return tls.Listen("tcp", addr, config) } l, err = mnet.Listen(h.address, fn) } else { fn := func(addr string) (net.Listener, error) { return net.Listen("tcp", addr) } l, err = mnet.Listen(h.address, fn) } if err != nil { return err } addr := h.address h.address = l.Addr().String() go http.Serve(l, h.mux) go func() { // 根据设置的registerInterval 心跳时间,检测服务是否存活 h.run(l) h.Lock() h.opts.Addrs = []string{addr} h.address = addr h.Unlock() }() // get registry reg, ok := h.opts.Context.Value(registryKey).(registry.Registry) if !ok { reg = registry.DefaultRegistry } // set cache h.r = cache.New(reg) // set running h.running = true return nil}Subscribe解析address创建唯一id拼装服务信息 最后的服务信息如下图调用Register(默认的是mdns)注册服务把service放到subscribers map[string][]*httpSubscriber中 ...

July 15, 2019 · 5 min · jiezi

Go-Micro-Register-源码分析

概述Go Micro是一个微服务框架分布式框架,既然是分布式那服务的注册和发现就是不可避免的。Micro又是一个可插拔插件的框架,只要实现下面代码中的接口就可以使用各种不同的服务注册发现。现在代码库中已经可以支持consul,etcd,zk等各种。下面我们来看一下Micro框架是如何注册和发现服务的。 流程服务端把服务的地址信息保存到Registry, 然后定时的心跳检查,或者定时的重新注册服务。客户端监听Registry,最好是把服务信息保存到本地,监听服务的变动,更新缓存。当调用服务端的接口是时,根据客户端的服务列表和负载算法选择服务端进行通信。 Register() 服务端服务注册Deregister() 服务端服务注销GetService() 客户端获取服务节点ListServices() 获取所有服务节点Watch() 客户端获取watcher监听Registry的服务节点信息type Registry interface { Init(...Option) error Options() Options Register(*Service, ...RegisterOption) error Deregister(*Service) error GetService(string) ([]*Service, error) ListServices() ([]*Service, error) Watch(...WatchOption) (Watcher, error) String() string}以下我们就以consul作为服务注册发现来分析源码服务端注册服务当服务开启时,调用consul插件作为服务发现注册。准备服务对象,包括服务名等信息。往consul中写入services节点。根据设置的RegisterInterval时间间隔参数,循环检测服务是否可用监听已注册服务。调用consul设置服务存货时间TTL。consul添加完节点如下图(下图展示了同一个服务名 开启了2个服务,相当于分布式的两台机器): 源码分析 // 服务开启 调用run方法func (s *service) Run() error { // 调用start函数 if err := s.Start(); err != nil { return err } ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) select { // wait on kill signal case <-ch: // wait on context cancel case <-s.opts.Context.Done(): } return s.Stop()}// 调用服务start方法 func (s *service) Start() error { // 执行服务开始之前函数列表 for _, fn := range s.opts.BeforeStart { if err := fn(); err != nil { return err } } // 服务开始 if err := s.opts.Server.Start(); err != nil { return err } // 执行服务结束之后函数列表 for _, fn := range s.opts.AfterStart { if err := fn(); err != nil { return err } } return nil}// 服务开启函数 func (s *rpcServer) Start() error { // 省略其他逻辑代码。。。 // 调用RegisterCheck检查注册服务是否可用 可从外部注入函数 if err = s.opts.RegisterCheck(s.opts.Context); err != nil { log.Logf("Server %s-%s register check error: %s", config.Name, config.Id, err) } else { // 注册服务 if err = s.Register(); err != nil { log.Logf("Server %s-%s register error: %s", config.Name, config.Id, err) } } exit := make(chan bool) // 省略broker 代码 // 开启goroutine 检查服务是否可用,间隔时间为设置的RegisterInterval go func() { t := new(time.Ticker) // only process if it exists if s.opts.RegisterInterval > time.Duration(0) { // new ticker t = time.NewTicker(s.opts.RegisterInterval) } // return error chan var ch chan error Loop: for { select { // register self on interval case <-t.C: s.RLock() registered := s.registered s.RUnlock() if err = s.opts.RegisterCheck(s.opts.Context); err != nil && registered { log.Logf("Server %s-%s register check error: %s, deregister it", config.Name, config.Id, err) // deregister self in case of error if err := s.Deregister(); err != nil { log.Logf("Server %s-%s deregister error: %s", config.Name, config.Id, err) } } else { if err := s.Register(); err != nil { log.Logf("Server %s-%s register error: %s", config.Name, config.Id, err) } } // wait for exit case ch = <-s.exit: t.Stop() close(exit) break Loop } } // deregister self if err := s.Deregister(); err != nil { log.Logf("Server %s-%s deregister error: %s", config.Name, config.Id, err) } // wait for requests to finish if s.wg != nil { s.wg.Wait() } // close transport listener ch <- ts.Close() // disconnect the broker config.Broker.Disconnect() // swap back address s.Lock() s.opts.Address = addr s.Unlock() }() return nil}// 调用consul插件中的Register函数func (c *consulRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error { // 省略组成注册服务和判断是否错误代码 // 向consul中写入服务 if err := c.Client.Agent().ServiceRegister(asr); err != nil { return err } // save our hash and time check of the service c.Lock() c.register[s.Name] = h c.lastChecked[s.Name] = time.Now() c.Unlock() // if the TTL is 0 we don't mess with the checks if options.TTL == time.Duration(0) { return nil } // pass the healthcheck return c.Client.Agent().PassTTL("service:"+node.Id, "")}// 使用http方式往consul中添加服务func (a *Agent) ServiceRegister(service *AgentServiceRegistration) error { r := a.c.newRequest("PUT", "/v1/agent/service/register") r.obj = service _, resp, err := requireOK(a.c.doRequest(r)) if err != nil { return err } resp.Body.Close() return nil}订阅服务注册调用broker的Subscribe(订阅方法)调用Register Register函数往consul添加服务节点如下图 ...

July 15, 2019 · 4 min · jiezi

Go-Micro-总体设计

Go-micro 是什么Go-micro框架是一套微服务分布式的框架,可以大幅度的提高开发效率。源码地址:https://github.com/micro/go-microGo-micro拥有很多特性: 服务注册、发现负载均衡消息解码,并默认支持json以及protobuf基于rpc的请求响应异步的消息通讯接口可插拔其中最值得一提的是最后一个特性,接口可插拔。只要实现上图的8个关键interface,就可以随意的根据需求重新时间这8个接口的功能。 这8个接口一实现了go-micro的整体架构。这些接口都有默认的实现方式,意味着你不需要写任何的插件就可以使用这个微服务架构。 主要interface整个Go Micro 都是有这8个interface构成的,换而言之只要理解了这8个接口,并仔细研究其中一个实现基本就能了解整个框架的实现和架构。下面先来看看这8个接口 Transort服务之间通信的接口。也就是服务发送和接收的最终实现方式,是由这些接口定制的。 type Socket interface { Recv(*Message) error Send(*Message) error Close() error}type Client interface { Socket}type Listener interface { Addr() string Close() error Accept(func(Socket)) error}type Transport interface { Dial(addr string, opts ...DialOption) (Client, error) Listen(addr string, opts ...ListenOption) (Listener, error) String() string}Codec有了传输方式,下面要解决的就是传输编码和解码问题,go-micro有很多种编码解码方式,默认的实现方式是protobuf,当然也有其他的实现方式,json、protobuf、jsonrpc、mercury等等。 源码 type Codec interface { ReadHeader(*Message, MessageType) error ReadBody(interface{}) error Write(*Message, interface{}) error Close() error String() string}type Message struct { Id uint64 Type MessageType Target string Method string Error string Header map[string]string}Codec接口的Write方法就是编码过程,两个Read是解码过程。 ...

July 10, 2019 · 2 min · jiezi

Go-Micro-Options-函数选项模式

函数选项 Functimional Options在Go语言中是没有默认函数的,但是我们可以使用函数选项模式来优雅的解决这个问题。函数选项模式不仅仅可以解决默认函数的问题还可以解决大量参数造成的代码复杂的问题。使用这个模式的有点: 支持默认参数:不必像结构体参数那样代码简介:即使想go-micro中 像Broker Cmd Client Server Registry 和BefroeStart等等都可以优雅的传入。扩展性好:如果有新增的参数,可以少量代码打到效果。函数选项模式在Go中的应用1. 先看几个模式使用的实力对象Option 封装了一个函数 函数接受一个Options的指针参数 Options 是具体的Micro 的具体选项实体// Option封装了一个函数type Option func(*Options)// 函数选项的具体对象// 保存了注册 客户端 服务以及 服务开始的方法列表 服务开启之后的方法列表等等type Options struct { Broker broker.Broker Cmd cmd.Cmd Client client.Client Server server.Server Registry registry.Registry Transport transport.Transport // Before and After funcs BeforeStart []func() error BeforeStop []func() error AfterStart []func() error AfterStop []func() error // Other options for implementations of the interface // can be stored in a context Context context.Context}2. Micro如何使用函数选项NewService 方法调用了内部方法newServicenewServices 调用了内部方法newOptions newOptions 1. 方法先给了默认的实现方式2. 循环传入的参数Options函数 执行方法 传入opt指针对象 3. 最终返回services对象func NewService(opts ...Option) Service { return newService(opts...)}func newService(opts ...Option) Service { options := newOptions(opts...) options.Client = &clientWrapper{ options.Client, metadata.Metadata{ HeaderPrefix + "From-Service": options.Server.Options().Name, }, } return &service{ opts: options, }}func newOptions(opts ...Option) Options { opt := Options{ Broker: broker.DefaultBroker, Cmd: cmd.DefaultCmd, Client: client.DefaultClient, Server: server.DefaultServer, Registry: registry.DefaultRegistry, Transport: transport.DefaultTransport, Context: context.Background(), } for _, o := range opts { o(&opt) } return opt}例子可以看到Name函数 接受一个string 返回一个Option 函数内部接受一个Options指针参数 内部给server复制了那么属性剩下的RegisterTTL 给server对象复制了time to live(生存时间)RegisterInterval函数设置了server的注册间隔时间可以看到 想Micro框架这么复杂的对象和这么多的设置,在不能使用默认参数的情况下,使用了函数选项模式,很优雅的实现了功能同事代码也很清楚和优雅。 ...

July 10, 2019 · 2 min · jiezi

Spring-Cloud-Alibaba-Nacos心跳与选举

通过阅读NACOS的源码,了解其心跳与选举机制。开始阅读此篇文章之前,建议先阅读如下两篇文章: Spring Cloud Alibaba Nacos(功能篇) Spring Cloud Alibaba Nacos(源码篇) 一、心跳机制只有NACOS服务与所注册的Instance之间才会有直接的心跳维持机制,换言之,这是一种典型的集中式管理机制。 在client这一侧是心跳的发起源,进入NacosNamingService,可以发现,只有注册服务实例的时候才会构造心跳包: @Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { if (instance.isEphemeral()) { BeatInfo beatInfo = new BeatInfo(); beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName)); beatInfo.setIp(instance.getIp()); beatInfo.setPort(instance.getPort()); beatInfo.setCluster(instance.getClusterName()); beatInfo.setWeight(instance.getWeight()); beatInfo.setMetadata(instance.getMetadata()); beatInfo.setScheduled(false); beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo); } serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance); }没有特殊情况,目前ephemeral都是true。BeatReactor维护了一个Map对象,记录了需要发送心跳的BeatInfo,构造了一个心跳包后,BeatReactor.addBeatInfo方法将BeatInfo放入Map中。然后,内部有一个定时器,每隔5秒发送一次心跳。 class BeatProcessor implements Runnable { @Override public void run() { try { for (Map.Entry<String, BeatInfo> entry : dom2Beat.entrySet()) { BeatInfo beatInfo = entry.getValue(); if (beatInfo.isScheduled()) { continue; } beatInfo.setScheduled(true); executorService.schedule(new BeatTask(beatInfo), 0, TimeUnit.MILLISECONDS); } } catch (Exception e) { NAMING_LOGGER.error("[CLIENT-BEAT] Exception while scheduling beat.", e); } finally { executorService.schedule(this, clientBeatInterval, TimeUnit.MILLISECONDS); } } }通过设置scheduled的值来控制是否已经下发了心跳任务,具体的心跳任务逻辑放在了BeatTask。 ...

July 8, 2019 · 4 min · jiezi

VUE2610computed计算属性

computed初始化在实例化Vue对象得时候,我们通过computed来定义计算属性: var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { return this.message.split('').reverse().join('') } }})在实例化时,初始化计算属性initComputed(源码路径/src/core/instance/state.js) for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get .... //定义Watcher vm._computedWatchers[key] = new Watcher({ vm, getter || noop, noop, { lazy: true } //调用时才计算属性的值 }) // defineComputed(vm, key, userDef)}在defineComputed重新定义属性 ...

July 7, 2019 · 1 min · jiezi

Spring-Cloud-Alibaba-Nacos源码篇

在看这篇文章之前,最好对NACOS相关功能有所了解,推荐看完Spring Cloud Alibaba Nacos(功能篇)。 针对功能,有目的的去找相对应的源代码,进一步了解功能是如何被实现出来的。 本文针对有一定源代码阅读经验的人群,不会深入太多的细节,还需要读者打开源码跟踪,自行领会。 一、引子进入GitHub对应的页面,将NACOS工程clone下来。目录和文件看起来很冗长,但是对于看源代码真正有帮助的部分并不多。 有了这三张图,就能顺利找到突破口了,核心内容就集中在nacos-console,nacos-naming,nacos-config,顺藤摸瓜,就能看到不少内容了。 如果还是感觉无从下手的话,那就移步nacos-example,里面有主要业务的调用入口,一看便知。 二、配置服务首先从一个工厂类说起:com.alibaba.nacos.api.NacosFactory。 里面的静态方法用于创建ConfigService和NamingService,代码类似,以创建ConfigService为例: public static ConfigService createConfigService(Properties properties) throws NacosException { try { Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService"); Constructor constructor = driverImplClass.getConstructor(Properties.class); ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties); return vendorImpl; } catch (Throwable e) { throw new NacosException(-400, e.getMessage()); }}没有什么复杂的逻辑,使用的是基本的反射原理。构造参数传入了properties,这些属性可以通过bootstrap.yml中指定,对应的是NacosConfigProperties。 需要细看的是构造函数中对于namespace初始化的那部分内容。 private void initNamespace(Properties properties) { String namespaceTmp = null; String isUseCloudNamespaceParsing = properties.getProperty(PropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING, System.getProperty(SystemPropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING, String.valueOf(Constants.DEFAULT_USE_CLOUD_NAMESPACE_PARSING))); if (Boolean.valueOf(isUseCloudNamespaceParsing)) { namespaceTmp = TemplateUtils.stringBlankAndThenExecute(namespaceTmp, new Callable<String>() { @Override public String call() { return TenantUtil.getUserTenantForAcm(); } }); namespaceTmp = TemplateUtils.stringBlankAndThenExecute(namespaceTmp, new Callable<String>() { @Override public String call() { String namespace = System.getenv(PropertyKeyConst.SystemEnv.ALIBABA_ALIWARE_NAMESPACE); return StringUtils.isNotBlank(namespace) ? namespace : EMPTY; } }); } if (StringUtils.isBlank(namespaceTmp)) { namespaceTmp = properties.getProperty(PropertyKeyConst.NAMESPACE); } namespace = StringUtils.isNotBlank(namespaceTmp) ? namespaceTmp.trim() : EMPTY; properties.put(PropertyKeyConst.NAMESPACE, namespace);}传入的properties会指定是否解析云环境中的namespace参数,如果是的,就是去读取阿里云环境的系统变量;如果不是,那么就读取properties中指定的namespace,没有指定的话,最终解析出来的是空字符串。从代码上看出来,获取云环境的namespace做成了异步化的形式,但是目前版本还是使用的同步调用。 ...

July 3, 2019 · 4 min · jiezi

自然语言处理手撕-FastText-源码02基于字母的-Ngram-实现FastTexts-subwords

作者:LogM 本文原载于 https://segmentfault.com/u/logm/articles ,不允许转载~ 1. 源码来源FastText 源码:https://github.com/facebookresearch/fastText 本文对应的源码版本:Commits on Jun 27 2019, 979d8a9ac99c731d653843890c2364ade0f7d9d3 FastText 论文: [1] P. Bojanowski, E. Grave, A. Joulin, T. Mikolov, Enriching Word Vectors with Subword Information [2] A. Joulin, E. Grave, P. Bojanowski, T. Mikolov, Bag of Tricks for Efficient Text Classification 2. 概述之前的博客介绍了"分类器的预测"的源码,里面有一个重点没有详细展开,就是"基于字母的 Ngram 是怎么实现的"。这块论文里面关于"字母Ngram的生成"讲的比较清楚,但是对于"字母Ngram"如何加入到模型中,讲的不太清楚,所以就求助于源码,源码里面把这块叫做 Subwords。 看懂了源码其实会发现 Subwords 加入到模型很简单,就是把它和"词语"一样对待,一起求和取平均。 另外,我自己再看源码的过程中还有个收获,就是关于"中文词怎么算subwords",之前我一直觉得 Subwords 对中文无效,看了源码才知道是有影响的。 最后是词向量中怎么把 Subwords 加到模型。这部分我估计大家也不怎么关心,所以我就相当于写给我自己看的,解答自己看论文的疑惑。以skipgram为例,输入的 vector 和所要预测的 vector 都是单个词语与subwords相加求和的结果。 3. 怎么计算 Subwords之前的博客有提到,Dictionary::getLine 这个函数的作用是从输入文件中读取一行,并将所有的Id(包括词语的Id,SubWords的Id,WordNgram的Id)存入到数组 words 中。 ...

June 30, 2019 · 5 min · jiezi

自然语言处理手撕-FastText-源码01分类器的预测过程

作者:LogM 本文原载于 https://segmentfault.com/u/logm/articles ,不允许转载~ 1. 源码来源FastText 源码:https://github.com/facebookre... 本文对应的源码版本:Commits on Jun 27 2019, 979d8a9ac99c731d653843890c2364ade0f7d9d3 FastText 论文: [1] P. Bojanowski, E. Grave, A. Joulin, T. Mikolov, Enriching Word Vectors with Subword Information [2] A. Joulin, E. Grave, P. Bojanowski, T. Mikolov, Bag of Tricks for Efficient Text Classification 2. 概述FastText 的论文写的比较简单,有些细节不明白,网上也查不到,所幸直接撕源码。 FastText 的"分类器"功能是用的最多的,所以先从"分类器的predict"开始挖。 3. 开撕先看程序入口的 main 函数,ok,是调用了 predict 函数。 // 文件:src/main.cc// 行数:403int main(int argc, char** argv) { std::vector<std::string> args(argv, argv + argc); if (args.size() < 2) { printUsage(); exit(EXIT_FAILURE); } std::string command(args[1]); if (command == "skipgram" || command == "cbow" || command == "supervised") { train(args); } else if (command == "test" || command == "test-label") { test(args); } else if (command == "quantize") { quantize(args); } else if (command == "print-word-vectors") { printWordVectors(args); } else if (command == "print-sentence-vectors") { printSentenceVectors(args); } else if (command == "print-ngrams") { printNgrams(args); } else if (command == "nn") { nn(args); } else if (command == "analogies") { analogies(args); } else if (command == "predict" || command == "predict-prob") { predict(args); // 这句是我们想要的 } else if (command == "dump") { dump(args); } else { printUsage(); exit(EXIT_FAILURE); } return 0;}再看 predict 函数,预处理的代码不用管,直接看 predict 的那行,调用了 FastText::predictLine。这里注意下,这是个 while 循环,所以FastText::predictLine 这个函数每次只处理一行。 ...

June 30, 2019 · 3 min · jiezi

Vue原理响应式原理-白话版

写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】响应式原理 - 白话版 本文打算 白话文的形式讲解 Vue 的响应式系统原理,尽量不涉及源码。 只阐述工作流程,不想内容过多过于繁杂,导致大家会没有什么阅读的兴趣。 所以我今后打算把每一个内容分成 白话版和 源码版。 白话版,就是让大家不用花费太多脑力,不用消耗太多时间,就能轻松地看完并大致了解内容。 有时间精力的人可以阅读源码版 ,然后自己参考源码,来进行研究学习。有什么错误的地方,感谢大家能够指出 响应式系统我们都知道,只要在 Vue 实例中声明过的数据,那么这个数据就是响应式的。 什么是响应式,也即是说,数据发生改变的时候,视图会重新渲染,匹配更新为最新的值。 也正是因为这个系统,让我们可以脱离界面的束缚,只需要操作数据。 我们可以问出下面三个问题 1、Vue 是怎么知道数据改变? 2、Vue 在数据改变时,怎么知道通知哪些视图更新? 3、Vue 在数据改变时,视图怎么知道什么时候更新? 现在,我将会讲解三个重要的概念 Object.defineProperty,依赖收集,依赖更新 Object.defineProperty这个方法,是 Vue 响应式系统的精髓,骨髓,脑髓 使用 Object.defineProperty 可以为对象中的每一个属性,设置 get 和 set 方法 Object.defineProperty 可以为属性设置很多特性,例如 configurable,enumerable,但是现在不过多解释,重点只放在 get 和 set 那么 get 和 set 方法有什么用? get 值是一个函数,当属性被访问时,会触发 get 函数 set 值同样是一个函数,当属性被赋值时,会触发 set 函数 ...

June 27, 2019 · 2 min · jiezi

JDK源码那些事儿之并发ConcurrentHashMap上篇

前面前已经说明了HashMap以及红黑树的一些基本知识,对JDK8的HashMap也有了一定的了解,本篇就开始看看并发包下的ConcurrentHashMap,说实话,还是比较复杂的,笔者在这里也不会过多深入,源码层次上了解一些主要流程即可,清楚多线程环境下整个Map的运作过程就算是很大进步了,更细的底层部分需要时间和精力来研究,暂不深入 前言jdk版本:1.8JDK7中,ConcurrentHashMap把内部细分成了若干个小的HashMap,称之为段(Segment),默认被分为16个段。多线程写操作对每个段进行加锁,段与段之间互不影响。而JDK8则抛弃了这种结构,类似HashMap,多线程下为了保证线程安全,通过CAS和synchronized进行并发控制,降低锁颗粒度,性能上也就提高许多 同时由于降低锁粒度,同时需要兼顾读操作的正确性,增加了许多内部类来帮助完成并发控制,保证读操作的正确执行,同时支持了并发扩容操作,算是相当复杂了,由于过于复杂,对ConcurrentHashMap的说明将分为两章说明,本章就对ConcurrentHashMap的常量,变量,内部类和构造方法进行说明,下一章将重点分析其中的重要方法 这里先提前说明下,有个整体印象: ConcurrentHashMap结构上类似HashMap,即数组+链表+红黑树锁颗粒度降低,复杂度提升多线程并发扩容多线程下保证读操作正确性计数方式处理:分段处理函数式编程(不是本文重点,自行查阅)类定义public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable 继承AbstractMap,实现了ConcurrentMap接口,ConcurrentMap也可以看看源码,加入了并发操作方法,是一个实现了并发访问的集合接口 常量有些常量和变量可能不是很好理解,在后边到方法时会尽量详细说明 /** * 最大容量 */ private static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认初始化容量,同HashMap,必须为2的倍数 */ private static final int DEFAULT_CAPACITY = 16; /** * 可能达到的最大的数组大小值(非2的次幂),2的31次方-8 */ static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * 默认并发级别,新版本无用,为了兼容旧版本,使用的地方在序列化方法writeObject中 */ private static final int DEFAULT_CONCURRENCY_LEVEL = 16; /** * 负载因子,只是为了兼容性 * 构造方法里指定的负载因子只会影响初始化的table容量 * 一般也不使用浮点数计算 * ConcurrentHashMap不会使用这个常量,而使用类似 n -(n >>> 2) 的方式来进行调整大小 */ private static final float LOAD_FACTOR = 0.75f; /** * 树化阈值 * 同HashMap */ static final int TREEIFY_THRESHOLD = 8; /** * 调整大小时树转化为链表的阈值 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 可以转化为红黑树的最小数组容量,即调整为红黑树时数组长度最小值必须为MIN_TREEIFY_CAPACITY * 如果bin包含太多节点,则会调整表的大小 * 该值应至少为4 * TREEIFY_THRESHOLD,避免扩容和树化阈值之间的冲突。 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * * 扩容的每个线程每次最少要迁移16个hash桶 * 每个线程都可参与迁移任务,每个线程至少要连续迁移MIN_TRANSFER_STRIDE个hash桶 * 帮助扩容提高了效率,当然复杂性也提高了很多,要处理的事情更多 */ private static final int MIN_TRANSFER_STRIDE = 16; /** * 与移位量和最大线程数相关 * 先了解就好,后边涉及到方法会进行说明 */ private static int RESIZE_STAMP_BITS = 16; /** * 帮助扩容的最大线程数 */ private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; /** * 移位量 */ private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; /** * Node hash值编码,规定了各自的含义 */ // forwarding nodes节点hash值 // 临时节点,扩容时出现,不存储实际数据 // 如果旧数组的一个hash桶中全部的节点都迁移到新数组中,旧数组就在这个hash桶中放置一个ForwardingNode // 读操作遇见该节点时,转到新的table数组上执行,写操作遇见时,则帮助扩容 static final int MOVED = -1; // hash for forwarding nodes // TREEBIN节点 // TreeBin是ConcurrentHashMap中用于代理操作TreeNode的特殊节点 // 保存实际的红黑树根节点,在红黑树插入节点时会对读操作造成影响,该对象维护了一个读写锁来保证多线程的正确性 static final int TREEBIN = -2; // hash for roots of trees // ReservationNode节点hash值 // 保留节点,JDK8的新特性会用到,这里不过多说明 static final int RESERVED = -3; // hash for transient reservations // 负数转正数,定位hash桶时用到了,负数Hash值有特殊含义,具体看后边 static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash // CPU数量,限制边界,计算一个线程需要做多少迁移量 static final int NCPU = Runtime.getRuntime().availableProcessors(); // 序列化兼容性 private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("segments", Segment[].class), new ObjectStreamField("segmentMask", Integer.TYPE), new ObjectStreamField("segmentShift", Integer.TYPE) };变量 /** * The array of bins. Lazily initialized upon first insertion. * Size is always a power of two. Accessed directly by iterators. * * volatile保证可见性 * Node类和HashMap中的类似,val和next属性通过volatile保证可见性 */ transient volatile Node<K,V>[] table; /** * The next table to use; non-null only while resizing. * * 扩容时使用的数组,只在扩容时非空,扩容时会创建 */ private transient volatile Node<K,V>[] nextTable; /** * Table initialization and resizing control. When negative, the * table is being initialized or resized: -1 for initialization, * else -(1 + the number of active resizing threads). Otherwise, * when table is null, holds the initial table size to use upon * creation, or 0 for default. After initialization, holds the * next element count value upon which to resize the table. * * sizeCtl = -1,表示有线程在进行初始化操作 * sizeCtl < 0且不为-1表示有多个线程正在进行扩容操作,jdk源码解释部分感觉有点问题 * 每次第一个线程参与扩容时,会将sizeCtl设置为一个与当前table长度相关的数值,避免出现问题,讲解方法时进行说明 * sizeCtl > 0,表示第一次初始化操作中使用的容量,或者初始化/扩容完成后的阈值 * sizeCtl = 0,默认值,此时在真正的初始化操作中使用默认容量 */ private transient volatile int sizeCtl; /** * 多线程帮助扩容相关 * 下一个transfer任务的起始下标index + 1 的值 * transfer时下标index从length - 1到0递减 * 扩容index从后往前和迭代从前往后为了避免冲突 */ private transient volatile int transferIndex; /** * Base counter value, used mainly when there is no contention, * but also as a fallback during table initialization * races. Updated via CAS. * * 计数器基础值,记录元素个数,通过CAS操作进行更新 */ private transient volatile long baseCount; /** * Spinlock (locked via CAS) used when resizing and/or creating CounterCells. * * CAS自旋锁标志位,counterCells扩容或初始化时使用 */ private transient volatile int cellsBusy; /** * Table of counter cells. When non-null, size is a power of 2. * * 高并发下计数数组,counterCells数组非空时大小是2的n次幂 */ private transient volatile CounterCell[] counterCells; // views private transient KeySetView<K,V> keySet; private transient ValuesView<K,V> values; private transient EntrySetView<K,V> entrySet;同时需要注意静态代码块中已经获取了一些变量在对象中的内存偏移量,这个是为了方便我们在CAS中的使用,如果不明白CAS,请自行查阅资料学习,源码如下: ...

June 23, 2019 · 11 min · jiezi

深入koa源码一架构设计

本文来自《心谭博客·深入koa源码:架构设计》前端面试、设计模式手册、Webpack4教程、NodeJs实战等更多专题,请来导航页领取食用所有系列文章都放在了Github。欢迎交流和Star ✿✿ ヽ(°▽°)ノ ✿最近读了 koa 的源码,理清楚了架构设计与用到的第三方库。本系列将分为 3 篇,分别介绍 koa 的架构设计和 3 个核心库的原理,最终会手动实现一个简易的 koa。 koa 的实现都在仓库的lib目录下,如下图所示,只有 4 个文件: 对于这四个文件,根据用途和封装逻辑,可以分为 3 类:req 和 res,上下文以及 application。 req 和 res对应的文件是:request.js 和 response.js。分别代表着客户端请求信息和服务端返回信息。 这两个文件在实现逻辑上完全一致。对外暴露都是一个对象,对象上的属性都使用了getter或setter来实现读写控制。 上下文对应的文件是:context.js。存了运行环境的上下文信息,例如cookies。 除此之外,因为request和response都属于上下文信息,所以通过delegate.js库来实现了对request.js和response.js上所有属性的代理。例如以下代码: /** * Response delegation. */delegate(proto, "response") .method("attachment") .method("redirect");/** * Request delegation. */delegate(proto, "request") .method("acceptsLanguages") .method("acceptsEncodings");使用代理的另外一个好处就是:更方便的访问 req 和 res 上的属性。比如在开发 koa 应用的时候,可以通过ctx.headers来读取客户端请求的头部信息,不需要写成ctx.res.headers了(这样写没错)。 注意:req 和 res 并不是在context.js中被绑定到上下文的,而是在application被绑定到上下文变量ctx中的。原因是因为每个请求的 req/res 都不是相同的。 Application对应的文件是: application.js。这个文件的逻辑是最重要的,它的作用主要是: 给用户暴露服务启动接口针对每个请求,生成新的上下文处理中间件,将其串联对外暴露接口使用 koa 时候,我们常通过listen或者callback来启动服务器: const app = new Koa();app.listen(3000); // listen启动http.createServer(app.callback()).listen(3000); // callback启动这两种启动方法是完全等价的。因为listen方法内部,就调用了callback,并且将它传给http.createServer。接着看一下callback这个方法主要做了什么: ...

June 21, 2019 · 1 min · jiezi

源码阅读基于Canvas贝塞尔曲线算法的平滑手写板

signature_pad一个基于Canvas的平滑手写画板工具介绍实现手写有多种方式。 一种比较容易做出的是对鼠标移动轨迹画点,再将两点之间以直线相连,最后再进行平滑处理,这种方案不需要什么算法支持,但同样,它面对一个性能和美观的抉择,打的点多,密集,性能相对较低,但更加美观,视觉上更平滑; 此处用的另一种方案,画贝塞尔曲线。 由于canvas没有默认的画出贝塞尔曲线方法,因此曲线是通过不断画出一个个点形成的,那么问题来了,这些点谁来定? 这里使用了贝塞尔曲线的一系列算法,包括求控制点,求长度,计算当前点的大小,最后用canvas画出每一个确定位置的点。 参数及配置介绍提供的可配置参数如下 export interface IOptions { // 点的大小(不是线条) dotSize?: number | (() => number); // 最粗的线条宽度 minWidth?: number; // 最细的线条宽度 maxWidth?: number; // 最小间隔距离(这个距离用贝塞尔曲线填充) minDistance?: number; // 背景色 backgroundColor?: string; // 笔颜色 penColor?: string; // 节流的间隔 throttle?: number; // 当前画笔速度的计算率,默认0.7,意思就是 当前速度=当前实际速度*0.7+上一次速度*0.3 velocityFilterWeight?: number; // 初始回调 onBegin?: (event: MouseEvent | Touch) => void; // 结束回调 onEnd?: (event: MouseEvent | Touch) => void;}这里要注意的是并没有线条粗细这个选项,因为这里面的粗细不等线条都是通过一个个大小不同的点构造而成; throttle这个配置可以参考loadsh或者underscore的_.throttle,功能一致,就是为了提高性能。 ...

June 18, 2019 · 2 min · jiezi

Gorm-源码分析二-简单query分析

简单使用上一篇文章我们已经知道了不使用orm如何调用mysql数据库,这篇文章我们要查看的是Gorm的源码,从最简单的一个查询语句作为切入点。当然Gorm的功能很多支持where条件支持外键group等等功能,这些功能大体的流程都是差不多先从简单的看起。下面先看如何使用 package mainimport ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/panlei/gorm")var db *gorm.DBfunc main() { InitMysql() var u User db.Where("Id = ?", 2).First(&u)}func InitMysql() { var err error db, err = gorm.Open("mysql", "root:***@******@tcp(**.***.***.***:****)/databasename?charset=utf8&loc=Asia%2FShanghai&parseTime=True") fmt.Println(err)}type User struct { Id int `gorm:"primary_key;column:Id" json:"id"` UserName string `json:"userName" gorm:"column:UserName"` Password string `json:"password" gorm:"column:Password"`}func (User) TableName() string { return "user"}首先注册对象 添加tag标注主键 设置数据库column名 数据库名可以和字段名不一样设置table名字,如果类名和数据库名一样则不需要设置初始化数据库连接 创建GormDB对象 使用Open方法返回DB使用最简单的where函数和First来获取 翻译过来的sql语句就是select * from user where Id = 2 limit 1源码分析1. DB、search、callback对象DB对象包含所有处理mysql的方法,主要的还是search和callbacks search对象存放了所有的查询条件Callback 对象存放了sql的调用链 存放了一系列的callback函数 ...

June 16, 2019 · 4 min · jiezi

VUE2610Vue对象

/src/core/index.js/src/core/instance/index.jsfunction Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }/** Vue.prototype._init*/initMixin(Vue) /** Object.defineProperty(Vue.prototype, '$data', dataDef)* Object.defineProperty(Vue.prototype, '$props', propsDef)* Vue.prototype.$set = set* Vue.prototype.$delete = del* Vue.prototype.$watch*/stateMixin(Vue) /** Vue.prototype.$on* Vue.prototype.$once* Vue.prototype.$off* Vue.prototype.$emit*/eventsMixin(Vue) /** Vue.prototype._update* Vue.prototype.$forceUpdate* Vue.prototype.$destroy*/lifecycleMixin(Vue) /** RenderHelpers...* Vue.prototype.$nextTick* Vue.prototype._render*/renderMixin(Vue) Vue.prototype._initinitLifecycle(vm) // 变量init 并把当前实例添加到parent的$children里initEvents(vm) // _events变量存放事件initRender(vm) //defineReactive $attrs $listenerscallHook(vm, 'beforeCreate') //函数钩子initInjections(vm) // provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。initState(vm) // initProps、initMethods、initData、initComputedinitProvide(vm) // resolve provide after data/propscallHook(vm, 'created')vm.$mount //vm._render -> _update 渲染声明周期钩子beforeCreate -> created -> beforeMount -> mounted参考资料Vue provide / inject ...

June 10, 2019 · 1 min · jiezi

根据调试工具看Vue源码之虚拟dom三

前言上回我们了解了 vnode 从创建到生成的流程,这回我们来探索 Vue 是如何将 vnode 转化成真实的 dom 节点/元素Vue.prototype._update上次我们提到的 _render 函数其实作为 _update 函数的参数传入,换句话说,_render 函数结束后 _update 将会执行???? Vue.prototype._update = function (vnode, hydrating) { var vm = this; var prevEl = vm.$el; var prevVnode = vm._vnode; var restoreActiveInstance = setActiveInstance(vm); vm._vnode = vnode; // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } else { // updates vm.$el = vm.__patch__(prevVnode, vnode); } restoreActiveInstance(); // update __vue__ reference if (prevEl) { prevEl.__vue__ = null; } if (vm.$el) { vm.$el.__vue__ = vm; } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el; } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. };简单梳理下这段代码的逻辑: ...

June 10, 2019 · 5 min · jiezi

从new-Vue看源码流程

demo<div id="app"> {{ message }}</div>var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' }})简要流程图 源码详细流程src/core/index.js这里主要是导出真正的Vue函数,初始化全局API import Vue from './instance/index'initGlobalAPI(Vue)export default Vuesrc/core/instance/index.js实例化的时候调用this._init方法 function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options)}src/core/instance/init.js这一块主要是初始化一系列的属性和方法。然后调用$mount方法挂载el元素。 Vue.prototype._init = function (options?: Object) { ... vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) ... // 调用一系列初始化函数 initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') ... if (vm.$options.el) { // 挂载el属性 vm.$mount(vm.$options.el) }}src/platforms/web/entry-runtime-with-compiler.js两个点,调用compileToFunctions函数生成render函数备用, 调用mount函数开始执行真正的挂载流程。 ...

June 5, 2019 · 2 min · jiezi

Gorm-源码分析一-databasesql

简介Gorm是Go语言开发用的比较多的一个ORM。它的功能比较全: 增删改查关联(包含一个,包含多个,属于,多对多,多种包含)CallBacks(创建、保存、更新、删除、查询找)之前 之后都可以有callback函数预加载事务复合主键日志database/sql 包但是这篇文章中并不会直接看Gorm的源码,我们会先从database/sql分析。原因是Gorm也是基于这个包来封装的一些功能。所以只有先了解了database/sql包才能更加好的理解Gorm源码。database/sql 其实也是一个对于mysql驱动的上层封装。"github.com/go-sql-driver/mysql"就是一个对于mysql的驱动,database/sql 就是在这个基础上做的基本封装包含连接池的使用 使用例子下面这个是最基本的增删改查操作操作分下面几个步骤: 引入github.com/go-sql-driver/mysql包(包中的init方法会初始化mysql驱动的注册)使用sql.Open 初始化一个sql.DB结构调用Prepare Exec 执行sql语句==注意:==使用Exec函数无需释放调用完毕之后会自动释放,把连接放入连接池中 使用Query 返回的sql.rows 需要手动释放连接 rows.Close()package mainimport ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" "strconv")func main() { // 打开连接 db, err := sql.Open("mysql", "root:feg@125800@tcp(47.100.245.167:3306)/artifact?charset=utf8&loc=Asia%2FShanghai&parseTime=True") if err != nil { fmt.Println("err:", err) } // 设置最大空闲连接数 db.SetMaxIdleConns(1) // 设置最大链接数 db.SetMaxOpenConns(1) query(db, 3)}//修改func update(db *sql.DB, id int, user string) { stmt, err := db.Prepare("update user set UserName=? where Id =?") if err != nil { fmt.Println(err) } res, err := stmt.Exec(user, id) updateId, err := res.LastInsertId() fmt.Println(updateId)}//删除func delete(db *sql.DB, id int) { stmt, err := db.Prepare("delete from user where id = ?") if err != nil { fmt.Println(err) } res, err := stmt.Exec(1) updateId, err := res.LastInsertId() fmt.Println(updateId)}//查询func query(db *sql.DB, id int) { rows, err := db.Query("select * from user where id = " + strconv.Itoa(id)) if err != nil { fmt.Println(err) return } for rows.Next() { var id int var user string var pwd string rows.Scan(&id, &user, &pwd) fmt.Println("id:", id, "user:", user, "pwd:", pwd) } rows.Close()}//插入func insert(db *sql.DB, user, pwd string) { stmt, err := db.Prepare("insert into user set UserName=?,Password=?") if err != nil { fmt.Println(err) } res, err := stmt.Exec("peter", "panlei") id, err := res.LastInsertId() fmt.Println(id)}连接池因为Gorm的连接池就是使用database/sql包中的连接池,所以这里我们需要学习一下包里的连接池的源码实现。其实所有连接池最重要的就是连接池对象、获取函数、释放函数下面来看一下database/sql中的连接池。 ...

June 5, 2019 · 4 min · jiezi

VUE2610entryruntimewithcompilerjs

entry-runtime-with-compiler.js 入口文件为Vue重写了$mount方法,并添加了compile方法。 $mount$mount 方法支持传入 2 个参数。 第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象。第二个参数是和服务端渲染相关,在浏览器环境下不需要传第二个参数。如果Vue实例option中没有render函数,会把option.template中的内容通过编辑器compileToFunctions编译成render函数,然后再调用Vue的$mount方法。compile--暂时没看明白,回头再看 参考资料Vue APIWindow.performance

June 4, 2019 · 1 min · jiezi

VUE2610scriptsconfigjs

rollup -w -c scripts/config.js --environment TARGET:web-full-dev-c 指定配置文件-w 监听文件,文件发生改变时重新构建--environment 设置环境变量。如rollup -c --environment TARGET:web-full-dev 可以通过process.env.TARGET获取 if (process.env.TARGET) { // 根据TARGET生成rollup config对象 module.exports = genConfig(process.env.TARGET) //生成rollup config对象} else { //如果没有设置TARGET,返回生成函数 exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig)}rollup -w -c scripts/config.js --environment TARGET:web-full-dev 对应rollup config对象如下: { input: opts.entry, //入口 src/platforms/web/entry-runtime-with-compiler.js external: opts.external, plugins: [ flow(), alias(Object.assign({}, aliases, { he: './entity-decoder' })) ].concat(opts.plugins || []), output: { file: resolve('dist/vue.js'), format: 'umd', // umd – 通用模块定义,以amd,cjs 和 iife 为一体 banner: opts.banner, name: opts.moduleName || 'Vue' }, onwarn: (msg, warn) => { //拦截警告信息 if (!/Circular/.test(msg)) { warn(msg) } }}rollup-plugin-flow-no-whitespace //去除flow静态类型检查代码rollup-plugin-alias //为模块提供别名rollup-plugin-buble //编译ES6+语法为ES2015,无需配置,比babel更轻量rollup-plugin-replace //替换代码中的变量为指定值参考资料:1、rollup文档 ...

June 4, 2019 · 1 min · jiezi

Go-Redigo-源码分析三-执行命令

简单使用简单使用Do函数获取单条和使用童丹请求多条获取多条数据。 func main() { // 1. 创建连接池 // 2. 简单设置连接池的最大链接数等参数 // 3. 注入拨号函数 // 4. 调用pool.Get() 获取连接 pool := &redis.Pool{ MaxIdle: 4, MaxActive: 4, Dial: func() (redis.Conn, error) { rc, err := redis.Dial("tcp", "127.0.0.1:6379") if err != nil { return nil, err } return rc, nil }, IdleTimeout: time.Second, Wait: true, } con := pool.Get() // 获取单条 str, err := redis.String(con.Do("get", "aaa")) fmt.Println(str, err) // 通道 发送多条接受多条 con.Send("get", "aaa") con.Send("get", "bbb") con.Send("get", "ccc") con.Flush() str, err = redis.String(con.Receive()) fmt.Println("value: ", str, " err:", err) str, err = redis.String(con.Receive()) fmt.Println("value: ", str, " err:", err) str, err = redis.String(con.Receive()) fmt.Println("value: ", str, " err:", err) con.Close()}源码查看上一篇看了Get方法获取连接池中的链接,获取到连接之后调用Do函数请求redis服务获取回复。现在我们就需要看Do函数的源码1. Conn接口 在rediso中有两个对象都实现了这个接口 ...

May 30, 2019 · 4 min · jiezi

Go-Redigo-源码分析二-连接池

Redigo 连接池的使用大家都知道go语言中的goroutine虽然消耗资源很小,并且是一个用户线程。但是goroutine也不是无限开的,所以我们会有很多关于协程池的库,当然啊我们自己也可以完成一些简单的携程池。redis也是相同的,redis的链接也是不推荐无限制的打开,否则会造成redis负荷加重。先看一下Redigo 中的连接池的使用 package mainimport ( "fmt" "github.com/panlei/redigo/redis" "time")func main() { pool := &redis.Pool{ MaxIdle: 4, MaxActive: 4, Dial: func() (redis.Conn, error) { rc, err := redis.Dial("tcp", "127.0.0.1:6379") if err != nil { return nil, err } return rc, nil }, IdleTimeout: time.Second, Wait: true, } con := pool.Get() str, err := redis.String(con.Do("get", "aaa")) con.Close() fmt.Println("value: ", str, " err:", err)}我们可以看到Redigo使用连接池还是很简单的步骤: 创建连接池简单设置连接池的最大链接数等参数注入拨号函数(设置redis地址 端口号等)调用pool.Get() 获取连接使用连接Do函数请求redis关闭连接源码Pool conn 对象的定义type Pool struct { // 拨号函数 从外部注入 Dial func() (Conn, error) // DialContext is an application supplied function for creating and configuring a DialContext func(ctx context.Context) (Conn, error) // 检测连接的可用性,从外部注入。如果返回error 则直接关闭连接 TestOnBorrow func(c Conn, t time.Time) error // 最大闲置连接数量 MaxIdle int // 最大活动连接数 MaxActive int // 闲置过期时间 在get函数中会有逻辑 删除过期的连接 IdleTimeout time.Duration // 设置如果活动连接达到上限 再获取时候是等待还是返回错误 // 如果是false 系统会返回redigo: connection pool exhausted // 如果是true 会利用p 的ch 属性让线程等待 知道有连接释放出来 Wait bool // 连接最长生存时间 如果超过时间会被从链表中删除 MaxConnLifetime time.Duration // 判断ch 是否被初始化了 chInitialized uint32 // set to 1 when field ch is initialized // 锁 mu sync.Mutex // mu protects the following fields closed bool // set to true when the pool is closed. active int // the number of open connections in the pool ch chan struct{} // limits open connections when p.Wait is true // 存放闲置连接的链表 idle idleList // idle connections // 等待获取连接的数量 waitCount int64 // total number of connections waited for. waitDuration time.Duration // total time waited for new connections.}// 连接池中的具体连接对象type conn struct { // 锁 mu sync.Mutex pending int err error // http 包中的conn对象 conn net.Conn // 读入过期时间 readTimeout time.Duration // bufio reader对象 用于读取redis服务返回的结果 br *bufio.Reader // 写入过期时间 writeTimeout time.Duration // bufio writer对象 带buf 用于往服务端写命令 bw *bufio.Writer // Scratch space for formatting argument length. // '*' or '$', length, "\r\n" lenScratch [32]byte // Scratch space for formatting integers and floats. numScratch [40]byte}我们可以看到,其中有几个关键性的字段比如最大活动连接数、最大闲置连接数、闲置链接过期时间、连接生存时间等。 ...

May 27, 2019 · 4 min · jiezi

ThinkPHP51-源码浅析二自动加载机制

继 生命周期的第二篇,大家尽可放心,不会随便鸽文章的第一篇中,我们提到了入口脚本,也说了,里面注册了自动加载的功能 本文默认你有自动加载和命名空间的基础。如果没有请 看此篇文章 php 类的自动加载与命名空间自动加载机制php 的自动加载是 Loader 类中实现的,这个类在 base.php 中被引入 //base .php// 载入Loader类require __DIR__ . '/library/think/Loader.php';// 注册自动加载Loader::register();我们程序在这里执行了 Loader 中静态方法 ,同时这也是一个全部的类register() 我们进入 Loader.php ,按照上面执行顺序看看其核心是什么? register()方法执行流程 注册系统自动加载此方法行数过长,我们一点一点来分析 // 注册系统自动加载 spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);这就是注册我们的自动加载函数,$autoload 这个变量是传的参数,考虑到你可以自己实现自己的加载类,为了方便拓展,TP可以让你自己实现自己的类加载方法。 如果不了解这个函数的同学,请看文章最顶部的那个连接,上面有详细讲解。 Composer自动加载支持$rootPath = self::getRootPath(); self::$composerPath = $rootPath . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR; // Composer自动加载支持 if (is_dir(self::$composerPath)) { if (is_file(self::$composerPath . 'autoload_static.php')) { require self::$composerPath . 'autoload_static.php'; // 获取当前加载的所有类 $declaredClass = get_declared_classes(); $composerClass = array_pop($declaredClass); foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) { if (property_exists($composerClass, $attr)) { self::${$attr} = $composerClass::${$attr}; } } } else { self::registerComposerLoader(self::$composerPath); } }为了支持 composer 拓展,在自动注册时候,把composer 也顺带一起注册了,方便对拓展的调用。 ...

May 23, 2019 · 3 min · jiezi

根据调试工具看Vue源码之虚拟dom二

前言上回我们提到,在子组件存在的情况下,父组件在执行完created钩子函数之后生成子组件的实例,子组件执行created钩子函数,同时也检查是否也有子组件,有则重复父组件的步骤,否则子组件的dom元素渲染深入了解vnode在上一篇文章中其实我们提到一个函数 —— createComponentInstanceForVnode???? function createComponentInstanceForVnode ( vnode, // we know it's MountedComponentVNode but flow doesn't parent // activeInstance in lifecycle state) { var options = { _isComponent: true, _parentVnode: vnode, parent: parent }; // check inline-template render functions var inlineTemplate = vnode.data.inlineTemplate; if (isDef(inlineTemplate)) { options.render = inlineTemplate.render; options.staticRenderFns = inlineTemplate.staticRenderFns; } return new vnode.componentOptions.Ctor(options)}与之相关的代码???? ...var child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance);child.$mount(hydrating ? vnode.elm : undefined, hydrating);从中我们可以得知: ...

May 13, 2019 · 3 min · jiezi

一张思维导图辅助你深入了解-Vue-VueRouter-Vuex-源码架构

1.前言本文内容讲解的内容:一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构。 项目地址:https://github.com/biaochenxuying/vue-family-mindmap 文章的图文结合版 Vue-family.md Vue-family.pdf 2. Vue 全家桶先来张 Vue 全家桶 总图: 3. Vue细分如下 源码目录 源码构建,基于 Rollup  Vue 本质:构造函数 数据驱动 组件化 深入响应式原理 编译 扩展 4. Vue-Router introduction 路由注册 VueRouter 对象 matcher 路径切换 5. Vuex introduction Vuex 初始化 API 插件 6. 已完成与待完成已完成: 思维导图待完成: 继续完善 思维导图添加 流程图因为该项目都是业余时间做的,笔者能力与时间也有限,很多细节还没有完善。 如果你是大神,或者对 vue 源码有更好的见解,欢迎提交 issue ,大家一起交流学习,一起打造一个像样的 讲解 Vue 全家桶源码架构 的开源项目。 7. 总结以上内容是笔者最近学习 Vue 源码时的收获与所做的笔记,本文内容大多是开源项目 Vue.js 技术揭秘 的内容,只不过是以思维导图的形式来展现,内容有省略,还加入了笔者的一点理解。 ...

May 12, 2019 · 1 min · jiezi

读懂源码系列3lodash-是如何实现深拷贝的上

前言上一篇文章 「前端面试题系列9」浅拷贝与深拷贝的含义、区别及实现 中提到了深拷贝的实现方法,从递归调用,到 JSON,再到终极方案 cloneForce。 不经让我想到,lodash 中的 _.cloneDeep 方法。它是如何实现深拷贝的呢?今天,就让我们来具体地解读一下 _.cloneDeep 的源码实现。 源码中的内容比较多,为了能将知识点讲明白,也为了更好的阅读体验,将会分为上下 2 篇进行解读。今天主要会涉及位掩码、对象判断、数组和正则的深拷贝写法。 ok,现在就让我们深入源码,共同探索吧~ _.cloneDeep 的源码实现它的源码内容很少,因为主要还是靠 baseClone 去实现。 /** Used to compose bitmasks for cloning. */const CLONE_DEEP_FLAG = 1const CLONE_SYMBOLS_FLAG = 4function cloneDeep(value) { return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)}刚看到前两行的常量就懵了,它们的用意是什么?然后,传入 baseClone 的第二个参数,似乎还将那两个常量做了运算,其结果是什么?这么做的目的是什么? 一番查找之后,终于明白这里其实涉及到了 位掩码 与 位运算 的概念。下面就来详细讲解一下。 位掩码技术回到第一行注释:Used to compose bitmasks for cloning。意思是,用于构成克隆方法的位掩码。 从注释看,这里的 CLONE_DEEP_FLAG 和 CLONE_SYMBOLS_FLAG 就是位掩码了,而 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG 其实是 位运算 中的 按位或 方法。 ...

May 8, 2019 · 4 min · jiezi

LCN502-lcn模式源码分析二

前言上一篇文章(https://segmentfault.com/a/11...)我们在springboot2.1.3上集成了lcn5.0.2并简单做了一个lcn模式的demo。LCN官网将源码都给了出来,但是分析源码的部分目前还不是很多,这篇文章主要分析一下LCN模式源码 事务控制原理分析源码之前,我们首先看一下LCN整体的框架模型: TX-LCN由两大模块组成, TxClient、TxManager。TxClient作为模块的依赖框架,提供TX-LCN的标准支持,TxManager作为分布式事务的控制放。事务发起方或者参与反都由TxClient端来控制。 原理图: lcn模式不难发现,开启处理的地方在拦截器(com.codingapi.txlcn.tc.aspect.TransactionAspect)里面 @Around("lcnTransactionPointcut() && !txcTransactionPointcut()" + "&& !tccTransactionPointcut() && !txTransactionPointcut()") public Object runWithLcnTransaction(ProceedingJoinPoint point) throws Throwable { //将执行分布式事务的方法放在DTXInfo对象里面 DTXInfo dtxInfo = DTXInfo.getFromCache(point); LcnTransaction lcnTransaction = dtxInfo.getBusinessMethod().getAnnotation(LcnTransaction.class); dtxInfo.setTransactionType(Transactions.LCN); dtxInfo.setTransactionPropagation(lcnTransaction.propagation()); //调用方法,正式开启(或继续,这里取决于是否是事务发起方)分布式事务 return dtxLogicWeaver.runTransaction(dtxInfo, point::proceed); }走进runTransaction方法,我们可以看到一下内容(伪代码,方便分析) public class DTXLogicWeaver { //执行分布式事务的核心方法 public Object runTransaction(){ //1.拿到当前模块的事务上下文和全局事务上下文 DTXLocalContext dtxLocalContext = DTXLocalContext.getOrNew(); TxContext txContext; // ---------- 保证每个模块在一个DTX下只会有一个TxContext ---------- if (globalContext.hasTxContext()) { // 有事务上下文的获取父上下文 txContext = globalContext.txContext(); dtxLocalContext.setInGroup(true);//加入事务组 log.debug("Unit[{}] used parent's TxContext[{}].", dtxInfo.getUnitId(), txContext.getGroupId()); } else { // 没有的开启本地事务上下文 txContext = globalContext.startTx();//下层创建了事务组 } //2.设置本地事务上下文的一些参数 if (Objects.nonNull(dtxLocalContext.getGroupId())) { dtxLocalContext.setDestroy(false); } dtxLocalContext.setUnitId(dtxInfo.getUnitId()); dtxLocalContext.setGroupId(txContext.getGroupId());//从全局上下文获取 dtxLocalContext.setTransactionType(dtxInfo.getTransactionType()); //3.设置分布式事务参数 TxTransactionInfo info = new TxTransactionInfo(); info.setBusinessCallback(business);//业务执行器(核心) info.setGroupId(txContext.getGroupId());//从全局上下文获取 info.setUnitId(dtxInfo.getUnitId()); info.setPointMethod(dtxInfo.getBusinessMethod()); info.setPropagation(dtxInfo.getTransactionPropagation()); info.setTransactionInfo(dtxInfo.getTransactionInfo()); info.setTransactionType(dtxInfo.getTransactionType()); info.setTransactionStart(txContext.isDtxStart()); //4.LCN事务处理器 try { return transactionServiceExecutor.transactionRunning(info); } finally { // 线程执行业务完毕清理本地数据 if (dtxLocalContext.isDestroy()) { // 通知事务执行完毕 synchronized (txContext.getLock()) { txContext.getLock().notifyAll(); } // TxContext生命周期是? 和事务组一样(不与具体模块相关的) if (!dtxLocalContext.isInGroup()) { globalContext.destroyTx(); } DTXLocalContext.makeNeverAppeared(); TracingContext.tracing().destroy(); } log.debug("<---- TxLcn end ---->"); } } }执行业务操作 ...

April 28, 2019 · 2 min · jiezi

Go-Scanner的使用和源码分析

简介go标准库bufio.Scanner,从字面意思来看是一个扫描器、扫描仪。 所用是不停的从一个reader中读取数据兵缓存在内存中,还提供了一个注入函数用来自定义分割符。库中还提供了4个预定义分割方法。 ScanLines:以换行符分割('n')ScanWords:返回通过“空格”分词的单词ScanRunes:返回单个 UTF-8 编码的 rune 作为一个 tokenScanBytes:返回单个字节作为一个 token使用方法在看使用方法之前,我们需要先看一个函数。 type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)这个函数接受一个byte数组,和一个atEOF标志位(标志位用来表示是否还有更多的数据)返回的是3个返回值。第一个是推进输入的字节(一般为标志位字节数)在splist函数判断是否找到标志位,如果没有找到则可以返回(0,nil,nil) Scan获取到这个返回值则会继续读取之后未读取完成的字符。如果找到则按照正确的返回值返回。下面是一个简单的使用例子func main() { input := "abcend234234234" fmt.Println(strings.Index(input,"end")) scanner := bufio.NewScanner(strings.NewReader(input)) scanner.Split(ScanEnd) //设置读取缓冲读取大小 每次读取2个字节 如果缓冲区不够则翻倍增加缓冲区大小 buf := make([]byte, 2) scanner.Buffer(buf, bufio.MaxScanTokenSize) for scanner.Scan() { fmt.Println("output:",scanner.Text()) } if scanner.Err() != nil { fmt.Printf("error: %s\n", scanner.Err()) }}func ScanEnd(data []byte, atEOF bool) (advance int, token []byte, err error) { //如果数据为空,数据已经读完直接返回 if atEOF && len(data) == 0 { return 0, nil, nil } // 获取自定义的结束标志位的位置 index:= strings.Index(string(data),"end") if index > 0{ //如果找到 返回的第一个参数为后推的字符长度 //第二个参数则指标志位之前的字符 //第三个参数为是否有错误 return index+3, data[0:index],nil } if atEOF { return len(data), data, nil } //如果没有找到则返回0,nil,nil return 0, nil, nil}上面的例子可以看到 字符串是”abcend234234234“ 因为设置的是每次读取2个字符串 第一次读取: buf = ab 没有找到end ScanEnd返回 0,nil,nil第二次读取: buf = abce 没有找到end ScanEnd返回 0,nil,nil第三次读取: buf = abcend23(buf翻倍扩容) 找到自定义标志位end 返回:6,abc, nil 打出 out abc 第四次读取: buf = 23423423 之前的已经读取的被去掉,犹豫buf大小为8 直接读取8个字符第五次读取: 由于buf容量不足翻倍之后 直接获取全部数据输出 out 234234234结果则是:output: abcoutput: 234234234可以看到 扫描器 按照自定义的读取大小和结束符token 输出结果 ...

April 23, 2019 · 3 min · jiezi

Java并发编程之CountDownLatch源码解析

一、导语最近在学习并发编程原理,所以准备整理一下自己学到的知识,先写一篇CountDownLatch的源码分析,之后希望可以慢慢写完整个并发编程。二、什么是CountDownLatchCountDownLatch是java的JUC并发包里的一个工具类,可以理解为一个倒计时器,主要是用来控制多个线程之间的通信。 比如有一个主线程A,它要等待其他4个子线程执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。三、简单使用public static void main(String[] args){ System.out.println(“主线程和他的两个小兄弟约好去吃火锅”); System.out.println(“主线程进入了饭店”); System.out.println(“主线程想要开始动筷子吃饭”); //new一个计数器,初始值为2,当计数器为0时,主线程开始执行 CountDownLatch latch = new CountDownLatch(2); new Thread(){ public void run() { try { System.out.println(“子线程1——小兄弟A 正在到饭店的路上”); Thread.sleep(3000); System.out.println(“子线程1——小兄弟A 到饭店了”); //一个小兄弟到了,计数器-1 latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); new Thread(){ public void run() { try { System.out.println(“子线程2——小兄弟B 正在到饭店的路上”); Thread.sleep(3000); System.out.println(“子线程2——小兄弟B 到饭店了”); //另一个小兄弟到了,计数器-1 latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); //主线程等待,直到其他两个小兄弟也进入饭店(计数器==0),主线程才能吃饭 latch.await(); System.out.println(“主线程终于可以开始吃饭了~”);}四、源码分析核心代码:CountDownLatch latch = new CountDownLatch(1); latch.await(); latch.countDown();其中构造函数的参数是计数器的值; await()方法是用来阻塞线程,直到计数器的值为0 countDown()方法是执行计数器-1操作1、首先来看构造函数的代码public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException(“count < 0”); this.sync = new Sync(count); }这段代码很简单,首先if判断传入的count是否<0,如果小于0直接抛异常。 然后new一个类Sync,这个Sync是什么呢?我们一起来看下private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } //尝试获取共享锁 protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } //尝试释放共享锁 protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }可以看到Sync是一个内部类,继承了AQS,AQS是一个同步器,之后我们会详细讲。 其中有几个核心点:变量 state是父类AQS里面的变量,在这里的语义是计数器的值getState()方法也是父类AQS里的方法,很简单,就是获取state的值tryAcquireShared和tryReleaseShared也是父类AQS里面的方法,在这里CountDownLatch对他们进行了重写,先有个印象,之后详讲。2、了解了CountDownLatch的构造函数之后,我们再来看它的核心代码,首先是await()。public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }可以看到,其实是通过内部类Sync调用了父类AQS的acquireSharedInterruptibly()方法。public final void acquireSharedInterruptibly(int arg) throws InterruptedException { //判断线程是否是中断状态 if (Thread.interrupted()) throw new InterruptedException(); //尝试获取state的值 if (tryAcquireShared(arg) < 0)//step1 doAcquireSharedInterruptibly(arg);//step2 }tryAcquireShared(arg)这个方法就是我们刚才在Sync内看到的重写父类AQS的方法,意思就是判断是否getState() == 0,如果state为0,返回1,则step1处不进入if体内acquireSharedInterruptibly(int arg)方法执行完毕。若state!=0,则返回-1,进入if体内step2处。 下面我们来看acquireSharedInterruptibly(int arg)方法:private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //step1、把当前线程封装为共享类型的Node,加入队列尾部 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //step2、获取当前node的前一个元素 final Node p = node.predecessor(); //step3、如果前一个元素是队首 if (p == head) { //step4、再次调用tryAcquireShared()方法,判断state的值是否为0 int r = tryAcquireShared(arg); //step5、如果state的值==0 if (r >= 0) { //step6、设置当前node为队首,并尝试释放共享锁 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } //step7、是否可以安心挂起当前线程,是就挂起;并且判断当前线程是否中断 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { //step8、如果出现异常,failed没有更新为false,则把当前node从队列中取消 if (failed) cancelAcquire(node); } }按照代码中的注释,我们可以大概了解该方法的内容,下面我们来仔细看下其中调用的一些方法是干什么的。 1、首先看addWaiter()//step1private Node addWaiter(Node mode) { //把当前线程封装为node Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure //获取当前队列的队尾tail,并赋值给pred Node pred = tail; //如果pred!=null,即当前队尾不为null if (pred != null) { //把当前队尾tail,变成当前node的前继节点 node.prev = pred; //cas更新当前node为新的队尾 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //如果队尾为空,走enq方法 enq(node);//step1.1 return node; }—————————————————————–//step1.1private Node enq(final Node node) { for (;;) { Node t = tail; //如果队尾tail为null,初始化队列 if (t == null) { // Must initialize //cas设置一个新的空node为队首 if (compareAndSetHead(new Node())) tail = head; } else { //cas把当前node设置为新队尾,把前队尾设置成当前node的前继节点 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }2、接下来我们在来看setHeadAndPropagate()方法,看其内部实现//step6private void setHeadAndPropagate(Node node, int propagate) { //获取队首head Node h = head; // Record old head for check below //设置当前node为队首,并取消node所关联的线程 setHead(node); // if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //如果当前node的后继节点为null或者是shared类型的 if (s == null || s.isShared()) //释放锁,唤醒下一个线程 doReleaseShared();//step6.1 } }——————————————————————–//step6.1private void doReleaseShared() { for (;;) { //找到头节点 Node h = head; if (h != null && h != tail) { //获取头节点状态 int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases //唤醒head节点的next节点 unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }3、接下来我们来看countDown()方法。public void countDown() { sync.releaseShared(1); }可以看到调用的是父类AQS的releaseShared 方法public final boolean releaseShared(int arg) { //state-1 if (tryReleaseShared(arg)) {//step1 //唤醒等待线程,内部调用的是LockSupport.unpark方法 doReleaseShared();//step2 return true; } return false; }——————————————————————//step1protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { //获取当前state的值 int c = getState(); if (c == 0) return false; int nextc = c-1; //cas操作来进行原子减1 if (compareAndSetState(c, nextc)) return nextc == 0; } }五、总结CountDownLatch主要是通过计数器state来控制是否可以执行其他操作,如果不能就通过LockSupport.park()方法挂起线程,直到其他线程执行完毕后唤醒它。下面我们通过一个简单的图来帮助我们理解一下:PS:本人也是还在学习的路上,理解的也不是特别透彻,如有错误,愿倾听教诲。^_^ ...

April 18, 2019 · 3 min · jiezi

【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读

概要前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,本篇分析源码篇幅较长。<!– more –>过滤器链前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。Filter Class介绍SecurityContextPersistenceFilter判断当前用户是否登录CrsfFilter用于防止csrf攻击LogoutFilter处理注销请求UsernamePasswordAuthenticationFilter处理表单登录的请求(也是我们今天的主角)BasicAuthenticationFilter处理http basic认证的请求由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。通过上面我们知道SpringSecurity对于表单登录的认证请求是交给了UsernamePasswordAuthenticationFilter处理的,那么具体的认证流程如下:从上图可知,UsernamePasswordAuthenticationFilter继承于抽象类AbstractAuthenticationProcessingFilter。具体认证是:进入doFilter方法,判断是否要认证,如果需要认证则进入attemptAuthentication方法,如果不需要直接结束attemptAuthentication方法中根据username跟password构造一个UsernamePasswordAuthenticationToken对象(此时的token是未认证的),并且将它交给ProviderManger来完成认证。ProviderManger中维护这一个AuthenticationProvider对象列表,通过遍历判断并且最后选择DaoAuthenticationProvider对象来完成最后的认证。DaoAuthenticationProvider根据ProviderManger传来的token取出username,并且调用我们写的UserDetailsService的loadUserByUsername方法从数据库中读取用户信息,然后对比用户密码,如果认证通过,则返回用户信息也是就是UserDetails对象,在重新构造UsernamePasswordAuthenticationToken(此时的token是 已经认证通过了的)。接下来我们将通过源码来分析具体的整个认证流程。AbstractAuthenticationProcessingFilterAbstractAuthenticationProcessingFilter 是一个抽象类。所有的认证认证请求的过滤器都会继承于它,它主要将一些公共的功能实现,而具体的验证逻辑交给子类实现,有点类似于父类设置好认证流程,子类负责具体的认证逻辑,这样跟设计模式的模板方法模式有点相似。现在我们分析一下 它里面比较重要的方法1、doFilterpublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 省略不相干代码。。。 // 1、判断当前请求是否要认证 if (!requiresAuthentication(request, response)) { // 不需要直接走下一个过滤器 chain.doFilter(request, response); return; } try { // 2、开始请求认证,attemptAuthentication具体实现给子类,如果认证成功返回一个认证通过的Authenticaion对象 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } // 3、登录成功 将认证成功的用户信息放入session SessionAuthenticationStrategy接口,用于扩展 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { //2.1、发生异常,登录失败,进入登录失败handler回调 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { //2.1、发生异常,登录失败,进入登录失败处理器 unsuccessfulAuthentication(request, response, failed); return; } // 3.1、登录成功,进入登录成功处理器。 successfulAuthentication(request, response, chain, authResult); }2、successfulAuthentication登录成功处理器protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //1、登录成功 将认证成功的Authentication对象存入SecurityContextHolder中 // SecurityContextHolder本质是一个ThreadLocal SecurityContextHolder.getContext().setAuthentication(authResult); //2、如果开启了记住我功能,将调用rememberMeServices的loginSuccess 将生成一个token // 将token放入cookie中这样 下次就不用登录就可以认证。具体关于记住我rememberMeServices的相关分析我 们下面几篇文章会深入分析的。 rememberMeServices.loginSuccess(request, response, authResult); // Fire event //3、发布一个登录事件。 if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //4、调用我们自己定义的登录成功处理器,这样也是我们扩展得知登录成功的一个扩展点。 successHandler.onAuthenticationSuccess(request, response, authResult); }3、unsuccessfulAuthentication登录失败处理器protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { //1、登录失败,将SecurityContextHolder中的信息清空 SecurityContextHolder.clearContext(); //2、关于记住我功能的登录失败处理 rememberMeServices.loginFail(request, response); //3、调用我们自己定义的登录失败处理器,这里可以扩展记录登录失败的日志。 failureHandler.onAuthenticationFailure(request, response, failed); }关于AbstractAuthenticationProcessingFilter主要分析就到这。我们可以从源码中知道,当请求进入该过滤器中具体的流程是判断该请求是否要被认证调用attemptAuthentication方法开始认证,由于是抽象方法具体认证逻辑给子类如果登录成功,则将认证结果Authentication对象根据session策略写入session中,将认证结果写入到SecurityContextHolder,如果开启了记住我功能,则根据记住我功能,生成token并且写入cookie中,最后调用一个successHandler对象的方法,这个对象可以是我们配置注入的,用于处理我们的自定义登录成功的一些逻辑(比如记录登录成功日志等等)。如果登录失败,则清空SecurityContextHolder中的信息,并且调用我们自己注入的failureHandler对象,处理我们自己的登录失败逻辑。UsernamePasswordAuthenticationFilter从上面分析我们可以知道,UsernamePasswordAuthenticationFilter是继承于AbstractAuthenticationProcessingFilter,并且实现它的attemptAuthentication方法,来实现认证具体的逻辑实现。接下来,我们通过阅读UsernamePasswordAuthenticationFilter的源码来解读,它是如何完成认证的。 由于这里会涉及UsernamePasswordAuthenticationToken对象构造,所以我们先看看UsernamePasswordAuthenticationToken的源码1、UsernamePasswordAuthenticationToken// 继承至AbstractAuthenticationToken // AbstractAuthenticationToken主要定义一下在SpringSecurity中toke需要存在一些必须信息// 例如权限集合 Collection<GrantedAuthority> authorities; 是否认证通过boolean authenticated = false;认证通过的用户信息Object details;public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { // 未登录情况下 存的是用户名 登录成功情况下存的是UserDetails对象 private final Object principal; // 密码 private Object credentials; /** * 构造函数,用户没有登录的情况下,此时的authenticated是false,代表尚未认证 / public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /* * 构造函数,用户登录成功的情况下,多了一个参数 是用户的权限集合,此时的authenticated是true,代表认证成功 / public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override }}接下来我们就可以分析attemptAuthentication方法了。2、attemptAuthenticationpublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 1、判断是不是post请求,如果不是则抛出AuthenticationServiceException异常,注意这里抛出的异常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕获,捕获之后会进入登录失败的逻辑。 if (postOnly && !request.getMethod().equals(“POST”)) { throw new AuthenticationServiceException( “Authentication method not supported: " + request.getMethod()); } // 2、从request中拿用户名跟密码 String username = obtainUsername(request); String password = obtainPassword(request); // 3、非空处理,防止NPE异常 if (username == null) { username = “”; } if (password == null) { password = “”; } // 4、除去空格 username = username.trim(); // 5、根据username跟password构造出一个UsernamePasswordAuthenticationToken对象 从上文分析可知道,此时的token是未认证的。 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // 6、配置一下其他信息 ip 等等 setDetails(request, authRequest); // 7、调用ProviderManger的authenticate的方法进行具体认证逻辑 return this.getAuthenticationManager().authenticate(authRequest); }ProviderManager维护一个AuthenticationProvider列表,进行认证逻辑验证1、authenticatepublic Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、拿到token的类型。 Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; // 2、遍历AuthenticationProvider列表 for (AuthenticationProvider provider : getProviders()) { // 3、AuthenticationProvider不支持当前token类型,则直接跳过 if (!provider.supports(toTest)) { continue; } try { // 4、如果Provider支持当前token,则交给Provider完成认证。 result = provider.authenticate(authentication); } catch (AccountStatusException e) { throw e; } catch (InternalAuthenticationServiceException e) { throw e; } catch (AuthenticationException e) { lastException = e; } } // 5、登录成功 返回登录成功的token if (result != null) { eventPublisher.publishAuthenticationSuccess(result); return result; } }AbstractUserDetailsAuthenticationProvider1、authenticateAbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口,并且实现了部分方法,DaoAuthenticationProvider继承于AbstractUserDetailsAuthenticationProvider类,所以我们先来看看AbstractUserDetailsAuthenticationProvider的实现。public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { // 国际化处理 protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); /* * 对token一些检查,具体检查逻辑交给子类实现,抽象方法 / protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; /* * 认证逻辑的实现,调用抽象方法retrieveUser根据username获取UserDetails对象 */ public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、获取usernmae String username = (authentication.getPrincipal() == null) ? “NONE_PROVIDED” : authentication.getName(); // 2、尝试去缓存中获取UserDetails对象 UserDetails user = this.userCache.getUserFromCache(username); // 3、如果为空,则代表当前对象没有缓存。 if (user == null) { cacheWasUsed = false; try { //4、调用retrieveUser去获取UserDetail对象,为什么这个方法是抽象方法大家很容易知道,如果UserDetail信息存在关系数据库 则可以重写该方法并且去关系数据库获取用户信息,如果UserDetail信息存在其他地方,可以重写该方法用其他的方法去获取用户信息,这样丝毫不影响整个认证流程,方便扩展。 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { // 捕获异常 日志处理 并且往上抛出,登录失败。 if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } else { throw notFound; } } } try { // 5、前置检查 判断当前用户是否锁定,禁用等等 preAuthenticationChecks.check(user); // 6、其他的检查,在DaoAuthenticationProvider是检查密码是否一致 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { } // 7、后置检查,判断密码是否过期 postAuthenticationChecks.check(user); // 8、登录成功通过UserDetail对象重新构造一个认证通过的Token对象 return createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // 调用第二个构造方法,构造一个认证通过的Token对象 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }}接下来我们具体看看retrieveUser的实现,没看源码大家应该也可以知道,retrieveUser方法应该是调用UserDetailsService去数据库查询是否有该用户,以及用户的密码是否一致。DaoAuthenticationProviderDaoAuthenticationProvider 主要是通过UserDetailService来获取UserDetail对象。1、retrieveUserprotected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { try { // 1、调用UserDetailsService接口的loadUserByUsername方法获取UserDeail对象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // 2、如果loadedUser为null 代表当前用户不存在,抛出异常 登录失败。 if (loadedUser == null) { throw new InternalAuthenticationServiceException( “UserDetailsService returned null, which is an interface contract violation”); } // 3、返回查询的结果 return loadedUser; } }2、additionalAuthenticationChecksprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 1、如果密码为空,则抛出异常、 if (authentication.getCredentials() == null) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } // 2、获取用户输入的密码 String presentedPassword = authentication.getCredentials().toString(); // 3、调用passwordEncoder的matche方法 判断密码是否一致 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug(“Authentication failed: password does not match stored value”); // 4、如果不一致 则抛出异常。 throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } }总结至此,整认证流程已经分析完毕,大家如果有什么不懂可以关注我的公众号一起讨论。学习是一个漫长的过程,学习源码可能会很困难但是只要努力一定就会有获取,大家一致共勉。 ...

April 16, 2019 · 4 min · jiezi

React源码系列一之createElement

前言:使用react也有二年多了,一直停留在使用层次。虽然很多时候这样是够了。但是总觉得不深入理解其背后是的实现逻辑,很难体会框架的精髓。最近会写一些相关的一些文章,来记录学习的过程。备注:react和react-dom源码版本为16.8.6 本文适合使用过REact进行开发,并有一定经验的人阅读。好了闲话少说,我们一起来看源码吧写过react知道,我们使用react编写代码都离不开webpack和babel,因为React要求我们使用的是class定义组件,并且使用了JSX语法编写HTML。浏览器是不支持JSX并且对于class的支持也不好,所以我们都是需要使用webpack的jsx-loader对jsx的语法做一个转换,并且对于ES6的语法和react的语法通过babel的babel/preset-react、babel/env和@babel/plugin-proposal-class-properties等进行转义。不熟悉怎么从头搭建react的可以看一下这篇文章好了,我们从一个最简单实例demo来看react到底做了什么1、createElement下面是我们的代码import React from “react”;import ReactDOM from “react-dom”;ReactDOM.render( <h1 style={{color:‘red’}} >11111</h1>, document.getElementById(“root”));这是页面上的效果我们现在看看在浏览器中的代码是如何实现的:react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, { style: { color: ‘red’ }}, “11111”), document.getElementById(“root”));最终经过编译后的代码是这样的,发现原本的<h1>11111</h1>变成了一个react.createElement的函数,其中原生标签的类型,内容都变成了参数传入这个函数中.这个时候我们大胆的猜测react.createElement接受三个参数,分别是元素的类型、元素的属性、子元素。好了带着我们的猜想来看一下源码。我们不难找到,源码位置在位置 ./node_modules/react/umd/react.development.js:1941function createElement(type, config, children) { var propName = void 0; // Reserved names are extracted var props = {}; var key = null; var ref = null; var self = null; var source = null; if (config != null) { if (hasValidRef(config)) { ref = config.ref; } if (hasValidKey(config)) { key = ’’ + config.key; } self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object for (propName in config) { if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { props[propName] = config[propName]; } } } // Children can be more than one argument, and those are transferred onto // the newly allocated props object. var childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { var childArray = Array(childrenLength); for (var i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props if (type && type.defaultProps) { var defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } { if (key || ref) { var displayName = typeof type === ‘function’ ? type.displayName || type.name || ‘Unknown’ : type; if (key) { defineKeyPropWarningGetter(props, displayName); } if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);}首先我们来看一下它的三个参数第一个type:我们想一下这个type的可能取值有哪些?第一种就是我们上面写的原生的标签类型(例如h1、div,span等);第二种就是我们React组件了,就是这面这种Appclass App extends React.Component { static defaultProps = { text: ‘DEMO’ } render() { return (<h1>222{this.props.text}</h1>) }}第二个config:这个就是我们传递的一些属性第三个children:这个就是子元素,最开始我们猜想就三个参数,其实后面看了源码就知道这里其实不止三个。接下来我们来看看react.createElement这个函数里面会帮我们做什么事情。1、首先会初始化一些列的变量,之后会判断我们传入的元素中是否带有有效的key和ref的属性,这两个属性对于react是有特殊意义的(key是可以优化React的渲染速度的,ref是可以获取到React渲染后的真实DOM节点的),如果检测到有传入key,ref,__self和__source这4个属性值,会将其保存起来。2、接着对传入的config做处理,遍历config对象,并且剔除掉4个内置的保留属性(key,ref,__self,__source),之后重新组装新的config为props。这个RESERVED_PROPS是定义保留属性的地方。 var RESERVED_PROPS = { key: true, ref: true, __self: true, __source: true };3、之后会检测传入的参数的长度,如果childrenLength等于1的情况下,那么就代表着当前createElement的元素只有一个子元素,那么将内容赋值到props.children。那什么时候childrenLength会大于1呢?那就是当你的元素里面涉及到多个子元素的时候,那么children将会有多个传入到createElement函数中。例如: ReactDOM.render( <h1 style={{color:‘red’}} key=‘22’> <div>111</div> <div>222</div> </h1>, document.getElementById(“root”) );编译后是什么样呢? react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render( react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, { style: { color: ‘red’ }, key: “22” }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“div”, null, “111”), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“div”, null, “222”)), document.getElementById(“root”) );这个时候react.createElement拿到的arguments.length就大于3了。也就是childrenLength大于1。这个时候我们就遍历把这些子元素添加到props.children中。4、接着函数将会检测是否存在defaultProps这个参数,因为现在的是一个最简单的demo,而且传入的只是原生元素,所以没有defaultProps这个参数。那么我们来看下面的例子: import React, { Component } from “react”; import ReactDOM from “react-dom”; class App extends Component { static defaultProps = { text: ‘33333’ } render() { return (<h1>222{this.props.text}</h1>) } } ReactDOM.render( <App/>, document.getElementById(“root”) );编译后的 var App = /#PURE/ function (_Component) { _inherits(App, _Component); function App() { _classCallCheck(this, App); return _possibleConstructorReturn(this, getPrototypeOf(App).apply(this, arguments)); } createClass(App, [{ key: “render”, value: function render() { return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, null, “222”, this.props.text); } }]); return App; }(react__WEBPACK_IMPORTED_MODULE_0[“Component”]); _defineProperty(App, “defaultProps”, { text: ‘33333’ }); react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render( react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(App, null), document.getElementById(“root”) );发现传入react.createElement的是一个App的函数,class经过babel转换后会变成一个构造函数。有兴趣可以自己去看babel对于class的转换,这里就不解析转换过程,总得来说就是返回一个App的构造函数传入到react.createElement中.如果type传的东西是个对象,且type有defaultProps这个东西并且props中对应的值是undefined,那就defaultProps的值也塞props里面。这就是我们组价默认属性的由来。5、 检测key和ref是否有赋值,如果有将会执行defineKeyPropWarningGetter和defineRefPropWarningGetter两个函数。function defineKeyPropWarningGetter(props, displayName) { var warnAboutAccessingKey = function () { if (!specialPropKeyWarningShown) { specialPropKeyWarningShown = true; warningWithoutStack$1(false, ‘%s: key is not a prop. Trying to access it will result ’ + ‘in undefined being returned. If you need to access the same ’ + ‘value within the child component, you should pass it as a different ’ + ‘prop. (https://fb.me/react-special-props)', displayName); } }; warnAboutAccessingKey.isReactWarning = true; Object.defineProperty(props, ‘key’, { get: warnAboutAccessingKey, configurable: true });}function defineRefPropWarningGetter(props, displayName) { var warnAboutAccessingRef = function () { if (!specialPropRefWarningShown) { specialPropRefWarningShown = true; warningWithoutStack$1(false, ‘%s: ref is not a prop. Trying to access it will result ’ + ‘in undefined being returned. If you need to access the same ’ + ‘value within the child component, you should pass it as a different ’ + ‘prop. (https://fb.me/react-special-props)', displayName); } }; warnAboutAccessingRef.isReactWarning = true; Object.defineProperty(props, ‘ref’, { get: warnAboutAccessingRef, configurable: true });}我么可以看出这个二个方法就是给key和ref添加了警告。这个应该只是在开发环境才有其中isReactWarning就是上面判断key与ref是否有效的一个标记。6、最后将一系列组装好的数据传入ReactElement函数中。2、ReactElementvar ReactElement = function (type, key, ref, self, source, owner, props) { var element = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props, _owner: owner }; { element._store = {}; Object.defineProperty(element._store, ‘validated’, { configurable: false, enumerable: false, writable: true, value: false }); Object.defineProperty(element, ‘_self’, { configurable: false, enumerable: false, writable: false, value: self }); Object.defineProperty(element, ‘_source’, { configurable: false, enumerable: false, writable: false, value: source }); if (Object.freeze) { Object.freeze(element.props); Object.freeze(element); } } return element;};其实里面非常简单,就是将传进来的值都包装在一个element对象中$$typeof:其中REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElementvar hasSymbol = typeof Symbol === ‘function’ && Symbol.for;var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for(‘react.element’) : 0xeac7;从代码上看如果支持Symbol就会用Symbol.for方法创建一个key为react.element的symbol,否则就会返回一个0xeac7type -> tagName或者是一个函数key -> 渲染元素的keyref -> 渲染元素的refprops -> 渲染元素的props_owner -> Record the component responsible for creating this element.(记录负责创建此元素的组件,默认为null)_store -> 新的对象_store中添加了一个新的对象validated(可写入),element对象中添加了_self和_source属性(只读),最后冻结了element.props和element。这样就解释了为什么我们在子组件内修改props是没有效果的,只有在父级修改了props后子组件才会生效最后就将组装好的element对象返回了出来,提供给ReactDOM.render使用。到这有关的主要内容我们看完了。下面我们来补充一下知识点Object.freezeObject.freeze方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。const obj = { a: 1, b: 2};Object.freeze(obj);obj.a = 3; // 修改无效需要注意的是冻结中能冻结当前对象的属性,如果obj中有一个另外的对象,那么该对象还是可以修改的。所以React才会需要冻结element和element.props。if (Object.freeze) { Object.freeze(element.props); Object.freeze(element);} ...

April 6, 2019 · 4 min · jiezi

vue自定义指令--directive

Vue中内置了很多的指令,如v-model、v-show、v-html等,但是有时候这些指令并不能满足我们,或者说我们想为元素附加一些特别的功能,这时候,我们就需要用到vue中一个很强大的功能了—自定义指令。在开始之前,我们需要明确一点,自定义指令解决的问题或者说使用场景是对普通 DOM 元素进行底层操作,所以我们不能盲目的胡乱的使用自定义指令。如何声明自定义指令?就像vue中有全局组件和局部组件一样,他也分全局自定义指令和局部指令。let Opt = { bind:function(el,binding,vnode){ }, inserted:function(el,binding,vnode){ }, update:function(el,binding,vnode){ }, componentUpdated:function(el,binding,vnode){ }, unbind:function(el,binding,vnode){ },}对于全局自定义指令的创建,我们需要使用 Vue.directive接口Vue.directive(‘demo’, Opt)对于局部组件,我们需要在组件的钩子函数directives中进行声明Directives: { Demo: Opt}Vue中的指令可以简写,上面Opt是一个对象,包含了5个钩子函数,我们可以根据需要只写其中几个函数。如果你想在 bind 和 update 时触发相同行为,而不关心其它的钩子,那么你可以将Opt改为一个函数。let Opt = function(el,binding,vnode){ }如何使用自定义指令?对于自定义指令的使用是非常简单的,如果你对vue有一定了解的话。我们可以像v-text=”’test’”一样,把我们需要传递的值放在‘=’号后面传递过去。我们可以像v-on:click=”handClick” 一样,为指令传递参数’click’。我们可以像v-on:click.stop=”handClick” 一样,为指令添加一个修饰符。我们也可以像v-once一样,什么都不传递。每个指令,他的底层封装肯定都不一样,所以我们应该先了解他的功能和用法,再去使用它。自定义指令的 钩子函数上面我们也介绍了,自定义指令一共有5个钩子函数,他们分别是:bind、inserted、update、componentUpdate和unbind。对于这几个钩子函数,了解的可以自行跳过,不了解的我也不介绍,自己去官网看,没有比官网上说的更详细的了:钩子函数项目中的bug在项目中,我们自定义一个全局指令my-click:Vue.directive(‘my-click’,{ bind:function(el, binding, vnode, oldVnode){ el.addEventListener(‘click’,function(){ console.log(el, binding.value) }) }})同时,有一个数组arr:[1,2,3,4,5,6],我们遍历数组,生成dom元素,并为元素绑定指令:<ul> <li v-for="(item,index) in arr" :key=“index” v-my-click=“item”>{{item}}</li></ul>可以看到,当我们点击元素的时候,成功打印了元素,以及传递过去的数据。可是,当我们把最后一个元素动态的改为8之后(6 –> 8),点击元素,元素是对的,可是打印的数据却仍然是6.或者,当我们删除了第一个元素之后,点击元素黑人问号脸,这是为什么呢????带着这个疑问,我去看了看源码。在进行下面的源码分析之前,先来说结论:组件进行初始化的时候,也就是第一次运行指令的时候,会执行bind钩子函数,我们所传入的参数(binding)都进入到了这里,并形成了一个闭包。当我们进行数据更新的时候,vue虚拟dom不会销毁这个组件(如果说删除某个数据,会从后往前销毁组件,前面的总是最后销毁),而是进行更新(根据数据改变),如果指令有update钩子会运行这个钩子函数,但是对于元素在bind中绑定的事件,在update中没有处理的话,他不会消失(依然引用初始化时形成的闭包中的数据),所以当我们更改数据再次点击元素后,看到的数据还是原数据。源码分析函数执行顺序:createElm/initComponent/patchVnode –> invokeCreateHooks (cbs.create) –> updateDirectives –> _update在createElm方法和initComponent方法和更新节点patchVnode时会调用invokeCreateHooks方法,它会去遍历cbs.create中钩子函数进行执行,cbs.create中的钩子函数如下图所示共8个。我们所需要看的就是updateDirectives这个函数,这个函数会继续调用_update函数,vue中的指令操作就都在这个_update函数中了。下面我们就来详细看下这个_update函数。function _update(oldVnode, vnode) { //判断旧节点是不是空节点,是的话表示新建/初始化组件 var isCreate = oldVnode === emptyNode; //判断新节点是不是空节点,是的话表示销毁组件 var isDestroy = vnode === emptyNode; //获取旧节点上的所有自定义指令 var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); //获取新节点上的所有自定义指令 var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); //保存inserted钩子函数 var dirsWithInsert = []; //保存componentUpdated钩子函数 var dirsWithPostpatch = []; var key, oldDir, dir; //这里先说下callHook$1函数的作用 //callHook$1有五个参数,第一个参数是指令对象,第二个参数是钩子函数名称,第三个参数新节点, //第四个参数是旧节点,第五个参数是是否为注销组件,默认为undefined,只在组件注销时使用 //在这个函数里,会根据我们传递的钩子函数名称,运行我们自定义组件时,所声明的钩子函数, //遍历所有新节点上的自定义指令 for(key in newDirs) { oldDir = oldDirs[key]; dir = newDirs[key]; //如果旧节点中没有对应的指令,一般都是初始化的时候运行 if(!oldDir) { //对该节点执行指令的bind钩子函数 callHook$1(dir, ‘bind’, vnode, oldVnode); //dir.def是我们所定义的指令的五个钩子函数的集合 //如果我们的指令中存在inserted钩子函数 if(dir.def && dir.def.inserted) { //把该指令存入dirsWithInsert中 dirsWithInsert.push(dir); } } else { //如果旧节点中有对应的指令,一般都是组件更新的时候运行 //那么这里进行更新操作,运行update钩子(如果有的话) //将旧值保存下来,供其他地方使用(仅在 update 和 componentUpdated 钩子中可用) dir.oldValue = oldDir.value; //对该节点执行指令的update钩子函数 callHook$1(dir, ‘update’, vnode, oldVnode); //dir.def是我们所定义的指令的五个钩子函数的集合 //如果我们的指令中存在componentUpdated钩子函数 if(dir.def && dir.def.componentUpdated) { //把该指令存入dirsWithPostpatch中 dirsWithPostpatch.push(dir); } } } //我们先来简单讲下mergeVNodeHook的作用 //mergeVNodeHook有三个参数,第一个参数是vnode节点,第二个参数是key值,第三个参数是回函数 //mergeVNodeHook会先用一个函数wrappedHook重新封装回调,在这个函数里运行回调函数 //如果该节点没有这个key属性,会新增一个key属性,值为一个数组,数组中包含上面说的函数wrappedHook //如果该节点有这个key属性,会把函数wrappedHook追加到数组中 //如果dirsWithInsert的长度不为0,也就是在初始化的时候,且至少有一个指令中有inserted钩子函数 if(dirsWithInsert.length) { //封装回调函数 var callInsert = function() { //遍历所有指令的inserted钩子 for(var i = 0; i < dirsWithInsert.length; i++) { //对节点执行指令的inserted钩子函数 callHook$1(dirsWithInsert[i], ‘inserted’, vnode, oldVnode); } }; if(isCreate) { //如果是新建/初始化组件,使用mergeVNodeHook绑定insert属性,等待后面调用。 mergeVNodeHook(vnode, ‘insert’, callInsert); } else { //如果是更新组件,直接调用函数,遍历inserted钩子 callInsert(); } } //如果dirsWithPostpatch的长度不为0,也就是在组件更新的时候,且至少有一个指令中有componentUpdated钩子函数 if(dirsWithPostpatch.length) { //使用mergeVNodeHook绑定postpatch属性,等待后面子组建全部更新完成调用。 mergeVNodeHook(vnode, ‘postpatch’, function() { for(var i = 0; i < dirsWithPostpatch.length; i++) { //对节点执行指令的componentUpdated钩子函数 callHook$1(dirsWithPostpatch[i], ‘componentUpdated’, vnode, oldVnode); } }); } //如果不是新建/初始化组件,也就是说是更新组件 if(!isCreate) { //遍历旧节点中的指令 for(key in oldDirs) { //如果新节点中没有这个指令(旧节点中有,新节点没有) if(!newDirs[key]) { //从旧节点中解绑,isDestroy表示组件是不是注销了 //对旧节点执行指令的unbind钩子函数 callHook$1(oldDirs[key], ‘unbind’, oldVnode, oldVnode, isDestroy); } } }}callHook$1函数function callHook$1(dir, hook, vnode, oldVnode, isDestroy) { var fn = dir.def && dir.def[hook]; if(fn) { try { fn(vnode.elm, dir, vnode, oldVnode, isDestroy); } catch(e) { handleError(e, vnode.context, (“directive " + (dir.name) + " " + hook + " hook”)); } }}解决看过了源码,我们再回到上面的bug,我们应该如何去解决呢?1、事件解绑,重新绑定我们在bind钩子中绑定了事件,当数据更新后,会运行update钩子,所以我们可以在update中先解绑再重新进行绑定。因为bind和update中的内容差不多,所以我们可以把bind和update合并为同一个函数,在用自定义指令的简写方法写成下面的代码:Vue.directive(‘my-click’, function(el, binding, vnode, oldVnode){ //点击事件的回调挂在在元素myClick属性上 el.myClick && el.removeEventListener(‘click’, el.myClick); el.addEventListener(‘click’, el.myClick = function(){ console.log(el, binding.value) })})可以看到,数据已经变成我们想要的数据了。2、把binding挂在到元素上,更新数据后更新binding我们已经知道了,造成问题的根本原因是初始化运行bind钩子的时候为元素绑定事件,事件内获取的数据是初始化的时候传递过来的数据,因为形成了闭包,那么我们不使用能引起闭包的数据,把数据存到某一个地方,然后去更新这个数据。Vue.directive(‘my-click’,{ bind: function(el, binding, vnode, oldVnode){ el.binding = binding el.addEventListener(‘click’, function(){ var binding = this.binding console.log(this, binding.value) }) }, update: function(el, binding, vnode, oldVnode){ el.binding = binding }})这样也能达到我们想要的效果。3、更新父元素如果我们为父元素ul绑定一个变化的key值,这样,当数据变更的时候就会更新父元素,从而重新创建子元素,达到重新绑定指令的效果。<ul :key=“Date.now()"> <li v-for="(item,index) in arr” :key=“index” v-my-click=“item”>{{item}}</li></ul>这样也能达到我们想要的效果。 ...

April 4, 2019 · 2 min · jiezi

根据调试工具看源码之虚拟dom(一)

初次探索什么是虚拟domVue 通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪。请仔细看这行代码:return createElement(‘h1’, this.blogTitle)createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,及其子节点。我们把这样的节点描述为“虚拟节点 (Virtual Node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。以上这段对虚拟Dom的简短介绍来自Vue的官网第一个断点我们一开始的断点先打在app.vue的两个hook上:export default { name: ‘app’, created () { debugger }, mounted () { debugger }}刷新页面,此时调用栈中显示的函数跟预想中的不太一样: 在created这个hook执行之前,多出了一些比较奇怪的函数:createComponentInstanceForVnodeVue._updatemountComponent????看完以后我心中出现了一个疑问:为什么在created钩子执行之前就出现了mountComponent这个方法,到底是文档出问题了,还是文档出问题了呢?带着这个疑惑我们接着往下看mountComponent做了什么?通过上面打第一个断点,其实不难看出这样的执行顺序(从上往下):(annoymous)Vue.$mountmountComponent(annoymous)这步其实就是在执行我们的main.js,代码很短:…new Vue({ render: h => h(App)}).$mount(’#app’)Vue.$mountVue.prototype.$mount = function ( el, hydrating) { // 判断是否处于浏览器的环境 el = el && inBrowser ? query(el) : undefined; // 执行mountComponent return mountComponent(this, el, hydrating)};mountComponentfunction mountComponent ( vm, el, hydrating) { vm.$el = el; if (!vm.$options.render) { vm.$options.render = createEmptyVNode; // 开发环境下给出警告提示 if (process.env.NODE_ENV !== ‘production’) { /* istanbul ignore if / if ((vm.$options.template && vm.$options.template.charAt(0) !== ‘#’) || vm.$options.el || el) { warn( ‘You are using the runtime-only build of Vue where the template ’ + ‘compiler is not available. Either pre-compile the templates into ’ + ‘render functions, or use the compiler-included build.’, vm ); } else { warn( ‘Failed to mount component: template or render function not defined.’, vm ); } } } callHook(vm, ‘beforeMount’); var updateComponent; / istanbul ignore if / // 这里对测试环境跟正式环境的updateComponent 做了实现上的一个区分 if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) { updateComponent = function () { var name = vm._name; var id = vm._uid; var startTag = “vue-perf-start:” + id; var endTag = “vue-perf-end:” + id; mark(startTag); var vnode = vm._render(); mark(endTag); measure((“vue " + name + " render”), startTag, endTag); mark(startTag); vm._update(vnode, hydrating); mark(endTag); measure((“vue " + name + " patch”), startTag, endTag); }; } else { updateComponent = function () { vm._update(vm._render(), hydrating); }; } // we set this to vm._watcher inside the watcher’s constructor // since the watcher’s initial patch may call $forceUpdate (e.g. inside child // component’s mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { before: function before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, ‘beforeUpdate’); } } }, true / isRenderWatcher /); hydrating = false; // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, ‘mounted’); } return vm}简单罗列下上面这两段代码的逻辑????:调用beforeMount钩子函数封装一个updateComponent函数执行new Watcher并将updateComponent当做参数传入调用vm._update方法_update方法是如何被触发的?Watchervar Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher) { … // 将函数赋值给this.getter,这里是updateComponent函数 if (typeof expOrFn === ‘function’) { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = noop; process.env.NODE_ENV !== ‘production’ && warn( “Failed watching path: "” + expOrFn + “" " + ‘Watcher only accepts simple dot-delimited paths. ’ + ‘For full control, use a function instead.’, vm ); } } // 根据this.lazy决定是否触发get方法 this.value = this.lazy ? undefined : this.get();};Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try { // 这里调用getter方法,实际上也就是调用updateComponent方法并拿到返回值 value = this.getter.call(vm, vm); } catch (e) { if (this.user) { handleError(e, vm, (“getter for watcher "” + (this.expression) + “"”)); } else { throw e } } finally { // “touch” every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value); } popTarget(); this.cleanupDeps(); } // 返回函数(updateComponent)执行结果 return value};简单梳理下上面这段代码的逻辑:新建Watcher实例时,将updateComponent赋值给getter属性通过this.get方法,触发updateComponent函数最终拿到函数的执行结果小结通过上面的分析我们可以初步得出一个结论:组件的渲染跟Watcher离不开关系,父组件在执行完created钩子函数之后,会调用updateComponent函数对子组件进行处理深入研究如果前面你动手跟着断点一直走,那么不难得知存在这样的调用关系(从上往下):…mountComponentWatchergetupdateComponentVue._updatepatchcreateElmcreateComponentinitcreateComponentInstanceForVnodeVueComponentVue._initcallHookinvokeWithErrorHandlingcreatedVue.prototype._updateVue.prototype._update = function (vnode, hydrating) { var vm = this; var prevEl = vm.$el; var prevVnode = vm._vnode; // 重存储当前父实例 var restoreActiveInstance = setActiveInstance(vm); vm._vnode = vnode; // Vue.prototype.patch is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.patch(vm.$el, vnode, hydrating, false / removeOnly */); } else { // 执行patch函数 vm.$el = vm.patch(prevVnode, vnode); } restoreActiveInstance(); … };当然,我们通过全局检索可以得知_patch函数相关的代码????:// 只在浏览器环境下patch函数有效Vue.prototype.patch = inBrowser ? patch : noop;var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });function createPatchFunction (backend) { … return function patch (oldVnode, vnode, hydrating, removeOnly) { … }}这里先不深究patch的实现,我们只要知道patch是使用createPatchFunction来生成的一个闭包函数即可。子组件的渲染我们注意到,在子组件created钩子执行之前存在一个init方法????:var componentVNodeHooks = { init: function init (vnode, hydrating) { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch var mountedNode = vnode; // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode); } else { // 创建子组件实例 var child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ); // 对子组件执行$mount方法 child.$mount(hydrating ? vnode.elm : undefined, hydrating); } }, …相关代码:createComponentInstanceForVnodefunction createComponentInstanceForVnode ( vnode, // we know it’s MountedComponentVNode but flow doesn’t parent // activeInstance in lifecycle state) { // 初始化一个子组件的vnode配置 var options = { _isComponent: true, _parentVnode: vnode, parent: parent }; // 检查render函数内是否有template模板 var inlineTemplate = vnode.data.inlineTemplate; if (isDef(inlineTemplate)) { options.render = inlineTemplate.render; options.staticRenderFns = inlineTemplate.staticRenderFns; } // 返回子组件实例 return new vnode.componentOptions.Ctor(options)}总结存在子组件时,先初始化父组件,在created钩子执行之后,生成子组件的vnode实例子组件的created钩子执行完,检查子组件是否也有子组件子组件也存在子组件时,则重复1,否则直接执行$mount函数,渲染子组件 ...

April 2, 2019 · 4 min · jiezi

mybatis-plus源码分析之sql注入器

mybatis-plus是完全基于mybatis开发的一个增强工具,它的设计理念是在mybatis的基础上只做增强不做改变,为简化开发、提高效率而生,它在mybatis的基础上增加了很多实用性的功能,比如增加了乐观锁插件、字段自动填充功能、分页插件、条件构造器、sql注入器等等,这些在开发过程中都是非常实用的功能,mybatis-plus可谓是站在巨人的肩膀上进行了一系列的创新,我个人极力推荐。下面我会详细地从源码的角度分析mybatis-plus(下文简写成mp)是如何实现sql自动注入的原理。温故知新我们回顾一下mybatis的Mapper的注册与绑定过程,我之前也写过一篇「Mybatis源码分析之Mapper注册与绑定」,在这篇文章中,我详细地讲解了Mapper绑定的最终目的是将xml或者注解上的sql信息与其对应Mapper类注册到MappedStatement中,既然mybatis-plus的设计理念是在mybatis的基础上只做增强不做改变,那么sql注入器必然也是在将我们预先定义好的sql和预先定义好的Mapper注册到MappedStatement中。现在我将Mapper的注册与绑定过程用时序图再梳理一遍:解析一下这几个类的作用:SqlSessionFactoryBean:继承了FactoryBean和InitializingBean,符合spring loc容器bean的基本规范,可在获取该bean时调用getObject()方法到SqlSessionFactory。XMLMapperBuilder:xml文件解析器,解析Mapper对应的xml文件信息,并将xml文件信息注册到Configuration中。XMLStatementBuilder:xml节点解析器,用于构建select/insert/update/delete节点信息。MapperBuilderAssistant:Mapper构建助手,将Mapper节点信息封装成statement添加到MappedStatement中。MapperRegistry:Mapper注册与绑定类,将Mapper的类信息与MapperProxyFactory绑定。MapperAnnotationBuilder:Mapper注解解析构建器,这也是为什么mybatis可以直接在Mapper方法添加注解信息就可以不用在xml写sql信息的原因,这个构建器专门用于解析Mapper方法注解信息,并将这些信息封装成statement添加到MappedStatement中。从时序图可知,Configuration配置类存储了所有Mapper注册与绑定的信息,然后创建SqlSessionFactory时再将Configuration注入进去,最后经过SqlSessionFactory创建出来的SqlSession会话,就可以根据Configuration信息进行数据库交互,而MapperProxyFactory会为每个Mapper创建一个MapperProxy代理类,MapperProxy包含了Mapper操作SqlSession所有的细节,因此我们就可以直接使用Mapper的方法就可以跟SqlSession进行交互。饶了一圈,发现我现在还没讲sql注入器的源码分析,你不用慌,你得体现出老司机的成熟稳定,之前我也跟你说了sql注入器的原理了,只剩下源码分析,这时候我们应该在源码分析之前做足前戏,前戏做足就剩下撕、拉、扯、剥开源码的外衣了,来不及解释了快上车!源码分析从Mapper的注册与绑定过程的时序图看,要想将sql注入器无缝链接地添加到mybatis里面,那就得从Mapper注册步骤添加,果然,mp很鸡贼地继承了MapperRegistry这个类然后重写了addMapper方法:com.baomidou.mybatisplus.MybatisMapperRegistry#addMapper:public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { // TODO 如果之前注入 直接返回 return; // throw new BindingException(“Type " + type + // " is already known to the MybatisPlusMapperRegistry.”); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<>(type)); // It’s important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won’t try. // TODO 自定义无 XML 注入 MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } }}方法中将MapperAnnotationBuilder替换成了自家的MybatisMapperAnnotationBuilder,在这里特别说明一下,mp为了不更改mybatis原有的逻辑,会用继承或者直接粗暴地将其复制过来,然后在原有的类名上加上前缀“Mybatis”。com.baomidou.mybatisplus.MybatisMapperAnnotationBuilder#parse:public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); // TODO 注入 CURD 动态 SQL (应该在注解之前注入) if (BaseMapper.class.isAssignableFrom(type)) { GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type); } for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods();}sql注入器就是从这个方法里面添加上去的,首先判断Mapper是否是BaseMapper的超类或者超接口,BaseMapper是mp的基础Mapper,里面定义了很多默认的基础方法,意味着我们一旦使用上mp,通过sql注入器,很多基础的数据库操作都可以直接继承BaseMapper实现了,开发效率爆棚有木有!com.baomidou.mybatisplus.toolkit.GlobalConfigUtils#getSqlInjector:public static ISqlInjector getSqlInjector(Configuration configuration) { // fix #140 GlobalConfiguration globalConfiguration = getGlobalConfig(configuration); ISqlInjector sqlInjector = globalConfiguration.getSqlInjector(); if (sqlInjector == null) { sqlInjector = new AutoSqlInjector(); globalConfiguration.setSqlInjector(sqlInjector); } return sqlInjector;}GlobalConfiguration是mp的全局缓存类,用于存放mp自带的一些功能,很明显,sql注入器就存放在GlobalConfiguration中。这个方法是先从全局缓存类中获取自定义的sql注入器,如果在GlobalConfiguration中没有找到自定义sql注入器,就会设置一个mp默认的sql注入器AutoSqlInjector。sql注入器接口:// SQL 自动注入器接口public interface ISqlInjector { // 根据mapperClass注入SQL void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass); // 检查SQL是否注入(已经注入过不再注入) void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass); // 注入SqlRunner相关 void injectSqlRunner(Configuration configuration);}所有自定义的sql注入器都需要实现ISqlInjector接口,mp已经为我们默认实现了一些基础的注入器:com.baomidou.mybatisplus.mapper.AutoSqlInjectorcom.baomidou.mybatisplus.mapper.LogicSqlInjector其中AutoSqlInjector提供了最基本的sql注入,以及一些通用的sql注入与拼装的逻辑,LogicSqlInjector在AutoSqlInjector的基础上复写了删除逻辑,因为我们的数据库的数据删除实质上是软删除,并不是真正的删除。com.baomidou.mybatisplus.mapper.AutoSqlInjector#inspectInject:public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { String className = mapperClass.toString(); Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration()); if (!mapperRegistryCache.contains(className)) { inject(builderAssistant, mapperClass); mapperRegistryCache.add(className); }}该方法是sql注入器的入口,在入口处添加了注入过后不再注入的判断功能。// 注入单点 crudSql@Overridepublic void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { this.configuration = builderAssistant.getConfiguration(); this.builderAssistant = builderAssistant; this.languageDriver = configuration.getDefaultScriptingLanguageInstance(); // 驼峰设置 PLUS 配置 > 原始配置 GlobalConfiguration globalCache = this.getGlobalConfig(); if (!globalCache.isDbColumnUnderline()) { globalCache.setDbColumnUnderline(configuration.isMapUnderscoreToCamelCase()); } Class<?> modelClass = extractModelClass(mapperClass); if (null != modelClass) { // 初始化 SQL 解析 if (globalCache.isSqlParserCache()) { PluginUtils.initSqlParserInfoCache(mapperClass); } TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass); injectSql(builderAssistant, mapperClass, modelClass, table); }}注入之前先将Mapper类提取泛型模型,因为继承BaseMapper需要将Mapper对应的model添加到泛型里面,这时候我们需要将其提取出来,提取出来后还需要将其初始化成一个TableInfo对象,TableInfo存储了数据库对应的model所有的信息,包括表主键ID类型、表名称、表字段信息列表等等信息,这些信息通过反射获取。com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectSql:protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { if (StringUtils.isNotEmpty(table.getKeyProperty())) { /** 删除 / this.injectDeleteByIdSql(false, mapperClass, modelClass, table); /* 修改 / this.injectUpdateByIdSql(true, mapperClass, modelClass, table); /* 查询 / this.injectSelectByIdSql(false, mapperClass, modelClass, table); } /* 自定义方法 / this.inject(configuration, builderAssistant, mapperClass, modelClass, table);}所有需要注入的sql都是通过该方法进行调用,AutoSqlInjector还提供了一个inject方法,自定义sql注入器时,继承AutoSqlInjector,实现该方法就行了。com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectDeleteByIdSql:protected void injectSelectByIdSql(boolean batch, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID; SqlSource sqlSource; if (batch) { sqlMethod = SqlMethod.SELECT_BATCH_BY_IDS; StringBuilder ids = new StringBuilder(); ids.append("\n<foreach item="item" index="index" collection="coll" separator=",">"); ids.append("#{item}"); ids.append("\n</foreach>"); sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), ids.toString()), modelClass); } else { sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), table.getKeyProperty()), Object.class); } this.addSelectMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource, modelClass, table);}我随机选择一个删除sql的注入,其它sql注入都是类似这么写,SqlMethod是一个枚举类,里面存储了所有自动注入的sql与方法名,如果是批量操作,SqlMethod的定义的sql语句在添加批量操作的语句。再根据table和sql信息创建一个SqlSource对象。com.baomidou.mybatisplus.mapper.AutoSqlInjector#addMappedStatement:public MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType, KeyGenerator keyGenerator, String keyProperty, String keyColumn) { // MappedStatement是否存在 String statementName = mapperClass.getName() + “.” + id; if (hasMappedStatement(statementName)) { System.err.println("{" + statementName + “} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL.”); return null; } /* 缓存逻辑处理 */ boolean isSelect = false; if (sqlCommandType == SqlCommandType.SELECT) { isSelect = true; } return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null, parameterClass, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn, configuration.getDatabaseId(), languageDriver, null);}sql注入器的最终操作,这里会判断MappedStatement是否存在,这个判断是有原因的,它会防止重复注入,如果你的Mapper方法已经在Mybatis的逻辑里面注册了,mp不会再次注入。最后调用MapperBuilderAssistant助手类的addMappedStatement方法执行注册操作。到这里,一个sql自动注入器的源码就分析完了,其实实现起来很简单,因为它利用了Mybatis的机制,站在巨人的肩膀上进行创新。我希望在你们今后的职业生涯里,不要只做一个只会调用API的crud程序员,我们要有一种刨根问底的精神。阅读源码很枯燥,但阅读源码不仅会让你知道API底层的实现原理,让你知其然也知其所以然,还可以开阔你的思维,提升你的架构设计能力,通过阅读源码,可以看到大佬们是如何设计一个框架的,为什么会这么设计。 ...

March 18, 2019 · 3 min · jiezi

element-ui 源码解析,你知道 v-loading 是如何实现的吗?

前言相信大家肯定都用过 element-ui 里面的 v-loading 来写加载,但是如果让你来写一个的话你会怎么写呢?众所周知,element-ui 框架的 v-loading 有两种使用方式,一种是在需要 loading 的标签上直接使用 :v-loading=‘true’,这种方式官方称为指令,还有一种就是使用 this.$loading(options) 来调用,这种方式官方称之为服务。人类对于对于美好的事物总会有趋向性,我也不外乎如此,话不多说(个屁,就你话最多),直接扒源码。有些人天生就适合你,有的代码天生就适合阅读,优秀的开源项目都是如此,希望接下来我的解析也可以让你恍然大悟,随后窃笑一声,原来就这样。正文使用方式指令来回顾一下 v-loading 的指令使用方式<template> <div :v-loading.fullscreen=“true”>全屏覆盖</div></template>服务再来看看服务的使用方式mounted() { let loading = this.$loading({ fullscreen: true }) setTimeout(() => { loading.close() }, 1000)}稍微留点印象,我们简单粗暴一些,直接扒源码吧。起点打开你的 node_modules,目标 element-ui,在 src 目录下的 index.js 便是引入所有组件的地方,让我看看今天是哪两个小可爱要被我们扒光光。// element-ui\src\index.js// …// directive 指令装载Vue.use(Loading.directive)// prototype 服务装载Vue.prototype.$loading = Loading.service// …本着循序渐进的原则,我们先从使用较多的指令方式看起。Vue.use() 这个指令是 Vue 用来安装插件的,如果传入的参数是一个对象,则该对象要提供一个 install 方法,如果是一个函数,则该函数被视为 install 方法,在 install 方法调用时,会将 Vue 作为参数传入。如果要了解更多有关 plugin,点击这里了解更多有关 plugin 的内容,尤大大的官方文档简直百读不厌。但是老板!这 Loading 下的两个是啥玩意儿呢!来,我们看这里。import directive from ‘./src/directive’import service from ‘./src/index’export default { // 这里为什么有个 install 呢 // 当你使用单组件单注册的时候就会调用这里了 // 效果和下面一样,挂载指令,挂载服务 install(Vue) { Vue.use(directive) Vue.prototype.$loading = service }, // 就是上面的 Loading.directive directive, // 就是上面的 Loading.service service}接下来我们终于要深入源码了!dokidoki…v-loading 指令解析喝杯水,压抑住激动的心情,我们打开 packages 下的 loading\index.js,可以看到其对外曝露了 directive 指令,来,上路,我们来看他的源码。看似有百来行的代码,但是客官不要着急,我给你精简一下,贴上主要代码,为了方便解说,在其中我们只取 fullscreen 修饰词。import Vue from ‘vue’// 这里就是我们写的比较多的单 .vue 文件了,不拓展开了// 值得注意的是在这个单文件里面的 data() {} 声明的值// 我们下面会碰到import Loading from ‘./loading.vue’// 老板!Vue.extend() 是什么!// 代码片段之后我会简单介绍const Mask = Vue.extend(Loading)const loadingDirective = {}// 还记得 Vue.use() 的使用方法么?若传入的是对象,该对象需要一个 install 属性loadingDirective.install = Vue => { // 这里处理显示、消失 loading const toggleLoading = (el, binding) => { // 若绑定值为 truthy 则插入 loading 元素 // binding 值为 directive 的几个钩子中会接受到的参数 if (binding.value) { if (binding.modifiers.fullscreen) { insertDom(document.body, el, binding) } // 不然则将其设为不可见 // 从上往下读我们是第一次看到 visible 属性 // 别急,往下看,这个属性可以其实就是单文件 loading.vue 里面的 // data() { return { visible: false } } } else { el.instance.visible = false } } const insertDom = (parent, el, binding) => { // 将 loading 设为可见 el.instance.visible = true // appendChild 添加的元素若为同一个,则不会重复添加 // 我们 el.mask 所指的为同一个 dom 元素 // 因为我们只在 bind 的时候给 el.mask 赋值 // 并且在组件存在期间,bind 只会调用一次 parent.appendChild(el.mask) } // 在此注册 directive 指令 Vue.directive(’loading’, { bind: function(el, binding, vnode) { // 创建一个子组件,这里和 new Vue(options) 类似 // 返回一个组件实例 const mask = new Mask({ el: document.createElement(‘div’), // 有些人看到这里会迷惑,为什么这个 data 不按照 Vue 官方建议传函数进去呢? // 其实这里两者皆可 // 稍微做一点延展好了,在 Vue 源码里面,data 是延迟求值的 // 贴一点 Vue 源码上来 // return function mergedInstanceDataFn() { // let instanceData = typeof childVal === ‘function’ // ? childVal.call(vm, vm) // : childVal; // let defaultData = typeof parentVal === ‘function’ // ? parentVal.call(vm, vm) // : parentVal; // if (instanceData) { // return mergeData(instanceData, defaultData) // } else { // return defaultData // } // } // instanceData 就是我们现在传入的 data: {} // defaultData 就是我们 loading.vue 里面的 data() {} // 看了这段代码应该就不难理解为什么可以传对象进去了 data: { fullscreen: !!binding.modifiers.fullscreen } }) // 将创建的子类挂载到 el 上 // 在 directive 的文档中建议 // 应该保证除了 el 之外其他参数(binding、vnode)都是只读的 el.instance = mask // 挂载 dom el.mask = mask.$el // 若 binding 的值为 truthy 运行 toogleLoading binding.value && toggleLoading(el, binding) }, update: function(el, binding) { // 若旧不等于新值得时候(一般都是由 true 切换为 false 的时候) if (binding.oldValue !== binding.value) { // 切换显示或消失 toggleLoading(el, binding) } }, unbind: function(el, binding) { // 当组件 unbind 的时候,执行组件销毁 el.instance && el.instance.$destroy() } })}export default loadingDirectiveVue.extend 是什么在平时的代码中该方法我们主动调用的不多,但是在我们注册组件的时候,比如,Vue.component(‘my-component’, options),这个时候会自动调用 Vue.extend,直接上源码吧,该代码是当调用 Vue.component() 的时候将会执行的。ps:稍微做一下延展,对于 .vue 单文件,想必大家都能猜到是如何运作的了,首先会把 <template> 标签的内容转为 render() 函数,说到这个 render() 函数,我又想安利一波 JSX 了(打住!),然后就接着走 Vue.component() 这条线路注册组件了。…if (type === ‘component’ && isPlainObject(definition)) { definition.name = definition.name || id definition = this.options._base.extend(definition)}…而在 extend 里面又做了什么事情呢?我很想直接贴上源码再来分析,但是这样就超出我们今天要说的范围了,一言以蔽之, Vue.extend 接受参数并返回一个构造器,new 该构造器可以返回一个组件实例。相信到了这里大家已经对如何实现 v-loading 有了一定的了解了,勤快的小伙伴已经卷了袖子开干了,但是文章还没完,分析完指令模式我们还要看看服务模式,稍作休息,上路。点击这里了解更多有关 Vue.extend 的内容,依旧是官方文档,Vue 的文档我简直吹爆(破音)。服务如果打开开发者模式看过两种 loading 的方式,应该会注意到指令模式和服务模式的区别,最直观的就是若有 fullscreen 参数,指令模式下不会移除生成的 dom 元素,而在服务模式下会移除生成的 dom 元素。我的废话太多了,直接上源码,同样的,我会提取我们需要关注的代码片段方便分析,有了指令模式的基础,看服务模式的就没什么难度了。import Vue from ‘vue’import loadingVue from ‘./loading.vue’// 和指令模式一样,创建实例构造器const LoadingConstructor = Vue.extend(loadingVue)// 定义变量,若使用的是全屏 loading 那就要保证全局的 loading 只有一个let fullscreenLoading// 这里可以看到和指令模式不同的地方// 在调用了 close 之后就会移除该元素并销毁组件LoadingConstructor.prototype.close = function() { setTimeout(() => { if (this.$el && this.$el.parentNode) { this.$el.parentNode.removeChild(this.$el) } this.$destroy() }, 3000)}const Loading = (options = {}) => { // 若调用 loading 的时候传入了 fullscreen 并且 fullscreenLoading 不为 falsy // fullscreenLoading 只会在下面赋值,并且指向了 loading 实例 if (options.fullscreen && fullscreenLoading) { return fullscreenLoading } // 这里就不用说了吧,和指令中是一样的 let instance = new LoadingConstructor({ el: document.createElement(‘div’), data: options }) let parent = document.body // 直接添加元素 parent.appendChild(instance.$el) // 将其设置为可见 // 另外,写到这里的时候我查阅了相关的资料 // 自己以前一直理解 nextTick 是在 dom 元素更新完毕之后再执行回调 // 但是发现可能并不是这么回事,后续我会继续研究 // 如果干货足够的话我会写一篇关于 nextTick ui-render microtask macrotask 的文章 Vue.nextTick(() => { instance.visible = true }) // 若传入了 fullscreen 参数,则将实例存储 if (options.fullscreen) { fullscreenLoading = instance } // 返回实例,方便之后能够调用原型上的 close() 方法 return instance}export default Loading关于代码里的 fullscreenLoading 变量,根由 element-ui 官方的说明我们应该能了解个大概,这是为了保证覆盖整个页面的 loading 实例只有一个才存在的,官方文档说明如下。需要注意的是,以服务的方式调用的全屏 Loading 是单例的:若在前一个全屏 Loading 关闭前再次调用全屏 Loading,并不会创建一个新的 Loading 实例,而是返回现有全屏 Loading 的实例后语至此文章主体内容已经结束了,看起来只是一个 v-loading 的功能但却延伸出去很多的内容,我还精简了很多代码,所以我这只是管中窥豹很小的一部分内容,更多的内容推荐大家也去读读源码,你会发现不一样的世界。有些人和我说,看源码的时候一看开头,十来个模块引入,一看组件,百来行,瞬间就软掉了,没有看下去的欲望了,但怎么说呢,这个时候我推荐可以先从你有些了解的功能开始看起,比如 v-loading,看到这个就知道是 directive,再源码里看的时候关注关键点,比如 binding、el、vnode,很快就能理清楚代码的含义了。如果身边有更优秀的人那还好,可以以他为目标,但是当你一枝独秀,身边的人都不如自己,你不知自己该如何成长的时候,这个时候就应该去看看源码,从源码中学习,从源码中提升自己。该如何看待阅读?一天的饭钱就能买到别人可能一辈子的心血,多么值钱的买卖。— 无名虽然源码可能不能称作是一辈子的心血,但是想象一下,看源码的过程中就好像和作者面对面在交谈,这个模块怎么安排,那个算法怎么优化,看 Vue 的源码更是了,好像面对面和尤大大在聊天,这这这,想想就湿了啊!(眼角,为什么?因为太感动了)这篇文章只是我在和大家分享一些优秀的人的心血,结合了一些自己的理解,希望大家看完整篇文章之后能够有一些收获有一些沉淀,当然卷起袖子直接写一个自己的 v-custom-loading 就更好了。我在平时看源码的过程中会发现不少有意思的代码,如果有兴趣的话可以来我的项目里看看,里面就有我自己写的 v-custom-loading,还结合了一些自己的想法,比如将组件实例挂载的时候,我推荐如下写法。我的 vue-tiny-code 欢迎 starconst context = ‘@@loadingContext’…el[context] = { instance: mask }…这么写的原因就是不要污染 el 元素本身有的属性,毕竟有可能自己定义的属性会和 dom 原有的属性冲突。另外我在 vue-element-admin 项目中提了 pr 用的就是这种写法。具体可以看 vue-element-admin issue 1704 和 vue-element-admin issue 1705。ps:最近面试题泛滥,我想如果把标题改成《面试题解析,你知道 v-loading 该如何实现么?》会不会更有人关注一些?(狗头保平安)页脚代码即人生,我甘之如饴。我在这里 gayhub@jsjzh 欢迎大家来找我玩儿。小伙伴们可以直接加我或者加群,我们一起学前端、看源码、学算法,前端进阶,加油。 ...

March 17, 2019 · 4 min · jiezi

Beego Logs 源码分析 中篇

文件输出引擎使用到的读写锁 sync.RWMutex读写锁是一种同步机制,允许多个读操作同时读取数据,但是只允许一个写操作写数据。锁的状态有三种:读模式加锁、写模式加锁、无锁。无锁。读/写进程都可以进入。读模式锁。读进程可以进入。写进程不可以进入。写模式锁。读/写进程都不可以进入。就拿文件行数这个变量来看,如果开启了日志文件按小时按行数切割的功能,要先读取当前文件行数变量值。当并发情况下,多个 goroutine 在打日志,读取文件行数和修改文件行数便成为一对“读写”操作,所以需要用读写锁,读写锁对于读操作不会导致锁竞争和 goroutine 阻塞。// WriteMsg write logger message into file.func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error { ··· if w.Rotate { w.RLock() if w.needRotateHourly(len(msg), h) { w.RUnlock() w.Lock() if w.needRotateHourly(len(msg), h) { if err := w.doRotate(when); err != nil { fmt.Fprintf(os.Stderr, “FileLogWriter(%q): %s\n”, w.Filename, err) } } w.Unlock() } else if w.needRotateDaily(len(msg), d) { w.RUnlock() w.Lock() if w.needRotateDaily(len(msg), d) { if err := w.doRotate(when); err != nil { fmt.Fprintf(os.Stderr, “FileLogWriter(%q): %s\n”, w.Filename, err) } } w.Unlock() } else { w.RUnlock() } } w.Lock() , err := w.fileWriter.Write([]byte(msg)) if err == nil { w.maxLinesCurLines++ w.maxSizeCurSize += len(msg) } w.Unlock() ···}总结下 Goroutine 的使用监听 msgChan第一处是开启异步选项时,启动一个 goroutine 监听 msgChan 是否为空,发现不为空便取走日志信息进行输出。// Async set the log to asynchronous and start the goroutinefunc (bl *BeeLogger) Async(msgLen …int64) *BeeLogger { ··· go bl.startLogger() ···}// start logger chan reading.// when chan is not empty, write logs.func (bl *BeeLogger) startLogger() { gameOver := false for { select { case bm := <-bl.msgChan: bl.writeToLoggers(bm.when, bm.msg, bm.level) logMsgPool.Put(bm) ··· } ··· }}监听计时器实现日志文件按日期分割文件输出引擎 file.go 文件中,初始化 fileWriter *os.File 时启动一个 goroutine 执行 dailyRotate() :func (w *fileLogWriter) initFd() error { fd := w.fileWriter fInfo, err := fd.Stat() if err != nil { return fmt.Errorf(“get stat err: %s”, err) } w.maxSizeCurSize = int(fInfo.Size()) w.dailyOpenTime = time.Now() w.dailyOpenDate = w.dailyOpenTime.Day() w.maxLinesCurLines = 0 if w.Daily { go w.dailyRotate(w.dailyOpenTime) // <—— } if fInfo.Size() > 0 && w.MaxLines > 0 { count, err := w.lines() if err != nil { return err } w.maxLinesCurLines = count } return nil}dailyRotate() 方法中,tm 定时器时间一到,便会往 tm.C 通道发送当前时间,此时 a 语句便停止阻塞,可以继续往下执行。func (w *fileLogWriter) dailyRotate(openTime time.Time) { y, m, d := openTime.Add(24 * time.Hour).Date() nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location()) tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100)) <-tm.C // <— a 语句 w.Lock() if w.needRotate(0, time.Now().Day()) { if err := w.doRotate(time.Now()); err != nil { fmt.Fprintf(os.Stderr, “FileLogWriter(%q): %s\n”, w.Filename, err) } } w.Unlock()}开启新的 goroutine 删除失效的日志文件因为删除文件涉及文件 IO 处理,为了避免阻塞主线程,便交由另外 goroutine 去做。,go w.deleteOldLog(),超过 MaxDays 的日志文件便是失效的。// DoRotate means it need to write file in new file.// new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size)func (w fileLogWriter) doRotate(logTime time.Time) error { ··· err = os.Rename(w.Filename, fName) ··· startLoggerErr := w.startLogger() go w.deleteOldLog() ···}func (w fileLogWriter) deleteOldLog() { dir := filepath.Dir(w.Filename) filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) { defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, “Unable to delete old log ‘%s’, error: %v\n”, path, r) } }() if info == nil { return } if !info.IsDir() && info.ModTime().Add(24time.Hourtime.Duration(w.MaxDays)).Before(time.Now()) { if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && strings.HasSuffix(filepath.Base(path), w.suffix) { os.Remove(path) } } return })}使用 goto 语句保证即使发生错误也要重启 LoggerdoRotate() 方法大体逻辑:重命名之前写入的日志文件,err = os.Rename(w.Filename, fName)首先找到 一个可用的 filename ,循环遍历1-999,如果找不到报错;,err:=os.Lstat(fName) :若以 fName 为名的文件不存在则返回 err 不为空。os.Chmod(fName, os.FileMode(rotatePerm)) 修改文件权限。重新启动 Logger :一是启动 Logger ,w.startLogger();二是开启一个 goroutine 删除失效的日志文件。注意到下面代码段中的 a 语句和 b 语句,它们并不是返回错误阻止代码继续执行,而是即使发生错误也会保证重启一个新的 Logger。如果是执行到 a 语句这种情况,有可能是该日志文件已经被别的程序删除或者其他原因导致文件不存在,但大可不必因为一个日志文件的丢失而阻止了新 Logger 的启动,简而言之,这个错误是可以忽略的。// DoRotate means it need to write file in new file.// new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size)func (w *fileLogWriter) doRotate(logTime time.Time) error { // file exists // Find the next available number num := 1 fName := "" rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64) if err != nil { return err } _, err = os.Lstat(w.Filename) if err != nil { //even if the file is not exist or other ,we should RESTART the logger goto RESTART_LOGGER // <——- a 语句 } if w.MaxLines > 0 || w.MaxSize > 0 { for ; err == nil && num <= 999; num++ { fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format(“2006-01-02”), num, w.suffix) _, err = os.Lstat(fName) } } else { fName = fmt.Sprintf("%s.%s%s", w.fileNameOnly, w.dailyOpenTime.Format(“2006-01-02”), w.suffix) _, err = os.Lstat(fName) for ; err == nil && num <= 999; num++ { fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", w.dailyOpenTime.Format(“2006-01-02”), num, w.suffix) _, err = os.Lstat(fName) } } // return error if the last file checked still existed if err == nil { return fmt.Errorf( “Rotate: Cannot find free log number to rename %s”, w.Filename) } // close fileWriter before rename w.fileWriter.Close() // Rename the file to its new found name // even if occurs error,we MUST guarantee to restart new logger err = os.Rename(w.Filename, fName) if err != nil { goto RESTART_LOGGER // <——- b 语句 } err = os.Chmod(fName, os.FileMode(rotatePerm))RESTART_LOGGER: // <——- startLoggerErr := w.startLogger() go w.deleteOldLog() if startLoggerErr != nil { return fmt.Errorf(“Rotate StartLogger: %s”, startLoggerErr) } if err != nil { return fmt.Errorf(“Rotate: %s”, err) } return nil}涉及到 sync.WaitGroup 的使用a 语句处,开启 goroutine 前计数器加一,执行完该 goroutine 后计数器减一,即 b 语句。// Async set the log to asynchronous and start the goroutinefunc (bl *BeeLogger) Async(msgLen …int64) *BeeLogger { ··· bl.wg.Add(1) // <—– a 语句 go bl.startLogger() return bl}// start logger chan reading.// when chan is not empty, write logs.func (bl *BeeLogger) startLogger() { gameOver := false for { select { case bm := <-bl.msgChan: bl.writeToLoggers(bm.when, bm.msg, bm.level) logMsgPool.Put(bm) case sg := <-bl.signalChan: // Now should only send “flush” or “close” to bl.signalChan bl.flush() if sg == “close” { for _, l := range bl.outputs { l.Destroy() } bl.outputs = nil gameOver = true } bl.wg.Done() // <—— b 语句 } if gameOver { break } }}分析并发执行下面 Flush() 方法的情况。假设有 A , B , C 三个 goroutine,并且假设 A 先执行到 e 语句,从a 语句知道初始计数器为 1 ,所以 e 语句必须等到上述 startLogger-goroutine 执行 b 语句完毕后才停止阻塞。而后 A 再让计数器加一。因为 bl.signalChan 的缓存大小为1,所以 B,C 阻塞在 d 语句,等到 B,C 其中之一能执行 e 语句的时候计数器必然大于0,才不会导致永久阻塞。所以 f 语句要放在 e 语句之后。// Flush flush all chan data.func (bl *BeeLogger) Flush() { if bl.asynchronous { bl.signalChan <- “flush” // <—— d 语句 bl.wg.Wait() // <—— e 语句 bl.wg.Add(1) // <—— f 语句 return } bl.flush()}因此再看下面的 Close() 方法,它是不能并发执行的,会导致 “panic: close of closed channel"错误。不过笔者暂时没懂为什么 beego logs 不把这里做一下改进,让 Close() 也支持并发调用不好吗?// Close close logger, flush all chan data and destroy all adapters in BeeLogger.func (bl *BeeLogger) Close() { if bl.asynchronous { bl.signalChan <- “close” bl.wg.Wait() // <—— g 语句 close(bl.msgChan) } else { bl.flush() for _, l := range bl.outputs { l.Destroy() } bl.outputs = nil } close(bl.signalChan)} ...

March 14, 2019 · 5 min · jiezi

Beego Logs 源码分析 上篇

最近参加春招,确实挺受打击,平常做项目遇到的问题,学到的知识点没有及时总结,导致在面试的时候无法清晰的描述出来,因此本专栏后续日常更新,总结编程之路的点滴。下面进入正题。Beego Logs 使用先大致了解怎么使用,再进行剖析。 // Test console without color func TestConsoleNoColor(t *testing.T) { log := NewLogger(100) log.SetLogger(“console”, {"color":false}) bl.Error(“error”) bl.Warning(“warning”) } // NewLogger returns a new BeeLogger. // channelLen means the number of messages in // chan(used where asynchronous is true). // if the buffering chan is full, logger adapters write to file or other way. func NewLogger(channelLens …int64) *BeeLogger { bl := new(BeeLogger) bl.level = LevelDebug bl.loggerFuncCallDepth = 2 bl.msgChanLen = append(channelLens, 0)[0] if bl.msgChanLen <= 0 { bl.msgChanLen = defaultAsyncMsgLen } bl.signalChan = make(chan string, 1) bl.setLogger(AdapterConsole) return bl }上面有一句代码:bl.msgChanLen = append(channelLens, 0)[0]往 channelLens 切片添加一个值为零的元素后再取头个元素,这个技巧有以下好处:Go 不支持可选参数,但 Go 支持可变参数,这样做变相达到了可选参数的效果。如果 chanelLens 原来为空的话也能拿出一个值为零的元素出来,不用再去判断参数是否为空数组。loggerFuncCallDepth 的值应设为多少这个变量表示函数调用的栈深度,用于记录日志时同时打印出当时执行语句的位置,包括文件名和行号。虽然 NewLogger 方法里面默认将 loggerFuncCallDepth 置为2,但是如果你单独使用logs包时应根据情况设置不同值。举个栗子:···bl.Error(“error”) // ———-a 语句···// Error Log ERROR level message.func (bl *BeeLogger) Error(format string, v …interface{}) { if LevelError > bl.level { return } bl.writeMsg(LevelError, format, v…) // ———-b 语句}func (bl *BeeLogger) writeMsg(logLevel int, msg string, v …interface{}) error { ··· if bl.enableFuncCallDepth { _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) // ———-c 语句 ··· } ···}func Caller(skip int) (pc uintptr, file string, line int, ok bool) { ···}关于 Caller 方法的 skip 参数:The argument skip is the number of stack frames to ascend, with 0 identifying the caller of Caller. (For historical reasons the meaning of skip differs between Caller and Callers.)即,skip 为零的时候,表示 Caller 方法本身,而我们需要的是 a 语句的所在的行号和文件名,所以这种情境下需要提升 2 个栈帧数。工厂方法模式自定义日志输出引擎以下是添加 console 输出引擎的用法,直接调用 SetLogger 方法即可。func TestConsole(t *testing.T) { ··· log.SetLogger(“console”, {"color":false}) ···}type newLoggerFunc func() Loggervar adapters = make(map[string]newLoggerFunc)func (bl *BeeLogger) SetLogger(adapterName string, configs …string) error { ··· return bl.setLogger(adapterName, configs…)}func (bl *BeeLogger) setLogger(adapterName string, configs …string) error { ··· log, ok := adapters[adapterName] if !ok { return fmt.Errorf(“logs: unknown adaptername %q (forgotten Register?)”, adapterName) } lg := log() //——— c 语句 err := lg.Init(config) if err != nil { fmt.Fprintln(os.Stderr, “logs.BeeLogger.SetLogger: “+err.Error()) return err } bl.outputs = append(bl.outputs, &nameLogger{name: adapterName, Logger: lg}) return nil}func Register(name string, log newLoggerFunc) { ··· adapters[name] = log}在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。上面 c 语句可以看到,具体需要用到什么输出引擎,BeeLogger 不负责它们的创建,而是由这些输出引擎自己去做。从 adapters 这个 map 结构里找到该输出引擎的构造方法, 并且执行这个构造方法。例如 file.go 里面定义了如何构造一个文件输出引擎,并通过 init 方法注册:func init() { Register(AdapterFile, newFileWriter)}// newFileWriter create a FileLogWriter returning as LoggerInterface.func newFileWriter() Logger { w := &fileLogWriter{ Daily: true, MaxDays: 7, Rotate: true, RotatePerm: “0440”, Level: LevelTrace, Perm: “0660”, } return w}为什么要用到互斥锁?直接找到以下四处代码段:func (bl *BeeLogger) Async(msgLen …int64) *BeeLogger { bl.lock.Lock() defer bl.lock.Unlock() ···}func (bl *BeeLogger) SetLogger(adapterName string, configs …string) error { bl.lock.Lock() defer bl.lock.Unlock() ···}func (bl *BeeLogger) DelLogger(adapterName string) error { bl.lock.Lock() defer bl.lock.Unlock() ···}func (bl *BeeLogger) writeMsg(logLevel int, msg string, v …interface{}) error { if !bl.init { bl.lock.Lock() bl.setLogger(AdapterConsole) bl.lock.Unlock() } ···}可以看出,在进行 SetLogger 、 DelLogger 这些操作时涉及到临界资源 bl *BeeLogger 相关配置字段的更改,必须操作前加锁保证并发安全。临界资源是指每次仅允许一个进程访问的资源。Asynchronous 选项为什么能提升性能func (bl *BeeLogger) writeMsg(logLevel int, msg string, v …interface{}) error { ··· if bl.asynchronous { lm := logMsgPool.Get().(*logMsg) lm.level = logLevel lm.msg = msg lm.when = when bl.msgChan <- lm } else { bl.writeToLoggers(when, msg, logLevel) } return nil}如果开启 asynchronous 选项,将日志信息写进 msgChan 就完事了,可以继续执行其他的逻辑代码,除非 msgChan 缓存满了,否则不会发生阻塞,同时,还开启一个 goroutine 监听 msgChan,一旦 msgChan 不为空,将日志信息输出:func (bl *BeeLogger) Async(msgLen …int64) *BeeLogger { ··· go bl.startLogger() ···}// start logger chan reading.// when chan is not empty, write logs.func (bl *BeeLogger) startLogger() { gameOver := false for { select { case bm := <-bl.msgChan: bl.writeToLoggers(bm.when, bm.msg, bm.level) logMsgPool.Put(bm) case sg := <-bl.signalChan: // Now should only send “flush” or “close” to bl.signalChan bl.flush() if sg == “close” { for _, l := range bl.outputs { l.Destroy() } bl.outputs = nil gameOver = true } bl.wg.Done() } if gameOver { break } }}从 logs package 外的 log.go 文件了解 beego 如何解耦在 logs 包(package)外面还有一个 beego package 下的 log.go 文件,截取一段代码: // github.com/astaxie/beego/log.go package beego import “github.com/astaxie/beego/logs” // BeeLogger references the used application logger. var BeeLogger = logs.GetBeeLogger() // SetLevel sets the global log level used by the simple logger. func SetLevel(l int) { logs.SetLevel(l) } // github.com/astaxie/beego/logs/log.go// beeLogger references the used application logger.var beeLogger = NewLogger()// GetBeeLogger returns the default BeeLoggerfunc GetBeeLogger() *BeeLogger { return beeLogger}// SetLevel sets the global log level used by the simple logger.func SetLevel(l int) { beeLogger.SetLevel(l)}beego 为什么还在外面包了一层调用 logs 包里面的方法呢?其实 beego 本身是一个 Web 框架,那么本质就是一个服务端程序,服务端程序需要一个日志记录器来记录服务器的运行状况,那么调用 logs 包的代码以及其他一些配置、初始化的逻辑,就在 log.go 中处理。这里其实也没有什么,就是一开始笔者在读源码的时候老是被这里疑惑,认为多此一举。其实要实现一个功能单一的 logs 包并与其他模块解耦,这么做的确不错。再如, beego 的 session 模块,为了不与 logs 模块耦合,所以 session 模块也造了一个仅供自己模块内使用的日志记录器 SessionLog 。代码如下:// Log implement the log.Loggertype Log struct { *log.Logger}// NewSessionLog set io.Writer to create a Logger for session.func NewSessionLog(out io.Writer) *Log { sl := new(Log) sl.Logger = log.New(out, “[SESSION]”, 1e9) return sl}不妨看看 Beego 官方的架构图:beego 是基于八大独立的模块构建的,是一个高度解耦的框架。用户即使不使用 beego 的 HTTP 逻辑,也依旧可以使用这些独立模块,例如:你可以使用 cache 模块来做你的缓存逻辑;使用日志模块来记录你的操作信息;使用 config 模块来解析你各种格式的文件。所以 beego 不仅可以用于 HTTP 类的应用开发,在你的 socket 游戏开发中也是很有用的模块,这也是 beego 为什么受欢迎的一个原因。大家如果玩过乐高的话,应该知道很多高级的东西都是一块一块的积木搭建出来的,而设计 beego 的时候,这些模块就是积木,高级机器人就是 beego。 ...

March 12, 2019 · 4 min · jiezi

Spring AOP从零单排-织入时期源码分析

问题:Spring AOP代理中的运行时期,是在初始化时期织入还是获取对象时期织入?织入就是代理的过程,指目标对象进行封装转换成代理,实现了代理,就可以运用各种代理的场景模式。何为AOP简单点来定义就是切面,是一种编程范式。与OOP对比,它是面向切面,为何需要切面,在开发中,我们的系统从上到下定义的模块中的过程中会产生一些横切性的问题,这些横切性的问题和我们的主业务逻辑关系不大,假如不进行AOP,会散落在代码的各个地方,造成难以维护。AOP的编程思想就是把业务逻辑和横切的问题进行分离,从而达到解耦的目的,使代码的重用性、侵入性低、开发效率高。AOP使用场景日志记录;记录调用方法的入参和结果返参。用户的权限验证;验证用户的权限放到AOP中,与主业务进行解耦。性能监控;监控程序运行方法的耗时,找出项目的瓶颈。事务管理;控制Spring事务,Mysql事务等。AOP概念点AOP和Spring AOP的关系在这里问题中,也有一个类似的一对IOC和DI(dependency injection)的关系,AOP可以理解是一种编程目标,Spring AOP就是这个实现这个目标的一种手段。同理IOC也是一种编程目标,DI就是它的一个手段。SpringAOP和AspectJ是什么关系在Spring官网可以看到,AOP的实现提供了两种支持分别为@AspectJ、Schema-based AOP。其实在Spring2.5版本时,Spring自己实现了一套AOP开发的规范和语言,但是这一套规范比较复杂,可读性差。之后,Spring借用了AspectJ编程风格,才有了@AspectJ的方式支持,那么何为编程风格。Annotation注解方式;对应@AspectJJavaConfig;对应Schema-based AOPSpringAOP和AspectJ的详细对比,在之后的章节会在进行更加详细的说明,将会在他们的背景、织入方法、性能做介绍。Spring AOP的应用阅读官网,是我们学习一个新知识的最好途径,这个就是Spring AOP的核心概念点,跟进它们的重要性,我做了重新的排序,以便好理解,这些会为我们后续的源码分析起到作用。Aspect:切面;使用@Aspect注解的Java类来实现,集合了所有的切点,做为切点的一个载体,做一个比喻就像是我们的一个数据库。Tips:这个要实现的话,一定要交给Spirng IOC去管理,也就是需要加入@Component。Pointcut:切点;表示为所有Join point的集合,就像是数据库中一个表。Join point:连接点;俗称为目标对象,具体来说就是servlet中的method,就像是数据库表中的记录。Advice:通知;这个就是before、after、After throwing、After (finally)。Weaving:把代理逻辑加入到目标对象上的过程叫做织入。target:目标对象、原始对象。aop Proxy:代理对象 包含了原始对象的代码和增加后的代码的那个对象。Tips这个应用点,有很多的知识点可以让我们去挖掘,比如Pointcut中execution、within的区别,我相信你去针对性搜索或者官网都未必能有好的解释,稍后会再专门挑一个文章做重点的使用介绍;SpringAOP源码分析为了回答我们的一开始的问题,前面的几个章节我们做了一些简单的概念介绍做为铺垫,那么接下来我们回归正题,正面去切入问题。以码说话,我们以最简洁的思路把AOP实现,我们先上代码。项目结构介绍项目目录结构,比较简单,5个主要的文件;pom.xml核心代码;spring-content是核心jar,已经包含了spring所有的基础jar,aspectjweaver是为了实现AOP。AppConfig.java;定义一个Annotation,做为我们Spirng IOC容器的启动类。package com.will.config;@Configuration@ComponentScan(“com.will”)@EnableAspectJAutoProxy(proxyTargetClass = false)public class AppConfig { }WilAspect.java ;按照官网首推的方式(@AspectJ support),实现AOP代理。package com.will.config;/** * 定义一个切面的载体 /@Aspect@Componentpublic class WilAspect { /* * 定义一个切点 / @Pointcut(“execution( com.will.dao..(..))”) public void pointCutExecution(){ } /** * 定义一个Advice为Before,并指定对应的切点 * @param joinPoint / @Before(“pointCutExecution()”) public void before(JoinPoint joinPoint){ System.out.println(“proxy-before”); }}Dao.javapackage com.will.dao;public interface Dao { public void query();}UserDao.javapackage com.will.dao;import org.springframework.stereotype.Component;@Componentpublic class UserDao implements Dao { public void query() { System.out.println(“query user”); }}Test.javapackage com.will.test;import com.will.config.AppConfig;import com.will.dao.Dao;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class Test { public static void main(String[] args) { /* * new一个注册配置类,启动IOC容器,初始化时期; / AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class); /* * 获取Dao对象,获取对象时期,并进行query打印 */ Dao dao = annotationConfigApplicationContext.getBean(Dao.class); dao.query(); annotationConfigApplicationContext.start(); }}好了,这样我们整体的AOP代理就已经完成。问题分析测试究竟是哪个时期进行对象织入的,比如Test类中,究竟是第一行还是第二行进行织入的,我们只能通过源码进行分析,假如是你,你会进行如何的分析源码解读。Spring的代码非常优秀,同时也非常复杂,那是一个大项目,里面进行了很多的代码封装,那么的代码你三天三夜也读不完,甚至于你都不清楚哪一行的该留意的,哪一行是起到关键性作用的,这里教几个小技巧。看方法返回类型;假如是void返回类型的,看都不看跳过。返回结果是对象,比如T果断进行去进行跟踪。假设法;就当前场景,我们大胆假设是第二行进行的织入。借助好的IDE;IDEA可以帮我们做很多的事情,它的debug模式中的条件断点、调用链(堆栈)会帮助到我们。假设法源码分析debug模式StepInfo(F5)后,进入 AbstractApplicationContext.getBean方法,这个是Spring应用上下文中最重要的一个类,这个抽象类中提供了几乎ApplicationContext的所有操作。这里第一个语句返回void,我们可以直接忽略,看下面的关键性代码。继续debug后,会进入到 DefaultListableBeanFactory 类中,看如下代码return new NamedBeanHolder<>(beanName, getBean(beanName, requiredType, args));在该语句中,这个可以理解为 DefaultListableBeanFactory 容器,帮我们获取相应的Bean。进入到AbstractBeanFactory类的doGetBean方法之后,我们运行完。Object sharedInstance = getSingleton(beanName);语句之后,看到 sharedInstance 对象打印出&Proxyxxx ,说明在getSingleton 方法的时候就已经获取到了对象,所以需要跟踪进入到 getSingleton 方法中,继续探究。不方便不方便我们进行问题追踪到这个步骤之后,我需要引入IDEA的条件断点,不方便我们进行问题追踪因为Spring会初始化很多的Bean,我们再ObjectsharedInstance=getSingleton(beanName);加入条件断点语句。继续debug进入到DefaultSingletonBeanRegistry的getSingleton方法。我们观察下执行完ObjectsingletonObject=this.singletonObjects.get(beanName); 之后的singletonObject已经变成为&ProxyUserDao,这个时候Spring最关键的一行代码出现了,请注意这个this.singletonObjects。this.singletonObjects就是相当IOC容器,反之IOC容器就是一个线程安全的线程安全的HashMap,里面存放着我们需要Bean。我们来看下singletonObjects存放着的数据,里面就有我们的UserDao类。这就说明,我们的初始化的时期进行织入的,上图也有整个Debug模式的调用链。源码深层次探索通过上一个环节已经得知是在第一行进行初始化的,但是它在初始化的时候是什么时候完成织入的,抱着求知的心态我们继续求证。还是那个问题,那么多的代码,我的切入点在哪里?既然singletonObjects是容器,存放我们的Bean,那么找到关键性代码在哪里进行存放(put方法)就可以了。于是我们通过搜索定位到了。我们通过debug模式的条件断点和debug调用链模式,就可以进行探索。这个时候借助上图中的调用链,我们把思路放到放到IDEA帮我定位到的两个方法代码上。DefaultSingletonBeanRegistry.getSingleton我们一步步断点,得知,当运行完singletonObject=singletonFactory.getObject();之后,singletonObject已经获得了代理。至此我们知道,代理对象的获取关键在于singletonFactory对象,于是又定位到了AbstractBeanFactorydoGetBean方法,发现singletonFactory参数是由createBean方法创造的。这个就是Spring中IOC容器最核心的地方了,这个代码的模式也值得我们去学习。sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } });这个第二个参数是用到了jdk8中的lambda,这一段的含义是就是为了传参,重点看下 createBean(beanName,mbd,args);代码。随着断点,我们进入到这个类方法里面。AbstractAutowireCapableBeanFactory.createBean中的;ObjectbeanInstance=doCreateBean(beanName,mbdToUse,args)方法;doCreateBean方法中,做了简化。Initialize the bean instance. Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } … return exposedObject;当运行完 exposedObject=initializeBean(beanName,exposedObject,mbd);之后,我们看到exposedObject已经是一个代理对象,并执行返回。这一行代码就是取判断对象要不要执行代理,要的话就去初始化代理对象,不需要直接返回。后面的initializeBean方法是涉及代理对象生成的逻辑(JDK、Cglib),后续会有一个专门的章节进行详细介绍。总结通过源码分析,我们得知,Spring AOP的代理对象的织入时期是在运行Spring初始化的时候就已经完成的织入,并且也分析了Spring是如何完成的织入。 ...

March 11, 2019 · 2 min · jiezi

不一样的redux源码解读

1、本文不涉及redux的使用方法,因此可能更适合使用过 redux 的同学阅读2、当前redux版本为4.0.1 Redux作为大型React应用状态管理最常用的工具。虽然在平时的工作中很多次的用到了它,但是一直没有对其原理进行研究。最近看了一下源码,下面是我自己的一些简单认识,如有疑问欢迎交流。 1.createStore 结合使用场景我们首先来看一下createStore方法。 // 这是我们平常使用时创建store const store = createStore(reducers, state, enhance); 以下源码为去除异常校验后的源码, export default function createStore(reducer, preloadedState, enhancer) {// 如果有传入合法的enhance,则通过enhancer再调用一次createStoreif (typeof enhancer !== 'undefined') {if (typeof enhancer !== 'function') {throw new Error('Expected the enhancer to be a function.')}return enhancer(createStore)(reducer, preloadedState) // 这里涉及到中间件,后面介绍applyMiddleware时在具体介绍}let currentReducer = reducer //把 reducer 赋值给 currentReducerlet currentState = preloadedState //把 preloadedState 赋值给 currentStatelet currentListeners = [] //初始化监听函数列表let nextListeners = currentListeners //监听列表的一个引用let isDispatching = false //是否正在dispatchfunction ensureCanMutateNextListeners() {}function getState() {}function subscribe(listener) {}function dispatch(action) {}function replaceReducer(nextReducer) {}// 在 creatorStore 内部没有看到此方法的调用,就不讲了function observable() {}//初始化 store 里的 state treedispatch({ type: ActionTypes.INIT })return {dispatch,subscribe,getState,replaceReducer,[$$observable]: observable}} 我们可以看到creatorStore方法除了返回我们常用的方法外,还做了一次初始化过程dispatch({ type: ActionTypes.INIT });那么dispatch干了什么事情呢? ...

March 10, 2019 · 4 min · jiezi

YYCache 源码学习(一):YYMemoryCache

其实最近是在重新熟练Swift的使用,我想出了一个比较实用的方法,那就是一边看OC的项目,看懂之后用Swift实现一遍。这样既学习了优秀的源码又练习了Swift,一举两得。之前看过几篇文章是剖析YYKit里面的一些小模块,对源码对一些解读。不得不说作者ibireme的设计思维和技术细节的处理都非常的棒。所以就选了YYKit里面的一些小模块入手。YYCache主要分为了两部分:YYMemoryCache内存缓存和磁盘缓存YYDiskCache。平常使用的时候我们一般都只直接操作YYCache这个类,他是对内存缓存和磁盘缓存的封装。这篇文章主要是讲解YYCache模块里面的YYMemoryCache部分。API我们先可以看一下YYMemoryCache的.h文件,浏览一起属性和方法。大多数的都可以见名知意的。@interface YYMemoryCache : NSObject#pragma mark - Attribute///=============================================================================/// @name Attribute///=============================================================================@property (nullable, copy) NSString *name;@property (readonly) NSUInteger totalCount;@property (readonly) NSUInteger totalCost;#pragma mark - Limit///=============================================================================/// @name Limit///=============================================================================@property NSUInteger countLimit;@property NSUInteger costLimit;@property NSTimeInterval ageLimit; //过期时间@property NSTimeInterval autoTrimInterval;//自动处理的间隔时间@property BOOL shouldRemoveAllObjectsOnMemoryWarning;@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);@property BOOL releaseOnMainThread;@property BOOL releaseAsynchronously;#pragma mark - Access Methods///=============================================================================/// @name Access Methods///=============================================================================- (BOOL)containsObjectForKey:(id)key;- (nullable id)objectForKey:(id)key;- (void)setObject:(nullable id)object forKey:(id)key;- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;- (void)removeObjectForKey:(id)key;- (void)removeAllObjects;#pragma mark - Trim///=============================================================================/// @name Trim///=============================================================================- (void)trimToCount:(NSUInteger)count;- (void)trimToCost:(NSUInteger)cost;- (void)trimToAge:(NSTimeInterval)age;@end我把乱七八糟的注释都删掉了,这样可以直观的来看,api分为四个部分,前两部分都是一些属性,后面两个是方法。第一个Attribute部分是YYMemoryCache类储存的一些基本的属性:name,totalCount(储存对象的总个数),totalCost(储存的总占内存)。Limit部分是一些限制条件,就不一一的说了,单说一个releaseOnMainThread这个属性,可能会有因为,如果如果能异步释放,为什么还要强制去主线程释放呢 ? 这是因为有一些类,像UIView/CALayer这种是要在主线程中释放的,源码注释中也有提到。第三部分就是一些跟储存相关的方法,最后一部分就是根据限制条件修剪处理内存的方法了 ~.m代码剖析LRU 缓存淘汰算法YYMemoryCache是提供了内存修剪的方法的,既然有修剪,那么我们得有一个算法来确定是修剪掉哪一些。YYMemoryCache 和 YYDiskCache 都是实现的 LRU (least-recently-used) ,即最近最少使用淘汰算法。具体怎么样实现我们往后再说。实现缓存方式.m的最前面是实现了两个内部类_YYLinkedMap和_YYLinkedMapNode。 可以看出具体的缓存方法是通过一个双向列表和散列容器来实现的。_YYLinkedMap中给出来操作结点的方法- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;- (void)bringNodeToHead:(_YYLinkedMapNode *)node;- (void)removeNode:(_YYLinkedMapNode *)node;- (_YYLinkedMapNode *)removeTailNode;- (void)removeAll;具体细节剖析先总起来说一下这些实现的代码,其实很容易读懂,就是通过一个链表的形式来处理缓存的数据,添加缓存的时候,就往链表的尾部添加一个节点,(通过节点来表示我们实际要储存的数据),如果要根据限制条件修剪内存的话,也是循环的删除尾部的那个节点,直到符合限制条件。那我们的LRU 缓存淘汰算法具体怎么使用,我发现在每一个读取了一个数据之后,会把这个数据在链表中对应的结点移动到头部,这样在大概率的情况下使用频率高的缓存数据会在链表的前面。所以修剪的时候可以从尾部修剪。在具体的实现代码中,作者有很多很亮眼的操作,我们来欣赏一下。1.定时修剪内存// 根据限制条件修剪内存的占用 并根绝设定的时间递归调用- (void)_trimRecursively { __weak typeof(self) _self = self; //注意这个dispatch_after后面使用的dispatch_get_global_queue 异步 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ __strong typeof(_self) self = _self; if (!self) return; [self _trimInBackground]; [self _trimRecursively]; });}这个就是通过一个延时来调用修剪内存的方法,然后在递归调用本身。注意的是dispatch_after后面使用的dispatch_get_global_queue 来进行异步操作。2.修剪内存的逻辑- (void)_trimToCost:(NSUInteger)costLimit { BOOL finish = NO; pthread_mutex_lock(&_lock); if (costLimit == 0) { [_lru removeAll]; finish = YES; } else if (_lru->_totalCost <= costLimit) { finish = YES; } pthread_mutex_unlock(&_lock); if (finish) return; NSMutableArray *holder = [NSMutableArray new]; while (!finish) { if (pthread_mutex_trylock(&_lock) == 0) { if (_lru->_totalCost > costLimit) { _YYLinkedMapNode *node = [_lru removeTailNode]; if (node) [holder addObject:node]; } else { finish = YES; } pthread_mutex_unlock(&_lock); } else { usleep(10 * 1000); //10 ms } } //释放 if (holder.count) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [holder count]; // release in queue }); }}我们以这个按照占内存大小的限制来修剪内存为例子来看一下具体的实现方法。开始先做几个判断,看是否超过限制需要修剪,不需要修剪就直接return。修剪的过程就跟上面说到的是一样的,判断是否超过内存限制,超过就删掉尾部的结点,如此循环操作,只要符合限制要求。异步释放资源:我们可以看到在removeNode的时候,会使用一个holder数组来接收被移除的这些node,然后最后释放这些结点。为什么要这样做?其实这就是通过异步释放这些资源来减少主线程资源的开销。这里作者在异步中调用了[holder count]; 其实最开始我也不知道这个是什么意思,但是作者标注了release in queue,我猜测是通过调用你这个holder的随便一个方法,让这个异步的线程来管理这个holder,进而通过此异步线程来实现holder中对象的释放。锁的使用:还有一个比较重要的点,为什么使用pthread_mutex_trylock这个方式加锁,然后在失败之后,线程要sleep。这个问题就需要我们去研究一下各种锁了。很惭愧我对锁的了解不是很深刻,但是通过看了大神的博客,有了一些了解。(下面内容引用自大神的博客,文末有地址)作者都是使用的pthread_mutex_t互斥锁,这个锁有一个特性,在多个线程竞争一个资源的时候,除了竞争成功的线程,其他的线程都会被动挂起状态,当竞争成功的线程解锁是,会去主动将挂起的其他线程激活,这个过程包含了上下文切换,CPU抢占,信号发送等开销,很明显,开销有些大。所以作者使用了pthread_mutex_trylock()尝试解锁,若解锁失败该方法会立即返回,让当前线程不会进入被动的挂起状态(也可以说阻塞),在下一次循环时又继续尝试获取锁。这个过程很有意思,感觉是手动实现了一个自旋锁。而自旋锁有个需要注意的问题是:死循环等待的时间越长,对 cpu 的消耗越大。所以作者做了一个很短的睡眠 usleep(10 * 1000),有效的减小了循环的调用次数,至于这个睡眠时间的长度为什么是 10ms, 作者应该做了测试。其他部分其他部分就不一一细说了,作者整体思路很清晰,然后代码逻辑也很好懂,像上面提到的一些细节的处理可见作者的技术水平了。参考https://www.jianshu.com/p/408… ...

February 28, 2019 · 2 min · jiezi

YYCache 源码学习(二):YYDiskCache

整体思路从作者的《YYCache 设计思路》一文中可以看出,作者在设计YYDiskCache之前做了充分的测试:iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。YYDiskCache的磁盘缓存结合使用了文件储存和数据库储存。个人理解:在进行磁盘缓存的时候,会判断要储存数据的大小,如果数据小于20K,则直接存入数据库(数据储存到inline_data字段,此时filename为空)。如果数据大于20K,先把数据以文件形式进行存储,然后再在数据库中储存对应的文件名(此时inline_data为NULL,filename为文件地址),具体的可以结合下文中提到的磁盘缓存的文件结构来看。磁盘缓存的核心类是YYKVStorage,他主要封装了文件储存操作和SQLite数据库的操作。YYDiskCache是对YYKVStorage的封装,抛出的API和内存缓存相似,都有数据读写和修剪内存。磁盘缓存的文件结构/* File: /path/ /manifest.sqlite /manifest.sqlite-shm /manifest.sqlite-wal /data/ /e10adc3949ba59abbe56e057f20f883e /e10adc3949ba59abbe56e057f20f883e /trash/ /unused_file_or_folder SQL: create table if not exists manifest ( key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key) ); create index if not exists last_access_time_idx on manifest(last_access_time); */这个结构我们不需要多说什么,只提一个小点,作者在path路径下面设计了一个/data/和一个/trash/。删除文件是一个比较耗时的操作,在删除文件的时候,先进行文件的移动,然后在一个子线程中处理要删掉的文件,提高了整体的效率。实现 LRU磁盘缓存对缓存淘汰算法的实现就比较简单了,因为每次存储都有对应的数据库记录,而且表中设计了last_access_time这个字段,我们可以直接使用数据库的排序语句就可以找到最不常用的文件了。代码分析1.- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql { if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL; sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql)); if (!stmt) { int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", FUNCTION, LINE, result, sqlite3_errmsg(_db)); return NULL; } CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt); } else { sqlite3_reset(stmt); } return stmt;}这个方法是提前生成了sql语句的句柄,可以理解成提前把sql语句编译成字节码留给后面的执行函数(当前不执行)。同时,作者使用_dbStmtCache对语句进行缓存,下次使用时可以更快度的加载出来。2.- (BOOL)_dbClose { if (!_db) return YES; int result = 0; BOOL retry = NO; BOOL stmtFinalized = NO; if (_dbStmtCache) CFRelease(_dbStmtCache); _dbStmtCache = NULL; do { retry = NO; result = sqlite3_close(_db); // 状态为busy或者lock if (result == SQLITE_BUSY || result == SQLITE_LOCKED) { if (!stmtFinalized) { stmtFinalized = YES; sqlite3_stmt *stmt; //sqlite3_stmt *sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt); //表示从数据库pDb中对应的pStmt语句开始一个个往下找出相应prepared语句,如果pStmt为nil,那么就从pDb的第一个prepared语句开始。 while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) { //释放数据库中的prepared语句资源 sqlite3_finalize(stmt); retry = YES; } } } else if (result != SQLITE_OK) { if (_errorLogsEnabled) { NSLog(@"%s line:%d sqlite close failed (%d).", FUNCTION, LINE, result); } } } while (retry); _db = NULL; return YES;}这个是关闭数据库的方法,_dbStmtCache中缓存了我们使用的句柄,所以首先要释放掉了_dbStmtCache。在真正关闭数据库的代码中使用了do-while循环,因为一次访问数据库并不一定成功,数据库可能是busy或者lock的状态,所以要使用一个循环来多次访问。如果为能关闭数据库,作者使用了sqlite3_next_stmt一个个的找出prepared语句,并使用sqlite3_finalize释放了prepared资源(防止内存泄露)。其他的就没什么好说的了,主要就是一些sql语句的用法,这些大家看一下,碰到陌生的api谷歌一下就有了 ~ 具体的文件的操作,比较常用,看起来就容易很多。 ...

February 28, 2019 · 2 min · jiezi

react源码浅析(三):ReactElement

react相关库源码浅析react ts3 项目总览:你将会明白:react元素的key和ref为什么不会存在props上,并且传递,开发环境下与生产环境下处理key和ref的区别?…内部方法│ ├── hasValidRef —————————– 检测获取config上的ref是否合法│ ├── hasValidKey —————————– 检测获取config上的key是否合法│ ├── defineKeyPropWarningGetter —– 锁定props.key的值使得无法获取props.key│ ├── defineRefPropWarningGetter —– 锁定props.ref的值使得无法获取props.ref│ ├── ReactElement ———— 被createElement函数调用,根据环境设置对应的属性向外暴露的函数│ ├── createElement —————————- 生成react元素,对其props改造│ ├── createFactory ————————————– react元素工厂函数│ ├── cloneAndReplaceKey —————————- 克隆react元素,替换key│ ├── cloneElement —————————– 克隆react元素,对其props改造│ ├── isValidElement ———————————判断元素是否是react元素 hasValidRef通过Ref属性的取值器对象的isReactWarning属性检测是否含有合法的Ref,在开发环境下,如果这个props是react元素的props那么获取上面的ref就是不合法的,因为在creatElement的时候已经调用了defineRefPropWarningGetter。生产环境下如果config.ref !== undefined,说明合法。function hasValidRef(config) { //在开发模式下 if (DEV) { //config调用Object.prototype.hasOwnProperty方法查看其对象自身是否含有’ref’属性 if (hasOwnProperty.call(config, ‘ref’)) { //获取‘ref’属性的描述对象的取值器 const getter = Object.getOwnPropertyDescriptor(config, ‘ref’).get; //如果取值器存在,并且取值器上的isReactWarning为true,就说明有错误,返回false,ref不合法 if (getter && getter.isReactWarning) { return false; } } } //在生产环境下如果config.ref !== undefined,说明合法; return config.ref !== undefined;}hasValidKey通过key属性的取值器对象的isReactWarning属性检测是否含有合法的key,也就是如果这个props是react元素的props那么上面的key就是不合法的,因为在creatElement的时候已经调用了defineKeyPropWarningGetter。逻辑与上同function hasValidKey(config) { if (DEV) { if (hasOwnProperty.call(config, ‘key’)) { const getter = Object.getOwnPropertyDescriptor(config, ‘key’).get; if (getter && getter.isReactWarning) { return false; } } } return config.key !== undefined;}defineKeyPropWarningGetter开发模式下,该函数在creatElement函数中可能被调用。锁定props.key的值使得无法获取props.key,标记获取props中的key值是不合法的,当使用props.key的时候,会执行warnAboutAccessingKey函数,进行报错,从而获取不到key属性的值。即如下调用始终返回undefined:props.key给props对象定义key属性,以及key属性的取值器为warnAboutAccessingKey对象该对象上存在一个isReactWarning为true的标志,在hasValidKey上就是通过isReactWarning来判断获取key是否合法specialPropKeyWarningShown用于标记key不合法的错误信息是否已经显示,初始值为undefined。function defineKeyPropWarningGetter(props, displayName) { const warnAboutAccessingKey = function() { if (!specialPropKeyWarningShown) { specialPropKeyWarningShown = true; warningWithoutStack( false, ‘%s: key is not a prop. Trying to access it will result ’ + ‘in undefined being returned. If you need to access the same ’ + ‘value within the child component, you should pass it as a different ’ + ‘prop. (https://fb.me/react-special-props)', displayName, ); } }; warnAboutAccessingKey.isReactWarning = true; Object.defineProperty(props, ‘key’, { get: warnAboutAccessingKey, configurable: true, });}defineRefPropWarningGetter逻辑与defineKeyPropWarningGetter一致,锁定props.ref的值使得无法获取props.ref,标记获取props中的ref值是不合法的,当使用props.ref的时候,会执行warnAboutAccessingKey函数,进行报错,从而获取不到ref属性的值。即如下调用始终返回undefined:props.refReactElement被createElement函数调用,根据环境设置对应的属性。代码性能优化:为提高测试环境下,element比较速度,将element的一些属性配置为不可数,for…in还是Object.keys都无法获取这些属性,提高了速度。开发环境比生产环境多了_store,_self,_source属性,并且props以及element被冻结,无法修改配置。const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type: type, key: key, ref: ref, props: props, // Record the component responsible for creating this element. _owner: owner, }; if (DEV) { element._store = {}; // To make comparing ReactElements easier for testing purposes, we make // the validation flag non-enumerable (where possible, which should // include every environment we run tests in), so the test framework // ignores it. Object.defineProperty(element._store, ‘validated’, { configurable: false, enumerable: false, writable: true, value: false, }); // self and source are DEV only properties. Object.defineProperty(element, ‘_self’, { configurable: false, enumerable: false, writable: false, value: self, }); // Two elements created in two different places should be considered // equal for testing purposes and therefore we hide it from enumeration. Object.defineProperty(element, ‘_source’, { configurable: false, enumerable: false, writable: false, value: source, }); if (Object.freeze) { Object.freeze(element.props); Object.freeze(element); } } return element;};createElement在开发模式和生产模式下,第二参数props中的ref与key属性不会传入新react元素的props上,所以开发模式和生产模式都无法通过props传递ref与key。生产模式下ref与key不为undefined就赋值给新react元素对应的ref与key属性上,开发模式下获取ref与key是合法的(第二参数不是某个react元素的props,其key与ref则为合法),则赋值给新react元素对应的ref与key属性上。使用 JSX 编写的代码将被转成使用 React.createElement() React.createElement API:React.createElement( type, [props], […children])type(类型) 参数:可以是一个标签名字字符串(例如 ‘div’ 或’span’),或者是一个 React 组件 类型(一个类或者是函数),或者一个 React fragment 类型。仅在开发模式下获取props中的ref与key会抛出错误props:将key,ref,__self,__source的属性分别复制到新react元素的key,ref,__self,__source上,其他的属性值,assign到type上的props上。当这个props是react元素的props,那么其ref与key是无法传入新元素上的ref与key。只有这个props是一个新对象的时候才是有效的。这里就切断了ref与key通过props的传递。children:当children存在的时候,createElement返回的组件的props中不会存在children,如果存在的时候,返回的组件的props.children会被传入的children覆盖掉。参数中的children覆盖顺序如下://创建Footerclass Footer extends React.Component{ constructor(props){ super(props) } render(){ return ( <div> this is Footer {this.props.children} </div> ) }}//创建FooterEnhanceconst FooterEnhance = React.createElement(Footer, null ,“0000000”);//使用Footer与FooterEnhance<div> <Footer>aaaaa</Footer> {FooterEnhance}</div>结果:this is Footer aaaaathis is Footer 0000000可以看到:第三个参数children覆盖掉原来的children:aaaaa由下面源码也可知道:第三个参数children也可以覆盖第二参数中的children,测试很简单。第二个参数props中的children会覆盖掉原来组件中的props.children返回值的使用:如{FooterEnhance}。不能当做普通组件使用。源码const RESERVED_PROPS = { key: true, ref: true, __self: true, __source: true,};export function createElement(type, config, children) { let propName; // Reserved names are extracted const props = {}; let key = null; let ref = null; let self = null; let source = null; //将config上有但是RESERVED_PROPS上没有的属性,添加到props上 //将config上合法的ref与key保存到内部变量ref和key if (config != null) { //判断config是否具有合法的ref与key,有就保存到内部变量ref和key中 if (hasValidRef(config)) { ref = config.ref; } if (hasValidKey(config)) { key = ’’ + config.key; } //保存self和source self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object //将config上的属性值保存到props的propName属性上 for (propName in config) { if ( hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; } } } // Children can be more than one argument, and those are transferred onto // the newly allocated props object. // 如果只有三个参数,将第三个参数直接覆盖到props.children上 // 如果不止三个参数,将后面的参数组成一个数组,覆盖到props.children上 const childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { const childArray = Array(childrenLength); for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (DEV) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props // 如果有默认的props值,那么将props上为undefined的属性设置初始值 if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } //开发环境下 if (DEV) { // 需要利用defineKeyPropWarningGetter与defineRefPropWarningGetter标记新组件上的props也就是这里的props上的ref与key在获取其值得时候是不合法的。 if (key || ref) { //type如果是个函数说明不是原生的dom标签,可能是一个组件,那么可以取 const displayName = typeof type === ‘function’ ? type.displayName || type.name || ‘Unknown’ : type; if (key) { //在开发环境下标记获取新组件的props.key是不合法的,获取不到值 defineKeyPropWarningGetter(props, displayName); } if (ref) { //在开发环境下标记获取新组件的props.ref是不合法的,获取不到值 defineRefPropWarningGetter(props, displayName); } } } //注意生产环境下的ref和key还是被赋值到组件上 return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );}createFactory返回一个函数,该函数生成给定类型的 React 元素。用于将在字符串或者函数或者类转换成一个react元素,该元素的type为字符串或者函数或者类的构造函数例如:Footer为文章的类组件console.log(React.createFactory(‘div’)())console.log(React.createFactory(Footer)())返回的结果分别为:$$typeof:Symbol(react.element)key:nullprops:{}ref:nulltype:“div”_owner:null_store:{validated: false}_self:null_source:null$$typeof:Symbol(react.element)key:nullprops:{}ref:nulltype:ƒ Footer(props)_owner:null_store:{validated: false}_self:null_source:null源码:export function createFactory(type) { const factory = createElement.bind(null, type); factory.type = type; return factory;}cloneAndReplaceKey克隆一个旧的react元素,得到的新的react元素被设置了新的keyexport function cloneAndReplaceKey(oldElement, newKey) { const newElement = ReactElement( oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props, ); return newElement;} isValidElement判断一个对象是否是合法的react元素,即判断其$$typeof属性是否为REACT_ELEMENT_TYPEexport function isValidElement(object) { return ( typeof object === ‘object’ && object !== null && object.$$typeof === REACT_ELEMENT_TYPE );} cloneElementcloneElement官方API介绍 React.cloneElement( element, [props], […children])使用 element 作为起点,克隆并返回一个新的 React 元素。 所产生的元素的props由原始元素的 props被新的 props 浅层合并而来,并且最终合并后的props的属性为undefined,就用element.type.defaultProps也就是默认props值进行设置。如果props不是react元素的props,呢么props中的key 和 ref 将被存放在返回的新元素的key与ref上。返回的元素相当于:<element.type {…element.props} {…props}>{children}</element.type>其源码与createElement类似,不同的地方是在开发环境下cloneElement不会对props调用defineKeyPropWarningGetter与defineRefPropWarningGetter对props.ref与props.key进行获取拦截。总结react元素的key和ref为什么不会在props上,并且传递,开发环境下与生产环境下处理key和ref的区别?creatElement函数中阻止ref、key等属性赋值给props,所以react元素的key和ref不会在props上,并且在组件间通过props传递for (propName in config) { if ( hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; }}开发环境下与生产环境下处理key和ref的区别:开发环境下还会调用defineRefPropWarningGetter与defineKeyPropWarningGetter,利用Object.defineProperty进行拦截报错: Object.defineProperty(props, ‘key’, { get: warnAboutAccessingKey, configurable: true, });不能将一个react元素的ref通过props传递给其他组件。 ...

February 28, 2019 · 4 min · jiezi

LinkedList源码分析

一、属性及获取属性:1、sizetransient int size = 0;/** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) /transient Node<E> first;/* * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) /transient Node<E> last;获取public int size() { return size;}二、构造函数//Constructs an empty listpublic LinkedList() {}public LinkedList(Collection<? extends E> c) { this(); addAll(c);}三、类private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }}四、方法1、NodeNode<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i–) x = x.prev; return x; }}、linkFirst/** * Links e as first element. /private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++;}void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++;}、add、addFirstpublic void addFirst(E e) { linkFirst(e);}public boolean add(E e) { linkLast(e); return true;}linkLast2、setpublic E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal;}3、getpublic E get(int index) { checkElementIndex(index); return node(index).item;}、clearpublic void clear() { // Clearing all of the links between nodes is “unnecessary”, but: // - helps a generational GC if the discarded nodes inhabit // more than one generation // - is sure to free memory even if there is a reachable Iterator for (Node<E> x = first; x != null; ) { Node<E> next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; size = 0; modCount++;}、Push Poppublic void push(E e) { addFirst(e);}public E pop() { return removeFirst();}*、node(int index)Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i–) x = x.prev; return x; }} ...

February 25, 2019 · 2 min · jiezi

thinkphp源码分析(五)—配置篇

源码分析—入口篇源码分析全局配置加载类全局配置类的主要代码如下:class Config{ /** * @var array 配置参数 / private static $config = []; /* * @var string 参数作用域 / private static $range = ‘sys’; /* * 设定配置参数的作用域 * @access public * @param string $range 作用域 * @return void / public static function range($range) { …. } /* * 解析配置文件或内容 * @access public * @param string $config 配置文件路径或内容 * @param string $type 配置解析类型 * @param string $name 配置名(如设置即表示二级配置) * @param string $range 作用域 * @return mixed / public static function parse($config, $type = ‘’, $name = ‘’, $range = ‘’) { …. } /* * 加载配置文件(PHP格式) * @access public * @param string $file 配置文件名 * @param string $name 配置名(如设置即表示二级配置) * @param string $range 作用域 * @return mixed / public static function load($file, $name = ‘’, $range = ‘’) { …. } /* * 检测配置是否存在 * @access public * @param string $name 配置参数名(支持二级配置 . 号分割) * @param string $range 作用域 * @return bool / public static function has($name, $range = ‘’) { …. } /* * 获取配置参数 为空则获取所有配置 * @access public * @param string $name 配置参数名(支持二级配置 . 号分割) * @param string $range 作用域 * @return mixed / public static function get($name = null, $range = ‘’) { …. } /* * 设置配置参数 name 为数组则为批量设置 * @access public * @param string|array $name 配置参数名(支持二级配置 . 号分割) * @param mixed $value 配置值 * @param string $range 作用域 * @return mixed / public static function set($name, $value = null, $range = ‘’) { …. } /* * 重置配置参数 * @access public * @param string $range 作用域 * @return void / public static function reset($range = ‘’) { …. }}添加配置添加配置用的是thinkConfig::set($name, $value = null, $range = ‘’)方法;当$name是字符串时候value是要设置的值,$name为数组时候,批量设置配置。 /* * 设置配置参数 name 为数组则为批量设置 * @access public * @param string|array $name 配置参数名(支持二级配置 . 号分割) * @param mixed $value 配置值 * @param string $range 作用域 * @return mixed / public static function set($name, $value = null, $range = ‘’) { $range = $range ?: self::$range; if (!isset(self::$config[$range])) self::$config[$range] = []; // 字符串则表示单个配置设置 if (is_string($name)) { if (!strpos($name, ‘.’)) { self::$config[$range][strtolower($name)] = $value; } else { // 二维数组 $name = explode(’.’, $name, 2); self::$config[$range][strtolower($name[0])][$name[1]] = $value; } return $value; } // 数组则表示批量设置 if (is_array($name)) { if (!empty($value)) { self::$config[$range][$value] = isset(self::$config[$range][$value]) ? array_merge(self::$config[$range][$value], $name) : $name; return self::$config[$range][$value]; } return self::$config[$range] = array_merge( self::$config[$range], array_change_key_case($name) ); } // 为空直接返回已有配置 return self::$config[$range]; }设置配置时候主要分了两种情况:1. $name是字符串2. $name是二维数组(目前只支持二维数组)配置会先判断配置的作用域,不设置就用默认的_sys_作用域,并且判断该作用域是否存在,不存在就初始化为数组。对于$name这两种不同形式的参数,处理方式也不一样,$name为字符串形式 // 字符串则表示单个配置设置 if (is_string($name)) { if (!strpos($name, ‘.’)) { self::$config[$range][strtolower($name)] = $value; } else { // 二维数组 $name = explode(’.’, $name, 2); self::$config[$range][strtolower($name[0])][$name[1]] = $value; } return $value; }判断字符串中是否带., 没有直接把$name的小写形式作为key,$value作为值设置到配置(self::$config)中.如果带.,只处理前面两项,即把字符串通过.分割成数组,取数组的前面两项,把$value设置到配置中。$name为数组形式 // 数组则表示批量设置 if (is_array($name)) { if (!empty($value)) { self::$config[$range][$value] = isset(self::$config[$range][$value]) ? array_merge(self::$config[$range][$value], $name) : $name; return self::$config[$range][$value]; } return self::$config[$range] = array_merge( self::$config[$range], array_change_key_case($name) ); }如果设置了$value的值,那么把$value作为配置的键,再把$name的配置设置到配置中(如果原来已经有值,数组合并用传入的值替换原来的值,如果原来没有值,直接赋值),如果没有设置$value的值,那么把数组的每一项设置到该作用域下。备注: array_change_key_case( $array, [ int $case = CASE_LOWER ] ) : array 把数组的键设置为大写或小写,默认是小写。获取配置看完了上面的分析,对于获取配置应该也有了一个大致的思路了,就是设置配置的反向。 /* * 获取配置参数 为空则获取所有配置 * @access public * @param string $name 配置参数名(支持二级配置 . 号分割) * @param string $range 作用域 * @return mixed / public static function get($name = null, $range = ‘’) { $range = $range ?: self::$range; // 无参数时获取所有 if (empty($name) && isset(self::$config[$range])) { return self::$config[$range]; } // 非二级配置时直接返回 if (!strpos($name, ‘.’)) { $name = strtolower($name); return isset(self::$config[$range][$name]) ? self::$config[$range][$name] : null; } // 二维数组设置和获取支持 $name = explode(’.’, $name, 2); $name[0] = strtolower($name[0]); if (!isset(self::$config[$range][$name[0]])) { // 动态载入额外配置 $module = Request::instance()->module(); $file = CONF_PATH . ($module ? $module . DS : ‘’) . ’extra’ . DS . $name[0] . CONF_EXT; is_file($file) && self::load($file, $name[0]); } return isset(self::$config[$range][$name[0]][$name[1]]) ? self::$config[$range][$name[0]][$name[1]] : null; }看了代码,应该对于无参获取和非二级获取已经懂了,那二维数组有个需要注意的地方,就是会动态加载额外的配置。$module = Request::instance()->module();该方法的实现如下: /* * 设置或者获取当前的模块名 * @access public * @param string $module 模块名 * @return string|Request / public function module($module = null) { if (!is_null($module)) { $this->module = $module; return $this; } else { return $this->module ?: ‘’; } }该方法就是获取当前请求的模块。 //二维数组处理逻辑 if (!isset(self::$config[$range][$name[0]])) { // 动态载入额外配置 $module = Request::instance()->module(); $file = CONF_PATH . ($module ? $module . DS : ‘’) . ’extra’ . DS . $name[0] . CONF_EXT; is_file($file) && self::load($file, $name[0]); } return isset(self::$config[$range][$name[0]][$name[1]]) ? self::$config[$range][$name[0]][$name[1]] : null;从代码中可以看出,通过request获取到当前访问的模块,判断当前模块中的或者配置目录中的extra目录总是否存在以为数组中键为名字的配置文件,存在就加载进来,再进行返回,动态加载通过thinkConfig::load($file)来进行加载。 /* * 加载配置文件(PHP格式) * @access public * @param string $file 配置文件名 * @param string $name 配置名(如设置即表示二级配置) * @param string $range 作用域 * @return mixed / public static function load($file, $name = ‘’, $range = ‘’) { $range = $range ?: self::$range; if (!isset(self::$config[$range])) self::$config[$range] = []; if (is_file($file)) { $name = strtolower($name); $type = pathinfo($file, PATHINFO_EXTENSION); if (‘php’ == $type) { return self::set(include $file, $name, $range); } if (‘yaml’ == $type && function_exists(‘yaml_parse_file’)) { return self::set(yaml_parse_file($file), $name, $range); } return self::parse($file, $type, $name, $range); } return self::$config[$range]; }该加载配置的方法主要的逻辑是处理php,yaml,ini,json,xml格式的配置。php类型的是直接include再set配置即可,yaml则是通过yaml_parse_file方法解析成数据再set配置。其他的通过固定的驱动来解析,业务逻辑再thinkConfig::parse()方法中。 /* * 解析配置文件或内容 * @access public * @param string $config 配置文件路径或内容 * @param string $type 配置解析类型 * @param string $name 配置名(如设置即表示二级配置) * @param string $range 作用域 * @return mixed */ public static function parse($config, $type = ‘’, $name = ‘’, $range = ‘’) { $range = $range ?: self::$range; if (empty($type)) $type = pathinfo($config, PATHINFO_EXTENSION); $class = false !== strpos($type, ‘\’) ? $type : ‘\think\config\driver\’ . ucwords($type); return self::set((new $class())->parse($config), $name, $range); }通过pathinfo()方法获取到路径信息, 第二个参数设置返回扩展名,判断扩展名中是否带有\如果有即传入的是一个类。直接通过类的parse方法解析配置,如果是一个文件扩展名称,即通过\think\config\driver\下对应的驱动来解析配置,再set到配置中。总结thinkphp中主要的配置加载方式有两种,1.加载框架内部预设的配置2.动态加载用户配置对于第一中方式,由于默认的配置是php类型的,是直接通过set方法执行配置的,第二中方式是通过load方法,判断文件的扩展名来进行不同的驱动解析,其中php和yaml有直接的方式可以解析成数组,xml,json,ini则是通过对应的驱动来解析再set配置的,通过调用parse方法自动判断扩展,再进行解析。至于Config类中其他的方法比较简单,可以直接查看代码获取相关信息。 ...

February 23, 2019 · 4 min · jiezi

源码分析(四)—错误及异常处理篇

源码分析错误及异常处理机制错误及异常处理机制文件是/thinkphp/library/think/Error.php,在框架引导文件的的基础文件base.php中注册(不知道的可以去看《《源码分析(二)—入口篇》》),通过thinkError::register()进行的注册。 /** * 注册异常处理 * @access public * @return void / public static function register() { error_reporting(E_ALL); set_error_handler([CLASS, ‘appError’]); set_exception_handler([CLASS, ‘appException’]); register_shutdown_function([CLASS, ‘appShutdown’]); }该方法做了四件事情:设置报错级别 E_ALL为E_STRICT所有报错。设置错误处理函数,set_error_handler([CLASS, ‘appError’])设置异常处理函数,set_exception_handler([CLASS, ‘appException’]);设置程序异常终止处理函数,register_shutdown_function([CLASS, ‘appShutdown’]);PHP报错级别php的报错级别有:E_STRICT,E_ALL, E_USER_WARNING等,具体可查看[php预定义常量](http://php.net/manual/zh/erro…。错误处理函数thinkphp中注册了thinkError::appError()方法对错误进行处理。 /* * 错误处理 * @access public * @param integer $errno 错误编号 * @param integer $errstr 详细错误信息 * @param string $errfile 出错的文件 * @param integer $errline 出错行号 * @return void * @throws ErrorException / public static function appError($errno, $errstr, $errfile = ‘’, $errline = 0) { $exception = new ErrorException($errno, $errstr, $errfile, $errline); // 符合异常处理的则将错误信息托管至 think\exception\ErrorException if (error_reporting() & $errno) { throw $exception; } self::getExceptionHandler()->report($exception); }在appError方法中,把符合异常处理的则将错误信息托管至系统的ErrorException,其他的异常通过thinkexceptionHandle进行处理。//think\exception\ErrorException文件/* * ThinkPHP错误异常 * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误 * 除开从 think\Exception 继承的功能 * 其他和PHP系统\ErrorException功能基本一样 /class ErrorException extends Exception{ /* * 用于保存错误级别 * @var integer / protected $severity; /* * 错误异常构造函数 * @param integer $severity 错误级别 * @param string $message 错误详细信息 * @param string $file 出错文件路径 * @param integer $line 出错行号 * @param array $context 错误上下文,会包含错误触发处作用域内所有变量的数组 / public function __construct($severity, $message, $file, $line, array $context = []) { $this->severity = $severity; $this->message = $message; $this->file = $file; $this->line = $line; $this->code = 0; empty($context) || $this->setData(‘Error Context’, $context); } /* * 获取错误级别 * @return integer 错误级别 / final public function getSeverity() { return $this->severity; }}errorException设置错误级别,错误信息,出错文件路径,行号,上下文。对exception进行处理的是thinkexceptionHandle的report()方法:self::getExceptionHandler()->report($exception); //self::getExceptionHandler() /* * 获取异常处理的实例 * @access public * @return Handle / public static function getExceptionHandler() { static $handle; if (!$handle) { // 异常处理 handle $class = Config::get(’exception_handle’); if ($class && is_string($class) && class_exists($class) && is_subclass_of($class, “\think\exception\Handle”) ) { $handle = new $class; } else { $handle = new Handle; if ($class instanceof \Closure) { $handle->setRender($class); } } } return $handle; }这里有一个关键的地方是:static $handle; 声明该变量是静态变量时候,当赋值给该变量后,函数调用结束后不会销毁,直到脚本结束才会销毁。这个逻辑就是判断$handle是否已经赋值,没有赋值,获取默认配置文件是否设置处理handle,如果设置,这个handle必须是\think\exception\Handle的子类(is_subclass_of($class, “\think\exception\Handle”)),如果没有设置,那么用默认的thinkexceptionHandle调用report方法进行处理, 记录到日志文件中。 /* * Report or log an exception. * * @param \Exception $exception * @return void / public function report(Exception $exception) { if (!$this->isIgnoreReport($exception)) { // 收集异常数据 if (App::$debug) { $data = [ ‘file’ => $exception->getFile(), ’line’ => $exception->getLine(), ‘message’ => $this->getMessage($exception), ‘code’ => $this->getCode($exception), ]; $log = “[{$data[‘code’]}]{$data[‘message’]}[{$data[‘file’]}:{$data[’line’]}]”; } else { $data = [ ‘code’ => $this->getCode($exception), ‘message’ => $this->getMessage($exception), ]; $log = “[{$data[‘code’]}]{$data[‘message’]}”; } if (Config::get(‘record_trace’)) { $log .= “\r\n” . $exception->getTraceAsString(); } Log::record($log, ’error’); } }把errorException的数据组装成对应的字符串,写入日志。异常处理函数thinkphp中注册了thinkError::appException()方法对错误进行处理。 /* * 异常处理 * @access public * @param \Exception|\Throwable $e 异常 * @return void / public static function appException($e) { if (!$e instanceof \Exception) { $e = new ThrowableError($e); } $handler = self::getExceptionHandler(); $handler->report($e); if (IS_CLI) { $handler->renderForConsole(new ConsoleOutput, $e); } else { $handler->render($e)->send(); } }方法和appError处理差不多,基本都是通过获取ExceptionHandle再调用handle的report方法,但是多了一步把异常呈现,如果是命令行写到命令行输出,如果是web的就把错误信息通过reponse响应返回客户端。异常中止时执行的函数thinkphp中注册了thinkError::appShutdown()方法对错误进行处理。 /* * 异常中止处理 * @access public * @return void */ public static function appShutdown() { // 将错误信息托管至 think\ErrorException if (!is_null($error = error_get_last()) && self::isFatal($error[’type’])) { self::appException(new ErrorException( $error[’type’], $error[‘message’], $error[‘file’], $error[’line’] )); } // 写入日志 Log::save(); }通过error_get_last()获取最后抛出的错误,把信息托管至thinkErrorException,在通过异常处理函数进行记录信息。最后写入日志。总结整体整个错误处理机制都是通过获取ExceptionHandle再调用handle的report方法,但是多了一步把异常呈现,如果是命令行写到命令行输出,如果是web的就把错误信息通过reponse响应返回客户端。默认的处理handle是thinkexceptionHandle,当然也可以自定义handle,但是必须是thinkexceptionHandle的子类, 通过self::getExceptionHandler的is_subclass_of($class, “\think\exception\Handle”)可以知。 ...

February 22, 2019 · 2 min · jiezi

源码分析(三)—自动加载篇(Loader的分析)

源码分析自动加载系统会调用 Loader::register()方法注册自动加载,在这一步完成后,所有符合规范的类库(包括Composer依赖加载的第三方类库)都将自动加载。系统的自动加载由下面主要部分组成:1. 注册系统的自动加载方法 \think\Loader::autoload2. 注册系统命名空间定义3. 加载类库映射文件(如果存在)4. 如果存在Composer安装,则注册Composer自动加载5. 注册extend扩展目录一个类库的自动加载检测顺序为:1. 是否定义类库映射;2. PSR-4自动加载检测;3. PSR-0自动加载检测;4. 可以看到,定义类库映射的方式是最高效的。源码 /** * 注册自动加载机制 * @access public * @param callable $autoload 自动加载处理方法 * @return void / public static function register($autoload = null) { // 注册系统自动加载 spl_autoload_register($autoload ?: ’think\Loader::autoload’, true, true); // Composer 自动加载支持 if (is_dir(VENDOR_PATH . ‘composer’)) { if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . ‘composer’ . DS . ‘autoload_static.php’)) { require VENDOR_PATH . ‘composer’ . DS . ‘autoload_static.php’; $declaredClass = get_declared_classes(); $composerClass = array_pop($declaredClass); foreach ([‘prefixLengthsPsr4’, ‘prefixDirsPsr4’, ‘fallbackDirsPsr4’, ‘prefixesPsr0’, ‘fallbackDirsPsr0’, ‘classMap’, ‘files’] as $attr) { if (property_exists($composerClass, $attr)) { self::${$attr} = $composerClass::${$attr}; } } } else { self::registerComposerLoader(); } } // 注册命名空间定义 self::addNamespace([ ’think’ => LIB_PATH . ’think’ . DS, ‘behavior’ => LIB_PATH . ‘behavior’ . DS, ’traits’ => LIB_PATH . ’traits’ . DS, ]); // 加载类库映射文件 if (is_file(RUNTIME_PATH . ‘classmap’ . EXT)) { self::addClassMap(__include_file(RUNTIME_PATH . ‘classmap’ . EXT)); } self::loadComposerAutoloadFiles(); // 自动加载 extend 目录 self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS); }框架自动加载 /* * 自动加载 * @access public * @param string $class 类名 * @return bool / public static function autoload($class) { // 检测命名空间别名 if (!empty(self::$namespaceAlias)) { $namespace = dirname($class); if (isset(self::$namespaceAlias[$namespace])) { $original = self::$namespaceAlias[$namespace] . ‘\’ . basename($class); if (class_exists($original)) { return class_alias($original, $class, false); } } } if ($file = self::findFile($class)) { // 非 Win 环境不严格区分大小写 if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) { __include_file($file); return true; } } return false; }检测命名空间别名检查是否添加了命名空间别名,通过别名寻找原命名空间。如://原\App\Http\Controller\Index::class//添加别名后\Controller\Index::classthinkphp通过 thinkLoader::addNamespaceAlias($namespace, $original) 添加命名空间别名。 //位置在thinkphp/library/think/Loader.php的260行 /* * 注册命名空间别名 * @access public * @param array|string $namespace 命名空间 * @param string $original 源文件 * @return void / public static function addNamespaceAlias($namespace, $original = ‘’) { if (is_array($namespace)) { self::$namespaceAlias = array_merge(self::$namespaceAlias, $namespace); } else { self::$namespaceAlias[$namespace] = $original; } } 通过键为别名,值为原命名空间的数组,注册到thinkLoader::$namespaceAlias的属性。通过classmap,psr-4,psr-0查找文件/* * 查找文件 * @access private * @param string $class 类名 * @return bool|string / private static function findFile($class) { // 类库映射 if (!empty(self::$classMap[$class])) { return self::$classMap[$class]; } // 查找 PSR-4 $logicalPathPsr4 = strtr($class, ‘\’, DS) . EXT; $first = $class[0]; if (isset(self::$prefixLengthsPsr4[$first])) { foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { if (0 === strpos($class, $prefix)) { foreach (self::$prefixDirsPsr4[$prefix] as $dir) { if (is_file($file = $dir . DS . substr($logicalPathPsr4, $length))) { return $file; } } } } } // 查找 PSR-4 fallback dirs foreach (self::$fallbackDirsPsr4 as $dir) { if (is_file($file = $dir . DS . $logicalPathPsr4)) { return $file; } } // 查找 PSR-0 if (false !== $pos = strrpos($class, ‘\’)) { // namespace class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), ‘’, DS); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, ‘’, DS) . EXT; } if (isset(self::$prefixesPsr0[$first])) { foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (is_file($file = $dir . DS . $logicalPathPsr0)) { return $file; } } } } } // 查找 PSR-0 fallback dirs foreach (self::$fallbackDirsPsr0 as $dir) { if (is_file($file = $dir . DS . $logicalPathPsr0)) { return $file; } } // 找不到则设置映射为 false 并返回 return self::$classMap[$class] = false; }thinkphp添加的自动加载是通过psr-4和classmap进行加载,方法分别是: thinkLoader::addClassMap($class, $map = ‘’) 和 thinkLoader::addNamespace($namespace, $path = ‘’)。psr-4的加载方式是通过命名空间的首字母,查找对应的命名空间,再通过对应的命名空间拼接对应的文件目录,判断该文件是否存在,如果存在就加载文件,实现类的自动加载。classmap的加载方式是通过composer du生成对应的类映射,通过直接查找class对应的文件,从而实现自动加载。其他的加载方式需要深入了解的可以自行研究代码。/* * 注册自动加载机制 * @access public * @param callable $autoload 自动加载处理方法 * @return void / public static function register($autoload = null) { …. // 注册命名空间定义 self::addNamespace([ ’think’ => LIB_PATH . ’think’ . DS, ‘behavior’ => LIB_PATH . ‘behavior’ . DS, ’traits’ => LIB_PATH . ’traits’ . DS, ]); // 加载类库映射文件 if (is_file(RUNTIME_PATH . ‘classmap’ . EXT)) { self::addClassMap(__include_file(RUNTIME_PATH . ‘classmap’ . EXT)); } …. }composer自动加载thinkphp中的composer自动加载不是通过composer自带的autoload.php进行自动加载的。是通过加载对应的psr文件进行注册加载的。 /* * 注册自动加载机制 * @access public * @param callable $autoload 自动加载处理方法 * @return void */ public static function register($autoload = null) { …. // Composer 自动加载支持 if (is_dir(VENDOR_PATH . ‘composer’)) { if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . ‘composer’ . DS . ‘autoload_static.php’)) { require VENDOR_PATH . ‘composer’ . DS . ‘autoload_static.php’; $declaredClass = get_declared_classes(); $composerClass = array_pop($declaredClass); foreach ([‘prefixLengthsPsr4’, ‘prefixDirsPsr4’, ‘fallbackDirsPsr4’, ‘prefixesPsr0’, ‘fallbackDirsPsr0’, ‘classMap’, ‘files’] as $attr) { if (property_exists($composerClass, $attr)) { self::${$attr} = $composerClass::${$attr}; } } } else { self::registerComposerLoader(); } } …. } ...

February 21, 2019 · 4 min · jiezi

源码分析(二)—入口篇

源码分析—入口篇源码分析应用入口用户发起的请求都会经过应用的入口文件,通常是 ==public/index.php==文件。当然,你也可以更改或者增加新的入口文件。通常入口文件的代码都比较简单,一个普通的入口文件代码如下:// 应用入口文件// 定义项目路径define(‘APP_PATH’, DIR . ‘/../application/’);// 加载框架引导文件require DIR . ‘/../thinkphp/start.php’;一般入口文件以定义一些常量为主,支持的常量请参考后续的内容或者附录部分。通常,我们不建议在应用入口文件中加入过多的代码,尤其是和业务逻辑相关的代码。加载引导文件// ThinkPHP 引导文件// 1. 加载基础文件require DIR . ‘/base.php’;// 2. 执行应用App::run()->send();加载基础文件// DIR . ‘/base.php’文件//定义常量define(‘THINK_VERSION’, ‘5.0.24’);….//常量太多省略了部分// 载入Loader类require CORE_PATH . ‘Loader.php’;// 加载环境变量配置文件if (is_file(ROOT_PATH . ‘.env’)) { $env = parse_ini_file(ROOT_PATH . ‘.env’, true); foreach ($env as $key => $val) { $name = ENV_PREFIX . strtoupper($key); if (is_array($val)) { foreach ($val as $k => $v) { $item = $name . ‘_’ . strtoupper($k); putenv("$item=$v"); } } else { putenv("$name=$val"); } }}// 注册自动加载\think\Loader::register();// 注册错误和异常处理机制\think\Error::register();// 加载惯例配置文件\think\Config::set(include THINK_PATH . ‘convention’ . EXT);该部分主要是定义一些系统常量,关键点是引入了自动加载类并且注册了自动加载,使得框架可以自动引入类文件,业务层只要直接use对应命名空间的类即可进行实例化,注册错误及异常处理机制, 加载默认配置等操作。最后就是应用启动,App::run()->send(); ...

February 21, 2019 · 1 min · jiezi

vue源码分析系列之入口文件分析

入口寻找入口platforms/web/entry-runtime-with-compiler中import了./runtime/index导出的vue。./runtime/index中引入了core/index中的vue.core/index中引入了instance/index中的vueinstance/index中,定义了vue的构造函数instance/index中的vuefunction Vue (options) { if (process.env.NODE_ENV !== ‘production’ && !(this instanceof Vue) ) { warn(‘Vue is a constructor and should be called with the new keyword’) } this._init(options)}// 原型上挂载了init方法,用来做初始化initMixin(Vue)// 原型上挂载$data的属性描述符getter,返回this._data// 原型上挂载$props的属性描述符getter, 返回this._props// 原型上挂载$set与$delete方法,用来为对象新增/删除响应式属性// 原型上挂载$watch方法stateMixin(Vue)// 原型上挂载事件相关的方法, $on、$once、$off、$emit。eventsMixin(Vue)// 原型上挂载_update、$destroy与$forceUpdate方法,与组件更新有关。lifecycleMixin(Vue)// 原型挂载组件渲染相关方法,_render方法(用来返回vnode,即虚拟dom)renderMixin(Vue)export default Vuecore/index中的vueindeximport Vue from ‘./instance/index’import { initGlobalAPI } from ‘./global-api/index’import { isServerRendering } from ‘core/util/env’// 初始化一些全局APIinitGlobalAPI(Vue)// 是否是服务端渲染Object.defineProperty(Vue.prototype, ‘$isServer’, { get: isServerRendering // global[‘process’].env.VUE_ENV === ‘server’})// ssr相关Object.defineProperty(Vue.prototype, ‘$ssrContext’, { get () { /* istanbul ignore next / return this.$vnode && this.$vnode.ssrContext }})Vue.version = ‘VERSION’export default VueinitGlobalAPI // 定义vue配置对象,配置对象详情见 import config from ‘../config’中的备注 const configDef = {} configDef.get = () => config Object.defineProperty(Vue, ‘config’, configDef) // 定义一些内部公用方法 Vue.util = { warn, // ⚠️警告打印相关 extend, // 浅拷贝函数 mergeOptions, // 配置合并,用到的时候细看 defineReactive // 定义响应式属性的方法。 } // 静态方法,同$set、$delete、$nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick Vue.options = Object.create(null) ASSET_TYPES.forEach(type => { Vue.options[type + ’s’] = Object.create(null) }) // Vue.options => {“components”:{},“directives”:{},“filters”:{}} // 跟Weex’s multi-instance scenarios多场景有关 Vue.options._base = Vue; //将内置组件塞进来 extend(Vue.options.components, builtInComponents) // 定义Vue.use,主要在应用在插件系统中 initUse(Vue) // 定义Vue.mixin, 就一句this.options = mergeOptions(this.options, mixin) initMixin(Vue) // 定义Vue.extend, 用作原型继承,通过它,可以创建子组件的构造函数 initExtend(Vue) // 扩展Vue.component,Vue.directive,Vue.filter方法 initAssetRegisters(Vue)runtime/index中的vueimport Vue from ‘core/index’import config from ‘core/config’import { extend, noop } from ‘shared/util’import { mountComponent } from ‘core/instance/lifecycle’import { devtools, inBrowser, isChrome } from ‘core/util/index’import { query, mustUseProp, isReservedTag, isReservedAttr, getTagNamespace, isUnknownElement} from ‘web/util/index’import { patch } from ‘./patch’import platformDirectives from ‘./directives/index’import platformComponents from ‘./components/index’// 一些标签检查类的方法。平台相关Vue.config.mustUseProp = mustUsePropVue.config.isReservedTag = isReservedTagVue.config.isReservedAttr = isReservedAttrVue.config.getTagNamespace = getTagNamespaceVue.config.isUnknownElement = isUnknownElement// 平台相关指令组件// 指令有model与show// 组件有Transition与TransitionGroupextend(Vue.options.directives, platformDirectives)extend(Vue.options.components, platformComponents)// install platform patch functionVue.prototype.patch = inBrowser ? patch : noop// public mount methodVue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating)}entry-runtime-with-compiler中的vueimport config from ‘core/config’import { warn, cached } from ‘core/util/index’import { mark, measure } from ‘core/util/perf’import Vue from ‘./runtime/index’import { query } from ‘./util/index’import { shouldDecodeNewlines } from ‘./util/compat’import { compileToFunctions } from ‘./compiler/index’// 根据id返回dom内容const idToTemplate = cached(id => { const el = query(id) return el && el.innerHTML})// 重写$mount方法const mount = Vue.prototype.$mountVue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean): Component { el = el && query(el) / 将vue绑定到body或者html元素上的错误提示 / if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== ‘production’ && warn( Do not mount Vue to &lt;html&gt; or &lt;body&gt; - mount to normal elements instead. ) return this } const options = this.$options // 解析template或者el属性,将其转化为render函数 if (!options.render) { let template = options.template // 获得模板字符串 if (template) { if (typeof template === ‘string’) { if (template.charAt(0) === ‘#’) { template = idToTemplate(template) } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== ‘production’) { warn(‘invalid template option:’ + template, this) } return this } } else if (el) { template = getOuterHTML(el) } // 获得模板字符串后,编译模板为render函数 if (template) { / istanbul ignore if / if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) { mark(‘compile’) } const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns / istanbul ignore if / if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) { mark(‘compile end’) measure(vue ${this._name} compile, ‘compile’, ‘compile end’) } } } return mount.call(this, el, hydrating)}/* * Get outerHTML of elements, taking care * of SVG elements in IE as well. */function getOuterHTML (el: Element): string { if (el.outerHTML) { return el.outerHTML } else { const container = document.createElement(‘div’) container.appendChild(el.cloneNode(true)) return container.innerHTML }}Vue.compile = compileToFunctionsexport default Vue ...

February 13, 2019 · 3 min · jiezi

EOS源码分析(3)案例分析

运行EOS节点智能合约是在EOS节点中运行的,因此要首先把EOS节点运行起来,在这里,我们运行一个本地节点,命令如下:cd path-to-eos/build/programs/eosiod/./eosd创建默认钱包创建钱包由于智能合约是与账号相关联的,因此在创建智能合约前,我们首先要创建账号,而账号的Key需要通过钱包来创建和保存,因此需要先创建钱包,如下:cd path-to-eos/build/programs/eosioc/./eosioc wallet create # Outputs a password that you need to save to be able to lock/unlock the wallet上面的命令会创建一个名称为default 的默认钱包,在创建钱包时,你也可以指定名称。钱包对应的密码需要保存,后续操作钱包状态的时候需要使用。导入测试账号的Private key钱包创建好之后,还不能直接创建账号,每个账号的创建需要一个Creator账号,在这里,我们使用genesis.json中的 inita账号作为Creator账号。作为Creator的账号,必须要把它的Private key导入到钱包中,否则在创建其他账号时,会提示权限不够,命令如下:./eosioc wallet import 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3创建账号创建账号所需的key每个账号的创建都需要owner_key 和active_key 两个key,我们通过以下命令创建key:cd path-to-eos/build/programs/eosioc/./eosioc create key # owner_key./eosioc create key # active_key以上命令会分别输出两组 private key 和 public key,如下:Private key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPublic key: EOSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX千万保存好以上的key,后续需要使用。创建账号创建账号的所有准备都已经齐备,现在可以开始创建账号了,账号创建的命令如下:./eosioc create account inita currency PUBLIC_KEY_1 PUBLIC_KEY_2在以上命令中,创建的当前账号名称为currency,创建当前账号的Creator 账号指定为 inita, 最后的两个public key 分别对应currency账号的 owner_key和active_key。当账号创建成功后,你将得到一个附带 transaction ID的 JSON反馈。如果你之前没有导入过inita账号的private key,则会出现如下错误提示:/Screen Shot 2018-02-21 at 10.36.38 PM.png这主要是因为以上这条命令必须要有inita的active权限才能执行,如果没有导入私钥,则权限不够,从而会给出以上错误提示。你也可以通过查询此账户的信息来确认账号是否创建成功:./eosioc get account currency以上命令将会返回如下结果:{“account_name”: “currency”,“eos_balance”: “0.0000 EOS”,“staked_balance”: “0.0001 EOS”,“unstaking_balance”: “0.0000 EOS”,“last_unstaking_time”: “2035-10-29T06:32:22”,…currency账号已经创建成功,但你目前还不能使用此账号进行智能合约等操作,因为系统中目前只有此账号的public key,而进行交易和智能合约等操作需要用到此账号的active权限,因此,我们需要把此账号对应的active private key导入到系统中,如下:./eosioc wallet import XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX请使用你之前创建的active private key 替代以上命令中的 xxx 字符串。部署智能合约准备工作在上传以前,首先确认currency账号下是没有智能合约的./eosioc get code currencycode hash: 0000000000000000000000000000000000000000000000000000000000000000部署智能合约上传-智能合约到currency 账号,此处的智能合约是EOS系统自带的测试合约./eosioc set contract currency ../../contracts/currency/currency.wast ../../contracts/currency/currency.abi当你得到一个带有transaction_id字段的JSON反馈后,说明智能合约已经上传成功。你还可以通过以下命令来验证合约是否上传成功。./eosioc get code currency以上命令应该会返回这样的结果:code hash: 9b9db1a7940503a88535517049e64467a6e8f4e9e03af15e9968ec89dd794975发行货币在使用currency智能合约以前,你首先要发行货币,以下命令将向currency中发行10,000,000 的货币./eosioc push action currency issue &#39;{“to”:“currency”,“quantity”:“1000.0000 CUR”}&#39; –permission currency@active通过以下命令可以验证 currency 账号中目前确实已经有 10,000,000 的余额了./eosioc get table currency currency account{“rows”: [{“currency”: 1381319428,“balance”: 10000000}],“more”: false}转账任何账号都可以在任何时间向任何智能合约发送消息,但如果权限不够的话,智能合约会拒绝消息的执行。更加准确的来说,消息并不一定是从某个账号发出来的,只要这条消息所关联的账号和权限等级是满足的,这条消息就能被执行。以下命令展示了如何向currency账号发送transfer消息,在这条命令中,需要从currency账号转账到其他账号,因此,需要currency账号权限才能完成此操作,具体命令如下:./eosioc push action currency transfer &#39;{“from”:“currency”,“to”:“inita”,“quantity”:“20.0000 CUR”,“memo”:“my first transfer”}&#39; –permission currency@active上面这条命令中,出现了三次currency, 第一次出现表示的是智能合约currency,第二次出现表示的转账账号currency,第三次出现则表示currency账号的权限; 如果我们换一个其他转出账号,则比较通用的命令格式应该如下:./eosioc push action currency transfer &#39;{“from”:"${usera}",“to”:"${userb}",“quantity”:“20.0000 CUR”,“memo”:""}&#39; –permission ${usera}@active以上命令是从usera 向userb转账,因此权限上面需要使用usera 的权限。此命令中currency只出现了一次,在这里表示智能合约的名称。 ...

January 28, 2019 · 1 min · jiezi

vue-router源码解析(三)路由模式

路由模式及降级处理vue-router 默认是 hash 模式 , 即使用 URL 的 hash 来模拟一个完整的 URL ,于是当 URL 改变时,页面不会重新加载。vue-router 还支持 history 模式,这种模式充分利用了 history.pushState 来完成 URL 跳转。在不支持 history.pushState 的浏览器 , 会自动会退到 hash 模式。是否回退可以通过 fallback 配置项来控制,默认值为 trueconst router = new VueRouter({ mode: ‘history’, // history 或 hash routes: […]});详细使用可参看文档: HTML5 History 模式根据 mode 确定类型首先看下 VueRouter 的构造方法 , 文件位置 src/index.jsimport { HashHistory } from ‘./history/hash’import { HTML5History } from ‘./history/html5’import { AbstractHistory } from ‘./history/abstract’ // … more constructor(options: RouterOptions = {}) { // … more // 默认hash模式 let mode = options.mode || ‘hash’ // 是否降级处理 this.fallback = mode === ‘history’ && !supportsPushState && options.fallback !== false // 进行降级处理 if (this.fallback) { mode = ‘hash’ } if (!inBrowser) { mode = ‘abstract’ } this.mode = mode // 根据不同的mode进行不同的处理 switch (mode) { case ‘history’: this.history = new HTML5History(this, options.base) break case ‘hash’: this.history = new HashHistory(this, options.base, this.fallback) break case ‘abstract’: this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== ‘production’) { assert(false, invalid mode: ${mode}) } } }我们可以看到,会判断是否支持 history , 然后根据 fallback 来确定是否要降级。然后,根据不同的 mode , 分别实例化不同的 history 。 (HTML5History、HashHistory、AbstractHistory)history我们看到 , HTML5History、HashHistory、AbstractHistory都是来自 history 目录。├── history // 操作浏览器记录的一系列内容│ ├── abstract.js // 非浏览器的history│ ├── base.js // 基本的history│ ├── hash.js // hash模式的history│ └── html5.js // html5模式的history其中, base.js 里面定义了 History 类基本的关系如下图:base.js 里面定义了一些列的方法, hash 、html5 模式,分别继承了这些方法,并实现了自己特有的逻辑从外部调用的时候,会直接调用到 this.history , 然后,由于初始化对象的不同,而进行不同的操作。接下来, 我们挑选其中一个我们最常用到的 push 方法来解释一整个过程push 方法我们平时调用的时候, 一直都是用 this.$router.push(‘home’) , 这种形式调用。首先,在 VueRouter 对象上有一个 push 方法 。// 文件位置: src/index.jsexport default class VueRouter { // … more push(location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort); }}我们看到,其没有做任何处理,直接转发到 this.history.push(location, onComplete, onAbort)。上面我们讲到,这个处理,会根据 history 的初始化对象不同而做不同处理。我们来分别看看细节mode === hashexport class HashHistory extends History { // …more // 跳转到 push(location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this; this.transitionTo( location, route => { pushHash(route.fullPath); handleScroll(this.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort ); }}// 切换路由// 会判断是否支持pushState ,支持则使用pushState,否则切换hashfunction pushHash(path) { if (supportsPushState) { pushState(getUrl(path)); } else { window.location.hash = path; }}mode === historyexport class HTML5History extends History { // …more // 增加 hash push(location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this; this.transitionTo( location, route => { pushState(cleanPath(this.base + route.fullPath)); handleScroll(this.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort ); }}两种模式的 push 实现区别并不大,都是调用了 transitionTo , 区别在于: 一个调用 pushHash , 一个调用 pushState.其他的 go 、 replace 、getCurrentLocation 都是类似的实现方式。transitionTo的具体实现,这里就先不详聊了,后面聊到路由守护的时候,会细讲这一块内容。其他系列文章列表个人博客 ...

January 22, 2019 · 2 min · jiezi

HashMap 浅析 —— LeetCode Two Sum 刷题总结

背景做了几年 CRUD 工程师,深感自己的计算机基础薄弱,在看了几篇大牛的分享文章之后,发现很多人都是通过刷 LeetCode 来提高自己的算法水平。的确,通过分析解决实际的问题,比自己潜心研究书本效率还是要高一些。一直以来遇到底层自己无法解决的问题,都是通过在 Google、GitHub 上搜索组件、博客来进行解决。这样虽然挺快,但是也让自己成为了一个“Ctrl+C/Ctrl+V”程序员。从来不花时间思考技术的内在原理。直到我刷了 Leetcode 第一道题目 Two Sum,接触到了 HashMap 的妙用,才激发起我去了解 HashMap 原理的兴趣。Two Sum(两数之和)TwoSum 是 Leetcode 中的第一道题,题干如下:给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。示例:给定 nums = [2, 7, 11, 15], target = 9因为 nums[0] + nums[1] = 2 + 7 = 9所以返回 [0, 1]初看这道题的时候,我当然是使用最简单的array遍历来解决了:public int[] twoSum(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { for (int j = i + 1; j < nums.length; j++) { if (nums[j] == target - nums[i]) { return new int[] { i, j }; } } } throw new IllegalArgumentException(“No two sum solution”);}这个解法在官方称为“暴力法”。通过这个“暴力法”我们可以看到里面有个我们在编程中经常遇到的一个场景:检查数组中是否存在某元素。官方的解析中提到,哈希表可以保持数组中每个元素与其索引相互对应,所以如果我们使用哈希表来解决这个问题,可以有效地降低算法的时间复杂度。(不了解哈希表和时间复杂度的的朋友别急,下文会详细说明)使用哈希表的解法是这样的:public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int complement = target - nums[i]; if (map.containsKey(complement)) { return new int[] { map.get(complement), i }; } map.put(nums[i], i); } throw new IllegalArgumentException(“No two sum solution”);}即使我们不是很会算时间复杂度,也能够明显看到,原来的双重循环,在哈希表解法里,变成了单重循环,代码的效率很明显提升了。但是令人好奇的是map.containsKey()到底是用了什么样的魔力,实现快速判断元素complement是否存在呢?这里就要引出本篇文章的主角 —— HashMap。HashMap注:以下内容基于JDK 1.8进行讲解在了解map.containsKey()这个方法之前,我们还是得补习一下基础,毕竟笔者在看到这里得时候,对于哈希表、哈希值得概念也都忘得一干二净了。什么是哈希表呢?哈希表是根据键(Key)而直接访问在内存存储位置的数据结构维基上的解释比较抽象。我们可以把一张哈希表理解成一个数组。数组中可以存储Object,当我们要保存一个Object到数组中时,我们通过一定的算法,计算出来Object的哈希值(Hash Code),然后把哈希值作为下标,Object作为值保存到数组中。我们就得到了一张哈希表。看到这里,我们前文中说到的哈希表可以保持数组中每个元素与其索引相互对应,应该就很好理解了吧。回到 Leetcode 的代示例,map.containsKey()中显然是通过获取 Key 的哈希值,然后判断哈希值是否存在,间接判断 Key 是否已经存在的。到了这里,如果我们仅仅是想要能够明白 HashMap 的使用原理,基本上已经足够了。但是相信有不少朋友对它的哈希算法感兴趣。下面我详细解释一下。map.containsKey()解析我们查看 JDK 的源码,可以看到map.containsKey()中最关键的代码是这段:/** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }一上来看不懂没关系,其实这段代码最关键的部分就只有这一句:first = tab[(n - 1) & hash]) != null。其中tab是 HashMap 的 Node 数组(每个 Node 是一个 Key&value 键值对,用来存在 HashMap的数据),这里对数组的长度n和hash值,做&运算(至于为什么要进行这样的&运算,是与 HashMap 的哈希算法有关的,具体要看java.util.HashMap.hash()这个方法,哈希算法是数学家和计算机基础科学家研究的领域,这里不做深入研究),得到一个数组下标,这个下标对应的数组数据,一般情况下就是我们要找的节点。注意这里我说的是一般情况下,因为哈希算法需要兼顾性能与准确性,是有一定概率出现重复的情况的。我们可以看到上文getNode方法,有一段遍历的代码:do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e;} while ((e = e.next) != null);就是为了处理极端情况下哈希算法得到的哈希值没有命中,需要进行遍历的情况。在这个时候,时间复杂度是O(n),而在这种极端情况以外,时间复杂度是O(1),这也就是map.containsKey()效率比遍历高的奥秘。Tips:看到这里,如果有人问你:两个对象,其哈希值(hash code)相等,他们一定是同一个对象吗?相信你一定有答案了。(如果两个对象不同,但哈希值相等,这种情况叫哈希冲突)哈希算法通过前文我们可以发现,HashMap 之所以能够高效地根据元素找到其索引,是借助了哈希表的魔力,而哈希算法是 哈希表的灵魂。哈希算法实际上是数学家和计算机基础科学家研究的领域。对于我们普通程序员来说,并不需要研究太透彻。但是如果我们能够搞清楚其实现原理,相信对于今后的程序涉及大有裨益。按笔者的理解,哈希算法是为了给对象生成一个尽可能独特的Code,以方便内存寻址。此外其作为一个底层的算法,需要同时兼顾性能与准确性。为了更好地理解 hash 算法,我们拿java.lang.String的hash 算法来举例。java.lang.String hashCode方法:public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h;}相信这段代码大家应该都看得懂,使用 String 的 char 数组的数字每次乘以 31 再叠加最后返回,因此,每个不同的字符串,返回的 hashCode 肯定不一样。那么为什么使用 31 呢?在名著 《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明:之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。可以看到,使用 31 最主要的还是为了性能。当然用 63 也可以。但是 63 的溢出风险就更大了。那么15 呢?仔细想想也可以。在《Effective Java》也说道:编写这种散列函数是个研究课题,最好留给数学家和理论方面的计算机科学家来完成。我们此次最重要的是知道了为什么使用 31。java.util.HashMap hash 算法实现原理相对复杂一些,这篇文章:深入理解 hashcode 和 hash 算法,讲得非常好,建议大家感兴趣的话通篇阅读。 ...

January 22, 2019 · 3 min · jiezi

vue-router源码解析(二)插件实现

vue-router 插件方式的实现vue-router 是作为插件集成到 vue 中的。我们使用 vue-router 的时候,第一部就是要 安装插件 Vue.use(VueRouter);关于插件的介绍可以查看 vue 的官方文档我们重点关注如何开发插件如何开发插件Vue.js 要求插件应该有一个公开方法 install。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。在 install 方法里面,便可以做相关的处理:添加全局方法或者属性添加全局资源:指令/过滤器/过渡等,通过全局 mixin 方法添加一些组件选项,添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。一个库,提供自己的 API,同时提供上面提到的一个或多个功能MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或属性 Vue.myGlobalMethod = function () { // 逻辑… } // 2. 添加全局资源 Vue.directive(‘my-directive’, { bind (el, binding, vnode, oldVnode) { // 逻辑… } … }) // 3. 注入组件 Vue.mixin({ created: function () { // 逻辑… } … }) // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { // 逻辑… }}在粗略了解了 vue.js 插件的实现思路之后,我们来看看 vue-router 的处理vue-router 的 install首先查看入口文件 src/index.jsimport { install } from ‘./install’;// …moreVueRouter.install = install;所以,具体的实现在 install里面。接下来我们来看具体做了些什么 ?install 实现install 相对来说逻辑较为简单。主要做了以下几个部分 :防止重复安装通过一个全局变量来确保只安装一次// 插件安装方法export let _Vue;export function install(Vue) { // 防止重复安装 if (install.installed && _Vue === Vue) return; install.installed = true; // …more}通过全局 mixin 注入一些生命周期的处理export function install(Vue) { // …more const isDef = v => v !== undefined; // 注册实例 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode; if ( isDef(i) && isDef((i = i.data)) && isDef((i = i.registerRouteInstance)) ) { i(vm, callVal); } }; // 混入生命周期的一些处理 Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { // 如果 router 已经定义了,则调用 this._routerRoot = this; this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive( this, ‘_route’, this._router.history.current ); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } // 注册实例 registerInstance(this, this); }, destroyed() { // 销毁实例 registerInstance(this); } }); // …more}我们看到 , 利用mixin,我们往实例增加了 beforeCreate 以及 destroyed 。在里面注册以及销毁实例。值得注意的是 registerInstance 函数里的vm.$options._parentVnode.data.registerRouteInstance;你可能会疑惑 , 它是从哪里来的 。它是在 ./src/components/view.js , route-view 组件的 render 方法里面定义的。主要用于注册及销毁实例,具体的我们后期再讲~挂载变量到原型上通过以下形式,定义变量。我们经常使用到的 this.$router ,this.$route 就是在这里定义的。// 挂载变量到原型上Object.defineProperty(Vue.prototype, ‘$router’, { get() { return this._routerRoot._router; }});// 挂载变量到原型上Object.defineProperty(Vue.prototype, ‘$route’, { get() { return this._routerRoot._route; }});这里通过 Object.defineProperty 定义 get 来实现 , 而不使用 Vue.prototype.$router = this.this._routerRoot._router。是为了让其只读,不可修改注册全局组件import View from ‘./components/view’;import Link from ‘./components/link’;export function install(Vue) { // …more // 注册全局组件 Vue.component(‘RouterView’, View); Vue.component(‘RouterLink’, Link); // …more}最后附上 install.js 完整的代码import View from ‘./components/view’;import Link from ‘./components/link’;export let _Vue;// 插件安装方法export function install(Vue) { // 防止重复安装 if (install.installed && _Vue === Vue) return; install.installed = true; _Vue = Vue; const isDef = v => v !== undefined; // 注册实例 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode; if ( isDef(i) && isDef((i = i.data)) && isDef((i = i.registerRouteInstance)) ) { i(vm, callVal); } }; // 混入生命周期的一些处理 Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { // 如果 router 已经定义了,则调用 this._routerRoot = this; this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive( this, ‘_route’, this._router.history.current ); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } // 注册实例 registerInstance(this, this); }, destroyed() { registerInstance(this); } }); // 挂载变量到原型上 Object.defineProperty(Vue.prototype, ‘$router’, { get() { return this._routerRoot._router; } }); // 挂载变量到原型上 Object.defineProperty(Vue.prototype, ‘$route’, { get() { return this._routerRoot._route; } }); // 注册全局组件 Vue.component(‘RouterView’, View); Vue.component(‘RouterLink’, Link); // 定义合并的策略 const strats = Vue.config.optionMergeStrategies; // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;}其他系列文章列表个人博客 ...

January 21, 2019 · 3 min · jiezi

vue-router源码解析(一)

准备工作Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。这里主要通过阅读 vue-router 的源码,对平时使用较多的一些特性以及功能,理解其背后实现的思路。阅读版本 : 3.0.2目录结构├── components // 组件│ ├── link.js // route-link的实现│ └── view.js // route-view的实现├── create-matcher.js // 创建匹配├── create-route-map.js // 创建路由的映射├── history // 操作浏览器记录的一系列内容│ ├── abstract.js // 非浏览器的history│ ├── base.js // 基本的history│ ├── hash.js // hash模式的history│ └── html5.js // html5模式的history├── index.js // 入口文件├── install.js // 插件安装的方法└── util // 工具类库 ├── async.js // 异步操作的工具库 ├── dom.js // dom相关的函数 ├── location.js // 对location的处理 ├── misc.js // 一个工具方法 ├── params.js // 处理参数 ├── path.js // 处理路径 ├── push-state.js // 处理html模式的 pushState ├── query.js //对query的处理 ├── resolve-components.js //异步加载组件 ├── route.js // 路由 ├── scroll.js //处理滚动 └── warn.js // 打印一些警告我们知道 , 我们在使用 vue-router 的时候 ,主要有以下几步:<div id=“app”> <!– 路由匹配到的组件将渲染在这里 –> <router-view></router-view></div>// 1. 安装 插件Vue.use(VueRouter);// 2. 创建router对象const router = new VueRouter({ routes // 路由列表 eg: [{ path: ‘/foo’, component: Foo }]});// 3. 挂载routerconst app = new Vue({ router}).$mount(’#app’);其中 VueRouter 对象,就在vue-router 的入口文件 src/index.jsVueRouter 原型上定义了一系列的函数,我们日常经常会使用到。主要有 : go 、 push 、 replace 、 back 、 forward 。以及一些导航守护 : beforeEach 、beforeResolve 、afterEach 等等上面html 中使用到的 router-view ,以及经常用到的 router-link 则存在 src/components 目录下。下一步到这里相信你对整个项目结构有一个大概的认识 。 接下来,我们会根据以下几点,一步步拆解 vue-router。vue 插件方式的实现路由模式及降级处理的实现导航守卫的原理路由匹配详解组件:route-view 和 route-link 都做了些什么 ?滚动行为的实现如何实现异步加载组件(路由懒加载)其他查看系列文章 ...

January 21, 2019 · 1 min · jiezi

关于属性描述符PropertyDescriptor

本文首发于本博客 猫叔的博客,转载请申明出处前言感谢GY丶L粉丝的提问:属性描述器PropertyDescriptor是干嘛用的?本来我也没有仔细了解过描述符这一块的知识,不过粉丝问了,我就抽周末的时间看看,顺便学习一下,粉丝问的刚好是PropertyDescriptor这个属性描述符,我看了下源码。/** * A PropertyDescriptor describes one property that a Java Bean * exports via a pair of accessor methods. /public class PropertyDescriptor extends FeatureDescriptor { //…}emmmm,假装自己英语能厉害的说,属性描述符描述了一个属性,即Java Bean 通过一对访问器方法来导出。(没错,他确实是存在于java.beans包下的)通过类关系图,可以知道,我们应该提前了解一下FeatureDescriptor才行了。很好,起码目前还没有设计抽象类或者接口。FeatureDescriptor/* * The FeatureDescriptor class is the common baseclass for PropertyDescriptor, * EventSetDescriptor, and MethodDescriptor, etc. * <p> * It supports some common information that can be set and retrieved for * any of the introspection descriptors. * <p> * In addition it provides an extension mechanism so that arbitrary * attribute/value pairs can be associated with a design feature. /public class FeatureDescriptor { //…}okay,这是很合理的设计方式,FeatureDescriptor为类似PropertyDescriptor、EvebtSetDescriptor、MethodDescriptor的描述符提供了一些共用的常量信息。同时它也提供一个扩展功能,方便任意属性或键值对可以于设计功能相关联。这里简单的说下,在我大致看了一下源码后(可能不够详细,最近有点忙,时间较赶),FeatureDescriptor主要是针对一下属性的一些get/set,同时这些属性都是基本通用于PropertyDescriptor、EvebtSetDescriptor、MethodDescriptor。 private boolean expert; // 专有 private boolean hidden; // 隐藏 private boolean preferred; // 首选 private String shortDescription; //简单说明 private String name; // 编程名称 private String displayName; //本地名称 private Hashtable<String, Object> table; // 属性表其实该类还有另外几个方法,比如深奥的构造函数等等,这里就不深入探讨了。PropertyDescriptor那么我们大致知道了FeatureDescriptor,接下来就可以来深入了解看看这个属性描述符PropertyDescriptor。说到属性,大家一定会想到的就是get/set这个些基础的东西,当我打开PropertyDescriptor源码的时候,我也看到了一开始猜想的点。 private final MethodRef readMethodRef = new MethodRef(); private final MethodRef writeMethodRef = new MethodRef(); private String writeMethodName; private String readMethodName;这里的代码是我从源码中抽离的一部分,起码我们这样看可以大致理解,是分为写和读的步骤,那么就和我们初学java的get/set是一致的。同时我还看到了,这个,及其注释。 // The base name of the method name which will be prefixed with the // read and write method. If name == “foo” then the baseName is “Foo” private String baseName;这好像可以解释,为什么我们的属性在生成get/set的时候,第一个字母变成大写?!注释好像确实是这样写的。由于可能需要一个Bean对象,所以我以前在案例中先创建了一个Cat类。public class Cat { private String name; private String describe; private int age; private int weight; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescribe() { return describe; } public void setDescribe(String describe) { this.describe = describe; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; }}构造函数起码目前,我还不知道我应该怎么使用它,那么我们就一步一步来吧,我看到它有好几个构造函数,这是一个有趣而且有难度的事情,我们先试着创建一个PropertyDescriptor吧。第一种构造函数 /* * Constructs a PropertyDescriptor for a property that follows * the standard Java convention by having getFoo and setFoo * accessor methods. Thus if the argument name is “fred”, it will * assume that the writer method is “setFred” and the reader method * is “getFred” (or “isFred” for a boolean property). Note that the * property name should start with a lower case character, which will * be capitalized in the method names. * * @param propertyName The programmatic name of the property. * @param beanClass The Class object for the target bean. For * example sun.beans.OurButton.class. * @exception IntrospectionException if an exception occurs during * introspection. / public PropertyDescriptor(String propertyName, Class<?> beanClass) throws IntrospectionException { this(propertyName, beanClass, Introspector.IS_PREFIX + NameGenerator.capitalize(propertyName), Introspector.SET_PREFIX + NameGenerator.capitalize(propertyName)); }这个好像是参数最少的,它只需要我们传入一个属性字符串,还有对应的类就好了,其实它也是调用了另一个构造函数,只是它会帮我们默认生成读方法和写方法。方法中的Introspector.IS_PREFIX + NameGenerator.capitalize(propertyName)其实就是自己拼出一个默认的get/set方法,大家有兴趣可以去看看源码。那么对应的实现内容,我想大家应该都想到了。 public static void main(String[] args) throws Exception { PropertyDescriptor CatPropertyOfName = new PropertyDescriptor(“name”, Cat.class); System.out.println(CatPropertyOfName.getPropertyType()); System.out.println(CatPropertyOfName.getPropertyEditorClass()); System.out.println(CatPropertyOfName.getReadMethod()); System.out.println(CatPropertyOfName.getWriteMethod()); }第二种构造函数/* * This constructor takes the name of a simple property, and method * names for reading and writing the property. * * @param propertyName The programmatic name of the property. * @param beanClass The Class object for the target bean. For * example sun.beans.OurButton.class. * @param readMethodName The name of the method used for reading the property * value. May be null if the property is write-only. * @param writeMethodName The name of the method used for writing the property * value. May be null if the property is read-only. * @exception IntrospectionException if an exception occurs during * introspection. / public PropertyDescriptor(String propertyName, Class<?> beanClass, String readMethodName, String writeMethodName) throws IntrospectionException { if (beanClass == null) { throw new IntrospectionException(“Target Bean class is null”); } if (propertyName == null || propertyName.length() == 0) { throw new IntrospectionException(“bad property name”); } if ("".equals(readMethodName) || “".equals(writeMethodName)) { throw new IntrospectionException(“read or write method name should not be the empty string”); } setName(propertyName); setClass0(beanClass); this.readMethodName = readMethodName; if (readMethodName != null && getReadMethod() == null) { throw new IntrospectionException(“Method not found: " + readMethodName); } this.writeMethodName = writeMethodName; if (writeMethodName != null && getWriteMethod() == null) { throw new IntrospectionException(“Method not found: " + writeMethodName); } // If this class or one of its base classes allow PropertyChangeListener, // then we assume that any properties we discover are “bound”. // See Introspector.getTargetPropertyInfo() method. Class[] args = { PropertyChangeListener.class }; this.bound = null != Introspector.findMethod(beanClass, “addPropertyChangeListener”, args.length, args); }没错,这个构造函数就是第一种构造函数内部二次调用的,所需要的参数很简单,同时我也希望大家可以借鉴这个方法中的一些检测方式。这次的实现方式也是同样的形式。 public static void main(String[] args) throws Exception { PropertyDescriptor CatPropertyOfName = new PropertyDescriptor(“name”, Cat.class,“getName”,“setName”); System.out.println(CatPropertyOfName.getPropertyType()); System.out.println(CatPropertyOfName.getPropertyEditorClass()); System.out.println(CatPropertyOfName.getReadMethod()); System.out.println(CatPropertyOfName.getWriteMethod()); }第三种构造函数 /* * This constructor takes the name of a simple property, and Method * objects for reading and writing the property. * * @param propertyName The programmatic name of the property. * @param readMethod The method used for reading the property value. * May be null if the property is write-only. * @param writeMethod The method used for writing the property value. * May be null if the property is read-only. * @exception IntrospectionException if an exception occurs during * introspection. */ public PropertyDescriptor(String propertyName, Method readMethod, Method writeMethod) throws IntrospectionException { if (propertyName == null || propertyName.length() == 0) { throw new IntrospectionException(“bad property name”); } setName(propertyName); setReadMethod(readMethod); setWriteMethod(writeMethod); }这个不用传类,因为你需要传递两个实际的方法进来,所以主要三个对应属性的参数既可。看看大致的实现内容 public static void main(String[] args) throws Exception { Class<?> classType = Cat.class; Method CatNameOfRead = classType.getMethod(“getName”); Method CatNameOfWrite = classType.getMethod(“setName”, String.class); PropertyDescriptor CatPropertyOfName = new PropertyDescriptor(“name”, CatNameOfRead,CatNameOfWrite); System.out.println(CatPropertyOfName.getPropertyType()); System.out.println(CatPropertyOfName.getPropertyEditorClass()); System.out.println(CatPropertyOfName.getReadMethod()); System.out.println(CatPropertyOfName.getWriteMethod()); }好了,大致介绍了几种构造函数与实现方式,起码我们现在知道它需要什么。一些使用方式其实在我上面写一些构造函数的时候,我想大家应该已经感受到与反射相关了,起码我感觉上是这样的,所以我一开始想到这样的案例形式,通过反射与这个属性描述类去赋予我的类。 public static void main(String[] args) throws Exception { //获取类 Class classType = Class.forName(“com.example.demo.beans.Cat”); Object catObj = classType.newInstance(); //获取Name属性 PropertyDescriptor catPropertyOfName = new PropertyDescriptor(“name”,classType); //得到对应的写方法 Method writeOfName = catPropertyOfName.getWriteMethod(); //将值赋进这个类中 writeOfName.invoke(catObj,“river”); Cat cat = (Cat)catObj; System.out.println(cat.toString()); }运行结果还是顺利的。Cat{name=‘river’, describe=‘null’, age=0, weight=0}可以看到,我们确实得到了一个理想中的对象。那么我是不是可以改变一个已经创建的对象呢? public static void main(String[] args) throws Exception { //一开始的默认对象 Cat cat = new Cat(“river”,“黑猫”,2,4); //获取name属性 PropertyDescriptor catPropertyOfName = new PropertyDescriptor(“name”,Cat.class); //得到读方法 Method readMethod = catPropertyOfName.getReadMethod(); //获取属性值 String name = (String) readMethod.invoke(cat); System.out.println(“默认:” + name); //得到写方法 Method writeMethod = catPropertyOfName.getWriteMethod(); //修改值 writeMethod.invoke(cat,“copy”); System.out.println(“修改后:” + cat); }上面的demo是,我先创建了一个对象,然后通过属性描述符读取name值,再进行修改值,最后输出的对象的值也确实改变了。默认:river修改后:Cat{name=‘copy’, describe=‘黑猫’, age=2, weight=4}收尾这是一个有趣的API,我想另外两个(EvebtSetDescriptor、MethodDescriptor)应该也差不多,大家可以再通过此方法去探究,只有自己尝试一次才能学到这里面的一些东西,还有一些项目场景的使用方式,不过一般的业务场景应该很少使用到这个API。那么这个东西究竟可以干什么呢?我想你试着敲一次也许有一些答案了。公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。 ...

January 19, 2019 · 5 min · jiezi