posted in JavaWeb 

在对一个履约系统进行平台化改造过程中,沉淀了一套轻量化的平台化框架——shuke,供大家参考。

1. 背景

要重构的这一履约系统作为一个历史悠久的应用,各层代码中堆砌着各种针对不同业务的if else逻辑,进而导致了几个问题:

  1. 代码可读性很差。
  2. 业务代码和平台代码高度耦合。
  3. 新业务接入困难。

举一个例子,
履约平台在商家呼叫运力时,需要把履约任务下发给CP,而不同的业务下发的CP也是不一样的。

重构之前代码是这样的:

业务代码和平台代码混杂在一起,且层层if else嵌套

  1. 对业务的修改可能引起平台逻辑的问题
  2. 对A业务的修改可能引发B业务的问题
  3. 可读性很差,很难在一个业务包中找到所有的业务个性化定制
  4. 新业务接入成本高,需要找到各种需要修改的点,加if else

在系统重构的过程,通过shuke这一平台化框架,最终达到以下几个目标:

  1. 解耦业务逻辑和平台逻辑
  2. 提升代码可维护性
  3. 降低新业务的接入成本

2. 使用说明

talk is cheap, show me the code.

重构完之后,效果是这样的

  • 平台层定义spi
public interface DispatchNode {
    DispatchNO dispatch2Cp(DispatchNOParam param);
}
  • 平台使用spi
        DispatchNode dispatchNode = PluginRouter.routeNode(DispatchNode, FlowId.CHINA);
        DispatchNO dispatchNO = dispatchNode.dispatch2Cp(param);
        System.out.println(dispatchNO);
  • 业务A实现spi
@Component
@FlowInfo(FlowId.USA)   // 业务身份
public class UsaDispatchNode implements DispatchNode {
    @Resource
    private CainiaoService cainiaoService;
    
    @Override
    DispatchNO dispatch2Cp(DispatchNOParam param);
        String expressNumber = cainiaoService.dispatch(xxx);
        return DispatchNO.builder()
                .result(expressNumber)
                .build();
    }
}
  • 业务B实现spi
@Component
@FlowInfo(FlowId.CHINA) // 业务身份
public class ChinaDispatchNode implements DispatchNode {
    @Resource
    private FnService fnService;
    
    @Override
    DispatchNO dispatch2Cp(DispatchNOParam param);
        String expressNumber = fnService.dispatch(xxx);
        return DispatchNO.builder()
                .result(expressNumber)
                .build();
    }
}

通过以上代码,便可以把业务代码和平台代码解耦开来。以后无论是现有业务的维护还是新业务的接入,都可以快速高效进行。

3. 组成

shuke涉及的部分有以下以下几部分:

  • spi
    • 也就是平台留给业务的拓展点
  • 业务spi实现
    • 不同的业务对spi有其不同的实现
  • 平台
    • 通用的平台代码,通过spi来隔离不同业务的定制点
  • ability
    • 平台能力,提供给插件方来使用。

最终的各模块之前的关系如下:

4. 设计哲学 && 其他框架对比

KISS:Keep it simple, keep it stupid.

Shuke设计的一大初衷便是足够的简单和轻量化,这也是shuke区分于现有的一些重量级平台化框架的最重要的特点。

相比于那些花费漫长时间学习上手、理解的平台化框架,shuke的一大特点是可以在几十分钟内了解其如何使用。学习成本的降低,意味着开发成本和犯错的机会都会随之降低。

5. todo

做一个一个新生的框架,目前在一些地方还是待完善的,比方说不同业务方只有代码隔离没有容器隔离、业务插件不支持热部署等等,这些都是后续可以改进的地方。

6. 源码

git@gitlab.alibaba-inc.com:zhongzheng.czz/shuke.git

测试用例:
com.alibaba.ascp.shuke.core.test.ShukeTest#testDispatch

有什么意见或者建议,欢迎留言~

posted in 网络 

第一章:初识Netty:背景、现状与趋势

揭开 Netty 面纱

  • 作者
  • 概述
  • 代码模块
  • helloworld

为什么舍近求远:不直接用 JDK NIO

  • 做的更多
    • 支持常用应用层协议;
    • 解决传输问题:粘包、半包现象;
    • 支持流量整形;
    • 完善的断连、Idle 等异常处理等
  • 做的更好
    • 避免jdk nio 的bug
    • API更好更强大
    • 隔离变化、屏蔽细节
  • 避免花大量时间造轮子
  • netty社区活跃,发展情景明朗

为什么孤注一掷:独选 Netty ?

  • 相比mina:同作者推荐
  • 相比grizzy:更新多、文档多用的多
  • 为什么不选 Apple SwfitNIO 、ACE 等:其他语言
  • 为什么不选 Cindy 等:生命周期不长。
  • 为什么不选 Tomcat、Jetty :还没有独立出来。

Netty 的前尘往事

  • 从归属组织上看发展
  • 从版本演变上看发展
  • 社区现状
  • 最新版本

Netty 的现状与趋势

  • 应用现状
  • 一些典型项目
  • 趋势

第二章:Netty 源码:从“点”(领域知识)的角度剖析

1. Netty 怎么切换三种 I/O 模式

  • 什么是经典的三种 I/O 模式
    • 一个吃饭的例子
  • Netty 对三种 I/O 模式的支持
    • BIO:过时
    • NIO:common、linux、bsd
    • AIO:移除
  • 为什么 Netty 仅支持 NIO 了?
    • BIO
    • AIO
  • 为什么 Netty 有多种 NIO 实现?
    • 更多功能
    • 更好性能
  • NIO 一定优于 BIO 么?
    • 仅在高并发时
  • 源码解读 Netty 怎么切换 I/O 模式?
    • EventLoopGroup
      • 线程池
      • 工厂模式+泛型+反射实现
    • ServerSocketChannel
      • 如何处理链接
    • 为什么服务器开发并不需要切换客户端对应NioSocketChannel ?
      • ServerSocketChannel 负责创建对应的 SocketChannel 。

2. Netty 如何支持三种 Reactor

2.1. 什么是Reactor模式?

2.2. Reactor模式的图示

  • Thread-Per-Connection模式

  • Reactor模式v1:单线程

  • Reactor模式v2:多线程

  • Reactore模式v3:主从多线程

2.3. 如何使用三种Reactor模式

2.4. Reactor相关源码分析

  • Netty 如何支持主从 Reactor 模式的?
  • 为什么说 Netty 的 main reactor 大多并不能用到一个线程组,只能线程组里面的一个?
  • Netty 给 Channel 分配 NIO event loop 的规则是什么
  • 通用模式的 NIO 实现多路复用器是怎么跨平台的

3. TCP 粘包/半包 Netty 全搞定

3.1. 什么是粘包和半包?

3.2. 为什么 TCP 应用中会出现粘包和半包现象?

粘包的主要原因:

  • 发送方每次写入数据 < 套接字缓冲区大小
  • 接收方读取套接字缓冲区数据不够及时

半包的主要原因:

  • 发送方写入数据 > 套接字缓冲区大小
  • 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包

换个角度看:

  • 收发
    • 一个发送可能被多次接收,多个发送可能被一次接收
  • 传输
    • 一个发送可能占用多个传输包,多个发送可能公用一个传输包

根本原因:

  • TCP 是流式协议,消息无边界。

3.3. 解决粘包和半包问题的几种常用方法

3.4. Netty 对三种常用封帧方式的支持

3.5. 解读 Netty 处理粘包、半包的源码

4. 常用的“二次”编解码方式

4.1. 为什么要“二次”解码?

Java对象和字节流之间相互转换。

4.2. 常用的“二次编解码”方式

  • Java序列化
  • Marshaling
  • XML
  • JSON
  • MessagePack
  • Protobuf
  • 其他

4.3. 选择编解码方式的要点

  • 大小
  • 速度
  • 可读性

4.4. Protobuf简介与适用

  • 工具生成对应相等代码

4.5. 源码解读:Netty对二次编解码的支持

ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
ch.pipeline().addLast(new ProtobufDecoder(PersonOuterClass.Person.getDefaultInstance())); ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(new ProtobufEncoder());

5. keepalive 与 Idle 监测

5.1. 为什么需要keepalive?

生活中的例子:
如果打电话中别人忽然不说话了,你会问一句“你还在吗?”,如果没有回复,就挂断。

好处:
避免长时间占用线路,别人就打不进来了

5.2. 怎么设计keepalive?以TCP keepalive 为例

TCP keepalive 核心参数:

# sysctl -a|grep tcp_keepalive 
net.ipv4.tcp_keepalive_time = 7200 
net.ipv4.tcp_keepalive_intvl = 75 
net.ipv4.tcp_keepalive_probes = 9

