1. 概述

本文的核心目标是通过一个泛型化的通用数据访问对象(DAO)来简化整个系统的 DAO 层,从而实现简洁优雅的数据访问逻辑,避免大量重复代码和冗余类。

我们基于之前文章中介绍的 Spring 与 Hibernate 持久层实现 中的抽象 DAO 类,进一步引入 Java 泛型机制,提升复用性和类型安全性。

最终效果:✅ 所有实体共用一个 DAO 实现,❌ 不再为每个实体写一套 CRUD 类。


2. Hibernate 与 JPA 的 DAO 实现

在大多数生产级项目中,都会存在 DAO 层。常见的实现方式五花八门:

  • 完全没有基类,每个实体独立实现
  • 存在一个抽象基类,子类继承并特化
  • 使用泛型减少重复

但一个普遍现象是:DAO 类的数量往往和实体类一一对应。这不仅增加了维护成本,也违背了 DRY 原则。

✅ 正确做法:用一个泛型 DAO 替代多个重复实现。
⚠️ 踩坑提醒:不要为了“看起来规范”而盲目创建一堆空壳 DAO。

我们可以通过泛型 + 抽象基类的方式,将核心 CRUD 逻辑集中管理。下面分别展示基于 Hibernate 和 JPA 的两种实现。

2.1 抽象 Hibernate DAO

public abstract class AbstractHibernateDao<T extends Serializable> {
    private Class<T> clazz;

    @Autowired
    protected SessionFactory sessionFactory;

    public void setClazz(final Class<T> clazzToSet) {
        clazz = Preconditions.checkNotNull(clazzToSet);
    }

    public T findOne(final long id) {
        return (T) getCurrentSession().get(clazz, id);
    }

    public List<T> findAll() {
        return getCurrentSession().createQuery("from " + clazz.getName()).list();
    }

    public T create(final T entity) {
        Preconditions.checkNotNull(entity);
        getCurrentSession().saveOrUpdate(entity);
        return entity;
    }

    public T update(final T entity) {
        Preconditions.checkNotNull(entity);
        return (T) getCurrentSession().merge(entity);
    }

    public void delete(final T entity) {
        Preconditions.checkNotNull(entity);
        getCurrentSession().delete(entity);
    }

    public void deleteById(final long entityId) {
        final T entity = findOne(entityId);
        Preconditions.checkState(entity != null);
        delete(entity);
    }

    protected Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }
}

关键点说明:

  • clazz 用于运行时确定操作的实体类型
  • 使用 SessionFactory 直接操作持久化上下文
  • Preconditions.checkNotNull() 来校验参数合法性(来自 Google Guava),比手动 if 判断更简洁且异常信息更友好
  • createQuery("from " + clazz.getName()) 是 HQL 查询,注意实体名是类全名

✅ 优点:封装了通用操作,子类无需重复写 CRUD。
⚠️ 注意:HQL 中的类名必须是全限定名,否则报错。


2.2 通用 Hibernate DAO

有了抽象基类后,我们只需要一个泛型实现类即可覆盖所有实体:

@Repository
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class GenericHibernateDao<T extends Serializable>
  extends AbstractHibernateDao<T> implements IGenericDao<T>{
   //
}

重点解析:

✅ 为什么使用泛型?

  • 客户端可通过 <User><Order> 等指定具体类型
  • 编译期类型安全,无需强制转换
  • 避免为每个实体创建新类

✅ 为什么是 prototype 作用域?

@Scope(BeanDefinition.SCOPE_PROTOTYPE)

这是关键!⚠️ 很多人在这里踩坑。

  • 默认 Spring Bean 是 singleton(单例)
  • 但泛型 DAO 需要绑定不同的 clazz(比如 User.class 或 Product.class)
  • 如果是单例,第一次注入后 clazz 就固定了,后续所有调用都指向同一个实体类型 ❌

使用 prototype 后,每次依赖注入都会创建新实例,确保每个服务拿到的是“专属”配置好的 DAO。

