Skip to content

Commit

Permalink
feat: add support for client endpoints live reload
Browse files Browse the repository at this point in the history
Adds the possibility to watch for changes in Hilla Java endpoints and
trigger Quarkus live reload without the need to refresh the brower page.
Quarkus will recompile the changed class and restart the server if
needed.
If 'quarkus.live-reload.instrumentation' is enabled, Quarkus may
redefine the class without a server restart. In this case, the extension
directly triggers Hilla hotswapper to regenerate client side code.

Fixes #261
Fixes #489
  • Loading branch information
mcollovati committed Sep 24, 2024
1 parent 54b959d commit dfbe682
Show file tree
Hide file tree
Showing 12 changed files with 715 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
Expand All @@ -61,6 +62,7 @@
import io.quarkus.deployment.builditem.ExcludeDependencyBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.LiveReloadBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.pkg.NativeConfig;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
Expand All @@ -82,6 +84,7 @@

import com.github.mcollovati.quarkus.hilla.BodyHandlerRecorder;
import com.github.mcollovati.quarkus.hilla.HillaAtmosphereObjectFactory;
import com.github.mcollovati.quarkus.hilla.HillaConfiguration;
import com.github.mcollovati.quarkus.hilla.HillaFormAuthenticationMechanism;
import com.github.mcollovati.quarkus.hilla.HillaSecurityPolicy;
import com.github.mcollovati.quarkus.hilla.HillaSecurityRecorder;
Expand All @@ -93,7 +96,8 @@
import com.github.mcollovati.quarkus.hilla.QuarkusNavigationAccessControl;
import com.github.mcollovati.quarkus.hilla.QuarkusVaadinServiceListenerPropagator;
import com.github.mcollovati.quarkus.hilla.crud.FilterableRepositorySupport;
import com.github.mcollovati.quarkus.hilla.deployment.asm.SpringReplacer;
import com.github.mcollovati.quarkus.hilla.deployment.asm.OffendingMethodCallsReplacer;
import com.github.mcollovati.quarkus.hilla.reload.HillaLiveReloadRecorder;
import com.github.mcollovati.quarkus.hilla.graal.DelayedInitBroadcaster;

class QuarkusHillaExtensionProcessor {
Expand Down Expand Up @@ -205,6 +209,17 @@ void installRequestBodyHandler(
}
}

@BuildStep(onlyIf = IsDevelopment.class)
@Record(value = ExecutionTime.RUNTIME_INIT)
void setupEndpointLiveReload(
LiveReloadBuildItem liveReloadBuildItem,
HillaConfiguration hillaConfiguration,
HillaLiveReloadRecorder recorder) {
if (hillaConfiguration.liveReload().enable()) {
recorder.startEndpointWatcher(liveReloadBuildItem.isLiveReload(), hillaConfiguration);
}
}

// EndpointsValidator checks for the presence of Spring, so it should be
// ignored
@BuildStep
Expand Down Expand Up @@ -292,8 +307,8 @@ void registerHillaPushServlet(BuildProducer<ServletBuildItem> servletProducer, N
}

