Post

【Effective Java 条目五】-- 优先考虑通过依赖注入来连接资源

Effective Java 条目五 代码补充与理解总结

【Effective Java 条目五】-- 优先考虑通过依赖注入来连接资源

《Effective Java》条目5总结(含代码详解)

核心结论不变:类不应自行创建/硬编码依赖资源,而应通过外部注入获取,既解决静态工具类/单例的灵活性问题,又实现解耦、可测试,下面结合代码拆解核心逻辑。

一、反例:为什么硬编码/静态工具类/单例不可取?

先看三个典型“坏例子”,理解它们的弊端:

1. 硬编码资源(最直观的错误)

类内部直接写死资源参数,完全无法适配多环境/多资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 硬编码资源的类(错误示范)
public class HardcodeDBService {
    // 资源参数直接硬编码,改环境/改数据库必须改代码
    private static final String DB_URL = "jdbc:mysql://prod-db.com:3306/user";
    private static final String USER = "prod_user";
    private static final String PWD = "prod_pwd123";

    // 自行创建资源(数据库连接),与具体资源强耦合
    public List<User> queryUser(String sql) {
        try (Connection conn = DriverManager.getConnection(DB_URL, USER, PWD)) {
            // 业务逻辑...
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

问题

  • 开发/测试/生产环境无法切换(要改代码重新编译);
  • 想连第二个数据库(如订单库)完全做不到;
  • 单元测试只能连真实数据库,无法用内存库模拟。

2. 静态工具类(全局资源不可变)

把资源改成静态变量,看似能配置,实则还是全局唯一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 静态工具类(错误示范)
public class StaticDBUtil {
    // 全局静态资源,初始化后无法修改
    private static DataSource DATA_SOURCE;

    // 静态初始化方法,只能调用一次
    public static void init(DataSource dataSource) {
        StaticDBUtil.DATA_SOURCE = dataSource;
    }

    // 静态方法依赖全局资源
    public static List<User> queryUser(String sql) {
        if (DATA_SOURCE == null) {
            throw new IllegalStateException("未初始化");
        }
        try (Connection conn = DATA_SOURCE.getConnection()) {
            // 业务逻辑...
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

问题

  • 全局只有一个DATA_SOURCE,无法同时连多个数据库(如用户库+订单库);
  • 若中途修改DATA_SOURCE(如init()第二次调用),会影响所有使用该工具类的代码(并发安全问题);
  • 单元测试时,多个测试用例共享DATA_SOURCE,容易互相干扰。

3. 单例模式(单一实例资源固定)

单例只有一个实例,资源初始化后无法切换:

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
32
33
34
// 单例模式(错误示范)
public class SingletonDBService {
    // 唯一实例
    private static final SingletonDBService INSTANCE;

    // 实例级资源,构造时初始化(只能一次)
    private final DataSource dataSource;

    // 静态初始化,加载固定配置
    static {
        // 资源参数还是固定的,无法灵活切换
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setURL("jdbc:mysql://prod-db.com:3306/user");
        dataSource.setUser("prod_user");
        dataSource.setPassword("prod_pwd123");
        INSTANCE = new SingletonDBService(dataSource);
    }

    private SingletonDBService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public static SingletonDBService getInstance() {
        return INSTANCE;
    }

    public List<User> queryUser(String sql) {
        try (Connection conn = dataSource.getConnection()) {
            // 业务逻辑...
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

问题

  • 只能操作构造时初始化的数据库,无法适配多资源;
  • 测试时无法替换为模拟DataSource,只能依赖真实环境;
  • 若想切换资源,必须修改单例的初始化逻辑,影响所有依赖者。

二、正例:依赖注入的三种实现(含代码讲解)

依赖注入的核心是:资源由外部创建,通过“构造函数/setter/工厂”传入类中,类只负责使用资源,不负责创建资源。

1. 构造函数注入(推荐,最安全)

资源通过构造函数传入,用final修饰保证不可变,实例创建时即完成资源绑定(无“半初始化”状态):

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 DBService {
    // 依赖抽象(DataSource是接口),而非具体实现(如MysqlDataSource)
    private final DataSource dataSource;

    // 构造函数接收外部传入的资源,明确声明依赖
    public DBService(DataSource dataSource) {
        // 校验参数,避免传入null
        this.dataSource = Objects.requireNonNull(dataSource, "数据源不能为空");
    }

    // 核心业务逻辑:只使用资源,不关心资源如何创建
    public List<User> queryUser(String sql) {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql);
             ResultSet rs = pstmt.executeQuery()) {

            List<User> users = new ArrayList<>();
            while (rs.next()) {
                users.add(new User(rs.getLong("id"), rs.getString("name")));
            }
            return users;
        } catch (SQLException e) {
            throw new RuntimeException("查询失败", e);
        }
    }
}

用法:外部创建资源,注入实例

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
32
33
34
35
36
37
38
39
40
41
public class Main {
    public static void main(String[] args) {
        // 1. 外部创建开发环境数据源(资源创建与使用分离)
        DataSource devDataSource = createDataSource(
            "jdbc:mysql://dev-db.com:3306/user",
            "dev_user",
            "dev_pwd123"
        );
        // 注入开发环境资源,得到操作开发库的实例
        DBService devDBService = new DBService(devDataSource);
        devDBService.queryUser("SELECT * FROM user");

        // 2. 外部创建生产环境数据源
        DataSource prodDataSource = createDataSource(
            "jdbc:mysql://prod-db.com:3306/user",
            "prod_user",
            "prod_pwd123"
        );
        // 注入生产环境资源,得到操作生产库的实例
        DBService prodDBService = new DBService(prodDataSource);
        prodDBService.queryUser("SELECT * FROM user");

        // 3. 同时操作多个数据库(如用户库+订单库)
        DataSource orderDataSource = createDataSource(
            "jdbc:mysql://prod-db.com:3306/order",
            "prod_user",
            "prod_pwd123"
        );
        DBService orderDBService = new DBService(orderDataSource);
        orderDBService.queryUser("SELECT * FROM order");
    }

    // 外部工具方法:负责创建具体的数据源(资源管理集中化)
    private static DataSource createDataSource(String url, String user, String pwd) {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setURL(url);
        dataSource.setUser(user);
        dataSource.setPassword(pwd);
        return dataSource;
    }
}

单元测试优势(注入模拟资源)

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
public class DBServiceTest {
    @Test
    public void testQueryUser() {
        // 1. 用Mockito创建模拟DataSource(无需真实数据库)
        DataSource mockDataSource = Mockito.mock(DataSource.class);
        Connection mockConn = Mockito.mock(Connection.class);
        PreparedStatement mockPstmt = Mockito.mock(PreparedStatement.class);
        ResultSet mockRs = Mockito.mock(ResultSet.class);

        // 2. 模拟方法调用返回值
        Mockito.when(mockDataSource.getConnection()).thenReturn(mockConn);
        Mockito.when(mockConn.prepareStatement(Mockito.anyString())).thenReturn(mockPstmt);
        Mockito.when(mockPstmt.executeQuery()).thenReturn(mockRs);
        Mockito.when(mockRs.next()).thenReturn(true).thenReturn(false); // 模拟一条数据
        Mockito.when(mockRs.getLong("id")).thenReturn(1L);
        Mockito.when(mockRs.getString("name")).thenReturn("测试用户");

        // 3. 注入模拟资源,测试业务逻辑
        DBService dbService = new DBService(mockDataSource);
        List<User> users = dbService.queryUser("SELECT * FROM user");

        // 4. 断言结果(验证业务逻辑正确性,与真实数据库无关)
        Assertions.assertEquals(1, users.size());
        Assertions.assertEquals("测试用户", users.get(0).getName());
    }
}

2. Setter 方法注入(适合可选依赖)

若资源是可选的(如非必需的缓存),可用setXxx()方法注入,支持动态修改资源:

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
public class UserService {
    // 可选依赖:缓存客户端(可注入,也可不注入)
    private CacheClient cacheClient;
    // 必需依赖:还是用构造函数注入
    private final DBService dbService;

    // 构造函数注入必需依赖
    public UserService(DBService dbService) {
        this.dbService = Objects.requireNonNull(dbService);
    }

    // Setter方法注入可选依赖
    public void setCacheClient(CacheClient cacheClient) {
        this.cacheClient = cacheClient;
    }

    public User getUserById(Long id) {
        // 有缓存则先查缓存,无缓存则查数据库
        if (cacheClient != null) {
            User cacheUser = cacheClient.get("user_" + id);
            if (cacheUser != null) {
                return cacheUser;
            }
        }
        // 查数据库
        List<User> users = dbService.queryUser("SELECT * FROM user WHERE id = " + id);
        return users.isEmpty() ? null : users.get(0);
    }
}

用法:按需注入可选依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 创建必需依赖
DataSource dataSource = createDataSource(...);
DBService dbService = new DBService(dataSource);

// 2. 创建UserService(已注入必需依赖)
UserService userService = new UserService(dbService);

// 3. 按需注入可选依赖(缓存)
CacheClient redisCache = new RedisCacheClient("redis://localhost:6379");
userService.setCacheClient(redisCache);

// 4. 后续可动态切换缓存(如换成本地缓存)
CacheClient localCache = new LocalCacheClient();
userService.setCacheClient(localCache);

3. 框架级注入(Spring示例,复杂项目首选)

大型项目中,手动创建资源和实例繁琐,可借助Spring等框架实现“自动注入”,通过注解声明依赖:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 1. 配置类:集中管理资源(替代手动createDataSource)
@Configuration
public class DataSourceConfig {
    // 配置开发环境数据源(可通过配置文件读取参数)
    @Bean(name = "devDataSource")
    public DataSource devDataSource(
            @Value("${dev.db.url}") String url,
            @Value("${dev.db.user}") String user,
            @Value("${dev.db.pwd}") String pwd) {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setURL(url);
        dataSource.setUser(user);
        dataSource.setPassword(pwd);
        return dataSource;
    }

    // 配置生产环境数据源
    @Bean(name = "prodDataSource")
    public DataSource prodDataSource(
            @Value("${prod.db.url}") String url,
            @Value("${prod.db.user}") String user,
            @Value("${prod.db.pwd}") String pwd) {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setURL(url);
        dataSource.setUser(user);
        dataSource.setPassword(pwd);
        return dataSource;
    }
}

// 2. 业务类:通过@Autowired自动注入资源
@Service
public class SpringDBService {
    // 按名称注入开发环境数据源(也可按类型注入)
    @Autowired
    @Qualifier("devDataSource")
    private DataSource dataSource;

    // 业务逻辑与之前一致
    public List<User> queryUser(String sql) {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql);
             ResultSet rs = pstmt.executeQuery()) {
            List<User> users = new ArrayList<>();
            while (rs.next()) {
                users.add(new User(rs.getLong("id"), rs.getString("name")));
            }
            return users;
        } catch (SQLException e) {
            throw new RuntimeException("查询失败", e);
        }
    }
}

优势:

  • 资源配置集中化(通过配置文件application.properties管理参数,无需改代码);
  • 自动创建实例和注入依赖,减少手动编码;
  • 支持多环境切换(通过spring.profiles.active=dev激活对应环境的资源)。

三、核心总结(代码视角)

  1. 依赖注入的本质:把new DataSource()这类“资源创建逻辑”从业务类中抽离,通过构造函数/setter/框架注解传入,让业务类只专注于queryUser()等核心逻辑;
  2. 对比静态工具类/单例:后两者的资源是“内部固定”的(静态变量/单例实例变量),而依赖注入的资源是“外部传入”的,可灵活替换;
  3. 代码层面的好处
    • 多环境适配:改配置文件/外部参数即可,无需改业务代码;
    • 多资源支持:创建多个资源实例,注入不同业务对象(如同时连多个数据库);
    • 可测试:注入Mock资源,脱离真实环境完成单元测试;
    • 解耦:业务类依赖DataSource接口,而非MysqlDataSource具体实现,后续可无缝切换到PostgreSQL。

简单说:让类“要资源”,而不是“造资源”,这就是依赖注入的核心价值。

This post is licensed under CC BY 4.0 by the author.