web中文件的上传与下载

一,表单的形式。

1.jsp:

<!-- pageContext.request.contextPath获取绝对路径 -->
<form action="${pageContext.request.contextPath}/UploadHandleServlet" enctype="multipart/form-data" method="post">
上传用户:<input type="text" name="username"><br/>
上传文件1:<input type="file" name="file1"><br/>
上传文件2:<input type="file" name="file2"><br/>
<input type="submit" value="提交">
</form>

2.Servlet:实现上传存储。

@WebServlet(urlPatterns= {"/UploadHandleServlet"})
public class UploadHandleServlet extends HttpServlet{
      private static final long serialVersionUID = 1L;
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doGet()");
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       //System.out.println(req.getParameter("file1")); //request直接是拿不到表单参数,要经过下面的工厂类解析。
             //首先要导外包:commons-fileupload-1.2.1.jar,commons-io-2.0.jar
        
//      ❶获取和创建保存文件的最终目录和监时目录。
        String savePath = req.getSession().getServletContext().getRealPath("/WEB-INF/upload");  //保存文件的服务器上的绝对路径,预先创建的(不是操作系统的)
        String tempPath = req.getSession().getServletContext().getRealPath("/WEB-INF/temp");    //保存文件的临时目录,动态创建的
        File tempFile = new File(tempPath);
        if(!tempFile.exists()) {                                                                 //如果临时目录不存在则创建 之。
            tempFile.mkdirs();
        }
//      ❷  解析 request请求
        //1.创建一个工厂类。 
        DiskFileItemFactory factory = new DiskFileItemFactory();
            //public DiskFileItemFactory(int sizeThreshold,File repository)
            //sizeThreshold:  服务器里内存,有资源上限,例如:上传文件大小超过物理内存,会死机
            //                 sizeThreshold临界值:600KB,上传文件小于600KB,我就直接把文件放在内存中,这样很快
            //                 传来的文件大于600KB,把它分成一块一块的,大于600KB的多余的放在磁盘中。程序需要时再去取之。
            //repository:      指定磁盘存放文件的文件夹。
        factory.setSizeThreshold(100*1024);                   //100KB,上传的文件小100KB,放在内存中,大100KB放进tempPath
        factory.setRepository(tempFile);                      //设置临时目录
        //2.创建request请求的解析器。
        ServletFileUpload  sfu=new ServletFileUpload(factory);
            //sfu这个解析器,也是可以设置对上传文件的大小的限制
            //sfu.setFileSizedMax()  总的文件大小
            //sfu.setSizeMax()       单个文件的大小
        sfu.setFileSizeMax(20*1204*1024);                    //限制上传单个文件的大小在20M以内
        sfu.setHeaderEncoding("UTF-8");                      //防止中文乱码
        sfu.setSizeMax(40*1204*1024);                        //上传所有文件的大小
        sfu.setProgressListener(new ProgressListener() {     //上传文件进度临听器
            @Override
            public void update(long yUploadFileSize, long uploadFileSize, int arg) {
                System.out.println("上传文件总大小为: "+uploadFileSize+",已上传文件大小: "+yUploadFileSize);
                
            }
        });
        //3.解析request请求,返回List<FileItem>
        if(!ServletFileUpload.isMultipartContent(req)) {
            return;                                      //如果不是multipart/form-data数据编码方式,则退出程序
        }
        OutputStream out = null;
        InputStream  in=null;
        try {
            List<FileItem> filelist = sfu.parseRequest(req);  //FileItem就是封装一个个form提交过来的表单项:普通表单项/文件域表单项
            if(filelist!=null && filelist.size()>0) {
            for(FileItem fileItem:filelist){
                
                if(fileItem.isFormField()) {              //如果是普通表单项,则只输出打印相关信息
                    String name=fileItem.getFieldName();  //拿到表单项的value如<input type="text" name="username" value="姓名">相当于键值对的键
                    String value = fileItem.getString("UTF-8");  //拿到表单中的内容
                    System.out.println("普通的表单项,名为:"+name+",内容为: "+value);
                }else {                    //若是文件域表单
                    String fileName = fileItem.getName();      //拿到文件的名字 名字带扩展名如:xiong.txt
                              //注意:fileName,IE中带绝对地址如'd:\abc\xiong.txt',火狐中只显示  'xiong.txt'
                    String fileType=fileItem.getContentType(); //拿到文件的类型
                    long fileSize=fileItem.getSize();          //文件的大小
                    fileName = fileName.substring(fileName.lastIndexOf("\\")+1);   //从最后一个'\'查找截取,这样就避免了不同浏览器的格式。
                    if(fileName==null || fileName.trim().equals("")) {
                        continue;                                     //如文件名为空或去掉首尾空格为空字符串,则退出本次循环,可以继续。。。
                    }
                    String fileNameEx =  fileName.substring(fileName.lastIndexOf(".")+1);   //拿到文件的后缀名即扩展名。
                    if(fileNameEx.equals("rar")||fileNameEx.equals("zip")) {
                        throw new RuntimeException("禁止上传压缩文件");
                    }
                    //将文件流写入保存的目录中(生成新的文件名,避免一个目录中文件太多而生成新的存储目录)
                    String saveFileName = makeFileName(fileName);  //确保产生的文件名不重复
                    String realSavePath = makePath(saveFileName,savePath);
                    
                    //先创建一个输出流
                    out = new FileOutputStream(realSavePath+"\\"+saveFileName);
                    in =fileItem.getInputStream();                  //拿到输入流
                    //建立缓冲器,建立般运流的勺子。
                    byte[] buffer =new byte[1024];
                    int len =0;
                    while((len=in.read(buffer))>0) {
                        out.write(buffer,0,len);   //写出去了
                    }
                    in.close();
                    out.close();
                }
            }
            
        }   
            
        } catch (FileUploadBase.FileSizeLimitExceededException e) {
            System.out.println("单个文件的大小超出限制");
        }catch(FileUploadBase.SizeLimitExceededException e2) {
            System.out.println("总文件超出限制大小!");
        }catch(Exception e) {
            System.out.println("上传文件失败!");
            e.printStackTrace();
        }finally {                      //最后,万一上面执行不成功,流没关闭不好,
            if(in!=null)
                in.close();
            if(out!=null)
                out.close();
        }
        
    }
    
    /**
     * 在真正保存文件的目录里打散文件
     * @param saveFileName
     * @param savePath
     * @return
     */
    private String makePath(String saveFileName,String savePath) {
        int hashCode = saveFileName.hashCode();  //哈希码由十进制数据组成
        int dir1= hashCode&0xf;  //dir1的值,这个与运算的结果范围为0-15,即一个目录只存放16个文件
        int dir2 = hashCode&0xf>>4; //这个与运算的结果范围为0-15
        String dir = savePath+"\\"+dir1+"\\"+dir2;    //这样更好,用"\\"后,liniux中运行web服务,会不创建文件夹
        File file =new File(dir);
        if(!file.exists()) {
            file.mkdirs();
            //file.mkdir();//如果你想在已经存在的文件夹(D盘下的yy文件夹)下建立新的文件夹(2019-06-17文件夹),就可以用此方法。此方法不能在不存在的文件夹下建立新的文件夹。假如想建立名字是”2019-06-17”文件夹,那么它的父文件夹必须存在。
        }
        
        return dir;
    }
    
    /**
     * 产生一个唯一文件名UUID+fileName的文件
     * @param fileName
     * @return
     */
    private String makeFileName(String fileName) {
        //uuid
        return UUID.randomUUID().toString()+"_"+fileName;  //确保产生的文件名不重复
    }
}
image.png

上段代码是文件上传时,打开文件地址的代码,其中method一定为POST,如果是GET,会上传上不了。
上传文件的表单注意项:
①请求方式必须是post
②使用file的表单域
③使用multipart/form-data的请求编码方式
④为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如放于WEB-INF目录下
⑤为防止一个目录下面出现太多文件,要使用hash算法打散存储。
enctype:规定了form表单在发送到服务器时候编码方式。
他有如下的三个值:
①application/x-www-form-urlencoded。默认的编码方式。但是在用文本的传输,大型文件的时候,使用这种编码就显得 效率低下。
②multipart/form-data。 指定传输数据为二进制类型,比如图片、mp3、文件,。
③text/plain。纯文体的传输。空格转换为 “+” 加号,但不对特殊字符编码。
明确在enctype参数为multipart/form-data的时候post和get请求参数和请求体是什么形式的
GET/www.xxx.com?name=%22hello+world%22&file=temp.png&submit=submit HTTP/1.1
get请求和multipart/form-data结合无效,因为文件上传需要请求体,get下‘网络’-‘请求’项是空的。
post请求(不需写?参数)请求头:
POST /www.xxx.com HTTP/1.1
请求体:在火狐上看看请求体(网络-请求-下显示的内容)!

image.png

通过观察发现这个的请求体就发生了变化。这种请求体被称之为多部件请求体。
什么是多部件请求体:就是把每一个表单项分割为一个部件。
因为表单项分为普通表单项和文件表单项,所以说部件也有区别
普通表单项:
一个请求头:Content-Disposition: form-data; name=”name”
一个请求体:里面就是我们表单的值”hello world“
文件表单项两个请求头:
Content-Disposition: form-data; name="file"; filename="temp.png"
Content-Type: image/png
一个请求体:
注:::::在multipart/form-data的post请求下到serlvet里取一下参数(表单域里)值!为null

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //System.out.println("doPost()");
        System.out.println(req.getParameter("file1"));  //null,即使不是type="file"的表单项也是为null
    }
运行:上传文件后会看到获取的参数值为null.

总结:参数获取不到主要是因为在使用multipart/form-data属性之后请求体发生了变化。不是key=value的形式出现所以说获取不到。

解决办法:
1.我们可以通过js代码来些修改,把我们的参数追加在url的后边

<form id="upload" name="upload" action="fileftp.jsp" method="post" ENCTYPE="multipart/form-data">
      <input type="hidden" name="otherName" id="otherName" value="abcdefg"/>
    <td nowrap>
      <input type="file" id="file1" name="file1" value="" size="40" class="sbttn"/>
     <input type="submit" value="上传" class="sbttn"/>
   </td>
</form>
<script language="javascript">
    function formSubmit(){
           var action="fileftp.jsp";
           action+="?otherName="+document.upload.otherName.value;
          document.upload.action=action;
          document.upload.submit();
 }
</script>

java代码获取FORM带enctype="multipart/form-data"提交的表单项内容,并解析。
2.通过修改服务器端代码。前提是利用jar包。
commons-fileupload-1.2.2.jar和commons-io-1.4.jar,网盘中有
步骤:
<1>.创建工厂类对象:

//1.创建工厂类对象
DiskFileItemFactory factoy=new DiskFileItemFactory();

-------该类有两个参成员是来设置内存临界值和多余部分存放的路径
int sizeThreshold;File repository
sizeThreshold临界值:600KB,上传文件小于600KB,我就直接把文件放在内存中,这样很快
传来的文件大于600KB,把它分成一块一块的,大于600KB的多余的放在磁盘中。程序需要时再去取之。
repository: 指定磁盘存放文件的文件夹。
-------Apache文件上传组件在解析上传数据中的每个字段内容时,需要临时保存解析出的数据,以便在后面进行数据的进一步处理(保存在磁盘特定位置或插入数据库)。因为Java虚拟机默认可以使用的内存空间是有限的,超出限制时将会抛出“java.lang.OutOfMemoryError”错误。如果上传的文件很大,例如800M的文件,在内存中将无法临时保存该文件内容,Apache文件上传组件转而采用临时文件来保存这些数据;但如果上传的文件很小,例如600个字节的文件,显然将其直接保存在内存中性能会更加好些。
-------setSizeThreshold方法用于设置是否将上传文件已临时文件的形式保存在磁盘的临界值(以字节为单位的int值),如果从没有调用该方法设置此临界值,将会采用系统默认值10KB。对应的getSizeThreshold() 方法用来获取此临界值。
------void setRepository(File repository)
setRepositoryPath方法用于设置当上传文件尺寸大于setSizeThreshold方法设置的临界值时,将文件以临时文件形式保存在磁盘上的存放目录。有一个对应的获得临时文件夹的 File getRespository() 方法。
当从没有调用此方法设置临时文件存储目录时,默认采用系统默认的临时文件路径,可以通过系统属性 java.io.tmpdir 获取。Tomcat系统默认临时目录为“<tomcat安装目录>/temp/”
如下代码:

System.getProperty("java.io.tmpdir");

factory工厂类对象:
可以设置上传文件的大小!
//2.创建解析器,解析request的请求。

ServletFileUpload sfu=new ServletFileUpload(factoy);
sfu.setSizeMax(int size)  //也可设置文件总的大小上限, 也可以setFileSizeMax设置单个文件的大小
try {
     List<FileItem> list = sfu.parseRequest(req);  //FileItem就是封装一个个form提交过来的表单项:普通表单项/文件域表单项
            for(FileItem fileItem:list){
                if(fileItem.isFormField()) {            //如果是普通表单项
                    String name=fileItem.getFieldName(); //拿到表单项的value如<input type="text" name="username" value="姓名">相当于键值对的键
                    String value = fileItem.getString();  //拿到表单中的内容
                }else {                    //若是文件域表单
                    fileItem.getName();    //拿到文件的名字
                    fileItem.getContentType(); //拿到文件的类型
                    fileItem.getSize();        //文件的大小
                    InputStream in = fileItem.getInputStream();  //拿到上传上来的文件的输入流
                    OutputStream out = new FileOutputStream("d://temp/xiong.jpg");
                    }
            }
        } catch (FileUploadException e) {
            e.printStackTrace();
        }

FileItem封装表单项内容的类的常用方法:

  1. boolean isFormField()
    isFormField方法用于判断FileItem类对象封装的数据是一个普通文本表单字段,还是一个文件表单字段,如果是普通表单字段则返回true,否则返回false。因此,可以使用该方法判断是否为普通表单域,还是文件上传表单域。
  2. String getName()
    getName方法用于获得文件上传字段中的文件名。
    注意IE或FireFox中获取的文件名是不一样的,IE中是绝对路径,FireFox中只是文件名。
  3. String getFieldName()
    getFieldName方法用于返回表单标项name属性的值。如上例中<input type="text" name="column" />中name的value属性值。
  4. void write(File file)
    write方法用于将FileItem对象中保存的主体内容保存到某个指定的文件中。如果FileItem对象中的主体内容是保存在某个临时文件中,该方法顺利完成后,临时文件有可能会被清除。该方法也可将普通表单字段内容写入到一个文件中,但它主要用途是将上传的文件内容保存在本地文件系统中。
  5. String getString()
    getString方法用于将FileItem对象中保存的数据流内容以一个字符串返回,它有两个重载的定义形式:
    public java.lang.String getString()
    public java.lang.String getString(java.lang.String encoding)
    throws java.io.UnsupportedEncodingException
    前者使用缺省的字符集编码将主体内容转换成字符串,后者使用参数指定的字符集编码将主体内容转换成字符串。如果在读取普通表单字段元素的内容时出现了中文乱码现象,请调用第二个getString方法,并为之传递正确的字符集编码名称。
  6. String getContentType()
    getContentType** **方法用于获得上传文件的类型,即表单字段元素描述头属性“Content-Type”的值,如“image/jpeg”。如果FileItem类对象对应的是普通表单字段,该方法将返回null。
  7. boolean isInMemory()
    isInMemory方法用来判断FileItem对象封装的数据内容是存储在内存中,还是存储在临时文件中,如果存储在内存中则返回true,否则返回false。
    8.void delete()
    delete方法用来清空FileItem类对象中存放的主体内容,如果主体内容被保存在临时文件中,delete方法将删除该临时文件。
    尽管当FileItem对象被垃圾收集器收集时会自动清除临时文件,但及时调用delete方法可以更早的清除临时文件,释放系统存储资源。另外,当系统出现异常时,仍有可能造成有的临时文件被永久保存在了硬盘中。
  8. InputStream getInputStream()
    以流的形式返回上传文件的数据内容。很有用。
  9. long getSize()
    返回该上传文件的大小(以字节为单位)。

例:文件上传的简单应用。

------结合上面讲的解析应用,因为没有解析request请求,在带enctype="multipart/form-data"的表单提交的请求,在后台服务器中用 req.getParameter()获取不了提交的内容。
1.index.jsp 提交请求

<body>
<form action="${pageContext.request.contextPath}/UploadHandleServlet" enctype="multipart/form-data" method="post">
选择上传的文件:<input type="file" name="file1"><br/> <!-- 文件域表单项 -->
上传文件的描述:<input type="text" name="desc"><br/>  <!-- 普通表单项 -->
<input type="submit" value="提交">
</form>
</body>

2.package cn.ybzy.upload.UploadHandleServlet 执行上传
参考最上面的代码,此处不再累述。

二,MVC的形式

总体框架如下图:


image.png

1. 在前一个简单例子的基础上升级, 上传例子编程思路:

11. 显示页批量上传中jsp页面中用js实现, 增加一个附件,删除一个附件,至少保留一个

error.jsp错误页面,显示捕获的异常信息,捕获异常在控制类中实现,再转发到此。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery-3.1.1.min.js"></script>
<script type="text/javascript">
   var i =2;    //
   $(function(){
       //新增附件案例的点击事件
       $("#addFile").click(function(){
           //this指调 用者,即下面'点击上传文件’按钮,this的爷爷是<tr>,下面代码作用是,选择上传文件和添加描述信息,放到this的前面
           $(this).parent().parent().before(
                   "<tr class=\"file\">"
                    +"<td>请  选 择 上 传的第  "+i+"  个文件:</td>"
                    +"<td><input type=\"file\" name=\"file1\"/></td></tr>"
                    +"<tr class=\"desc\">"
                    +"  <td>请输入第"+i+"个的文件的描述:</td>"
                    +"  <td><input type=\"text\" name=\"desc1\"/>"
                    +"<button id=\"delete"+i+"\">删除</button>"
                    +"</td> </tr>"
                   );
                    i++;
                    alert(i);  //测试当前是第几个
                    //获取当前生成的删除按钮,并删除不想要的新增附件
                    $("#delete"+(i-1)).click(function(){
                      var $tr =$(this).parent().parent();
                      $tr.prev("tr").remove();
                      $tr.remove();
                    //删除了中间的tr节点,我们要对所有的tr的节点,重新排序,不要123456成为13456要成为12345
                       $(".file").each(function(index){
                           var count=index+1;
                           $(this).find("td:first").text("请选择上传的第"+count+"个文件:");
                           $(this).find("td:last input").attr("name","file"+count);
                       });
                     $(".desc").each(function(index){
                           var count=index+1;
                           $(this).find("td:first").text("请输入第"+count+"个的文件的描述:");
                           $(this).find("td:last input").attr("name","desc"+count);
                       });
                   });
       });
       
       
   });
</script>
<style type="text/css">
   /* tr:first-child{
       text-align:right;
   } */
    tr{
      height:45px;
   } 
   table{
        margin-left:30px;
   }
</style>
</head>
<body>
<!-- pageContext.request.contextPath获取绝对路径 -->
<form action="${pageContext.request.contextPath}/upload.up"  enctype="multipart/form-data" method="post">
<table>
<c:if test="${not empty errorMsg}">
            <tr>
                <td colspan="2" style="color: red; font-weight: bolder;" >${errorMsg}</td>
            </tr>
            </c:if>
   <tr class="file">
       <td>请  选 择 上 传的第     1 个文件</td>
       <td><input type="file" name="file1"/></td>
   </tr>
   <tr class="desc">
       <td>请输入第1个文件的描述:</td>
       <td><input type="text" name="desc1"/></td>
   </tr>
   <tr>
   <td><input style="float:right;" type="submit" value="点击上传文件"/></td>
   <td><button type="button" id="addFile" >点击新增加一个附件</button></td>   <!--因此处button包在form中,所以点击它相当于提交,所以要加type属性,声明它只是按钮没有提交功能  -->
   </tr>
</table>
</form>
<br>
    <br>
    <br>
    <br> 已经上传的文件:<br><br>
    <table border="1" cellpadding="0" cellspacing="0">
        <tr>
            <td>id</td>
            <td>源文件名</td>
            <td>大小</td>
            <td>描述</td>
            <td>上传日期</td>
            <td>删除/下载</td>
        </tr>
        <c:forEach var="uf" items="${upfiles}">
            <tr>
                <td>${uf.id }</td>
                <td>${uf.oldFileName }</td>
                <td>${uf.fileSize}B</td>
                <td>${uf.desc }</td>
                <td>${uf.saveTime }</td>
                <td><a href="${pageContext.request.contextPath}/deleteFile.up?id=${uf.id}">删除</a> | <a href="${pageContext.request.contextPath}/downloadFile.up?id=${uf.id}">下载</a></td>
            </tr>
        </c:forEach>
    </table>
</body>

error.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>异常显示</title>
</head>
<body>
    <c:if test="${not empty errorMsg }">${errorMsg }</c:if>
    <br>
    <br>
    <a href="${pageContext.request.contextPath }/index.up">返回</a>
</body>
</html>

重点:注意一个代码:

<a href="${pageContext.request.contextPath}/deleteFile.up?id=${uf.id}&fp=${fn:replace(uf.savePath,'\\','%5c')}%5c${uf.saveName}">删除</a> 

这里是,请求时,浏览器不识别地址格式中的..反斜杠,所以传的时候,要把\替换成URL编码 %5c.
实际应用中不写&fp={fn:replace(uf.savePath,'\\','%5c')}%5c{uf.saveName},这样会暴露我们的文件,叫别人来攻击。这里我们只要id即可。

12. 对文件扩展名进行验证, 不是所有的文件扩展名都能上传, 具体可以上传的扩展名放到配置文件里; 对单个文件大小判断, 设置一个文件大小上限; 对多个文件上传时设置一个总的文件大小上限;

UploadFileController(HttpServlet) 何存转到它实现--〉 ,UploadFileServiceImpl

@SuppressWarnings("unused")
@WebServlet(urlPatterns = { "*.up" })
public class UploadFileController extends HttpServlet{
   private static final long serialVersionUID = 1L;
   UploadFileService ufs= FactoryService.getUploadFileService();
 @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    @Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        String mn = req.getServletPath();
        mn = mn.substring(1);
        mn = mn.substring(0, mn.length() - 3);
        try {
            Method method = this.getClass().getDeclaredMethod(mn, HttpServletRequest.class, HttpServletResponse.class);
            method.invoke(this, req, resp);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        //resp.getWriter().println(FileUploadPropertiesUtils.getInstance().getProperty("sizeMax"));  //测试:打印接收到一个properties中的一个键的值。
        
   }
    private void index(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<UploadFile> list = ufs.getUploadFiles();
        req.setAttribute("upfiles", list);
        req.getRequestDispatcher("/index.jsp").forward(req, resp);
    }
    
    protected void upload(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //这controller层,接收index.jsp页面发送来的文件信息,文件本身,描述信息
        //保存接收到的文件的工作,不在控制层里实现, 转发到service,实现保存文件
        try {
            ufs.saveFile(req, resp);
            //这里没有抓到异常,上传文件,成功,要到index.up执行,要把保存的上传信息对象放到域空间里(request),转送出去让显示页面获取,显示在页面上
            resp.sendRedirect(req.getContextPath()+"/index.up"); //没句代码,我们上传成功,看到的是空页
        } catch (Exception e) {
            //让服务层去实现保存文件具体业务逻辑的功能代码, 单个文件, 总的文件, 类型
            //这里,获取到异常信息,注入jsp页面, 显示
            //System.out.println("contoller's error:" + e.getMessage());
            req.setAttribute("errorMsg", e.getMessage());
            req.getRequestDispatcher("/error.jsp").forward(req, resp);
        }
    }
    private void deleteFile(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int id = Integer.parseInt(req.getParameter("id"));
        String filePath = req.getParameter("fp");
        System.out.println(filePath);
        ufs.deletUploadFile(id);
        resp.sendRedirect(req.getContextPath()+"/index.up");
    }
    
    private void downloadFile(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取要下载的文件的绝对路径
        int id = Integer.parseInt(req.getParameter("id"));
        UploadFile uf = ufs.getUploadFileById(id);
        String filePath = uf.getSavePath()+"\\"+uf.getSaveName();
        String userAgent = req.getHeader("User-Agent");
        //2.获取要下载的文件名
        String fileName = uf.getOldFileName();
        //针对IE或者以IE为内核的浏览器:主要是解决,下载的时候,文件名是中文乱码
        if (userAgent.contains("MSIE")||userAgent.contains("Trident")) {
            fileName = java.net.URLEncoder.encode(fileName, "UTF-8");
        } else {
        //非IE浏览器的处理:
            fileName = new String(fileName.getBytes("UTF-8"),"ISO-8859-1");
        }
      //3.设置content-disposition响应头控制浏览器以下载的方式打开文件
        resp.setHeader("content-disposition","attachment;filename="+fileName);
        
      //4.获取要下载的文件输入流
        InputStream in = new FileInputStream(filePath);
        int len = 0;
        //5.创建书缓冲区
        byte[] buffer = new byte[1024];
        //6.通过response对象获取OutputStream输出流对象
        OutputStream os = resp.getOutputStream();
        //7.将FileInputStream流对象写入到buffer缓冲区
        while((len=in.read(buffer))>0){
            os.write(buffer,0,len);
        }
        //8.关闭流
        in.close();
        os.close();
    }
    

}

UploadFileServiceImpl,UploadFileService:

public interface UploadFileService {
        //新增
        public void addFileInfo(UploadFile uploadFile);
        //获取到所有上传到服务器上的文件的信息列表
        public List<UploadFile> getUploadFiles();
        //删除信息,根据id
        public void deletUploadFile(int id);
        //保存文件到服务器上的方法,肯定涉及到HTTP,所以参数有req,resp,现在理解到了服务层与Dao层的关系了,服务层不管数据操作,减少耦合
        public void saveFile(HttpServletRequest req,HttpServletResponse resp);
        //删除文件
        public void deleteFile(String savePath);
        //获取一条上传文件的信息用于,显示在下载或删除中。
        public UploadFile getUploadFileById(int id);
}



public class UploadFileServiceImpl implements UploadFileService {
    UploadFileDao uploadFileDao = FactoryDao.getUploadFileDao(); // 减藕
   //下面是获取properties文件中的6个键值对中的值,分别用于设置上传文件大小,存放路径
    private String savePath = FileUploadPropertiesUtils.getInstance().getProperty("savePath");
    private String tempPath = FileUploadPropertiesUtils.getInstance().getProperty("tempPath");
    private String sizeThreshold = FileUploadPropertiesUtils.getInstance().getProperty("sizeThreshold");
    private String sizeMax = FileUploadPropertiesUtils.getInstance().getProperty("sizeMax");
    private String fileSizeMax = FileUploadPropertiesUtils.getInstance().getProperty("fileSizeMax");
    private String fileEx = FileUploadPropertiesUtils.getInstance().getProperty("fileEx");

    @Override
    public void addFileInfo(UploadFile uploadFile) {
        // 把上传来的文件的信息,保存到数据库之前,我们肯定是要先把文件存到服务器上savePath,下面的saveFile()方法就是干这个的
        uploadFileDao.addFileInfo(uploadFile);

    }

    @Override
    public List<UploadFile> getUploadFiles() {
        
        return uploadFileDao.getUploadFiles();
    }

    @Override
    public void deletUploadFile(int id) {
        UploadFile uFile = uploadFileDao.get(id);
        // 先把数据库里的信息删除
        uploadFileDao.deletUploadFile(id);
        //还得把服务器磁盘上的文件删除
        deleteFile(uFile.getSavePath()+"\\" + uFile.getSaveName());

    }

    @Override
    public void saveFile(HttpServletRequest req, HttpServletResponse resp)  {
        // 先把文件保存下来,到服务器指定的目录。
        String savePath = req.getSession().getServletContext().getRealPath(this.savePath); // 保存文件的服务器上的绝对路径,预先创建的(不是操作系统的)
        String tempPath = req.getSession().getServletContext().getRealPath(this.tempPath); // 保存文件的临时目录,动态创建的
        File tempFile = new File(tempPath);
        if (!tempFile.exists()) { // 如果临时目录不存在则创建 之。
            tempFile.mkdirs();
        }
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(Integer.parseInt(this.sizeThreshold)); // 100KB,上传的文件小100KB,放在内存中,大100KB放进tempPath
        factory.setRepository(tempFile);
        // .创建request请求的解析器。
        ServletFileUpload sfu = new ServletFileUpload(factory);
        sfu.setFileSizeMax(Integer.parseInt(this.fileSizeMax)); // 限制上传单个文件的大小在20M以内
        sfu.setHeaderEncoding("UTF-8"); // 防止中文乱码
        sfu.setSizeMax(Integer.parseInt(this.sizeMax)); // 上传所有文件的大小400M
        if(!ServletFileUpload.isMultipartContent(req)) {
            throw new RuntimeException("上传文件的form的编码方式不正确!");                                      //如果不是multipart/form-data数据编码方式,则退出程序
        }
        String desc = "";
        String fileName = "";
        String fileType = "";
        long fileSize = 0;
        String saveFileName = "";
        String realSavePath ="";
        String fileEx1 ="";
        OutputStream out = null;
        InputStream in = null;
        try {
            List<FileItem> filelist = sfu.parseRequest(req);
            if(filelist!=null && filelist.size()>0) {
                for(FileItem fileItem:filelist) {
                    if(fileItem.isFormField()) {   //若是普通表单项,如type="text"
                        desc = fileItem.getString("UTF-8");  //拿到表单中的内容xxxx,如请求时的'请输第n个文件的描述':xxxxx
                        
                        //每一次为desc  赋值 :  代表着一个文件已经上来 , 意味着完成里一次文件的上传操作  
                        //在这里把上传上来的文件的信息,写入数据库里
                        if(!"".equals(fileName)) {
                            UploadFile uf =new UploadFile();
                            uf.setDesc(desc);
                            uf.setFileSize(fileSize+"");
                            uf.setFileType(fileType);
                            uf.setOldFileName(fileName);
                            uf.setSavePath(realSavePath);
                            uf.setSaveName(saveFileName);
                            uf.setSaveTime(new Date());
                            addFileInfo(uf);   //本类的一个方法-保存一个上传来的文件信息到数据库中,最上面
                            
                        }
                    }else {                        //若是文件域表单,则
                        fileName = fileItem.getName();
                        fileType = fileItem.getContentType();
                        fileName = fileName.substring(fileName.lastIndexOf("\\")+1);   //从最后一个'\'查找截取,这样就避免了不同浏览器的格式。
                        fileSize =fileItem.getSize();
                        fileEx1 = fileName.substring(fileName.lastIndexOf(".")+1);  //拿到文件名的后缀(doc,txt,exel...)
                        if(this.fileEx.indexOf(fileEx1)==-1) {     //后缀串(一串以逗号隔开的文件扩展名的字符串)找不到本对象(文件后缀)即不可上传
                            throw new RuntimeException("禁止上传该类型文件");
                        }
                        saveFileName = makeFileName(fileName);            //修改存后的文件名为唯一性,即保证不重名
                        realSavePath = makePath(saveFileName,savePath);   //上传了很多文件,打散放
                        System.out.println("上传文件到达的路径:  "+realSavePath);   //测试用:看看路径拿到否,没有说明程序有问题
                        long starttime = System.currentTimeMillis();  
                       //创建输入输出流 
                        out =new FileOutputStream(realSavePath+"\\"+saveFileName);
                        in = fileItem.getInputStream();                    //拿到文件的输入流--文件流中有文件的数据
                    
                     // 建立缓存区,做一个搬运文件数据流的勺子
                        byte[] buffer = new byte[10240];  //10240本人测试,66m的文件1k缓冲区的话上传花250mills左右,而1m的话只要60mills左右,再大没效果
                        int len =0;
                        while((len=in.read(buffer))>0){  //只要读就会len>0,把文件流(输入流)数据放到buffer中,一下放1024个
                            out.write(buffer,0,len);     //把buffer数据放到要保存的文件中
                            }                            
                            
                
                        long endtime = System.currentTimeMillis();
                        System.out.println("上传文件共花了  :"
                                + (endtime - starttime) + " millis");
                        in.close();
                        out.close();
                    }
                }
            }
          //删除临时目录下临时文件
            File tempd = new File(tempPath);
            for(File file:tempd.listFiles()) {
                file.delete();
            }
        
        } catch (FileUploadBase.SizeLimitExceededException e) {
            throw new RuntimeException("上传文件总大小超出了限制: "+Integer.parseInt(this.sizeMax)/(1024*1024)+"MB!");
        }catch (FileUploadBase.FileSizeLimitExceededException e) {
            throw new RuntimeException("上传单个文件大小超出了限制: "+Integer.parseInt(this.fileSizeMax)/(1024*1024)+"MB!");
        } catch(Exception e) {
            throw new RuntimeException(e.getMessage());
        }finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            
            if(out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    } 

    @Override
    public void deleteFile(String savePath) {
        //删除服务器上的上传文件
                System.out.println("要删除的服务器文件地址:"+savePath);
                File file = new File(savePath);
                if(file.isFile()) {
                    file.delete();
                }

    }
    
    /**
     * 在真正保存文件的目录中创建目录
     * @param saveFileName
     * @param savePath
     * @return
     */
    private String makePath(String saveFileName,String savePath) {
        int hashCode = saveFileName.hashCode();  //哈希码由十进制数据组成
        int dir1= hashCode&0xf;  //dir1的值,这个与运算的结果范围为0-15,即一个目录只存放16个文件
        int dir2 = hashCode&0xf>>4; //这个与运算的结果范围为0-15
        String dir = savePath+"\\"+dir1+"\\"+dir2;   //这样更好,用"\\"后,liniux中运行web服务,会不创建文件夹
        File file =new File(dir);
        if(!file.exists()) {
            file.mkdirs();
            //file.mkdir();//如果你想在已经存在的文件夹(D盘下的yy文件夹)下建立新的文件夹(2019-06-17文件夹),就可以用此方法。此方法不能在不存在的文件夹下建立新的文件夹。假如想建立名字是”2019-06-17”文件夹,那么它的父文件夹必须存在。
        }
        
        return dir;
    }
    
    /**
     * 产生一个唯一文件名UUID+fileName
     * @param fileName
     * @return
     */
    private String makeFileName(String fileName) {
        //uuid
        return UUID.randomUUID().toString()+"_"+fileName;  //确保产生的文件名不重复
    }
    
    @Override
    public UploadFile getUploadFileById(int id) {
        return uploadFileDao.get(id);
    }
}

③验证失败, 要在上传页面中显示错误提示信息! 如:某某文件的扩展名不合法, 某某文件的 大小超过上限xxxM,上传的总文件大小不能超过上限xxxxM;
④通过验证, 则进行真正的上传操作: 给上传的文件起一个不可能重复的新文件名,扩展名不变; 在数据库的文件上传表中记录: 记录id, 文件旧名字, 文件类型, 文件保存位置(包括新文件名D:/upload/2/3/xxxx.doc), 文件的描述信息

13. DAO层--只操作数据库,不涉业务操作。

