中介者模式介绍 中介者的作用是,当复杂功能应用指尖重复调用时,在中间添加一层中介者包装服务,对外提供简单、通用和易扩展的服务能力。
这种模式在日常生活和实际业务开发中随处可见,如十字路口有交警指挥交通,飞机降落时有营运人员在塔台喊话,公司系统中有中台系统包括所有接口和提供提供同意的服务等,除此之外,平时用到一些中间件,它们包装了底层的多种数据库的差异化,对外提供非常简单的调用。
手写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.*;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) ))。
这是对ORM框架实现的核心类,包括:加载配置文件、解析XML、获取数据库Session、操作数据库及返回借口。左上方是对数据库的定义和处理,基本包括常用的处理类,这里的工厂会造作DefaultSession。之后是对工厂建造者类SqlSessionFactoryBuilder,是对数据库操作的核心类:处理工厂、解析文件和获取Session等。
接下来分别介绍各个类的(重点)功能实现过程。
定义SqlSession接口 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.bestrookie.design.mediator;import java.util.List;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;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;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;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 ; } 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" ); 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;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的效果。除了以上这种组件模式的开发,还可以使用中介者模式实现服务接口的包装。比如公司有很多的奖品接口需要在营销活动中对接,可以把这些奖品接口统一汇总到中台再开发一个奖品中心,对外提供服务。这样就不需要每一位研发人员都去找奖品接口提供方,而是找中台服务即可。
在上述的实现和测试中可以看到,这种设计模式满足了单一职责和开闭原则,也就是符合了迪米特法则,即越少人知道越好。外部的人只需要按照需求调用,不需要知道具体是如何实现的,复杂的内容由组件合作服务平台处理即可。