代码规范

170 阅读8分钟

代码格式

代码格式关系到代码的可读性,因此需要遵从一定的规范,包括缩进、水平对齐、注释格式等。关于代码格式,可能会因为语言和个人偏好而不同,但是一个团队最好是选定一种格式,因为一致性可以减少复杂度。

空行规范

空行在代码中的概念区隔作用同样适用。以Spring中的BeanDefinitionVisitor为例,在包声明、导入声明和每个函数之间,都有空行隔开。这种极其简单的规则极大地影响代码的视觉外观,每个空白行都是一条线索,提示你下一组代码表示的是不同的概念或功能。

package org.springframework.beans.factory.config;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringValueResolver;

public class BeanDefinitionVisitor {

    private StringValueResolver valueResolver;

    public BeanDefinitionVisitor(StringValueResolver valueResolver) {
        Assert.notNull(valueResolver, "StringValueResolver must not be null");
        this.valueResolver = valueResolver;
    }

    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        visitParentName(beanDefinition);
        visitBeanClassName(beanDefinition);
        visitFactoryBeanName(beanDefinition);
        visitFactoryMethodName(beanDefinition);
        visitScope(beanDefinition);
        visitPropertyValues(beanDefinition.getPropertyValues());
        ConstructorArgumentValues cas = beanDefinition. getConstructor ArgumentValues();
        visitIndexedArgumentValues(cas.getIndexedArgumentValues());
        visitGenericArgumentValues(cas.getGenericArgumentValues());
    }
}

删掉这些空白行,代码的可读性就弱了很多,如下所示。

package org.springframework.beans.factory.config;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringValueResolver;
public class BeanDefinitionVisitor {
    private StringValueResolver valueResolver;
    public BeanDefinitionVisitor(StringValueResolver valueResolver) {
        Assert.notNull(valueResolver, "StringValueResolver must not be null");
        this.valueResolver = valueResolver;
    }
    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        visitParentName(beanDefinition);
        visitBeanClassName(beanDefinition);
        visitFactoryBeanName(beanDefinition);
        visitFactoryMethodName(beanDefinition);
        visitScope(beanDefinition);
        visitPropertyValues(beanDefinition.getPropertyValues());
        ConstructorArgumentValues cas = beanDefinition. getConstructor ArgumentValues();
        visitIndexedArgumentValues(cas.getIndexedArgumentValues());
        visitGenericArgumentValues(cas.getGenericArgumentValues());
    }
}

还有一种极端是在每一行代码后面都加上空行,这样空行就失去了意义,其结果是和没有空行一样。 一个简单的原则就是将概念相关的代码放在一起:相关性越强,彼此之间的距离应该越短。

命名规范

在Java中,我们通常使用如下命名约定。

  • 类名采用“大驼峰”形式,即首字母大写的驼峰,例如Object、StringBuffer、FileInputStream。
  • 方法名采用“小驼峰”形式,即首字母小写的驼峰,方法名一般为动词,与参数组成动宾结构,例如Thread的sleep(long millis)、StringBuffer的append(String str)。
  • 常量命名的字母全部大写,单词之间用下划线连接,例如TOTAL_COUNT、PAGE_SIZE等。
  • 枚举类以Enum或Type结尾,枚举类成员名称需要全大写,单词间用下划线连接,例如SexEnum.MALE、SexEnum.FEMALE。
  • 抽象类名使用Abstract开头;异常类使用Exception结尾;实现类以impl结尾;测试类以它要测试的类名开始,以Test结尾。
  • 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词,包名统一使用单数形式。通常以com或org开头,加上公司名,再加上组件或者功能模块名,例如org.springframework.beans。

日志规范

在打印日志时,要特别注意日志输出级别,这是系统运维的需要。DEBUG、ALL或者自定义的级别。我认为比较有用的4个级别依次是ERROR、WARN、INFO和DEBUG。通常这4个级别就能够很好地满足我们的需求了。

ERROR级别

ERROR表示不能自己恢复的错误,需要立即被关注和解决。 例如,数据库操作错误、I/O错误(网络调用超时、文件读取错误等)、未知的系统错误(NullPointerException、OutOfMemoryError等)。

对于ERROR,我们不仅要打印线程堆栈,最好打印出一定的上下文(链路TraceId、用户Id、订单Id、外部传来的关键数据),以便于排查问题。

ERROR要接入监控和报警系统。ERROR需要人工介入处理,及时止损,否则会影响系统的可用性。当然也不能滥用ERROR,否则就会出现“狼来了”的情况。我在实际工作中曾碰到过系统每天会发出上千条错误报警的情况,导致根本没有人看报警内容,在真正出现问题时,也没有人关注,从而引发线上故障。因此,一定要做好ERROR输出的场景定义和规范,再配合监控治理,双管齐下,确保线上系统的稳定。

