天天看點

這 10 個 Spring 錯誤别再犯了!老程式員一不留神就掉坑 。。。

這 10 個 Spring 錯誤别再犯了!老程式員一不留神就掉坑 。。。

譯者:萬想

作者:Toni Kukurin

本文整理了研發人員使用 Spring framework 架構時經常會出現的錯誤。

錯誤一:太過關注底層

我們正在解決這個常見錯誤,是因為 “非我所創” 綜合症在軟體開發領域很是常見。症狀包括經常重寫一些常見的代碼,很多開發人員都有這種症狀。

雖然了解特定庫的内部結構及其實作,在很大程度上是好的并且很有必要的(也可以是一個很好的學習過程),但作為軟體工程師,不斷地處理相同的底層實作細節對個人的開發生涯是有害的。

像 Spring 這種抽象架構的存在是有原因的,它将你從重複地手工勞作中解放出來,并允許你專注于更高層次的細節 —— 領域對象和業務邏輯。

是以,接受抽象。下次面對特定問題時,首先進行快速搜尋,确定解決該問題的庫是否已被內建到 Spring 中;現在,你可能找到一個合适的現成解決方案。

比如,一個很有用的庫,在本文的其他部分,我将在示例中使用 Project Lombok 注解。Lombok 被用作樣闆代碼生成器,希望懶惰的開發人員在熟悉這個庫時不會遇到問題。舉個例子,看看使用 Lombok 的 “标準 Java Bean” 是什麼樣子的:

如你所想,上述代碼被編譯為:

但是,請注意,如果你打算在 IDE 中使用 Lombok,很可能需要安裝一個插件,可在 此處 找到 Intellij IDEA 版本的插件。

錯誤二:内部結構 “洩露”

公開你的内部結構,從來都不是一個好主意,因為它在服務設計中造成了不靈活性,進而促進了不好的編碼實踐。“洩露” 的内部機制表現為使資料庫結構可以從某些 API 端點通路。例如,下面的POJO(“Plain Old Java Object”)類表示資料庫中的一個表:

@Entity
@NoArgsConstructor
@Getter
public class TopTalentEntity {


    @Id
    @GeneratedValue
    private Integer id;


    @Column
    private String name;


    public TopTalentEntity(String name) {
        this.name = name;
    }


}
           

假設,存在一個端點,他需要通路

TopTalentEntity

資料。傳回

TopTalentEntity

執行個體可能很誘人,但更靈活的解決方案是建立一個新的類來表示 API 端點上的

TopTalentEntity

資料。

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class TopTalentData {
    private String name;
}
           

這樣,對資料庫後端進行更改将不需要在服務層進行任何額外的更改。考慮下,在

TopTalentEntity

中添加一個 “password” 字段來存儲資料庫中使用者密碼的 Hash 值 —— 如果沒有

TopTalentData

之類的連接配接器,忘記更改服務前端,将會意外地暴露一些不必要的秘密資訊。

錯誤三:缺乏關注點分離

随着程式規模的增長,逐漸地,代碼組織成為一個越來越重要的問題。諷刺的是,大多數好的軟體工程原則開始在規模上崩潰 —— 特别是在沒有太多考慮程式體系結構設計的情況下。開發人員最常犯的一個錯誤就是混淆代碼關注點,這很容易做到!

通常,打破 關注點分離 的是将新功能簡單地 “倒” 在現有類中。當然,這是一個很好的短期解決方案(對于初學者來說,它需要更少的輸入),但它也不可避免地會在将來成為一個問題,無論是在測試期間、維護期間還是介于兩者之間。考慮下下面的控制器,它将從資料庫傳回

TopTalentData

@RestController
public class TopTalentController {


    private final TopTalentRepository topTalentRepository;


    @RequestMapping("/toptal/get")
    public List<TopTalentData> getTopTalent() {
        return topTalentRepository.findAll()
                .stream()
                .map(this::entityToData)
                .collect(Collectors.toList());
    }


    private TopTalentData entityToData(TopTalentEntity topTalentEntity) {
        return new TopTalentData(topTalentEntity.getName());
    }


}
           

起初,這段代碼似乎沒什麼特别的問題;它提供了一個從

TopTalentEntity

執行個體檢索出來的

TopTalentData

的 List。

然而,仔細觀察下,我們可以看到

TopTalentController

實際上在此做了些事情;也就是說,它将請求映射到特定端點,從資料庫檢索資料,并将從

TopTalentRepository

接收的實體轉換為另一種格式。一個“更幹淨” 的解決方案是将這些關注點分離到他們自己的類中。看起來可能是這個樣子的:

@RestController
@RequestMapping("/toptal")
@AllArgsConstructor
public class TopTalentController {


    private final TopTalentService topTalentService;


    @RequestMapping("/get")
    public List<TopTalentData> getTopTalent() {
        return topTalentService.getTopTalent();
    }
}


