通知+注解的切点使用+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通知:目标方法执行前执行
应用:参数校验、日志记录、权限检查

(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;
}

(3)afterThrowing:目标方法抛出异常后
应用:处理异常、记录错误日志
①若有异常:显示异常信息

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

(4)after:目标方法执行后(无论成功或失败)
应用:资源释放、清理工作
有没有异常都显示,例如手写一个异常后

为什么异常信息在下面?
这是因为标准输出流(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());
}
}
执行结果:

三、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依赖,所以不必重新引入。

②导入数据库表结构
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();
}
浙公网安备 33010602011771号