Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

arthas是阿里开源的一个非常好用的java诊断工具,提供了很多很好用的命令,这里讲一个最近使用到的
就是将arthas挂载上我们的springboot应用,然后调用其中的方法,这样能够在如果没加日志已经看不到函数返回时更方便的排查问题
首先举个例子,我们有个Controller
它的一个query方法是这样的

1
2
3
4
5
@RequestMapping(value = "/query", method = RequestMethod.GET)
@ResponseBody
public String query() {
return demoService.queryName("1");
}

而在这个demoService中它的实现是这样

1
2
3
4
5
6
7
public String queryName(String no) {
if ("1".equals(no)) {
return "no1";
} else {
return "no2";
}
}

假如现在Controller这的这个方法有点问题,那么我想确认下是不是demoService这个方法的实现有问题,或者说确定下它的返回值是否符合预期
那么我们就可以在应用启动后,运行arthas,找到这个应用的进程,进行挂载
然后执行

1
vmtool --action getInstances --className com.nicksxs.spbdemo.service.DemoServiceImpl --express 'instances[0].queryName("1")'

先介绍下这个vmtool命令
主要来说 vmtool 可以利用JVMTI接口,实现查询内存对象,强制 GC 等功能。
例如官方示例里的,我想把内存里的string对象捞一些出来看看存的是啥

1
vmtool --action getInstances --className java.lang.String --limit 10

就可以这样,首先这个action就是指定要做的操作,支持的action 还包括

1
2
forceGc
interruptThread

等,那么对于 getInstances 就是从内存里捞出这个类的对象,然后是后面一部分
--express 就是执行表达式,这里的表达式,
instances[0].queryName("1") 其中 instances 就是前面从内存中获取的对象数组,因为这些是对象的非静态方法,那就需要从其中取一个来执行我们的方法
另外假如我们的场景里需要对比如返回结果做个json序列化
我们可以这样

1
vmtool --action getInstances --className com.nicksxs.spbdemo.service.DemoServiceImpl --express '@com.alibaba.fastjson.JSON@toJSONString(instances[0].queryName("1"))'

这里为什么类开头跟方法开头要用 @, 是因为对于类和静态方法的调用规则是这样,还有如果代码比较多,有可能默认的类加载器中没有加载这个JSON类,那么就需要在参数中加上指定的classloader,
可以用sc命令来查找我们的目标类的类加载器,一般来说如果目标类是我们核心业务的,大概率也会有JSON这个类

1
sc -d com.nicksxs.spbdemo.service.DemoServiceImpl

然后在上面命令中加上sc结果中的 classLoaderHash 的值,

1
vmtool --action getInstances -c 18b4aac2 --className com.nicksxs.spbdemo.service.DemoServiceImpl --express '@com.alibaba.fastjson.JSON@toJSONString(instances[0].queryName("1"))'

这样就能正常执行了

之前发现Termux在安卓手机中是个比较厉害的神器,就相当于一个随身携带的小型服务器,伴随着现在手机性能的逐渐强大,有些手机可能已经比很多个人用的云服务器还要强大很多,只是有着散热和电量的限制,但是充当一下临时的使用还是很不错的
记得之前提过,在用rustdesk的时候,如果被连端是Mac,并且被连端进入锁屏了,可能需要ssh连上去执行个唤醒命令,正好最近有需求要偶尔从手机连上家里的Mac,从而引发的一个问题是比如我经常用Termux来执行ssh命令,那么理论上我直接从shell的history就可以找到执行过的ssh命令,也就不用经常记对应的ip啥的,但是试用了下发现不行,一开始搜了下以为是没有设置历史存储的大小,可能默认是0,那么需要通过配置来设置

1
2
HISTSIZE=1000
HISTFILESIZE=2000

比如设置1000个命令大小,只是在Termux里不是这个原因,而是在于shell去保存历史的时机,安卓手机在退出应用或者切换应用程序都没法记录这个执行历史,而是需要在Termux中手动地用exit命令退出,才会记录下history历史,
当然也有人在Termux的issue上提到了,能不能捕捉安卓的事件来执行这个exit,否则这个exit有点难记得,相对来说也不方便了很多,容易遗忘,只是这个issue也被关掉了,看看后面会不会优化吧

因为上次那个问题,所以打算把图库迁移到靠谱一些的cloudflare上,这里用到了rclone这个很强大的工具

在开始迁移前,先做一下准备

  1. 分别在腾讯云和 Cloudflare 平台申请 Access Key 和 Secret Key
  2. 安装 rclone 工具(可从 rclone 官网 下载)

配置远程存储

配置步骤

  1. 打开终端,输入 rclone config 开始远程存储配置
  2. n 创建新的远程存储

配置腾讯云 COS

  1. 输入远程存储名称,例如 cos
  2. 选择存储类型,选择 Amazon S3 兼容模式(选项编号可能因 rclone 版本而异)
  3. 选择服务提供商为腾讯云 COS
  4. 输入您的 access_key_idsecret_access_key(从腾讯云控制台获取)
  5. 选择相应的端点 API(根据您的存储桶所在地区选择北京或上海等)
  6. 配置访问权限:可以选择仅资源所有者拥有完全访问权限,或根据您的安全需求选择其他选项
  7. 选择存储类型(通常选择标准存储)
  8. 确认配置