当启用(默认关闭)keepalive 时,TCP 在连接没有数据
通过的7200秒后发送 keepalive 消息,当探测没有确认时, 按75秒的重试频率重发,一直发 9 个探测包都没有确认,就认定 连接失效。

所以总耗时一般为:2 小时 11 分钟 (7200 秒 + 75 秒* 9 次)

5.3. 为什么还需要应用层keepalive?

  • 协议分层,各层关注点不同:
    传输层关注是否“通”,应用层关注是否可服务? 类比前面的电话订餐例子,电话能通, 不代表有人接;服务器连接在,但是不定可以服务(例如服务不过来等)。
  • TCP 层的 keepalive 默认关闭,且经过路由等中转设备 keepalive 包可能会被丢弃。
  • TCP 层的 keepalive 时间太长:
    默认 > 2 小时,虽然可改,但属于系统参数,改动影响所有应用。

5.4. idle监测是什么?

生活例子:
在打电话时,如果别人忽然不讲讲话了,隔一段时间后,你会问“你还在吗?”

总结:
Idle监测用做诊断,配合keepalive,减少keepalive消息。

Idle监测的演进:
v1:定时发送监测消息。
v2:有其他数据传输时,不发送监测消息。无数据传输时,定时发送监测消息。

5.5. 如何在Netty中开启TCP keepalive 和Idle监测?

  • Server 端开启 TCP keepalive
bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true) bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)

提示:.option(ChannelOption.SO_KEEPALIVE,true) 存在但是无效

  • 开启不同的 Idle Check:
ch.pipeline().addLast(“idleCheckHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));

5.6. 源码解读 Netty 对 TCP keepalive 和三种 Idle 检测的支持

源码解读:

  • 设置 TCP keepalive 怎么生效的?
    • 调用jdk接口,设置chennel的option
  • 两种设置 keepalive 的方式有什么区别?
    • 没啥区别,一个是netty抽象的,一个是nio的
  • Idle 检测类包(io.netty.handler.timeout)的功能浏览
  • 读 Idle 检测的原理
  • 写 Idle 检测原理和参数 observeOutput 用途?

6. Netty 的那些“锁”事

6.1. 分析同步问题的核心三要素

  • 原子性
  • 可见性
  • 有序性

6.2. 锁的分类

  • 对竞争的态度:乐观锁(java.util.concurrent 包中的原子类)与悲观锁(Synchronized)
  • 等待锁的人是否公平而言:公平锁 new ReentrantLock (true)与非公平锁 new ReentrantLock ()
  • 是否可以共享:共享锁与独享锁:ReadWriteLock ,其读锁是共享锁,其写锁是独享锁

6.3. Netty玩转锁的五个关键点:

  • 在意锁的对象和范围 -> 减少粒度
  • 注意所得对象本身大小 -> 减少空间占用
  • 注意锁的速度 -> 提高速度
  • 不同场景选择不同的并发类 -> 因需而变
  • 衡量好锁的价值 -> 能不能则不用
    ### 7. Netty 如何玩转内存使用
    #### 7.1. 内存使用技巧的目标

目标:

  • 内存占用少(空间)
  • 应用速度快(时间)
    对 Java 而言:减少 Full GC 的 STW(Stop the world)时间

7.2. Netty 内存使用技巧 - 减少对像本身大小

例 1:用基本类型就不要用包装类:
例 2: 应该定义成类变量的不要定义为实例变量:
例 3: Netty 中结合前两者:
io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long, boolean) 统计待写的请求的字节数

7.3. Netty 内存使用技巧 - 对分配内存进行预估

例 1:对于已经可以预知固定 size 的 HashMap避免扩容
可以提前计算好初始size或者直接使用 com.google.common.collect.Maps#newHashMapWithExpectedSize

例 2:Netty 根据接受到的数据动态调整(guess)下个要分配的 Buffer 的大小。可参考 io.netty.channel.AdaptiveRecvByteBufAllocator

7.4. Netty 内存使用技巧 - Zero-Copy

例 1:使用逻辑组合,代替实际复制。
例如 CompositeByteBuf: io.netty.handler.codec.ByteToMessageDecoder#COMPOSITE_CUMULATOR

例 2:使用包装,代替实际复制。
byte[] bytes = data.getBytes();
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

例 3:调用 JDK 的 Zero-Copy 接口。
Netty 中也通过在 DefaultFileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实 现了零拷贝:io.netty.channel.DefaultFileRegion#transferTo

7.5. Netty 内存使用技巧 - 堆外内存

优点:

  • 更广阔的“空间 ”,缓解店铺内压力 -> 破除堆空间限制,减轻 GC 压力
  • 减少“冗余”细节(假设烧烤过程为了气氛在室外进行:烤好直接上桌:vs 烤好还 要进店内)-> 避免复制

缺点:

  • 需要搬桌子 -> 创建速度稍慢
  • 受城管管、风险大 -> 堆外内存受操作系统管理

7.6. Netty 内存使用技巧 - 内存池

为什么引入对象池:

  • 创建对象开销大
  • 对象高频率创建且可复用
  • 支持并发又能保护系统
  • 维护、共享有限的资源

如何实现对象池?

  • 开源实现:Apache Commons Pool
  • Netty 轻量级对象池实现 io.netty.util.Recycler

7.7. 源码解读Netty 内存使用

源码解读:

  • 怎么从堆外内存切换堆内使用?以UnpooledByteBufAllocator为例
  • 堆外内存的分配本质?
  • 内存池/非内存池的默认选择及切换方式?
    io.netty.channel.DefaultChannelConfig#allocator
  • 内存池实现(以 PooledDirectByteBuf 为例)
    io.netty.buffer.PooledDirectByteBuf
posted in 网络 

第一章:高并发编程Netty实战课程介绍

1、高并发编程Netty框架实战课程介绍

简介:讲解Netty课程大纲

2、异步事件驱动NIO框架Netty介绍

简介:介绍Netty来源,版本,目前在哪些主流公司和产品框架使用

1、Netty是由JBOSS提供的一个java开源框架, 是业界最流行的NIO框架,整合了多种协议(
包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,精心设计的框架,在多个大型商业项目中得到充分验证。
1)API使用简单
2)成熟、稳定
3)社区活跃 有很多种NIO框架 如mina
4)经过大规模的验证(互联网、大数据、网络游戏、电信通信行业)

2、那些主流框架产品在用?
1)搜索引擎框架 ElasticSerach
2) Hadopp子项目Avro项目,使用Netty作为底层通信框架
3)阿里巴巴开源的RPC框架 Dubbo
地址:http://dubbo.apache.org/zh-cn/
Netty在Dubbo里面使用的地址
https://github.com/apache/incubator-dubbo/tree/master/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4
补充:netty4是dubbo2.5.6后引入的,2.5.6之前的netty用的是netty3

3、高并发编程Netty实战课程开发环境准备

简介:讲解Netty实战开发环境

1、IDEA旗舰版/Eclipse + JDK8 + Maven3.5以上版本 + Netty4.x

Netty版本说明
采用最新的4.x版本,只要大版本一致就可以
官方文档: https://netty.io/wiki/user-guide-for-4.x.html
Github地址:https://github.com/netty/netty

第二章:使用JDK自带BIO编写一个Client-Server通信

1、BIO网络编程实战之编写BioServer服务端

简介: 使用jdk自带的Bio编写一个统一时间服务

2、BIO网络编程实战之编写BioClient客服端

简介:使用BIO网络编程编写BioClient客户端

3、BIO编写Client/Server通信优缺点分析

简介:讲解BIO的优缺点,为啥不能高并发情况下性能弱

优点:
模型简单
编码简单

缺点:
性能瓶颈,请求数和线程数 N:N关系
高并发情况下,CPU切换线程上下文损耗大

案例:web服务器Tomcat7之前,都是使用BIO,7之后就使用NIO
改进:伪NIO,使用线程池去处理业务逻辑

第三章:服务端网络编程常见网络IO模型讲解(面试核心)

1、什么是阻塞/非阻塞,什么是同/异步

简介:使用最通俗概念讲解 同步异步、堵塞和非堵塞

