spring框架(1)— 依赖注入

依赖注入

  spring核心容器就是一个超级大工厂,所以的对象(数据源、hibernate SessionFactory等基础性资源)都会被当做spring核心容器的管理对象——spring把容器中的一切对象统称为Bean。

  Spring对Bean没有任何要求,只要是一个java类,spring就可以管理这个java类,并把它当做Bean处理。对于spring框架而言,一切java对象都是Bean。

package service;

public class Axe 
{
	public String chop()
	{
		return "使用斧头砍柴";
	}
}

  这个Axe类是一个普通的java类。

package service;

public class Person 
{
	private Axe axe;
	//设值注入所需的setter方法
	public void setAxe(Axe axe)
	{
		this.axe = axe;
	}
	public void useAxe()
	{
		System.out.print("我打算去砍柴!");
		//调用axe的chop()方法
		//声明Person对象依赖于Axe
		System.out.println(axe.chop());
	}
}

  这个Person类的useAxe()方法需要调用Axe对象的chop()方法,这种A对象需要调用B对象方法的情形,被称为依赖。

  Spring核心容器是整个应用的超级工厂,所有的java对象都会讲给Spring的容器管理——这些java对象被称为Spring容器中的Bean。

  Spring对于bean的管理需要在xml中进行配置。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<!--配置名为person的Bean,其实现类为service.Person-->
	<bean id="person" class="service.Person">
	<!--控制调用setAxe()方法,将容器中的ax Bean作为参数传递进去-->
		<property name="axe" ref="axe"/>
	</bean>
	<!--配置名为axe的Bean,其实现类为service.Axe-->
	<bean id="axe" class="service.Axe"></bean>

</beans>

  配置文件的根元素是<beans .../>,根元素中包含多个<bean .../>元素,每个元素定义一个Bean。在这里配置了两个Bean,分别是service.Person和service.Axe。

  只要将java类配置到xml中,spring就可以对其进行管理。

<bean id="person" class="service.Person">

  配置文件会将<bean .../>元素默认以反射方式类调用这个类的无参构造器,spring解析这一元素之后得到两个字符串,其中idStr的值为"person"(对应的是id属性的值),classStr的值为"service.Person"(对于的是class属性的值)。

String idStr = ...;
String classStr = ...;
Class clazz = Class.forName(classStr);
Object obj = clazz.newInstance();
//container代表spring容器
container.put(idStr, obj);

  spring框架通过反射根据<bean .../>元素的class属性创建了一个java对象,并以<bean .../>元素的id属性的值为key,将该对象放入spring容器中——这个java对象就成为了spring容器中的Bean。

  在spring配置文件中配置Bean时,class属性的值必须是Bean实现类的完整类名。

上面配置文件中还包括一个<property .../> 子元素,它驱动spring在底层以反射执行一次setter方法。name属性决定了执行哪些setter方法,value或者ref决定执行setter方法的传入参数。

  • 如果传入的是基本类型及其包装类、String等类型,则使用value属性指定传入参数;
  • 如果以容器中其他Bean作为传入参数,则使用ref属性指定传入的参数。

  spring框架只要看到<property .../>子元素,就会在底层反射执行一次setter方法,该Bean一旦创建,spring就会立即根据<property .../>子元素来执行setter方法。也就是说:

  1. <bean .../>元素驱动spring调用构造器创建对象;
  2. <property .../>元素驱动spring执行setter方法。

  这两步是先后执行的,中间几乎没有时间间隔。

  上面的配置中<property .../>元素的name属性为axe,该元素驱动spring以反射方式执行person Bean中的setAxe()方法;ref属性值为axe,该属性值指定以容器名为axe的Bean作为执行setter方法的传入参数。

  也就是说,在spring的底层会执行如下代码:

//解析<property .../>元素的name属性得到该字符串值为"axe"
String nameStr = ...;
//解析<property .../>元素的ref属性得到该字符串的值为"axe"
String refStr = ...;
String setterName = "set" + nameStr.substring(0, 1).toUpperCase() + nameStr.substring(1);
//获取spring容器中名为refStr的Bean,该Bean将会作为传入参数
Object paramBean = container.get(refStr);
//此处的clazz是从反射得到的Class对象
Method setter = clazz.getMethod(setterName, paramBean.getClass());
//此处的obj参数是之前一段反射代码创建的对象
setter.invoke(obj, paramBean);

  上述的代码是反射代码的实例,通过<property .../>的name属性决定调用哪个setter方法,并且根据value和ref决定调用setter方法的传入参数。

  id为person的<bean .../>元素还包括一个<property .../>子元素,因此spring会在创建完person Bean之后,立即以容器中id为axe的Bean被赋值给person对象的axe实例变量。

  接下来程序会通过spring容器来访问容器中的Bean,ApplicationContext是Spring容器中最常用的接口,该接口有如下两个实现类。

  1. ClassPathXmlApplicationContext:从类加载路径下搜索配置文件,并根据配置文件来创建spring容器;
  2. FileSystemXmlApplicationContext:从文件系统的相对路径或绝对路径下去搜索配置文件,并根据配置文件来创建spring容器。

在spring中,类加载路径是稳定的,因此通常使用ClassPathXmlApplicationContext来创建容器。

package service;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

public class Main {
	public static void main(String[] args)
	{
		//引入配置文件,创建spring容器
		Resource r = new FileSystemResource("src/beans.xml");
		//加载配置文件
		BeanFactory f = new XmlBeanFactory(r);
		//获取id为person的Bean
		Person p = (Person)f.getBean("person", Person.class);
		//调用useAxe()方法
		p.useAxe();
	}

}

  spring获取Bean对象的方式有两种:

1.Object getBean(String id):根据容器中Bean的id来获取Bean,获取Bean之后需要进行强制类型转换;

2.T getBean(String name, Class<T> requiredType):根据容器中Bean的id来获取指定的Bean,但是该方法带一个泛型参数,因此获得Bean之后无需进行强制类型转换。

  获取Bean对象之后,可以调用方法、访问实例变量,即可以像使用java对象一样使用这个Bean对象。

1、依赖注入

1.调用者面向被依赖对象的接口编程;

  2.将被依赖对象的创建交给工厂完成;

  3.调用者通过工厂来获取被依赖组件。

  通过这三点,可以保证调用者主需与被依赖对象的接口耦合,这就避免了类层次的硬编码耦合,使用spring框架之后,调用者无需主动获取被依赖对象,只需被动接受spring容器为调用者的成员变量赋值即可(只要配置一个<property .../>子元素,spring就会执行对应的setter方法为调用者的成员变量赋值)。于是,使用了spring之后,调用者获取被依赖对象的方式由原来的主动获取变成了变动接受,这被称为控制反转(Inversion of Control,IoC)。

  从spring框架的角度来说,spring容器负责将被依赖对象赋值给调用者的成员变量——相当于为调用者注入它依赖的实例,因此这种方式被称为依赖注入(Dependency Injection)。

  使用了spring框架之后,主要有两个变化:

1.程序员无需使用new创建对象,所有的java对象的创建都交给spring容器完成;

2.当调用者需要调用被依赖对象的方法的时候,调用者无需主动获取被依赖对象,只需要等待spring容器注入即可。

2、注入方式

  • 设值注入:IoC容器使用成员变量的setter方法来注入被依赖对象;
  • 构造注入:IoC容器使用构造器来注入被依赖对象。

2.1 设值注入

  设值注入指的是IoC容器通过成员变量的setter方法来注入被依赖对象。

  spring推荐面向接口编程,不管是调用者还是被依赖的对象,都应该为之定义接口,程序应该面向它们的接口,而不是面向实现类编程,这样利于后期的维护和升级。

①Axe接口

package test;

public interface Axe 
{
	public String chop();
}

②Person接口

package test;

public interface Person 
{
	public void useAxe();

}

③Chinese类

package test;

public class Chinese implements Person
{
	private Axe axe;
	public void setAxe(Axe axe)
	{
		this.axe = axe;
	}
	//实现Person接口定义的useAxe()方法
	public void useAxe()
	{
		System.out.println(axe.chop());
	}
}

④StoneAxe类

package test;

public class StoneAxe implements Axe
{
	public String chop()
	{
		return "石斧砍柴好慢";
	}

}

⑤beans.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="chinese" class="test.Chinese">
	<property name="axe" ref="stoneAxe"/></bean>
<bean id="stoneAxe" class="test.StoneAxe"></bean>
</beans>

⑥Main类

package test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

//import org.springframework.context.ApplicationContext;
public class Main {
	public static void main(String[] args)
	{
		ApplicationContext ctx = new FileSystemXmlApplicationContext("D:/java/workspace_j2ee/SpringDemo3/src/beans.xml");
		Person p = (Person)ctx.getBean("chinese", Person.class);
		p.useAxe();
	}

}

