初识Spring之IOC,DI

IOC(控制反转)

IOC是Spring框架的核心容器,任何Spring框架的模块都依赖于此容器,因此,在学习Spring时,一定要注意理解IOC容器的含义.

IOC主要解决了什么问题?

耦合

​ 先不着急给出答案,我们先来看一下,web持久层中的数据库链接是如何建立对象的
准备工作,创建数据库demo,然后创建表account,

create table account(
	id int primary key auto_increment,
    name varhchar(40),
    money float
) character set utf8 collate utf8_general_ci;
插入数据
insert into account(name,money) values('gyg',1000);
insert into account(name,money) values('gyk',1200);
insert into account(name,money) values('gyl',1500);

添加mysql依赖坐标

<dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
</dependencies>

JDBCDemo如下:

public class JDBCDemo {
    public static void main(String[] args) throws Exception{
        //1.注册驱动
        DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        //2.获取连接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo","root","gyg06103234");
        //3.获取操作数据库预处理对象
        PreparedStatement pstm = conn.prepareStatement("select * from account");
        //4.通过sql语句,查询数据库得到结果集
        ResultSet rs = pstm.executeQuery();
        //5.遍历结果集
        while (rs.next()) {
            System.out.println(rs.getString("name")+" "+ rs.getString("money"));
        }
        //6.释放资源
        rs.close();
        pstm.close();
        conn.close();
    }
}

此时如果在pom.xml中把mysql驱动注释掉,就会出现编译时异常,如果你用的是高级的IDE(例如IDEA),会直接提示错误Cannot resolve symbol 'mysql',因为在编译的时候找不到驱动,肯定会报错,这就是程序的耦合:

  • 类之间的耦合
  • 方法之间的耦合
  • 需要注意耦合无法完全消除,只能降低耦合(这个过程就是解耦)

实际开发中应做到,编译器不依赖,运行期依赖

上述例子中解耦的方式想家都知道,例如注册驱动的解耦

//1.注册驱动
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());
Class.forName("com.mysql.jdbc.Driver");

此时,在编译期并不会报错,但是程序并不能运行,因为没有依赖,这就做到了把编译期依赖转换成运行时依赖

总结一下上述做法

  • 创建对象时,使用反射创建,避免使用new关键字(上述例子中可以看出,一个是依赖类,另一个是依赖字符串,但是上述做法有一个问题 驱动类是写死的,更换数据库仍然需要修改)
  • 通过读取配置文件的形式,获取要创建对象的全限定类名

我们依旧可以举一个保存账户的例子

目录结构如下:

1aFkfe.png

可以看出在ui.Client里面类的耦合程度很高,那么该如何解决这个问题呢?

根据上一个例子,我们依然可以采用反射的方式,将类名存在一个配置文件,然后建立一个工厂类,读取配置文件,创建对象,这样在ui.Client就不用依赖具体的类,而只依赖类的全限定类名(字符串),这就实现了解耦

具体做法如下:

创建factory.BeanFactory类内容如下:

public class BeanFactory {
    private static Properties props;
    static {
        try {
            //1.实例化对象
            props = new Properties();
            //2.获取propertoes流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("be
        } catch (Exception e) {
            throw new ExceptionInInitializerError("初始化properties文件失败");
        }
    }
     public static Object getBean(String beanName) {
        Object bean = null;
        try {
            String beanPath = props.getProperty(beanName);
            bean = Class.forName(beanPath).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return beans.get(beanName);
    }
}

这样就可以通过工厂模式创建对象

此时可以将ui.Client和service.AccountServiceImpl修改为:

public class Client {
    public static void main(String[] args) {
        //IAccountService as = new AccountServiceImpl();
        IAccountService as = (IAccountService)BeanFactory.getBean("accountService");
        as.saveAccount();
    }
}


public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
    public void saveAccount() {
        accountDao.saveAccount();
    }
}

这样就降低了类之间的耦合,实现方式如上述例子中的相似,都是通过反射的方式解耦

多例化对象问题

但是此时,我们会发现每次通过IAccountService as = (IAccountService)BeanFactory.getBean("accountService");获取对象,都是一个新的对象,不是从始至终都是一个对象.这就是多例化对象

  • 多例化对象会创建多次,效率低

  • 但是,多例化对象更加适用于多线程,因为每个对象都是独立的

  • 当然,如果对象中存在成员变量,则可能多线程兼容不太好

  • 所以应该尽可能的把成员变量加入到成员方法中

在web中一般并没有多少成员变量,因此一般使用单例化创建对象

那么该如何解决呢?

