在典型的 Java Web 分層架構中,留言板系統的服務層 (Service Layer) 負責業務邏輯編排、事務控制與數據訪問協調。本文基於 Hibernate 實現服務層接口,展示從 DAO 到 Service 的完整鏈路,涵蓋實體映射、會話管理、事務邊界與異常處理。
@Entity
@Table(name = "message_board")
public class Message {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String author;
@Column(nullable = false, length = 100)
private String title;
@Lob @Column(nullable = false)
private String content;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "post_time", nullable = false, updatable = false)
private Date postTime = new Date();
@Column(name = "ip_address", length = 45)
private String ipAddress;
// Getters/Setters, equals/hashCode 基於 id
}
@Entity
@Table(name = "message_reply")
public class Reply {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "message_id", nullable = false)
private Message message;
@Column(nullable = false, length = 50)
private String author;
@Lob @Column(nullable = false)
private String content;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "reply_time", nullable = false)
private Date replyTime = new Date();
}
public interface GenericDao {
T save(T entity);
void delete(T entity);
T getById(ID id);
List findAll();
List findByCriteria(DetachedCriteria criteria);
long countByCriteria(DetachedCriteria criteria);
}
@Repository
public class HibernateGenericDao implements GenericDao {
@Autowired private SessionFactory sessionFactory;
private final Class entityClass;
public HibernateGenericDao(Class entityClass) {
this.entityClass = entityClass;
}
protected Session getCurrentSession() {
return sessionFactory.getCurrentSession(); // 綁定到 Spring 事務
}
@Override
public T save(T entity) {
getCurrentSession().saveOrUpdate(entity);
return entity;
}
@Override
public void delete(T entity) {
getCurrentSession().delete(entity);
}
@Override
public T getById(ID id) {
return getCurrentSession().get(entityClass, id);
}
@Override
public List findAll() {
return getCurrentSession().createCriteria(entityClass).list();
}
@Override
public List findByCriteria(DetachedCriteria criteria) {
return criteria.getExecutableCriteria(getCurrentSession()).list();
}
@Override
public long countByCriteria(DetachedCriteria criteria) {
Criteria executable = criteria.getExecutableCriteria(getCurrentSession());
executable.setProjection(Projections.rowCount());
return (Long) executable.uniqueResult();
}
}
public interface MessageDao extends GenericDao {
List findLatest(int maxResults);
List findByAuthor(String author, int offset, int limit);
long countByAuthor(String author);
}
@Repository
public class HibernateMessageDao extends HibernateGenericDao implements MessageDao {
public HibernateMessageDao() { super(Message.class); }
@Override
public List findLatest(int maxResults) {
DetachedCriteria dc = DetachedCriteria.forClass(Message.class)
.addOrder(Order.desc("postTime"));
return findByCriteria(dc.setMaxResults(maxResults));
}
@Override
public List findByAuthor(String author, int offset, int limit) {
DetachedCriteria dc = DetachedCriteria.forClass(Message.class)
.add(Restrictions.eq("author", author))
.addOrder(Order.desc("postTime"));
return findByCriteria(dc.setFirstResult(offset).setMaxResults(limit));
}
@Override
public long countByAuthor(String author) {
DetachedCriteria dc = DetachedCriteria.forClass(Message.class)
.add(Restrictions.eq("author", author));
return countByCriteria(dc);
}
}
public interface MessageService {
Message postMessage(String author, String title, String content, String ip);
Message getMessage(Long id);
List getLatestMessages(int count);
PagedResult getMessagesByAuthor(String author, int page, int pageSize);
void deleteMessage(Long id, String currentUser); // 權限檢查
Reply addReply(Long messageId, String author, String content);
}
@Service
@Transactional(readOnly = true) // 默認只讀,寫操作方法覆蓋
public class MessageServiceImpl implements MessageService {
@Autowired private MessageDao messageDao;
@Autowired private GenericDao replyDao;
@Override
@Transactional(readOnly = false, rollbackFor = Exception.class)
public Message postMessage(String author, String title, String content, String ip) {
// 輸入驗證
if (StringUtils.isAnyBlank(author, title, content)) {
throw new IllegalArgumentException("作者、標題、內容均不能為空");
}
if (author.length() > 50 || title.length() > 100) {
throw new IllegalArgumentException("作者或標題長度超限");
}
Message msg = new Message();
msg.setAuthor(HtmlUtils.htmlEscape(author)); // 防 XSS
msg.setTitle(HtmlUtils.htmlEscape(title));
msg.setContent(HtmlUtils.htmlEscape(content));
msg.setIpAddress(ip);
messageDao.save(msg);
return msg; // 返回持久化後帶 ID 的實體
}
@Override
public Message getMessage(Long id) {
return messageDao.getById(id);
}
@Override
public List getLatestMessages(int count) {
return messageDao.findLatest(Math.min(count, 100)); // 防濫用
}
@Override
public PagedResult getMessagesByAuthor(String author, int page, int pageSize) {
int safePage = Math.max(page, 1);
int safeSize = Math.min(Math.max(pageSize, 1), 50);
int offset = (safePage - 1) * safeSize;
List data = messageDao.findByAuthor(author, offset, safeSize);
long total = messageDao.countByAuthor(author);
return new PagedResult<>(data, total, safePage, safeSize);
}
@Override
@Transactional(readOnly = false)
public void deleteMessage(Long id, String currentUser) {
Message msg = messageDao.getById(id);
if (msg == null) throw new EntityNotFoundException("留言不存在: " + id);
// 權限檢查:作者本人或管理員
if (!msg.getAuthor().equals(currentUser) && !"admin".equals(currentUser)) {
throw new AccessDeniedException("無權刪除他人留言");
}
// 級聯刪除回覆 (或由資料庫外鍵 CASCADE 處理)
messageDao.delete(msg);
}
@Override
@Transactional(readOnly = false)
public Reply addReply(Long messageId, String author, String content) {
Message msg = messageDao.getById(messageId);
if (msg == null) throw new EntityNotFoundException("留言不存在: " + messageId);
Reply reply = new Reply();
reply.setMessage(msg);
reply.setAuthor(HtmlUtils.htmlEscape(author));
reply.setContent(HtmlUtils.htmlEscape(content));
replyDao.save(reply);
return reply;
}
}
| 領域 | 最佳實踐 |
|---|---|
| 會話管理 | Spring OpenSessionInViewFilter 或 @Transactional 綁定 Session 到線程,DAO 用 getCurrentSession() |
| 事務邊界 | 服務層方法標註 @Transactional,讀操作 readOnly=true,寫操作顯式 readOnly=false + rollbackFor |
| 異常轉換 | 捕獲 Hibernate/JDBC 異常轉為業務異常 (DataAccessException 體系),不向上拋漏實現細節 |
| 防 XSS | 輸入端 HtmlUtils.htmlEscape(),輸出端 JSTL <c:out escapeXml="true"> 雙重保障 |
| 分頁查詢 | DAO 層封裝 DetachedCriteria + setFirstResult/setMaxResults,Service 組裝 PagedResult |
| 延遲加載 | 關聯默認 LAZY,Service 層按需初始化 (如 Hibernate.initialize(message.getReplies())) 或用 FETCH JOIN |
<!-- SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan" value="com.example.messageboard.entity"/>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>
<!-- 事務管理器 -->
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
本實現展示了領域驅動設計思想在留言板服務層的落地:實體豐富行為、DAO 抽象持久化細節、Service 封裝業務用例與事務邊界、Spring 聲明式事務消除樣板代碼。核心優勢在於: