通知+注解的切点使用+AOP案例(2.25)

一、通知

1.通知的类型:

环绕通知:Around

前置通知:Before

后置通知:AfterReturning

异常通知:AfterThrowing

最终通知:After

2.代码实战:

CarController:

@RestController
@RequestMapping("/car")
public class CarController {

    @Autowired
    private CarService carService;

    @GetMapping("/all")
    @MyLog
    public List<Car> listGet() {
        List<Car> carList = null;
      	//出现异常,后面不执行
        //System.out.println(1/0);
     
        carList = carService.findAll();
        System.out.println("carList:"+carList);
       
        return carList;
    }

}

aop.TimeAspect:

@Component
@Aspect //当前类是个切面
public class TimeAspect {
    //1 切点逻辑 哪些类的哪些方法
    //execution([访问修饰符] 返回值类型 包名.类名.方法名(参数类型) [异常])
    //      任意返回值类型  包名.类名.方法名
    @Pointcut("execution(* cn.wolfcode.controller.*.*(..))")
    public void pointcut(){}

    //2 通知方法 Around 表示通知方式
    //@Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = null; //目标方法调用
        try {
            //1.前置通知 before
            pjp.proceed();
            //2.后置通知 afterreturning
        } catch (Throwable e) {
            e.printStackTrace();
            //3.异常通知 afterThrowing
        } finally {
            //4.最终通知 after
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end-start) + "ms");
        return result;
    }
    //@Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("======" + joinPoint.getSignature());
    }
    //@AfterReturning(pointcut = "pointcut()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("======" + joinPoint.getSignature());
        System.out.println("======" + result);
    }
    //@AfterThrowing(pointcut = "pointcut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("======" + e.getMessage());
    }
    //@After("pointcut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("======" + joinPoint.getSignature());
        System.out.println("======" + joinPoint.getTarget());
    }
}

BoosApplicationTests:

@SpringBootTest
class BoosApplicationTests {

    @Autowired
    private CarController carController;

    @Test
    void contextLoads() throws SQLException, FileNotFoundException {
        
        List<Car> carList = carController.listGet();

    }
}

(1)before通知:目标方法执行前执行

应用:参数校验、日志记录、权限检查

image-20260304153646890

(2)afterReturning:目标方法正常返回后执行,有异常不显示

应用:处理返回值、记录成功日志

例如在CarController中手写一个异常,就不会执行:

 public List<Car> listGet() {
        List<Car> carList = null;
      	//出现异常,后面不执行
        System.out.println(1/0);
     
        carList = carService.findAll();
        System.out.println("carList:"+carList);
       
        return carList;
    }

image-20260304154637348

(3)afterThrowing:目标方法抛出异常后

应用:处理异常、记录错误日志

①若有异常:显示异常信息

image-20260304160829230

②删除异常后,则正常执行:

image-20260304161128171

(4)after:目标方法执行后(无论成功或失败)

应用:资源释放、清理工作

有没有异常都显示,例如手写一个异常后

image-20260304155435313

为什么异常信息在下面?

这是因为标准输出流(stdout)和标准错误流(stderr)的输出顺序是独立的,它们在控制台的显示顺序不一定和代码执行顺序一致。

(5)around:包裹目标方法的整个执行过程

应用:性能统计、事务控制、缓存控制

总结:

@Around 能实现 @Before@AfterReturning@AfterThrowing@After 的所有功能,是功能最完整的通知类型;

3.切面类的执行顺序:

①单个切面内不同通知类型的执行顺序:

  • 目标方法正常执行(无异常)

@Around(执行前逻辑)→ @Before → 目标方法执行 → @Around(执行后逻辑)→ @AfterReturning → @After

  • 目标方法抛出异常

@Around(执行前逻辑)→ @Before → 目标方法执行(抛异常)→ @Around(异常捕获逻辑)→ @AfterThrowing → @After

例如:

// 无异常时控制台输出顺序
Around前置:开始计时
Before:打印方法签名
目标方法:执行list()
Around后置:计算耗时
AfterReturning:打印返回值
After:打印目标对象

// 有异常时控制台输出顺序
Around前置:开始计时
Before:打印方法签名
目标方法:执行list()(抛异常)
Around异常:捕获并打印异常
AfterThrowing:打印异常信息
After:打印目标对象

多个切面类之间的执行顺序:

当项目中有多个切面类(比如 LogAspect、TimeAspect、AuthAspect)时,默认执行顺序是不确定的(Spring 按 Bean 加载顺序执行),需要手动指定,有两种方式:

  • 使用 @Order 注解

例如:在切面类上添加@Order(数字),数字越小,切面越先执行:

@Order(1)
@Component
@Aspect
public class AuthAspect { // 权限校验切面
    @Before("execution(* cn.wolfcode.controller.*.*(..))")
    public void before() {
        System.out.println("1. 权限校验");
    }
}

// 后执行
@Order(2)
@Component
@Aspect
public class LogAspect { // 日志记录切面
    @Before("execution(* cn.wolfcode.controller.*.*(..))")
    public void before() {
        System.out.println("2. 日志记录");
    }
}
  • 实现 Ordered 接口

例如:切面类实现Ordered接口,重写getOrder()方法返回数字(数字越小越先执行):

@Component
@Aspect
public class TimeAspect implements Ordered {
    @Override
    public int getOrder() {
        return 3; // 最后执行
    }

    @Before("execution(* cn.wolfcode.controller.*.*(..))")
    public void before() {
        System.out.println("3. 耗时统计");
    }
}

多个切面的完整执行逻辑(无异常):

假设有 2 个切面:AuthAspect(@Order (1))、LogAspect(@Order (2)),每个切面都有 @Before 和 @After

AuthAspect@Before → LogAspect@Before → 目标方法 → LogAspect@After → AuthAspect@After

类似 “洋葱模型”:外层切面先执行前置,后执行后置

二、注解的切点使用:

1.当 AOP 通知需要作用于 controller 层全部方法时,可使用基于 execution 表达式的方式实现;

2.若想精准控制 AOP 通知作用于某一层的某个方法,或不同层的某些指定方法,就需要采用基于自定义注解的方式来实现。

3.代码实战:

①自定义注解annotation.MyLog:

@Target(ElementType.METHOD) //修饰方法 
@Retention(RetentionPolicy.RUNTIME) //运行时
@Documented //日志
public @interface MyLog {
}

②在CarController的listGet方法上贴上@MyLog注解:

@RestController
@RequestMapping("/car")
public class CarController {

    @Autowired
    private CarService carService;

    @GetMapping("/all")
    @MyLog
    public List<Car> listGet() {
        List<Car> carList = null;
        
        //System.out.println(1/0);
        
        carList = carService.findAll();
        System.out.println("carList:"+carList);
       
        return carList;
    }
}

③修改aop.TimeAspect:

@Component
@Aspect //当前类是个切面
public class TimeAspect {
    // 1. 封装注解切点
    @Pointcut("@annotation(cn.wolfcode.annotation.MyLog)")
    public void pointcut(){}

    // 2. 通知绑定封装后的切点
    @Before("pointcut()")
    public void before1(JoinPoint joinPoint) {
        System.out.println("===MyLog===" + joinPoint.getSignature());
    }
}

执行结果:

image-20260304171448181

三、AOP案例(日志):

1.日志:

  • 系统日志:程序在运行过程中自己产生的数据 --->修复bug --->写在控制台日志文件
  • 业务日志:用户行为产生的日志 --->DML操作都需要写日志,重要的查询、登录、登出也要写日志 --->存储在数据库表

无论哪种日志,都会只增加不减少,只有insert和select,不会有update和delete,因为用户的行为是不可变的

2.需求:

将案例中增、删、改相关接口的操作日志记录到数据库表中

  • 就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。

操作日志信息包含:

  • 操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

所记录的日志信息包括当前接口的操作人是谁操作的,什么时间点操作的,以及访问的是哪个类当中的哪个方法,在访问这个方法的时候传入进来的参数是什么,访问这个方法最终拿到的返回值是什么,以及整个接口方法的运行时长是多长时间。

3.分析:

可以使用AOP解决 ---> 环绕通知 ---> 使用annotation来描述表达式

4.代码实战:

①AOP起步依赖:

<!--AOP起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.46</version>
</dependency>

我们的项目中已经引入了Aop依赖,所以不必重新引入。

image-20260304231806087

②导入数据库表结构

