package ru.vyarus.dropwizard.guice.module.installer.scanner; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.installer.scanner.util.OReflectionHelper; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ScanClassesCount; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ScanTime; /** * Classpath scanner, reduced to provided packages. * Ignores classes annotated with {@link InvisibleForScanner}. * <p> * Actual scan is performed only on first {@link #scan(ClassVisitor)} call. Later scans used cached classes. * {@link #cleanup()} must be used to clear cache. * * @author Vyacheslav Rusakov * @since 31.08.2014 */ public class ClasspathScanner { private static final int SCAN_THRESHOLD = 1000; private final Logger logger = LoggerFactory.getLogger(ClasspathScanner.class); private final StatsTracker tracker; private final Set<String> packages; private List<Class> scanned; public ClasspathScanner(final Set<String> packages) { // for backwards compatibility allow using without tracker this(packages, null); } public ClasspathScanner(final Set<String> packages, final StatsTracker tracker) { this.packages = validate(packages); this.tracker = tracker; // perform scan before to fill cache and get accurate traversing stats performScan(); } /** * Scan configured classpath packages. * * @param visitor visitor to investigate found classes */ public void scan(final ClassVisitor visitor) { if (scanned == null) { performScan(); } for (Class<?> cls : scanned) { visitor.visit(cls); } } /** * Should be called to flush scanner cache. */ @SuppressWarnings("PMD.NullAssignment") public void cleanup() { scanned = null; } /** * @param packages specified packages * @return original set if validation pass * @throws IllegalStateException if packages intersect */ private Set<String> validate(final Set<String> packages) { final List<String> pkg = Lists.newArrayList(packages); Collections.sort(pkg, new Comparator<String>() { @Override public int compare(final String o1, final String o2) { return Integer.compare(o1.length(), o2.length()); } }); for (int i = 0; i < pkg.size(); i++) { final String path = pkg.get(i); for (int j = i + 1; j < pkg.size(); j++) { final String path2 = pkg.get(j); Preconditions.checkState(!path2.startsWith(path + "."), "Autoscan path '%s' is already covered by '%s' and may lead " + "to duplicate instances in runtime", path2, path); } } return packages; } @SuppressWarnings("PMD.PrematureDeclaration") private void performScan() { final Stopwatch timer = tracker == null ? null : tracker.timer(ScanTime); int count = 0; scanned = Lists.newArrayList(); for (String pkg : packages) { final List<Class<?>> found; try { found = OReflectionHelper.getClassesFor(pkg, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException e) { throw new IllegalStateException("Failed to scan classpath", e); } count += found.size(); for (Class<?> cls : found) { if (!cls.isAnnotationPresent(InvisibleForScanner.class)) { scanned.add(cls); } } } if (count > SCAN_THRESHOLD) { logger.warn("{} classes were loaded while scanning '{}' packages. Reduce packages to scan " + "to increase efficiency.", count, Joiner.on(',').join(packages)); } if (timer != null) { timer.stop(); tracker.count(ScanClassesCount, count); } } }