注意使用FileSystemXmlApplicationContext的时候,可以直接使用"src/beans.xml"作为beans.xml的路径,spring可以找的到配置文件的位置。

  假设Axe有另外的实现类:SteelAxe。

package test;

public class SteelAxe implements Axe{
	public String chop()
	{
		return "钢斧砍柴更快";
	}

}

  此时,需要将SteelAxe部署在spring容器中去,只需要在beans.xml中添加配置信息。

<bean id="steelAxe" class="test.SteelAxe"></bean>

  这一行定义了一个Axe实例,id是steelAxe,实现的类是SteelAxe,然后需要修改chinese的配置信息。

<bean id="chinese" class="test.Chinese">
	<!-- <property name="axe" ref="stoneAxe"/></bean> -->
	<property name="axe" ref="stoneAxe"/></bean>

  因为chinese实例与具体的Axe实现类之间没有任何关系,chinese仅仅与接口Axe耦合,这样就能保证chinese实例与Axe的松耦合——这是spring强调面向接口编程的原因。

  Bean与Bean之间的依赖关系由spring管理,spring采用setter方法为目标Bean注入所依赖的Bean,让Bean之间的耦合从代码层次上分离出来,依赖注入是一种优秀的解耦方式。

  所以,可以看出spring的IoC容器的三个基本要点:

  ①应用程序的各组件面向接口编程,面向接口编程可以将组件之间的耦合关系提升到接口层次,从而利于项目后期拓展;

  ②应用程序各组件不再由程序主动创建,而是由spring容器来负责产生并初始化;

  ③spring采用配置文件或注解来管理Bean的实现类、依赖关系,spring容器根据配置文件或注解,利用反射机制来创建实例,并将其注入依赖关系。

2、 构造注入

这种方式在构造实例的时候,已经为其完成了依赖关系的初始化,这种利用构造器来设置依赖关系的方式,被称为构造注入

  驱动spring在底层以反射方式执行带指定参数的构造器,当执行带参数的构造器时,就可利用构造器参数对成员变量执行初始化——这是构造注入的本质。

  <bean .../>元素默认总是驱动spring调用无参数的构造器来创建对象,使用<consrtructor-arg .../>子元素,每个<constructor-arg .../>子元素代表一个构造器参数,如果<bean .../>元素包含N个<constructor-arg .../>子元素,就会驱动spring调用带N个参数的构造器来创建对象。

①Roles类

package test;

public class Roles {
	private int id;
	private String roleName;
	public Roles(){
	}
	public Roles(int id, String roleName)
	{
		this.id = id;
		this.roleName = roleName;
	}
	public String toString()
	{
		return "Users [id = " + id + ", name= " + roleName + "]"; 
	}
}

②beans.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="roles" class="test.Roles">
	<constructor-arg value="1"/>
	<constructor-arg value="小明"/>
</bean>
</beans>

③测试

package test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

public class SpringTest {
	public static void main(String[] args)
	{
		ApplicationContext ctx = new FileSystemXmlApplicationContext("src/bean.xml");
		Roles r = (Roles)ctx.getBean("roles");
		System.out.println(r.toString());
	}	
}

  设值注入是先通过无参数的构造器创建一个Bean实例,然后调用setter方法注入依赖关系,而构造注入则直接调用有参数的构造器,当Bean实例创建完成之后,已经完成了依赖关系的注入。

  <constructor-arg .../>可以指定参数的值,  

<bean id="roles" class="test.Roles">
	<constructor-arg value="1"/>
	<constructor-arg value="小明"/>

  这一段的内容相当于:

Roles role = new Role(1, "小明");

  有时候,如果包含的构造器中的参数不同,但是构造器的名称相同,假设Test(String, String) 和Test(String, int),假如通过<constructor-arg value="1">由于spring只能解析出"1"字符串,但是到底转换为哪一个明确的构造器的数据类型,就无从判断了,所以spring允许为<constructor-arg .../>元素指定一个type类型,比如说<constructor-arg value="1", type="int"/>,此时就完成了int类型参数的配置。

  假如存在依赖,看下面的例子:

①Users类

package test;

public class Users {
	private int id;
	private String name;
	public Users(){
	}
	public Users(int id, String name)
	{
		this.id = id;
		this.name = name;
	}
	public String toString()
	{
		return "User [id = " + id + ", name = " + name + "]";
	}

}

②Roles类

package test;

