1. 概述

本文将带你实现一个基于 Spring 自定义注解 + BeanPostProcessor 的通用 DAO 解决方案

核心目标很明确:避免为每个实体类重复编写几乎相同的 DAO 类,转而使用一个灵活的 GenericDao,通过注解自动注入对应实体类型的 DAO 实例。

简单粗暴地说:✅ 一个 GenericDao 走天下,❌ 再也不用手动写一堆 PersonDaoOrderDao 了。

我们将通过自定义 @DataAccess 注解 + BeanPostProcessor 实现这一目标,过程中会踩到一些 Spring 容器的“坑”,但最终效果非常实用。

2. Maven 依赖

要实现这个功能,你需要引入 spring-context-support,它包含了 AOP 和上下文扩展所需的核心类。

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

💡 提示:推荐使用与项目主版本一致的 Spring 版本。最新版本可前往 Maven 仓库 查看。

3. 通用 DAO 与自定义注解实现

3.1. 通用 DAO 实现

传统项目中,每个实体都有一个对应的 DAO 接口和实现类,代码重复度高。我们用一个泛型 GenericDao 取代它们:

public class GenericDao<E> {

    private Class<E> entityClass;

    public GenericDao(Class<E> entityClass) {
        this.entityClass = entityClass;
    }

    public List<E> findAll() {
        // 模拟生成查询:SELECT * FROM person
        System.out.println("Would create findAll query from " + entityClass.getSimpleName());
        return new ArrayList<>();
    }

    public Optional<E> persist(E toPersist) {
        // 模拟插入操作
        System.out.println("Would create persist query from " + entityClass.getSimpleName());
        return Optional.of(toPersist);
    }
}

⚠️ 注意:真实项目中你需要注入 EntityManager 并实现具体逻辑。本文重点在“注解驱动”,所以持久化细节做了简化。

3.2. 自定义注解 @DataAccess

我们定义一个运行时注解,用于标记需要注入 GenericDao 的字段:

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

使用方式如下:

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

@DataAccess(entity = Account.class)
private GenericDao<Account> accountDao;

但 Spring 并不认识这个注解,怎么办?—— 用 BeanPostProcessor 告诉它!

3.3. DataAccessAnnotationProcessor:注解处理器

这个类是核心,它会在 Spring 容器初始化 Bean 前后扫描字段,发现 @DataAccess 就自动创建并注入对应的 GenericDao 实例。

@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 {
        scanDataAccessAnnotation(bean, beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) 
            throws BeansException {
        return bean;
    }

    protected void scanDataAccessAnnotation(Object bean, String beanName) {
        configureFieldInjection(bean);
    }

    private void configureFieldInjection(Object bean) {
        Class<?> managedBeanClass = bean.getClass();
        FieldCallback fieldCallback = new DataAccessFieldCallback(configurableBeanFactory, bean);
        ReflectionUtils.doWithFields(managedBeanClass, fieldCallback);
    }
}

✅ 关键点:

  • 实现 BeanPostProcessor,利用 postProcessBeforeInitialization 钩子
  • 使用 ReflectionUtils.doWithFields 遍历所有字段
  • 交给 DataAccessFieldCallback 处理具体逻辑

3.4. DataAccessFieldCallback:字段处理逻辑

这是真正干活的类,负责解析注解、创建 Bean 实例并注入字段。

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

✅ 核心逻辑解析:

  • genericTypeIsValid():校验泛型类型是否匹配,避免 @DataAccess(entity=Person.class) 被错误注入到 GenericDao<Order> 字段
  • getBeanInstance()
    • 生成唯一 beanName:PersonGenericDao
    • 若不存在,则通过反射创建实例
    • 手动调用 initializeBeanautowireBeanProperties 触发 Spring 生命周期
    • 注册为单例,确保相同 entity 共享同一个 DAO 实例

⚠️ 踩坑提醒:必须手动调用 initializeBeanautowireBeanProperties,否则 @AutowiredGenericDao 内部不会生效!

3.5. Spring 配置类

@Configuration
@ComponentScan("com.example.dao")
public class CustomAnnotationConfiguration {}

✅ 关键点:

  • @ComponentScan 必须包含 DataAccessAnnotationProcessor 所在包路径
  • 确保处理器被 Spring 扫描并注册为 Bean

4. 测试验证

我们用一个集成测试验证功能是否正常。

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

    @Test
    public void whenGenericDaoInjected_thenItIsSingleton() {
        // 不同实体 → 不同实例
        assertThat(personGenericDao, not(sameInstance(accountGenericDao)));
        assertThat(personGenericDao, not(equalTo(accountGenericDao)));

        // 相同实体 → 同一个单例实例
        assertThat(personGenericDao, sameInstance(anotherPersonGenericDao));
    }

    @Test
    public void whenFindAll_thenMessagesIsCorrect() {
        personGenericDao.findAll();
        // 模拟输出验证
        // 输出: Would create findAll query from Person

        accountGenericDao.findAll();
        // 输出: Would create findAll query from Account
    }

    @Test
    public void whenPersist_thenMessagesIsCorrect() {
        personGenericDao.persist(new Person());
        // 输出: Would create persist query from Person

        accountGenericDao.persist(new Account());
        // 输出: Would create persist query from Account
    }
}

测试结果:

  • ✅ 注入成功
  • ✅ 相同 entity 共享单例
  • ✅ 方法行为符合预期

5. 总结

本文实现了一个非常实用的 Spring 扩展方案:

  • 通过 @DataAccess + BeanPostProcessor 实现了泛型 DAO 的自动注入
  • 避免了大量重复的 DAO 类,提升代码简洁性
  • 利用 Spring 容器的 BeanFactory API 动态注册单例 Bean
  • 保留了类型安全和依赖注入能力

这个方案在中大型项目中尤其有价值,能显著减少持久层模板代码。

💡 项目源码已托管至 GitHub:https://github.com/example/spring-custom-dao-annotation(基于 Maven,导入即可运行)


原始标题:A Spring Custom Annotation for a Better DAO