我们的目的是想在代码commit之前去做这个检查,把不符合规范的代码标记出来,直到修改完成之后才允许提交。脚本涉及到几个重要的文件:1.pre-commit, 这个是git的一个hook文件,可以帮助我们在commit前去做一些事情,比如,调用我们第二步定义的checkstyle。2.checkstyle.gradle,这里面主要包含gradle的一个task,通过依赖checkstyle插件,然后定义我们自己的checkstyle task 去获取commit前的一些待提交文件,再运用我们定义的规则去做检查。3.checkstyle.xml文件,这个就是定义规则的文件,参考了google编码规范和华为编码规范。4.suppressions.xml,这个是过滤选项,也即是可以排除某些指定文件不受代码规范的检查,比如协议文件。
由于现在AndroidStudio已经支持了checkstyle插件,所以我们只要在.gradle文件里面引用就可以了。(我们这里采用的是checkstyle 6.5)
1.通常,我们会包装自己的checkstyle脚本,将其命名为checkstyle.gradle。代码如下:
apply plugin: 'checkstyle' dependencies { //也可依赖于生成的jar包,以后再说 //compile fileTree(dir: 'libs', include: ['*.jar']) //checkstyle files('../custom-checkstyle/custom-checkstyle.jar') //checkstyle task 依赖 custom-checkstyle module, custom-checkstyle moudle有自定义的Check checkstyle project(':custom-checkstyle') // for using compiled version //checkstyle 'com.puppycrawl.tools:checkstyle:6.5' } def reportsDir = "${project.buildDir}/reports" checkstyle { //工具版本 toolVersion '6.5' //配置文件路径 configFile file("${project.rootDir}/checkstyle.xml") //filter路径 configProperties.checkstyleSuppressionsPath = file("${project.rootDir}/suppressions.xml").absolutePath } task checkstyle(type: Checkstyle, group: 'verification') { try {//try一下,即使发生异常也不影响正常编译 def isCheck = true //是否打开代码规范检查的开关 def isCommit = project.hasProperty('checkCommit') //是否是提交前检查 if (isCheck) { if (isCommit) { //检测代码路径 //source project.rootDir //--- 检查项目中所有的文件, 比较慢, 下面分模块检查, 主要是src下面的java文件 source 'xxx/src' source 'lib-xxx/src' source 'src' //submodules的检查 //排除项 exclude '**/gen/**' exclude '**/test/**' exclude '**/res/**' exclude '**/androidTest/**' exclude '**/R.java' exclude '**/BuildConfig.java' //判断是否是git pre-commit hook触发的checkstyle //如果是,只检测要提交的java文件,否则检测路径下的所有java文件 if (project.hasProperty('checkCommit') && project.property("checkCommit")) { def ft = filterCommitter(getChangeFiles()) def includeList = new ArrayList<String>() for (int i = 0; i < ft.size(); i++) { String spliter = ft.getAt(i) String[] spliterlist = spliter.split("/") String fileName = spliterlist[spliterlist.length - 1] includeList.add("**/" + fileName) } if (includeList.size() == 0) { exclude '**/*.java' } else { println("includeList==" + includeList) include includeList } } else { include '**/*.java' } classpath = files() reports { // 支持html和xml两种报告形式,可以任选其一(html更具有可读性) xml.enabled = false html.enabled = true xml { destination file("$reportsDir/checkstyle/checkstyle.xml") } html { destination file("$reportsDir/checkstyle/checkstyle.html") } } } else { //如果不是提交触发的,也就是对项目进行构建,那么需要对pre-commit文件进行copy def forceCopy = false //如有需要,可以强制去更新客户端的pre-commit文件 try { copyPreCommitFile(forceCopy) //copySubmodulesPreCommitFile(forceCopy) } catch (Exception e) { println(e) } } } }catch (Exception e){ println("checkstyle catch an exception.") e.printStackTrace() } } //src是一个文件路径,target是一个目录路径 def copyFile(boolean forceUpdate, String src, String target){ def fileName = "pre-commit" def targetFile = file(target + "/" + fileName) if(targetFile.exists() && targetFile.isFile() && !forceUpdate){ //目标文件存在且没有强制更新,不需要copy操作 println(targetFile.absolutePath + " exist.") }else { //targetFile.delete() def srcFile = file(src) if (srcFile.isFile()) { copy { from srcFile into target } } } //targetFile = file(target + "/" + fileName) if(targetFile.isFile()) { if (!targetFile.canExecute()) { targetFile.setExecutable(true) } if (!targetFile.canWrite()) { targetFile.setWritable(true) } } } //把根目录下的pre-commit文件复制到.git-->hooks目录 def copyPreCommitFile(boolean forceUpdate){ def src = "${project.rootDir}/pre-commit" def target = "${project.rootDir}/.git/hooks" copyFile(forceUpdate, src, target) println("copyPreCommitFile") } //把submodules目录下的pre-commit文件复制到.git-->modules-->submodules-->XXXmoudles-->hooks 目录 def copySubmodulesPreCommitFile(boolean forceUpdate){ def src = "${project.rootDir}/submodules/pre-commit" def submodulesDir = "${project.rootDir}/.git/modules/submodules" File file = new File(submodulesDir) File[] fileList = file.listFiles() if(fileList != null && fileList.length > 0) { def size = fileList.length for (int i = 0; i < size; i++) { if (fileList[i].isDirectory()) { //target = "${project.rootDir}/.git/modules/submodules/XXX/hooks" def target = submodulesDir + "/" + fileList[i].getName() + "/hooks" copyFile(forceUpdate, src, target) } } } println("copySubmodulesPreCommitFile") } //过滤java文件 def filterCommitter(String gitstatusinfo) { ArrayList<String> filterList = new ArrayList<String>() String[] lines = gitstatusinfo.split("\\n") for (String line : lines) { if (!line.startsWith("D ") && line.contains(".java") ) { String[] spliters = line.trim().split(" ") for (String str : spliters) { if (str.contains(".java")) { filterList.add(str) } } } } return filterList } //获取git commit待提交的文件列表 def getChangeFiles() { try { String changeInfo = 'git status -s'.execute(null, project.rootDir).text.trim() return changeInfo == null ? "" : changeInfo } catch (Exception e) { return "" } }
2.定制自己的checkstyle.xml文件,就是代码检查需要应用的哪些规则,代码如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.2//EN" "http://www.puppycrawl.com/dtds/configuration_1_2.dtd"> <module name="Checker"> <property name="charset" value="UTF-8"/> <property name="severity" value="error"/> <module name="SuppressionCommentFilter"/> <module name="SuppressionFilter"> <property name="file" value="${checkstyleSuppressionsPath}"/> </module> <!--代码规范--> <module name="TreeWalker"> <!--1.代码块检查--> <!--检查空白块 option, 需要去掉空白块, 选项 text,stmt default stmt --> <!--<module name="EmptyBlock">--> <!--<property name="option" value="text"/>--> <!--<property name="tokens"--> <!--value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>--> <!--</module>--> <!--Empty catch block. 检查空的 catch块, 需要把异常堆栈打印出来, 方便定位原因 --> <module name="EmptyCatchBlock" /> <!--option: 定义左大括号'{'显示位置,eol在同一行显示,nl在下一行显示 maxLineLength: 大括号'{'所在行行最多容纳的字符数 tokens: 该属性适用的类型,例:CLASS_DEF,INTERFACE_DEF,METHOD_DEF,CTOR_DEF --> <module name="LeftCurly"> <property name="option" value="eol"/> </module> <!-- NeedBraces 检查是否应该使用括号的地方没有加括号, 主要在if, else时有这样的情况 tokens: 定义检查的类型 --> <module name="NeedBraces"/> <!-- Checks the placement of right curly braces ('}') for else, try, and catch tokens. The policy to verify is specified using property option. option: 右大括号是否单独一行显示 tokens: 定义检查的类型 --> <module name="RightCurly"> <property name="id" value="RightCurlySame"/> <property name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO"/> </module> <module name="RightCurly"> <property name="id" value="RightCurlyAlone"/> <property name="option" value="alone"/> <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, INSTANCE_INIT"/> </module> <!--2.类检查--> <!--Checks that a class which has only private constructors is declared as final.只有私有构造器的类必须声明为final--> <module name="FinalClass" /> <!--Fields and methods should be before inner classes. 内部类检查,确保内部类在外部类的底部--> <!--<module name="InnerTypeLast" />--> <!--检查每个java文件是否只含有一个顶层类--> <module name="OneTopLevelClass" /> <!--自定义Check规则--> <module name="com.xxx.customcheckstyle.ProhibitExtendCheck"> <property name="subClassWhiteList" value="XXWebViewClient" /> <property name="superClassSet" value="WebViewClient" /> </module> <!-- Anonymous inner class length is {0,number,integer} lines (max allowed is {1,number,integer}).匿名类的最大行数,缺省为20 --> <!--<module name="AnonInnerLength">--> <!--<property name="max" value="200" />--> <!--</module>--> <!--3.代码检查--> <!--Default should be last label in the switch. switch 中default的位置检查,在所有case下面--> <module name="DefaultComesLast" /> <!--空行检查 一行 ; --> <module name="EmptyStatement" /> <!--String literal expressions should be on the left side of an equals comparison. 使用 equals 时 避免出现null 如 str.equls("xx") 正确使用 "xx".equls(str) --> <module name="EqualsAvoidNull" /> <!--检查 重写equals方法时是否有重写hashCode方法--> <module name="EqualsHashCode" /> <!--检查 switch 语句 中 case 代码块是否包含 break, return, throw or continue--> <module name="FallThrough" /> <!--检查局部变量,参数 是否跟全局变量同名 ignoreConstructorParameter 忽略构造方法的参数 default false ignoreSetter 忽略setter default false setterCanReturnItsClass 忽略返回 this 类型的setter default false --> <!--<module name="HiddenField">--> <!--<property name="ignoreConstructorParameter" value="true" />--> <!--<property name="ignoreSetter" value="true" />--> <!--<property name="setterCanReturnItsClass" value="true" />--> <!--</module>--> <!--检查switch 是否包含 default--> <module name="MissingSwitchDefault" /> <!--检查同个文件中是否出现多个同样的字面量--> <!--<module name="MultipleStringLiterals" />--> <!-- 每一行只能定义一个变量 如 Button b1,b2,b3; --> <module name="MultipleVariableDeclarations" /> <!-- if-else嵌套语句个数 最多5层 --> <module name="NestedIfDepth"> <property name="max" value="5"/> </module> <!-- try-catch 嵌套语句个数 最多2层 --> <module name="NestedTryDepth"> <property name="max" value="2"/> </module> <!--一行只允许一条语句,即末尾一个 ; --> <module name="OneStatementPerLine" /> <!--重载方法是否在同个地方--> <!--<module name="OverloadMethodsDeclarationOrder" />--> <!--Assignment of parameter 'phone' is not allowed. 参数中不允许有赋值语句--> <!--<module name="ParameterAssignment" />--> <!-- Checks for overly complicated boolean expressions. Currently finds code like if (b == true), b || true, !false, etc. 检查boolean值是否冗余的地方.Expression can be simplified. Rationale: Complex boolean logic makes code hard to understand and maintain. --> <module name="SimplifyBooleanExpression"/> <!--字符串比较检查,字符串比较用equals--> <module name="StringLiteralEquality" /> <!--检查 重写的 clone 方法是否调用了 super.clone()--> <module name="SuperClone" /> <!--检查 重写的 finalize 方法是否调用了 super.finalize()--> <module name="SuperFinalize" /> <!--Unnecessary parentheses around expression. 检查多余的圆括号--> <module name="UnnecessaryParentheses" /> <!-- 每行字符数 --> <module name="LineLength"> <property name="max" value="200" /> </module> <!-- Checks for long methods and constructors. max default 150行. max=500 设置长度500 --> <!--<module name="MethodLength">--> <!--<property name="max" value="500"/>--> <!--</module>--> <!-- ModifierOrder 检查修饰符的顺序,默认是 public,protected,private,abstract,static,final,transient,volatile,synchronized,native --> <module name="ModifierOrder" /> <!-- 检查是否有多余的修饰符,例如:接口中的方法不必使用public、abstract修饰 --> <module name="RedundantModifier" /> <!-- Checks the number of parameters of a method or constructor. max default 7个. --> <module name="ParameterNumber"> <property name="max" value="9" /> </module> <!-- Checks that long constants are defined with an upper ell. That is ' L' and not 'l'. This is in accordance to the Java Language Specification, Section 3.10.1. 检查是否在long类型是否定义了大写的L.字母小写l和数字1(一)很相似。Should use uppercase 'L'. looks a lot like 1. --> <module name="UpperEll"/> <!-- A check for TODO: comments. Actually it is a generic regular expression matcher on Java comments. To check for other patterns in Java comments, set property format. 检查是否存在TODO(待处理) TODO是javaIDE自动生成的。一般代码写完后要去掉。Comment matches to-do format 'TODO:'. --> <module name="TodoComment"/> <!-- Checks the style of array type definitions. Some like Java-style: public static void main(String[] args) and some like C-style: public static void main(String args[]) 检查再定义数组时,采用java风格还是c风格,例如:int[] num是java风格,int num[]是c风格。默认是java风格--> <module name="ArrayTypeStyle"/> <!--4.导包检查--> <!-- 必须导入类的完整路径,即不能使用*导入所需的类 --> <module name="AvoidStarImport" /> <!-- 检查是否从非法的包中导入了类 illegalPkgs: 定义非法的包名称--> <module name="IllegalImport"/> <!-- defaults to sun.* packages --> <!-- 检查是否导入了不必显示导入的类--> <module name="RedundantImport" /> <!-- 检查是否导入的包没有使用--> <module name="UnusedImports" /> <!-- 检查方法的javadoc的注释 scope: 可以检查的方法的范围,例如:public只能检查public修饰的方法,private可以检查所有的方法 allowMissingParamTags: 是否忽略对参数注释的检查 allowMissingThrowsTags: 是否忽略对throws注释的检查 allowMissingReturnTag: 是否忽略对return注释的检查 --> <!--<module name="JavadocMethod">--> <!--<property name="scope" value="public"/>--> <!--<property name="allowMissingParamTags" value="false"/>--> <!--<property name="allowMissingThrowsTags" value="false"/>--> <!--<property name="allowMissingReturnTag" value="false"/>--> <!--<property name="tokens" value="METHOD_DEF"/>--> <!--<property name="allowUndeclaredRTE" value="true"/>--> <!--<property name="allowThrowsTagsForSubclasses" value="true"/>--> <!--<!–允许get set 方法没有注释–>--> <!--<property name="allowMissingPropertyJavadoc" value="true"/>--> <!--</module>--> <!-- 检查类和接口的javadoc 默认不检查author 和version tags authorFormat: 检查author标签的格式 versionFormat: 检查version标签的格式 scope: 可以检查的类的范围,例如:public只能检查public修饰的类,private可以检查所有的类 excludeScope: 不能检查的类的范围,例如:public,public的类将不被检查,但访问权限小于public的类仍然会检查,其他的权限以此类推 tokens: 该属性适用的类型,例如:CLASS_DEF,INTERFACE_DEF --> <!--<module name="JavadocType">--> <!--<property name="scope" value="public"/>--> <!--<property name="tokens" value="CLASS_DEF,INTERFACE_DEF"/>--> <!--</module>--> <!--First sentence should be present. 检查是否包含不允许使用的词汇--> <!--<module name="SummaryJavadocCheck" />--> <!--5.命名规范检查--> <!-- local, final variables, including catch parameters --> <module name="LocalFinalVariableName" /> <!-- local, non-final variables, including catch parameters--> <module name="LocalVariableName" /> <!-- 静态变量命名 不能有小写字母,长度(0,39) --> <module name="StaticVariableName"> <property name="format" value="(^s[A-Z][a-zA-Z0-9]{0,39}$)" /> </module> <!-- 包命名 小写字母开头 --> <module name="PackageName"> <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" /> </module> <!-- 类的命名,匹配规则默认:(^[A-Z][a-zA-Z0-9]*$),必须以大写字母开始 --> <module name="TypeName"> <property name="format" value="(^[A-Z][a-zA-Z0-9]{0,39}$)"/> <property name="tokens" value="CLASS_DEF"/> </module> <!-- 接口命名,匹配规则:(^I[A-Z][a-zA-Z0-9]*$),必须以大写I开始 --> <module name="TypeName"> <property name="format" value="^I[A-Z][a-zA-Z0-9]*$"/> <property name="tokens" value="INTERFACE_DEF"/> </module> <!-- 方法命名 小写字母开头,长度(0,39) --> <module name="MethodName"> <property name="format" value="(^[a-z][a-zA-Z0-9]{0,39}$)" /> </module> <!-- 成员变量命名 小写字母开头,长度(0,39) --> <module name="MemberName"> <property name="format" value="(^m[A-Z][a-zA-Z0-9]{0,39}$)" /> </module> <!-- 常量命名 不能有小写字母,长度(0,39) --> <module name="ConstantName"> <property name="format" value="(^[A-Z0-9_]{0,39}$)" /> </module> </module> </module>
3.支持添加过滤条件,也就是可以过滤某些文件不受这个规则的检查,定义suppression.xml文件
<?xml version="1.0"?> <!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd"> <!--checkstyle排除项--> <!--需要过滤协议文件, 注解文件等等--> <!--也可以对某个java文件的某段代码过滤某个规则的检查 <suppress checks="JavadocStyleCheck" files="GeneratedObject.java" lines="50-9999"/> --> <suppressions> <suppress checks="[a-zA-Z0-9]*" files="R.java" /> <suppress checks="[a-zA-Z0-9]*" files="BuildConfig.java" /> <suppress checks="[a-zA-Z0-9]*" files="Test" /> <suppress checks="[a-zA-Z0-9]*" files="Dagger*" /> <suppress checks="[a-zA-Z0-9]*" files=".*_.*Factory.java" /> <suppress checks="[a-zA-Z0-9]*" files=".*ViewInjector.java" /> <suppress checks="[a-zA-Z0-9]*" files=".*_MembersInjector.java" /> <suppress checks="[a-zA-Z0-9]*" files=".*_ViewBinding.java" /> <suppress checks="[a-zA-Z0-9]*Check" files=".*ResProtocal.java" /> <suppress checks="[a-zA-Z0-9]*Check" files=".*ReqProtocal.java" /> <suppress checks="[a-zA-Z0-9]*Check" files="[\\/]protocol[\\/]" /> <!--过滤整个包名下面的协议文件--> <suppress checks="[a-zA-Z0-9]*Check" files="[\\/]proto[\\/]" /> <!--过滤整个包名下面的协议文件--> </suppressions>
4.编写git提交前的脚本文件,此文件需要位于.git--->hooks目录下面,所以我们在上面的task中会做一个自动的拷贝,pre-commit
#!/bin/sh # # An example hook script to verify what is about to be committed. # Called by "git commit" with no arguments. The hook should # exit with non-zero status after issuing an appropriate message if # it wants to stop the commit. # # To enable this hook, rename this file to "pre-commit". if git rev-parse --verify HEAD >/dev/null 2>&1 then against=HEAD else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi echo "start checkstyle task" SCRIPT_DIR=$(dirname "$0") SCRIPT_ABS_PATH=`cd "$SCRIPT_DIR"; pwd` $SCRIPT_ABS_PATH/../../gradlew -PcheckCommit="true" checkstyle if [ $? -eq 0 ]; then echo "checkstyle OK" exit 0 else echo "checkstyle fail, for details see /build/reports/checkstyle/checkstyle.html" exit 1 fi5.在最外层的build.gradle文件中引用我们自定义的checkstyle.gradle脚本,
apply from: 'checkstyle.gradle'
6.把这些文件的路径再调整一下,然后就大功告成了,提交代码时执行 git commit就会有代码规范的提示了。检查结果是,符合规范的代码可正常提交,不符合规范的代码会统一放到 /build/reports/checkstyle/checkstyle.html文件中,可用浏览器打开,直至按照规范修改完成之后才能提交。
7.如果checkstyle.xml里面的规则满足不了你的需要,那么需要自定义Check规则,那么需要学习Check的语法树,比如,一个class的定义如下,
* <pre> * +--CLASS_DEF * | * +--MODIFIERS * | * +--LITERAL_PUBLIC (public) * +--LITERAL_CLASS (class) * +--IDENT (MyClass) * +--EXTENDS_CLAUSE * +--IMPLEMENTS_CLAUSE * | * +--IDENT (Serializable) * +--OBJBLOCK * | * +--LCURLY ({) * +--RCURLY (}) * </pre>
更多请参考checkstyle插件的源码 TokenTypes类。
8.在项目中新建一个module,命名为custom-checkstyle,修改其build.gradle文件为
apply plugin: 'java' //jar { // destinationDir rootProject.file('custom-checkstyle') //} //sourceCompatibility = JavaVersion.VERSION_1_7 //targetCompatibility = JavaVersion.VERSION_1_7 dependencies { //compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.puppycrawl.tools:checkstyle:6.5' }
9.编写自定义Check规则的代码,需要继承自Check(6.5版本是这个,更高的版本应该是Abstract***Check),比如,我写了一个禁止继承自某个类的Check
package com.bigo.customcheckstyle; import com.google.common.base.Joiner; import com.puppycrawl.tools.checkstyle.api.Check; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * * 禁止继承某个类(除白名单子类以外) */ public class ProhibitExtendCheck extends Check { /** * log中对应的key */ public static final String MSG_KEY = "Class ''{0}'' should not extend class ''{1}'' directly, looking for help by lianzhan or caikaiwu."; /** * Character separate package names in qualified name of java class. */ public static final String PACKAGE_SEPARATOR = "."; /** * 子类白名单, 对继承没有限制 */ private Set<String> mSubClassWhiteList = new HashSet<>(); /** * 需要检查继承关系的父类的集合 */ private Set<String> mSuperClassSet = new HashSet<>(); @Override public int[] getDefaultTokens() { return getAcceptableTokens(); } @Override public int[] getAcceptableTokens() { return new int[] {TokenTypes.CLASS_DEF}; } @Override public int[] getRequiredTokens() { return getAcceptableTokens(); } /** * checkstyle.xml中对应的属性调用的setter方法 * @param names */ public void setSubClassWhiteList(final String[] names) { if (names != null && names.length > 0) { for (String name : names) { mSubClassWhiteList.add(name); } } } /** * checkstyle.xml中对应的属性调用的setter方法 * @param names */ public void setSuperClassSet(final String[] names) { if (names != null && names.length > 0) { for (String name : names) { mSuperClassSet.add(name); } } } /** * 遍历语法树中的每个结点, 结点信息参考{@link TokenTypes.CLASS_DEF} * @param ast */ @Override public void visitToken(DetailAST ast) { DetailAST currentNode = ast; while (currentNode != null) { if (currentNode.getType() == TokenTypes.CLASS_DEF) { String subClassName = currentNode.findFirstToken(TokenTypes.IDENT).getText(); //获取子类的名字 if (!mSubClassWhiteList.contains(subClassName)) {//不在白名单中 String superClassName = null; DetailAST extendNode = currentNode.findFirstToken(TokenTypes.EXTENDS_CLAUSE); if (extendNode != null) { superClassName = extendNode.findFirstToken(TokenTypes.IDENT).getText(); //获取父类的名字 } if (mSuperClassSet.contains(superClassName)) { log(currentNode.getLineNo(), MSG_KEY, subClassName, superClassName); } } } currentNode = currentNode.getNextSibling(); } } /** * Get super class name of given class. * @param classAst class * @return super class name or null if super class is not specified */ private String getSuperClassName(DetailAST classAst) { String superClassName = null; final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE); if (classExtend != null) { superClassName = extractQualifiedName(classExtend); } return superClassName; } /** * Get name of class(with qualified package if specified) in extend clause. * @param classExtend extend clause to extract class name * @return super class name */ private static String extractQualifiedName(DetailAST classExtend) { final String className; if (classExtend.findFirstToken(TokenTypes.IDENT) == null) { // Name specified with packages, have to traverse DOT final DetailAST firstChild = classExtend.findFirstToken(TokenTypes.DOT); final List<String> qualifiedNameParts = new LinkedList<>(); qualifiedNameParts.add(0, firstChild.findFirstToken(TokenTypes.IDENT).getText()); DetailAST traverse = firstChild.findFirstToken(TokenTypes.DOT); while (traverse != null) { qualifiedNameParts.add(0, traverse.findFirstToken(TokenTypes.IDENT).getText()); traverse = traverse.findFirstToken(TokenTypes.DOT); } className = Joiner.on(PACKAGE_SEPARATOR).join(qualifiedNameParts); } else { className = classExtend.findFirstToken(TokenTypes.IDENT).getText(); } return className; } }10.到此全部OK了。