Une annotation personnalisée printanière pour un meilleur DAO

1. Vue d’ensemble

Dans ce tutoriel, nous allons implémenter une annotation Spring personnalisée avec un post-processeur bean .

Alors, comment cela aide-t-il? En termes simples, nous pouvons réutiliser le même bean au lieu de créer plusieurs beans similaires du même type.

Nous allons faire cela pour les implémentations DAO dans un projet simple - en les remplaçant toutes par un unique, génériqueDao__ flexible.

2. Maven

Nous avons besoin des fichiers JAR spring-core , spring-aop et spring-context-support-support . Nous pouvons simplement déclarer spring-context-support dans notre pom.xml .

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

Si vous voulez utiliser une version plus récente de la dépendance Spring, consultez https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework%22%20AND%20a%3A % 22spring-context-support% 22[le référentiel maven].

** 3. Nouveau DAO générique

**

La plupart des implémentations Spring/JPA/Hibernate utilisent le DAO standard - généralement un pour chaque entité.

Nous allons remplacer cette solution par un GenericDao ; nous allons plutôt écrire un processeur d’annotation personnalisé et utiliser cette implémentation GenericDao :

3.1. DAO générique

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) {
       //...
    }
}

Dans un scénario réel, vous devrez évidemment câbler un fichier PersistenceContext et fournir les implémentations de ces méthodes. . Pour le moment, nous allons rendre cela aussi simple que possible.

Créons maintenant une annotation pour une injection personnalisée.

3.2. Accès aux données

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Documented
public @interface DataAccess {
    Class<?> entity();
}

Nous allons utiliser l’annotation ci-dessus pour injecter un GenericDao comme suit:

@DataAccess(entity=Person.class)
private GenericDao<Person> personDao;

Peut-être que certains d’entre vous demandent: "Comment Spring reconnaît-il notre annotation DataAccess ?". Ce n’est pas le cas - pas par défaut.

Mais nous pourrions dire à Spring de reconnaître l’annotation via un fichier personnalisé: BeanPostProcessor - mettons ceci en œuvre ensuite.

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

Ensuite, voici l’implémentation de DataAccessFieldCallback que nous venons d’utiliser:

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

Maintenant, c’est tout à fait une implémentation, mais l’essentiel est la méthode doWith ()

genericDaoInstance = configurableBeanFactory.initializeBean(beanToRegister, beanName);
configurableBeanFactory.autowireBeanProperties(genericDaoInstance, autowireMode, true);
configurableBeanFactory.registerSingleton(beanName, genericDaoInstance);

Cela indiquerait à Spring d’initialiser un bean en fonction de l’objet injecté au moment de l’exécution via l’annotation @ DataAccess .

BeanName s’assurera que nous obtiendrons une instance unique du bean car, dans ce cas, nous voulons créer un seul objet de GenericDao en fonction de l’entité injectée via l’annotation @ DataAccess .

Enfin, utilisons ce nouveau processeur de beans dans une configuration Spring.

3.5. CustomAnnotationConfiguration

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

Une chose qui est importante ici est que la valeur de la @ComponentScan annotation doit être pointez sur le package où se trouve notre post-processeur de beans personnalisé et assurez-vous qu’il a été scanné et câblé automatiquement par Spring au moment de l’exécution.

** 4. Tester le nouveau DAO

**

Commençons par un test activé par Spring et deux exemples d’entités simples: Person et 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;

    ...
}

Nous injectons quelques instances de GenericDao à l’aide de l’annotation DataAccess . Pour vérifier que les nouveaux haricots sont correctement injectés, nous devrons couvrir:

  1. Si l’injection est réussie

  2. Si les instances de bean avec la même entité sont les mêmes

  3. Si les méthodes du GenericDao fonctionnent réellement comme prévu

Le point 1 est en fait couvert par Spring lui-même, car le cadre lève une exception assez tôt si un haricot ne peut pas être câblé.

Pour tester le point 2, nous devons examiner les 2 instances du GenericDao qui utilisent toutes deux la classe Person :

@Test
public void whenGenericDaoInjected__thenItIsSingleton() {
    assertThat(personGenericDao, not(sameInstance(accountGenericDao)));
    assertThat(personGenericDao, not(equalTo(accountGenericDao)));
    assertThat(personGenericDao, sameInstance(anotherPersonGenericDao));
}

Nous ne voulons pas que personGenericDao soit égal à accountGenericDao .

Mais nous voulons que personGenericDao et anotherPersonGenericDao soient exactement la même instance.

Pour tester le point 3, nous testons simplement une logique simple liée à la persistance:

@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. Conclusion

Dans cet article, nous avons effectué une mise en œuvre très cool d’une annotation personnalisée dans Spring - avec un BeanPostProcessor . L’objectif global était de se débarrasser des multiples implémentations DAO que nous avons habituellement dans notre couche de persistance et d’utiliser une implémentation générique simple et agréable sans rien perdre du processus.

Vous trouverez la mise en œuvre de tous ces exemples et extraits de code dans my github project - il s’agit d’un projet basé sur Eclipse, donc il devrait être facile à importer et à exécuter tel quel.