elastic-job

一:概述

elastic-job是由当当网开源的一种分布式任务调度框架,由李亮牵头开发的,他们的另外一个开源项目是sharding-jdbc

和xxl-job的中心化调度不同的是,elastic-job采用无中心化设计,核心任务调度逻辑还是采用的quartz,

不过使用的是基于内存的RAMJobStore,这种架构对分布式内存配置数据的一致性有很高要求。

elastic-job采用zookeeper作为分布式协调配置中心的方案。(入坑zk推荐阅读《从Paxos到zookeeper分布式一致性原理与实践》)

二:原理

作业启动流程

1

  • 第一台服务器上线触发主服务器选举。主服务器一旦下线,则重新触发选举,选举过程中阻塞,只有主服务器选举完成,才会执行其他任务。

  • 某作业服务器上线时会自动将服务器信息注册到注册中心,下线时会自动更新服务器状态。

  • 主节点选举,服务器上下线,分片总数变更均会更新重新分片标记。

  • 定时任务触发时,如需重新分片,则通过主服务器分片,分片过程中阻塞,分片结束后才可执行任务。如分片过程中主服务器下线,则先选举主服务器,再分片。

  • 为了维持作业运行时的稳定性,运行过程中只会标记分片状态,不会重新分片。分片仅可能发生在下次任务触发前。

  • 每次分片都会按服务器IP排序,保证分片结果不会产生较大波动。

  • 实现失效转移功能,在某台服务器执行完毕后主动抓取未分配的分片,并且在某台服务器下线后主动寻找可用的服务器执行任务。

作业执行流程

2

三:实现

比如有两个作业服务器实例,namespace为tms-crontab,内部配置了一个名为testJob的定时任务,分片数为2

启动阶段

当作业服务器A启动时,会去zk创建任务,zk目录数据结构如下

3

首先会在根目录下创建namespace的目录,即tms-crontab

然后在tms-crontab下创建任务名为testJob的目录

目录 是否临时节点 目录说明
/tms-crontab/testJob/leader 用于leader选举,leader的主要作用是分配分片到作业服务器
/tms-crontab/testJob/leader/election/latch 主节点选举的分布式锁
/tms-crontab/testJob/leader/election/instance leader服务器IP地址一旦该节点被删除将会触发重新选举重新选举的过程中一切主节点相关的操作都将阻塞
/tms-crontab/testJob/leader/sharding/necessary 此节点存在表示任务下次执行之前需要让leader做重新分片虽然这个节点不是临时节点,但是它不是一直存在的,重新分片完就会被删除掉如果有新的作业服务器加入到集群中来,或者有作业服务器下线,则会创建这个节点
/tms-crontab/testJob/servers/{IP} 作业服务器信息,子节点是作业服务器的IP地址。可在IP地址节点写入DISABLED表示该服务器禁用。
/tms-crontab/testJob/config 存放作业配置信息,json结构
/tms-crontab/testJob/instances/{instanceId} 当前作业运行实例的主键。作业运行实例主键由作业运行服务器的IP地址和PID构成。作业运行实例主键均为临时节点,当作业实例上线时注册,下线时自动清理。注册中心监控这些节点的变化来协调分布式作业的分片以及高可用。可在作业运行实例节点写入TRIGGER表示该实例立即执行一次

在创建这些节点的同时,会注册某些zk目录的监听,比较重要的监听有

目录 监听类型 说明
/tms-crontab/testJob/instances 子节点创建与删除 这个目录的子目录发生变化说明有新作业服务器加入或者下线这个时候需要写重新分片标志,即创建/tms-crontab/testJob/leader/sharding/necessary目录
/tms-crontab/testJob/leader/election/instance 删除 此临时节点目录被删除说明leader下线,所以要做重新选举leader,选举完之后同样要写重新分片标志
/tms-crontab/testJob/config 修改 此节点内容变化说明任务配置变更了,所有的作业服务都需要更新最新配置到内存如果分片数量配置发生变更,则需要写重新分片标志
/tms-crontab/testJob/servers/{IP} 修改 写入DISABLED表示该服务器禁用,变更成任意其他内容说明任务启用
/tms-crontab/testJob/instances/{instanceId} 修改 写入TRIGGER表示该实例立即执行一次,触发之后会清空这个值

之后会调用Quartz的API来发布定时任务执行完以上核心逻辑以后,作业服务器A就算启动完成了后面启动作业服务器B当B启动时,发现需要的目录已经创建好了,而且已经有leader节点了,那么B会自动成为从节点,并把本实例写入到/tms-crontab/testJob/instances/instanceB目录下如果重新分片标志目录不存在,就创建该目录,即前面所说的/tms-crontab/testJob/leader/sharding/necessary目录如果作业服务器B的jobTest任务配置是覆盖模式,那么会直接写入该配置到/tms-crontab/testJob/config目录,作业服务器A的监听器会收到此目录修改事件,从而更新自己内存里的配置数据。

作业服务器B启动完成

运行阶段

当任务到达运行时间点,Quartz就会启动该任务

执行逻辑是先去zk查询分片标志是否存在,即是否存在/tms-crontab/testJob/leader/sharding/necessary目录

如果存在,则先让leader做任务分片,按照任务配置的分片数,尽可能均匀的分配给每个作业服务器。

分配完成的zk目录如下

4

比原先刚启动的时候多了/tms-crontab/testJob/sharding目录,这个目录下的数据结构就是某个分片应该由哪个作业服务器来执行。

分片完成之后,作业服务器会去sharding目录根据自己的instance取对应的分片配置作为任务运行的参数,然后运行业务逻辑。

执行完成之后,如果配置了日志数据库,则会写入任务运行日志。

这样一个定时任务就执行完成了。

失效转移

所谓失效转移,就是在执行任务的过程中遇见异常的情况,这个分片任务可以在其他节点再次执行。

elastic-job的任务配置有个failover,如果开启设置为true的时候且monitorExecution也为true时,才会启动真正的失效转移。

作业服务器监听到zk的instance节点删除事件。如果任务配置了failover等于true,其中某个instance与zk失去联系或被删除,并且失效的节点又不是本身,就会触发失效转移逻辑。

首先,在某个任务实例失效时,elastic-job会在leader节点下面创建failover节点以及items节点。

items节点下会有失效任务实例的原本应该做的分片好。比如,失效的任务实例原来负责分片1。那么items节点下就会有名字叫1的子节点,就代表分片1需要转移到其他节点上去运行。如下图:
5

然后,由于每个存活着的任务实例都会收到zk节点丢失的事件,哪个分片失效也已经在leader节点的failover子节点下。

所以这些或者的任务实例就会争抢这个分片任务来执行。为了保证不重复执行,elastic-job使用了curator的LeaderLatch类来进行选举执行。

在获得执行权后,就会在sharding节点的分片上添加failover节点,并写上任务实例,表示这个故障任务迁移到某一个任务实例上去完成。如下图中的sharding节点上的分片1:

6

执行完成后,会把相应的节点和数据删除,避免下一次重复执行。

任务错过

elastic-job的任务错过机制是错过就错过了,不会补偿。从源码来看其实是设置了quartz的org.quartz.jobStore.misfireThreshold配置为1

而且如果配置的任务是1分钟执行一次,但是任务执行时间为10分钟,那么这10分钟内,这个任务不会再触发。等完成之后才会触发下一次,具体的控制逻辑就不细讲了

7

四:优点与缺点

优点

无中心化设计,更容易封装成简单易用的定时任务工具

基于内存的单机RAMJobstore比基于数据库的JobstoreTX更加轻量高效

支持任务分片,支持故障转移

有UI操作界面

代码质量非常高

缺点或缺陷

不支持动态增加任务

不支持一次性延时任务

如果用UI暂停了某个定时任务,把作业服务器重启,那么这个任务又恢复成启动状态了

