反射 + AOP + 注解 讲解
目录
注解(Annotation)
- 什么是注解
- 元注解与自定义注解
- 如何在反射中读取注解
反射(Reflection)
- 反射的核心类:
Class、Method、Field - 动态创建对象、调用方法、访问字段
- 反射与注解的结合
- 反射的核心类:
AOP(面向切面编程)
- AOP 的核心概念
- Spring AOP 的实现原理(代理)
- 通知类型与切入点表达式
- 如何在切面中使用反射和注解
三剑合璧实战案例
- 案例1:自定义
@Log日志记录 - 案例2:自定义
@RequirePermission权限控制
- 案例1:自定义
- 总结与关系图谱
一、注解(Annotation)
1.1 什么是注解?
注解是 Java 5 引入的一种元数据,它本身不影响代码逻辑,但可以被编译、运行时工具或框架读取,从而赋予代码额外的语义或行为。
常见注解如 @Override、@Deprecated、@Autowired 等。
1.2 元注解
定义注解时需要用到的注解,共有四个:
| 元注解 | 作用 |
|---|---|
@Target | 指定注解能用在哪里(类、方法、字段等) |
@Retention | 指定注解的存活周期(源码、字节码、运行时) |
@Documented | 是否包含在 JavaDoc 中 |
@Inherited | 是否允许子类继承父类的注解 |
@Retention的取值:RetentionPolicy.SOURCE:只在源码中存在,编译时丢弃(如@Override)RetentionPolicy.CLASS:在字节码中存在,但运行时不可见(默认)RetentionPolicy.RUNTIME:运行时可通过反射读取(自定义注解必须选这个)
1.3 自定义一个注解
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface Loggable {
String value() default ""; // 注解属性
}使用:
@Loggable("删除用户")
public void deleteUser(Long id) { ... }1.4 在反射中读取注解
无论是类、方法还是字段,反射 API 都提供了读取注解的方法:
getAnnotation(Class<T> annotationClass)—— 获取指定类型的注解getAnnotations()—— 获取所有注解(含继承)getDeclaredAnnotations()—— 获取直接声明的注解(不含继承)isAnnotationPresent(Class<?> annotationClass)—— 判断是否存在某注解
示例:读取方法上的 @Loggable
Method method = ...; // 通过反射拿到方法对象
Loggable log = method.getAnnotation(Loggable.class);
if (log != null) {
System.out.println("描述:" + log.value());
}二、反射(Reflection)
2.1 反射的核心类
| 类 | 作用 | 获取方式 |
|---|---|---|
Class | 类的元信息 | 对象.getClass()、类名.class、Class.forName("全类名") |
Method | 方法信息 | Class 对象的 getMethod(...)、getDeclaredMethods() 等 |
Field | 字段信息 | Class 对象的 getField(...)、getDeclaredFields() 等 |
Constructor | 构造器信息 | Class 对象的 getConstructor(...)、getDeclaredConstructors() 等 |
2.2 获取 Class 对象的三种方式
// 1. 通过类名.class
Class<String> clz1 = String.class;
// 2. 通过实例的 getClass()
String s = "hello";
Class<?> clz2 = s.getClass();
// 3. 通过完全限定名字符串
Class<?> clz3 = Class.forName("java.lang.String");2.3 获取 Method 对象
| 方法 | 说明 |
|---|---|
getMethod(name, paramTypes...) | 获取指定名称和参数的 public 方法(含继承) |
getMethods() | 获取所有 public 方法(含继承) |
getDeclaredMethod(name, paramTypes...) | 获取本类中声明的方法(不含继承,可访问私有) |
getDeclaredMethods() | 获取本类中声明的所有方法(不含继承) |
示例:获取 deleteUser(Long) 方法
Method method = UserService.class.getMethod("deleteUser", Long.class);2.4 动态调用方法
UserService target = new UserService();
Method method = UserService.class.getMethod("deleteUser", Long.class);
Object result = method.invoke(target, 123L); // 相当于 target.deleteUser(123L)如果方法是私有的,需要先 setAccessible(true)。
2.5 反射与注解的结合
反射的 Method、Field、Class 等都实现了 AnnotatedElement 接口,因此可以直接读取注解。
Method method = ...;
if (method.isAnnotationPresent(Loggable.class)) {
Loggable log = method.getAnnotation(Loggable.class);
System.out.println("需要记录日志:" + log.value());
}应用场景:
- 框架(Spring、Hibernate)通过反射扫描注解,完成依赖注入、ORM 映射等。
- 自定义工具类,根据注解动态生成 SQL、校验参数等。
三、AOP(面向切面编程)
3.1 AOP 核心概念
- 切面(Aspect):封装横切逻辑的模块(比如日志、权限)。
- 连接点(JoinPoint):程序执行过程中的一个点(方法调用、字段访问等),Spring AOP 只支持方法连接点。
- 切入点(Pointcut):匹配连接点的表达式,决定哪些方法会被增强。
- 通知(Advice):在切入点处执行的动作(具体逻辑)。
- 目标对象(Target):被代理的原始对象。
- 代理(Proxy):包裹目标对象,加入切面逻辑后生成的对象。
3.2 Spring AOP 的实现
Spring AOP 基于动态代理:
- 如果目标类实现了接口,默认使用 JDK 动态代理(
java.lang.reflect.Proxy)。 - 如果没有接口,则使用 CGLIB 生成子类作为代理。
- 从 Spring Boot 2.0 开始,默认使用 CGLIB 代理(即使有接口也会用 CGLIB)。
3.3 通知类型与切入点表达式
通知类型
| 注解 | 说明 |
|---|---|
@Before | 目标方法执行前 |
@AfterReturning | 目标方法正常返回后 |
@AfterThrowing | 目标方法抛出异常后 |
@After | 目标方法结束后(类似 finally) |
@Around | 包围目标方法,可控制是否执行 |
常用切入点表达式
| 表达式 | 含义 |
|---|---|
execution(* com.example.service.*.*(..)) | 匹配 service 包下的所有方法 |
@annotation(com.example.Loggable) | 匹配带有 @Loggable 注解的方法 |
execution(public * *(..)) | 匹配所有 public 方法 |
within(com.example.service.*) | 匹配 service 包下的所有类的所有方法 |
3.4 切面中如何使用反射和注解
在 @Around 通知中,通过 ProceedingJoinPoint 可以获取方法签名(MethodSignature),然后通过反射拿到 Method 对象,进而读取注解。
@Around("@annotation(loggable)")
public Object log(ProceedingJoinPoint pjp, Loggable loggable) throws Throwable {
// 获得方法签名
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); // 反射 Method 对象
// 可以读取更多注解
AnotherAnnotation anno = method.getAnnotation(AnotherAnnotation.class);
// 执行原方法
return pjp.proceed();
}为什么要 MethodSignature?
因为 pjp.getSignature() 返回的是通用的 Signature 接口,只有强转成 MethodSignature 才能获取反射 Method 以及参数名、返回类型等详细信息。
3.5 JoinPoint 信息树(速查)
JoinPoint (ProceedingJoinPoint 继承它)
├─ getTarget() → 目标对象(被代理的原始 Bean)
├─ getThis() → 当前代理对象
├─ getArgs() → 方法参数值数组
├─ getSignature() → Signature (实际是 MethodSignature)
│ ├─ getName() → 方法名
│ ├─ getDeclaringTypeName() → 声明该方法的类全名
│ └─ (MethodSignature 特有)
│ ├─ getMethod() → 反射 Method ★
│ │ └─ getAnnotation(..) → 获取注解
│ ├─ getParameterNames() → 参数名数组(需编译配置)
│ ├─ getReturnType() → 返回类型
│ └─ getParameterTypes() → 参数类型数组
└─ getStaticPart() → 同 getSignature()四、三剑合璧实战案例
4.1 案例 1:@Log 注解实现方法日志记录
需求:在方法执行时自动打印入参、出参和耗时。
步骤:
- 自定义
@Log注解 - 编写切面
LogAspect,使用@Around+@annotation - 在切面中通过
MethodSignature获取方法名,通过getArgs()获取参数值
// 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log
// 切面
@Aspect
@Component
public class LogAspect {
@Around("@annotation(com.example.Log)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature sig = (MethodSignature) pjp.getSignature();
String method = sig.getDeclaringTypeName() + "." + sig.getName();
Object[] args = pjp.getArgs();
System.out.println(">>> 调用 " + method + ",参数:" + Arrays.toString(args));
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long time = System.currentTimeMillis() - start;
System.out.println("<<< 返回:" + result + " 耗时:" + time + "ms");
return result;
}
}
// 业务使用
@Service
public class UserService {
@Log
public String getUser(Long id) { return "User#" + id; }
}4.2 案例 2:@RequirePermission 权限控制
需求:标记方法需要某权限,没有则拒绝访问。
实现思路:
- 定义注解
@RequirePermission("user:delete") - 在拦截器中把用户权限存入
ThreadLocal - 切面中读取注解所需的权限值,与用户权限对比,不满足抛异常
// 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value();
}
// 权限上下文
public class UserContext {
private static final ThreadLocal<List<String>> PERMISSIONS = new ThreadLocal<>();
public static void set(List<String> p) { PERMISSIONS.set(p); }
public static List<String> get() {
return PERMISSIONS.get() != null ? PERMISSIONS.get() : Collections.emptyList();
}
public static void clear() { PERMISSIONS.remove(); }
}
// 切面
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(RequirePermission)")
public Object check(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature sig = (MethodSignature) pjp.getSignature();
Method method = sig.getMethod();
RequirePermission permission = method.getAnnotation(RequirePermission.class);
String required = permission.value();
if (!UserContext.get().contains(required)) {
throw new RuntimeException("无权限:" + required);
}
return pjp.proceed();
}
}
// 拦截器(简化)
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String user = request.getHeader("X-User");
List<String> permissions = loadPermissions(user); // 从数据库查
UserContext.set(permissions);
return true;
}
@Override
public void afterCompletion(...) { UserContext.clear(); }
}五、总结与关系图谱
三者关系:
- 注解 是标记,贴在代码上描述元数据。
- 反射 是读取这些标记和操作代码结构(类、方法)的能力。
- AOP 是利用反射机制,在运行时动态地将横切逻辑织入到带有特定注解(或符合表达式)的方法周围。
常见协作模式:
- 自定义一个注解(如
@Log) - 在 AOP 切面中,通过切入点表达式
@annotation(Log)拦截所有标注了该注解的方法 - 在切面通知方法里,利用
JoinPoint/ProceedingJoinPoint获取方法签名(MethodSignature) - 通过
MethodSignature.getMethod()拿到反射Method对象,再用getAnnotation()读取注解的具体值,实现差异化逻辑
学习路径建议:
- 先熟练 反射(
Class、Method、注解读取) - 理解 代理模式(JDK 动态代理、CGLIB)
- 掌握 Spring AOP 的基本用法(切入点表达式、通知类型)
- 最后将三者结合,实现自定义注解驱动切面
当你遇到如下代码时,大脑能快速解析:
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
Method method = sig.getMethod();
MyAnnotation ann = method.getAnnotation(MyAnnotation.class);它正在用 AOP 的连接点 获取 反射的 Method,然后 读取自定义注解,从而执行相应逻辑——这。
评论