总结代码风格

好久没更新博客,最近公司让我出一个代码规范,我吓了一跳。赶忙翻出阿里《 码出高效》,不敢造次,我就补充点个人的想法吧。代码是给人看的。代码风格应该遵循极简主义

1、避免复杂、追求简单

日常开发中,除了解决业务问题还需要解决许多的工程问题。如何选择当下合适的方法解决问题;需要不断尝试和摸索,没有最好的方法只有更好的方法。

2、合理平滑的处理技术债务

技术的演变速度太快,如何避免长时间的技术债务是非常严重的问题,总之弊大于利。尽可能保持轻装上阵,避免拖油瓶项目。

关键字:

  • 一方库: 本工程内部子项目模块依赖的库(jar 包)。
  • 二方库: 公司内部发布到中央仓库,可供公司内部其它应用依赖的库(jar包)。
  • 三方库: 公司之外的开源库(jar 包)。

『避免过度封装』

特别是没有完整的技术人员编制的情况下,怎么简单怎么处理(去除中间商赚差价一个道理。)避免过度开发一方库;建议使用原生方式(综合评估代码量)避免框架过度封装。(Java的方法调用链过长是出了名的恶心)

『避免代码过度重复』

每次开发业务都会写很多的代码。定期对公司项目进行基础代码的重构。合理的拆分业务无关的基础代码

『避免版本混乱』

使用统一的版本管理。约束所有的项目jar版本依赖。防止因为过度使用三方库出现奇怪的bug。公司bom需要单独处理。尽量保持与社区版本同步,比如springboot最新版是2.1.3 ,那么公司使用的版本最好是近半年的GA版本。

『物极必反』

请勿过度依赖某框架栈或者解决方式,客观的对比相关解决方案优缺点。

『选择大于努力』

集中精力掌握核心知识。按照目前技术演变的速度,更新最快的是应用技术,其次是行业规范相关技术。最后才是革命性的技术。

3、指定一种工程结构

统一的工程结构,对开发人员来说如同指路明灯,可以快速的区分相关代码位置。

『阿里巴巴Java开发手册-应用分层』

  • 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。
  • 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。
  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service 层:相对具体的业务逻辑服务层。
  • Manager 层:通用业务处理层,它有如下特征:
    1) 对第三方平台封装的层,预处理返回结果及转化异常信息;
    2) 对Service层通用能力的下沉,如缓存方案、中间件通用处理;
    3) 与DAO层交互,对多个DAO的组合复用。
  • DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
  • 外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。

『建议分层方式』

根据工作实际情况,参考阿里的应用分层后适当采纳。

  • 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。

  • 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染, JSP 渲染,移动端展示等

  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等

  • Service 层:相对具体的业务逻辑服务层。

  • Manager 层:通用业务处理层,它有如下特征:
    1) 对第三方平台封装的层,预处理返回结果及转化异常信息。
    2) 对Service层通用能力的下沉,如缓存方案、中间件通用处理。

    3) 与DAO层交互,对多个DAO的组合复用。

  • DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。

  • 外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。

目前Java已经不写前端代码,所以终端显示层和Web已经没有使用的价值。这里需要额外的注意,开放接口层不需要任何的业务操作,方便做完整的单元测试。将参数校验等操作放在具体业务执行的过程中。

『建议分层异常处理方式』

  • (分层异常处理规约)在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行catch,使用catch(Exception e)方式,并throw new xxxException(e),不需要打印日志,因为日志在 Manager/Service 层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在 Service 层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。如果 Manager 层与 Service 同机部署,日志方式与 DAO层处理一致,如果是单独部署,则采用与 Service 一致的处理方式。

『分层领域模型规约』

  • DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
  • BO(Business Object):业务对象,由 Service 层输出的封装业务逻辑的对象。
  • AO(ApplicationObject):应用对象,在Web层与Service层之间抽象的复用对象模型, 极为贴近展示层,复用度不高
  • VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象
  • Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的建议查询封装,禁止使用 Map 类来传输

没有必要过度设计、导致代码量增加。DTO、AO、VO Query 在实际开发过程实际上可以完全复用。Query是查询参数,通常用于用户分页查询和普通参数查询等封装体。BO需要高度抽象,属于业务模型。目前接口数据显示要求不高的情况可以适当放宽,不使用 BO层。

『分层参考』

  • 推荐的参考

主目录示例:com.github.z201.pre

  • com.github.z201 公司域名(这只是例子)
  • pre 项目名称
  • dao 模块名称

模块示例:com.github.z201.pre

  • dao #mybaits接口映射层
  • entity # mysql表映射层
  • dto # 网络传输层
  • manger # 第三方(缓存、事务、mp、外部接口)
  • service # 主要业务实现(无事务处理、简单业务)
  • utils # 项目工具类(该项目独立使用的)

对于该模块关键模块,建议单独区分,用于识别。按照黄金法则,一个模块中核心接口少数。

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
➜  demo git:(develop) ✗ tree src/main -d -L 7
src/main
├── java
│   └── com
│   └── github
│   └── z201
│   └── pre
│   ├── annotation # 关键业务(注解拦截层)
│   ├── dao # mybaits接口映射层
│   ├── entity # 实体映射层
│   ├── dto
│  │ ├── cache # 缓存传输层
│   │   ├── param # 请求参数封装体(Query的细化)
│   │   ├── result # 响应参数封装体
│   │   └── search # 查询参数封装体(Query的细化)
│   ├── limit # 关键业务实现(关键业务不建议放到service层,方便快速识别。)
│   │   └── impl
│   ├── manger # 第三方(缓存、事务、mp、外部接口)
│   │   └── impl # 实现类
│   ├── service # 主要业务实现(无事务处理)
│   │   └── impl # 实现类
│   └── utils # 项目工具类(该项目独立使用的)
│   └── common
│  
└── resource
└── mapper # mybatis文件。

4、保持代码的整洁

写出运行代码(bug)是简单的、写出适合阅读的代码是困难,建议阅读阿里巴巴Java开发手册 以及码出高效Java开发手册相关章节。这里做下内容补充。

『尽可能少写代码』

写的越多错的越多,错的越多修复越难。

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
// 使用lombok语法糖,简化模版代码。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmSpaceFunctionCache {

/**
* 主键
*/
private Integer id;

/**
* 功能名称
*/
private String name;

/**
* 图片主键id(使用运营后台 sm_admin_pictrue表存储图片)
*/
private Integer adminPictureId;

/**
* 图片地址
*/
private String picPath;

/**
* 排序 默认 0
*/
private Integer orderBy;
// 无 get 、 set 更多特性请查阅lombok使用方法。
}

『提高代码覆盖率』

(避免无调用代码,避免过度使用代码生成器)健壮的代码是干净、简洁的。避免重型项目出现(保持项目业务代码1-2W行,可以适当的模块化)

『合理拆分代码逻辑』

(避免代码过度优化和过早优化,需求一定会改、一定会改、一定会改)适当调整代码,保证阅读方便即可。推荐使用阿里p3c代码检查工具。鬼故事:李光磊以前劝华为的同事用Eclipse,人家打死不肯用。他自己搞起来,给人家说,你看,多方便。华为的同时默默的输入了一个文件名,跳过去,Eclipse崩贵了,文件太大。

『更新注释』

代码千万行,注释第一行。注释不规范,同事两行泪。改代码不改注释非常容易误导他人。尽可能保持个人代码的注释信息合理。

『面向接口编程』

  • 面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其一部分。或者说,它是面向对象编程体系中的思想精髓之一。
  • 在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的。在这种情况下,各个对象内部是如何实现自己的,对系统设计人员来讲就不那么重要了;而各个对象之间的协作关系则成为系统设计的关键。小到不同类之间的通信,大到各模块之间的交互,在系统设计之初都是要着重考虑的,这也是系统设计的主要工作内容。面向接口编程就是指按照这种思想来编程。
  • 降低程序的耦合性。在程序中紧密的联系并不是一件好的事情,因为两种事物之间联系越紧密,更换其中之一的难度就越大,扩展功能和debug的难度也就越大。
  • 易于程序的扩展。
  • 有利于程序的维护。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 示例代码,在接口暴露层,是不建议做任何的业务操作。建议使用bean封装数据。
**/
@RestController
@RequestMapping(FunctionController.ROUTER_INDEX)
public class FunctionController{

public static final String ROUTER_INDEX = "/api/space";

@Autowired
PackageFunctionServiceI packageFunctionService;

/**
* 获取功能分页列表
* @return
*/
@PostMapping("/function/list")
public Object listSpaceFunction(@RequestBody SpacePackageFunctionSearch pageSearch) {
return packageFunctionService.listSpaceFunction(pageSearch);
}
.....
  • if/for/while/switch/do等保留字与左右括号之间都必须加空格。
1
2
3
4
# 合理的增加空格,方便阅读代码。
if (null == endTime || 0L == endTime) {
throw new RuntimeException("获取用户vip的到期时间失败了,数据出现异常。~~~");
}

5、防御式编程

  • 请不要相信任何参数。尽可能保持客观的态度编写代码。参考<<代码大全>> 『人类都是不安全、不值得信任的,所有的人,都会犯错误,而你写的代码,应该考虑到所有可能发生的错误,让你的程序不会因为他人的错误而发生错误』
1
2
3
4
5
6
7
8
9
10
11
12
// 这是从内部接口调用的数据,首先不信任给的数据。避免常规错误导致自己的逻辑出现明显的bug。
// 获取vip的到期时间
try {
endTime = spaceVipCacheService.getSpaceVipEndTimeByUserId(userId);
} catch (InvalidProtocolBufferException e) {
log.warn("获取用户vip的到期时间失败了,系统出现异常。~~~ ");
throw new RuntimeException("获取用户vip的到期时间失败了,系统出现异常。~~~");
}
if (null == endTime || 0L == endTime) {
log.warn("获取用户vip的到期时间失败了,系统出现异常。~~~ ");
throw new RuntimeException("获取用户vip的到期时间失败了,数据出现异常。~~~");
}

6、抛异常 or 返回错误码 or 日志

公司外的http/api开放接口必须使用“错误码”;应用内部推荐异常抛出(适当抛出堆栈,性能影响)。避免恶意请求接口,并通过返回消息猜出接口参数的问题。日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。

