Java注解與反射實戰(zhàn)之實現(xiàn)自定義日志與參數(shù)校驗注解
前言:為什么需要自定義注解?
在日常開發(fā)中,我們經(jīng)常遇到兩類重復工作:
- 日志記錄:每個重要方法都要寫 "開始執(zhí)行"、"參數(shù)是 xxx"、"執(zhí)行結(jié)束" 的代碼;
- 參數(shù)校驗:判斷輸入是否為 null、年齡是否在合理范圍、手機號格式是否正確等。
這些工作機械且冗余,而注解 + 反射正是解決這類問題的 "銀彈"—— 用注解標記需要處理的地方,用反射自動執(zhí)行邏輯,實現(xiàn) "一次定義,多處復用"。
本文將帶你從零實現(xiàn)兩個實用案例:
- 自定義日志注解
@Log:自動記錄方法調(diào)用細節(jié); - 自定義參數(shù)校驗注解
@NotNull、@Range:自動校驗方法參數(shù)合法性。
全程實戰(zhàn),代碼可直接運行,搭配圖解幫你吃透底層邏輯。
案例一:自定義日志注解@Log—— 自動記錄方法調(diào)用軌跡
需求分析
我們需要一個注解,標記在方法上后,能自動完成:
- 記錄方法開始執(zhí)行的時間;
- 打印方法參數(shù)(可選);
- 記錄方法執(zhí)行耗時;
- 打印返回結(jié)果(可選);
- 捕獲并記錄方法拋出的異常。
步驟 1:定義@Log注解
import java.lang.annotation.*;
// 只能標記在方法上
@Target(ElementType.METHOD)
// 運行時保留,允許反射獲取
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
// 操作描述(如"創(chuàng)建訂單")
String description() default "";
// 是否記錄參數(shù)
boolean recordParams() default true;
// 是否記錄返回值
boolean recordResult() default true;
// 是否記錄異常
boolean recordException() default true;
}
元注解說明:
@Target(ElementType.METHOD):限制注解僅用于方法(符合日志記錄的場景);@Retention(RetentionPolicy.RUNTIME):必須保留到運行時,否則反射無法獲取。
步驟 2:創(chuàng)建使用@Log注解的業(yè)務類
public class OrderService {
// 標記日志:記錄參數(shù)和返回值,描述為"創(chuàng)建訂單"
@Log(description = "創(chuàng)建訂單", recordParams = true, recordResult = true)
public String createOrder(String userId, double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("金額必須大于0");
}
// 模擬業(yè)務耗時
try { Thread.sleep(100); } catch (InterruptedException e) {}
return "ORDER_" + System.currentTimeMillis();
}
// 標記日志:不記錄返回值(無返回值),描述為"取消訂單"
@Log(description = "取消訂單", recordParams = true, recordResult = false)
public void cancelOrder(String orderId) {
if (orderId == null || orderId.isEmpty()) {
throw new IllegalArgumentException("訂單ID不能為空");
}
// 模擬業(yè)務耗時
try { Thread.sleep(50); } catch (InterruptedException e) {}
}
}
步驟 3:實現(xiàn)注解解析器(核心邏輯)
通過反射攔截方法調(diào)用,解析@Log注解并執(zhí)行日志記錄邏輯:
import java.lang.reflect.Method;
import java.util.Arrays;
public class LogAnnotationProcessor {
/**
* 執(zhí)行帶日志注解的方法
* @param target 目標對象
* @param methodName 方法名
* @param args 方法參數(shù)
* @return 方法返回值
*/
public static Object executeWithLog(Object target, String methodName, Object... args) {
try {
// 1. 獲取方法對應的Class對象和參數(shù)類型
Class<?>[] paramTypes = Arrays.stream(args)
.map(arg -> arg == null ? Object.class : arg.getClass())
.toArray(Class[]::new);
Method method = target.getClass().getMethod(methodName, paramTypes);
// 2. 檢查方法是否有@Log注解,無則直接執(zhí)行
if (!method.isAnnotationPresent(Log.class)) {
return method.invoke(target, args);
}
// 3. 解析注解屬性
Log logAnnotation = method.getAnnotation(Log.class);
String description = logAnnotation.description();
boolean recordParams = logAnnotation.recordParams();
boolean recordResult = logAnnotation.recordResult();
boolean recordException = logAnnotation.recordException();
// 4. 記錄方法開始日志
long startTime = System.currentTimeMillis();
System.out.println("\n===== 【日志開始】" + (description.isEmpty() ? methodName : description) + " =====");
if (recordParams) {
System.out.println("參數(shù):" + Arrays.toString(args));
}
// 5. 執(zhí)行目標方法(捕獲異常并記錄)
Object result;
try {
result = method.invoke(target, args);
} catch (Exception e) {
// 記錄異常信息
if (recordException) {
System.out.println("執(zhí)行異常:" + e.getCause().getMessage());
}
throw e; // 繼續(xù)拋出異常,不掩蓋業(yè)務邏輯
}
// 6. 記錄方法結(jié)束日志
long endTime = System.currentTimeMillis();
System.out.println("耗時:" + (endTime - startTime) + "ms");
if (recordResult) {
System.out.println("返回值:" + result);
}
System.out.println("===== 【日志結(jié)束】" + (description.isEmpty() ? methodName : description) + " =====");
return result;
} catch (Exception e) {
// 處理反射或業(yè)務異常(實際項目中可轉(zhuǎn)為自定義異常)
throw new RuntimeException("方法執(zhí)行失敗:" + e.getMessage(), e);
}
}
}
步驟 4:測試日志注解效果
public class LogTest {
public static void main(String[] args) {
OrderService orderService = new OrderService();
// 測試正常創(chuàng)建訂單
LogAnnotationProcessor.executeWithLog(orderService, "createOrder", "user_001", 99.9);
// 測試取消訂單
LogAnnotationProcessor.executeWithLog(orderService, "cancelOrder", "ORDER_123456");
// 測試異常場景(金額為負數(shù))
try {
LogAnnotationProcessor.executeWithLog(orderService, "createOrder", "user_002", -10.0);
} catch (Exception e) {
// 此處僅捕獲,不處理(異常已被日志記錄)
}
}
}
執(zhí)行結(jié)果
===== 【日志開始】創(chuàng)建訂單 =====
參數(shù):[user_001, 99.9]
耗時:101ms
返回值:ORDER_1698765432100
===== 【日志結(jié)束】創(chuàng)建訂單 ========== 【日志開始】取消訂單 =====
參數(shù):[ORDER_123456]
耗時:50ms
===== 【日志結(jié)束】取消訂單 ========== 【日志開始】創(chuàng)建訂單 =====
參數(shù):[user_002, -10.0]
執(zhí)行異常:金額必須大于0
===== 【日志結(jié)束】創(chuàng)建訂單 =====
日志注解執(zhí)行流程圖解

