A Spring Custom Annotation for a Better DAO – 用于更好的DAO的Spring自定义注释

最后修改: 2015年 10月 21日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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

在本教程中,我们将实现带有Bean后处理程序的自定义Spring注解

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.

那么这有什么帮助呢?简单地说–我们可以重复使用同一个Bean,而不必创建多个相同类型的类似Bean。

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

我们将为一个简单的项目中的DAO实现做这件事–用一个单一的、灵活的GenericDao取代所有的DAO。

2. Maven

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.

我们需要spring-corespring-aopspring-context-support JARs来实现工作。我们可以在我们的pom.xml中声明spring-context-support

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

如果你想选择较新的Spring依赖版本–请查看的maven仓库

3. New Generic DAO

3.新的通用DAO

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

大多数Spring / JPA / Hibernate的实现都使用标准的DAO–通常每个实体都有一个。

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:

我们将用GenericDao来替代该解决方案;我们将编写一个自定义的注释处理器,并使用该GenericDao实现。

3.1. Generic DAO

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

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.

在现实世界中,你当然需要在PersistenceContext中接线,并实际提供这些方法的实现。现在–我们将尽可能地使之简单化。

Now, lets create annotation for custom injection.

现在,让我们为自定义注入创建注释。

3.2. Data Access

3.2.数据访问

@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:

我们将使用上面的注解来注入一个GenericDao,如下。

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

也许你们中的一些人在问,”Spring如何识别我们的DataAccess注解?”。它没有–默认情况下没有。

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

但我们可以告诉Spring通过一个自定义的BeanPostProcessor来识别注释–接下来让我们来实现这一点。

3.3. DataAccessAnnotationProcessor

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:

接下来–这是我们刚才使用的DataAccessFieldCallback的实现。

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

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

现在–这是一个相当不错的实现–但其中最重要的部分是doWith()方法。

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.

这将告诉Spring根据运行时通过@DataAccess注解注入的对象来初始化Bean。

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.

beanName将确保我们得到一个唯一的bean实例,因为在这种情况下,我们确实想根据通过@DataAccess注解注入的实体来创建GenericDao的单一对象。

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

最后,接下来让我们在Spring配置中使用这个新的Bean处理器。

3.5. CustomAnnotationConfiguration

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.

这里很重要的一点是,@ComponentScan注解需要指向我们的自定义Bean post处理器所在的包,并确保它在运行时被Spring扫描和自动连接。

4. Testing the New DAO

4.测试新的DAO

Let’s start with a Spring enabled test and two simple example entity classes here – Person and Account.

让我们从一个启用了Spring的测试开始,这里有两个简单的实体类例子–PersonAccount

@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:

我们要在DataAccess注解的帮助下,注入GenericDao的几个实例。为了测试新Bean是否被正确注入,我们需要覆盖。

  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.

第1点实际上已经被Spring本身所覆盖–因为如果一个bean不能被连接进来,框架会很早就抛出一个异常。

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

为了测试第2点,我们需要看一下GenericDao的2个实例,它们都使用Person类。

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

我们不希望personGenericDao等同于accountGenericDao

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

但是我们确实希望personGenericDaoanotherPersonGenericDao是完全相同的实例。

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

为了测试第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. Conclusion

5.结论

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.

在这篇文章中,我们在Spring中做了一个非常酷的自定义注解的实现–连同一个BeanPostProcessor。总体目标是摆脱我们在持久层中通常有的多种DAO实现,并使用一个漂亮的、简单的通用实现,而在这个过程中没有任何损失。

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.

所有这些例子和代码片段的实现都可以在我的GitHub项目中找到 – 这是一个基于Eclipse的项目,所以应该很容易导入并按原样运行。