【问题标题】:How to serve image files to frontend with Spring Data REST?如何使用 Spring Data REST 将图像文件提供给前端?
【发布时间】:2019-11-02 00:33:16
【问题描述】:

我正在尝试创建一个电影数据库网络应用程序。每部电影都应该有一张海报图片。我不知道如何使用 Spring Data REST 正确地将图像提供给前端。

Movie.java

import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.File;
import java.sql.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String director;
    private Date releaseDate;
    private File posterFile;

    @ManyToMany
    @JoinTable(
            name = "MOVIE_GENRES",
            joinColumns = @JoinColumn(name = "MOVIE_ID"),
            inverseJoinColumns = @JoinColumn(name = "GENRE_ID"))
    private Set<Genre> genres = new HashSet<>();

    @OneToMany
    @MapKeyColumn(name = "ACTOR_ROLE")
    private Map<String, Actor> cast = new HashMap<>();

    public Movie(String title) {
        this.title = title;
    }

    public void addActor(String role, Actor actor) {
        cast.put(role, actor);
    }

    public void removeActor(String role) {
        cast.remove(role);
    }

    public void addGenre(Genre genre) {
        genres.add(genre);
    }

    public void removeGenre(Genre genre) {
        genres.remove(genre);
    }
}

我不能在电影 bean 中使用字节数组,因为它太大而无法保存在数据库中。我可以存储文件对象或路径对象或包含路径的字符串: private File posterFile; 问题是,它将保存像"C:\user\documents\project\backend\images\posterxyz.png" 这样的本地路径。 当我尝试在前端将此路径用作 img-src 时,出现错误“不允许加载本地资源”。我的意思是,无论如何,这听起来像是一种愚蠢的做法。我只是不知道这样做的正确方法是什么。

这是电影资料库。 我在后端使用 Spring Data REST 生成超媒体应用语言格式的 JSON。

MovieRepository.java

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "movies", path = "movies")
public interface MovieRepository extends PagingAndSortingRepository<Movie, Long> {

}

【问题讨论】:

  • 您能解释一下您找到的解决方案吗?您将下面的答案标记为已接受,但它没有解决原始问题,即如何序列化文件?如果将文件对象放入实体中,究竟会发生什么? spring data rest是否只是将文件保存到数据库中?
  • 图像不存储在数据库中,它们位于静态文件夹中,例如:src/main/resources/static/lord-of-the-rings.jpg 我实际上从未真正持久化任何东西,我在应用程序启动时使用 CommandLineRunner 类创建了 Spring 实体并使用内存数据库。所以我不确定 Spring 在调用存储库的保存函数后对文件对象做了什么。

标签: image file vue.js spring-data-jpa spring-data-rest


【解决方案1】:

我愿意:

一个

通过向字段添加@JsonIgnore 注释来防止posterFile 属性被序列化。

@JsonIgnore
private File posterFile;

您也可以通过 Jackson 混合类来执行此操作,以避免使用 Json 处理指令“污染”您的实体,但您需要自己研究。

两个

向资源表示添加自定义链接,允许客户端按需获取图像数据。例如/movies/21/poster

有关如何将自定义链接添加到资源的详细信息,请参见此处:

Spring Data Rest Custom Links on Resource

专门用于创建指向 Spring MVC 控制器的链接:

https://docs.spring.io/spring-hateoas/docs/0.24.0.RELEASE/api/org/springframework/hateoas/mvc/ControllerLinkBuilder.html

https://stackoverflow.com/a/24791083/1356423

三个

创建一个标准 Spring MVC 控制器,该控制器绑定到您的自定义链接指向的路径,该控制器将读取文件数据并流式传输响应。

例如

@Controller
public MoviePosterController{

    @GetMapping(path="/movies/{movieId}/poster")
    //https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web for auto resolution of path var to domain Object
    public @ResponseBody byte[] getPoster(@PathVariable("movieId") Movie movie, HttpServletResponse response){
        File file = movie.getPosterFile();
        //stream the bytes of the file
        // see https://www.baeldung.com/spring-controller-return-image-file
        // see https://www.baeldung.com/spring-mvc-image-media-data
    }
}

【讨论】:

  • 这是一个简单的解决方案,谢谢。我唯一不确定的是如何创建自定义链接。我目前正在这样做,但看起来非常错误:java LinkBuilder link = configuration.entityLinks().linkForSingleResource(Movie.class, resource.getContent().getId()); Link newLink = new Link(link.toString() + "/poster"); 你知道如何以正确的方式做到这一点吗?
  • 我已经用几个链接更新了答案,这些链接展示了如何在没有硬编码的情况下创建到标准 Spring MVC 控制器的链接。
  • 我试过使用ControllerLinkBuilder.linkTo(MoviePosterController.class).withRel("poster"),但这导致http://localhost:8080不在http://localhost:8080/api/movies/1/poster
  • 您知道我如何调整您的解决方案来处理每部电影的一组文件吗?
【解决方案2】:

这对于 Spring Data/REST 来说是不可能的,因为它专注于结构化数据;即大部分的表格和关联。是的,您可以按照其他答案中的说明跳过一些障碍,但还有一个名为 Spring Content 的相关项目可以准确解决这个问题域。

Spring Content 提供与 Spring Data/REST 相同的编程范式,只是针对非结构化数据;即图像、文档、电影等。因此,使用此项目,您可以将一个或多个“内容”对象与 Spring Data 实体相关联,并通过 HTTP 管理它们,就像使用 Spring Data 实体一样。

添加到您的项目非常简单,如下所示:

pom.xml(也可以使用启动器)

   <!-- Java API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-jpa</artifactId>
      <version>0.9.0</version>
   </dependency>
   <!-- REST API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-rest</artifactId>
      <version>0.9.0</version>
   </dependency>

配置

@Configuration
@EnableJpaStores
@Import("org.springframework.content.rest.config.RestConfiguration.class")
public class ContentConfig {

    // schema management (assuming mysql)
    // 
    @Value("/org/springframework/content/jpa/schema-drop-mysql.sql")
    private Resource dropContentTables;

    @Value("/org/springframework/content/jpa/schema-mysql.sql")
    private Resource createContentTables;

    @Bean
    DataSourceInitializer datasourceInitializer() {
        ResourceDatabasePopulator databasePopulator =
                new ResourceDatabasePopulator();

        databasePopulator.addScript(dropContentTables);
        databasePopulator.addScript(createContentTables);
        databasePopulator.setIgnoreFailedDrops(true);

        DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDataSource(dataSource());
        initializer.setDatabasePopulator(databasePopulator);

        return initializer;
    }
}

要关联内容,请将 Spring Content 注释添加到您的 Movie 实体。

电影.java

@Entity
public class Movie {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
    .. existing fields...    
  // private File posterFile; no longer required

  @ContentId
  private String contentId;

  @ContentLength
  private long contentLength = 0L;

  // if you have rest endpoints
  @MimeType
  private String mimeType = "text/plain";
}

创建一个“商店”:

MoviePosterContentStore.java

@StoreRestResource(path="moviePosters")
public interface MoviePosterContentStore extends ContentStore<Movie, String> {
}

这就是创建 REST 端点 @/moviePosters 所需的全部内容。当您的应用程序启动时,Spring Content 将查看 Spring Content JPA 的依赖项,查看您的 MoviePosterContentStore 接口并为 JPA 注入该接口的实现。它还将查看 Spring Content REST 依赖项并注入一个 @Controller 实现,该实现将 HTTP 请求转发到您的 MoviePosterContentStore。这使您不必自己实施任何这些,我认为这就是您所追求的。

所以...

使用注入的 REST API 管理内容:

curl -X POST /moviePosters/{movieId} -F 文件=@/path/to/poster.jpg

将图像存储在数据库中(作为 BLOB)将其与 id 为 movieId 的电影实体相关联。

curl /moviePosters/{movieId} -H "Accept: image/jpeg"

将再次获取它等等...支持所有 CRUD 方法和视频流以及顺便说一句!

有一些入门指南here。 JPA 的参考指南是here。还有一个教程视频here。编码位从大约 1/2 处开始。

另外几点: - 如果您使用 Spring Boot Starters,那么大部分情况下您不需要 @Configuration。
- 就像 Spring Data 是一种抽象一样,Spring Content 也是如此,因此您不限于将海报图像作为 BLOB 存储在数据库中。您可以将它们存储在文件系统或云存储(如 S3)或 Spring Content 支持的任何其他存储中。

HTH

【讨论】:

  • 我尝试使用 spring-content 启动器执行此操作,但在使用 GET:Failed to convert from type [java.lang.String] to type [java.lang.Long] for value '80854cc1-ecc8-4458-b81b-3bc1e13ac006' 调用 "http://localhost:8080/moviePosters/80854cc1-ecc8-4458-b81b-3bc1e13ac006" 时出现错误。
  • at internal.org.springframework.content.rest.controllers.AbstractContentPropertyController.findOne(AbstractContentPropertyController.java:160) ~[spring-content-rest-0.9.0.jar:na] at internal.org.springframework.content.rest.controllers.ContentEntityRestController.getContent(ContentEntityRestController.java:84) ~[spring-content-rest-0.9.0.jar:na]
  • 您是否有可能在 getContent 请求中使用content id,而不是movie id?从堆栈竞争看来,控制器正试图将内容 id guid 转换为 Long(电影 id 的类型)以获取电影。如果没有,请随时在 GitHub 上使用完整的堆栈跟踪提出问题。
  • 假设您按照上面的示例进行操作,那么您只有 1 条内容与每部电影相关联,因此可以通过指定正确的内容类型通过电影 URI 来寻址该内容。我更新了示例以澄清。
【解决方案3】:
@RestController 
// Becareful here, never use @RepositoyRestController.it will be got Error look like NoConverter for.....

@RequiredArgsConstructor
// becase you use  Path rootLocation, don't use @AllArgsContructor
public class StorageController {

    private final StorageRepository storageRepository;

    @Value("${file.upload.path}")
    private Path rootLocation;

    @Bean
    public RepresentationModelProcessor<EntityModel<Storage>> storageProcessor() {
        return new RepresentationModelProcessor<EntityModel<Storage>>() {
            @Override
            public EntityModel<Storage> process(EntityModel<Storage> model) {
                model.add(
                        linkTo(methodOn(StorageController.class).look(model.getContent().getId())).withRel("view")
                );
                return model;
            }
        };
    }

    @GetMapping(path = "storages/{id}/view")
    public ResponseEntity<?> look(@PathVariable final Long id) {
        Storage storage = storageRepository.findById(id).orElseThrow(RuntimeException::new);
        Path path = rootLocation.resolve(storage.getPath());
        Resource resource = null;
        try {
            resource = new UrlResource(path.toUri());
        } catch (MalformedURLException e) {
            throw new RuntimeException("Error!!!!");
        }

        if (resource.exists() || resource.isReadable()) {
            return ResponseEntity
                    .ok()
                    .header(HttpHeaders.CONTENT_TYPE, storage.getMime())
                    .body(resource);
        } else {
            throw new RuntimeException("Error!!!");
        }
    }
}

【讨论】:

  • 请添加一些解释,以便读者更好地理解您的答案。
猜你喜欢
  • 1970-01-01
  • 2019-12-27
  • 2020-02-17
  • 2022-10-06
  • 1970-01-01
  • 2017-11-28
  • 2016-12-26
  • 2012-01-17
  • 1970-01-01
相关资源
最近更新 更多