今天我们来看一下spring的第二大特性,依赖注入,英文名称“Dependency Injecttion”,简称DI。在上一篇博客中我们讨论了IOC的作用就是降低程序之间的耦合性。简单说依赖注入就是给系统中的某个变量赋值。
那问题来了,那这种依赖关系由谁来管理那?毫无疑问,肯定是交给我们的spring容器了,我们只需在配置文件中说明即可。这种依赖关系的维护就称之为依赖的注入。
接下来我们 创建一个maven项目,导入spring的jar包。完成spring的基本开发环境。
jar包如下:
<!--spring依赖-->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>
这里说明一下,为什么一定要用maven来导入jar包那。看下图:
打开"External Libraries",发现下面有好多子包,其中我们发现有我们需要的sprign-context这个jar包。这就对了。这就是maven的强大之处。在今天的案例当中,我们只能用到spring-context这个jar包。如果我们要做一个完整的项目,上面的这些jar包是最基本的spring环境,如果我们手动的去导包,那多麻烦,还要面临版本兼容的问题。而利用maven来管理jar包的话,他会把我们与spring开发相关的jar都导进来。这就是他的好处。不会存在版本的兼容问题。推荐大家以后使用maven来管理jar包。
还是转入正题,看看spring的如何注入数据的?
spring注入的数据类型有哪些?
1. 基本数据类型和String
2. 其他bean类型(在配置文件中或注解中配置过的bean)
3. 复杂类型/集合类型
spring的注入方式有哪些?
1. 使用构造器来注入
使用的标签:constructor-arg
标签出现的位置:bean标签的内部
标签中的属性:
type:用于指定要注入的数据类型,该数据类型也是构造函数中某个或某些参数的数据类型
index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值,索引的位置是从0开始
name:用于指定给构造函数中指定名称的参数赋值
==============以上三个用于指定给构造函数中的那个参数赋值==================
value:用于指定给构造函数和String类型的数据注入
ref:用于指定其他bean类型的数据,他指的就是在spring的IOC核心容器中出现过的bean对象。
优点:
在获取bean对象时,注入的数据是必须的操作,否则对象无法创建。
缺点:
改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据也必须提供
代码如下:
备注:这里展示的是部分代码:只展示spring的配置文件和要交给spring容器管理的类。
import java.util.Date;
public class EntityServiceImpl {
private Integer age; //基本类型的包装类
private String name; //String类型
private Date birthday; //其他类型
public EntityServiceImpl(Integer age, String name, Date birthday){
this.age = age;
this.name = name;
this.birthday = birthday;
}
//测试方法
public void print(){
System.out.println("name: "+name+" age: "+age+" birthday: "+birthday);
}
}
配置文件:
<bean id="EntityService1" class="jdbc.service.EntityServiceImpl">
<constructor-arg type="java.lang.Integer" index="0" name="age" value="19"></constructor-arg>
<constructor-arg name="name" value="zyy"></constructor-arg>
<!-- 这个now就是bean对象的其他类型,要想使用,就必须现在spring中配置他(或者也叫实例化)-->
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
其实,我们可以看出,type和index就可以唯一确定一个构造函数中参数的位置,所以,在没有必要写name属性了。一般情况下使用name属性来进行注入。
很明显,这种方式是有前提条件的。就是我们要在类EntityServiceImpl 中有一个指定参数的构造函数。此例中的构造函数有三个参数,所以,在使用构造器进行注入的时候必须也是三个。否则就会报错。这就是我们总结出来的缺点,有时候,有些数据在实例化的时候我们并不用,但是没办法,即使不用,也要注入,这样,才能保证不会出错。
2. 使用set方法来注入
使用的标签:property
出现的位置:bean标签的内部
标签的属性:
name: 用于指定注入时所调用的set方法名称(其实就是属性名)
value:用于提供提供基本数据类型和String类型的数据
ref:用于指定其他bean 类型的数据,他指的就是在spring的IOC容器中出现过的bean对象
优点:
创建对象时没有明确的限制,可以直接使用默认的构造函数
缺点:
如果某个成员必须有值,则获取对象时有可能set方法没有执行
代码如下:
import java.util.Date;
public class EntityServiceImpl {
private Integer age;
private String name;
private Date birthday;
//默认构造函数必须存在(如果自己创建了构造器,默认的构造器将不会存在)
public EntityServiceImpl(){}
//有参构造器(使用set方法注入时,可以去掉有参构造器)
public EntityServiceImpl(Integer age, String name, Date birthday){
this.age = age;
this.name = name;
this.birthday = birthday;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
//测试方法
public void print(){
System.out.println("name: "+name+" age: "+age+" birthday: "+birthday);
}
}
spring 配置文件如下:
<bean id="EntityService2" class="jdbc.service.EntityServiceImpl">
<property name="age" value="22"></property>
<property name="name" value="yyl"></property>
<!-- 要使用now,必须现在spring的IOC容器中注入这个bean类型 -->
<property name="birthday" ref="now"></property>
</bean>
小结一下:set方式来注入数据。第一:我们必须给类中的成员变量提供标准的set方法,第二,类中默认的构造方法必须有。
3. 使用注解注入(请看下篇)
spring框架的注解配置应该是现在比较流行的配置方式,我们一定要掌握。其好处多多。什么简化配置,方便使用,提高开发效率等等一系列的好看、好听、中用的词汇都适合用来修饰spring使用注解带来的好处。但是,有人就会问,既然注解这么多好处,我们为什么还要学基于XML注入的配置。原因很简单,不入虎穴,焉得虎子。就是这个道理,你不了解spring的过去,怎么可能掌握未来。好了,接下来我们看一下spring的注解如何初始化bean对象和注入数据的。
在看注解之前,我们先在复习一下曾经的XML配置(XML曾经也牛掰过):
<bean id="userService" class="com.tff.demo.service.impl.UserServiceImpl"
scope="" destroy-method="" init-method="" >
<property name="" ref=""| value=""></property>
</bean>
bean: 标签就是用于实例化对象的
id : 用来表示一个一个唯一的bean对象
class:用来表示这个对象的全路径
scope:用来表示这个bean对象的作用范围
destroy-method:表示bean对象的销毁方法
init-method:用来表示对象的初始化方法
property:用来给这个类的属性注入数据
我们再来看一下了Spring注解是如何完成的。
spring的注解基本上可以分为四类
- 用于创建对象的
他们的作用就和XML文件中的bean标签实现的功能一样
@Component
作用: 用于把当前对象存入spring容器当中
@Controller
@Service
@Repository
属性(这四个注解的属性一致):
value:用于指定bean的id,相当于我们用XML配置时的id属性,可以忽略不写, 当我们不写时,他的默认值是当前的类名(类名的首字母小写)。
小结:以上三个注解的作用与属性和Component是一样的,只是spring框架为我们提供明确的三层使用的注解,使三层模式更清晰 - 用于注入数据的
他们的作用就和在XML配置文件中的bean标签中的property标签的作用是一样的
@Autowired:
作用:自动化装配,自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型相匹配时,就可以注入成功。如果没有,就会注入失败。
如果IOC容器中有多个类型匹配时:解决办法如下:
- 1.使用Qualifier注解
作用:在按照类型注入的基础上再按照名称注入,它在给类成员注入时不能单独使用,必须和Autowired注解联合使用,但是在给方法注入时可以单独使用。
属性:value:用于指定注入的bean的id。 - 2.Resource注解:
作用:直接按照bean的id注入,他可以独立使用
属性:
name :用于指定bean的id。
备注:以上三个注入都只能注入bean类型的数据,而基本数据类型和String类型都无法使用上述注入方式。另外,集合类的注入只能通过XML来实现。
- 用于改变作用范围的
他们的作用和在bean标签中的scope属性是一样的
@Scope:
属性值:
singleton: 单例对象,默认的就是单例对象
prototype: 多例对象 - 和生命周期有关的
他们的作用就和在bean标签中使用init-method和destory-method作用是一样的。
@PostConstruct
@PreDestory
代码如下:
dao层接口和实现类:
package com.tff.demo.dao;
public interface EntityDao {
void saveEntity();
void insertEntity();
void updateEntity();
void deleteEntity();
}
package com.tff.demo.dao.impl;
import com.tff.demo.dao.EntityDao;
import org.springframework.stereotype.Repository;
//@Repository(value="dao")
@Repository
public class EntityDaoImpl implements EntityDao {
public void saveEntity() {
System.out.println("dao层的添加方法");
}
public void insertEntity() {
System.out.println("dao层的插入方法");
}
public void updateEntity() {
System.out.println("dao层的修改方法");
}
public void deleteEntity() {
System.out.println("dao层的删除方法");
}
}
Service 层接口和实现类
package com.tff.demo.service;
public interface EntityService {
void saveEntity();
void insertEntity();
void updateEntity();
void deleteEntity();
}
package com.tff.demo.service.impl;
import com.tff.demo.dao.EntityDao;
import com.tff.demo.service.EntityService;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
@Service(value="service")
@Service
@Scope("singleton")
public class EntityServiceImpl implements EntityService {
/* @Autowired
@Qualifier(value = "bean的id也就是实例化对象注解的属性的value值")*/
@Resource(name = "dao")
private EntityDao entityDao;
public void saveEntity() {
entityDao.saveEntity();
}
public void insertEntity() {
entityDao.insertEntity();
}
public void updateEntity() {
entityDao.updateEntity();
}
public void deleteEntity() {
entityDao.deleteEntity();
}
@PostConstruct
public void init(){
System.out.println("模拟初始化方法");
}
@PreDestroy
public void destory(){
System.out.println("模拟销毁方法");
}
}
测试类:
package com.tff.demo.test;
import com.tff.demo.service.EntityService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestMethod {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
EntityService entityService = ac.getBean("entityServiceImpl",EntityService.class);
entityService.saveEntity();
}
}
还有最后一步:
就是大家可以看到,我们main函数中依然加载spring的配置文件,你们难道不好奇这个配置文件中究竟有什么东东吗?
答案是肯定有的。我先把代码贴出来再解释:
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx">
<!--
告知spring在创建容器时要扫描的包,配置所需要的标签不是在bean的约束中
,而是一个名称为context名称空间和约束中
-->
<context:component-scan base-package="com.tff.demo"></context:component-scan>
</beans>
我相信上面的注释已经说的很秦楚了。虽然我们已经在要交给spring容器帮我们实例化的类上面加了相应的注解,但是,spring并不知道在那个包下面去扫描这些类。所以,只欠我们告诉spring你应该去哪里找就OK了。
还有一个问题需要说明一下,就是前面提到的,如果IOC容器中有多个类型与bean对象匹配时,我们有两种解决方案:
第一种:两个注解联合使用(@Autowired+@Qualifier),按照先类型在id的匹配方式诸如数据。
第二种:一个注解解决问题(@Resource),按照指定的id进行注入
但是,问题来了,IOC容器中多个类型bean对象匹配,那怎么样才能使这样的情况发生那?那就是多个类实现了同一个接口或者多个类集成了同一个抽象类。这样,就导致一个接口或者一个抽象类有好多个子类。我们在注入的时候都喜欢用多态的形式来给对象赋值:用图片说吧!
现在我们直接交给spring来实例化。我们可以看到前面的类型是接口类型,后面是实现类类型。我们知道spring在实例化bean对象的时候,id属性是不允许重负的。他帮我们实例化的是等号右边的,也就是帮我们实例化实现类的(这个就不用解释了吧!,抽象类和接口都不能被实例化,java基础知识),而我们的类型是接口,spring实例化的是实现类。一个接口有很多个实现类。很明显,就出现了IOC容器中多个类型与bean对象匹配。哎呀,不知道你们能不能理解了。我尽力了(作者当时也是废了好大的劲)。
这时候,就是上面的两者解决办法
第一种:
@AutoWried
@Qualifier(value=“如果这个对象交给spring实例化的时候没有没有给id属性赋值,那这里默认就是这个类名(首字母小写)反之,就是id属性的值”)
备注:当然,如果你没有用XML形式实例化,用的是注解形式的话,那就是value的属性值。
第二种:
@Resource(name=“id|value”)
这里的id和value同上面Qualifier注解里面value的值。
复杂类型的注入/集合类型的注入
这个复杂类型就包括我们常用的List,Array,Set,Properties,Map类型的。注入的方式还是通过set方式来注入的。那就意味着我们必须为类中的每个成员提供必要的标准的set方法和默认构造函数。
下面我们上代码:
import java.util.*;
public class EntityService2 {
private String [] arrStr;
private List<Object> list;
private Set<Object> set;
private Map<String,Object> map;
private Properties pros;
public void setArrStr(String[] arrStr) {
this.arrStr = arrStr;
}
public void setList(List<Object> list) {
this.list = list;
}
public void setSet(Set<Object> set) {
this.set = set;
}
public void setMap(Map<String, Object> map) {
this.map = map;
}
public void setPros(Properties pros) {
this.pros = pros;
}
//测试方法
public void print(){
System.out.println("array: "+Arrays.toString(arrStr));
System.out.println(list);
System.out.println(map);
System.out.println(pros);
System.out.println(set);
}
}
spring配置文件
<!-- 给数组注入 -->
<property name="arrStr">
<array>
<value>zyy</value>
<value>gxr</value>
<value>wwh</value>
</array>
</property>
<!-- 给list集合注入数据 -->
<property name="list">
<array>
<value>张三</value>
<value>李四</value>
<value>王五</value>
</array>
</property>
<!-- 给set集合注入 -->
<property name="set">
<set>
<value>lisi</value>
<value>duanting</value>
<value>zhaiyuting</value>
<value>lisiguang</value>
</set>
</property>
<!-- map集合注入数据 -->
<property name="map">
<map>
<entry key="name1" value="yyl"></entry>
<entry key="name2" value="sty"></entry>
<entry key="name3" value="zjs"></entry>
</map>
</property>
<!-- 给Properties注入数据-->
<property name="pros">
<props>
<prop key="driver">com.jabc.mysql.Driver</prop>
<prop key="url">http://localhost:8080/loveyou</prop>
<prop key="username">gxr</prop>
<prop key="password">1314520</prop>
</props>
</property>
</bean>
小结:我们可以看出,给什么类型注入数据,就用什么标签,数组就是Array标签,list就是list标签,Set就是set标签等等。
不过我们还发现,这五类当中,基本可以划分为两大类:
第一类:线性结构:Array、List、Set
线性结构是我们用以上三个任意一个标签都可以注入成功
第二类:键值对 : Properties、Map
键值对时,我们就采用提供标签对来进行对数据的注入。
最后,我同意给出测试类的代码和运行结果:
import jdbc.service.EntityService2;
import jdbc.service.EntityServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestMethod {
//private static EntityService entityService = new EntityServiceImpl();
public static void main(String[] args) {
//加载配置文件
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//使用构造器注入
EntityServiceImpl entityService = (EntityServiceImpl) ac.getBean("EntityService1");
entityService.print();
//使用set方式来注入
EntityServiceImpl entityService2 = (EntityServiceImpl) ac.getBean("EntityService2");
entityService2.print();
//注入复杂类型的数据
EntityService2 entity = ac.getBean("EntityService3",EntityService2.class);
entity.print();
}
}
代码运行结果:
说明:这里需要说明一个问题,就是什么样的数据需要注入。一句话,就是经常变换的数据不适合注入。我们需要注入的数据基本上都是不变的。比如数据库的配置文件等。