论坛开发实例 (精通Spring+4.x++企业应用开发实战)
来源:互联网 发布:编程语言实现模式 源码 编辑:程序博客网 时间:2024/06/07 15:37
功能概述
论坛整体功能
论坛普通功能为普通用户所有,论坛板块管理为论坛版块管理员和论坛管理员所有,论坛管理为论坛管理员所有
论坛用例描述
操作权限是递增的
主要功能描述
用户登录
发表主题帖子
游客不可发表主题帖子
回复主题帖
游客不可回复帖子
删除帖子
置精华帖
指定论坛板块管理员
系统设计
技术框架选择
采用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
@Repositorypublic 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
@Repositorypublic 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
@Repositorypublic class LoginLogDao extends BaseDao<LoginLog> { public void save(LoginLog loginLog) { this.getHibernateTemplate().save(loginLog); }}
PostDao
@Repositorypublic 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 - linkxjdbc.driverClassName=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://localhost:3306/sampledb?useUnicode=true&characterEncoding=UTF-8jdbc.username=rootjdbc.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,springunitils.module.dbunit.className=org.dbunit.MySqlDbUnitModule#②配置数据库连接database.driverClassName=com.mysql.jdbc.Driverdatabase.url=jdbc:mysql://localhost:3306/sampledb?useUnicode=true&characterEncoding=UTF-8database.dialect = mysqldatabase.userName=rootdatabase.password=123456database.schemaNames=sampledb#③ 配置数据库维护策略updateDataBaseSchema.enabled=false#④配置数据库创建策略dbMaintainer.autoCreateExecutedScriptsTable=false#dbMaintainer.script.locations=D:/book/svn2/code/spring4x-project/spring4x-chapter20/src/test/resources/dbscriptsdbMaintainer.script.locations=src/test/resources/dbscripts#⑤ 配置数据集工厂DbUnitModule.DataSet.factory.default=com.smart.test.dataset.excel.MultiSchemaXlsDataSetFactoryDbUnitModule.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 是没有事务,默认情况下,事务管理是disabledDatabaseModule.Transactional.value.default=commit# XSD generatordataSetStructureGenerator.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;@Servicepublic 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
@Controllerpublic 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负责处理这些请求
//论坛管理,这部分功能由论坛管理员操作,包括:创建论坛版块、指定论坛版块管理员、用户锁定/解锁。@Controllerpublic 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负责处理论坛普通操作功能的请求,包括:显示论坛版块列表、显示论坛版块主题列表、 * 表主题帖、回复帖子、删除帖子、设置精华帖子等操作。 */@Controllerpublic 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; }}
- 论坛开发实例 (精通Spring+4.x++企业应用开发实战)
- Spring 入门实例 简易登录系统(精通Spring+4.x++企业应用开发实战 学习笔记一)
- Spring boot入门实例 简易登录(精通Spring+4.x++企业应用开发实战 学习笔记二)
- Spring容器高级主题(精通Spring+4.x++企业应用开发实战 五)
- Spring基础(精通Spring+4.x++企业应用开发实战 第7章)
- Spring SpEL(精通Spring+4.x++企业应用开发实战 第九章)
- Spring对DAO的支持(精通Spring+4.x++企业应用开发实战 第十章)
- Spring的事务管理(精通Spring+4.x++企业应用开发实战 第十一章)
- Ioc容器 (精通Spring+4.x++企业应用开发实战 三)
- 在IoC容器中装配Bean(精通Spring+4.x++企业应用开发实战 四)
- Spring 3.x 企业应用开发实战
- Spring 3.x企业应用开发实战
- Spring的事务管理难点剖析(精通Spring+4.x++企业应用开发实战 第12章)
- 基于@AspectJ和Schema的AOP(精通Spring+4.x++企业应用开发实战 第八章)
- 精通Spring 2.x —— 企业应用开发详解
- [读书笔记] 精通Spring 2.x - 企业应用开发详解
- Spring 3.x企业应用开发实战(4)----资源加载
- Spring 3.x企业应用开发实战(6)----BeanFactory
- 计蒜客-2017 ACM-ICPC 亚洲区(乌鲁木齐赛区)网络赛-E-Half-consecutive Numbers
- Maven项目引入log4j的详细配置
- 【上外青年】人物 ‖ 何晗:天才都是异类
- tar.xz文件的压缩解压
- gitlab常用命令
- 论坛开发实例 (精通Spring+4.x++企业应用开发实战)
- raphael.js+servlet仿http://map.norsecorp.com/做态势感知
- ExecutorService
- SpringBoot中进行邮件发送
- 深入java 序列化的一些坑(转载)
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 多线程/并发笔记:线程创建的三种方式
- 本地数据库-SQLLite
- 《Effective C++读书笔记》--条款34:区分接口继承和实现继承