create table operate_log(
    id bigint primary key auto_increment        comment 'ID',
    operate_user bigint     					comment '操作人',
    operate_time timestamp						comment '操作时间',
    class_name varchar(100) 					comment '操作的类名',
    method_name varchar(100) 					comment '操作的方法名',
    method_params varchar(1000) 				comment '方法参数',
    return_value varchar(2000) 					comment '返回值',
    cost_time bigint                            comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

③实体类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Long id; //主键ID
    private Long operateUser; //操作人ID
    private Date operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private Object returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}

④Mapper接口:

public interface OperateLogMapper {

    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
    public void insert(OperateLog log);

}

⑤自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
}

⑥定义切面类:

@Component
@Aspect
public class LogAspect {

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Autowired
    private HttpSession session;
    
    @Around("@annotation(cn.wolfcode.annotation.MyLog)")
    public Object saveLog(ProceedingJoinPoint joinPoint) throws Throwable {

       Employee user = (Employee)session.getAttribute("user");
       if(StringUtils.isEmpty(user)){
           throw new RuntimeException("请登录");
       }
       Object ret = null;
       try {
           long start = System.currentTimeMillis();
           ret = joinPoint.proceed();
           long end = System.currentTimeMillis();
           //保存日志
           OperateLog log = new OperateLog();
           //log.setId();
           log.setOperateUser(user.getId()); //登录用户的id
           log.setOperateTime(new Date()); //当前系统时间
           log.setClassName(joinPoint.getTarget().getClass().getName()); //类的名称
           log.setMethodName(joinPoint.getSignature().getName()); //方法名称
           log.setMethodParams(Arrays.toString(joinPoint.getArgs())); //参数名称
           log.setReturnValue(ret); //方法返回值
           log.setCostTime(end-start); //操作耗时
           operateLogMapper.insert(log);


       } catch (Throwable e) {
          e.printStackTrace();
       }
       return ret;
    }
}

⑦贴注解:

在DepartmentServiceImpl中的添加、更新、删除方法上贴@MyLog

在EmployeeServiceImpl中的保存方法上贴@MyLog

小结:@MyLog也可以贴在类上,不过要进行修改

第一步:

修改 @MyLog 注解的 @Target,增加 ElementType.TYPE(允许修饰类 / 接口)

@Target({ElementType.METHOD, ElementType.TYPE}) // 同时支持方法、类

第二步:

调整 AOP 切面的切点,支持 类上有 @MyLog”或 方法上有 @MyLog

@Before("@within(cn.wolfcode.annotation.MyLog) || @annotation(cn.wolfcode.annotation.MyLog)")

第三步:

@MyLog 贴在实现类上,该类所有方法都会被拦截

四、Aop案例(统一异常处理)

①定义切面类ExceptionAspect:

@Component
@Aspect
public class ExceptionAspect {

    @Around("@annotation(cn.wolfcode.annotation.MyExceptionAdvice)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object ret = null;
        try {
            ret = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
            return Result.fail(e.getMessage());
        }
        return ret;
    }

}

②自定义注解MyExceptionAdvice:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExceptionAdvice {

}

③贴注解:

在DepartmentController的saveOrUpdate()方法上贴@MyExceptionAdvice

 @PostMapping("/saveOrUpdate")
    @ResponseBody
    @RequireLogin
    @MyExceptionAdvice
    public Result saveOrUpdate(@RequestBody Department department){
       /* try {
            if(StringUtils.isEmpty(department.getId())){
                departmentService.save(department);
            }else {
                departmentService.update(department);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return Result.fail(e.getMessage());
        }

        return Result.success();*/

        if(StringUtils.isEmpty(department.getId())){
            //System.out.println(1/0);
            departmentService.save(department);
        }else {
            departmentService.update(department);
        }
        return Result.success();
    }

在DepartmentController的delete()方法上贴@MyExceptionAdvice

@GetMapping("/delete")
    @ResponseBody
    @RequireLogin
    @MyExceptionAdvice
    public Result delete(Long id){
    /*    try {
            departmentService.delete(id);
        } catch (Exception e) {
            e.printStackTrace();
            return Result.fail(e.getMessage());
        }
        return Result.success();*/

        departmentService.delete(id);
        return Result.success();
    }
posted on 2026-03-05 00:23  冬冬咚  阅读(0)  评论(0)    收藏  举报