蒼穹外賣技術鏈Day03、04

枚舉常量記得全是大寫字母!用大寫字母去找枚舉類

①公共字段自動填充

我們做完員工管理模塊和分類管理模塊,發現這些模塊在修改的時候存在許多公共字段,且這些字段沒有具體的業務含義,且需要我們每次都手動添加這些字段,煩得很!那怎麼辦呢?公共字段自動填充!!!

  • createTime:記錄數據創建時間
  • updateTime:記錄數據最後更新時間
  • createUser:記錄數據創建人 ID(需關聯登錄用户)
  • updateUser:記錄數據最後更新人 ID(需關聯登錄用户)

實現步驟 – AOP技術

選擇哪種通知類型 ---->>>> 前置通知、增強Mapper方法

選擇哪種切入點表達式 ---->>>>@annotation

如何區分是插入還是更新 ---->>>> 因為插入的話要填充四個字段,而更新只需要填充兩個字段 ---->>>> 通過方法註解上的屬性值來判斷是新增還是更新

總結技術使用點:AOP + 自定義註解 +枚舉(限制屬性值為新增還是更新)+反射(因為我命中了這個方法後,我要去做字段填充,那我應該獲取到這個方法參數的對象,獲取到對象後,我要去調用這個對象裏面的Setter方法,用到反射!)

規定:自定義註解 AutoFile用於標識需要進行公共字段自動填充的方法;自定義切面類AutoFillAspect攔截加入了AutoFile註解的方法;在Mapper方法上加入AutoFile註解

以下是基於AOP+自定義註解+枚舉+反射實現公共字段自動填充的詳細步驟,聚焦核心實現邏輯:

步驟1:定義操作類型枚舉(OperationType)

用於區分當前操作是“新增”還是“更新”,限制註解屬性的取值範圍。

public enum OperationType {
    INSERT,  // 新增操作
    UPDATE   // 更新操作
}

步驟2:定義自定義註解@AutoFill

用於標記需要進行公共字段自動填充的Mapper方法,並通過value屬性指定操作類型(INSERT/UPDATE)。

import java.lang.annotation.*;

// 註解作用在方法上
@Target(ElementType.METHOD)
// 註解在運行時生效(AOP需要在運行時獲取註解信息)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    // 註解屬性:指定操作類型(默認空,必須顯式指定)
    OperationType value();
}

步驟3:實現切面類AutoFillAspect(核心邏輯)

通過AOP攔截帶有@AutoFill註解的Mapper方法,在方法執行前(前置通知)通過反射為公共字段賦值。

3.1 切面類結構説明
  • 切入點:攔截com.sky.mapper包下所有帶有@AutoFill註解的方法。
  • 通知類型@Before前置通知(在Mapper方法執行前填充字段,確保入庫時字段已賦值)。
3.2核心代碼邏輯:

以下是每個步驟對應的核心代碼實現,結合文字説明和代碼片段,清晰展示每一步的具體操作:

3.2.1:獲取註解中的操作類型(INSERT/UPDATE)

目的:通過註解判斷當前是新增還是更新操作,確定填充字段範圍。

核心代碼

// 1. 通過JoinPoint獲取方法簽名(包含方法信息)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();

// 2. 從方法簽名中獲取@AutoFill註解實例
AutoFill autoFillAnnotation = signature.getMethod().getAnnotation(AutoFill.class);

// 3. 從註解中提取操作類型(INSERT/UPDATE)
OperationType operationType = autoFillAnnotation.value();

説明

  • MethodSignature:是JoinPoint中獲取方法信息的工具類,可獲取方法的註解、參數類型等。
  • autoFillAnnotation.value():直接拿到註解中指定的操作類型(如OperationType.INSERT),用於後續邏輯分支判斷。
3.2.2:獲取Mapper方法的參數(實體對象)

目的:拿到需要填充公共字段的實體對象(如EmployeeCategory)。

核心代碼

// 1. 獲取當前方法的所有參數(數組形式)
Object[] args = joinPoint.getArgs();

// 2. 判空:無參數則無需處理
if (args == null || args.length == 0) {
    return;
}

// 3. 提取實體對象(默認第一個參數為實體,符合MyBatis Mapper常規寫法)
Object entity = args[0];

説明

  • MyBatis的Mapper方法通常將實體對象作為第一個參數(如insert(Employee employee)update(Category category)),因此直接取args[0]即可。
  • 若有特殊場景(如參數位置不同),可根據實際情況調整參數索引。
3.2.3:通過反射調用setter方法,按操作類型填充字段

目的:動態為實體的公共字段賦值(避免硬編碼,適配所有含公共字段的實體)。

1、 準備填充數據(通用)
// 1. 獲取當前時間(用於createTime/updateTime)
LocalDateTime now = LocalDateTime.now();

// 2. 獲取當前登錄用户ID(用於createUser/updateUser,基於ThreadLocal)
Long currentUserId = UserContext.getCurrentId();
2、 若為INSERT(新增)操作:填充4個字段
if (operationType == OperationType.INSERT) {
    try {
        // 1. 反射獲取實體的4個setter方法(方法名必須為set+字段名首字母大寫,參數類型匹配)
        Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
        Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
        Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
        Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

        // 2. 調用setter方法賦值
        setCreateTime.invoke(entity, now);       // 創建時間 = 當前時間
        setUpdateTime.invoke(entity, now);       // 更新時間 = 當前時間
        setCreateUser.invoke(entity, currentUserId);  // 創建人 = 當前用户ID
        setUpdateUser.invoke(entity, currentUserId);  // 更新人 = 當前用户ID
    } catch (Exception e) {
        // 實際開發中需捕獲異常(如方法不存在、參數不匹配)
        e.printStackTrace();
    }
}
3、 若為UPDATE(更新)操作:填充2個字段
else if (operationType == OperationType.UPDATE) {
    try {
        // 1. 反射獲取實體的2個setter方法
        Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
        Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

        // 2. 調用setter方法賦值
        setUpdateTime.invoke(entity, now);       // 更新時間 = 當前時間
        setUpdateUser.invoke(entity, currentUserId);  // 更新人 = 當前用户ID
    } catch (Exception e) {
        e.printStackTrace();
    }
}

説明

  • 反射方法getDeclaredMethod(String name, Class<?>... parameterTypes):需嚴格匹配方法名(如setCreateTime)和參數類型(如LocalDateTime.class),否則會拋出NoSuchMethodException
  • invoke(Object obj, Object... args):執行setter方法,為實體字段賦值(第一個參數是實體對象,第二個參數是字段值)。
3.3 代碼實現
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;

@Aspect  // 標記為切面類
@Component  // 交給Spring容器管理
public class AutoFillAspect {

