Mybatis
缓存机制
MyBatis 提供了两级缓存来满足不同的需求。
-
一级缓存(本地缓存):
- 作用范围: 一级缓存是在SqlSession的生命周期内有效,也就是说,每个SqlSession拥有独立的一级缓存。
- 默认开启: 一级缓存在MyBatis中默认是开启的,无需额外配置。
- 特点: 当执行查询操作时,查询的结果会被缓存在当前SqlSession中。如果再次执行相同的查询,MyBatis会首先尝试从缓存中获取数据,而不再访问数据库。
- 自动刷新: MyBatis会在执行insert、update、delete等写操作时自动清空一级缓存,以保持数据的一致性。
-
二级缓存(全局缓存):
-
作用范围: 二级缓存是在多个SqlSession之间共享的,即多个SqlSession可以共享同一个二级缓存。
-
配置开启: 二级缓存需要手动配置开启,需要在映射文件的<mapper>标签下添加<cache>元素。
xml<cache eviction="LRU" <!-- 缓存回收策略 --> flushInterval="60000" <!-- 自动刷新间隔(毫秒) --> size="1024" <!-- 缓存对象数量 --> readOnly="true"> <!-- 是否只读 --> -
特点: 二级缓存能够跨SqlSession共享查询结果,有效减少数据库访问次数。它的数据存储在全局范围的缓存中,可以由多个SqlSession访问。
-
缓存策略: 你可以根据需求选择不同的缓存回收策略(例如LRU-最近最少使用、FIFO等),以及配置刷新间隔、缓存的大小等参数。
-
自动刷新: MyBatis会在执行insert、update、delete等写操作时(保证数据一致性)、到达 flushInterval 指定时间时(防止缓存长时间陈旧)自动清空二级缓存
-
注意事项: 二级缓存可以缓存的对象必须实现 Serializable 接口,以确保能够正确序列化和反序列化。此外,MyBatis 的二级缓存是基于 Mapper(namespace)的,不是基于表的,因此对于关联数据的更新操作(如更新 A 表后影响 B 表的查询结果),需要手动清除相关的二级缓存或使用
@CacheNamespaceRef注解来建立缓存关系,以避免脏数据的问题。
-
延迟加载
所谓的延迟加载,其实就是一种优化方法,目标是为了在查数据库的时候,尽量不读取多余的数据,从而提高我们应用的表现和节约资源。
Mybatis仅支持association关联对象 和collection关联集合对象的延迟加载,association指的就是一 对一,collection指的就是一对多查询。
在Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。
假设有两张表,一张是订单表,另一张是商品表。每个订单下面可能有好几个商品。用延迟加载的话,当我们查一个订单的时候,MyBatis不会马上查出这个订单的所有商品,而是等到我们真的要用商品的数据时才去查。这样做就避免了在查订单的时候额外加载了一堆没用的商品。
#{}和${}的区别是什么?
# 和 $
我们先来看 # 的用法:
text
select * from t_user where name = #{param}
如果想执行相同的 SQL,用 $ 写法如下:
text
select * from t_user where name = '${param}'
#会进行预编译,而且会进行类型匹配,参数是在编译之后填充进去的,所以不需要加上单引号;$不会进行数据类型的匹配,只是单纯地进行字符串的拼接,所以要自己手动加上单引号,否则最终拼接出来的SQL语句就不对了。
这就跟JDBC中的Prepare Statement和Statement的区别是一样的,使用 # 就相当于使用 PrepareStatement(预编译的参数化执行器),而使用 $ 就相当于使用 Statement(直接拼接 SQL 的静态执行器)。
什么是SQL注入
在CURD的过程中,以SQL查询为例,假设我们想根据用户名和密码查询某一个用户是否存在,现在要让用户在前端根据用户名和密码进行查询。
现在假设我们后台的SQL语句如下:
text
select * from user where username='${username}' and password='${password}'
前端网页有两个输入框,一个是用户名输入框,另一个是密码输入框。假设用户老老实实地输入用户名为zhangsan,密码为123,那么最终生成的SQL语句如下:
text
select * from user where username='zhangsan' and password='123'
以上过程似乎很完美!
但是!!!要是有人不老实呢?
假设用户输入的用户名是 zhangsan' or 1=1 #,密码则随意给了个值 111,那么最终生成的 SQL语句可能就是下面这样了:
text
select * from user where username='zhangsan' or 1=1 #' and password='123'
实际执行的 SQL:
sql
select * from user where username='zhangsan' or 1=1
大家知道,# 在 SQL语句中的含义是注释,#后面的SQL内容是不执行的。所以这个SQL无论怎么执行,最终都会返回true。这就是所谓的SQL注入了。
区别
1、#{}为预编译处理;${}为字符串替换。
2、MyBatis在处理#{}时,会将sql中的#{}替换成?问号,调用PreparedStatement的set方法来赋值。
3、MyBatis在处理${}时,就是把${}替换为变量的值。
4、使用的#{}可以有效地防止SQL注入风险,提高系统安全性。
在 MyBatis 中,99% 的查询都应该使用
#{}。所有用户输入的参数都必须使用#{}只有在极少数需要动态 SQL 结构(表名、排序字段、列名)且参数是程序已知的、安全的值的情况下,才考虑使用
${}。
动态SQL
假设你有一个搜索页面,用户可以根据不同的条件来搜索商品,比如商品名、价格范围和分类。使用动态SQL,你可以构建一个灵活的查询语句,只在用户提供相关条件时包含这些条件。
在MyBatis中,你可以使用<if>、<choose>、<when>、<otherwise>等标签来构建动态SQL。
以下是一个简单的例子,假设你要根据用户的选择来动态构建查询语句:
xml
<select id="searchProducts" resultType="Product">
SELECT * FROM products
<where>
<if test="productName != null">
AND name = #{productName}
</if>
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND price <= #{maxPrice}
</if>
<if test="category != null">
AND category = #{category}
</if>
</where>
</select>
在这个例子中,如果用户输入了商品名、价格范围或分类,对应的条件会被包含在查询语句中。如果用户没有提供某个条件,那么相应的<if>块就会被忽略,从而构建出适合的查询语句。
Spring
IOC 与 DI
- Spring的IOC,翻译过来,叫
控制反转。 指的是在Spring中使用工厂模式,为我们创建了对象,并且将这些对象放在了一个容器中,我们在使用的时候,就不用每次都去new对象了,直接让容器为我们提供这些对象就可以了。 这就是控制反转的思想。 - 而DI,翻译过来,叫
依赖注入。 那刚才提到,现在对象已经交给容器管理了,那程序运行时,需要用到某个对象,此时就需要让容器给我们提供,这个过程呢,称之为依赖注入。
那如何将一个对象,讲给IOC容器管理呢?
- 那现在项目开发,都是基于Springboot构建的项目,所以呢,声明bean对象,我们只需要在对应的类上加上注解就可以了。 比如:
- 如果是controller层,直接在类上加上 @Controller 或 @RestController 注解。
- 如果是service层,直接在类上加上 @Service 注解。
- 如果是dao层,直接在类上加上 @Repository 注解。当然现在基本都是Mybatis 或 MybatisPlus,所以这个注解很少用了,都用的是 @Mapper 注解。
- 如果是一些其他的工具类、配置类啊,我们可以通过 @Component 、 @Configuration 来声明。
那如何完成依赖注入操作呢 ?
依赖注入的方式比较多,我们可以使用构造函数注入 或 成员变量输入,也是使用对应的注解就可以了。
常用的注解有两个: @Autowired 和 @Resource 注解。 那 @Autowired 默认是根据类型注入,而 @Resource 注解默认是根据名称注入。
AOP
AOP面向切面编程, 其实就是动态地对类方法做增强. 可以在方法执 行的前后做一些事情, 能够将通用操作(例如事务处理、日志管理、 权限控制等)封装起来.
你们项目中有没有使用到AOP?
我们当时在后台管理系统中,就是使用aop来记录了系统的操作日志
主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式、请求参数、当 前操作人、操作时间、返回值等信息,全部记录下来,保存在数据库中。
AOP的底层是如何实现的?
AOP 的底层实现是动态代理技术,Spring 默认使用 JDK 动态代理(当目标类实现接口时),当目标类没有实现接口时自动切换到 CGLIB 动态代理。
JDK 动态代理基于接口,性能更好,是 Spring 的默认选择;CGLIB 动态代理基于类的继承,当目标类没有实现接口时使用。
Spring容器中的单例bean是线程安全的吗?
"Spring 容器中的单例 Bean 并不自动是线程安全的!"
单例指的是容器中只存在一个 Bean 实例,但这个实例可能被多个线程同时访问,如果 Bean 有可变状态(实例变量),则存在线程安全问题。
比如:
java
@Service
public class CounterService {
private int count = 0; // 可变状态
public void increment() {
count++; // 非线程安全操作
}
}
为了确保单例Bean的线程安全性,可以采取以下几种方式:
- 无状态 Bean,让 Bean 不包含任何状态(无实例变量),所有操作只依赖于输入参数。
- 使用线程安全的集合,如果必须维护状态,使用线程安全的集合类。
- 使用同步机制(synchronized) ,在关键方法或代码块上使用
synchronized。 - 使用 ThreadLocal(每个线程独立状态),为每个线程创建独立的变量副本。
- 使用原型作用域(@Scope("prototype")),即每次使用创建一个新的实例
Spring Bean的作用域如何设置,常见的取值有哪些 ?
Spring Bean的作用域可以通过 @Scope 注解来设置。常见的取值如下:
- singleton :这种bean范围是默认的,这种范围确保不管接受到多少个请求,每个容器中同一个名称的bean只有一个实例,也就是单例的
- prototype :这种范围,表示非单例的。也就是说每一次用到的bean都是一个新的 。
- request :同一个请求,使用的是同一个bean。会为每一个来自客户端的请求都创建一个实例,在请求完成以后, bean会失效并被垃圾回 收器回收 。
- session:与request 请求范围类似,确保每个session会话范围内,是同一个实例,在session过期后, bean会随之失效
虽然,bean作用域可以设置这些值,但是在项目开发中,绝大部分的bean都不会添加这个 @Scope 注解,也就是说默认都是用的是单例的 bean。
Spring容器的bean什么时候初始化的?
- 如果是单例的bean,默认是Spring容器启动的时候,就完成bean的初始化操作,那这是默认情况,我们可以通过 @Lazy 注解来延迟bean 的初始化,延迟到第一次使用的时候。
- 而如果是非单例的bean(也就是prototype),则是在每次使用这个bean的时候,都会重新实例化一个新的bean。
Spring中的事务是如何实现的?
spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。 而如果我们进行项目开发,不存在分布式事务问题,我们就可以直接使用Spring提供的 @Transactional 注解来控制事务即可。
**在开发中,有没有遇到事务失效的场景? **
-
第一个,如果业务方法上try、catch处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了抛出去就行。
java@Service public class UserService { @Transactional public void saveUser(User user) { try { // 业务逻辑 userRepository.save(user); // 模拟异常 int i = 1 / 0; } catch (Exception e) { // 处理异常,但没有重新抛出 System.out.println("异常已处理: " + e.getMessage()); // 事务不会回滚! } } }原因:事务通知只有捕捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理了异常,事务通知无法知悉;
解决:在catch块中添加throw e抛出;
-
第二个,如果方法抛出检查异常,如果报错也会导致事务失效,因为默认spring事务管理只会针对于RuntimeException进行回滚。那这个呢,就可以在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception.class,这样别管是什么异常,都会回滚事务。
java@Service public class UserService { @Transactional public void saveUser(User user) throws Exception { userRepository.save(user); throw new Exception("手动抛出检查异常"); // 检查异常 } }原因:Spring默认只会回滚非检查异常;
解决: @Transactional(rollbackFor = Exception.class) // 关键:指定回滚所有异常
-
第三个,是我早期开发中遇到的一个,如果业务方法上不是public修饰的,也会导致事务失效。
java@Service public class UserService { @Transactional void saveUser(User user) { // 问题:不是public方法 userRepository.save(user); } }原因:Spring为方法创建代理、添加事务通知,前提条件都是该方法是public的;
解决:改为public;
什么是事务的传播行为 ?
Spring的事务传播行为,指的是两个被事务控制的方法,相互调用的过程中,到底是加入到已存在的事务,还是创建一个新的事务,控制的是 这个事儿。 我们设置事务的传播行为,可以通过 @Transactional 注解的propagation属性来设置,可取值有很多啊,但是常见的就只有两个:
- REQUIRED:也是默认值,表示如果没有事务,就新建一个事务,如果有事务,就加入到已存在的事务中。
- REQIRES_NEW:表示需要一个新的事务,无论当前环境是否存在事务,都会开启一个新的事务。
Spring框架中的常用注解
- 第一类是:声明bean,有@Component、@Service、@Repository、@Controller
- 第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse
- 第三类是:设置作用域 @Scope
- 第四类是:spring配置相关的,比如@Configuration,@ComponentScan 和 @Bean
- 第五类是:跟aop相关做增强的注解 @Aspect,@Before,@After,@Around,@AfterReturning,@AfterThrowing,@Pointcut
SpringMVC
MVC分层是什么
MVC全名是Model View Controller, 模型(model), 视图(view), 控制 器(controller). 它是一种软件设计规范,可以用一种业务逻辑、数 据、界面显示分离的方法组织代码.
- 视图(view): 为用户提供使用界面,与用户直接进行交互。
- 模型(model): 模型分为两类, 一类称为数据承载Bean, 一类称为 业务处理Bean。所谓数据承载Bean是指实体类(如:User类), 专门为用户承载业务数据的;业务处理Bean则是指Service或 Dao对象,专门用于处理用户提交请求的。
- 控制器(controller): 用于将用户请求转发给相应的Model进行处 理,并根据Model的计算结果向用户提供相应响应。它使视图与模型分离。
SpringMVC的执行流程

- 客户端将请求发送给前端控制器DispatcherServlet,这是一个调度中心
- 前端控制器将请求发送给处理器映射器HandlerMapping,处理器映射器根据路径找到方法的执行链,返回给前端控制器。
- 前端控制器将方法的执行链发送给处理器适配器HandlerAdapter,处理器适配器根据方法类型找到对应的处理Handler(这个 Handler指的是我们自己写的Controller)
- Handler完成对用户请求的处理后,会返回一个ModelAndView 对象给前端控制器。
- 前端控制器DispatcherServlet将结果发送给视图解析器ViewReslover,视图解析器找到视图文件位置。
- 视图渲染数据并将结果显示到客户端。
现在主流的开发方式是前后端分离,后端不会在管View了(不再负责 渲染页面). View由前端框架(Vue,React等)来处理. 后端只会给前 端返回JSON数据. 前端渲染数据到View上
Handler完成对用户请求的处理后,会将返回转为JSON给 前端控制器
前端控制器把JSON给前端, 前端框架(Vue,React等)来渲染数据.
SpringMVC常见的注解有哪些?
嗯,这个也很多的 。有@RequestMapping:用于映射请求路径; @RequestBody:注解实现接收http请求的json数据,将json转换为java对象; @RequestParam:指定请求参数的名称; @PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数;@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。@RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping这些。
SpringMVC的拦截器
拦截器的应用场景还是很多的,比如在项目中,我们基于拦截器实现**权限验证(登录检查)、请求日志记录(可获取请求参数、执行时间)**等功能。 在SpringBoot项目拦截器的使用分为两步进行:
- 第一步呢,需要定义一个类实现HandlerInterceptor接口,然后再实现接口中的方法,比如:preHandle、postHandle、afterCompletion。
- 第二步呢,就是需要定义一个配置类,然后实现WebMvcConfigure,然后在这个配置类中配置拦截器,指定拦截器的拦截路径、排除哪些路径 等信息。
你说的这些个功能,过滤器好像也能干,那拦截器Interceptor 与 过滤器Filter有什么区别?
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
- 拦截范围不同:Filter 是 Servlet 容器的过滤机制 ,会拦截web服务器中的所有资源,而Interceptor 是 Spring MVC 的拦截机制,只会拦截Spring环境的资源,主要就是Controller。
- 执行顺序不同:首先执行过滤器Filter,然后再执行拦截器Interceptor。
SpringMVC怎么处理异常?
SpringMVC的异常处理,就比较简单了,可以直接使用Spring MVC中的全局异常处理器对异常进行统一处理,此时在我们的三层架构中,都不 需要处理异常了,如果运行过程中出现异常,最终会被全局异常处理器捕获,然后返回统一的错误信息。 开发一个全局异常处理器需要使用到两个注解:@RestControllerAdvice 、@ExceptionHandler。
@RestControllerAdvice加在全局异常处理器的这个类上,而@ExceptionHandler加在异常处理的方法上,来指定这个方法捕获什么样的异常。 那在定义异常处理方法的时候,可以也定义多个,根据业务的需求,可以针对不同类型的异常,进行不同的处理。
SpringBoot
SpringBoot-bean管理
Bean扫描
-
标签
xml<context:component-scan base-package="com.lxx"/> -
注解
java@ComponentScan(basePackages = "com.lxx")
SpringBoot默认扫描启动类所在的包及其子包
Bean注册
@Component、@Controller、@Service、@Repository
如果要注册的bean对象来自于第三方(不是自定义的),是无法用 @Component 及衍生注解声明bean的
-
@Bean
如果要注册第三方bean,建议在配置类中集中注册
java@Configuration public class Config { @Bean public DataSource getDataSource(){ //如果方法的内部需要使用到ioc容器中已经存在的bean对象,那么只需要在方法上的参数位置声明即可,spring会自动的注入 return new DruidDataSource(); } } -
@Import
-
导入 配置类
java@Import(Config.class) @SpringBootApplication public class HeimaSpringbootBasicsApplication { public static void main(String[] args) { SpringApplication.run(HeimaSpringbootBasicsApplication.class, args); } } -
导入 ImportSelector 接口实现类
javapublic class LxxImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"com.alibaba.druid.pool.DruidDataSource"}; } }java@Import(LxxImportSelector.class) @SpringBootApplication public class HeimaSpringbootBasicsApplication { public static void main(String[] args) { SpringApplication.run(HeimaSpringbootBasicsApplication.class, args); } }ImportSelector接口实现类中selectImports方法的返回值不应该以硬编码的方式写死,这些类名一般是从配置文件中读取的。
在resources下新建lxx.imports
com.alibaba.druid.pool.DruidDataSource修改LxxImportSelector
javapublic class LxxImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { //读取配置文件的内容 List<String> imports = new ArrayList<>(); InputStream is = LxxImportSelector.class.getClassLoader().getResourceAsStream("lxx.imports"); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line = null; try { while((line = br.readLine())!=null){ imports.add(line); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (br!=null){ try { br.close(); } catch (IOException e) { throw new RuntimeException(e); } } } return imports.toArray(new String[0]); } }定义组合注解,优化@Import(LxxImportSelector.class)
java@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(LxxImportSelector.class) public @interface EnableLxxConfig { }java@EnableLxxConfig @SpringBootApplication public class HeimaSpringbootBasicsApplication { public static void main(String[] args) { SpringApplication.run(HeimaSpringbootBasicsApplication.class, args); } }
-
注册条件
SpringBoot提供了设置注册生效条件的注解 @Conditional
-
@ConditionalOnProperty:配置文件中存在对应的属性,才声明该bean
在application.yml中配置
ymlname: lxx添加@Conditional
java@Configuration public class Config { @Bean //如果配置文件中配置了指定的信息,则注入,否则不注入 @ConditionalOnProperty(name="name") public DataSource getDataSource(){ return new DruidDataSource(); } }结果:
去除application.yml中的name: lxx时: HikariDataSource (null) 加上name: lxx时: { CreateTime:"2023-12-21 14:00:17", ActiveCount:0, PoolingCount:0, CreateCount:0, DestroyCount:0, CloseCount:0, ConnectCount:0, Connections:[ ] } -
@ConditionalOnBean:当存在当前类型的bean时,才声明该bean
java@Configuration public class Config { @Bean public Student getStudent() { return new Student(); } @Bean // 如果ioc容器中存在Student则注入 @ConditionalOnBean(Student.class) public DataSource getDataSource() { return new DruidDataSource(); } } // 结果:注入DataSource -
@ConditionalOnMissingBean:当不存在当前类型的bean时,才声明该bean
java@Configuration public class Config { @Bean public Student getStudent() { return new Student(); } @Bean // 如果ioc容器中不存在Student则注入 @ConditionalOnMissingBean(Student.class) public DataSource getDataSource() { return new DruidDataSource(); } } // 结果:没有注入DataSource -
@ConditionalOnClass:当前环境存在指定的这个类时,才声明该bean
java@Configuration public class Config { @Bean //如果当前环境中存在Student类,则注入,否则不注入 @ConditionalOnClass(Student.class) public DataSource getDataSource(){ return new DruidDataSource(); } }
前置知识
ApplicationContextInitializer:IOC容器创建完之后执行,可以对上下文环境做一些操作,例如运行环境属性注册等
ApplicationListener:监听容器发布的事件,运行程序员执行自己的代码,完成事件驱动开发,它可以监听容器初始化完成,初始化失败等事件,通常情况下可以使用监听器加载资源,开启定时任务等
BeanFactory:Bean容器的根接口,提供Bean对象的创建、配置、依赖注入等功能
BeanDefinition:用于描述Bean,包括Bean的名称,Bean的属性,Bean的行为,实现的接口,添加的注解等等。Spring中,Bean在创建之前,都需要封装成对应的BeanDefinition,然后根据BeanDefinition进一步创建Bean对象
BeanFactoryPostProcessor:Bean工厂后置处理器,当BeanFactory准备好之后(Bean初始化之前)会调用该接口的postProcessBeanFactory方法,经常用于新增BeanDefinition
Aware:感知接口,Spring提供的一种机制,通过实现该接口,重写方法,可以感知Spring应用程序执行过程中的一些变化。Spring会判断当前的Bean有没有实现Aware接口,如果实现了,会在特定的时机回调接口中对应的方法
InitializingBean/DisposableBean:初始化接口,当Bean被实例化好后,会回调里面的函数,经常用于做一些加载资源的工作;销毁接口,当Bean被销毁之前,会回调里面的函数,经常用于做一些释放资源的工作
BeanPostProcessor:Bean的后置处理器,当Bean对象初始化之前以及初始化之后,会回调该接口对应的方法(postProcessBeforeInitialization、postProcessAfterInitialization)
SpringBoot自动配置原理
- 在主启动类上添加了SpringBootApplication注解,这个注解组合了EnableAutoConfiguration注解
- EnableAutoConfiguration注解又组合了Import注解,导入了AutoConfigurationImportSelector类
- 实现selectImports方法,这个方法经过层层调用,最终会读取META-INF 目录下的 后缀名 为imports的文件,当然了,boot2.7以前的版本,读取的是spring.factories文件
- 读取到全类名了之后,会解析注册条件,也就是@Conditional及其衍生注解,把满足注册条件的Bean对象自动注入到IOC容器中
SpringBoot的启动流程
总:SpringBoot启动,其本质就是加载各种配置信息,然后初始化IOC容器并返回
分:在启动的过程中会做这么几个事情
- 首先,当我们在启动类执行SpringApplication.run这行代码的时候,在它的方法内部其实会做两个 事情
- 创建SpringApplication对象;
- 执行run方法。
- 其次,在创建SpringApplication对象的时候,在它的构造方法内部主要做3个 事情。
- 确认web应用类型,一般情况下是Servlet类型,这种类型的应用,将来会自动启动一个tomcat
- 从spring.factories配置文件中,加载默认的ApplicationContextInitializer和ApplicationListener
- 记录当前应用的主启动类,将来做包扫描使用
- 最后,对象创建好了以后,再调用该对象的run方法,在run方法的内部主要做4个 事情
- 准备Environment对象,它里面会封装一些当前应用运行环境的参数,比如环境变量等等
- 实例化容器,这里仅仅是创建ApplicationContext对象
- 容器创建好之后,会为容器做一些准备工作,比如为容器设置Environment,BeanFactoryPostProcessor-Bean工厂后置处理器,并加载主类对应的BeanDefinition
- 刷新容器,就是我们常说的referesh,在这里会真正的创建Bean实例
总:总结一下我刚说的,其实SpringBoot启动的核心就两步,创建SpringApplication对象以及run方法的调用,在run方法中会真正的实例化容器,并创建容器中需要的Bean实例,最终返回。
IOC容器的初始化流程
总:IOC容器的初始化,核心工作是在AbstractApplicationContext.refresh()方法中完成的
分:在refresh()方法中主要做了这么几件事
- 准备BeanFactory-Bean容器的根接口(提供Bean对象的创建、配置、依赖注入等功能),在这一块需要给BeanFactory设置很多属性,比如类加载器,Environment等
- 执行BeanFactoryPostProcessor-Bean工厂后置处理器,这一阶段会扫描要放入到容器中的Bean信息,得到对应的BeanDefinition(注意:这里只是扫描,不创建)
- 注册BeanPostProcessor-Bean的后置处理器,我们自定义的BeanPostProcessor就是在这一阶段被加载的,将来Bean对象实例化好后需要用到
- 启动tomcat
- 实例化容器中非懒加载的单例Bean,这里需要说的是,多例Bean和懒加载的Bean不会在这个阶段实例化,将来用到的时候再创建
- 容器初始化完毕后,再做一些扫尾工作,比如清除缓存等
总:简单总结一下,在IOC容器的初始化过程中,首先得准备执行BeanFactoryPostProcessor-Bean工厂后置处理器,其次得注册BeanPostProcessor-Bean的后置处理器,并启动tomcat,最后需要借助于BeanFactory完成Bean的实例化。
Bean的生命周期
总:Bean的生命周期总的来说有4个阶段,分别是创建对象,初始化对象,使用对象以及销毁对象,而且这些工作大部分都是交给Bean工厂的doCreateBean方法完成的
分:
-
首先,在创建对象阶段,先调用构造方法实例化对象,对象有了后会填充该对象的内容,其实就是处理依赖注入
-
其次,对象创建完毕后,需要做一些初始化的操作,在这里涉及到几个扩展点
- 执行Aware感知接口的回调方法
- 执行BeanPostProcessor-Bean的后置处理器的postProcessBeforeInitialization方法
- 执行InitializingBean接口的回调,在这一步如果Bean中有标注了@PostConstruct注解的方法,会先执行它
- 执行BeanPostProcessor-Bean的后置处理器的postProcessAfterInitialization方法
把这些扩展点都执行完,Bean的初始化就完成了
-
接下来,在使用阶段就是程序员从容器中获取该Bean使用即可
-
最后,在容器销毁之前,会先销毁对象,此时会执行DisposableBean接口的回调,在这一步如果Bean中有标注了@PreDestroy注解的方法,会先执行它
总:简单总结一下,Bean的生命周期分为4个阶段,其中初始化对象和销毁对象我们程序员可以通过一些扩展点执行自己的代码
在 Spring Boot 中,推荐使用
@PostConstruct和@PreDestroy注解代替 XML 配置的init-method和destroy-method,这是 Spring 官方推荐的 Java 标准方式。
Bean的循环依赖
总 :Bean的循环依赖指的是À依赖B,B又依赖A这样的依赖闭环问题,在Spring中,通过三个对象缓存区来解决循环依赖问题,这三个缓存区被定义到了DefaultSingletonBeanRegistry中,分别是singletonObjects(成品)用来存储创建完毕的Bean,earlySingletonObjecs(半成品)用来存储未完成依赖注入的Bean,还有SingletonFactories(用于生成半成品 Bean)用来存储创建Bean的ObjectFactory。假如说现在A依赖B,B依赖A,整个 Bean的创建过程是这样的
分:
-
首先,调用A的构造方法实例化A,当前的A还没有处理依赖注入,暂且把它称为半成品,此时会把半成品A封装到一个 ObjectFactory中,并存储到singletonFactories缓存区

-
接下来,要处理A的依赖注入了,由于此时还没有B,所以得先实例化一个B,同样的,半成品B也会被封装到ObjectFactory中并存储到singletonFactories缓存区

-
紧接着,要处理B的依赖注入了,此时会找到singletonFactories中A对应的ObjecFactory,调用它的getObject方法得到刚才实例化的半成品A (如果需要代理对象,则会自动创建代理对象,将来得到的就是代理对象) ,把得到的半成品A注入给B,并同时会把半成品A存入到earlySingletonObjects中,将来如果还有其他的类循环依赖了A,就可以直接从earlySingletonObjects中找到它了,那么此时 singletonFactories中创建A的ObjectFactory也可以删除了
-
至此,B的依赖注入处理完了后,B就创建完毕了,就可以把B的对象存入到singletonObjects中了,并同时删除掉 singletonFactories中创建B的ObjectFactory

-
B创建完毕后,就可以继续处理A的依赖注入了,把B注入给A,此时A也创建完毕了,就可以把A的对象存储到 singletonObjects中,并同时删除掉earlySingletonObjects中的半成品A
-
截此为止,A和B对象全部创建完毕,并存储到了singletonObjects中,将来通过容器获取对象,都是从singletonObejcts中获取

总:总结起来还是一句话,借助于DefaultSingletonBeanRegistry的三个缓存区可以解决循环依赖问题
三级缓存的必要性:AOP 代理的关键作用
在理解了三级缓存的核心作用和解决循环依赖的流程后,我们不禁要问:为什么 Spring 需要三级缓存,而不是两级或一级缓存呢?这其中的关键在于 AOP 代理的处理。
在 Spring 中,当一个 Bean 需要被 AOP 增强时(例如使用了@Transactional注解开启事务),Spring 会在初始化阶段生成代理对象。这个代理对象就像是给原始对象穿上了一层特殊的 "外衣",使其具备了额外的功能(如事务管理、日志记录等)。如果只有两级缓存(一级缓存存放成品 Bean,二级缓存存放半成品 Bean),在填充属性时,注入的将是原始对象,而不是代理对象。这就好比给一个人穿上了普通的衣服,而不是他需要的特殊 "外衣",最终会导致注入的对象与实际需要的对象不一致,引发运行时异常。
而三级缓存的存在,通过ObjectFactory延迟代理生成,巧妙地解决了这个问题。在存在循环依赖时,三级缓存中的工厂对象会根据实际情况动态生成代理对象,确保注入的是最终形态的 Bean。例如,当 A 被 AOP 增强时,工厂会生成代理对象并返回,而不是原始对象。这样在整个循环依赖的解决过程中,无论是 B 对 A 的依赖注入,还是最终 A 的初始化,使用的都是同一个代理对象,保证了对象的一致性和正确性。
Spring 的三级缓存机制通过巧妙的设计,不仅成功解决了循环依赖问题,还兼顾了 AOP 代理的处理,确保了 Spring 容器中 Bean 的正确创建和初始化,是 Spring 框架设计的精妙之处。
聊聊你对SpringBoot框架的理解
SpringBoot是现在Spring家族最为流行的子项目,因为采用原始的SpringFramework框架开发项目,配置起来非常的繁琐,所以在Spring的4.0 版本之后,Spring家族推出了SpringBoot框架,而Springboot就是来解决Spring框架开发繁琐的问题的,是用来简化spring框架开发的。
主要提供了这么三大块功能:
- starter起步依赖。springboot提供了各种各样的starter,在starter起步依赖中,就封装了常用的依赖配置,大大简化了项目引入依赖坐标的复杂度。
- 自动配置。 这也是springboot中最核心的功能,springboot可以根据特定的条件(当前环境是否引入对应的依赖、配置文件中是否有某个配置项、当前环境是否已经有了某个bean)来创建对象的bean,从而完成bean的自动配置。
- jar包方式运行。 springboot中内嵌了web服务器,所以我们开发的web项目,也可以直接打成一个jar包,直接基于java -jar 执行运行,非常的方便。
SpringBoot应用可以同时处理多少请求?
SpringBoot默认的内嵌容器是Tomcat,也就是我们的程序实际上是运行在Tomcat里的。所以与其说SpringBoot可以处理多少请求,到不如说Tomcat可以处理多少请求。
在 application.properties 或 yml 中,你可以看到这些配置:
server.tomcat.threads.max(最大线程数)- 默认值:200
- 含义: 这是 Tomcat 真正能同时执行业务逻辑的工作线程数。
- 类比: 银行柜台里正在办理业务的窗口数量。如果 200 个窗口都坐满了人,第 201 个人就需要等待。
server.tomcat.max-connections(最大连接数)- 默认值:8192 (对于 NIO 模式)
- 含义: 这是 Tomcat 能够保持监听的最大 TCP 连接数。
- 类比: 银行大厅里能容纳的总人数(包括正在办业务的和在椅子上排队等候的)。
- 注意: 即使 200 个线程都在忙,剩下的 7992 个连接依然可以连上来,只是在排队,不会报错。
server.tomcat.accept-count(最大排队数)- 默认值:100
- 含义: 当连接数超过 8192 后,操作系统(OS)还可以接受的排队请求。
- 类比: 银行大厅也站不下了,在银行门口露天排队的人数。如果这个排队也满了,新的请求会被拒绝(Connection Refused)。
理论上的并发极限
基于默认配置:
- 瞬时并发处理能力: 200 个请求(受
max-threads限制)。 - 最大容纳能力: 8192 + 100 = 8292 个请求。
如果并发请求数量低于server.tomcat.threads.max,则会被立即处理,超过的部分会先进行等待,如果数量超过max-connections与accept-count之和,则多余的部分则会被直接丢弃。
SpringCloud
微服务调用链场景:
- 用户请求先进SpringCloudGateway,在网关层配合Sentinel做限流熔断,避免突发流量把后端打爆;
- 网关要找服务地址就去Nacos查,拿到可用实例列表;
- 服务之间互相调用,比如A调用B用OpenFegin,同时通过LoadBalancer在多个实例里做负载均衡;
- 配置咋办,统一放在NacosConfig一起维护;
- 要是下游挂了,Sentinel还能出手走降级兜底,先保核心链路;
- 异步逻辑丢进SpringCloudStream,靠 RabbitMQ或RocketMQ来解耦、削峰;
- 运维和排查问题,用Sleuth + Zipkin或SkyWalking做链路追踪,配合SpringBootAdmin看监控;
- 分布式事务使用Seata,分布式锁使用Redisson
CAP定理
一致性:Consistency(一致性),用户访问分布式系统中的任意节点,得到的数据必须一致。
可用性:Availability (可用性),用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
分区容错:Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区;Tolerance(容错**)**:在集群出现分区时,整个系统也要持续对外提供服务
矛盾:
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此分区容错Partition Tolerance不可避免。
当节点接收到新的数据变更时,就会出现问题了:

如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那node01、node02与node03之间就会出现数据不一致。
也就是说,在P一定会出现的情况下,A和C之间只能实现一个。
BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
Basically Available (基本可用) :分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft State(软状态) : :在一定时间内,允许出现中间状态,比如临时的不一致状态。
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
解决分布式事务的思路
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:
-
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
-
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
Seata模式
Seata分布式事务的四种模式
- AT模式,默认,简单,需要增加undo_log表,生成反向SQL,性能高
- TCC模式,try confirm/cancel,三个阶段的代码都得自己实现,Seata只负责调度
- SAGA模式,长事务解决方案
- XA模式,分布式强一致性的解决方案,但性能低而使用较少。
负载均衡的意义
负载平衡能优化资源使用,最大化吞吐量,并避免任何单一资源的过载.
简单来说, 某个服务器只能承受100并发, 另外一个能承受200并发. 那么请求被转发到第一台服务器和第二台服务器的比例应该是1:2. 第二台服务器应该接受更多请求, 避免第一台服务接受更多请求产生过载.
负载均衡常见实现策略
- 简单轮询:将请求按顺序分发给后端服务器上,不关心服务器当 前的状态,比如后端服务器的性能、当前的负载
- 加权轮询:根据服务器自身的性能给服务器设置不同的权重,将 请求按顺序和权重分发给后端服务器,可以让性能高的机器处理 更多的请求
- 简单随机:将请求随机分发给后端服务器上,请求越多,各个服 务器接收到的请求越平均
- 加权随机:根据服务器自身的性能给服务器设置不同的权重,将 请求按各个服务器的权重随机分发给后端服务器
- 一致性哈希:根据请求的客户端ip、或请求参数通过哈希算法得 到一个数值,利用该数值取模映射出对应的后端服务器,这样能 保证同一个客户端或相同参数的请求每次都使用同一台服务器
- 最小活跃数:统计每台服务器上当前正在处理的请求数,也就是 请求活跃数,将请求分发给活跃数最少的后台服务器
什么是服务降级?什么是服务熔断?什么是服务限流?这三者又是什么关系?
先说人话版本:限流是门口保安,熔断是自动跳闸,降级是备用方案。
场景:
去年双十一,商品服务扛不住挂了。
然后订单服务调商品服务,一直超时,线程全卡在那等着。用户服务调订单服务,也超时,线程池也满了。十分钟不到,整条链路全崩了,首页都打不开。
用户请求
│
▼
┌─────────┐ 调用 ┌─────────┐ 调用 ┌─────────┐
│ 用户服务 │ ────────▶ │ 订单服务 │ ────────▶ │ 商品服务 │ ← 挂了
└─────────┘ └─────────┘ └─────────┘
│
▼
线程傻等超时
│
▼
线程池满了
│
▼
全完蛋
这就是雪崩。一个服务挂,拖死一串。
当时要是配好了限流熔断降级,至少不会全崩,顶多商品详情页看不了,首页和订单列表还能用。
限流
就是控制流量,别让太多请求进来把自己压死。
景区限流你懂吧?每天就放5万人进去,多了就别进了。系统也一样,你能抗1000 QPS,来了5000就得挡掉4000。
常见的限流算法,令牌桶和漏桶]用得最多。令牌桶就是系统按固定速率往桶里放令牌,请求来了先拿令牌,没令牌就拒绝。好处是能应对突发流量,桶里攒了一堆令牌的话,短时间能放过去更多请求。漏桶呢,请求先进桶,然后匀速流出,不管你进来多快,出去的速度是固定的。
超过100就直接返回"系统繁忙",不让请求进来。
熔断
下游挂了就别调了,快速失败,别傻等。
这名字来自电路里的保险丝,电流太大就熔断,防止把整个电路烧了。
熔断器有三个状态,正常的时候是关闭状态,请求正常过。失败多了就打开,打开之后请求直接失败,根本不调下游了。过一会进入半开状态,放几个请求试试水,好了就恢复正常,没好继续熔断。
举个例子吧。商品服务响应从100ms变成了5秒,可能是数据库慢查询啥的。
没熔断的话,每个请求都要等5秒才超时失败,线程都卡着,很快线程池就满了,后面的请求排队,最后整个服务卡死。
有熔断就不一样了。发现最近10个请求有5个超时了,直接触发熔断,后续请求压根不调商品服务,直接返回失败。等10秒后,放几个请求试试,好了就恢复,没好继续熔断。
关键就是快速失败,别让线程傻等。
降级
服务不行了,总得给用户一个交代。
降级就是Plan B。正常方案走不通,走备选方案。
比如商品服务挂了,商品详情页总不能白屏吧?返回个"商品信息加载中...",起码页面能展示。
或者返回之前缓存的数据,数据可能不是最新的,但总比没有强。
还有一种是功能降级。双十一的时候把退款功能直接关掉,所有资源保交易,这也是降级。
首页推荐位也是,推荐服务挂了就展示提前准备好的热门商品,用户根本感知不到。
三者啥关系
画个图就清楚了:
你的服务
│
请求进来 ──▶ [限流] ──▶ 业务处理 ──▶ [熔断] ──▶ 调下游
│ │
│ 超了 │ 下游挂了
▼ ▼
[降级] [降级]
返回兜底 返回兜底
限流在入口,控制进来多少流量,保护自己。熔断在出口,调别人之前判断一下,别人挂了就别调了。降级是兜底,不管哪里出问题,都得有个备选方案。
三个不是互斥的,是配合用的。一个请求进来,先过限流,然后处理业务,调下游前过熔断,任何环节出问题都走降级。