2015-05-20 10 views
5

Tôi có việc thực hiện sơ đồ sau của một thiết bị đầu cuối dịch vụ JAX-RS:Đọc từ một JDBC Blob sau khi rời khỏi giao dịch Xuân

@GET 
@Path("...") 
@Transactional 
public Response download() { 
    java.sql.Blob blob = findBlob(...); 
    return Response.ok(blob.getBinaryStream()).build(); 
} 

Gọi JAX-RS Endpoint sẽ lấy một Blob từ cơ sở dữ liệu (thông qua JPA) và truyền kết quả lại cho máy khách HTTP. Mục đích sử dụng Blob và luồng thay vì ví dụ: JPA của ngây thơ BLOB để byte [] lập bản đồ là để ngăn chặn rằng tất cả các dữ liệu phải được lưu giữ trong bộ nhớ, nhưng thay vì dòng trực tiếp từ cơ sở dữ liệu để phản ứng HTTP.

Điều này hoạt động như dự định và tôi thực sự không hiểu tại sao. Không phải là xử lý Blob tôi nhận được từ cơ sở dữ liệu liên quan đến cả kết nối và giao dịch JDBC cơ bản? Nếu vậy, tôi đã có thể mong đợi giao dịch Spring được cam kết khi tôi trở về từ phương thức download(), làm cho việc triển khai JAX-RS không thể truy cập dữ liệu sau này từ Blob để truyền trở lại phản hồi HTTP.

Trả lời

1

Tôi đã dành một chút thời gian để gỡ lỗi mã và tất cả các giả định của tôi trong câu hỏi ít nhiều chính xác. Chú thích @Transactional hoạt động như mong đợi, giao dịch (cả giao dịch Spring và DB) được commit ngay sau khi trở về từ phương thức tải xuống, kết nối DB vật lý được trả về pool kết nối và nội dung của BLOB được đọc rõ ràng sau và được truyền tới phản hồi HTTP.

Lý do tại sao điều này vẫn hoạt động là trình điều khiển JDBC Oracle thực hiện chức năng vượt quá những gì được yêu cầu bởi đặc tả JDBC. Như Daniel đã chỉ ra, tài liệu JDBC API nói rằng "Một đối tượng Blob là hợp lệ trong suốt thời gian giao dịch trong đó được tạo ra." Tài liệu chỉ cho biết rằng Blob có giá trị trong giao dịch, trạng thái này là không phải trạng thái (như được Daniel xác nhận và ban đầu được tôi giả định), rằng Blob là không hợp lệ sau khi kết thúc giao dịch.

Sử dụng đồng bằng JDBC, lấy InputStream từ hai Blobs trong hai giao dịch khác nhau từ các kết nối vật lý giống nhau và không đọc dữ liệu Blob trước sau khi giao dịch được cam kết cho thấy hành vi này:

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"); 

Thậm chí nếu cả hai Blobs đã được chọn từ cùng một kết nối vật lý và từ trong hai giao dịch khác nhau, cả hai đều có thể được đọc hoàn toàn.

Việc đóng kết nối JDBC (conn.close()) cuối cùng sẽ làm mất hiệu lực luồng Blob.

+0