    /**
     * 切入點:攔截com.sky.mapper包下所有帶有@AutoFill註解的方法
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointcut() {}

    /**
     * 前置通知:在Mapper方法執行前填充公共字段
     */
    @Before("autoFillPointcut()")
    public void autoFill(JoinPoint joinPoint) {
        // 1. 獲取當前方法的註解信息,確定操作類型(INSERT/UPDATE)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();  // 方法簽名
        AutoFill autoFillAnnotation = signature.getMethod().getAnnotation(AutoFill.class);  // 獲取方法上的@AutoFill註解
        OperationType operationType = autoFillAnnotation.value();  // 獲取操作類型

        // 2. 獲取Mapper方法的參數(實體對象)
        Object[] args = joinPoint.getArgs();  // 獲取方法參數數組
        if (args == null || args.length == 0) {
            return;  // 無參數則無需處理
        }
        Object entity = args[0];  // 假設第一個參數是需要填充的實體對象(符合MyBatis Mapper的常規寫法)

        // 3. 準備填充的數據
        LocalDateTime now = LocalDateTime.now();  // 當前時間
        Long currentUserId = UserContext.getCurrentId();  // 當前登錄用户ID

        // 4. 根據操作類型,通過反射調用實體的setter方法填充字段
        if (operationType == OperationType.INSERT) {
            // 新增操作:填充4個字段(createTime、updateTime、createUser、updateUser)
            try {
                // 獲取實體類的setter方法(方法名需嚴格匹配:set+字段名首字母大寫)
                Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

                // 調用setter方法賦值
                setCreateTime.invoke(entity, now);
                setUpdateTime.invoke(entity, now);
                setCreateUser.invoke(entity, currentUserId);
                setUpdateUser.invoke(entity, currentUserId);
            } catch (Exception e) {
                e.printStackTrace();  // 實際開發中需更規範的異常處理
            }
        } else if (operationType == OperationType.UPDATE) {
            // 更新操作:填充2個字段(updateTime、updateUser)
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentUserId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

步驟4:在Mapper接口方法上添加@AutoFill註解

在需要自動填充的Mapper方法(新增/更新)上添加註解,並指定操作類型。

import com.sky.annotation.AutoFill;
import com.sky.enumeration.OperationType;
import com.sky.entity.Employee;

public interface EmployeeMapper {

    // 新增員工:標記為INSERT操作,自動填充4個字段
    @AutoFill(OperationType.INSERT)
    void insert(Employee employee);

    // 更新員工:標記為UPDATE操作,自動填充2個字段
    @AutoFill(OperationType.UPDATE)
    void update(Employee employee);

    // ... 其他方法省略
}

核心邏輯總結

  1. 註解標記:通過@AutoFill明確哪些Mapper方法需要自動填充,以及操作類型。
  2. AOP攔截:切面類攔截帶註解的方法,在執行前觸發填充邏輯。
  3. 反射賦值:通過反射動態調用實體的setter方法,避免硬編碼(適配所有含公共字段的實體)。
  4. 上下文關聯:通過ThreadLocal獲取當前登錄用户ID,確保createUserupdateUser正確。

通過以上步驟,新增/更新操作時無需手動設置公共字段,AOP會自動完成填充,減少重複代碼。

②AliyunOSS對象使用

一、配置文件:application.ymlapplication-dev.yml

Spring Boot的配置文件,用來存放項目的可配置參數(比如阿里雲OSS的密鑰、服務地址,數據庫連接信息等)。

  • application-dev.yml開發環境專用配置,裏面配置了阿里雲OSS的具體參數(endpoint是阿里雲OSS的服務地址,accessKeyIdaccessKeySecret是你的阿里雲賬號密鑰,bucket-name是你在阿里雲創建的存儲桶名稱),還有數據庫的連接信息。
  • application.yml主配置文件,可以引用application-dev.yml的配置(比如${sky.alioss.endpoint}就是引用開發環境配置裏的阿里雲服務地址),實現“多環境配置”的複用。

二、屬性映射類:AliOssProperties.java

這個類的作用是把配置文件裏的參數“綁定”到Java類的字段上,方便代碼中統一獲取配置。

  • @Component:告訴Spring“把這個類當成一個組件,交給Spring管理”(Spring會創建這個類的對象,後續其他地方可以直接用)。
  • @ConfigurationProperties(prefix = "sky.alioss"):指定“配置文件中以sky.alioss開頭的屬性”要和這個類的字段一一對應。比如配置文件裏的sky.alioss.endpoint,會自動賦值給這個類的private String endpoint;字段。
  • 簡單説:這個類是“配置文件”和“Java代碼”之間的“翻譯官”,讓你不用在代碼裏硬寫配置,改配置只需要動配置文件即可。

三、配置類:OssConfiguration.java

這個類是專門用來配置“阿里雲OSS工具類”的初始化邏輯,讓Spring幫我們管理工具類的對象。

  • @Configuration:標記這是一個“配置類”,Spring會專門處理這裏的配置邏輯。
  • @Bean:這個註解加在方法上,表示“Spring要創建這個方法返回的對象,並交給自己管理”。
  • 方法aliOssUtil(AliOssProperties aliOssProperties):創建AliOssUtil(阿里雲OSS工具類)的實例,創建時需要傳入阿里雲的配置信息(這些信息來自AliOssProperties,而AliOssProperties已經是Spring管理的對象了,所以可以直接作為參數注入)。
  • 作用:以後任何地方需要用AliOssUtil操作阿里雲OSS(比如上傳文件),直接從Spring要這個對象就行,不用自己手動new和初始化了。

四、關鍵註解

  • @Component:“把這個類交給Spring管”,Spring會創建它的對象,後續可以“自動注入”到其他類中。
  • @ConfigurationProperties:“把配置文件的屬性和Java類的字段綁定”,實現配置的靈活讀取。
  • @Configuration:“這是個配置類,裏面可以定義很多創建Bean的規則”。
  • @Bean:“這個方法的返回值是個Bean,Spring要接管這個方法返回的Bean對象的創建和管理”。

整體流程總結

  1. 你在application-dev.yml開發配置文件裏寫好阿里雲OSS的配置參數。
  2. application.yml主配置文件引用開發配置文件裏的阿里雲OSS的配置參數
  3. AliOssProperties把這些參數“接過來”,存到自己的字段裏。
  4. OssConfiguration通過@Bean方法,用這些參數初始化AliOssUtil工具類,並交給Spring管理。
  5. 以後業務代碼裏要上傳文件到阿里雲OSS,直接注入AliOssUtil,調用它的方法即可。

這樣拆分後,每個部分的職責就清晰了:配置文件存參數→屬性類做映射→配置類初始化工具→業務代碼用工具。註解的作用就是讓Spring幫我們“自動化”管理這些對象和配置,減少手動操作的麻煩~

③函數式編程和SQL結合應用!

集合.forEach (id -> {通過 mapper 查詢 SQL 獲取數據;條件判斷;不符合則拋異常})

public void delete(List<Long> ids) {
//        TODO 如果菜品和套餐有關聯 不能刪
        ids.forEach(id -> {
            Integer count = setmealDishMapper.selectDishCount(id);
            if (count > 0) throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        });

//        TODO 如果菜品處於在售狀態 不能刪
        ids.forEach(id -> {
            Dish dish = dishMapper.selectById(id);
            if (dish.getStatus() == StatusConstant.ENABLE) throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
        });

        dishMapper.deleteDish(ids);
        dishFlavorMapper.deleteFlavors(ids);
    }

補充知識點:

①JoinPoint常用鏈式API

這些鏈式調用本質是通過 JoinPoint 逐層獲取連接點的細節信息(方法、類、參數、對象等),為AOP切面邏輯提供數據支撐。

鏈式調用

目的與作用

獲得的結果

joinPoint.getSignature().getName()

通過連接點簽名獲取方法名

目標方法的簡單名稱(如 addUser

joinPoint.getSignature().getDeclaringTypeName()

通過簽名獲取方法所在類的全限定名(直接獲取字符串)

方法所在類的全限定類名(如 com.demo.UserService

joinPoint.getSignature().getDeclaringType().getName()

通過簽名先獲取類的 Class 對象,再獲取全限定名

同上面,結果一致(類的全限定名)

joinPoint.getSignature().getDeclaringType().getSimpleName()

通過簽名的類 Class 對象獲取類的簡單名稱(不含包名)

方法所在類的簡單名稱(如 UserService

joinPoint.getSignature().getModifiers()

獲取方法的修飾符(如 publicstatic 等對應的整數常量)

修飾符的整數表示(需結合 java.lang.reflect.Modifier 解析)

joinPoint.getTarget().getClass().getName()

通過目標對象(原始對象)獲取其類的全限定名

目標對象類的全限定名(如 com.demo.UserService

joinPoint.getTarget().getClass().getSimpleName()

通過目標對象獲取其類的簡單名稱

目標對象類的簡單名稱(如 UserService

joinPoint.getThis().getClass().getName()

通過代理對象獲取其類的全限定名(代理對象由AOP動態生成)

代理對象類的全限定名(如 UserService$$EnhancerByCGLIB$$xxx

joinPoint.getThis().getClass().getSimpleName()

通過代理對象獲取其類的簡單名稱

代理對象類的簡單名稱(如 UserService$$EnhancerByCGLIB$$xxx 的簡短形式)

joinPoint.getArgs().length

獲取目標方法的參數數量

方法入參的個數(如調用 addUser("Tom", 20) 時返回 2

joinPoint.getArgs()[index]

獲取目標方法第 index 個參數的值(index 從0開始)

index 個入參的具體值(如上述例子中 index=0 時返回 "Tom"

joinPoint.getStaticPart().getSignature().getName()

通過連接點的靜態部分(包含切點靜態信息)獲取方法名

目標方法的簡單名稱(同 getSignature().getName()

joinPoint.getSignature().toShortString()

獲取簽名的簡短描述(包含方法名和參數簡略信息)

簽名的簡短字符串(如 void UserService.addUser(..)

joinPoint.getSignature().toLongString()

獲取簽名的完整描述(包含修飾符、返回值類型、參數類型等)

簽名的完整字符串(如 public void com.demo.UserService.addUser(java.lang.String, int)

核心用途場景
  • 日誌記錄:通過 getSignature().getName() 記錄方法名,getArgs()[index] 記錄參數值,getDeclaringTypeName() 記錄類名,實現完整的調用日誌。
  • 參數校驗:用 getArgs().length 判斷參數數量是否符合要求,用 getArgs()[index] 獲取參數值進行合法性校驗。
  • 權限控制:通過 getDeclaringTypeName()getName() 定位具體方法,結合權限規則判斷是否允許調用。
  • 調試與監控:通過 getThis().getClass().getName() 區分代理對象和原始對象,排查AOP代理相關問題。

要理解這些代碼和配置的作用,我們可以把它們拆分成配置文件、屬性映射類、配置類、工具類幾個部分,一步步搞清楚它們的職責和關聯,同時解釋關鍵註解的意義。

②前後端在接收或返回變量時,變量名是否需要一致問題

關於前後端變量名匹配的問題,核心取決於數據傳輸格式框架的默認映射規則,結合你的疑問具體分析如下:

一、後端返回值的變量名是否需要和前端接收的一致?

需要默認一致,但可通過註解調整

前後端交互通常使用 JSON 格式傳輸數據(如後端 Result.success(date) 本質是將數據序列化為 JSON 字符串返回)。JSON 是“鍵值對”結構,前端解析時會根據JSON 的鍵名獲取對應的值。

例如:

  • 後端返回的 JSON 是 {"createTime": "2025-10-30"}(假設 Result.success 中封裝的變量名是 createTime);
  • 前端需要用 response.createTime 才能拿到值(如果前端變量名寫成 create_date,則會獲取失敗)。

如果想讓後端變量名和前端接收名不同,可以通過序列化註解(如 Jackson 的 @JsonProperty)指定 JSON 鍵名。例如:
後端變量名是 createTime,但註解為 @JsonProperty("create_date"),則返回的 JSON 是 {"create_date": "2025-10-30"},前端用 response.create_date 接收即可。

二、後端接收前端參數時,變量名是否需要和前端一致?

默認需要一致,但可通過註解自定義映射,且該規則適用於所有參數類型(QueryString、路徑參數、JSON 參數)。

  1. QueryString 參數(URL 中 ?key=value 形式)
  • 若不使用 @RequestParam:後端接收的變量名必須和前端傳遞的 QueryString 鍵名一致。
    例如:前端傳 ?userName=zhangsan,後端必須用 String userName 接收(變量名必須是 userName)。
  • 若使用 @RequestParam:可通過 name 屬性自定義映射,變量名可不同。
    例如:前端傳 ?userName=zhangsan,後端可寫 @RequestParam(name = "userName") String name(變量名是 name,但映射到 userName 鍵)。
  1. 路徑參數(URL 中 /{佔位符} 形式,如 /user/{id}
  • 若不使用 @PathVariablevalue 屬性:後端變量名必須和路徑佔位符名稱一致。
    例如:URL 是 /user/{userId},後端必須用 @PathVariable String userId 接收(變量名必須是 userId)。
  • 若指定 @PathVariablevalue:可自定義映射,變量名可不同。
    例如:URL 是 /user/{userId},後端可寫 @PathVariable("userId") String id(變量名是 id,但映射到 userId 佔位符)。
  1. JSON 參數(請求體中的 JSON,通常是 POST 請求,Content-Type: application/json
  • 後端用 @RequestBody 接收對象時,默認要求對象的屬性名和 JSON 中的鍵名一致。
    例如:前端傳 JSON {"age": 18},後端接收對象必須有 private Integer age 屬性(屬性名必須是 age)。
  • 若想讓屬性名和 JSON 鍵名不同,可通過 @JsonProperty 註解指定。
    例如:前端傳 {"user_age": 18},後端對象屬性可寫 @JsonProperty("user_age") private Integer age(屬性名是 age,但映射到 user_age 鍵)。
三、是否需要一致?關鍵在於傳輸的數據是否是 “鍵值對結構”
  1. 當傳輸的數據是 “鍵值對結構” 時(如 JSON 對象)無論是前端傳參還是後端返回,本質都是傳遞 “鍵(key)+ 值(value)” 的組合。此時前端 / 後端必須通過相同的 “鍵名” 才能正確匹配對應的值。
  • 例如:後端返回 {"imgUrl": "https://xxx.jpg"}(JSON 對象,鍵名是imgUrl),前端必須用 response.imgUrl 接收(依賴鍵名匹配)。
  • 這裏的 “後端變量名” 本質是用來生成 JSON 鍵名的(默認變量名即鍵名,可通過@JsonProperty修改),前端的 “變量名” 其實是訪問 JSON 鍵名的方式,二者需通過 “鍵名” 關聯。
  1. 當傳輸的數據是 “無鍵名的簡單類型” 時(如字符串、數字、布爾值等)此時傳輸的是 “純值”,沒有鍵名,因此前端接收時的變量名可以任意定義,無需和後端的變量名一致。
  • 例如:後端直接返回一個字符串 return "https://xxx.jpg";(注意:不是返回{"url": "xxx"}),此時 HTTP 響應體就是純字符串https://xxx.jpg,沒有鍵名。
  • 前端接收時,只需把這個字符串賦值給任意變量即可,比如 const imageAddress = 後端返回的結果,這裏imageAddress和後端的變量名(比如後端可能用String imgUrl = "xxx")毫無關係,無需一致。

關鍵結論:

  • 變量名是否需要一致,取決於傳輸的數據是否包含 “鍵名”:
  • 有鍵名(如 JSON 對象、表單鍵值對):必須通過 “鍵名” 匹配(默認由變量名生成鍵名,可通過註解修改)。
  • 無鍵名(如純字符串、數字等原始值):不存在鍵名,前端變量名可任意,與後端變量名無關。

總結:如果傳輸的是JSON格式的數據,則需要鍵名一致!

  1. 後端返回值:默認需要和前端接收的變量名一致(本質是 JSON 鍵名一致),可通過 @JsonProperty 調整;
  2. 後端接收參數:無論 QueryString、路徑參數還是 JSON 參數,默認都需要和前端傳遞的參數名(鍵名/佔位符)一致,可通過 @RequestParam@PathVariable@JsonProperty 等註解自定義映射,無需嚴格一致。
  3. 變量名是否需要一致,取決於傳輸的數據是否包含 “鍵名”:
  • 有鍵名(如 JSON 對象、表單鍵值對):必須通過 “鍵名” 匹配(默認由變量名生成鍵名,可通過註解修改)。
  • 無鍵名(如純字符串、數字等原始值):不存在鍵名,前端變量名可任意,與後端變量名無關。

將單個變量快速轉換成集合!

new ArrayList<>(Arrays.asList(dish.getId()))