How to cache a InputStreamResource In RestController? - spring

I have a servlet that returns an image as InputStreamResource. There are approx 50 static images that are to be returned based on some get query parameters.
For not having to look up each of those images every time it is requested (which is very often), I'd like to cache those images responses.
#RestController
public class MyRestController {
//code is just example; may be any number of parameters
#RequestMapping("/{code}")
#Cachable("code.cache")
public ResponseEntity<InputStreamResource> getCodeLogo(#PathVariable("code") String code) {
FileSystemResource file = new FileSystemResource("d:/images/" + code + ".jpg");
return ResponseEntity.ok()
.contentType("image/jpg")
.lastModified(file.lastModified())
.contentLength(file.contentLength())
.body(new InputStreamResource(file.getInputStream()));
}
}
When using the #Cacheable annotation (no matter if directly on the RestMapping method or refactored to an external service), I_'m getting the following exception:
cause: java.lang.IllegalStateException: InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times - error: InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times
org.springframework.core.io.InputStreamResource.getInputStream(InputStreamResource.java:96)
org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:100)
org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:47)
org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:195)
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:238)
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:183)
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:126)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743)
Question: how can I then cache the ResponseEntity of type InputStreamResource at all?

Cache manager will add to cache ResponseEntity with InputStreamResource inside of it. First time it will be ok. But when cached ResponseEntity will try to read InputStreamResouce second time you'll get exception, because it is unable to read stream more than one time.
Solution: don't cache InputStreamResouce itself, but cache the content of stream.
#RestController
public class MyRestController {
#RequestMapping("/{code}")
#Cachable("code.cache")
public ResponseEntity<byte[]> getCodeLogo(#PathVariable("code") String code) {
FileSystemResource file = new FileSystemResource("d:/images/" + code + ".jpg");
byte [] content = new byte[(int)file.contentLength()];
IOUtils.read(file.getInputStream(), content);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
.lastModified(file.lastModified())
.contentLength(file.contentLength())
.body(content);
}
}
I've used IOUtils.read() from org.apache.commons.io, to copy bytes from stream to array, but you can do it by any preferred way.

You can't cache Streams. Once they are read, they are gone.
The error message is pretty clear about that:
InputStream has already been read -
do not use InputStreamResource if a stream needs to be read multiple times
By your code and comments, it seems to me that you have a big images folder with JPG logos (which might be added, deleted or modified), and you want to have a daily cache of the one's you're being asked for, so you don't have to constantly reload them from disk.
If that's the case, your best option is to read the File's content to a ByteArray and cache/return that instead.

Related

Tomcat Performance with Spring Boot API for File Upload