任务是强制分片执行的,没有随机选择一个作业服务器执行完整业务逻辑的功能。那么使用起来的话,可能只会让分片0执行完整的业务逻辑,而且每次都是固定的作业服务器执行,这样可能会导致某台机器压力大

界面功能比较弱,目前只有日志展示、暂停任务、手工触发一次任务和停止任务的功能

由于是无中心的化的,所以写数据库日志这块比较的蛋疼。如果写本服务配置的数据库,那么会导致很多数据库都有这两张日志表,而且有的服务压根就没数据库。如果是多配置一个数据源,那么很多项目会有两个datasource,挺占资源的。或直接直接不记日志

总结0425

1.spring cloud 监控
springboot admin
2.限流

1
2
3
4
5
6
7
8
9
10
限流算法:
1.漏桶算法:
2.令牌桶算法:单点应用没问题,但是集群情况不行
3.计数器算法:会有临界问题,通过滑动窗口解决

应用级限流
分布式限流
接入层限流

redis+lua:实现服务级的限流

3.redis key失效
Redis key过期的方式有三种:

  • 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
  • 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
  • 当前已用内存超过maxmemory限定时,触发主动清理策略
    https://www.cnblogs.com/chenpingzhao/p/5022467.html

4.日志表,redis
5.bloom filter 实现排行榜
6.redis实现的功能

1
2
3
4
1.缓存数据
2.计数器
3.限流,基于令牌桶算法
4.分布式锁

7.redis的bitmap,zset

总结0422

1.CaseFormat
可以将其他格式转换为驼峰写法等。
com.google.common.base.CaseFormat是一种实用工具类,以提供不同的ASCII字符格式之间的转换。
例:
System.out.println(CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, “test-data”));
2.springbatch
3.# RowMapper
4.ftp

1
2
3
4
5
6
7
 chroot_local_user=YES  --改为YES chroot_local_user=YES将所有用户限定在主目录内
chroot_list_enable=YES --改为YES chroot_list_enable=YES表示要启用chroot_list_file # (default follows)

chroot_list_file=/etc/vsftpd/chroot_list --注释放开 chroot_list_file这时列出的是那些“不会被限制在主目录下”的用户。
userlist_deny=NO --新增

userlist_enable=YES --默认是YES

总结0423

1.ftp相关操作

1
2
3
4
5
6
7
8
9
10
ftpClient = new FTPClient();
ftpClient.connect(ftpHost); ftpClient.login(ftpUserName, ftpPassword); // 以二进制进行传输 ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); // 用被动模式传输 ftpClient.enterLocalPassiveMode(); // 中文支持 ftpClient.setControlEncoding("UTF-8"); // 设置默认超时时间 ftpClient.setDefaultTimeout(DEFAULT_TIMEOUT);
//上传文件
ftpClient.storeFile(String remote, InputStream local);
//切换工作路径(操作之前需要切换)
ftpClient.changeWorkingDirectory(String pathname)
//获取文件流
ftpClient.retrieveFileStream(String remote)
//创建路径
ftpClient.makeDirectory(String pathname);

2.jssdk

1
2
3
4
5
6
7
8
9
10
11
12
13
联系人是多个渠道账号合并出的一个人

自定义渠道连接:
获取jssdk代码,插入head中:
1.系统事件统计:sdk会自动统计pv事件
2.可以创建动态组:比如用户范围跟网页
创建事件分析,同动态组
3.识别联系人,需要实现相关的js代码:
window.LFAPP.identify({});
4.新增自定义事件:先创建自定义事件,然后代码中写入js:
window.linkflow.sendEvent({});

私有App,获取App Key和App Secret,然后获取Access Token来访问open api 得到Linkflow的资源

3.gzip和zip转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//解压gzip
public static final byte[] unGzip(byte[] data) throws IOException {
GZIPInputStream zin = new GZIPInputStream(new ByteArrayInputStream(data));
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
data = new byte[10240];
int len;
while ((len = zin.read(data)) != -1) {
out.write(data, 0, len);
}
return out.toByteArray();
} finally {
zin.close();
out.close();
}
}

//解压zip
public static final byte[] unzip(byte[] data) throws IOException {
byte[] b = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ZipInputStream zip = new ZipInputStream(bis);
while (zip.getNextEntry() != null) {
byte[] buf = new byte[1024];
int num = -1;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((num = zip.read(buf, 0, buf.length)) != -1) {
baos.write(buf, 0, num);
}
b = baos.toByteArray();
baos.flush();
baos.close();
}
zip.close();
bis.close();
} catch (Exception ex) {
ex.printStackTrace();
}
return b; }

//压缩为gzip
public static final byte[] gzip(byte[] data) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream zout = new GZIPOutputStream(out);
zout.write(data);
zout.close();
return out.toByteArray(); }

springboot启动原理

前言

前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏。所以这次博主就跟你们一起一步步揭开SpringBoot的神秘面纱,让它不在神秘。

正文

我们开发任何一个Spring Boot项目,都会用到如下的启动类

从上面代码可以看出,Annotation定义(@SpringBootApplication)和类定义(SpringApplication.run)最为耀眼,所以要揭开SpringBoot的神秘面纱,我们要从这两位开始就可以了。

SpringBootApplication背后的秘密

虽然定义使用了多个Annotation进行了原信息标注,但实际上重要的只有三个Annotation:

  • @Configuration(@SpringBootConfiguration点开查看发现里面还是应用了@Configuration)

  • @EnableAutoConfiguration

  • @ComponentScan

所以,如果我们使用如下的SpringBoot启动类,整个SpringBoot应用依然可以与之前的启动类功能对等:

每次写这3个比较累,所以写一个@SpringBootApplication方便点。接下来分别介绍这3个Annotation。

@Configuration

这里的@Configuration对我们来说不陌生,它就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@Configuration,SpringBoot社区推荐使用基于JavaConfig的配置形式,所以,这里的启动类标注了@Configuration之后,本身其实也是一个IoC容器的配置类。

举几个简单例子回顾下,XML跟config配置方式的区别:

  • 表达形式层面

    基于XML配置的方式是这样:

而基于JavaConfig的配置方式是这样:

任何一个标注了@Configuration的Java类定义都是一个JavaConfig配置类。

  • 注册bean定义层面

    基于XML的配置形式是这样:

而基于JavaConfig的配置形式是这样的:

任何一个标注了@Bean的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。

  • 表达依赖注入关系层面

    为了表达bean与bean之间的依赖关系,在XML形式中一般是这样:

而基于JavaConfig的配置形式是这样的:

如果一个bean的定义依赖其他bean,则直接调用对应的JavaConfig类中依赖bean的创建方法就可以了。

@ComponentScan

@ComponentScan这个注解在Spring中很重要,它对应XML配置中的元素,@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。

我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。

注:所以SpringBoot的启动类最好是放在root package下,因为默认不指定basePackages。

@EnableAutoConfiguration

个人感觉@EnableAutoConfiguration这个Annotation最为重要,所以放在最后来解读,大家是否还记得Spring框架提供的各种名字为@Enable开头的Annotation定义?比如@EnableScheduling、@EnableCaching、@EnableMBeanExport等,@EnableAutoConfiguration的理念和做事方式其实一脉相承,简单概括一下就是,借助@Import的支持,收集和注册特定场景相关的bean定义。

  • @EnableScheduling是通过@Import将Spring调度框架相关的bean定义都加载到IoC容器。

  • @EnableMBeanExport是通过@Import将JMX相关的bean定义加载到IoC容器。

而@EnableAutoConfiguration也是借助@Import的帮助,将所有符合自动配置条件的bean定义加载到IoC容器,仅此而已!

@EnableAutoConfiguration作为一个复合Annotation,其自身定义关键信息如下:

其中,最关键的要属@Import(EnableAutoConfigurationImportSelector.class),借助EnableAutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。就像一只“八爪鱼”一样

