2015-03-03 9 views
5

Nachdem ich this article gelesen habe, möchte ich Spring verwenden, um Datenbankabfrageergebnisse direkt an eine JSON-Antwort zu streamen, um eine konstante Speicherbelegung zu gewährleisten (kein gieriges Laden eines List im Speicher).Stream verschließbare Ressource mit Spring MVC

Ähnlich wie in dem Artikel mit Hibernate, habe ich ein greetingRepository Objekt zusammengestellt, das einen Stream des Datenbankinhalts basierend auf einem JdbcTemplate zurückgibt. In dieser Implementierung schaffe ich einen Iterator über den abgefragten ResultSet, und kehre ich den Stream wie folgt:

return StreamSupport.stream(spliterator(), false).onClose(() -> { 
    log.info("Closing ResultSetIterator stream"); 
    JdbcUtils.closeResultSet(resultSet); 
}); 

dh mit einer onClose() Verfahren gewährleistet, dass der zugrunde liegende ResultSet wird geschlossen, wenn der Strom in einem try-with-resources Konstrukt deklariert :

try(Stream<Greeting> stream = greetingRepository.stream()) { 
    // operate on the stream 
} // ResultSet underlying the stream will be guaranteed to be closed 

Aber wie in dem Artikel, mag ich dieser Strom von einem benutzerdefinierten Objekt Mapper verbraucht werden (die verbesserten MappingJackson2HttpMessageConverter in dem Artikel definiert). Wenn wir die try-with-resources beiseite müssen nehmen, ist dies möglich, wie folgt:

@RequestMapping(method = GET) 
Stream<GreetingResource> stream() { 
    return greetingRepository.stream().map(GreetingResource::new); 
} 

jedoch als ein Kollege an der Unterseite dieses Artikels kommentierte dies nicht aufpasst zugrunde liegende Ressourcen zu schließen.

Im Zusammenhang mit Spring MVC, wie kann ich aus der Datenbank vollständig in eine JSON-Antwort streamen und immer noch garantieren, dass die ResultSet geschlossen wird? Können Sie eine konkrete Beispiellösung bereitstellen?

+2

Ich glaube nicht, dass Ihr Problem Ressourcenverlust ist: Spring wird definitiv die Transaktion festschreiben und die Verbindung freigeben, Ihre Ergebnismenge transitiv schließen. Aber ich erwarte das umgekehrte Problem: Wie schaffen Sie es, dass die Verbindung in der Ansichtsebene überlebt? Ich verlasse mich dafür auf "OpenSessionInViewInterceptor", was Hibernate-spezifisch ist. –

+0

Richtig, in meinem Testszenario habe ich vergessen, Transaktionen zu verwenden, mit denen ich nicht mehr auf die Ansichtsebene streamen kann (zusätzlich verwende ich MySQL, also habe ich kein Glück). Ich schließe daraus, dass das Streaming in die Ansichtsebene verlockend ist, da es möglicherweise effizient und ziemlich elegant ist, aber in der Praxis leider immer noch schwer zu verwenden ist. –

+0

Dafür gibt es leider immer noch keine ausreichende Unterstützung. Es ist die wilde Grenze. Ich hoffe aber, dass es sich durchsetzt, weil Java-Architekturen in dieser Abteilung bisher sehr fehlten. –

Antwort

0

Sie könnten ein Konstrukt erstellen, um die Abfrageausführung zur Serialisierungszeit zu verschieben. Dieses Konstrukt startet und beendet die Transaktion programmatisch.

public class TransactionalStreamable<T> { 

    private final PlatformTransactionManager platformTransactionManager; 

    private final Callable<Stream<T>> callable; 

    public TransactionalStreamable(PlatformTransactionManager platformTransactionManager, Callable<Stream<T>> callable) { 
     this.platformTransactionManager = platformTransactionManager; 
     this.callable = callable; 
    } 

    public Stream stream() { 
     TransactionTemplate txTemplate = new TransactionTemplate(platformTransactionManager); 
     txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); 
     txTemplate.setReadOnly(true); 

     TransactionStatus transaction = platformTransactionManager.getTransaction(txTemplate); 

     try { 
      return callable.call().onClose(() -> { 
       platformTransactionManager.commit(transaction); 
      }); 
     } catch (Exception e) { 
      platformTransactionManager.rollback(transaction); 
      throw new RuntimeException(e); 
     } 
    } 

    public void forEach(Consumer<T> c) { 
     try (Stream<T> s = stream()){ 
      s.forEach(c); 
     } 
    } 
} 

einen dedizierten json Serializer verwenden:

JsonSerializer<?> transactionalStreamableSer = new StdSerializer<TransactionalStreamable<?>>(TransactionalStreamable.class, true) { 
    @Override 
    public void serialize(TransactionalStreamable<?> streamable, JsonGenerator jgen, SerializerProvider provider) throws IOException { 
     jgen.writeStartArray(); 
     streamable.forEach((CheckedConsumer) e -> { 
      provider.findValueSerializer(e.getClass(), null).serialize(e, jgen, provider); 
     }); 

     jgen.writeEndArray(); 
    } 
}; 

, die wie folgt verwendet werden:

@RequestMapping(method = GET) 
TransactionalStreamable<GreetingResource> stream() { 
    return new TransactionalStreamable(platformTransactionManager ,() -> greetingRepository.stream().map(GreetingResource::new)); 
} 

Alle Arbeiten erledigt werden, wenn Jackson wird das Objekt serialisiert werden. Dies kann ein Problem in Bezug auf die Fehlerbehandlung sein oder nicht (z. B. Verwendung des Controller-Hinweises).