From 8261c8553b3076345af133820a56b5fd7768f60c Mon Sep 17 00:00:00 2001 From: Alex Landau Date: Wed, 21 Jul 2021 17:40:28 -0700 Subject: [PATCH 1/5] Speed up class uniqueness analyses by caching results of reading jars This will likely close to double the runtime performance of a single task (sharing results between main and test classpaths) and should significantly improve the performance of these tasks in monorepos. --- .../BaselineClassUniquenessPlugin.java | 12 ++- .../baseline/services/JarClassHasher.java | 97 +++++++++++++++++++ .../tasks/CheckClassUniquenessLockTask.java | 10 +- .../tasks/ClassUniquenessAnalyzer.java | 57 ++++------- 4 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineClassUniquenessPlugin.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineClassUniquenessPlugin.java index 62bf62a53..f4e67090f 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineClassUniquenessPlugin.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineClassUniquenessPlugin.java @@ -16,10 +16,12 @@ package com.palantir.baseline.plugins; +import com.palantir.baseline.services.JarClassHasher; import com.palantir.baseline.tasks.CheckClassUniquenessLockTask; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskProvider; import org.gradle.language.base.plugins.LifecycleBasePlugin; @@ -33,8 +35,14 @@ public class BaselineClassUniquenessPlugin extends AbstractBaselinePlugin { @Override public final void apply(Project project) { - TaskProvider checkClassUniqueness = - project.getTasks().register("checkClassUniqueness", CheckClassUniquenessLockTask.class); + Provider jarClassHasher = project.getGradle() + .getSharedServices() + .registerIfAbsent("jarClassHasher", JarClassHasher.class, _spec -> {}); + TaskProvider checkClassUniqueness = project.getTasks() + .register("checkClassUniqueness", CheckClassUniquenessLockTask.class, task -> { + task.jarClassHasher.set(jarClassHasher); + task.usesService(jarClassHasher); + }); project.getPlugins().apply(LifecycleBasePlugin.class); project.getTasks().getByName(LifecycleBasePlugin.CHECK_TASK_NAME).dependsOn(checkClassUniqueness); diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java new file mode 100644 index 000000000..2ac0f4bff --- /dev/null +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java @@ -0,0 +1,97 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.baseline.services; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.hash.HashCode; +import com.google.common.hash.Hashing; +import com.google.common.hash.HashingInputStream; +import com.google.common.io.ByteStreams; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.services.BuildService; +import org.gradle.api.services.BuildServiceParameters; + +public abstract class JarClassHasher implements BuildService, AutoCloseable { + private final Cache cache = + Caffeine.newBuilder().build(); + + public static class Result { + private final ImmutableSetMultimap hashesByClassName; + + private Result(ImmutableSetMultimap hashesByClassName) { + this.hashesByClassName = hashesByClassName; + } + + public ImmutableSetMultimap getHashesByClassName() { + return hashesByClassName; + } + + public static Result empty() { + return new Result(ImmutableSetMultimap.of()); + } + } + + public final Result hashClasses(ResolvedArtifact resolvedArtifact) { + return cache.get(resolvedArtifact.getModuleVersion().getId(), _moduleId -> { + File file = resolvedArtifact.getFile(); + if (!file.exists()) { + return Result.empty(); + } + + ImmutableSetMultimap.Builder hashesByClassName = ImmutableSetMultimap.builder(); + try (FileInputStream fileInputStream = new FileInputStream(file); + JarInputStream jarInputStream = new JarInputStream(fileInputStream)) { + JarEntry entry; + while ((entry = jarInputStream.getNextJarEntry()) != null) { + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + + if (entry.getName().contains("module-info.class")) { + // Java 9 allows jars to have a module-info.class file in the root, + // we shouldn't complain about these. + continue; + } + + String className = entry.getName().replaceAll("/", ".").replaceAll("\\.class$", ""); + HashingInputStream inputStream = new HashingInputStream(Hashing.sha256(), jarInputStream); + ByteStreams.exhaust(inputStream); + + hashesByClassName.put(className, inputStream.hash()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return new Result(hashesByClassName.build()); + }); + } + + @Override + public final void close() { + // Try to free up memory when this is no longer needed + cache.invalidateAll(); + cache.cleanUp(); + } +} diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/CheckClassUniquenessLockTask.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/CheckClassUniquenessLockTask.java index e0d5e1c5c..e359f4605 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/CheckClassUniquenessLockTask.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/CheckClassUniquenessLockTask.java @@ -19,6 +19,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; +import com.palantir.baseline.services.JarClassHasher; import difflib.DiffUtils; import difflib.Patch; import java.io.File; @@ -36,6 +37,7 @@ import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.provider.Property; import org.gradle.api.provider.SetProperty; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.CacheableTask; @@ -55,10 +57,14 @@ public class CheckClassUniquenessLockTask extends DefaultTask { @SuppressWarnings("VisibilityModifier") public final SetProperty configurations; + @SuppressWarnings("VisibilityModifier") + public final Property jarClassHasher; + private final File lockFile; public CheckClassUniquenessLockTask() { this.configurations = getProject().getObjects().setProperty(Configuration.class); + this.jarClassHasher = getProject().getObjects().property(JarClassHasher.class); this.lockFile = getProject().file("baseline-class-uniqueness.lock"); onlyIf(new Spec() { @Override @@ -91,8 +97,8 @@ public final void doIt() { ImmutableSortedMap> resultsByConfiguration = configurations.get().stream() .collect(ImmutableSortedMap.toImmutableSortedMap( Comparator.naturalOrder(), Configuration::getName, configuration -> { - ClassUniquenessAnalyzer analyzer = - new ClassUniquenessAnalyzer(getProject().getLogger()); + ClassUniquenessAnalyzer analyzer = new ClassUniquenessAnalyzer( + jarClassHasher.get(), getProject().getLogger()); analyzer.analyzeConfiguration(configuration); Collection> problemJars = analyzer.getDifferingProblemJars(); diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java index 7736367a0..7d5bb6f7d 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java @@ -18,13 +18,10 @@ import static java.util.stream.Collectors.toSet; +import com.google.common.collect.ImmutableSetMultimap; import com.google.common.hash.HashCode; -import com.google.common.hash.Hashing; -import com.google.common.hash.HashingInputStream; -import com.google.common.io.ByteStreams; +import com.palantir.baseline.services.JarClassHasher; import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.Collection; @@ -32,8 +29,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; import java.util.stream.Collectors; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ModuleVersionIdentifier; @@ -41,12 +36,13 @@ import org.slf4j.Logger; public final class ClassUniquenessAnalyzer { - + private final JarClassHasher jarHasher; private final Map, Set> jarsToClasses = new HashMap<>(); private final Map> classToHashCodes = new HashMap<>(); private final Logger log; - public ClassUniquenessAnalyzer(Logger log) { + public ClassUniquenessAnalyzer(JarClassHasher jarHasher, Logger log) { + this.jarHasher = jarHasher; this.log = log; } @@ -60,43 +56,26 @@ public void analyzeConfiguration(Configuration configuration) { Map> classToJars = new HashMap<>(); Map> tempClassToHashCodes = new HashMap<>(); - dependencies.stream().forEach(resolvedArtifact -> { + for (ResolvedArtifact resolvedArtifact : dependencies) { File file = resolvedArtifact.getFile(); if (!file.exists()) { log.info("Skipping non-existent jar {}: {}", resolvedArtifact, file); return; } - try (FileInputStream fileInputStream = new FileInputStream(file); - JarInputStream jarInputStream = new JarInputStream(fileInputStream)) { - JarEntry entry; - while ((entry = jarInputStream.getNextJarEntry()) != null) { - if (entry.isDirectory() || !entry.getName().endsWith(".class")) { - continue; - } - - if (entry.getName().contains("module-info.class")) { - // Java 9 allows jars to have a module-info.class file in the root, - // we shouldn't complain about these. - continue; - } - - String className = entry.getName().replaceAll("/", ".").replaceAll(".class", ""); - HashingInputStream inputStream = new HashingInputStream(Hashing.sha256(), jarInputStream); - ByteStreams.exhaust(inputStream); - - multiMapPut( - classToJars, - className, - resolvedArtifact.getModuleVersion().getId()); - - multiMapPut(tempClassToHashCodes, className, inputStream.hash()); - } - } catch (IOException e) { - log.error("Failed to read JarFile {}", resolvedArtifact, e); - throw new RuntimeException(e); + ImmutableSetMultimap hashes = + jarHasher.hashClasses(resolvedArtifact).getHashesByClassName(); + + for (Map.Entry entry : hashes.entries()) { + String className = entry.getKey(); + HashCode hashValue = entry.getValue(); + multiMapPut( + classToJars, + className, + resolvedArtifact.getModuleVersion().getId()); + multiMapPut(tempClassToHashCodes, className, hashValue); } - }); + } // discard all the classes that only come from one jar - these are completely safe! classToJars.entrySet().stream() From 00a24c808a7c8113690dd5fad767e8a9cf5c4204 Mon Sep 17 00:00:00 2001 From: svc-changelog Date: Thu, 22 Jul 2021 03:31:17 +0000 Subject: [PATCH 2/5] Add generated changelog entries --- changelog/@unreleased/pr-1837.v2.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog/@unreleased/pr-1837.v2.yml diff --git a/changelog/@unreleased/pr-1837.v2.yml b/changelog/@unreleased/pr-1837.v2.yml new file mode 100644 index 000000000..a07e70afa --- /dev/null +++ b/changelog/@unreleased/pr-1837.v2.yml @@ -0,0 +1,8 @@ +type: improvement +improvement: + description: |- + Increase the speed of the `checkClassUniqueness` task, especially in large repos, by adding caching of jar information. + + Fix class names listed in the `baseline-class-uniqueness.lock` when the class or package name contains `class`. In rare cases, this may require running `./gradlew checkClassUniqueness --write-locks` to update the files. + links: + - https://github.com/palantir/gradle-baseline/pull/1837 From b27b64c9082dbded3c016e27fc5639e0870a6f46 Mon Sep 17 00:00:00 2001 From: Alex Landau Date: Wed, 21 Jul 2021 20:32:59 -0700 Subject: [PATCH 3/5] Small changelog edit --- changelog/@unreleased/pr-1837.v2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/@unreleased/pr-1837.v2.yml b/changelog/@unreleased/pr-1837.v2.yml index a07e70afa..808fbdc4e 100644 --- a/changelog/@unreleased/pr-1837.v2.yml +++ b/changelog/@unreleased/pr-1837.v2.yml @@ -3,6 +3,6 @@ improvement: description: |- Increase the speed of the `checkClassUniqueness` task, especially in large repos, by adding caching of jar information. - Fix class names listed in the `baseline-class-uniqueness.lock` when the class or package name contains `class`. In rare cases, this may require running `./gradlew checkClassUniqueness --write-locks` to update the files. + Fix class names listed in the `baseline-class-uniqueness.lock` when the class or package name contains the substring `class`. In rare cases, this may require running `./gradlew checkClassUniqueness --write-locks` to update the files. links: - https://github.com/palantir/gradle-baseline/pull/1837 From d097af116994259fb550802d76d2f6e1c9c65604 Mon Sep 17 00:00:00 2001 From: Alex Landau Date: Fri, 20 Aug 2021 14:10:10 -0700 Subject: [PATCH 4/5] Address code review feedback --- .../baseline/services/JarClassHasher.java | 2 +- .../tasks/ClassUniquenessAnalyzer.java | 41 +++++++------------ 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java index 2ac0f4bff..16da80130 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java @@ -37,7 +37,7 @@ public abstract class JarClassHasher implements BuildService cache = Caffeine.newBuilder().build(); - public static class Result { + public static final class Result { private final ImmutableSetMultimap hashesByClassName; private Result(ImmutableSetMultimap hashesByClassName) { diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java index 7d5bb6f7d..9a98da2e0 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java @@ -18,15 +18,16 @@ import static java.util.stream.Collectors.toSet; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.SetMultimap; import com.google.common.hash.HashCode; import com.palantir.baseline.services.JarClassHasher; import java.io.File; import java.time.Duration; import java.time.Instant; import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -37,8 +38,8 @@ public final class ClassUniquenessAnalyzer { private final JarClassHasher jarHasher; - private final Map, Set> jarsToClasses = new HashMap<>(); - private final Map> classToHashCodes = new HashMap<>(); + private final SetMultimap, String> jarsToClasses = HashMultimap.create(); + private final SetMultimap classToHashCodes = HashMultimap.create(); private final Logger log; public ClassUniquenessAnalyzer(JarClassHasher jarHasher, Logger log) { @@ -53,8 +54,8 @@ public void analyzeConfiguration(Configuration configuration) { // we use these temporary maps to accumulate information as we process each jar, // so they may include singletons which we filter out later - Map> classToJars = new HashMap<>(); - Map> tempClassToHashCodes = new HashMap<>(); + SetMultimap classToJars = HashMultimap.create(); + SetMultimap tempClassToHashCodes = HashMultimap.create(); for (ResolvedArtifact resolvedArtifact : dependencies) { File file = resolvedArtifact.getFile(); @@ -69,24 +70,20 @@ public void analyzeConfiguration(Configuration configuration) { for (Map.Entry entry : hashes.entries()) { String className = entry.getKey(); HashCode hashValue = entry.getValue(); - multiMapPut( - classToJars, - className, - resolvedArtifact.getModuleVersion().getId()); - multiMapPut(tempClassToHashCodes, className, hashValue); + classToJars.put(className, resolvedArtifact.getModuleVersion().getId()); + tempClassToHashCodes.put(className, hashValue); } } // discard all the classes that only come from one jar - these are completely safe! - classToJars.entrySet().stream() + classToJars.asMap().entrySet().stream() .filter(entry -> entry.getValue().size() > 1) - .forEach(entry -> multiMapPut(jarsToClasses, entry.getValue(), entry.getKey())); + .forEach(entry -> jarsToClasses.put(ImmutableSet.copyOf(entry.getValue()), entry.getKey())); // figure out which classes have differing hashes - tempClassToHashCodes.entrySet().stream() + tempClassToHashCodes.asMap().entrySet().stream() .filter(entry -> entry.getValue().size() > 1) - .forEach(entry -> - entry.getValue().forEach(value -> multiMapPut(classToHashCodes, entry.getKey(), value))); + .forEach(entry -> entry.getValue().forEach(value -> classToHashCodes.put(entry.getKey(), value))); Instant after = Instant.now(); log.info( @@ -105,7 +102,7 @@ private Collection> getProblemJars() { } /** Class names that appear in all of the given jars. */ - public Set getSharedClassesInProblemJars(Collection problemJars) { + public Set getSharedClassesInProblemJars(Set problemJars) { return jarsToClasses.get(problemJars); } @@ -117,17 +114,9 @@ public Collection> getDifferingProblemJars() { } /** Class names which appear in all of the given jars and also have non-identical implementations. */ - public Set getDifferingSharedClassesInProblemJars(Collection problemJars) { + public Set getDifferingSharedClassesInProblemJars(Set problemJars) { return getSharedClassesInProblemJars(problemJars).stream() .filter(classToHashCodes::containsKey) .collect(toSet()); } - - private static void multiMapPut(Map> map, K key, V value) { - map.compute(key, (unused, collection) -> { - Set newCollection = collection != null ? collection : new HashSet<>(); - newCollection.add(value); - return newCollection; - }); - } } From 7e73be830fd4d6892a43919849e94be7ead53909 Mon Sep 17 00:00:00 2001 From: Alex Landau Date: Tue, 7 Sep 2021 12:45:04 -0700 Subject: [PATCH 5/5] Add logging when a jar has multiple entries for one class --- .../baseline/services/JarClassHasher.java | 28 +++++++++++++++++-- .../tasks/ClassUniquenessAnalyzer.java | 2 +- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java index 16da80130..34eab9e99 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/services/JarClassHasher.java @@ -18,6 +18,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; @@ -26,12 +27,16 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; +import java.util.stream.Collectors; import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.services.BuildService; import org.gradle.api.services.BuildServiceParameters; +import org.slf4j.Logger; public abstract class JarClassHasher implements BuildService, AutoCloseable { private final Cache cache = @@ -53,14 +58,14 @@ public static Result empty() { } } - public final Result hashClasses(ResolvedArtifact resolvedArtifact) { + public final Result hashClasses(ResolvedArtifact resolvedArtifact, Logger logger) { return cache.get(resolvedArtifact.getModuleVersion().getId(), _moduleId -> { File file = resolvedArtifact.getFile(); if (!file.exists()) { return Result.empty(); } - ImmutableSetMultimap.Builder hashesByClassName = ImmutableSetMultimap.builder(); + ImmutableListMultimap.Builder hashesByClassName = ImmutableListMultimap.builder(); try (FileInputStream fileInputStream = new FileInputStream(file); JarInputStream jarInputStream = new JarInputStream(fileInputStream)) { JarEntry entry; @@ -84,7 +89,24 @@ public final Result hashClasses(ResolvedArtifact resolvedArtifact) { } catch (IOException e) { throw new RuntimeException(e); } - return new Result(hashesByClassName.build()); + + ImmutableListMultimap builtHashesByClassName = hashesByClassName.build(); + List keysWithDuplicateEntries = builtHashesByClassName.asMap().entrySet().stream() + .filter(entry -> entry.getValue().size() > 1) + .map(Map.Entry::getKey) + .sorted() + .collect(Collectors.toList()); + if (!keysWithDuplicateEntries.isEmpty()) { + logger.warn( + "Warning: Gradle Baseline found a dependency jar that contains more than one zip entry for " + + "a class and is likely malformed: {}\n" + + "The following entries appear multiple times: {}\n" + + "This issue should be reported to the maintainer of the dependency.", + resolvedArtifact.getModuleVersion(), + keysWithDuplicateEntries); + } + + return new Result(ImmutableSetMultimap.copyOf(builtHashesByClassName)); }); } diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java index 9a98da2e0..212dcd4d2 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/tasks/ClassUniquenessAnalyzer.java @@ -65,7 +65,7 @@ public void analyzeConfiguration(Configuration configuration) { } ImmutableSetMultimap hashes = - jarHasher.hashClasses(resolvedArtifact).getHashesByClassName(); + jarHasher.hashClasses(resolvedArtifact, log).getHashesByClassName(); for (Map.Entry entry : hashes.entries()) { String className = entry.getKey();