最近使用 Guava 中 RateLimiter.getRate() 方法时遇到了一个反直觉的现象,这里贴出来给大家分享一下。

现象

我们先看下下面这段代码

1
2
3
4
5
public static void main(String[] args) {
RateLimiter rateLimiter = RateLimiter.create(10.0D);
double rate = rateLimiter.getRate();
System.out.println(rate);
}

直觉上最终会输出 10.0,实际最终也是输出了 10.0

可是如果我们将上述代码中的 10.0D 变为 15.0D,结果会输出什么呢?

1
2
3
4
5
public static void main(String[] args) {
RateLimiter rateLimiter = RateLimiter.create(15.0D);
double rate = rateLimiter.getRate();
System.out.println(rate);
}

执行后输出的值竟然是 14.999999999999998。看到这个结果我觉得非常的奇怪,毕竟直觉告诉我应该输出 15.0 才对。

刨根问底

看到这样的现象,我们首先就要从源码上分析一下,看一下 Guava 内部是如何处理的。

阅读全文 »

JaCoCo 简介

JaCoCo should provide the standard technology for code coverage analysis in Java VM based environments. The focus is providing a lightweight, flexible and well documented library for integration with various build and development tools.

JaCoCo 要为基于 Java VM 的环境中的代码提供覆盖率分析标准技术,重点是提供一个轻量、灵活且文档齐全的库,以便与各种构建和开发工具集成。

功能特性

  • 指令(C0)、分支(C1)、行、方法、类型、圈复杂度的覆盖率分析;
  • 基于 Java 字节码,即使没有源码依然可以运行;
  • 支持 on-the-fly 模式,可以通过 javaagent 便捷地集成,也可以通过 API 来自定义类装载器进行集成;
  • 与框架无关,可以对基于 Java VM 的应用程序顺利的集成;
  • 与已所有已发布的 Java 版本兼容;
  • 支持不同的 JVM 语言;
  • 可以生成多种格式的报告(HTML、XML、CSV);
  • 支持远程协议和 JMX 控制,可以在任何时间点来进行覆盖率数据的存储;

使用

下载完 JaCoCo 之后会得到几个 jar 包,其中 jacocoagent.jar 和 jacococli.jar 是我们比较关注的。

  • jacocoagent.jar 是 JVM 应用程序启动时的代理
  • jacococli.jar 是生成覆盖率报告、合并 dump 文件的命令行工具

jacocoagent

Java Agent 的原理可以阅读 java.lang.instrument.Instrumentation 这个类,这里先介绍 jacocoagent 的使用方法。

阅读全文 »

最近在做基于 JaCoCo 的代码覆盖率工具,了解到了一下 javaagent 相关的一些知识点。

javaagent 是什么?

javaagent 是 java 命令的一个参数选项,可以用于加载 Java 语言的代理。

那么这个参数加载的 Java 语言的代理需要满足什么样的规范呢?

  1. 代理 jar 包中的 MANIFEST.MF 文件需指定 Premain-Class;
  2. Premain-Class 指定的类中必须实现 premain 方法。

zsh 可能是目前最好用的终端。

安装

Mac OS

MacOS 自带 zsh,并且当前最新版本 MasOS 默认终端即为 zsh

Ubuntu

使用 sudo apt install zsh 命令即可安装 zsh

CentOS

使用 sudo yum install zsh 命令即可安装 zsh

在安装好 zsh 之后,第一次使用的时候,会有如下的提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
This is the Z Shell configuration function for new users,
zsh-newuser-install.
You are seeing this message because you have no zsh startup files
(the files .zshenv, .zprofile, .zshrc, .zlogin in the directory
~). This function can help you with a few settings that should
make your use of the shell easier.

You can:

(q) Quit and do nothing. The function will be run again next time.

(0) Exit, creating the file ~/.zshrc containing just a comment.
That will prevent this function being run again.

