从零开始使用Spring Cloud Config

在传统的Java应用中,应用配置都是直接存储在代码库中(比如放在application.yml中),配置的更新需要修改发布包并重新发布。

而配置的审计也会和代码夹杂,需要从众多提交中中找到配置变更的提交,很难独立完成,也很难追溯何时修改了某一项配置。

随着微服务的发展,服务越来越多、配置也越来越多,这种原始的配置管理方式,很难适应分布式环境。作为开发者,还是需要配置能单独地管理和审计。

Spring Cloud Config就提供了配置管理和审计的解决方案。

Spring Cloud Config有如下特点:

  1. 与Spring生态紧密集成
    提供了和Spring中的EnvironmentPropertySource一致的抽象,能够和原有的配置文件很好的对应起来。
  2. 可以动态更新
    只需要添加注解,不需要重启就能更新配置项
  3. 支持多种provider
    配置仓库可以是git仓库、本地文件、数据库等等。

开始使用Spring Cloud config

Spring Cloud Config 分为两部分:

Spring Cloud Config Server负责从配置仓库中获取配置,并给client提供获取配置的HTTP接口

Spring Cloud Config Client负责给目标应用提供从config-server获取配置、更新配置的能力。

阅读更多

Jackson如何反序列化Java14中的record类型?

Java14近日发布,其中引入了新的record类型,虽然这是个预览特性,但是也不妨碍我们尝试下。

record类型

record类型和普通的类相比,有几个特点

  1. 每个字段都是private final的,类本身是final
  2. 类只有一个所有传递所有参数的构造函数
  3. 每个字段会自动会有一个get方法,但是方法名和字段名一致(没有get前缀
  4. 可以给字段添加注解,支持FIELD、METHOD和PARAMETER(如果一个注解是FIELD、METHOD和PARAMETER的,那么注解会在字段、get方法和构造函数参数上同时出现)
  5. toStringequalshashCode方法都是自带的

这几个特点决定了这个非常适合做DO(Model),DTO(比如RPC中定义的各种entity,http返回的json)。

由于业务中经常用到jackson来序列化反序列化对象,今天我们就来试一试jackson对record类型的支持。

如何使用jackson反序列化record对象

如果我们按照原来的方式来反序列化record(如果字段上没有@JsonProperty注解的话),会直接报错:
InvalidDefinitionException: Cannot construct instance of Person (no Creators, like default construct, exist)

这是因为jackson没法找到构造函数和字段的映射,所以我们自己指定默认的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
// from https://gist.github.com/youribonnaffe/03176be516c0ed06828ccc7d6c1724ce
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
@Override
public String findImplicitPropertyName(AnnotatedMember m) {
if (m.getDeclaringClass().isRecord()) {
if (m instanceof AnnotatedParameter parameter) {
return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
}
}
return super.findImplicitPropertyName(m);
}
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);
阅读更多

Java 14中的 JEP 358: Helpful NullPointerExceptions是如何实现的

Java 14在2020年3月17号正式发布,不久AdoptOpenJDK也跟进发布了新版。

JEP 358: Helpful NullPointerExceptions

其中对使用者比较有用的功能是 JEP 358: Helpful NullPointerExceptions,即在NullPointerException的message中指明为什么产生了这个NPE。

示例代码如下(需要Java14,并配置参数-XX:+ShowCodeDetailsInExceptionMessages):

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class Main {
static class Class1 {
Object object;
}
static class Class2 {
Class1 class1;
public Class1 getClass1() {
return class1;
}
}
static Class2 class2;
private static Object get(int i) {
return null;
}
// 打印NPE的message
private static void printNPE(Runnable runnable) {
try {
runnable.run();
} catch (NullPointerException e) {
System.err.println(e.getMessage());
}
}
public static void main(String[] args) {
Class2 class21 = null;
printNPE(() -> {
// 1. getstatic为null
// Cannot invoke "Main$Class2.getClass1()" because "Main.class2" is null
System.out.println(class2.getClass1().object.toString());
});
Class2 class22 = new Class2();
printNPE(() -> {
// 2. 调用函数结果为null
// Cannot read field "object" because the return value of "Main$Class2.getClass1()" is null
System.out.println(class22.getClass1().object.toString());
});
class22.class1 = new Class1();
printNPE(() -> {
// 3. 函数调用后,getfield为null
// Cannot invoke "Object.toString()" because "Main$Class2.getClass1().object" is null
System.out.println(class22.getClass1().object.toString());
});
printNPE(() -> {
// 4. 链式getfield
// Cannot invoke "Object.toString()" because "class22.class1.object" is null
System.out.println(class22.class1.object.toString());
});
Integer[] ary1 = null;
printNPE(() -> {
// 5. 对null执行aaload
// Cannot load from object array because "ary1" is null
System.out.println(ary1[0].intValue());
});
Integer[] ary2 = new Integer[1];
printNPE(() -> {
// 6. iconst_0+aaload后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[0]" is null
System.out.println(ary2[0].intValue());
});
printNPE(() -> {
int i = 0;
// 7. iconst_0+iload_1后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[i]" is null
System.out.println(ary2[i].intValue());
});
printNPE(() -> {
int i = 0;
// 8. iconst_0+iadd后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[...]" is null
System.out.println(ary2[i + 0].intValue());
});
printNPE(() -> {
// 9. invokevirtual结果为null
// Cannot invoke "Object.toString()" because the return value of "Main.get(int)" is null
System.out.println(Main.get(1).toString());
});
}
}

令我比较意外的是情况4和情况7,Java能够追溯链式回调,而且连变量名都带上了!

JEP 358实现细节

那我们就看下这么神奇的特性是如何实现的吧。

