JavaRush /Java Blog /Random-TW /什麼是 Mapstruct 以及如何正確配置它以在 SpringBoot 應用程式中進行單元測試
Roman Beekeeper
等級 35

什麼是 Mapstruct 以及如何正確配置它以在 SpringBoot 應用程式中進行單元測試

在 Random-TW 群組發布

背景

大家好,親愛的朋友們和讀者們!在我們寫這篇文章之前,先介紹一下背景…我最近在使用Mapstruct庫時遇到了一個問題,我在我的電報頻道中對此進行了簡要描述。帖子的問題在評論中得到了解決;我之前專案的同事幫助解決了這個問題。 什麼是 Mapstruct 以及如何正確配置它以在 SpringBoot 應用程式中進行單元測試。 第 1 - 1 部分之後,我決定寫一篇關於這個主題的文章,但我們當然不會採取狹隘的觀點,而是首先嘗試加快速度,理解 Mapstruct 是什麼以及為什麼需要它,並使用一個真實的例子,我們將分析一下之前出現的情況以及如何解決。因此,我強烈建議在閱讀本文的同時進行所有計算,以便在實踐中體驗一切。 在我們開始之前,請訂閱我的電報頻道,我在那裡收集我的活動,寫下關於 Java 和 IT 開發的整體想法。 訂閱了?偉大的!好吧,現在我們走吧!

地圖結構,常見問題?

用於快速類型安全 bean 映射的程式碼產生器。 我們的首要任務是弄清楚 Mapstruct 是什麼以及為什麼我們需要它。一般來說,你可以在官方網站上了解它。網站的主頁上有三個問題的答案:這是什麼?為了什麼?如何?我們也會嘗試這樣做:

這是什麼?

Mapstruct 是一個庫,可協助使用基於透過介面描述的配置產生的程式碼將某些實體的物件映射(映射,一般來說,這就是人們常說的:映射、映射等)到其他實體的物件。

為了什麼?

在大多數情況下,我們開發多層應用程式(一層用於處理資料庫,一層業務邏輯,一層用於應用程式與外界的互動),並且每一層都有自己的物件來儲存和處理數據。而這些資料需要透過從一個實體傳輸到另一個實體來從一層傳輸到另一層。對於那些沒有使用過這種方法的人來說,這可能看起來有點複雜。例如,我們有一個學生資料庫實體。當這個實體的資料進入業務邏輯(服務)層時,我們需要將資料從Student類別傳輸到StudentModel類別。接下來,在完成所有業務邏輯的操作之後,需要將資料發佈到外部。為此,我們有 StudentDto 類別。當然,我們需要將資料從StudentModel類別傳遞到StudentDto。每次要轉移的方法都要手工編寫,這是一項勞動密集型工作。另外,這是程式碼庫中需要維護的額外程式碼。你可能會犯錯。Mapstruct 在編譯階段產生此類方法並將它們儲存在 generated-source 中。

如何?

