前言毕业快三年了,前后在几家公司工作,认识了各种各样的同事。各种代码我都见过,优秀的,垃圾的,难看的,看了就想跑的等等,所以本文记录一个优秀的Java后端开发应该具备哪些良好的开发习惯。拆分合理的目录结构是受传统MVC模型的影响。传统的做法多是固定几个文件夹controller、service、mapper、entity,然后无限添加。最后你会发现一个service文件夹下有几十个文件夹。Service类有上百个,业务模块没法区分。正确的做法是在写服务的上层新建一个modules文件夹,在moudles文件夹下根据不同的业务创建不同的包,在这些包下写具体的service、controller、entity、enums包或者继续拆分。开发版本迭代后,如果某个包能继续拆字段,继续拆,就可以清楚的看到项目的业务模块。后续拆解微服务也很简单。封装方法参数当你的方法参数太多的时候,请封装一个对象……下面是反面教材,谁教你这么写代码的!publicvoidupdateCustomerDeviceAndInstallInfo(longcustomerId,StringchannelKey,StringandroidId,Stringimei,StringgaId,StringgcmPushToken,StringinstanceId){}复制代码写一个对象publicclassCustomerDeviceRequest{privateLongcustomerId;//省略属性......}你为什么要这样写复制代码?例如,您的方法用于查询。如果以后增加查询条件,是否需要修改方法?每次添加方法参数列表都必须更改。封装一个对象,无论以后添加多少查询条件,只需要在对象中添加字段即可。而且关键是代码看着很舒服!封装业务逻辑如果你看过“屎山”,你会有很深的感受。这种方法可以写几千行代码,而且一点规律都没有……经常负责人会说,这个业务太复杂了,没办法改进。其实,这是偷懒的借口。无论业务多么复杂,我们都可以通过合理的设计和封装来提高代码的可读性。下面是两段高级开发人员写的代码(假装是高级开发人员)@TransactionalpublicChildOrdersubmit(LongorderId,OrderSubmitRequest.Shopshop){ChildOrderchildOrder=this.generateOrder(shop);childOrder.setOrderId(orderId);//订单源APP/微信小程序childOrder.setSource(userService.getOrderSource());//验证优惠券orderAdjustmentService.validate(shop.getOrderAdjustments());//订单产品orderProductService.add(childOrder,shop);//订单附件orderAnnexService.add(childOrder.getId(),shop.getOrderAnnexes());//处理订单地址信息processAddress(childOrder,shop);//最后插入订单childOrderMapper.insert(childOrder);this.updateSkuInventory(shop,childOrder);//发送订单创建事件applicationEventPublisher.publishEvent(newChildOrderCreatedEvent(this,shop,childOrder));returnchildOrder;}复制代码@TransactionalpublicvoidclearBills(LongcustomerId){//获取清算所需的账单、押金等信息ClearContextcontext=getClearContext(customerId);//验证金额是否合法checkAmount(context);//判断优惠券是否可用,返回抵扣金额CouponDeductibleResponsedeductibleResponse=couponDeducted(context);//清除所有账单DepositClearResponseresponse=clearBills(context);//更新l_pay_depositlPayDepositService.clear(context.getDeposit(),response);//发送还款对账消息repaymentService.sendVerifyBillMessage(customerId,context.getDeposit(),EventName.DEPOSIT_SUCCEED_FL)/更新账户balanceaccountService.clear(context,response);//处理已清算的优惠券,已使用或未绑定的couponService.clear(deductibleResponse);//保存优惠券抵扣记录clearCouponDeductService.add(context,deductibleResponse);}复制代码这两段代码中的业务其实很复杂。估计内部保守做了5万件,但是不同层级的人写的完全不一样。不得不赞一下这个评论,业务的拆分和方法的封装一个大业务里面有多个小业务。不同的业务可以调用不同的服务方法。后来接手的人即使没有流程图等相关文件也能很快了解这里的业务。很多初级开发写的业务方法都是上一行代码是A业务,下一行代码是B业务,下一行代码是A业务,还有一堆单元逻辑嵌套在业务之间电话,这很混乱,有很多代码。判断集合类型是否不为空的正确方法很多人喜欢写这样的代码来判断集合if(list==null||list.size()==0){returnnull;}复制代码当然你非要写这个没问题。。。但是你不觉得难受吗,现在framework里面任何一个jar包都有一个采集工具类,比如org.springframework.util.CollectionUtils,com.baomidou.mybatisplus.core.toolkit.CollectionUtils。以后请这样写if(CollectionUtils.isEmpty(list)||CollectionUtils.isNotEmpty(list)){returnnull;}复制代码集合类型返回值不要returnnull当你的业务方法返回值是集合类型时,请不要返回null,正确的操作是返回一个空集合。看mybatis的列表查询。如果未找到任何元素,则返回一个空集合而不是null。否则,调用者就得做NULL判断,大多数场景下对象也是如此。尽量不要使用基本类型来映射数据库属性。我们都知道作为成员变量的int/long等基本数据类型的默认值为0。现在流行mybatisplus、mybatis等ORM框架,使用默认值插入更新到数据库时很方便插入或更新。真想砍掉之前的开发,重构后的项目中的实体类都是基本数据类型。现场破解...封装判断条件publicvoidmethod(LoanAppEntityloanAppEntity,longoperatorId){if(LoanAppEntity.LoanAppStatus.OVERDUE!=loanAppEntity.getStatus()&&LoanAppEntity.LoanAppStatus.CURRENT!=loanAppEntity.getStatus()&&LoanAppEntity.LoanAppStatus.GRACE_PERIOD!=loanAppEntity.getStatus()){//...return;}复制代码这段代码的可读性很差,谁知道这个if里面做了什么?如果我们用面向对象的思想在loanApp对象中封装一个方法不就行了吗?publicvoidmethod(LoanAppEntityloan,longoperatorId){if(!loan.finished()){//...return;}复制代码LoanApp在这个类中封装了一个方法,简单来说这个逻辑判断细节不应该出现在一种商业方法。/**贷款订单是否完成*/publicbooleanfinished(){returnLoanAppEntity.LoanAppStatus.OVERDUE!=this.getStatus()&&LoanAppEntity.LoanAppStatus.CURRENT!=this.getStatus()&&LoanAppEntity.LoanAppStatus.GRACE_PERIOD!=this.getStatus();}复制代码控制方法复杂度推荐一个IDEA插件CodeMetrics,它可以显示方法的复杂度,就是计算方法中的表达式,布尔表达式,if/else分支,循环,ETC。。点击查看哪些代码增加了方法的复杂度,可以适当参考。毕竟我们一般都是写业务代码的。最重要的是在保证正常工作的前提下,让别人快速理解。当你的方法的复杂度超过10时,你需要考虑是否可以优化。在使用@ConfigurationProperties而不是@Value之前,其实看到一篇文章推荐@Value优于@ConfigurationProperties。吐出来,别误会孩子。列出@ConfigurationProperties的好处。按住ctrl+鼠标左键,点击项目application.yml配置文件中的configuration属性,可以快速定位到配置类。也可以在编写配置时自动补全并关联注释。需要引入额外的依赖org.springframework.boot:spring-boot-configuration-processor。@ConfigurationProperties支持自动刷新NACOS配置。使用@Value需要在BEAN上使用@RefreshScope注解来实现自动刷新。@ConfigurationProperties可以与Validation检查、@NotNull、@Length和其他注解结合使用。如果配置检查失败,程序将不会启动,及早发现生产丢失配置等问题。@ConfigurationProperties可以注入多个属性,@Value只能一个一个写@ConfigurationProperties可以支持复杂类型,不管嵌套多少层,都可以正确映射成对象对比之下,不明白为什么那么多人不愿意接受新的东西,分裂。。。可以看看所有使用@ConfigurationProperties连接配置属性的springboot-starter。推荐使用lombok是一个没有实际意义的点,我的习惯是用它来保存getters、setters、toString等。不要在AService中调用BMapper。我们必须遵循AService->BService->BMapper的过程。如果每个Service都可以直接调用其他的Mappers,为什么还需要其他的Service呢?旧项目也从控制器调用映射器,并将控制器作为服务处理。..尽量少写工具类。为什么说要少写工具类呢?因为你写的大部分工具类都包含在你无形中导入的jar包中,比如String、Assert断言、IO上传文件、拷贝流、Bigdecimal等等。自己很容易出错,加载冗余类。不要包装OpenFeign接口的返回值。不明白为什么那么多人喜欢把接口的返回值用Response包起来。。。加一个code,message,success字段,然后每次调用就变成这样了CouponCommonResultbindResult=couponApi.useCoupon(request.getCustomerId(),order.getLoanId(),coupon.getCode());if(Objects.isNull(bindResult)||!bindResult.getResult()){thrownewAppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);}}复制代码相当于在coupon-api中抛出异常,在coupon-api中拦截异常,修改response.code,在调用方判断response.code。如果是FAIELD,那就抛异常。。。你直接在服务提供者抛异常不就好了吗?..而且这样打包的HTTP请求永远都是200,无法重试和监听。当然,这个问题涉及到如何设计接口响应体。目前,互联网上主要存在三种类型。Theinterfaceresponsestatusis200.接口响应状态跟随HTTP的真实状态。佛教发展,领导说到做到,不接受反驳。我建议使用HTTP标准。状态。针对包括参数校验失败等具体场景,使用400向前端弹出toast。下篇文章会讲解uniform200的缺点,不建议将OpenFeign接口打包成jar。我见过很多使用OpenFeign的接口都是这样使用的。在服务提供者上编写OpenFeign接口,打包成jar。比如服务A调用B时,在项目B中单独开一个模块写接口定义,创建jar包让A导入依赖。感受一下调用一个Feign接口实现的步骤:在服务B中编写Controller实现,在服务B中定义OpenFeign接口定义,在服务B中修改jar版本+1,打印一个jar包到本地仓库,在服务A中修改根据jar版本的不同,刷新maven/gradle,乍一看也不麻烦吧?但是你要知道,我们在开发中经常会丢失参数,缺少响应属性。一旦出现什么小问题,就要重新走一遍上面的流程。...建议在消费者A上定义OpenFeign接口,B只需要提供一个接口实现即可。不好的是XxxRequest和XxxResponse类是多余的,但其实是没有问题的,因为对于Feign来说,请求和响应的BO类不需要有完全相同的字段,它的decoder会智能解析Response和将其包装到您的XxxResponse接收类中。如果你这样理解,你就会明白,XxxRequest、XxxResponse等这个类只是你的A服务本地自定义的映射请求结果的映射数据结构。这个映射数据结构与B服务无关。所以你当然应该把A放在这里。你纠结是因为你觉得这个东西好像是可以复用的,所以你纠结是放A还是B,要不要把它作为publicdependency拉出来。很早以前就纠结过这个东西,后来踩了太多的坑,想法就变了。高内聚低耦合的本质是关联一个服务(组件、应用、包等)的所有代码。编写有意义的方法注释。写这种注释是怕接手的人瞎了么。。。你后面写的字段参数的意思。。。/**请求手机验证*@paramcredentialNum@paramcallback@paramparam@returnphoneVerifyResult*/publicvoidmethod(StringcredentialNum,Stringcallback,Stringparam,StringphoneVerifyResult){}复制代码要么不写,要么在后面加上说明……难不难受写这样的评论会被IDEA报一堆警告?与前端交互的DTO对象的名称是什么?VO,BO,DTO,PO我觉得真的没有必要细细的去细说。至少我们和前端交互的时候类名要合适,不要直接用映射数据库的类返回给前端,这样会返回很多不必要的信息,需要特殊处理如果有敏感信息。推荐的方法是定义接受前端请求的类为XxxRequest,响应的定义为XxxResponse。以订单为例:接受、保存和更新订单信息的实体类可以定义为OrderRequest,订单查询响应定义为OrderResponse,订单查询条件请求定义为OrderQueryRequest。不要跨服务循环数据库跨服务查询时,如果有批量数据查询场景,直接写一个批量Feign查询接口,不要像下面的list.foreach(id->{UserResponseuser=userClient.findById(ID);});复制代码因为OpenFeign的每一次请求都是一次Http请求,一次数据库IO操作,还需要经过各种框架中的拦截器,解码器等,都是损失。直接定义一个批量查询接口@PostMapping("/user/batch-info")List>partition=Lists.partition(userIds,500);//拆分ListList
