使用 VisualVMVirsual GC 插件需要先和服务器建立 jstatd 连接,在 JDK 9 之前需要首先创建一个 policy 文件并声明权限:

1
2
3
grant codebase "file:${java.home}/lib/tools.jar" {
permission java.security.AllPermission;
};

然而,从 JDK 9 开始,tools.jar 已被移除,需要将 policy 文件的内容修改为:

1
2
3
4
5
6
7
grant codebase "jrt:/jdk.jstatd" {    
permission java.security.AllPermission;
};

grant codebase "jrt:/jdk.internal.jvmstat" {
permission java.security.AllPermission;
};

参考:

使用 AppVeyorVisual Studio 2019 镜像构建 Java 项目时默认使用的是 JDK 1.8这里说明了 AppVeyor 各个镜像下默认使用的 JDK 版本,虽然表格里写着 Visual Studio 2019 镜像下的默认 JDK 是1.7,不过实际是1.8),如果想更换 JDK 版本,比如更换为 JDK 11,可以重新设置 JAVA_HOMEPATH

1
2
3
before_test:
- SET JAVA_HOME=C:\Program Files\Java\jdk11
- SET PATH=%JAVA_HOME%\bin;%PATH%

完整的代码可参考 GitHub

假设有如下的 SumService

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SumService {
public static int sum(int a, int b) {
if (a <= 0) {
throw new IllegalArgumentException("a must be positive");
}

if (b <= 0) {
throw new IllegalArgumentException("b must be positive");
}

return a + b;
}
}

a 或者 b 非正数时会抛出 IllegalArgumentException 异常,由于两者抛出的是同一个异常,所以无法直接使用 expected = IllegalArgumentException.class 进行区分测试,故需要测试具体的异常信息。

使用 try/catch

用一个 try/catch 包裹测试的方法,判断抛出的异常信息:

1
2
3
4
5
6
7
@Test
public void shouldAssertExceptionMessageByAssertThrows() {
IllegalArgumentException illegalArgumentException =
Assert.assertThrows(IllegalArgumentException.class, () -> SumService.sum(0, 1));

Assert.assertEquals("a must be positive", illegalArgumentException.getMessage());
}

使用 assertThrows

借助 Assert.assertThrows 执行测试方法返回一个异常,然后判断返回的异常信息:

1
2
3
4
5
6
@Test
public void shouldAssertExceptionMessageByAssertThrows() {
IllegalArgumentException illegalArgumentException = Assert.assertThrows(IllegalArgumentException.class, () -> SumService.sum(0, 1));

Assert.assertEquals("a must be positive", illegalArgumentException.getMessage());
}

使用 ExpectedException

借助 ExpectedException 预先设定预期抛出的异常和异常信息,然后执行测试方法:

1
2
3
4
5
6
7
8
9
10
@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void shouldAssertExceptionMessageByRule() {
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("a must be positive");

SumService.sum(0, 1);
}

完整的代码可参考 GitHub

参考

问题

对于以下的异步代码:

1
2
3
4
5
public class DemoService {
public CompletableFuture<String> hello() {
return CompletableFuture.supplyAsync(() -> "hello");
}
}

我们为其编写一个测试用例,并在 CompletableFuture#whenComplete 中判断返回值是否与预期相符,然而即使返回值与预期不符,该测试也不会抛出异常:

1
2
3
4
5
6
7
8
9
@Test
public void exceptionWontBeCaptured() {
DemoService demoService = new DemoService();

demoService.hello()
.whenComplete((result, e) -> {
Assert.assertEquals("wrongValue", result);
});
}

解决

CompletableFuture#get()

我们可以借助 CompletableFuture#get() 阻塞主线程等待结果的特点,将异步代码转成同步:

1
2
3
4
5
6
@Test
public void blockMainThreadByGet() throws ExecutionException, InterruptedException {
DemoService demoService = new DemoService();

Assert.assertEquals("hello", demoService.hello().get());
}

CountDownLatch

上述方案依赖了一个具体的异步类方法,如果实际的异步类不提供相应的同步方法,上述方案则不适合。针对这种情况,可以借助 CountDownLatch,初始化一个计数为1的 CountDownLatch 的实例,在测试方法中调用 CountDownLatch#await() 方法进行等待,当异步方法执行成功后在其回调中调用 CountDownLatch#countDown() 使计数器减1变为0,从而继续执行后续的测试判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void waitOnCountDown() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
DemoService demoService = new DemoService();
AtomicReference<String> actualValue = new AtomicReference<>("");

