如何在Spring MVC中实现HTTP字节范围请求

我的网站上有video倒带的问题。

我找出了http标题的问题。

我当前的控制器方法返回video:

@RequestMapping(method = RequestMethod.GET, value = "/testVideo") @ResponseBody public FileSystemResource testVideo(Principal principal) throws IOException { return new FileSystemResource(new File("D:\\oceans.mp4")); } 

如何用字节范围支持重写以下代码?

PS

我见过以下示例http://balusc.blogspot.in/2009/02/fileservlet-supporting-resume-and.html

但是这段代码看起来很难,我无法理解。 我希望在springmvc存在方式更简单。

支持http字节范围的请求在本答复时已打开,但已在Spring 4.2.RC1中修复。 检查jira SPR-10805或此处的PR。

但根据您在问题中链接的代码,Davin Kevin构建了一个可以满足您的请求的解决方案 。

MultipartFileSender代码:

 import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Created by kevin on 10/02/15. * See full code here : https://github.com/davinkevin/Podcast-Server/blob/d927d9b8cb9ea1268af74316cd20b7192ca92da7/src/main/java/lan/dk/podcastserver/utils/multipart/MultipartFileSender.java */ public class MultipartFileSender { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB. private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week. private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES"; Path filepath; HttpServletRequest request; HttpServletResponse response; public MultipartFileSender() { } public static MultipartFileSender fromPath(Path path) { return new MultipartFileSender().setFilepath(path); } public static MultipartFileSender fromFile(File file) { return new MultipartFileSender().setFilepath(file.toPath()); } public static MultipartFileSender fromURIString(String uri) { return new MultipartFileSender().setFilepath(Paths.get(uri)); } //** internal setter **// private MultipartFileSender setFilepath(Path filepath) { this.filepath = filepath; return this; } public MultipartFileSender with(HttpServletRequest httpRequest) { request = httpRequest; return this; } public MultipartFileSender with(HttpServletResponse httpResponse) { response = httpResponse; return this; } public void serveResource() throws Exception { if (response == null || request == null) { return; } if (!Files.exists(filepath)) { logger.error("File doesn't exist at URI : {}", filepath.toAbsolutePath().toString()); response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } Long length = Files.size(filepath); String fileName = filepath.getFileName().toString(); FileTime lastModifiedObj = Files.getLastModifiedTime(filepath); if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneOffset.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC); String contentType = "video/mp4"; // Validate request headers for caching --------------------------------------------------- // If-None-Match header should contain "*" or ETag. If so, then return 304. String ifNoneMatch = request.getHeader("If-None-Match"); if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) { response.setHeader("ETag", fileName); // Required in 304. response.sendError(HttpServletResponse.SC_NOT_MODIFIED); return; } // If-Modified-Since header should be greater than LastModified. If so, then return 304. // This header is ignored if any If-None-Match header is specified. long ifModifiedSince = request.getDateHeader("If-Modified-Since"); if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) { response.setHeader("ETag", fileName); // Required in 304. response.sendError(HttpServletResponse.SC_NOT_MODIFIED); return; } // Validate request headers for resume ---------------------------------------------------- // If-Match header should contain "*" or ETag. If not, then return 412. String ifMatch = request.getHeader("If-Match"); if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return; } // If-Unmodified-Since header should be greater than LastModified. If not, then return 412. long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since"); if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return; } // Validate and process range ------------------------------------------------------------- // Prepare some variables. The full Range represents the complete file. Range full = new Range(0, length - 1, length); List ranges = new ArrayList<>(); // Validate and process Range and If-Range headers. String range = request.getHeader("Range"); if (range != null) { // Range header should match format "bytes=nn,nn,nn...". If not, then return 416. if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { response.setHeader("Content-Range", "bytes */" + length); // Required in 416. response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } String ifRange = request.getHeader("If-Range"); if (ifRange != null && !ifRange.equals(fileName)) { try { long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid. if (ifRangeTime != -1) { ranges.add(full); } } catch (IllegalArgumentException ignore) { ranges.add(full); } } // If any valid If-Range header, then process each part of byte range. if (ranges.isEmpty()) { for (String part : range.substring(6).split(",")) { // Assuming a file with length of 100, the following examples returns bytes at: // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100). long start = Range.sublong(part, 0, part.indexOf("-")); long end = Range.sublong(part, part.indexOf("-") + 1, part.length()); if (start == -1) { start = length - end; end = length - 1; } else if (end == -1 || end > length - 1) { end = length - 1; } // Check if Range is syntactically valid. If not, then return 416. if (start > end) { response.setHeader("Content-Range", "bytes */" + length); // Required in 416. response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } // Add range. ranges.add(new Range(start, end, length)); } } } // Prepare and initialize response -------------------------------------------------------- // Get content type by file name and set content disposition. String disposition = "inline"; // If content type is unknown, then set the default value. // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp // To add new content types, add new mime-mapping entry in web.xml. if (contentType == null) { contentType = "application/octet-stream"; } else if (!contentType.startsWith("image")) { // Else, expect for images, determine content disposition. If content type is supported by // the browser, then set to inline, else attachment which will pop a 'save as' dialogue. String accept = request.getHeader("Accept"); disposition = accept != null && HttpUtils.accepts(accept, contentType) ? "inline" : "attachment"; } logger.debug("Content-Type : {}", contentType); // Initialize response. response.reset(); response.setBufferSize(DEFAULT_BUFFER_SIZE); response.setHeader("Content-Type", contentType); response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\""); logger.debug("Content-Disposition : {}", disposition); response.setHeader("Accept-Ranges", "bytes"); response.setHeader("ETag", fileName); response.setDateHeader("Last-Modified", lastModified); response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME); // Send requested file (part(s)) to client ------------------------------------------------ // Prepare streams. try (InputStream input = new BufferedInputStream(Files.newInputStream(filepath)); OutputStream output = response.getOutputStream()) { if (ranges.isEmpty() || ranges.get(0) == full) { // Return full file. logger.info("Return full file"); response.setContentType(contentType); response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total); response.setHeader("Content-Length", String.valueOf(full.length)); Range.copy(input, output, length, full.start, full.length); } else if (ranges.size() == 1) { // Return single part of file. Range r = ranges.get(0); logger.info("Return 1 part of file : from ({}) to ({})", r.start, r.end); response.setContentType(contentType); response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total); response.setHeader("Content-Length", String.valueOf(r.length)); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. // Copy single part range. Range.copy(input, output, length, r.start, r.length); } else { // Return multiple parts of file. response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. // Cast back to ServletOutputStream to get the easy println methods. ServletOutputStream sos = (ServletOutputStream) output; // Copy multi part range. for (Range r : ranges) { logger.info("Return multi part of file : from ({}) to ({})", r.start, r.end); // Add multipart boundary and header fields for every range. sos.println(); sos.println("--" + MULTIPART_BOUNDARY); sos.println("Content-Type: " + contentType); sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total); // Copy single part range of multi part range. Range.copy(input, output, length, r.start, r.length); } // End with multipart boundary. sos.println(); sos.println("--" + MULTIPART_BOUNDARY + "--"); } } } private static class Range { long start; long end; long length; long total; /** * Construct a byte range. * @param start Start of the byte range. * @param end End of the byte range. * @param total Total length of the byte source. */ public Range(long start, long end, long total) { this.start = start; this.end = end; this.length = end - start + 1; this.total = total; } public static long sublong(String value, int beginIndex, int endIndex) { String substring = value.substring(beginIndex, endIndex); return (substring.length() > 0) ? Long.parseLong(substring) : -1; } private static void copy(InputStream input, OutputStream output, long inputSize, long start, long length) throws IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int read; if (inputSize == length) { // Write full range. while ((read = input.read(buffer)) > 0) { output.write(buffer, 0, read); output.flush(); } } else { input.skip(start); long toRead = length; while ((read = input.read(buffer)) > 0) { if ((toRead -= read) > 0) { output.write(buffer, 0, read); output.flush(); } else { output.write(buffer, 0, (int) toRead + read); output.flush(); break; } } } } } private static class HttpUtils { /** * Returns true if the given accept header accepts the given value. * @param acceptHeader The accept header. * @param toAccept The value to be accepted. * @return True if the given accept header accepts the given value. */ public static boolean accepts(String acceptHeader, String toAccept) { String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*"); Arrays.sort(acceptValues); return Arrays.binarySearch(acceptValues, toAccept) > -1 || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1 || Arrays.binarySearch(acceptValues, "*/*") > -1; } /** * Returns true if the given match header matches the given value. * @param matchHeader The match header. * @param toMatch The value to be matched. * @return True if the given match header matches the given value. */ public static boolean matches(String matchHeader, String toMatch) { String[] matchValues = matchHeader.split("\\s*,\\s*"); Arrays.sort(matchValues); return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; } } } 