  1. BaseDao<T>
public class BaseDao<T> {

    QueryRunner queryRunner = new QueryRunner();
    
    private Class<T> clazz;
    
    @SuppressWarnings("unchecked")
    public BaseDao(){
        //用baseDao的构造方法初始化clazz属性,User  User.class
        Type superType = this.getClass().getGenericSuperclass(); // getGenericSuperclass作用是拿到调用者的父类的类型
        if(superType instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) superType;
            Type[] tarry = pt.getActualTypeArguments(); // 返回一个类型数组,第一个元素就是我们要的,T,User.class
            if(tarry[0] instanceof Class) {
                clazz = (Class<T>) tarry[0];
            }
        }
    }
    
    /**
     * 查询数据表,取出sql语句的结果集的第一条数据,封装成一个类的对象返回,不支持事务
     * 用到dbutils工具类
     * null的位置,应该传入BaseDao<T>里边的T的真正用的时候的类型的Class 
     * @param sql
     * @param args
     * @return
     */
    public T get(String sql, Object... args) {
        Connection conn = null;
        T entity = null;
        try {
            // 拿conn
            conn = JdbcUtils.getConnection();
            entity = queryRunner.query(conn, sql, new BeanHandler<T>(clazz), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtils.closeConn(conn);
        }
        return entity;
    }

    /**
     * 查询数据表,取出sql语句的结果集的第一条数据,封装成一个类的对象返回,支持事务
     * 
     * @param sql
     * @param args
     * @return
     */
    public T get(Connection conn,String sql, Object... args) {
        T entity = null;
        try {
            entity = queryRunner.query(conn, sql, new BeanHandler<T>(clazz), args);
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return entity;
    }
    
    /**
     * 获取多条记录的通用方法,通用,用泛型才能实现通用
     * @return
     */
    public List<T> getList(String sql,Object... args){
        Connection conn = null;
        List<T> list = null;
        try {
            // 拿conn
            conn = JdbcUtils.getConnection();
            list = queryRunner.query(conn, sql, new BeanListHandler<>(clazz), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtils.closeConn(conn);
        }
        return list;
    }
    
    
    /**
     * 实现insert , update , delete通用的更新方法
     * @param sql
     * @param args
     * @return
     */
    public int update(String sql,Object... args) {
        Connection conn = null;
        int rows = 0;
        try {
            // 拿conn
            conn = JdbcUtils.getConnection();
            rows = queryRunner.update(conn, sql, args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtils.closeConn(conn);
        }
        return rows;
    }
    
    
    /**
     * 通用的放回sql语句的结果只有一个数值的类型的查询,用户个数. count(id)
     * @param sql
     * @param args
     * @return
     */
    public Object getValue(String sql,Object... args) {
        Connection conn = null;
        Object obj = null;
        try {
            // 拿conn
            conn = JdbcUtils.getConnection();
            obj = queryRunner.query(conn, sql, new ScalarHandler(), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtils.closeConn(conn);
        }
        return obj;
    }
    
  1. public interface UploadFileDao 定义上传文件的数据库操作规则。
import java.util.List;

import cn.ybzy.model.UploadFile;

public interface UploadFileDao {
    //新增
    public void addFileInfo(UploadFile uploadFile);
    //获取到所有上传到服务器上的文件的信息列表
    public List<UploadFile> getUploadFiles();
    //删除信息,根据id
      public void deletUploadFile(int id);
    
    UploadFile get(int id);
}
  1. UploadFileDaoImpl 按规作实现操作,有父类BaseDao。
public class UploadFileDaoImpl extends BaseDao<UploadFile> implements UploadFileDao{

    @Override
    public void addFileInfo(UploadFile uploadFile) {
        //id是自增,不用插入
        String sql ="INSERT INTO `uploadfiles`(`old_file_name`,`file_type`,`file_size`,`save_path`,"
        +"`save_time`,`desc`,`save_name` ) VALUES(?,?,?,?,?,?,?)";
        super.update(sql, uploadFile.getOldFileName(),uploadFile.getFileType(),
                uploadFile.getFileSize(),uploadFile.getSavePath(),uploadFile.getSaveTime(),uploadFile.getDesc()
                ,uploadFile.getSaveName());
    }

    @Override
    public List<UploadFile> getUploadFiles() {
        //查询时,要用到别名,我们要用到字段值,上面的插入不用别名,那自系统自动完成的。
        String sql = "SELECT `id` id,`old_file_name` oldFileName,`file_type` fileType,`file_size` fileSize,`save_path` savePath,`save_time` saveTime,`desc` `desc`,`save_name` saveName FROM `uploadfiles`";
        return super.getList(sql);
    }

    @Override
    public void deletUploadFile(int id) {
        String sql = "DELETE FROM `uploadfiles` WHERE `id`=? ";
        super.update(sql, id);
    }
    
    @Override
    public UploadFile get(int id) {
        String sql = "SELECT `id` id,`old_file_name` oldFileName,`file_type` fileType,`file_size` fileSize,`save_path` savePath,`save_time` saveTime,`desc` `desc`,`save_name` saveName FROM `uploadfiles` WHERE id=?";
        return super.get(sql, id);
    }

}

4.FactoryDao

public class FactoryDao {
    public static UploadFileDao  getUploadFileDao() {
        return new UploadFileDaoImpl();
    }

}

14. 服务层 UploadService,UploadServiceImpl ,FactoryService

前两个类,见 12.
FactoryService

public class FactoryService {
    public static UploadFileService getUploadFileService() {
        return new UploadFileServiceImpl();
    }
}

15. 数据库连接配置文件,库表的创建,属性文件

  1. c3p0-config.xml 连接池配置,放在src下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<c3p0-config>
  <named-config name="mysql"> 
    <!-- 连接myslq数据库的基本必须的信息的配置 -->
    <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
    <property name="jdbcUrl">jdbc:mysql://xiongshaowen.com:3306/uploadweb</property>
    <property name="user">root</property>
    <property name="password">xiong</property>
  
    <!-- 若数据库中的连接数量不足的时候,向数据库申请的连接数量 -->
    <property name="acquireIncrement">5</property>
    <!-- 初始化数据库连接池时连接的数量 -->
    <property name="initialPoolSize">10</property>
    <!-- 数据库连接池中的最小的数据库连接数 -->
    <property name="minPoolSize">5</property>
    <!-- 数据库连接池中的最大的数据库连接数 -->
    <property name="maxPoolSize">100</property>
    <!-- C3P0数据库连接池可以维护的Statement数量 -->
    <property name="maxStatements">2</property> 
    <!-- 每个连接同时可以使用Statement的数量 -->
    <property name="maxStatementsPerConnection">5</property>

  </named-config>
</c3p0-config>

JdbcUtils.java连接池的配置

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import com.mchange.v2.c3p0.ComboPooledDataSource;

/**
 * jdbc工具类
 * @author Administrator
 *
 */
public class JdbcUtils {
    
    //数据库连接池,C3P0
    private static DataSource dataSource = null;
    static { // 静态代码块只会被执行一次
        dataSource = new ComboPooledDataSource("mysql");
    }
    
    /**
     * 获取到数据库mysql的数据连接对象conn
     * @return
     */
    public static Connection getConnection() {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
    
    /**
     * 是通用的关闭数据库连接对象的方法
     * @param conn
     */
    public static void  closeConn(Connection conn) {
        if(conn!=null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    
    public static void rollbackTransation(Connection conn) {
        if(conn!=null) {
            try {
                conn.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    
}

  1. 数据库,uploadwe,表uploadfiles
CREATE DATABASE /*!32312 IF NOT EXISTS*/`uploadweb` /*!40100 DEFAULT CHARACTER SET utf8 */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `uploadweb`;

/*Table structure for table `uploadfiles` */

DROP TABLE IF EXISTS `uploadfiles`;

CREATE TABLE `uploadfiles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `old_file_name` varchar(100) NOT NULL,
  `file_type` varchar(100) NOT NULL,
  `file_size` varchar(100) NOT NULL,
  `save_path` varchar(200) NOT NULL,
  `save_time` timestamp NOT NULL,
  `desc` varchar(200) DEFAULT NULL,
  `save_name` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8;

  1. uploadfile.properties 放在src下
savePath=/WEB-INF/upload
tempPath=/WEB-INF/temp
sizeThreshold=102400
fileSizeMax=109715200
sizeMax=219430400
fileEx=zip,rar,doc,docx,ppt,pptx,txt,mp3,exe,rar,pdf

2.MVC过程中遇到的问题:

SP使用FileUpload上传文件设置setSizeMax后连接被重置


image.png

这个和Tomcat默认设置有关,server.xml中有个重要参数,maxSwallowSize吞吐量默认为2M,改为-1表示无限制

修改前:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
修改后:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" connectionUploadTimeout="36000000"
disableUploadTimeout="false" maxSwallowSize="-1"
redirectPort="8443" />

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容