欢迎关注方志朋的博客,回复”666“获面试宝典

来源:https://blog.csdn.net/new_com/article/details/108399421

为什么要写好业务代码?

直接分享一段痛苦的项目维护经历吧,看大家有没有类似的经历。当时,我接手了一个维护项目,刚上班就接到新增一个显示字段的任务。我以为这应该是一个分分钟就能够搞定的小需求,没有想到这就开始了我的痛苦之旅。我梳理了关联的api后,发现每个api都是从controller控制层-》service-》服务层-dao数据层,甚至每个api都对应一个sql查询;但是,所有的api之间又有很大类似的代码。我开始阅读代码的时候,发现一个特殊的controller,在该controller里包括身份校验,参数校验,各种业务代码,各种if else,for循环语句(更多精彩内容欢迎关注微信公众号:肥朝),甚至dao层的逻辑都融到了一块。更让人悲痛欲绝的是项目没有文档,代码也几乎没注释,没有测试用例,我还是直接撸代码梳理业务,很多属性字段无法理解到底代表什么,例如,ajAmount,gjjAmount;在sql语句中写status in(1,2,4,6),case when,等很多魔法数条件判断。我最后直接抓包调用了一下api,然后,通过与页面的展示端字段匹配我才知道ajAmount,gjjAmount分别表示按揭贷款,公积金代码,status的部分字段是什么意思。这样的项目维护经历,你有没有类似的经历?

个人认为,只要我们做到api拒绝烟囱式开发,业务代码拒绝All in one,项目做好代码注释,就可以写出易阅读,好扩展的代码。

api如何拒绝烟囱式开发

上述的api开发开发过程就是典型的烟囱式开发模式,所有的api服务与相似业务,但是每个api都是完全独立的开发,其开发流程如图:

如上的开发流程有几个弊端,如下:

业务代码重复,在不同的service实现中,业务相似的话会有大量重复代码。数据库表结构的改动需要修改所有涉及到的dao层,维护成本比较高。此类相似业务,api层定义各自显示对象,dao层负责获取全量数据(例如,用户查询,就获取整个用户表字段的数据),service层定义业务对象,根据不同api不同业务类型的判断,根据dao查询的数据组转业务对象,以及业务对象向api显示对象的转换。开发流程如图:

这样的开发模式有如下优势:

1.业务代码集中在service层,专注业务对象bo的封装,以及业务对象向给类显示层vo的转换;封装复用逻辑,可以大量减少重复代码。如果,设计模式从一开始就设计得易扩展,后期维护就快捷的多。

2.数据库的改动只涉及到db层,能够快速的在各个业务响应。

业务代码如何拒绝All in one?

以上的controller代码最突出的缺点就是代码完全无法复用,完全没有使用到面向对象封装,集成,多态的特性。业务开发中,(更多精彩内容欢迎关注微信公众号:肥朝)一般都是权限校验,参数校验,业务判断,业务对象转换数据库操作。我的做法是业务抽象,把公共代码进行抽取,通过配置的形式的方式调用,使业务代码可以以可插拔的方式选择指定的权限校验,参数校验。简单来说,就是善用AOP面向切面编程的思想,示例如下:

  • 权限校验:使用aop对权限校验逻辑进行抽取,能够通过注解的方式指定哪些controller需要进行权限校验。对用户进行数据过滤时,使用controller的拦截器获取该用户拥有的各类权限,并把用户数据保存在上下文threadloal中,并且通过配置对指定url进行拦截。在业务层,从上下文拿到用户权限数据做各类数据业务过滤,通过aop实现各类拦截业务的指定调用。

  • 参数校验:使用java validtion对通用的字段,例如电话号码,身份证,进行扩展,详细可以参考,如何使用validation校验参数?,在项目中其他类似校验进行复用。

  • 业务判断:使用设计模式对不同类型的业务开发进行封装,集成,多态扩展;这样在后期的扩展中可以基于开发封闭原则,针对新的业务扩展子类即可。

  • 业务对象转换数:业务开发过程中,依照阿里巴巴研发规范的要求,存在DO(数据库表结构一致的对象),BO(业务对象),DTO(数据传输对象),VO(显示层对象),Query(查询对象)。使用MapStruct,可以灵活的控制的不同属性值之间的转换规格,比org.springframework.beans.BeanUtils.copyProperties()方法更加灵活。示例:

