hello 2015
hello 2015,我们在上线!
数据库设计原则
从@蔡学镛看到的数据库的一些设计原则,可以考虑考虑.
1.
梳理数据库时,你会很惊讶地发现,各种数据都被塞进数据库,所以做数据库梳理的第一步是把它们区分出来,我的区分方式是:核心数据、业务数据、核心缓存数据、业务缓存数据、Session 数据。核心数据及其缓存都要再根据领域(domain)来区分,业务数据及其缓存都要再根据业务(business)来区分。
2.
梳理数据库或设计数据存储时,可以考虑数据的属性:
1. 访问频率 (高/中/低)
2. 读写比 (只读/读多/读少)
3. 重要性 (重要/普通/不重要)
4. 保密性 (保密/普通/不需保密)
5. 数据笔数 (多/一般/少)
6. 数据体积 (大/中/小)
7. 一致性要求 (强/中/弱)
8. 热点现象 (强/中/弱)
9. 索引方式 ( __ )
double cookie验证
from: http://www.75team.com/archives/729
什么是double cookie验证
double cookie验证是利用cookie来验证请求合法性的一种方法。
一个double cookie验证的url形如
http://a.com?c=cookie
向服务器请求的url带上cookie,服务器收到请求后,解析出url中的cookie和http请求带过来的cookie进行对比。如果一样就说明请求合法,不一样就可以判定请求非法。
因为是用url中的cookie和http请求中的cookie进行验证,所以叫double cookie验证。
验证原理
url是客户端javascript生成的,javascript可以读取cookie。
url可以从任意客户端访问,但是只有一个客户端的http请求带给服务器的cookie和url中的cookie是一致的。
注意事项
- 用来验证的cookie在每个客户端必须唯一
- 用来验证的cookie不能是敏感信息。比如登录用户的token不能作为验证的cookie
应用场景
double cookie验证不能判断用户修改本地cookie然后再进行的访问。但是没有关系。举个应用场景。
比如一个网站的 评分功能的请求是这样的
http://a.com?score=100&id=10000
如果有人把这个url发到网上,引诱其它用户来点击,那么就会生成大量的评分请求。这时就可以用double cookie验证了。因为很难让点击这个链接的人先修改本地cookie然后再点击链接
使用建议
有的同学喜欢把cookie做个变换,防止别人一眼看出来是用哪个cookie做的验证。没必要这样做,也不应该这样做。
- 变换的方法一定会暴露在客户端
- 验证的目的不是为了防止有意伪造
- 对cooie做变换的方法如果不得当,可能会对cookie的结构造成依赖。如果哪天cookie的结构改变了,就会使验证代码失效,甚至报错。
Web安全之SQL注入攻击技巧与防范
from: http://www.plhwin.com/2014/06/13/web-security-sql/
Web安全简史
在Web1.0时代,人们更多是关注服务器端动态脚本语言的安全问题,比如将一个可执行脚本(俗称Webshell)通过脚本语言的漏洞上传到服务器上,从而获得服务器权限。在Web发展初期,随着动态脚本语言的发展和普及,以及早期工程师对安全问题认知不足导致很多”安全血案”的发生,至今仍然遗留下许多历史问题,比如PHP语言至今仍然无法从语言本身杜绝「文件包含漏洞」(参见这里),只能依靠工程师良好的代码规范和安全意识。
伴随着Web2.0、社交网络、微博等一系列新型互联网产品的兴起,基于Web环境的互联网应用越来越广泛,Web攻击的手段也越来越多样,Web安全史上的一个重要里程碑是大约1999年发现的SQL注入攻击,之后的XSS,CSRF等攻击手段愈发强大,Web攻击的思路也从服务端转向了客户端,转向了浏览器和用户。
在安全领域,一般用帽子的颜色来比喻黑客的善与恶,白帽子是指那些工作在反黑客领域的技术专家,这个群体是”善”的的象征;而黑帽子则是指那些利用黑客技术造成破坏甚至谋取私利造成犯罪的群体,他们是”恶”的代表。
“白帽子”和”黑帽子”是两个完全对立的群体。对于黑帽子而言,他们只要找到系统的一个切入点就可以达到入侵破坏的目的,而白帽子必须将自己系统所有可能被突破的地方都设防,以保证系统的安全运行。
这看起来好像是不公平的,但是安全世界里的规则就是这样,可能我们的网站1000处都布防的很好,考虑的很周到,但是只要有一个地方疏忽了,攻击者就会利用这个点进行突破,让我们另外的1000处努力白费。
常见攻击方式
一般说来,在Web安全领域,常见的攻击方式大概有以下几种:
1、SQL注入攻击
2、跨站脚本攻击 - XSS
3、跨站伪造请求攻击 - CSRF
4、文件上传漏洞攻击
5、分布式拒绝服务攻击 - DDOS
说个题外话,本来这篇文章一开始的标题叫做 「Web安全之常见攻击方法与防范」,我原本想把上面的这5种方法都全部写在一篇文章里,可是刚写完第一个SQL注入攻击的时候,就发现文章篇幅已经不短了,又很难再进行大幅度的精简,所以索性把Web安全分成一个系列,分多篇文章来呈现给大家,下面你看到的就是第一篇「Web安全之SQL注入攻击的技巧与防范」。
SQL注入常见攻击技巧
SQL注入攻击是Web安全史上的一个重要里程碑,它从1999年首次进入人们的视线,至今已经有十几年的历史了,虽然我们现在已经有了很全面的防范对策,但是它的威力仍然不容小觑,SQL注入攻击至今仍然是Web安全领域中的一个重要组成部分。
以PHP+MySQL为例,让我们以一个Web网站中最基本的用户系统来做实例演示,看看SQL注入究竟是怎么发生的。
1、创建一个名为demo的数据库:
CREATE DATABASE |
CREATE TABLE |
实例一
通过传入username
参数,在页面打印出这个会员的详细信息,编写 userinfo.php
程序代码:
<?php |
multi_query
方法来支持同时执行多条SQL语句,这样能更好的说明SQL注入攻击的危害性。假设我们可以通过
http://localhost/test/userinfo.php?username=plhwin
这个URL来访问到具体某个会员的详情,正常情况下,如果浏览器里传入的username是合法的,那么SQL语句会执行:SELECT uid,username FROM user WHERE username=’plhwin’ |
plhwin';SHOW TABLES-- hack
,也就是当URL变为 http://localhost/test/userinfo.php?username=plhwin';SHOW TABLES-- hack
的时候,此时我们程序实际执行的SQL语句变成了:SELECT uid,username FROM user WHERE username=’plhwin’;SHOW TABLES– hack’ |
-- hack
结尾。经过上面的SQL注入后,原本想要执行查询会员详情的SQL语句,此时还额外执行了
SHOW TABLES;
语句,这显然不是开发者的本意,此时可以在浏览器里看到页面的输出:Array |
user
也被打印在了页面上,如果作恶的黑客此时将参数换成 plhwin';DROP TABLE user-- hack
,那将产生灾难性的严重结果,当你在浏览器中执行http://localhost/test/userinfo.php?username=plhwin';DROP TABLE user-- hack
这个URL后,你会发现整个 user
数据表都消失不见了。通过上面的例子,大家已经认识到SQL注入攻击的危害性,但是仍然会有人心存疑问,MySQL默认驱动的
mysql_query
方法现在已经不支持多条语句同时执行了,大部分开发者怎么可能像上面的演示程序那样又麻烦又不安全。是的,在PHP程序中,MySQL是不允许在一个
mysql_query
中使用分号执行多SQL语句的,这使得很多开发者都认为MySQL本身就不允许多语句执行了,但实际上MySQL早在4.1版本就允许多语句执行,通过PHP的源代码,我们发现其实只是PHP语言自身限制了这种用法,具体情况大家可以看看这篇文章「PHP+MySQL多语句执行」。## 实例二
如果系统不允许同时执行多条SQL语句,那么SQL注入攻击是不是就不再这么可怕呢?答案是否定的,我们仍然以上面的user数据表,用Web网站中常用的会员登录系统来做另外一个场景实例,编写程序
login.php
,代码如下:<?php |
plhwin
和密码 123456
,执行的SQL语句为:SELECT uid,username FROM user WHERE username=’plhwin’ AND password=’e10adc3949ba59abbe56e057f20f883e’ |
plhwin' AND 1=1-- hack
,密码随意输入,比如aaaaaa
,那么拼接之后的SQL查询语句就变成了如下内容:SELECT uid,username FROM user WHERE username=’plhwin’ AND 1=1– hack’ AND password=’0b4e7a0e5fe84ad35fb5f95b9ceeac79’ |
1=1
是永远成立的条件,这意味着黑客只需要知道别人的会员名,无需知道密码就能顺利登录到系统。# 如何确定SQL注入漏洞
通过以上的实例,我们仍然还会有疑问:黑客并不知道我们程序代码的逻辑和SQL语句的写法,他是如何确定一个网站是否存在SQL注入漏洞呢?一般说来有以下2种途径:
## 1、错误提示
如果目标Web网站开启了错误显示,攻击者就可以通过反复调整发送的参数、查看页面打印的错误信息,推测出Web网站使用的数据库和开发语言等重要信息。
## 2、盲注
除非运维人员疏忽,否则大部分的Web运营网站应该都关闭了错误提示信息,此时攻击者一般会采用盲注的技巧来进行反复的尝试判断。 仍然以上面的数据表user为例,我们之前的查看会员详情页面的url地址为
userinfo.php?username=plhwin
,此时黑客分别访问userinfo.php?username=plhwin' AND 1=1-- hack
和userinfo.php?username=plhwin' AND 1=2-- hack
,如果前者访问能返回正常的信息而后者不能,就基本可以判断此网站存在SQL注入漏洞,因为后者的1=2
这个表达式永远不成立,所以即使username传入了正确的参数也无法通过,由此可以推断这个页面存在SQL注入漏洞,并且可以通过username
参数进行注入。# 如何防御SQL注入
对于服务器配置层面的防范,应该保证生产环境的Webserver是关闭错误信息的,比如PHP在生产环境的配置文件php.ini中的display_errors应该设置为Off,这样就关闭了错误提示,下面我们更多的从编码的角度来看看如何防范SQL注入。
上面用两个实例分析了SQL注入攻击的技巧,可以看到,但凡有SQL注入漏洞的程序,都是因为程序要接受来自客户端用户输入的变量或URL传递的参数,并且这个变量或参数是组成SQL语句的一部分,对于用户输入的内容或传递的参数,我们应该要时刻保持警惕,这是安全领域里的「外部数据不可信任」的原则,纵观Web安全领域的各种攻击方式,大多数都是因为开发者违反了这个原则而导致的,所以自然能想到的,就是从变量的检测、过滤、验证下手,确保变量是开发者所预想的。
## 1、检查变量数据类型和格式
如果你的SQL语句是类似
where id={$id}
这种形式,数据库里所有的id都是数字,那么就应该在SQL被执行前,检查确保变量id是int类型;如果是接受邮箱,那就应该检查并严格确保变量一定是邮箱的格式,其他的类型比如日期、时间等也是一个道理。总结起来:只要是有固定格式的变量,在SQL语句执行前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很大程度上可以避免SQL注入攻击。比如,我们前面接受
username
参数例子中,我们的产品设计应该是在用户注册的一开始,就有一个用户名的规则,比如5-20个字符,只能由大小写字母、数字以及一些安全的符号组成,不包含特殊字符
。此时我们应该有一个check_username
的函数来进行统一的检查。不过,仍然有很多例外情况并不能应用到这一准则,比如文章发布系统,评论系统等必须要允许用户提交任意字符串的场景,这就需要采用过滤等其他方案了。## 2、过滤特殊符号
对于无法确定固定格式的变量,一定要进行特殊符号过滤或转义处理。以PHP为例,通常是采用
addslashes
函数,它会在指定的预定义字符前添加反斜杠转义,这些预定义的字符是:单引号 (') 双引号 (") 反斜杠 (\) NULL
。来看2条SQL语句:
$uid = isset($_GET[‘uid’]) ? $_GET[‘uid’] : 0; |
$uid = isset($_GET[‘uid’]) ? $_GET[‘uid’] : 0; |
对于PHP程序+MySQL构架的程序,在动态的SQL语句中,使用单引号把变量包含起来配合
addslashes
函数是应对SQL注入攻击的有效手段,但这做的还不够,像上面的2条SQL语句,根据「检查数据类型」的原则,uid都应该经过intval
函数格式为int型,这样不仅能有效避免第二条语句的SQL注入漏洞,还能使得程序看起来更自然,尤其是在NoSQL(如MongoDB)中,变量类型一定要与字段类型相匹配才可以。从上面可以看出,第二个SQL语句是有漏洞的,不过由于使用了addslashes函数,你会发现黑客的攻击语句也存在不能使用特殊符号的条件限制,类似
where username='plhwin'
这样的攻击语句是没法执行的,但是黑客可以将字符串转为16进制编码数据或使用char
函数进行转化,同样能达到相同的目的,如果对这部分内容感兴趣,可以点击这里查看。而且由于SQL保留关键字,如「HAVING」、「ORDER BY」的存在,即使是基于黑白名单的过滤方法仍然会有或多或少问题,那么是否还有其他方法来防御SQL注入呢?## 3、绑定变量,使用预编译语句
MySQL的
mysqli
驱动提供了预编译语句的支持,不同的程序语言,都分别有使用预编译语句的方法,我们这里仍然以PHP为例,编写userinfo2.php
代码:<?php |
addslashes
函数,但是浏览器里运行http://localhost/test/userinfo2.php?username=plhwin' AND 1=1-- hack
里得不到任何结果,说明SQL漏洞在这个程序里并不存在。实际上,绑定变量使用预编译语句是预防SQL注入的最佳方式,使用预编译的SQL语句语义不会发生改变,在SQL语句中,变量用问号
?
表示,黑客即使本事再大,也无法改变SQL语句的结构,像上面例子中,username变量传递的plhwin' AND 1=1-- hack
参数,也只会当作username字符串来解释查询,从根本上杜绝了SQL注入攻击的发生。# 数据库信息加密安全
相信大家都还对2011年爆出的CSDN拖库事件记忆犹新,这件事情导致CSDN处在风口浪尖被大家痛骂的原因就在于他们竟然明文存储用户的密码,这引发了科技界对用户信息安全尤其是密码安全的强烈关注,我们在防范SQL注入的发生的同时,也应该未雨绸缪,说不定下一个被拖库的就是你,谁知道呢。
在Web开发中,传统的加解密大致可以分为三种:
1、对称加密:即加密方和解密方都使用相同的加密算法和密钥,这种方案的密钥的保存非常关键,因为算法是公开的,而密钥是保密的,一旦密匙泄露,黑客仍然可以轻易解密。常见的对称加密算法有:
AES
、DES
等。2、非对称加密:即使用不同的密钥来进行加解密,密钥被分为公钥和私钥,用私钥加密的数据必须使用公钥来解密,同样用公钥加密的数据必须用对应的私钥来解密,常见的非对称加密算法有:
RSA
等。3、不可逆加密:利用哈希算法使数据加密之后无法解密回原数据,这样的哈希算法常用的有:
md5
、SHA-1
等。在我们上面登录系统的示例代码中,
$md5password = md5($password);
从这句代码可以看到采用了md5的不可逆加密算法来存储密码,这也是多年来业界常用的密码加密算法,但是这仍然不安全。为什么呢?这是因为md5加密有一个特点:同样的字符串经过md5哈希计算之后生成的加密字符串也是相同的,由于业界采用这种加密的方式由来已久,黑客们也准备了自己强大的md5彩虹表来逆向匹配加密前的字符串,这种用于逆向反推MD5加密的彩虹表在互联网上随处可见,在Google里使用
md5 解密
作为关键词搜索,一下就能找到md5在线破解网站,把我们插入用户数据时候的MD5加密字符串e10adc3949ba59abbe56e057f20f883e
填入进去,瞬间就能得到加密前的密码:123456
。当然也并不是每一个都能成功,但可以肯定的是,这个彩虹表会越来越完善。所以,我们有迫切的需求采用更好的方法对密码数据进行不可逆加密,通常的做法是为每个用户确定不同的密码加盐(salt)后,再混合用户的真实密码进行md5加密,如以下代码:
<?php |
小结
1、不要随意开启生产环境中Webserver的错误显示。
2、永远不要信任来自用户端的变量输入,有固定格式的变量一定要严格检查对应的格式,没有固定格式的变量需要对引号等特殊字符进行必要的过滤转义。
3、使用预编译绑定变量的SQL语句。
4、做好数据库帐号权限管理。
5、严格加密处理用户的机密信息。
Web应用的缓存设计模式
from: http://robbinfan.com/blog/38/orm-cache-sumup
ORM缓存引言
从10年前的2003年开始,在Web应用领域,ORM(对象-关系映射)框架就开始逐渐普及,并且流行开来,其中最广为人知的就是Java的开源ORM框架Hibernate,后来Hibernate也成为了EJB3的实现框架;2005年以后,ORM开始普及到其他编程语言领域,其中最有名气的是Ruby on rails框架的ORM - ActiveRecord。如今各种开源框架的ORM,乃至ODM(对象-文档关系映射,用在访问NoSQLDB)层出不穷,功能都十分强大,也很普及。
然而围绕ORM的性能问题,也一直有很多批评的声音。其实ORM的架构对插入缓存技术是非常容易的,我做的很多项目和产品,但凡使用ORM,缓存都是标配,性能都非常好。而且我发现业界使用ORM的案例都忽视了缓存的运用,或者说没有意识到ORM缓存可以带来巨大的性能提升。
ORM缓存应用案例
我们去年有一个老产品重写的项目,这个产品有超过10年历史了,数据库的数据量很大,多个表都是上千万条记录,最大的表记录达到了9000万条,Web访问的请求数每天有300万左右。
老产品采用了传统的解决性能问题的方案:Web层采用了动态页面静态化技术,超过一定时间的文章生成静态HTML文件;对数据库进行分库分表,按年拆表。动态页面静态化和分库分表是应对大访问量和大数据量的常规手段,本身也有效。但它的缺点也很多,比方说增加了代码复杂度和维护难度,跨库运算的困难等等,这个产品的代码维护历来非常困难,导致bug很多。
进行产品重写的时候,我们放弃了动态页面静态化,采用了纯动态网页;放弃了分库分表,直接操作千万级,乃至近亿条记录的大表进行SQL查询;也没有采取读写分离技术,全部查询都是在单台主数据库上进行;数据库访问全部使用ActiveRecord,进行了大量的ORM缓存。上线以后的效果非常好:单台MySQL数据库服务器CPU的IO Wait低于5%;用单台1U服务器2颗4核至强CPU已经可以轻松支持每天350万动态请求量;最重要的是,插入缓存并不需要代码增加多少复杂度,可维护性非常好。
总之,采用ORM缓存是Web应用提升性能一种有效的思路,这种思路和传统的提升性能的解决方案有很大的不同,但它在很多应用场景(包括高度动态化的SNS类型应用)非常有效,而且不会显著增加代码复杂度,所以这也是我自己一直偏爱的方式。因此我一直很想写篇文章,结合示例代码介绍ORM缓存的编程技巧。
今年春节前后,我开发自己的个人网站项目,有意识的大量使用了ORM缓存技巧。对一个没多少访问量的个人站点来说,有些过度设计了,但我也想借这个机会把常用的ORM缓存设计模式写成示例代码,提供给大家参考。我的个人网站源代码是开源的,托管在github上:robbin_site
ORM缓存的基本理念
我在2007年的时候写过一篇文章,分析ORM缓存的理念:ORM对象缓存探讨 ,所以这篇文章不展开详谈了,总结来说,ORM缓存的基本理念是:
- 以减少数据库服务器磁盘IO为最终目的,而不是减少发送到数据库的SQL条数。实际上使用ORM,会显著增加SQL条数,有时候会成倍增加SQL。
- 数据库schema设计的取向是尽量设计 细颗粒度 的表,表和表之间用外键关联,颗粒度越细,缓存对象的单位越小,缓存的应用场景越广泛
- 尽量避免多表关联查询,尽量拆成多个表单独的主键查询,尽量多制造 n + 1 条查询,不要害怕“臭名昭著”的 n + 1 问题,实际上 n + 1 才能有效利用ORM缓存
利用表关联实现透明的对象缓存
在设计数据库的schema的时候,设计多个细颗粒度的表,用外键关联起来。当通过ORM访问关联对象的时候,ORM框架会将关联对象的访问转化成用主键查询关联表,发送 n + 1条SQL。而基于主键的查询可以直接利用对象缓存。
我们自己开发了一个基于ActiveRecord封装的对象缓存框架:second_level_cache ,从这个ruby插件的名称就可以看出,实现借鉴了Hibernate的二级缓存实现。这个对象缓存的配置和使用,可以看我写的ActiveRecord对象缓存配置 。
下面用一个实际例子来演示一下对象缓存起到的作用:访问我个人站点的首页。 这个页面的数据需要读取三张表:blogs表获取文章信息,blog_contents表获取文章内容,accounts表获取作者信息。三张表的model定义片段如下,完整代码请看models :
class Account < ActiveRecord::Base
acts_as_cached
has_many :blogs
end
class Blog < ActiveRecord::Base
acts_as_cached
belongs_to :blog_content, :dependent => :destroy
belongs_to :account, :counter_cache => true
end
class BlogContent < ActiveRecord::Base
acts_as_cached
end
传统的做法是发送一条三表关联的查询语句,类似这样的:
SELECT blogs.*, blog_contents.content, account.name
FROM blogs
LEFT JOIN blog_contents ON blogs.blog_content_id = blog_contents.id
LEFT JOIN accounts ON blogs.account_id = account.id
往往单条SQL语句就搞定了,但是复杂SQL的带来的表扫描范围可能比较大,造成的数据库服务器磁盘IO会高很多,数据库实际IO负载往往无法得到有效缓解。
我的做法如下,完整代码请看home.rb :
@blogs = Blog.order(‘id DESC’).page(params[:page])
这是一条分页查询,实际发送的SQL如下:
SELECT * FROM blogs ORDER BY id DESC LIMIT 20
转成了单表查询,磁盘IO会小很多。至于文章内容,则是通过blog.content的对象访问获得的,由于首页抓取20篇文章,所以实际上会多出来20条主键查询SQL访问blog_contents表。就像下面这样:
DEBUG - BlogContent Load (0.3ms) SELECTblog_contents
. FROMblog_contents
WHEREblog_contents
.id
= 29 LIMIT 1
DEBUG - BlogContent Load (0.2ms) SELECTblog_contents
. FROMblog_contents
WHEREblog_contents
.id
= 28 LIMIT 1
DEBUG - BlogContent Load (1.3ms) SELECTblog_contents
. FROMblog_contents
WHEREblog_contents
.id
= 27 LIMIT 1
……
DEBUG - BlogContent Load (0.9ms) SELECTblog_contents
. FROMblog_contents
WHEREblog_contents
.id
= 10 LIMIT 1
但是主键查询SQL不会造成表的扫描,而且往往已经被数据库buffer缓存,所以基本不会发生数据库服务器的磁盘IO,因而总体的数据库IO负载会远远小于前者的多表联合查询。特别是当使用对象缓存之后,会缓存所有主键查询语句,这20条SQL语句往往并不会全部发生,特别是热点数据,缓存命中率很高:
DEBUG - Cache read: robbin/blog/29/1
DEBUG - Cache read: robbin/account/1/0
DEBUG - Cache read: robbin/blogcontent/29/0
DEBUG - Cache read: robbin/account/1/0
DEBUG - Cache read: robbin/blog/28/1
……
DEBUG - Cache read: robbin/blogcontent/11/0
DEBUG - Cache read: robbin/account/1/0
DEBUG - Cache read: robbin/blog/10/1
DEBUG - Cache read: robbin/blogcontent/10/0
DEBUG - Cache read: robbin/account/1/0
拆分n+1条查询的方式,看起来似乎非常违反大家的直觉,但实际上这是真理,我实践经验证明:数据库服务器的瓶颈往往是磁盘IO,而不是SQL并发数量。因此 拆分n+1条查询本质上是以增加n条SQL语句为代价,简化复杂SQL,换取数据库服务器磁盘IO的降低 当然这样做以后,对于ORM来说,有额外的好处,就是可以高效的使用缓存了。
按照column拆表实现细粒度对象缓存
数据库的瓶颈往往在磁盘IO上,所以应该尽量避免对大表的扫描。传统的拆表是按照row去拆分,保持表的体积不会过大,但是缺点是造成应用代码复杂度很高;使用ORM缓存的办法,则是按照column进行拆表,原则一般是:
- 将大字段拆分出来,放在一个单独的表里面,表只有主键和大字段,外键放在主表当中
- 将不参与where条件和统计查询的字段拆分出来,放在独立的表中,外键放在主表当中
按照column拆表本质上是一个去关系化的过程。主表只保留参与关系运算的字段,将非关系型的字段剥离到关联表当中,关联表仅允许主键查询,以Key-Value DB的方式来访问。因此这种缓存设计模式本质上是一种SQLDB和NoSQLDB的混合架构设计
下面看一个实际的例子:文章的内容content字段是一个大字段,该字段不能放在blogs表中,否则会造成blogs表过大,表扫描造成较多的磁盘IO。我实际做法是创建blog_contents表,保存content字段,schema简化定义如下:
CREATE TABLEblogs
(
id
int(11) NOT NULL AUTO_INCREMENT,
title
varchar(255) NOT NULL,
blog_content_id
int(11) NOT NULL,
content_updated_at
datetime DEFAULT NULL,
PRIMARY KEY (id
),
);
CREATE TABLE blog_contents
(
id
int(11) NOT NULL AUTO_INCREMENT,
content
mediumtext NOT NULL,
PRIMARY KEY (id
)
);
blog_contents表只有content大字段,其外键保存到主表blogs的blog_content_id字段里面。
model定义和相关的封装如下:
class Blog < ActiveRecord::Base
acts_as_cached
delegate :content, :to => :blog_content, :allow_nil => true
def content=(value)
self.blog_content ||= BlogContent.new
self.blog_content.content = value
self.content_updated_at = Time.now
end
end
class BlogContent < ActiveRecord::Base
acts_as_cached
validates :content, :presence => true
end
在Blog类上定义了虚拟属性content,当访问blog.content的时候,实际上会发生一条主键查询的SQL语句,获取blog_content.content内容。由于BlogContent上面定义了对象缓存acts_as_cached,只要被访问过一次,content内容就会被缓存到memcached里面。
这种缓存技术实际会非常有效,因为: 只要缓存足够大,所有文章内容可以全部被加载到缓存当中,无论文章内容表有多么大,你都不需要再访问数据库了 更进一步的是: 这张大表你永远都只需要通过主键进行访问,绝无可能出现表扫描的状况 为何当数据量大到9000万条记录以后,我们的系统仍然能够保持良好的性能,秘密就在于此。
还有一点非常重要: 使用以上两种对象缓存的设计模式,你除了需要添加一条缓存声明语句acts_as_cached以外,不需要显式编写一行代码 有效利用缓存的代价如此之低,何乐而不为呢?
以上两种缓存设计模式都不需要显式编写缓存代码,以下的缓存设计模式则需要编写少量的缓存代码,不过代码的增加量非常少。
写一致性缓存
写一致性缓存,叫做write-through cache,是一个CPU Cache借鉴过来的概念,意思是说,当数据库记录被修改以后,同时更新缓存,不必进行额外的缓存过期处理操作。但在应用系统中,我们需要一点技巧来实现写一致性缓存。来看一个例子:
我的网站文章原文是markdown格式的,当页面显示的时候,需要转换成html的页面,这个转换过程本身是非常消耗CPU的,我使用的是Github的markdown的库。Github为了提高性能,用C写了转换库,但如果是非常大的文章,仍然是一个耗时的过程,Ruby应用服务器的负载就会比较高。
我的解决办法是缓存markdown原文转换好的html页面的内容,这样当再次访问该页面的时候,就不必再次转换了,直接从缓存当中取出已经缓存好的页面内容即可,极大提升了系统性能。我的网站文章最终页的代码执行时间开销往往小于10ms,就是这个原因。代码如下:
def md_content # cached markdown format blog content
APP_CACHE.fetch(content_cache_key) { GitHub::Markdown.to_html(content, :gfm) }
end
这里存在一个如何进行缓存过期的问题,当文章内容被修改以后,应该更新缓存内容,让老的缓存过期,否则就会出现数据不一致的现象。进行缓存过期处理是比较麻烦的,我们可以利用一个技巧来实现自动缓存过期:
def content_cache_key
“#{CACHE_PREFIX}/blog_content/#{self.id}/#{content_updated_at.to_i}”
end
当构造缓存对象的key的时候,我用文章内容被更新的时间来构造key值,这个文章内容更新时间用的是blogs表的content_updated_at字段,当文章被更新的时候,blogs表会进行update,更新该字段。因此每当文章内容被更新,缓存的页面内容的key就会改变,应用程序下次访问文章页面的时候,缓存就会失效,于是重新调用GitHub::Markdown.to_html(content, :gfm)生成新的页面内容。 而老的页面缓存内容再也不会被应用程序存取,根据memcached的LRU算法,当缓存填满之后,将被优先剔除。
除了文章内容缓存之外,文章的评论内容转换成html以后也使用了这种缓存设计模式。具体可以看相应的源代码:blog_comment.rb
片段缓存和过期处理
Web应用当中有大量的并非实时更新的数据,这些数据都可以使用缓存,避免每次存取的时候都进行数据库查询和运算。这种片段缓存的应用场景很多,例如:
- 展示网站的Tag分类统计(只要没有更新文章分类,或者发布新文章,缓存一直有效)
- 输出网站RSS(只要没有发新文章,缓存一直有效)
- 网站右侧栏(如果没有新的评论或者发布新文章,则在一段时间例如一天内基本不需要更新)
以上应用场景都可以使用缓存,代码示例:def self.cached_tag_cloud
APP_CACHE.fetch(“#{CACHE_PREFIX}/blog_tags/tag_cloud”) do
self.tag_counts.sort_by(&:count).reverse
end
end
对全站文章的Tag云进行查询,对查询结果进行缓存<% cache(“#{CACHE_PREFIX}/layout/right”, :expires_in => 1.day) do %>
<div class=”tag”>
<% Blog.cached_tag_cloud.select {|t| t.count > 2}.each do |tag| %>
<%= link_to “#{tag.name}<span>#{tag.count}</span>”.html_safe, url(:blog, :tag, :name => tag.name) %>
<% end %>
</div>
……
<% end %>
对全站右侧栏页面进行缓存,过期时间是1天。
缓存的过期处理往往是比较麻烦的事情,但在ORM框架当中,我们可以利用model对象的回调,很容易实现缓存过期处理。我们的缓存都是和文章,以及评论相关的,所以可以直接注册Blog类和BlogComment类的回调接口,声明当对象被保存或者删除的时候调用删除方法:
class Blog < ActiveRecord::Base
acts_as_cached
after_save :clean_cache
before_destroy :clean_cache
def clean_cache
APP_CACHE.delete(“#{CACHE_PREFIX}/blog_tags/tag_cloud”) # clean tag_cloud
APP_CACHE.delete(“#{CACHE_PREFIX}/rss/all”) # clean rss cache
APP_CACHE.delete(“#{CACHE_PREFIX}/layout/right”) # clean layout right column cache in _right.erb
end
end
class BlogComment < ActiveRecord::Base
acts_as_cached
after_save :clean_cache
before_destroy :clean_cache
def clean_cache
APP_CACHE.delete(“#{CACHE_PREFIX}/layout/right”) # clean layout right column cache in _right.erb
end
end
在Blog对象的after_save和before_destroy上注册clean_cache方法,当文章被修改或者删除的时候,删除以上缓存内容。总之,可以利用ORM对象的回调接口进行缓存过期处理,而不需要到处写缓存清理代码。
对象写入缓存
我们通常说到缓存,总是认为缓存是提升应用读取性能的,其实缓存也可以有效的提升应用的写入性能。我们看一个常见的应用场景:记录文章点击次数这个功能。
文章点击次数需要每次访问文章页面的时候,都要更新文章的点击次数字段view_count,然后文章必须实时显示文章的点击次数,因此常见的读缓存模式完全无效了。每次访问都必须更新数据库,当访问量很大以后数据库是吃不消的,因此我们必须同时做到两点:
- 每次文章页面被访问,都要实时更新文章的点击次数,并且显示出来
- 不能每次文章页面被访问,都更新数据库,否则数据库吃不消
对付这种应用场景,我们可以利用对象缓存的不一致,来实现对象写入缓存。原理就是每次页面展示的时候,只更新缓存中的对象,页面显示的时候优先读取缓存,但是不更新数据库,让缓存保持不一致,积累到n次,直接更新一次数据库,但绕过缓存过期操作。具体的做法可以参考blog.rb :# blog viewer hit counter
def increment_view_count
increment(:view_count) # add view_count += 1
write_second_level_cache # update cache per hit, but do not touch db
self.class.update_all({:view_count => view_count}, :id => id) if view_count % 10 == 0# update db per 10 hits
end
increment(:view_count)增加view_count计数,关键代码是第2行write_second_level_cache,更新view_count之后直接写入缓存,但不更新数据库。累计10次点击,再更新一次数据库相应的字段。另外还要注意,如果blog对象不是通过主键查询,而是通过查询语句构造的,要优先读取一次缓存,保证页面点击次数的显示一致性,因此 _blog.erb 这个页面模版文件开头有这样一段代码:<%
read view_count from model cache if model has been cached.
view_count = blog.view_count
if b = Blog.read_second_level_cache(blog.id)
view_count = b.view_count
end
%>
采用对象写入缓存的设计模式,就可以非常容易的实现写入操作的缓存,在这个例子当中,我们仅仅增加了一行缓存写入代码,而这个时间开销大约是1ms,就可以实现文章实时点击计数功能,是不是非常简单和巧妙?实际上我们也可以使用这种设计模式实现很多数据库写入的缓存功能。
常用的ORM缓存设计模式就是以上的几种,本质上都是非常简单的编程技巧,代码的增加量和复杂度也非常低,只需要很少的代码就可以实现,但是在实际应用当中,特别是当数据量很庞大,访问量很高的时候,可以发挥惊人的效果。我们实际的系统当中,缓存命中次数:SQL查询语句,一般都是5:1左右,即每次向数据库查询一条SQL,都会在缓存当中命中5次,数据主要都是从缓存当中得到,而非来自于数据库了。
其他缓存的使用技巧
还有一些并非ORM特有的缓存设计模式,但是在Web应用当中也比较常见,简单提及一下:
用数据库来实现的缓存
在我这个网站当中,每篇文章都标记了若干tag,而tag关联关系都是保存到数据库里面的,如果每次显示文章,都需要额外查询关联表获取tag,显然会非常消耗数据库。在我使用的acts-as-taggable-on插件中,它在blogs表当中添加了一个cached_tag_list字段,保存了该文章标记的tag。当文章被修改的时候,会自动相应更新该字段,避免了每次显示文章的时候都需要去查询关联表的开销。
HTTP客户端缓存
基于资源协议实现的HTTP客户端缓存也是一种非常有效的缓存设计模式,我在2009年写过一篇文章详细的讲解了:基于资源的HTTP Cache的实现介绍 ,所以这里就不再复述了。
用缓存实现计数器功能
这种设计模式有点类似于对象写入缓存,利用缓存写入的低开销来实现高性能计数器。举一个例子:用户登录为了避免遭遇密码暴力破解,我限定了每小时每IP只能尝试登录5次,如果超过5次,拒绝该IP再次尝试登录。代码实现很简单,如下:
post :login, :map => ‘/login’ do
login_tries = APP_CACHE.read(“#{CACHE_PREFIX}/login_counter/#{request.ip}”)
halt 403 if login_tries && login_tries.to_i > 5 # reject ip if login tries is over 5 times
@account = Account.new(params[:account])
if login_account = Account.authenticate(@account.email, @account.password)
session[:account_id] = login_account.id
redirect url(:index)
else
# retry 5 times per one hour
APP_CACHE.increment("#{CACHE_PREFIX}/login_counter/#{request.ip}", 1, :expires_in => 1.hour)
render 'home/login'
end
end
等用户POST提交登录信息之后,先从缓存当中取该IP尝试登录次数,如果大于5次,直接拒绝掉;如果不足5次,而且登录失败,计数加1,显示再次尝试登录页面。
以上相关代码可以从这里获取:robbin_site
安全存储密码:Hashing 还是加密?
一篇很不错的进行加密的文章!
from: http://www.oschina.net/news/52976/hashing-or-encrypt
对于网站来说, 再没有什么比用户信息泄露更让人尴尬的了。 尤其是当存有用户密码的文件如果被黑客获取, 对网站的安全和用户的信心来说都是巨大的打击。 如最近的Ebay泄密事件和小米的用户数据泄露事件。 保证用户信息安全首先需要正确理解对于用户密码的安全控制和保护。 这里OWASP的主席Michael Coates最近的一篇关于一些基本概念的介绍能够帮助开发人员更好的理解现代Hashing算法和加密对于用户密码保护的作用。 安全牛编译如下:
在过去几个月, 我们看到了一些严重的数据泄露事件, Ebay和Adobe的数据泄露事件影响了几百万用户。 Snapchat也遭受到了数据泄露事件的影响。 每一次密码泄露事件后, 人们都会问同一个问题, 这些密码的存储是不是安全? 不幸的是, 这个看上去简单的问题其实并不好回答。
尽管在很多情况下, Hashing和加密都能够满足安全存储的需要, 对于在线应用而言, 很多情况下, 对于用户密码的安全存储往往只有一种正确的方案。 Hashing.是通过一个不可逆的杂凑函数计算出一个Hash值, 而通过这个值无法逆向计算出输入值(比如用户密码)。 对称加密则是采用密钥进行加密计算, 这是一种可逆的运算。 任何人如果有了密钥, 就能够解密出原始明文。
下表是Hashing和对称加密的对比
显示第 1 至 6 项结果,共 6 项
当在线应用收到一个用户名和一个密码后, 就以密码为输入到杂凑函数中去得出一个Hash值, 然后用这个Hash值与数据库中存储的该用户的密码Hash值做比较, 如果两个Hash值相同, 就可以认为用户提供了有效的用户名和密码。 采用Hashing的好处是, 应用不需要存储用户的明文密码, 只需要存储Hash值。
在线应用如何利用密码的Hash值来认证用户
下图就是关于采用Hashing方式的简单描述:
那么, 所有杂凑算法都能用吗? 不是的, 事实上, 杂凑算法中不同的算法的差别很大, 并不是所有的杂凑算法都适合存储密码。
说起来可能有点出人预料, 早期的杂凑算法速度过快, 黑客们尽管不能通过Hash值逆向计算出原输入值, 但是黑客们可以通过暴力破解的方式遍历所有可能的密码组合来尝试能够能够“碰撞”到用户密码的Hash值。 为了避免这种威胁, 现代的杂凑算法能够通过多重迭代, 使得在每次Hash计算时产生一些延时, 对单次Hash计算, 这样的延时基本没有任何影响, 而对于黑客的暴力破解来说, 几百万次计算的延时能够被放大几百年, 这样到使得暴力破解基本不现实的地步。
在Hashing中, 最好采用针对每个用户的盐化方式, 通过对用户密码添加一个随机字符串(随机字符串可以是显式存储), 这样可以相同的密码产生相同的Hash值, 这样, 攻击者可以下载一个巨大的存有事先计算好Hash值的查找表, 也叫做彩虹表。 通过Hash值, 反向查找对应的输入值。
而通过下面两个表格可以看出, 通过对不同用户进行不同的盐化, 同样的密码就会出现不同的Hash值, 这样使得攻击者利用彩虹表进行攻击变得困难。
没有盐化
盐化后
类似于账户锁定的机制对于密码存储的模式有什么影响吗?
简单的回答, 就是, 没有影响。 对密码的安全存储是为了提供在密码文件被盗取后的防护。 黑客对于密码Hash的攻击是一种离线攻击。 也就是说, 密码文件已经被盗取, 黑客可以利用自己的计算机通过尝试不同的密码来找出密码。 由于是离线攻击, 账号锁定或者验证码之类的安全机制已经没有作用了。 这些机制只有在针对网站服务器的在线登录页面攻击时才会起作用。
对于密码存储, 采用对称加密而不是Hashing的风险在哪里?
对称加密的设计就是一个可逆的运算, 这意味着在线应用必须能够访问到密钥, 并且在每次密码验证时都要使用。 如果加密后的密码被窃取的话, 黑客需要获取对称加密的密钥, 而一旦密钥被破解出来, 不管是通过某种方式泄露出来, 或者一些弱的密钥被暴力方式破解出来, 所有的密码都会被黑客获得。
总结
对于密码的安全存储来说, 理解对称加密与Hashing的区别非常重要。 一些如PBKDF2, bcrypt以及scrypt等算法都采用的每用户盐化以及多重迭代的Hashing方式以安全存储密码。
互联网已经日益成为重要的用户信息存储的场所。 网站开发人员及网站老板们需要尽其所能地保证用户信息的安全。 了解如何利用现代的Hashing算法对用户密码进行基本的安全控制保护非常重要。
GistBox 代码管理工具
GistBox 提供一种漂亮的方式来组织代码片段。将你的库保存到云端进行备份,再也不用担心丢失。GistBox采用标准的HTML5技术构建。
GistBox使用GitHub的后端,但增加了自己的标签和搜索功能层。使用Github账号登陆Gistbox可以将你的代码直接同步进来,反过来,你在GB上的所有改动也都会同步到Github上;GistBox的结构设 计清晰,从左至右分别是主导航(新建Gist,Gists入口,收藏入口-Labels)、Gists列表(Public/Private)、具体代码 区,亲们可以用Label给代码加上各种分辨标签,方便分类整理,在检索代码时可以用顶部的搜索栏,输入关键词或Label可以更快的搜索到目标代码。
如何提高设计 API 的能力?
from: http://z.ihu.im/u/BZp(7
1、依赖倒置原则
依赖倒置的意思,是不要想象别人应该怎么用你的API、以及用你的API做什么;而是“倒”过来想,我的API对外提供了一个什么样的抽象、这个抽象是否够好、够简洁,有没有把不必要的细节暴露给用户。
比如说,之前我所在的公司做了个类似电子商城的东西,其它项目组做出产品,就放在这个商城里面销售。
那个商城团队就犯了干涉他人实现的大忌:他们非要了解其他项目组的逻辑,才知道如何才能把别人的产品上架销售。不要这样。
这种做法就导致两个团队耦合过重;且新产品和老产品逻辑无法兼容、甚至因为修改逻辑去迎合新产品,导致老产品销售逻辑出现问题——又因为怕老产品出现问题,于是不得不逼新产品去适应老产品的流程,而不管两者差别有多大。
这就导致,我们经常花1个月实现了一个新产品;为了把它上架却经常需要3个月甚至超过半年——并且,商城团队和其它项目团队都必须加班、修改逻辑,甚至经常因为商城方提出的诡异逻辑/需求而出现摩擦(这伙人傻的……我都忍不住发过几次火……
实在看不下去,我就给了一个方案,要求商城团队修改。
这个方案基于依赖倒置原则,要求商城对外提供一个商品的抽象;商品的定义是有名字有价格、通过销售转移所有权的逻辑实体。
如此一来,任何项目想要上架,只要给自己起个名字、定个价格、用html或某种富文本格式或商城项目组喜欢的任何格式提供一段商品介绍、最后再提供一个“所有权成功转移给xx用户”的回调接口即可。双方从此再不需要哪怕一个字的交流。
这个接口可以永远不修改哪怕一个字节,足以支持任何商品种类的交易。
(事实上,我们团队就是按这个要求写程序的。写完再和商城团队扯皮。这可以避免他们动辄增加的猪逻辑/需求影响到我们的内部结构:随便他们要什么,我们都可以弄个空逻辑或者不同商品规格搪塞过去)
但商城团队觉得一旦做成这样,他们以后就没事可干了,所以拒绝修改……
好在,后来他们集体辞职了。
——如果你的接口不能稳定,那么你一定违反了依赖倒置原则,或者是做了一个超烂的抽象。
——至于什么叫好抽象,请参考KISS原则
2、完整且最小原则
完整,就是接口的功能要完整,该有的功能必须有;最小,是接口功能没有冗余,不要接口A提供的功能,用一点外部逻辑再加上接口B或者接口B+接口C也能实现。
当然,这是一个比较理想化的指标。某些时候,为了易用性或者性能,是可以甚至必须做一些更简单、方便、易用,但加起来却不是最简的接口的。
但,这个完整且最小的接口必须找出来。它是一切的基础,也体现了开发者是否已经做出了一个清晰的抽象。
有了它,其它可以以后慢慢补;但如果没有这个基础,什么易用、简洁,都不过是扯淡。
我宁可用一个繁琐、难用、难理解,但可保证不变的接口,也不想碰做点简单的活计非常方便、易用,但根本没法实现稍难的需求、并且不能保证稳定的接口。
事实上,两者的差别就在于有没有好的抽象;而一旦有了好的抽象,哪怕想做得繁琐、难用、难理解,都不是容易的事。
总结:有了好的抽象,接口才可能稳定;然后找出完整且最小的接口,那么只要抽象不变,这个接口就绝对稳定;最后,在这个基础之上,做出易用、易理解的接口。
如何写出更好的Java代码
from: http://it.deepinmind.com/java/2014/05/21/better-java.html
Java是最流行的编程语言之一,但似乎并没有人喜欢使用它。好吧,实际上Java是一门还不错的编程语言,由于最近Java 8发布了,我决定来编辑一个如何能更好地使用Java的列表,这里面包括一些库,实践技巧以及工具。
这篇文章在GitHub上也有。你可以随时在上面贡献或者添加你自己的Java使用技巧或者最佳实践。
编码风格
结构体
* builder模式
- 依赖注入
- 避免null值
- 不可变
- 避免过多的工具类
格式
* 文档
- Stream
部署
- 框架
Maven
* 依赖收敛
- 持续集成
- Maven仓储
- 配置管理
库
遗失的特性
* Apache Commons
- Guava
- Gson
- Java Tuples
- Joda-Time
- Lombok
- Play framework
- SLF4J
jOOQ
测试
- jUnit 4
- jMock
- AssertJ
工具
IntelliJ IDEA
* Chronon
- JRebel
- 校验框架
- Eclipse Memory Analyzer
资源
- 书籍
- 播客
编码风格
传统的Java编码方式是非常啰嗦的企业级JavaBean的风格。新的风格更简洁准确,对眼睛也更好。
结构体
我们这些码农干的最简单的事情就是传递数据了。传统的方式就是定义一个JavaBean:
public class DataHolder {
private String data;
public DataHolder() {
}
public void setData(String data) {
this.data = data;
}
public String getData() {
return this.data;
}
}
这不仅拖沓而且浪费。尽管你的IDE可以自动地生成这个,但这还是浪费。因此,不要这么写。
相反的,我更喜欢C的结构体的风格,写出来的类只是包装数据:
public class DataHolder {
public final String data;
public DataHolder(String data) {
this.data = data;
}
}
这样写减少了一半的代码。不仅如此,除非你继承它,不然这个类是不可变的,由于它是不可变的,因此推断它的值就简单多了。
如果你存储的是Map或者List这些可以容易被修改的数据,你可以使用ImmutableMap或者ImmutableList,这个在不可变性这节中会有讨论。
#### Builder模式
如果你有一个相对复杂的对象,可以考虑下Builder模式。
你在对象里边创建一个子类,用来构造你的这个对象。它使用的是可修改的状态,但一旦你调用了build方法,它会生成一个不可变对象。
想象一下我们有一个非常复杂的对象DataHolder。它的构造器看起来应该是这样的:
public class ComplicatedDataHolder {
public final String data;
public final int num;
// lots more fields and a constructor
public class Builder {
private String data;
private int num;
public Builder data(String data) {
this.data = data;
return this;
}
public Builder num(int num) {
this.num = num;
return this;
}
public ComplicatedDataHolder build() {
return new ComplicatedDataHolder(data, num); // etc
}
}
}
现在你可以使用它了:
final ComplicatedDataHolder cdh = new ComplicatedDataHolder.Builder()
.data(“set this”)
.num(523)
.build();
关于Builder的使用这里还有些更好的例子,我这里举的例子只是想让你大概感受一下。当然这会产生许多我们希望避免的样板代码,不过好处就是你有了一个不可变对象以及一个连贯接口。
#### 依赖注入
这更像是一个软件工程的章节而不是Java的,写出可测的软件的一个最佳方式就是使用依赖注入(Dependency injection,DI)。由于Java强烈鼓励使用面向对象设计 ,因此想写出可测性强的软件,你需要使用DI。
在Java中,这个通常都是用Spring框架来完成的。它有一个基于XML配置的绑定方式,并且仍然相当流行。重要的一点是你不要因为它的基于XML的配置格式而过度使用它了。在XML中应该没有任何的逻辑和控制结构。它只应该是依赖注入。
还有一个不错的方式是使用Dagger库以及Google的Guice。它们并没有使用Spring的XML配置文件的格式,而是将注入的逻辑放到了注解和代码里。
#### 避免null值
如果有可能的话尽量避免使用null值。你可以返回一个空的集合,但不要返回null集合。如果你准备使用null的话,考虑一下@Nullable注解。IntelliJ IDEA对于@Nullable注解有内建的支持。
如果你使用的是Java 8的话,可以考虑下新的Optional类型。如果一个值可能存在也可能不存在,把它封装到Optional类里面,就像这样:
public class FooWidget {
private final String data;
private final Optional<Bar> bar;
public FooWidget(String data) {
this(data, Optional.empty());
}
public FooWidget(String data, Optional<Bar> bar) {
this.data = data;
this.bar = bar;
}
public Optional<Bar> getBar() {
return bar;
}
}
现在问题就清楚了,data是不会为null的,而bar可能为空。Optional类有一些像isPresent这样的方法,这让它感觉跟检查null没什么区别。不过有了它你可以写出这样的语句:
final Optional<FooWidget> fooWidget = maybeGetFooWidget();
final Baz baz = fooWidget.flatMap(FooWidget::getBar)
.flatMap(BarWidget::getBaz)
.orElse(defaultBaz);
这比使用if来检查null好多了。唯一的缺点就是标准类库中对Optional的支持并不是很好,因此你还是需要对null进行检查的。
#### 不可变
变量,类,集合,这些都应该是不可变的,除非你有更好的理由它们的确需要进行修改。
变量可以通过final来设置成不可变的:
final FooWidget fooWidget;
if (condition()) {
fooWidget = getWidget();
} else {
try {
fooWidget = cachedFooWidget.get();
} catch (CachingException e) {
log.error(“Couldn’t get cached value”, e);
throw e;
}
}
// fooWidget is guaranteed to be set here
现在你可以确认fooWidget不会不小心被重新赋值了。final关键字可以和if/else块以及try/catch块配合使用。当然了,如果fooWidget对象不是不可变的,你也可以很容易地对它进行修改。
有可能的话,集合都应该尽量使用Guava的ImmutableMap, ImmutableList, or ImmutableSet类。这些类都有自己的构造器,你可以动态的创建它们,然后将它们设置成不可变的,。
要使一个类不可变,你可以将它的字段声明成不可变的(设置成final)。你也可以把类自身也设置成final的这样它就不能被扩展并且修改了,当然这是可选的。
#### 避免大量的工具类
如果你发现自己添加了许多方法到一个Util类里,你要注意了。
public class MiscUtil {
public static String frobnicateString(String base, int times) {
// … etc
}
public static void throwIfCondition(boolean condition, String msg) {
// … etc
}
}
这些类乍一看挺吸引人的,因为它们里面的这些方法不属于任何一个地方。因此你以代码重用之名将它们全都扔到这里了。
这么解决问题结果更糟。把它们放回它们原本属于的地方吧,如果你确实有一些类似的常用方法,考虑下Java 8里接口的默认方法。并且由于它们是接口,你可以实现多个方法。
public interface Thrower {
public void throwIfCondition(boolean condition, String msg) {
// …
}
public void throwAorB(Throwable a, Throwable b, boolean throwA) {
// …
}
}
这样需要使用它的类只需简单的实现下这个接口就可以了。
#### 格式
格式远比许多程序员相像的要重要的多。一致的格式说明你关注自己的代码或者对别人有所帮助?是的。不过你先不要着急为了让代码整齐点而浪费一整天的时间在那给if块加空格了。
如果你确实需要一份代码格式规范,我强烈推荐Google的Java风格指南。这份指南最精彩的部分就是编程实践这节了。非常值得一读。
#### 文档
面向用户的代码编写下文档还是很重要的。这意味着你需要提供一些使用的示例,同时你的变量方法和类名都应该有适当的描述信息。
结论就是不要给不需要文档的地方添加文档。如果对于某个参数你没什么可说的,或者它已经非常明显了,别写文档了。模板化的文档比没有文档更糟糕,因为它欺骗了你的用户,让他觉得这里有文档。
#### 流
Java 8有一个漂亮的流和lambda表达式的语法。你的代码可以这么写:
final List<String> filtered = list.stream()
.filter(s -> s.startsWith(“s”))
.map(s -> s.toUpperCase());
而不是这样:
final List<String> filtered = Lists.newArrayList();
for (String str : list) {
if (str.startsWith(“s”) {
filtered.add(str.toUpperCase());
}
}
这样你能写出更连贯的代码,可读性也更强。
### 部署
正确地部署Java程序还是需要点技巧的。现在部署Java代码的主流方式有两种 :使用框架或者使用自家摸索出来的解决方案,当然那样更灵活。
#### 框架
由于部署Java程序并不容易,因此才有了各种框架来用于部署。最好的两个是Dropwizard以及Spring Boot。Play Framework也可以算是一个部署框架。
这些框架都试图降低部署程序的门槛。如果你是一个Java的新手或者你需要快速把事情搞定的话,那么框架就派上用场了。单个jar的部署当然会比复杂的WAR或者EAR部署要更容易一些。
然而,这些框架的灵活性不够,并且相当顽固,因此如果这些框架的开发人员给出的方式不太适合你的项目的话,你只能自己进行配置了。
#### Maven
备选方案:Gradle。
Maven仍然是编译,打包,运行测试的标准化工具。还有其它一些选择,比如Gradle,不过它们的采用程度远不Maven。如果你之前没用过Maven,你可以看下这个Maven的使用示例。
我喜欢用一个根POM文件来包含所有的外部依赖。它看起来就像是这样。这个根POM文件只有一个外部依赖,不过如果你的产品很大的话,你可能会有很多依赖。你的根POM文件自己就应该是一个项目:它有版本控制,并且和其它的Java项目一样进行发布。
如果你觉得为每个外部依赖的修改都给POM文件打个标签(tag)有点太浪费了,那是你还没有经历过花了一个星期的时间来跟踪项目依赖错误的问题。
你的所有Maven工程都应该包含你的主POM文件以及所有的版本信息。这样的话,你能知道公司项目的每个外部依赖所选择的版本,以及所有正确的Maven插件。如果你需要引入一个外部依赖的话,大概是这样的:
<dependencies>
<dependency>
<groupId>org.third.party</groupId>
<artifactId>some-artifact</artifactId>
</dependency>
</dependencies>
如果你需要进行内部依赖的话,应该在项目的段中单独进行维护。不然的话,主POM文件的版本号就要疯涨了。
#### 依赖收敛
Java的一个最好的地方就是有大量的第三方库,它们无所不能。几乎每个API或者工具都有相应的Java SDK,并且可以很容易地引入到Maven中来。
所有的这些Java库自身可能又会依赖一些特定的版本的其它类库。如果你引入了大量的库,可能会出现版本冲突 ,比如说像这样:
Foo library depends on Bar library v1.0
Widget library depends on Bar library v0.9
你的工程应该引入哪个版本?
有了Maven的依赖收敛的插件后,如果你的依赖版本不一致的话,编译的时候就会报错。那么你有两种解决冲突的方案:
在dependencyManagement区中显式地选择某个版本的bar。 Foo或者Widget都不要依赖Bar。
到底选择哪种方案取决你的具体情况: 如果你想要跟踪某个工程的版本,不依赖它是最好的。另一方面,如果你想要明确一点,你可以自己选择一个版本,不过这样的话,如果更新了其它的依赖,也得同步地修改它。
### 持续集成
很明显你需要某种持续集成的服务器来不断地编译你的SNAPSHOT版本,或者对Git分支进行构建。
Jenkins和Travis-CI是你的不二选择。
代码覆盖率也很重要,Cobertura有一个不错的Maven插件,并且对CI支持的也不错。当然还有其它的代码覆盖的工具,不过我用的是Cobertura。
### Maven库
你需要一个地方来存储你编译好的jar包,war包,以及EAR包,因此你需要一个代码仓库。
常见的选择是Artifactory或者Nexus。两个都能用,并且各有利弊。
你应该自己进行Artifactory/Nexus的安装并且将你的依赖做一份镜像。这样不会由于下载Maven 库的时候出错了导到编译中断。
#### 配置管理
那现在你的代码可以编译了,仓库也搭建起来了,你需要把你的代码带出开发环境,走向最终的发布了。别马虎了,因为自动化执行从长远来看,好处是大大的。
Chef,Puppet,和Ansible都是常见的选择。我自己也写了一个可选方案,Squadron。这个嘛,当然了,我自然是希望你们能下载下它的,因为它比其它那些要好用多了。
不管你用的是哪个工具,别忘了自动化部署就好。
### 库
可能Java最好的特性就是它拥有的这些库了。下面列出了一些库,应该绝大多数人都会用得上。
Java的标准库,曾经还是很不错的,但在现在看来它也遗漏掉了很多关键的特性。
#### Apache Commons
Apache Commons项目有许多有用的功能。
Commons Codec有许多有用的Base64或者16进制字符串的编解码的方法。别浪费时间自己又写一遍了。 Commons Lang是一个字符串操作,创建,字符集,以及许多工具方法的类库。
Commons IO,你想要的文件相关的方法都在这里了。它有FileUtils.copyDirectory,FileUtils.writeStringToFile, IOUtils.readLines,等等。
#### Guava
Guava是一个非常棒的库,它就是Java标准库”所缺失的那部分”。它有很多我喜欢的地方,很难一一赘述,不过我还是想试一下。 Cache,这是一个最简单的获取内存缓存的方式了,你可以用它来缓存网络访问,磁盘访问,或者几乎所有东西。你只需实现一个CacheBuilder,告诉Guava如何创建缓存就好了。
* 不可变集合。这里有许多类:ImmutableMap, ImmutableList,甚至还有ImmutableSortedMultiSet,如果这就是你想要的话。
我还喜欢用Guava的方式来新建可变集合:
// Instead of
final Map<String, Widget> map = new HashMap<String, Widget>();
// You can use
final Map<String, Widget> map = Maps.newHashMap();
有许多像Lists, Maps, Sets的静态类,他们都更简洁易懂一些。
如果你还在坚持使用Java 6或者7的话,你可以用下Collections2,它有一些诸如filter和transform的方法。没有Jvaa 8的stream的支持,你也可以用它们来写出连贯的代码。
Guava也有一些很简单的东西,比如Joiner,你可以用它来拼接字符串,还有一个类可以用来处理中断。
#### Gson
Google的Gson是一个简单高效的JSON解析库。它是这样工作的:
final Gson gson = new Gson();
final String json = gson.toJson(fooWidget);
final FooWidget newFooWidget = gson.fromJson(json, FooWidget.class);
真的很简单,使用它会感觉非常愉快。Gson的用户指南中有更多的示例。
#### Java Tuples
我对Java一个不爽的地方就是它的标准库中居然没有元组。幸运的是, Java tuples工程解决了这一问题。
它也很容易使用,并且真的很赞:
Pair<String, Integer> func(String input) {
// something…
return Pair.with(stringResult, intResult);
}
Joda-Time
Joda-Time是我用过的最好的时间库了。简直,直接,容易测试。你还想要什么?
这个库里我最喜欢的一个类就是Duration,因为我用它来告诉说我要等待多长时间,或者过多久我才进行重试。
Lombok
Lombok是一个非常有趣的库。它通过注释来减少了Java中的饱受诟病的样板代码(注:setter,getter之类的)。
想给你类中的变量增加setter, getter方法?太简单了:
public class Foo {
@Getter @Setter private int var;
}
现在你可以这么写了:
final Foo foo = new Foo();
foo.setVar(5);
这里还有更多的示例。我还没在生产代码中用过Lombok,不过我有点等不及了。
Play框架
在Java中实现REST风格的WEB服务有两大阵营:JAX-RS和其它。
JAX-RS是传统的方式。你使用像Jersey这样的东西来将注解和接口,实现组合到一起来实现WEB服务。这样做的好处就是,你可以通过一个接口就能很容易创建出一个调用的客户端来。
Play框架是在JVM上实现WEB服务的截然不同的一种方式:你有一个routes文件,然后你去实现routes中那些规则所引用到的类。它其实就是个完整的MVC框架,不过你可以只用它来实现REST服务。
它同时支持Java和Scala。它优先使用Scala这点可能有点令人沮丧,但是用Java进行开发的话也非常不错。
如果你习惯了Python里的Flask这类的微框架,那么你应该会对Spark感到很熟悉。有了Java 8它简直如虎添翼。
SLF4J
Java打印日志有许多不错的解决方案。我个人最喜欢的是SLF4J,因为它是可挺插拔的,并且可以同时混合不同的日志框架中输出的日志。有个奇怪的工程同时使用了java.util.logging, JCL, 和log4j?没问题,SLF4J就是为它而生的。
想入门的话,看下它的这个两页的手册就足够的了。
jOOQ
我不喜欢很重的ORM框架,因为我喜欢SQL。因此我写了许多的JDBC模板,但它们很难维护。jOOQ是个更不错的解决方案。
你可以在Java中以一种类型安全的方式来书写SQL语句:
// Typesafely execute the SQL statement directly with jOOQ
Result<Record3<String, String, String>> result =
create.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
.from(BOOK)
.join(AUTHOR)
.on(BOOK.AUTHOR_ID.equal(AUTHOR.ID))
.where(BOOK.PUBLISHED_IN.equal(1948))
.fetch();
将它以及DAO模式结合起来,你可以让数据库访问变得更简单。
测试
测试对软件来说至关重要。下面这些库能让测试变得更加容易。
jUnit 4
jUnit就不用介绍了。它是Java中单元测试的标准工具。
不过可能你还没有完全发挥jUnit的威力。jUnit还支持参数化测试,以及能让你少写很多样板代码的测试规则,还有能随机测试代码的Theory,以及Assumptions。
jMock
如果你已经完成了依赖注入,那么它回报你的时候来了:你可以mock出带副作用的代码(就像和REST服务器通信那样),并且仍然能对调用它的代码执行断言操作。
jMock是Java中标准的mock工具。它的使用方式是这样的:
public class FooWidgetTest {
private Mockery context = new Mockery();
@Test
public void basicTest() {
final FooWidgetDependency dep = context.mock(FooWidgetDependency.class);
context.checking(new Expectations() {
oneOf(dep).call(with(any(String.class)));
atLeast(0).of(dep).optionalCall();
});
final FooWidget foo = new FooWidget(dep);
Assert.assertTrue(foo.doThing());
context.assertIsSatisfied();
}
}
这段代码通过jMock设置了一个FooWidgetDependency ,然后添加了一些期望的操作。我们希望dep的call方法被调用一次而dep的optionalCall 方法会被调用0或更多次。
如果你反复的构造同样的FooWidgetDependency,你应该把它放到一个测试设备(Test Fixture)里,然后把assertIsSatisfied放到一个@After方法中。
AssertJ
你是不是用jUnit写过这些?
final List<String> result = some.testMethod();
assertEquals(4, result.size());
assertTrue(result.contains(“some result”));
assertTrue(result.contains(“some other result”));
assertFalse(result.contains(“shouldn’t be here”));
这些样板代码有点太聒噪了。AssertJ解决了这个问题。同样的代码可以变成这样:
assertThat(some.testMethod()).hasSize(4)
.contains(“some result”, “some other result”)
.doesNotContain(“shouldn’t be here”);
连贯接口让你的测试代码可读性更强了。代码如此,夫复何求?
工具
IntelliJ IDEA
备选方案: Eclipse and Netbeans
最好的Java IDE当然是 IntelliJ IDEA。它有许多很棒的特性,我们之所以还能忍受Java这些冗长的代码,它起了很大的作用。自动补全很棒,< a href=”http://i.imgur.com/92ztcCd.png“ target=”_blank”>代码检查也超赞,重构工具也非常实用。
免费的社区版对我来说已经足够了,不过在旗舰版中有许多不错的特性比如数据库工具,Srping框架的支持以及Chronon等。
Chronon
GDB 7中我最喜欢的特性就是调试的时候可以按时间进行遍历了。有了IntelliJ IDEA的Chronon插件后,这个也成为现实了。当然你得是旗舰版的。
你可以获取到变量的历史值,跳回前面执行的地方,获取方法的调用历史等等。第一次使用的话会感觉有点怪,但它能帮忙你调试一些很棘手的BUG。
JRebel
持续集成通常都是SaaS产品的一个目标。你想想如果你甚至都不需要等到编译完成就可以看到代码的更新?
这就是JRebel在做的事情。只要你把你的服务器挂到某个JRebel客户端上,代码一旦有改动你马上就能看到效果。当你想快速体验一个功能的话,这个的确能节省不少时间。
验证框架
Java的类型系统是相当弱的。它不能区分出普通字符串以及实际上是正则的字符串,也不能进行