Skip to content

Commit

Permalink
feat: add a common API to integrate with hotswap tools (#19603)
Browse files Browse the repository at this point in the history
* feat: add a common API to intergrate with hotswap tools

Adds API to integrate with hotswap agents and to allow plugging class change
reload plugins.
The hotswapper also tries to refresh the views instead of reloading the
page, if PUSH feature is enabled.

Part of #19261
Part of #19262

* don't refresh is navigation has not yet happened

* apply review suggestions

* ignore events after VaadinService is destroyed

* Update flow-server/src/main/java/com/vaadin/flow/router/internal/RouteRegistryHotswapper.java

---------

Co-authored-by: Mikhail Shabarov <[email protected]>
  • Loading branch information
mcollovati and mshabarov authored Jun 26, 2024
1 parent 0f04327 commit 6286b1e
Show file tree
Hide file tree
Showing 9 changed files with 1,688 additions and 2 deletions.
432 changes: 432 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* 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.vaadin.flow.hotswap;

import java.util.Set;

import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;

/**
* Implementor ot this interface are responsible for update Vaadin components
* when application classes change.
* <p>
* </p>
* Listener instances are by default discovered using Flow
* {@link com.vaadin.flow.di.Lookup} mechanisms. Implementors are usually
* discovered and instantiated using {@link java.util.ServiceLoader}, meaning
* that all implementations must have a zero-argument constructor and the fully
* qualified name of the implementation class must be listed on a separate line
* in a META-INF/services/com.vaadin.flow.hotswap.VaadinHotSwapper file present
* in the jar file containing the implementation class. Integrations for
* specific runtime environments, such as Spring and CDI, might also provide
* other ways of discovering implementors.
* <p>
* </p>
* For internal use only. May be renamed or removed in a future release.
*
* @author Vaadin Ltd
* @since 24.5
*/
public interface VaadinHotswapper {

/**
* Called by Vaadin hotswap entry point when one or more application classes
* have been updated.
* <p>
* </p>
* This method is meant to perform application-wide updates. Operation
* targeting Vaadin session should be implemented in
* {@link #onClassLoadEvent(VaadinSession, Set, boolean)} method.
*
* @param vaadinService
* active {@link VaadinService} instance.
* @param classes
* the set of changed classes.
* @param redefined
* {@literal true} if the classes have been redefined by hotswap
* mechanism, {@literal false} if they have been loaded for the
* first time by the ClassLoader.
* @return {@literal true} if a browser page reload is required,
* {@literal false} otherwise.
* @see #onClassLoadEvent(VaadinSession, Set, boolean)
*/
default boolean onClassLoadEvent(VaadinService vaadinService,
Set<Class<?>> classes, boolean redefined) {
// no-op by default
return false;
}

/**
* Called by Vaadin hotswap entry point when one or more application classes
* have been updated.
* <p>
* </p>
* This method is meant to perform updates at {@link VaadinSession} level.
* Operation targeting the entire application should be implemented in
* {@link #onClassLoadEvent(VaadinService, Set, boolean)} method.
*
* @param vaadinSession
* the {@link VaadinSession} to be potentially updated.
* @param classes
* the set of changed classes.
* @param redefined
* {@literal true} if the classes have been redefined by hotswap
* mechanism, {@literal false} if they have been loaded for the
* first time by the ClassLoader.
* @return {@literal true} if a browser page reload is required,
* {@literal false} otherwise.
* @see #onClassLoadEvent(VaadinService, Set, boolean)
*/
default boolean onClassLoadEvent(VaadinSession vaadinSession,
Set<Class<?>> classes, boolean redefined) {
// no-op by default
return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* 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.vaadin.flow.internal;

import java.util.Set;

import com.vaadin.flow.hotswap.VaadinHotswapper;
import com.vaadin.flow.server.VaadinService;

/**
* Clears all mappings from all reflection caches and related resources when one
* or more classes has been changed.
*/
public class ReflectionCacheHotswapper implements VaadinHotswapper {

@Override
public boolean onClassLoadEvent(VaadinService vaadinService,
Set<Class<?>> classes, boolean redefined) {
ReflectionCache.clearAll();
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* 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.vaadin.flow.router.internal;

import java.util.Set;
import java.util.stream.Stream;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.hotswap.VaadinHotswapper;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.SessionRouteRegistry;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;

/**
* A component that reacts on class changes to update route registries.
* <p>
* </p>
* This class is meant to be used in combination wit Flow
* {@link com.vaadin.flow.hotswap.Hotswapper} to immediately update routes
* registries when classes have been added or modified.
* <p>
* </p>
* For internal use only. May be renamed or removed in a future release.
*
* @since 24.5
*/
public class RouteRegistryHotswapper implements VaadinHotswapper {

/**
* Updates both application registry, to reflect provided class changes.
* <p>
* </p>
* For modified route classes, the following changes are taken into account:
* <ul>
* <li>{@link Route} annotation removed: the previous route is removed from
* the registry</li>
* <li>{@link Route#value()} modified</li>
* </ul>
*
*/
@Override
public boolean onClassLoadEvent(VaadinService vaadinService,
Set<Class<?>> classes, boolean redefined) {
Set<Class<?>> addedClasses = redefined ? Set.of() : classes;
Set<Class<?>> modifiedClasses = redefined ? classes : Set.of();
Set<Class<?>> removedClasses = Set.of();

if (hasComponentClasses(addedClasses, modifiedClasses,
removedClasses)) {
ApplicationRouteRegistry appRegistry = ApplicationRouteRegistry
.getInstance(vaadinService.getContext());
RouteUtil.updateRouteRegistry(appRegistry, addedClasses,
modifiedClasses, removedClasses);
}
return false;
}

@Override
public boolean onClassLoadEvent(VaadinSession session,
Set<Class<?>> classes, boolean redefined) {
Set<Class<?>> addedClasses = redefined ? Set.of() : classes;
Set<Class<?>> modifiedClasses = redefined ? classes : Set.of();
Set<Class<?>> removedClasses = Set.of();
if (session.getAttribute(SessionRouteRegistry.class) != null
&& hasComponentClasses(addedClasses, modifiedClasses,
removedClasses)) {
RouteUtil.updateRouteRegistry(
SessionRouteRegistry.getSessionRegistry(session), Set.of(),
modifiedClasses, removedClasses);
}
return false;
}

@SafeVarargs
private boolean hasComponentClasses(Set<Class<?>>... classes) {
return Stream.of(classes)
.filter(classSet -> classSet != null && !classSet.isEmpty())
.flatMap(Set::stream)
.anyMatch(Component.class::isAssignableFrom);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
Expand Down Expand Up @@ -377,8 +378,8 @@ public static void updateRouteRegistry(RouteRegistry registry,
Map<? extends Class<? extends Component>, RouteTarget> routeTargets = registry
.getRegisteredRoutes().stream()
.map(routeData -> routesMap.get(routeData.getTemplate()))
.collect(Collectors.toMap(RouteTarget::getTarget,
Function.identity()));
.filter(Objects::nonNull).collect(Collectors.toMap(
RouteTarget::getTarget, Function.identity()));
modifiedClassesRouteRemovalFilter = modifiedClassesRouteRemovalFilter
.and(clazz -> {
RouteTarget routeTarget = routeTargets.get(clazz);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# Copyright 2000-2024 Vaadin Ltd.
#
# 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.
#

com.vaadin.flow.router.internal.RouteRegistryHotswapper
com.vaadin.flow.internal.ReflectionCacheHotswapper
Loading

0 comments on commit 6286b1e

Please sign in to comment.