用 Spring Boot 接收文件上传:表单、存储、下载与测试边界

原作者:Spring 团队;中文翻译整理:未完纪。原文及许可证见文末。

六步示意:用户选择文件,经 multipart POST 到控制器交给存储层,再重定向到文件列表并通过下载接口返回文件;安全边界需覆盖全程。
multipart 请求、StorageService、重定向与下载接口的关系(自绘示意图,非截图。)

文件上传的服务端工作不止是接收字节。浏览器需要一个 multipart 表单,控制器需要把请求交给存储层,用户还需要查看已上传文件并下载它们。Spring 官方的 Uploading Files 指南把这些环节放进一个小型 Spring Boot MVC 应用。本稿按完整正文整理,采用 Java 路线展示代码;原文同时提供等价 Kotlin 实现。

版本与安全说明:原页列出 Java 17+、Gradle 7.5+ 或 Maven 3.5+,当前代码使用 spring-boot-starter-webmvc、@MockitoBean 与新的测试包路径。请以下载项目的构建文件和所选 Spring Boot 版本为准,不把不同版本代码拼接。本稿没有构建或运行项目。原例只适合教学,其启动清空目录、同名覆盖和开放下载等行为下文逐项说明。

准备项目,让 Spring Boot 接管 multipart 配置

在 Spring Initializr 选择 Maven 或 Gradle、Java,添加 Spring Web 与 Thymeleaf,生成并解压项目。也可以下载 官方示例仓库,从 initial 目录开始,完成后对照 complete 目录。原指南将练习预计时间写为约 15 分钟,实际所需时间取决于环境。IDE 集成可替代网页生成步骤。

Thymeleaf starter 用于生成 HTML,Web MVC starter 提供 Servlet Web 应用。Servlet 上传依赖 MultipartConfigElement;Spring Boot 的自动配置会注册对应配置,无须手写 web.xml。入口类的 @SpringBootApplication 组合配置声明、自动配置和组件扫描;扫描范围以入口类所在包及子包为基础。

package com.example.uploadingfiles;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UploadingFilesApplication {
    public static void main(String[] args) {
        SpringApplication.run(UploadingFilesApplication.class, args);
    }
}

用三个路由串起上传、列表与下载

请求 职责
GET / 调用 StorageService.loadAll(),构造下载地址,将 files 放入模型,渲染 uploadForm。
POST / 接收字段名为 file 的 MultipartFile,交给存储服务,再以重定向返回首页。
GET /files/{filename} 取得 Resource,以 Content-Disposition: attachment 返回;找不到文件则返回 404。

下面保留原指南控制器的主要行为,合并导入以便阅读。构造器注入的 StorageService 负责磁盘操作;控制器自身不决定文件保存方式。MvcUriComponentsBuilder 从方法映射构造 URL,避免到处手拼路由。Flash 属性跨一次重定向保留成功提示。

package com.example.uploadingfiles;

import java.util.stream.Collectors;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.example.uploadingfiles.storage.*;

@Controller
public class FileUploadController {
    private final StorageService storageService;
    public FileUploadController(StorageService storageService) {
        this.storageService = storageService;
    }
    @GetMapping("/")
    public String listUploadedFiles(Model model) {
        model.addAttribute("files", storageService.loadAll().map(path ->
            MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
                "serveFile", path.getFileName().toString()).build().toUri().toString()
        ).collect(Collectors.toList()));
        return "uploadForm";
    }
    @GetMapping("/files/{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
        Resource file = storageService.loadAsResource(filename);
        if (file == null) return ResponseEntity.notFound().build();
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + file.getFilename() + "\"").body(file);
    }
    @PostMapping("/")
    public String handleFileUpload(@RequestParam("file") MultipartFile file,
                                  RedirectAttributes attributes) {
        storageService.store(file);
        attributes.addFlashAttribute("message",
            "You successfully uploaded " + file.getOriginalFilename() + "!");
        return "redirect:/";
    }
    @ExceptionHandler(StorageFileNotFoundException.class)
    public ResponseEntity<?> handleMissing(StorageFileNotFoundException exc) {
        return ResponseEntity.notFound().build();
    }
}

原示例没有身份认证、文件所有者判断或配额管理,任何能访问接口的人都可能上传、列举和下载文件。attachment 只控制下载呈现方式,不能替代访问控制。原代码手拼下载文件名响应头;生产实现应使用框架的 ContentDisposition 构造器,并清理控制字符。

存储接口与本地文件系统实现

控制器依赖的接口位于 com.example.uploadingfiles.storage。它包含初始化、保存、列举、路径解析、资源加载和清空六项操作。

package com.example.uploadingfiles.storage;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface StorageService {
    void init();
    void store(MultipartFile file);
    Stream<Path> loadAll();
    Path load(String filename);
    Resource loadAsResource(String filename);
    void deleteAll();
}

StorageProperties 用 @ConfigurationProperties("storage") 绑定配置,默认目录为 upload-dir;字段提供 getter/setter,因此可用 storage.location 改写。StorageException 继承 RuntimeException,提供 message 及 message/cause 两个构造器;StorageFileNotFoundException 再继承它,区分应返回 404 的情况。

@ConfigurationProperties("storage")
public class StorageProperties {
    private String location = "upload-dir";
    public String getLocation() { return location; }
    public void setLocation(String location) { this.location = location; }
}

官方 FileSystemStorageService 使用 @Service 注入。构造器拒绝空白 location,并保存 Paths.get(properties.getLocation())。保存文件时先拒绝空文件,将客户端文件名与根目录组合,再规范化为绝对路径;仅当目标的父目录等于配置的根目录绝对路径时才复制。关键代码如下,保持原逻辑:

if (file.isEmpty()) {
    throw new StorageException("Failed to store empty file.");
}
Path destinationFile = this.rootLocation.resolve(
    Paths.get(file.getOriginalFilename())).normalize().toAbsolutePath();
if (!destinationFile.getParent().equals(this.rootLocation.toAbsolutePath())) {
    throw new StorageException("Cannot store file outside current directory.");
}
try (InputStream inputStream = file.getInputStream()) {
    Files.copy(inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING);
}

这个父目录检查会拦截常见的上传路径穿越,但不能据此宣布整个服务不存在路径漏洞。原 Java 版未显式处理空文件名;Kotlin 版检查了 originalFilename 是否为 null。REPLACE_EXISTING 允许覆盖同名文件。目录由其他不可信主体写入时还要考虑符号链接与竞态。原实现的 load(filename) 直接返回 rootLocation.resolve(filename),没有复用上传路径检查;下载路径也必须独立验证,不能依赖浏览器或路由层一定过滤所有输入。

其余方法是这个实现的重要部分:loadAll() 用 Files.walk(rootLocation, 1) 只列一层、排除根目录本身,再转成相对路径;返回 Stream 的使用者需要安排关闭。loadAsResource() 把路径转为 UrlResource,原文以 exists() || isReadable() 判断是否返回资源;更明确的服务应确认文件存在、可读且为允许下载的常规文件。init() 使用 Files.createDirectories;deleteAll() 使用递归删除。I/O 错误包装为 StorageException,资源不存在或 URL 无效包装为 StorageFileNotFoundException。

原文建议生产环境根据需求使用独立的暂存位置、数据库或如 GridFS 一类存储,而不是持续填满应用自身的文件系统。无论换成哪种介质,仍需要服务器生成的存储标识、容量和速率限制、类型与内容检查、恶意文件处置、权限及保留策略。只信任扩展名或客户端 Content-Type 都不充分。

让表单与控制器使用相同字段名

在 src/main/resources/templates/uploadForm.html 放入模板。原模板分为消息、multipart 表单和文件链接列表三块。以下仅把表格布局改成 label 布局,并使用完整闭合标签,行为不变。th:text 按文本输出消息,不把客户端文件名作为 HTML 执行。

<html xmlns:th="https://www.thymeleaf.org">
<body>
  <p th:if="${message}" th:text="${message}"></p>
  <form method="POST" enctype="multipart/form-data" action="/">
    <label>选择文件 <input type="file" name="file"></label>
    <button type="submit">上传</button>
  </form>
  <ul>
    <li th:each="file : ${files}">
      <a th:href="${file}" th:text="${file}"></a>
    </li>
  </ul>
</body>
</html>

限制上传大小,避免启动时丢失内容

原文在 src/main/resources/application.properties 中将单个文件和整个 multipart 请求都限制为 128KB。前者约束一个文件,后者还包含边界及其他字段开销,因此并不保证一个恰好 128KB 的文件能在总请求限制同为 128KB 时通过。实际部署还需要对齐反向代理的请求限制,并设计友好的超限错误响应。

spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB

原文危险行为:指南的 CommandLineRunner 在每次启动时先调用 storageService.deleteAll(),再调用 init(),用于重置教学目录。若 storage.location 指向有用的数据目录,该逻辑会递归删除其中内容。本文不运行这个示例,并采用下面的明确改写:只创建目录,不在应用启动时删除数据。该改动必须同时应用于 Java 或 Kotlin 的入口初始化逻辑。

@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class UploadingFilesApplication {
    public static void main(String[] args) {
        SpringApplication.run(UploadingFilesApplication.class, args);
    }
    @Bean
    CommandLineRunner init(StorageService storageService) {
        // 编辑改写:删除原文的 storageService.deleteAll() 调用。
        return args -> storageService.init();
    }
}

这段入口代码还需要导入 CommandLineRunner、Bean、EnableConfigurationProperties 及两个 storage 类。它只处理初始化风险,没有把整个上传服务改造成生产成品。

运行方式与原文测试究竟证明了什么

在独立练习副本中,官方给出的直接启动命令是 ./gradlew bootRun 或 ./mvnw spring-boot:run。打包分别使用 ./gradlew build 或 ./mvnw clean package,再以 java -jar 运行 build/libs 或 target 中生成的 JAR。原示例 JAR 名为 gs-uploading-files-0.0.1-SNAPSHOT.jar,生成项目的实际名称可能不同。本文仅记录命令,未执行它们。

服务器启动后,原文让读者访问 http://localhost:8080/、选取小文件并提交,预期显示上传成功提示,超限文件则可能得到未经定制的错误页。这是作者说明的预期行为,不是本次实测结果。

控制器测试使用 @SpringBootTest、@AutoConfigureMockMvc 和 @MockitoBean StorageService。MockMvc 无须启动真实 Servlet 容器;MockMultipartFile 用于构造请求。三个测试分别约定:返回 first.txt 与 second.txt 的下载链接;上传后返回 302 与 Location: /,并验证存储服务收到文件;存储服务抛出缺失异常时返回 404。上传测试核心为:

MockMultipartFile multipartFile = new MockMultipartFile(
    "file", "test.txt", "text/plain", "Spring Framework".getBytes());
this.mvc.perform(multipart("/").file(multipartFile))
    .andExpect(status().isFound())
    .andExpect(header().string("Location", "/"));
then(this.storageService).should().store(multipartFile);

因为 StorageService 在这些测试里是 mock,断言并不能证明文件真的安全落盘、权限配置正确或病毒检查有效。原文另指向 FileUploadIntegrationTests 供进一步阅读。本稿没有运行任一测试;上线前应在隔离环境补测重名处理、路径和符号链接、超限上传、缺失文件、认证与授权、磁盘满以及错误恢复。

附:原文 Java 存储类与控制器测试完整清单

为便于逐文件对照,以下保留原文四个存储支持类和 FileUploadTests 全部代码。代码未经执行,存储实现中仍包含前文已指出的风险;其中 deleteAll 定义会递归删除目录,本文入口改写已移除对它的自动调用。不要将本附录视为生产安全实现。

StorageProperties.java

package com.example.uploadingfiles.storage;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("storage")
public class StorageProperties {

  /**
   * Folder location for storing files
   */
  private String location = "upload-dir";

  public String getLocation() {
    return location;
  }

  public void setLocation(String location) {
    this.location = location;
  }

}

StorageException.java

package com.example.uploadingfiles.storage;

public class StorageException extends RuntimeException {

  public StorageException(String message) {
    super(message);
  }

  public StorageException(String message, Throwable cause) {
    super(message, cause);
  }
}

StorageFileNotFoundException.java

package com.example.uploadingfiles.storage;

public class StorageFileNotFoundException extends StorageException {

  public StorageFileNotFoundException(String message) {
    super(message);
  }

  public StorageFileNotFoundException(String message, Throwable cause) {
    super(message, cause);
  }
}

FileSystemStorageService.java

package com.example.uploadingfiles.storage;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

@Service
public class FileSystemStorageService implements StorageService {

  private final Path rootLocation;

  @Autowired
  public FileSystemStorageService(StorageProperties properties) {
        if(properties.getLocation().trim().length() == 0){
            throw new StorageException("File upload location can not be Empty.");
        }

    this.rootLocation = Paths.get(properties.getLocation());
  }

