蒼穹外賣技術鏈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方法的參數(實體對象)
目的:拿到需要填充公共字段的實體對象(如Employee、Category)。
核心代碼:
// 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);
// ... 其他方法省略
}
核心邏輯總結
- 註解標記:通過
@AutoFill明確哪些Mapper方法需要自動填充,以及操作類型。 - AOP攔截:切面類攔截帶註解的方法,在執行前觸發填充邏輯。
- 反射賦值:通過反射動態調用實體的setter方法,避免硬編碼(適配所有含公共字段的實體)。
- 上下文關聯:通過
ThreadLocal獲取當前登錄用户ID,確保createUser和updateUser正確。
通過以上步驟,新增/更新操作時無需手動設置公共字段,AOP會自動完成填充,減少重複代碼。
②AliyunOSS對象使用
一、配置文件:application.yml 和 application-dev.yml
Spring Boot的配置文件,用來存放項目的可配置參數(比如阿里雲OSS的密鑰、服務地址,數據庫連接信息等)。
application-dev.yml:開發環境專用配置,裏面配置了阿里雲OSS的具體參數(endpoint是阿里雲OSS的服務地址,accessKeyId和accessKeySecret是你的阿里雲賬號密鑰,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對象的創建和管理”。
整體流程總結
- 你在
application-dev.yml開發配置文件裏寫好阿里雲OSS的配置參數。 application.yml主配置文件引用開發配置文件裏的阿里雲OSS的配置參數AliOssProperties把這些參數“接過來”,存到自己的字段裏。OssConfiguration通過@Bean方法,用這些參數初始化AliOssUtil工具類,並交給Spring管理。- 以後業務代碼裏要上傳文件到阿里雲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切面邏輯提供數據支撐。
|
鏈式調用
|
目的與作用
|
獲得的結果
|
|
|
通過連接點簽名獲取方法名
|
目標方法的簡單名稱(如 |
|
|
通過簽名獲取方法所在類的全限定名(直接獲取字符串)
|
方法所在類的全限定類名(如 |
|
|
通過簽名先獲取類的 |
同上面,結果一致(類的全限定名)
|
|
|
通過簽名的類 |
方法所在類的簡單名稱(如 |
|
|
獲取方法的修飾符(如 |
修飾符的整數表示(需結合 |
|
|
通過目標對象(原始對象)獲取其類的全限定名
|
目標對象類的全限定名(如 |
|
|
通過目標對象獲取其類的簡單名稱
|
目標對象類的簡單名稱(如 |
|
|
通過代理對象獲取其類的全限定名(代理對象由AOP動態生成)
|
代理對象類的全限定名(如 |
|
|
通過代理對象獲取其類的簡單名稱
|
代理對象類的簡單名稱(如 |
|
|
獲取目標方法的參數數量
|
方法入參的個數(如調用 |
|
|
獲取目標方法第 |
第 |
|
|
通過連接點的靜態部分(包含切點靜態信息)獲取方法名
|
目標方法的簡單名稱(同 |
|
|
獲取簽名的簡短描述(包含方法名和參數簡略信息)
|
簽名的簡短字符串(如 |
|
|
獲取簽名的完整描述(包含修飾符、返回值類型、參數類型等)
|
簽名的完整字符串(如 |
核心用途場景
- 日誌記錄:通過
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 參數)。
- QueryString 參數(URL 中
?key=value形式)
- 若不使用
@RequestParam:後端接收的變量名必須和前端傳遞的 QueryString 鍵名一致。
例如:前端傳?userName=zhangsan,後端必須用String userName接收(變量名必須是userName)。 - 若使用
@RequestParam:可通過name屬性自定義映射,變量名可不同。
例如:前端傳?userName=zhangsan,後端可寫@RequestParam(name = "userName") String name(變量名是name,但映射到userName鍵)。
- 路徑參數(URL 中
/{佔位符}形式,如/user/{id})
- 若不使用
@PathVariable的value屬性:後端變量名必須和路徑佔位符名稱一致。
例如:URL 是/user/{userId},後端必須用@PathVariable String userId接收(變量名必須是userId)。 - 若指定
@PathVariable的value:可自定義映射,變量名可不同。
例如:URL 是/user/{userId},後端可寫@PathVariable("userId") String id(變量名是id,但映射到userId佔位符)。
- 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鍵)。
三、是否需要一致?關鍵在於傳輸的數據是否是 “鍵值對結構”
- 當傳輸的數據是 “鍵值對結構” 時(如 JSON 對象)無論是前端傳參還是後端返回,本質都是傳遞 “鍵(key)+ 值(value)” 的組合。此時前端 / 後端必須通過相同的 “鍵名” 才能正確匹配對應的值。
- 例如:後端返回
{"imgUrl": "https://xxx.jpg"}(JSON 對象,鍵名是imgUrl),前端必須用response.imgUrl接收(依賴鍵名匹配)。 - 這裏的 “後端變量名” 本質是用來生成 JSON 鍵名的(默認變量名即鍵名,可通過
@JsonProperty修改),前端的 “變量名” 其實是訪問 JSON 鍵名的方式,二者需通過 “鍵名” 關聯。
- 當傳輸的數據是 “無鍵名的簡單類型” 時(如字符串、數字、布爾值等)此時傳輸的是 “純值”,沒有鍵名,因此前端接收時的變量名可以任意定義,無需和後端的變量名一致。
- 例如:後端直接返回一個字符串
return "https://xxx.jpg";(注意:不是返回{"url": "xxx"}),此時 HTTP 響應體就是純字符串https://xxx.jpg,沒有鍵名。 - 前端接收時,只需把這個字符串賦值給任意變量即可,比如
const imageAddress = 後端返回的結果,這裏imageAddress和後端的變量名(比如後端可能用String imgUrl = "xxx")毫無關係,無需一致。
關鍵結論:
- 變量名是否需要一致,取決於傳輸的數據是否包含 “鍵名”:
- 有鍵名(如 JSON 對象、表單鍵值對):必須通過 “鍵名” 匹配(默認由變量名生成鍵名,可通過註解修改)。
- 無鍵名(如純字符串、數字等原始值):不存在鍵名,前端變量名可任意,與後端變量名無關。
總結:如果傳輸的是JSON格式的數據,則需要鍵名一致!
- 後端返回值:默認需要和前端接收的變量名一致(本質是 JSON 鍵名一致),可通過
@JsonProperty調整; - 後端接收參數:無論 QueryString、路徑參數還是 JSON 參數,默認都需要和前端傳遞的參數名(鍵名/佔位符)一致,可通過
@RequestParam、@PathVariable、@JsonProperty等註解自定義映射,無需嚴格一致。 - 變量名是否需要一致,取決於傳輸的數據是否包含 “鍵名”:
- 有鍵名(如 JSON 對象、表單鍵值對):必須通過 “鍵名” 匹配(默認由變量名生成鍵名,可通過註解修改)。
- 無鍵名(如純字符串、數字等原始值):不存在鍵名,前端變量名可任意,與後端變量名無關。
將單個變量快速轉換成集合!
new ArrayList<>(Arrays.asList(dish.getId()))