-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReactiveServlet.java
More file actions
206 lines (204 loc) · 7.9 KB
/
ReactiveServlet.java
File metadata and controls
206 lines (204 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// Part of Hookless Servlets: https://hookless.machinezoo.com/servlets
package com.machinezoo.hookless.servlets;
import static java.util.stream.Collectors.*;
import java.lang.reflect.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.*;
import com.machinezoo.hookless.*;
import com.machinezoo.hookless.util.*;
import com.machinezoo.stagean.*;
import jakarta.servlet.http.*;
/*
* Reactive servlet is a comfy wrapper around the incredibly messy async servlet API.
* All the app has to do is to supply a single method that may possibly reactively block.
* Reactive servlet will wait for the first non-draft response and return that to the client.
* It will do all that without blocking any threads. It also handles non-blocking reads and writes.
*
* We are deriving from HttpServlet, which has the downside of importing all the servlet cruft,
* but the advantage is that reactive servlet can be passed wherever HttpServlet is expected,
* it can be configured in XML or via annotations, and all the container-provided HttpServlet methods are accessible.
* Apps also get a chance to hook into any low-level servlet functionality if they find a reason to do so.
*/
/**
* Reactive alternative to {@link HttpServlet}.
*/
@StubDocs
@NoTests
@SuppressWarnings("serial")
public abstract class ReactiveServlet extends HttpServlet {
public ReactiveServlet() {
OwnerTrace.of(this)
.alias("servlet")
.tag("classname", getClass().getSimpleName());
}
/*
* We will overload service() and doXXX() methods with our own reactive request and response.
* This will make the API very familiar to servlet developers.
*
* We are however opting to have the response returned instead of passing it in as an output parameter.
* This is intended to encourage functional programming style that reconstructs data instead of modifying it.
* This functional API is aided by the fact that the response carries all data.
* We have thus no need for complicated output stream logic that would require the response to be an out parameter.
*
* Reactive requests and responses are so different from HttpServletRequest/Response
* that we are implementing them as root classes without inheriting from HttpServletRequest/Response.
*
* We are making service() and all doXXX() methods public to facilitate unit testing
* on reactive request/response level without having to simulate the whole servlet container.
* Direct unit testing is further facilitated by pure data nature of reactive request/response
* and reasonable defaults for reactive requests.
*
* Just like with normal servlets, doXXX methods default to returning 405 Method Not Allowed except for HEAD and OPTIONS.
* HttpServlet also provides TRACE implementation, but we choose not to for security reasons.
*/
private static ReactiveServletResponse disallowed = new ReactiveServletResponse()
.status(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
static {
/*
* HttpServlet by default sends 405 Method Not Allowed and other errors with caching enabled.
* We add Cache-Control header by default to ensure errors aren't cached.
*/
disallowed.headers().put("Cache-Control", "no-cache, no-store");
}
public ReactiveServletResponse service(ReactiveServletRequest request) {
switch (request.method()) {
case "GET":
return doGet(request);
case "HEAD":
return doHead(request);
case "POST":
return doPost(request);
case "PUT":
return doPut(request);
case "DELETE":
return doDelete(request);
case "OPTIONS":
return doOptions(request);
case "TRACE":
return doTrace(request);
default:
/*
* HttpServlet returns 501 Not Implemented instead of 405 Method Not Allowed here.
* I think it is more consistent to return 405 for unknown methods.
* It also makes it clear to the client that the problem is in the HTTP method.
*/
return disallowed;
}
}
public ReactiveServletResponse doGet(ReactiveServletRequest request) {
return disallowed;
}
public ReactiveServletResponse doPost(ReactiveServletRequest request) {
return disallowed;
}
public ReactiveServletResponse doPut(ReactiveServletRequest request) {
return disallowed;
}
public ReactiveServletResponse doDelete(ReactiveServletRequest request) {
return disallowed;
}
public ReactiveServletResponse doTrace(ReactiveServletRequest request) {
return disallowed;
}
public ReactiveServletResponse doHead(ReactiveServletRequest request) {
/*
* Just like in HttpServlet, HEAD defaults to returning GET without the body.
*/
ReactiveServletResponse response = doGet(request);
response.data(ByteBuffer.allocate(0));
return response;
}
public ReactiveServletResponse doOptions(ReactiveServletRequest request) {
/*
* Just like in HttpServlet, we return the list of supported HTTP methods
* by scanning this class for implementations of doXXX methods.
*
* OPTIONS must be included as otherwise this method wouldn't be executed.
*/
Set<String> methods = new HashSet<>(Arrays.asList("OPTIONS"));
for (Class<?> clazz = getClass(); clazz != ReactiveServlet.class; clazz = clazz.getSuperclass()) {
for (Method method : clazz.getDeclaredMethods()) {
switch (method.getName()) {
case "doGet":
/*
* If GET is implemented, then our default HEAD implementation will work too.
*/
methods.add("GET");
methods.add("HEAD");
break;
case "doHead":
/*
* HEAD can be also implemented on its own even when GET isn't.
* This scenario is very unlikely though. We support it for completeness.
*/
methods.add("HEAD");
break;
case "doPost":
methods.add("POST");
break;
case "doPut":
methods.add("PUT");
break;
case "doDelete":
methods.add("DELETE");
break;
case "doTrace":
/*
* Contrary to HttpServlet, we don't implement TRACE method.
* We will however report it as supported if application implements it.
*/
methods.add("TRACE");
break;
}
}
}
/*
* If application wishes to add more headers to OPTIONS response,
* it can override this method, call super to get the defaults, and then add any headers it wants.
*
* Our response by default includes Cache-Control to avoid surprisingly long caching.
*/
ReactiveServletResponse response = new ReactiveServletResponse();
response.headers().put("Allow", methods.stream().sorted().collect(joining(", ")));
response.headers().put("Cache-Control", "no-cache, no-store");
return response;
}
/*
* All request handling is actually performed in ReactiveServletTask.
* We need to instantiate an object for every request,
* because reactive request handling might take some time and
* we need to store intermediate state somewhere meantime.
*/
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) {
Objects.requireNonNull(request);
Objects.requireNonNull(response);
new ReactiveServletTask(ReactiveServlet.this, request, response).start();
}
/*
* We allow configuring custom executor for the reactive code.
* This is important for servlets that might do heavy processing or blocking,
* which is a poor fit for the main hookless thread pool.
* Networking code will still run on servlet container's thread pool.
*
* We have a choice between fluent style and bean style getters/setters.
* Bean style is consistent with HttpServlet while fluent style
* is consistent with ReactiveServletRequest and ReactiveServletResponse.
* XML and other dynamic configuration is irrelevant,
* because ExecutorService is not something to be configured in XML.
* We are opting for fluent style to keep the whole reactive servlet API consistent.
*/
private Executor executor = ReactiveExecutor.common();
public Executor executor() {
return executor;
}
public void executor(Executor executor) {
Objects.requireNonNull(executor);
this.executor = executor;
}
@Override
public String toString() {
return OwnerTrace.of(this).toString();
}
}