properties 属性文件的实现方式

数据库连接的 properties 配置文件,配置两个数据源信息:

1
2
3
4
5
6
7
8
9
spring.jdbc.driver=com.mysql.cj.jdbc.Driver
spring.jdbc.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.jdbc.username=root
spring.jdbc.password=123456

mybatis.jdbc.driver=com.mysql.cj.jdbc.Driver
mybatis.jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
mybatis.jdbc.username=root
mybatis.jdbc.password=123456

Mybatis 配置文件,配置两个 environment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 引入数据库配置文件 -->
<properties resource="database.properties"/>

<environments default="Spring">
<environment id="Spring">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${spring.jdbc.driver}"/>
<property name="url" value="${spring.jdbc.url}"/>
<property name="username" value="${spring.jdbc.username}"/>
<property name="password" value="${spring.jdbc.password}"/>
</dataSource>
</environment>

<environment id="Mybatis">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mybatis.jdbc.driver}"/>
<property name="url" value="${mybatis.jdbc.url}"/>
<property name="username" value="${mybatis.jdbc.username}"/>
<property name="password" value="${mybatis.jdbc.password}"/>
</dataSource>
</environment>
</environments>

通过 DataSourceSessionFactory 工具类,获取 SqlSessionFactory

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
package cn.worstone.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Optional;

public final class DataSourceSessionFactory {

private static String MybatisConfigPath = "mybatis.xml";
private static Map<DataSourceEnvironment, SqlSessionFactory> sessionFactoryMap = new HashMap<>();

/**
* Mybatis 配置的 Environment 中的数据源的枚举类型(即 Environment 的 id 属性值)
*/
public static enum DataSourceEnvironment {
Spring,
Mybatis;
}

public static SqlSessionFactory getSessionFactory(DataSourceEnvironment dataSourceEnvironment) {
SqlSessionFactory sqlSessionFactory = DataSourceSessionFactory.sessionFactoryMap.get(dataSourceEnvironment);
sqlSessionFactory = Optional.ofNullable(sqlSessionFactory).orElseGet(()->{
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(DataSourceSessionFactory.MybatisConfigPath);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory temp = new SqlSessionFactoryBuilder().build(inputStream, dataSourceProperties);
sessionFactoryMap.put(dataSourceProperties, temp);
return temp;
});

return sqlSessionFactory;
}
}

扩展

定义一个统一的 Mapper 标识接口,每一个具体的 Mapper 接口继承该接口:

1
2
3
4
5
6
7
package cn.worstone.mapper;

/**
* 通用的 Mapper 接口, 可以定制所有的通用方法, 设置参数类型为 Map<String, Object>
*/
public interface Mapper {
}

定义一个 Mapper 代理工厂,并获取 Mapper 代理,便于在执行完 Mapper 中的方法后关闭 SqlSession

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
54
55
56
57
58
59
60
61
62
63
64
package cn.worstone.utils;

import cn.worstone.mapper.Mapper;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public final class MapperFactory {

public static <T> T createMapper(Class<? extends Mapper> clazz, DataSourceSessionFactory.DataSourceEnvironment dataSourceEnvironment, boolean isCommit) {
SqlSessionFactory sqlSessionFactory = MapperFactory.getSqlSessionFactory(dataSourceEnvironment);
SqlSession sqlSession = sqlSessionFactory.openSession(isCommit);
Mapper mapper = sqlSession.getMapper(clazz);
return (T) MapperProxy.bind(mapper, sqlSession);
}

/**
* 默认不提交事务的方法
* @param clazz
* @param dataSourceEnvironment
* @param <T>
* @return
*/
public static <T> T createMapper(Class<? extends Mapper> clazz, DataSourceSessionFactory.DataSourceEnvironment dataSourceEnvironment) {
return MapperFactory.createMapper(clazz, dataSourceEnvironment, false);
}

private static SqlSessionFactory getSqlSessionFactory(DataSourceSessionFactory.DataSourceEnvironment dataSourceEnvironment) {
return DataSourceSessionFactory.getSessionFactory(dataSourceEnvironment);
}

private static class MapperProxy implements InvocationHandler {

private Mapper mapper;
private SqlSession sqlSession;

private MapperProxy(Mapper mapper, SqlSession sqlSession) {
this.mapper = mapper;
this.sqlSession = sqlSession;
}

// 获取代理对象的实例 Java 只能代理接口
public static Mapper bind(Mapper mapper, SqlSession sqlSession) {
return (Mapper) Proxy.newProxyInstance(mapper.getClass().getClassLoader(), mapper.getClass().getInterfaces(), new MapperProxy(mapper, sqlSession));
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
result = method.invoke(mapper, args);
} catch (Exception e) {
e.printStackTrace();
} finally {
sqlSession.close();
}

return result;
}
}
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void query() {
UserMapper userMapper = MapperFactory.createMapper(UserMapper.class, DataSourceEnvironment.Spring);
User user = userMapper.query("5f414d80-c413-42d4-8844-909d3e107ce8");
System.out.println(user);
}