@BuildStep
void replaceCallsToSpring(BuildProducer<BytecodeTransformerBuildItem> producer) {
SpringReplacer.addClassVisitors(producer);
void replaceOffendingMethodCalls(BuildProducer<BytecodeTransformerBuildItem> producer) {
OffendingMethodCallsReplacer.addClassVisitors(producer);
}

@BuildStep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.vaadin.hilla.EndpointInvoker;
import com.vaadin.hilla.EndpointRegistry;
import com.vaadin.hilla.EndpointUtil;
import com.vaadin.hilla.Hotswapper;
import com.vaadin.hilla.parser.utils.ConfigList;
import com.vaadin.hilla.push.PushEndpoint;
import com.vaadin.hilla.push.PushMessageHandler;
Expand All @@ -30,11 +31,15 @@

import com.github.mcollovati.quarkus.hilla.SpringReplacements;

public class SpringReplacer {
public class OffendingMethodCallsReplacer {

private static Map.Entry<MethodSignature, MethodSignature> ClassUtils_getUserClass = Map.entry(
MethodSignature.of("org/springframework/util/ClassUtils", "getUserClass"),
MethodSignature.of(SpringReplacements.class, "classUtils_getUserClass"));
private static Map.Entry<MethodSignature, MethodSignature> Class_forName = Map.entry(
MethodSignature.of(Class.class, "forName", "(Ljava/lang/String;)Ljava/lang/Class;"),
MethodSignature.of(SpringReplacements.class, "class_forName"));

private static Map.Entry<MethodSignature, MethodSignature> AuthenticationUtil_getSecurityHolderAuthentication =
Map.entry(
MethodSignature.of(
Expand All @@ -61,6 +66,7 @@ public class SpringReplacer {
MethodSignature.of(SpringReplacements.class, "endpointInvoker_createDefaultEndpointMapper"));

public static void addClassVisitors(BuildProducer<BytecodeTransformerBuildItem> producer) {
producer.produce(transform(Hotswapper.class, "affectsEndpoints", Class_forName));
producer.produce(transform(EndpointRegistry.class, "registerEndpoint", ClassUtils_getUserClass));
producer.produce(transform(EndpointUtil.class, "isAnonymousEndpoint", ClassUtils_getUserClass));
producer.produce(transform(EndpointInvoker.class, "checkAccess", ClassUtils_getUserClass));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ public String sayHello(String name) {
return "Hello " + name;
}
}

public int sum(int a, int b) {
return a + b;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2024 Marco Collovati, Dario Götze
*
* 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
*
* http://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 com.github.mcollovati.quarkus.hilla;

import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;

import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

/**
* Hilla configuration.
*/
@ConfigMapping(prefix = "vaadin.hilla")
@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public interface HillaConfiguration {

/**
* Configuration properties for endpoints live reload.
*
* @return configuration properties for endpoints live reload.
*/
LiveReloadConfig liveReload();

/**
* Configuration properties for endpoints hot reload.
* <p></p>
* The extension watches source folders for changes in Java files and triggers a live reload if affected classes are Hilla endpoints or used in endpoints.
*/
interface LiveReloadConfig {

/**
* Enabled endpoints live reload.
* @return {@literal true} if live reload is enabled, otherwise {@literal false}
*/
@WithDefault("true")
boolean enable();

/**
* The list of paths to watch for changes, relative to a root folder.
* <p></p>
* For example, given a SOURCE {@link #watchStrategy()} and Maven project with source code in the default {@literal src/main/java} folder and
* endpoints related classes in {@literal src/main/java/com/example/service} and {@literal src/main/java/com/example/model},
* the configuration should be {@literal vaadin.hilla.live-reload.watchedSourcePaths=com/example/service,com/example/model}.
* <p></p>
* By default, all sub folders are watched.
*
* @return the list of paths to watch for changes.
*/
Optional<Set<Path>> watchedPaths();

/**
* The strategy to use to watch for changes in Hilla endpoints.
* <p></p>
* @return the strategy to use to watch for changes in Hilla endpoints.
*/
@WithDefault("CLASS")
WatchStrategy watchStrategy();

/**
* The strategy to use to watch for changes in Hilla endpoints.
*/
enum WatchStrategy {
/**
* Watch for changes in source files
*/
SOURCE,
/**
* Watch for changes in compiled classes.
* <p></p>
* Best to be used in combination with {@literal quarkus.live-reload.instrumentation=true} to prevent excessive server restarts.
*/
CLASS
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.vaadin.flow.server.VaadinServletContext;
import com.vaadin.flow.server.auth.AccessAnnotationChecker;
import com.vaadin.flow.server.auth.NavigationAccessControl;
import com.vaadin.hilla.ApplicationContextProvider;
import com.vaadin.hilla.EndpointCodeGenerator;
import com.vaadin.hilla.EndpointController;
import com.vaadin.hilla.EndpointInvoker;
Expand Down Expand Up @@ -171,8 +172,17 @@ public ObjectMapper build() {

@Produces
@Singleton
ApplicationContext applicationContext(BeanManager beanManager) {
return new QuarkusApplicationContext(beanManager);
ApplicationContext applicationContext(BeanManager beanManager, ApplicationContextProvider appCtxProvider) {
QuarkusApplicationContext applicationContext = new QuarkusApplicationContext(beanManager);
appCtxProvider.setApplicationContext(applicationContext);
return applicationContext;
}

@Produces
@Singleton
@DefaultBean
ApplicationContextProvider applicationContextProvider() {
return new ApplicationContextProvider();
}

@Produces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,12 @@ private static SecurityIdentity currentIdentity() {
public static ObjectMapper endpointInvoker_createDefaultEndpointMapper(ApplicationContext context) {
return null;
}

public static Class<?> class_forName(String className) throws ClassNotFoundException {
try {
return Class.forName(className, true, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
return Class.forName(className);
}
}
}
Loading

0 comments on commit dfbe682

Please sign in to comment.