在日常项目开发中,单例模式可以说是最常用到的设计模式,项目也常常在单例模式中需要使用 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(); } }
可以看到,在 UserController
中自动注入 UserService
是可以正常获取到数据的。
但是如果使用在单例模式中使用自动注入的话, 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
,验证通过。