1. 簡介
在 Java 中使用泛型時,我們經常會遇到類型擦除。這在處理返回泛型集合或複雜參數化類型的 HTTP 請求時尤其具有挑戰性。Spring 的 <em>ParameterizedTypeReference</em> 提供了優雅的解決方案。
在本教程中,我們將探討如何使用 <em>ParameterizedTypeReference</em> 與 <em>RestTemplate</em> 和 <em>WebClient</em> 一起使用。我們還將涵蓋處理現代 Java 應用程序中複雜泛型類型的底層概念和最佳實踐。
2. 理解類型擦除及其帶來的問題
Java 的類型擦除會在運行時移除泛型類型信息。例如,<em List<String> </em>和<em List<Integer> 都會在運行時變為`<em List 。1這在我們需要保留泛型類型信息時,會帶來挑戰。
讓我們創建一個`<em User 類,以幫助我們在代碼演示中:
public class User {
private Long id;
private String name;
private String email;
private String department;
//constructors, getters and setters
}現在,讓我們考慮一個常見場景,即從 REST API 中檢索用户列表:
RestTemplate restTemplate = new RestTemplate();
List<User> users = restTemplate.getForObject("/users", List.class);然而,這會導致產生一個 List<Object>,而不是我們想要的 List<User>。 此外,列表中的每個元素都需要手動進行類型轉換。 這種方法可能導致錯誤,並且與使用泛型的目的背道而馳。
毫無疑問,這正是 ParameterizedTypeReference 的價值所在。 它能夠捕獲並保留完整的泛型類型信息,以便在運行時可用。
3. 使用 RestTemplate 的基本用法
RestTemplate 仍然廣泛應用於 Spring 應用中。理解如何使用它處理通用類型非常重要。接下來,我們將探討幾個實際場景,以便更好地理解 ParameterizedTypeReference。
3.1. 使用通用集合
讓我們以一個示例來演示如何使用 RestTemplate 檢索用户列表:
@Service
public class ApiService {
//properties and constructor
public List<User> fetchUserList() {
ParameterizedTypeReference<List<User>> typeRef =
new ParameterizedTypeReference<List<User>>() {};
ResponseEntity<List<User>> response = restTemplate.exchange(
baseUrl + "/api/users",
HttpMethod.GET,
null,
typeRef
);
return response.getBody();
}
}在上述示例中,關鍵要素是創建ParameterizedTypeReference,並指定我們期望的精確泛型類型。空花括號創建了一個匿名類,該類用作exchange()方法中的參數。 ParameterizedTypeReference允許exchange()直接返回一個List<User>,而無需進行任何類型轉換。
讓我們驗證這個邏輯:
@Test
void whenFetchingUserList_thenReturnsCorrectType() {
// given
wireMockServer.stubFor(get(urlEqualTo("/api/users"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
[
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"department": "Engineering"
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"department": "Marketing"
}
]
""")));
// when
List<User> result = apiService.fetchUserList();
// then
assertEquals(2, result.size());
assertEquals("John Doe", result.get(0).getName());
assertEquals("[email protected]", result.get(1).getEmail());
assertEquals("Engineering", result.get(0).getDepartment());
assertEquals("Marketing", result.get(1).getDepartment());
}測試結果確認,我們的ParameterizedTypeReference能夠正確地保留泛型類型信息,從而使我們能夠使用正確類型的List<User> ,而不是原始的List
我們使用WireMock 來進行測試,模擬一個實時 API 端點。這使得我們的RestTemplate 能夠發出實際的 HTTP 調用並接收有效的 JSON 響應,這些響應隨後根據我們的ParameterizedTypeReference進行反序列化。
WireMock 提供了一個可靠的選擇,可以在不依賴外部服務的情況下測試 HTTP 客户端的行為,從而確保我們的測試既可靠又快速。
3.2. 比較 getForEntity() 與 exchange()
理解 getForEntity() 和 exchange() 之間的差異對於使用泛型類型的工作至關重要。 此外,許多開發者最初會嘗試使用更簡單的 getForEntity() 方法,但最終會遇到類型安全問題。
關鍵問題在於,getForEntity() 不接受 ParameterizedTypeReference — 它僅接受響應對象的類類型。
以下是一個 不當使用 getForEntity() 的示例:
public List<User> fetchUsersWrongApproach() {
ResponseEntity response = restTemplate.getForEntity(
baseUrl + "/api/users",
List.class
);
return (List) response.getBody();
}我們使用上述解決方案時會遇到一個未檢查的類型轉換。即使代碼編譯成功,它也會丟失類型信息。
現在,讓我們來看一下推薦的解決方案,使用 <em >exchange()</em>:
public List<User> fetchUsersCorrectApproach() {
ParameterizedTypeReference<List<User>> typeRef =
new ParameterizedTypeReference<List<User>>() {};
ResponseEntity<List<User>> response = restTemplate.exchange(
baseUrl + "/api/users",
HttpMethod.GET,
null,
typeRef
);
return response.getBody();
}一些關鍵差異在於,getForEntity()接受一個Class<T>參數,不能代表泛型類型。另一方面,exchange()接受ParameterizedTypeReference<T>,保留了完整的類型信息。
因此,編譯器可以使用exchange()進行類型安全驗證,從而在運行時防止ClassCastException。
4. 使用 WebClient
WebClient 提供了一種現代、響應式的 HTTP 通信方法。與 RestTemplate 不同,它返回響應式類型,例如 Mono 和 Flux。
4.1. 使用複雜類型進行反應式操作
讓我們看看 ParameterizedTypeReference 如何處理嵌套泛型類型在反應式編程中的應用:
@Service
public class ReactiveApiService {
private final WebClient webClient;
public ReactiveApiService(String baseUrl) {
this.webClient = WebClient.builder().baseUrl(baseUrl).build();
}
public Mono<Map<String, List<User>>> fetchUsersByDepartment() {
ParameterizedTypeReference<Map<String, List<User>>> typeRef =
new ParameterizedTypeReference<Map<String, List<User>>>() {};
return webClient.get()
.uri("/users/by-department")
.retrieve()
.bodyToMono(typeRef);
}
}該方法返回一個包含Mono的映射,其中每個鍵是部門名稱,每個值是該部門中的用户列表。這展示了WebClient在處理複雜泛型結構的同時保持類型安全的能力。
現在,讓我們測試我們的實現:
@Test
void whenFetchingUsersByDepartment_thenReturnsCorrectMap() {
// given
wireMockServer.stubFor(get(urlEqualTo("/users/by-department"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"Engineering": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"department": "Engineering"
}
],
"Marketing": [
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"department": "Marketing"
}
]
}
""")));
// when
Mono<Map<String, List<User>>> result = reactiveApiService.fetchUsersByDepartment();
// then
StepVerifier.create(result)
.assertNext(map -> {
assertTrue(map.containsKey("Engineering"));
assertTrue(map.containsKey("Marketing"));
assertEquals("John Doe", map.get("Engineering").get(0).getName());
assertEquals("Jane Smith", map.get("Marketing").get(0).getName());
// Verify proper typing - this would fail if ParameterizedTypeReference didn't work
List engineeringUsers = map.get("Engineering");
User firstUser = engineeringUsers.get(0);
assertEquals(Long.valueOf(1L), firstUser.getId());
})
.verifyComplete();
}請注意,我們能夠正確地將 JSON 響應反序列化為預期的 Map<String, List<User>>。
4.2. 自定義通用包裝器
現實世界的API通常使用通用包裝器類。以下是如何處理它們的方法。首先,讓我們創建我們的包裝器對象:
public record ApiResponse<T>(boolean success, String message, T data) {}現在,讓我們使用這個包裝器與ParameterizedTypeReference:
public Mono<ApiResponse<List<User>>> fetchUsersWithWrapper() {
ParameterizedTypeReference<ApiResponse<List<User>>> typeRef =
new ParameterizedTypeReference<ApiResponse<List<User>>>() {};
return webClient.get()
.uri("/users/wrapped")
.retrieve()
.bodyToMono(typeRef);
}最後,讓我們測試我們的實現,以確保它正確處理通用包裝器:
@Test
void whenFetchingUsersWithWrapper_thenReturnsApiResponse() {
// given
wireMockServer.stubFor(get(urlEqualTo("/users/wrapped"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"success": true,
"message": "Success",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"department": "Engineering"
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"department": "Marketing"
}
]
}
""")));
// when
Mono<ApiResponse<List<User>>> result = reactiveApiService.fetchUsersWithWrapper();
// then
StepVerifier.create(result)
.assertNext(response -> {
assertTrue(response.success());
assertEquals("Success", response.message());
assertEquals(2, response.data().size());
assertEquals("John Doe", response.data().get(0).getName());
assertEquals("Jane Smith", response.data().get(1).getName());
// Verify proper generic typing - this ensures ParameterizedTypeReference worked
List users = response.data();
User firstUser = users.get(0);
assertEquals(Long.valueOf(1L), firstUser.getId());
assertEquals("Engineering", firstUser.getDepartment());
})
.verifyComplete();
}5. 最佳實踐
在生產應用程序中工作時,使用ParameterizedTypeReference時,遵循某些最佳實踐可以提高性能和可維護性。
讓我們探索這些最佳實踐。我們涵蓋的策略專注於優化我們的代碼並優雅地處理錯誤。
5.1. 使用 ParameterizedTypeReference 的時機
理解何時使用 ParameterizedTypeReference 對於編寫清晰的代碼至關重要。它並非在每次 HTTP 調用中都必需的。此外,不必要的應用會增加複雜性。
我們應該在以下情況下使用 ParameterizedTypeReference:
- 處理泛型集合(List<T>, Set<T>, Map<K, V>)
- 處理自定義泛型包裝類(ApiResponse<T>)
- 處理嵌套泛型類型(Map<String, List<User>>)
我們應該避免在以下情況下使用 ParameterizedTypeReference:
- 處理簡單的非泛型類型
- 響應是一個單一對象,且不包含泛型
- 使用基本類型或它們的包裝器
下面是一些避免使用的示例:
public User fetchUser(Long id) {
return restTemplate.getForObject(baseUrl + "/api/users/" + id, User.class);
}
public User[] fetchUsersArray() {
return restTemplate.getForObject(baseUrl + "/api/users", User[].class);
}直觀地看,我們就能明白為什麼ParameterizedTypeReference在fetchUser()方法中是不需要的。 類似於上述解釋,該方法返回一個簡單的對象,User。 此外,這也適用於數組類型。 因此,我們也不需要為fetchUsersArray()方法使用ParameterizedTypeReference。
另一方面,讓我們來看一個需要它的例子:
public List<User> fetchUsersList() {
ParameterizedTypeReference<List<User>> typeRef =
new ParameterizedTypeReference<List<User>>() {};
ResponseEntity<List<User>> response = restTemplate.exchange(
baseUrl + "/api/users",
HttpMethod.GET,
null,
typeRef
);
return response.getBody();
}在上面的示例中,我們需要使用 ParameterizedTypeReference,因為我們正在處理一個泛型集合,具體是 List<User>。
我們的主要結論總結如下: 關鍵區別在於類型擦除是否會影響我們的用例。如果 Java 運行時可以確定類型而無需泛型信息,則 ParameterizedTypeReference 是不必要的。
5.2. 重用類型引用
創建 <em>ParameterizedTypeReference</em> 實例會產生性能開銷。因此,對於經常使用的類型,應創建 <em>靜態</em> 實例:
public class TypeReferences {
public static final ParameterizedTypeReference<List<User>> USER_LIST =
new ParameterizedTypeReference<List<User>>() {};
public static final ParameterizedTypeReference<Map<String, List<User>>> USER_MAP =
new ParameterizedTypeReference<Map<String, List<User>>>() {};
}以下是一個使用靜態實例的示例:
public List<User> fetchUsersListWithExistingReference() {
ResponseEntity<List<User>> response =
restTemplate.exchange(baseUrl + "/api/users", HttpMethod.GET, null, USER_LIST);
return response.getBody();
}6. 結論
在本文中,我們探討了如何使用ParameterizedTypeReference來處理 Java 應用程序中的複雜泛型類型。此外,我們還看到了這種方法如何解決類型擦除問題,並使我們能夠無縫地與泛型集合進行交互。
ParameterizedTypeReference在與泛型類型一起工作時至關重要,尤其是在 Spring HTTP 客户端中。它與 RestTemplate 和 WebClient 均兼容,並重用類型引用可以提高性能。
因此,通過遵循這些模式,我們可以編寫更健壯和易於維護的代碼,以處理 Java 應用程序中的泛型類型。