1. Обзор
В этом руководстве мы реализуем пользовательскую аннотацию Spring с постпроцессором компонента .
Итак, как это помогает? Проще говоря, мы можем повторно использовать один и тот же bean-компонент вместо того, чтобы создавать несколько похожих bean-компонентов одного типа.
Мы сделаем это для реализаций DAO в простом проекте — заменив их все одним гибким GenericDao
.
2. Мавен
Нам нужны JAR-файлы spring-core
, spring-aop
и spring-context-support
, чтобы это заработало. Мы можем просто объявить поддержку spring-context
в нашем pom.xml
.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
Если вы хотите перейти на более новую версию зависимости Spring — загляните в репозиторий maven .
3. Новый общий DAO
В большинстве реализаций Spring/JPA/Hibernate используется стандартный DAO — обычно по одному для каждой сущности.
Мы собираемся заменить это решение на GenericDao
; вместо этого мы собираемся написать собственный обработчик аннотаций и использовать эту реализацию GenericDao
:
3.1. Общий ДАО
public class GenericDao<E> {
private Class<E> entityClass;
public GenericDao(Class<E> entityClass) {
this.entityClass = entityClass;
}
public List<E> findAll() {
// ...
}
public Optional<E> persist(E toPersist) {
// ...
}
}
В реальном сценарии вам, конечно, потребуется подключить PersistenceContext и фактически предоставить реализации этих методов. А пока — мы сделаем это как можно проще.
Теперь давайте создадим аннотацию для пользовательской инъекции.
3.2. Доступ к данным
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Documented
public @interface DataAccess {
Class<?> entity();
}
Мы будем использовать приведенную выше аннотацию для внедрения GenericDao
следующим образом:
@DataAccess(entity=Person.class)
private GenericDao<Person> personDao;
Возможно, некоторые из вас спрашивают: «Как Spring распознает нашу аннотацию DataAccess
?». Это не так — не по умолчанию.
Но мы могли бы сказать Spring распознавать аннотацию через собственный BeanPostProcessor
— давайте реализуем это дальше.
3.3. Процессор аннотаций доступа к данным
@Component
public class DataAccessAnnotationProcessor implements BeanPostProcessor {
private ConfigurableListableBeanFactory configurableBeanFactory;
@Autowired
public DataAccessAnnotationProcessor(ConfigurableListableBeanFactory beanFactory) {
this.configurableBeanFactory = beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
this.scanDataAccessAnnotation(bean, beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
protected void scanDataAccessAnnotation(Object bean, String beanName) {
this.configureFieldInjection(bean);
}
private void configureFieldInjection(Object bean) {
Class<?> managedBeanClass = bean.getClass();
FieldCallback fieldCallback =
new DataAccessFieldCallback(configurableBeanFactory, bean);
ReflectionUtils.doWithFields(managedBeanClass, fieldCallback);
}
}
Далее — вот реализация DataAccessFieldCallback
, которую мы только что использовали:
3.4. DataAccessFieldCallback
public class DataAccessFieldCallback implements FieldCallback {
private static Logger logger = LoggerFactory.getLogger(DataAccessFieldCallback.class);
private static int AUTOWIRE_MODE = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;
private static String ERROR_ENTITY_VALUE_NOT_SAME = "@DataAccess(entity) "
+ "value should have same type with injected generic type.";
private static String WARN_NON_GENERIC_VALUE = "@DataAccess annotation assigned "
+ "to raw (non-generic) declaration. This will make your code less type-safe.";
private static String ERROR_CREATE_INSTANCE = "Cannot create instance of "
+ "type '{}' or instance creation is failed because: {}";
private ConfigurableListableBeanFactory configurableBeanFactory;
private Object bean;
public DataAccessFieldCallback(ConfigurableListableBeanFactory bf, Object bean) {
configurableBeanFactory = bf;
this.bean = bean;
}
@Override
public void doWith(Field field)
throws IllegalArgumentException, IllegalAccessException {
if (!field.isAnnotationPresent(DataAccess.class)) {
return;
}
ReflectionUtils.makeAccessible(field);
Type fieldGenericType = field.getGenericType();
// In this example, get actual "GenericDAO' type.
Class<?> generic = field.getType();
Class<?> classValue = field.getDeclaredAnnotation(DataAccess.class).entity();
if (genericTypeIsValid(classValue, fieldGenericType)) {
String beanName = classValue.getSimpleName() + generic.getSimpleName();
Object beanInstance = getBeanInstance(beanName, generic, classValue);
field.set(bean, beanInstance);
} else {
throw new IllegalArgumentException(ERROR_ENTITY_VALUE_NOT_SAME);
}
}
public boolean genericTypeIsValid(Class<?> clazz, Type field) {
if (field instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) field;
Type type = parameterizedType.getActualTypeArguments()[0];
return type.equals(clazz);
} else {
logger.warn(WARN_NON_GENERIC_VALUE);
return true;
}
}
public Object getBeanInstance(
String beanName, Class<?> genericClass, Class<?> paramClass) {
Object daoInstance = null;
if (!configurableBeanFactory.containsBean(beanName)) {
logger.info("Creating new DataAccess bean named '{}'.", beanName);
Object toRegister = null;
try {
Constructor<?> ctr = genericClass.getConstructor(Class.class);
toRegister = ctr.newInstance(paramClass);
} catch (Exception e) {
logger.error(ERROR_CREATE_INSTANCE, genericClass.getTypeName(), e);
throw new RuntimeException(e);
}
daoInstance = configurableBeanFactory.initializeBean(toRegister, beanName);
configurableBeanFactory.autowireBeanProperties(daoInstance, AUTOWIRE_MODE, true);
configurableBeanFactory.registerSingleton(beanName, daoInstance);
logger.info("Bean named '{}' created successfully.", beanName);
} else {
daoInstance = configurableBeanFactory.getBean(beanName);
logger.info(
"Bean named '{}' already exists used as current bean reference.", beanName);
}
return daoInstance;
}
}
Теперь — это довольно реализация — но самая важная ее часть — это метод doWith()
:
genericDaoInstance = configurableBeanFactory.initializeBean(beanToRegister, beanName);
configurableBeanFactory.autowireBeanProperties(genericDaoInstance, autowireMode, true);
configurableBeanFactory.registerSingleton(beanName, genericDaoInstance);
Это укажет Spring инициализировать bean-компонент на основе объекта, введенного во время выполнения с помощью аннотации @DataAccess
.
beanName гарантирует
, что мы получим уникальный экземпляр bean-компонента, потому что — в этом случае — мы хотим создать единый объект GenericDao
в зависимости от объекта, внедренного через аннотацию @DataAccess
.
Наконец, давайте использовать этот новый bean-процессор в конфигурации Spring.
3.5. Конфигурация пользовательской аннотации
@Configuration
@ComponentScan("com.foreach.springcustomannotation")
public class CustomAnnotationConfiguration {}
Здесь важно то, что значение аннотации @ComponentScan
должно указывать на пакет, в котором находится наш пользовательский постпроцессор компонента, и убедиться, что он сканируется и автоматически подключается Spring во время выполнения.
4. Тестирование нового DAO
Давайте начнем с теста с поддержкой Spring и двух простых примеров классов сущностей — Person
и Account
.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={CustomAnnotationConfiguration.class})
public class DataAccessAnnotationTest {
@DataAccess(entity=Person.class)
private GenericDao<Person> personGenericDao;
@DataAccess(entity=Account.class)
private GenericDao<Account> accountGenericDao;
@DataAccess(entity=Person.class)
private GenericDao<Person> anotherPersonGenericDao;
...
}
Мы внедряем несколько экземпляров GenericDao
с помощью аннотации DataAccess
. Чтобы проверить правильность внедрения новых bean-компонентов, нам нужно покрыть:
- Если инъекция прошла успешно
- Если экземпляры компонента с одним и тем же объектом одинаковы
- Если методы в
GenericDao
действительно работают так, как ожидалось
Пункт 1 на самом деле покрывается самим Spring — поскольку фреймворк выдает исключение довольно рано, если bean-компонент не может быть подключен.
Чтобы проверить точку 2, нам нужно посмотреть на 2 экземпляра GenericDao
, которые оба используют класс Person :
@Test
public void whenGenericDaoInjected_thenItIsSingleton() {
assertThat(personGenericDao, not(sameInstance(accountGenericDao)));
assertThat(personGenericDao, not(equalTo(accountGenericDao)));
assertThat(personGenericDao, sameInstance(anotherPersonGenericDao));
}
Мы не хотим, чтобы personGenericDao
был равен accountGenericDao
.
Но мы хотим, чтобы personGenericDao
и otherPersonGenericDao
были одним и тем же экземпляром.
Чтобы проверить пункт 3, мы просто тестируем здесь простую логику, связанную с сохраняемостью:
@Test
public void whenFindAll_thenMessagesIsCorrect() {
personGenericDao.findAll();
assertThat(personGenericDao.getMessage(),
is("Would create findAll query from Person"));
accountGenericDao.findAll();
assertThat(accountGenericDao.getMessage(),
is("Would create findAll query from Account"));
}
@Test
public void whenPersist_thenMessagesIsCorrect() {
personGenericDao.persist(new Person());
assertThat(personGenericDao.getMessage(),
is("Would create persist query from Person"));
accountGenericDao.persist(new Account());
assertThat(accountGenericDao.getMessage(),
is("Would create persist query from Account"));
}
5. Вывод
В этой статье мы сделали очень классную реализацию пользовательской аннотации в Spring — вместе с BeanPostProcessor
. Общая цель состояла в том, чтобы избавиться от нескольких реализаций DAO, которые мы обычно имеем на нашем уровне постоянства, и использовать красивую, простую универсальную реализацию, ничего не теряя в процессе.
Реализацию всех этих примеров и фрагментов кода можно найти в моем проекте GitHub — это проект на основе Eclipse, поэтому его легко импортировать и запускать как есть.