本文参考EL-ADMIN 后台管理系统,学习相关AOP(Aspect-Oriented Programming:面向切面编程),并实现使用AOP进行日志管理。特别感谢该项目的源代码作者:elunez


文章目录

  • 一、准备阶段
  • 二、编码阶段
  • 三、使用阶段


一、准备阶段

SpringBoot中需要先引入aop依赖

<!-- Mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus</artifactId>
    <version>3.4.1</version>
</dependency>
<!-- aop切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- ip处理 -->
<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
    <version>1.7.2</version>
</dependency>
<!-- 解析客户端操作系统、浏览器信息 -->
<dependency>
    <groupId>nl.basjes.parse.useragent</groupId>
    <artifactId>yauaa</artifactId>
    <version>5.23</version>
</dependency>

创建一张sys_log的日志表

CREATE TABLE `sys_log` (
  `log_id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `log_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `method` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `params` text CHARACTER SET utf8 COLLATE utf8_general_ci,
  `request_ip` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `time` bigint DEFAULT NULL,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `browser` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `exception_detail` text CHARACTER SET utf8 COLLATE utf8_general_ci,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`log_id`) USING BTREE,
  KEY `log_create_time_index` (`create_time`) USING BTREE,
  KEY `inx_log_type` (`log_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3537 DEFAULT CHARSET=utf8mb3 COMMENT='系统日志';

二、编码阶段

日志工具类

/**
 * @author: Vainycos
 * @description 日志服务工具类
 * @date: 2021/9/29 15:07
 */
@Slf4j
public class LogUtils {

    private static boolean ipLocal = false;

    private static File file = null;

    private static DbConfig config;

    // IP归属地查询
    public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true";

    /**
     * 根据ip获取详细地址
     */
    public static String getCityInfo(String ip) {
        if (ipLocal) {
            return getLocalCityInfo(ip);
        } else {
            return getHttpCityInfo(ip);
        }
    }

    /**
     * 根据ip获取详细地址
     */
    private static String getLocalCityInfo(String ip) {
        try {
            DataBlock dataBlock = new DbSearcher(config, file.getPath())
                    .binarySearch(ip);
            String region = dataBlock.getRegion();
            String address = region.replace("0|", "");
            char symbol = '|';
            if (address.charAt(address.length() - 1) == symbol) {
                address = address.substring(0, address.length() - 1);
            }
            return address;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return "";
    }

    /**
     * 根据ip获取详细地址
     */
    private static String getHttpCityInfo(String ip) {
        String api = String.format(IP_URL, ip);
        JSONObject object = JSONUtil.parseObj(HttpUtil.get(api));
        return object.get("addr", String.class);
    }
}

日志实体类

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_log")
public class LogDO {

    private static final long serialVersionUID = 1L;

    /**
     * 唯一标识
     */
    @TableId(value = "log_id", type = IdType.AUTO)
    private Long id;

    /** 操作用户 */
    private String username;

    /** 描述 */
    private String description;

    /** 方法名 */
    private String method;

    /** 参数 */
    private String params;

    /** 日志类型 */
    private String logType;

    /** 请求ip */
    private String requestIp;

    /** 地址 */
    private String address;

    /** 浏览器  */
    private String browser;

    /** 请求耗时 */
    private Long time;

    /** 异常详细  */
    private byte[] exceptionDetail;

    /** 创建日期 */
    private Timestamp createTime;

    public LogDO(String logType, Long time) {
        this.logType = logType;
        this.time = time;
    }
}

Serivce类

public interface LogService {
    /**
     * 保存日志数据
     * @param username 用户
     * @param browser 浏览器
     * @param ip 请求IP
     * @param joinPoint /
     * @param log 日志实体
     */
    @Async
    void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, LogDO log);
}

Service实现类

@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogMapper logMapper;

    @Override
    public void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, LogDO log) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        MyLog aopLog = method.getAnnotation(MyLog.class);
        // 方法路径
        String methodName = joinPoint.getTarget().getClass().getName() + "." + signature.getName() + "()";
        // 描述
        if (log != null) {
            log.setDescription(aopLog.value());
        }
        assert log != null;
        log.setRequestIp(ip);

        log.setAddress(LogUtils.getCityInfo(log.getRequestIp()));
        log.setMethod(methodName);
        log.setUsername(username);
        log.setParams(getParameter(method, joinPoint.getArgs()));
        log.setBrowser(browser);
        logMapper.insert(log);
    }

    /**
     * 根据方法和传入的参数获取请求参数
     */
    private String getParameter(Method method, Object[] args) {
        List<Object> argList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argList.add(args[i]);
            }
            //将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (!StringUtils.isEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                map.put(key, args[i]);
                argList.add(map);
            }
        }
        if (argList.size() == 0) {
            return "";
        }
        return argList.size() == 1 ? JSONUtil.toJsonStr(argList.get(0)) : JSONUtil.toJsonStr(argList);
    }
}

Mapper接口(此处使用了Mybatis-plus)

@Repository
public interface LogMapper extends BaseMapper<LogDO> {
}

创建annotation包,创建Log注解类

public @interface MyLog {
    String value() default "";
}

创建aspect包,创建LogAspect的日志切面类,并加上@Aspect注解标记为切面

@Component
@Aspect
@Slf4j
public class LogAspect {
    
    ThreadLocal<Long> currentTime = new ThreadLocal<>();

    @Autowired
    private LogService logService;
}

在LogAspect切面类中,需要补充以下方法:

/**
* 配置切入点
*/
@Pointcut("@annotation(com.xxx.MyLog)")
public void logPointcut(){
    // 无方法体,主要在@Pointcut中体现@Log注解类的所在位置
}

/**
 * 配置环绕通知,使用在方法logPoint()上注册切入点
 */
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    Object result;
    currentTime.set(System.currentTimeMillis());
    result = joinPoint.proceed();
    LogDO logDO = new LogDO("INFO", System.currentTimeMillis() - currentTime.get());
    currentTime.remove();
    HttpServletRequest request = getHttpServletRequest();
    logService.save("用户姓名xxx", LogUtils.getBrowser(request), LogUtils.getIp(request),joinPoint, logDO);
    return result;
}

/**
 * 配置异常通知
 *
 * @param joinPoint join point for advice
 * @param e exception
 */
@AfterThrowing(pointcut = "logPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
    LogDO log = new LogDO("ERROR",System.currentTimeMillis() - currentTime.get());
    currentTime.remove();
    log.setExceptionDetail(getStackTrace(e).getBytes());
    HttpServletRequest request = getHttpServletRequest();
    logService.save("用户姓名xxx", LogUtils.getBrowser(request), LogUtils.getIp(request), (ProceedingJoinPoint)joinPoint, log);
}

public static HttpServletRequest getHttpServletRequest() {
    return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}

/**
 * 获取堆栈信息
 */
public static String getStackTrace(Throwable throwable){
    StringWriter sw = new StringWriter();
    try (PrintWriter pw = new PrintWriter(sw)) {
        throwable.printStackTrace(pw);
        return sw.toString();
    }
}

三、使用阶段

在对应的Controller类的方法上,加上自定义的@MyLog注解,并写上该方法的简短说明。

访问/test/go,即可在sys_log表中看到日志记录的信息。

@RestController
@RequestMapping("/test")
public class TestController {
    
    @MyLog("测试方法")
    @GetMapping("/go")
    String go(){
        return "日志记录成功!";
    }
}

后续若有时间将会实现一个简单的切面日志,并开放代码。

参考资料: