功能概述
论坛整体功能
论坛普通功能为普通用户所有,论坛板块管理为论坛版块管理员和论坛管理员所有,论坛管理为论坛管理员所有
论坛用例描述
操作权限是递增的
主要功能描述
用户登录
发表主题帖子
游客不可发表主题帖子
回复主题帖
游客不可回复帖子
删除帖子
置精华帖
指定论坛板块管理员
系统设计
技术框架选择
采用Maven构建项目
使用Maven推荐的标准目录结构
resources文件夹用于放置系统配置文件,java文件夹用于放置Java源代码文件,webapp文件夹用于放置web应用的程序文件。
dao对应持久层的程序,service和web分别对应服务层和Web成的程序
由于PO(persistant object,持久对象)会在多个层出现,我们放置在单独的domain包中
为了避免在程序中直接使用字面值常量,需要通过常量定义的方式予以规避,在cons包中定义了应用级的常量
为了统一管理应用系统异常体系,在exception包中定义了业务异常类及系统异常等
为DAO和服务类Bean分别提供了一个Spring配置文件,前者为dao.xml后者为service.xml
jdbc.properties属性文件提供了数据库链接的信息,它将被service.xml使用
log4j.properties属性文件是Log4J的配置问价
将这些配置文件直接放置在类路径下
webapp目录结构很简单,我们将大部分的JSP放置在WEB-INF/jsp目录中,放置用户直接通过URL调用这些文件。
WEB-INF/servlet.xml为Spring MVC的配置文件
单元测试类包结构规划
一般情况下,根据应用程序分层建立相应的单元测试目录结构
系统架构图
PO类设计
有7个PO类
BaseDomain是所有PO的基类,实现了Serializable接口
Board:论坛版块PO类
Topic:论坛主题PO类,包含主题帖子的作者,所属论坛版块,创建时间,浏览数,回复数等信息,其中mainPost对应主题帖子
Post:帖子PO类,一个Topic拥有一个MainPost,有若干个Post(回复帖子)
User:论坛用户PO类
LoginLog:论坛用户登录日志PO类
持久层设计
采用Hibernate技术,创建所有DAO的基类BaseDao<T>,并注入Spring为Hibernate提供的HibernateTemplate模板类。
T为DAO操作的PO类类型,子类在继承BaseDao<T>时仅需指定T的类型,BaseDao<T>中的方法即可确定操作的PO类型
服务层设计
服务层通过封装持久层的DAO完成业务逻辑,Web层通过调用服务层的服务类完成各模块的业务。
服务层提供两个服务类,分别是UserService和ForumService
UserService通过调用持久层的UserDao操作持久化对象,提供了保存,更新,锁定,解锁等对User持久类的操作方法,也提供了根据用户名或用户ID查询单个用户及根据用户名模糊查询多个用户的方法
操作论坛版块,主题,帖子等论坛功能使用的服务方法封装在ForumService中。
Web层设计
定义一个Controller的基类:BaseController,它提供了其他Controller共有的一些方法
RegisterController:用户注册的控制器
LoginController:用户登录,登录注销的控制器
ForumManageController:论坛管理的控制器,包括添加论坛版块,指定论坛版块管理员,对用户进行锁定/解锁
BoardManageController:论坛的基本功能,包括发表主题帖子,回复帖子,删除帖子,置精华帖子等
数据库设计
6张数据表,t_board_manager用于维护t_board和t_user的多对多关系
这些表都可以找到对应的PO类,但t_board_manager没有对应的PO类,它对应User和Board的多对多关系,反映在Hibernate的映射文件中
开发前的准备
①登录mysql,设置默认格式为utf8,否则会出现乱码
mysql -u root -p –default-character-set=utf8
运行source <项目地址>/schema/sampledb.sql(别加分号)脚本创建论坛数据库,该脚本初始化了两个用户:一个是john/1234(普通用户),另一个tom/1234(系统管理员)
可以看到sampledb中有六个数据表
②在pom.xml中增加依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.smart</groupId>
<artifactId>chapter18</artifactId>
<version>1.0</version>
<name>Spring4.x第十八章实例</name>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>${commons-dbcp.version}</version>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>4.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<!-- http://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-core</artifactId>
<version>${unitils.version}</version>
</dependency>
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-testng</artifactId>
<version>${unitils.version}</version>
</dependency>
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-spring</artifactId>
<version>${unitils.version}</version>
</dependency>
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-orm</artifactId>
<version>${unitils.version}</version>
</dependency>
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-database</artifactId>
<version>${unitils.version}</version>
</dependency>
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-dbmaintainer</artifactId>
<version>${unitils.version}</version>
</dependency>
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-dbunit</artifactId>
<version>${unitils.version}</version>
<exclusions>
<exclusion>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- aspectj依赖(spring依赖) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>${aopalliance.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.6.0.GA</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-chrome-driver</artifactId>
<version>${selenium.version}</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
<version>${selenium.version}</version>
</dependency>
<dependency>
<groupId>com.opera</groupId>
<artifactId>operadriver</artifactId>
<version>0.8.1</version>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>${dbunit.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-api</artifactId>
<version>${selenium.version}</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-htmlunit-driver</artifactId>
<version>${selenium.version}</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>${hamcrest.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7.2</version>
<configuration>
<forkMode>once</forkMode>
<threadCount>10</threadCount>
<argLine>-Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
<!-- jetty插件 -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.25</version>
<configuration>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>80</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
<contextPath>/forum</contextPath>
<scanIntervalSeconds>0</scanIntervalSeconds>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<file.encoding>UTF-8</file.encoding>
<spring.version>4.2.2.RELEASE</spring.version>
<slf4j.version>1.7.5</slf4j.version>
<aspectj.version>1.8.1</aspectj.version>
<aopalliance.version>1.0</aopalliance.version>
<mysql.version>5.1.29</mysql.version>
<servlet.version>3.0-alpha-1</servlet.version>
<commons-dbcp.version>1.4</commons-dbcp.version>
<jetty.version>8.1.8.v20121106</jetty.version>
<aspectjweaver.version>1.6.8</aspectjweaver.version>
<hibernate.version>4.2.0.Final</hibernate.version>
<mockito.version>1.10.19</mockito.version>
<unitils.version>3.4.2</unitils.version>
<selenium.version>2.41.0</selenium.version>
<dbunit.version>2.5.1</dbunit.version>
<hamcrest.version>1.3</hamcrest.version>
</properties>
</project>
持久层开发
一般将PO和DAO的类统一归到持久层中,持久层既负责将PO持久化到数据中,也负责从数据库中加载数据到PO对象中
PO类
所有的PO类都直接或间接地继承BaseDomain类
//实现了Serializable接口,以便JVM可以实例化PO实例
public class BaseDomain implements Serializable{
//统一的toString()方法
public String toString(){ //需导入lang包依赖
return ToStringBuilder.reflectionToString(this);
}
}
一般,PO类最好都实现Serializable接口,这样JVM就能够方便地将PO实例序列化到硬盘中,或者通过流的方式进行发送,为缓存,集群等功能带来便利。
我们往往需要将PO对象打印为一个字符串,这是由对象的toString()方法来完成的,这里通过Apache的ToStringBuilder工具类提供统一的实现
下面是Board PO类及Hibernate JPA注解配置
@Entity //每个持久化PO类都是一个实体Bean,通过在类的定义中使用@Entity注解来进行声明
@Cache(usage= CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)//通过@Cache注解为Board设置缓存策略
@Table(name="t_board") //通过@Table注解为Board指定对应数据库表,目录和schema的名字
public class Board extends BaseDomain {
@Id //通过@Id注解可见Board中的boardId属性定义为主键
@GeneratedValue(strategy = GenerationType.IDENTITY) //使用@GenerateValue注解定义主键生成策略
@Column(name = "board_id") //通过@Colum注解将Board的各个属性映射到数据表库t_board中相应的列
private int boardId;
@Column(name="board_name")
private String boardName;
@Column(name="board_desc")
private String boardDesc; //论坛简介
@Column(name="topic_num")
private int topicNum;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "manBoards", fetch = FetchType.LAZY)
private Set<User> users = new HashSet<User>();
//省略属性的get/setter方法
}
每个持久化PO类都是一个实体Bean,通过在类的定义中使用@Entity注解来进行声明
通过@Table注解为Board指定对应数据库表,目录和schema的名字,其中t_board如下
通过@Cache注解为Board设置缓存策略
Hibernate提供了以下几种缓存策略
通过@Id注解可将Board中的boardId属性定义为主键
使用@GenerateValue注解定义主键生成策略(分别是AUTO,TABLE,IDENTITY,SEQUENCE)
通过@Colum注解将Board的各个属性映射到数据表库t_board中相应的列
下面看Post PO类及Hibernate JPA注解配置
@Entity
@Cache(usage= CacheConcurrencyStrategy.READ_WRITE)
@Table(name="t_post")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) //通过@Inheritance注解指定了PO映射继承关系
@DiscriminatorColumn(name="post_type",discriminatorType = DiscriminatorType.STRING)
@DiscriminatorValue("1")
public class Post extends BaseDomain{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="post_id")
private int postId;
@Column(name = "post_title")
private String postTitle;
@Column(name = "post_text")
private String postText;
@Column(name = "board_id")
private int boardId;
@Column(name = "create_time")
private Date createTime;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(cascade = {CascadeType.PERSIST,CascadeType.MERGE})
@JoinColumn(name="topic_id")
private Topic topic;
//省略get/setter方法
}
Post(回复的帖子)和其子类MainPost(主题帖)都映射到t_post表中,t_post表通过post_type字符值分别两者。
当post_type=1,对应MainPost,=2时对应Post
通过@Inheritance注解指定了PO映射继承关系。Hibernate提供了三种方式:
①每个类一张表(InheritanceType.TABLE_PER_CLASS)
②连接的子类(InheritanceType.JOINED)
③每个类层次结构一张表(InheritanceType.SINGLE_TABLE)
通过@DiscriminatorColumn注解定义了辨别符列
对于继承层次结构中的每个类,@DiscriminatorValue注解指定了用来辨别该类的值
辨别符列名字默认为DTYPE,其默认值为实体名,类型为DiscriminatorType.STRING
通过 @ManyToOne注解定义了多对一关系
通过 @JoinColumn注解定义了多对一的关联关系,如果没有 @JoinColumn注解,系统自动处理,在主表中创建连接列,列出名为“主题的关联属性名+下划线+被关联端的主键列名”
其他的PO类类似,通过下载源代码可直接使用
在resources文件夹下创建com.smart.domain文件夹,在hbm文件夹中创建PO类对应的hbm映射文件
例如Board.hbm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping >
<class name="com.smart.domain.Board" table="t_board">
<id name="boardId" column="board_id">
<generator class="identity" />
</id>
<property name="boardName" column="board_name" />
<property name="boardDesc" column="board_desc" />
<property name="topicNum" column="topic_num"/>
</class>
</hibernate-mapping>
DAO基类
DAO基类的基本方法
由于每个PO的DAO类都需要执行一些相同的操作,如保存,更新,删除PO和根据ID加载PO等,所以可以编写一个提供这些通用操作的基类,让所有的PO的DAO类都继承这个DAO基类
//DAO基类,其它DAO可以直接继承这个DAO,不但可以复用共用的方法,还可以获得泛型的好处。
public class BaseDao<T>{
private Class<T> entityClass;
//@Autowired
private HibernateTemplate hibernateTemplate;
//通过反射获取子类确定的泛型类
public BaseDao() {
Type genType = getClass().getGenericSuperclass();
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
entityClass = (Class) params[0];
}
//根据ID加载PO实例
public T load(Serializable id) {
return (T) getHibernateTemplate().load(entityClass, id);
}
// 根据ID获取PO实例
public T get(Serializable id) {
return (T) getHibernateTemplate().get(entityClass, id);
}
// 获取PO的所有对象
public List<T> loadAll() {
return getHibernateTemplate().loadAll(entityClass);
}
//保存PO
public void save(T entity) {
getHibernateTemplate().save(entity);
}
//删除PO
public void remove(T entity) {
getHibernateTemplate().delete(entity);
}
// 删除tableNames数据
public void removeAll(String tableName) {
getSession().createSQLQuery("truncate TABLE " + tableName +"").executeUpdate();
}
/**
* 更改PO
*
* @param entity
*/
public void update(T entity) {
getHibernateTemplate().update(entity);
}
//执行HQL查询
public List find(String hql) {
return this.getHibernateTemplate().find(hql);
}
//执行带参的HQL查询,Object ...是在不确定方法参数的情况下的一种多态表现形式。即这个方法可以传递多个参数,这个参数的个数是不确定的
public List find(String hql, Object... params) {
return this.getHibernateTemplate().find(hql,params);
}
//对延迟加载的实体PO执行初始化
public void initialize(Object entity) {
this.getHibernateTemplate().initialize(entity);
}
/**
* 分页查询函数,使用hql.
*
* @param pageNo 页号,从1开始.
*/
public Page pagedQuery(String hql, int pageNo, int pageSize, Object... values) {
Assert.hasText(hql);
Assert.isTrue(pageNo >= 1, "pageNo should start from 1");
// Count查询
String countQueryString = " select count (*) " + removeSelect(removeOrders(hql));
List countlist = getHibernateTemplate().find(countQueryString, values);
long totalCount = (Long) countlist.get(0);
if (totalCount < 1)
return new Page();
// 实际查询返回分页对象
int startIndex = Page.getStartOfPage(pageNo, pageSize);
Query query = createQuery(hql, values);
List list = query.setFirstResult(startIndex).setMaxResults(pageSize).list();
return new Page(startIndex, totalCount, pageSize, list);
}
/**
* 创建Query对象. 对于需要first,max,fetchsize,cache,cacheRegion等诸多设置的函数,可以在返回Query后自行设置.
* 留意可以连续设置,如下:
* <pre>
* dao.getQuery(hql).setMaxResult(100).setCacheable(true).list();
* </pre>
* 调用方式如下:
* <pre>
* dao.createQuery(hql)
* dao.createQuery(hql,arg0);
* dao.createQuery(hql,arg0,arg1);
* dao.createQuery(hql,new Object[arg0,arg1,arg2])
* </pre>
*
* @param values 可变参数.
*/
public Query createQuery(String hql, Object... values) {
Assert.hasText(hql);
Query query = getSession().createQuery(hql);
for (int i = 0; i < values.length; i++) {
query.setParameter(i, values[i]);
}
return query;
}
/**
* 去除hql的select 子句,未考虑union的情况,用于pagedQuery.
*
* @see #pagedQuery(String,int,int,Object[])
*/
private static String removeSelect(String hql) {
Assert.hasText(hql);
int beginPos = hql.toLowerCase().indexOf("from");
Assert.isTrue(beginPos != -1, " hql : " + hql + " must has a keyword 'from'");
return hql.substring(beginPos);
}
/**
* 去除hql的orderby 子句,用于pagedQuery.
*
* @see #pagedQuery(String,int,int,Object[])
*/
private static String removeOrders(String hql) {
Assert.hasText(hql);
Pattern p = Pattern.compile("order\\s*by[\\w|\\W|\\s|\\S]*", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(hql);
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, "");
}
m.appendTail(sb);
return sb.toString();
}
public HibernateTemplate getHibernateTemplate() {
return hibernateTemplate;
}
@Autowired
public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
this.hibernateTemplate = hibernateTemplate;
}
public Session getSession() {
return hibernateTemplate.getSessionFactory().getCurrentSession();
}
}
基类直接注入Spring为Hibernate提供的HibernateTemplate模板操作类,可以借由它执行Hibernate的各项操作
对数据分页的支持
可以看到BaseDao中提供了对数据分页的支持,仅需提供HQL及分页的一些配置信息,就可以获取特定页面的数据。特定页面的信息通过Page类进行表达
package com.smart.dao;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 分页对象. 包含当前页数据及分页信息如总记录数.
*
*/
public class Page implements Serializable {
private static int DEFAULT_PAGE_SIZE = 20;
private int pageSize = DEFAULT_PAGE_SIZE; // 每页的记录数
private long start; // 当前页第一条数据在List中的位置,从0开始
private List data; // 当前页中存放的记录,类型一般为List
private long totalCount; // 总记录数
/**
* 构造方法,只构造空页.
*/
public Page() {
this(0, 0, DEFAULT_PAGE_SIZE, new ArrayList());
}
/**
* 默认构造方法.
*
* @param start 本页数据在数据库中的起始位置
* @param totalSize 数据库中总记录条数
* @param pageSize 本页容量
* @param data 本页包含的数据
*/
public Page(long start, long totalSize, int pageSize, List data) {
this.pageSize = pageSize;
this.start = start;
this.totalCount = totalSize;
this.data = data;
}
/**
* 取总记录数.
*/
public long getTotalCount() {
return this.totalCount;
}
/**
* 取总页数.
*/
public long getTotalPageCount() {
if (totalCount % pageSize == 0)
return totalCount / pageSize;
else
return totalCount / pageSize + 1;
}
/**
* 取每页数据容量.
*/
public int getPageSize() {
return pageSize;
}
/**
* 取当前页中的记录.
*/
public List getResult() {
return data;
}
/**
* 取该页当前页码,页码从1开始.
*/
public long getCurrentPageNo() {
return start / pageSize + 1;
}
/**
* 该页是否有下一页.
*/
public boolean isHasNextPage() {
return this.getCurrentPageNo() < this.getTotalPageCount();
}
/**
* 该页是否有上一页.
*/
public boolean isHasPreviousPage() {
return this.getCurrentPageNo() > 1;
}
/**
* 获取任一页第一条数据在数据集的位置,每页条数使用默认值.
*
* @see #getStartOfPage(int,int)
*/
protected static int getStartOfPage(int pageNo) {
return getStartOfPage(pageNo, DEFAULT_PAGE_SIZE);
}
/**
* 获取任一页第一条数据在数据集的位置.
*
* @param pageNo 从1开始的页号
* @param pageSize 每页记录条数
* @return 该页第一条数据
*/
public static int getStartOfPage(int pageNo, int pageSize) {
return (pageNo - 1) * pageSize;
}
}
通过扩展基类定义DAO类
BoardDao
//扩展BaseDao,并确定泛型的类为Board
@Repository //指定为DAO类
public class BoardDao extends BaseDao<Board> {
private static final String GET_BOARD_NUM = "select count(f.boardId) from Board f";
//获取论坛版块数目的方法
public long getBoardNum() {
Iterator iter = getHibernateTemplate().iterate(GET_BOARD_NUM);
return ((Long)iter.next());
}
}
BoardDao是操作Board的DAO类,扩展于BoardDao<T>,同时指定泛型类型T为Board,这样在基类中定义的save(T obj)等通用方法的入参就确定了类型为Board。
TopicDao
@Repository
public class TopicDao extends BaseDao<Topic>{
private static final String GET_BOARD_DIGEST_TOPICS = "from Topic t where t.boardId = ? and digest > 0 order by t.lastPost desc,digest desc";
private static final String GET_PAGED_TOPICS = "from Topic where boardId = ? order by lastPost desc";
private static final String QUERY_TOPIC_BY_TITILE = "from Topic where topicTitle like ? order by lastPost desc";
/**
* 获取论坛版块某一页的精华主题帖,按最后回复时间以及精华级别 降序排列
* @param boardId 论坛版块ID
* @return 该论坛下的所有精华主题帖
*/
public Page getBoardDigestTopics(int boardId,int pageNo,int pageSize){
return pagedQuery(GET_BOARD_DIGEST_TOPICS,pageNo,pageSize,boardId);
}
/**
* 获取论坛版块分页的主题帖子
* @param boardId 论坛版块ID
* @param pageNo 页号,从1开始。
* @param pageSize 每页的记录数
* @return 包含分页信息的Page对象
*/
public Page getPagedTopics(int boardId,int pageNo,int pageSize) {
return pagedQuery(GET_PAGED_TOPICS,pageNo,pageSize,boardId);
}
/**
* 根据主题帖标题查询所有模糊匹配的主题帖
* @param title 标题的查询条件
* @param pageNo 页号,从1开始。
* @param pageSize 每页的记录数
* @return 包含分页信息的Page对象
*/
public Page queryTopicByTitle(String title, int pageNo, int pageSize) {
return pagedQuery(QUERY_TOPIC_BY_TITILE,pageNo,pageSize,title);
}
}
UserDao
@Repository
public class UserDao extends BaseDao<User> {
private static final String GET_USER_BY_USERNAME = "from User u where u.userName = ?";
private static final String QUERY_USER_BY_USERNAME = "from User u where u.userName like ?";
/**
* 根据用户名查询User对象
* @param userName 用户名
* @return 对应userName的User对象,如果不存在,返回null。
*/
public User getUserByUserName(String userName){
List<User> users = (List<User>)getHibernateTemplate().find(GET_USER_BY_USERNAME,userName);
if (users.size() == 0) {
return null;
}else{
return users.get(0);
}
}
/**
* 根据用户名为模糊查询条件,查询出所有前缀匹配的User对象
* @param userName 用户名查询条件
* @return 用户名前缀匹配的所有User对象
*/
public List<User> queryUserByUserName(String userName){
return (List<User>)getHibernateTemplate().find(QUERY_USER_BY_USERNAME,userName+"%");
}
}
LoginLogDao
@Repository
public class LoginLogDao extends BaseDao<LoginLog> {
public void save(LoginLog loginLog) {
this.getHibernateTemplate().save(loginLog);
}
}
PostDao
@Repository
public class PostDao extends BaseDao<Post> {
private static final String GET_PAGED_POSTS = "from Post where topic.topicId =? order by createTime desc";
private static final String DELETE_TOPIC_POSTS = "delete from Post where topic.topicId=?";
public Page getPagedPosts(int topicId, int pageNo, int pageSize) {
return pagedQuery(GET_PAGED_POSTS,pageNo,pageSize,topicId);
}
/**
* 删除主题下的所有帖子
* @param topicId 主题ID
*/
public void deleteTopicPosts(int topicId) {
getHibernateTemplate().bulkUpdate(DELETE_TOPIC_POSTS,topicId);
}
}
DAO Bean的装配
在完成DAO的开发后,需要在Spring配置文件中将它们定义为Bean。在resources目录下创建一个用于配置DAO的Spring配置文件dao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 扫描com.smart.dao包下所有标注@Repository的DAO组件 -->
<context:component-scan base-package="com.smart.dao"/>
<!-- ①引入定义JDBC连接的属性文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- ②定义一个数据源-->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="${jdbc.driverClassName}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}" />
<!-- ③定义Hibernate的Session工厂-->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="packagesToScan"> <!--扫描基于JPA注解的PO类目录 -->
<list>
<value>com.smart.domain</value>
</list>
</property>
<!-- 指定Hibernate的属性信息-->
<property name="hibernateProperties">
<props>
<!-- 指定数据库的类型为MySQL-->
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQLDialect
</prop>
<!-- 在提供数据库操作里显示SQL,方便开发期的调试,在部署时应将其设计为false-->
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.cache.use_second_level_cache">true</prop>
<!-- 采用EHCache缓存实现方案-->
<prop key="hibernate.cache.region.factory_class">
org.hibernate.cache.ehcache.EhCacheRegionFactory
</prop>
<prop key="hibernate.cache.use_query_cache">false</prop>
</props>
</property>
</bean>
<!-- ④定义HibernateTemplate-->
<bean id="hibernateTemplate"
class="org.springframework.orm.hibernate4.HibernateTemplate"
p:sessionFactory-ref="sessionFactory" />
<bean id="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" read-only="true"/>
<tx:method name="create*" propagation="REQUIRED" read-only="false" />
<tx:method name="save*" propagation="REQUIRED" read-only="false" />
<tx:method name="reg*" propagation="REQUIRED" read-only="false" />
<tx:method name="update*" propagation="REQUIRED" read-only="false" />
<tx:method name="delete*" propagation="REQUIRED" read-only="false" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor id="managerTx" advice-ref="txAdvice" pointcut="execution(* com.smart.*.*(..)))" order="1"/>
</aop:config>
</beans>
①处引入了一个外部的属性文件,定义了JDBC联结的相关信息,jdbc.properties如下
#Mysql - linkx
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/sampledb?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456
②定义了一个数据源,jdbc.url属性有两个属性:useUnicode=true&characterEncoding=UTF-8,告诉JDBC在和MySQL数据库通信时需要使用特定的编码
在③定义了一个Hibernate Session工厂,需要使用数据源,还必须为其指定Hibernate注解包扫描路径
在④处定义了一个HibernateTemplate的实现,HibernateTemplate是Spring提供的旨在简化Hibernate API调用的模板类
使用Hibernate二级缓存
Hibernate拥有一级和二级缓存。一级缓存是由Session实现的。Hibernate使用插件的方式实现二级缓存,默认情况下,二级缓存是关闭的
配置二级缓存主要有两个步骤
①选择第三方二级缓存组件(如EHCache,MemCached等),在基于JPA注解的实体对象或SessionFactory的配置中定义缓存策略
②配置所选第三方缓存组件的配置文件。每种缓存组件都有自己的配置文件,需要手工编辑它们的配置文件,并将它们放置在类路径下。EHCache的配置文件为ehcache.xml,而JBossCache的配置文件为treecache.xml
在dao.xml中可以看到我们使用EHCache缓存实现方案,通过hibernate.cache.region.factory_class指定了缓存实现类
还需要配置EHCache的配置文件,将其命名为ehcache.xml并放置在类路径下(resources中)
<ehcache>
<diskStore path="java.io.tmpdir" />
<defaultCache maxElementsInMemory="10000" eternal="false"
overflowToDisk="false" timeToIdleSeconds="0" timeToLiveSeconds="0"
diskPersistent="false" diskExpiryThreadIntervalSeconds="120" />
<!-- ①存放Board的缓冲区-->
<cache name="fixedRegion" maxElementsInMemory="100" eternal="true" overflowToDisk="false"/>
<!-- ②存放User,Topic和Post的缓存区-->
<cache name="freqChangeRegion" maxElementsInMemory="5000" eternal="false"
overflowToDisk="true" timeToIdleSeconds="300" timeToLiveSeconds="1800"/>
</ehcache>
在①处定义的fixedRegion缓存区不使用硬盘缓存,所有对象都在内存中,缓存区中的对象永不过期,适合缓存类似Board的实例。
在②定义 freqChangeRegion缓存区使用硬盘缓存,对象在闲置300秒后就从缓存中清除,且对象的最大存活期限为30分钟(1800s),缓存区中最大的缓存实例个数为5000个,超出此限制的实例将被写到硬盘中。
当启动Spring时,二级缓存就开始工作了
对持久层进行测试
配置Unitils测试环境
在resources中创建一个项目而别的unitils.properties配置文件
# ①启用Unitils所需模块
unitils.modules=database,dbunit,hibernate,spring
unitils.module.dbunit.className=org.dbunit.MySqlDbUnitModule
#②配置数据库连接
database.driverClassName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/sampledb?useUnicode=true&characterEncoding=UTF-8
database.dialect = mysql
database.userName=root
database.password=123456
database.schemaNames=sampledb
#③ 配置数据库维护策略
updateDataBaseSchema.enabled=false
#④配置数据库创建策略
dbMaintainer.autoCreateExecutedScriptsTable=false
#dbMaintainer.script.locations=D:/book/svn2/code/spring4x-project/spring4x-chapter20/src/test/resources/dbscripts
dbMaintainer.script.locations=src/test/resources/dbscripts
#⑤ 配置数据集工厂
DbUnitModule.DataSet.factory.default=com.smart.test.dataset.excel.MultiSchemaXlsDataSetFactory
DbUnitModule.ExpectedDataSet.factory.default=com.smart.test.dataset.excel.MultiSchemaXlsDataSetFactory
#CleanInsertLoadStrategy:先删除dateSet中有关表的数据,然后再插入数据
#InsertLoadStrategy:只插入数据
#RefreshLoadStrategy:有同样key的数据更新,没有的插入
#UpdateLoadStrategy:有同样key的数据更新,没有的不做任何操作
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.impl.CleanInsertLoadStrategy
#commit 是单元测试方法过后提交事务
#rollback 是回滚事务
#disabled 是没有事务,默认情况下,事务管理是disabled
DatabaseModule.Transactional.value.default=commit
# XSD generator
dataSetStructureGenerator.xsd.dirName=resources/xsd
#dbMaintainer.generateDataSetStructure.enabled=true
在①处加载的模块有database,dbunit,hibernate,spring
准备测试数据库及测试数据
在src/test/resources/dbscripts中创建一个数据库创建脚本文件001_create_sampledb,里面分别是创建论坛版块表t_board,帖子表t_post,话题表t_topic等创建数据库脚本信息
CREATE TABLE `t_board` (
`board_id` int(11) NOT NULL auto_increment COMMENT '论坛版块ID',
`board_name` varchar(150) NOT NULL default '' COMMENT '论坛版块名',
`board_desc` varchar(255) default NULL COMMENT '论坛版块描述',
`topic_num` int(11) NOT NULL default '0' COMMENT '帖子数目',
PRIMARY KEY (`board_id`),
KEY `AK_Board_NAME` (`board_name`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
CREATE TABLE `t_board_manager` (
`board_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`board_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='论坛管理员';
CREATE TABLE `t_login_log` (
`login_log_id` int(11) NOT NULL auto_increment,
`user_id` int(11) default NULL,
`ip` varchar(30) NOT NULL default '',
`login_datetime` varchar(14) NOT NULL,
PRIMARY KEY (`login_log_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_post` (
`post_id` int(11) NOT NULL auto_increment COMMENT '帖子ID',
`board_id` int(11) NOT NULL default '0' COMMENT '论坛ID',
`topic_id` int(11) NOT NULL default '0' COMMENT '话题ID',
`user_id` int(11) NOT NULL default '0' COMMENT '发表者ID',
`post_type` tinyint(4) NOT NULL default '2' COMMENT '帖子类型 1:主帖子 2:回复帖子',
`post_title` varchar(50) NOT NULL COMMENT '帖子标题',
`post_text` text NOT NULL COMMENT '帖子内容',
`create_time` date NOT NULL COMMENT '创建时间',
PRIMARY KEY (`post_id`),
KEY `IDX_POST_TOPIC_ID` (`topic_id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8 COMMENT='帖子';
CREATE TABLE `t_topic` (
`topic_id` int(11) NOT NULL auto_increment COMMENT '帖子ID',
`board_id` int(11) NOT NULL COMMENT '所属论坛',
`topic_title` varchar(100) NOT NULL default '' COMMENT '帖子标题',
`user_id` int(11) NOT NULL default '0' COMMENT '发表用户',
`create_time` date NOT NULL COMMENT '发表时间',
`last_post` date NOT NULL COMMENT '最后回复时间',
`topic_views` int(11) NOT NULL default '1' COMMENT '浏览数',
`topic_replies` int(11) NOT NULL default '0' COMMENT '回复数',
`digest` int(11) NOT NULL COMMENT '0:不是精华话题 1:是精华话题',
PRIMARY KEY (`topic_id`),
KEY `IDX_TOPIC_USER_ID` (`user_id`),
KEY `IDX_TOPIC_TITLE` (`topic_title`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COMMENT='话题';
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL auto_increment COMMENT '用户Id',
`user_name` varchar(30) NOT NULL COMMENT '用户名',
`password` varchar(30) NOT NULL default '' COMMENT '密码',
`user_type` tinyint(4) NOT NULL default '1' COMMENT '1:普通用户 2:管理员',
`locked` tinyint(4) NOT NULL default '0' COMMENT '0:未锁定 1:锁定',
`credit` int(11) default NULL COMMENT '积分',
`last_visit` datetime default NULL COMMENT '最后登陆时间',
`last_ip` varchar(20) default NULL COMMENT '最后登陆IP',
PRIMARY KEY (`user_id`),
KEY `AK_AK_USER_USER_NAME` (`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
接下来使用Excel准备测试数据及验证数据,分别对BoardDao,TopicDao,PostDao,UserDao4个DAO进行测试,需要为每个DAO创建相应的测试数据及验证数据,放在DAO相应的类路径中(test/resources/com/smart/dao)
编写DAO测试基类
编写一个DAO测试基类,所有的DAO测试用例都需要扩展这个基类
BaseDaoTest
@SpringApplicationContext({"dao.xml"})
public class BaseDaoTest extends UnitilsTestNG{
}
只需用到DAO层的配置信息,因此,只需通过@SpringApplicationContext({“dao.xml”})注解加载dao.xml文件即可
由于DAO测试用例基于Unitils,TestNG测试框架,因此需要扩展Unitils提供的UnitilsTestNG测试基类
编写BoardDao测试用例
BoardDaoTest
@SpringApplicationContext({"dao.xml"})
public class BoardDaoTest extends UnitilsTestNG {
//①注入论坛版块DAO
@SpringBean("boardDao")
private BoardDao boardDao;
//创建一个新的论坛版块,并更新
@Test
@DataSet(value = "XiaoChun.SaveBoards.xls")//准备数据
@ExpectedDataSet("XiaoChun.ExpectedBoards.xls")
public void addBoard() throws Exception {
//通过XlsDataSetBeanFactory数据集绑定工厂创建测试实体
List<Board> boards = XlsDataSetBeanFactory.createBeans(BoardDaoTest.class, "XiaoChun.SaveBoards.xls", "t_board", Board.class);
//持久化Board实例
for (Board board : boards) {
boardDao.update(board);
}
}
//删除一个版块
@Test
@DataSet(value = "XiaoChun.Boards.xls")//准备数据
@ExpectedDataSet(value = "XiaoChun.ExpectedBoards.xls")
public void removeBoard() {
//加载指定过得版块
Board board = boardDao.get(7);
//删除指定的版块
boardDao.remove(board);
}
//测试加载版块
@Test
@DataSet("XiaoChun.Boards.xls")//准备数据
public void getBoard() {
//加载版块
Board board = boardDao.load(1);
//验证结果
assertNotNull(board);
assertEquals(board.getBoardName(), "SpringMVC");
}
}
在①通过Unitils提供的@SpringBean注解,从Spring容器中加载BoardDao实例
服务层开发
UserService
package com.smart.service;
import com.smart.dao.LoginLogDao;
import com.smart.dao.UserDao;
import com.smart.domain.LoginLog;
import com.smart.domain.User;
import com.smart.exception.UserExistException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
//用户管理服务器,负责执行查询用户,注册用户,锁定用户等操作
@Service //标记为一个Service类
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private LoginLogDao loginLogDao;
//注册一个新用户,如果用户名已存在,则抛出UserExistException异常
public void register(User user) throws UserExistException {
User u=this.getUserByUserName(user.getUserName());
if(u!=null){
throw new UserExistException("用户名已存在");
}else{
user.setCredit(100);
user.setUserType(1);
userDao.save(user);
}
}
//根据用户名/密码查询User对象
public User getUserByUserName(String userName){
return userDao.getUserByUserName(userName);
}
//根据userId加载User对象
public User getUserById(int userId){
return userDao.get(userId);
}
//将用户锁定,锁定的用户不能登录
public void lockUser(String userName){
User user=userDao.getUserByUserName(userName);
user.setLocked(User.USER_LOCK);
userDao.update(user);
}
//解除用户的锁定
public void unlockUser(String userName){
User user=userDao.getUserByUserName(userName);
user.setLocked(User.USER_UNLOCK);
userDao.update(user);
}
// 根据用户名为条件,执行模糊查询操作
public List<User> queryUserByUserName(String userName){
return userDao.queryUserByUserName(userName);
}
//获取所有用户
public List<User> getAllUsers(){
return userDao.loadAll();
}
// 登陆成功
public void loginSuccess(User user) {
user.setCredit( 5 + user.getCredit());
LoginLog loginLog = new LoginLog();
loginLog.setUser(user);
loginLog.setIp(user.getLastIp());
loginLog.setLoginDate(new Date());
userDao.update(user);
loginLogDao.save(loginLog);
}
}
通过@Autowired注解,自动从Spring容器中加载UserDao和LoginLog两个实例。
UserService事务管理通过Spring声明式事务管理的功能实现,铜鼓事务的声明性信息,Spring负责将事务管理增强逻辑动态织入业务方法响应的连接点中
ForumService的开发
package com.smart.service;
import com.smart.dao.*;
import com.smart.domain.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class ForumService {
private TopicDao topicDao;
private UserDao userDao;
private BoardDao boardDao;
private PostDao postDao;
@Autowired
public void setTopicDao(TopicDao topicDao) {
this.topicDao = topicDao;
}
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Autowired
public void setBoardDao(BoardDao boardDao) {
this.boardDao = boardDao;
}
@Autowired
public void setPostDao(PostDao postDao) {
this.postDao = postDao;
}
//①发表一个主题帖子,用户积分加10,论坛版块的主题帖子数加1
public void addTopic(Topic topic){
Board board=(Board)boardDao.get(topic.getBoardId());
board.setTopicNum(board.getTopicNum()+1);
topicDao.save(topic);
//创建主题帖子
topic.getMainPost().setTopic(topic);
MainPost post=topic.getMainPost();
post.setCreateTime(new Date());
post.setUser(topic.getUser());
post.setPostTitle(topic.getTopicTitle());
post.setBoardId(topic.getBoardId());
//持久化主题帖子
postDao.save(topic.getMainPost());
//更新用户积分
User user=topic.getUser();
user.setCredit(user.getCredit()+10);
userDao.update(user);
}
//② 删除一个主题帖子,用户积分减50,论坛版块的主题帖子减1
//删除主题帖子所有关联的帖子
public void removeTopic(int topicId){
Topic topic=topicDao.get(topicId);
//将论坛版块的主题帖子数减1
Board board=boardDao.get(topic.getBoardId());
board.setTopicNum(board.getTopicNum()-1);
//发布该主题帖子的用户扣除50个积分
User user=topic.getUser();
user.setCredit(user.getCredit()-50);
//删除主题帖子及其关联的帖子
topicDao.remove(topic);
postDao.deleteTopicPosts(topicId);
}
//③ 添加一个回复帖子,用户积分加5,主题帖子回复数加1,并更新最后回复时间
public void addPost(Post post){
postDao.save(post);
User user=post.getUser();
user.setCredit(user.getCredit()+5);
userDao.update(user);
Topic topic=topicDao.get(post.getTopic().getTopicId());
topic.setReplies(topic.getReplies()+1);
topic.setLastPost(new Date());
//topic处于Hibernate受管状态,无需显示更新
//topicDao.update(topic);
}
//删除一个回复的帖子,发表回复帖子的用户积分减20,主题帖的回复数减1
public void removePost(int postId){
Post post = postDao.get(postId);
postDao.remove(post);
Topic topic = topicDao.get(post.getTopic().getTopicId());
topic.setReplies(topic.getReplies() - 1);
User user =post.getUser();
user.setCredit(user.getCredit() - 20);
//topic处于Hibernate受管状态,无须显示更新
//topicDao.update(topic);
//userDao.update(user);
}
//创建一个新的论坛版块
public void addBoard(Board board) {
boardDao.save(board);
}
//删除一个版块
public void removeBoard(int boardId){
Board board = boardDao.get(boardId);
boardDao.remove(board);
}
//将帖子置为精华主题帖
public void makeDigestTopic(int topicId){
Topic topic = topicDao.get(topicId);
topic.setDigest(Topic.DIGEST_TOPIC);
User user = topic.getUser();
user.setCredit(user.getCredit() + 100);
//topic 处于Hibernate受管状态,无须显示更新
//topicDao.update(topic);
//userDao.update(user);
}
//获取所有的论坛版块
public List<Board> getAllBoards(){
return boardDao.loadAll();
}
//获取论坛版块某一页主题帖,以最后回复时间降序排列
public Page getPagedTopics(int boardId, int pageNo, int pageSize){
return topicDao.getPagedTopics(boardId,pageNo,pageSize);
}
//获取同主题每一页帖子,以最后回复时间降序排列
public Page getPagedPosts(int topicId,int pageNo,int pageSize){
return postDao.getPagedPosts(topicId,pageNo,pageSize);
}
/**
* 查找出所有包括标题包含title的主题帖
* @param title 标题查询条件
* @return 标题包含title的主题帖
*/
public Page queryTopicByTitle(String title,int pageNo,int pageSize) {
return topicDao.queryTopicByTitle(title,pageNo,pageSize);
}
/**
* 根据boardId获取Board对象
* @param boardId
*/
public Board getBoardById(int boardId) {
return boardDao.get(boardId);
}
/**
* 根据topicId获取Topic对象
* @param topicId
* @return Topic
*/
public Topic getTopicByTopicId(int topicId) {
return topicDao.get(topicId);
}
/**
* 获取回复帖子的对象
* @param postId
* @return 回复帖子的对象
*/
public Post getPostByPostId(int postId){
return postDao.get(postId);
}
/**
* 将用户设为论坛版块的管理员
* @param boardId 论坛版块ID
* @param userName 设为论坛管理的用户名
*/
public void addBoardManager(int boardId,String userName){
User user = userDao.getUserByUserName(userName);
if(user == null){
throw new RuntimeException("用户名为"+userName+"的用户不存在。");
}else{
Board board = boardDao.get(boardId);
user.getManBoards().add(board);
userDao.update(user);
}
}
//更改主题帖
public void updateTopic(Topic topic){
topicDao.update(topic);
}
//更改回复帖子的内容
public void updatePost(Post post){
postDao.update(post);
}
}
③中,添加了一个回复帖子,同时更新主题帖子的回复帖子数及主题的最后回复时间,并没有调用TopicDao的update()更新Topic,因为我们通过topicDao.get(post.getTopic().getTopicId())方法从数据表中加载Topic实例,所以这个Topic实例处于受管状态,在方法中调整其replies和lastPost属性,Hibernate会将Topic状态更改自动同步到数据表中,无须显式调用topicDao.update()方法
服务类Bean的装配
编写完UserService和ForumService后,需要在Spring配置文件中进行配置,以便注入DAO Bean并实施事务管理增强。在src/main/resources下创建一个service.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- 扫描com.smart.service包下所有标注@Service的服务组件 -->
<context:component-scan base-package="com.smart.service"/>
<!-- ②事务管理器-->
<bean id="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory" />
<bean id="requestMappingHandlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
</bean>
<!--③使用强大的切点表达式语言轻松定义目标方法-->
<aop:config>
<!--通过aop定义事务增强切面-->
<aop:pointcut id="serviceMethod"
expression="execution(* com.smart.service.*Service.*(..))" />
<!--引用事务增强-->
<aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
</aop:config>
<!--事务增强-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--事务属性定义-->
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 基于EHCache的系统缓存管理器-->
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
p:configLocation="classpath:ehcache.xml"/>
</beans>
对论坛服务层配置事务管理,需要在配置文件中引入tx命名空间的声明。
采用aop/tx定义事务方法时,对IoC容器中的Bean进行事务管理配置定义,再由Spring将这些配置织入对应的Bean中
在aop命名空间中,通过切点表达式语言,将service包下所有以Service为后缀的类纳入了需要进行事务增强的范围,并配合<tx:advice>的<aop:advisor>完成了事务切面的定义,如③
事务增强必须有一个事务管理器的支持,<tx:advice>通过transaction-manager属性引用了在②定义的事务管理器(默认查找名为transactionManager的事务管理器,如果为此名则可以不指定transaction-manager属性)
对服务层进行测试
编写Service测试基类
BaseServiceTest
@SpringApplicationContext( {"service.xml", "dao.xml"})
public class BaseServiceTest extends UnitilsTestNG {
@SpringBean(value = "hibernateTemplate")
public HibernateTemplate hibernateTemplate;
}
本例采用集成测试方法,首先通过Unitils提供的@SpringApplicationContext注解加载Service层和DAO层的配置文件service.xml和dao.xml,然后通过@SpringBean注解从Spring容器中加载HibernateTemplate实例
Web层开发
BaseController基类
public class BaseController {
protected static final String ERROR_MSG_KEY = "errorMsg";
//① 获取保存在Session中的用户对象
protected User getSessionUser(HttpServletRequest request){
return (User) request.getSession().getAttribute(CommonConstant.USER_CONTEXT);
}
//② 将用户对象保存到Session中
protected void setSessionUser(HttpServletRequest request,User user){
request.getSession().setAttribute(CommonConstant.USER_CONTEXT,user);
}
//获取基于应用程序的URL绝对路径
public final String getAppbaseUrl(HttpServletRequest request,String url){
Assert.hasLength(url,"url不能为空");
Assert.isTrue(url.startsWith("/"),"必须以/打头");
return request.getContextPath()+url;
}
}
Web层的每个Controller都有可能设计登录验证处理逻辑,如论坛中只有登录用户才能发表新话题,所以我们提供一个过滤器来处理
public class ForumFilter implements Filter {
private static final String FILTERED_REQUEST = "@@session_context_filtered_request";
// ① 不需要登录即可访问的URI资源
private static final String[] INHERENT_ESCAPE_URIS = { "/index.jsp",
"/index.html", "/login.jsp", "/login/doLogin.html",
"/register.jsp", "/register.html", "/board/listBoardTopics-",
"/board/listTopicPosts-" };
//②执行过滤
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,ServletException{
//确保该过滤器在一次请求中只被调用一次
if((request!=null)&&request.getAttribute(FILTERED_REQUEST)!=null){
chain.doFilter(request,response);
}else{
//设置过滤标识,防止一次请求被多次过滤
request.setAttribute(FILTERED_REQUEST,Boolean.TRUE);
HttpServletRequest httpRequest=(HttpServletRequest)request;
User userContext=getSessionUser(httpRequest);
// ②-3 用户未登录, 且当前URI资源需要登录才能访问
if (userContext == null
&& !isURILogin(httpRequest.getRequestURI(), httpRequest)) {
String toUrl = httpRequest.getRequestURL().toString();
if (!StringUtils.isEmpty(httpRequest.getQueryString())) {
toUrl += "?" + httpRequest.getQueryString();
}
// ②-4将用户的请求URL保存在session中,用于登录成功之后,跳到目标URL
httpRequest.getSession().setAttribute(LOGIN_TO_URL, toUrl);
// ②-5转发到登录页面
request.getRequestDispatcher("/login.jsp").forward(request,
response);
return;
}
chain.doFilter(request, response);
}
}
//当前URI资源是否需要登录才能访问
private boolean isURILogin(String requestURI, HttpServletRequest request) {
if (request.getContextPath().equalsIgnoreCase(requestURI)
|| (request.getContextPath() + "/").equalsIgnoreCase(requestURI))
return true;
for (String uri : INHERENT_ESCAPE_URIS) {
if (requestURI != null && requestURI.indexOf(uri) >= 0) {
return true;
}
}
return false;
}
protected User getSessionUser(HttpServletRequest request) {
return (User) request.getSession().getAttribute(USER_CONTEXT);
}
public void destroy() {
}
public void init(FilterConfig filterConfig) throws ServletException {
}
}
用户登录和注销
用户登录和注销功能由LoginController负责,LoginController通过调用服务层的UserService类完成相应的业务操作
@Controller //标注为一个Spring MVC的Controller
@RequestMapping("/login") //负责处理login.jsp的请求
public class LoginController extends BaseController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
//①用户登录
@RequestMapping("/doLogin")
public ModelAndView login(HttpServletRequest request,User user){
User dbUser=userService.getUserByUserName(user.getUserName());
ModelAndView mav=new ModelAndView();
mav.setViewName("forward:/login.jsp"); //转向的页面
if(dbUser==null){
mav.addObject("errorMsg","用户不存在");
}else if(!dbUser.getPassword().equals(user.getPassword())){
mav.addObject("errorMsg","用户密码不正确");
}else if(dbUser.getLocked()==User.USER_LOCK){
mav.addObject("errorMsg","用户已被锁定,不能登录");
}else {
dbUser.setLastIp(request.getRemoteAddr());
dbUser.setLastVisit(new Date());
userService.loginSuccess(dbUser);
setSessionUser(request,dbUser);
String toUrl = (String)request.getSession().getAttribute(CommonConstant.LOGIN_TO_URL);
request.getSession().removeAttribute(CommonConstant.LOGIN_TO_URL);
//如果当前会话中没有保存登录之前的请求URL,则直接跳转到主页
if(StringUtils.isEmpty(toUrl)){
toUrl="/index.html";
}
mav.setViewName("redirect:"+toUrl);
}
return mav;
}
//② 登录注销
//logout将User从Session中移除,并转到论坛主页中
@RequestMapping("/doLogout")
public String logout(HttpSession session){
session.removeAttribute(USER_CONTEXT);
return "forward:/index.jsp";
}
}
login()方法负责处理用户登录操作,当用户名不存在,密码不正确或用户已被锁定时,都直接转到登录页面并报告相关的错误信息;否则添加5个积分并将其保存到HTTP Session中,然后转向成功页面
在①中还判断当前会话是否存在登录之前的请求URL(这个请求URL在论坛的过滤器中设置),如果存在则跳转到这个URL,否则就直接主页(index.html)
logout将User从Session中移除,并转到论坛主页中
用户注册
负责用户注册的RegisterController
@Controller
public class RegisterController extends BaseController{
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
//用户注册
@RequestMapping(value="/register",method = RequestMethod.POST)
public ModelAndView register(HttpServletRequest request,User user){
ModelAndView view=new ModelAndView();
view.setViewName("/success");
try {
userService.register(user);
}catch (UserExistException e){
view.addObject("errorMsg","用户名已经存在,请选择其他名字");
view.setViewName("forward:/register.jsp");
}
setSessionUser(request,user);
return view;
}
}
配置用户注册的JSP页面register.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户注册</title>
<script>
function mycheck(){
if(document.all("user.password").value != document.all("again").value){
alert("两次输入的密码不正确,请更正。");
return false;
}else
{
return true;
}
}
</script>
</head>
<body>
用户注册信息:
<form action="<c:url value="/register.html" />" method="post" onsubmit="return mycheck()">
<c:if test="${!empty errorMsg}">
<div style="color=red">${errorMsg}</div>
</c:if>
<table border="1px" width="60%">
<tr>
<td width="20%">用户名</td>
<td width="80%">
<input type="text" name="userName"/>
</td>
</tr>
<tr>
<td width="20%">密码</td>
<td width="80%"><input type="password" name="password"/></td>
</tr>
<tr>
<td width="20%">密码确认</td>
<td width="80%"><input type="password" name="again"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="保存">
<input type="reset" value="重置">
</td>
</tr>
</table>
</form>
</body>
</html>
论坛管理
论坛管理模块对应论坛管理员所使用的各项操作功能,包括创建论坛版块,指定论坛版块管理员,用户锁定/解锁功能。
ForumManagerController负责处理这些请求
//论坛管理,这部分功能由论坛管理员操作,包括:创建论坛版块、指定论坛版块管理员、用户锁定/解锁。
@Controller
public class ForumManageController extends BaseController{
private ForumService forumService;
private UserService userService;
@Autowired
public void setForumService(ForumService forumService) {
this.forumService = forumService;
}
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
// ① 列出所有的论坛模块
@RequestMapping(value = "/index", method = RequestMethod.GET)
public ModelAndView listAllBoards() {
ModelAndView view =new ModelAndView();
List<Board> boards = forumService.getAllBoards();
view.addObject("boards", boards);
view.setViewName("/listAllBoards");
return view;
}
//② 添加一个主题帖子页面
@RequestMapping(value = "/forum/addBoardPage", method = RequestMethod.GET)
public String addBoardPage() {
return "/addBoard";
}
//③ 添加一个主题帖子
@RequestMapping(value = "/forum/addBoard", method = RequestMethod.POST)
public String addBoard(Board board) {
forumService.addBoard(board);
return "/addBoardSuccess";
}
// ④ 指定论坛管理员的页面
@RequestMapping(value="/forum/setBoardManagerPage",method = RequestMethod.GET)
public ModelAndView setBoardManagerPage(){
ModelAndView view=new ModelAndView();
List<Board> boards=forumService.getAllBoards();
List<User> users=userService.getAllUsers();
view.addObject("boards",boards);
view.addObject("users",users);
view.setViewName("/setBoardManager");
return view;
}
//设置版块管理
@RequestMapping(value = "/forum/setBoardManager", method = RequestMethod.POST)
public ModelAndView setBoardManager(@RequestParam("userName") String userName
,@RequestParam("boardId") String boardId) {
ModelAndView view =new ModelAndView();
User user = userService.getUserByUserName(userName);
if (user == null) {
view.addObject("errorMsg", "用户名(" + userName
+ ")不存在");
view.setViewName("/fail");
} else {
Board board = forumService.getBoardById(Integer.parseInt(boardId));
user.getManBoards().add(board);
userService.update(user);
view.setViewName("/success");
}
return view;
}
//用户锁定及解锁管理页面
@RequestMapping(value = "/forum/userLockManagePage", method = RequestMethod.GET)
public ModelAndView userLockManagePage() {
ModelAndView view =new ModelAndView();
List<User> users = userService.getAllUsers();
view.setViewName("/userLockManage");
view.addObject("users", users);
return view;
}
//用户锁定及解锁设定
@RequestMapping(value = "/forum/userLockManage", method = RequestMethod.POST)
public ModelAndView userLockManage(@RequestParam("userName") String userName
,@RequestParam("locked") String locked) {
ModelAndView view =new ModelAndView();
User user = userService.getUserByUserName(userName);
if (user == null) {
view.addObject("errorMsg", "用户名(" + userName
+ ")不存在");
view.setViewName("/fail");
} else {
user.setLocked(Integer.parseInt(locked));
userService.update(user);
view.setViewName("/success");
}
return view;
}
}
通过调用服务层的UserService和ForumService完成相应业务逻辑。
由于进行用户锁定,指定论坛版块管理员等操作都需要一个具体的操作页面,所以ForumManageController的另一个工作是将请求导向一个具体的操作页面中,如②和④
ForumManageController共有4个转向页面
①userLockManagePage:对应WEB-INF/jsp/userLockManagePage.jsp页面,即用户解锁和锁定的操作页面
②setBoardManagerPage:对应WEB-INF/jsp/setBoardManagerPage.jsp页面,这是设置论坛版块管理员的处理页面
③listAllBoards:对应WEB-INF/jsp/listAllBoards.jsp页面,该页面显示论坛版块列表
④addBoardPage:对应WEB-INF/jsp/addBoard.jsp页面,是新增论坛版块的表单页面
论坛普通功能
论坛普通功能包括显示论坛版块列表,显示论坛版块主题列表,发表主题帖子,回复帖子,删除帖子,设置精华帖子等,这些功能由BoardManageController负责处理
/**
* 这个Action负责处理论坛普通操作功能的请求,包括:显示论坛版块列表、显示论坛版块主题列表、
* 表主题帖、回复帖子、删除帖子、设置精华帖子等操作。
*/
@Controller
public class BoardManageController extends BaseController {
private ForumService forumService;
@Autowired
public void setForumService(ForumService forumService) {
this.forumService = forumService;
}
/**
* 列出论坛模块下的主题帖子
*
* @param boardId
* @return
*/
@RequestMapping(value = "/board/listBoardTopics-{boardId}", method = RequestMethod.GET)
public ModelAndView listBoardTopics(@PathVariable Integer boardId,@RequestParam(value = "pageNo", required = false) Integer pageNo) {
ModelAndView view =new ModelAndView();
Board board = forumService.getBoardById(boardId);
pageNo = pageNo==null?1:pageNo;
Page pagedTopic = forumService.getPagedTopics(boardId, pageNo,
CommonConstant.PAGE_SIZE);
view.addObject("board", board);
view.addObject("pagedTopic", pagedTopic);
view.setViewName("/listBoardTopics");
return view;
}
/**
* 添加主题帖页面
*
* @param boardId
* @return
*/
@RequestMapping(value = "/board/addTopicPage-{boardId}", method = RequestMethod.GET)
public ModelAndView addTopicPage(@PathVariable Integer boardId) {
ModelAndView view =new ModelAndView();
view.addObject("boardId", boardId);
view.setViewName("/addTopic");
return view;
}
/**
* 添加一个主题帖
*
* @param request
* @param topic
* @return
*/
@RequestMapping(value = "/board/addTopic", method = RequestMethod.POST)
public String addTopic(HttpServletRequest request,Topic topic) {
User user = getSessionUser(request);
topic.setUser(user);
Date now = new Date();
topic.setCreateTime(now);
topic.setLastPost(now);
forumService.addTopic(topic);
String targetUrl = "/board/listBoardTopics-" + topic.getBoardId()
+ ".html";
return "redirect:"+targetUrl;
}
/**
* 列出主题的所有帖子
*
* @param topicId
* @return
*/
@RequestMapping(value = "/board/listTopicPosts-{topicId}", method = RequestMethod.GET)
public ModelAndView listTopicPosts(@PathVariable Integer topicId,@RequestParam(value = "pageNo", required = false) Integer pageNo) {
ModelAndView view =new ModelAndView();
Topic topic = forumService.getTopicByTopicId(topicId);
pageNo = pageNo==null?1:pageNo;
Page pagedPost = forumService.getPagedPosts(topicId, pageNo,
CommonConstant.PAGE_SIZE);
// 为回复帖子表单准备数据
view.addObject("topic", topic);
view.addObject("pagedPost", pagedPost);
view.setViewName("/listTopicPosts");
return view;
}
/**
* 回复主题
*
* @param request
* @param post
* @return
*/
@RequestMapping(value = "/board/addPost")
public String addPost(HttpServletRequest request, Post post) {
post.setCreateTime(new Date());
post.setUser(getSessionUser(request));
Topic topic = new Topic();
int topicId = Integer.valueOf(request.getParameter("topicId"));
if (topicId >0) {
topic.setTopicId(topicId);
post.setTopic(topic);
}
forumService.addPost(post);
String targetUrl = "/board/listTopicPosts-"
+ post.getTopic().getTopicId() + ".html";
return "redirect:"+targetUrl;
}
/**
* 删除版块
*/
@RequestMapping(value = "/board/removeBoard", method = RequestMethod.GET)
public String removeBoard(@RequestParam("boardIds") String boardIds) {
String[] arrIds = boardIds.split(",");
for (int i = 0; i < arrIds.length; i++) {
forumService.removeBoard(new Integer(arrIds[i]));
}
String targetUrl = "/index.html";
return "redirect:"+targetUrl;
}
/**
* 删除主题
*/
@RequestMapping(value = "/board/removeTopic", method = RequestMethod.GET)
public String removeTopic(@RequestParam("topicIds") String topicIds,@RequestParam("boardId") String boardId) {
String[] arrIds = topicIds.split(",");
for (int i = 0; i < arrIds.length; i++) {
forumService.removeTopic(new Integer(arrIds[i]));
}
String targetUrl = "/board/listBoardTopics-" + boardId + ".html";
return "redirect:"+targetUrl;
}
/**
* 设置精华帖
*/
@RequestMapping(value = "/board/makeDigestTopic", method = RequestMethod.GET)
public String makeDigestTopic(@RequestParam("topicIds") String topicIds,@RequestParam("boardId") String boardId) {
String[] arrIds = topicIds.split(",");
for (int i = 0; i < arrIds.length; i++) {
forumService.makeDigestTopic(new Integer(arrIds[i]));
}
String targetUrl = "/board/listBoardTopics-" + boardId + ".html";
return "redirect:"+targetUrl;
}
}