public interface CategoryConverter {CategoryConverter INSTANCE = Mappers.getMapper(CategoryConverter.class);@Mappings({@Mapping(target = "ext", expression = "java(getCategoryExt(updateCategoryDto.getStyle(),updateCategoryDto.getGoodsPageSize()))")})Category update2Category(UpdateCategoryDto updateCategoryDto);@Mappings({@Mapping(target = "ext", expression = "java(getCategoryExt(addCategoryDto.getStyle(),addCategoryDto.getGoodsPageSize()))")})Category add2Category(AddCategoryDto addCategoryDto);
}
  • DB数据库公共字段填充,例如,公共字段,生成日期,创建人,修改时间,修改人使用插件的形式进行封装,在mybatis-plus中使用MetaObjectHandler,在执行sql之前完成统一字段值的填充。

  • 业务平台字段查询过滤:在中台的开发中,数据采用不同平台code的列实现不同平台业务数据的隔离。基于mybatis插件机制的多租户过滤机制实现可以参考如何使用MyBatis的plugin插件实现多租户的数据过滤?。在dao层的方法或者接口上加上自定义过滤条件即可,示例如下:

@Mapper
@Repository
@MultiTenancy(multiTenancyQueryValueFactory = CustomerQueryValueFactory.class)
public interface ProductDao extends BaseMapper<Product> {}

缓存的使用:Spring开发中通常集成spring cache使用以注解的形式使用缓存。整合redis并且自定义默认时间设置可以参考(Spring Cache+redis自定义缓存过期时间)。示例如下:

/*** 使用CacheEvict注解更新指定key的缓存*/@Override@CacheEvict(value = {ALL_PRODUCT_KEY,ONLINE_PRODUCT_KEY}, allEntries = true)public Boolean add(ProductAddDto dto) {//   TODO 添加商品更新cache
}@Override
@Cacheable(value = {ALL_PRODUCT_KEY})
public List<ProductVo> findAllProductVo() {return this.baseMapper.selectList(null);
}@Override
@Cacheable(value = {ONLINE_PRODUCT_KEY})
public ProductVo getOnlineProductVo() {//   TODO 设置查询条件return this.baseMapper.selectList(query);
}

项目如何做好代码注释?

枚举类的使用:在业务中特别是状态的值,在对外发布api的vo对象中,加上状态枚举值的注释,并且使用@link 注解,可以直接连接到枚举类,让开发者一目了然。(更多精彩内容欢迎关注微信公众号:肥朝)示例如下:

public class ProductVo implements Serializable {   /*** 审核状态* {@link ProductStatus}*/@ApiModelProperty("状态")private Integer status;
}

迁移sql查询条件:避免在sql层写固定的通用的过滤条件,迁移到服务层做处理。示例如下:

// sql查询条件SELECT * from product
where status != -1 and shop_status != 6// 在业务层把各类状态值进行条件设置
public PageData<ProductVo> findCustPage(Query query ){// 产品上线,显示状态query.setStatus(ProductStatus.ONSHELF);// 产品显示状态query.setHideState(HideState.VISIBAL);// 店铺未下线query.setNotStatus(ShopStatus.OFFLINE);return   productService.findProductVoPage(query);
}

加分项的规范

乐观锁与悲观锁的使用

乐观锁(使用Spring AOP+注解基于CAS方式实现java的乐观锁)设置重试次数以及重试时间,在简单的对象属性修改使用乐观锁,示例如下:

@Transactional(rollbackFor = Exception.class)
@OptimisticRetry
public void updateGoods(GoodsUpdateDto dto) {Goods existGoods = this.getGoods(dto.getCode());// 属性逻辑判断 //if (0 == goodsDao.updateGoods(existGoods, dto)) {throw new OptimisticLockingFailureException("update goods optimistic locking failure!");}
}

悲观锁在业务场景比较复杂,关联关系比较多的情况下使用。例如修改SKU属性时,需要修改商品的价格,库存,分类,等等属性,这时可以对关联关系的聚合根产品进行加锁,代码如下:

@Transactional
public void updateProduct(Long id,ProductUpdateDto dto){Product existingProduct;// 根据产品id对数据加锁Assert.notNull(existingProduct = lockProduct(id), "无效的产品id!");// TODO 逻辑条件判断 // TODO 修改商品属性,名称,状态// TODO 修改价格// TODO 修改库存// TODO 修改商品规格
}

读写分离的使用

开发中,经常使用mybatisplus实现读写分离。常规的查询操作,就走从库查询,查询请求可以不加数据库事务,例如列表查询,示例如下:

@Override
@DS("slave_1")
public List<Product> findList(ProductQuery query) {QueryWrapper<Product> queryWrapper = this.buildQueryWrapper(query);return this.baseMapper.selectList(queryWrapper);
}

mybatisplus动态数据源默认是主库,(更多精彩内容欢迎关注微信公众号:肥朝)写操作为了保证数据一直性,需要加上事务控制。简单的操作可以直接加上@Transactional注解,如果写操作涉及到非必要的查询,或者使用到消息中间件,reids等第三方插件,可以使用声明式事务,避免查询或者第三方查询异常造成数据库长事务问题。示例,产品下线时,使用reids生成日志code,产品相关写操作执行完成后,发送消息,代码如下:

public void offlineProduct(OfflineProductDto dto){// TODO 修改操作为涉及到的查询操作// TODO 使用redis生成业务code// 使用声明式事务控制产品状态修改的相关数据库操作
boolean status = transactionTemplate.execute(new TransactionCallback<Boolean>() {@Nullable@Overridepublic Boolean doInTransaction(TransactionStatus status) {try {// TODO 更改产品状态} catch (Exception e) {status.setRollbackOnly();throw e;}return true;}
});// TODO 使用消息中间件发送消息}

数据库自动给容灾

结合配置中心,简单实现数据库的自动容灾。以nacous配置中心为例,如何使用Nacos实现数据库连接的自动切换?。在springboot启动类加上@EnableNacosDynamicDataSource配置注解,即可无侵入的实现数据库连接的动态切换,示例如下:

@EnableNacosDynamicDataSource
public class ProductApplication {public static void main(String[] args) {SpringApplication.run(ProductApplication.class, args);}}

测试用例的编写

基于TDD的原则,(更多精彩内容欢迎关注微信公众号:肥朝)结合junit和mockito实现服务功能的测试用例,为什么要写单元测试?基于junit如何写单元测试?。添加或者修改对象时,需要校验入参的有效性,并且校验操作以后的对象的各类属性。以添加类目的api测试用例为例,如下,添加类别,成功后,校验添加参数以及添加成功后的属性,以及其他默认字段例如状态,排序等字段,源码如下:

// 添加类别的测试用例@Test@Transactional@Rollbackpublic void success2addCategory() throws Exception {AddCategoryDto addCategoryDto = new AddCategoryDto();addCategoryDto.setName("服装");addCategoryDto.setLevel(1);addCategoryDto.setSort(1);Response<CategorySuccessVo> responseCategorySuccessVo = this.addCategory(addCategoryDto);CategorySuccessVo addParentCategorySuccessVo = responseCategorySuccessVo.getData();org.junit.Assert.assertNotNull(addParentCategorySuccessVo);org.junit.Assert.assertNotNull(addParentCategorySuccessVo.getId());org.junit.Assert.assertEquals(addParentCategorySuccessVo.getPid(), ROOT_PID);org.junit.Assert.assertEquals(addParentCategorySuccessVo.getStatus(), CategoryEnum.CATEGORY_STATUS_DOWN.getValue());org.junit.Assert.assertEquals(addParentCategorySuccessVo.getName(), addCategoryDto.getName());org.junit.Assert.assertEquals(addParentCategorySuccessVo.getLevel(), addCategoryDto.getLevel());org.junit.Assert.assertEquals(addParentCategorySuccessVo.getSort(), addCategoryDto.getSort());}// 新增类目,成功添加后,返回根据id查询CategorySuccessVopublic CategorySuccessVo add(AddCategoryDto addCategoryDto, UserContext userContext) {Category addingCategory = CategoryConverter.INSTANCE.add2Category(addCategoryDto);addingCategory.setStatus(CategoryEnum.CATEGORY_STATUS_DOWN.getValue());if (Objects.isNull(addCategoryDto.getLevel())) {addingCategory.setLevel(1);}if (Objects.isNull(addCategoryDto.getSort())) {addingCategory.setSort(100);}categoryDao.insert(addingCategory);return getCategorySuccessVo(addingCategory.getId());}

也需要对添加类目的参数进行校验,例如,名称不能重复的校验,示例如下:

// 添加类目的入参
public class AddCategoryDto implements Serializable {private static final long serialVersionUID = -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 = "类目名称", required = true)private String name;@ApiModelProperty(value = "类目层级")private Integer level;@ApiModelProperty(value = "排序")private Integer sort;}//添加失败的校验校验测试用例@Testpublic void fail2addCategory() throws Exception {AddCategoryDto addCategoryDto = new AddCategoryDto();addCategoryDto.setName("服装");addCategoryDto.setLevel(1);addCategoryDto.setSort(1);// 名称为空addCategoryDto.setName(null);Response<CategorySuccessVo> 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);}
热门内容:
  • 面试官:private修饰的方法可以通过反射访问,那么private的意义是什么?

  • 最新 955 不加班的公司名单(2022版)

  • SpringCloud 微服务架构,适合接私活(附源码)

  • 一款基于 Spring Boot 的现代化社区

  • Spring Boot 3.0 M1 发布,正式弃用 Java 8,最低要求 Java 17。。。

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

要写好业务代码,也没这么简单!相关推荐

  1. 阿里高级技术专家方法论:如何写复杂业务代码?

    阿里妹导读:张建飞是阿里巴巴高级技术专家,一直在致力于应用架构和代码复杂度的治理.最近,他在看零售通商品域的代码.面对零售通如此复杂的业务场景,如何在架构和代码层面进行应对,是一个新课题.结合实际的业 ...

  2. 资深技术专家推荐:如何写复杂业务代码-阿里实践

    一个复杂业务的处理过程 业务背景 简单的介绍下业务背景,零售通是给线下小店供货的B2B模式,我们希望通过数字化重构传统供应链渠道,提升供应链效率,为新零售助力.阿里在中间是一个平台角色,提供的是Bsb ...

  3. 写代码犹如写文章: “大师级程序员把系统当故事来讲,而不是当做程序来写” | 如何架构设计复杂业务系统? 如何写复杂业务代码?

    写代码犹如写文章: "大师级程序员把系统当故事来讲,而不是当做程序来写" | 如何架构设计复杂业务系统? 如何写复杂业务代码? Kotlin 开发者社区 "大师级程序员把 ...

  4. 这两年多我写PHP业务代码的方式是如何进化的

    曾今 谁都有过迷茫期,下面是我开始PHP开发中,不断改变的代码组织方式. 初期:所有代码一股脑控制器controller 曾今只是简单的理解MVC 中期:业务代码抽象一部分到模型层model 开始觉得 ...

  5. 关于程序员30/35岁以后就写不了代码(没前途)的问题。

    2010年,俺已经30了,俗话说得好"三十而立",我是既没成家也没立业.混的还是比较很惨的.只是我对写代码还是很感兴趣的.如今已经30了,长了不敢说,至少还能写两年的代码.就是说我 ...

  6. 程序员新手写实际业务代码思维混乱的一些建议

    很多新手系统全套学完就业班之后,其实能力还是不错的,对常用的技术栈也都会理解,也能看得懂别人的代码.模块.架构等,但是我发现有些新手一遇到业务就非常的茫然,好像一下子就不知道怎么去写了 1.对业务的理 ...

  7. 天天写业务代码的程序员,怎么成为技术大牛,开始写技术代码?

    作者:李运华 链接:https://www.zhihu.com/question/39430220/answer/81648584 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载 ...

  8. 如何写出一手好的业务代码?

    以下文章来源方志朋的博客,回复"666"获面试宝典 这里举一个非常简单的例子,以案例的业务实现来分析如何写好业务代码. ★ 本案例只是简单的模拟,可能与真实的情况有出入,这里只是为 ...

  9. 复杂业务代码要怎么写

    了解我的人都知道,我一直在致力于应用架构和代码复杂度的治理. 这两天在看零售通商品域的代码.面对零售通如此复杂的业务场景,如何在架构和代码层面进行应对,是一个新课题.针对该命题,我进行了比较细致的思考 ...

最新文章

  1. JDK与JRE的关系和path的作用浅谈
  2. 哪些操作会造成raid数据无法恢复?raid多块硬盘离线后切记不要做这些事情
  3. C#可用的日出日落时间类
  4. android动画送礼物,Android仿直播类app赠送礼物功能
  5. C++实现链式存储二叉树
  6. redis.conf配置文件详解
  7. 在新项目中使用 Vue3 使用总结
  8. bcdedit添加linux引导,Linux_利用Bcdedit创建Linux系统引导,Bcdedit在Windows Vista中的一个命 - phpStudy...
  9. python invalid literal for int_求助!运行出现错误“ValueError: invalid literal for int() ··...
  10. Unity Occlusion Culling 遮挡剔除研究
  11. 19_完成“我的订单”
  12. matlab中inv a,设A为矩阵,b为列向量,则Matlab中运算A\b 和运算inv(A)*b
  13. cad电气工程量计算机,CAD电气管线快速算量方法
  14. 交换机常用配置命令(华为)
  15. 【自定义WPS插件xlam】
  16. Hcse 交换知识点-3
  17. 声音场景分类问题探讨
  18. 06.Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用
  19. Spring Boot之Spring-Data-JPA
  20. java计算机毕业设计小区物业管理系统录像展示.mp4源程序+mysql+系统+lw文档+远程调试

热门文章

  1. 2018-3-19李宏毅机器学习笔记八--HomeWork1-PM2.5 Predicition
  2. 设计模式的征途—10.装饰(Decorator)模式
  3. [ZJOI2018]历史
  4. 常见的函数式编程模型
  5. 洛谷 P2126 Mzc家中的男家丁
  6. MAC OS X El CAPITAN 搭建SPRING MVC (1)- 目录、包名、创建web.xml
  7. pat1004. Counting Leaves (30)
  8. 【青少年编程】【四级】奇偶之和
  9. 【青少年编程】【三级】小鸡吃虫
  10. 【直播】陈安东,但扬:CNN模型搭建、训练以及LSTM模型思路详解