面向切面编程(AOP) VS 面向对象编程(OOP)

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。

  • OOP:从纵向解决代码重复问题
  • AOP:从横向解决代码重复问题,取代了传统从纵向继承体系重复代码(性能监控、事务管理、缓存,日志等)将业务逻辑与系统处理代码进行解耦。

image-20250404085620522

图左结构为正常AOP架构,日志处理会被加入到每一个方法中。图右结构为AOP架构,日志处理会被抽离出来,方法中不需要处理日志,会被AOP以切面的形式添加到每个方法开始之前。

  • 连接点(Jointpoint):表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等Spring只支持方法执行连接点,在AOP中表示为在哪里干

  • 切入点(Pointcut): 选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合

  • 通知(Advice):在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么

  • 切面(Aspect):横切关注点的模块化,比如上边提到的日志组件。可以认为是通知和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合

Spring AOP和AspectJ

AspectJ是一个Java实现的AOP框架,它能够对Java代码进行AOP编译(一般在编译期进行),让Java代码具有AspectJ的AOP功能(需要特殊的编译器)

Spring 框架通过定义切面, 通过拦截切点实现了不同业务模块的解耦,这个就叫**面向切面编程 - Aspect Oriented Programming (AOP)**。

AOP的本质也是为了解耦,通过预编译方式运行期间动态代理实现程序的统一维护的一种技术。

区别:

  • AspectJ是更强的AOP框架,是实际意义的AOP标准

  • Spring AOP使用纯Java实现, 它不需要专门的编译过程, 它一个重要的原则就是无侵入性(non-invasiveness)

  • Spring小组喜欢@AspectJ注解风格更胜于Spring XML配置,在Spring 2.0使用了和AspectJ 5一样的注解,并使用AspectJ来做切入点解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或者织入器(weaver)

Spring AOP AspectJ
在纯 Java 中实现 使用 Java 编程语言的扩展实现
不需要单独的编译过程 除非设置 LTW,否则需要 AspectJ 编译器 (ajc)
只能使用运行时织入 运行时织入不可用。支持编译时、编译后和加载时织入
功能不强-仅支持方法级编织 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等
只能在由 Spring 容器管理的 bean 上实现 可以在所有域对象上实现
仅支持方法执行切入点 支持所有切入点
代理是由目标对象创建的, 并且切面应用在这些代理上 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入
比 AspectJ 慢多了 更好的性能
易于学习和应用 相对于 Spring AOP 来说更复杂

织入器(weaver):织入分为动态织入和静态织入,织入可以理解为将切面应用到目标函数中,即定义切面后,需要将切面织入到目标代码内。

  • 动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术

  • ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。

Spring AOP

Spring AOP 支持对XML模式和基于@AspectJ注解的两种配置方式。

XML模式

定义业务实现类xxxServiceImpl.java,定义切面类xxxAspect.java,编写XML配置文件xxx.xml。将业务实现类和切面类注册为bean,然后配置切面(切面中包含切入点和通知类型)。

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
<beans>
<context:component-scan base-package="tech.pdai.springframework" />

<aop:aspectj-autoproxy/>

<!-- 目标类 -->
<bean id="" class="">
<!-- configure properties of bean here as normal -->
</bean>

<!-- 切面 -->
<bean id="" class="">
<!-- configure properties of aspect here as normal -->
</bean>

<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="">
<!-- 配置切入点 -->
<aop:pointcut id="pointCutMethod" expression="execution(* xxx.service.*.*(..))"/>
<!-- 环绕通知 -->
<aop:around method="doAround" pointcut-ref="pointCutMethod"/>
<!-- 前置通知 -->
<aop:before method="doBefore" pointcut-ref="pointCutMethod"/>
<!-- 后置通知;returning属性:用于设置后置通知的第二个参数的名称,类型是Object -->
<aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/>
<!-- 异常通知:如果没有异常,将不会执行增强;throwing属性:用于设置通知第二个参数的的名称、类型-->
<aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/>
<!-- 最终通知 -->
<aop:after method="doAfter" pointcut-ref="pointCutMethod"/>
</aop:aspect>
</aop:config>
</beans>

Spring AOP注解

注解名称 解释
@Aspect 用来定义一个切面。
@pointcut 用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。
@Before 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。
@AfterReturning 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。
@Around 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。
@After-Throwing 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。
@After 用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。
@DeclareParents 用于定义引介通知,相当于IntroductionInterceptor。

定义日志注解

首先定义一个日志注解,该注解用来标记需要被切面处理的方法上。

1
2
3
4
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}

定义切面

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableAspectJAutoProxy
@Aspect
@Component
public class LoggingAspect {
@Pointcut("@annotation(top.chc.aspectdemo.aspect.Loggable)")
public void loggable() {
}

@Before("loggable()")
public void beforeAdvice() {
System.out.println("Before the method is called");
}
}

其中切入点是注解Loggable,然后通过@Before @After等注解定义通知

放置注解

将注解放置到业务代码中

1
2
3
4
5
6
7
@Service
public class UserServiceImpl {
@Loggable
public void getUser(String name){
System.out.printf("User name: %s\n", name);
}
}

这种方式是侵入式的,会在业务代码中添加了@Loggable注解。

可以在定义切面的时候,直接将切入点定义到具体的业务类方法上,而不是注解。

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
@EnableAspectJAutoProxy
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

// 定义切点(这里以controller包下的所有方法为例)
@Pointcut("execution(* com.example.controller..*.*(..))")
public void logPointcut() {}

// 前置通知:在方法执行前记录日志
@Before("logPointcut()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();

logger.info("方法开始执行: {}.{}", className, methodName);
logger.info("入参: {}", args);
}

// 后置通知:在方法执行后记录日志
@AfterReturning(pointcut = "logPointcut()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();

logger.info("方法执行结束: {}.{}", className, methodName);
logger.info("返回结果: {}", result);
}

// 异常通知:在方法抛出异常时记录日志
@AfterThrowing(pointcut = "logPointcut()", throwing = "exception")
public void logAfterThrowing(JoinPoint joinPoint, Throwable exception) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();

logger.error("方法执行异常: {}.{}", className, methodName);
logger.error("异常信息: ", exception);
}

// 环绕通知:可以控制方法执行的整个过程
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();

// 执行目标方法
Object result = joinPoint.proceed();

long timeCost = System.currentTimeMillis() - startTime;
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();

logger.info("方法: {}.{} 执行耗时: {}ms", className, methodName, timeCost);

return result;
}
}

@Pointcut:定义切入点,可以使用不同的表达式

  • execution(* com.example..*.*(..)):匹配指定包下的所有方法
  • within(com.example.controller.*):匹配指定包下的所有类
  • @annotation(com.example.annotation.Log):匹配带有特定注解的方法
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
// 任意公共方法的执行:
execution(public * *(..))

// 任何一个名字以“set”开始的方法的执行:
execution(* set*(..))

// AccountService接口定义的任意方法的执行:
execution(* com.xyz.service.AccountService.*(..))

// 在service包中定义的任意方法的执行:
execution(* com.xyz.service.*.*(..))

// 在service包或其子包中定义的任意方法的执行:
execution(* com.xyz.service..*.*(..))

// 在service包中的任意连接点(在Spring AOP中只是方法执行):
within(com.xyz.service.*)

// 在service包或其子包中的任意连接点(在Spring AOP中只是方法执行):
within(com.xyz.service..*)

// 实现了AccountService接口的代理对象的任意连接点 (在Spring AOP中只是方法执行):
this(com.xyz.service.AccountService)// 'this'在绑定表单中更加常用

// 实现AccountService接口的目标对象的任意连接点 (在Spring AOP中只是方法执行):
target(com.xyz.service.AccountService) // 'target'在绑定表单中更加常用

// 任何一个只接受一个参数,并且运行时所传入的参数是Serializable 接口的连接点(在Spring AOP中只是方法执行)
args(java.io.Serializable) // 'args'在绑定表单中更加常用; 请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args版本只有在动态运行时候传入参数是Serializable时才匹配,而execution版本在方法签名中声明只有一个 Serializable类型的参数时候匹配。

// 目标对象中有一个 @Transactional 注解的任意连接点 (在Spring AOP中只是方法执行)
@target(org.springframework.transaction.annotation.Transactional)// '@target'在绑定表单中更加常用

// 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行):
@within(org.springframework.transaction.annotation.Transactional) // '@within'在绑定表单中更加常用

// 任何一个执行的方法有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行)
@annotation(org.springframework.transaction.annotation.Transactional) // '@annotation'在绑定表单中更加常用

// 任何一个只接受一个参数,并且运行时所传入的参数类型具有@Classified 注解的连接点(在Spring AOP中只是方法执行)
@args(com.xyz.security.Classified) // '@args'在绑定表单中更加常用

// 任何一个在名为'tradeService'的Spring bean之上的连接点 (在Spring AOP中只是方法执行)
bean(tradeService)

// 任何一个在名字匹配通配符表达式'*Service'的Spring bean之上的连接点 (在Spring AOP中只是方法执行)
bean(*Service)

// &&:要求连接点同时匹配两个切入点表达式
// ||:要求连接点匹配任意个切入点表达式
// !::要求连接点不匹配指定的切入点表达式

总结:

  • Spring AOP比完全使用AspectJ更加简单, 因为它不需要引入AspectJ的编译器/织入器到你开发和构建过程中。 如果你仅仅需要在Spring bean上通知执行操作,那么Spring AOP是合适的选择
  • 如果你需要通知domain对象或其它没有在Spring容器中管理的任意对象,那么你需要使用AspectJ。
  • 如果你想通知除了简单的方法执行之外的连接点(调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。

[TOC]

红黑树

红黑树的定义:

  • 每个节点为红色或黑色。
  • 根节点始终为黑色。
  • 红色节点的子节点必须为黑色(无连续红色节点)。
  • 从根到每个叶节点(NIL节点)的黑色节点数量(黑色高度)相等。
  • 所有叶节点(NIL节点)为黑色。

通过红黑树的定义,可确保其没有一条路径长度能够超出其他路径的2倍。 通过下图可知,因为需要保证从根到每个叶节点的黑色节点数量(黑色高度)相等,所以左侧为black -> red -> black -> red -> black -> null 右侧为black -> black -> black -> null从根节点到NIL节点(图中标为NULL)都只有三个,两者高度相差相差是小于2倍的。

image-20250508203425141

红黑树控制了最长路径和最短路径之比,间接地使红黑树达到了“近似平衡”,增删查改地时间消耗不会过大,并且相比AVL树,旋转调整次数会更少。

红黑树节点的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RBNode<K extends Comparable<K>, V> {
K key;
V value;
RBNode<K, V> parent, leftChild, rightChild;
boolean isRed;

public RBNode(K key, V value, RBNode<K, V> leftChild, RBNode<K, V> rightChild, RBNode<K, V> parent, boolean isRed) {
this.key = key;
this.value = value;
this.leftChild = leftChild;
this.rightChild = rightChild;
this.parent = parent;
this.isRed = isRed;
}
}

红黑树的插入

为了不影响红黑树中黑色数量的平衡,将插入的节点默认设置为红色,但插入红色后会出现双红的情况,需进行调整。

情况1:root是空

直接插入到root并设置为黑色

情况2:root不为空

根据二叉树找到需要插入的位置,即某个叶子节点(NIL)并记录它的父节点(p)

情况2.1:父节点(p)为黑色

直接在该叶子节点(NIL)插入并设置为红色

情况2.1:父节点(p)为红色

若加入红色的插入节点,会出现双红的情况。

情况2.1.1:父节点(p)的兄弟节点(u)为黑色或(NIL)

父节点(p)的兄弟节点(u)也就是插入节点(cur)的叔叔节点。该情况共有四种情况:

  • (p)是(g)的左儿子,(cur)是(p)的左儿子,执行LL情况
  • (p)是(g)的左儿子,(cur)是(p)的右儿子,执行LR情况
  • (p)是(g)的右儿子,(cur)是(p)的左儿子,执行RL情况
  • (p)是(g)的右儿子,(cur)是(p)的右儿子,执行RR情况

LL情况:

算法:

  1. 对(p)节点右旋
  2. 重新着色: (p)设置为黑色,(g)设置为红色
  3. 结束

分析(其他情况类推):

若下图是红黑树的最底层叶子节点,则(curl) = NIL, (curr) = NIL, (pr) = NIL, (u) = NIL。所以旋转结束后,以(p)为根节点的子树黑色平衡且高度为一 没有发生变化。

若下图是递归迭代过程中,并假设(u)存在,如果不存在就变为上面红黑树的最底层的情况一致了。那么(g)的右子树黑色高度为 [1 + (u)子树黑色高度] 为保持平衡,(pr)中黑色节点的高度为 [1 + (u)子树黑色高度]也就是至少有一个黑色节点,(curl)与(curr)跟(pr)是一样的。在旋转后,(curl)(curr)(pr)的黑色高度都为 [1 + (u)子树黑色高度],(g)的右子树黑色高度也是 [1 + (u)子树黑色高度] 所以旋转后黑色仍然平衡且没有发生变化。

无论是红黑树的最底层叶子节点还是递归迭代过程中,旋转变色之后黑色都是平衡且高度不变的,并且子树的根节点为黑色不变。故不需要向上调整。

LR情况:

算法:

  1. 对(cur)节点左旋
  2. 变为LL情况,按照LL情况执行后续操作

RR情况:

算法:

  1. 对(p)节点左旋
  2. 重新着色: (p)设置为黑色,(g)设置为红色
  3. 结束

RL情况:

算法:

  1. 对(cur)节点右旋
  2. 变为RR情况,按照RR情况执行后续操作

image-20250512173800440

情况2.1.2:父节点(p)的兄弟节点(u)为红色

算法:

  1. 重新着色:(p)设置为黑色,(u)设置为黑色,(g)设置为红色
  2. 将(g)看作新插入的红色节点,向上递归迭代。一直迭代到根节点即(g)为root,将(g)改为黑色,直接结束。

image-20250512183009307

红黑树的删除

首先要明确几个概念:

1.约定(NIL)是黑色,也可以作为实际节点的儿子。

旋转操作 包括左旋右旋

2.左旋

本文以(cur)为基点进行旋转(有些文章是以(p)为基点)

算法:

  1. 记录节点(p)的父节点(g)(如果有),图中并为标出
  2. 将节点(curl)移至节点(p)的右儿子
  3. 将节点(p)移至节点(cur)的左儿子
  4. 将节点(cur)移至节点(g)的儿子指向,(如果节点(p)有父节点(g))

image-20250511120425567

3.右旋:

本文以(cur)为基点进行旋转(有些文章是以(p)为基点)

算法:

  1. 记录节点(p)的父节点(g)(如果有),图中并为标出
  2. 将节点(curr)移至节点(p)的左儿子
  3. 将节点(p)移至节点(cur)的右儿子
  4. 将节点(cur)移至节点(g)的儿子指向,(如果节点(p)有父节点(g))

image-20250511120419480

4.删除节点

针对删除操作,不同的文章有不同的标准,有直接删除节点的,有定义双黑节点的。本文会结合两者。

为了更直观的观察节点的删除情况,本文以直接删除节点的情况来绘图,但删除过程中有一种情况需要递归向上的平衡子树,所以在递归的情况下,使用双黑节点。无论使用直接删除节点还是双黑节点,两者的原理是相同的。

代码实现中:在寻找到删除节点后(该节点是经过移动后最终的删除节点没有儿子的情况)便立刻用(NIL)替代删除节点,后续判断过程中无论是最底层节点的直接删除,还是递归向上平衡子树都不需要再次删除了,即相当于使用的是双黑节点的思路,即双黑节点的内容是删除后的最后结果,不需要再操作了,只需要平衡该节点即可。

下图中要删除的节点是(cur):

  • 若(cur)没有子节点则它的两个儿子都是NIL节点,删除后需要用(NIL)替代。右上子树是其双黑节点的表示形式
  • 若(cur)有子节点,这种情况只出现在递归向上平衡子树的情况中,该情况在图中用双黑节点表示,但是旋转变色过程与直接删除节点的情况是一样的。右下子树是其双黑节点的表现形式。

这三种表现形式都是一样的,都是以(cur)节点为根的子树内部的不同表现形式,并不会影响到(p)以及上面的节点。

image-20250511141844943

5.图标

黑圆代表黑色节点,红圆代表红色节点,虚线圆代表红或者黑节点。

双圆代表双黑节点

黑方形代表根是黑色的子树,红方形代表根是红色的子树,虚线方形代表根是黑色或者红色的子树。其中子树可能是(NIL),可能只有一个节点,也可能有多层。

删除原理: 首先通过二叉树搜索找到需要删除的节点(cur),若节点为红色用其某个儿子节点替代即可。若节点为黑色,删除该节点会导致黑色节点数量减一,导致黑色节点数量不再平衡,为了保证删除后仍然黑色平衡,需要借用一个红色节点转移到该子树上变为黑色,这样该子树的黑色节点数量和删除之前是一样的,根据不同的情况,该红色节点可以从删除节点(cur)的孩子上,兄弟节点(b)的子树上、父节点(p)以其上面祖辈这三个地方借用红色。

注:其中双黑节点就是删除原节点后黑色高度减一的子树的根,等借一个红节点变为黑色后黑色高度平衡,即可将该双黑节点改为单黑节点(即正常黑色节点)

首先通过二叉树搜索找到需要删除的节点(cur),根据(cur)的孩子节点情况分类:

  • (cur)有两个儿子节点
  • (cur)有一个儿子节点
  • (cur)没有儿子节点

情况1:(cur)有两个儿子节点

找到(cur)节点的前驱节点后继节点来替代(cur)节点,替代后颜色设置为(cur)节点的颜色,然后继续删除该前驱节点后继节点。本文采用后继节点来替代删除节点(cur)

情况2:(cur)有一个儿子节点

(cur)有一个儿子节点有两种可能:

情况2.1:(cur)是红色 (不存在)

当(cur)是红色且它有一个儿子节点。若儿子节点是黑色,则黑色数量不平衡;若儿子节点是红色,则两个红色不能为父子。所以该情况不存在。

情况2.2:(cur)是黑色

当(cur)是黑色且它有一个儿子节点。若儿子节点是黑色,则黑色数量不平衡;若儿子节点是红色,成立。

该情况只有(cur)是黑色,(cur)的一个儿子是红色。因为自己的儿子就是红色,可以像自己的儿子借红色节点。

算法:

  1. 将(cur)节点的红色儿子替代(cur)节点。即(p)节点的儿子换为(cur)节点的红色儿子(如果(p)存在)
  2. 将(cur)节点红色儿子的颜色改为(cur)的颜色,即黑色

image-20250511164052174

情况3:(cur)没有儿子节点

(cur)没有儿子节点分为两种情况:

情况3.1:(cur)是红色

(cur)是红色且没有儿子节点,直接删除该节点用(NIL)替代即可。因为删除红色节点并不会影响黑色平衡。

算法:

  1. 用(NIL)节点替代(cur)节点,即(p)节点的儿子换为(cur)节点的(NIL)节点(如果(p)存在)
image-20250511170746244

情况3.2:(cur)是黑色

(cur)是黑色,删除后会导致(cur)锁在的原子树黑色高度减一,所以需要借红色节点变为黑色来补齐黑色高度,而(cur)没有孩子节点,此时需要向兄弟节点或者父亲节点来借。

情况3.2.1:(cur)的兄弟节点(b)是黑色,且(b)的外节点儿子是红色

当(cur)是(p)的左儿子,(b)为(p)的右儿子

算法:

  1. 对兄弟节点(b)左旋
  2. 重新着色:(br)设置为黑色,(b)设置为(p)的颜色,(p)设置为黑色
  3. 删除(cur)节点,用(NIL)替代;若是递归过程中,(cur)节点已经替代过了,不需要操作,以双黑的话术是一开始即用 (NIL)(cur)自身 替代(cur)形成双黑节点,此时将双黑节点变为单黑(即正常的黑色节点)

完成操作后,以(b)为根节点的子树黑色平衡且高度与原来一致,结束平衡过程。

image-20250511184757521

当(cur)是(p)的右儿子,(b)为(p)的左儿子

算法:

  1. 对兄弟节点(b)右旋转
  2. 重新着色:(bl)设置为黑色,(b)设置为(p)的颜色,(p)设置为黑色
  3. 删除(cur)节点,用(NIL)替代;若是递归过程中,(cur)节点已经替代过了,不需要操作,以双黑的话术是一开始即用**(NIL)(cur)自身**替代(cur)形成双黑节点,此时将双黑节点变为单黑(即正常的黑色节点)

完成操作后,以(b)为根节点的子树黑色平衡黑色高度与原来一致且**(b)的颜色与原来根节点(p)颜色相同**,结束平衡过程。

image-20250511184808148

情况3.2.2:(cur)的兄弟节点(b)是黑色,且(b)的内节点儿子是红色(其外节点儿子是黑色)

当(cur)是(p)的左儿子,(b)为(p)的右儿子

算法:

  1. 对(bl)节点进行右旋
  2. 再对(bl)节点进行左旋
  3. 重新着色:(bl)设置为(p)的颜色,(p)设置为黑色
  4. 删除(cur)节点,用(NIL)替代;若是递归过程中,(cur)节点已经替代过了,不需要操作,以双黑的话术是一开始即用**(NIL)(cur)自身**替代(cur)形成双黑节点,此时将双黑节点变为单黑(即正常的黑色节点)

完成操作后,以(bl)为根节点的子树黑色平衡黑色高度与原来一致且**(bl)的颜色与原来根节点(p)颜色相同**,结束平衡过程。

image-20250511202439661

当(cur)是(p)的右儿子,(b)为(p)的左儿子

算法:

  1. 对(br)节点进行左旋
  2. 再对(br)节点进行右旋
  3. 重新着色:(br)设置为(p)的颜色,(p)设置为黑色
  4. 删除(cur)节点,用(NIL)替代;若是递归过程中,(cur)节点已经替代过了,不需要操作,以双黑的话术是一开始即用**(NIL)(cur)自身**替代(cur)形成双黑节点,此时将双黑节点变为单黑(即正常的黑色节点)

完成操作后,以(br)为根节点的子树黑色平衡黑色高度与原来一致且**(br)的颜色与原来根节点(p)颜色相同**,结束平衡过程。

image-20250511202455584

情况3.2.3:(cur)的兄弟节点(b)是黑色,且(b)的两个儿子都是黑色

该情况下,(b) (bl) (br) (cur)均为黑色

情况3.2.3.1: 父节点(p)为红色

算法:

  1. 重新着色:(p)设置为黑色,(b)设置为红色
  2. 删除(cur)节点,用(NIL)替代;若是递归过程中,(cur)节点已经替代过了,不需要操作,以双黑的话术是一开始即用**(NIL)(cur)自身**替代(cur)形成双黑节点,此时将双黑节点变为单黑(即正常的黑色节点)

image-20250511205342926

完成操作后,以(p)为根节点的子树黑色平衡黑色高度与原来一致且(p)为黑色不会与其父节点冲突,结束平衡过程。

情况3.2.3.2: 父节点(p)为黑色

算法:

  1. 重新着色:(b)设置为红色
  2. 删除(cur)节点,用(NIL)替代;若是递归过程中,(cur)节点已经替代过了,不需要操作,以双黑的话术是一开始即用**(NIL)(cur)自身**替代(cur)形成双黑节点,此时将双黑节点变为单黑(即正常的黑色节点)
  3. 将(p)设为双黑节点,递归向上继续平衡(p),向上平衡过程中需要以下图规定的顺序进行判断,若都不满足需要将(p)的父节点设为双黑,继续向上平衡。若(b)为根节点,即(p)为NULL,终止递归。

完成操作后,以(p)为根节点的子树黑色平衡黑色高度减少了一,所以需要继续向上平衡(p)节点。此时可以将以(p)为根节点的子树看作一个整体,在以(pp)为根节点的子树中平衡,如果以(pp)为根节点的子树仍然不能平衡,就继续向上迭代,直至整棵树的根节点结束。

递归判断顺序:

  1. (bb)为黑色且(bbr)为红色
  2. (bb)为黑色且(bbl)为红色
  3. (bb)为红色
  4. (bb)为黑色且(bbl)和(bbr)均为黑色且(pp)为红色

若上述条件都不满足即出现了**(bb)为黑色且(bbl)和(bbr)均为黑色且(pp)为黑色**,这时需要继续平衡(pp)节点,即将(pp)设置为双黑节点,继续向上递归。一直递归到(pp)节点为NULL

image-20250511211440827

情况3.2.4:(cur)的兄弟节点(b)是红色

该情况下(p)一定是黑色,(bl)和(br)一定是黑色

当(cur)是(p)的左儿子,(b)为(p)的右儿子

算法:

  1. 对(b)进行左旋
  2. 重新着色:(b)设置为黑色,(p)设置为红色
  3. 平衡以(p)为根节点的子树,继续平衡(cur),此时变为了兄弟节点是黑色的情况。平衡结束即全部结束

在以(p)为根节点的子树中,可能出现三种情况:

  • (blr)是红色,执行情况3.2.1:(cur)的兄弟节点(b)是黑色,且(b)的外节点儿子是红色
  • (bll)是红色且(blr)是黑色,执行情况3.2.2:(cur)的兄弟节点(b)是黑色,且(b)的内节点儿子是红色(其外节点儿子是黑色)
  • (bll)是黑色且(blr)是黑色,执行情况3.2.3:(cur)的兄弟节点(b)是黑色,且(b)的儿子都是黑色)(p)为红色 的情况

这三种情况都是可以完成平衡的,不需要递归。

image-20250511231147168

当(cur)是(p)的右儿子,(b)为(p)的左儿子

算法:

  1. 对(b)进行右旋
  2. 重新着色:(b)设置为黑色,(p)设置为红色
  3. 平衡以(p)为根节点的子树,继续平衡(cur),此时变为了兄弟节点是黑色的情况。平衡结束即全部结束

在以(p)为根节点的子树中,可能出现三种情况:

  • (brl)是红色,执行情况3.2.1:(cur)的兄弟节点(b)是黑色,且(b)的外节点儿子是红色
  • (brr)是红色且(brl)是黑色,执行情况3.2.2:(cur)的兄弟节点(b)是黑色,且(b)的内节点儿子是红色(其外节点儿子是黑色)
  • (brl)是黑色且(brr)是黑色,执行情况3.2.3:(cur)的兄弟节点(b)是黑色,且(b)的儿子都是黑色)(p)为红色 的情况

这三种情况都是可以完成平衡的,不需要递归。

image-20250511231157008

情况3.2.5:(cur)的兄弟节点(b)是(NIL) (不存在)

因为(cur)是黑色,若兄弟节点(b)是(NIL),则黑色数量不平衡,不存在。


删除总结

至此所有的删除情况都讨论完了。目前规定一下在代码中的情况3.2:(cur)没有子节点是黑色的判断顺序:

  1. 情况3.2.1:(cur)的兄弟节点(b)是黑色,且(b)的外节点儿子是红色 [可完成平衡]
  2. 情况3.2.2:(cur)的兄弟节点(b)是黑色,且(b)的内节点儿子是红色(其外节点儿子是黑色) [可完成平衡]
  3. 情况3.2.4:(cur)的兄弟节点(b)是红色 [可完成平衡]
  4. 情况3.2.3:(cur)的兄弟节点(b)是黑色,且(b)的两个儿子都是黑色 中的 **情况3.2.3.1: 父节点(p)为红色 **[可完成平衡]
  5. 情况3.2.3:(cur)的兄弟节点(b)是黑色,且(b)的两个儿子都是黑色 中的 情况3.2.3.2: 父节点(p)为黑色 [需向上递归完成平衡]

所有的情况中只有情况3.2.3:(cur)的兄弟节点(b)是黑色,且(b)的两个儿子都是黑色 中的 情况3.2.3.2: 父节点(p)为黑色是需要向上递归完成平衡的。情况3.2.4:(cur)的兄弟节点(b)是红色 只需要转化为 情况3.2:(cur)没有子节点是黑色 中的 情况3.2.1情况3.2.2情况3.2.3中的一种,都可直接完成平衡操作。

情况3.2:(cur)没有子节点是黑色的总结:

  • 兄弟节点为红色:通过旋转和颜色交换,转化为黑色兄弟节点的情况。

  • 兄弟节点为黑色

    • 两个子节点均为黑色:将兄弟设为红色,传播双黑或利用红色父节点解决。

    • 远子节点为红色:通过单次旋转和颜色调整消除双黑。

    • 近子节点为红色:通过两次旋转和颜色调整消除双黑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# install
sudo apt update
sudo apt install nginx

sudo systemctl start nginx
sudo systemctl stop nginx

#平滑启动,不会中断
sudo systemctl reload nginx
#完全重启,会中断
sudo systemctl restart nginx
# 状态
sudo systemctl status nginx
# 开机自启动
sudo systemctl enable nginx
# 关闭开机启动
sudo systemctl disable nginx

ps aux | grep nginx

1. 什么是Nginx

Nginx 是一个高性能的HTTP服务器反向代理服务器,它以轻量级高并发处理能力而闻名。Nginx 的核心是基于事件驱动架构,这使得它在处理大量并发连接时表现出色。它的设计不像传统的服务器那样使用线程处理请求,而是一个更加高级的机制—事件驱动机制,是一种异步事件驱动结构。此外,Nginx 还提供了邮件代理、TCP/UDP 代理服务器的功能,以及强大的负载均衡缓存机制。它的模块化设计也使得它能够灵活地适应不同的应用场景。

Nginx 的反向代理功能允许它作为前端服务器,接收客户端的请求并将它们转发到后端服务器,同时,Nginx 也能够作为负载均衡器,将流量分配到多个后端服务器,这样可以提高网站的可用性和扩展性。Nginx 还支持静态文件服务,由于其高效的文件处理能力,它经常被用来作为静态资源的服务器。

2.Nginx优点

  • 高性能和高并发:Nginx 能够处理大量的并发连接,而内存消耗相对较小。这使得它成为处理高流量网站的理想选择。
  • 高可靠性: Nginx有健康监听机制,当某服务器损坏后,可自动将请求转移到其他正常的服务器。
  • 内存消耗小:与其他服务器相比,Nginx 在开启多个进程时,内存消耗仍然很低,这对于资源有限的环境非常有用。
  • 静态文件处理:Nginx 在处理静态文件方面非常高效,它能够快速地提供图片、CSS、JavaScript 等静态资源。
  • 反向代理 负载均衡:Nginx 可以作为负载均衡器,将流量分配到多个后端服务器,这样可以提高网站的可用性和扩展性。
  • 模块化和灵活性:Nginx 的模块化设计使得它能够灵活地适应不同的应用场景,如作为邮件代理、通用 TCP/UDP 代理服务器等。
  • 安全性:Nginx 提供了多种安全特性,如防止 DDoS 攻击、限制请求频率等,这有助于提高网站的安全性。

缺点:动态处理能力:Nginx 在处理动态内容方面相对较弱,它更适合作为静态资源的服务器和反向代理。对于需要复杂动态处理的应用,可能需要与其他应用服务器(如 PHP、Node.js)配合使用。

3. Nginx性能高的原因

  • 异步非阻塞事件处理机制:Nginx 使用了 epoll(在 Linux 上)模型,这是一种异步非阻塞的事件处理机制,它可以有效地处理大量的并发连接,而不会因为等待 I/O 操作而阻塞。
  • 轻量级进程/线程模型:Nginx 使用了轻量级的进程/线程模型,这使得它能够在有限的系统资源下处理大量的并发连接。
  • 高效的内存管理:Nginx 在内存管理上非常高效,它使用了自己的内存分配器,这减少了内存碎片和内存泄露的风险。
  • 模块化设计:Nginx 的模块化设计使得它能够灵活地添加或移除功能,这样可以确保只有需要的功能被加载,从而减少了资源的消耗。
  • 静态文件处理优化:Nginx 对静态文件的处理进行了优化,它能够快速地提供静态资源,而不需要后端服务器的参与。

4. Nginx如何处理请求

  1. 接收请求: 当客户端发起一个请求时,Nginx 的工作进程会监听网络端口,接收客户端的连接请求。

  2. 建立连接: Nginx 接收到客户端的连接请求后,会为该连接分配一个连接对象(ngx_connection_t)。连接对象包含连接的状态信息、读写事件处理器等。

  3. 读取请求头: Nginx 从客户端读取请求头信息。请求头包含 HTTP 方法(如 GET POST)、URL HTTP 版本以及各种请求头字段(如 Host User-Agent Content-Length 等)。

  4. 解析请求头: Nginx 解析请求头信息,提取必要的参数,如请求方法、URI Host 等。解析后的请求头信息存储在 ngx_http_request_t 结构体中。

  5. 查找匹配的 server 块: Nginx 根据请求头中的 Host 字段查找匹配的虚拟主机(server 块)。每个虚拟主机可以配置不同的域名和监听端口。

  6. 查找匹配的 location 块: 在找到匹配的虚拟主机后,Nginx 继续查找与请求 URI 匹配的 location 块。location 块定义了如何处理特定路径的请求。

  7. 执行处理阶段: Nginx 的请求处理分为多个阶段,每个阶段可以由多个模块处理。这些阶段包括但不限于:

  • rewrite phase:执行重写规则,如 URL 重写。

  • post rewrite phase:处理重写后的请求。

  • preaccess phase:执行访问控制,如 IP 地址过滤。

  • access phase:执行访问控制,如身份验证。

  • content phase:生成响应内容,如静态文件服务、反向代理、FastCGI 等。

  1. 生成响应: 在 content phase 阶段,Nginx 根据配置生成响应内容。这可能涉及读取静态文件、调用后端服务(如反向代理、FastCGI、uWSGI 等)、生成动态内容等。

  2. 发送响应头: Nginx 将生成的响应头发送回客户端。响应头包含 HTTP 状态码、响应头字段(如 Content-Type、Content-Length 等)。

  3. 发送响应体: Nginx 将生成的响应体发送回客户端。响应体可以是静态文件内容、后端服务返回的数据等。

  4. 关闭连接: 一旦响应发送完毕,Nginx 会关闭连接。如果启用了 keep-alive 连接,则连接可以保持打开状态,用于后续请求。

5. 正向代理和反向代理

都是网络代理服务器:

  • 正向代理:

    • 正向代理位于客户端和目标服务器之间。
    • 客户端通过代理服务器向目标服务器发送请求。
    • 目标服务器只能看到代理服务器的 IP 地址,而看不到客户端的真实 IP 地址。
    • 正向代理通常用于客户端访问互联网时,通过代理服务器来访问外部资源,这可以提高安全性和隐私保护。
  • 反向代理:

    • 反向代理位于客户端和目标服务器之间,但与正向代理不同,客户端通常不知道反向代理的存在。

    • 客户端向反向代理服务器发送请求,然后反向代理服务器将请求转发到一个或多个后端服务器。

    • 后端服务器处理请求并将响应返回给反向代理服务器,反向代理服务器再将响应返回给客户端。

    • 反向代理可以提高网站的可用性和扩展性,同时也可以提供缓存、负载均衡和 SSL 终端等功能。

    • 隐藏服务器:反向代理服务器可以隐藏后端服务器的存在和特征,这有助于提高安全性,因为外部用户无法直接访问后端服务器。
      负载均衡:反向代理可以作为负载均衡器,将流量分配到多个后端服务器,这样可以提高网站的可用性和扩展性。
      缓存静态内容:反向代理服务器可以缓存静态内容,如图片、CSS 和 JavaScript 文件等,这样可以减少后端服务器的负载并提高响应速度。
      SSL 终端:反向代理服务器可以处理 SSL/TLS 加密,这样可以减轻后端服务器的加密负担。
      压缩和优化:反向代理服务器可以在将内容发送到客户端之前对其进行压缩和优化,这样可以减少带宽消耗并提高响应速度。
      提供额外的安全层:反向代理服务器可以提供额外的安全层,如防火墙、DDoS 防护等。


      版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

6. 使用“反向代理服务器”的优点

  • 隐藏服务器:反向代理服务器可以隐藏后端服务器的存在和特征,这有助于提高安全性,因为外部用户无法直接访问后端服务器。

  • 负载均衡:反向代理可以作为负载均衡器,将流量分配到多个后端服务器,这样可以提高网站的可用性和扩展性。

  • 缓存静态内容:反向代理服务器可以缓存静态内容,如图片、CSS 和 JavaScript 文件等,这样可以减少后端服务器的负载并提高响应速度。

  • SSL 终端:反向代理服务器可以处理 SSL/TLS 加密,这样可以减轻后端服务器的加密负担。
    压缩和优化:反向代理服务器可以在将内容发送到客户端之前对其进行压缩和优化,这样可以

    减少带宽消耗并提高响应速度。

  • 提供额外的安全层:反向代理服务器可以提供额外的安全层,如防火墙、DDoS 防护等。

7. Nginx应用场景

  • HTTP 服务器:Nginx 可以作为 HTTP 服务器独立提供 HTTP 服务,适用于静态网站托管。

  • 虚拟主机:Nginx 支持虚拟主机功能,可以在一台服务器上托管多个网站,这对于托管提供商来说非常有用。

  • 反向代理和负载均衡:Nginx 可以作为反向代理服务器,将请求转发到后端服务器,并支持负载均衡,这对于高流量网站和应用来说非常重要。

  • API 网关:Nginx 可以配置为 API 网关,对每个接口服务进行拦截和路由,提供额外的安全层和流量控制。

  • 媒体流服务:Nginx 支持媒体流服务,可以用于视频点播和直播服务。

  • 邮件代理:Nginx 还可以作为邮件代理服务器,处理邮件传输。

  • 缓存服务器:Nginx 可以设置为缓存服务器,缓存静态内容和动态内容,减少后端服务器的负载,提高响应速度。

  • SSL 终端:Nginx 可以处理 SSL/TLS 加密,提供 HTTPS 服务,增强数据传输的安全性。

8. Nginx目录结构

client_body_temp:用于存储客户端请求的临时文件。
conf:存放 Nginx 的配置文件,包括 nginx.conf 主配置文件和其他配置文件。
fastcgi_temp:用于存储 FastCGI 进程的临时文件。
html:默认的站点目录,通常用于存放静态文件,如 HTML、CSS、JavaScript 文件等。
logs:存放 Nginx 的日志文件,包括访问日志 access.log、错误日志 error.log 和进程 ID 文件 nginx.pid。
proxy_temp:用于存储代理服务器的临时文件。
sbin:存放 Nginx 的可执行文件,如 nginx 命令。
scgi_temp:用于存储 SCGI 进程的临时文件。
uwsgi_temp:用于存储 uWSGI 进程的临时文件。

9. Nginx配置文件nginx.conf有哪些属性模块

  • http:定义了 HTTP 服务器的配置,包括文件类型、默认类型、连接超时等。

  • server:定义了虚拟主机的配置,可以包含多个 server 块,每个块定义了一个虚拟主机的设置。

  • location:定义了请求的匹配和处理规则,可以根据 URI、正则表达式等匹配请求,并指定处理方式。

  • upstream:定义了负载均衡的配置,可以指定多个后端服务器,并设置负载均衡策略。

  • events:定义了事件处理的配置,如工作连接数 worker_connections。

  • mail:如果 Nginx 用于邮件代理,这个模块用于配置邮件服务的相关参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    全局模块
    event模块
    http模块
    upstream模块

    server模块
    location块
    location块
    ....
    server模块
    location块
    location块
    ...
    ....

10. 静态文件服务

Nginx 在处理静态文件(如 HTML、CSS、JavaScript 和图像文件)时非常高效。它可以直接从文件系统中读取静态文件并返回给客户端,而不需要经过复杂的处理流程。

  • root 指令:指定文件系统上的根目录,用于查找静态资源。
  • alias 指令:为静态资源定义一个别名,方便管理和引用。
  • autoindex 指令:当请求的 URI 以斜杠 / 结尾时,启用目录索引功能,列出目录下的所有文件。
  • sendfile 指令:开启高效传输模式,允许 Nginx 直接在内核空间将文件发送给客户端,提高文件传输效率。
1
2
3
4
5
6
7
8
9
10
11
http{
server {
listen 8081;
server_name localhost;
root /home/amber/Downloads/photo;
autoindex on; # 启用目录列表
location / {
try_files $uri $uri/ =404;
}
}
}

11. 如何用Nginx解决前端跨域问题

Nginx 可以通过配置 CORS(跨源资源共享)头部来解决前端跨域问题。

在 server 或 location 块中,使用 add_header 指令添加 Access-Control-Allow-Origin 头部,指定允许访问的源。如果需要,还可以添加 Access-Control-Allow-Methods 头部,指定允许的 HTTP 方法。对于需要凭证的请求,可以添加 Access-Control-Allow-Credentials 头部。

产生跨域问题的主要原因就在于同源策略,为了保证用户信息安全,防止恶意网站窃取数据,同源策略是必须的,否则cookie可以共享。由于http无状态协议通常会借助cookie来实现有状态的信息记录,例如用户的身份/密码等,因此一旦cookie被共享,那么会导致用户的身份信息被盗取。
同源策略主要是指三点相同,协议+域名+端口 相同的两个请求,则可以被看做是同源的,但如果其中任意一点存在不同,则代表是两个不同源的请求,同源策略会限制了不同源之间的资源交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
location / {
# 允许跨域的请求,可以自定义变量$http_origin,*表示所有
add_header 'Access-Control-Allow-Origin' *;
# 允许携带cookie请求
add_header 'Access-Control-Allow-Credentials' 'true';
# 允许跨域请求的方法:GET,POST,OPTIONS,PUT
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT';
# 允许请求时携带的头部信息,*表示所有
add_header 'Access-Control-Allow-Headers' *;
# 允许发送按段获取资源的请求
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
# 一定要有!!!否则Post请求无法进行跨域!
# 在发送Post跨域请求前,会以Options方式发送预检请求,服务器接受时才会正式请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
# 对于Options方式的请求返回204,表示接受跨域请求
return 204;
}
}

12. Nginx虚拟主机怎么配置

在 Nginx 中配置虚拟主机主要涉及 server 块的设置。

  • 定义 server 块:每个 server 块定义了一个虚拟主机的配置。
  • 设置监听端口:使用 listen 指令设置服务器监听的端口,通常是 80(HTTP)和 443(HTTPS)。
  • 设置服务器名称:使用 server_name 指令设置虚拟主机的域名。
  • 定义 location 块:在 server 块内部定义 location 块,设置请求的处理规则。
  • 设置根目录:使用 root 指令设置网站内容的根目录。
  • 设置默认首页:使用 index 指令设置默认首页文件。

location 指令在 Nginx 配置中扮演着核心角色,它定义了如何处理进入 Nginx 的 HTTP 请求。location 块可以匹配不同的 URI、正则表达式或指定的字符串,从而允许对特定的请求路径应用不同的处理规则。

  • 精确匹配:使用 = 符号进行精确匹配,例如 location = / 匹配根路径。
  • 字符串开头匹配:使用 ^~ 符号匹配以特定字符串开头的 URI。
  • 正则表达式匹配:使用 ~ 或 ~* 符号进行正则表达式匹配,其中 ~ 是区分大小写的,而 ~* 是不区分大小写的。
  • 通用匹配(前缀匹配):使用 / 符号进行通用匹配,作为最后的选择,如果其他匹配都未成功,请求将被这个 location 块处理。

location 块可以包含多种指令,如 proxy_pass root try_files 等,用于定义请求的处理方式,例如代理到后端服务器、提供静态文件服务或重定向。

13. Nginx配置SSL证书

为了缓解后端服务器的压力,加密解密一般都放置在Nginx反向代理中,而后端服务器处于局域网内,不需要加密传输。

  1. 先去CA机构或从云控制台中申请对应的SSL证书,审核通过后下载Nginx版本的证书。

  2. 下载数字证书后,完整的文件总共有三个:.crt .key .pem,

    • .crt:数字证书文件,.crt.pem的拓展文件,因此有些人下载后可能没有。`
    • .key:服务器的私钥文件,及非对称加密的私钥,用于解密公钥传输的数据。
    • .pemBase64-encoded编码格式的源证书文本文件,可自行根需求修改拓展名。
  3. Nginx目录下新建certificate目录,并将下载好的证书/私钥等文件上传至该目录。

  4. 修改Nginx配置文件:

    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
    # ----------HTTPS配置-----------
    server {
    # 监听HTTPS默认的443端口
    listen 443;
    # 配置自己项目的域名
    server_name www.xxx.com;
    # 打开SSL加密传输
    ssl on;
    # 输入域名后,首页文件所在的目录
    root html;
    # 配置首页的文件名
    index index.html index.htm index.jsp index.ftl;
    # 配置自己下载的数字证书
    ssl_certificate certificate/xxx.pem;
    # 配置自己下载的服务器私钥
    ssl_certificate_key certificate/xxx.key;
    # 停止通信时,加密会话的有效期,在该时间段内不需要重新交换密钥
    ssl_session_timeout 5m;
    # TLS握手时,服务器采用的密码套件
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    # 服务器支持的TLS版本
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    # 开启由服务器决定采用的密码套件
    ssl_prefer_server_ciphers on;

    location / {
    ....
    }
    }

    # ---------HTTP请求转HTTPS-------------
    server {
    # 监听HTTP默认的80端口
    listen 80;
    # 如果80端口出现访问该域名的请求
    server_name www.xxx.com;
    # 将请求改写为HTTPS(这里写你配置了HTTPS的域名)
    rewrite ^(.*)$ https://www.xxx.com;
    }

14. 如何配置限流

Nginx 的限流基于令牌桶算法,主要依赖以下指令:

  • limit_req_zone:定义限流的共享内存区域和限制规则。
  • limit_req:在具体的 location 中应用限流规则。
1
2
3
4
5
6
7
8
9
10
11
# 限制每个 IP 每秒 2 次请求
http {
limit_req_zone $binary_remote_addr zone=myzone:10m rate=2r/s;

server {
location / {
#允许额外 5 个突发请求,超限请求会被延迟处理
limit_req zone=myzone burst=5;
}
}
}

如果客户端在 1 秒内发送 10 个请求:

  • 前 2 个请求立即处理(符合 rate)。
  • 接下来的 5 个请求进入队列(burst 容量)。
  • 剩余 3 个请求被拒绝(桶满)。
  • 队列中的 5 个请求会以每秒 2 个的速率逐步处理。

15. 漏桶流算法和令牌桶算法

漏桶算法和令牌桶算法是两种常用的流量整形和速率限制算法,它们在网络中用于控制数据的传输速率。

  • 漏桶算法:
    漏桶算法通过一个固定容量的桶和漏水龙头来控制数据流。数据以任意速率进入桶中,然后以固定的速率从桶中流出。如果桶满了,额外的数据将被丢弃。这种机制可以平滑突发流量,确保数据以固定的速率被处理,防止网络拥塞。

  • 令牌桶算法:
    令牌桶算法使用一个令牌桶,桶中会以固定的速率生成令牌。每个数据包的发送需要消耗一个令牌。如果桶中没有令牌,数据包将被延迟发送,直到桶中有令牌可用。这种算法允许数据以峰值速率发送,直到令牌耗尽,然后数据发送速率将降至平均速率。

令牌桶算法更适合需要突发传输的应用,因为它允许在令牌充足时快速发送数据。而漏桶算法则更适合于那些需要持续、均匀速率发送数据的应用。

16. Nginx 动静分离

动静分离是指将动态内容和静态内容分开处理和存储。

  • 性能优化:静态资源如图片 CSS JavaScript 文件等不经常变化,可以由 Nginx 直接提供,这样可以减少后端应用服务器的负载,提高响应速度。
  • 缓存策略:静态资源适合设置较长的缓存时间,而动态内容通常需要实时处理,动静分离后可以针对静态资源设置更有效的缓存策略。
  • 扩展性:在高流量的情况下,可以通过增加静态资源服务器的数量来提高服务能力,而不用担心影响动态内容的处理。
  • 安全性:静态资源通常不需要访问后端数据库或其他安全敏感的服务,将它们与动态内容分离可以减少安全风险。

通过动静分离,可以提高网站的整体性能和可扩展性,同时降低运维成本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80;
server_name example.com;

# 静态资源配置
location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css){
root /xxx/static_resources;
expires 7d;
}

# 动态资源配置
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

17. Nginx负载均衡的算法

Nginx 实现负载均衡主要通过 upstream 模块,该模块允许定义一个后端服务器组,并根据特定的策略将请求分发到这些服务器。以下是 Nginx 支持的一些负载均衡策略:

  • 轮询(round-robin):默认策略,将请求按顺序轮流分配给每个服务器。

  • 权重(weight):通过给每个服务器设置权重来控制流量分配的比例。

    1
    2
    3
    4
    upstream app1 {
    server 10.10.10.1 weight=3;
    server 10.10.10.2;
    }
  • IP绑定(ip_hash):通过请求来源的 IP 地址进行哈希,确保同一 IP 的请求总是被分配到同一个服务器。

    1
    2
    3
    4
    5
    6
    upstream app1 {
    ip_hash;
    server 10.10.10.1;
    server 10.10.10.2;
    }

  • 最少连接(least-connected):每次都找连接数最少的服务器来转发请求。

    1
    2
    3
    4
    5
    upstream app1 {
    least_conn;
    server 10.10.10.1;
    server 10.10.10.2;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http{
upstream backend {
server server1 weight=3;
server server2 weight=2;
server server3;
}

server {
listen 80;
server_name example.com;

location / {
proxy_pass http://backend;
}
}
}

18. Nginx配置高可用性

配置 Nginx 以实现高可用性主要涉及确保 Nginx 能够处理后端服务器的故障,并在必要时将流量重定向到健康的服务器。

  • 定义多个后端服务器:在 upstream 块中定义多个服务器,以便在一个服务器失败时有备用服务器可用。
  • 设置超时参数:配置 proxy_connect_timeout、proxy_send_timeout 和 proxy_read_timeout 指令,以便在后端服务器无响应时及时失败转移。
  • 使用 max_fails 和 fail_timeout:配置 max_fails 指令来设置在多长时间内允许多少次失败,以及 fail_timeout 指令来设置服务器失败后应该被排除在外的时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
http {
upstream backend {
server server1;
server server2;
server server3;

max_fails=3;
fail_timeout=30s;
}

server {
listen 80;
server_name example.com;

location / {
proxy_pass http://backend;
proxy_connect_timeout 1s;
proxy_send_timeout 1s;
proxy_read_timeout 1s;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
}
}
}

如果后端服务器在 30 秒内失败超过 3 次,它将被认为不可用,并从轮询中排除 30 秒。同时,如果 Nginx 遇到超时或指定的 HTTP 状态码,它将尝试将请求代理到另一个健康的服务器

19. 限制IP或浏览器访问

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
# 只允许Chrome浏览器访问
server {
listen 80;
server_name example.com;

location / {
if ($http_user_agent ~ Chrome) {
return 500;
}
proxy_pass http://backend;
}
}

# ------------------------------------------------------
# 只允许192.168.1.100和192.168.1.101访问
geo $block_ip {
default 0;
192.168.1.100 1;
192.168.1.101 1;
}

server {
listen 80;
server_name example.com;

location / {
if ($block_ip) {
return 403;
}
proxy_pass http://backend;
}
}

20. 多进程模型 主进程 和 工作进程

Nginx的多进程模型主要由一个主进程(master process)和多个工作进程(worker process)组成。主进程负责管理和监控工作进程,而工作进程负责处理实际的客户端请求。

每个工作进程都是单线程的,这意味着每个工作进程在同一时间只能处理一个客户端请求。这种设计选择主要基于以下原因:

  • 轻量级:单线程模型相对于多线程或多进程模型来说更加轻量级,减少了线程切换和进程间通信的开销。
  • 可扩展性:通过创建多个工作进程,Nginx能够同时处理多个请求,实现高并发处理能力。每个工作进程之间相互独立,可以并行处理请求,提高系统的吞吐量。
  • 高效的事件驱动模型:Nginx使用了高效的事件驱动模型(基于epoll kqueue等),通过异步非阻塞方式处理网络请求,从而避免了线程阻塞和资源浪费。

需要注意的是,尽管每个工作进程是单线程的,但Nginx通过事件驱动和非阻塞I/O的方式能够处理大量并发请求,实现高性能和高吞吐量。这种设计在处理静态内容和反向代理等场景下表现出色,但在涉及大量计算密集型任务的场景下可能会受到性能限制。在这种情况下,通常会将计算任务委托给后端应用服务器来处理。

当Nginx启动时,它会创建一个主进程(master process),主进程主要负责以下任务:

  • 读取并解析配置文件:主进程会读取Nginx的主配置文件(nginx.conf),包括全局配置和默认服务器配置。它还可以包含其他辅助配置文件(如conf.d目录下的文件)。
  • 启动工作进程:主进程会根据配置文件中指定的工作进程数量,创建相应数量的工作进程。每个工作进程都是一个独立的进程,负责实际处理客户端请求。
  • 监控工作进程:主进程会监控工作进程的运行状态,如果工作进程异常退出或终止,主进程会重新启动新的工作进程来替代。
  • 处理信号:主进程可以接收系统信号(如重启、停止等),并根据信号进行相应的操作,例如重新加载配置文件或优雅地关闭工作进程。

每个工作进程都是单线程的,每个工作进程通过异步非阻塞方式处理客户端请求。这种事件驱动的模型允许每个工作进程同时处理多个并发请求,而无需为每个请求创建一个新的线程。

工作进程的主要任务包括:

  • 接收和处理连接:工作进程通过监听套接字接收客户端的连接请求,并根据配置文件中的服务器块进行请求分发。它会接受请求、解析请求头和请求体,并执行相应的操作。
  • 处理请求:工作进程通过事件驱动模型,使用非阻塞I/O方式处理请求。它会执行请求所需的操作,如读取文件、向后端服务器发起代理请求等。
  • 响应客户端:工作进程会生成响应数据,并将响应发送回客户端。它会设置响应头、发送响应体,并根据配置进行内容压缩、缓存等操作。
  • 日志记录:工作进程会将访问日志和错误日志写入相应的日志文件,记录请求的详细信息和错误信息。

Nginx的多进程单线程模型结合了事件驱动和非阻塞I/O的优势,使得它在高并发场景下能够高效地处理大量的并发请求,同时保持较低的资源消耗。这使得Nginx成为一个流行的高性能Web服务器和反向代理服务器。

SpringBoot连接数据库基本都通过数据库框架,主要框架分为纯底层框架,半ORM框架,ORM框架。

ORM框架:ORM(Object-Relational Mapping)框架是一种编程技术,用于在对象模型和关系数据库之间建立映射,从而实现数据的持久化存储和操作。具体来说,ORM框架将数据库中的表(table)映射为编程语言中的类(class),表中的记录(record)映射为类的实例(object),字段(field)映射为对象的属性(attribute)。

Java 数据库框架

JDBC(Java Database Connectivity)

JDBC(Java Database Connectivity,Java 数据库连接)是 Java 官方设计的一套 关系型数据库访问标准 API

设计目标:统一访问标准,屏蔽关系型数据库差异。

  • 关系型数据库:MySQL、PostgreSQL、Oracle、SQL Server、DB2、SQLite、MariaDB
  • 嵌入式数据库:H2、HSQLDB、Derby
  • 云数据库:Amazon RDS、Google Cloud SQL、Azure SQL Database

注:部分非关系型数据库如MongoDB虽然侧面支持JDBC驱动但仍然推荐使用原生驱动,redis也需要使用原生驱动。

  1. 加载驱动:通过Class.forName()或DriverManager注册数据库驱动,加载JDBC驱动类。
  2. 建立连接:使用DriverManager.getConnection(url, user, password)创建与数据库的连接,生成Connection对象。
  3. 创建语句:通过Connection创建Statement或PreparedStatement对象,用于执行SQL查询。
  4. 执行SQL:调用executeQuery()(查询)或executeUpdate()(增删改)发送SQL语句到数据库,获取结果。
  5. 处理结果:对于查询,处理ResultSet对象,提取数据;对于更新,返回受影响行数。
  6. 释放资源:关闭ResultSet、Statement和Connection对象,释放数据库和JDBC资源。

ORM(Object Relational Mapping)

面向对象编程语言中的对象 映射为 关系型数据库中的数据表 的技术。

  • 将数据库表结构映射为 Java 对象
  • 提供 CRUD(增删改查)接口,自动生成 SQL
  • 提供查询语法(如 JPQL、HQL)
  • 提供事务、缓存、懒加载等扩展功能

流程图:

1
Java 对象 → ORM 映射 → SQL 生成 → JDBC 执行 → 结果映射回 Java 对象

详细流程:

  1. ORM 扫描实体类(@Entity)读取注解或 XML 映射信息。
  2. ORM 根据对象操作(save、find、update、delete)动态生成 SQL。
  3. ORM 调用 JDBC 执行 SQL。
  4. ORM 将 ResultSet 转换为 Java 对象,自动封装数据。
框架 特点 适用场景
Hibernate 功能最全,自动 SQL,支持缓存 复杂对象关系,高度自动化需求
JPA(规范) 标准接口(Hibernate 常用实现) 需要标准化 ORM 接口,Spring 官方推荐
MyBatis 半自动映射,手写 SQL SQL 灵活控制,复杂查询
Spring Data JPA 基于 JPA,进一步封装 简单快速开发,配合 Spring Boot 最佳

SpringBoot数据库框架

在使用 Spring Boot 开发应用时,选择合适的数据库框架(主要是 ORM 框架)对项目的开发效率和性能至关重要。

1.Spring Data JPA(ORM框架)

Spring Data JPA 是 Spring Boot 的默认 ORM 框架,基于 Hibernate 实现。它提供了简单易用的接口和注解,可以极大减少样板代码。

程序员不需要编写SQL语句,框架已经高度封装CRUD,调用对应的Java方法即可执行SQL语句。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

2. MyBatis(半ORM框架)

MyBatis 是一个轻量级的持久层框架,提供 SQL 映射功能,开发者可以直接编写 SQL 语句。

MyBatis允许程序员自行编写SQL语句,并通过@Mapper将编写的SQL封装到Java对象并注册为Spring的Bean中,程序员可以通过@Autowired获取Java对象并调用方法执行对应的SQL语句。

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency

3.Spring JDBC(纯底层框架)

Spring JDBC 是 Spring 提供的最底层的 JDBC 封装,适合直接操作数据库的场景。

所有的SQL都需要程序员手动编写,不支持ORM的功能。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

4. Hibernate(ORM框架)

Hibernate 是一个功能强大的 ORM 框架,Spring Data JPA 底层就是基于 Hibernate 实现的。如果你需要直接使用 Hibernate,可以绕过 Spring Data JPA。这里就不做详细介绍了。

框架 推荐场景 学习曲线 性能优化空间
Spring Data JPA 中小型项目,快速开发 中等
MyBatis 性能敏感型项目,复杂查询 中等
Hibernate 需要高级 ORM 特性 中等
JOOQ 类型安全 SQL 查询,复杂数据库操作 中等
Spring JDBC 极致性能,轻量级应用 最高

对于有经验的程序员,Mybatis框架是一个很好的选择,程序员可以自定义SQL语句用于优化性能,又可以通过ORM框架将SQL语句封装到Java对象中。

如果对数据库不甚了解,可以选择Spring Data JPA通过Java对象直接执行SQL语句,不需要了解SQL语句。

SpringBoot数据库连接原理

SpringBoot需要创建一个DataSource的bean类,用于告诉框架如何连接数据库,而且其中也可以包含连接池。

DataSource 是一个数据库连接的统一管理接口,用于获取数据库连接。

原理:

  • DataSource数据库连接池的门面
  • 程序通过 DataSource 向数据库请求连接(Connection)。
  • 连接池的管理细节(如 HikariCP、Druid、Tomcat Pool)都被 DataSource 封装起来,对上层透明。
  • 数据库的驱动:DataSource通过DriverManager利用Java SPI机制(ServiceLoader.load(Driver.class))加载类路径中的 JDBC 驱动(基于 META-INF/services/java.sql.Driver 配置)。

Mybatis数据库框架

添加依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

加载DataSource

当类路径中存在javax.sql.DataSource和嵌入式数据库相关的类,会激活自动配置类DataSourceAutoConfiguration用于配置DataSource的Bean。

其中DataSourceAutoConfiguration存在于org.springframework.boot:spring-boot-autoconfigureMETA-INF/spring/org.sringframework.boot.autoconfigure.AutoConfiguration.imports

SpringBoot启动过程中,在处理注解@EnableAutoConfiguration时,会在@Import(AutoConfigurationImportSelector.class)导入AutoConfigurationImportSelector,该类会搜索classpath内的所有/META-INF/spring/xxx.imports内的自动配置类。

注解的处理逻辑存在于AbstractApplicationContext.refresh.invokeBeanFactoryPostProcessors(beanFactory)中。

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration:

1
2
3
4
5
6
7
8
9
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class })
public class DataSourceAutoConfiguration {
//......
}

触发条件

@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })只有Spring中同时具有DataSource.classEmbeddedDatabaseType.class才会加载该配置类。其中DataSource.class是JDK标准库的一部分所以条件横为真。

  • 当引入mysql-connector-j会提供com.mysql.cj.jdbc.MysqlDataSource和数据库驱动。

  • 当引入mybatis-spring-boot-starter,包含spring-jdbc依赖,其中包含了/org/springframework/spring-jdbc/6.2.3/spring-jdbc-6.2.3.jar!/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.class

    1
    2
    3
    4
    5
    6
    7
    mybatis-spring-boot-starter
    ├── mybatis-spring-boot-autoconfigure
    ├── mybatis
    ├── mybatis-spring
    └── spring-boot-starter-jdbc
    ├── spring-jdbc
    └── HikariCP(默认连接池)

javax.sql.DataSource:由 JDK 提供,总是存在(Java 6 及以上版本)。

EmbeddedDatabaseType:由 spring-jdbc 提供,通过 mybatis-spring-boot-starter 间接引入。

读取数据库配置信息

@EnableConfigurationProperties(DataSourceProperties.class),该注解会将配置中的数据库信息封装到DataSourceProperties.class,并加载为Spring Bean。

加载数据池

@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class })通过DataSourcePoolMetadataProvidersConfiguration.class配置数据池。

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
@Configuration(proxyBeanMethods = false)
public class DataSourcePoolMetadataProvidersConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
static class TomcatDataSourcePoolMetadataProviderConfiguration {

@Bean
DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() {
return (dataSource) -> {
org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = DataSourceUnwrapper.unwrap(dataSource,
ConnectionPoolMBean.class, org.apache.tomcat.jdbc.pool.DataSource.class);
if (tomcatDataSource != null) {
return new TomcatDataSourcePoolMetadata(tomcatDataSource);
}
return null;
};
}
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
static class HikariPoolDataSourceMetadataProviderConfiguration {

@Bean
DataSourcePoolMetadataProvider hikariPoolDataSourceMetadataProvider() {
return (dataSource) -> {
HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class,
HikariDataSource.class);
if (hikariDataSource != null) {
return new HikariDataSourcePoolMetadata(hikariDataSource);
}
return null;
};
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(BasicDataSource.class)
static class CommonsDbcp2PoolDataSourceMetadataProviderConfiguration {

@Bean
DataSourcePoolMetadataProvider commonsDbcp2PoolDataSourceMetadataProvider() {
return (dataSource) -> {
BasicDataSource dbcpDataSource = DataSourceUnwrapper.unwrap(dataSource, BasicDataSourceMXBean.class,
BasicDataSource.class);
if (dbcpDataSource != null) {
return new CommonsDbcp2DataSourcePoolMetadata(dbcpDataSource);
}
return null;
};
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ PoolDataSource.class, OracleConnection.class })
static class OracleUcpPoolDataSourceMetadataProviderConfiguration {

@Bean
DataSourcePoolMetadataProvider oracleUcpPoolDataSourceMetadataProvider() {
return (dataSource) -> {
PoolDataSource ucpDataSource = DataSourceUnwrapper.unwrap(dataSource, PoolDataSource.class);
if (ucpDataSource != null) {
return new OracleUcpDataSourcePoolMetadata(ucpDataSource);
}
return null;
};
}
}
}

通过源码可以得出,提供了4种数据库链接池:

HikariCP(默认):

  • 类:com.zaxxer.hikari.HikariDataSource
  • 依赖:com.zaxxer:HikariCP
  • 默认引入:通过 spring-boot-starter-jdbcspring-boot-starter-data-jpa

Tomcat JDBC

  • 类:org.apache.tomcat.jdbc.pool.DataSource
  • 依赖:org.apache.tomcat:tomcat-jdbc
  • 使用条件:类路径中存在,且未引入 HikariCP。

Commons DBCP2

  • 类:org.apache.commons.dbcp2.BasicDataSource
  • 依赖:org.apache.commons:commons-dbcp2
  • 使用条件:类路径中存在,且未引入 HikariCP 或 Tomcat JDBC。

OracleUcp:

spring-boot-starter-jdbc中引入了HikariCP,所以Mybatis会自动配置HikariDataSourcePoolMetadata

可以通过配置文件指定数据池类型:

1
2
3
spring:
datasource:
type: org.apache.tomcat.jdbc.pool.DataSource

并加入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>

了解Java注解原理后,可知除了内置注解(@Override等),其他的通过元注解编写的注解需要自行编写注解处理器并让JVM加载这些处理器以用于解析注解。

目前为止,SpringBoot中使用了大量的注解来简化开发,如启动类的@SpringBootApplication,自定义bean的@Configuration @bean这些自定义注解都需要SpringBoot框架去解析。本文将通过SpringBoot加载Bean来讲解注解的处理。

SpringBoot启动时Bean的加载过程

1
2
3
4
5
6
7
|--LearnApplication //启动类
|----SpringApplication.run()
|------refreshContext(context);
|--------applicationContext.refresh(); //此处以ServletWebServerApplicationContext为例子
|----------AbstractApplicationContext.refresh() //实际调用的是该抽象类的方法
|------------invokeBeanFactoryPostProcessors(beanFactory); //本方法是扫描注解加载Bean的核心方法
|--------------PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

通过对SpringBoot启动流程的分析,扫描注解加载Bean类的核心方法是PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()详解

这个方法写的是真垃圾,看它在注解中直接加入issue链接就知道它写的多乱了。

该方法执行 BeanFactoryPostProcessor 及其子接口 BeanDefinitionRegistryPostProcessor 的关键方法。

  • 执行 BeanDefinitionRegistryPostProcessor

    • 先执行 PriorityOrderedBeanDefinitionRegistryPostProcessor(按优先级排序)
    • 再执行 OrderedBeanDefinitionRegistryPostProcessor
    • 最后执行普通的 BeanDefinitionRegistryPostProcessor
  • 执行 BeanFactoryPostProcessor

    • 先执行 PriorityOrderedBeanFactoryPostProcessor

    • 再执行 OrderedBeanFactoryPostProcessor

    • 最后执行普通的 BeanFactoryPostProcessor

*注:在 Spring 中,BeanFactoryPostProcessor 用于 在 Bean 实例化之前 修改 BeanDefinition,而 BeanDefinitionRegistryPostProcessor可以额外注册新的 BeanDefinition。*

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

// WARNING: Although it may appear that the body of this method can be easily
// refactored to avoid the use of multiple loops and multiple lists, the use
// of multiple lists and multiple passes over the names of processors is
// intentional. We must ensure that we honor the contracts for PriorityOrdered
// and Ordered processors. Specifically, we must NOT cause processors to be
// instantiated (via getBean() invocations) or registered in the ApplicationContext
// in the wrong order.
//
// Before submitting a pull request (PR) to change this method, please review the
// list of all declined PRs involving changes to PostProcessorRegistrationDelegate
// to ensure that your proposal does not result in a breaking change:
// https://github.com/spring-projects/spring-framework/issues?q=PostProcessorRegistrationDelegate+is%3Aclosed+label%3A%22status%3A+declined%22

// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<>();

// 如果 BeanFactory 是 BeanDefinitionRegistry,则执行 BeanDefinitionRegistryPostProcessor
if (beanFactory instanceof BeanDefinitionRegistry registry) {
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();

// 先处理方法参数传入的 BeanFactoryPostProcessors(非 Spring 扫描的)
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor registryProcessor) {
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}

// 处理BeanDefinitionRegistryPostProcessor
// 这里会处理内置于SpringBoot的ConfigurationClassPostProcessor
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();

// 首先处理实现PriorityOrdered的BeanDefinitionRegistryPostProcessors
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
//排序
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
//调用postProcessBeanDefinitionRegistry方法
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();

// 首先处理实现 Ordered 的BeanDefinitionRegistryPostProcessors
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
//调用postProcessBeanDefinitionRegistry方法
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();

// 最后 执行普通的 BeanDefinitionRegistryPostProcessor
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
// 调用 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor 的postProcessBeanFactory()方法
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// 如果 BeanFactory 不是 BeanDefinitionRegistry,则直接执行普通的 BeanFactoryPostProcessor 的postProcessBeanFactory()方法
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// 处理 Spring 容器注册的 BeanFactoryPostProcessor
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, for example, replacing placeholders in values...
beanFactory.clearMetadataCache();
}

ConfigurationClassPostProcessor

ConfigurationClassPostProcessor 是 **Spring 内部关键的 BeanFactoryPostProcessor**,它的主要作用是 解析 @Configuration@ComponentScan@Import@Bean 等注解,并将解析后的 BeanDefinition 注册到 BeanFactory 中。

Spring 在 invokeBeanFactoryPostProcessors() 时执行所有 BeanFactoryPostProcessor,其中 ConfigurationClassPostProcessorSpring 内置的 BeanFactoryPostProcessor,优先执行

1
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);

通过beanFactory.getBeanNamesForType()来获取BeanDefinitionRegistryPostProcessor.class的后处理类。调试后会得到postProcessorNames=org.springframework.context.annotation.internalConfigurationAnnotationProcessor然后将这个名字传入beanFactory.getBean()并获取它的bean类型,得到的bean类就是ConfigurationClassPostProcessor。后续会调用该类的postProcessBeanDefinitionRegistry()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);

processConfigBeanDefinitions(registry);
}

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
// 获取所有注册的bean类型
String[] candidateNames = registry.getBeanDefinitionNames();
//遍历所有的bean类,寻找有@Configuration注解的类
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
// 如果bean类中包含@Configuration注解则将该类加入到configCandidates List中
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}

// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}

// 根据@Order排序
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});

// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry singletonRegistry = null;
if (registry instanceof SingletonBeanRegistry sbr) {
singletonRegistry = sbr;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}

if (this.environment == null) {
this.environment = new StandardEnvironment();
}

// Parse each @Configuration class
// 通过ConfigurationClassParser解析@Configuration的类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = CollectionUtils.newHashSet(configCandidates.size());
do {
StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();

candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = Set.of(candidateNames);
Set<String> alreadyParsedClasses = CollectionUtils.newHashSet(alreadyParsed.size());
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());

// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (singletonRegistry != null && !singletonRegistry.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}

// Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary
this.propertySourceDescriptors = parser.getPropertySourceDescriptors();

if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory cachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
cachingMetadataReaderFactory.clearCache();
}
}

Spring Boot 是一个开源的 Java 框架,用于简化 Spring 应用程序的开发。它是 Spring 生态系统的一部分,旨在帮助开发人员更快速和方便地创建独立、生产级的 Spring 应用。

  • 快速入门:Spring Boot 通过约定优于配置的原则,使开发者可以快速上手,无需繁琐的 XML 配置。

  • 自带嵌入式服务器:Spring Boot 支持嵌入式服务器(如 Tomcat Jetty 和 Undertow),开发者无需单独部署应用服务器,可以直接运行 Java 应用。

  • 自动配置:Spring Boot 提供了自动配置功能,根据项目的类路径和配置自动设置 Spring 应用的各种组件,减少了大量的配置工作。

  • 生产级特性:Spring Boot 内置了许多生产环境所需的功能,如健康检查、指标监控、应用配置管理等。

  • 开箱即用:Spring Boot 提供了一系列的 starter 依赖,简化了依赖管理。例如,spring-boot-starter-web 可以帮助快速构建基于 Spring MVC 的 web 应用。

  • 支持微服务架构:Spring Boot 适合构建微服务应用,结合 Spring Cloud,能够实现分布式系统的开发。1.

1.@SpringBootApplication注解

1
2
3
4
5
6
7
8
@SpringBootApplication
public class LearnApplication {

public static void main(String[] args) {
SpringApplication.run(LearnApplication.class, args);
}

}

SpringBoot启动类中包含@SpringBootApplication注解和SpringApplication.run(LearnApplication.class, args);方法。SpringBoot的入口是main方法。本章节先讲解@SpringBootApplication注解。

在 Java 中,注解(Annotation) 是一种元数据的形式,可以添加到代码中(例如类、方法、字段等),以提供额外的信息。注解本身并不直接影响代码的执行逻辑,但可以通过反射机制或编译时处理来实现特定的功能。

注解主要分为:

  • 元注解:用于定义其他注解的行为 (@Retention,@Target,@Documented,@Inherited

  • 内置注解:该类注解的执行器被内置到了Javac中,由Javac负责解析注解。(@Override,@Deprecated,@SuppressWarnings

  • 自定义注解:使用元注解自定义注解,需要编写注解处理器来处理该类注解。自定义注解的处理取决于@Retention定义的阶段。

    • RetentionPolicy.SOURCE:注解仅保留在源代码中,编译后丢弃。该类注解由编译器或源码级工具在编译时处理。

      注:内置注解也属于该类,但内置注解是嵌入到编译器中的语义分析和语法分析中,属于编译器的一部分。但该类的自定义注解的处理需要通过注解处理器(Annotation Processor)处理,在 javac 编译时,注解处理器会扫描源代码中的注解,并根据注解信息执行特定逻辑,例如生成新代码、验证规则或抛出错误,比较常用的就是Lombok的注解。

    • RetentionPolicy.CLASS:注解保留在编译后的 .class 文件中,但运行时不可见。也是由注解处理器(Annotation Processor)处理。用于生成代码或验证,可用在构建工具中,一般不常用。

    • RetentionPolicy.RUNTIME:注解保留到运行时,可通过反射访问。由程序在运行时通过反射动态读取和处理。比较常见的就是各种框架,如SpringBoot。

通过@SpringBootApplication注解的源码注释可以得出,其为一个组合注解,组合了@SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan。经过后面的源码分析,可以得出SpringBoot并不是直接处理@SpringBootApplication而是处理他的三个组合注解。

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
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};

@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};

@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};

@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
  • @SpringBootConfiguration组合了@Configuration,表明是一个Java配置类
  • @EnableAutoConfiguration 组合了@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)用于将所有符合配置条件的 Bean 都加载到 IOC 容器中
  • @ComponentScan指明了包搜索路径以及需要过滤的类

2.SpringApplication.run(LearnApplication.class, args);

SpringApplication.run(LearnApplication.class, args);是整个SpringBoot的入口。查看源码:

1
2
3
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
  • 创建ConfigurableApplicationContext实例
  • 运行该实例

SpringAplplication构造函数

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
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//判断WEB应用的类型
this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());
// 将类型为BootstrapRegistryInitializer的初始化类数组赋值给this.bootstrapRegistryInitializers
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
//将类型为ApplicationContextInitializer的初始化类数组赋值给this.initializers
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//将类型为ApplicationListener的监听类数组赋值给this.listeners
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从调用栈中拿到main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

private Class<?> deduceMainApplicationClass() {
//使用Java9引入的StackWalker遍历当前线程的调用栈
return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
// 找到第一个包含main方法的类并返回它的Class<?>
.walk(this::findMainClass)
//找不到返回null
.orElse(null);
}
private Optional<Class<?>> findMainClass(Stream<StackFrame> stack) {
return stack.filter((frame) -> Objects.equals(frame.getMethodName(), "main"))
.findFirst()
.map(StackWalker.StackFrame::getDeclaringClass);
}

SpringApplication 构造函数的参数是资源加载器和主程序源(SpringBoot启动类)

1
2
//判断WEB应用的类型
this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());

WebApplicationType有三种类型:

  • NONE:程序不是WEB应用,不需要启动内置WEB服务器
  • SERVLET: 程序是WEB应用,需要启动WEB服务器
  • REACTIVE:程序是响应式WEB应用,启动REACTIVE WEB服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };

private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}

判断方法很简单,就是寻找在Classpath加载路径上有某些Class,比如有DispatcherHandler且没有DispatcherServletServletContainer则是响应式WEB应用。如果包含Servlet ConfigurableWebApplicationContext则是Servlet WEB应用。否则就不是WEB应用。

下面三个赋值中都使用了getSpringFactoriesInstances()用于获取Spring工厂对象。即将对应的class文件加载到内存对象。

1
2
3
4
5
6
7
private <T> List<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, null);
}

private <T> List<T> getSpringFactoriesInstances(Class<T> type, ArgumentResolver argumentResolver) {
return SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);
}

getSpringFactoriesInstances()

classpath 是Java的系统变量,Javac通过classpath寻找class文件或依赖包。classpath默认路径是当前目录下(./)以及javac -cp xxx指定的路径。

在SpringBoot中,对classpath进行了增强:

  • 开发模式:classpath 包含了target/classes/(编译前来自于src/main/resources/)和Maven从本地仓库中获取的依赖包(~/.m2/repository/xxx.jar)。此模式下Maven会解析pom.xml,并自动添加~/.m2/repository/ 里的 JAR 到 classpath

  • Spring Boot Fat JAR 模式:此时SpringBoot不使用classpath,而是用 LaunchedURLClassLoader 来解析 BOOT-INF/lib/ 里的 JAR。此模式下Maven会将依赖包从~/.m2/repository/中直接复制到target/myapp-1.0.0.jar/BOOT-INF/lib/中。

1
2
3
4
5
target/myapp-1.0.0.jar
├── BOOT-INF/classes/ # 编译后的 .class 文件
├── BOOT-INF/lib/ # 所有依赖的 JAR
├── META-INF/MANIFEST.MF # 启动信息
├── org/springframework/... # Spring 相关类

注:开发模式中,也可以不使用Maven的启动,直接使用Java命令。编译之后执行java -cp target/classes:$(mvn dependency:build-classpath) com.example.MainApplication,使用-cp参数将target/classes~/.m2/repository/下的依赖包添加到classpath中,也可以正常启动项目。

1
2
3
4
//maven中查看依赖
mvn dependency:tree
//输出依赖的完整路径
mvn dependency:build-classpath

了解了classpath工作原理,重点讲解SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);该类是利用classloader将classpath中的所有满足要求的类都加载进来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static SpringFactoriesLoader forDefaultResourceLocation(@Nullable ClassLoader classLoader) {
return forResourceLocation(FACTORIES_RESOURCE_LOCATION, classLoader);
}
public static SpringFactoriesLoader forResourceLocation(String resourceLocation, @Nullable ClassLoader classLoader) {
Assert.hasText(resourceLocation, "'resourceLocation' must not be empty");
//获取classloader
ClassLoader resourceClassLoader = (classLoader != null ? classLoader :
SpringFactoriesLoader.class.getClassLoader());
//从缓存中查看该classloader是否存在,如果没有创建一个cache[classloader]=hashmap<string, SpringFactoriesLoader>
//并将该hashmap返回给loaders
Map<String, SpringFactoriesLoader> loaders = cache.computeIfAbsent(
resourceClassLoader, key -> new ConcurrentReferenceHashMap<>());
//获取loaders["META-INF/spring.factories"],如果不存在,创建一个新的SpringFactoriesLoader
//参数1:classloader
//参数2:factories : Map<String, List<String>>
return loaders.computeIfAbsent(resourceLocation, key ->
new SpringFactoriesLoader(classLoader, loadFactoriesResource(resourceClassLoader, resourceLocation)));
}

其中FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories",ClassLoader是默认的类加载器 Thread.currentThread().getContextClassLoader(); 通过调试classloader = ClassLoaders$AppClassLoader

loadFactoriesResource()会将符合条件的类加载进来。

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
//classloader = ClassLoaders$AppClassLoader
//resourceLocation = "META-INF/spring.factories"
protected static Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
//result中包含[初始化类型,对应需要加载的所有初始化类]
Map<String, List<String>> result = new LinkedHashMap<>();
try {
//classLoader将classpath路径下的所有的resourceLocation文件都加载到urls中
//urls中包含了所有META-INF/spring.factories的文件路径
Enumeration<URL> urls = classLoader.getResources(resourceLocation);
while (urls.hasMoreElements()) {
//根据urls中的文件路径加载到UrlResource中
//举个例子 resource=jar:file:/home/amber/.m2/repository/org/springframework/boot/spring-boot/3.4.3/spring-boot-3.4.3.jar!/META-INF/spring.factories
UrlResource resource = new UrlResource(urls.nextElement());
//将文件中的所有属性加载到properties中
//Properties 是一个 ConcurrentHashMap<Object, Object>
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
//遍历所有属性
properties.forEach((name, value) -> {
//以逗号分割,将value字符串分割为String[]数组
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) value);
//将{name: ArrayList<String>{}}放入到result中,ArrayList的长度就是value数组的长度
List<String> implementations = result.computeIfAbsent(((String) name).trim(),
key -> new ArrayList<>(factoryImplementationNames.length));
//将factoryImplementationNames流化,将每一个元素都trim后再将每一个元素添加到implementations中
Arrays.stream(factoryImplementationNames).map(String::trim).forEach(implementations::add);
});
}
//对每一种初始化类型中的所有初始化类去重
result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
}
//返回一个不可以修改的Map,最终赋值给了SpringFactoriesLoader.factories
return Collections.unmodifiableMap(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
# file:/home/amber/.m2/repository/org/springframework/boot/spring-boot/3.4.3/spring-boot-3.4.3.jar!/META-INF/spring.factories
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
.....

到目前已经通过SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader())得到了SpringFactoriesLoader对象,其中包含了SpringFactoriesLoader.classLoader,SpringFactoriesLoader.factories。后续调用了该对象的load(type, argumentResolver)方法

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
public <T> List<T> load(Class<T> factoryType, @Nullable FailureHandler failureHandler) {
return load(factoryType, null, failureHandler);
}

public <T> List<T> load(Class<T> factoryType, @Nullable ArgumentResolver argumentResolver,
@Nullable FailureHandler failureHandler) {

Assert.notNull(factoryType, "'factoryType' must not be null");
//构造函数中已经把{初始化类型:[对应类型的实现类数组]}放入到factories中
//取出初始化类型为factoryType的类数组
List<String> implementationNames = loadFactoryNames(factoryType);
logger.trace(LogMessage.format("Loaded [%s] names: %s", factoryType.getName(), implementationNames));
//结果是一个类型为T的列表
List<T> result = new ArrayList<>(implementationNames.size());
FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_FAILURE_HANDLER;
//遍历所有的实现类
for (String implementationName : implementationNames) {
//将实现类实例化
T factory = instantiateFactory(implementationName, factoryType, argumentResolver, failureHandlerToUse);
//将实例化的实现类放入到result中
if (factory != null) {
result.add(factory);
}
}
//根据@Order @Priority对实现类进行排序
AnnotationAwareOrderComparator.sort(result);
return result;
}

private List<String> loadFactoryNames(Class<?> factoryType) {
return this.factories.getOrDefault(factoryType.getName(), Collections.emptyList());
}

@Nullable
protected <T> T instantiateFactory(String implementationName, Class<T> type,
@Nullable ArgumentResolver argumentResolver, FailureHandler failureHandler) {

try {
//使用class.forName(xxx, false, zzz)将xxx类加载到元数据区,只类加载不执行类初始化
Class<?> factoryImplementationClass = ClassUtils.forName(implementationName, this.classLoader);
Assert.isTrue(type.isAssignableFrom(factoryImplementationClass), () ->
"Class [%s] is not assignable to factory type [%s]".formatted(implementationName, type.getName()));
//创建一个用于初始化factoryImplementationClass类的内部创建类(该类包含了factoryImplementationClass的构造方法)
FactoryInstantiator<T> factoryInstantiator = FactoryInstantiator.forClass(factoryImplementationClass);
// 处理构造函数所需的参数,使用this.constructor.newInstance(args)创建factoryImplementationClass类
return factoryInstantiator.instantiate(argumentResolver);
}
catch (Throwable ex) {
failureHandler.handleFailure(type, implementationName, ex);
return null;
}
}

到目前为止,得出了getSpringFactoriesInstances(xxx.class)方法是将初始化类型为xxx.class的类从META-INF/spring.factories中提取出来,并返回该类型的所有的实现类,返回的是一个xxx.class类型的数组。

ConfigurableApplicationContext后续的三个赋值都是利用getSpringFactoriesInstances(xxx.class)将初始化类数组赋值给类成员,最后一步是从调用栈中获取main方法所在的类。

2. 执行SpringApplication的run方法

SpringApplication构造函数中都是一些赋值操作,run方法才是真正启动SpringBoot项目。

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
public ConfigurableApplicationContext run(String... args) {
// 初始化启动时钟
Startup startup = Startup.create();
// 检查properties中是否注册关闭钩子
if (this.properties.isRegisterShutdownHook()) {
//启 Shutdown Hook注册机制,保证Spring Boot应用在JVM关闭时能够正确释放资源(如数据库连接池、线程池等)。
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
// 创建引导上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
// 创建应用上下文
ConfigurableApplicationContext context = null;
// 将环境变量中java.awt.headless设置为原值或者默认true
configureHeadlessProperty();
// 用getSpringFactoriesInstances()方法获取了所有类型为
// SpringApplicationRunListener.class的启动监听类,
// 将其封装在SpringApplicationRunListeners类中
SpringApplicationRunListeners listeners = getRunListeners(args);
// 将bootstrapContext传递给所有的启动监听类
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 解析命令行传入的参数,并封装到ApplicationArguments
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 打印Banner 根据条件 判断打印到log还是console,是使用默认值还是指定banner
Banner printedBanner = printBanner(environment);
// 根据WebApplicationType创建 应用程序上下文
// 获取META-INF/spring.factories中的ApplicationContextFactory接口实现类
// ReactiveWebServerApplicationContextFactory 创建了 ReactiveWebServerApplicationContext
// ServletWebServerApplicationContextFactory 创建了 ServletWebServerApplicationContext
context = createApplicationContext();
// 将applicationStartup赋值给context
// applicationStartup 用于收集和分析应用的启动性能数据,帮助开发者优化 Spring Boot 启动过程
context.setApplicationStartup(this.applicationStartup);
// 准备 应用程序上下文
// 将参数绑定到应用程序上下文(ApplicationContext)为应用程序启动运行做好准备
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 启动 应用上下文 (SpringBoot核心启动方法)
refreshContext(context);
// 刷新之后需要的处理,默认是空
afterRefresh(context, applicationArguments);
//SpringBoot启动
startup.started();
//日志
if (this.properties.isLogStartupInfo()) {
new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
}
// 将ConfigurableApplicationContext传递给所有的启动监听类,表明started
listeners.started(context, startup.timeTakenToStarted());
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, listeners);
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, null);
}
return context;
}

创建引导程序上下文createBootstrapContext()

引导程序上下文的作用:SpringApplication 启动过程中,提前注册某些组件,以便在应用上下文(ApplicationContext)正式创建时使用

DefaultBootstrapContext 存在的时间非常短,它只在 SpringApplication.run() 方法执行的早期阶段存在,等 ApplicationContext 初始化完成后,它就会被丢弃。

引导上下文主要用于支持 Spring Cloud 的配置加载机制(例如 Spring Cloud Config)。它并不是 Spring Boot 核心的一部分,而是 Spring Cloud 提供的高级功能。

1
2
3
4
5
6
7
8
9
10
DefaultBootstrapContext bootstrapContext = createBootstrapContext();

private DefaultBootstrapContext createBootstrapContext() {
// 创建引导上下文
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
// 构造函数中已经将BootstrapRegistryInitializer类型的初始化类型数组赋值给了this.bootstrapRegistryInitializers
// 遍历初始化类型数组,并执行每一个初始化类型的initialize方法,将引导上下文DefaultBootstrapContext赋值给它
this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
return bootstrapContext;
}

监听类SpringApplicationRunListeners

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private SpringApplicationRunListeners getRunListeners(String[] args) {
ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
argumentResolver = argumentResolver.and(String[].class, args);
// 通过getSpringFactoriesInstances()获取所有接口为SpringApplicationRunListener.class的实现类
List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
argumentResolver);
SpringApplicationHook hook = applicationHook.get();
SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null;
if (hookListener != null) {
listeners = new ArrayList<>(listeners);
listeners.add(hookListener);
}
// 将监听类List和applicationStartup传递給SpringApplicationRunListeners
// 其中this.applicationStartup是一个性能分析工具,默认状态下,它什么都没做,是一个空实现。
return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup);
}

比如starting()方法赋值监听名称为"spring.boot.application.starting"并将bootstrapContext通过listener.starting()赋值给每一个监听类。

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
class SpringApplicationRunListeners {

private final Log log;

private final List<SpringApplicationRunListener> listeners;

private final ApplicationStartup applicationStartup;

SpringApplicationRunListeners(Log log, List<SpringApplicationRunListener> listeners,
ApplicationStartup applicationStartup) {
this.log = log;
this.listeners = List.copyOf(listeners);
this.applicationStartup = applicationStartup;
}

void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
(step) -> {
if (mainApplicationClass != null) {
step.tag("mainApplicationClass", mainApplicationClass.getName());
}
});
}

void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
doWithListeners("spring.boot.application.environment-prepared",
(listener) -> listener.environmentPrepared(bootstrapContext, environment));
}

void contextPrepared(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.context-prepared", (listener) -> listener.contextPrepared(context));
}

void contextLoaded(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.context-loaded", (listener) -> listener.contextLoaded(context));
}

void started(ConfigurableApplicationContext context, Duration timeTaken) {
doWithListeners("spring.boot.application.started", (listener) -> listener.started(context, timeTaken));
}

void ready(ConfigurableApplicationContext context, Duration timeTaken) {
doWithListeners("spring.boot.application.ready", (listener) -> listener.ready(context, timeTaken));
}

void failed(ConfigurableApplicationContext context, Throwable exception) {
doWithListeners("spring.boot.application.failed",
(listener) -> callFailedListener(listener, context, exception), (step) -> {
step.tag("exception", exception.getClass().toString());
step.tag("message", exception.getMessage());
});
}

private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context,
Throwable exception) {
try {
listener.failed(context, exception);
}
catch (Throwable ex) {
if (exception == null) {
ReflectionUtils.rethrowRuntimeException(ex);
}
if (this.log.isDebugEnabled()) {
this.log.error("Error handling failed", ex);
}
else {
String message = ex.getMessage();
message = (message != null) ? message : "no error message";
this.log.warn("Error handling failed (" + message + ")");
}
}
}

private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction) {
doWithListeners(stepName, listenerAction, null);
}

//每一个方法都调用了doWithListeners()
//参数1:步骤名称
//参数2:消费型函数式接口,它可以接收一个 SpringApplicationRunListener 对象,并对其执行某些操作。
private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,
Consumer<StartupStep> stepAction) {
// 性能监听,默认是空函数
StartupStep step = this.applicationStartup.start(stepName);
// listeners是SpringApplicationRunListener对象的List
// 对listeners中的每一个SpringApplicationRunListener对象执行listenerAction方法。
this.listeners.forEach(listenerAction);
if (stepAction != null) {
stepAction.accept(step);
}
step.end();
}
}

准备环境 prepareEnvironment()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
// 根据应用类型创建对应的环境类
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 将命令行的参数添加到Springboot环境变量中
// 配置spring.profiles.active
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 将 ConfigurationPropertySources 附加到 Environment,让 Spring Environment 识别 ConfigurationPropertySource
ConfigurationPropertySources.attach(environment);
// 告诉所有的监听类,环境准备阶段
listeners.environmentPrepared(bootstrapContext, environment);
ApplicationInfoPropertySource.moveToEnd(environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
// 在构造函数中已经赋值,此时拿出webapplication的类型
WebApplicationType webApplicationType = this.properties.getWebApplicationType();
// 根据不同的webapplication类型创建不同的ConfigurableEnvironment
// DefaultApplicationContextFactory
// ServletWebServerApplicationContextFactory
// ReactiveWebServerApplicationContextFactory
// 会从META/spring.factories中获取ApplicationContextFactory并根据webApplicationType选取对应的类
ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(webApplicationType);
if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
environment = ApplicationContextFactory.DEFAULT.createEnvironment(webApplicationType);
}
return (environment != null) ? environment : new ApplicationEnvironment();
}

准备应用上下文 prepareContext()

将参数绑定到应用程序上下文(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
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
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// environment赋值给context
context.setEnvironment(environment);
postProcessApplicationContext(context);
addAotGeneratedInitializerIfNecessary(this.initializers);
applyInitializers(context);
//触发所有SpringApplicationRunListerner监听器的contextPrepared
listeners.contextPrepared(context);
// 发送事件 当 bootstrapContext关闭且ApplicationContext准备好后发送该事件
bootstrapContext.close(context);
//日志处理
if (this.properties.isLogStartupInfo()) {
logStartupInfo(context.getParent() == null);
logStartupInfo(context);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
// 通过 应用上下文 获取BeanFactory
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 通过 BeanFactory 注册单例对象 springApplicationArguments
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
// 通过 BeanFactory 注册单例对象 springBootBanner
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences());
if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding());
}
}
// 懒加载 给应用上下文添加懒加载BeanFactory处理类
if (this.properties.isLazyInitialization()) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
//
if (this.properties.isKeepAlive()) {
context.addApplicationListener(new KeepAlive());
}
//
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
}
//触发所有SpringApplicationRunListerner监听器的contextLoaded
listeners.contextLoaded(context);
}

启动应用上下文refreshContext()

1
2
3
4
5
6
7
8
9
private void refreshContext(ConfigurableApplicationContext context) {
if (this.properties.isRegisterShutdownHook()) {
shutdownHook.registerApplicationContext(context);
}
refresh(context);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}

其中applicationContext有两种类:

  • ReactiveWebServerApplicationContext

  • ServletWebServerApplicationContext

他们的共同抽象父类AbstractApplicationContext,主要的refresh()方法也是在这个抽象父类中定义的。

refresh() 是 Spring 容器启动的核心方法,负责加载 Bean、初始化 BeanFactory、注册监听器、完成 Bean 初始化,并最终发布 ContextRefreshedEvent 事件

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
@Override
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();

StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

// Prepare this context for refreshing.
// 检查SpringBoot中的环境变量
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
// 从子类中获取beanfactory,默认是ConfigurableListableBeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
//为 ConfigurableListableBeanFactory配置一些基础设置和功能,以确保它能够正确地创建和管理 Bean
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
// 允许子类对beanFactory中的bean在初始化之前修改bean
postProcessBeanFactory(beanFactory);

StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
// 处理注解的主要方法
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
// 注册BeanPostProcessors的bean类
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();

// Initialize message source for this context.
// 添加消息bean
initMessageSource();

// Initialize event multicaster for this context.
// 添加事件传播bean
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
// 处理子类的刷新,如Servlet的启动Tomcat
onRefresh();

// Check for listener beans and register them.
// 注册监听bean
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
// 初始化所有的 单例bean
// 并将bean锁住
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (RuntimeException | Error 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 {
contextRefresh.end();
}
}
finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}
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
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// Tell the internal bean factory to use the context's class loader etc.
// 为 BeanFactory 设置当前的类加载器,以便在加载 Bean 定义时使用
beanFactory.setBeanClassLoader(getClassLoader());
// 用于支持 Spring 表达式语言(SpEL),例如在 @Value 注解中解析表达式。
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
// 用于处理资源(如 Resource 类型)的注入和解析
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

// Configure the bean factory with context callbacks.
// 实现了 ApplicationContextAware 接口的 Bean 能够感知到当前的 ApplicationContext
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 配置 BeanFactory 忽略一些特定的 Aware 接口的依赖自动注入,因为这些接口的实现通常由 Spring 框架自动处理。
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
beanFactory.ignoreDependencyInterface(ApplicationStartupAware.class);

// BeanFactory interface not registered as resolvable type in a plain factory.
// MessageSource registered (and found for autowiring) as a bean.
// 为一些常见的类型注册默认的单例 Bean, 可以通过依赖注入直接获取对应的实例
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// Register early post-processor for detecting inner beans as ApplicationListeners.
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// Detect a LoadTimeWeaver and prepare for weaving, if found.
if (!NativeDetector.inNativeImage() && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}

// Register default environment beans.
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
if (!beanFactory.containsLocalBean(APPLICATION_STARTUP_BEAN_NAME)) {
beanFactory.registerSingleton(APPLICATION_STARTUP_BEAN_NAME, getApplicationStartup());
}
}

Bean扫描注册invokeBeanFactoryPostProcessors()

通过PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());按优先级调用所有注册的 BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor,完成对 Bean 定义的修改。

该方法主要完成BeanDefinition的扫描、解析、注册:

1.实例化实现了BeanDefinitionRegistryPostProcessor接口的Bean

2.调用实现了BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry()方法

3.实例化实现了BeanFactoryPostProcessor接口的Bean

4.调用实现了BeanFactoryPostProcessor的postProcessBeanFactory()方法

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

// WARNING: Although it may appear that the body of this method can be easily
// refactored to avoid the use of multiple loops and multiple lists, the use
// of multiple lists and multiple passes over the names of processors is
// intentional. We must ensure that we honor the contracts for PriorityOrdered
// and Ordered processors. Specifically, we must NOT cause processors to be
// instantiated (via getBean() invocations) or registered in the ApplicationContext
// in the wrong order.
//
// Before submitting a pull request (PR) to change this method, please review the
// list of all declined PRs involving changes to PostProcessorRegistrationDelegate
// to ensure that your proposal does not result in a breaking change:
// https://github.com/spring-projects/spring-framework/issues?q=PostProcessorRegistrationDelegate+is%3Aclosed+label%3A%22status%3A+declined%22

// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<>();

if (beanFactory instanceof BeanDefinitionRegistry registry) {
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();

for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor registryProcessor) {
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
// Separate between BeanDefinitionRegistryPostProcessors that implement
// PriorityOrdered, Ordered, and the rest.
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();

// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();

// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();

// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
}

// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, for example, replacing placeholders in values...
beanFactory.clearMetadataCache();
}

注解处理

prepareBeanFactory 阶段,Spring 会为 BeanFactory 添加一些默认的处理器,这些处理器会影响后续注解的处理。例如:

  • 添加 ApplicationContextAwareProcessor,用于处理 Aware 接口(如 ApplicationContextAware)。

  • 添加 BeanNameAwareBeanFactoryAware 的支持。

invokeBeanFactoryPostProcessors 阶段的核心处理器是 ConfigurationClassPostProcessor,它实现了 BeanFactoryPostProcessor 接口,负责解析配置类及其相关注解。它的执行逻辑包括:

  • 解析主配置类(例如带有 @SpringBootApplication 的类)。
  • 处理 @ComponentScan,扫描并注册组件。
  • 处理 @Import,加载导入的配置类或执行 ImportSelector/ImportBeanDefinitionRegistrar。
  • 处理 @Bean,将配置类中的 @Bean 方法注册为 Bean 定义。

registerBeanPostProcessors 阶段,Spring 会注册所有 BeanPostProcessor,这些处理器负责处理与 Bean 初始化和生命周期相关的注解。典型注解包括:

  • @Autowired@Inject:由 AutowiredAnnotationBeanPostProcessor 处理,用于依赖注入。
  • @Value:由 AutowiredAnnotationBeanPostProcessor 处理,用于属性值注入。
  • @PostConstruct@PreDestroy:由 CommonAnnotationBeanPostProcessor 处理,用于生命周期回调。
  • **@Resource**:由 CommonAnnotationBeanPostProcessor 处理,用于 JSR-250 注解的依赖注入。

finishBeanFactoryInitialization 阶段,Spring 会实例化所有单例 Bean,并触发 BeanPostProcessor 的处理逻辑。这一阶段会实际执行依赖注入、初始化回调等操作。典型注解包括:

  • **@Autowired**:注入依赖。
  • **@PostConstruct**:执行初始化方法。
  • **@EventListener**:由 EventListenerMethodProcessor 处理,注册事件监听器。

Spring Boot 还扩展了许多自定义注解,这些注解的处理逻辑分布在不同的阶段。例如:

  • @ConditionalOnClass,@ConditionalOnMissingBean 等条件注解:由 ConditionEvaluator 在 invokeBeanFactoryPostProcessors 阶段处理,用于决定是否加载某些配置类或 Bean。
  • **@SpringBootApplication**:由 ConfigurationClassPostProcessorinvokeBeanFactoryPostProcessors 阶段处理,分解为 @Configuration @ComponentScan@EnableAutoConfiguration
  • @RestController 和 **@RequestMapping**:由 Spring MVC 相关的 BeanPostProcessor(如 RequestMappingHandlerMapping)在 finishBeanFactoryInitialization 阶段处理,用于注册控制器和映射请求路径。
注解类型 处理阶段 核心处理器
@Configuration invokeBeanFactoryPostProcessors ConfigurationClassPostProcessor
@ComponentScan invokeBeanFactoryPostProcessors ConfigurationClassPostProcessor
@Import invokeBeanFactoryPostProcessors ConfigurationClassPostProcessor
@EnableAutoConfiguration invokeBeanFactoryPostProcessors AutoConfigurationImportSelector
@ConditionalOnClass 等 invokeBeanFactoryPostProcessors ConditionEvaluator
@Bean invokeBeanFactoryPostProcessors ConfigurationClassPostProcessor
@Autowired registerBeanPostProcessors / finishBeanFactoryInitialization AutowiredAnnotationBeanPostProcessor
@Value registerBeanPostProcessors / finishBeanFactoryInitialization AutowiredAnnotationBeanPostProcessor
@PostConstruct registerBeanPostProcessors / finishBeanFactoryInitialization CommonAnnotationBeanPostProcessor
@EventListener finishBeanFactoryInitialization EventListenerMethodProcessor
@RestController finishBeanFactoryInitialization RequestMappingHandlerMapping
@RequestMapping finishBeanFactoryInitialization RequestMappingHandlerMapping

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

注: C和CPP是支持指针的,直接操作内存内容,但Java中是不支持指针的。

两数值和

双指针实现:$O(n)$

1
2
3
4
5
6
7
8
9
10
11
public int[] twoSum(int[] numbers, int target) {
int left = 0, right = numbers.length - 1;
while (left < right && numbers[left] + numbers[right] != target) {
if(numbers[left] + numbers[right] < target){
left++;
}else{
right--;
}
}
return new int[]{left + 1, right + 1};
}

二分搜索实现:$O(nlog(n))$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int[] twoSum(int[] numbers, int target) {
for (int i = 0; i < numbers.length; i++) {
int val = target - numbers[i];
int index = binarySearch(numbers, i + 1, numbers.length - 1, val);
if (index != -1) {
return new int[] { i + 1, index + 1 };
}
}
return null;
}

public static int binarySearch(int[] arr, int start, int end, int val) {
while (start <= end) {
int mid = start + (end - start) / 2;
if (arr[mid] == val) {
return mid;
} else if (arr[mid] > val) {
end = mid - 1;
} else {
start = mid + 1;
}
}
return -1;
}

复杂度

基本题目要求都是1s内完成。根据不同复杂度对应的n值

复杂度 N值
$O(log(N))$ $10^{20}$
$O(N^{\frac{1}{2}})$ $10^{12}$
$O(N)$ $10^6$
$O(N^2)$ $10^3$
$O(N^3)$ $100$
$O(N^4)$ 50
$O(2^N)$ 20
$O(N!)$ 10

数组复制

Arrays.copyOf:由Arrays类提供

1
2
public static <T> T[] copyOf(T[] original, int newLength)
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType)
1
2
int[] original = {1, 2, 3, 4, 5};
int[] copied = Arrays.copyOf(original, 3);

其中original可以是泛型类数组:

1
2
3
void method(T[] arr){
T[] newArr = Arrays.copyOf(arr, arr.length)
}

System.arraycopy: 是本地方法,更底层效率更高。

1
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
1
2
3
4
int[] original = {1, 2, 3, 4, 5};
int[] copied = new int[3];
System.arraycopy(original, 2, copied, 0, 3);
// copied = {3, 4, 5}

数组排序

对象数组:让对象实现Comparable接口,并重写compareTo()方法。

基本类型数组:向Arrays.sort传入Comparator接口

1
2
3
4
5
6
7
8
int[][] points;
Arrays.sort(points, (o1, o2) -> {
if (o1[1] != o2[1]) {
return Integer.compare(o1[1], o2[1]);
} else {
return Integer.compare(o1[0], o2[0]);
}
});

最小(大)堆

最小堆的主要操作:

  • 将已有数组进行堆化
  • 向最小堆中的添加,删除操作
  • 将最小堆的数组排序化

堆化已有数组

从最后一个非叶子节点开始向下调整。

向下调整:

  • 选择左右儿子中值更大且大于本节点值的节点,与本节点交换位置(本节点值大于左右儿子时不需要操作)
  • 交换后,将交换后的儿子节点继续执行向下调整

向上调整:

  • 将儿子元素与父亲比较,如果儿子元素大于父亲元素,则与父亲元素交换
  • 交换后,堆交换后的父亲节点继续执行向上调整

在一个完全二叉树中,最后一个非叶子节点是 $n/2-1$,对所有的非叶子节点执行向下调整。完成后数组elements即成为最小(大)堆。

添加元素:将元素添加到数组最后(完全二叉树最后一个叶子节点)然后对该元素执行向上调整

删除元素:删除根节点元素(数组第一个元素),将最后一个元素赋值给根节点并对根节点执行向下调整

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
import java.util.Arrays;

public class minHeap<T extends Comparable<T>> {
private T[] elements;

public minHeap(T[] arr) {
elements = Arrays.copyOf(arr, arr.length);
// 对所有非叶子节点执行向下调整
for (int i = arr.length / 2 - 1; i >= 0; i--) {
downMove(elements, i, elements.length - 1);
}
}
//向下调整
public void downMove(T[] elements, int start, int end) {
int parent = start;
for (int i = start * 2 + 1; i <= end; i = i * 2 + 1) {
//选择左右儿子中较大的
if (i < end && elements[i].compareTo(elements[i + 1]) < 0) {
i++;
}
//如果较大的儿子比父亲值还大,需要交换位置,并对儿子节点继续向下调整,否则退出循环
if (elements[i].compareTo(elements[parent]) > 0) {
T tmp = elements[parent];
elements[parent] = elements[i];
elements[i] = tmp;
parent = i;
} else {
break;
}
}
}

}

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
package ds;

import java.util.Arrays;

public class minHeap<T extends Comparable<T>> {
private T[] elements;
private int size;

public minHeap(T[] arr, int size) {
size = arr.length;
elements = Arrays.copyOf(arr, Math.max(arr.length, size));
// 对所有非叶子节点执行向下调整
for (int i = arr.length / 2 - 1; i >= 0; i--) {
downMove(elements, i, elements.length - 1);
}
}

// 向下调整
private void downMove(T[] elements, int start, int end) {
int parent = start;
for (int i = start * 2 + 1; i <= end; i = i * 2 + 1) {
// 选择左右儿子中较大的
if (i < end && elements[i].compareTo(elements[i + 1]) < 0) {
i++;
}
// 如果较大的儿子比父亲值还大,需要交换位置,并对儿子节点继续向下调整,否则退出循环
if (elements[i].compareTo(elements[parent]) > 0) {
T tmp = elements[parent];
elements[parent] = elements[i];
elements[i] = tmp;
parent = i;
} else {
break;
}
}
}

private void upMove(T[] elements, int start) {
int child = start;
for (int i = (start - 1) / 2; i > 0; i = (i - 1) / 2) {
if (elements[child].compareTo(elements[i]) > 0) {
T tmp = elements[child];
elements[child] = elements[i];
elements[i] = tmp;
child = i;
}
}
}

// 添加元素至最后一个位置并向上调整
public void add(T element) {
elements[size] = element;
upMove(elements, size);
size++;
}

// 最后一个元素覆盖第一个元素并向下调整
public void remove() {
elements[0] = elements[size - 1];
size--;
downMove(elements, 0, size);
}

public T[] heapSort() {
sortArrayFromHeap(elements);
return elements;
}

private void sortArrayFromHeap(T[] elements) {
for (int i = elements.length - 1; i > 0; i--) {
T tmp = elements[0];
elements[0] = elements[i];
elements[i] = tmp;
downMove(elements, 0, i - 1);
}
}

public static void main(String[] args) {
int[] arr = { -4, 0, 7, 4, 9, -5, -1, 0, -7, -1 };
Integer[] arrInteger = Arrays.stream(arr).boxed().toArray(Integer[]::new);
minHeap<Integer> m = new minHeap<>(arrInteger, arrInteger.length + 10);
m.add(10);
int[] res = Arrays.stream(m.elements).mapToInt(Integer::intValue).toArray();
System.out.println(Arrays.toString(res));
}
}

  • Quokka.js

  • Import Cost

  • Color Highlight

  • CSS Peek

  • REST Client

  • Live Server

  • JSON

  • json2ts

  • Path Intellisense

  • Path Autocomplete

  • Npm Intellisense

  • NPM

  • NPM-Scripts

  • JavaScript (ES6) code snippets

  • Tailwind CSS IntelliSense

  • ESLint

  • Prettier - Code formatter

  • Nextjs snippets

  • Simple React Snippets

  • Git Graph

  • Hex Editor

  • Code Runner

  • IntelliJ IDEA Keybindings

  • Material Icon Theme

  • Atom One Dark Theme

0%