(二十七)文件上传下载开发流程(含部分代码)

目录


开发之前的一些注意点

  1. 创建数据库的时候,指定码表,避免本地正常,发布到服务器上的时候出现乱码 ;
  2. 创建表的时候,id选用UUID,是为了以后合并表的时候,id不会冲突;(主要考虑是否有合并的可能性,没有合并的可能性,选择自增长的int

  3. table打开边框的时候,使用 frame="boder" 打开,这样即使没数据也会显示空的单元格;

  4. 删除文件的时候,将删除数据记录和删除硬盘文件放在一个事务里面;并且先删除数据库记录,后删除文件 ;假如先删除文件,后删除记录,再删除记录的时候,抛出异常,则数据库回滚,这时候文件已经被删除了,是不能被回滚的,就是操作数据库会出现异常,要保证异常发生时,2者统一;
  5. 数据库建库 规定字符集为UTF-8

    CREATE DATABASE test CHARACTER SET utf8 COLLATE
    utf8_general_ci;
  6. 建表 规定字符集为UTF-8

    ENGINE=InnoDB DEFAULT CHARSET=utf8;
  7. request中获取参数的时候,如果Javabean对应的属性,没有提交过来的参数。Beautil 会为对应的属性赋予默认值

  8. 建表语句(实例):

    最后加上规定字符集的语句
    
    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设计:

  1. 对文件的描述:

    File类 : 属性包括:上传者姓名上传时间文件名文件UUID文件保存路径文件描述

  2. 分页的设计

    封装查询信息:

    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连接池,注意配置文件的存放位置,在包的根目录下面;构造器不写参数,就是读取默认配置)
  1. 使用 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();
        }

    }


}
  1. request中的数据封装到Javabean中;

            使用Beanutils;
    
            先将request中的参数,封装到map集合中:request.getParameterMap() ;
    
            再调用Beanutils的方法(BeanUtils.populate(bean,map)),将map中的元素,封装到Javabean中;
    
            注意,如果request没有对应的属性,则属性被赋予默认值;
    
  2. ShowUtils

            将request中的查询信息封装到QueryInfo中,便于分页;,写一个工具类,就避免了我们为QueryInfo对象的每一个属性,一一赋值;
    
            然后通过services获取到PageBean,传给显示的JSP;
    
  3. 将上传的文件保存到服务器硬盘上,并且返回一个bean对象;

            首先通过文件的item,获取文件的名字,如果为空串,则表示没有上传文件;
    
            检查文件后缀名是否允许上传;合法,就将其保存到文件对象的属性中,不合法则抛出自定义的后缀名异常;
    
            然后通过文件名字,进行分配文件保存路径;还是保存到文件对象的Javabean上;
    
            获取UUID、日期,都保存到文件对象的Javabean上;
    
            最后将文件写到服务器硬盘上,注意的是,数据库里面仅仅保存文件保存的路径,而不真正的保存文件;
    
            写文件,通过文件的item,获取输入流,然后new FileOutputStream 参数是 路径_文件名 ; 跟java中IO一样,这里参数写上绝对路径,假如写上相对路径,
    
                在JSP、servlet中写上相对路径,相对的是tomcat的bin路径,因为这是在J2E项目中;
    
  4. 计算文件分配路径

            通过文件名的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获取到文件信息,将文件信息回显到表格中,便于修改信息;
原文地址:https://www.cnblogs.com/young-youth/p/11665694.html