配置 Cloudflare R2

  1. 再次输入 n 创建新的远程存储
  2. 输入远程存储名称,例如 r2
  3. 同样选择 Amazon S3 兼容模式
  4. 选择服务提供商为 Cloudflare R2
  5. 输入您的 access_key_idsecret_access_key(从 Cloudflare 控制台获取)
  6. 输入 R2 的端点 URL(通常格式为 https://<account_id>.r2.cloudflarestorage.com
  7. 配置访问权限
  8. 选择存储类型
  9. 确认配置

执行迁移操作

完成配置后,使用以下命令执行迁移:

1
rclone copy cos:mystore-xxxxxxx/img/ r2:img/ --progress

上述命令将把腾讯云 COS 中 mystore-xxxxxxx 存储桶下的 img 目录中的所有文件复制到 Cloudflare R2 的 img 存储桶中。添加的 --progress 参数可以显示迁移进度。

高级选项

为了获得更好的迁移体验,您可以考虑以下高级选项:

  • 使用 --transfers=N 参数设置并行传输数量,加快迁移速度
  • 使用 --checkers=N 参数设置并行检查数量
  • 使用 --dry-run 参数测试迁移过程而不实际复制文件
  • 使用 --log-file=FILE 参数将迁移日志保存到文件中

例如:

1
rclone copy cos:mystore-xxxxxxx/img/ r2:img/ --progress --transfers=4 --checkers=8 --stats=10s

验证迁移结果

迁移完成后,可以使用以下命令验证两边的文件是否一致:

1
rclone check cos:mystore-xxxxxxx/img/ r2:img/ --one-way

结语

通过 rclone 这一强大工具,可以轻松实现云存储服务之间的数据迁移。Cloudflare R2 提供了稳定可靠的对象存储服务,同时其无出站流量费用的特性也使其成为图库存储的理想选择。

上周四,突然接到腾讯云的电话,当然不是叫我去面试的,是说我账户欠费了,而且一下欠了二十多,因为这个账户我除了包年包月的我都会及时续费,剩下的就是一个放我博客图片的cos对象存储,本来可能一天就几分钱,一年充个十块钱都足够用了,突然一天就欠了二十多还是引起了我的警觉,去看了下说是什么外网下行流量费,我想着怎么又有这么个收费名目了,腾讯云给我的感觉就是一直巧立名目,或者增加收费项,后面也问了客服,说是我开的是公有读私有写,而且允许空refer访问(这个我记得是改过的,不知道为啥现在变成了空refer),也挺奇怪的,这种page托管的博客,不都是只能公有读么,私有读的话我还放啥图片,除非全都走服务端中转。
唯一一个优点是总算有个日志,结果看了下是江苏泰顺一哥们一直在换着ip刷,用的还是固定的okhttp客户端,这里想放个图,但是这个优点也被后面的缺点给淹没了,因为腾讯云不支持按访问客户端和ip段设置黑名单,简直离谱,都立了这个名目收费了,对应的功能还不行,都能怀疑是故意漏个口子让刷
可能暂时就不能放出来图片了,主要是也没精力搞什么服务端加密,打算迁到r2或者其他方式了,如果有大佬对这方面有经验又碰巧看到了这篇文章,希望能给下指导。
还有这位用了江苏泰顺ip(说不定是肉鸡)的哥们,是有啥想不开的呢

String.format是Java中String类非常常用的一个方法,可以帮我们将占位符替换成变量,比如%d可以作为整型的占位符,%s可以作为字符串的占位符,但是吧有的时候常用归常用,有的时候碰到问题了还是得学习记录下
比如我们要写一个字符串替换方法

1
2
3
4
5
public class Demo {
public static void main(String[] args) {
System.out.printf("成功是%d%的努力和%d%的天赋", 99, 1);
}
}

就是希望输出 “成功是99%的努力和1%的天赋” ,先不管这句话的正确性,看看能不能正常输出(肯定是不能)

1
2
3
4
5
6
7
Exception in thread "main" java.util.UnknownFormatConversionException: Conversion = '的'
at java.util.Formatter.checkText(Formatter.java:2579)
at java.util.Formatter.parse(Formatter.java:2555)
at java.util.Formatter.format(Formatter.java:2501)
at java.io.PrintStream.format(PrintStream.java:970)
at java.io.PrintStream.printf(PrintStream.java:871)
at demo.Demo.main(Demo.java:9)

会有这样的报错,虽然是个小问题我们还是来仔细研究下,
首先这边format函数会根据可能的占位符去提取并分割字符串

1
2
3
4
5
6
7
8
9
10
11
private static final String formatSpecifier
= "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";

private static Pattern fsPattern = Pattern.compile(formatSpecifier);

/**
* Finds format specifiers in the format string.
*/
private FormatString[] parse(String s) {
ArrayList<FormatString> al = new ArrayList<>();
Matcher m = fsPattern.matcher(s);

比如上面的字符串,可以提取到两个有效的 %d
然后对剩余的进行分割,分割后再遍历校验每一个字符
如果碰到单独的 % 就说明出现了错误的分隔符
会调用 checkText 来识别报错
而如果我们想要能够正常输出% 也非常简单,只需要使用两个百分号 %%
这里其实就是匹配到了上面 formatSpecifier 的最后面,
%后面可以匹配的可以使 a-zA-Z 以及 %
当是连续的两个 %% 时就会被识别成转义成实际的 %

1
2
3
public static void main(String[] args) {
System.out.printf("成功是%d%%的努力和%d%%的天赋", 99, 1);
}

换成这样就能正常输出了

0%