跳到主要内容Spring Boot 自定义注解实战:5 个高频案例详解 | 极客日志Javajava
Spring Boot 自定义注解实战:5 个高频案例详解
Spring Boot 自定义注解结合 AOP 实现业务解耦与代码复用。涵盖日志记录、参数校验、权限控制、分布式限流及加解密等五个核心场景。通过注解定义元数据,利用切面编程在运行时织入逻辑,避免硬编码,提升系统可维护性与扩展性。实际开发中可根据需求灵活组合,构建声明式编程风格的应用架构。
云间漫步7 浏览 
在 Java 开发中,Spring Boot 提供的各种注解如 @RestController、@Autowired、@Transactional 等极大地提高了开发效率。但面对业务中重复出现的逻辑时,自定义注解能进一步提升代码的优雅性和可维护性。
自定义注解是一种强大的元编程工具,允许在不修改原有代码逻辑的情况下,为程序添加额外功能。通过 AOP(面向切面编程)与自定义注解的结合,我们可以实现关注点分离,让业务代码更加清晰简洁。
自定义注解有哪些好处?
- 代码复用:将通用逻辑封装到注解中
- 业务解耦:横切关注点与核心业务逻辑分离
- 声明式编程:通过注解配置行为,代码更直观
- 可维护性:通用逻辑集中管理,修改更方便
自定义注解的原理
Spring Boot 自定义注解的底层原理主要依赖于:
- Java 注解机制(
@interface 定义注解)
- AOP(面向切面编程)或拦截器结合反射来解析注解
- Spring 容器在运行时自动识别和织入逻辑
自定义注解的实现步骤
引入依赖
首先确保 pom.xml 中包含必要的依赖:
<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>
定义自定义注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String value() default "default";
}
注解说明:
@Target:指定注解作用的范围(类、方法、字段、参数…)
@Retention:指定注解生命周期(源码、编译期、运行时)
@Documented:生成 Javadoc 时包含注解信息
常见的自定义注解案例
下面演示几个日常开发中常见的自定义注解案例,帮助大家深入理解。
❶ 自定义日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodLog {
String value() default "";
boolean printArgs() default true;
boolean printResult() default true;
boolean timing() default true;
}
这里以打印输出为案例,实际生产环境中可以结合数据库、日志系统将信息记录入库。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@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();
long startTime = System.currentTimeMillis();
if (methodLog.printArgs()) {
Object[] args = joinPoint.getArgs();
log.info("[{}#{}] 方法调用,参数:{}", className, methodName, Arrays.toString(args));
} else {
log.info("[{}#{}] 方法调用", 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("[{}#{}] 方法执行耗时:{}ms", className, methodName, cost);
}
return result;
} catch (Exception e) {
log.error("[{}#{}] 方法执行异常:{}", className, methodName, e.getMessage());
throw e;
}
}
private String getMethodName(ProceedingJoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
}
以用户接口为例,创建用户时会记录该接口的详细信息。
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping
@MethodLog(value = "创建用户", printArgs = true, printResult = true, timing = true)
public User createUser(@RequestBody User user) {
return userService.save(user);
}
@GetMapping("/{id}")
@MethodLog("根据 ID 查询用户")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
❷ 自定义参数校验注解
通常我们在 Controller 中进行数据校验都用 validation,虽然默认注解足以应付大部分场景,但遇到特殊验证要求时,可以用自定义参数校验注解。
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
这里简单验证是否正确的手机号,可以根据需要加入更多逻辑,比如仅限移动用户等。
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;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RegisterController {
@PostMapping("/register")
public String register(@Valid @RequestBody UserDTO userDTO) {
return "注册成功";
}
public static class UserDTO {
@NotBlank
private String name;
@Phone
private String phone;
}
}
❸ 自定义权限校验注解
模拟 Spring Security 中的 @PreAuthorize 注解,实现权限校验功能。
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
String value();
}
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();
String userPermission = "USER";
if (!userPermission.equals(requiredPermission)) {
throw new RuntimeException("权限不足,缺少:" + requiredPermission);
}
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AdminController {
@CheckPermission("ADMIN")
@GetMapping("/admin")
public String adminPage() {
return "管理员页面";
}
}
❹ 自定义分布式限流注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "";
int limit() default 100;
int timeWindow() default 60;
String message() default "访问过于频繁,请稍后再试";
}
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@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 (key == null || key.isEmpty()) {
return "rate_limit:" + System.identityHashCode(rateLimit);
}
return "rate_limit:" + key;
}
}
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;
}
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/public/data")
@RateLimit(limit = 10, timeWindow = 60, message = "接口调用频率超限")
public ApiResponse getPublicData() {
return ApiResponse.success("公开数据");
}
@PostMapping("/submit")
@RateLimit(key = "submit_limit", limit = 5, timeWindow = 30)
public ApiResponse submitData(@RequestBody Data data) {
return ApiResponse.success("提交成功");
}
}
❺ 自定义加解密注解
参考 Spring Boot 整合 Jasypt 库实现敏感字段的加解密,利用自定义注解+AOP 对特定字段进行加密存储和解密读取。具体实现思路与上述日志、权限注解类似,重点在于拦截 Setter 和 Getter 方法进行加解密操作。
总结
以上通过 5 个案例演示,完整讲解了 Spring Boot 自定义注解的使用。通过合理使用自定义注解,我们可以大幅提升代码的可读性、可维护性和复用性。在实际项目中,可以根据业务需求灵活组合和扩展这些注解,构建更加健壮和安全的应用程序。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online