在控制器中使用:

 MultipartFileSender.fromPath(pathtofile) .with(httprequest) .with(httpresponse) .serveResource(); 

如果要避免使用MimeTypeUtils ,可以使用Files.probeContentType(Path path) 。 这将使用系统的默认实现。

根据您要实现的目标,将请求映射到容器的默认servlet可能是有意义的。

即,在初始化期间:

 ServletContext container = ... // Map *.mp4 to the default servlet to support range requests ServletRegistration servletRegistration = container.getServletRegistration("default"); servletRegistration.addMapping("*.mp4"); 

Leandro的答案对我来说非常合适。 我只使用RandomAccessFile更改了InputStream,它可以更好地访问文件中的随机点。

这是MultipartFileSender类

 import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class MultipartFileSender { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB. private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week. private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES"; Path filepath; HttpServletRequest request; HttpServletResponse response; public MultipartFileSender() { } public static MultipartFileSender fromPath(Path path) { return new MultipartFileSender().setFilepath(path); } public static MultipartFileSender fromFile(File file) { return new MultipartFileSender().setFilepath(file.toPath()); } public static MultipartFileSender fromURIString(String uri) { return new MultipartFileSender().setFilepath(Paths.get(uri)); } //** internal setter **// private MultipartFileSender setFilepath(Path filepath) { this.filepath = filepath; return this; } public MultipartFileSender with(HttpServletRequest httpRequest) { request = httpRequest; return this; } public MultipartFileSender with(HttpServletResponse httpResponse) { response = httpResponse; return this; } public void serveResource() throws Exception { if (response == null || request == null) { return; } if (!Files.exists(filepath)) { logger.error("File doesn't exist at URI : {}", filepath.toAbsolutePath().toString()); response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } Long length = Files.size(filepath); String fileName = filepath.getFileName().toString(); FileTime lastModifiedObj = Files.getLastModifiedTime(filepath); if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneOffset.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC); String contentType = "video/mp4"; // Validate request headers for caching --------------------------------------------------- // If-None-Match header should contain "*" or ETag. If so, then return 304. String ifNoneMatch = request.getHeader("If-None-Match"); if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) { response.setHeader("ETag", fileName); // Required in 304. response.sendError(HttpServletResponse.SC_NOT_MODIFIED); return; } // If-Modified-Since header should be greater than LastModified. If so, then return 304. // This header is ignored if any If-None-Match header is specified. long ifModifiedSince = request.getDateHeader("If-Modified-Since"); if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) { response.setHeader("ETag", fileName); // Required in 304. response.sendError(HttpServletResponse.SC_NOT_MODIFIED); return; } // Validate request headers for resume ---------------------------------------------------- // If-Match header should contain "*" or ETag. If not, then return 412. String ifMatch = request.getHeader("If-Match"); if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return; } // If-Unmodified-Since header should be greater than LastModified. If not, then return 412. long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since"); if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return; } // Validate and process range ------------------------------------------------------------- // Prepare some variables. The full Range represents the complete file. Range full = new Range(0, length - 1, length); List ranges = new ArrayList<>(); // Validate and process Range and If-Range headers. String range = request.getHeader("Range"); if (range != null) { // Range header should match format "bytes=nn,nn,nn...". If not, then return 416. if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { response.setHeader("Content-Range", "bytes */" + length); // Required in 416. response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } String ifRange = request.getHeader("If-Range"); if (ifRange != null && !ifRange.equals(fileName)) { try { long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid. if (ifRangeTime != -1) { ranges.add(full); } } catch (IllegalArgumentException ignore) { ranges.add(full); } } // If any valid If-Range header, then process each part of byte range. if (ranges.isEmpty()) { for (String part : range.substring(6).split(",")) { // Assuming a file with length of 100, the following examples returns bytes at: // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100). long start = Range.sublong(part, 0, part.indexOf("-")); long end = Range.sublong(part, part.indexOf("-") + 1, part.length()); if (start == -1) { start = length - end; end = length - 1; } else if (end == -1 || end > length - 1) { end = length - 1; } // Check if Range is syntactically valid. If not, then return 416. if (start > end) { response.setHeader("Content-Range", "bytes */" + length); // Required in 416. response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } // Add range. ranges.add(new Range(start, end, length)); } } } // Prepare and initialize response -------------------------------------------------------- // Get content type by file name and set content disposition. String disposition = "inline"; // If content type is unknown, then set the default value. // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp // To add new content types, add new mime-mapping entry in web.xml. if (contentType == null) { contentType = "application/octet-stream"; } else if (!contentType.startsWith("image")) { // Else, expect for images, determine content disposition. If content type is supported by // the browser, then set to inline, else attachment which will pop a 'save as' dialogue. String accept = request.getHeader("Accept"); disposition = accept != null && HttpUtils.accepts(accept, contentType) ? "inline" : "attachment"; } logger.debug("Content-Type : {}", contentType); // Initialize response. response.reset(); response.setBufferSize(DEFAULT_BUFFER_SIZE); response.setHeader("Content-Type", contentType); response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\""); logger.debug("Content-Disposition : {}", disposition); response.setHeader("Accept-Ranges", "bytes"); response.setHeader("ETag", fileName); response.setDateHeader("Last-Modified", lastModified); response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME); // Send requested file (part(s)) to client ------------------------------------------------ // Prepare streams. try (RandomAccessFile input = new RandomAccessFile(filepath.toFile(), "r"); OutputStream output = response.getOutputStream()) { if (ranges.isEmpty() || ranges.get(0) == full) { // Return full file. logger.info("Return full file"); response.setContentType(contentType); response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total); response.setHeader("Content-Length", String.valueOf(full.length)); Range.copy(input, output, length, full.start, full.length); } else if (ranges.size() == 1) { // Return single part of file. Range r = ranges.get(0); logger.info("Return 1 part of file : from ({}) to ({})", r.start, r.end); response.setContentType(contentType); response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total); response.setHeader("Content-Length", String.valueOf(r.length)); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. // Copy single part range. Range.copy(input, output, length, r.start, r.length); } else { // Return multiple parts of file. response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. // Cast back to ServletOutputStream to get the easy println methods. ServletOutputStream sos = (ServletOutputStream) output; // Copy multi part range. for (Range r : ranges) { logger.info("Return multi part of file : from ({}) to ({})", r.start, r.end); // Add multipart boundary and header fields for every range. sos.println(); sos.println("--" + MULTIPART_BOUNDARY); sos.println("Content-Type: " + contentType); sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total); // Copy single part range of multi part range. Range.copy(input, output, length, r.start, r.length); } // End with multipart boundary. sos.println(); sos.println("--" + MULTIPART_BOUNDARY + "--"); } } } private static class Range { long start; long end; long length; long total; /** * Construct a byte range. * @param start Start of the byte range. * @param end End of the byte range. * @param total Total length of the byte source. */ public Range(long start, long end, long total) { this.start = start; this.end = end; this.length = end - start + 1; this.total = total; } public static long sublong(String value, int beginIndex, int endIndex) { String substring = value.substring(beginIndex, endIndex); return (substring.length() > 0) ? Long.parseLong(substring) : -1; } private static void copy(RandomAccessFile input, OutputStream output, long inputSize, long start, long length) throws IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int read; if (inputSize == length) { // Write full range. while ((read = input.read(buffer)) > 0) { output.write(buffer, 0, read); output.flush(); } } else { input.seek(start); long toRead = length; while ((read = input.read(buffer)) > 0) { if ((toRead -= read) > 0) { output.write(buffer, 0, read); output.flush(); } else { output.write(buffer, 0, (int) toRead + read); output.flush(); break; } } } } } private static class HttpUtils { /** * Returns true if the given accept header accepts the given value. * @param acceptHeader The accept header. * @param toAccept The value to be accepted. * @return True if the given accept header accepts the given value. */ public static boolean accepts(String acceptHeader, String toAccept) { String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*"); Arrays.sort(acceptValues); return Arrays.binarySearch(acceptValues, toAccept) > -1 || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1 || Arrays.binarySearch(acceptValues, "*/*") > -1; } /** * Returns true if the given match header matches the given value. * @param matchHeader The match header. * @param toMatch The value to be matched. * @return True if the given match header matches the given value. */ public static boolean matches(String matchHeader, String toMatch) { String[] matchValues = matchHeader.split("\\s*,\\s*"); Arrays.sort(matchValues); return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; } } } 

哇,Leandro的答案如此睿智。 只需创建服务文件跟随他并在控制器中使用。 在这里我的追随他的指导方针

 @RequestMapping(value = [(SKConstant.URL_PROJECT_API + "/video/{token}")], method = arrayOf(RequestMethod.GET) fun getVideo(@PathVariable("token") token: String, response: HttpServletResponse, request: HttpServletRequest): Any?{ val file = baseService.getByToken(token) if(file != null) { return MultipartFileSender.fromFile(File(fileService.getUploadFileRealPath(file.uniqueName!!))) .with(request) .with(response) .serveResource() } 

试试这个简单的代码。

 @GET @Produces({"application/octet-stream"}) public Response getFile() { File file = ... return Response.ok(file, MediaType.APPLICATION_OCTET_STREAM) .header("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"" ) .build(); } 

更新:

它用Spring MVC包装Spring Jersey jersey.java.net RESTful实现。

对于纯Spring mvc使用下面的代码。

 @RequestMapping("/video") public ResponseEntity getvideo() throws IOException { InputStream in = ... final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); return new ResponseEntityIOUtils.toByteArray(in),headers,HttpStatus.CREATED); } 

IOUtils来自apache普通jar