(1) Continue to the main menu.

(2) Populate your ~/.zshrc with the configuration recommended
by the system administrator and exit (you will need to edit
the file by hand, if so desired).

--- Type one of the keys in parentheses ---
阅读全文 »

使用 Spring 做项目的先思考一个问题,一个接口有在多个实现类的情况下,在成员变量的声明上如果没有指定注入那个别名的 Bean 的时候 Spring 会如何选择对应的 Bean 来进行注入?

问题模拟

先来简单说明一下示例代码。

定义一个接口,用于打印当前注入的 Bean 的名称

1
2
3
public interface TestService {
void printBeanName();
}

针对上面定义的接口编写两个实现类,并分别定义其 beanName 为 testService 和 testService2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service("testService")
public class TestServiceImpl implements TestService {

@Override
public void printBeanName() {
System.out.println("testService");
}
}

@Service("testService2")
public class TestServiceImpl2 implements TestService {

@Override
public void printBeanName() {
System.out.println("testService2");
}
}

编写 Handler,并注入 TestService

1
2
3
4
5
6
7
8
9
10
@Service
public class TestHandler {

@Autowired
private TestService testService;

public void test() {
testService.printBeanName();
}
}

编写单元测试,并观察输出

阅读全文 »

线上业务使用了 Guava 中的 RateLimiter 来做单机的限流。最近运维同事反馈,在修改了限流的配置之后,请求频率没有超过限流的配置却触发了限流逻辑,在此对该问题进行一下分析与总结。

问题模拟

根据上述问题的现象,初步推测问题可能跟 Guava 中 RateLimiter 的限流算法实现有关,为了方便排查问题,参考线上业务的限流的逻辑写了一个 demo,其核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@RestController
@RequestMapping("/hello")
public class HelloController {

/** 用于存储不同的接口,对应的 RateLimiter **/
private static Map<String, RateLimiter> RATE_LIMITER_MAP = Maps.newConcurrentMap();
/** 用于存储不同接口限流配置的参数 **/
private static Map<String, String> RATE_MAP = Maps.newConcurrentMap();

static {
// 初始化一个接口默认的限流配置
RATE_MAP.put("loan", "100000");
}

/**
* 测试限流的接口
* @param key 限流配置的 key
*/
@GetMapping("/testLimit")
public String testLimit(String key) {
String value = RATE_MAP.get(key);
logger.info("自定义限流配置={}", value);
Double currentRate = Double.parseDouble(value);

RateLimiter rateLimiter = RATE_LIMITER_MAP.get(key);
if (rateLimiter == null || !isSameRate(currentRate, rateLimiter.getRate())) {
logger.info("创建新的RateLimiter|速率={}", currentRate.toString());
rateLimiter = RateLimiter.create(currentRate);
RATE_LIMITER_MAP.putIfAbsent(key, rateLimiter);
}
boolean allowed = rateLimiter.tryAcquire();
logger.info("是否允许当前请求通过={}|rate={}", allowed, rateLimiter.getRate());
return allowed ? "SUCCESS" : "FAIL";
}

/**
* 模拟修改限流配置
* @param key 限流配置的key
* @param rate 限流的值
*/
@GetMapping("/setRate")
public String setRate(String key, String rate) {
RATE_MAP.put(key, rate);
String currentValue = RATE_MAP.get(key);
logger.info("设置完之后的值={}", currentValue);
return "SUCCESS";
}

}