洗衣机洗衣服

  • 同步阻塞:你把衣服丢到洗衣机洗,然后看着洗衣机洗完,洗好后再去晾衣服(你就干等,啥都不做,阻塞在那边)
  • 同步非阻塞:你把衣服丢到洗衣机洗,然后会客厅做其他事情,定时去阳台看洗衣机是不是洗完了,洗好后再去晾衣服
    (等待期间你可以做其他事情,比如用电脑打开小D课堂看视频学习)
  • 异步阻塞: 你把衣服丢到洗衣机洗,然后看着洗衣机洗完,洗好后再去晾衣服(几乎没这个情况,几乎没这个说法,可以忽略)
  • 异步非阻塞:你把衣服丢到洗衣机洗,然后会客厅做其他事情,洗衣机洗好后会自动去晾衣服,晾完成后放个音乐告诉你洗好衣服并晾好了

2、Linux网络编程中的五种I/O模型讲解

网络IO,用户程序和内核的交互为基础进行讲解

IO操作分两步:

  • 发起IO请求等待数据准备,
  • 实际IO操作(洗衣服,晾衣服)

  • 同步须要主动读写数据,在读写数据的过程中还是会阻塞(好比晾衣服阻塞了你)

  • 异步仅仅须要I/O操作完毕的通知。并不主动读写数据,由操作系统内核完毕数据的读写(机器人帮你自动晾衣服)

五种IO的模型:

  • 阻塞IO
  • 非阻塞IO
  • 多路复用IO
  • 信号驱动IO
  • 异步IO
    前四种都是同步IO,在内核数据copy到用户空间时都是阻塞的

权威:RFC标准,或者书籍 《UNIX Network Programming》中文名《UNIX网络编程-卷一》第六章

1)阻塞式I/O;

阻塞IO

2)非阻塞式I/O;

非阻塞IO

3)I/O复用(select,poll,epoll...);

I/O多路复用是阻塞在select,epoll这样的系统调用,没有阻塞在真正的I/O系统调用如recvfrom
进程受阻于select,等待可能多个套接口中的任一个变为可读

IO多路复用使用两个系统调用(select和recvfrom)
blocking IO只调用了一个系统调用(recvfrom)
select/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好
多路复用模型中,每一个socket,设置为non-blocking,
阻塞是被select这个函数block,而不是被socket阻塞的

IO多路复用

4)信号驱动式I/O(SIGIO);

信号驱动

5)异步I/O(POSIX的aio_系列函数) Future-Listener机制;

异步IO

IO操作分为两步
1)发起IO请求,等待数据准备(Waiting for the data to be ready)
2)实际的IO操作,将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

前四种IO模型都是同步IO操作,区别在于第一阶段,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用或者select()函数。 相反,异步I/O模型在这两个阶段都要处理。

阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。

几个核心点:
阻塞非阻塞说的是线程的状态(重要)
同步和异步说的是消息的通知机制(重要)

同步需要主动读写数据,异步是不需要主动读写数据
同步IO和异步IO是针对用户应用程序和内核的交互

4、 IO多路复用技术

简介:高并发编程必备知识IO多路复用技术select、poll讲解

什么是IO多路复用

I/O多路复用,I/O是指网络I/O, 多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。
简单来说:就是使用一个或者几个线程处理多个TCP连接
最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程

select

  • 基本原理:
    监视文件3类描述符: writefds、readfds、和exceptfds
    调用后select函数会阻塞住,等有数据 可读、可写、出异常 或者 超时 就会返回
    select函数正常返回后,通过遍历fdset整个数组才能发现哪些句柄发生了事件,来找到就绪的描述符fd,然后进行对应的IO操作
    几乎在所有的平台上支持,跨平台支持性好

  • 缺点:
    1)select采用轮询的方式扫描文件描述符,全部扫描,随着文件描述符FD数量增多而性能下降
    2)每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
    3)最大的缺陷就是单个进程打开的FD有限制,默认是1024 (可修改宏定义,但是效率仍然慢)
    static final int MAX_FD = 1024

poll

基本流程:
select() 和 poll() 系统调用的大体一样,处理多个描述符也是使用轮询的方式,根据描述符的状态进行处理
一样需要把 fd 集合从用户态拷贝到内核态,并进行遍历。
最大区别是: poll没有最大文件描述符限制(使用链表的方式存储fd)

epoll

  • 基本原理:
    在2.6内核中提出的,对比select和poll,epoll更加灵活,没有描述符限制,用户态拷贝到内核态只需要一次
    使用事件通知,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用callback的回调机制来激活对应的fd

  • 优点:
    1)没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
    2)效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降
    2)通过callback机制通知,内核和用户空间mmap同一块内存实现

  • Linux内核核心函数
    1)epoll_create() 在Linux内核里面申请一个文件系统 B+树,返回epoll对象,也是一个fd
    2)epoll_ctl() 操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数
    3)epoll_wait() 判断并完成对应的IO操作

  • 缺点:
    编程模型比select/poll 复杂

例子:100万个连接,里面有1万个连接是活跃,在 select、poll、epoll分别是怎样的表现

select:不修改宏定义,则需要 1000个进程才可以支持 100万连接
poll:100万个链接,遍历都响应不过来了,还有空间的拷贝消耗大量的资源
epoll:

6、Java的I/O演进历史

简介:讲解java的IO演进历史
1、jdk1.4之前是采用同步阻塞模型,也就是BIO
大型服务一般采用C或者C++, 因为可以直接操作系统提供的异步IO,AIO

2、jdk1.4推出NIO,支持非阻塞IO,jdk1.7升级,推出NIO2.0,提供AIO的功能,支持文件和网络套接字的异步IO

7、大话Netty线程模型和Reactor模式

简介:讲解reactor模式 和 Netty线程模型

设计模式——Reactor模式(反应器设计模式),是一种基于事件驱动的设计模式,在事件驱动的应用中,将一个或多个客户的服务请求分离(demultiplex)和调度(dispatch)给应用程序。在事件驱动的应用中,同步地、有序地处理同时接收的多个服务请求
一般出现在高并发系统中,比如Netty,Redis等

  • 优点
    1)响应快,不会因为单个同步而阻塞,虽然Reactor本身依然是同步的;
    2)编程相对简单,最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
    3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;

  • 缺点
    1)相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
    2)Reactor模式需要系统底层的的支持,比如Java中的Selector支持,操作系统的select系统调用支持

通俗理解:KTV例子 前台接待,服务人员带领去开机器

Reactor模式基于事件驱动,适合处理海量的I/O事件,属于同步非阻塞IO(NIO)

Reactor单线程模型(比较少用)

内容:
1)作为NIO服务端,接收客户端的TCP连接;作为NIO客户端,向服务端发起TCP连接;
2)服务端读请求数据并响应;客户端写请求并读取响应

使用场景:
对应小业务则适合,编码简单;对于高负载、大并发的应用场景不适合,一个NIO线程处理太多请求,则负载过高,并且可能响应变慢,导致大量请求超时,而且万一线程挂了,则不可用了

Reactor多线程模型

内容:
1)一个Acceptor线程,一组NIO线程,一般是使用自带的线程池,包含一个任务队列和多个可用的线程

使用场景:
可满足大多数场景,但是当Acceptor需要做复杂操作的时候,比如认证等耗时操作,再高并发情况下则也会有性能问题

Reactor主从线程模型

内容:
1) Acceptor不在是一个线程,而是一组NIO线程;IO线程也是一组NIO线程,这样就是两个线程池去处理接入连接和处理IO

使用场景:
满足目前的大部分场景,也是Netty推荐使用的线程模型

BossGroup
WorkGroup

附属资料:为什么Netty使用NIO而不是AIO,是同步非阻塞还是异步非阻塞?

答案:
在Linux系统上,AIO的底层实现仍使用EPOLL,与NIO相同,因此在性能上没有明显的优势
Netty整体架构是reactor模型,采用epoll机制,所以往深的说,还是IO多路复用模式,所以也可说netty是同步非阻塞模型(看的层次不一样)

很多人说这是netty是基于Java NIO 类库实现的异步通讯框架
特点:异步非阻塞、基于事件驱动,性能高,高可靠性和高可定制性。

参考资料:
https://github.com/netty/netty/issues/2515

第四章:Netty第一个案例

1、讲解什么是Echo服务和Netty项目搭建

简介:讲解什么是Echo服务和快速创建Netty项目

1)什么是Echo服务:就是一个应答服务(回显服务器),客户端发送什么数据,服务端就响应的对应的数据
是一个非常有的用于调试和检测的服务
2)IDEA + Maven + jdk8
netty依赖包
3) maven地址:https://mvnrepository.com/artifact/io.netty/netty-all/4.1.32.Final

2、Netty实战之Echo服务-服务端程序编写实战

简介:讲解Echo服务-服务端程序编写实战,对应的启动类和handler处理器

3、Netty实战之Echo服务-客户端程序编写实战

简介:讲解Echo服务客户端程序编写

