前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >因为这个bug,我被同事鄙视了

因为这个bug,我被同事鄙视了

作者头像
燃192
发布2023-02-28 13:51:32
3280
发布2023-02-28 13:51:32
举报
文章被收录于专栏:Java技术圈子

Spring的Bean对象生命周期初探——依赖注入的时机

代码语言:javascript
复制
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserCacheService userCacheService;

    private Map<String,String> userInfoMap = userCacheService.getUserInfoMap();

    @Override
    public String queryAllUser() {
        return "{all}" + userInfoMap;
    }
}

这段代码有什么问题呢?

能一眼看出来的同学,说明你不是受众,点击退出就可以了

像我一样马虎没看出来的,基础薄弱或者没有把概念和实践结合起来的同学继续往下看

这段代码会导致应用无法启动

究其原因是犯了一个很经典的低级错误,没有理解spring对bean的初始化和加载

当UserServiceImpl这个类被初始化的时候,会同时创建类中的对象userInfoMap

而userInfoMap是通过userCacheService这个对象获取的

那么问题来了,类还在初始化,还没有被spring注入进去,即userCacheService还没有被实际赋值

所以userCacheService对象必定为空,我使用userCacheService.getUserInfoMap()获取对象必定会报nep异常,导致应用无法启动

那么要怎么修改呢,如下所示即可

代码语言:javascript
复制
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserCacheService userCacheService;

    private Map<String,String> userInfoMap = null;

    @PostConstruct
    public void init(){
        userInfoMap =  userCacheService.getUserInfoMap();
    }

    @Override
    public String queryAllUser() {
        return "{all}" + userInfoMap;
    }
}

@PostConstruct该注解被用来修饰一个非静态的void()方法。

被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。

PostConstruct在构造函数之后执行,init()方法之前执行。

通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序:

Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法) -> InitializingBean -> init-method

用以上任意方法只要在初始化使用对象前,对userCacheService初始化即可。

问题现在是解决了,但是不求甚解肯定不是一个合格的搬砖人应有的态度,@PostConstruct注解的方法为什么就能在类对属性初始化之前被执行呢

@PostConstruct的使用

Spring容器的每个受管Bean在调用初始化方法之前,都会获得BeanPostProcessor接口实现类的一个回调。

在BeanPostProcessor的方法中有一段逻辑就是会判断当前被回调的bean的方法中有没有被initAnnotationType/destroyAnnotationType注释,如果有,则添加到init/destroy队列中,后续一一执行。initAnnotationType/destroyAnnotationType注解就是@PostConstruct/@PreDestroy。

加载顺序的探究

代码语言:javascript
复制
@Service
public class UserServiceImpl implements UserService {

    private Map<String,String> userInfoMap =  null;

    static {
        System.out.println("UserServiceImpl静态代码块");
    }
    public UserServiceImpl(){
        System.out.println("UserServiceImpl构造方法");
    }

    @PostConstruct
    public void init() {
        System.out.println("UserServiceImpl PostConstruct init");
    }
    @Override
    public String queryAllUser() {
        return "{all}" + userInfoMap;
    }
}

结果如下:
UserServiceImpl静态代码块
UserServiceImpl构造方法
UserServiceImpl PostConstruct init

上面是单个Bean中添加了@PostConstruct的加载顺序,

现在放两个互不干涉的Bean

代码语言:javascript
复制
UserServiceImpl静态代码块
UserServiceImpl构造方法
UserServiceImpl PostConstruct init
UserCacheServiceImpl静态代码块
UserCacheServiceImpl构造方法
UserCacheService PostConstruct init

分别是独立加载的,那么在UserServiceImpl调用UserCacheService的结果是怎么样的呢

代码语言:javascript
复制
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserCacheService userCacheService;

    private Map<String,String> userInfoMap =  null;

    static {
        System.out.println("UserServiceImpl静态代码块");
    }
    public UserServiceImpl(){
        System.out.println("UserServiceImpl构造方法");
    }

    @PostConstruct
    public void init() {
        System.out.println("UserServiceImpl PostConstruct init");
        userInfoMap = userCacheService.getUserInfoMap();
    }
    @Override
    public String queryAllUser() {
        return "{all}" + userInfoMap;
    }
}

UserServiceImpl静态代码块
UserServiceImpl构造方法
UserCacheServiceImpl静态代码块
UserCacheServiceImpl构造方法
UserCacheService PostConstruct init
UserServiceImpl PostConstruct init

可以观察到,在各自独立加载的时候是先UserServiceImpl,后UserCacheServiceImpl,但是在执行PostConstruct inti方法的时候

情况发生了逆转,顺序如下

1.UserServiceImpl实际初始化,

2.执行UserServiceImpl静态代码块

3.然后初始化UserServiceImpl属性

4.然后执行UserServiceImpl构造方法

5.初始化spring注入的对象UserCacheService

6.初始化UserCacheService

6.1UserCacheService 静态代码块

6.2UserCacheService 属性

6.3UserCacheService 构造方法

6.4UserCacheService的 PostConstruct init方法

6.然后执行UserServiceImpl的PostConstruct init方法给UserServiceImpl的userInfoMap属性实际赋值

这样就避免了在使用对象的时候,springbean没有注入而造成的NEP。

Bean的加载

顺着图片可以很清晰的理解方法的执行顺序

Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法) -> InitializingBean -> init-method

有时间debug看一下实际应用的启动过程,真正在机器上走一遍jvm如何加载字节码到类创建实体对象,然后把实体对象交给java程序管理,

然后实体对象创建的生命周期和如何托管给spring,然后运行的流程,立flag今年十一假期完成

封面图片提供 b站up主 Dara_达拉不崩巴

https://space.bilibili.com/455710953

里面有好康的

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java技术圈子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Spring的Bean对象生命周期初探——依赖注入的时机
  • @PostConstruct的使用
    • 加载顺序的探究
    • Bean的加载
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档