Skip to content

Commit b2ffdd9

Browse files
authored
Merge pull request #333 from zentol/memory
Eagerly filter classes
2 parents 498fc20 + 9ae1c98 commit b2ffdd9

File tree

3 files changed

+175
-54
lines changed

3 files changed

+175
-54
lines changed

japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java

Lines changed: 156 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@
1313
import japicmp.model.JavaObjectSerializationCompatibility;
1414
import japicmp.output.OutputFilter;
1515
import japicmp.util.AnnotationHelper;
16+
import java.util.function.Supplier;
1617
import javassist.ClassPool;
1718
import javassist.CtClass;
1819
import javassist.NotFoundException;
1920

2021
import java.io.File;
2122
import java.io.IOException;
22-
import java.util.ArrayList;
23+
import java.io.InputStream;
2324
import java.util.Collections;
2425
import java.util.Enumeration;
26+
import java.util.Iterator;
2527
import java.util.LinkedList;
2628
import java.util.List;
2729
import java.util.jar.JarEntry;
@@ -36,9 +38,9 @@
3638
*/
3739
public class JarArchiveComparator {
3840
private static final Logger LOGGER = Logger.getLogger(JarArchiveComparator.class.getName());
39-
private ClassPool commonClassPool;
40-
private ClassPool oldClassPool;
41-
private ClassPool newClassPool;
41+
private ReducibleClassPool commonClassPool;
42+
private ReducibleClassPool oldClassPool;
43+
private ReducibleClassPool newClassPool;
4244
private String commonClassPathAsString = "";
4345
private String oldClassPathAsString = "";
4446
private String newClassPathAsString = "";
@@ -102,12 +104,12 @@ private void checkJavaObjectSerializationCompatibility(List<JApiClass> jApiClass
102104

103105
private void setupClasspaths() {
104106
if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.ONE_COMMON_CLASSPATH) {
105-
commonClassPool = new ClassPool();
107+
commonClassPool = new ReducibleClassPool();
106108
commonClassPathAsString = setupClasspath(commonClassPool, this.options.getClassPathEntries());
107109
} else if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.TWO_SEPARATE_CLASSPATHS) {
108-
oldClassPool = new ClassPool();
110+
oldClassPool = new ReducibleClassPool();
109111
oldClassPathAsString = setupClasspath(oldClassPool, this.options.getOldClassPath());
110-
newClassPool = new ClassPool();
112+
newClassPool = new ReducibleClassPool();
111113
newClassPathAsString = setupClasspath(newClassPool, this.options.getNewClassPath());
112114
} else {
113115
throw new JApiCmpException(Reason.IllegalState, "Unknown classpath mode: " + this.options.getClassPathMode());
@@ -201,10 +203,8 @@ private List<JApiClass> createAndCompareClassLists(List<File> oldArchives, List<
201203
* @return a list of {@link japicmp.model.JApiClass} that represent the changes
202204
*/
203205
List<JApiClass> compareClassLists(JarArchiveComparatorOptions options, List<CtClass> oldClasses, List<CtClass> newClasses) {
204-
List<CtClass> oldClassesFiltered = applyFilter(options, oldClasses);
205-
List<CtClass> newClassesFiltered = applyFilter(options, newClasses);
206206
ClassesComparator classesComparator = new ClassesComparator(this, options);
207-
classesComparator.compare(oldClassesFiltered, newClassesFiltered);
207+
classesComparator.compare(oldClasses, newClasses);
208208
List<JApiClass> classList = classesComparator.getClasses();
209209
if (LOGGER.isLoggable(Level.FINE)) {
210210
for (JApiClass jApiClass : classList) {
@@ -217,55 +217,53 @@ List<JApiClass> compareClassLists(JarArchiveComparatorOptions options, List<CtCl
217217
return classList;
218218
}
219219

220-
private List<CtClass> applyFilter(JarArchiveComparatorOptions options, List<CtClass> ctClasses) {
221-
List<CtClass> newList = new ArrayList<>(ctClasses.size());
222-
for (CtClass ctClass : ctClasses) {
223-
if (options.getFilters().includeClass(ctClass)) {
224-
newList.add(ctClass);
225-
}
226-
}
227-
return newList;
220+
private List<CtClass> createListOfCtClasses(List<File> archives, ReducibleClassPool classPool) {
221+
return createListOfCtClasses(() -> new JarsCtClassIterable(archives, classPool), classPool);
222+
}
223+
224+
List<CtClass> createListOfCtClasses(Supplier<Iterable<CtClass>> ctClasses, ReducibleClassPool classPool) {
225+
return loadAndFilterClasses(ctClasses, classPool, false);
228226
}
229227

230-
private List<CtClass> createListOfCtClasses(List<File> archives, ClassPool classPool) {
228+
private List<CtClass> loadAndFilterClasses(Supplier<Iterable<CtClass>> ctClasses, ReducibleClassPool classPool, boolean ignorePackageFilters) {
229+
// marks whether any package was found
230+
// if so we need to go over _all_ classes again
231+
boolean packageFilterEncountered = false;
232+
231233
List<CtClass> classes = new LinkedList<>();
232-
for (File archive : archives) {
233-
if (LOGGER.isLoggable(Level.FINE)) {
234-
LOGGER.fine("Loading classes from jar file '" + archive.getAbsolutePath() + "'");
235-
}
236-
try (JarFile jarFile = new JarFile(archive)) {
237-
Enumeration<JarEntry> entryEnumeration = jarFile.entries();
238-
while (entryEnumeration.hasMoreElements()) {
239-
JarEntry jarEntry = entryEnumeration.nextElement();
240-
String name = jarEntry.getName();
241-
if (name.endsWith(".class")) {
242-
CtClass ctClass;
243-
try {
244-
ctClass = classPool.makeClass(jarFile.getInputStream(jarEntry));
245-
} catch (Exception e) {
246-
throw new JApiCmpException(Reason.IoException, String.format("Failed to load file from jar '%s' as class file: %s.", name, e.getMessage()), e);
247-
}
248-
classes.add(ctClass);
249-
if (LOGGER.isLoggable(Level.FINE)) {
250-
LOGGER.fine(String.format("Adding class '%s' with jar name '%s' to list.", ctClass.getName(), name));
251-
}
252-
if (name.endsWith("package-info.class")) {
253-
updatePackageFilter(ctClass);
254-
}
255-
} else {
256-
if (LOGGER.isLoggable(Level.FINE)) {
257-
LOGGER.fine(String.format("Skipping file '%s' because filename does not end with '.class'.", name));
258-
}
234+
for (CtClass ctClass : ctClasses.get()) {
235+
if (!packageFilterEncountered) {
236+
if (options.getFilters().includeClass(ctClass)) {
237+
classes.add(ctClass);
238+
if (LOGGER.isLoggable(Level.FINE)) {
239+
LOGGER.fine(String.format("Adding class '%s' with jar name '%s' to list.", ctClass.getName(), ctClass.getName()));
240+
}
241+
} else {
242+
classPool.remove(ctClass);
243+
if (LOGGER.isLoggable(Level.FINE)) {
244+
LOGGER.fine(String.format("Ignoring class '%s' with jar name '%s'.", ctClass.getName(), ctClass.getName()));
259245
}
260246
}
261-
} catch (IOException e) {
262-
throw new JApiCmpException(Reason.IoException, String.format("Processing of jar file %s failed: %s", archive.getAbsolutePath(), e.getMessage()), e);
247+
}
248+
if (!ignorePackageFilters && ctClass.getName().endsWith("package-info")) {
249+
packageFilterEncountered |= updatePackageFilter(ctClass);
250+
if (packageFilterEncountered) {
251+
// we found a package filter, so any filtering we did so far may be invalid
252+
// reset everything and restart after having read all remaining filters
253+
classes.forEach(classPool::remove);
254+
classes.clear();
255+
}
263256
}
264257
}
265-
return classes;
258+
259+
return packageFilterEncountered
260+
? loadAndFilterClasses(ctClasses, classPool, true)
261+
: classes;
266262
}
267263

268-
private void updatePackageFilter(CtClass ctClass) {
264+
265+
private boolean updatePackageFilter(CtClass ctClass) {
266+
boolean filtersUpdated = false;
269267
Filters filters = options.getFilters();
270268
List<Filter> newFilters = new LinkedList<>();
271269
for (Filter filter : filters.getIncludes()) {
@@ -279,6 +277,7 @@ private void updatePackageFilter(CtClass ctClass) {
279277
if (newFilters.size() > 0) {
280278
filters.getIncludes().addAll(newFilters);
281279
newFilters.clear();
280+
filtersUpdated = true;
282281
}
283282
for (Filter filter : filters.getExcludes()) {
284283
if (filter instanceof AnnotationFilterBase) {
@@ -291,7 +290,9 @@ private void updatePackageFilter(CtClass ctClass) {
291290
if (newFilters.size() > 0) {
292291
filters.getExcludes().addAll(newFilters);
293292
newFilters.clear();
293+
filtersUpdated = true;
294294
}
295+
return filtersUpdated;
295296
}
296297

297298
/**
@@ -309,7 +310,7 @@ public JarArchiveComparatorOptions getJarArchiveComparatorOptions() {
309310
*
310311
* @return an instance of ClassPool
311312
*/
312-
public ClassPool getCommonClassPool() {
313+
public ReducibleClassPool getCommonClassPool() {
313314
return commonClassPool;
314315
}
315316

@@ -377,4 +378,108 @@ public Optional<CtClass> loadClass(ArchiveType archiveType, String name) {
377378
}
378379
return loadedClass;
379380
}
381+
382+
private static class JarsCtClassIterable implements Iterable<CtClass>, Iterator<CtClass> {
383+
private final Iterator<File> archives;
384+
private final ClassPool classPool;
385+
386+
private Iterator<CtClass> currentIterator = null;
387+
388+
public JarsCtClassIterable(List<File> archives, ClassPool classPool) {
389+
this.archives = archives.iterator();
390+
this.classPool = classPool;
391+
}
392+
393+
@Override
394+
public boolean hasNext() {
395+
if (currentIterator != null) {
396+
if (currentIterator.hasNext()) {
397+
return true;
398+
} else {
399+
currentIterator = null;
400+
}
401+
}
402+
if (archives.hasNext()) {
403+
final File archive = archives.next();
404+
currentIterator = new JarCtClassIterator(archive, classPool);
405+
return hasNext();
406+
}
407+
return false;
408+
}
409+
410+
@Override
411+
public CtClass next() {
412+
return currentIterator.next();
413+
}
414+
415+
@Override
416+
public Iterator<CtClass> iterator() {
417+
return this;
418+
}
419+
}
420+
421+
private static class JarCtClassIterator implements Iterator<CtClass> {
422+
423+
private final File archive;
424+
private final JarFile jarFile;
425+
private final Enumeration<JarEntry> entryEnumeration;
426+
private final ClassPool classPool;
427+
428+
private CtClass next = null;
429+
430+
public JarCtClassIterator(File archive, ClassPool classPool) {
431+
this.archive = archive;
432+
try {
433+
this.jarFile = new JarFile(archive);
434+
} catch (IOException e) {
435+
throw new JApiCmpException(Reason.IoException, String.format("Processing of jar file %s failed: %s", archive.getAbsolutePath(), e.getMessage()), e);
436+
}
437+
if (LOGGER.isLoggable(Level.FINE)) {
438+
LOGGER.fine("Loading classes from jar file '" + archive.getAbsolutePath() + "'");
439+
}
440+
this.entryEnumeration = jarFile.entries();
441+
this.classPool = classPool;
442+
}
443+
444+
@Override
445+
public boolean hasNext() {
446+
if (next != null) {
447+
return true;
448+
}
449+
450+
while (entryEnumeration.hasMoreElements()) {
451+
JarEntry jarEntry = entryEnumeration.nextElement();
452+
String name = jarEntry.getName();
453+
if (name.endsWith(".class")) {
454+
CtClass ctClass;
455+
try (InputStream classFile = jarFile.getInputStream(jarEntry)) {
456+
ctClass = classPool.makeClass(classFile);
457+
} catch (Exception e) {
458+
throw new JApiCmpException(Reason.IoException, String.format("Failed to load file from jar '%s' as class file: %s.", name, e.getMessage()), e);
459+
}
460+
next = ctClass;
461+
return true;
462+
} else {
463+
if (LOGGER.isLoggable(Level.FINE)) {
464+
LOGGER.fine(String.format("Skipping file '%s' because filename does not end with '.class'.", name));
465+
}
466+
}
467+
}
468+
try {
469+
jarFile.close();
470+
} catch (IOException e) {
471+
throw new JApiCmpException(Reason.IoException, String.format("Processing of jar file %s failed: %s", archive.getAbsolutePath(), e.getMessage()), e);
472+
}
473+
return false;
474+
}
475+
476+
@Override
477+
public CtClass next() {
478+
try {
479+
return next;
480+
} finally {
481+
next = null;
482+
}
483+
}
484+
}
380485
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package japicmp.cmp;
2+
3+
import javassist.ClassPool;
4+
import javassist.CtClass;
5+
6+
/**
7+
* A {@link ClassPool} that allows to remove a class from the pool.
8+
*/
9+
public class ReducibleClassPool extends ClassPool {
10+
public void remove(CtClass ctClass) {
11+
removeCached(ctClass.getName());
12+
}
13+
}
14+

japicmp/src/test/java/japicmp/cmp/ClassesHelper.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ public interface ClassesGenerator {
1616

1717
public static List<JApiClass> compareClasses(JarArchiveComparatorOptions options, ClassesGenerator classesGenerator) throws Exception {
1818
JarArchiveComparator jarArchiveComparator = new JarArchiveComparator(options);
19-
ClassPool classPool = jarArchiveComparator.getCommonClassPool();
20-
List<CtClass> oldClasses = classesGenerator.createOldClasses(classPool);
19+
ReducibleClassPool classPool = jarArchiveComparator.getCommonClassPool();
20+
final List<CtClass> oldClasses = classesGenerator.createOldClasses(classPool);
2121
List<CtClass> newClasses = classesGenerator.createNewClasses(classPool);
22-
return jarArchiveComparator.compareClassLists(options, oldClasses, newClasses);
22+
List<CtClass> filteredOldClasses = jarArchiveComparator.createListOfCtClasses(() -> oldClasses, classPool);
23+
List<CtClass> filteredNewClasses = jarArchiveComparator.createListOfCtClasses(() -> newClasses, classPool);
24+
return jarArchiveComparator.compareClassLists(options, filteredOldClasses, filteredNewClasses);
2325
}
2426

2527
public static class CompareClassesResult {

0 commit comments

Comments
 (0)