2015-05-20 2 views
5

Ich habe die folgende schematische Implementierung eines JAX-RS Service-Endpunkt:von einem JDBC Blob Lesen nach dem Frühlings Transaktion verlassen

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

die JAX-RS-Endpunkt (über JPA einen Blob aus der Datenbank holen aufrufen) und streamen das Ergebnis zurück zum HTTP-Client. Der Zweck der Verwendung eines Blobs und eines Streams anstelle von z.B. Das naive BLOB to byte [] - Mapping von JPA verhindert, dass alle Daten im Speicher gehalten werden müssen, sondern stattdessen direkt von der Datenbank in die HTTP-Antwort strömen.

Das funktioniert wie vorgesehen und ich verstehe eigentlich nicht warum. Ist das Blob-Handle nicht von der Datenbank, die sowohl der zugrunde liegenden JDBC-Verbindung als auch der Transaktion zugeordnet ist? Wenn dies der Fall wäre, hätte ich erwartet, dass die Spring-Transaktion bei der Rückkehr von der download() - Methode ausgeführt wird, was es der JAX-RS-Implementierung unmöglich macht, später auf Daten aus dem Blob zuzugreifen, um sie zurück zur HTTP-Antwort zu streamen.

Antwort

1

Ich habe einige Zeit damit verbracht, den Code zu debuggen, und alle meine Annahmen in der Frage sind mehr oder weniger korrekt. Die @Transactional-Annotation funktioniert wie erwartet, die Transaktion (sowohl die Spring- als auch die DB-Transaktion) wird unmittelbar nach der Rückkehr von der Download-Methode übergeben, die physische DB-Verbindung wird an den Verbindungspool zurückgegeben und der Inhalt des BLOB wird offensichtlich später gelesen und gestreamt zu der HTTP-Antwort.

Der Grund, warum dies immer noch funktioniert, ist, dass der Oracle JDBC-Treiber Funktionen implementiert, die über die Anforderungen der JDBC-Spezifikation hinausgehen. Wie Daniel hervorhob, gibt die JDBC-API-Dokumentation an, dass "ein Blob-Objekt für die Dauer der Transaktion gültig ist, in der es erstellt wurde". Die Dokumentation besagt nur, dass das Blob während der Transaktion gültig ist, es tut nicht Staat (wie von Daniel behauptet und zunächst von mir übernommen), dass der Blob nicht gültig nach dem Beenden der Transaktion ist.

Mit Ebene JDBC, in zwei verschiedenen Transaktionen aus der gleichen physikalischen Verbindung die Input von zwei Blobs Abrufen und nicht die Blob-Daten vor, nachdem die Transaktionen zu lesen sind, veranschaulicht dieses Verhalten verpflichtet:

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

Auch wenn beide Blobs Aus derselben physischen Verbindung und aus zwei verschiedenen Transaktionen ausgewählt, können beide vollständig gelesen werden.

Durch das Schließen der JDBC-Verbindung (conn.close()) werden jedoch die Blob-Streams endgültig ungültig.

+0

§16.3.7 der Spezifikation JDBC 4.2 bestätigt Ihre Interpretation, dass ein 'Blob' außerhalb der Transaktion gültig sein kann (ich habe meine Antwort entsprechend aktualisiert).Beim Lesen von JDBC Developer's Guide (http://docs.oracle.com/database/121/JJDBC/toc.htm) von Oracle sehe ich jedoch keine zusätzlichen Garantien bezüglich der Gültigkeit eines "Blobs" außerhalb der Transaktion in wo es erstellt wurde Ich hätte viele Fragen, beispielsweise was passiert, wenn die Verbindung wiederverwendet wird und die LOB-Daten geändert werden? Was passiert, wenn das LOB gelöscht wird? Funktioniert das nur innerhalb der LOB-Prefetch-Größe? Etc. –

4

Sind Sie sicher, dass der Transaktionsrat ausgeführt wird? By default, verwendet Spring den "Proxy" -Ratenmodus. Der Transaktionshinweis wird nur ausgeführt, wenn Sie die Springproxied-Instanz Ihrer Ressource mit JAX-RS registriert haben oder wenn Sie "aspectj" -Weben anstelle des Standardberatungsmodus "Proxy" verwendet haben.