(注:核心展示流程節(jié)點:調(diào)用方法→反射檢查注解→記錄開始日志→執(zhí)行方法→記錄結(jié)束日志)
案例二:自定義參數(shù)校驗注解 —— 告別重復的 if-else
需求分析
我們需要兩個注解:
@NotNull:標記參數(shù)不能為 null;@Range:標記數(shù)值參數(shù)必須在指定范圍內(nèi)(如年齡 1-120 歲)。
實現(xiàn)效果:方法調(diào)用時自動校驗參數(shù),不符合規(guī)則則拋出明確的異常信息。
步驟 1:定義校驗注解
1.1@NotNull注解
import java.lang.annotation.*;
@Target(ElementType.PARAMETER) // 僅用于方法參數(shù)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
// 校驗失敗的提示信息
String message() default "參數(shù)不能為null";
}
1.2@Range注解
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
long min() default 0; // 最小值(包含)
long max() default Long.MAX_VALUE; // 最大值(包含)
String message() default "參數(shù)不在指定范圍內(nèi)";
}
步驟 2:創(chuàng)建使用校驗注解的業(yè)務類
public class UserService {
// 新增用戶:ID不能為空,年齡必須1-120歲
public void addUser(
@NotNull(message = "用戶ID不能為空") String userId,
@Range(min = 1, max = 120, message = "年齡必須在1-120歲之間") int age) {
System.out.println("新增用戶成功:userId=" + userId + ", age=" + age);
}
// 更新積分:積分必須≥0
public void updatePoints(
@NotNull String userId,
@Range(min = 0, message = "積分不能為負數(shù)") int points) {
System.out.println("更新積分成功:userId=" + userId + ", points=" + points);
}
}
步驟 3:實現(xiàn)參數(shù)校驗器(核心邏輯)
通過反射獲取方法參數(shù)上的注解,逐個校驗參數(shù)是否符合注解規(guī)則:
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class ParamValidator {
/**
* 校驗方法參數(shù)是否符合注解規(guī)則
* @param target 目標對象
* @param methodName 方法名
* @param args 方法參數(shù)
* @throws IllegalArgumentException 校驗失敗時拋出
*/
public static void validate(Object target, String methodName, Object... args) throws Exception {
// 1. 獲取方法對象
Class<?>[] paramTypes = Arrays.stream(args)
.map(arg -> arg == null ? Object.class : arg.getClass())
.toArray(Class[]::new);
Method method = target.getClass().getMethod(methodName, paramTypes);
// 2. 獲取方法參數(shù)列表(包含注解信息)
Parameter[] parameters = method.getParameters();
// 3. 遍歷參數(shù),執(zhí)行校驗
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
Object arg = args[i]; // 當前參數(shù)值
// 3.1 校驗@NotNull注解
if (param.isAnnotationPresent(NotNull.class)) {
NotNull notNull = param.getAnnotation(NotNull.class);
if (arg == null) {
throw new IllegalArgumentException(notNull.message());
}
}
// 3.2 校驗@Range注解(僅對數(shù)值類型生效)
if (param.isAnnotationPresent(Range.class) && arg instanceof Number) {
Range range = param.getAnnotation(Range.class);
long value = ((Number) arg).longValue(); // 轉(zhuǎn)為long統(tǒng)一處理
if (value < range.min() || value > range.max()) {
throw new IllegalArgumentException(range.message());
}
}
}
// 4. 校驗通過,執(zhí)行方法
method.invoke(target, args);
}
}
步驟 4:測試參數(shù)校驗效果
public class ValidatorTest {
public static void main(String[] args) {
UserService userService = new UserService();
// 測試正常參數(shù)
try {
ParamValidator.validate(userService, "addUser", "user_001", 25);
} catch (Exception e) {
System.out.println("錯誤:" + e.getMessage());
}
// 測試null參數(shù)(userId為null)
try {
ParamValidator.validate(userService, "addUser", null, 25);
} catch (Exception e) {
System.out.println("錯誤:" + e.getMessage());
}
// 測試年齡超出范圍(150歲)
try {
ParamValidator.validate(userService, "addUser", "user_002", 150);
} catch (Exception e) {
System.out.println("錯誤:" + e.getMessage());
}
// 測試積分負數(shù)
try {
ParamValidator.validate(userService, "updatePoints", "user_003", -10);
} catch (Exception e) {
System.out.println("錯誤:" + e.getMessage());
}
}
}
執(zhí)行結(jié)果
新增用戶成功:userId=user_001, age=25
錯誤:用戶ID不能為空
錯誤:年齡必須在1-120歲之間
錯誤:積分不能為負數(shù)
參數(shù)校驗流程圖解

