如何写好 Java 业务代码?这也是有很多规范的!

为什么要写好业务代码?

api如何拒绝烟囱式开发

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

加分项的规范

为什么要写好业务代码?

直接分享一段痛苦的项目维护经历吧,看大家有没有类似的经历。当时,我接手了一个维护项目,刚上班就接到新增一个显示字段的任务。我以为这应该是一个分分钟就能够搞定的小需求,没有想到这就开始了我的痛苦之旅。我梳理了关联的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显示对象的转换。

开发流程如图:

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

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

数据库的改动只涉及到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()方法更加灵活。

示例:

publicinterface 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)

publicinterfaceProductDaoextendsBaseMapper

{

}

缓存的使用:

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

示例如下:

/**

* 使用CacheEvict注解更新指定key的缓存

*/

@Override

@CacheEvict(value = {ALL_PRODUCT_KEY,ONLINE_PRODUCT_KEY}, allEntries =true)

publicBooleanadd(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 注解,可以直接连接到枚举类,让开发者一目了然。

示例如下:

publicclass ProductVo implements Serializable {/**

    *审核状态

* {@link ProductStatus}

    */

@ApiModelProperty("状态")

privateInteger status;

}

迁移sql查询条件:

避免在sql层写固定的通用的过滤条件,迁移到服务层做处理。

示例如下:

//sql查询条件

SELECT * from product

where status != -1and shop_status !=6

//在业务层把各类状态值进行条件设置

publicPageData findCustPage(Query query ){

//产品上线,显示状态

query.setStatus(ProductStatus.ONSHELF);

//产品显示状态

query.setHideState(HideState.VISIBAL);

//店铺未下线

query.setNotStatus(ShopStatus.OFFLINE);

returnproductService.findProductVoPage(query);

}

加分项的规范

乐观锁与悲观锁的使用

阿里的《Java开发手册》建议看下。乐观锁(使用Spring AOP+注解基于CAS方式实现java的乐观锁)设置重试次数以及重试时间,在简单的对象属性修改使用乐观锁,示例如下:

@Transactional(rollbackFor = Exception.class)

@OptimisticRetry

publicvoidupdateGoods(GoodsUpdateDtodto)

{

Goods existGoods =this.getGoods(dto.getCode());

// 属性逻辑判断 //

if(0== goodsDao.updateGoods(existGoods, dto)) {

thrownewOptimisticLockingFailureException("update goods optimistic locking failure!");

}

}

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

@Transactional

publicvoidupdateProduct(Long id,ProductUpdateDto dto){

Product existingProduct;

//根据产品id对数据加锁

Assert.notNull(existingProduct = lockProduct(id),"无效的产品id!");

//TODO 逻辑条件判断

// TODO 修改商品属性,名称,状态


    // TODO 修改价格


    // TODO 修改库存


    // TODO 修改商品规格

}

读写分离的使用

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

@Override

@DS("slave_1")

publicListfindList(ProductQuery query){

QueryWrapper queryWrapper =this.buildQueryWrapper(query);

returnthis.baseMapper.selectList(queryWrapper);

}

mybatisplus动态数据源默认是主库,写操作为了保证数据一直性,需要加上事务控制。简单的操作可以直接加上@Transactional注解,如果写操作涉及到非必要的查询,或者使用到消息中间件,reids等第三方插件,可以使用声明式事务,避免查询或者第三方查询异常造成数据库长事务问题。

示例,产品下线时,使用reids生成日志code,产品相关写操作执行完成后,发送消息,代码如下:

publicvoidofflineProduct(OfflineProductDto dto){

//TODO 修改操作为涉及到的查询操作

//TODO 使用redis生成业务code

//使用声明式事务控制产品状态修改的相关数据库操作

booleanstatus = transactionTemplate.execute(newTransactionCallback() {

@Nullable

@Override

publicBooleandoInTransaction(TransactionStatus status){

try{

//TODO 更改产品状态

}catch(Exception e) {

status.setRollbackOnly();

throwe;

}

returntrue;

}

});

//TODO 使用消息中间件发送消息

}

数据库自动给容灾

结合配置中心,简单实现数据库的自动容灾。以nacous配置中心为例,如何使用Nacos实现数据库连接的自动切换?

在springboot启动类加上@EnableNacosDynamicDataSource配置注解,即可无侵入的实现数据库连接的动态切换,示例如下:

@EnableNacosDynamicDataSource

publicclass ProductApplication {

publicstaticvoidmain(String[] args){

SpringApplication.run(ProductApplication.class,args);

}

}

测试用例的编写

基于TDD的原则,结合junit和mockito实现服务功能的测试用例,为什么要写单元测试?基于junit如何写单元测试?。添加或者修改对象时,需要校验入参的有效性,并且校验操作以后的对象的各类属性。

以添加类目的api测试用例为例,如下,添加类别,成功后,校验添加参数以及添加成功后的属性,以及其他默认字段例如状态,排序等字段,源码如下:

// 添加类别的测试用例

@Test

@Transactional

@Rollback

public 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查询CategorySuccessVo

publicCategorySuccessVoadd(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);

returngetCategorySuccessVo(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)

privateString name;

@ApiModelProperty(value ="类目层级")

privateInteger level;

@ApiModelProperty(value ="排序")

privateInteger sort;

}

//添加失败的校验校验测试用例

@Test

public void fail2addCategory() throws Exception {

    AddCategoryDto addCategoryDto = new AddCategoryDto();

    addCategoryDto.setName("服装");

    addCategoryDto.setLevel(1);

    addCategoryDto.setSort(1);

    //名称为空

addCategoryDto.setName(null);

Response 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);

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • [TOC] 代码整洁之道-理论 前言 学习中、工作中遇到很多乱七八糟的糟糕代码,自己入门时也写过不少糟糕代码。在一...
    丁永辉Dave阅读 325评论 0 1
  • 一、Java语言规范 详见:Android开发java编写规范 二、Android资源文件命名与使用 1. 【推荐...
    王朋6阅读 963评论 0 0
  • 前言 断言assert是测试框架的重要组成部分。本篇介绍断言的各种类型,结合测试框架介绍3种断言工具。//TODO...
    antony已经被占用阅读 2,797评论 0 1
  • 一. Java基础部分.................................................
    wy_sure阅读 3,810评论 0 11
  • 文档版本说明v1.0 基础版本v1.1 补充规范,增加规范等级 一、代码流程规范 (一) java代码处理 【强制...
    tonyZj阅读 964评论 0 2