我们可以在读取完配置文件后就创建所有对象,存入一个HashMap容器(这已经类似于IOC容器了)中,如果不保存,可能会因为java的垃圾处理机制导致对象被回收,然后修改getBean,返回Map中对应的value

修改factory.BeanFactory如下:

public class BeanFactory {
    private static Properties props;
    private static Map<String,Object> beans;
    static {
        try {
            //1.实例化对象
            props = new Properties();
            //2.获取propertoes流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);
            //3.实例化容器
            beans = new HashMap<String, Object>();
            //4.取出配置文件中的所有key
            Enumeration keys = props.keys();
            //5.遍历枚举,取出key
            while (keys.hasMoreElements()) {
                String key = keys.nextElement().toString();
                //6.根据key获取value 反射创建对象
                String beanPath = props.getProperty(key);
                Object value = Class.forName(beanPath).newInstance();
                //7.存入Map
                beans.put(key,value);
            }
        } catch (Exception e) {
            throw new ExceptionInInitializerError("初始化properties文件失败");
        }
    }
    public static Object getBean(String beanName) {
       return beans.get(beanName);
    }
}

这就解决了多例化对象的问题

至此,我们就已经模拟了spring ioc的设计模式,即通过配置文件方式,用工厂模式创建对象

IOC的理解

//IAccountService as = new AccountServiceImpl();
IAccountService as = (IAccountService)BeanFactory.getBean("accountService");

这两句话就体现了IOC为何称之为控制反转

  • 第一种方式(被注释掉的),是程序员自主的创建一个对象,拥有这个对象的所有控制权

  • 第二种方式,则将创建对象的权利交给了工厂,这就是控制反转

  • 工厂得到的对象由配置文件控制

  • 当然程序员有权选啧这两种创建方式

IOC含义:

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。(来自百度百科)

IOC解决了什么问题

graph LR
	App --> 资源1
	App --> 资源2
	App --> 资源3
	

原始的创建对象的方式,App和资源之间依赖关系很强,而IOC模式为

graph LR
	App --获取资源--> 工厂
	工厂 --控制--> 资源1
	工厂 --控制--> 资源2
	工厂 --控制--> 资源3
  • IOC解决了app和资源之间依赖关系,使得App和资源的独立更方便

  • IOC降低了类之间耦合程度

Spring IOC

​ 通过Spring获取Ioc容器可分为两种:

  • 通过xml配置文件
  • 通过注解

可以通过ApplicationContext接口获取核心容器,该接口有三个常用实现类

  • ClassPathApplicationContext 顾名思义就是 读取类路径下的配置文件
  • SystemXmlApplicationContext 同上,系统内的xml文件(需要有读取权限)
  • AnnotationConfigApplicationContext 通过注解的方式取得核心容器

我们先来了解前两种通过xml配置文件的方式

maven工程在resource文件夹下创建bean.xml,这个文件名可以任意(非中文),但是有一个约定俗成的文件名applicationContext.xml暂时先使用bean.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="accountService" class="com.henu.service.impl.AccountServiceImpl"/>
    <bean id="accountDao" class="com.henu.dao.impl.AccountDaoImpl"/>
</beans>

联想前面的bean.properties文件内容,不难发现属性id必须是唯一的,class属性就是该类的全限定类名,此配置文件到后面还会细说,此处可以先用

ApplicationContext context = new ClassPathApplicationContext("bean.xml");

这个context便类似于上面模拟的BeanFactory,即为spring的核心容器,接下来就可以使用此容器,根据配置的id获取对象了

  • 可以直接传入id获取Object对象然后强转
IAccountService ac = (IAccountService)context.getBean("accountService");
  • 亦可以传入class,直接获得对应对象
IAccountService ac = context.getBean("accountService",IAccountServiceImpl.class);

ApplicationContext创建容器的细节:

  • 一旦读取完配置文件,立马创建配置文件内配置的对象
  • 适用于单例对象的创建

当然还有另一种方式就是采用顶层接口BeanFactory

先展示一下接口间的依赖关系

1XY658.png

可以看出BeanFactory是一个顶层接口(也就意味着功能不是特别强大)

我们用BeanFactory(已经过时了的)的方式获取:

Resource resource = new ClassPathResource("bean.xml");
BeanFactory factory = new XmlBeanFactory(resource);
IAccountService as = (IAccountService)factory.getBean("accountService");

BeanFactory区别于ApplicationContext采用的立即加载策略

  • 采用的是延迟加载的策略,即要真正获取对象时才会创建对象
  • 适用于多例对象的创建

