[common Spring error cases] exception of calling Bean object method and throwing null pointer in constructor

Posted by beselabios on Wed, 09 Mar 2022 01:49:35 +0100

preface

Sometimes, when you don't understand the way Spring manages Bean, you may encounter various problems, such as calling a method of Bean object in the container, such as:

@Service
public class MyServiceImpl {

    @Resource
    private UserServiceImpl userService;

    public MyServiceImpl() {
        userService.method();
    }

}

Then the following exception will be thrown:

Constructor threw exception; nested exception is java.lang.NullPointerException

1, Case demonstration

Both Service classes are injected into the Bean object container of Spring through the @ Service annotation. We hope that the MyServiceImpl object can call the init method of UserServiceImpl during initialization, so we write it as follows.

@Service
public class MyServiceImpl {

    @Resource
    private UserServiceImpl userService;

    public MyServiceImpl() {
        userService.init();
    }

}

@Service
public class UserServiceImpl {

    public void init() {
        System.out.println("UserServiceImpl method");
    }

}

Finally, you will find that null pointer exceptions will be thrown when the project starts.

2, Problem analysis

1. The main process of spring loading Bean objects

The reason why we make such a mistake is entirely because we don't understand the process of initializing Bean objects in Spring. Then, we might as well briefly analyze its key process first.

The process of Spring loading Bean objects can be roughly divided into three parts:

  • Scan all Bean objects that need to be managed by Spring.
  • Instantiate and initialize Bean objects.
  • Confirm whether the proxy object needs to be generated for the Bean object.

It can be confirmed that steps 1 and 3 are not related to our problem this time, so we focus on the second step.

The key method of instantiating and initializing Bean objects is doCreateBean. It can be seen that the process is mainly divided into three steps: instantiation, attribute injection and initialization.

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] 
		throws BeanCreationException {
	// ... Omit non critical code
	if (instanceWrapper == null) {
		// instantiation 
		instanceWrapper = createBeanInstance(beanName, mbd, args);
	}
	// ... Omit non critical code
	Object exposedObject = bean;
	try {
		// Dependency injection of bean s
		populateBean(beanName, mbd, instanceWrapper);
		// Initialize bean object
		exposedObject = initializeBean(beanName, exposedObject, mbd);
	}
	// ... Omit non critical code
}

Obviously, the problem with null pointers is that properties are not injected when the object is instantiated.

2. Problem solving

Now that the root cause of the problem has been found, the modification is very simple. There are many ways to realize our needs.

2.1. Construction method

The simplest way is to use constructor injection, because if it is constructor injection, Spring will instantiate the parameters in the constructor in advance, that is, the instantiation of UserServiceImpl is before MyServiceImpl.

@Service
public class MyServiceImpl {

    //@Resource
    //private UserServiceImpl userService;

    public MyServiceImpl(UserServiceImpl userService) {
        userService.method();
    }

}

2.2. Use @ PostConstruct annotation

@Service
public class MyServiceImpl {

    @Resource
    private UserServiceImpl userService;

    @PostConstruct
    public void init() {
        userService.method();
    }

}

This annotation is called when the Bean object is initialized

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
	// ... Omit non critical code
	Object wrappedBean = bean;
	if (mbd == null || !mbd.isSynthetic()) {
		// Call with @ PostConstruct annotation
		wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
	}
	try {
		invokeInitMethods(beanName, wrappedBean, mbd);
	}
	// ... Omit non critical code
}
@Override
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
		throws BeansException {
	Object result = existingBean;
	for (BeanPostProcessor processor : getBeanPostProcessors()) {
		Object current = processor.postProcessBeforeInitialization(result, beanName);
		if (current == null) {
			return result;
		}
		result = current;
	}
	return result;
}

Call the postProcessBeforeInitialization method of InitDestroyAnnotationBeanPostProcessor

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
	LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());
	try {
		metadata.invokeInitMethods(bean, beanName);
	}
	// ... Omit non critical code
}
private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {
	if (this.lifecycleMetadataCache == null) {
		// Happens after deserialization, during destruction...
		return buildLifecycleMetadata(clazz);
	}
	// ... Omit non critical code
}

Where initAnnotationType and destroyAnnotationType correspond to @ PostConstruct and @ PreDestroy annotations respectively

