我有以下JAX-RS服务端点的示意图实现:
@GET
@Path("...")
@Transactional
public Response download() {
java.sql.Blob blob = findBlob(...);
return Response.ok(blob.getBinaryStream()).build();
}
调用JAX-RS端点将从数据库中获取Blob(通过JPA)并将结果传输回HTTP客户端。使用Blob和流的目的而不是例如JPA的初始BLOB到byte []映射是为了防止所有数据必须保存在内存中,而是直接从数据库流式传输到HTTP响应。
这是按预期工作的,我实际上不明白为什么。我是否从与底层JDBC连接和事务相关联的数据库中获取Blob句柄?如果是这样,我会期望在从download()方法返回时提交Spring事务,这使得JAX-RS实现以后无法从Blob访问数据以将其流回HTTP响应。
答案 0 :(得分:5)
您确定交易建议正在运行吗? By default,Spring使用“代理”建议模式。只有在使用JAX-RS Application
注册资源的Spring代理实例时,或者如果您使用“aspectj”编织而不是默认的“代理”建议模式,事务建议才会运行。
假设由于事务传播而没有重用physical事务,那么在这个download()方法上使用@Transactional
通常是不正确的。
如果事务通知实际上正在运行,则从download()方法返回时事务结束。 Blob
Javadoc表示:“Blob
对象在创建事务的持续时间内有效。”但是,JDBC 4.2规范的§16.3.7说:“Blob
,Clob
和NClob
对象至少在创建它们的事务期间保持有效。”因此,getBinaryStream()返回的InputStream
不能保证对提供响应有效;有效性取决于JDBC驱动程序提供的任何保证。为了获得最大的可移植性,您应该仅依赖于Blob
在交易期间有效。
无论事务通知是否正在运行,您都可能具有竞争条件,因为用于检索Blob
的基础JDBC连接可能会以使Blob
无效的方式重新使用。 / p>
编辑:测试Jersey 2.17,似乎从Response
构造InputStream
的行为取决于指定的响应MIME类型。在某些情况下,InputStream
在发送响应之前首先完全读入内存。在其他情况下,InputStream
会回传。
这是我的测试用例:
@Path("test")
public class MyResource {
@GET
public Response getIt() {
return Response.ok(new InputStream() {
@Override
public int read() throws IOException {
return 97; // 'a'
}
}).build();
}
}
如果getIt()方法注释了@Produces(MediaType.TEXT_PLAIN)
或没有@Produces
注释,那么Jersey会尝试将整个(无限)InputStream
读入内存,应用服务器最终会崩溃内存耗尽。如果使用@Produces(MediaType.APPLICATION_OCTET_STREAM)
注释了getIt()方法,则会回传该响应。
因此,您的download()方法可能只是因为blob 不正在回传。泽西岛可能正在将整个blob读入记忆中。
相关:How to stream an endless InputStream with JAX-RS
EDIT2:我使用Spring Boot和Apache CXF创建了一个演示项目:
https://github.com/dtrebbien/so30356840-cxf
如果您运行项目并在命令行上执行:
curl 'http://localhost:8080/myapp/test/data/1' >/dev/null
然后您将看到如下所示的日志输出:
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]
为了便于阅读,我修剪了日志输出。需要注意的重要事项是提交事务并在发送响应之前返回的JDBC连接。因此,InputStream
返回的blob.getBinaryStream()
不一定有效,而getIt() resource method可能正在调用未定义的行为。
EDIT3:使用Spring的@Transactional
注释的推荐做法是注释服务方法(请参阅Spring @Transactional Annotation Best Practice)。您可以使用一种服务方法来查找blob并将blob数据传输到响应OutputStream
。可以使用@Transactional
对服务方法进行注释,以便创建Blob
的事务在传输期间保持打开状态。但是,在我看来,这种方法可能会通过"slow read" attack引入拒绝服务漏洞。由于事务应在传输期间保持打开以实现最大的可移植性,因此许多慢速读取器可能通过保持打开的事务来锁定数据库表。
一种可能的方法是将blob保存到临时文件并流回文件。有关在同时编写文件时读取文件的一些想法,请参阅How do I use Java to read from a file that is actively being written?,尽管这种情况更简单,因为可以通过调用Blob#length()方法确定blob的长度。
答案 1 :(得分:1)
我现在花了一些时间来调试代码,而我在问题中的所有假设都或多或少是正确的。 @Transactional注释按预期工作,事务(Spring和DB事务)在从下载方法返回后立即提交,物理数据库连接返回到连接池,BLOB的内容显然已在稍后读取并流式传输到HTTP响应。
这仍然有效的原因是Oracle JDBC驱动程序实现的功能超出了JDBC规范所要求的范围。正如Daniel指出的那样,JDBC API文档指出"一个Blob对象在创建事务的持续时间内有效。"该文档仅指出Blob在事务期间有效,它 not 状态(由Daniel声称并且最初由我承担),Blob 不后有效结束交易。
使用普通JDBC,从同一物理连接中的两个不同事务中的两个Blob中检索InputStream,而不是在提交事务之前读取Blob数据,这表明了这种行为:
Connection conn = DriverManager.getConnection(...);
conn.setAutoCommit(false);
ResultSet rs = conn.createStatement().executeQuery("select data from ...");
rs.next();
InputStream is1 = rs.getBlob(1).getBinaryStream();
rs.close();
conn.commit();
rs = conn.createStatement().executeQuery("select data from ...");
rs.next();
InputStream is2 = rs.getBlob(1).getBinaryStream();
rs.close();
conn.commit();
int b1 = 0, b2 = 0;
while(is1.read()>=0) b1++;
while(is2.read()>=0) b2++;
System.out.println("Read " + b1 + " bytes from 1st blob");
System.out.println("Read " + b2 + " bytes from 2nd blob");
即使从同一物理连接和两个不同的事务中选择了两个Blob,也可以完全读取它们。
关闭JDBC连接(conn.close()
)确实会使Blob流无效。
答案 2 :(得分:0)
我有一个类似的相关问题,我可以确认至少在我的情况下,PostgreSQL在使用Invalid large object descriptor : 0 with autocommit
方法时抛出异常StreamingOutput
。这样做的原因是,当返回来自JAX-RS的Response
时,提交事务并且稍后执行流方法。同时文件描述符不再有效。
我已经创建了一些帮助方法,因此流媒体部分正在打开一个新事务并可以流式传输Blob。 com.foobar.model.Blob
只是一个封装blob的返回类,因此不必获取完整的实体。 findByID
是一种使用blob列上的投影并仅获取此列的方法。
在JPA和Spring事务下,JAX-RS和Blob的StreamingOutput
正在运行,但必须进行调整。我想这同样适用于JPA和EJB。
// NOTE: has to run inside a transaction to be able to stream from the DB
@Transactional
public void streamBlobToOutputStream(OutputStream outputStream, Class entityClass, String id, SingularAttribute attribute) {
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
try {
com.foobar.model.Blob blob = fooDao.findByID(id, entityClass, com.foobar.model.Blob.class, attribute);
if (blob.getBlob() == null) {
return;
}
InputStream inputStream;
try {
inputStream = blob.getBlob().getBinaryStream();
} catch (SQLException e) {
throw new RuntimeException("Could not read binary data.", e);
}
IOUtils.copy(inputStream, bufferedOutputStream);
// NOTE: the buffer must be flushed without data seems to be missing
bufferedOutputStream.flush();
} catch (Exception e) {
throw new RuntimeException("Could not send data.", e);
}
}
/**
* Builds streaming response for data which can be streamed from a Blob.
*
* @param contentType The content type. If <code>null</code> application/octet-stream is used.
* @param contentDisposition The content disposition. E.g. naming of the file download. Optional.
* @param entityClass The entity class to search in.
* @param id The Id of the entity with the blob field to stream.
* @param attribute The Blob attribute in the entity.
* @return the response builder.
*/
protected Response.ResponseBuilder buildStreamingResponseBuilder(String contentType, String contentDisposition,
Class entityClass, String id, SingularAttribute attribute) {
StreamingOutput streamingOutput = new StreamingOutput() {
@Override
public void write(OutputStream output) throws IOException, WebApplicationException {
streamBlobToOutputStream(output, entityClass, id, attribute);
}
};
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
if (contentType != null) {
mediaType = MediaType.valueOf(contentType);
}
Response.ResponseBuilder response = Response.ok(streamingOutput, mediaType);
if (contentDisposition != null) {
response.header("Content-Disposition", contentDisposition);
}
return response;
}
/**
* Stream a blob from the database.
* @param contentType The content type. If <code>null</code> application/octet-stream is used.
* @param contentDisposition The content disposition. E.g. naming of the file download. Optional.
* @param currentBlob The current blob value of the entity.
* @param entityClass The entity class to search in.
* @param id The Id of the entity with the blob field to stream.
* @param attribute The Blob attribute in the entity.
* @return the response.
*/
@Transactional
public Response streamBlob(String contentType, String contentDisposition,
Blob currentBlob, Class entityClass, String id, SingularAttribute attribute) {
if (currentBlob == null) {
return Response.noContent().build();
}
return buildStreamingResponseBuilder(contentType, contentDisposition, entityClass, id, attribute).build();
}
我还要补充一点,Hibernate下的Blob行为可能存在问题。默认情况下,Hibernate将整个实体与数据库合并,如果只更改了一个字段,即如果更新字段name
并且还有一个大的Blob image
未更改,则图像将被更新。更糟糕的是因为在合并之前如果实体被分离,Hibernate必须从DB获取Blob以确定dirty
状态。因为blob不能进行字节比较(太大),所以它们被认为是不可变的,并且相等的比较仅基于blob的对象引用。来自DB的获取对象引用将是不同的对象引用,因此尽管没有更改,但blob会再次更新。至少这是我的情况。我在实体上使用了注释@DynamicUpdate
,并编写了一个以不同方式处理blob的用户类型,并检查是否必须更新。