SpringBoot AOP完美記錄用戶操作日誌,附源碼

記錄內容

  1. 接口名稱
  2. 瀏覽器名稱
  3. 操作系統
  4. 請求ip
  5. 接口入參、出參
  6. 接口耗時
  7. 。。。。

表結構

 

 

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `module_name` varchar(256) DEFAULT NULL COMMENT '模塊名稱',
  `browser_name` varchar(1024) DEFAULT NULL COMMENT '瀏覽器名稱',
  `os_name` varchar(256) DEFAULT NULL COMMENT '操作系統名稱',
  `ip_addr` varchar(256) DEFAULT NULL COMMENT '請求ip',
  `app_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '服務名稱',
  `class_name` varchar(1024) DEFAULT NULL COMMENT '類名',
  `method_name` varchar(512) DEFAULT NULL COMMENT '方法',
  `request_url` varchar(1024) DEFAULT NULL COMMENT '請求url',
  `request_method` varchar(255) DEFAULT NULL COMMENT '請求方式,POST、GET',
  `request_param` text COMMENT '請求參數',
  `result_text` text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '響應參數',
  `status` tinyint(1) DEFAULT NULL COMMENT '接口狀態(0成功 1失敗)',
  `error_text` text COMMENT '錯誤信息',
  `take_up_time` varchar(64) DEFAULT NULL COMMENT '耗時',
  `edit_table_id` bigint(20) DEFAULT NULL COMMENT '編輯的表主鍵,只有修改時纔有值',
  `edit_table_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '編輯的表名稱,只有修改時纔有值',
  `create_time` datetime DEFAULT NULL COMMENT '操作時間',
  `create_user_id` bigint(20) DEFAULT NULL COMMENT '創建人id',
  `create_phone_number` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '創建人手機號',
  `create_user_name` varchar(64) DEFAULT NULL COMMENT '創建人姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系統操作日誌';

SET FOREIGN_KEY_CHECKS = 1;
sys_log.sql

添加依賴 

           <!-- AOP -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
                <version>${spring.boot.version}</version>
            </dependency>
            <!-- 獲取瀏覽器信息 -->
            <dependency>
                <groupId>eu.bitwalker</groupId>
                <artifactId>UserAgentUtils</artifactId>
                <version>1.21</version>
            </dependency>

自定義註解(一)

import java.lang.annotation.*;

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

備註:被該註解修飾的方法,會被記錄到日誌中

自定義註解(二)

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogPlus {
    /**
     * 編輯的表主鍵
     * @return
     */
    String editTableId() default "id";

    /**
     * 編輯的表名稱
     * @return
     */
    String editTableName() default "未知";
}

備註:被該註解修飾的類,會記錄從表的id值和從表的表名(用於記錄某張表的一行記錄,歷史修改信息,不需要可忽略)

日誌表實體類

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * 系統操作日誌
 * </p>
 *
 * @author chenyanbin
 * @since 2021-10-13
 */
@Data
@TableName("sys_log")
@ApiModel(value = "SysLogDO對象", description = "系統操作日誌")
public class LogDO implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "主鍵")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "模塊名稱")
    private String moduleName;

    @ApiModelProperty(value = "瀏覽器名稱")
    private String browserName;

    @ApiModelProperty(value = "操作系統名稱")
    private String osName;

    @ApiModelProperty(value = "請求ip")
    private String ipAddr;

    @ApiModelProperty(value = "服務名稱")
    private String appName;

    @ApiModelProperty(value = "類名")
    private String className;

    @ApiModelProperty(value = "方法")
    private String methodName;

    @ApiModelProperty(value = "請求url")
    private String requestUrl;

    @ApiModelProperty(value = "請求方式,POST、GET")
    private String requestMethod;

    @ApiModelProperty(value = "請求參數")
    private String requestParam;

    @ApiModelProperty(value = "響應參數")
    private String resultText;

    @ApiModelProperty(value = "接口狀態(0成功 1失敗)")
    private Byte status;

    @ApiModelProperty(value = "錯誤信息")
    private String errorText;

    @ApiModelProperty(value = "耗時")
    private String takeUpTime;

    @ApiModelProperty(value = "編輯的表主鍵,只有修改時纔有值")
    private Long editTableId;

    @ApiModelProperty(value = "編輯的表名稱,只有修改時纔有值")
    private String editTableName;

    @ApiModelProperty(value = "操作時間")
    private Date createTime;

    @ApiModelProperty(value = "創建人id")
    private Long createUserId;

    @ApiModelProperty(value = "創建人手機號")
    private String createPhoneNumber;

    @ApiModelProperty(value = "創建人姓名")
    private String createUserName;

}

Mapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface LogMapper extends BaseMapper<LogDO> {
}

Aspect (AOP)

import com.alibaba.fastjson.JSON;
import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;

/**
 * 操作日誌處理
 *
 * @Author:chenyanbin
 */
@Slf4j
@Aspect
@Component
public class LogAspect { @Autowired LogMapper logMapper; @Value("${spring.application.name}") private String appNname; @Pointcut("@annotation(com.yida.annotation.Log)") public void logPoint() { } @Around("logPoint()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Object result = null; LogDO logDO = new LogDO(); try { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); HttpServletRequest request = (HttpServletRequest) requestAttributes .resolveReference(RequestAttributes.REFERENCE_REQUEST); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); //瀏覽器對象 Browser browser = userAgent.getBrowser(); //操作系統對象 OperatingSystem operatingSystem = userAgent.getOperatingSystem(); logDO.setBrowserName(browser.getName()); ApiOperation aon = methodSignature.getMethod().getAnnotation(ApiOperation.class); if (aon != null) { logDO.setModuleName(aon.value()); } logDO.setOsName(operatingSystem.getName()); logDO.setIpAddr(CommonUtil.getIpAddr(request)); logDO.setAppName(appNname); logDO.setClassName(joinPoint.getTarget().getClass().getName()); logDO.setMethodName(methodSignature.getMethod().getName()); logDO.setRequestUrl(request.getRequestURI()); logDO.setRequestMethod(request.getMethod()); //獲取請求參數 CommonUtil.getRequestParam(joinPoint, methodSignature, logDO); logDO.setResultText(JSON.toJSONString(result)); logDO.setStatus((byte) 0); logDO.setCreateTime(CommonUtil.getCurrentDate()); logDO.setCreateUserId(CommonUtil.getCurrentUserId()); logDO.setCreatePhoneNumber(CommonUtil.getCurrentPhoneNumber()); logDO.setCreateUserName(CommonUtil.getCurrentUserName()); long startTime = System.currentTimeMillis(); result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); logDO.setTakeUpTime(String.format("耗時:%s 毫秒", endTime - startTime)); logDO.setResultText(result.toString()); } catch (Exception e) { logDO.setStatus((byte) 1); if (e instanceof BizException) { BizException bizException = (BizException) e; result = JsonData.buildCodeAndMsg(bizException.getCode(), bizException.getMessage()); logDO.setErrorText(result.toString()); } else if (e instanceof RpvException) { RpvException ve = (RpvException) e; result = JsonData.buildCodeAndMsg(ve.getCode(), ve.getMessage()); logDO.setErrorText(result.toString()); } else { logDO.setErrorText(e.getMessage()); result = e.getMessage(); } } finally { logMapper.insert(logDO); } return result; } }
String agent=request.getHeader("User-Agent");
//解析agent字符串
UserAgent userAgent = UserAgent.parseUserAgentString(agent);
//獲取瀏覽器對象
Browser browser = userAgent.getBrowser();
//獲取操作系統對象
OperatingSystem operatingSystem = userAgent.getOperatingSystem();

System.out.println("瀏覽器名:"+browser.getName());
System.out.println("瀏覽器類型:"+browser.getBrowserType());
System.out.println("瀏覽器家族:"+browser.getGroup());
System.out.println("瀏覽器生產廠商:"+browser.getManufacturer());
System.out.println("瀏覽器使用的渲染引擎:"+browser.getRenderingEngine());
System.out.println("瀏覽器版本:"+userAgent.getBrowserVersion());
        
System.out.println("操作系統名:"+operatingSystem.getName());
System.out.println("訪問設備類型:"+operatingSystem.getDeviceType());
System.out.println("操作系統家族:"+operatingSystem.getGroup());
System.out.println("操作系統生產廠商:"+operatingSystem.getManufacturer());

其他類

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.*;

/**
 * 公共工具類
 *
 * @Author:chenyanbin
 */
@Slf4j
public class CommonUtil {
    private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    /**
     * 獲取ip
     *
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根據網卡取本機配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 對於通過多個代理的情況,第一個IP爲客戶端真實IP,多個IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }

    /**
     * 獲取當前時間戳
     *
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis();
    }

    /**
     * 獲取當前日期
     *
     * @return
     */
    public static Date getCurrentDate() {
        return new Date();
    }

    /**
     * 獲取當前操作用戶的主鍵id
     *
     * @return
     */
    public static Long getCurrentUserId() {
        LoginUser loginUser = getLoginUser();
        if (loginUser == null) {
            return null;
        }
        return loginUser.getId();
    }

    private static LoginUser getLoginUser() {
        return JwtFilter.threadLocal.get();
    }

    /**
     * 獲取當前操作用戶的手機號
     *
     * @return
     */
    public static String getCurrentPhoneNumber() {
        LoginUser loginUser = getLoginUser();
        if (loginUser == null) {
            return null;
        }
        return loginUser.getPhoneNumber();
    }

    /**
     * 獲取當前操作用戶的名稱
     *
     * @return
     */
    public static String getCurrentUserName() {
        LoginUser loginUser = getLoginUser();
        if (loginUser == null) {
            return null;
        }
        return loginUser.getUserName();
    }

    /**
     * 判斷當前用戶是否管理員
     */
    public static boolean isAdmin() {
        return getCurrentUserId() == 1;
    }


    /**
     * 獲取請求參數
     *
     * @param joinPoint 切入點
     * @param signature 方法簽名
     * @param logDO     日誌對象
     */
    public static void getRequestParam(ProceedingJoinPoint joinPoint, MethodSignature signature, LogDO logDO) {
        // 參數值
        Object[] args = joinPoint.getArgs();
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
        Method method = signature.getMethod();
        String[] parameterNames = pnd.getParameterNames(method);
        Map<String, Object> paramMap = new HashMap<>(32);
        for (int i = 0; i < parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
            if (args[i] != null) {
                //反射獲取具體的值
                LogPlus logPlus = args[i].getClass().getAnnotation(LogPlus.class);
                if (logPlus != null) {
                    Field f = null;
                    try {
                        f = args[i].getClass().getDeclaredField(logPlus.editTableId());
                        f.setAccessible(true);
                        Object obj = f.get(args[i]);
                        logDO.setEditTableId(Long.valueOf(obj + ""));
                        logDO.setEditTableName(logPlus.editTableName());
                    } catch (Exception e) {
                        log.error("反射獲取編輯的表主鍵異常:{}", e.getMessage());
                    } finally {
                        if (f != null) {
                            f.setAccessible(false);
                        }
                    }
                }
            }
        }
        logDO.setRequestParam(paramMap.toString());
    }
}
CommonUtil.java
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

/**
 * 登錄用戶
 * @Author:chenyanbin
 */
@Data
public class LoginUser {
    /**
     * 主鍵
     */
    private Long id;

    /**
     * 手機號
     */
    @JsonProperty("phone_number")
    private String phoneNumber;

    /**
     * 用戶暱稱
     */
    @JsonProperty("user_name")
    private String userName;

    /**
     * 是否貨主(0是 1否)
     */
    @JsonProperty("cargo_master")
    private byte cargoMaster;

    /**
     * 管理員 (0是 1否)
     */
    private byte admin;
}
LoginUser.java
import lombok.Data;

/**
 * 業務異常類
 * @Author:chenyanbin
 */
@Data
public class BizException extends RuntimeException {
    private int code;
    private String message;

    public BizException(int code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }

    public BizException(BizCodeEnum bizCodeEnum) {
        super(bizCodeEnum.getMessage());
        this.code = bizCodeEnum.getCode();
        this.message = bizCodeEnum.getMessage();
    }
}
BizException.java
import lombok.Data;

/**
 * Request Param Value:請求參數異常
 * @Author:chenyanbin
 */
@Data
public class RpvException extends RuntimeException {
    private int code;
    private String message;

    public RpvException(int code, String message) {
        this.code = code;
        this.message = message;
    }
}
RpvException.java
package com.yida.utils;

import com.yida.enums.BizCodeEnum;

import java.io.Serializable;

/**
 * @Description:統一協議JsonData工具類
 * @Author:chenyanbin
 * @Date:2021/5/9 下午8:09
 * @Versiion:1.0
 */
public class JsonData<T> implements Serializable {
    /**
     * 狀態碼 0 表示成功,1表示處理中,-1表示失敗
     */
    private Integer code;
    /**
     * 數據
     */
    private T data;
    /**
     * 描述
     */
    private String msg;

    private JsonData() {
    }

    private static <T> JsonData<T> build(Integer code, T data, String msg) {
        JsonData json = new JsonData();
        json.setCode(code);
        json.setData(data);
        json.setMsg(msg);
        return json;
    }

    /**
     * 成功,傳⼊數據
     *
     * @return
     */
    public static <T> JsonData<T> buildSuccess() {
        return build(0, (T) "", "");
    }

    /**
     * 默認添加成功
     *
     * @return
     */
    public static <T> JsonData<T> buildAddSuccess() {
        return build(0, (T) "添加成功", "");
    }

    /**
     * 默認修改成功
     *
     * @return
     */
    public static <T> JsonData<T> buildEditSuccess() {
        return build(0, (T) "修改成功", "");
    }

    /**
     * 默認刪除成功
     *
     * @return
     */
    public static <T> JsonData<T> buildRemoveSuccess() {
        return build(0, (T) "刪除成功", "");
    }


    /**
     * 成功,傳⼊數據
     *
     * @param data
     * @return
     */
    public static <T> JsonData<T> buildSuccess(T data) {
        return build(0, data, "");
    }

    /**
     * 失敗,傳⼊描述信息
     *
     * @param msg
     * @return
     */
    public static <T> JsonData<T> buildError(String msg) {
        return build(-1, (T) "", msg);
    }

    /**
     * ⾃定義狀態碼和錯誤信息
     *
     * @param code
     * @param msg
     * @return
     */
    public static <T> JsonData<T> buildCodeAndMsg(int code, String msg) {
        return build(code, null, msg);
    }

    /**
     * 傳⼊枚舉,返回信息
     *
     * @param codeEnum
     * @return
     */
    public static <T> JsonData<T> buildResult(BizCodeEnum codeEnum) {
        return buildCodeAndMsg(codeEnum.getCode(), codeEnum.getMessage());
    }

    /**
     * 判斷接口響應是否成功,只是判斷狀態碼是否等於:0
     *
     * @param data
     * @return
     */
    public static boolean isSuccess(JsonData data) {
        return data.getCode() == 0;
    }

    /**
     * 判斷接口響應是否失敗,狀態碼除了0以外的,默認調用失敗
     *
     * @param data
     * @return
     */
    public static boolean isFailure(JsonData data) {
        return !isSuccess(data);
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public String toString() {
        return "JsonData{" +
                "code=" + code +
                ", data=" + data +
                ", msg='" + msg + '\'' +
                '}';
    }
}
JsonData.java

控制器添加@Log註解

    @ApiOperation("用戶登錄")
    @PostMapping("login")
    @Log
    public JsonData login(
            @ApiParam("用戶登錄對象") @RequestBody UserLoginRequest userLoginRequest
    ) {
        return userService.login(userLoginRequest);
    }

修改接口,json對象添加@LogPlus

@Data
@ApiModel(value = "角色編輯對象", description = "用戶編輯請求對象")
@LogPlus(editTableId = "id", editTableName = "sys_role")
public class RoleEditRequest {
    @ApiModelProperty(value = "主鍵id", example = "-1")
    @DecimalMin(value = "1", message = "角色id最小爲1")
    private Long id;

    @ApiModelProperty(value = "角色名稱", example = "貨主")
    @JsonProperty("role_name")
    @NotBlank(message = "角色名稱不能爲空")
    private String roleName;

    @ApiModelProperty(value = "角色狀態(0正常 1停用)", example = "0")
    @Min(value = 0, message = "角色狀態(0正常 1停用)")
    @Max(value = 1, message = "角色狀態(0正常 1停用)")
    private byte status;

    @ApiModelProperty(value = "備註", example = "貨主角色")
    private String remark;

    @ApiModelProperty(value = "菜單id列表", example = "[1,2,3,4]")
    private List<Long> menuIds;
}

效果演示

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章