当前位置: 首页 > 后端技术 > Java

如何写出好的Java业务代码?这也是一大堆规范,

时间:2023-04-01 19:59:28 Java

为什么要写好业务代码?直接分享一个痛苦的项目维护经历,看看大家有没有类似的经历。当时接手了一个维护项目,一上班就接到了添加显示字段的任务。本以为这应该是分分钟搞定的小需求,没想到就这样开始了我的痛苦之旅。整理完关联的api,发现每一个api都是从controller控制层-》service-》服务层-dao数据层,甚至每一个api都对应一个sql查询。但是,所有API之间有很多相似的代码。开始看代码的时候,发现了一个特殊的controller,里面有身份验证,参数验证,各种业务代码,各种ifelse,for循环语句,甚至还有dao层的逻辑。更让人苦恼的是,项目没有文档,代码几乎没有注释,更没有测试用例。还是直接review代码梳理业务。很多属性字段无法理解它们代表什么,例如ajAmount,gjjAmount;在sql语句中写statusin(1,2,4,6),casewhen,等等很多magicnumber条件判断。最后我直接抓包调用api,然后通过页面显示的字段匹配,知道了ajAmount和gjjAmount分别代表的是什么房贷,公积金码,状态的一些字段.这样的项目维护经历,你有没有类似的经历?个人认为,API只要拒绝烟囱式开发,业务代码拒绝Allinone,项目做代码注释,就能写出易读、可扩展的代码。API如何拒绝烟囱式开发上述API的开发过程就是典型的烟囱式开发模式。所有API服务和类似的业务,但每个API都是完全独立开发的。开发流程如图所示:以上开发流程有以下几个缺点:业务代码重复,在不同的服务实现中,如果业务相似,就会有很多重复的代码。数据库表结构的改变需要修改所有涉及到的dao层,维护成本比较高。对于此类类似的业务,api层定义了各自的展示对象,dao层负责获取全量数据(比如用户查询时,获取整个用户表字段的数据),service层定义业务对象,根据不同api和不同业务类型的判断,根据dao查询的数据组传递给业务对象,将业务对象转换为api展示对象。开发流程如图所示:这种开发模式有以下优点:业务代码集中在服务层,着重于业务对象bo的封装,以及业务对象到类展示层vo的转换;封装和重用逻辑可以大大减少重复代码。如果设计模式一开始就设计成易于扩展,那么后期的维护就会快很多。数据库的变更只涉及到db层,可以快速响应各个业务。业务代码如何拒绝Allinone?上述controller代码最突出的缺点就是代码根本无法复用,根本没有利用到面向对象的封装、集成、多态等特性。在业务开发中,一般是权限校验、参数校验、业务判断、业务对象转换数据库操作。我的做法是对业务进行抽象,将公共代码抽取出来,以配置的形式调用,让业务代码以可插拔的方式选择指定的权限校验和参数校验。简单来说,就是利用好AOP的思想进行面向切面的编程。示例如下:权限验证:使用aop提取权限验证逻辑,通过注解指定哪些controller需要进行权限验证。在为用户过滤数据时,使用controller的拦截器获取用户拥有的各种权限,将用户数据保存在contextthreadloal中,通过配置拦截指定的url。在业务层,从上下文中获取用户权限数据,过滤各种数据服务,通过aop实现各种拦截服务的指定调用。参数校验:使用java校验扩展常用字段,如电话号码、身份证等。详情请参考如何使用validation校验参数?,在项目中的其他类似检查中重复使用。业务判断:利用设计模式对不同类型的业务开发进行封装、集成、多态扩展;这样在后期的扩展中,基于开发封闭的原则,可以针对新的业务扩展子类。业务对象转换数:在业务开发过程中,根据阿里巴巴研发规范的要求,有DO(数据库表结构一致的对象)、BO(业务对象)、DTO(数据传输对象)、VO(展示层)对象)和Query(查询对象)。使用MapStruct可以灵活控制不同属性值之间的转换规范,比org.springframework.beans.BeanUtils.copyProperties()方法更加灵活。参考这篇文章:https://www.javastack.cn/arti...@Mappings({@Mapping(target="ext",expression="java(getCategoryExt(updateCategoryDto.getStyle(),updateCategoryDto.getGoodsPageSize()))")})分类update2Category(UpdateCategoryDtoupdateCategoryDto);@Mappings({@Mapping(target="ext",expression="java(getCategoryExt(addCategoryDto.getStyle(),addCategoryDto.getGoodsPageSize())")})Categoryadd2Category(AddCategoryDtoaddCategoryDto);}DB数据库公共字段填写:例如公共字段、生成日期、创建者、修改时间、修改人使用插件以mybatis-plus的形式封装,在mybatis-plus中使用MetaObjectHandler,在执行sql之前完成统一字段值的填充。业务平台字段查询过滤:在中台开发中,数据采用不同平台编码的列,实现不同平台业务数据的隔离。基于mybatis插件实现多租户过滤机制in机制可以参考如何使用MyBatis的plugin插件实现多租户数据过滤?。只需要在dao层的方法或者接口中添加一个自定义的过滤条件即可。示例如下:@Mapper@Repository@MultiTenancy(multiTenancyQueryValueFactory=CustomerQueryValueFactory.class)publicinterfaceProductDaoextendsBaseMapper{}缓存的使用:Spring开发通常集成的springcache使用注解形式的缓存。集成redis和自定义默认时间设置可以参考(SpringCache+redis自定义缓存过期时间)。示例如下:/***使用CacheEvict注解更新指定key的缓存*/@Override@CacheEvict(value={ALL_PRODUCT_KEY,ONLINE_PRODUCT_KEY},allEntries=true)publicBooleanadd(ProductAddDtodto){//TODO添加产品更新缓存}@Override@Cacheable(value={ALL_PRODUCT_KEY})publicListfindAllProductVo(){returnthis.baseMapper.selectList(null);}@Override@Cacheable(value={ONLINE_PRODUCT_KEY})publicProductVogetOnlineProductVo(){//TODO设置查询条件returnthis.baseMapper.selectList(query);}如何给项目做代码注释?枚举类的使用:在业务中,尤其是状态的值,在外部API的vo对象中,添加状态枚举值的注解,使用@link注解直接连接枚举类,允许开发者一目了然。示例如下:publicclassProductVoimplementsSerializable{/***auditstatus*{@linkProductStatus}*/@ApiModelProperty("status")privateIntegerstatus;}迁移sql查询条件:避免在sql层条件,迁移到service层处理。示例如下://sql查询条件SELECT*fromproductwherestatus!=-1andshop_status!=6//在业务层有条件地设置各种状态值publicPageDatafindCustPage(Queryquery){//商品上线,显示状态query.setStatus(ProductStatus.ONSHELF);//商品显示状态query.setHideState(HideState.VISIBAL);//店铺未离线query.setNotStatus(ShopStatus.OFFLINE);returnproductService.findProductVoPage(query);}奖励项标准化乐观锁和悲观锁使用乐观锁(使用SpringAOP+注解基于CAS方式实现java乐观锁)设置重试次数和重试时间,在simple中使用乐观锁对象属性修改,示例如下:@Transactional(rollbackFor=Exception.class)@OptimisticRetrypublicvoidupdateGoods(GoodsUpdateDtodto){//属性逻辑判断//if(0==goodsDao.updateGoods(existGoods,dto)){thrownewOptimisticLockingFailureException("更新商品乐观锁失败!");}}当业务场景复杂,关系较多时使用悲观锁。比如修改SKU属性时,需要修改商品的价格、库存、分类等属性。这时候可以锁定关联关系的聚合根积。代码如下:@TransactionalpublicvoidupdateProduct(Longid,ProductUpdateDtodto){ProductexistingProduct;//根据商品id锁定数据Assert.notNull(existingProduct=lockProduct(id),"Invalidproductid!");//TODO逻辑条件判断//TODO修改商品属性、名称、状态//TODO修改价格//TODO修改库存//TODO修改商品规格}在读写分离的使用和开发中,经常使用mybatisplus来实现读写分离。对于常规的查询操作,只使用从库查询,查询请求不能添加数据库事务,比如列表查询,示例如下:@Override@DS("slave_1")publicListfindList(ProductQueryquery){QueryWrapperqueryWrapper=this.buildQueryWrapper(查询);返回this.baseMapper.selectList(queryWrapper);}mybatisplus动态数据源默认为主库,写入操作需要事务控制,保证数据一致性。简单的操作可以直接加上@Transactional注解。如果写操作涉及到不必要的查询,或者使用了消息中间件、reids等第三方插件,可以使用声明式事务,避免查询或者第三方查询异常导致长时间数据库业务问题。例如产品下线时,使用reids生成日志代码,产品相关写操作完成后发送消息,代码如下:publicvoidofflineProduct(OfflineProductDtodto){//TODO修改操作到相关查询操作//TODO使用Redis生成业务代码//使用声明式事务控制与产品状态修改相关的数据库操作booleanstatus=transactionTemplate.execute(newTransactionCallback(){@Nullable@OverridepublicBooleandoInTransaction(TransactionStatusstatus){try{//TODO更改产品状态}catch(Exceptione){status.setRollbackOnly();throwe;}returntrue;}});//TODO使用消息中间件发送消息}数据库自动发送到容灾集成配置中心,简单实现数据库自动容灾。以nacous配置中心为例,如何使用Nacos实现数据库连接的自动切换?.在springboot启动类中添加@EnableNacosDynamicDataSource配置注解,实现无侵入的动态切换数据库连接。示例如下:推荐一个SpringBoot基础教程和实例:https://github.com/javastacks...@EnableNacosDynamicDataSourcepublicclassProductApplication{publicstaticvoidmain(String[]args){SpringApplication.run(ProductApplication.类,参数);}}测试用例的编写是基于TDD的原理,结合junit和mockito实现服务功能的测试用例,为什么要写单元测试呢?如何基于junit编写单元测试?.添加或修改对象时,需要验证输入参数的有效性,并在操作后验证对象的各种属性。以添加分类的api测试用例为例,如下,添加分类后,验证添加成功后添加的参数和属性,以及状态、排序等其他默认字段,源码为如下://添加类别测试用例@Test@Transactional@Rollbackpublicvoidsuccess2addCategory()throwsException{AddCategoryDtoaddCategoryDto=newAddCategoryDto();addCategoryDto.setName("服装");addCategoryDto.setLevel(1);=this.addCategory(addCategoryDto);CategorySuccessVoaddParentCategorySuccessVo=responseCategorySuccessVo.getData();org.junit.Assert.assertNotNull(addParentCategorySuccessVo);org.junit.Assert.assertNotNull(addParentCategorySuccessVo.getId());org.jasserts(addParentCategorySuccessVo.getPid(),ROOT_PID);org.junit.Assert.assertEquals(addParentCategorySuccessVo.getStatus(),CategoryEnum.CATEGORY_STATUS_DOWN.getValue());org.junit.Assert.assertEquals(addParentCategorySuccessVo.getNamegory(),toaddCate());org.junit.Assert.assertEquals(addParentCategorySuccessVo.getLevel(),addCategoryDto.getLevel());org.addingCategory=CategoryConverter.INSTANCE.add2Category(addCategoryDto);addingCategory.setStatus(CategoryEnum.CATEGORY_STATUS_DOWN.getValue());如果(Objects.isNull(addCategoryDto.getLevel())){addingCategory.setLevel(1);}if(Objects.isNull(addCategoryDto.getSort())){addingCategory.setSort(100);}categoryDao.insert(addingCategory);returngetCategorySuccessVo(addingCategory.getId());}还需要验证添加分类的参数,比如名称不能重复,示例如下://添加分类公共类的入参AddCategoryDtoimplementsSerializable{privatestaticfinallongserialVersionUID=-4752897765723264858L;//名字不能为空,名字不能重复@NotEmpty(message=CATEGORY_NAME_IS_EMPTY,groups={ValidateGroup.First.class})@EffectiveValue(shouldBeNull=true,message=CATEGORY_NAME_IS_DUPLICATE,serviceBean=NameOfCategoryForAddValidator.class,groups={ValidateGroup.Second.class})@ApiModelProperty(value=“类别名称”,必需=true)privateStringname;@ApiModelProperty(value="categorylevel")privateIntegerlevel;@ApiModelProperty(value="sort")privateIntegersort;}//添加验证失败验证测试用例@Testpublicvoidfail2addCategory()throws异常{AddCategoryDtoaddCategoryDto=newAddCategoryDto();addCategoryDto.setName("服装");addCategoryDto.setLevel(1);addCategoryDto.setSort(1);//名称为空addCategoryDto.setName(null);响应errorResponse=this.addCategory(addCategoryDto);org.junit.Assert.assertNotNull(errorResponse);org.junit.Assert.assertNotNull(errorResponse.getMsg(),CATEGORY_NAME_IS_EMPTY);addCategoryDto.setName(//成功添加类别this.addCategory(addCategoryDto);//名称重复errorResponse=this.addCategory(addCategoryDto);org.junit.Assert.assertNotNull(errorResponse);org.junit.Assert.assertNotNull(errorResponse.getMsg(),CATEGORY_NAME_IS_DUPLICATE);}