JSP文件的上传

Java Web 专栏收录该内容
1 篇文章 0 订阅

1. getInputStream()

对于web中简单的文件上传,思路是很清晰的,即:
->客户端使用form中的file选择文件
->提交表单
->服务端通过获取表单数据
->保存文件。
对应到jsp的话,客户端不用管,服务端使用servlet中的request获取表单数据,再将数据通过io流进行输出保存。上代码举例说明:


<form action="Upload" method="post" enctype="multipart/form-data">
    <input type="file" name="upload" value="选择文件" multiple="multiple"> 
    <input type="submit" value="提交">
</form>

首先是注意的是表单的enctype属性,该属性用于设置表单提交数据的编码方式。它有以下3个值。

  • application/x-www-form-urlencoded:这是默认值。主要用于处理少量文本数据的传递。在向服务器发送大量的文本、包含非ASCII字符的文本或二进制数据时这种编码方式效率很低。
  • multipart/form-data:上传二进制数据,只用使用了multipart/form-data,才能完整的传递文件数据,进行上传的操作。
  • text/plain:主要用于向服务器传递大量的文本数据。比较适用于电子邮件的应用。

关于这几种属性,扩展一下。针对multipart/form-data,更多详情参考这里
还有一个地方,input file里面的multiple=”multiple”,这是html5才支持的新功能,部分浏览器可能不兼容,作用是选择文件时支持多选,这样可以直接实现多文件上传。

接下来处理服务端,按照思路,我们只需在servlet里获取数据,request的getInputStream()可以直接读取数据流,然后进行Output,来试一下。

