学习笔记
🍃Spring框架知识体系
00 分钟
2024-9-18
2024-9-19
type
status
date
summary
slug
tags
category
password
URL
icon
 

我们为什么需要使用Spring框架?

Spring框架推出的目的就是为了提升Java企业级开发的效率。在Spring出来之前,我们主要使用EJB进行企业级开发,但是EJB企业级开发存在许多限制:
  1. 对象生命周期的管理,EJB需要用户自己管理对象,不支持依赖注入,每次使用对象时,都需要自己New一个,容易造成资源浪费;
  1. 部署成本较高,EJB框架部署时往往需要完整的Java EE应用服务器(包括EJB容器、Web容器、Java消息容器等),即使当前部署不涉及该功能,也需要进行部署,造成资源浪费,且应用的启动速度较慢;
  1. 本地测试较复杂,我们使用Spring框架时,只通过@Test注解即可进行单元测试,但是使用EJB时,需要自己手动初始化EJB容器,增加了重复代码;
  1. Spring提供了强大的AOP能力,而EJB提供的拦截器能力有限;

Spring框架有哪些组件?

notion image

Core Container(核心容器)

核心容器模块是其他模块功能建立的基础,这个模块由Beans、Core、Context和SpEL这几个子模块组成。
Beans模块提供框架的基础能力,包括依赖注入和控制反转;
Core模块封装了底层常用能力,包括类型转换、资源访问和一些常用工具类;
Context上下文模块,建立在Beans和Core模块的基础上,主要负责Bean的生命周期管理等功能,ApplicationContext接口是上下文模块的焦点;
SpEL模块,提供了强大的表达式语言支持,支持访问和修改属性,支持从Spring容器中获取Bean等。我们在@Value注解中,可以使用#{}方式的SpEL表达式来进行取值。
 

Data Access/Integration(数据访问、集成)

数据访问、集成层包括JDBC、ORM、OXM、JMS和Transactions模块。
JDBC模块:提供了一个简化的API来执行SQL操作;与Spring事务管理集成,支持多数据源配置;将JDBC的检查异常转换为Spring的数据访问异常;
ORM模块:提供与流行的“对象-关系”映射框架无缝集成的API;与Spring事务管理集成。
OXM框架:提供Java对象和XML之间的抽象映射,支持将Java对象映射为XML数据,或者把XML数据转为Java对象。
JMS模块:提供消息生产和消费的功能,支持异步接收详细。
Transaction模块:提供了声明式事务和编程式事务的支持。支持对不同的事务API进行抽象,我们可以通过注解的方式或者编程的方式实现对事务的支持。
声明式事务
编程式事务
我们常用的Mybatis框架,底层是基于JDBC的API实现的,但是他也有一些ORM(对象关系映射)的特性。在JDBC上,MyBatis实现了对JDBC的轻量级封装,允许直接执行SQL,同时将返回结果映射为Java对象;在ORM上,它支持数据库表与Java对象的映射,支持关联对象的延迟加载,同时也支持一级、二级缓存。

Web模块

Spring的Web层包括Web模块、Servlet模块、WebSocket模块以及WebFlux模块。
Web模块:提供了常用的Web开发工具,比如多文件上传、使用Servlet监听器初始化IoC容器以及Web应用上下文;
Servlet模块:该模块包含SPring中MVC的视线。主要功能如下:
  1. DispatcherServlet,用于将请求路由到对应的处理器;
  1. 处理器映射,用于定义请求到处理器的映射;
  1. 控制器,提供了@Controller和@RequestMapping注解用于定义处理请求的方法;
  1. Restful API支持,通过@RestController注解支持Restful API开发。
WebSocket模块:用于提供一种双全工的通信方式,开发者只需要实现相应的接口逻辑即可实现WebSocket接口。

AOP、Aspects、Instrumentation和Messaging

