0%

Spring源码分析

关键类介绍

ApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
万能的 applicationContext, 但实际上各种能力都是依赖于其他的类, 比如 getBean 是 beanFactory 的, publishEvent 是事件广播器的, 等等. 其本身是一个综合体, 整合这些能力, 便于开发者调用和理解.

# 下面列一下相关的接口, 抽象类, 和具体类
ApplicationContext
是一个只读的 bean 容器
可以加载解析配置文件(如xml)
可以发布事件和注册监听
具有国际化消息处理能力
ConfigurableApplicationContext
是一个具有可配置能力的 容器(可设置各个参数, 如id, 父容器)
具有容器生命周期概念, 如启动,停止,关闭.
AbstractApplicationContext
模板方法模式的抽象类, 定义了容器的模板(refresh方法), 但由具体的子类实现部分方法
管理Bean和BeanFactory的PostProcessor
管理事件的监听和处理
AbstractRefreshableApplicationContext
为可重复刷新的容器提供基类
加入了BeanFactory的管理(创建/关闭等)
AbstractRefreshableConfigApplicationContext
加入了configLocation字段, 用于某些容器初始化BeanFactory和Bean
AbstractXmlApplicationContext
定义了读取xml配置文件来加载BeanFactory的代码, 使得子类只需提供配置文件地址或Resource
ClassPathXmlApplicationContext
继承基类, 提供配置文件地址的构造方法, 调用refresh加载BeanFactory

BeanFactory

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
1.核心中的核心, 加载和管理 beanDefinitions(Bean配置信息), 创建和管理 bean 对象实例, 注册和管理 BeanPostProcessor(Bean扩展)

# 下面列一下相关的接口, 抽象类, 和具体类
BeanFactory
定义了 Bean 的基础操作接口, 如 getBean, getType, isSingleton 等

SingletonBeanRegistry
定义了单例对象的操作接口 (注册/获取/是否已存在)

HierarchicalBeanFactory
定义了父 BeanFactory 的相关操作接口(获取)

ConfigurableBeanFactory
定义了对 BeanFactory 做各种配置的操作接口, 包括 BeanPostProcessor, setParentBeanFactory, destroyBean, registerAlias, resolveAliases 等

DefaultSingletonBeanRegistry
实现了 SingletonBeanRegistry 接口, 即实现了单例对象的缓存管理, 包括一级/二级/三级(二级三级只依赖循环用上的两个缓存)

FactoryBeanRegistrySupport
继承了 DefaultSingletonBeanRegistry
实现对使用 FactoryBean 存储和获取 bean 对象实例方式的支持

AbstractBeanFactory
继承了 FactoryBeanRegistrySupport
实现了 BeanFactory/HierarchicalBeanFactory/ConfigurableBeanFactory 定义的接口
实现了具体 getBean, 包括缓存管理等

AutowireCapableBeanFactory
定义了根据 class 类型获取 BeanDefinition 信息以及 Bean 对象的接口

AbstractAutowireCapableBeanFactory
继承自 AbstractBeanFactory
实现了 AutowireCapableBeanFactory 中定义的方法(就是实现了根据 class 获取 bean 或 BeanDefinition)
实现了 createBean, 也就是真正的实例化一个对象的过程, 包括实例化, 为需要赋值的字段注入相应的值
同时触发了 BeanPostProcessor 的方法调用

BeanDefinitionRegistry
定义了 BeanDefinition 的注册/获取/移除

ListableBeanFactory
定义了 BeanDefinition 的可遍历性

ConfigurableListableBeanFactory
结合 ListableBeanFactory 和 ConfigurableBeanFactory 并补充完善了几个相关接口 (如 getBeanNamesIterator 接口 )

DefaultListableBeanFactory
继承了 AbstractAutowireCapableBeanFactory
实现了 BeanDefinitionRegistry/ConfigurableListableBeanFactory 的接口

# 总结:
定义处:
BeanFactory(getBean)
SingletonBeanRegistry(addSingleton)
HierarchicalBeanFactory(getParentBeanFactory)
ConfigurableBeanFactory(addBeanPostProcessor)
AutowireCapableBeanFactory(autowireBean)
BeanDefinitionRegistry(registerBeanDefinition)
ListableBeanFactory(getBeanDefinitionNames)
ConfigurableListableBeanFactory(getBeanNamesIterator)

实现处:
DefaultSingletonBeanRegistry(registerSingleton)
FactoryBeanRegistrySupport(getObjectFromFactoryBean)
AbstractBeanFactory(doGetBean)
AbstractAutowireCapableBeanFactory(createBean)
DefaultListableBeanFactory(registerBeanDefinition)

容器初始化过程

1
2
3
4
5
6
7
8
9
10
11
12
13
1.setParent(): 处理父容器 
2.setConfigLocations(): 解析并设置xml配置文件路径
3.refresh(): 创建 beanFactory 对象并初始化, 读取 xml 配置文件得到 beanDefinitions, 接着处理两种 PostProcessor, 然后添加国际化处理器和事件广播器以及相应的初始化和一些处理, 最后实例化单例的 bean 等等.

#外圈结束, 再看 refresh() 里面的每个方法
1.prepareRefresh(): 准备工作, 一些字段值的设置和处理.
2.obtainFreshBeanFactory(): 创建一个 beanFactory 对象并注册到 applicationContext (即赋值到字段上), 然后解析 xml 配置文件(或注解配置)的信息, 解析得到 beanDefinitions 并注册到容器中.
3.然后是一些对 beanFactory 对象的完善配置的代码
4.扫描并执行 BeanFactoryPostProcessor(其作用是为beanFactory对象添加东西提供扩展性), 其中我认识的就只有 ConfigurationClassPostProcessor(这个类作用就是解析 @Configuration/@Component/@Import/@ImportSource/@ComponentScan等基础注解).
5.扫描实现了 BeanPostProcessor 接口的 bean 并注册到 beanFactory 中存起来, 等 createBean 创建对象时会在对应的时机执行一些对应的方法(钩子). 常见的各种 XxxAware 就是靠这个实现的咯.
6.接着, 初始化国际化资源处理器, 事件广播器, 并注册一些需要注册的事件(也注册容器内实现对应接口的 bean)
7.处理一些 beanFactory 的配置, 接着为所有单例且非懒加载的(不就是默认策略嘛) bean 创建实例, 缓存起来.
8.广播容器加载完成了的事件. 以及处理生命周期.

最后总结下, 先创建容器, 再将根据配置文件解析得到 BeanDefinition 注册到容器中, 然后处理两大扩展(BeanFactoryPostProcessor/BeanPostProcessor), 接着是Spring的国际化, 以及相当有用的事件广播器, 最后实例化 bean. 整体感觉其实很简单, 但其实有大量的工作交给了 BeanPostProcessor.

超长源码分析过程

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
// 先随便写个 main 方法, 如我写的, 可测试依赖循环问题和事件监听:
// 包名: cn.gudqs7.spring.tests, 改动则需同步修改xml哦
// 进入对应类代码: 快捷键 Cmd+Option+鼠标点击 (或 Ctrl+Alt+鼠标左键 ); 如果是接口松开 Option(或Alt)键

Test.java
public class Test {

public static void main(String[] args) {
ApplicationContext xmlContext = new ClassPathXmlApplicationContext("application-wq.xml");
UserServiceImpl userService = xmlContext.getBean(UserServiceImpl.class);
userService.sayHi();
}

}


application-wq.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName">

<bean name="userService" class="cn.gudqs7.spring.tests.UserServiceImpl">
<property name="starter"><ref bean="serverStarter"/></property>
</bean>
<bean name="serverStarter" class="cn.gudqs7.spring.tests.ServerStarter">
<property name="userService"><ref bean="userService"/></property>
</bean>

</beans>


UserServiceImpl.java
@Service
public class UserServiceImpl {

@Resource
private ServerStarter starter;

public void sayHi() {
System.out.println(starter);
System.out.println("Hello Spring!");
}

public void setStarter(ServerStarter starter) {
this.starter = starter;
}
}


ServerStarter.java
@Service
public class ServerStarter implements ApplicationListener<ContextRefreshedEvent> {

@Inject
private UserServiceImpl userService;

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
String applicationName = event.getApplicationContext().getApplicationName();
System.out.println(applicationName);
System.out.println(userService);
System.out.println("========== started by gudqs7 ==============");
System.out.println("========== started by gudqs7 ==============");
System.out.println("========== started by gudqs7 ==============");
}

public void setUserService(UserServiceImpl userService) {
this.userService = userService;
}
}

😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 接下来, 进入ClassPathXmlApplicationContext#ClassPathXmlApplicationContext(java.lang.String) 方法中

// 其跳转到了
public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {

// 为容器的 parent 字段赋值, 若 parent 不为空, 且有 ConfigurableEnvironment, 则合并数据(将父容器有的加到子容器中)
// 即执行了 org.springframework.context.support.AbstractApplicationContext.setParent()
super(parent);

// 为 configLocations 字段赋值(告知配置文件位置), 赋值前会根据环境变量解析(此时环境变量中只有系统环境变量: 如JAVA_HOME).
setConfigLocations(configLocations);
if (refresh) {
// 注释在下面
refresh();
}
}

😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄

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
// 然后具体的看 refresh 方法
public void refresh() throws BeansException, IllegalStateException {
// 1.设置容器初始的一些属性(时间,状态),初始化占位符数据源并校验所有 bean 所使用的占位符是否存在, 清空事件和监听
// 2.清空重置旧的 beanFactory, 再创建新的 beanFactory 并通过解析 xml或注解 加载 beanDefinitions
// 3.设置了一些 beanFactory 的属性, 添加了几个有用的 BeanPostProcessor, 还添加了几个 bean 到容器中(都是环境相关的 bean)
// 4.子类对beanFactory 添加自己的特殊的 BeanPostProcessor (如servletContxt/servletConfig注入)
// 5.扫描容器中实现了 BeanFactoryPostProcessor 接口的 bean 将其注册到 beanFactory 中并执行
// 6.扫描容器中实现了 BeanPostProcessor 接口的 bean 将其注册到 beanFactory 但不执行(实例化 bean 对象那会有几个执行时机)
// 7.创建一个国际化资源解析器并注册到 beanFactory; 创建一个事件广播器并注册到 beanFactory.
// 8.调用子类的其他刷新时需要做的事情(模板方法)
// 9.扫描容器中实现了 ApplicationListener 接口的 bean, 将其预存到广播器中但不执行
//10.完成 beanFactory 的一些配置(包括终结一些东西, 如 setTempClassLoader(null) ); 将单例的 bean 创建出来放入容器中(未设置lazy-init=true)的 bean
//11.广播 ContextRefreshedEvent 事件, 初始化LifeCycleProcessor及调用其 onRefresh 方法.

synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
// 设置容器初始的一些属性(时间,状态),初始化占位符数据源并校验所有 bean 所使用的占位符是否存在, 清空事件和监听
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
// 清空重置旧的 beanFactory, 再创建新的 beanFactory 并解析 xml或注解 加载 beanDefinitions
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
// 设置了一些 beanFactory 的属性, 添加了几个有用的 BeanPostProcessor, 还添加了几个 bean 到容器中(都是环境相关的 bean)
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
// 子类对beanFactory 添加自己的特殊的 BeanPostProcessor (如servletContxt/servletConfig注入)
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
// 扫描容器中实现了 BeanFactoryPostProcessor 接口的 bean 将其注册到 beanFactory 中并执行
// 扫描6次, 2(BeanDefinitionRegistry/BeanFactory) x 3(优先级:高/中/其他)
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
// 扫描容器中实现了 BeanPostProcessor 接口的 bean 将其注册到 beanFactory 但不执行; 扫描6次: 2(MergedBeanDefinitionPostProcessor/其他) x 3(优先级: 高/中/其他)
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
// 创建一个国际化资源解析器并注册到 beanFactory.
initMessageSource();

// Initialize event multicaster for this context.
// 创建一个事件广播器并注册到 beanFactory.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
// 调用子类的其他刷新时需要做的事情(模板方法)
onRefresh();

// Check for listener beans and register them.
// 将可能存在的 applicationListeners 注册到事件广播器中(新建时是不存在的),
// 然后扫描容器中实现了 ApplicationListener 接口的 bean, 将其预存到广播器中但不执行
// 将之前 publishEvent() 想广播的事件广播出去, 然后字段 earlyApplicationEvents 赋值为空(代表之后可立即广播)
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
// 完成 beanFactory 的一些配置(包括终结一些东西, 如 setTempClassLoader(null) )
// 注册默认的表达式解析器(若无相应的bean存在)
// 扫描容器中实现了 LoadTimeWeaverAware 接口的 bean, 并触发(getBean)之前注册过的 BeanPostProcessor
// 将单例的 bean 创建出来放入容器中(未设置lazy-init=true)的 bean
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
// 广播 ContextRefreshedEvent 事件, 初始化LifeCycleProcessor及调用其 onRefresh 方法.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
// 销毁缓存的单例对象
destroyBeans();

// Reset 'active' flag.
// 变更状态
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
// 清空公共工具产生的缓存(内存松一口气).
resetCommonCaches();
}
}
}

我错了, 代码都放上去不如给个GitHub地址, 接下来省略代码吧, 只放注释😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄

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
// 挨个看里面的方法
org.springframework.context.support.AbstractApplicationContext#prepareRefresh
// 1.设置容器初始的一些属性, 如启动时间, 当前状态
// 2.打印开始日志
// 3.初始化占位符数据源
// 4.校验所有 bean 所使用的占位符是否存在
// 5.清空事件和监听
// Switch to active.

org.springframework.context.support.AbstractRefreshableApplicationContext#refreshBeanFactory
// 1.存在旧的则先摧毁 bean 对象实例及缓存数据, 再将旧的置为 null
// 2.创建一个新的 beanFactory 对象, 再设置 id及一些配置
// 3.扫描并加载 beanDefinations
// 4.设置这个新的 beanFactory 对象为 applicationContext 的 beanFactory 字段值.

org.springframework.context.support.AbstractApplicationContext#prepareBeanFactory
// 1.设置 beanFactory 的类加载器
// 2.设置 beanFactory 的表达式解析器
// 3.1:注册一个 BeanPostProcessor 用于将实现了ApplicationContext能力相关的Aware接口的 bean, 触发赋值setter 注入 applicationContext 对象
// 3.2:设置 beanFactory 处理 bean 时要忽略的接口(主要是setter注入时忽视一些也是setter的方法, 因为这些方法会由 PostProcessor 来触发)
// 4.注册一些特殊的 bean(注入这些bean时会注入 this 对象: 多功能工具人 ApplicationContext, 可见其和普通 bean 的注册方式不一样)
// 5.注册一个 BeanPostProcessor 用于检测加载的 bean 是否实现了 ApplicationListener 接口, 若是, 则注册到事件广播器中(不是,是暂存在applicationListeners字段中, 等事件广播器创建后才注册)
// 6.注册一个 BeanPostProcessor 用于触发实现了 LoadTimeWeaverAware 接口的 bean 的 setLoadTimeWeaver() 社会 LTW 实例.
// 7.注册几个环境相关 bean 到容器中(Spring的环境对象, 以及系统环境变量和系统配置文件)

org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors(org.springframework.beans.factory.config.ConfigurableListableBeanFactory, java.util.List<org.springframework.beans.factory.config.BeanFactoryPostProcessor>)
// 1.若 beanFactory 实现了 BeanDefinitionRegistry 接口(new AnnotationConfigApplicationContext 就实现了)
// 则扫描所有实现了 BeanDefinitionRegistryPostProcessor 接口的 bean, 根据优先级分三类(高/中/其他)依次执行
// 2.然后扫描所有实现了 BeanFactoryPostProcessor 接口的 bean, 依旧是根据优先级分三类依次执行.
// 3.每次执行前都会根据 Order 信息排序, 再遍历执行

org.springframework.context.support.PostProcessorRegistrationDelegate#registerBeanPostProcessors(org.springframework.beans.factory.config.ConfigurableListableBeanFactory, org.springframework.context.support.AbstractApplicationContext)
// 1.扫描6次: 2(MergedBeanDefinitionPostProcessor/其他) x 3(优先级: 高/中/其他)
// 将其加入到 beanFactory 的 beanPostProcessors 集合中
// 2.再次加入 ApplicationListenerDetector (用于处理实现 ApplicationListener 的 bean 注册到事件广播器), 主要是使其在链末尾, 可以最后执行.

org.springframework.context.support.AbstractApplicationContext#registerListeners
// 1.将可能存在的 applicationListeners 注册到事件广播器中(新建时是不存在的)
// 2.扫描容器中实现了 ApplicationListener 接口的 bean, 将其预存到广播器中但不执行
// 3.将之前 publishEvent() 想广播的事件广播出去, 然后字段 earlyApplicationEvents 赋值为空
// (因为 publishEvent() 中根据是否为空判断立刻执行或先存着) (另这也解释了 prepareRefresh() 中为何要赋值一个空集合)


org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization
// 1.完成 beanFactory 的一些配置
// 2.注册默认的表达式解析器(若无相应的bean存在)
// 3.扫描容器中实现了 LoadTimeWeaverAware 接口的 bean, 并触发(getBean)之前注册过的 BeanPostProcessor
// 4.将单例的 bean 创建出来放入容器中(未设置lazy-init=true)的 bean

org.springframework.context.support.AbstractApplicationContext#finishRefresh
// 1.清空资源缓存
// 2.创建一个生命周期管理器(start, refresh, stop等)并注册到 beanFactory
// 3.触发生命周期管理器的 onRefresh()
// 4.广播容器刷新完成的事件
// 5.为 Spring Tool Suite 提供某些便捷(没用过, 不知道...)

可算复制完了, 如果你有幸直接跳读到这里, 那么送上地址 : 注意分支吧

另外上面方法前带个 # 的, 复制到 IDEA 双击 Shift 然后粘贴, 选择 Symbols 搜索更准确呢!

获取容器对象过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 从getBean(Class type) 中进入
1.检查 applicationContext 和 beanFactory 的状态, 若有异常则给出准确的错误.
2.扫描容器中所有此 type 的 beanName, 遍历判断每个 beanName 是否可用
可用则判断可用的个数是否刚好是一个, 是则直接调用 getBean() 返回对象实例
若可用的个数超过一个, 则根据 beanDefinition 的 isPrimary 和对比配置的优先级是否为有最高的再返回最高的
若都不行, 则报错.
3.接着看 getBean, 先试着从单例的缓存中获取, 若存在则返回.
4.若缓存中不存在, 则判断父容器是否存, 若存在则从父容器获取
若父容器不存在, 则自己新建, 先标记 beanName 到 alreadyCreated 中(表示已经创建了防止重复创建) 再开始创建一个 bean.
5.创建一个新的 bean 实例, 先处理 beanDefinition 的 dependsOn 属性(即若存在则先调用 getBean 获取依赖的 bean)
6.若 beanDefinition 的设置是单例, 则通过闭包对创建对象前后进行一些异常处理和缓存处理(主要是彻底创建完后加入到单例一级缓存, 移除二级和三级缓存[循环依赖相关的两个缓存]).
7.通过反射根据 beanClass 创建一个对象实例, 然后将其添加到 singletonFactories 中(解决依赖循环问题)
8.调用 populateBean() 为对象的字段(属性)注入它所需要的值(可能是@Resource, @Value等); (此时可能会遇到依赖循环问题, 但解决这个问题的缓存在此之前就添加了, 所以不怕)
9.最后调用 initializeBean() 完成 bean 的初始化(调用 bean 的一些方法, 如 afterPropertiesSet), 返回对象实例.

总结: 先根据 type 找到 beanName, 找到后根据 beanName 创建对象; 创建对象前先检查缓存(单例), 再考虑父容器, 最后才是自己创建, 自己创建会先创建 dependsOn 的 bean 对象, 然后才通过反射实例化出一个对象实例(这里反射用到的class和构造方法, 通过实现 SmartInstantiationAwareBeanPostProcessor接口都可进行干预), 实例化后存到二级缓存, 再为字段赋值(注入); 最后调用 bean 的 init 相关的接口(如afterPropertiesSet), 就可以返回这个对象实例了.

超长源码分析过程

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
// 从这个方法进入
@Override
public <T> T getBean(Class<T> requiredType) throws BeansException {
// 判断容器的状态, 确保 beanFactory 可用.(主要是若不可用, 提示的错误信息会比getBeanFacgtory()中更准确)
// 使用 beanFactory 的 getBean 方法获取对象并返回.
assertBeanFactoryActive();
return getBeanFactory().getBean(requiredType);
}

// 然后其他所有涉及的核心方法的注释
org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveBean
// 调用 resolveNamedBean, 如存在 bean 则直接返回. (核心)
// 若不存在则从父容器中寻找, 父容器实现了 DefaultListableBeanFactory 则调与同子容器相同的方法
// 若没实现则 通过 getBeanProvider 获取.

org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveNamedBean(org.springframework.core.ResolvableType, java.lang.Object[], boolean)
// 1.调用 getBeanNamesForType 获取所有与 type 相匹配的 beanName 集合.
// 2.遍历判断每个 beanName 是否可用
// 3.若可用的 beanName 只有一个, 则调用 getBean(beanName) 获取对象实例并返回
// 若可用数超过一个, 则试着根据是否主要以及高优先级来确定一个 beanName 实例, 若能确定则返回, 不能则报错.

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
// 1.获取完整的 beanName
// 2.调用 getSingleton1() 检查是否存在缓存, 这层检查可防止依赖循环.
// 若存在, 则通过 getObjectForBeanInstance() 判断缓存的是 bean 还是 FactoryBean 并返回相应的对象实例.
// 3.若不存在, 先试着从父容器获取(子容器不存在这个 beanDefinition 且父容器不为空)
// 没有父容器则 调用 markBeanAsCreated() 标记这个 bean已经创建了 (先标记, 再创建)
// 获取 beanDefinition, 判断其 dependsOn 属性是否存在, 存在则 先获取依赖的 bean
// 调用 getSingleton2() 处理单例缓存
// 4.而 getSingleton2() 中的闭包中 执行的 createBean() 方法中则才是创建实例并调用 BeanPostProcessor

org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
// 1.判断是否存在于 singletonObjects 中
// 2.若不存在则判断 bean 是否处于创建中(未创建完成, 如循环依赖时)
// 3.若处于创建中, 则同步后判断是否存在于 earlySingletonObjects (也就是 singletonFactories 移除后存入的地方)
// (因为FactoryBean占用空间大, 获取对象麻烦且速度更慢, 这是为了防止如果循环依赖链条很长 多次获取浪费CPU的问题)
// 4.不存于 earlySingletonObjects 则代表第一次(也只会有一次)取 singletonFactories
// 取出后调用 getObject() 并将其存入到 earlySingletonObjects, 然后从 singletonFactories 中移除. 以后就少走几行代码了.

org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)
// 1.先确保是第一次创建单例对象, 防止重复创建
// 2.进行一些异常处理
// 3.调用 singletonFactory.getObject() 创建对象
// 4.创建对象结束添加单例缓存和清空 singletonFactories / earlySingletonObjects 缓存.

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])
// 1.调用 resolveBeanClass 解析得到真正的 bean class, 若解析不为空且处于某些情况下, 则复制一份 beanDefinition 并设置 beanClass 为解析所得
// 2.执行 BeanPostProcessor 的 postProcessorsBeforeInstantiation() 方法
// 3.调用 doCreateBean() 创建对象 并返回

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
// 1.调用 createBeanInstance() 获得一个 对象实例的 包装类
// 2.同步锁下执行 BeanFactoryPostProcessor 的 postProcessMergedBeanDefinition().
// 3.添加 singletonFactories 缓存, 移除 earlySingletonObjects; 解决循环依赖问题.
// 4.调用 populateBean() 检查字段是否需要注入对象实例, 是则获取对应的 bean 注入. (可能引起循环依赖)
// 5.调用 initializeBean() 执行对象的一些 Aware 和 init 方法和 BeanPostProcessor 的 postProcessBeforeInitialization.
// 6.最后返回对象实例.

各种实现的原理

1
2
3
4
5
6
7
1.为何我写的 class 实现一些接口(如ApplicationContextAware)后并放入容器中, 就可以获取到一些对象(如applicationContext)?
2.为何我写的 class 实现 ApplicationListener<XxxEvent> 后并放入容器中, 就能监听我想知道的事件?
3.为何Spring中遇到各种顺序问题, 只需要实现 Ordered 接口(或加上@Order注解)就能使其有序?
4.Spring是如何解决循环依赖的(指用字段注入而非构造方法)?
5.Spring可以用注解替换XML配置文件了, 是如何实现的呢(常用注解的实现原理)?
6.Spring AOP是如何实现的(指@Aspect)?
7.Spring 事务是如何实现的(指@Transaction)?

为何我写的 class 实现一些接口(如ApplicationContextAware)后并放入容器中, 就可以获取到一些对象(如applicationContext)?

1
2
3
1) 首先 AbstractApplicationContext#prepareBeanFactory 会添加一个ApplicationContextAwareProcessor
2) 这个 beanPostProcessor 负责在bean初始化之前注入context对象.
3) 这个 beanPostProcessor 的执行时机是在 doCreateBean 中的 postProcessBeforeInitialization()

为何我写的 class 实现 ApplicationListener 后并放入容器中, 就能监听我想知道的事件?

1
2
3
4
1) 在 AbstractApplicationContext#registerListeners() 中扫描容器内所有相关实现类加入到事件监听者集合中
2) 然后在publishEvent时,遍历事件监听者集合调用bean的方法即可。观察者模式!
3) 另外也用了BeanPostProcessor去实现, 叫 ApplicationListenerDetector, 加入时机同1, 执行时机同1.
4) 至于为何使用2种机制, 应该是因为 registerListeners() 时, 扫描只是当前的, 后续可能容器内的 bean 还会增加(我也猜不到啥形式增加, 反正简单写个类肯定不会), 所以还是需要 ApplicationListenerDetector 在这个 Bean 初始化时加入到监听者中去.

为何Spring中遇到各种顺序问题, 只需要实现 Ordered 接口(或加上@Order注解)就能使其有序?

因为 Spring 预先在执行这些东西之前, 进行一个排序动作, 然后才遍历执行. 包括AOP, BeanFactoryPostProcessor, BeanPostProcessor .

1
2
1) 比如说 BeanPostProcesser, 容器扫描后, 会像对bean集合排序, 再遍历执行.
2) 详细过程见 PostProcessorRegistrationDelegate#sortPostProcessors()

Spring是如何解决循环依赖的(指用字段注入而非构造方法)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1) 首先, 假定有两个单例 bean A 和 B, A 持有 B, B 持有A, 构成循环
2) 此时程序调用getBean获取A,则在 doCreateBean 中 创建后将 bean 缓存到 singletonFactories 中
3) 然后设置属性B, 解析属性, 需要获取B对象
4) 获取B, 则执行doCreateBean 后执行解析属性, 需要获取 A对象 (又一次)
5) 获取A, 进入 doGetBean 中的 getSingleton, 此时判断 singletonFactories 中有A, 则可以直接取出A
6) 获得A后, 即可完成B的属性赋值, 然后会完成B的创建.
7) B创建完后, A就能获得B, 则A也完成了属性赋值, 最后完成创建A.
8) 到此, 返回即可.

> 总结: 首次获取A, 创建A对象后缓存一个存储A对象的 ObjectFactory 实例, 再解析属性时触发 getBean(B), 同理也会做缓存, 然后也解析属性, 触发getBean(A), 第二次获取A, 进入另一个逻辑, 返回 ObjectFactory 实例中存储的对象A, 即可完成getBean(A), 然后完成getBean(B), 再完成外层的getBean(A).

TIPS:
步骤4中, 会先判断 earlySingletonObjects, 不存在才判断 singletonFactories, 而从 singletonFactories 中取得对象后, 则会将其从 singletonFactories 移除并加入 earlySingletonObjects

这是因为 singletonFactories 缓存的 FactoryBean, 若反复调用 getObject(), 则每次获取都会调用 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#getEarlyBeanReference 方法, 而此方法会执行 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference(), 这会导致 BeanPostProcessor 重复执行, 显然是不行的.

Spring可以用注解替换XML配置文件了, 是如何实现的呢(常用注解的实现原理)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1) 首先是指定包名或指定类名
如指定包名则 scan 时会执行, 如指定类名则在构造方法初始化 reader 时执行
2) 无论哪种, 最终都会走一段代码 AnnotationConfigUtils#registerAnnotationConfigProcessors()
3) 这段代码会添加一些 BeanFactoryPostProcessor
如 ConfigurationClassPostProcessor 负责解析 @Configuration/@Import/@Bean 等注解
然后由 ConfigurationClassBeanDefinitionReader 负责将信息转换成BeanDefinition再注册到容器。
如 AutowiredAnnotationBeanPostProcessor 负责解析 @Autowired/@Value 注解
如 CommonAnnotationBeanPostProcessor 负责解析 @Resource 注解
解析放在 postProcessProperties() 方法中, 先扫描bean的字段和方法, 然后一一调用方法和为字段注入值
4) 之后, 他会将扫描的类放到 beanDefinitions 中(或指定的类注册进去)
5) BeanFactory加载完毕后, 回到AbstractApplicationContext的refresh逻辑
如会执行 postProcessBeanFactory(), 调用前面加入的ConfigurationClassPostProcessor
然后会添加更多的类到容器中.

注意事项:
@Configuration 和 @Component的区别?
观察发现,即使使用@Component 其下带 @Bean 的方法依然可以注入到容器中。所以似乎两者没有区别?
仔细查看源码和资料后,发现 postProcessBeanFactory() 方法在 processConfigBeanDefinitions() 后还会调用 enhanceConfigurationClasses()
而在这个方法中, 对前面解析了class 是 CONFIGURATION_CLASS_FULL (即代表@Configuration)的类
会生成一个 cglib 的代理, 这样获取@Bean注解的方法的bean时,不会每次调用方法new 一个, 而是有缓存.

总结: 就是利用 BeanFactoryPostProcessor 可获取 BeanDefinitionRegistry 对象, 然后扫描容器内带有注解的 bean, 解析这些注解得到一些 BeanDefinition, 再通过获得的 BeanDefinitionRegistry对象注册到 BeanFactory 中.

Spring AOP是如何实现的(指@Aspect)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1) 使用 @EnableAspectJAutoProxy
2) @EnableAspectJAutoProxy 中使用了 @Import(AspectJAutoProxyRegistrar.class)
3) ConfigurationClassPostProcessor 会解析@Import, 进入 registerBeanDefinitions() 中
4) registerBeanDefinitions() 中添加了 AnnotationAwareAspectJAutoProxyCreator 到容器中
5) AnnotationAwareAspectJAutoProxyCreator 本质上时一个 BeanPostProcessor
6) 因此在 createBean 时, 会被自动调用. 其中 postProcessAfterInitialization() 负责创建代理对象
7) 而 getAdvicesAndAdvisorsForBean() 则负责查找对应的增强. 然后会调用子类的findCandidateAdvisors
8) 如 AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors() 负责注解编写增强@Before/@After等
9) 简单说下逻辑, 就是查找容器所有类, 判断这个类有没有 @Aspect 注解, 然后先找出所有Pointcut
再遍历所有方法, 找出方法上带有@Before等注解且有关联的Pointcut的方法,
然后使用这个方法和关联的Pointcut 来new 一个Advisor, 加入到Advisor集合中, 遍历结束后返回即可.
10) 查找到所有的增强后, 再比较Pointcut表达式是否匹配当前的bean, 如可以则加入.
11) 根据找到的Advisor集合, 创建一个带配置(advisor集合等)的代理对象, 代理对象执行方法前
12) 会先根据配置中的advisor集合生成一个执行链, 然后在拦截代理方法处调用. 执行链会负责执行通知.
13) 不同的通知由不同的适配器执行.

总结就是通过 @EnableAspectJAutoProxy 的@Import, 使得程序最终会执行 AnnotationAwareAspectJAutoProxyCreator 的 postProcessAfterInitialization(对象初始化后调用) 方法, 这个方法在 BeanFactory创建完对象后触发, 此时便可通过 CGlib 等动态代理技术为 创建的 bean 对象创建一个代理对象, 然后这个代理对象会根据 Pointcut 找到关联的 Advisor, 并在合适的时机执行对应的 Advisor, 如 @Before产生的Advisor 会在执行了 bean 对象的指定方法(看Pointcut配置)后执行.

Spring 事务是如何实现的(指@Transaction)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0) 事务是由AOP实现的, 所以需要找到对应的Pointcut 和 Advisor
1) 打开了 @EnableTransactionManagement 注解
2) 然后@Import 了 TransactionManagementConfigurationSelector
3) 之后导入了 ProxyTransactionManagementConfiguration 到容器中
4) ProxyTransactionManagementConfiguration 带有 @Configuration
5) @Bean 注入了一个通用的Advisor: BeanFactoryTransactionAttributeSourceAdvisor
6) 这个Advisor的 Pointcut 是由 TransactionAttributeSourcePointcut 实现的
实现逻辑是 TransactionAttributeSourcePointcut 的 matches()
这个方法调用了 getTransactionAttributeSource() 获取 AnnotationTransactionAttributeSource
然后通过 getTransactionAttribute() 调用了 findTransactionAttribute()
最终使用SpringTransactionAnnotationParser 类判断方法是否有@Transactional注解
并解析注解信息然后返回. 另外这个方法还可以获取@Transactional注解的信息, 而这里只用于判断是否需要拦截这个方法.
7) TransactionInterceptor 是一个Advisor
也可以通过AnnotationTransactionAttributeSource获取@Transactional注解上的信息
然后在invoke中, 拦截方法, 打开事务, 在执行完方法后, 提交事务, 报错时回滚事务
这个 Advisor 不同于传统的前置/后置, 而是更具体的 MethodInterceptor(动态代理直接相关).

总结: 就是基于AOP实现的, 只需找到对应的 Pointcut 和 Advisor 即可. Pointcut 就是根据 @Transaction 注解判断方法是否需要代理, 这个很简单; 比较有意思的是 Advisor 不是我们写AOP那种 @Before,@Around之类的, 而是更接近动态代理原始的语法的 MethodInterceptor 即 TransactionInterceptor.

BeanFactoryPostProcessor 相关类分析

BeanFactoryPostProcessor 生效原理

生效原理就是, ApplicationContext 的 refresh 方法中会扫描出容器中实现了 BeanFactoryPostProcessor 接口的 bean, 将其排序后执行相应的接口, 这样我们写的类实现的相应的接口的方法就被执行了.

1
2
3
常用的 BeanFactoryPostProcessor
# ConfigurationClassPostProcessor
这个类作用就是解析 @Configuration/@Component/@Import/@ImportSource/@ComponentScan 等基础注解. 是注解开发的基石, 更是 Spring Boot 的基石.

BeanPostProcessor 相关类分析

BeanPostProcessor 生效原理

在 refresh() 中会扫描容器中所有 实现了 BeanPostProcessor 接口的类, 添加到 BeanFactory 的 beanPostProcessors 字段中(是个List[CopyOnWriteArrayList自定义版, 自定义加入了清空缓存的逻辑]), 然后在 BeanFactory 创建对象时 createBean() 在适当的时机调用对应的方法.

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
有哪几种 BeanPostProcessor (默认的+扩展)
1.InstantiationAwareBeanPostProcessor
postProcessAfterInstantiation: 对象实例化后调用
postProcessBeforeInstantiation: 对象实例化前调用
postProcessProperties: 设置属性值前
postProcessPropertyValues: 设置属性值前, 若上个方法不处理(返回null)才会触发

2.SmartInstantiationAwareBeanPostProcessor
predictBeanType: 获取一个 bean 的 class 类型前调用
getEarlyBeanReference: 获取一个二级缓存对象(singletonFactories的getObject)时调用
determineCandidateConstructors: 决定一个 bean 实例化的构造参数是什么时调用

3.DestructionAwareBeanPostProcessor
postProcessBeforeDestruction: 对象销毁前调用
requiresDestruction: 判断这个类针对某个 bean 是否执行 postProcessBeforeDestruction()

4.MergedBeanDefinitionPostProcessor
postProcessMergedBeanDefinition: 在创建对象前调用, 可对 BeanDefinition 做修改
resetBeanDefinition: 在重置 BeanDefinition 时调用, 用于清空 PostProcessor 对应的缓存

5.BeanPostProcessor(基础)
postProcessBeforeInitialization: 创建对象后(也设置好了字段), 在调用 init 之前调用
postProcessAfterInitialization: 在创建对象时, 调用了 init 之后调用

总结:
0.对 BeanDefinition 做干预
1.对象实例化过程中(对class/构造参数进行干预)
2.对象实例化前后
3.对象设置属性前, 对属性做干预
4.对象初始化(init)前后
5.对象销毁前

调用时机

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
  // 1.1: InstantiationAwareBeanPostProcessor 的 postProcessAfterInstantiation()
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean 第一段
// 1.2: InstantiationAwareBeanPostProcessor 的 postProcessProperties()
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean 第二段
// 1.3: InstantiationAwareBeanPostProcessor 的 postProcessPropertyValues
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean 第三段
// 1.4: InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation()
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation 中

// 2.1: SmartInstantiationAwareBeanPostProcessor 的 predictBeanType()
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType 中
// 2.2: SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference()
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getEarlyBeanReference 中
// 2.3: SmartInstantiationAwareBeanPostProcessor 的 determineCandidateConstructors()
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors 中

// 3.1: MergedBeanDefinitionPostProcessor 的 postProcessMergedBeanDefinition()
// 在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyMergedBeanDefinitionPostProcessors 中
// 3.2: MergedBeanDefinitionPostProcessor 的 resetBeanDefinition()
// 在 org.springframework.beans.factory.support.DefaultListableBeanFactory.resetBeanDefinition 中

// 4.1: DestructionAwareBeanPostProcessor 的 postProcessBeforeDestruction()
// 在 org.springframework.beans.factory.support.DisposableBeanAdapter.destroy 中
// 4.2: DestructionAwareBeanPostProcessor 的 requiresDestruction()
// 在 org.springframework.beans.factory.support.DisposableBeanAdapter.filterPostProcessors 和 org.springframework.beans.factory.support.DisposableBeanAdapter.hasApplicableProcessors 中

😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁

1
2
3
4
5
6
7
# 有哪些常用的 BeanPostProcessor
1.AsyncAnnotationBeanPostProcessor: 用于在将 @Async 相应的 Advisor 加入到对象的代理中
2.ScheduledAnnotationBeanPostProcessor: 用于处理 @Scheduled 注解, 将 bean 生产代理类
3.AnnotationAwareAspectJAutoProxyCreator: AOP 实现核心类
4.AutowiredAnnotationBeanPostProcessor: 用于处理 @Autowired 注解
5.ApplicationListenerDetector: 用于处理实现 ApplicationListener 接口的 bean 对象, 将其添加到事件广播器的监听者集合中.
...

一句话

说一下你重构代码都做了什么?

首先是两层改成三层, 把controller 的代码尽量迁移到 service 层. 然后将请求风格和响应数据结构统一. 还有就是处理全局异常, 最后对某些重复代码封装成工具类. 另外还会根据实际业务场景使用一些设计模式, 提高代码可扩展性, 降低代码之间的耦合性.

什么是 JVM?

JVM 就是由编译器, 类加载器, 执行引擎, 运行时数据区组成. 其中数据区包含 堆,栈,本地方法栈, 方法区和程序计数器(PC 寄存器), 其中栈是由局部变量表, 操作数栈, 动态链接, 返回地址组成的.

你是怎么对 jvm 垃圾回收进行优化的?

根据服务器的配置, 调整青年代和老年代的内存大小及比例, 在回收频率和回收速度上做取舍, 使用 G1 垃圾回收期控制 STW 停顿时间, 提高吞吐量.

说说 MySQL 优化

首先是 SQL 查询优化, 通过对联表字段, 查询条件, 分组字段, 排序字段进行综合分析, 根据最左原则建立一个或多个复合索引, 然后使用 explain 分析SQL执行计划, 判断索引使用情况, 根据分析结果进一步改进索引.

然后是对于数据量大的表, 考虑垂直或水平分表, 读多写少的情况, 可以一主多从集群. 另外对于一些统计类的查询, 可以用定时任务将统计结果存储起来, 而非实时查询.

你用 Redis 做了什么?

将高访问的首页商品列表缓存到 redis 中, 避免数据库瓶颈, 提高响应速度.

商品同步问题: 定时任务刷新. 或修改商品时更新, 缓存设置失效时间, 失效后自动读取数据库.

将购物车数据存放到 redis, 提高购物车交互体验(加快响应速度).

你使用消息队列做了什么?

解耦: 如下单系统调用库存系统减库存, 若调用时库存系统挂了或出错了, 下单系统还需要做重试处理, 异常处理, 此时可将减库存请求放到消息队列中, 库存系统读取消息进行处理, 若出错则放回消息队列重试. 这样即使代码 bug 导致一直不成功也可在升级后自动重试, 无需人工干预. 另微信支付回调也可如此处理.

削峰: 如秒杀瞬间请求过高, 可将请求放到消息队列中, 另一端缓慢消费, 可防止系统卡住.

异步: 比如下单后发送下单通知, 有短信通知, 微信公众号通知等, 一个一个发送会导致下单这个请求响应很慢, 因此可以将几个通知做成一个消息, 放到消息队列, 由另一处代码异步执行.

你使用线程池做了什么?

将线程池封装到一个工具类中, 工具类再做成单例模式. 这样使用到多线程的地方都可以使用同一个公共线程池, 减少线程对象创建销毁. 提高线程的利用率.

一些地方异步操作, 拦截器添加请求日志时异步添加.

你在代码中使用了哪些设计模式?

单例模式, 静态工厂模式, 模板方法, 观察者, 装饰者, 策略模式, 状态模式, 职责链模式.

观察者: 监听商品信息更新, 根据佣金变化幅度决定是否删除, 根据佣金变化和价格变化幅度决定是否通知用户收藏商品变化.

策略模式: 订单不同类型, 对应的商品源不同, 查询数据方式不同, 因此使用策略模式, 便于新增类型的扩展.

状态模式: 红包状态的变化, 可以做成状态模式, 使得红包新增状态时扩展更简单.

架构

重构

1
2
3
4
5
6
重复代码重构, 抽象出工具类, 返回值/自定义异常 整理重构, 统一请求风格
使用状态模式/策略模式优化 if/else
使用工厂模式统一管理需要的实例对象, 如工具类, 邮件服务等
封装通用 CRUD 接口及实现, 减少 Dao 层代码

模块的拆分, 数据库分库分表, 微服务拆分

集群

MySQL 集群

MySQL 默认支持主从架构集群, 可配合 mycat 实现读写分离.

Redis 集群

Redis Cluster, Codis

Tomcat 集群

tomcat 集群一般需要考虑 session 共享, 可通过 redis 实现 session 共享.

分布式

SpringCloud

Spring Cloud 是一套分布式开发的解决方案, 集合了分布式调用, 链路追踪, 降级处理, 服务注册发现.

Dubbo

Dubbo 是一个分布式 RPC 调用框架, 底层使用 netty 框架.

运维

Docker

docker 是一个容器, 提供了标准化的接口, 可用于快速构建部署环境, 简化部署流程

docker-compose

docker-compose 使用 yml 文件描述容器间的关系以及容器的配置, 可用于快速构建复杂的运行环境.

K8s

K8s 是一个根据容器快速搭建和管理集群的工具.

JVM

JVM 内存模型

每个线程有自己的内存区域, 多线程之间通信主要通过共享内存来实现.

  • 有序性: 在 CPU 执行指令时, 可能会对非 happens-before 指令进行重排, 优化执行效率. 在单线程情况, 往往不会产生问题, 但涉及多线程时, 可能导致 bug.
  • 可见性: 一个线程修改了一个共享变量, 另一个线程不会知道这个改变, 这就是不可见, 要确保可见性, 一般使用 volatile 关键词, 当然, 加锁也可以.
  • 原子性: 即对于某代码, 实际执行时会分为好几个原子指令, 确保原子性必须加锁 (如synchronized) 处理
1
2
3
4
5
happens-before:
读后写
写后写
锁后解锁
可传递性

多线程

线程是一个进程中的不同执行路径, 一个进程至少有一个主线程.

进程是一个程序的抽象, 一个程序运行后一般为一个进程.

线程状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.New (新建)
2.Runnable (就绪)
3.Running (运行中)
4.Blocked (阻塞)
5.WAITING (等待)
6.TIMED_WAITING (超时等待)
7.Dead (死亡)

[t:thread对象, obj: 同步块中的对象]
New: new Thread()
Runnable: t.start(), t.yield()
Running: after t.start() and cpu run it
Blocked: when enter synchronized block
WAITING: obj.wait(), t.join(), LockSupport.park()
TIMED_WAITING: Thread.sleep(x), obj.wait(x), t.join(x)
Dead: when t.run() is over

JVM 对象结构

1
2
3
4
5
6
对象头:
Mark Word(hash, 锁状态, 分代年龄)
类型指针
[数组长度]
实例数据
对齐

垃圾回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
标记算法:
1.引用计数法
2.可达性分析算法(根搜索)(根对象: 栈中的对象, 静态属性引用对象, 常量引用对象)
回收算法:
1.标记-清除算法
2.标记-整理算法
3.标记-复制算法
4.分代算法( Eden 区(复制算法)--> Survivor 区(缓存, 复制算法) --> Old 区(标记-整理)

回收器: (前 3 个 Young GC 使用, 后面的 Full GC 使用)
1.Serial (串行, 复制算法)
2.ParNew (多线程, 复制算法)
3.Parallel Scavenge (多线程, 改进版, 可控制吞吐量)
4.Serial Old(单线程, 标记-整理)
5.Parallel Old (多线程, 控制吞吐量, 标记-整理算法)
6.CMS (多线程, 低停顿, 标记-清除算法) : 初始(STW)-并发-重新(STW)-清除
7.G1 (多线程, CMS 升级版, 标记-整理算法): 初始(STW)-并发-最终(STW)-筛选清除(可控制停顿时间)

MySQL

SQL 优化

索引原理

MySQL 索引一般选择 B+树做为数据结构存储. B+ 树的优点是, 对文件IO的访问次数控制在 3 次, 保证速度的同时, 能存储千万行数据.

索引

1
2
3
1.对常用列添加索引, 视具体情况选择单一索引或复合索引(一般为复合)
2.通过 Explain 语句分析执行计划, 将 type 提升到至少 index 级别.
3.通过 Explain 语句分析执行计划, 将 extra 中 Using filesort消除(排序列加索引), Using join buffer消除 (通过给关联表的关联列加索引), Using temporary (一般通过分组列加索引), Using where(根据最左原则对条件列加复合索引)

事务

1
2
3
4
5
ACID:
A: 原子性, 多个操作要么都做, 要么都不做
C: 一致性, 数据库文件的状态必须从一个一致性状态到另一个一致性状态.
I: 隔离性, 事物之间相互隔离, 互不影响.
D: 持续性, 一个事务一但提交, 则对数据库的改变是永久的.

数据库隔离级别

1
2
3
4
1.读未提交: 可读取其他未提交事务的执行结果(如更新了某个字段), 可能会造成读取错误的数据(未提交的事务回滚了), 造成脏读.
2.读已提交: 可读取其他已提交事务的执行结果, 2次读取数据还是可能不一致(其他事务又提交了), 造成不可重复读.
3.可重复读: 确保同一事务内多次读取数据时, 会看到相同的数据. 但可能造成幻读, 如批量修改登录密码后, 另一个事务新增了一条记录, 导致新纪录未修改.
4.串行化: 事务串行化执行, 效率低.

MySQL 默认隔离级别

可重读读

数据库锁

锁原理

1
2
行锁: 分为排它锁(X) 和共享锁(S). 即写锁和读锁.
表锁: 分为元数据锁(MDL)和表锁.

锁触发方式

1
2
行锁: 隐式(条件带有索引则锁对应列, 不带索引则锁全部行, RR 总会带有 GAP 锁, RC 不会), 显式(使用 for update, lock in share mode)
表锁: 隐式(对整个表不带条件进行增删改, 或任何 DDL 操作) 显示(使用 for update, lock in share mode)

源码和框架

ReentrantLock

加锁流程 lock()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1) acquire(): 尝试获取一个许可证, 获取成功则直接返回(lock结束), 获取失败则需要排队
2) tryAcquire(): 判断当前许可证数量(state), 若为0则尝试获取
分公平和非公平, 公平锁会判断 hasQueuedPredecessors, 非公平则直接抢 compareAndSetState
若不为0, 则判断持有锁的人是否为我本身, 是则增加当前许可证数量, 返回true获取成功
不是则 返回 false, 获取失败(将排队).
3) addWaiter(): AQS 队列尾部添加一个 Node(waiter=X[独占锁]), 若 tail 不存在, 则先初始化一个 空head[空指不代表任何线程] 后再加入队列
4) acquireQueued: 进入队列的节点, 尝试获取许可证, 失败则 park()
先判断node的上一个节点是否为 head 节点, 若是, 则要尝试获取一次许可证(因为这说明上一个线程已经在执行过程中了, 也许已经走完了unlock() 方法(即已经运行过唤醒队列下一位的代码了,而因为你那时还不在队列中或没进入睡眠中, 唤醒代码是无意义的), 而你则刚加入队列, 如果你此时直接park()去等待唤醒, 则根本无人唤醒你, 同理你的下一个节点也就等不到你去唤醒它.)
如果不是, 设置了上一个节点的 waitStatus 为 SINGLE 后, 自己睡眠 park(), 等待唤醒

唤醒后:
5) 判断上一个节点是不是 head, 一般来说是(因为unkock唤醒的一般就是head.next)
如果不是则进入 shouldParkAfterFailedAcquire: 将队列中一些已取消的节点从队列中删除, 重新设置节点的prev
因为是for循环, 所以又会再回来判断, 这时应该是head了, 尝试获取许可证, 2种可能, 非公平时被刚lock的人抢了(概率较小吧), 另一种就是获取成功
获取成功后, 把原head节点删掉, 自己设为head节点(head象征一个拿到许可证的节点,除队列第一次初始化), 然后返回到acquire(), 中途没有线程被打断就正常出方法, lock结束

总结:
得到方式1: acquire 时 state = 0, 且抢到了.
得到方式2: 没抢到或不让抢(公平锁), 进入队列等上一个来唤醒我, 上一个等上上个来唤醒他, 上上个等上上上个唤醒....
unlock 唤醒队列第二个非取消的线程并删除队列第一个元素 [其他元素移位]
这样第二个线程就可以唤醒非取消的第三个线程[相对而言的第三个,实际上唤醒时还是第二个, 只是唤醒后会删除第一个, 所以第三变第二]

解锁流程 unlock()

1
2
3
4
5
6
7
8
1) release(): 释放一个许可证, 并根据当前许可证数量是否为0 判断是否可以唤醒下一个节点
2) tryRelease(): 释放一个许可证, 判断线程是否正确(是不是当前独占锁), 许可证减一
当前许可证数量是否为 0 返回是否可以唤醒队列的 bool 标识.
3) unparkSuccessor(): 唤醒队列中除head外第一个处于阻塞(非取消)的节点(查找方式, 先看next, next状态不对则从后往前找最前的非取消的节点, 因为next如果为null, 无法找null的next).
4) 唤醒后, 会将head设置为唤醒的节点, 以此达到下次唤醒下一个的目的.

总结:
唤醒的逻辑就是将排队的所有节点挨个唤醒, 而节点被唤醒后又会出队列; 所以代码将出队列和唤醒逻辑一起做, 先唤醒下一个, 下一个负责把前一个移出队列. 然后唤醒自己的下一个, 以此类推, 就实现了唤醒和出队列的操作.

ReentrantReadWriteLock

1
2
3
4
5
6
7
8
9
读锁
获取锁 tryAcquireShared(), 若当前没有写锁存在, 则 state + 1个读单位, 然后返回获取成功. 防止返回获取失败, 进入队列休眠.
释放锁 tryReleaseShared(), state - 1个读单位, 然后根据 state = 0 判断返回是否可以唤醒队列.

写锁
获取锁 tryAcquire(), 若存在读锁, 则失败, 若存在写锁, 判断是否重入获取, 是则返回获取成功. 否则失败; 失败就意味着加队列,休眠.
释放锁 tryRelease(), state - 1, 判断 state 中写锁数量是否为0, 是则可以唤醒队列. 否则代表这时一个可重入锁的释放逻辑.

总结: 读写锁也好, 可重入锁也好, CountDownLatch 等工具类也好, 都是对 state 操作为多, 或者说, 实现了 AQS的它们, 只负责操作 state, 而队列, 唤醒, 都交给 AQS 来处理.

CountDownLatch

countDown()

1
1) state 数量减一, 然后判断 state 数量是否为0, 若是则唤醒等待队列的线程.

await()

1
2
1) new 之后, state 数量大于0, 所以会进入等待队列, 然后线程会进入休眠.
2) 等待countDown释放锁, 释放到许可证为0时, 唤醒等待队列的线程.

总结:

利用了加共享锁进入队列等待特性实现 await()

释放共享锁减少许可证数量且唤醒队列中的等待的线程 实现 countDown()

Semaphore

1
2
与 CountDownLatch 相反, 初始数量一般为 0, acquire() 时判断是否有许可证, 有则成功, 无则队列休眠
而 release 则是添加一个许可证, 添加后总是唤醒队列.

CycleBarrier

1
2
3
含义: 凑足一定个数线程, 然后批量唤醒.
await(): 利用 ReenrantLock 的 lock 和 condition 的 await 进入休眠
当凑足后,用condition 的 singleAll 唤醒所有 await 的线程.

HashMap

请简述 HashMap 的底层数据结构

1
2
1. 使用了数组加链表, 以数组为主, 链表加红黑树为补充的数据结构来存储键值对.
2. 当键发送冲突(碰撞)时, 数据将串成链表存于数组中, 当链表长度超过指定值(默认 8)时, 链表转成红黑树, 当红黑树长度小于指定值时(默认 6), 则又转成链表

为什么 HashMap 的初始容量以及扩容后的容量均为 2 的指数幂

因为计算机做运算时, 取模运算速度远远慢于位运算, 而若容量始终为 2 的指数幂, 则根据 hash 获取数组下标时只需要 使用 (数组长度-1) & hash 值 即可确定数组下标, 与取模得到的下标一样可靠.

而扩容后后, 因为需要进行 rehash 运算来确定 数据的新下标, 多次进行取数组下标则更能体现位运算的优势.

为什么 HashMap 的加载因子是 0.75 (3/4)

1
2
3
4
使用排除法:
1.若加载因子为 1. 则每次 HashMap 满了才进行扩容, 必将有更高的几率触发 hash 碰撞导致数组下标一致需要转成链表或红黑树, 导致读取和更新速度降低.
2.若加载因子为 0.5. 则每次 HashMap 都有一半容量剩余, 空间大大浪费, 对内存开销太大. 容易引发 OOM 事故.
3.0.5-1 之间那么多可能, 选哪个都行, 但作为 HashMap 的默认值, 选中间的 0.75, 走中庸之路, 也是解释的通的.

为什么 HashMap 1.8 扩容无需 rehash

1
2
3
4
5
6
7
1. 因为1.8的获取 hash 值的算法优化了. 无需一个 hashSeed 进行辅助运算 (主因)
2. 由于 hash 值不变, 原链表中的所有节点只有 2 种可能:
一是 hash 值高于原数组长度, 则属于高位, 这些高位的节点, 新的下标一定是 (当前下标 + 旧数组长度).
另一种是 hash 值低于原数组长度, 属于低位, 这些节点的下标无需重新计算, 必然与当前下标一致
(不信自己那几个示例数据用画出完整二进制计算一下)(神奇的位运算)
3. 重新计算下标时, 根据第 2 点可知, 其下标大小一定不高于(当前下标+旧数组长度), 即下一次循环的下标必然比上一次循环的下标要高, 所以 1.8 源码 resize 进行高低位分组然后转移数据时, 无需担心下一次循环会将刚刚放到新数组的值覆盖(下标相同则会覆盖)
4. 1.8 的 resize 优化了算法, 保持了原有的链表顺序(不知道有啥用)

总得来说, 1.8 优化了 hash 算法, 使 hashcode 的高 16 位与 低 16 位进行异或运算, 降低了碰撞率

而 resize 算法也优化链表节点的迁移, 避免了 1.7 的链环产生

最大的区别就是, 1.7 没有将二进制的神奇发挥到极致, 依然像普通 java 程序一般逻辑. 而 1.8 则充分利用了二进制的优点(也充分的让人头晕), 提高了 HashMap 的效率.

为什么 HashMap 从链表达到 8 个时转成红黑树, 达到 6 个时转回链表?

1
2
1.根据 Poisson distribution 定律, 凑齐8个节点碰撞到同一个下标, 组成长度为 8 的链表概率极低, 约为 0.00000006, 而超过 8 个的几率则更低, 大约为千万分之一. 所以将阈值设置为 8, 因为这种概率极低. 因此可以减少链表转红黑树的, 提高增删改效率.
2.若达到 7 个时转回链表, 则可能会导致HashMap 不停的在链表和红黑树之间转换, 所以阈值设置为 6, 可起到缓冲效果.

Spring

关键类解析

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
ApplicationContext
是一个只读的 bean 容器
可以加载解析配置文件(如xml)
可以发布事件和注册监听
具有国际化消息处理能力
ConfigurableApplicationContext
是一个具有可配置能力的 容器(可设置各个参数, 如id, 父容器)
具有容器生命周期概念, 如启动,停止,关闭.
AbstractApplicationContext
模板方法模式的抽象类, 定义了容器的模板(refresh方法), 但由具体的子类实现部分方法
管理Bean和BeanFactory的PostProcessor
管理事件的监听和处理
AbstractRefreshableApplicationContext
为可重复刷新的容器提供基类
加入了BeanFactory的管理(创建/关闭等)
AbstractRefreshableConfigApplicationContext
加入了configLocation字段, 用于某些容器初始化BeanFactory和Bean
AbstractXmlApplicationContext
定义了读取xml配置文件来加载BeanFactory的代码, 使得子类只需提供配置文件地址或Resource
ClassPathXmlApplicationContext
继承基类, 提供配置文件地址的构造方法, 调用refresh加载BeanFactory

BeanFactoryPostProcessor
用于给BeanFactory添加插件式功能, 如配置文件解析 ${} 占位符
如ConfigurationClassPostProcessor 将@Configuration类下的带@Bean的method返回值注册到beanDefinitions 中

BeanPostProcessor
用于给bean添加功能, 如ApplicationContextAware的自动注入就是如此实现的

容器初始化流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1) 从XmlClassPathApplicationContext构造方法中进入 refresh 方法
2) 先设置容器状态
3) 调用子类初始化 BeanFactory
4) 设置BeanFactory 一些属性,添加一些内置的PostProcessor 注册一些 environment 相关的bean
5) 子类设置一些内置的PostProcessor
6) 扫描添加并执行容器内的 BeanFactoryPostProcessor
7) 扫描容器内的 BeanPostProcessor 并注册
8) 初始化国际化消息处理器
9) 初始化事件广播处理器
10) 执行子类的 refresh 逻辑
11) 扫描容器内的 ApplicationEvent (指实现类) 并注册到事件广播处理器
12) 完成BeanFactory的初始化, 并加载一些单例对象(设置了急于加载的bean)
13) 初始化LifcycleProcessor, 调用onRefresh方法, 发布 ContextRefreshedEvent 事件.
14) 清除一些缓存(如反射缓存, 注解等)

某些实现原理

实现 ApplicationContextAware 为何会自动注入 applicationContext?
1
2
3
1) 首先 AbstractApplicationContext#prepareBeanFactory 会添加一个ApplicationContextAwareProcessor
2) 这个 beanPostProcessor 负责在bean初始化之前注入context对象.
3) 这个 beanPostProcessor 的执行时机是在 doCreateBean 中的 postProcessBeforeInitialization()
实现 ApplicationListener 为何会在事件触发时自动执行我们实现的方法?
1
2
3
4
1) 在 AbstractApplicationContext#registerListeners() 中扫描容器内所有相关实现类加入到事件监听者集合中
2) 然后在publishEvent时,遍历事件监听者集合调用bean的方法即可。观察者模式!
3) 另外也用了BeanPostProcessor去实现, 叫 ApplicationListenerDetector, 加入时机同1, 执行时机同1.
4) 至于为何使用2种机制,与多例有关吧!(scope="prototype")
实现Order接口或注解时如何自动排序的?
1
2
1) 比如说 BeanPostProcesser, 容器扫描后, 会像对bean集合排序, 再遍历执行.
2) 详细过程见 PostProcessorRegistrationDelegate#sortPostProcessors()
单例对象如何实现循环依赖注入?
1
2
3
4
5
6
7
8
9
10
1) 首先, 设定对象A,B, A 持有 B, B 持有A, 构成循环
2) 此时程序调用getBean获取A,则在 doCreateBean 中 创建后将bean缓存到 singletonFactories 中
3) 然后设置属性B, 解析属性, 需要获取B对象
4) 获取B, 则执行doCreateBean 后执行解析属性, 需要获取 A对象 (又一次)
5) 获取A, 进入 doGetBean 中的 getSingleton, 此时判断singletonFactories中有A, 则可以直接取出A
6) 获得A后, 即可完成B的属性赋值, 然后会完成B的创建.
7) B创建完后, A就能获得B, 则A也完成了属性赋值, 最后完成创建A.
8) 到此, 返回即可.

> 总结: 首次获取A, 创建A对象后缓存一个存储A对象的 ObjectFactory 实例, 再解析属性时触发 getBean(B), 同理也会做缓存, 然后也解析属性, 触发getBean(A), 第二次获取A, 进入另一个逻辑, 返回 ObjectFactory 实例中存储的对象A, 即可完成getBean(A), 然后完成getBean(B), 再完成外层的getBean(A).

TIPS

观察源码, 发现有2个缓存, 一个是 singletonFactories, 另一个是 earlySingletonObjects.
其中earlySingletonObjects的管理都在 getSingleton 方法中做, 而 singletonFactories 则在doCreateBean中加入, 在 getSingleton 中删除(有earlySingletonObjects后就可以删除了).
虽然有2个缓存, 但如果你的bean没有使用BeanFactory创建, 则其实一个缓存也足够了
(因为这样的话 singletonFactories 每次创建返回的都是同一个, 因为此时 singletonFactories存的只是代码包装的一个内部类, 而非用户自定义的.)

InstantiationAwareBeanPostProcessor等一些特殊BeanProcessor的扩展方法是何时自动调用的?
1
2
3
4
1) 首先 getBeanPostProcessorCache 获取一些特殊的BeanPostProcessor
2) 如 InstantiationAwareBeanPostProcessor/SmartInstantiationAwareBeanPostProcessor
3) 然后 createBean时, 会在正确的时机使用到这些特殊的 PostProcessor, 取出来, 然后执行对应方法
4) 具体何时可以查看 getBeanPostProcessorCache() 的调用位置一一查看.

注解的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1) 首先是指定包名或指定类名
如指定包名则scan时会执行, 如指定类名则在构造方法初始化 reader 时执行
2) 无论哪种, 最终都会走一段代码 AnnotationConfigUtils#registerAnnotationConfigProcessors()
3) 这段代码会添加一些 BeanFactoryPostProcessor
如 ConfigurationClassPostProcessor 负责解析 @Configuration/@Import/@Bean 注解
postProcessBeanDefinitionRegistry() 中 parse 所有的带以上注解的 beanDefinitions
ConfigurationClassParser 将注解信息解析保存,
然后由 ConfigurationClassBeanDefinitionReader 负责注册到容器。
如 AutowiredAnnotationBeanPostProcessor 负责解析 @Autowired/@Value 注解
如 CommonAnnotationBeanPostProcessor 负责解析 @Resource 注解
解析放在 postProcessProperties() 方法中, 先扫描bean的字段和方法, 然后一一调用方法和为字段注入值
4) 之后, 他会将扫描的类放到 beanDefinitions 中(或指定的类注册进去)
5) BeanFactory加载完毕后, 回到AbstractApplicationContext的refresh逻辑
如会执行 postProcessBeanFactory(), 调用前面加入的ConfigurationClassPostProcessor
然后会添加更多的类到容器中.

注意事项:
@Configuration@Component的区别?
观察发现,即使使用@Component 其下带 @Bean 的方法依然可以注入到容器中。所以似乎两者没有区别?
仔细查看源码和资料后,发现 postProcessBeanFactory() 方法在 processConfigBeanDefinitions() 后还会调用 enhanceConfigurationClasses()
而在这个方法中, 对前面解析了classCONFIGURATION_CLASS_FULL (即代表@Configuration)的类
会生成一个 cglib 的代理, 这样获取@Bean注解的方法的bean时,不会每次调用方法new 一个, 而是有缓存.

AOP 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1) 使用 @EnableAspectJAutoProxy
2) @EnableAspectJAutoProxy 中使用了 @Import(AspectJAutoProxyRegistrar.class)
3) ConfigurationClassPostProcessor 会解析@Import, 进入 registerBeanDefinitions() 中
4) registerBeanDefinitions() 中添加了 AnnotationAwareAspectJAutoProxyCreator 到容器中
5) AnnotationAwareAspectJAutoProxyCreator 本质上时一个 BeanPostProcessor
6) 因此在 createBean 时, 会被自动调用. 其中 postProcessAfterInitialization() 负责创建代理对象
7) 而 getAdvicesAndAdvisorsForBean() 则负责查找对应的增强. 然后会调用子类的findCandidateAdvisors
8) 如 AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors() 负责注解编写增强@Before/@After
9) 简单说下逻辑, 就是查找容器所有类, 判断这个类有没有 @Aspect 注解, 然后先找出所有Pointcut
再遍历所有方法, 找出方法上带有@Before等注解且有关联的Pointcut的方法,
然后使用这个方法和关联的Pointcutnew 一个Advisor, 加入到Advisor集合中, 遍历结束后返回即可.
10) 查找到所有的增强后, 再比较Pointcut表达式是否匹配当前的bean, 如可以则加入.
11) 根据找到的Advisor集合, 创建一个带配置(advisor集合等)的代理对象, 代理对象执行方法前
12) 会先根据配置中的advisor集合生成一个执行链, 然后在拦截代理方法处调用. 执行链会负责执行通知.
13) 不同的通知由不同的适配器执行.

Spring 事务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0) 事务是由AOP实现的, 所以需要找到对应的Pointcut 和 Advisor
1) 打开了 @EnableTransactionManagement 注解
2) 然后@Import 了 TransactionManagementConfigurationSelector
3) 之后导入了 ProxyTransactionManagementConfiguration 到容器中
4) ProxyTransactionManagementConfiguration 带有 @Configuration
5) @Bean 注入了一个通用的Advisor: BeanFactoryTransactionAttributeSourceAdvisor
6) 这个Advisor的 Pointcut 是由 TransactionAttributeSourcePointcut 实现的
实现逻辑是 TransactionAttributeSourcePointcut 的 matches()
这个方法调用了 getTransactionAttributeSource() 获取 AnnotationTransactionAttributeSource
然后通过 getTransactionAttribute() 调用了 findTransactionAttribute()
最终使用SpringTransactionAnnotationParser 类判断方法是否有@Transactional注解
并解析注解信息然后返回. 另外这个方法还可以获取@Transactional注解的信息, 而这里只用于判断是否需要拦截这个方法.
7) TransactionInterceptor 是一个Advisor
也可以通过AnnotationTransactionAttributeSource获取@Transactional注解上的信息
然后在invoke中, 拦截方法, 打开事务, 在执行完方法后, 提交事务, 报错时回滚事务
这个 Advisor 不同于传统的前置/后置, 而是更具体的 MethodInterceptor.

Spring Boot 源码

简化了多少操作?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.MultipartAutoConfiguration
添加了一个bean StandardServletMultipartResolver, 并设置上传文件大小等属性
使得Spring MVC中的 DispatcherServlet 可以获取 multipartResolver, 处理文件上传.
若不是Spring Boot, 需要在xml或注解中手动添加一个 这样的bean才能处理文件上传, 而且配置文件还要自己读取.

2.MailSenderAutoConfiguration
添加了一个 JavaMailSenderImpl 的bean到容器中, 并设置邮箱服务器/账号/密码等属性
若不是Spring Boot, 需要在xml中配置一个bean即配置他的属性(邮箱配置)

3.TransactionAutoConfiguration
在内部类添加 @EnableTransactionManagement,剩下的参考上面的 Spring 事务实现

4.WebMvcAutoConfiguration
添加了 RequestMappingHandlerAdapter/RequestMappingHandlerMapping
添加了 ContentNegotiatingViewResolver

启动流程

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
1) 创建 SpringApplicationRunListeners 管理 run 过程的事件, 监听者取自 spring.factories
2) 触发 run 的开始(starting)事件
3) 初始化 environment 对象, 并利用 run 的 environmentPrepared 事件将application.yml的数据注入
4) 打印 Banner, 可自定义 Banner 通过 banner.txt 文件
5) 根据webApplicationType创建一个ApplicationContext 容器
默认使用 AnnotationConfigApplicationContext
6) 为 context 做一些初始化和设置
设置环境变量, 使得 context 可以获取 application.yml 中的配置
调用子类扩展的设置
加载 容器的 initializers
触发 run 的 contextPrepared 事件
打印日志
添加spring boot 启动参数信息到 bean 容器中
设置beanFactory的 allowBeanDefinitionOverriding 属性
设置 懒加载策略, 添加 postProcessor 则会将每个 beanDefinition 的 lazyInit 设置为 true
把启动类class封装成 BeanDefinition 放到容器中, 使得@Configuration之类的注解生效
触发 runcontextLoaded 事件
7) 调用 contextrefresh()
执行 BeanFactoryPostProcessor, 如 ConfigurationClassPostProcessor 解析 @Import 注解
@Import 会实现 @EnableAutoConfiguration, 总之都是熟悉的 spring 套路. boot的东西就少了.
8) 调用留给子类的 afterRefresh() 方法, 默认空实现
9) 打印启动完毕信息
10) 触发 runstarted 事件
11) 调用 容器内所有的 ApplicationRunnerCommandLineRunner 实现类, 类似事件监听.
12) 使用 spring.factories 中的 exceptionReporters 处理可能出现的异常.
13) 触发 runrunning 事件

一些东西的实现原理

@ConfigurationProperties 如何实现自动注入application.properties中配置的值?
1
2
3
4
5
6
7
8
9
10
11
12
13
1) 首先加了 @EnableConfigurationProperties 也会解析里面的 @Import 
2) @Import 则引入了 EnableConfigurationPropertiesRegistrar.class
3) 这是一个 ImportBeanDefinitionRegistrar 的实现类, 会在解析 @Configuration 注解时调用指定方法
4) 指定方法 registerBeanDefinitions() 获取 @EnableConfigurationPropertiesRegistrar 的数据
如 @EnableConfigurationProperties(RabbitProperties.class) 加载 RabbitProperties.class
然后, 将这些 class 都注册到容器中
5) 指定方法还注册了一些工具bean和一个重要的 BeanPostProcessorregisterInfrastructureBeans()中
6) registerInfrastructureBeans() 加载了 ConfigurationPropertiesBindingPostProcessor.class
7) 在 postProcessorBeforeInitialization() 中 调用 ConfigurationPropertiesBinder
8) 调用链很长, 最后 property.setValue(beanSupplier, bound);设置了值 -- JavaBeanBinder

总结: 就是先将 XxxProperties 类定义注入到容器中, 这样可以getBean, 然后通过 BeanPostProcessor
再实例化后将属性值一一绑定.
@ConditionalXxx 的实现原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1) 在类上加上注解 @Conditional 或 带有 @Conditional 的其他注解:其他扩展实现
2) 在所有的扫描类和注解的地方,如解析@Configuration, AnotatedBeanDefinitionReader等reader
会使用 ConditionEvaluator 的 shouldSkip() 判断是否可以加载, 时机点如下
AnnotatedBeanDefinitionReader#doRegisterBean() 的第二行代码
ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod() 第四行
ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass()
ConfigurationClassParser#doProcessConfigurationClass() 处理 ComponentScan 那段
3) 然后再 shouldSkip 中判断, 判断逻辑大致如下:
先遍历所有注解取得所有的 @Conditional 下的 所有 value, 这个 value 是具体的Condition实现, 如OnClassCondition
实例化 Condition 然后添加到 conditions中
排序并遍历调用 matches(), 一个不匹配则返回true, 代表应该跳过.

TIPS:
ConditionOutcome 封装了是否匹配和匹配日志信息[为啥成功/为啥失败]
SpringBootCondition 提供了通用的根据 ConditionOutcome 判断是否匹配并记录日志信息的抽象类.
子类只需实现 getMatchOutcome(): 根据 metadata[注解信息] 返回 ConditionOutcome 对象.
因此, 如果我们要实现自己的 Condition, 可以继承它.

另发现 AutoConfigurationImportSelector 也含有判断Condition的逻辑,
刚开始以为是 AutoConfiguration 的类没有走之前说到的判断, 所以这里要做判断.
后来我一想, 这是 @Import 引入的, 所以, 是走了判断的. 因此这里多出的一个 AutoConfigurationImportFilter, 应该是一个额外的插件, 专门过滤配置在 spring.factories 中的 AutoConfiguration 类的. 而插件的读取, 也是读取 spring.factories 来遍历.

@AutoConfigureAfter 的实现原理
1
2
3
4
5
6
7
8
9
1) 首先 AutoConfigurationImportSelector 是一个 DeferredImportSelector  
2) 这种 DeferredImportSelector 会延迟加载, 原理是 parse 后再加载, 而非parse执行过程中就加载.
3) 延迟加载机制 会先调用 process 方法, 将要加载的class保存起来, 然后再调用 selectImports 返回.
4) 此时 AutoConfigurationImportSelector.AutoConfigurationGroupselectImports() 会调用 sortAutoConfigurations(), 也就是调用了 AutoConfigurationSorter.getInPriorityOrder()
5) getInPriorityOrder() 调用了 sortByAnnotation() 这个方法根据2个注解 @AutoConfigureBefore @AutoConfigureAfter 排序.
6) 最后返回的就是有序的了. 另外, 这两个注解只能作用再 AutoConfiguration 上.

总结:
所有的 AutoConfiguration 所引入的class文件解析完毕后, 再准备加载之前, 进行排序, 然后一一加载.
各种 AutoConfiguration 实现大致流程.
1
2
3
4
5
6
7
8
9
10
11
1) 首先是 @SpringBootApplication 启用了 @EnableAutoConfiguration
2) @EnableAutoConfiguration 又使用 @Import 导入了 AutoConfigurationImportSelector.class
3) 然后 AutoConfigurationImportSelector 导入 spring.factories所有的 EnableAutoConfiguration
4) 最后,当一个项目依赖一个starter-xxx,会继承starter的依赖项, spring-boot-autoconfigure 由此被依赖
5) spring-boot-autoconfigure 带有所有 boot 实现的 AutoConfiguration 和 spring.factories 配置
6) 所以会加载该项目下 spirng.factories 中定义的 EnableAutoConfiguration
7) 每个 AutoConfiguration 实现类被导入到容器中后
又被 ConfigurationClassPostProcessor 解析@Configuration, @Import 等注解 (老千层饼了)
这些AutoConfiguration会添加一些提供服务的 bean,或者再嵌套一层@Import,@Configuration等。
另外, 这些bean还是被自动配置了属性值的, 属性值哪里来? 都在 application.yml 中, 或是默认配置中.
9) 这样,容器中就加入了一个或多个配置好的bean了, 可以直接使用. 如 stringRedisTemplate, jdbcTemplate
Spring Boot 是如何自动扫描main方法所在类所在包的?
1
2
3
4
1) 首先是 @SpringBootApplication 启用了 @EnableAutoConfiguration
2) @EnableAutoConfiguration 又使用 @AutoConfigurationPackage
3) @AutoConfigurationPackage 中的 @Import 会被解析, Registrar.class 的registerBeanDefinitions会被执行
4) 最终根据带有 @SpringBootApplication 的类对应的包名, 然后自动扫描到容器中, 效果同 @ComponentScan
application.properties 是如何被加载到Environment中的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1) run方法中创建了Environment对象, 当初始化好一些东西后会触发事件
2) 通过 SpringApplicationRunListeners 的 environmentPrepared() 告知监听者
3) 默认存在 spring.factories 中的 EventPublishingRunListener 监听者负责转发事件
4) 事件被转发到 ApplicationListener 下的监听者来处理
5) 监听者配置在 spring.factories 中, 其中 ConfigFileApplicationListener 监听了此事件
6) 此监听者接受 ApplicationEnvironmentPreparedEvent 事件后
7) 加载一些 postProcessor 专门用于处理 environment 对象的 postProcessor
8) 其他的 postProcessor 暂时不管, 真正做了加载的是 postProcessors.add(this); 自己的实现
9) 自己的处理方法是: addPropertySources(), 此方法将会扫描指定的路径下指定的某些文件
10) 然后使用 spring.factories 下的 PropertySourceLoader 一一尝试解析
11) 文件存在且解析正确则加入到 environment 的 propertySources.
某些路径: getSearchLocations() ,默认: classpath:/,classpath:/config/ ...
某些文件: getSearchNames() ,默认: application

TIPS:
PropertySourceLoader 有 PropertiesPropertySourceLoader/YamlPropertySourceLoader
一个尝试后缀有 xml/properties, 另一个是 yml/yaml, 所以所有可能性有:
classpath:/application.xml; classpath:/application.properties
classpath:/application.yml; classpath:/application.yaml
......
SpringApplication.run() 如何加载 tomcat 的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1) boot 的 run 里面会创建 applicationContext, 如是 webApplicationType = SERVLET, 则实现类为 AnnotationConfigServletWebServerApplicationContext
另外一提, webApplicationType 是根据 classpath 下是否有哪些类来推断的.
2) 这个类继承了 ServletWebServerApplicationContext
3) ServletWebServerApplicationContext 实现了 onRefresh()
4) onRefresh() 调用了 createWebServer()
5) createWebServer() 使用 ServletWebServerFactory.getWebServer() 获取 webServer 对象
6) ServletWebServerFactory 会被 ServletWebServerFactoryAutoConfiguration 引入
详细见 ServletWebServerFactoryConfiguration.EmbeddedTomcat.class
7) 引入后调用 getWebServer(), 大概为 new Tomcat(), 设置属性, 然后启动.
8) 至此, run() 启动了 tomcatjetty/undertow.

TIPS:
spring 使用工厂模式获取webServer, 然后工厂又通过AutoConfiguration自动注入(还会判断Condition).
这样如果新增一种 webServer, 只需要在写一个 AutoConfiguration 注入一个 工厂即可. 非常灵活.

Spring MVC 源码

关键类解析

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
WebMvcConfigurationSupport
默认注册了很多东西,如HandlerMapping几个实现, HandlerAdaptor几个实现

HandlerMapping
添加容器内所有带有RequestMaping的类的公开方法到 mappings 中
(AbstractHandlerMethodMapping#afterPropertiesSet中)
根据request的uri查找对应的HandlerMethod, 步骤概述:
把RequestMapping注解内的path作为key保持到一个map1
其他信息封装成mapping作为key也保持到另一个map2
根据uri去 map1 获取 mapping, 再根据mapping 获取 HandlerMethod
封装成Match对象, 与其他匹配对象做比较后, 返回 HandlerMethod

HandlerAdapter
初始化参数解析,返回值解析等
(RequestMappingHandlerAdapter#afterPropertiesSet)
根据Handle确定对应的HandlerAdapter, 然后执行这个 handler
如RequestMappingHandlerAdapter 则负责执行 HandlerMethod
简单说就是封装 HandlerMethod, 根据参数值设置参数, 然后调用方法, 再处理返回值封装成ModelAndView
另外, 这里如果使用了@ResponseBody,会进入 RequestResponseBodyMethodProcessor
然后使用messageConverters(json)写入到响应流
最后mv也直接返回null, 不需要render了。

ViewResolver
负责将ModelAndView解析成HTML, 如JSP, FreeMarker

HandlerExecutionChian
管理拦截器和封装Handler, 负责拦截器的实际调用逻辑实现

DispatcherServlet
调度整个HTTP请求响应流程, 调用各个子组件负责执行处理方法, 解析视图, 处理异常等.

执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET
HttpServlet#service()
HttpServlet#doGet()
FrameworkServlet#doGet()
FrameworkServlet#processRequest()
FrameworkServlet#doService()
DispatcherServlet#doService()
DispatcherServlet#doDispatch()
调用容器内所有的HandlerMapping的实现类的getHandler方法, 返回HandlerExecutionChain
调用容器内所有的HandlerAdaptor的实现类寻找适合的当前Hanlder的HandlerAdaptor
执行拦截器的 preHandle 方法, 并根据返回结果判断是否接续执行
HandlerAdaptor执行handle方法
进入RequestMappingHandlerAdaptor执行handle->handlerInternal->invokeHandlerMethod
生成ServletInvocableHandlerMethod(就是实现了反射调用方法和设置参数,处理返回值等操作)
调用invokeAndHandle-->invokeForRequest
其中getMethodArgumentValues挨个调用HandlerMethodArgumentResolver获取参数值
然后执行 doInvoke, 利用反射技术调用 Method, method.invoke(obj, args);
执行完后, 返回结果, 回到 invokeAndHandle 调用 returnValueHandlers 处理返回结果
返回后 回到 invokeHandlerMethod, 调用 getModelAndView 获取通用的返回值(可能是空)
返回ModelAndView后, 回到doDispatch, 设置默认viewName (mv为空则不需要设置)
执行拦截器的 postHandle 方法
processDispatchResult 中的 render 解析视图后通过response 响应这个view
根据异常情况执行异常处理器, 以及执行拦截器的 afterCompletion 方法

拦截器原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
何时加入?
从WebMvcConfigurationSupport的子类中调用addInterceptors
添加一些拦截器和拦截器的路径配置 InterceptorRegistry 和 MappedInterceptor
实现拦截器路径匹配, 在 new HandlerExecutionChian 时判断

何时执行?
DispatcherServlet 负责在正确的时机调用 HandlerExecutionChian 来调用 preHanlde 等方法.
拿到 HandlerExecutionChian 后调用 preHanlde
HandlerAdapter执行完handler后, 调用 postHandle
解析视图并渲染到response之后, 调用 afterCompletion
如果中途出现异常, 或preHandle提前结束, 则也调用afterCompletion

总结
DispatcherServlet 去调用 HandlerExecutionChian去调用 拦截器具体方法.
复杂点是添加一个拦截器到被加入到HandlerExecutionChian比较复杂一点, 以及带路径匹配的拦截器实现略复杂一些.

Mybatis 源码

关键类解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Configuration
作用: 解析和保存大配置(mybatis全局配置,如数据库连接, 别名等), 小配置(每个mapper文件)信息

MapperProxy, MapperMethod
作用: 生成daoImpl代理对象和实现接口方法: 调用sqlSession 操作方法

Executor
作用: 协调和管理StatementHandler, ParameterHandler, ResultSetHandler, 解析mapperStatement配置信息成BoundSql.

StatementHandler
作用: 生成preparedStatment对象(JDBC的), 调用execute方法.

ParameterHandler
作用: 管理并使用TypeHandler为preparedStatment对象设置参数,

ResultSetHandler
作用: 将ResultSet结果集转成接口返回值认识的数据, 负责延迟加载.

生命周期与执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
1.读取配置文件, 解析xml(或properties)获得配置信息(数据库配置和mapperSql等) Configuration
2.获取数据库连接, 开启事务, 准备执行sql (openSession)
3.使用动态代理技术生成代理类
4.代理类根据代理方法对应的mapperStatement类型调用sqlSession的insert,update,delete,selectXxx方法 (MapperProxy, MapperMethod)
4.sqlSession调用Executor执行sql (Exector, SimpleExecutor, CachingExecutor)
5.Executor 生成StatementHandler对象并解析xml中sql参数, 再使用ParameterHandler设置参数. (BaseExecutor, StatmentHandler, ParameterHandler, TypeHandler 前三个可被插件拦截任意方法)
6.最后使用OGNL表达式解析库对 if,foreach,where等标签解析生成最总sql (ONGL)
7.执行sql后获得resultSet, 使用 ResultSetHandler 处理后返回结果(resultMap, resultType的处理在这里) (ResultSetHandler 可被插件拦截任意方法)
8.根据事务配置提交事务(自动提交), 保存一级缓存
9.如开启二级缓存, Executor还会被装饰器模式包装一层, 将结果缓存到MapperStatement的cache变量中.

TIPS:
1.插件的使用实在Executor, StatementHandler, ParameterHandler, ResultSetHandler这几个对象实例化的时候, 使用jdk动态代理将配置里配置的形成代理链. 返回代理对象, 但调用方法时, 会判断是否拦截此方法并执行插件的代码.

使用到的设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.代理模式
MapperProxy 生成 mapper 实现类 jdk代理
Plugin 生成代理对象实现插件 jdk代理
ResultSetHandler(LazyLoad) cblib, javassist
2.装饰者模式
CachingExecutor: 装饰SimpleExecutor添加二级缓存功能
Cache,FifoCache... 使用装饰者模式为缓存添加不同特性(功能)
3.适配器模式
StatementHandler
4.责任链模式
Interceptor(拦截Executor、StatementHandler、ParameterHandler、ResultSetHandler对象)
5.策略模式
StatementHandler
PreparedStatementHandler,CallableStatementHandler,SimpleStatementHandler
Executor
SimpleExecutor,ReuseExecutor,BatchExecutor
TypeHandler
UnknownTypeHandler, IntegerTypeHandler, NStringTypeHandler
6.建造器模式
org.apache.ibatis.mapping.MappedStatement.Builder
org.apache.ibatis.builder.xml.XMLConfigBuilder

一级缓存实现

1
2
3
4
5
6
7
8
9
1.put时机: 
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase 的 localCache.putObject(key, list)

2.get时机:
org.apache.ibatis.executor.BaseExecutor#query 的 localCache.getObject(key)

3.存在哪里
org.apache.ibatis.executor.BaseExecutor#localCache
这个类每次openSession会新建一个Executor实例

二级缓存实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.put时机: 
org.apache.ibatis.executor.CachingExecutor#query的tcm.putObject
然后实际上是调用了cache的put, cache实际上来自Configuration解析mapper文件时创建的, 即同一个mapper共用同一个cache.

2.get时机:
org.apache.ibatis.executor.CachingExecutor#query的tcm.getObject, cache实际是: 同上

3.remove时机:
org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired

注意事项: 上述时机都是TransactionCacheManager的调用, 而TransactionCacheManager会根据事务的提交和回滚来处理缓存的写入与移除时机, 如真实移除时机是下一次commit触发, 真实写入也是commit时触发.


4.存在哪里
org.apache.ibatis.mapping.MappedStatement
也算是在 org.apache.ibatis.session.Configuration 中

Mybatis-Plus

IO / 网络

AIO, NIO, BIO

1
2
3
BIO: 从 jdk1.4 前, 采用 ServerSocket API 进行网络连接, 所有操作都是阻塞的(如监听, 读/写)
NIO: 从 jdk1.4 起, 对于所有操作都是依赖事件驱动, 只需较少的线程即可处理大量请求
AIO: jdk1.7 起, 在 NIO 基础上实现异步, 所有事件通知由系统通知, 而非NIO 那样轮询

TCP/UDP

1
2
3
4
5
6
7
8
9
10
11
TCP: 是面向连接的协议, 收发数据前, 必须建立可靠的连接(通过三次握手)
UDP: 是无连接的协议, 收发数据不需要建立连接(握手)

区别:
速度: TCP 满, UDP 快
正确性/完整性: TCP 好, UDP 无
顺序性: TCP 好, UDP 无

应用:
TCP: 文件下载, HTTP, DNS, IM 通讯
UDP: 网络游戏, 直播推流

Netty

封装了复杂的NIO接口, 提供了简单的 API 实现服务端和客户端高并发通讯, 并封装了很多工具, 如心跳超时, 半包处理, websocket 等.

数据结构与算法

数据结构

队列, 栈

1
2
队列: 先进先出 FIFO
栈: LIFO

链表

1
2
3
单向链表: Node{next: Node}
双向链表: Node{prev: Node, next: Node}
循环链表: Node{first: Node, last: Node, prev: Node, next: Node}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
二叉树:
先序遍历: 根->左->右
中序遍历: 左->根->右
后序遍历: 左->右->根

红黑树:
红黑树是一种含有红黑结点并能自平衡的二叉查找树.
任意一结点到每个叶子结点的路径都包含数量相同的黑结点.
速度: O(log2 n) 最坏: O(log n)
对比 BST: 查找更快, 插入更慢.
对比 AVL: 查找略慢, 插入更快.

B+树:
所有记录节点存放在叶子节点上,且是顺序存放,由各叶子节点指针进行连接。
如果从最左边的叶子节点开始顺序遍历,能得到所有键值的顺序排序。
查找速度和树高度有关, 如 MySQL 树高度为 3, 则总是查询 3 次找到子节点, 即得到数据.

算法

排序算法

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
快速排序: O(nlog(n))
将数组分为2半,并计算一个中间值,将左边所有大于中间值的移到右边,把右边所有小于中间值的移到左边,
然后分别对左边和右边递归处理(再分2半,再取中间值,再移动,再递归),直到无需递归为止。

归并排序: O(nlog(n))
将数组不断两两拆分,直到无法拆分(只有2个元素)时开始对拆分后的元素进行排序,然后不断回溯,
不断排序,直到第一次拆分,即所有元素都是有序的了。排序方式为先取两个部分的第一个下标,
互相判断,谁小谁放到有序数组中,然后小的那个下标移位,继续判断,直到其中一个部分所有元素取完,
则将另一部分剩余元素批量直接拷贝到有序数组即可。

冒泡排序: O(n^2) 最好 O(n)
进行 n - 1 轮, 每轮遍历数组两两比较交换,从而达到把最大的交换到最后面。下轮遍历数组个数减一。

选择排序: O(n^2)
进行 n - 1 轮,每轮遍历选择数组最小值放到最前面,下轮遍历数组个数减一

插入排序: O(n^2) 最好 O(n)
分2个数组,有序和无序,开局有序默认含有数组第一个元素,无序含有剩余的元素。
遍历无序所有元素,从后向前比较大小,然后进行插入到有序数组中或对数组元素移位。

希尔排序: O(n^1.25)
对数组进行分组,对每组使用插入排序的移位进行排序,第一次分 数组长度/2 组,
下一次分组为数组长度/2再/2组,若分 长度/2 组,则每组2个, 若分 长度/2/2 组,
则每组 2*2 个,直到长度为1. 这是一个将数组后段数据尽可能与前段数据排序,
改进插入排序后端数据过小导致移位操作过多的问题。

基数排序: O(n)
创建10个桶,每个桶大小均为数组长度,然后遍历数组,从元素的最低位(个位)开始,
取出并放到对应的桶(如个位是8,则放到第8个桶中),放完后,顺序遍历所有桶,
顺序取出每个元素,组成新数组,再将新数组按以上流程处理十位,再百位,直到数组中的最大数的最高位。

堆排序:
。。。

优化算法

1
2
3
4
5
贪心算法
爬山算法
模拟退火算法
遗传算法
蚁群搜索算法

设计模式

7 大原则

1
2
3
4
5
6
7
单一职责: 每个类只负责一个职责(或每个方法)
接口隔离: 一个类对另一个类的依赖应建立在最小的接口上
依赖倒转: 高层模块不应依赖低层模块, 二者都应该依赖接口而非细节. 细节依赖抽象, 面向接口编程
里式替换: 子类应该做到可以替换父类, 及子类应尽量不重写父类方法.
开闭原则: 对提供者而已可以修改, 对使用者而言不需要修改(即代码兼容性), 尽量使用扩展增加功能, 而非修改原有类
迪米特法则: 一个对象应该对其他对象保持最小了解(最少知道原则)
合成复用原则: 一个类使用另一个类的代码(方法), 尽量使用合成, 而不是继承

创建型

单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
原理: 确保一个类只有一个实例,并提供该实例的全局访问点。

饿汉式:
静态常量
静态代码块
懒汉式:
直接判断(线程不安全)
方法加 synchronized(线程安全, 效率低)
判断后再同步(错误写法)
双重判断(if-同步-if) (推荐写法)
匿名静态内部类 (简单, 推荐)
枚举(简单, 但对象方法写在枚举中, 略有不适)

示例:
java.lang.Runtime#getRuntime()
java.awt.Desktop#getDesktop()

原型模式

1
2
3
4
5
6
7
8
9
原理: 使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象.
示例: Java 的 Object 对象的 clone 方法, java.util.Arrays.ArrayList#toArray()

浅拷贝: 仅对基础类型及字符串类型的字段拷贝值
深拷贝: 同时对引用类型(如数组,对象) 也进行拷贝

深拷贝实现:
1.重写 clone, 一一处理每个引用对象(调用对象的 clone), 麻烦, 且若对象之间关系复杂, 其中一个未实现深拷贝则导致 bug.
2.利用序列化和反序列化, 如 Json, 或 Java 自带的序列化方式(二进制)

创建者模式(生成器模式)

1
2
3
4
5
6
原理:
封装一个对象的构造过程,并允许按步骤构造.
若对象的生成过于复杂(字段极多且赋值还有依赖关系, 需要顺序调用), 则可将赋值过程封装成一个build(), 并放到一个 Builder 类中. 此类对外提供各个字段的赋值方法并先保存起来, 直到调用 build(), 此方法返回对象实例.
使用此模式, 调用者无需关注构建过程, 只需设置自己想要的值, 然后调用 build() 即可得到对象实例. 且若增加或修改字段, 构造过程变化, 调用者无感知, 无需修改代码. 符合开闭原则.

示例: StringBuilder, 一些框架的 ConfigurationBuilder(如 xmpp), 用于构建配置.

简单工厂模式

1
2
3
4
5
原理:
在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。
此模式可避免多个调用者创建对象时判断创建哪个子类的重复代码, 且若多一个子类, 调用者无需修改代码.

示例: Spring ApplicationContext 的 getBean 方法.

工厂方法模式

1
2
3
4
5
6
原理:
定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类。
此模式解决了简单工厂每增加一个子类需要修改工厂类的问题.
此模式存在问题, 若新增一个子类, 需同时新增一个子类工厂, 系统复杂性更高.

示例: Calendar, NumberFormat

抽象工厂模式

1
2
3
4
5
6
原理:
提供一个接口,用于创建 相关的对象家族.
同上, 由子类工厂决定创建哪些对象.
此模式是工厂方法的升级版, 不同之处在于它同时创建多个种类的对象(工厂类具有多个方法).
此模式将一个对象家族的新建集合到一个工厂类创建管理, 这些对象家族相互之间一般有关联, 在创建时就可以处理这些关联. 且对于 2 个子类工厂, 一般可以无缝切换, 使得修改代码极为方便(即换一个子类工厂).
此模式在新增一个对象家族的成员时非常麻烦(即所有工厂类需要新增一个方法), 但再新增一类对象家族时比较简单(即新增一个子类工厂).

结构型

适配器模式

1
2
3
4
5
6
原理:
把一个接口转换成另一个用户需要的接口.
定义一个类, 实现用户需要的接口, 并聚合一个需要转换的接口对象, 在重写的方法(用户需要的方法)中调用聚合的对象的方法, 若需要返回值, 且返回值类型不一致, 则还需要在方法中处理一番, 然后返回. 这个过程叫做适配.这个类叫做适配器类.
使用此模式可对一些老旧接口适配兼容.

示例: java.util.Arrays#asList() 将数组适配成 List, Spring MVC的 HandlerAdapter

装饰者模式

1
2
3
4
5
6
原理:
将一个或多个功能(方法)动态的新增到一个类中.
把需要新增功能类称为 A,定义一个类B,实现A的上层接口, 并聚合一个A 的实例对象, B类实现的接口中, 对其他不关心的方法直接调用聚合的对象的方法. 对于关心的方法则可以在调用前后进行加料处理(如一个方法返回一个数, 可以在原来的返回值上乘以 2), 同时, B类也可以新增一些其他方法, 这些方法就是多出的功能. B类就是装饰者类, A就是被装饰类.
此模式的优点是, 装饰类也可以当做被装饰类, 然后再来一层装饰, 可以无限的装饰.

示例: java IO 流

代理模式

1
2
3
4
5
6
7
8
9
10
11
原理:
控制其他对象的访问(方法级), 将一些前置或后置的处理, 通过代理对象注入到目标对象的方法前后. 面向切面编程.

类型:
静态代理: 定义一个代理类实现目标对象的上层接口, 并聚合一个目标对象, 重写方法时将前置后置处理加上.
动态代理:
JDK 动态代理: 需要目标对象有上层接口(自然接口内的方法才可以代理)
使用java.lang.reflect.Proxy#getProxyClass
CGLIB 动态代理: 是个类就行. 实现原理是 ASM 框架动态生成目标对象类的子类字节码, 然后通过反射生成代理对象.

示例: Spring AOP

桥接模式

1
2
3
4
5
原理:
将抽象与实现分离开来,使它们可以独立变化。
桥接的含义是, 一个桥, 放在哪里都有桥的 2 边, 桥的 2 边可以变化, 但桥始终不变. 此处, 桥代表一个操作(如手机上运行软件), 2 边代表 一个操作的 2 个维度(如手机和软件). 同时, 桥接后的操作也可以视为一个维度, 与另一个维度桥接(如手机上运行软件和人这 2 个维度, 可以进行桥接, 组成 3 维度嵌套桥接).

示例: JDBC 获取连接, 获取连接是一个维度, 数据库是一个维度, 数据库有多个, 所以这是一个数据库维度变化, 另一维度不变的桥接模式.

享元模式

1
2
3
4
5
原理:
利用共享的方式来支持大量细粒度的对象,这些对象一部分内部状态是相同的。
如常见的 线程池, 常量池等, 使得对象的获取速度加快.

示例: java.lang.Integer#valueOf() java.lang.Boolean#valueOf()

组合模式

1
2
3
4
5
6
7
8
9
10
原理:
将对象组合成树形结构来表示“整体/部分”层次关系,允许用户以相同的方式处理单独对象和组合对象。
一般需要部分和整体具有一定的相似度, 才能对其进行抽象.
对部分/整体进行抽象, 得出一个公共抽象类或接口, 再实现类中根据具体角色做不同处理.

示例:
javax.swing.JComponent#add(Component)
java.util.Map#putAll(Map)
java.util.List#addAll(Collection)
java.util.Set#addAll(Collection)

外观模式

1
2
原理:
提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用.

行为型

职责链(责任链)模式

1
2
3
4
5
6
原理:
使多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链发送该请求,直到有一个对象处理它为止, 从而避免请求的发送者和接收者之间的耦合关系。

示例:
javax.servlet.Filter#doFilter()
netty 的 Handler Chain

观察者模式

1
2
3
4
5
6
7
原理:
定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。
主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。

示例:
swing 的事件监听(按钮事件, 鼠标事件)
JS 的 事件监听

状态模式

1
2
3
原理:
允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。
状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为.

策略模式

1
2
3
4
5
6
原理:
定义一系列算法,封装每个算法,并使它们可以互换。
策略模式可以让算法独立于使用它的客户端。
策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法.

示例: java.util.Comparator#compare() javax.servlet.http.HttpServlet

模板方法模式

1
2
3
4
5
原理:
定义算法框架,并将一些步骤的实现延迟到子类。
通过模板方法,子类可以重新定义算法的某些步骤,而不用改变算法的结构。

示例: java.util.Collections#sort()

命令模式

1
2
3
4
5
原理:
将一个对象(命令接收者)的每个操作拆分到每一个命令类中, 再使用一个命令管理类来管理这些命令. 使得命令可以放入队列中有序执行, 且可以统一记录命令的操作日志, 还可以支持撤销操作(每个命令都实现对应的撤销即可).
此模式的好处是, 若将命令抽象为几个标准的命令(如开,关), 然后管理多个命令接收者(如灯,电视机,空调)的操作, 可使新增命令接收者变得简单, 即扩展性好.

又称万能遥控器.

中介模式

1
2
3
4
5
原理:
集中相关对象之间复杂的沟通和控制方式。降低子系统之间的耦合.
类似一个消息收发中心, 负责字系统的消息中转, 使得子系统之间可以进行一定的交互.

示例: 线程池管理者线程和要执行的任务.

备忘录模式

1
2
3
原理:
在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。
如对游戏的当前状态进行一个保存, 然后在后续游戏中死亡后可以读取这个状态重新开始.

访问者模式

1
2
3
原理: 
为一个对象结构(比如组合结构)增加新能力。
使用访问者模式可实现重载的动态绑定(即伪双分派), 效果与重载方法内使用 instanceof 是一样的, 但使用访问者模式, 可扩展性更好.

迭代器模式

1
2
3
4
原理:
提供一种顺序访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。

示例: java.util.Iterator

解释器模式

1
2
3
4
原理:
为语言创建解释器,通常由语言的语法和语法分析来定义。

示例: EL 表达式, Freemaker模板

空对象模式

1
2
原理:
使用什么都不做的空对象来代替 NULL, 避免空对象判断, 避免空指针异常.

高并发

Redis 缓存

缓存穿透(攻击型)

1
2
3
4
5
6
含义:
对于一个不存在的 key 进行访问, 会导致数据库不停地查询这个 key 进行缓存.

解决方案:
1.使用布隆过滤器, 一定不存在的数据会被过滤.
2.查询后缓存一个空结果, 但很快超时.(有缺点, 但简单)

缓存雪崩

1
2
3
4
5
6
含义:
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案:
1.设置超时时, 在原有的失效时间基础上增加一个随机值,比如1-5分钟随机
2.加锁或者队列的方式保证缓存的单线 程(进程), 避免大量请求打到数据库.

缓存击穿

1
2
3
4
5
6
7
含义:
单一热点 key 失效时导致大量请求打到数据库。

解决方案:
1.分布式互斥锁(redis,zookper)
2.添加超时字段记录超时(比实际超时小一些), 每次获取数据根据字段判断是否超时, 若是, 则马上延长超时字段, 然后加载数据库重新缓存
3.redis不设置过期, 通过添加超时字段判断, 超时则代码异步跑一个重新缓存的任务(这里代码需要先本地加锁, 再加分布式锁).

RabbitMQ

解耦, 异步, 削峰

解耦

1
2
3
定义:
为面向服务的架构(SOA)提供基本的最终一致性实现. 即将 2 个系统的交互通过消息队列中转, 以防止某个系统临时挂了导致调用失败.
示例: 下单系统调用库存系统, 若当时库存系统正好挂了, 则导致下单失败. 此时将请求放到消息队列中, 库存系统读取消息进行处理, 若当时库存挂了也没关系, 处理失败也没关系(可重试, 且重试代码比较简单).

异步

1
2
3
4
定义:
对于某些不要求返回值的耗时操作, 可异步处理.
示例:
用户下单后, 需发送多个下单提醒(微信通知, 短信通知, 邮件通知), 每个操作都比较耗时, 可考虑将其放入消息队列后直接返回, 由另一段代码负责读取消息发送通知.

削峰

1
2
3
4
定义:
将请求高峰打平, 使得系统可以处理过来.
示例:
某次秒杀 1 分钟过来 1 万请求, 而系统一分钟大概只能处理 1千 请求, 系统要处理完这些请求理论需要 10 分钟, 但如果不做处理, 请求瞬间打过来, 系统直接卡死, 卡住时候一分钟可能只能处理 100 请求. 此时需要将所有请求都打到队列里面, 系统再慢慢从队列中读取处理.

文档生成插件更多细节分享

更多细节

code 含义

先看效果

image-20210710205744491

实现方式

在原来的 UserController 的基础上,再加一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


/**
* 新增一个用户
*
* @param userEntity 用户信息
* @return 操作是否成功
* #code 1 用户名已存在 建议换个用户名再试
* #code 2 手机号已存在 同上
* #hiddenRequest userId,lastLoginTime
* #hiddenResponse totalCount
*/
@PostMapping("/addUser")
public BaseResponse<Boolean> addUser(@RequestBody UserEntity userEntity) {
// todo 新增用户
log.info("addUser--> " + JsonUtils.getJsonString(userEntity));

return BaseResponse.success(true);
}

其中 ``#code开头的注释便是关键,这应该是一目了然了吧,先#code` 开头,再空格分隔 code、含义、出现原因三个信息

根据不同方法隐藏不同字段(参数、返回值均可)

先看效果

先写两个方法,使用 #hiddenRequest、#hiddenResponse 设置要隐藏的字段,这里注意方法参数变量名可以省略(即 userEntity

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

/**
* 新增一个用户
*
* @param userEntity 用户信息
* @return 操作是否成功
* #code 1 用户名已存在 建议换个用户名再试
* #code 2 手机号已存在 同上
* #hiddenRequest userId,lastLoginTime
* #hiddenResponse totalCount
*/
@PostMapping("/addUser")
public BaseResponse<Boolean> addUser(@RequestBody UserEntity userEntity) {
// todo 新增用户
log.info("addUser--> " + JsonUtils.getJsonString(userEntity));

return BaseResponse.success(true);
}

/**
* 修改用户信息
*
* @param userEntity 用户信息
* @return 操作是否成功
* #hiddenRequest lastLoginTime,userGender,userAge
* #hiddenResponse totalCount
*/
@PostMapping("/updateUser")
public BaseResponse<Boolean> updateUser(@RequestBody UserEntity userEntity) {
// todo 修改用户信息
log.info("updateUser--> " + JsonUtils.getJsonString(userEntity));

return BaseResponse.success(true);
}

生成文档如图:

  • 新增用户接口
image-20210710210651803
  • 修改用户接口

image-20210710210846356

这里没展示返回字段,但效果是一样的,也是示例和说明表格都会隐藏

实现方式

如我刚提的,使用 #hiddenRequest、#hiddenResponse 设置要隐藏的字段接口,这个设置仅对文档生成,导出到Postman起作用,不用担心影响代码运行。

这里需要注意的点是,多个字段之间使用英文逗号分隔。另外方法参数的变量名不需要带上,比如 userId ,直接写 userId 即可,不需要写 userEntity.userId,因为 Spring MVC 也是这么识别的,相应的我生成Postman时就需要这么处理,最终干脆保持统一,省的混乱!

还有一点就是,如果是纯方法的参数,是不支持设置隐藏的,原因如下:

1.首先,如果不需要用到此参数,可直接删除,而不像对象,可能多个地方用到,所以需要根据方法定制的去隐藏部分字段

2.其次,其实我还提供了另一种方式隐藏,甚至可以说,这个隐藏更早的被支持。那就是:在 @param 注释后面 加上 hidden即可,如图:

image-20210710212211293

字段的更多注释

既然刚才说到了 hidden,那么就此引出字段级别的注释、注解,是的,我们还支持Swagger 注解,实际上注释也是根据Swagger注解定义出来的。

字段级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/**
* 测试字段上的注释有哪些 tag
* 字段注释说明是可以换行的
* 当然有个前提就是
* 最终负责渲染Markdown的引擎需要支持 <br> 标签,也就是支持部分 HTML
* #required
* #hidden false
* #important
* #example 测试示例值 此处可带空格,方法上的 @param 后面不行
* #notes 对应其他参考信息 同理,可带空格,方法上的 @param 后面不行
* #notes 谁说我不能换行的,只是没那么优雅罢了
* #notes 不过也还凑合,换行
*/
@ApiModelProperty(value = "这里是Swagger的字段说明\n试试换行", required = true, hidden = false,
example = "测试示例值", notes = "对应其他参考信息\n试试换行")
private String testField;

生成的示例如下:

1
2
3
4
5
6
7
8
9
userId:0
nickName:用户昵称
realName:用户姓名
phoneNumber:110
userGender:2
userAge:1024
userAvatar:https://mp.weixin.com/p/jewagheiajifejgihewjg.ifew
lastLoginTime:最近一次登录时间
testField:测试示例值 此处可带空格,方法上的 @param 后面不行

生成的字段说明表格如下:

image-20210710221547548

使用的 tag 用法说明如下

  • 啥都不加直接写,支持换行,对应字段说明表格的含义

  • required 对应字段说明表格中的必填性,不写默认为 false(否),加上后代表必填

  • hidden 加上即隐藏此字段,后面跟个 false 和不写效果一样(这个道理放到任何Boolean型tag都生效)

  • important 加上代表注释比注解优先级高(默认注解优先级高,即写了注解优先使用注解中的信息)

  • example 示例值,生成示例值和导出到Postman时有大用处,懂得都懂

  • notes 对应字段说明表上的其他信息参考,若需要换行,请反复添加 notes

而其注解,则完全与 tag 同名,且默认注解优先级高,即写了注解优先使用注解中的信息!

还是推荐使用注释,毕竟不依赖于 Swagger!

这里顺带提下我对Swagger的看法,总得来说就是,对前端不够友好,其次就是依赖于代码的运行,一旦程序起不起来,就完蛋了,而插件就不一样了,哪怕是你有部分语法错误,只要不撞到参数的解析上,就没有关系,基本是确保方法声明不报错就OK了!而且运行一个 web 程序,始终是需要时间的,而我这插件的运行,只能说是比飞快还飞快;这里要感谢IDEA提供的Api,是IDEA提前解析好这些信息,我直接拿来用的,所以很快!

方法级别注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* 查询用户信息(分页)
* 试试换行
* #notes 方法的更多信息需要换行展示,毕竟内容很多啊
* #notes 这样看起来如何!
*
* @param listUserPage 过滤条件
* @param userEntities 没用的参数 hidden
* @return 用户列表数据-分页
* #hiddenRequest userEntities
* #hiddenResponse data.userAvatar
*/
@PostMapping("/listUser")
public BaseResponse<List<ListUserResponse>> listUserPage(ListUserRequest listUserPage, List<UserEntity> userEntities) {
// todo 查询分页数据
log.info("listUserPage--> " + JsonUtils.getJsonString(listUserPage));

return BaseResponse.success(new ArrayList<>(), 0L);
}

文档效果图:

image-20210710222036880

  • 啥都不加直接写,支持换行,对应接口名标题(不建议换行,不太好看!!)

  • code 不再赘述

  • hiddenRequest 不再赘述

  • hiddenResponse 不再赘述

  • hidden 隐藏此方法,则怎么右键都不会生成文档!批量生成时用于隐藏单个,非常好使!

  • important 代表即使使用了 @ApiOperation 注解也优先读取注释

  • notes 写上,再看看文档的第二行,你就明白了!用于给方法添加更多说明,若需要换行,请反复添加 notes!

1
2
3
4
5
6
7
8
9
// 另外注释有几个对应上面的
@ApiOperation(value = "类似啥都不写,使用\n换行", notes = "对应notes", hidden = true)
// 对应 code,可使用 @ApiResponses 添加多个 code
@ApiResponses({
@ApiResponse(code = 1, message = "出错了"),
@ApiResponse(code = 2, message = "出错了2"),
})

// 另外 hiddenRequest/hiddenResponse 不支持注解配置,必须使用注释咯,放心可以上面几个信息使用注解,然后搭配注释中的 hiddenRequest/hiddenResponse,这是OK的!

@param 单独说说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/**
* 删除一个用户
*
* @param userId 用户id example=10000 required notes=更多信息&br;试试换行
* @param operator 操作人 example=admin required
* @param deleteFlag 是否彻底删除 hidden
* @return 操作是否成功
* #hiddenResponse totalCount
*/
@GetMapping("/deleteUser")
@Order(1)
public BaseResponse<Boolean> deleteUser(Integer userId, String operator, Boolean deleteFlag) {
// todo 删除一个用户
if (deleteFlag != null && deleteFlag) {
//todo 彻底删除数据
} else {
// todo 逻辑删除, 修改 deleted 字段值
}

return BaseResponse.success(true);
}

文档效果图:

image-20210710222919511

首先 @param userId 用户id@param 后面紧跟的字段名和字段说明,这个是 javadoc 就这么写,后面的内容,则支持几个 tag,如下

  • hidden 隐藏此参数
  • example 设置示例值
  • notes 设置更多信息

这里必须提一下,notes说明,example都不能有空格,否则影响插件解析注释。不过有个方式可以换行,即添加 &br;,这里不能直接加 <br>,因为插件的代码会转义所有的 < 和 > 符号。

对应注解:

1
2
// 除了 notes,其他都对的上
@ApiParam(value = "1", required = true, hidden = true, example = "2")

最后提一嘴 @return ,这个 javadoc,idea 敲 /** 回车自动生成的 tag,我也有解析,但一般情况还用不到,只有返回值是普通类型(如 Integer,String,Double,Date)时才会有效,这个大家试试就知道了!另外 @return 不支持 @param 哪些附加 tag(如 hidden,example,notes)!

类级别注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 用户接口
* @description 用户接口
* @author wq
* #tags 用户接口
* #hidden
* #important
*/
@Api(description = "用户接口", tags = "用户接口", hidden = true)
@Slf4j
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
}

效果:批量生成时的文件名会从注释、注解中取

  • 什么都不写,类的说明
  • description 和什么都不写效果一样
  • tags 参考 Swagger 的注解含义,代表对接口的分类,用来对应 @Api 注解的 tags
  • hidden 批量生成时过滤此类
  • important 提高注释的优先级(默认 @Api 注解优先级高)

注解作用类似!

上面列的 description,tags,以及啥都不写,本质都用于批量生成接口时作为Markdown文件的文件名或导出到Postman时作为请求的上级文件夹名称,优先级最高的是 tags,这是考虑到使用了 Swagger 的项目,一般这个地方都会填写中文含义。其次是

description 和 啥都不写,看谁写在前面,则谁优先!这个是考虑到好多人使用 javadoc 写类注释的模板都是带有这个 @description 用于描述改类的。

啥都不写则是我个人认为比较好的方式!

效果如图:

image-20210710225904864

总结

以上所有我使用 # 开头的 tag,本质上都可替换成 @(除了 @param、@return),比如

#hidden 和 @hidden 效果是一样的!

其实 @ 开头才是比较正宗的注释的 tag 写法,但太正宗了也不行,IDEA识别了,但只识别了一半,只认识部分tag,这就是使得我们写的这些 tag 都要报个黄色警告(如果你打开了Editor => Inspections);所以干脆加了个 # 开头,避开idea识别,合适的不得了!

其实都支持,怎么用就看大家了,我个人偏爱 # 开头!!!

本来打算分享部分核心代码的,太晚了,算了,开源吧,但不保证开源的是最新版!!

项目地址:Github:open-doc-generator-idea-plugin

文档生成插件使用指南

根据 Spring MVC Controller 下方法生成接口文档

先看示例代码

UserController

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
package cn.gudqs.business.docer.controller;

import cn.gudqs.business.docer.dto.request.ListUserRequest;
import cn.gudqs.business.docer.dto.response.BaseResponse;
import cn.gudqs.business.docer.dto.response.ListUserResponse;
import cn.gudqs.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
* 用户接口
* @author wq
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/user")
public class UserController {

/**
* 查询用户信息(分页)
* @param listUserPage 过滤条件
* @return 用户列表数据-分页
*/
@PostMapping("/listUser")
public BaseResponse<List<ListUserResponse>> listUserPage(ListUserRequest listUserPage) {
// todo 查询分页数据
log.info("listUserPage--> "+ JsonUtils.getJsonString(listUserPage));

return BaseResponse.success(new ArrayList<>(), 0L);
}

}

BaseResponse

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
package cn.gudqs.business.docer.dto.response;

import lombok.Data;

/**
* @author wq
*/
@Data
public class BaseResponse<T> {

/**
* 状态码
* 0: 代表成功
* -1: 代表未知异常
* >0: 代表已知异常
*/
private Integer code;

/**
* 错误信息
*/
private String message;

/**
* 是否成功
* true: 代表成功, 此时 code = 0
* false: 代表失败, 此时 code != 0
*/
private Boolean success;

/**
* 返回数据
*/
private T data;

/**
* 分页-总条数
*/
private Long totalCount;

public static <T> BaseResponse<T> success() {
return success(null);
}

public static <T> BaseResponse<T> success(T data) {
return success(data, 0L);
}

/**
* 返回一个成功的 BaseResponse
* @param data 携带的数据
* @param totalCount 分页-总条数
* @return BaseResponse
*/
public static <T> BaseResponse<T> success(T data, Long totalCount) {
BaseResponse<T> baseResponse = new BaseResponse<>();
baseResponse.setSuccess(true);
baseResponse.setCode(0);
baseResponse.setMessage("success");
baseResponse.setData(data);
baseResponse.setTotalCount(totalCount);
return baseResponse;
}

}

ListUserResponse

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
package cn.gudqs.business.docer.dto.response;

import lombok.Data;

import java.util.Date;

/**
* 用户信息
*
* @author wq
*/
@Data
public class ListUserResponse {

/**
* 用户昵称
*/
private String nickName;

/**
* 用户姓名
*/
private String realName;

/**
* 用户手机号
* #example 110
*/
private String phoneNumber;

/**
* 用户性别
* 0: 保密
* 1: 男
* 2: 女
* #example 2
*/
private Integer userGender;

/**
* 用户年龄
* #example 1024
*/
private Integer userAge;

/**
* 用户头像地址
* #example https://mp.weixin.com/p/jewagheiajifejgihewjg.ifew
*/
private String userAvatar;

/**
* 最近一次登录时间
*/
private Date lastLoginTime;

}

ListUserRequest

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
package cn.gudqs.business.docer.dto.request;

import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.Date;

/**
* 查询用户-过滤条件
*
* @author wq
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ListUserRequest extends BasePageRequest {

/**
* 模糊搜索词
* 支持用户昵称, 用户姓名, 用户手机号
*/
private String searchKeyword;

/**
* 用户性别
* 0: 保密
* 1: 男
* 2: 女
* #example 2
*/
private Integer gender;

/**
* 过滤年龄范围-起始
*/
private Integer ageStart;

/**
* 过滤年龄范围-结束
*/
private Integer ageEnd;

/**
* 过滤登录时间范围-开始
*/
private Date loginTimeStart;

/**
* 过滤登录时间范围-结束
*/
private Date loginTimeEnd;

}

BasePageRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.gudqs.business.docer.dto.request;

import lombok.Data;

/**
* @author wq
*/
@Data
public class BasePageRequest {

/**
* 分页-当前页码
* #example 1
*/
private Integer pageNumber;

/**
* 分页-分页大小
* #example 20
*/
private Integer pageSize;

}

说明

以上都是大家常写的分页代码,此时我在 listUserPage 这个方法上右键,弹出菜单中选择:生成Api接口文档(restful),如图

image-20210710095313814

然后就会得到一个弹框,当然如果你根据提示点了 OK,则直到下次重启,都不会再出现

image-20210710100138879

弹框中内容如下,此内容区域可滚动预览,全选复制粘贴

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
# 查询用户信息(分页)
## 请求信息

### 请求地址
'''
http://192.168.0.104:8080/api/v1/user/listUser
'''

### 请求方法
'''
POST
'''

### 请求体类型
'''
application/x-www-form-urlencoded
'''

## 入参
### 入参示例(Postman==> Bulk Edit)
'''json
pageNumber:1
pageSize:20
searchKeyword:模糊搜索词<br>支持用户昵称, 用户姓名, 用户手机号
gender:2
ageStart:0
ageEnd:0
loginTimeStart:过滤登录时间范围-开始
loginTimeEnd:过滤登录时间范围-结束
'''

### 入参字段说明
| **字段** | **类型** | **必填性** | **含义** | **其他信息参考** |
| -------- | -------- | -------- | -------- | -------- |
| pageNumber | **Integer** | 否 | 分页-当前页码 | |
| pageSize | **Integer** | 否 | 分页-分页大小 | |
| searchKeyword | **String** | 否 | 模糊搜索词<br>支持用户昵称, 用户姓名, 用户手机号 | |
| gender | **Integer** | 否 | 用户性别<br>0: 保密<br>1: 男<br>2: 女 | |
| ageStart | **Integer** | 否 | 过滤年龄范围-起始 | |
| ageEnd | **Integer** | 否 | 过滤年龄范围-结束 | |
| loginTimeStart | **Date** | 否 | 过滤登录时间范围-开始 | |
| loginTimeEnd | **Date** | 否 | 过滤登录时间范围-结束 | |






## 出参
### 出参示例
'''json
{
"code": 0,
"message": "错误信息",
"success": false,
"data": [
{
"nickName": "用户昵称",
"realName": "用户姓名",
"phoneNumber": "110",
"userGender": 2,
"userAge": 1024,
"userAvatar": "https://mp.weixin.com/p/jewagheiajifejgihewjg.ifew",
"lastLoginTime": "最近一次登录时间"
}
],
"totalCount": 0
}
'''

### 返回字段说明
| **字段** | **类型** | **含义** | **其他信息参考** |
| -------- | -------- | -------- | -------- |
| code | **Integer** | 否 | 状态码<br>0: 代表成功<br>-1: 代表未知异常<br>\>0: 代表已知异常 | |
| message | **String** | 否 | 错误信息 | |
| success | **Boolean** | 否 | 是否成功<br>true: 代表成功, 此时 code = 0<br>false: 代表失败, 此时 code != 0 | |
| data | **List\<ListUserResponse\>** | 否 | 返回数据 | |
|└─ nickName | **String** | 否 | 用户昵称 | |
|└─ realName | **String** | 否 | 用户姓名 | |
|└─ phoneNumber | **String** | 否 | 用户手机号 | |
|└─ userGender | **Integer** | 否 | 用户性别<br>0: 保密<br>1: 男<br>2: 女 | |
|└─ userAge | **Integer** | 否 | 用户年龄 | |
|└─ userAvatar | **String** | 否 | 用户头像地址 | |
|└─ lastLoginTime | **Date** | 否 | 最近一次登录时间 | |
| totalCount | **Long** | 否 | 分页-总条数 | |

由于文章本身就是Markdown格式,所以上面中 ``` 我用 ''' 代替,我用截图来展示下效果:截图使用 Chrome 插件打开 HTML 滚动截屏得到,HTML 是由 typora 生成的!

screencapture-file-Users-wq-File-TempFile-html-2021-07-10-10_09_50

总结

如果有多个方法,想一次性生成,则可在 UserController 上右键,点击生成Api文档(restful) ,如图:

image-20210710101618777

此时生成的文档,就是多个方法单独生成的文档的简单的拼接。

根据 Spring MVC Controller 的接口信息导出到 Postman

项目视图选择类、包,点右键

还是沿用上面的代码,但是考虑到单个方法单独导出到 Postman 非常费时费力,可用性不高,因此方法级别的右键导出,并没有实现!但是单个文件级别,还是OK的,只需在文件导航视图找到 UserController 类,右键,点击 导出Api到Postman即可!如图

image-20210710102303483

另外这个右键,其实不管是类,包,还是文件夹,乃至根目录,都可以点击导出到 Postman,只是导出的范围不同!

由于范围比较大,因此过滤了非 @Controller、@RestController 注解的类,可放心使用。

点击导出后,会将 Postman 的json 生成到项目下的 api-doc/postman 文件夹,如图

image-20210710102846460

打开Postman点击 File –> Import

此时我们打开 Postman,使用快捷键 Ctrl + OCommand + O , 接着拖拽文件过去,或点击 UploadFiles,弹框选择文件打开,如图

image-20210710103242732

image-20210710103321866

最终得到一个 Collection

如图:不管是请求,返回示例,文档,或者说 url 变量,body和参数处理,都帮你搞定了,你只需要修改下参数的实际值(如果使用 #example,则这步也可以省略),然后点蓝色的 Send 按钮就 ok 了(千万别忘记启动项目)!

这里简单说下,url 的组成

1.http:// 这个本地开发一般不用 https 对吧,嘿嘿

2.ip: 这个使用代码遍历网卡,过滤掉 ipv6,127.0.0.1 后得出的局域网 ip,如果没搞虚拟机啥的,一般获取比较准确,要还是错了,点击 Collection 名称,在 Variables 下修改 NaN 的 Current Value 就好了

3.port: 这个默认8080,然后支持 Spring Boot 项目,即读取 application.yml/application.properties 文件获取 server.port 的值,嗯~~ 若存在 application-xxx 这种情况,还真不好取,只能读取 spring.profiles.active 但是也未必准确,毕竟有很多地方支持设置。理论性idea的东西都可以获取,但idea的插件开发文档找不到这方面的信息,大概是不会花精力搞了,毕竟错了直接改变量就好了,改一处,所有请求生效。

4.剩下的部分就是 Controller 的 @RequestMapping 注解 + 方法的 @RequestMapping(或@GetMapping之类)注解的值拼凑起来。若方法为 GET,则参数会以?xxx=xxx&xx=xx 形式拼接上去,也可在 Params 下查看

image-20210710104427563

总结

总得来说,这个插件还是太简单了,其实不用使用指南,知道在哪右键起什么作用,就OK了,因为处了右键点击,其他一概不用你管(包括复制)!可谓是偷懒极了。。。。。

说下这个插件的发展史。

一开始是用于微服务的接口方法,根据参数和返回值来生成文档给其他开发看,当时还是自己写了个 Util,然后传 Class 作为参数来解析得到参数、返回值等信息,最终生成文档,当时的所有中文备注,都是通过 Swagger 的注解来实现的,没办法,运行时注释早干掉了,得不到。

后来觉得这东西可以抽出来,不要被项目束缚,先是想到了idea插件,但是发现 Api 及其不同,idea中也无法获取 Class,也就是当时写的代码无法复用,于是又考虑 maven 插件,发现这个是可以取到 Class,进行复用的。

写完后,又发现使用这个插件,必须设置参数,也就是你要生成那个类的文档,很麻烦啊这,于是想到之前看idea插件的一些api可以获取到这些信息,干脆来了个结合,idea右键获取Class 的全限定名,传给maven插件,maven插件来生成,最后idea弹框展示。其实这样也不错,但是又有个巨大的问题,就是maven插件其实依赖于 target 文件中的信息,这部分必须 compile 后才有,使得每次运行插件都要先跑compile 再生成,本来maven运行速度就慢,这样就更慢了。加上进度条后还是感觉体验极差!!!

最终,在某个夜黑风高的晚上,我死活睡不着觉,干脆打开电脑,点击备忘录,记下明天摸鱼时间要写个纯idea插件,然后躺下数羊!啊那是不可能的。

不过这插件最终确实都是在闲的没活干的摸鱼时间写的,最终把原来代码拷贝一份到idea插件项目中,一边参照一边使用idea提供的Api修改代码,最终得到了纯idea插件版本!

之后遇到了比较坑的点就是泛型的解析和参数的解析,目前都解决了,可以说是随便怎么写,都能识别了。比如最上面的示例代码的 T data,这还是小 case,如果改成 List<T> 呢? 如果再来一个带泛型的类 PageInfo<T> ,而我加个字段 PageInfo<T> pageData;来把 T 传给 PageInfo 呢,解析会有问题吗?刚开始会,后来解决了!!!只能说我自己都快绕晕了,但最终还是解决了。这其实值得复盘的,不过我这人比较懒,就不搞了。其实这种情况繁多的时候,要写代码不能只考虑部分情况,或是来一种情况加一种情况,必须找到通用的方法,当然未必什么时候都有通用的方法,不过还是要去追求的!!

后续还会和大家分享一些我插件中的核心代码,包括idea插件提供的api,自己百度找的工具类等等。

另外我上面说的微服务那个接口文档,其实就是不带(restful)后缀的菜单选项,其生产的文档不含有HTTP协议相关的信息,而是纯粹的入参、出参、code含义等。另外文件视图右键有三个选项,另外另个就是批量生成 Markdown文件,毕竟一个一个复制粘贴很累的,而很多笔记软件都支持批量导入Markdown文件,这样就不就简单了。

简单说 Spring 原理

我先来,千万别较真,千万别较真,千万别较真!

先看官网介绍哈

The Spring Framework provides a comprehensive programming and configuration model for modern Java-based enterprise applications - on any kind of deployment platform.

A key element of Spring is infrastructural support at the application level: Spring focuses on the “plumbing” of enterprise applications so that teams can focus on application-level business logic, without unnecessary ties to specific deployment environments.

我来翻译一下,就是说(等等,我软件呢?谁动了啊!)

额,就是说:

Spring框架为任何类型的部署平台上的基于Java的现代企业应用程序提供了全面的编程和配置模型。

Spring的一个关键元素是在应用程序级别的基础架构支持:Spring专注于企业应用程序的“管道”,以便团队可以专注于应用程序级别的业务逻辑,而不必与特定的部署环境建立不必要的联系。

简单理解呢就是让你关注业务而非琐碎的、无关紧要的其他东西。好的,那么 Spring 是怎么做到的呢?其实也不难,先这样,再这样,然后再那样,最后再这样(啊,放错了)

其实也不难,主要是它有两大法宝,唉,知道的同学肯定要说了啊;我知道,IOC、AOP嘛!没错就是这两玩意,哪怕是还没内卷那会,我也听腻了,今天我们就来唠叨唠叨这两玩意它的一些原理或者说特点。

IOC

咱先说这 IOC,英文那就是 Inversion of Control,什么意思呢?控制反转,不让你控制我了,我要自己控制自己,这就是控制反转,我说那还得了,这程序它不听话了都!唉,不是这样理解的,其实啊,这代表这程序,它成熟了,它呀,可以自己干自己该干的事情了,不需要你管它了;这样子有什么好处呢?有啊,就像你家的熊孩子,还小的时候,特别黏人,不能没人带,你呢,总是需要找人帮忙看看,自己才有空闲出去玩,后来呢,它长大了,它就会自己出去玩了,熊是熊了点,但你也空闲不少啊,有时间可以看看书,喝喝茶,多好啊!咱说回来啊,这控制反转呢,就是让你少花费心思在这些地方,这样不就可以像官网说的 “可以专注于应用程序级别的业务逻辑”了吗。

那前面呢,其实只是IOC的一个思想,它不是一个实现,毕竟啊,你不能指望它真的能自己控制自己。所以呢,我们偷偷引入一个东西,也就是容器,由这个容器来管理它。这个啊,就像你把你家小孩送幼儿园,你交钱,剩下交给幼儿园。那么说到这里啊,大家应该也明白了,它呢,并不能自己控制自己,还是得靠容器;那么这个容器,又是何方神圣呢?

其实这个容器呢,也不难,名副其实,就是一个装东西的东西,有人说你套娃呢?我没有,不过这容器确实不禁止你往容器放容器,比如父子容器,就像子容器塞进了父容器里面,有时候呢,就会穿过子容器,从父容器取东西。好了咱说回来,容器就是一存东西的,这东西很常见啊,Java 集合框架啊,大学学过,还学了好几个,我现在还记得,什么 List,Map,Set 啥的。对!没错,真让你蒙对了!原理确实是这个,这个东西呢,选来选取,我们就选 Map 吧。众所周知,Map 是数组+链表为底层实现的键值对容器啊,因为有这个键值对的存在呢,我们就可以非常准确无误的存放和读取,就像银行账户吧,你往你账户存钱,再从你账户就能取到之前存的钱,非常有效的避免了“乱拿乱放”问题。好的,那我们就说完了容器的第一个特点,就是能存能取,这时候你就会说,这不废话吗?啊确实!行,我们整点正常的特点,那其实接下来要说的特点呢,都是围绕着取这个动作的。

其实上面的特点呢,说的不是特别清晰,这存的是什么?取的又是什么?总不可能和银行一样,都是钱吧!当然不是,这存的呢,一般是 Class。什么是 Class? Class 就是一设计图,照着这个设计图呢,容器 Duang 的一下,就给了一个对象!你说还有这好事?写个代码还有对象领?不是的,这个对象呢,不是那个对象哈,这个呢,是 Java 实例化出来的对象,用来调用方法,获取字段的。那么也就是说,这个容器,它还是个工厂?它还能生产对象呢!没错,但严格说对象其实也不是它生产的,对象呢,其实是 JVM 生产的对不对?它呢,就负责加工,所以说,其实也就是个加工工厂。所以咱这第二个特点呢,就是工厂,能照着设计图加工一个对象出来。你肯定要说啦,这有啥用,这不产生中间商差价了,我直接找 JVM 爸爸行不行?别急,可以是可以,但咱用这个东西,就一点有它的用处,对不对?这不,第三个特点就来了!

那么聪明的小伙伴呢,肯定已经知道我要讲什么了?对,没错!就是依赖注入,前面说了 Class 是一个设计图,但没说清楚,这个设计图呢,它只是一个小零件的设计图,但和真正的零件设计图还是有一些不一样的,比方说啊,它会告诉你它与另一个零件的关系,有了这个关系呢,工厂的作用就有了,工厂生产到一半,发现这个零件要加工好啊,还需要另一个零件,那它又转头去生产另一个零件(是不是非常不科学,不应该先批量生产出来,之后再一起组装吗?),生产好了呢,又回过来按设计图把另一个零件加到这个零件上去。如果生产另一个零件的时候发现,也依赖另外某个零件,那也照旧,先生产依赖的。这时候你肯定会说了,套娃就算了,万一你依赖我,我依赖你,那不无限套娃了吗?这个问题呢?有办法解决,但是这文章太短了,我写不下!(请叫我费马 · 心累)

最后说下容器的另一个特点,其实都是层层递进的,你甚至可以认为,只有一个特点,都行,看你怎么看!

这个特点呢就是插件 DIY!之前咱也说了,工厂呢,不搞生产,光做加工了,那这个加工过程啊,除了依赖注入啊,一些属性配置注入这样的通过配置的方式去影响对象,能不能开放一个接口,我想怎么 DIY,自己写代码来实现呢?当时是可以的啊!这样的接口呢,Spring 早就准备好了,您呢,只需要实现这个接口,写点代码,再把它放到容器里面去,工厂呢,就会在合适的时机执行你的代码。而且他这个接口呢,一共有好几个,每个接口呢,也好好几个方法,每个方法啊,都是代表了相应的时机。这时候你就要问了,这有什么用呢?别急,咋看看 Spring 怎么用的!(再多嘴一句啊,干预加工过程算什么,干预工厂本身才牛逼呢!我说的啥,懂得都懂!)

AOP

Spring 的 AOP 啊,终于说到了,其实呢,就是干预加工过程的一个产物!著名历史事件狸猫换太子大家都听过吧,AOP 呢,那是一模一样。著名的曹操挟天子以令诸侯都听过吧,AOP 呢,还是一模一样!

这两个呢,一个是说的 AOP 的实现原理,另一个说的,就是它的特点:把真正的对象当傀儡使!

咱先说说实现原理啊,其实也不难(请问这是第几遍?),工厂都被咱劫持了,加工的时候,得到成品了的时候,做一个壳子:代理对象,把真正的对象装进去,这样呢,有什么方法被调用,都是先到代理对象这里,代理对象呢,操纵着真正的对象,心情好了,就执行一下真正的对象对应的方法,得到劳动果实后呢,再返回出去,谁也不知道真正的对象。就好像光听着收音机,不知道的还以为声音都是收音机生产的,其实那就是个代理,真正创造声音的人,远在天涯呢!所以说啊,AOP 的特点是不是如我所言呢!

OK各位,这就是这期文章的全部内容啦,非常感谢你能看到这里,如果你觉得写的还不错的话,求赞,求收藏,求硬币(这啥玩意),求转发,最重要的是点个大大的关注!你的支持就是我做文章的最大动力!OK各位,我们下期再见!(动作指导:何同学)

JVM 底层探秘笔记

JVM 速记

什么是 JVM?

JVM 就是由编译器, 类加载器, 执行引擎, 运行时数据区组成. 其中数据区包含 堆,栈,本地方法栈, 方法区和程序计数器(PC 寄存器), 其中栈是由局部变量表, 操作数栈, 动态链接, 返回地址组成的.

你是怎么对 jvm 垃圾回收进行优化的?

根据服务器的配置, 调整青年代和老年代的内存大小及比例, 在回收频率和回收速度上做取舍, 使用 G1 垃圾回收期控制 STW 停顿时间, 提高吞吐量.

JVM 内存模型

每个线程有自己的内存区域, 多线程之间通信主要通过共享内存来实现.

  • 有序性: 在 CPU 执行指令时, 可能会对非 happens-before 指令进行重排, 优化执行效率. 在单线程情况, 往往不会产生问题, 但涉及多线程时, 可能导致 bug.
  • 可见性: 一个线程修改了一个共享变量, 另一个线程不会知道这个改变, 这就是不可见, 要确保可见性, 一般使用 volatile 关键词, 当然, 加锁也可以.
  • 原子性: 即对于某代码, 实际执行时会分为好几个原子指令, 确保原子性必须加锁 (如synchronized) 处理
1
2
3
4
5
happens-before:
读后写
写后写
锁后解锁
可传递性

多线程

线程是一个进程中的不同执行路径, 一个进程至少有一个主线程.

进程是一个程序的抽象, 一个程序运行后一般为一个进程.

线程状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.New (新建)
2.Runnable (就绪)
3.Running (运行中)
4.Blocked (阻塞)
5.WAITING (等待)
6.TIMED_WAITING (超时等待)
7.Dead (死亡)

[t:thread对象, obj: 同步块中的对象]
New: new Thread()
Runnable: t.start(), t.yield()
Running: after t.start() and cpu run it
Blocked: when enter synchronized block
WAITING: obj.wait(), t.join(), LockSupport.park()
TIMED_WAITING: Thread.sleep(x), obj.wait(x), t.join(x)
Dead: when t.run() is over

JVM 对象结构

  • 对象头: 存储对象运行时数据和类型指针
    • Mark Word: 运行时数据
      • 标志位为01-未锁定: 对象 hash 码, 对象分代年龄
      • 标志位为00-轻量级锁定: 指向锁记录的指针
      • 标志位为10-重量级锁定: 执行重量级锁的指针
      • 标志位为11-GC标记: 空, 不需要记录信息
      • 标志位为01-可偏向: 偏向线程ID、偏向时间戳、对象分代年龄
    • 类型指针: 指向对象的类的元数据的指针
    • [数组长度]: 若对象时数组,则会保存数组长度
  • 实例数据: 保存字段内容,包括父级
  • 对齐: 确保 8 字节,凑整

垃圾回收

标记算法

  1. 引用计数法: 通过计算对象的引用数来判断对象是否可回收. 缺点是无法处理循环引用的问题.
  2. 可达性分析算法: 从根对象(两个栈引用的对象、静态属性引用的对象、常量引用的对象)出发, 寻找所有引用的对象, 形成根对象的引用链, 判断对象是否与根对象的引用链相关联来决定对象是否可回收.

回收算法

  1. 标记-清除算法:标记后直接清除, 效率低, 产生内存碎片
  2. 标记-复制算法:需要两个大小相同的内存区域, 其中一块保持空闲, 先标记可用对象, 然后将可用对象复制到空闲区域, 再清空整块内存, 使其满足一块保持空闲, 如此循环即可.
  3. 标记-整理算法:标记后移动内存, 令内存使用的区域与未使用的区域均无碎片, 缺点是效率低, 有的是空间占用少.
  4. 分代算法:将堆分为新生代与老年代,根据自个区域的特定采用不同的回收算法. 新生代又分为 Eden 区(复制算法)、 Survivor 区(因使用复制算法, 分为 From、To 两块内存区域).

回收器

(前 3 个 Young GC 使用, 后面的 Full GC 使用)

  1. Serial :单线程,使用复制算法,整个过程一直 STW
  2. ParNew :多线程,使用复制算法,还是一直 STW
  3. Parallel Scavenge :多线程,使用复制算法,与 ParNew 不同之处在于其关注吞吐量,并可通过调节参数对吞吐量进行控制
  4. Serial Old:单线程,使用标记-整理算法,老年代使用
  5. Parallel Old :多线程,使用标记-整理算法,与Parallel Scavenge一样,关注吞吐量。
  6. CMS :多线程,使用标记-清除算法,其步骤为:初始标记(STW)-并发标记-重新标记(STW)-清除,具有低停顿的性质,缺点是占用CPU资源严重,且产生内存碎片,整理内存碎片依旧会STW
  7. G1 :多线程,与CMS类似,使用标记-整理算法,步骤为:初始标记(STW)-并发标记-最终标记(STW)-筛选清除(可控制停顿时间),其优点是引入 Region 概念,优先清理垃圾更多的 Region

对象分配策略

  • 对象优先在 Eden 去分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代,可配置多少岁进入(默认15岁),熬过一次 Minor GC 涨一岁
  • 动态对象年龄判定, Survivor 区空间中相同年龄所有对象大小的总和大于空间的一半,则年龄大于或等于该年龄的对象直接进入老年代;
  • 空间分配担保,即防止新生代的对象大量存活时,Survivor 区装不下,则需要判定老年代是否还有足够大的连续空间(大于新生代Eden区或开启参数则取历代存活平均值),若有则可无需 FullGC,反之需要FullGC

JVM 详记

组成图

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
graph LR

R(JVM)

A(编译器)
B(类加载器)
C(执行引擎)
D(运行时数据区)

D1(堆)
D2(栈)
D3(本地方法栈)
D4(方法区)
D5(程序计数器 Program Count Register)

D2A(局部变量表)
D2B(操作数栈)
D2C(动态链接)
D2D(返回地址)

R-->A
R-->B
R-->C
R-->D

D-->D1
D-->D2
D-->D3
D-->D4
D-->D5

D2-->D2A
D2-->D2B
D2-->D2C
D2-->D2D

概念

JVM:

  • 编译器:对 Java 代码做优化,并将代码中结构、数据、实际代码编译成字节码。
  • 类加载器
    • 类加载过程: 加载(把字节码二进制流加载到方法区,如文件到内存)、验证(文件格式、元数据合法性、字节码指令等的校验)、准备(静态变量分配内存、初始化零值)、解析(字符符号引用解析成直接引用)、初始化(执行生成的 cinit 方法,里面是对静态变量的赋值指令和 static 块代码)
    • new 对象过程: 先进行类加载(若未加载), 接着为对象分配内存(内存大小是固定的), 接着讲内存空间的字段值初始化为零值(即内存中的值都是根据字段类型所对应的默认值), 对对象进行必要的设置(如与类信息绑定, 对象头的数据初始化等), 到此, 一个对象产生了, 然后再执行它的 init 方法(构造方法)就 ok 了.
  • 执行引擎:主要负责方法的分派,执行指令控制操作数栈。
  • 运行时数据区
    • : 最大的内存区域, 线程共享, 主要用于存放对象实例数据, 由于垃圾回收的原因, 也可分为新年代与老年代, 新年代具体可分为 Eden 区, From Survivor 区, To Surviovr 区.
    • 本地方法栈: 与栈功能类似, 但服务于 Native 方法, 而Native 方法使用何种语言实现由具体虚拟机实现.
    • 方法区: 用于存储加载的类信息、常量、静态变量等,线程共享.
    • 程序计数器 Program Count Register: 一块较小的内存空间, 线程私有. 存储线程所执行的字节码指令的行号, 分支、循环、跳转、异常处理、线程恢复等功能都需要依赖此内存. 当运行本地方法时, 值为空.
    • : 是Java方法执行的内存模型, 线程私有, 生命周期与线程相同, 每个方法执行时会创建一个栈桢, 方法的调用和结束对应着栈桢的入栈和出栈. (总结: 栈像个集合, 存储着每个方法的栈桢)
      • (以下属于栈桢)
      • 局部变量表: 一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
      • 操作数栈: 是一个后入先出的栈,数据的写入与写出都由字节码指令控制,一开始是空的;这很像CPU的操作
      • 动态链接: 指向运行时常量池中该栈桢所属方法的引用,为了支持方法调用过程中的动态链接。
      • 方法出口(返回地址): 提供用于回到方法被调用的位置的地址,一般为进入前PC计数器中的值。

总结: 编译Java源代码, 创建内存并划分区域, 加载类字节码, 从入口开始执行指令, 本质上, 所有的执行都是方法直接的组合或嵌套而成, 因此最终所有的指令, 都在方法栈执行, 也就是操作数栈. 所以需要了解常用的指令, 如内存相关, 锁相关, 方法跳转, 指令间跳转(if/for).

指令(操作数栈)

  • 字节码与数据类型
    • i 开头代表对 int 类型的数据操作
    • l 开头代表对 long 类型的数据操作
    • s 开头代表对 short 类型的数据操作
    • b 开头代表对 byte 类型的数据操作
    • c 开头代表对 char 类型的数据操作
    • f 开头代表对 float 类型的数据操作
    • d 开头代表对 double 类型的数据操作
    • a 开头代表对 reference 类型的数据操作
  • 加载和存储指令
    • 讲一个局部变量加载到操作栈: iload、iload_load
    • 将一个数值从操作数栈存储到局部变量表: istore、istore_ 即 store
    • 将一个常量加载到操作数栈: bipush、sipush、ldc
    • 扩充局部变量表的访问所以的指令: wide
  • 运算指令
    • 加法(iadd、ladd)、减法(isub、lsub)、乘法(imul、lmul)、除法(idiv、ldiv)、求余(irem、lrem)
    • 取反(ineg、lneg)、位移(ishl、ishr、iushr)、按位或(ior、lor)、按位与(iand、land)、按位异或(ixor、lxor)
    • 局部变量自增指令(iinc)
    • 比较指令(dcmpg、dcmpl、fcmpg、fcmpl)
  • 类型转换指令
    • 宽化不需要转换(隐式)
    • 宅化有 i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f
  • 对象创建于访问指令
    • 创建实例的指令:new
    • 创建数组的指令:newarray、anewarray、multianewarray
    • 访问类字段(static 字段)和实例字段的指令:getfield、putfield、getstatic、putstatic
    • 把一个数组元素度加载到操作数栈:baload、caload、saload;即 aload
    • 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、即 astore
    • 取数组长度的指令:arraylength
    • 检查类实例类型的指令:instanceof、checkcast
  • 操作数栈管理指令
    • 将操作数栈的站定一个或两个元素出栈:pop、pop2
    • 复制栈顶一个或两个数值并将复制值或双份复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1
    • 将栈顶最顶端的两个值互换: swap
  • 控制转移指令
    • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq…
    • 复合条件分支:tableswitch、lookupswitch
    • 无条件分支:goto、goto_w、jsr、jsr_w、ret
  • 方法调用和返回指令
    • invokevirtual 调用对象的实例方法,根据对象的实际类型进行分派
    • invokeinterface 调用接口方法,搜索实现了接口方法的对象,找出适合的方法调用
    • invokespecial 调用一些需要特殊处理的方法,包括实例初始化方法、私有方法、父类方法
    • invokestatic 调用静态方法 (static 方法)
    • invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法
    • 返回指令 ireturn、lreturn、freturn、dreturn、areturn、return( void 返回值时)
  • 异常处理指令 athrow;其他异常由涉及的指令抛出;另外 catch 不是由字节码指令实现,而是采用异常表
  • 同步指令
    • monitorenter 以栈顶元素作为锁开始同步
    • monitorexit 推出同步

类文件结构

  • 魔数 0xCAFEBABE 和主版本号(判断兼容性)
  • 常量池
    • 文本字符串
    • 声明为 final 的常量值
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
  • 常量池类型: 字符, 字段, 方法, 类名, 普通类型常量
    • 字符串 CONSTANT_Utf8_info: tag u1, length u2, bytes, u1[]
    • Integer, Float, Double,String
    • 类 CONSTANT_Class_info: tag u1, index u2 (指向字符串)
    • CONSTANT_Name-AndType_info: tag u1, index u2, index u2
    • 字段 CONSTANT_Fieldref_info: tag u1, index u2(指向 CONSTANT_Class_info), index u2(指向CONSTANT_Name-AndType_info)
    • 字段 CONSTANT_Methodref_info: tag u1, index u2(指向 CONSTANT_Class_info), index u2(指向CONSTANT_Name-AndType_info)
  • 访问标识: 两个字节, 标识类还是接口, 是否 public, 是否 final, 是否 abstract, 是否注解, 枚举…
  • 类索引、父类索引、接口索引集合 :都是 CONSTANT_Class_info,其实就是全限定名
  • 字段表集合: 由多个字段表组成,下面为字段表的构成
    • access_flags 字段修饰符: 描述字段是否 public ,是否 private,是否 protected 等等
    • name_index 字段的简单名称
    • descriptor_index 字段的数据类型, 如 B(byte)、C(char)、D(double)、F(float)、L(对象类型)、[L(对象数组类型)
    • attribute_info 字段的额外信息, 如字段为常量时会含有额外信息
  • 方法表集合:有多个方法表组成, 下面为方法表的构成
    • access_flags 方法修饰符:修饰方法是否为 public、是否为 private、是否为 static、是否 synchronized 等等
    • name_index 方法的简单名称
    • descriptor_index 方法的参数类型和返回值类型, 先参数,后返回值,且参数被括号包围,如 ()V、(LLDF)L
    • attribute_info 方法的额外信息, 里面有名为 Code 的属性, 存放方法代码编译成的字节码指令
  • 属性表集合:类、字段和方法的额外信息存放处,类型有
    • 方法表的 Code:
    • 字段表的 ConstantValue
    • 方法表的异常信息 Exceptions
    • 类中的内部类 InnerClasses
    • 源码行号与字节码行号的对应关系 LineNumberTable
    • 栈桢中局部变量表中变量与Java源码定义变量直接的关系 LocalVariableTable
    • 泛型的签名信息 Signature

类加载过程

  • 加载
    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的惊天存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
  • 验证
    • 文件格式验证(如魔数,版本号、常量的类型等等)会简单校验字节流,将字节流转为内存里的数据结构
    • 元数据验证(这个类是否有父类、继承的类是否合法、非抽象类是否实现了所有抽象方法等等)
    • 字节码验证(校验方法代码中的字节码指令语义是否正确、保证方法的类型转换时有效的)
    • 符号引用验证(校验常量区的符号引用是否正确,确保解析阶段能正常执行)
  • 准备
    • 为静态变量分配内存,并初始化为零值
  • 解析
    • 类或接口的解析:解析的符号引用的类不是数组则需要先加载这个类,再校验访问权限
    • 字段解析:先从自身找,自身无且实现了接口,则找继承的接口中的引用,自身无也无实现接口,则找父类,都找不到则报错,找到则校验权限。
    • 方法解析:与字段类似,从自身到实现的接口、继承的父类中找,找到后校验权限。
    • 接口方法解析:从自身接口中或父接口中找、找到无需校验权限(都是 public)
  • 初始化
    • 执行 cinit 方法,方法中执行对静态变量的赋值指令和 static 快
    • 父类的初始化方法一定先于子类执行
    • 执行时会加锁同步执行、因此不要进行耗时操作

顺带一提类加载器,作用是加载字节码二进制流到虚拟机中,除了系统顶级的类加载器(用于加载 rt.jar)是由 C++ 实现外,其他的类加载器都是 Java代码,通过组合一个类加载器(即父类加载器),实现双亲委派模型:即总是先通过父类加载类,除非父类加载失败;若自己实现一个类加载器,一般需要组合 ClassLoader.getSystemClassLoader()

系统的类加载器有:

  • 启动类加载器 Bootstrap ClassLoader 加载 JAVA_HOME/lib 下的类,如 rt.jar
  • 扩展类加载器 Extension ClassLoader 加载 JAVA_HOME/lib/ext 下的类
  • 应用程序类加载器 Application ClassLoader 加载 classpath 下的类

Java 内存模型

主要是为了屏蔽调各种硬件和操作系统的内存访问差异,以实现 Java 程序在各种平台下都能达到一致的内存访问效果。可有效防止不同平台的并发访问因平台差异有所不同引发线程安全问题。

  • 主内存与工作内存:这里变量不包括线程私有的内存区域
    • 所有变量都存储主内存中
    • 每条线程有自己的工作内存
    • 线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝
    • 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存的变量。
    • 不同线程之间无法直接访问对方工作内存中的变量
    • 线程间变量值的传递均需要通过主内存来完成
    • 主内存类似于物理硬件中的内存,而工作内存优先存储于寄存器和高速缓存中(看 JVM 具体实现)
  • 内存间交互操作
    • lock:作用于主内存的变量,它把一个变量表示为一条线程独占的状态。
    • unlock:作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
    • read:作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
    • load:作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
    • use:作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令都会执行这个操作。
    • assign:作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个需要给变量赋值的字节码指令时执行这个操作。
    • store:存储作用于工作内存的变量,它把工作内存中一个变量的值传递给主内存中,以便随后的 write 操作使用
    • write:作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存中的变量中。
  • 对于 volatile 型变量的特殊规则
    • 保证此变量对所有线程的可见性,但不保证原子性,因此部分操作仍是线程不安全的, 如 i++
    • 禁用指令重排序优化, 即保证变量的赋值操作的顺序与代码中的顺序一致;其他变量可能会因指令重排序优化而不一致。
  • 对于 long 和 double 型变量的特殊规则
    • 对于这两类型变量, JVM 规范不严格要求对 8个操作(read、load等)都具有原子性
    • 但大多数虚拟机实现仍保证了这一点,所有不需要把这类型的变量特别的添加 volatile 修饰
  • 原子性、可见性、有序性
    • 原子性:有 Java 内存模型来直接保证原子性变量操作包括(read、load、assign、use、store、write),基本可以认为基本数据类型的访问读写是具有原子性的,可使用 synchronized 实现原子性
    • 可见性:是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile、synchronized、final 可实现可见性
    • 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。可使用 volatile、synchronized 实现有序性
  • 先行发生原则
    • 程序次序规则:在一个线程中,按照程序代码顺序,书写在前的操作先行发生于书写在后的操作。
    • 管程锁定规则:一个 unlock 操作先行发生与后面对同一个锁的 lock 操作。
    • volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
    • 线程启动规则:Thread 对象的 start 方法先行发生于此线程的每一个动作
    • 线程终止规则:线程中所有操作都先行发生于对此线程的终止检测。
    • 线程中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
    • 对象终结规则:一个对象的初始化完成先行发生于他的 finalize 方法的开始
    • 传递性:如果操作 A 先行发生于操作 B, 操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。

Java 线程安全

定义: 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外同步(就是调用者不需要额外同步,代码本身可使用同步),或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象时线程安全的。

作者说:把“调用这个对象的行为” 限定为 “单词调用” ,也可称为线程安全。这是个弱化。

  • Java 语言中的线程安全
    • 不可变:不可变的对象一定是线程安全的;如 String
    • 绝对线程安全:与定义等同,过于严格
    • 相对线程安全:仅保证对这个对象单独的操作是线程安全的,如 Vector。
    • 线程兼容:对象本身不安全,但通过操作前后加同步手段来保证操作线程安全。则称为线程兼容,如 ArrayList。
    • 线程对立:即使使用同步手段,还是无法做到线程安全的代码。比如 System.setIn()
  • 线程安全的实现方法
    • 互斥同步:即只被一个线程使用,如 synchronized
    • 非阻塞同步:一般指乐观锁,即先进行操作,若无其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采用其他的补偿措施(通常是重试);依赖 CAS 原子操作,即系统底层支持
    • 无同步方案:一般指 ThreadLocal,将变量控制在线程中,不与其他线程共享,则也是线程安全的。
  • 锁优化
    • 自旋锁与自适应锁:自旋指线程在等待锁时不让出CPU资源,而是循环的重试来获取锁;自然,可以配置重试次数,可以配置是否开启。自适应指的是自旋的时间或者说次数,根据虚拟机统计的自旋获得锁的成功与否,来增长或缩短自旋的时间。
    • 锁消除:即 JVM 判断某些代码的变量无需使用同步,因为这些变量不会被其他线程锁读写,如方法里是局部变量的 StringBuffer 对象的 append 方法。其使用 synchronized,但局部变量都是线程私有的,并不会被其他线程读写。
    • 锁粗化:避免锁粒度太小导致频繁的互斥,带来额外的开销,虚拟机会对这种情况将锁的范围粗化。
    • 轻量级锁:即当一个锁没有两条以上的线程争用的时候,通过 CAS 操作加锁和解锁,比重量级锁开销更少。若超过两个线程争用,则通过修改对象头标志位将锁升级为重量级锁,因为此时再使用 CAS 反而是一种消耗(这是因为 CAS 总是会失败)
    • 偏向锁:即一个线程第一次通过 CAS 获得这个锁后,之后再获取将不再需要加锁(即使释放了锁也没关系),这是因为在第一次获取时会将对象头的标志改为偏向锁,并记录线程 ID(这个动作也是通过 CAS 进行的),这样以后只需要对比线程 ID,刷脸入场。

大致流程

  1. 编译器编译 .java 后缀文件得到字节码文件 .class;
  2. 类加载器加载字节码文件到运行时区域;
  3. 执行器为类的方法创建栈桢,并结合操作数栈执行字节码指令,遇到方法调用的指令,则可能需要根据对象类型判断执行哪个具体的方法,然后执行对应的字节码指令。

Redis 底层探秘

Redis 数据结构实现原理

copy from https://juejin.cn/post/6844903856313368589#heading-3

1.string

1
2
3
4
5
6
7
8
9
10
11
12
13
// sdshdr64 变成 sdshdr32, 则相应的 len 和 alloc 变成 uint32_t
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
/*
len:已使用的长度,即字符串的真实长度
alloc:除去标头和终止符('\0')后的长度
flags:低3位表示字符串类型,其余5位未使用(我暂时没发现redis在哪里使用过这个属性)
buf[]:存储字符数据
*/

redis 写的字符串库有以下4个优点:

  • 降低获取字符串长度的时间复杂度到O(1)
  • 减少了修改字符串时的内存重分配次数
  • 兼容c字符串的同时,提高了一些字符串工具方法的效率
  • 二进制安全(数据写入的格式和读取的格式一致)

总结: string 就是字符串, 但 Redis 其实有实现自己的类库提高各方面的速度, 只能说, 专业!!!

2.list

ziplist

ziplist并不是一个类名,其结构是下面这样的: <zlbytes><zltail><entries><entry>...<entry><zlend>

其中各部分代表的含义如下:

  • zlbytes:4个字节(32bits),表示ziplist占用的总字节数
  • zltail:4个字节(32bits),表示ziplist中最后一个节点在ziplist中的偏移字节数
  • entries:2个字节(16bits),表示ziplist中的元素数
  • entry:长度不定,表示ziplist中的数据
  • zlend:1个字节(8bits),表示结束标记,这个值固定为ff(255)

这些数据均为小端存储,所以可能有些人查看数据的二进制流与其含义对应不上,其实是因为读数据的方式错了

ziplist内部采取数据压缩的方式进行存储,压缩方式就不是重点了,我们仅从宏观来看,ziplist类似一个封装的数组,通过zltail可以方便地进行追加和删除尾部数据、使用entries可以方便地计算长度

quicklist

1
2
3
4
5
6
7
8
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* ziplist所有节点的个数 */
unsigned long len; /* quicklistNode节点的个数 */
int fill : 16; /* 单个节点的填充因子 */
unsigned int compress : 16; /* 压缩端结点的深度 */
} quicklist;

我们可以明显地看出,quicklist是一个双向链表的结构,但是内部又涉及了ziplist,我们可以这么说,在宏观上,quicklist是一个双向链表,在微观上,每一个quicklist的节点都是一个ziplist

在redis.conf中,可以使用下面两个参数来进行优化:

  • list-max-ziplist-size:表示每个quicklistNode的字节大小。默认为2,表示8KB
  • list-compress-depth:表示quicklistNode节点是否要压缩。默认为0,表示不压缩

这种存储方式的优点和链表的优点一致,就是插入和删除的效率很高,而链表查询的效率又由ziplist来进行弥补,所以quicklist就成为了list数据结构的首选

总结: 是一个双向链表 + ziplist, 同时具有两者的优点(指插入速度和查询速度, 具体咋回事, 怎么做到的.. 完全不知道啊).

3.hash

ziphash

zipmap其格式形如下面这样: <zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"

各部分的含义如下:

  • zmlen:1个字节,表示zipmap的总字节数
  • len:1~5个字节,表示接下来存储的字符串长度
  • free:1个字节,是一个无符号的8位数,表示字符串后面的空闲未使用字节数,由于修改与键对应的值而产生

这其中相邻的两个字符串就分别是键和值,比如在上面的例子中,就表示"foo" => "bar", "hello" => "world"这样的对应关系

这种方式的缺点也很明显,就是查找的时间复杂度为O(n),所以只能当作一个轻量级的hashmap来使用

dict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct dict {
dictType *type; /* 指向自定义类型的指针,可以存储各类型数据 */
void *privdata; /* 私有数据的指针 */
dictht ht[2]; /* 两个hash表,一般只有h[0]有效,h1[1]只在rehash的时候才有值 */
long rehashidx; /* -1:没有在rehash的过程中,大于等于0:表示执行rehash到第几步 */
unsigned long iterators; /* 正在遍历的迭代器个数 */
} dict;

// 真正的
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;

很明显是一个链表,我们知道这是采用链式结构存储就足够了

这种方式会消耗较多的内存,所以一般数据较少时会采用轻量级的zipmap

总结: 是个链表, 缺点是消耗内存高, 因此数据较少会采用 zipmap 这种…. 按字节精打细算的方式存储.

4.set

1
2
3
4
5
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;

其中各字段含义如下:

  • encoding:数据编码格式,表示每个数据元素用几个字节存储(可取的值有2、4,和8)
  • length:元素个数
  • contents:柔性数组,这部分内存单独分配,不包含在intset中

intset有一个数据升级的概念,比方说我们有一个16位整数的set,这时候插入了一个32位整数,所以就导致整个集合都升级为32位整数,但是反过来却不行,这也就是柔性数组的由来

如果集合过大,会采用dict的方式来进行存储

总结: 底层是个数组的 inset 结构体, 另外集合过大会采用 dict 来存储.

5.zset

zset,有很多地方也叫做sorted set,是一个键值对的结构,其键被称为member,也就是集合元素(zset依然是set,所以member不能相同),其对应的值被称为score,是一个浮点数,可以理解为优先级,用于排列zset的顺序

其也有两种存储方式,一种是ziplist/zipmap的格式,这种方式我们就不过多介绍了,只需要了解这种格式将数据按照score的顺序排列即可

另一种存储格式是采用了skiplist,意为跳跃表,可以看成平衡树映射的数组,其查找的时间复杂度和平衡树基本没有差别,但是实现更为简单,形如下面这样的结构(图来源跳跃表的原理)

例图

总结: 看不懂….

再看看这个吧… 更细致一点! https://juejin.cn/post/6844904192042074126

Redis 的持久化

摘记一下: copy from https://mp.weixin.qq.com/s/zfkhQFEBkKSRsfKF63R94A

RDB

写RDB文件是Redis的一种持久化方式。在指定的时间间隔内将内存中的数据写入到磁盘,RDB文件是一个紧凑的二进制文件,每一个文件都代表了某一个时刻(执行fork的时刻)Redis完整的数据快照,恢复数据时,将快照文件读入内存即可。

触发保存RDB文件4种情况

  1. 手动执行save命令、bgsave
  2. 满足配置文件中配置的save相关配置项时,自动触发
  3. 手动执行flushall
  4. 关闭redis , 执行 shutdown 命令

如何让redis加载rdb文件?

只需要将rdb文件放在redis的启动目录下,redis其中时会自动加载它。

RDB模式的优缺点:

优点:RDB过程中,由子进程代替主进程进行备份的IO操作。保证了主进程仍然提供高性能的服务。适合大规模的数据备份恢复过程。

缺点:

  1. 默认情况下,它是每隔一段时间进行一次数据备份,所以一旦出现最后一次持久化的数据丢失,将丢失大规模的数据。
  2. fork()子进程时会占用一定的内存空间,如果在fork()子进程的过程中,父进程夯住了,那也就是redis卡住了,不能对外提供服务。所以不要让生成RDB文件的时间间隔太长,不然每次生成的RDB文件过大对Redis本身也是有影响的。
image-20210203195701943

总结: 每隔一段时间(一般比较久)触发的全量备份, 备份速度取决于使用情况, 备份时 Redis 仍然可读可写, 但写还是有些问题. 缺点是宕机容易丢失较久的数据.

AOF

Append Only File,他也是Redis的持久化策略。即将所有的写命令都以日志的方式追加记录下来(只追加,不修改),恢复的时候将这个文件中的命令读出来回放。

aof模式的优缺点
优点

  1. aof是用追加的形式写,没有随机磁盘IO那样的寻址开销,性能还是比较高的。
  2. aof可以更好的保护数据不丢失或者尽可能的少丢失:设置让redis每秒同步一次数据,即使redis宕机了,最多也就丢失1秒的数据。
  3. 即使aof真的体积很大,也可以设置后台重写,不影响客户端的重写。
  4. aof适合做灾难性的误删除紧急恢复:比如不小心执行了flushall,然后可以在发生rewrite之前 快速备份下aof文件,去掉末尾的 flushall,通过恢复机制恢复数据。

缺点:使用aof一直追加写,导致aof的体积远大于RDB文件的体积,恢复数据、修复的速度要比rdb慢很多。

aof的重写

AOF采取的是文件追加的方式,文件的体积越来越大,为了优化这种现象,增加了重写机制,当aof文件的体积到达我们在上面的配置文件上的阕值时,就会触发重写策略,只保留和数据恢复相关的命令。

手动触发重写

1
2
3
# redis会fork出一条新的进程
# 同样是先复制到一份新的临时文件,最后再rename,遍历每一条语句,记录下有set的语句
bgrewriteaof

RDB和AOF的选择

  • 如果我们的redis只是简单的作为缓存,那两者都不要也没事。
  • 如果数据需要持久化,那不要仅仅使用RDB,因为一旦发生故障,你会丢失很多数据。
  • 同时开启两者: 在这种情况下,redis优先加载的是aof,因为它的数据很可能比rdb更全,但是并不建议只是用aof,因为aof不是那么的安全,很可能存在潜在的bug。

推荐:

  • 建议在从机slave上只备份rdb文件,而且只要15分钟备份一次就够了。
  • 如果启动了aof,我们尽量减少rewrite的频率,基础大小设置为5G完全可以,起步也要3G。
  • 如果我们不选择aof, 而是选择了主从复制的架构实现高可用同样可以,能省掉一大笔IO操作,但是意外发生的话,会丢失十几分钟的数据。
image-20210203195722608

总结: AOF 记录的是对Redis数据库做更改的命令列表, 类似 MySQL 的 binlog.

一致性 Hash 算法

copy from https://juejin.cn/post/6844903670430040078#heading-1

总计: 一个选取集群内服务器的算法, 使其在大量的请求下可做到流量均匀的落到每台服务器上.

首先引入一个概念, 就是 hash 环, 其是 hash 的取值范围大小, 展开就是 范围下限-范围上限, 围起来则是一个环.

实现原理是, 对每个服务器取 hash, 记录下 hash 位置, 再对每个请求的特定信息(如ip, Redis Key)也取 hash, 在根据这两个 hash 大小关系, 顺时针(找比其更大的)寻找离得最近的服务器;

因为大量的请求会均匀的散落在 hash 环上, 这个是 hash 算法决定的性质, 同理大量的服务器节点也会均衡的散落在 hash 值范围内, 因此总体来看, 请求总是均匀的打落到每台服务器.

但这也引出一个问题, 就是 hash 值范围过大, 而服务器数量过少, 无法保证服务器均衡的散落在整个 hash 上, 因此引入虚拟节点, 使得每个虚拟节点的 hash 与原节点不一致, 然后每个节点创建大量的(比如1000)虚拟节点, 计算的 hash 则会均匀的散落在 hash 环上了.

再总结: 充分利用 hash 算法的特性, 以及加入取 hash 找距离近的服务器这个理念, 使得即使对服务器进行伸缩, 也可以降低被影响的key.

对比 HashSlot

了解了一致性Hash算法的特点后,我们也不难发现一些不尽人意的地方:

  • 整个分布式缓存需要一个路由服务来做负载均衡,存在单点问题(如果路由服务挂了,整个缓存也就凉了)
  • Hash环上的节点非常多或者更新频繁时,查找性能会比较低下

针对这些问题,Redis在实现自己的分布式集群方案时,设计了全新的思路:基于P2P结构的HashSlot算法,下面简单介绍一下:

  1. 使用HashSlot

    类似于Hash环,Redis Cluster采用HashSlot来实现Key值的均匀分布和实例的增删管理。

    首先默认分配了16384个Slot(这个大小正好可以使用2kb的空间保存),每个Slot相当于一致性Hash环上的一个节点。接入集群的所有实例将均匀地占有这些Slot,而最终当我们Set一个Key时,使用CRC16(Key) % 16384来计算出这个Key属于哪个Slot,并最终映射到对应的实例上去。

    那么当增删实例时,Slot和实例间的对应要如何进行对应的改动呢?

    举个例子,原本有3个节点A,B,C,那么一开始创建集群时Slot的覆盖情况是:

    1
    2
    3
    4
    节点A	0-5460
    节点B 5461-10922
    节点C 10923-16383
    复制代码

    现在假设要增加一个节点D,RedisCluster的做法是将之前每台机器上的一部分Slot移动到D上(注意这个过程也意味着要对节点D写入的KV储存),成功接入后Slot的覆盖情况将变为如下情况:

    1
    2
    3
    4
    5
    节点A	1365-5460
    节点B 6827-10922
    节点C 12288-16383
    节点D 0-1364,5461-6826,10923-12287
    复制代码

    同理删除一个节点,就是将其原来占有的Slot以及对应的KV储存均匀地归还给其他节点。

  2. P2P节点寻找

    现在我们考虑如何实现去中心化的访问,也就是说无论访问集群中的哪个节点,你都能够拿到想要的数据。其实这有点类似于路由器的路由表,具体说来就是:

    • 每个节点都保存有完整的HashSlot - 节点映射表,也就是说,每个节点都知道自己拥有哪些Slot,以及某个确定的Slot究竟对应着哪个节点。
    • 无论向哪个节点发出寻找Key的请求,该节点都会通过CRC(Key) % 16384计算该Key究竟存在于哪个Slot,并将请求转发至该Slot所在的节点。

    总结一下就是两个要点:映射表内部转发,这是通过著名的Gossip协议来实现的。

总结: 映射表记录了我这个 key 计算 hash 位于的槽属于哪个节点, 若是节点自身, 则处理请求, 若不是则转发到那个节点.

CAP理论

摘记一下: copy from https://mp.weixin.qq.com/s/zfkhQFEBkKSRsfKF63R94A

  • C(Consistency 强一致性)
  • A(Availability可用性)
  • P(Partition tolerance分区容错性)

CAP 的理论核心是: 一个分布式的系统,不可能很好的满足一致性,可用性,分区容错性这三个需求,最多同时只能满足两个.因此CAP原理将nosql分成了三大原则:

  • CA- 单点集群,满足强一致性和可用性,比如说oracle,扩展性收到了限制。
  • CP- 满足一致性,和分区容错性Redis和MongoDB都属于这种类型。
  • AP- 选择了可用性和分区容错性,他也是大多数网站的选择,容忍数据可以暂时不一致,但是不容忍系统挂掉。

Redis 配置文件

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
## 启动redis的方式
2./redis-server /path/to/redis.conf
3
4# 可以像下面这样让在当前配置文件包含引用其他配置文件
5include /path/to/local.conf
6include /path/to/other.conf
7
8# 指定哪些客户端可以连接使用redis
9Examples:
10bind 192.168.1.100 10.0.0.1 # 指定ip
11bind 127.0.0.1 ::1 # 仅限于本机可访问
12
13# 是否处于受保护的模式,默认开启
14protected-mode yes
15
16# 对外暴露的端口
17port 16379
18
19# TCP的通用配置
20tcp-backlog 511
21timeout 0
22tcp-keepalive 300
23
24# 是否以守护进程的方式运行,默认为no
25daemonize yes
26
27# 如果进程在后台运行,需要指定这个pid文件
28pidfile /var/run/redis_6379.pid
29
30# 日志级别
31# debug 测试开发节点
32# verbose (和dubug很像,会产生大量日志)
33# notice (生产环境使用)
34# warning (only very important / critical messages are logged)
35loglevel notice
36
37# 日志文件名
38logfile ""
39
40# 数据库的数量,默认16个
41databases 16
42
43# 是否总是显示logo
44always-show-logo yes
45
46
47# 设置redis的登陆密码(默认没有密码)
48# 设置完密码后,使用redis-cli登陆时,使用auth password 认证登陆
49requirepass foobared
50
51# 设置能连接上redis的客户端的最大数量
52maxclients 10000
53
54# 给redis设置最大的内存容量
55maxmemory <bytes>
56
57# 内存达到上限后的处理策略
58# volatile-lru -> 只针对设置了过期时间的key进行LRU移除
59# allkeys-lru -> 删除LRU算法的Key
60# volatile-lfu -> 使用具有过期集的密钥在近似的LFU中进行驱逐。
61# allkeys-lfu -> 使用近似的LFU退出任何密钥。
62# volatile-random -> 随机删除即将过期的key
63# allkeys-random -> 随机删除
64# volatile-ttl -> 删除即将过期的
65# noeviction -> 永不过期,返回错误
66maxmemory-policy noeviction

image-20210203194803411

MySQL 探秘笔记

由SQL语句的执行过程引出 MySQL 核心组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.客户端发送 SQL 语句给 MySQL.
2.MySQL 的 Server 层与客户端通信, 得到 SQL 语句.
3.Server 层先检查缓存, 后调用分析器.
4.分析器解析 SQL 语句得到解析树, 然后调用优化器.
5.优化器基于成本控制来寻找较优解, 如逻辑转换/选取索引/计算成本/改进计划, 最终得到查询计划; 然后调用执行器
5.执行器根据查询计划执行存储引擎并获取返回结果再返回给客户端.


# 存储引擎 InnoDB
0.先根据语句的 where 查询需要操作的列, 若单条, 单个执行, 多条(未知, 遍历执行不太可能...)
1.由于事务是自动开启的(默认设置), 因此单条语句也会自动被事务包围.
2.所以先写 undo log(默认置于共享表空间), 写这个是用于事务回滚的.
3.接着将修改写入到 Buffer Pool(即修改内存中的值).
4.二阶段提交第一阶段, 写 redo log, 此作用为防止写 binlog 时宕机造成数据不一致(前提是开了 binlog); 这里标记 redo log 为 prepare 状态.
5.接着写 binlog, 若有从库集群, 应该还要等待从库同步.
6.二阶段提交第二阶段, 写完 binlog 后, 将 redo log 标记成 commit, 代表这次提交和 binlog 保持了一致.


# PS 二阶段提交会标记 redo log 为 prepare 状态, 这样如果数据库宕机(在写 binlog 时)再重启, 读取到这个标志, 就知道提交不是完整的, 于是就要通过判断 binlog 的LSN 做一些处理了.(啥处理我也不知道, 可能是照常提交, 也可能是回滚)

PS: 选取索引只能用一个索引, 除了 union 好像会触发合并索引, 但合并后也算一个索引.

即使是 update 语句也会使用优化器寻找查询计划, 因为当带条件时, 需要先锁定记录, 再进行修改.

image-20210203163806936

几个核心组件的作用

  • 连接器: 网络编程建立端口监听, 接收客户端发送的SQL语句
  • 分析器: 对SQL进行语法、词法上的分析。
  • 优化器: 生成执行计划、选择索引。
    • *逻辑转换: *包括否定消除、等值传递和常量传递、常量表达式求值、外连接转换为内连接、子查询转换、视图合并等;
    • *优化准备: *例如索引 ref 和 range 访问方法分析、查询条件扇出值(fan out,过滤后的记录数)分析、常量表检测;
    • *基于成本优化: *包括访问方法和连接顺序的选择等;
    • *执行计划改进: *例如表条件下推、访问方法调整、排序避免以及索引条件下推。
  • 执行器: 操作执行引擎,获取SQL的执行结果
  • 存储引擎(执行引擎): 负责具体的语句执行, 查询等.

PS: 基数(值某列数据去重后剩余个数, 估算得到) 会被用于分析索引的过滤效果.

然后是存储引擎里面的一些概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 存储引擎中
Buffer Pool: 用于缓存表数据的改动, 有自己的落盘时机.

Redo log Buffer: 用于缓存 redo log, 有自己的落盘时机.
commit 事务时落盘
可配置 everysec 每秒触发落盘

Binlog Buffer: 用于缓存 binlog, 即记录每个改动, 有自己的落盘时机.
commit 事务时

undo log 文件: 默认存于共享表空间中, 也可单独存放于一个表空间. 保存 undo log, 有瘦身机制.

redo log: 用于保存 Buffer Pool 发送的变动. 防止 Buffer Pool 的脏页未刷新到磁盘就宕机导致数据丢失.
binlog: 记录逻辑表的改动, 可用于集群同步数据, 审计SQL渗透, 备份/恢复数据库.
undo log: 用于保存事务中的改动, 便于事务失败触发回滚时回滚数据.

redo log vs binlog
一个(redo log)保存数据页变动, 而数据页是实际的物理空间加载到内存中的缓存, 所以记录的是物理上的改动.
一个(binlog)保存逻辑上的改动, 比如 xx 表的 id=xxx 的行的 xx 列数据修改为了 xx. 或者哪张表新增了一行, 数据是 xxx... 所以记录的是逻辑上的改动.
两者最大的区别就是 redo log 是大小是有限的, 到了一定的大小, 会将无用的数据删掉, 而 binlog 更是一种备份, 只会越来越大... 而且没法通过瘦身保持某个大小...

事务的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 事务回滚
1.undo log: 分 insert/update, update 又分 update/delete; 但无论啥类型, 目的都是辅助事务回滚,
其中 insert 比较简单, 当回滚时根据记录下的主键(联合主键)通过索引找到对应行, 删除即可.
当回滚时 update 是根据记录的主键找到记录后, 再根据记录下的修改过的列数据, 反向修改回去.
当回滚时 delete 是根据记录的整行数据, 然后 insert 回去.
2.但一个事务往往不止一条 SQL, 因此也不止一个 undo log, 每增加一条 SQL, undo log 也随之增加, 这些 undo log 会组成一个链条.
2.若为单个事务, 当回滚时, 情况比较简单, 即只需从链条尾部, 反向遍历, 往前回滚每个 undo log 就行了.
3.若为多个事务, 与单个事务类似, 只是还需要注意链条的维护(即维持链条的连通性)

# 隔离级别
1.原理是开启事务会创建一个 ReadView, 其作用是判断 undo log 链条中哪些数据是可读的.
2.先说 ReadView, 其存有当前事务 id, 事务启动时那会未提交的事务 id 列表, 所有未提交事务中最小的事务, 下一个事务id; 这里面未提交的事务是重点.
3.RR: 可重复读, 事务一开启则创建一个 ReadView, 直到事务结束; 接着倒着遍历链条, 直到 undo log 是比自己小且不在未提交事务中的事务记录时停下, 仅获取此时的数据, 由于事务开启过程中, ReadView 不改变, 因此整个事务过程中的读取总是一致的.
4.RC: 读已提交, 和 RR 略有不同, 不同之处在于其每次 select 都会重新获取 ReadView, 这使得若有事务提交后, 再 select 数据, 则生成的 ReadView 数据会发生变化(即未提交事务 id 列表中少了刚刚提交的事务). 因此同样的逻辑进行判断, RC 能读取到已提交的事务的改动.

# PS: 居然问我 读未提交?? 那不是不需要 ReadView 就能实现吗...

图为回滚对应的 undo 链; 以及隔离级别原理的 ReadView.

image-20210203155232258

image-20210203155256012

MySQL 速记

SQL 优化

索引原理

MySQL 索引一般选择 B+树做为数据结构存储. B+ 树的优点是, 对文件IO的访问次数控制在 3 次, 保证速度的同时, 能存储千万行数据.

索引

1
2
3
1.对常用列添加索引, 视具体情况选择单一索引或复合索引(一般为复合)
2.通过 Explain 语句分析执行计划, 将 type 提升到至少 index 级别.
3.通过 Explain 语句分析执行计划, 将 extra 中 Using filesort消除(排序列加索引), Using join buffer消除 (通过给关联表的关联列加索引), Using temporary (一般通过分组列加索引), Using where(根据最左原则对条件列加复合索引)

事务

1
2
3
4
5
ACID:
A: 原子性, 多个操作要么都做, 要么都不做
C: 一致性, 数据库文件的状态必须从一个一致性状态到另一个一致性状态.
I: 隔离性, 事物之间相互隔离, 互不影响.
D: 持续性, 一个事务一但提交, 则对数据库的改变是永久的.

隔离级别

1
2
3
4
1.读未提交: 可读取其他未提交事务的执行结果(如更新了某个字段), 可能会造成读取错误的数据(未提交的事务回滚了), 造成脏读.
2.读已提交: 可读取其他已提交事务的执行结果, 2次读取数据还是可能不一致(其他事务又提交了), 造成不可重复读.
3.可重复读: 确保同一事务内多次读取数据时, 会看到相同的数据. 但可能造成幻读, 如批量修改登录密码后, 另一个事务新增了一条记录, 导致新纪录未修改.
4.串行化: 事务串行化执行, 效率低.

MySQL 默认隔离级别

可重读读

数据库锁

锁原理

1
2
行锁: 分为排它锁(X) 和共享锁(S). 即写锁和读锁.
表锁: 分为元数据锁(MDL)和表锁.

锁触发方式

1
2
行锁: 隐式(条件带有索引则锁对应列, 不带索引则锁全部行, RR 总会带有 GAP 锁, RC 不会), 显式(使用 for update, lock in share mode)
表锁: 隐式(对整个表不带条件进行增删改, 或任何 DDL 操作) 显示(使用 for update, lock in share mode)

Spring Gateway 源码笔记

关键类

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
1.DispatcherHandler
Spring Webflux 的核心类, 负责协调 HandlerMapping 和 HandlerAdapter

2.HandlerMapping
Spring Webflux 的核心类, 负责根据请求信息查找 handler

3.HandlerAdapter
Spring Webflux 的核心类, 负责执行 handler

4.RoutePredicateHandlerMapping
Spring Gateway 实现的 HandlerMapping, 负责根据谓词查找 Route 对象并返回 handler(FilteringWebHandler)

5.FilteringWebHandler
是一个 handler
用于获取 route 对象的信息(主要是 GatewayFilter), 然后封装所有拦截器(包括 GlobalFilter)到 DefaultGatewayFilterChain, 挨个执行, 倒叙回归.

6.RoutePredicateFactory
谓词实现类的工厂类
负责创建具体的谓词工厂(如 Path, Method, Before 等)
apply() 返回一个 Predicate

7.Predicate
定义了 test 方法, 返回 Boolean 值, true 代表匹配, false 代表不匹配(指匹配 Route)

8.GlobalFilter/GatewayFilter
定义了一个拦截方法, 可拦截请求进行相应处理

9.AsyncPredicate/AndAsyncPredicate
AsyncPredicate 本质上是一个方法(单方法接口), 方法被调用时会调用保存的 Predicate 类型字段的 test() 方法.
AndAsyncPredicate 是一个左右结构的 AsyncPredicate, 进行判断时先判断左边, 再判断右边
若不断的 and, 会形成树结构. 所以执行时类似遍历二叉树.

10.RouteDefinitionRouteLocator
负责从不同的 Locator(如配置文件) 获取 RouteDefinition, 并负责将 RouteDefinition 转成 Route 对象

11.RouteDefinition
包含有路由的所有配置信息, 含谓词, 拦截器, id, url 等等, 但都是字符串.

12.Route
含有的配置信是转化好了的, 如 Predicate 和 GatewayFilter.

13.NettyRoutingFilter
使用 netty 发送 http/wss 等请求.

14.GatewayFilterFactory
拦截器实现类的工厂类
负责创建具体的 GatewayFilter 对象.

15.AbstractConfigurable
负责处理谓词的配置和拦截器的配置转化成不同的 Class 配置.
实际上由 shortcutFieldOrder 属性配合 Binder 实现.
即按 shortcutFieldOrder 配置的字段列表, 按顺序从 PropertySource 中读取数据进行绑定; 而 PropertySource 则是从 RouteDefinition 的配置信息加入到 Map 后再用 MapConfigurationPropertySource 包装 Map 而得到的.

总结: 除了谓词工厂和拦截器工厂这块的实现复杂, 其他都算比较简单, 哪怕响应式编程的代码到处都是, 也还是可以大致的理解代码的意思.

这两个工厂其实基本是同一套逻辑, 同一套代码, 就是有一个接口名不同, 然后其作用也不同, 但很相似.

谓语的实现原理(如何判断哪些请求该走哪个道)?

1
2
3
4
5
6
7
8
9
1.Spring gateway 基于 Spring webflux, 因此会执行 DispatcherHandler, 这是一个 WebHandler, 所以会调用 DispatcherHandler.handle(), 这个类通过 HandlerMapping 查找对应的 handler 来执行.
2.在初始化时会从容器中查找 HandlerMapping 类型的 bean, 用于查找 handler; 而 gateway 实现了一个 RoutePredicateHandlerMapping(GatewayAutoConfiguration 中注册进去的); 因此这里是入口!!!
3.再看 RoutePredicateHandlerMapping 的逻辑, 其通过继承抽象类, 因此会在 getHandlerInternal() 中获取 handler. 接着回顾下 Route 对象的获取.
4.其使用 RouteDefinitionRouteLocator 对象来调用 PropertiesRouteDefinitionLocator.getRouteDefinitions() 获取 RouteDefinition 集合对象(这里还有好几个不同的 Locator), 然后在 RouteDefinitionRouteLocator.convertToRoute() 中将 RouteDefinition 转成 Route 对象, 此时会调用 RouteDefinitionRouteLocator.combinePredicates() 将 RouteDefinition 中的 PredicateDefinition 集合信息转成了 AsyncPredicate 对象集合. 这个 AsyncPredicate 是通过解析谓词字符串, 根据谓词名称获取工厂类(PredicateFactory)再调用相应工厂类的方法(apply 方法)生成含相应逻辑判断的 GatewayPredicate 类(判断逻辑在 test 方法中); 顺带一提谓词配置信息是 从 apply 的参数中传递过来的.
5.Spring Gateway 默认注入了很多工厂(见 GatewayAutoConfiguration), 如 Host,Path,Method,Query,Cookie 等等.
6.这样 在 RoutePredicateHandlerMapping.lookupRoute() 中的 r.getPredicate().apply(exchange) 就会执行 Route 中的谓词判断, 仅保留匹配的 Route, 然后再剩下的 Route 中取第一个返回. 最后将 Route 对象存到 exchange 中, 然后返回 FilteringWebHandler.
7.这是一个 WebHandler, 会由自带的适配器 SimpleHandlerAdapter 执行, 即调用其 handle 方法.
8.FilteringWebHandler 从 exchange 中取出 Route 对象, 在将 globalFilter 和 gatewayFilter 放到集合中(相当于链)再递归调用所有 Filter; 其中有几个 Filter 会执行请求, 即将请求分发到指定地址. 如 NettyRoutingFilter(见 GatewayAutoConfiguration).
9.到此以将 Spring gateway 接收到的请求根据谓语匹配对应的 Route(路由) 再执行所有的 Filter 后请求 route 配置的地址.

总结: 从 Spring webflux 的入口, 先 getHandler(), 得到的是返回值 FilteringWebHandler, 这个返回值会被 SimpleHandlerAdapter 执行, 但与谓词的逻辑无关; 但 getHandler() 还将配置文件(以及其他地方)获取到的 RouteDefinition 转成 Route 对象, 同时也将 RouteDefinition 中的 PredicateDefinition 转换成了 Predicate, 再将多个 Predicate 组成树结构变成一个 AsyncPredicate; 然后调用 AsyncPredicate 遍历执行每个谓词判断. 有一个返回 false, 则整个结果返回 false (指 and 相连). 如此便完成了根据谓词判断是否匹配 Route!!

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
graph TB

A1(DispatcherHandler)
A2(HandlerMapping#getHandler)
A3(RoutePredicateHandlerMapping#getHandlerInternal)
A4(RouteDefinitionRouteLocator#getRouteDefinitions)
A5(RouteDefinition)
A6(RouteDefinitionRouteLocator#convertToRoute)
A7(Route)
A8(RouteDefinitionRouteLocator#combinePredicates)
A9(PredicateDefinition#谓词配置信息)
A0(AsyncPredicate#可执行的谓词)

B0(PredicateFactory#apply)
B1(GatewayPredicate#test 谓词判断)
B2(GlobalFilter 含 NettyRoutingFilter)
B3(FilteringWebHandler)
B4(SimpleHandlerAdapter#适配器)
B5(DefaultGatewayFilterChain#拦截器链)


A1--调用-->A2
A2--调用子类-->A3

A3--后触发所有-->A0
A0--通过-->B1


A4--从配置文件等地方获得-->A5
A5--通过-->A6

A6--还调用-->A8
A8--将-->A9
A9--通过调用-->B0
B0--得到 Predicate 对象, 再由多个对象组合得到-->A0

B1--返回匹配的-->A7
A7--将其存到 exchange 对象中, 然后返回-->B3

B4--负责执行-->B3
B3--从 exchange 对象获取 Route 后再将-->B2
B2--打包封装成一个-->B5

B5--最终会执行-->G1(NettyRoutingFilter#发起HTTP请求)

A1--调用-->B4

iShot2021-01-31 00.48.56

与 LoadBalancer 对接步骤

1
2
1.上面讲到执行所有的 Filter, 那么除了用户配置的 GatewayFilter, Gateway 配置的全局 Filter 也会执行(见 GatewayAutoConfiguration); 而负责处理 lb:// 的 Filter 是 ReactiveLoadBalancerClientFilter (见 GatewayReactiveLoadBalancerClientAutoConfiguration) 
2.具体方法为 ReactiveLoadBalancerClientFilter.filter(), 其逻辑是取出 host, 调用 LoadBalancer 的 choose() 获取 ServiceInstance, 然后将 url put 回 exchange 的上下文中存起来即可.

拦截器的执行方式?

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
// 拦截链是肯定的, 所有的 Filter 都可以先执行自己逻辑, 再使用 chain 触发下一个 Filter 直到无拦截器
// 然后开始返回, 因为是顺序进入, 所以是倒叙返回
// 返回后正常是一路不断往回返回, 但你的 Filter 也可以在 调用 chain 的时候不直接返回, 而是先暂存返回值, 再通过 exchange 对象(此时执行了其他 Filter 包括实际请求也执行了, 因此会有响应数据)取出响应数据(或返回值), 进行修改, 再 return 暂存的变量.
private static class DefaultGatewayFilterChain implements GatewayFilterChain {

private final int index;

private final List<GatewayFilter> filters;

DefaultGatewayFilterChain(List<GatewayFilter> filters) {
this.filters = filters;
this.index = 0;
}

private DefaultGatewayFilterChain(DefaultGatewayFilterChain parent, int index) {
this.filters = parent.getFilters();
this.index = index;
}

public List<GatewayFilter> getFilters() {
return filters;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
// 这里挨个取出, 虽然没有对 filters 进行 pop 操作啥的, 但是下一个执行完后, 会调用 chain.filter
// 等于在递归进入这个方法, 虽然是不同对象的.. 但因为维护了 parent 和 index 的值
// 使得 this.index < filters.size() 这个递归终止条件得以正确执行. 因此和递归是类似的.
// 也就是说, 最后所有 filter 执行完后, 也会不断的回归.
// 那么, 最后的 Filter 完成请求(比如 http 请求), 得到的 response 会存在 exchange 中
// 这样回归过程(也就是 chain.filter 方法之后得代码就可以取得数据进行干预)
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index);
DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
return filter.filter(exchange, chain);
}
else {
return Mono.empty(); // complete
}
});
}

}

Sentinel 与 Openfeign 整合

关键类

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
1.SentinelFeignAutoConfiguration
注入了 Feign.Builder(即 SentinelFeign.Builder) 到容器中

2.SentinelFeign.Builder
重写 build 方法注入 sentinel 的 InvocationHandler(SentinelInvocationHandler)

3.SentinelInvocationHandler
拦截方法, 包装方法为一个资源, 进行流控降级等处理

4.SentinelAutoConfiguration
注入了 SentinelResourceAspect 来支持 @SentinelResource 注解
注入了 SentinelBeanPostProcessor 来处理 @SentinelRestTemplate
注入了 SentinelDataSourceHandler 来加载各种数据源为规则配置

5.SentinelResourceAspect
对加了 @SentinelResource 的方法添加 @Around 通知, 包围原方法已实现流控降级等处理.

6.SentinelBeanPostProcessor
对加了 @SentinelRestTemplate 注解的 RestTemplate bean, 添加一个 SentinelProtectInterceptor

7.SentinelDataSourceHandler
解析 spring.cloud.sentinel.datasource 的配置, 加载配置的规则到 Sentinel 中.

8.SentinelProtectInterceptor
包装 RestTemplate 的请求, 使其可被流控降级等操作.

整合步骤

1
2
3
4
5
6
1.Feign 那边的步骤是, 先从容器中获取 Feign.Builder 对象, 并且 FeignContext 也会注入一个默认的 Builder 对象, 但是毕竟是子容器, 优先级没有父容器高(加载配置更后, 所以 @ConditionalOnMissingBean 触发, 子容器就不注册了). 因此我们在父容器中中配置一个 Builder 就能进行对接.
2.Feign 的实现是通过 JDK 生成一个代理对象拦截方法来构造并执行 HTTP 请求, 因此其需要一个 InvocationHandler 来拦截配置; 在 Feign 中, 通过字段 invocationHandlerFactory 来创建这个 InvocationHandler, 所以我们注入自己实现的 Builder 需要设置这个字段.
3.即 SentinelFeign.Builder.build() 中调用 super.invocationHandlerFactory(xxx) 来设置.
4.xxx 是匿名内部类, 直接看 create 方法, 这里根据 @FeignClient 注解的配置(fallback/fallbackFactory) 创建了一个 SentinelInvocationHandler
5.SentinelInvocationHandler.invoke() 的逻辑是使用 SphU.entry() 包围 feign 生成的 method(这个 method 是干正事的: 执行负载均衡和发送 Http 请求), 这就对这个 method 进行流量控制了; 然后还在 catch 中处理 fallback.
6.至此, 与 feign 的对接就完成了

总结: 新建一个 Builder 令生成的 Feign 对象持有我们指定的 invocationHandlerFactory, 使其创建代理对象时使用我们创建的 SentinelInvocationHandler 拦截对象方法; 这样就把方法的执行包围起来, 进行流量控制和熔断降级(catch 异常调用 fallback)了.

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
graph TB

A1(FeignClientsRegistrar)
A2(FeignClientFactoryBean)
A3(真实 XxxService 接口)
A4(SentinelInvocationHandler)
A5(SentinelFeignAutoConfiguration)
A6(Feign.Builder 即 SentinelFeign.Builder)
A7(Feign 对象)
A8(代理对象)
A9( sentinel 将方法当做资源进行拦截)
B1(在 catch 中处理 fallback 熔断逻辑)

A1--扫描 FeignClient 注解, 注入-->A2

A3--被 controller 调用, 触发-->A4

A5--注入了一个-->A6
A2--调用-->A6


A6--的 build 方法创建一个-->A7
A7--调用 newInstance 方法创建了含-->A4
A8--用来代替-->A3
A4--的-->A8

A4--调用-->A9
A9--然后-->B1

image-20210130163418057

整合 FeignCircuitBreaker.Builder 步骤

即不注入自己的 Builder, 使用 openfeign 提供的 Builder, 通过扩展 CircuitBreakerFactory(即扩展 CircuitBreaker) 来实现流控降级(倒是 fallback 的处理便轻松了不少)

1
2
3
4
1.在 SentinelCircuitBreakerAutoConfiguration 注入一个 CircuitBreakerFactory.
2.在 Openfeign 中, 会调用其 create() 创建得到一个 CircuitBreaker (即 SentinelCircuitBreaker)
3.接着会在 FeignCircuitBreakerInvocationHandler.invoke() 中获取这个 CircuitBreaker, 调用其 run() 将要执行的 method 交给 SentinelCircuitBreaker 来处理.
4.SentinelCircuitBreaker 中 run() 的实现就是简单的 SphU.entry() 来包装方法为资源进行流控降级, 至于 fallback 则直接调用 apply 交由 Openfeign 处理.

总结: 不写自己的 Builder, 简单多了! 主要是去掉了 fallback 相关配置的获取与处理.