前言

本文隶属于专栏《1000个问题搞定大数据技术体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和参考文献请见1000个问题搞定大数据技术体系

正文

本文参考 Apache Spark 的 scalastyle 配置。

首先需要在 pom.xml 里面新增 scalastyle 的 plugin。

pom.xml

先定义 2 个和文件字符编码相关的全局变量。

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

配置 scalastyle-maven-plugin

				<plugin>
                    <groupId>org.scalastyle</groupId>
                    <artifactId>scalastyle-maven-plugin</artifactId>
                    <version>1.0.0</version>
                    <configuration>
                        <verbose>false</verbose>
                        <failOnViolation>true</failOnViolation>
                        <includeTestSourceDirectory>false</includeTestSourceDirectory>
                        <failOnWarning>false</failOnWarning>
                        <sourceDirectory>${basedir}/src/main/scala</sourceDirectory>
                        <testSourceDirectory>${basedir}/src/test/scala</testSourceDirectory>
                        <configLocation>scalastyle-config.xml</configLocation>
                        <outputFile>${basedir}/target/scalastyle-output.xml</outputFile>
                        <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
                        <outputEncoding>${project.reporting.outputEncoding}</outputEncoding>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>check</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>

在项目根目录下定义 scalastyle 的配置文件:scalastyle-config.xml

scalastyle-config.xml

可以无脑复制的部分

<!--
这里参考的 Apache Spark 的 scala 代码风格配置文件。

如果希望关闭代码段的检查,可以在源代码中添加注释
在代码段之前和之后,使用以下语法:

  // scalastyle:off
  ...  // 用来破坏风格的东西
  // scalastyle:on

也可以通过指定规则 ID 来单独取消一条规则,就像这样:
  http://www.scalastyle.org/rules-0.7.0.html

  // scalastyle:off no.finalize
  override def finalize(): Unit = ...
  // scalastyle:on no.finalize

这个文件分成了 3 个部分:

 (1) 我们应当遵守的规则
 (2) 我们想要遵守的规则,但是实际情况不允许。
 (3) 我们不想遵守的规则
-->

