# IoC、DI 和 AOP 思想
三种思想总结
1)IoC 控制反转,是将程序创建 Bean 的权利反转给第三方;
2)DI 依赖注入,某个完整 Bean 需要依赖于其他 Bean(或属性)的注入;
3)AOP 面向切面编程,用横向抽取方法(属性、对象等)思想,组装成一个功能性切面。
# 面试题: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 面向切面思想的提出
AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程 OOP 的升华。OOP 是纵向对一个
事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而 AOP 是横向的对不同事物的抽象,属
性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程
从此图可以看出,不论是 UserService 还是 ExceptionLog,两个类的体现都是面向对象,如果我提取其中一个类的方法和另一个类的方法,或者一个类的属性和另一个类的属性等等组成到一起形成一个通用的类,这就体现了 AOP 的横向切面思想。
# 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。
# 验证 BeanFactory 和 ApplicationContext 对 Bean 的初始化时机
验证 BeanFactory 和 ApplicationContext 对 Bean 的初始化时机,在 UserDaoImpl 的无参构造内打印一句话,验证
构造方法的执行时机
1 | public class UserDaoImpl implements UserDao { |
断点观察,BeanFactory 方式时,当调用 getBean 方法时才会把需要的 Bean 实例创建,即延迟加载;而
ApplicationContext 是加载配置文件,容器创建时就将所有的 Bean 实例都创建好了,存储到一个单例池中,当调
用 getBean 时直接从单例池中获取 Bean 实例返回。
# BeanFactory 的继承体系
BeanFactory 是核心接口,项目运行过程中肯定有具体实现参与,这个具体实现就是 DefaultListableBeanFactory
,而 ApplicationContext 内部维护的 Beanfactory 的实现类也是它
# ApplicationContext 的继承体系
只在 Spring 基础环境下,即只导入 spring-context 坐标时,此时 ApplicationContext 的继承体系
只在 Spring 基础环境下,常用的三个 ApplicationContext 作用如下:
如果 Spring 基础环境中加入了其他组件解决方案,如 web 层解决方案,即导入 spring-web 坐标,此时
ApplicationContext 的继承体系
1 | <dependency> |
在 Spring 的 web 环境下,常用的两个 ApplicationContext 作用如下:
# SpringBean 的配置详解
Spring 开发中主要是对 Bean 的配置,Bean 的常用配置一览如下:
配置项 | 功能描述 |
---|---|
<bean id="" class=""> | 配置 Bean 的基本信息, id 是唯一标识符, class 是全限定类名 |
<bean name=""> | 为 Bean 设置别名,通过别名也可以获取 Bean 实例 |
<bean scope=""> | 配置 Bean 的作用范围,BeanFactory 作为容器时取值 singleton 和 prototype |
<bean lazy-init=""> | 配置 Bean 的实例化时机, true 表示延迟加载,BeanFactory 作为容器时无效 |
<bean init-method=""> | 配置 Bean 实例化后自动执行的初始化方法,method 指定方法名 |
<bean destroy-method=""> | 配置 Bean 实例销毁前需要执行的方法,method 指定方法名 |
<bean autowire=""> | 配置 Bean 的自动注入模式,常用的有按照类型 byType,按照名字 byName |
<bean factory-bean="" factory-method=""> | 指定哪个工厂 Bean 的哪个方法来创建 Bean |
1)Bean 的基础配置
例如:配置 UserDaoImpl 由 Spring 容器负责管理
1 | <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/> |
此时存储到 Spring 容器(singleObjects 单例池)中的 Bean 的 beanName 是 userDao,值是 UserDaoImpl 对象,可
以根据 beanName 获取 Bean 实例
1 | applicationContext.getBean("userDao"); |
如果不配置 id,则 Spring 会把当前 Bean 实例的全限定名作为 beanName
1 | applicationContext.getBean("com.itheima.dao.impl.UserDaoImpl"); |
2)Bean 的别名配置
可以为当前 Bean 指定多个别名,根据别名也可以获得 Bean 对象
1 | <bean id="userDao" name="aaa,bbb" class="com.itheima.dao.impl.UserDaoImpl"/> |
此时多个名称都可以获得 UserDaoImpl 实例对象
1 | applicationContext.getBean("userDao"); |
3)Bean 的范围配置
默认情况下,单纯的 Spring 环境 Bean 的作用范围有两个:Singleton 和 Prototype
singleton:
1 | <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" scope="singleton"/> |
单例,默认值,Spring 容器创建的时候,就会进行 Bean 的实例化,并存储到容器内部的单例池中
,每次 getBean 时都是从单例池中获取相同的 Bean 实例;
prototype:
1 | <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" scope="prototype"/> |
原型,Spring 容器初始化时不会创建 Bean 实例,当调用 getBean 时才会实例化 Bean,每次
getBean 都会创建一个新的 Bean 实例。
当 scope 设置为 singleton 时,获得两次对象打印结果是一样的
当 scope 设置为 prototype 时,获得两次对象打印结果是不一样的
4)Bean 的延迟加载
当 lazy-init 设置为 true 时为延迟加载,也就是当 Spring 容器创建的时候,不会立即创建 Bean 实例,等待用到时在创
建 Bean 实例并存储到单例池中去,后续在使用该 Bean 直接从单例池获取即可,本质上该 Bean 还是单例的
1 | <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" lazy-init="true"/> |
5)Bean 的初始化和销毁方法配置
Bean 在被实例化后,可以执行指定的初始化方法完成一些初始化的操作,Bean 在销毁之前也可以执行指定的销毁
方法完成一些操作,初始化方法名称和销毁方法名称通过
1 | <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" init-method="init" |
1 | public class UserDaoImpl implements UserDao { |
扩展:除此之外,我们还可以通过实现 InitializingBean 接口,完成一些 Bean 的初始化操作,如下:
1 | public class UserDaoImpl implements UserDao, InitializingBean { |
6)Bean 的实例化配置
Spring 的实例化方式主要如下两种:
- 构造方式实例化:底层通过构造方法对 Bean 进行实例化
- 工厂方式实例化:底层通过调用自定义的工厂方法对 Bean 进行实例化
构造方式实例化 Bean 又分为无参构造方法实例化和有参构造方法实例化,Spring 中配置的 <bean> 几乎都是无参构
造该方式,此处不在赘述。下面讲解有参构造方法实例化 Bean
1 | //有参构造方法 |
有参构造在实例化 Bean 时,需要参数的注入,通过 <constructor-arg> 标签,嵌入在 < bean > 标签内部提供构造参数,如下:
1 | <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"> |
工厂方式实例化 Bean,又分为如下三种:
- 静态工厂方法实例化 Bean
- 实例工厂方法实例化 Bean
- 实现 FactoryBean 规范延迟实例化 Bean
静态工厂方法实例化 Bean,其实就是定义一个工厂类,提供一个静态方法用于生产 Bean 实例,在将该工厂类及其
静态方法配置给 Spring 即可
1 | //工厂类 |
1 | <bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean" factory-method="getUserDao"> |
PS: <constructor-arg>
标签不仅仅是为构造方法传递参数,只要是为了实例化对象而传递的参数都可以通过
<constructor-arg>
标签完成,例如上面通过静态工厂方法实例化 Bean 所传递的参数也是要通过 <constructor-arg>
进行传递的
断点调试,UserDaoImpl 实例对象会存在于单例池中
实例工厂方法,也就是非静态工厂方法产生 Bean 实例,与静态工厂方式比较,该方式需要先有工厂对象,在用工厂对象去调用非静态方法,所以在进行配置时,要先配置工厂 Bean,在配置目标 Bean
1 | //工厂类 |
1 | <!-- 配置实例工厂Bean --> |
通过断点观察单例池 singletonObjects,发现单例池中既有工厂 Bean 实例,也有目标 Bean 实例,且都是在 Spring
容器创建时,就完成了 Bean 的实例化
上面不管是静态工厂方式还是非静态工厂方式,都是自定义的工厂方法,Spring 提供了 FactoryBean 的接口规范,
FactoryBean 接口定义如下:
1 | public interface FactoryBean<T> { |
定义工厂实现 FactoryBean
1 | public class UserDaoFactoryBean3 implements FactoryBean<UserDao> { |
配置 FactoryBean 交由 Spring 管理即可
1 | <bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean3"/> |
通过 Spring 容器根据 beanName 可以正常获得 UserDaoImpl
1 | ApplicationContext applicationContext = new ClassPathxmlApplicationContext("applicationContext.xml"); |
通过断点观察发现 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> 集合 – 普通数据
1 | void setStrList(List<String> strList){ |
1 | <property name="strList"> |
注入 List<T> 集合 – 引用数据
1 | public void setObjList(List<UserDao> objList){ |
1 | <property name="objList"> |
注入 List<T> 集合 – 引用数据
也可以直接引用容器中存在的 Bean
1 | <!--配置UserDao--> |
注入 Set<T> 集合
1 | //注入泛型为字符串的Set集合 |
1 | <!-- 注入泛型为字符串的Set集合 --> |
注入 Map<K,V> 集合
1 | //注入值为字符串的Map集合 |
1 | <!--注入值为字符串的Map集合--> |
注入 Properties 键值对
1 | //注入Properties |
1 | <property name="properties"> |
扩展:自动装配方式
如果被注入的属性类型是 Bean 引用的话,那么可以在 <bean>
标签中使用 autowire 属性去配置自动注入方式,属
性值有两个:
- byName:通过属性名自动装配,即去匹配 setXxx 与 id="xxx"(name="xxx")是否一致;
- byType:通过 Bean 的类型从容器中匹配,匹配出多个相同 Bean 类型时,报错。
举例:
1 | public class UserService { |
1 | <bean id="userDao" class="com.example.UserDao" /> |
在这个例子中:
- 我们首先定义了一个
userDao
Bean。 - 然后分别定义了
userService
和orderService
Bean, 并设置它们的autowire
属性为byType
。
当 Spring 容器在创建 userService
和 orderService
Bean 的时候,它会自动根据属性的类型 (在这里是 UserDao
) 去容器中查找匹配的 Bean, 并自动注入到对应的属性中。
也就是说,Spring 会自动将 userDao
Bean 注入到 userService
和 orderService
的 userDao
属性中,开发者不需要手动配置注入关系。
这种 byType
的自动注入方式在某些场景下非常方便,可以减少大量的手动配置工作。但前提是容器中只能有一个与属性类型匹配的 Bean, 否则会抛出异常
异常举例:
1 | public class UserService { |
1 | <bean id="adminUserDao" class="com.example.AdminUserDao" /> |
在这个例子中:
- 我们定义了两个实现了
UserDao
接口的 Bean, 分别是adminUserDao
和normalUserDao
。 - 然后定义了
userService
Bean, 并设置它的autowire
属性为byType
。
当 Spring 容器在创建 userService
Bean 的时候,它会去查找与 UserDao
类型匹配的 Bean。但由于容器中有两个匹配的 Bean ( adminUserDao
和 normalUserDao
),Spring 无法确定应该注入哪个,所以会抛出以下异常:
1 | org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.UserDao' available: expected single matching bean but found 2: adminUserDao,normalUserDao |
8)Spring 的其他配置标签
Spring 的 xml 标签大体上分为两类,一种是默认标签,一种是自定义标签
- 默认标签:就是不用额外导入其他命名空间约束的标签,例如
<bean>
标签 - 自定义标签:就是需要额外引入其他命名空间约束,并通过前缀引用的标签,例如
<context:property-placeholder/>
标签
Spring 的默认标签用到的是 Spring 的默认命名空间
1 |
|
该命名空间约束下的默认标签如下:
签 | 作用 |
---|---|
<beans> | 作为 XML 配置的根标签,其他标签都是该标签的子标签 |
<bean> | 用于定义 Bean 的配置信息,如 id、class、scope 等 |
<import> | 用于导入外部的 Spring 配置文件 |
<alias> | 用于为 Bean 指定一个或多个别名 |
<beans>
标签,除了经常用的做为根标签外,还可以嵌套在根标签内,使用 profile 属性切换开发环境
1 | <!-- 配置测试环境下,需要加载的Bean实例 --> |
可以使用以下两种方式指定被激活的环境:
使用命令行动态参数,虚拟机参数位置加载 -Dspring.profiles.active=test
使用代码的方式设置环境变量 System.setProperty ("spring.profiles.active","test")
<import>
标签,用于导入其他配置文件,项目变大后,就会导致一个配置文件内容过多,可以将一个配置文件根
据业务某块进行拆分,拆分后,最终通过 <import>
标签导入到一个主配置文件中,项目加载主配置文件就连同
<import>
导入的文件一并加载了
1 | <!--导入用户模块配置文件--> |
<alias>
标签是为某个 Bean 添加别名,与在 <bean>
标签上使用 name 属性添加别名的方式一样,我们为
UserServiceImpl 指定四个别名:aaa、bbb、xxx、yyy
1 | <!--配置UserService--> |
断点调试,在 beanFactory 中维护着一个名为 aliasMap 的 Map<String,String> 集合,存储别名和 beanName
之间的映射关系
Spring 的自定义标签需要引入外部的命名空间,并为外部的命名空间指定前缀,使用 <前缀:标签> 形式的标签,称
之为自定义标签,自定义标签的解析流程也是 Spring xml 扩展点方式之一
1 | <!--默认标签--> |
poe 对 spring 自定义标签的理解
假设我们需要在 Spring 配置文件中声明一个自定义的 <cache>
标签,用于配置缓存相关的属性。
首先,我们需要创建一个 Spring 命名空间,并定义对应的 XSD 文件。例如,我们可以创建一个 cache.xsd
文件:
1 |
|
然后,我们需要在 Spring 配置文件中引入自定义命名空间,并使用 <cache>
标签进行配置:
1 |
|
在这个例子中,我们定义了一个 <cache>
标签,它有三个属性: name
、 max-size
和 expire-after
。
最后,我们需要编写一个自定义标签的处理器,用于解析 <cache>
标签并将其转换为 Java 对象。例如:
1 | public class CacheBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { |
在这个自定义标签处理器中,我们解析 <cache>
标签的属性,并将其转换为 Java 对象的属性值。
通过这种方式,我们就可以在 Spring 配置文件中使用自定义的 <cache>
标签来配置缓存相关的设置了。