Spring Framework精讲之一:依赖注入与控制反转

本文详细介绍了SpringFramework的核心概念,包括依赖注入(DI)和IoC容器的作用,以及如何通过ApplicationContext进行依赖管理,以简化DAO类的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        如果你看spring.io网站,它有差不多20多核心项目,10多个非核心项目(attic projects),Spring Framework是这些项目中最核心的一个,是基础的基础。我们通过这个系列文章来讲一下Spring Framework的核心,精讲就是精炼、精髓、精简地讲,删繁就简,直击关键,废话少说,直捣黄龙,看一篇就够。

什么是Spring Framework?

简单说,Spring Framework是一个DI(Dependency Injection) container或IoC container,即依赖注入容器或控制反转容器,在容器的基础上,以DI为基本方式添加了一些常用、必备工具集,或者就叫子框架,如 Web MVC,AOP,DB access等。

什么是依赖注入(DI)?

编写一个 Java 类用于访问数据库中的user table这些类一般称为 DAO或repository。如下面的 UserDAO 类。

public class UserDao {

    public User findById(Integer id) {
        // execute a sql query to find the user
    }
}

然后UserDAO中有一个方法可以让通过 ID 在table中查找用户,这个方法调用 SQL 查询就需要相应的数据库连接。在 Java 中,一般需要从一个称为 DataSource 的类获取数据库连接,如下:

import javax.sql.DataSource;

public class UserDao {

    public User findById(Integer id) throws SQLException {
        try (Connection connection = dataSource.getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // use the connection etc.
        }
    }

}

如注释中的(1)所示,UserDao依赖datasource,称为dependency,那么从哪儿获得这个依赖呢?没有这个datasource显然下面的SQL也无法执行。方法之一是每次需要时通过构造函数创建一个新的数据源,所以我们的UserDAO会如下所示:

import com.mysql.cj.jdbc.MysqlDataSource;

public class UserDao {

    public User findById(Integer id) {
        MysqlDataSource dataSource = new MysqlDataSource(); // (1)
        dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
        dataSource.setUser("root");
        dataSource.setPassword("s3cr3t");

        try (Connection connection = dataSource.getConnection()) { // (2)
             PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
             // execute the statement..convert the raw jdbc resultset to a user
             return user;
        }
    }
}

对注释中的标号解释如下:

(1)用构造函数 MysqlDataSource new一个datasource,并硬编码设定相关属性,访问url,用户名、密码等。

(2)使用新创建的数据源进行查询。

这时,如果在UserDAO中增加一个新的操作数据表的方法,我们可能需要再用构造函数初始化一个datasource,当然也可以把初始化数据源的工作放在外面的另一个方法里,然后由不同的操作数据库的方法进行调用,如下:

import com.mysql.cj.jdbc.MysqlDataSource;

public class UserDao {

    public User findById(Integer id) {
        try (Connection connection = newDataSource().getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO execute the select , handle exceptions, return the user
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = newDataSource().getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO execute the select ,  handle exceptions, return the user
        }
    }

    public DataSource newDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource(); // (3)
        dataSource.setUser("root");
        dataSource.setPassword("s3cr3t");
        dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
        return dataSource;
    }
}

不幸的是,该方法还是需要显式地初始化数据源才能使用,即可以通过引入newDataSource方法将该新方法添加到 UserDAO,供其它操作数据的方法调用。

这种方法有效,但有两个缺点:

  1. 如果想创建一个新的DAO类,比如 ProductDAO 也执行 SQL 语句,那这个ProductDAO 势必也必须具有DataSource 依赖项,但该依赖项是在 UserDAO 类初始化的,ProductDAO用不了。

  2. 事实是,这种方法为每个 SQL 查询创建一个新的数据源,开销无疑会很大。如果我们只打开一个数据源并重复使用它,而不是打开和关闭多个数据源,那就更好了。当然实现此目的的一种方法是将 DataSource 保存在 UserDao 的私有属性中,以便可以在方法之间重用 - 但这对多个 DAO 之间的重复没有帮助。

事情也未必那么复杂,如果把Datasource的初始化放在一个“全局”类中来处理,问题似乎就得到了很大的解决,如下所示:

import com.mysql.cj.jdbc.MysqlDataSource;

public enum Application {

    INSTANCE;

    private DataSource dataSource;

    public DataSource dataSource() {
        if (dataSource == null) {
            MysqlDataSource dataSource = new MysqlDataSource();
            dataSource.setUser("root");
            dataSource.setPassword("s3cr3t");
            dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
            this.dataSource = dataSource;
        }
        return dataSource;
    }
}

有了以上全局的Datasource后,UserDAO可以改写为如下:

import com.yourpackage.Application;

