总方针

构建易于理解和使用的RESTful接口。

接口应该是直观的,调用者可以通过接口来获得系统或应用程序中所有业务服务的工作节奏。

一般来说,可以使用以下的指导方针来进行接口的设计。

  1. 使用标准HTTP动词–围绕这些HTTP动词(GET/PUT/POST/PATCHDELETE)对基本的行为进行建模。
  2. 使用URI来传达意图–使用URI来描述问题域中的不同资源,并为问题域内的资源的关系提供一种基本机制。
  3. 使用JSON进行响应–JSON是一种轻量级的数据序列化协议。
  4. 使用HTTP状态码来传达结果–HTTP协议具有丰富的标准响应代码,来指示服务的成功和失败。学习这些状态码,并且,最重要的是,在所有接口中始终如一地使用它们。

所有这些指导方针都是为了完成一件事,那就是使接口易于理解和使用。 我们希望调用者坐下来查看一下接口就能开始使用它们。 如果接口不容易使用,开发人员就会另辟道路,破坏架构的意图。

资源与URI

REST全称是 Representational State Transfer (表述性状态转移)。其中表述指的就是资源。

URI既可看成是资源的地址,也可以看成是资源的名称。

URI的设计应该遵循可寻址性原则,具有自描述性,需要在形式上给人以直觉的关联。

URIHTTP动词作用的对象。它应该只有名词,不能包含动词。

URI的设计应该注意:

  1. URI中不能有动词: 动词应该由HTTP的动作(GET/POST/PUT/PATCH/DELETE等)来表示
  2. URI结尾不应该包含斜杠“/”
  3. 正斜杠分隔符“/”必须用来指示层级关系
  4. 应该使用连接符“-”来提高URI的可读性:浏览器默认会给超链接加上下划线,因此不要用其做URI分隔符
  5. URI路径首选小写字母:RFC-3986URI定义为区分大小写,但URI中的scheme(协议名)和host(主机名)除外
  6. URI路径中的名词建议使用复数
  7. 避免层级过深的URI(太多的层级在另一个侧面反应该接口有太多的职责)

资源操作

HTTP 通常有以下5种动词:

  • GET:获取资源(幂等)
  • POST:新建资源(非幂等)
  • PUT:更新资源(所有属性)(幂等)
  • PATCH:更新资源(部分属性)(非幂等)
  • DELETE:删除资源(幂等)

根据 HTTP 规范,动词一律大写。

资源过滤

很多情况,资源会有多级分类,因此很容易写出多级的URI,比如某个作者的某一类文章(/authors/123/categories/2)。

这种URI不易于扩展,语义也不明确,不能直观表达其含义。

更好的做法是,将次要的级别用查询字符串进行表达。如:

/authors/123?category=2
/articles?published=true

同样的,通过使用查询字符串,实现排序、投影和分页。

与之相反

经常使用的、复杂的查询可以标签化。 如:

/authors/123?status=close&sort=created,desc

可转化为:

/authors/123/closed
// 或者
/authors/123#closed

返回状态码

HTTP状态码为三位数,分五类:

  • 1** 相关信息
  • 2** 操作成功
  • 3** 重定向相关
  • 4** 客户端(导致的)错误
  • 5** 服务端(导致的)错误

HTTP包含了100多个状态码,覆盖了大多数可能遇到的情况。 每一种状态都有标准的(或约定的)解释,客户端只需查看状态,就可以大致判断发生了什么情况。 所以服务器应该尽可能使用这些标准的HTTP状态码,来表达执行结果状态。