启动工程,我们模拟一下出现问题时的操作。

  1. 构造 HTTP 请求调用 /setRate 接口,将 loan 对应的限流的值修改为 5;
  2. 构造 HTTP 请求快速调用 /testLimit 接口 2 次,发现第一调用接口返回 SUCCESS, 第二次接口返回 FAIL,说明第 2 次请求触发了限流;
  3. 构造 HTTP 请求调用 /testLimit 5 次,保证 5 次请求在 1 秒内接收,发现这 5 次请求接口均返回 SUCCESS,说明这 5 次请求均未触发限流;
  4. 构造 HTTP 请求调用 /testLimit 6 次,保证 6 次请求均在 1 秒内接收,发现前 6 次请求接口均返回 SUCCESS,说明这 6 次请求均未触发限流;
  5. 构造 HTTP 请求调用 /testLimit 7 次,保证 7 次请求均在 1 秒内接收,发发现前 6 次请求接口均返回 SUCCESS,第 7 次请求接口返回 FAIL,说明前 6 次请求均未触发限流,第 7 次请求触发限流。

至此,我们模拟的现象与线上一致。步骤 3 是我们预期的结果,步骤 2 中在未超过限流配置的情况下触发了限流,步骤 4、5 中的第 6 次请求在超过了限流配置的情况下,仍然允许通过,我们直观上认为这些是不合理的。

排查与分析

Guava 中 RateLimiter 提供了两种令牌桶的算法实现:

  • 平滑突发限流(SmoothBursty)
  • 平滑预热限流(SmoothWarmingUp)
阅读全文 »

近期收到一些小伙伴问 Java 中如何调用 https 中双向认证的接口的问题,本文采用 okhttp 以及 Java11 中的 httpclient 来介绍如何使用 Java 代码调用 https 双向认证的接口。

前置工作

  1. 生成服务端、客户端证书,并将各自的公钥加入到对方的信任链中以供后续的使用;
  2. 搭建一个 https 协议的服务以供客户端调用。

制作证书

这里我们采用 jdk 提供的 keytool 命令来生成我们所需要的服务端、客户端证书,并且可以使用 keytool 命令来将各自的公钥加入到对方的信任连中。

生成服务端、客户端密钥库以及证书

1
2
3
4
# 生成服务端keystore,这里加拓展项的目的是为了说明域名验证的问题
keytool -genkey -validity 3650 -alias server -keyalg RSA -keystore server.keystore -ext san=dns:www.goku.edu,ip:127.0.0.1
# 生成客户端keystore,这里针对客户端的密钥库我们采用 PKCS12 的格式
keytool -genkey -validity 3650 -alias client -keyalg RSA -storetype PKCS12 -keystore client.p12

导出服务端、客户端证书中的公钥

1
2
3
4
# 导出服务端证书的公钥
keytool -export -keystore server.keystore -alias server -file server.cer -rfc
# 导出客户端证书的公钥
keytool -export -keystore client.p12 -storetype PKCS12 -alias client -file client.cer

将各自的公钥加入到对方的密钥库中

阅读全文 »

配置 npmmirror 镜像

  1. 备份旧镜像

在替换 npmmirror 镜像之前,我们先查看一下原有的镜像

1
npm get registry

结果为 https://registry.npmjs.org/

1
yarn get registry

结果为 https://registry.yarnpkg.com

  1. 替换为 npmmirror 镜像
1
2
3
4
# 设置 npm 的源为 npmmirror
npm config set registry https://registry.npmmirror.com
# 设置 yarn 的源为 npmmirror
yarn config set registry https://registry.npmmirror.com

升级版本

阅读全文 »

在日常开发中,如果我们一不小心删除了某个分支,又想重新使用这个分支,该怎么找回该分支上的代码呢?

场景还原

  1. 有一个 git 仓库,从 master 分支新建 feature/delete_branch 分支,并进行一次提交,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 新建一个 feature/delete_branch 分支
git branch feature/delete_branch
# 切换到 feature/delete_branch 分支
git checkout feature/delete_branch
# 新建一个文件,进行一次提交并推送至远程分支
git add delete-file.md
git commit -m ':sparkles: add a delete-file.md file'
git push origin feature/delete_branch:feature/delete_branch