demoService.hello()
.whenComplete((result, e) -> {
actualValue.set(result);
countDownLatch.countDown();
});

countDownLatch.await();

Assert.assertEquals("hello", actualValue.get());
}

Awaitility

Awaitility 让测试异步代码变得简单明了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void poweredByAwaitility() {
DemoService demoService = new DemoService();
AtomicReference<String> actualValue = new AtomicReference<>("");

demoService.hello()
.whenComplete((result, e) -> {
actualValue.set(result);
});

await().atMost(5, SECONDS).untilAsserted(() -> {
Assert.assertEquals("hello", actualValue.get());
});
}

完整的代码可参考 GitHub

参考

问题

首先来看一个例子,构建一个 C 语言版的 hello world 镜像:

1
2
3
4
5
/* hello.c */
int main () {
puts("Hello, world!");
return 0;
}

对应的 Dockerfile 为:

1
2
3
4
FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]

然后执行 docker build -t hello-world . 构建一个名为 hello-world 的镜像,然而以这种方式构建的镜像的大小竟然有1.19 GB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest b11e170bd1d2 6 minutes ago 1.19GB

因为这种构建方式生成的镜像会同时包含 gcc 镜像的内容,查看 gcc 镜像大小发现达到了1.19 GB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
gcc latest 21f378ba43ec 11 days ago 1.19GB

如果我们把基础镜像换成 Ubuntu 并安装 gcc 编译 hello.c 重新构建镜像,最后的镜像大小为213 MB:

1
2
3
4
5
FROM ubuntu
COPY hello.c .
RUN apt-get update && apt-get install gcc -y
RUN gcc -o hello hello.c
CMD ["./hello"]
1
2
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
hello-world latest 42f17d1d12a5 About a minute ago 213MB

虽然新镜像相比1.19 GB有大幅减少,但相比于 hello-world 程序本身的大小(17k)来说,213 MB依然是个庞大的数字:

1
2
$ ls hello -hl
-rwxr-xr-x 1 root root 17K Aug 4 13:54 hello

解决

Multi-stage

对于 hello-world 这个镜像来说,我们真正需要的只是最终的可执行程序,而并不关心中间的编译过程,如果能将编译阶段作为一个临时阶段而并不包含在最终的镜像中,则可有效减少最终的镜像大小。针对此,Docker 在 17.05 版本开始提供了名为 multi-stage 构建的功能。我们将原来的 Dockerfile 稍作修改,将原来的编译阶段抽取为一个 stage,然后将编译好的可执行文件复制到最终的 stage 中:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=mybuildstage hello .
CMD ["./hello"]

最终的镜像大小只有73.9 MB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest 7dd2b51c53b2 7 minutes ago 73.9MB

FROM scratch

在上一步中,我们使用 Ubuntu 作为基础镜像来运行 hello-world,相比于一个可执行程序,Ubuntu 依然过于庞大,有没有比 Ubuntu 更轻量的镜像呢?有,那就是 scratch,这表示一个空的镜像,继续将 Dockerfile 稍作修改:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM scratch
COPY --from=mybuildstage hello .
CMD ["./hello"]

最终的镜像大小只有16.4 KB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest 676253b0e9c4 31 minutes ago 16.4kB

不过在运行该镜像时却提示错误:

1
standard_init_linux.go:211: exec user process caused "no such file or directory"

这是因为这种方式构建出的镜像缺少 hello-world 运行时依赖的库。我们可以在编译 hello-world 时通过指定 -static 参数将依赖的库包含到最后的可执行文件中来解决这个问题:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c -static
FROM scratch
COPY --from=mybuildstage hello .
CMD ["./hello"]

不过包含了依赖的库后最终镜像的大小也上涨为945 KB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest e6a1fccc2de7 9 seconds ago 945kB

另外,如果不想将依赖的库包含到最终的镜像中,可以使用 busybox:glibc 这个基础镜像,该镜像包含了 C 语言的标准库,有了这个镜像在编译 hello-world 时则无需指定 -static 参数:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM busybox:glibc
COPY --from=mybuildstage hello .
CMD ["./hello"]