借助于Spring框架原有的一个工具类:SpringFactoriesLoader的支持,@EnableAutoConfiguration可以智能的自动配置功效才得以大功告成!

自动配置幕后英雄:SpringFactoriesLoader详解

SpringFactoriesLoader属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。

配合@EnableAutoConfiguration使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为查找的Key,获取对应的一组@Configuration类

上图就是从SpringBoot的autoconfigure依赖包中的META-INF/spring.factories配置文件中摘录的一段内容,可以很好地说明问题。

所以,@EnableAutoConfiguration自动配置的魔法骑士就变成了:从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。

深入探索SpringApplication执行流程

SpringApplication的run方法的实现是我们本次旅程的主要线路,该方法的主要流程大体可以归纳如下:

1) 如果我们使用的是SpringApplication的静态run方法,那么,这个方法里面首先要创建一个SpringApplication对象实例,然后调用这个创建好的SpringApplication的实例方法。在SpringApplication实例初始化的时候,它会提前做几件事情:

  • 根据classpath里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为Web应用使用的ApplicationContext类型。

  • 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationContextInitializer。

  • 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationListener。

  • 推断并设置main方法的定义类。

2) SpringApplication实例初始化完成并且完成设置后,就开始执行run方法的逻辑了,方法执行伊始,首先遍历执行所有通过SpringFactoriesLoader可以查找到并加载的SpringApplicationRunListener。调用它们的started()方法,告诉这些SpringApplicationRunListener,“嘿,SpringBoot应用要开始执行咯!”。

3) 创建并配置当前Spring Boot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile)。

4) 遍历调用所有SpringApplicationRunListener的environmentPrepared()的方法,告诉他们:“当前SpringBoot应用使用的Environment准备好了咯!”。

5) 如果SpringApplication的showBanner属性被设置为true,则打印banner。

6) 根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用创建什么类型的ApplicationContext并创建完成,然后根据条件决定是否添加ShutdownHook,决定是否使用自定义的BeanNameGenerator,决定是否使用自定义的ResourceLoader,当然,最重要的,将之前准备好的Environment设置给创建好的ApplicationContext使用。

7) ApplicationContext创建好之后,SpringApplication会再次借助Spring-FactoriesLoader,查找并加载classpath中所有可用的ApplicationContext-Initializer,然后遍历调用这些ApplicationContextInitializer的initialize(applicationContext)方法来对已经创建好的ApplicationContext进行进一步的处理。

8) 遍历调用所有SpringApplicationRunListener的contextPrepared()方法。

9) 最核心的一步,将之前通过@EnableAutoConfiguration获取的所有配置以及其他形式的IoC容器配置加载到已经准备完毕的ApplicationContext。

10) 遍历调用所有SpringApplicationRunListener的contextLoaded()方法。

11) 调用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。

12) 查找当前ApplicationContext中是否注册有CommandLineRunner,如果有,则遍历执行它们。

13) 正常情况下,遍历执行SpringApplicationRunListener的finished()方法、(如果整个过程出现异常,则依然调用所有SpringApplicationRunListener的finished()方法,只不过这种情况下会将异常信息一并传入处理)

去除事件通知点后,整个流程如下:

总结

到此,SpringBoot的核心组件完成了基本的解析,综合来看,大部分都是Spring框架背后的一些概念和实践方式,SpringBoot只是在这些概念和实践上对特定的场景事先进行了固化和升华,而也恰恰是这些固化让我们开发基于Sping框架的应用更加方便高效。

logback实践

一、logback介绍

Logback是由log4j创始人设计的一个开源日志组件。LogBack被分为3个组件,logback-core, logback-classic 和 logback-access。

1
2
3
1\. logback-core:提供了LogBack的核心功能,是另外两个组件的基础。
2\. logback-classic:实现了Slf4j的API,所以当想配合Slf4j使用时,需要引入logback-classic。
3\. logback-access:为了集成Servlet环境而准备的,可提供HTTP-access的日志接口。

Logback是要与SLF4J结合起来。这两个组件的官方网站如下:

logback官方网站: logback官方网站

SLF4J官方网站: SLF4J官方网站

  • Slf4j:简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。
  • 在使用SLF4J的时候,不需要在代码中或配置文件中指定你打算使用那个具体的日志系统。

二、slf4j + logback是如何绑定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
1. private static final Logger LOGGER = LoggerFactory.getLogger(LogbackTest.class);

2. 查看LoggerFactory.getLogger()方法
public static Logger getLogger(Class<?> clazz) {
// 获取Logger对象
Logger logger = getLogger(clazz.getName());
if (DETECT_LOGGER_NAME_MISMATCH) {
Class<?> autoComputedCallingClass = Util.getCallingClass();
if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
autoComputedCallingClass.getName()));
Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
}
}
return logger;
}

3. 继续跟进 getLogger()
/**
* Return a logger named according to the name parameter using the
* statically bound {@link ILoggerFactory} instance.
*
* @param name
* The name of the logger.
* @return logger
*/
public static Logger getLogger(String name) {
// 获取日志工厂
ILoggerFactory iLoggerFactory = getILoggerFactory();
// 返回日志对象
return iLoggerFactory.getLogger(name);
}

4. 获取工厂实例
/**
* Return the {@link ILoggerFactory} instance in use.
* <p/>
* <p/>
* ILoggerFactory instance is bound with this class at compile time.
* 编译时绑定工厂实例
*
* @return the ILoggerFactory instance in use
*/
public static ILoggerFactory getILoggerFactory() {
// 没有初始化情况
// 双重检测锁
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
// 初始化
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}

5. 初始化
private final static void performInitialization() {
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}

6. 绑定
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
// 真正的绑定,将具体的实现绑定到slf4j
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
}
}

7. logback-classic: org.slf4j.impl.StaticLoggerBinder
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}

由此可以看出slf4j在编译时就找了具体的日志实现了,也就是 org.slf4j.impl.StaticLoggerBinder。

三、logback对配置文件的加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
1. getSingleton()方法获取logback实例对象,说明在对象之前已经加载了相关的配置文件,跟进 StaticLoggerBinder
static {
// 初始化
SINGLETON.init();
}

private boolean initialized = false;
private LoggerContext defaultLoggerContext = new LoggerContext();
private final ContextSelectorStaticBinder contextSelectorBinder = ContextSelectorStaticBinder.getSingleton();

private StaticLoggerBinder() {
defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
}

public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}

2. 查看 init()
/**
* Package access for testing purposes.
*/
void init() {
try {
try {
// 上下文初始化环境
new ContextInitializer(defaultLoggerContext).autoConfig();
} catch (JoranException je) {
Util.report("Failed to auto configure default logger context", je);
}
// logback-292
if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
}
contextSelectorBinder.init(defaultLoggerContext, KEY);
initialized = true;
} catch (Throwable t) {
// we should never get here
Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
}
}

3. 跟进autoConfig()
public void autoConfig() throws JoranException {
StatusListenerConfigHelper.installIfAsked(loggerContext);
// 寻找默认配置文件
URL url = findURLOfDefaultConfigurationFile(true);
if (url != null) {
configureByResource(url);
} else {
Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
if (c != null) {
try {
c.setContext(loggerContext);
c.configure(loggerContext);
} catch (Exception e) {
throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
.getCanonicalName() : "null"), e);
}
} else {
// 没有找到配置文件,则使用默认的配置器,那么日志只会打印在控制台
BasicConfigurator basicConfigurator = new BasicConfigurator();
basicConfigurator.setContext(loggerContext);
basicConfigurator.configure(loggerContext);
}
}
}