<scalastyle>
    <name>Scalastyle standard configuration</name>

    <!-- ================================================================================ -->
    <!--                               我们应当遵守的规则                                   -->
    <!-- ================================================================================ -->

    <!-- 检查文件中是否有标签 -->
    <check level="error" class="org.scalastyle.file.FileTabChecker" enabled="true"></check>

    <!-- 检查加号后面是否有空格 -->
    <check level="error" class="org.scalastyle.scalariform.SpacesAfterPlusChecker" enabled="true"></check>

    <!-- 检查加号前面是否有空格 -->
    <check level="error" class="org.scalastyle.scalariform.SpacesBeforePlusChecker" enabled="true"></check>

    <!-- 检查行尾没有空格 -->
    <check level="error" class="org.scalastyle.file.WhitespaceEndOfLineChecker" enabled="true"></check>

    <!-- 检查一行中的字符数 -->
    <check level="error" class="org.scalastyle.file.FileLineLengthChecker" enabled="true">
        <parameters>
            <parameter name="maxLineLength"><![CDATA[100]]></parameter>
            <parameter name="tabSize"><![CDATA[2]]></parameter>
            <parameter name="ignoreImports">true</parameter>
        </parameters>
    </check>

    <!-- 检查类名是否与正则表达式匹配 -->
    <check level="error" class="org.scalastyle.scalariform.ClassNamesChecker" enabled="true">
        <parameters>
            <parameter name="regex"><![CDATA[[A-Z][A-Za-z]*]]></parameter>
        </parameters>
    </check>

    <!-- 检查对象名称是否与正则表达式匹配 -->
    <check level="error" class="org.scalastyle.scalariform.ObjectNamesChecker" enabled="true">
        <parameters>
            <parameter name="regex"><![CDATA[(config|[A-Z][A-Za-z]*)]]></parameter>
        </parameters>
    </check>

    <!-- 检查包对象名是否与正则表达式匹配 -->
    <check level="error" class="org.scalastyle.scalariform.PackageObjectNamesChecker" enabled="true">
        <parameters>
            <parameter name="regex"><![CDATA[^[a-z][A-Za-z]*$]]></parameter>
        </parameters>
    </check>

    <!-- 检查方法的参数个数是否超过设置数量 -->
    <check customId="argcount" level="error" class="org.scalastyle.scalariform.ParameterNumberChecker" enabled="true">
        <parameters>
            <parameter name="maxParameters"><![CDATA[10]]></parameter>
        </parameters>
    </check>

    <!-- 检查类和对象是否定义了finalize()方法 -->
    <check level="error" class="org.scalastyle.scalariform.NoFinalizeChecker" enabled="true"></check>

    <!-- 检查类和对象在没有覆盖的情况下定义了相等(java.lang.object) -->
    <check level="error" class="org.scalastyle.scalariform.CovariantEqualsChecker" enabled="true"></check>

    <!-- 检查结构类型是否未被使用 -->
    <check level="error" class="org.scalastyle.scalariform.StructuralTypeChecker" enabled="true"></check>

    <!-- 检查如果使用长字符串,则使用大写字母 -->
    <check level="error" class="org.scalastyle.scalariform.UppercaseLChecker" enabled="true"></check>

    <!-- 检查if是否使用大括号 -->
    <check level="error" class="org.scalastyle.scalariform.IfBraceChecker" enabled="true">
        <parameters>
            <parameter name="singleLineAllowed"><![CDATA[true]]></parameter>
            <parameter name="doubleLineAllowed"><![CDATA[true]]></parameter>
        </parameters>
    </check>

    <!-- 检查方法是否具有显式返回类型 -->
    <check level="error" class="org.scalastyle.scalariform.PublicMethodsHaveTypeChecker" enabled="true"></check>

    <!-- 检查文件是否以换行符结尾 -->
    <check level="error" class="org.scalastyle.file.NewLineAtEofChecker" enabled="true"></check>

    <!-- 检查是否使用非ASCII字符(Unicode字符) -->
    <check customId="nonascii" level="error" class="org.scalastyle.scalariform.NonASCIICharacterChecker"
           enabled="true"></check>

    <!-- 检查在注释后是否有一个空格 -->
    <check level="error" class="org.scalastyle.scalariform.SpaceAfterCommentStartChecker" enabled="true"></check>

    <!-- 检查某些既定标记前是否有空间 -->
    <check level="error" class="org.scalastyle.scalariform.EnsureSingleSpaceBeforeTokenChecker" enabled="true">
        <parameters>
            <parameter name="tokens">ARROW, EQUALS, ELSE, TRY, CATCH, FINALLY, LARROW, RARROW</parameter>
        </parameters>
    </check>

    <!-- 检查某些既定标记后是否有空间 -->
    <check level="error" class="org.scalastyle.scalariform.EnsureSingleSpaceAfterTokenChecker" enabled="true">
        <parameters>
            <parameter name="tokens">ARROW, EQUALS, COMMA, COLON, IF, ELSE, DO, WHILE, FOR, MATCH, TRY, CATCH, FINALLY,
                LARROW, RARROW
            </parameter>
        </parameters>
    </check>

    <!-- 检查代码是否有 ??? 操作符 -->
    <check level="error" class="org.scalastyle.scalariform.NotImplementedErrorUsage" enabled="true"></check>

    <!-- 由于 SPARK-7977, 所有的 println 都应该由 '// scalastyle:off/on println' 包裹-->
    <check customId="println" level="error" class="org.scalastyle.scalariform.TokenChecker" enabled="true">
        <parameters>
            <parameter name="regex">^println$</parameter>
        </parameters>
        <customMessage><![CDATA[确定要 println 吗? 如果是的话,需要用下面的代码段包裹:
        // scalastyle:off println
        println(...)
        // scalastyle:on println]]></customMessage>
    </check>

    <!-- mutable.SynchronizedBuffer -->
    <check customId="mutablesynchronizedbuffer" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">mutable\.SynchronizedBuffer</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 mutable.SynchronizedBuffer 吗? 大多数场景下,你应该使用
      java.util.concurrent.ConcurrentLinkedQueue 来替代。
      如果你必须使用 mutable.SynchronizedBuffer,使用下面的代码段来包裹:
      // scalastyle:off mutablesynchronizedbuffer
      mutable.SynchronizedBuffer[...]
      // scalastyle:on mutablesynchronizedbuffer
    ]]></customMessage>
    </check>

    <!-- Class.forName -->
    <check customId="classforname" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">Class\.forName</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 Class.forName 吗? 大多数场景下,你应该使用 Utils.classForName 来替代.
      如果你必须使用 Class.forName,使用下面的代码段来包裹:
      // scalastyle:off classforname
      Class.forName(...)
      // scalastyle:on classforname
    ]]></customMessage>
    </check>

    <!-- 检查正则表达式是否匹配 -->
    <check customId="caselocale" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">(\.toUpperCase|\.toLowerCase)(?!(\(|\(Locale.ROOT\)))</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 toUpperCase or toLowerCase without the root locale ? 大多数场景下,你应该使用
       toUpperCase(Locale.ROOT) or toLowerCase(Locale.ROOT) 来替代。
      如果你必须使用 toUpperCase or toLowerCase without the root locale,使用下面的代码段来包裹:
      // scalastyle:off caselocale
      .toUpperCase
      .toLowerCase
      // scalastyle:on caselocale
    ]]></customMessage>
    </check>

    <!-- throw new Error -->
    <check customId="throwerror" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">throw new \w+Error\(</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要这样抛出异常吗?大多数场景下,你应该使用精确的异常来替代。
      如果你必须这样抛出异常,使用下面的代码段来包裹:
      // scalastyle:off throwerror
      throw new XXXError(...)
      // scalastyle:on throwerror
    ]]></customMessage>
    </check>

    <!-- 由于 SPARK-9613, JavaConversions 应该替换成 JavaConverters -->
    <check customId="javaconversions" level="error" class="org.scalastyle.scalariform.TokenChecker" enabled="true">
        <parameters>
            <parameter name="regex">JavaConversions</parameter>
        </parameters>
        <customMessage>不要去隐式导入 scala.collection.JavaConversions._ 了, 导入
            scala.collection.JavaConverters._ 并且使用 .asScala / .asJava 方法
        </customMessage>
    </check>

    <!-- extractOpt -->
    <check customId="extractopt" level="error" class="org.scalastyle.scalariform.TokenChecker" enabled="true">
        <parameters>
            <parameter name="regex">extractOpt</parameter>
        </parameters>
        <customMessage>使用 jsonOption(x).map(.extract[T]) 来替代 .extractOpt[T],因为后者太低级了。
        </customMessage>
    </check>

    <!-- 检查某些既定标记前不允许使用空格 -->
    <check level="error" class="org.scalastyle.scalariform.DisallowSpaceBeforeTokenChecker" enabled="true">
        <parameters>
            <parameter name="tokens">COMMA</parameter>
        </parameters>
    </check>

    <!-- SPARK-3854: 在 ')' 和 '{' 之间使用单个空格隔开 -->
    <check customId="SingleSpaceBetweenRParenAndLCurlyBrace" level="error" class="org.scalastyle.file.RegexChecker"
           enabled="true">
        <parameters>
            <parameter name="regex">\)\{</parameter>
        </parameters>
        <customMessage><![CDATA[
      在 ')' 和 '{' 之间使用单个空格隔开。
    ]]></customMessage>
    </check>

    <!-- 多行注释使用 Javadoc 风格的行首缩进 -->
    <check customId="NoScalaDoc" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">(?m)^(\s*)/[*][*].*$(\r|)\n^\1 [*]</parameter>
        </parameters>
        <customMessage>多行注释使用 Javadoc 风格的行首缩进</customMessage>
    </check>

    <!-- case 子句省略括号 -->
    <check customId="OmitBracesInCase" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">case[^\n>]*=>\s*\{</parameter>
        </parameters>
        <customMessage> case 子句省略括号</customMessage>
    </check>

    <!-- SPARK-16877: 避免使用 Java 的 @Override 注解 -->
    <check level="error" class="org.scalastyle.scalariform.OverrideJavaChecker" enabled="true"></check>

    <!-- 检查应该用Scala @deprecated代替Java @Deprecated -->
    <check level="error" class="org.scalastyle.scalariform.DeprecatedJavaChecker" enabled="true"></check>

    <!-- 检查类没有导入某些类 -->
    <check level="error" class="org.scalastyle.scalariform.IllegalImportsChecker" enabled="true">
        <parameters>
            <parameter name="illegalImports"><![CDATA[scala.collection.Seq,scala.collection.IndexedSeq]]></parameter>
        </parameters>
        <customMessage><![CDATA[
      不要导入 scala.collection.Seq 和 scala.collection.IndexedSeq, 因为它会带来 Scala 2.12 和 2.13 混合编译问题。

      查看下面的页面了解 Seq / IndexedSeq 变更的细节。
      https://docs.scala-lang.org/overviews/core/collections-migration-213.html

      如果你必须使用 scala.collection.Seq 或者 scala.collection.IndexedSeq,请使用全限定名称。
    ]]></customMessage>
    </check>

    <!-- ================================================================================ -->
    <!--                        我们想要遵守的规则,但是实际情况不允许                          -->
    <!-- ================================================================================ -->

    <!-- 检查每个文件的前几行与文本匹配-->
    <check level="error" class="org.scalastyle.file.HeaderMatchesChecker" enabled="false"/>

    <!-- 我们无法启用以下两个选项,因为它会使许多字符串插值用例失败。-->
    <!-- 理想情况下,应配置以下两条规则以排除字符串插值。 -->
    <check level="error" class="org.scalastyle.scalariform.NoWhitespaceBeforeLeftBracketChecker"
           enabled="false"></check>
    <check level="error" class="org.scalastyle.scalariform.NoWhitespaceAfterLeftBracketChecker" enabled="false"></check>

    <!-- 检查方法名是否与正则表达式匹配 -->
    <check level="error" class="org.scalastyle.scalariform.MethodNamesChecker" enabled="false">
        <parameters>
            <parameter name="regex"><![CDATA[^[a-z][A-Za-z0-9]*$]]></parameter>
        </parameters>
    </check>

    <!-- 检查如果一个类实现了 equals 或者 hashCode,那它应该实现另一个-->
    <check level="error" class="org.scalastyle.scalariform.EqualsHashCodeChecker" enabled="true"></check>

    <!-- ================================================================================ -->
    <!--                               我们不想遵守的规则                                    -->
    <!-- ================================================================================ -->

    <!-- 检查类没有导入某些类 -->
    <check level="error" class="org.scalastyle.scalariform.IllegalImportsChecker" enabled="false">
        <parameters>
            <parameter name="illegalImports"><![CDATA[sun._,java.awt._]]></parameter>
        </parameters>
    </check>

    <!-- 检查文件不是以换行符结尾 -->
    <check level="error" class="org.scalastyle.file.NoNewLineAtEofChecker" enabled="false"></check>

    <!-- 检查Boolean表达式是否可以简化 -->
    <check level="error" class="org.scalastyle.scalariform.SimplifyBooleanExpressionChecker" enabled="false"></check>

    <!-- 检查是否使用 return -->
    <!-- 对于控制流和守卫,我们使用了相当多的 return -->
    <check level="error" class="org.scalastyle.scalariform.ReturnChecker" enabled="false"></check>

    <!-- 检查是否使用 null -->
    <!-- 我们在低级代码中大量使用null,并通过接口调用第三方代码 -->
    <check level="error" class="org.scalastyle.scalariform.NullChecker" enabled="false"></check>

    <!-- 检查类和对象没有定义clone()方法 -->
    <check level="error" class="org.scalastyle.scalariform.NoCloneChecker" enabled="false"></check>

    <!-- 检查文件中的行数 -->
    <check level="error" class="org.scalastyle.file.FileLengthChecker" enabled="false">
        <parameters>
            <parameter name="maxFileLength">800></parameter>
        </parameters>
    </check>

    <!-- 检查文件中声明的类型是否过多 -->
    <check level="error" class="org.scalastyle.scalariform.NumberOfTypesChecker" enabled="false">
        <parameters>
            <parameter name="maxTypes">30</parameter>
        </parameters>
    </check>

    <!-- 检查方法的参数复杂度是否超过设定值 -->
    <check level="error" class="org.scalastyle.scalariform.CyclomaticComplexityChecker" enabled="false">
        <parameters>
            <parameter name="maximum">10</parameter>
        </parameters>
    </check>

    <!-- 检查方法不超过最大长度 -->
    <check level="error" class="org.scalastyle.scalariform.MethodLengthChecker" enabled="false">
        <parameters>
            <parameter name="maxLength">50</parameter>
        </parameters>
    </check>

    <!-- 检查一个类/特性/对象是否申明太多的方法 -->
    <check level="error" class="org.scalastyle.scalariform.NumberOfMethodsInTypeChecker" enabled="false">
        <parameters>
            <parameter name="maxMethods"><![CDATA[30]]></parameter>
        </parameters>
    </check>

    <!-- 检查魔数是否使用var定义 -->
    <check level="error" class="org.scalastyle.scalariform.MagicNumberChecker" enabled="false">
        <parameters>
            <parameter name="ignore">-1,0,1,2,3</parameter>
        </parameters>
    </check>
</scalastyle>

需要自定义配置的部分

	<!-- 检查是否根据样式配置对导入进行分组和排序,根据自己的工程来自定义配置 -->
    <check level="error" class="org.scalastyle.scalariform.ImportOrderChecker" enabled="true">
        <parameters>
            <parameter name="groups">java,scala,3rdParty,shockang</parameter>
            <parameter name="group.java">javax?\..*</parameter>
            <parameter name="group.scala">scala\..*</parameter>
            <parameter name="group.3rdParty">(?!com\.shockang\.study\.spark\.).*</parameter>
            <parameter name="group.shockang">com\.shockang\.study\.spark\..*</parameter>
        </parameters>
    </check>

可选部分

	<!-- hadoopConfiguration -->
    <check customId="hadoopconfiguration" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">spark(.sqlContext)?.sparkContext.hadoopConfiguration</parameter>
        </parameters>
        <customMessage><![CDATA[
        确认要使用 sparkContext.hadoopConfiguration 吗?大多数场景下,你应该使用 spark.sessionState.newHadoopConf()
        来替代。这样的话,Spark session 里面的 hadoop 配置将会很快起作用。
        如果你必须使用 sparkContext.hadoopConfiguration,使用下面的代码段来包裹:
        // scalastyle:off hadoopconfiguration
        spark.sparkContext.hadoopConfiguration...
        // scalastyle:on hadoopconfiguration
    ]]></customMessage>
    </check>

    <!-- Runtime.getRuntime.addShutdownHook -->
    <check customId="runtimeaddshutdownhook" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">Runtime\.getRuntime\.addShutdownHook</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 Runtime.getRuntime.addShutdownHook 吗? 大多数场景下,你应该使用
      ShutdownHookManager.addShutdownHook 来替代。
      如果你必须使用 Runtime.getRuntime.addShutdownHook,使用下面的代码段来包裹:
      // scalastyle:off runtimeaddshutdownhook
      Runtime.getRuntime.addShutdownHook(...)
      // scalastyle:on runtimeaddshutdownhook
    ]]></customMessage>
    </check>

	<!-- Class.forName -->
    <check customId="classforname" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">Class\.forName</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 Class.forName 吗? 大多数场景下,你应该使用 Utils.classForName 来替代.
      如果你必须使用 Class.forName,使用下面的代码段来包裹:
      // scalastyle:off classforname
      Class.forName(...)
      // scalastyle:on classforname
    ]]></customMessage>
    </check>

    <!-- Await.result -->
    <check customId="awaitresult" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">Await\.result</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 Await.result? 大多数场景下,你应该使用 ThreadUtils.awaitResult 来替代。
      如果你必须使用 Await.result,使用下面的代码段来包裹:
      // scalastyle:off awaitresult
      Await.result(...)
      // scalastyle:on awaitresult
    ]]></customMessage>
    </check>

    <!-- Await.ready -->
    <check customId="awaitready" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">Await\.ready</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 Await.ready? 大多数场景下,你应该使用 ThreadUtils.awaitReady 来替代。
      如果你必须使用 Await.ready,使用下面的代码段来包裹:
      // scalastyle:off awaitready
      Await.ready(...)
      // scalastyle:on awaitready
    ]]></customMessage>
    </check>

	<!-- org.apache.commons.lang -->
    <check customId="commonslang2" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">org\.apache\.commons\.lang\.</parameter>
        </parameters>
        <customMessage>使用 Commons Lang 3 的类 (包 org.apache.commons.lang3.*) ,不要去使用
            Commons Lang 2 了(包 org.apache.commons.lang.*)
        </customMessage>
    </check>

    <!-- FileSystem.get -->
    <check customId="FileSystemGet" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">FileSystem.get\([a-zA-Z_$][a-zA-Z_$0-9]*\)</parameter>
        </parameters>
        <customMessage><![CDATA[
      确定要使用 "FileSystem.get(Configuration conf)"?如果输入配置没有合理的被设置,将会返回一个默认的
      FileSystem 实例。这会导致你在处理多文件系统的时候出现错误。
      请考虑使用 "FileSystem.get(URI uri, Configuration conf)" 或者 "Path.getFileSystem(Configuration conf)" 。
      如果你必须使用 "FileSystem.get(Configuration conf)",使用下面的代码段来包裹:
      // scalastyle:off FileSystemGet
      FileSystem.get(...)
      // scalastyle:on FileSystemGet
    ]]></customMessage>
    </check>

	<!-- Objects.toStringHelper -->
    <check customId="GuavaToStringHelper" level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
        <parameters>
            <parameter name="regex">Objects.toStringHelper</parameter>
        </parameters>
        <customMessage>避免使用 Object.toStringHelper。使用 ToStringBuilder 来替代。</customMessage>
    </check>

笔者后续会开源一些 scala 工程,里面使用了上面的 scalastyle 配置,敬请期待。

上一篇 下一篇