Are you sure that the transaction advice is running? By default, Spring uses the "proxy" advice mode. The transaction advice would only run if you registered the Spring-proxied instance of your resource with the JAX-RS Application
, or if you were using "aspectj" weaving instead of the default "proxy" advice mode.
Assuming that a physical transaction is not being re-used as a result of transaction propagation, using @Transactional
on this download() method is incorrect in general.
If the transaction advice is actually running, the transaction ends when returning from the download() method. The Blob
Javadoc says: "A Blob
object is valid for the duration of the transaction in which is was created." However, §16.3.7 of the JDBC 4.2 spec says: "Blob
, Clob
and NClob
objects remain valid for at least the duration of the transaction in which they are created." Therefore, the InputStream
returned by getBinaryStream() is not guaranteed to be valid for serving the response; the validity would depend on any guarantees provided by the JDBC driver. For maximum portability, you should rely on the Blob
being valid only for the duration of the transaction.
Regardless of whether the transaction advice is running, you potentially have a race condition because the underlying JDBC connection used to retrieve the Blob
might be re-used in a way that invalidates the Blob
.
EDIT: Testing Jersey 2.17, it appears that the behavior of constructing a Response
from an InputStream
depends on the specified response MIME type. In some cases, the InputStream
is read entirely into memory first before the response is sent. In other cases, the InputStream
is streamed back.
Here is my test case:
@Path("test")
public class MyResource {
@GET
public Response getIt() {
return Response.ok(new InputStream() {
@Override
public int read() throws IOException {
return 97; // 'a'
}
}).build();
}
}
If the getIt() method is annotated with @Produces(MediaType.TEXT_PLAIN)
or no @Produces
annotation, then Jersey attempts to read the entire (infinite) InputStream
into memory and the application server eventually crashes from running out of memory. If the getIt() method is annotated with @Produces(MediaType.APPLICATION_OCTET_STREAM)
, then the response is streamed back.
So, your download() method may be working simply because the blob is not being streamed back. Jersey might be reading the entire blob into memory.
Related: How to stream an endless InputStream with JAX-RS
EDIT2: I have created a demonstration project using Spring Boot and Apache CXF:
https://github.com/dtrebbien/so30356840-cxf
If you run the project and execute on the command line:
curl 'http://localhost:8080/myapp/test/data/1' >/dev/null
Then you will see log output like the following:
2015-06-01 15:58:14.573 DEBUG 9362 --- [nio-8080-exec-1] org.apache.cxf.transport.http.Headers : Request Headers: {Accept=[*/*], Content-Type=[null], host=[localhost:8080], user-agent=[curl/7.37.1]}
2015-06-01 15:58:14.584 DEBUG 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils : Trying to select a resource class, request path : /test/data/1
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils : Trying to select a resource operation on the resource class com.sample.resource.MyResource
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils : Resource operation getIt may get selected
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils : Resource operation getIt on the resource class com.sample.resource.MyResource has been selected
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor : Request path is: /test/data/1
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor : Request HTTP method is: GET
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor : Request contentType is: */*
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor : Accept contentType is: */*
2015-06-01 15:58:14.585 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor : Found operation: getIt
2015-06-01 15:58:14.595 DEBUG 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.sample.resource.MyResource.getIt]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
2015-06-01 15:58:14.595 DEBUG 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Acquired Connection [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]] for JDBC transaction
2015-06-01 15:58:14.596 DEBUG 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]] to manual commit
2015-06-01 15:58:14.602 DEBUG 9362 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2015-06-01 15:58:14.603 DEBUG 9362 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [SELECT data FROM images WHERE id = ?]
2015-06-01 15:58:14.620 DEBUG 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2015-06-01 15:58:14.620 DEBUG 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]]
2015-06-01 15:58:14.621 DEBUG 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]] after transaction
2015-06-01 15:58:14.621 DEBUG 9362 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
2015-06-01 15:58:14.621 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Invoking handleMessage on interceptor org.apache.cxf.interceptor.OutgoingChainInterceptor@7eaf4562
2015-06-01 15:58:14.622 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Adding interceptor org.apache.cxf.interceptor.MessageSenderInterceptor@20ffeb47 to phase prepare-send
2015-06-01 15:58:14.622 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Adding interceptor org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor@5714d386 to phase marshal
2015-06-01 15:58:14.622 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Chain org.apache.cxf.phase.PhaseInterceptorChain@11ca802c was created. Current flow:
prepare-send [MessageSenderInterceptor]
marshal [JAXRSOutInterceptor]
2015-06-01 15:58:14.623 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Invoking handleMessage on interceptor org.apache.cxf.interceptor.MessageSenderInterceptor@20ffeb47
2015-06-01 15:58:14.623 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Adding interceptor org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor@6129236d to phase prepare-send-ending
2015-06-01 15:58:14.623 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Chain org.apache.cxf.phase.PhaseInterceptorChain@11ca802c was modified. Current flow:
prepare-send [MessageSenderInterceptor]
marshal [JAXRSOutInterceptor]
prepare-send-ending [MessageSenderEndingInterceptor]
2015-06-01 15:58:14.623 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Invoking handleMessage on interceptor org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor@5714d386
2015-06-01 15:58:14.627 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSOutInterceptor : Response content type is: application/octet-stream
2015-06-01 15:58:14.631 DEBUG 9362 --- [nio-8080-exec-1] o.apache.cxf.ws.addressing.ContextUtils : retrieving MAPs from context property javax.xml.ws.addressing.context.inbound
2015-06-01 15:58:14.631 DEBUG 9362 --- [nio-8080-exec-1] o.apache.cxf.ws.addressing.ContextUtils : WS-Addressing - failed to retrieve Message Addressing Properties from context
2015-06-01 15:58:14.636 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain : Invoking handleMessage on interceptor org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor@6129236d
2015-06-01 15:58:14.639 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.t.http.AbstractHTTPDestination : Finished servicing http request on thread: Thread[http-nio-8080-exec-1,5,main]
2015-06-01 15:58:14.639 DEBUG 9362 --- [nio-8080-exec-1] o.a.c.t.servlet.ServletController : Finished servicing http request on thread: Thread[http-nio-8080-exec-1,5,main]
I have trimmed the log output for readability. The important thing to note is that the transaction is committed and the JDBC connection is returned before the response is sent. Therefore, the InputStream
returned by blob.getBinaryStream()
is not necessarily valid and the getIt() resource method may be invoking undefined behavior.
EDIT3: A recommended practice for using Spring's @Transactional
annotation is to annotate the service method (see Spring @Transactional Annotation Best Practice). You could have a service method that finds the blob and transfers the blob data to the response OutputStream
. The service method could be annotated with @Transactional
so that the transaction in which the Blob
is created would remain open for the duration of the transfer. However, it seems to me that this approach could introduce a denial of service vulnerability by way of a "slow read" attack. Because the transaction should be kept open for the duration of the transfer for maximum portability, numerous slow readers could lock up your database table(s) by holding open transactions.
One possible approach is to save the blob to a temporary file and stream back the file. See How do I use Java to read from a file that is actively being written? for some ideas on reading a file while it's being simultaneously written, though this case is more straightforward because the length of the blob can be determined by calling the Blob#length() method.