Spring IoC
文章目录
Spring是一个轻量级的免费框架,其最重要的两个特性就是控制反转(IoC)和面向切面(AOP)。
1. IoC概念
要了解控制反转(IoC)是什么,首先要了解它的概念。
假设我们有一个接口:
UserDao.java
public interface UserDao{
public void getUser();
}
然后我们去写它的两个实现类,他们都实现了getUser()方法:
FirstUserDao.java
public class FirstUserDao implements UserDao{
private String daoName;
@Override
public void getUser(){
System.out.println("get First Dao User.")
}
}
SecondUserDao.java
public class SecondUserDao implements UserDao{
private String daoName;
@Override
public void getUser(){
System.out.println("get Second Dao User.")
}
}
这个时候如果我想要用Service来使用Dao:
UserService.java
public class UserService{
private UserDao userDao = new FirstUserDao();
//private UserDao userDao = new SecondUserDao();
public void getUser(){
userDao.getUser();
}
}
这个时候就会发现一个问题。当我们的Service因为业务需求,不再使用FirstUserDao而要使用SecondUserDao的时候,我们就必须修改UserService中的调用代码。
但这种修改是很不理想的修改。
首先,这违背了开闭原则(OCP),一个好的程序应该对扩展开放,对修改关闭。再者,若按照这样修改,当每次需求变化时,我们都需要程序员手工修改代码,或者当两个或以上的需求同时需要上线时,我们甚至需要添加几倍冗余的代码。这种开发的耦合性太高了。
既然不能用初始化引用指向具体对象的方式,我们是否可以用setter来实现?
public class UserService{
private UserDao userDao;
public void setUserDao(UserDao userDao){
this.userDao = userDao;
}
public void getUser(){
userDao.getUser();
}
}
通过这么一个小改动,我们在修改需要使用的UserDao实现时,只需要把需要使用的UserDao放进去即可,不再需要修改调用代码了。
那这里又有了问题,这实际上不还是要传入具体的UserDao对象吗?其实这很好解决。确实这个setter仍需要传入具体的UserDao,但是若我们使用工厂模式,让工厂根据我们具体的需求来输出实例化好的特定UserDao,我们就可以根据具体需求来得到具体的服务。
public class UserDaoFactory{
public static UserDao getUserDao(String UserDaoName){
UserDao userDao;
switch(UserDaoName){
case "FirstUserDao":
userDao = new FirstUserDao();
case "SecondUserDao":
userDao = new SecondUserDao();
}
return userDao;
}
}
但这种工厂也没能遵守我们的开闭原则,当我们新增一个UserDao的实现时,都需要在工厂上改代码。为了解决这个修改的问题,我们可以使用反射机制来优化。
public class UserDaoFactory{
public static UserDao getUserDao(String className){
UserDao userDao;
try{
userDao = (UserDao)Class.forname(className).newInstance();
} catch (Exception e){
e.printStackTrace();
}
return userDao;
}
}
这样,我们就可以通过反射机制,在程序运行的时候输入具体的className来生成所需的UserDao对象。
那在Spring中,这个className是怎么得到的呢?
答案是:从配置文件中读取。
我们可以从beans.xml中写入对应的类名,使用的就是一个叫做bean的对象。Spring官方文档中是这么解释bean的:
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.
意思是:
在Spring中,构成你应用程序主干并由Spring IoC容器管理的对象称为bean。bean是一个由Spring IoC容器实例化、组装和管理的对象。
实际上,bean在application.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">
<!--bean就是java对象 , 由Spring创建和管理-->
<bean id="firstUserDao" class="com.zky.dao.FirstUserDao">
<property name="daoName" value="firstDaoName"></property>
</bean>
<bean id="secondUserDao" class="com.zky.dao.SecondUserDao">
<property name="daoName" value="secondDaoName"></property>
</bean>
</beans>
可以看到,这里有bean的id,有bean对应的className,有成员属性赋值。不难看出,我们就是在这里读取配置文件中的className,并使用className通过工厂模式得到具体的UserDao实现对象。
从这里我们可以得到IoC的一个原理了。所谓的IoC容器就是一个工厂,工厂根据在配置文件里写好的class,利用反射机制生成配置文件中bean的实例化对象,最后再交给调用代码使用。
IoC的全称控制反转(Inverse of Control),实际上就分成控制和反转。控制是指创建指定对象的控制权,反转就是指这项控制由程序主动创建反转为交给IoC容器来控制。
上面所写的都只是描述IoC思想的代码,并非Spring的源码,源码解析请看这篇文章。
IoC小案例
首先,想要使用IoC,要先导入相应的maven依赖。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.10.RELEASE</version>
</dependency>
一个UserDao接口:
public interface UserDao{
public void getUser();
}
一个实现类:
public class FirstUserDao implements UserDao{
private String daoName;
@Override
public void getUser(){
System.out.println("get First Dao User.")
}
}
一个定义了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就是java对象 , 由Spring创建和管理-->
<bean id="firstUserDao" class="com.zky.dao.FirstUserDao">
<property name="daoName" value="firstDaoName"></property>
</bean>
</beans>
接下来我们就可以来做测试了:
@Test
public void test2(){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
UserDao userDao = context.getBean("firstUserDao");
userDao.getUser();
}
2. IoC创建对象
IoC有无参构造、有参构造和命名空间注入三种注入方法。这里我只介绍最常见的无参构造和有参构造两种方法。
无参构造
IoC的无参构造是根据setter来注入属性的。
User.java
public class User{
private String name;
public void setName(String name) {
this.name = name;
}
public void display(){
System.out.println("I'm "+name);
}
}
application.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">
<bean id="user" class="com.zky.pojo.User">
<property name="name" value="zky"/>
</bean>
</beans>
测试:
@Test
public void test2(){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
User user = (User)context.getBean("user");
user.display();
}
有参构造
有参构造通过构造函数来注入。
public class User{
private String name;
public User(String name){
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public void display(){
System.out.println("I'm "+name);
}
}
application.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">
<bean id="user" class="com.zky.pojo.User">
<constructor-arg index="0" value="zky"/>
</bean>
</beans>
测试:
@Test
public void test2(){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
User user = (User)context.getBean("user");
user.display();
}
import
调用不同的bean配置文件
<import resource="{path}/beans.xml">
3. 依赖注入
依赖注入(Dependence Injection, DI),可以分解为依赖和注入。
依赖:一个类可能需要调用其他的类,此时这个类依赖于其他多个类,在创建一个类的时候,或许要附带创建其他它依赖的类。(比如组合的时候)
注入:实例化这个类并将这个类实体化所依赖的其他类实例化并注入到实例中。
set注入
属性注入在上面的例子已写,这里主要写Bean注入。
User.java
public class User{
private String name;
private Phone phone;
public void setName(String name) {
this.name = name;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
}
Phone.java
public class Phone{
private String name;
private Double price;
public void setName(String name) {
this.name = name;
}
public void setPrice(Double price) {
this.price = price;
}
}
application.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">
<bean id="user" class="com.zky.pojo.User">
<property name="name" value="zky"/>
<property name="phone" ref="phone"/>
</bean>
<bean id="phone" class="com.zky.pojo.Phone">
<property name="name" value="xiaomi"/>
<property name="price" value="2999"/>
</bean>
</beans>
这里只给出Bean注入的例子,其他例如数组、List、Map、set、Null、Properties注入请自行查阅。
构造器注入
这种注入方式和set注入差不多,只是使用constructor-arg标签进行ref注入,此处不再赘述。
4. Bean作用域
Bean的作用域分为四类:
Singleton
Singleton表示单例,当Bean的作用域是Singleton,这表示IoC容器中只会创建一个Bean对象,无论请求多少次Bean,得到的都是同一个Bean。
配置方法如下:
<bean id="FirstuserDao" class="com.zky.dao.FirstUserDao" scope="singleton"></bean>
Bean默认作用域是singleton
Prototype
当Bean作用域为Prototype时,每次从容器中调用Bean时都返回一个新的实例。
配置方法如下:
<bean id="FirstuserDao" class="com.zky.dao.FirstUserDao" scope="prototype"></bean>
或者
<bean id="FirstuserDao" class="com.zky.dao.FirstUserDao" scope="false"></bean>
Request
当Bean作用域为Prototype时,每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext。
配置方法如下:
<bean id="login" class="com.zky.utils.Login" scope="request"></bean>
Session
当Bean作用域为Session时,每个Session共享一个Bean,不同的Session中的Bean仅在自己的Session中有效。Session被销毁时,其对应的Bean也被销毁。该作用域仅适用于WebApplicationContext。
配置方法如下:
<bean id="userLoginMsg" class="com.zky.utils.userLoginMsg" scope="session"></bean>
5. 自动装配(Autowire)
自动装配机制可以在创建对象时不需要在xml或者java显示注入对象,而是让Spring自动扫描Bean并按照名字或者类型自动装配Bean。这样就可以不用ref的显示注入方法,可减少很多工作量。
在xml中有两种自动装配方法,分别是按名字装配(byName)和按类型装配(byType)。除此之外,还可以使用注解的方式自动装配。
byName
通过autowire=“byName”,就可以按照set方法名找到对应的bean id并自动装配注入。
例如:User里有setPhone()方法,那么就按照set后首字母小写的名字phone作为bean id自动注入。
<?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 id="user" class="com.zky.pojo.User" autowire="byName">
<property name="name" value="zky"/>
</bean>
<bean id="phone" class="com.zky.pojo.Phone">
<property name="name" value="xiaomi"/>
<property name="price" value="2999"/>
</bean>
</beans>
byType
通过autowire=“byType”,就可以按照类型找到对应class的bean并自动装配注入。
这里要注意的是,由于是按照类型来注入,所以需要保证通过类型自动装配的Bean,其类型只有这唯一对象,否则会报NoUniqueBeanDefinitionException异常。
<?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 id="user" class="com.zky.pojo.User" autowire="byType">
<property name="name" value="zky"/>
</bean>
<bean id="phone" class="com.zky.pojo.Phone">
<property name="name" value="xiaomi"/>
<property name="price" value="2999"/>
</bean>
</beans>
@Autowired
@Autowired使用注解进行自动装配,按照类型自动装配。使用@Autowired需要导spring-aop依赖。
public class User{
private String name;
@Autowired
private Phone phone;
public void setName(String name) {
this.name = name;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
}
在需要自动装配的属性上加一个@Autowired即可。
此时application.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">
<bean id="user" class="com.zky.pojo.User">
<property name="name" value="zky"/>
</bean>
<bean id="phone" class="com.zky.pojo.Phone">
<property name="name" value="xiaomi"/>
<property name="price" value="2999"/>
</bean>
</beans>
@Qualifier
@Qualifier不能单独使用,而是以@Autowired组合使用,组合使用后等效于byName自动装配。
public class User{
private String name;
@Autowired
@Qualifier(value="phone")
private Phone phone;
public void setName(String name) {
this.name = name;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
}
@Resource
@Resource并不是spring的注解,而是annotation-api.jar包中的类,类路径是javax.annotation.Resource。
@Resource的执行优先级如下:
- 先看有没有指定name属性,有则以此属性byName装配
- 如果上一步没成功,则按照默认的byName进行装配
- 如果上两步不成功,则按照byType进行装配
- 如果还是不成功,则报异常
public class User{
private String name;
@Resource(value="phone")
private Phone phone;
public void setName(String name) {
this.name = name;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
}
在实际开发过程中,使用注解的频率会比使用xml的频率更高。