WARN级别

对于可预知的业务问题,最好不要用ERROR输出日志,以免污染报警系统。 例如,参数校验不通过、没有访问权限等业务异常,就不应该用ERROR输出。 需要注意的是,在短时间内产生过多的WARN日志,也是一种系统不健康的表现。因此,我们有必要为WARN配置一个适当阈值的报警,比如访问受限WARN超过100次/分,则发出报警。这样在WARN日志过于频繁时,我们能及时收到系统报警,去跟进用户问题。例如,如果是产品设计上有缺陷导致用户频繁出现操作卡点,可以考虑做一下流程或者产品上的优化。

INFO级别

INFO用于记录系统的基本运行过程和运行状态。

通常来说,优先根据INFO日志可初步定位,主要包括系统状态变化日志、业务流程的核心处理、关键动作和业务流程的状态变化。 适当的INFO可以协助我们排查问题,但是切忌把INFO当成DEBUG使用,这样会导致记录的数据过多,一方面影响系统性能,日志文件增长过快,消耗不必要的存储资源;另一方面也不利于阅读日志文件。

DEBUG级别

DEBUG是输出调试信息,如request/response的对象内容。 在输出对象内容时,要覆盖Object的toString方法,否则输出的是对象的内存地址,就起不到调试的作用了。

通常在开发和预发环境下,DEBUG日志会打开,以方便开发和调试。而在线上环境,DEBUG开关需要关闭,因为在生产环境下开启DEBUG会导致日志量非常大,其损耗是难以接受的。只有当线上出现bug或者棘手的问题时,才可以动态地开启DEBUG。为了防止日志量过大,我们可以采用分布式配置工具来实现基于requestId判断的日志过滤,从而只打印我们所需请求的DEBUG日志。

异常规范

建议在业务系统中设定两个异常,分别是BizException(业务异常)和SysException(系统异常),而且这两个异常都应该是Unchecked Exception。

为什么不建议用Checked Exception呢?

因为它破坏了开闭原则。如果你在一个方法中抛出了CheckedException,而catch语句在3个层级之上,那么你就要在catch语句和抛出异常处理之间的每个方法签名中声明该异常。这意味着在软件中修改较低层级时,都将波及较高层级,修改好的模块必须重新构建、发布,即便它们自身所关注的任何东西都没有被改动过。这也是C#、Python和Ruby语言都不支持Checked Exception的原因,因为其依赖成本要高于显式声明带来的收益。

最后,针对业务异常和系统异常要做统一的异常处理,类似于AOP,在应用处理请求的切面上进行异常处理收敛,其处理流程如下:

try {
    //业务处理
    Response res = process(request);
}

catch (BizException e) {
    //业务异常使用WARN级别
    logger.warn("BizException with{}", e.getErrorCode(), e.getErrorMsg());
}

catch (SysException ex) {
    //系统异常使用ERROR级别
    log.error("System error" + ex.getMessage(), ex);
}

catch (Exception ex) {
    //兜底
    log.error("System error" + ex.getMessage(), ex);
}

千万不要在业务处理内部到处使用try/catch打印错误日志,这样会使功能代码和业务代码缠绕在一起,让代码显得很凌乱,并且影响代码的可读性。

错误码规范

编号错误码 对于平台、底层系统或软件产品,可以采用编号式的编码规范,好处是编码风格固定,给人一种正式感;缺点是必须要配合文档才能理解错误码代表的意思。

例如,数据库软件Oracle中总共有2000多个异常,其编码规则是ORA-00001~ORA-02149,每一个错误码都有对应的错误解释。

淘宝开放平台也采用类似的编码方式,0~100表示平台解析错误,4表示User call limited(ISV调用次数超限)。

另外要注意,对不同的错误波段,一定要预留足够的码号。例如,淘宝开放平台所用的3位数就显得有些拘谨,其支撑的错误数最多不能超过100,超过100后,为了向后兼容,只能通过子错误码的方式进行变通处理。

显性化错误码 大型分布式架构下的业务系统中,每个业务都由很多分布式服务组成,而且这些服务都提供给内部系统使用。在这种情况下,除了编号错误码之外,更推荐使用显性化的错误码。

显性化的错误码具有更强的灵活性,适合敏捷开发。例如,我们可以将错误码定义成3个部分:类型+场景+自定义标识。每个部分之间用下划线连接,内容以大驼峰的方式书写。这里可以打破Java的常量命名规范,驼峰方式会更方便阅读。

对于错误类型,我们可以做一个约定:P代表参数异常(ParamException)、B代表业务异常(BizException)、S代表系统异常(SystemException)。例如:

image.png

如果业务应用的错误都用这种约定来描述和表达,那么只要大家都遵守相同的规范,系统的可维护性和可理解性就会大大提升。