mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1241 lines
32 KiB
HTML
1241 lines
32 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>13 如何实现支持快速交付的技术中台设计?.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/00 开篇词 让我们把 DDD 的思想真正落地.md.html">00 开篇词 让我们把 DDD 的思想真正落地.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/01 DDD :杜绝软件退化的利器.md.html">01 DDD :杜绝软件退化的利器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/02 以电商支付功能为例演练 DDD.md.html">02 以电商支付功能为例演练 DDD.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/03 DDD 是如何落地到数据库设计的?.md.html">03 DDD 是如何落地到数据库设计的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/04 领域模型是如何指导程序设计的?.md.html">04 领域模型是如何指导程序设计的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/05 聚合、仓库与工厂:傻傻分不清楚.md.html">05 聚合、仓库与工厂:傻傻分不清楚.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/06 限界上下文:冲破微服务设计困局的利器.md.html">06 限界上下文:冲破微服务设计困局的利器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/07 在线订餐场景中是如何开事件风暴会议的?.md.html">07 在线订餐场景中是如何开事件风暴会议的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/08 DDD 是如何解决微服务拆分难题的?.md.html">08 DDD 是如何解决微服务拆分难题的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/09 DDD 是如何落地微服务设计实现的?.md.html">09 DDD 是如何落地微服务设计实现的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/10 微服务落地的技术实践.md.html">10 微服务落地的技术实践.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/11 解决技术改造困局的钥匙:整洁架构.md.html">11 解决技术改造困局的钥匙:整洁架构.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/12 如何设计支持快速交付的技术中台战略?.md.html">12 如何设计支持快速交付的技术中台战略?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/DDD 微服务落地实战/13 如何实现支持快速交付的技术中台设计?.md.html">13 如何实现支持快速交付的技术中台设计?.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/14 如何设计支持 DDD 的技术中台?.md.html">14 如何设计支持 DDD 的技术中台?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/15 如何设计支持微服务的技术中台?.md.html">15 如何设计支持微服务的技术中台?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/16 基于 DDD 的代码设计演示(含 DDD 的技术中台设计).md.html">16 基于 DDD 的代码设计演示(含 DDD 的技术中台设计).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/17 基于 DDD 的微服务设计演示(含支持微服务的 DDD 技术中台设计).md.html">17 基于 DDD 的微服务设计演示(含支持微服务的 DDD 技术中台设计).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/18 基于事件溯源的设计开发.md.html">18 基于事件溯源的设计开发.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>13 如何实现支持快速交付的技术中台设计?</h1>
|
||
|
||
<p>前面提到了“大前端”的思想,也就是软件团队组织形式的趋势是“大前端 + 技术中台”,从而通过快速交付提高市场竞争力。所谓的“大前端 + 技术中台”,就是在开发团队中有一个<strong>架构支持团队</strong>,他们通过架构强大的技术中台,将软件开发中的许多技术架构封装在平台中。有了这样一个技术中台,其他各个开发团队都基于它进行业务开发。</p>
|
||
|
||
<p>这样,既可以降低业务开发的工作量,提高开发速度,又可以降低技术门槛。业务开发人员也不必过于关注技术,而是将更多的精力集中在对业务的理解,并将对业务深刻的理解融入领域建模的过程中,从而开发出用户更满意的软件,提高用户体验。</p>
|
||
|
||
<p>因此,如何打造一个强大而实用的技术中台,成了各个软件开发团队迫切的需求。现在我们就从实战的角度看一看,以上这些设计思想该如何落地技术中台建设。</p>
|
||
|
||
<p><strong>命令与查询职责分离</strong>(CQRS),是软件大师 Martin Fowler 在他的著作《企业应用架构模式》中提出来的一种架构设计模式。该模式将系统按照职责划分为<strong>命令</strong>(即增删改操作)与<strong>查询</strong>两个部分。</p>
|
||
|
||
<ul>
|
||
|
||
<li>所有命令部分的增删改操作,应当采用<strong>领域驱动设计的思想</strong>进行软件设计,从而更好地应对大规模复杂应用;</li>
|
||
|
||
<li>所有的查询功能则不适用于领域驱动设计,而应当采用<strong>事务脚本模式</strong>(Transaction Script),即直接通过 SQL 语句进行查询。</li>
|
||
|
||
</ul>
|
||
|
||
<p>遵循该设计模式,是我们在许多软件项目中总结出来的最佳实践。因此,技术中台在建设时,对业务系统的支持也分为<strong>增删改</strong>与<strong>查询</strong>两个部分。</p>
|
||
|
||
<p><img src="assets/Cip5yF_kdC6AKyj5AACxhqoIRjw348.png" alt="Drawing 0.png" /></p>
|
||
|
||
<h3>增删改的架构设计</h3>
|
||
|
||
<p><img src="assets/CgpVE1_kdD6AZg1HAALxpOUS2sc783.png" alt="Drawing 1.png" /></p>
|
||
|
||
<p>增删改部分的技术中台架构设计</p>
|
||
|
||
<p>在增删改部分中,采用了前面提到的单 Controller、单 Dao 的架构设计。如上图所示,各功能都有各自的前端 UI。但与以往架构不同的是,每个功能的前端 UI 对后台请求时,不再调用各自的 Controller,而是统一调用一个 Controller。然而,每个功能的前端在调用这一个 Controller 时,传递的参数是不一样的。首先从前端传递的是 bean,这个 bean 是什么呢?后台各功能都有一个 Service,将该 Service 注入 Dao 以后,会在 Spring 框架中配置成一个bean。这时,前端只知道调用的是这个 bean,但不知道它是哪个 Service。</p>
|
||
|
||
<p>这样的设计,既保障了<strong>安全性</strong>(前端不知道具体是哪个类),又有效地实现了<strong>前后端分离</strong>,将前端代码与后端解耦。</p>
|
||
|
||
<p>紧接着,前端还要传递一个 method,即调用的是哪个方法,和哪个 JSON 对象。这样,Controller 就可以通过反射进行相应的操作。这里的设计思想是,在软件开发过程中,通过<strong>规范与契约</strong>的约定,我们认为前端开发人员已经知道了他需要调用后端哪个 bean、哪个method,以及什么格式的 JSON,就可以大大简化技术中台的设计。</p>
|
||
|
||
<h4>单 Controller 的设计</h4>
|
||
|
||
<p>前端所有功能的增删改操作,以及基于 ID 的 get/load 操作,都是访问的 OrmController。</p>
|
||
|
||
<p>前端在访问 OrmController 时,输入如下 HTTP 请求:</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>http://localhost:9003/orm/{bean}/{method}</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>例如:</p>
|
||
|
||
<ul>
|
||
|
||
<li>GET 请求</li>
|
||
|
||
</ul>
|
||
|
||
<blockquote>
|
||
|
||
<p><a href="http://localhost:9003/orm/product/deleteProduct?id=P00006">http://localhost:9003/orm/product/deleteProduct?id=P00006</a></p>
|
||
|
||
</blockquote>
|
||
|
||
<ul>
|
||
|
||
<li>POST 请求</li>
|
||
|
||
</ul>
|
||
|
||
<blockquote>
|
||
|
||
<p>http://localhost:9003/orm/product/saveProduct-d"id=P00006&name=ThinkPad+T220&price=4600&unit=%E4%B8%AA&supplierId=20002&classify=%E5%8A%9E%E5%85%AC%E7%94%A8%E5%93%81"</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>这里的 {<strong>bean</strong>} 是配置在 Spring 中的 bean.id,{<strong>method</strong>} 是该 bean 中需要调用的方法(注意,此处不支持方法的重写,如果出现重写,它将去调用同名方法中的最后一个)。</p>
|
||
|
||
<ul>
|
||
|
||
<li>如果要调用的方法有<strong>值对象</strong>,按照规范,必须将<strong>值对象放在方法的第一个参数上</strong>。</li>
|
||
|
||
<li>如果要调用的方法<strong>既有值对象</strong>,<strong>又有其他参数</strong>,则值对象中的属性与其他参数都放在该 JSON 对象中。如:要调用的方法为 saveProduct(product, saveMode),POST 请求为:</li>
|
||
|
||
</ul>
|
||
|
||
<blockquote>
|
||
|
||
<p>http://localhost:9003/orm/product/saveProduct -d "id=500006&name=ThinkPad+T220&price=4600&unit=%E4%B8%AA&supplierId=20002&classify=%E5%8A%9E%E5%85%AC%E7%94%A8%E5%93%81&saveMode=1"</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>特别需要注意的是:目前 OrmController 不包含任何权限校验,因此配置在 Spring 中的 bean 的所有方法都可以被前端调用。所以在实际项目中需要在 OrmController 之前进行一个<strong>权限校验</strong>,来规范前端可以调用的方法。建议使用<strong>服务网关</strong>或 <strong>filter</strong> 进行校验。</p>
|
||
|
||
<p>OrmController 的流程设计如下:</p>
|
||
|
||
<ul>
|
||
|
||
<li>根据前端参数 bean,从 Spring 中获得 Service;</li>
|
||
|
||
<li>根据前端参数 method,通过反射获得调用方法;</li>
|
||
|
||
<li>通过反射获得调用方法的第一个参数作为值对象;</li>
|
||
|
||
<li>通过反射创建值对象,根据反射获得值对象的所有属性,从前端 JSON 中获得对应属性的值,写入值对象;</li>
|
||
|
||
<li>根据前端 JSON 获得其他参数;</li>
|
||
|
||
<li>将值对象与其他参数,使用反射调用 Service 中的 method 方法。</li>
|
||
|
||
</ul>
|
||
|
||
<h4>单 Dao 的设计</h4>
|
||
|
||
<p>当系统在 Service 中完成了一系列的业务操作,最终要存盘时,都统一调用一个单 Dao。但是,在调用单 Dao 之前,每个值对象都应当<strong>通过 vObj.xml 进行配置</strong>。在该配置中,将每个值对象对应的表,以及值对象中每个属性对应的字段,通过 vObj.xml 配置文件进行对应。那么通用的 BasicDao 就可以通过配置文件形成 SQL,并最终完成数据库持久化操作。</p>
|
||
|
||
<p>vObj.xml 配置文件如下:</p>
|
||
|
||
<pre><code><?xml version="1.0" encoding="UTF-8"?>
|
||
|
||
|
||
|
||
<vobjs>
|
||
|
||
|
||
|
||
<vo class="com.demo2.customer.entity.Customer" tableName="Customer">
|
||
|
||
|
||
|
||
<property name="id" column="id" isPrimaryKey="true"></property>
|
||
|
||
|
||
|
||
<property name="name" column="name"></property>
|
||
|
||
|
||
|
||
<property name="sex" column="sex"></property>
|
||
|
||
|
||
|
||
<property name="birthday" column="birthday"></property>
|
||
|
||
|
||
|
||
<property name="identification" column="identification"></property>
|
||
|
||
|
||
|
||
<property name="phone_number" column="phone_number"></property>
|
||
|
||
|
||
|
||
</vo>
|
||
|
||
|
||
|
||
</vobjs>
|
||
|
||
</code></pre>
|
||
|
||
<p>值对象中可以有很多的属性变量,但只有最终作<strong>持久化的属性变量才需要配置</strong>。这样可以使值对象的设计具有更大的空间,可以去做更多的数据转换与业务操作。前面提到充血模型的设计,就是需要在值对象中加入更多的操作与转换,使值对象可以长得与数据库的表不一样。但只要配置最后要持久化的属性,就会将这些属性写入到数据库相应的表中,或者从数据库中读取数据。</p>
|
||
|
||
<p>有了以上的设计,每个 Service 在 Spring 中都是统一注入 BasicDao。</p>
|
||
|
||
<ul>
|
||
|
||
<li>如果要使用 DDD 的功能支持,注入通用仓库 Repository;</li>
|
||
|
||
<li>如果要使用 Redis 缓存,注入 RepositoryWithCache。</li>
|
||
|
||
</ul>
|
||
|
||
<p>Spring 配置如下:</p>
|
||
|
||
<pre><code><?xml version="1.0" encoding="UTF-8"?>
|
||
|
||
|
||
|
||
<beans xmlns="http://www.springframework.org/schema/beans" ...>
|
||
|
||
|
||
|
||
<description>The application context for orm</description>
|
||
|
||
|
||
|
||
<bean id="customer" class="com.demo2...CustomerServiceImpl">
|
||
|
||
|
||
|
||
<property name="dao" ref="basicDao"></property>
|
||
|
||
|
||
|
||
</bean>
|
||
|
||
|
||
|
||
</beans>
|
||
|
||
</code></pre>
|
||
|
||
<p>特别需要说明的是,虽然当下注解比较流行,并且有诸多优势,但最大的问题是让业务代码对技术框架形成依赖,违背了技术中台设计的初衷。因此,在这里,虽然 Controller、Dao 以及其他功能设计使用了注解,但基于本框架进行的业务开发,包括 Spring 的配置、MyBatis 的配置、vObj 的配置,建议都<strong>采用 XML 文件的形式</strong>,而<strong>不要采用注解</strong>。这样,业务开发中设计的 Service 都是纯洁的,没有任何技术依赖,才能在将来移植到各种技术框架中,并长盛不衰。</p>
|
||
|
||
<p>这样,单 Dao 的流程设计如下。</p>
|
||
|
||
<p>1.单 Dao 调用 VObjFactory.getVObj(class) 获得配置信息 vObj。</p>
|
||
|
||
<p>2.根据 vObj.getTable() 获得对应的表名。</p>
|
||
|
||
<p>3.for(Property prop : vObj.getPreperties() ) {</p>
|
||
|
||
<ul>
|
||
|
||
<li>通过 prop.getColumn() 获得值对象对应的字段;</li>
|
||
|
||
<li>运用反射从值对象中获得所有属性及其对应的值;</li>
|
||
|
||
<li>通过以上参数形成 SQL 语句。</li>
|
||
|
||
</ul>
|
||
|
||
<p>4.通过 SQL 语句执行数据库操作。</p>
|
||
|
||
<h3>查询功能的架构设计</h3>
|
||
|
||
<p>接着,是查询功能的技术中台设计,如图所示:</p>
|
||
|
||
<p><img src="assets/Ciqc1F_kdKOAU7-vAAUEADk6Fzw219.png" alt="Drawing 2.png" /></p>
|
||
|
||
<p>查询功能的技术中台架构设计</p>
|
||
|
||
<p>与增删改部分一样的是,查询功能中,每个功能的前端 UI 也是统一调用一个 Controller。但与增删改的部分不一样的是,查询功能的前端 UI 传递的参数不同,因此是另一个类 QueryController。</p>
|
||
|
||
<p>在调用时,首先需要传递的还是 bean。但与增删改不同的是,查询功能的 Service 只有一个,那就是 QueryService。但是,该 Service 在 Spring 中配置的时候,往 Service 中注入的是不同的 Dao,就可以装配成各种不同的 bean。这样,前端调用的是不同的 bean,最后执行的就是不同的查询。</p>
|
||
|
||
<p>此外,与增删改不同的是,查询部分不需要传递 method 参数,因为每次查询调用的方法都是query()。最后,前端还要以 JSON 的形式传递各种查询参数,就能进行后端查询了。</p>
|
||
|
||
<h4>单 Controller 的设计</h4>
|
||
|
||
<p>在进行查询时,前端输入 HTTP 请求:</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>http://localhost:9003/query/{bean}</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>例如:</p>
|
||
|
||
<blockquote>
|
||
|
||
<p><a href="http://localhost:9003/query/customerQry?gender=male&page=1&size=30">http://localhost:9003/query/customerQry?gender=male&page=1&size=30</a></p>
|
||
|
||
</blockquote>
|
||
|
||
<p>该方法既可以接收 GET 请求,也可以接收 POST 请求。{bean} 是配置在 Spring 中的Service。QueryController 通过该请求,在 Spring 中找到 Service,并调用Service.query(map) 进行查询,此处的 map 就是该请求传递的所有查询参数。</p>
|
||
|
||
<p>为此,查询部分的单 Controller 的流程设计如下:</p>
|
||
|
||
<ul>
|
||
|
||
<li>从前端获得 bean、page、size、count,以及查询参数;</li>
|
||
|
||
<li>根据 bean 从 Spring 中获得相应的 Serivce;</li>
|
||
|
||
<li>从前端获得查询参数 JSON,将其转换为 Map;</li>
|
||
|
||
<li>执行 service.query(map);</li>
|
||
|
||
<li>执行完查询后,以不同形式返回给前端。</li>
|
||
|
||
</ul>
|
||
|
||
<h4><strong>单 Service 的设计</strong></h4>
|
||
|
||
<p>查询的部分采用了单 Service 的设计,即所有的查询都是<strong>配置的 QueryService 进行查询</strong>,但注入的是不同的 Dao,就可以配置成不同的 bean,完成各自不同的查询。为了设计更加简化,每个 Dao 可以通过 MyBatis 框架,注入同一个 Dao,但配置不同的 Mapper,就可以完成不同的查询。因此,先配置 MyBatis 的 Mapper 文件:</p>
|
||
|
||
<pre><code><?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.demo2.customer.query.dao.CustomerMapper">
|
||
|
||
|
||
|
||
<!--筛选条件-->
|
||
|
||
|
||
|
||
<sql id="searchParam">
|
||
|
||
|
||
|
||
<if test="id != '' and id != null">
|
||
|
||
|
||
|
||
and id = #{id}
|
||
|
||
|
||
|
||
</if>
|
||
|
||
|
||
|
||
</sql>
|
||
|
||
<!--求count判断-->
|
||
|
||
|
||
|
||
<sql id="isCount1">
|
||
|
||
|
||
|
||
<if test="count == null and notCount ==1">
|
||
|
||
|
||
|
||
select count(*) from (
|
||
|
||
|
||
|
||
</if>
|
||
|
||
|
||
|
||
</sql>
|
||
|
||
|
||
|
||
<sql id="isCount2">
|
||
|
||
|
||
|
||
<if test="count == null and notCount ==1">
|
||
|
||
|
||
|
||
) count
|
||
|
||
|
||
|
||
</if>
|
||
|
||
|
||
|
||
</sql>
|
||
|
||
<!--是否分页判断-->
|
||
|
||
|
||
|
||
<sql id="isPage">
|
||
|
||
|
||
|
||
<if test="size != null and size !=''">
|
||
|
||
|
||
|
||
limit #{size} offset #{firstRow}
|
||
|
||
|
||
|
||
</if>
|
||
|
||
|
||
|
||
<if test="size ==null or size ==''">
|
||
|
||
|
||
|
||
<if test="pageSize != null and pageSize !=''">
|
||
|
||
|
||
|
||
limit #{pageSize} offset #{startNum}
|
||
|
||
|
||
|
||
</if>
|
||
|
||
|
||
|
||
</if>
|
||
|
||
|
||
|
||
</sql>
|
||
|
||
<select id="query" parameterType="java.util.HashMap" resultType="com.demo2.customer.entity.Customer">
|
||
|
||
|
||
|
||
<include refid="isCount1"/>
|
||
|
||
|
||
|
||
SELECT * FROM Customer WHERE 1 = 1
|
||
|
||
|
||
|
||
<include refid="searchParam"/>
|
||
|
||
|
||
|
||
<include refid="isPage"/>
|
||
|
||
|
||
|
||
<include refid="isCount2"/>
|
||
|
||
|
||
|
||
</select>
|
||
|
||
|
||
|
||
</mapper>
|
||
|
||
</code></pre>
|
||
|
||
<p>然后,将其注入 Spring 中,完成相应的配置,就可以进行查询了:</p>
|
||
|
||
<pre><code><?xml version="1.0" encoding="UTF-8"?>
|
||
|
||
|
||
|
||
<beans xmlns="http://www.springframework.org/schema/beans" ...>
|
||
|
||
|
||
|
||
<description>The application context for query</description>
|
||
|
||
|
||
|
||
<bean id="customerQry" class="com.demo2.support.service.impl.QueryServiceImpl">
|
||
|
||
|
||
|
||
<property name="queryDao">
|
||
|
||
|
||
|
||
<bean class="com.demo2.support.dao.impl.QueryDaoMybatisImpl">
|
||
|
||
|
||
|
||
<property name="sqlMapper" value="com.demo2.customer.query.dao.CustomerMapper.query"></property>
|
||
|
||
|
||
|
||
</bean>
|
||
|
||
|
||
|
||
</property>
|
||
|
||
|
||
|
||
</bean>
|
||
|
||
|
||
|
||
</beans>
|
||
|
||
</code></pre>
|
||
|
||
<p>每个查询的 bean 都是配置的 QueryServiceImpl,但每个 bean 配置的是不同的 sqlMapper,就会执行不同的查询。这里的 sqlMapper 应当与前面 MyBatis 配置中的namespace 相对应。</p>
|
||
|
||
<p>这样,查询部分的单 Service 流程设计如下:</p>
|
||
|
||
<ul>
|
||
|
||
<li>将查询参数 map、 page、size 传递给 Dao,执行查询 dao.query(map);</li>
|
||
|
||
<li>在查询的前后增加空方法 beforeQuery()、afterQuery() 作为 hook,当某业务需要在查询前后进行处理时,通过重载子类去实现;</li>
|
||
|
||
<li>判断前端是否传递 count,如果有则不再求和,否则调用 dao.count() 求和计算“第 x 页,共 y 页”;</li>
|
||
|
||
<li>将数据打包成 ResultSet 对象返回。</li>
|
||
|
||
</ul>
|
||
|
||
<p>通常,在执行查询时,只需要执行 dao.query(map) 就可以了。由于不同的 bean 注入的 Dao不同,因此执行 dao.query(map) 就会执行不同的查询。但是,在某些业务中,需要个性地在查询前进行某些处理,如对查询参数进行某些转换,或者在查询后对查询结果进行某些转换与补填。现在的设计中只有一个 Service,如何实现查询前后的这些处理呢?</p>
|
||
|
||
<p>首先,在 QueryService 中增加了 beforeQuery() 和 afterQuery() 两个方法,但这两个方法在QueryService 中设计成空方法,什么都没写,因此调用它们就跟没有调用一样。这样的设计叫“钩子 hook”,如下代码所示:</p>
|
||
|
||
<pre><code> /**
|
||
|
||
|
||
|
||
* do something before query.
|
||
|
||
|
||
|
||
* It just a hook that override the function in subclass if we need do something before query.
|
||
|
||
|
||
|
||
* @param params the parameters the query need
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
protected void beforeQuery(Map<String, Object> params) {
|
||
|
||
|
||
|
||
//just a hood
|
||
|
||
|
||
|
||
}
|
||
|
||
/**
|
||
|
||
|
||
|
||
* do something after query.
|
||
|
||
|
||
|
||
* It just a hook that override the function in subclass if we need do something after query.
|
||
|
||
|
||
|
||
* @param params the parameters the query need
|
||
|
||
|
||
|
||
* @param resultSet the result set after query.
|
||
|
||
|
||
|
||
* @return
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
protected ResultSet afterQuery(Map<String, Object> params, ResultSet resultSet) {
|
||
|
||
|
||
|
||
//just a hood
|
||
|
||
|
||
|
||
return resultSet;
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这样,如果不需要在查询前后添加处理,直接配置 QueryService 就行了。在执行查询时,就像没有这两个方法一样。然而,如果需要在查询前或查询后添加某些处理时,则通过继承编写一个 QueryService 的子类,并重写 beforeQuery() 或 afterQuery()。在 Spring 配置时配置的是这个子类,就实现了查询前后的处理。</p>
|
||
|
||
<p>譬如,ProductQuery 这个查询需要在查询后,对查询结果集补填 Supplier。这时,通过继承编写一个子类 ProductQueryServiceImpl,重写 afterQuery()。</p>
|
||
|
||
<pre><code>public class ProductQueryServiceImpl extends QueryServiceImpl {
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private SupplierService supplierService;
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
protected ResultSet afterQuery(Map<String, Object> params,
|
||
|
||
|
||
|
||
ResultSet resultSet) {
|
||
|
||
|
||
|
||
@SuppressWarnings("unchecked")
|
||
|
||
|
||
|
||
List<Product> list = (List<Product>)resultSet.getData();
|
||
|
||
|
||
|
||
for(Product product : list) {
|
||
|
||
|
||
|
||
String supplierId = product.getSupplierId();
|
||
|
||
|
||
|
||
Supplier supplier = supplierService.loadSupplier(supplierId);
|
||
|
||
|
||
|
||
product.setSupplier(supplier);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
resultSet.setData(list);
|
||
|
||
|
||
|
||
return resultSet;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>最后,将查询结果以 ResultSet 值对象的形式返回给 Controller,Controller 再返回给前端。在这个 ResultSet 中:</p>
|
||
|
||
<ul>
|
||
|
||
<li>属性 data 是这一页的查询结果集;</li>
|
||
|
||
<li>page、size 是分页信息;</li>
|
||
|
||
<li>count 是记录总数。</li>
|
||
|
||
</ul>
|
||
|
||
<p>通过这 3 个值就可以在前端显示“第 x 页,共 y 页,z 条记录”。在第一次查询时,除了查询这一页的数据,还要执行 count。将该 count 记录下来后,在进行分页查询时,就不再需要执行 count,从而有效提高查询性能。</p>
|
||
|
||
<p>属性 aggregate 是一个 map,如果该查询在前端展现时,需要在表格的最下方对某些字段进行汇总,并且这个汇总是对整个查询结果的汇总,而不是这一页的汇总,则将该字段作为 Key 值写入 aggregate 中,Value 是汇总的方式,如 count、sum、max 等。通过这样的设置,就可以在查询结果集的最后一行返回一个汇总记录。</p>
|
||
|
||
<p>通过以上技术中台的设计,各查询功能的编码就会极大地简化。具体来说,设计一个普通的查询,只需要制作一个 MyBatis 的查询语句配置,在 Spring 配置中制作一个 bean。然后,就可以通过前端进行查询了,甚至都不需要编写任何 class。只有在查询前后添加操作时,才需要自己制作一个子类。</p>
|
||
|
||
<p>此外,对于进行查询结果集的补填,也可以使用通用程序 AutofillQueryServiceImpl,在下一讲“如何设计支持领域驱动的技术中台”中会详细讲解。</p>
|
||
|
||
<h3>总结</h3>
|
||
|
||
<p>本讲讲解了一个强大而落地的技术中台设计实践。通过该技术中台的封装:</p>
|
||
|
||
<ul>
|
||
|
||
<li>在增删改操作时,只需编写前端界面、Service 与值对象就可以了,更多技术细节被封装起来了,这样,开发人员就可以专心地将领域模型转换成业务代码的设计实现,并随着领域模型的变更而变更代码,不断满足用户需求;</li>
|
||
|
||
<li>在查询操作时,在大多数情况下只需要编写 MyBatis 与 Spring 的配置就可以完成查询功能的编写,开发工作量大大降低,同时变更也变得轻松快捷。</li>
|
||
|
||
</ul>
|
||
|
||
<p>以上技术中台的设计是普通技术中台的设计,那么支持 DDD 的技术中台又该如何设计呢?是不是可以编写通用的仓库与工厂呢?下一讲将为你讲解。</p>
|
||
|
||
<p><a href="https://github.com/mooodo/demo-service2-support">点击 GitHub 链接</a>,查看源码。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/12 如何设计支持快速交付的技术中台战略?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/DDD 微服务落地实战/14 如何设计支持 DDD 的技术中台?.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70996ec7083f3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|