如何验证?

我们可以在IAccountService中加入一个默认构造器,输出一句话(service对象已经创建)

然后分别在两种方式读取配置文件时打断点

ApplicationContext:

1XY28g.pngBeanFactory:

1XY75T.png

红色是断点位置,绿色是下一步要执行的即,此步执行的是绿色上面的语句,很容易就能看出两种方式区别

spring对bean对象的管理细节

  • 创建对象的三种方式
  • bean对象的作用范围
  • bean对象的生命周期

创建对象的三种方式

  • 使用默认构造函数创建

    在spring的配置文件中用bean标签配置id和class属性后,没有其他的标签和属性,就采用默认构造函数创建bean对象,此时如果类中无默认构造函数,则无法创建

<bean id="accountService" class="com.henu.service.impl.AccountServiceImpl"/>
<bean id="accountDao" class="com.henu.dao.impl.AccountDaoImpl"/>
  • 使用类中成员方法创建对象,存入spring容器(非静态方法返回值)
//创建一个简单工厂类进行模拟
factory.InstanceFactory
    
public class InstanceFactory {
    public IAccountService getAccountService() {
        return new AccountServiceImpl();
    }
}
<bean id="instanceFactory" class="com.henu.factory.InstanceFactory"/>
<bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService" />

可以通过此方式创建accountService对象

factory-bean属性值为 类的id必须提前编写此类的bean标签

factory-method属性值为 类中用于生成对象的方法名

  • 使用类中静态方法返回值创建对象(静态方法返回值)

将上述工厂类中的方法加上static修饰符,可以通过以下配置文件创建

<bean id="accountService" class="com.henu.factory.InstanceFactory" factory-method="getAccountService" />

其中后两种方法适用于jar文件中的类

bean对象的作用范围

  • bean标签scope属性

    作用:指定bean的作用范围

    取值:

    singleton :单例的,默认值

    prototype:多例的

    request:作用于web应用请求范围

    session:作用web应用会话范围

    global-session:作用于集群环境的会话范围(雾)

    只验证前两个值

    <bean id="accountDao" class="com.henu.dao.impl.AccountDaoImpl" scope="singleton"/>
        
    IAccountDao dao1 = ac.getBean("accountDao",IAccountDao.class);
    IAccountDao dao2 = ac.getBean("accountDao",IAccountDao.class);
    System.out.println(dao1==dao2); //true,证明只创建了一次对象,即是单例对象
    
    <bean id="accountDao" class="com.henu.dao.impl.AccountDaoImpl" scope="prototype"/>
    IAccountDao dao1 = ac.getBean("accountDao",IAccountDao.class);
    IAccountDao dao2 = ac.getBean("accountDao",IAccountDao.class);
    System.out.println(dao1==dao2); //false,证明只创建了两次次对象,即是多例对象    
    
  • bean对象的生命周期

    单例对象:

    ​ 出生:容器创建时,对象就被创建

    ​ 活着:容器存在时.对象就存在

    ​ 死亡:容器销毁,对象狗带

    ​ 总结单例对象生命周期与容器一致

    //bean 标签指定初始化方法和销毁方法,在AccountDaoimpl类中添加相应方法
    <bean id="accountDao" class="com.henu.dao.impl.AccountDaoImpl" scope="singleton" init-method="init" destroy-method="destory"/>
     
    public void init() {
        System.out.println("对象被创建了");
    }
    public void destory() {
        System.out.println("对象销毁了");
    }
    /* 结果正确的输出了"对象被创建了",没有输出对象被销毁了
     * why? 在main方法执行完毕后,就会把当前应用中线程栈中的内存就会被全部释放,但是还并没有调用销毁方法
     * 需要手动释放方法才能调用销毁方法
     * 在main方法中作出如下修改
     **/
    
    //ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");java多态的体现
    ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
    //添加
    ac.close();
    //此时,销毁方法便会正常执行
    

    多例对象

    ​ 出生:当要使用对象时,spring才会创建,验证方法与上文类似,打断点即可

    ​ 活着 : 对象在使用过程中,就会一直活着

    ​ 死亡: 当对象长时间不使用,且没有别的对象引用时,由Java的垃圾回收器销毁

依赖注入Dependency Injection

  • IOC作用:

    ​ 降低程序间的耦合(依赖关系)

  • 依赖关系

    ​ 在当前类需要用到其他类的对象,由spring提供,只需在配置文件说明

  • 依赖关系的管理

    ​ 交给spring管理和维护

  • 依赖注入

    • 能注入的数据

      • 基本类型和String
      • 其他bean类型(在配置文件中或者注释配置过的)
      • 复杂类型/集合类型
    • 注入方式

      • 使用构造函数提供

      • 使用set方法提供

      • 使用注解提供(雾)

例如, 我们将AccountService修改为:

public class AccountServiceImpl implements IAccountService {
    private String name;
    private Integer age;
    private Date birthday;

    public AccountServiceImpl(String name,Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }
    public void saveAccount() {
        System.out.println(name + " " + age + " " + " " + birthday);
    }

}

构造函数注入

使用 constructor-arg标签

  • type :用于指定要注入数据的类型,该类型必须是构造函数中某个或默写参数的类型

  • index:用于指定要注入数据给构造函数中指定索引位置的参数赋值,起始值为0

  • name: 用于给构造函数中指定名称的参数赋值(常用)

    --------------用于指定给构造函数中哪个参数赋值--------------

  • value: 用于提供基本类型和String类型的数据

  • ref: 用于指定在spring核心容器ioc中出现过的bean对象

因此可以用以下配置文件

<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="accountService" class="com.henu.service.impl.AccountServiceImpl">
        <constructor-arg name="name" value="test"/>
        <constructor-arg name="age" value="18"/>
        <constructor-arg name="birthday" value="1999-11-23"/>
    </bean>
</beans>

注意:

  • 配置文件里面所有的value都是字符串
  • Integer 可以 直接用字符串类型的18赋值(spring可以强转)
  • Date类型不能直接用字符串"1999-11-23"

因此上述配置文件是错误的,需要作出如下修改

<bean id="accountService" class="com.henu.service.impl.AccountServiceImpl">
    <constructor-arg name="name" value="test"/>
    <constructor-arg name="age" value="18"/>
    <constructor-arg name="birthday" ref="now"/>
</bean>
<bean id="now" class="java.util.Date"/>

优势:

​ 在获取bean对象时,诸如数据是必须操作,否则无法创建对象(没有默认构造参数)

弊端:

​ 改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供

set方法注入(更常用)

使用property标签

  • name: 用于指定注入时调用的set方法名称(与成员变量名无关,只与set方法名称有关,去掉set,首字母变小写)
  • value: 用于提供基本类型和String类型的数据
  • ref: 用于指定在spring核心容器ioc中出现过的bean对象

在AccountService中生成setter方法,并且注释掉带参构造函数

public void setUserName(String name) {
    this.name = name;
}
public void setAge(Integer age) {
     this.age = age;
}
public void setBirthday(Date birthday) {
    this.birthday = birthday;
}

则配置文件可以这么写

<bean id="now" class="java.util.Date"/>
<bean id="accountService" class="com.henu.service.impl.AccountServiceImpl">
    <property name="userName" value="qaq"/>
    <property name="age" value="18"/>
    <property name="birthday" ref="now"/>
</bean>

优势:

​ 创建对象时没有限制,可以使用默认构造函数

弊端:

​ 如果某个成员必须有值,则获取对象时,set方法没有执行

复杂类型/集合类型注入

  • 用于给List结构注入的标签有

    list array set

  • 用于给Map结构注入的标签有

    map props

  • 结构相同,标签可以互换

修改AccountService

private String[] myStrings;
private List<String> myList;
private Set<String> mySet;
private Map<String,String> myMap;
private Properties myProps;
public void saveAccount() {
    System.out.println(Arrays.toString(myStrings));
    System.out.println(myList);
    System.out.println(mySet);
    System.out.println(myMap);
    System.out.println(myProps);
}

public void setMySet(Set<String> mySet) {
    this.mySet = mySet;
}

public void setMyProps(Properties myPops) {
    this.myProps = myPops;
}

public void setMyStrings(String[] myStrings) {
    this.myStrings = myStrings;
}

public void setMyList(List<String> myList) {
    this.myList = myList;
}

public void setMyMap(Map<String, String> myMap) {
    this.myMap = myMap;
}

依旧可以使用set方式注入


<bean id="accountService" class="com.henu.service.impl.AccountServiceImpl" >
    <property name="myStrings">
        <set>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </set>
    </property>
    <property name="myList">
        <array>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </array>
    </property>
    <property name="mySet">
        <list>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </list>
    </property>
    <property name="myMap">
        <map>
            <entry key="A" value="a" />
            <entry key="B">
                <value>b</value>
            </entry>
        </map>
    </property>
    <property name="myProps">
        <props>
            <prop key="AA">aa</prop>
        </props>
    </property>
</bean>

基于xml的spring ioc di到此结束,明天继续注解ioc(雾)