perwendel / spark

A simple expressive web framework for java. Spark has a kotlin DSL https://github.com/perwendel/spark-kotlin
Apache License 2.0
9.64k stars 1.56k forks source link

Streaming a response to the client as it's being rendered? #1181

Open grishka opened 4 years ago

grishka commented 4 years ago

The template engine I'm using is capable of outputting into a Writer. I'd like to use that to avoid having the client to wait while the entire template is being rendered into memory and then the result transferred over the network. Additionally, I'd like my code doing that to not be weird.

Weird solution that works: render a template into response.raw().getWriter() in a route handler. Whatever is returned from the route handler is then ignored.

But then I'd like to return an object, like Spark's ModelAndView but a bit better, from a route, and have the rest handled behind the scenes to avoid unnecessary duplication. Nice solutions that don't work:

Ideally, I'd like the ability to register a global "response transformer" that's like an after filter that only runs when an object of a specific type is returned by a route, and takes the request, the response, and the returned object. Something like this:

get("/test", (req, resp)->{
    return new RenderedTemplateResponse("index").with("hello", "world");
});

responseTypeTransformer(RenderedTemplateResponse.class, (req, resp, obj)->{
    obj.renderToResponse(req, resp);
    return "";
});

// Method:
public static <T> void responseTypeTransformer(Class<T> cls, ResponseTypeTransformer<T> transformer);
// Interface:
@FunctionalInterface
public interface ResponseTypeTransformer<T>{
    public Object transform(Request request, Response response, T object);
}

But that's ideally and that's something that doesn't exist. Is there a way to achieve what I want with the existing implementation without getting too hacky?

grishka commented 4 years ago

Actually, the solution that "works" does so with a quirk. If you write something into the response from a route handler, then by the time it all reaches the after/after-after filters, the response has already been sent and you can't modify it.

grishka commented 3 years ago

Okay, it turns out the necessary infrastructure is already in place, but it's not exposed to outside. With the magic and curse of java reflection, I did this:

    private static void setupCustomSerializer(){
        try{
            Method m=Spark.class.getDeclaredMethod("getInstance");
            m.setAccessible(true);
            Service svc=(Service) m.invoke(null);
            Field serverFld=svc.getClass().getDeclaredField("server");
            serverFld.setAccessible(true);
            EmbeddedJettyServer server=(EmbeddedJettyServer) serverFld.get(svc);
            Field handlerFld=server.getClass().getDeclaredField("handler");
            handlerFld.setAccessible(true);
            JettyHandler handler=(JettyHandler) handlerFld.get(server);
            Field filterFld=handler.getClass().getDeclaredField("filter");
            filterFld.setAccessible(true);
            MatcherFilter matcher=(MatcherFilter) filterFld.get(handler);
            Field serializerChainFld=matcher.getClass().getDeclaredField("serializerChain");
            serializerChainFld.setAccessible(true);
            SerializerChain chain=(SerializerChain) serializerChainFld.get(matcher);
            Field rootFld=chain.getClass().getDeclaredField("root");
            rootFld.setAccessible(true);
            Serializer serializer=(Serializer) rootFld.get(chain);
            ExtendedStreamingSerializer mySerializer=new ExtendedStreamingSerializer();
            mySerializer.setNext(serializer);
            rootFld.set(chain, mySerializer);
        }catch(Exception x){
            x.printStackTrace();
        }
    }

ExtendedStreamingSerializer is my class that extends spark.serialization.Serializer, the two abstract methods are pretty self-explanatory.

So now this feature request becomes "let applications plug into this serializer infrastructure without hacking their way through private fields".