Skip to content

Commit a08f348

Browse files
committed
feat: add support for native image build
Adds support for native image build by registering classes for reflection and patching Atmosphere. Fixes #157
1 parent a7aae14 commit a08f348

8 files changed

+1329
-0
lines changed

deployment/src/main/java/com/vaadin/quarkus/deployment/VaadinQuarkusNativeProcessor.java

+362
Large diffs are not rendered by default.

deployment/src/main/java/com/vaadin/quarkus/deployment/nativebuild/AtmospherePatches.java

+400
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.quarkus.deployment.nativebuild;
18+
19+
import java.util.HashMap;
20+
import java.util.ListIterator;
21+
import java.util.Map;
22+
import java.util.function.Predicate;
23+
24+
import io.quarkus.gizmo.Gizmo;
25+
import org.objectweb.asm.MethodVisitor;
26+
import org.objectweb.asm.Opcodes;
27+
import org.objectweb.asm.Type;
28+
import org.objectweb.asm.tree.AbstractInsnNode;
29+
import org.objectweb.asm.tree.FieldInsnNode;
30+
import org.objectweb.asm.tree.InsnNode;
31+
import org.objectweb.asm.tree.LdcInsnNode;
32+
import org.objectweb.asm.tree.MethodInsnNode;
33+
import org.objectweb.asm.tree.MethodNode;
34+
35+
/**
36+
* Patches Atmosphere {@code InjectableObjectFactory} to remove the
37+
* {@code injectableServiceLoader} field and its initialization that is causing
38+
* issue during STATIC_INIT phase in native build, and replaces field usages
39+
* with a direct calls to {@code ServiceLoader.load(Injectable.class)}.
40+
*/
41+
final class InjectableObjectFactoryFieldUsageRemovalMethodVisitor
42+
extends MethodNode {
43+
public InjectableObjectFactoryFieldUsageRemovalMethodVisitor(
44+
MethodVisitor mv, int access, String name, String descriptor,
45+
String signature, String[] exceptions) {
46+
super(Gizmo.ASM_API_VERSION, access, name, descriptor, signature,
47+
exceptions);
48+
this.mv = mv;
49+
}
50+
51+
@Override
52+
public void visitEnd() {
53+
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
54+
Map<AbstractInsnNode, AbstractInsnNode> nodesToReplace = new HashMap<>();
55+
56+
FieldInsnNode fieldUsage;
57+
while ((fieldUsage = findFieldUsage(iterator)) != null) {
58+
if (fieldUsage.getOpcode() == Opcodes.PUTFIELD) {
59+
/*
60+
* Remove injectableServiceLoader initialization
61+
*
62+
* L6 LINENUMBER 66 L6 ALOAD 0 LDC
63+
* Lorg/atmosphere/inject/Injectable;.class INVOKESTATIC
64+
* java/util/ServiceLoader.load
65+
* (Ljava/lang/Class;)Ljava/util/ServiceLoader; PUTFIELD
66+
* org/atmosphere/inject/InjectableObjectFactory.
67+
* injectableServiceLoader : Ljava/util/ServiceLoader;
68+
*/
69+
nodesToReplace.put(iterator.previous(),
70+
new InsnNode(Opcodes.NOP)); // PUTFIELD
71+
nodesToReplace.put(iterator.previous(),
72+
new InsnNode(Opcodes.NOP)); // INVOKESTATIC
73+
nodesToReplace.put(iterator.previous(),
74+
new InsnNode(Opcodes.NOP)); // LDC
75+
nodesToReplace.put(iterator.previous(),
76+
new InsnNode(Opcodes.NOP)); // ALOAD
77+
78+
AbstractInsnNode stop = fieldUsage;
79+
findNextNode(iterator, n -> n == stop);
80+
} else if (fieldUsage.getOpcode() == Opcodes.GETFIELD) {
81+
/*
82+
* Replaces usage of injectableServiceLoader field with a call
83+
* to ServiceLoader.load(Injectable.class)
84+
*
85+
* L13 LINENUMBER 86 L13 FRAME FULL
86+
* [org/atmosphere/inject/InjectableObjectFactory
87+
* org/atmosphere/cpr/AtmosphereConfig java/lang/String] []
88+
* ALOAD 0 GETFIELD
89+
* org/atmosphere/inject/InjectableObjectFactory.
90+
* injectableServiceLoader : Ljava/util/ServiceLoader;
91+
* INVOKEVIRTUAL java/util/ServiceLoader.iterator
92+
* ()Ljava/util/Iterator; ASTORE 3
93+
*/
94+
nodesToReplace.put(iterator.previous(), new MethodInsnNode(
95+
Opcodes.INVOKESTATIC, "java/util/ServiceLoader", "load",
96+
"(Ljava/lang/Class;)Ljava/util/ServiceLoader;", false)); // GETFIELD
97+
nodesToReplace.put(iterator.previous(), new LdcInsnNode(
98+
Type.getType("Lorg/atmosphere/inject/Injectable;"))); // ALOAD
99+
AbstractInsnNode stop = fieldUsage;
100+
findNextNode(iterator, n -> n == stop);
101+
}
102+
}
103+
nodesToReplace.forEach(instructions::set);
104+
instructions.resetLabels();
105+
accept(mv);
106+
}
107+
108+
private FieldInsnNode findFieldUsage(
109+
ListIterator<AbstractInsnNode> iterator) {
110+
return (FieldInsnNode) findNextNode(iterator,
111+
node -> node instanceof FieldInsnNode field
112+
&& field.name.equals("injectableServiceLoader"));
113+
}
114+
115+
private AbstractInsnNode findNextNode(
116+
ListIterator<AbstractInsnNode> iterator,
117+
Predicate<AbstractInsnNode> test) {
118+
while (iterator.hasNext()) {
119+
AbstractInsnNode node = iterator.next();
120+
if (test.test(node)) {
121+
return node;
122+
}
123+
}
124+
return null;
125+
}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.quarkus.graal;
17+
18+
import jakarta.servlet.ServletConfig;
19+
import jakarta.servlet.ServletContext;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import org.atmosphere.cpr.AtmosphereFramework;
24+
25+
/**
26+
* Defers the initialization of executors at runtime to prevent failures during
27+
* native build.
28+
*/
29+
public class AtmosphereDeferredInitializer {
30+
31+
transient List<AtmosphereFramework> frameworks = new ArrayList<>();
32+
33+
/**
34+
* Called by @Recorder at RUNTIME_INIT to complete deferred Atmosphere
35+
* initialization that cannot be performed at build time (e.g. starting
36+
* thread pools).
37+
*
38+
* @param servletContext
39+
* the servlet context.
40+
*/
41+
static void completeInitialization(ServletContext servletContext) {
42+
AtmosphereDeferredInitializer initializer = getOrCreateInitializer(
43+
servletContext);
44+
initializer.frameworks.forEach(DelayedInitBroadcaster::startExecutors);
45+
}
46+
47+
/**
48+
* Takes a reference to an Atmosphere instance to defer the initialization
49+
* of Atmosphere at RUNTIME_INIT phase. The effective initialization is
50+
* performed by the AtmosphereDeferredInitializerRecorder @Recoder.
51+
*
52+
* @param config
53+
* the servlet config.
54+
* @param framework
55+
* the Atmosphere framework instance.
56+
*/
57+
public static void register(ServletConfig config,
58+
AtmosphereFramework framework) {
59+
ServletContext context = config.getServletContext();
60+
AtmosphereDeferredInitializer initializer = getOrCreateInitializer(
61+
context);
62+
initializer.frameworks.add(framework);
63+
}
64+
65+
private static AtmosphereDeferredInitializer getOrCreateInitializer(
66+
ServletContext context) {
67+
AtmosphereDeferredInitializer initializer = (AtmosphereDeferredInitializer) context
68+
.getAttribute(AtmosphereDeferredInitializer.class.getName());
69+
if (initializer == null) {
70+
initializer = new AtmosphereDeferredInitializer();
71+
context.setAttribute(AtmosphereDeferredInitializer.class.getName(),
72+
initializer);
73+
}
74+
return initializer;
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.quarkus.graal;
17+
18+
import io.quarkus.runtime.annotations.Recorder;
19+
import io.undertow.servlet.api.DeploymentManager;
20+
21+
/**
22+
* Initializes the Atmosphere framework at RUNTIME_INIT phase.
23+
*/
24+
@Recorder
25+
public class AtmosphereDeferredInitializerRecorder {
26+
27+
public void initAtmosphere(DeploymentManager deploymentManager) {
28+
AtmosphereDeferredInitializer.completeInitialization(
29+
deploymentManager.getDeployment().getServletContext());
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.quarkus.graal;
17+
18+
import jakarta.servlet.ServletConfig;
19+
import jakarta.servlet.ServletContext;
20+
import java.util.Enumeration;
21+
22+
import org.atmosphere.cpr.ApplicationConfig;
23+
24+
/**
25+
* A {@link ServletConfig} wrapper that forces the usage of
26+
* {@link DelayedInitBroadcaster} to prevent executors to be started during
27+
* static init in a native build.
28+
*/
29+
public class AtmosphereServletConfig implements ServletConfig {
30+
31+
private final ServletConfig delegate;
32+
33+
public AtmosphereServletConfig(ServletConfig delegate) {
34+
this.delegate = delegate;
35+
}
36+
37+
@Override
38+
public String getServletName() {
39+
return delegate.getServletName();
40+
}
41+
42+
@Override
43+
public ServletContext getServletContext() {
44+
return delegate.getServletContext();
45+
}
46+
47+
@Override
48+
public String getInitParameter(String name) {
49+
if (ApplicationConfig.BROADCASTER_CLASS.equals(name)) {
50+
return DelayedInitBroadcaster.class.getName();
51+
}
52+
return delegate.getInitParameter(name);
53+
}
54+
55+
@Override
56+
public Enumeration<String> getInitParameterNames() {
57+
return delegate.getInitParameterNames();
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.quarkus.graal;
17+
18+
import java.util.List;
19+
import java.util.Objects;
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
22+
import org.atmosphere.cpr.AtmosphereConfig;
23+
import org.atmosphere.cpr.AtmosphereFramework;
24+
import org.atmosphere.cpr.BroadcasterConfig;
25+
import org.atmosphere.cpr.DefaultBroadcaster;
26+
27+
/**
28+
* A broadcaster implementation that postpone schedulers initialization.
29+
* <p>
30+
* </p>
31+
* Usually initialization is performed during Atmosphere init() call, but this
32+
* prevents a native build to complete because starting threads at build time is
33+
* not supported. Postponed initialization is activated by a call to
34+
* {@link #startExecutors(AtmosphereFramework)}.
35+
*/
36+
public class DelayedInitBroadcaster extends DefaultBroadcaster {
37+
38+
private final AtomicBoolean executorsInitialized = new AtomicBoolean(false);
39+
40+
@Override
41+
protected BroadcasterConfig createBroadcasterConfig(
42+
AtmosphereConfig config) {
43+
return new DelayedInitBroadcasterConfig(
44+
config.framework().broadcasterFilters(), config, getID())
45+
.init();
46+
}
47+
48+
@Override
49+
protected void spawnReactor() {
50+
if (executorsInitialized.get()) {
51+
super.spawnReactor();
52+
}
53+
}
54+
55+
@Override
56+
protected void start() {
57+
if (!started.getAndSet(true)) {
58+
if (executorsInitialized.get()) {
59+
super.start();
60+
}
61+
}
62+
}
63+
64+
void delayedInit() {
65+
if (getBroadcasterConfig()instanceof DelayedInitBroadcasterConfig cfg) {
66+
if (executorsInitialized.compareAndSet(false, true)) {
67+
cfg.configExecutors();
68+
if (started.get()) {
69+
super.start();
70+
}
71+
}
72+
}
73+
}
74+
75+
static void startExecutors(AtmosphereFramework framework) {
76+
if (framework != null) {
77+
framework.getAtmosphereHandlers().values().stream()
78+
.map(h -> h.broadcaster).filter(Objects::nonNull)
79+
.filter(b -> b instanceof DelayedInitBroadcaster)
80+
.map(DelayedInitBroadcaster.class::cast)
81+
.forEach(DelayedInitBroadcaster::delayedInit);
82+
}
83+
}
84+
85+
private static class DelayedInitBroadcasterConfig
86+
extends BroadcasterConfig {
87+
88+
public DelayedInitBroadcasterConfig(List<String> broadcastFilters,
89+
AtmosphereConfig config, String broadcasterId) {
90+
super(broadcastFilters, config, false, broadcasterId);
91+
}
92+
93+
@Override
94+
protected synchronized void configExecutors() {
95+
super.configExecutors();
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)