Phinehasz Zhi

超越你看到的

从静态代码扫描引擎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也很简单,就讲这么多.


 
posted @ 2018-07-21 21:18  phinehasz  阅读(834)  评论(0编辑  收藏  举报