Bean post processors in Spring

on waitingforcode.com

Bean post processors in Spring

You're still doing Java/C#/JavaScript/Python/PHP... and need a wind of change? I was like that 4 years ago. I changed then to the data engineering field and it solved my existential problems :) If you want to follow my path, I prepared a course that will help you with that! Join the class!
We've already discovered the bean post processor factory in Spring. However, they left one similar concept to explain, bean post processors.

This article will be divided in two parts. In the first one, you will learn about Spring's single post processors beans. The second part will concern some a practical example of post processors use.

What is bean post processor ?

The bean life cycle starts with loading bean definition. Thanks to knowing the definition, Spring can construct the beans and inject components. After that, all beans can be post processed. It means that we can implement some custom logic and call it. The call is made by Spring container before and/or after the call of bean's initialization method (attribute init-method from definition).

You can't specify explicitly one bean post processor for a given bean type. Each defined post processor can be applied to all defined beans in application context. Post processor beans must implement org.springframework.beans.factory.config.BeanPostProcessor interface and define postProcessBeforeInitialization and postProcessAfterInitialization methods. The first one is invoked before the call of initialization method and the second one after this call. Both take two parameters:
- Object: represents the instance of processed bean.
- String: contains the name of processed bean.

As you can imagine now, we'll need to detect if one bean can be post processed or not. To avoid to make to much if-else cases, we can create an interface that will be implemented by all beans supporting post processing. Thanks to it, the code will be more readable.

Maybe do you remember the article about Bean factory post processors in Spring ? If not, read it too as a complement for current article. You need to understand that they are some differences between them. Bean factory post processors can act only on bean definitions. They are invoked before objects creation and it's the reason why they can change only beans metadata. Unlike BeanPostProcessors beans which can change objects properties. You can also guess that if bean factory post processors and post processor override the same object's property, retained value will be those set by bean post processor. It's because it's invoked after bean factory post processors.

Example of bean post processor

In our case, we want to invalidate beans that can't be used at the deployment moment. We can imagine that we have a VOD streaming site where all films can be viewed free of charge the first week of the month. The verification code looks like:

@Controller
public MovieController {
  @Autowired
  private ViewChecker viewChecker;

  // some of request mapped methods

  // check method
  private boolean movieCanBeWatched(Movie movie) {
    if (viewChecker == null) {
      return true;
    }
    return viewChecker.canBeWatched(movie);
  }
}

we make an A&B testing for a bean that gets and formats the list of products in a web shop. The first bean make it by getting the most visited items. The second one is based on user preferencies. It means that it takes the most liked products. First, let's define a bean configuration:



The first bean represents the post processor bean. The second one, viewChecker, is a class that will check if user can view a movie. First, let's take a look on the second class:

public class ViewChecker implements ProcessedBean {

  @Override
  public boolean isValid() {
    // visitors can watch movies freely between the 1st and 7th day of every month
    Calendar calendar = Calendar.getInstance();
    return calendar.get(Calendar.DAY_OF_MONTH) > 8;
  }
}

As you can see, they aren't a lot of code. The most important information to retain is the implementation of ProcessedBean interface, presented as below:

public interface ProcessedBean {
  public boolean isValid();	
}

All beans implementing this interface must override isValid() method to indicates if the bean can be used or not by the application context. The call of ifValid method is made in BeanPostProcessorSample:

public class BeanPostProcessorSample implements BeanPostProcessor  {
  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    if (bean instanceof ProcessedBean) {
      if (!((ProcessedBean)bean).isValid()) {
        return null;
      }
    }
    return bean;
  }

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    return bean;
  }

}

As you see, we implement only the afterInitialization post processor method. Thanks to it we are sure that analyzed bean has the data potentially set in init-method (if specified). If analyzed bean is not valid, we return null. But beware about returning this nulls. If invalid bean is used as a dependency by another one, you can see the exceptions like that:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'adminController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'adminService': Injection of resource dependencies failed; nested exception is java.lang.IllegalArgumentException: DataSource must not be null
  at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:307)
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1185)
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
  at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:700)
  at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)
  at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)
  at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:381)
  at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:293)
  at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106)
  at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4701)
  at org.apache.catalina.core.StandardContext$1.call(StandardContext.java:5204)
  at org.apache.catalina.core.StandardContext$1.call(StandardContext.java:5199)
  at java.util.concurrent.FutureTask$Sync.innerRun(Unknown Source)
  at java.util.concurrent.FutureTask.run(Unknown Source)
  at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
  at java.lang.Thread.run(Unknown Source)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'adminService': Injection of resource dependencies failed; nested exception is java.lang.IllegalArgumentException: DataSource must not be null
  at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:307)
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1185)
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
  at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
  at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.autowireResource(CommonAnnotationBeanPostProcessor.java:445)
  at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.getResource(CommonAnnotationBeanPostProcessor.java:419)
  at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor$ResourceElement.getResourceToInject(CommonAnnotationBeanPostProcessor.java:544)
  at org.springframework.beans.factory.annotation.InjectionMetadata$InjectedElement.inject(InjectionMetadata.java:150)
  at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
  at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:304)
  ... 21 more
Caused by: java.lang.IllegalArgumentException: DataSource must not be null
  at org.springframework.util.Assert.notNull(Assert.java:112)
  at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.(NamedParameterJdbcTemplate.java:89)
  at org.springframework.jdbc.core.simple.SimpleJdbcTemplate.(SimpleJdbcTemplate.java:70)
  at com.waitingforcode.service.AdminService.setDataSource(AdminService.java:38)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
  at java.lang.reflect.Method.invoke(Unknown Source)
  at org.springframework.beans.factory.annotation.InjectionMetadata$InjectedElement.inject(InjectionMetadata.java:159)
  at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
  at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:304)
  ... 34 more

This problem occurred in adminService creation, at this level:

@Service("adminService")
public class AdminService implements GenericService {

  private SimpleJdbcTemplate jdbcTemplate;

  @Resource(name="dataSource")
  public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
  }
}

This article shows that we can manipulate beans at almost every level of theirs life cycle. We can change the beans definitions in the fly with BeanFactoryPostProcessors, but also beans objects with bean post processors. But before changing anything, you can analyze the dependencies. Because invalidating a bean (by returning null in post processor methods) can cause the problems with initialization of dependent beans.

Share on: