diff --git a/flow-tests/vaadin-spring-tests/pom.xml b/flow-tests/vaadin-spring-tests/pom.xml
index d9db694125d..6d73ceaca51 100644
--- a/flow-tests/vaadin-spring-tests/pom.xml
+++ b/flow-tests/vaadin-spring-tests/pom.xml
@@ -335,6 +335,7 @@
test-spring-boot-only-prepare
test-spring-white-list
+ test-spring-filter-packages
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/pom.xml
new file mode 100644
index 00000000000..4bfc2a17e1a
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/pom.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ 4.0.0
+
+ vaadin-test-spring-filter-packages
+ com.vaadin
+ 24.5-SNAPSHOT
+
+ vaadin-test-spring-filter-packages-lib-allowed
+ Library with vaadin.allowed-packages property
+
+
+ jar
+
+ true
+
+
+
+
+ com.vaadin
+ flow-server
+ ${project.version}
+
+
+
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/BlockedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/BlockedRoute.java
new file mode 100644
index 00000000000..42753d36dea
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/BlockedRoute.java
@@ -0,0 +1,29 @@
+/*
+ * 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.spring.test.allowed;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+
+/**
+ * Blocked route in a jar package.
+ */
+@Route("blocked-route-in-jar")
+public class BlockedRoute extends Div {
+
+ public BlockedRoute() {
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/CustomVaadinServiceInitListener.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/CustomVaadinServiceInitListener.java
new file mode 100644
index 00000000000..42bf8c6cced
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/CustomVaadinServiceInitListener.java
@@ -0,0 +1,27 @@
+/*
+ * 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.spring.test.allowed.startup;
+
+import com.vaadin.flow.server.ServiceInitEvent;
+import com.vaadin.flow.server.VaadinServiceInitListener;
+
+public class CustomVaadinServiceInitListener
+ implements VaadinServiceInitListener {
+
+ @Override
+ public void serviceInit(ServiceInitEvent event) {
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/vaadin/AllowedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/vaadin/AllowedRoute.java
new file mode 100644
index 00000000000..5b265fe6147
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/vaadin/AllowedRoute.java
@@ -0,0 +1,26 @@
+/*
+ * 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.spring.test.allowed.startup.vaadin;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+
+@Route(value = "allowed-route-in-jar")
+public class AllowedRoute extends Div {
+
+ public AllowedRoute() {
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/resources/META-INF/VAADIN/package.properties b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/resources/META-INF/VAADIN/package.properties
new file mode 100644
index 00000000000..27b339a7c3b
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/resources/META-INF/VAADIN/package.properties
@@ -0,0 +1 @@
+vaadin.allowed-packages=com/vaadin/flow/spring/test/allowed/startup
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/pom.xml
new file mode 100644
index 00000000000..15a6afbb362
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/pom.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ 4.0.0
+
+ vaadin-test-spring-filter-packages
+ com.vaadin
+ 24.5-SNAPSHOT
+
+ vaadin-test-spring-filter-packages-lib-blocked
+ Library with vaadin.blocked-packages property
+
+
+ jar
+
+ true
+
+
+
+
+ com.vaadin
+ flow-server
+ ${project.version}
+
+
+
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/ScannedAllowedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/ScannedAllowedRoute.java
new file mode 100644
index 00000000000..10d49a1a14c
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/ScannedAllowedRoute.java
@@ -0,0 +1,26 @@
+/*
+ * 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.spring.test.blocked;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+
+@Route("allowed-route-in-another-jar")
+public class ScannedAllowedRoute extends Div {
+
+ public ScannedAllowedRoute() {
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/BlockedCustomVaadinServiceInitListener.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/BlockedCustomVaadinServiceInitListener.java
new file mode 100644
index 00000000000..35fc7c8bcb2
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/BlockedCustomVaadinServiceInitListener.java
@@ -0,0 +1,27 @@
+/*
+ * 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.spring.test.blocked.startup;
+
+import com.vaadin.flow.server.ServiceInitEvent;
+import com.vaadin.flow.server.VaadinServiceInitListener;
+
+public class BlockedCustomVaadinServiceInitListener
+ implements VaadinServiceInitListener {
+
+ @Override
+ public void serviceInit(ServiceInitEvent event) {
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/vaadin/ScannedBlockedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/vaadin/ScannedBlockedRoute.java
new file mode 100644
index 00000000000..f7f46d1bcf3
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/vaadin/ScannedBlockedRoute.java
@@ -0,0 +1,26 @@
+/*
+ * 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.spring.test.blocked.startup.vaadin;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+
+@Route(value = "blocked-route-in-scanned-jar")
+public class ScannedBlockedRoute extends Div {
+
+ public ScannedBlockedRoute() {
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/resources/META-INF/VAADIN/package.properties b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/resources/META-INF/VAADIN/package.properties
new file mode 100644
index 00000000000..7bb9c099f12
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/resources/META-INF/VAADIN/package.properties
@@ -0,0 +1 @@
+vaadin.blocked-packages=com/vaadin/flow/spring/test/blocked/startup
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude/pom.xml
new file mode 100644
index 00000000000..71fb3f7a0ce
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude/pom.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ 4.0.0
+
+ vaadin-test-spring-filter-packages
+ com.vaadin
+ 24.5-SNAPSHOT
+
+ vaadin-test-spring-filter-packages-lib-exclude
+ Library with vaadin.blocked-jar property
+
+
+ jar
+
+ true
+
+
+
+
+ com.vaadin
+ flow-server
+ ${project.version}
+
+
+
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/pom.xml
new file mode 100644
index 00000000000..0a1b762ffbc
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/pom.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ 4.0.0
+
+ vaadin-spring-tests
+ com.vaadin
+ 24.5-SNAPSHOT
+
+ vaadin-test-spring-filter-packages
+ The main module for a Spring boot package filter tests
+
+
+ pom
+
+ true
+
+
+
+ lib-allowed
+ lib-blocked
+ lib-exclude
+ ui
+
+
+
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/pom.xml
new file mode 100644
index 00000000000..cf88cab2bc7
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/pom.xml
@@ -0,0 +1,134 @@
+
+
+
+
+ 4.0.0
+
+ vaadin-test-spring-filter-packages
+ com.vaadin
+ 24.5-SNAPSHOT
+
+ vaadin-test-spring-filter-packages-ui
+ jar
+
+
+ true
+
+
+
+
+ com.vaadin
+ vaadin-spring
+ ${project.version}
+
+
+
+ com.vaadin
+ vaadin-dev-server
+ ${project.version}
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jetty
+
+
+
+ com.vaadin
+ vaadin-test-spring-filter-packages-lib-allowed
+ ${project.version}
+
+
+
+ com.vaadin
+ vaadin-test-spring-filter-packages-lib-blocked
+ ${project.version}
+
+
+
+
+
+
+
+ com.vaadin
+ flow-maven-plugin
+ ${project.version}
+
+
+
+ prepare-frontend
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+ 100
+ 2500
+ 9009
+
+ -Xdebug
+ -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=18888
+
+
+
+
+
+ pre-integration-test
+
+ start
+
+
+
+ post-integration-test
+
+ stop
+
+
+
+
+
+
+
+
+
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/ClassScannerView.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/ClassScannerView.java
new file mode 100644
index 00000000000..15930edffee
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/ClassScannerView.java
@@ -0,0 +1,40 @@
+/*
+ * 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.spring.test.filtering;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.router.Route;
+
+@Route("")
+public class ClassScannerView extends Div {
+
+ public static Set> classes = Collections.emptySet();
+ public static final String SCANNED_CLASSES = "scanned-classes";
+
+ public ClassScannerView() {
+ Span scannedClasses = new Span(classes.stream()
+ .map(Class::getSimpleName).collect(Collectors.joining(",")));
+ scannedClasses.setId(SCANNED_CLASSES);
+ add(scannedClasses);
+ }
+
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/TestServletInitializer.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/TestServletInitializer.java
new file mode 100644
index 00000000000..4639a0371a7
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/TestServletInitializer.java
@@ -0,0 +1,58 @@
+/*
+ * 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.spring.test.filtering;
+
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.vaadin.flow.spring.VaadinServletContextInitializer;
+
+/**
+ * The entry point of the Spring Boot application.
+ */
+@SpringBootApplication
+@Configuration
+public class TestServletInitializer {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TestServletInitializer.class, args);
+ }
+
+ @Bean
+ public VaadinServletContextInitializer vaadinServletContextInitializer(
+ ApplicationContext context) {
+
+ return new VaadinServletContextInitializer(context) {
+ @Override
+ protected Set> findClassesForDevMode(
+ Set basePackages,
+ List> annotations,
+ List> superTypes) {
+ ClassScannerView.classes = super.findClassesForDevMode(
+ basePackages, annotations, superTypes);
+ return ClassScannerView.classes;
+ }
+ };
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/blocked/BlockedView.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/blocked/BlockedView.java
new file mode 100644
index 00000000000..2345e312883
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/blocked/BlockedView.java
@@ -0,0 +1,27 @@
+/*
+ * 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.spring.test.filtering.blocked;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+
+@Route("blocked")
+public class BlockedView extends Div {
+
+ public BlockedView() {
+ }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/resources/application.properties
new file mode 100644
index 00000000000..a9214ff1f4f
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+server.port=8888
+vaadin.blocked-packages=blocked,com/vaadin/flow/spring/test/filtering/blocked
diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/test/java/com/vaadin/flow/spring/test/filtering/ClassScannerIT.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/test/java/com/vaadin/flow/spring/test/filtering/ClassScannerIT.java
new file mode 100644
index 00000000000..7ba72c0a4f6
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/test/java/com/vaadin/flow/spring/test/filtering/ClassScannerIT.java
@@ -0,0 +1,88 @@
+/*
+ * 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.spring.test.filtering;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import com.vaadin.flow.spring.test.allowed.startup.CustomVaadinServiceInitListener;
+import com.vaadin.flow.spring.test.allowed.startup.vaadin.AllowedRoute;
+import com.vaadin.flow.spring.test.allowed.BlockedRoute;
+import com.vaadin.flow.spring.test.blocked.ScannedAllowedRoute;
+import com.vaadin.flow.spring.test.blocked.startup.BlockedCustomVaadinServiceInitListener;
+import com.vaadin.flow.spring.test.blocked.startup.vaadin.ScannedBlockedRoute;
+import com.vaadin.flow.spring.test.filtering.blocked.BlockedView;
+import com.vaadin.flow.testutil.ChromeBrowserTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+
+/**
+ * Primary target of this IT is class scanning of DevModeServletContextListener
+ * in {@link com.vaadin.flow.spring.VaadinServletContextInitializer} and
+ * especially usage of {@code vaadin.blocked-packages} and
+ * {@code vaadin.allowed-packages} in a multi-module Maven project with jar
+ * packaged dependencies.
+ */
+public class ClassScannerIT extends ChromeBrowserTest {
+
+ @Test
+ public void uiModule_withBlockedPackages() {
+ open();
+ assertClassAllowed(ClassScannerView.class.getSimpleName());
+ assertClassBlocked(BlockedView.class.getSimpleName());
+ }
+
+ @Test
+ public void libAllowedModule_withAllowedPackagesJar() {
+ open();
+ assertClassAllowed(AllowedRoute.class.getSimpleName());
+ assertClassAllowed(
+ CustomVaadinServiceInitListener.class.getSimpleName());
+ assertClassBlocked(BlockedRoute.class.getSimpleName());
+ }
+
+ @Test
+ public void libBlockedModule_withBlockedPackagesJar() {
+ open();
+ assertClassBlocked(ScannedBlockedRoute.class.getSimpleName());
+ assertClassBlocked(
+ BlockedCustomVaadinServiceInitListener.class.getSimpleName());
+ assertClassAllowed(ScannedAllowedRoute.class.getSimpleName());
+ }
+
+ private void assertClassAllowed(String className) {
+ Assert.assertTrue(className + " should be allowed.",
+ getScannedClasses().contains(className));
+ }
+
+ private void assertClassBlocked(String className) {
+ Assert.assertFalse(className + " should be blocked.",
+ getScannedClasses().contains(className));
+ }
+
+ private List getScannedClasses() {
+ return Stream.of(findElement(By.id(ClassScannerView.SCANNED_CLASSES))
+ .getText().split(",")).map(String::trim).toList();
+ }
+
+ @Override
+ protected String getTestPath() {
+ return "/";
+ }
+}
diff --git a/scripts/computeMatrix.js b/scripts/computeMatrix.js
index 7040642ceb0..a713f6ed112 100755
--- a/scripts/computeMatrix.js
+++ b/scripts/computeMatrix.js
@@ -90,6 +90,10 @@ const moduleWeights = {
'flow-tests/vaadin-spring-tests/test-spring-boot-scan': { pos: 4, weight: 4 },
'flow-tests/vaadin-spring-tests/test-spring-boot-contextpath': { pos: 4, weight: 4 },
'flow-tests/vaadin-spring-tests/test-spring-boot-reverseproxy': { pos: 4, weight: 4 },
+ 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui': { pos: 4, weight: 4 },
+ 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed': { pos: 4, weight: 4 },
+ 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked': { pos: 4, weight: 4 },
+ 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude': { pos: 4, weight: 4 },
'flow-tests/vaadin-spring-tests/test-spring-white-list': { pos: 4, weight: 3 },
'flow-tests/vaadin-spring-tests/test-spring-common': { pos: 4, weight: 2 },
'flow-tests/vaadin-spring-tests/test-spring-helpers': { pos: 4, weight: 1 },
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java
index d201b142cd0..7b6380aae31 100644
--- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java
@@ -37,6 +37,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -85,6 +86,7 @@
import com.vaadin.flow.server.startup.WebComponentExporterAwareValidator;
import com.vaadin.flow.server.webcomponent.WebComponentConfigurationRegistry;
import com.vaadin.flow.spring.VaadinScanPackagesRegistrar.VaadinScanPackages;
+import com.vaadin.flow.spring.io.FilterableResourceResolver;
import com.vaadin.flow.theme.Theme;
/**
@@ -523,9 +525,8 @@ public void failFastContextInitialized(ServletContextEvent event)
collectHandleTypes(devModeHandlerManager.getHandlesTypes(),
annotations, superTypes);
- Set> classes = findByAnnotationOrSuperType(basePackages,
- customLoader, annotations, superTypes)
- .collect(Collectors.toSet());
+ Set> classes = findClassesForDevMode(basePackages,
+ annotations, superTypes);
if (devModeCachingEnabled) {
classes.addAll(ReloadCache.jarClasses);
@@ -592,6 +593,13 @@ private boolean isScanOnlySet() {
}
+ protected Set> findClassesForDevMode(Set basePackages,
+ List> annotations,
+ List> superTypes) {
+ return findByAnnotationOrSuperType(basePackages, customLoader,
+ annotations, superTypes).collect(Collectors.toSet());
+ }
+
private class WebComponentServletContextListener
implements FailFastServletContextListener {
@@ -801,7 +809,7 @@ private CompositeServletContextListener createCompositeListener(
private Stream> findByAnnotation(Collection packages,
Class extends Annotation>... annotations) {
- return findByAnnotation(packages, appContext, annotations);
+ return findByAnnotation(packages, customLoader, annotations);
}
private Stream> findByAnnotation(Collection packages,
@@ -812,7 +820,7 @@ private Stream> findByAnnotation(Collection packages,
Stream> findBySuperType(Collection packages,
Class> type) {
- return findBySuperType(packages, appContext, type);
+ return findBySuperType(packages, customLoader, type);
}
private Stream> findBySuperType(Collection packages,
@@ -921,7 +929,7 @@ private static void collectHandleTypes(Class>[] handleTypes,
* with atmosphere we skip known packaged from our resources collection.
*/
private static class CustomResourceLoader
- extends PathMatchingResourcePatternResolver {
+ extends FilterableResourceResolver {
private final PrefixTree scanNever = new PrefixTree(DEFAULT_SCAN_NEVER);
@@ -1013,22 +1021,25 @@ private Resource[] collectResources(String locationPattern)
.replace(".class", "");
ReloadCache.jarClassNames.add(className);
}
- if (shouldPathBeScanned(relativePath)) {
+ if (shouldPathBeScanned(relativePath,
+ path.substring(0, index))) {
resourcesList.add(resource);
}
} else {
List parents = rootPaths.stream()
- .filter(path::startsWith)
- .collect(Collectors.toList());
+ .filter(path::startsWith).toList();
if (parents.isEmpty()) {
throw new IllegalStateException(String.format(
"Parent resource of [%s] not found in the resources!",
path));
}
-
+ AtomicBoolean parentIsAllowedByPackageProperties = new AtomicBoolean(
+ true);
if (parents.stream()
.anyMatch(parent -> shouldPathBeScanned(
- path.substring(parent.length())))) {
+ path.substring(parent.length()),
+ parent,
+ parentIsAllowedByPackageProperties))) {
resourcesList.add(resource);
}
}
@@ -1047,9 +1058,67 @@ private Resource[] collectResources(String locationPattern)
return resourcesList.toArray(new Resource[0]);
}
+ /**
+ * Checks if the given path should be scanned.
+ *
+ * @param path
+ * the relative path to check
+ * @return {@code true} if the path should be scanned, {@code false}
+ * otherwise
+ */
private boolean shouldPathBeScanned(String path) {
return scanAlways.hasPrefix(path) || !scanNever.hasPrefix(path);
}
+
+ /**
+ * Checks if the given path should be scanned. Checks
+ * package.properties.
+ *
+ * @param path
+ * the relative path to check
+ * @param rootPath
+ * the root path of the resource. Also, a key for cached
+ * properties.
+ * @return {@code true} if the path should be scanned, {@code false}
+ * otherwise
+ */
+ private boolean shouldPathBeScanned(String path, String rootPath) {
+ return shouldPathBeScanned(path, rootPath, null);
+ }
+
+ /**
+ * Checks if the given path should be scanned. Checks
+ * package.properties.
+ *
+ * @param path
+ * the relative path to check
+ * @param rootPath
+ * the root path of the resource. Also, a key for cached
+ * properties.
+ * @param parentIsAllowedByPackageProperties
+ * This value is used as a default value for the
+ * package.properties check. Value of the object may be
+ * changed, if result changes. null defaults to true.
+ * @return {@code true} if the path should be scanned, {@code false}
+ * otherwise
+ */
+ private boolean shouldPathBeScanned(String path, String rootPath,
+ AtomicBoolean parentIsAllowedByPackageProperties) {
+ if (shouldPathBeScanned(path)) {
+ // The given parentIsAllowedByPackageProperties ensures that
+ // result from the previous check follows up here as a default
+ // value.
+ boolean defaultValue = parentIsAllowedByPackageProperties == null
+ || parentIsAllowedByPackageProperties.get();
+ boolean allowed = isAllowedByPackageProperties(rootPath, path,
+ defaultValue);
+ if (parentIsAllowedByPackageProperties != null) {
+ parentIsAllowedByPackageProperties.set(allowed);
+ }
+ return allowed;
+ }
+ return false;
+ }
}
private static Logger getLogger() {
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/io/FilterableResourceResolver.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/io/FilterableResourceResolver.java
new file mode 100644
index 00000000000..10b143860a8
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/io/FilterableResourceResolver.java
@@ -0,0 +1,388 @@
+/*
+ * 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.spring.io;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.UrlResource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.PropertiesLoaderUtils;
+import org.springframework.util.ResourceUtils;
+
+/**
+ * A {@link PathMatchingResourcePatternResolver} that allows filtering resources
+ * by package properties. The resolver reads META-INF/VAADIN/package.properties
+ * from JAR files and directories. The properties file can contain a list of the
+ * allowed or blocked packages. If it contains both, the allowed packages take
+ * precedence. Allowed packages are mapped with the key
+ * "vaadin.allowed-packages". Blocked packages are mapped with the key
+ * "vaadin.blocked-packages".
+ *
+ * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver
+ */
+public class FilterableResourceResolver
+ extends PathMatchingResourcePatternResolver implements Serializable {
+
+ private static final String JAR_PROTOCOL = "jar:";
+ private static final String JAR_KEY = ".jar!/";
+ private static final String PACKAGE_PROPERTIES_PATH = "META-INF/VAADIN/package.properties";
+
+ /**
+ * The property key for allowed packages.
+ */
+ public static final String ALLOWED_PACKAGES_PROPERTY = "vaadin.allowed-packages";
+ /**
+ * The property key for blocked packages.
+ */
+ public static final String BLOCKED_PACKAGES_PROPERTY = "vaadin.blocked-packages";
+
+ private final Map propertiesCache = new HashMap<>();
+
+ private record PackageInfo(Set allowedPackages,
+ Set blockedPackages) implements Serializable {
+ }
+
+ /**
+ * Creates a new instance of the resolver.
+ *
+ * @param resourceLoader
+ * the resource loader to use
+ */
+ public FilterableResourceResolver(ResourceLoader resourceLoader) {
+ super(resourceLoader);
+ }
+
+ private static Logger getLogger() {
+ return LoggerFactory.getLogger(FilterableResourceResolver.class);
+ }
+
+ private String toJarPath(String path) {
+ return path.substring(0, path.lastIndexOf(JAR_KEY) + JAR_KEY.length());
+ }
+
+ private String pathToKey(String path) {
+ String key = path.substring(0, path.lastIndexOf(JAR_KEY));
+ if (key.startsWith(JAR_PROTOCOL)) {
+ // clear the jar: prefix
+ key = key.substring(4);
+ }
+ return key;
+ }
+
+ /**
+ * Checks if the given path is a JAR file.
+ *
+ * @param path
+ * the path to check. Not null.
+ * @return {@code true} if the path is a JAR file, {@code false} otherwise
+ */
+ protected boolean isJar(String path) {
+ return path.lastIndexOf(JAR_KEY) != -1;
+ }
+
+ private Resource doResolveRootDirResource(Resource original)
+ throws IOException {
+ String rootDirPath = original.getURI().getPath();
+ if (rootDirPath != null) {
+ int index = rootDirPath.lastIndexOf(JAR_KEY);
+ if (index != -1) {
+ String jarPath = rootDirPath.substring(0,
+ index + JAR_KEY.length());
+ return new UrlResource(jarPath);
+ }
+ }
+ return super.resolveRootDirResource(original);
+ }
+
+ /**
+ * Find all resources in jar files that match the given location pattern via
+ * the Ant-style PathMatcher. Supports additional filtering based on allowed
+ * or blocked packages in package.properties.
+ *
+ * @param rootDirResource
+ * the root directory as Resource
+ * @param rootDirUrl
+ * the pre-resolved root directory URL
+ * @param subPattern
+ * the sub pattern to match (below the root directory)
+ * @return a mutable Set of matching Resource instances
+ * @throws IOException
+ * in case of I/O error
+ */
+ @Override
+ protected Set doFindPathMatchingJarResources(
+ Resource rootDirResource, URL rootDirUrl, String subPattern)
+ throws IOException {
+ String path = rootDirResource.getURI().toString();
+ cachePackageProperties(path, rootDirResource, rootDirUrl);
+
+ if (isBlockedJar(rootDirResource)) {
+ return Collections.emptySet();
+ }
+ return super.doFindPathMatchingJarResources(rootDirResource, rootDirUrl,
+ subPattern);
+ }
+
+ /**
+ * Find all class path resources with the given path via the configured
+ * ClassLoader. Called by findAllClassPathResources(String). Supports
+ * additional filtering based on allowed or blocked packages in
+ * package.properties.
+ *
+ * @param path
+ * the absolute path within the class path (never a leading
+ * slash)
+ * @return a mutable Set of matching Resource instances
+ * @throws IOException
+ * in case of I/O errors
+ */
+ @Override
+ protected Set doFindAllClassPathResources(String path)
+ throws IOException {
+ var result = super.doFindAllClassPathResources(path);
+ result.removeIf(res -> {
+ cachePackageProperties(res);
+ return isBlockedJar(res);
+ });
+ return result;
+ }
+
+ private void cachePackageProperties(String path, Resource rootDirResource,
+ URL rootDirUrl) throws IOException {
+ if (!propertiesCache.containsKey(path)) {
+ if (isJar(path)) {
+ String jarPath = pathToKey(path);
+ propertiesCache.put(jarPath, readPackageProperties(rootDirUrl,
+ path, doResolveRootDirResource(rootDirResource)));
+ getLogger().trace("Caching package.properties of JAR {}", path);
+ } else {
+ Resource resource = doFindPathMatchingFileResources(
+ rootDirResource, PACKAGE_PROPERTIES_PATH).stream()
+ .findFirst().orElse(null);
+ Properties properties = resource != null
+ ? PropertiesLoaderUtils.loadProperties(resource)
+ : null;
+ propertiesCache.put(path, createPackageInfo(properties));
+ getLogger().trace("Caching package.properties of directory {}",
+ path);
+ }
+ }
+ }
+
+ private void cachePackageProperties(Resource res) {
+ try {
+ Resource rootDirResource = convertClassLoaderURL(res.getURL());
+ String rootDirPath = rootDirResource.getURI().toString();
+ String rootPath = rootDirResource.getURI().getPath();
+ if (rootPath != null && isJar(rootDirPath)) {
+ String jarPath = toJarPath(rootDirPath);
+ String key = pathToKey(rootPath);
+ if (!propertiesCache.containsKey(key)) {
+ propertiesCache.put(key, readPackageProperties(null,
+ jarPath, rootDirResource));
+ getLogger().trace("Caching package.properties of JAR {}",
+ rootPath);
+ }
+ } else if (!propertiesCache.containsKey(rootPath)) {
+ Resource resource = doFindPathMatchingFileResources(
+ rootDirResource, PACKAGE_PROPERTIES_PATH).stream()
+ .findFirst().orElse(null);
+ Properties properties = resource != null
+ ? PropertiesLoaderUtils.loadProperties(resource)
+ : null;
+ propertiesCache.put(rootPath, createPackageInfo(properties));
+ getLogger().trace("Caching package.properties of directory {}",
+ rootPath);
+ }
+
+ } catch (IOException e) {
+ getLogger().warn("Failed to find {} for path {}",
+ PACKAGE_PROPERTIES_PATH, res, e);
+ }
+ }
+
+ /**
+ * Returns whether the given resource is a blocked jar and shouldn't be
+ * included.
+ *
+ * @param resource
+ * the resource to check
+ * @return {@code true} if the resource is a blocked jar, {@code false}
+ * otherwise
+ */
+ protected boolean isBlockedJar(Resource resource) {
+ // placeholder to handle case of package.properties with
+ // vaadin.blocked-jar=true
+ return false;
+ }
+
+ /**
+ * See {@link super#doFindPathMatchingJarResources(Resource, URL, String)}.
+ * This method is slightly adjusted from the origin to just read
+ * META-INF/VAADIN/package.properties and transform it to Properties object.
+ */
+ private PackageInfo readPackageProperties(URL jarPathURL, String jarPath,
+ Resource rootDirResource) throws IOException {
+ URLConnection con = null;
+ JarFile jarFile;
+ String jarFileUrl;
+ String urlFile = null;
+ boolean closeJarFile;
+ if (jarPathURL != null) {
+ con = jarPathURL.openConnection();
+ urlFile = jarPathURL.getPath();
+ }
+ if (con instanceof JarURLConnection jarCon) {
+ jarFile = jarCon.getJarFile();
+ closeJarFile = !jarCon.getUseCaches();
+ } else {
+ // No JarURLConnection -> need to resort to URL file parsing.
+ // We'll assume URLs of the format "jar:path!/entry", with the
+ // protocol
+ // being arbitrary as long as following the entry format.
+ // We'll also handle paths with and without leading "file:"
+ // prefix.
+ urlFile = urlFile != null ? urlFile : jarPath;
+ try {
+ int separatorIndex = urlFile
+ .indexOf(ResourceUtils.WAR_URL_SEPARATOR);
+ if (separatorIndex == -1) {
+ separatorIndex = urlFile
+ .indexOf(ResourceUtils.JAR_URL_SEPARATOR);
+ }
+ if (separatorIndex != -1) {
+ jarFileUrl = urlFile.substring(0, separatorIndex);
+ jarFile = getJarFile(jarFileUrl);
+ } else {
+ jarFile = new JarFile(urlFile);
+ }
+ closeJarFile = true;
+ } catch (ZipException ex) {
+ if (getLogger().isDebugEnabled()) {
+ getLogger().debug("Skipping invalid jar class path entry ["
+ + urlFile + "]");
+ }
+ return null;
+ }
+ }
+
+ try {
+ if (getLogger().isTraceEnabled()) {
+ getLogger().trace("Looking for package.properties in jar file ["
+ + rootDirResource + "]");
+ }
+ for (Enumeration entries = jarFile.entries(); entries
+ .hasMoreElements();) {
+ JarEntry entry = entries.nextElement();
+ String entryPath = entry.getName();
+ if (entryPath.endsWith(PACKAGE_PROPERTIES_PATH)) {
+ Resource resource = doFindPathMatchingFileResources(
+ rootDirResource, PACKAGE_PROPERTIES_PATH).stream()
+ .findFirst().orElseGet(() -> {
+ try {
+ return rootDirResource.createRelative(
+ PACKAGE_PROPERTIES_PATH);
+ } catch (IOException e) {
+ getLogger().warn(
+ "Could not read package.properties",
+ e);
+ return null;
+ }
+ });
+ Properties prop = resource != null
+ ? PropertiesLoaderUtils.loadProperties(resource)
+ : null;
+ if (getLogger().isTraceEnabled()) {
+ getLogger().trace("Read package.properties: [{}]",
+ prop);
+ }
+ return prop != null ? createPackageInfo(prop) : null;
+ }
+ }
+ return null;
+ } finally {
+ if (closeJarFile) {
+ jarFile.close();
+ }
+ }
+ }
+
+ /**
+ * Check if the target path is allowed by the package properties.
+ *
+ * @param rootPath
+ * Root path as a key for the cached properties
+ * @param targetPath
+ * relative path to check
+ * @param defaultValue
+ * default value to return if the properties are not found
+ * @return {@code true} if the target path is allowed by the package
+ * properties,
+ */
+ protected boolean isAllowedByPackageProperties(String rootPath,
+ String targetPath, boolean defaultValue) {
+ PackageInfo packageInfo = propertiesCache.get(rootPath);
+ if (packageInfo == null) {
+ return defaultValue;
+ }
+
+ if (!packageInfo.allowedPackages().isEmpty()) {
+ return packageInfo.allowedPackages().stream()
+ .anyMatch(targetPath::startsWith);
+ } else if (!packageInfo.blockedPackages().isEmpty()) {
+ return packageInfo.blockedPackages().stream()
+ .noneMatch(targetPath::startsWith);
+ }
+ return defaultValue;
+ }
+
+ private PackageInfo createPackageInfo(Properties properties) {
+ if (properties == null) {
+ return null;
+ }
+ Set allowedPackages = Stream
+ .of(properties.getProperty(ALLOWED_PACKAGES_PROPERTY, "")
+ .split(","))
+ .filter(pkg -> !pkg.isBlank()).map(String::trim)
+ .map(pkg -> pkg.replace(".", "/")).collect(Collectors.toSet());
+ Set blockedPackages = Stream
+ .of(properties.getProperty(BLOCKED_PACKAGES_PROPERTY, "")
+ .split(","))
+ .filter(pkg -> !pkg.isBlank()).map(String::trim)
+ .map(pkg -> pkg.replace(".", "/")).collect(Collectors.toSet());
+ return new PackageInfo(allowedPackages, blockedPackages);
+ }
+}