目录
开发之前的一些注意点
- 创建数据库的时候,指定
码表
,避免本地正常,发布到服务器上的时候出现乱码 ; 创建表的时候,id选用
UUID
,是为了以后合并表的时候,id
不会冲突;(主要考虑是否有合并的可能性,没有合并的可能性,选择自增长的int
)table
打开边框的时候,使用frame="boder"
打开,这样即使没数据也会显示空的单元格;删除文件的时候,将删除数据记录和删除硬盘文件放在一个事务里面;并且先删除数据库记录,后删除文件 ;
假如先删除文件,后删除记录,再删除记录的时候,抛出异常,则数据库回滚,这时候文件已经被删除了,是不能被回滚的,就是操作数据库会出现异常,要保证异常发生时,2者统一;数据库建库 规定字符集为
UTF-8
:CREATE DATABASE test CHARACTER SET utf8 COLLATE utf8_general_ci;
建表 规定字符集为
UTF-8
:ENGINE=InnoDB DEFAULT CHARSET=utf8;
从
request
中获取参数的时候,如果Javabean
对应的属性,没有提交过来的参数。Beautil
会为对应的属性赋予默认值
;建表语句(实例):
最后加上规定字符集的语句
create table file ( uuid varchar(40) unique , simpleName varchar(20) not null, uploaderName varchar(10) not null, uploadDate date not null, description varchar(40) not null, savePath varchar(100) not null ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
domain设计:
对文件的描述:
File类
: 属性包括:上传者姓名
,上传时间
、文件名
、文件UUID
、文件保存路径
、文件描述
;分页的设计
封装查询信息:
QueryInfo
对象 : 属性包括:页面数据量
,查询的页码
,数据库查询起始下标
;其中页面数据量和起始下标有个默认值,不指定就以该默认值为准;
public class QueryInfo { private int startIndex ; private int pageSize = 5 ; private int currentPage = 1 ; // 起始坐标,通过当前页和页面显示数据量计算得出 public int getStartIndex() { startIndex = (currentPage - 1) * pageSize ; return startIndex; } .... .... }
QueryResult
对象 :属性包括:保存查询到的文件对象的集合
、数据库数据总量
;PageBean
对象 :(属性就是页面要显示的数据)1、当前页面(也就是QueryInfo的查询的页码),为了显示当前在第几页; 2、页面显示的数据量(就是QueryInfo里面的页码数据量), 为了显示当前页面显示多少数据量,也为了翻页的pageSize的属性自动赋值 ; 3、总页数(根据总条数除以页面显示数量,计算得到)
// 计算总页数 public int getTotalPage() { totalPage = totalRecord % pageSize == 0 ? totalRecord / pageSize : totalRecord / pageSize + 1; return totalPage; }
4、页码条 先设置一个开始坐标和一个结束坐标;初始值都是0; 然后判断页码总长度是否大于我们设定的页码条的长度; 如果不大于,则页码条就是当前的页面数,无需做特殊处理; 如果大于,则开始坐标是当前页面坐标减去页码条长度的一半, 结束坐标是当前页面坐标加上页码条长度的一半; 然后判断,开始坐标是否小于1,如果小于,则令开始坐标为1,结束坐标为页码条的长度; 结束坐标是否大于总页面数,如果大于,则令结束坐标为页面总长度,开始坐标为页面总长度减去页码条长度;
// 计算页码条 public int[] getPageBar() { int startIndex = 0; int endIndex = 0; // 默认显示 5 个页码条 if (getTotalPage() < 5) { pageBar = new int[getTotalPage()]; startIndex = 1; endIndex = getTotalPage(); } else { pageBar = new int[5]; startIndex = currentPage - 2; endIndex = currentPage + 2; // 判断是否越界 if (startIndex < 1) { startIndex = 1; endIndex = startIndex + 4; } if (endIndex > getTotalPage()) { endIndex = getTotalPage(); startIndex = getTotalPage() - 4; } } int index = 0; for (int i = startIndex; i <= endIndex; i++) { pageBar[index++] = i; } return pageBar; }
5、页面显示的文件对象集合 6、上一页、下一页 7、总记录数。(计算总页数需要用到)
JdbcUtils设计
(使用C3P0连接池,注意配置文件的存放位置,在包的根目录下面;构造器不写参数,就是读取默认配置)
使用
ThreadLocal
类,实现事务;getDataSource() :获取连接池 getConnecetion() : 获取连接 ,获取之前,先判断当前线程上是否有绑定的连接 ; stratTransaction() : 开启事务,先获取连接,然后绑定到当前线程上,然后开启事务 ; commitTranscation(): 提交事务,获取当前线程上的连接,然后提交事务 ; closeConnecetion() : 关闭连接,已经移除ThreadLocal里面的连接 ;
public class JdbcUtils {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
private static ComboPooledDataSource dataSource;
static {
try {
// 没有参数,则使用默认的配置,创建数据库连接池
dataSource = new ComboPooledDataSource();
} catch (Exception e) {
// 抛出初始化异常
throw new ExceptionInInitializerError(e);
}
}
/**
* 获取数据库连接池
*
* @return
*/
public static DataSource getDataSource() {
return dataSource;
}
/**
* 获取连接,底层也是从数据库里面获取连接,
* <p>
* 判断当前线程上有没有连接,便于开启事务
*
* @return
* @throws SQLException
*/
public static Connection getConnection() {
Connection connection = threadLocal.get();
try {
if (connection == null) {
connection = dataSource.getConnection();
}
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 为当前线程开启事务操作
*/
public static void startTransaction() {
Connection connection = threadLocal.get();
try {
if (connection == null) {
connection = dataSource.getConnection();
threadLocal.set(connection);
}
connection.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 提交事务
*/
public static void commitTransaction(){
Connection connection = threadLocal.get() ;
if (connection == null) {
return;
}
try {
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e) ;
}
}
/**
* 使用了连接池以后,不需要我们去关注连接的关闭问题了。
*
* 但是使用了事务的连接,还是需要我们自己去关闭的 ;
*/
public static void closeConnection(){
Connection connection = threadLocal.get() ;
try {
if (connection != null) {
connection.close();
}
}catch (SQLException e){
e.printStackTrace();
throw new RuntimeException(e) ;
}finally {
threadLocal.remove();
}
}
}
将
request
中的数据封装到Javabean
中;使用Beanutils; 先将request中的参数,封装到map集合中:request.getParameterMap() ; 再调用Beanutils的方法(BeanUtils.populate(bean,map)),将map中的元素,封装到Javabean中; 注意,如果request没有对应的属性,则属性被赋予默认值;
ShowUtils
将request中的查询信息封装到QueryInfo中,便于分页;,写一个工具类,就避免了我们为QueryInfo对象的每一个属性,一一赋值; 然后通过services获取到PageBean,传给显示的JSP;
将上传的文件保存到服务器硬盘上,并且返回一个
bean
对象;首先通过文件的item,获取文件的名字,如果为空串,则表示没有上传文件; 检查文件后缀名是否允许上传;合法,就将其保存到文件对象的属性中,不合法则抛出自定义的后缀名异常; 然后通过文件名字,进行分配文件保存路径;还是保存到文件对象的Javabean上; 获取UUID、日期,都保存到文件对象的Javabean上; 最后将文件写到服务器硬盘上,注意的是,数据库里面仅仅保存文件保存的路径,而不真正的保存文件; 写文件,通过文件的item,获取输入流,然后new FileOutputStream 参数是 路径_文件名 ; 跟java中IO一样,这里参数写上绝对路径,假如写上相对路径, 在JSP、servlet中写上相对路径,相对的是tomcat的bin路径,因为这是在J2E项目中;
计算文件分配路径
通过文件名的hashcode,分配文件路径;跟之前讲的分配算法是一样的思路;
/**
* 最多可以保存8层,这样的算法,这里仅取两层
*
* @param name
* @return WEB-INF/upload/5/6
*/
public static String getPath(String path, String name) {
int hashcode = name.hashCode();
int one = hashcode & 0xf;
int two = (hashcode >> 4) & 0xf;
File file = new File(path + File.separator + one + File.separator + two);
if (!file.exists()) {
file.mkdirs();
}
return file.getPath();
}
dao
层设计
add(File) : 往数据库添加文件 ;
delete(UUID); 根据uuid删除数据库记录,已经本地磁盘保存的文件;
用事务操作,以保证操作的原子性,并且对本地文件的操作,需要放在操作数据库之前,
因为一旦数据库出现问题,可以回滚 ,而本地文件一旦执行操作是无法回滚的,
因此,先执行数据库操作,错误发生,代码停止执行,这样避免执行对本地的操作;
update(File) : 更新数据库记录,以及本地磁盘文件的文件名;
如果需要的话,也需要开启事务 ,改名字使用 原文件.renameTo(想要给成的样子的文件);
getFiles(QueryInfo) ; 返回一个QueryResult, 根据起始下标,查询数据,一页显示的数据,总记录数;
总记录用ScalarHandler<T>() 类,它将查询的结果以泛型的类型返回,注意的是count(*)返回的是long类型 ;
find(UUID): 根据UUID 查询对应的file对象 ;
// 查询总记录数
sql = "select count(*) from file";
int count = runner.query(sql, new ScalarHandler<Long>()).intValue();
services
层设计
增删改查,直接调用dao层方法即可,注意删除和更改的时候,需要开启事务;
pageBean对象,从pageResult对象中获取四个属性,剩下的属性,通过计算得出;
总记录数,页面数据、当前页,页面显示数量,从pageResult中获取;上一页、下一页、总页数、页码条,通过计算得出;
web
层设计
·上传文件
先利用ServletFileUpload判断表单类型;
然后获取工厂、解析器、设置解析器的码表,这里的码表设定,仅仅能解决字段名字的乱码,字段内容的乱码,等下还需要设定码表;
解析客户端传来的流,判断是普通字段还是文件,对文件进行保存;这里使用2次for循环,第一次将普通字段保存下来,第二次将文件保存下来;
·显示文件
获取到PageBean 对象,在JSP中显示,其中点击函数的参数可以这样写:onclick="go2Page(document.getElementById('goPage').value)" ;通过获取页面的节点值;
控制浏览器URI地址 : window.location.href = '${pageContext.request.contextPath}/show?currentPage=' + currentPage + '&pageSize=' + value;
显示的时候,超链接后面挂的都是文件的UUID,便于在数据可中检索文件;
·下载文件
通过UUID获取到文件,判断是否为空,为空则文件已经被删除了,告知来晚了,文件已被删除!
通过数据库查询出来的File对象,获取其path属性,得到文件保存的实际位置;
对文件的名字,进行处理,避免文件名乱码;
// 下载文件名乱码解决方法
String path = file.getSavePath();
String simpleName = file.getSimpleName();
// 浏览器分为 火狐 和 非火狐
if (request.getHeader("USER-AGENT").toLowerCase().indexOf("firefox") >= 0) {
// 火狐头大,需要独特设置一下
simpleName = new String(simpleName.getBytes("UTF-8"), "iso-8859-1");
} else {
simpleName = URLEncoder.encode(simpleName, "UTF-8");
// IE 文件名有空格会被加号代替。需要自己替换回去
simpleName = simpleName.replaceAll("\+","%20");
}
// 文件名有空格。火狐则会截断文件名,需要将文件名用字符串包起来
// 告诉浏览器下载方式打开.,
response.addHeader("Content-Disposition", "attachment;filename="" + simpleName+""");
·删除文件
通过客户端传来的UUID,调用services层删除对应的文件;删除的时候注意判断下,文件是否存在;
·更新文件
通过UUID获取到文件信息,将文件信息回显到表格中,便于修改信息;