1. 问题定位

昨天发现日志里有报空指针的异常,定位到代码位置如下:

1
2
3
4
5
public void xxxTest(Type type) {
IDemoService demoService = demoBaseService.getDemoService(type);
// 该行报空指针!!!!!!!!
return demoService.fillXxxx();
}

DemoBaseService相关代码如下:

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
public class DemoBaseService {

private Map<Type, IDemoService> demoServiceMap;

public IDemoService getDemoService(Type type) {
if (demoServiceMap == null) {
demoServiceMap = new ConcurrentHashMap<>();
Map<String, IDemoService> beans = GkAppContext.getBeansOfType(IDemoService.class);
beans.forEach((name, bean) -> {
if (!IDemoService.class.getSimpleName().equals(name)) {
bean.getTypes().forEach(type -> demoServiceMap.put(type, bean));
}
});
}
return demoServiceMap.get(type);
}
}

//
public interface IDemoService {
Set<Type> getTypes();
}

public class A implements IDemoService {
@Override
public Set<Type> getTypes() {
return Sets.newHashSet(Type.A);
}
}

public class B implements IDemoService {
@Override
public Set<Type> getTypes() {
return Sets.newHashSet(Type.B);
}
}

第一次分析代码后,感觉没有问题。并且在前端页面触发接口调用也没有问题,所以得出了错误结论。经过高手指点,发现是并发场景下延迟初始化引发的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IDemoService getDemoService(Type type) {
// B 线程在这里
if (demoServiceMap == null) {
demoServiceMap = new ConcurrentHashMap<>();
// A 线程此时在这里
Map<String, IDemoService> beans = GkAppContext.getBeansOfType(IDemoService.class);
beans.forEach((name, bean) -> {
if (!IDemoService.class.getSimpleName().equals(name)) {
bean.getTypes().forEach(type -> demoServiceMap.put(type, bean));
}
});
}
return demoServiceMap.get(type);
}

详细说就是getDemoService方法是在被调用的时候初始化 Map,但是如果两个线程同一时间调用这个方法,那么B 线程可能获取到一个不完整的 Map,导致 返回为 null,最后引发 NPE。

2. 解决思路

有两种,一种是保证Map初始化的时候使用单线程,另外一种是通过并发策略保证初始化的正确。

2.1 借助 Spring 自带的onApplicationEvent方法,Spring事件监听器默认是单线程同步执行,在onApplicationEvent方法里执行初始化逻辑即可

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
public class DemoBaseService implements ApplicationListener<ContextRefreshedEvent> {

private Map<Type, IDemoService> demoServiceMap;

// 单线程执行
@Override
public void onApplicationEvent(@NotNull ContextRefreshedEvent event) {
initAssetQuoteServiceMap();
}

public void initAssetQuoteServiceMap() {
if (demoServiceMap == null) {
demoServiceMap = new ConcurrentHashMap<>();
Map<String, IDemoService> beans = GkAppContext.getBeansOfType(IDemoService.class);
beans.forEach((name, bean) -> {
if (!IDemoService.class.getSimpleName().equals(name)) {
bean.getTypes().forEach(type -> demoServiceMap.put(type, bean));
}
});
}
return demoServiceMap.get(type);
}

public IDemoService getDemoService(Type type) {
return demoServiceMap.get(type);
}
}

2.2 基于 2.1 的思路,使用@EventListener

Spring提供了@EventListener注解,可以表示该是一个事件监听器,方法的参数即监听的事件,如下:

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
public class DemoBaseService {

private Map<Type, IDemoService> demoServiceMap;

@EventListener
public void onApplicationEvent(@NotNull ContextRefreshedEvent event) {
initAssetQuoteServiceMap();
}

public void initAssetQuoteServiceMap() {
if (demoServiceMap == null) {
demoServiceMap = new ConcurrentHashMap<>();
Map<String, IDemoService> beans = GkAppContext.getBeansOfType(IDemoService.class);
beans.forEach((name, bean) -> {
if (!IDemoService.class.getSimpleName().equals(name)) {
bean.getTypes().forEach(type -> demoServiceMap.put(type, bean));
}
});
}
}

public IDemoService getDemoService(Type type) {
return demoServiceMap.get(type);
}
}

2.3 基于 2.1 的Getter(lazy=true)注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DemoBaseService {

@Getter(lazy = true)
private final Map<Type, IDemoService> demoServiceMap = initAssetQuoteServiceMap();

public void initAssetQuoteServiceMap() {
if (demoServiceMap == null) {
demoServiceMap = new ConcurrentHashMap<>();
Map<String, IDemoService> beans = GkAppContext.getBeansOfType(IDemoService.class);
beans.forEach((name, bean) -> {
if (!IDemoService.class.getSimpleName().equals(name)) {
bean.getTypes().forEach(type -> demoServiceMap.put(type, bean));
}
});
}
}

public IDemoService getDemoService(Type type) {
return demoServiceMap.get(type);
}
}

@Getter(lazy = true) 使用了 java.util.concurrent.atomic.AtomicReference 来确保懒加载的线程安全性。即使多个线程同时调用 getEnv(), 也只会有一个线程执行 initEnv() 方法,其他线程会等待初始化完成后直接获取已初始化的值。

2.4 单例模式 - 使用双重检查锁

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private volatile static Singleton INSTANCE; // 将 INSTANCE 声明为 volatile 型
private Singleton() {}
public static Singleton getInstance(){
if(INSTANCE == null) {
synchronized (Singleton.class) {
if(INSTANCE == null) INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}

3.引入策略模式进行改进

策略模式: 定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。 可以看出,业务的场景很适合策略模式,所以试着改进一下代码

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
interface IDemoService extends InitializingBean {
void demo();
Set<Type> getSupportTypes();
@Override
default void afterPropertiesSet() throws Exception {
getSupportTypes().forEach(type -> DemoFactory.register(type, this));
}
}

@Component
class ADemoService implements IDemoService {

@Override
public void demo() {
// A process
}
@Override
public Set<Type> getSupportTypes() {
return Sets.newHashSet(Type.A);
}
}

@Component
class BDemoService implements IDemoService {

@Override
public void demo() {
// B process
}
@Override
public Set<Type> getSupportTypes() {
return Sets.newHashSet(Type.B);
}
}

class DemoFactory {
private static final Map<Type, IDemoService> DEMO_SERVICE = Maps.newHashMap();
public static void register(Type type, IDemoService demoService) {
DEMO_SERVICE.put(type, demoService);
}
public static IDemoService get(Type type) {
return DEMO_SERVICE.get(type);
}
}

//在具体使用的业务逻辑处
public void use(Type type) {
DemoFactory.get(type);
}