§16.3.7 của đặc tả JDBC 4.2 xác nhận giải thích của bạn rằng 'Blob' có thể hợp lệ bên ngoài giao dịch (tôi đã cập nhật câu trả lời của mình cho phù hợp).Tuy nhiên, đọc [Hướng dẫn của nhà phát triển JDBC] của Oracle (http://docs.oracle.com/database/121/JJDBC/toc.htm), tôi không thấy các bảo đảm bổ sung về tính hợp lệ của 'Blob' bên ngoài giao dịch trong được tạo/tạo. Tôi sẽ có nhiều câu hỏi như điều gì xảy ra khi kết nối được sử dụng lại và dữ liệu LOB được sửa đổi? Điều gì sẽ xảy ra khi LOB bị xóa? Điều này chỉ hoạt động trong phạm vi tìm nạp trước LOB không? Vv –

4

Bạn có chắc chắn rằng lời khuyên giao dịch đang hoạt động không? By default, Mùa xuân sử dụng chế độ lời khuyên "proxy". Lời khuyên giao dịch sẽ chỉ chạy nếu bạn đã đăng ký trường hợp tài nguyên của bạn với JAX-RS Application hoặc nếu bạn đang sử dụng chế độ dệt "aspectj" thay vì chế độ lời khuyên "proxy" mặc định.

Giả sử rằng giao dịch physical không được sử dụng lại do việc tuyên truyền giao dịch, sử dụng phương thức tải xuống()là không chính xác nói chung.

Nếu lời khuyên giao dịch thực sự đang chạy, giao dịch kết thúc khi trở về từ phương thức tải xuống(). Blob Javadoc nói:   "Đối tượng A Blob hợp lệ trong thời gian giao dịch được tạo." Tuy nhiên, §16.3.7 của đặc tả JDBC 4.2 cho biết:   "Blob, ClobNClob đối tượng vẫn còn hiệu lực trong ít nhất thời lượng của giao dịch mà chúng được tạo." Do đó, InputStream được trả về bởi getBinaryStream() không được đảm bảo là hợp lệ để phục vụ phản hồi; hiệu lực sẽ phụ thuộc vào bất kỳ bảo đảm nào được cung cấp bởi trình điều khiển JDBC. Để có tính di động tối đa, bạn nên dựa vào số Blob chỉ hợp lệ trong thời gian giao dịch.

Bất kể lời khuyên giao dịch có đang chạy hay không, bạn có thể có điều kiện chủng tộc vì kết nối JDBC cơ bản được sử dụng để truy xuất Blob có thể được sử dụng lại theo cách làm mất hiệu lực Blob.

EDIT: Testing Jersey 2.17, có vẻ như hành vi xây dựng Response từ một InputStream tùy thuộc vào loại MIME phản hồi được chỉ định. Trong một số trường hợp, InputStream được đọc hoàn toàn vào bộ nhớ trước khi gửi phản hồi. Trong các trường hợp khác, InputStream được phát lại.

Đây là trường hợp thử nghiệm của tôi:

@Path("test") 
public class MyResource { 

    @GET 
    public Response getIt() { 
     return Response.ok(new InputStream() { 
      @Override 
      public int read() throws IOException { 
       return 97; // 'a' 
      } 
     }).build(); 
    } 
} 

Nếu getIt() phương pháp được chú thích với @Produces(MediaType.TEXT_PLAIN) hoặc không @Produces chú thích, sau đó Jersey cố gắng để đọc toàn bộ (vô hạn) InputStream vào bộ nhớ và máy chủ ứng dụng cuối cùng gặp sự cố khi hết bộ nhớ. Nếu phương thức getIt() được chú thích bằng @Produces(MediaType.APPLICATION_OCTET_STREAM), thì phản hồi sẽ được phát lại.

Vì vậy, phương thức tải xuống() của bạn có thể hoạt động đơn giản vì đốm màu là không phải là đang được phát lại. Jersey có thể đang đọc toàn bộ đốm màu trong bộ nhớ.

liên quan: How to stream an endless InputStream with JAX-RS

EDIT2: Tôi đã tạo ra một dự án trình diễn sử dụng Spring Boot và Apache CXF:
https://github.com/dtrebbien/so30356840-cxf

Nếu bạn chạy dự án và thực hiện trên dòng lệnh:

 
curl 'http://localhost:8080/myapp/test/data/1' >/dev/null 

Sau đó, bạn sẽ thấy kết xuất nhật ký như sau:

 
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[[email protected]]]] 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[[email protected]]]] 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[[email protected]]]] 
2015-06-01 15:58:14.621 DEBUG 9362 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager  : Releasing JDBC Connection [ProxyConnection[PooledConnection[[email protected]]]] 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 [email protected] 

2015-06-01 15:58:14.622 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain  : Adding interceptor [email protected] to phase prepare-send 
2015-06-01 15:58:14.622 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain  : Adding interceptor [email protected] to phase marshal 
2015-06-01 15:58:14.622 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain  : Chain [email protected] 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 [email protected] 
2015-06-01 15:58:14.623 DEBUG 9362 --- [nio-8080-exec-1] o.a.cxf.phase.PhaseInterceptorChain  : Adding interceptor org.apache.cxf.inte[email protected]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 [email protected] 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 [email protected] 
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.inte[email protected]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] 

Tôi đã cắt đầu ra nhật ký để dễ đọc. Điều quan trọng cần lưu ý là giao dịch được cam kết và kết nối JDBC được trả về trước phản hồi được gửi. Do đó, InputStream được trả lại bởi blob.getBinaryStream() không nhất thiết phải hợp lệ và getIt() resource method có thể đang gọi hành vi không xác định.

EDIT3: Thực tiễn được khuyến nghị để sử dụng chú thích @Transactional của Spring là chú thích phương thức dịch vụ (xem Spring @Transactional Annotation Best Practice). Bạn có thể có một phương pháp dịch vụ tìm thấy đốm màu và chuyển dữ liệu blob đến phản hồi OutputStream. Phương thức dịch vụ có thể được chú thích bằng @Transactional để giao dịch trong đó Blob được tạo sẽ vẫn mở trong suốt thời gian chuyển. Tuy nhiên, có vẻ như với tôi rằng cách tiếp cận này có thể giới thiệu một lỗ hổng dịch vụ từ chối theo cách của "slow read" attack. Bởi vì giao dịch nên được giữ mở trong suốt thời gian chuyển giao cho tính di động tối đa, nhiều độc giả chậm có thể khóa các bảng cơ sở dữ liệu của bạn bằng cách giữ các giao dịch mở.

