前言
随着SpringBoot的风靡,我们享受到了开箱即用带来的便利。但是,我们对于Spring的学习还是要从根本学起。要从最初的使用方式作为切入点,达到一通百通的目的。
本博文主要讲述了Spring框架的几种使用方式以及各种使用方式对应的注意事项。为之后的源码学习打下一个基础。也为之后的SpringBoot的学习做铺垫。
Spring的使用方式
BeanFactory与ApplicationContext区别
首先,我们来介绍一下BeanFactory和ApplicationContext的区别。
BeanFactory
是Spring框架中IoC容器的顶层接口,它只是用来定义⼀些基础功能,定义⼀些基础规范,而ApplicationContext
是它的⼀个子接口,所以ApplicationContext
是具备BeanFactory
提供的全部功能的。
通常,我们称BeanFactory
为Spring IoC的基础容器,ApplicationContext
是容器的⾼级接口,比BeanFactory
要拥有更多的功能,⽐如说国际化⽀持和资源访问(xml,java配置类)等等。它们的继承关系如下图:
启动 IoC 容器的方式
我们再来介绍一下IoC容器的启动方式。Spring为各个场景下都定制了特殊的容器类。每一种容器类都有它独到的启动方式。
java环境下启动IoC容器:
- ClassPathXmlApplicationContext:从类的根路径下加载配置文件(推荐使用)。
- FileSystemXmlApplicationContext:从磁盘路径上加载配置文件。
- AnnotationConfigApplicationContext:纯注解模式下启动Spring容器。
web环境下启动IoC容器:
- 从xml启动容器,配置方式如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--配置Spring IoC容器的配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--使用监听器启动Spring的IoC容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
- 从配置类启动容器,配置方式如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--告诉ContextloaderListener知道我们使用注解的方式启动IoC容器-->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!--配置启动类的全限定类名-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.rubin.SpringConfig</param-value>
</context-param>
<!--使用监听器启动Spring的IoC容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
纯xml的方式使用Spring
我们最开始接触Spring,当是官方推荐的就是xml模式。我们先来说一下纯xml方式使用Spring。
xml的文件头如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
创建bean的方式
此种方式下,初始化bean有三种方式:
- 使用无参构造函数
在默认情况下,它会通过反射调用无参构造函数来创建对象。如果类中没有无参构造函数,将创建失败。
<!--配置service对象-->
<bean id="userService" class="com.rubin.service.impl.UserServiceImpl">
</bean>
- 使用静态方法创建
在实际开发中,我们使用的对象有些时候并不是直接通过构造函数就可以创建出来的,它可能在创建的过程中会做很多额外的操作。此时会提供⼀个创建对象的⽅法,恰好这个方法是static修饰的⽅法,即是此种情况。
例如,我们在做Jdbc操作时,会用到java.sql.Connection
接口的实现类,如果是MySQL数据库,那么用的就是JDBC4Connection
,但是我们不会去写 JDBC4Connection connection = new JDBC4Connection()
,因为我们要注册驱动,还要提供URL和凭证信息,用DriverManager.getConnection
方法来获取连接。
那么在实际开发中,尤其早期的项目没有使用Spring框架来管理对象的创建,但是在设计时使用了工厂模式解耦,那么当接入Spring之后,工厂类创建对象就具有和上述例子相同特征,即可采用此种方式配置。
<!--使用静态方法创建对象的配置方式-->
<bean id="userService" class="com.rubin.factory.BeanFactory" factory-method="getUserService">
</bean>
- 使用实例化方法创建
此种方式和上面静态方法创建其实类似,区别是用于获取对象的方法不再是static修饰的了,而是类中的⼀ 个普通方法。此种方式比静态方法创建的使用几率要⾼⼀些。
在早期开发的项目中,工厂类中的方法有可能是静态的,也有可能是非静态方法,当是非静态方法时,即可采用下面的配置⽅式:
<!--使⽤实例⽅法创建对象的配置⽅式-->
<bean id="beanFactory" class="com.rubin.factory.instancemethod.BeanFactory">
</bean>
<bean id="transferService" factory-bean="beanFactory" factory-method="getTransferService">
</bean>
bean的作用范围与生命周期
作用范围
Spring框架管理的bean对象在创建时默认都是单例的。但是,我们可以通过配置的方式来改变bean的作用范围。bean
的所有作用范围如下图所示:
在上图列举的作用范围中,我们常用的也就是singleton(单例)和prototype(原型)。配置方式如下:
<!--配置service对象-->
<bean id="transferService" class="com.rubin.service.impl.TransferServiceImpl" scope="singleton">
</bean>
生命周期
bean
的不同的作用范围所对应的生命周期各不相同。常用的如下所示:
- 单例模式:singleton
对象出生:当创建容器的时候,对象就被创建了。
对象活着:只要容器不销毁,对象就一直存活。
对象死亡:容器销毁,对象也一起销毁。
一句话总结:单例模式的bean
的生命周期和容器相同。
- 多例模式:prototype
对象出生:当使用对象时,创建新的对象实例。
对象活着:只要对象在使用中,对象就一直存活。
对象死亡:对象长时间不用,就会被GC回收。
一句话总结:多例模式的bean
对象,Spring框架只负责创建,不负责销毁。
bean标签的属性
在基于xml的IoC配置中,bean
标签是最基础的标签。它表示了IoC容器中的一个对象。话句话说,如果一个对象想要让IoC容器管理,在xml的配置中都需要使用此标签配置。bean
标签的属性如下:
- id:用于给
bean
提供一个全局唯一个标识。 - classs:用于指定创建
bean
对象的全限定类名。 - name:用于给
bean
提供一个或多个名称。多个名称用空格分隔。 - factory-bean:用于指定创建该对象的工厂
bean
的唯一标识。当指定了该属性之后,class属性将失效。 - factory-method:用于指定创建改对象的工厂方法。若配合
factory-bean
使用,class属性将失效。若配合class使用,则该方法必须是静态的。 - scope:用于指定
bean
对象的作用范围。通常情况下就是singleton。当用到其他模式是可以自定义指定。 - init-method:用于指定
bean
对象的初始化方法,此方法会在bean
对象装配后被调用。该方法必须是一个无参的方法。 - destroy-method:用于指定
bean
对象的销毁方法,此方法会在bean
对象销毁前执行。它只能为scope为singleton的bean生效。
依赖注入的配置
依赖注入按照注入的方式分为以下两类:
- 构造器注入:利用有参构造器来给属性赋值。
- set方法注入:利用set方法来给属性赋值。
按照注入的数据类型,分为以下两类:
- 基本类型和String:注入的值为基本类型或者
String
。 - 其他bean类型:注入的数据类型是对象类型。称为其他bean类型的原因是,注入的对象类型要求出现在容器中。
- 复杂类型:注入的数据类型是
Array
、List
、Set
、Map
、Properties
中的一种类型。
构造器注入
顾名思义,就是利用构造函数实现对类成员的赋值。它的使用要求是,类中提供的构造函数参数个数必须和配置的参数个数⼀致,且数据类型匹配。同时需要注意的是,当没有⽆参构造时,则必须提供构造函数参数的注⼊,否则Spring框架会报错。
当使用有参构造器注入时,涉及的标签是constructor-arg
,改标签有如下属性:
- name:用于给构造函数中指定名称的参数赋值。
- index:用于给构造函数中指定索引位置的参数赋值。
- value:用于指定基本类型或者String类型的数据。
- ref:用于指定其他Bean类型的数据。写的是其他bean的唯⼀标识。
set方法注入
在使用set方法注入时,需要使用property标签,该标签属性如下:
- name:指定注入时调⽤的set方法名称。(注:不包含set这三个字⺟,druid连接池指定属性名称)。
- value:指定注入的数据。它⽀持基本类型和
String
类型。 - ref:指定注入的数据。它⽀持其他
bean
类型。写的是其他bean
的唯⼀标识。
复杂类型注入
复杂类型不是一个特殊的注入类型,而是它配置起来有些复杂,所以我们单独说一下。复杂类型在构造器注入和set方法注入都可以配置。我们以set方式注入为示例来讲解。
set方法注入的示例如下图所示:
在List结构的集合数据注入时,array
,list
, set
这三个标签通用,另外注值的value
标签内部可以直接写值,也可以使用bean
标签配置⼀个对象,或者用ref
标签引用⼀个已经配置的bean
的唯⼀标识。
在Map结构的集合数据注入时, map
标签使用entry
子标签实现数据注入,entry
标签可以使用key
和value
属性指定存⼊map
中的数据。使用value-ref
属性指定已经配置好的bean
的引⽤。同时entry
标签中也可以使⽤ref
标签,但是不能使用bean
标签。而property
标签中不能使用ref
或者bean
标签引用对象。
xml和注解相结合模式使用Spring
在我们的实际开发中,单纯使用xml的方式来使用Spring已经很罕见了。而xml和注解相结合的方式才是当下最流行的使用方式。
这种结合的使用方式,可以很清晰的管理我们自己的bean和第三方的bean
。我们在使用过程中,约定俗成的规则是:将第三方的bean
(数据库连接池等等)配置在配置文件中,而将我们自己开发的bean
使用注解来管理。
我们Spring的注解其实和xml中的配置项是一一对应的,下表列出了常用的注解和配置项的对应关系:
xml形式 | 对应的注解形式 |
标签 | @Component("accountDao"),注解加在类上。bean的id属性内容直接配置在注解后面。如果不配置,默认定义个这个bean的id为类的类名⾸字母小写。 另外,针对分层代码开发提供了@Componenet的三种别名@Controller、@Service、@Repository分别用于控制层类、服务层类、dao层类的bean定义,这四个注解的用法完全⼀样,只是为了更清晰的区分而已。 |
标签的scope属性 | @Scope("prototype"),默认单例,注解加在类上。 |
标签的init-method属性 | @PostConstruct,注解加在方法上,该方法就是初始化后调用的方法。 |
标签的destory-method属性 | @PreDestory,注解加在方法上,该方法就是销毁前调用的方法。 |
依赖注入
@Autowire注入
@Autowire
是Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired
。此中注入方式是我们比较推荐使用的一种。
@Autowire
默认的注入策略是按照类型注入。示例代码如下:
public class TransferServiceImpl {
@Autowired
private AccountDao accountDao;
}
如上代码所示,这样装配会去Spring容器中找到类型为AccountDao
的类,然后将其注入进来。这样会产生⼀个问题,当⼀个类型有多个bean
值的时候,会造成无法选择具体注入哪⼀个的情况,这个时候我们需要配合着@Qualifier
使用。
public class TransferServiceImpl {
@Autowired
@Qualifier(name="jdbcAccountDaoImpl")
private AccountDao accountDao;
}
@Resource注入
@Resource
注解由 J2EE 提供,需要导入包 javax.annotation.Resource
。@Resource
默认按照 ByName 自动注入。
public class TransferService {
@Resource
private AccountDao accountDao;
@Resource(name="studentDao")
private StudentDao studentDao;
@Resource(type="TeacherDao")
private TeacherDao teacherDao;
@Resource(name="manDao",type="ManDao")
private ManDao manDao;
}
它的注入规则如下:
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配。找不到则抛出异常。
- 如果指定了name,则从Spring上下文中找到id为name的bean进行装配。找不到则抛出异常。
- 如果指定了type,则从Spring上下文中找到类型和type匹配的bean进行装配。如果找到多个或找不到都会抛出异常。
- 如果既没有指定name,也没有指定type,则按照byName的方式自动装配。
注意:
@Resource
在 Jdk 11中已经移除,如果要使用,需要单独引入jar包:
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
纯注解方式使用Spring
顾名思义,也就是说我们可以完全使用注解的方式来使用Spring,而舍弃掉xml配置文件。常用的注解如下所示:
@Configuration
注解,表名当前类是⼀个配置类。@ComponentScan
注解,替代context:component-scan
。@PropertySource
,引入外部属性配置⽂件。@Import
引入其他配置类。@Value
对变量赋值,可以直接赋值,也可以使⽤ ${} 读取资源配置⽂件中的信息。@Bean
将⽅法返回对象加入Spring IoC容器。
Spring IoC高级特性
lazy-init延迟加载
ApplicationContext
容器的默认行为是在启动服务器时将所有singleton bean
提前进⾏实例化。提前实例化意味着作为初始化过程的⼀部分,ApplicationContext
实例会创建并配置所有的singleton bean
。
比如:
<bean id="testBean" class="com.rubin.LazyBean"/>
该bean默认的设置为:
<bean id="testBean" calss="com.rubin.LazyBean" lazy-init="false"/>
lazy-init="false"
就表示立即加载,也就是在容器启动的时候就实例化。
如果我们不想让一个singleton bean
在容器启动的时候就实例化,而是想着什么时候用到,什么时候再实例化的话,我们可以设置如下:
<bean id="testBean" calss="com.rubin.LazyBean" lazy-init="true"/>
如果一个设置了立即加载的bean1
引用了一个延迟加载的bean2
,那么bean1
在容器启动时实例化的过程中会把bean2
也实例化好,这也符合延迟加载在第一次使用时被加载的规则。
我们也可以在容器层次中通过在元素上设置default-lazy-init
属性来控制全局的bean是否默认延迟加载,如下所示:
<beans default-lazy-init="true">
<!-- no beans will be eagerly pre-instantiated... -->
</beans>
如果一个bean
的scope
设置为了scope="pototype"
时,即使设置了lazy-init="false"
,容器启动的时候也不会实例化该bean
。而是等到调用getBean()
方法的时候实例化。
延迟加载的应用场景是可以将一些加载耗时并且不常用的bean设置成延迟加载。这样的话会提高系统的启动速度,也可以在不使用该bean的时候不加载,节省了资源。
FactoryBean和BeanFactory
BeanFactory
接口是容器的顶级接口,定义了容器的一些基本行为是负责生产和管理bean
的一个工厂。我们是用的是它下面具体的子接口类型,例如ApplicationContext
。
Spring中bean
有两种类型:一种是普通的bean
,一种是FactoryBean
。FactoryBean
可以生成一个具体的bean
实例返回给容器,也就是说,我们可以借助它来自定义实现bean
的创建过程。
bean
创建的三种方式中的静态方法和实例化方法和FactoryBean
作用类似,FactoryBean
使用较多,尤其在Spring框架⼀些组件中会使用,还有其他框架和Spring框架整合时使用。
// 可以让我们⾃定义Bean的创建过程(完成复杂Bean的定义)
public interface FactoryBean<T> {
@Nullable
// 返回FactoryBean创建的Bean实例,如果isSingleton返回true,则该实例会放到Spring容器的单例对象缓存池中Map
T getObject() throws Exception;
@Nullable
// 返回FactoryBean创建的Bean类型
Class<?> getObjectType();
// 返回作⽤域是否单例
default boolean isSingleton() {
return true;
}
}
例如我们有一个自定义的类User
:
@Data
public class User {
private Long id;
private String name;
private Integer age;
}
我们来自定义过一个FactoryBean
来自定义构件我们的User
对象:
public class UserFactoryBean implements FactoryBean<User> {
private String userInfo;
public void setCompanyInfo(String companyInfo) {
this.companyInfo = companyInfo;
}
@Override
public Company getObject() throws Exception {
// 模拟创建复杂对象User
User user = new User();
String[] strings = userInfo.split(",");
user.setId(Long.valueOf(strings[0]));
user.setName(strings[1]);
user.setAge(Integer.valueOf(strings[2]));
return user;
}
@Override
public Class<?> getObjectType() {
return User.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
将其配置到我们的容器中:
<bean id="userBean" class="com.rubin.factory.UserFactoryBean">
<property name="userInfo" value="1,小明,18"/>
</bean>
测试我们的配置结果:
Object userBean = applicationContext.getBean("userBean");
System.out.println("bean:" + userBean);
// 结果如下
bean:User{id=1, name='小明', age=18}
获取FactoryBean
,我们需要在id前面加一个"&":
Object userBean = applicationContext.getBean("&userBean");
System.out.println("bean:" + userBean);
// 结果如下
bean:com.rubin.factory.UserFactoryBean@53f6fd09
后置处理器
BeanPostProcessor
BeanPostProcessor
是针对bean
级别的处理。可以针对具体的某个bean
。定义如下图所示:
该接口提供了两个方法,分别在bean
的初始化方法前和初始化方法后执行。具体这个初始化方法是什么方法,类似于我们定义的init-method
方法。
定义⼀个类实现了BeanPostProcessor
,默认是会对整个Spring容器中所有的bean
进⾏处理。如果要对具体的某个bean
处理,可以通过方法参数判断,两个类型参数分别为Object
和String
,第⼀个参数是每个bean
的实例,第⼆个参数是每个bean
的name
或者id
属性的值。所以我们可以通过第⼆个参数,来判断我们将要处理的具体的bean
。
注意:处理是发生在Spring容器的实例化和依赖注⼊之后。
BeanFactoryPostProcessor
BeanFactory
级别的处理,是针对整个bean
的工厂进行处理,典型应用:PropertyPlaceholderConfigurer
。
此接口只提供了一个方法,方法参数为ConfigrableListableBeanFactory
。该参数类型定义了一些方法:
其中,有一个getBeanDefinition(String)
的方法。我们可以根据此方法,找到我们定义bean
的BeanDefinition
对象。然后我们对定义的属性进行修改,以下是BeanDefinition
中的方法:
方法名字类似我们bean
标签的属性,setBeanClassName(String)
对应bean
标签中的class
属性,所以当我们拿
到BeanDefinition
对象时,我们可以⼿动修改bean
标签中所定义的属性值。
BeanDefinition对象:我们在xml或者配置类里面配置的类或者标有@Component
注解以及子注解的类,都会被Spring容器解析成一个描述bean
对象的JavaBean
。这个JavaBean
就是我们的BeanDefinition
。
注意:容器调用BeanFactoryPostProcessor
的方法时,bean
还没有实例化,只是全部被解析成了BeanDefinition
对象。
文章评论