老大喊我用AOP記錄下日誌

老大喊我記錄下API的操做日誌,省得前端甩鍋,主要記錄新增,修改,刪除等操做。我想了下就決定用AOP來實現這個功能。前端

因爲使用的是SpringBoot,因此首先應該在依賴中引入AOP包。java

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
複製代碼

通常引入了AOP以後,通常不用作其餘特殊配置,也不用加上@EnableAspectJAutoProxy註解。可是它仍有兩個屬性須要咱們注意web

# 至關於@EnableAspectJAutoProxy
spring.aop.auto=true

# 默認使用jdk來實現AOP,若是想要使用CGLIB的話,這裏要改爲true
spring.aop.proxy-target-class=false
複製代碼

實現日誌記錄功能

我想要記錄某個API對模塊進行了什麼操做,操做的key,對於修改,刪除來講咱們記錄id,對於新增來講,最開始沒有id,咱們記錄name便可(也能夠是其餘屬性),因此OpLog註解是這樣的spring

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface OpLog {

  // 模塊
  String module();
  // 類型
  String opType();
  // 級別,默認通常
  String level() default OpLevel.COMMON;
  // 須要記錄的key
  String key();
}
複製代碼

爲了讓這個功能更加易用,因此我要求其餘開發者必須在Controller上加上註解@OpLog,爲何是Controller呢?由於API是在Controller上,我並不關心具體業務,我只要記錄對應的操做便可。在Controller層你只須要這要作就好了springboot

@RestController
@RequestMapping("/user")
public class CpGroupController {

  // 注意這裏的userParam和@RequestBody的userParam名字必須要要一致
  @OpLog(module = "用戶管理", opType = "新增", key = "userParam.name")
  @PostMapping("/add")
  public BaseResponse<Boolean> add(@RequestBody UserParam userParam) {
      
      return ResponseUtil.success(userService.add(userParam));
  }
}
複製代碼

接下來就是切面代碼的編寫了,其中咱們要記錄訪問的url以及必要的操做信息。bash

@Component
@Aspect
@Slf4j
public class OpLogAspect {

  @Autowired
  private OperateLogService operateLogService;

  int paramNameIndex = 0;
  int propertyIndexFrom = 1;

  @Pointcut("execution(public * com.generalthink.springboot.web.controller..*.*(..))")
  public void webLogPointcut() {
  }

  @Around(value = "webLogPointcut() && @annotation(opLog)", argNames = "joinPoint,opLog")
  public Object around(ProceedingJoinPoint joinPoint, OpLog opLog) throws Throwable {

    Object objReturn = joinPoint.proceed();

    try {
        OperateLog operationLog = generateOperateLog(joinPoint, opLog);

        operateLogService.save(operationLog);
    } catch (Exception e) {
        log.error("operateLog record error", e);
    }

    return objReturn;
  }

  private OperateLog generateOperateLog(ProceedingJoinPoint joinPoint, OpLog opLog) {

    String requestUrl = extractRequestUrl();

    Object recordKey = getOpLogRecordKey(joinPoint, opLog);

    return OperateLog.builder()
            .module(opLog.module())
            .opType(opLog.opType())
            .level(opLog.level())
            .operateTimeUnix(CommonUtils.getNowTimeUnix())
            .recordKey(recordKey != null ? recordKey.toString() : null)
            .url(requestUrl)
            .operator(getCurrentUser())
            .build();
  }

  // 獲取當前用戶
  private String getCurrentUser() {
    Subject subject = SecurityUtils.getSubject();
    return (String) subject.getPrincipal();
  }

  // 獲取想要記錄的key
  private Object getOpLogRecordKey(ProceedingJoinPoint joinPoint, OpLog opLog) {
    String key = opLog.key();

    if(StringUtils.isEmpty(key)) {
        return null;
    }

    String[] keys = key.split("\\.");

    //入參 value
    Object[] args = joinPoint.getArgs();

    CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();

    // 獲取Controller方法上的參數名稱
    String[] paramNames = codeSignature.getParameterNames();

    Object paramArg = null;

    for (int i = 0; i < paramNames.length; i++) {
        String paramName = paramNames[i];
        if (paramName.equals(keys[paramNameIndex])) {
            paramArg = args[i];
            break;
        }
    }

    if(keys.length == 1) {
        return paramArg;
    }

    // 獲取參數的值
    Object paramValue = null;
    if(null != paramArg) {
        try {
            paramValue = getParamValue(paramArg, keys, propertyIndexFrom);
        } catch (IllegalAccessException e) {
            log.error("parse field error", e);
        }
    }

    return paramValue;
  }

  public Object getParamValue(Object param, String[] keys, int idx) throws IllegalAccessException {
    Optional<Field> fieldOptional = getAllFields(new ArrayList<>(), param.getClass())
            .stream()
            .filter(field -> field.getName().equalsIgnoreCase(keys[idx]))
            .findFirst();

    if(!fieldOptional.isPresent()) {
        return null;
    }
    
    // 設置屬性可訪問,由於bean當中屬性通常都是private
    Field field = fieldOptional.get();
    field.setAccessible(true);


    if(idx + 1 == keys.length) {
        return field.get(param);
    }

    return getParamValue(field.get(param), keys, idx + 1);
  }

  // 遞歸獲取全部Field
  public List<Field> getAllFields(List<Field> fieldList, Class<?> type) {

    // 獲取當前類的全部Field
    fieldList.addAll(Arrays.asList(type.getDeclaredFields()));
    
    // 獲取父類的全部Field
    Class<?> superClass = type.getSuperclass();
    if(superClass != null && superClass != Object.class) {
        getAllFields(fieldList,superClass);
    }

    return fieldList;
  }
  
  // 獲取訪問URL
  private String extractRequestUrl() {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
            .getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();

    return request.getRequestURL().toString();
  }
}

複製代碼

主要就是記錄了操做了什麼模塊,操做內容等等,對於修改和刪除操做咱們能夠記錄id,可是對於save操做,咱們沒有辦法記錄id,只能記錄其餘屬性,好比說name,就能夠記錄保存的數據了app

{
  name: "zhangsan",
  age: 20
}

複製代碼

對於上面的數據就會把"zhangsan"這個值保存下來,後面就能夠經過查詢日誌表知道是誰操做的,修改和刪除也是一樣的.spring-boot

相關文章
相關標籤/搜索