@Test
public void add() {
UserMapper userMapper = MapperFactory.createMapper(UserMapper.class, DataSourceEnvironment.Spring, true);
User user = new User(UUID.randomUUID().toString(), "XXX", "Second", "123456");
int num = userMapper.add(user);
System.out.println(num);
}

代码优化

以上的方式就可以支持多个数据源了,但是代码还不够优雅。以上代码,我们发现 DataSourceEnvironment 这个枚举变量在 Mapper 实现类以及 MapperFactory 中传来传去,我们应该尽量减少一个类对其他类的耦合,也就是上面的代码,不符合 Java 编程原则中的 迪米特法则

那我们来优化一下代码吧。

MapperFactory 设计成枚举策略模式,并且将 SqlSession 存放到 ThreadLocal 中,减少资源浪费:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package cn.worstone.utils;

import cn.worstone.mapper.Mapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Optional;

public enum MapperFactory {

Spring {
private SqlSessionFactory sqlSessionFactory;

private final ThreadLocal<SqlSession> localSession = new ThreadLocal<>();

@Override
public <T> T createMapper(Class<? extends Mapper> clazz, boolean isCommit, boolean isClose) {
return MapperFactory.createMapper(clazz, this, isCommit, isClose);
}

@Override
public <T> T createAutoCommitMapper(Class<? extends Mapper> clazz) {
return MapperFactory.createMapper(clazz, this, true, true);
}

@Override
public <T> T createNotCloseMapper(Class<? extends Mapper> clazz) {
return MapperFactory.createMapper(clazz, this, false, false);
}

@Override
public <T> T createMapper(Class<? extends Mapper> clazz) {
return this.createMapper(clazz, false, true);
}

@Override
protected void createSqlSessionFactory() throws IOException {
// 这个 I/O 流不用手动关, new SqlSessionFactoryBuilder().build() 里面已经关闭了这个流
InputStream inputStream = Resources.getResourceAsStream(MapperFactory.MybatisConfigPath);
this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, this.name());
}

@Override
protected void closeThreadLocal() {
this.localSession.remove();
}

@Override
public ThreadLocal<SqlSession> getThreadLocal() {
return this.localSession;
}

@Override
public SqlSessionFactory getSqlSessionFactory() {
return this.sqlSessionFactory;
}

@Override
public SqlSession getSqlSession() {
return this.getSqlSession(false);
}

@Override
public void closeSqlSession() {
MapperFactory.closeSqlSession(this);
}

@Override
public SqlSession getSqlSession(boolean isCommit) {
return MapperFactory.getSqlSession(this, isCommit);
}
},
Mybatis {
private SqlSessionFactory sqlSessionFactory;

private final ThreadLocal<SqlSession> localSession = new ThreadLocal<>();

@Override
public <T> T createMapper(Class<? extends Mapper> clazz, boolean isCommit, boolean isClose) {
return MapperFactory.createMapper(clazz, this, isCommit, isClose);
}

@Override
public <T> T createAutoCommitMapper(Class<? extends Mapper> clazz) {
return MapperFactory.createMapper(clazz, this, true, true);
}

@Override
public <T> T createNotCloseMapper(Class<? extends Mapper> clazz) {
return MapperFactory.createMapper(clazz, this, false, false);
}

@Override
public <T> T createMapper(Class<? extends Mapper> clazz) {
return this.createMapper(clazz, false, true);
}

@Override
protected void createSqlSessionFactory() throws IOException {
InputStream inputStream = Resources.getResourceAsStream(MapperFactory.MybatisConfigPath);
this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, this.name());
}

@Override
protected void closeThreadLocal() {
this.localSession.remove();
}

@Override
public ThreadLocal<SqlSession> getThreadLocal() {
return this.localSession;
}

@Override
public SqlSessionFactory getSqlSessionFactory() {
return this.sqlSessionFactory;
}

@Override
public SqlSession getSqlSession() {
return this.getSqlSession(false);
}

@Override
public void closeSqlSession() {
MapperFactory.closeSqlSession(this);
}

@Override
public SqlSession getSqlSession(boolean isCommit) {
return MapperFactory.getSqlSession(this, isCommit);
}
};

public abstract <T> T createMapper(Class<? extends Mapper> clazz, boolean isCommit, boolean isClose);

public abstract <T> T createAutoCommitMapper(Class<? extends Mapper> clazz);

public abstract <T> T createNotCloseMapper(Class<? extends Mapper> clazz);

// 默认的 SqlSession 不提交事务, 而且调用一个方法后自动关闭
public abstract <T> T createMapper(Class<? extends Mapper> clazz);

protected abstract void createSqlSessionFactory() throws IOException;

protected abstract void closeThreadLocal();

public abstract ThreadLocal<SqlSession> getThreadLocal();

public abstract SqlSessionFactory getSqlSessionFactory();

public abstract SqlSession getSqlSession(boolean isCommit);

public abstract SqlSession getSqlSession();

public abstract void closeSqlSession();

private static final String MybatisConfigPath = "mybatis.xml";

static {
try {
MapperFactory.Mybatis.createSqlSessionFactory();
MapperFactory.Spring.createSqlSessionFactory();
} catch (IOException e) {
e.printStackTrace();
}
}

