阅读 3356

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

在日常项目开发中,单例模式可以说是最常用到的设计模式,项目也常常在单例模式中需要使用 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

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

点击阅读原文

文章分类
后端
文章标签