SpringBoot文件上传原理全过程

1、文件上传

文件上传核心核心要点:

  • 文件通过前端表单或者ajax提交,文件上传应该使用enctype="multipart/form-data"标签。

  • 前端文件上传是面向多用户的,多用户之间可能存在上传同一个名称、类型的文件;为了避免文件冲突导致的覆盖问题这些应该在后台进行解决!

  • 对于文件名称采用UUID、雪花算法、MD5等一些哈希手段确保不会重复;

  • 对于用户上传的文件不能让用户轻易的获取到,应该将上传的文件放在一个相对隐秘的或者禁止的路径中。

  • 针对不同场景应该限制用户上传文件的类型、大小;

  • 后台在处理文件上传的时候应该不应该占用主线程,应该使用异步的形式处理文件上传;主线程继续向下执行代码,异步的优势在于页面不会白屏转圈太久增强用户体验!


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线程中不应该直接对文件处理;而应该将文件交由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、整体调度
  1. 先将请求当做一个普通请求processedRequest,然后checkMultipart(request)检查本次请求是否是文件上传。

  2. 检查的方式很简单通过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);
    }
    
  3. 如果是文件上传那么会将本次请求调用resolveMultipart进行解析一下并且封装成一个新的请求。此时processRequest 一定不等于 request。

  4. 之后就是拿到HandlerExecution执行链、查找HandlerAdapter适配器、请求方式method解析、调用preHandler前置拦截器做拦截。

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、参数解析大致流程
  • 首先要避开一个弯,参数是在调用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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值