java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Boot 文件上传

Spring Boot文件上传原理与实现详解

作者:Splaying

这篇文章主要介绍了Spring Boot 文件上传原理与实现详解,前端文件上传是面向多用户的,多用户之间可能存在上传同一个名称、类型的文件;为了避免文件冲突导致的覆盖问题这些应该在后台进行解决,需要的朋友可以参考下

1、文件上传

文件上传核心核心要点:

2、文件上传简单实现

2.1、编写前端页面

  1. 文件上传请求类型必须是post请求
  2. 同时必须是enctype=“multipart/form-data”
  3. 可以通过accept设置上传文件的类型
  4. 多文件可以使用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
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、整体调度

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);
}

3.2、设置与校验

  1. 即上面执行完毕后,来到ha.handle()方法;所有上面在执行controller时没做的东西都会在这里执行(请求方式验证、参数解析、反射调用controller…)
  2. 并且在这里会设置一堆的东西,例如:参数解析器(不同注解、类型的参数由不同的解析器)、数据绑定器(DataBinder),之后数据解析与绑定就是交由DataBinder做。
  3. 再一堆杂七杂八的设置之后来到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、参数解析大致流程

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;
}
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);
}
@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 文件上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文