Kotlin/Java TDD 开发流程记录

通过使用 Kotlin / Java 中 Junit5 和 Mockito 测试框架,在预约功能中演示 TDD 开发流程。 TDD 介绍 TDD(Test-Driven Development) 是一种开发流程,中文是「测试驱动开发」。用一句白话形容,就是「先写测试再开发」。先写测试除了能确保测试程式的撰写,还有一个好处:有助于在开发初期厘清程式介面如何设计。详细理论知识可以前往 Wiki 了解,这里不再过多介绍。 测试驱动开发 Test Driven Development TDD 开发流程(5步) 术语说明: 红灯 - Failure - 测试用例失败 绿灯 - Success - 测试用例成功 重构 - Refactor - 重构功能代码 具体步骤: 选定一个功能,编写测试用例 执行测试,得到【红灯】 编写满足测试用例的功能代码 再次执行,得到【绿灯】 【重构】代码 小结: 对于每一个功能,在【红灯】-【绿灯】-【重构】间来回循环往复,不断得到完善。 前置工作 代码说明 使用 Kotlin 语言(会有相对应的 Java 代码) 使用到的测试框架 Running: JUnit5 Mock: MockK / Mockito Assertion: Kotest / AssertJ 只涉及 TDD 的具体流程,不涉及单元测试如何编写(可以看 SpringBoot 单元测试各层) 功能介绍 假设一个用户预约的场景。 ...

五月 3, 2021 · 2 分钟 · Lex Cao

Kotlin 奇怪的相等现象探究

最近遇到一个平时没怎么关注的 Kotlin 相等问题,决定记录一下探究过程。 事由 以下代码片段 Kotlin 版本 1.3.72。 还原问题代码,已去除业务逻辑部分,仅保留关键代码,片段如下: // 有一个状态枚举 enum class MyState { OK, CANCELED } // 某个处理函数会返回 nullable MyState fun processing(): MyState? { // 假设当前某种情况下返回 取消 这个状态 return MyState.CANCELED } // 在处理状态时 fun handleState() { // 此时编译器推断出类型为 State? val state = processing() if (state == CANCELED) { // 当处理 CANCELED 以下代码没有执行 println("Handle <CANCELED> state") } } 当处理 CANCELED 代码没有执行,原因在于***「import」***。 import javax.print.attribute.standard.JobState.CANCELED // 此处使用静态导入引入了一个其他包中同名的一个静态变量,该变量声明如下 // public static final JobState CANCELED = new JobState (7); 解决方法: ...

四月 21, 2020 · 5 分钟 · Lex Cao

使用 KAPT 生成 Kotlin Data Class 转换器