@SuppressWarnings("unchecked")
private static <T> T createMapper(Class<? extends Mapper> clazz, MapperFactory mapperFactory, boolean isCommit, boolean isClose) {
SqlSession sqlSession = mapperFactory.getSqlSession(isCommit);
Mapper mapper = sqlSession.getMapper(clazz);
if (isClose) {
mapperFactory.closeThreadLocal();
}
return (T) MapperProxy.bind(mapper, sqlSession, isClose);
}

private static SqlSession getSqlSession(MapperFactory mapperFactory, boolean isCommit) {
SqlSession sqlSession = mapperFactory.getThreadLocal().get();
sqlSession = Optional.ofNullable(sqlSession).orElseGet(() -> {
SqlSession temp = mapperFactory.getSqlSessionFactory().openSession(isCommit);
mapperFactory.getThreadLocal().set(temp);
return temp;
});
return sqlSession;
}

private static void closeSqlSession(MapperFactory mapperFactory) {
SqlSession sqlSession = mapperFactory.getThreadLocal().get();
if (sqlSession != null) {
sqlSession.close();
}
mapperFactory.closeThreadLocal();
}

private static class MapperProxy implements InvocationHandler {

private final Mapper mapper;
private final SqlSession sqlSession;
private final boolean isClose;

private MapperProxy(Mapper mapper, SqlSession sqlSession, boolean isClose) {
this.mapper = mapper;
this.sqlSession = sqlSession;
this.isClose = isClose;
}

// 获取代理对象的实例 Java 只能代理接口
public static Mapper bind(Mapper mapper, SqlSession sqlSession, boolean isClose) {
return (Mapper) Proxy.newProxyInstance(mapper.getClass().getClassLoader(), mapper.getClass().getInterfaces(), new MapperProxy(mapper, sqlSession, isClose));
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) {
Object result = null;
try {
result = method.invoke(mapper, args);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 默认关闭, 可以手动设置不关闭
if (isClose) {
sqlSession.close();
}
}

return result;
}
}
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void query() {
UserMapper userMapper = MapperFactory.Spring.createMapper(UserMapper.class);
User user = userMapper.query("5f414d80-c413-42d4-8844-909d3e107ce8");
System.out.println(user);
}

@Test
public void add() {
UserMapper userMapper = MapperFactory.Spring.createMapper(UserMapper.class, true);
User user = new User(UUID.randomUUID().toString(), "XXX", "Second", "123456");
int num = userMapper.add(user);
System.out.println(num);
}

补充

对于复杂的事务可以采用如下方法进行控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
SqlSession sqlSession = MapperFactory.Spring.getSqlSessionFactory().openSession(false);
try {
// to do something
// to do something
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}

通过 new SqlSessionFactoryBuilder().build() 方法创建 SqlSessionFactory 时,不需要关闭参数中的 I/O 流,因为 build() 方法中已经将其关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}

Properties 属性类型的实现方式

思路:

  1. Mybatis 配置文件中 <environment> 标签设置为动态传参。
  2. DataSourceSessionFactory 类中构建 SqlSessionFactory 时,读取 DataBaseProperties 数据库配置类中的数据库连接信息动态生成。

Mybatis 配置信息:

1
2
3
4
5
6
7
8
9
10
11
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

DataBaseProperties 数据库配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cn.worstone.utils;

import java.util.Properties;

public class DataBaseProperties {

public final static Properties Spring;

public final static Properties Mybatis;

static {
Spring = new Properties();
Spring.setProperty("driver", "com.mysql.cj.jdbc.Driver");
Spring.setProperty("url", "jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC");
Spring.setProperty("username", "root");
Spring.setProperty("password", "123456");

Mybatis = new Properties();
Mybatis.setProperty("driver", "com.mysql.cj.jdbc.Driver");
Mybatis.setProperty("url", "jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC");
Mybatis.setProperty("username", "root");
Mybatis.setProperty("password", "123456");
}
}

DataSourceSessionFactory 类以及测试方法:

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
package cn.worstone.utils;

import cn.worstone.bean.User;
import cn.worstone.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public final class DataSourceSessionFactory {

private static String MybatisConfigPath = "mybatis.xml";
private static Map<Properties, SqlSessionFactory> sessionFactoryMap = new HashMap<>();

public static SqlSessionFactory getSessionFactory(Properties dataSourceProperties) {
SqlSessionFactory sqlSessionFactory = DataSourceSessionFactory.sessionFactoryMap.get(dataSourceProperties);
sqlSessionFactory = Optional.ofNullable(sqlSessionFactory).orElseGet(()->{
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(DataSourceSessionFactory.MybatisConfigPath);
} catch (IOException e) {
e.printStackTrace();
}
return new SqlSessionFactoryBuilder().build(inputStream, dataSourceProperties);
});

sessionFactoryMap.put(dataSourceProperties, sqlSessionFactory);
return sqlSessionFactory;
}

public static void main(String[] args) {
SqlSession sqlSession = DataSourceSessionFactory.getSessionFactory(DataBaseProperties.Spring).openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.queryList();
sqlSession.close();
for (User user : userList) {
System.out.println(user);
}
}
}

参考资料:

__END__