作者:小傅哥
部落格:https://bugstack.cn -
原創系列專題文章
沉澱、分享、成長,讓自己和他人都能有所收獲!😄
一、前言
同齡人的差距是從什麼時候拉開的
同樣的幼稚園、同樣的國小、一樣的書本、一樣的課堂,有人學習好、有人學習差。不隻是上學,幾乎人生處處都是賽道,發令槍響起的時刻,也就把人生的差距拉開。程式設計開發這條路也是很長很寬,有人跑得快有人跑得慢。那麼你是否想起過,這一點點的差距到遙不可及的距離,是從哪一天開始的。摸摸肚子的肉,看看遠處的路,别人講的是
故事
,你想起的都是
事故
。
思想沒有産品高才寫出一片的ifelse
當你承接一個需求的時候,比如;交易、訂單、營銷、保險等各類場景。如果你不熟悉這個場景下的業務模式,以及将來的拓展方向,那麼很難設計出良好可擴充的系統。再加上産品功能初建,說老闆要的急,盡快上線。作為程式員的你更沒有時間思考,整體一看現在的需求也不難,直接上手開幹(
一個方法兩個if語句
),這樣确實滿足了目前需求。但老闆的想法多呀,産品也跟着變化快,到你這就是改改改,加加加。當然你也不客氣,回首掏就是1024個if語句!
日積月累的技術沉澱是為了厚積薄發
粗略的估算過,如果從上大學開始每天寫
200
行,一個月是
6000
行,一年算10個月話,就是6萬行,第三年出去實習的是時候就有
20
萬行的代碼量。如果你能做到這一點,找工作難?有時候很多事情就是靠時間積累出來的,想走捷徑有時候真的沒有。你的技術水準、你的業務能力、你身上的肉,都是一點點積累下來的,不要浪費看似很短的時間,一年年堅持下來,留下印刻青春的痕迹,多給自己武裝上一些能力。
二、開發環境
- JDK 1.8
- Idea + Maven
- mysql 5.1.20
- 涉及工程一個,可以通過關注公衆号:
,回複bugstack蟲洞棧
擷取(打開擷取的連結,找到序号18)源碼下載下傳
工程 | 描述 |
---|---|
itstack-demo-design-16-01 | 使用JDBC方式連接配接資料庫 |
itstack-demo-design-16-02 | 手寫ORM架構操作資料庫 |
三、中介者模式介紹
中介者模式要解決的就是複雜功能應用之間的重複調用,在這中間添加一層中介者包裝服務,對外提供簡單、通用、易擴充的服務能力。
這樣的設計模式幾乎在我們日常生活和實際業務開發中都會見到,例如;飛機🛬降落有小姐姐在塔台喊話、無論哪個方向來的候車都從站台上下、公司的系統中有一個中台專門為你包裝所有接口和提供統一的服務等等,這些都運用了中介者模式。除此之外,你用到的一些中間件,他們包裝了底層多種資料庫的差異化,提供非常簡單的方式進行使用。
四、案例場景模拟
在本案例中我們通過模仿Mybatis手寫ORM架構,通過這樣操作資料庫學習中介者運用場景
除了這樣的中間件層使用場景外,對于一些外部接口,例如N種獎品服務,可以由中台系統進行統一包裝對外提供服務能力。也是中介者模式的一種思想展現。
在本案例中我們會把jdbc層進行包裝,讓使用者在使用資料庫服務的時候,可以和使用mybatis一樣簡單友善,通過這樣的源碼方式學習中介者模式,也友善對源碼知識的拓展學習,增強知識棧。
五、用一坨坨代碼實作
這是一種關于資料庫操作最初的方式
基本上每一個學習開發的人都學習過直接使用jdbc方式連接配接資料庫,進行CRUD操作。以下的例子可以當做回憶。
1. 工程結構
itstack-demo-design-16-01
└── src
└── main
└── java
└── org.itstack.demo.design
└── JDBCUtil.java
- 這裡的類比較簡單隻包括了一個資料庫操作類。
2. 代碼實作
public class JDBCUtil {
private static Logger logger = LoggerFactory.getLogger(JDBCUtil.class);
public static final String URL = "jdbc:mysql://127.0.0.1:3306/itstack-demo-design";
public static final String USER = "root";
public static final String PASSWORD = "123456";
public static void main(String[] args) throws Exception {
//1. 加載驅動程式
Class.forName("com.mysql.jdbc.Driver");
//2. 獲得資料庫連接配接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
//3. 操作資料庫
Statement stmt = conn.createStatement();
ResultSet resultSet = stmt.executeQuery("SELECT id, name, age, createTime, updateTime FROM user");
//4. 如果有資料 resultSet.next() 傳回true
while (resultSet.next()) {
logger.info("測試結果 姓名:{} 年齡:{}", resultSet.getString("name"),resultSet.getInt("age"));
}
}
}
- 以上是使用JDBC的方式進行直接操作資料庫,幾乎大家都使用過這樣的方式。
3. 測試結果
15:38:10.919 [main] INFO org.itstack.demo.design.JDBCUtil - 測試結果 姓名:水水 年齡:18
15:38:10.922 [main] INFO org.itstack.demo.design.JDBCUtil - 測試結果 姓名:豆豆 年齡:18
15:38:10.922 [main] INFO org.itstack.demo.design.JDBCUtil - 測試結果 姓名:花花 年齡:19
Process finished with exit code 0
- 從測試結果可以看到這裡已經查詢到了資料庫中的資料。隻不過如果在全部的業務開發中都這樣實作,會非常的麻煩。
六、中介模式開發ORM架構
`接下來就使用中介模式的思想完成模仿Mybatis的ORM架構開發~
itstack-demo-design-16-02
└── src
├── main
│ ├── java
│ │ └── org.itstack.demo.design
│ │ ├── dao
│ │ │ ├── ISchool.java
│ │ │ └── IUserDao.java
│ │ ├── mediator
│ │ │ ├── Configuration.java
│ │ │ ├── DefaultSqlSession.java
│ │ │ ├── DefaultSqlSessionFactory.java
│ │ │ ├── Resources.java
│ │ │ ├── SqlSession.java
│ │ │ ├── SqlSessionFactory.java
│ │ │ ├── SqlSessionFactoryBuilder.java
│ │ │ └── SqlSessionFactoryBuilder.java
│ │ └── po
│ │ ├── School.java
│ │ └── User.java
│ └── resources
│ ├── mapper
│ │ ├── School_Mapper.xml
│ │ └── User_Mapper.xml
│ └── mybatis-config-datasource.xml
└── test
└── java
└── org.itstack.demo.design.test
└── ApiTest.java
中介者模式模型結構
- 以上是對ORM架構實作的核心類,包括了;加載配置檔案、對xml解析、擷取資料庫session、操作資料庫以及結果傳回。
- 左上是對資料庫的定義和處理,基本包括我們常用的方法;
、<T> T selectOne
等。<T> List<T> selectList
- 右側藍色部分是對資料庫配置的開啟session的工廠處理類,這裡的工廠會操作
DefaultSqlSession
- 之後是紅色地方的
,這個類是對資料庫操作的核心類;處理工廠、解析檔案、拿到session等。SqlSessionFactoryBuilder
接下來我們就分别介紹各個類的功能實作過程。
2.1 定義SqlSession接口
public interface SqlSession {
<T> T selectOne(String statement);
<T> T selectOne(String statement, Object parameter);
<T> List<T> selectList(String statement);
<T> List<T> selectList(String statement, Object parameter);
void close();
}
- 這裡定義了對資料庫操作的查詢接口,分為查詢一個結果和查詢多個結果,同時包括有參數和沒有參數的方法。
2.2 SqlSession具體實作類
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) {
try {
XNode xNode = mapperElement.get(statement);
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> 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;
}
// ...
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 Timestamp) {
method = clazz.getMethod(setMethod, Date.class);
} else {
method = clazz.getMethod(setMethod, value.getClass());
}
method.invoke(obj, value);
}
list.add(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
@Override
public void close() {
if (null == connection) return;
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- 這裡包括了接口定義的方法實作,也就是包裝了jdbc層。
- 通過這樣的包裝可以讓對資料庫的jdbc操作隐藏起來,外部調用的時候對入參、出參都有内部進行處理。
2.3 定義SqlSessionFactory接口
public interface SqlSessionFactory {
SqlSession openSession();
}
- 開啟一個
, 這幾乎是大家在平時的使用中都需要進行操作的内容。雖然你看不見,但是當你有資料庫操作的時候都會擷取每一次執行的SqlSession
SqlSession
2.4 SqlSessionFactory具體實作類
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);
}
}
-
,是使用mybatis最常用的類,這裡我們簡單的實作了一個版本。DefaultSqlSessionFactory
- 雖然是簡單的版本,但是包括了最基本的核心思路。當開啟
時會進行傳回一個SqlSession
DefaultSqlSession
- 這個構造函數中向下傳遞了
配置檔案,在這個配置檔案中包括;Configuration
Connection connection
Map<String, String> dataSource
。如果有你閱讀過Mybatis源碼,對這個就不會陌生。Map<String, XNode> mapperElement
2.5 SqlSessionFactoryBuilder實作
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(建構執行個體化元素)
parseConfiguration(解析配置)
dataSource(擷取資料庫配置)
connection(Map<String, String> dataSource) (連結資料庫)
mapperElement (解析sql語句)
- 接下來我們分别介紹這樣的幾個核心方法。
build(建構執行個體化元素)
這個類主要用于建立解析xml檔案的類,以及初始化SqlSession工廠類
DefaultSqlSessionFactory
。另外需要注意這段代碼
saxReader.setEntityResolver(new XMLMapperEntityResolver());
,是為了保證在不聯網的時候一樣可以解析xml,否則會需要從網際網路擷取dtd檔案。
parseConfiguration(解析配置)
是對xml中的元素進行擷取,這裡主要擷取了;
dataSource
mappers
,而這兩個配置一個是我們資料庫的連結資訊,另外一個是對資料庫操作語句的解析。
connection(Map<String, String> dataSource) (連結資料庫)
連結資料庫的地方和我們常見的方式是一樣的;
Class.forName(dataSource.get("driver"));
,但是這樣包裝以後外部是不需要知道具體的操作。同時當我們需要連結多套資料庫的時候,也是可以在這裡擴充。
mapperElement (解析sql語句)
這部分代碼塊内容相對來說比較長,但是核心的點就是為了解析xml中的sql語句配置。在我們平常的使用中基本都會配置一些sql語句,也有一些入參的占位符。在這裡我們使用正規表達式的方式進行解析操作。
解析完成的sql語句就有了一個名稱和sql的映射關系,當我們進行資料庫操作的時候,這個元件就可以通過映射關系擷取到對應sql語句進行操作。
3. 測試驗證
在測試之前需要導入sql語句到資料庫中;
- 庫名:
itstack-demo-design
- 表名:
user
school
CREATE TABLE school ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64), address varchar(256), createTime datetime, updateTime datetime, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into school (id, name, address, createTime, updateTime) values (1, '北京大學', '北京市海澱區頤和園路5号', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (2, '南開大學', '中國天津市南開區衛津路94号', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (3, '同濟大學', '上海市彰武路1号同濟大廈A樓7樓7區', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
CREATE TABLE user ( id bigint(11) NOT NULL AUTO_INCREMENT, name varchar(32), age int(4), address varchar(128), entryTime datetime, remark varchar(64), createTime datetime, updateTime datetime, status int(4) DEFAULT '0', dateTime varchar(64), PRIMARY KEY (id), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (1, '水水', 18, '吉林省榆樹市黑林鎮尹家村5組', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309');
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (2, '豆豆', 18, '遼甯省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null);
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (3, '花花', 19, '遼甯省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');
3.1 建立資料庫對象類
使用者類
public class User {
private Long id;
private String name;
private Integer age;
private Date createTime;
private Date updateTime;
// ... get/set
}
學校類
public class School {
private Long id;
private String name;
private String address;
private Date createTime;
private Date updateTime;
// ... get/set
}
- 這兩個類都非常簡單,就是基本的資料庫資訊。
3.2 建立DAO包
使用者Dao
public interface IUserDao {
User queryUserInfoById(Long id);
}
學校Dao
public interface ISchoolDao {
School querySchoolInfoById(Long treeId);
}
3.3 ORM配置檔案
連結配置
<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://127.0.0.1:3306/itstack_demo_design?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>
- 這個配置與我們平常使用的mybatis基本是一樣的,包括了資料庫的連接配接池資訊以及需要引入的mapper映射檔案。
操作配置(使用者)
<mapper namespace="org.itstack.demo.design.dao.IUserDao">
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where id = #{id}
</select>
<select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where age = #{age}
</select>
</mapper>
操作配置(學校)
<mapper namespace="org.itstack.demo.design.dao.ISchoolDao">
<select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
SELECT id, name, address, createTime, updateTime
FROM school
where id = #{id}
</select>
</mapper>
3.4 單個結果查詢測試
@Test
public void test_queryUserInfoById() {
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("org.itstack.demo.design.dao.IUserDao.queryUserInfoById", 1L);
logger.info("測試結果:{}", JSON.toJSONString(user));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
- 這裡的使用方式和
是一樣的,都包括了;資源加載和解析、Mybatis
工廠建構、開啟SqlSession
以及最後執行查詢操作SqlSession
selectOne
測試結果
16:56:51.831 [main] INFO org.itstack.demo.design.demo.ApiTest - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
Process finished with exit code 0
- 從結果上看已經滿足了我們的查詢需求。
3.5 集合結果查詢測試
@Test
public void test_queryUserList() {
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 req = new User();
req.setAge(18);
List<User> userList = session.selectList("org.itstack.demo.design.dao.IUserDao.queryUserList", req);
logger.info("測試結果:{}", JSON.toJSONString(userList));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
- 這個測試内容與以上隻是查詢方法有所不同;
,是查詢一個集合結果。session.selectList
16:58:13.963 [main] INFO org.itstack.demo.design.demo.ApiTest - 測試結果:[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]
Process finished with exit code 0
- 測試驗證集合的結果也是正常的,目前位置測試全部通過。
七、總結
- 以上通過中介者模式的設計思想我們手寫了一個ORM架構,隐去了對資料庫操作的複雜度,讓外部的調用方可以非常簡單的進行操作資料庫。這也是我們平常使用的
的原型,在我們日常的開發使用中,隻需要按照配置即可非常簡單的操作資料庫。Mybatis
- 除了以上這種元件模式的開發外,還有服務接口的包裝也可以使用中介者模式來實作。比如你們公司有很多的獎品接口需要在營銷活動中對接,那麼可以把這些獎品接口統一收到中台開發一個獎品中心,對外提供服務。這樣就不需要每一個需要對接獎品的接口,都去找具體的提供者,而是找中台服務即可。
- 在上述的實作和測試使用中可以看到,這種模式的設計滿足了;
和單一職責
,也就符合了開閉原則
,即越少人知道越好。外部的人隻需要按照需求進行調用,不需要知道具體的是如何實作的,複雜的一面已經有元件合作服務平台處理。迪米特原則
八、推薦閱讀
-
1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
-
2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
-
3. 重學 Java 設計模式:實戰橋接模式「多支付管道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
-
4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
-
5. 重學 Java 設計模式:實戰外觀模式「基于SpringBoot開發門面模式中間件,統一控制接口白名單場景」
-
6. 重學 Java 設計模式:實戰享元模式「基于Redis秒殺,提供活動與庫存資訊查詢場景」
公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!