Spring Boot文件上传原理与实现详解
1、文件上传
文件上传核心核心要点:
- 文件通过前端表单或者ajax提交,文件上传应该使用enctype="multipart/form-data"标签。
- 前端文件上传是面向多用户的,多用户之间可能存在上传同一个名称、类型的文件;为了避免文件冲突导致的覆盖问题这些应该在后台进行解决!
- 对于文件名称采用UUID、雪花算法、MD5等一些哈希手段确保不会重复;
- 对于用户上传的文件不能让用户轻易的获取到,应该将上传的文件放在一个相对隐秘的或者禁止的路径中。
- 针对不同场景应该限制用户上传文件的类型、大小;
- 后台在处理文件上传的时候应该不应该占用主线程,应该使用异步的形式处理文件上传;主线程继续向下执行代码,异步的优势在于页面不会白屏转圈太久增强用户体验!
2、文件上传简单实现
2.1、编写前端页面
- 文件上传请求类型必须是post请求
- 同时必须是enctype=“multipart/form-data”
- 可以通过accept设置上传文件的类型
- 多文件可以使用ctrl多选,标签中携带上multiple
<!DOCTYPE html> <html lang="en" xml> <head> <meta charset="UTF-8"> <title>文件上传</title> </head> <body> <form method="post" action="/upload" enctype="multipart/form-data"> 单文件: <input type="file" name="headimg"><br/> <hr/> 多文件: <input type="file" name="photos" multiple><br/> <input type="submit" value="上传"> </form> </body> </html>
2.2、Controller层
- 依据上传核心应该使用异步的形式,因此Controller线程中不应该直接对文件处理;而应该将文件交由Service层进行异步处理,Controller线程继续向下执行处理未执行完毕的代码!
- @RequestPart注解用于标注文件上传参数
- MultipartFile参数是一个封装IO流的简易文件处理接口,StandardMultipartFile实现类。
@Controller public class FileController { @Autowired FileUploadService service; @RequestMapping("/upload") @ResponseBody public String upload(@RequestPart MultipartFile headimg, @RequestPart MultipartFile[] photos) throws IOException { System.out.println(" Controller线程: =============== "+Thread.currentThread().getName()+" ==========="); System.out.println("头像大小: " + headimg.getSize()); System.out.println("照片数量: " + photos.length); service.upload(new MultipartFile[]{headimg}); service.upload(photos); return "File Upload Success!"; } }
2.3、Service层异步
- 针对用户上传的文件判断文件是否存在、是否为空之类的东西。
- 由于需要对文件进行哈希避免冲突,因此需要将文件的类型从名称中截取出来、然后另外使用哈希给文件生成一个随机名称并且拼接文件类型!
@Service @EnableAsync public class FileUploadService { @Async public void upload(MultipartFile[] file) throws IOException { System.out.println(" =========================== "+Thread.currentThread().getName()+" ==========="); int length = file.length; if(length > 0){ for(int i = 0;i < length;i++){ // 获取文件的类型 String type = file[i].getOriginalFilename().substring(file[i].getOriginalFilename().lastIndexOf(".")); System.out.println(type); // UUID、雪花算法、MD5等一些哈希算法对文件名进行特殊处理,避免文件重名 String name = UUID.randomUUID().toString(); file[i].transferTo(new File("C:\\Users\\Splay\\Desktop\\上传的文件\\" + name + type)); } } System.out.println("上传完毕!"); } }
2.4、参数配置
springboot可以支持自定义的参数配置,用于限制上传文件的大小。
spring: servlet: multipart: enabled: true max-file-size: 10MB # 单个文件大小 max-request-size: 100MB # 多文件总大小
3、文件上传原理
首先文件上传是通过请求发送出去的,那么肯定在中央调度DispatcherServlet中。
任何数据在网络传输的时候都是01比特串,因此只需要将文件上传与普通参数一同看待即可!
```java protected void doDispatch(HttpServletRequest request, HttpServletResponse response) { // 1. 保存一个额外请求processedRequest HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; // 这里检查是否异步请求 暂时忽略 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { // 2. 检查是不是文件上传的请求 processedRequest = checkMultipart(request); // 3. 判断检查前后请求是否一致 multipartRequestParsed = (processedRequest != request); // 4. 拿到HandlerExecution执行链 mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // 查找适配器HandlerAdapter HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 请求方式解析 String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } // 前置拦截器调用 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 5. 所有参数解析并且执行 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); ... ... // 善后处理 } } }
3.1、整体调度
- 先将请求当做一个普通请求processedRequest,然后checkMultipart(request)检查本次请求是否是文件上传。
- 检查的方式很简单通过StandardServletMultipartResolver类判断form表单中的contentType是否为enctype=“multipart/form-data”。
public class StandardServletMultipartResolver implements MultipartResolver { @Override public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); } } @Override public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { // 返回一个文件上传请求的对象 return new StandardMultipartHttpServletRequest(request, this.resolveLazily); }
- 如果是文件上传那么会将本次请求调用resolveMultipart进行解析一下并且封装成一个新的请求。此时processRequest 一定不等于 request。
- 之后就是拿到HandlerExecution执行链、查找HandlerAdapter适配器、请求方式method解析、调用preHandler前置拦截器做拦截。
3.2、设置与校验
- 即上面执行完毕后,来到ha.handle()方法;所有上面在执行controller时没做的东西都会在这里执行(请求方式验证、参数解析、反射调用controller…)
- 并且在这里会设置一堆的东西,例如:参数解析器(不同注解、类型的参数由不同的解析器)、数据绑定器(DataBinder),之后数据解析与绑定就是交由DataBinder做。
- 再一堆杂七杂八的设置之后来到invokeForRequest方法,拿到参数之后调用doInvoke()反射执行controller。
public class InvocableHandlerMethod extends HandlerMethod { @Nullable public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 参数解析 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } return doInvoke(args); //执行Controller } }
3.3、参数解析大致流程
- 首先要避开一个弯,参数是在调用controller之前解析完毕的
- 不同参数使用不同的参数解析器,这里采用了策略模式,supportsParameter方法中是一个增强for循环;匹配合适的直接丢入map中,在第4步的解析中直接从map中获取!
- 整个方法核心就是不同参数是如何适配到解析器的、参数又是如何解析的。
public class InvocableHandlerMethod extends HandlerMethod { protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception { // 1. 拿到前端上传的所有参数名称 MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } // 2. 参数分配空间 Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } // 3. 参数解析器的适配,不同参数会使用不同解析器 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { // 4. 参数解析 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } .... } return args; } }
3.4、参数解析器的适配
这里只是适配每一个参数的解析器、并不会解析参数;因此缓存池是非常有必要的,下次解析参数就可以直接从缓存池中拿!
@Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // 缓存池便于 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); break; } } } return result; }
- 文件上传参数的解析器的适配是通过RequestPartMethodArgumentResolver类判断的。
- 这里直接判断参数上的注解类型是否为@RequestPart,而参数的信息在之前执行过程中就已经全部拿到了。
- 判断为true之后这个RequestPartMethodArgumentResolver解析器就会被扔到上面的缓存池中便于下次直接获取
public boolean supportsParameter(MethodParameter parameter) { // 直接判断参数上的注解类型是否为@RequestPart if (parameter.hasParameterAnnotation(RequestPart.class)) { return true; } else { if (parameter.hasParameterAnnotation(RequestParam.class)) { return false; } return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional()); } }
3.5、参数解析
由于前面铺垫太多东西,参数解析就变得非常简单了。缓存拿到对应的解析器、然后解析
@Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { // map缓存池拿解析器 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } // 解析文件 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); }
- 这里整体的流程就是先拿到参数注解判断注解中的属性情况,是否required、是否为空…
- 然后resolveMultipartArgument()方法判断是单文件还是多文件上传
- 找到对应的HttpMessageConvert转换器进行对应参数数据到目标参数类型的解析
- 最后将转换器交由DataBinder进行解析与数据绑定。
@Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); Assert.state(servletRequest != null, "No HttpServletRequest"); // 拿到参数注解 RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); // 注解是否必须,且是否为空 boolean isRequired = ((requestPart == null || requestPart.required()) &&!parameter.isOptional()); // 参数名 String name = getPartName(parameter, requestPart); parameter = parameter.nestedIfOptional(); Object arg = null; // 这里判断是否文件上传、并且是单文件还是多文件上传 Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { arg = mpArg; ... HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name); // 拿到convert转换器 arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); if (binderFactory != null) { ... // dataBinder参数解析,这里结束文件就成型了! WebDataBinder binder = binderFactory.createBinder(request, arg, name); .... return adaptArgumentIfNecessary(arg, parameter); }
到此这篇关于Spring Boot文件上传原理与实现详解的文章就介绍到这了,更多相关Spring Boot 文件上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
基于Failed to load ApplicationContext异常的解决思路
这篇文章主要介绍了基于Failed to load ApplicationContext异常的解决思路,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-01-01Java源码解析之SortedMap和NavigableMap
今天带大家来学习Java SortedMap和NavigableMap,文中有非常详细的代码示例,对正在学习java的小伙伴们有很好地帮助,需要的朋友可以参考下2021-05-05
最新评论