SpringBoot泛型封裝的坑
前言
CRUD 寫多了才發(fā)現(xiàn):泛型用對了是神器,用錯了是噩夢。在
寫業(yè)務代碼時,泛型是我們每天都在用的東西:
Result<List<UserDTO>> getUsers(); BaseService<User, UserDTO> service; Response<R<List<Order>>> orders();
看起來很標準,但實際項目中,泛型相關的坑一踩一個準:
- 明明定義了泛型上限,序列化時卻變成了
LinkedHashMap - 泛型擦除導致
instanceof判斷失效 - 泛型方法里
new T()報編譯錯誤 - 工具類封裝時泛型參數(shù)對不上
- ……
今天盤一盤泛型封裝中 8 個高頻踩坑點,看完直接落地。
1. 坑一:泛型擦除——instanceof判斷永遠為 false
常見寫法
public class Response<T> {
private T data;
public boolean isList() {
// 以為是 List 就返回 true?
return data instanceof List; // 永遠 false!
}
}
問題在哪
運行期泛型會被擦除為 Object,List<T> 會被擦除成 List,根本不存在 List<String> 這種具體類型。
所以 data instanceof List<String> 語法上就是錯的,編譯器直接報錯。
正確做法
方案一:通過傳入 Class 參數(shù)判斷
public class Response<T> {
private T data;
private Class<T> clazz;
public Response(Class<T> clazz) {
this.clazz = clazz;
}
public boolean isList() {
return List.class.isAssignableFrom(clazz);
}
}
?
// 使用
Response<List<UserDTO>> response = new Response<>(new TypeToken<List<UserDTO>>(){}.getType());
方案二:用 TypeReference 保留泛型信息(JSON 序列化場景)
public class Result<T> {
private T data;
// 配合 Jackson 使用
public static <T> Result<T> fromJson(String json, TypeReference<Result<T>> typeRef) {
try {
return new ObjectMapper().readValue(json, typeRef);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
?
// 調(diào)用
Result<List<UserDTO>> result = Result.fromJson(json,
new TypeReference<Result<List<UserDTO>>>() {});
2. 坑二:工具類封裝時泛型參數(shù)"對不上"
常見寫法
public class ResultUtil {
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setData(data);
result.setCode(200);
return result;
}
}
?
// 調(diào)用
List<UserDTO> users = userService.list();
Result<List<UserDTO>> result = ResultUtil.success(users); // 完美
這看起來沒問題。但如果這樣呢?
public static <T> T getData(Result<T> result) {
if (result.getCode() == 200) {
return result.getData(); // OK
}
return null; // 這里有問題嗎?
}
問題在哪
當 T 是基本類型包裝類(如 Integer、Long)時,返回 null 可能導致 NPE。
正確做法
public static <T> T getDataOrThrow(Result<T> result) {
if (result.getCode() != 200) {
throw new BusinessException(result.getMessage());
}
return result.getData(); // 非空,編譯器保證
}
?
// 或者返回空對象而非 null
public static <T> T getData(Result<T> result, T defaultValue) {
if (result.getCode() != 200) {
return defaultValue;
}
return result.getData();
}
3. 坑三:new T()永遠編譯不過
常見寫法
public class BaseService<T> {
public T createEntity() {
// 想動態(tài)創(chuàng)建實例
return new T(); // 編譯錯誤!
}
}
問題在哪
泛型擦除后,運行時根本不知道 T 是什么類型,無法調(diào)用構造函數(shù)。這是 Java 類型系統(tǒng)的限制。
正確做法
方案一:通過 Class 對象創(chuàng)建
public class BaseService<T> {
private final Class<T> entityClass;
public BaseService(Class<T> entityClass) {
this.entityClass = entityClass;
}
public T createEntity() {
try {
return entityClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("創(chuàng)建實例失敗", e);
}
}
}
?
// 使用
UserService userService = new UserService(User.class);
User user = userService.createEntity();
方案二:用反射工具類封裝(推薦)
public class BeanUtil {
public static <T> T newInstance(Class<T> clazz) {
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new IllegalStateException("無法創(chuàng)建實例: " + clazz.getName(), e);
}
}
public static <T> T copyProperties(Object source, Class<T> targetClass) {
T target = newInstance(targetClass);
BeanUtils.copyProperties(source, target);
return target;
}
}
4. 坑四:泛型上限沒設對,導致類型轉換 ClassCastException
常見寫法
// 隨便定義一個泛型
public class DataHolder<T> {
private T data;
public void process() {
// 假設需要調(diào)用 Comparable 方法
Comparable<T> comparable = data; // 可能出問題
comparable.compareTo(data); // 如果 T 是 String,OK;如果是 User?User 沒實現(xiàn) Comparable
}
}
問題在哪
沒有約束 T 的上限,任何類型都能傳,但代碼里可能需要特定能力(如 Comparable、Serializable)。
正確做法
明確泛型上限
// 限定 T 必須實現(xiàn) Comparable
public class DataHolder<T extends Comparable<T>> {
private T data;
public T max(T other) {
return data.compareTo(other) > 0 ? data : other; // 編譯器保證安全
}
}
?
// 限定 T 必須實現(xiàn)序列化
public class CacheWrapper<T extends Serializable> {
private T data;
}
?
// 限定多重上限
public class Processor<T extends Number & Comparable<T>> {
public double doubleValue(T value) {
return value.doubleValue();
}
}
常見場景:Service 層基類
// 基礎 Service,限定 entity 必須繼承 BaseEntity
public abstract class BaseService<T extends BaseEntity, DTO> {
protected abstract Mapper<T> getMapper();
public DTO getById(Long id) {
T entity = getMapper().selectById(id);
return convertToDTO(entity); // entity 一定有 id、createTime 等
}
protected abstract DTO convertToDTO(T entity);
}
?
// 子類實現(xiàn)
public class UserServiceImpl extends BaseService<User, UserDTO> {
@Override
protected Mapper<User> getMapper() {
return userMapper;
}
@Override
protected UserDTO convertToDTO(User user) {
// user 一定有 getId(),因為繼承了 BaseEntity
return UserDTO.builder()
.id(user.getId())
.name(user.getName())
.build();
}
}
5. 坑五:泛型方法定義錯誤,調(diào)用時類型推斷失敗
常見寫法
public class Converter {
// 以為是泛型方法,實際上不是
public static T convert(Object source) {
return (T) source; // 編譯警告,運行時可能 ClassCastException
}
}
?
// 調(diào)用
String str = Converter.convert(someObject); // 誰知道轉成啥?
問題在哪
這個 T 不是方法級別泛型,而是類級別泛型。如果類沒有聲明 <T>,這里的 T 就是普通的類型參數(shù)(雖然也能編譯,但語義完全錯誤)。
正確做法
正確的泛型方法
public class Converter {
// 正確的泛型方法:<T> 是方法聲明的一部分
public static <T> T convert(Object source, Class<T> targetClass) {
if (source == null) {
return null;
}
return targetClass.cast(source);
}
// 更安全的版本
public static <T, S> T convert(S source, Function<S, T> converter) {
if (source == null) {
return null;
}
return converter.apply(source);
}
}
?
// 使用
String str = Converter.convert(someObject, String.class);
UserDTO dto = Converter.convert(user, UserDTO::toDTO);
復雜場景:返回多種類型的泛型方法
// 業(yè)務場景:統(tǒng)一處理成功/失敗返回
public class ApiResult {
public static <T> T getOrThrow(ApiResponse<T> response) {
if (!response.isSuccess()) {
throw new ApiException(response.getCode(), response.getMessage());
}
return response.getData();
}
// 配合 Optional 使用
public static <T> Optional<T> toOptional(ApiResponse<T> response) {
if (response.isSuccess() && response.getData() != null) {
return Optional.of(response.getData());
}
return Optional.empty();
}
}
6. 坑六:泛型通配符? extends和? super傻傻分不清
常見寫法
// 讀取數(shù)據(jù)時用 extends
public void read(List<? extends Object> list) {
Object item = list.get(0); // 讀 OK
list.add(new Object()); // 寫?編譯錯誤
}
?
// 寫入數(shù)據(jù)時用 super
public void write(List<? super String> list) {
list.add("hello"); // 寫 OK
String item = list.get(0); // 讀?需要強制轉型
}
問題在哪
搞不清 PECS 原則(Producer Extends, Consumer Super):
- 讀取數(shù)據(jù)(生產(chǎn)者)→ 用
? extends - 寫入數(shù)據(jù)(消費者)→ 用
? super
正確做法
記住 PECS 原則
// 生產(chǎn)者:用 extends,只能讀
public double sumOfPrices(List<? extends Product> products) {
double total = 0;
for (Product p : products) { // 讀 OK
total += p.getPrice();
}
// products.add(new Product()); // 編譯錯誤,不能寫
return total;
}
?
// 消費者:用 super,只能寫
public void addNumbers(List<? super Integer> list) {
list.add(1); // 寫 OK
list.add(2);
// Integer num = list.get(0); // 讀出來是 Object
}
?
// 既讀又寫?別用通配符
public <T> void copy(List<T> dest, List<? extends T> src) {
for (T item : src) { // src 是生產(chǎn)者,可以讀
dest.add(item); // dest 是消費者,可以寫
}
}
實際業(yè)務場景
// DTO 轉換:源列表是生產(chǎn)者,目標列表是消費者
public <S, T> void convertList(List<S> sources, List<T> targets,
Function<S, T> converter) {
for (S source : sources) {
targets.add(converter.apply(source));
}
}
?
// 使用
List<User> users = userMapper.selectList();
List<UserDTO> dtos = new ArrayList<>();
convertList(users, dtos, User::toDTO);
7. 坑七:泛型與序列化沖突,返回給前端變成了 LinkedHashMap
常見寫法
public class Result<T> {
private T data;
// 序列化給前端
public String toJson() {
return new ObjectMapper().writeValueAsString(this);
}
}
?
// 接口
@GetMapping("/user")
public Result<UserDTO> getUser() {
Result<UserDTO> result = new Result<>();
result.setData(userDTO);
return result;
}
前端收到的 JSON:
{
"data": {
"name": "張三",
"id": 1
}
}
這看起來沒問題。但如果前端拿到的是 List<UserDTO> 呢?
問題在哪
當 T 是泛型集合時,Jackson 默認反序列化會丟失具體類型信息,反序列化成 LinkedHashMap 而不是具體 DTO。
// 后端
Result<List<UserDTO>> result = new Result<>();
result.setData(Arrays.asList(userDTO1, userDTO2));
?
// 前端收到
{
"data": [
{"name": "張三", "id": 1}, // 不再是 UserDTO
{"name": "李四", "id": 2}
]
}
正確做法
方案一:用 TypeReference 顯式指定泛型
public class Result<T> {
public String toJson() {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper.writeValueAsString(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 泛型反序列化方法
public static <T> T fromJson(String json, TypeReference<T> typeRef) {
try {
return new ObjectMapper().readValue(json, typeRef);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
?
// 后端給前端:直接序列化,不需要改動
// 前端拿到字符串后:
Result<List<UserDTO>> result = Result.fromJson(jsonString,
new TypeReference<Result<List<UserDTO>>>() {});
方案二:用 @JsonTypeInfo 標記具體類型
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public class Result<T> {
private T data;
}
?
// 序列化成
{
"data": {
"@c": ".UserDTO",
"name": "張三"
}
}
方案三:返回 ResponseEntity(Spring 官方推薦)
@GetMapping("/users")
public ResponseEntity<Result<List<UserDTO>>> getUsers() {
Result<List<UserDTO>> result = Result.success(userService.list());
return ResponseEntity.ok(result);
}
8. 坑八:泛型嵌套太深,代碼可讀性災難
常見寫法
// 四層泛型嵌套,你能一眼看出 data 是什么嗎? Response<Result<Page<List<UserDTO>>>> result = userService.query(request); ? // 訪問數(shù)據(jù)時 List<UserDTO> users = result.getData().getData().getData().getRecords();
問題在哪
泛型是為了類型安全,但如果嵌套太深,反而降低了可讀性,而且修改維護時容易出錯。
正確做法
方案一:抽取中間類型
// 第一層:接口返回統(tǒng)一封裝
public class ApiResponse<T> {
private int code;
private String message;
private T data;
}
?
// 第二層:分頁數(shù)據(jù)統(tǒng)一封裝
public class PageResult<T> {
private List<T> records;
private long total;
private int pageNum;
private int pageSize;
}
?
// 簡化后的調(diào)用
ApiResponse<PageResult<UserDTO>> result = userService.query(request);
PageResult<UserDTO> page = result.getData();
List<UserDTO> users = page.getRecords();
方案二:用 Optional 消除空判斷
public class Result<T> {
private T data;
public Optional<T> getOptionalData() {
return Optional.ofNullable(data);
}
}
?
// 使用
user.getOptionalData()
.map(PageResult::getRecords)
.orElse(Collections.emptyList());
方案三:工具方法封裝常用路徑
public class ResultHelper {
public static <T> List<T> getRecordsOrEmpty(Result<PageResult<T>> result) {
if (result == null || result.getData() == null) {
return Collections.emptyList();
}
return result.getData().getRecords();
}
}
?
// 調(diào)用
List<UserDTO> users = ResultHelper.getRecordsOrEmpty(result);
最佳實踐總結
| 坑點 | 問題 | 解決方案 |
|---|---|---|
| instanceof 失效 | 泛型擦除 | 用 Class 或 TypeReference 判斷 |
| 工具類泛型失效 | 泛型參數(shù)對不上 | 顯式傳入 Class 或用函數(shù)式接口 |
| new T() 編譯錯誤 | 類型擦除限制 | 通過 Class.newInstance() 或構造函數(shù)引用 |
| ClassCastException | 泛型上限未設 | 用 約束 |
| 類型推斷失敗 | 泛型方法定義錯誤 | 放在返回類型前 |
| extends/super 混淆 | PECS 原則不清 | 記?。鹤x用 extends,寫用 super |
| 序列化變成 Map | 泛型信息丟失 | 用 TypeReference 或 ResponseEntity |
| 泛型嵌套太深 | 可讀性差 | 抽取中間類型 + 工具方法封裝 |
泛型封裝黃金法則
1. 永遠不要 new T()
2. 永遠不要寫 instance of T
3. 永遠明確泛型上限
4. 永遠記住 PECS 原則
5. 永遠用 TypeReference 處理 JSON 序列化
記住:泛型是給編譯器用的,不是給運行時用的。想清楚這一點,大部分坑都能避開。
到此這篇關于SpringBoot泛型封裝的坑的文章就介紹到這了,更多相關SpringBoot泛型封裝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
關于@ComponentScan?TypeFilter自定義指定掃描bean的規(guī)則
這篇文章主要介紹了關于@ComponentScan?TypeFilter自定義指定掃描bean的規(guī)則,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09
Springboot實現(xiàn)多線程及線程池監(jiān)控
線程池的監(jiān)控很重要,本文就來介紹一下Springboot實現(xiàn)多線程及線程池監(jiān)控,文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧2024-01-01

