Skip to main content

Command Palette

Search for a command to run...

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

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

Thomas Schühly’s server-side rendering journey started as a developer trying to make life easier while developing his first bootstrapped product in his free time. Creating Spring ViewComponent enabled him to be the youngest Speaker at the largest European Spring conference and build awesome software full-time with his open-source library at alanda.io. He regularly talks at Java User Groups about htmx and server-side rendering with Spring while contributing to the open-source community. PhotoQuest

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

More from this blog

Thomas Schilling | Spring/HTMX/Claude Code

22 posts

Youngest Speaker @Spring I/O & Spring ViewComponent creator.

Passionate about building awesome software with Spring + HTMX. Pushing full-stack development with Spring forward.

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