4、Netty实战之Echo服务演示和整个流程分析

简介:分析整个Echo服务各个组件名称和作用
1)EventLoop和EventLoopGroup
2) Bootstrapt启动引导类
3)Channel 生命周期,状态变化
4)ChannelHandler和ChannelPipline

第五章:核心链路源码讲解

1、深入剖析EventLoop和EventLoopGroup线程模型

简介:源码讲解EventLoop和EventLoopGroup模块

1)高性能RPC框架的3个要素:IO模型、数据协议、线程模型
2)EventLoop好比一个线程,1个EventLoop可以服务多个Channel,1个Channel只有一个EventLoop
可以创建多个 EventLoop 来优化资源利用,也就是EventLoopGroup
3)EventLoopGroup 负责分配 EventLoop 到新创建的 Channel,里面包含多个EventLoop
EventLoopGroup -> 多个 EventLoop
EventLoop -> 维护一个 Selector
学习资料:http://ifeve.com/selectors/
4)源码分析默认线程池数量

2、Netty启动引导类Bootstrap模块讲解

简介:讲解Netty启动引导类Bootstrap作用和tcp通道参数设置
参考:https://blog.csdn.net/QH_JAVA/article/details/78383543

2.1. 服务器启动引导类ServerBootstrap

1) group :设置线程组模型,Reactor线程模型对比EventLoopGroup
1)单线程
2)多线程
3)主从线程
参考:https://blog.csdn.net/QH_JAVA/article/details/78443646

2)channel:设置channel通道类型NioServerSocketChannel、OioServerSocketChannel

3) option: 作用于每个新建立的channel,设置TCP连接中的一些参数,如下

2.2. 客户端启动引导类Bootstrap

1)remoteAddress: 服务端地址
2)handler:和服务端通信的处理器

3、Netty核心组件Channel模块讲解

简介:讲解Channel作用,核心模块知识点,生命周期等

什么是Channel: 客户端和服务端建立的一个连接通道
什么是ChannelHandler: 负责Channel的逻辑处理
什么是ChannelPipeline: 负责管理ChannelHandler的有序容器

他们是什么关系:

  • 一个Channel包含一个ChannelPipeline,所有ChannelHandler都会顺序加入到ChannelPipeline中
  • 创建Channel时会自动创建一个ChannelPipeline,每个Channel都有一个管理它的pipeline,这关联是永久性的

Channel当状态出现变化,就会触发对应的事件

状态:
(1)channelRegistered: channel注册到一个EventLoop
(2)channelActive: 变为活跃状态(连接到了远程主机),可以接受和发送数据
(3)channelInactive: channel处于非活跃状态,没有连接到远程主机
(4)channelUnregistered: channel已经创建,但是未注册到一个EventLoop里面,也就是没有和Selector绑定

4、ChannelHandler和ChannelPipeline模块讲解

简介:讲解ChannelHandler和ChannelPipeline核心作用和生命周期
方法:
handlerAdded : 当 ChannelHandler 添加到 ChannelPipeline 调用
handlerRemoved : 当 ChannelHandler 从 ChannelPipeline 移除时调用
exceptionCaught : 执行抛出异常时调用

ChannelHandler下主要是两个子接口

  • ChannelInboundHandler:(入站)
    • 处理输入数据和Channel状态类型改变,
    • 适配器 ChannelInboundHandlerAdapter(适配器设计模式)
    • 常用的:SimpleChannelInboundHandler
  • ChannelOutboundHandler:(出站)
    • 处理输出数据,适配器 ChannelOutboundHandlerAdapter

ChannelPipeline:
好比厂里的流水线一样,可以在上面添加多个ChannelHanler,也可看成是一串 ChannelHandler 实例,拦截穿过 Channel 的输入输出 event, ChannelPipeline 实现了拦截器的一种高级形式,使得用户可以对事件的处理以及ChannelHanler之间交互获得完全的控制权

WechatIMG2

5、Netty核心模块指ChannelHandlerContext模块讲解

简介:讲解ChannelHandlerContext模块的作用和分析

1、ChannelHandlerContext是连接ChannelHandler和ChannelPipeline的桥梁
ChannelHandlerContext部分方法和Channel及ChannelPipeline重合,好比调用write方法,

Channel、ChannelPipeline、ChannelHandlerContext 都可以调用此方法,前两者都会在整个管道流里传播,而ChannelHandlerContext就只会在后续的Handler里面传播

2、AbstractChannelHandlerContext类
双向链表结构,next/prev分别是后继节点,和前驱节点

3、DefaultChannelHandlerContext 是实现类,但是大部分都是父类那边完成,这个只是简单的实现一些方法
主要就是判断Handler的类型。
ChannelInboundHandler之间的传递,主要通过调用ctx里面的fireXXX()方法来实现下个handler的调用

6、Netty案例实战常见问题之入站出站Handler执行顺序

简介: 讲解多个入站出站ChannelHandler的执行顺序

问题

一般的项目中,inboundHandler和outboundHandler有多个,在Pipeline中的执行顺序?

回答

InboundHandler顺序执行,OutboundHandler逆序执行
问题:ch.pipeline().addLast(new InboundHandler1());
ch.pipeline().addLast(new OutboundHandler1());
ch.pipeline().addLast(new OutboundHandler2());
ch.pipeline().addLast(new InboundHandler2());
或者:
ch.pipeline().addLast(new OutboundHandler1());
ch.pipeline().addLast(new OutboundHandler2());
ch.pipeline().addLast(new InboundHandler1());
ch.pipeline().addLast(new InboundHandler2());

执行顺序是:
InboundHandler1 channelRead
InboundHandler2 channelRead
OutboundHandler2 write
OutboundHandler1 write

结论

1)InboundHandler顺序执行,OutboundHandler逆序执行
2)InboundHandler之间传递数据,通过ctx.fireChannelRead(msg)
3)InboundHandler通过ctx.write(msg),则会传递到outboundHandler
4) 使用ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然outboundhandler会不执行;
但是使用channel.write(msg)、pipline.write(msg)情况会不一致,都会执行
5) outBound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接受数据,先outbound再inbound,服务端则相反

7、Netty异步操作模块ChannelFuture讲解

简介:讲解ChannelFuture异步操作模块及使用注意事项

Netty中的所有I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而ChannelFuture会提供有关的信息I/O操作的结果或状态。
  
1)ChannelFuture状态:

  • 未完成:当I/O操作开始时,将创建一个新的对象,新的最初是未完成的 - 它既没有成功,也没有成功,也没有被取消,因为I/O操作尚未完成。

  • 已完成:当I/O操作完成,不管是成功、失败还是取消,Future都是标记为已完成的, 失败的时候也有具体的信息,例如原因失败,但请注意,即使失败和取消属于完成状态。

  • 注意:

    • 不要在IO线程内调用future对象的sync或者await方法
    • 不能在channelHandler中调用sync或者await方法

2)ChannelPromise:继承于ChannelFuture,进一步拓展用于设置IO操作的结果

handle
WechatIMG1

第六章:网络数据传输编解码

1、什么是编码、解码

简介:讲解Netty编写的网络数据传输中的编码和解码

前面说的:高性能RPC框架的3个要素:IO模型、数据协议、线程模型

最开始接触的编码码:java序列化/反序列化(就是编解码)、url编码、base64编解码

为啥jdk有编解码,还要netty自己开发编解码?

java自带序列化的缺点
1)无法跨语言
2) 序列化后的码流太大,也就是数据包太大
3) 序列化和反序列化性能比较差

业界里面也有其他编码框架: google的 protobuf(PB)、Facebook的Trift、Jboss的Marshalling、Kyro等

Netty里面的编解码:

解码器:负责处理“入站 InboundHandler”数据
编码器:负责“出站 OutboundHandler” 数据

Netty里面提供默认的编解码器,也支持自定义编解码器
Encoder:编码器
Decoder:解码器
Codec:编解码器

2、解码器Decoder讲解

简介:讲解Netty的解码器Decoder和使用场景

Decoder对应的就是ChannelInboundHandler,主要就是字节数组转换为消息对象

主要是两个方法
decode
decodeLast

抽象解码器

1)ByteToMessageDecoder
用于将字节转为消息,需要检查缓冲区是否有足够的字节
2)ReplayingDecoder
继承ByteToMessageDecoder,不需要检查缓冲区是否有足够的字节,但是ReplayingDecoder速度略满于ByteToMessageDecoder,不是所有的ByteBuf都支持
选择:项目复杂性高则使用ReplayingDecoder,否则使用 ByteToMessageDecoder
3)MessageToMessageDecoder
用于从一种消息解码为另外一种消息(例如POJO到POJO)

