Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。

1. 什么是Ioc容器

IoC全称Inversion of Control,直译为控制反转。

1.1 控制反转

先来举一个例子:

image-20210829162221507

假设上图是机械式手表的结构,各个齿轮分别带动时针、分针和秒针顺时针旋转,从而在表盘上产生正确的时间,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。

但是如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转

软件系统中的耦合关系和齿轮之间的啮合关系很像,随着系统规模越来越庞大,对象之间的依赖关系越来越复杂,如何降低耦合度,达到解耦的目的,成为软件工程领域追求的目标之一。

软件专家Michael Mattson 1996年提出了IOC理论,用来实现对象之间的“解耦”。

IOC理论借助使用“第三方”来实现对象之间的解耦。

image-20210829170401882

通过上图,可以看出,由于引进了中间的“第三方”,也就是IOC容器,使得时针齿轮,分针齿轮和秒针齿轮没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心。

IOC: 控制反转,将对象(或者称为资源)的控制权由应用程序转交给容器。

1.2 传统的程序实现

先来看一下,如果没有Spring的时候,我们是如何来实现代码的。

场景:商品购买

实现需求:

  1. 商品购买
  2. 购买历史

1.2.1 商品购买

public class BuyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1. 查询用户
        UserService userService = new UserService();
        User user = userService.getCurrentUser();
        //2. 查询商品
        GoodsService goodsService = new GoodsService();
        Goods goods = goodsService.findGoods();
        //3. 创建订单
        OrderService orderService = new OrderService();
        Order order = orderService.createOrder(user,goods);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

1.2.2 购买历史

public class UserHistoryServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1. 查询用户
        UserService userService = new UserService();
        User user = userService.getCurrentUser();
        //2. 查询用户订单
        OrderService orderService = new OrderService();
        List<Order> orderList = orderService.findListByUser(user);
        //3. 根据订单中的商品id,查询详细的订单商品详情,展示
        GoodsService goodsService = new GoodsService();
        List<Goods> goodsList = goodsService.findGoodsList(orderList);

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们发现了一个问题,UserService,OrderService,GoodsService每次在使用的时候都要new

1.2.3 Service代码

Service都要访问数据库,所以在service代码中,需要有数据库的连接。

public class GoodsService {

    private HikariConfig configuration = new HikariConfig();
    private HikariDataSource dataSource = new HikariDataSource(configuration);
    public Goods findGoods() {
        try {
            Connection connection = dataSource.getConnection();
            //执行查询 省略

        } catch (SQLException e) {
            e.printStackTrace();
        }

        return new Goods();
    }

