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); // 关键一步:指定实体类型
}
// 其他业务方法...
}
关键步骤:
- Spring 通过 setter 注入一个
IGenericDao<Foo>
实例 - 由于是
prototype
作用域,每次注入都是新对象 - 手动调用
setClazz(Foo.class)
完成类型绑定 - 后续可直接使用
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 应用