[TOC]

写在前面

本文学习自:https://www.bilibili.com/video/BV1yT411H7YK

部分来源自网络

Spring

基本概念

spring基本概念:

  • IoC(Inversion of control)控制反转:

    • 使用对象时,由主动new产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转
  • Spring技术对IoC思想进行了实现

    • Spring提供了一个容器,称为IoC容器,用来充当IoC思想中的(外部
    • IoC容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在IoC容器中统称为Bean
  • DI(Dependency Injection)依赖注入

    • 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入
  • 目标:充分解耦

  • 使用IoC容器管理bean(Ioc)

  • 在IoC容器内将有依赖关系的bean进行关系绑定(DI)

  • 最终效果:

    • 使用对象时不仅可以直接从IoC容器中获取,并且获取到的bean已经绑定了所有的依赖关系

AOP核心概念:

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等在springAoP中,理解为方法的执行

  • 切入点(Pointcut):匹配连接点的式子

  • 在SpringAoP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法

    • 一个具体方法:com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法
    • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
  • 通知(Advice):在切入点处执行的操作,也就是共性功能在SpringA0P中,功能最终以方法的形式呈现

  • 通知类:定义通知的类

  • 切面(Aspect):描述通知与切入点的对应关系

Spring中的单例bean是线程安全的吗

通常情况下是线程安全的,线程安全的前提是无状态和不做修改,被Spring管理的bean是默认为单例模式的,即在整个应用程序中是全局共享一个的,一旦单例bean被初始化完成,它会被缓存起来,之后每次请求该bean时都会返回同一个实例。这确保了在整个应用程序中只有一个实例存在,因此不会发生竞争条件,从而保证了线程安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
@RequestMapping("/user")
public class UserController{

private int count; // 这个就是有状态的对象,存在线程安全问题

@Autowired
private UserService userService // 这个被spring所管理的bean是单例的,没有线程安全问题

@GetMapping("/getByld/fid;")
public User getByld(@PathVariable("id") Integer id){
count++:
System.out.println(count);
return userService.getByld(id);
}
}

⭐什么是AOP,你们的项目中有没有使用到AOP

AOP是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等

我们当时在后台管理系统中,就是使用AOP来记录了系统的操作日志

主要思路是这样的,使用AOP中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库

Spring中的事务是如何实现的

spring实现的事务本质就是AOP完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

aop工作流程:

  1. Spring容器启动

  2. 读取所有切面配置中的切入点

  3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点

    • 匹配失败,创建对象

    • 匹配成功,创建原始对象(目标对象)的代理对象

  4. 获取bean执行方法

    • 获取bean,调用方法并执行,完成操作
    • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

⭐Spring中事务失效的场景有哪些

第一个,如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了跑出去就行了,即在catch块中抛出运行时异常

第二个,如果方法抛出检查异常,如果报错也会导致事务失效,最后在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样别管是什么异常,都会回滚事务

第三,如果方法上不是public修饰的,也会导致事务失效

1.方法访问权限问题

只支持public

2.方法使用了final,static修饰后事务会失效

spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final或static修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

  • JDK动态代理:只适用于接口,代理类实现了目标对象的接口,并在方法调用时添加额外的功能(例如事务管理)。
  • CGLIB代理:基于子类的动态代理,适用于类。CGLIB通过创建目标类的子类并覆盖其中的方法来添加额外的功能。
  1. 方法使用final修饰时

当方法被final修饰时,意味着这个方法不能被子类重写。这对CGLIB代理的机制产生了影响,因为CGLIB是通过生成目标类的子类并覆盖方法来实现代理功能的。如果方法是final的,CGLIB无法覆盖这个方法,从而无法在方法前后添加事务处理逻辑。

  1. 方法使用static修饰时

static方法属于类而不是实例。AOP代理机制无论是基于JDK动态代理还是CGLIB,都无法拦截静态方法的调用,因为代理对象是实例级别的而不是类级别的。静态方法不在代理对象的调用路径中,因此Spring无法在静态方法前后添加事务处理逻辑。

3.方法内部调用

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class userService{

public void add(){
update();
}

@Transactional
public void update(){
//...
}
}

在上面这段方法中,add方法调用了update方法,相当于是用this去调用update方法,而使用this相当于new出来了一个对象,这个对象去调用update方法,这样的相当于new出来的对象不是springaop的代理对象,不具备aop的功能,故不能实现事务控制。

解决方式是:(将自己这个类注入进来,这样不会有循环依赖,因为spring的三级缓存解决了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class userService{

@Autowired
private userService proxy;

public void add(){
proxy.update();
}

@Transactional
public void update(){
//...
}
}

4.未被spring控制

例如:未加@Service等

1
2
3
4
5
6
7
8
9
10
11
12
//@Service
public class userService{

public void add(){
update();
}

@Transactional
public void update(){
//...
}
}

5.多线程调用

当多线程调用的时候,因为这样导致了方法不在同一个线程中,所以获取到的数据库连接也不一样,从而是不同的事务。

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚

6.表不支持事务

例如MySQL的Myisam 就不支持事务,需要换成innodb

7.事务未开启

springboot默认开启,但是spring需要进行设置

8.事务传播特性

事务传播特性选择了不支持事务

9.自己捕获了异常

用try catch捕获了,没有向上抛出去,Spring自然就捕获不到异常,则不会进行事务回滚

10.异常抛错了

默认的是rollbackFor = RuntimeException.class,如果抛了个比如是Exception的异常就捕获不到

11.自定义异常

同样是捕获不到对应的异常

12.嵌套

一个加了事务的方法A调用另一个加了事务的方法B,如果想实现方法B报错且方法A剩余部分不回滚,只能在方法B进行捕获异常,不让其往上抛异常

⭐Spring的bean的生命周期✏️

Bean的生命周期大致可以分为四个阶段:实例化、属性赋值、初始化、销毁

https://juejin.cn/post/7075168883744718856?searchId=20231231212710EF990A2551C424C4ECA6#heading-0

待补充。。。

https://www.bilibili.com/video/BV1584y1r7n6/?spm_id_from=333.337.search-card.all.click&vd_source=fa7ba4ae353f08f1d08d1bb24528e96c

⭐Springbean的循环依赖✏️

在Spring框架中,三级缓存是用于解决循环依赖问题的一个重要机制。循环依赖是指两个或多个bean互相依赖对方,导致在创建bean的过程中出现无限递归的问题。Spring通过引入三级缓存机制,优雅地解决了这个问题。

三级缓存简介

Spring的三级缓存包含以下三个级别:

  1. 一级缓存(singletonObjects)
    • 存储完全初始化好的单例bean
  2. 二级缓存(earlySingletonObjects)
    • 存储原始的早期半成品bean(已实例化未注入属性(未初始化)),通常是为了暴露这些bean以解决循环依赖。
  3. 三级缓存(singletonFactories)
    • 存储bean工厂对象,这些工厂对象可以在需要时生成早期bean的代理

解决循环依赖的过程

以下是Spring如何通过三级缓存解决循环依赖的具体步骤:

  1. 创建bean实例
    • Spring首先尝试创建bean的实例(仅实例化,还未初始化)。
    • 如果在实例化过程中检测到当前bean存在依赖其他bean,则会尝试从缓存中获取这些依赖bean。
  2. 从缓存中获取依赖
    • 一级缓存:首先尝试从一级缓存中获取依赖,如果找到则直接使用。
    • 二级缓存:如果一级缓存没有,则从二级缓存中获取。
    • 三级缓存:如果二级缓存也没有,则从三级缓存中获取。如果三级缓存中存在一个工厂对象,Spring将使用该工厂对象生成一个早期bean的代理,并放入二级缓存中。
  3. 解决循环依赖
    • 当一个bean A 依赖于 bean B,而bean B 又依赖于bean A时,Spring能够通过三级缓存提供一个bean的早期引用,从而打破循环依赖。
    • 在初始化bean A时,Spring会将bean A的早期引用放入三级缓存中。当bean B需要bean A时,Spring可以从三级缓存中获取bean A的早期引用,并继续初始化bean B。
  4. 完成bean的初始化
    • 最终,所有bean都会完成初始化并放入一级缓存中。三级缓存和二级缓存则用来处理初始化过程中出现的依赖问题。

Spring 的常见注解有哪些

第一类是:声明bean,有@Component、@Service、@Repository、@Controller

第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse

第三类是:设置作用域 @Scope

第四类是:spring配置相关的,比如@Configuration,@ComponentScan 和 @Bean

第五类是:跟aop相关做增强的注解 @Aspect,@Before,@After,@Around,@Pointcut

@Resourse 和 @Aotuwired 的区别

@Autowired和 @Resource 都是用来实现依赖注入的注解(在Spring/Spring Boot 项目中),但二者
却有着4点不同

  1. 来源不同: @Autowired 来自 Spring 框架,而 @Resource 来自于 (Java) JSR-250;
  2. 依赖查找的顺序不同: @Autowired 先根据类型再根据名称查询,而 @Resource 先根据名称再根据
    类型查询;
  3. 支持的参数不同: @Autowired 只支持设置1 个参数,而 @Resource 支持设置7个参数
  4. 依赖注入的用法支持不同: @Autowired 既支持构造方法注入,又支持属性注入和 Setter注入,而
    @Resource 只支持属性注入和 Setter 注入;

依赖注入

在Spring框架中,依赖注入的执行顺序是由Spring容器管理的。依赖注入通常在对象实例化之后、初始化之前进行。具体来说,构造器注入、setter注入和字段注入的执行顺序如下:

1. 构造器注入(Constructor Injection)

构造器注入是在对象实例化时进行的。Spring容器会在创建对象时,通过构造函数传递依赖对象。这意味着在调用构造函数时,依赖对象已经被注入。

1
2
3
4
5
6
7
8
9
@Component
public class ExampleService {
private final DependencyService dependencyService;

@Autowired
public ExampleService(DependencyService dependencyService) {
this.dependencyService = dependencyService;
}
}

2. 字段注入(Field Injection)

字段注入是在对象实例化之后、任何初始化方法(如@PostConstruct标注的方法)之前进行的。Spring容器会直接设置被注解的字段的值,而不通过任何setter方法。

1
2
3
4
5
@Component
public class ExampleService {
@Autowired
private DependencyService dependencyService;
}

3. Setter注入(Setter Injection)

Setter注入是在对象实例化之后、任何初始化方法之前进行的。Spring容器会通过调用setter方法来注入依赖对象。Setter注入发生在字段注入之后,因为它需要在对象实例化之后调用setter方法。

1
2
3
4
5
6
7
8
9
@Component
public class ExampleService {
private DependencyService dependencyService;

@Autowired
public void setDependencyService(DependencyService dependencyService) {
this.dependencyService = dependencyService;
}
}

注解注入(Annotation-based Injection)

对于注解注入的顺序,当使用@Autowired@Inject进行依赖注入时,Spring容器会遵循以下顺序:

  1. 构造器注入:首先处理构造器注入。这是对象实例化的一部分,构造器注入必须在对象创建时完成。
  2. 字段注入:接着处理字段注入。字段注入是在对象实例化之后、任何初始化方法之前进行的。
  3. Setter注入:最后处理setter注入。setter注入也是在对象实例化之后、任何初始化方法之前进行的,通常在字段注入之后。

示例

考虑一个类使用了这三种注入方式的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class ExampleService {
private final DependencyService dependencyService;
private AnotherService anotherService;

@Autowired
public ExampleService(DependencyService dependencyService) {
this.dependencyService = dependencyService;
}

@Autowired
private YetAnotherService yetAnotherService;

@Autowired
public void setAnotherService(AnotherService anotherService) {
this.anotherService = anotherService;
}
}

在这个例子中:

  1. 构造器注入:Spring容器首先调用构造函数ExampleService(DependencyService dependencyService)进行构造器注入。
  2. 字段注入:接着,Spring容器进行字段注入,注入yetAnotherService
  3. Setter注入:最后,Spring容器调用setter方法setAnotherService(AnotherService anotherService)进行setter注入。

总结

  • 构造器注入:对象实例化时进行。
  • 字段注入:对象实例化之后、初始化方法之前进行。
  • Setter注入:对象实例化之后、初始化方法之前进行,通常在字段注入之后。

SpringMVC

⭐SpringMVC执行流程

Spring MVC(Model-View-Controller)的执行流程如下:

  1. 接收请求:用户通过浏览器发送请求,Servlet 容器(如 Tomcat)接收请求。
  2. 分发请求:请求由 Servlet 容器转发给 DispatcherServlet(前端控制器)。
  3. HandlerMapping:DispatcherServlet 根据请求 URL 调用 HandlerMapping 来找到对应的处理器(Controller)。
  4. HandlerAdapter:DispatcherServlet 调用 HandlerAdapter 以适配处理器的执行。
  5. 处理请求:具体的处理器(Controller)执行逻辑处理,并返回一个 ModelAndView 对象。
  6. 视图解析:DispatcherServlet 使用 ViewResolver 将逻辑视图名解析为具体的 View(视图)。
  7. 渲染视图:View 渲染模型数据并生成最终的 HTML 响应。
  8. 返回响应:DispatcherServlet 将生成的 HTML 响应返回给 Servlet 容器,最终响应用户。

image-20240603201358007

SpringMVC常见的注解有哪些

@RequestMapping:用于映射请求路径;

@RequestBody:注解实现接收http请求的json数据,将json转换为java对象;

@RequestParam:指定请求参数的名称;

@PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数;

@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。

@RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping这些。

@RestController:@Controller + @ResponseBody

Spring MVC中的拦截器和过滤器的区别

  1. 两者都可以在请求过程中介入并可能阻止请求,但执行顺序不同:过滤器先于拦截器执行,且更底层,处理的是Servlet请求的前后过程;拦截器则在Controller层,更偏向业务逻辑,处理请求前后的工作。

  2. 过滤器依赖于Servlet容器的Filter接口,执行顺序由配置决定,而拦截器依赖于Spring MVC的HandlerInterceptor接口,执行顺序由Bean配置或@Order注解决定。

  3. 拦截器常用于实现业务相关的非必要功能,如身份验证、授权、性能监控等,而过滤器主要用于基础设施任务,如编码处理、请求参数处理和URL重定向。在日常业务开发中,通常使用拦截器即可满足需求,且其与Spring集成较好,方便业务处理。

SpringBoot

⭐Springboot自动配置原理

在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:

  • @SpringBootConfiguration

  • @EnableAutoConfiguration

  • @ComponentScan

其中@EnableAutoConfiguration是实现自动化配置的核心注解。

该注解通过@Import注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名

其实上面这一步就是利用的SPI机制来实现

在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。

一般条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。

SpringBoot自动配置流程.png

Springboot常见注解有哪些

Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 :

  • @SpringBootConfiguration: 组合了- @Configuration注解,实现配置文件的功能;
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
  • @ComponentScan:Spring:组件扫描

SpringBoot启动完成标志

在 Spring Boot 项目启动完成时,可以通过以下几个标志确认项目已经成功启动:

  1. 控制台日志:这是最常见的方式,控制台会打印出类似 “Started [ApplicationName] in [time] seconds” 的日志信息,表明应用已经启动完成。

  2. 内嵌的 Web 服务器端口绑定成功:如果你使用的是内嵌的 Web 服务器(如 Tomcat、Jetty 或 Undertow),当它们成功绑定到指定端口时,也表明应用已经启动完成。你可以在控制台日志中看到服务器启动并绑定到端口的日志信息。

  3. **Spring Boot ApplicationRunnerCommandLineRunner**:这两个接口可以用来执行一些在应用启动完成后的逻辑。当它们的 run 方法被调用时,表明 Spring Boot 应用已经启动并完成了所有初始化工作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class MyApplication implements CommandLineRunner {

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

    @Override
    public void run(String... args) throws Exception {
    System.out.println("Application has started successfully.");
    }
    }
  4. 注册的 Bean 已完全初始化:当所有的 Spring Beans 都已经初始化完成并且所有的 @PostConstruct 方法都已经执行完毕时,也可以认为应用已经启动完成。

  5. Spring Boot Actuator:如果你的应用集成了 Spring Boot Actuator,你可以通过访问 /actuator/health 端点来确认应用的健康状况。当这个端点返回 UP 状态时,表明应用已经启动并正常运行。

  6. 通过自定义事件监听器:你可以定义一个监听器来监听 ApplicationReadyEvent,这个事件在 Spring Boot 应用启动完成并且所有的初始化工作完成后触发。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import org.springframework.boot.context.event.ApplicationReadyEvent;
    import org.springframework.context.event.EventListener;
    import org.springframework.stereotype.Component;

    @Component
    public class StartupListener {

    @EventListener
    public void onApplicationReadyEvent(ApplicationReadyEvent event) {
    System.out.println("Application is ready to service requests.");
    }
    }

这些方式都可以帮助你确认 Spring Boot 项目是否已经启动完成,并且可以根据需要选择适合你项目的方式进行验证。

MyBatis

MyBatis执行流程

①读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件

②构造会话工厂SqlSessionFactory,一个项目只需要一个,单例的,一般由spring进行管理

③会话工厂创建SqlSession对象,这里面就含了执行SQL语句的所有方法

④操作数据库的接口,Executor执行器,同时负责查询和缓存的维护

⑤Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息

⑥输入参数映射(将Java对象转换为数据库的信息)

⑦输出结果映射(将数据库的信息转换为Java对象)

Mybatis是否支持延迟加载

是支持的,延迟加载的意思是:它允许在需要时才加载关联对象的数据,而不是在主查询时就将关联对象的数据全部加载进来。

Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false,默认是关闭的

延迟加载的例子:

假设我们有两个实体类:UserOrder,它们之间存在一对多的关联关系,即一个用户可以拥有多个订单。在这个示例中,User是主实体,Order是关联实体。

首先,定义 User 类:

1
2
3
4
5
6
7
public class User {
private Long id;
private String username;
private List<Order> orders; // 关联对象

// 省略构造方法和其他属性的getter和setter
}

然后,定义 Order 类:

1
2
3
4
5
6
7
public class Order {
private Long id;
private String orderNumber;
private User user; // 关联对象

// 省略构造方法和其他属性的getter和setter
}

UserMapper 中,我们可以定义一个方法,通过用户ID查询用户信息及其关联的订单信息:

1
2
3
public interface UserMapper {
User getUserWithOrders(Long userId);
}

在 MyBatis 的 XML 配置文件中,我们可以使用 ResultMap 配置来指定关联对象的映射关系:

1
2
3
4
5
6
7
8
9
10
<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order" resultMap="orderMap"/>
</resultMap>

<resultMap id="orderMap" type="Order">
<id property="id" column="order_id"/>
<result property="orderNumber" column="order_number"/>
</resultMap>

最后,在查询时,我们可以使用延迟加载,确保在需要时才加载关联对象的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
SqlSession sqlSession = // 获取 SqlSession 的方法,这里省略
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

User user = userMapper.getUserWithOrders(1L);

// 此时关联对象 orders 并未被立即加载,只有在访问 orders 时才触发加载
List<Order> orders = user.getOrders();

// 在这里,访问 orders 属性,会触发延迟加载,查询关联的订单数据
for (Order order : orders) {
System.out.println("Order ID: " + order.getId() + ", Order Number: " + order.getOrderNumber());
}
}
}

这个示例演示了通过 MyBatis 进行延迟加载,只在需要时才加载关联对象的数据,而不是在主查询时就将关联对象的数据全部加载进来。

延迟加载的底层原理知道吗

延迟加载在底层主要使用的CGLIB动态代理完成的

第一是,使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper

第二个是当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,再执行sql查询

第三个是获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了

Mybatis的一级、二级缓存用过吗

mybatis的一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存

关于二级缓存需要单独开启

二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储。

如果想要开启二级缓存需要在全局配置文件和映射文件中开启配置才行。

Mybatis的二级缓存什么时候会清理缓存中的数据?

当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。