Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

中介者模式介绍

中介者的作用是,当复杂功能应用指尖重复调用时,在中间添加一层中介者包装服务,对外提供简单、通用和易扩展的服务能力。

这种模式在日常生活和实际业务开发中随处可见,如十字路口有交警指挥交通,飞机降落时有营运人员在塔台喊话,公司系统中有中台系统包括所有接口和提供提供同意的服务等,除此之外,平时用到一些中间件,它们包装了底层的多种数据库的差异化,对外提供非常简单的调用。

手写ORM中间件场景

模仿Mybatis手写ORM框架,通过操作数据库学习中介者模式。

除了中间件视同场景,对于外部接口,例如N中奖品服务,也可以由中台系统同意包装,再对外提供服务能力,这也是一种中介者模式思想方案的落地体现。这个例子是对JDBC层包装,让用户在使用数据库服务时像使用Mybatis一样简单方便,通过对ORM框架源码技术迁移运用的方式学习中介者模式,更能增强和扩展知识栈。

违背设计模式实现

下面的方式是采用原始的对数据库操作方式。

代码实现

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
package com.bestrookie;

import java.sql.*;

/**
* @author bestrookie
* @date 2021/11/23 2:16 下午
*/
public class JDBCUtil {
public static final String URL = "jdbc:mysql://localhost:3306/yin";
public static final String USER = "root";
public static final String PASSWORD = "123456";

public static void main(String[] args) throws Exception {
//加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//获得数据库连接
Connection conn = DriverManager.getConnection(URL,USER,PASSWORD);
//操作数据库
Statement stmt = conn.createStatement();
ResultSet resultSet = stmt.executeQuery("select id, name,age,createTime,updateTime from user");
while (resultSet.next()){
System.out.println(resultSet.getString("name")+" 年龄:"+resultSet.getInt("age"));
}
}
}

以上是使用JDBC方式直接操作数据库,整个过程可以分为:加载驱动、获得数据库连接、操作数据库和获取执行结果。

中介者模式开发ORM框架

接下来使用中介者模式模仿Mybatis的ORM框架的开发。Mybatis的框架涉及内容较多,虽然在使用时非常方便,直接使用注解或者XML配置就可以操作数据库返回结果。但在是线上,MyBatis作为中间层已经处理了SQL语句的获取、数据库连接、执行和返回封装结果等。等接下来把Mybatis最核心的部分抽离出来,手动实现一个ORM框架,以便学习中介模式(非[完整代码](itstack-demo-design/itstack-demo-design-16-02 at master · fuzhengwei/itstack-demo-design (github.com)))。

image-20211128230142899

这是对ORM框架实现的核心类,包括:加载配置文件、解析XML、获取数据库Session、操作数据库及返回借口。左上方是对数据库的定义和处理,基本包括常用的处理类,这里的工厂会造作DefaultSession。之后是对工厂建造者类SqlSessionFactoryBuilder,是对数据库操作的核心类:处理工厂、解析文件和获取Session等。

接下来分别介绍各个类的(重点)功能实现过程。

image-20211129220339258

定义SqlSession接口

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.bestrookie.design.mediator;
import java.util.List;
/**
* @author bestrookie
* @date 2021/11/23 4:16 下午
*/
public interface SqlSession {
<T> T selectOne(String statement);
<T> T selectOne(String statement,Object parameter);
<T> List<T> selectList(String statement, Object parameter);
<T> List<T> selectList(String statement);
void close();
}

这里定义了操作数据库的查询接口,分别查询一个结果和多个结果,同时包括有参数方法和无参数方法。

Session具体实现类

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
package com.bestrookie.design.mediator;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Date;

