写点什么

SpringBoot 日志收集 -Aop 方式 - 存进数据库

作者:宁在春
  • 2022 年 7 月 30 日
  • 本文字数:12300 字

    阅读完需:约 40 分钟

SpringBoot日志收集-Aop方式-存进数据库

现在大多数项目都会输出日志或保存日志,现在这个大数据时代,数据已经是一种非常非常重要的资源了。

日志也有很大作用的,不要小瞧它哦。😁

很喜欢一句话:“八小时内谋生活,八小时外谋发展”

如果你也喜欢,让我们一起坚持吧!!

共勉😁


我们:待别日相见时,都已有所成

一、前言

本文使用的SpringBoot版本为:2.5.2

1)概述:

日志网络设备、系统及服务程序等,在运作时都会产生一个叫 log 的事件记录;每一行日志都记载着日期、时间、使用者及动作等相关操作的描述

2)介绍:

Windows 网络操作系统设计有各种各样的日志文件,如应用程序日志,安全日志、系统日志、Scheduler 服务日志、FTP 日志、WWW 日志、DNS 服务器日志等等,这些根据你的系统开启的服务的不同而有所不同。


本文介绍的更多的是偏向于行为日志,并非系统日志级别的


我们在系统上进行一些操作时,这些日志文件通常会记录下我们操作的一些相关内容,这些内容也许对我们来说并没有什么用处,但是对系统安全工作人员却相当有用。


比如说有人对系统进行了 IPC 探测,系统就会在安全日志里迅速地记下探测者探测时所用的 IP、时间、用户名等,用 FTP 探测后,就会在 FTP 日志中记下 IP、时间、探测所用的用户名等。

3)使用场景:

简单介绍几个~~(我还菜很多不晓得,狗头保命😂)~~


  1. 排查 bug,从日志查看错误出现地方

  2. 异地登录。(登录日志会记录下你的 Ip)


对了哈,本文更多的是提供一个方法、思路和用一个完整案例来让大家对 SpringBoot-注解 Aop 记录日志有一个认识

二、前期准备

案例


使用 SpringBoot 的 Aop 方式,将访问者的信息写入数据库中。


项目结构



说明:因为习惯了用 MybatisPlus,拿了之前的完整配置,所以看起来 java 文件有多,但是关于 log 的其实并不复杂,代码中也带有注释, 请放心食用。


对 MybatisPlus 感兴趣的可以点👉SpringBoot整合MybatisPlus

2.1、数据库

tb_user 表


CREATE TABLE `tb_user`  (  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `passwrod` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `deleted` int(1) NOT NULL DEFAULT 0,  `create_time` datetime(0) NOT NULL COMMENT '创建时间',  `update_time` datetime(0) NOT NULL COMMENT '修改时间',  PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `tb_user` VALUES ('1', '宁在春', '123456', 0, '2021-07-23 14:32:46', '2021-07-29 23:56:10');INSERT INTO `tb_user` VALUES ('2', '青冬栗', 'qwerasd', 0, '2021-07-23 15:02:02', '2021-07-23 15:49:55');
复制代码


tb_log 表


DROP TABLE IF EXISTS `tb_log`;CREATE TABLE `tb_log`  (  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `user_id` int(10) NOT NULL,  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `login_ip` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `type` int(10) NOT NULL,  `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `operation` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,  `create_time` datetime(0) NULL DEFAULT NULL,  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '',  `update_time` datetime(0) NULL DEFAULT NULL,  PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `tb_log` VALUES ('e5b49465-b20a-453f-b15c-b284733f2f8e', 1, '宁在春', '0:0:0:0:0:0:0:1', 1, '127.0.0.1', '查询用户信息', '2021-08-15 01:04:31', '', '2021-08-15 01:04:31');
复制代码

2.2、导入依赖

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>2.5.2</version>    <relativePath/> <!-- lookup parent from repository --></parent><dependencies>    <!--spring切面aop依赖-->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-aop</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter</artifactId>    </dependency>    <dependency>        <groupId>com.baomidou</groupId>        <artifactId>mybatis-plus-boot-starter</artifactId>        <version>3.4.1</version>    </dependency>    <dependency>        <groupId>com.alibaba</groupId>        <artifactId>druid-spring-boot-starter</artifactId>        <version>1.2.6</version>    </dependency>    <dependency>        <groupId>com.alibaba</groupId>        <artifactId>fastjson</artifactId>        <version>1.2.72</version>    </dependency>    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>8.0.23</version>    </dependency>    <dependency>        <groupId>org.projectlombok</groupId>        <artifactId>lombok</artifactId>    </dependency>    <dependency>        <groupId>cn.hutool</groupId>        <artifactId>hutool-all</artifactId>        <version>5.6.5</version>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>    </dependency>    <dependency>        <groupId>junit</groupId>        <artifactId>junit</artifactId>        <scope>test</scope>    </dependency></dependencies>
复制代码


依赖都是常用的哈,没啥要说的哈。😀

2.3、yml 配置文件

server:  port: 8091spring:  application:    name: springboot-log  # 数据源配置  datasource:    type: com.alibaba.druid.pool.DruidDataSource    driver-class-name: com.mysql.cj.jdbc.Driver    # 阿里的数据库连接池    druid:      username: root      password: 123456      url: jdbc:mysql://localhost:3306/commons_utils?serverTimezone=UTC&useSSL=false&characterEncoding=utf8&serverTimezone=GMT      # 初使化连接数(向数据库要五个连接)      initial-size: 5      # 最小连接数(常住10个连接)      min-idle: 10      # 最大连接数(最多获得10个连接,多到10个数据库将进入一个阻塞状态,等待其他连接释放)      max-active: 20      # 获取连接最长等待时间,单位毫秒      max-wait: 10000      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒      timeBetweenEvictionRunsMillis: 60000      # 配置一个连接在池中最小生存的时间,单位是毫秒      minEvictableIdleTimeMillis: 300000      # 配置一个连接在池中最大生存的时间,单位是毫秒      maxEvictableIdleTimeMillis: 900000  jackson:    date-format: yyyy-MM-dd HH:mm:ss    time-zone: GMT+8mybatis-plus:  configuration:    cache-enabled: true #开启缓存    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志  mapper-locations: classpath:/mapper/*Mapper.xml  global-config:    db-config:      logic-delete-field: flag  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)      logic-delete-value: 1 # 逻辑已删除值(默认为 1)      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
复制代码

2.3、配置自定义 log 注解类

如果需要收集多种日志的话,可以做扩展,增加注解也可,用编码也可,当然如果项目多的话,那么必然是要抽取出来才是最合适的。


(经验不足、如有不妥,请及时提出,蟹蟹各位大佬😁)


/** * 配置自定义log注解类 * @author crush */@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行@Documented //生成文档public @interface MyLog {    /** 操作事件     */    String operation () default "";
/** 日志类型 */ int type ();}
复制代码

2.4、SysLogAspect:切面处理类

import cn.hutool.core.lang.UUID;import com.crush.log.annotation.MyLog;import com.crush.log.entity.LogOperation;import com.crush.log.entity.LogUser;import com.crush.log.mapper.LogOperationMapper;import com.crush.log.utils.IpUtils;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;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.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;
/** 系统日志:切面处理类 */@Aspect@Componentpublic class SysLogAspect { /**我这里是使用log4j2把一些信息打印在控制台上面,可以不写 */ private static final Logger log = LogManager.getLogger(SysLogAspect.class);
/**操作数据库 */ @Autowired private LogOperationMapper logOperationMapper;
/** * 定义切点 @Pointcut * 在注解的位置切入代码 * 这里的意思就是注解写在那个方法上,那个方法就是被切入的。 */ @Pointcut("@annotation(com.crush.log.annotation.MyLog)") public void logPoinCut() { }
//切面 配置通知 @Before("logPoinCut()") //AfterReturning public void saveOperation(JoinPoint joinPoint) { log.info("---------------接口日志记录---------------"); //用于保存日志 LogOperation logOperation = new LogOperation();
// 这里是获得当前请求的request ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = servletRequestAttributes.getRequest();
String requestURL = request.getRequestURL().toString(); logOperation.setUrl(requestURL);
// 客户端ip 这里还可以与之前做一个比较,如果不同的话,就给他推送消息什么的,说异地登录 什么的。 String ip = IpUtils.getIpAddr(request); logOperation.setLoginIp(ip);
//从切面织入点处通过反射机制获取织入点处的方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法 Method method = signature.getMethod();
//获取操作--方法上的Log的值 MyLog myLog = method.getAnnotation(MyLog.class); if (myLog != null) { //保存操作事件 String operation = myLog.operation(); logOperation.setOperation(operation);
//保存日志类型 这里也可以做扩展 根据不同的类型,你可以做不同的操作 int type = myLog.type(); logOperation.setType(type);
log.info("operation="+operation+",type="+type); }
// 操作人账号、姓名(需要提前将用户信息存到session) // 因为这里是模拟 所以偷懒用了个 session // 实际上用了security 获取的应该是当前授权对象的信息 而不是从session 中获取 // 也或者说是从 redis 中获取,这只是提供一个思路,请见谅 LogUser user = (LogUser) request.getSession().getAttribute("user"); if(user != null) { String userId = user.getId(); String userName = user.getUsername(); logOperation.setUserId(userId); logOperation.setUsername(userName); System.out.println(user); } log.info("url="+requestURL,"ip="+ip); //调用service保存Operation实体类到数据库 //我id使用的是UUID,不需要的可以注释掉 String id = UUID.randomUUID().toString().replace("-",""); logOperation.setId(id); logOperationMapper.insert(logOperation); }}
复制代码

2.5、MybatisPlus 相关配置类

MybatisPlusConfig


/** * @EnableTransactionManagement :开启事务 * @Author: crush * @Date: 2021-07-23 14:14 * version 1.0 */@Configuration@EnableTransactionManagement@MapperScan("com.crush.log.mapper")public class MybatisPlusConfig {
/*** 分页*/ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 注册乐观锁 插件 return mybatisPlusInterceptor; }
/** 配置数据源 druid*/ @Bean @Primary @ConfigurationProperties("spring.datasource.druid") public DruidDataSource druidDataSource() { return DruidDataSourceBuilder.create().build(); }}
复制代码


MyMetaObjectHandler:自动填充


/** * 填充创建和修改时间 * @Author: crush * @Date: 2021-07-23 14:14 */@Slf4j@Componentpublic class MyMetaObjectHandler implements MetaObjectHandler {
@Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.setFieldValByName("createTime", LocalDateTime.now(),metaObject); this.setFieldValByName("updateTime",LocalDateTime.now(),metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.setFieldValByName("updateTime",LocalDateTime.now(),metaObject); }}
复制代码


LocalDateTimeSerializerConfig:配置全局的 LocalDateTime 格式化


@Configurationpublic class LocalDateTimeSerializerConfig {    @Value("${spring.jackson.date-format}")    private String DATE_TIME_PATTERN;        @Value("${spring.jackson.date-format}")    private  String DATE_PATTERN ;    /*** string转localdate*/    @Bean    public Converter<String, LocalDate> localDateConverter() {        return new Converter<String, LocalDate>() {            @Override            public LocalDate convert(String source) {                if (source.trim().length() == 0) {                    return null;                }                try {                    return LocalDate.parse(source);                } catch (Exception e) {                    return LocalDate.parse(source, DateTimeFormatter.ofPattern(DATE_PATTERN));                }            }        };    }
/** * string转localdatetime*/ @Bean public Converter<String, LocalDateTime> localDateTimeConverter() { return new Converter<String, LocalDateTime>() { @Override public LocalDateTime convert(String source) { if (source.trim().length() == 0) { return null; } // 先尝试ISO格式: 2019-07-15T16:00:00 try { return LocalDateTime.parse(source); } catch (Exception e) { return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)); } } }; }
/** * 统一配置 LocalDateTime 格式化*/ @Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { JavaTimeModule module = new JavaTimeModule(); LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer); return builder -> { builder.simpleDateFormat(DATE_TIME_PATTERN); builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN))); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))); builder.modules(module); }; }}
复制代码

2.6、IpUtils

import javax.servlet.http.HttpServletRequest;import java.net.InetAddress;import java.net.UnknownHostException;
/** * 获取IP方法 */public class IpUtils{ public static String getIpAddr(HttpServletRequest request) { if (request == null) { return "unknown"; } 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("X-Forwarded-For"); } 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.getHeader("X-Real-IP"); }
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); }
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; }
public static boolean internalIp(String ip) { byte[] addr = textToNumericFormatV4(ip); return internalIp(addr) || "127.0.0.1".equals(ip); }
private static boolean internalIp(byte[] addr) { if (addr == null || addr.length < 2) { return true; } final byte b0 = addr[0]; final byte b1 = addr[1]; // 10.x.x.x/8 final byte SECTION_1 = 0x0A; // 172.16.x.x/12 final byte SECTION_2 = (byte) 0xAC; final byte SECTION_3 = (byte) 0x10; final byte SECTION_4 = (byte) 0x1F; // 192.168.x.x/16 final byte SECTION_5 = (byte) 0xC0; final byte SECTION_6 = (byte) 0xA8; switch (b0) { case SECTION_1: return true; case SECTION_2: if (b1 >= SECTION_3 && b1 <= SECTION_4) { return true; } case SECTION_5: if (b1 == SECTION_6) { return true; } default: return false; } }
/** * 将IPv4地址转换成字节 * * @param text IPv4地址 * @return byte 字节 */ public static byte[] textToNumericFormatV4(String text) { if (text.length() == 0) { return null; }
byte[] bytes = new byte[4]; String[] elements = text.split("\\.", -1); try { long l; int i; switch (elements.length) { case 1: l = Long.parseLong(elements[0]); if ((l < 0L) || (l > 4294967295L)) return null; bytes[0] = (byte) (int) (l >> 24 & 0xFF); bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 2: l = Integer.parseInt(elements[0]); if ((l < 0L) || (l > 255L)) return null; bytes[0] = (byte) (int) (l & 0xFF); l = Integer.parseInt(elements[1]); if ((l < 0L) || (l > 16777215L)) return null; bytes[1] = (byte) (int) (l >> 16 & 0xFF); bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 3: for (i = 0; i < 2; ++i) { l = Integer.parseInt(elements[i]); if ((l < 0L) || (l > 255L)) return null; bytes[i] = (byte) (int) (l & 0xFF); } l = Integer.parseInt(elements[2]); if ((l < 0L) || (l > 65535L)) return null; bytes[2] = (byte) (int) (l >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 4: for (i = 0; i < 4; ++i) { l = Integer.parseInt(elements[i]); if ((l < 0L) || (l > 255L)) return null; bytes[i] = (byte) (int) (l & 0xFF); } break; default: return null; } } catch (NumberFormatException e) { return null; } return bytes; }
public static String getHostIp() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException ignored) {
} return "127.0.0.1"; }
public static String getHostName() { try{ return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException ignored) { } return "未知"; }}
复制代码

三、业务代码

我这里没有写查看日志的接口,存数据库,管理员可以随时查看这些信息,也可以使用 web 页面、会方便许多。

1、entity

LogUser


/** * @Author: crush * @Date: 2021-08-14 8:43 * version 1.0 */@Data@Accessors(chain = true)@TableName("tb_user")public class LogUser implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String username;
private String passwrod;
/*** 逻辑删除字段 */ @TableLogic private Integer deleted;
/*** 创建时间*/ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime;
/*** 修改时间*/ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;}
复制代码


package com.crush.log.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Builder;import lombok.Data;import lombok.experimental.Accessors;
import java.io.Serializable;import java.time.LocalDateTime;
/** * 日志表 * @author crush */@Data@Accessors(chain = true)@TableName("tb_log")public class LogOperation implements Serializable {
private static final long serialVersionUID = 7925874058046995566L;
private String id; /*** 用户id 操作人ID */ private String userId; /** * 用户名称 关联admin_user */ private String username; /** * 登录ip */ private String loginIp; /** * 操作类型(0登录、1查询、2修改) 这个根据自己需求定义即可 ,还有很多其他方式,这个并不完善,只是刚刚够用的那种 */ private int type;
/** * 操作的url*/ private String url; /** * 操作内容 */ private String operation; /** * 操作时间*/ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime;
/*** 备注*/ private String remark; /*** 修改时间*/ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;}
复制代码

2、mapper

@Repository@Mapperpublic interface LogOperationMapper extends BaseMapper<LogOperation> {}
复制代码


@Repositorypublic interface LogUserMapper extends BaseMapper<LogUser> {}
复制代码

3、Service

public interface ILogUserService extends IService<LogUser> {}
复制代码


@Servicepublic class LogUserServiceImpl extends ServiceImpl<LogUserMapper, LogUser> implements ILogUserService {}
复制代码

4、Controller

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.crush.log.annotation.MyLog;import com.crush.log.entity.LogUser;import com.crush.log.service.ILogUserService;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;import java.util.List;
@RestController@RequestMapping("user")public class UserController {
private static final Logger log = LogManager.getLogger(UserController.class);
@Autowired private ILogUserService userService;

/** * 假装登录,将用户信息存到session(方法是我之前写的懒得改,) * */ @RequestMapping("/login") public String login(@RequestBody LogUser logUser,HttpServletRequest request){ QueryWrapper<LogUser> wrapper = new QueryWrapper<>(); wrapper.eq("username",logUser.getUsername()).eq("passwrod",logUser.getPasswrod()); LogUser user = userService.getOne(wrapper); if(user!=null){ request.getSession().setAttribute("user",user); return "登录成功"; } return "登录失败"; }
/**记录日志*/ @MyLog(operation = "查询用户信息",type = 1) @RequestMapping("/log") public List<LogUser> insertLog(HttpServletRequest request){ List<LogUser> users = userService.list(); return users; }}
复制代码


记得写个主启动类,这我就不写啦。

5、测试

直接启动测试,先登录,再访问/log.



再访问/log



我们再看一下后台输出:


四、自言自语

本文只是给大家提供一个小思路,代码写的较为粗糙,请见谅。😁


还有很多地方可以扩展和完善,大家感兴趣的话,可以多试一试,这样学习才有乐趣啦。😂


日志的话他还会分很多类的,大家可以根据自己的需求扩展。



我知道咱们掘金的大佬,讲话又好听,长的又帅,女朋友随便 new,给小弟一个赞👍,这肯定的吧。😁

发布于: 刚刚阅读数: 3
用户头像

宁在春

关注

一个喜欢文艺风的程序员 2022.07.01 加入

他日凌云,万事胜意

评论

发布
暂无评论
SpringBoot日志收集-Aop方式-存进数据库_aop_宁在春_InfoQ写作社区