# Spring Boot REST API 教程，第 4 部分:在 H2 中存储数据并打包 JAR

> For the complete documentation index, see [llms.txt](https://www.jetbrains.com.cn/help/idea/llms.txt).

* [1：创建并运行 Spring Boot 项目](spring-boot-tutorial-create-project.html)

* [2：添加 REST 端点](spring-boot-tutorial-rest-endpoints.html)

* [3：上传和下载文件](spring-boot-tutorial-file-upload-download.html)

* [4：在 H2 中存储数据并创建 JAR 软件包]()

从 [Spring Boot REST API 教程，第 3 部分：上传和下载文件](spring-boot-tutorial-file-upload-download.html) 继续。 用 H2 数据库替换内存中的映射，以便照片在重启后仍然存在，然后打包应用程序并在 IntelliJ IDEA 外部运行。

Procedure: 配置基于文件的 H2 数据库

创建项目时,已将 H2 和 Spring Data JDBC 添加为项目依赖项。 H2 是一个以 Java 库形式编写的数据库,它在应用程序内部运行。 Spring Data JDBC 将对象映射到数据库行,并为仓库操作提供 SQL。

H2 可以将数据保留在内存中,但这样会在关闭时丢失照片,就像当前的映射一样。 改为将其指向一个文件,然后定义一个表,其列对应照片字段。

1. 在  `application.properties`  中,添加基于文件的数据源并启用架构初始化:

```PROPERTIES
spring.datasource.url=jdbc:h2:file:~/photos-demo-db
spring.sql.init.mode=always

```

`jdbc:h2:file:`  指示 H2 使用本地文件,  `~`  表示主目录,因此  `~/photos-demo-db`  是该文件的路径。

`spring.sql.init.mode=always`  指示 Spring Boot 在启动时运行  `schema.sql` 。

2. 重启应用程序以检查文件是否已创建。 在 Finder 或 Explorer 中打开主目录。

![Finder 主目录中包含 H2 在将数据源 URL 切换为基于文件的存储后创建的 photos-demo-db.mv.db 文件。](https://resources.jetbrains.com.cn/help/img/idea/2026.3/sb_data_source_file.png)

H2 会将  `.mv.db`  追加到你提供的名称后面,因此文件是  `photos-demo-db.mv.db` 。

这一个文件就是整个数据库。 可以复制它并在另一台已安装 Java 的计算机上打开,但不要忘记先停止应用程序。

3. 数据库中还没有表。 Spring Boot 可以在启动时创建它们，并且有多种方式可以实现。 本教程使用一个由 Spring 在启动时运行的 SQL 文件。

在  `src/main/resources`  中,创建一个名为  `schema.sql`  的新文件:

```SQL
create table if not exists photos (
    id bigint generated by default as identity primary key,
    file_name varchar(255),
    content_type varchar(255),
    data blob
);

```

这些列与  `Photo`  上的字段匹配。 数据库生成标识符,  `blob`  存储图像字节。

> **Note:**
> `if not exists`  允许重复启动，但在编辑架构时不会更新现有表。 在本教程中,若要重新创建表,请停止应用程序并从主目录中删除  `photos-demo-db.mv.db` 。 这会移除所有已存储的照片。

4. 若要配置 SQL 辅助功能,请打开  `设置 | 语言 & 框架 | SQL 方言` 。 点击 !["添加"按钮](https://resources.jetbrains.com.cn/help/img/idea/2026.3/schema.sql，以便 IntelliJ IDEA 高亮显示架构中的错误。](https://resources.jetbrains.com.cn/help/img/idea/2026.3/sb_sql_dialect.png)

设置方言后,IntelliJ IDEA 会在输入时高亮显示语句中的错误,包括缺失的分号。

Procedure: 创建 Spring Data JDBC 仓库

架构定义照片的存储位置。 仓库为 Java 代码提供读取和写入它们的操作。 Spring Data 使用模型映射在  `Photo`  和表中的行之间进行转换。

1. 创建一个  `repository`  软件包，放在  `service`  旁边，并添加一个名称为  `PhotosRepository`  的  接口 :

```JAVA
public interface PhotosRepository extends CrudRepository<Photo, Integer> {
}

```

`CrudRepository`  提供用于保存、查找和删除照片的方法。 其类型形参指定模型和标识符类型。 无需编写实现类:Spring 会为此接口创建一个实现,并为这些操作提供 SQL。

2. 在  `Photo`  中,添加映射注解(  `@Table("PHOTOS")`  和  `@Id` ),并将标识符(  `id` )字段、getter 和 setter 更改为  `Integer` 。 保持无实参构造函数和其他访问器不变:

```JAVA
@Table("PHOTOS")
public class Photo {

    @Id
    private Integer id;

    private String fileName;

    private String contentType;

    @JsonIgnore
    private byte[] data;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    // no-argument constructor and the other accessors unchanged

}

```

使用  `org.springframework.data.annotation.Id`  和  `org.springframework.data.relational.core.mapping.Table`  作为导入。

`@Table("PHOTOS")`  与 H2 的大写表名称匹配，  `@Id`  标识主键。 其他字段按名称映射:  `fileName`  变为  `file_name` ,  `contentType`  变为  `content_type` 。

Procedure: 用仓库替换内存中的映射

控制器已经向服务请求照片。 现在更改服务获取它们的位置:每个映射操作都变为一次仓库调用。 当仓库保存新照片时,数据库会生成其标识符,替换之前由服务生成的 UUID。

1. 在  `PhotosService`  中,移除映射字段,并添加仓库字段和构造函数:

```JAVA
@Service
public class PhotosService {

    private final PhotosRepository photosRepository;

    public PhotosService(PhotosRepository photosRepository) {
        this.photosRepository = photosRepository;
    }

}

```

Spring inject 仓库实现，就像 inject 服务到控制器中一样。

2. 将服务方法替换为这些仓库调用:

```JAVA
public Iterable<Photo> get() {
    return photosRepository.findAll();
}

public Photo get(Integer id) {
    return photosRepository.findById(id).orElse(null);
}

public void remove(Integer id) {
    photosRepository.deleteById(id);
}

public Photo save(String fileName, String contentType, byte[] data) {
    Photo photo = new Photo();
    photo.setFileName(fileName);
    photo.setContentType(contentType);
    photo.setData(data);
    return photosRepository.save(photo);
}

```

仓库方法的返回值类型与映射操作不同。  `findAll()`  返回  `Iterable` ,因此服务也公开该类型。  `findById()`  返回  `Optional` ,其中可以包含照片,也可以为空。 调用  `orElse(null)`  可让控制器中现有的缺失照片检查继续工作。

`deleteById()`  不返回任何内容,因此  `remove()`  无法再返回已删除的照片。 下一个过程会根据这些签名更改调整控制器。

`save()`  返回带有数据库生成标识符的已保存照片,因此服务不再分配 UUID。

Procedure: 更新控制器方法

1. 在  `PhotosController`  和  `DownloadController`  中,将每个  `@PathVariable String id`  更改为  `@PathVariable Integer id` 。

2. 在  `PhotosController`  中,更新列表端点以返回  `Iterable` :

```JAVA
@GetMapping("/photos")
public Iterable<Photo> get() {
    return photosService.get();
}

```

响应仍然是 JSON 数组。

3. 更新删除端点,使其调用服务而不检查返回值:

```JAVA
@DeleteMapping("/photos/{id}")
public void delete(@PathVariable Integer id) {
    photosService.remove(id);
}

```

删除缺失的照片现在返回 200 而不是 404,因为仓库删除操作不会报告该照片是否存在。

Procedure: 检查照片在重启后是否仍然存在

上传并列出照片会检查仓库调用。 重启会增加持久性检查:应用程序必须在内存状态消失后从数据库文件中检索照片。

1. 重启应用程序并从  `http://localhost:8080/upload.html`  上传图像。 响应包含由数据库分配的数字标识符。

![上传 ij.png 后显示 JSON 响应的浏览器警报,其中 id 字段显示由 H2 数据库分配的标识符 1。](https://resources.jetbrains.com.cn/help/img/idea/2026.3/sb_photo_with_identifier.png)

2. 停止应用程序,再次启动它,然后打开  `http://localhost:8080/photos` 。 该照片仍然列出,因为它现在位于文件中,而不是位于新进程创建的空映射中。

![应用程序重启后 localhost:8080/photos 的响应仍然列出 id 为 1 的 ij.png,因为它存储在数据库文件中。](https://resources.jetbrains.com.cn/help/img/idea/2026.3/sb_photo_listed.png)

在  `http://localhost:8080/download/<id>`  使用其标识符下载已存储的图像。

Procedure: 将应用程序打包为可执行 JAR

到目前为止,你一直从 IntelliJ IDEA 启动应用程序。 打包会生成一个可执行 JAR，其中包含代码、依赖项和嵌入式 web 服务器。 它可在 IDE 外部使用兼容的 Java 运行时运行,而无需安装单独的应用程序服务器。

1. 在 IntelliJ IDEA 中停止应用程序,以释放端口 8080 和数据库文件。 Maven 构建会运行打开同一数据库的测试,因此如果应用程序仍在运行,可能会因为数据库文件被锁定而导致构建失败。

2. 打开 [Maven](maven.html) 工具窗口，展开  `photos-demo | 生命周期` ，然后双击   package  。 等待构建成功。

IntelliJ IDEA 运行构建,并在 [运行](run-tool-window.html) 工具窗口中报告进度。 留意确认构建成功的行 —  `package`  阶段会先编译代码并运行测试，因此测试失败会在此处停止构建。

![“运行”工具窗口显示 photos-demo package Maven 目标已完成，并通过 BUILD SUCCESS 行确认 JAR 已构建。](https://resources.jetbrains.com.cn/help/img/idea/2026.3/sb_maven_package.png)

3. 在  `target`  目录中找到  `photos-demo-0.0.1-SNAPSHOT.jar` 。

![“项目”工具窗口显示运行 Maven package 后，目标目录中的 photos-demo-0.0.1-SNAPSHOT.jar 及其 .jar.original 文件。](https://resources.jetbrains.com.cn/help/img/idea/2026.3/sb_target_jar.png)

使用这个可执行 JAR；  `.jar.original`  文件是不含捆绑依赖项的原始归档。

4. 在终端中,切换到  `target`  目录并运行:

```BASH
java -jar photos-demo-0.0.1-SNAPSHOT.jar

```

5. 打开  `http://localhost:8080/photos` 。 上传的照片仍然列出,因为此进程使用主目录中的同一个数据库文件。 将 JAR 复制到另一台计算机会转移应用程序,但不会转移此数据库。

## 检查代码

如果应用程序未按预期工作，请将文件与下面的代码进行比较。 未列出  `PhotosDemoApplication` ,因为 Spring Initializr 生成它后它不会再更改。

```JAVA
package com.jetbrains.photos.demo.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Table("PHOTOS")
public class Photo {

    @Id
    private Integer id;

    private String fileName;

    private String contentType;

    @JsonIgnore
    private byte[] data;

    public Photo() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }

}

```

```JAVA
package com.jetbrains.photos.demo.repository;

import com.jetbrains.photos.demo.model.Photo;
import org.springframework.data.repository.CrudRepository;

public interface PhotosRepository extends CrudRepository<Photo, Integer> {
}

```

```JAVA
package com.jetbrains.photos.demo.service;

import com.jetbrains.photos.demo.model.Photo;
import com.jetbrains.photos.demo.repository.PhotosRepository;
import org.springframework.stereotype.Service;

@Service
public class PhotosService {

    private final PhotosRepository photosRepository;

    public PhotosService(PhotosRepository photosRepository) {
        this.photosRepository = photosRepository;
    }

    public Iterable<Photo> get() {
        return photosRepository.findAll();
    }

    public Photo get(Integer id) {
        return photosRepository.findById(id).orElse(null);
    }

    public void remove(Integer id) {
        photosRepository.deleteById(id);
    }

    public Photo save(String fileName, String contentType, byte[] data) {
        Photo photo = new Photo();
        photo.setFileName(fileName);
        photo.setContentType(contentType);
        photo.setData(data);
        return photosRepository.save(photo);
    }

}

```

```JAVA
package com.jetbrains.photos.demo.web;

import com.jetbrains.photos.demo.model.Photo;
import com.jetbrains.photos.demo.service.PhotosService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;

@RestController
public class PhotosController {

    private final PhotosService photosService;

    public PhotosController(PhotosService photosService) {
        this.photosService = photosService;
    }

    @GetMapping("/")
    public String hello() {
        return "Hello World";
    }

    @GetMapping("/photos")
    public Iterable<Photo> get() {
        return photosService.get();
    }

    @GetMapping("/photos/{id}")
    public Photo get(@PathVariable Integer id) {
        Photo photo = photosService.get(id);
        if (photo == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        return photo;
    }

    @DeleteMapping("/photos/{id}")
    public void delete(@PathVariable Integer id) {
        photosService.remove(id);
    }

    @PostMapping("/photos")
    public Photo create(@RequestPart("data") MultipartFile file) throws IOException {
        return photosService.save(file.getOriginalFilename(), file.getContentType(), file.getBytes());
    }

}

```

```JAVA
package com.jetbrains.photos.demo.web;

import com.jetbrains.photos.demo.model.Photo;
import com.jetbrains.photos.demo.service.PhotosService;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
public class DownloadController {

    private final PhotosService photosService;

    public DownloadController(PhotosService photosService) {
        this.photosService = photosService;
    }

    @GetMapping("/download/{id}")
    public ResponseEntity<byte[]> download(@PathVariable Integer id) {
        Photo photo = photosService.get(id);
        if (photo == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf(photo.getContentType()));
        headers.setContentDisposition(
                ContentDisposition.builder("attachment")
                        .filename(photo.getFileName())
                        .build());

        return new ResponseEntity<>(photo.getData(), headers, HttpStatus.OK);
    }

}

```

```PROPERTIES
spring.application.name=photos-demo
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
spring.datasource.url=jdbc:h2:file:~/photos-demo-db
spring.sql.init.mode=always

```

```SQL
create table if not exists photos (
    id bigint generated by default as identity primary key,
    file_name varchar(255),
    content_type varchar(255),
    data blob
);

```

```HTML
<!DOCTYPE html>
<html lang="en">
<head><link rel="canonical" href="https://www.jetbrains.com.cn/help/idea/spring-boot-tutorial-h2-database-jar.html.md/" data-react-helmet="true"/>
    <meta charset="UTF-8">
    <title>Photo Upload</title>
</head>
<body>

<h2>Photo Upload</h2>

<input id="fileupload" type="file" name="fileupload"/>
<button id="upload-button" onclick="uploadFile()">Upload</button>

<script>
    async function uploadFile() {
        let formData = new FormData();
        formData.append("data", fileupload.files[0]);
        await fetch("http://localhost:8080/photos", {
            method: "POST",
            body: formData
        }).then(result => result.text())
            .then(text => alert(text));
    }
</script>
</body>
</html>

```

## 总结和后续步骤

你已经使用 Spring Boot 构建了一个照片共享 REST API。 在本教程的四个部分中,你已:

* 在 IntelliJ IDEA 中 [创建](spring-boot-tutorial-create-project.html#create-project)并 [运行](spring-boot-tutorial-create-project.html#first-run)了 Spring Boot 项目。

* 添加了用于 [列出](spring-boot-tutorial-rest-endpoints.html#return-all-photos)、 [检索](spring-boot-tutorial-rest-endpoints.html#return-one-photo)和 [删除](spring-boot-tutorial-rest-endpoints.html#delete-photo)照片的 REST 端点。

* 通过 [共享服务和构造函数注入](spring-boot-tutorial-rest-endpoints.html#create-service)，将照片存储与请求处理分离。

* 添加了 [文件上传](spring-boot-tutorial-file-upload-download.html#receive-file)和一个 [下载端点](spring-boot-tutorial-file-upload-download.html#serve-image)，该端点返回带有原始文件名称的图像。

* 使用 Spring Data JDBC 将 [照片存储](#rewrite-service)在 H2 中,并 [检查](#check-round-trip)它们在应用程序重启后是否仍然存在。

* 使用 Maven [构建](#build-jar)了可执行 JAR，并在 IDE 外部运行了应用程序。

要继续开展该项目，请在 [测试驱动开发教程](tdd-with-intellij-idea.html)中学习如何编写和运行测试，并使用 [Git 基础教程](working-with-git-tutorial.html)跟踪更改。

有关配置和扩展 Spring Boot 应用程序的更多信息，请参阅 [Spring Boot 文档](https://docs.spring.io/spring-boot/index.html)。

