跳到主要内容Spring Boot 自定义注解的几种典型用法 | 极客日志Javajava
Spring Boot 自定义注解的几种典型用法
介绍 Spring Boot 中自定义注解结合 AOP 的实现方式,覆盖日志输出、参数校验、权限控制、单机限流和字段加解密等典型场景,给出关键注解定义及切面代码,并指出自实现令牌桶等方案的适用性与替代选择。
云间运维58 浏览 在 Spring Boot 里写业务时,重复逻辑总是让人头疼。自定义注解配合 AOP 是一种很直接的解决思路——日志打印、参数校验、权限检查这些横切关注点可以抽出来,既减少重复,也让核心代码干净一些。
注解本质上就是个标记(@interface),加上 AOP 或者拦截器,在运行时通过反射解析标记然后织入逻辑。Spring 容器会在启动时自动识别,不用手动注册。
准备工作
引入 AOP 和 Web 依赖。如果已经用了 Spring Boot 的 Web 场景,通常 AOP 额外加一下就行:
<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 决定生命周期(RUNTIME 才能在运行时反射读),@Documented 是标记它会被 Javadoc 带上。
下面整理几个实际用得上的场景。
日志注解
最常见的就是给方法自动打印入参、出参和耗时。比如调试的时候,不想每个方法都手写 log.info。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodLog {
String value() default "";
boolean printArgs() default true;
boolean printResult() default true;
boolean timing() default true;
}
@Aspect
@Component
@Slf4j
public class MethodLogAspect {
@Around("@annotation(methodLog)")
public Object around(ProceedingJoinPoint joinPoint, MethodLog methodLog) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
long startTime = System.currentTimeMillis();
if (methodLog.printArgs()) {
log.info("[{}#{}] 方法调用,参数:{}", className, methodName, Arrays.toString(joinPoint.getArgs()));
}
try {
Object result = joinPoint.proceed();
if (methodLog.printResult()) {
log.info("[{}#{}] 方法返回:{}", className, methodName, result);
}
if (methodLog.timing()) {
log.info("[{}#{}] 方法执行耗时:{}ms", className, methodName, System.currentTimeMillis() - startTime);
}
return result;
} catch (Exception e) {
log.error("[{}#{}] 方法执行异常:{}", className, methodName, e.getMessage());
throw e;
}
}
}
@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);
}
}
生产上用的时候,我会把输出替换成异步写日志文件或者发到 ELK,直接 System.out 或 log.info 在高频接口上不太扛得住。不过作为示意,这样足够。
定制参数校验
JSR 303 的校验注解比如 @NotNull、@Size 能覆盖大部分情况,但碰到特殊规则(比如手机号格式)就得自己写。
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 {};
}
校验器实现 ConstraintValidator:
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}$");
}
}
@RestController
public class RegisterController {
@PostMapping("/register")
public String register(@Valid @RequestBody UserDTO userDTO) {
return "注册成功";
}
public static class UserDTO {
private String name;
@Phone
private String phone;
}
}
如果校验逻辑比较复杂(比如需要查数据库),记得把 ConstraintValidator 注册为 Spring Bean,用 @Component 就行,否则依赖注入会失效。
权限控制
类似 @PreAuthorize 的效果,可以自己搞个轻量版,适合不想引入 Spring Security 的场景。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
String value();
}
@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);
}
}
}
示例中用户权限是硬编码的,真实项目里会从 SecurityContext 或请求头里取。
@RestController
public class AdminController {
@CheckPermission("ADMIN")
@GetMapping("/admin")
public String adminPage() {
return "管理员页面";
}
}
异常可以用 @ControllerAdvice 统一处理,返回更友好的提示。
简单限流
单机限流用注解实现也挺方便。这里写了个令牌桶的例子,用 ConcurrentHashMap 存每个 key 的限流器实例。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "";
int limit() default 100;
int timeWindow() default 60;
String message() default "访问过于频繁,请稍后再试";
}
@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)) {
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;
}
}
}
令牌桶的实现是线程安全的,但 RateLimiter 内部用了 synchronized,高并发下性能可能会受影响。本地限流通常够用,线上可以考虑 Guava 的 RateLimiter 或者直接上 Sentinel。
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/public/data")
@RateLimit(limit = 10, timeWindow = 60, message = "接口调用频率超限")
public ApiResponse getPublicData() {
return ApiResponse.success("公开数据");
}
}
key 缺省时用注解实例的哈希,同一注解不同方法会区分开。如果需要按用户限流,可以把 key 设为用户 ID,切面里再动态生成。
字段加解密
敏感字段入库前加密、出库后解密也是很常见的需求。思路类似:定义一个注解标记实体类的字段,通过 AOP 拦截 Setter/Getter 做处理。比如整合 Jasypt,用 @Encrypt 标识哪些字段需要加解密,这里就不贴具体代码了,整体结构和前面几个案例一致。
以上几个例子覆盖了日常开发中最常见的横切关注点。自定义注解本质上就是给方法或字段贴个标签,用 AOP 集中处理逻辑。代码量不大,但用好之后确实能让项目结构清晰不少,改起来也方便。
相关免费在线工具
- 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