  @Override
  public void store(MultipartFile file) {
    try {
      if (file.isEmpty()) {
        throw new StorageException("Failed to store empty file.");
      }
      Path destinationFile = this.rootLocation.resolve(
          Paths.get(file.getOriginalFilename()))
          .normalize().toAbsolutePath();
      if (!destinationFile.getParent().equals(this.rootLocation.toAbsolutePath())) {
        // This is a security check
        throw new StorageException(
            "Cannot store file outside current directory.");
      }
      try (InputStream inputStream = file.getInputStream()) {
        Files.copy(inputStream, destinationFile,
          StandardCopyOption.REPLACE_EXISTING);
      }
    }
    catch (IOException e) {
      throw new StorageException("Failed to store file.", e);
    }
  }

  @Override
  public Stream<Path> loadAll() {
    try {
      return Files.walk(this.rootLocation, 1)
        .filter(path -> !path.equals(this.rootLocation))
        .map(this.rootLocation::relativize);
    }
    catch (IOException e) {
      throw new StorageException("Failed to read stored files", e);
    }

  }

  @Override
  public Path load(String filename) {
    return rootLocation.resolve(filename);
  }

  @Override
  public Resource loadAsResource(String filename) {
    try {
      Path file = load(filename);
      Resource resource = new UrlResource(file.toUri());
      if (resource.exists() || resource.isReadable()) {
        return resource;
      }
      else {
        throw new StorageFileNotFoundException(
            "Could not read file: " + filename);

      }
    }
    catch (MalformedURLException e) {
      throw new StorageFileNotFoundException("Could not read file: " + filename, e);
    }
  }

  @Override
  public void deleteAll() {
    FileSystemUtils.deleteRecursively(rootLocation.toFile());
  }

  @Override
  public void init() {
    try {
      Files.createDirectories(rootLocation);
    }
    catch (IOException e) {
      throw new StorageException("Could not initialize storage", e);
    }
  }
}

FileUploadTests.java

package com.example.uploadingfiles;

import java.nio.file.Paths;
import java.util.stream.Stream;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.example.uploadingfiles.storage.StorageFileNotFoundException;
import com.example.uploadingfiles.storage.StorageService;

@SpringBootTest
@AutoConfigureMockMvc
public class FileUploadTests {
  @Autowired
  private MockMvc mvc;

  @MockitoBean
  private StorageService storageService;

  @Test
  public void shouldListAllFiles() throws Exception {
    given(this.storageService.loadAll())
        .willReturn(Stream.of(Paths.get("first.txt"), Paths.get("second.txt")));

    this.mvc.perform(get("/")).andExpect(status().isOk())
        .andExpect(model().attribute("files",
            Matchers.contains("http://localhost/files/first.txt",
                "http://localhost/files/second.txt")));
  }

  @Test
  public void shouldSaveUploadedFile() throws Exception {
    MockMultipartFile multipartFile = new MockMultipartFile("file", "test.txt",
        "text/plain", "Spring Framework".getBytes());
    this.mvc.perform(multipart("/").file(multipartFile))
        .andExpect(status().isFound())
        .andExpect(header().string("Location", "/"));
    then(this.storageService).should().store(multipartFile);
  }

  @SuppressWarnings("unchecked")
  @Test
  public void should404WhenMissingFile() throws Exception {
    given(this.storageService.loadAsResource("test.txt"))
        .willThrow(StorageFileNotFoundException.class);

    this.mvc.perform(get("/files/test.txt")).andExpect(status().isNotFound());
  }

}

来源、归属与许可

依据 Spring 团队《Uploading Files》 全文翻译整理;原页无个人署名,核验日期 2026-10-05。官方声明示例代码使用 Apache License 2.0,文字使用 CC BY-ND 3.0。本中文整理及标明的修改依据权利人额外授权,不以 ND 许可证本身主张改编许可。代码改动及风险说明为编辑补充,原作者不对这些改动背书。原页版权声明:Copyright © 2005–2026 Broadcom。

示例代码 Apache License 2.0 完整许可

以下逐字保留 Spring 官方示例仓库 LICENSE.txt。代码归属 Spring 团队;原页版权声明及文字许可见上文。本文已标明控制器导入、模板布局及启动初始化等编辑修改(2026-10-05);代码按 Apache License 2.0 提供,不代表原作者背书。

                                 Apache License
                           Version 2.0, January 2004
                        https://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       https://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容