A Spring Custom Annotation for a Better DAO

1. Overview

In this tutorial, we’ll implement a custom Spring annotation with a
bean post-processor
.

So how does this help? Simply put – we can reuse the same bean instead
of having to create multiple, similar beans of the same type.

We’ll do that for the DAO implementations in a simple project –
replacing all of them with a single, flexible GenericDao.

2. Maven

We need spring-core, spring-aop, and spring-context-support JARs
to get this working. We can just declare spring-context-support in our
pom.xml.

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

If you want to go for a newer version of the Spring dependency – check
out
the
maven repository
.

*3. New Generic DAO

*

Most Spring / JPA / Hibernate implementation use the standard DAO –
usually one for each entity.

We’re going to be replacing that solution with a GenericDao; we’re
going to write a custom annotation processor instead and use that
GenericDao implementation:

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

In a real world scenario, you’ll of course need to wire in a
PersistenceContext
and actually provide the implementations of these methods. For now –
we’ll make this as simple as possible.

Now, lets create annotation for custom injection.

3.2. Data Access

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

We’ll use the annotation above to inject a GenericDao as follows:

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

Maybe some of you asking, “How does Spring recognize our DataAccess
annotation?”. It doesn’t – not by default.

But we could tell Spring to recognize the annotation via a custom
BeanPostProcessor
– let’s get this implemented next.

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

Next – here’s the implementation of the DataAccessFieldCallback we
just used:

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

Now – that’s quite an implementation – but the most important part of it
is the doWith() method:

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

This would tell Spring to initialize a bean based on the object injected
at runtime via the @DataAccess annotation.

The beanName will make sure that we’ll get an unique instance of the
bean because – in this case – we do want to create single object of
GenericDao depending on the entity injected via the @DataAccess
annotation.

Finally, let’s use this new bean processor in a Spring configuration
next.

3.5. CustomAnnotationConfiguration

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

One thing that important here is that, value of the
@ComponentScan
annotation needs to point to the package where our custom bean post
processor is located and make sure that it scanned and autowired by
Spring at runtime.

*4. Testing the new DAO

*

Let’s start with a Spring enabled test and two simple example entity
classes here – Person and 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;

    ...
}

We’re injecting a few instances of the GenericDao with the help of the
DataAccess annotation. To test that the new beans are correctly
injected, we’ll need to cover:

  1. If injection is successful

  2. If bean instances with the same entity are the same

  3. If the methods in the GenericDao actually work as expected

Point 1 is actually covered by Spring itself – as the framework throws
an exception quite early if a bean cannot be wired in.

To test point 2, we need to look at the 2 instances of the GenericDao
that both use the Person class:

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

We don’t want personGenericDao to be equal to the accountGenericDao.

But we do want the personGenericDao and anotherPersonGenericDao to
be exactly the same instance.

To test point 3, we just test some simple persistence related logic
here:

@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

In this article we did a very cool implementation of a custom annotation
in Spring – along with a BeanPostProcessor. The overall goal was to
get rid of the multiple DAO implementations we usually have in our
persistence layer and use a nice, simple generic implementation without
loosing anything in the process.

The implementation of all these examples and code snippets can be found
in
my
github project
– this is an Eclipse based project, so it should be
easy to import and run as it is.

Leave a Reply

Your email address will not be published.