  • 就java日志框架而言,建议使用侨接slf4j来输出日志。
    • 当遇到问题的时候,只能功过debug功能来确定问题。应该考虑输出日志信息,良好的系统日志信息对问题进行定位的。
    • 项目中大量的分支判断if \else、switch 的分支时候使用日志可以定位具体是哪个业务流程。
1
2
3
4
5
6
7
8
9
10
//尽可能参数化日志信息,日志是给人看的。不是为了输出而输出。关键参数可以隔离显示比如 [{}]
logger.debug("这是一条debug日志 [{}]" , userId);

// 对于debug日志,必须判断日志的级别才能进行输出。
if(logger.isDebugEnabled()){
logger.debug("这是一条debug日志 [{}]" , userId);
}

// 避免使用字符串拼接的方式输出日志,这样会导致生产了很多的string对象。
logger.debug("这是一条字符串拼接的日志输出 : [" + userId +"]");
  • 日志级别的使用
    • 日志级别 trace, debug, info, warn, error, fatal
1
2
3
4
5
6
7
8
9
10
11
12
 这里以log4j相关的日志的打印级别,OFF即不打印,其他则按照标准级别配置即可,如 debug 
关闭:OFF(0)
致命:FATAL(100),对应Logger.fatal方法
错误:ERROR(200),对应Logger.error方法
警告:WARN(300),对应Logger.warn方法
信息:INFO(400),对应Logger.info方法
调试:DEBUG(500),对应Logger.debug方法
跟踪:TRACE(600),对应Logger.trace方法
全部:ALL(Integer.MAX_VALUE)
当指定某一个级别时,比如DEBUG,那么所有低于这个级别的其它级别日志,都会被打印。
当指定级别为DEBUG时,Logger.debug、Logger.info、Logger.warn、Logger.error以及Logger.fatal等方法
都能输出日志,但Logger.trace无法输出日志。
  • error:对于影响到程序正常运行的信息,需要及时补货并输出,适用范围配置文件读取失败、第三方调用失败、数据库连接失败、缓存等关键组件失败异常

7、方法命名、变量命名

建议阅读阿里巴巴Java开发手册 以及码出高效Java开发手册相关章节。这里做下内容补充。

  • 变量命名

    如果想不到合适的变量名字,麻烦把注释写清楚。如果连思考的时间都没有请使用TODO标记。

  • service层

    如果想不到合适的方法名字,麻烦把注释写全。如果连思考的时间都没有请使用TODO标记。

  • Dao层

    如果是批量建议加上Batch。

1
2
3
4
5
6
7
获取单个对象 `getXxx`
获取多个对象`listXxx`
通过复杂的查询`listXxxBySearch`这里的`search`就是参数封装体
获取统计值 `countXxx`
插入`saveXxx` / `insertXxx`
删除 `removeXxx` / `deleteXxx`
修改 `updateXxx`

8、Redis

  • key名设计
1
2
3
4
5
6
7
8
9
10
#建议:可读性和可管理性(redis作为标准缓存时推荐)
#以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
ugc:video:1

#简洁性
#建议:保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:
user:{uid}:friends:messages:{mid}简化为u:{uid}:fr:m:{mid}。

#强制:不要包含特殊字符
反例:包含空格、换行、单双引号以及其他转义字符
  • value设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 强制:拒绝bigkey(防止网卡流量、慢查询)
string类型控制在20KB以内,hash、list、set、zset元素个数不要超过5000,这里指的是field(不是key)。
# 反例:一个包含200万个元素的list。
非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查))。

# 建议- 选择适合的数据类型。
例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡)

# 反例
set user:1:name tom
set user:1:age 19
set user:1:favor football

改进
hmset user:1 name tom age 19 favor football
  • 控制key的生命周期,redis不是垃圾桶。

建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。

  • 【建议】:禁用命令

禁止线上(正式环境)使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。

  • 【建议】使用批量操作提高效率
1
2
3
4
5
6
7
原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
注意两者不同:
1. 原生是原子操作,pipeline是非原子操作。
2. pipeline可以打包不同的命令,原生做不到。
3. pipeline需要客户端和服务端同时支持。
  • 【建议】Redis(伪)事务功能较弱,不建议过多使用

Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上(可以使用hashtag功能解决)。