目录
◆ 传统Javaweb开发的困惑
-传统Javaweb开发代码分析-用户模块
-传统Javaweb开发困惑及解决方案
◆ IoC、DI和AOP思想提出
- IoC 控制反转思想的提出
- DI 依赖注入思想的提出
- AOP 面向切面思想的提出
- 框架概念的出现
- 思想、框架和编码关系
◆ Spring框架的诞生
- Spring 框架概述
- Spring 框架的历史
- Spring Framework技术栈图示
- BeanFactory 快速入门
- ApplicationContext快速入门
- BeanFactory与ApplicationContext的关系
- BeanFactory的继承体系
- ApplicationContext的继承体系
◆ 基于xml的Spring应用
- SpringBean 的配置详解
- Spring 的get方法
- Spring 配置非自定义Bean
- Bean 实例化的基本流程
- Spring的后处理器
- Spring Bean的生命周期
- Spring IoC 整体流程总结
- Spring xml方式整合第三方框架
◆ 基于注解的Spring应用
- Bean基本注解开发
- Bean依赖注入注解开发
- 非自定义Bean注解开发
- Bean配置类的注解开发
- Spring 配置其他注解
- Spring注解的解析原理
- Spring注解方式整合第三方框架
◆ 传统Javaweb开发的困惑
-传统Javaweb开发代码分析-用户模块
-传统Javaweb开发困惑及解决方案
困惑一:层与层之间紧密耦合在了一起,接口与具体实现紧密耦合在了一起
解决思路:程序代码中不要手动new对象,第三方根据要求为程序提供需要的Bean对象
困惑二:通用的事务功能耦合在业务代码中,通用的日志功能耦合在业务代码中
解决思路:程序代码中不要手动new对象,第三方根据要求为程序提供需要的Bean对象的代理对象,代理对象内部动态结合业务和通用功能
◆ IoC、DI和AOP思想提出
- IoC 控制反转思想的提出
谁去充当第三方角色呢?
工厂设计模式,BeanFactory来充当第三方的角色,来产生Bean实例BeanFactory怎么知道产生哪些Bean实例呢?
可以使用配置文件配置Bean的基本信息,BeanFactory根据配置文件来生产Bean实例
例如,用IoC思想去反转UserServiceImpl的创建权,由原来程序中创建反转给通过BeanFactory去创建
- DI 依赖注入思想的提出
上面使用BeanFactory的方式已经实现的"控制反转",将Bean的创建权交给了BeanFactory,如果我们想将UserDao的创建权也反转给BeanFactory,与此同时UserService内部还需要用到UserDao实例对象,那应该怎
样操作呢?
1)在程序中,通过BeanFactory获得UserService
2)在程序中,通过BeanFactory获得UserDao
3)在程序中,将UserDao设置给UserService
该方式是否存在一些问题?
UserService存在于BeanFactory中,UserDao也存在于BeanFactory中,可以在BeanFactory内部进行结合
将UserDao在BeanFactory内部设置给UserService的过程叫做“注入”,而UserService需要依赖UserDao的注入才能正常工作,这个过程叫做“依赖注入”
面试题:IoC 和 DI 的关系?
首先,先回答IoC和DI的是什么:
IoC: Inversion of Control,控制反转,将Bean的创建权由原来程序反转给第三方
DI:Dependency Injection,依赖注入,某个Bean的完整创建依赖于其他Bean(或普通参数)的注入
其次,在回答IoC和DI的关系:
第一种观点:IoC强调的是Bean创建权的反转,而DI强调的是Bean的依赖关系,认为不是一回事
第二种观点:IoC强调的是Bean创建权的反转,而DI强调的是通过注入的方式反转Bean的创建权,认为DI是IoC的其中一种实现方式
- AOP 面向切面思想的提出
IoC和DI思想主要是解决前面我们的困惑一,困惑二还没有解决
困惑二的解决方案是,借助于IoC思想,将Bean的创建权反转给BeanFactory,而BeanFactory生产的Bean是目标Bean的代理对象,这样就可以在代理对象中对目标对象方法进行相应的增强。
AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程
三种思想总结
1)IoC控制反转,是将程序创建Bean的权利反转给第三方;
2)DI依赖注入,某个完整Bean需要依赖于其他Bean(或属性)的注入;
3)AOP面向切面编程,用横向抽取方法(属性、对象等)思想,组装成一个功能性切面。
- 框架概念的出现
上面提出了一些思想来解决遇到的问题,而这些思想的实现就需要通过编码去落地,往往我们把具备一定业务领域解决方案的"工具"称为框架。
框架的基本特点:
⚫ 框架(Framework),是基于基础技术之上,从众多业务中抽取出的通用解决方案;
⚫ 框架是一个半成品,使用框架规定的语法开发可以提高开发效率,可以用简单的代码就能完 成复杂的基础业务
⚫ 框架内部使用大量的设计模式、算法、底层代码操作技术,如反射、内省、xml解析、注解 解析等;
⚫ 框架一般都具备扩展性;
⚫ 有了框架,我们可以将精力尽可能的投入在纯业务开发上而不用去费心技术实现以及一些辅助业务。
Java中常用的框架:
不同语言,不同领域都有属于自己的框架,使用框架开发是作为程序员的最基础的底线。Java语言中的框架,可以分为基础框架和服务框架:
- 基础框架:完成基本业务操作的框架,如MyBatis、Spring、SpringMVC、Struts2、Hibernate等
- 服务框架:特定领域的框架,一般还可以对外提供服务框架,如MQ、ES、Nacos等
- 思想、框架和编码关系
- 架构师(高级程序员):把思想落地变为实现的人,例如上面的设计和BeanFactory的编写,即框架的设计和实现者。
- 程序员:使用框架完成业务的人,其中UserServiceImpl、beans.xml、测试类都是我们编写的。
◆ Spring框架的诞生
- Spring 框架概述
spring是一个开源的轻量级Java开发应用框架,可以简化企业级应用开发。Spring解决了开发者在JavaEE开发中遇到的许多常见的问题,提供了功能强大IOC、AOP及Web MVC等功能。是当前企业中Java开发几乎不能缺少的框架之一。Spring的生态及其完善,不管是Spring哪个领域的解决方案都是依附于在SpringFramework基础框架的。
Spring的官网:www.spring.io
- Spring 框架的历史
● Jsp 默默扛下所有;
● MVC+三层架构分工明确,但开发成本及其高;
● EJB 重量级框架出现,走出一个困境,有进入另一个困境;
● Spring 春天来到,随之,SSH风生水起、称霸武林;
● Spring 稳住江湖大哥位置,SSM开始上位;
● Spring 本着“拿来主义”的思维快速发展,生态不断健全;
● SpringBoot 又一里程碑崛起,把“约定大于配置“思想玩儿的炉火纯青;
● SpringCloud 打包了微服务众多解决方案,应对互联网项目更加easy!
- Spring Framework技术栈图示
- BeanFactory 快速入门
根据下图,分析一下Spring的BeanFactory的开发步骤:
1)导入Spring的jar包或Maven坐标;
<!--Spring核心-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.7</version>
</dependency>
2)定义UserService接口及其UserServiceImpl实现类;
public interface UserService {}
public class UserServiceImpl implements UserService {}
3)创建beans.xml配置文件,将UserServiceImpl的信息配置到该xml中;
<bean id="userService" class="com.green.service.impl.UserServiceImpl"></bean>
4)编写测试代码,创建BeanFactory,加载配置文件,获取UserService实例对象。
//创建BeanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
//创建读取器
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
//加载配置文件
reader.loadBeanDefinitions("beans.xml");
//获取Bean实例对象
UserDao userService = (UserService) beanFactory.getBean("userService");
上面使用BeanFactory完成了IoC思想的实现,下面去实现以下DI依赖注入:
1)定义UserDao接口及其UserDaoImpl实现类;
public interface UserDao {}
public class UserDaoImpl implements UserDao {}
2)修改UserServiceImpl代码,添加一个setUserDao(UserDao userDao)用于接收注入的对象;
public class UserServiceImpl implements UserService {
public void setUserDao(UserDao userDao) {
System.out.println(userDao);
}
}
3)修改beans.xml配置文件,在UserDaoImpl的<bean>中嵌入<property>配置注入;
<bean id="userService" class="com.green.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
4)修改测试代码,获得UserService时,setUserService方法执行了注入操作。
//创建BeanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
//创建读取器
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
//加载配置文件
reader.loadBeanDefinitions("beans.xml");
//获取Bean实例对象
UserDao userService = (UserService) beanFactory.getBean("userService");
- ApplicationContext快速入门
ApplicationContext 称为Spring容器,内部封装了BeanFactory,比BeanFactory功能更丰富更强大,使用ApplicationContext 进行开发时,xml配置文件的名称习惯写成applicationContext.xml
- BeanFactory与ApplicationContext的关系
1)BeanFactory是Spring的早期接口,称为Spring的Bean工厂,ApplicationContext是后期更高级接口,称之为Spring 容器;
2)ApplicationContext在BeanFactory基础上对功能进行了扩展,例如:监听功能、国际化功能等。BeanFactory的API更偏向底层,ApplicationContext的API大多数是对这些底层API的封装;
3)Bean创建的主要逻辑和功能都被封装在BeanFactory中,ApplicationContext不仅继承了BeanFactory,而且ApplicationContext内部还维护着BeanFactory的引用,所以ApplicationContext与BeanFactory既有继承关系,又有融合关系。
4)Bean的初始化时机不同,原始BeanFactory是在首次调用getBean时才进行Bean的创建,而ApplicationContext则是配置文件加载,容器一创建就将Bean都实例化并初始化好。
ApplicationContext除了继承了BeanFactory外,还继承了ApplicationEventPublisher(事件发布器)、ResouresPatternResolver(资源解析器)、MessageSource(消息资源)等。但是ApplicationContext的核心功能还是BeanFactory。
applicationContext内部维护着beanFactory的引用,在学习过程中会查看beanFactory内部维护的属性,断点查看如下图示内容的
验证BeanFactory和ApplicationContext对Bean的初始化时机,在UserDaoImpl的无参构造内打印一句话,验证构造方法的执行时机
断点观察,BeanFactory方式时,当调用getBean方法时才会把需要的Bean实例创建,即延迟加载;而ApplicationContext是加载配置文件,容器创建时就将所有的Bean实例都创建好了,存储到一个单例池中,当调用getBean时直接从单例池中获取Bean实例返回
- BeanFactory的继承体系
BeanFactory是核心接口,项目运行过程中肯定有具体实现参与,这个具体实现就是DefaultListableBeanFactory,而ApplicationContext内部维护的Beanfactory的实现类也是它
- ApplicationContext的继承体系
只在Spring基础环境下,即只导入spring-context坐标时,此时ApplicationContext的继承体系
只在Spring基础环境下,常用的三个ApplicationContext作用如下:
实现类 | 功能描述 |
ClassPathXmlApplicationContext | 加载类路径下的xml配置的ApplicationContext |
FileSystemXmlApplicationContext | 加载磁盘路径下的xml配置的ApplicationContext |
AnnotationConfigApplicationContext | 加载注解配置类的ApplicationContext |
如果Spring基础环境中加入了其他组件解决方案,如web层解决方案,即导入spring-web坐标,此时ApplicationContext的继承体系
在Spring的web环境下,常用的两个ApplicationContext作用如下:
实现类 | 功能描述 |
XmlWebApplicationContext | web环境下,加载类路径下的xml配置的ApplicationContext |
AnnotationConfigWebApplicationContext | web环境下,加载磁盘路径下的xml配置的ApplicationContext |
PS:web环境下的这两个ApplicationContext,在学习Spring集成web时在进行讲解
◆ 基于xml的Spring应用
- SpringBean 的配置详解
Spring开发中主要是对Bean的配置,Bean的常用配置一览如下:
Xml配置方式 | 功能描述 |
<bean id="" class=""> | Bean的id和全限定名配置 |
<bean name=""> | 通过name设置Bean的别名,通过别名也能直接获取到Bean实例 |
<bean scope=""> | Bean的作用范围,BeanFactory作为容器时取值singleton和prototype |
<bean lazy-init=""> | Bean的实例化时机,是否延迟加载。BeanFactory作为容器时无效 |
<bean init-method=""> | Bean实例化后自动执行的初始化方法,method指定方法名 |
<bean destroy-method=""> | Bean实例销毁前的方法,method指定方法名 |
<bean autowire="byType"> | 设置自动注入模式,常用的有按照类型byType,按照名字byName |
<bean factory-bean="" factory-method=""/> | 指定哪个工厂Bean的哪个方法完成Bean的创建 |
1)Bean的基础配置
例如:配置UserDaoImpl由Spring容器负责管理
此时存储到Spring容器(singleObjects单例池)中的Bean的beanName是userDao,值是UserDaoImpl对象,可以根据beanName获取Bean实例
如果不配置id,则Spring会把当前Bean实例的全类名作为beanName
2)Bean的别名配置
可以为当前Bean指定多个别名,根据别名也可以获得Bean对象
此时多个名称都可以获得UserDaoImpl实例对象
3)Bean的范围配置
默认情况下,单纯的Spring环境Bean的作用范围有两个:Singleton和Prototype
● singleton:单例,默认值,Spring容器创建的时候,就会进行Bean的实例化,并存储到容器内部的单例池中,每次getBean时都是从单例池中获取相同的Bean实例;
● prototype:原型,Spring容器初始化时不会创建Bean实例,当调用getBean时才会实例化Bean,每次getBean都会创建一个新的Bean实例。
当scope设置为singleton时,获得两次对象打印结果是一样的
通过断点调试,观察可以发现单例池中存在 userDao 实例
当scope设置为prototype时,获得两次对象打印结果是不一样的结果
通过断点调试,观察可以发现单例池中不存在 userDao 实例,但是 userDao的信息已经被存储到beanDefinitionMap中了
4)Bean的延迟加载
当lazy-init设置为true时为延迟加载,也就是当Spring容器创建的时候,不会立即创建Bean实例,等待用到时在创建Bean实例并存储到单例池中去,后续在使用该Bean直接从单例池获取即可,本质上该Bean还是单例的
5)Bean的初始化和销毁方法配置
Bean在被实例化后,可以执行指定的初始化方法完成一些初始化的操作,Bean在销毁之前也可以执行指定的销毁方法完成一些操作,初始化方法名称和销毁方法名称通过
扩展:除此之外,我们还可以通过实现 InitializingBean 接口,完成一些Bean的初始化操作,如下:
6)Bean的实例化配置
Spring的实例化方式主要如下两种:
● 构造方式实例化:底层通过构造方法对Bean进行实例化
● 工厂方式实例化:底层通过调用自定义的工厂方法对Bean进行实例化
构造方式实例化Bean又分为无参构造方法实例化和有参构造方法实例化,Spring中配置的<bean>几乎都是无参构造该方式,此处不在赘述。下面讲解有参构造方法实例化Bean
有参构造在实例化Bean时,需要参数的注入,通过<constructor-arg>标签,嵌入在<bean>标签内部提供构造参数,如下:
工厂方式实例化Bean,又分为如下三种:
● 静态工厂方法实例化Bean
● 实例工厂方法实例化Bean
● 实现FactoryBean规范延迟实例化Bean
静态工厂方法实例化Bean,其实就是定义一个工厂类,提供一个静态方法用于生产Bean实例,在将该工厂类及其静态方法配置给Spring即可
PS:<constructor-arg>标签不仅仅是为构造方法传递参数,只要是为了实例化对象而传递的参数都可以通过<constructor-arg>标签完成,例如上面通过静态工厂方法实例化Bean所传递的参数也是要通过<constructor-arg>进行传递的
测试代码,直接通过ApplicationContext获得userDao即可
断点调试,UserDaoImpl实例对象会存在于单例池中
实例工厂方法,也就是非静态工厂方法产生Bean实例,与静态工厂方式比较,该方式需要先有工厂对象,在用工厂对象去调用非静态方法,所以在进行配置时,要先配置工厂Bean,再配置目标Bean
测试代码同上,直接通过ApplicationContext获得userDao即可,不在赘述
通过断点观察单例池singletonObjects,发现单例池中既有工厂Bean实例,也有目标Bean实例,且都是在Spring容器创建时,就完成了Bean的实例化
上面不管是静态工厂方式还是非静态工厂方式,都是自定义的工厂方法,Spring提供了FactoryBean的接口规范,FactoryBean接口定义如下:
定义工厂实现FactoryBean
配置FactoryBean交由Spring管理即可
通过Spring容器根据beanName可以正常获得UserDaoImpl
通过断点观察发现Spring容器创建时,FactoryBean被实例化了,并存储到了单例池singletonObjects中,但是getObject() 方法尚未被执行,UserDaoImpl也没被实例化,当首次用到UserDaoImpl时,才调用getObject() ,此工厂方式产生的Bean实例不会存储到单例池singletonObjects中,会存储到 factoryBeanObjectCache 缓存池中,并且后期每次使用到userDao都从该缓存池中返回的是同一个userDao实例。
7)Bean的依赖注入配置
Bean的依赖注入有两种方式:
注入方式 | 配置方式 |
通过Bean的set方法注入 | <property name="userDao" ref="userDao"/> <property name="userDao" value="haohao"/> |
通过构造Bean的方法进行注入 | <constructor-arg name="name" ref="userDao"/> <constructor-arg name="name" value="haohao"/> |
其中,ref 是 reference 的缩写形式,翻译为:涉及,参考的意思,用于引用其他Bean的id。value 用于注入普通属性值。
依赖注入的数据类型有如下三种:
● 普通数据类型,例如:String、int、boolean等,通过value属性指定。
● 引用数据类型,例如:UserDaoImpl、DataSource等,通过ref属性指定。
● 集合数据类型,例如:List、Map、Properties等。
注入 List<T> 集合 – 普通数据
注入 List<T> 集合 – 引用数据
也可以直接引用容器中存在的Bean
注入 Set<T> 集合
注入 Map<K,V> 集合
注入 Properties 键值对
扩展:自动装配方式
如果被注入的属性类型是Bean引用的话,那么可以在<bean> 标签中使用 autowire 属性去配置自动注入方式,属性值有两个:
● byName:通过属性名自动装配,即去匹配 setXxx 与 id="xxx"(name="xxx")是否一致;
● byType:通过Bean的类型从容器中匹配,匹配出多个相同Bean类型时,报错。
8)Spring的其他配置标签
Spring 的 xml 标签大体上分为两类,一种是默认标签,一种是自定义标签
● 默认标签:就是不用额外导入其他命名空间约束的标签,例如 <bean> 标签
● 自定义标签:就是需要额外引入其他命名空间约束,并通过前缀引用的标签,例如 <context:property-placeholder/> 标签
Spring的默认标签用到的是Spring的默认命名空间
该命名空间约束下的默认标签如下:
标签 | 作用 |
<beans> | 一般作为 xml 配置根标签,其他标签都是该标签的子标签 |
<bean> | Bean的配置标签,上面已经详解了,此处不再阐述 |
<import> | 外部资源导入标签 |
<alias> | 指定Bean的别名标签,使用较少 |
<beans>标签,除了经常用的做为根标签外,还可以嵌套在根标签内,使用profile属性切换开发环境
可以使用以下两种方式指定被激活的环境:
● 使用命令行动态参数,虚拟机参数位置加载 -Dspring.profiles.active=test
● 使用代码的方式设置环境变量 System.setProperty("spring.profiles.active","test")
<import>标签,用于导入其他配置文件,项目变大后,就会导致一个配置文件内容过多,可以将一个配置文件根据业务某块进行拆分,拆分后,最终通过<import>标签导入到一个主配置文件中,项目加载主配置文件就连同<import> 导入的文件一并加载了
<alias> 标签是为某个Bean添加别名,与在<bean> 标签上使用name属性添加别名的方式一样,我们为UserServiceImpl指定四个别名:aaa、bbb、xxx、yyy
断点调试,在beanFactory中维护着一个名为aliasMap的Map<String,String>集合,存储别名和beanName之间的映射关系
Spring的自定义标签需要引入外部的命名空间,并为外部的命名空间指定前缀,使用 <前缀:标签> 形式的标签,称之为自定义标签,自定义标签的解析流程也是 Spring xml扩展点方式之一
- Spring 的get方法
方法定义 | 返回值和参数 |
Object getBean (String beanName) | 根据beanName从容器中获取Bean实例,要求容器中Bean唯一,返回值为Object,需要强转 |
T getBean (Class type) | 根据Class类型从容器中获取Bean实例,要求容器中Bean类型唯一,返回值为Class类型实例,无需强转 |
T getBean (String beanName,Class type) | 根据beanName从容器中获得Bean实例,返回值为Class类型实例,无需强转 |
- Spring 配置非自定义Bean
以上在 xml 中配置的Bean都是自己定义的,例如:UserDaoImpl,UserServiceImpl。但是,在实际开发中有些功能类并不是我们自己定义的,而是使用的第三方jar包中的,那么,这些Bean要想让Spring进行管理,也需要对其进行配置
配置非自定义的Bean需要考虑如下两个问题:
● 被配置的Bean的实例化方式是什么?无参构造、有参构造、静态工厂方式还是实例工厂方式;
● 被配置的Bean是否需要注入必要属性。
1)配置 Druid 数据源交由Spring管理
导入Druid坐标
配置 DruidDataSource
2)配置Connection交由Spring管理
Connection 的产生是通过DriverManager的静态方法getConnection获取的,所以用静态工厂方式配置
3)配置日期对象交由Spring管理
产生一个指定日期格式的对象,原始代码按如下:
可以看成是实例工厂方式,使用Spring配置方式产生Date实例
4)配置MyBatis的SqlSessionFactory交由Spring管理
导入MyBatis的相关坐标:
MyBatis原始获得SqlSessionFactory的方式:
SqlSessionFactory交由Spring管理配置如下:
- Bean 实例化的基本流程
Spring容器在进行初始化时,会将xml配置的<bean>的信息封装成一个BeanDefinition对象,所有的BeanDefinition存储到一个名为beanDefinitionMap的Map集合中去,Spring框架在对该Map进行遍历,使用反射创建Bean实例对象,创建好的Bean对象存储在一个名为singletonObjects的Map集合中,当调用getBean方法时则最终从该Map集合中取出Bean实例对象返回。
Bean信息定义对象-BeanDefinition
DefaultListableBeanFactory对象内部维护着一个Map用于存储封装好的BeanDefinitionMap
Spring框架会取出beanDefinitionMap中的每个BeanDefinition信息,反射构造方法或调用指定的工厂方法生成Bean实例对象,所以只要将BeanDefinition注册到beanDefinitionMap这个Map中,Spring就会进行对应的Bean的实例化操作
Bean实例及单例池singletonObjects, beanDefinitionMap中的BeanDefinition会被转化成对应的Bean实例对象,存储到单例池singletonObjects中去,在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中,维护着singletonObjects,源码如下:
● 加载xml配置文件,解析获取配置中的每个<bean>的信息,封装成一个个的BeanDefinition对象;
● 将BeanDefinition存储在一个名为beanDefinitionMap的Map<String,BeanDefinition>中;
● ApplicationContext底层遍历beanDefinitionMap,创建Bean实例对象;
● 创建好的Bean实例对象,被存储到一个名为singletonObjects的Map<String,Object>中;
● 当执行applicationContext.getBean(beanName)时,从singletonObjects去匹配Bean实例返回。
- Spring的后处理器
Spring的后处理器是Spring对外开发的重要扩展点,允许我们介入到Bean的整个实例化流程中来,以达到动态注册BeanDefinition,动态修改BeanDefinition,以及动态修改Bean的作用。Spring主要有两种后处理器:
● BeanFactoryPostProcessor:Bean工厂后处理器,在BeanDefinitionMap填充完毕,Bean实例化之前执行;
● BeanPostProcessor:Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行。
Bean工厂后处理器 – BeanFactoryPostProcessor
BeanFactoryPostProcessor是一个接口规范,实现了该接口的类只要交由Spring容器管理的话,那么Spring就会回调该接口的方法,用于对BeanDefinition注册和修改的功能。
BeanFactoryPostProcessor 定义如下:
编写BeanFactoryPostProcessor
配置BeanFactoryPostProcessor
postProcessBeanFactory 参数本质就是 DefaultListableBeanFactory,拿到BeanFactory的引用,自然就可以对beanDefinitionMap中的BeanDefinition进行操作了 ,例如对UserDaoImpl的BeanDefinition进行修改操作
上面已经对指定的BeanDefinition进行了修改操作,下面对BeanDefiition进行注册操作
Spring 提供了一个BeanFactoryPostProcessor的子接口BeanDefinitionRegistryPostProcessor专门用于注册BeanDefinition操作
案例 使用Spring的BeanFactoryPostProcessor扩展点完成自定义注解扫描
要求如下:
● 自定义@MyComponent注解,使用在类上;
● 使用资料中提供好的包扫描器工具BaseClassScanUtils 完成指定包的类扫描;
● 自定义BeanFactoryPostProcessor完成注解@MyComponent的解析,解析后最终被Spring管理。
自定义@MyComponent注解,使用在类上
在类上使用@MyComponent
在类上使用@MyComponent
自定义BeanFactoryPostProcessor完成注解解析
Bean后处理器 – BeanPostProcessor
Bean被实例化后,到最终缓存到名为singletonObjects单例池之前,中间会经过Bean的初始化过程,例如:属性的填充、初始方法init的执行等,其中有一个对外进行扩展的点BeanPostProcessor,我们称为Bean后处理。跟上面的Bean工厂后处理器相似,它也是一个接口,实现了该接口并被容器管理的BeanPostProcessor,会在流程节点上被Spring自动调用。
Bean后处理器 – BeanPostProcessor
自定义MyBeanPostProcessor,完成快速入门测试
配置MyBeanPostProcessor
测试控制台打印结果如下:
案例: 对Bean方法进行执行时间日志增强
要求如下:
● Bean的方法执行之前控制台打印当前时间;
● Bean的方法执行之后控制台打印当前时间。
分析:
● 对方法进行增强主要就是代理设计模式和包装设计模式;
● 由于Bean方法不确定,所以使用动态代理在运行期间执行增强操作;
● 在Bean实例创建完毕后,进入到单例池之前,使用Proxy代替真是的目标Bean
编写BeanPostProcessor,增强逻辑编写在 after方法中
- Spring Bean的生命周期
Spring Bean的生命周期是从 Bean 实例化之后,即通过反射创建出对象之后,到Bean成为一个完整对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期。Spring Bean的生命周期大体上分为三个阶段:
● Bean的实例化阶段:Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的,是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化;
● Bean的初始化阶段:Bean创建之后还仅仅是个"半成品",还需要对Bean实例的属性进行填充、执行一些Aware接口方法、执行BeanPostProcessor方法、执行InitializingBean接口的初始化方法、执行自定义初始化init方法等。该阶段是Spring最具技术含量和复杂度的阶段,Aop增强功能,后面要学习的Spring的注解功能等、spring高频面试题Bean的循环引用问题都是在这个阶段体现的;
● Bean的完成阶段:经过初始化阶段,Bean就成为了一个完整的Spring Bean,被存储到单例池singletonObjects中去了,即完成了Spring Bean的整个生命周期。
由于Bean的初始化阶段的步骤比较复杂,所以着重研究Bean的初始化阶段
Spring Bean的初始化过程涉及如下几个过程:
● Bean实例的属性填充
● Aware接口属性注入
● BeanPostProcessor的before()方法回调
● InitializingBean接口的初始化方法回调
● 自定义初始化方法init回调
● BeanPostProcessor的after()方法回调
PS:通过代码验证上述初始化顺序. .
Bean实例属性填充
BeanDefinition 中有对当前Bean实体的注入信息通过属性propertyValues进行了存储,例如UserService的属性信息如下:
Spring在进行属性注入时,会分为如下几种情况:
● 注入普通属性,String、int或存储基本类型的集合时,直接通过set方法的反射设置进去;
● 注入单向对象引用属性时,从容器中getBean获取后通过set方法反射设置进去,如果容器中没有,则先创建被注入对象Bean实例(完成整个生命周期)后,在进行注入操作;
● 注入双向对象引用属性时,就比较复杂了,涉及了循环引用(循环依赖)问题,下面会详细阐述解决方案。
PS:通过代码验证上述第二第三种属性填充. .
多个实体之间相互依赖并形成闭环的情况就叫做"循环依赖",也叫做"循环引用"
代码验证后,分析出UserService与UserDao实例化与初始化的顺序如下:
Spring提供了三级缓存存储 完整Bean实例 和 半成品Bean实例 ,用于解决循环引用问题
在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map:
UserService和UserDao循环依赖的过程结合上述三级缓存描述一下
● UserService 实例化对象,但尚未初始化,将UserService存储到三级缓存;
● UserService 属性注入,需要UserDao,从缓存中获取,没有UserDao;
● UserDao实例化对象,但尚未初始化,将UserDao存储到到三级缓存;
● UserDao属性注入,需要UserService,从三级缓存获取UserService,UserService从三级缓存移入二级缓存;
● UserDao执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存;
● UserService 注入UserDao;
● UserService执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存。
常用的Aware接口
Aware接口是一种框架辅助属性注入的一种思想,其他框架中也可以看到类似的接口。框架具备高度封装性,我们接触到的一般都是业务代码,一个底层功能API不能轻易的获取到,但是这不意味着永远用不到这些对象,如果用到了,就可以使用框架提供的类似Aware的接口,让框架给我们注入该对象。
Aware接口 | 回调方法 | 作用 |
ServletContextAware | setServletContext(ServletContext context) | Spring框架回调方法注入ServletContext对象,web环境下才生效 |
BeanFactoryAware | setBeanFactory(BeanFactory factory) | Spring框架回调方法注入beanFactory对象 |
BeanNameAware | setBeanName(String beanName) | Spring框架回调方法注入当前Bean在容器中的beanName |
ApplicationContextAware | setApplicationContext(ApplicationContext applicationContext) | Spring框架回调方法注入applicationContext 对象 |
- Spring IoC 整体流程总结
- Spring xml方式整合第三方框架
xml整合第三方框架有两种整合方案:
● 不需要自定义名空间,不需要使用Spring的配置文件配置第三方框架本身内容,如:MyBatis;
● 需要引入第三方框架命名空间,要使用Spring的配置文件配置第三方框架本身内容,如:Dubbo
Spring整合MyBatis,之前已经在Spring中简单的配置了SqlSessionFactory,但是这不是正规的整合方式,MyBatis提供了mybatis-spring.jar专门用于两大框架的整合。
Spring整合MyBatis的步骤如下:
● 导入MyBatis整合Spring的相关坐标;(请见资料中的pom.xml)
● 编写Mapper和Mapper.xml;
● 配置SqlSessionFactoryBean和MapperScannerConfigurer;
● 编写测试代码
Spring整合MyBatis的原理剖析
整合包里提供了一个SqlSessionFactoryBean和一个扫描Mapper的配置对象,SqlSessionFactoryBean一旦被实例化,就开始扫描Mapper并通过动态代理产生Mapper的实现类存储到Spring容器中。相关的有如下四个类:
● SqlSessionFactoryBean:需要进行配置,用于提供SqlSessionFactory;
● MapperScannerConfigurer:需要进行配置,用于扫描指定mapper注册BeanDefinition;
● MapperFactoryBean:Mapper的FactoryBean,获得指定Mapper时调用getObject方法;
● ClassPathMapperScanner:definition.setAutowireMode(2) 修改了自动注入状态,所以MapperFactoryBean中的setSqlSessionFactory会自动注入进去。
Spring 整合其他组件时就不像MyBatis这么简单了,例如Dubbo框架在于Spring进行整合时,要使用Dubbo提供的命名空间的扩展方式,自定义了一些Dubbo的标签
为了降低此处的学习成本,不在引入Dubbo第三方框架了,以Spring的 context 命名空间去进行讲解,该方式也是命名空间扩展方式。
需求:加载外部properties文件,将键值对存储在Spring容器中
原理剖析解析过程,只能从源头ClassPathXmlApplicationContext入手,经历复杂的源码追踪,找到如下两个点:
1)在创建DefaultNamespaceHandlerResolver时,为处理器映射地址handlerMappingsLocation属性赋值,并加载命名空间处理器到Map<String, Object> handlerMappings 中去
第一点完成后,Map集合handlerMappings被填充了很多XxxNamespaceHandler,继续往下追代码
2)在DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法中,发现如下逻辑:
如果是默认命名空间,则执行parseDefaultElement方法
如果是自定义命名空间,则执行parseCustomElement方法
在执行resovle方法时,就是从Map<String, Object> handlerMappings中根据命名空间名称获得对应的处理器对象,此处是ContextNamespaceHandler,最终执行NamespaceHandler的parse方法
ContextNamespaceHandler源码如下,间接实现了NamespaceHandler接口,初始化方法init会被自动调用。由于context命名空间下有多个标签,所以每个标签又单独注册了对应的解析器,注册到了其父类NamespaceHandlerSupport的Map<String, BeanDefinitionParser> parsers中去了
通过上述分析,清楚的了解了外部命名空间标签的执行流程,如下:
● 将自定义标签的约束 与 物理约束文件与网络约束名称的约束 以键值对形式存储到一个spring.schemas文件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
● 将自定义命名空间的名称 与 自定义命名空间的处理器映射关系 以键值对形式存在到一个叫spring.handlers文件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
● 准备好NamespaceHandler,如果命名空间只有一个标签,那么直接在parse方法中进行解析即可,一般解析结果就是注册该标签对应的BeanDefinition。如果命名空间里有多个标签,那么可以在init方法中为每个标签都注册一个BeanDefinitionParser,在执行NamespaceHandler的parse方法时在分流给不同的BeanDefinitionParser进行解析(重写doParse方法即可)。
案例:设想自己是一名架构师,进行某一个框架与Spring的集成开发,效果是通过一个指示标签,向Spring容器中自动注入一个BeanPostProcessor
步骤分析:
1. 确定命名空间名称、schema虚拟路径、标签名称;
2. 编写schema约束文件haohao-annotation.xsd
3. 在类加载路径下创建META目录,编写约束映射文件spring.schemas和处理器映射文件spring.handlers
4. 编写命名空间处理器 HaohaoNamespaceHandler,在init方法中注册HaohaoBeanDefinitionParser
5. 编写标签的解析器 HaohaoBeanDefinitionParser,在parse方法中注册HaohaoBeanPostProcessor
6. 编写HaohaoBeanPostProcessor
==========以上五步是框架开发者写的,以下是框架使用者写的===========
1. 在applicationContext.xml配置文件中引入命名空间
2. 在applicationContext.xml配置文件中使用自定义的标签
◆ 基于注解的Spring应用
- Bean基本注解开发
Spring除了xml配置文件进行配置之外,还可以使用注解方式进行配置,注解方式慢慢成为xml配置的替代方案。我们有了xml开发的经验,学习注解开发就方便了许多,注解开发更加快捷方便。
Spring提供的注解有三个版本:
● 2.0时代,Spring开始出现注解
● 2.5时代,Spring的Bean配置可以使用注解完成
● 3.0时代,Spring其他配置也可以使用注解完成,我们进入全注解时代
基本Bean注解,主要是使用注解的方式替代原有xml的 <bean> 标签及其标签属性的配置
使用@Component 注解替代<bean>标签
xml配置 | 注解 | 描述 |
<bean id="" class=""> | @Component | 被该注解标识的类,会在指定扫描范围内被Spring加载并实例化 |
可以通过@Component注解的value属性指定当前Bean实例的beanName,也可以省略不写,不写的情况下为当前类名首字母小写
使用注解对需要被Spring实例化的Bean进行标注,但是需要告诉Spring去哪找这些Bean,要配置组件扫描路径
@Component就单纯一个value属性,那么xml配置 <bean> 时那些属性怎么进行配置呢?Spring 是通过注解方式去配置的之前 <bean> 标签中的那些属性,例如:@Scope
使用@Component 注解替代<bean>标签
xml配置 | 注解 | 描述 |
<bean scope=""> | @Scope | 在类上或使用了@Bean标注的方法上,标注Bean的作用范围,取值为singleton或prototype |
<bean lazy-init=""> | @Lazy | 在类上或使用了@Bean标注的方法上,标注Bean是否延迟加载,取值为true和false |
<bean init-method=""> | @PostConstruct | 在方法上使用,标注Bean的实例化后执行的方法 |
<bean destroy-method=""> | @PreDestroy | 在方法上使用,标注Bean的销毁前执行方法 |
使用上述注解完成UserDaoImpl的基本配置
由于JavaEE开发是分层的,为了每层Bean标识的注解语义化更加明确,@Component又衍生出如下三个注解:
@Component衍生注解 | 描述 |
@Repository | 在Dao层类上使用 |
@Service | 在Service层类上使用 |
@Controller | 在Web层类上使用 |
- Bean依赖注入注解开发
Bean依赖注入的注解,主要是使用注解的方式替代xml的 <property> 标签完成属性的注入操作
Spring主要提供如下注解,用于在Bean内部进行属性注入的:
属性注入注解 | 描述 |
@Value | 使用在字段或方法上,用于注入普通数据 |
@Autowired | 使用在字段或方法上,用于根据类型(byType)注入引用数据 |
@Qualifier | 使用在字段或方法上,结合@Autowired,根据名称注入 |
@Resource | 使用在字段或方法上,根据类型或名称进行注入 |
通过@Value 直接注入普通属性
通过@Value 注入properties文件中的属性
加载properties文件
@Autowired注解,用于根据类型进行注入
当容器中同一类型的Bean实例有多个时,会尝试自动根据名字进行匹配:
@Qualifier配合@Autowired可以完成根据名称注入Bean实例,使用@Qualifier指定名称
@Resource注解既可以根据类型注入,也可以根据名称注入,无参就是根据类型注入,有参数就是根据名称注入
PS:@Resource注解存在与 javax.annotation 包中,Spring对其进行了解析
- 非自定义Bean注解开发
非自定义Bean不能像自定义Bean一样使用@Component进行管理,非自定义Bean要通过工厂的方式进行实例化,使用@Bean标注方法即可,@Bean的属性为beanName,如不指定为当前工厂方法名称
PS:工厂方法所在类必须要被Spring管理
如果@Bean工厂方法需要参数的话,则有如下几种注入方式:
● 使用@Autowired 根据类型自动进行Bean的匹配,@Autowired可以省略 ;
● 使用@Qualifier 根据名称进行Bean的匹配;
● 使用@Value 根据名称进行普通数据类型匹配。
- Bean配置类的注解开发
@Component等注解替代了<bean>标签,但是像<import>、<context:componentScan> 等非<bean> 标签怎样去使用注解替代呢?
定义一个配置类替代原有的xml配置文件,<bean>标签以外的标签,一般都是在配置类上使用注解完成的
@Configuration注解标识的类为配置类,替代原有xml配置文件,该注解第一个作用是标识该类是一个配置类,第二个作用是具备@Component作用
@ComponentScan 组件扫描配置,替代原有xml文件中的
<context:component-scan base-package=""/>
base-package的配置方式:
● 指定一个或多个包名:扫描指定包及其子包下使用注解的类
● 不配置包名:扫描当前@componentScan注解配置类所在包及其子包下的类
@PropertySource 注解用于加载外部properties资源配置,替代原有xml中的
<context:property-placeholder location=“”/> 配置
@Import 用于加载其他配置类,替代原有xml中的<import resource=“classpath:beans.xml”/>配置
- Spring 配置其他注解
扩展:@Primary注解用于标注相同类型的Bean优先被使用权,@Primary 是Spring3.0引入的,与@Component和@Bean一起使用,标注该Bean的优先级更高,则在通过类型获取Bean或通过@Autowired根据类型进行注入时,会选用优先级更高的
扩展:@Profile 注解的作用同于xml配置时学习profile属性,是进行环境切换使用的
注解 @Profile 标注在类或方法上,标注当前产生的Bean从属于哪个环境,只有激活了当前环境,被标注的Bean才能被注册到Spring容器里,不指定环境的Bean,任何环境下都能注册到Spring容器里
可以使用以下两种方式指定被激活的环境:
● 使用命令行动态参数,虚拟机参数位置加载 -Dspring.profiles.active=test
● 使用代码的方式设置环境变量 System.setProperty("spring.profiles.active","test");
- Spring注解的解析原理
使用@Component等注解配置完毕后,要配置组件扫描才能使注解生效
● xml配置组件扫描:
● 配置类配置组件扫描:
使用xml方式配置组件扫描,而component-scan是一个context命名空间下的自定义标签,所以要找到对应的命名空间处理器NamespaceHandler 和 解析器,查看spring-context包下的spring.handlers文件
查看 ContextNamespaceHandler 类
将ComponentScanBeanDefinitionParser进行了注册,对其源码进行跟踪,最终将标注的@Component的类,生成对应的BeanDefiition进行了注册
使用配置类配置组件扫描,使用AnnotationConfigApplicationContext容器在进行创建时,内部调用了如下代码,该工具注册了几个Bean后处理器:
- Spring注解方式整合第三方框架
第三方框架整合,依然使用MyBatis作为整合对象,之前我们已经使用xml方式整合了MyBatis,现在使用注解方式无非就是将xml标签替换为注解,将xml配置文件替换为配置类而已,原有xml方式整合配置如下:
使用@Bean将DataSource和SqlSessionFactoryBean存储到Spring容器中,而MapperScannerConfigurer使用注解@MapperScan进行指明需要扫描的Mapper在哪个包下,使用注解整合MyBatis配置方式如下:
注解方式,Spring整合MyBatis的原理,关键在于@MapperScan,@MapperScan不是Spring提供的注解,是MyBatis为了整合Spring,在整合包org.mybatis.spring.annotation中提供的注解,源码如下:
重点关注一下@Import({MapperScannerRegistrar.class}),当@MapperScan被扫描加载时,会解析@Import注解,从而加载指定的类,此处就是加载了MapperScannerRegistrar
MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,Spring会自动调用
registerBeanDefinitions方法,该方法中又注册MapperScannerConfigurer类,而MapperScannerConfigurer类作用是扫描Mapper,向容器中注册Mapper对应的MapperFactoryBean
Spring与MyBatis注解方式整合有个重要的技术点就是@Import,第三方框架与Spring整合xml方式很多是凭借自定义标签完成的,而第三方框架与Spring整合注解方式很多是靠@Import注解完成的。
@Import可以导入如下三种类:
● 普通的配置类
● 实现ImportSelector接口的类
● 实现ImportBeanDefinitionRegistrar接口的类
@Import导入实现了ImportSelector接口的类
ImportSelector接口selectImports方法的参数AnnotationMetadata代表注解的媒体数据,可以获得当前注解修饰的类的元信息,例如:获得组件扫描的包名
@Import导入实现ImportBeanDefinitionRegistrar接口的类,实现了该接口的类的registerBeanDefinitions方法会被自动调用,在该方法内可以注册BeanDefinition
来源:黑马程序员新版Spring零基础入门到精通,一套搞定spring全套视频教程(含实战源码)