I have a Spring boot API and one of the endpoints allows users to upload video's. Now My controller basically takes the file as a MultiPart file and then I store it in a temp folder accessible to tomcat. Once I have it stored on Disk, I then push the video to an S3 bucket.
Now to me anyway, this seems to be less than optimal, Like if I wanted to have a 100 or a 1000 users upload at once it seems really non performant to write the files to disk first.
As a little background I'm storing it on disk with the intention that if there is a issue pushing to S3 I can retry
The below code might show what I'm doing better than the above:
public Video addVideo(#RequestParam("title") String title,
#RequestParam("Description") String Description,
#RequestParam(value = "file", required = true) MultipartFile file) {
this.amazonS3ClientService.uploadFileToS3Bucket(file, title, description));
}
Method for storing Video file:
String fileNameWithExtenstion = awsS3FileName + "." + FilenameUtils.getExtension(multipartFile.getOriginalFilename());
//creating the file in the server (temporarily)
File file = new File(tomcatTempDir + fileNameWithExtenstion);FileOutputStream fos = new FileOutputStream(file);
fos.write(multipartFile.getBytes());
fos.close();PutObjectRequest putObjectRequest = new PutObjectRequest(this.awsS3Bucket, awsS3BucketFolder + UnigueId + "/" + fileNameWithExtenstion, file);
if (enablePublicReadAccess) {
putObjectRequest.withCannedAcl(CannedAccessControlList.PublicRead);
}
// Upload a file as a new object with ContentType and title
specified.amazonS3.putObject(putObjectRequest);
//removing the file created in the server
file.delete();
So my question is....is there a better way in Tomcat to:
A) Take in a file via a controllerB) Push to S3
There is no other way to do it with multipart. The problem with multipart that to properly segement parts from the requst they need sometimes skipped or be repeatable. That is impossible within memory w/o having memory to explode. Therefore, Commons FileUpload caches them on disk after a certain threshold is reached.
Multipart requests are the worst way for that. I highly recommend to use either PUT or POST with content type application/octet-stream. You can take the bare request input stream and pass to HttpClient to stream to your backend server. I did this already 5 years ago and it works for gigabytes. I have posted the solution in the Apache HttpClient mailing list.
There is one possibility how this could work under specific conditions:
All parts are in the correct physical order you want to read
Your write to a backend is fast enough to sustain the read from the front
Consume the root part and then go over to the next physical one, process the request body lazily. JAX-WS RI (Metro) has a very nice handling of multipart requests for XOP/MTOM. Learn from that because you won't be able to make it any better.
Perhaps you can try to direct stream the input stream from your MultipartFile to S3.
Consider the following uploadFileToS3Bucket method:
public PutObjectResult uploadFileToS3Bucket(InputStream input, long size, String title, String description) {
// Indicate the length of the information to avoid the need to compute it by the AWS SDK
// See: https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/PutObjectRequest.html#PutObjectRequest-java.lang.String-java.lang.String-java.io.InputStream-com.amazonaws.services.s3.model.ObjectMetadata-
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(size); // rely on Spring implementation. Maybe you probably also can use input.available()
// compute the object name as appropriate
String key = "...";
PutObjectRequest putObjectRequest = new PutObjectRequest(
this.awsS3Bucket, key, input, objectMetadata
);
// The rest of your code
if (enablePublicReadAccess) {
putObjectRequest.withCannedAcl(CannedAccessControlList.PublicRead);
}
// Upload a file as a new object with ContentType and title
return specified.amazonS3.putObject(putObjectRequest);
}
Of course, you need to provide the service the input stream obtained from the client request associated with the MutipartFile object:
public Video addVideo(
#RequestParam("title") String title,
#RequestParam("Description") String Description,
#RequestParam(value = "file", required = true) MultipartFile file) {
try (InputStream input = file.getInputStream()) {
this.amazonS3ClientService.uploadFileToS3Bucket(input, file.getSize(), title, description));
}
}
Probably you can also play with the getBytes method of MultipartFile and create a ByteArrayInputStream to perform the operation.
In addVideo:
byte[] bytes = file.getBytes();
In uploadFileToS3Bucket:
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(bytes.length);
PutObjectRequest putObjectRequest = new PutObjectRequest(
this.awsS3Bucket, key, new ByteArrayInputStream(bytes), objectMetadata
);
I would prefer the first solution, but try to determine which option offers you the best performance.

How to mock a multipart file upload when using Spring and Apache File Upload

