【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激活对应环境的资源)。
三、核心总结(代码视角)
- 依赖注入的本质:把
new DataSource()这类“资源创建逻辑”从业务类中抽离,通过构造函数/setter/框架注解传入,让业务类只专注于queryUser()等核心逻辑; - 对比静态工具类/单例:后两者的资源是“内部固定”的(静态变量/单例实例变量),而依赖注入的资源是“外部传入”的,可灵活替换;
- 代码层面的好处:
- 多环境适配:改配置文件/外部参数即可,无需改业务代码;
- 多资源支持:创建多个资源实例,注入不同业务对象(如同时连多个数据库);
- 可测试:注入Mock资源,脱离真实环境完成单元测试;
- 解耦:业务类依赖
DataSource接口,而非MysqlDataSource具体实现,后续可无缝切换到PostgreSQL。
简单说:让类“要资源”,而不是“造资源”,这就是依赖注入的核心价值。
This post is licensed under CC BY 4.0 by the author.