Spring Boot with Kotlin, AWS S3, and S3Template

The second article in a series dedicated to Spring Boot AWS S3 integration focused on S3Template and Kotlin.
Image is a featured image for the article about Spring Boot S3Template integration and consist of the Spring Boot logo in the foreground with AWS S3 logo and a blurred photo of a laptop in the background.

Welcome to the second article in a series dedicated to integrating a Spring Boot Kotlin app with AWS S3 Object Storage, in which we will learn how to make our lives easier with S3Template.

Of course, I highly encourage you to take a look at other articles in this series, too:

Video Tutorial

If you prefer video content, then check out my video that covers all three articles:

If you find this content useful, please leave a subscription 🙂

What is S3Template?

Before we get our hands dirty with the code, let’s take a second to understand better with S3Template is and how it can make our lives easier when integrating a Spring Boot app with S3.

Let me quote the S3Template documentation here:

Higher level abstraction over S3Client providing methods for the most common use cases.

So, if you have already worked with, or you have seen my previous article with S3Client, then you saw that simple operations require some boilerplate. And that is exactly what S3Template solves.

And as a note from my end, I just wanted to note S3Template handles only some subset of S3Client operations, so in our projects those two will rather coexist, instead of being each others alternatives.

AWS S3Template Operations

With all of that said, let’s get to work.

Let’s add the controller package and BucketController class to it. We will use it to expose a bunch of endpoints triggering various operations on S3 buckets and files.

When it comes to the operations- we will use the same ones as in the previous article, and as the last one, I will show you how to serialize and deserialize objects with AWS S3 and S3Template.

List All Buckets

Although the S3Template does not expose any method that would help us with this task, I wanted to mention it as we discussed it in the previous tutorial.

Additionally, this is a great example of S3Client and S3Template co-existence:

@RestController
@RequestMapping("/buckets")
class BucketController(
  private val s3Template: S3Template,
  private val s3Client: S3Client,
) {

  @GetMapping
  fun listBuckets(): List<String> {
    val response = s3Client.listBuckets()

    return response.buckets()
      .mapIndexed { index, bucket ->
        "Bucket #${index + 1}: ${bucket.name()}"
      }
  }
}

As we can see, not too much S3Template could improve here, so I bet this is the reason why it was not introduced.

New S3 Bucket

Nextly, let’s take a look at how we can create a brand new bucket:

@PostMapping
fun createBucket(@RequestBody request: BucketRequest) {
  s3Template.createBucket(request.bucketName)
}

data class BucketRequest(val bucketName: String)

As we can see, no additional request classes- the only thing we need is the bucket name.

Of course, let’s rerun our application and verify if everything is working:

curl --location --request POST 'http://localhost:8080/buckets' \
--header 'Content-Type: application/json' \
--data-raw '{
    "bucketName": "codersee-awesome-bucket"
}'

As a result, we should get 200 OK without a response body.

Upload File to S3 Bucket

Nextly, let’s take a look at how to upload a new file to S3.

We have a few options, among which the store function is the easiest:

@PostMapping("/{bucketName}/objects")
fun createObject(@PathVariable bucketName: String, @RequestBody request: ObjectRequest) {
  s3Template.store(bucketName, request.objectName, request.content)
}

data class ObjectRequest(val objectName: String, val content: String)

As we can see, this function takes three arguments:

  • the bucket name,
  • filename,
  • and the content to upload (to be specific Object object)

Alternatively, we could use the upload function, which allows us to send InputStream instance and metadata:

@Override
public S3Resource upload(
  String bucketName, 
  String key, 
  InputStream inputStream,
  @Nullable ObjectMetadata objectMetadata
) 

Lastly, let’s verify that everything is fine with the following curl:

curl --location --request POST 'http://localhost:8080/buckets/codersee-awesome-bucket/objects' \
--header 'Content-Type: application/json' \
--data-raw '{
    "objectName": "file-example.txt",
    "content": "My file content"
}'