4. findURLOfDefaultConfigurationFile() logback配置文件加载规则
public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
// 获取当前实例的类加载器,目的是在classpath下寻找配置文件
ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
// 先找logback.configurationFile文件
URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
if (url != null) {
return url;
}
// logback.configurationFile文件没找到,再找logback.groovy
url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus);
if (url != null) {
return url;
}
// logback.groovy没找到,再找logback-test.xml
url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
if (url != null) {
return url;
}
// logback-test.xml没找到,最后找logback.xml
return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
}

小结:
编译期间,完成slf4j的绑定已经logback配置文件的加载。slf4j会在classpath中寻找org/slf4j/impl/StaticLoggerBinder.class(会在具体的日志框架如log4j、logback等中存在),找到并完成绑定;同时,logback也会在classpath中寻找配置文件,先找logback.configurationFile、没有则找logback.groovy,若logback.groovy也没有,则找logback-test.xml,若logback-test.xml还是没有,则找logback.xml,若连logback.xml也没有,那么说明没有配置logback的配置文件,那么logback则会启用默认的配置(日志信息只会打印在控制台)。

四、使用步骤

1.引入slf4j、logback相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>

<!-- logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${logback.version}</version>
</dependency>

2.添加配置文件logback.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<!-- 定义日志文件的存储地址 -->
<!--
关于catalina.base解释如下:
catalina.home指向公用信息的位置,就是bin和lib的父目录。
catalina.base指向每个Tomcat目录私有信息的位置,就是conf、logs、temp、webapps和work的父目录。
-->
<property name="LOG_DIR" value="${catalina.base}/logs"/>

<!--
%p:输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%r:输出自应用启动到输出该日志讯息所耗费的毫秒数
%t:输出产生该日志事件的线程名
%f:输出日志讯息所属的类别的类别名
%c:输出日志讯息所属的类的全名
%d:输出日志时间点的日期或时间,指定格式的方式: %d{yyyy-MM-dd HH:mm:ss}
%l:输出日志事件的发生位置,即输出日志讯息的语句在他所在类别的第几行。
%m:输出代码中指定的讯息,如log(message)中的message
%n:输出一个换行符号
-->
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<property name="pattern" value="%d{yyyyMMdd:HH:mm:ss.SSS} [%thread] %-5level %msg%n"/>

<!--
Appender: 设置日志信息的去向,常用的有以下几个
ch.qos.logback.core.ConsoleAppender (控制台)
ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
ch.qos.logback.core.FileAppender (文件)
-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 字符串System.out(默认)或者System.err -->
<target>System.out</target>
<!-- 对记录事件进行格式化 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>

<appender name="SQL_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建 -->
<file>${LOG_DIR}/sql_info.log</file>
<!-- 当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名。属性class定义具体的滚动策略类 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 必要节点,包含文件名及"%d"转换符,"%d"可以包含一个java.text.SimpleDateFormat指定的时间格式,默认格式是 yyyy-MM-dd -->
<fileNamePattern>${LOG_DIR}/sql_info_%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>20MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,如果是6,则只保存最近6个月的文件,删除之前的旧文件 -->
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!-- LevelFilter: 级别过滤器,根据日志级别进行过滤 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<!-- 用于配置符合过滤条件的操作 ACCEPT:日志会被立即处理,不再经过剩余过滤器 -->
<onMatch>ACCEPT</onMatch>
<!-- 用于配置不符合过滤条件的操作 DENY:日志将立即被抛弃不再经过其他过滤器 -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<appender name="SQL_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/sql_error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/sql_error_%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>20MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<appender name="APP_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_DIR}/info.%d{yyyy-MM-dd}.log
</FileNamePattern>
</rollingPolicy>
<encoder>
<Pattern>[%date{yyyy-MM-dd HH:mm:ss}] [%-5level] [%thread] [%logger:%line]--%mdc{client} %msg%n</Pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<property name="pattern" value="%d{yyyyMMdd:HH:mm:ss.SSS} [%thread] %-5level %msg%n"/>
<pattern>%d{yyyyMMdd:HH:mm:ss.SSS}%thread%-5level%F{32}%M%L%msg</pattern>
</layout>
</encoder>
<file>${LOG_DIR}/test.html</file>
</appender>

