1、可读性 1.1 准确命名 每种编程语言都有自己的命名规范,不同语言的风格差异有大有小,下面以Java为例:
Java整体命名风格为UpperCamelCase或lowerCamelCase形式。不管是类还是变量命名要见名知意,切勿使用缩写或中文;风格统一,尽量使用英文名词,切勿中英文混合;尽量避免和Java自带类库重名,切勿使用Java关键字命名。
包命名使用小写英文名词,使用“.”分割,每个被分割的单元尽量只有一个名词,命名规范为:
域名.公司/个人名称.项目名称.模块名称
类采用UpperCamelCase命名风格,一些特殊的缩写可以采用全大写,如XML。类命名使用名词描述类的作用。
接口采用UpperCamelCase命名风格,由于接口定义的是一类功能或动作,所以接口的命名一般使用形容词或动词描述接口的行为。
抽象类除了满足UpperCamelCase风格外,一般需要加上Abstract前缀。
异常类除了满足UpperCamelCase风格外,一般需要加上Exception或Error后缀,使用名词描述什么异常或错误。
枚举类除了满足UpperCamelCase风格外,一般加上Enum后缀,枚举类中的枚举值采用全部大写风格,单词与单词之间使用“_”进行分割。
方法命名采用lowerCamelCase风格,一般使用动词+名词来命名,比较常见的有doXxx,handleXxx,findXxxx。
变量命名采用lowerCamelCase风格,一般使用名词描述变量的作用,需要注意的是区别于常量,尽量不要使用特殊符号前缀或使用“_”分割符号。
常量命名采用全部大写,单词与单词之间使用“_”进行分割。
1.2 代码风格 在日常的项目开发中,一个项目有很多人协同开发,每个人使用的开发工具不一样,比如大家常用的vs code和idea,不同的开发工具或代码习惯也会导致代码风格不一致,我们在开发时可能习惯性的进行代码格式化,就会导致整个类改动非常多,在代码合并时容易冲突。
我们可以在项目中增加.editorconfig文件来统一代码风格。
复制 root = true
[ * .{ adoc, bat, groovy, html, java, js, jsp, kt, kts, md, properties, py, rb, sh, sql, svg, txt, xml, xsd} ]
charset = utf- 8
[ * .{ groovy, java, kt, kts, xml, xsd} ]
indent_style = tab #tab键缩进,可选"space" 、"tab"
indent_size = 4 #缩进空格为4个
end_of_line = lf #结尾换行符,可选"lf" 、"cr" 、"crlf"
charset = utf- 8 #文件编码
trim_trailing_whitespace = true #不保留行末的空格
insert_final_newline = true #文件末尾增加一个空行
curly_bracket_next_line = false #大括号不另起一行
spaces_around_operators = true #运算符两边都有空格
indent_brace_style = 1 tbs #条件语句格式是1tbs
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 1.3 注释规约 类注释
类注释采用/**......*/,在每个类的头部要有必要的注释信息,包括:作者、创建时间、类功能描述
复制
/**
* 简单分流算法实验, 每次分流只用考虑当前的桶, 不用回溯历史版本
* {@link https://duapp.yuque.com/team_tech/confluence-data-iwskfg/dzmogk}
* @author hufei
* @date 2021/6/8 7:59 下午
*/
接口注释
接口注释采用/**......*/,在满足类注释的基础上,接口注释应该包含接口的目的、如何使用。
复制
/**
* AB分桶算法接口规范
* 对外暴露实验桶计算接口,该接口有一个抽象实现类AbstractBucketAlgorithm,具体的分桶算法实现这个抽象类
* @author hufei
* @date 2021/6/8 6:06 下午
*/
方法注释
方法注释采用/**......*/,描述方法的功能、输入、输出及返回值说明
复制 /**
* 计算实验层的桶信息
* @param layerId 分层 id
* @param expId 实验 Id
* @param expRatio 新的实验占层流量比例
* @param existsLayerBucket 老的层流量实验配比
* @return 新的层流量实验配比
* @throws BucketAlgorithmException
*/
方法内部注释
代码做了些什么以及为什么这样做,特别是复杂的逻辑处理部分,要尽可能的给出详细的注释。
全局变量注释
包括变量的功能、取值范围、注意事项等的说明。
复制
/**
* 代表kafka收到消息的答复数,0就是不要答复,爱收到没收到.1就是有一个leader broker答复就行,all是所有broker都要收到才行
* 0: Producer不等待kafka服务器的答复,消息立刻发往socket buffer,这种方式不能保证kafka收到消息,设置成这个值的时候retries参数就失效了,因为producer不知道kafka收没收到消息,所以所谓的重试就没有意义了,发送返回值的offset全默认是-1.
* 1: 等待leader记录数据到broker本地log即可.不等待leader同步到其他followers,那么假如此时刚好leader收到消息并答复后,leader突然挂了,其他fowller还没来得及复制消息呢,那么这条消息就会丢失了.
* all:等待所有broker记录消息.保证消息不会丢失(只要从节点没全挂),这种方式是最高可用的 acks默认值是1.
*/
private String acks = "0";
局部变量注释
主要的局部变量必须有注释,无特别意义的情况下可以不加注释。
2可靠性 2.1 增强健壮性 慎用递归算法
递归算法写起来很简单,但用的不好容易导致堆栈溢出和死循环问题。因此尽量不要使用递归算法,如果要使用需要注意以下几个问题:
把退出条件放在函数最上方,这样比较清晰,防止程序一直不满足退出条件而导致堆栈溢出
复制
public int recursiveAlgorithm( ) {
if ( 退出条件)
return 0 ;
......
}
避免在递归函数中出现过大的局部变量,这会加速堆栈空间的消耗
复制
public int recursiveAlgorithm( ) {
char buf[ ] = new char [ 1024 ] ;
}
增加一个最大递归深度,防止出现死循环导致堆栈溢出
使用参数校验
项目中有很多接口提供给前端,有一些参数在后续的逻辑处理中不能为空并且没有做非空校验,靠口头的约定参数不能为空,如果前端调用时没有传参就会出现空指针异常。最好的解决办法是统一参数校验框架,在接口的入参进行非空限制,如果为空,统一抛出异常。
复制 @Valids( {
@Valid( names = "request.expStatus" , required = true , regex = "[1,2]" , error = "实验状态必须为1或者2" ) ,
@Valid( names = "request.weekDateList,request.type" , required = true )
} )
幂等校验
我们的系统应该做好幂等校验,等于同一笔业务操作,不管调用多少次,得到的结果都是一样的。比如用户重复下单、MQ消息重复消费、前端重复提交表单等,这些都是需要保证幂等操作。
2.2 善始善终 异常处理
我们在进行异常处理时,一定要把必须执行的语句放到finally块中,比如读写文件时,在finally块中关闭IO连接。
复制 BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("")));
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14.
加锁释放锁
我们在日常的开发中经常会碰到资源竞争的问题,此时我们需要对竞争的资源进行加锁处理,如果我们忘记释放锁,就会导致其他的请求阻塞。
复制 Lock lock = new ReentrantLock( ) ;
lock.lock ( ) ;
......
lock.unlock ( ) ;
资源释放
我们在日常的开发中会用到数据库或网络资源,此时我们需要建立数据库连接或网络连接,如果我们忘记释放连接就会导致数据库资源或网络资源一直被占用,直到连接失效。虽然我们在日常的开发中不会直接创建连接而是使用连接池,但是连接数最大连接数如果设置的过大,也会导致资源的耗尽。
复制
ResultSet resultSet = null ;
Statement statement = null ;
Connection connection = null ;
try {
Class.forName ( "com.mysql.cj.jdbc.Driver" ) ;
connection = DriverManager.getConnection ( url, user, pwd) ;
statement = connection.createStatement ( ) ;
resultSet = statement.executeQuery ( "select * from student" ) ;
} catch ( SQLException e) {
e.printStackTrace ( ) ;
} catch ( ClassNotFoundException e) {
e.printStackTrace ( ) ;
} finally {
try {
if ( resultSet != null ) {
resultSet.close ( ) ;
}
} catch ( SQLException e) {
log.error ( e.getMessage ( ) , e) ;
}
try {
if ( statement != null ) {
statement.close ( ) ;
}
} catch ( SQLException e) {
log.error ( e.getMessage ( ) , e) ;
}
try {
if ( connection != null ) {
connection.close ( ) ;
}
} catch ( SQLException e) {
log.error ( e.getMessage ( ) , e) ;
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 2.3 异常处理 尽量使用非受检异常
受检异常的缺陷:
例如我们定义一个接口
复制 interface User {
public void changePassword( ) throws MySecurityException;
}
随着业务的开发,接口抛出的异常增加,比如新增一个RejectChangeException,那就需要修改User接口,这就会导致所有的User接口调用者都要追加对RejectChangeException异常的处理。
一个方法增加了受检异常,则调用者必须对异常进行处理,比如调用无受检异常:
复制 public static void main( String[ ] args) {
userImpl.changePassword ( ) ;
}
如果调用受检异常就不一样了:
复制
public static void main(String[] args) {
try {
userImpl.changePassword();
} catch(Exception e){
e.printStackTrace();
}
}
如果一个方法使用了受检异常,那么调用者就必须处理,特别是在多个异常的情况下,要增加多个catch块进行处理,就会增加代码复杂度。
不要在finally块中处理返回值
在finally块中进行return会导致如下问题:
覆盖了try代码块中的return返回值,比如下面的方法,我们传入100返回结果也是-1。
复制
public static void main( String[ ] args) {
calculate( 100 ) ;
}
public static int calculate( int number) throws Exception {
try {
if ( number < 100 ) {
throw new DataFormatException( "数据格式错误" ) ;
} else {
return number;
}
} catch ( Exception e) {
throw e;
} finally {
return - 1 ;
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 当我们在try块中抛出异常时,异常线程在监视到有异常发生时,就会在异常表中登记当前的异常类型为DataFormatException,但是当执行器执行finally代码块时,就会重新为方法赋值,也就是告诉调用者“该方法执行正确,没有异常产生”,比如我们调用上面的方法,传入-1,不会抛出异常。
异常封装 Java中提供了异常处理机制,可以保证程序的健壮性,但是Java中提供的都是通用异常,我们在项目的开发中需要封装一些业务的异常。
统一异常处理 在项目开发中,可以用切面统一异常处理或依赖SpringMVC的ControllerAdvice,将错误信息按项目统一格式返回给前端,这样在开发过程中只用抛出异常就可以了。
禁止直接吞掉异常
吞掉异常会导致难以排查程序运行过程中出现的问题,应该将异常向上抛出。
2.4 留意编译告警 程序中的编译告警容易被忽略,因为即使出现了告警,源文件仍能被编辑通过并运行,尤其是我们在开发的过程中使用IDE,但这些告警中往往隐藏着一些潜在的问题。
2.5 尽早暴露问题 一个bug在项目的开发、自测、测试、发布等阶段被发现,其修复成本是不一样的,越往后修复成本越高,尤其到了线上可能还会造成一定的资损。项目研发在开发和自测的过程中,应重视代码质量,自测或使用bug扫描工具来尽早的发现问题。
SpotBugs
SpotBugs提供静态字节代码分析,它使用静态分析来查找400多种错误模式,例如空指针取消引用、无限递归循环,对Java库的错误使用和死锁。
3、可维护性 3.1 记录日志 所有后台都要有操作日志、数据变更日志 日志要配置异步写盘 线上仅保留WARN和ERROR级别日志 所有日志都要有traceId 异常日志要有堆栈、入参、能说清楚是什么错误的信息 打印日志时,禁止直接用JSON工具将对象转换成String 3.2 明确错误提示 在产品的使用中,我们会提示一些错误信息给用户,但是如果提供笼统的错误提示可能令用户感到困惑,如:“服务暂不可用”,尤其我们不能提示:“系统内部错误,请联系系统管理员!”给用户,这会降低用户对产品的信任度。我们可以提示具体的错误信息,如“xxx信息未填,请先填写完成。”
3.3 保持代码简洁性 避免嵌套if/else
代码中经常会进行空值判断和逻辑判断,if/else嵌套会使得代码逻辑看起来非常复杂。
复制
public static String getDepartmentNameOfUser( String username) {
Result< User> result = getUserByName( username) ;
if ( result != null ) {
User user = result.getData ( ) ;
if ( user != null ) {
Department department = user.getDepartment ( ) ;
if ( department != null ) {
return department.getName ( ) ;
}
}
}
return "未知部门" ;
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 尽量避免嵌套if/else,可以这样写:
复制 public static String getDepartmentNameOfUser( String username) {
Result< User> result = getUserByName( username) ;
if ( result == null ) {
return "未知部门" ;
}
User user = result.getData ( ) ;
if ( user == null ) {
return "未知部门" ;
}
Department department = user.getDepartment ( ) ;
if ( department == null ) {
return "Department为空" ;
}
return department.getName ( ) ;
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 抽取类、方法
让类或方法的职责更加明确,不要把所有的逻辑写到一个方法里面。
复制 public boolean addExp( ExpVO expVO) {
// 校验参数是否正确,如果失败直接抛出异常
checkParamValidate( expVO) ;
// 新增实验信息
addExp( expVO) ;
// 新增实验组
expGroupService.addExpGroup ( expVO) ;
// 新增实验层
expLayerService.addExpLayer ( expVO) ;
// 计算实验流量
List< ObjectFLowEntity> flowEntityList = flowService.calculateFlow ( expVO) ;
// 保存流量信息
flowService.saveFlow ( flowEntityList) ;
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 不要使用魔法值
不要在代码中使用魔法值,这样后续如果值变化改动起来会漏掉。
复制
public boolean addExp( ) {
// 服务端实验
expEntity.setExpType ( 1 ) ;
}
可以使用枚举:
复制
public boolean addExp( ) {
expEntity.setExpType ( ExpTypeEnum.SERVER .getExpType ( ) ) ;
}
3.4 使用开源工具 使用一些开源工具可以减少我们重复造轮子,而且常用的开源工具都有完整的单元测试覆盖,可以有效的减少bug的出现。
Google Guava
Guava是一组来自Google的核心Java库,其中包括集合、缓存、原生类型、并发、常用注解、基本字符串操作和I/O等等。
比如集合的交、并、查集,使用Google Guava就很方便。
复制
Set < Integer > sets = Sets.newHashSet ( 1 , 2 , 3 , 4 , 5 , 6 ) ;
Set < Integer > sets2 = Sets.newHashSet ( 3 , 4 , 5 , 6 , 7 , 8 , 9 ) ;
// 交集
SetView< Integer > intersection = Sets.intersection ( sets, sets2) ;
// 差集
SetView< Integer > diff = Sets.difference ( sets, sets2) ;
// 并集
SetView< Integer > union = Sets.union ( sets, sets2) ;
Apache Commons
Apache Commons是对JDK的扩展,包含了很多开源的工具,下面是我们项目中常用的工具:
Commons Lang3:处理Java基本对象方法的工具类,提供对字符、数组等基本对象的操作。
Commons Codec:提供常用的编码和解码方法,如DES、SHA1、Base64。
Commons BeanUtils:提供Bean的动态生成。
Commons HttpClient:简化HTTP客户端与服务端的各种通讯。
Log4j
各大开源框架和项目中用的最多的日志框架。
4、可扩展性 我们写的代码都是为了特定的需求服务的,但是这些需求并不是一成不变的,当需求变更了,如果我们代码的扩展性很好,我们可能只需要简单的添加或者删除模块就行了,如果扩展性不好,可能所有的代码都需要重写,所以提供代码的扩展性是必须的,我们在写代码的时候使用设计模式可以使代码具备很好的扩展性。
比如AB分流算法,分流算法根据不同的场景不同的需求有不同的实现,我们定义好算法的接口,不同的分流算法实现这个接口,那么我们在使用的时候只需要考虑使用哪个算法就行了,不需要关心算法的实现。
复制 /**
* AB分桶算法
* @author hufei
* @date 2021/6/8 6:06 下午
*/
public interface BucketAlgorithmTemplate {
/**
* ab分桶算法
*
* @param layerId
* @param expId 实验id
* @param expRatio 实验占层流量比
* @param expGroups 实验组详情
* @param existsBucket 已存在的实验层和实验组流量详情,第一次创建传null
* @return
* @throws BucketAlgorithmException
*/
public Map< Integer , JSONObject> calculateBucket( Integer layerId, Integer expId, Integer expRatio, Map< Integer , Integer > expGroups, Map< Integer , List< JSONObject>> existsBucket) throws BucketAlgorithmException;
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20.
复制 /**
* AB分桶算法
* @author hufei
* @date 2021/6/8 6:39 下午
*/
public abstract class AbstractBucketAlgorithm implements BucketAlgorithmTemplate {
/**
* @param expId 实验id
* @param expRatio 实验占层流量比
* @param expGroups 实验组详情
* @param existsBucket 已存在的实验层和实验组流量详情,第一次创建传null
* @return
* @author hufei
* @description ab分桶算法
* @date 2021/6/8 6:31 下午
*/
public Map< Integer , JSONObject> calculateBucket( Integer layerId, Integer expId, Integer expRatio, Map< Integer , Integer > expGroups, Map< Integer , List< JSONObject>> existsBucket) throws BucketAlgorithmException {
calculateVerify( ) ;
Map< Integer , JSONObject> bucketMap = new HashMap<> ( ) ;
JSONObject layerObj = new JSONObject( ) ;
JSONObject expObj = new JSONObject( ) ;
if ( existsBucket == null ) {
// 如果不存在历史分流信息, 说明层是新建, 实验也是新建
layerObj = newLayer( layerId, expId, expRatio) ;
expObj = newExp( expGroups) ;
} else if ( existsBucket.get ( layerId) != null && existsBucket.size ( ) == 1 ) {
// 只有层的历史记录, 但是没有实验的历史记录, 说明层已经存在实验是新建
layerObj = calculateLayer( layerId, expId, expRatio, existsBucket.get ( layerId) ) ;
expObj = newExp( expGroups) ;
} else if ( existsBucket.get ( layerId) != null && existsBucket.get ( expId) != null ) {
// 有层和实验的历史记录, 说明层不是新建并且实验也不是新建
layerObj = calculateLayer( layerId, expId, expRatio, existsBucket.get ( layerId) ) ;
expObj = calculateExp( expGroups, existsBucket.get ( expId) ) ;
}
bucketMap.put ( layerId, layerObj) ;
bucketMap.put ( expId, expObj) ;
return bucketMap;
}
/**
* @param layerId
* @param expId
* @param expRatio
* @return
* @author hufei
* @description 新建层
* @date 2021/6/10 11:24 上午
*/
public JSONObject newLayer( Integer layerId, Integer expId, Integer expRatio) {
return new JSONObject( ) ;
}
public JSONObject calculateLayer( Integer layerId, Integer expId, Integer expRatio, List< JSONObject> layerHistoryFlow) {
return new JSONObject( ) ;
}
/**
* @return
* @author hufei
* @description 新建实验
* @date 2021/6/10 11:24 上午
*/
public JSONObject newExp( Map< Integer , Integer > expGroups) {
return new JSONObject( ) ;
}
public JSONObject calculateExp( Map< Integer , Integer > expGroups, List< JSONObject> expHistoryFlow) {
return new JSONObject( ) ;
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67.
复制 /**
* 简单分流算法实验, 每次分流只用考虑当前的桶, 不用回溯历史版本
* {@link https://duapp.yuque.com/team_tech/confluence-data-iwskfg/dzmogk}
* @author hufei
* @date 2021/6/8 7:59 下午
*/
public class SimpleBucketAlgorithm extends AbstractBucketAlgorithm {
/**
* @param layerId
* @param expId
* @param expRatio
* @return
* @author hufei
* @description 新建层
* @date 2021/6/10 11:24 上午
*/
@Override
public JSONObject newLayer( Integer layerId, Integer expId, Integer expRatio) {
......
}
@Override
public JSONObject calculateLayer( Integer layerId, Integer expId, Integer expRatio, List< JSONObject> layerHistoryFlow) {
......
}
/**
* @return
* @author hufei
* @description 新建实验
* @date 2021/6/10 11:24 上午
*/
@Override
public JSONObject newExp( Map< Integer , Integer > expGroups) {
......
}
/**
* 实验组流量变更:
* 1.优先从右边空白开始分配
* 2.先增后减
*
* @param expGroups
* @param expHistoryFlow
* @return
*/
@Override
public JSONObject calculateExp( Map< Integer , Integer > expGroups, List< JSONObject> expHistoryFlow) {
.......
}
/**
* 递归计算
*/
private void calculateExpRecursion( JSONObject currentExpFlow, Map< Integer , Long > groupCountMap, List< Integer > positiveRatioList, List< Integer > negativeRatioList, Map< Integer , Long > groupNeedAddOrReduceRatioMap) {
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54.
5、效率 5.1 代码优化 循环优化
复制 for ( int i= 0 ; i< list.size ; i++ ) {
...
}
for ( int i= 0 , size= list.size ( ) ; i< size; i++ ) {
...
}
不要在循环中创建对象
集合优化
在初始化集合时,尽量指定可预知的集合大小,减少集合的扩容次数。
5.2 引入并发 并发可以很好的提升程序的执行时间,但是使用不好也会带来很多问题。如果任务和任务之间没有关联性,我们并发的执行任务来缩短整体时间。
复制
CompletableFuture[ ] cfs = tailorEntry.getValue ( ) .values ( ) .stream ( ) .map ( layerExtraInfo -> CompletableFuture.supplyAsync ( ( ) -> layerCalculate( layerExtraInfo, userHitWhiteListMap, request.getUserId ( ) , needGroupId, request.getCurrentGroupParm ( ) ) , asyncFlowServiceExecutor) .whenComplete ( ( r, e) -> {
if ( ! r.isEmpty ( ) ) {
hitGroupList.addAll ( r) ;
r.forEach ( g -> {
needGroupId.add ( g.getId ( ) ) ;
} ) ;
}
} ) ) .toArray ( CompletableFuture[ ] :: new) ;
CompletableFuture.allOf ( cfs) .join ( ) ;
提高代码质量是一个复杂且持续的工作,一篇文章的讲解也很有限,我们在项目的开发中需要持续不断的迭代优化,来保证代码的质量。
*文