commons-fileupload框架源码解析(四)--FileItemIterator

  1. commons-fileupload框架源码解析(一)--实例
  2. commons-fileupload框架源码解析(二)--HTTP
  3. commons-fileupload框架源码解析(三)--ParseRequest
  4. commons-fileupload框架源码解析(四)--FileItemIterator
  5. commons-fileupload框架源码解析(五)--MultipartStream
  6. commons-fileupload框架源码解析(六)--ParameterParser
  7. commons-fileupload框架源码解析(七)--FileCleaningTracker
  8. commons-fileupload框架源码解析(八)--DeferredFileOutputStream

前言

从第三篇的解析中,我们知道FileUploadBase是将HttpServletContext的流(流的数据就是主体文本)交给了FileItemIterator解析,将主体文本的参数内容解析封装成方便操作的FileItemStream。现在我们看看FileItemIterator的源码

源码

FileUploadBase.getItemIterator(RequestContext)

public FileItemIterator getItemIterator(RequestContext ctx)
    throws FileUploadException, IOException {
        try {
            return new FileItemIteratorImpl(ctx);
        } catch (FileUploadIOException e) {
            // unwrap encapsulated SizeException
            throw (FileUploadException) e.getCause();
        }
    }

从代码中可以看出FileItemIterator的业务,交给了继承FileItemIterator的UploadFileBase.FileItemIteratorImpl实现进行处理,所以我们再深入到FileItemIteratorImpl中

FileItemIteratorImpl