使用註釋。我們只需要建立一個具有主 Mapper 註釋的註釋,該註釋告訴庫該介面中的方法可用於從一個物件轉換為另一個物件。正如我之前關於學生所說的,在我們的例子中,這將是 StudentMapper 接口,它將有多種將資料從一層傳輸到另一層的方法:
public class Student {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentDTO {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentModel {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}
對於這些類,我們創建一個映射器(下文中這就是我們所說的接口,它描述了我們要傳輸的內容以及傳輸的位置):
@Mapper
public interface StudentMapper {
   StudentModel toModel(StudentDTO dto);
   Student toEntity(StudentModel model);
   StudentModel toModel(Student entity);
   StudentDTO toDto(StudentModel model);
}
這種方法的美妙之處在於,如果不同類別中欄位的名稱和類型相同(如我們的例子),那麼 Mapstruct 的設定就足以在編譯階段基於 StudentMapper 介面產生必要的實現,這會翻譯。那麼事情已經變得很清楚了,對吧?讓我們更進一步,用一個真實的範例來分析Spring Boot應用程式中的工作。

Spring Boot 和 Mapstruct 工作的範例

我們需要的第一件事是建立一個 Spring Boot 專案並在其中加入 Mapstruct。就此事而言,我在 GitHub 上有一個組織,其中包含儲存庫模板,Spring Boot 的啟動就是其中之一。基於它,我們創建一個新專案: 什麼是 Mapstruct 以及如何正確配置它以在 SpringBoot 應用程式中進行單元測試。 第 1 - 2 部分接下來,我們得到該專案是的,朋友們,如果您覺得該專案有用,請給它一顆星,這樣我就知道我沒有白做。在這個項目中,我們將揭示我在工作中收到並在我的 Telegram 頻道上的帖子 中描述的情況。我將向那些不了解情況的人簡要概述一下情況:當我們為映射器(即我們之前討論的那些介面實作)編寫測試時,我們希望測試盡快通過。使用映射器的最簡單選項是在執行測試時使用 SpringBootTest 註釋,它將取得 Spring Boot 應用程式的整個 ApplicationContext 並將測試所需的映射器注入到測試中。但這個選項非常耗費資源並且需要更多的時間,所以它不適合我們。我們需要編寫一個單元測試,簡單地建立所需的映射器並檢查其方法是否完全按照我們的預期工作。為什麼需要測試才能跑得更快?如果測試花費很長時間,就會減慢整個開發過程。在新程式碼的測試通過之前,程式碼不能被認為是正確的,並且不會被用於測試,這意味著它不會被投入生產,這意味著開發人員還沒有完成工作。看起來,為什麼要為一個運行毫無疑問的函式庫編寫測試呢?然而我們需要編寫一個測試,因為我們正在測試我們描述映射器的正確程度以及它是否符合我們的預期。首先,為了讓我們的工作更輕鬆,讓我們透過在 pom.xml 添加另一個依賴項來將 Lombok 新增到我們的專案中:
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
</dependency>
在我們的專案中,我們需要從模型類別(用於處理業務邏輯)轉移到 DTO 類,用於與外界通訊。在我們的簡化版本中,我們假設欄位不會改變,並且我們的映射器將很簡單。但是,如果有願望,可以寫一篇更詳細的文章,介紹如何使用 Mapstruct、如何配置它以及如何利用它的優勢。但是,由於這篇文章會很長。假設我們有一個學生,有他參加的講座和講師的清單。讓我們創建一個模型包。在此基礎上,我們將建立一個簡單的模型:
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
他的講座
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
和講師
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
並在模型包旁邊 創建一個dto包:
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
講座
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
和講師
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
現在讓我們建立一個映射器,將講座模型集合轉換為 DTO 講座集合。首先要做的是將 Mapstruct 加入到專案中。為此,我們將使用他們的官方網站,那裡有所有描述。也就是說,我們需要在記憶體中添加一個依賴項和一個外掛程式(如果您對記憶體是什麼有疑問,請看這裡,Article1Article2):
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>1.4.2.Final</version>
</dependency>
並在記憶體中的 <build/> 區塊中。我們還沒有:
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.5.1</version>
           <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>1.4.2.Final</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>
接下來,我們在dtomodel旁邊建立一個映射器套件。根據我們之前展示的類,您將需要建立另外五個映射器:
  • Mapper LectureModel <-> LectureDTO
  • 映射器清單<LectureModel> <-> 清單<LectureDTO>
  • 映射器講師模型 <-> 講師DTO
  • 映射器清單<LecturerModel> <-> 清單<LecturerDTO>
  • 映射器 StudentModel <-> StudentDTO
去:

講座映射器

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LecturerDTO dto);
}

講座列表映射器

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LectureMapper.class)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}

講師製圖師

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LectureDTO dto);
}

講師清單映射器

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LecturerModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LecturerMapper.class)
public interface LecturerListMapper {
   List<LecturerModel> toModelList(List<LecturerDTO> dloList);
   List<LecturerDTO> toDTOList(List<LecturerModel> modelList);
}

學生繪圖員

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.StudentDTO;
import com.github.romankh3.templaterepository.springboot.model.StudentModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
public interface StudentMapper {
   StudentDTO toDTO(StudentModel model);
   StudentModel toModel(StudentDTO dto);
}
應該單獨指出的是,在映射器中我們引用了其他映射器。這是透過Mapper 註解中的 use欄位完成的,如 StudentMapper 中所做的那樣:
@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
這裡我們使用兩個映射器來正確地映射講座清單和講師清單。現在我們需要編譯我們的程式碼並查看其中有什麼以及如何進行。這可以使用mvn clean 編譯指令來完成。但是,事實證明,在建立映射器的 Mapstruct 實作時,映射器實作並沒有覆蓋欄位。為什麼?事實證明,無法從 Lombok 取得資料註釋。必須做點什麼......因此,我們在文章中增加了一個新的部分。

連接 Lombok 和 Mapstruct

經過幾分鐘的搜索,發現我們需要以某種方式連接Lombok和Mapstruct。Mapstruct 文件中有關於此的資訊。檢查了 Mapstruct 開發人員提出的範例後,讓我們更新 pom.xml:讓我們加入單獨的版本:
​​<properties>
   <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
