From 6a6dabf89c2f91bfecb804175c977b5b30a3ff5d Mon Sep 17 00:00:00 2001 From: Alexandre Jacob Date: Fri, 1 May 2020 10:48:11 +0200 Subject: [PATCH] Add package-to-scan feature # Context Some applications can have thousands of classes. When JPA is configured to scan classes (`exclude-unlisted-classes` is `false` in `persistence.xml`), the time used to scan all classes can be quite long. With reasonable application size, class scanning can take only ten's of milliseconds but for heavy application it can take a few seconds. In order to help reducing startup time, we can restrict class scanning to a pre-configured list of known packages to avoid loading unnecessary classes metadata. # What was done - Introduced a new property `PersistenceUnitProperties.PACKAGES_TO_SCAN`: `eclipselink.packages-to-scan` - Modified `PersistenceUnitProcessor.getClassNamesFromURL` to only return eligible classes This new property takes as a value a list of packages as a string: `com.foo.bar, some.other.packages` # Performance result I've tested this feature with 2 applications, one with ~1000 classes and another with more than 30k classes. `MetadataProcessor.initPersistenceUnitClasses` was faster by ~25% for the small application and ~45% for the big one. Signed-off-by: Alexandre Jacob --- .../config/PersistenceUnitProperties.java | 9 +++ .../PersistenceUnitProcessorTest.java | 35 ++++++++++- .../deployment/PersistenceUnitProcessor.java | 62 +++++++++++++++++-- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java index 74c009314ac..6f800f82d92 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/config/PersistenceUnitProperties.java @@ -3906,6 +3906,15 @@ public class PersistenceUnitProperties { */ public static final String NAMING_INTO_INDEXED = "eclipselink.jpa.naming_into_indexed"; + /** + * The "eclipselink.packages-to-scan" property + * allows you to specify a comma separated list of packages you allow to be scanned. + * This could help reducing startup time. + *

+ * By default, no restriction will be applied and classes from every packages will be considered. + */ + public static final String PACKAGES_TO_SCAN = "eclipselink.packages-to-scan"; + /** * INTERNAL: The following properties will not be displayed through logging * but instead have an alternate value shown in the log. diff --git a/jpa/eclipselink.jpa.test/src/it/java/org/eclipse/persistence/testing/tests/jpa/advanced/PersistenceUnitProcessorTest.java b/jpa/eclipselink.jpa.test/src/it/java/org/eclipse/persistence/testing/tests/jpa/advanced/PersistenceUnitProcessorTest.java index a1fe8967ba1..089aefbfd5f 100644 --- a/jpa/eclipselink.jpa.test/src/it/java/org/eclipse/persistence/testing/tests/jpa/advanced/PersistenceUnitProcessorTest.java +++ b/jpa/eclipselink.jpa.test/src/it/java/org/eclipse/persistence/testing/tests/jpa/advanced/PersistenceUnitProcessorTest.java @@ -20,6 +20,7 @@ import junit.framework.Test; import junit.framework.TestSuite; +import org.eclipse.persistence.config.PersistenceUnitProperties; import org.eclipse.persistence.config.SystemProperties; import org.eclipse.persistence.exceptions.ValidationException; import org.eclipse.persistence.internal.jpa.deployment.ArchiveFactoryImpl; @@ -32,8 +33,7 @@ import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import jakarta.persistence.EntityManagerFactory; @@ -222,4 +222,35 @@ public void testGetArchiveFactoryOverride() { public static class AF1 extends ArchiveFactoryImpl {} public static class AF2 extends ArchiveFactoryImpl {} + public void testIsEligibleToScan() { + Assert.assertTrue(PersistenceUnitProcessor.isEligibleToScan(Collections.emptyList(), "foo/bar/MyClass.class")); + + List pathsToScan = new ArrayList<>(); + pathsToScan.add("foo/bar/"); + pathsToScan.add("com/test/"); + + Assert.assertTrue(PersistenceUnitProcessor.isEligibleToScan(pathsToScan, "foo/bar/MyClass.class")); + Assert.assertTrue(PersistenceUnitProcessor.isEligibleToScan(pathsToScan, "foo/bar/inner/MyClass.class")); + Assert.assertFalse(PersistenceUnitProcessor.isEligibleToScan(pathsToScan, "foo/MyClass.class")); + Assert.assertFalse(PersistenceUnitProcessor.isEligibleToScan(pathsToScan, "foo/barMyClass.class")); + + Assert.assertTrue(PersistenceUnitProcessor.isEligibleToScan(pathsToScan, "com/test/MyClass.class")); + + Assert.assertFalse(PersistenceUnitProcessor.isEligibleToScan(pathsToScan, "org/apache/SomeClass.class")); + } + + public void testPathsToScan() { + Assert.assertTrue(PersistenceUnitProcessor.pathsToScan(null).isEmpty()); + Assert.assertTrue(PersistenceUnitProcessor.pathsToScan("").isEmpty()); + Assert.assertTrue(PersistenceUnitProcessor.pathsToScan(" ").isEmpty()); + + List pathsToScan = PersistenceUnitProcessor.pathsToScan("foo.bar,com.test"); + Assert.assertEquals(2, pathsToScan.size()); + Assert.assertTrue(pathsToScan.contains("foo/bar/")); + Assert.assertTrue(pathsToScan.contains("com/test/")); + + List pathsToScan2 = PersistenceUnitProcessor.pathsToScan("foo.bar, "); + Assert.assertEquals(1, pathsToScan2.size()); + Assert.assertTrue(pathsToScan2.contains("foo/bar/")); + } } diff --git a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/deployment/PersistenceUnitProcessor.java b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/deployment/PersistenceUnitProcessor.java index 024a6511aa7..d273ce8a589 100644 --- a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/deployment/PersistenceUnitProcessor.java +++ b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/deployment/PersistenceUnitProcessor.java @@ -1,6 +1,6 @@ /* * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 1998, 2018 IBM Corporation. All rights reserved. + * Copyright (c) 1998, 2020 IBM Corporation. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -42,6 +42,7 @@ import java.security.PrivilegedActionException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; @@ -479,22 +480,22 @@ public static ArchiveFactory getArchiveFactory(ClassLoader loader, Map propertie } public static Set getClassNamesFromURL(URL url, ClassLoader loader, Map properties) { - Set classNames = new HashSet(); + Set classNames = new HashSet<>(); Archive archive = null; try { archive = PersistenceUnitProcessor.getArchiveFactory(loader, properties).createArchive(url, properties); + List pathsToScan = pathsToScan(properties != null ? (String) properties.get(PersistenceUnitProperties.PACKAGES_TO_SCAN) : null); + if (archive != null) { for (Iterator entries = archive.getEntries(); entries.hasNext();) { String entry = entries.next(); - if (entry.endsWith(".class")){ // NOI18N + if (entry.endsWith(".class") && isEligibleToScan(pathsToScan, entry)) { // NOI18N classNames.add(buildClassNameFromEntryString(entry)); } } } - } catch (URISyntaxException e) { - throw new RuntimeException("url = [" + url + "]", e); // NOI18N - } catch (IOException e) { + } catch (IOException | URISyntaxException e) { throw new RuntimeException("url = [" + url + "]", e); // NOI18N } finally { if (archive != null) { @@ -504,6 +505,55 @@ public static Set getClassNamesFromURL(URL url, ClassLoader loader, Map return classNames; } + /** + * Returns true if a class entry is eligible to scan. + * A class entry is eligible if: + * - pathsToScan is empty (not configured) + * - it resides in one of {@code pathsToScan} list + * + * @param pathsToScan list of paths where the class entry must be. Can be empty + * @param classEntry path of the class we want to check in the form : foo/bar/MyClass.class + * @return true is the class entry is eligible + */ + public static boolean isEligibleToScan(List pathsToScan, String classEntry) { + if (pathsToScan.isEmpty()) { + return true; + } + + for (String pathToScan : pathsToScan) { + if (classEntry.startsWith(pathToScan)) { + return true; + } + } + + return false; + } + + /** + * Takes a comma separated list of packages to scan and returns a list of + * paths. + * + * @param packagesToScanProperty list of packages to scan (comma-separated) + * @return list of paths to scans + */ + public static List pathsToScan(String packagesToScanProperty) { + if (packagesToScanProperty == null || packagesToScanProperty.isEmpty()) { + return Collections.emptyList(); + } + + List packagesToScan = new ArrayList<>(); + + for (String packageToScan : packagesToScanProperty.split(",")) { + String trimmedPackageToScan = packageToScan.trim(); + + if (!trimmedPackageToScan.isEmpty()) { + packagesToScan.add(trimmedPackageToScan.replace('.', '/') + "/"); + } + } + + return packagesToScan; + } + /** * Return if a given class is annotated with @Embeddable. */