通常不需要1**这一类状态码。 以下是常用的:

  • 200 OK : 成功返回请求数据(幂等)
  • 201 Created : 新建数据成功
  • 202 Accepted : 表示服务器已接收请求,但未处理。通常用于异步操作。
  • 204 No Content : 删除数据成功
  • 301 Moved Permanently : 资源已永久性迁移,需要使用新的(写在相应头Location中的)URI访问。允许客户端把POST请求修改为GET
  • 302 Found : 不推荐使用,此代码在HTTP1.1协议中已被303/307替代。目前对302的使用和最初的HTTP1.0定义的语义是有出入的,应该只有在GET/HEAD方法下,客户端才能根据Location执行自动跳转,而目前的客户端基本上是不会判断原请求方法,无条件的执行重定向。
  • 303 See Other : 参考另一个URI(区别:307用于GET303用于POSTPUTDELETE),但不强制要求重定向。
  • 304 Not Modified : 服务器资源与客户端最近访问的一致,不返回资源消息体。
  • 307 Temporary Redirect : 目前URI不能提供所请求的资源,临时重定向到另外一个URI。用来替代HTTP1.0中的302
  • 308 Permanent Redirect : 与301类似,但客户端不能修改原请求方法
  • 400 Bad Request : 服务器不理解客户端的请求,未做任何处理
  • 401 Unauthorized : 用户未提供身份验证凭据,或没通过验证
  • 403 Forbidden : 用户通过的验证,但不具有访问权限
  • 404 Not Found : 请求资源不存在或不可用。可以对某些用户未授权访问的资源操作返回该状态码,目的是防止私有资源泄露(知道有该资源)。
  • 405 Method Not Allowed : 用户已通过验证,但所用的HTTP方法不在权限内或资源只读等。响应Header中应申明支持的方法
  • 406 Not Acceptable : 表示拒绝处理该请求(如:服务端只能返回JSON,但客户端要求XML
  • 409 Conflict : 资源状态冲突,例如客户端尝试删除一个有约束的资源
  • 410 Gone : 请求资源已从这个地址转移,不再可用
  • 412 Precondition Failed : 用于有条件的操作不能被满足
  • 415 Unsupported Media Type : 请求格式不支持(如:服务端只能返回JSON,但客户端要求XML
  • 422 Unprocessable Entity : 请求无法处理,或发生了一个验证错误
  • 429 Too Many Requests : 请求次数超过限制
  • 500 Internal Server Error : 请求有效,服务器处理时发生内部错误
  • 503 Service Unavailable : 服务器无法处理请求,多半是服务器问题,如CPU高等

返回内容

返回内容数据格式应该是结构化的(如:一个JSON对象)。

客户端请求时也要明确告诉服务器,可以接受的格式。

  • GET /collections 200 返回资源列表
  • GET /collections/:id 200 返回单个资源
  • POST /colections 201 返回新增的资源对象
  • PUT /collections/:id 200 返回完整的资源对象
  • PATCH /collections/:id 200 返回完整的资源对象或被修改的属性
  • DELETE /collections/:id 204 返回空文档

错误处理

错误时不要返回200状态码。 因为只有解析数据体后,才能得知操作失败。而且与HTTP状态码描述冲突。

假如你不利用HTTP状态码丰富的应用语义,那么你就错失了提高重用性、增强互操作性和提升松耦合性的机会。

这些所谓的RESTful应用必须通过响应体才能给出错误信息,那么这个跟SOAP没什么区别。

正确的做法是,状态码反映发生的错误,而具体的错误信息放在数据体中。 如:

HTTP/1.1 400 Bab Request
Content-Type: application/json

{
	"error": "Invalid param."
	"data": {
		"name": "This field is required."
	}
}

另外建议要区分业务异常和非业务异常。 业务异常的返回4**的状态码,非业务异常的返回500状态码。

资源的表述

客户端通过HTTP方法获取的不是资源本身,而是资源的一种表述而已。 资源在外界的具体呈现,可以有多种表述形式,如:html、xml、json、png、jpg等。

资源的表述包括数据和描述数据的元数据,如:HTTP头中的Content-Type就是一个元数据属性。

所以应该通过HTTP的内容协商,来获取资源的表述。

如:客户端可以通过Accept头请求一种特定格式的表述,服务器则通过Content-Type告诉客户端资源的表述形式。

区分格式

应该优先使用内容协商来区分表述格式。

使用后缀表示格式,无疑是直观的,但它混淆了资源的名称和资源的表述形式。

超媒体(Hypermedia)

“超媒体即应用状态引擎(hypermedia as the engine of application state)”。

当浏览Web网页时,我们从一个链接跳到一个页面,再从页面里的另一个链接跳到另一个页面,这就是在用超媒体的概念:把一个个资源链接起来。

要达到这个目的,就要求在资源的表述里加上链接来引导客户端。

如 GitHub api 中的分页,是在头信息的Link提供:

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"

应该多花时间来给资源的表述提供链接,而不是专注于寻找漂亮的URI

速率限制

响应头建议包含当前限流状态

如 GitHub api 中使用3个相关的头信息进行说明:

  • X-RateLimit-Limit: 用户在时间窗口下发送请求的最大值
  • X-RateLimit-Remaining: 当前时间窗口剩下的可用请求数
  • X-RateLimit-Rest: 为了得到最大请求数(或到下一时间窗口)所等待的秒数

建议同时提供可以不影响RateLimit的请求接口,来查询当前的RateLimit

无状态

RESTful应该是无状态通信的。 服务端不应该保存客户端(应用)状态。

客户端与服务端交互必须是无状态的,并在每次请求中包含处理所需的一切信息。

这种无状态通信,使得服务端能够理解独立的请求和响应。 在多次请求中,同一客户端也不再需要依赖同一服务器,方便实现高可扩展和高可用性的服务端。

服务端通过超媒体告诉客户端当前(应用)状态可以有哪些后续的状态。 这些类似“下一页”的链接将指引客户端如何从当前状态进入下一个可能的状态。

版本

三种方式:

  1. URI中:/api/v1/**
  2. Accept Header: Accept: application/json+v1
  3. 自定义Header: X-Api-Version: 1

建议第一种,虽然没那么优雅,但最明显方便。

另一种观点:一个资源,应只有一个单一的URI来标示,资源版本不应该体现在URI中。

以上见仁见智,不强制要求。

  • API 失效:返回404 Not Found410 Gone
  • API 迁移:返回301/303307

其他(Header)头信息的使用

  • Last-Modified : 用于服务器端的响应,是一个资源最后被修改的时间戳,客户端(缓存)可以根据此信息判断是否需要重新获取该资源。
  • ETag : 服务器端资源版本的标识,客户端(缓存)可以根据此信息判断是否需要重新获取该资源,需要注意的是,ETage如果通过服务器随机生产,可能会存在多个主机对同一个资源产生不同的ETag的问题。
  • Location : 如在成功创建了一个资源后,可以把新资源的URL放在Location中;又如,在异步请求时,接口返回响应202(Accepted)的同时,可以给客户端一个查询异步状态的地址。
  • Cache-Control, Expires, Date : 通过缓存机制提升接口响应性能,同时根据实际需要也可以禁止客户端对接口请求做缓存。对应某些实时性要求不高的情况下,可以使用max-age来指定一个小的缓存时间,这样对客户端和服务端都有利。一般来说,只对GET方法且返回200的资源使用缓存,在某些情况下也可以对返回3**4**的情况做缓存,防范错误访问带来的负载。
  • 自定义头 : 不能改变HTTP方法的性质,尽量保持的简单,不要用body中的信息对其补充说明。

其他

1. 动词的覆盖

有些客户端仅支持GETPOST两种方法。那么,服务器必须可以接受通过POST模拟其他方法(PUTPACTHDELETE)。

客户端在发送HTTP请求时,要加上X-HTTP-Method-Override头信息,告诉服务器应该使用那个动词,覆盖POST方法。

2. 提供相关链接

服务接口的使用者未必知道接口有那些,以及它的相关服务。 好的接口,应该在相应中给出相关链接,以便于下一步操作。 这样,用户就可以发现其他接口的URI。 这种方法叫HATEOAS。 如 GitHub 的 API 都在api.github.com这个域名。

参考

Read More

简介

Apache Shiro是Java的一个安全框架。目前,使用Apache Shiro的人越来越多,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。

Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:

shiro基本功能

下图是从应用程序角度观察Shiro完成的工作:

shiro 调用链

可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是SubjectSecurityManager管理Subject并执行相关安全操作。 Realm则负责提供安全相关的数据。

下面会逐一介绍

Realm

Realm 是 Shiro 的一个重要抽象概念。 它向Shiro提供安全数据(如用户、凭证、角色、权限),来确认身份是否合法、是否能进行操作等。

Realm相关类的 UML 如下图:

Realm相关类UML

  • Realm 基接口,提供获取认证信息(用户、凭证)的方法
  • AuthenticatingRealm 封装了用户的认证过程,通过覆盖其虚方法doGetAuthenticationInfo可实现自己的认证信息获取;也可注入credentialsMatcher实现自定义的凭证校验,如HashedCredentialsMatcher等。
  • AuthorizingRealm 封装了角色和权限相关操作,通过覆盖其虚方法doGetAuthorizationInfo可获取自定义的授权信息。
  • JdbcRealm Shiro 提供的一个数据库相关的Realm实现
  • JndiLdapRealm Shiro 提供的与 LDAP 相关的Realm实现
  • ActiveDirectoryRealm Shiro 提供的与活动目录相关的Realm实现

  • AuthenticationInfo 认证信息(用户、凭证)
  • SaltedAuthenticationInfo 带盐的认证信息(用户、凭证)
  • MergableAuthenticationInfo 可以合并的认证信息(用户、凭证),用于处理多个Realm时,将AuthenticationInfo进行合并输出
  • SimpleAuthenticationInfo Shiro 提供的(实现了上面两个接口的)一个实现

  • CredentialsMatcher 凭证验证接口
  • SimpleCredentialsMatcher
  • HashedCredentialsMatcher 加密凭证验证器,可以通过注入hashAlgorithmName来设置加密类型(如:MD5)

认证

认证相关类 UML 如下图:

认证相关类UML

  • Authenticator 认证器,对用户凭证/token进行认证
  • AbstractAuthenticator 提供认证监听器,对认证结果进行监听
  • ModularRealmAuthenticator Shiro 提供的,真正用于认证的类,将认证过程委托给Realm,当有多个Realm时,使用了策略模式进行认证。
  • AuthenticatingSecurityManager 继承SecurityManager,创建ModularRealmAuthenticator以提供认证方法。

  • AuthenticationStrategy 认证策略模式的接口
  • AtLeastOneSuccessfulStrategy Shiro 提供的认证策略之一,默认使用
  • FirstSuccessfulStrategy Shiro 提供的认证策略之一
  • AllSuccessfulStrategy Shiro 提供的认证策略之一

授权

授权相关类 UML 如下图:

授权相关类UML

  • Authorizer 鉴权器,对用户-角色或用户-资源进行鉴权,即检查用户是否属于某个角色,或检查用户是否有某项资源的权限
  • ModularRealmAuthorizer Shiro 提供的,真正用于鉴权的类,其将鉴权过程委托给Realm
  • AuthorizingSecurityManager 继承SecurityManager,创建ModularRealmAuthorizer以提供鉴权方法。

  • AuthorizationInfo 角色、权限信息
  • SimpleAuthorizationInfo Shiro 提供的AuthorizationInfo的实现

  • Permission 权限接口,实现该接口的类可以相互比对是否一致,即鉴别权限是否一致。
  • WildcardPermission Shiro 提供的Permission的实现,可以对带通配符的权限比对。
  • PermissionResolver 将字符串转换为Permission对象的接口
  • WildcardPermissionResolver

Filter

Filter相关类 UML 如下图:

Filter相关类UML

  • Filter servlet 定义的接口
  • OncePerRequestFilter 每个请求只拦截一次
  • AdviceFilter 提供类似 AOP 功能
  • PathMatchingFilter 提供了基于Ant风格的请求路径匹配功能及拦截器参数解析的功能,如“roles[admin,user]”自动根据“,”分割解析到一个路径参数配置并绑定到相应的路径
  • AccessControlFilter 提供了访问控制的基础功能。isAccessAllowed:表示是否允许访问,onAccessDenied:表示当访问拒绝时是否已经处理了
  • AuthenticationFilter 需认证Filter,若用户未认证,会跳转至指定 URL
  • AuthenticatingFilter 认证Filter,对认证进行拦截
  • FormAuthenticationFilter Shiro 提供的表单认证Filter
  • UserFilter Shiro 提供的Filter,判断是否是登录的用户
  • AuthorizationFilter 权限Filter,若用户没有权限,会跳转至指定 URL 或返回401
  • PermissionsAuthorizationFilter Shiro 提供的权限Filter,对是否有权限进行拦截

  • AbstractShiroFilter
  • ShiroFilter 是整个Shiro的入口点
  • SpringShiroFilter Shiro 针对 spring 提供的入口点

  • PathConfigProcessor

Subject & SecurityManager

相关类 UML 如下图:

相关类UML

  • Subject 是 shiro 重要的接口,对外的API核心
  • DelegatingSubject 实现了将Subject上大多数的方法委托给SecurityManager去处理的。
  • WebDelegatingSubject shiro 提供的Subject的实现
  • SecurityManager 是 shiro 上执行所有安全操作的接口,且它管理着所有Subject,并负责与其他组件进行交互。
  • DefaultSecurityManager
  • DefaultWebSecurityManager shiro 上的一个默认实现,可以向其注入SessionManager,以实现 Session 的管理
  • ShiroFilterFactoryBean 是shiro 提供给 spring 中需要配置的一个bean,提供SecurityManagerFilterRealm等的注入,被注入的SecurityManager会传递给SpringShiroFilter
  • FilterChainManager 管理ShiroFilterFactoryBean中配置的自定义Filter
  • DefaultFilterChainManager shiro 上的一个默认实现

参考

Read More

一、编程规约

(八)、注释规约

1.【强制】类、类属性、类方法的注释必须使用Javadoc规范,使用/**内容*/格式,不得使用// xxx方式。

说明:在IDE编辑窗口中,Javadoc方式会提示相关注释,生成Javadoc可以正确输出相应注释;
在IDE中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。

2.【强制】所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。

说明:对子类的实现要求,或者调用注意事项,请一并说明。

3.【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。

说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。

4.【推荐】无用的代码,应删除,而非注释。

说明:代码仓库保存了历史代码。代码仓库已有代码备份功能,无需将备份代码注释。
过多的无用代码会影响阅读代码的质量与速度。

5.【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。

  • 待办事宜(TODO)

    表示需要实现,但目前还未实现的功能。这实际上是一个Javadoc的标签,目前的Javadoc还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个Javadoc标签)。

  • 错误,不能工作(FIXME)

    在注释中用FIXME标记某代码是错误的,而且不能工作,需要及时纠正的情况。

二、异常日志

(一)、异常处理

1.【强制】不要捕获Java类库中定义的继承自RuntimeException的运行时异常类(MdpExeption及其子类除外)。 如:IndexOutOfBoundsException/NullPointerException,这类异常由程序员预检查来规避,保证程序健壮性。

正例:if(obj != null) {...}

反例:try { obj.method() } catch(NullPointerException e){...}

2.【强制】对大段代码进行try-catch,这是不负责任的表现。 catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。 对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。

3.【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。 最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。 在 TSP 中 最外层的已有异常处理类SimpleMappingExceptionResolver,编写业务类时可以只抛异常。 但若是ajax请求,在处理error(请求失败)时,需调用Mdp.handleException显示错误信息

4.【强制】try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。

5.【强制】不能在finally块中使用returnfinally块中的return返回后方法结束执行,不会再执行try块中的return语句。

6.【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。

(二)、日志规约

1.【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。

正例:logger.error(各类参数或者对象toString, e);

反例:logger.error(e.getMessage());

三、单元测试

1.【强制】好的单元测试必须遵守AIR原则。

说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。
好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
  • A:Automatic(自动化)
  • I:Independent(独立性)
  • R:Repeatable(可重复)

2.【强制】单元测试应该是全自动执行的,并且非交互式的。 测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。 输出结果需要人工检查的测试不是一个好的单元测试。 单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。

3.【强制】保持单元测试的独立性。 为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。

反例:method2需要依赖method1的执行,将执行结果作为method2的输入。

4.【强制】单元测试是可以重复执行的,不能受到外界环境的影响。

说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。
如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。

正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring这样的DI框架注入一个本地(内存)实现或者Mock实现。

6.【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。

说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。

8.【推荐】单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%

说明:可重用度高的`Service`,都应该进行单元测试。

10.【推荐】对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。

反例:删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数据并不符合业务插入规则,导致测试结果异常。

11.【推荐】和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。 或者对单元测试产生的数据有明确的前后缀标识。

正例:在RDC内部单元测试中,使用RDC_UNIT_TEST_的前缀标识数据。

正例:TSP系统中,继承MdpTransactionalContextTests的类已自带回滚机制。

12.【推荐】对于不可测的代码建议做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码。

14.【参考】为了更方便地进行单元测试,业务代码应避免以下情况:

  • 构造方法中做的事情过多。
  • 存在过多的全局变量和静态方法。
  • 存在过多的外部依赖。
  • 存在过多的条件语句。

    说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。

15.【参考】不要对单元测试存在如下误解:

  • 那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。
  • 单元测试代码是多余的。汽车的整体功能与各单元部件的测试正常与否是强相关的。
  • 单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
  • 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。

零、其他

1.【强制】Controller类中,不需要进行额外的异常处理,所有异常处理交个spring框架统一处理。

2.【推荐】Controller类中,不应出现业务逻辑;业务逻辑应封装至Service接口中,以便于单元测试和复用。

3.【推荐】Controller类中的Action方法的参数尽量不要使用Json对象来传参,尽量使用基本类型或定义的类(如DTO等)。

4.【推荐】少用Map对象进行传参(包括:入参、出参)。

说明:使用`Map`做为参数或返回值时,调用方或使用该方法的同学无法知道需要传递/返回哪些数据并进行校验。除非在javadoc中进行说明。

5.【推荐】ServiceDAO层的接口方法,在实现时应该对传入的参数进行合法性检查。 说明:防御式编程–人类都是不安全、不值得信任的,所有的人,都会犯错误,而你写的代码,应该考虑到所有可能发生的错误,让你的程序不会因为他人的错误而发生错误。

参考

Read More

Info

JDK 安装目录下的 bin 目录中的工具其实实现是在 tool.jar 中的。

jps

类似 Linux 下的 ps,但只列出 Java 的进程。

参数说明:

  • -q: 值输出进程 ID
  • -m: 输出传递给 Java 进程(主函数)的参数
  • -l: 输出主函数的完整路径
  • -v: 显示传递给 JVM 的参数

jstat

查看堆信息。

jstat -<option> [-t] [-h<line>] <vmid> [<interval> [count]]

选项 option 说明:

  • -class: ClassLoader 相关信息
  • -compiler: 显示 JIT 编译相关信息
  • -gc: 显示 GC 大小及使用情况
  • -gccapacity: 显示各代当前、最小、最大值
  • -gccause: 显示 GC 原因(上一次和当前)
  • -gcnew: 显示 GC 新生代信息
  • -gcnewcapacity:
  • -gcold: 显示老年代和永久代信息
  • -gcoldcapacity:
  • -gcpermcapacity:
  • -gcutil: 显示 GC 使用百分比
  • -printcompilation: 输出 JIT 编译的方法信息

其他参数说明:

  • -t: 显示程序运行时间
  • -h: 周期输出数据多少行后再次输出表头信息
  • interval: 采样间隔(秒)
  • count: 采样次数

表头列说明(大小单位均为 KB):

  • Loaded: 载人类的数量
  • Unloaded: 卸载类的数量
  • S0C: s0 大小
  • S1C: s1 大小
  • S0U: s0 已使用空间
  • S1U: s1 已使用空间
  • EC: eden 大小
  • EU: eden 已使用空间
  • OC: 老年代大小
  • OU: 老年代已使用空间
  • PC: 永久代大小
  • PU: 永久代已使用空间
  • YGC: 新生代 GC 次数
  • YGCT: 新生代 GC 时间
  • FGC: 老年代 GC 次数
  • FGCT: 老年代 GC 时间
  • GCT: GC 总耗时
  • LGCC: 上次 GC 原因
  • GCC: 当前 GC 原因
  • TT: 新生代晋升到老年代的年龄
  • MTT: 新生代晋升到老年代的最大年龄
  • DSS: 所需 survivor 区大小

jinfo

// todo

jmap

查看堆内存使用状况。

// 查看进程堆内存使用情况,包括使用的GC算法、堆配置参数和各代中堆内存使用情况
jmap -heap <pid>

// 查看堆内存中的对象数目、大小统计直方图,如果带上live则只统计活对象
jmap -histo[:live] <pid>

// 把进程内存使用情况dump到文件中,再用jhat分析查看
jmap -dump:format=b,file=<dumpFileName> <pid>

jhat

分析堆内存

jhat [-port <num>] <hprofFileName>

jstack

导出 java 程序的线程堆栈

jstack [-l] <pid>

-l: 打印锁的附加信息

可以配合使用的其他 linux 命令:

// 显示指定进程的线程信息(cpu使用率等)
top -Hp <pid>
// 10进制转16进制
printf "%x\n" <num>

jstatd

// todo

hprof

// todo

参考

Read More

svn 工作流及实践

基础

  • 语义化版本: 如何定义项目版本号

  • 保证系统有合理边界,即:
    • 可单独演进
    • 可单独测试
    • 可单独部署
    • ……
  • 每个项目下需包含以下3个目录
    • trunk : 主干(代码是稳定的,不负担开发)
    • branches : 开发分支(功能,bugfix)
    • tags : 稳定版(只读),用于发布 。

流程

svn分支方案建议

svn分支方案建议

本方案 svn 目录结构:

root
|-- trunk
|-- branches
|   |-- v1.0-dev
|   |-- v1.0-stage
|   |-- v2.1-dev
|   |-- v2.1-stage
|   |-- v2.1.1-stage
|   `-- ...
`-- tags
    |-- v1.0.0
    |-- v1.1.0
    |-- v1.1.1
    |-- v2.0.0
    `-- ...

其中:

trunk 主干是代码稳定的。每次上线时都是从 trunk 上创建一个对应版本号的 tag 分支。

branchs 上有两种分支 dev 和 stage ,均从同一稳定的 trunk 上创建。 其中 dev 用于代码开发,开发完成后合并至 stage 分支并测试,最后当 stage 测试稳定了,再合并回 trunk 主干。

具体操作(java)

功能开发(项目迭代)

每增加一个(大的)功能或迭代

  1. 从最新的稳定的 trunk 建立两个 branche 分支,分别为开发分支(命名格式:v<主版本号>.<次版本号>-dev)和 stage 分支(命名格式:v<主版本号>.<次版本号>-stage)。
  2. 在开发分支上进行开发并测试后提交代码至 svn。
  3. 当该分支的所有功能点均完成后,将该分支的所有提交合并至 stage 分支上。
  4. 对 stage 分支上的功能进行测试。测试不通过的返回步骤 2,通过的跳至步骤 5。
  5. 当 stage 分支上的功能达到稳定状态时,将其合并至 trunk 主干进行上线操作:
    1. 将程序(pom.xml)中的版本号<主版本号>.<次版本号>.<修订号>-SNAPSHOT更改为<主版本号>.<次版本号>.<修订号>,并提交 svn。
    2. 打标签(tag):从 trunk 中建立一个 tag 分支。(命名格式:v<主版本号>.<次版本号>.<修订号>)
    3. 程序(pom.xml)中的版本号的次版本号(或修订号)+1,并在版本号后添加-SNAPSHOT,然后提交 svn。
    4. 从 svn 中检出刚刚的 tag 分支,对该分支代码打包、上线。

注:上线操作可以考虑使用 maven-release-plugin 插件

修复缺陷(bug fix)

缺陷的修复分两种情况:

对未上线的版本进行修复

走功能开发流程

对已经上线的版本进行修复(紧急修复)

流程如下:

  1. 在 dev 分支上进行开发,当所有 bug 修复完成后,提交至 svn。
  2. 从最新的稳定的 trunk 建立一个 branche 分支,用于测试此次修复的 stage 分支(命名格式:v<主版本号>.<次版本号>.<修订号>-stage)。
  3. 将提交(仅修复的代码)合并进此 stage 分支,并对其进行测试。测试不通过的返回步骤1,通过的跳至步骤 4.
  4. 当 stage 分支上的功能达到稳定状态时,将其合并至 trunk 主干进行上线操作:
    1. 将程序(pom.xml)中的版本号的修订号+1,并提交 svn。
    2. 打标签(tag):从 trunk 中建立一个 tag 分支。(命名格式:v<主版本号>.<次版本号>.<修订号>)
    3. 从 svn 中检出刚刚的 tag 分支,对该分支代码打包、上线。

流程简化

世界上没有银弹,任何实际问题都要具体分析、解决。 每个项目、每个团队都有适合自己的流程。 可以根据自己项目和团队的特点相应简化开发流程。

简化点1

不必每次增加功能都重新创建 dev 和 stage 分支。

简化点2

紧急修复时可以不建立新的 stage 分支。 但必须保证原 stage 分支上的代码不影响修改的代码。

简化点3

可舍弃 stage 分支,直接在 trunk 上测试。 但这时必须清楚 trunk 上的代码是不稳定的,以及有合适的紧急修复方案。

关于标签

不建议取消打标签,标签可以清楚划分系统的各个版本,有利于维护。 版本相关知识参加下面链接。

其他

git 工作流简介

分支模型:

git-branch-model

两个主分支:

  • 产品分支: master
  • 开发分支: develop

其他分支:

  • 功能分支: feature/***
  • 修复分支: hotfix/***
  • 发布分支: release/***

关键操作:

  • 每个功能均要建立一个功能分支,单独演进,完成后合并至 develp
  • 修复分支必须从 master 中建立,完成后必须同时合并至 master 和 develop

具体参见:一个成功的 Git 分支模型

git 与 svn 的一些差异

  • 分支对于 git 来说成本非常低,而且建立快且方便,git 官方也鼓励多建立分支。但这并不适用与 svn。
  • git 上的合并会将两个分支上的所有更改都进行合并;而在 svn 上可以只合并分支上的某次或多次提交。

参考

Read More

相关知识

使用 RMAN 进行备份,数据库需要在归档模式下。

重做日志

重做日志记录了数据库的所有变更信息,如:INSERT,UPDATE,DELETE,CREATE,ALTER 或者 DROP 等。

每个数据库须最少有两个重做日志组,默认自动创建3个。

每个重做日志均有大小限制,循环使用。

归档日志

将重做日志归档保存后即为归档日志。

归档模式/非归档模式

数据库可以运行在归档模式或非归档模式下。

归档模式,又分自动存档和手工存档,默认是自动的。

  • 非归档模式:若所有重做日志均写满,将自动覆盖。

  • 归档模式(自动存档):若重做日志写满,自动将重做日志保存至指定的归档位置(10g后默认为闪回区,见下)。

  • 归档模式(手动存档):若重做日志写满,数据库将阻止数据写入,提示相关错误,须手工归档后才能继续提供服务。

闪回区

10g 后增加了闪回区(flash recovery area)来存放归档日志。

默认路径为数据库路径下的 flash_recovery_area 目录。

默认大小为 2G。

注意:若超出了大小,也会提示错误,阻止数据写入。 须进行备份后删除(使用 RMAN)或扩大闪回区大小。数据库才能继续提供服务。

综上,服务器若运行在归档模式下,则必须进行归档日志备份, 否则会因重做日志写满或归档日志满而导致数据库无法提供服务。

备份

脚本及说明

文件backupLv0.bat: 为 window 下的批处理文件,其调用 rman 脚本进行全库备份(0级)。

# 设置数据库sid
set oracle_sid=orcl
set y=%date:~0,4%
set m=%date:~5,2%
set d=%date:~8,2%
if "%time:~0,2%" lss "10" (set h=0%time:~1,1%) else (set h=%time:~0,2%)
set mi=%time:~3,2%
set s=%time:~6,2%
# 调用 rman 脚本
rman target / log='%y%%m%%d%_%h%%mi%%s%.log' cmdfile='backupWithRmanLv0.rman'

文件backupWithRmanLv0.rman: 为 rman 备份脚本

run{
    # 备份集保留策略,保留14天内的备份
    configure retention policy to recovery window of 14 days;

    # 设置自动备份控制文件及路径
	configure controlfile autobackup on;
	configure controlfile autobackup format for device type disk to "F:/OracleBackup/rman/ctrl_%F.bak";

    # 设置备份集通道和路径,表空间全备份(0级),同时备份归档日志,完成后删除已备份的归档日志
	allocate channel ch1 device type disk format "F:/OracleBackup/rman/%d_%T_%t.bak";
	backup incremental level=0 tablespace ELBONLINE_DATA skip inaccessible
	    plus archivelog
		delete all input;
	release channel ch1;
}
allocate channel for maintenance device type disk;
crosscheck backupset;
delete noprompt obsolete;

以上脚本为0级备份脚本,1级备份请修改 rman 脚本中的相关参数(level=*)。 关于备份级别,请参考下文。

表空间、数据文件备份与全库备份的差别:非全库备份时,备份的保留策略不会应用于归档日志文件备份集

因为归档日志文件里包含其他未备份的数据。

备份时间

备份时间由系统执行计划进行设置。(注意全局与差异备份应有时间差,否则同时执行会影响备份执行时间和数据库性能)

关于增量备份

Oracle 的增量备份是通过备份级别实现的。

level 0 级是对数据库的全库备份,增量备份必须从 0 级开始。然后才有 1,2,3,4 级。

rman 增量备份分为差异增量备份(默认)和累积增量备份。

差异增量备份示例:

我们在星期天执行0级差异增量备份操作,这个备份操作会备份整个数据库。 根据这个0级备份,我们在星期一执行1级差异增量备份操作。该备份操作将备份自周日0级备份以来所有发生变化的数据块。 在周二时1级增量备份将备份所有自周一1级备份以来发生变化的数据块。 如果要执行恢复操作,就需要星期一、星期二生成的备份以及星期天生成的基本备份。

更多相关信息查看文章最后链接。

恢复

恢复步骤

1.设置数据库状态为:mount 或 open。整库恢复需要在 mount 状态下,表控件或数据文件恢复可以在 open 状态下。

2.执行恢复操作。分完全和不完全,区别是,完全恢复是应用所有的重做日志,不完全则可以指定部分重做日志来恢复到指定的时间点。

3.打开数据库。不完全恢复需额外附加 resetlogs

示例

整库恢复(完全介质恢复)

以下命令无特殊说明,均在 RMAN 提示符下操作,进入 RMAN 提示符:rman target user/password@sid

// 启动数据库至 mount 状态
startup mount;
// 恢复
restore database;
recover database delete archivelogs skip tablespace temp;
// 打开数据库
alter database open;

其中:

delete archivelogs 表示删除恢复过程中产生的归档日志文件。

skip tablespace temp 标识跳过临时表空间

表空间和数据文件恢复

表空间和数据库文件恢复,可以在 open 状态下进行。

// 表空间
sql 'alter tablespace *** offline immediate';
restore tablespace ***;
recover tablespace ***;
sql 'alter tablespace ** online';
// 数据文件,其中 n 为数据文件序号或详细路径
sql 'alter database datafile n offline';
restore datafile n;
recover datafile n;
sql 'alter database datafile n online';

若,表空间或数据库文件所在的磁盘出现故障,需要切换至其他路径时(控制文件没有损坏)

// 表空间,其中 datafile 1 为原表空间对应的数据文件
run {
	sql 'alter tablespace *** offline immediate';
	set newname for datafile 1 to 'f:\path\***.dbf'
	restore tablespace ***;
	swith tablespace ***;
	recover tablespace ***;
	sql 'alter tablespace ** online';
}
// 数据文件,其中 n 为数据文件序号
run {
	sql 'alter database datafile n offline';
	set newname for datafile n to 'f:\path\***.dbf'
	restore datafile n;
	swith datafile n;
	recover datafile n;
	sql 'alter database datafile n online';
}

控制文件的恢复

若控制文件也损坏了,必须先恢复控制文件,因为控制文件记录了备份的信息。

// 需先设置 DBID ,如何查询目标数据库的 DBID ,请自行 google
set dbid=***********;
// 恢复控制文件,数据库需在 nomount 状态下
startup nomount;
// 设置控制文件备份所在的路径及名称
set controlfile autobackup format for device type disk to 'f:\OracleBackup\rman\ctrl_%F.bak';
// 恢复至默认路径
restore controlfile from autobackup;
// 或恢复至指定路径
restore controlfile to 'f:\path\control01.ctl' from autobackup;
// 下面这句是应用控制文件备份以后的重做日志,即恢复备份以后的数据库
alter database mount;
recover database;
alter database open resetlogs;

又或者

set dbid=***********;
startup nomount;
// 直接从指定的备份文件恢复
restore controlfile to 'f:\path\control01.ctl' from 'f:\OracleBackup\rman\ctrl_****.bak';

不完全恢复

run {
	startup force mount;
	// 以下3选1
	// 恢复到指定时间
	set until time='yyyy-MM-DD HH:Mi:SS';
	// 恢复到指定 SCN
	set until scn=123456;
	// 恢复到指定日志序列号
	set until sequence=10;
	restore database;
	recover database;
	sql 'alter database open resetlogs';
}

恢复到其他服务器上

须保证待恢复的目标端目录结构与源端的一致。

须保证数据库的 SID 一致。

须先恢复 SPFILE 文件和控制文件。

其他操作同上。


参考

Read More

重做日志的作用与原理

概念:

  • Redo Log Buffer
  • LGWR
  • 日志文件(Redo Log File)
  • 日志组(至少两个,默认3个,循环使用)
  • Log Switch
  • 检查点(checkpoint)
  • 归档模式(archivelog)

重做日志与锁(latch)

锁:

  • Redo Copy Latch:复制日志到 Redo Log Buffer
  • Redo Allocation Latch:分配内存空间

select * from v$latch;

select addr,latch#,child#,name,gets,immediate_gets,immediate_misses 
  from v$latch_children where name = 'redo writing'; 
  
  SELECT  substr(ln.name, 1, 20), gets, misses, immediate_gets, immediate_misses  
FROM v$latch l, v$latchname ln  
WHERE   ln.name in ('redo allocation', 'redo copy')  and ln.latch# = l.latch#;

select * from v$version where rownum <2; 

重做日志的大小

 select a.name,b.value 
  from v$statname a,v$mystat b 
  where a.STATISTIC# = b.STATISTIC# and a.name = 'redo size'; 
  
  select * from v$mystat;
  select * from v$statname;
  
  select name,value 
  from v$sysstat where name='redo size';
  select startup_time from v$instance; 
  
 select trunc(COMPLETION_TIME),sum(Mb)/1024 DAY_GB from 
  (select name,COMPLETION_TIME,BLOCKS*BLOCK_SIZE/1024/1024 Mb from v$archived_log 
  where COMPLETION_TIME between trunc(sysdate) -2 and trunc(sysdate) -1) 
  group by trunc(COMPLETION_TIME) ;
  
  SELECT   TRUNC (completion_time), SUM (mb) / 1024 day_gb 
      FROM (SELECT NAME, completion_time, blocks * block_size / 1024 / 1024 mb 
              FROM v$archived_log) 
  GROUP BY TRUNC (completion_time); 

重做日志触发条件

  • 每 3 秒
  • 阈值
  • 提交(commit)
  • DBWn 写之前

Redo Log Buffer 大小设置

Redo Log Buffer 的写出是很频繁的,过大的 Log Buffer 是没有必要的。

当 Log Buffer Space 等待事件出现较为显著时,可以适当增大 Log Buffer 大小以减少竞争。

-- 查询 Log Buffer Space 事件
select event#,name from v$event_name where name='log buffer space'; 
-- 
show parameter log_buffer;

不是明显的性能问题,一般缺省设置(Max(512 KB , 128 KB * CPU_COUNT))是足够的。

Log File Sync

commit 流程:

  1. 用户进程向 LGWR 进程提交写请求;
  2. LGWR 执行写出(日志),用户进程处于 Log File Sync 等待;
  3. LGWR 进程发出日志写动作;
  4. LGWR 写完成;
  5. LGWR 通知用户进程;
  6. 用户进程标记提交完成。

重做日志状态

  • current
  • active
  • inactive
  • unused
  select name,value from v$sysstat 
  where name in ('redo size','redo wastage','redo blocks written');

重做日志块大小

512K

重做日志文件大小及调整

原则:把日志切换(Log Switch)的时间控制在 30 分钟左右。

热备产生的重做日志

-- 查询热备信息
select * from v$backup;

不生成重做日志

数据库模式 表模式 插入模式 redo 生成
archive log logging append
archive log logging no append
archive log nologging append
archive log nologging no append
no archive log logging append
no archive log logging no append
no archive log nologging append
no archive log nologging no append
-- 查询表模式
select table_name,logging 
from dba_tables where table_name='TEST'; 

数值在 Oracle 的内部储存

  select dump(0),dump(0,16) from dual; 

Read More

webpack 使用

第3方代码/库的引入

本项目支持npm上所有的代码库。

只需要运行如下命令安装所需<代码库>

npm i <代码库> --save

然后在代码中添加如下代码:

require('<代码库>')
// 或
var modulename = require('<代码库>')

就可以使用该代码库提供的功能。

如引入jquery-uidraggablesortable:

先执行命令:

npm i jquery-ui --save

然后在代码中添加:

require('jquery-ui/draggable');
require('jquery-ui/sortable');

上面是只添加jquery-ui的 draggable 和 sortable 功能,其他的功能代码不会引入。

webpack 实时编译 js

运行

webpack --watch
// 或
npm run watch

则,当js文件变化后,将马上重新编译打包所有的js文件。

webpack 实时预览

运行

gulp hmr
//或
.\node_modules\.bin\gulp hmr	//(未全局安装`gulp`时)

会打开一个浏览器,实时监控,若js改变了,会自动重新加载新的文件。

若出现如下错误:

events.js:141
      throw er; // Unhandled 'error' event
      ^ Error: listen EADDRINUSE 127.0.0.1:8088

请检查8088端口是否被占用,释放该端口或修改gulpfile.js文件中相应的端口。


参考:

webpack 官方文档

Read More

Install Ruby

RubyInstallers 下载并安装。

安装时,勾选 “Add Ruby executables to your PATH”,这样执行程序会被自动添加至 PATH 而避免不必要的头疼。

运行以下命令检测安装是否成功:

ruby -v

Install DevKit

DevKit 是一个在 Windows 上帮助简化安装及使用 Ruby C/C++ 扩展如 RDiscountRedCloth 的工具箱。详细的安装指南可以在程序的 wiki 页面阅读。

再次前往 RubyInstallers 下载与ruby相对应的版本。

运行安装包并解压至某个文件夹(我的是D:/Devkit),然后创建 config.yml 文件:

cd D:\DevKit
ruby dk.rb init

然后会在解压的目录下创建一个 config.yml 文件,使用编辑器打开,并在末尾添加一行 -D:/Ruby200-x64 (即ruby的安装路径)。

再输入如下命令进行安装:

ruby dk.rb review
ruby dk.rb install

Install Jekyll

运行以下命令:

gem install jekyll

若出错无法链接,就试试 ruby 的国内镜像吧。

Install pygments

新版本的 Jekyll 已经包含了 pygments 的 ruby 版,但仍需要再安装 Python (依赖 Python)。

Install Python

Python 官网下载并安装。 Python 2 可能会更合适,因为 Python 3 可能不会正常工作。

检验 Python 安装是否成功

python -version

Install pip

python 2.7.9 和3.4以后的版本已经内置累pip程序,不需另外安装。

若已经使用 pygments 的 ruby 版,不需要这一步了。

下载 get-pip.py 脚本。 然后运行该脚本,如下:

python get-pip.py

Install pygments

若已经使用 pygments 的 ruby 版,不需要这一步了。

运行以下命令进行安装

pip pygments

参考

Read More

Info

// TODO

Install

npm install -g grunt-cli

Gruntfile 配置文件

// 每一份 Gruntfile (和grunt插件)都遵循同样的格式,你所书写的Grunt代码必须放在此函数内
module.exports = function (grunt) {
	// grunt 相关的东西都在这里

	grunt.initConfig({
		// 项目和任务配置在这里
		jshint: { /* jshint的参数 */ },
		concat: { /* concat的参数 */ },
		uglify: { /* uglify的参数 */ },
		watch:  { /* watch的参数 */ }
	});

	// 从node_modules目录加载插件
	grunt.loadNpmTasks('grunt-contrib-jshint');

	// 每行registerTask定义一个任务
	// default 是 grunt 的默认任务,执行`grunt`命令时将执行默认任务
	grunt.registerTask('default', ['jshint', 'concat', 'uglify']);	
	// 定义任务名称为 check
	grunt.registerTask('check', ['jshint']);

	// 还可以自定义任务
	grunt.registerTask('default', 'Log some stuff.', function() {
		grunt.log.write('Logging some stuff ...').ok();
	});
	
};

参考

Read More