Return flow from Spring REST controller

I am curious if it is possible to return Stream from Spring RestController

 @RestController public class X { @RequestMapping(...) public Stream<?> getAll() { ... } } 

Is it possible to do something like this? I tried, and Spring returns something other than stream values.

Should I keep returning List<?> ?

+5
source share
2 answers

You can pass objects in Spring 5.0 / WebFlux .

Take a look at this REACTIVE Rest Controller example ( spring.main.web-application-type: "REACTIVE" ):

 @RestController public class XService { class XDto{ final int x; public XDto(int x) {this.x = x;} } Stream<XDto> produceX(){ return IntStream.range(1,10).mapToObj(i -> { System.out.println("produce "+i); try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} return new XDto(i); }); } // stream of Server-Sent Events (SSE) @GetMapping(value = "/api/x/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<XDto> getXSse() { return Flux.fromStream(produceX()); } // stream of JSON lines @GetMapping(value = "/api/x/json-stream", produces = MediaType.APPLICATION_STREAM_JSON_VALUE) public Flux<XDto> getAllJsonStream() { return Flux.fromStream(produceX()); } // same as List<XDto> - blocking JSON list @GetMapping(value = "/api/x/json-list", produces = MediaType.APPLICATION_JSON_VALUE) public Flux<XDto> getAll() { return Flux.fromStream(produceX()); } } 

Spring Framework 5.0 - WebFlux:

Spring Jet Stack Development Environment, new in 5.0, fully reactive and non-blocking. It is suitable for handling event loop style with few threads.

Server Events (SSE):

Events dispatched by a server are a standard that describes how servers can initiate data transfers to clients after the initial client connection is established.

Web Sites and Events Sent by Server / EventSource

+5
source

This can also be done using the Spring MVC Controller, but there are a few problems: limitations in the Spring Data JPA Repository, whether the database supports Holdable Cursors (ResultSet Holdability) and the Jackson version.

The key concept with which I was trying to understand is that Java 8 Stream returns a number of functions that are executed in a terminal operation, and therefore the database must be accessible in the context of the terminal operation.

Spring JPA Data Limitations

I found that the Spring Data JPA documentation does not provide enough details for Java 8. It looks like you can just declare Stream<MyObject> readAll() , but I needed to annotate the method with @Query to make it work. I was also unable to use the JPA Specification API criteria. So I had to agree to a hard-coded request, for example:

 @Query("select mo from MyObject mo where mo.foo.id in :fooIds") Stream<MyObject> readAllByFooIn(@Param("fooIds") Long[] fooIds); 

Hold cursor

If you have a database that supports Holdable Cursors, the result set is available after the transaction. This is important because we usually comment on our methods of the @Service class using @Transactional , so if your database supports portable cursors, you can access the ResultSet after returning the service method, i.e. To the @Controller method. If the database does not support portable cursors, for example. MySQL, you need to add the @Transaction annotation to the @RequestMapping controller @RequestMapping .

So now ResultSet is available outside the @Service method, right? It again depends on sustainability. For MySQL, it is available only in the @Transactional method, so the following will work (although it defeats the whole purpose of using Java 8 threads):

 @Transaction @RequestMapping(...) public List<MyObject> getAll() { try(Stream<MyObject> stream = service.streamAll) { return stream.collect(Collectors.toList()) }; } 

but not

 @Transaction @RequestMapping public Stream<MyObject> getAll() { return service.streamAll; } 

since the terminal statement is not in @Controller , this happens in Spring after the controller method returns.

Serializing a stream for JSON without supporting a supported cursor

To serialize a stream in JSON without a held cursor, add an HttpServletResponse response to the controller method, get the output stream, and use ObjectMapper to write the stream. With FasterXML 3.x you can call ObjectMapper().writeValue(writer, stream) , but with 2.8.x you need to use a stream iterator:

 @RequestMapping(...) @Transactional public void getAll(HttpServletResponse response) throws IOException { try(final Stream<MyObject> stream = service.streamAll()) { final Writer writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream())); new ObjectMapper().writerFor(Iterator.class).writeValue(writer, stream.iterator()); } } 

Next steps

My next steps are to try refactoring in Callable WebAsyncTask and moving the JSON serialization to the service.

References

0
source

Source: https://habr.com/ru/post/1258008/


All Articles