首先,这个功能需要添加参数-XX:+ShowCodeDetailsInExceptionMessages才能开启,那我们只要在JDK的代码中搜索这个字符串就行。于是找到给NPE填充Message的代码:

阅读更多

jdk 8u91一个lambda类型推断的bug

公司内部发布maven包是在公司构建服务器上编译的。最近一次上线,发现同样的代码,在本地的jdk8上能编译通过,但是在构建服务器上,就编译报错。

简化后的代码如下:

JavaBugTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Optional;

public class JavaBugTest {

public static <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E {
return retryCallback.doWithRetry(new Object());
}

public static void main(String[] args) {
Optional.ofNullable(execute((param) -> new Object()))
.ifPresent(s -> System.out.println(s));
}
}
RetryCallback.java
1
2
3
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(Object var1) throws E;
}

用maven编译报错如下:

1
JavaBugTest.java:[10,36] unreported exception E; must be caught or declared to be thrown

最后发现,是构建服务器jdk版本太老导致的。

二分法尝试了几个版本,发现8u91不行,但是8u92就能编译通过了。

于是开始调试javac代码,发现两个版本的AST不一样:

阅读更多

HashMap的初始容量设置

先说结论

知道大小的情况下,new HashMap的时候这么写:

1
HashMap<Integer, String> map = Maps.newHashMapWithExpectedSize(expectedSize);

正文

Java中的HashMap大家都很熟悉,其底层使用了Node数组来存储Map中的数据。但是如果存储的数据太多,空间不够,就需要扩容这个数组来存储新的数据了。

扩容的实现可以看下java.util.HashMap#resize函数,基本上就是将数组中的内容逐个复制到新数组中。

扩容操作的时间复杂度是O(n),空间复杂度是O(n),还需要计算对象的hash。在平常编码中,如果我们提前知道map的大小,就应该指定初始容量,避免发生扩容。

《阿里巴巴开发手册》中也有这么一条建议:

于是,很多开发者(包括刚开始的我),就这么写:

阅读更多

如何以string方式查看heapdump中的byte数组

昨天,线上OOM,dump下来hprof文件,里面有两个大数组:

从表象上来看,和thrift导致的oom是一样的:

但是问题是,这种情况是怎么出现的呢?

找了好几种办法,没有头绪。

最后发现,把这个byte数组转成string就看到了thrift服务端的错误信息。

当时为了快速解决问题,是直接将前60个byte手抄到java代码中,然后转成string输出。

但是,不能一直都这么干,所以就看了下如何方便的将heapdump中的byte[]输出为string。

查了半天,发现OQL没有这样的功能,但是VisualVM倒是可以间接的做这事:

阅读更多

不正确使用Thrift Client导致的OOM问题排查

最近线上有一个多线程的任务,会调用几个Thrift服务。 上线后观察到这个脚本在执行一段时间后,会有好几次Full GC,然后就会报OOM错误。

那就先下载heap dump(推荐压缩后,使用rz下载到本地),使用VisualVM分析。首先切换到Objects页面,看下是否有大对象:

heapdump-Objects

可以看到,有两个byte数组占用了大量内存,也可以看到这个对象是在Java栈上的,接下来就是要找谁在使用这个变量。

右击该对象,点击Select in Threads:

可以看到是名为rebuilder-9的线程,再查看这个线程的调用栈:

再结合readStringBody的代码:

阅读更多

lambda表达式导致arthas无法redefine的问题

作为一个从PHP转Java的人,发现alibaba的arthas很好用。通过arthas的redefine命令,可以像PHP一样,不用重新发布,就可以改变程序行为(前提是不改变类结构,不改变方法签名)。

但是用多了,发现很多时候,我们就改了几行代码,甚至有的时候就添加了一行日志,就无法redefine了。提示

redefine error! java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

它提示我们新增加方法,那我们就看看是不是新增加了方法。通过javap来查看定义的方法:

老的类:

新的类:

对比之后发现,新的类,即本地编译的类,其中的lambda对应的方法名都是lambda$getAllCity$0这样的。

阅读更多

LeetCode 1190. Reverse Substrings Between Each Pair of Parentheses

题意

每层括号里面的东西需要反转一次。即在在偶数层括号里面的字符是正序的,在奇数层括号里面的字符是逆序的。然后拼成结果。

例子:

“(abcd)”
反转之后就是:
dcba

“(u(love)i)”
love不反转,u,love,i三个反转,答案为:
iloveu

思路

网上有人直接用栈存储,每一个元素代表当前层级括号中的字符串,如果遇到括号关闭,将当前层级字符串反转再append到上一级的字符串中。

但是,括号层级一多,字符串反转的次数就非常多了。而且很多反转都是没有必要的。

其实直接递归就好,或者说分治:

每个括号内部都算作独立的子问题,如果正序,直接逐个append,逆序则反向append;如果遇到括号,则继续分治。

阅读更多

从fastjson漏洞谈防御式编程

最近,fastjson又爆出一个漏洞,在解析特殊字符的时候,直接OOM:

首先分析一下整体流程:

在scanString时,会直接读取两个字符:

而在next方法中,每次读取都会将bp的值加一(即使没有从输入中读取字符):

1
2
3
4
5
6
public final char next() {
int index = ++bp;
return ch = (index >= this.len ? //
EOI //
: text.charAt(index));
}

在处理完x之后,继续解析剩下的字符。由于没有更多字符了,所以读到的总是EOI,然后进入如下分支:

1
2
3
4
5
6
7
if (ch == EOI) {
if (!isEOF()) {
putChar((char) EOI);
continue;
}
throw new JSONException("unclosed string : " + ch);
}

本来到这一步,isEOF应该是true了,但是isEOF是这样的:

阅读更多