From beaa80d0a0af95d566fa6fe81082e60d6c97bae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Miller=20=28=E9=94=BA=E4=BF=8A=29?= Date: Tue, 10 Aug 2021 18:28:10 +0800 Subject: [PATCH] perf($OSS): support streaming partial content support streaming partial content by returning resource region; default byte count is 4 MB BREAKING CHANGE: support streaming partial content(HTTP 206) --- docker/docker-compose.yml | 2 + .../controller/ReadResourceController.java | 7 ++- .../read/service/ReadResourceService.java | 5 +- .../service/impl/ReadResourceServiceImpl.java | 58 ++++++++++++++----- 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index bf9e901c..eee68949 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -69,6 +69,8 @@ services: redis-slave-1: container_name: ${REDIS_SLAVE_1_CONTAINER_NAME} image: redis:${REDIS_TAG} + depends_on: + - redis-master ports: - "6380:6379" restart: always diff --git a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/controller/ReadResourceController.java b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/controller/ReadResourceController.java index a174006f..ce104761 100644 --- a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/controller/ReadResourceController.java +++ b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/controller/ReadResourceController.java @@ -6,10 +6,12 @@ import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @@ -31,7 +33,8 @@ public class ReadResourceController { @GetMapping("/{bucket}/{object}") @ApiOperation(value = "Get single resource", notes = "Get or download single resource") public ResponseEntity getSingleResource(@PathVariable String bucket, @PathVariable String object, - @Valid GetSingleResourcePayload payload) { - return readResourceService.getSingleResource(bucket, object, payload); + @Valid GetSingleResourcePayload payload, + @RequestHeader(name = HttpHeaders.RANGE, required = false) String range) { + return this.readResourceService.getSingleResource(bucket, object, payload, range); } } diff --git a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/ReadResourceService.java b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/ReadResourceService.java index dabe7873..ac25b083 100644 --- a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/ReadResourceService.java +++ b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/ReadResourceService.java @@ -3,6 +3,7 @@ import com.jmsoftware.maf.osscenter.read.entity.GetSingleResourcePayload; import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; @@ -27,8 +28,10 @@ public interface ReadResourceService { * @param bucket the bucket * @param object the object * @param payload the payload + * @param range * @return the single resource */ ResponseEntity getSingleResource(@NotBlank String bucket, @NotBlank String object, - @Valid @NotNull GetSingleResourcePayload payload); + @Valid @NotNull GetSingleResourcePayload payload, + @Nullable String range); } diff --git a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java index d27f27b1..9ee5e1eb 100644 --- a/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java +++ b/oss-center/src/main/java/com/jmsoftware/maf/osscenter/read/service/impl/ReadResourceServiceImpl.java @@ -1,23 +1,25 @@ package com.jmsoftware.maf.osscenter.read.service.impl; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.ObjectUtil; import com.jmsoftware.maf.osscenter.read.entity.GetSingleResourcePayload; import com.jmsoftware.maf.osscenter.read.service.ReadResourceService; import com.jmsoftware.maf.springcloudstarter.helper.MinioHelper; +import io.minio.StatObjectResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.http.*; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; +import org.springframework.util.unit.DataSize; import javax.validation.Valid; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; - -import static java.lang.Math.min; +import java.util.List; /** *

ReadResourceServiceImpl

@@ -30,25 +32,55 @@ @Service @RequiredArgsConstructor public class ReadResourceServiceImpl implements ReadResourceService { + private static final DataSize SMALL_DATA_SIZE = DataSize.ofMegabytes(1); + private static final DataSize MEDIUM_DATA_SIZE = DataSize.ofMegabytes(4); + private static final DataSize LARGE_DATA_SIZE = DataSize.ofMegabytes(8); private final MinioHelper minioHelper; @Override public ResponseEntity getSingleResource(@NotBlank String bucket, @NotBlank String object, - @Valid @NotNull GetSingleResourcePayload payload) { - val statObjectResponse = minioHelper.statObject(bucket, object); - if (ObjectUtil.isNull(statObjectResponse)) { + @Valid @NotNull GetSingleResourcePayload payload, + @Nullable String range) { + StatObjectResponse statObjectResponse; + try { + statObjectResponse = this.minioHelper.statObject(bucket, object); + } catch (Exception e) { + log.error("Exception occurred when looking for object. Exception message: {}", e.getMessage()); return ResponseEntity.notFound().build(); } - val inputStream = minioHelper.getObject(bucket, object); - val bodyBuilder = ResponseEntity.ok(); - if (BooleanUtil.isTrue(payload.getDownloadable())) { - String contentDisposition = ContentDisposition.builder("attachment").filename(object).build().toString(); - bodyBuilder.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition); + val httpRanges = HttpRange.parseRanges(range); + if (CollUtil.isEmpty(httpRanges)) { + val bodyBuilder = ResponseEntity.ok(); + if (BooleanUtil.isTrue(payload.getDownloadable())) { + bodyBuilder.header(HttpHeaders.CONTENT_DISPOSITION, + ContentDisposition.builder("attachment").filename(object).build().toString()); + } + val getObjectResponse = this.minioHelper.getObject(bucket, object, 0, MEDIUM_DATA_SIZE.toBytes()); + return bodyBuilder + .header(HttpHeaders.ACCEPT_RANGES, "bytes") + .contentLength(statObjectResponse.size()) + .contentType(MediaType.parseMediaType(statObjectResponse.contentType())) + .body(new InputStreamResource(getObjectResponse)); } + return this.getResourceRegion(bucket, object, statObjectResponse, httpRanges); + } + + private ResponseEntity getResourceRegion(String bucket, String object, + StatObjectResponse statObjectResponse, + List httpRanges) { + val bodyBuilder = ResponseEntity.status(HttpStatus.PARTIAL_CONTENT); + val getObjectResponse = this.minioHelper.getObject(bucket, object, httpRanges.get(0).getRangeStart(0), + MEDIUM_DATA_SIZE.toBytes()); + val start = httpRanges.get(0).getRangeStart(0); + var end = start + MEDIUM_DATA_SIZE.toBytes() - 1; + val resourceLength = statObjectResponse.size(); + end = Math.min(end, resourceLength - 1); + val rangeLength = end - start + 1; return bodyBuilder .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .contentLength(statObjectResponse.size()) + .header(HttpHeaders.CONTENT_RANGE, String.format("bytes %d-%d/%d", start, end, resourceLength)) + .contentLength(rangeLength) .contentType(MediaType.parseMediaType(statObjectResponse.contentType())) - .body(new InputStreamResource(inputStream)); + .body(new InputStreamResource(getObjectResponse)); } }