代码格式
代码格式关系到代码的可读性,因此需要遵从一定的规范,包括缩进、水平对齐、注释格式等。关于代码格式,可能会因为语言和个人偏好而不同,但是一个团队最好是选定一种格式,因为一致性可以减少复杂度。
空行规范
空行在代码中的概念区隔作用同样适用。以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)。例如:
如果业务应用的错误都用这种约定来描述和表达,那么只要大家都遵守相同的规范,系统的可维护性和可理解性就会大大提升。