From bb50f6c643cf82bfdc3146cb36c54fec2e7f4562 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Wed, 30 Oct 2024 15:16:51 +0100
Subject: [PATCH 01/31] React: bump GraalJS and turn on sandboxing tests.
This version fixes a JVM crash that occurs with the latest GraalVM, and removes the need for the prior test disables related to sandboxing.
The JSON object ordering isn't stable across versions so fix the assumption that it is.
---
gradle/libs.versions.toml | 2 +-
views-react/build.gradle | 1 +
.../views/react/SandboxReactRenderSpec.groovy | 28 +++++++++----------
3 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index cd4035ecf..f4acd9b2a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,7 +18,7 @@ managed-soy = "2023-09-13"
org-json = "20240303"
managed-thymeleaf = "3.1.2.RELEASE"
managed-velocity = "2.4.1"
-graal = "24.0.1"
+graal = "24.1.1"
pebble = "3.2.2"
thymeleaf-extra-java8time = "3.0.4.RELEASE"
diff --git a/views-react/build.gradle b/views-react/build.gradle
index d92c40fc6..2bb785cf7 100644
--- a/views-react/build.gradle
+++ b/views-react/build.gradle
@@ -31,6 +31,7 @@ dependencies {
testImplementation(mn.micronaut.management)
testImplementation(mnValidation.micronaut.validation)
testImplementation(mn.snakeyaml)
+ testImplementation(mn.groovy.json)
// We will use enterprise edition for testing.
testImplementation(libs.graal.js)
diff --git a/views-react/src/test/groovy/io/micronaut/views/react/SandboxReactRenderSpec.groovy b/views-react/src/test/groovy/io/micronaut/views/react/SandboxReactRenderSpec.groovy
index 73a65cbff..b37ac19c9 100644
--- a/views-react/src/test/groovy/io/micronaut/views/react/SandboxReactRenderSpec.groovy
+++ b/views-react/src/test/groovy/io/micronaut/views/react/SandboxReactRenderSpec.groovy
@@ -1,11 +1,12 @@
package io.micronaut.views.react
+import groovy.json.JsonSlurper
import io.micronaut.context.annotation.Property
-import io.micronaut.context.exceptions.BeanInstantiationException
import io.micronaut.core.io.Writable
+import io.micronaut.http.exceptions.MessageBodyException
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
-import spock.lang.FailsWith
+import org.graalvm.polyglot.PolyglotException
import spock.lang.Specification
@MicronautTest(startApplication = false, rebuildContext = true)
@@ -15,20 +16,22 @@ class SandboxReactRenderSpec extends Specification {
@Inject
ReactViewsRenderer> renderer
- // The version of GraalJS currently depended on is not compatible with the sandbox. When GraalJS is upgraded,
- // this unit test can be enabled.
- @FailsWith(BeanInstantiationException)
void "views can be rendered with sandboxing enabled"() {
when:
Writable writable = renderer.render("App", TestProps.basic, null)
- String result = new StringWriter().with {
+ String resultAsString = new StringWriter().with {
writable.writeTo(it)
it.toString()
}
+ String dataJSON = resultAsString.find(~/var Micronaut = (\{[^;]+});/).replace("var Micronaut = ", "")
+ def data = new JsonSlurper().parseText(dataJSON)
+
then:
- result.contains("Hello there")
- result.contains("{\"name\":\"Mike\",\"obj\":{\"bar\":null,\"foo\":\"bar\"}}")
+ resultAsString.contains("Hello there")
+ data.rootProps.name == "Mike"
+ data.rootProps.obj.foo == "foo"
+ data.rootProps.obj.bar == null
}
void "host types are inaccessible with the sandbox enabled"() {
@@ -40,11 +43,8 @@ class SandboxReactRenderSpec extends Specification {
}
then:
- // The version of GraalJS currently depended on is not compatible with the sandbox. When GraalJS is upgraded,
- // this unit test can be enabled.
- thrown(BeanInstantiationException)
-// def t = thrown(MessageBodyException)
-// t.cause instanceof PolyglotException
-// t.cause.message.contains("Java is not defined")
+ def t = thrown(MessageBodyException)
+ t.cause instanceof PolyglotException
+ t.cause.message.contains("Java is not defined")
}
}
From 9ec50d2923b8a41dda48bd4de91220e66de7facf Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Wed, 30 Oct 2024 15:18:19 +0100
Subject: [PATCH 02/31] React: reactivate on CI now JVM crash is avoided.
---
views-react/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/views-react/build.gradle b/views-react/build.gradle
index 2bb785cf7..242a227e5 100644
--- a/views-react/build.gradle
+++ b/views-react/build.gradle
@@ -88,7 +88,7 @@ tasks.named("test") {
// This module depends on GraalVM 21 or higher due to the need for the esm-exports GraalJS option and inability
// to upgrade it on older JDKs that bundled languages together.
onlyIf {
- JavaVersion.current() >= JavaVersion.VERSION_21 && !System.getenv("CI")
+ JavaVersion.current() >= JavaVersion.VERSION_21
}
}
From 382bdafcf03968ab6830b4af17e273086bb0c432 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 11:49:26 +0200
Subject: [PATCH 03/31] ReactJS: Update README.
---
views-react/README.md | 18 ++++--------------
1 file changed, 4 insertions(+), 14 deletions(-)
diff --git a/views-react/README.md b/views-react/README.md
index 9620518c1..4106ee4b0 100644
--- a/views-react/README.md
+++ b/views-react/README.md
@@ -1,17 +1,7 @@
# React SSR support for Micronaut
-## TODO
+This module uses GraalJS to implement server-side rendering for applications using React or similar libraries like Preact.
-1. Eliminate all TODOs from the docs.
-2. Make HTTP prefetches run in parallel.
-3. Reduce the need for config:
- 1. Work out what `micronaut.views.folder` is supposed to be when run from Maven. Get rid of the need to specify this.
- 2. Make it configurable and allow the path to the static assets to be configured so it doesn't have to be served from MN itself.
- 3. Get rid of the blocking of the event loop when prefetching. Pending answer from MN team about why IO pool switch isn't implemented.
-4. Write unit tests.
-5. Document what you can and cannot do in GraalJS.
-6. Find a way to use `renderToPipeableStream`?
-7. Replace `__micronaut_prefetch` with Sam's implementation of fetch() for Micronaut?
-8. Document how to do debugging?
-9. Implement / get implemented TextEncoder/TextDecoder
-10. Update the micronaut-spa-app sample.
+To get a project that uses it, try Micronaut Starter and request Views React. That will give you a fully functional
+frontend project with build system integration (NPM and Webpack integrated with Maven and Gradle). You can then easily
+add components and build up your frontend, or connect up an existing frontend codebase.
From 10f6b560d95a20e249b824629d2fb6ff0bcc090a Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 11:59:00 +0200
Subject: [PATCH 04/31] ReactJS: Refactor file reload handler out of
JSContextPool, mark some fields private.
---
.../micronaut/views/react/JSBundlePaths.java | 32 +++++++++++++----
.../micronaut/views/react/JSContextPool.java | 36 ++++++++++---------
2 files changed, 45 insertions(+), 23 deletions(-)
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java b/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
index 6e4f73f86..fd355a3dd 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
@@ -15,13 +15,17 @@
*/
package io.micronaut.views.react;
+import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.io.ResourceResolver;
-import io.micronaut.views.ViewsConfiguration;
+import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
+import io.micronaut.scheduling.io.watch.event.WatchEventType;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.graalvm.polyglot.Source;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
@@ -34,27 +38,33 @@
import static java.lang.String.format;
/**
- * Wraps the computation of where to find the JS for client and server.
+ * Wraps the computation of where to find the JS for client and server. Hot reloads on file changes.
*/
@Singleton
@Internal
-class JSBundlePaths {
+class JSBundlePaths implements ApplicationEventListener {
+ private static final Logger LOG = LoggerFactory.getLogger(JSBundlePaths.class);
+
+ private final JSContextPool contextPool;
+
// Source code file name, for JS stack traces.
- final String bundleFileName;
+ private final String bundleFileName;
// URL of bundle file, could be a file:// or in a classpath jar.
- final URL bundleURL;
+ private final URL bundleURL;
// If a file:// (during development), the path of that file. Used for hot reloads.
@Nullable
- final Path bundlePath;
+ private final Path bundlePath;
@Inject
JSBundlePaths(
- ViewsConfiguration viewsConfiguration,
+ JSContextPool contextPool,
ReactViewsRendererConfiguration reactConfiguration,
ResourceResolver resolver
) throws IOException {
+ this.contextPool = contextPool;
+
Optional bundlePathOpt = resolver.getResource(reactConfiguration.getServerBundlePath());
if (bundlePathOpt.isEmpty()) {
throw new FileNotFoundException(format("Server bundle %s could not be found. Check your %s property.", reactConfiguration.getServerBundlePath(), ReactViewsRendererConfiguration.PREFIX + ".server-bundle-path"));
@@ -75,4 +85,12 @@ Source readServerBundle() throws IOException {
.build();
}
}
+
+ @Override
+ public void onApplicationEvent(FileChangedEvent event) {
+ if (bundlePath != null && event.getPath().equals(bundlePath) && event.getEventType() != WatchEventType.DELETE) {
+ LOG.info("Reloading Javascript bundle due to file change.");
+ contextPool.releaseAll();
+ }
+ }
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSContextPool.java b/views-react/src/main/java/io/micronaut/views/react/JSContextPool.java
index 035a79880..0f174f942 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSContextPool.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSContextPool.java
@@ -16,10 +16,7 @@
package io.micronaut.views.react;
import io.micronaut.context.ApplicationContext;
-import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.core.annotation.Internal;
-import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
-import io.micronaut.scheduling.io.watch.event.WatchEventType;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
@@ -37,19 +34,16 @@
*/
@Singleton
@Internal
-class JSContextPool implements ApplicationEventListener {
- private static final Logger LOG = LoggerFactory.getLogger(JSContextPool.class);
+class JSContextPool {
private final ApplicationContext applicationContext;
- private final JSBundlePaths paths;
// Synchronized on 'this'.
private final LinkedList> contexts = new LinkedList<>();
private int versionCounter = 0; // File reloads.
@Inject
- JSContextPool(ApplicationContext applicationContext, JSBundlePaths paths) {
+ JSContextPool(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
- this.paths = paths;
}
/**
@@ -75,16 +69,26 @@ synchronized JSContext acquire() {
return applicationContext.createBean(JSContext.class, versionCounter);
}
+ /**
+ * Puts a context back into the pool. It should be returned in a 'clean' state, so whatever
+ * thread picks it up next finds it ready to use and without any leftover data from prior
+ * usages.
+ */
synchronized void release(JSContext jsContext) {
- // Put it back into the pool for reuse.
- contexts.add(new SoftReference<>(jsContext));
+ // Put it back into the pool for reuse unless it's out of date, in which case just let it drift.
+ if (jsContext.versionCounter == versionCounter)
+ contexts.add(new SoftReference<>(jsContext));
}
- @Override
- public synchronized void onApplicationEvent(FileChangedEvent event) {
- if (paths.bundlePath != null && event.getPath().equals(paths.bundlePath) && event.getEventType() != WatchEventType.DELETE) {
- LOG.info("Reloading Javascript bundle due to file change.");
- versionCounter++;
- }
+ /**
+ * Semantically this method empties the pool. The actual contexts won't be released until they
+ * are requested later, this implementation just marks them as out of date. Out of date contexts
+ * won't be re-added to the pool even if {@link #release(JSContext)} is called on them. It can
+ * be used if there is a need to reload all the contexts, e.g. because a file on disk changed.
+ */
+ synchronized void releaseAll() {
+ versionCounter++;
+ if (versionCounter < 0)
+ throw new IllegalStateException("Version counter wrapped, you can't call releaseAll this many times.");
}
}
From f80af4833b95dc1f9f5c2691b4b8c4f698017d77 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 14:36:39 +0200
Subject: [PATCH 05/31] ReactJS: Improve the docs for how to enable hot file
reload.
---
.../views/templates/react/reactsettingproperties.adoc | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/main/docs/guide/views/templates/react/reactsettingproperties.adoc b/src/main/docs/guide/views/templates/react/reactsettingproperties.adoc
index 39676a2b1..e084e3b9b 100644
--- a/src/main/docs/guide/views/templates/react/reactsettingproperties.adoc
+++ b/src/main/docs/guide/views/templates/react/reactsettingproperties.adoc
@@ -27,7 +27,7 @@ IMPORTANT: Watch out for the last property that disables virtual threads. If you
[[react-dev-mode]]
== Development
-During development you want the fastest iteration speed possible. Firstly turn off response caching so hot reload works with `npx webpack --watch`. Micronaut Views React will automatically notice the file changed on disk and reload it.
+During development you want the fastest iteration speed possible. These property changes will help:
[configuration]
----
@@ -38,9 +38,15 @@ micronaut:
responses:
file:
cache-seconds: 0
+ io:
+ watch:
+ enabled: true
+ paths: build/resources/main/views
----
-If using Maven turn off Micronaut's automatic restart features so that changes to the compiled bundle JS don't cause the whole server to reboot:
+The `paths` property is correct if you're using the default JS compilation setup created for you by Micronaut Starter. If your JS bundle is held in a different directory in your project, make sure to set the path appropriately.
+
+Now you can tell your build system to only recompile the needed files. In a Micronaut Starter based project that uses Gradle, just run `./gradlew --continuous processResources`. This will cause new bundles to be created for both client and server whenever your input JS changes. If using Maven turn off Micronaut's automatic restart features so that changes to the compiled bundle JS don't cause the whole server to reboot, and then make sure to re-run the Maven build when necessary:
[xml]
----
@@ -61,4 +67,3 @@ If using Maven turn off Micronaut's automatic restart features so that changes t
----
-
From 4ce83fab9bbe2ebf6ce5e394795482d86644d01e Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 14:37:28 +0200
Subject: [PATCH 06/31] ReactJS: Refactor JSContextPool into BeanPool.
This is a new generic utility useful for not only other Truffle languages, but Micronaut in general.
Improved the documentation, efficiency and usability of the class along the way.
---
.../io/micronaut/views/react/BeanPool.java | 151 ++++++++++++++++++
.../io/micronaut/views/react/CompiledJS.java | 20 ++-
.../micronaut/views/react/JSBeanFactory.java | 10 +-
.../micronaut/views/react/JSBundlePaths.java | 26 +--
.../io/micronaut/views/react/JSContext.java | 11 +-
.../micronaut/views/react/JSContextPool.java | 94 -----------
.../views/react/ReactViewsRenderer.java | 19 +--
.../IntrospectableBeansAreProxiedSpec.groovy | 6 +-
8 files changed, 196 insertions(+), 141 deletions(-)
create mode 100644 views-react/src/main/java/io/micronaut/views/react/BeanPool.java
delete mode 100644 views-react/src/main/java/io/micronaut/views/react/JSContextPool.java
diff --git a/views-react/src/main/java/io/micronaut/views/react/BeanPool.java b/views-react/src/main/java/io/micronaut/views/react/BeanPool.java
new file mode 100644
index 000000000..8f0998d70
--- /dev/null
+++ b/views-react/src/main/java/io/micronaut/views/react/BeanPool.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2017-2020 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.views.react;
+
+import io.micronaut.core.annotation.Internal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.ref.SoftReference;
+import java.util.LinkedList;
+import java.util.function.Supplier;
+
+/**
+ *
An object pool for beans that are both expensive to construct and not thread safe.
+ *
+ *
+ * The pool vends them temporarily to threads that need them when they call {@link #checkOut()}.
+ * This pool is designed to work well with virtual threads, by avoiding thread locals or any
+ * assumptions about how many distinct threads will request objects. If all the beans are currently
+ * checked out and a new bean is requested it will be created using the factory provided to the
+ * constructor. Therefore the number of beans created and available in the pool should grow to match
+ * the general rate of contention, rather than how many threads are in use.
+ *
+ *
+ *
Note: this pool doesn't release objects in the background. If you have
+ * a sudden spike of traffic that drives many checkouts, memory usage may grow significantly
+ * and not be released. Fixing this would be a good future improvement to the pool.
+ */
+@Internal
+public class BeanPool {
+ // TODO: Use @Scheduled to occasionally clear out beans that weren't accessed for a while to recover from traffic spikes.
+
+ private static final Logger LOG = LoggerFactory.getLogger(BeanPool.class);
+
+ private final Supplier factory;
+
+ /**
+ * A handle to a pooled object. Call {@link #get()} to obtain the wrapped reference, and then
+ * pass this handle to {@link BeanPool#checkIn(Handle)} to put it back. Alternatively you can
+ * just close this object to check it back in.
+ */
+ public interface Handle extends Supplier, AutoCloseable {
+ @Override
+ void close();
+ }
+
+ private final class PoolEntry implements Handle {
+ final T obj;
+ final int version;
+
+ private PoolEntry(T obj, int version) {
+ this.obj = obj;
+ this.version = version;
+ }
+
+ @Override
+ public T get() {
+ return obj;
+ }
+
+ @Override
+ public void close() {
+ checkIn(this);
+ }
+
+ @Override
+ public String toString() {
+ return "PoolEntry[" +
+ "obj=" + obj + ", " +
+ "version=" + version + ']';
+ }
+ }
+
+ // Synchronized on 'this'.
+ private final LinkedList> pool = new LinkedList<>();
+ private int versionCounter = 0; // File reloads.
+
+ /**
+ * Constructs an object pool that uses the given factory to create new entries.
+ *
+ * @param factory Used to create new entries when the pool is empty. Must be thread safe.
+ */
+ public BeanPool(Supplier factory) {
+ this.factory = factory;
+ }
+
+ /**
+ * Returns a cached bean or creates a new one. Call {@link Handle#get()} to obtain the
+ * underlying object.
+ *
+ * @return a wrapper for the entry that you should pass to {@link #checkIn(Handle)} when you're
+ * done with it to put it back into the pool.
+ */
+ public synchronized Handle checkOut() {
+ while (!pool.isEmpty()) {
+ SoftReference ref = pool.poll();
+ assert ref != null;
+
+ PoolEntry handle = ref.get();
+
+ // The entry may have been garbage collected (== null), or it might be for an old
+ // version. In both cases we just let it drift away as we now hold the only reference.
+ LOG.info("handle {}", handle);
+ if (handle != null && handle.version == versionCounter) {
+ return handle;
+ }
+ }
+
+ // No more pooled contexts available, create one and return it. It'll be added [back] to the
+ // pool when release() is called.
+ return new PoolEntry(factory.get(), versionCounter);
+ }
+
+ /**
+ * Puts a context back into the pool. It should be returned in a 'clean' state, so whatever
+ * thread picks it up next finds it ready to use and without any leftover data from prior
+ * usages.
+ *
+ * @param handle The object you got from {@link #checkOut()}.
+ */
+ public synchronized void checkIn(Handle handle) {
+ var impl = (PoolEntry) handle;
+ // Put it back into the pool for reuse unless it's out of date, in which case just let it drift.
+ if (impl.version == versionCounter)
+ pool.add(new SoftReference<>(impl));
+ }
+
+ /**
+ * Empties the pool. Beans currently checked out with {@link #checkOut()} will not be re-added
+ * to the pool when {@link #checkIn(Handle)} is called.
+ */
+ public synchronized void clear() {
+ versionCounter++;
+ if (versionCounter < 0)
+ throw new IllegalStateException("Version counter wrapped, you can't call releaseAll this many times.");
+ pool.clear();
+ }
+}
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
index 54bb299c3..73a14cf29 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
@@ -15,7 +15,10 @@
*/
package io.micronaut.views.react;
+import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.core.annotation.Internal;
+import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
+import io.micronaut.scheduling.io.watch.event.WatchEventType;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@@ -33,21 +36,23 @@
*/
@Singleton
@Internal
-class CompiledJS implements AutoCloseable {
+class CompiledJS implements AutoCloseable, ApplicationEventListener {
private static final Logger LOG = LoggerFactory.getLogger("js");
final Engine engine;
+ private final BeanPool beanPool;
private Source source;
private final JSBundlePaths jsBundlePaths;
@Inject
- CompiledJS(JSBundlePaths jsBundlePaths, JSEngineLogHandler engineLogHandler, JSSandboxing sandboxing) {
+ CompiledJS(JSBundlePaths jsBundlePaths, JSEngineLogHandler engineLogHandler, JSSandboxing sandboxing, BeanPool beanPool) {
var engineBuilder = Engine.newBuilder("js")
.out(new OutputStreamToSLF4J(LOG, Level.INFO))
.err(new OutputStreamToSLF4J(LOG, Level.ERROR))
.logHandler(engineLogHandler);
engine = sandboxing.configure(engineBuilder).build();
this.jsBundlePaths = jsBundlePaths;
+ this.beanPool = beanPool;
reload();
}
@@ -55,7 +60,7 @@ synchronized Source getSource() {
return source;
}
- synchronized void reload() {
+ private synchronized void reload() {
try {
source = jsBundlePaths.readServerBundle();
} catch (IOException e) {
@@ -68,4 +73,13 @@ synchronized void reload() {
public void close() throws Exception {
engine.close();
}
+
+ @Override
+ public void onApplicationEvent(FileChangedEvent event) {
+ if (jsBundlePaths.bundlePath != null && event.getPath().equals(jsBundlePaths.bundlePath) && event.getEventType() != WatchEventType.DELETE) {
+ LOG.info("Reloading Javascript bundle due to file change.");
+ reload();
+ beanPool.clear();
+ }
+ }
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
index 972930924..80c4d2535 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
@@ -15,11 +15,14 @@
*/
package io.micronaut.views.react;
-import jakarta.inject.Singleton;;
+import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Internal;
+import jakarta.inject.Singleton;
import org.graalvm.polyglot.HostAccess;
+;
+
/**
* Allows the default Javascript context and host access policy to be controlled.
*/
@@ -40,4 +43,9 @@ HostAccess hostAccess(ReactViewsRendererConfiguration configuration) {
? HostAccess.CONSTRAINED
: HostAccess.ALL;
}
+
+ @Singleton
+ BeanPool contextPool(ApplicationContext applicationContext) {
+ return new BeanPool<>(() -> applicationContext.createBean(JSContext.class));
+ }
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java b/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
index fd355a3dd..19afab1a1 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
@@ -15,12 +15,9 @@
*/
package io.micronaut.views.react;
-import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.io.ResourceResolver;
-import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
-import io.micronaut.scheduling.io.watch.event.WatchEventType;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.graalvm.polyglot.Source;
@@ -42,11 +39,9 @@
*/
@Singleton
@Internal
-class JSBundlePaths implements ApplicationEventListener {
+class JSBundlePaths {
private static final Logger LOG = LoggerFactory.getLogger(JSBundlePaths.class);
- private final JSContextPool contextPool;
-
// Source code file name, for JS stack traces.
private final String bundleFileName;
@@ -55,16 +50,10 @@ class JSBundlePaths implements ApplicationEventListener {
// If a file:// (during development), the path of that file. Used for hot reloads.
@Nullable
- private final Path bundlePath;
+ final Path bundlePath;
@Inject
- JSBundlePaths(
- JSContextPool contextPool,
- ReactViewsRendererConfiguration reactConfiguration,
- ResourceResolver resolver
- ) throws IOException {
- this.contextPool = contextPool;
-
+ JSBundlePaths(ReactViewsRendererConfiguration reactConfiguration, ResourceResolver resolver) throws IOException {
Optional bundlePathOpt = resolver.getResource(reactConfiguration.getServerBundlePath());
if (bundlePathOpt.isEmpty()) {
throw new FileNotFoundException(format("Server bundle %s could not be found. Check your %s property.", reactConfiguration.getServerBundlePath(), ReactViewsRendererConfiguration.PREFIX + ".server-bundle-path"));
@@ -73,6 +62,7 @@ class JSBundlePaths implements ApplicationEventListener {
bundleFileName = bundleURL.getFile();
if (bundleURL.getProtocol().equals("file")) {
bundlePath = Path.of(bundleURL.getPath());
+ LOG.info("Using server-side JS bundle from local disk: {}", bundlePath);
} else {
bundlePath = null;
}
@@ -85,12 +75,4 @@ Source readServerBundle() throws IOException {
.build();
}
}
-
- @Override
- public void onApplicationEvent(FileChangedEvent event) {
- if (bundlePath != null && event.getPath().equals(bundlePath) && event.getEventType() != WatchEventType.DELETE) {
- LOG.info("Reloading Javascript bundle due to file change.");
- contextPool.releaseAll();
- }
- }
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSContext.java b/views-react/src/main/java/io/micronaut/views/react/JSContext.java
index de87c7510..93f32013c 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSContext.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSContext.java
@@ -15,7 +15,7 @@
*/
package io.micronaut.views.react;
-import io.micronaut.context.annotation.Parameter;
+import io.micronaut.context.annotation.Bean;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import jakarta.annotation.PostConstruct;
@@ -37,6 +37,7 @@
* environment usable by one thread at a time.
*/
@Internal
+@Bean
class JSContext implements AutoCloseable {
// Symbols the user's server side bundle might supply us with.
private static final List IMPORT_SYMBOLS = List.of("React", "ReactDOMServer", "renderToString", "h");
@@ -46,19 +47,15 @@ class JSContext implements AutoCloseable {
Value render;
Value ssrModule;
- // What version of the on-disk bundle (considering file change events) we were loaded from.
- final int versionCounter;
-
private final CompiledJS compiledJS;
private final ReactViewsRendererConfiguration configuration;
private final JSSandboxing sandboxing;
@Inject
- JSContext(CompiledJS compiledJS, ReactViewsRendererConfiguration configuration, JSSandboxing sandboxing, @Parameter int versionCounter) {
+ JSContext(CompiledJS compiledJS, ReactViewsRendererConfiguration configuration, JSSandboxing sandboxing) {
this.compiledJS = compiledJS;
this.configuration = configuration;
this.sandboxing = sandboxing;
- this.versionCounter = versionCounter;
}
@PostConstruct
@@ -137,7 +134,7 @@ private Context createContext() {
throw e;
}
} catch (IllegalArgumentException e) {
- // We need esm-eval-returns-exports=true but it's not compatible with the sandbox in this version of GraalJS.
+ // We need esm-eval-returns-exports=true, but it's not compatible with the sandbox in this version of GraalJS.
if (e.getMessage().contains("Option 'js.esm-eval-returns-exports' is experimental")) {
throw new IllegalStateException("The sandboxing feature requires a newer version of GraalJS. Please upgrade and try again, or disable the sandboxing feature.");
} else {
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSContextPool.java b/views-react/src/main/java/io/micronaut/views/react/JSContextPool.java
deleted file mode 100644
index 0f174f942..000000000
--- a/views-react/src/main/java/io/micronaut/views/react/JSContextPool.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright 2017-2020 original authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.micronaut.views.react;
-
-import io.micronaut.context.ApplicationContext;
-import io.micronaut.core.annotation.Internal;
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.ref.SoftReference;
-import java.util.LinkedList;
-
-/**
- * Vends contexts to threads that need them. We don't use ThreadLocals here because what matters
- * is contention. If there are 30 server threads, but only a few requests ever use React, then
- * we don't want to have 30 contexts in memory at all times because they are quite chunky objects.
- * By only creating more when we are genuinely under load, we avoid bloat. This also fits better
- * with virtual threads, where a thread may not live beyond the lifetime of a single request.
- */
-@Singleton
-@Internal
-class JSContextPool {
- private final ApplicationContext applicationContext;
-
- // Synchronized on 'this'.
- private final LinkedList> contexts = new LinkedList<>();
- private int versionCounter = 0; // File reloads.
-
- @Inject
- JSContextPool(ApplicationContext applicationContext) {
- this.applicationContext = applicationContext;
- }
-
- /**
- * Returns a cached context or creates a new one. You must give the JSContext to
- * {@link #release(JSContext)} when you're done with it to put it (back) into the pool.
- */
- synchronized JSContext acquire() {
- while (!contexts.isEmpty()) {
- SoftReference ref = contexts.poll();
- assert ref != null;
-
- var context = ref.get();
- // context may have been garbage collected (== null), or it might be for an old
- // version of the on-disk bundle. In both cases we just let it drift away as we
- // now hold the only reference.
- if (context != null && context.versionCounter == versionCounter) {
- return context;
- }
- }
-
- // No more pooled contexts available, create one and return it. It'll be added [back] to the
- // pool when release() is called.
- return applicationContext.createBean(JSContext.class, versionCounter);
- }
-
- /**
- * Puts a context back into the pool. It should be returned in a 'clean' state, so whatever
- * thread picks it up next finds it ready to use and without any leftover data from prior
- * usages.
- */
- synchronized void release(JSContext jsContext) {
- // Put it back into the pool for reuse unless it's out of date, in which case just let it drift.
- if (jsContext.versionCounter == versionCounter)
- contexts.add(new SoftReference<>(jsContext));
- }
-
- /**
- * Semantically this method empties the pool. The actual contexts won't be released until they
- * are requested later, this implementation just marks them as out of date. Out of date contexts
- * won't be re-added to the pool even if {@link #release(JSContext)} is called on them. It can
- * be used if there is a need to reload all the contexts, e.g. because a file on disk changed.
- */
- synchronized void releaseAll() {
- versionCounter++;
- if (versionCounter < 0)
- throw new IllegalStateException("Version counter wrapped, you can't call releaseAll this many times.");
- }
-}
diff --git a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
index 612b36d89..807271aad 100644
--- a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
@@ -15,6 +15,7 @@
*/
package io.micronaut.views.react;
+import io.micronaut.context.exceptions.BeanInstantiationException;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.io.Writable;
@@ -42,7 +43,7 @@ public class ReactViewsRenderer implements ViewsRenderer contextPool;
/**
* Construct this renderer. Don't call it yourself, as Micronaut Views will set it up for you.
@@ -63,25 +64,21 @@ public ReactViewsRenderer() {
@Override
public @NonNull Writable render(@NonNull String viewName, @Nullable PROPS props, @Nullable HttpRequest> request) {
return writer -> {
- JSContext context = contextPool.acquire();
- try {
- render(viewName, props, writer, context, request);
+ try (BeanPool.Handle contextHandle = contextPool.checkOut()) {
+ render(viewName, props, writer, contextHandle.get(), request);
+ } catch (BeanInstantiationException e) {
+ throw e;
} catch (Exception e) {
// If we don't wrap and rethrow, the exception is swallowed and the request hangs.
throw new MessageBodyException("Could not render component " + viewName, e);
- } finally {
- contextPool.release(context);
}
};
}
@Override
public boolean exists(@NonNull String viewName) {
- var context = contextPool.acquire();
- try {
- return context.moduleHasMember(viewName);
- } finally {
- contextPool.release(context);
+ try (var contextHandle = contextPool.checkOut()) {
+ return contextHandle.get().moduleHasMember(viewName);
}
}
diff --git a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
index d84cf8277..1a5672546 100644
--- a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
+++ b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
@@ -9,12 +9,12 @@ import spock.lang.Specification
@MicronautTest(startApplication = false)
class IntrospectableBeansAreProxiedSpec extends Specification {
@Inject
- JSContextPool contextPool
+ BeanPool contextPool
void "introspectable bean can be proxied"() {
given:
- def jsContext = contextPool.acquire()
- def context = jsContext.polyglotContext
+ BeanPool.Handle jsContext = contextPool.checkOut()
+ def context = jsContext.get().polyglotContext
def bean = new SomeBean("foo value", "bar value", new SomeBean.InnerBean(10, Map.of("key", 123), List.of("one", "two", "three")))
when:
From c5d29166fb8f01ef7f13b94d4636b13b4cd46f4a Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 14:38:03 +0200
Subject: [PATCH 07/31] ReactJS: Move BeanPool into a sub-package.
---
.../src/main/java/io/micronaut/views/react/CompiledJS.java | 1 +
.../src/main/java/io/micronaut/views/react/JSBeanFactory.java | 1 +
.../main/java/io/micronaut/views/react/ReactViewsRenderer.java | 1 +
.../java/io/micronaut/views/react/{ => util}/BeanPool.java | 3 +--
.../views/react/IntrospectableBeansAreProxiedSpec.groovy | 1 +
5 files changed, 5 insertions(+), 2 deletions(-)
rename views-react/src/main/java/io/micronaut/views/react/{ => util}/BeanPool.java (98%)
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
index 73a14cf29..d2ad4318f 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
@@ -19,6 +19,7 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
import io.micronaut.scheduling.io.watch.event.WatchEventType;
+import io.micronaut.views.react.util.BeanPool;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
index 80c4d2535..26768eeac 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
@@ -18,6 +18,7 @@
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Internal;
+import io.micronaut.views.react.util.BeanPool;
import jakarta.inject.Singleton;
import org.graalvm.polyglot.HostAccess;
diff --git a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
index 807271aad..4c8fbe976 100644
--- a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
@@ -22,6 +22,7 @@
import io.micronaut.http.HttpRequest;
import io.micronaut.http.exceptions.MessageBodyException;
import io.micronaut.views.ViewsRenderer;
+import io.micronaut.views.react.util.BeanPool;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.graalvm.polyglot.HostAccess;
diff --git a/views-react/src/main/java/io/micronaut/views/react/BeanPool.java b/views-react/src/main/java/io/micronaut/views/react/util/BeanPool.java
similarity index 98%
rename from views-react/src/main/java/io/micronaut/views/react/BeanPool.java
rename to views-react/src/main/java/io/micronaut/views/react/util/BeanPool.java
index 8f0998d70..fd0badcc0 100644
--- a/views-react/src/main/java/io/micronaut/views/react/BeanPool.java
+++ b/views-react/src/main/java/io/micronaut/views/react/util/BeanPool.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micronaut.views.react;
+package io.micronaut.views.react.util;
import io.micronaut.core.annotation.Internal;
import org.slf4j.Logger;
@@ -113,7 +113,6 @@ public synchronized Handle checkOut() {
// The entry may have been garbage collected (== null), or it might be for an old
// version. In both cases we just let it drift away as we now hold the only reference.
- LOG.info("handle {}", handle);
if (handle != null && handle.version == versionCounter) {
return handle;
}
diff --git a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
index 1a5672546..8e34ed618 100644
--- a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
+++ b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
@@ -1,6 +1,7 @@
package io.micronaut.views.react
import io.micronaut.test.extensions.spock.annotation.MicronautTest
+import io.micronaut.views.react.util.BeanPool
import jakarta.inject.Inject
import org.graalvm.polyglot.Value
import org.graalvm.polyglot.proxy.ProxyObject
From e8ff5140d9845897143f9ca3a79ffa3f4bd24aac Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 14:39:26 +0200
Subject: [PATCH 08/31] ReactJS: Move OutputStreamToSLF4J into the utils
sub-package.
This class is generally useful and could be a part of Micronaut (or really, SLF4J itself).
---
.../src/main/java/io/micronaut/views/react/CompiledJS.java | 1 +
.../micronaut/views/react/{ => util}/OutputStreamToSLF4J.java | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
rename views-react/src/main/java/io/micronaut/views/react/{ => util}/OutputStreamToSLF4J.java (96%)
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
index d2ad4318f..9c7ac8833 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
@@ -20,6 +20,7 @@
import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
import io.micronaut.scheduling.io.watch.event.WatchEventType;
import io.micronaut.views.react.util.BeanPool;
+import io.micronaut.views.react.util.OutputStreamToSLF4J;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
diff --git a/views-react/src/main/java/io/micronaut/views/react/OutputStreamToSLF4J.java b/views-react/src/main/java/io/micronaut/views/react/util/OutputStreamToSLF4J.java
similarity index 96%
rename from views-react/src/main/java/io/micronaut/views/react/OutputStreamToSLF4J.java
rename to views-react/src/main/java/io/micronaut/views/react/util/OutputStreamToSLF4J.java
index 077657f75..1f4101afa 100644
--- a/views-react/src/main/java/io/micronaut/views/react/OutputStreamToSLF4J.java
+++ b/views-react/src/main/java/io/micronaut/views/react/util/OutputStreamToSLF4J.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micronaut.views.react;
+package io.micronaut.views.react.util;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
@@ -30,7 +30,7 @@
* An output stream that looks for line separators and then writes out the lines of text to the given logger.
*/
@Internal
-final class OutputStreamToSLF4J extends OutputStream {
+public final class OutputStreamToSLF4J extends OutputStream {
private final Charset charset;
private ByteBuffer buffer = ByteBuffer.allocate(512);
From 5a7c4c9823fef3b32f3f2dc957b82ee3dea07b58 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 14:41:22 +0200
Subject: [PATCH 09/31] ReactJS: Move the JUL2SLF4J adapter into the utils
sub-package.
This class is generally useful and could be a part of Micronaut (or really, SLF4J itself).
---
.../io/micronaut/views/react/CompiledJS.java | 5 ++--
.../JavaUtilLoggingToSLF4J.java} | 23 ++++++++++---------
2 files changed, 15 insertions(+), 13 deletions(-)
rename views-react/src/main/java/io/micronaut/views/react/{JSEngineLogHandler.java => util/JavaUtilLoggingToSLF4J.java} (71%)
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
index 9c7ac8833..ed091ade8 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
@@ -20,6 +20,7 @@
import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
import io.micronaut.scheduling.io.watch.event.WatchEventType;
import io.micronaut.views.react.util.BeanPool;
+import io.micronaut.views.react.util.JavaUtilLoggingToSLF4J;
import io.micronaut.views.react.util.OutputStreamToSLF4J;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
@@ -47,11 +48,11 @@ class CompiledJS implements AutoCloseable, ApplicationEventListener beanPool) {
+ CompiledJS(JSBundlePaths jsBundlePaths, JSSandboxing sandboxing, BeanPool beanPool) {
var engineBuilder = Engine.newBuilder("js")
.out(new OutputStreamToSLF4J(LOG, Level.INFO))
.err(new OutputStreamToSLF4J(LOG, Level.ERROR))
- .logHandler(engineLogHandler);
+ .logHandler(new JavaUtilLoggingToSLF4J(LOG));
engine = sandboxing.configure(engineBuilder).build();
this.jsBundlePaths = jsBundlePaths;
this.beanPool = beanPool;
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSEngineLogHandler.java b/views-react/src/main/java/io/micronaut/views/react/util/JavaUtilLoggingToSLF4J.java
similarity index 71%
rename from views-react/src/main/java/io/micronaut/views/react/JSEngineLogHandler.java
rename to views-react/src/main/java/io/micronaut/views/react/util/JavaUtilLoggingToSLF4J.java
index ecba254c0..f34656bfc 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSEngineLogHandler.java
+++ b/views-react/src/main/java/io/micronaut/views/react/util/JavaUtilLoggingToSLF4J.java
@@ -13,12 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micronaut.views.react;
+package io.micronaut.views.react.util;
import io.micronaut.core.annotation.Internal;
-import jakarta.inject.Singleton;
import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
@@ -27,10 +25,13 @@
* Forwards/redirects log messages from the GraalJS / Truffle engines themselves to SLF4J.
* Note that Javascript's {@code console.log()} is handled differently.
*/
-@Singleton
@Internal
-class JSEngineLogHandler extends Handler {
- private static final Logger LOG = LoggerFactory.getLogger(ReactViewsRenderer.class);
+public class JavaUtilLoggingToSLF4J extends Handler {
+ private final Logger logger;
+
+ public JavaUtilLoggingToSLF4J(Logger logger) {
+ this.logger = logger;
+ }
@Override
public void publish(LogRecord record) {
@@ -38,11 +39,11 @@ public void publish(LogRecord record) {
Throwable thrown = record.getThrown();
String level = record.getLevel().getName();
switch (level) {
- case "SEVERE" -> LOG.error(message, thrown);
- case "WARNING" -> LOG.warn(message, thrown);
- case "INFO" -> LOG.info(message, thrown);
- case "CONFIG", "FINE" -> LOG.debug(message, thrown);
- case "FINER", "FINEST" -> LOG.trace(message, thrown);
+ case "SEVERE" -> logger.error(message, thrown);
+ case "WARNING" -> logger.warn(message, thrown);
+ case "INFO" -> logger.info(message, thrown);
+ case "CONFIG", "FINE" -> logger.debug(message, thrown);
+ case "FINER", "FINEST" -> logger.trace(message, thrown);
default -> throw new IllegalStateException("Unexpected value: " + level);
}
}
From be7f13d4b51445e84a7a308ebd9628761b2c1ca0 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 14:42:31 +0200
Subject: [PATCH 10/31] ReactJS: Move and rename the proxy adapter class.
---
.../micronaut/views/react/ReactViewsRenderer.java | 3 ++-
.../IntrospectableToTruffleAdapter.java} | 15 ++++++++-------
.../IntrospectableBeansAreProxiedSpec.groovy | 5 +++--
3 files changed, 13 insertions(+), 10 deletions(-)
rename views-react/src/main/java/io/micronaut/views/react/{ProxyObjectWithIntrospectableSupport.java => truffle/IntrospectableToTruffleAdapter.java} (90%)
diff --git a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
index 4c8fbe976..bffe80817 100644
--- a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
@@ -22,6 +22,7 @@
import io.micronaut.http.HttpRequest;
import io.micronaut.http.exceptions.MessageBodyException;
import io.micronaut.views.ViewsRenderer;
+import io.micronaut.views.react.truffle.IntrospectableToTruffleAdapter;
import io.micronaut.views.react.util.BeanPool;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@@ -94,7 +95,7 @@ private void render(String componentName, PROPS props, Writer writer, JSContext
// We wrap the props object so we can use Micronaut's compile-time reflection implementation.
// This should be more native-image friendly (no need to write reflection config files), and
// might also be faster.
- Value guestProps = ProxyObjectWithIntrospectableSupport.wrap(context.polyglotContext, props);
+ Value guestProps = IntrospectableToTruffleAdapter.wrap(context.polyglotContext, props);
context.render.executeVoid(component, guestProps, renderCallback, reactConfiguration.getClientBundleURL(), request);
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/ProxyObjectWithIntrospectableSupport.java b/views-react/src/main/java/io/micronaut/views/react/truffle/IntrospectableToTruffleAdapter.java
similarity index 90%
rename from views-react/src/main/java/io/micronaut/views/react/ProxyObjectWithIntrospectableSupport.java
rename to views-react/src/main/java/io/micronaut/views/react/truffle/IntrospectableToTruffleAdapter.java
index 1d9d3f6c7..cb858b7f9 100644
--- a/views-react/src/main/java/io/micronaut/views/react/ProxyObjectWithIntrospectableSupport.java
+++ b/views-react/src/main/java/io/micronaut/views/react/truffle/IntrospectableToTruffleAdapter.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micronaut.views.react;
+package io.micronaut.views.react.truffle;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
@@ -39,23 +39,23 @@
* the regular polyglot mapping.
*/
@Internal
-final class ProxyObjectWithIntrospectableSupport implements ProxyObject {
+public final class IntrospectableToTruffleAdapter implements ProxyObject {
private final Context context;
private final Object target;
@Nullable
private final BeanIntrospection> introspection;
- private ProxyObjectWithIntrospectableSupport(Context context, Object target, BeanIntrospection> introspection) {
+ private IntrospectableToTruffleAdapter(Context context, Object target, BeanIntrospection> introspection) {
this.context = context;
this.target = target;
this.introspection = introspection;
}
/**
- * Returns an object as a Truffle {@link Value} suitable for guest access, wrapping introspectable types with {@link ProxyObjectWithIntrospectableSupport}.
+ * Returns an object as a Truffle {@link Value} suitable for guest access, wrapping introspectable types with {@link IntrospectableToTruffleAdapter}.
*/
- static Value wrap(Context context, Object object) {
+ public static Value wrap(Context context, Object object) {
if (object == null) {
return context.asValue(null);
} else if (object instanceof Map, ?> map) {
@@ -67,12 +67,13 @@ static Value wrap(Context context, Object object) {
// We need to recursively map the items. This could be lazy.
return context.asValue(collection.stream().map(it -> wrap(context, it)).toList());
} else if (object instanceof String) {
- // We could ignore this case because we'd fall through the BeanIntrospector check, but that logs some debug spam and it's slower to look up objects we know we won't wrap anyway.
+ // We could ignore this case because we'd fall through the BeanIntrospector check, but that logs some debug spam,
+ // and it's slower to look up objects we know we won't wrap anyway.
return context.asValue(object);
} else {
var introspection = BeanIntrospector.SHARED.findIntrospection(object.getClass()).orElse(null);
if (introspection != null) {
- return context.asValue(new ProxyObjectWithIntrospectableSupport(context, object, introspection));
+ return context.asValue(new IntrospectableToTruffleAdapter(context, object, introspection));
} else {
return context.asValue(object);
}
diff --git a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
index 8e34ed618..5f4cc2be5 100644
--- a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
+++ b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
@@ -1,6 +1,7 @@
package io.micronaut.views.react
import io.micronaut.test.extensions.spock.annotation.MicronautTest
+import io.micronaut.views.react.truffle.IntrospectableToTruffleAdapter
import io.micronaut.views.react.util.BeanPool
import jakarta.inject.Inject
import org.graalvm.polyglot.Value
@@ -19,7 +20,7 @@ class IntrospectableBeansAreProxiedSpec extends Specification {
def bean = new SomeBean("foo value", "bar value", new SomeBean.InnerBean(10, Map.of("key", 123), List.of("one", "two", "three")))
when:
- ProxyObject proxy = ProxyObjectWithIntrospectableSupport.wrap(context, bean).asProxyObject()
+ ProxyObject proxy = IntrospectableToTruffleAdapter.wrap(context, bean).asProxyObject()
context.getBindings("js").putMember("bean", proxy)
then:
@@ -30,7 +31,7 @@ class IntrospectableBeansAreProxiedSpec extends Specification {
ProxyObject innerBean = ((Value) proxy.getMember("innerBean")).asProxyObject()
then:
- innerBean instanceof ProxyObjectWithIntrospectableSupport
+ innerBean instanceof IntrospectableToTruffleAdapter
when:
ProxyObject innerBeanMap = ((Value) innerBean.getMember("map")).asProxyObject()
From ec77c8d65fb385b152c961b34a3e4e0d87c73797 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 15:12:15 +0200
Subject: [PATCH 11/31] ReactJS: Make BeanPool optionally close beans when they
are cleared or obsoleted.
---
.../micronaut/views/react/util/BeanPool.java | 41 ++++++++++++++++---
1 file changed, 35 insertions(+), 6 deletions(-)
diff --git a/views-react/src/main/java/io/micronaut/views/react/util/BeanPool.java b/views-react/src/main/java/io/micronaut/views/react/util/BeanPool.java
index fd0badcc0..b9dc4076e 100644
--- a/views-react/src/main/java/io/micronaut/views/react/util/BeanPool.java
+++ b/views-react/src/main/java/io/micronaut/views/react/util/BeanPool.java
@@ -19,6 +19,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.Closeable;
+import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.function.Supplier;
@@ -35,9 +37,17 @@
* the general rate of contention, rather than how many threads are in use.
*
*
- *
Note: this pool doesn't release objects in the background. If you have
+ *
+ * Beans in the pool can be atomically cleared and closed (if they implement {@link Closeable}).
+ * Any beans checked out when {@link #clear()} is called will remain untouched, but when they are
+ * checked back in they will be closed and discarded at that point.
+ *
+ *
+ *
+ * Note: this pool doesn't release objects in the background. If you have
* a sudden spike of traffic that drives many checkouts, memory usage may grow significantly
- * and not be released. Fixing this would be a good future improvement to the pool.
+ * and not be released. Fixing this would be a good future improvement to the pool.
+ *
*/
@Internal
public class BeanPool {
@@ -126,25 +136,44 @@ public synchronized Handle checkOut() {
/**
* Puts a context back into the pool. It should be returned in a 'clean' state, so whatever
* thread picks it up next finds it ready to use and without any leftover data from prior
- * usages.
+ * usages. If the pool has been {@link #clear() cleared} previously, and the pooled object is
+ * {@link Closeable}, then the object will be closed at this point (exceptions are ignored).
*
* @param handle The object you got from {@link #checkOut()}.
*/
public synchronized void checkIn(Handle handle) {
var impl = (PoolEntry) handle;
// Put it back into the pool for reuse unless it's out of date, in which case just let it drift.
- if (impl.version == versionCounter)
+ if (impl.version == versionCounter) {
pool.add(new SoftReference<>(impl));
+ } else if (impl.obj instanceof Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException ignored) {
+ }
+ }
}
/**
* Empties the pool. Beans currently checked out with {@link #checkOut()} will not be re-added
- * to the pool when {@link #checkIn(Handle)} is called.
+ * to the pool when {@link #checkIn(Handle)} is called, and may be closed if they are
+ * {@link Closeable}. Likewise, all beans in the pool are closed if they are {@link Closeable}
+ * and exceptions thrown by {@link Closeable#close()} are ignored.
*/
public synchronized void clear() {
versionCounter++;
- if (versionCounter < 0)
+ if (versionCounter < 0) {
throw new IllegalStateException("Version counter wrapped, you can't call releaseAll this many times.");
+ }
+ for (SoftReference ref : pool) {
+ var r = ref.get();
+ if (r != null && r.obj instanceof Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
pool.clear();
}
}
From 9311f337422ec114403924286a7d422177ad1593 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 15:13:56 +0200
Subject: [PATCH 12/31] ReactJS: Fix an obsolete JavaDoc.
---
.../src/main/java/io/micronaut/views/react/JSBundlePaths.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java b/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
index 19afab1a1..eef084131 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
@@ -35,7 +35,7 @@
import static java.lang.String.format;
/**
- * Wraps the computation of where to find the JS for client and server. Hot reloads on file changes.
+ * Wraps the computation of where to find the JS for client and server.
*/
@Singleton
@Internal
From c31023e093909ea8778768129aa6c5df23b6a46f Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 15:30:09 +0200
Subject: [PATCH 13/31] ReactJS: Refactoring to move Engine creation to a bean
factory.
This makes the code a bit more generic.
---
.../io/micronaut/views/react/CompiledJS.java | 11 +---
.../micronaut/views/react/JSBeanFactory.java | 23 +++++++-
.../io/micronaut/views/react/JSContext.java | 31 +++++++---
.../micronaut/views/react/JSSandboxing.java | 59 -------------------
4 files changed, 46 insertions(+), 78 deletions(-)
delete mode 100644 views-react/src/main/java/io/micronaut/views/react/JSSandboxing.java
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
index ed091ade8..b6c5f4d40 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
@@ -20,8 +20,6 @@
import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
import io.micronaut.scheduling.io.watch.event.WatchEventType;
import io.micronaut.views.react.util.BeanPool;
-import io.micronaut.views.react.util.JavaUtilLoggingToSLF4J;
-import io.micronaut.views.react.util.OutputStreamToSLF4J;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@@ -29,7 +27,6 @@
import org.graalvm.polyglot.Source;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.slf4j.event.Level;
import java.io.IOException;
@@ -48,13 +45,9 @@ class CompiledJS implements AutoCloseable, ApplicationEventListener beanPool) {
- var engineBuilder = Engine.newBuilder("js")
- .out(new OutputStreamToSLF4J(LOG, Level.INFO))
- .err(new OutputStreamToSLF4J(LOG, Level.ERROR))
- .logHandler(new JavaUtilLoggingToSLF4J(LOG));
- engine = sandboxing.configure(engineBuilder).build();
+ CompiledJS(JSBundlePaths jsBundlePaths, Engine engine, BeanPool beanPool) {
this.jsBundlePaths = jsBundlePaths;
+ this.engine = engine;
this.beanPool = beanPool;
reload();
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
index 26768eeac..41c3856b1 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
@@ -19,10 +19,15 @@
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Internal;
import io.micronaut.views.react.util.BeanPool;
+import io.micronaut.views.react.util.JavaUtilLoggingToSLF4J;
+import io.micronaut.views.react.util.OutputStreamToSLF4J;
import jakarta.inject.Singleton;
+import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.HostAccess;
-
-;
+import org.graalvm.polyglot.SandboxPolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.event.Level;
/**
* Allows the default Javascript context and host access policy to be controlled.
@@ -30,6 +35,8 @@
@Factory
@Internal
class JSBeanFactory {
+ private static final Logger LOG = LoggerFactory.getLogger("js");
+
/**
* This defaults to
* {@link HostAccess#ALL} if the sandbox is disabled, or {@link HostAccess#CONSTRAINED} if it's on.
@@ -49,4 +56,16 @@ HostAccess hostAccess(ReactViewsRendererConfiguration configuration) {
BeanPool contextPool(ApplicationContext applicationContext) {
return new BeanPool<>(() -> applicationContext.createBean(JSContext.class));
}
+
+ @Singleton
+ Engine engine(ReactViewsRendererConfiguration configuration) {
+ boolean sandbox = configuration.getSandbox();
+ LOG.debug("ReactJS sandboxing {}", sandbox ? "enabled" : "disabled");
+ return Engine.newBuilder("js")
+ .out(new OutputStreamToSLF4J(LOG, Level.INFO))
+ .err(new OutputStreamToSLF4J(LOG, Level.ERROR))
+ .logHandler(new JavaUtilLoggingToSLF4J(LOG))
+ .sandbox(sandbox ? SandboxPolicy.CONSTRAINED : SandboxPolicy.TRUSTED)
+ .build();
+ }
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSContext.java b/views-react/src/main/java/io/micronaut/views/react/JSContext.java
index 93f32013c..cb25c0327 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSContext.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSContext.java
@@ -21,9 +21,7 @@
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
-import org.graalvm.polyglot.Context;
-import org.graalvm.polyglot.Source;
-import org.graalvm.polyglot.Value;
+import org.graalvm.polyglot.*;
import java.io.IOException;
import java.nio.file.Files;
@@ -41,6 +39,8 @@
class JSContext implements AutoCloseable {
// Symbols the user's server side bundle might supply us with.
private static final List IMPORT_SYMBOLS = List.of("React", "ReactDOMServer", "renderToString", "h");
+ private final Engine engine;
+ private final HostAccess hostAccess;
// Accessed from ReactViewsRenderer
Context polyglotContext;
@@ -49,13 +49,13 @@ class JSContext implements AutoCloseable {
private final CompiledJS compiledJS;
private final ReactViewsRendererConfiguration configuration;
- private final JSSandboxing sandboxing;
@Inject
- JSContext(CompiledJS compiledJS, ReactViewsRendererConfiguration configuration, JSSandboxing sandboxing) {
+ JSContext(CompiledJS compiledJS, ReactViewsRendererConfiguration configuration, Engine engine, HostAccess hostAccess) {
this.compiledJS = compiledJS;
this.configuration = configuration;
- this.sandboxing = sandboxing;
+ this.engine = engine;
+ this.hostAccess = hostAccess;
}
@PostConstruct
@@ -121,11 +121,26 @@ private Source loadRenderSource() throws IOException {
private Context createContext() {
var contextBuilder = Context.newBuilder()
- .engine(compiledJS.engine)
+ .engine(engine)
.option("js.esm-eval-returns-exports", "true")
.option("js.unhandled-rejections", "throw");
+
+ if (configuration.getSandbox()) {
+ contextBuilder
+ .sandbox(SandboxPolicy.CONSTRAINED)
+ .allowHostAccess(hostAccess);
+ } else {
+ // allowExperimentalOptions is here because as of the time of writing (August 2024)
+ // the esm-eval-returns-exports option is experimental. That got fixed and this
+ // can be removed once the base version of GraalJS is bumped to 24.1 or higher.
+ contextBuilder
+ .sandbox(SandboxPolicy.TRUSTED)
+ .allowAllAccess(true)
+ .allowExperimentalOptions(true);
+ }
+
try {
- return sandboxing.configure(contextBuilder).build();
+ return contextBuilder.build();
} catch (ExceptionInInitializerError e) {
// The catch handler is to work around a bug in Polyglot 24.0.0
if (e.getCause().getMessage().contains("version compatibility check failed")) {
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSSandboxing.java b/views-react/src/main/java/io/micronaut/views/react/JSSandboxing.java
deleted file mode 100644
index 3610735e8..000000000
--- a/views-react/src/main/java/io/micronaut/views/react/JSSandboxing.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2017-2020 original authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.micronaut.views.react;
-
-import io.micronaut.core.annotation.Internal;
-import jakarta.inject.Singleton;
-import org.graalvm.polyglot.Context;
-import org.graalvm.polyglot.Engine;
-import org.graalvm.polyglot.HostAccess;
-import org.graalvm.polyglot.SandboxPolicy;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Some internal wrappers useful for centralizing sandbox configuration.
- */
-@Singleton
-@Internal
-class JSSandboxing {
- private static final Logger LOG = LoggerFactory.getLogger(JSSandboxing.class);
- private final boolean sandbox;
- private final HostAccess hostAccess;
-
- JSSandboxing(ReactViewsRendererConfiguration configuration, HostAccess hostAccess) {
- sandbox = configuration.getSandbox();
- if (LOG.isDebugEnabled()) {
- LOG.debug("ReactJS sandboxing {}", sandbox ? "enabled" : "disabled");
- }
- this.hostAccess = hostAccess;
- }
-
- Engine.Builder configure(Engine.Builder engineBuilder) {
- return engineBuilder.sandbox(sandbox ? SandboxPolicy.CONSTRAINED : SandboxPolicy.TRUSTED);
- }
-
- Context.Builder configure(Context.Builder builder) {
- if (sandbox) {
- return builder.sandbox(SandboxPolicy.CONSTRAINED).allowHostAccess(hostAccess);
- } else {
- // allowExperimentalOptions is here because as of the time of writing (August 2024)
- // the esm-eval-returns-exports option is experimental. That got fixed and this
- // can be removed once the base version of GraalJS is bumped to 24.1 or higher.
- return builder.sandbox(SandboxPolicy.TRUSTED).allowAllAccess(true).allowExperimentalOptions(true);
- }
- }
-}
From ed7c4d679a1ec45d4865b8d3a5e653183c45437d Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 15:31:08 +0200
Subject: [PATCH 14/31] ReactJS: Bugfix to the sandbox policy.
List/map access wasn't previously allowed, so in sandbox mode lists and maps wouldn't be exposed to JS and would appear empty.
---
.../src/main/java/io/micronaut/views/react/JSBeanFactory.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
index 41c3856b1..339fb6ce1 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
+++ b/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
@@ -48,7 +48,7 @@ class JSBeanFactory {
@Singleton
HostAccess hostAccess(ReactViewsRendererConfiguration configuration) {
return configuration.getSandbox()
- ? HostAccess.CONSTRAINED
+ ? HostAccess.newBuilder(HostAccess.CONSTRAINED).allowListAccess(true).allowMapAccess(true).build()
: HostAccess.ALL;
}
From 42cc120875ce1cc6a8a26c61023362ee7606bebe Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 15:36:04 +0200
Subject: [PATCH 15/31] ReactJS: Rename some classes to make it clear they are
React specific and not JS specific.
---
.../{CompiledJS.java => CompiledReactJSBundle.java} | 10 +++++-----
.../{JSBeanFactory.java => ReactJSBeanFactory.java} | 6 +++---
.../{JSBundlePaths.java => ReactJSBundlePaths.java} | 6 +++---
.../react/{JSContext.java => ReactJSContext.java} | 6 +++---
.../io/micronaut/views/react/ReactViewsRenderer.java | 6 +++---
.../react/IntrospectableBeansAreProxiedSpec.groovy | 2 +-
6 files changed, 18 insertions(+), 18 deletions(-)
rename views-react/src/main/java/io/micronaut/views/react/{CompiledJS.java => CompiledReactJSBundle.java} (86%)
rename views-react/src/main/java/io/micronaut/views/react/{JSBeanFactory.java => ReactJSBeanFactory.java} (95%)
rename views-react/src/main/java/io/micronaut/views/react/{JSBundlePaths.java => ReactJSBundlePaths.java} (91%)
rename views-react/src/main/java/io/micronaut/views/react/{JSContext.java => ReactJSContext.java} (96%)
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java b/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
similarity index 86%
rename from views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
rename to views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
index b6c5f4d40..52581a7e8 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledJS.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
@@ -36,16 +36,16 @@
*/
@Singleton
@Internal
-class CompiledJS implements AutoCloseable, ApplicationEventListener {
+class CompiledReactJSBundle implements AutoCloseable, ApplicationEventListener {
private static final Logger LOG = LoggerFactory.getLogger("js");
- final Engine engine;
- private final BeanPool beanPool;
+ private final Engine engine;
+ private final BeanPool beanPool;
private Source source;
- private final JSBundlePaths jsBundlePaths;
+ private final ReactJSBundlePaths jsBundlePaths;
@Inject
- CompiledJS(JSBundlePaths jsBundlePaths, Engine engine, BeanPool beanPool) {
+ CompiledReactJSBundle(ReactJSBundlePaths jsBundlePaths, Engine engine, BeanPool beanPool) {
this.jsBundlePaths = jsBundlePaths;
this.engine = engine;
this.beanPool = beanPool;
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java b/views-react/src/main/java/io/micronaut/views/react/ReactJSBeanFactory.java
similarity index 95%
rename from views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
rename to views-react/src/main/java/io/micronaut/views/react/ReactJSBeanFactory.java
index 339fb6ce1..0e6f5d528 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBeanFactory.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactJSBeanFactory.java
@@ -34,7 +34,7 @@
*/
@Factory
@Internal
-class JSBeanFactory {
+class ReactJSBeanFactory {
private static final Logger LOG = LoggerFactory.getLogger("js");
/**
@@ -53,8 +53,8 @@ HostAccess hostAccess(ReactViewsRendererConfiguration configuration) {
}
@Singleton
- BeanPool contextPool(ApplicationContext applicationContext) {
- return new BeanPool<>(() -> applicationContext.createBean(JSContext.class));
+ BeanPool contextPool(ApplicationContext applicationContext) {
+ return new BeanPool<>(() -> applicationContext.createBean(ReactJSContext.class));
}
@Singleton
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java b/views-react/src/main/java/io/micronaut/views/react/ReactJSBundlePaths.java
similarity index 91%
rename from views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
rename to views-react/src/main/java/io/micronaut/views/react/ReactJSBundlePaths.java
index eef084131..b25299ffa 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSBundlePaths.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactJSBundlePaths.java
@@ -39,8 +39,8 @@
*/
@Singleton
@Internal
-class JSBundlePaths {
- private static final Logger LOG = LoggerFactory.getLogger(JSBundlePaths.class);
+class ReactJSBundlePaths {
+ private static final Logger LOG = LoggerFactory.getLogger(ReactJSBundlePaths.class);
// Source code file name, for JS stack traces.
private final String bundleFileName;
@@ -53,7 +53,7 @@ class JSBundlePaths {
final Path bundlePath;
@Inject
- JSBundlePaths(ReactViewsRendererConfiguration reactConfiguration, ResourceResolver resolver) throws IOException {
+ ReactJSBundlePaths(ReactViewsRendererConfiguration reactConfiguration, ResourceResolver resolver) throws IOException {
Optional bundlePathOpt = resolver.getResource(reactConfiguration.getServerBundlePath());
if (bundlePathOpt.isEmpty()) {
throw new FileNotFoundException(format("Server bundle %s could not be found. Check your %s property.", reactConfiguration.getServerBundlePath(), ReactViewsRendererConfiguration.PREFIX + ".server-bundle-path"));
diff --git a/views-react/src/main/java/io/micronaut/views/react/JSContext.java b/views-react/src/main/java/io/micronaut/views/react/ReactJSContext.java
similarity index 96%
rename from views-react/src/main/java/io/micronaut/views/react/JSContext.java
rename to views-react/src/main/java/io/micronaut/views/react/ReactJSContext.java
index cb25c0327..371c80f3e 100644
--- a/views-react/src/main/java/io/micronaut/views/react/JSContext.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactJSContext.java
@@ -36,7 +36,7 @@
*/
@Internal
@Bean
-class JSContext implements AutoCloseable {
+class ReactJSContext implements AutoCloseable {
// Symbols the user's server side bundle might supply us with.
private static final List IMPORT_SYMBOLS = List.of("React", "ReactDOMServer", "renderToString", "h");
private final Engine engine;
@@ -47,11 +47,11 @@ class JSContext implements AutoCloseable {
Value render;
Value ssrModule;
- private final CompiledJS compiledJS;
+ private final CompiledReactJSBundle compiledJS;
private final ReactViewsRendererConfiguration configuration;
@Inject
- JSContext(CompiledJS compiledJS, ReactViewsRendererConfiguration configuration, Engine engine, HostAccess hostAccess) {
+ ReactJSContext(CompiledReactJSBundle compiledJS, ReactViewsRendererConfiguration configuration, Engine engine, HostAccess hostAccess) {
this.compiledJS = compiledJS;
this.configuration = configuration;
this.engine = engine;
diff --git a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
index bffe80817..d97cedcc5 100644
--- a/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactViewsRenderer.java
@@ -45,7 +45,7 @@ public class ReactViewsRenderer implements ViewsRenderer contextPool;
+ BeanPool contextPool;
/**
* Construct this renderer. Don't call it yourself, as Micronaut Views will set it up for you.
@@ -66,7 +66,7 @@ public ReactViewsRenderer() {
@Override
public @NonNull Writable render(@NonNull String viewName, @Nullable PROPS props, @Nullable HttpRequest> request) {
return writer -> {
- try (BeanPool.Handle contextHandle = contextPool.checkOut()) {
+ try (BeanPool.Handle contextHandle = contextPool.checkOut()) {
render(viewName, props, writer, contextHandle.get(), request);
} catch (BeanInstantiationException e) {
throw e;
@@ -84,7 +84,7 @@ public boolean exists(@NonNull String viewName) {
}
}
- private void render(String componentName, PROPS props, Writer writer, JSContext context, @Nullable HttpRequest> request) {
+ private void render(String componentName, PROPS props, Writer writer, ReactJSContext context, @Nullable HttpRequest> request) {
Value component = context.ssrModule.getMember(componentName);
if (component == null) {
throw new IllegalArgumentException("Component name %s wasn't exported from the SSR module.".formatted(componentName));
diff --git a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
index 5f4cc2be5..881004caf 100644
--- a/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
+++ b/views-react/src/test/groovy/io/micronaut/views/react/IntrospectableBeansAreProxiedSpec.groovy
@@ -15,7 +15,7 @@ class IntrospectableBeansAreProxiedSpec extends Specification {
void "introspectable bean can be proxied"() {
given:
- BeanPool.Handle jsContext = contextPool.checkOut()
+ BeanPool.Handle jsContext = contextPool.checkOut()
def context = jsContext.get().polyglotContext
def bean = new SomeBean("foo value", "bar value", new SomeBean.InnerBean(10, Map.of("key", 123), List.of("one", "two", "three")))
From 7e352f4cbbeb63e8f040e29012cfbefee8be7e24 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 15:50:53 +0200
Subject: [PATCH 16/31] ReactJS: Refactor more code into the bean factory.
---
.../views/react/CompiledReactJSBundle.java | 23 +++---
.../views/react/ReactJSBeanFactory.java | 28 +++++++
.../views/react/ReactJSBundlePaths.java | 78 -------------------
3 files changed, 38 insertions(+), 91 deletions(-)
delete mode 100644 views-react/src/main/java/io/micronaut/views/react/ReactJSBundlePaths.java
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java b/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
index 52581a7e8..7438a3ea2 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
@@ -15,6 +15,7 @@
*/
package io.micronaut.views.react;
+import io.micronaut.context.ApplicationContext;
import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.core.annotation.Internal;
import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
@@ -28,7 +29,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
+import java.nio.file.Paths;
/**
* Holds the thread-safe {@link Engine} and {@link Source} which together pin compiled machine code
@@ -41,14 +42,14 @@ class CompiledReactJSBundle implements AutoCloseable, ApplicationEventListener beanPool;
+ private final ApplicationContext applicationContext;
private Source source;
- private final ReactJSBundlePaths jsBundlePaths;
@Inject
- CompiledReactJSBundle(ReactJSBundlePaths jsBundlePaths, Engine engine, BeanPool beanPool) {
- this.jsBundlePaths = jsBundlePaths;
+ CompiledReactJSBundle(Engine engine, BeanPool beanPool, ApplicationContext applicationContext) {
this.engine = engine;
this.beanPool = beanPool;
+ this.applicationContext = applicationContext;
reload();
}
@@ -56,14 +57,6 @@ synchronized Source getSource() {
return source;
}
- private synchronized void reload() {
- try {
- source = jsBundlePaths.readServerBundle();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
@Override
@PreDestroy
public void close() throws Exception {
@@ -72,10 +65,14 @@ public void close() throws Exception {
@Override
public void onApplicationEvent(FileChangedEvent event) {
- if (jsBundlePaths.bundlePath != null && event.getPath().equals(jsBundlePaths.bundlePath) && event.getEventType() != WatchEventType.DELETE) {
+ if (event.getPath().equals(Paths.get(source.getPath())) && event.getEventType() != WatchEventType.DELETE) {
LOG.info("Reloading Javascript bundle due to file change.");
reload();
beanPool.clear();
}
}
+
+ private synchronized void reload() {
+ source = applicationContext.createBean(Source.class);
+ }
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/ReactJSBeanFactory.java b/views-react/src/main/java/io/micronaut/views/react/ReactJSBeanFactory.java
index 0e6f5d528..53c6e9c84 100644
--- a/views-react/src/main/java/io/micronaut/views/react/ReactJSBeanFactory.java
+++ b/views-react/src/main/java/io/micronaut/views/react/ReactJSBeanFactory.java
@@ -16,8 +16,10 @@
package io.micronaut.views.react;
import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.io.ResourceResolver;
import io.micronaut.views.react.util.BeanPool;
import io.micronaut.views.react.util.JavaUtilLoggingToSLF4J;
import io.micronaut.views.react.util.OutputStreamToSLF4J;
@@ -25,10 +27,20 @@
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.SandboxPolicy;
+import org.graalvm.polyglot.Source;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Optional;
+
+import static java.lang.String.format;
+
/**
* Allows the default Javascript context and host access policy to be controlled.
*/
@@ -68,4 +80,20 @@ Engine engine(ReactViewsRendererConfiguration configuration) {
.sandbox(sandbox ? SandboxPolicy.CONSTRAINED : SandboxPolicy.TRUSTED)
.build();
}
+
+ @Bean
+ Source serverBundle(ResourceResolver resolver, ReactViewsRendererConfiguration reactConfiguration) throws IOException, URISyntaxException {
+ Optional bundlePathOpt = resolver.getResource(reactConfiguration.getServerBundlePath());
+ if (bundlePathOpt.isEmpty()) {
+ throw new FileNotFoundException(format("Server bundle %s could not be found. Check your %s property.", reactConfiguration.getServerBundlePath(), ReactViewsRendererConfiguration.PREFIX + ".server-bundle-path"));
+ }
+ var bundleURL = bundlePathOpt.get();
+ Source.Builder sourceBuilder;
+ if (bundleURL.getProtocol().equals("file")) {
+ sourceBuilder = Source.newBuilder("js", new File(bundleURL.toURI()));
+ } else {
+ sourceBuilder = Source.newBuilder("js", bundleURL);
+ }
+ return sourceBuilder.mimeType("application/javascript+module").build();
+ }
}
diff --git a/views-react/src/main/java/io/micronaut/views/react/ReactJSBundlePaths.java b/views-react/src/main/java/io/micronaut/views/react/ReactJSBundlePaths.java
deleted file mode 100644
index b25299ffa..000000000
--- a/views-react/src/main/java/io/micronaut/views/react/ReactJSBundlePaths.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2017-2020 original authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.micronaut.views.react;
-
-import io.micronaut.core.annotation.Internal;
-import io.micronaut.core.annotation.Nullable;
-import io.micronaut.core.io.ResourceResolver;
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import org.graalvm.polyglot.Source;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.nio.file.Path;
-import java.util.Optional;
-
-import static java.lang.String.format;
-
-/**
- * Wraps the computation of where to find the JS for client and server.
- */
-@Singleton
-@Internal
-class ReactJSBundlePaths {
- private static final Logger LOG = LoggerFactory.getLogger(ReactJSBundlePaths.class);
-
- // Source code file name, for JS stack traces.
- private final String bundleFileName;
-
- // URL of bundle file, could be a file:// or in a classpath jar.
- private final URL bundleURL;
-
- // If a file:// (during development), the path of that file. Used for hot reloads.
- @Nullable
- final Path bundlePath;
-
- @Inject
- ReactJSBundlePaths(ReactViewsRendererConfiguration reactConfiguration, ResourceResolver resolver) throws IOException {
- Optional bundlePathOpt = resolver.getResource(reactConfiguration.getServerBundlePath());
- if (bundlePathOpt.isEmpty()) {
- throw new FileNotFoundException(format("Server bundle %s could not be found. Check your %s property.", reactConfiguration.getServerBundlePath(), ReactViewsRendererConfiguration.PREFIX + ".server-bundle-path"));
- }
- bundleURL = bundlePathOpt.get();
- bundleFileName = bundleURL.getFile();
- if (bundleURL.getProtocol().equals("file")) {
- bundlePath = Path.of(bundleURL.getPath());
- LOG.info("Using server-side JS bundle from local disk: {}", bundlePath);
- } else {
- bundlePath = null;
- }
- }
-
- Source readServerBundle() throws IOException {
- try (var reader = new BufferedReader(new InputStreamReader(bundleURL.openStream()))) {
- return Source.newBuilder("js", reader, bundleFileName)
- .mimeType("application/javascript+module")
- .build();
- }
- }
-}
From 760344a23ebaed08afe798b0776ce294803b6bed Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Mon, 12 Aug 2024 16:08:14 +0200
Subject: [PATCH 17/31] ReactJS: Use qualifiers to avoid conflicts with other
Micronaut code that uses Truffle classes.
---
.../views/react/CompiledReactJSBundle.java | 21 +++++++++++++++++--
.../views/react/ReactJSBeanFactory.java | 15 +++++++------
.../micronaut/views/react/ReactJSContext.java | 3 ++-
3 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java b/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
index 7438a3ea2..6ca8d12e5 100644
--- a/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
+++ b/views-react/src/main/java/io/micronaut/views/react/CompiledReactJSBundle.java
@@ -16,13 +16,16 @@
package io.micronaut.views.react;
import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.Qualifier;
import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.core.annotation.Internal;
+import io.micronaut.inject.BeanType;
import io.micronaut.scheduling.io.watch.event.FileChangedEvent;
import io.micronaut.scheduling.io.watch.event.WatchEventType;
import io.micronaut.views.react.util.BeanPool;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
+import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Source;
@@ -30,6 +33,7 @@
import org.slf4j.LoggerFactory;
import java.nio.file.Paths;
+import java.util.stream.Stream;
/**
* Holds the thread-safe {@link Engine} and {@link Source} which together pin compiled machine code
@@ -46,7 +50,7 @@ class CompiledReactJSBundle implements AutoCloseable, ApplicationEventListener beanPool, ApplicationContext applicationContext) {
+ CompiledReactJSBundle(@Named("react") Engine engine, BeanPool beanPool, ApplicationContext applicationContext) {
this.engine = engine;
this.beanPool = beanPool;
this.applicationContext = applicationContext;
@@ -72,7 +76,20 @@ public void onApplicationEvent(FileChangedEvent event) {
}
}
+ private static class ReactSourceQualifier implements Qualifier