您的位置:首页 > 编程语言 > Java开发

Spring依赖注入总结

2017-11-14 00:00 393 查看

Spring依赖注入总结

0.为什么我们需要依赖注入

因为我们的应用要尽可能地做到高内聚,低耦合。既然我们需要提供解耦,而解耦最重要的原则就是依赖倒置原则(Dependence Inversion Principle):高层模块不应该依赖底层模块,他们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。那既然高层模块和底层模块都依赖于抽象了,这就带来了依赖倒置的概念,比方说,底层模块依赖于高层模块对某个接口的实现。这就又引出了控制反转(Inversion of Control)的概念,即底层模块(服务端)去回调高层模块(客户端),程序的控制权反转到了高层模块手中。

1.什么是依赖注入

我们知道,A对象依赖于B对象,等价于A对象内部存在对B对象的“调用”,亦即A对象内部拿到了B对象的引用。B对象的引用的来源无非有以下两种:A对象内部创建(无论是作为字段还是作为临时变量)、从外部注入,即依赖注入。

依赖注入(Dependency Injection),又名控制反转,意为某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定。换句话说,让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。

抛开具体接管依赖注入功能的容器不谈,我们先举两个简单的例子来展示一下依赖注入几种方式。第三方容器要做的,无非是去实现这几种注入方式,设法将调用类所依赖的实现类传递给调用类。

1.1 构造函数注入

我们可以在调用类通过构造函数初始化的时候,将接口实现类通过构造方法变量传入。这就需要调用类提供一个以接口实现类为参数的构造方法,如下所示,MoAttack可以通过传入Geli实现类来构造:

public class MoAttack {
private GeLi geli;

public MoAttack(GeLi geli) {
this.geli = geli;
}

public void cityGateAsk() {
geli.responseAsk("墨者革离");
}
}

这样,第三方Director类就可以负责为MoAttack类注入geli属性:

public class Director {
public void direct(){
//创建Geli实现类
GeLi geli = new LiuDeHua();

//注入Geli实现类
MoAttack moAttack = new MoAttack(geli);

moAttack.cityGateAsk();
}
}

我们可以看到,这波操作使得MoAttack类不必去在自己内部去实现geli成员,而是由Director类代劳了,并通过MoAttack类的构造方法注入进了MoAttack类中。

1.2 属性注入

结合例子,我们可以发现,构造方法注入的一个弊端是,MoAttack的geli成员的引用在类创建时就被决定了,要是我之后想更改的话,要么重新创建一次MoAttack类,要么在内部创建一个新类替换geli成员原来的引用,那我之前就相当于白注入了。所以为了更灵活地进行依赖注入,我们可以采用属性注入的方式:

public class MoAttack {
private GeLi geli;

public void setGeli(GeLi geli) {
this.geli = geli;
}

public void cityGateAsk() {
geli.responseAsk("墨者革离");
}
}

MoAttack为geli属性提供了一个setter方法,这样任何时候,Director类都可以在需要的时候为MoAttack注入geli属性的值:

public class Director {
public void direct(){
GeLi geli = new LiuDeHua();
MoAttack moAttack = new MoAttack();
//注入Geli实现类
moAttack.setGeli(geli);
moAttack.cityGateAsk();
}
}

我们可以看到,虽然MoAttack类和LiuDeHua类实现了解耦,但Geli类的实例化工作无非是被转移到了Director类中而已。我们希望能够将这些类的初始化、装配工作交给一个“第三方”来办理,从而让开发者从这些底层实现类的实例化、依赖关系装配等工作中解脱出来,专注于更有意义的业务逻辑开发工作。而Spring则是提供了这样一套依赖注入、java bean管理的容器。

借助于java语言提供的反射特性,Spring可以让开发者使用简单的配置文件完成依赖注入。因此,在介绍Spring依赖注入的基本用法之前,我们先简单回顾一下java的反射机制。

2.java反射简介