protected void doPost(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException {
        // TODO Auto-generated method stub
        PrintWriter writer=response.getWriter();
        InputStream in=request.getInputStream();
        File file=new File("D://aaa.txt");
        FileOutputStream fout = new FileOutputStream(file);
        byte[] b=new byte[1024]; 
        int n=0;
        while((n=in.read(b))!=-1){
            fout.write(b, 0, n);
        }
        fout.close();
        in.close();
        writer.println("成功");
        writer.close();
    }

我们测试一下,没毛病,但是数据似乎有点问题

原文件:
原文件

上传后的文件:
上传后的文件

由于上传的实现方式是页面表单 + RFC1867规范 + http协议上传,传输过来的字节信息是boundary值+文件相关信息+body+boundary值,由于我上传的文件格式是文本,这些信息能够显示出来,但是要是上传的图片,多出来的这些字节信息可是会导致图片直接挂掉。
挂掉的图片

因为我们只需要body,解决方案是自己写一个处理方法,将这些多余的字节删除, 自己手写了一下,小文件没什么问题,但是使用的内存做文件缓存,不知道文件过大会怎么样。

protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        int dataLength = request.getContentLength(); // 获取文件大小,单位字节
        byte[] b = new byte[dataLength];
        InputStream in = request.getInputStream();
        int off = 0;
        int n = 0;
        byte[] tmp = new byte[8000];
        while ((n = in.read(tmp)) != -1) { // 这里严重注意,我一开始直接就in.read(b)了,但是debug发现后面的数据都为0,经导师指正才明白read()每次不一定读取了所以数据,传过来的大小要根据n来确定,所以在IO里读取字节要while循环读取
            for (int i = 0; i < n; i++) {
                b[off + i] = tmp[i];
            }
            off += n;
        }
        if (off != dataLength) { // 这里的功能是判断读取的实际字节数和文件真实字节数是否一致,若不一致则意味着文件传输过程中数据丢失
            PrintWriter writer = response.getWriter();
            writer.print("<script language=javascript>window.alert('文件传输失败!');window.history.back();</script>");
            writer.close();
            return;
        }
        int begin = bytesIndexOf(b, "\r\n\r\n") + 4;// 根据协议,头信息和正文之间有一个\r\n\r\n,通过这个匹配位置
        String header = getHeader(b, begin, request);
        String fileName = getFileName(header); // 获取文件名
        String contentType = request.getContentType(); // 获取头信息,主要是为了获取boundary值
        String boundaryText = contentType.substring(contentType.lastIndexOf("=") + 1); // 格式:boundary=-------------xxxxxxxx
        int end = bytesLastIndexOf(b, boundaryText) - 4; // 这里我看到的是两个字符,但多次尝试后发现还是得-4,不明白为什么
        FileOutputStream fout = new FileOutputStream(new File("e://" + fileName)); // 文件保存路径
        fout.write(b, begin, end - begin); // 结束位置-开始位置即为body
        fout.close();
        in.close();
    }

    String getHeader(byte[] b, int index, HttpServletRequest request) throws UnsupportedEncodingException {
        String charEncoding = request.getCharacterEncoding();// 获取浏览器编码
        byte[] header = new byte[index + 1];
        System.arraycopy(b, 0, header, 0, index + 1);// 截取头信息的字节数组
        return new String(header, charEncoding);
    }

    String getFileName(String dataString) throws UnsupportedEncodingException {
        int stringBegin = dataString.indexOf("filename=\"") + 10; // 'filename=\'这一段即10个字符,所以+10
        int stringEnd = dataString.indexOf("\"", stringBegin);
        String fileName = dataString.substring(stringBegin, stringEnd);
        int index = fileName.lastIndexOf('\\');// 针对IE浏览器,filename的值为路径+文件名
        if (index != -1)
            fileName = fileName.substring(index + 1);
        return fileName;
    }

    /*
     * byte数组的indexOf,同String的indexOf
     */
    int bytesIndexOf(byte[] b, String s) {
        byte[] mate = s.getBytes();
        for (int i = 0; i < b.length - mate.length; i++) {
            for (int j = 0; j < mate.length && b[i + j] == mate[j]; j++) {
                if (j == mate.length - 1) {
                    return i;
                }
            }
        }
        return -1;
    }

    /*
     * byte数组的lastIndexOf,同String的lastIndexOf
     */
    int bytesLastIndexOf(byte[] b, String s) {
        byte[] mate = s.getBytes();
        for (int i = b.length - mate.length; i >= 0; i--) {
            for (int j = 0; j < mate.length && b[i + j] == mate[j]; j++) {
                if (j == mate.length - 1) {
                    return i;
                }
            }
        }
        return -1;
    }

**编写这段代码可是历经艰辛,迈进了好多坑,一一总结下:

  • 原来学IO流的时候一直不明白,为什么要while((n=in.read(bytes))!=-1)循环读入,原来read并不是根据bytes大小直接读满的,而n是判断读入了多少的关键,所以下一句才是write(bytes,o,n)而不是直接write(bytes)
  • 注意不同浏览器的情况不同,在chrome中filename就是文件名,而IE中filename是文件路径+文件名,在获取文件名的时候要处理
  • 还是不同浏览器的问题,在判断body结束的位置的时候,我一开始只用了chrome,多次测试后发现最后的需要删除的就是46个字节,但是换了IE就不一样了,boundary值每个浏览器都不一样,所以还得先获取contentType取出boundary值来判断位置,不能想当然- -!
  • 一开始我的解决思路的是把这些字节转换成字符串再进行查找位置的操作,但是文件内容很多的话本来byte数组就占了很多内存了再整个String是件很蠢的事情,参考了Commons-FileUpload的源码发现直接在byte数组里面匹配就好了,虽然没用String的indexOf方法但自己写一个也没什么问题。
  • 在获取begin的值的时候,我一开始是仔细debug几次发现那些头信息就四行,于是用了四次indexOf(“\n”)+1,后来老师指正要根据协议来,查找”\r\n\r\n”就可以了,唉~

2. Commons-FileUpload

