现在大多数项目都会输出日志或保存日志,现在这个大数据时代,数据已经是一种非常非常重要的资源了。
日志也有很大作用的,不要小瞧它哦。😁
很喜欢一句话:
“八小时内谋生活,八小时外谋发展”
。
如果你也喜欢,让我们一起坚持吧!!
共勉
😁
我们:待别日相见时,都已有所成
一、前言
本文使用的SpringBoot版本为:2.5.2
1)概述:
日志
:网络设备、系统及服务程序等,在运作时都会产生一个叫log的事件记录;每一行日志都记载着日期、时间、使用者及动作等相关操作的描述。
2)介绍:
Windows网络操作系统设计有各种各样的日志文件,如应用程序日志,安全日志、系统日志、Scheduler服务日志、FTP日志、WWW日志、DNS服务器日志等等,这些根据你的系统开启的服务的不同而有所不同。
本文介绍的更多的是偏向于行为日志,并非系统日志级别的
。
我们在系统上进行一些操作时,这些日志文件通常会记录下我们操作的一些相关内容,这些内容也许对我们来说并没有什么用处,但是对系统安全工作人员却相当有用。
比如说有人对系统进行了IPC探测,系统就会在安全日志里迅速地记下探测者探测时所用的IP、时间、用户名等,用FTP探测后,就会在FTP日志中记下IP、时间、探测所用的用户名等。
3)使用场景:
简单介绍几个~~(我还菜很多不晓得,狗头保命😂)~~
- 排查bug,从日志查看错误出现地方
- 异地登录。(登录日志会记录下你的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: 8091 spring: 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+8 mybatis-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 @Component public 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 @Component public 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格式化
@Configuration public 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 @Mapper public interface LogOperationMapper extends BaseMapper<LogOperation> { }
@Repository public interface LogUserMapper extends BaseMapper<LogUser> { }
3、Service
public interface ILogUserService extends IService<LogUser> { }
@Service public 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,给小弟一个赞👍,这肯定的吧。😁