Search code examples
springspring-mvcresourcesinputstreamillegalstateexception

How to cache a InputStreamResource In RestController?


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?


Solution

  • 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.