Integrating Spring Boot with Minio for Direct Client Uploads Using Presigned Policies
This article demonstrates how to integrate Spring Boot with Minio, covering two upload strategies—backend‑mediated storage and direct client uploads with presigned credentials—while providing complete configuration, Java code, and a Vue‑based frontend example that includes chunked, instant, and resumable upload techniques.
Hello everyone, I am Chen.
Environment Preparation
Deployed Minio instance: http://mylocalhost:9001
Spring Boot Integration with Minio
A quick overview of the integration steps.
Add Minio Dependency
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>Define Configuration Properties
# application.yml
minio:
endpoint: http://mylocalhost:9001
accessKey: minio
secretKey: minio123
bucket: demoDefine Property Class
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
/** Object storage service URL */
private String endpoint;
/** Access key (user ID) */
private String accessKey;
/** Secret key (password) */
private String secretKey;
/** Default bucket */
private String bucket;
// getters and setters omitted
...
}Define Minio Configuration Class
@Configuration
public class MinioConfig {
@Bean
public MinioClient minioClient(MinioProperties properties) {
try {
MinioClient.Builder builder = MinioClient.builder();
builder.endpoint(properties.getEndpoint());
if (StringUtils.hasLength(properties.getAccessKey()) && StringUtils.hasLength(properties.getSecretKey())) {
builder.credentials(properties.getAccessKey(), properties.getSecretKey());
}
return builder.build();
} catch (Exception e) {
return null;
}
}
}Start the Spring Boot service after adding the above beans.
Upload Credential Endpoint
Write a controller method that returns a presigned upload policy.
@RequestMapping(value = "/presign", method = {RequestMethod.POST})
public Map<String, String> presign(@RequestBody PresignParam presignParam) {
if (StringUtils.isEmpty(presignParam.getBucket())) {
presignParam.setBucket("demo");
}
if (StringUtils.isEmpty(presignParam.getFilename())) {
presignParam.setFilename(UUID.randomUUID().toString());
}
ZonedDateTime expirationDate = ZonedDateTime.now().plusMinutes(10);
PostPolicy policy = new PostPolicy(presignParam.getBucket(), presignParam.getFilename(), expirationDate);
try {
final Map<String, String> map = minioClient.presignedPostPolicy(policy);
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
return map;
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}The response contains fields such as bucket, x-amz-date, x-amz-signature, key, x-amz-algorithm, x-amz-credential, and policy, which the frontend uses to upload directly to Minio.
Frontend Upload Using the Credential
uploadFile(file, policy) {
console.log("Preparing to upload file:");
console.log("file: " + file);
console.log("policy: " + policy);
var formData = new FormData();
formData.append('file', file);
formData.append('key', policy['key']);
formData.append('x-amz-algorithm', policy['x-amz-algorithm']);
formData.append('x-amz-credential', policy['x-amz-credential']);
formData.append('x-amz-signature', policy['x-amz-signature']);
formData.append('x-amz-date', policy['x-amz-date']);
formData.append('policy', policy['policy']);
return new Promise((resolve, reject) => {
$.ajax({
method: 'POST',
url: 'http://mylocalhost:9001/' + policy['bucket'],
data: formData,
dataType: 'json',
contentType: false,
processData: false,
xhr: function () {
var xhr = $.ajaxSettings.xhr();
if (xhr.upload) {
xhr.upload.addEventListener('progress', function (e) {
var percentage = parseInt(e.loaded / e.total * 100);
vm.uploadResult = percentage + "%" + ":" + policy['key'];
}, false);
}
return xhr;
},
success: function (result) {
vm.uploadResult = 'File uploaded successfully: ' + policy['key'];
resolve(result);
},
error: function (e) {
reject();
}
});
});
}Chunked Upload, Instant Upload, and Resume Upload
Chunked Upload
Large files can be split into smaller chunks (e.g., a 100 MB file into ten 10 MB parts) to reduce per‑node pressure in a Minio cluster and to enable multi‑threaded uploading.
Instant Upload
Before uploading, compute the file’s MD5 hash (e.g., 3cc1f3c3c2d1a29ecf60ffad4de278c7) and concatenate it with the filename. If the backend detects that a file with the same hash already exists, it can skip credential generation and report the file as already uploaded.
Resume Upload
By combining chunked upload and instant‑upload logic, a client can resume an interrupted upload: already‑uploaded chunks are skipped, and only the missing parts are sent.
File Merge
After all chunks are uploaded to a temporary bucket (e.g., slice), the backend merges them into the final object.
@GetMapping("/compose")
public void merge() {
List<ComposeSource> sources = new ArrayList<>();
sources.add(ComposeSource.builder().bucket("slice").object("0寂寞的季节.mp4").build());
sources.add(ComposeSource.builder().bucket("slice").object("1寂寞的季节.mp4").build());
sources.add(ComposeSource.builder().bucket("slice").object("2寂寞的季节.mp4").build());
ComposeObjectArgs args = ComposeObjectArgs.builder()
.bucket("demo")
.object("寂寞的季节.mp4")
.sources(sources)
.build();
try {
minioClient.composeObject(args);
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}Frontend Example Code
A minimal single‑page application (Vue 2 + jQuery) that obtains a presigned policy, uploads files, and demonstrates chunked upload, merge, and progress reporting.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Minio Test</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.14/vue.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
<div id="app">
<h1>{{title}}</h1>
<form @submit.prevent="getPolicyForm">
<label>Bucket<input type="text" v-model="policyParams.bucket"/></label><br/>
<label>Filename<input type="text" v-model="policyParams.filename"/></label><br/>
<button type="submit">Get Upload Credential</button>
</form>
<form @submit.prevent="uploadFileForm" v-show="policy != null">
<label>File<input type="file" @change="fileChange"/></label><br/>
<button type="submit" v-show="file != null">Upload File</button>
</form>
<div v-show="file != null">
<button @click="sliceEvent">Test Chunked Upload</button>
<button @click="sliceComposeEvent">Merge Chunks</button>
</div>
<p>{{uploadResult}}</p>
<ul>
<li v-for="(item, index) in sliceUploadResult" :key="index">{{item}}</li>
</ul>
</div>
<script>
var vm = new Vue({
el: "#app",
data() {
return {
title: "Minio Test",
policyParams: { bucket: null, filename: null },
policy: null,
file: null,
uploadResult: null,
sliceUploadResult: null,
// other state omitted for brevity
};
},
methods: {
getPolicyForm() {
this.policyParams.bucket = "demo";
this.policyParams.filename = "寂寞的季节.mp4";
this.requestPolicy(this.policyParams);
},
requestPolicy(params) {
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: "http://localhost:8888/presign",
contentType: "application/json",
data: JSON.stringify(params),
success: result => { console.log(result); vm.policy = result; resolve(result); },
error: () => reject()
});
});
},
fileChange(event) { this.file = event.target.files[0]; },
uploadFileForm() { this.uploadFile(this.file, this.policy); },
// uploadFile, sliceEvent, sliceComposeEvent, calculateMD5 implementations omitted for brevity
}
});
</script>
</body>
</html>Final Note
If this article helped you, please like, watch, share, and bookmark. Your support keeps the content coming!
Join my Knowledge Planet for ¥199 to access premium projects such as Spring full‑stack practice, massive data sharding, DDD micro‑services, and more.
Follow the public account "码猿技术专栏" and reply with "加群" to join the discussion group.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