<!--
用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
<loger>仅有一个name属性,一个可选的level和一个可选的addtivity属性
name:
用来指定受此logger约束的某一个包或者具体的某一个类。
level:
用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
如果未设置此属性,那么当前logger将会继承上级的级别。
additivity:
是否向上级loger传递打印信息。默认是true。
<logger>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger
-->
<logger name="java.sql" level="info" additivity="false">
<level value="info" />
<appender-ref ref="STDOUT"></appender-ref>
<appender-ref ref="SQL_INFO"></appender-ref>
<appender-ref ref="SQL_ERROR"></appender-ref>
</logger>

<logger name="com.souche.LogbackTest" additivity="false">
<level value="info" />
<appender-ref ref="STDOUT" />
<appender-ref ref="APP_INFO" />
<appender-ref ref="FILE"/>
</logger>

<!--
也是<logger>元素,但是它是根logger。默认debug
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
<root>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger。
-->
<root level="info">
<level>info</level>
<appender-ref ref="STDOUT"/>
<appender-ref ref="SQL_INFO"/>
<appender-ref ref="SQL_ERROR"/>
<appender-ref ref="FILE"/>
</root>

</configuration>

五、实际应用

就举例最近新做的项目弹个X中的应用吧。因为在与前端联调阶段,api自测感觉没啥问题的,然后联调就会有各种问题,没法避免,技术还是太水了,哈哈哈哈……..
调详情页的时候,听到有问题就赶紧看日志去,果然报错了。如下:

错误日志

有错误信息就能当做本地控制台一样,可以直接看到错误信息。

Cause: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column ‘saler_phone’ at row 1
; SQL []; Data truncation: Data too long for column ‘saler_phone’ at row 1; nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column ‘saler_phone’ at row 1

然后进一步加了log日志

log日志

传参错了,查了原因,登录失效,然后salerPhone居然还有值,而且多了一位……

六、为什么使用logback

logback具有以下优点:

  • 内核重写、测试充分、初始化内存加载更小,这一切让logback性能和log4j相比有诸多倍的提升
  • logback非常自然地直接实现了slf4j,这个严格来说算不上优点,只是这样,再理解slf4j的前提下会很容易理解logback,也同时很容易用其他日志框架替换logbac
  • logback有比较齐全的200多页的文档
  • logback当配置文件修改了,支持自动重新加载配置文件,扫描过程快且安全,它并不需要另外创建一个扫描线程
  • 支持自动去除旧的日志文件,可以控制已经产生日志文件的最大数量

总而言之,如果大家的项目里面需要选择一个日志框架,那么我个人非常建议使用logback。

七、logback性能大比拼

log4j、logback、log4j2性能测试,直接引用公司的博客吧。logback log4j log4j2 性能实测

参考链接

  1. Java日志框架-logback的介绍及配置使用方法
  2. 从源码来理解slf4j的绑定,以及logback对配置文件的加载
  3. logback 配置详解和使用
  4. logback.xml常用配置详解 : filter
  5. Java 日志框架:logback 详解
  6. logback log4j log4j2 性能实测

链接:https://www.jianshu.com/p/b3dedb8fb61e

总结0417

1.Locale
国际化
Locale 表示地区。每一个Locale对象都代表了一个特定的地理、政治和文化地区。
在操作 Date, Calendar等表示日期/时间的对象时,经常会用到;因为不同的区域,时间表示方式都不同。
https://blog.csdn.net/qq_27727251/article/details/80077189
2.retrytemplete
3.wrk压测工具
4.elastic-job
5.hystrixcommend
6.声明式和命令式 (Declarative vs Imperative)
声明式和命令式是两种编程范式。react是声明式的,jquery那样直接操作dom是命令式
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
7.netstat -ano|findstr 8080
tasklist | findstr 9268
taskkill /pid 1111 /f
8.GZip
9.MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。
10.swagger
https://blog.csdn.net/weixin_37509652/article/details/80094370