最近在实验一个用 GraphQL 支持 REST 接口的方案,需要在 Java 里写大量的 graphql 的内容,如下所示。
@RestController
@AllArgsConstructor
class GraphQLTest {
private DgsQueryExecutor dgsQueryExecutor;
@GetMapping("/g")
public ResponseEntity<?> test() {
String query =
"query {\n" +
" plans(category: [PERMANENT_COMPUTATION]) {\n" +
" id\n" +
" ... on TimeBoxedComputationPlan {\n" +
" resource {\n" +
" name\n" +
" gpu {\n" +
" ... on PhysicalGPU {\n" +
" type\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
ExecutionResult result = dgsQueryExecutor.execute(query);
return ResponseEntity.ok(result.getData());
}
}
可以看到这种多行拼接的 string 非常丑陋,并且没办法使用 intellij 的 inject language
这样的功能。
不过在 java 15 就已经引入了 text blocks 的语法了,并且在目前的情况下,引入这个语法确实会为后续开发提供不少便利。于是决定就直接从目前的 java 11 升级到最近的 LTS 版本 java 17 了。这里就记录下更新版本需要处理的问题。
去 azul 官网安装对应的操作系统的版本。
安装后在命令行输入命令确认安装成功了:
$ java --version
openjdk 17.0.3 2022-04-19 LTS
OpenJDK Runtime Environment Zulu17.34+19-CA (build 17.0.3+7-LTS)
OpenJDK 64-Bit Server VM Zulu17.34+19-CA (build 17.0.3+7-LTS, mixed mode, sharing)
打开 idea intellij 对配置做如下修改。
java 17 相对比较新,有些 gradle 的插件没有做很好的适配,需要做一些修改。我这里遇到了两个插件的问题,这里也记录下。
spotless 是做代码格式化的,里面用了 googleJavaFormat 似乎有一些兼容性问题。这里没有花太多时间做调研,只是跟着 issue 做了调整。
根据这个 PR 将 gradle.properties
修改如下:
org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
build.gradle
里更新 google java format 到最新版本spotless {
java {
target project.fileTree(project.rootDir) {
include '**/*.java'
exclude 'build/generated/**/*.*', 'build/generated-examples/**/*.*'
}
toggleOffOn('@formatter:off', '@formatter:on')
+ googleJavaFormat('1.15.0')
}
}
按照 https://github.com/n0mer/gradle-git-properties/issues/171#issuecomment-817569604 对 gradle.properties
做修改:
org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
+ --add-opens java.base/java.io=ALL-UNNAMED
做了如上更新后,本地跑 java 的项目就不报错了,不过还是有一些 warning 后续慢慢处理。
text blocks 就能用了,效果如下:
又发现了集群里出现了挂掉的 nvidia gpu 这里记录下如何屏蔽掉它以保证其他 GPU 可以继续被使用。
首先是监控告警,告知 nvidia-smi
命令出错了,去机器上看一下有这么个错误:
$ nvidia-smi
Unable to determine the device handle for GPU 0000:89:00.0: Unknown Error
感觉是这块卡 0000:89:00.0
出问题了。然后去执行下 dmesg
看看情况:
$ dmesg -T
[Mon May 9 20:37:33 2022] xhci_hcd 0000:89:00.2: PCI post-resume error -19!
[Mon May 9 20:37:33 2022] xhci_hcd 0000:89:00.2: HC died; cleaning up
[Mon May 9 20:37:34 2022] nvidia-gpu 0000:89:00.3: i2c timeout error ffffffff
[Mon May 9 20:37:34 2022] ucsi_ccg 6-0008: i2c_transfer failed -110
看不懂,搜了搜也有点懵逼。这台机器已经运行了挺久了,驱动也在短期内没有出过问题,那么就感觉是硬件出问题了。重启机器后,nvidia-smi
恢复了。不过如果有其他任务使用了 GPU 就又会出现这个问题了。所以考虑使用 nvidia-smi
的命令先屏蔽掉这块报错的 GPU 。
$ nvidia-smi drain -p 0000:89:00.0 -m 1
Successfully set GPU 00000000:89:00.0 drain state to: draining.
然后再执行命令 nvidia-smi
就看不到这块卡了。
最近在做一些代码的重构和基础库的迁移,这样的工作绝大部分时候不产生新的功能点,每次更换了类库后也都会将原来对应的测试同步迁移过来,保证新的代码和原来的代码一样工作。不过在迁移的过程中我发现 jacoco 所提示的代码覆盖率越来越低,让我很慌。为了搞明白这是啥原因,做了一些调研,这里把一些结论记录在这里加深印象,也便于后续查看。
在 Intro to JaCoCo 这里讲的非常明白了,代码测试覆盖率(或者说代码覆盖率)讲的是在跑测试的时候,到底有多少代码被执行了。按照粒度来分可以有以下几种:
Line coverage
按照字节码统计的多少 instruction 被执行了Branch converage
按照 if/else
switch
等分支统计多少分支被执行了由于代码确实会有很多防御性的 if/else
导致其实每一个分支并不是很对等,所以我个人感觉 Line coverage
会稍微好一些。
不过要注意,测试覆盖率只反映你多少代码在测试的时候被执行了,执行了一次就算是执行了,但事实上不同的参数会导致不同的结果,很多边界条件是否被测试到也表现不出来。因此 100% 测试覆盖率不表示所有代码都是对的了。如何写测试本身是一件极其复杂的事情,有些编程思路如 TDD 都是围绕测试进行的,我也讲不明白。
为了搞明白为什么我的测试覆盖率一降再降,我需要用 JaCoCo 给我生成一下报告,我去看一下到底哪些地方的哪些代码测试出了问题。
我的项目是用 gradle
管理的,执行如下命令重新跑一下测试并生成报告:
./gradlew cleanTest test jacocoTestReport
然后去项目目录 build/reports/jacoco/test/html
打开 index.html
看看情况,发现核心为题在于很多生成的代码被纳入了代码测试覆盖中。具体来讲有两个方面的内容:
Entity
生成的 Q + Entity
的名称的代码那问题就显而易见了,尤其是后者,每次迁移一个 JPA 的 Entity 类型就会对应生成一段 Q + Entity
的代码,这部分代码统统成了测试覆盖率的分母,覆盖率能不低么。
这部分的很多信息在 Exclusions from Jacoco Report 可以找到。
我们只需要测试自己写的代码,生成的代码不应纳入统计范围。这里直接粘贴下 gradle
里面 jacocoTestReport
的配置:
jacocoTestReport {
// rule from https://bottom-to-top.tistory.com/36
// 过滤 QA-QZ 开头的所有类,对应了 querydsl 生成的 Q + Entity 的格式
def Qdomains = []
for(qPattern in "**/QA" .. "**/QZ"){
Qdomains.add(qPattern + "*")
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
'com/openbayes/graphql/**', // 所有 graphql 生成的代码
'com/openbayes/application/data/**', // 所有的 DTO 所在的包
'db/migration/**', // 所有的数据库 migration 代码
'**/*Exception*', // 所有包含 Exception 的异常类
'**/*Mixin*', // 所有 Jackson Mixin
'**/*Command', // 所有带 Command 的类,也是 DTO
] + Qdomains)
}))
}
reports {
csv.required = true
}
}
可以看到,我这里主要是按照 classDirectory
对类进行了过滤,包括了以下内容:
*Command
data/**
等对 querydsl 这部分的过滤比较 tricky ,因为其默认生成的名字是 Q + Entity
,本身过滤就有点难,如果采用 **/Q*
的形式会导致个别以 Q
开头但不是 querydsl 生成的类也被纳入过滤的范围,这里我采用的是 https://bottom-to-top.tistory.com/36 的方法,过滤掉由 QA-QZ
开头的所有的类。
这里过滤 querydsl 的方法其实官方还有其他方案,但目前
gradle
这边支持不是很好,我目前用的querydsl
版本为 5.0 有一些 PR 还没有纳入进来,后续如果官方有了更好支持会考虑做相应的调整。
JaCoCo 本身是考虑了要过滤掉生成的代码的,它提供了一个规则:
Starting from JaCoCo 0.8.2, we can exclude classes and methods by annotating them with a custom annotation with the following properties:
- The name of the annotation should include Generated.
- The retention policy of annotation should be runtime or class.
简单的翻一下,就是在 JaCoCo 0.8.2 后,通过提供一个带有 Generated
名称的 RetentionPolicy
为 CLASS
或者 RUNTIME
的注解,JaCoCo 会帮你自动过滤这些类。
Lombok 也对这部分做了支持,只要提供一个配置 lombok.config
就能让 Lombok 给自己生成的代码添加上相应的注解了:
lombok.addLombokGeneratedAnnotation = true
在做了上述两方面的修改后,测试覆盖率重回 70% 了。