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 Aug 29, 2024
1 parent 58f20d0 commit 16ac8b4
Show file tree
Hide file tree
Showing 11 changed files with 752 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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 @@ -60,6 +61,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.builditem.CurateOutcomeBuildItem;
import io.quarkus.undertow.deployment.IgnoredServletContainerInitializerBuildItem;
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 @@ -92,7 +95,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;

class QuarkusHillaExtensionProcessor {

Expand Down Expand Up @@ -203,6 +207,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 @@ -283,8 +298,8 @@ void registerHillaPushServlet(
}

@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 @@ -57,6 +62,7 @@ public class SpringReplacer {
MethodSignature.DROP_METHOD);

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,68 @@
/*
* 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 source folder.
* <p></p>
* For example, given a 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 source folders are watched.
*
* @return the list of source paths to watch for changes.
*/
Optional<Set<Path>> watchedSourcePaths();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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 @@ -170,8 +171,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();
}

private EndpointController endpointController;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ public static Function<String, Boolean> authenticationUtil_getSecurityHolderRole
}
return role -> role != null && identity.hasRole(role);
}

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 16ac8b4

Please sign in to comment.