[特殊字符] 操作日志模块复习笔记

📝 操作日志模块复习笔记

一、架构概览

操作日志模块采用了 **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,面向切面编程)是一种编程思想,用于将横切关注点(如日志、事务、权限)与业务逻辑分离。

**核心概念:**

  1. **切面(Aspect)**:横切关注点的模块化(如日志切面)

  2. **连接点(JoinPoint)**:程序执行过程中的某个点(如方法调用)

  3. **通知(Advice)**:在特定连接点执行的动作

  • Before、After、AfterReturning、AfterThrowing、Around
  1. **切入点(Pointcut)**:匹配连接点的表达式

  2. **目标对象(Target)**:被代理的对象

  3. **代理(Proxy)**:AOP框架创建的对象


❓ 问题2:Spring AOP的实现原理是什么?

**答案:**

Spring AOP基于**动态代理**实现:

  1. **JDK动态代理**(默认)
  • 要求目标类实现接口

  • 通过`java.lang.reflect.Proxy`创建代理对象

  • 代理的是接口

  1. **CGLIB代理**
  • 目标类没有实现接口时使用

  • 通过继承目标类生成子类

  • 使用字节码技术(ASM)

  1. **选择策略:**

```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在实际项目中的应用场景?

**答案:**

  1. **日志记录** ✅(本项目)

  2. **事务管理** - `@Transactional`

  3. **权限校验** - 检查用户是否有权限

  4. **性能监控** - 记录方法执行时间

  5. **缓存管理** - `@Cacheable`

  6. **全局异常处理** - `@ControllerAdvice`

  7. **限流熔断** - 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解析

✅ **异常处理**:必须重新抛出异常

✅ **设计优势**:业务代码零侵入,日志统一管理


相关推荐
Cando学算法2 小时前
回声服务器项目
linux·开发语言·c++·计算机网络·ubuntu
好好研究2 小时前
Java基础学习(十三):IO流基础
java·开发语言·学习·io流
HHHHH1010HHHHH2 小时前
golang如何实现可靠消息最终一致_golang可靠消息最终一致实现实战
jvm·数据库·python
wuxinyan1232 小时前
Java面试题52:一文深入了解Kubernetes 核心资源对象
java·kubernetes·面试题
知识分享小能手2 小时前
R语言入门学习教程,从入门到精通,R语言传统绘图系统 - 完整知识点与案例代码(2)
开发语言·学习·r语言
SamDeepThinking2 小时前
秒杀下单,用户点一下按钮,后端要过六道关卡
java·后端·架构
代龙涛2 小时前
WordPress archive.php 分类与归档页面开发指南
开发语言·后端·php·wordpress
格林威2 小时前
面阵相机 vs 线阵相机:堡盟与大恒相机选型差异全解析 附C++ 实战演示
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
Sam_Deep_Thinking2 小时前
适合中小型企业的出口入口网关微服务
java·微服务·架构