8
8
import java .nio .file .Files ;
9
9
import java .nio .file .Path ;
10
10
import java .nio .file .Paths ;
11
+ import java .util .ArrayList ;
12
+ import java .util .List ;
11
13
import java .util .Map ;
12
14
import java .util .Objects ;
15
+ import java .util .concurrent .ConcurrentHashMap ;
16
+ import java .util .concurrent .ConcurrentMap ;
13
17
14
18
import com .fasterxml .jackson .databind .ObjectMapper ;
15
19
import org .graalvm .polyglot .Context ;
19
23
import org .slf4j .Logger ;
20
24
import org .slf4j .LoggerFactory ;
21
25
26
+ import org .springframework .core .NamedInheritableThreadLocal ;
22
27
import org .springframework .core .NativeDetector ;
23
28
import org .springframework .core .io .ClassPathResource ;
24
29
import org .springframework .core .io .FileSystemResource ;
31
36
@ Component
32
37
public class ReactRenderer implements AutoCloseable {
33
38
34
- private final Context context ;
35
-
36
- private final Value render ;
39
+ private final ThreadLocal <Value > renderHolder = new NamedInheritableThreadLocal <>("React Render" );
37
40
38
41
private final String template ;
39
42
40
43
private final ObjectMapper objectMapper ;
41
44
45
+ private static final ConcurrentMap <String , File > fileMap = new ConcurrentHashMap <>();
46
+
47
+ private final List <Context > contexts = new ArrayList <>();
48
+
42
49
private static final Logger log = LoggerFactory .getLogger (ReactRenderer .class );
43
50
44
51
public ReactRenderer (ObjectMapper objectMapper ) throws IOException {
45
52
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" );
70
53
this .template = Files .readString (Paths .get (getRoot ("META-INF/resources" ).getAbsolutePath (), "index.html" ));
71
- ;
54
+ // pre-computing
55
+ getRoot ("polyfill" );
56
+ getRoot ("server" );
72
57
}
73
58
74
59
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
+ }
75
65
try {
76
66
String s = this .objectMapper .writeValueAsString (input );
77
- Value executed = this . render .execute (url , s );
67
+ Value executed = render .execute (url , s );
78
68
Value head = executed .getMember ("head" );
79
69
Value html = executed .getMember ("html" );
80
70
return this .template
@@ -89,21 +79,63 @@ public String render(String url, Map<String, Object> input) {
89
79
}
90
80
}
91
81
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" );
101
110
}
102
- else {
103
- return resource . getFile ( );
111
+ catch ( IOException e ) {
112
+ throw new UncheckedIOException ( e );
104
113
}
105
114
}
106
115
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
+
107
139
private static Path copyResources (String root ) {
108
140
try {
109
141
Path baseDir = Files .createTempDirectory ("copied-" );
@@ -140,7 +172,7 @@ private static Path copyResources(String root) {
140
172
141
173
@ Override
142
174
public void close () {
143
- this .context . close ( );
175
+ this .contexts . forEach ( Context :: close );
144
176
}
145
177
146
178
}
0 commit comments