解码器具体的实现

用的比较多的是(更多是为了解决TCP底层的粘包和拆包问题)

  • DelimiterBasedFrameDecoder: 指定消息分隔符的解码器
  • LineBasedFrameDecoder: 以换行符为结束标志的解码器
  • FixedLengthFrameDecoder:固定长度解码器
  • LengthFieldBasedFrameDecoder:message = header+body, 基于长度解码的通用解码器
  • StringDecoder:文本解码器,将接收到的对象转化为字符串,一般会与上面的进行配合,然后在后面添加业务handle

3、编码器Encoder讲解

简介:讲解Netty编码器Encoder

Encoder对应的就是ChannelOutboundHandler,消息对象转换为字节数组
Netty本身未提供和解码一样的编码器,是因为场景不同,两者非对等的

1)MessageToByteEncoder
消息转为字节数组,调用write方法,会先判断当前编码器是否支持需要发送的消息类型,如果不支持,则透传;

2)MessageToMessageEncoder
用于从一种消息编码为另外一种消息(例如POJO到POJO)

4、数据协议处理之Netty编解码器类Codec讲解

简介:讲解组合编解码器类Codec

组合解码器和编码器,以此提供对于字节和消息都相同的操作

优点:成对出现,编解码都是在一个类里面完成

缺点:耦合在一起,拓展性不佳

Codec:组合编解码
1)ByteToMessageCodec
2)MessageToMessageCodec

decoder:解码
1)ByteToMessageDecoder
2)MessageToMessageDecoder

encoder:编码
1)ByteToMessageEncoder
2)MessageToMessageEncoder

第七章:网络传输TCP粘包拆包

1、网络编程核心知识之TCP粘包拆包讲解

简介:讲解什么是TCP粘包拆包讲解

1)TCP拆包: 一个完整的包可能会被TCP拆分为多个包进行发送
2)TCP粘包: 把多个小的包封装成一个大的数据包发送, client发送的若干数据包 Server接收时粘成一包

出现的原因

  • 发送方的原因:TCP默认会使用Nagle算法
  • 接收方的原因: TCP接收到数据放置缓存中,应用程序从缓存中读取

UDP: 是没有粘包和拆包的问题,有边界协议

2、TCP半包读写常见解决方案

简介:讲解TCP半包读写常见的解决办法

发送方:可以关闭Nagle算法
接受方: TCP是无界的数据流,并没有处理粘包现象的机制, 且协议本身无法避免粘包,半包读写的发生需要在应用层进行处理
应用层解决半包读写的办法
1)设置定长消息 (10字符)
xdclass000xdclass000xdclass000xdclass000
2)设置消息的边界 (\[ 切割)
sdfafwefqwefwe\]dsafadfadsfwqehidwuehfiw\[879329832r89qweew\]
3)使用带消息头的协议,消息头存储消息开始标识及消息的长度信息
Header+Body

3、Netty自带解决TCP半包读写方案

简介:讲解Netty自带解决半包读写问题方案介绍

  • DelimiterBasedFrameDecoder: 指定消息分隔符的解码器
  • LineBasedFrameDecoder: 以换行符为结束标志的解码器
  • FixedLengthFrameDecoder:固定长度解码器
  • LengthFieldBasedFrameDecoder:message = header+body, 基于长度解码的通用解码器

4、Netty案例实战之半包读写问题演示

简介:案例实战之使用netty进行开发,出现的TCP半包读写问题

5、Netty案例实战之LineBasedFrameDecoder解决TCP半包读写

简介:讲解使用解码器LineBasedFrameDecoder解决半包读写问题

1)LineBaseFrameDecoder 以换行符为结束标志的解码器 ,构造函数里面的数字表示最长遍历的帧数
2)StringDecoder解码器将对象转成字符串

6、Netty案例实战之自定义分隔符解决TCP读写问题

简介:讲解使用DelimiterBasedFrameDecoder解决TCP半包读写问题

  • maxLength:
    • 表示一行最大的长度,如果超过这个长度依然没有检测自定义分隔符,将会抛出TooLongFrameException
  • failFast:
    • 如果为true,则超出maxLength后立即抛出TooLongFrameException,不进行继续解码
    • 如果为false,则等到完整的消息被解码后,再抛出TooLongFrameException异常
  • stripDelimiter:
    • 解码后的消息是否去除掉分隔符
  • delimiters:
    • 分隔符,ByteBuf类型

7、自定义长度半包读写器LengthFieldBasedFrameDecoder讲解

简介:自定义长度半包读写器LengthFieldBasedFrameDecoder讲解

官方文档:https://netty.io/4.0/api/io/netty/handler/codec/LengthFieldBasedFrameDecoder.html

  • maxFrameLength
    • 数据包的最大长度
  • lengthFieldOffset
    • 长度字段的偏移位,长度字段开始的地方,意思是跳过指定长度个字节之后的才是消息体字段
  • lengthFieldLength
    • 长度字段占的字节数, 帧数据长度的字段本身的长度
  • lengthAdjustment
    • 一般 Header + Body,添加到长度字段的补偿值,如果为负数,开发人员认为这个 Header的长度字段是整个消息包的长度,则Netty应该减去对应的数字
  • initialBytesToStrip
    • 从解码帧中第一次去除的字节数, 获取完一个完整的数据包之后,忽略前面的指定位数的长度字节,应用解码器拿到的就是不带长度域的数据包
  • failFast
    • 是否快速失败

第八章:Netty源码分析之基础数据传输讲解和设计模式

1、Netty核心模块缓冲ByteBuf

简介:讲解Netty核心之ByteBuf介绍,对比JDK原生ByteBuffer
ByteBuf:是数据容器(字节容器)

JDK ByteBuffer
共用读写索引,每次读写操作都需要Flip()
扩容麻烦,而且扩容后容易造成浪费

Netty ByteBuf
读写使用不同的索引,所以操作便捷
自动扩容,使用便捷

2、Netty数据存储模块ByteBuf创建方法和常用的模式

简介:讲解ByteBuf创建方法和常用的模式

ByteBuf:传递字节数据的容器

ByteBuf的创建方法

1)ByteBufAllocator
池化(Netty4.x版本后默认使用 PooledByteBufAllocator
提高性能并且最大程度减少内存碎片
非池化UnpooledByteBufAllocator: 每次返回新的实例
2)Unpooled: 提供静态方法创建未池化的ByteBuf,可以创建堆内存和直接内存缓冲区

ByteBuf使用模式

堆缓存区HEAP BUFFER:
优点:存储在JVM的堆空间中,可以快速的分配和释放
缺点:每次使用前会拷贝到直接缓存区(也叫堆外内存)

直接缓存区DIRECR BUFFER:
优点:存储在堆外内存上,堆外分配的直接内存,不会占用堆空间
缺点:内存的分配和释放,比在堆缓冲区更复杂

复合缓冲区COMPOSITE BUFFER:
可以创建多个不同的ByteBuf,然后放在一起,但是只是一个视图

选择:大量IO数据读写,用“直接缓存区”; 业务消息编解码用“堆缓存区”

3、Netty里面的设计模式应用分析

简介:讲解设计模式的在Netty里面的应用

  • Builder构造器模式:ServerBootstap
  • 责任链设计模式:pipeline的事件传播
  • 工厂模式: 创建Channel
  • 适配器模式:HandlerAdapter

推荐书籍:《Head First设计模式》

第九章:使用Netty搭建单机百万连接测试实战

1、搭建单机百万连接的服务器实例的必备知识

简介:搭建单机百万连接的服务器实例的必备知识
1、网络IO模型
2、Linux文件描述符
单进程文件句柄数(默认1024,不同系统不一样,每个进程都有最大的文件描述符限制)
全局文件句柄数

3、如何确定一个唯一的TCP连接
TCP四元组:源IP地址、源端口、目的ip、目的端口

2、Netty单机百万连接实战之服务端代码编写

简介:讲解Netty单机百万连接服务端代码编写

3、Netty单机百万连接实战之客户端代码编写

简介:讲解Netty单机百万连接之客户端代码编写

4、阿里云服务器Netty单机百万连接部署实战

简介:在阿里云服务器部署Netty服务端和Netty客户端代码

(如果没条件,则自己搭建虚拟机 6G,4核,centos6.5/7,需要关闭防火墙,或者使用云服务器需要开放安全组)

5、Netty单机百万连接Linux内核参数优化

简介:单机百万连接Linux核心参数优化