實戰(zhàn)總結(jié):自定義注解的設(shè)計原則與優(yōu)化方向
1. 注解設(shè)計三要素
- 明確作用范圍:用
@Target嚴格限制注解的使用場景(如日志注解只用于方法);- 合理生命周期:需要反射解析的注解必須用
@Retention(RUNTIME);- 屬性精簡實用:只保留必要的屬性(如日志注解的
description、校驗注解的message),并設(shè)置合理默認值。
2. 反射解析優(yōu)化技巧
- 緩存反射結(jié)果:
Method、Parameter等對象的獲取有性能開銷,可通過ConcurrentHashMap緩存注解與方法的映射關(guān)系;- 批量處理注解:用
getAnnotations()一次性獲取所有注解,避免多次反射調(diào)用;- 異常友好提示:校驗或日志失敗時,異常信息要明確(如 "年齡必須在 1-120 歲之間" 而非 "參數(shù)錯誤")。
3. 與現(xiàn)有框架的對比
- 日志注解:Spring 的
@Log、Lombok 的@Log4j2功能更完善,但自定義注解可靈活適配業(yè)務需求(如對接特定日志系統(tǒng));- 參數(shù)校驗:JSR-303 規(guī)范(
javax.validation)提供了豐富的校驗注解,但自定義注解可實現(xiàn)框架不支持的特殊校驗(如手機號格式、身份證號規(guī)則)。
結(jié)語:注解 + 反射 = 代碼的 "隱形翅膀"
通過本文的兩個實戰(zhàn)案例,你應該體會到:
- 注解是 "聲明式編程" 的載體,讓代碼更簡潔、意圖更明確;
- 反射是注解的 "執(zhí)行引擎",讓標記轉(zhuǎn)化為實際邏輯。
這對組合在框架開發(fā)(如 Spring、MyBatis)中無處不在,掌握它們能讓你從 "使用者" 升級為 "設(shè)計者"。
動手練習:嘗試擴展本文案例 ——
- 給日志注解增加
level屬性(INFO/ERROR),實現(xiàn)不同級別日志的輸出; - 新增
@Pattern注解,校驗字符串是否符合正則表達式(如手機號、郵箱)。
總結(jié)
到此這篇關(guān)于Java注解與反射實戰(zhàn)之實現(xiàn)自定義日志與參數(shù)校驗注解的文章就介紹到這了,更多相關(guān)Java自定義日志與參數(shù)校驗注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實現(xiàn)公共字段自動填充的方法步驟
這篇文章主要介紹了SpringBoot實現(xiàn)公共字段自動填充的方法步驟,文中通過代碼示例講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2024-11-11
Spring Boot整合logback一個簡單的日志集成架構(gòu)
今天小編就為大家分享一篇關(guān)于Spring Boot整合logback一個簡單的日志集成架構(gòu),小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-01-01
Spring Cloud Gateway內(nèi)置的斷言和過濾器作用說明
這篇文章主要介紹了Spring Cloud Gateway內(nèi)置的斷言和過濾器作用說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06
Spring?@Transactional?注解從入門到避坑指南
文章主要講解了Spring的@Transactional注解的使用場景、核心屬性詳解、事務傳播行為、隔離級別、回滾配置、底層原理以及失效場景和避免之道,并還給出了最佳實踐,感興趣的朋友跟隨小編一起看看吧2026-05-05