AOP:提供了成熟的面向切面编程的实现,可以在切面中做一些与业务无关的逻辑,比如日志记录,访问统计等。
Aspects:提供了与AspectJ框架的集成,AspectJ提供了一个全面且强大的AOP工具。
Instrumentation:提供了类工具的支持和类加载器的实现,可以在特定的应用服务器中使用。
messaging 模块:Spring 4.0 以后新增了消息(Spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。

Test模块

Spring支持Junit和TestNG测试框架,而且还额外提供一些基于Spring的测试功能。

Spring配置方式演化

notion image
Spring配置的核心思想是“约定大于配置”。
在开始版本的Spring中,我们主要是通过XML文件进行配置。具体示例如下:
我们可能需要创建多个类似的xml文件,然后在服务启动的时候,将这些XML文件导入到上下文中。
然后,Spring开始支持基于注解的配置。开始的注解方式如下:
我们会新建一个配置文件,然后在这一个配置文件中将需要的Bean通过注解的方式进行声明;
再后来,Spring对注解支持的更加完善,我们也不需要再新建配置文件,而是直接在对应的Java对象上使用注解即可,即我们现在看到的使用方式。注意,使用这种方式,我们需要在服务启动时配置包扫描路径。

Spring核心-控制反转(IoC)

什么是控制反转?

Spring框架管理Bean的创建工作和Bean的整个生命周期,由用户控制转变为Spring框架控制,这个就被成为控制反转

什么是Spring bean?

Spring bean可以看作是一个具有某个功能的可以复用的组件,当我们想使用某个功能的时候,找到对应功能的bean就可以直接调用。

IoC能做什么?

将创建对象的工作交由框架来完成,同时查找和依赖注入的工作也由框架来完成,类与类之间的耦合不再那么紧密,用户也只需要关注具体的业务逻辑即可。

Ioc和DI之间是什么关系?

Ioc控制反转是一种思想,而DI依赖注入是一种实现方式。Spring框架可以在程序运行过程中在IoC容器中找到需要依赖的bean并注入到对应的类中。

Ioc配置有哪三种方式?

XML配置

将bean的信息配置在xml文件中,通过Spring加载XML文件为我们配置Bean。这种方式主要出现在比较早的项目中,或者引用的一些第三方包不支持注解,我们也需要通过XML文件的方式进行配置。
举例:
  1. 配置xxx.xml文件
  1. 声明命名空间和配置bean

Java配置

将bean的信息配置在Java的配置类中,这个配置类通过@Configuration注解进行声明,一个配置类中可以配置多个bean信息。
举例
  1. 创建一个Java配置类,并使用@Configuration注解声明。
  1. 在该类中使用添加Bean的创建方法,使用@Bean注解声明,声明的方法实现是如何创建这个Bean。

Java注解

通过在类上加注解的方式,声明这个类交由Spring进行管理。Spring会自动扫描被@Component、@Service、@Controller和@Repository注解声明的类,前提是需要配置好Spring的注解扫描器。
注意,对于一些第三方的类,由于无法添加注解,无法通过该方式实现Bean的管理,这种情况下,需要通过XML文件或者Java配置的方式实现。
举例
  1. 对类添加@Component相关的注解,比如@Controller,@Service,@Repository
  1. 在服务启动时,通过@ComponentScan("tech.pdai.springframework")或者new AnnotationConfigApplicationContext("tech.pdai.springframework")方式指定扫描的包路径。
如果不配置包路径,则会导致所有使用注解声明的类无法被Ioc容器管理,因此一些需要依赖注入的地方,会存在NPE的问题。注意,Spring AOP功能也无法使用,因为Spring AOP技术只能应用于被Ioc容器管理的类。

依赖注入的三种方式?

setter方式

调用类中的set方法注入依赖,我们常见的在XML文件中通过property注入就是通过setter方法注入的。
以下是一个XML配置文件中使用setter方法注入的一个示例。
本质上包含两步:
  1. 第一步,需要new UserServiceImpl()创建对象, 所以需要默认构造函数
  1. 第二步,调用setUserDao()函数注入userDao的值, 所以需要setUserDao()函数
 

构造函数注入

构造函数注入,在XML配置的方式中,是通过<constructor-arg>标识的。示例如下:
本质上是通过new UserService(UserDao userDao)方法实现的。

注解注入

即在类中依赖的字段上通过@AutoWried或者@Resource注解进行声明。以@AutoWried注解为例,AutoWired默认使用byType的方式,找到相同类型的bean进行注入;如果存在多个相同类型的bean,则再通过byName,即名字进行注入,如果有多个同名同类型的,则会抛出异常。注意,我们可以使用@Qualifier 注解选择bean的名称。

为什么推荐构造器方式注入?

简答:因为通过这种注入方式能够保证注入的组件不变,并且确保需要的依赖不为空。此外,构造器中注入的依赖总是能够保证是初始化完成的。
详解
依赖不可变:因为注入的依赖被final关键词修饰。
依赖不为空:因为实现了有参数的构造器,所以在创建这个Bean的时候,Spring容器会使用有参数的构造器,因此如果获取不到参数中的bean对象时,直接在初始化的时候就会报错,不会造成后续调用的时候发生NPE问题。
完全初始化的状态:这个可以跟上面的依赖不为空结合起来,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法),所以返回来的都是初始化之后的状态。

@AutoWired注解、@Resource注解和@Inject注解有何区别于联系?

@AutoWired是Spring实现的注解,默认是根据类型进行自动装配的,如果需要根据名称进行装配,则需要借助@Qualifier注解,可以作用在构造器、字段、方法、方法入参以及注解上;
@Resouce注解默认是通过名称进行装配的,可以作用在类、字段和方法上;
@Inject是基于类型进行装配的,如果需要指定名称,则需要结合@Named注解实现。可以作用在构造函数、方法和字段上。

总结

1、@Autowired是Spring自带的,@Resource是JSR250规范实现的,@Inject是JSR330规范实现的
2、@Autowired、@Inject用法基本一样,不同的是@Inject没有required属性
3、@Autowired、@Inject是默认按照类型匹配的,@Resource是按照名称匹配的
4、@Autowired如果需要按照名称匹配需要和@Qualifier一起使用,@Inject和@Named一起使用,@Resource则通过name进行指定。