    public List<Goods> findGoodsList(List<Order> orderList) {
        try {
            Connection connection = dataSource.getConnection();
            //执行查询 省略

        } catch (SQLException e) {
            e.printStackTrace();
        }
        return new ArrayList<Goods>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class OrderService {
    private HikariConfig configuration = new HikariConfig();
    private HikariDataSource dataSource = new HikariDataSource(configuration);

    public Order createOrder(User user, Goods goods) {
        try {
            Connection connection = dataSource.getConnection();
            //调用数据库 省略不写
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return new Order();
    }

    public List<Order> findListByUser(User user) {
        return new ArrayList<Order>();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserService {
    private HikariConfig configuration = new HikariConfig();
    private HikariDataSource dataSource = new HikariDataSource(configuration);

    public User getCurrentUser() {
        try {
            Connection connection = dataSource.getConnection();
            //调用数据库 省略不写
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return new User();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们发现每次使用Service的时候,由于Service需要调用数据库来实现业务,所有都持有了HikariDataSource的对象,同时HikariDataSource又依赖于HikariConfig,所以有实例化了一个HikariConfig

1.2.4 结论

上述的代码中,我们发现了以下缺点:

  1. 多个Service之间都用到了DataSource,完全可以共享DataSource
  2. 多个Servlet之间都用到了Service,完全可以共享
  3. 生命周期的问题,比如DataSource使用完,应该被释放掉,如果被多个对象使用,引用,如果保证使用方全部销毁
  4. 随着系统的复杂,Servlet越写越多,Service越写越多,对象之间的依赖关系就会越来越复杂,代码写起来或者阅读起来会变的相当困难

如果一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。

于是通过引入IOC容器来解决这些问题。

代码就变成了这样:

public class UserService {

    private DataSource dataSource;
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
1
2
3
4
5
6
7

通过上述代码我们发现,DataSource不在由应用程序自己(UserService)来创建,而是通过外部通过setDataSource注入的方式引入

那么这个外部就称为是 IOC容器

1.3 依赖注入

首先明白一个概念,上述我们讲的对象,资源,组件等,在Spring中,统一被称为Bean

既然现在创建对象的行为是由IOC容器负责的,那么必然需要有一种注入机制,来将对象注入到应用程序中。

依赖注入有两种方式:

  1. set注入

    public void setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
    1
    2
    3
  2. 构造器注入

    	private DataSource dataSource;
        public UserService(DataSource dataSource){
            this.dataSource = dataSource;
        }
    
    1
    2
    3
    4

控制反转和依赖注入是从不同角度描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦

控制反转:将对象(或者称为资源)的控制权由应用程序转交给Ioc容器

依赖注入:应用程序所需要的资源由Ioc容器主动注入

2. 入门案例

IOC容器的两个主要包是:org.spring framework.beans和org.springframework.context包。

如果想使用IOC容器,下面两个依赖是必须的:

  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.2.16.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.16.RELEASE</version>
    </dependency>
1
2
3
4
5
6
7
8
9
10
11

2.1 需求

场景需求:

用户通过邮箱和密码注册的时候,会发送一封邮件到用户的邮箱

2.2 工程搭建

新建maven工程 spring01:

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>spring01</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.2.16.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.16.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
    </dependencies>
</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

2.3 Service

package com.mszlu.service;

import com.mszlu.domain.User;

public class MailService {


    public boolean sendRegisterMail(User user){
        System.out.println(String.format("%s 正在注册码神之路,您可以点击以下的链接完成注册 %s,如果不是你注册的,请忽略本邮件",user.getNickname(),"http://www.mszlu.com"));
        return true;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.mszlu.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Long userId;

    private String nickname;

    private String mail;

    private String password;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.mszlu.service;

import com.mszlu.domain.User;

import java.util.ArrayList;
import java.util.List;

public class UserService {

    private List<User> users = new ArrayList<>(List.of(
            new User(1L, "aaa","aaa@mszlu.com", "123456" ),
            new User(2L, "bbb","bbb@mszlu.com", "123456" ),
            new User(3L, "ccc","ccc@mszlu.com", "123456")));

    private MailService mailService;

    public void setMailService(MailService mailService) {
        this.mailService = mailService;
    }

    public void registerUser(String mail,String password,String nickname){
        users.forEach(user -> {
            if(user.getMail().equalsIgnoreCase(mail)){
                throw new RuntimeException("已经注册");
            }
        });
        User user = new User(null,mail,password,nickname);
        mailService.sendRegisterMail(user);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

注意到UserService通过setMailService()注入了一个MailService

2.4 Spring的xml配置文件

编写完上述的代码后,接下来我们需要编写一个特定的application.xml配置文件,xml文件的名称可以随意指定,但建议使用spring,application开头的命名方式

application.xml 目前可以近似当做就是Ioc容器。

在配置中指名如何创建并组装Bean。

xml文件放在resources目录下

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.mszlu.service.UserService">
        <property name="mailService" ref="mailService" />
    </bean>

    <bean id="mailService" class="com.mszlu.service.MailService" />
</beans>
1
2
3
4
5
6
7
8
9
10
11
12

上述配置文件,其中与XML Schema相关的部分格式是固定的,我们只关注两个<bean ...>的配置

上述的配置,使用Java代码表示:

UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);
1
2
3
  • 每个<bean ...>都有一个id标识,相当于Bean的唯一ID
  • userServiceBean中,通过<property name="..." ref="..." />注入了另一个Bean

2.5 运行

  1. 通过application.xml 创建一个Ioc容器,ApplicationContext

    package com.mszlu;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class App {
    
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  2. 通过Bean的id获取到对应的Bean实例 UserService

    package com.mszlu;
    
    import com.mszlu.service.UserService;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class App {
    
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
            UserService userService = (UserService) context.getBean("userService");
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  3. 调用registerUser方法

    package com.mszlu;
    
    import com.mszlu.service.UserService;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class App {
    
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
            UserService userService = (UserService) context.getBean("userService");
            userService.registerUser("ddd@mszlu.com","123456","ddd");
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

2.6 分析

ApplicationContext就是Spring的容器,它是一个接口,有很多实现类,这里我们选择ClassPathXmlApplicationContext,表示它会自动从classpath中查找指定的XML配置文件。

获得了ApplicationContext的实例,就获得了IoC容器的引用。从ApplicationContext中我们可以根据Bean的ID获取Bean。

//也可以通过Bean的类型 来获取
UserService userService = context.getBean(UserService.class);
1
2

3. Bean配置说明

bean基础配置

类别描述
名称bean
类型标签
所属beans标签
功能定义Spring核心容器管理的对象
格式<beans> <bean/> <bean></bean> </beans>
属性 列表id:bean的id,使用容器可以通过id值获取对应的bean,在一个容器中id值唯一 class:bean的类型,即配置的bean的全路径类名

bean别名配置

类别描述
名称name
类型属性
所属bean标签
功能定义bean的别名,可定义多个,使用逗号(,)分号(;)空格( )分隔

示例:

<!--通过mailService2也可以获得mailService的实例--> 
<bean id="mailService" name="mailService2" class="com.mszlu.service.MailService" />
1
2

bean作用范围配置

类别描述
名称scope
类型属性
所属bean标签
功能定义bean的作用范围,可选范围如下 singleton:单例(默认) prototype:非单例

示例:

<!--单例,默认是单例,可以不写--> 
<bean id="mailService" name="mailService2" class="com.mszlu.service.MailService" scope="singleton"/>
<!--非单例,每次获取都是一个新的实例,不常用--> 
<bean id="mailService" name="mailService2" class="com.mszlu.service.MailService" scope="prototype"/>
1
2
3
4