初探Spoon(附上用spoon检查HideUtilityClassConstructor)

最近收到友人的委托,帮忙研究下spoon,尝试下用spoon来实现一个工具类的检查,于是做了个demo,以供参考。

首先说下工具类的一些规范约束,又或者是最佳实践,以下是checkstyle网站上面的定义:

http://checkstyle.sourceforge.net/config_design.html#HideUtilityClassConstructor

HideUtilityClassConstructor Description

Since Checkstyle 3.1

Makes sure that utility classes (classes that contain only static
methods or fields in their API) do not have a public constructor.

Rationale: Instantiating utility classes does not make sense. Hence
the constructors should either be private or (if you want to allow
subclassing) protected. A common mistake is forgetting to hide the
default constructor.

If you make the constructor protected you may want to consider the
following constructor implementation technique to disallow
instantiating subclasses:

public class StringUtils // not final to allow subclassing {
protected StringUtils() {
// prevents calls from subclass
throw new UnsupportedOperationException();
}

public static int count(char c, String s) {
    // ...
} }

其实宗旨很简单,对于一个工具类来说最好的做法是:

  1. 所有方法都是静态的
  2. 不需要一个公共的构造方法(并没有任何理由去实例化一个工具类),如何做?
    1)隐藏默认的构造方法(如果在类的修饰前是public 则默认构造函数访问权限是 public ,如果 没有显示采用public修饰,则 默认构造函数的访问权限是 friendly),将其设置为private

    2)如果构造方法是protected的,则建议在构造方法里面抛出异常,以防止子类被实例化

 protected StringUtils() {
        // prevents calls from subclass
        throw new UnsupportedOperationException();
    }

同时也科普下访问权限的作用域:

下图引用自博文:
http://blog.csdn.net/fatherican/article/details/6876596

初探Spoon(附上用spoon检查HideUtilityClassConstructor)_第1张图片


那么,什么是spoon?上官网介绍:

http://spoon.gforge.inria.fr/architecture_test.html
Spoon is a library to analyze, transform, rewrite, transpile Java source code (incl Java 8). It parses source files to build a well-designed AST with powerful analysis and transformation API. 

简单滴说,就是使用了抽象语法树AST(abstract syntax tree)的方式去拆解一个java源码(有点像反射机制做的东西?),并提取里面的元素,后续做一些我们想要做的操作,例如代码格式检查,xxx,xxx,xxx(反正我只用到格式检查)….

然后看到关于codean alysis的介绍:

http://spoon.gforge.inria.fr/first_analysis_processor.html

不错,这正是我想要的,我看了下例子,主要说的是检查try {…} catch {…}代码块里面的catch代码块是不是没有做任何的处理,那既然能检查try catch代码块,是不是也能检查方法的作用域呢?我把 spoon的jar包下了下来,反编译看了下源码,果然有!

只要在processor的类上集成了AbstractProcessor ,注意枚举类型是CtClass的话,就会在解析到构造函数的时候,就会触发processor的处理

public class ClassProcessor extends AbstractProcessor<CtClass> 

接着完善processor,实现父类的方法public void process(CtClass paramE),然后后用paramE.hasModifier来判断方法的作用域,是不是很简单?! So easy! 妈妈再也不用担心我的代码不规范了!

/**
     * If this class is only a utility class, you should make the class final and define a private constructor
     */
    @Override
    public void process(CtClass paramE) {
        System.out.println(">>>>>>>>> Start to check the [" + paramE.getActualClass().getName() +"] Class definition <<<<<<<<<<<<<<");

        if(!paramE.hasModifier(ModifierKind.FINAL)) {
             System.out.println("Utility Class is recommanded to be defined as FINAL!!!");
             System.out.println();
        }else {
             System.out.println("Utility Class is FINAL now. : )");
        }


        System.out.println(">>>>>>>>> Start to check the Class definition <<<<<<<<<<<<<<");
        Set set = paramE.getConstructors();
        int counter = 0;

        for(CtConstructor constructor : set) {
            if(constructor.hasModifier(ModifierKind.PUBLIC) || constructor.hasModifier(ModifierKind.PROTECTED)) {
                 getFactory().getEnvironment().report(this, Level.WARN, paramE, "Utility Class Constructor is accessible!");
                 counter ++;
                 System.out.println("------------------- " +counter+ " ---------------------");
                 System.out.println("Utility Class Constructor is accessible! It should be defined as private! Constructor is :");
                 System.out.println(constructor.toString());
                 System.out.println("------------------------------------------");
            }

        }


    }

接着在main函数调用new Launcher().run(params), 就大功告成了

 public static void main( String[] args )
    {
        String[] params = {"-i","J:\\eclipse-workspace\\Spoon\\src\\main\\java\\com\\nathan\\Spoon\\Utils","-p","com.nathan.Spoon.ClassProcessor"};
        new Launcher().run(params);

    }

至于new Launcher().run参数的定义,可以参考源代码可以看到

/*     */   public void printUsage() {
/* 121 */     this.commandLineArgs = new String[] { "--help" };
/* 122 */     processArguments();
/*     */   }

可以输入help来查看参数定义,很贴心有木有,心都要化了:

String[] params = {"--help"};
        new Launcher().run(params);

结果很清晰了,就是说,你需要输入什么参数,首先在前面告知你需要输入参数的类型,如
[(-i|–input) ]
List of path to sources files.

[(-p|–processors) ]
List of processor’s qualified name to be used.:

Spoon version 5.7.0
Usage: java name> [option(s)]

Options : 

  [-h|--help]

  [--tabs]
        Use tabulations instead of spaces in the generated code (use spaces by
        default).

  [--tabsize ]
        Define tabulation size. (default: 4)

  [--level ]
        Level of the ouput messages about what spoon is doing. Default value is
        ALL level. (default: OFF)

  [--with-imports]
        Enable imports in generated files.

  [--compliance ]
        Java source code compliance level (1,2,3,4,5, 6, 7 or 8). (default: 8)

  [--encoding ]
        Forces the compiler to use a specific encoding (UTF-8, UTF-16, ...).
        (default: UTF-8)

  [(-i|--input) ]
        List of path to sources files.

  [(-p|--processors) ]
        List of processor's qualified name to be used.

  [(-t|--template)