局部文件句柄限制

  • 说明
    • 单个进程最大文件打开数
    • 一个进程最大打开的文件数 fd 不同系统有不同的默认值
  • 查看
    • ulimit -n
  • 修改
    • root身份编辑,sudo vim /etc/security/limits.conf
    • 增加下面:
      • *表示当前用户,修改后要重启 config root soft nofile 1000000
        root hard nofile 1000000
        * soft nofile 1000000
        * hard nofile 1000000

全局文件句柄限制

  • 说明
    • 所有进程最大打开的文件数,不同系统是不一样,可以直接echo临时修改
  • 查看命令
    • cat /proc/sys/fs/file-max
  • 永久修改全局文件句柄,
    • sudo vim /etc/sysctl.conf
    • 增加 fs.file-max = 1000000
    • 修改后生效 sysctl -p

启动

java -jar millionServer-1.0-SNAPSHOT.jar -Xms5g -Xmx5g -XX:NewSize=3g -XX:MaxNewSize=3g

6、互联网架构数据链路分析总结

简介:讲解当下互联网架构中,数据链路分析总结


    输入域名-》浏览器内核调度-》本地DNS解析-》远程DNS解析-》ip -》路由多层调转-》目的服务器
    服务器内核-》代理服务器 nginx/ 网关 / 负载均衡设备-》目的服务器
    服务器内核-》 应用程序(springboot)-》Redis-》Mysql

第十章:高并发系列之百万连接Netty实战课程总结

1、高并发系列之百万连接Netty实战课程总结

简介:总结Netty实战课程和第二季展望

websocket
推送系统
RPC框架

《Netty权威指南》《Netty进阶之路》

概论

1

提高系统复用性的设计原则

“开一闭”原则( Open 一 Closed Principle , OCP )
里氏代换原则( Liskov substitution Principle , LSP )
依赖倒转原则 〔 Dependency Inversion principle , DIP )
接口隔离原则( Interfaces Segregation Principle , ISP )
组合聚合复用原则 〔 Composition/Aggregation Principle , CARP )
迪米特法则( Law of Demeter,LOD 〕

依赖倒转原则

要依赖于抽象,不要依赖于实现
表述一:应该让框架调用你的代码,而不要由你主动调用框架本身。
表述二:抽象不应依赖于细节,细节应当依赖于抽象。
表述三:要针对接口编程,不要针对实现编程。

昵称:好莱坞原则Hollywood Principle:
它和好莱坞大导演们的做派很相像。导演们常对寻求角色的人说 :
Don’t call us, we’ll call you
“别给我们打电话,我们打给你”
(不要调用我,让我来调用你)

设计模式分类

创建模式

封装动态产生对象的过程和所使用的类的信息,解决系统在创建对象时,抽象化类的实例化过程:

  • Abstract Factory 抽象工厂
  • Builder 建造者模式
  • Factory Method 工厂方法
  • Prototype 原型模式
  • Singleton 单例

    结构性模式

    考虑如何组合类和对象构成较大的结构。
    1.结构性类模式:使用继承来组合接口或实现
    2.结构性对象模式:对象合成实现新功能。

  • Adapter 适配器模式

  • Bridge 桥

  • Composite 组合模式

行为模式

主要解决算法和对象之间的责任分配问题。
行为模式描述了:

  • 对象或类的模式
  • 它们之间的通信模式。

Chain of Responsibility
Command
Interpreter
Iterator
Mediator

1. 创建模式

1.1. 简单工厂

2
使用同一个工厂类
每个工厂类可以有多于-个的工厂方法,分别负责创建不同的产品对象

  • 缺点
    • 工厂类集中了所有的产品创建逻辑,形成全能类(上帝类 )。当产品类有复杂的多层次等级结构时,工厂类需要判断创建产品时机和种类,增加新的产品导致工厂类的修改。使系统功能扩展困难。
    • 由于简单工厂模式使用静态方法,而静态方法无法由子类继承,因此,工厂角色无法形成基于继承的等级结构。
      以上缺点在工厂方法模式中得到克服。
@startuml

    together {
        interface Product
        Product <|.d. ConcreteProduct
    }

    together {
        class SimpleFactory {
            Product create()
        }
    }

    SimpleFactory -r-> Product
    SimpleFactory -> ConcreteProduct
@enduml
@startuml

    together {
        interface Fruit {
            void grow()
            void harvest()
            void plant()
        }
        class Grape {
            void grow()
            void harvest()
            void plant()
            --
            boolean seedless()
        }
        class Apple {
            void grow()
            void harvest()
            void plant()
            --
            int treeAge()
        }
        class Strawberry {
            void grow()
            void harvest()
            void plant()
        }
        
        Fruit <|.d. Grape
        Fruit <|.d. Apple
        Fruit <|.d. Strawberry
    }

    together {
        class FruitGradener {
            void plant()
        }
    }

    FruitGradener -r-> Fruit
    FruitGradener --> Grape
    FruitGradener --> Apple
    FruitGradener --> Strawberry
@enduml
  • 举例
    DateFormat
    XMLReaderFactory

1.2. 工厂方法

3
和简单工厂模式区别 :多态性

工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类

@startuml

    together {
        interface Product
        Product <|.d. ConcreteProduct
    }

    together {
        class Factory {
            Product create()
        }
        class ConcreteFactory {
            Product create()
        }
        ConcreteFactory .u.|> Factory     
    }

    Factory -r-> Product
    ConcreteFactory -r> ConcreteProduct
@enduml
@startuml

    together {
        interface Fruit {
            void grow()
            void harvest()
            void plant()
        }
        class Grape {
            void grow()
            void harvest()
            void plant()
            --
            boolean seedless()
        }
        class Apple {
            void grow()
            void harvest()
            void plant()
            --
            int treeAge()
        }
        class Strawberry {
            void grow()
            void harvest()
            void plant()
        }
        
        Fruit <|.d. Grape
        Fruit <|.d. Apple
        Fruit <|.d. Strawberry
    }

    together {
        class FruitGradener {
            void plant()
        }

        class GrapeGradener {
            void plant()
        }

        class AppleGradener {
            void plant()
        }

        class StrawberryGradener {
            void plant()
        }

        FruitGradener <|.d. GrapeGradener
        FruitGradener <|.d. AppleGradener
        FruitGradener <|.d. StrawberryGradener
    }

    FruitGradener -r-> Fruit
    GrapeGradener -r-> Grape
    AppleGradener -r-> Apple
    StrawberryGradener -r-> Strawberry
@enduml

1.3. 抽象工厂

4
在不指明类型的情况下,提供一个创建相联系或相依赖的对象族接口

@startuml

    together {
        interface ProductA
        ProductA <|.d. ConcreteProductA
    }

    together {
        interface ProductB
        ProductB <|.d. ConcreteProductB
    }

    together {
        class Factory {
            Product create()
        }
        class ConcreteFactory {
            Product create()
        }
        ConcreteFactory .u.|> Factory     
    }

    Factory -r-> ProductA
    Factory -r-> ProductB
    ConcreteFactory -r> ConcreteProductA
    ConcreteFactory -r> ConcreteProductB
@enduml
@startuml

    together {
        interface Window
        Window <|.d. PMWindow
        Window <|.d. MotifWindow
    }

    together {
        interface ScrollBar
        ScrollBar <|.d. PMScrollBar
        ScrollBar <|.d. MotifScrollBar
    }

    together {
        interface WidgetFactory {
            ScrollBar createScrollBar()
            Window createWindow()
        }
        class PMWidgetFactory {
            PMScrollBar createScrollBar()
            PMWindow createWindow()
        }
        class MotifWidgetFactory {
            MotifScrollBar createScrollBar()
            MotifWindow createWindow()
        }
        WidgetFactory .d.|> PMWidgetFactory     
        WidgetFactory .d.|> MotifWidgetFactory    
    }

    together {
        Class Client
    }

    WidgetFactory -r-> Window
    WidgetFactory -r-> ScrollBar
    PMWidgetFactory -r> PMScrollBar
    PMWidgetFactory -r> PMWindow
    MotifWidgetFactory -r> MotifScrollBar
    MotifWidgetFactory -r> MotifWindow

    Client -d->WidgetFactory
    Client -d->Window
    Client -d->ScrollBar
@enduml

1.4. 建造者模式

5
用过一个专用的Builder对象,封装对象的创建过程,有几个好处

  1. 更加的清晰易懂。
    • 相比于若干个重载的构造函数。
  2. 避免对象处于不一致的状态。
    • 如果用若干set方法,可能一个对象在还没有全部set完成就被外部对象使用了。
  3. 可以简化不可变对象的创建过程。
    • 不用重载若干个构造函数。
@startuml
    class Director {
    }

    class Builder {
        buildPart1()
        buildPart2()
        retrieveResult()
    }

    class ConcreteBuilder {
        buildPart1()
        buildPart2()
        retrieveResult()
    }

    class Product {
    }

    Director -r-> Builder: builder
    ConcreteBuilder -u-|> Builder
    ConcreteBuilder -r-> Product: create
    
@enduml

1.5. 原型模式

6
使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。

使用原型模式的另一个好处是简化对象的创建,使得创建对象就像我们在编辑文档时的复制粘贴一样简单。

@startuml
    class Client {
    }
    interface Prototype {
        clone()
    }
    class ConcretePrototype {
        clone()
    }
    
    ConcretePrototype .u.|> Prototype
    Client -r-> Prototype: prototype
    
@enduml

2. 结构模式

2.1. 单例模式

7

  • 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 主要解决:一个全局使用的类频繁地创建与销毁。
  • 何时使用:当您想控制实例数目,节省系统资源的时候。
  • 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  • 关键代码:构造函数是私有的。
  • 应用实例:
    • 1、一个班级只有一个班主任。
    • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
    • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
  • 优点:
    • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
    • 2、避免对资源的多重占用(比如写文件操作)。
  • 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
  • 使用场景:

    • 1、要求生产唯一序列号。
    • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
    • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
  • 注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

@startuml
    class Singleton {
        private Singleton instance
        public Singleton getInstance()
    }
    
    Singleton --o Singleton
@enduml

2.2. 适配器模式

将一种接口转换成另一种接口

@startuml
    interface Target {
        request()
    }
    class Adapter {
        request()
    }
    
    class Adaptee {
        otherKindRequest()
    }
    
    Client-r->Target
    Target<|-d-Adapter
    Adapter-r->Adaptee: adapt
@enduml
@startuml
    class Computer {
        read(SD sd)
    }
    interface SD {
    }
    class SDAdaptTF {
        private TF tf
    }
    class TF {
    }

    Computer-r->SD
    SD<|-d-SDAdaptTF
    SDAdaptTF-r->TF: adapt
    
    
@enduml

2.3. 桥接模式

意图:将抽象部分与实现部分分离,使它们都可以独立的变化。

主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。

何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。

如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。

关键代码:抽象类依赖实现类。

应用实例: 1、猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择。 2、墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。

优点: 1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。

缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。

使用场景: 1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。 2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。

注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。

@startuml
    Abstraction o-r- Implementor
    Abstraction <|-d- RefinedAbstraction
    Implementor <|-d- ConcreteImplementor
@enduml

参考8

2.4. 装饰者模式

是你还有你,一些拜托你

@startuml
    VisualComponent <.u. TextView
    VisualComponent <.u. Decorator
    Decorator --> VisualComponent
    Decorator <.u. ScrollDecorator
    Decorator <.u. BorderDecorator
    
@enduml

2.5. 组合模式

用来表示树形结构

@startuml
    Node o-- Node: has many of 
@enduml
@startuml
    class Graphic {
        draw()
        add(Graphic)
        remove(Graphic)
        getChild(int)
    }

    class Line {
        draw()
    }

    class Rectangle {
        draw()
    }

    class Text {
        draw()
    }

    class Picture {
        draw()
        add(Graphic)
        remove(Graphic)
        getChild(int)
    }

    Graphic <|-d- Line
    Graphic o-d- Line
    Graphic <|-d- Rectangle
    Graphic o-d- Rectangle
    Graphic <|-d- Text
    Graphic o-d- Text
    Graphic <|-d- Picture
    Graphic o-d- Picture
@enduml

2.6. 外观模式

为复杂的子系统创建一个简单的接口

@startuml

Client -d-> Facade

Facade -d-> SystemA
Facade -d-> SystemB
Facade -d-> SystemC


病人 -d-> 医院.接待员

namespace 医院 {
    接待员 -d-> 门诊
    接待员 -d-> 挂号
    接待员 -d-> 化验
    接待员 -d-> 取药
}

@enduml

参考9

2.7. 享元模式

意图:运用共享技术有效地支持大量细粒度的对象。

主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

何时使用: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。

如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。

关键代码:用 HashMap 存储这些对象。

应用实例: 1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。 2、数据库的数据池。

优点:大大减少对象的创建,降低系统的内存,使效率提高。

缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

使用场景: 1、系统有大量相似对象。 2、需要缓冲池的场景。

注意事项: 1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。 2、这些类必须有一个工厂对象加以控制。

@startuml

class FlyWeightPatternDemo {
    main()
    getRandomColor()
    getRandomX()
    getRandomY()
}

class ShapeFactory {
    circleMap: HashMap
    getCircle()
}

class shape.Shape {
    draw()
}

class shape.Circle {
    x, y, radius: int
}

FlyWeightPatternDemo -d-> ShapeFactory

ShapeFactory -r-> shape.Shape

namespace shape {
    Shape <|-d- Circle
}


@enduml

参考:1011

2.8. 代理模式

意图:为其他对象提供一种代理以控制对这个对象的访问。

主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

何时使用:想在访问一个类时做一些控制。

如何解决:增加中间层。

关键代码:实现与被代理类组合。

应用实例: 1、Windows 里面的快捷方式。 2、猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。 3、买火车票不一定在火车站买,也可以去代售点。 4、一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。 5、spring aop。

优点: 1、职责清晰。 2、高扩展性。 3、智能化。

缺点: 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

使用场景:按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。

注意事项: 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。

3. 行为模式

3.1. 责任链模式

意图:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。

主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。

何时使用:在处理消息的时候以过滤很多道。

如何解决:拦截的类都实现统一接口。

关键代码:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。

应用实例: 1、红楼梦中的"击鼓传花"。 2、JS 中的事件冒泡。 3、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,jsp servlet 的 Filter。

优点: 1、降低耦合度。它将请求的发送者和接收者解耦。 2、简化了对象。使得对象不需要知道链的结构。 3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。 4、增加新的请求处理类很方便。

缺点: 1、不能保证请求一定被接收。 2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。 3、可能不容易观察运行时的特征,有碍于除错。

使用场景: 1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 3、可动态指定一组对象处理请求。

注意事项:在 JAVA WEB 中遇到很多应用。

@startuml
Handler --> Handler: successor

Handler <|-d- ConcreteHandler1
Handler <|-d- ConcreteHandler2

Client -r-> Handler

@enduml
@startuml
AbstractLogger --> AbstractLogger: nextLogger

AbstractLogger <|-d- ConsoleLogger
AbstractLogger <|-d- ErrorLogger
AbstractLogger <|-d- FileLogger

Client -r-> AbstractLogger

class AbstractLogger {
    nextLogger: AbstractLogger

    setNextLogger()
    logMessage()
}

@enduml

3.2. 命令模式

介绍
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。

主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。

如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。

关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口

应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。

优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。

缺点:使用命令模式可能会导致某些系统有过多的具体命令类。

使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。

注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。

@startuml

Command <|-d- ConcreteCommand

Receiver -r-> Command

Client -r-> Receiver

@enduml
@startuml

Order <|-d- BuyStockOrder
Order <|-d- SellStockOrder


Broker -r-> Order

Client -r-> Broker

@enduml

3.3. 迭代器模式

意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。

主要解决:不同的方式来遍历整个整合对象。

何时使用:遍历一个聚合对象。

如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。

关键代码:定义接口:hasNext, next。

应用实例:JAVA 中的 iterator。

优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。

缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

使用场景: 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。

注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。

@startuml


class Container {
    {abstract} iterator(): Iterator
}

class MyContainer {
    iterator(): Iterator
}

class Iterator {
    {abstract} hasNext()
    {abstract} next()
}

class ConcreteIterator {
    hasNext()
    next()
}

Iterator <|-d- ConcreteIterator
Container <|-d- MyContainer
Container -r-> Iterator
MyContainer -r-> ConcreteIterator

Client --> Container
Client --> Iterator

@enduml

3.4. 模板方法模式

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

介绍
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

主要解决:一些方法通用,却在每一个子类都重新写了这一方法。

何时使用:有一些通用的方法。

如何解决:将这些通用算法抽象出来。

关键代码:在抽象类实现,其他步骤在子类实现。

应用实例: 1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3、spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。

优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。

缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。

注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。

@startuml


class AbstractClass {
    +templateMethod()
    #primitiveMethod1()
    #primitiveMethod2()
}

class ConcreteClassA {
    +templateMethod()
    #primitiveMethod1()
    #primitiveMethod2()
}

class ConcreteClassB {
    +templateMethod()
    #primitiveMethod1()
    #primitiveMethod2()
}

AbstractClass <|-d- ConcreteClassA
AbstractClass <|-d- ConcreteClassB


Client -r-> AbstractClass

@enduml

3.5. 观察者模式

@startuml
class Subject {
    attach()
    detach()
    notify()
}

class ConcreteSubject{
    getState()
    setState()
}

Subject <|-- ConcreteSubject


class Observer {
    update()
}

class ConcreteObserver {
    update()
}

Observer <|-- ConcreteObserver

Subject <-l- Observer: subscribe
Subject -r-> Observer: notify
@enduml

3.6. 状态模式

在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。

在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

介绍
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。

何时使用:代码中包含大量与对象状态有关的条件语句。

如何解决:将各种具体的状态类抽象出来。

关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。

应用实例: 1、打篮球的时候运动员可以有正常状态、不正常状态和超常状态。 2、曾侯乙编钟中,'钟是抽象接口','钟A'等是具体状态,'曾侯乙编钟'是具体环境(Context)。

优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。

注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。

@startuml
interface State {
    doAction()
}

class StartState {
    doAction()
}

class StopState {
    doAction()
}

State <|.d. StartState
State <|.d. StopState

class Context {
    state: State

    setState()
    getState()
}

State <-l- Context
@enduml

12
13
14

3.7. 策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

介绍
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。

何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。

如何解决:将这些算法封装成一个一个的类,任意地替换。

关键代码:实现同一个接口。

应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。

优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。

缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

@startuml
interface Strategy {
    doOperation()
}

class AddStrategy {
    doOperation()
}

class SubtractStrategy {
    doAction()
}

class MultiplyStrategy {
    doAction()
}

Strategy <|.d. AddStrategy
Strategy <|.d. SubtractStrategy
Strategy <|.d. MultiplyStrategy

class Context {
    strategy: Strategy

    execute()
}

Strategy <-l- Context
@enduml

3.8. 访问者模式

在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。

介绍
意图:主要将数据结构与数据操作分离。

主要解决:稳定的数据结构和易变的操作耦合问题。

何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。

如何解决:在被访问的类里面加一个对外提供接待访问者的接口。

关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。

应用实例:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。

优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。

缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。

使用场景: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。

注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。

@startuml
interface Element {
    accept(Visitor visitor)
}

class ConcreteElementA {
    accept(Visitor visitor)
}

class ConcreteElementB {
    accept(Visitor visitor)
}


Element <|-- ConcreteElementA
Element <|-- ConcreteElementB
ObjectStructure -r-> Element


interface Visitor {
    visit(ConcreteElementA a)
    visit(ConcreteElementB b)
}

class Visitor1 {
    visit(ConcreteElementA a)
    visit(ConcreteElementB b)
}

class Visitor2 {
    visit(ConcreteElementA a)
    visit(ConcreteElementB b)
}

Visitor <|-- Visitor1
Visitor <|-- Visitor2

Client --> ObjectStructure
Client --> Visitor
@enduml

15
16

3.9. 中介者模式

中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。

介绍
意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。

何时使用:多个类相互耦合,形成了网状结构。

如何解决:将上述网状结构分离为星型结构。

关键代码:对象 Colleague 之间的通信封装到一个类中单独处理。

应用实例: 1、中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。 2、机场调度系统。 3、MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。

优点: 1、降低了类的复杂度,将一对多转化成了一对一。 2、各个类之间的解耦。 3、符合迪米特原则。

缺点:中介者会庞大,变得复杂难以维护。

使用场景: 1、系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。 2、想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。

注意事项:不应当在职责混乱的时候使用。

@startuml
Mediator <|-- ConcreteMediator

Colleague <|-- ConcreteColleague1
Colleague <|-- ConcreteColleague2

Colleague -l-> Mediator
ConcreteMediator -r-> ConcreteColleague1
ConcreteMediator -r-> ConcreteColleague2
@enduml

3.10. 备忘录模式

备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。

介绍
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。

何时使用:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。

如何解决:通过一个备忘录类专门存储对象状态。

关键代码:客户不与备忘录类耦合,与备忘录管理类耦合。

应用实例: 1、后悔药。 2、打游戏时的存档。 3、Windows 里的 ctri + z。 4、IE 中的后退。 4、数据库的事务管理。

优点: 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 2、实现了信息的封装,使得用户不需要关心状态的保存细节。

缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

使用场景: 1、需要保存/恢复数据的相关状态场景。 2、提供一个可回滚的操作。

注意事项: 1、为了符合迪米特原则,还要增加一个管理备忘录的类。 2、为了节约内存,可使用原型模式+备忘录模式。

@startuml
class Originator {
    getStateFromMemento(Memento memento)
    saveStateToMemento()
    state: int
}

class Memento {
    getState()
    state: int
}

class Caretaker {
    mementoList: List<Memento>
    get(): Memento
    add(Memento memento)
}

Originator -r-> Memento
Caretaker -l-> Memento
@enduml

3.11. 解释器模式

解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。

介绍
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

主要解决:对于一些固定文法构建一个解释句子的解释器。

何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

如何解决:构建语法树,定义终结符与非终结符。

关键代码:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。

应用实例:编译器、运算表达式计算。

优点: 1、可扩展性比较好,灵活。 2、增加了新的解释表达式的方式。 3、易于实现简单文法。

缺点: 1、可利用场景比较少。 2、对于复杂的文法比较难维护。 3、解释器模式会引起类膨胀。 4、解释器模式采用递归调用方法。

使用场景: 1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。 2、一些重复出现的问题可以用一种简单的语言来进行表达。 3、一个简单语法需要解释的场景。

注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。

@startuml

interface Expression {
    interpret()
}

class TerminalExpression {
    data
    interpret()
}

class AndExpression {
    expr1: Expression
    expr2: Expression
    interpret()
}

class OrExpression {
    expr1: Expression
    expr2: Expression
    interpret()
}

Expression <|-- TerminalExpression
Expression <|-- AndExpression
Expression <|-- OrExpression


@enduml

4. 反模式

4.1. 开发反模式

  • The Blob 胖球 上帝类
  • Continuous Obsolescence 持续过时
  • Lava Flow 岩浆流 Dead Code 死代码
  • Ambiguous Viewpoint 模糊视角
  • Functional Decomposition 功能分解
  • Poltergeist 恶作剧鬼 类增殖
  • Boat Anchor 船锚
  • Golden Hammer 金锤 鸵鸟政策
  • Dead End 死胡同
  • Spaghetti Code 面条代码
  • Input Kludge 输入拼凑
  • Walk though a Mine Field 穿越雷区 Nothing Works
  • Cut And Paste Programming 剪贴编程
  • Mushroom Managerment 蘑菇管理 Pseudo-Analysis 伪分析

4.2. 架构反模式

参考

3.创建模式-下

4.结构模式-上

8.行为模式-下

9.设计模式复习

10.并发设计模式

11.常见的反模式-上

12.常见的反模式-下

posted in JavaWeb 

在进行SOA系统设计,设计微服务的接口形式,会遇到一些问题,

  • 入参是多个入参还是包装在一个对象中
  • 异常返回是通过Exception还是返回码
Read on →
posted in JavaWeb 

Read on →

Read on →
posted in JavaWeb 

Read on →

Read on →
posted in JavaWeb 

最近工作中涉及到两个对批量用户进行离线处理的工作:

  1. 对若干用户进行打标记。
  2. 对若干用户进行消息推送。

联想到之前也有很多这种类似的工作,索性把其中共用的部分抽离出来做成了框架——取名叫绿萝。

通过使用该容器,使用者编写处理业务相关的代码,业务无关的部分交给容器来解决。好比 Servlet和Tomcat之间的关系,绿萝作为一个容器来运行业务代码。

Read on →

最早是在CSDN上写博客的,可是渐渐发现CSDN写博客有诸多限制,而且CSDN博客时不时会挂掉,因此后面用Hexo在github上搭了一个新博客,也就是现在这个。然而这样一来很多文章就留在CSDN上,而在新博客上找不到了。

因此前几天用Java写了一个爬虫,把CSDN上的博客内容爬了下来,并解析成markdown格式存储在本地,这样就可以方便的迁移到新的博客了。

Read on →
posted in JavaWeb 

项目组有两个老项目需要从resin迁移到tomcat,因为这两个项目的调用方比较多,所以要保证从resin迁移到tomcat过程中对调用方完全透明才行。这就需要对resin和tomcat的不同之处进行处理。

在修改过程中踩了若干坑,最终相关的Resin2Tomcat处理代码有了一个相对稳定的版本。于是把这部分相关处理代码抽成了一个maven工程,上传到公司仓库,方便以后其他同学使用。

Read on →