Java项目(日志追踪系统)1 -
笔记中涉及资源:
提取码:Coke
一、环境准备及日志测试
①:配置并运行SpringBoot项目
1.可以直接创建SpringBoot项目
2.有时由于网络原因,也可以先创建Maven项目再改造成SpringBoot项目
- 创建Maven工程后
添加SpringBoot父类依赖- spring-boot-starter-parent这个POM比较好用,里面包含大部分常用的开发所需的jar包.
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>
- 添加其他依赖
<!--springmvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
②:springboot中注解讲解:
1.@SpringBootApplication:
Sprnig Boot项目的核心注解,目的是开启自动配置.
里面又包含3个注解:
-
@ComponentScan 注解 在应用程序所在的包上启用 @component扫描(参见最佳实践)
-
@EnableAutoConfiguration 注解启用Spring Boot的自动配置机制
-
@SpringBootConfiguration 注解允许在上下文中注册额外的bean或导入额外的配置类
2.创建启动类 com.build.log.ServerApplication
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
3.启动测试
③:测试logback打印日志
1. 使用Lombok并设置IDEA
- 导入依赖
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
检查 IntellijIDEA->Preferences..->Build,Exection,Deployment->Compiler->Annotation Processors中Enable annotation processing是勾选状态
使用Lombok要勾选这个选项
JAVA 注解处理的相关特性提供于包 javax.annotation.processing 中, 但其已经有一个部分实现的抽象类 AbstractProcessor, 所以最终我们只需要继承这个抽象类即可
2. logback的使用及配置
logback,一个“可靠、通用、快速而又灵活的Java日志框架”,是springboot默认的日志框架.
logback.xml参考:
1.pom依赖
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<!--
引入以上依赖,会自动引入以下jar
logback-classic.x.x.x.jar
logback-core.x.x.x.jar
slf4j-api-x.X.x.jar
注意spring-boot-starter-parent里已集成logback,可直接使用.
2.logback.xml
在工程resources目录下建立logback.xml
-
logback首先会试着查找logback.groovy文件
-
当没有找到时,继续试着查找logback-test.xml文件
-
当没有找到时,继续试着查找logback.xml文件
-
如果仍然没有找到,则使用默认配置(打印到控制台)
3.Spring Boot推荐使用logback-spring.xml来替代logback.xml来配置logback日志
因为,logback.xml加载早于application.properties,所以如果你在logback.xml使用了变量时,而恰好这个变量是写在application.properties时,那么就会获取不到,只要改成logback-spring.xml就可以解决.
- 创建 logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
</pattern>
</layout>
</appender>
<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder>
<pattern>
%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
</pattern>
</encoder>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--路径-->
<fileNamePattern>logs/logbackInfo.%d.log</fileNamePattern>
</rollingPolicy>
</appender>
<appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<pattern>
%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
</pattern>
</encoder>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--路径-->
<fileNamePattern>logs/logbackError.%d.log</fileNamePattern>
</rollingPolicy>
</appender>
<root level="info">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileInfoLog"/>
<appender-ref ref="fileErrorLog"/>
</root>
</configuration>
3. 测试日志打印
- 项目结构
1.创建接口 com.build.log.service.api.TestService
public interface TestService {
String test();
}
2.创建接口实现类 com.build.log.service.TestServiceImpl
@Slf4j
@Service
public class TestServiceImpl implements TestService {
@Override
public String test() {
log.info("Service 打印日志");
return "这是一个测试方法(日志已经打印请查看)~~";
}
}
3.创建控制器 com.build.log.controller.TestController
@Slf4j
@RestController
public class TestController {
@Autowired
private TestService testService;
@GetMapping("test")
public String test() {
log.info("Controller 打印日志");
return testService.test();
}
}
4.访问测试
- 可以看到日志已经输出到文件中了
④:SpringBoot自定义starter
1. 简介:
SpringBoot最强大的功能就是把我们常用的场景抽取成了一个个starter(场景启动器),我们通过引入springboot为我提供的这些场景启动器,我们再进行少量的配置就能使用相应的功能,有时往往我们需要自定义starter,来简化我们对springboot的使用.
常见的starter:
2. 自定义stater
项目名:log-trace
groupId: com.log.trace
SpringBoot官网有建议:For example, assume that you are creating a starter for "acme" and that you name the auto-configuremodule acme-spring-boot-autoconfigure and the starter acme-spring- boot-starter. If you only have one module that combines the two, name it acme-spring-boot-starter.
翻译: 例如,假设您正在为“acme”创建一个启动器,并将自动配置模块命名为acme-spring-boot-autoconfigure,将启动器命名为acme-spring- boot-starter。如果只有一个模块结合了这两个模块,则将其命名为acme-spring-boot-starter。
- 创建工程后
添加SpringBoot父类依赖- spring-boot-starter-parent这个POM比较好用,里面包含大部分常用的开发所需的jar包.
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>
- 在刚刚新创建的子模块中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
- 创建一个目录结构
com.log.trace.starter
⑤:logback结构介绍
<configuration scan="true" scanPeriod="60 seconds" debug="false">
</configuration>
- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true
- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟
- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false
<configuration>下面一共有2个属性分别是:
属性1:设置上下文名称
<contextName>:
每个logger都关联到logger上下文,默认上下文名称为"default"”。但可以使用设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改,可以通过%contextName来打印日志上下文名称.
<contextName>logback</contextName>
属性2:设置变量
<property>:
用来定义变量值的标签,有两个属性,name和value;其中name的值是变量的名称,value的值是变量定义的值.定义的值会被插入到logger上下文中.定义变量后,可以使"${}"来使用变量.
<property name="log.path" value="D:/logback.log"/>
1. 什么是Appender?
Logback将执行日志事件输出的组件称为Appender,是控制输出日志格式和地方的,实现的Appender必须继承ch.qos.logback.core.Appender接口
package ch.qos.logback.core;
import ch.qos.logback.core.spi.ContextAware;
import ch.qos.logback.core.spi.FilterAttachable;
import ch.qos.logback.core.spi.LifeCycle;
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {
String getName();
void doAppend(E var1) throws LogbackException;
void setName(String var1);
}
Appender接口里的大多数方法都是getter和setter,值得注意的是doAppend()方法,它唯一的参数是类型E的对象.类型E的实际类型视logback模块的不同而不同.在logback-classic模块里,E可能是"ILoggingEvent"类型,在logback-access模块里,E可能是"AccessEvent"类型.doAppend()方法是 logback框架里最重要的方法,它负责以适当的格式将记录事件输出到合适的设备.
appender用来格式化日志输出节点,有两个属性name和class,name指定appender名称,class用来指定哪种输出策略,常用就是控制台输出策略和文件输出策略.
2. <encoder>对日志进行格式化
表示对日志进行编码:
-
%d{HH:mm:ss.SSS}——日志输出时间
-
%thread——输出日志的进程名字,这在Web应用以及异步任务处理中很有用
-
%-5level——日志级别,并且使用5个字符靠左对齐
-
%logger{36}——日志输出者的名字
-
%msg——日志消息
-
%n——平台的换行符
3. ThresholdFilter
为系统定义的拦截器,例如我们用ThresholdFilter来过滤掉ERROR级别以下的日志不输出到文件中。如果不用记得注释掉,不然你控制台会发现没日志.
4. AppenderBase
类ch.qos.logback.core.AppenderBase是实现了Appender接口的抽象类,AppenderBase提供所有appender共享的基本功能,比如设置/获取名字的方法,其活动状态和过滤器
AppenderBase是logback里所有appender的超类,尽管是抽象类,AppenderBase实际上实现了Appender接口的doAppend()方法.
public synchronized void doAppend(E eventObject) {
// WARNING: The guard check MUST be the first statement in the
// doAppend() method.
// prevent re-entry.
if (guard) {
return;
}
try {
guard = true;
if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
// ok, we now invoke derived class' implementation of append
this.append(eventObject);
} catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
guard = false;
}
}
这里的doAppend()方法的实现是同步的,确保不同线程对同一个appender的记录是线程安全的.
5. AppenderBase有哪些子类或实现类:
1.UnsynchronizedAppenderBase:
UnsynchronizedAppenderBase类实现AppenderBase这个接口,但是它本身是一个抽象类,需要继承它才能得到真正的实现类,他的doAppend方法是不同步的.
public void doAppend(E eventObject) {
// WARNING: The guard check MUST be the first statement in the
// doAppend() method.
// prevent re-entry.
if (Boolean.TRUE.equals(guard.get())) {
return;
}
try {
guard.set(Boolean.TRUE);
if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
// ok, we now invoke derived class' implementation of append
this.append(eventObject);
} catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
guard.set(Boolean.FALSE);
}
}
2.OutputStreamAppender
OutputStreamAppender是继承自UnsynchronizedAppenderBase的Appender实现类,虽然它已经不是抽象类了,但是实际也是不能直接使用的,它的实现类就是最常见的ConsoleAppender和FileAppender.
3.ConsoleAppender
如同它的名字一样,这个Appender将日志输出到console,更准确的说是System.out 或者System.err.
4.FileAppender:
将日志输出到文件当中,目标文件取决于file属性。是否追加输出,取决于append属性
5.RollingFileAppender
RollingFileAppender继承自FileAppender,提供日志目标文件自动切换的功能。例如可以用日期作为日志分割的条件.
6.SocketAppender及SSLSocketAppender
SocketAppender是被设计用来输出日志到远程实例中的,SocketAppender输出日志采用明文方式,SSLSocketAppender则采用加密方式传输日志.
7.ServerSocketAppender及SSLSeverSocketAppender
SocketAppender作为与日志服务器建立连接的主动方,而ServerSocketAppender是被动的,它监听来自客户端的连接请求.
6. 自定义RedisAppender并测试
1.创建com.build.log.controller.RedisAppender类
public class RedisAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent eventObject) {
System.out.println("appender~~~~~~~~");
}
}
2.src/main/resources/logback-spring.xml中添加以下代码
<appender name="redisAppender" class="com.build.log.controller.RedisAppender">
</appender>
<root level="info">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileInfoLog"/>
<appender-ref ref="fileErrorLog"/>
<appender-ref ref="redisAppender"/>
</root>
3.启动项目测试
⑥:blockingQueue阻塞队列
前言
BlockingQueue很好的解决了多线程中,如何高效安全"传输"数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利.
1.认识BlockingQueue
阻塞队列,顾名思义,首先它是一个队列,在数据结构中所起的作用大致如下图所示:
从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出.
1.常用的队列主要有以下两种:
先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件
多线程环境中,通过队列可以很容易实现数据共享,比如经典的"生产者”和"消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到—定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒),下面两幅图演示了BlockingQueue的两个常见阻塞场景:
2.当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列:
3.当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒:
这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这—切BlockingQueue都给你 一手包办了.
2. BlockingQueue的核心方法
1.获取数据
1.take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入
2.drainTo(): —次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁
3.封装BlockingQueue工具类
1.创建 com.build.log.controller.SysLog 类
/**
* 日志对象
*/
@Data
public class SysLog implements Serializable {
// 应用名称
private String appName;
// 时间戳
private Long timestamp;
// 用于追踪
private String traceId;
// 内容
private String content;
// 日志级别
private String logLevel;
// 时间
private LocalDateTime dateTime;
// 类名
private String className;
// 方法名
private String method;
public static SysLog getSysLog(final ILoggingEvent event){
SysLog sysLog = new SysLog();
long timeStamp = event.getTimeStamp();
sysLog.setLogLevel(event.getLevel().levelStr);
sysLog.setAppName(event.getLoggerContextVO().getName());
sysLog.setContent(event.getFormattedMessage());
sysLog.setMethodName(event.getThreadName());
sysLog.setTimeStamp(timeStamp);
sysLog.setDataTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(timeStamp), ZoneId.systemDefault()));
sysLog.setClassName(event.getLoggerName());
// todo 设置traceId
return sysLog;
}
}
2.创建 com.build.log.controller.UtilsQueue 工具类
/**
* 队列操作工具类
*/
public class UtilsQueue {
/**
* 一次性从blockingQueue获取100条数据
*/
private static final int MAX_ELEMENT = 100;
/**
* 阻塞队列容量2000
*/
private static final BlockingQueue<SysLog> QUEUE_LIST = new LinkedBlockingQueue<>(2000);
/**
* 添加
* @param sysLog 参数
*/
public static void add(SysLog sysLog){
try {
QUEUE_LIST.add(sysLog);
} catch (IllegalStateException e) {
// 如果队列满了 清空队列
QUEUE_LIST.clear();
e.printStackTrace();
}
}
/**
* 从队列中获取数据
* @return 获取数据的集合
*/
public static List<SysLog> pop(){
LinkedList<SysLog> listSysLog = new LinkedList<>();
// 一次性获取100条数据
QUEUE_LIST.drainTo(listSysLog,MAX_ELEMENT);
return listSysLog;
}
}
⑦:封装ThreadPoolExecutor工具类
1. 线程池的3大好处
-
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
2. 线程池的创建
1.ThreadPoolExecutor 有以下四种方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
this.prestartAllCoreThreads();
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
this.prestartAllCoreThreads();
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new ThreadPoolExecutor.RejectHandler());
this.prestartAllCoreThreads();
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new ThreadPoolExecutor.RejectHandler());
this.prestartAllCoreThreads();
}
这里面需要几个参数
1、corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程.
2、workQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
-
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,按FIFO原则进行排序
-
LinkedBlockingQueue:—个基于链表结构的阻塞队列,吞吐量高于ArrayBlockingQueue。静态工厂方法Excutors.newFixedThreadPool()使用了这个队列
-
SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等到另—个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,静态工厂方法Excutors.newCachedThreadPool()使用了这个队列
-
PriorityBlockingQueue:—个具有优先级的无限阻塞队列
3、maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的 任务队列这个参数就没用了。
4、threadFactory(线程工厂):可以通过线程工厂为每个创建出来的线程设置更有意义的名字,如开源框架guava
5、RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取—种策略还处理新提交的任务。它可以有如下四个选项
- AbortPolicy:直接抛出异常,默认情况下采用这种策略
- CallerRunsPolicy:使用调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
更多的时候,我们应该通过实现RejectedExecutionHandler接口来自定义策略,比如记录日志或持久化存储等。
6、keepAliveTime(线程活动时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程利用率。
7、TimeUnit(线程活动时间的单位):可选的单位有天(Days)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒 (NANOSECONDS,千分之一微秒)
2.创建 com.build.log.controller.UtilsThreadPool 类(自定义线程池)
/**
* 线程池工具类
*/
public class UtilsThreadPool {
/**
* 线程池
* @return ThreadPoolExecutor
*/
public static ThreadPoolExecutor logTraceThreadPoolExecutor(){
return new ThreadPoolExecutor(5,
10,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
二、Redis配置和封装
①:封装Redis缓存工具类
1.导入依赖
<!-- springboot种redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
2.需要特别注意pushAII操作:
public void leftPushAll(String key, Collection<?> value){
redisTemplate.opsForList().leftPushAll(key,value);
)
以下面的操作为例子:
List<string> list = UtilsQueue.pop();
leftPushAll("cba", list);
直接将list放进了队列中,那么在redis的队列中是如何存储的呢?
可以看到一个key,放了整个队列,而不是一个key—行数据.这种情况我们需要自己去遍历数据插入.
3.创建 com.build.log.controller.UtilsRedis 工具类
/**
* Redis工具类
*/
public class UtilsRedis {
/**
* Redis模板
*/
private RedisTemplate<String,Object> redisTemplate;
/**
* Redis Key
*/
public static final String LOG_TRACE_QUQEUE_KEY = "log_trace_queue";
/**
* 获取Spring容器种的自身
*/
private static UtilsRedis SELF_UTILS_REDIS;
public UtilsRedis(RedisTemplate< String,Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 放入Redis缓存
*/
public void action(){
List<SysLog> list = UtilsQueue.pop();
if (list.size() > BigDecimal.ZERO.intValue()){
leftPushAll(LOG_TRACE_QUQEUE_KEY,list);
}
}
/**
* 单条队列添加
* @param key
* @param value
*/
private void leftPush(String key, Object value){
redisTemplate.opsForList().leftPush(key, value);
}
/**
* 批量添加
* @param key
* @param value
*/
private void leftPushAll(String key, Collection<SysLog> value){
for (SysLog sysLog : value) {
leftPush(key, sysLog);
}
}
public static UtilsRedis getSelfUtilsRedis() {
return SELF_UTILS_REDIS;
}
public static void setSelfUtilsRedis(UtilsRedis selfUtilsRedis) {
SELF_UTILS_REDIS = selfUtilsRedis;
}
}
②:配置Redis
1.添加 src/main/resources/application.yml 文件
# 开发环境配置
server:
# 服务器端口
port: 10000
servlet:
# 应用的访问路径
context-path: /
# spring的配置
spring:
application:
name: 01_buildLog
# redis配置
redis:
host: 127.0.0.1
port: 6379
# 连接超时时间
timeout: 10s
jedis:
pool:
# 连接池种的最小空闲链接
min-idle: 0
# 连接池中的最大空闲链接
max-idle: 2
# 连接池中最大数据库链接
max-active: 8
# 连接池最大阻塞等待时间
max-wait: -1ms
2.导入依赖
<!-- redis池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
fastjson依赖放在父模块中
<!-- fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.55</version>
</dependency>
3.创建 com.build.log.controller.LogTraceAutoConfiguration 类
@Configuration
public class LogTraceAutoConfiguration {
@Bean
public RedisTemplate<String,Object> logTraceRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 使用fastJson序列化器
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();
// value值序列化 采用fastJson的GenericFastJsonRedisSerializer
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
// key的序列化 StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
@Bean
public UtilsRedis UtilsRedis(RedisTemplate< String,Object> template){
UtilsRedis utilsRedis = new UtilsRedis(template);
UtilsRedis.setSelfUtilsRedis(utilsRedis);
return utilsRedis;
}
}
③:将日志封装进Redis中
1.修改 com.build.log.controller.RedisAppender 中的方法
public class RedisAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
/**
* 自定义线程池
*/
private static ThreadPoolExecutor EXECUTOR = UtilsThreadPool.logTraceThreadPoolExecutor();
@Override
protected void append(ILoggingEvent eventObject) {
UtilsQueue.add(SysLog.getSysLog(eventObject));
UtilsRedis selfUtilsRedis = UtilsRedis.getSelfUtilsRedis();
if (Objects.nonNull(selfUtilsRedis)){
EXECUTOR.execute(selfUtilsRedis::action);
}
}
}
2.测试
④:全链路跟踪TraceId
前言
数据库主键:标示唯┬一条数据,譬如唯一商品,唯一订单
全局事务ID:实现分布式事务一致性的必备良药
请求ID:requestld,seesionld,标示一个请求或者一次会话的生命周期
身份证ID:代表你在中国的唯一标示
学号:你在某个机构的特殊代号
1.全链路跟踪Traceld的作用:
标示一次调用的上下文ID,通过此ID可以获取所做事情的调用链,当请求来时生成—个traceld放在ThreadLocal里,然后打印时去取就行了.
1 Traceld 实现
-
负载均衡:譬如nginx,初始化 traceld 放入header
-
web request:通过fliter获取 header的traceld,无则初始化 traceld
-
rpc调用:通过扩展机制传递traceld ,无则初始化 traceld
-
定时任务@Schedule:通过 注解切面@Traceld, 初始化 traceld
-
消息消费:通过消息传递协议添加tracelD,无则使用注解切面@Traceld初始化traceld
-
线程池或者异步:封装runnable和callable初始化传递traceld或者封装线程池初始化传递traceld
2.封装ThreadLocal 创建 com.build.log.controller.TraceIdThreadLocal 类
/**
* traceId线程对象
*/
public class TraceIdThreadLocal {
/**
* 线程trace对象
*/
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
/**
* 添加操作
* @param value
*/
public static void add(String value){
TRACE_ID.set(value);
}
/**
* 获取操作
* @return
*/
public static String get(){
return TRACE_ID.get();
}
/**
* 删除操作
*/
public static void remove(){
TRACE_ID.remove();
}
}
2.使用拦截器或过滤器实现web request traceId:
1.创建自己的拦截器:
定义一个Interceptor非常简单方式也有几种,我这里简单列举两种:
-
实现Spring的HandlerInterceptor接口:
-
继承实现了Handlerlnterceptor接口的类,例如已经提供的实现了HandlerInterceptor 接口的抽象类HandlerInterceptorAdapter
-
HandlerInterceptor方法介绍
boolean preHandle(HttpServletRequest request, HttpservletResponse response, object handler)throws Exception;
void postHandle(HttpservletRequest request, HttpServletResponse response,object handler, ModelAndView modelAndView)throws Exception;
void afterCompletion(HttpServletRequest request, HttpservletResponse response, object handler, Exception ex)throws Exception;
preHandle: 在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理.
postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理
afterCompletion:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等
- 同时需要根据拦截规则进行注册,创建一个Java类继承WebMvcConfigurationSupport,并重写 addInterceptors方法
在一个项目中WebMvcConfigurationSupport只能存在一个,多个的时候,只有一个会生效.
2创建过滤器:
- 创建一个Java类实现Filter接口,并使用@Component注解标注为组件自动注入bean
- 引入生成全局唯—id jar包:
<!--生成唯一的uuid 生成的是排序的-->
<dependency>
<groupId>com.fasterxml.uuid</groupId>
<artifactId>java-uuid-generator</artifactId>
<version>3.1.4</version>
</dependency>
/**
* traceId拦截器
*/
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
TraceIdThreadLocal.add(Generators.timeBasedGenerator().generate().toString());
filterChain.doFilter(request, response);
}
@Override
public void destroy() {
TraceIdThreadLocal.remove();
}
}
3.修改 com.build.log.controller.LogTraceAutoConfiguration 类 添加以下内容
@Configuration
public class LogTraceAutoConfiguration {
......
@Bean
public FilterRegistrationBean<TraceIdFilter> traceIdFilterBean(){
FilterRegistrationBean<TraceIdFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TraceIdFilter());
registrationBean.setUrlPatterns(Collections.singletonList("/*"));
registrationBean.setOrder(0);
return registrationBean;
}
}
4.修改 com.build.log.controller.SysLog 类 添加以下内容
3. 测试
1.启动项目后查看Redis
2.访问接口后
三、整合内容到自定义starter中
①:引入依赖及代码文件
1.log-trace 包中引入以下依赖
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- redis池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
<!-- springboot种redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<!-- logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.11.0</version>
</dependency>
2.log-trace-spring-boot-starter 包中引入以下依赖
<!-- fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.55</version>
</dependency>
<!--springmvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--生成唯一的uuid 生成的是排序的-->
<dependency>
<groupId>com.fasterxml.uuid</groupId>
<artifactId>java-uuid-generator</artifactId>
<version>3.1.4</version>
</dependency>
3.复制代码
②:配置类进行注册
在resources文件夹下面新建一个META-INF文件,并在下面创建spring.factories文件:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=/
com.XXXX. XXXXXXX
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.log.trace.starter.configuration.LogTraceAutoConfiguration
③:测试
1.删除原本无用的类文件
2.将自定义的starter包安装到本地仓库
3.在 01_buildLog 中引入刚刚安装的jar包
<dependency>
<groupId>com.log.trace</groupId>
<artifactId>log-trace-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
4.修改 logback-spring.xml 文件
5.访问接口然后查看Redis缓存
经过测试发现我们已经将starter集成到了项目中
四、通过npm运行前端项目
①:安装npm
1. 什么是npm及安装
npm是javascript的包管理工具,是前端模块化下的一个标志性产物,简单地地说,就是通过npm下载模块,复用已有的代码,提高工作效率.
下载地址:nodejs.org/dist/
1.需要设置环境变量:
-
从社区的角度:把针对某一特定问题的模块发布到npm的服务器上,供社区里的其他人下载和使用,同时自己也可以在社区里寻找特定的模块的资源,解决问题
-
从团队的角度:有了npm这个包管理工具,复用团队既有的代码也变的更加地方便 2.这里我已经安装过别的版本了(测试一下)
2.利用npm安装包
npm安装的方式——本地安装和全局安装
什么时候用本地/全局安装?
1.当你试图安装命令行工具的时候,例如gruntCLI的时候,使用全局安装
- 全局安装的方式:npm install-g模块名称
2.当你试图通过npm install某个模块,并通过require(XXX')的方式引入的时候,使用本地安装
- 本地安装的方式:npm install 模块名称
本地安装的时候,将依赖包信息写入package.json中
注意一个问题,在团队协作中,一个常见的情景是他人从github上clone你的项目,然后通过npminstall安装必要的依赖,(刚从github上clone下来是没有node__modules的,需要安装)那么根据什么信息安装依赖呢?就是你的package.json中的dependencies和devDepencies。所以,在本地安装的同时,将依赖包的信息(要求的名称和版本)写入package.json中是很重要的!
npm install 模块:安装好后不写入package.json中
npm install 模块 --save 安装好后写入package.json的dependencies中(生产环境依赖)
npm install 模块 --save-dev 安装好后写入package.json的devDepencies中(开发环境依赖)
3.利用npm删除包
1.删除全局模块
npm uninstall -g 包名
2.删除本地模块
npm uninstall 模块
3.删除本地模块时你应该思考的问题:是否将在package.json上的相应依赖信息也消除?
npm uninstall 模块:删除模块,但不删除模块留在package.json中的对应信息
npm uninstall 模块 --save 删除模块,同时删除模块留在package.json中dependencies下的对应信息
npm uninstall 模块 --save-dev 删除模块,同时删除模块留在package.json中devDependencies下的对应信息
4.npm的缺点和安装cnpm
由于npm的源在国外,所以国内用户使用起来各种不方便,于是就有了cnpm
安装cnpm,输入以下命令:
npm install -g cnpm --registry=registry.npm.taobao.org
安装指定版本的cnpm
npm install cnpm@7.1.0 -g --registry=registry.npm.taobao.org
- 安装成功
②:使用cnpm下载前端模块
③:使用node运行前端项目并访问
命令:node app
五、安装Elasticsearch和Kibana
安装笔记链接:juejin.cn/post/717775…
六、集成Elasticsearch
①:集成引入依赖
下面两个包一个不能少,否则会在创建文档的时候报错
引入到 log-trace-server 包中
<!-- https://mvnrepository.com/artifact/org.elasticsearch/elasticsearch -->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.8.0</version>
</dependency>
<!--elasticsearch-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.8.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
客户端使用:
从Elasticsearch7开始,High-level REST Client(HLRC)API的所有功能已经基本完成。对Java客户端而言,官方建议使用这种方式来访问集群,而原来的Transport Client测会被逐步废弃. Elasticsearch支持很多不同的类型的客户端,以Java客户端为例,早期的版本推荐使用TransportClient,默认使用 9300端口,以TCP的方式与集群进行交互。
在Elasticsearch 7中这种客户端将被废弃,转为推荐使用REST Client,包括Low Level REST Client与High Level REST Client两种客户端,均使用HTTP方式访问集群,更加轻量级,兼容性也更好.
Elasticsearch(ES)提供了两种连接方式:
1.transport:通过TCP方式访问ES
对应的库是 org.elasticsearch.client.transport
2.rest:通过HTTP API方式访问 ES
对应的库是:
elasticsearch-rest-client + org.elasticsearch.client.rest,提供 low-level rest API
elasticsearch-rest-high-level-client ,提供 high-level rest API 。从 Elasticsearch 6.0.0-beta1 开始提供
虽然说,ES提供了2 种方式,官方目前建议使用rest 方式,而不是transport方式。并且,transport 在未来的计划中,准备废弃.
②:创建启动类
在log-trace-server包中创建 com.log.trace.server.ServerApplication 启动类
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
③:配置Elasticsearch及索引创建规范
1. 配置Elasticsearch
在 log-trace-server 包中进行以下操作
1.创建配置类 com.log.trace.server.config.RestHighLevelConfig
@Configuration
public class RestHighLevelConfig {
@Bean
public RestHighLevelClient elasticsearchClient(){
return new RestHighLevelClient(RestClient.builder(
new HttpHost("127.0.0.1",9200,"http")));
}
}
2.添加yml配置文件 src/main/resources/application.yml
#开发环境配置
server:
#服务器端口
port: 8806
servlet:
#应用的访问路径
context-path: /
#spring配置
spring:
application:
name: buidLog
#redis配置
redis:
#地址
host: 127.0.0.1
#端口
port: 6379
#连接超时时间
timeout: 10s
lettuce:
pool:
#连接池中的最小空闲连接
min-idle: 0
#连接池中的最大空闲连接
max-idle: 2
#连接池的最大数据库连接数
max-active: 8
#连接池最大阻塞等待时间
max-wait: -1ms
3.创建 com.log.trace.server.utils.UtilsRedis
/**
* Redis工具类
*/
public class UtilsRedis {
/**
* Redis模板
*/
private RedisTemplate<String,Object> redisTemplate;
/**
* Redis Key
*/
public static final String LOG_TRACE_QUQEUE_KEY = "log_trace_queue";
/**
* 有参构造函数
* @param redisTemplate
*/
public UtilsRedis(RedisTemplate< String,Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 获取队列中的一个元素
* @param key
* @return
*/
public Object rightPop(String key){
return redisTemplate.opsForList().rightPop(key);
}
/**
* 获取指定区间的元素
* @param key
* @param count
* @return
*/
public List<Object> rightPopRange(String key, long count){
List<Object> list = new ArrayList<>();
for (long i = 0; i < count; i++) {
Object item = rightPop(key);
if (Objects.nonNull(item)){
list.add(item);
}
}
return list;
}
}
4.创建 com.log.trace.server.config.RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> logTraceRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 使用fastJson序列化器
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();
// value值序列化 采用fastJson的GenericFastJsonRedisSerializer
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
// key的序列化 StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
@Bean
public UtilsRedis UtilsRedis(RedisTemplate< String,Object> template){
return new UtilsRedis(template);
}
}
2. 索引创建规范
1.索引设计:
1)建立订单索引时用“log_trace_yyyyMM”命名。采用日志创建时间,按月建立新的索引
2)为这些订单索引建立别名order
3)写数据时需要根据日志创建时间,把日志数据写到对应的月份索引;读取日志数据时根据别名读取,就可以查询到所有日志数据
2.索引模板:
索引模板,就是创建索引的模板,模板中包含公共的配置(settings)和映射(mappings),并包含一个简单触发条件,及条件满足时使用该模板创建一个新的索引。
模板只在创建索引时应用,更改模板不会对现有索引产生影响。当使用create index API时,作为create index调用的一部分定义的设置/映射将优先于模板中定义的任何匹配设置/映射.
设置好模板后,每次创建order索引,就会自动关联别名,查询数据时就可以通过别名一下把所有数据都获取到.
3.封装一个工具类 Elasticsearch 索引工具 com.log.trace.server.utils.UtilsElasticsearch
@Component
public class UtilsElasticsearch {
@Autowired
protected RestHighLevelClient restHighLevelClient;
/**
* 判断索引是否已经存在
* @param indexName
* @return
*/
public boolean exists(String indexName) throws IOException {
GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
getIndexRequest.humanReadable(true);
return restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
}
/**
* 创建索引
* @param indexName 索引名称
* @param shards 索引主分片
* @param replicas 索引主分片的副本数
* @return
*/
public boolean createIndex(String indexName, int shards, int replicas) throws IOException {
CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
createIndexRequest.settings(Settings.builder().
put("index.number_of_shards",shards).
put("index.number_of_replicas",replicas));
// 设置别名
createIndexRequest.alias(new Alias("log_trace"));
CreateIndexResponse createIndexResponse =
restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
return createIndexResponse.isAcknowledged();
}
/**
* 批量添加
* @param indexName
* @param documents
* @return
*/
public boolean batchAddDocument(String indexName, List<Object> documents) throws IOException {
BulkRequest bulkRequest = new BulkRequest();
documents.forEach(item -> {
bulkRequest.add(new IndexRequest(indexName, "_doc")
.source(JSON.toJSONString(item), XContentType.JSON));
});
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
return !bulk.hasFailures();
}
}
4.创建 com.log.trace.server.init.StartService 类
注意while循环慎用
1.while循环使用不当会阻塞当前线程:
Java线程一般在执行完run方法就可以正常结束,不过有一类线程叫做伺服线程,不间断地执行,往往在run方法中有一个死循环,监视着某些条件,只有当这些条件满足时才能结束。例:
public void run(){
while(true){
somework();
if(finished){
break;
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
有些执行伺服任务的线程,在while(true)这样的死循环内部,是一个阻塞中的方法,当该方法没有返回时,该线程一直处于阻塞当中,根本无法执行其他语句.
@Component
public class StartService {
public static final String 方法 = "方法";
@Autowired
private UtilsRedis utilsRedis;
@Autowired
private UtilsElasticsearch utilsElasticsearch;
@PostConstruct
public void start() {
while (true) {
List<Object> list = utilsRedis.rightPopRange(UtilsRedis.LOG_TRACE_QUQEUE_KEY, 1000);
try {
if (CollectionUtils.isEmpty(list)){
Thread.sleep(5000);
continue;
}
String indexName = "log_trace" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
if (!utilsElasticsearch.exists(indexName)){
utilsElasticsearch.createIndex(indexName,1,0);
}
utilsElasticsearch.batchAddDocument(indexName,list);
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}
}
}
七、测试并运行
①:准备
1.运行Elasticsearch Kibana Redis
②:测试(这里es和Kibana换成了6.8.0)
1.运行log-trace-server 服务(确保服务正常运行)
2.将 log-trace 项目重新安装到本地仓库
3.运行01_buildLog 服务
4.查看Redis缓存
5.查看Elasticsearch 视图管理工具
6.查看日志追踪系统