您确定事务通知正在运行吗? 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 无效的方式重复使用。
编辑:测试 Jersey 2.17,似乎从 InputStream 构造 Response 的行为取决于指定的响应 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 读入内存,并且应用程序服务器最终会因内存不足而崩溃。如果 getIt() 方法带有 @Produces(MediaType.APPLICATION_OCTET_STREAM) 注释,则响应会被流回。
因此,您的 download() 方法可能只是因为 blob 没有被流式传输回来。 Jersey 可能会将整个 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 调试 9362 --- [nio-8080-exec-1] org.apache.cxf.transport.http.Headers:请求标头:{Accept=[*/*],内容-类型=[null],主机=[localhost:8080],用户代理=[curl/7.37.1]}
2015-06-01 15:58:14.584 调试 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils:尝试选择资源类,请求路径:/test/data /1
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils:尝试在资源类 com.sample 上选择资源操作。资源.MyResource
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils:资源操作 getIt 可能被选中
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] org.apache.cxf.jaxrs.utils.JAXRSUtils:资源类 com.sample.resource.MyResource 上的资源操作 getIt已被选中
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor:请求路径是:/test/data/1
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor:请求 HTTP 方法是:GET
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor:请求内容类型为:*/*
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor:接受内容类型为:*/*
2015-06-01 15:58:14.585 调试 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor:找到操作:getIt
2015-06-01 15:58:14.595 调试 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager:创建名称为 [com.sample.resource.MyResource.getIt] 的新事务:PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
2015-06-01 15:58:14.595 调试 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : 为 JDBC 事务获取连接 [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]]
2015-06-01 15:58:14.596 调试 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager:切换 JDBC 连接 [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]] 到手动提交
2015-06-01 15:58:14.602 DEBUG 9362 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate:执行准备好的 SQL 查询
2015-06-01 15:58:14.603 调试 9362 --- [nio-8080-exec-1] os.jdbc.core.JdbcTemplate:执行准备好的 SQL 语句 [从图像中选择数据 WHERE id = ?]
2015-06-01 15:58:14.620 调试 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager:启动事务提交
2015-06-01 15:58:14.620 调试 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager:在连接 [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]] 上提交 JDBC 事务
2015-06-01 15:58:14.621 调试 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager :事务后释放 JDBC 连接 [ProxyConnection[PooledConnection[org.hsqldb.jdbc.JDBCConnection@7b191894]]]
2015-06-01 15:58:14.621 调试 9362 --- [nio-8080-exec-1] os.jdbc.datasource.DataSourceUtils:返回 JDBC 连接到数据源
2015-06-01 15:58:14.621 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain:在拦截器 org.apache.cxf.interceptor.OutgoingChainInterceptor@7eaf4562 上调用 handleMessage
2015-06-01 15:58:14.622 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain:添加拦截器 org.apache.cxf.interceptor.MessageSenderInterceptor@20ffeb47 到阶段准备发送
2015-06-01 15:58:14.622 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain :将拦截器 org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor@5714d386 添加到阶段元帅
2015-06-01 15:58:14.622 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain:链 org.apache.cxf.phase.PhaseInterceptorChain@11ca802c 已创建。电流:
准备发送 [MessageSenderInterceptor]
元帅 [JAXRSOutInterceptor]
2015-06-01 15:58:14.623 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain:在拦截器 org.apache.cxf.interceptor.MessageSenderInterceptor@20ffeb47 上调用 handleMessage
2015-06-01 15:58:14.623 调试 9362 --- [nio-8080-exec-1] oacxf.phase.PhaseInterceptorChain:添加拦截器 org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor@6129236d 到阶段准备 -发送结束
2015-06-01 15:58:14.623 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain:链 org.apache.cxf.phase.PhaseInterceptorChain@11ca802c 已修改。电流:
准备发送 [MessageSenderInterceptor]
元帅 [JAXRSOutInterceptor]
准备发送结束 [MessageSenderEndingInterceptor]
2015-06-01 15:58:14.623 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain:在拦截器 org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor@5714d386 上调用 handleMessage
2015-06-01 15:58:14.627 调试 9362 --- [nio-8080-exec-1] o.a.c.j.interceptor.JAXRSOutInterceptor:响应内容类型为:应用程序/八位字节流
2015-06-01 15:58:14.631 调试 9362 --- [nio-8080-exec-1] o.apache.cxf.ws.addressing.ContextUtils:从上下文属性 javax.xml.ws.addressing.context 中检索 MAP .入站
2015-06-01 15:58:14.631 调试 9362 --- [nio-8080-exec-1] o.apache.cxf.ws.addressing.ContextUtils:WS-Addressing - 无法从上下文中检索消息寻址属性
2015-06-01 15:58:14.636 调试 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain:在拦截器 org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor@6129236d 上调用 handleMessage
2015-06-01 15:58:14.639 调试 9362 --- [nio-8080-exec-1] oacthttp.AbstractHTTPDestination:已完成线程上的 http 请求服务:线程 [http-nio-8080-exec-1,5,主要的]
2015-06-01 15:58:14.639 调试 9362 --- [nio-8080-exec-1] oactservlet.ServletController:已完成线程上的 http 请求服务:线程 [http-nio-8080-exec-1,5,主要的]
为了便于阅读,我已经修剪了日志输出。需要注意的重要一点是事务已提交,并且在发送响应之前返回了 JDBC 连接。因此,blob.getBinaryStream() 返回的InputStream 不一定有效,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 的长度。