离开Spring事务后从JDBC Blob读取

时间:2015-05-20 17:46:07

标签: 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响应。

3 个答案:

答案 0 :(得分:5)

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

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

如果事务通知实际上正在运行,则从download()方法返回时事务结束。 Blob Javadoc表示:“Blob对象在创建事务的持续时间内有效。”但是,JDBC 4.2规范的§16.3.7说:“BlobClobNClob对象至少在创建它们的事务期间保持有效。”因此,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的用户类型,并检查是否必须更新。