В этой статье я хочу предложить простую задачку, которая поможет дать большее представление о BeanPostProcessor тем, кто с ним не работал. Если вы бывалый и такую базу знаете, то эта статья будет для вас бесполезной.

Допустим, я хочу создать класс Cat, который будет реализовывать интерфейс Animal и переопределять метод say(). У этого класса будет приватная переменная age, и я хочу, чтобы она инициализировалась автоматически при создании бина.

Кроме того, мне (по какой-то причине) нужно использовать эту переменную в init-методе бина. Поэтому к моменту запуска метода переменная уже должна быть заполнена случайным числом.

Немного теории

BeanPostProcessor (BPP) — это интерфейс, который позволяет разработчикам выполнять дополнительные настройки бинов до и после их инициализации.

Опишу алгоритм создания бина на примере annotation-config:
1. AnnotationConfigApplicationContext сканирует указанный пакет, на предмет классов аннотированных @Component

2. ClassPathBeanDefinitionScanner выполняет сканирование на уровне класса и создает BeanDefinition для каждого

3. BeanDefinitionsRegistry из всех BeanDefinition создает специальную Map "BeanDefinitions"

4. BeanFactoryPostProcessor (если указаны в конфигурации) что-то изменяет в BeanDefenitions или в контексте

5. Контекст начинает работать по BeanDefenitions:
1. Берет BeanDefinition
2. Настраивает согласно конфигурации (выполняя внедрение зависимостей и другие настройки, указанные в BeanDefinition)
3. Отсылает по очереди ко всем BPP
4. Выполняется init-метод бина
5. Отправляется еще раз по всем BPP (на случай проксирования)
6. Если scope бина singleton, то кладет его в контейнер

Решение

Для начала я добавлю необходимые зависимости:

<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. Создадим интерфейс Animal

    public interface Animal {
        void say();
    }
  2. Создадим класс Cat и реализуем интерфейс

    public class Cat implements Animal {
        private int age;
    
        @Override
        public void say() {
            System.out.println("Meow");
        }
    }
  3. Пометим для Spring, что из этого класса нужно создать бин с id = "catBean"

    @Component("catBean")
    public class Cat implements Animal {
        private int age;
        
        @Override
        public void say() {
            System.out.println("Meow");
        }
    }
  4. Создадим init-метод аннотацией @PostConstruct (из пакета jakarta.annotation)

    @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. Создадим свою кастомную аннотацию для случайных чисел

    @Target(ElementType.FIELD)//Ставиться должна над полем
    @Retention(RetentionPolicy.RUNTIME)//Аннотация должна быть доступна в RUNTIME
    public @interface InjectRandomInt {
        int min() default 1;
        int max() default 15;
    }
  6. Ставим нашу аннотацию над полем (значения min и max пусть будут по умолчанию)

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

Далее нам необходимо создать BPP, у которого есть два метода:

  • postProcessBeforeInitialization - метод, который выполняется до init-метода бина

  • postProcessAfterInitialization - метод, который выполняется после init-метода бина

Я думаю, вы уже поняли, какой метод нам нужен — тот, который выполняется до метода init (потому что к моменту выполнения этого метода наше поле уже должно быть заполнено).

  1. Создаем BPP и переопределяем метод

    public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor {
        
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
    }
  1. Реализуем логику, по которой мы получаем бин (до выполнения у него init-метода), проверяем поля его класса (на факт аннотирования @InjectRandomInt) и выполняем соответствующие действия

    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;//Возвращаем бин с измененным полем
        }
    }

Но пока что это просто класс, который нигде не зарегистрирован. Как это исправить? Создать из него бин!
Это мы уже делали, при помощи @Component
Конечный код BPP выглядит так:

@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. Осталось только создать контекст и указать ему что сканировать на поиск бинов (в моем случае я не создавал конфигурационный класс, а вместо этого указал пакет)

    public class LectionApplication {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("van.karm.lection.Classes");
    	}
    }
  1. Из контекста я получаю бин кота, потому что хочу, чтобы он выполнил метод say()

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

Заключение

Я знаю, что это можно сделать по другому, но эта задача на простом примере демонстрирует работу BeanPostProcessor.

Кому вот прям ОЧЕНЬ не нравится такой пример - дам другой: представьте, что у вас множество сервисов, реализующих разные интерфейсы, но вам нужно добавить логирование именно тем, которые реализуют определенный (таких сервисов может быть сотня). Чтобы не прописывать логирование каждому, вы можете проверить какие интерфейсы реализуют классы бинов этих сервисов и добавить им логирование.

Веселитесь :D

Комментарии (3)