Getting to Know BeanPostProcessor

In this article I want to offer a simple task that will help to give a better idea about BeanPostProcessor to those who have not worked with it. If you are an experienced user and know such a base, then this article will be useless for you.

Let's say I want to create a class Cat that implements the Animal interface and overrides the say() method. This class will have a private variable age, and I want it to be initialized automatically when the bean is created.

Also, I (for some reason) need to use this variable in the bean's init method. So by the time the method is run, the variable should already be filled with a random number.

A bit of theory

BeanPostProcessor (BPP) — is an interface that allows developers to perform additional configuration of beans before and after their initialization.

I will describe the algorithm for creating a bean using the example of annotation-config:
1. AnnotationConfigApplicationContext scans the specified package for classes annotated with @Component

2. ClassPathBeanDefinitionScanner performs a class-level scan and creates a BeanDefinition for each

3. BeanDefinitionsRegistry of all BeanDefinition creates a special Map “BeanDefinitions”

4. BeanFactoryPostProcessor (if specified in config) changes something in BeanDefenitions or in context

5. The context starts working according to BeanDefenitions:
1. Beret BeanDefinition
2. Configures according to the configuration (performing dependency injection and other settings specified in BeanDefinition)
3. Sends to everyone in turn BPP
4. The bean's init method is executed
5. Sent again to all BPP (in case of proxying)
6. If the scope of the bean is singleton, then puts it in the container

Solution

First, I'll add the necessary dependencies:

<dependencies>
		<!-- Spring Core -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>6.1.6</version>
		</dependency>
		<!-- Spring Context -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>6.1.6</version>
		</dependency>
		<!-- Annotation -->
		<dependency>
			<groupId>jakarta.annotation</groupId>
			<artifactId>jakarta.annotation-api</artifactId>
			<version>2.1.0</version>
		</dependency>
</dependencies>
  1. Let's create an Animal interface

    public interface Animal {
        void say();
    }
  2. Let's create a Cat class and inherit the interface

    public class Cat implements Animal {
        private int age;
    
        @Override
        public void say() {
            System.out.println("Meow");
        }
    }
  3. Let's mark Spring that a bean with id = “catBean” should be created from this class.

    @Component("catBean")
    public class Cat implements Animal {
        private int age;
        
        @Override
        public void say() {
            System.out.println("Meow");
        }
    }
  4. Let's create an init method with the @PostConstruct annotation (from the jakarta.annotation package)

    @Component("catBean")
    public class Cat implements Animal {
        private int age;
    
        @PostConstruct
        public void initMethod(){
            System.out.println("Hi! I'm "+age+" years old");
        }
    
        @Override
        public void say() {
            System.out.println("Meow");
        }
    }
  5. Let's create our own custom annotation for random numbers

    @Target(ElementType.FIELD)//Ставиться должна над полем
    @Retention(RetentionPolicy.RUNTIME)//Аннотация должна быть доступна в RUNTIME
    public @interface InjectRandomInt {
        int min() default 1;
        int max() default 15;
    }
  6. We put our annotation above the field (let the min and max values ​​be the default)

    @Component("catBean")
    public class Cat implements Animal {
        @InjectRandomInt
        private int age;
    
        @PostConstruct
        public void initMethod(){
            System.out.println("Hi! I'm "+age+" years old");
        }
    
        @Override
        public void say() {
            System.out.println("Meow");
        }
    }

Next we need to create a BPP that has two methods:

  • postProcessBeforeInitialization – a method that is executed before the bean's init method

  • postProcessAfterInitialization – a method that is executed after the bean's init method

I think you already understood which method we need – the one that is executed before the init method (because by the time this method is executed, our field should already be filled).

  1. Create a BPP and override the method

    public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor {
        
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
    }
  1. We implement the logic by which we receive a bean (before its init method is executed), check the fields of its class (for the fact of annotation @InjectRandomInt) and perform the appropriate actions

    public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor {
      
        private final Random random = new Random();//Создаем объект класса Random
      
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            Class<?> beanClass = bean.getClass();//Узнаем класс бина
            Field[] classFields = beanClass.getDeclaredFields();//Получаем все поля класса (включая закрытые)
            for (Field field:classFields){//Проходимся по списке полей
                if (field.isAnnotationPresent(InjectRandomInt.class)){//Проверяем помечено ли поле аннотацией @InjectRandomInt
                    InjectRandomInt annotation = field.getAnnotation(InjectRandomInt.class);//Получаем аннотацию с её параметрами
                    int min = annotation.min();//Узнаем параметр min из аннотации
                    int max = annotation.max();//Узнаем параметр max из аннотации
                    int randomValue = random.nextInt(max - min + 1) + min;//Генерируем случайное число
    
                    field.setAccessible(true);//Даем разрешение на изменение private поля
    
                    ReflectionUtils.setField(field,bean,randomValue);//Через рефлексию говорим: какое поле, какому бину и какое значение установить
                }
            }
            return bean;//Возвращаем бин с измененным полем
        }
    }

But for now it's just a class that isn't registered anywhere. How to fix it? Create a bean from it!
We've already done this using @Component
The final BPP code looks like this:

@Component
public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor {
  
    private final Random random = new Random();
  
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class<?> beanClass = bean.getClass();
        Field[] classFields = beanClass.getDeclaredFields();
        for (Field field:classFields){
            if (field.isAnnotationPresent(InjectRandomInt.class)){
                InjectRandomInt annotation = field.getAnnotation(InjectRandomInt.class);
                int min = annotation.min();
                int max = annotation.max();
                int randomValue = random.nextInt(max - min + 1) + min;

                field.setAccessible(true);

                ReflectionUtils.setField(field,bean,randomValue);
            }
        }
        return bean;
    }
}
  1. All that's left is to create a context and tell it what to scan for beans (in my case, I didn't create a configuration class, but instead specified a package)

    public class LectionApplication {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("van.karm.lection.Classes");
    	}
    }
  1. From the context I get the cat bean because I want it to execute the say() method

    public class LectionApplication {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("van.karm.lection.Classes");
    		Cat cat = (Cat) context.getBean("catBean");
    		cat.say();
    	}
    }

Conclusion

I knowthat this can be done differently, but this task demonstrates the work of BeanPostProcessor using a simple example.

To whom exactly? VERY don't like this example – I'll give you another: imagine that you have many services implementing different interfaces, but you need to add logging to those that implement a certain one (there can be hundreds of such services). In order not to prescribe logging for each one, you can check which interfaces the bean classes of these services implement and add logging to them.

Have fun 😀

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *