Spring IoC控制反转(Spring中最重要的话题)
1. Spring IoC简介
IoC:控制反转(Inversion of Control),在传统开发模式下,都是由开发人员自行创建对象,并管理对象(也包括属性赋值的问题),当使用了Spring框架后,这些事情都交给框架来完成(在加载spring配置文件时,创建对象,ac.close的时候销毁),可以理解“将开发人员对对象的管理权交给了框架”,所以,就出现了“管理权”的移交,则称之为“控制反转”。所以控制反转的意思是,开发人员把控制权交给了框架,所以叫做控制翻转。
DI:依赖注入(Dependency Injection),具体表现为通过框架为对象的属性赋值!注入:其实就是一个属性赋值的过程。
IoC与DI的区别:Spring框架通过DI实现了IoC,DI是实现手段,而IoC是希望实现的目标!
关于Spring IoC,其实质是如何通过Spring框架确定某个类的对象的属性值!
2. 通过SET方式注入属性的值
假设存在User
类,其中有String password
属性,希望Spring在创建User
类的对象时,还能为password
属性注入值(赋值),则先在 src/main/java 下面准备好User
类,例如:
(1)注入User类中的password属性值:
首先,在User了中添加两个属性:password,并且生成get和set方法:
package cn.tedu.spring;
public class User {
private String password;
//然后添加get set方法
public String getPassword() {
return password;
}
public void setPassword(String password) {
System.out.println("User.setPassword()");
this.password = password;
}
}
然后,在src/main/resources中的Spring的配置文件中进行配置:
如果一个类要用Spring来管理,那么在
只要是使用Set方式为属性注入值,一定是在
<bean id="user" class="cn.tedu.spring.User">
在bean的头和尾中间加入一行代码
<!-- 在bean的内部(子集)写:property节点:用于配置属性的值。翻译:property:属性 -->
<property name="password" value="123456"/>
<!-- property节点:用于配置属性的值 -->
<!-- name属性:需要配置值的属性名称,实际是set方法对应的名称 -->
<!-- value属性:需要配置值的属性的值 -->
<!-- 如果标签内部不需要写子集的话,可以这样写:
<property name="password" value="123456"/> -->
<property name="age" value="23"/>
<!-- Spring做法: -->
<!-- password 对应的是——>setPassword -->
<!-- value 对应的是——>setPassword(123456)-->
</bean>
然后,在SpringDemo类中:
package cn.tedu.spring;
/**
* SET方式注入属性值测试
*/
import java.util.Arrays;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringDemo {
public static void main(String[] args) {
//1.加载Spring配置文件,获取Spring容器
ClassPathXmlApplicationContext ac =
new ClassPathXmlApplicationContext("spring.xml");
//2.从Spring容器中获取对象
User user = ac.getBean("user",User.class);
//User user = (User) ac.getBean("user");能不转换,就不转换
//3.测试
System.out.println(user); //cn.tedu.spring.User@727803de
System.out.println(user.getPassword());//123456
System.out.println(user.getAge());//23
//4.关闭,释放资源
ac.close();
最后,运行时,当获取对象后就可以直接输出属性值,可以看到,该属性已经存在所配置的值!
注意:在<property>
属性中,name
属性其实是SET方法对应的名称,例如以上配置为name="password"
,则Spring框架会基于这个配置值,将首字母大写,并在左侧拼上set
,得到setPassword
,以此为方法名称,结合value
属性值,调用了setPassword("000111")
,从而使得password
属性被赋值!所以,本质是要求name
属性的值与类的SET方法是对应的!但是,规范的SET方法名称与Spring组织出SET方法的方式是完全相同的,所以,也可以简单的理解为name
属性指的就是属性名称!
建立一个类:SampleBean
package cn.tedu.spring;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
//属性是集合
public class SampleBean {
//David, Lucy, Tom, Lily, Alex
private List<String> names;
//9527
private int[] numbers;
//Beijing, Shanghai, Guangzhou, Shenzhen
private Set<String> cities;//city复数:cities
// {username=root,password:1234, from:Beijing}
private Map<String, String> session;
//name:Jack, age:25, gender:Male
private Properties profile;//property的复数形态,多个属性的意思
//数据来自于jdbc.properties文件
private Properties jdbc;
//然后生成所有的get、set方法
public List<String> getNames() {
return names;
}
public void setNames(List<String> names) {
this.names = names;
}
public int[] getNumbers() {
return numbers;
}
public void setNumbers(int[] numbers) {
this.numbers = numbers;
}
public Set<String> getCities() {
return cities;
}
public void setCities(Set<String> cities) {
this.cities = cities;
}
public Map<String, String> getSession() {
return session;
}
public void setSession(Map<String, String> session) {
this.session = session;
}
public Properties getProfile() {
return profile;
}
public void setProfile(Properties profile) {
this.profile = profile;
}
public Properties getJdbc() {
return jdbc;
}
public void setJdbc(Properties jdbc) {
this.jdbc = jdbc;
}
}
(2)注入List集合类型的值names:
首先,在SampleBean类中:
// David, Lucy, Tom, Lily, Alex
private List<String> names;
然后,在src/main/resources中的Spring的配置文件中进行配置:
当要为一个list集合类型的属性赋值的时候,在property内部加入list节点,在list节点的子集再加value节点。
<bean id="sampleBean" class="cn.tedu.spring.SampleBean">
<property name="names">因为里面的属性名字是names,实际上是set方法是getNames
<list>
<value>David</value>
<value>Lucy</value>
<value>Tom</value>
<value>Lily</value>
<value>Alex</value>
</list>
</property>
</bean>
最后,在SpringDemo类的main方法中:
System.out.println(sampleBean.getNames());//[David, Lucy, Tom, Lily, Alex]
System.out.println(sampleBean.getNames().get(3));//Lily
System.out.println(sampleBean.getNames().getClass());//class java.util.ArrayList
注意:
List集合在Spring中是用什么实现类的来实现的?
通过代码:System.out.println(sampleBean.getNames().getClass());测试可知,是class java.util.ArrayList,那么在spring.xml文件中:
<property name="numbers">
<list>这里用<array>配置也可以,但不提倡。
<value>9</value>
<value>5</value>
<value>2</value>
<value>7</value>
</list>
</property>
在配置List集合属性的时候,用array和list都是可以的,同理,在配置array的时候,也可以用list,因为ArrayList的本质,在内存中的数据表现就是数组,所以array和list这两个节点可以混为一谈,想怎么用就怎么用,但是不提倡,只是了解即可。
(3)注入数组类型的值numbers:
首先,在SampleBean类中:
// 9, 5, 2, 7
private int[] numbers;
然后,在src/main/resources中的Spring的配置文件中进行配置:
数组对应的是<array>
节点。
<property name="numbers">
<array>
<value>9</value>
<value>5</value>
<value>2</value>
<value>7</value>
</array>
</property>
最后,在SpringDemo类的main方法中:
System.out.println(sampleBean.getNumbers());//[I@704921a5
/**
* 注意:
* 数组与其他的数据类型不同,并不继承自Object,虽然可以用Object去声明,但是却没有里面的
* toString方法
* 如果要让数组显示合理,就要用到Arrays的工具类,这个工具类有toString方法,然后把
* 数组作为参数放进去
* 所以如果输出数组,那么一般用Arrays.toString()方法,如下:
*/
System.out.println(Arrays.toString(sampleBean.getNumbers()));//[9, 5, 2, 7]
(4)注入Set集合类型的值cities:
首先,在SampleBean类中:
// Beijing, Shanghai, Guangzhou, Shenzhen
private Set<String> cities;
//一般如果是数组或者集合的变量,变量名一般是用复数形式
然后,在src/main/resources中的Spring的配置文件中进行配置:
set集合类型的属性,对应的就是
<property name="cities">
<set>
<value>Beijing</value>
<value>Shanghai</value>
<value>Guangzhou</value>
<value>Shenzhen</value>
</set>
</property>
最后,在SpringDemo类的main方法中:
//3.测试
System.out.println(sampleBean.getCities());
//[Beijing, Shanghai, Guangzhou, Shenzhen]
/*
* 注意:输出的城市顺序是和放入的城市顺序一致的
* 原因是:Spring框架默认的用的是LinkedHashSet,要验证的话,用下面验证:
*/
System.out.println(sampleBean.getCities().getClass());
//class java.util.LinkedHashSet
(5)注入Map类型的值session
首先,如果需要注入的是Map
类型的数据,在SampleBean类中:
// {username:root, password:1234, from:Beijing}
private Map<String, String> session;//session翻译:阶段;一场;一节;一段时间;
然后,在src/main/resources中的Spring.xml的配置文件中进行配置:
property标签中——map标签中——entry标签
<property name="session">
<map>
<entry key="username" value="root"/>
<entry key="password" value="1234"/>
<entry key="from" value="Beijing"/>
</map>
</property>
最后,在SpringDemo类的main方法中:
System.out.println(sampleBean.getSession().getClass());
//class java.util.LinkedHashMap
//依然是通过链表的形式来存储数据的,所以输出的顺序是和配置的顺序是保持一致的。
System.out.println(sampleBean.getSession());
//{username=root, password=1234, from=Beijing}
Set与Map的实质:
世上本没有Set,每一个Set都是只关心Key,不关心Value的Map。
看HashSet的源代码:
public HashSet() {
map = new HashMap<>();
}
当new一个HashSet的时候,其本质是new一个HashMap。
并且:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{//HashSet有一个声明,用的是占位符E,而内部声明了一个HashMap,HashMap的泛型Key也是E,同时,它的Value是Object,Object是随便声明的一个类型
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
并且在add方法中:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
其本质是往Map中put了一个数据,而添加进来的参数就变成了Map这个数据的Key。
所以,Map内的Key的表现特征,与Set非常相似,或者说就是一样的。而实际上,Set本身就是基于Map来实现的!。
这是HashSet的源代码,而TreeSet也是一样的。
public TreeSet() {
this(new TreeMap<E,Object>());
}
所以,Set实际上一直都是用Map来作为一个内部实现的工具,看到用的是Set,其实本质上都是Map,所以才有:世上本没有Set,每一个Set都是只关心Key,不关心Value的Map。
(6)注入Properties类型的数据:
1.以前老师没有明确的说Properties是一种集合类型,但是它也是能放很多数据的。
2.在哪出现过:以前在学JDBC的时候,写的一个数据库连接的jdbc.properties
文件,读到程序中,就是一个Properties类型的数据,写配置文件的时候用的。这个配置文件读出来之后,和Map差不多,与Map有相似之处,但不是Map。
3.如果需要注入的是java.util.Properties
类型的数据,在SampleBean类中:
// name:Jack, age:25, gender:Male
private Properties profile;
//属性是Properties,是property的复数形态,多个属性的意思
//profile翻译:概述、简介。里面准备一些个人资料。
//然后生成set、get方法
4.注意,这个Properties属性类型的数据,很少在xml里面写死,更多的还是像以前JDBC一样,在项目中会有一个jdbc.properties这样的一个配置文件,那么private Properties profile;这个对象的值就应该是来自于配置文件中,所以,关于Properties的注入就有这两种方式:常用:使用配置文件注入属性值,不常用:直接在Spring.xml中写死。
不常用方式:直接在Spring.xml写,在src/main/resources中的Spring的配置文件中进行配置:
<property name="profile">
<props>
<!-- props是Properties的简写 -->
<!-- 子集只有一个,注意,没有value -->
<prop key="name">Jack</prop>
<prop key="age">25</prop>
<prop key="gender">Male</prop>
</props>
</property>
然后在SpringDemo中新增:
System.out.println(sampleBean.getProfile().getClass());
//class java.util.Properties
//不是其他实现类,Properties类型就是一种直接可用的类型
System.out.println(sampleBean.getProfile());
//{age=25, name=Jcak, gender=Male}
//输出顺序和写的顺序不一样,数据在内存中是散列存储的,在实际使用中,也并不关心先后顺序
/*因为在实际使用这样数据的时候,更多的是读配置文件,去获取某一个属性的值,比如获取数据库连接的用户名,另外一处再获取数据库连接的密码,是取出其中的某一个数据来用,其实并不关心先后顺序*/
常用方式:当然,Properties
类型的数据也可以从.properties
类型的文件中读取!可以先在src/main/resources下创建jdbc.properties文件,右键new出File(搜File的话,是在General下面的),并在其中进行属性与值的配置:
url=jdbc:mysql://localhost:3306/database_name
driver=com.mysql.jdbc.Driver
username=root
password=root
initialSize=2
maxActive=10
#initialSize=2:表示如果用数据库连接池的话,初始化连接数有几个,这个值一般就是随便写,正式编程的时候也是随便写,因为这个值我们自己无法直接确定,后期根据运营情况要一直调整的。
这种方式配置Spring时,就和之前的不太一样了:
与<bean>
节点并列:在Spring的配置文件中,需要使用<util:properties>
节点来读取这个文件,这个节点与<bean>
节点是同一个级别的节点,不要将这个节点写在<bean>
的子级!在配置时:
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>
<!-- util:properties节点:用于读取.properties文件中的配置信息 -->
<!-- location属性:需要读取的文件的位置。 (location翻译:位置)-->
<!-- classpath:指的就是resources文件夹 -->
如果这些数据需要被读取到程序中或者某个属性中,是希望类当中是有这个属性的,比如读到SampleBean
的Properties jdbc
属性中,那么要先在SampleBean
中添加:
// 数据来自于jdbc.properties文件
private Properties jdbc;
//现在是需要为这个属性注入值
//然后生成set、get方法
那么现在要配置的是SampleBean
中的jdbc
的这个属性,我希望这个属性是有值的,
那么,在Spring的配置文件中,在配置SampleBean
的<bean>
节点子级添加:(在后面补充)
<property name="jdbc"/>
name为jdbc之后,那么jdbc的值来自下面这个properties节点:
<util:properties location="classpath:jdbc.properties"></util:properties>
只需要为这个properties节点指定一个location,它就会自动的把jdbc.properties这个文件给读了,自动的读取这个文件中的所有信息,并且把它封装到一个propertie类型的对象里面去,如果这个jdbc要来自上面properties节点读取的结果,首先,这个读取结果就要给他一个名字,否则他们两个之间就无法有对应关系,所以给名字:依然是用id给名字,一般建议:描述的是同一个数据,就用同样的名字就行,
所以写成:
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>
关于ref属性:
然后,既然
<property name="jdbc" ref="jdbc"/>
写到这里,配置就写完了。
最终配置代码,在spring.xml文件中写成:
<property name="jdbc" ref="jdbc"/>
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>
知识点:在为属性注入值时,如果该属性值是另一个Bean,则应该通过ref
属性引用到另一个Bean的id。
其实可以理解为,使用的<util:properties>
是一种比较特殊的<bean>
。
然后,在SpringDemo中添加:
System.out.println(sampleBean.getJdbc().getClass());
System.out.println(sampleBean.getJdbc());
//输出结果
//class java.util.Properties
//{maxActive=10, password=root, url=jdbc:mysql://localhost:3306/database_name, driver=com.mysql.jdbc.Driver, initialSize=2, username=root}
//这些属性就是在jdbc.properties文件中写的属性。
以后,凡是直接能写的值,就写value,不能直接写的值,如果是来自于另外一个Bean,就写ref。
小结:如果某个类中的某个属性需要被注入值,
1.首先,需要为这个属性添加匹配的SET方法(一般GET方法也都直接添加了),
2.在Spring的配置文件中,在该类对应的<bean>
节点的子级,添加<property>
节点,用于为属性注入值,如果有多个属性都需要注入值,则同一个<bean>
节点的子级应该有多个<property>
节点,
3.在每个<property>
节点中,都通过name
属性指定需要注入值的属性名称,假设需要注入值的属性叫password
,则应该配置为name="password"
,
(1)如果该属性的值是可以直接写出来的(例如数值、字符串、布尔值),则在<property>
节点中添加value
属性来配置值,
(2)如果该属性的值是另一个Bean对象,则在<property>
节点添加ref
属性来引用另一个<bean>
节点或等同节点的id
值,
(3)如果该属性的值是集合类型的,则需要在<property>
节点的子级添加<list>
/ <set>
/ <array>
/ <map>
/ <props>
进行配置,具体配置方式参考以上笔记中的示例代码。
jdbc.properties文件的配置
使用配置文件访问数据库的优点是:一次编写随时调用,数据库类型发生变化只需要修改配置文件。
配置文件的设置:在配置文件中,key-value对应的方式编写。
开发中获得连接的4个参数(驱动、URL、用户名、密码)通常都存在配置文件中,方便后期维护,程序如果需要更换数据库,只需要修改配置文件即可。
使用properties文件,Java中有专门用来装载配置文件的类Properties(Java.util.Properties),配置文件用来保存一些在程序中可能修改的值,修改时只要修改配置文件即可,而不用修改程序本身。Java配置文件常为.properties文件,格式为文本文件,文件的内容的格式是“键=值”的格式。在properties的文件中,在行首加上 # 就是注释这行,或者是用 包括这行也是注释
加载配置文件:
public static void loadPro() throws Exception {
//从文件中读取输入流
FileInputStream fis = new FileInputStream("src/database.properties");
//创建Properties对象
Properties pro = new Properties();
//从流中加载数据
pro.load(fis);
//关闭流
fis.close();
//从Properties对象中根据键读取值
String driverClass = pro.getProperty("driverClass");
String url = pro.getProperty("url");
String username = pro.getProperty("username");
String password = pro.getProperty("password");
//打印值
System.out.println(driverClass);
System.out.println(url);
System.out.println(username);
System.out.println(password);
}
1.通过IO读取文件
2.创建Properties对象
3.使用Properties对象的load(流)方法加载数据
4.使用Properties对象的getProperty(键)方法获取对应值
用JDBC建立连接:
/* 假设已经取得 driverClass,url, username, password 的值 */
// 注册驱动
Class.forName(driverClass);
// 建立连接
Connection con = DriverManager.getConnection(url, username, password);
// 获取SQL语句执行对象
Statement stat = con.createStatement();
// 调用执行者对象方法,执行SQL语句获取结果集
String sql = "SELECT * FROM sort";
ResultSet rs = stat.executeQuery(sql);
// ResultSet接口方法 boolean next() 返回true,有结果集,返回false没有结果集
while(rs.next()){// 获取每列数据,使用是ResultSet接口的方法 getXX方法参数中,建议写String列名
System.out.println(rs.getInt("sid")+" "+rs.getString("sname")+" "+rs.getDouble("sprice")+" "+rs.getString("sdesc"));
}
// 关闭对象,先开后闭rs.close();
stat.close();
con.close();
Properties对象的常用方法
-
getProperty ( String key),用指定的键在此属性列表中搜索属性。也就是通过参数 key ,得到 key 所对应的 value。
-
load ( InputStream inStream),从输入流中读取属性列表(键和元素对)。通过对指定的文件(比如说上面的 test.properties 文件)进行装载来获取该文件中的所有键 - 值对。以供 getProperty ( String key) 来搜索。
-
setProperty ( String key, String value) ,调用 Hashtable 的方法 put 。他通过调用基类的put方法来设置 键 - 值对。
-
store ( OutputStream out, String comments),以适合使用 load 方法加载到 Properties 表中的格式,将此 Properties 表中的属性列表(键和元素对)写入输出流。与 load 方法相反,该方法将键 - 值对写入到指定的文件中去。
-
clear (),清除所有装载的 键 - 值对。该方法在基类中提供。
-
keySet(),获取键集合(Properties继承Hashtable,内部通过Map实现)。
3. 通过构造方法注入属性的值(仅了解)
一般在写一个类的时候,都是非常提倡这个类是有无参数构造方法的,这样才便于框架对其进行管理,但是:
如果某个类没有无参数的构造方法,并且,希望通过构造方法为某些属性注入值,例如:
package cn.tedu.spring;
// 使用构造方法注入属性的值
// 使用构造方法注入时,并不要求每个属性都有SET/GET方法
public class Person {
// Zoe
private String username;
//拓展:Spring框架对于权限的要求是不严格的,因为它的核心工作底层工作原理都是通过反射机制来操作的,所以即使是写成私有的也能正常用,所以对权限没有太多的要求,但是从开发规范上来说,建议还是用私有的。
// 26
private int age;
//生成构造方法:快捷键Alt+Shift+S,O
public Person(String username, int age) {
//此构造方法有两个参数,第一个是username,第二个是age
super();
this.username = username;
this.age = age;
}
//为了好看一会的执行效果,生成toString,快捷键:Alt+Shift+S, S
@Override
public String toString() {
return "Person [username=" + username + ", age=" + age + "]";
}
}
在Spring的配置文件中,需要配置为:
<bean id="person" class="cn.tedu.spring.Person">
<!-- constructor-arg节点:配置构造方法的参数 -->
<!-- index属性:参数的位置,即第几个参数 -->
<constructor-arg index="0" value="Zoe"/>
<constructor-arg index="1" value="26"/>
<!-- 子级代码是不需要写后续的,所以用/结尾 -->
</bean>
然后在SpringDemo中写:
由于新写了个Person类,要获取这个Person对象,所以在第2步,从Spring容器中获取对象中补充:
Person person = ac.getBean("person",Person.class);
System.out.println(person);
//Person [username=Zoe, age=26]
构造方法注入属性值-小节
所以 ,如果构造方法中有多个参数,在<bean>
节点的子级就需要添加多个<constructor-arg>
节点进行配置,在<constructor-arg>
节点中,index
属性表示参数的位置,是从0
开始顺序编号,也就是:第1个参数的index
值是0
,第2个参数的index
值是1
,以此类推,如果对应的参数值是数值、字符串、布尔值,就通过value
属性进行配置,如果对应的参数值是另一个Bean对象,就需要通过ref
属性引用到另一个<bean>
节点的id
值,如果对应的参数值是某种集合类型的,则需要在<constructor-arg>
的子级添加集合节点进行配置!
如下:
<constructor-arg index="0" value="Zoe"/>
<constructor-arg index="1" value="26"/>
假设还有第三个参数,如果第三个参数的类型是数组或者集合,如下:
<constructor-arg index="2">
<list>
<value>11</value>
<value>22</value>
<value>33</value>
</list>
</constructor-arg>
value取值的实质
不管是通过Set方式还是通过构造方法来注入属性值,关于“值”应该怎么取,它的原则是不变的:
1.能够直接写,就是value=""
;
比如
<property name="password" value="123456"/>
<constructor-arg index="0" value="Zoe"/>
2.如果值是另外一个<bean>
,那就写ref
;
<property name="jdbc" ref="jdbc"/>
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>
<constructor-arg index="0" ref="jdbc"/>
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>
3.如果是集合,就在子级添加节点再来配置就行,而其他类型,比如数组、Set、Map、Properties等,也是参照上述写法来写。
<bean id="sampleBean" class="cn.tedu.spring.SampleBean">
<property name="names">
<list>
<value>David</value>
<value>Lucy</value>
<value>Tom</value>
<value>Lily</value>
<value>Alex</value>
</list>
</property>
</bean>
<constructor-arg index="0" value="Zoe"/>
<constructor-arg index="1" value="26"/>
假设还有第三个参数,如果第三个参数的类型是数组或者集合,如下:
<constructor-arg index="2">
<list>
<value>11</value>
<value>22</value>
<value>33</value>
</list>
</constructor-arg>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效