背景 Web 后台开发中,对于一个实体的操作会衍生出多个类似的对象进行操作(避免直接使用实体),由此出现相关名词 持久化对象,即实体 PO(Persistent Object) 传输对象 DTO(Data Transfer Object) 业务对象 BO(Business Object) 展示对象 VO(View Object) 等等…… 这些对象大多数直接从实体里面裁剪几个字段,比如,在一次创建订单请求中以订单实体(OrderEntity)为例,经历如下流程: 1. 接收请求体 CreateOrderRequest 2. 根据 OrderQuery 构造查询对象查询订单 3. 构造 OrderEntity 进行持久化操作 4. 构造 OrderBO 进行下游消费 5. 返回响应体 CreateOrderResponse 可见,从 OrderEntity 衍生出 4 个对象,仅仅是对订单的实体的部分裁剪,但是要编写很多重复的代码(复制也行)。当然,如果是新增字段的话可以使用继承解决。 在 Kotlin Web 后台开发中,data class 的语法特性带来很多优势,但还是避免不了创建类似的重复对象。 所以 Konverter 诞生于此,解决实体对象裁剪问题。还有另一个功能那就是自动生成两个实体间的转换方法。 注意:目前只支持 Kotlin,并且生成的转换方法是通过扩展函数实现 是什么 通过 KAPT(Kotlin Annotation Processing Tool 注解处理以及 Kotlin Poet 代码生成,实现自动生成对实体的相关裁剪的对象。 主要有两个注解: @Konvertable 生成裁剪的实体以及对应的转换方法 @Konvert 单独针对某个类生成转换方法 废话不多说来看怎么使用。 怎么用 1. 引入依赖 // for build.gradle.kts repositories { maven("https://jitpack.io") } dependencies { kapt("com.github.lexcao:konverter:master-SNAPSHOT") implementation("com.github.lexcao:konverter-annotation:master-SNAPSHOT") } // for build.gradle repositories { maven { url 'https://jitpack.io' } } dependencies { kapt 'com.github.lexcao:konverter:master-SNAPSHOT' implementation 'com.github.lexcao:konverter-annotation:master-SNAPSHOT' } 2. 在需要转换的类上加上注解 @Konvertable( To(name = "LoginDTO", pick = ["username", "password"]), To(name = "UserListDTO", omit = ["password"]) ) @Konvert(to = UserVO::class) data class UserEntity( val id: Long, @Konvert.Field("name") val username: String, val password: String, @Konvert.By(GenderEnumConverter::class) val gender: Int ) 3. 生成的代码如下: // @Konvertable /** * Auto generated code by @Konvertable */ data class LoginDTO( val username: String, val password: String ) /** * Auto generated code by @Konvertable */ data class UserListDTO( val id: Long, val username: String, val gender: Int ) /** * Auto generated code by @Konvert */ fun UserEntity.toLoginDTO(username: String = [email protected], password: String = [email protected]): LoginDTO = LoginDTO(username=username,password=password) /** * Auto generated code by @Konvert */ fun LoginDTO.toUserEntity( id: Long = 0L, username: String = [email protected], password: String = [email protected], gender: Int = 0 ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) /** * Auto generated code by @Konvert */ fun UserEntity.toUserListDTO( id: Long = [email protected], username: String = [email protected], gender: Int = [email protected] ): UserListDTO = UserListDTO(id=id,username=username,gender=gender) /** * Auto generated code by @Konvert */ fun UserListDTO.toUserEntity( id: Long = [email protected], username: String = [email protected], password: String = "", gender: Int = [email protected] ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) /** * Auto generated code by @Konvert */ fun UserEntity.toRegisterDTO( username: String = [email protected], password: String = [email protected], gender: Int = [email protected] ): RegisterDTO = RegisterDTO(username=username,password=password,gender=gender) /** * Auto generated code by @Konvert */ fun RegisterDTO.toUserEntity( id: Long = 0L, username: String = [email protected], password: String = [email protected], gender: Int = [email protected] ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) // @Konvert // 转换为如下对象 data class UserVO( val id: String, val name: String, val gender: GenderEnum ) enum class GenderEnum { MALE, FEMALE; } object GenderEnumConverter : Konvert.KonvertBy<Int, GenderEnum> { override fun Int.forward(): GenderEnum { return GenderEnum.values()[this] } override fun GenderEnum.backward(): Int { return this.ordinal } } // 生成的代码 ** * Auto generated code by @Konvert */ fun UserEntity.toUserVO( id: String = [email protected](), name: String = [email protected], gender: GenderEnum = with(GenderEnumConverter) { [email protected]() } ): UserVO = UserVO(id=id,name=name,gender=gender) /** * Auto generated code by @Konvert */ fun UserVO.toUserEntity( id: Long = [email protected](), username: String = [email protected], password: String = "", gender: Int = with(GenderEnumConverter) { [email protected]() } ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) 相关 API 说明 转换规则 ...

四月 13, 2020 · 3 分钟 · Lex Cao

一次线程死锁排查记录

背景 某次发版之后,线上服务低概率出现某台实例接口响应超时,具体表现为: /health 接口超时报警; 线程死锁,Tomcat 线程池吃满; 服务完全无响应。 保留下当前 heap dump 和 thread stack 后,临时重启服务器恢复正常。 # heap dump $ jmap -dump:format=b,file=dump.hprof [pid] # thread stack jstack [pid] > stack.txt 接下来简单记录一下排查结果。 (此文为回忆所写,当时排查的思考细节和过程已省略) 排查 heap dump 使用工具 Eclipse + MAT 安装 # 安装 eclipse $ brew cask install eclipse-java # 安装 eclipse MAT $ brew cask install mat # 是的,这个就是 eclipse-mat 相关连接 Eclipse Eclipse MAT 使用 把 heap dump 文件 dump.hprof 导入到 MAT。 发现有一个名称为 Spring-Async-Scheduler 异步队列占用特别大。【TODO 图片】 ...

三月 21, 2020 · 2 分钟 · Lex Cao

Reactive 概览

Reactive Streams Reactive Streams 在 Netflix 、Pivotal 和 Lightbend 工程师于 2013 年底发起这项计划。 Reactive Streams 是一项提议,为无阻塞背压的异步流提供一个标准。这包括针对运行时环境(JVM 和 JavaScript)以及网络协议上的工作。 你可以在 Reactive Streams 官网网站阅读这个原始规范。 你也可以在 这里 阅读它的中文翻译。 无阻塞背压的异步流处理 Reactive Streams 由以下组成: 异步; 流式; 无阻塞; 背压(回压)。 以下是 Java 接口,你可以在 GitHub 阅读更详细内容。 public interface Publisher<T> { public void subscribe(Subscriber<? super T> s); } public interface Subscriber<T> { public void onSubscribe(Subscription s); public void onNext(T t); public void onError(Throwable t); public void onComplete(); } public interface Subscription { public void request(long n); public void cancel(); } public interface Processor<T, R> extends Subscriber<T>, Publisher<R> { } Reactive Extensions 用于可观察流的异步编程 API。 ReactiveX 是来自观察者模式、迭代器模式以及函数式编程的最佳创意组合。 ...

十一月 29, 2019 · 2 分钟 · Lex Cao