Java语言为我们提供了一套反射API,所谓“反射(Reflection)”,就如同通过镜子看一个对象的倒影。通俗点将,反射API为我们提供了程序运行时获取程序在运行时刻的内部结构,在知道了java类在运行时候的内部结构之后,就可以与其交互,包括创建新的对象和调用对象中的方法等。反射API提供了运行时的灵活性,但代价就是性能比较差,即使jvm针对此做了优化。

Java 反射API的第一个主要作用是获取程序在运行时刻的内部结构,我们知道java中,每当编写并编译了一个新类,类加载器就会为这个类产生一个Class对象,它包含了与类有关的信息。Class类与java.lang.reflect类库一起对反射的概念进行了支持,其本质在于jvm在运行时打开和检查.class文件。只要有了java.lang.Class类的对象,就可以通过其提供的getFields()、getMethods()和getConstructors()来获取到该类中表示域、方法和构造方法的对象的数组,这样,匿名对象的类信息就可以在运行时被完全确定下来,而在编译时不需要知道任何事情。

反射API的另一个作用是在运行时刻对一个java对象进行操作,这些操作包括动态创建一个Java类的对象,获取某个域的值以及调用某个方法。在Java源代码中编写的对类和对象的操作,都可以在运行时刻通过反射API来实现。

3.如何使用Spring进行依赖注入

现在,我们来看具体如何通过Spring来实现依赖注入的。Spring支持属性注入、构造函数注入、方法注入等方式,并且提供了我们通过xml配置文件、注解等来定义Bean的属性。

3.1 属性注入

属性注入要求Bean提供一个默认的构造函数,并为需要注入的属性提供对应的setter方法。Spring先调用Bean的不带参数的默认构造函数实例化Bean对象,然后通过反射的方式调用Setter方法注入属性值。

public class Car {
private int maxSpeed;
public String brand;
private double price;
public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public int getMaxSpeed() {
return maxSpeed;
}

public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public String toString() {
return "brand:" + brand + "/maxSpeed:" + maxSpeed + "/price:" + price;
}

}

接下来,我们通过Spring配置文件对Car进行属性注入

<?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-4.0.xsd"> <bean id="car" class="com.smart.attr.Car" lazy-init="default">
<property name="brand">
<value>
<![CDATA[红旗&CA72]]>
</value>
</property>
<property name="maxSpeed">
<value>200</value>
</property>
<property name="price" value="2000.00" />
</bean>
</beans>

上述配置为Car这个Bean的maxSpeed, brand, price三个属性注入了对应的值。配置生效的前提是Car类中提供了这三个属性对应的setter方法。

3.2 构造函数注入

Spring当然也支持我们通过构造函数的方式注入,这种注入方式可以保证一些必要的属性在Bean实例化的时候就得到设置,确保Bean实例化后就可以使用。

使用构造函数注入的前提是Bean必须提供带参的构造函数:

public class Car {
public String brand;
private String corp;
private double price;
private int maxSpeed;

public Car() {}
public Car(String brand, double price) {
this.brand = brand;
this.price = price;
}

public Car(String brand, String corp, double price) {
this.brand = brand;
this.corp = corp;
this.price = price;
}
public Car(String brand, String corp, int maxSpeed) {
this.brand = brand;
this.corp = corp;
this.maxSpeed = maxSpeed;
}

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public int getMaxSpeed() {
return maxSpeed;
}

public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public String toString(){
return "brand:"+brand+"/maxSpeed:"+maxSpeed+"/price:"+price;
}

}

对应的xml配置文件如下:

<bean id="car1" class="com.smart.ditype.Car">
<constructor-arg type="java.lang.String">
<value>红旗CA72</value>
</constructor-arg>
<constructor-arg type="double">
<value>20000</value>
</constructor-arg>
</bean>

通过配置元素属性,即可以完成将指定值作为构造函数参数注入。针对区分不同重载构造方法,Spring提供了按类型匹配入参、按索引匹配入参、通过自身类型反射匹配入参,也支持联合使用类型和索引匹配入参的配置方式。

3.3 工厂方法注入

Spring IOC容器以框架的方式提供工厂方法的功能,并以透明的方式开放给开发者,同时,Spring自然也支持开发者自己使用工厂方法注入。如果工厂方法是非静态的,那么必须实例化工厂类后才能调用工厂方法。而如果工厂方法是静态的,则用户在无需创建工厂类实例的情况下就可以调用工厂类方法。所以,静态工厂方法一般比非静态工厂更易用。

我们可以试图创建一个静态工厂方法:

public class CarFactory {
public Car createHongQiCar(){
Car car = new Car();
car.setBrand("红旗CA72");
return car;
}

public static Car createCar(){
Car car = new Car();
return car;
}
}

因为静态工厂方法使得我们无需创建工厂类实例,所以配置文件中,我们只需要直接在中通过class属性指定工厂类,然后再通过factory-method指定对应的工厂方法即可:

<bean id="car6" class="com.smart.ditype.CarFactory"

如果是非静态工厂方法,则用户还需要在配置文件中定义工厂类的Bean:

<bean id="carFactory" class="com.smart.ditype.CarFactory" />
<bean id="car5" factory-bean="carFactory" factory-method="createHongQiCar">

3.4 方法注入

3.5 基于注解的配置

Spring容器成功启动,需要我们同时提供Bean定义信息和Bean实现类,使用基于xml的配置,则Bean定义信息和Bean实现类本身是分离的。Spring也支持基于注解的配置,如果采用基于注解的配置文件,则Bean定义信息通过在Bean实现类上标注注解实现。

Spring提供了@Component、@Repository、@Service、@Controller四个注解,其中@Component用于标注普通的POJO,@Repository用于对DAO实现类进行标注,@Service用于对Service实现类进行标注,@Controller用于对Controller实现类进行标注。

如下所示,我们通过Spring注解将UserDao类标记为一个Bean

@Component("userDao")
public class UserDao {

}

Spring容器会识别出该类并自动将其转化为容器管理的Bean。

同时,Spring提供了一个context命名空间,其提供了通过扫描类包以应用注解定义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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd" >
<context:component-scan base-package="com.smart.anno"/>
</beans>

声明context命名空间后,接下来即可以通过context命名空间的component-scan的base-package属性指定一个需要扫描的基类包,Spring容器将会扫描这个基类包里的所有类,并从类的注解信息里获取Bean的定义信息。

Spring也提供了灵活的类过滤支持,我们可以自定义过滤表达式来进行类过滤。具体的过滤表达式类型和编写方法可以参考Spring官方文档。

3.6 自动装配Bean

Spring通过@Autowired注解实现Bean的依赖注入。如下所示,@Service将LogonService标注为一个Bean,并通过@Autowired注入LogDao以及UserDao的Bean。@Autowired默认按类型匹配的方式在容器中查找有且仅有一个匹配的Bean,查找到后,Spring将其注入@Autowired标注的变量中。

@Service
public class LogonService implements BeanNameAware {

@Lazy
@Autowired(required=false)
private LogDao logDao;

@Autowired
@Qualifier("userDao")
private UserDao userDao;

public void saveLog(){
logDao.saveLog();
}

我们也可以看到,可以使用@Qualifier注解限定Bean的名称。使用@Lazy注解可以使得Spring容器在启动的时候不会立即注入属性值,而是延迟到调用次属性的时候才注入属性值。

@AutoWired属性还可以对类成员变量及方法的入参进行标注,并允许对方法入参标注@Qualifier已指定注入Bean的名称。更多相关内容可参考Spring官方文档。

4.引申:框架和类库的区别

使用开发框架时,框架掌握程序流程的控制权,而使用类库时,则是应用程序掌握程序流程的控制权。或者说,使用框架时,程序的主循环位于框架中,而使用类库时,程序的主循环位于应用程序之中。框架会回调应用程序,而类库则不会回调应用程序。

5. Ref

代码均摘自《精通Spring 4.x企业应用开发实战》

https://www.zhihu.com/question/20821697
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Spring 依赖注入 IOC