public class Roles {
	private int id;
	private String roleName;
	//用户
	private Users users;
	public Roles(){
	}
	public Roles(int id, String roleName, Users users)
	{
		this.id = id;
		this.roleName = roleName;
		this.users = users;
	}
	public String toString()
	{
           //因为这里是在字符串加法中调用的Users对象,所以会隐式调用user.toString()方法,也可以显示调用users.toString方法
		 return "Roles [id=" + id + ", roleName=" + roleName + ", users="  
	                + users + "]";  
	}
}

③配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="roles" class="test.Roles">
	<constructor-arg value="1"/>
	<constructor-arg value="小明"/>
	<constructor-arg ref="users"/>
</bean>
<bean id = "users" class = "test.Users">
	<constructor-arg value = "2"/>
	<constructor-arg value = "小华"/>
</bean>
</beans>

④测试

package test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

public class SpringTest {
	public static void main(String[] args)
	{
		ApplicationContext ctx = new FileSystemXmlApplicationContext("src/bean.xml");
		Roles r = (Roles)ctx.getBean("roles");
		System.out.println(r.toString());
	}	
}

⑤输出结果

Roles [id=1, roleName=小明, users=User [id = 2, name = 小华]]

3.两种注入方法的对比

设置注入的适用场景:

  1. 与传统的JavaBean的写法相似,更容易理解,通过setter方法设定依赖关系显得更加直观,自然;
  2. 对于复杂的依赖关系,如果采用构造注入, 会导致构造器过于臃肿,难以阅读,spring在创建Bean实例的时候,需要同时实例化其依赖的全部实例,因而导致性能下降,而如果使用设值注入,会比较轻松;
  3. 尤其是在某些成员变量可选的情况下,多参数的构造器很笨重。

  构造注入的适用场景:

  1. 构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入,例如,组件中其它依赖关系的注入,常常需要依赖于Datasource的注入,采用构造注入可以设置注入的顺序;
  2. 对于依赖关系无需变化的Bean,构造注入更加实用。因为没有setter方法,所有的依赖关系都在构造器中设定,因此,无需担心后续代码对依赖关系产生破坏;
  3. 依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全是透明的,更符合高内聚的原则。

总结:

建议以设置注入为主,构造注入为辅。对于依赖关系无需变化的注入,尽量采用构造注入,而其他依赖关系的注入,考虑使用设值注入的方式。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏林德熙的博客

C# 金额转中文大写

创建的项目是创建一个 dot net core 的项目,实际上这个项目可以创建为 Stand 的。

1222
来自专栏小樱的经验随笔

C/C++中int128的那点事

最近群友对int128这个东西讨论的热火朝天的。讲道理的话,编译器的gcc是不支持__int128这种数据类型的,比如在codeblocks 16.01/Dev...

2611
来自专栏张首富-小白的成长历程

redis管理

Redis发布消息通常有两种模式: • 队列模式(queuing) • 发布-订阅模式(publish-subscribe) 任务队列:顾名思义,就是“...

1883
来自专栏Java技术分享圈

Java的数据库连接工具类的编写

1064
来自专栏Java学习之路

02 Spring框架 简单配置和三种bean的创建方式

整理了一下之前学习Spring框架时候的一点笔记。如有错误欢迎指正,不喜勿喷。 上一节学习了如何搭建SpringIOC的环境,下一步我们就来讨论一下如何利...

3215
来自专栏专注 Java 基础分享

Spring框架学习之高级依赖关系配置(二)

     紧接着上篇内容,本篇文章将主要介绍XML Schema的简化配置和使用SpEL表达式语言来优化我们的配置文件。 一、基于XML Schema的简化配置...

19710
来自专栏ppjun专栏

Android十八章:从Android看Binder

Binder是android上的一个类 继承了IBinder,用作Ipc进程间通讯,同是帮助Serivermanager连接各种manager(activity...

582
来自专栏Java帮帮-微信公众号-技术文章全总结

Java设计模式-模板方式模式

模板方法模式: 定义一个操作中的算法的骨架, 而将一些步骤延迟到子类中. 模板方法使得子类可以在不改变一个算法的结构的前提下重定义该算法的某些特定步骤. ? ...

4878
来自专栏漫漫全栈路

ASP.NET MVC 行为详解

前面分别介绍了MVC中的三个重要部分,而行为,则是其中C-Controller中的重要内容,下面详解一二。 一般继承自Controller类,类Controll...

2794
来自专栏爱撒谎的男孩

Struts2之获取请求参数

3436

扫码关注云+社区