接口定义 IGenericDao

public interface IGenericDao<T extends Serializable> {
    void setClazz(Class<T> clazzToSet);

    T findOne(final long id);

    List<T> findAll();

    T create(final T entity);

    T update(final T entity);

    void delete(final T entity);

    void deleteById(final long entityId);
}

接口便于解耦和测试,也可以方便地进行 AOP 增强。


2.3 抽象 JPA DAO

JPA 版本几乎和 Hibernate 版本一致,只是底层换成了 EntityManager

public abstract class AbstractJpaDAO<T extends Serializable> {
    private Class<T> clazz;

    @PersistenceContext(unitName = "entityManagerFactory")
    private EntityManager entityManager;

    public final void setClazz(final Class<T> clazzToSet) {
        this.clazz = clazzToSet;
    }

    public T findOne(final long id) {
        return entityManager.find(clazz, id);
    }

    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        return entityManager.createQuery("from " + clazz.getName()).getResultList();
    }

    public T create(final T entity) {
        entityManager.persist(entity);
        return entity;
    }

    public T update(final T entity) {
        return entityManager.merge(entity);
    }

    public void delete(final T entity) {
        entityManager.remove(entity);
    }

    public void deleteById(final long entityId) {
        final T entity = findOne(entityId);
        delete(entity);
    }
}

区别点:

  • 使用 @PersistenceContext 注入 EntityManager
  • find() 方法直接支持主键查询
  • 不再使用已废弃的 JpaTemplate(Spring 旧版封装)

✅ 现代 Spring 项目推荐直接使用原生 JPA API。


2.4 通用 JPA DAO

同样,只需一个泛型实现类:

@Repository
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class GenericJpaDao<T extends Serializable>
  extends AbstractJpaDAO<T> implements IGenericDao<T> {
   //
}

结构完全对称,迁移成本极低。


3. 如何注入并使用这个泛型 DAO

现在我们有了统一的 IGenericDao<T> 接口,接下来是如何在 Service 中使用它。

示例:在 FooService 中操作 Foo 实体

@Service
class FooService implements IFooService {

   private IGenericDao<Foo> dao;

   @Autowired
   public void setDao(IGenericDao<Foo> daoToSet) {
      this.dao = daoToSet;
      dao.setClazz(Foo.class); // 关键一步:指定实体类型
   }

   // 其他业务方法...
}

关键步骤:

  1. Spring 通过 setter 注入一个 IGenericDao<Foo> 实例
  2. 由于是 prototype 作用域,每次注入都是新对象
  3. 手动调用 setClazz(Foo.class) 完成类型绑定
  4. 后续可直接使用 dao.findOne(1L)dao.findAll() 等方法

✅ 为什么不在构造函数里设 clazz?
因为泛型擦除,无法在运行时获取 T.class,所以必须显式传入 Class 对象。

替代方案(不推荐):

  • 反射获取泛型类型(复杂且易出错)
  • XML 配置(过时)

👉 我推荐当前这种显式设置的方式:简单粗暴,清晰可控。


4. 总结

本文展示了如何通过 泛型 + 抽象基类 + prototype 作用域 的组合拳,将传统的多 DAO 模式简化为一个通用 DAO。

最终收益:

  • ✅ 显著减少代码量
  • ✅ 提高可维护性
  • ✅ 保持类型安全
  • ✅ 支持 Hibernate 和 JPA 两种主流持久层框架

📌 适用场景:中小型项目、CRUD 为主的应用、希望快速搭建数据访问层的团队。
⚠️ 不适用场景:需要高度定制化查询或复杂事务控制的大型系统(可在此基础上扩展)。

配套代码已开源,可在 GitHub 获取:

👉 https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-jpa-2

如需了解 Spring 上下文配置和 Maven 依赖搭建,可参考:
使用 Java 配置启动 Spring Web 应用


原始标题:Simplify the DAO with Spring and Java Generics