The project I'm working on needs to support large file uploads and know the time taken during their upload.
To handle the large files I'm using the streaming API of Apache FileUpload, this also allows me to measure the time taken for the complete stream to be saved.
The problem I'm having is that I cannot seem to be able to utilise MockMvc in an Integration Test on this controller. I know that the controller works as I've successfully uploaded files using postman.
Simplified Controller Code:
#PostMapping("/upload")
public String handleUpload(HttpServletRequest request) throws Exception {
ServletFileUpload upload = new ServletFileUpload();
FileItemIterator iterStream = upload.getItemIterator(request);
while (iterStream.hasNext()) {
FileItemStream item = iterStream.next();
String name = item.getFieldName();
InputStream stream = item.openStream();
if (!item.isFormField()) {
// Process the InputStream
} else {
String formFieldValue = Streams.asString(stream);
}
}
}
Simplified Test Code:
private fun uploadFile(tfr: TestFileContainer) {
val mockFile = MockMultipartFile("file", tfr.getData()) // .getData*() returns a ByteArray
val receiveFileRequest = MockMvcRequestBuilders.multipart("/upload")
.file(mockFile)
.contentType(MediaType.MULTIPART_FORM_DATA)
val result = mockMvc.perform(receiveFileRequest)
.andExpect(status().isCreated)
.andExpect(header().exists(LOCATION))
.andReturn(
}
This is the error I'm currently getting
org.apache.tomcat.util.http.fileupload.FileUploadException: the
request was rejected because no multipart boundary was found
Can anyone help?
The MockMultipartFile approach won't work as Spring does work behind the scenes and simply passes the file around.
Ended up using RestTemplate instead as it actually constructs requests.

RxNetty download large file

I'm trying to create controller that download large file using RxNetty
I write something stupid like
#RequestMapping(method = RequestMethod.GET, path = "largeFile")
public DeferredResult<ResponseEntity<byte[]>> largeFile() throws IOException {
Observable<ResponseEntity<byte[]>> observable = RxNetty.createHttpGet(URL)
.flatMap(AbstractHttpContentHolder::getContent)
.map(data -> {
byte[] bytes = new byte[data.readableBytes()];
data.readBytes(bytes);
return new ResponseEntity<>(bytes, HttpStatus.OK);
});
DeferredResult<ResponseEntity<byte[]>> deferredResult = new DeferredResult<>();
observable.subscribe(deferredResult::setResult, deferredResult::setErrorResult);
return deferredResult;
}
Nevertheless I have the following error:
Caused by: io.netty.handler.codec.TooLongFrameException: HTTP content length exceeded 1048576 bytes.
The default client in RxNetty 0.4.x aggregates HTTP payload which has a limit on the maximum content length. The exception you see is because of that limit. You can alter the default client using a PipelineConfigurator as shown in this example:
https://github.com/ReactiveX/RxNetty/blob/0.4.x/rxnetty-examples/src/main/java/io/reactivex/netty/examples/http/chunk/HttpChunkClient.java#L49
after which the payload will be chunked into multiple buffers.
Alternatively, if you know the max size, then you can use an appropriate payload aggregator in the configurator.

FileSystemResource is returned with content type json

I have the following spring mvc method that returns a file:
#RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public FileSystemResource getFiles(#PathVariable String fileName){
String path="/home/marios/Desktop/";
return new FileSystemResource(path+fileName);
}
I expect a ResourceHttpMessageConverter to create the appropriate response with an octet-stream type according to its documentation:
If JAF is not available, application/octet-stream is used.
However although I correctly get the file without a problem, the result has Content-Type: application/json;charset=UTF-8
Can you tell me why this happens?
(I use spring version 4.1.4. I have not set explicitly any message converters and I know that spring loads by default among others the ResourceHttpMessageConverter and also the MappingJackson2HttpMessageConverter because I have jackson 2 in my classpath due to the fact that I have other mvc methods that return json.
Also if I use HttpEntity<FileSystemResource> and set manually the content type, or specify it with produces = MediaType.APPLICATION_OCTET_STREAM it works fine.
Note also that in my request I do not specify any accept content types, and prefer not to rely on my clients to do that)
I ended up debugging the whole thing, and I found that AbstractJackson2HttpMessageConverter has a canWrite implementation that returns true in case of the FileSystemResource because it just checks if class is serializable, and the set media type which is null since I do not specify any which in that case is supposed to be supported by it.
As a result it ends up putting the json content types in a list of producible media types. Of course ResourceHttpMessageConverter.canWrite implementation also naturally returns true, but the ResourceHttpMessageConverter does not return any producible media types.
When the time to write the actual response comes, from the write method implementation, the write of the ResourceHttpMessageConverter runs first due to the fact that the ResourceHttpMessageConverter is first in the list of the available converters (if MappingJackson2HttpMessageConverter was first, it would try to call write since its canWrite returns true and throw exception), and since there was already a producible content type set, it does not default to running the ResourceHttpMessageConverter.getDefaultContentType that would set the correct content type.
If I remove json converter all would work fine, but unfortunately none of my json methods would work. Therefore specifying the content type is the only way to get rid of the returned json content type
For anyone still looking for a piece of code:
You should wrap your FileSystemResource into a ResponseEntity<>
Then determine your image's content type and append it to ResponseEntity as a header.
Here is an example:
#GetMapping("/image")
public #ResponseBody ResponseEntity<FileSystemResource> getImage() throws IOException {
File file = /* load your image file from anywhere */;
if (!file.exists()) {
//TODO: throw 404
}
FileSystemResource resource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(/* determine your image's media type or just set it as a constant using MediaType.<value> */);
headers.setContentLength(resource.contentLength());
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
}

Identify image request in servlet by filename in URI instead of as request parameter

I use a servlet to get images that saved out of my project. Code in my servlet is:
String file = request.getParameter("file");
BufferedInputStream in = new BufferedInputStream(new FileInputStream(directory + file));
// Get image contents.
byte[] bytes = new byte[in.available()];
in.read(bytes);
in.close();
// Write image contents to response.
response.getOutputStream().write(bytes);
The HTML image tags are like this:
<img src="/images/?file=example.jpg" />
Everything is good. But now I would like to have image filename in URI instead of as request parameter like so:
<img src="/images/example.jpg" />
How can I achieve it?
Map the servlet on a prefix URL pattern /images/* instead of an apparently exact URL pattern /images. Then, you can specify URLs exactly like that and obtain the filename as URI path information by HttpServletRequest#getPathInfo().
Kickoff example:
#WebServlet("/images/*")
public class ImageServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String filename = request.getPathInfo().substring(1);
File file = new File(directory, filename);
response.setHeader("Content-Type", getServletContext().getMimeType(file.getName()));
response.setHeader("Content-Length", String.valueOf(file.length()));
Files.copy(file.toPath(), response.getOutputStream());
}
}
Unrelated to the concrete problem: InputStream#available() as in your initial code doesn't do what you apparently thought it does. It doesn't return the entire length of the content. It just returns the length of the first block which the code can read without blocking the disk file system I/O. I.e. it returns the length of content currently in I/O buffer. This does not necessarily represent the entire content length! For sure not on larger images. If you're on Java 7, just use Files#copy() if the sole purpose is to achieve the job with least possible amount of code as shown above.

Resources