从静态代码扫描引擎PMD源码学习-多线程任务模型和File过滤设计
不知不觉在工作中研究PMD并定制规则已经4个月左右了。其实PMD有许多值得我学习的源码,不过出于时间并不曾动笔。今天简单记录总结一下PMD的多线程和File过滤设计的源码。
1 public class MultiThreadProcessor extends AbstractPMDProcessor { 2 3 private ExecutorService executor; 4 private CompletionService<Report> completionService; 5 private List<Future<Report>> tasks = new ArrayList<>(); 6 7 public MultiThreadProcessor(final PMDConfiguration configuration) { 8 super(configuration); 9 10 executor = Executors.newFixedThreadPool(configuration.getThreads(), new PmdThreadFactory()); 11 completionService = new ExecutorCompletionService<>(executor); 12 } 13 14 @Override 15 protected void runAnalysis(PmdRunnable runnable) { 16 // multi-threaded execution, dispatch analysis to worker threads 17 tasks.add(completionService.submit(runnable)); 18 } 19 20 @Override 21 protected void collectReports(List<Renderer> renderers) { 22 // Collect result analysis, waiting for termination if needed 23 try { 24 for (int i = 0; i < tasks.size(); i++) { 25 final Report report = completionService.take().get(); 26 super.renderReports(renderers, report); 27 } 28 } catch (InterruptedException ie) { 29 Thread.currentThread().interrupt(); 30 } catch (ExecutionException ee) { 31 Throwable t = ee.getCause(); 32 if (t instanceof RuntimeException) { 33 throw (RuntimeException) t; 34 } else if (t instanceof Error) { 35 throw (Error) t; 36 } else { 37 throw new IllegalStateException("PmdRunnable exception", t); 38 } 39 } finally { 40 executor.shutdownNow(); 41 } 42 } 43 }
这是MultiThreadProcessor,多线程执行类。
关键类是CompletionService,PMD使用实现了Callable接口的PMDRunnable类来封装线程的具体运行,在这个类的call方法里进行重写。今天的核心不是call里的代码,核心是这种任务式提交的多线程操作的常见写法。也就是结合CompletionService。
我们知道,如果实现了Callable接口,那么使用者是想获得结果的,而结果是Future对象,PMD代码中的Report就是call的返回,使用Future来泛型封装。
CompletionService内部维护一个BlockQuene队列,通过ExecutorCompletionService<>(executor)来传入一个executor,也就是一个线程池。
这里使用jdk自带的Executors.newFixedThreadPool来创建固定大小的线程池,也就是上限啦。
如果自己使用callable接口来向池子submit,返回future需要自己管理,而CompletionService则可以完美管理,应当每次都使用该类。
通过completionService.submit(runnable),提交callable实例,获得Future对象,我们通过一个集合List存储,但实际上我们后续并不真正要遍历使用每一个Future。
注意,submit是让线程开始启动。
线程运行后,如何拿结果?
使用completionService.take().get();获得call方法最终返回的结果类对象。
completionService.take就是获得Future对象,但这里已经是异步操作.
通过submit启动了一个线程,take操作会获得先运行完的线程的结果Future实例,这是这个服务类内部代码所实现的.
通过get获得Future封装的对象report
至此,一连串标准操作,非常舒服.
---------------------------------------------------------------
接下来是,文件的Filter篇:
最近在给PMD增加exclude的功能.发现他的FileUtil类里写了以后要做exclude功能的注释...结果6.x版本也还没实现啊..然后我做了半天左右实现好了.(参数传入)
实际上PMD的文件过滤是基于Filter实现,也和FilenameFilter有关.
核心来自于jdk的File.list(FilenameFilter filter)
1 public static List<DataSource> collectFiles(String fileLocations, FilenameFilter filenameFilter) { 2 List<DataSource> dataSources = new ArrayList<>(); 3 for (String fileLocation : fileLocations.split(",")) { 4 collect(dataSources, fileLocation, filenameFilter); 5 } 6 return dataSources; 7 } 8 9 private static List<DataSource> collect(List<DataSource> dataSources, String fileLocation, 10 FilenameFilter filenameFilter) { 11 File file = new File(fileLocation); 12 if (!file.exists()) { 13 throw new RuntimeException("File " + file.getName() + " doesn't exist"); 14 } 15 if (!file.isDirectory()) { 16 if (fileLocation.endsWith(".zip") || fileLocation.endsWith(".jar")) { 17 ZipFile zipFile; 18 try { 19 zipFile = new ZipFile(fileLocation); 20 Enumeration<? extends ZipEntry> e = zipFile.entries(); 21 while (e.hasMoreElements()) { 22 ZipEntry zipEntry = e.nextElement(); 23 if (filenameFilter.accept(null, zipEntry.getName())) { 24 dataSources.add(new ZipDataSource(zipFile, zipEntry)); 25 } 26 } 27 } catch (IOException ze) { 28 throw new RuntimeException("Archive file " + file.getName() + " can't be opened"); 29 } 30 } else { 31 dataSources.add(new FileDataSource(file)); 32 } 33 } else { 34 // Match files, or directories which are not excluded. 35 // FUTURE Make the excluded directories be some configurable option 36 Filter<File> filter = new OrFilter<>(Filters.toFileFilter(filenameFilter), 37 new AndFilter<>(Filters.getDirectoryFilter(), Filters.toNormalizedFileFilter( 38 Filters.buildRegexFilterExcludeOverInclude(null, Collections.singletonList("SCCS"))))); 39 FileFinder finder = new FileFinder(); 40 List<File> files = finder.findFilesFrom(file, Filters.toFilenameFilter(filter), true); 41 for (File f : files) { 42 dataSources.add(new FileDataSource(f)); 43 } 44 } 45 return dataSources; 46 }
这是PMD来collect的核心代码.dataSource其实就是一个File再次封装的类.
collectFiles这个方法就是,传入source路径,一个目录,一个文件路径皆可.
再传入一个过滤类FilenameFilter,在PMD当前实现里是直接传入基于Language代码语言来做的后缀扩展名extension过滤FilenameFilter,
最后是传入一个集合,就是做封装的盒子.
因为路径参数是逗号分隔的,所以先做遍历,然后对每个路径进行调用collect方法.
接下来是核心,collect传入一个FilenameFilter,然后过滤这个路径下的File.
其实也是简单的实现.
然后再回到,如果我想增加一个过滤目录的功能怎么办?看似我可以在主代码里传入一个exclude的FilenameFilter,实际上它这个collect返回了dataSource,很不利于再次分析.
也就是说,这块代码其实没什么向后兼容的可能了.
既然PMD的作者,写死了,那我也就稍做修改.
Filter的精髓其实是OrFilter和AndFilter这种类,理解也很简单,一个OrFilter里维护一个Filter的List,只要有一个返回true,那就返回true
AndFilter里维护着Filter的List,全部返回true才会返回true.我记得apache commons里也有类似的io的file的Filter类.
以后如果想自己做这种功能,也可以用apache的包.
其实Filter也很简单,就讲这么多.