HarryTruman

 

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)    收藏  举报

导航