美文网首页Techno Geek
Spring Boot 单例模式中依赖注入问题

Spring Boot 单例模式中依赖注入问题

作者: 程序员莫语 | 来源:发表于2020-05-14 09:57 被阅读0次

    在日常项目开发中,单例模式可以说是最常用到的设计模式,项目也常常在单例模式中需要使用 Service 逻辑层的方法来实现某些功能。通常可能会使用 @Resource 或者 @Autowired 来自动注入实例,然而这种方法在单例模式中却会出现 NullPointException 的问题。那么本篇就此问题做一下研究。

    演示代码地址

    问题初探

    一般我们的项目是分层开发的,最经典的可能就是下面这种结构:

    ├── UserDao -- DAO 层,负责和数据源交互,获取数据。
    ├── UserService -- 服务逻辑层,负责业务逻辑实现。
    └── UserController -- 控制层,负责提供与外界交互的接口。
    

    此时需要一个单例对象,此对象需要 UserService 来提供用户服务。代码如下:

    @Slf4j
    public class UserSingleton {
    
        private static volatile UserSingleton INSTANCE;
    
        @Resource
        private UserService userService;
    
        public static UserSingleton getInstance() {
            if (null == INSTANCE) {
                synchronized (UserSingleton.class) {
                    if (null == INSTANCE) {
                        INSTANCE = new UserSingleton();
                    }
                }
            }
            return INSTANCE;
        }
    
        public String getUser() {
            if (null == userService) {
                log.debug("UserSingleton userService is null");
                return "UserSingleton Exception: userService is null";
            }
            return userService.getUser();
        }
    }
    

    然后创建一个 UserController 来调用 UserSingleton.getUser() 方法看看返回数据是什么。

    @RestController
    public class UserController {
    
        @Resource
        private UserService userService;
    
        /**
         * 正常方式,在 Controller 自动注入 Service。
         *
         * @return  user info
         */
        @GetMapping("/user")
        public String getUser(){
            return userService.getUser();
        }
    
        /**
         * 使用单例对象中自动注入的 UserService 的方法
         *
         * @return  UserSingleton Exception: userService is null
         */
        @GetMapping("/user/singleton/ioc")
        public String getUserFromSingletonForIoc(){
            return UserSingleton.getInstance().getUser();
        }
    }
    
    user-info.png

    可以看到,在 UserController 中自动注入 UserService 是可以正常获取到数据的。

    UserSingleton-exception.png

    但是如果使用在单例模式中使用自动注入的话,UserService 是一个空的对象。

    所以使用 @Resource 或者 @Autowired 注解的方式在单例中获取 UserService 的对象实例是不行的。如果没有做空值判断,会报 NullPointException 异常。

    问题产生原因

    之所以在单例模式中无法使用自动依赖注入,是因为单例对象使用 static 标记,INSTANCE 是一个静态对象,而静态对象的加载是要优先于 Spring 容器的。所以在这里无法使用自动依赖注入。

    问题解决方法

    解决这种问题,其实也很简单,只要不使用自动依赖注入就好了,在 new UserSingleton() 初始化对象的时候,手动实例化 UserService 就可以了嘛。但是这种方法可能会有一个坑,或者说只能在某些情况下可以实现。先看代码:

    @Slf4j
    public class UserSingleton {
    
        private static volatile UserSingleton INSTANCE;
    
        @Resource
        private UserService userService;
    
        // 为了和上面自动依赖注入的对象做区分。
        // 这里加上 ForNew 的后缀代表这是通过 new Object()创建出来的
        private UserService userServiceForNew;
    
        private UserSingleton() {
            userServiceForNew = new UserServiceImpl();
        }
    
        public static UserSingleton getInstance() {
            if (null == INSTANCE) {
                synchronized (UserSingleton.class) {
                    if (null == INSTANCE) {
                        INSTANCE = new UserSingleton();
                    }
                }
            }
            return INSTANCE;
        }
    
        public String getUser() {
            if (null == userService) {
                log.debug("UserSingleton userService is null");
                return "UserSingleton Exception: userService is null";
            }
            return userService.getUser();
        }
    
        public String getUserForNew() {
            if (null == userServiceForNew) {
                log.debug("UserSingleton userService is null");
                return "UserSingleton Exception: userService is null";
            }
            return userServiceForNew.getUser();
        }
    }
    

    下面是 UserService 的代码。

    public interface UserService {
    
        /**
         * 获取用户信息
         *
         * @return  @link{String}
         */
        String getUser();
    
        /**
         * 获取用户信息,从 DAO 层获取数据
         *
         * @return
         */
        String getUserForDao();
    }
    
    
    @Slf4j
    @Service
    public class UserServiceImpl implements UserService {
    
        @Resource
        private UserDao userDao;
    
        @Override
        public String getUser() {
            return "user info";
        }
    
        @Override
        public String getUserForDao(){
            if(null == userDao){
                log.debug("UserServiceImpl Exception: userDao is null");
                return "UserServiceImpl Exception: userDao is null";
            }
            return userDao.select();
        }
    }
    

    创建一个 UserController 调用单例中的方法做下验证。

    @RestController
    public class UserController {
    
        @Resource
        private UserService userService;
    
        // 正常方式,在 Controller 自动注入 Service。
        @GetMapping("/user")
        public String getUser(){
            return userService.getUser();
        }
    
        // 使用单例对象中自动注入的 UserService 的方法
        // 返回值是: UserSingleton Exception: userService is null
        @GetMapping("/user/singleton/ioc")
        public String getUserFromSingletonForIoc(){
            return UserSingleton.getInstance().getUser();
        }
    
        // 使用单例对象中手动实例化的 UserService 的方法
        // 返回值是: user info
        @GetMapping("/user/singleton/new")
        public String getUserFromSingletonForNew(){
            return UserSingleton.getInstance().getUserForNew();
        }
    
        // 使用单例对象中手动实例化的 UserService 的方法,在 UserService 中,通过 DAO 获取数据
        // 返回值是: UserServiceImpl Exception: userDao is null
        @GetMapping("/user/singleton/new/dao")
        public String getUserFromSingletonForNewFromDao(){
            return UserSingleton.getInstance().getUserForNewFromDao();
        }
    }
    

    通过上面的代码,可以发现,通过手动实例化的方式是可以一定程度上解决问题的。但是当 UserService 中也使用自动依赖注入,比如 @Resource private UserDao userDao;,并且单例中使用的方法有用到 userDao 就会发现 userDao 是个空的对象。

    也就是说虽然在单例对象中手动实例化了 UserService ,但 UserService 中的 UserDao 却无法自动注入。其原因其实与单例中无法自动注入 UserService 是一样的。所以说这种方法只能一定程度上解决问题。

    最终解决方案

    我们可以创建一个工具类实现 ApplicationContextAware 接口,用来获取 ApplicationContext 上下文对象,然后通过 ApplicationContext.getBean() 来动态的获取实例。代码如下:

    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.stereotype.Component;
    
    /**
     * Spring 工具类,用来动态获取 bean
     *
     * @author James
     * @date 2020/4/28
     */
    @Component
    public class SpringContextUtils implements ApplicationContextAware {
    
        private static ApplicationContext applicationContext;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            SpringContextUtils.applicationContext = applicationContext;
        }
    
        /**
         * 获取 ApplicationContext
         *
         * @return
         */
        public static ApplicationContext getApplicationContext() {
            return applicationContext;
        }
    
        public static Object getBean(String name) {
            return applicationContext.getBean(name);
        }
    
        public static <T> T getBean(Class<T> clazz) {
            return applicationContext.getBean(clazz);
        }
    
        public static <T> T getBean(String name, Class<T> clazz) {
            return applicationContext.getBean(name, clazz);
        }
    }
    

    然后改造下我们的单例对象。

    @Slf4j
    public class UserSingleton {
    
        private static volatile UserSingleton INSTANCE;
    
        // 加上 ForTool 后缀来和之前两种方式创建的对象作区分。
        private UserService userServiceForTool;
    
        private UserSingleton() {
            userServiceForTool = SpringContextUtils.getBean(UserService.class);
        }
    
        public static UserSingleton getInstance() {
            if (null == INSTANCE) {
                synchronized (UserSingleton.class) {
                    if (null == INSTANCE) {
                        INSTANCE = new UserSingleton();
                    }
                }
            }
            return INSTANCE;
        }
    
        /**
         * 使用 SpringContextUtils 获取的 UserService 对象,并从 UserDao 中获取数据
         * @return
         */
        public String getUserForToolFromDao() {
            if (null == userServiceForTool) {
                log.debug("UserSingleton userService is null");
                return "UserSingleton Exception: userService is null";
            }
            return userServiceForTool.getUserForDao();
        }
    }
    

    UserController 中进行测试,看一下结果。

    @RestController
    public class UserController {
      /**
       * 使用 SpringContextUtils 获取的的 UserService 的方法,在 UserService 中,通过 DAO 获取数据
       *
       * @return  user info for dao
       */
      @GetMapping("/user/singleton/tool/dao")
      public String getUserFromSingletonForToolFromDao(){
          return UserSingleton.getInstance().getUserForToolFromDao();
      }
    }
    

    访问接口,返回结果是:user info for dao,验证通过。

    其他

    本文源码地址

    欢迎关注本人 github 中的 spring-boot-examplespring-cloud-example 项目,为您提供更多的 spring bootspring cloud 教程及样例代码。博主会在空闲时间持续更新相关的文档。

    spring-boot-example

    spring-cloud-example

    更多技术文章欢迎关注我的博客主页:http://JemGeek.com

    点击阅读原文

    相关文章

      网友评论

        本文标题:Spring Boot 单例模式中依赖注入问题

        本文链接:https://www.haomeiwen.com/subject/ewvznhtx.html