SpringBoot自定義注解的5個(gè)實(shí)戰(zhàn)案例分享
自定義注解是一種強(qiáng)大的元編程工具,允許在不修改原有代碼邏輯的情況下,為程序添加額外的功能。通過(guò)AOP(面向切面編程)與自定義注解的結(jié)合,我們可以實(shí)現(xiàn)關(guān)注點(diǎn)分離,讓業(yè)務(wù)代碼更加清晰簡(jiǎn)潔。
自定義注解有哪些好處?
- 代碼復(fù)用:將通用邏輯封裝到注解中
- 業(yè)務(wù)解耦:橫切關(guān)注點(diǎn)與核心業(yè)務(wù)邏輯分離
- 聲明式編程:通過(guò)注解配置行為,代碼更直觀
- 可維護(hù)性:通用邏輯集中管理,修改更方便
自定義注解的原理
Spring Boot 自定義注解的底層原理主要依賴(lài)于:
- Java 注解機(jī)制(@interface 定義注解)
- AOP(面向切面編程) 或 攔截器 結(jié)合反射來(lái)解析注解
- Spring 容器在運(yùn)行時(shí)自動(dòng)識(shí)別和織入邏輯
自定義注解的實(shí)現(xiàn)步驟
引入依賴(lài)
首先確保pom.xml中包含必要的依賴(lài):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
定義自定義注解
以一個(gè)最基礎(chǔ)的自定義注解為例:
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 注解作用目標(biāo):方法
@Retention(RetentionPolicy.RUNTIME) // 運(yùn)行時(shí)生效
@Documented
public @interface MyAnnotation {
String value() default "default";
}
注解說(shuō)明:
@Target:指定注解作用的范圍(類(lèi)、方法、字段、參數(shù)…)@Retention:指定注解生命周期(源碼、編譯期、運(yùn)行時(shí))@Documented:生成 Javadoc 時(shí)包含注解信息
常見(jiàn)的自定義注解案例
下面博主講完整演示幾個(gè)日常開(kāi)發(fā)中我們常見(jiàn)的自定義注解案例來(lái)讓大家深入的了解
自定義日志注解
定義注解
效果:調(diào)用接口時(shí),自動(dòng)打印方法耗時(shí)和相關(guān)日志
/**
* 方法日志注解
* 用于自動(dòng)記錄方法入?yún)?、出參和?zhí)行時(shí)間
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodLog {
String value() default "";
boolean printArgs() default true;
boolean printResult() default true;
boolean timing() default true;
}
切面實(shí)現(xiàn)
這里僅僅以打印輸出為案例,實(shí)際生產(chǎn)環(huán)境中,小伙伴們可以結(jié)合數(shù)據(jù)庫(kù)、日志系統(tǒng)等將信息記錄入庫(kù)
@Aspect
@Component
@Slf4j
public class MethodLogAspect {
@Around("@annotation(methodLog)")
public Object around(ProceedingJoinPoint joinPoint, MethodLog methodLog) throws Throwable {
String methodName = getMethodName(joinPoint);
String className = joinPoint.getTarget().getClass().getSimpleName();
// 記錄開(kāi)始時(shí)間
long startTime = System.currentTimeMillis();
if (methodLog.printArgs()) {
Object[] args = joinPoint.getArgs();
log.info("[{}#{}] 方法調(diào)用, 參數(shù): {}", className, methodName, Arrays.toString(args));
} else {
log.info("[{}#{}] 方法調(diào)用", className, methodName);
}
try {
Object result = joinPoint.proceed();
if (methodLog.printResult()) {
log.info("[{}#{}] 方法返回: {}", className, methodName, result);
}
if (methodLog.timing()) {
long cost = System.currentTimeMillis() - startTime;
log.info("[{}#{}] 方法執(zhí)行耗時(shí): {}ms", className, methodName, cost);
}
return result;
} catch (Exception e) {
log.error("[{}#{}] 方法執(zhí)行異常: {}", className, methodName, e.getMessage());
throw e;
}
}
private String getMethodName(ProceedingJoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
}
使用示例
以用戶(hù)接口為例,創(chuàng)建用戶(hù)的時(shí)候會(huì)記錄該接口會(huì)打印相關(guān)的信息日志
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping
@MethodLog(value = "創(chuàng)建用戶(hù)", printArgs = true, printResult = true, timing = true)
public User createUser(@RequestBody User user) {
// 業(yè)務(wù)邏輯
return userService.save(user);
}
@GetMapping("/{id}")
@MethodLog("根據(jù)ID查詢(xún)用戶(hù)")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
自定義參數(shù)校驗(yàn)注解
通常我們?cè)?code>Controller中進(jìn)行數(shù)據(jù)校驗(yàn)都是用validation, 可以大大節(jié)省我們參數(shù)校驗(yàn)的時(shí)間,雖然validation 默認(rèn)的注解已經(jīng)足以應(yīng)付我們工作中大部分場(chǎng)景,但還是會(huì)有一些參數(shù)校驗(yàn)有其它的一些驗(yàn)證要求,那么就可以用到自定義參數(shù)校驗(yàn)注解。
你也查閱博主之前寫(xiě)的 【Spring Boot數(shù)據(jù)校驗(yàn)validation實(shí)戰(zhàn):寫(xiě)少一半代碼,還更優(yōu)雅!】學(xué)習(xí)Spring Boot數(shù)據(jù)校驗(yàn)
定義注解
效果:提交手機(jī)號(hào)不合法時(shí),自動(dòng)拋出校驗(yàn)異常
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class) // 綁定校驗(yàn)器
public @interface Phone {
String message() default "手機(jī)號(hào)格式錯(cuò)誤";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
實(shí)現(xiàn)校驗(yàn)器
這里就簡(jiǎn)單驗(yàn)證一下是否正確的手機(jī)號(hào),小伙伴們可以加入自己需要的驗(yàn)證邏輯,比如僅限移動(dòng)用戶(hù)等
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.matches("^1[3-9]\\d{9}$");
}
}
使用示例
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
@RestController
public class RegisterController {
@PostMapping("/register")
public String register(@Valid @RequestBody UserDTO userDTO) {
return "注冊(cè)成功";
}
public static class UserDTO {
@NotBlank
private String name;
@Phone
private String phone;
// getter/setter
}
}
自定義權(quán)限校驗(yàn)注解
本次我們模擬Spring Security中的@PreAuthorize注解,想完整學(xué)習(xí)@PreAuthorize注解用法的小伙伴可以參考博主Spring Security專(zhuān)欄下的 【最新Spring Security實(shí)戰(zhàn)教程(七)方法級(jí)安全控制@PreAuthorize注解的靈活運(yùn)用】
這里我們就模擬一下全縣校驗(yàn)的功能
定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
String value(); // 權(quán)限標(biāo)識(shí)
}
實(shí)現(xiàn) AOP 權(quán)限校驗(yàn)
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class PermissionAspect {
@Before("@annotation(checkPermission)")
public void check(JoinPoint joinPoint, CheckPermission checkPermission) {
String requiredPermission = checkPermission.value();
// 模擬從上下文獲取當(dāng)前用戶(hù)權(quán)限
String userPermission = "USER";
if (!userPermission.equals(requiredPermission)) {
throw new RuntimeException("權(quán)限不足,缺少:" + requiredPermission);
}
}
}
使用示例
@RestController
public class AdminController {
@CheckPermission("ADMIN")
@GetMapping("/admin")
public String adminPage() {
return "管理員頁(yè)面";
}
}
自定義分布式限流注解
定義注解
/**
* 限流注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "";
int limit() default 100;
int timeWindow() default 60; // 時(shí)間窗口,單位:秒
String message() default "訪問(wèn)過(guò)于頻繁,請(qǐng)稍后再試";
}
切面實(shí)現(xiàn)
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
private final Map<String, RateLimiter> limiterMap = new ConcurrentHashMap<>();
@Before("@annotation(rateLimit)")
public void rateLimitCheck(RateLimit rateLimit) {
String key = generateKey(rateLimit);
RateLimiter limiter = limiterMap.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.limit() / (double) rateLimit.timeWindow()));
if (!limiter.tryAcquire()) {
throw new RuntimeException(rateLimit.message());
}
}
private String generateKey(RateLimit rateLimit) {
String key = rateLimit.key();
if (StringUtils.isEmpty(key)) {
// 可以結(jié)合用戶(hù)信息、IP等生成唯一key
return "rate_limit:" + System.identityHashCode(rateLimit);
}
return "rate_limit:" + key;
}
}
// 簡(jiǎn)單的令牌桶限流器實(shí)現(xiàn)
class RateLimiter {
private final double capacity;
private final double refillTokensPerOneMillis;
private double availableTokens;
private long lastRefillTimestamp;
public static RateLimiter create(double permitsPerSecond) {
return new RateLimiter(permitsPerSecond);
}
private RateLimiter(double permitsPerSecond) {
this.capacity = permitsPerSecond;
this.refillTokensPerOneMillis = permitsPerSecond / 1000.0;
this.availableTokens = permitsPerSecond;
this.lastRefillTimestamp = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
refill();
if (availableTokens < 1) {
return false;
}
availableTokens -= 1;
return true;
}
private void refill() {
long currentTime = System.currentTimeMillis();
if (currentTime > lastRefillTimestamp) {
long millisSinceLastRefill = currentTime - lastRefillTimestamp;
double refill = millisSinceLastRefill * refillTokensPerOneMillis;
this.availableTokens = Math.min(capacity, availableTokens + refill);
this.lastRefillTimestamp = currentTime;
}
}
}
使用示例
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/public/data")
@RateLimit(limit = 10, timeWindow = 60, message = "接口調(diào)用頻率超限")
public ApiResponse getPublicData() {
return ApiResponse.success("公開(kāi)數(shù)據(jù)");
}
@PostMapping("/submit")
@RateLimit(key = "submit_limit", limit = 5, timeWindow = 30)
public ApiResponse submitData(@RequestBody Data data) {
// 處理提交
return ApiResponse.success("提交成功");
}
}
自定義加解密注解
可參考博主之前寫(xiě)的 【Spring Boot中整合Jasypt 使用自定義注解+AOP實(shí)現(xiàn)敏感字段的加解密】進(jìn)行學(xué)習(xí),這里就不再贅述了!
總結(jié)
以上通過(guò)5個(gè)案例演示,完整講解了Spring Boot 自定義注解的使用,通過(guò)合理使用自定義注解,我們可以大幅提升代碼的可讀性、可維護(hù)性和復(fù)用性。在實(shí)際項(xiàng)目中,可以根據(jù)業(yè)務(wù)需求靈活組合和擴(kuò)展這些注解,構(gòu)建更加健壯和安全的應(yīng)用程序。
到此這篇關(guān)于SpringBoot自定義注解的5個(gè)實(shí)戰(zhàn)案例分享的文章就介紹到這了,更多相關(guān)SpringBoot自定義注解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實(shí)現(xiàn)自定義指標(biāo)監(jiān)控功能
本文主要介紹了SpringBoot實(shí)現(xiàn)自定義指標(biāo)監(jiān)控功能的實(shí)現(xiàn),,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,感興趣的小伙伴跟著著小編來(lái)一起來(lái)學(xué)習(xí)吧2024-01-01
Springboot2.x+ShardingSphere實(shí)現(xiàn)分庫(kù)分表的示例代碼
這篇文章主要介紹了Springboot2.x+ShardingSphere實(shí)現(xiàn)分庫(kù)分表的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10
Java實(shí)現(xiàn)動(dòng)態(tài)生成GIF圖像詳解
在互聯(lián)網(wǎng)上有許多有趣的場(chǎng)景,其中的一種就是動(dòng)圖。這不是視頻,而是一種GIF圖像信息。本文將利用Java實(shí)現(xiàn)動(dòng)態(tài)生成GIF圖像功能,需要的可以參考一下2022-09-09
java HashMap擴(kuò)容詳解及實(shí)例代碼
這篇文章主要介紹了java HashMap擴(kuò)容詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02
Java?常量池詳解之字符串常量池實(shí)現(xiàn)代碼
這篇文章主要介紹了Java?常量池詳解之字符串常量池,本文結(jié)合示例代碼對(duì)java字符串常量池相關(guān)知識(shí)講解的非常詳細(xì),需要的朋友可以參考下2022-12-12
Java實(shí)現(xiàn)ODT轉(zhuǎn)PDF的常用方案詳解
在?Java?開(kāi)發(fā)中,文檔格式轉(zhuǎn)換是一個(gè)常見(jiàn)的技術(shù)需求,ODT?是基于?XML?的開(kāi)放文檔格式標(biāo)準(zhǔn),下面我們就來(lái)看看如何使用Java實(shí)現(xiàn)ODT轉(zhuǎn)PDF吧2026-04-04
深入Sqlite多線(xiàn)程入庫(kù)的問(wèn)題
本篇文章是對(duì)Sqlite多線(xiàn)程入庫(kù)的問(wèn)題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05