我们要是设计实现一个IoC容器,应该怎么做?

先思考需要实现哪些功能?解析配置(XML文件配置、注解配置、Java配置),然后基于配置创建Class对象,然后基于Class对象再创建具体的实例。支持基于名称或者类型获取Bean。

Spring是如何实现Ioc容器的?

我们以XML配置文件为例,我们先将XML解析成Document类,然后通过解析Document类中的元素去创建BeanDefinitions类,然后将beanName和BeanDefinitions放入到ConcurrnetHashMap中,后面需要创建Bean实例的时候,基于beanName取出这个BeanDefinitions,此类中包含有类是否是单例、类的Class对象等信息,然后通过反射的方式创建Bean的实例,创建Bean实例所需的一些参数可以从BeanDefinitions中获取到。
对于一些单例模式的无参构造器的类,我们可以将这些类的示例缓存起来,后面使用时可以避免重复创建。
 

Spring是如何解决循环依赖问题的?

注意,Spring只是解决了单例对象中通过属性注入的循环依赖问题。
重点看下Spring中的三级缓存,Spring就是靠着三级缓存来解决上述问题的。因为是使用缓存解决的,所以有对类有两个要求,一个是单例模式,否则缓存对象就违反了类创建的初衷,第二是类的构造器必须是无参构造器,有参构造器每次构造的入参可能不一样,缓存数量较大,成本太高。
  • 第一层缓存:单例对象缓存池,已经实例化并且完成赋值,是成熟的类对象;
  • 第二层缓存:单例对象缓存池,存储的是实例化但尚未赋值的类对象,是半成熟的类对象;
  • 第三层缓存:单例工厂的缓存。
我们先回忆一下bean创建的过程:
  1. 调用类的构造器,创建实例;
  1. 对实例的属性进行注入(依赖注入);
  1. 初始化实例(执行实例的一些后置处理逻辑);
然后我们基于类的创建过程,描述一下三级缓存是如何解决单例模式下属性的循环依赖问题的:当我们创建一个实例时,先调用类A的构造方法,我们将类A的工厂放入三级缓存中,然后进行属性注入,如果发现有依赖的类B,我们就调用类B的构造器进行注入,然后把类B的单例工厂放入三级缓存;在进行类B的属性注入时,发现类B依赖类A,我们先去一级缓存中查找,没有的话再去二级、三级缓存中查询,在三级缓存中查询到后,我们拿到类A对象后,我们把类A对象的实例也放入到二级缓存中,这样,类B的初始化完成后,我们再继续进行类A的初始化,初始化完成后,再放入一级缓存中。

为什么使用三级缓存而不是二级缓存?

二级缓存在一些情况下也能解决循环依赖的问题,那我们为什么还要使用三级缓存呢?
  • 延迟代理创建:只在真正需要时才创建代理
  • 灵活性:允许在运行时决定是否需要创建代理
  • 性能优化:避免对不需要的bean创建代理

Spring中Bean的声明周期?

Spring只会接管单例模式Bean的声明周期管理,对于非单例模式的对象,创建后需要由开发者自行决定处理策略。

Spring AOP相关知识

AOP全称是面向切面编程,其有两种实现方式:静态代理动态代理。Spring AOP的实现原理是基于动态代理实现的(无论是JDK动态代理还是Cglib代理),而AspectJ则往往是通过静态代理,在编译期织入的。
下面介绍一下Spring AOP和AspectJ的区别与联系。
Spring AOP
AspectJ
在纯Java中实现
在纯Java中实现
不需要单独的编译过程
默认需要单独的ajc编译期
运行时代理
支持编译时、编译后以及加载时织入
仅支持方法级别的代理
更强大,可以代理方法、构造器、字段等
只能在Spring容器管理的bean上生效
可以在所有的类对象上生效
仅支持方法执行切入点
支持所有切入点
代理是由目标对象创建的, 并且切面应用在这些代理上
在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入
比 AspectJ 慢多了
更好的性能
易于学习和应用
相对于 Spring AOP 来说更复杂

Spring AOP实现代理的两种方式

Cglib动态代理

Cglib是一个强大的、高性能的代码生成包,它广泛被许多AOP框架使用,为他们提供方法的拦截。其底层是通过修改字节码文件来进行方法代理的。
notion image
notion image
在Spring中,默认使用JDK动态代理实现AOP技术。如果要被代理的对象没有实现接口,则会使用Cglib动态代理来实现AOP。
Cglib动态代理的实现逻辑是创建一个被代理类的子类,然后实现MethodInterceptor接口中的invoke方法,通过反射的方式最终去执行被代理的子类的真实逻辑。

JDK动态代理

 

Spring MVC的请求流程

notion image
 
上一篇
数据增强——在图片中添加遮挡物
下一篇
ThreadLocal里的变量一定是线程独享的吗?