我采用的是Apache开发的文件上传处理库,Commons-FileUpload。在删除了多余信息的同时,将那些信息中的fieldNamefilenameContent-Type三个参数的值可供调用,方便了文件的保存处理。同时通过一个list集合保存多个文件域,支持多文件上传。
需要用到两个jar包,commons-fileupload和commons-io,自行搜索下载。
注意:这里面的getName()方法若使用chrome没毛病,但是要是用IE的话也会出现上文中提到的获取的是路径+文件名,Apache官网提供了解决方案。链接
Apache

protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        DiskFileItemFactory factory = new DiskFileItemFactory();  //这里首先开一个用于文件缓存的对象
        factory.setRepository(new File("D://apache-tomcat-7.0.72//Test//tmp"));  //缓存文件路径
        factory.setSizeThreshold(1024 * 1024 * 20);  //设置内存大小,文件数据存放到内存中超出这个大小才会生成缓存文件
        ServletFileUpload upload = new ServletFileUpload(factory);  //创建上传文件对象
        List items = null;  //存放多个上传文件的集合
        try {
            items = upload.parseRequest(request);  //获取集合,返回类型为List<FileItem>
        } catch (FileUploadException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        for (int i = 0; i < items.size(); i++) {  //遍历集合
            FileItem item = (FileItem) items.get(i);  
            if (item.isFormField()) {  //判断表单域类型,若不是文件域,则返回true
                /*处理方式略*/
            } else {
                String fieldName = item.getFieldName(); // 获取文件域的表单name
                String fileName = item.getName(); // 获取文件名(后缀也有)
                String contentType = item.getContentType(); // 获取文件类型
                FileOutputStream fos = new FileOutputStream(new File("D://apache-tomcat-7.0.72//Test//" + fileName));  //存放路径
                InputStream in = item.getInputStream();
                byte[] b = new byte[1024];
                int n = 0;
                while ((n = in.read(b)) != -1) {
                    fos.write(b, 0, n);
                }
                fos.close();
                in.close();
            }
        }
    }

选择文件

上传成功


3. Part

在servlet3.0中,加入了对上传表单的支持,Part,它继承于javax.servlet.http,用于接收表单文件(type=”file”),并且要求enctype为multipart/form-data以及method为POST。
其中有个很重要的设置,@MultipartConfig标注,它的属性有:

属性名类型允许为null描述
fileSizeThersholdint当前数据量大于该值时,内容将被写入tmp临时文件。默认空则全部生成临时文件
locationString存放生成文件的地址
maxFileSizelong允许上传的文件最大值,默认为-1,表示没有限制
maxRequestSizelong针对 multipart/form-data 请求的最大数量,默认为-1,表示没有限制

最常用的莫过与location和fileSizeThershold,尤其是location,忒省事了。
Part提供的方法不多,有以下方法:

方法名返回值描述
getContentType()String获取上传文件的文件类型,也就是下图中的text/plain
getHeader(String headerName)String下图中的headerName只有Content-Disposition和Content-Type,根据headerName获取值,其中有很重要的文件名
getHeaderNames()Collection<String>获取所有headerName的字符串集合
getHeaders(String headerName)Collection<String>获取headerName值的集合,这里只有一个,所以这个方法一般用不上
getInputStream()InputStream获取字节流,可以采用传统方式使用IO输出文件,但是这里有更好的方法
getName()String就是下图的name=”upload1”,这是input file的name值
getSize()long获取文件大小,单位为字节
write(String fileName)void将文件保存到标注里location的路径里,以fileName命名文件

参考值:
参考图

下面上代码:

import java.io.IOException;
import java.util.Collection;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

/**
 * Servlet implementation class PartUpload
 */
