Skip to content

Commit 074ed04

Browse files
authored
Merge pull request #247 from donbeave/master
Added ability to cache the whole graphql response
2 parents 88a7614 + 2902ee7 commit 074ed04

13 files changed

+482
-17
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ target/
1010
.project
1111
.settings
1212
bin
13+
.DS_Store

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AbstractGraphQLHttpServlet.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import graphql.kickstart.execution.GraphQLQueryInvoker;
88
import graphql.kickstart.execution.GraphQLRequest;
99
import graphql.kickstart.execution.input.GraphQLSingleInvocationInput;
10+
import graphql.kickstart.servlet.cache.CachingHttpRequestHandlerImpl;
1011
import graphql.schema.GraphQLFieldDefinition;
1112
import graphql.kickstart.servlet.core.GraphQLMBean;
1213
import graphql.kickstart.servlet.core.GraphQLServletListener;
@@ -83,7 +84,11 @@ protected GraphQLConfiguration getConfiguration() {
8384
public void init() {
8485
if (configuration == null) {
8586
this.configuration = getConfiguration();
86-
this.requestHandler = HttpRequestHandlerFactory.create(configuration);
87+
if (configuration.getResponseCacheManager() != null) {
88+
this.requestHandler = new CachingHttpRequestHandlerImpl(configuration);
89+
} else {
90+
this.requestHandler = HttpRequestHandlerFactory.create(configuration);
91+
}
8792
}
8893
}
8994

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/BatchedQueryResponseWriter.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
import graphql.ExecutionResult;
44
import graphql.kickstart.execution.GraphQLObjectMapper;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
8+
import javax.servlet.http.HttpServletRequest;
9+
import javax.servlet.http.HttpServletResponse;
510
import java.io.IOException;
611
import java.nio.charset.StandardCharsets;
712
import java.util.Iterator;
813
import java.util.List;
9-
import javax.servlet.http.HttpServletRequest;
10-
import javax.servlet.http.HttpServletResponse;
11-
import lombok.RequiredArgsConstructor;
1214

15+
@Slf4j
1316
@RequiredArgsConstructor
1417
class BatchedQueryResponseWriter implements QueryResponseWriter {
1518

@@ -34,6 +37,7 @@ public void write(HttpServletRequest request, HttpServletResponse response) thro
3437

3538
String responseContent = responseBuilder.toString();
3639
byte[] contentBytes = responseContent.getBytes(StandardCharsets.UTF_8);
40+
3741
response.setContentLength(contentBytes.length);
3842
response.getOutputStream().write(contentBytes);
3943
}

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLConfiguration.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import graphql.kickstart.execution.GraphQLObjectMapper;
55
import graphql.kickstart.execution.GraphQLQueryInvoker;
66
import graphql.kickstart.execution.context.ContextSetting;
7+
import graphql.kickstart.servlet.cache.GraphQLResponseCacheManager;
78
import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider;
89
import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider;
910
import graphql.kickstart.servlet.context.GraphQLServletContextBuilder;
@@ -36,12 +37,13 @@ public class GraphQLConfiguration {
3637
@Getter
3738
private final long asyncTimeout;
3839
private final ContextSetting contextSetting;
40+
private final GraphQLResponseCacheManager responseCacheManager;
3941

4042
private GraphQLConfiguration(GraphQLInvocationInputFactory invocationInputFactory,
4143
GraphQLQueryInvoker queryInvoker,
4244
GraphQLObjectMapper objectMapper, List<GraphQLServletListener> listeners, boolean asyncServletModeEnabled,
4345
Executor asyncExecutor, long subscriptionTimeout, long asyncTimeout, ContextSetting contextSetting,
44-
Supplier<BatchInputPreProcessor> batchInputPreProcessor) {
46+
Supplier<BatchInputPreProcessor> batchInputPreProcessor, GraphQLResponseCacheManager responseCacheManager) {
4547
this.invocationInputFactory = invocationInputFactory;
4648
this.queryInvoker = queryInvoker;
4749
this.graphQLInvoker = queryInvoker.toGraphQLInvoker();
@@ -53,6 +55,7 @@ private GraphQLConfiguration(GraphQLInvocationInputFactory invocationInputFactor
5355
this.asyncTimeout = asyncTimeout;
5456
this.contextSetting = contextSetting;
5557
this.batchInputPreProcessor = batchInputPreProcessor;
58+
this.responseCacheManager = responseCacheManager;
5659
}
5760

5861
public static GraphQLConfiguration.Builder with(GraphQLSchema schema) {
@@ -113,6 +116,10 @@ public BatchInputPreProcessor getBatchInputPreProcessor() {
113116
return batchInputPreProcessor.get();
114117
}
115118

119+
public GraphQLResponseCacheManager getResponseCacheManager() {
120+
return responseCacheManager;
121+
}
122+
116123
public static class Builder {
117124

118125
private GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder;
@@ -126,6 +133,7 @@ public static class Builder {
126133
private long asyncTimeout = 30;
127134
private ContextSetting contextSetting = ContextSetting.PER_QUERY_WITH_INSTRUMENTATION;
128135
private Supplier<BatchInputPreProcessor> batchInputPreProcessorSupplier = NoOpBatchInputPreProcessor::new;
136+
private GraphQLResponseCacheManager responseCacheManager;
129137

130138
private Builder(GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder) {
131139
this.invocationInputFactoryBuilder = invocationInputFactoryBuilder;
@@ -209,6 +217,11 @@ public Builder with(Supplier<BatchInputPreProcessor> batchInputPreProcessor) {
209217
return this;
210218
}
211219

220+
public Builder with(GraphQLResponseCacheManager responseCache) {
221+
this.responseCacheManager = responseCache;
222+
return this;
223+
}
224+
212225
public GraphQLConfiguration build() {
213226
return new GraphQLConfiguration(
214227
this.invocationInputFactory != null ? this.invocationInputFactory : invocationInputFactoryBuilder.build(),
@@ -220,7 +233,8 @@ public GraphQLConfiguration build() {
220233
subscriptionTimeout,
221234
asyncTimeout,
222235
contextSetting,
223-
batchInputPreProcessorSupplier
236+
batchInputPreProcessorSupplier,
237+
responseCacheManager
224238
);
225239
}
226240

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestHandlerImpl.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
import lombok.extern.slf4j.Slf4j;
1515

1616
@Slf4j
17-
class HttpRequestHandlerImpl implements HttpRequestHandler {
17+
public class HttpRequestHandlerImpl implements HttpRequestHandler {
1818

1919
private final GraphQLConfiguration configuration;
2020
private final GraphQLInvoker graphQLInvoker;
2121

22-
HttpRequestHandlerImpl(GraphQLConfiguration configuration) {
22+
public HttpRequestHandlerImpl(GraphQLConfiguration configuration) {
2323
this.configuration = configuration;
2424
graphQLInvoker = configuration.getGraphQLInvoker();
2525
}
@@ -46,15 +46,19 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr
4646
}
4747
}
4848

49-
private void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request,
49+
protected void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request,
5050
HttpServletResponse response) throws IOException {
5151
GraphQLQueryResult queryResult = invoke(invocationInput, request, response);
5252

53-
QueryResponseWriter queryResponseWriter = QueryResponseWriter.createWriter(queryResult, configuration.getObjectMapper(),
54-
configuration.getSubscriptionTimeout());
53+
QueryResponseWriter queryResponseWriter = createWriter(invocationInput, queryResult);
5554
queryResponseWriter.write(request, response);
5655
}
5756

57+
protected QueryResponseWriter createWriter(GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult) {
58+
return QueryResponseWriter.createWriter(queryResult, configuration.getObjectMapper(),
59+
configuration.getSubscriptionTimeout());
60+
}
61+
5862
private GraphQLQueryResult invoke(GraphQLInvocationInput invocationInput, HttpServletRequest request,
5963
HttpServletResponse response) {
6064
if (invocationInput instanceof GraphQLSingleInvocationInput) {

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/QueryResponseWriter.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package graphql.kickstart.servlet;
22

3-
import graphql.kickstart.execution.GraphQLQueryResult;
43
import graphql.kickstart.execution.GraphQLObjectMapper;
5-
import java.io.IOException;
6-
import java.util.Objects;
4+
import graphql.kickstart.execution.GraphQLQueryResult;
5+
76
import javax.servlet.http.HttpServletRequest;
87
import javax.servlet.http.HttpServletResponse;
8+
import java.io.IOException;
9+
import java.util.Objects;
910

10-
interface QueryResponseWriter {
11+
public interface QueryResponseWriter {
1112

1213
static QueryResponseWriter createWriter(
1314
GraphQLQueryResult result,

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/SingleQueryResponseWriter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import graphql.kickstart.execution.GraphQLObjectMapper;
55
import lombok.RequiredArgsConstructor;
66

7-
import java.io.IOException;
8-
import java.nio.charset.StandardCharsets;
97
import javax.servlet.http.HttpServletRequest;
108
import javax.servlet.http.HttpServletResponse;
9+
import java.io.IOException;
10+
import java.nio.charset.StandardCharsets;
1111

1212
@RequiredArgsConstructor
1313
class SingleQueryResponseWriter implements QueryResponseWriter {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package graphql.kickstart.servlet.cache;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
import javax.servlet.ServletOutputStream;
6+
import javax.servlet.WriteListener;
7+
import javax.servlet.http.HttpServletResponse;
8+
import javax.servlet.http.HttpServletResponseWrapper;
9+
import java.io.ByteArrayOutputStream;
10+
import java.io.IOException;
11+
import java.io.OutputStream;
12+
import java.io.OutputStreamWriter;
13+
import java.io.PrintWriter;
14+
15+
@Slf4j
16+
public class BufferedHttpServletResponse extends HttpServletResponseWrapper {
17+
18+
private static final class BufferedOutputStream extends ServletOutputStream {
19+
20+
private final OutputStream delegate;
21+
private final ByteArrayOutputStream buf = new ByteArrayOutputStream();
22+
23+
public BufferedOutputStream(OutputStream delegate) {
24+
this.delegate = delegate;
25+
}
26+
27+
public void write(int b) throws IOException {
28+
buf.write(b);
29+
delegate.write(b);
30+
}
31+
32+
@Override
33+
public void flush() throws IOException {
34+
buf.flush();
35+
delegate.flush();
36+
}
37+
38+
@Override
39+
public void close() throws IOException {
40+
buf.close();
41+
delegate.close();
42+
}
43+
44+
@Override
45+
public boolean isReady() {
46+
return true;
47+
}
48+
49+
@Override
50+
public void setWriteListener(WriteListener writeListener) {
51+
}
52+
53+
public byte[] toByteArray() {
54+
return buf.toByteArray();
55+
}
56+
57+
}
58+
59+
private BufferedOutputStream copier;
60+
61+
private ServletOutputStream outputStream;
62+
private PrintWriter writer;
63+
private String errorMessage;
64+
65+
public BufferedHttpServletResponse(HttpServletResponse response) {
66+
super(response);
67+
}
68+
69+
@Override
70+
public void sendError(int sc, String msg) throws IOException {
71+
errorMessage = msg;
72+
super.sendError(sc, msg);
73+
}
74+
75+
@Override
76+
public void sendError(int sc) throws IOException {
77+
sendError(sc, null);
78+
}
79+
80+
@Override
81+
public ServletOutputStream getOutputStream() throws IOException {
82+
if (writer != null) {
83+
throw new IllegalStateException("getWriter() has already been called on this response.");
84+
}
85+
86+
if (outputStream == null) {
87+
outputStream = getResponse().getOutputStream();
88+
copier = new BufferedOutputStream(outputStream);
89+
}
90+
91+
return copier;
92+
}
93+
94+
@Override
95+
public PrintWriter getWriter() throws IOException {
96+
if (outputStream != null) {
97+
throw new IllegalStateException("getOutputStream() has already been called on this response.");
98+
}
99+
100+
if (writer == null) {
101+
copier = new BufferedOutputStream(getResponse().getOutputStream());
102+
writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
103+
}
104+
105+
return writer;
106+
}
107+
108+
@Override
109+
public void flushBuffer() throws IOException {
110+
if (writer != null) {
111+
writer.flush();
112+
} else if (copier != null) {
113+
copier.flush();
114+
}
115+
}
116+
117+
@Override
118+
public boolean isCommitted() {
119+
return false;
120+
}
121+
122+
public void close() throws IOException {
123+
if (writer != null) {
124+
writer.close();
125+
} else if (copier != null) {
126+
copier.close();
127+
}
128+
}
129+
130+
public String getErrorMessage() {
131+
return errorMessage;
132+
}
133+
134+
public byte[] getContentAsByteArray() {
135+
if (copier != null) {
136+
return copier.toByteArray();
137+
} else {
138+
return new byte[0];
139+
}
140+
}
141+
142+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package graphql.kickstart.servlet.cache;
2+
3+
import graphql.kickstart.execution.input.GraphQLInvocationInput;
4+
import graphql.kickstart.servlet.HttpRequestHandler;
5+
import lombok.extern.slf4j.Slf4j;
6+
7+
import javax.servlet.http.HttpServletRequest;
8+
import javax.servlet.http.HttpServletResponse;
9+
import java.io.IOException;
10+
import java.nio.charset.StandardCharsets;
11+
12+
@Slf4j
13+
public final class CacheReader {
14+
15+
private CacheReader() {
16+
}
17+
18+
/**
19+
* Response from cache if possible, if nothing in cache will not produce any response
20+
*
21+
* @return {@literal true} if response was fulfilled from cache, {@literal false} is cache not found or an error
22+
* occurred while reading value from cache
23+
* @throws IOException if can not read value from the cache
24+
*/
25+
public static boolean responseFromCache(GraphQLInvocationInput invocationInput,
26+
HttpServletRequest request,
27+
HttpServletResponse response,
28+
GraphQLResponseCacheManager cacheManager) throws IOException {
29+
CachedResponse cachedResponse = null;
30+
try {
31+
cachedResponse = cacheManager.get(request, invocationInput);
32+
} catch (Throwable t) {
33+
log.warn("Ignore read from cache, unexpected error happened", t);
34+
}
35+
36+
if (cachedResponse != null) {
37+
write(response, cachedResponse);
38+
return true;
39+
}
40+
41+
return false;
42+
}
43+
44+
private static void write(HttpServletResponse response, CachedResponse cachedResponse)
45+
throws IOException {
46+
if (cachedResponse.isError()) {
47+
response.sendError(cachedResponse.getErrorStatusCode(), cachedResponse.getErrorMessage());
48+
} else {
49+
response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8);
50+
response.setStatus(HttpRequestHandler.STATUS_OK);
51+
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
52+
response.setContentLength(cachedResponse.getContentBytes().length);
53+
response.getOutputStream().write(cachedResponse.getContentBytes());
54+
}
55+
}
56+
57+
}

0 commit comments

Comments
 (0)