不过由于该镜像本身有一定大小,最终镜像的大小达到了5.22 MB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest e2f2c0544800 7 seconds ago 5.22MB

总结

通过 multi-stage 构建可以有效的减少 Docker 镜像的大小,而基础镜像的选择则要具体情况分析,在满足需求的情况下选择合理的基础镜像。

参考

Finder 默认并不显示隐藏文件,可以通过如下两种方式开启:

  1. 在终端中输入 defaults write com.apple.finder AppleShowAllFiles YES,然后重启 Finder
  2. Finder 中使用快捷键 Shift + Command + "."

参考:

An enum switch case label must be the unqualified name of an enumeration constantJava 中常见的编译错误,基本上 Google 搜索出来的错误场景都是因为在 switch 中使用枚举时搭配了类名造成,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Season season = Season.SPRING;

switch (season) {
// 编译错误,直接使用 SPRING 即可
case Season.SPRING:
System.out.println("spring");

break;
case Season.SUMMER:
System.out.println("summer");

break;
default:
break;
}

然而,如果某个枚举值不存在,也会提示一样的错误,例如:

1
2
3
4
5
6
7
8
9
10
Season season = Season.SPRING;

switch (season) {
case SOME_VALUE_DOES_NOT_EXIST:
System.out.println("spring");

break;
default:
break;
}

这种情况下的错误提示容易让人摸不着头脑,IntelliJ IDEA 的错误提示则较为友好:Cannot resolve symbol 'SOME_VALUE_DOES_NOT_EXIST'。对于这种错误场景,实际工作中遇到一个例子:

  1. 在开发阶段,A 拉了个 some.package 的分支,更新版本号为 a.b-SNAPSHOT 并发布,将其引入 some.app,推送代码后触发了 some.appJenkins 构建任务
  2. B 也拉了个 some.package 的分支,同样更新版本号为 a.b-SNAPSHOT 并发布,并增加了一个新的枚举值到 SomeEnum,同样将其引入 some.app,推送代码后触发了 some.appJenkins 构建任务,此时任务构建失败,提示编译错误:An enum switch case label must be the unqualified name of an enumeration constant

出现这样的原因是 Jenkins 执行构建任务时执行的编译命令是 mvn compile,在 A 提交任务时,构建服务器下载了 some.packagea.b-SNAPSHOT 版本,由于是 SNAPSHOT 版本,在 B 提交任务时,构建服务器没有重新下载 some.package,导致服务器中的 some.package 没有 B 新增的修改,从而出现编译错误,解决方法是在编译时增加 -U 参数来强制更新 SNAPSHOT

参考:

假设我们希望将 server1 下的 /data1 目录中的数据同步到 server2 下的 /data2 目录,首先需要建立 server1server2 的免密登陆,在 server1 上执行 ssh-keygen,默认情况会在 ~/.ssh 目录下生成 id_rsaid_rsa.pub 两个文件,然后将 ~/.ssh/id_rsa.pub 文件的内容复制到 server2~/.ssh/authorized_keys 文件中即可。

接着,就可以使用 rsync 进行数据同步,具体命令为 rsync -az --delete /data1/ server2-user@server2-ip:/data2,其中 -a 表示递归同步 /data1 下的子文件夹及保留文件的权限、组、软连接等信息,如果不需要这些额外的文件信息而只想要递归同步可以使用 -r 来代替 -a-z 表示开启文件压缩来减少网络传输;--delete 表示在 /data1 中删除的文件在 /data2 中也会同步删除。最后需要注意命令中 /data1/ 末尾的 /,加了 / 表示将 /data1 下的所有文件同步到 /data2,没有 / 则表示将 /data1 这个文件夹同步到 /data2 下,假设 /data1 下有 abc 三个文件,两种写法最后的同步区别为:

  • /data1//data2/a,b,c
  • /data1/data2/data1/a,b,c

最后,我们需要将 rsync 加入到定时任务中进行自动备份。执行 crontab -e,将定时任务添加到文件中,如每小时执行一次:0 * * * * rsync -az --delete /data1/ server2-user@server2-ip:/data2

参考:

0%