我正在参与「启航方案」
一、前语
咱们在企业级的开发中,必不可少的是对日志的记载,完成有很多种办法,常见的便是依据AOP+注解
进行保存,但是考虑到程序的流畅和功率,咱们能够运用异步
进行保存,小编最近在spring和springboot
源码中看到有很多的监听处理贯穿前后:这便是闻名的观察者形式
!!
二、基础环境
项目这儿小编就不带我们创建了,直接开始!!
1. 导入依赖
小编这儿的springboot版本是:2.7.4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<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>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
2. 编写yml配置
server:
port: 8088
spring:
datasource:
#运用阿里的Druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.239.131:3306/test?serverTimezone=UTC
username: root
password: root
三、数据库规划
数据库保存日志表的规划,小编一切从简,一般日志多的后期会进行分库分表,或许调配ELK
进行分析,分库分表一般采用依据办法类型,这需求开发人员遵循rest风格
,不然必定都是post
,纯属个人见解哈!!我们能够依据自己的公司的要求进行弥补哈!!
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
`title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '模块标题',
`business_type` int(2) NULL DEFAULT 0 COMMENT '事务类型(0其它 1新增 2修正 3删去)',
`method` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '办法名称',
`request_method` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '恳求办法',
`oper_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '操作人员',
`oper_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '恳求URL',
`oper_ip` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '主机地址',
`oper_time` datetime(0) NULL DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1585197503834284034 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志记载' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
实体类:
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作日志记载表 sys_log
*
*/
@Data
@TableName("sys_log")
public class SysLog {
private static final long serialVersionUID = 1L;
/**
* 日志主键
*/
@TableId
private Long id;
/**
* 操作模块
*/
private String title;
/**
* 事务类型(0其它 1新增 2修正 3删去)
*/
private Integer businessType;
/**
* 恳求办法
*/
private String requestMethod;
/**
* 操作人员
*/
private String operName;
/**
* 恳求url
*/
private String operUrl;
/**
* 操作地址
*/
private String operIp;
/**
* 操作时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operTime;
}
四、主要功用
大体思路: ==先手写一个注解—>切面来进行获取要保存的数据—>一个发布者来发布要保存的数据—>一个监听者监听后保存(异步)==
完好项目架构图如下:
1. 编写注解
import com.example.demo.constant.BusinessTypeEnum;
import java.lang.annotation.*;
/**
* 自定义操作日志记载注解
* @author wangzhenjun
* @date 2022/10/26 15:37
*/
@Target(ElementType.METHOD) // 注解只能用于办法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface Log {
String value() default "";
/**
* 模块
*/
String title() default "测验模块";
/**
* 功用
*/
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}
2. 事务类型枚举
/**
* @author wangzhenjun
* @date 2022/10/26 11:22
*/
public enum BusinessTypeEnum {
/**
* 其它
*/
OTHER(0,"其它"),
/**
* 新增
*/
INSERT(1,"新增"),
/**
* 修正
*/
UPDATE(2,"修正"),
/**
* 删去
*/
DELETE(3,"删去");
private Integer code;
private String message;
BusinessTypeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
3. 编写切片
这儿小编是以切片后进行建议的,当然标准流程是要加反常后的切片,这儿以最简单的进行测验哈,我们按需进行增加!!
import com.example.demo.annotation.Log;
import com.example.demo.entity.SysLog;
import com.example.demo.listener.EventPubListener;
import com.example.demo.utils.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.time.LocalDateTime;
/**
* @author wangzhenjun
* @date 2022/10/26 15:39
*/
@Aspect
@Component
public class SysLogAspect {
private final Logger logger = LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private EventPubListener eventPubListener;
/**
* 以注解所标示的办法作为切入点
*/
@Pointcut("@annotation(com.example.demo.annotation.Log)")
public void sysLog() {}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("sysLog()")
public void doAfter(JoinPoint joinPoint) {
Log log = ((MethodSignature) joinPoint.getSignature()).getMethod()
.getAnnotation(Log.class);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String method = request.getMethod();
String url = request.getRequestURL().toString();
String ip = IpUtils.getIpAddr(request);
SysLog sysLog = new SysLog();
sysLog.setBusinessType(log.businessType().getCode());
sysLog.setTitle(log.title());
sysLog.setRequestMethod(method);
sysLog.setOperIp(ip);
sysLog.setOperUrl(url);
// 从登录中token获取登录人员信息即可
sysLog.setOperName("我是测验人员");
sysLog.setOperTime(LocalDateTime.now());
// 发布音讯
eventPubListener.pushListener(sysLog);
logger.info("=======日志发送成功,内容:{}",sysLog);
}
}
4. ip工具类
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* @author wangzhenjun
* @date 2022/10/26 16:27
* 获取IP办法
*
* @author jw
*/
public class IpUtils {
/**
* 获取客户端IP
*
* @param request 恳求目标
* @return IP地址
*/
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" : getMultistageReverseProxyIp(ip);
}
/**
* 从多级反向署理中取得第一个非unknown IP地址
*
* @param ip 取得的IP地址
* @return 第一个非unknown IP地址
*/
public static String getMultistageReverseProxyIp(String ip) {
// 多级反向署理检测
if (ip != null && ip.indexOf(",") > 0) {
final String[] ips = ip.trim().split(",");
for (String subIp : ips) {
if (false == isUnknown(subIp)) {
ip = subIp;
break;
}
}
}
return ip;
}
/**
* 检测给定字符串是否为不知道,多用于检测HTTP恳求相关
*
* @param checkString 被检测的字符串
* @return 是否不知道
*/
public static boolean isUnknown(String checkString) {
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
}
}
5. 事情发布
事情发布是由ApplicationContext目标进行发布的,直接注入运用即可!
运用观察者形式的==目的==:为了事务逻辑之间的解耦
,提高可扩展性
。
这种形式在spring和springboot底层是经常出现的,我们能够去看看。
发布者只需求重视发布音讯,监听者只需求监听自己需求的,不管谁发的,契合自己监听条件即可。
import com.example.demo.entity.SysLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author wangzhenjun
* @date 2022/10/26 16:38
*/
@Component
public class EventPubListener {
@Autowired
private ApplicationContext applicationContext;
// 事情发布办法
public void pushListener(SysLog sysLogEvent) {
applicationContext.publishEvent(sysLogEvent);
}
}
6. 监听者
@Async
:单独敞开一个新线程去保存,提高功率!
@EventListener
:监听
/**
* @author wangzhenjun
* @date 2022/10/25 15:22
*/
@Slf4j
@Component
public class MyEventListener {
@Autowired
private TestService testService;
// 敞开线程池异步
@Async("asyncExecutor")
// 敞开监听
@EventListener(SysLog.class)
public void saveSysLog(SysLog event) {
log.info("=====行将异步保存到数据库======");
testService.saveLog(event);
}
}
7. 设置线程池
上面会存在一个问题,并发上来后就会出现线程忽然打满,导致OOM
。
所以在阿里开发标准说到:
线程池不允许运用 Executors 去创建,而是经过 ThreadPoolExecutor
的办法,这样的处理方
式让写的同学愈加清晰线程池的运转规则,规避资源耗尽的风险
。
阐明:Executors 返回的线程池目标的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的恳求行列长度为 Integer.MAX_VALUE,可能会堆积很多的恳求,然后导致 OOM。 2)CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建很多的线程,然后导致 OOM。 3)ScheduledThreadPool: 允许的恳求行列长度为 Integer.MAX_VALUE,可能会堆积很多的恳求,然后导致 OOM。
编写自定义线程池PoolConfig
:
/**
* @author wangzhenjun
* @date 2022/11/25 8:47
*/
@Configuration
public class PoolConfig {
@Bean
public ThreadPoolExecutor asyncExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(30),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
return executor;
}
}
上面便是直接运用@Async("asyncExecutor")
就能够直接引用了!!
==发动类上增加:@EnableAsync
,不然@Async
不会生效的!!==
五、测验
1. controller
/**
* @author wangzhenjun
* @date 2022/10/26 16:51
*/
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@Log(title = "测验呢",businessType = BusinessTypeEnum.INSERT)
@GetMapping("/saveLog")
public void saveLog(){
log.info("我便是来测验一下是否成功!");
}
}
2. service
/**
* @author wangzhenjun
* @date 2022/10/26 16:55
*/
public interface TestService {
int saveLog(SysLog sysLog);
}
/**
* @author wangzhenjun
* @date 2022/10/26 16:56
*/
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public int saveLog(SysLog sysLog) {
return testMapper.insert(sysLog);
}
}
3. mapper
这儿运用mybatis-plus进行保存
/**
* @author wangzhenjun
* @date 2022/10/26 17:07
*/
public interface TestMapper extends BaseMapper<SysLog> {
}
4. 测验
5. 数据库
六、总结
铛铛铛,终于完成了!这个实战在企业级必不可少的,每个项目建立人不同,但是结果都是一样的,保存日志到数据,这样能够进行按钮的点击进行计算,分析那个功用是否经常运用,那些东西需求优化。只要是有数据的东西,分析一下总会有收获的!后边日志多了就行分库分表,ELK建立。知道的越多不知道的就越多,这一次下来,知道下面要学什么了嘛!!
能够看下一小编的微信大众号:小王博客基地,文章首发看,欢迎重视,一起交流哈!!