If everything worked, then a new file should be present in our bucket 🙂

List Files From The Bucket

Nextly, let’s see how we can list the bucket content with S3Template:

@GetMapping("/{bucketName}/objects")
fun listObjects(@PathVariable bucketName: String): List<String> =
  s3Template.listObjects(bucketName, "")
    .map { s3Resource -> s3Resource.filename }

We can clearly see that this is not rocket science 😉

Nevertheless, it is worth mentioning that this time we get the S3Resource instance and instead of the key() we use it getFilename method.

And just like previously, let’s see the endpoint in action:

curl --location --request POST 'http://localhost:8080/buckets/codersee-awesome-bucket/objects' \
--header 'Content-Type: application/json' \
--data-raw '{
    "objectName": "file-example.txt",
    "content": "My file content"
}'

# Response status: 200 OK
# Response body: 
[
    "file-example.txt"
]

Download a File

So what about fetching files from the S3 bucket?

With S3Template, it’s a piece of cake:

@GetMapping("/{bucketName}/objects/{objectName}")
fun getObject(@PathVariable bucketName: String, @PathVariable objectName: String): String =
  s3Template.download(bucketName, objectName).getContentAsString(UTF_8)

We specify the bucket name and the object name, and as a result, we get the S3Resource that exposes a bunch of methods. Among others, the getContentAsString that is pretty descriptive 😉

Similarly, let’s hit the endpoint:

curl --location --request GET 'http://localhost:8080/buckets/codersee-awesome-bucket/objects/file-example.txt'

# Response status: 200 OK
# Response body: 
"My file content"

Delete the Bucket

Last before least, let’s take a look at how we can get rid of the bucket:

@DeleteMapping("/{bucketName}")
fun deleteBucket(@PathVariable bucketName: String) {
  s3Template.listObjects(bucketName, "")
    .forEach { s3Template.deleteObject(bucketName, it.filename) }

  s3Template.deleteBucket(bucketName)
}

And just like in the previous article- we must ensure the bucket does not contain any objects.

To do so, we list out objects by specifying the bucket name and objects prefix (as we don’t have any, we pass an empty String). Then, for each object, we use its key to delete it. And lastly, we simply invoke the deleteBucket by passing the name of a bucket to delete.

Of course, let’s verify this logic, too:

curl --location --request DELETE 'http://localhost:8080/buckets/codersee-awesome-bucket'

If we run this command and the S3 bucket exists, then we should see 200 OK and our bucket will disappear.

Serialize/Deserialize objects

As the last thing, let’s take a look at how easily we can persist objects using the combination of store and read functions:

@PostMapping("/{bucketName}/objects")
fun createExampleObject(@PathVariable bucketName: String): Example {
  val example = Example(id = UUID.randomUUID(), name = "Some name")

  s3Template.store(bucketName, "example.json", example)

  return s3Template.read(bucketName, "example.json", Example::class.java)
}

data class Example(val id: UUID, val name: String)

As we can see, we made a small update to our POST /{bucketName}/objects endpoint logic.

Basically, the first part is exactly the same, we use the store again to push the file to the bucket.

Nevertheless, instead of the download we saw previously, we use the read function that uses the S3ObjectConverter that will automatically deserialize the JSON into the Example class instance.

And for the last time, let’s hit our API:

curl --location --request POST 'http://localhost:8080/buckets/codersee-awesome-bucket/objects' \
--data-raw ''

# Response status: 200 OK 
# Response body: 
{
    "id": "3eacd8a3-48b2-4756-86db-e4c9f4e291da",
    "name": "Some name"
}

And as we can see, the output confirms that everything is working fine.

Summary

And that’s all for this article on how to make our lives easier in Spring Boot with S3Template.

I hope you enjoyed it, and again wanted to invite you to take a look at other content of this series:

Lastly, just wanted to show that you can find the source code in this GitHub repository and that you can join my newsletter to stay up-to-date with Kotlin on the backend.

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy