学习总结录 学习总结录
首页
归档
分类
标签
  • Java基础
  • Java集合
  • MySQL
  • Redis
  • JVM
  • 多线程
  • 计算机网络
  • 操作系统
  • Spring
  • Kafka
  • Elasticsearch
  • Python
  • 面试专题
  • 案例实践
  • 工具使用
  • 项目搭建
  • 服务治理
  • ORM框架
  • 分布式组件
  • MiniSpring
  • 设计模式
  • 算法思想
  • 编码规范
友链
关于
GitHub (opens new window)
首页
归档
分类
标签
  • Java基础
  • Java集合
  • MySQL
  • Redis
  • JVM
  • 多线程
  • 计算机网络
  • 操作系统
  • Spring
  • Kafka
  • Elasticsearch
  • Python
  • 面试专题
  • 案例实践
  • 工具使用
  • 项目搭建
  • 服务治理
  • ORM框架
  • 分布式组件
  • MiniSpring
  • 设计模式
  • 算法思想
  • 编码规范
友链
关于
GitHub (opens new window)
  • 案例实践

    • 定时任务
    • 邮件发送
    • 日志管理
      • 关于日志管理
      • AOP自定义注解
        • 相关依赖
        • 日志我们需要记录那些?
        • 自定义注解
        • 新建切面类
        • 配置切入点
        • 配置环绕通知
        • 配置异常通知
        • 相关CRUD操作
        • 其他参数获取
        • HttpServletRequest
        • IP地址
        • UserAgent
        • 游览器
        • 操作平台
        • 操作参数
        • 地址
      • 实际演示
        • 正确操作
        • 错误操作
      • 参考
    • Word生成
    • 数据导入
    • 验证码
  • 工具使用

  • 项目搭建

  • 服务治理

  • ORM框架

  • MiniSpring

  • 案例归档
  • 案例实践
旭日
2023-03-27
目录

日志管理

# 关于日志管理

日志的重要性不言而喻,对于一个后台系统来说,我们需要对一些敏感操作进行记录,比如修改、删除操作。有时候也需要去记录一些用户的操作:比如登录操作、重置密码操作等。而随着系统业务的扩展,我们需要对很多模块进行日志记录,这时候单独对一个业务模块进行日志记录就不太行了,所以我们需要通过AOP的方式随时对某一个业务进行日志记录,进而实现系统的日志管理模块。

# AOP自定义注解

我们想要的效果是在需要记录日志的方法加一个注解,比如现在我们要记录用户的登录操作,当用户登录成功的时候,记录INFO日志,当用户登录失败的时候,记录ERROR日志。这时候我们只需要在登录相关业务模块添加我们的自定义注解即可,这时候登录这个业务模块,如果成功,那么就会自动记录INFO日志,如果失败,就会自动记录ERROR日志。

# 相关依赖

 		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.16</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>

除了AOP以外,其他依赖是为了后续将日志存入数据库,对日志进行管理。

# 日志我们需要记录那些?

作为记录操作的日志,我们应该需要记录请求时间、请求耗时、操作用户、方法、参数、日志描述、日志类型等,同时如果是ERROR日志,我们还需要记录异常信息。这里给出一个大致的设计,具体日志的设计还需要根据业务逻辑来设计。

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

日志实体参考如下:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("sys_log")
public class Log implements Serializable {
    private static final long serialVersionUID = 1L;

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

    private String description;

    private String logType;

    private String method;

    private String params;

    private String requestIp;

    private Long time;

    private String username;

    private String address;

    private String browser;

    private String platform;

    private byte[] exceptionDetail;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

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

# 自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}
  • @Target(ElementType.METHOD):该注解用于方法
  • @Retention(RetentionPolicy.RUNTIME):该注解的生命周期
  • @interface Log: 该注解的名称
  • String value() default "": 用来记录日志的描述

# 新建切面类

@Aspect
@Component
@Slf4j
public class AopLog {
    @Autowired
    private LogService logService;
    
    // 用于后续请求的耗时
    ThreadLocal<Long> currentTime = new ThreadLocal<>();
    
    // 后续代码参考如下
}

# 配置切入点

/**
 * 配置切入点
 */
@Pointcut("@annotation(com.xu.logaop.annotation.Log)")
public void logPointcut() {
    // 该方法无方法体,主要为了让同类中其他方法使用此切入点
}

# 配置环绕通知

     /**
     * 配置环绕通知,使用在方法logPointcut()上注册的切入点
     *
     * @param point join point for advice
     */
    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint point) throws Throwable {
        Object result;
        // 设置开始时间
        currentTime.set(System.currentTimeMillis());
        result = point.proceed();
        // 初始化一个INFO类型的log
        Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
        currentTime.remove();
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        // 用户名根据当前登录的得到
        logService.save("admin", getBrowser(request), getIp(request), getPlatform(request), point, log);
        return result;
    }

# 配置异常通知

    /**
     * 配置异常通知
     *
     * @param point join point for advice
     * @param e exception
     */
    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint point, Throwable e) {
        // 初始化一个ERROR类型的log
        Log log = new Log("ERROR", System.currentTimeMillis() - currentTime.get());
        currentTime.remove();
        log.setExceptionDetail(ThrowableUtil.getStackTrace(e).getBytes());
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        logService.save("admin", getBrowser(request), getIp(request), getPlatform(request), (ProceedingJoinPoint)point, log);
    }
public class ThrowableUtil {
    /**
     * 获取堆栈信息
     */
    public static String getStackTrace(Throwable throwable){
        StringWriter sw = new StringWriter();
        try (PrintWriter pw = new PrintWriter(sw)) {
            throwable.printStackTrace(pw);
            return sw.toString();
        }
    }
}

# 相关CRUD操作

这里我们采用MybatisPlus来进行对日志存储。

Mapper

@Mapper
public interface LogMapper extends BaseMapper<Log> {

}

Service

public interface LogService extends IService<Log> {
    /**
     * 保存日志数据
     * @param username 用户
     * @param browser 浏览器
     * @param ip 请求IP
     * @param os 操作系统
     * @param point
     * @param log 日志实体
     */
    @Async
    void save(String username, String browser, String ip, String os, ProceedingJoinPoint point, Log log);
}
@Service
@Slf4j
public class LogServiceImpl extends ServiceImpl<LogMapper, Log> implements LogService {

    @Override
    public void save(String username, String browser, String ip, String os, ProceedingJoinPoint point, Log log) {
        // 提前构建好这个日志的描述
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        com.xu.logaop.annotation.Log aopLog = method.getAnnotation(com.xu.logaop.annotation.Log.class);

        // 方法路径
        String methodName = point.getTarget().getClass().getName() + "." + signature.getName() + "()";

        // 描述
        if (log != null) {
            log.setDescription(aopLog.value());
        }

        assert log != null;
        log.setRequestIp(ip);
        log.setAddress(getCityInfo(ip));
        log.setMethod(methodName);
        log.setUsername(username);
        log.setParams(getParameter(method, point.getArgs()));
        log.setBrowser(browser);
        log.setPlatform(os);
        if (log.getId() == null) {
            save(log);
        } else {
            updateById(log);
        }
    }
}

在存储日志的时候,最重要的就是拿到我们自定义的日志描述

        // 提前构建好这个日志的描述
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        com.xu.logaop.annotation.Log aopLog = method.getAnnotation(com.xu.logaop.annotation.Log.class);

# 其他参数获取

# HttpServletRequest

public class RequestHolder {

    public static HttpServletRequest getHttpServletRequest() {
        // 得到request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        return request;
    }
}

# IP地址

    /**
     * 获取ip地址
     */
    public static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        String comma = ",";
        String localhost = "127.0.0.1";
        if (ip.contains(comma)) {
            ip = ip.split(",")[0];
        }
        if (localhost.equals(ip)) {
            // 获取本机真正的ip地址
            try {
                ip = InetAddress.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
                log.error(e.getMessage(), e);
            }
        }
        return ip;
    }

# UserAgent

获取UserAgent是为了方便获取游览器、操作平台等数据。

    /**
     * 获得UserAgent
     * @param request HttpServletRequest
     * @return UserAgent
     */
    public static UserAgent getUserAgent(HttpServletRequest request) {
        return UserAgentUtil.parse(request.getHeader("User-Agent"));
    }

# 游览器

    /**
     * 获得游览器
     * @param request HttpServletRequest
     * @return String
     */
    public static String getBrowser(HttpServletRequest request) {
        // 得到UserAgent
        UserAgent userAgent = getUserAgent(request);
        return userAgent.getBrowser().toString().concat(" " + userAgent.getVersion());
    }

# 操作平台

    /**
     * 获得操作平台
     * @param request HttpServletRequest
     * @return String
     */
    public static String getPlatform(HttpServletRequest request) {
        // 得到UserAgent
        UserAgent userAgent = getUserAgent(request);
        return userAgent.getPlatform().toString();
    }

# 操作参数

    /**
     * 根据方法和传入的参数获取请求参数
     */
    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 (!StrUtil.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);
    }

# 地址

这里采用的是高德IP定位API,网上有很多通过IP地址获取地址的方法,但是觉得高德返回API比较简洁,所以选择了高德。这里的代码仅供参考:

    /**
     * 根据IP获得
     * @return
     */
    private String getCityInfo(String ip) {
        try {
            // 优先调用高德API
            JSONObject jsonObject = JSONUtil.parseObj(HttpUtil.get(String.format(ApiConst.GaoDe.IP_URL, ip)));
            if ("[]".equals(jsonObject.get("city", String.class)))
            {
                return jsonObject.get("province", String.class);
            }
            return jsonObject.get("province", String.class) + jsonObject.get("city", String.class);
        } catch (Exception e) {
            log.error(e.getMessage());
            // 异常,默认设置为空
            return "";
        }
    }

相关文档:高德Web服务API文档 (opens new window)

# 实际演示

下面我们通过一个简单的小例子,来对我们自定义的日志注解来进行测试。

@Slf4j
@RestController
public class TestController {

    @Log("消息测试")
    @GetMapping("/test/send")
    public String testError(@RequestParam(value = "msg") String msg) throws Exception {
        if ("错误消息".equals(msg)) {
            throw new Exception("错误消息");
        } else {
            return msg;
        }
    }
}

当我们的发送一般消息的时候,这时候就只会记录我们INFO日志,而当我们发送操作错误消息的时候,就会记录ERROR日志。

# 正确操作

image-20211221220159642

image-20211221220225213

# 错误操作

image-20211221220316071

image-20211221220334171

这样我们就利用AOP实现了自定义日志注解,后续什么模块当我们需要记录日志的时候,我们只需要加上自定义的注解即可完成日志的记录。

# 参考

https://zhuanlan.zhihu.com/p/143434806

https://el-admin.vip/

https://github.com/xkcoding/spring-boot-demo/tree/master/demo-log-aop

上次更新: 2024/06/29, 15:13:44
邮件发送
Word生成

← 邮件发送 Word生成→

最近更新
01
基础概念
10-31
02
Pytorch
10-30
03
Numpy
10-30
更多文章>
Theme by Vdoing | Copyright © 2021-2024 旭日 | 蜀ICP备2021000788号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式