使用Aspect切面实现系统日志并存入数据库

本文介绍如何利用Spring框架的AspectJ特性实现系统操作日志的自动记录,并将日志信息存储到数据库中。主要步骤包括:配置Maven依赖、启用Spring切面功能、自定义注解和切面类、在Controller层应用注解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用Aspect切面实现系统日志并存入数据库

1.pom.xml中:加入Maven依赖

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>${spring.version}</version>
/dependency>

2.SpringMVC.xml中:开启切面注解

<aop:aspectj-autoproxy proxy-target-class="true" />

3.自定义注解用于切面切入点

import java.lang.annotation.*;

/**
 * 系统日志注解
 * 
 * @author wangxueqing
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {

   String value() default "";
}

4.自定义切面在方法执行时自动执行

import com.alibaba.fastjson.JSON;
import com.sm.share3d.annotion.SysLog;
import com.sm.share3d.bean.SysLogEntity;
import com.sm.share3d.utils.HttpContextUtils;
import com.sm.share3d.utils.IPUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


/**
 * 系统日志,切面处理类
 * 
 * @author wangxueqing
 */
@Component
@Aspect
public class SysLogAspect {

   //切入点,以方法的形式存在
   @Pointcut("@annotation(com.sm.share3d.annotion.SysLog)")
   public void sportPoint(){
      System.out.println(1111111);
   }

   @Before("sportPoint()")
   public void before(JoinPoint joinPoint){
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      Method method = signature.getMethod();

      SysLogEntity logEntity = new SysLogEntity();
      SysLog syslog = method.getAnnotation(SysLog.class);
      if(syslog != null){
         //注解上的描述
         logEntity.setOperation(syslog.value());
      }

      //请求的方法名
      String className = joinPoint.getTarget().getClass().getName();
      String methodName = signature.getName();
      logEntity.setMethod(className + "." + methodName + "()");

      //请求的参数
      Object[] args = joinPoint.getArgs();
      if(args.length>0){
         /*String params = JSON.toJSONString(args[0]);
         logEntity.setParams(params);*/
      }


      //获取request
      HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
      //设置IP地址
      logEntity.setIp(IPUtils.getIpAddr(request));

      //用户名
//    String username = ShiroUtils.getUserEntity().getUsername();
      logEntity.setUsername(username);
      logEntity.setCreateDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
      sysLogService.save(logEntity);
   }
}

5.在Controller层需要保存日志的方法上加上自定义的注解

/**
* 保存用户
* @param user
* @return
*/
@SysLog("新增用户")
@RequestMapping("/saveUser")
public R saveUser(@RequestBody User user){
    try {
	userService.save(user);
	return R.ok();
    } catch (Exception e) {
	e.printStackTrace();
        return R.error("新增失败");
    }		
}


<think>我们参考用户的需求:使用Java切面记录接口操作(新增、删除、编辑)的审计日志将表数据的变化映射到审计日志表的描述字段。 根据用户提供的引用,我们可以定义自定义注解来标记需要记录日志的方法,利用AOP切面在方法执行前后获取数据变化。 方案如下: 1. 定义自定义注解,用于标记需要记录审计日志的方法,传递操作类型和模块信息。 2. 创建审计日志实体类,用于映射到数据库表。 3. 实现切面逻辑,在方法执行前后获取数据状态,记录变化。 4. 将变化信息保存到审计日志表的描述字段(或者可以设计为单独的操作前和操作后字段,然后组合成描述)。 考虑到引用[1]中的`LoggableFullDB`注解,我们可以设计类似的注解,但增加操作类型和模块属性。 具体步骤: 一、定义审计日志注解 参考引用[1],我们定义如下注解: ``` @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuditLog { String operateType(); // 操作类型:新增、删除、编辑 String module(); // 业务模块 } ``` 二、审计日志实体类 审计日志表需要记录操作的相关信息,包括操作人、操作时间、操作类型、模块、操作的数据ID、操作前的数据、操作后的数据等。 但用户要求将数据变化映射到描述字段,我们可以将变化用字符串描述,存入描述字段。也可以分别记录操作前和操作后的数据,然后在描述字段中展示变化。 不过,通常审计日志需要详细记录变化,所以我们可以设计两个字段:变更前和变更后,然后描述字段可以存储一个简短的描述(比如“修改了用户名”)。但用户要求映射到描述字段,所以我们可以将变更前后的差异用字符串描述后放入描述字段。 这里我们有两种做法: 1. 在描述字段中存储变更的详细信息(例如:将变更前后的数据都转换成字符串,然后拼接成一句话)。 2. 分别存储变更前和变更后的数据,然后描述字段只存储操作说明(如“更新用户信息”),而变更细节用另外两个字段存储。 根据问题,用户要求“将表数据的变化映射到审计日志表的描述字段”,所以我们可以将变化信息转换为字符串,然后存入描述字段。 审计日志实体类设计如下(简化): ``` @Entity @Table(name = "audit_log") public class AuditLogEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String operator; // 操作人 private String operateType; // 操作类型 private String module; // 业务模块 private String description; // 描述(这里按照用户要求,存储数据变化信息) private LocalDateTime operateTime; // 操作时间 private String ipAddress; // 操作IP // 还可以有其他字段,如操作对象的ID等 // 省略getter和setter } ``` 三、切面实现 我们需要在方法执行前获取原始数据(对于更新和删除操作),在方法执行后获取新数据(对于新增和更新操作),然后比较变化,生成描述字符串。 步骤: 1. 在方法执行前,如果是更新或删除操作,则获取原始数据(例如根据ID从数据库查询)。 2. 执行目标方法。 3. 在方法执行后,如果是新增或更新操作,则获取新数据。 4. 比较变化(如果是更新操作),生成描述字符串。 5. 构建审计日志实体,设置描述字段,保存。 但是,获取原始数据可能需要根据方法参数(如ID)去查询,这可能会增加一次查询。另外,对于删除操作,我们只能在执行前获取数据,执行后数据已经删除。 因此,切面可以使用@Around通知,这样可以在方法执行前后获取数据。 注意:对于新增操作,我们只需要记录新增后的数据;对于删除操作,只需要记录删除前的数据;对于更新操作,需要记录更新前后的变化。 考虑到性能,我们可以只记录变化字段,而不是整个对象。但为了简化,我们这里记录整个对象的变化(用JSON表示),然后将整个变化字符串存入描述字段。 切面代码框架: ``` @Aspect @Component public class AuditLogAspect { @Autowired private AuditLogService auditLogService; @Autowired private HttpServletRequest request; // 用于获取IP @Around("@annotation(auditLog)") public Object auditLog(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable { // 获取方法参数 Object[] args = joinPoint.getArgs(); String operateType = auditLog.operateType(); String module = auditLog.module(); // 1. 获取操作前的数据(针对更新和删除) Object beforeData = null; if ("更新".equals(operateType) || "删除".equals(operateType)) { // 根据参数获取原始数据,这里需要根据实际情况实现 beforeData = getOriginalData(joinPoint, operateType); } // 2. 执行目标方法 Object result = joinPoint.proceed(); // 3. 获取操作后的数据(针对新增和更新) Object afterData = null; if ("新增".equals(operateType) || "更新".equals(operateType)) { // 如果是新增,返回的是新对象;如果是更新,返回的是更新后的对象 afterData = result; } // 4. 构建描述信息 String description = buildDescription(operateType, beforeData, afterData); // 5. 创建审计日志实体保存 saveAuditLog(auditLog, description); return result; } private Object getOriginalData(ProceedingJoinPoint joinPoint, String operateType) { // 实现获取原始数据的逻辑 // 例如,对于更新和删除操作,通常第一个参数是ID或者包含ID的对象,然后根据ID从数据库查询 // 这里需要根据具体方法参数设计 // 简化:假设第一个参数是ID if (joinPoint.getArgs().length > 0) { Object arg = joinPoint.getArgs()[0]; if (arg instanceof Long) { Long id = (Long) arg; // 根据ID和操作类型确定实体类型,然后查询数据库 // 这里需要知道实体类型,可以通过方法签名等,但比较困难 // 另一种方法:在注解中增加实体类型属性,然后通过反射查询 // 由于实现复杂,这里不展开,实际项目需要根据情况设计 } } return null; } private String buildDescription(String operateType, Object beforeData, Object afterData) { // 根据操作类型构建描述 if ("新增".equals(operateType)) { return "新增数据: " + JSON.toJSONString(afterData); } else if ("删除".equals(operateType)) { return "删除数据: " + JSON.toJSONString(beforeData); } else if ("更新".equals(operateType)) { // 可以比较前后变化,这里简化,直接输出前后数据 return String.format("更新前: %s, 更新后: %s", JSON.toJSONString(beforeData), JSON.toJSONString(afterData)); } return ""; } private void saveAuditLog(AuditLog auditLog, String description) { AuditLogEntity logEntity = new AuditLogEntity(); logEntity.setOperateType(auditLog.operateType()); logEntity.setModule(auditLog.module()); logEntity.setDescription(description); logEntity.setOperateTime(LocalDateTime.now()); // 设置操作人(从安全上下文获取) Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { logEntity.setOperator(authentication.getName()); } // 设置IP logEntity.setIpAddress(getClientIP()); auditLogService.save(logEntity); } private String getClientIP() { // 从HttpServletRequest中获取IP // 注意:可能有代理,所以使用X-Forwarded-For String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } } ``` 四、使用注解标记方法 在Service层的方法上使用注解: ``` @Service public class UserService { @AuditLog(operateType = "新增", module = "用户管理") public User createUser(User user) { // 保存用户 return userRepository.save(user); } @AuditLog(operateType = "更新", module = "用户管理") public User updateUser(User user) { // 更新用户 return userRepository.save(user); } @AuditLog(operateType = "删除", module = "用户管理") public void deleteUser(Long id) { userRepository.deleteById(id); } } ``` 问题与优化: 1. 获取原始数据:在更新和删除操作时,需要根据ID查询数据库,这会增加一次查询,可能影响性能。可以考虑缓存或者异步查询,但异步查询可能导致数据不一致(在查询后数据被修改,但概率低)。另外,也可以要求方法参数中包含完整的原始对象(对于更新操作,有时前端会传回原始对象,但不可靠)。 2. 对于删除操作,目标方法执行后数据已经删除,所以必须在方法执行前获取数据。 3. 描述字段存储整个对象的JSON,数据量可能很大,可以考虑只记录变化的字段(需要比较对象,生成变化描述)。 4. 异步保存审计日志:为了不影响主业务,建议使用异步方式保存审计日志。 改进:只记录变化的字段 我们可以通过比较更新前后的对象,找出变化的字段,然后只记录这些字段的变化。 例如: ``` private String buildDescription(Object beforeData, Object afterData) { if (beforeData == null || afterData == null) { // 新增或删除,直接返回整个对象 // 这里省略 } // 使用反射比较两个对象 // 假设是同一个类 Class<?> clazz = beforeData.getClass(); Field[] fields = clazz.getDeclaredFields(); StringBuilder sb = new StringBuilder(); for (Field field : fields) { field.setAccessible(true); try { Object beforeValue = field.get(beforeData); Object afterValue = field.get(afterData); if ((beforeValue == null && afterValue != null) || (beforeValue != null && !beforeValue.equals(afterValue))) { sb.append(field.getName()).append(": ") .append(beforeValue).append(" -> ").append(afterValue).append("; "); } } catch (IllegalAccessException e) { e.printStackTrace(); } } return sb.toString(); } ``` 然后在buildDescription方法中根据操作类型调用不同的生成逻辑。 但注意:删除操作只有beforeData,新增操作只有afterData。 因此,我们可以这样构建: ``` private String buildDescription(String operateType, Object beforeData, Object afterData) { switch (operateType) { case "新增": return "新增数据: " + objectToString(afterData); case "删除": return "删除数据: " + objectToString(beforeData); case "更新": return "更新数据: " + diffObjects(beforeData, afterData); default: return ""; } } private String objectToString(Object obj) { // 简单使用JSON return JSON.toJSONString(obj); } private String diffObjects(Object before, Object after) { // 比较两个对象,返回变化字段的描述 // 实现略,参考上面的反射比较 } ``` 考虑到反射比较的性能和复杂性,也可以使用第三方库,如java-object-diff(https://github.com/SQiShER/java-object-diff)。 总结: 以上方案实现使用切面记录审计日志将数据变化记录到描述字段。但实际应用中需要根据业务调整,特别是获取原始数据的方式。 相关问题: 1. 如何避免获取原始数据时的额外数据库查询? 2. 审计日志记录的数据量很大,如何优化存储? 3. 如何实现审计日志的敏感信息脱敏? 4. 如何异步保存审计日志以提高性能? 5. 如何记录操作失败的情况?</think>### Java 切面实现接口操作审计日志记录 我将提供一套完整的解决方案,使用 Java 切面记录接口操作(新增、删除、编辑)的审计日志将表数据变化映射到审计日志表的描述字段。 #### 1. 定义审计日志注解 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DataAudit { OperationType operation(); // 操作类型 String domain(); // 业务领域 String identifierParam() default "id"; // 标识参数名 } public enum OperationType { CREATE, UPDATE, DELETE } ``` #### 2. 创建审计日志实体类 ```java @Entity @Table(name = "sys_audit_log") public class AuditLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String operation; // 操作类型 private String domain; // 业务领域 private String targetId; // 目标对象ID private String description; // 变化描述(核心字段) private String operator; // 操作人 private LocalDateTime operateTime; private String ipAddress; // 省略getter/setter } ``` #### 3. 实现切面逻辑(核心实现) ```java @Aspect @Component public class DataAuditAspect { @Autowired private AuditLogService auditLogService; @Autowired(required = false) private HttpServletRequest request; // 引用通用切入点(参考引用[3]) @Pointcut("@annotation(dataAudit)") public void auditedMethods(DataAudit dataAudit) {} @Around("auditedMethods(dataAudit)") public Object auditDataChange(ProceedingJoinPoint joinPoint, DataAudit dataAudit) throws Throwable { // 1. 获取方法参数和操作信息 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Object[] args = joinPoint.getArgs(); OperationType operation = dataAudit.operation(); String domain = dataAudit.domain(); // 2. 获取操作前数据(针对更新/删除) Object beforeState = null; if (operation == OperationType.UPDATE || operation == OperationType.DELETE) { beforeState = fetchBeforeState(joinPoint, dataAudit); } // 3. 执行目标方法 Object result = joinPoint.proceed(); // 4. 获取操作后数据(针对创建/更新) Object afterState = null; if (operation == OperationType.CREATE || operation == OperationType.UPDATE) { afterState = (result != null) ? result : args[0]; } // 5. 生成变化描述 String changeDescription = generateChangeDescription( operation, beforeState, afterState ); // 6. 构建保存审计日志 saveAuditLog(dataAudit, changeDescription, beforeState, afterState); return result; } private Object fetchBeforeState(ProceedingJoinPoint joinPoint, DataAudit dataAudit) { try { // 获取目标对象ID Object identifier = getIdentifierValue(joinPoint, dataAudit); if (identifier == null) return null; // 根据领域类型获取对应Repository(需实现) JpaRepository<?, ?> repository = RepositoryLocator.getRepository(dataAudit.domain()); return repository.findById(identifier).orElse(null); } catch (Exception e) { return null; // 降级处理 } } private Object getIdentifierValue(ProceedingJoinPoint joinPoint, DataAudit dataAudit) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String[] paramNames = signature.getParameterNames(); Object[] args = joinPoint.getArgs(); for (int i = 0; i < paramNames.length; i++) { if (dataAudit.identifierParam().equals(paramNames[i])) { return args[i]; } } return null; } private String generateChangeDescription(OperationType operation, Object before, Object after) { if (operation == OperationType.CREATE) { return "创建记录: " + summarizeObject(after); } if (operation == OperationType.DELETE) { return "删除记录: " + summarizeObject(before); } if (operation == OperationType.UPDATE && before != null && after != null) { return "字段变更: " + detectFieldChanges(before, after); } return "数据变更"; } private String summarizeObject(Object obj) { try { // 示例:使用反射获取关键字段 Class<?> clazz = obj.getClass(); Field idField = clazz.getDeclaredField("id"); idField.setAccessible(true); Object idValue = idField.get(obj); return clazz.getSimpleName() + "#" + idValue; } catch (Exception e) { return obj.getClass().getSimpleName(); } } private String detectFieldChanges(Object before, Object after) { // 简化的字段变更检测 Map<String, String> changes = new LinkedHashMap<>(); Class<?> clazz = before.getClass(); for (Field field : clazz.getDeclaredFields()) { try { field.setAccessible(true); Object beforeValue = field.get(before); Object afterValue = field.get(after); if (!Objects.equals(beforeValue, afterValue)) { changes.put(field.getName(), formatValue(beforeValue) + " → " + formatValue(afterValue)); } } catch (Exception ignored) {} } return changes.toString(); } private String formatValue(Object value) { if (value == null) return "null"; if (value instanceof String) return "\"" + value + "\""; return value.toString(); } private void saveAuditLog(DataAudit dataAudit, String description, Object before, Object after) { AuditLog log = new AuditLog(); log.setOperation(dataAudit.operation().name()); log.setDomain(dataAudit.domain()); log.setDescription(description); // 核心描述字段 log.setOperateTime(LocalDateTime.now()); // 设置操作人(从安全上下文获取) Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { log.setOperator(authentication.getName()); } // 设置IP地址 if (request != null) { log.setIpAddress(getClientIp(request)); } // 设置目标ID(从before或after对象提取) if (before != null) { log.setTargetId(extractId(before)); } else if (after != null) { log.setTargetId(extractId(after)); } // 异步保存(参考引用[1]) auditLogService.asyncSave(log); } private String extractId(Object entity) { try { Field idField = entity.getClass().getDeclaredField("id"); idField.setAccessible(true); return String.valueOf(idField.get(entity)); } catch (Exception e) { return "N/A"; } } private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } } ``` #### 4. 辅助工具类 ```java @Component public class RepositoryLocator implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext context) { RepositoryLocator.context = context; } public static JpaRepository<?, ?> getRepository(String domain) { String beanName = StringUtils.uncapitalize(domain) + "Repository"; return (JpaRepository<?, ?>) context.getBean(beanName); } } ``` #### 5. 服务层实现 ```java @Service public class AuditLogService { @Autowired private AuditLogRepository auditLogRepository; @Async public void asyncSave(AuditLog log) { auditLogRepository.save(log); } } ``` #### 6. 使用示例 ```java @Service public class ProductService { @DataAudit(operation = OperationType.CREATE, domain = "Product") public Product createProduct(Product product) { return productRepository.save(product); } @DataAudit( operation = OperationType.UPDATE, domain = "Product", identifierParam = "id" ) public Product updateProduct(Long id, Product product) { product.setId(id); return productRepository.save(product); } @DataAudit( operation = OperationType.DELETE, domain = "Product", identifierParam = "id" ) public void deleteProduct(Long id) { productRepository.deleteById(id); } } ``` ### 关键设计说明 1. **智能数据映射**: - 通过反射自动提取实体ID和关键字段 - 动态定位Repository(基于领域名称) - 自动检测字段级变更(参考`detectFieldChanges`方法) 2. **变化描述生成**: ```java // 创建操作:创建记录: Product#123 // 更新操作:字段变更: {name="Old" → "New", price=100 → 120} // 删除操作:删除记录: Product#123 ``` 3. **性能优化**: - 使用`@Async`异步保存日志 - 仅对变更字段进行记录 - 通过`identifierParam`精准定位参数 4. **异常韧性**: - 所有反射操作添加异常保护 - 提供降级描述生成方案 - 空值安全处理 5. **扩展性设计**: - 支持自定义字段提取逻辑 - 可扩展为多数据源支持 - 支持敏感字段脱敏处理 ### 最佳实践建议 1. **敏感数据处理**: ```java private String formatValue(Object value) { if (value instanceof String str) { if (str.contains("@")) return "[EMAIL]"; if (str.length() > 10) return str.substring(0, 2) + "***"; } return String.valueOf(value); } ``` 2. **批量操作支持**: ```java @DataAudit(operation = OperationType.BATCH_UPDATE, domain = "Product") public void batchUpdate(List<Product> products) { products.forEach(this::updateProduct); } // 在切面中处理集合类型 if (result instanceof Collection<?> collection) { collection.forEach(item -> processItem(item, operation)); } ``` 3. **性能关键系统优化**: ```java // 使用缓存减少数据库查询 @Cacheable(value = "entityState", key = "#domain + #id") public Object getEntityState(String domain, Object id) { return RepositoryLocator.getRepository(domain).findById(id); } ``` 4. **审计日志查询优化**: ```sql -- 添加索引提高查询效率 CREATE INDEX idx_audit_domain ON sys_audit_log(domain); CREATE INDEX idx_audit_operation ON sys_audit_log(operation); CREATE INDEX idx_audit_time ON sys_audit_log(operate_time); ``` ### 相关问题 1. 如何避免审计日志切面中的反射性能开销? 2. 审计日志数据量过大时如何实现分库分表? 3. 如何确保审计日志与业务操作的事务一致性? 4. 对于加密存储的字段,如何正确记录变更? 5. 如何实现审计日志的自动清理和归档机制? 6. 在微服务架构中如何集中管理审计日志
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值