public class UserDao {
    public User findById(Integer id) {
        try (Connection connection = Application.INSTANCE.dataSource().getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO execute the select etc.
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = Application.INSTANCE.dataSource().getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO execute the select etc.
        }
    }
}

如其中的注释标签所示,程序得到了很大的改进:

(1)UserDAO 不再需要构建自己的 DataSource 依赖项,而是可以要求 Application 类为其提供。如果有类似的 DAO,也可以按此操作。

(2)Application类是一个singleton,也就是只创建一个实例,单一实例对Datasource单一引用,减少了开销。

但是虽然有很大改进,仍然存在如下一些缺点:

  1. UserDAO必须知道从哪里获取其依赖项,即它必须调用应用程序类 → Application.INSTANCE.dataSource()。

  2. 当程序变得越来越大越来越复杂的时候,当需要获得越来越多的依赖项的时候, Application类就会变得越来越复杂,此时如何划分的类/工厂就显得异常重要。我们需要新的方法。

控制反转(IoC)来解决问题

如果 UserDAO不必操心查找依赖项,而是拿来就用不是很好吗?UserDAO 随用随取(以某种方式),而不用主动调用 Application.INSTANCE.dataSource(),也就是 UserDAO不再控制何时/怎么样/从哪里获取它?而是把这种控制让渡给“别人”,这就是所谓的控制反转(Inversion of Control)

让我们看看带有全新构造函数的 UserDAO 会是什么样子。

import javax.sql.DataSource;

public class UserDao {

    private DataSource dataSource;

    public UserDao(DataSource dataSource) { // (1)
        this.dataSource = dataSource;
    }

    public User findById(Integer id) {
        try (Connection connection = dataSource.getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO execute the select etc.
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = dataSource.getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO execute the select etc.
        }
    }
}
  1. 每当调用者通过其构造函数创建新的 UserDao 时,调用者还必须传入有效的 DataSource。

  2. findByX 方法将简单地使用该数据源。

从 UserDao 的角度来看,这读起来要好得多。它需要了解Application类,也不知道如何构造 DataSource 本身。

那么这个由UserDAO的构造函数直接传入的Datasource到底是谁来构建的呢?实际它是container构建的,也就是由UserDAO所运行的context来构建的,我们叫它DI Container(依赖注入容器)。

Spring的依赖注入容器(DI Container)

Spring Framework 的核心是DI Container,容器负责管理你说开发的类及其依赖。DI Container具体体现的类是ApplicationContext这个接口,为了理解这个一点,我们来看下面的程序片断:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import javax.sql.DataSource;

public class MyApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass); // (1)

        UserDao userDao = ctx.getBean(UserDao.class); // (2)
        User user1 = userDao.findById(1);
        User user2 = userDao.findById(2);

        DataSource dataSource = ctx.getBean(DataSource.class); // (3)
        // etc ...
    }
}

(1)初始化 Spring ApplicationContext。

(2)ApplicationContext 可以为提供一个完全配置的 UserDao,即具有其 DataSource 依赖的 UserDao。

(3)ApplicationContext 还可以直接为我们提供 DataSource,这与它在 UserDao 中设置的 DataSource相同。

作为调用者不必再担心构造类,只需要求 ApplicationContext 为您提供相应object即可

但这是如何实现的呢?它实际是通过把一个“配置类”作为参数传递给ApplcationContext实现类的构造函数来实现的,此处所说的“配置类”,就是上段程序里的someconfigclass,下面这个代码段是一个它的具体实现的例子:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyApplicationContextConfiguration {  // (1)

    @Bean
    public DataSource dataSource() {  // (2)
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("s3cr3t");
        dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase");
        return dataSource;
    }

    @Bean
    public UserDao userDao() { // (3)
        return new UserDao(dataSource());
    }

}

(1)一个专用的 ApplicationContext 配置类,必须用@Configuration 注解来对类进行注解。

(2)一个返回 DataSource 的方法,并使用 Spring 特定的 @Bean 进行注解。

(3)另外一种方法,它返回 UserDao 并通过调用 dataSource()方法构造 UserDao。

用这个配置类初始化一个ApplicationContext,即初始化容器的代码段如下:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MyApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(MyApplicationContextConfiguration.class);
        UserDao userDao = ctx.getBean(UserDao.class);
        // User user1 = userDao.findById(1);
        // User user2 = userDao.findById(1);
        DataSource dataSource = ctx.getBean(DataSource.class);
    }
}

传入ApplicationContext构造函数的参数,可以是Java配置类,也可以是一个XML文件,如果是后者,那这个ApplicationContext的具体实现类就不是AnnotationConfigApplicationContext,而是ClassPathXmlApplicationContext

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值