@MultipartConfig(location = "e:/", fileSizeThreshold = 1024 * 1024 * 10) // 保存到e盘,设置文件大小超过10M就生成缓存文件
@WebServlet("/PartUpload")
public class PartUpload extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        Collection<Part> parts = request.getParts(); // Parts()的功能就是获取一个Part的集合,用于多文件上传
        for (Part part : parts) {
            /**
             * 具体说明见上表 
             * String name=part.getName(); 
             * String contentType=part.getContentType(); 
             * Collection <String> headerNames=part.getHeaderNames(); 
             * Collection <String> headers=part.getHeaders("Content-Disposition"); 
             * long size=part.getSize(); 
             * InputStream in = part.getInputStream();
             */
            String header = part.getHeader("Content-Disposition");
            String fileName = getFileName(header);
            part.write(fileName);
        }
    }

    String getFileName(String header) {
        String fileName = header.substring(header.indexOf("filename=\"") + 10, header.lastIndexOf("\"")); // 截取字符串,获得文件名
        int index = fileName.lastIndexOf('\\');// 针对IE浏览器,filename的值为路径+文件名
        if (index != -1)
            fileName = fileName.substring(index + 1);
        return fileName;
    }
}

Demo参考

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

本代码主要应用的是jsp技术,而实现的文件上传功能,这个功能也是比较常见的,也是比较常用的,更是在网络中比较行的。 技术为创建显示动态生成内容的Web页面提供了一个简捷而快速的方法。JSP技术的设计目的是使得构造基于Web的应用程序更加容易和快捷,而这些应用程序能够与各种Web服务器,应用服务器,浏览器和开发工具共同工作。 Web应用开发的JavaServer Pages技术方法 在开发JSP规范的过程中,太阳微系统公司(Sun Microsystems Inc.)与许许多多主要的Web服务器、应用服务器和开发工具供应商,以及各种各样富有经验的开发团体进行合作。其结果是找到了一种为应用和页面开发人员平衡了可移植性和易用性的开发方法。 JSP技术在多个方面加速了动态Web页面的开发: 将内容的生成和显示进行分离 使用JSP技术,Web页面开发人员可以使用HTML或者XML标识来设计和格式化最终页面。使用JSP标识或者小脚本来生成页面上的动态内容(内容是根据请求来变化的,例如请求帐户信息或者特定的一瓶酒的价格)。生成内容的逻辑被封装在标识和JavaBeans组件中,并且捆绑在小脚本中,所有的脚本在服务器端运行。如果核心逻辑被封装在标识和Beans中,那么其他人,如Web管理人员和页面设计者,能够编辑和使用JSP页面,而不影响内容的生成。 在服务器端,JSP引擎解释JSP标识和小脚本,生成所请求的内容(例如,通过访问JavaBeans组件,使用JDBCTM技术访问数据库,或者包含文件),并且将结果以HTML(或者XML)页面的形式发送回浏览器。这有助于作者保护自己的代码,而又保证任何基于HTML的Web浏览器的完全可用性。 强调可重用的组件 绝大多数JSP页面依赖于可重用的,跨平台的组件(JavaBeans或者Enterprise JavaBeansTM组件)来执行应用程序所要求的更为复杂的处理。开发人员能够共享和交换执行普通操作的组件,或者使得这些组件为更多的使用者或者客户团体所使用。基于组件的方法加速了总体开发过程,并且使得各种组织在他们现有的技能和优化结果的开发努力中得到平衡。 采用标识简化页面开发 Web页面开发人员不会都是熟悉脚本语言的编程人员。JavaServer Page技术封装了许多功能,这些功能是在易用的、与JSP相关的XML标识中进行动态内容生成所需要的。标准的JSP标识能够访问和实例化JavaBeans组件,设置或者检索组件属性,下载Applet,以及执行用其他方法更难于编码和耗时的功能。 通过开发定制化标识库,JSP技术是可以扩展的。今后,第三方开发人员和其他人员可以为常用功能创建自己的标识库。这使得Web页面开发人员能够使用熟悉的工具和如同标识一样的执行特定功能的构件来工作。
参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

96151

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值