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>5.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 losing 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-is.

Course – LSD (cat=Persistence)

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE
res – Persistence (eBook) (cat=Persistence)
Comments are closed on this article!