<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>树下魅狐的博客</title>
            <link>https://www.ramostear.com</link>
        <generator>Halo 1.4.17</generator>
        <lastBuildDate>Fri, 08 Apr 2022 14:23:32 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[MyBatisPlus 一对多分页异常处理及性能优化]]>
                    </title>
                    <link>https://www.ramostear.com/2022/04/mybatis-pagination-problem.html</link>
                    <description>
                            <![CDATA[<p>场景描述：</p><ul><li>现有三张表：账户表(t_account)，账户-角色关联表(account_role_link)和角色表(t_role)</li><li>对角色表进行分页查询并附带查询条件</li><li>查询结果需要关联角色对应的账户数据</li></ul><h2 id="1异常情况">1.异常情况</h2><p>角色和用户存在一对多关系，可以使用collection对多个账户数据进行处理。对角色数据的封装如下(Java)：</p><pre><code class="language-java">public class RoleVO {  private String id;  private String name;  private String code;  private List&lt;UserVO&gt; users;  //此处省略getters&amp;setters}</code></pre><pre><code class="language-java">public class UserVO {  private String id;  private String username;  //此处省略getters&amp;setters}</code></pre><p>RoleMapper.xml中的查询语句如下：</p><pre><code class="language-xml">&lt;resultMap id=&quot;UserVOMap&quot; type=&quot;com.ramostear.console.domain.vo.UserVO&quot;&gt;  &lt;id property=&quot;id&quot; column=&quot;uid&quot;/&gt;  &lt;result property=&quot;username&quot; column=&quot;username&quot;/&gt;&lt;/resultMap&gt;&lt;resultMap id=&quot;RoleVOMap&quot; type=&quot;com.ramostear.console.domain.vo.RoleVO&quot;&gt;  &lt;id property=&quot;id&quot; column=&quot;id&quot;/&gt;  &lt;result property=&quot;name&quot; column=&quot;name&quot;/&gt;  &lt;result property=&quot;code&quot; column=&quot;code&quot;/&gt;  &lt;result property=&quot;status&quot; column=&quot;status&quot;/&gt;  &lt;result property=&quot;remark&quot; column=&quot;remark&quot;/&gt;  &lt;result property=&quot;createTime&quot; column=&quot;create_time&quot;/&gt;  &lt;result property=&quot;modifyTime&quot; column=&quot;modify_time&quot;/&gt;  &lt;collection property=&quot;users&quot; resultMap=&quot;UserVOMap&quot;/&gt;&lt;/resultMap&gt;&lt;select id=&quot;queryPage&quot; resultMap=&quot;RoleVOMap&quot;&gt;  SELECT t_0.*,t_2.id AS uid,t_2.username  FROM t_role t_0  LEFT JOIN account_role_link t_1 ON t_0.id = t_1.role_id  LEFT join t_account t_2 ON t_2.id=t_1.account_id  &lt;where&gt;    &lt;if test=&quot;keyword != null and keyword != ''&quot;&gt;      AND t_0.name LIKE CONCAT('%',#{keyword}, '%')    &lt;/if&gt;    &lt;if test=&quot;status != null&quot;&gt;      AND t_0.status=#{status}    &lt;/if&gt;  &lt;/where&gt;  ORDER BY t_0.create_time DESC,t_0.modify_time DESC&lt;/select&gt;</code></pre><p>当前角色表中有13条数据，分页查询时，每页展示10条数据，执行查询，结果如下(JSON):</p><p><img src="https://cdn.ramostear.com/image-20220408121343916_1649398610604.png" alt="image20220408121343916.png" /></p><p>正常情况下，查询的结果中应该是10条角色数据，但此时只有8条(和预期结果有出入)。再观察控制台输入的SQL语句：</p><p><img src="https://cdn.ramostear.com/image-20220408121821070_1649398628558.png" alt="image20220408121821070.png" /></p><p>角色数据总数：13条，分页查询条数：10条，从查询结果数量上看，SQL没有问题。但黄色框中的数据会有一个小坑。</p><p>上述的SQL写法，是对角色和账户两张表JOIN后的结果集进行了分页(10条，其中有三条角色数据是重复的)，而我们最开始的需求是对角色数据进行分页查询并带上角色对应的账户数据。Mapper中的collection在处理结果集时，会对黄色框中的数据进行合并收集（一对多处理），在进行实体对象映射时，MyBatisPlus将三条角色重复而用户不同的数据合并为一个RoleVO对象实例，这就导致了最终拿到的查询结果只有8条数据。</p><p>导致这个问题，是我们把原先对角色数据进行分页的需求，变成了对角色和账户JOIN后的数据进行分页，且在返回最终结果前，MyBatis 的collection又把数据进行了“合并”。</p><blockquote><p>解决该问题的重点就是：对角色分页！对角色分页！对角色分页！</p></blockquote><h2 id="2使用子查询">2.使用子查询</h2><p>找到问题的所在，我们不应该对JOIN后的结果进行分页处理，而是先对角色数据进行分页处理，然后再处理角色和账户的一对多映射。</p><p>使用MyBatis提供的子查询，主表(t_role)查询不参杂对t_acctount表的处理，t_role表分页查询处理完成后，传递ro leId到子查询中，对关联的账户数据进行查找。</p><pre><code class="language-xml"># ============================ 改造后的ResultMap =========================&lt;resultMap id=&quot;RoleVOMapBySubQuery&quot; type=&quot;cn.zysmartcity.console.domain.vo.RoleVO&quot;&gt;  &lt;id property=&quot;id&quot; column=&quot;id&quot;/&gt;  &lt;result property=&quot;name&quot; column=&quot;name&quot;/&gt;  &lt;result property=&quot;code&quot; column=&quot;code&quot;/&gt;  &lt;result property=&quot;status&quot; column=&quot;status&quot;/&gt;  &lt;result property=&quot;remark&quot; column=&quot;remark&quot;/&gt;  &lt;result property=&quot;createTime&quot; column=&quot;create_time&quot;/&gt;  &lt;result property=&quot;modifyTime&quot; column=&quot;modify_time&quot;/&gt;  &lt;collection property=&quot;users&quot; ofType=&quot;com.ramostear.console.domain.vo.UserVO&quot; column=&quot;roleId&quot; select=&quot;queryUserByRole&quot;&gt;    &lt;id column=&quot;uid&quot; property=&quot;id&quot; jdbcType=&quot;VARCHAR&quot;/&gt;    &lt;result column=&quot;username&quot; property=&quot;username&quot; jdbcType=&quot;VARCHAR&quot;/&gt;  &lt;/collection&gt;&lt;/resultMap&gt;# ============================ 主表的查询语句 =========================&lt;select id=&quot;queryPageBySubQuery&quot; resultMap=&quot;RoleVOMapBySubQuery&quot;&gt;  SELECT role.*,role.id as roleId FROM t_role role  &lt;where&gt;    &lt;if test=&quot;keyword != null and keyword != ''&quot;&gt;      AND role.name LIKE CONCAT('%',#{keyword}, '%')    &lt;/if&gt;    &lt;if test=&quot;status != null&quot;&gt;      AND role.status=#{status}    &lt;/if&gt;  &lt;/where&gt;  ORDER BY role.create_time,role.modify_time DESC&lt;/select&gt;# ============================ 子表的查询语句 =========================&lt;select id=&quot;queryUserByRole&quot; resultMap=&quot;UserVOMap&quot;&gt;  SELECT t_1.id AS uid,t_1.username FROM account_role_link t_0  LEFT JOIN t_account t_1 ON t_1.id=t_0.account_id  WHERE t_0.role_id=#{roleId}&lt;/select&gt;</code></pre><blockquote><p>主表查询中role.id as roleId既为ResultMap中定义的需要传递到子查询queryUserByRole的roleId参数</p></blockquote><p>执行SQL查询语句，观察返回的结果集：</p><p><img src="https://cdn.ramostear.com/image-20220408125755946_1649398655321.png" alt="image20220408125755946.png" /></p><p>数据总数：13条，当前查询条数：10条，分页异常的问题得以解决；但是，问题还有完全解决，我们先看控制台输出的SQL语句：</p><p><img src="https://cdn.ramostear.com/image-20220408130153434_1649398666764.png" alt="image20220408130153434.png" /></p><p>原本只需要发2次SQL语句(统计总数和分页)的查询，现在变成了发N+1条SQL语句(N为分页大小)，再看执行的时间</p><p><img src="https://cdn.ramostear.com/image-20220408130654336_1649398680952.png" alt="image20220408130654336.png" /></p><p>13条数据分页查询，耗时66毫秒，如果数据更多呢？每页显示数据量更大呢？子查询的这种方式显然是不太合理的。</p><h2 id="3自定义分页">3.自定义分页</h2><blockquote><p>解决思路：先对角色表进行分页查询，得到的结果集再和账户进行JOIN操作。</p></blockquote><p>去掉ResultMap中colletion的column和select属性，保持和最开始的ResultMap一致：</p><pre><code class="language-xml">&lt;resultMap id=&quot;UserVOMap&quot; type=&quot;com.ramostear.console.domain.vo.UserVO&quot;&gt;        &lt;id property=&quot;id&quot; column=&quot;uid&quot;/&gt;        &lt;result property=&quot;username&quot; column=&quot;username&quot;/&gt;    &lt;/resultMap&gt;    &lt;resultMap id=&quot;RoleVOMap&quot; type=&quot;com.ramostear.console.domain.vo.RoleVO&quot;&gt;        &lt;id property=&quot;id&quot; column=&quot;id&quot;/&gt;        &lt;result property=&quot;name&quot; column=&quot;name&quot;/&gt;        &lt;result property=&quot;code&quot; column=&quot;code&quot;/&gt;        &lt;result property=&quot;status&quot; column=&quot;status&quot;/&gt;        &lt;result property=&quot;remark&quot; column=&quot;remark&quot;/&gt;        &lt;result property=&quot;createTime&quot; column=&quot;create_time&quot;/&gt;        &lt;result property=&quot;modifyTime&quot; column=&quot;modify_time&quot;/&gt;        &lt;collection property=&quot;users&quot; resultMap=&quot;UserVOMap&quot;/&gt;    &lt;/resultMap&gt;</code></pre><p>调整查询语句如下：</p><pre><code class="language-xml">&lt;select id=&quot;queryPageRecords&quot; resultMap=&quot;RoleVOMap&quot;&gt;  SELECT t_0.*,t_2.id AS uid,t_2.username  FROM (    # ===========处理角色的分页==============    SELECT t.id,    t.name,    t.code,    t.status,    t.remark,    t.create_time,    t.modify_time from t_role t    &lt;where&gt;      &lt;if test=&quot;keyword != null and keyword != ''&quot;&gt;        AND t.name LIKE CONCAT('%',#{keyword}, '%')      &lt;/if&gt;      &lt;if test=&quot;status != null&quot;&gt;        AND t.status=#{status}      &lt;/if&gt;    &lt;/where&gt;    ORDER BY t.create_time,t.modify_time DESC    LIMIT #{offset}, #{size}  ) t_0 # =======角色分页后的结果集，在同账户进行JOIN==========  left join account_role_link t_1 on t_0.id = t_1.role_id  left join t_account t_2 on t_2.id=t_1.account_id&lt;/select&gt;</code></pre><blockquote><p>offset和size是上层（controller -&gt; service）传递的分页参数，可以从Page<T>中获取到</p></blockquote><p>改造后，我们还需要手动去统计一下角色表的数据总量（查询条件需要和分页查询中保持一致），最终改造如下：</p><p>RoleService.java</p><pre><code class="language-java">public class RoleService {  //此处省略其他代码....  public Page&lt;RoleVO&gt; queryPage(String keyword,int status,Page&lt;RoleVO&gt; page) {    //执行分页查询    List&lt;RoleVO&gt; records = baseMapper.queryPageRecords(keyword, status, page.offset(), page.getSize());          //统计数据总数      LambdaQueryWrapper&lt;Role&gt; queryWrapper = new LambdaQueryWrapper&lt;Role&gt;()                .eq(status != null, Role::getStatus, status)                .like(StringUtils.isNotEmpty(keyword), Role::getName, keyword);      long total = this.count(queryWrapper);        //设置分页数据      page.setRecords(records);      page.setTotal(total);    return page;  }}</code></pre><p>执行SQL查询语句，观察返回的结果集：</p><p><img src="https://cdn.ramostear.com/image-20220408140200116_1649398701338.png" alt="image20220408140200116.png" /></p><p>数据总数：13条，当前查询条数：10条，返回数据正常，再观察控制台SQL日志和消耗的查询时间：</p><p><img src="https://cdn.ramostear.com/image-20220408140424012_1649398712400.png" alt="image20220408140424012.png" /></p><p>共发送了2次SQL语句，查询结果集为13条数据（3条角色重复，一对多合并后10条数据），耗时 8毫秒，相较之前的66毫秒，速度提升了不少，且减少了发SQL的次数。</p>]]>
                    </description>
                    <pubDate>Fri, 08 Apr 2022 14:20:35 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[IntelliJ IDEA中运行Nacos官方源码]]>
                    </title>
                    <link>https://www.ramostear.com/2022/03/run-nacos-in-idea.html</link>
                    <description>
                            <![CDATA[<h1 id="intellij-idea中运行nacos源码">IntelliJ IDEA中运行Nacos源码</h1><h2 id="1获取源码">1.获取源码</h2><p>访问<a href="https://github.com">Github</a>官网，进入Nacos源代码主页：<a href="https://github.com/alibaba/nacos">https://github.com/alibaba/nacos</a>,可以通过Git工具下载Nacos源码，或者直接点击<code>Download</code>按钮下载源码。</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2012.38.24_1646293323424.png" alt="截屏20220303 12.38.24.png" /></p><p>下载Nacos源码：</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2012.42.08_1646293347861.png" alt="截屏20220303 12.42.08.png" /></p><h2 id="2使用intellij-idea打开源码">2.使用IntelliJ IDEA打开源码</h2><p>打开IntelliJ IDEA工具，导入刚刚下载到本地的源代码。</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2012.46.54_1646293365581.png" alt="截屏20220303 12.46.54.png" /></p><h2 id="3创建数据库">3.创建数据库</h2><p>找到distribution/conf/nacos-mysql.sql文件，并复制SQL语句，然后创建数据库并执行所复制的SQL语句。数据库名称为<code>nacos</code>,用户名：<code>nacos</code>，密码：<code>nacos</code>;</p><blockquote><p>你可以根据实际情况，重新定义数据库的名称，用户名和密码</p></blockquote><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2012.56.00_1646293384448.png" alt="截屏20220303 12.56.00.png" /></p><p>数据库及数据表</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2012.58.35_1646293420194.png" alt="截屏20220303 12.58.35.png" /></p><h2 id="4修改配置">4.修改配置</h2><p>打开console/src/main/resource/application.properties文件，修改数据库的相关配置（默认配置是被注释掉的）。</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2013.04.05_1646293437009.png" alt="截屏20220303 13.04.05.png" /></p><p>打开注释，配置本地的MySQL数据库。</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2013.11.54_1646293475996.png" alt="截屏20220303 13.11.54.png" /></p><p>配置项如下：</p><pre><code class="language-properties">#*************** Config Module Related Configurations ***************#### If use MySQL as datasource:spring.datasource.platform=mysql### Count of DB:db.num=1### Connect URL of DB:db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useUnicode=true&amp;useSSL=false&amp;serverTimezone=UTCdb.user.0=nacosdb.password.0=nacos</code></pre><h2 id="5配置启动参数">5.配置启动参数</h2><p>选择console模块，点击<code>Add Configuration...</code> 按钮，进入配置面板，点击<code>Add new run configuration..</code>配置启动参数。配置参数如下：</p><pre><code class="language-tex">-Dnacos.standalone=true -Dspring.datasource.platform=mysql</code></pre><p>找到<code>Add Configuration...</code>按钮</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2013.20.05_1646293500693.png" alt="截屏20220303 13.20.05.png" /></p><p>添加新配置</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2013.26.05_1646293513814.png" alt="截屏20220303 13.26.05.png" /></p><p>选择SpringBoot启动项</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2013.28.52_1646293526600.png" alt="截屏20220303 13.28.52.png" /></p><p>选择入口类（启动类）</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2013.30.28_1646293542986.png" alt="截屏20220303 13.30.28.png" /></p><p>配置环境变量</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2013.31.15_1646293555157.png" alt="截屏20220303 13.31.15.png" /></p><p>点击<code>OK</code>按钮完成配置。</p><h2 id="6安装protobuf插件">6.安装Protobuf插件</h2><p>Nacos源码中，使用了Protobuf对项目源码进行编译，如果你的IDEA中没有安装Protobuf插件，项目在编译时会暴类找不到的异常信息，例如：</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2014.19.02_1646293573167.png" alt="截屏20220303 14.19.02.png" /></p><p>该问题可以借助IDEA中的Protobuf插件来解决。打开Preferences/Plugins/Marketplace,搜索protobuf并安装，安装成功后重启IDEA。重新使用protobuf插件对相关的模块进行编译。</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2014.27.40_1646293585423.png" alt="截屏20220303 14.27.40.png" /></p><p>如果不想安装插件，可以直接全局install项目，然后再启动。</p><h2 id="7启动项目">7.启动项目</h2><p>点击启动按钮，启动项目</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2014.50.11_1646293601608.png" alt="截屏20220303 14.50.11.png" /></p><p>启动成功后，控制台有如下输出</p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2014.52.40_1646293613708.png" alt="截屏20220303 14.52.40.png" /></p><p>打开浏览器，在地址栏输入“<a href="http://localhost:8848/nacos”即可进入nacos控制台。">http://localhost:8848/nacos”即可进入nacos控制台。</a></p><p><img src="https://cdn.ramostear.com/%E6%88%AA%E5%B1%8F2022-03-03%2014.55.55_1646293625443.png" alt="截屏20220303 14.55.55.png" /></p><blockquote><p>Nacos 默认账号：nacos,密码：nacos</p></blockquote><h2 id="8其他">8.其他</h2><p>其他配置参数，请参考Nacos官方文档：<a href="https://nacos.io/zh-cn/docs/deployment.html">https://nacos.io/zh-cn/docs/deployment.html</a></p>]]>
                    </description>
                    <pubDate>Thu, 03 Mar 2022 15:49:29 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[关于设计模式（About Design Pattern）]]>
                    </title>
                    <link>https://www.ramostear.com/2022/03/aboutdesignpattern.html</link>
                    <description>
                            <![CDATA[<h1 id="关于设计模式about-design-pattern">关于设计模式（About Design Pattern）</h1><h2 id="一-什么是设计模式">一、 什么是设计模式</h2><p>设计模式不是我们熟知的某种框架，也不是某种具体的实现，而是作为编程问题的解决方案（指导思想）而存在。它描述了众多有经验的开发人员以往多次遇到问题的通用解决办法。</p><h2 id="二设计模式分类">二、设计模式分类</h2><p>从类型上看，设计模式可以分为三大类：生成模式（Creational Patterns）、结构模式（Structural Patterns）和行为模式（Behavioral Pattern）。</p><h3 id="21-生成模式creational-patterns">2.1 生成模式（Creational Patterns）</h3><p>生成类包含以下几种设计模式：</p><ul><li>抽象工厂模式-Abstract Factory Pattern: 产品有多种类别，N个工厂类把工厂抽象出来（不一定是一个工厂只负责生产一类产品，看实际需求）</li><li>建造者模式-Builder Pattern: 产品与产品建造流程进行解耦，用简单方式生产出复杂的产品，不用深入了解产品构造，只是简单的配置就好。</li><li>工厂方法模式-Factory Method Pattern: 将同一类产品分为不同的部分，每个工厂负责生产其中的一部分。</li><li>原型模式-Prototype Pattern: 可以直接使用Java自带的clone进行浅拷贝，也可以用Java反序列化，进行深拷贝。</li><li>单例模式-Singleton Pattern: 在整个系统中，有且只有一个单体的对象实例。</li></ul><h3 id="22结构模式structural-patterns">2.2结构模式（Structural Patterns）</h3><p>结构类包含以下几种设计模式：</p><ul><li>适配器模式-Adapter Pattern：将不能直接使用或者使用复杂的东西，变成能直接使用或使用方便的东西，例如：电源适配器，将220V电转换为200V电。</li><li>桥接模式-Bridge Pattern：抽象（定义）和实现分离。</li><li>组合模式-Composite Pattern：用于把一组相似的对象当作一个单一的对象，组合模式依据树形结构来组合对象，用来表示部分与整体层次。</li><li>装饰者模式-Decorator Pattern：动态的将新功能附加到对象上。</li><li>外观模式-Facade Pattern：为子系统中的一组接口提供统一的界面，这个接口使得子系统更容易使用。</li><li>享元模式-Flyweight Pattern：对象复用（常量池，连接池），重点在于复用而不是内部状态。</li><li>代理模式-Proxy Pattern：代理对象可以在客户端和目标对象之间起到中介作用，对目标对象起到一定的保护。</li></ul><h3 id="23-行为模式behavioral-patterns">2.3 行为模式（Behavioral Patterns）</h3><p>行为类包含以下几种设计模式：</p><ul><li>责任链模式-Chain of Responsibility Pattern：如果一个对象不能处理当前请求，则将请求转发（传递）给下一个接收者。</li><li>命令模式-Command Pattern：将一个请求封装为一个对象，使发出请求的责任和执行请求的责任分离。</li><li>解释器模式-Interpreter Pattern：实现一个表达式接口，用于解释一个特定的上下文。</li><li>迭代器模式-Iterator Pattern：提供一种方法，顺序访问一个聚合对象中各个元素，且无需暴露该对象的内部结构。</li><li>中介模式-Mediator Pattern：降低多个对象和类之间的通信复杂度。</li><li>备忘录模式-Memento Pattern：在不破坏封装的前提下，捕获一个对象的内部状态，并在该对象之外保存这个状态。</li><li>观察者模式-Observer Pattern：当一个对象的状态发生改变时，会自动通知依赖它的对象做出反应。</li><li>状态模式-State Pattern：允许对象在内部状态发生改变时改变它的行为。</li><li>策略模式-Strategy Pattern：利用面向对象的多态特点，引用的是抽象类，当实际调用的时候，是被调用者是该对象的实体子类。</li><li>模版方法模式-Template Methods Pattern：定义一个操作中的算法骨架，将一些步骤延迟到子类中。</li><li>访问者模式-Visitor Pattern：为数据结构中的每个元素提供多种访问方式。</li></ul><h2 id="三设计模式全图">三、设计模式全图</h2><p>23种设计模式分类归纳如下：</p><p><img src="https://cdn.ramostear.com/%E8%B5%84%E6%BA%90%201_1646222060882.png" alt="资源 1.png" /></p>]]>
                    </description>
                    <pubDate>Wed, 02 Mar 2022 19:55:39 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[DDD-CQRS & Event Sourcing]]>
                    </title>
                    <link>https://www.ramostear.com/2022/02/ddd-cqrseventsourcing.html</link>
                    <description>
                            <![CDATA[<h1 id="ddd-cqrs--event-sourcing">DDD-CQRS &amp; Event Sourcing</h1><p>现在，每个开发人员都很熟悉MVC标准体系结构设计模式。大多数的应用程序都是基于这种体系结构进行创建的。它允许我们创建可扩展的大型企业应用程序，但近期我们还听到了另外的一些有关于CQRS/ES的相关信息。这些方法应该被放在MVC中一起使用吗？他们可以解决什么问题？现在，让我们一起来看看CQRS/ES是什么，以及他们都有哪些优点和缺点。</p><h1 id="cqrs--模式介绍">CQRS — 模式介绍</h1><p>CQRS（Command Query Responsibility Segregation）是一种简单的设计模式。它衍生与CQS，即命令和查询分离，CQS是由Bertrand Meyer所设计。按照这一设计概念，系统中的方法应该分为两种：改变状态的命令和返回值的查询。Greg young将引入了这个设计概念，并将其应用于对象或者组件当中，这就是今天所要将的CQRS。它背后的主要思想是应用程序更改对象或组件状态（Command）应该与获取对象或者组件信息（Query）分开。</p><p>下面，将通一张图来说明应用程序中有关CQRS部分的组成结构：</p><p><img src="https://cdn.ramostear.com/2019-04-20-18-57-35-b059113aecb444a0a795159d99e5a662.jpg" alt="CQRS模式介绍" title="CQRS模式介绍" /></p><p><strong>Commands（命令）</strong>—表示用户的操作意图。它们包含了与用户将要对系统执行操作的所有必要信息。</p><ul><li><strong>Command Bus（命令总线）</strong>：是一种接收命令并将命令传递给命令处理程序的队列。</li><li><strong>Command Handler（命令处理程序）</strong>：包含实际的业务逻辑，用于验证和处理命令中接收到的数据。Command handler负责生成和传播域事件（Event）到事件总线（Event Bus）。</li><li><strong>Event Bus（事件总线）</strong>：将事件发布给订阅特定事件类型的事件处理程序。如果存在连续的事件依赖，事件总线可以使用异步或者同步的方式将事件发布出去。</li><li><strong>Event Handler（事件处理程序）</strong>：负责处理特定类型的事件。它们的职责是将应用程序的最新状态保存到读库中，并执行终端的相关操作，如发送电子邮件，存储文件等。</li></ul><p><strong>Query（查询）</strong>：表示用户实际可用的应用程序状态。获取UI的数据应该通过这些对象完成。</p><p>下面我们将介绍有关CQRS的诸多优点，它们是：</p><ul><li>我们可以给处理业务逻辑部分和处理查询部分的开发人员分别分配任务，但需要小心的是，这种模式可能会破坏信息的完整性。</li><li>通过在多个不同的服务器上扩展Commands和Query，我们可以进一步提升应用程序的读/写性能。</li><li>使用两个不同的数据库（读库/写库）进行同步，可以实现自动备份，无需额外的干预工作。</li><li>读取数据时不会涉及到写库的操作，因此在使用事件源是读数据操作会更快。</li><li>我们可以直接为视图层构建数据，而无需考虑域逻辑，这可以简化视图层的工作并提高性能。</li></ul><p>尽管使用<strong>CQRS模式</strong>具有上述诸多的优点，但是在使用前还需要慎重考虑。对于只具有简单域的简单项目，其UI模型与域模型紧密联系的，使用CQRS反而会增加项目的复杂度和冗余度，这无疑是过度的设计项目。此外，对于数据量较少或者性能要求较低的项目实施CQRS模式不会带来显著的性能提升。</p><h1 id="event-sourcing--案例研究">Event Sourcing — 案例研究</h1><p>有这样一个案例，我们想要检索任何一个域对象的历史状态数据，而且在任何时间都可以生成统计数据。我们想要检查上个月、上个季度或者过去任何时间的状态汇总。想要解决这个问题并不容易。我们可以在特定的时间范围内将额外的数据保存在数据库中，但这种方法也存在一些缺点。我们不知道范围应该是什么样子，以及未来统计数据需要哪些数据项。为了避免这些问题，我们可以每天为所有聚合创建快照，但它们同样会产生大量的冗余数据。</p><p>Event Sourcing（ES）似乎是目前解决这些问题的最佳方案。Event Sourcing允许我们将Aggregate（聚合）状态的每一个更改事件保存在Event Store的事件存储库中。通过Command Handler将事件写入到事件存储库中，并处理相关的逻辑。要创建Aggregate（聚合）对象的当前状态，我们需要运行创建预期域对象的所有事件并对其执行所有的更改。下面我们将通过一张图来说明这一架构设计方式：</p><p><img src="https://cdn.ramostear.com/2019-04-20-18-57-08-9f0cb17b814b41edb040eae69e444894.jpg" alt="event-sourcing" title="event-sourcing" /></p><p>下面我们将列举一些使用ES的优点：</p><ul><li><strong>时间穿梭机</strong>：可以及时重建特定聚合的状态。每个事件都包含一个时间戳。根据这些时间戳可以在特定的时间内运行事件或者停止事件。</li><li><strong>自动审计</strong>：我们不需要额外的工作就可以检查出在特定的时间范围内谁做了什么以及改变了什么。这和可以显示更改历史记录的系统日志不同，事件可以告知我们每次更改背后所对应的操作意图。</li><li><strong>易于引入纠正措施</strong>：当数据库中的数据发生错误时，我们可以将应用程序的状态回退到特定的时间点上，并重建当时的应用程序状态。</li><li><strong>易于调试</strong>：如果应用程序出现问题，我们可以将特定事件内的所有事件取出，并逐条的重建应用状态，以检查应用程序可能出现问题的地方。这样我们可以更快的找到问题，缩短调试所需的时间。</li></ul><h1 id="aggregates">Aggregates</h1><p>**Aggregate（聚合）**一词在本文中多次被提及，那它到底是什么意思？**Aggregate（聚合）**来自于领域驱动设计（DDD）的一个概念，它指的是始终保持一致状态的实体或者相关实体组。我们可以简单的理解为接收和处理Command（包含Command Handler）的一个边界，然后根据当前状态生成事件。在通常情况下，Aggregate root（聚合根）由一个域对象构成，但它可以由多个对象组成。我们还需要注意整个应用程序可以包含多个Aggregate（聚合），并且所有事件都存储在同一个存储库中。</p><h1 id="总结">总结</h1><p>CQRS/ES可以作为特定问题的解决方案。它可以在标准N层架构设计的应用程序的某些层中进行引入，它可以解决非标准问题，常规架构中我们所拿到的是最终状态，在很多情况下，固然当前状态很重要，但我们还需要知道当前状态是如何产生的。CQRS和ES两种概念应该一起使用吗？事实表明，并没有。我们想要统计任何时间范文内的域对象状态，而写库只能存储当前状态。引入CQRS并没能帮助我们解决这一问题。在下一章节中，我们将引入Axon框架，Axon框架时间了CQRS/ES，用于解决某些域对象的一些特定问题，尤其是收集历史统计数据。我们将阐述如何使用Axon框架实现CQRS/ES并实现与Spring Boot应用程的整合。</p><blockquote><p>作者：LukaszKucik ，译：谭朝红，原文：<a href="https://www.nexocode.com/blog/posts/cqrs-and-event-sourcing/">CQRS and Event Sourcing as an antidote for problems with retrieving application states</a></p></blockquote>]]>
                    </description>
                    <pubDate>Wed, 23 Feb 2022 15:58:07 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[使用Spring Boot和Axon实现CQRS&Event Sourcing]]>
                    </title>
                    <link>https://www.ramostear.com/2022/02/springbootaxoncqrseventsourcing.html</link>
                    <description>
                            <![CDATA[<p>尽管可以在不适用任何其他框架或库的情况下实现CQRS/ES，但我们还是建议使用已有的一些工具。这些工具可以简化开发过程，同时运行开发人员专注于业务逻辑的处理，避免重复的造轮子。在本节中，我们将选择Axon框架来实现CQRS/ES。</p><h1 id="什么是axon">什么是Axon?</h1><p>Axon是一个轻量级的Java开源框架，可以帮助构建你构建基于CQRS模式的可伸缩、可扩展和库维护的Java应用程序。它还可以帮助你准备Event Sourcing所需要的环境。Axon提供了所有重要构建模块的实现，如聚合、存储库，命令和时间总线。Axon可以让开发人员的工作更为轻松。</p><h1 id="为何选择axon">为何选择Axon?</h1><p>Axon让我们避免了负载的配置和对数据流的操作，我们可以专注于应用程序业务规则的定制，而不是创建样板代码。使用Axon可以获得如下的一些优势：</p><ul><li><strong>优化事件处理</strong>：我们应该注意到，事件的发布具有先后顺序。Axon框架保证了事件被执行的先后顺序。</li><li><strong>内置测试环境</strong>：Axon提供了一个测试工具集，允许在特定的时间上对系统进行测试，这使得单元测试更为容易。</li><li><strong>Spring Boot AutoConfiguration</strong>:在Spring Boot应用程序中配置Axon是一件非常简单的事情。只需要提供必要的依赖项，Axon将会自动配置一些基本组件。</li><li><strong>支持注解</strong>：Axon提供了注解支持，这使得我们的代码更清晰可读，我们可以使用这些注解轻松构建聚合和事件处理程序，而无需关心Axon特定的处理逻辑</li></ul><h1 id="spring-boot应用程序中快速配置axon">Spring Boot应用程序中快速配置Axon</h1><p>在默认情况下，Axon以经提供了对Spring Boot的集成支持。我们只需要通过一些简单的配置步骤就可以将Axon与Spring Boot整合在一起。</p><h2 id="步骤1">步骤1</h2><p>第一步是使用合适的项目构建工具在项目中配置Axon依赖项。以下是使用Gradle来配置Axon的方法：</p><pre><code class="language-groovy">dependencies{    compile(&quot;org.axonframework:axon-spring-boot-starter:3.2&quot;)    compile(&quot;org.axonframework:axon-mongo:3.2&quot;)    testCompile(&quot;org.axonframework:axon-test:32.&quot;)}</code></pre><p>第一个依赖项为我们提供了与Spring Boot集成的最基本的Axon所有必要组件，如命令中线，事件总线和聚合。第二个依赖项是为我们的聚合或事件配置库提供所需的基本环境。最后一个依赖项用于构建先关的测试环境。</p><h2 id="步骤2">步骤2</h2><p>根据需要配置Axon所需要的一些Spring Bean。比如EventHandlerConfiguration(负责控制事件处理程序行为的组件)，如果其中有一个事件执行失败，则终止处理后续的所有事件。当然这是非必须的，但是还是值得在应用程序中进行此配置，以防止系统中数据的不一致。配置代码如下：</p><pre><code class="language-java">@Configurationpublic class AxonConfig {private final EventHandlingConfiguration eventHandlingConfiguration;@Autowiredpublic AxonConfig(EventHandlingConfiguration eventHandlingConfiguration) {   this.eventHandlingConfiguration = eventHandlingConfiguration;}@PostConstructpublic void registerErrorHandling() {   eventHandlingConfiguration.configureListenerInvocationErrorHandler(configuration -&gt; (exception, event, listener) -&gt; {       String msg = String.format(               &quot;[EventHandling] Event handler failed when processing event with id %s. Aborting all further event handlers.&quot;,               event.getIdentifier());       log.error(msg, exception);       throw exception;   });}}</code></pre><p>这里的主要思想是创建一个额外的配置文件（使用**@Configuration<strong>注释的类）。该类的构造函数注入由Spring自身所管理的</strong>EventHandlingConfiguration<strong>依赖项。由于绑定依赖，我们可以在此对象上调用</strong>configureListenerInvocationErrorHandler（）**并通过记录异常将异常传播到上层去处理错误。</p><h2 id="步骤3">步骤3</h2><p>我们使用MongoDB来存储Event Store中所发生的所有事件。要实现此功能，可以通过如下的方法来实现：</p><pre><code class="language-java">@Beanpublic EventStorageEngine eventStore(MongoTemplate mongoTemplate) {   return new MongoEventStorageEngine(           new JacksonSerializer(), null, mongoTemplate, new DocumentPerEventStorageStrategy());}</code></pre><p>这样，在事件总线上发布的所有事件都将自动保存到MongoDB数据库中。通过这种简单的配置，我们就可以在应用程序中使用MongoDB的数据源。</p><p>在Axon配置方面就是这样的简单。当然，我们还有其他很多的配置方式。但我们可以通过上述简单的配置，就可以使用Axon的功能了。</p><h1 id="使用axon实现cqrs">使用Axon实现CQRS</h1><p><img src="https://cdn.ramostear.com/2019-04-21-07-28-45-a1d2bd7f25f848948ecec50bfa7ff77e.jpg" alt="" /></p><p>根据上图，创建命令，将命令传递给命令总线然后创建事件并将事件放在事件总线上还不是CQRS。我们必须记住改变写入存储库的状态并从读取数据库中读取当前状态，这才是CQRS模式的关键点。</p><p>配置此流程也并不复杂。在将命令传递给命令网关时，Spring将名利类型作为参数以搜索带有**@CommandHandler** 注释的方法。</p><p><img src="https://cdn.ramostear.com/2019-04-21-07-29-00-954bd4414520423b9379723606c147a4.gif" alt="" /></p><pre><code class="language-java">@Valueclass SubmitApplicationCommand {   private String appId;   private String category;}@AllArgsConstructorpublic class ApplicationService {   private final CommandGateway commandGateway;   public CompletableFuture&lt;Void&gt; createForm(String appId) {       return CompletableFuture.supplyAsync(() -&gt; new SubmitExpertsFormCommand(appId, &quot;Android&quot;))               .thenCompose(commandGateway::send);   }}</code></pre><p>除其他事项外，命令处理程序负责将创建的事件发送到事件总线。它将事件对象放置到AggregateLifecycle静态导入的apply（）方法中。然后调度该事件以查找预期的处理程序，并且由于我们配置了事件存储库，所有事件都自动保存在数据库中。</p><p><img src="https://cdn.ramostear.com/2019-04-21-07-29-29-be06308cccb24ba7bfa0d35913a89271.gif" alt="" /></p><pre><code class="language-java">@Valueclass ApplicationSubmittedEvent {   private String appId;   private String category;}@Aggregate@NoArgsConstructorpublic class ApplicationAggregate {   @AggregateIdentifier   private String id;   @CommandHandler   public ApplicationAggregate(SubmitApplicationCommand command) {      //some validation       this.id = command.getAppId;       apply(new ApplicationSubmittedEvent(command.getAppId(), command.getCategory()));   }}</code></pre><p>要更改写库的状态，我们需要提供一个使用**@EventHandler**注释的方法。该应用程序可以包含多个事件处理程序。他们每个人都应该执行一个特定的任务，如发送电子邮件，记录或保存在数据库中。</p><p><img src="https://cdn.ramostear.com/2019-04-21-07-29-49-96788b0622da48c9843d6b6719162061.gif" alt="" /></p><pre><code class="language-java">@RequiredArgsConstructor@Order(1)public class ProjectingEventHandler {   private final IApplicationSubmittedProjection projection;   @EventHandler   public CompletableFuture&lt;Void&gt; onApplicationSubmitted(ExpertsFormSubmittedEvent event) {       return projection.submitApplication(event.getApplicationId(), event.getCategory());   }</code></pre><p>如果我们想确定所有事件处理程序的处理顺序，我们可以用**@Order**注释一个类并设置一个序列号。**submitApplication（）**方法负责进行所有必要的更改并将新的数据存储到写库中。</p><p>这些都是使我们的应用程序采用CQRS模式原则的关键点。当然，这些原则只能应用于我们应用程序的某些部分，具体取决于业务需求。Event Sourcing不适合我们正在构建的每个应用程序或模块。在实现此模式时也要谨慎，因为更复杂的应用程序可能难以维护。</p><h1 id="总结">总结</h1><p>使用Axon框架，CQRS和Event Sourcing的实现变得简单化。有关高级配置的更多详细信息，请访问Axon的网站<a href="https://docs.axonframework.org/">https://docs.axonframework.org/</a>。</p><blockquote><p>原文作者：ŁukaszKucik</p><p>原文地址：<a href="https://www.nexocode.com/blog/posts/smooth-implementation-cqrs-es-with-sping-boot-and-axon/">https://www.nexocode.com/blog/posts/smooth-implementation-cqrs-es-with-sping-boot-and-axon/</a></p><p>译        者：谭朝红</p></blockquote>]]>
                    </description>
                    <pubDate>Wed, 23 Feb 2022 15:56:01 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[管中窥豹- JSON Web Token]]>
                    </title>
                    <link>https://www.ramostear.com/2022/02/jsonwebtoken.html</link>
                    <description>
                            <![CDATA[<blockquote><p>越来越多的开发者开始学习JWT技术并在实际项目中运用JWT来保护应用安全。一时间，JWT技术风光无限，很多公司的应用程序也开始使用JWT（Json Web Token）来管理用户会话信息。本文将从JWT的基本原理出发，分析在使用JWT构建基于Token的身份验证系统时需要谨慎对待的细节。</p></blockquote><p>​任何技术框架都有自身的局限性，不可能一劳永逸，JWT也不例外。接下来，将从JWT的概念，基本原理和适用范围来剖析为什么说JWT不是银弹，需要谨慎处理。</p><p>​众所周知，如果我们的账户信息（用户名和密码）泄露，存储在服务器上的隐私数据将受到毁灭性的打击，如果是管理员的账户信息泄露，系统还有被攻击的危险。那么，JWT的信息发生泄露，会带来什么样的影响？该如何防范？这将是本文重点阐述的内容。</p><h1 id="1什么是token">1、什么是Token?</h1><p><img src="https://cdn.ramostear.com/2019-08-03-04-06-03-bb96077607f64b3096a0027d912ee047.jpg" alt="" /></p><p>​Token(令牌)通常是指Security Token(安全令牌)，可以分为Hardware Token(硬件令牌)，Authentication Token(授权令牌)，USB Token(USB令牌)，Cryptographic Token(加密令牌)，Virtual Token(虚拟令牌)和Key Fob(钥匙卡)。其主要作用是验证身份的合法性，以允许计算机系统的用户可以操作系统资源。生活中常见的令牌如：登录密码，指纹，声纹，门禁卡，银行电子卡等。Token的主要目的是为计算机系统提供一个可以识别用户的任意数值，例如“token123”这样的明文字符串，或者像“41ea873f-3a4d-57c8-1e38-ef74f31015af”之类的加密字符。</p><p>​由于篇幅关系，Token就了解到这里。接下来将聊聊有关JWT(JSON Web Token)的原理。</p><h1 id="2什么是json-web-token">2、什么是JSON Web Token?</h1><p><img src="https://cdn.ramostear.com/2019-08-03-04-06-40-e654e28c0a364d3f8e9d7f716186da0d.jpg" alt="" /></p><p>​JSON Web Token(JWT)是一个基于RFC 7519的开放数据标准，它定义了一种宽松且紧凑的数据组合方式，使用JSON对象在各应用之间传输加密信息。该JSON对象可以通过数字签名进行鉴签和校验，一般地，JWT可以采用HMAC算法，RSA或者ECDSA的公钥/私钥对数据进行签名操作。</p><p>​一个JWT通常有HEADER(头)，PAYLOAD(有效载荷)和SIGNATURE(签名)三个部分组成，三者之间使用“.”链接，格式如下：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-06-59-92e3425909f646348bd68441d408c798.png" alt="" /></p><p>下面是的字符串是一个JWT的实际案例：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-07-16-6af6fb13b89a43cf9fcc8a3883bb3cab.png" alt="" /></p><blockquote><p>注意三者之间有一个点号(&quot;.&quot;)相连</p></blockquote><p>​为了更直观的了解JWT的创建过程和使用方式，我们通过一个简单的例子来演示这两个过程。</p><h1 id="3如何创建jwt">3、如何创建JWT?</h1><p>​JWT通常由“标头.有效载荷.签名”的格式组成。其中，标头用于存储有关如何计算JWT签名的信息，如对象类型，签名算法等。下面是JWT中Header部分的JSON对象实例：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-07-30-9865c15c41a84a7a86459b149b94e719.png" alt="" /></p><p>在此JSON对象中，type表示该对象为JWT,alg表示创建JWT时使用HMAC-SHA256散列算法计算签名。有效载荷主要用于存储用户信息，如用户ID，Email，角色和权限信息等。下面是有效载荷的一个简单示例：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-07-48-d2941e9b84b44e6a8d9523a1cab935cd.png" alt="" /></p><p>而签名则需要使用Base64URL编码技术对标头(Header)和有效载荷(Payload)进行编码，并作为参数和秘钥一同传递给签名算法，生成最终的签名(Signature)。以HMAC-SHA256算法为例，下面是生成签名的一个伪代码：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-08-06-78b019c820894f7aae4092f787b4bc36.png" alt="" /></p><p><img src="https://cdn.ramostear.com/2019-08-03-04-08-34-5e4002b812ae434ea17706b15ee5bdd2.png" alt="" /></p><p>​现在，我们已经了解了JWT的基本原理，接下来将使用Java来演示生成JWT的完整过程。</p><h1 id="4基于java实现的jwtjjwt案例">4、基于Java实现的JWT(JJWT)案例</h1><h2 id="4-1依赖">4-1、依赖</h2><p>以Maven工程为例，需要在pom.xml文件中添加入下的配置信息：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-08-49-281c082b45df41fd8c800ea7a0963064.png" alt="" /></p><p>如果是非Maven工程，你也可以到Maven中央仓库搜索<strong>jjwt</strong>，然后选择相应的版本(0.9.0)下载到本地，并将jar包添加到工程的类路径(classpath)中。</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-09-05-f41eadfaeeba431396d70f4bcda41fcc.png" alt="" /></p><h2 id="4-2生成jwt">4-2、生成JWT</h2><p>​在工程中新建JJWTUitls.java工具类，使用jjwt提供的方法实现JWT的生成，实现细节如下：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-09-25-b383b3085dee4a4faab07aa1bd5bf0da.png" alt="" /></p><p>在此方法中，JJWT已经处理好JWT标头(Header)的信息，我们只需要提供签名所使用的算法(如SignatureAlgorithm.HS256)，有效载荷，主题(包含了用户信息)，过期时间(exp-time)和秘钥即可，最后使用jjwt的builder()方法组装JWT。下面是生成秘钥方法key()的源代码：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-09-49-c7a4b54759c844e9a7836048ea5bafbe.png" alt="" /></p><h2 id="4-3解析jwt">4-3、解析JWT</h2><p>​使用JJWT解析JWT相对简单，首先获取秘钥，然后通过Jwts.parse()方法设置秘钥并JWT进行解析，实现细节如下：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-10-02-980ca2898038495992e776975dc1c531.png" alt="" /></p><h2 id="4-4测试jjwt">4-4、测试JJWT</h2><p>​最后，在工程中新建一个JavaJWT.java类，并在main方法中检验JJWTUtils工具类中生成和解析JWT两个方法是否有效。实现细节如下：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-10-17-b7b070b89437431091ea12f4d8c8a5a7.png" alt="" /></p><p>如上图所示，“jwt”将作为JWT标头(Header)“type”的值，有效载荷(payload)中的主题信息如下：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-10-49-274f93452a3542e794a29fcaa43e85f3.png" alt="" /></p><p>且JWT签名的有效时间为60,000毫秒。执行main方法，输出信息如下所示：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-11-11-d9bfb3c5cdca4c3b81453239d8bd8001.png" alt="" /></p><p>​从测试结果可以看出，我们成功的使用JJWT创建并解析了JWT。接下来，我们将了解到在实际的应用中，JWT对用户信息进行验证的基本流程。</p><h1 id="5-json-web-token的工作流程">5、 JSON Web Token的工作流程</h1><p>​在身份验证中，当用户成功登录系统时，授权服务器将会把JSON Web Token返回给客户端，用户需要将此凭证信息存储在本地(cookie或浏览器缓存)。当用户发起新的请求时，需要在请求头中附带此凭证信息，当服务器接收到用户请求时，会先检查请求头中有无凭证，是否过期，是否有效。如果凭证有效，将放行请求；若凭证非法或者过期，服务器将回跳到认证中心，重新对用户身份进行验证，直至用户身份验证成功。以访问API资源为例，下图显示了获取并使用JWT的基本流程：</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-11-26-1469e3959f584d89b19f1a160443b6a2.png" alt="" /></p><p>​现在，我们已经完全了解了JWT是什么，怎么实现以及用来干什么这三个问题。在上述的案例中，我们使用HS256算法对JWT进行签名，在这个过程中，只有身份验证服务器和应用服务器知道秘钥是什么。如果身份验证服务器和应用服务器完全独立，则应用服务器的JWT校验工作也可以交由认证服务器完成。当客户端对应用服务器发起调用时，应用服务器会使用秘钥对签名进行校验，如果签名有效且未过期，则允许客户端的请求，反之则拒绝请求。</p><h1 id="6使用json-web-token的利弊">6、使用JSON Web Token的利弊</h1><p>​优势与劣势是相对而言的，这里主要以传统的Session模式作为参考，总结使用JWT可以获得优势以及带来的弊端。</p><h2 id="6-1-使用jwt的优势">6-1、 使用JWT的优势</h2><p>​使用JSON Web Token保护应用安全，你至少可以获得以下几个优势：</p><ol><li><strong>更少的数据库连接</strong>：因其基于算法来实现身份认证，在使用JWT时查询数据的次数更少(更少的数据连接不等于不连接数据库)，可以获得更快的系统响应时间。</li><li><strong>构建更简单</strong>：如果你的应用程序本身是无状态的，那么选择JWT可以加快系统构建过程。</li><li><strong>跨服务调用</strong>：你可以构建一个认证中心来处理用户身份认证和发放签名的工作，其他应用服务在后续的用户请求中不需要(理论上)在询问认证中心，可使用自有的公钥对用户签名进行验证。</li><li><strong>无状态</strong>：你不需要向传统的Web应用那样将用户状态保存于Session中。</li></ol><h2 id="6-2使用jwt的弊端">6-2、使用JWT的弊端</h2><p>​JWT不是万能的，使用JWT也会带来诸多问题。就个人使用情况，使用JWT时可能会面临以下几个麻烦：</p><ol><li><strong>严重依赖于秘钥</strong>：JWT的生成与解析过程都需要依赖于秘钥(Secret)，且都以硬编码的方式存在于系统中(也有放在外部配置文件中的)。如果秘钥不小心泄露，系统的安全性将收到威胁。</li><li><strong>服务端无法管理客户端的信息</strong>：如果用户身份发生异常(信息泄露，或者被攻击)，服务端很难向操作Session那样主动将异常用户进行隔离。</li><li><strong>服务端无法主动推送消息</strong>：服务端由于是无状态的，他将无法使用像Session那样的方式推送消息到客户端，例如过期时间将至，服务端无法主动为用户续约，需要客户端向服务端发起续约请求。</li><li><strong>冗余的数据开销</strong>：一个JWT签名的大小要远比一个Session ID长很多，如果你对有效载荷(payload)中的数据不做有效控制，其长度会成几何倍数增长，且在每一次请求时都需要负担额外的网络开销。</li></ol><p>​JSON Web Token 很流行，但是它相比于Session,OIDC(OpenId Connect)等技术还比较新，支持JSON Web Token的库还比较少，而且JWT也并非比传统Session更安全，他们都没有解决CSRF和XSS的问题。因此，在决定使用JWT前，你需要仔细考虑其利弊。</p><h1 id="7json-web-token并非银弹蹲坑需谨慎">7、JSON Web Token并非银弹，“蹲坑”需谨慎</h1><blockquote><p>考虑这样一个问题：如果客户端的JWT令牌泄露或者被盗取，会发生什么严重的后果？有什么补救措施？</p></blockquote><p>​如果单纯的依靠JSON Web Token解决用户认证的所有问题，那么系统的安全性将是脆弱的。由于JWT令牌存储于客户端中，一旦客户端存储的令牌发生泄露事件或者被攻击，攻击者就可以轻而易举的伪造用户身份去修改/删除系统资源，岁如按JWT自带过期时间，但在过期之前，攻击者可以肆无忌惮的操作系统数据。通过算法来校验用户身份合法性是JWT的优势，同时也是最大的弊端——它太过于依赖算法。</p><p>​反观传统的用户认证措施，通常会包含多种组合，如手机验证码，人脸识别，语音识别，指纹锁等。用户名和密码只做用户身份识别使用，当用户名和密码泄露后，在遇到敏感操作时(如新增，修改，删除，下载，上传)，都会采用另外的方式对用户的合法性进行验证(发送验证码，邮箱验证码，指纹信息等)以确保数据安全。</p><p>​与传统的身份验证方式相比，JWT过多的依赖于算法，缺乏灵活性，而且服务端往往是被动执行用户身份验证操作，无法及时对异常用户进行隔离。那是否有补救措施呢？答案是坑定的。接下来，将介绍在发生令牌泄露事件后，如何保证系统的安全。</p><h1 id="8使用json-web-token-爬坑指南">8、使用JSON Web Token 爬坑指南</h1><p>​不管是基于Sessions还是基于JSON Web Token，一旦密令被盗取，都是一件棘手的事情。接下来，将讲述基于JSON Web Token的方式发生令牌泄露是该采取什么样的措施(解决方案包含但不局限与本文所涉及的内容)。</p><p><img src="https://cdn.ramostear.com/2019-08-03-04-11-45-9fa45203b142488fb09cd4ce7bc75034.png" alt="" /></p><p>为了防止用户JWT令牌泄露而威胁系统安全，你可以在以下几个方面完善系统功能：</p><ol><li><strong>清除已泄露的令牌</strong>：此方案最直接，也容易实现，你需将JWT令牌在服务端也存储一份，若发现有异常的令牌存在，则从服务端令牌列表中将此异常令牌清除。当用户发起请求时，强制用户重新进行身份验证，直至验证成功。对于服务端的令牌存储，可以借助Redis等缓存服务器进行管理，也可以使用Ehcache将令牌信息存储在内存中。</li><li><strong>敏感操作保护</strong>：在涉及到诸如新增，修改，删除，上传，下载等敏感性操作时，定期(30分钟，15分钟甚至更短)检查用户身份，如手机验证码，扫描二维码等手段，确认操作者是用户本人。如果身份验证不通过，则终止请求，并要求重新验证用户身份信息。</li><li><strong>地域检查</strong>：通常用户会在一个相对固定的地理范围内访问应用程序，可以将地理位置信息作为一个辅助来甄别用户的JWT令牌是否存在问题。如果发现用户A由经常所在的地区1变到了相对较远的地区2，或者频繁在多个地区间切换，不管用户有没有可能在短时间内在多个地域活动(一般不可能)，都应当终止当前请求，强制用户重新进行验证身份，颁发新的JWT令牌，并提醒(或要求)用户重置密码。</li><li><strong>监控请求频率</strong>：如果JWT密令被盗取，攻击者或通过某些工具伪造用户身份，高频次的对系统发送请求，以套取用户数据。针对这种情况，可以监控用户在单位时间内的请求次数，当单位时间内的请求次数超出预定阈值值，则判定该用户密令是有问题的。例如1秒内连续超过5次请求，则视为用户身份非法，服务端终止请求并强制将该用户的JWT密令清除，然后回跳到认证中心对用户身份进行验证。</li><li><strong>客户端环境检查</strong>：对于一些移动端应用来说，可以将用户信息与设备(手机,平板)的机器码进行绑定，并存储于服务端中，当客户端发起请求时，可以先校验客户端的机器码与服务端的是否匹配，如果不匹配，则视为非法请求，并终止用户的后续请求。</li></ol><h1 id="总结">总结</h1><blockquote><p>​本文从Token的基本含义，JSON Web Token的原理和流程出发，并结合实际的案例分析了使用JSON Web Token的优势与劣势；与此同时，结合自己实际使用JSON Web Token过程中发现的问题给出了避免“踩坑”的解决方案。</p><p>​世上没有完美的解决方案，系统的安全性需要开发者积极主动地去提升，其过程是漫长且复杂的，也许一开始的MVP系统并不需要那么强大的安全性，但随着业务的增长系统需要升级，或者说最终将重写整个系统，提前了解技术背后可能会遇到的问题，不失为一种好的编程习惯。</p><p>​JSON Web Token的出现，为解决Web应用安全性问题提供了一种新思路。但JSON Web Token也不是银弹，你任然需要做很多复杂的工作才能提升系统的安全性。</p></blockquote>]]>
                    </description>
                    <pubDate>Wed, 23 Feb 2022 15:53:11 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[微服务架构的进程间通信(IPC)]]>
                    </title>
                    <link>https://www.ramostear.com/2022/02/microservice-ipc.html</link>
                    <description>
                            <![CDATA[<h1 id="1-进程间通信ipc">1. 进程间通信(IPC)</h1><p>在单体式应用中，各个模块之间的调用是通过编程语言级别的方法或者函数来实现的。但是一个基于微服务的分布式应用是运行在多台机器上的。</p><p>一般来说，每个服务实例都是一个进程。因此，如下图所示，服务之间的交互必须通过进程间通信（IPC）来实现。</p><img src="https://cdn.ramostear.com/20200408-3268ef9b881049818c4eba8dbb578fab.png" style="zoom:150%;display:block;margin:5px auto;" /><h1 id="2-客户端与微服务的交互模式">2. 客户端与微服务的交互模式</h1><p>交互模式可以从两个维度进行归类。<br />（1）第一个维度是一对一还是一对多：</p><ul><li>一对一：每个客户端请求有一个服务实例来响应。</li><li>一对多：每个客户端请求有多个服务实例来响应。</li></ul><p>（2）第二个维度是这些交互式同步还是异步：</p><ul><li>同步模式：客户端请求需要服务端即时响应，甚至可能由于等待而阻塞。</li><li>异步模式：客户端请求不会阻塞进程，服务端的响应可以是非即时的。</li></ul><p>（3）一对一的交互模式有以下几种方式：</p><ul><li>请求/响应：一个客户端向服务器端发起请求，等待响应。客户端期望此响应即时到达。在一个基于线程的应用中，等待过程可能造成线程阻塞。</li><li>通知（也就是常说的单向请求）：一个客户端请求发送到服务端，但是并不期望服务端响应。</li><li>请求/异步响应：客户端发送请求到服务端，服务端异步响应请求。客户端不会阻塞，而且被设计成默认响应不会立刻到达。</li></ul><p>（4）一对多的交互模式有以下几种方式：</p><ul><li>发布/订阅模式：客户端发布通知消息，被零个或者多个感兴趣的服务消费。</li><li>发布/异步响应模式：客户端发布请求消息，然后等待从感兴趣服务发回的响应。</li></ul><p>下表显示了不同交互模式：</p><img src="https://cdn.ramostear.com/20200408-ea3cc645c55249daae7351716bf9da5f.jpg" style="zoom:150%;display:block;margin:5px auto;" /><p>每个服务都是以上这些模式的组合，对某些服务，一个IPC机制就足够了；而对另外一些服务则需要多种IPC机制组合。下图展示了在一个打车服务请求中服务之间是如何通信的。</p><img src="https://cdn.ramostear.com/20200408-b0f3a72fa9514c0dbc34852400dddf17.png" style="zoom:150%;display:block;margin:5px auto;" /><p>上图中的服务通信使用了通知、请求/响应、发布/订阅等方式。例如，乘客通过移动端给『行程管理服务』发送通知，希望申请一次出租服务。『行程管理服务』发送请求/响应消息给『乘客服务』以确认乘客账号是有效的。紧接着创建此次行程，并用发布/订阅交互模式通知其他服务，包括定位可用司机的调度服务。</p><h1 id="3客户端与服务端接口api">3.客户端与服务端接口API</h1><p>不管选择了什么样的IPC机制，重要的是使用某种交互式定义语言（IDL）来精确定义一个服务的接口API。<br />接口API的定义实质上依赖于选择哪种IPC。如果使用消息机制，API则由消息频道（channel）和消息类型构成；如果选择使用HTTP机制，API则由URL和请求、响应格式构成。<br />API的变化是不可避免的，微小的改变可以和之前版本兼容。比如，你可能只是为某个请求和响应添加了一个属性。这时，客户端使用旧版API应该也能和新版本一起工作。但是有时候，API需要进行大规模的改动，并且可能与之前版本不兼容。因为你不可能强制让所有的客户端立即升级，所以支持老版本客户端的服务还需要再运行一段时间。如果你正在使用基于基于HTTP机制的IPC，例如REST，一种解决方案是把版本号嵌入到URL中。每个服务都可能同时处理多个版本的API。或者，你可以部署多个实例，每个实例负责处理一个版本的请求。</p><h1 id="4容错处理">4.容错处理</h1><p>分布式系统中部分失败是普遍存在的问题。因为客户端和服务端是都是独立的进程，一个服务端有可能因为故障或者维护而停止服务，或者此服务因为过载而停止或者反应很慢。<br />假设推荐服务无法响应请求，那客户端就会由于等待响应而阻塞，这不仅会给客户带来很差的体验，而且在很多应用中还会占用很多资源，比如线程，以至于到最后由于等待响应被阻塞的客户端越来越多，线程资源被耗费完了。如下图所示：</p><img src="https://cdn.ramostear.com/20200408-4048441606c64c9aa1bee0943a13f916.png" style="zoom:150%;display:block;margin:5px auto;" /><p>Netfilix Hystrix提供了一个比较好的解决方案，具体的应对措施包括：</p><ul><li>网络超时：当等待响应时，不要无限期的阻塞，而是采用超时策略。使用超时策略可以确保资源不会无限期的占用。</li><li>限制请求的次数：可以为客户端对某特定服务的请求设置一个访问上限。如果请求已达上限，就要立刻终止请求服务。</li><li>断路器模式（Circuit Breaker Pattern）：记录成功和失败请求的数量。如果失效率超过一个阈值，触发断路器使得后续的请求立刻失败。如果大量的请求失败，就可能是这个服务不可用，再发请求也无意义。在一个失效期后，客户端可以再试，如果成功，关闭此断路器。</li><li>提供回滚：当一个请求失败后可以进行回滚逻辑。例如，返回缓存数据或者一个系统默认值。</li></ul><h1 id="5ipc实现技术">5.IPC实现技术</h1><p>服务之间的通信采用同步的请求/响应模式，可以选择基于HTTP的REST或者Thrift。<br />服务之间的通信采用异步的、基于消息的通信模式，可以选择AMQP或者STOMP。大量开源消息中间件可供选择，比如RabbitMQ、Apache Kafka、Apache ActiveMQ和NSQ。<br />消息格式可以选择基于文本的，比如 JSON和XML；二进制格式（效率更高）的，比如Avro和Protocol Buffer。</p><h2 id="51-采用异步的基于消息的通信模式">5.1 采用异步的，基于消息的通信模式</h2><p>下图展示了打车软件如何使用消息发布/订阅：</p><img src="https://cdn.ramostear.com/20200408-e15ef4605dc947b4bcf543e936340317.png" style="zoom:150%;display:block;margin:5px auto;" /><p>行程管理服务在发布-订阅channel内创建一个行程消息，并通知调度服务有一个新的行程请求，调度服务发现一个可用的司机然后向发布-订阅channel写入司机建议消息（Driver Proposed message）来通知其他服务。</p><h2 id="52-采用同步的基于请求响应的通信模式">5.2 采用同步的，基于请求/响应的通信模式</h2><p>下图展示了打车软件如何使用REST:</p><img src="https://cdn.ramostear.com/20200408-b3979062e20c495eabacd86eebf494de.png" style="zoom:150%;display:block;margin:5px atuo;" /><p>乘客通过移动端向行程管理服务的/trips资源提交了一个POST请求。行程管理服务收到请求之后，会发送一个GET请求到乘客管理服务以获取乘客信息。当确认乘客信息之后，紧接着会创建一个行程，并向移动端返回201状态码响应。</p><p>使用基于HTTP的协议的好处：</p><ul><li>HTTP非常简单并且大家都很熟悉。</li><li>可以使用浏览器扩展（比如Postman）或者curl之类的命令行来测试API。</li><li>内置支持请求/响应模式的通信。</li><li>HTTP对防火墙友好。</li><li>不需要中间代理，简化了系统架构。</li></ul><p>使用基于HTTP的协议的不足之处：</p><ul><li>只支持请求/响应模式交互。可以使用HTTP通知，但是服务端必须一直发送HTTP响应才行。</li><li>因为客户端和服务端直接通信（没有代理或者buffer机制），在交互期间必须都在线。</li><li>客户端必须知道每个服务实例的URL。客户端必须使用服务实例发现机制。</li></ul><hr /><p><a href="https://maping930883.blogspot.com/2016/06/architect018.html" target="_blank"><img src="https://img.shields.io/badge/转载-https://maping930883.blogspot.com/2016/06/architect018.html-green" style="zoom: 200%;display:block;margin:5px auto;" ></a></p>]]>
                    </description>
                    <pubDate>Wed, 23 Feb 2022 15:19:23 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[如何在运行时重启SpringBoot Application?]]>
                    </title>
                    <link>https://www.ramostear.com/2020/05/howtorestartspringbootatruntime.html</link>
                    <description>
                            <![CDATA[<h1 id="运行时重启springboot-application">运行时重启SpringBoot Application</h1><h2 id="需求背景">需求背景</h2><p>在一个很奇葩的需求下，要求在客户端动态修改Spring Boot配置文件中的属性，例如端口号、应用名称、数据库连接信息等，然后通过一个Http请求重启Spring Boot程序。这个需求类似于操作系统更新配置后需要进行重启系统才能生效的应用场景。</p><p>动态配置系统并更新生效是应用的一种通用性需求，实现的方式也有很多种。例如监听配置文件变化、使用配置中心等等。网络上也有很多类似的教程存在，但大多数都是在开发阶段，借助Spring Boot DevTools插件实现应用程序的重启，或者是使用spring-boot-starter-actuator和spring-cloud-starter-config来提供端点（Endpoint）的刷新。</p><blockquote><p>第一种方式无法在生产环境中使用（不考虑），第二种方式需要引入Spring Cloud相关内容，这无疑是杀鸡用了宰牛刀。</p></blockquote><p>接下来，我将尝试采用另外一种方式实现HTTP请求重启Spring Boot应用程序这个怪异的需求。</p><h2 id="尝试思路">尝试思路</h2><p>重启Spring Boot应用程序的关键步骤是对主类中**SpringApplication.run(Application.class,args);<strong>方法返回值的处理。<strong>SpringApplication#run()<strong>方法将会返回一个</strong>ConfigurableApplicationContext</strong>类型对象，通过查看官方文档可以看到，<strong>ConfigurableApplicationContext</strong>接口类中定义了一个</strong>close()**方法，可以用来关闭当前应用的上下文：</p><pre><code class="language-java">package org.springframework.context;import java.io.Closeable;import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.BeanFactoryPostProcessor;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;import org.springframework.core.env.ConfigurableEnvironment;import org.springframework.core.io.ProtocolResolver;import org.springframework.lang.Nullable;public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {void close();    }    </code></pre><p>继续看官方源码，<strong>AbstractApplicationContext</strong>类中实现**close()**方法，下面是实现类中的方法摘要：</p><pre><code class="language-java">public void close() {        Object var1 = this.startupShutdownMonitor;        synchronized(this.startupShutdownMonitor) {            this.doClose();            if (this.shutdownHook != null) {                try {                    Runtime.getRuntime().removeShutdownHook(this.shutdownHook);                } catch (IllegalStateException var4) {                    ;                }            }        }    }</code></pre><p>**#close()<strong>方法将会调用#doClose()方法，我们再来看看#doClose()方法做了哪些操作，下面是</strong>doClose()**方法的摘要：</p><pre><code class="language-java">protected void doClose() {        if (this.active.get() &amp;&amp; this.closed.compareAndSet(false, true)) {                        ...                        LiveBeansView.unregisterApplicationContext(this);            ...            this.destroyBeans();            this.closeBeanFactory();            this.onClose();            if (this.earlyApplicationListeners != null) {                this.applicationListeners.clear();                this.applicationListeners.addAll(this.earlyApplicationListeners);            }            this.active.set(false);        }    }</code></pre><p>在**#doClose()**方法中，首先将应用上下文从注册表中清除掉，然后是销毁Bean工厂中的Beans,紧接着关闭Bean工厂。</p><p>官方文档看到这里，就产生了解决一个结局重启应用应用程序的大胆猜想。在应用程序的<strong>main()<strong>方法中，我们可以使用一个临时变量来存放SpringApplication.run()返回的ConfigurableApplicationContext对象，当我们完成对Spring Boot应用程序中属性的设置后，调用</strong>ConfigurableApplicationContext</strong>的**#close()**方法，最后再调用SpringApplication.run()方法重新给ConfigurableApplicationContext对象进行赋值已达到重启的效果。</p><p>现在，我们再来看一下SpringApplication.run()方法中是如何重新创建ConfigurableApplicationContext对象的。在SpringApplication类中，run()方法会调用createApplicationContext()方法来创建一个ApplicationContext对象：</p><pre><code class="language-java">protected ConfigurableApplicationContext createApplicationContext() {        Class&lt;?&gt; contextClass = this.applicationContextClass;        if (contextClass == null) {            try {                switch(this.webApplicationType) {                case SERVLET:                    contextClass = Class.forName(&quot;org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext&quot;);                    break;                case REACTIVE:                    contextClass = Class.forName(&quot;org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext&quot;);                    break;                default:                    contextClass = Class.forName(&quot;org.springframework.context.annotation.AnnotationConfigApplicationContext&quot;);                }            } catch (ClassNotFoundException var3) {                throw new IllegalStateException(&quot;Unable create a default ApplicationContext, please specify an ApplicationContextClass&quot;, var3);            }        }        return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);    }</code></pre><p><strong>createApplicationContext()<strong>方法会根据</strong>WebApplicationType</strong>类型来创建ApplicationContext对象。在<strong>WebApplicationType</strong>中定义了三种种类型：<strong>NONE</strong>、<strong>SERVLET</strong>和<strong>REACTIVE</strong>。通常情况下，将会创建servlet类型的ApplicationContext对象。</p><p>接下来，我将以一个简单的Spring Boot工程来验证上述的猜想是否能够达到重启Spring Boot应用程序的需求。</p><h2 id="编码实现">编码实现</h2><p>首先，在application.properties文件中加入如下的配置信息，为动态修改配置信息提供数据：</p><pre><code class="language-properties">spring.application.name= SPRING-BOOT-APPLICATION</code></pre><p>接下来，在Spring Boot主类中定义两个私有变量，用于存放main()方法的参数和SpringApplication.run()方法返回的值。下面的代码给出了主类的示例：</p><pre><code class="language-java">public class ExampleRestartApplication {@Value ( &quot;${spring.application.name}&quot; )String appName;private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );private static String[] args;private static ConfigurableApplicationContext context;public static void main(String[] args) {ExampleRestartApplication.args = args;ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);}}</code></pre><p>最后，直接在主类中定义用于刷新并重启Spring Boot应用程序的端点(Endpoint)，并使用**@RestController**注解对主类进行注释。</p><pre><code class="language-java">@GetMapping(&quot;/refresh&quot;)public String restart(){    logger.info ( &quot;spring.application.name:&quot;+appName);    try {        PropUtil.init ().write ( &quot;spring.application.name&quot;,&quot;SPRING-DYNAMIC-SERVER&quot; );    } catch (IOException e) {        e.printStackTrace ( );    }    ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue&lt;&gt; ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());    threadPool.execute (()-&gt;{        context.close ();        context = SpringApplication.run ( ExampleRestartApplication.class,args );    } );    threadPool.shutdown ();    return &quot;spring.application.name:&quot;+appName;}</code></pre><blockquote><p>说明：为了能够重新启动Spring Boot应用程序，需要将close()和run()方法放在一个独立的线程中执行。</p></blockquote><p>为了验证Spring Boot应用程序在被修改重启有相关的属性有没有生效，再添加一个获取属性信息的端点，返回配置属性的信息。</p><pre><code class="language-java">@GetMapping(&quot;/info&quot;)public String info(){    logger.info ( &quot;spring.application.name:&quot;+appName);    return appName;}</code></pre><h2 id="完整的代码">完整的代码</h2><p>下面给出了主类的全部代码：</p><pre><code class="language-java">package com.ramostear.application;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.io.IOException;import java.util.concurrent.*;/** * @author ramostear */@SpringBootApplication@RestControllerpublic class ExampleRestartApplication {@Value ( &quot;${spring.application.name}&quot; )String appName;private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );private static String[] args;private static ConfigurableApplicationContext context;public static void main(String[] args) {ExampleRestartApplication.args = args;ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);}@GetMapping(&quot;/refresh&quot;)public String restart(){logger.info ( &quot;spring.application.name:&quot;+appName);try {PropUtil.init ().write ( &quot;spring.application.name&quot;,&quot;SPRING-DYNAMIC-SERVER&quot; );} catch (IOException e) {e.printStackTrace ( );}ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue&lt;&gt; ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());threadPool.execute (()-&gt;{context.close ();context = SpringApplication.run ( ExampleRestartApplication.class,args );} );threadPool.shutdown ();return &quot;spring.application.name:&quot;+appName;}@GetMapping(&quot;/info&quot;)public String info(){logger.info ( &quot;spring.application.name:&quot;+appName);return appName;}}</code></pre><p>接下来，运行Spring Boot程序，下面是应用程序启动成功后控制台输出的日志信息：</p><pre><code class="language-tex">[2019-03-12T19:05:53.053z][org.springframework.scheduling.concurrent.ExecutorConfigurationSupport][main][171][INFO ] Initializing ExecutorService 'applicationTaskExecutor'[2019-03-12T19:05:53.053z][org.apache.juli.logging.DirectJDKLog][main][173][INFO ] Starting ProtocolHandler [&quot;http-nio-8080&quot;][2019-03-12T19:05:53.053z][org.springframework.boot.web.embedded.tomcat.TomcatWebServer][main][204][INFO ] Tomcat started on port(s): 8080 (http) with context path ''[2019-03-12T19:05:53.053z][org.springframework.boot.StartupInfoLogger][main][59][INFO ] Started ExampleRestartApplication in 1.587 seconds (JVM running for 2.058)</code></pre><p>在测试修改系统配置并重启之前，使用Postman测试工具访问：<a href="http://localhost:8080/info">http://localhost:8080/info</a> ，查看一下返回的信息：</p><p><img src="https://cdn.ramostear.com/2019-03-12-19-50-27-c2194858b5cf4caf8718b19691a02049.png" alt="" /></p><p>成功返回<strong>SPRING-BOOT-APPLICATION</strong>提示信息。</p><p>然后，访问：<a href="http://localhost:8080/refresh">http://localhost:8080/refresh</a> ，设置应用应用程序<strong>spring.application.name</strong>的值为<strong>SPRING-DYNAMIC-SERVER</strong>，观察控制台输出的日志信息：</p><p><img src="https://cdn.ramostear.com/2019-03-12-19-50-39-0e48b7f05cd8427985646b9d133b53ce.png" alt="" /></p><p>可以看到，Spring Boot应用程序已经重新启动成功，最后，在此访问：<a href="http://localhost:8080/info">http://localhost:8080/info</a> ,验证之前的修改是否生效：</p><p><img src="https://cdn.ramostear.com/2019-03-12-19-50-51-fbb61e0691a7438c868dbbfda90c99b5.png" alt="" /></p><p>请求成功返回了<strong>SPRING-DYNAMIC-SERVER</strong>信息，最后在看一眼application.properties文件中的配置信息是否真的被修改了：</p><p><img src="https://cdn.ramostear.com/2019-03-12-19-51-02-ecbec9f0421f4f9ebe153da9358e1d5e.png" alt="" /></p><p>配置文件的属性也被成功的修改，证明之前的猜想验证成功了。</p><blockquote><p>本次内容所描述的方法不适用于以JAR文件启动的Spring Boot应用程序，以WAR包的方式启动应用程序亲测可用。┏ (<sup>ω</sup>)=☞目前该药方副作用未知，如有大牛路过，还望留步指点迷津，不胜感激。</p></blockquote><h2 id="结束语">结束语</h2><p>本次内容记录了自己验证HTTP请求重启Spring Boot应用程序试验的一次经历，文章中所涉及到的内容仅代表个人的一些观点和不成熟的想法，并未将此方法应用到实际的项目中去，如因引用本次内容中的方法应用到实际生产开发工作中所带来的风险，需引用者自行承担因风险带来的后遗症(๑￫ܫ￩)——此药方还有待商榷(O_o)(o_O)。</p>]]>
                    </description>
                    <pubDate>Fri, 08 May 2020 15:33:42 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[传统应用架构如何向微服务架构演进]]>
                    </title>
                    <link>https://www.ramostear.com/2020/04/传统应用架构如何向微服务架构演进.html</link>
                    <description>
                            <![CDATA[<h1 id="第一步停止单体式应用继续膨胀">第一步：停止单体式应用继续膨胀</h1><p>首先，应该采取逐步迁移单体式应用的策略，通过逐步生成微服务新应用，与旧的单体式应用集成，随着时间推移，单体式应用在整个架构中比例逐渐下降直到消失或者成为微服务架构一部分。当开发新功能时不应该为旧单体应用添加新代码，应该是将新功能开发成独立微服务。</p><img src="https://cdn.ramostear.com/20200408-63acbe616598434ba370f19668c5d286.png" style="zoom:150%;display:block;margin:5px auto;" /><p>除了新服务和传统应用，还有两个模块：</p><ul><li>其一是请求路由器，负责处理入口（http）请求，有点像之前提到的API网关。路由器将新功能请求发送给新开发的服务，而将传统请求还发给单体式应用。</li><li>其二是胶水代码（glue code），将微服务和单体应用集成起来，微服务很少能独立存在，经常会访问单体应用的数据。胶水代码，可能在单体应或者为服务或者二者兼而有之，负责数据整合。微服务通过胶水代码从单体应用中读写数据，单体应用需要提供的远程API。</li></ul><p>胶水代码也被称为容灾层（anti-corruption layer），这是因为胶水代码保护微服务全新域模型免受传统单体应用域模型污染。胶水代码在这两种模型间提供翻译功能。</p><p>将新功能以轻量级微服务方式实现由很多优点，例如可以阻止单体应用变的更加无法管理。微服务本身可以开发、部署和独立扩展。<br />然而，这方法并不解决任何单体式本身问题，为了解决单体式本身问题必须深入单体应用​做出改变。</p><h1 id="第二步前后端分离">第二步：前后端分离</h1><p>减小单体式应用复杂度的策略是将表现层和业务逻辑、数据访问层分开。典型的企业应用至少有三个不同元素构成：</p><ul><li>表现层 处理HTTP请求，要么响应一个RESTAPI请求，要么是提供一个基于HTML的图形接口。对于一个复杂用户接口应用，表现层经常是代码重要的部分。</li><li>业务逻辑层 完成业务逻辑的应用核心</li><li>数据访问层 访问基础元素，例如数据库和消息代理。</li></ul><p>前端和后端分离后，表现层逻辑应用远程调用业务逻辑层应用，下图表示迁移前后架构不同：</p><img src="https://cdn.ramostear.com/20200408-c487b07267004e8a9f47d2d75834007c.png" style="zoom:150%;display:block;margin:5px auto;" /><h1 id="第三步抽取模块成为独立服务">第三步：抽取模块成为独立服务</h1><p>一个巨大的复杂单体应用由成十上百个模块构成，每个都是被抽取对象。</p><p>（1）可以先抽取容易的模块，让开发者积累足够经验，这些经验可以为后续模块化工作带来巨大好处。可以抽取经常变化的模块，这样获益较大。<br />（2）可以抽取资源消耗大的模块，例如，将内存数据库抽取出来成为一个微服务会非常有用，可以将其部署在大内存主机上。同样的，将对计算资源很敏感的算法应用抽取出来也是非常有益的，这种服务可以被部署在有很多CPU的主机上。<br />（3）可以抽取有有粗粒度边界的模块，例如，只与其他应用异步同步消息的模块。</p><h2 id="如何抽取模块">如何抽取模块？</h2><p>（1）定义好模块和单体应用之间粗粒度接口<br />由于单体应用需要微服务的数据，反之亦然，因此更像是一个双向API。<br />因为必须在负责依赖关系和细粒度接口模式之间做好平衡，因此开发这种API很有挑战性，尤其对使用域模型模式的业务逻辑层来说更具有挑战，因此经常需要改变代码来解决依赖性问题。</p><p>在本例中，准备抽取使用Y模块的Z模块，X模块引用了Z模块的对象，第一步是定义一套粗粒度APIs，第一个接口应该是被X模块使用的内部接口，用于激活Z模块；第二个接口是被Z模块使用的外部接口，用于激活Y模块。<br />（2）将模块转换成独立服务。<br />​一旦完成粗粒度接口，也就将此模块转换成独立微服务。为了实现，必须写代码使得单体应用和微服务之间通过使用进程间通信（IPC）机制的API来交换信息。</p><p>在本例中，将Z模块整合成一个微服务基础框架，例如服务发现。</p><img src="https://cdn.ramostear.com/20200408-dd8fcd51fb8a4bf28519e84a6a0a281b.png" style="zoom:150%;display:block;margin:5px auto;" /><hr /><p><a href="https://maping930883.blogspot.com/2016/06/architect022.html" target="_blank"><img src="https://img.shields.io/badge/转载-https%3A%2F%2Fmaping930883.blogspot.com%2F2016%2F06%2Farchitect022.html-green" style="zoom: 200%;display:block;margin:5px auto;" ></a></p>]]>
                    </description>
                    <pubDate>Tue, 28 Apr 2020 15:04:33 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[如何部署微服务架构下的应用程序？]]>
                    </title>
                    <link>https://www.ramostear.com/2020/04/howtodeployemicroservice.html</link>
                    <description>
                            <![CDATA[<blockquote><p>一个微服务应用由上百个服务构成，服务采用不同语言和框架。每个服务可以有自己的部署、资源、扩展和监控需求。例如，可以根据服务需求运行若干个服务实例，除此之外，每个实例必须有自己的CPU，内存和I/O资源。尽管很复杂，但是更挑战的是服务部署必须快速、可靠和性价比高。</p></blockquote><h1 id="1单主机多服务实例模式">1.单主机多服务实例模式</h1><p>使用单主机多服务实例模式，需要提供若干台物理或者虚拟机，每台机器上运行多个服务实例。很多情况下，这是传统的应用部署方法。每个服务实例运行一个或者多个主机的well-known端口。</p><img src="https://cdn.ramostear.com/20200408-08eb0023ebe748779ea3ca09388e7b3f.png" style="zoom:150%;margin:5px auto;display:block;" /><p>这种模式有一个参数代表每个服务实例由多少进程构成。例如，可以在Tomcat 上部署一个Java服务实例作为web应用；而一个Node.js服务实例可能有一个父进程和若干个子进程构成。这种模式有另外一个参数定义同一进程组内有多少服务实例运行。例如，可以在同一个Tomcat上运行多个Java web应用，或者在同一个OSGI容器内运行多个OSGI捆绑实例。</p><p>单主机多服务实例模式的缺点之一是服务实例间很少或者没有隔离，除非每个服务实例是独立进程。如果想精确监控每个服务实例资源使用，就不能限制每个实例资源使用。因此有可能造成某个糟糕的服务实例占用了主机的所有内存或者CPU。</p><p>单主机多服务实例模式的缺点之二是运维团队必须知道如何部署的详细步骤。服务可以用不同语言和框架写成，因此开发团队肯定有很多需要跟运维团队沟通事项。其中复杂性增加了部署过程中出错的可能性。</p><h1 id="2-单主机但服务实例模式">2. 单主机但服务实例模式</h1><p>使用单主机单实例模式，每个主机上服务实例都是各自独立的。有两种不同实现模式：单虚拟机单实例和单容器单实例。</p><h2 id="21-单虚拟机但服务实例模式">2.1 单虚拟机但服务实例模式</h2><p>使用单虚拟机单实例模式，一般将服务打包成虚拟机 image。每个服务实例是一个使用此 image 启动的VM。下图展示了此架构：</p><img src="https://cdn.ramostear.com/20200408-4e4fd514d796469a93e63bca1d576b14.png" style="zoom:150%;margin:5px auto;display:block;" /><ul><li>资源利用效率不高。每个服务实例占有整个虚机的资源，包括操作系统。</li><li>IaaS按照VM来收费，而不管虚机是否繁忙。</li><li>部署服务新版本比较慢。虚机镜像因为大小原因创建起来比较慢，同样原因，虚机初始化也比较慢，操作系统启动也需要时间。</li><li>运维团队有大量的创建和管理虚机的工作。</li></ul><h2 id="22单容器单服务实例模式">2.2单容器单服务实例模式</h2><p>使用单容器单服务实例模式时，每个服务实例都运行在各自容器中。容器是运行在操作系统层面的虚拟化机制。一个容器包含若干运行在沙箱中的进程。从进程角度来看，他们有各自的命名空间和根文件系统；可以限制容器的内存和CPU资源。某些容器还具有I/O限制，这类容器技术包括Docker和Solaris Zones。下图展示了这种模式：</p><img src="https://cdn.ramostear.com/20200408-c39cd296e367432bb1b973ffe3ab59c0.png" style="zoom:150%;margin:5px auto;display:block;" /><p>使用这种模式需要将服务打包成容器 image。一个容器image是一个运行包含服务所需库和应用的文件系统。某些容器映像由完整的linux根文件系统组成，其它则是轻量级的。例如，为了部署Java服务，需要创建包含Java运行库的容器映像，也许还要包含Tomcat ，以及编译过的Java应用。</p><p>一旦将服务打包成容器映像，就需要启动若干容器。一般在一个物理机或者虚拟机上运行多个容器，可能需要集群管理系统，例如k8s或者Marathon，来管理容器。集群管理系统将主机作为资源池，根据每个容器对资源的需求，决定将容器调度到那个主机上。</p><p>容器的优点跟虚机很相似，服务实例之间完全独立，可以很容易监控每个容器消耗的资源。跟虚机相似，容器使用隔离技术部署服务。容器管理API也可以作为管理服务的API。<br />然而，跟虚机不一样，容器是一个轻量级技术。容器 image 创建起来很快，容器启动也很快。</p><hr /><p><a href="https://maping930883.blogspot.com/2016/06/architect021.html" target="_blank"><img src="https://img.shields.io/badge/转载-https://maping930883.blogspot.com/2016/06/architect021.html-green" style="zoom: 200%;display:block;margin:5px auto;" ></a></p>]]>
                    </description>
                    <pubDate>Fri, 24 Apr 2020 15:28:40 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[微服务架构下的数据管理]]>
                    </title>
                    <link>https://www.ramostear.com/2020/04/microservicedatamanagement.html</link>
                    <description>
                            <![CDATA[<blockquote><p>使用微服务架构后，数据访问变得非常复杂，这是因为数据都是微服务私有的，唯一可访问的方式就是通过API。</p><p>不同的微服务经常使用不同的数据库，关系型数据库并不一定是最佳选择。某些场景，某个NoSQL数据库可能提供更方便的数据模型，提供更加的性能和可扩展性。例如，某个产生和查询字符串的应用采用例如Elasticsearch的字符搜索引擎。同样的，某个产生社交图片数据的应用可以采用图片数据库，例如，Neo4j。<br />因此，基于微服务的应用一般都使用SQL和NoSQL结合的数据库，也就是被称为polyglot persistence的方法。</p><p>分区的polyglot-persistent架构用于存储数据有许多优势，包括松耦合服务和更佳性能和可扩展性。然而，随之而来的则是分布式数据管理带来的挑战。</p></blockquote><p><em>挑战一：如何完成一笔交易的同时保持多个服务之间数据一致性？</em></p><p>以一个在线B2B商店为例，客户服务维护包括客户的各种信息，例如credit lines。订单服务管理订单，需要验证某个新订单与客户的信用限制没有冲突。<br />在微服务架构下，订单和客户表分别是相对应服务的私有表，如下图所示：</p><img src="https://cdn.ramostear.com/20200408-b281b29bab574edab7b84fc7db387e32.png" style="zoom:150%;display:block;margin:5px auto;" /><p>订单服务不能直接访问客户表，只能通过客户服务发布的API来访问。</p><p><em>挑战二：如何从多个服务中检索数据？</em></p><p>假设有个需求要显示客户和他的订单。而用户服务只接受用户信息，订单服务只支持私有主键来查询订单。</p><p><em>挑战三：多个服务访问同一数据时，如果保证数据的唯一性？</em></p><p>如果多个服务访问同一个数据，schema会更新访问时间，并在所有服务之间进行协调。</p><h1 id="1-事件驱动架构">1. 事件驱动架构</h1><p>事件驱动架构可以解决上述两个挑战。在这种架构中，当某件重要事情发生时，微服务会发布一个事件，例如更新一个业务实体。当订阅这些事件的微服务接收此事件时，就可以更新自己的业务实体，这个更新可能会触发更多的事件发布。</p><h2 id="11-使用事件来实现跨多服务的业务交易">1.1 使用事件来实现跨多服务的业务交易</h2><p>交易一般由一系列步骤构成，每一步骤都由一个更新业务实体的微服务和发布激活下一步骤的事件构成。<br />下图展现如何使用事件驱动方法，在创建订单时检查信用可用度，微服务通过消息代理（Messsage Broker）来交换事件。<br />（1）订单服务创建一个带有NEW状态的Order （订单），发布了一个“Order Created Event（创建订单）”的事件。</p><img src="https://cdn.ramostear.com/20200408-48a932f2bd3c44f28243ecc40e42de53.png" style="zoom:150%;display:block;margin:5px auto;" /><p>（2）客户服务消费Order Created Event事件，为此订单预留信用，发布“Credit Reserved Event（信用预留）”事件。</p><img src="https://cdn.ramostear.com/20200408-fc4efdf3f6e14738b211b68fc2ed8435.png" style="zoom:150%;display:block;margin:5px atuo;" /><p>（3）订单服务消费Credit Reserved Event，改变订单的状态为OPEN。</p><img src="https://cdn.ramostear.com/20200408-0e1bf9c6a11141f895bae1c01eefb006.png" style="zoom:150%;display:block;margin:5px auto;" /><p>更复杂的场景可以引入更多步骤，例如在检查用户信用的同时预留库存等。</p><p>这种模式提供弱确定性，保证数据的最终一致性。其特点是先由每个服务原子性更新数据库和发布事件；然后通过消息Broker确保事件传递至少一次，从而完成跨多个服务的业务交易。</p><h2 id="12-创建一个新的视图维护此视图的服务订阅相关事件并且更新视图">1.2 创建一个新的视图，维护此视图的服务订阅相关事件并且更新视图</h2><p>例如，客户订单视图更新服务（维护客户订单视图）会订阅由客户服务和订单服务发布的事件。当客户订单视图更新服务收到客户或者订单事件，就会更新 客户订单视图数据集。可以使用文档数据库（例如MongoDB）来实现客户订单视图，为每个用户存储一个文档。客户订单视图查询服务负责响应对客户以及最近订单（通过查询客户订单视图数据集）的查询。</p><img src="https://cdn.ramostear.com/20200408-3387fa6ac94b4a1aa08c03304291f8b9.png" style="zoom:150%;display:block;margin:5px auto;" /><blockquote><p>小结：事件驱动架构可以使得交易跨多个服务且提供最终一致性，并且可以使应用维护最终视图；其缺点在于编程模式比较复杂：为了从应用层级失效中恢复，还需要完成补偿性交易，例如，如果信用检查不成功则必须取消订单；另外，应用必须应对不一致的数据，这是因为临时（in-flight）交易造成的改变是可见的，另外当应用读取未更新的最终视图时也会遭遇数据不一致问题。另外一个缺点在于订阅者必须检测和忽略冗余事 件。</p></blockquote><h1 id="2-原子操作">2. 原子操作</h1><p>事件驱动架构会遇到数据库更新和发布事件原子性问题。例如，订单服务必须向ORDER表插入一行，然后发布Order Created event，这两个操作需要原子性。如果更新数据库后，服务瘫掉了，会造成事件未能发布，系统变成不一致状态。确保原子操作的标准方式是使用一个分布式交易，其中包括数据库和消息代理。然而，基于以上描述的CAP理论，这却并不是我们想要的，因为这里我们不能接受最终一致性，必须all or nothing。</p><h2 id="21-使用本地交易发布事件">2.1 使用本地交易发布事件</h2><p>获得原子性的一个方法是对发布事件应用采用 multi-step process involving only local transactions。其技巧在于使用一个EVENT表，此表在存储业务实体数据库中起到消息列表功能。应用发起一个（本地）数据库交易，更新业务实体状态，向EVENT表中插入一个事件，然后提交此次交易。另外一个独立应用进程或者线程查询此EVENT表，向消息代理发布事件，然后使用本地交易标志此事件为已发布，如下图所示：</p><img src="https://cdn.ramostear.com/20200408-29d12ebbe79b46d1a65e974d2933688a.png" style="zoom:150%;display:block;margin:5px auto;" /><p>订单服务向ORDER表插入一行，然后向EVENT表中插入Order Created event，事件发布线程或者进程查询EVENT表，请求未发布事件，发布它们，然后更新EVENT表标志此事件为已发布。</p><blockquote><p>笔者注：我觉得还是有问题，发布事件和更新EVENT表必须要在一个事务里，如何保证？</p></blockquote><h2 id="22-挖掘数据库交易日志">2.2 挖掘数据库交易日志</h2><p>应用更新数据库，在数据库交易日志中产生变化，交易日志挖掘进程或者线程读这些交易日志，将日志发布给消息代理。如下图所见：</p><img src="https://cdn.ramostear.com/20200408-4b4a6f594e6849ba8be2b027c1e6ed16.png" style="zoom:150%;display:block;margin:5px auto;" /><blockquote><p>成功案例：LinkedIn Databus 项目，Databus 挖掘Oracle交易日志，根据变化发布事件，LinkedIn使用Databus来保证系统内各记录之间的一致性。</p></blockquote><p>交易日志挖掘的优点是将发布事件和应用业务逻辑分离，缺点在于交易日志对不同数据库有不同格式，甚至不同数据库版本也有不同格式；而且很难从底层交易日志更新记录转换为高层业务事件。</p><h2 id="23-使用事件源">2.3 使用事件源</h2><p>Event sourcing （事件源）保存业务实体一系列状态改变事件，而不是存储实体现在的状态。应用可以通过重放事件来重建实体现在状态。只要业务实体发生变化，新事件就会添加到时间表中。因为保存事件是单一操作，因此肯定是原子性的。<br />为了理解事件源工作方式，考虑事件实体作为一个例子。订单服务以事件状态改变方式存储一个订单：创建的，已批准的，已发货的，取消的；每个事件包括足够数据来重建订单状态。<br />事件是长期保存在事件数据库中，提供API添加和获取实体事件。事件存储跟之前描述的消息代理类似，提供API来订阅事件。事件存储将事件递送到所有感兴趣的订阅者，事件存储是事件驱动微服务架构的基干。</p><blockquote><p>笔者注：事件数据库是事件微服务使用的数据库，那么创建订单服务和生成事件记录如何保证在一个事务中？</p></blockquote><img src="https://cdn.ramostear.com/20200408-bf651db21d8b40b49af71da36b2029d8.png" style="zoom:150%;margin:5px auto;display:block;" /><hr /><p><a href="https://maping930883.blogspot.com/2016/06/architect020.html" target="_blank"><img src="https://img.shields.io/badge/转载-https://maping930883.blogspot.com/2016/06/architect020.html-green" style="zoom: 200%;display:block;margin:5px auto;" ></a></p>]]>
                    </description>
                    <pubDate>Fri, 17 Apr 2020 15:22:53 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[微服务架构之服务发现和服务注册]]>
                    </title>
                    <link>https://www.ramostear.com/2020/04/serviceregistrationandservicediscovery.html</link>
                    <description>
                            <![CDATA[<blockquote><p>微服务的实例的网络位置都是动态分配的，而且因为扩展、失效和升级等需求，服务实例会经常动态改变，因此，需要一种更加复杂的服务发现机制。</p></blockquote><img src="https://cdn.ramostear.com/20200408-a1a2e3fdd1dd44918c24d29ddf1d0726.png" style="zoom:150%;display:block;margin:5px auto;" /><p>目前有两大类服务发现模式：客户端发现和服务端发现。</p><h1 id="1-客户端发现模式">1. 客户端发现模式</h1><p>当使用客户端发现模式时，客户端负责决定相应服务实例的网络位置，并且对请求实现负载均衡。客户端从一个服务注册服务中查询，其中是所有可用服务实例的库。客户端使用负载均衡算法从多个服务实例中选择出一个，然后发出请求。</p><p>服务实例的网络位置在启动时注册到服务注册表中，并且在服务终止时从注册表中删除。<br />服务实例注册信息一般是使用心跳机制来定期刷新的。</p><img src="https://cdn.ramostear.com/20200408-276e3882a67e48138d38c873fa499de0.png" style="zoom:150%;margin:5px auto;display:block;" /><p>客户端发现模式相对比较直接，除了服务注册表，没有做其它改变。除此之外，因为客户端知道可用服务注册表信息，因此客户端可以通过使用哈希一致性（hashing consistently）变得更加聪明，更加有效的负载均衡。<br />这种模式最大的缺点是需要针对不同的编程语言注册不同的服务，在客户端需要为每种语言开发不同的服务发现逻辑。<br />成功案例：Netflix OSS 使用了客户端发现模式。<br />Netflix Eureka 是一个服务注册表，为服务实例注册管理和查询可用实例提供了REST API接口。Netflix Ribbon 是一种IPC客户端，与Eureka 合作工作实现对请求的负载均衡。</p><h1 id="2-服务端发现模式">2. 服务端发现模式</h1><p>客户端通过负载均衡器向某个服务提出请求，负载均衡器向服务注册表发出请求，将每个请求转发给可用的服务实例。跟客户端发现一样，服务实例在服务注册表中注册或者注销。</p><img src="https://cdn.ramostear.com/20200408-730e95e5b9a7413da2eb8c4fd78a7a0b.png" style="zoom:150%;display:block;margin:5px auto;" /><p>服务端发现模式最大的优点是客户端无需关注发现的细节，客户端只需要简单的向负载均衡器发送请求，实际上减少了编程语言框架需要完成的发现逻辑。</p><h1 id="3-服务注册表">3. 服务注册表</h1><p>服务注册表是服务发现很重要的部分，它是包含服务实例网络地址的数据库。服务注册表需要高可用而且随时更新。客户端可以缓存从服务注册表获得的网络地址。然而，这些信息最终会变得过时，客户端也无法发现服务实例。因此，服务注册表由若干使用复制协议保持同步的服务器构成。****<br />****<br />服务注册表例子：</p><ul><li>etcd – 是一个高可用，分布式的，一致性的，键值表，用于共享配置和服务发现。两个著名案例包括Kubernetes和Cloud Foundry。</li><li>consul – 是一个用于发现和配置的服务。提供了一个API允许客户端注册和发现服务。Consul可以用于健康检查来判断服务可用性。</li><li>Apache ZooKeeper – 是一个广泛使用，为分布式应用提供高性能整合的服务。</li></ul><h1 id="4-服务注册方式">4. 服务注册方式</h1><h2 id="41-自注册方式">4.1 自注册方式</h2><p>当使用自注册模式时，服务实例负责在服务注册表中注册和注销，并且，服务实例也要发送心跳来保证注册信息不会过时。下图描述了这种架构：</p><img src="https://cdn.ramostear.com/20200408-0c02bf95f7114e9db8359388d359250b.png" style="zoom:150%;display:block;margin:5px auto;" /><p>自注册模式的优点是相对简单，不需要其他系统功能。<br />自注册模式的缺点是把服务实例跟服务注册表联系起来，必须在每种编程语言和框架内部实现注册代码。</p><h2 id="42-第三方注册方式">4.2 第三方注册方式</h2><p>当使用第三方注册模式时，服务实例并不负责向服务注册表注册，而是由另外一个系统模块，叫做服务管理器，负责注册。<br />服务管理器通过查询部署环境或订阅事件来跟踪运行服务的改变。当管理器发现一个新可用服务，会向注册表注册此服务。服务管理器也负责注销终止的服务实例。下图是这种模式的架构图。</p><img src="https://cdn.ramostear.com/20200408-5019a53d98d542289b1366c16b7c1f80.png" style="zoom:150%;display:block;margin:5px auto;" /><hr /><p><a href="https://maping930883.blogspot.com/2016/06/architect019.html" target="_blank"><img src="https://img.shields.io/badge/转载-https://maping930883.blogspot.com/2016/06/architect019.html-green" style="zoom: 200%;display:block;margin:5px auto;" ></a></p>]]>
                    </description>
                    <pubDate>Sat, 11 Apr 2020 15:20:01 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[客户端与微服务通信:API Gateway]]>
                    </title>
                    <link>https://www.ramostear.com/2020/04/client-and-microservice-apigateway.html</link>
                    <description>
                            <![CDATA[<h1 id="1手机产品页面">1.手机产品页面</h1><p>这个产品页面展示了非常多的信息：</p><ul><li>产品基本信息（名字、描述和价格）</li><li>购物车中的物品数</li><li>下单历史</li><li>用户评论</li><li>低库存警告</li><li>快递选项</li><li>各式各样的推荐，包括经常跟这个物品一起被购买的产品、购买该物品的其他顾客购买的产品以及购买该产品的顾客还浏览了哪些产品。</li><li>可选的购物选项</li></ul><img src="https://cdn.ramostear.com/20200408-23d197a9346e4bb39db683db8f5c0a7b.png" style="zoom:150%;margin:5px auto;display:block;" /><p>如果采用一个单体式应用架构，一个移动客户端将会通过一个REST请求（GET api.company.com/productdetails/productId）来获取这些数据。然后通过一个负载均衡将请求分发到多个应用实例之一。应用将查询各种数据库并返回请求给客户端。如果采用微服务架构，最终页上的数据会分布在不同的微服务上。比如：</p><ul><li>购物车服务 -- 购物车中的物品数</li><li>下单服务 -- 下单历史</li><li>分类服务 -- 基本产品信息，如名字、图片和价格</li><li>评论服务 -- 用户评论</li><li>库存服务 -- 低库存警告</li><li>快递服务 -- 快递选项、截止时间、来自不同快递API的成本计算</li><li>推荐服务 -- 推荐产品</li></ul><h1 id="2客户端与微服务通信">2.客户端与微服务通信</h1><h2 id="21-客户端与微服务直接通信">2.1 客户端与微服务直接通信</h2><p>从理论上讲，一个客户端可以直接给多个微服务中的任何一个发起请求。每一个微服务都会有一个对外服务端，比如：<a href="https://serviceName.api.company.name">https://serviceName.api.company.name</a>。这个 URL 可能会映射到微服务的负载均衡上，它再转发请求到具体节点上。为了搜索产品细节，移动端需要向上述微服务逐个发请求。不幸的是，这个方案有很多困难和限制。</p><p>（1）客户端的需求量与每个微服务暴露的细粒度 API 数量的不匹配。如图中，客户端需要7次单独请求。在更复杂的场景中，可能会需要更多次请求。例如，亚马逊的产品最终页要请求数百个微服务。<br />（2）客户端直接请求微服务的协议可能并不是web友好型。一个服务可能是用 Thrift的 RPC 协议，而另一个服务可能是用 AMQP 消息协议。它们都不是浏览或防火墙友好的，并且最好是内部使用。应用应该在防火墙外采用类似 HTTP 或者 WebSocket 协议。<br />（3）很难重构微服务。随着时间的推移，我们可能需要改变系统微服务目前的切分方案。例如，我们可能需要将两个服务合并或者将一个服务拆分为多个。但是，如果客户端直接与微服务交互，那么这种重构就很难实施。</p><p>鉴于上述三个问题，客户端直接与服务器端通信的方式很少在实际中使用。</p><img src="https://cdn.ramostear.com/20200408-bb8bd49671104ca5a4e394ef27208c6a.png" style="zoom:150%;display:block;margin:5px auto;" /><h2 id="22-采用api-gateway">2.2 采用API Gateway</h2><p>API Gateway 是一个服务器，也可以说是进入系统的唯一节点。跟面向对象设计模式中的Facade模式很像。API Gateway 负责请求转发、请求合成和协议转换，封装内部系统的架构，并且提供API给各个客户端。它还有其他功能，如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理、通过返回缓存或者默认值的方式来掩盖后端服务的错误等。下图展示了一个适应当前架构的API Gateway。</p><img src="https://cdn.ramostear.com/20200408-0dbd276c692e4ca59098da6bd720ec7c.png" style="zoom:150%;margin:5px auto;display:block;" /><p>API Gateway负责请求转发、合成和协议转换。所有来自客户端的请求都要先经过API Gateway，然后路由这些请求到对应的微服务。API Gateway将经常通过调用多个微服务来处理一个请求以及聚合多个服务的结果。它可以在web协议与内部使用的非Web友好型协议间进行转换，如HTTP协议、WebSocket协议。</p><p>API Gateway可以提供给客户端一个定制化的API。它暴露一个粗粒度API给移动客户端。以产品最终页这个使用场景为例。API Gateway提供一个服务提供点（/productdetails?productid=xxx）使得移动客户端可以在一个请求中检索到产品最终页的全部数据。API Gateway通过调用多个服务来处理这一个请求并返回结果，涉及产品信息、推荐、评论等。</p><p>成功案例：Netfix API Gateway。<br />Netflix流服务提供数百个不同的微服务，包括电视、机顶盒、智能手机、游戏系统、平板电脑等。采用一个API Gateway来提供容错性高的API，针对不同类型设备有相应代码。一个适配器处理一个请求平均要调用6到8个后端服务。Netflix API Gateway每天处理数十亿的请求。</p><h1 id="3-如可设计和开发api-gateway">3. 如可设计和开发API Gateway</h1><h2 id="31-高可用性">3.1 高可用性</h2><p>API Gateway 是一个高可用的组件，必须要开发、部署和管理。它可能成为系统的一个瓶颈。开发者必须更新API Gateway来提供新服务提供点来支持新暴露的微服务。更新API Gateway时必须越轻量级越好。否则，开发者将因为更新Gateway而排队列。</p><h2 id="32-性能和可扩展性">3.2 性能和可扩展性</h2><p>API Gateway 必须要支持同步、非阻塞I/O。<br />如果是基于Java开发，可以采用基于NIO技术的框架，比如：Netty，Vertx，Spring Reactor或者JBoss Undertow。<br />如果是基于Node.js，它是一个在Chrome的JavaScript引擎基础上建立的平台。<br />一个可选的方案是Nginx Plus。Nginx Plus提供一个成熟的、可扩展的、高性能web服务器和反向代理，它们均容易部署、配置和二次开发。Nginx Plus可以管理授权、权限控制、负载均衡、缓存并提供应用健康检查和监控。</p><h2 id="33-采用反应性编程模型">3.3 采用反应性编程模型</h2><p>对于有些请求，API Gateway可以通过直接路由请求到对应的后端服务上的方式来处理。对于另外一些请求，它需要调用多个后端服务并合并结果来处理。对于一些请求，例如产品最终页面请求，发给后端服务的请求是相互独立的。为了最小化响应时间，API Gateway应该并发的处理相互独立的请求。但是，有时候请求之间是有依赖的。API Gateway可能需要先通过授权服务来验证请求，然后在路由到后端服务。类似的，为了获得客户的产品愿望清单，需要先获取该用户的资料，然后返回清单上产品的信息。这样的一个API 组件是Netflix Video Grid。</p><p>利用传统的同步回调方法来实现API合并的代码会使得你进入回调函数的噩梦中。这种代码将非常难度且难以维护。一个优雅的解决方案是采用反应性编程模式来实现。类似的反应抽象实现有Scala的Future，Java8的CompletableFuture和JavaScript的Promise。基于微软.Net平台的有Reactive Extensions(Rx)。Netflix为JVM环境创建了RxJava来使用他们的API Gateway。同样地，JavaScript平台有RxJS，可以在浏览器和Node.js平台上运行。采用反应编程方法可以帮助快速实现一个高效的API Gateway代码。</p><h2 id="34-服务调用">3.4 服务调用</h2><p>一个基于微服务的应用是一个分布式系统，并且必须采用线程间通信的机制。有两种线程间通信的方法。一种是采用异步机制，基于消息的方法。这类的实现方法有JMS和AMQP。另外的，例如ZeroMQ 属于服务间直接通信。还有一种线程间通信采用同步机制，例如Thrift和HTTP。事实上一个系统会同时采用同步和异步两种机制。由于它的实现方式有很多种，因此API Gateway就需要支持多种通信方式。</p><h2 id="35-服务发现">3.5 服务发现</h2><p>API Gateway需要知道每一个微服务的IP和端口。在传统应用中，你可能会硬编码这些地址，但是在现在云基础的微服务应用中，这将是个简单的问题。基础服务通常会采用静态地址，可以采用操作系统环境变量来指定。但是，探测应用服务的地址就没那么容易了。应用服务通常动态分配地址和端口。同样的，由于扩展或者升级，服务的实例也会动态的改变。因此，API Gateway需要采用系统的服务发现机制，要么采用服务端发现，要么是客户端发现。如果采用客户端发现服务，API Gateway必须要去查询服务注册处，也就是微服务实例地址的数据库。</p><h2 id="36-容错处理">3.6 容错处理</h2><p>在实现API Gateway过程中，另外一个需要考虑的问题就是部分失败。这个问题发生在分布式系统中当一个服务调用另外一个服务超时或者不可用的情况。API Gateway不应该被阻断并处于无限期等待下游服务的状态。但是，如何处理这种失败依赖于特定的场景和具体服务。例如，如果是在产品详情页的推荐服务模块无响应，那么API Gateway应该返回剩下的其他信息给用户，因为这些信息也是有用的。推荐部分可以返回空，也可以返回固定的顶部10个给用户。但是，如果是产品信息服务无响应，那么API Gateway就应该给客户端返回一个错误。</p><p>在缓存有效的时候，API Gateway应该能够返回缓存。例如，由于产品价格变化并不频繁，API Gateway在价格服务不可用时应该返回缓存中的数值。这类数据可以由API Gateway自身来缓存，也可以由Redis或Memcached这类外部缓存实现。通过返回缓存数据或者默认数据，API Gateway来确保系统错误不影响到用户体验。</p><p>Netflix Hystrix对于实现远程服务调用代码来说是一个非常好用的库。Hystrix记录那些超过预设定的极限值的调用。它实现了circuit break模式，使得可以将客户端从无响应服务的无尽等待中停止。如果一个服务的错误率超过预设值，Hystrix将中断服务，并且在一段时间内所有请求立刻失效。Hystrix可以为请求失败定义一个fallback操作，例如读取缓存或者返回默认值。如果你在用JVM，就应该考虑使用Hystrix。如果你采用的非JVM环境，那么应该考虑采用类似功能的库。</p><hr /><p><a href="https://maping930883.blogspot.com/2016/06/architect017api-gateway.html" target="_blank"><img src="https://img.shields.io/badge/转载-https%3A%2F%2Fmaping930883.blogspot.com%2F2016%2F06%2Farchitect017api--gateway.html-green" style="zoom: 200%;display:block;margin:5px auto;" ></a></p>]]>
                    </description>
                    <pubDate>Wed, 08 Apr 2020 15:16:32 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[微服务架构简介]]>
                    </title>
                    <link>https://www.ramostear.com/2020/04/wei-fu-wu-jia-gou-jian-jie.html</link>
                    <description>
                            <![CDATA[<h1 id="什么式微服务">什么式微服务？</h1><p>微服务是指开发一个单个、小型、具备有业务功能的服务。其特点如下：</p><ul><li>每个服务运行在自己的进程中，通过轻量的通讯机制（基于HTTP/REST API）联系。<br />其中，使用 REST API 更好些，因为 REST本身就是 Web，而不是基于 Web：“Be of the web, not behind the web”。</li><li>每个服务可以使用不同的编程语言编写。</li><li>每个服务提供一个模块边界，服务上下文。</li><li>每个服务都有一个用RPC-或者消息驱动API定义清楚的边界。</li><li>每个服务能够通过自动化方式独立地部署在单个或多个服务器上。</li><li>每个服务能够通过自动化方式弹性扩展伸缩。</li><li>每个服务能够独立更换、独立升级，而不影响其它服务。</li><li>每个微服务都有自己的存储能力，可以有自己的数据库，也可以有统一的数据库。</li></ul><h1 id="传统应用架构与微服务架构之间的区别">传统应用架构与微服务架构之间的区别</h1><img src="https://cdn.ramostear.com/20200408-f62fa42b6fac4a72835f2a77380f02ea.png" style="zoom:150%;display:block;margin:5px atuo;" /><p>单体式应用的需求的一个小改变会导致整个系统重新构建重新部署，难以让变化只影响在一个模块内，进行扩展伸缩时也只能整个系统扩展，而不能针对其一部分扩展其资源能力。</p><p>单体式应用在不同模块发生资源冲突时，扩展将会非常困难。比如，一个模块对CPU敏感，另外一个内存数据库模块对内存敏感。然而，由于这些模块部署在一起，因此不得不在硬件选择上做一个妥协。</p><p>单体式应用另外一个问题是可靠性。因为所有模块都运行在一个进程中，任何一个模块中的一个BUG，比如内存泄露，将会有可能弄垮整个进程。除此之外，因为所有应用实例都是唯一的，这个BUG 将会影响到整个应用的可靠性。</p><h1 id="康威定律设计系统的组织最终产生的设计会反映出组织内部和组织之间的沟通结构">康威定律：设计系统的组织，最终产生的设计会反映出组织内部和组织之间的沟通结构</h1><blockquote><p>“organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations”</p><p style="text-align:right;">—— Melvin Conway 1960’s</p></blockquote><p>传统的MVC架构，会导致任何一个需求改变都必须要跨团队交流。</p><img src="https://cdn.ramostear.com/20200408-837e017d95a249b4b42dfe13148fe2a0.png" style="zoom:150%;display:block;margin:5px auto;" /><p>微服务这种极度松耦合的架构需要极度松耦合的组织团队。按业务而不是按功能划分服务，每个服务可以使用不同的技术堆栈，每个服务是跨功能的，团队成员是拥有全部的技能。</p><img src="https://cdn.ramostear.com/20200408-aecb9644964e4785858e8b0ed13b99d9.png" style="zoom:150%;margin:5px auto;display:block;" /><h1 id="微服务与soa的区别">微服务与SOA的区别</h1><p>从表面上看，微服务架构模式有点像SOA，它们都是由多个服务构成。<br />从业务角度看，微服务是具备有业务功能的服务，而SOA中Web服务可能是个非业务功能的原子服务。<br />从技术角度看，微服务架构模式是一个不包含Web服务（WS-）和 ESB服务的SOA。微服务应用采用简单轻量级协议，比如REST，而不是WS-，在微服务内部避免使用ESB以及ESB类似功能。微服务架构模式也 拒绝使用canonical schema等SOA概念。</p><hr /><p><a href="https://maping930883.blogspot.com/2016/06/architect015.html" target="_blank"><img src="https://img.shields.io/badge/转载-https://maping930883.blogspot.com/2016/06/architect015.html-green" style="zoom: 200%;display:block;margin:5px auto;" ></a></p>]]>
                    </description>
                    <pubDate>Wed, 01 Apr 2020 15:10:28 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[何为SaaS(软件即服务外)？]]>
                    </title>
                    <link>https://www.ramostear.com/2019/10/whatissoftwareasaservice.html</link>
                    <description>
                            <![CDATA[<h1 id="何为saas软件即服务">何为SaaS(软件即服务)？</h1><p style="text-align:right">SaaS(软件即服务)平台架构设计指南</p><h2 id="1介绍">1、介绍</h2><p>从计算机诞生开始，就伴随着计算机应用程序的演变。简短的回顾历史，我们可以清楚的看到应用程序发生的巨大变化。上世纪70年代中期，随着个人PC机的爆炸式增长以及程序员的崛起，让计算机的计算能力得到了大跨越的提升，个人PC机上可以运行非常复杂的应用程序。</p><p>进入上世纪80年代，随着Bulletin Board System（简称：BBS）电子公告板系统的兴起，它可以为广大PC机用户提供基本的在线服务，如在线聊天、电子邮件、消息发送和文件下载。由于受到那个时代计算机网络传输速度的限制，在线服务的响应速度慢，交互体验差是最大的通病。</p><p>进入90年代中后期，随着万维网的出现，计算机的计算能开始进入快速提升阶段，加之网络基础设施的持续完善，计算机网络技术也随之发展起来，这让Web网站可以提供功能多元化和更为复杂的在线服务，直到今天，我们所看到的互联网（或云）开发的在线服务应用程序。</p><p>在这段计算机技术快速成长的时间里，计算机软件到底发生了哪些变化？从历史的发展中，我们可以看到，应用程序本身没有发生本质的变化（程序=数据结构+算法），变化的是软件的供需方式发生了改变。现在，应用程序消费者不需要再在他们的PC机上下载和安装特定的应用程序，即可获得软件所提供的计算服务。在云计算技术的支持下，消费者（企业或个人）只需要使用Web工具（浏览器）访问并登录软件提供商的Web系统，通过简单的配置，就可以获得自己所需应用程序服务。这种通过网络即可使用软件的服务，即使SaaS（软件即服务）。</p><p><img src="https://cdn.ramostear.com/2019-05-28-07-06-12-88d5bcd927674ddcb485628266a53dfb.jpg" alt="" /></p><center>图 1-1 2015中国SaaS生态</center><p>在本篇文章中，我们将着重介绍SaaS架构设计,并围绕WHAT（是什么？）、WHY（为什么？）、WHERE（在哪里？）和HOW（怎么样？）这四个问题，对以下的几点进行阐述：</p><p><img src="https://cdn.ramostear.com/2019-05-28-07-07-01-de27b9f3bf92433e86f85f9cbf71108f.png" alt="" /></p><center>图 1-2 文章结构</center><ul><li>1、什么是SaaS平台？</li><li>2、为什么需要使用SaaS平台架构？</li><li>3、SaaS平台主要的特性和优势有哪些？</li><li>4、SaaS平台适合在什么领域进行实施？</li><li>5、SaaS平台有哪些先天性的缺陷？</li><li>6、SaaS平台有哪些核心的组件？</li><li>7、实施SaaS架构设计时的注意事项有哪些？</li></ul><h2 id="2什么是saas平台">2、什么是SaaS平台？</h2><p><img src="https://cdn.ramostear.com/2019-05-28-07-06-26-4dbd6523bf2d4d52b5c9a78ef64e2ad6.png" alt="" /></p><center>图 2-1 SaaS组成结构</center><p>在你决定实施SaaS品台架构设计前，你有必要先了解SaaS平台是什么。从宏观的角度来看，SaaS是一种软件应用程序交付方式，软件提供商集中化托管一个或多个软件应用程序，并通过互联网向租户体用这些软件应用程序。从分类上看，SaaS（软件即服务）也是云计算重要的一部分。目前国内主流的云服务提供商如阿里云、百度云、腾讯云等，为广大用户提供了不同业务需求的云服务，它们大致可以分为以下几类：</p><ul><li>1、基础设施即服务：如CPU、Network、Disk和Memory等</li><li>2、平台即服务：如阿里云服务器和云数据库等</li><li>3、软件即服务：阿里短信、阿里邮箱等</li><li>4、数据即服务：如阿里云对象存储，七牛云存储等</li><li>5、其他软件服务：机器学习、人工智能等</li></ul><p>SaaS应用程序的任何更新或者修复漏洞操作都是由软件提供商负责实施和处理的，由于租户是通过互联网获取软件服务，所以租户端无需下载任何的升级包或者修复补丁，是一种开箱即获取最新软件产品的服务方式。</p><p>通过对什么是SaaS的介绍，接下来，我们了解一下选择SaaS作为软件架构来设计产品的一些理由。</p><h2 id="3为什么选择saas">3、为什么选择SaaS?</h2><p>我们将从不同的角度来阐述几个为什么选择SaaS的理由。透过对这些因素的分析，为你是否需要将自己的软件SaaS化提供一定的参考依据。</p><h3 id="31消费者角度">3.1、消费者角度</h3><p>获取软件服务的方式足够简单，SaaS也许是迄今为止使用软件最简单的方式之一，租户只需要动动鼠标和键盘，即可在几小时甚至几分钟内获得一个大型的软件服务。相比于传统使用软件的方式，租户省去了研发、部署、运维等一系列繁复的过程，且获得软件的时间和费用成本都大幅度降低。</p><h2 id="32商业角度">3.2、商业角度</h2><p>SaaS可以体用跨地域、跨平台的软件服务。与此同时，软件服务商可以统一对软件进行版本管理，这将带来以下几点好处（包括但不限于）：</p><ul><li>1、缩短产品上线时间：多端适配，统一版本，统一更新</li><li>2、降低维护成本：不需要同时维护多个版本的软件实例，运维压力减小</li><li>3、容易升级：由于版本得到有效控制，一次升级，即可覆盖所有租户端</li></ul><h2 id="4saas的特性和优势">4、SaaS的特性和优势</h2><p>我们将SaaS应用程序与传统的桌面应用程序做一个水平的对比，部署一个SaaS产品将可以获得以下的几点优势。</p><h3 id="41简单">4.1、简单</h3><p>SaaS化的产品通过互联网向租户提供软件服务，随着Web技术（如jQuery、Node.js）的进步，Web页面的交互体验度大幅度提升，交互更流畅、更人性化。与传统的桌面应用程序的人机交互效果相差无几。</p><h3 id="42经济实惠">4.2、经济实惠</h3><p>SaaS化产品可以为租户提供弹性的付费方案，如按日、按月、按年、按使用人数或者按使用量进行计费，它将给租户提供更经济的使用软件的财务预算表。</p><h3 id="43安全">4.3、安全</h3><p>使用SaaS产品无需担心数据安全问题，这好比将钱存入银行一样安全。相较于企业内部部署的软件系统而言，SaaS产品具备更高的安全保障能力，因为软件提供商具有更多软件安全防护的技术资源、人力资源和财政资源。</p><h3 id="44兼容性">4.4、兼容性</h3><p>与传统软件相比、SaaS软件的兼容性更好，它没有传统软件的多本版维护问题和操作系统兼容问题。在SaaS软件中，租户用户在使用软件的过程中，几乎上感觉不到软件发生了改变。当租户用户登录到系统上时，就已经获得了最新版本的软件。</p><h2 id="5saas软件的适用范围">5、SaaS软件的适用范围</h2><p>SaaS产品具有广泛的适应范围，特别是与其他云产品（如IaaS(基础设施即服务)和PaaS(平台即服务)）配合使用时这种能力表现尤为突出，例如阿里云之类的云计算技术允许你配置可托管的Web站点、数据库服务器等。你只需要打开浏览器并登录到阿里云控制台，通过操作对应的控制面板，即可获得相关的软件服务。</p><p>从理论上讲，SaaS可以将任何的软件SaaS，下面列举一些通用的分类供大家参考：</p><ul><li>1、Office在线办公类SaaS产品</li><li>2、电子邮件和即时消息类SaaS产品</li><li>3、社交媒体类SaaS产品</li><li>4、第三方API类SaaS产品</li><li>5、安全和访问控制类SaaS产品</li><li>6、机器学习类SaaS产品</li><li>7、人工智能类SaaS产品</li><li>8、地理位置服务类SaaS产品</li><li>9、数据流和数据检索类SaaS产品</li></ul><h2 id="6saas产品的天生缺陷">6、SaaS产品的天生缺陷</h2><p><img src="https://cdn.ramostear.com/2019-05-28-07-06-47-1f5f51ee9eca4d00a1b7f426bfbf4aa7.png" alt="" /></p><center>图 6-1 SaaS产品的缺点</center><p>从上图我们可以直观的看到，SaaS产品与生俱来的几个缺陷，接下来我们将逐一进行描述。</p><h3 id="61软件控制权">6.1、软件控制权</h3><p>与企业内部部署的软件不同，由于SaaS软件被击中托管在服务提供商的Web服务器中，所以租户无法控制所有的软件应用程序，SaaS化的软件比企业自行部署的软件获得的控制权更少，租户可操作的自定义控制权极度有限。</p><h3 id="62消费者基数小">6.2、消费者基数小</h3><p>由于SaaS软件是将一套应用程序共享给一个或者多个租户共同使用，这种共享的消费方式还未被大多数的消费者所接受。同时，受制于市场环境的影响，目前还有大多数的软件还未SaaS化。</p><h3 id="63性能瓶颈">6.3、性能瓶颈</h3><p>共享应用程序必然会带来服务器性能的下降、如计算速度、网络资源、I/O读写等都将面临严峻的考验。在性能方面，企业内部部署的“独享模式”的应用程序比SaaS软件的“共享模式”略胜一筹。</p><h3 id="64安全问题">6.4、安全问题</h3><p>当租户在选择一款SaaS产品时，产品的安全性将会被放置在第一位进行考虑。如数据的隔离、敏感数据的加密、数据访问权限控制、个人隐私等问题。在2018年5月25日，GDPR(General Data Protection Regulation)《通用数据保护条例》出现之后，越来越多的人开始重视数据安全问题。如何最大程度的打消租户的这一顾虑，需要服务提供商加强对自身信誉度的提升，以赢得租户的信赖。</p><h2 id="7saas产品的核心组件">7、SaaS产品的核心组件</h2><p>不同类型的SaaS产品，由于要面对不同的用户愿景，可能在功能和业务上会有所不同，但任何一个SaaS产品，都具备以下几个共同的核心组件。</p><p><img src="https://cdn.ramostear.com/2019-05-28-07-07-45-19118cb442464b0fb46467478730b5d7.png" alt="" /></p><center>图 7-1 SaaS 核心组件</center><h3 id="71安全组件">7.1、安全组件</h3><p>在SaaS产品中，系统安全永远是第一位需要考虑的事情，如何保障租户数据的安全，是你首要的事情。这如同银行首选需要保障储户资金安全一样。安全组件就是统一的对SaaS产品进行安全防护，保障系统数据安全。</p><h3 id="72数据隔离组件">7.2、数据隔离组件</h3><p>安全组件解决了用户数据安全可靠的问题，但数据往往还需要解决隐私问题，各企业之间的数据必须相互不可见，即相互隔离。在SaaS产品中，如何识别、区分、隔离个租户的数据时你在实施SaaS平台架构设计时需要考虑的第二个问题。</p><h3 id="73可配置组件">7.3、可配置组件</h3><p>尽管SaaS产品在设计之初就考虑了大多数通用的功能，让租户开箱即用，但任然有为数不少的租户需要定制服务自身业务需求的配置项，如UI布局、主题、标识（Logo）等信息。正因为无法抽象出一个完全通用的应用程序，所以在SaaS产品中，你需要提供一个可用于自定义配置的组件。</p><h3 id="74可扩展组件">7.4、可扩展组件</h3><p>随着SaaS产品业务和租户数量的增长，原有的服务器配置将无法继续满足新的需求，系统性能将会与业务量和用户量成反比。此时，SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术，在多个服务器上部署多个软件运行示例并提供相同的软件服务，以此实现水平扩展SaaS产品的整体服务性能。为了实现可扩展能力，就需要SaaS展示层的代码与业务逻辑部分的代码进行分离，两者独立部署。例如使用VUE+微服务构建前后端分离且可水平进行扩展的分布式SaaS应用产品。对于可扩展，还有另外一种方式，即垂直扩展，其做法比较简单，也比较粗暴：通过增加单台服务器的配置，如购买性能更好的CUP、存储更大的内存条、增大带宽等措施，让服务器能够处理更多的用户请求。但此做法对于提升产品性能没有质的改变，且成本很高。</p><h3 id="750停机时间升级产品">7.5、0停机时间升级产品</h3><p>以往的软件在升级或者修复Bug是，都需要将运行的程序脱机一段时间，等待升级或修复工作完成后，再重新启动应用程序。而SaaS产品则需要全天候保障服务的可用性。这就需要你考虑如何实现在不重启原有应用程序的情况下，完成应用程序的升级修复工作。</p><h3 id="76多租户组件">7.6、多租户组件</h3><p>要将原有产品SaaS化，就必须提供多租户组件，多租户组件是衡量一个应用程序是否具备SaaS服务能力的重要指标之一。SaaS产品需要同时容纳多个租户的数据，同时还需要保证各租户之间的数据不会相互干扰，保证租户中的用户能够按期望索引到正确的数据，多租户组件是你必须要解决的一个问题。其余的组件都将围绕此组件展开各自的业务。</p><h2 id="总结">总结</h2><p>本文将软件应用程序的发展历程作为切入点，并围绕WHAT(是什么？)、WHY(为什么？)、WHERE(在哪些领域实施？)和HOW(怎么样？)这四个问题对SaaS展开了介绍。文中详细的阐述了基于SaaS架构的软件设计需要注意的问题，并分析了SaaS产品的特性、有点、缺点。最后还介绍了基于SaaS架构的软件产品应该具备的几个核心组件以及他们各自的作用。希望本次能够让你对SaaS平台架构有一个全面的了解，并且在你准备实施SaaS平台架构设计前能够提供一些价值的参考信息。</p>]]>
                    </description>
                    <pubDate>Thu, 10 Oct 2019 15:47:27 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[基于Spring Boot实现多租户SaaS平台示例]]>
                    </title>
                    <link>https://www.ramostear.com/2019/09/springbootsaas.html</link>
                    <description>
                            <![CDATA[<blockquote><p>本次教程所涉及到的源码已上传至<a href="https://github.com/ramostear/una-saas-toturial">Github</a>,如果你不需要继续阅读下面的内容，你可以直接点击此链接获取源码内容。<a href="https://github.com/ramostear/una-saas-toturial">https://github.com/ramostear/una-saas-toturial</a></p></blockquote><h2 id="1-概述">1. 概述</h2><p>笔者从2014年开始接触SaaS（Software as a Service），即多租户（或多承租）软件应用平台；并一直从事相关领域的架构设计及研发工作。机缘巧合，在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究，从中收获颇多。最早接触SaaS时，国内相关资源匮乏，唯一有的参照资料是《互联网时代的软件革命：SaaS架构设计》（叶伟等著）一书。最后课题的实现是基于OSGI（Open Service Gateway Initiative）Java动态模块化系统规范来实现的。</p><p>时至今日，五年的时间过去了，软件开发的技术发生了巨大的改变，笔者所实现SaaS平台的技术栈也更新了好几波，真是印证了那就话：“山重水尽疑无路，柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑，以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统，决定写一篇文章聊一聊关于SaaS的硬核技术。</p><p>说起SaaS，它只是一种软件架构，并没有多少神秘的东西，也不是什么很难的系统，我个人的感觉，SaaS平台的难度在于商业上的运营，而非技术上的实现。就技术上来说，SaaS是这样一种架构模式：它让多个不同环境的用户使用同一套应用程序，且保证用户之间的数据相互隔离。现在想想看，这也有点共享经济的味道在里面。</p><p>笔者在这里就不再深入聊SaaS软件成熟度模型和数据隔离方案对比的事情了。今天要聊的是使用Spring Boot快速构建独立数据库/共享数据库独立Schema的多租户系统。我将提供一个SaaS系统最核心的技术实现，而其他的部分有兴趣的朋友可以在此基础上自行扩展。</p><h2 id="2-尝试了解多租户的应用场景">2. 尝试了解多租户的应用场景</h2><p>假设我们需要开发一个应用程序，并且希望将同一个应用程序销售给N家客户使用。在常规情况下，我们需要为此创建N个Web服务器（Tomcat）,N个数据库（DB），并为N个客户部署相同的应用程序N次。现在，如果我们的应用程序进行了升级或者做了其他任何的改动，那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来，如果业务开始增长，客户由原来的N个变成了现在的N+M个，我们将面临N个应用程序和M个应用程序版本维护，设备维护以及成本控制的问题。运维几乎要哭死在机房了...</p><p>为了解决上述的问题，我们可以开发多租户应用程序，我们可以根据当前用户是谁，从而选择对应的数据库。例如，当请求来自A公司的用户时，应用程序就连接A公司的数据库，当请求来自B公司的用户时，自动将数据库切换到B公司数据库，以此类推。从理论上将没有什么问题，但我们如果考虑将现有的应用程序改造成SaaS模式，我们将遇到第一个问题：如果识别请求来自哪一个租户？如何自动切换数据源？</p><h2 id="3-维护识别和路由租户数据源">3. 维护、识别和路由租户数据源</h2><p>我们可以提供一个独立的库来存放租户信息，如数据库名称、链接地址、用户名、密码等，这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决，下面列举几个常用的方式：</p><ul><li>1.可以通过域名的方式来识别租户：我们可以为每一个租户提供一个唯一的二级域名，通过二级域名就可以达到识别租户的能力，如tenantone.example.com,tenant.example.com；tenantone和tenant就是我们识别租户的关键信息。</li><li>2.可以将租户信息作为请求参数传递给服务端，为服务端识别租户提供支持，如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。</li><li>3.可以在请求头（Header）中设置租户信息，例如JWT等技术，服务端通过解析Header中相关参数以获得租户信息。</li><li>4.在用户成功登录系统后，将租户信息保存在Session中，在需要的时候从Session取出租户信息。</li></ul><p>解决了上述问题后，我们再来看看如何获取客户端传入的租户信息，以及在我们的业务代码中如何使用租户信息（最关键的是DataSources的问题）。</p><p>我们都知道，在启动Spring Boot应用程序之前，就需要为其提供有关数据源的配置信息（有使用到数据库的情况下）,按照一开始的需求，有N个客户需要使用我们的应用程序，我们就需要提前配置好N个数据源（多数据源）,如果N&lt;50,我认为我还能忍受，如果更多，这样显然是无法接受的。为了解决这一问题，我们需要借助Hibernate 5提供的动态数据源特性，让我们的应用程序具备动态配置客户端数据源的能力。简单来说，当用户请求系统资源时，我们将用户提供的租户信息（tenantId）存放在ThreadLoacal中，紧接着获取TheadLocal中的租户信息，并根据此信息查询单独的租户库，获取当前租户的数据配置信息，然后借助Hibernate动态配置数据源的能力，为当前请求设置数据源，最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息（租户数据库配置库），其余的数据源动态查询配置。接下来，我们将快速的演示这一功能。</p><h2 id="4-项目构建">4. 项目构建</h2><p>我们将使用Spring Boot 2.1.5版本来实现这一演示项目，首先你需要在Maven配置文件中加入如下的一些配置：</p><pre><code class="language-xml">&lt;dependencies&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;&lt;scope&gt;runtime&lt;/scope&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.projectlombok&lt;/groupId&gt;&lt;artifactId&gt;lombok&lt;/artifactId&gt;&lt;optional&gt;true&lt;/optional&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;&lt;scope&gt;test&lt;/scope&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;mysql&lt;/groupId&gt;&lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;&lt;version&gt;5.1.47&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-freemarker&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.apache.commons&lt;/groupId&gt;&lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;&lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><p>然后提供一个可用的配置文件，并加入如下的内容：</p><pre><code class="language-yaml">spring:  freemarker:    cache: false    template-loader-path:    - classpath:/templates/    prefix:    suffix: .html  resources:    static-locations:    - classpath:/static/  devtools:    restart:      enabled: true  jpa:    database: mysql    show-sql: true    generate-ddl: false    hibernate:      ddl-auto: noneuna:  master:    datasource:      url:  jdbc:mysql://localhost:3306/master_tenant?useSSL=false      username: root      password: root      driverClassName:  com.mysql.jdbc.Driver      maxPoolSize:  10      idleTimeout:  300000      minIdle:  10      poolName: master-database-connection-poollogging:  level:    root: warn    org:      springframework:        web:  debug      hibernate: debug</code></pre><blockquote><p>由于采用Freemarker作为视图渲染引擎，所以需要提供Freemarker的相关技术</p><p>una:master:datasource配置项就是上面说的统一存放租户信息的数据源配置信息，你可以理解为主库。</p></blockquote><p>接下来，我们需要关闭Spring Boot自动配置数据源的功能，在项目主类上添加如下的设置：</p><pre><code class="language-java">@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})public class UnaSaasApplication {public static void main(String[] args) {SpringApplication.run(UnaSaasApplication.class, args);}}</code></pre><p>最后，让我们看看整个项目的结构：<br /><img src="https://cdn.ramostear.com/2019-05-27-05-00-05-3252d32bc9eb44c1b0375f4565a915b0.png" alt="" /></p><h2 id="5-实现租户数据源查询模块">5. 实现租户数据源查询模块</h2><p>我们将定义一个实体类存放租户数据源信息，它包含了租户名，数据库连接地址，用户名和密码等信息，其代码如下：</p><pre><code class="language-java">@Data@Entity@Table(name = &quot;MASTER_TENANT&quot;)@NoArgsConstructor@AllArgsConstructor@Builderpublic class MasterTenant implements Serializable{    @Id    @Column(name=&quot;ID&quot;)    private String id;    @Column(name = &quot;TENANT&quot;)    @NotEmpty(message = &quot;Tenant identifier must be provided&quot;)    private String tenant;    @Column(name = &quot;URL&quot;)    @Size(max = 256)    @NotEmpty(message = &quot;Tenant jdbc url must be provided&quot;)    private String url;    @Column(name = &quot;USERNAME&quot;)    @Size(min = 4,max = 30,message = &quot;db username length must between 4 and 30&quot;)    @NotEmpty(message = &quot;Tenant db username must be provided&quot;)    private String username;    @Column(name = &quot;PASSWORD&quot;)    @Size(min = 4,max = 30)    @NotEmpty(message = &quot;Tenant db password must be provided&quot;)    private String password;    @Version    private int version = 0;}</code></pre><p>持久层我们将继承JpaRepository接口，快速实现对数据源的CURD操作，同时提供了一个通过租户名查找租户数据源的接口，其代码如下：</p><pre><code class="language-java">package com.ramostear.una.saas.master.repository;import com.ramostear.una.saas.master.model.MasterTenant;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.query.Param;import org.springframework.stereotype.Repository;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:22 * @modify by : * @since: */@Repositorypublic interface MasterTenantRepository extends JpaRepository&lt;MasterTenant,String&gt;{    @Query(&quot;select p from MasterTenant p where p.tenant = :tenant&quot;)    MasterTenant findByTenant(@Param(&quot;tenant&quot;) String tenant);}</code></pre><p>业务层提供通过租户名获取租户数据源信息的服务（其余的服务各位可自行添加）：</p><pre><code class="language-java">package com.ramostear.una.saas.master.service;import com.ramostear.una.saas.master.model.MasterTenant;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:26 * @modify by : * @since: */public interface MasterTenantService {    /**     * Using custom tenant name query     * @param tenant    tenant name     * @return          masterTenant     */    MasterTenant findByTenant(String tenant);}</code></pre><p>最后，我们需要关注的重点是配置主数据源（Spring Boot需要为其提供一个默认的数据源）。在配置之前，我们需要获取配置项，可以通过@ConfigurationProperties(&quot;una.master.datasource&quot;)获取配置文件中的相关配置信息：</p><pre><code class="language-java">@Getter@Setter@Configuration@ConfigurationProperties(&quot;una.master.datasource&quot;)public class MasterDatabaseProperties {    private String url;    private String password;    private String username;    private String driverClassName;    private long connectionTimeout;    private int maxPoolSize;    private long idleTimeout;    private int minIdle;    private String poolName;    @Override    public String toString(){        StringBuilder builder = new StringBuilder();        builder.append(&quot;MasterDatabaseProperties [ url=&quot;)                .append(url)                .append(&quot;, username=&quot;)                .append(username)                .append(&quot;, password=&quot;)                .append(password)                .append(&quot;, driverClassName=&quot;)                .append(driverClassName)                .append(&quot;, connectionTimeout=&quot;)                .append(connectionTimeout)                .append(&quot;, maxPoolSize=&quot;)                .append(maxPoolSize)                .append(&quot;, idleTimeout=&quot;)                .append(idleTimeout)                .append(&quot;, minIdle=&quot;)                .append(minIdle)                .append(&quot;, poolName=&quot;)                .append(poolName)                .append(&quot;]&quot;);        return builder.toString();    }}</code></pre><p>接下来是配置自定义的数据源，其源码如下：</p><pre><code class="language-java">package com.ramostear.una.saas.master.config;import com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;import com.ramostear.una.saas.master.model.MasterTenant;import com.ramostear.una.saas.master.repository.MasterTenantRepository;import com.zaxxer.hikari.HikariDataSource;import lombok.extern.slf4j.Slf4j;import org.hibernate.cfg.Environment;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;import org.springframework.data.jpa.repository.config.EnableJpaRepositories;import org.springframework.orm.jpa.JpaTransactionManager;import org.springframework.orm.jpa.JpaVendorAdapter;import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.persistence.EntityManagerFactory;import javax.sql.DataSource;import java.util.Properties;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:31 * @modify by : * @since: */@Configuration@EnableTransactionManagement@EnableJpaRepositories(basePackages = {&quot;com.ramostear.una.saas.master.model&quot;,&quot;com.ramostear.una.saas.master.repository&quot;},                       entityManagerFactoryRef = &quot;masterEntityManagerFactory&quot;,                       transactionManagerRef = &quot;masterTransactionManager&quot;)@Slf4jpublic class MasterDatabaseConfig {    @Autowired    private MasterDatabaseProperties masterDatabaseProperties;    @Bean(name = &quot;masterDatasource&quot;)    public DataSource masterDatasource(){        log.info(&quot;Setting up masterDatasource with :{}&quot;,masterDatabaseProperties.toString());        HikariDataSource datasource = new HikariDataSource();        datasource.setUsername(masterDatabaseProperties.getUsername());        datasource.setPassword(masterDatabaseProperties.getPassword());        datasource.setJdbcUrl(masterDatabaseProperties.getUrl());        datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName());        datasource.setPoolName(masterDatabaseProperties.getPoolName());        datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize());        datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle());        datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());        datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());        log.info(&quot;Setup of masterDatasource successfully.&quot;);        return datasource;    }    @Primary    @Bean(name = &quot;masterEntityManagerFactory&quot;)    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){        LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean();        lb.setDataSource(masterDatasource());        lb.setPackagesToScan(           new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()}        );        //Setting a name for the persistence unit as Spring sets it as 'default' if not defined.        lb.setPersistenceUnitName(&quot;master-database-persistence-unit&quot;);        //Setting Hibernate as the JPA provider.        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();        lb.setJpaVendorAdapter(vendorAdapter);        //Setting the hibernate properties        lb.setJpaProperties(hibernateProperties());        log.info(&quot;Setup of masterEntityManagerFactory successfully.&quot;);        return lb;    }    @Bean(name = &quot;masterTransactionManager&quot;)    public JpaTransactionManager masterTransactionManager(@Qualifier(&quot;masterEntityManagerFactory&quot;)EntityManagerFactory emf){        JpaTransactionManager transactionManager = new JpaTransactionManager();        transactionManager.setEntityManagerFactory(emf);        log.info(&quot;Setup of masterTransactionManager successfully.&quot;);        return transactionManager;    }    @Bean    public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){        return new PersistenceExceptionTranslationPostProcessor();    }    private Properties hibernateProperties(){        Properties properties = new Properties();        properties.put(Environment.DIALECT,&quot;org.hibernate.dialect.MySQL5Dialect&quot;);        properties.put(Environment.SHOW_SQL,true);        properties.put(Environment.FORMAT_SQL,true);        properties.put(Environment.HBM2DDL_AUTO,&quot;update&quot;);        return properties;    }}</code></pre><p>在改配置类中，我们主要提供包扫描路径，实体管理工程，事务管理器和数据源配置参数的配置。</p><h2 id="6-实现租户业务模块">6. 实现租户业务模块</h2><p>在此小节中，租户业务模块我们仅提供一个用户登录的场景来演示SaaS的功能。其实体层、业务层和持久化层根普通的Spring Boot Web项目没有什么区别，你甚至感觉不到它是一个SaaS应用程序的代码。</p><p>首先，创建一个用户实体User，其源码如下：</p><pre><code class="language-java">@Entity@Table(name = &quot;USER&quot;)@Data@NoArgsConstructor@AllArgsConstructor@Builderpublic class User implements Serializable {    private static final long serialVersionUID = -156890917814957041L;    @Id    @Column(name = &quot;ID&quot;)    private String id;    @Column(name = &quot;USERNAME&quot;)    private String username;    @Column(name = &quot;PASSWORD&quot;)    @Size(min = 6,max = 22,message = &quot;User password must be provided and length between 6 and 22.&quot;)    private String password;    @Column(name = &quot;TENANT&quot;)    private String tenant;}</code></pre><p>业务层提供了一个根据用户名检索用户信息的服务，它将调用持久层的方法根据用户名对租户的用户表进行检索，如果找到满足条件的用户记录，则返回用户信息，如果没有找到，则返回null;持久层和业务层的源码分别如下：</p><pre><code class="language-java">@Repositorypublic interface UserRepository extends JpaRepository&lt;User,String&gt;,JpaSpecificationExecutor&lt;User&gt;{    User findByUsername(String username);}</code></pre><pre><code class="language-java">@Service(&quot;userService&quot;)public class UserServiceImpl implements UserService{    @Autowired    private UserRepository userRepository;    private static TwitterIdentifier identifier = new TwitterIdentifier();    @Override    public void save(User user) {        user.setId(identifier.generalIdentifier());        user.setTenant(TenantContextHolder.getTenant());        userRepository.save(user);    }    @Override    public User findById(String userId) {        Optional&lt;User&gt; optional = userRepository.findById(userId);        if(optional.isPresent()){            return optional.get();        }else{            return null;        }    }    @Override    public User findByUsername(String username) {        System.out.println(TenantContextHolder.getTenant());        return userRepository.findByUsername(username);    }</code></pre><blockquote><p>在这里，我们采用了Twitter的雪花算法来实现了一个ID生成器。</p></blockquote><h2 id="7-配置拦截器">7. 配置拦截器</h2><p>我们需要提供一个租户信息的拦截器，用以获取租户标识符，其源代码和配置拦截器的源代码如下：</p><pre><code class="language-java">/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/26 0026-23:17 * @modify by : * @since: */@Slf4jpublic class TenantInterceptor implements HandlerInterceptor{    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        String tenant = request.getParameter(&quot;tenant&quot;);        if(StringUtils.isBlank(tenant)){            response.sendRedirect(&quot;/login.html&quot;);            return false;        }else{            TenantContextHolder.setTenant(tenant);            return true;        }    }}</code></pre><pre><code class="language-java">@Configurationpublic class InterceptorConfig extends WebMvcConfigurationSupport {    @Override    protected void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new TenantInterceptor()).addPathPatterns(&quot;/**&quot;).excludePathPatterns(&quot;/login.html&quot;);        super.addInterceptors(registry);    }}</code></pre><blockquote><p>/login.html是系统的登录路径，我们需要将其排除在拦截器拦截的范围之外，否则我们永远无法进行登录</p></blockquote><h2 id="8-维护租户标识信息">8. 维护租户标识信息</h2><p>在这里，我们使用ThreadLocal来存放租户标识信息，为动态设置数据源提供数据支持，该类提供了设置租户标识、获取租户标识以及清除租户标识三个静态方法。其源码如下：</p><pre><code class="language-java">public class TenantContextHolder {    private static final ThreadLocal&lt;String&gt; CONTEXT = new ThreadLocal&lt;&gt;();    public static void setTenant(String tenant){        CONTEXT.set(tenant);    }    public static String getTenant(){        return CONTEXT.get();    }    public static void clear(){        CONTEXT.remove();    }}</code></pre><blockquote><p>此类时实现动态数据源设置的关键</p></blockquote><h2 id="9-动态数据源切换">9. 动态数据源切换</h2><p>要实现动态数据源切换，我们需要借助两个类来完成，CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。从它们的命名上就可以看出，一个负责解析租户标识，一个负责提供租户标识对应的租户数据源信息。</p><p>首先，我们需要实现CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法，完成租户标识的解析功能。实现类的源码如下：</p><pre><code class="language-java">package com.ramostear.una.saas.tenant.config;import com.ramostear.una.saas.context.TenantContextHolder;import org.apache.commons.lang3.StringUtils;import org.hibernate.context.spi.CurrentTenantIdentifierResolver;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/26 0026-22:38 * @modify by : * @since: */public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {    /**     * 默认的租户ID     */    private static final String DEFAULT_TENANT = &quot;tenant_1&quot;;    /**     * 解析当前租户的ID     * @return     */    @Override    public String resolveCurrentTenantIdentifier() {        //通过租户上下文获取租户ID，此ID是用户登录时在header中进行设置的        String tenant = TenantContextHolder.getTenant();        //如果上下文中没有找到该租户ID，则使用默认的租户ID，或者直接报异常信息        return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;    }    @Override    public boolean validateExistingCurrentSessions() {        return true;    }}</code></pre><blockquote><p>此类的逻辑非常简单，就是从ThreadLocal中获取当前设置的租户标识符</p></blockquote><p>有了租户标识符解析类之后，我们需要扩展租户数据源提供类，实现从数据库动态查询租户数据源信息，其源码如下：</p><pre><code>@Slf4j@Configurationpublic class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{    private static final long serialVersionUID = -7522287771874314380L;    @Autowired    private MasterTenantRepository masterTenantRepository;    private Map&lt;String,DataSource&gt; dataSources = new TreeMap&lt;&gt;();    @Override    protected DataSource selectAnyDataSource() {        if(dataSources.isEmpty()){            List&lt;MasterTenant&gt; tenants = masterTenantRepository.findAll();            tenants.forEach(masterTenant-&gt;{                dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant));            });        }        return dataSources.values().iterator().next();    }    @Override    protected DataSource selectDataSource(String tenant) {        if(!dataSources.containsKey(tenant)){            List&lt;MasterTenant&gt; tenants = masterTenantRepository.findAll();            tenants.forEach(masterTenant-&gt;{                dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));            });        }        return dataSources.get(tenant);    }}</code></pre><blockquote><p>在该类中，通过查询租户数据源库，动态获得租户数据源信息，为租户业务模块的数据源配置提供数据数据支持。</p></blockquote><p>最后，我们还需要提供租户业务模块数据源配置，这是整个项目核心的地方，其代码如下：</p><pre><code class="language-java">@Slf4j@Configuration@EnableTransactionManagement@ComponentScan(basePackages = {        &quot;com.ramostear.una.saas.tenant.model&quot;,        &quot;com.ramostear.una.saas.tenant.repository&quot;})@EnableJpaRepositories(basePackages = {        &quot;com.ramostear.una.saas.tenant.repository&quot;,        &quot;com.ramostear.una.saas.tenant.service&quot;},entityManagerFactoryRef = &quot;tenantEntityManagerFactory&quot;,transactionManagerRef = &quot;tenantTransactionManager&quot;)public class TenantDataSourceConfig {    @Bean(&quot;jpaVendorAdapter&quot;)    public JpaVendorAdapter jpaVendorAdapter(){        return new HibernateJpaVendorAdapter();    }    @Bean(name = &quot;tenantTransactionManager&quot;)    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){        JpaTransactionManager transactionManager = new JpaTransactionManager();        transactionManager.setEntityManagerFactory(entityManagerFactory);        return transactionManager;    }    @Bean(name = &quot;datasourceBasedMultiTenantConnectionProvider&quot;)    @ConditionalOnBean(name = &quot;masterEntityManagerFactory&quot;)    public MultiTenantConnectionProvider multiTenantConnectionProvider(){        return new DataSourceBasedMultiTenantConnectionProviderImpl();    }    @Bean(name = &quot;currentTenantIdentifierResolver&quot;)    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){        return new CurrentTenantIdentifierResolverImpl();    }    @Bean(name = &quot;tenantEntityManagerFactory&quot;)    @ConditionalOnBean(name = &quot;datasourceBasedMultiTenantConnectionProvider&quot;)    public LocalContainerEntityManagerFactoryBean entityManagerFactory(            @Qualifier(&quot;datasourceBasedMultiTenantConnectionProvider&quot;)MultiTenantConnectionProvider connectionProvider,            @Qualifier(&quot;currentTenantIdentifierResolver&quot;)CurrentTenantIdentifierResolver tenantIdentifierResolver    ){        LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean();        localBean.setPackagesToScan(                new String[]{                        User.class.getPackage().getName(),                        UserRepository.class.getPackage().getName(),                        UserService.class.getPackage().getName()                }        );        localBean.setJpaVendorAdapter(jpaVendorAdapter());        localBean.setPersistenceUnitName(&quot;tenant-database-persistence-unit&quot;);        Map&lt;String,Object&gt; properties = new HashMap&lt;&gt;();        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);        properties.put(Environment.DIALECT,&quot;org.hibernate.dialect.MySQL5Dialect&quot;);        properties.put(Environment.SHOW_SQL,true);        properties.put(Environment.FORMAT_SQL,true);        properties.put(Environment.HBM2DDL_AUTO,&quot;update&quot;);        localBean.setJpaPropertyMap(properties);        return localBean;    }}</code></pre><blockquote><p>在改配置文件中，大部分内容与主数据源的配置相同，唯一的区别是租户标识解析器与租户数据源补给源的设置，它将告诉Hibernate在执行数据库操作命令前，应该设置什么样的数据库连接信息，以及用户名和密码等信息。</p></blockquote><h2 id="10-应用测试">10. 应用测试</h2><p>最后，我们通过一个简单的登录案例来测试本次课程中的SaaS应用程序，为此，需要提供一个Controller用于处理用户登录逻辑。在本案例中，没有严格的对用户密码进行加密，而是使用明文进行比对，也没有提供任何的权限认证框架，知识单纯的验证SaaS的基本特性是否具备。登录控制器代码如下：</p><pre><code class="language-java">/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/27 0027-0:18 * @modify by : * @since: */@Controllerpublic class LoginController {    @Autowired    private UserService userService;    @GetMapping(&quot;/login.html&quot;)    public String login(){        return &quot;/login&quot;;    }    @PostMapping(&quot;/login&quot;)    public String login(@RequestParam(name = &quot;username&quot;) String username, @RequestParam(name = &quot;password&quot;)String password, ModelMap model){        System.out.println(&quot;tenant:&quot;+TenantContextHolder.getTenant());        User user = userService.findByUsername(username);        if(user != null){            if(user.getPassword().equals(password)){                model.put(&quot;user&quot;,user);                return &quot;/index&quot;;            }else{                return &quot;/login&quot;;            }        }else{            return &quot;/login&quot;;        }    }}</code></pre><p>在启动项目之前，我们需要为主数据源创建对应的数据库和数据表，用于存放租户数据源信息，同时还需要提供一个租户业务模块数据库和数据表，用来存放租户业务数据。一切准备就绪后，启动项目，在浏览器中输入：<a href="http://localhost:8080/login.html">http://localhost:8080/login.html</a></p><p><img src="https://cdn.ramostear.com/2019-05-27-05-00-35-b3c8f49c2d054047bf00ca5623c9b919.png" alt="" /></p><p>在登录窗口中输入对应的租户名，用户名和密码，测试是否能够正常到达主页。可以多增加几个租户和用户，测试用户是否正常切换到对应的租户下。</p><h2 id="总结">总结</h2><p>在这里，我分享了使用Spring Boot+JPA快速实现多租户应用程序的方法，此方法只涉及了实现SaaS应用平台的最核心技术手段，并不是一个完整可用的项目代码，如用户的认证、授权等并未出现在本文中。额外的业务模块感兴趣的朋友可以在此设计基础上自行扩展，如对其中的代码有任何的疑问，欢迎大家在下方给我留言。</p>]]>
                    </description>
                    <pubDate>Fri, 20 Sep 2019 15:42:02 CST</pubDate>
                </item>
    </channel>
</rss>