@AllArgsConstructor
@Service
public class TopTalentService {


    private final TopTalentRepository topTalentRepository;
    private final TopTalentEntityConverter topTalentEntityConverter;


    public List<TopTalentData> getTopTalent() {
        return topTalentRepository.findAll()
                .stream()
                .map(topTalentEntityConverter::toResponse)
                .collect(Collectors.toList());
    }
}


@Component
public class TopTalentEntityConverter {
    public TopTalentData toResponse(TopTalentEntity topTalentEntity) {
        return new TopTalentData(topTalentEntity.getName());
    }
}
           

這種層次結構的另一個優點是,它允許我們通過檢查類名來确定将功能駐留在何處。此外,在測試期間,如果需要,我們可以很容易地用模拟實作來替換任何類。

錯誤四:缺乏異常處理或處理不當

一緻性的主題并非是 Spring(或 Java)所獨有的,但仍然是處理 Spring 項目時需要考慮的一個重要方面。雖然編碼風格可能存在争議(通常團隊或整個公司内部已達成一緻),但擁有一個共同的标準最終會極大地提高生産力。對多人團隊尤為如此;一緻性允許交流發生,而不需要花費很多資源在手把手交接上,也不需要就不同類的職責提供冗長的解釋。

考慮一個包含各種配置檔案、服務和控制器的 Spring 項目。在命名時保持語義上的一緻性,可以建立一個易于搜尋的結構,任何新的開發人員都可以按照自己的方式管理代碼;例如,将 Config 字尾添加到配置類,服務層以 Service 結尾,以及控制器用 Controller 結尾。

與一緻性主題密切相關,伺服器端的錯誤處理值得特别強調。如果你曾經不得不處理編寫很差的 API 的異常響應,那你可能知道原因 —— 正确解析異常會是一件痛苦的事情,而确定這些異常最初發生的原因則更為痛苦。

作為一名 API 開發者,理想情況下你希望覆寫所有面向使用者的端點,并将他們轉換為常見的錯誤格式。這通常意味着有一個通用的錯誤代碼和描述,而不是逃避解決問題:a) 傳回一個 “500 Internal Server Error”資訊。b) 直接傳回異常的堆棧資訊給使用者。(實際上,這些都應該不惜一切代價地去避免,因為除了用戶端難以處理以外,它還暴露了你的内部資訊)。

例如,常見錯誤響應格式可能長這樣:

@Value
public class ErrorResponse {


    private Integer errorCode;
    private String errorMessage;


}
           

與此類似的事情在大多數流行的 API 中也經常遇到,由于可以容易且系統地記錄,效果往往很不錯。将異常轉換為這種格式可以通過向方法提供

@ExceptionHandler

注解來完成(注解案例可見于第六章)。

錯誤五:多線程處理不當

不管是桌面應用還是 Web 應用,無論是 Spring 還是 No Spring,多線程都是很難破解的。由并行執行程式所引起的問題是令人毛骨悚然且難以捉摸的,而且常常難以調試 —— 實際上,由于問題的本質,一旦你意識到你正在處理一個并行執行問題,你可能就不得不完全放棄調試器了,并 “手動” 檢查代碼,直到找到根本上的錯誤原因。

不幸的是,這類問題并沒有千篇一律的解決方案;根據具體場景來評估情況,然後從你認為最好的角度來解決問題。

當然,理想情況下,你也希望完全避免多線程錯誤。同樣,不存在那種一刀切的方法,但這有一些調試和防止多線程錯誤的實際考慮因素:

避免全局狀态

首先,牢記 “全局狀态” 問題。如果你正建立一個多線程應用,那麼應該密切關注任何可能全局修改的内容,如果可能的話,将他們全部删掉。如果某個全局變量有必須保持可修改的原因,請仔細使用 synchronization,并對程式性能進行跟蹤,以确定沒有因為新引入的等待時間而導緻系統性能降低。

避免可變性

這點直接來自于 函數式程式設計,并且适用于 OOP,聲明應該避免類和狀态的改變。簡而言之,這意味着放棄 setter 方法,并在所有模型類上擁有私有的 final 字段。它們的值唯一發生變化的時間是在構造期間。這樣,你可以确定不會出現争用問題,且通路對象屬性将始終提供正确的值。

記錄關鍵資料

評估你的程式可能會在何處發生異常,并預先記錄所有關鍵資料。如果發生錯誤,你将很高興可以得到資訊說明收到了哪些請求,并可更好地了解你的應用程式為什麼會出現錯誤。需要再次注意的是,日志記錄引入了額外的檔案 I/O,可能會嚴重影響應用的性能,是以請不要濫用日志。

複用現存實作

