Create and Connect a Micronaut Application to Oracle Cloud Infrastructure Object Storage
This guide describes how to create a Micronaut application that demonstrates how to store, retrieve, and delete user profile pictures in Oracle Cloud Infrastructure Object Storage using the Micronaut Object Storage API.
The Micronaut Object Storage API provides a uniform API to create, read, and delete objects in the major cloud providers:
- Amazon S3
- Google Cloud Storage
- Oracle Cloud Infrastructure Object Storage
Using this API enables the creation of truly multicloud, portable applications.
Prerequisites #
- JDK 17 or higher. See Setting up Your Desktop.
- An Oracle Cloud Infrastructure account. See Setting up Your Cloud Accounts.
- The Oracle Cloud Infrastructure CLI installed with local access configured.
- An Oracle Cloud Infrastructure compartment with appropriate permission granted to your Oracle Cloud Infrastructure user account to manage the Object Storage family in the compartment.
- The GCN CLI. See Setting up Your Desktop. (Optional.)
Follow the steps below to create the application from scratch. However, you can also download the completed example in Java:
A note regarding your development environment
Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools extension.
Note: If you use IntelliJ IDEA, enable annotation processing.
1. Create the Application #
Create an application using the GCN Launcher.
-
Open the GCN Launcher in advanced mode.
- Create a new project using the following selections. (Alternatively, use these shortcuts for Maven or Gradle.)
- Project Type: Application (Default)
- Project Name: oci-storage-demo
- Base Package: com.example (Default)
- Clouds: OCI
- Language: Java (Default)
- Build Tool: Gradle (Groovy) or Maven
- Test Framework: JUnit (Default)
- Java Version: 17 (Default)
- Micronaut Version: (Default)
- Cloud Services: Object Storage
- Features: GraalVM Native Image (Default)
- Sample Code: Yes (Default)
- Click Generate Project. The GCN Launcher creates an application with the default package
com.example
in a directory named oci-storage-demo. The application ZIP file will be downloaded in your default downloads directory. Unzip it, open in your code editor, and proceed to the next steps.
Alternatively, use the GCN CLI as follows:
gcn create-app com.example.oci-storage-demo \
--clouds=oci \
--services=objectstore \
--features=graalvm \
--build=gradle \
--lang=java
gcn create-app com.example.oci-storage-demo \
--clouds=oci \
--services=objectstore \
--features=graalvm \
--build=maven \
--lang=java
For more information, see Using the GCN CLI.
2. ProfilePicturesController #
The launcher created an interface with the endpoints of the “profile pictures” microservice in a file named lib/src/main/java/com/example/ProfilePicturesApi.java:
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Status;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.micronaut.http.server.types.files.StreamedFile;
import java.util.Optional;
import static io.micronaut.http.HttpStatus.NO_CONTENT;
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
public interface ProfilePicturesApi {
@Post(uri = "/{userId}", consumes = MULTIPART_FORM_DATA) // <1>
HttpResponse<?> upload(CompletedFileUpload fileUpload, String userId, HttpRequest<?> request);
@Get("/{userId}") // <2>
Optional<HttpResponse<StreamedFile>> download(String userId);
@Status(NO_CONTENT) // <3>
@Delete("/{userId}") // <4>
void delete(String userId);
}
1 The @Post
annotation maps the method to an HTTP POST request.
2 The @Get
annotation maps the method to an HTTP GET request.
3 You can return void
in your controller’s method and specify the HTTP status code via the @Status
annotation.
4 The @Delete
annotation maps the delete
method to an HTTP Delete request on /{userId}
.
The launcher also created the ProfilePicturesController
class that implements the ProfilePicturesApi
interface in a file named lib/src/main/java/com/example/ProfilePicturesController.java. It contains the class definition and constructor:
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.micronaut.http.server.types.files.StreamedFile;
import io.micronaut.http.server.util.HttpHostResolver;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.objectstorage.ObjectStorageEntry;
import io.micronaut.objectstorage.ObjectStorageOperations;
import io.micronaut.objectstorage.request.UploadRequest;
import io.micronaut.objectstorage.response.UploadResponse;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import java.net.URI;
import java.util.Optional;
import static io.micronaut.http.HttpHeaders.ETAG;
import static io.micronaut.http.MediaType.IMAGE_JPEG_TYPE;
@Controller(ProfilePicturesController.PREFIX) // <1>
@ExecuteOn(TaskExecutors.IO) // <2>
class ProfilePicturesController implements ProfilePicturesApi {
static final String PREFIX = "/pictures";
private final ObjectStorageOperations<?, ?, ?> objectStorage; // <3>
private final HttpHostResolver httpHostResolver; // <4>
ProfilePicturesController(ObjectStorageOperations<?, ?, ?> objectStorage,
HttpHostResolver httpHostResolver) {
this.objectStorage = objectStorage;
this.httpHostResolver = httpHostResolver;
}
}
1 The class is defined as a controller with the @Controller
annotation mapped to the path /pictures
.
2 It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop.
3 ObjectStorageOperations
provides a uniform API to create, read and delete objects in the major cloud providers.
4 HttpHostResolver
enables you to resolve the host for an HTTP request.
2.1. Upload Endpoint #
The launcher also generated the /upload
endpoint which receives the file from the HTTP client via CompletedFileUpload
, and the userId
path parameter. It uploads the file to Oracle Cloud Infrastructure Object Storage using ObjectStorageOperations, and then returns its ETag
in an HTTP response header to the client:
@Override
public HttpResponse<?> upload(CompletedFileUpload fileUpload,
String userId,
HttpRequest<?> request) {
String key = buildKey(userId); // <1>
UploadRequest objectStorageUpload = UploadRequest.fromCompletedFileUpload(fileUpload, key); // <2>
UploadResponse<?> response = objectStorage.upload(objectStorageUpload); // <3>
return HttpResponse
.created(location(request, userId)) // <4>
.header(ETAG, response.getETag()); // <5>
}
private static String buildKey(String userId) {
return userId + ".jpg";
}
private URI location(HttpRequest<?> request, String userId) {
return UriBuilder.of(httpHostResolver.resolve(request))
.path(PREFIX)
.path(userId)
.build();
}
1 The key represents the path under which the file will be stored.
2 You can use any of the UploadRequest
static methods to build an upload request.
3 The upload operation returns an UploadResponse
, which wraps the cloud-specific SDK response.
4 Return the absolute URL of the resource in the location
header.
5 The response object contains some common properties for all cloud vendors, such as the ETag
, that is sent in a header to the client.
2.2. Download Endpoint #
The generated /download
endpoint simply retrieves the entry from the expected key, and transforms it into a StreamedFile
:
@Override
public Optional<HttpResponse<StreamedFile>> download(String userId) {
String key = buildKey(userId);
return objectStorage.retrieve(key) // <1>
.map(ProfilePicturesController::buildStreamedFile); // <2>
}
private static HttpResponse<StreamedFile> buildStreamedFile(ObjectStorageEntry<?> entry) {
StreamedFile file = new StreamedFile(entry.getInputStream(), IMAGE_JPEG_TYPE).attach(entry.getKey());
MutableHttpResponse<Object> httpResponse = HttpResponse.ok();
file.process(httpResponse);
return httpResponse.body(file);
}
1 The retrieve operation returns a cloud-independent ObjectStorageEntry
.
2 Transform the cloud-specific storage entry into an HttpResponse<StreamedFile>
.
The HTTP client could have used the ETag
from the upload operation and sent it in a If-None-Match
header in the download request to implement caching, which then would have been to be implemented in the download endpoint. But this is beyond the scope of this guide.
2.3. Delete Endpoint #
For the /delete
endpoint, all you have to do is invoke the delete
method with the expected key:
@Override
public void delete(String userId) {
String key = buildKey(userId);
objectStorage.delete(key);
}
3. Set up Oracle Cloud Infrastructure Resources #
In this section, you find the OCID of the compartment and create an Object Storage bucket using the Oracle Cloud Infrastructure CLI. Instead of using the CLI, you can also Create an Object Storage Bucket from the Oracle Cloud Console.
3.1. Compartment OCID #
Find the OCID of the compartment where you will be deploying. Run this command to list all the compartments in your root compartment:
oci iam compartment list
Find the compartment by the name or description in the JSON output. It should look like this:
{
"compartment-id": "ocid1.tenancy.oc1..aaaaaaaaud4g4e5ovjaw...",
"defined-tags": {},
"description": "GCN Guides",
"freeform-tags": {},
"id": "ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...",
"inactive-status": null,
"is-accessible": null,
"lifecycle-state": "ACTIVE",
"name": "gcn-guides",
"time-created": "2021-05-02T23:54:28.392000+00:00"
}
In this case, there is a compartment named “gcn-guides”.
Use the OCID from the id
property; the compartment-id
property is the parent compartment.
For convenience, save the compartment OCID as an environment variable. For Linux or macOS, run the following command:
export C=ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...
set C=ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...
$C = "ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm..."
3.2. Create a Bucket #
Use the Oracle Cloud Infrastructure CLI to create the bucket:
export OBJECT_STORAGE_BUCKET=gcn-guide-object-storage
oci os bucket create --compartment-id $C --name $OBJECT_STORAGE_BUCKET
set OBJECT_STORAGE_BUCKET=gcn-guide-object-storage
oci os bucket create --compartment-id %C% --name %OBJECT_STORAGE_BUCKET%
$OBJECT_STORAGE_BUCKET = "gcn-guide-object-storage"
oci os bucket create --compartment-id $C --name $OBJECT_STORAGE_BUCKET
You can also use the CLI to get the Object Storage namespace:
export OBJECT_STORAGE_NAMESPACE=$(oci os ns get --query "data" --raw-output)
Run the following command:
oci os ns get --query "data" --raw-output
Copy and store its result in an environment variable:
set OBJECT_STORAGE_NAMESPACE=your_namespace
Run the following command:
oci os ns get --query "data" --raw-output
Copy and store its result in an environment variable:
$OBJECT_STORAGE_NAMESPACE = "your_namespace"
To create an Object Storage bucket from the Oracle Cloud Console, see this guide.
Then, configure the bucket name and namespace in oci/src/main/resources/application-oraclecloud.properties:
micronaut.object-storage.oracle-cloud.default.namespace=${OBJECT_STORAGE_NAMESPACE}
micronaut.object-storage.oracle-cloud.default.bucket=${OBJECT_STORAGE_BUCKET}
3.3 Configure the Upload Parameters #
To upload a file larger than 1MB, configure the file oci/src/main/resources/application-oraclecloud.properties as follows:
# 20MB = 20 * 1024 * 1024b = 20971520b
micronaut.server.multipart.max-file-size=20971520
4. Run the Application #
To run the application, use the following command, which starts the application on port 8080.
5. Test the Application #
Test the application by uploading, downloading, and deleting a user profile picture.
5.1. Upload a Profile Picture #
Assuming you have a profile picture in a local file named profile.jpg, you can upload it to your application using the following command:
curl -i -F "fileUpload=@profile.jpg" http://localhost:8080/pictures/user_name
HTTP/1.1 201 Created
location: http://localhost:8080/pictures/user_name
ETag: "617cb82e296e153c29b34cccf7af0908"
date: Wed, 14 Sep 2022 12:50:30 GMT
connection: keep-alive
transfer-encoding: chunked
Note the location
and ETag
headers.
Use the oci
CLI to verify that the file has been uploaded to an Oracle Cloud Infrastructure bucket, as follows
oci os object list --bucket-name $OBJECT_STORAGE_BUCKET
oci os object list --bucket-name %OBJECT_STORAGE_BUCKET%
oci os object list --bucket-name $OBJECT_STORAGE_BUCKET
Note: You may need to export the
OBJECT_STORAGE_BUCKET
environment variable again as you did in Section 3.2. for the command above to work.
5.2. Download a Profile Picture #
Use the following command to download a picture:
curl http://localhost:8080/pictures/user_name -O -J
The file will be saved as user_name.jpg because the download endpoint includes a Content-Disposition: attachment
header. Open it to check that it is the same image as profile.jpg.
5.3. Delete a Profile Picture #
Use the following command to delete a picture:
curl -X DELETE http://localhost:8080/pictures/user_name
Then, check that the file has actually been deleted using the following command:
oci os object list --bucket-name $OBJECT_STORAGE_BUCKET
oci os object list --bucket-name %OBJECT_STORAGE_BUCKET%
oci os object list --bucket-name $OBJECT_STORAGE_BUCKET
6. Generate a Native Executable Using GraalVM #
GCN supports compiling a Java application ahead-of-time into a native executable using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.
To generate a native executable, run the following command:
./gradlew :oci:nativeCompile
The native executable is created in the oci/build/native/nativeCompile/ directory and can be run with the following command:
oci/build/native/nativeCompile/oci
./mvnw install -pl lib -am
./mvnw package -pl oci -Dpackaging=native-image
The native executable is created in the oci/target/ directory and can be run with the following command:
oci/target/oci
7. Run and Test the Native Executable #
Run the native executable, and then perform the same tests as in step 5.
8. Clean up #
When you have completed the guide, remove the bucket from Oracle Cloud Infrastructure to avoid stale resources. Use the following command to delete the bucket:
oci os bucket delete --bucket-name $OBJECT_STORAGE_BUCKET
oci os bucket delete --bucket-name %OBJECT_STORAGE_BUCKET%
oci os bucket delete --bucket-name $OBJECT_STORAGE_BUCKET
Confirm the deletion by entering y
when prompted.
Summary #
This guide demonstrated how to create a Java application to store, retrieve, and delete user profile pictures in Oracle Cloud Infrastructure Object Storage.