Spring Content - The better way to save and serve files and images with Spring Boot

Spring Content - The better way to save and serve files and images with Spring Boot

Table Of Contents

Save images directly into the DB

Recently I added a feature to my web app where my customer could upload images and display them inside the app.

At first, I stored them directly into my Postgres Database as ByteArray inside a JPA Entity with a @Lob annotation

@Entity
class Picture(
    val name: String?,
    val type: String?,
    @Lob
    val data: ByteArray
)

I then returned them as Byte64 encoded string inside my JSON API Response.

Issues

It was working fine in my local dev environment, but as soon as I pushed it to prod I encountered an error where my whole JSON structure needed for my frontend wasn't displayed properly. The Byte64 encoded image string just stopped in the middle.

I cloned the prod db locally into a Postgres container and then the issue occured also locally.

HHH000100: Fail-safe cleanup (collections) : org.hibernate.engine.loading.internal.CollectionLoadContext@2555100f<rs=HikariProxyResultSet@1189576917 wrapping org.postgresql.jdbc.PgResultSet@545635a4>

After some 2 am debugging I got a JacksonMapping exception and found the error on stackoverflow.

I thought the solution was too complex and looked for a simpler way.

Learnings

  1. Lessons: Always test with an environment as similar as production. I can recommend testcontainers for easier integration tests.

  2. Lesson: Use a database versioning tool such as liquibase instead of spring.jpa.hibernate.ddl-auto = update.

With h2 the @Lob was correctly saved as bytearray. With postgres the type was assumed to be oid which caused a issue mapping between @Lob and the bytea column type.

This was fixed with the hibernate Annotation

@Type(type = "org.hibernate.type.MaterializedBlobType")

  1. Lesson: Don't serve images directly as Base64 encoded string.

Instead, use a separate controller and serve the image data raw.

Serialize the entity which stores the data and the metadata.

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
@JsonIdentityReference(alwaysAsId=true)

This avoids huge JSON responses and slowdowns to your website.

Spring Content to the Rescue

Then I discovered spring-content which looked like a better alternative than saving them directly to a db, as I could switch between db, filesystem, AWS S3, and MongoDB GridFs by just switching the dependency.

To use Spring Content you just need to add the dependency in your build.gradle.kts

implementation("com.github.paulcwarren:content-jpa-spring-boot-starter:1.2.5")

Add the @ContentId annotation to your file entity. In my Case I replaced:

    @JsonIgnore
    @Type(type = "org.hibernate.type.MaterializedBlobType")
    val data: ByteArray

with

@ContentId var imageId: String

Create a ContentStore

interface PictureContentStore : ContentStore<Picture, String>

In your method where you save the files you need to use the contentstore.setContent() method. It is important to first save the entity.

val picture = pictureRepository.save(Picture(pictureFile.originalFilename, pictureFile.contentType,null))
pictureContentStore.setContent(picture, pictureFile.inputStream)
pictureRepository.save(picture)

Add the handling of pictures to your file entity

@RestController
@RequestMapping("api/picture")
class PictureController(
    val pictureService: PictureService,
    val pictureContentStore: PictureContentStore,
) {
    @GetMapping("/{id}")
    fun getById(
        @PathVariable id: Long,
    ): ResponseEntity<InputStreamResource> {
        pictureService.getById(id)
            ?.let { pic ->
                val content = pictureContentStore.getContent(pic)
                val inputStreamResource = InputStreamResource(content)

                return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(pic.type.toString()))
                    .body(inputStreamResource)
            }
        throw NoSuchElementException("There is no Image with the corresponding id")
    }
}

and you are done.

You can look up my source code on Github.

This is my first project as a freelancer. Any suggestions or feedback is very welcome
If you want to learn more about HTMX + Spring Boot check out my series Web development without the JavaScript headache with Spring + HTMX.

My side business PhotoQuest is also built with HTMX + JTE