FileItemIteratorImpl构造方法

    FileItemIteratorImpl(RequestContext ctx)
                throws FileUploadException, IOException {
            if (ctx == null) {
                throw new NullPointerException("ctx parameter");
            }

            String contentType = ctx.getContentType();//获取Http的contentType类型
            if ((null == contentType)
                    || (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {//如果contentType不是multipart的会抛出异常
                throw new InvalidContentTypeException(
                        format("the request doesn't contain a %s or %s stream, content type header is %s",
                               MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
            }

            //这里修复获取Contentlength的代码bug,如果当前RequestContext有继承UploadContext类,就去UploadContext.contextLength的值。
            @SuppressWarnings("deprecation") // still has to be backward compatible
            final int contentLengthInt = ctx.getContentLength();
            final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass())
                                     // Inline conditional is OK here CHECKSTYLE:OFF
                                     ? ((UploadContext) ctx).contentLength()
                                     : contentLengthInt;
                                     // CHECKSTYLE:ON

            InputStream input; // N.B. this is eventually closed in MultipartStream processing
            if (sizeMax >= 0) {
                //检查从Http的消息头contentLength中获取下来的的值是否超过其设置的最大Http字节数sizeMax,超过会抛出异常
                if (requestSize != -1 && requestSize > sizeMax) {
                    throw new SizeLimitExceededException(
                        format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
                                Long.valueOf(requestSize), Long.valueOf(sizeMax)),
                               requestSize, sizeMax);
                }
                // N.B. this is eventually closed in MultipartStream processing
                //装饰者模式,将主体文本字节流和最大Http字节数sizeMax封装到LimitedInputStream中,当读取主体文本字节流的时候,
                // 会检查已被读的字节数是否大于最大Http字节数sizeMax,如果大于,会调用LimitedInputStream的抽象方法raiseError,
                input = new LimitedInputStream(ctx.getInputStream(), sizeMax) {
                    @Override
                    protected void raiseError(long pSizeMax, long pCount)
                            throws IOException {
                        //如果检查到已被读的字节数大于最大Http字节数sizeMax,则抛出异常
                        FileUploadException ex = new SizeLimitExceededException(
                        format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
                                Long.valueOf(pCount), Long.valueOf(pSizeMax)),
                               pCount, pSizeMax);
                        throw new FileUploadIOException(ex);
                    }
                };
            } else {
                input = ctx.getInputStream();//如果没有设置最大Http字节数sizeMax,就直接用HttpSerletRequest的字节流
            }

            //获取设置的编码headerEncoding,如果没有设置,就直接用HttpServletRequest的编码
            String charEncoding = headerEncoding;
            if (charEncoding == null) {
                charEncoding = ctx.getCharacterEncoding();
            }

            //获取Content-Type的分割符的值
            // 如:Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
            // 取:----WebKitFormBoundary7MA4YWxkTrZu0gW
            boundary = getBoundary(contentType);
            if (boundary == null) {//没有分割符的主体文本是不符合multipart类型的HTTP请求的
                IOUtils.closeQuietly(input); // avoid possible resource leak 关闭HttpServletRequest请求流,防止内存溢出
                throw new FileUploadException("the request was rejected because no multipart boundary was found");
            }

            //进度通知器,用于监听多少个字节数被读取,已经完成多少个参数内容的封装,将这些事件回调给ProgressListener
            notifier = new MultipartStream.ProgressNotifier(listener, requestSize);
            try {
                //主体文本读取流,所有主体文本的读取解析封装操作的业务代码都在这个类上
                multi = new MultipartStream(input, boundary, notifier);
            } catch (IllegalArgumentException iae) {
                IOUtils.closeQuietly(input); // avoid possible resource leak
                throw new InvalidContentTypeException(
                        format("The boundary specified in the %s header is too long", CONTENT_TYPE), iae);
            }
            multi.setHeaderEncoding(charEncoding);//设置编码

            skipPreamble = true;
            findNextItem();//查找主体文本的第一个参数内容
        }

总体来说FileItemIteratorImpl的构造方法就是对业务初始化,而解析封装主体文本的参数内容操作需要看FileItemIteratorImpl.findNextItem方法。

findNextItem()

 /**
         * Called for finding the next item, if any.
         *
         * @return True, if an next item was found, otherwise false.
         * @throws IOException An I/O error occurred.
         */
        private boolean findNextItem() throws IOException {
            if (eof) {//是否已经到了最后
                return false;
            }
            if (currentItem != null) {//currentItem指当前已经解析好的一个参数内容
                currentItem.close();//关闭curentItem里面的流
                currentItem = null;
            }
            for (;;) {
                boolean nextPart;//是否存在下一个参数内容
                if (skipPreamble) {
                    nextPart = multi.skipPreamble();//返回是否存在第一条分割线
                } else {
                    nextPart = multi.readBoundary();//返回下一条分割线
                }
                if (!nextPart) {//不存在下一个参数内容
                    if (currentFieldName == null) {//当前参数名也为null时,就认为已经到了主体文本的最后
                        // Outer multipart terminated -> No more data
                        eof = true;
                        return false;
                    }
                    //在currentFieldName不为null,表明当前解析封装的参数内容是multipart/mixed类型,也意味着multi的boundary存放的参数内容中的分割线,
                    //而不是主体文本的分割线,当代码执行到这里时,表示当前multipart/mixed类型参数内容已经处理完了,
                    //这个时候要将multi的boundary设置回主体文本的分割线,继续读取下一个主体文本中参数内容。
                    // Inner multipart terminated -> Return to parsing the outer
                    multi.setBoundary(boundary);
                    currentFieldName = null;
                    continue;
                }
                FileItemHeaders headers = getParsedHeaders(multi.readHeaders());//获取参数内容的消息头
                if (currentFieldName == null) {//currentFieldName只要在发现参数内容的请求类型是multipart/mixed类型时才不为空
                    // We're parsing the outer multipart
                    String fieldName = getFieldName(headers);//从参数内容的消息头中获取字段名
                    if (fieldName != null) {
                        String subContentType = headers.getHeader(CONTENT_TYPE);
                        //判断该参数内容数据的消息头是否属于multipart/mixed类型,
                        //这个种类型会将参数内容分成多块,用该参数类型指定的分割线进行分开。
                        if (subContentType != null
                                &&  subContentType.toLowerCase(Locale.ENGLISH)
                                        .startsWith(MULTIPART_MIXED)) {
                            currentFieldName = fieldName;
                            // Multiple files associated with this field name
                            byte[] subBoundary = getBoundary(subContentType);//获取参数内容里的分割符,附件类型的
                            multi.setBoundary(subBoundary);
                            skipPreamble = true;
                            continue;
                        }
                        String fileName = getFileName(headers);//获取文件名
                        //封装参数内容currentItem,原文件名,参数名,参数内容的请求类型,表单的参数内容,参数内容长度,参数内容的所有消息头
                        currentItem = new FileItemStreamImpl(fileName,
                                fieldName, headers.getHeader(CONTENT_TYPE),
                                fileName == null, getContentLength(headers));
                        currentItem.setHeaders(headers);
                        //通知通知器已完成一个参数内容的封装
                        notifier.noteItem();
                        itemValid = true;//如果itemVaild=false,表示该数据块的解析发生了异常,当解析下一个数据块的时候,会抛出NoSuchElementException异常
                        return true;
                    }
                } else {
                    //封装multipart/mixed类型的参数内容的每一块数据。注意这里的封装后的currentItem的是否是表单的参数内容pFormField属性永远为false。
                    String fileName = getFileName(headers);
                    if (fileName != null) {
                        currentItem = new FileItemStreamImpl(fileName,
                                currentFieldName,
                                headers.getHeader(CONTENT_TYPE),
                                false, getContentLength(headers));
                        currentItem.setHeaders(headers);
                        //通知通知器已完成一个参数内容的封装
                        notifier.noteItem();
                        itemValid = true;
                        return true;
                    }
                }
                //丢弃数据,主要用于调用主体文本中的参数内容之间可能会出现的一些注释信息,这些信息对参数内容的解析封装并没有什么用处,所以选择丢弃
                multi.discardBodyData();
            }
        }

FileItemIteratorImpl.findNextItem总体来说就是查找出下一个参数内容,并将找到的参数内容封装到currentItem中,并返回一个boolean类型,如果为true表示找到了下一个参数内容,否则,表示没有找到。
在FileUploadBase.parseRequest(RequestContext)中,还调用了FileItemIteratorImpl.hasNext()

hasNext()

  @Override
        public boolean hasNext() throws FileUploadException, IOException {
            if (eof) {//如果已经到最后了
                return false;
            }
            if (itemValid) {//当前参数内容currentItem如果有效,返回true
                return true;
            }
            try {
                return findNextItem();//再次解析请求文本题的下一个参数内容            } catch (FileUploadIOException e) {
                // unwrap encapsulated SizeException
                throw (FileUploadException) e.getCause();
            }
        }

从代码上看出FileItemIteratorImpl.hasNext()方法,会从重新调用FileItemIteratorImpl.findNextItem()进行查找封装下一个参数内容。
在FileUploadBase.parseRequest(RequestContext)中,还调用了FileItemIteratorImpl.next()

next()

@Override
        public FileItemStream next() throws FileUploadException, IOException {
            //但结束的时候,或者(参数内容解析封装标记itemValid为false且没有下一个参数内容时,抛出异常
            //值得注意的是,当参数内容解析封装标记itemValid为false,采取去执行hasNext(),而hashNext()会调用findNextItem()
            // 进行对下一个参数内容解析封装到currentItem中。
            if (eof  ||  (!itemValid && !hasNext())) {
                throw new NoSuchElementException();
            }
            itemValid = false;
            return currentItem;//返回带去
        }

FileItemIteratorImpl.next就是获取下一个解析封装好参数内容的FileItemStream,如果不通过hasNext判断之后循环调用next,很容易会因为到了主体文本解析的最后,或者解析封装参数内容,而抛出NoSuchElementException,这个应该也是使用迭代器经常出现的问题。

getParsedHeaders(String)

  protected FileItemHeaders getParsedHeaders(String headerPart) {
        final int len = headerPart.length();
        FileItemHeadersImpl headers = newFileItemHeaders();
        int start = 0;
        for (;;) {
            int end = parseEndOfLine(headerPart, start);//返回消息头后面的回车换行的起始位置
            if (start == end) {
                break;
            }
            StringBuilder header = new StringBuilder(headerPart.substring(start, end));//去掉后面两个回车换行
            //判断是否还存在消息头,如果有,继续调用parseEndOfLine方法,拿到消息头后面的回车换行的起始位置,
            // 再通过字符串截取的出来得到无回车换行的消息头,将其消息头拼装到header变量里,其中header变量的存放的消息头,使用空格分隔
            start = end + 2;
            while (start < len) {
                int nonWs = start;
                while (nonWs < len) {
                    char c = headerPart.charAt(nonWs);
                    if (c != ' '  &&  c != '\t') {
                        break;
                    }
                    ++nonWs;
                }
                if (nonWs == start) {
                    break;
                }
                // Continuation line found
                end = parseEndOfLine(headerPart, nonWs);
                header.append(" ").append(headerPart.substring(nonWs, end));
                start = end + 2;
            }
            parseHeaderLine(headers, header.toString());
        }
        return headers;
    }

要注意一下传过来的参数内容的消息头headerPart会有一些\r\n,如Content-Disposition: form-data; name="file"; filename="1.txt"\r\nContent-Type: text/plain\r\n\r\n。
这里会将这些\r\n去掉再交给parseHeaderLine进行解析封装

parseEndOfLine()

 private int parseEndOfLine(String headerPart, int end) {
        int index = end;
        for (;;) {
            int offset = headerPart.indexOf('\r', index);
            if (offset == -1  ||  offset + 1 >= headerPart.length()) {
                throw new IllegalStateException(
                    "Expected headers to be terminated by an empty line.");
            }
            if (headerPart.charAt(offset + 1) == '\n') {
                return offset;
            }
            index = offset + 1;
        }
    }

返回消息头后面的回车换行的起始位置,没有找到,会抛出异常

parseHeaderLine(FileItemHeadersImpl,String)

   private void parseHeaderLine(FileItemHeadersImpl headers, String header) {
        final int colonOffset = header.indexOf(':');
        if (colonOffset == -1) {
            // This header line is malformed, skip it.
            return;
        }
        String headerName = header.substring(0, colonOffset).trim();
        String headerValue =
            header.substring(header.indexOf(':') + 1).trim();
        headers.addHeader(headerName, headerValue);
    }

通过':'将字符串header分成两个字符串,':'前面的值为消息头键名,':'后面为消息头值,并在键名和值添加到headers封装类中,如果没有从字符串header中的找到':',就不处理

你可能感兴趣的:(commons-fileupload框架源码解析(四)--FileItemIterator)