图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于 Web层,也可以直接依赖于 Service层,依此类推。
开放接口层:可直接封装 Service 方法暴露成RPC 接口;通过Web 封装成http 接口;网关控制层等。
终端显示层:各个端的模板渲染并执行显示的层。当前主要是velocity 渲染,JS 渲染,JSP 渲染,移 动端展示等。
Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
Service 层:相对具体的业务逻辑服务层。
Manager 层:通用业务处理层,它有如下特征:
1) 对第三方平台封装的层,预处理返回结果及转化异常信息。
2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
3) 与 DAO层交互,对多个DAO的组合复用。
• DAO层:数据访问层,与底层 MySQL、Oracle、Hbase、OB等进行数据交互。
• 外部接口或第三方平台:包括其它部门 RPC开放接口,基础平台,其它公司的 HTTP 接口。
个人理解:这个工程结构其实是非常重要的。很多公司对这个结构建的乱七八糟的,像某的这种Globle 500 的项目工程结构也没有统一的标准。而在京东这种互联网企业确是有明确规范的。在使用Dubbo与springcloud框架下的工程结构是不一样的。
开放接口层:如果是采用dubbo做为微服务框架,一般在垂直拆分应用中,一般是采用单独的一个module提供rpc接口。供内部服务依赖调用。而使用spring cloud,个人建议使用一个包就可以。
//TODO 这个地方以后再补充。
(分层异常处理规约)在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行 catch,使用 catch(Exception e)方式,并 throw new DAOException(e),不需要打印日志,因为日志在 Manager/Service层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志, 浪费性能和存储。在 Service层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息, 相当于保护案发现场。Manager层与 Service同机部署,日志方式与 DAO层处理一致,如果是 单独部署,则采用与 Service一致的处理方式。Web层绝不应该继续往上抛异常,因为已经处 于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面, 尽量加上友好的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。
个人理解:这个地方可以做为强制规范,因为日志的规范打印,对于线上问题的处理能起到很大作用。
这段文字大概是这个意思:
Web层可以通过切面需要统一做异常处理,不允许往外抛异常。
Dao层因为异常种类太多,直接向外抛异常即可。
Service层/Manager层:service层需要打印日志。manager层是否打印日志,与部署关系有关。在大厂中,rpc服务一般是单独部署的,这个时候service与manger层是分在两个不同的进程中,此时,manager需要捕获异常并打印日志,很多开发工程师为了偷懒,在rpc上做切面全部切出日志。如果manager与service是在同一个进程中,则向Dao一样抛出异常即可。
个人建议:其实这个命名是可以。但是很多时候使用BeanCopyUtils类来拷贝对象,在大对象拷贝时,很容易影响性能。所以我个人建议,如果是采用springcloud,如果对象不需要加工,就不需要这么多层对象转换。如果是需要转换,则这么命名。在dubbo框架下,则可以按照这个规范使用,但是能不对象转化,就不要对象转换。尤其是大对象。
所谓二方库是指公司内部的依赖jar包。三方库一般是指其他公司的jar包。
定义 GAV遵从以下规则:
1) GroupID 格式:com.{公司/BU }.业务线 [.子业务线],最多4 级。
说明:{公司/BU}
例如:alibaba/taobao/tmall/aliexpress 等BU 一级;子业务线可选。
正例:com.taobao.jstorm 或 com.alibaba.dubbo.register
2) ArtifactID 格式:产品线名-模块名。语义不重复不遗漏,先到中央仓库去查证一下。
正例:dubbo-client / fastjson-api / jstorm-tool
3) Version:详细规定参考下方
二方库版本号命名方式:主版本号.次版本号.修订号
1)主版本号:产品方向改变,或者大规模 API不兼容,或者架构不兼容升级。
2)次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API不兼容修改。
3) 修订号:保持完全兼容性,修复BUG、新增次要功能特性等。
说明:注意起始版本号必须为:1.0.0,而不是0.0.1。
反例:仓库内某二方库版本号从 1.0.0.0 开始,一直默默“升级”成 1.0.0.64,完全失去版本的语义信息。
线上应用不要依赖 SNAPSHOT版本(安全包除外);
正式发布的类库必须先去中央仓库进行查证,使RELEASE版本号有延续性,且版本号不允许覆盖升级。
说明:不依赖SNAPSHOT 版本是保证应用发布的幂等性。另外,也可以加快编译时的打包构建。
个人理解:尤其在多个并行分支的情况下,不要在生产依赖于SNAPSHOT版本。某的其实在这块的管理是没有太多关注的。
二方库的新增或升级,保持除功能点之外的其它jar包仲裁结果不变。如果有改变, 必须明确评估和验证。
说明:在升级时,进行 dependency:resolve 前后信息比对,如果仲裁结果完全不一致,那么通过 dependency:tree 命令,找出差异点,进行排除 jar 包。
二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO对象。
就是说不要在Java类中包含枚举类,而应该单独定义枚举类。并且在rpc调用过程中返回值不应该是枚举类型。
依赖于一个二方库群时,必须定义一个统一的版本变量,避免版本号不一致。 说明:依赖springframework-core,-context,-beans,它们都是同一个版本,可以定义一个变量来保存版 本:${spring.version},定义依赖的时候,引用该版本。
禁止在子项目的 pom依赖中出现相同的 GroupId,相同的 ArtifactId,但是不同的 Version。
说明:在本地调试时会使用各子项目指定的版本号,但是合并成一个 war,只能有一个版本号出现在最后的 lib 目录中。曾经出现过线下调试是正确的,发布到线上却出故障的先例。
底层基础技术框架、核心数据管理平台、或近硬件端系统谨慎引入第三方实现。
所有 pom文件中的依赖声明放在语句块中,所有版本仲裁放在 语句块中。
说明:里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖, version 和scope 都读取自父 pom。而所有声明在主 pom的里的依 赖都会自动引入,并默认被所有的子项目继承。
二方库不要有配置项,最低限度不要再增加配置项。
不要使用不稳定的工具包或者Utils类。
说明:不稳定指的是提供方无法做到向下兼容,在编译阶段正常,但在运行时产生异常,因此,尽量使用 业界稳定的二方工具包。
为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则:
1)精简可控原则。移除一切不必要的 API和依赖,只包含 Service API、必要的领域模型对象、Utils类、 常量、枚举等。如果依赖其它二方库,尽量是 provided 引入,让二方库使用者去依赖具体版本号;无log 具体实现,只依赖日志框架。
2)稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,都需要能方便查到。除 非用户主动升级版本,否则公共二方库的行为不应该发生变化。
高并发服务器建议调小 TCP协议的 time_wait超时时间。
说明:操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服务器端会因为 处于 time_wait 的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值。
正例:在linux服务器上请通过变更/etc/sysctl.conf 文件去修改该缺省值(秒):
net.ipv4.tcp_fin_timeout = 3
调大服务器所支持的最大文件句柄数(File Descriptor,简写为 fd)。 说明:主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd。 主流的linux服务器默认所支持最大fd数量为1024,当并发连接数很大时很容易因为fd不足而出现“open too many files”错误,导致新的连接无法建立。建议将 linux服务器所支持的最大句柄数调高数倍(与服 务器的内存数量相关)。
给JVM环境参数设置-XX:+HeapDumpOnOutOfMemoryError参数,让JVM碰到OOM 场景时输出dump信息。
说明:OOM 的发生是有概率的,甚至相隔数月才出现一例,出错时的堆内信息对解决问题非常有帮助。
个人理解:这点非常重要。之前一个线上OOM问题,并没有打印堆栈信息,对于定位问题很不利。
在线上生产环境,JVM的 Xms和 Xmx设置一样大小的内存容量,避免在 GC 后调整 堆大小带来的压力。
服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL Broker生成,否 则因线上采用HTTPS协议而导致浏览器提示“不安全“。此外,还会带来 URL维护不一致的 问题。
存储方案和底层数据结构的设计获得评审一致通过,并沉淀成为文档。
说明:有缺陷的底层数据结构容易导致系统风险上升,可扩展性下降,重构成本也会因历史数据迁移和系 统平滑过渡而陡然增加,所以,存储方案和数据结构需要认真地进行设计和评审,生产环境提交执行后, 需要进行 double check。
正例:评审内容包括存储介质选型、表结构设计能否满足技术方案、存取性能和存储空间能否满足业务发 展、表或字段之间的辩证关系、字段名称、字段类型、索引等;数据结构变更(如在原有表中新增字段) 也需要进行评审通过后上线。
在需求分析阶段,如果与系统交互的User超过一类并且相关的User Case超过5个, 使用用例图来表达更加清晰的结构化需求。
如果某个业务对象的状态超过3个,使用状态图来表达并且明确状态变化的各个触发 条件。
说明:状态图的核心是对象状态,首先明确对象有多少种状态,然后明确两两状态之间是否存在直接转换 关系,再明确触发状态转换的条件是什么。
正例:淘宝订单状态有已下单、待付款、已付款、待发货、已发货、已收货等。比如已下单与已收货这两 种状态之间是不可能有直接转换关系的。
如果系统中某个功能的调用链路上的涉及对象超过3个,使用时序图来表达并且明确各调用环节的输入与输出。
说明:时序图反映了一系列对象间的交互与协作关系,清晰立体地反映系统的调用纵深链路。
如果系统中模型类超过5个,并且存在复杂的依赖关系,使用类图来表达并且明确类之间的关系。 说明:类图像建筑领域的施工图,如果搭平房,可能不需要,但如果建造蚂蚁 Z空间大楼,肯定需要详细 的施工图。
个人理解:这块其实我自己很多时候都没有做到。很多时候只做了概要设计就开始开发了。对重要或核心的流程才用了思维导图做了。这块很多公司应该都没有做类图的设计吧。对于类图的编写,可以参考rocketMQ的文档。
如果系统中超过2个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活 动图来表示。 说明:活动图是流程图的扩展,增加了能够体现协作关系的对象泳道,支持表示并发等。
系统架构设计时明确以下目标:
⚫ 确定系统边界。确定系统在技术层面上的做与不做。
⚫ 确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。
⚫ 确定指导后续设计与演化的原则。使后续的子系统或模块设计在一个既定的框架内和技术方向上继续演化。
⚫ 确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等
很多时候后续的设计与演化是与需求相关的,技术人员或架构师必须知道以后产品的迭代方向,所以,要求需求的提供者能尽早对产品的发展趋势做规范。
需求分析与系统设计在考虑主干功能的同时,需要充分评估异常流程与业务边界。 反例:用户在淘宝付款过程中,银行扣款成功,发送给用户扣款成功短信,但是支付宝入款时由于断网演 练产生异常,淘宝订单页面依然显示未付款,导致用户投诉。
个人理解:设计时需要对重点主干流程做分析,这块是不能出现偏差的。对这块的设计过程中,需要重点考虑异常流程及业务边界。
异常流程包括:分布式事务退回方案。交易的幂等与状态,流程的合理及黑客攻击性。比如图形验证码的业务流程。该业务流程的边界和发展趋势。
类在设计与实现时要符合单一原则。
说明:单一原则最易理解却是最难实现的一条规则,随着系统演进,很多时候,忘记了类设计的初衷。
类设计的五个原则:
单一职责原则:要求类只负责一件事情。
它规定一个类应该只有一个发生变化的原因。将不同的职责封装到不同的类或模块中。
开闭原则:要求类不作修改而能够扩展功能,体现了类的封装与继承。
开闭原则指的是开放封闭原则,即对扩展开放,对修改封闭。
所谓修改封闭,就是之前设计好的类,不要去修改。比如删除掉一个成员函数、改变成员函数的形参列表或更改数据成员类型等。实现对修改封闭,关键在于抽象化。对一个事物抽象化,实质上是对一个事物进行概括、归纳、总结,将其本质特征抽象地用一个类来表示,这样类才会相对稳定,无需更改。
所谓扩展开放,就是在不改变已存在的类的前提下可以添加很多功能。一般是通过继承和多态来实现,如此一来,可以保持父类的原样,只需在子类中添加些所需的新功能。
接口隔离原则:让客户只关心他们所需的接口。单一职责原则与接口分离原都体现了内聚的思想;
使用多个小的专门的接口,而不要使用一个大的总接口。具体而言,接口应该是内聚的,应该避免“胖”接口。一个类对另一个类的依赖应该建立在最小的接口上,而不要强迫依赖不同的方法,这是一种接口污染。
在某司系统中,某个服务的所有方法都是通过该接口的invoke方法来实现的。这么个胖子,怎么一个了得。
里氏替换原则:要求派生类要能够替换基类,是对类继承的规范。
子类可以替换父类并出现在父类能够出现的任何地方。
依赖倒置原则:
其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都依赖于抽象;抽象不依赖于具体,具体依赖于抽象。依赖倒置原则是对传统过程性设计方法的“倒转”,是高层次模块复用及其可维护性的有效规范。
这个就是面向对象开发中的抽象。
谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现。
说明:不得已使用继承的话,必须符合里氏代换原则,此原则说父类能够出现的地方子类一定能够出现, 比如,“把钱交出来”,钱的子类美元、欧元、人民币等都可以出现。
系统设计阶段,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护。 说明:低层次模块依赖于高层次模块的抽象,方便系统间的解耦。
因为类的设计是详细设计的范畴,所以在详细设计中,可以定义抽象类和接口。
系统设计阶段,注意对扩展开放,对修改闭合。
说明:极端情况下,交付的代码是不可修改的,同一业务域内的需求变化,通过模块或类的扩展来实现。
应该尽量向这种极端情况看齐。
系统设计阶段,共性业务或公共行为抽取出来公共模块、公共配置、公共类、公共方法等,在系统中不出现重复代码的情况。
说明:随着代码的重复次数不断增加,维护成本指数级上升。
避免如下误解:敏捷开发 = 讲故事 + 编码 + 发布。
说明:敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摒弃传统的审批流程,但核心关键点上 的必要设计和文档沉淀是需要的。
反例:某团队为了业务快速发展,敏捷成了产品经理催进度的借口,系统中均是勉强能运行但像面条一样的代码,可维护性和可扩展性极差,一年之后,不得不进行大规模重构,得不偿失。
一个系统应该要考虑满足未来三年的业务,不然频繁进行重构,导致系统稳定性极差。
设计文档的作用是明确需求、理顺逻辑、后期维护,次要目的用于指导编码。
说明:避免为了设计而设计,系统设计文档有助于后期的系统维护和重构,所以设计结果需要进行分类归档保存。
个人建议,概要设计是必须的,如果只有概要设计,那建议由架构师编写主要的接口及评估核心流程的技术方案。如果有详细设计,则需要补充类图等。不要为了设计而设计。设计的重点是明确需求,理顺逻辑,后期维护。
可扩展性的本质是找到系统的变化点,并隔离变化点。
说明:世间众多设计模式其实就是一种设计模式即隔离变化点的模式。
正例:极致扩展性的标志,就是需求的新增,不会在原有代码交付物上进行任何形式的修改。
设计的本质就是识别和表达系统难点。
说明:识别和表达完全是两回事,很多人错误地认为识别到系统难点在哪里,表达只是自然而然的事情, 但是大家在设计评审中经常出现语焉不详,甚至是词不达意的情况。准确地表达系统难点需要具备如下能力: 表达规则和表达工具的熟练性。抽象思维和总结能力的局限性。基础知识体系的完备性。深入浅出的 生动表达力。
代码即文档的观点是错误的,清晰的代码只是文档的某个片断,而不是全部。
说明:代码的深度调用,模块层面上的依赖关系网,业务场景逻辑,非功能性需求等问题是需要相应的文 档来完整地呈现的。
尤其是复杂业务的深层调用,可以考虑用类图来呈现。
在做无障碍产品设计时,需要考虑到:
⚫ 所有可交互的控件元素必须能被 tab 键聚焦,并且焦点顺序需符合自然操作逻辑。
⚫ 用于登陆校验和请求拦截的验证码均需提供图形验证以外的其它方式。
⚫ 自定义的控件类型需明确交互方式。
正例:用户登陆场景中,输入框的按钮都需要考虑 tab键聚焦,符合自然逻辑的操作顺序如下,“输入用 户名,输入密码,输入验证码,点击登录”,其中验证码实现语音验证方式。如果有自定义标签实现的控 件设置控件类型可使用 role 属性。