Springdoc 生成 API 文档 — 迁移自 Springfox

傻傻分不清的近义词辨析 提到 Spring 自动生成 API 文档,多数人脑中就浮现出几个单词:Swagger, OpenAPI, Springfox… 所以首先有必要搞清楚他们的关系。 Open

June 19, 2021 · Chenhe

[译] Mocks 与 Stubs 的区别

原始链接:https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs 很多人经常会混淆这两个测试用的术语,要想完全理解测试替身(test doubles)的用法,我们就必须搞清楚 mocks 和其他术语的区别。当我们进行测试的时候,通常一次只关注一个元素,所以产生了一个术语叫单元测试。问题在于为了使得某个独立单元工作,通常需要其他单元的配合,因此在这个情况下我们需要某种类型的替代品。 在上文(见原文)提到的两个测试类型中,前者使用的是一个真实的对象,而后者使用的是一个 mock 对象,也就是非真实的。使用 mock 是在测试中避免使用真实对象的方法之一,同时也有其他形式的模拟手段。描述这些手段的词汇就很繁杂了,比如 stub, mock, fake, dummy。在这篇文章中,我使用 Gerard Meszaros 的书中所定义的词汇,虽然不是每个人都认可他,但我比较赞同。 Meszaros 使用 Test Double 这个术语来描述任何一个在测试中用于代替真实对象的虚拟对象,这个名称源于电影中替身演员的概念。随后 Meszaros 定义了五种典型的虚拟对象: Dummy 被传递但从不使用,通常只用于填充形式参数。 Fake 则具有可以正常工作的实现,但通常采用了一些不适合生产环境的便捷手段。(一个典型例子是内存数据库)。 Stub 在测试中总是返回固定的返回值,对于与测试无关的代码,通常直接忽略。 Spy 和 Stub 一样,但是它会记录自身被调用的情况。一个例子是邮件服务会记录发送了多少封邮件。 Mock 就是我们这里所谈论的:它根据预先编写的逻辑,基于调用者所期望的返回值。 在所有这些类型中,只有 mock 进行行为验证,而其他类型的虚拟对象通常只能进行状态验证。和其他类型一样,mock 在测试过程中也具有一些行为,以便让待测程序正常工作,但在设置和验证阶段有所不同。 为了进一步研究测试替身,我们需要扩展一下之前的例子。很多人只在真实的对象难以配合进行测试时才会使用测试替身。比如一个常见的用例:如果订单异常,那么就需要发送一封邮件通知。问题是在测试期间我们并不想真正给客户发一封邮件过去,所以我们为邮件系统创建一个我们可以控制操作的测试替身。 现在我们可以来研究 mock 和 stub 之间的区别了。假设我们正为发送邮件这一行为编写测试,代码大致如下: public interface MailService { public void send (Message msg); } public class MailServiceStub implements MailService { private List<Message> messages = new ArrayList<Message>(); public void send (Message msg) { messages....

January 9, 2021 · Chenhe

Android 磁盘最近最少缓存实现 - DiskLruCache 源码分析

 DiskLruCache 是一个 Android 端使用广泛的磁盘 LRU (最近最少使用)缓存算法的实现库,甚至在 AOSP 中都有使用。 基础使用 为了对整体架构有个印象,方便找分析入口,先看看基本的使用。 实例化: val cache = DiskLruCache.open(cacheFile, version, valueCount, maxSize) 写入: val editor = cache.edit(key.md5()) val os = editor.newOutputStream(0) 读取: val snapshot = cache.get(key.md5()) val is = snapshot?.getInputStream(0) 关于它的具体使用可以看这篇文章。 整体来看并不复杂,实际上代码量也确实不多。通过读取时的 snapshot 封装猜测后续修改不会影响已经读取到的值。 日志文件 DiskLruCache 的一个显著特征就是在缓存目录有一个 journal 文件,用于记录所有操作日志,大概长这个样子: libcore.io.DiskLruCache 1 19 1 DIRTY c915a1313f3e413252fae2957469dcd2 CLEAN c915a1313f3e413252fae2957469dcd2 50196 READ c915a1313f3e413252fae2957469dcd2 DIRTY 8b33d00d5f9d443095e20829a1367e05 CLEAN 8b33d00d5f9d443095e20829a1367e05 159594 前五行是元数据: 一个魔法数,固定字符串,用于标识这是一个 DiskLruCache 的日志文件。 DiskLruCache 版本号,从发布以来就没变过,预留一下便于后续格式更新。 开发者声明的应用版本号。 每个 key 对应几个 value,一般都是1。一个 key 支持写入多个值,每个值都是一个文件,文件名是 key....

March 9, 2020 · Chenhe

Git HTTP+SSH 代理配置

本来是没有什么技术含量的东西。奇怪的是网上答案千篇一律而且大部分都是错的,不知道要坑坏多少人:smiling_imp:。 HTTP 代理 HTTP 代理相对简单 # HTTP 代理 git config --global http.proxy http://127.0.0.1:1080 git config --global https.proxy http://127.0.0.1:1080 # Socks5 代理 git config --global http.proxy socks5://127.0.0.1:1080 git config --global https.proxy socks5://127.0.0.1:1080 注意这里的 socks5 仅仅是代理使用的协议,它依然是针对 http 设置的,所以仅对 http 协议的仓库有效。使用 git@xxx 这种 ssh 连接的不会使用代理。 也可以分域名设置代理: git config --global http.https://github.com.proxy http://127.0.0.1:1080 git config --global https.https://github.com.proxy https://127.0.0.1:1080 SSH 代理 SSH 代理需要在密钥目录 (~/.ssh) (Windows 下是 C:\Users\{UserName}\.ssh) 新建一个 config 文件,没有后缀名。 Linux 系统写入以下配置(未验证): # 需要 netcat ProxyCommand nc -v -x 127....

March 3, 2020 · Chenhe

Android 自定义 View 的默认属性值(四个构造函数的作用)

常见的套路 自定义 View 同学们一定或多或少都接触过,毕竟设计给出的样式往往从来不是系统默认的🙂。 自定义 View 时往往遵守下面这样的套路: 在 attrs.xml 中声明自定义属性(或使用系统预定义属性),经常用 declare-styleable 包裹起来。 继承一个 View 并写两个构造函数:constructor(context: Context), constructor(context: Context, attrs: AttributeSet?). 前者用于代码创建,后者用于 xml 创建。 context.theme.obtainStyledAttributes() 来读取属性值,并在 getXXX() 函数中给出一个默认值。 一切似乎理所应当行云流水并且工作良好,但这真的合理吗?(废话 要是合理这篇文章是在自嗨嘛😅 考虑以下问题: 用户(本文指使用 View 的开发者)如何统一设置默认样式? 默认值硬编码在代码文件中是否耦合过重? 当然用户可以定义一个 style 每次使用都设置一下,你不觉得太麻烦了吗? 当然也可以直接写在主题的样式中,但如果有冲突属性怎么办?例如自定义 View 使用了系统定义的 android:textColor 但又不希望其他控件例如 TextView 受影响。 为了后边便于说明,我们定义个 CircleCheckBox,并定义如下属性: <!--lib: attrs.xml--> <resources> <declare-styleable name="CircleCheckBox"> <attr name="normalColor" format="color" /> <attr name="selectColor" format="color" /> <attr name="outlineColor" format="color" /> <attr name="outlineStrokeWidth" format="dimension" /> <attr name="android:checked" /> </declare-styleable> </resources> 被忽略的两兄弟 除了常用的两个构造函数之外,还有一个三个参数的构造函数,从 Android 5....

February 16, 2020 · Chenhe

一起动才够嗨!Android CoordinatorLayout 自定义 Behavior

CoordinatorLayout 的今生前世 联动效果 现代化的 Android 开发一定对 CoordinatorLayout 不陌生,CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout + Toolbar 的全家桶更是信手拈来,无需一行代码光靠 xml 就能实现下面这种折叠导航栏的炫酷效果: 这种搭配的教程已经非常多了,不是本文的重点。在使用 xml 时候肯定不少同学掉过一个坑:界面主要内容与头部元素重叠了!粗略了解一下因为 CoordinatorLayout 的布局方式类似 FrameLayout 默认情况下所有元素都会叠加在一起,解决方案也非常玄学,就是给内容元素添加一个 app:layout_behavior="@string/appbar_scrolling_view_behavior" 属性就好了,简直像黑魔法! Unfortunately,代码并没有魔法,我们能偷懒是因为有人封装好了。跟踪进这个字符串是 com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior 显然这是个类!事实上这就是今天的重头戏 —— Behavior. 这个效果太复杂了,所以 Google 才会帮我们包装好,下面换一个简单的例子便于学习: 这是仿三星 One UI 的界面。上面是一个头布局,下面是一个 RecyclerView,向上滑动时首先头布局收缩渐隐并有个视差效果,头部彻底隐藏后 RecyclerView 无缝衔接。向下滑动时同理。 事件拦截实现 在继续探索之前,先思考一下如果没有 CoordinatorLayout 这种现代化东西怎么办?因为这牵扯到滑动手势与 View 效果的糅合,毫无疑问应该从触摸事件上入手。简单起见暂时只考虑手指向上滑动(列表向下展示更多内容),大概需要进行以下操作: 在父布局 onInterceptTouchEvent 中拦截事件。 父布局 onTouchEvent 处理事件,对 HeaderView 进行操作(移动、改变透明度等)。 HeaderView 完全折叠后父布局不再拦截事件,RecyclerView 正常处理滑动。 现在已经遇到问题了。因为一开始父布局拦截了事件,因此根据 Android 事件分发机制,哪怕后续不再拦截其子控件也无法收到事件,除非重新触摸,这就造成了两者的滑动不能无缝衔接。 接着还有一个问题,反过来当 RecyclerView 向下滑动至顶部时,如何通知 HeaderView 展开? 哪怕解决了上述主要问题,肯定还有其他小毛病,例如子控件无法触发点击事件等等等非常恼人💢。假设你是大佬完美解决了所有问题,肯定耦合特别严重,又是自定义 View 又是互相引用的乱七八糟😵 所以现在就不往下深究了,有闲情雅致有能力的同学可以尝试实现。...

February 15, 2020 · Chenhe

Android 事件分发机制

Android 事件分发机制一直让人头痛,之前也是面向 GitHub 编程得过且过。今天下定决心了解一下,以便后面自己定制 View 效果。Android 触摸事件有三个基本类型:ACTION_DOWN, ACTION_MOVE, ACTION_UP,后两者的传递顺序取决于 DOWN 的传递结果,所以就从 ACTION_DOWN 开始分析。 ACTION_DOWN 全景 借用一张下面参考文章里的全景图片,注意这里指的仅仅是 ACTION_DOWN 事件的传递。先解释一下: 白色箭头表示事件传递(函数调用) 箭头上的标注表示调用前提。(supper 表示上一级直接调用,false 表示若上级返回 false 则系统继续向下调用) 白色方块内的消费箭头表示若此函数返回对应值,则事件终止传递(也称作被消费了) 以左上角事件入口为例,首先 Activity 收到事件触发 dispatchTouchEvent,不论返回 true 还是 false 事件均终止,任何组件的任何函数均不会再被调用(包括 activity 自己的 onTouchEvent),只有 return super.dispatchTouchEvent() 也就是调用了 super 才会继续传递到下一级。 对于下一级 ViewGroup 的 dispatchTouchEvent 来讲,返回 true 同样消费事件立即终止传递。返回 false 则会回溯到上一层的 onTouchEvent。调用 super 则继续向下传递。 全程传递 我们假设事件没有被拦截、消费,那么整个传输流程类似 U 型: 不难看出,整个流程分为左右两部分,我们暂且叫做分派与回溯。分派是自顶到底的,主要用于事件的传递。回溯是从底到顶的,主要用于事件的处理。所有方法的默认实现就是 return super.xxx() 因此事件默认情况下可以走完整个流程。 拦截器 ViewGroup 细心的同学应该注意到了,在分派过程中除了整齐的 dispatchTouchEvent 方法外,乱入了一个 onInterceptEvent 方法,可以称之为拦截器。顾名思义拦截器的作用就是拦截此事件供自己使用(就像大理 gov 一样对待口罩那样)。不难看出 dispatchTouchEvent 方法调用后根据内部处理的不同有三个后果,分别是 ①终止传递 ②向下传递 ③向上回溯,而前面提到过,一般来讲处理具体的处理会放在 onTouchEvent 中。那么问题来了,终止传递后我居然自己无法处理事件?(参考 U 型图中的 ViewGroup 层,无论 dispatchTouchEvent 作何响应都无法调用自己的 onTouchEvent)...

February 13, 2020 · Chenhe

Jenkins+Github 持续测试

说明下环境: Windows 10 (因为还需要跑其他一些东西) Jenkins 2.204.1 LTS 内置 Jenkins war 2.217 配置 GitHub 鉴权 Jenkins 的安装过程很简单就不写了。首先需要安装 GitHub Plugin 插件,如果安装 Jenkins 时选择了安装常用插件那么应该已经装好了,否则就去手动安装一下。 为了和 GitHub 账户连接,当然要配置一下鉴权。进入 Manage Jenkins - Configure System,找到 GitHub,添加一个 GitHub 服务器,API URL 保持默认 https://api.github.com 就行,名称随便填。点击凭据右边的问号可以看到帮助。 去 GitHub 生成一下 AccessToken,Note 就是一个备注随便填,下面的授权范围要勾选 repo 和 admin:repo_hook. 复制一下生成的 Token 回到 Jenkins 添加凭据,类型选 Secret text,将 Token 粘贴到 Secret 编辑框内,ID 留空,描述 自己填一下其他保持默认。 最后在下拉框中选选中刚刚添加的,点一下连接测试,通过就行啦😁 Webhooks Webhooks 用于当发生特定的事件(例如一个新的 push)时 GitHub 主动去通知外部服务。打开需要集成的项目,进入 Settings - Webhooks,点击 Add webhook....

January 28, 2020 · Chenhe

Moshi with Kotlin Json 库—现代化的最佳损友

Moshi 与 Kotlin Android(Java) 平台已经有许多 Json 库了,包括 Google 推荐的 Gson,广受欢迎的 Jackson,阿里的 FastJson 等,但今天要说的是大名鼎鼎 Square 公司的 Moshi. (什么?没听说过 Square?OkHttp, Retrofit 可都是他的著作) 轮子已经有很多了,不过自从 Google 将 Kotlin 定为 Android 亲儿子开发语言后,竟然包括 Gson 在内的几乎所有 Json 库均不支持,这当然可以理解,因为他们大多数采用了 Java 反射机制,注定难以适配 Kotlin 独有的特性。因此今天要介绍 Moshi —— 一个与 Kotlin 兼容极好的现代化解析库。 传统 Java Json 库用于 Kotlin 主要产生两个问题: 不支持空安全。即使 Kotlin 变量定义为非空,若 Json 为空则解析出 null 而不会抛出异常,直到数据使用时才抛出诡异的 NPE. 为啥说诡异?因为定义非空的变量默认不需要进行空判断,但实际上他是空的,有点挂羊头买狗肉的意思。 不支持默认参数。Kotlin 的 data class 极大地方便了开发,默认参数的语法更是直呼太爽。可惜这种现代化写法或遇到两个问题:①默认参数无效。②解析失败因为没有无参构造函数。而解决办法更是令人崩溃:①不要使用默认参数。②给所有形参全部加上默认参数。 因此,如果你已经使用 Kotlin 作为主要语言,Moshi 将会是绝佳的选择。(KT 自带的解析库也可以考虑) 另外 Moshi 的贡献者也是 Gson 的主要贡献者,因为 Gson 的先天不足且他已经离开了 Google,故开发了 Moshi,具体可以参考他在 Reddit 的回答:Why use Moshi over Gson?...

January 25, 2020 · Chenhe

Koin in Android: 更简单的依赖注入

Dagger 之殇 如果还不清楚什么是依赖注入,那么请参考之前写的 Dagger2 in Android(一)通俗基础开头部分。如果你不了解 Dagger 倒也无妨,本文会进行一定的对比,但仅针对接触过 Dagger 的同学,否则大可以忽略。 Dagger2 作为著名优秀的依赖注入框架广为流传,何况还是 Android 的亲爸爸 - Google 在维护,因此相信很多人会将其作为 Android 开发的首选 DI 框架。Dagger 从入门到放弃一定是很多很多人必经甚至多次经历的历程:anger: 诚然,Dagger 很强大。但它的学习曲线太过于陡峭,即使好不容易搞清楚了各种注解与概念,也很难适当地运用到项目中。同时对于 Activity 之类重要但却不能我们自己初始化的类 Dagger 明显水土不服。为此,Google 搞了个 .android 扩展库来「简化」使用。我不否认最终确实简化了代码,但是这玩意本身就很难度,学习成本堪称指数级。 除此之外,.android 扩展库对于 ViewModel 依然是严重的水土不服,甚至 Google 官方 Demo 的实现也是一堆问题。 Koin 基础 Koin 是纯 Kotlin 编写的轻量级依赖注入框架,轻量是因为它只使用 Kotlin 的函数解析特性,没有代理,没有代码生成,没有反射!官方声称5分钟快速上手。随着 Kotlin 的推广,Koin 这个后起之秀也获得了越来越多的关注。当然它也提供了 implementation "org.koin: koin-java:1.0.0" 扩展库来支持 java,但本文不会涉及。 不建议新手阅读 Koin 源码。作为 DSL,它大量使用了 Kotlin 的高级特性,例如 inline 函数。相对来说难以理解。 使用 Koin 所需的依赖在官方文档已经说得很明确了。这里因为使用了 AndroidX 库,所以引入 org....

July 5, 2019 · Chenhe