В этой статье я хочу предложить простую задачку, которая поможет дать большее представление о 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>
-
Создадим интерфейс Animal
public interface Animal { void say(); }
-
Создадим класс Cat и реализуем интерфейс
public class Cat implements Animal { private int age; @Override public void say() { System.out.println("Meow"); } }
-
Пометим для Spring, что из этого класса нужно создать бин с id = "catBean"
@Component("catBean") public class Cat implements Animal { private int age; @Override public void say() { System.out.println("Meow"); } }
-
Создадим 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"); } }
-
Создадим свою кастомную аннотацию для случайных чисел
@Target(ElementType.FIELD)//Ставиться должна над полем @Retention(RetentionPolicy.RUNTIME)//Аннотация должна быть доступна в RUNTIME public @interface InjectRandomInt { int min() default 1; int max() default 15; }
-
Ставим нашу аннотацию над полем (значения 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 (потому что к моменту выполнения этого метода наше поле уже должно быть заполнено).
-
Создаем BPP и переопределяем метод
public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } }
-
Реализуем логику, по которой мы получаем бин (до выполнения у него 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;
}
}
-
Осталось только создать контекст и указать ему что сканировать на поиск бинов (в моем случае я не создавал конфигурационный класс, а вместо этого указал пакет)
public class LectionApplication { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("van.karm.lection.Classes"); } }
-
Из контекста я получаю бин кота, потому что хочу, чтобы он выполнил метод 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