Một cách tiếp cận có thể là để lưu đốm màu vào tệp tạm thời và phát lại tệp. Xem How do I use Java to read from a file that is actively being written? để biết một số ý tưởng về việc đọc tệp trong khi nó đang được viết đồng thời, mặc dù trường hợp này đơn giản hơn vì độ dài của đốm màu có thể được xác định bằng cách gọi phương thức Blob#length().

+0

Bạn đang thực hiện một vài điểm hợp lệ, nhưng tôi không nghĩ nó giải thích được hành vi của tôi. Lớp thực hiện là một bean được quản lý Spring và máy chủ JAX-RS được cấu hình với Spring (sử dụng jaxrs: server trong định nghĩa ngữ cảnh XML). Tôi cũng chắc chắn 100% rằng phản hồi HTTP được truyền trực tiếp từ cơ sở dữ liệu, vì chúng tôi có kết nối mạng rất chậm giữa DB và máy chủ HTTP và kết nối nhanh giữa máy chủ HTTP và máy khách, vì vậy thật dễ dàng để xem cách khách hàng nhận được dữ liệu, rằng nó không hoàn toàn được lưu trữ bởi máy chủ HTTP trước khi gửi đến máy khách. – jarnbjo

+0

@jarnbjo: Tôi cho rằng bạn đang sử dụng Apache CXF để triển khai JAX-RS? Tôi vừa thêm một ví dụ hoàn chỉnh vào câu trả lời của tôi. –

+0

Tôi đã có thời gian để gỡ lỗi mã để tìm hiểu điều gì thực sự xảy ra. Ngay cả khi triển khai hiện tại có thể không phải là cách tiếp cận thông minh nhất và có thể có các phương pháp 'được khuyến nghị' khác, nó thực sự hoạt động. Như tôi đã chỉ ra trong câu trả lời của riêng tôi, tất cả các giả định liên quan đến quản lý giao dịch mùa xuân và xử lý dòng Apache CXF của luồng phản hồi là chính xác. Tôi đã giả định không chính xác rằng việc đọc từ luồng BLOB sẽ không thành công sau khi thực hiện giao dịch. – jarnbjo

0

Tôi đã có một vấn đề tương tự liên quan và tôi có thể xác nhận rằng ít nhất trong tình huống của tôi PostgreSQL ném một ngoại lệ Invalid large object descriptor : 0 with autocommit khi sử dụng cách tiếp cận StreamingOutput. Lý do của việc này là khi Response từ JAX-RS được trả về, giao dịch được thực hiện và phương thức phát trực tuyến đang thực hiện sau. Trong khi đó, bộ mô tả tập tin không còn giá trị nữa.

Tôi đã tạo một số phương thức trợ giúp để phần trực tuyến đang mở giao dịch mới và có thể truyền Blob. com.foobar.model.Blob chỉ là một lớp trả về gói gọn blob sao cho không phải thực thể hoàn chỉnh phải được tìm nạp. findByID là một phương pháp sử dụng phép chiếu trên cột blob và chỉ tìm nạp cột này.

Vì vậy, StreamingOutput của JAX-RS và Blob trong giao dịch JPA và Spring đang hoạt động, nhưng nó phải được tinh chỉnh. Điều tương tự cũng áp dụng cho JPA và EJB, tôi đoán vậy.

// 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(); 
} 

Tôi cũng phải thêm vào câu trả lời của mình rằng có thể có vấn đề với hành vi Blob trong Hibernate. Theo mặc định Hibernate đang hợp nhất thực thể hoàn chỉnh với DB, nếu chỉ có một trường đã được thay đổi, tức là nếu bạn cập nhật một trường name và cũng có một Blob lớn image bị ảnh hưởng, hình ảnh sẽ được cập nhật. Thậm chí tệ hơn vì trước khi hợp nhất nếu thực thể được tách rời Hibernate phải lấy Blob từ DB để xác định trạng thái dirty. Bởi vì các đốm màu không thể là byte khôn ngoan so với (quá lớn) chúng được coi là bất biến và so sánh bằng nhau chỉ dựa trên các tham chiếu đối tượng của blob. Tham chiếu đối tượng được lấy từ DB sẽ là một tham chiếu đối tượng khác, do đó, mặc dù không có gì thay đổi được blob được cập nhật lại. Ít nhất đây là tình huống của tôi. Tôi đã sử dụng chú thích @DynamicUpdate tại thực thể và đã viết loại người dùng xử lý đốm màu theo cách khác và kiểm tra xem liệu có phải được cập nhật hay không.

Các vấn đề liên quan