Dependency injection in Spring Boot singleton pattern

Posted by TashaAT on Sun, 26 Dec 2021 05:41:03 +0100

In daily project development, singleton pattern is the most commonly used design pattern. Projects often need to use the method of Service logic layer to realize some functions in singleton pattern. Usually @ Resource or @ Autowired may be used to inject instances automatically. However, this method will cause the problem of NullPointException in singleton mode. Then this article will do some research on this issue.

Demo code address

Preliminary discussion on Problems

Generally, our projects are developed in layers, and the most classic may be the following structure:

├── UserDao -- DAO Layer, which is responsible for interacting with the data source and obtaining data.
├── UserService -- The service logic layer is responsible for the implementation of business logic.
└── UserController -- The control layer is responsible for providing an interface for interaction with the outside world.

At this point, you need a singleton object that requires UserService to provide user services. The code is as follows:

@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();
    }
}

Then create a UserController to call usersingleton Getuser () method to see what the returned data is.

@RestController
public class UserController {

    @Resource
    private UserService userService;

    /**
     * In normal mode, the Service is automatically injected in the Controller.
     *
     * @return  user info
     */
    @GetMapping("/user")
    public String getUser(){
        return userService.getUser();
    }

    /**
     * Method of using UserService automatically injected in singleton object
     *
     * @return  UserSingleton Exception: userService is null
     */
    @GetMapping("/user/singleton/ioc")
    public String getUserFromSingletonForIoc(){
        return UserSingleton.getInstance().getUser();
    }
}

 

user-info.png

It can be seen that data can be obtained normally by automatically injecting UserService into UserController.

 

UserSingleton-exception.png

However, if automatic injection is used in singleton mode, UserService is an empty object.

Therefore, it is not feasible to use @ Resource or @ Autowired annotation to obtain the object instance of UserService in a singleton. If there is no short value judgment, a NullPointException exception will be reported.

Causes of problems

The reason why automatic dependency injection cannot be used in singleton mode is that singleton objects use static tags, INSTANCE is a static object, and the loading of static objects takes precedence over Spring containers. Therefore, automatic dependency injection cannot be used here.

Problem solving method

To solve this problem, it's actually very simple. As long as you don't use automatic dependency injection, you can manually instantiate UserService when initializing the object with new UserSingleton(). However, this method may have a pit, or it can only be realized in some cases. Look at the code first:

@Slf4j
public class UserSingleton {

    private static volatile UserSingleton INSTANCE;

    @Resource
    private UserService userService;

    // In order to distinguish it from the above automatic dependency injection objects.
    // The suffix ForNew is added here to indicate that this is created through 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();
    }
}

The following is the code of UserService.

public interface UserService {

    /**
     * Get user information
     *
     * @return  @link{String}
     */
    String getUser();

    /**
     * Obtain user information and data from DAO layer
     *
     * @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();
    }
}

Create a UserController and call the method in the singleton for verification.

@RestController
public class UserController {

    @Resource
    private UserService userService;

    // In normal mode, the Service is automatically injected in the Controller.
    @GetMapping("/user")
    public String getUser(){
        return userService.getUser();
    }

    // Method of using UserService automatically injected in singleton object
    // The return value is: UserSingleton Exception: userService is null
    @GetMapping("/user/singleton/ioc")
    public String getUserFromSingletonForIoc(){
        return UserSingleton.getInstance().getUser();
    }

    // Method of manually instantiating UserService in singleton object
    // The return value is: user info
    @GetMapping("/user/singleton/new")
    public String getUserFromSingletonForNew(){
        return UserSingleton.getInstance().getUserForNew();
    }

    // Use the method of manually instantiating UserService in the singleton object to obtain data through DAO in UserService
    // The return value is: UserServiceImpl Exception: userDao is null
    @GetMapping("/user/singleton/new/dao")
    public String getUserFromSingletonForNewFromDao(){
        return UserSingleton.getInstance().getUserForNewFromDao();
    }
}

Through the above code, it can be found that the problem can be solved to a certain extent by manual instantiation. However, when automatic dependency injection is also used in UserService, such as @ Resource private UserDao userDao;, Moreover, if the method used in the singleton is useful to userDao, it will be found that userDao is an empty object.

That is, although UserService is manually instantiated in the singleton object, UserDao in UserService cannot be injected automatically. In fact, the reason is the same as that UserService cannot be automatically injected in a single example. So this method can only solve the problem to a certain extent.

Final solution

We can create a tool class to implement the ApplicationContextAware interface to obtain the ApplicationContext context object, and then use ApplicationContext GetBean () to dynamically get the instance. The code is as follows:

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * Spring Tool class, which is used to dynamically obtain bean s
 *
 * @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;
    }

    /**
     * Get 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);
    }
}

Then transform our singleton object.

@Slf4j
public class UserSingleton {

    private static volatile UserSingleton INSTANCE;

    // Add the ForTool suffix to distinguish it from the objects created in the previous two ways.
    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;
    }

    /**
     * Use the UserService object obtained by SpringContextUtils and obtain data from UserDao
     * @return
     */
    public String getUserForToolFromDao() {
        if (null == userServiceForTool) {
            log.debug("UserSingleton userService is null");
            return "UserSingleton Exception: userService is null";
        }
        return userServiceForTool.getUserForDao();
    }
}

Test in UserController and see the results.

@RestController
public class UserController {
  /**
   * Use the UserService method obtained by SpringContextUtils to obtain data through DAO in UserService
   *
   * @return  user info for dao
   */
  @GetMapping("/user/singleton/tool/dao")
  public String getUserFromSingletonForToolFromDao(){
      return UserSingleton.getInstance().getUserForToolFromDao();
  }
}

Access interface. The returned result is: user info for dao. The verification is passed.



 

Topics: Java Spring