# 查看当前分支图
git log --all --decorate --oneline --graph
* 88ac246 (HEAD -> feature/delete_branch, origin/feature/delete_branch) :sparkles: add a delete-file.md file
* 8693480 (origin/master, master) :sparkles: add a hello-git.md file
  1. 切换到 master 分支,并删除 feature/delete_branch 分支
1
2
3
4
5
6
7
8
9
# 切换到 master 分支
git checkout master
# 删除 feature/delete_branch 分支
git branch -d feature/delete_branch
# 将删除的分支同步只至远程
git push origin --delete feature/delete_branch
# 此时无论是本地还是远程都已经删除了 feature/delete_branch
git log --all --decorate --oneline --graph
* 8693480 (HEAD -> master, origin/master) :sparkles: add a hello-git.md file

还原删除的分支

  1. 通过 git refloggit log 命令找到我们需要恢复的 commit id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 这里演示 git reflog 命令展示出来的提交
git reflog
8693480 (HEAD -> master, origin/master) HEAD@{0}: checkout: moving from feature/delete_branch to master
88ac246 (feature/delete_branch) HEAD@{1}: commit: :sparkles: add a delete-file.md file
8693480 (HEAD -> master, origin/master) HEAD@{2}: checkout: moving from master to feature/delete_branch
8693480 (HEAD -> master, origin/master) HEAD@{3}: commit (initial): :sparkles: add a hello-git.md file

# 这里演示 git log 命令 展示出来的提交
git log -g
commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -> master, origin/master)
Reflog: HEAD@{0} (fuyongde <fuyongde@foxmail.com>)
Reflog message: checkout: moving from feature/delete_branch to master
Author: fuyongde <fuyongde@foxmail.com>
Date: Thu Mar 26 18:36:35 2020 +0800

:sparkles: add a hello-git.md file

commit 88ac246020a0cc25071fb7fd3ac88363f4bf2beb (feature/delete_branch)
Reflog: HEAD@{1} (fuyongde <fuyongde@foxmail.com>)
Reflog message: commit: :sparkles: add a delete-file.md file
Author: fuyongde <fuyongde@foxmail.com>
Date: Thu Mar 26 18:44:01 2020 +0800

:sparkles: add a delete-file.md file

commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -> master, origin/master)
Reflog: HEAD@{2} (fuyongde <fuyongde@foxmail.com>)
Reflog message: checkout: moving from master to feature/delete_branch
Author: fuyongde <fuyongde@foxmail.com>
Date: Thu Mar 26 18:36:35 2020 +0800

:sparkles: add a hello-git.md file

commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -> master, origin/master)
Reflog: HEAD@{3} (fuyongde <fuyongde@foxmail.com>)
Reflog message: commit (initial): :sparkles: add a hello-git.md file
Author: fuyongde <fuyongde@foxmail.com>
Date: Thu Mar 26 18:36:35 2020 +0800

:sparkles: add a hello-git.md file
  1. 通过 git branch [branch name] [commit id] 命令来恢复删除的分支
阅读全文 »

很久没有搞过 Android 开发了,Android 的命令行安装方式相比之前发生了一些变化,这里针对安装最新的 Android 命令行时遇到的一些问题,记录一下解决方法。

问题:下载最新的 commandlinetools 在自己指定的目录安装之后,无法使用

原因:最新版本的 commandlinetools 对安装的目录是有要求的

  1. Download latest Command line tools from android i.e. commandlinetools-win-6200805_latest.zip
  2. Unzip the downloaded file
  3. Create directory for storing commandline tools somewhere on your disk, with following path included: android/cmdline-tools/latest Basically when You unzip this Cmd line tools, just rename tools directory to latest and make sure You put this latest folder in android/cmdline-tools directory somewhere on your disk
  4. Create ANDROID_HOME environment variable for directory that stores the cmdline tools directory location like: C:\YourLocationWhereYouStoreTheDirectory\android\cmdline-tools\latest
  5. Create new entry in Path environment variable as %ANDROID_HOME%\bin