/**
* @author bestrookie
* @date 2021/11/23 4:41 下午
*/
public class DefaultSqlSession implements SqlSession{
private Connection connection;
private Map<String, XNode> mapperElement;
public DefaultSqlSession(Connection connection,Map<String, XNode> mapperElement){
this.connection = connection;
this.mapperElement = mapperElement;
}
@Override
public <T> T selectOne(String statement){
XNode xNode = mapperElement.get(statement);
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
return objects.get(0);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@Override
public <T> T selectOne(String statement, Object parameter) {
XNode xNode = mapperElement.get(statement);
Map<Integer, String> parameterMap = xNode.getParameter();
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
buildParameter(preparedStatement,parameter,parameterMap);
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
return objects.get(0);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@Override
public <T> List<T> selectList(String statement, Object parameter) {
XNode xNode = mapperElement.get(statement);
Map<Integer,String> parameterMap = xNode.getParameter();
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
buildParameter(preparedStatement,parameter,parameterMap);
ResultSet resultSet = preparedStatement.executeQuery();
return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@Override
public <T> List<T> selectList(String statement) {
XNode xNode = mapperElement.get(statement);
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
ResultSet resultSet = preparedStatement.executeQuery();
return resultSet2Obj(resultSet,Class.forName(xNode.getResultType()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@Override
public void close() {
if (null == connection) {
return;
}
try{
connection.close();
}catch (SQLException e){
e.printStackTrace();
}
}

private <T>List<T> resultSet2Obj(ResultSet resultSet,Class<?> clazz){
List<T> list = new ArrayList<>();
try {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
// 每次遍历行值
while (resultSet.next()) {
T obj = (T) clazz.newInstance();
for (int i = 1; i <= columnCount; i++) {
Object value = resultSet.getObject(i);
String columnName = metaData.getColumnName(i);
String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
Method method;
if (value instanceof java.util.Date) {
method = clazz.getMethod(setMethod, java.util.Date.class);
} else {
method = clazz.getMethod(setMethod, value.getClass());
}
method.invoke(obj, value);
}
list.add(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
private void buildParameter(PreparedStatement preparedStatement,Object parameter,Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {
int size = parameterMap.size();
if (parameter instanceof Long){
for (int i = 1; i<= size; i++){
preparedStatement.setLong(i,Long.parseLong(parameter.toString()));
}
return;
}
if (parameter instanceof Integer){
for (int i = 1;i<= size;i++){
preparedStatement.setInt(i,Integer.parseInt(parameter.toString()));
}
return;
}
if (parameter instanceof String){
for (int i = 1; i<= size;i++){
preparedStatement.setString(i,parameter.toString());
}
return;
}
Map<String,Object> fieldMap = new HashMap<>();
Field[] declaredFields = parameter.getClass().getDeclaredFields();
for (Field field : declaredFields){
String name = field.getName();
field.setAccessible(true);
Object obj = field.get(parameter);
field.setAccessible(false);
fieldMap.put(name,obj);
}
for (int i = 1 ; i<= size; i++){
String parameterDefine = parameterMap.get(i);
Object obj = fieldMap.get(parameterDefine);
if (obj instanceof Short){
preparedStatement.setShort(i,Short.parseShort(obj.toString()));
continue;
}
if (obj instanceof Integer) {
preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
continue;
}
if (obj instanceof Long) {
preparedStatement.setLong(i, Long.parseLong(obj.toString()));
continue;
}

if (obj instanceof String) {
preparedStatement.setString(i, obj.toString());
continue;
}

if (obj instanceof Date) {
preparedStatement.setDate(i, (java.sql.Date) obj);
}
}
}
}

这里包括了接口定义的方法实现,即包装了JDBC的使用。通过这种包装,可以隐藏数据库的JDBC操作,当外部调用时,对入参、出参都由内部处理

定义SqlSessionFactory接口

1
2
3
4
package com.bestrookie.design.mediator;
public interface SqlSessionFactory {
SqlSession openSession();
}

开启一个SqlSession,这是平时都需要操作的内容。虽然看不见,但是当有数据库时,都会获取每一次执行的SqlSession。

SqlSessionFactory具体实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.bestrookie.design.mediator;
/**
* @author bestrookie
* @version 1.0
* @date 2021/11/28 11:07
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory{
private final Configuration configuration;

public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}

@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
}
}

DefaultSqlSessionFactory是Mybatis最常用的类,这里简单地实现了一个版本。虽然是简单版本,但包括了最基本的核心思路。开启SqlSession时,会返回一个DefaultSqlSession。这个构造函数向下传递了Configuration配置文件,包括:Connection connection、Map<String,String> dataSource、Map<Strring、XNode> mapperElement。如果阅读过Mybatis源码,对此应该不会陌生。

SqlSessionFactoryBuilder实现

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
package com.bestrookie.design.mediator;

import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;

import java.io.Reader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author bestrookie
* @version 1.0
* @date 2021/11/28 11:11
*/
public class SqlSessionFactoryBuilder {
public DefaultSqlSessionFactory build(Reader reader) {
SAXReader saxReader = new SAXReader();
try {
saxReader.setEntityResolver(new XMLMapperEntityResolver());
Document document = saxReader.read(new InputSource(reader));
Configuration configuration = parseConfiguration(document.getRootElement());
return new DefaultSqlSessionFactory(configuration);
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}

private Configuration parseConfiguration(Element root) {
Configuration configuration = new Configuration();
configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
configuration.setConnection(connection(configuration.dataSource));
configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
return configuration;
}

// 获取数据源配置信息
private Map<String, String> dataSource(List<Element> list) {
Map<String, String> dataSource = new HashMap<>(4);
Element element = list.get(0);
List content = element.content();
for (Object o : content) {
Element e = (Element) o;
String name = e.attributeValue("name");
String value = e.attributeValue("value");
dataSource.put(name, value);
}
return dataSource;
}

private Connection connection(Map<String, String> dataSource) {
try {
Class.forName(dataSource.get("driver"));
return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return null;
}

// 获取SQL语句信息
private Map<String, XNode> mapperElement(List<Element> list) {
Map<String, XNode> map = new HashMap<>();

Element element = list.get(0);
List content = element.content();
for (Object o : content) {
Element e = (Element) o;
String resource = e.attributeValue("resource");

try {
Reader reader = Resources.getResourceAsReader(resource);
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new InputSource(reader));
Element root = document.getRootElement();
//命名空间
String namespace = root.attributeValue("namespace");

// SELECT
List<Element> selectNodes = root.selectNodes("select");
for (Element node : selectNodes) {
String id = node.attributeValue("id");
String parameterType = node.attributeValue("parameterType");
String resultType = node.attributeValue("resultType");
String sql = node.getText();

// ? 匹配
Map<Integer, String> parameter = new HashMap<>();
Pattern pattern = Pattern.compile("(#\\{(.*?)})");
Matcher matcher = pattern.matcher(sql);
for (int i = 1; matcher.find(); i++) {
String g1 = matcher.group(1);
String g2 = matcher.group(2);
parameter.put(i, g2);
sql = sql.replace(g1, "?");
}

XNode xNode = new XNode();
xNode.setNamespace(namespace);
xNode.setId(id);
xNode.setParameterType(parameterType);
xNode.setResultType(resultType);
xNode.setSql(sql);
xNode.setParameter(parameter);

map.put(namespace + "." + id, xNode);
}
} catch (Exception ex) {
ex.printStackTrace();
}

}
return map;
}

}

这个类包括的核心方法有:build(构建实例化元素)、paseConfiguration(解析配置)、dataSource(获取数据库配置)、connection(Map<String,String>dataSource)(连接数据库)和mapperElement(解析SQL语句)。接下来分别介绍这几种核心方法。

·1、build(构建实例化元素)

这个类的主要作用于创建解析XML文件的类,以及初始化SqlSession工厂类DefaultSqlSessionFactory。另外,需要注意代码saxReader.setEntityResolver(new XML MapperEntityResolver());是为了保证在不联网的情况下同样可以解析XML,否则会需要冲互联网获取dtd文件。

2、parseConfiguration(解析配置)

这个类是对XML中的元素进行获取,这里主要获取了dataSource、mappers两个配置,一个是数据库的链接信息,另一个是对数据库操作语句的解析。

3、connection(Map<String,String>dataSource)(连接数据库)

数据库连接开启操作的方法和常见的方式一样的:Class.forName(dataSource.get(“driver”));但是这样包装以后,外部不需要知道具体是如何操作的。同时,当需要连接多套数据库时,也可以在这里扩展。

4、mapperElement(解析SQL语句)

这部分代码块的内容相对来说比较长,但核心是为了解析XML中的SQL语句配置。在平常的使用中,基本都会配置一些SQL语句,也有一些入参的占位符。此处使用正则表达式的方式解析操作。

解析完的SQL语句就有了一个名称和SQL的映射关系,当操作数据库时,这个组件就可以通过映射关系获取对一个的SQL语句。

创建数据库对象类

这里就创建两个简单的表来测试

用户类
1
2
3
4
5
6
7
8
9
10
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private Integer age;
private Date createTime;
private Date updateTime;
}
学校类
1
2
3
4
5
6
7
8
9
@Data
@AllArgsConstructor
@NoArgsConstructor
public class School {
private Long id;
private String name;
private Date createTime;
private Date updateTime;
}

创建Dao包

用户Dao
1
2
3
public interface IUserDao {
User queryUserInfoById(Integer id);
}
学校Dao
1
2
3
public interface ISchoolDao {
School querySchoolInfoById(Long id);
}

两个类和平时使用的Mybatis一样,一个用户查询用户信息IUserDao,另一个用于查询学校信息ISchooleDao。

ORM配置文件

连接配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://locallhost:3306/yin?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/User_Mapper.xml"/>
<mapper resource="mapper/School_Mapper.xml"/>
</mappers>

</configuration>
操作配置(用户)
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bestrookie.design.dao.IUserDao">
<select id="queryUserInfoById" parameterType="java.lang.Integer" resultType="com.bestrookie.design.pojo.User">
SELECT * FROM user WHERE id = #{id}
</select>

</mapper>
操作配置(学校)
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bestrookie.design.dao.ISchoolDao">
<select id="querySchoolInfoById" resultType="com.bestrookie.design.pojo.School" parameterType="java.lang.Long">
SELECT * from school where id =#{id}
</select>

</mapper>

简单测试

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
package com.bestrookie.design;

import com.alibaba.fastjson.JSON;
import com.bestrookie.design.mediator.Resources;
import com.bestrookie.design.mediator.SqlSession;
import com.bestrookie.design.mediator.SqlSessionFactory;
import com.bestrookie.design.mediator.SqlSessionFactoryBuilder;
import com.bestrookie.design.pojo.User;

import java.io.IOException;
import java.io.Reader;

/**
* @author bestrookie
* @version 1.0
* @date 2021/11/28 21:51
*/
public class Test {
public static void main(String[] args) {
String resource = "mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlMapper.openSession();
try {
User user = session.selectOne("com.bestrookie.design.dao.IUserDao.queryUserInfoById", 1);
System.out.println("测试结果:"+ JSON.toJSONString(user));
}finally {
session.close();
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

总结

运用中介者模式的设计思想手写了一个ORM框架,隐去对数据库的复杂操作,让外部的调用方能够非常简单地操作数据库,这也是平常使用Mybatis的效果。除了以上这种组件模式的开发,还可以使用中介者模式实现服务接口的包装。比如公司有很多的奖品接口需要在营销活动中对接,可以把这些奖品接口统一汇总到中台再开发一个奖品中心,对外提供服务。这样就不需要每一位研发人员都去找奖品接口提供方,而是找中台服务即可。

在上述的实现和测试中可以看到,这种设计模式满足了单一职责和开闭原则,也就是符合了迪米特法则,即越少人知道越好。外部的人只需要按照需求调用,不需要知道具体是如何实现的,复杂的内容由组件合作服务平台处理即可。

评论