Spring概述
我们常说的 Spring 实际上是指 Spring Framework,而 Spring Framework 只是 Spring 家族中的一个分支而已。Spring 是为了解决企业级应用开发的复杂性而创建的。
如果我们想实现某个功能,代码量一般都是固定的,要么全自己写,要么用已有的优秀框架,而Spring不仅已经给我们提供了各种优秀组件,还提供了良好的代码组织逻辑跟业务开发流程规范框架,我们主要学习Spring中以下几点:
- IOC/DI
- AOP
- 声明式事务
- JdbcTemplate
Spirng组件
Spring框架具有很多组件,大约有20多个模块 。我们要记住以下7个核心组件和它的含义。
- Spring Core:Spring核心,它是框架最基础的部分,提供IOC和依赖注入DI特性。
- Spring Context:Spring上下文容器,它是 BeanFactory 功能加强的一个子接口。
- Spring Web:它提供Web应用开发的支持。
- Spring MVC:它针对Web应用中MVC思想的实现。
- Spring DAO:提供对JDBC抽象层,简化了JDBC编码,同时,编码更具有健壮性。
- Spring ORM:它支持用于流行的ORM框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO的整合等。
- Spring AOP:即面向切面编程,它提供了与AOP联盟兼容的编程实现。
下图就是maven导入spring后的组件:
IOC
IOC(Inversion of Control),中文叫做控制反转。
Spring提出了一种思想:由Spring
来负责控制对象的生命周期和对象间的关系。所有的类都会在Spring容器中登记,告诉Spring这
这个类是什么,需要什么,然后Spring会在系统运行到适当的时候,把你要的东西主动
给你,同时也把你交给其他
需要你的Bean。所有的类的创建、销毁都由Spring
来控制,也就是说控制对象生存周期的不再是引用它的对象,而是Spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转(Inversion of Controller),也可以叫依赖注入 DI(Dependency Injection)。
举个例子:
public class Book {
private Integer id;
private String name;
private Double price;
//省略getter/setter
}
public class User {
private Integer id;
private String name;
private Integer age;
public void doSth() {
Book book = new Book();
book.setId(1);
book.setName("故事新编");
book.setPrice((double) 20);
}
}
上面这个例子中,Book对象的控制权在User里面,Book和User高度耦合,如果在其他对象中需要使用 Book 对象,得重新创建,也就是说,对象的创建、初始化、销毁等操作,统统都要开发者自己来完成。
使用 Spring 之后,我们可以将对象的创建、初始化、销毁等操作交给 Spring 容器来管理。就是说,在项目启动时,所有的 Bean 都将自己注册到 Spring 容器中去(如果有必要的话),然后如果其他 Bean 需要使用到这个 Bean ,则不需要自己去 new,而是直接去 Spring 容器去要。
什么是Bean
我们本篇会一直提到Bean,先在前文给Bean做一个大概的介绍。Spring Bean是被实例的,组装的及被Spring 容器管理的Java对象。Spring 容器会自动完成@bean对象的实例化。创建应用对象之间的协作关系的行为称为:装配(wiring),这就是依赖注入的本质。
何为控制
是 bean 的创建、管理的权利,控制 bean 的整个生命周期。
何为反转
把这个权利交给了 Spring 容器,而不是自己去控制,就是反转。由之前的自己主动创建对象,变成现在被动接收别人给我们的对象的过程,这就是反转。
何为依赖
程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。
何为注入
配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系。
实例1:进一步了解IOC
我们在IDEA创建一个普通Maven项目,然后再pom文件中引入spring-context 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>SpringDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>
</project>
然后在 resources 目录下创建一个 spring 的配置文件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 http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
文件中我们可以配置bean,把我们的book配置进去
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.javaboy.Book" id="book"/>
</beans>
说明:class 属性表示需要注册的 bean 的全路径,id 则表示 bean 的唯一标记,也开可以 name 属性作为 bean 的标记,在超过 99% 的情况下,id 和 name 其实是一样的。
最后,我们来测试一下,创建一个Main方法来加载这个配置文件,通过 getBean 方法,从容器中去获取对象:
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = (Book) ctx.getBean("book");
System.out.println(book);
}
}
打印的结果为:Book{id=null, name='null', price=null}
。
说明:上面getBean中传入的是我们命名的name或id,这种方式好处是我们给它起了个名,在引入两个或多个相同对象时,有别名不至于混淆。有些教程喜欢在getBean中直接通过Class 去获取一个 Bean,如传入Book.class
,如果spring.xml中引入两个book的bean,那么这种做法就会报错,所以我个人不推荐通过Class去获取Bean。
name与id之间的一些注意点
上文中提到了name与id,确实,大部分时候name和id是一样的,也很少在开发中又用name又用id,但是还是要注意一些细节。
- 配置两个相同的 id 或者 name 都不能通过。
- 如果既配置了 id ,也配置了 name ,则两个都生效。如果id和name都没有指定,则用类全名作为name,如
<bean class="com.stamen.BeanLifeCycleImpl">
,则你可以通过getBean("com.stamen.BeanLifeCycleImpl")
返回该实例。 - 如果配置基本类的时候,注解和配置文件都使用的时候,注解和配置文件中 name 相同的时候, 则两个冲突,配置文件生效; 如果配置基本类的时候,注解和配置文件都使用的时候,注解和配置文件中 name 不相同的时候, 则两个不冲突,都能够生效。
属性注入
上一个例子中我们明白了Spirng的IOC可以帮我们管理Bean,将对象的创建、初始化、销毁等操作交给Spring管理,使开发更加方便。此外,上一个例子中我们在spring.xml中配置了Book,最后打印的结果中我们看到Book中的属性并没有值,是null。这一小节就是了解spring是如何注入属性的,有以下几种方式:
- 构造方法注入
- set方法注入
- p命名空间注入
- 自动装配和@Autowired注解注入
- 静态工厂注入
- 实例工厂注入
构造方法注入
-
给Book添加一个有参和无参构造方法:
-
在xml中注入bean
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean class="com.wms.Demo01.Book" id="book"> <constructor-arg index="0" value="1"/> <constructor-arg index="1" value="宇宙未解之谜"/> <constructor-arg index="2" value="39.9"/> </bean> </beans>
或者使用name标签,更加清晰
<bean class="com.wms.Demo01.Book" id="book"> <constructor-arg name="id" value="1"/> <constructor-arg name="name" value="宇宙未解之谜"/> <constructor-arg name="price" value="39.9"/> </bean>
-
说明:
constructor-arg
--> 指定构造函数的参数index
--> 参数的顺序name
--> 参数的名称value
--> 给参数赋值- 注意,你也可以按index2, 0, 1等顺序来给属性注入值,但记得 index 的值和 value 要一一对应,id就要注入id相符的值。
set方法注入
set方法注入方式如下:
<bean class="com.wms.Demo01.Book" id="book">
<property name="id" value="1"/>
<property name="name" value="宇宙未解之谜"/>
<property name="price" value="39.9"/>
</bean>
注意这里使用的是property标签,而且name并不是Book这个对象中定义的属性,而是通过get/set方法分析出来的属性名,只是我们规范get/set方法就是get/set + 属性名。
p命名空间注入
p 名称空间注入,使用的比较少,它本质上也是调用了 set 方法。
<bean class="com.wms.Demo01.Book" id="book" p:id="1" p:bookName="宇宙未解之谜" p:price="39.9"></bean>
自动装配和@Autowired注解注入
我将这两个放在一起讲,因为要介绍到byName和byTpye的区别。Spring提供了自动装配的功能,简化了我们的配置,自动装配默认是不打开的,常用的方式有两种,byName和byType。方式是在bean标签中加入autowire="byName"
或autowire="byType"
。
<bean class="com.wms.Demo01.Book" id="book" autowire="byType"/>
后续我们还会接触到@Autowired注解,@Autowired注解可以实现自动装配,只要在对应的属性上标记该注解,但是@Autowired注解只按照byType注入。这个注解常见于Controller层。如下:
public class UserController {
@Autowired
private IUserService userService;
}
这里我们主要还是熟悉byName和byType的区别。其实byName根据被注入的名称作为bean名称作为依赖查找,并将对象设置到该属性。byType通过属性的类型查找javabean依赖的对象并为其注入。
byName和byType
-
byName按名称自动装配,当一个bean节点带有 autowire byName的属性时。
- 将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。
- 去spring容器中寻找是否有此字符串名称id的对象。
- 如果有,就取出注入;如果没有,就报空指针异常。
-
byType按类型自动装配,使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常。
@Autowired、@Qualifier和@Resource
既然讲到了@Autowired,就展开讲一下一些相关的注解。(下面出现的代码来自狂神说的spring教程)
-
@Autowired
- @Autowired是按类型自动转配的,不支持id匹配。
- 需要导入 spring-aop的包!
- @Autowired(required=false) 说明:false,对象可以为null;true,对象必须存对象,不能为null。
-
@Qualifier
-
@Autowired是根据类型自动装配的,加上@Qualifier则可以根据byName的方式自动装配。
-
@Qualifier不能单独使用。
-
在属性上添加Qualifier注解
-
示例:
@Autowired @Qualifier(value = "cat2") private Cat cat; @Autowired @Qualifier(value = "dog2") private Dog dog;
-
-
-
@Resource
- @Resource如有指定的name属性,先按该属性进行byName方式查找装配;
- 其次再进行默认的byName方式进行装配;
- 如果以上都不成功,则按byType的方式自动装配。
- 都不成功,则报异常。
实体类:
public class User {
//如果允许对象为null,设置required = false,默认为true
@Resource(name = "cat2")
private Cat cat;
@Resource
private Dog dog;
private String str;
}
beans.xml
<bean id="dog" class="com.kuang.pojo.Dog"/>
<bean id="cat1" class="com.kuang.pojo.Cat"/>
<bean id="cat2" class="com.kuang.pojo.Cat"/>
<bean id="user" class="com.kuang.pojo.User"/>
测试:结果OK
配置文件2:beans.xml , 删掉cat2
<bean id="dog" class="com.kuang.pojo.Dog"/>
<bean id="cat1" class="com.kuang.pojo.Cat"/>
实体类上只保留注解
@Resource
private Cat cat;
@Resource
private Dog dog;
结果:OK
结论:先进行byName查找,失败;再进行byType查找,成功。
@Autowired与@Resource异同:
1、@Autowired与@Resource都可以用来装配bean。都可以写在字段上,或写在setter方法上。
2、@Autowired默认按类型装配(属于spring规范),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用
3、@Resource(属于J2EE复返),默认按照名称进行装配,名称可以通过name属性进行指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
静态工厂注入(TODO)
实例工厂注入(TODO)
更多属性注入方式
上述的属性注入方式是比较基础的注入,实际开发中遇到的问题往往更加复杂,比如在开发过程中可能会注入跟多类型的数据,如:
- 对象
- 数组
- Map
对象注入
可以通过 ref 来引用一个对象。
<bean class="com.wms.Demo01.User" id="user">
<property name="cat" ref="cat"/>
</bean>
<bean class="org.javaboy.Cat" id="cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>
数组注入
-
array
<bean class="com.wms.Demo01.User" id="user"> <property name="cat" ref="cat"/> <property name="favorites"> <array> <value>足球</value> <value>篮球</value> <value>乒乓球</value> </array> </property> </bean> <bean class="com.wms.Demo01.Cat" id="cat"> <property name="name" value="小白"/> <property name="color" value="白色"/> </bean>
-
list
-
array 节点,也可以被 list 节点代替。
-
array 或者 list 节点中也可以是对象。
-
即可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。
<bean class="com.wms.Demo01.User" id="user"> <property name="cat" ref="cat"/> <property name="favorites"> <list> <value>足球</value> <value>篮球</value> <value>乒乓球</value> </list> </property> <property name="cats"> <list> <ref bean="cat"/> <ref bean="cat2"/> <bean class="com.wms.Demo01.Cat" id="cat3"> <property name="name" value="小花"/> <property name="color" value="花色"/> </bean> </list> </property> </bean> <bean class="com.wms.Demo01.Cat" id="cat"> <property name="name" value="小白"/> <property name="color" value="白色"/> </bean> <bean class="com.wms.Demo01.Cat" id="cat2"> <property name="name" value="小黑"/> <property name="color" value="黑色"/> </bean>
-
Map 注入
<property name="map">
<map>
<entry key="age" value="100"/>
<entry key="name" value="abc"/>
</map>
</property>
Properties 注入
<property name="info">
<props>
<prop key="age">100</prop>
<prop key="name">abc</prop>
</props>
</property>
补充
- 如果炫技,上面的都可以说。
- 如果问Spring对象创建方式,要说到构造方法、静态工厂、实例工厂
- 如果问到Spring注入方式,要说到构造方法、set方法、自动注入、p命名空间
Context
IOC 容器只是提供一个管理对象的空间而已,如何向容器中放入我们需要容器代为管理的对象呢?这就涉及到Spring的应用上下文Context。
工作中通过XML配置或注解 将需要管理的Bean跟Bean之间的协作关系配置好,然后利用应用上下文对象Context加载进Spring容器,容器就能为你的程序提供你想要的对象管理服务了。Spring 框架本身就提供了很多个容器的实现。我们在实例1中的Main中出现了ClassPathXmlApplicationContext
,就是一种容器,还有很多容器,如下:
- AnnotationConfigApplicationContext:从一个或多个基于java的配置类中加载上下文定义,适用于java注解的方式。
- ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式。
- FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件。
- AnnotationConfigWebApplicationContext:专门为web应用准备的,适用于注解方式。
- XmlWebApplicationContext:从web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式。
比如ClassPathXmlApplicationContext
,来自于我们常提到的ApplicationContext,但如果你点开源码看,就知道ClassPathXmlApplicationContext
并不是直接实现ApplicationContext的,而是一层一层的递进,这是为了IOC全面性而考虑。此处的ApplicationContext也是面试中经常出现的问题,经常与BeanFactory一起作比较。
ApplicationContext & BeanFactory区别
BeanFactory接口
- spring的原始接口,针对原始接口的实现类功能较为单一, 可以理解为 HashMap:它一般只有 get, put 两个功能。
- Key - bean name
- Value - bean object
- BeanFactory接口实现类的容器,特点是每次在获得对象时才会创建对象
- 优缺点:
- 优点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
- 缺点:运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过Bean工厂创建的Bean生命周期会简单一些。
ApplicationContext接口
- 每次容器启动时就会创建容器中配置的所有对象
- 它是
BeanFactory
的子类,更好的补充并实现了BeanFactory.ApplicationContext
多了很多功能,因为它继承了多个接口。ApplicationContext
的里面有两个具体的实现子类,用来读取配置配件的,上面列举了5个。 - 优缺点:
- 优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快;在系统启动的时候,可以发现系统中的配置问题。
- 缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是内存占用较大。
BeanFactory & FactoryBean的区别
既然提到了BeanFactory,面试中往往会用FactoryBean来“坑”面试者,这里就讲一下两者的区别。
- BeanFactory
- BeanFactory 以 Factory 结尾,表示它是一个工厂类(接口),BeanFacotry 是 Spring 中比较原始的Factory。
- BeanFactory 无法支持 Spring 的许多插件,如AOP功能、Web应用等。ApplicationContext 接口由BeanFactory接口派生而来,提供了国际化访问、事件传播等多个功能。
- BeanFactory 是 IOC 容器的核心,负责生产和管理 Bean 对象。
- FactoryBean
- FactoryBean 以 Bean 结尾,表示它是一个Bean。
- FactoryBean 是工厂类接口,用户可以通过实现该接口定制实例化 Bean 的逻辑。FactoryBean 接口对于 Spring 框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。
- 当在IOC容器中的Bean实现了 FactoryBean 后,通过getBean(String BeanName)获取到的 Bean 对象并不是 FactoryBean 的实现类对象,而是这个实现类中的 getObject() 方法返回的对象。要想获取FactoryBean的实现类,就要 getBean(String &BeanName),在BeanName之前加上 &。
循环依赖
从字面上来理解就是A依赖B的同时B也依赖了A,例如
自动化配置
例如我有一个 UserService,我希望在自动化扫描时,这个类能够自动注册到 Spring 容器中去,那么可以给该类添加一个 @Service,作为一个标记。
和 @Service 注解功能类似的注解,一共有四个:
-
@Component
-
@Repository
-
@Service
-
@Controller
这四个中,另外三个都是基于 @Component 做出来的,而且从目前的源码来看,功能也是一致的,那么为什么要搞三个呢?主要是为了在不同的类上面添加时方便。
-
在 Service 层上,添加注解时,使用 @Service
-
在 Dao 层,添加注解时,使用 @Repository
-
在 Controller 层,添加注解时,使用 @Controller
-
在其他组件上添加注解时,使用 @Component
添加完成后,自动化扫描有两种方式,一种就是通过 Java 代码配置自动化扫描,另一种则是通过 xml 文件来配置自动化扫描。
Java 代码配置自动扫描
在项目启动中加载配置类,在配置类中,通过 @ComponentScan
注解指定要扫描的包(如果不指定,默认情况下扫描的是配置类所在的包下载的 Bean 以及配置类所在的包下的子包下的类)
XML 配置自动化扫描
<context:component-scan base-package="org.javaboy.javaconfig"/>
上面这行配置表示扫描 org.javaboy.javaconfig 下的所有 Bean。当然也可以按照类来扫描。
Bean的生命周期和作用域
这两个问题也是面试中常客
Bean的生命周期
Spring IOC 初始化跟销毁 Bean 的过程大致分为Bean定义、Bean初始化、Bean的生存期 跟 Bean的销毁4个部分。流程图如下:
浓缩一下:
Bean的生命周期,从Spring容器的创建开始,到Spring容器销毁结束。
- 实例化Bean对象
- 装配:填充属性
- 回调:(可选,如果实现了Aware系列的接口,则会调用回调函数)
- 调用预初始化方法(可选,如果实现了BeanPost-Processor的预初始化方法)
- 初始化(init-method)
- 调用预初始化后置方法(如果实现了BeanPost-Processor的初始化后方法)
- 使用bean
- 容器关闭
- 如果实现了DisposableBean接口,则调用该方法的destory()方法。
- 调用自定义的destory方法。
Bean的作用域
在 XML 配置中注册的 Bean,或者用 Java 配置注册的 Bean,如果我多次获取,获取到的对象是否是同一个?
答案是是,因为Spring中Bean默认是单例的,所以多次获取的Bean都是同一个。这里就涉及到Bean的作用域的知识点,
四种常见的 Spring Bean 的作用域:
-
singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
-
prototype : 每次请求都会创建一个新的 bean 实例。
-
request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
-
session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
怎么更改作用域?
xml中:
<bean class="org.javaboy.User" id="user" scope="prototype" />
注解:
@Repository @Scope("prototype") public class UserDao { public String hello() { return "userdao"; } }