所谓定位就是回答几个问题,我出于什么目的要写一个框架,个人这个框架是干什么的,有什么特性适用于什么场景,个人这个框架的用户对象是谁,他们会怎么使用,框架由谁维护未来怎么发展等等前端
咱们来为本文模拟一个场景,假设咱们以为现有的Spring MVC等框架开发起来效率有点低,打算重复造轮子,对于新框架的定位是一个给Java程序员使用的轻量级的、零配置的、易用的、易扩展的Web MVC框架。程序员
调研正则表达式
虽然到这里你已经决定去写一个框架了,可是在着手写以前仍是至少建议评估一下市面上的相似(成熟)框架。须要作的是通读这些框架的文档以及阅读一些源码,这么作有几个目的:数据库
新 开发一个框架的好处是没有兼容历史版本的包袱,可是责任也一样重大,由于若是对于一开始的定位或设计工做没有作好的话,未来若是要对格局进行改变就会有巨 大的向前兼容的包袱(除非你的框架没有在任何正式项目中使用),兼容意味着框架可能会愈来愈重,可能会愈来愈难看,阅读至少一到两个开源实现,作好充分的 调研工做能够使你避免犯大错。编程
假设咱们评估了一些主流框架后已经很明确,咱们的MVC框架是一个Java平台的、基于Servlet的轻量级的Web MVC框架,主要的理念是约定优于配置,高内聚大于低耦合,提供主流Web MVC框架的大部分功能,而且易用方面有所创新,新特性体包括:json
提供一套通用的控件模版,使得,而且支持多种模版引擎,好比Jsp、Velocity、Freemarker、Mustache等等。后端
嗯,看上去挺诱人的,这是一个不错的开端,若是你要写的框架本身都不以为想用的话,那么别人就更不会有兴趣来尝试使用你的框架了。设计模式
解决难点缓存
之 因此把解决难点放在开搞以前是由于,若是实现这个框架的某些特性,甚至说实现这个框架的主流程有一些核心问题难以解决,那么就要考虑对框架的特性进行调 整,甚至取消框架的开发计划了。有的时候咱们在用A平台的时候发现一个很好用的框架,但愿把这个框架移植到B平台,这个想法是好的,但之因此在这之前这么 多年没有人这么干过是由于这个平台的限制压根不可能实现这样的东西。好比咱们要实现一个MVC框架,势必须要依赖平台提供的反射特性,若是你的语言平台压 根就没有运行时反射这个功能,那么这就是一个很是难以解决的难点。又好比咱们在某个平台实现一个相似于.NET平台Linq2Sql的数据访问框架,但如 果这个目标平台的开发语言并不像C#那样提供了类型推断、匿名类型、Lambda表达式、扩展方法的话那么因为语法的限制你写出来的框架在使用的时候是无 法像.NET平台Linq2Sql那样优雅的,这就违背了实现框架的主要目的,实现新的框架也就变得意义不大了。安全
对于咱们要实现的MVC框 架貌似不存在什么根本性的没法解决的问题,毕竟在Java平台已经有不少能够参考的例子了。若是框架的实现整体上没什么问题的话,就须要逐一评估框架的这 些新特性是否能够解决。建议对于每个难点特性作一个原型项目来证实可行,以避免在框架实现到一半的时候发现有没法解决的问题就比较尴尬了。
分析一下,貌似咱们要实现的这8大特性只有第1点要研究一下,看看如何免配置经过让代码方式让咱们的Web MVC框架能够和Servlet进行整合,若是没法实现的话,咱们可能就须要把第1点特性从零配置改成一分钟快速配置了。
开搞
首先须要给本身框架取一个名字,取名要考虑到易读、易写、易记,也须要尽可能避免和市面上其它产品的名字重复,还有就是最好不要起一个侮辱其它同类框架的名字以避免引发公愤。
若是未来打算把项目搞大的话,能够提早注册一下项目的相关域名,毕竟如今域名也便宜,避免到时候项目名和域名差距很大,或项目的.com或.org域名对应了一个什么不太和谐的网站这就尴尬了。
而后就是找一个地方来托管本身的代码,若是一开始不但愿公开代码的话,最好除了本地源代码仓库还有一个异地的仓库以避免磁盘损坏致使抱憾终身,固然若是不怕出丑的话也能够在起步的时候就使用Github等网站来托管本身的代码。
整体设计
对 于整体设计个人建议是一开始不必定须要写什么设计文档画什么类图,由于可能一开始的时候没法造成这么具体的概念,咱们能够直接从代码开始作第一步。框架的 使用者通常而言仍是开发人员,抛开框架的内在的实现不说,框架的API设计的好坏取决于两个方面。对于普通开发人员而言就是使用层面的API是否易于使 用,拿咱们的MVC框架举例来讲:
最基本的,搭建一个HelloWorld项目,声明一个Controller和Action,配置一个路由规则让Get方法的请求能够解析到这个Action,能够输出HelloWorld文字,怎么实现?
若是要实现从Cookie以及表单中获取相关数据绑定到Action的参数里面,怎么实现?
若是要配置一个Action在调用前须要判断权限,在调用后须要记录日志,怎么实现?
咱们这里说的API,它不必定全都是方法调用的API,广义上来讲咱们认为框架提供的接入层的使用均可以认为是API,因此上面的一些功能均可以认为是MVC框架的API。
框架除了提供基本的功能,还要提供必定程度的扩展功能,使得一些复杂的项目可以在某些方面对框架进行加强以适应各类需求,好比:
一 般而言若是要实现这样的功能就须要本身实现框架公开的一些类或接口,而后把本身的实现"注册"到框架中,让框架能够在某个时候去使用这些新的实现。这就需 要框架的设计者来考虑应该以怎么样的友好形式公开出去哪些内容,使得之后的扩展实如今自由度以及最少实现上的平衡,同时要兼顾外来的实现不破坏框架已有的 结构。
要想清楚这些不是一件容易的事情,因此在框架的设计阶段彻底能够使用从上到下的方式进行设计。也就是不去考虑框架怎么实现,而是以一 个使用者的身份来写一个框架的示例网站,API怎么简单怎么舒服就怎么设计,只从使用者的角度来考虑问题。对于相关用到的类,直接写一个空的类(能用接口 的尽可能用接口,你的目的只是经过编译而不是能运行起来),让程序能够经过编译就能够了。你能够从框架的普通使用开始写这样一个示例网站,而后再写各类扩展 应用,在此期间你可能会用到框架内部的20个类,这些类就是框架的接入类,在你的示例网站经过编译的那刹那,其实你已经实现了框架的接入层的设计。
这里值得一说的是API的设计蕴含了很是多的学问以及经验,要在目标平台设计一套合理易用的API首先须要对目标平台足够了解,每个平台都有一些约定俗成的规范,若是设计的API能符合这些规范那么开发人员会更容易接受这个框架,此外还有一些建议:
下一步工做就是把项目中那些空的类按照功能进行划分。目的很简单,就是让你的框架 的100个类或接口可以按照功能进行拆分和归类,这样别人一打开你的框架就能够立刻知道你的框架分为哪几个主要部分,而不是在100个类中晕眩;还有由于 一旦在你的框架有使用者后你再要为API相关的那些类调整包就比困难了,即便你在建立框架的时候以为个人框架就那么十几个类无需进行过多的分类,可是在将 来框架变大又发现当初设计的不合理,没法进行结构调整就会变得很痛苦。所以这个工做仍是至关重要的,对于大多数框架来讲,能够有几种切蛋糕的方式:
若是是一个RPC框架,大概是这样的结构:
对于咱们的Web MVC框架,举例以下:
这里咱们以IXXX来描述一个抽象,能够是接口也能够是抽象类,在具体实现的时候根据需求再来肯定。
这 种结构的划分方式彻底吻合上面说的切蛋糕方式,能够看到除了横切部分和分层部分,做为一个Web MVC框架,它核心的组件就是routing、model、view、controller、action(固然,对于有些MVC框架它没有route部 分,route部分是交由Web框架实现的)。
若是咱们在这个时候还没法肯定框架的模块划分的话,问题也不大,咱们能够在后续的搭建龙骨的步骤中随着更多的类的创建,继续理清和肯定模块的划分。
通过了设计的步骤,咱们应该内心对下面的问题有一个初步的规划了:
搭建龙骨
在 通过了初步的设计以后,咱们能够考虑为框架搭建一套龙骨,一套抽象的层次关系。也就是用抽象类、接口或空的类实现框架,能够经过编译,让框架撑起来,就像 造房子搭建房子的钢筋混凝土结构(添砖加瓦是后面的事情,咱们先要有一个结构)。对于开发应用程序来讲,其实没有什么撑起来一说,由于应用程序中不少模块 都是并行的,它可能并无一个主结构,主流程,而对于框架来讲,它每每是一个高度面向对象的,高度抽象的一套程序,搭建龙骨也就是搭建一套抽象层。这么说 可能有点抽象,咱们仍是来想一下若是要作一个Web MVC框架,须要怎么为上面说的几个核心模块进行抽象(咱们也来体会一下框架中一些类的命名,这里咱们为了更清晰,为全部接口都命名为IXXX,这点不太 符合Java的命名规范):
接下去就再也不详细阐述model、plugin等模块的内容了。
看到这里,咱们来总结一下,咱们的MVC框架在组织结构上有着高度的统一:
同 时咱们框架的相关类的命名也是很是统一的,能够一眼看出这是实现、仍是抽象类仍是接口;是提供程序,是执行结果仍是上下文。固然,在未来的代码实现过程当中 极可能会把不少接口变为抽象类提供一些默认的实现,这并不会影响项目的主结构。咱们会在模式篇对框架经常使用的一些高层设计模式作更多的介绍。
到了这里,咱们的项目里已经有几十个空的(抽象)类、接口了,其中也定义了各类方法能够把各个模块串起来(各类find()方法和execute()方法),能够说整个项目的龙骨已经创建起来了,这种感受很好,由于咱们内心颇有底,咱们只须要在接下去的工做中作两个事情:
走通主线流程
所谓走通主线流程,就是让这个框架能够以一个HelloWorld形式跑起来,这就须要把几个核心类的核心方法使用最简单的方式进行实现,仍是拿咱们的MVC框架来举例子:
在这一步,咱们并不必定要去触碰filter和model这部分的内容,咱们的主线流程只是解析路由,得到控制器,执行方法,找到视图而后渲染视图。过滤器和视图模型的绑定属于加强型的功能,属于支线流程,不属于主线流程。
虽 然在这里咱们说了一些MVC的实现,但本文的目的不在于教你实现一个MVC框架,因此不用深究每个类的实现细节,这里想说的是,在前面的龙骨搭建完后, 你会发现按照这个龙骨为它加一点肉上去实现主要的流程是瓜熟蒂落的事情,毫无痛苦。在整个实现的过程当中,你能够不断完善common下的一些 context,把方法的调用参数封装到上下文对象中去,不但看起来清楚且符合开闭原则。到这里,咱们应该能够跑起来在设计阶段作的那个示例网站的 HelloWorld功能了。
在这里还想说一点,有些人在实现框架的时候并无搭建龙骨的一步骤,直接以非OOP的方式实现了主线流程,这种方式有如下几个缺点:
不容易作到SRP单一指责原则,你很容易把各类逻辑都集中写在一块儿,好比大量的逻辑直接写到了DispatcherServlet中,辅助一些Service或Helper,整个框架就肥瘦不匀,有些类特别庞大有些类特别小。
不容易作到OCP开闭原则,扩展起来不方便须要修改老的代码,咱们指望的扩展是实现新的类而后让框架感知,而不是直接修改框架的某些代码来加强功能。
很难实现DIP依赖倒置原则,即便你依赖的确实是IService但其实就没意义,由于它只有一个实现,只是把他看成帮助类来用罢了。
实现各类支线流程
咱们想一下,对于这个MVC框架有哪些没有实现的支线流程?其实无需多思考,由于咱们在搭建龙骨阶段的设计已经给了咱们明确的方向了,咱们只须要把除了主线以外的那些龙骨上也填充一些实体便可,好比:
实现了这一步后,你会发现整个框架饱满起来了,每个包中再也不是仅有的那些接口和默认实现,并且会有一种OOP的爽快感,爽快感来源于几个方面:
咱们再来总结一下以前说的那些内容,实现一个框架的第一大步就是:
经 过这样的一些步骤后能够发现这个框架是很稳固的,很平衡的,很易于扩展的。其实到这里不少人以为框架已经完成了,有血有肉,其实我的以为只能说开发工做实 现了差很少30%,后文会继续说,毕竟直接把这样一个血肉之躯拿出去对外有点吓人,咱们须要为它进行不少包装和完善。
单元测试
在这以前咱们写的框架只能说是一个在最基本的状况下能够使用的框架,做为一个框架咱们没法预测开发人员未来会怎么使用它,因此咱们须要作大量的工做来确保框架不但各类功能都是正确的,并且仍是健壮的。写应用系统的代码,大多数项目是不会去写单元测试的,缘由不少:
对于框架,偏偏相反,没有配套的单元测试的框架(也就是仅仅使用人工的方式进行测试,好比在main中调用一些方法观察日志或输出,或者运行一下示例项目查看各类功能是否正常,是很是可怕的)缘由以下:
若是框架的时间需求不是特别紧的话,单元测试的引入能够是走通主线流程的阶段就引入,越早引入框架的成熟度可能就会越高,之后重构返工的机会会越小,框架的可靠性也确定会大幅提升。以前我有写过一个类库项目,并无写单元测试,在项目中使用了这个类库一段时间也没有出现任何问题,后来花了一点时间为类库写了单元测试,出乎我意料以外的是,个人类库提供的全部API中有超过一半是没法经过单元测试的(原觉得这是一个成熟的类库,其实包含了数十个BUG),甚至其中有一个API是在个人项目中使用的。你可能会问,为何在使用这个API的时候没有发生问题而在单元测试的时候发生问题了呢?缘由以前提到过,我是框架的设计者,我在使用类库提供的API的时候是知道使用的最佳实践的,所以我在使用的时候为类库进行了一个特别的设置,这个问题若是不是经过单元测试暴露的话,那么其它人在使用这个类库的时候基本都会遇到一个潜在的BUG。
示范项目
写一个示例项目不只仅是为了给别人参考,并且还可以帮助本身去完善框架,对于示例项目,最好兼顾下面几点:
完善日志和异常
一个好的框架不但须要设计精良,日志和异常的处理是否到位也是很是重要的标准,这里有一些反例:
其实我的以为,一个框架的主逻辑代码并不必定是最难的,最难的是对一些细节的处理,让框架保持一套规范的统一的日志和异常的使用反而对框架开发者来讲是一个难点,下面是针对记录日志的一些建议:
一、首先要对框架使用的日志级别有一个规范,好比定义:
二、按照上面的级别规范,在须要记录日志的地方记录日志,除了DEBUG级别的日志其它日志不能记录过多,若是框架老是在运行的时候输出几十个WARNNING也容易让使用者忽略真正的问题。
三、日志记录的消息须要是明确的,最好包含一些上下文信息,好比"没法在xxx下找到配置文件xxx.config,框架将采用默认的配置",而不是"加载配置失败!"
下面是一些针对使用异常的建议:
完善配置
配置的部分能够留到框架写的差很少了再去写,由于这个时候已经能够想清楚哪些配置是:
通常来讲配置有几种方式:
不少框架提供了多种配置方式,好比Spring MVC同时支持上面三种方式的配置,我的以为对配置,咱们仍是应该区别对待,而不是无脑把全部的配置项都同时以上面三种方式提供配置,咱们要考虑高内聚和低耦合原则,对于Web框架来讲,高内聚须要考虑的比低耦合更多,个人建议是对不一样的配置项提供不一样的配置方式:
提供状态服务
所谓状态服务就是反映框架内部运做状态的服务,不少开源服务或系统(Nginx、Mongodb等)都提供了相似的模块和功能,做为框架的话我以为也有必要提供一些内部信息(主要是配置、数据统计以及内部资源状态)出来,这样使用你框架的人能够在开发的时候或线上运做的时候了解框架的运做状态,咱们举两个例子,对于一个咱们以前提到的Web MVC框架来讲,能够提供这些信息:
对于一个Socket框架来讲,有一些不一样,Socket框架是有状态的,其状态服务提供的信息除了当前生效的配置信息以外,更多的是反映当前框架内部一些资源的状态以及统计数据:
状态服务能够如下面几种形式来提供:
检查线程安全
框架对多线程环境支持的是否好,是框架质量的一个重要的评估标准,每每能够看到甚至有一些成熟的框架也会有多线程问题。这里涉及几个方面:
1,你没法预料框架的使用者会怎么样去实例化和保存你的API的入口类,若是你的入口类被用成为了一个单例,在并发调用的状况下会不会有单线程问题?
这是一个老话题,以前已经说过不少次,你在设计框架的时候内心若是把一个类定位成了单例的类但却没有提供单例模式,你是没法要求使用者来帮你实现单例的。这其中涉及的不只仅是多线程问题,可能还有性能问题。好比见过某分布式缓存的客户端的CacheClient在文档中要求使用者针对一个缓存集群保持一个CacheClient的单例(由于其中有了链接池),可是用的人仍是每一次都实例化了一个CacheClient出来,几小时后就会产生几万个半死的Socket致使网络奔溃。又见过某类库的入口工厂的代码注释中写了要求使用的人把XXXFactory做为单例来使用(由于其中缓存了大量数据),可是用的人就没有注意到这个注释,每一次都实例化了一个XXXFactory,形成GC的崩溃。因此我以为做为框架的设计者开发人员,最好仍是把框架的最佳实践直接作到API中,使得使用者不可能出错(以前说过一句话,再重复一次,好的框架不会让使用的人犯错)。你可能会说对于CacheClient的例子,不可能作成单例的,由于个人程序可能须要用到多个缓存的集群,换个思路,咱们彻底能够在封装一层,经过一个CacheClientCreator之类的类来管理多个单例的CacheClient。即便在某些极端的状况下,你不能只提供一条路给使用者去走,也须要在框架内作一些检测机制,及时提醒使用者 "咱们发现您这样使用了框架,这可能会产生问题,你本意是否打算那样作呢?"
2,若是你的入口类原本就是单例的,那么你是类中是否持有共享资源,你的API在并发的状况下被调用是否能够确保这些资源的线程安全?在解决多线程问题的时候每每有几个难点:
百密难有一疏,你很难想到这段代码会有人这样去并发调用。好比某init()方法,某config()方法,你老是假设使用者会调用而且仅调用一次,但事实不必定这样,有的时候调用者本身也不清楚个人容器会调用我这段代码多少次。
好吧,解决多线程问题各类烦躁,那就对各类涉及到共享资源的方法所有加锁。对方法进行粗犷(粒度)的锁可能会致使性能急剧降低甚至是死锁问题。
自觉得使用了优雅的无锁代码或并发容器但却达不到目的。咱们每每在大量使用了并发集合心中暗自窃喜解决了多线程问题的同时又达到了极佳的性能,但你觉得这样是解决了线程安全问题但其实根本就没有,咱们不能假设A和B都方法是线程安全的,但对A和B方法调用的整个代码段是线程安全的。
对于多线程问题,我没有好的解决办法,不过下面的几条我以为能够尝试:
须要很是仔细的过一遍代码,把涉及到共享资源的地方,以及相关的方法和类列出来,不要去假设什么,只要API暴露出去了则假设它可能被并发调用。共享资源不必定是静态资源,哪怕资源是非静态的,在并发环境下对相同对象的资源进行操做也可能产生问题。
通常而言对于公开的API,做为框架的设计者咱们须要确保全部的静态方法(或但单例类的实例方法)是线程安全的,对于实例方法咱们能够不这么作(由于性能缘由),可是须要在注释中明确提示使用者方法的非线程安全,若是须要并发调用请自行处理线程安全问题。
能够看看是否有可能让这些资源(字段)变为方法内的局部变量,有的时候咱们并非真正的须要类持有一个字段,只是由于多个方法要使用相同的东西,随手一写罢了。
对于使用频率低的一些方法相关的一些资源没有必要使用并发容器,直接采用粗狂的方式进行资源加锁甚至是方法级别加锁,先确保没有线程安全,若是之后作压测出现性能问题再来解决。
对于使用频率高的一些方法相关的一些资源能够使用并发容器,但须要仔细思考一下代码是否会存在线程安全问题,必要的话为代码设计一些多线程环境的单元测试去验证。
性能测试和优化
以前也提到过,你不会预测到你的项目会在怎么样的访问量下使用,咱们不但愿框架和同类的框架相比有明显的性能差距(若是你作的是一个ORM框架或RPC框架,这个工做就是必不可少的),因此在框架基本完成后咱们须要作Benchmark:
封装和扩展
我的以为一个框架若是只是能用那是第一个层次,能很方便的进行扩展或二次开发那是另一个层次,若是咱们龙骨阶段的工做作的足够好,框架是一个立体饱满的框架,那么这部分的工做量就会小不少,不然咱们须要对框架进行很多的重构以即可以达到这个层次。
重构仍是重构
光是重构这个事情其实就能够说一本书了,其实我有一点代码的洁癖,这里列一些我本身写代码的时候注重的地方:
除了上面说的一些问题,我以为对于重构,最重要的一句话就是:不要让同一段代码出现两遍,主要围绕这个原则进行重构每每就会解决不少设计问题,要实现这个目标可能须要:
其实也不必定是在重构的时候再去处理上面全部的问题,若是在写代码的时候都带着这些意识来写的话那么重构的负担就会小一点(不过写代码思想的负担比较大,须要同时考虑封装问题、优雅问题、日志异常问题、多线程问题等等,因此写一套能用的代码和写一套好的代码其实不是一回事情)。
项目文档
若是要别人来使用你的框架,除了示例项目来讲提供和维护一份项目文档是颇有必要的,我建议文档分为这几个部分:
开源
开源的好处是有不少人能够看到你的代码帮助你改进,你的框架也可能会在更多的复杂环境下使用,框架的发展会较快框架的代码质量也会有很大的提高。
要把框架进行开源,除了上面的各类工做以外可能还有一些额外的工做须要作:
看到这里你可能相信我一开始的话了吧,框架能够使用到完善能够商用差距仍是很大的,并且还要确保在迭代的过程当中框架不能偏离开始的初衷不能有很大的性能问题出现,任重道远。