Angenommen, eine physical-Transaktion wird nicht als Ergebnis der Transaktionsausbreitung wiederverwendet. Die Verwendung von @Transactional für diese download() -Methode ist im Allgemeinen falsch.

Wenn der Transaktionshinweis tatsächlich ausgeführt wird, endet die Transaktion beim Zurückkehren von der download() - Methode. Die Blob Javadoc sagt:   "Ein Blob Objekt ist gültig für die Dauer der Transaktion, in der wurde erstellt wurde." In §16.3.7 der Spezifikation JDBC 4.2 heißt es jedoch:   "Blob, Clob und Objekte bleiben mindestens für die Dauer der Transaktion, in der sie erstellt werden, gültig." Daher ist die von getBinaryStream() zurückgegebene InputStream nicht garantiert gültig für die Antwort; Die Gültigkeit hängt von den vom JDBC-Treiber bereitgestellten Garantien ab. Für maximale Portabilität sollten Sie sich darauf verlassen, dass Blob nur für die Dauer der Transaktion gültig ist.

Unabhängig davon, ob der Transaktionshinweis ausgeführt wird, haben Sie möglicherweise eine Racebedingung, da die zugrunde liegende JDBC-Verbindung, die zum Abrufen der Blob verwendet wurde, möglicherweise so wiederverwendet wird, dass Blob ungültig wird.

EDIT: Testing Jersey 2.17, scheint es, dass das Verhalten eines Response von einem InputStream der Konstruktion auf die Antwort MIME-Typ angegeben abhängt. In einigen Fällen wird der InputStream zuerst vollständig in den Speicher gelesen, bevor die Antwort gesendet wird. In anderen Fällen wird der InputStream zurückgestreamt.

Hier ist mein Testfall:

@Path("test") 
public class MyResource { 

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

Wenn die getIT() -Methode mit @Produces(MediaType.TEXT_PLAIN) oder keine @Produces Anmerkung kommentiert wird, dann versucht Jersey die gesamte (unendlich) InputStream in den Speicher und den Anwendungsserver schließlich zu lesen stürzt ab, weil der Speicher nicht mehr ausreicht. Wenn die Methode getIt() mit @Produces(MediaType.APPLICATION_OCTET_STREAM) annotiert ist, wird die Antwort zurückgestreamt.

So funktioniert Ihre download() - Methode möglicherweise einfach, weil der Blob nicht zurückgestreamt wird. Jersey könnte den gesamten Blob in Erinnerung behalten.

Verwandte: How to stream an endless InputStream with JAX-RS

EDIT2: Ich habe ein Demonstrationsprojekt mit Spring-Boot und Apache CXF erstellt:
https://github.com/dtrebbien/so30356840-cxf

Wenn Sie das Projekt und führen Sie auf der Kommandozeile:

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

Dann sehen Sie eine Protokollausgabe wie folgt:

 
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[org.hsqldb.jdbc.JDBCConnect[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] 

Ich habe die Protokollausgabe zur besseren Lesbarkeit getrimmt. Die wichtige Sache zu beachten ist, dass die Transaktion festgeschrieben wird und die JDBC-Verbindung zurückgegeben wird, bevor die Antwort gesendet wird. Daher ist die InputStream, die von blob.getBinaryStream() zurückgegeben wird, nicht unbedingt gültig und die getIt() resource method kann undefiniertes Verhalten aufrufen.

EDIT3: Eine Praxis empfohlen @Transactional Anmerkung Spring für die Verwendung ist die Service-Methode (siehe Spring @Transactional Annotation Best Practice) mit Anmerkungen zu versehen. Sie könnten eine Servicemethode haben, die den Blob findet und die Blobdaten an die Antwort OutputStream überträgt. Die Service-Methode könnte mit @Transactional kommentiert werden, so dass die Transaktion, in der die Blob erstellt wird, für die Dauer der Übertragung geöffnet bleiben würde. Es scheint mir jedoch, dass dieser Ansatz eine Denial-of-Service-Sicherheitslücke durch eine "slow read" attack einleiten könnte. Da die Transaktion für die Dauer der Übertragung für maximale Portabilität geöffnet bleiben sollte, könnten zahlreiche langsame Leser Ihre Datenbanktabelle (n) sperren, indem sie offene Transaktionen halten.

Ein möglicher Ansatz besteht darin, den Blob in einer temporären Datei zu speichern und die Datei zurückzustreamen. Einige Ideen zum Lesen einer Datei, während sie gleichzeitig geschrieben wird, finden Sie unter How do I use Java to read from a file that is actively being written?. Dieser Fall ist jedoch einfacher, da die Länge des Blobs durch Aufruf der Methode Blob#length() ermittelt werden kann.

+0

Sie machen ein paar gültige Punkte, aber ich denke nicht, dass es mein Verhalten erklärt. Die implementierende Klasse ist eine von Spring verwaltete Bean, und der JAX-RS-Server ist mit Spring konfiguriert (in der XML-Kontextdefinition wird jaxrs: server verwendet). Ich bin auch 100% sicher, dass die HTTP-Antwort von der Datenbank gestreamt wird, da wir eine sehr langsame Netzwerkverbindung zwischen DB und HTTP-Server und schnelle Verbindungen zwischen HTTP-Server und Clients haben, so ist es einfach zu sehen, auf welche Weise der Client empfängt Daten, die nicht vollständig vom HTTP-Server zwischengespeichert werden, bevor sie an den Client gesendet werden. – jarnbjo

+0

@jarnbjo: Ich nehme an, dass Sie Apache CXF für die JAX-RS-Implementierung verwenden? Ich habe meiner Antwort ein vollständiges Beispiel hinzugefügt. –

+0

Ich hatte jetzt Zeit, den Code zu debuggen, um herauszufinden, was tatsächlich passiert. Auch wenn die aktuelle Implementierung möglicherweise nicht der cleverste Ansatz ist und es möglicherweise andere "empfohlene" Vorgehensweisen gibt, funktioniert es tatsächlich. Wie ich in meiner eigenen Antwort darauf hingewiesen habe, waren alle Annahmen bezüglich des Spring-Transaktionsmanagements und der Apache CXF-Behandlung des Antwortstreams korrekt. Ich hatte fälschlicherweise angenommen, dass das Lesen aus dem BLOB-Stream nach dem Ausführen der Transaktion fehlschlagen wird. – jarnbjo

0

Ich hatte ein ähnliches Problem und ich kann bestätigen, dass zumindest in meiner Situation PostgreSQL eine Ausnahme Invalid large object descriptor : 0 with autocommit bei Verwendung der StreamingOutput Ansatz löst. Der Grund dafür ist, dass wenn die Response von JAX-RS zurückgegeben wird die Transaktion festgeschrieben wird und die Streaming-Methode später ausgeführt wird. In der Zwischenzeit ist der Dateideskriptor nicht mehr gültig.

Ich habe eine Hilfsmethode erstellt, so dass der Streaming-Teil eine neue Transaktion öffnet und den Blob streamen kann. com.foobar.model.Blob ist nur eine Rückgabeklasse, die den Blob einkapselt, sodass nicht die vollständige Entität abgerufen werden muss. findByID ist eine Methode, die eine Projektion auf die BLOB-Spalte verwendet und nur diese Spalte abruft.

Also StreamingOutput von JAX-RS und Blob unter JPA und Spring-Transaktionen funktionieren, aber es muss optimiert werden. Das gleiche gilt für JPA und EJB, denke ich.

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

Ich muss auch meine Antwort hinzufügen, dass es ein Problem mit dem Blob-Verhalten unter Hibernate geben könnte. Standardmäßig führt Hibernate die komplette Entität mit der DB zusammen, auch wenn nur ein Feld geändert wurde, d. H. Wenn Sie ein Feld name aktualisieren und auch einen großen Blob image unberührt haben, wird das Bild aktualisiert. Noch schlimmer, weil Hibernate vor dem Zusammenführen, wenn die Entität getrennt ist, den Blob aus der DB holen muss, um den Status dirty zu bestimmen. Da Blobs nicht byteweise verglichen werden können (zu groß), werden sie als unveränderlich betrachtet, und der Gleichheitsvergleich basiert nur auf der Objektreferenz des Blobs. Die abgerufene Objektreferenz aus der DB ist eine andere Objektreferenz. Obwohl nichts geändert wurde, wird das Blob erneut aktualisiert. Zumindest war das die Situation für mich. Ich habe die Annotation @DynamicUpdate bei der Entität verwendet und einen Benutzertyp geschrieben, der den Blob auf eine andere Weise behandelt, und überprüft, ob dieser aktualisiert werden muss.