AOP面向切面编程
位置
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com.itstudent.springbootdemo/
│ │ │ ├── aop/ # 在这创建aop文件夹,下面全是切面类
│ │ │ │ └── RecoreTimeAspect.java # 关于记录方法运行时间的切面类
需求①:记录方法各个参数
@Aspect // 标记切面类
@Component // 交给IOC容器管理
@Slf4j
public class LogAspect {
@Around("execution(* com.example.controller..*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis(); // 当前时间
String methodName = joinPoint.getSignature().toShortString(); // joinPoint获取方法签名
Object[] args = joinPoint.getArgs(); // 方法参数数组
log.info("调用接口:{},参数:{}", methodName, args);
Object result = joinPoint.proceed(); // 继续执行方法
long cost = System.currentTimeMillis() - start;
log.info("接口:{},执行耗时:{}ms,返回结果:{}", methodName, cost, result);
return result;
}
}
需求②:校验当前用户权限
当前端发来一项请求,需要执行某方法的时候,比如获取所有用户的信息。而用户信息只能由特定权限的管理员获取到,这里把特定权限用户名设置为 Harry 。此时需要做:
1、定义一个注解类,将方法使用@NeedPermission("Harry")的办法保护起来
位置:
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com.itstudent.springbootdemo/
│ │ │ ├── annotation/ # 在这创建注解文件夹,保存所有的注解
│ │ │ │ └── NeedPermission.java # 需要权限 注解
程序:
/**
* 自定义注解:写在方法上,权限校验
*/
@Target(ElementType.METHOD) // 适用范围:方法
@Retention(RetentionPolicy.RUNTIME) // 运行时
public @interface NeedPermission {
String value(); // 调用的时候 @NeedPermission("字符串")
}
2、创建一个切面类,切入点是所有含有@NeedPermission注解的方法
获取当前的用户名和@NeedPermission规定的用户名,比对再进行处理:放行&拦截
@Aspect
@Component
@Slf4j
public class PermissionAspect {
@Before("@annotation(needPermission)") // 方法前执行,切入点-注解形式
public void checkPermission(JoinPoint joinPoint, NeedPermission needPermission) {
String currentRole = "Harry1"; // 获取当前用户逻辑,现在是最小化测试
String requiredRole = needPermission.value(); // 获取注解值:Harry
if (currentRole.equals(requiredRole)) {
log.info("权限校验通过");
} else {
String str = "当前用户名为[" + currentRole + "],无权限访问,请联系管理员";
throw new RuntimeException(str);
}
}
}
当前的逻辑会抛出运行时异常并输出当前用户名为[Harry1],无权限访问,请联系管理员,因为当前用户是Harry1而合法的访问该方法的用户名是Harry,以后需要将获取当前用户的逻辑实现。
3、给需要的方法加上@NeedPermission注解
例如:
@GetMapping
@NeedPermission("Harry")
public R<User> findByName(String name) {
log.info("查找name:{}", name);
User user = userService.findByName(name);
return R.success(user);
}
需求③:@AutoFill自动填充字段
当前端传递用户的信息,需要存入到数据库的时候,经常需要手动设置些固定的字段:createTime、updateTime。这个时候,为了降低出错风险、保证数据一致性、实现关注点分离和方便未来的扩展,需要将这些重复的、未来有可能散落在Service层各处的set时间逻辑统一抽取出来。
想法:日后可以在mapper层插入数据库时,采用注解的形式@AutoFill()来说明这是一个需要插入时间属性的方法,利用切面类在执行方法前拦截并利用反射给对象赋上时间属性的值。
1、定义枚举类:
位置:com/itstudent/springbootdemo/enumeration/OperationType.java
配合AutoFill注解,规定值只能为 INSERT 或 UPDATE
public enum OperationType {
INSERT,UPDATE
}
2、定义注解
/**
* 标识某个方法需要进行功能字段自动填充
*/
@Target(ElementType.METHOD) // 使用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留注解
public @interface AutoFill {
OperationType value(); // 这是一个枚举类:INSERT, UPDATE
}
3、定义切面类测试(切入点的添加也有这种写法)
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 定义自动填充方法的切入点
*/
@Pointcut("execution(* com.itstudent.springbootdemo.mapper.*.*(..))" +
" && " +
"@annotation(com.itstudent.springbootdemo.annotation.AutoFill)")
public void autoFillPointCut() {};
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("测试测试测试测试测试测试测试测试测试测试测试测试测试");
}
}
4、切面实现
流程:
@AutoFill(INSERT) mapper方法上加注解,填入OperateType枚举类型
👇
AutoFillAspect类进行拦截,在方法运行前执行“通知”
👇
利用反射获取到参数-实体类对象,调用set方法插入当前时间
👇
执行mapper方法
切面的通知部分:
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
try {
// 1、获取方法注解内容 - 操作类型 operationType
// 这是 Spring AOP 中获取方法注解的唯一标准方式,只能通过 (MethodSignature) 强转得到 Signature 子接口
// MethodSignature 是 Signature 的子接口,专为方法设计,提供了获取 Method 对象的核心能力
// 需求包括:获取方法对象、返回类型、参数类型、参数名、注解
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
AutoFill autoFill = method.getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
// 2、获取传入的参数:实体类对象 entity
// 一般来说设置INSERT和UPDATE时参数只有一个 - 实体类,args[0]就是这个实体类
// 例如:joinPoint.getArgs() → [StudentDTO(id=1, studentNo=20210012, name=高坚果)]
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return;
}
Object entity = args[0];
// 判断
LocalDateTime now = LocalDateTime.now();
if (operationType == OperationType.INSERT) {
// insert:填充createtime和updatetime:反射调用set方法
Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
setCreateTime.invoke(entity, now);
setUpdateTime.invoke(entity, now);
} else if (operationType == OperationType.UPDATE) {
// update:填充updatetime:反射直接设置属性
Field updateTime = entity.getClass().getDeclaredField("updateTime");
updateTime.setAccessible(true);
updateTime.set(entity, now);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
关于填充字段,在INSERT和UPDATE的操作方式上我使用了两种逻辑:
① 使用 Field 直接赋值,优势:
- 通用性更强 - 不依赖 setter 方法是否存在
- 代码更简洁 - 不需要动态查找和调用方法
- 性能更好 - 避免了方法调用的开销
- 符合 AOP 切面的设计目标 - 无侵入式地处理字段填充
②调用set方法赋值,优势:
- 可以执行set方法中的校验逻辑
- 支持继承和多态
- 更好的调试性
posted on 2026-06-10 21:45 HarryTruman 阅读(3) 评论(0) 收藏 举报
浙公网安备 33010602011771号