<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何寫好 Java 業(yè)務(wù)代碼?這也是有很多規(guī)范的!

          共 14386字,需瀏覽 29分鐘

           ·

          2022-01-23 14:52

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          為什么要寫好業(yè)務(wù)代碼?

          直接分享一段痛苦的項(xiàng)目維護(hù)經(jīng)歷吧,看大家有沒(méi)有類似的經(jīng)歷。當(dāng)時(shí),我接手了一個(gè)維護(hù)項(xiàng)目,剛上班就接到新增一個(gè)顯示字段的任務(wù)。我以為這應(yīng)該是一個(gè)分分鐘就能夠搞定的小需求,沒(méi)有想到這就開始了我的痛苦之旅。我梳理了關(guān)聯(lián)的api后,發(fā)現(xiàn)每個(gè)api都是從controller控制層-》service-》服務(wù)層-dao數(shù)據(jù)層,甚至每個(gè)api都對(duì)應(yīng)一個(gè)sql查詢。

          但是,所有的api之間又有很大類似的代碼。我開始閱讀代碼的時(shí)候,發(fā)現(xiàn)一個(gè)特殊的controller,在該controller里包括身份校驗(yàn),參數(shù)校驗(yàn),各種業(yè)務(wù)代碼,各種if else,for循環(huán)語(yǔ)句,甚至dao層的邏輯都融到了一塊。

          更讓人悲痛欲絕的是項(xiàng)目沒(méi)有文檔,代碼也幾乎沒(méi)注釋,沒(méi)有測(cè)試用例,我還是直接擼代碼梳理業(yè)務(wù),很多屬性字段無(wú)法理解到底代表什么,例如,ajAmount,gjjAmount;在sql語(yǔ)句中寫status in(1,2,4,6),case when,等很多魔法數(shù)條件判斷。

          我最后直接抓包調(diào)用了一下api,然后,通過(guò)與頁(yè)面的展示端字段匹配我才知道ajAmount,gjjAmount分別表示按揭貸款,公積金代碼,status的部分字段是什么意思。這樣的項(xiàng)目維護(hù)經(jīng)歷,你有沒(méi)有類似的經(jīng)歷?

          個(gè)人認(rèn)為,只要我們做到api拒絕煙囪式開發(fā),業(yè)務(wù)代碼拒絕All in one,項(xiàng)目做好代碼注釋,就可以寫出易閱讀,好擴(kuò)展的代碼。

          api如何拒絕煙囪式開發(fā)

          上述的api開發(fā)開發(fā)過(guò)程就是典型的煙囪式開發(fā)模式,所有的api服務(wù)與相似業(yè)務(wù),但是每個(gè)api都是完全獨(dú)立的開發(fā),其開發(fā)流程如圖:

          如上的開發(fā)流程有幾個(gè)弊端,如下:

          業(yè)務(wù)代碼重復(fù),在不同的service實(shí)現(xiàn)中,業(yè)務(wù)相似的話會(huì)有大量重復(fù)代碼。

          數(shù)據(jù)庫(kù)表結(jié)構(gòu)的改動(dòng)需要修改所有涉及到的dao層,維護(hù)成本比較高。

          此類相似業(yè)務(wù),api層定義各自顯示對(duì)象,dao層負(fù)責(zé)獲取全量數(shù)據(jù)(例如,用戶查詢,就獲取整個(gè)用戶表字段的數(shù)據(jù)),service層定義業(yè)務(wù)對(duì)象,根據(jù)不同api不同業(yè)務(wù)類型的判斷,根據(jù)dao查詢的數(shù)據(jù)組轉(zhuǎn)業(yè)務(wù)對(duì)象,以及業(yè)務(wù)對(duì)象向api顯示對(duì)象的轉(zhuǎn)換。

          開發(fā)流程如圖:

          這樣的開發(fā)模式有如下優(yōu)勢(shì):

          業(yè)務(wù)代碼集中在service層,專注業(yè)務(wù)對(duì)象bo的封裝,以及業(yè)務(wù)對(duì)象向給類顯示層vo的轉(zhuǎn)換;封裝復(fù)用邏輯,可以大量減少重復(fù)代碼。如果,設(shè)計(jì)模式從一開始就設(shè)計(jì)得易擴(kuò)展,后期維護(hù)就快捷的多。

          數(shù)據(jù)庫(kù)的改動(dòng)只涉及到db層,能夠快速的在各個(gè)業(yè)務(wù)響應(yīng)。

          業(yè)務(wù)代碼如何拒絕All in one?

          以上的controller代碼最突出的缺點(diǎn)就是代碼完全無(wú)法復(fù)用,完全沒(méi)有使用到面向?qū)ο蠓庋b,集成,多態(tài)的特性。業(yè)務(wù)開發(fā)中,一般都是權(quán)限校驗(yàn),參數(shù)校驗(yàn),業(yè)務(wù)判斷,業(yè)務(wù)對(duì)象轉(zhuǎn)換數(shù)據(jù)庫(kù)操作。

          我的做法是業(yè)務(wù)抽象,把公共代碼進(jìn)行抽取,通過(guò)配置的形式的方式調(diào)用,使業(yè)務(wù)代碼可以以可插拔的方式選擇指定的權(quán)限校驗(yàn),參數(shù)校驗(yàn)。簡(jiǎn)單來(lái)說(shuō),就是善用AOP面向切面編程的思想,示例如下:

          權(quán)限校驗(yàn):

          使用aop對(duì)權(quán)限校驗(yàn)邏輯進(jìn)行抽取,能夠通過(guò)注解的方式指定哪些controller需要進(jìn)行權(quán)限校驗(yàn)。對(duì)用戶進(jìn)行數(shù)據(jù)過(guò)濾時(shí),使用controller的攔截器獲取該用戶擁有的各類權(quán)限,并把用戶數(shù)據(jù)保存在上下文threadloal中,并且通過(guò)配置對(duì)指定url進(jìn)行攔截。在業(yè)務(wù)層,從上下文拿到用戶權(quán)限數(shù)據(jù)做各類數(shù)據(jù)業(yè)務(wù)過(guò)濾,通過(guò)aop實(shí)現(xiàn)各類攔截業(yè)務(wù)的指定調(diào)用。

          參數(shù)校驗(yàn):

          使用java validtion對(duì)通用的字段,例如電話號(hào)碼,身份證,進(jìn)行擴(kuò)展,詳細(xì)可以參考,如何使用validation校驗(yàn)參數(shù)?,在項(xiàng)目中其他類似校驗(yàn)進(jìn)行復(fù)用。最新面試題整理好了,點(diǎn)擊Java面試庫(kù)小程序在線刷題。

          業(yè)務(wù)判斷:使用設(shè)計(jì)模式對(duì)不同類型的業(yè)務(wù)開發(fā)進(jìn)行封裝,集成,多態(tài)擴(kuò)展;這樣在后期的擴(kuò)展中可以基于開發(fā)封閉原則,針對(duì)新的業(yè)務(wù)擴(kuò)展子類即可。

          業(yè)務(wù)對(duì)象轉(zhuǎn)換數(shù):

          業(yè)務(wù)開發(fā)過(guò)程中,依照阿里巴巴研發(fā)規(guī)范的要求,存在DO(數(shù)據(jù)庫(kù)表結(jié)構(gòu)一致的對(duì)象),BO(業(yè)務(wù)對(duì)象),DTO(數(shù)據(jù)傳輸對(duì)象),VO(顯示層對(duì)象),Query(查詢對(duì)象)。

          使用MapStruct,可以靈活的控制的不同屬性值之間的轉(zhuǎn)換規(guī)格,比org.springframework.beans.BeanUtils.copyProperties()方法更加靈活。

          參考這篇文章:

          https://www.javastack.cn/article/2021/maptruct-advanced-useages/

          示例:

          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數(shù)據(jù)庫(kù)公共字段填充:

          例如,公共字段,生成日期,創(chuàng)建人,修改時(shí)間,修改人使用插件的形式進(jìn)行封裝,在mybatis-plus中使用MetaObjectHandler,在執(zhí)行sql之前完成統(tǒng)一字段值的填充。

          業(yè)務(wù)平臺(tái)字段查詢過(guò)濾:

          在中臺(tái)的開發(fā)中,數(shù)據(jù)采用不同平臺(tái)code的列實(shí)現(xiàn)不同平臺(tái)業(yè)務(wù)數(shù)據(jù)的隔離。基于mybatis插件機(jī)制的多租戶過(guò)濾機(jī)制實(shí)現(xiàn)可以參考如何使用MyBatis的plugin插件實(shí)現(xiàn)多租戶的數(shù)據(jù)過(guò)濾?。

          在dao層的方法或者接口上加上自定義過(guò)濾條件即可,示例如下:

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

          }

          緩存的使用:

          Spring開發(fā)中通常集成spring cache使用以注解的形式使用緩存。整合redis并且自定義默認(rèn)時(shí)間設(shè)置可以參考(Spring Cache+redis自定義緩存過(guò)期時(shí)間)。

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          示例如下:

          /**
          * 使用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 設(shè)置查詢條件
              return this.baseMapper.selectList(query);
          }

          項(xiàng)目如何做好代碼注釋?

          枚舉類的使用:

          在業(yè)務(wù)中特別是狀態(tài)的值,在對(duì)外發(fā)布api的vo對(duì)象中,加上狀態(tài)枚舉值的注釋,并且使用@link 注解,可以直接連接到枚舉類,讓開發(fā)者一目了然。

          示例如下:

          public class ProductVo implements Serializable {   /**
               * 審核狀態(tài)
               * {@link ProductStatus}
               */
              @ApiModelProperty("狀態(tài)")
              private Integer status;
          }

          遷移sql查詢條件:

          避免在sql層寫固定的通用的過(guò)濾條件,遷移到服務(wù)層做處理。

          示例如下:

          // sql查詢條件

          SELECT * from product
          where status != -1 and shop_status != 6


          // 在業(yè)務(wù)層把各類狀態(tài)值進(jìn)行條件設(shè)置
          public PageData<ProductVo> findCustPage(Query query ){

                 // 產(chǎn)品上線,顯示狀態(tài)
                 query.setStatus(ProductStatus.ONSHELF);
                 // 產(chǎn)品顯示狀態(tài)
                 query.setHideState(HideState.VISIBAL);
                 // 店鋪未下線
                 query.setNotStatus(ShopStatus.OFFLINE);
              return   productService.findProductVoPage(query);
          }   

          加分項(xiàng)的規(guī)范

          樂(lè)觀鎖與悲觀鎖的使用

          阿里的《Java開發(fā)手冊(cè)》建議看下。樂(lè)觀鎖(使用Spring AOP+注解基于CAS方式實(shí)現(xiàn)java的樂(lè)觀鎖)設(shè)置重試次數(shù)以及重試時(shí)間,在簡(jiǎn)單的對(duì)象屬性修改使用樂(lè)觀鎖,示例如下:

          @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!");
              }
          }

          悲觀鎖在業(yè)務(wù)場(chǎng)景比較復(fù)雜,關(guān)聯(lián)關(guān)系比較多的情況下使用。例如修改SKU屬性時(shí),需要修改商品的價(jià)格,庫(kù)存,分類,等等屬性,這時(shí)可以對(duì)關(guān)聯(lián)關(guān)系的聚合根產(chǎn)品進(jìn)行加鎖,代碼如下:

          @Transactional
          public void updateProduct(Long id,ProductUpdateDto dto){

              Product existingProduct;
              // 根據(jù)產(chǎn)品id對(duì)數(shù)據(jù)加鎖
              Assert.notNull(existingProduct = lockProduct(id), "無(wú)效的產(chǎn)品id!");


              
              // TODO 邏輯條件判斷 
               
              // TODO 修改商品屬性,名稱,狀態(tài)
                   
              // TODO 修改價(jià)格
               
              // TODO 修改庫(kù)存
               
              // TODO 修改商品規(guī)格
          }

          讀寫分離的使用

          開發(fā)中,經(jīng)常使用mybatisplus實(shí)現(xiàn)讀寫分離。常規(guī)的查詢操作,就走從庫(kù)查詢,查詢請(qǐng)求可以不加數(shù)據(jù)庫(kù)事務(wù),例如列表查詢,示例如下:

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

          mybatisplus動(dòng)態(tài)數(shù)據(jù)源默認(rèn)是主庫(kù),寫操作為了保證數(shù)據(jù)一直性,需要加上事務(wù)控制。簡(jiǎn)單的操作可以直接加上@Transactional注解,如果寫操作涉及到非必要的查詢,或者使用到消息中間件,reids等第三方插件,可以使用聲明式事務(wù),避免查詢或者第三方查詢異常造成數(shù)據(jù)庫(kù)長(zhǎng)事務(wù)問(wèn)題。

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          示例,產(chǎn)品下線時(shí),使用reids生成日志code,產(chǎn)品相關(guān)寫操作執(zhí)行完成后,發(fā)送消息,代碼如下:

          public void offlineProduct(OfflineProductDto dto){

              // TODO 修改操作為涉及到的查詢操作

              // TODO 使用redis生成業(yè)務(wù)code

              // 使用聲明式事務(wù)控制產(chǎn)品狀態(tài)修改的相關(guān)數(shù)據(jù)庫(kù)操作
              boolean status = transactionTemplate.execute(new TransactionCallback<Boolean>() {
                  @Nullable
                  @Override
                  public Boolean doInTransaction(TransactionStatus status) {
                        try {

                           // TODO 更改產(chǎn)品狀態(tài)

                        } catch (Exception e) {
                           status.setRollbackOnly();
                           throw e;
                        }
                        return true;
                     }
                  });

              // TODO 使用消息中間件發(fā)送消息

          }

          數(shù)據(jù)庫(kù)自動(dòng)給容災(zāi)

          結(jié)合配置中心,簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)庫(kù)的自動(dòng)容災(zāi)。以nacous配置中心為例,如何使用Nacos實(shí)現(xiàn)數(shù)據(jù)庫(kù)連接的自動(dòng)切換?。最新面試題整理好了,點(diǎn)擊Java面試庫(kù)小程序在線刷題。

          在springboot啟動(dòng)類加上@EnableNacosDynamicDataSource配置注解,即可無(wú)侵入的實(shí)現(xiàn)數(shù)據(jù)庫(kù)連接的動(dòng)態(tài)切換,示例如下:

          推薦一個(gè) Spring Boot 基礎(chǔ)教程及實(shí)戰(zhàn)示例:https://github.com/javastacks/spring-boot-best-practice

          @EnableNacosDynamicDataSource
          public class ProductApplication {

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

          }

          測(cè)試用例的編寫

          基于TDD的原則,結(jié)合junit和mockito實(shí)現(xiàn)服務(wù)功能的測(cè)試用例,為什么要寫單元測(cè)試?基于junit如何寫單元測(cè)試?。添加或者修改對(duì)象時(shí),需要校驗(yàn)入?yún)⒌挠行裕⑶倚r?yàn)操作以后的對(duì)象的各類屬性。

          以添加類目的api測(cè)試用例為例,如下,添加類別,成功后,校驗(yàn)添加參數(shù)以及添加成功后的屬性,以及其他默認(rèn)字段例如狀態(tài),排序等字段,源碼如下:

          // 添加類別的測(cè)試用例
          @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());
          }

          // 新增類目,成功添加后,返回根據(jù)id查詢CategorySuccessVo
          public 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());
          }
          也需要對(duì)添加類目的參數(shù)進(jìn)行校驗(yàn),例如,名稱不能重復(fù)的校驗(yàn),示例如下:

          // 添加類目的入?yún)?br>public class AddCategoryDto implements Serializable {

          private static final long serialVersionUID = -4752897765723264858L;

          // 名稱不能為空,名稱不能重復(fù)
          @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 = "類目層級(jí)")
          private Integer level;

          @ApiModelProperty(value = "排序")
          private Integer sort;

          }

          //添加失敗的校驗(yàn)校驗(yàn)測(cè)試用例
          @Test
          public 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);
               // 名稱重復(fù)
              errorResponse = this.addCategory(addCategoryDto);
              org.junit.Assert.assertNotNull(errorResponse);
              org.junit.Assert.assertNotNull(errorResponse.getMsg(), CATEGORY_NAME_IS_DUPLICATE);

          }

          原文鏈接:https://blog.csdn.net/new_com/article/details/108399421

          版權(quán)聲明:本文為CSDN博主「iloveoverfly」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。








          微信官宣:一大波新年紅包封面來(lái)了!
          2021 年發(fā)生的 10 件技術(shù)大事!!
          23 種設(shè)計(jì)模式實(shí)戰(zhàn)(很全)
          換掉 Log4j2!tinylog 橫空出世
          一款基于 Spring Boot 的神仙接私活項(xiàng)目
          勁爆!Java 協(xié)程要來(lái)了
          重磅官宣:Redis 對(duì)象映射框架來(lái)了!!
          推薦一款代碼神器,代碼量至少省一半!
          程序員精通各種技術(shù)體系,45歲求職難!
          重磅!Spring Boot 2.6 正式發(fā)布
          Spring Boot 學(xué)習(xí)筆記,這個(gè)太全了!



          關(guān)注Java技術(shù)棧看更多干貨



          獲取 Spring Boot 實(shí)戰(zhàn)筆記!
          瀏覽 60
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www做爱 | 亚洲无在线观看 | 大香蕉色视频 | 亚洲最大在线观看视频 | 午夜福利剧场 |