最近在Java上踩的坑
- 前言
- 项目规范
- README
- MVC
- Feign的本地熔断
- 代码规范
- 访问修饰符
- 日志的打印
- 数据库规范
- 表规范
- 表字段规范
- 职业素养
前言
换工作也有两个多月了,在新环境中无论说是站着前人的肩膀上也好,还是说踩前人挖的坑也罢,总结一下总是没错的。
项目规范
README
作为一个新人,刚进项目拿到代码,特别是当下流行的微服务体系,一个项目十几个微服务。也不可能有人有耐心告诉你每个微服务是干嘛的,核心功能是什么,是如何与其他微服务或第三方交互的。流水的程序员,铁打的项目。所以这时候READEME就显得特别重要了。README里面可以描述项目的核心业务、编码规范、工具类、启动参数等等,让新人拿到项目起码有个宏观的认知,而不是看到的只有一堆冰冷的代码,甚至连注释都没。
第二个月时,我负责一个新项目的开发,处理好项目结构后,第一件事就把README写了,当然README也是需要时时更新维护的。
MVC
SpringBoot对于MVC没有显性的配置,通过spring-boot-starter-web完成自动配置。但并不代表在编码中不需要遵守MVC的编码规范。以下是错误示范
@Api(description = "客户合同管理", value = "客户合同管理")
@RequestMapping("/customerContract")
public interface CustomerContractService extends IService<CustomerContract> {
/**
* 新增
*
* @param addCustomerContractDTO
* @return
*/
@PostMapping(value = "/saveCustomerContract")
JsonResult<Object> saveCustomerContract(@Valid @RequestBody AddCustomerContractDTO addCustomerContractDTO);
}
---------------------------------------------------------------------------------
@Api(description = "客户合同管理", value = "客户合同管理")
@Slf4j
@RestController
public class CustomerContractServiceImpl extends CommonService<CustomerContractDao, CustomerContract>
implements CustomerContractService, CustomerContractRemoteService {
@Override
public JsonResult<Object> saveCustomerContract(AddCustomerContractDTO addCustomerContractDTO) {
......
}
}
在接口中通过@RequestMapping来指定路径,然后把实现类当成@RestController,简便了开发但混淆了MVC的传统分层,我认为是得不偿失的。
Feign的本地熔断
大型微服务项目中,如果没有处理好熔断和限流机制,容易产生服务的雪崩,最后导致宕机。FeignClient提供了FallbackFactory,当远程调用出现超时或错误时,会执行本地的fallback方法进行兜底,防止出现长时间的等待从而拖垮服务器的性能。在新项目中缺失了FallbackFactory,所有的Feign接口都没有做调用失败的处理。这一块的优化必须排上日程了。
代码规范
访问修饰符
对变量使用的访问修饰符不严谨或者缺少访问修饰符,比如自动注入的接口,只在本类使用,应该再上private修饰符,下面是错误示范
日志的打印
对外的接口,入参的报文是一定要完整打印或者存储的,方便问题的排查。对于日志的拼接,使用占位符的方式会使得代码比较简洁,不推荐直接使用字符串拼接。
// 正例
log.info("中移动下单请求参数 = {}", JSON.toJSONString(string));
// 反例
log.info("中移动下单请求参数 = " + JSON.toJSONString(string));
有很多人觉得,用占位符可以减少字符串拼接的性能损耗,但并不是的。来看一下使用占位符的日志拼接源码。
public static final FormattingTuple arrayFormat(String messagePattern, Object[] argArray, Throwable throwable) {
if (messagePattern == null) {
return new FormattingTuple((String)null, argArray, throwable);
} else if (argArray == null) {
return new FormattingTuple(messagePattern);
} else {
int i = 0;
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
for(int L = 0; L < argArray.length; ++L) {
int j = messagePattern.indexOf("{}", i);
if (j == -1) {
if (i == 0) {
return new FormattingTuple(messagePattern, argArray, throwable);
}
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
if (isEscapedDelimeter(messagePattern, j)) {
if (!isDoubleEscaped(messagePattern, j)) {
--L;
sbuf.append(messagePattern, i, j - 1);
sbuf.append('{');
i = j + 1;
} else {
sbuf.append(messagePattern, i, j - 1);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
} else {
sbuf.append(messagePattern, i, j);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
}
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
}
从源码中可以发现,对于使用占位符加可变参数的方式进行日志拼接,源码的处理是遍历日志模板,取出占位符来进行替换,看到这里我觉得在性能上不一定会比直接拼接字符串的性能高,于是做了一个测试。
@Test
public void testStringLog() {
for (int i = 0; i < 10000; i++) {
log.info("测试" + "字符串" + "拼接");
}
}
@Test
public void testPlaceholder() {
for (int i = 0; i < 10000; i++) {
log.info("测试{}拼接", "字符串");
}
}
执行结果如下
时间实际上是差不多的,甚至多次执行的结果,字符串拼接由于String缓存池的存在,速度还会比占位符更快一些。但出于项目代码的统一规范,还是推荐使用占位符来进行日志的打印。最后说一下日志级别,根据不同环境的日志级别,对于代码中的日志输出也要有相应的处理,比如业务流程出现异常,日志级别要满足在生产也能输出的条件。
数据库规范
表规范
如果项目是微服务体系,建议在表名前加上服务名,如OMS的订单表ORDER,建议命名为OMS_ORDER。一是见名知意,看到实体就能知道对应的服务,二是可能其他服务也存在订单表,容易发生歧义。
表字段规范
1.主键: 不建议直接使用ID作为主键,而是使用表名加ID,如果订单表ORDER的主键为ORDER_ID。因为当主键作为其他表外键时,在涉及BEAN COPY时,ID字段的值会赋值错误,比如订单表ORDER的主键为ID,在订单详情表中主键也为ID,外键为ORDER_ID,那么BEAN COPY是,ID的赋值就出现错误了。主键的值不建议使用数据库自增,推荐雪花算法,如果使用数据库自增,那么数据库主键的类型必须为bigint,如果只是int,在业务量飞速增长的年代,很有可能出现超过21亿的数据,到时候再来重构,就很头疼了。
2.金额: 首先数据库所有的金额最好是统一单位,出现过有的表存储的是分,有的表存储的厘,在开发过程中简直是折磨人。金额的存储比较主流的有两种方式,一是直接存储小数,类型是BigDecimal,二是存储最小单位,比如厘,可以避免在计算过程中的精度丢失。存储小数的优点是返回给前端可以直接展示,不需要做转换。存储最小单位使用与需要大量计算的场景,可以提高数据的准确性。
3.注释: 数据库的注释一定要准确,如有修改也必须时时更新,包括代码实体中的注解,特别是数据字典类型的属性,否则在开发中难免会踩坑。
职业素养
写好每一行代码,写好每一句注解。我觉得程序员要对生产环境抱有敬畏之心,别让自己的BUG给别人带来麻烦。注解和代码一样重要,注解不仅是给下一个接手的人看的,也是给自己看的,在排查问题的时候,几行注解能帮助快速定位问题,所以代码的优化也包括补齐关键节点、里程碑节点的注解。最后说一下代码重构,这是一件我很热衷的事。程序员多多少少都有点代码的洁癖,觉得别人的代码写的看不惯,不符合自己的风格和逻辑,那既然接手了,为何只是抱怨而不去重构呢?未上生产的代码,大胆重构,已经在生产的代码,小心重构,充分测试。当然,风险是有,我也因为重构生产代码发生过生产事故,但这不是你拒绝重构代码的理由,而是作为下一次重构代码的宝贵经验,哪些地方没有考虑到,哪些地方需要更充分的测试。