private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
	if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) {
		return this.emptyLifecycleMetadata;
	}
	List<LifecycleElement> initMethods = new ArrayList<>();
	List<LifecycleElement> destroyMethods = new ArrayList<>();
	Class<?> targetClass = clazz;
	do {
		final List<LifecycleElement> currInitMethods = new ArrayList<>();
		final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
		ReflectionUtils.doWithLocalMethods(targetClass, method -> {
			if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
				LifecycleElement element = new LifecycleElement(method);
				currInitMethods.add(element);
				if (logger.isTraceEnabled()) {
					logger.trace("Found init method on class [" + clazz.getName() + "]: " + method);
				}
			}
			if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {
				currDestroyMethods.add(new LifecycleElement(method));
				if (logger.isTraceEnabled()) {
					logger.trace("Found destroy method on class [" + clazz.getName() + "]: " + method);
				}
			}
		});
		initMethods.addAll(0, currInitMethods);
		destroyMethods.addAll(currDestroyMethods);
		targetClass = targetClass.getSuperclass();
	}
	while (targetClass != null && targetClass != Object.class);
	return (initMethods.isEmpty() && destroyMethods.isEmpty() ? this.emptyLifecycleMetadata :
			new LifecycleMetadata(clazz, initMethods, destroyMethods));
}

2.3. Implement InitializingBean interface

@Service
public class MyServiceImpl implements InitializingBean {

    @Resource
    private UserServiceImpl userService;

    @Override
    public void afterPropertiesSet() {
        userService.method();
    }
}

In addition to calling the @ PostConstruct annotated method, the initializeBean method also completes the afterpropertieset method call of the InitializingBean interface

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
	// ... Omit non critical code
	Object wrappedBean = bean;
	if (mbd == null || !mbd.isSynthetic()) {
		// Call with @ PostConstruct annotation
		wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
	}
	try {
		// Call the afterpropertieset method that implements the InitializingBean interface
		invokeInitMethods(beanName, wrappedBean, mbd);
	}
	// ... Omit non critical code
}
protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
		throws Throwable {
	boolean isInitializingBean = (bean instanceof InitializingBean);
	if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
		if (logger.isTraceEnabled()) {
			logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
		}
		if (System.getSecurityManager() != null) {
			try {
				AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
					((InitializingBean) bean).afterPropertiesSet();
					return null;
				}, getAccessControlContext());
			}
			catch (PrivilegedActionException pae) {
				throw pae.getException();
			}
		}
		else {
			((InitializingBean) bean).afterPropertiesSet();
		}
	}
	// ... Omit non critical code

2.4. Implement ApplicationContextAware interface

@Service
public class MyServiceImpl implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        UserServiceImpl userService = (UserServiceImpl) applicationContext.getBean("userServiceImpl");
        userService.method();
    }
}

In fact, the setApplicationContext method in the ApplicationContextAware interface is also called when the Bean object is initialized. The call entry is still the method of postProcessBeforeInitialization, but it is in the ApplicationContextAwareProcessor.

@Override
@Nullable
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
	if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
			bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
			bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)){
		return bean;
	}
	AccessControlContext acc = null;
	if (System.getSecurityManager() != null) {
		acc = this.applicationContext.getBeanFactory().getAccessControlContext();
	}
	if (acc != null) {
		AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
			invokeAwareInterfaces(bean);
			return null;
		}, acc);
	}
	else {
		// Call the corresponding interface method
		invokeAwareInterfaces(bean);
	}
	return bean;
}
private void invokeAwareInterfaces(Object bean) {
	if (bean instanceof EnvironmentAware) {
		((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
	}
	if (bean instanceof EmbeddedValueResolverAware) {
		((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
	}
	if (bean instanceof ResourceLoaderAware) {
		((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
	}
	if (bean instanceof ApplicationEventPublisherAware) {
		((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
	}
	if (bean instanceof MessageSourceAware) {
		((MessageSourceAware) bean).setMessageSource(this.applicationContext);
	}
	if (bean instanceof ApplicationContextAware) {
		((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
	}
}

summary

For the initial problem, we used four different ways to solve and realize the final business requirements. Obviously, these solutions can only be found after you analyze the source code. On the one hand, we have to admire the rich extension points left by Spring design, on the other hand, it also shows the importance of learning the source code.

Topics: Java Spring Back-end