AOP还不会用?一个案例学会 - 详解

基于 AOP 实现记录系统操作日志( Spring 内置线程池 + @Async)

创建系统操作表

CREATE TABLE sys_operation_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
  username VARCHAR(50) NOT NULL COMMENT '操作用户',
  operation VARCHAR(100) COMMENT '操作名称',
  method VARCHAR(200) COMMENT '调用方法全路径',
  params TEXT COMMENT '请求参数(JSON)',
  ip VARCHAR(50) COMMENT '请求IP',
  status TINYINT DEFAULT 1 COMMENT '操作状态(1成功 0失败)',
  error_msg TEXT COMMENT '错误信息(失败时存储)',
  time DATETIME NOT NULL COMMENT '操作时间',
  cost BIGINT COMMENT '执行耗时(ms)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';

自定义注解

定义数据库操作类型

/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}

定义注解

/**
* 自定义注解,用于实现AOP日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
// 需要记录日志的操作类型
OperationType value();
}

创建日志实体类

与数据库表内容相对应。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SysLog {
private String userName; //操作用户
private String operation; // 操作名称
private String method; // 调用方法全路径
private String params; // 请求参数
private String ip; // 请求ip
private Integer status; //操作状态
private String errorMsg; // 错误信息(失败时储存)
private LocalDateTime time; // 操作时间
private Long cost; // 执行耗时
}

AOP 切面拦截

自定义切面

/**
* 自定义切面,实现AOP日志
*/
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private LogService logService;
//切入点
@Pointcut("@annotation(com.sky.annotation.OperationLog)")
public void logPointCut() {}
//环绕通知
@Around("logPointCut() && @annotation(operationLog)")
public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
// 环绕通知是在方法执行前后都可以监视操作的
long start = System.currentTimeMillis();
SysLog sysLog = new SysLog();
Object result = null;
try{
// 执行业务逻辑
result = joinPoint.proceed();
sysLog.setStatus(1); //成功
}catch (Exception e){
// 失败
sysLog.setStatus(0);
sysLog.setErrorMsg(e.getMessage());
throw e; // 继续抛出,保证业务逻辑感知异常
}finally{
long cost = System.currentTimeMillis() - start; //方法执行时间
// 封装日志实体类
sysLog.setOperation(String.valueOf(operationLog.value()));
sysLog.setIp(getIp());
sysLog.setCost(cost);
sysLog.setParams(Arrays.toString(joinPoint.getArgs()));
sysLog.setUserName(String.valueOf(BaseContext.getCurrentId()));
sysLog.setMethod(joinPoint.getSignature().toShortString());
sysLog.setTime(LocalDateTime.now());
}
// 异步落库
logService.saveLogSync(sysLog);
return result;
}
private String getIp(){
//先写死
return "127.0.0.1";
}
}

创建 LogService 方法用于写日志

public interface LogService {
/**
* 保存AOP日志到数据库
* @param sysLog
*/
void saveLog(SysLog sysLog);
}

Service 层

@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
@Override
public void saveLog(SysLog sysLog) {
// 插入日志到数据库中
logMapper.insert(sysLog);
}
@Override
public void saveLogSync(SysLog sysLog) {
//插入日志
logMapper.insert(sysLog);
}
}

Mapper 层执行数据库插入操作

@Mapper
public interface LogMapper {
/**
* 日志插入
* @param sysLog
*/
@Insert("insert into sys_operation_log (username, operation, method, params, ip, status, error_msg, time, cost) values " +
"(#{userName}, #{operation}, #{method}, #{params}, #{ip}, #{status}, #{errorMsg}, #{time}, #{cost})")
void insert(SysLog sysLog);
}

配置线程池

@Configuration
@EnableAsync
public class AsyncConfiguration {
@Bean("logExecutor")
public Executor logExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("log-async-");
executor.initialize();
return executor;
}
}

在实现类中要添加日志的方法添加注解

/**
* 新增菜品及口味
* @param dishDTO
*/
@Transactional
@OperationLog(OperationType.INSERT)
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//向菜品表插入1条数据
dishMapper.insert(dish);
//获取insert语句生成的主键值
Long dishId = dish.getId();
//向口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
  if(flavors != null && flavors.size() > 0){
  flavors.forEach(dishFlavor -> {
  dishFlavor.setDishId(dishId);
  });
  //向口味表插入n条数据
  dishFlavorMapper.insertBatch(flavors);
  }
  }

下图就是执行方法后的日志,方便运维而且即使服务器挂掉也会留存记录。

基于 AOP 实现记录系统操作日志(消息队列异步落库)

这种方案也不难做,在上个方案的基础上,把封装的SysLog转为Json格式,直接丢到 MQ 队列里,定义消费者取消息,自己慢慢转换格式,存数据库。

// 切面里
rabbitTemplate.convertAndSend("log.queue", sysLogJson);

消费者:

@RabbitListener(queues = "log.queue")
public void saveLog(String logJson) {
SysLog log = JSON.parseObject(logJson, SysLog.class);
sysLogRepository.save(log);
}

二者区别:

特性线程池异步落库MQ异步落库
实现复杂度低(几行代码)高(引入 MQ)
部署运维成本高,需要运维 MQ
实时性高(毫秒级)中(几十~几百毫秒)
可靠性一般(服务挂了任务丢)高(消息持久化)
抗压能力弱(队列有限)强(削峰填谷)
扩展性弱(只能写库)强(多消费者、多场景)
适用场景中小规模系统高并发、大规模系统

当然,把日志一股脑的写入数据库显然不是什么好方法,应该怎么做以后再学习。

总结

我们说,学东西就要学透。做了这个么个业务后,这里面涉及了什么知识,怎么用的?是我们得反思的地方。

前两天看到过一句话,觉得讲的非常好。”知识有我不会的,但是没有我不能会的。”共勉。

一、元注解

元注解就是 用于修饰注解的注解,它们定义了你这个注解能用在什么地方、能保存到什么时候、由谁来读取。

常见的元注解有:

  • @Target
  • @Retention
  • @Documented
  • @Inherited

在当前业务里用到了前两个,正是最常用的。

/**
* 自定义注解,用于实现AOP日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
// 需要记录日志的操作类型
OperationType value();
}

@Target

作用:限制自定义注解能用在什么位置。

括号内的参数是 ElementType 枚举,常见的有

枚举值含义举例
ElementType.TYPE类、接口、枚举上@Controller
ElementType.METHOD方法上@GetMapping
ElementType.FIELD成员变量上@Autowired
ElementType.PARAMETER方法参数上@RequestParam
ElementType.CONSTRUCTOR构造方法上构造函数注解
ElementType.ANNOTATION_TYPE注解类型上元注解
ElementType.PACKAGE包上包级别注解

前四个我们都很熟悉,非常常用,这就跟之前的知识联系起来了。

AOP 面向切面编程,最后作用于切入点,一般为某个具体方法。我们一想这个逻辑也知道,日志肯定是要作用于方法,执行了某个业务方法才去记日志,不然也没必要。实现公共字段自动填充和检测同样的道理。所以这里我们用的ElementType.METHOD

@Retention

作用:定义注解的生命周期,决定编译后是否还存在。

参数是RetentionPolicy枚举

枚举值生命周期使用场景
SOURCE只在源码阶段保留,编译成 .class
文件后就没了
@Override
@SuppressWarnings
CLASS编译后保留在 .class
文件,但运行时不会加载到 JVM
一般不用
RUNTIME一直保留到运行时,可以通过反射获取@Autowired
@RequestMapping

我们需要获取注解信息,所以RetentionPolicy.RUNTIME

补充:@Documented@Inherited

  • @Documented:生成 javadoc 时会包含这个注解信息。
  • @Inherited:子类会继承父类的注解(只对 @Target(TYPE) 有效)。

二、实体类中的注解

  • getter/setter、toString、equals、hashCode(来自 @Data
  • 无参构造器(来自 @NoArgsConstructor
  • 全参构造器(来自 @AllArgsConstructor
  • Builder 链式构建(来自 @Builder

可以看到都是常用的,免去了自己写构造方法。而且 Builder 链式构建更是方便,比如:

@Builder
public class User {
private Long id;
private String name;
}

链式构建下:

User user = User.builder()
.id(1L)
.name("jack")
.build();

非常优雅,可读性也高。

三、String.valueOf()toString()的区别

toString()

对象的方法,对象不能为空,否则会抛NullPointerException

示例:

Object obj = new Object();
System.out.println(obj.toString()); // java.lang.Object@1b6d3586
Object nullObj = null;
System.out.println(nullObj.toString()); // ❌ NullPointerException

String.valueOf()

是 String 类的静态方法,有空值保护,如果传入 null,返回"null"(字符串字面量),不会报异常。

Object obj = new Object();
System.out.println(String.valueOf(obj)); // java.lang.Object@1b6d3586
Object nullObj = null;
System.out.println(String.valueOf(nullObj)); // "null"(安全,不报错)

区别

看一下 valueOf() 的源码

其实相比toString(),就是多了一层null判断。

其实,valueOf()还有很多重载方法,

valueOf() 对各种类型都能转,但toString()不能转基本数据类型,所以使用上尽量用valueOf()

posted @ 2025-10-20 20:48  yjbjingcha  阅读(1)  评论(0)    收藏  举报