运维同学反馈,集群的 NAT网关 的 tcp连接数过高,存在丢包风险
先让中间件的同学在集群的NAT网关服务器上,进行netstat 统计,初步定位到是请求外部系统a的连接数过高导致的问题
再在nat上进行抓包发现,是在请求外部系统的时候,外部系统主动关闭tcp请求导致的tcp连接没有复用 1
Connection: close
可以禁用keepalive 3查看http请求的header,果然找到了 Connection: close
再看代码,果然有隐藏的梗,找到原因了
日常会遇到一些问题:
OpenVPN的运行原理其实很简单,其核心机制就是在OpenVPN服务器和客户端所在的计算机上都安装一个虚拟网卡(又称虚拟网络适配器),并获得一个对应的虚拟IP地址。OpenVPN的服务器和多个客户端就可以通过虚拟网卡,使用这些虚拟IP进行相互访问了。其中,OpenVPN服务器起到一个路由和控制的作用(相当于一个虚拟的路由器)。
在本地运行ifconfig
可以看到所有的网卡
红圈中代表的意思是 utun3 这个虚拟网卡
IP是 172.31.69.66
网关是 172.31.69.66
子网掩码 0xffffff00
进行网络访问的时候,如果确定该走哪个网卡呢?
通过本地的路由表来确定。
通过以下命令查看路由表:
netstat -r
4
以代理软件surge为例:
要想使 Surge 实现后续的转发、修改和截获等功能,首先需要 Surge 对网络连接进行接管。
在 macOS 和 iOS 下,要想使程序发出的网络连接被另一个程序所接管,而不是直接将数据发送到物理网卡,有以下三种方式:
如果系统配置了代理服务器,那么程序在执行网络请求的时候,就不会直接连接目标服务器,而是产生一个发向代理服务器的连接。利用这个特性,可以在本地启动一个代理服务,并配置系统代理为 127.0.0.1 (即本机)的一个端口,这样就可以接管网络请求。
主流操作系统几乎都存在 TUN 和 TAP 两种虚拟网卡接口,原本是为了提供对 VPN 的支持。通过在系统中建立虚拟网卡并配置全局路由表,可以接管所有的网络请求。
这是 macOS 的一项内核特性,可以通过注入一个 Kernel Extension(kext)对所有 socket 调用进行 hook,以此接管请求。
这三种方式各有优劣:
Surge 主要使用方法 1 接管网络请求。方法 2 作为补充,接管不支持代理的程序。
以上的说明针对的是 Surge 对本机程序的接管。当使用 Surge 接管另一个设备的网络请求时:
举例说明:
192.168.1.0/26
等价于
192.168.1.0 netmask 0xffffffc0
代表
举例说明:
例1:A电脑IP地址为192.168.1.1,子网掩码为255.255.255.0;B电脑IP地址为192.168.1.2,子网掩码为255.255.225.0。大家都知道这二台电脑在同一网段,相互能PING通。
例2:A电脑的IP地址为192.168.1.1,子网掩码为255.255.255.0;B电脑的IP地址为192.168.2.1,子网掩码为255.255.0.0。大家分析一下二台电脑能相互PING通吗?
分析:这个问题需要大家理解子网掩码在网络通讯时的作用。不能简单的认为A电脑处在192.168.1.0网段,B电脑处在192.168.0.0网段,所以不能PING通。正确的分析应该如下:
⑴ 每台电脑事先会把自己IP和自己的子网掩码进行“与”操作,得到自己的网段号,如A电脑处在192.168.1.0网段,B电脑处在192.168.0.0网段。
⑵ B电脑向A电脑发数据包时,会把A电脑的IP与B电脑的子网掩码进行“与”操作,得到网络号是192.168.0.0,B电脑会认为A电脑与自己在同一网段,所以数据包会顺利发出。
⑶ A电脑由于与B电脑在同一网段,肯定能收到B电脑发出的数据包,由于PING操作要求A电脑回应一个响应包。这样A电脑会把B电脑的IP与A电脑的子网掩码进行“与”操作,得到网络号192.168.2.0,A电脑发现网络号与自己所处的192.168.1.0不在同一网段,由于A电脑目前没有设置默认网关,所以对该数据包将进行丢弃操作,结果B电脑当然就无法收到A电脑的回应包,所以B电脑上会显示“Request timed out”,即网络超时。
⑷ 如果在A电脑上去PING B电脑,根据前面的分析,A电脑会认为B电脑与A电脑不在网段,而A电脑又没有设置默认网关,所以会显示“Destination host unreachable”,即目标主机不可达。
新装的软件应该放在哪个目录下?
以前安装的的软件,应该在哪个目录下查找?
Directory | Description |
---|---|
/Applications | Self explanatory, this is where your Mac’s applications are kept |
/Developer | The Developer directory appears only if you have installed Apple’s Developer Tools, and no surprise, contains developer related tools, documentation, and files. |
/Library | Shared libraries, files necessary for the operating system to function properly, including settings, preferences, and other necessities (note: you also have a Libraries folder in your home directory, which holds files specific to that user). |
/Network | largely self explanatory, network related devices, servers, libraries, etc |
/System | System related files, libraries, preferences, critical for the proper function of Mac OS X |
/Users | All user accounts on the machine and their accompanying unique files, settings, etc. Much like /home in Linux |
/Volumes | Mounted devices and volumes, either virtual or real, such as hard disks, CD’s, DVD’s, DMG mounts, etc |
Directory | Description |
---|---|
/ | Root directory, present on virtually all UNIX based file systems. Parent directory of all other files |
/bin | Essential common binaries, holds files and programs needed to boot the operating system and run properly |
/etc | Machine local system configuration, holds administrative, configuration, and other system files |
/dev | Device files, all files that represent peripheral devices including keyboards, mice, trackpads, etc |
/usr | Second major hierarchy, includes subdirectories that contain information, configuration files, and other essentials used by the operating system |
/sbin | Essential system binaries, contains utilities for system administration |
/tmp | Temporary files, caches, etc |
/var | Variable data, contains files whose contents change as the operating system runs |
macOS Standard folders:例如 /Library | Posix or Unix directories:例如 /bin | Other directories:例如 /Volumes | |
---|---|---|---|
network domain | |||
system domain | /System/Library | /bin | |
user domain | /Library | /usr/bin | |
local domain | /Users/username/Library | /usr/local/bin |
一级路径 | 二级路径 | 介绍 | 举例 | |
---|---|---|---|---|
/Users/xxx | /Users/xxx | 各种用户常用的文件夹 | /Users/xxx/Desktop 是桌面文件夹 | |
/Users/xxx/Library | 各种应用的缓存、插件、数据 | /Users/chenzz/Library/Mail 保存邮件缓存 | ||
/Users/xxx/Library/Containers | 各种应用的缓存 | /Users/chenzz/Library/Containers/com.tencent.WeWorkMac 保存企业微信的缓存 | ||
/Users/xxx/Library/Application Support | 各种应用的插件 | /Users/chenzz/Library/Application Support/JetBrains 保存IDEA的各种插件信息 | ||
/usr/local | /usr/local/bin | 常用命令的文件夹 | /usr/local/bin/docker-compose 是快捷方式,指向 /Applications/Docker.app/Contents/Resources/bin/docker-compose | |
/usr/local/Cella | brew下载各种软件的路径 | |||
/Library | /Java/JavaVirtualMachines | 各种JDK的安装路径 | ||
/Application Surpport/BootCamp | bootcamp下载windows插件的路径 |
https://difyel.com/apple/macos/macos-directory-structure/#Standard_folders
]]>APM 通常认为是 Application Performance Management 的简写,它主要有三个方面的内容,分别是 Logs(日志)、Traces(链路追踪) 和 Metrics(报表统计)。
Logs 系统的重要性不言而喻,通常我们在排查特定的请求的时候,是非常依赖于上下文的日志的。
以前我们都是通过 terminal 登录到机器里面去查 log(我好几年都是这样过来的),但是由于集群化和微服务化的原因,继续使用这种方式工作效率会比较低,因为你可能需要登录好几台机器搜索日志才能找到需要的信息,所以需要有一个地方中心化存储日志,并且提供日志查询。
Logs 的典型实现是 ELK (ElasticSearch、Logstash、Kibana),三个项目都是由 Elastic 开源,其中最核心的就是 ES 的储存和查询的性能得到了大家的认可,经受了非常多公司的业务考验。
Logstash 负责收集日志,然后解析并存储到 ES。通常有两种比较主流的日志采集方式,一种是通过一个客户端程序 FileBeat,收集每个应用打印到本地磁盘的日志,发送给 Logstash;另一种则是每个应用不需要将日志存储到磁盘,而是直接发送到 Kafka 集群中,由 Logstash 来消费。
Kibana 是一个非常好用的工具,用于对 ES 的数据进行可视化,简单来说,它就是 ES 的客户端。
我们回过头来分析 Logs 系统,Logs 系统的数据来自于应用中打印的日志,它的特点是数据量可能很大,取决于应用开发者怎么打日志,Logs 系统需要存储全量数据,通常都要支持至少 1 周的储存。
每条日志包含 ip、thread、class、timestamp、traceId、message 等信息,它涉及到的技术点非常容易理解,就是日志的存储和查询。
使用也非常简单,排查问题时,通常先通过关键字搜到一条日志,然后通过它的 traceId 来搜索整个链路的日志。
题外话,Elastic 其实除了 Logs 以外,也提供了 Metrics 和 Traces 的解决方案,不过目前国内用户主要是使用它的 Logs 功能。
前面介绍的 Logs 系统使用的是开发者打印的日志,所以它是最贴近业务的。而 Traces 系统就离业务更远一些了,它关注的是一个请求进来以后,经过了哪些应用、哪些方法,分别在各个节点耗费了多少时间,在哪个地方抛出的异常等,用来快速定位问题。
经过多年的发展,Traces 系统虽然在服务端的设计很多样,但是客户端的设计慢慢地趋于统一,所以有了 OpenTracing 项目,我们可以简单理解为它是一个规范,它定义了一套 API,把客户端的模型固化下来。当前比较主流的 Traces 系统中,Jaeger、SkyWalking 是使用这个规范的,而 Zipkin、Pinpoint 没有使用该规范。限于篇幅,本文不对 OpenTracing 展开介绍。
从上面这个图中,可以非常方便地看出,这个请求经过了 3 个应用,通过线的长短可以非常容易看出各个节点的耗时情况。通常点击某个节点,我们可以有更多的信息展示,比如点击 HttpClient 节点我们可能有 request 和 response 的数据。
另一个比较好的开源 Traces 系统是由韩国人开源的 Pinpoint,它的打点数据非常丰富,这里有官方提供的 Live Demo,大家可以去玩一玩。
最近比较火的是由 CNCF(Cloud Native Computing Foundation) 基金会管理的 Jeager:
当然也有很多人使用的是 Zipkin,算是 Traces 系统中开源项目的老前辈了:
上面介绍的是目前比较主流的 Traces 系统,在排查具体问题的时候它们非常有用,通过链路分析,很容易就可以看出来这个请求经过了哪些节点、在每个节点的耗时、是否在某个节点执行异常等。
在 Metrics 方面做得比较好的开源系统,是大众点评开源的 Cat,下面这个图是 Cat 中的 transaction 视图,它展示了很多的我们经常需要关心的统计数据:
下图是 Cat 的 problem 视图,对我们开发者来说就太有用了,应用开发者的目标就是让这个视图中的数据越少越好。
Metrics 做的是数据统计,比如某个 URL 或 DB 访问被请求多少次,P90 是多少毫秒,错误数是多少等这种问题。而 Traces 是用来分析某次请求,它经过了哪些链路,比如进入 A 应用后,调用了哪些方法,之后可能又请求了 B 应用,在 B 应用里面又调用了哪些方法,或者整个链路在哪个地方出错等这些问题。
Metrics 和 Traces 之间的联系是非常紧密的,它们的数据结构都是一颗调用树,区别在于这颗树的枝干和叶子多不多。在 Traces 系统中,一个请求所经过的链路数据是非常全的,这样对排查问题的时候非常有用,但是如果要对 Traces 中的所有节点的数据做报表统计,将会非常地耗费资源,性价比太低。而 Metrics 系统就是面向数据统计而生的,所以树上的每个节点我们都会进行统计,所以这棵树不能太“茂盛”。
我们关心的其实是,哪些数据值得统计?首先是入口,其次是耗时比较大的地方,比如 db 访问、http 请求、redis 请求、跨服务调用等。当我们有了这些关键节点的统计数据以后,对于系统的健康监控就非常容易了。
对于一条 Message 来说,用于统计的字段是 type, name, status,所以我们能基于 type、type+name、type+name+status 三种维度的数据进行统计。
Message 中其他的字段:timestamp 表示事件发生的时间;success 如果是 false,那么该事件会在 problem 报表中进行统计;data 不具有统计意义,它只在链路追踪排查问题的时候有用;businessData 用来给业务系统上报业务数据,需要手动打点,之后用来做业务数据分析。
Message 有两个子类 Event 和 Transaction,区别在于 Transaction 带有 duration 属性,用来标识该 transaction 耗时多久,可以用来做 max time, min time, avg time, p90, p95 等,而 event 指的是发生了某件事,只能用来统计发生了多少次,并没有时间长短的概念。
Transaction 有个属性 children,可以嵌套 Transaction 或者 Event,最后形成一颗树状结构,用来做 trace,我们稍后再介绍。
Tree 的属性很好理解,它持有 root transaction 的引用,用来遍历整颗树。另外就是需要携带机器信息 messageEnv。
treeId 应该有个算法能保证全局唯一。
关于treeId的一些说明如下:
下面简单介绍几个 tree id 相关的内容,假设一个请求从 A->B->C->D 经过 4 个应用,A 是入口应用,那么会有:
1、总共会有 4 个 Tree 对象实例从 4 个应用投递到 Kafka,跨应用调用的时候需要传递 treeId, parentTreeId, rootTreeId 三个参数;
2、A 应用的 treeId 是所有节点的 rootTreeId;
3、B 应用的 parentTreeId 是 A 的 treeId,同理 C 的 parentTreeId 是 B 应用的 treeId;
4、在跨应用调用的时候,比如从 A 调用 B 的时候,为了知道 A 的下一个节点是什么,所以在 A 中提前为 B 生成 treeId,B 收到请求后,如果发现 A 已经为它生成了 treeId,直接使用该 treeId。
大家应该也很容易知道,通过这几个 tree id,我们是想要实现 trace 的功能。
Message 有两个子类 Event 和 Transaction,这里我们再加一个子类 Heartbeat,用来上报心跳数据。
我们主要收集了 thread、os、gc、heap、client 运行情况(产生多少个 tree,数据大小,发送失败数)等,同时也提供了 api 让开发者自定义数据进行上报。Dog client 会开启一个后台线程,每分钟运行一次 Heartbeat 收集程序,上报数据。
再介绍细一些。核心结构是一个 Map<String, Double>,key 类似于 “os.systemLoadAverage”,“thread.count” 等,前缀 os,thread,gc 等其实是用来在页面上的分类,后缀是显示的折线图的名称。
三种方案各有优缺点,
Pinpoint 和 Skywalking 使用的是 第一种方案,
Zipkin、Jaeger、Cat 使用的是第二种方案
阿里的Pandora是第三种方案。
JVMTI是一套Native接口,在Java SE 5之前,要实现一个Agent只能通过编写Native代码来实现。从Java SE 5开始,可以使用Java的Instrumentation接口(java.lang.instrument)来编写Agent。无论是通过Native的方式还是通过Java Instrumentation接口的方式来编写Agent,它们的工作都是借助JVMTI来进行完成,下面介绍通过Java Instrumentation接口编写Agent的方法。
public static void premain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs, Instrumentation inst);
我们的agent的可能会用到一些第三方的类库,这就会产生一个问题。
这些第三方的类库可能和业务应用中的类会产生冲突,导致各种奇怪的报错。
解决方案方案有几个:
本文概述了 apm的分类、核心数据结构、以及若干实现的关键知识点。
通过这些知识点的介绍,
一方面,在我们使用apm系统的时候,能更加的得心应手。做到知其然,知其所以然。
另一方面,基于这些涉及到的知识点,也可以方便实现一些其他有用的工具和中间件,提高业务效能。
SkyWalking 极简入门
APM 介绍与实现
APM系统的设计和实现
Dapper,大规模分布式系统的跟踪系统
Java 动态调试技术原理及实践
jvm-sandbox
Java字节码技术(二)字节码增强之ASM、JavaAssist、Agent、Instrumentation
阿里巴巴微服务架构演进
1、支持事物型消息
2、支持延时消息
3、支持消息重发
4、支持consumer端tag过滤
5、支持消息回放
1、基础对比
2、可靠性、可用性对比
3、功能对比
1、Broker主从部署,自身信息注册在NameServer中
2、Client从NameServer中获取Broker信息
3、NameServer节点相互独立,无数据交互
1)同步刷盘:性能低,可靠性高
2)异步刷盘:性能高,可靠性低
1)异步复制:多Master、多Slave模式-异步复制
2)同步双写:多Master、多Slave模式-同步双写
主从模式master宕机
集群搭建方式
1、CommitLog:存储消息主体
2、ConsumerQueue:消息消费队列
3、IndexFile:消息索引文件
1)同步(sync)
2) 异步(async)
3) 单向(oneway)
1)PUSH :消息队列主动地将消息推送给消费者
消息实时性高,但没有考虑客户端的消费能力
2)PULL:由消费者客户端主动向消息队列去拉消息
消息实时性低,可能造成大量无效请求
3) RocketMq采用拉去模式 【LongPoll()使用一种长轮询机制】,来平衡上面push/pull模型的各自缺点
Consumer发送拉去消息
Broker hold住请求,直到有新消息再返回
请求超时,Consumer再次发起请求
请求超时时间默认30m
1、Producer 端负载均衡
1)定时获取Queue信息
2)负载均衡算法:随机递增取模
3)容错机制:故障延迟
2)Consumer端负载均衡
1)客户端心跳上报数据
2)定时Rebalance20s
获取队列信息、获取消费者信息、排序平均分配
客户端负载均衡
1)获取队列信息
2)获取消费者信息
3)排序平均分配
4)与上次结果比对
1、半消息主题
1)HALF 消息:RMQ_SYS_TRANS_HALF_TOPIC(临时存放消息信息)
a: 事务消息替换主题,保存原主题和队列信息
b:半消息对Consumer不可见,不会被投递
2)OP 消息:RMQ_SYS_TRANS_OP_HALF_TOPIC(记录二阶段操作)
a:Rollback:只做记录
b:Commit:根据备份信息重新构造消息并投递
3)回查
a:对比HALF消息和OP消息进行回查
tar -xzvf apache-skywalking-apm-es7-8.7.0.tar.gz
cd apache-skywalking-apm-bin-es7
storage:
selector: elasticsearch7
启动
bin/oapService.sh
查看启动日志
# SkyWalking Agent 配置
export SW_AGENT_NAME=demo-application # 配置 Agent 名字。一般来说,我们直接使用 Spring Boot 项目的 `spring.application.name` 。
export SW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800 # 配置 Collector 地址。
export SW_AGENT_SPAN_LIMIT=2000 # 配置链路的最大 Span 数量。一般情况下,不需要配置,默认为 300 。主要考虑,有些新上 SkyWalking Agent 的项目,代码可能比较糟糕。
export JAVA_AGENT=-javaagent:/Users/chenzz/OpenSource/skywalking-distribution/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar # SkyWalking Agent jar 地址。
# Jar 启动
java -jar $JAVA_AGENT -jar lab-39-demo-2.2.2.RELEASE.jar
启动成功的日志:
DEBUG 2021-10-26 11:45:08:452 main AgentPackagePath : The beacon class location is jar:file:/Users/chenzz/OpenSource/skywalking-distribution/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar!/org/apache/skywalking/apm/agent/core/boot/AgentPackagePath.class.
INFO 2021-10-26 11:45:08:455 main SnifferConfigInitializer : Config file found in /Users/chenzz/OpenSource/skywalking-distribution/apache-skywalking-apm-bin-es7/agent/config/agent.config.
把 skywalking-java 作为一个module和agent工程在一个窗口打开
断点
git clone https://github.com/apache/skywalking.git
cd skywalking
git submodule init
git submodule update
mvn clean package -DskipTests
cd apm-dist/target # 编译结果目录
tar -zxvf apache-skywalking-apm-bin.tar.gz # 解压 Linux 包
cd apache-skywalking-apm-bin
ls -ls
SpringMVC 插件入口
org.apache.skywalking.apm.plugin.spring.mvc.v4.define.AbstractControllerInstrumentation
异步发送信息给server的代码入口 3
org.apache.skywalking.apm.agent.core.remote.TraceSegmentServiceClient#consume
获取traceId
TraceContext.traceId()
创建span (各种插件的入口)
org.apache.skywalking.apm.agent.core.context.ContextManager#createEntrySpan
协议:https://www.jianshu.com/p/7d9e060c68d2
https://e.naixuejiaoyu.com/live_pc/l_60d58980e4b057a4e7280ab0
调用FeignClient的时候,SpringCloud到底干了哪些事情?
这块工作是SpringCloud的OpenFeign模块做的,了解之后可以有几个用处:
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 省略代码……
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
// 省略代码……
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// 省略代码……
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
// 省略代码……
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
// 省略代码……
@Override
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
// 省略代码……
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
final class SynchronousMethodHandler implements MethodHandler {
// 省略代码……
@Override
public Object invoke(Object[] argv) throws Throwable {
// 省略代码……
response = client.execute(request, options);
// 省略代码……
Spring Cloud OpenFeign 原理浅析
springcloud @RibbonClients 与NamedContextFactory
Spring Cloud组件那么多超时设置,如何理解和运用?
SaaS、Paas、Iaas是IT行业云服务领域的几个概念,代表云服务的几种服务的层次。其实生活中,也存在类似SaaS还是IaaS的选择。这时候,SaaS通常是比IaaS更好的选择。
最近随着我家娃的长大,照片也越来越多,就有了云相册的需求。
因此需要有一个云相册满足以下需求:
因此总结出了几个解决方案:
除了云相册这件事之外,我发现了生活中大量类似的事情:一开始想选择IaaS的方案,结果花费了大量的时间精力却没有达到一个好的效果;后面采用SaaS的方案反而省心省力省钱。
例如,
觉得自己搭梯子翻(he)墙(xie)省钱,花费大量精力还不稳定;后面选用机场省心省力。
觉得自己开车方便,结果买车花钱停车收费扣分罚钱走错路浪费时间;后面直接打车,省心还能在车上睡觉休息。
阿里巴巴的CEO张勇家在上海,觉得杭州买房住太麻烦直接长期住在酒店。
自己买了自行车,只是偶尔骑结果还经常出问题;骑共享单车方便又便宜。
为了省钱看电影在网上花费很多精力搜集大量高清电影存在NAS中,最后看的没几部;反而不如开一个腾讯视频会员或者奈飞会员来的方便。
等等。
一图抵千言,相信看我文章的同学对我说的这三个概念都懂,不懂的话可以参考阮一峰老师的科普文章 IaaS,PaaS,SaaS 的区别。
我通过表格总结了以下这些场景。
SaaS | Paas或者Iaas | |
---|---|---|
云相册 | 一刻相册 | NAS |
翻(he)墙(xie) | 稳定的机(he)场(xie) | 谷歌云 |
出行 | 打车 | 租车或者买车 |
居住 | 住酒店 | 买房装修 |
电影 | 电影院或者视频会员 | 买NAS下载高清电影 |
玩游戏 | 网吧玩 | 买各种硬件在家玩 |
这些场景SaaS共同的的好处是:
省心、省钱、省时间、省精力、稳定,依赖小。
如果需要一项服务,充分衡量 SaaS和IaaS投入和产品,避免为了解决一个问题,而陷入到问题中去。
在那些高频使用的场景,或者吃饭相关的场景,需要采用IaaS的策略。
比如,阿里的云服务器都是自建的,而不是使用阿里云。
比如,如果一夜情找小姐是SaaS,找老婆是IaaS,那么也是应该采用IaaS的策略,毕竟找老婆涉及到后续的若干问题。😝
比如,专业滴滴司机也应该采用IaaS的策略买辆车跑滴滴,而不是租车跑滴滴。
比如,如果天天骑车上下班,那么买辆自行车也是更优的选择。
从IT行业的角度,如果遇到一个问题就直接SaaS其实可能错失了很多学习新知识的机会,这个算是采用SaaS策略的弊端吧。
不过随着人的成长,对于工作中负责事情的越来越深入,有些事情的确也就没那么多精力来兼顾了,这种情况下有所取舍才是更加明智的策略。
]]>定义
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1
和AVL树的管理
AVL树是平衡二叉树的一种实现
AVL树缺点
插入、删除元素的时候自平衡的过程非常复杂
牺牲了严格的高度平衡的优越条件为代价,实现
B树也称B-树,它是一颗多路平衡查找树。我们描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数。当m取2时,就是我们常见的二叉搜索树。
一颗m阶的B树定义如下:
1)每个结点最多有m-1个关键字。
2)根结点最少可以只有1个关键字。
3)非根结点至少有Math.ceil(m/2)-1个关键字。
4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
5)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。
上图是一颗阶数为4的B树。在实际应用中的B树的阶数m都非常大(通常大于100),所以即使存储大量的数据,B树的高度仍然比较小。每个结点中存储了关键字(key)和关键字对应的数据(data),以及孩子结点的指针。我们将一个key和其对应的data称为一个记录。但为了方便描述,除非特别说明,后续文中就用key来代替(key, value)键值对这个整体。在数据库中我们将B树(和B+树)作为索引结构,可以加快查询速速,此时B树中的key就表示键,而data表示了这个键对应的条目在硬盘上的逻辑地址。
各种资料上B+树的定义各有不同,一种定义方式是关键字个数和孩子结点个数相同。这里我们采取维基百科上所定义的方式,即关键字个数比孩子结点个数小1,这种方式是和B树基本等价的。上图就是一颗阶数为4的B+树。
除此之外B+树还有以下的要求。
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
83. 删除排序链表中的重复元素
82. 删除排序链表中的重复元素 II
206. 反转链表
92. 反转链表 II
21. 合并两个有序链表
86. 分隔链表
148. 排序链表
143. 重排链表
141. 环形链表
142. 环形链表 II
234. 回文链表
138. 复制带随机指针的链表
// 最好用
Deque<String> stack = new ArrayDeque<>();
// 也可以用
Stack<String> stack = new Stack<>();
stack.push();
stack.pop();
stack.peek();
Queue<String> queue = new LinkedList<String>();
queue.offer("a");
String s= queue.poll;
155. 最小栈
150. 逆波兰表达式求值
394. 字符串解码
94. 二叉树的中序遍历
133. 克隆图
200. 岛屿数量
84. 柱状图中最大的矩形
232. 用栈实现队列
542. 01 矩阵
104. 二叉树的最大深度
110. 平衡二叉树
124. 二叉树中的最大路径和
236. 二叉树的最近公共祖先
102. 二叉树的层序遍历
107. 二叉树的层序遍历 II
103. 二叉树的锯齿形层序遍历
98. 验证二叉搜索树
701. 二叉搜索树中的插入操作
35. 搜索插入位置
74. 搜索二维矩阵
240. 搜索二维矩阵 II
278. 第一个错误的版本
153. 寻找旋转排序数组中的最小值
154. 寻找旋转排序数组中的最小值 II
33. 搜索旋转排序数组
81. 搜索旋转排序数组 II
136. 只出现一次的数字
137. 只出现一次的数字 II
260. 只出现一次的数字 III
191. 位1的个数
338. 比特位计数
190. 颠倒二进制位
120. 三角形最小路径和
62. 不同路径
63. 不同路径 II
70. 爬楼梯
55. 跳跃游戏
45. 跳跃游戏 II
300. 最长递增子序列
139. 单词拆分
96. 不同的二叉搜索树
344. 反转字符串
24. 两两交换链表中的节点
509. 斐波那契数
78. 子集
90. 子集 II
46. 全排列
47. 全排列 II
131. 分割回文串
最近折腾一下了NAS,最终探索出了一个利用闲置电脑打造nas的方案。
仔细思索了下,使用一个nas主要出于以下目的:
3. 参考
* http://blog.zivers.com/post/2193.html
* https://www.zhihu.com/question/51578219
7. mac (10.15版本)上设置netbios以访问smb服务
1. echo “[default]” | sudo tee -a /etc/nsmb.conf
2. echo “port445=both” | sudo tee -a /etc/nsmb.conf
3. 打开finder - 菜单栏 - Go - Connect to server - smb://x.x.x.x
4. 参考:https://medium.com/@gobinathm/how-to-access-smb-printer-shares-in-macos-catalina-10-15-17ea91d2c10b
pc相比nas唯一的缺点可能是耗电量大一点。但是这点耗电量在今天显得微不足道。
比方说pc功率50w(我的是笔记本),那么一天电费是大概0.5元(峰电5毛,谷电3毛算),一年就是182元。
大概28年的电费花销抵得上一台nas花销加其对应的电费(nas算10w功耗)
计算方式:4000 / ((50 - 10) * 365 * 24 * (0.5 + 0.3) / 2) = 28
在对一个履约系统进行平台化改造过程中,沉淀了一套轻量化的平台化框架——shuke,供大家参考。
要重构的这一履约系统作为一个历史悠久的应用,各层代码中堆砌着各种针对不同业务的if else逻辑,进而导致了几个问题:
举一个例子,
履约平台在商家呼叫运力时,需要把履约任务下发给CP,而不同的业务下发的CP也是不一样的。
重构之前代码是这样的:
业务代码和平台代码混杂在一起,且层层if else嵌套
在系统重构的过程,通过shuke这一平台化框架,最终达到以下几个目标:
talk is cheap, show me the code.
重构完之后,效果是这样的
public interface DispatchNode {
DispatchNO dispatch2Cp(DispatchNOParam param);
}
DispatchNode dispatchNode = PluginRouter.routeNode(DispatchNode, FlowId.CHINA);
DispatchNO dispatchNO = dispatchNode.dispatch2Cp(param);
System.out.println(dispatchNO);
@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();
}
}
@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();
}
}
通过以上代码,便可以把业务代码和平台代码解耦开来。以后无论是现有业务的维护还是新业务的接入,都可以快速高效进行。
shuke涉及的部分有以下以下几部分:
最终的各模块之前的关系如下:
KISS:Keep it simple, keep it stupid.
Shuke设计的一大初衷便是足够的简单和轻量化,这也是shuke区分于现有的一些重量级平台化框架的最重要的特点。
相比于那些花费漫长时间学习上手、理解的平台化框架,shuke的一大特点是可以在几十分钟内了解其如何使用。学习成本的降低,意味着开发成本和犯错的机会都会随之降低。
做一个一个新生的框架,目前在一些地方还是待完善的,比方说不同业务方只有代码隔离没有容器隔离、业务插件不支持热部署等等,这些都是后续可以改进的地方。
git@gitlab.alibaba-inc.com:zhongzheng.czz/shuke.git
测试用例:
com.alibaba.ascp.shuke.core.test.ShukeTest#testDispatch
有什么意见或者建议,欢迎留言~
]]>Thread-Per-Connection模式
Reactor模式v1:单线程
Reactor模式v2:多线程
Reactore模式v3:主从多线程
粘包的主要原因:
半包的主要原因:
换个角度看:
根本原因:
Java对象和字节流之间相互转换。
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
ch.pipeline().addLast(new ProtobufDecoder(PersonOuterClass.Person.getDefaultInstance())); ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(new ProtobufEncoder());
生活中的例子:
如果打电话中别人忽然不说话了,你会问一句“你还在吗?”,如果没有回复,就挂断。
好处:
避免长时间占用线路,别人就打不进来了
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 次)
生活例子:
在打电话时,如果别人忽然不讲讲话了,隔一段时间后,你会问“你还在吗?”
总结:
Idle监测用做诊断,配合keepalive,减少keepalive消息。
Idle监测的演进:
v1:定时发送监测消息。
v2:有其他数据传输时,不发送监测消息。无数据传输时,定时发送监测消息。
bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true) bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)
提示:.option(ChannelOption.SO_KEEPALIVE,true) 存在但是无效
ch.pipeline().addLast(“idleCheckHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));
源码解读:
目标:
例 1:用基本类型就不要用包装类:
例 2: 应该定义成类变量的不要定义为实例变量:
例 3: Netty 中结合前两者:
io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long, boolean) 统计待写的请求的字节数
例 1:对于已经可以预知固定 size 的 HashMap避免扩容
可以提前计算好初始size或者直接使用 com.google.common.collect.Maps#newHashMapWithExpectedSize
例 2:Netty 根据接受到的数据动态调整(guess)下个要分配的 Buffer 的大小。可参考 io.netty.channel.AdaptiveRecvByteBufAllocator
例 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
优点:
缺点:
为什么引入对象池:
如何实现对象池?
源码解读:
大纲
简介:介绍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
简介:讲解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编写一个统一时间服务
简介:使用BIO网络编程编写BioClient客户端
简介:讲解BIO的优缺点,为啥不能高并发情况下性能弱
优点:
模型简单
编码简单
缺点:
性能瓶颈,请求数和线程数 N:N关系
高并发情况下,CPU切换线程上下文损耗大
案例:web服务器Tomcat7之前,都是使用BIO,7之后就使用NIO
改进:伪NIO,使用线程池去处理业务逻辑
简介:使用最通俗概念讲解 同步异步、堵塞和非堵塞
洗衣机洗衣服
网络IO,用户程序和内核的交互为基础进行讲解
IO操作分两步:
发起IO请求等待数据准备,
实际IO操作(洗衣服,晾衣服)
同步须要主动读写数据,在读写数据的过程中还是会阻塞(好比晾衣服阻塞了你)
异步仅仅须要I/O操作完毕的通知。并不主动读写数据,由操作系统内核完毕数据的读写(机器人帮你自动晾衣服)
五种IO的模型:
权威:RFC标准,或者书籍 《UNIX Network Programming》中文名《UNIX网络编程-卷一》第六章
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操作分为两步
前四种IO模型都是同步IO操作,
异步I/O模型在这两个阶段都处理
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,
几个核心点:
拓展阅读:
IO模型进化历史:https://www.zhihu.com/question/59975081
简介:高并发编程必备知识IO多路复用技术select、poll讲解
I/O多路复用,I/O是指网络I/O, 多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。
简单来说:就是使用一个或者几个线程处理多个TCP连接
最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程
基本原理:
监视文件3类描述符: writefds、readfds、和exceptfds
调用后select函数会阻塞住,等有数据 可读、可写、出异常 或者 超时 就会返回
select函数正常返回后,通过遍历fdset整个数组才能发现哪些句柄发生了事件,来找到就绪的描述符fd,然后进行对应的IO操作
几乎在所有的平台上支持,跨平台支持性好
缺点:
1)select采用轮询的方式扫描文件描述符,全部扫描,随着文件描述符FD数量增多而性能下降
2)每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
3)最大的缺陷就是单个进程打开的FD有限制,默认是1024 (可修改宏定义,但是效率仍然慢)
static final int MAX_FD = 1024
基本流程:
select() 和 poll() 系统调用的大体一样,处理多个描述符也是使用轮询的方式,根据描述符的状态进行处理
一样需要把 fd 集合从用户态拷贝到内核态,并进行遍历。
最大区别是: poll没有最大文件描述符限制(使用链表的方式存储fd)
基本原理:
在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 复杂
select:不修改宏定义,则需要 1000个进程才可以支持 100万连接
poll:100万个链接,遍历都响应不过来了,还有空间的拷贝消耗大量的资源
epoll:
简介:讲解java的IO演进历史
1、jdk1.4之前是采用同步阻塞模型,也就是BIO
大型服务一般采用C或者C++, 因为可以直接操作系统提供的异步IO,AIO
2、jdk1.4推出NIO,支持非阻塞IO,jdk1.7升级,推出NIO2.0,提供AIO的功能,支持文件和网络套接字的异步IO
设计模式——Reactor模式(反应器设计模式),是一种基于事件驱动的设计模式,在事件驱动的应用中,将一个或多个客户的服务请求分离(demultiplex)和调度(dispatch)给应用程序。在事件驱动的应用中,同步地、有序地处理同时接收的多个服务请求
一般出现在高并发系统中,比如Netty,Redis等
优点
1)响应快,不会因为单个同步而阻塞,虽然Reactor本身依然是同步的;
2)编程相对简单,最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
缺点
1)相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
2)Reactor模式需要系统底层的的支持,比如Java中的Selector支持,操作系统的select系统调用支持
通俗理解:KTV例子 前台接待,服务人员带领去开机器
Reactor模式基于事件驱动,适合处理海量的I/O事件,属于同步非阻塞IO(NIO)
内容:
1)作为NIO服务端,接收客户端的TCP连接;作为NIO客户端,向服务端发起TCP连接;
2)服务端读请求数据并响应;客户端写请求并读取响应
使用场景:
对应小业务则适合,编码简单;对于高负载、大并发的应用场景不适合,一个NIO线程处理太多请求,则负载过高,并且可能响应变慢,导致大量请求超时,而且万一线程挂了,则不可用了
内容:
1)一个Acceptor线程,一组NIO线程,一般是使用自带的线程池,包含一个任务队列和多个可用的线程
使用场景:
可满足大多数场景,但是当Acceptor需要做复杂操作的时候,比如认证等耗时操作,再高并发情况下则也会有性能问题
内容:
使用场景:
满足目前的大部分场景,也是Netty推荐使用的线程模型
BossGroup
WorkGroup
答案:
在Linux系统上,AIO的底层实现仍使用EPOLL,与NIO相同,因此在性能上没有明显的优势
Netty整体架构是reactor模型,采用epoll机制,所以往深的说,还是IO多路复用模式,所以也可说netty是同步非阻塞模型(看的层次不一样)
很多人说这是netty是基于Java NIO 类库实现的异步通讯框架
特点:异步非阻塞、基于事件驱动,性能高,高可靠性和高可定制性。
参考资料:
https://github.com/netty/netty/issues/2515
简介:讲解什么是Echo服务和快速创建Netty项目
1)什么是Echo服务:就是一个应答服务(回显服务器),客户端发送什么数据,服务端就响应的对应的数据
是一个非常有的用于调试和检测的服务
2)IDEA + Maven + jdk8
netty依赖包
3) maven地址:https://mvnrepository.com/artifact/io.netty/netty-all/4.1.32.Final
简介:讲解Echo服务-服务端程序编写实战,对应的启动类和handler处理器
简介:讲解Echo服务客户端程序编写
简介:分析整个Echo服务各个组件名称和作用
1)EventLoop和EventLoopGroup
2) Bootstrapt启动引导类
3)Channel 生命周期,状态变化
4)ChannelHandler和ChannelPipline
简介:源码讲解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)源码分析默认线程池数量
简介:讲解Netty启动引导类Bootstrap作用和tcp通道参数设置
参考:https://blog.csdn.net/QH_JAVA/article/details/78383543
2)channel:设置channel通道类型NioServerSocketChannel、OioServerSocketChannel
1)remoteAddress: 服务端地址
2)handler:和服务端通信的处理器
简介:讲解Channel作用,核心模块知识点,生命周期等
什么是Channel: 客户端和服务端建立的一个连接通道
什么是ChannelHandler: 负责Channel的逻辑处理
什么是ChannelPipeline: 负责管理ChannelHandler的有序容器
他们是什么关系:
Channel当状态出现变化,就会触发对应的事件
状态:
(1)channelRegistered: channel注册到一个EventLoop
(2)channelActive: 变为活跃状态(连接到了远程主机),可以接受和发送数据
(3)channelInactive: channel处于非活跃状态,没有连接到远程主机
(4)channelUnregistered: channel已经创建,但是未注册到一个EventLoop里面,也就是没有和Selector绑定
简介:讲解ChannelHandler和ChannelPipeline核心作用和生命周期
方法:
handlerAdded : 当 ChannelHandler 添加到 ChannelPipeline 调用
handlerRemoved : 当 ChannelHandler 从 ChannelPipeline 移除时调用
exceptionCaught : 执行抛出异常时调用
ChannelHandler下主要是两个子接口
ChannelPipeline:
好比厂里的流水线一样,可以在上面添加多个ChannelHanler,也可看成是一串 ChannelHandler 实例,拦截穿过 Channel 的输入输出 event, ChannelPipeline 实现了拦截器的一种高级形式,使得用户可以对事件的处理以及ChannelHanler之间交互获得完全的控制权
简介:讲解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的调用
简介: 讲解多个入站出站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,服务端则相反
简介:讲解ChannelFuture异步操作模块及使用注意事项
Netty中的所有I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而ChannelFuture会提供有关的信息I/O操作的结果或状态。
1)ChannelFuture状态:
未完成:当I/O操作开始时,将创建一个新的对象,新的最初是未完成的 - 它既没有成功,也没有成功,也没有被取消,因为I/O操作尚未完成。
已完成:当I/O操作完成,不管是成功、失败还是取消,Future都是标记为已完成的, 失败的时候也有具体的信息,例如原因失败,但请注意,即使失败和取消属于完成状态。
注意:
2)ChannelPromise:继承于ChannelFuture,进一步拓展用于设置IO操作的结果
简介:讲解Netty编写的网络数据传输中的编码和解码
前面说的:高性能RPC框架的3个要素:IO模型、数据协议、线程模型
最开始接触的编码码:java序列化/反序列化(就是编解码)、url编码、base64编解码
java自带序列化的缺点
1)无法跨语言
2) 序列化后的码流太大,也就是数据包太大
3) 序列化和反序列化性能比较差
业界里面也有其他编码框架: google的 protobuf(PB)、Facebook的Trift、Jboss的Marshalling、Kyro等
解码器:负责处理“入站 InboundHandler”数据
编码器:负责“出站 OutboundHandler” 数据
Netty里面提供默认的编解码器,也支持自定义编解码器
Encoder:编码器
Decoder:解码器
Codec:编解码器
简介:讲解Netty的解码器Decoder和使用场景
Decoder对应的就是ChannelInboundHandler,主要就是字节数组转换为消息对象
主要是两个方法
decode
decodeLast
1)ByteToMessageDecoder
用于将字节转为消息,需要检查缓冲区是否有足够的字节
2)ReplayingDecoder
继承ByteToMessageDecoder,不需要检查缓冲区是否有足够的字节,但是ReplayingDecoder速度略满于ByteToMessageDecoder,不是所有的ByteBuf都支持
选择:项目复杂性高则使用ReplayingDecoder,否则使用 ByteToMessageDecoder
3)MessageToMessageDecoder
用于从一种消息解码为另外一种消息(例如POJO到POJO)
用的比较多的是(更多是为了解决TCP底层的粘包和拆包问题)
简介:讲解Netty编码器Encoder
Encoder对应的就是ChannelOutboundHandler,消息对象转换为字节数组
Netty本身未提供和解码一样的编码器,是因为场景不同,两者非对等的
1)MessageToByteEncoder
消息转为字节数组,调用write方法,会先判断当前编码器是否支持需要发送的消息类型,如果不支持,则透传;
2)MessageToMessageEncoder
用于从一种消息编码为另外一种消息(例如POJO到POJO)
简介:讲解组合编解码器类Codec
组合解码器和编码器,以此提供对于字节和消息都相同的操作
优点:成对出现,编解码都是在一个类里面完成
缺点:耦合在一起,拓展性不佳
Codec:组合编解码
1)ByteToMessageCodec
2)MessageToMessageCodec
decoder:解码
1)ByteToMessageDecoder
2)MessageToMessageDecoder
encoder:编码
1)ByteToMessageEncoder
2)MessageToMessageEncoder
简介:讲解什么是TCP粘包拆包讲解
1)TCP拆包: 一个完整的包可能会被TCP拆分为多个包进行发送
2)TCP粘包: 把多个小的包封装成一个大的数据包发送, client发送的若干数据包 Server接收时粘成一包
UDP: 是没有粘包和拆包的问题,有边界协议
简介:讲解TCP半包读写常见的解决办法
发送方:可以关闭Nagle算法
接受方: TCP是无界的数据流,并没有处理粘包现象的机制, 且协议本身无法避免粘包,半包读写的发生需要在应用层进行处理
应用层解决半包读写的办法
1)设置定长消息 (10字符)
xdclass000xdclass000xdclass000xdclass000
2)设置消息的边界 (
dsafadfadsfwqehidwuehfiw
\[879329832r89qweew \] 3)使用带消息头的协议,消息头存储消息开始标识及消息的长度信息
Header+Body
简介:讲解Netty自带解决半包读写问题方案介绍
简介:案例实战之使用netty进行开发,出现的TCP半包读写问题
简介:讲解使用解码器LineBasedFrameDecoder解决半包读写问题
1)LineBaseFrameDecoder 以换行符为结束标志的解码器 ,构造函数里面的数字表示最长遍历的帧数
2)StringDecoder解码器将对象转成字符串
简介:讲解使用DelimiterBasedFrameDecoder解决TCP半包读写问题
简介:自定义长度半包读写器LengthFieldBasedFrameDecoder讲解
官方文档:https://netty.io/4.0/api/io/netty/handler/codec/LengthFieldBasedFrameDecoder.html
简介:讲解Netty核心之ByteBuf介绍,对比JDK原生ByteBuffer
ByteBuf:是数据容器(字节容器)
JDK ByteBuffer
共用读写索引,每次读写操作都需要Flip()
扩容麻烦,而且扩容后容易造成浪费
Netty ByteBuf
读写使用不同的索引,所以操作便捷
自动扩容,使用便捷
简介:讲解ByteBuf创建方法和常用的模式
ByteBuf:传递字节数据的容器
1)ByteBufAllocator
池化(Netty4.x版本后默认使用 PooledByteBufAllocator
提高性能并且最大程度减少内存碎片
非池化UnpooledByteBufAllocator: 每次返回新的实例
2)Unpooled: 提供静态方法创建未池化的ByteBuf,可以创建堆内存和直接内存缓冲区
堆缓存区HEAP BUFFER:
优点:存储在JVM的堆空间中,可以快速的分配和释放
缺点:每次使用前会拷贝到直接缓存区(也叫堆外内存)
直接缓存区DIRECR BUFFER:
优点:存储在堆外内存上,堆外分配的直接内存,不会占用堆空间
缺点:内存的分配和释放,比在堆缓冲区更复杂
复合缓冲区COMPOSITE BUFFER:
可以创建多个不同的ByteBuf,然后放在一起,但是只是一个视图
选择:大量IO数据读写,用“直接缓存区”; 业务消息编解码用“堆缓存区”
简介:讲解设计模式的在Netty里面的应用
推荐书籍:《Head First设计模式》
简介:搭建单机百万连接的服务器实例的必备知识
1、网络IO模型
2、Linux文件描述符
单进程文件句柄数(默认1024,不同系统不一样,每个进程都有最大的文件描述符限制)
全局文件句柄数
3、如何确定一个唯一的TCP连接
TCP四元组:源IP地址、源端口、目的ip、目的端口
简介:讲解Netty单机百万连接服务端代码编写
简介:讲解Netty单机百万连接之客户端代码编写
简介:在阿里云服务器部署Netty服务端和Netty客户端代码
(如果没条件,则自己搭建虚拟机 6G,4核,centos6.5/7,需要关闭防火墙,或者使用云服务器需要开放安全组)
简介:单机百万连接Linux核心参数优化
sudo vim /etc/security/limits.conf
*
表示当前用户,修改后要重启 root soft nofile 1000000
root hard nofile 1000000
* soft nofile 1000000
* hard nofile 1000000
java -jar millionServer-1.0-SNAPSHOT.jar -Xms5g -Xmx5g -XX:NewSize=3g -XX:MaxNewSize=3g
简介:讲解当下互联网架构中,数据链路分析总结
输入域名-》浏览器内核调度-》本地DNS解析-》远程DNS解析-》ip -》路由多层调转-》目的服务器
服务器内核-》代理服务器 nginx/ 网关 / 负载均衡设备-》目的服务器
服务器内核-》 应用程序(springboot)-》Redis-》Mysql
简介:总结Netty实战课程和第二季展望
websocket
推送系统
RPC框架
《Netty权威指南》《Netty进阶之路》
]]>动态映射:顾名思义,就是自动创建出来的映射。es 根据存入的文档,自动分析出来文档中字段的类型以及存储方式,这种就是动态映射。
静态映射:创建索引时指定 mappings
具体配置方式
dynamic 属性有三种取值:
true,默认即此。自动添加新字段。
false,忽略新字段。
strict,严格模式,发现新字段会抛出异常。
JSON 中的数据 | 自动推断出来的数据类型 |
---|---|
null | 没有字段被添加 |
true/false | boolean |
浮点数字 | float |
数字 | long |
JSON 对象 | object |
数组 | 数组中的第一个非空值来决定 |
string | text/keyword/date/double/long 都有可能 |
string:这是一个已经过期的字符串类型。在 es5 之前,用这个来描述字符串,现在的话,它已经被 text 和 keyword 替代了。
text:如果一个字段是要被全文检索的,比如说博客内容、新闻内容、产品描述,那么可以使用 text。用了 text 之后,字段内容会被分析,在生成倒排索引之前,字符串会被分词器分成一个个词项。text 类型的字段不用于排序,很少用于聚合。这种字符串也被称为 analyzed 字段。
keyword:这种类型适用于结构化的字段,例如标签、email 地址、手机号码等等,这种类型的字段可以用作过滤、排序、聚合等。这种字符串也称之为 not-analyzed 字段。
类型 | 取值范围 |
---|---|
long | -2^63到2^63-1 |
integer | -2^31到2^31-1 |
short | -2^15到2^15-1 |
byte | -2^7到2^7-1 |
double | 64 位的双精度 IEEE754 浮点类型 |
float | 32 位的双精度 IEEE754 浮点类型 |
half_float | 16 位的双精度 IEEE754 浮点类型 |
scaled_float | 缩放类型的浮点类型 |
由于 JSON 中没有日期类型,所以 es 中的日期类型形式就比较多样:
es 内部将时间转为 UTC,然后将时间按照 millseconds-since-the-epoch 的长整型来存储。
JSON 中的 “true”、“false”、true、false 都可以。
二进制接受的是 base64 编码的字符串,默认不存储,也不可搜索。
integer_range
float_range
long_range
double_range
date_range
ip_range
es 中没有专门的数组类型。默认情况下,任何字段都可以有一个或者多个值。需要注意的是,数组中的元素必须是同一种类型。
添加数组是,数组中的第一个元素决定了整个数组的类型。
由于 JSON 本身具有层级关系,所以文档包含内部对象。内部对象中,还可以再包含内部对象。
PUT product/_doc/2
{
"date":"2020-11-11T11:11:11Z",
"ext_info":{
"address":"China"
}
}
nested 是 object 中的一个特例。
如果使用 object 类型,假如有如下一个文档:
{
"user":[
{
"first":"Zhang",
"last":"san"
},
{
"first":"Li",
"last":"si"
}
]
}
由于 Lucene 没有内部对象的概念,所以 es 会将对象层次扁平化,将一个对象转为字段名和值构成的简单列表。即上面的文档,最终存储形式如下:
{
"user.first":["Zhang","Li"],
"user.last":["san","si"]
}
扁平化之后,用户名之间的关系没了。这样会导致如果搜索 Zhang si 这个人,会搜索到。
此时可以 nested 类型来解决问题,nested 对象类型可以保持数组中每个对象的独立性。nested 类型将数组中的每一饿对象作为独立隐藏文档来索引,这样每一个嵌套对象都可以独立被索引。
{
{
"user.first":"Zhang",
"user.last":"san"
},
{
"user.first":"Li",
"user.last":"si"
}
}
优点
文档存储在一起,读取性能高。
缺点
更新父或者子文档时需要更新更个文档。
GET操作
查询操作
文档的PUT, POST, BULK操作
UPDATE或DELETE操作
参考:https://cloud.tencent.com/developer/article/1546416
https://blog.csdn.net/tanga842428/article/details/75127418
Elasticsearch是通过Lucene的倒排索引技术实现比关系型数据库更快的过滤。
特别是它对多条件的过滤支持非常好,比如年龄在18和30之间,性别为女性这样的组合查询。
倒排索引很多地方都有介绍,但是其比关系型数据库的b-tree索引快在哪里?到底为什么快呢?
笼统的来说,b-tree索引是为写入优化的索引结构。当我们不需要支持快速的更新的时候,可以用预先排序等方式换取更小的存储空间,更快的检索速度等好处,其代价就是更新慢。
要进一步深入的化,还是要看一下Lucene的倒排索引是怎么构成的。
这里有好几个概念。我们来看一个实际的例子,假设有如下的数据:
这里每一行是一个document。每个document都有一个docid。那么给这些document建立的倒排索引就是:
可以看到,倒排索引是per field的,一个字段由一个自己的倒排索引。18,20这些叫做 term,而[1,3]
就是posting list。
Posting list就是一个int的数组,存储了所有符合某个term的文档id。
那么什么是term dictionary 和 term index?
假设我们有很多个term,比如:
Carla,Sara,Elin,Ada,Patty,Kate,Selena
如果按照这样的顺序排列,找出某个特定的term一定很慢,因为term没有排序,需要全部过滤一遍才能找出特定的term。排序之后就变成了:
Ada,Carla,Elin,Kate,Patty,Sara,Selena
这样我们可以用二分查找的方式,比全遍历更快地找出目标的term。这个就是 term dictionary。
有了term dictionary之后,可以用 logN 次磁盘查找得到目标。
但是磁盘的随机读操作仍然是非常昂贵的(一次random access大概需要10ms的时间)。
所以尽量少的读磁盘,有必要把一些数据缓存到内存里。
但是整个term dictionary本身又太大了,无法完整地放到内存里。
于是就有了term index。term index有点像一本字典的大的章节表。比如:
A开头的term ……………. Xxx页
C开头的term ……………. Xxx页
E开头的term ……………. Xxx页
如果所有的term都是英文字符的话,可能这个term index就真的是26个英文字符表构成的了。
但是实际的情况是,term未必都是英文字符,term可以是任意的byte数组。
而且26个英文字符也未必是每一个字符都有均等的term,比如x字符开头的term可能一个都没有,而s开头的term又特别多。
实际的term index是一棵trie 树:
例子是一个包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 树。这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。再加上一些压缩技术(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的几十分之一,使得用内存缓存整个term index变成可能。整体上来说就是这样的效果。
现在我们可以回答“为什么Elasticsearch/Lucene检索可以比mysql快了。
Mysql只有term dictionary这一层,是以b-tree排序的方式存储在磁盘上的。检索一个term需要若干次的random access的磁盘操作。
而Lucene在term dictionary的基础上添加了term index来加速检索,term index以树的形式缓存在内存中。
从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘的random access次数。
额外值得一提的两点是:term index在内存中是以FST(finite state transducers)的形式保存的,其特点是非常节省内存。
Term dictionary在磁盘上是以分block的方式保存的,一个block内部利用公共前缀压缩,比如都是Ab开头的单词就可以把Ab省去。这样term dictionary可以比b-tree更节约磁盘空间。
所以给定查询过滤条件 age=18 的过程就是先从term index找到18在term dictionary的大概位置,然后再从term dictionary里精确地找到18这个term,然后得到一个posting list或者一个指向posting list位置的指针。然后再查询 gender=女 的过程也是类似的。最后得出 age=18 AND gender=女 就是把两个 posting list 做一个“与”的合并。
这个理论上的“与”合并的操作可不容易。对于mysql来说,如果你给age和gender两个字段都建立了索引,查询的时候只会选择其中最selective的来用,然后另外一个条件是在遍历行的过程中在内存中计算之后过滤掉。那么要如何才能联合使用两个索引呢?有两种办法:
PostgreSQL 从 8.4 版本开始支持通过bitmap联合使用两个索引,就是利用了bitset数据结构来做到的。当然一些商业的关系型数据库也支持类似的联合索引的功能。Elasticsearch支持以上两种的联合索引方式,如果查询的filter缓存到了内存中(以bitset的形式),那么合并就是两个bitset的AND。如果查询的filter没有缓存,那么就用skip list的方式去遍历两个on disk的posting list。
以上是三个posting list。我们现在需要把它们用AND的关系合并,得出posting list的交集。首先选择最短的posting list,然后从小到大遍历。遍历的过程可以跳过一些元素,比如我们遍历到绿色的13的时候,就可以跳过蓝色的3了,因为3比13要小。
整个过程如下
Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!
最后得出的交集是[13,98],所需的时间比完整遍历三个posting list要快得多。但是前提是每个list需要指出Advance这个操作,快速移动指向的位置。什么样的list可以这样Advance往前做蛙跳?skip list:
从概念上来说,对于一个很长的posting list,比如:
[1,3,13,101,105,108,255,256,257]
我们可以把这个list分成三个block:
[1,3,13] [101,105,108] [255,256,257]
然后可以构建出skip list的第二层:
[1,101,255]
1,101,255分别指向自己对应的block。这样就可以很快地跨block的移动指向位置了。
Lucene自然会对这个block再次进行压缩。其压缩方式叫做Frame Of Reference编码。示例如下:
考虑到频繁出现的term(所谓low cardinality的值),比如gender里的男或者女。如果有1百万个文档,那么性别为男的posting list里就会有50万个int值。用Frame of Reference编码进行压缩可以极大减少磁盘占用。这个优化对于减少索引尺寸有非常重要的意义。当然mysql b-tree里也有一个类似的posting list的东西,是未经过这样压缩的。
因为这个Frame of Reference的编码是有解压缩成本的。利用skip list,除了跳过了遍历的成本,也跳过了解压缩这些压缩过的block的过程,从而节省了cpu。
Bitset是一种很直观的数据结构,对应posting list如:
[1,3,4,7,10]
对应的bitset就是:
[1,0,1,1,0,0,1,0,0,1]
每个文档按照文档id排序对应其中的一个bit。Bitset自身就有压缩的特点,其用一个byte就可以代表8个文档。所以100万个文档只需要12.5万个byte。但是考虑到文档可能有数十亿之多,在内存里保存bitset仍然是很奢侈的事情。而且对于个每一个filter都要消耗一个bitset,比如age=18缓存起来的话是一个bitset,18<=age<25是另外一个filter缓存起来也要一个bitset。
所以秘诀就在于需要有一个数据结构:
Lucene使用的这个数据结构叫做 Roaring Bitmap。
其压缩的思路其实很简单。与其保存100个0,占用100个bit。还不如保存0一次,然后声明这个0重复了100遍。
这两种合并使用索引的方式都有其用途。Elasticsearch对其性能有详细的对比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。简单的结论是:因为Frame of Reference编码是如此 高效,对于简单的相等条件的过滤缓存成纯内存的bitset还不如需要访问磁盘的skip list的方式要快。
一种常见的压缩存储时间序列的方式是把多个数据点合并成一行。Opentsdb支持海量数据的一个绝招就是定期把很多行数据合并成一行,这个过程叫compaction。类似的vivdcortext使用mysql存储的时候,也把一分钟的很多数据点合并存储到mysql的一行里以减少行数。
这个过程可以示例如下:
可以看到,行变成了列了。每一列可以代表这一分钟内一秒的数据。
Elasticsearch有一个功能可以实现类似的优化效果,那就是Nested Document。我们可以把一段时间的很多个数据点打包存储到一个父文档里,变成其嵌套的子文档。示例如下:
{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}
可以打包成:
{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}
这样可以把数据点公共的维度字段上移到父文档里,而不用在每个子文档里重复存储,从而减少索引的尺寸。
在存储的时候,无论父文档还是子文档,对于Lucene来说都是文档,都会有文档Id。但是对于嵌套文档来说,可以保存起子文档和父文档的文档id是连续的,而且父文档总是最后一个。有这样一个排序性作为保障,那么有一个所有父文档的posting list就可以跟踪所有的父子关系。也可以很容易地在父子文档id之间做转换。把父子关系也理解为一个filter,那么查询时检索的时候不过是又AND了另外一个filter而已。前面我们已经看到了Elasticsearch可以非常高效地处理多filter的情况,充分利用底层的索引。
使用了嵌套文档之后,对于term的posting list只需要保存父文档的doc id就可以了,可以比保存所有的数据点的doc id要少很多。如果我们可以在一个父文档里塞入50个嵌套文档,那么posting list可以变成之前的1/50。
Refresh
,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,但是可以提供文档的检索功能且不能被修改。512M
或者时间超过 30
分钟时,会触发一次 Flush
。内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据通过 Fsync
刷新到磁盘中,生成提交点,日志文件被删除,创建一个空的新日志。ES
不仅要根据提交点去加载已经持久化过的段,还需要读取 Translog
里的记录,把未持久化的数据重新持久化到磁盘上,避免了数据丢失的可能。“开一闭”原则( 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
“别给我们打电话,我们打给你”
(不要调用我,让我来调用你)
封装动态产生对象的过程和所使用的类的信息,解决系统在创建对象时,抽象化类的实例化过程:
考虑如何组合类和对象构成较大的结构。
1.结构性类模式:使用继承来组合接口或实现
2.结构性对象模式:对象合成实现新功能。
主要解决算法和对象之间的责任分配问题。
行为模式描述了:
Chain of Responsibility
Command
Interpreter
Iterator
Mediator
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
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
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
5
用过一个专用的Builder对象,封装对象的创建过程,有几个好处
@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
6
使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。
使用原型模式的另一个好处是简化对象的创建,使得创建对象就像我们在编辑文档时的复制粘贴一样简单。
@startuml
class Client {
}
interface Prototype {
clone()
}
class ConcretePrototype {
clone()
}
ConcretePrototype .u.|> Prototype
Client -r-> Prototype: prototype
@enduml
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
优点:
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
@startuml
class Singleton {
private Singleton instance
public Singleton getInstance()
}
Singleton --o Singleton
@enduml
将一种接口转换成另一种接口
@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
意图:将抽象部分与实现部分分离,使它们都可以独立的变化。
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
关键代码:抽象类依赖实现类。
应用实例: 1、猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择。 2、墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。
优点: 1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。
缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
使用场景: 1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。 2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
@startuml
Abstraction o-r- Implementor
Abstraction <|-d- RefinedAbstraction
Implementor <|-d- ConcreteImplementor
@enduml
参考[^bridge_model]
是你还有你,一些拜托你
@startuml
VisualComponent <.u. TextView
VisualComponent <.u. Decorator
Decorator --> VisualComponent
Decorator <.u. ScrollDecorator
Decorator <.u. BorderDecorator
@enduml
用来表示树形结构
@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
为复杂的子系统创建一个简单的接口
@startuml
Client -d-> Facade
Facade -d-> SystemA
Facade -d-> SystemB
Facade -d-> SystemC
病人 -d-> 医院.接待员
namespace 医院 {
接待员 -d-> 门诊
接待员 -d-> 挂号
接待员 -d-> 化验
接待员 -d-> 取药
}
@enduml
参考8
意图:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
何时使用: 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
参考:[^fly_weight][^fly_weight2]
意图:为其他对象提供一种代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
何时使用:想在访问一个类时做一些控制。
如何解决:增加中间层。
关键代码:实现与被代理类组合。
应用实例: 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、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
意图:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
何时使用:在处理消息的时候以过滤很多道。
如何解决:拦截的类都实现统一接口。
关键代码: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
介绍
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。
关键代码:定义三个角色: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
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
主要解决:不同的方式来遍历整个整合对象。
何时使用:遍历一个聚合对象。
如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。
关键代码:定义接口: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
在模板模式(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
@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
在状态模式(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
[^state_model1]
[^state_model2]
[^state_model3]
在策略模式(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
在访问者模式(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
中介者模式(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
备忘录模式(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
解释器模式(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
前段时间有项目有读写分离的需要,因此完成了该类库mybatis-read-write-split
来实现读写分离。
以上两种模式可以混合使用:缺省自动解析sql的读写类型,如果注解有指定数据源,则根据注解进行路由。
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-read-write-split-core</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<!--替换原本的DataSource-->
<bean id="dataSource" class="org.mybatis.rw.MultiReadDataSource">
<property name="masterDataSource" ref="masterDataSource"/>
<property name="slaveDataSourceList">
<list>
<ref bean="slaveDataSource1"></ref>
<ref bean="slaveDataSource2"></ref>
</list>
</property>
</bean>
<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<!--各数据源配置-->
</bean>
<bean id="slaveDataSource1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<!--各数据源配置-->
</bean>
<bean id="slaveDataSource2" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<!--各数据源配置-->
</bean>
mybatis自动分析读or写操作,并进行相应的路由操作
<plugins>
<plugin interceptor="org.mybatis.rw.interceptor.ReadWriteDistinguishInterceptor">
</plugin>
</plugins>
通过方法上的注解显示指定读主库or备库
@DataSource()
如
@DataSource(DataSourceType.MASTER)
public User getUserByIdFromMaster(Integer userId) {
//some operation
}
在进行SOA系统设计,设计微服务的接口形式,会遇到一些问题,
以下是开发过程中的一些思考与实践:
定义成 GetUserInfoResp getUserInfo(GetUserInfoReq req)
的形式,
入参
GetUserInfoReq
继承了AbstractReq
,AbstractReq
保存了公用的请求参数成员变量
,如username、ip
等出参
GetUserInfoResp
继承了AbstractResp
,AbstractResp
保存了公用的返回码成员变量
,如retCode
等interface UserProvider {
GetUserInfoResp getUserInfo(GetUserInfoReq req);
}
class GetUserInfoReq extends AbstractReq {
}
class AbstractReq {
private String username;
private String ip;
}
class GetUserInfoResp extends AbstractResp {
private String school;
private double weight;
}
class AbstractResp {
private String retCode;
private String message;
}
为什么要把各种成员变量包装在一个Req
对象中,而不是做成getUserInfo(String username, String disctrict)
的形式呢?
如果这个接口有两个调用方,某一天这个接口新添加了一个入参int gender
,其中一个调用方会传这个参数,另一个调用方不传。
如果是当前这个方案,只需要在Req
对象中添加一个成员变量int gender
即可,然后两个消费方都可以兼容的进行调用。
如果是采用getUserInfo(String username, String disctrict)
的方案,那么就只能新加一个方法getUserInfo(String username, String disctrict, int gender)
了,造成了代码的冗余。
如果某一天,这个系统要支持不同国家的业务,所有的接口都要添加一个参数String nation
。
那么基于当前的方案,只需要在AbstractReq
中添加一个String nation
即可,非常方便。
如果基于getUserInfo(String username, String disctrict)
的形式,那么可能需要批量修改几百个接口,花费了大量的时间,同时也提高了错误发生的概率。
在SOA的系统设计中,往往会在内部系统的前面添加一个gateway系统来做一些公共逻辑,网关对外提供http服务,而网关和内部系统通过专有协议进行通信,
想想这样一个场景,/user/getUserInfo
这个接口提供获取用户信息的服务,用户的请求body可能有以下几种形式:
{
"name": "jason",
"nation": "america"
}
{
"nation": "america",
"name": "jason"
}
要把这些形式的请求映射到后端接口上:
即如果是getUserInfo(GetUserInfoReq req)
的形式
那么网关很好设计,
GetUserInfoReq
对象即可。即如果getUserInfo(String name, String nation)
的形式
那么网关很不好设计,
提前记录url和接口的映射规则,
这个记录会很麻烦,因为需要记录 body中的每个参数和接口中的第几个参数是对应的
映射也很麻烦,
需要遍历body中的变量,把body中的某个变量映射成接口第n个参数
究其原因,是因为通过反射可以获取到一个对象的成员变量的名称,却无法获取到一个方法的参数名称。
封装成Req对象,可以把相关的函数封装在Req对象内部,比如validateParam()
方法。
更进一步,可以定义一个BaseReq
,在其内部定义一个 abstract validateParam()
方法,来强制所有的Req对象定义validateParam()
方法。
public
为什么通过通过retCode
来包装异常情况,而不是直接抛出异常?
如果getUserInfo
通过异常来表达业务异常情况,那调用方势必要接触到 提供方的各种Exception类型,大大提高了系统的耦合性。
Exception会带着一个调用栈,
如果通过网络抛出异常,一方面会占用带宽,一方面会造成延迟。
基于以上原因,应该采用GetUserInfoResp getUserInfo(GetUserInfoReq req)
的形式定义接口。
一个最简单的MyBatis Demo
源码下载:mybatis-demo
对应DB表:mybatis-demo
public class UserService {
public User getUserById(Integer userId) {
SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
return userMapper.getUserById(userId);
} finally {
sqlSession.close();
}
}
}
public class MyBatisUtil
{
private static SqlSessionFactory factory;
private MyBatisUtil() {
}
static
{
Reader reader = null;
try {
reader = Resources.getResourceAsReader("mybatis-config.xml");
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
factory = new SqlSessionFactoryBuilder().build(reader);
}
public static SqlSessionFactory getSqlSessionFactory()
{
return factory;
}
}
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE configuration
PUBLIC '-//mybatis.org//DTD Config 3.0//EN'
'http://mybatis.org/dtd/mybatis-3-config.dtd'>
<configuration>
<properties resource='jdbc.properties'/>
<typeAliases>
<typeAlias type='com.sivalabs.mybatisdemo.domain.User' alias='user'></typeAlias>
</typeAliases>
<environments default='development'>
<environment id='development'>
<transactionManager type='JDBC'/>
<dataSource type='POOLED'>
<property name='driver' value='${jdbc.driverClassName}'/>
<property name='url' value='${jdbc.url}'/>
<property name='username' value='${jdbc.username}'/>
<property name='password' value='${jdbc.password}'/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource='com/sivalabs/mybatisdemo/mappers/UserMapper.xml'/>
</mappers>
</configuration>
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE mapper PUBLIC '-//mybatis.org//DTD Mapper 3.0//EN'
'http://mybatis.org/dtd/mybatis-3-mapper.dtd'>
<mapper namespace='com.sivalabs.mybatisdemo.mappers.UserMapper'>
<select id='getUserById' parameterType='int' resultType='com.sivalabs.mybatisdemo.domain.User'>
SELECT
user_id as userId,
email_id as emailId ,
password,
first_name as firstName,
last_name as lastName
FROM USER
WHERE USER_ID = #{userId}
</select>
<!-- Instead of referencing Fully Qualified Class Names we can register Aliases in mybatis-config.xml and use Alias names. -->
<resultMap type='com.sivalabs.mybatisdemo.domain.User' id='UserResult'>
<id property='userId' column='user_id'/>
<result property='emailId' column='email_id'/>
<result property='password' column='password'/>
<result property='firstName' column='first_name'/>
<result property='lastName' column='last_name'/>
</resultMap>
<select id='getAllUsers' resultMap='UserResult'>
SELECT * FROM USER
</select>
userMapper.getUserById(userId)
之前,会先执行1.2.1
和1.2.2
代码1.2.3
是userMapper.getUserById(userId)
真正执行的代码 Reader reader = null;
try {
reader = Resources.getResourceAsReader("mybatis-config.xml");
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
factory = new SqlSessionFactoryBuilder().build(reader);
该段代码主要把mybatis-config.xml
和UserMapper.xml
中的各种配置解析到Configuration
实例中去。
SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
openSession()
来进行实例的初始化此处可暂时略过。
getMapper()
方法来获取Mapper实例此处getMapper()
方法的内部实现是为UserMapper
生成一个动态代理,并且返回:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
此处的意思为:
用MapperProxy
这个类的invoke()
方法来实现对应的函数调用流程。
return userMapper.getUserById(userId);
因为这里的userMapper
对象是动态代理对象,所以其实这里执行的是MapperProxy.invoke()
方法。
MapperProxy.invoke()
方法内容如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
这里生成了一个MapperMethod
对象,并且调用mapperMethod.execute(sqlSession, args)
来进行真正的DB查询逻辑。
mapperMethod.execute(sqlSession, args)
的内部逻辑如下所示
DefaultSqlSession#selectList()
方法内容如下:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
根据接口找到对应的sql,然后交由executor执行
缺省情况下executor是SimpleExecutor
,SimpleExecutor.doQuery()
方法内容如下:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
演示 MyBatis Plugin 功能的 Demo。
源码下载:mybatis-demo
对应DB表:mybatis-demo
添加Interceptor类:
@Intercepts({@Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class})})
public class SQLStatsInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
System.out.println("mybatis intercept sql:" + sql);
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
String dialect = properties.getProperty("dialect");
System.out.println("mybatis intercept dialect:" + dialect);
}
}
添加interceptor配置:
mybatis-config.xml 添加以下配置,
<plugins>
<plugin interceptor="com.sivalabs.mybatisdemo.interceptor.SQLStatsInterceptor">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
同上。2.1 解析配置文件
SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
openSession()
来进行实例的初始化。openSession()
的内部内部实现如下:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
主要业务逻辑为
newExecutor
configuration.newExecutor()
的内部实现如下:
executor = (Executor) interceptorChain.pluginAll(executor);
把所有的插件(Interceptor)嵌入到Executor
中,具体实现如下:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
细节已经展现,MyBatis 内部通过 Java 的动态代理来实现 Plugin(Interceptor)的嵌入。
getMapper()
方法来获取Mapper实例。同上 1.2.2.2
同上 1.2.3
同上 1.2.3.1
缺省情况下executor是SimpleExecutor
,SimpleExecutor.doQuery()
方法内容如下:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
其中1.
调用了configuration.newStatementHandler()
来构造一个 statement,其内部实现如下:
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
我们看到了interceptorChain.pluginAll(statementHandler)
,这里的逻辑便和2.2.2.1
一致了,用 Java 的动态代理,把 Interceptor
的代码嵌入到Handler
实例中。
这样一来,如 SQLStatsInterceptor
的注解中声明的那样,在调用 StatementHandler.parameterize
时,便会回调 Interceptor
的代码实现了。
MyBatis 配合Spring使用时,MyBatis类库会把Mapper对象注册在Spring容器中,这样用户使用Mapper对象就十分方便了。
源码下载:mybatis-spring-demo
对应DB表:mybatis-demo
UserMapper.java
. UserMapper.xml
和1.
. 2.
中的逻辑一样,主要的区别如下:
dataSource
. sqlSessionFactory
)由原本的mybatis-config.xml
中移动到Spring配置文件中@AutoWired
用Spring注入。Spring配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--获取数据库配置文件-->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="filters" value="stat"/>
<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
</bean>
<!--sqlsessionFactory bean-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:SqlMapConfig.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>
<!--自动扫描mapper接口,并注入sqlsession-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.mybatis.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>
代码测试用例如下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-dao.xml")
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testGetUserById() {
User user = userMapper.getUserById(1);
Assert.assertNotNull(user);
System.out.println(user);
}
}
这部分逻辑主要是通过以下Spring拓展点实现的:
<!--自动扫描mapper接口,并注入sqlsession-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.mybatis.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
在Spring中添加以上Beam即可注册Mapper对象到Spring容器。
因为MapperScannerConfigurer
实现了Spring的BeanDefinitionRegistryPostProcessor
这个拓展点,该拓展点作用如下:
Spring允许在Bean创建之前,读取Bean的元属性,并根据自己的需求对元属性进行改变 1
Spring会回调BeanDefinitionRegistryPostProcessor
这个接口的postProcessBeanDefinitionRegistry
方法,看下该方法内容的内容定义:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
这里的逻辑是,扫描basePackage
包,找出所有符合要求的Bean注册到Spring容器中。
更具体的分为以下两步:
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan内容如下:
* Perform a scan within the specified base packages,
* returning the registered bean definitions.
* <p>This method does <i>not</i> register an annotation config processor
* but rather leaves this up to the caller.
* @param basePackages the packages to check for annotated classes
* @return set of beans registered if any for tooling registration purposes (never {@code null})
*/
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
这个类的主要逻辑是找出符合要求的bean,包装成一个BeanDefinitionHolder
,然后丢进Spring容器中。
org.mybatis.spring.mapper.ClassPathMapperScanner#doScan
/**
* Calls the parent search that will search and register all the candidates.
* Then the registered objects are post processed to set them as
* MapperFactoryBeans
*/
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
.....
definition.setBeanClass(this.mapperFactoryBean.getClass());
.....
}
.....
}
这里的主要逻辑是把每个BeanDefinitionHolder
的BeanClass
都设置成了MapperFactoryBean
,而MapperFactoryBean
的getObject
逻辑如下:
org.mybatis.spring.mapper.MapperFactoryBean#getObject
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
getMapper的底层实现为为Mapper对象生成一个动态代理对象:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
这里和1.2.2.2
便一样了。
此部分内容和 1.2.3
的逻辑一致。
MyBatis-Plus 相比于直接使用 MyBatis 更简化了一步,连XML配置文件都不用写了,直接就可以使用内置的 Mapper 实现CRUD。
官方文档关于MyBatis-Plus 的介绍是:
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发. 提高效率而生。
MyBatis-Plus是如何做到这点的呢?
奥秘就在于,MyBatis-Plus增强了MyBatis,在MyBatis进行初始化的时候,直接使用反射生成了对应的XML丢尽了mybatis 容器中,从而实现了mybatis的增强!
接下来从代码的层面进行详细介绍。
@SpringBootApplication
@MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(QuickStartApplication.class, args);
}
}
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
public interface UserMapper extends BaseMapper<User> {
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleTest {
@Autowired
private UserMapper userMapper;
@Test
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List<User> userList = userMapper.selectList(null);
Assert.assertEquals(5, userList.size());
userList.forEach(System.out::println);
}
}
MyBatis 的 configration中保存了 mapper方法 和 对应sql的映射关系,只要在 xml解析的时候,动态生成 mapper方法对应的 sql并且丢进 configuration中,就可以实现MyBatis的增强!
mybatis-plus-boot-start
的spring.factories
的配置如下org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
@Override
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
// TODO 这里就不抛异常了
// throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
MybatisMapperAnnotationBuilder#parse()
调用 ISqlInjector#inspectInject
进行 statement注入 public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
String mapperName = type.getName();
assistant.setCurrentNamespace(mapperName);
parseCache();
parseCacheRef();
InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
parseStatement(method);
// TODO 加入 SqlParser 注解过滤缓存
InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
SqlParserHelper.initSqlParserInfoCache(mapperName, method);
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
}
parsePendingMethods();
}
AbstractSqlInjector#inspectInject
调用 AbstractMethod#inject
进行注入mybatis通过巧妙的方式,增强了mybatis,而没有改变mybatis的原先的执行流程,非常的巧妙。一方面是mybatis-plus作者设计非常巧妙,另一方便mybatis提前预留的拓展点也为后续的增强提供了极大的便利。