jax-rs - 离开Spring事务后从JDBC Blob中读取

标签 jax-rs blob spring-transactions

我具有以下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响应。

最佳答案

您确定交易建议正在运行吗? By default,Spring使用“代理”建议模式。仅当您使用JAX-RS Application注册了Spring代理的资源实例,或者使用的是“aspectj”编织而不是默认的“proxy”建议模式时,交易建议才会运行。

假定由于事务传播而没有重新使用physical事务,那么在此download()方法上使用@Transactional通常是不正确的。

如果事务通知实际上正在运行,则从download()方法返回时,事务结束。 Blob Javadoc说:“Blob对象在创建交易的持续时间内有效。”但是,JDBC 4.2规范的第16.3.7节说:“BlobClobNClob对象至少在创建它们的事务期间保持有效。”因此,不能保证由getBinaryStream()返回的InputStream对于提供响应有效。有效性将取决于JDBC驱动程序提供的任何保证。为了获得最大的可移植性,您应该依赖Blob仅在事务期间有效。

不管交易建议是否在运行,您都可能处于竞争状态,因为用于检索Blob的基础JDBC连接可能会以使Blob无效的方式重新使用。

编辑:测试 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读取到内存中,并且应用程序服务器最终会因内存不足而崩溃。如果getIt()方法用@Produces(MediaType.APPLICATION_OCTET_STREAM)注释,那么响应将被流回。

因此,您的download()方法可能仅由于blob是而不是被流回而起作用。 Jersey 可能正在将整个Blob读取到内存中。

相关:How to stream an endless InputStream with JAX-RS

编辑2:我已经使用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:请求 header :{Accept = [*/*],Content-类型= [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:请求contentType为:*/*
2015-06-01 15:58:14.585调试9362-[nio-8080-exec-1] o.a.c.j.interceptor.JAXRSInInterceptor:接受contentType为:*/*
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 DEBUG 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调试9362 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate:执行准备好的SQL查询
2015-06-01 15:58:14.603调试9362 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate:执行准备好的SQL语句[从图像中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:在连接上提交JDBC事务[ProxyConnection [PooledConnection [org.hsqldb.jdbc.JDBCConnection@7b191894]]]
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] o.s.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]
编码(marshal)[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]
编码(marshal)[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:响应内容类型为:application/octet-stream
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-寻址-无法从上下文检索消息寻址属性
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 DEBUG 9362 --- [nio-8080-exec-1] oacthttp.AbstractHTTPDestination:已完成对线程上的http请求的服务:Thread [http-nio-8080-exec-1,5,主要的]
2015-06-01 15:58:14.639 DEBUG 9362 --- [nio-8080-exec-1] oactservlet.ServletController:已完成对线程上的http请求的服务:Thread [http-nio-8080-exec-1,5,主要的]

我已修剪日志输出以提高可读性。需要注意的重要一点是,在发送响应之前,已提交事务并返回了JDBC连接。因此,由InputStream返回的blob.getBinaryStream()不一定有效,并且getIt() resource method可能正在调用未定义的行为。

编辑3:建议使用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的长度。

关于jax-rs - 离开Spring事务后从JDBC Blob中读取,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30356840/

相关文章:

java - 当 Bean 验证抛出 ConstraintViolationException 时自定义 JAX-RS 响应

mysql - 使用php将纯文本转换为mysql数据库,并在需要时将其作为纯文本取回

javascript - 从 blob 在 nodeJs 中创建图像文件

javascript - 如何区分 blob 和 String?

java - appCtx 中的多个事务遇到 NoUniqueBeanDefinitionException

mysql - Spring Transactions with Hibernate 和 SQL 最佳实践

jax-rs - ParamConverterProvider 方法返回类型不匹配

java - 检查 JAXBElement 参数是否为 null

java - 使用 JAX-RS 进行 JSON 解析

spring - 使用 Spring JDBC 在事务中插入新的父子记录时出错