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) { 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); }
|
详细说就是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; 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() { } @Override public Set<Type> getSupportTypes() { return Sets.newHashSet(Type.A); } }
@Component class BDemoService implements IDemoService {
@Override public void demo() { } @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); }
|