每當你需要建立自己的線程時(例如:向不同的服務發出異步請求),複用現有的安全實作來代替建立自己的解決方案。這在很大程度上意味着要使用 ExecutorServices 和 Java 8 簡潔的函數式 CompletableFutures 來建立線程。Spring 還允許通過 DeferredResult 類來進行異步請求處理。

錯誤六:不使用基于注解的驗證

假設我們之前的 TopTalent 服務需要一個端點來添加新的 TopTalent。此外,假設基于某些原因,每個新名詞都需要為 10 個字元長度。執行此操作的一種方法可能如下:

@RequestMapping("/put")
public void addTopTalent(@RequestBody TopTalentData topTalentData) {
    boolean nameNonExistentOrHasInvalidLength =
            Optional.ofNullable(topTalentData)
         .map(TopTalentData::getName)
   .map(name -> name.length() == 10)
   .orElse(true);


    if (nameNonExistentOrInvalidLength) {
        // throw some exception
    }


    topTalentService.addTopTalent(topTalentData);
}
           

然而,上面的方法(除了構造很差以外)并不是一個真正 “幹淨” 的解決辦法。我們正檢查不止一種類型的有效性(即 TopTalentData 不得為空,TopTalentData.name 不得為空,且 TopTalentData.name 為 10 個字元長度),以及在資料無效時抛出異常。

通過在Spring中內建 Hibernate validator,資料校驗可以更幹淨地進行。讓我們首先重構 addTopTalent 方法來支援驗證:

@RequestMapping("/put")
public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) {
    topTalentService.addTopTalent(topTalentData);
}


@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) {
    // handle validation exception
}
// 此外,我們還必須指出我們想要在 TopTalentData 類中驗證什麼屬性:
public class TopTalentData {
    @Length(min = 10, max = 10)
    @NotNull
    private String name;
}
           

現在,Spring 将在調用方法之前攔截其請求并對參數進行驗證 —— 無需使用額外的手工測試。

另一種實作相同功能的方法是建立我們自己的注解。雖然你通常隻在需要超出 Hibernate的内置限制集 時才使用自定義注解,本例中,我們假設 @Length 不存在。你可以建立兩個額外的類來驗證字元串長度,一個用于驗證,一個用于對屬性進行注解:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { MyAnnotationValidator.class })
public @interface MyAnnotation {


    String message() default "String length does not match expected";


    Class<?>[] groups() default {};


    Class<? extends Payload>[] payload() default {};


    int value();


}


@Component
public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> {


    private int expectedLength;


    @Override
    public void initialize(MyAnnotation myAnnotation) {
        this.expectedLength = myAnnotation.value();
    }


    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s == null || s.length() == this.expectedLength;
    }
}
           

請注意,這些情況下,關注點分離的最佳實踐要求在屬性為 null 時,将其标記為有效(

isValid

方法中的

s == null

),如果這是屬性的附加要求,則使用

@NotNull

注解。

public class TopTalentData {
    @MyAnnotation(value = 10)
    @NotNull
    private String name;
}
           

錯誤七:(依舊)使用基于xml的配置

雖然之前版本的 Spring 需要 XML,但如今大部配置設定置均可通過 Java 代碼或注解來完成;XML 配置隻是作為附加的不必要的樣闆代碼。

本文(及其附帶的 GitHub 倉庫)均使用注解來配置 Spring,Spring 知道應該連接配接哪些 Bean,因為待掃描的頂級包目錄已在

@SpringBootApplication

複合注解中做了聲明,如下所示:

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

複合注解(可通過 Spring 文檔 了解更多資訊)隻是向 Spring 提示應該掃描哪些包來檢索 Bean。在我們的案例中,這意味着這個頂級包 (co.kukurin)将用于檢索:

  • @Component

    (

    TopTalentConverter

    ,

    MyAnnotationValidator

    )
  • @RestController

    (

    TopTalentController

    )
  • @Repository

    (

    TopTalentRepository

    )
  • @Service

    (

    TopTalentService

    ) 類

如果我們有任何額外的

@Configuration

注解類,它們也會檢查基于 Java 的配置。

錯誤八:忽略 profile

在服務端開發中,經常遇到的一個問題是區分不同的配置類型,通常是生産配置和開發配置。在每次從測試切換到部署應用程式時,不要手動替換各種配置項,更有效的方法是使用 profile。

考慮這麼一種情況:你正在使用記憶體資料庫進行本地開發,而在生産環境中使用 MySQL 資料庫。本質上,這意味着你需要使用不同的 URL 和 (希望如此) 不同的憑證來通路這兩者。讓我們看看可以如何做到這兩個不同的配置檔案:

APPLICATION.YAML 檔案

# set default profile to 'dev'
spring.profiles.active: dev


# production database details
# 公衆号:Web項目聚集地
spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal'
spring.datasource.username: root
spring.datasource.password:
8.2. APPLICATION-DEV.YAML 檔案
spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2
           

