Пользовательская аннотация Spring для лучшего DAO

1. Обзор

В этом руководстве мы реализуем пользовательскую аннотацию Spring с постпроцессором бина .

Так как это поможет? Проще говоря - мы можем повторно использовать один и тот же bean-компонент вместо создания нескольких похожих bean-компонентов одного типа.

Мы сделаем это для реализаций DAO в простом проекте - заменив их все одним гибким GenericDao .

2. Maven

Нам нужны JAR-файлы spring-core , spring-aop и spring-context-support , чтобы это работало. Мы можем просто объявить spring-context-support в нашем pom.xml .

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>4.2.2.RELEASE</version>
</dependency>

Если вы хотите перейти на более новую версию зависимости Spring - посмотрите https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework%22%20AND%20a%3A % 22spring-context-support% 22[хранилище maven].

** 3. Новый универсальный DAO

**

Большинство реализаций Spring/JPA/Hibernate используют стандарт DAO - обычно по одному для каждой сущности.

Мы собираемся заменить это решение на GenericDao ; вместо этого мы собираемся написать собственный процессор аннотаций и использовать реализацию GenericDao :

3.1. Универсальный DAO

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. DataAccessAnnotationProcessor

@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 будет гарантировать, что мы получим уникальный экземпляр компонента, потому что - в этом случае - мы хотим создать один объект GenericDao в зависимости от объекта, введенного с помощью аннотации @ DataAccess .

Наконец, давайте теперь будем использовать этот новый процессор bean-компонентов в конфигурации Spring.

3.5. CustomAnnotationConfiguration

@Configuration
@ComponentScan("com.baeldung.springcustomannotation")
public class CustomAnnotationConfiguration {}

Здесь важно то, что значение аннотации @ComponentScan необходимо укажите на пакет, в котором находится наш пользовательский постпроцессор bean-компонента, и убедитесь, что он отсканирован и автоматически подключен 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-компоненты, нам нужно покрыть:

, Если инъекция прошла успешно

, Если экземпляры 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 и anotherPersonGenericDao были точно такими же экземплярами.

Чтобы проверить пункт 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, которые мы обычно имеем на нашем уровне персистентности, и использовать красивую, простую универсальную реализацию, не теряя ничего в процессе.

Реализация всех этих примеров и фрагментов кода может быть найдена в my github project - это проект на основе Eclipse, поэтому его легко импортировать и запускать как есть.