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

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

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、整体调度

  • 先将请求当做一个普通请求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、设置与校验

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

到此这篇关于Spring Boot文件上传原理与实现详解的文章就介绍到这了,更多相关Spring Boot 文件上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 微信公众平台开发实战Java版之微信获取用户基本信息

    微信公众平台开发实战Java版之微信获取用户基本信息

    这篇文章主要介绍了微信公众平台开发实战Java版之微信获取用户基本信息 的相关资料,需要的朋友可以参考下
    2015-12-12
  • 使用@Value注解从配置文件中读取数组

    使用@Value注解从配置文件中读取数组

    这篇文章主要介绍了使用@Value注解从配置文件中读取数组的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • maven快速生成SpringBoot打包文件的方法步骤

    maven快速生成SpringBoot打包文件的方法步骤

    本文主要介绍了使用Maven快速生成SpringBoot项目打包文件的方法,包括如何生成可执行的JAR文件,如何将配置文件、运行脚本、调试脚本、证书文件等拷贝到指定目录,及如何编译出部署包,这种方法能大大方便微服务的部署,提高部署效率
    2024-10-10
  • 初识Java一些常见的数据类型

    初识Java一些常见的数据类型

    这篇文章主要介绍Java一些常见的数据类型,Java是一种优秀的程序设计语言,它具有令人赏心悦目的语法和易于理解的语义,下面文章小编就来简单介绍为什么说Java是最好的语言并且介绍它的各种常见类型,需要的朋友可以参考一下
    2021-10-10
  • Ribbon负载均衡服务调用的示例详解

    Ribbon负载均衡服务调用的示例详解

    Rbbo其实就是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,这篇文章主要介绍了Ribbon负载均衡服务调用案例代码,需要的朋友可以参考下
    2023-01-01
  • 详解Java 虚拟机垃圾收集机制

    详解Java 虚拟机垃圾收集机制

    这篇文章主要介绍了Java 虚拟机垃圾收集机制的相关资料,帮助大家更好的理解和学习Java虚拟机的相关知识,感兴趣的朋友可以了解下
    2020-12-12
  • java 实现将一个string保存到txt文档中

    java 实现将一个string保存到txt文档中

    今天小编就为大家分享一篇java 实现将一个string保存到txt文档中的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • 基于Failed to load ApplicationContext异常的解决思路

    基于Failed to load ApplicationContext异常的解决思路

    这篇文章主要介绍了基于Failed to load ApplicationContext异常的解决思路,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • Java webSerivce的使用看完你就明白了

    Java webSerivce的使用看完你就明白了

    因为前段时间,需要使用到webService来调用公司的其他系统api接口,但是请求方式和我熟知的http请求不一样,是基于soap协议来传输xml数据格式,请求的参数极其复杂,需要封装多层xml数据格式,并且我不知道对方的api接口是什么语言,甚至不知道他们存在于什么平台
    2022-03-03
  • Java源码解析之SortedMap和NavigableMap

    Java源码解析之SortedMap和NavigableMap

    今天带大家来学习Java SortedMap和NavigableMap,文中有非常详细的代码示例,对正在学习java的小伙伴们有很好地帮助,需要的朋友可以参考下
    2021-05-05

最新评论