Spring 学习笔记
概述
Spring 是一个企业级 J2EE 应用开发一站式解决方案,其提供的功能贯穿了项目开发的表现层、业务层和持久化层,同时,Spring 可以和其他应用框架无缝整合
Spring 的特性包括以下几个方面:
- 轻量:Spring 是一个轻量级的框架,其核心 JAR 包的大小均为 1MB 左右。从系统的资源使用上来说,Spring 也是一个轻量级的框架,在其运行期间只需少量的操作系统资源便能稳定运行
- 控制反转:Spring 的控制反转指一个对象依赖的其他对象将会在容器的初始化完成后主动将其依赖的对象传递给它,而不需要这个对象自己创建或者查找其依赖的对象。Spring 基于控制反转技术实现系统对象之间依赖的解耦。
- 面向容器:Spring 实现了对象的配置化生成和对象的生命周期管理,通过 Spring 的 XML 文件或者注解方式,应用程序可以配置每个 Bean 对象被创建和销毁的时间,以及 Bean 对象被创建的先后顺序和依赖关系
- 面向切面:Spring 提供了面向切面的编程支持,面向切面技术通过分离系统逻辑和业务逻辑来提高系统的内聚性。在具体的使用过程中,业务层只需关注并实现和业务相关的代码逻辑,而不需要关注系统功能(例如系统日志、事务支持)
- 模块化:Spring 是模块化的,应用程序在使用过程中可以根据需求引入模块(以 JAR 包依赖方式引入)来实现不同的功能
Spring 的核心 Jar 包
- Spring Core:Spring 的核心工具包
- Spring Beans:SpingIoC 的实现,通过 XML 配置文件或注解的方式实现对 Spring Bean 的管理
- Spring Context:Spring 上下文环境。用于对 Bean 关系的管理和推护等
- Spring Aspects:Spring 对 AspectJ 框架的整合和支持
- Spring Context Support:SpringContext 的扩展支持,用于支持 MVC 方面的功能
- Spring Expression Language:Spring 的表达式语言
- Spring Framework Bom:处理不同的项目依赖不同版本的 Spring 引起的版本冲突
- Spring JDBC:Spring 针对 JDBC 的封装
- Spring ORM:Spring 整合第三方 ORM 的实现,例如 Mybatis
- Spring Test:Spring 对 JUnit 等测试框架的支持
- Spring TX:Spring 提供的一致性声明式事务管理和编程式事务管理
- Spring Web:基于 Spring 构建 Web 应用开发所需的核心类
- Spring WebMVC:包含 SpringMVC 框架相关的所有类
SpringIoC 原理
SpringIoC(Inversion of Control)即“控制反转”,是一种设计思想,将对象的创建和对象之间依赖关系的维护交给容器来负责,以实现对象与对象之间的松耦合
Spring 通过一个配置文件或注解描述 Bean 和 Bean 之间的依赖关系,利用 Java 的反射功实例化 Bean 并建立 Bean 之间的依赖关系。
Spring 在启动时会从 XML 配置文件或注解中读取应用程序提供的 Bean 配置信息,并在 Spring Bean 容器中生成一份相应的 Bean 配置注册表,然后根据这张注册表实例化 Bean,装配好 Bean 之间的依赖关系,将 Bean 实例放入缓存池,Bean 缓存池采用 HashMap 实现
Spring 常用注解
1. 声明 Bean 相关
@Component
:组件,没有明确的角色@Service
:在业务逻辑层使用(service 层)@Repository
:在数据访问层使用(dao 层)@Controller
:在控制器层声明(controller 层)
2. 注入 Bean 相关
@Autowired
:Spring 框架特有的注解,默认按类型注入,也可结合@Qualifier
注解按名称注入。它会先根据类型进行匹配,如果有多个相同类型的 Bean,再根据@Qualifier
指定的名称来确定具体要注入的 Bean@Resource
:Java 标准的注解,默认按名称(byName)注入,若找不到名称匹配的 Bean,则按类型(byType)注入。它会先按名称匹配,如果没有找到匹配名称的 Bean,再按类型进行匹配@Inject
:Java 标准的注解,与@Autowired
类似,默认按类型(byType)注入,可结合@Named
注解按名称(byName)注入
3. 配置类相关注解
@Configuration
:声明当前类为配置类,相当于 xml 形式的 Spring 配置,其中内部组合了@Component
注解,表明这个类是一个 Bean@Bean
:注解在方法上,通常与@Configuration
搭配使用,声明当前方法的返回值为一个 bean,替代在 xml 中定义 Bean 的方式,从而更加灵活的配置 Bean 的创建过程,比如设置构造函数参数,调用方法进行初始化w@ComponentScan
:注解在类上,用于对 Component 进行扫描,通常与@Configuration
搭配使用,相当于 xml 中的定义扫描路径
4. AOP 相关注解
@Aspect
:声明一个切面@PointCut
:声明切点@After
:在方法执行之后执行@Before
:在方法执行之前执行@Around
:在方法执行之前与之后执行
5. SpringMVC 相关注解
@RequestBody
:用于将 HTTP 请求中的请求体数据绑定到方法的参数上,在使用 RESTful 风格的 API 时,客户端通常会以 JSON、XML 等格式在请求体中发送数据, 该注解可以方便地将这些数据转换为 Java 对象,供后端方法处理@RequestParam
:用于将 HTTP 请求中的查询参数绑定到控制器方法的参数上,能从 URL 的查询字符串或表单数据中获取参数值,并将其转换为方法参数所需的类型,如 GET 请求 URL 中的参数或 POST 请求表单中的参数@PathVariable
:用于将 HTTP 请求 URL 中的路径变量绑定到控制器方法的参数上,在 RESTful 风格的 API 设计中,经常需要在 URL 包含一些变量来表示资源的标识符等信息, 该注解可以方便地获取这些变量的值
Spring Bean 作用域
Spring 为 Bean 定义了五种作用域:
- Singleton:Singleton 是单例模式,当实例类型为单例模式时,在 SpringIoC 容器中只会存在一个共享的 Bean 实例,无论有多少个 Bean 引用它,都始终指向同一个 Bean 对象。该模式在多线程下是不安全的。Singleton 作用域是 Spring 中的默认作用域
- Prototype:Prototype 是原型模式,每次通过 Spring 容器获取 Prototype 定义的 Bean 时,容器都将创建一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态。因此,对有状态的 Bean 经常使用 Prototype 作用域,而对无状态的 Bean 则使用 Singleton 作用域
- Request:Request 指在一次 HTTP 请求中容器会返回该 Bean 的同一个实例,对不同的 HTTP 请求则会创建新的 Bean 实例,并且该 Bean 实例仅在当前 HTTP 请求内有效,在当前 HTTP 请求结束后,该 Bean 实例也将随之销毁
- Session:Session 指在一次 HTTP Session 中,容器会返回该 Bean 的同一个实例,对不同的 Session 请求则会创建新的 Bean 实例,该 Bean 实例仅在当前 Session 内有效,Session 每一次都会创建新的 Bean 实例,而不同的 Bean 实例之间不共享数据,请求结束,则 Bean 实例将随之销毁
- Global Session:Global Session 类似于 Session,不过仅在 Portlet Web 应用中使用。 portlet 规范定义了构成单个 Portlet Web 应用的所有 portlet 之间共享的全局会话的概念。如果不是 Portlet Web 应用,则与 Session 无异
Spring Bean 的生命周期
Spring Bean 的生命周期如下所示:
- 实例化:实例化一个 Bean
- 依赖注入:为 Bean 的属性设置值或对其他 Bean 的引用
- 初始化:根据配置加载执行
- 如果 Bean 实现了 BeanNameAware 接口,则会执行它实现的 setBeanName(String) 方法。该方法传递的参数是 Spring 配置文件中 Bean 的 id 值
- 如果 Bean 实现了 BeanFactoryAware 接口,则会执行它实现的 setBeanFactory(BeanFactory) 方法,该方法传递的参数是 Spring 工厂自身
- 如果 Bean 实现了 ApplicationContextAware 接口,则会执行 setApplicationContext(ApplicationContext) 方法,该方法传递的参数是 Spring 上下文
- 如果 Bean 关联了 BeanPostProcessor 接口,则会执行 postProcessBeforeInitialization(Object obj, String s) 方法,该方法在 Bean 初始化前调用,常用于定义初始化 Bean 的前置工作,比如系统缓存的初始化
- 如果 Bean 在 Spring 配置文件中配置了 init-method 属性,则会自动执行其配置的初始化方法
- 如果 Bean 关联了 BeanPostProcessor 接口,将会执行 postProcessAfterInitialization(Object obj, String s) 方法,至此,Bean 的初始化工作就完成了,应用程序可以开始使用Bean实例了
- 使用:应用程序使用 Bean
- 销毁:当容器关闭,则会将 Bean 销毁
- 如果 Bean 实现了 DisposableBean 接口,Spring 会在退出前调用实现类的 destroy 方法
- 如果 Bean 的 Spring 配置文件中配置了 destroy-method 属性,则在 Bean 销毁前会自动调用其配置的销毁方法
Spring Aware 接口
使用 Aware 接口能帮助 Bean 获取自身容器中的信息,这些接口实现的方法将在特定的事件发生后被回调
几个常用的 Aware 接口如下:
- BeanNameAware:获取 Bean 的名称
- BeanFactoryAware:获取当前 BeanFactory
- ApplicationContextAware:获取 ApplicationContext 容器
- ApplicationEventPublisherAware:获取用于发布事件的 ApplicationEventPublisher
SpringAOP 原理
SpringAOP 通过面向切面技术将与业务无关却为业务模块所共用的逻辑代码封装起来。以提高代码的复用率,降低模块之间的耦合度
SpringAOP 核心概念如下:
- 横切关注点:定义对哪些方法进行拦裁,以及在拦被后执行哪些操作
- 切面(Aspect):横切关注点的抽象
- 连接点(JoinPoint):指被拦截到的方法
- 切入点(PointCut):对连接点进行拦戳的定义
- 通知(Advice):拦截到连接点之后要执行的具体操作,分为前置通知、后置通知、成功通知、异常通知和环绕通知
- 目标对象:代理的目标对象
- 织入(Weave):将切面应用到目标对象并执行代理对象创建的过程
- 引入(Introduction):在运行期为类动态地添加一些方法或字段而不用修改类的代码
SpringAOP 有五种通知类型:
- 前置通知:在一个方法执行之前执行通知
- 后置通知:在一个方法执行之后执行通知,无论方法执行成功还是失败
- 成功通知:在一个方法执行成功之后执行通知,只有在方法执行成功时才执行
- 异常通知:在一个方法抛出异常时才执行
- 环绕通知:在拦截方法调用之前和之后分别执行
Spring 提供了 JDK 和 CGLib 两种方式来生成代理对象,把生成的代理对象放入容器。Spring 默认的代理对象生成策略:如果是目标类接口,则使用 JDK 动态代理技术,否则使用 CGLib 动态代理技术。CGLib 动态代理和 JDK 动态代理的区别:JDK 只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则只能通过 CGLib 创建动态代理来实现
基于 JDK 动态代理创建的代理类会实现了原来目标类的全部接口,并对接口定义的所有方法进行代理。当我们通过代理对象执行目标类的方法时,代理类会通过反射机制,回调 InvocationHandler 接口的 invoke 方法,并且这个代理类是 Proxy 类的子类
基于 CGLib 创建的代理类,其原理是采用了 ASM 字节码生成技术,直接对目标类的字节码进行操作,生成目标类的子类,并重写了目标类所有可以重写的方法。在重写的过程中,将切面逻辑织入到方法中。此时代理类是目标类的子类。并且生成的代理对象还有一个 target 属性指向原对象,使用原对象来调用原方法的逻辑
class UserServiceProxy extends UserService {
UserService target;
public void test() {
// 切面逻辑 @Before
target.test();
...
}
}
Spring 事务
Spring 支持两种方式的事务:
- 编程式事务:使用 TransactionTemplate 显式执行事务,比如显示调用 commit 或者 rollback 方法
- 声明式事务:只需在配置文件做相关的事务规则声明或通过
@Transactional
注解的方式,本质是通过 AOP 对方法前后进行拦截,将事务处理的功能编织到拦截的方法中
使用声明式事务时,Spring 事务管理器会新建一个数据库连接,保存在 ThreadLocal<Map<DateSource,conn>>
,其中 key 是数据源,value 是数据库连接,并修改数据库连接的 autocommit 为 false。注意这里不能由 JDBCTemplate 或 Mybatis 自己建立连接,否则就无法用到事务了。之后该事务便使用此数据库连接
如果有 a 和 b 两个开启了事务的方法,a 方法直接调用 b 方法,那么 b 方法的事务是不会生效的。因为根据 AOP 原理,代理对象在调用 a 方法时会使用原对象来调用,也就是说 a 方法中的 b 方法也是由原对象调用,而原对象并没有对 b 添加事务切面逻辑,因此需要使用代理对象来调用 b 方法
事务传播属性是指当一个事务方法被另一个事务方法调用时,应该如何处理事务的行为,Spring 定义了多种事务传播属性:
- 支持当前事务:
- PROPAGATION_REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择
- PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行
- PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常
- 不支持当前事务:
- PROPAGATION_REQUIRES_NEW:如果当前存在事务,把当前事务挂起,否则新建事务
- PROPAGATION_NOT_SUPPORTED:如果当前存在事务,就把当前事务挂起,否则以非事务方式执行操作
- PROPAGATION_NEVER:如果当前存在事务,则抛出异常,否则以非事务方式执行
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与 PROPAGATION_REQUIRED 类似的操作,即新建一个事务。嵌套事务是已经存在事务的一个子事务,嵌套事务开始执行时将获得一个 savepoint,如果嵌套事务失败,则回滚到此 savepoint。嵌套事务是外部事务的一部分, 只有外部事务结束它才会被提交
Spring 循环依赖
假设有如下情况:
@Component
public class A {
@Autowired
B b;
}
@Component
public class B {
@Autowired
A a;
}
A 和 B 互相依赖,当 A 被创建时,发现依赖 B,就会尝试创建 B,而 B 又发现依赖 A,又会尝试创建 A,于是便发生了循环依赖
可以通过引入缓存池来解决这个问题。当创建 A 对象时但还未进行依赖注入时,先将这个原始的 A 对象放入缓存池。当创建 B 对象时,先从缓存池中取出原始 A 对象用着,等到 B 对象创建完成后就可以注入 A 对象,A 对象继续创建流程。Java 是引用传递,B 拿到的是 A 的引用,那么当 A 创建完成后,B 所拿到的 A 就是完整的对象了
上面提到的缓存池叫做 earlySingletonObjects,它是 Spring 三级缓存中的二级缓存,保存的是刚创建出来的 Bean,这些 Bean 还没有经历过完整生命周期,Bean 的属性可能都还没有设置,Bean 需要的依赖都还没有注入进来
另外两级缓存分别是:
- singletonObjects:一级缓存,保存的是所有经历完整生命周期的 Bean,在对象创建完成后放入缓存
- singletonFactories:三级缓存,在一级缓存和二级缓存中,缓存的 key 是 beanName,缓存的 value 则是一个 Bean 对象,但在三级缓存,缓存的 value 是一个 Lambda 表达式,通过这个 Lambda 表达式可以创建出来目标对象的一个代理对象
三级缓存是为了解决 AOP 的情况。假设 A 对象实现 AOP,那么它就需要是一个代理对象,但是在二级缓存中 A 还只是一个原始的普通对象,不能直接注入 B。此时 Spring 会做两件事:
- 向三级缓存添加一条记录,记录的 key 就是当前 Bean 的 beanName,value 则是一个 Lambda 表达式 ObjectFactory,通过执行这个 Lambda 可以给当前 A 生成代理对象
- 如果二级缓存存在当前 A 则移除掉
当 B 需要用到 A,会先去一级缓存查找是否有 A,如果有,就使用,如果没有,则去二级缓存查找是否有 A,如果有,就使用,如果没有,则去三级缓存查找,然后执行 Lambda。执行的过程中会判断是否需要生成一个代理对象,如果需要就生成代理对象返回,否则返回原始对象。最后,把拿到手的对象存入到二级缓存以备下次使用,同时删除掉三级缓存中对应的数据
Spring 整合 MyBatis 原理
Spring 整合 MyBatis 的思路就是要将 MyBatis 生成的 Mapper 代理对象放入 Spring 容器管理
我们可以使用 FactoryBean 来实现
首先将 SqlSessionFactory 注入 Spring 容器
@ComponentScan("com")
public class AppConfig {
@Bean
public SqlSesionFactory sqlSessionFactory() throws Exception {
// 读取 mybatis 配置文件
InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sf = new SqlSessionFactoryBuilder().build(is);
return sf;
}
}
实现 FactoryBean,先从容器获取 SqlSession,再从 SqlSession 获取 Mapper 代理对象,通过 getObject 将其放入容器
@Component
public class TestFactoryBean implements FactoryBean {
private SqlSession sqlSession;
private Class mapperClass;
// 通过构造方法为 mapper 类型赋值
public TestFactoryBean(Class mapperClass) {
this.mapperClass = mapperClass;
}
public void setSqlSession(SqlSessionFactory sf) {
// 设置要生成代理对象 Mapper 的类型
sf.getConfiguration().addMapper(mapperClass);
this.sqlSession = sf.openSession();
}
@Override
public Object getObject() throws Exception {
// 生成对应类型的 Mapper 对象并放入 Spring 容器
return sqlSession.getMapper(mapperClass);
}
@Override
public Class<?> getObjectType() {
return mapperClass;
}
}
启动 Spring 容器,首先创建 FactoryBean 并加入容器,然后就会调用 FactoryBean 的 getObject 方法将对应的 Mapper 代理对象放入容器了
public class Test {
public static void main(String[] args) {
// 根据配置创建 Spring 容器
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(AppConfig.class);
// 首先使用 BeanDefinition 创建 TestFactoryBean 并加入容器
// 之后 TestFactoryBean 会调用 getObject 方法生成对应类型的 Bean 加入容器
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
// 设置生成 Bean 的类型
beanDefinition.setBeanClass(TestFactoryBean.class);
// 设置构造方法传参
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
// 将 BeanDefinition 注册到容器
applicationContext.registerBeanDefinition("userMapper", beanDefinition);
applicationContext.refresh();
}
}
至此我们就实现了将 Mapper 代理对象放入 Spring 容器的目的,但我们肯定不能为每一个 Mapper 都写一遍上述的逻辑。为此,MyBatis 提供了 @MapperScan
注解,其通过重写 Spring 的扫描方法,为指定目录下的每个 Mapper 生成 FactoryBean,从而实现最终的目的
Spring 扫描原理
Spring 会根据 @ComponentScan
注解配置的路径扫描符合条件的 Bean 创建并加入容器,具体流程如下:
- 构造扫描器 ClassPathBeanDefinitionScanner
- 根据
@ComponentScan
注解的属性配置扫描器 - 扫描配置路径下的所有 class 文件(Resource 对象)
- 利用 ASM 技术读取 class 文件信息,判断是否符合条件(比如类上有
@Component
注解) - 过滤器和条件注解判断
- 独立类、接口、抽象类、
@Lookup
注解判断 - 生成 BeanDefinition 并判断是否有重复
- 根据 BeanDefinition 生成 Bean 对象并加入容器
BeanFactory 和 FactoryBean 的区别
BeanFactory 负责创建、管理和获取 Spring 容器的 Bean 实例,是一个通用的 Bean 管理中心
FactoryBean 是一个特殊的接口,允许用户自定义 Bean 的创建过程,实现该接口的类可以作为工厂创建其他 Bean 实例,更像是一个具体的 Bean 创建工厂
BeanFactory 示例代码:
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
MyBean myBean = (MyBean) factory.getBean("myBean");
FactoryBean 示例代码:
public class MyFactoryBean implements FactoryBean<MyBean> {
@Override
public MyBean getObject() throws Exception {
// 自定义创建MyBean的逻辑
return new MyBean();
}
@Override
public Class<?> getObjectType() {
return MyBean.class;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战