假設你不希望在修改代碼時意外地對生産資料庫進行任何操作,是以将預設配置檔案設為 dev 是很有意義的。

然後,在伺服器上,你可以通過提供

-Dspring.profiles.active=prod

參數給 JVM 來手動覆寫配置檔案。另外,還可将作業系統的環境變量設定為所需的預設 profile。

錯誤九:無法接受依賴項注入

正确使用 Spring 的依賴注入意味着允許其通過掃描所有必須的配置類來将所有對象連接配接在一起;這對于解耦關系非常有用,也使測試變得更為容易,而不是通過類之間的緊耦合來做這樣的事情:

public class TopTalentController {


    private final TopTalentService topTalentService;


    public TopTalentController() {
        this.topTalentService = new TopTalentService();
    }
}
           

我們讓 Spring 為我們做連接配接:

public class TopTalentController {


    private final TopTalentService topTalentService;


    public TopTalentController(TopTalentService topTalentService) {
        this.topTalentService = topTalentService;
    }
}
           

Misko Hevery 的 Google talk 深入解釋了依賴注入的 “為什麼”,是以,讓我們看看它在實踐中是如何使用的。在關注點分離(常見錯誤 #3)一節中,我們建立了一個服務和控制器類。

假設我們想在

TopTalentService

行為正确的前提下測試控制器。我們可以通過提供一個單獨的配置類來插入一個模拟對象來代替實際的服務實作:

@Configuration
public class SampleUnitTestConfig {
    @Bean
    public TopTalentService topTalentService() {
        TopTalentService topTalentService = Mockito.mock(TopTalentService.class);
        Mockito.when(topTalentService.getTopTalent()).thenReturn(
                Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList()));
        return topTalentService;
    }
}
           

然後,我們可以通過告訴 Spring 使用

SampleUnitTestConfig

作為它的配置類來注入模拟對象:

@ContextConfiguration(classes = { SampleUnitTestConfig.class })
           

之後,我們就可以使用上下文配置将 Bean 注入到單元測試中。

錯誤十:缺乏測試,或測試不當

盡管單元測試的概念已經存在很長時間了,但很多開發人員似乎要麼 “忘記” 做這件事(特别是如果它不是 “必需” 的時候),要麼隻是在事後把它添加進來。這顯然是不可取的,因為測試不僅應該驗證代碼的正确性,還應該作為程式在不同場景下應如何表現的文檔。

在測試 Web 服務時,很少隻進行 “純” 單元測試,因為通過 HTTP 進行通信通常需要調用 Spring 的

DispatcherServlet

,并檢視當收到一個實際的

HttpServletRequest

時會發生什麼(使它成為一個 “內建” 測試,處理驗證、序列化等)。

REST Assured,一個用于簡化測試REST服務的 Java DSL,在 MockMVC 之上,已經被證明提供了一個非常優雅的解決方案。考慮以下帶有依賴項注入的代碼片段:

@RunWith(SpringJUnit4Cla***unner.class)
@ContextConfiguration(classes = {
        Application.class,
        SampleUnitTestConfig.class
})
public class RestAssuredTestDemonstration {


    @Autowired
    private TopTalentController topTalentController;


    @Test
    public void shouldGetMaryAndJoel() throws Exception {
        // given
        MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given()
                .standaloneSetup(topTalentController);


        // when
        MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get");


        // then
        response.then().statusCode(200);
        response.then().body("name", hasItems("Mary", "Joel"));
    }


}
           

SampleUnitTestConfig

類将

TopTalentService

的模拟實作連接配接到

TopTalentController

中,而所有的其他類都是通過掃描應用類所在包的下級包目錄來推斷出的标準配置。

RestAssuredMockMvc

隻是用來設定一個輕量級環境,并向

/toptal/get

端點發送一個

GET

請求。

原文:https://www.toptal.com/spring/top-10-most-common-spring-framework-mistakes

  • 一個超牛逼的 Java 檔案線上預覽項目
  • SpringBoot 的@Value注解太強大了,用了都說爽!
  • SpringBoot:如何優雅地進行參數傳遞、響應資料封裝、異常處理?
  • 徹底搞懂 Nginx 的五大應用場景
  • 記一個自己項目上線的全過程
  • SpringBoot+Gradle+ MyBatisPlus3.x搭建企業級的背景分離架構(實戰版)
  • 号稱 GitHub 上有史以來,高仿版微信最牛逼的項目來了(附完整源碼)
  • Spring Boot 這樣優化,讓你的項目飛起來!
  • 快手二面:Java 裡的 for (;;) 與 while (true),哪個更快?
  • springboot+redis+Interceptor+自定義annotation實作接口自動幂等
這 10 個 Spring 錯誤别再犯了!老程式員一不留神就掉坑 。。。