论坛开发实例 (精通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;    }}
阅读全文
0 0
原创粉丝点击