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()。
浙公网安备 33010602011771号