讓我們加入缺少的依賴項:
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
讓我們更新我們的編譯器插件,以便它可以連接 Lombok 和 Mapstruct:
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>3.5.1</version>
   <configuration>
       <source>1.8</source>
       <target>1.8</target>
       <annotationProcessorPaths>
           <path>
               <groupId>org.mapstruct</groupId>
               <artifactId>mapstruct-processor</artifactId>
               <version>${org.mapstruct.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
               <version>${lombok.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok-mapstruct-binding</artifactId>
               <version>${lombok-mapstruct-binding.version}</version>
           </path>
       </annotationProcessorPaths>
   </configuration>
</plugin>
在此之後一切都會解決。讓我們再次編譯我們的專案。但是在哪裡可以找到 Mapstruct 產生的類別呢?它們位於生成來源中: ${projectDir}/target/ generated-sources/annotations/ 什麼是 Mapstruct 以及如何正確配置它以在 SpringBoot 應用程式中進行單元測試。 第 1 - 3 部分現在我們已經準備好認識到我對 Mapstruct 帖子的失望了,讓我們嘗試為映射器創建測試。

我們為我們的映射器編寫測試

我將創建一個快速而簡單的測試,在我們創建集成測試並且不用擔心其完成時間的情況下測試其中一個映射器:

講座映射器測試

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class LectureMapperTest {

   @Autowired
   private LectureMapper mapperUnderTest;

   @Test
   void shouldProperlyMapModelToDto() {
       //given
       LectureModel model = new LectureModel();
       model.setId(11L);
       model.setName("lecture name");

       //when
       LectureDTO dto = mapperUnderTest.toDTO(model);

       //then
       Assertions.assertNotNull(dto);
       Assertions.assertEquals(model.getId(), dto.getId());
       Assertions.assertEquals(model.getName(), dto.getName());
   }

   @Test
   void shouldProperlyMapDtoToModel() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(11L);
       dto.setName("lecture name");

       //when
       LectureModel model = mapperUnderTest.toModel(dto);

       //then
       Assertions.assertNotNull(model);
       Assertions.assertEquals(dto.getId(), model.getId());
       Assertions.assertEquals(dto.getName(), model.getName());
   }
}
在這裡,我們使用 SpringBootTest 註解啟動整個 applicationContext,並使用 Autowired 註解從中提取測試所需的類別。從編寫測試的速度和易用性的角度來看,這非常好。測試順利通過,一切正常。但我們會採取另一種方式,為映射器編寫單元測試,例如 LectureListMapper...
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl();

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
由於 Mapstruct 產生的實作與我們的專案位於同一類別中,因此我們可以輕鬆地在測試中使用它們。一切看起來都很棒 - 沒有註釋,我們以最簡單的方式創建我們需要的類,僅此而已。但是當我們執行測試時,我們會發現它會崩潰,並且在控制台中會出現 NullPointerException...這是因為 LectureListMapper 映射器的實作如下所示:
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T21:46:12+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   @Autowired
   private LectureMapper lectureMapper;

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
如果我們查看 NPE(NullPointerException 的縮寫),我們會從LectureMapper變數中取得它,結果發現該變數尚未初始化。但在我們的實作中,我們沒有可以用來初始化變數的建構子。這正是 Mapstruct 以這種方式實作映射器的原因!在 Spring 中,您可以透過多種方式將 bean 新增至類別中,您可以透過欄位以及 Autowired 註解來注入它們,如上所述,也可以透過建構函式註入它們。當我需要優化測試執行時間時,我發現自己在工作上遇到了這樣的問題。我認為對此無能為力,並在我的 Telegram 頻道上傾訴了我的痛苦。然後他們在評論中幫我說可以客製化注入策略。Mapper介面有一個injectionStrategy字段,它只接受InjectionStrategy名稱,它有兩個值:FIELDCONSTRUCTOR。現在,了解了這一點,讓我們將此設定添加到我們的映射器中;我將使用LectureListMapper作為範例來展示它:
@Mapper(componentModel = "spring", uses = LectureMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}
我用粗體突出顯示了我添加的部分。讓我們為所有其他選項新增此選項並重新編譯項目,以便使用新行產生映射器。當我們這樣做時,讓我們看看LectureListMapper的映射器的實作如何改變(以粗體突出顯示我們需要的部分):
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T22:25:37+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   private final LectureMapper lectureMapper;

   @Autowired
   public LectureListMapperImpl(LectureMapper lectureMapper) {

       this.lectureMapper = lectureMapper;
   }

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
現在Mapstruct已經透過建構函式實現了mapper注入。這正是我們想要實現的目標。現在我們的測試將停止編譯,讓我們更新它並取得:
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl(new LectureMapperImpl());

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
現在,如果我們執行測試,一切都會按預期工作,因為在 LectureListMapperImpl 中我們傳遞了它需要的 LectureMapper...勝利!這對你來說並不困難,但我很高興: 朋友們,一切如常,訂閱我的GitHub 帳戶,訂閱我的Telegram 帳戶。在那裡我發布了我的活動結果,確實有用的東西)我特別邀請您加入telegram頻道的討論小組。碰巧的是,如果有人有技術問題,他們可以在那裡得到答案。這種格式對每個人來說都很有趣,您可以閱讀誰知道什麼並獲得經驗。

結論

透過這篇文章,我們認識了Mapstruct這樣一個必要且經常使用的產品。我們弄清楚了它是什麼、為什麼以及如何。透過一個真實的例子,我們感受到了可以做什麼以及如何改變它。我們也研究如何透過建構函式設定 bean 注入,以便可以正確測試映射器。Mapstruct 的同事允許其產品的使用者準確地選擇如何注入映射器,對此我們無疑要感謝他們。但是,儘管 Spring 建議透過建構函式註入 bean,但 Mapstruct 的人員預設透過欄位設定注入。這是為什麼?沒有答案。我懷疑可能有我們不知道的原因,這就是他們這樣做的原因。為了向他們了解情況,我在他們的官方產品儲存庫上 創建了一個 GitHub 問題。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION