📝 操作日志模块复习笔记
一、架构概览
操作日志模块采用了 **AOP(面向切面编程)+ 自定义注解** 的设计模式,实现了业务代码与日志记录的解耦。
技术栈分层结构:
```
表现层 (Controller) → 业务层 (Service) → 数据访问层 (Mapper) → 数据库
↑
切面层 (Aspect) + 注解层 (Annotation)
```
二、各层详细解析
1️⃣ **注解层 (Annotation Layer)**
**文件位置**: `src/main/java/com/it/annotation/OperationLogAnnotation.java`
完整代码及注释:
```java
package com.it.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义操作日志注解
*
* @Target(ElementType.METHOD) - 指定注解只能用在方法上
* @Retention(RetentionPolicy.RUNTIME) - 注解在运行时可通过反射获取
*/
@Target(ElementType.METHOD) // 方法级别注解,不能用于类或字段
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,AOP才能读取
public @interface OperationLogAnnotation {
/**
* 操作描述信息,默认为空字符串
* 使用时可以指定:@OperationLogAnnotation(description = "新增用户")
*/
String description() default "";
}
```
核心知识点:
-
**@Target**: 限制注解的使用位置
-
`ElementType.METHOD` - 只能用于方法
-
其他可选值:TYPE(类)、FIELD(字段)、PARAMETER(参数)等
-
**@Retention**: 控制注解的生命周期
-
`RetentionPolicy.SOURCE` - 仅源码阶段,编译后丢弃
-
`RetentionPolicy.CLASS` - 编译到class文件,运行时不可见
-
`RetentionPolicy.RUNTIME` - 运行时可见,可通过反射获取 ✅
2️⃣ **切面层 (Aspect Layer)** ⭐核心层
**文件位置**: `src/main/java/com/it/aspect/OperationLogAspect.java`
完整代码及注释:
```java
package com.it.aspect;
import com.it.pojo.OperationLog;
import com.it.service.OperationLogService;
import com.it.util.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
/**
* 操作日志切面类
*
* @Slf4j - Lombok提供的日志记录器
* @Aspect - 声明这是一个切面类
* @Component - 交给Spring容器管理
*/
@Slf4j
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private OperationLogService operationLogService;
/**
* 环绕通知 - 在目标方法执行前后进行拦截
*
* @Around("@annotation(com.it.annotation.OperationLogAnnotation)")
* - 切入点表达式:拦截所有带有@OperationLogAnnotation注解的方法
*
* ProceedingJoinPoint - 连接点对象,可以获取方法信息并执行目标方法
*/
@Around("@annotation(com.it.annotation.OperationLogAnnotation)")
public Object recordOperationLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录开始时间
long startTime = System.currentTimeMillis();
try {
// ========== 前置处理:执行目标方法 ==========
Object result = joinPoint.proceed(); // 执行被拦截的方法
// ========== 后置处理:记录成功日志 ==========
long endTime = System.currentTimeMillis();
long duration = endTime - startTime; // 计算方法执行耗时
// 构建操作日志对象
OperationLog operationLog = new OperationLog();
operationLog.setOperationTime(LocalDateTime.now()); // 操作时间
operationLog.setClassName(joinPoint.getSignature().getDeclaringTypeName()); // 类名
operationLog.setMethodName(joinPoint.getSignature().getName()); // 方法名
operationLog.setReturnValue(result != null ? result.toString() : ""); // 返回值
operationLog.setDuration(duration); // 执行耗时
// 获取当前操作用户
String operator = getCurrentUser();
operationLog.setOperator(operator);
// 保存日志到数据库
operationLogService.saveOperationLog(operationLog);
return result; // 返回目标方法的执行结果
} catch (Throwable throwable) {
// ========== 异常处理:记录异常日志 ==========
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
OperationLog operationLog = new OperationLog();
operationLog.setOperationTime(LocalDateTime.now());
operationLog.setClassName(joinPoint.getSignature().getDeclaringTypeName());
operationLog.setMethodName(joinPoint.getSignature().getName());
operationLog.setReturnValue("异常: " + throwable.getMessage()); // 记录异常信息
operationLog.setDuration(duration);
String operator = getCurrentUser();
operationLog.setOperator(operator);
operationLogService.saveOperationLog(operationLog);
throw throwable; // 重要:异常需要继续抛出,不能吞掉
}
}
/**
* 从JWT Token中获取当前操作用户
*
* RequestContextHolder - Spring提供的请求上下文持有者
* - 可以在任何地方获取当前请求的HttpServletRequest
*
* @return 用户名,如果获取失败返回"anonymous"
*/
private String getCurrentUser() {
try {
// 从请求上下文中获取ServletRequestAttributes
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return "system"; // 非HTTP请求场景
}
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("Authorization"); // 从请求头获取Token
if (token != null && !token.isEmpty()) {
Claims claims = JwtUtils.parseJWT(token); // 解析JWT
Object name = claims.get("name"); // 获取用户名字段
if (name != null) {
return name.toString();
}
}
} catch (Exception e) {
log.warn("获取当前用户失败: {}", e.getMessage());
}
return "anonymous"; // 匿名用户的默认标识
}
}
```
核心知识点:
**1. AOP通知类型:**
-
`@Before` - 前置通知:目标方法执行前
-
`@After` - 后置通知:目标方法执行后(无论是否异常)
-
`@AfterReturning` - 返回通知:目标方法成功返回后
-
`@AfterThrowing` - 异常通知:目标方法抛出异常后
-
`@Around` - 环绕通知:可以控制目标方法何时执行 ✅最强大
**2. ProceedingJoinPoint常用方法:**
```java
joinPoint.getSignature().getName() // 方法名
joinPoint.getSignature().getDeclaringTypeName() // 类名
joinPoint.getArgs() // 方法参数
joinPoint.proceed() // 执行目标方法
joinPoint.getTarget() // 目标对象
```
**3. RequestContextHolder的作用:**
-
在非Controller层也能获取HttpServletRequest
-
基于ThreadLocal实现,线程安全
-
避免了层层传递request对象的麻烦
3️⃣ **实体层 (POJO Layer)**
**文件位置**: `src/main/java/com/it/pojo/OperationLog.java`
完整代码及注释:
```java
package com.it.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 操作日志实体类
*
* @Data - 自动生成getter/setter/toString/equals/hashCode
* @NoArgsConstructor - 无参构造
* @AllArgsConstructor - 全参构造
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperationLog {
private Long id; // 主键ID(自增)
private String operator; // 操作人
private LocalDateTime operationTime; // 操作时间
private String className; // 类名
private String methodName; // 方法名
private String returnValue; // 返回值或异常信息
private Long duration; // 执行耗时(毫秒)
}
```
4️⃣ **数据访问层 (Mapper Layer)**
**文件位置**: `src/main/java/com/it/mapper/OperationLogMapper.java`
完整代码及注释:
```java
package com.it.mapper;
import com.it.pojo.OperationLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 操作日志Mapper接口
*
* @Mapper - MyBatis标识,自动生成代理实现类
*/
@Mapper
public interface OperationLogMapper {
/**
* 插入操作日志
*
* @Insert - MyBatis注解式SQL
* @Options(useGeneratedKeys = true, keyProperty = "id")
* - 使用数据库自增主键,并回填到operationLog.id
*/
@Insert("INSERT INTO operation_log (operator, operation_time, class_name, method_name, return_value, duration) " +
"VALUES (#{operator}, #{operationTime}, #{className}, #{methodName}, #{returnValue}, #{duration})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(OperationLog operationLog);
/**
* 查询所有操作日志,按时间倒序排列
*/
@Select("SELECT * FROM operation_log ORDER BY operation_time DESC")
List<OperationLog> findAll();
}
```
核心知识点:
-
**MyBatis两种方式**:
-
注解方式:简单SQL用注解(如本项目)
-
XML方式:复杂SQL用XML映射文件
-
**useGeneratedKeys**:
-
告诉MyBatis使用数据库自增主键
-
插入后自动将生成的ID设置到对象的id属性
5️⃣ **业务层 (Service Layer)**
Service接口
**文件位置**: `src/main/java/com/it/service/OperationLogService.java`
```java
package com.it.service;
import com.it.pojo.OperationLog;
import java.util.List;
/**
* 操作日志服务接口
*/
public interface OperationLogService {
/**
* 保存操作日志
*/
void saveOperationLog(OperationLog operationLog);
/**
* 查询所有操作日志
*/
List<OperationLog> findAll();
}
```
Service实现类
**文件位置**: `src/main/java/com/it/service/impl/OperationLogServiceImpl.java`
```java
package com.it.service.impl;
import com.it.mapper.OperationLogMapper;
import com.it.pojo.OperationLog;
import com.it.service.OperationLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 操作日志服务实现类
*
* @Service - 声明为Spring的业务层组件
*/
@Service
public class OperationLogServiceImpl implements OperationLogService {
@Autowired
private OperationLogMapper operationLogMapper;
@Override
public void saveOperationLog(OperationLog operationLog) {
operationLogMapper.insert(operationLog);
}
@Override
public List<OperationLog> findAll() {
return operationLogMapper.findAll();
}
}
```
6️⃣ **表现层 (Controller Layer)**
**文件位置**: `src/main/java/com/it/controller/OperationLogController.java`
```java
package com.it.controller;
import com.it.pojo.OperationLog;
import com.it.pojo.Result;
import com.it.service.OperationLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 操作日志控制器
*
* @Slf4j - 日志记录器
* @RestController - 组合注解:@Controller + @ResponseBody
*/
@Slf4j
@RestController
@RequestMapping("/operation-log")
public class OperationLogController {
@Autowired
private OperationLogService operationLogService;
/**
* 查询所有操作日志
* GET /operation-log
*/
@GetMapping
public Result list() {
log.info("查询所有操作日志");
List<OperationLog> logList = operationLogService.findAll();
return Result.success(logList);
}
}
```
7️⃣ **使用示例**
**文件位置**: `src/main/java/com/it/controller/HabitsController.java`
```java
package com.it.controller;
import com.it.annotation.OperationLogAnnotation;
import com.it.pojo.Habits;
import com.it.pojo.Result;
import com.it.service.HabitsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
@RestController
public class HabitsController {
@Autowired
private HabitsService habitsService;
private static final Logger log = LoggerFactory.getLogger(HabitsController.class);
/**
* 只需要加上@OperationLogAnnotation注解
* AOP会自动拦截并记录日志
*/
@OperationLogAnnotation(description = "查询全部习惯")
@GetMapping("/habits")
public Result list(){
log.info("查询全部习惯");
List<Habits> habitList = habitsService.findAll();
return Result.success(habitList);
}
@OperationLogAnnotation(description = "根据ID删除习惯")
@DeleteMapping("/habits")
public Result delete(@RequestParam Integer id){
log.info("根据id删除习惯, id={}", id);
habitsService.deleteById(id);
return Result.success();
}
@OperationLogAnnotation(description = "新增习惯")
@PostMapping("/habits")
public Result save(@RequestBody Habits habits){
log.info("新增习惯, habit={}", habits);
habitsService.save(habits);
return Result.success();
}
}
```
三、工作流程图
```
用户请求
↓
Controller方法(带@OperationLogAnnotation)
↓
AOP切面拦截 (@Around)
↓
┌─────────────────────────────┐
│ 1. 记录开始时间 │
│ 2. 执行目标方法 (proceed) │
│ 3. 记录结束时间 │
│ 4. 构建OperationLog对象 │
│ 5. 从JWT获取操作用户 │
│ 6. 调用Service保存日志 │
│ 7. 返回结果 │
└─────────────────────────────┘
↓
Mapper插入数据库
↓
返回响应给前端
```
四、🔥 Java面试题高频考点
❓ 问题1:什么是AOP?它的核心概念有哪些?
**答案:**
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程思想,用于将横切关注点(如日志、事务、权限)与业务逻辑分离。
**核心概念:**
-
**切面(Aspect)**:横切关注点的模块化(如日志切面)
-
**连接点(JoinPoint)**:程序执行过程中的某个点(如方法调用)
-
**通知(Advice)**:在特定连接点执行的动作
- Before、After、AfterReturning、AfterThrowing、Around
-
**切入点(Pointcut)**:匹配连接点的表达式
-
**目标对象(Target)**:被代理的对象
-
**代理(Proxy)**:AOP框架创建的对象
❓ 问题2:Spring AOP的实现原理是什么?
**答案:**
Spring AOP基于**动态代理**实现:
- **JDK动态代理**(默认)
-
要求目标类实现接口
-
通过`java.lang.reflect.Proxy`创建代理对象
-
代理的是接口
- **CGLIB代理**
-
目标类没有实现接口时使用
-
通过继承目标类生成子类
-
使用字节码技术(ASM)
- **选择策略:**
```java
// 如果目标对象实现了接口 → JDK动态代理
// 如果目标对象没有实现接口 → CGLIB代理
// 可以通过配置强制使用CGLIB:spring.aop.proxy-target-class=true
```
❓ 问题3:@Around、@Before、@After的执行顺序?
**答案:**
```java
@Before → 目标方法执行前
@Around前 → proceed()之前的代码
目标方法 → joinPoint.proceed()
@Around后 → proceed()之后的代码
@After → 目标方法执行后(无论是否异常)
@AfterReturning → 成功返回后
@AfterThrowing → 抛出异常后
```
**实际执行流程:**
```
@Before
↓
@Around前半部分
↓
目标方法执行
↓
@Around后半部分
↓
@After
↓
@AfterReturning(成功)或 @AfterThrowing(异常)
```
❓ 问题4:如何保证AOP中异常不被吞掉?
**答案:**
```java
@Around("@annotation(xxx)")
public Object xxx(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
// 记录异常日志
log.error("异常", throwable);
// ⚠️ 必须重新抛出异常,否则调用方无法感知
throw throwable;
}
}
```
**关键点:**
-
捕获异常后必须`throw throwable`
-
方法签名要声明`throws Throwable`
-
否则会导致事务不回滚、前端收不到错误信息
❓ 问题5:自定义注解的元注解有哪些?分别什么作用?
**答案:**
| 元注解 | 作用 | 常用值 |
|--------|------|--------|
| `@Target` | 指定注解使用位置 | METHOD, TYPE, FIELD, PARAMETER |
| `@Retention` | 指定注解生命周期 | SOURCE, CLASS, RUNTIME |
| `@Documented` | 是否包含在JavaDoc中 | - |
| `@Inherited` | 是否被子类继承 | - |
| `@Repeatable` | 是否可重复使用 | - |
**本题中的使用:**
```java
@Target(ElementType.METHOD) // 只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可见,AOP才能读取
```
❓ 问题6:RequestContextHolder的工作原理?
**答案:**
`RequestContextHolder`基于**ThreadLocal**实现:
```java
// Spring MVC在请求进入时
public void doFilter(ServletRequest req, ServletResponse res) {
try {
// 将request绑定到当前线程
RequestContextHolder.setRequestAttributes(attributes);
// 执行业务逻辑...
chain.doFilter(req, res);
} finally {
// 请求结束后清理,防止内存泄漏
RequestContextHolder.resetRequestAttributes();
}
}
```
**优点:**
-
在Service、Mapper等任何地方都能获取request
-
线程安全(每个线程独立存储)
-
避免层层传递参数
**注意事项:**
- 必须在finally中清理,防止线程池复用导致内存泄漏
❓ 问题7:为什么切面类要加@Component注解?
**答案:**
```java
@Aspect // 只是声明这是一个切面
@Component // 交给Spring容器管理
```
-
`@Aspect`只是标识,不会让Spring自动扫描
-
必须加上`@Component`(或`@Configuration`)才能让Spring管理
-
或者在配置类上加`@EnableAspectJAutoProxy`启用AOP
❓ 问题8:如果多个切面拦截同一个方法,执行顺序如何控制?
**答案:**
使用`@Order`注解:
```java
@Aspect
@Component
@Order(1) // 数字越小,优先级越高
public class LogAspect { }
@Aspect
@Component
@Order(2)
public class TransactionAspect { }
```
**执行顺序:**
```
Order(1)前置 → Order(2)前置 → 目标方法 → Order(2)后置 → Order(1)后置
```
❓ 问题9:AOP在实际项目中的应用场景?
**答案:**
-
**日志记录** ✅(本项目)
-
**事务管理** - `@Transactional`
-
**权限校验** - 检查用户是否有权限
-
**性能监控** - 记录方法执行时间
-
**缓存管理** - `@Cacheable`
-
**全局异常处理** - `@ControllerAdvice`
-
**限流熔断** - Sentinel、Hystrix
❓ 问题10:Lombok的@Data注解生成了哪些方法?
**答案:**
```java
@Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor
```
生成的方法:
-
所有字段的getter方法
-
非final字段的setter方法
-
`toString()`方法
-
`equals()`和`hashCode()`方法
-
必需参数的构造器
**注意:**
-
如果有`@AllArgsConstructor`,会额外生成全参构造
-
如果有`@NoArgsConstructor`,会额外生成无参构造
五、💡 扩展优化建议
1. 异步保存日志(提升性能)
```java
@Service
public class OperationLogServiceImpl implements OperationLogService {
@Async // 异步执行,不阻塞主线程
@Override
public void saveOperationLog(OperationLog operationLog) {
operationLogMapper.insert(operationLog);
}
}
// 启动类需要加@EnableAsync
@SpringBootApplication
@EnableAsync
public class HabitSystemApplication { }
```
2. 添加分页查询
```java
@Select("SELECT * FROM operation_log ORDER BY operation_time DESC LIMIT #{offset}, #{pageSize}")
List<OperationLog> findByPage(@Param("offset") int offset, @Param("pageSize") int pageSize);
```
3. 定期清理历史日志
```java
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanOldLogs() {
// 删除90天前的日志
operationLogMapper.deleteOldLogs(LocalDateTime.now().minusDays(90));
}
```
4. 记录方法参数
```java
// 在切面中获取方法参数
Object[] args = joinPoint.getArgs();
operationLog.setParams(JSON.toJSONString(args));
```
六、总结记忆要点
✅ **三层架构**:Controller → Service → Mapper
✅ **AOP核心**:@Aspect + @Around + ProceedingJoinPoint
✅ **自定义注解**:@Target + @Retention
✅ **用户获取**:RequestContextHolder + JWT解析
✅ **异常处理**:必须重新抛出异常
✅ **设计优势**:业务代码零侵入,日志统一管理