Skip to content

Commit bd3c2d8

Browse files
committed
Avoid concurrent access to JavaScript objects
1 parent 0a9f733 commit bd3c2d8

File tree

2 files changed

+74
-42
lines changed

2 files changed

+74
-42
lines changed

src/main/java/com/example/post/ssr/ReactRenderer.java

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
import java.nio.file.Files;
99
import java.nio.file.Path;
1010
import java.nio.file.Paths;
11+
import java.util.ArrayList;
12+
import java.util.List;
1113
import java.util.Map;
1214
import java.util.Objects;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.concurrent.ConcurrentMap;
1317

1418
import com.fasterxml.jackson.databind.ObjectMapper;
1519
import org.graalvm.polyglot.Context;
@@ -19,6 +23,7 @@
1923
import org.slf4j.Logger;
2024
import org.slf4j.LoggerFactory;
2125

26+
import org.springframework.core.NamedInheritableThreadLocal;
2227
import org.springframework.core.NativeDetector;
2328
import org.springframework.core.io.ClassPathResource;
2429
import org.springframework.core.io.FileSystemResource;
@@ -31,50 +36,35 @@
3136
@Component
3237
public class ReactRenderer implements AutoCloseable {
3338

34-
private final Context context;
35-
36-
private final Value render;
39+
private final ThreadLocal<Value> renderHolder = new NamedInheritableThreadLocal<>("React Render");
3740

3841
private final String template;
3942

4043
private final ObjectMapper objectMapper;
4144

45+
private static final ConcurrentMap<String, File> fileMap = new ConcurrentHashMap<>();
46+
47+
private final List<Context> contexts = new ArrayList<>();
48+
4249
private static final Logger log = LoggerFactory.getLogger(ReactRenderer.class);
4350

4451
public ReactRenderer(ObjectMapper objectMapper) throws IOException {
4552
this.objectMapper = objectMapper;
46-
this.context = Context.newBuilder("js")
47-
.allowIO(IOAccess.ALL)
48-
.allowExperimentalOptions(true)
49-
.option("js.esm-eval-returns-exports", "true")
50-
.option("js.commonjs-require", "true")
51-
.option("js.commonjs-require-cwd", getRoot("polyfill").getAbsolutePath())
52-
.option("js.commonjs-core-modules-replacements",
53-
"stream:stream-browserify,util:fastestsmallesttextencoderdecoder,buffer:buffer/")
54-
.build();
55-
this.context.eval("js", """
56-
globalThis.Buffer = require('buffer').Buffer;
57-
globalThis.URL = require('whatwg-url-without-unicode').URL;
58-
globalThis.process = {
59-
env: {
60-
NODE_ENV: 'production'
61-
}
62-
};
63-
globalThis.document = {};
64-
global = globalThis;
65-
""");
66-
Path code = Paths.get(getRoot("server").getAbsolutePath(), "main-server.js");
67-
Source source = Source.newBuilder("js", code.toFile()).mimeType("application/javascript+module").build();
68-
Value exports = this.context.eval(source);
69-
this.render = exports.getMember("render");
7053
this.template = Files.readString(Paths.get(getRoot("META-INF/resources").getAbsolutePath(), "index.html"));
71-
;
54+
// pre-computing
55+
getRoot("polyfill");
56+
getRoot("server");
7257
}
7358

7459
public String render(String url, Map<String, Object> input) {
60+
Value render = this.renderHolder.get();
61+
if (render == null) {
62+
render = createRender();
63+
renderHolder.set(render);
64+
}
7565
try {
7666
String s = this.objectMapper.writeValueAsString(input);
77-
Value executed = this.render.execute(url, s);
67+
Value executed = render.execute(url, s);
7868
Value head = executed.getMember("head");
7969
Value html = executed.getMember("html");
8070
return this.template
@@ -89,21 +79,63 @@ public String render(String url, Map<String, Object> input) {
8979
}
9080
}
9181

92-
static File getRoot(String root) throws IOException {
93-
if (NativeDetector.inNativeImage()) {
94-
// in native image
95-
return copyResources(root).toFile();
96-
}
97-
ClassPathResource resource = new ClassPathResource(root);
98-
if (resource.getURL().toString().startsWith("jar:")) {
99-
// in jar file
100-
return new FileSystemResource("./target/classes/" + root).getFile();
82+
Value createRender() {
83+
log.trace("createRender");
84+
try {
85+
Context context = Context.newBuilder("js")
86+
.allowIO(IOAccess.ALL)
87+
.allowExperimentalOptions(true)
88+
.option("js.esm-eval-returns-exports", "true")
89+
.option("js.commonjs-require", "true")
90+
.option("js.commonjs-require-cwd", getRoot("polyfill").getAbsolutePath())
91+
.option("js.commonjs-core-modules-replacements",
92+
"stream:stream-browserify,util:fastestsmallesttextencoderdecoder,buffer:buffer/")
93+
.build();
94+
context.eval("js", """
95+
globalThis.Buffer = require('buffer').Buffer;
96+
globalThis.URL = require('whatwg-url-without-unicode').URL;
97+
globalThis.process = {
98+
env: {
99+
NODE_ENV: 'production'
100+
}
101+
};
102+
globalThis.document = {};
103+
global = globalThis;
104+
""");
105+
Path code = Paths.get(getRoot("server").getAbsolutePath(), "main-server.js");
106+
Source source = Source.newBuilder("js", code.toFile()).mimeType("application/javascript+module").build();
107+
Value exports = context.eval(source);
108+
this.contexts.add(context);
109+
return exports.getMember("render");
101110
}
102-
else {
103-
return resource.getFile();
111+
catch (IOException e) {
112+
throw new UncheckedIOException(e);
104113
}
105114
}
106115

116+
static File getRoot(String root) {
117+
return fileMap.computeIfAbsent(root, key -> {
118+
log.trace("computing getRoot({})", key);
119+
try {
120+
if (NativeDetector.inNativeImage()) {
121+
// in native image
122+
return copyResources(root).toFile();
123+
}
124+
ClassPathResource resource = new ClassPathResource(root);
125+
if (resource.getURL().toString().startsWith("jar:")) {
126+
// in jar file
127+
return new FileSystemResource("./target/classes/" + root).getFile();
128+
}
129+
else {
130+
return resource.getFile();
131+
}
132+
}
133+
catch (IOException e) {
134+
throw new UncheckedIOException(e);
135+
}
136+
});
137+
}
138+
107139
private static Path copyResources(String root) {
108140
try {
109141
Path baseDir = Files.createTempDirectory("copied-");
@@ -140,7 +172,7 @@ private static Path copyResources(String root) {
140172

141173
@Override
142174
public void close() {
143-
this.context.close();
175+
this.contexts.forEach(Context::close);
144176
}
145177

146178
}

src/test/java/com/example/post/ssr/SsrControllerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import static org.assertj.core.api.Assertions.assertThat;
2222
import static org.mockito.BDDMockito.given;
2323

24-
@WebMvcTest(SsrController.class)
24+
@WebMvcTest(controllers = SsrController.class, properties = "logging.level.com.example=trace")
2525
@Import(ReactRenderer.class)
2626
class SsrControllerTest {
2727

0 commit comments

Comments
 (0)