0%

日常编程中,接触了很多种类的一致性,这里希望把一致性中基本原理与原则做一个总结。

归纳的说,一致性描述的问题是,在什么情况下参与者可以观察到对方的变化,并以怎样的规范才能保证对于临界区的处理可控。即参与者A与B,当A write v时,B何时可以read到v的变化; A write v 与B write v 谁发生在前。编程者须遵循何种规范才能保证对于v的修改是符合意愿的。

一致性从最根本上讲,只包含两个原则:1、可见性原则;2、原子性原则;只要参与者满足上述两种原则同时满足上述两种原则,即可实现一致性保证。

为何称之为原则?主要是因为,参与者都遵守这样的原则之后,才可以使整个系统达到预期的一致性。

上述描述过于抽象,我们拆解一下:

1、可见性原则:
即上述描述的,A write v时,B何时才能观察到。 举个例子, 当参与者是同一个进程中的多个线程时,可见性的表达常用volitale表达,即可见性原则总结为了happen before关系。happen-before则表明A write v happens-before B read v。
可见性保证了什么呢?看个例子

1
2
3
4
5
6
7
8
9
10
11
12
public class Visibility {
static volatile a=0;

public static void main(String[] args) {

//in thread 1
a = 1;

//in thread 2
System.out.printlin(a);
}
}

2、原子性原则:
如上所述,可见性是保证对于一个值v的一致性的最基本且直观的方式,当希望保证对于一组值v1, v2, v3…保证一致性时只满足可见性原则还不够,还需要满足原子性原则。即我们希望能够支持,A write v1, v2, v3…,全部完成后,B read v1,v2,v3…时才可以读到改变。

场景描述:问题的发生来源一个工作中的问题,简要描述是在一个多module的maven project中,job module依赖server module完成工作,在intelliJ中执行良好,但是用maven package打包的时候,就会报找不到server module中对应的类。

于是乎,楼主开始了问题的排查。

怀疑是maven的锅
首先,怀疑是本地maven问题,尝试了各种解决办法,如maven -U, 删除本地maven库中server module的相应版本等。

察觉到问题关节
一通忙碌之后,楼主注意到报错的细节,不是找不到jar包,也不是server module的某一个类找不到,而是无论引用server module中的哪个类,package的时候都会报找不到响应的类。于是开始怀疑是不是server module的jar包有什么与众不同。

对比问题jar与普通jar的不同之处
那么楼主便将server module的jar包unzip开,查看细节

有问题jar的结构
其中问题jar的包名是com.admin.server
jar–
--BOOT-INF
--META-INF
--org
--springframework
--*

普通jar结构
普通jar的包名是com.admin.job
jar–
--META-INF
--com
--admin
--job
\
.class

从中楼主比较出了不同,问题jar的主目录下并没有自己包名(com.admin.server)的目录,而普通的可依赖jar则有,那么是谁会修改这个jar的目录结构呢?

spring-boot接锅
我们仔细观察可以发现问题jar的主目录下多了一个BOOT-INF,于是把矛头指向了spring-boot,在观察一下server module的pom.xml,楼主感觉真実はいつも一つ (真相只有一个)



${project.artifactId}


org.springframework.boot
spring-boot-maven-plugin
1.4.0-RELEASE



repackage





从上述代码我们可以看出spring-boot做了个maven的plugin,在repackage时期的时候接手了打包过程。所以有理由怀疑这次问题是由于升级到了spring-boot 1.4.0-RELEASE引起的。

经过简单的搜索,便找到了stack overflow上得解答以及spring-boot自己的文档说明,spring-boot文档

然而intresting
找到了问题后,稍一兴奋,便有一个问题萦绕在楼主心头,久久不能散去,classloader究竟是怎样在jar文件中查找类的呢?

阅读了一会URLClassLoader的源码,感觉不得要领,于是开始了写测试用例调试URLClassLoader的过程。

将上述问题jar作为全路径输入,查找其中的类名,debug走起


@Test
public void test() throws MalformedURLException {
String pathname = “//target/admin-server-exec.jar”;
try {
Class<?> aClass = new URLClassLoader(new URL[]{FileURL.makeURL(pathname)}, null)
.loadClass(“com.admin.server.web.controller.AdminController.class”);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

不断跟踪深入,直到发现一处

esource getResource(String var1, boolean var2) {
if(this.metaIndex != null && !this.metaIndex.mayContain(var1)) {
return null;
} else {
try {
this.ensureOpen();
} catch (IOException var5) {
throw new InternalError(var5);
}

JarEntry var3 = this.jar.getJarEntry(var1);

以及getJarEntry的实现:

public ZipEntry getEntry(String name) {
if (name == null) {
throw new NullPointerException(“name”);
}
long jzentry = 0;
synchronized (this) {
ensureOpen();
jzentry = getEntry(jzfile, zc.getBytes(name), true);
if (jzentry != 0) {
ZipEntry ze = getZipEntry(name, jzentry);
freeEntry(jzfile, jzentry);
return ze;
}
}
return null;
}

那么一切豁然开朗,从jar中查找一个类,最后的实现便是查找待寻找的名字是否在zip的相应位置,由此可见,被spring-boot重新打包之后的jar中找不到相应的类非常合理。

Spring-Annotation的哲学是要有开关:

  1. 例如当你使用@ConfigurationProperties时,必须要搭配EnableConfigurationProperties

  2. 例如当你使用AutoConfiguration配置在META-INF中时,必须搭配使用@EnableAutoConfiguration

  3. 例如当在测试用例中要使用某些bean上的AOP方法生效(通过ASPECTJ实现的),则可以对改bean打上@EnableAspectJAutoProxy

从哪说起,先从MySQL的官方文档说起。

当MySQL 5.5在全世界范围内广泛部署与使用之后,架构师们都在翘首以盼,期待着MySQL 5.6的将临。MySQL 5.6在MySQL 5.5的基础上开发完成。

今年MySQL的会议上,会听到如下的主题:

  • 优化器全面提高性能
  • InnoDB提高事务吞吐
  • 全新的NoSQL memcached APIs
  • 查询和超大表分区优化
  • replication多方面提高
  • PERFORMANCE_SCHEMA 提供了更多的性能监控数据

注1:MySQL的实现分为两层,即服务器层(后文简称为server)和存储引擎层(storage engine)
注2:MySQL的索引遍历是在存储引擎层实现。

## 优化器全面提高性能

### 索引条件下推到存储引擎
将where子句中的处理更多的转移到存储引擎处理。相比取出完整行后再使用where子句评估,ICP(Index Condition Push-down)将这些where子句发送到存储引擎,这样存储引擎可以根据索引组(index tupples)来修剪结果集。从而,降低了对主索引的IO开销,降低了server与存储引擎之间的传输开销。这项特点在innodb,MyISAM,NDCluster引擎上都有实现。

ICP是一项优化,在MySQL从一个有索引的表取数据时生效。没有ICP,存储引擎将会根据索引来定位所需要的行,然后把这些行完整返回给server,在server出对这些行验证where子句(where子句部分,如果验证不通过,将会把这些行丢弃)。一旦启用了ICP,如果where子句中有一部分条件可以只使用了索引中的列,(MySQL)server将会把这一部分(只涉及索引部分的where子句)下推到存储引擎层。存储引擎将会根据索引中的数据来判定这些下推的where子句,只有(index中的数据)满足这些(下推的)条件时才会从主表中真正读取这些行。ICP可以减少存储引擎访问主表的次数,同时还可以减少MySQL Server访问存储引擎的次数。

当需要访问整个行的完整数据是,ICP优化对于range,ref,eq_ref和ref_or_null等访问方式有效。这种策略对于InnoDB和MyISAM类型的表都有效。(注意:在MySQL 5.6中的partitioned 表不支持ICP.)对于InnoDB表来说,ICP只对二级索引(非主键索引)有效。ICP的目标是检索整行扫描读的数量,以此减少IO操作。对于InnoDB聚合索引来说,整行记录已经被读入InnoDB的缓存中。在这种情况下,使用ICP起不到减少IO的作用。

为了看出这项优化是怎么工作的,首先考虑一下在没有ICP时,索引扫描是怎样进行的。

  1. 得到下一行:首先,读取索引中的下一个元组,然后使用该元组到主表中定位和读取整行数据。
  2. 验证where子句中(应用在该表上)的条件,根据验证结果决定接受或者拒绝改行。

当启用了ICP时,索引扫描的过程会是这样:

  1. 获取索引中的下一个元组(而不是获取整行数据)。
  2. 验证where子句中(应用在该表上)的条件中的一部分(可以用该索引判定的部分),如果条件不满足,继续验证索引中的下一个元组。
  3. 如果条件满足了,使用该索引元组来定位和读取完整行记录。
  4. 验证where字句中(应用在该表上)的剩余部分,根据验证结果接受或者拒绝改行。

当ICP被启用时,Explain中的Extra列中将展示”Using index condition”. 它不会显示”Index only”,因为当完整行记录必须被读取(不管是判断where条件还是返回select需要数据)时就不是只使用Index了。

假设,我们有一张表包含了人和他们的地址,并且该表拥有一个索引index(zipcode, lastname, firstname). 如果我们知道一个人的zipcode值,但是并不确定他的姓,我们可以这样查询:

1
2
3
4
SELECT * FROM people
WHERE zipcode='95054'
AND lastname LIKE '%etrunia%'
AND address LIKE '%Main Street%'

MySQL可以根据zipcode=’95054’使用索引扫描people表。而index中剩余的部分(lastname like ‘%etrunia%’)不能被用来限制需要扫描的行数,所以没有ICP,这条查询将必须取得所有满足zipcode=’95054’完整行记录。

如果启用了ICP,MySQL将会在读取完整行记录之前检查(lastname like ‘%etrunia%’)。这样就避免了对于不满足lastname条件的行的完整行记录扫描。

ICP默认打开;可以使用optimizer_switch系统变量来设置index_condition_pushdown参数。

### 多范围读(Multi-Range Read,MRR)
直到你有足够的SSD之前,从磁盘中顺序读都会比随机读要快。对于二级索引(非主键索引),硬盘上索引条目的顺序和完整行记录所在的硬盘块的顺序是不一致的。与其是通过一些列无序的读磁盘来获取完整的行记录,MRR扫描一个查询中的一个或多个索引范围,然后所得的索引元组按照相应的完整行数据所在的磁盘块排序。然后使用更大的顺序IO的请求读取这些磁盘块。这项优化对范围索引扫描以及在索引列上的equi-join有效。(考虑InnoDB外键。)对于所有的存储引擎都有效。

通过在二级索引上的范围扫描来读取数据,当主表很大并且没有在存储引擎的缓存中时,会造成对主表大量的随即磁盘读。通过磁盘块交换MMR优化,MySQL试图减少范围扫描造成的随机磁盘读取次数,主要通过首先只扫描索引,然后手机相关行的键(主键?)。随后这些键被排序,最后这些完整行记录按照主键的顺序被读取出来。MRR的主要目的是减少硬盘随机读,代之以更顺序的方式扫描基表。

MRR读优化提供了一下好处:

  1. MRR使得数据行可以被顺序读取而不是按照索引顺序随机读取。server获取了满足查询条件的索引元组,按照行数据ID(主键)的顺序排序,然后使用这些排序后的元组来顺序的获取数据。这使得获取数据更加的有效和低耗。
  2. MRR使得可以对需要访问索引并通过索引元组来访问完整行记录的请求进行批量处理。MRR遍历一系列的索引范围来获取满足条件的索引元组(类似于tcp拥塞控制中,把较小的包累计后一起发送)。不断的收集索引元组,当累积到一定量时,(这些元组)用来到主表中读取数据行。

在以下的场景中,应用MRR优化可能会带来利好

####Senario A: MRR可以被用于InnoDB和MyISAM表的索引范围查询和equi-join操作。

  1. 一部分的索引元组在缓存中累积。
  2. 缓存中的元组按照ID排序。
  3. 按照拍好序的顺序访问主表中的数据行。

####Senario B: MRR可以被用于NDB表,用于多范围索引扫描以及通过一个属性进行equi-join

  1. 一部分的多个范围索引的元组,或是单个索引范围的元组,当查询提交时,在中心节点的缓存中累积。
  2. 这些范围被发送到执行节点上用以访问数据行。
  3. 这些被访问的行被打成协议包,回传给中央节点。
  4. (中心节点)接收到包含数据行的协议包后,把它们放在缓存中。
  5. 数据被从缓存中读取。

当MRR被使用时,当MRR被使用时,EXPLAIN的Extra列将展示

1
Using MRR

当查询结果不需要整行数据时,InnoDB和MyISAM不会使用MRR。原因是,有可能索引中信息足够用来构造查询结果(即覆盖索引),在这样的情况下,MRR没有任何增益。

栗子

在下面的栗子中,MRR会被使用(假设有一个索引INDEX(key_part1, key_part2))

1
2
3
SELECT * FROM t
WHERE key_part1 >= 1000 AND key_part1 < 2000
AND key_part2 = 10000;

索引中包含(key_part1, key_part2)元组的值,按照”首先按照key_part1排序,再按照key_part2排序”的规则排序。

如果没有MRR,一次索引扫描将覆盖所有的keypart1从1000到2000的元组,不管key_part2在这些元组中的值是不是10000^footnote1。这样的扫描出的元组中key_part2的值不一定是10000。

如果启用了MRR,扫描会被分解成多个范围,每个范围包含key_part1单个的一个值。每个这样的扫描都只需过滤处理那些key_part2的值为10000的元组。MRR使得索引扫描的元组数量大大减少。

用范围标记表示(不使用MRR的查询),不使用MRR的扫描必须要验证这样的索引区间[{1000, 10000}, {2000, INT_MIN}][^footnote2]。这样扫描结果可能会包含许多key_part2不为10000的元组。
然而MRR扫描,则验证多个单点区间[{1000, 10000}], [{1001, 10000}]…, [{2000, 10000}],这样扫描得到的索引元组将只包含key_part2=10000的部分。

两个optimizer_switch系统变量标志为可以提供接口来使用MRR优化。mrr标志位控制MRR是否启用。如果mrr被启用了(on),mrr_cost_based标志位将控制优化其是否要根据开销来选择使用使用MRR(ON),
还是只要能使用MRR就是用它(OFF)。mrr,mrr_cost_based都是默认打开的(on)。

对于MRR,存储引擎使用read_rnd_buffer_size系统变量的值作为能够申请多少内存(用于MRR)的指导值。引擎将根据read_rnd_buffer_size来决定将单次处理分割成多少个range。

##…未完待续

<<<<<<< HEAD

文件排序优化(File Sort Optimization, FSO)
对于组合了ORDER BY non_index_column(无索引列) 与LIMIT x的查询,本特性,在X列的内容可以被装入排序buffer中时,会加速排序。对于所有的存储引擎都生效。

Innodb优化
MySQL 5.6以innodb作为默认的存储引擎,从MySQL 5.5开始就是这样。

存储优化器状态
提供了更精确的InnoDB索引统计,并且在MySQL重启之后不会消失。InnoDB提前通过对索引的一部分进行采样计算统计,这些统计会帮助优化器决定在一个查询中要选择使用哪个索引。这些统计在MySQL重启后不会丢失。而不是在重启或发生一些运行中事件时重新计算。更加准确的统计会提高查询性能,并且存储方面会保持查询性能的稳定。这项特性被innodb_analyze_is_persistent, innodb_stat_persistent_sample_pages以及innodb_stats_transient_sample_pages等配置项控制。当这项存储状态特性被激活,这些统计信息只会在明确的对表调用ANALYZE_TABLEi。

新的INFOMATION_SCHEMA表

[^footnote2]: 原因是在SQL的比较中组合索引的比较逻辑先以第一个key为主,然后在比较第二个,依次类推。例如{1001, 0} > {1000, 10000}

05540260346b54dcac9d41b4fac10bab58d0e736

本文翻译自Martin Fowler博客,原文地址

如果你要验证一些输入数据,通常不应该选择用Exception的方式来表明验证失败。我想仔细的描述如何使用Notification模式来重构这样的代码。

目录:
什么时候使用此项重构 从哪里开始
构建一个Notification 分解检验方法
验证数字 验证日期
*缩减调用栈

我最近正在琢磨一些处理传入Json消息的验证代码。就像下面这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void check(){
if(date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try{
parsedDate = LocalDate.parse(date);
} catch(Exception e){
throw new IllegalArgumentException("Invalid format for date", e);
}
if(parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if(numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if(numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
}

(JAVA代码)

这是处理数据验证的通用方式。在一些数据上执行一系列的检查。如果任何一个检查失败了,就抛出一个带有错误信息的异常。

这样的方式存在一些问题。首先,在这样的场景下使用Exception让我不爽。异常表明发生了一些超出程序预期的事情。但是,如果你对外来输入进行了检查,就说明你对一些信息不合法是有预期的。如果这样的失败是可以预期的,那么就不应该使用异常。

其次,程序只能检测出一个错误,但是最好是能够报告输入参数的所有问题。这样客户端可以将所有的错误都展示给用户,而不是让用户一遍一遍的尝试。

我比较倾向的报告验证方式是像后文描述的Notification模式。一个Notification收集错误,每个验证失败都会想notification增加一个错误。一个验证方法返回一个Notification,让你可以向用户交付更多信息。一个简单的例子如下。

1
2
3
4
private void validateNumberOfSeats(Notication note){
if(numberOfSeats < 1) note.addError("number of seats must be positive");
//more check like this
}

我们可以用一个简单的调用aNotification.hasErrors()来检查是不是有错误。Notification的其他方法可以包含更多详细的错误信息。

代码转变

# 何时使用这项重构
我需要强调,我不是建议从你的代码中删除所有的Exception。异常是处理异常行为并使之与正常逻辑流分离的好方式。这个重构方式,在一些特征表明异常并非是异常性的时候,才比较合适,因此我们需要在主要的逻辑流中来处理。我上面举的例子,就是一个典型的代表。

一个《Pragmatic Programmers》提供的比较有用的经验是:
我们相信异常在程序的正常流程里应该是很少使用的。异常应该表征不可预测的行为。假设一个没被抓住的异常是你的程序崩溃了,那么问问你自己,“如果我去掉了所有的异常处理器,程序还能否运行”,如果答案是NO,那么也许是在不应该使用Exception的地方使用了Exception。 – David Thomas and Andy Hunt

这样的一个重要结果是是否要使用异常来处理特定的任务是要看所处环境的。所以,就像《Pragmatic Programmers》继续解释的那样,读取文件时如果文件不存在,抛不抛出异常要根据具体情况分析了。如果要读的文件是一个众所周知(默认一定存在,例如unix环境下的/etc/hosts),我们将假设次文件存在,如果此时文件不存在,那么此时抛出异常就是合理的。然而,如果你要读的文件是用户通过command line输入的,那么应该认为这个文件有可能不存在,并应使用其他的机制(而不是异常),一个可以表征这个错误本质上不是异常状况的方式。

还有一种情况会比较适合食用异常来验证失败。例如,当使用的数据,在之前的流程已经被验证过,但是想要再次检查以防止衰代码引入脏数据。

这篇文章是关于在验证(用户)原始输入时,使用Notification替代Exception。有可能在其他场景下,用Notification也比使用Exception更合适,但是我还是专注于这个问题(校验用户的原始输入)。

# 起点

到现在还没有给个例子,只是由于我对各种形态的代码都很感兴趣(一时无法取舍)。但是如果我们聊的再深入些,我就必须得对某一种(代码形态)深入探讨了。我想用这样一个例子,即预定电影院座位时的JSON信息的处理。代码是在一个预定请求类中,(这个类)是从JSON中(使用gson)提取出来的。

1
gson.fromJson(jsonString, BookingRequest.class)

这个预定请求中,我们需要检查的只有两个域,预定的时间和预定的座位数。

1
2
3
4
class BookingRequest{
private Integer numberOfSeats;
private String date;
}

验证代码既是向上面展示的那样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void check(){
if(date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try{
parsedDate = LocalDate.parse(date);
} catch(Exception e){
throw new IllegalArgumentException("Invalid format for date", e);
}
if(parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if(numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if(numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
}

(JAVA代码)

# 构建一个通知
为了使用通知,你必须穿件一个通知对象。一个通知可以非常简单,有时候字符串List就可以玩这个把戏。

1
2
3
4
5
6
List<String> notification = new ArrayList<>();
if (numberOfSeats < 5) notification.add("number of seats too small");
// do some more checks

// then later…
if ( ! notification.isEmpty()) // handle the error condition

尽管阿哥简单的List实现了非常轻量的Notification模式,我通常喜欢做的更多些,创建一个简单的类。

1
2
3
4
5
6
7
8
public class Notification {
private List<String> errors = new ArrayList<>();

public void addError(String message) { errors.add(message); }
public boolean hasErrors() {
return ! errors.isEmpty();
}


通过使用一个类,可以让我的意图更加明确,(代码的)读者不需要去脑补我的意图和我的实现之间的映射关系。

# 分解检查方法
第一步是将检查方法分解为两个部分,一个内含的部分只会处理通知,并不抛出异常;一个外部部分会维持检查当前检查方法的行为,既是抛出一个异常来表明验证失败。

第一步,用Extract Method将检查相关(check方法)的逻辑抽离到一个新的validation方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BookingRequest
public void check() {
validation();
}

public void validation() {
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
}

接下来,我会把validation的返回值改为返回一个notification。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BookingRequest
public Notification validation() {
*** Notification note = new Notification();
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
*** return note;
}

我现在可以检查notification,如果有错误的话,可以抛出一个异常了。
1
2
3
4
5
class BookingRequest
public void check() {
*** if (validation().hasErrors())
*** throw new IllegalArgumentException(validation().errorMessage());
}

我把validation方法置为public,因为我希望更多的调用者可以使用这种方式来验证,而不是check方法。

在这点上,我并没有改变代码的行为,notification不会遗漏任何的错误。但是我已经准备好使用notification来替代异常了。

在我继续之前,我必须说一些有关错误信息的事。当我们进行一次重构时,原则是避免对可观测的行为产生影响。很明显地,抛出相应的异常是外部程序所观测的行为。但是他们(外部程序)对错误信息关注到什么程度呢?notification会最终将多个错误收集起来,汇总称为一个单独的信息,如下:

1
2
3
4
5
class Notification
public String errorMessage() {
return errors.stream()
.collect(Collectors.joining(", "));
}

但是,如果高层程序依赖于“获取第一个错误的信息”,那么就会产生问题。别人没有把这个事搞砸,不代表我不会把他搞砸。

# 验证数字
当前要做的就是把第一个验证替换掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BookingRequest…
public Notification validation() {
Notification note = new Notification();
*** if (date == null) note.addError("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
return note;
}

一个理所当然的改变,但是并不好,因为改变了代码行为。如果我们将一个null的date传入下面的处理过程,会引起NPE,这并不是我们需要的异常。
所以,一个不那么直观,但是更有效的方式是后退一步(即从后向前处理)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BookingRequest…
public Notification validation() {
Notification note = new Notification();
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
*** if (numberOfSeats < 1) note.addError("number of seats must be positive");
return note;
}

前面的检查,是一个null的检查,所以我们需要使用条件判断来避免NPE。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BookingRequest…
public Notification validation() {
Notification note = new Notification();
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
*** if (numberOfSeats == null) note.addError("number of seats cannot be null");
*** else if (numberOfSeats < 1) note.addError("number of seats must be positive");
return note;
}

我们看到下一个价差涉及到了不同的域。我们必须要给之前的重构引入条件判断了,我开始考虑validation方法开始变的太过复杂并且可以对他做一些肢解。所以我把数字验证部分抽离了出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class BookingRequest…
public Notification validation() {
Notification note = new Notification();
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
*** validateNumberOfSeats(note);
return note;
}

private void validateNumberOfSeats(Notification note) {
if (numberOfSeats == null) note.addError("number of seats cannot be null");
else if (numberOfSeats < 1) note.addError("number of seats must be positive");
}

看看抽离出的对数字的验证方法,我并不喜欢这种结构。在验证的时候,我不喜欢用if-then-else块,因为很容易会导致代码杂糅在一起。我选择流畅的短路代码,此时我们可以使用哨兵检测来替代嵌套的判断条件。

1
2
3
4
5
6
7
8
class BookingRequest
private void validateNumberOfSeats(Notification note) {
if (numberOfSeats == null) {
note.addError("number of seats cannot be null");
return;
}
if (numberOfSeats < 1) note.addError("number of seats must be positive");
}

我决定从后向前来保证代码能够通过遗留测试,是重构过程中的重要原则。重构是一个特殊的技术,通过一系列行为来改变代码结构的同时保持代码原油的行为。所以当我们重构的时候,我们总是需要保持小步前进,以保持代码原有的行为。通过这样做,我们减少犯错机会,减少debug时间。

# 验证日期
对于日期的验证,我将从提取方法开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BookingRequest…
public Notification validation() {
Notification note = new Notification();
*** validateDate(note);
validateNumberOfSeats(note);
return note;
}

***private void validateDate(Notification note) {
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
}

现在我们可以开始从后向前验证日期了
1
2
3
4
5
6
7
8
9
10
11
12
class BookingRequest…
private void validateDate(Notification note) {
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
*** if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");
}

通过第二步,处理错误的过程有些复杂,由于抛出的异常中包含了引起它的异常。为了出了这个情况,我需要调整notification让它能够处理嵌套的异常(cause exception).由于我处于重构的中间步骤,我的代码还不能通过测试,所以我决定暂且将validateDate方法留在上述状态中,我将去丰富notification以使其支持嵌套的异常。

我开始修改notification,增加了一个以异常原因为参数的addError方法,并调整原有的方法使其适配。

1
2
3
4
5
6
7
8
class Notification
public void addError(String message) {
addError(message, null);
}

public void addError(String message, Exception e) {
errors.add(message);
}

这意味着我们接受嵌套异常,并把他忽略了。为了把cause异常纪录下来,我把error记录从一个简单的对象转变成了一个不那么简单的对象。
1
2
3
4
5
6
7
8
9
10
class Notification…
private static class Error {
String message;
Exception cause;

private Error(String message, Exception cause) {
this.message = message;
this.cause = cause;
}
}

我通常不喜欢java中出现非private的域,但由于这是一个私有的内部类,我觉得也还行。如果我要把error类暴露出去,我会将这几个域包装一下。
1
2
3
4
5
6
7
8
9
10
private List<Error> errors = new ArrayList<>();

public void addError(String message, Exception e) {
errors.add(new Error(message, e));
}
public String errorMessage() {
return errors.stream()
.map(e -> e.message)
.collect(Collectors.joining(", "));
}

通过最新的notification,我可以开始改预定请求了。

1
2
3
4
5
6
7
8
9
10
11
12
class BookingRequest…
private void validateDate(Notification note) {
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
note.addError("Invalid format for date", e);
return;
}
if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");

由于我已经提取了方法,现在做起来就很容易了。
最后一步修改也很简单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BookingRequest…
private void validateDate(Notification note) {
if (date == null) {
note.addError("date is missing");
return;
}
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
note.addError("Invalid format for date", e);
return;
}
if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");
}

# 将工作栈上移
一旦我们有了新方法,下一个任务就是查看调用原来check方法的调用方,调整他们,使之可以使用心得validate方法。这意味着更广阔的视野来查看validation方法是否适合应用的流程,那么这就超出了本次重构的范畴。但是中期目标应该是当我们想验证是否失败时,将所有的异常移除。

许多情况下,本次重构将会完全避免继续使用check方法。所有的验证都应该直接调用validation方法。我们也应该同时调整测试来使用notification。

Leonard Richardson的模型将REST的基本元素分解到三个步骤当中。这三个步骤包括:资源(resources),http谓词(get, post, put, delete, head, option),超链接控制。

注:文章译自Martin Fowler博客,原文链接.
最近我(Martin Fowler)阅读了一本《Rest In Praces》,我的同事们写的一本书。他们的目的是解释如何使用Rest Web Service来解决企业级应用于道的集成难题。这本书的核心来自于一个观察,即当前的互联网(Web)就是一个大规模的分布式系统,并且它运行良好,所以我们可以借鉴互联网(Web)的组织方式来构建我们的集成系统。
Figure 1: REST的层次

为了更好的解释类互联网(web-style)系统的详细属性,(Rest In Practise)作者们使用了restfult成熟度模型(注:这一模型由Leonard Richardson发现,并在QCon上提出)。该模型采用了很好的方式来思考如何使用Web相关的技术。所以我(Martin Fowler)会以自己的方式来解释一下Rest成熟度模型(下面的例子只是用作说明,不值得拿去编码测试)。

# Level 0

起点是将HTTP作为远程交互的传输协议,而不使用Web相关的机制。实际上,这种方式只是讲HTTP座位一个通讯管道,而其中包含的是自定义的远程交互机制,通常是基于Remote Procedure Invocation。
Figure 2: 使用Level 0的交互样例

假设我要预约一下我的医生。我的预约软件首先需要知道,我的医生在特定日期哪个时段是可约的,所以它像医院的预约系统发送请求,以查看该信息。在Level 0的场景下,医院会在某个url上开放一个服务接入点(service endpoint)。我会向该接入点发送一个包含了我详细请求的文档。

1
2
3
4
POST /appointmentService HTTP/1.1
[various other headers]

<openSlotRequest date = "2015-06-16" doctor="mjones">

医院的服务器则会给我返回如下信息:
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
[various headers]

<openSlotList>
<slot start = "14:00", end = "14:50">
<doctor id = "mjones" />
</slot>
<slot start = "16:00", end = "16:50">
<doctor id = "mjones"/>
</slot>
</openSlotList>

我使用了XML座位例子,但这个内容可以是任何形式:JSON,YAML,key-Value Pairs,或是其他的自定义格式。
下面,我需要预约一个时段,同样的我可以想服务接入点发送一个请求。
1
2
3
4
5
6
7
POST /appointmentService HTTP/1.1
[various other headers]

<appointmentRequest>
<slot doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointmentRequest>

如果一切顺利,我将会得到反馈说我的预约成功了。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<appointment>
<slot doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointment>

如果遇到了问题,例如另一个人在我之前预约了,我将得到一个错误反馈。
1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
[various headers]

<appointmentRequestFailure>
<slot doctor = "mjones" start="14:00" end = "14:50"/>
<patient id = "jsmith" />
<reason>Slot not available</reason>
</appointmentRequestFailure>

到现在为止,仍是一个直观的RPC类系统。它只是在把XML发送与返回。如果你使用SOAP或者XML-RPC,基本与此类似,唯一的区别就是把你的XML信息包装在了一个“信封”中。

# Level 1 - Resources

走向REST的在RMM中巅峰的第一步是引入资源(resources)。那么,现在与其将所有的请求都发往一个单独的服务接入点,我们现在可以想独立的资源发送请求了。
Figure 3: Level1 增加了资源(resource)
那么对于我们的第一个请求,可能有一个资源代表指定的医生。

1
2
3
4
POST /doctors/mjones HTTP/1.1
[various other headers]

<openSlotRequest date = "2015-06-16">

响应携带类似的基本信息,但是每个slot都是一个可以被单独请求。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<openSlotList>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50">
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50">
<openSlotList/>

有了对应的资源之后,预约一个时段意味着向对应的slot发送请求。
1
2
3
4
5
6
POST /slots/1234 HTTP/1.1
[various other headers]

<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

如果一起顺利,会得到一个类似下述的响应。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<appointment>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointment>

与Level0的差别是,如果有谁想对appointment做些什么,像预定一些化验,他们需要先拿到相应的appointment资源,该资源可能有个URI,像http://roylhope.nhs.uk/slots/1234/appointment,并对此URL发送请求。

对于像我这样的面相对象的人来说,资源特别像是对象的唯一标示符。相比于调用一个方法并传递(所有信息)参数,我们调用一个指定对象上的方法并提供其他相关的参数。

# Level 2 - HTTP Verbs
在上述(Level0 和 Level1)的所有请求中,我都适用了HTTP的POST谓词,但是一些人会使用GET取而代之。在这些层面上(Level0和Level)并不会产生什么不同,他们都用来作为你在HTTP上传输交互信息的管道。Level2 比此更进了一步,使用HTTP的谓词来表征将会对响应的资源(resource)产生怎样的操作。
Figure 4 Level 2 增加HTTP谓词语义
对于我们想获得医生的空闲时段,意味着我们需要使用GET。

1
2
GET /doctors/mjones/slots?date=20150616&status=open HTTP/1.1
Host: royalhope.nhs.uk

得到的响应和使用POST一致。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<openSlotList>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50">
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50">
<openSlotList/>

在Level2中,对于GET的使用是个关键。HTTP定义GET是一个安全操作,也就是说它不会对任何事物的状态引起任何特别的影响。这允许我们安全的任意次数的以任意顺序的执行GET,并得到相同的结果。一个重要的结果的是,它允许请求链路上的任何一个参与者使用缓存,而缓存恰恰是web性能的重要优化方式。HTTP包含多种方式支持缓存,这些可以被通讯中的所有参与者所使用。通过遵从HTTP的原则,我们能够利用它提供的能力。

为了预约一个时段,我们需要一个HTTP位次来改变(响应资源)状态,一个POST或是一个PUT。我会像之前一样使用POST。

1
2
3
4
5
6
POST /slots/1234 HTTP/1.1
[various other headers]

<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

POST和PUT之间的取舍超出了本文的范围,可能需要一篇新的文章来说明这个问题。但是我想指出一些人错误的映射了POST/PUT和CREATE/UPDATE之间的对应关系。他们之间的选择则是与这种映射关系更为不同。

尽管如果我使用了与Lelvel 1一样的POST请求,然而服务端的反馈则会有明显的不同。如果一切顺利,服务会返回一个带有201状态码的响应,201代表这有一个新的资源产生了。

1
2
3
4
5
6
7
8
HTTP/1.1 201 Created
Location: slots/1234/appointment
[various headers]

<appointment>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointment>

201状态码表示了一个location参数,并携带了一个URI,该URI可以被客户端使用GET来查看资源状态。响应中也包含了新增资源的描述,可以引导客户端发送一个附加的请求。

当发生错误时还会产生另一种不同,例如某人已经预定了该时段。

1
2
3
4
5
6
HTTP/1.1 409 Conflict
[various headers]

<openSlotList>
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50" />
<openSLotList>

这次响应中重要的部分是使用HTTP的状态码来表征发生了错误。在这种情况下,用409来表示其他人已经更新了该资源看起来是一个比较合适的选择。相比于返回一个状态码是200,但是内容中包含了错误信息,在Level 2中,我们显示的使用这种方式来表达错误的响应。具体使用什么状态码来表征什么反馈是协议设计者的事情,但是至少应该要采用一个非2XX的状态码来表征错误。Level 2 引入了HTTP的谓词和状态码。

这里有一个不一致的地方。REST建议使用所有的HTTP谓词。他们也在证实他们的方式是借鉴自Web系统的成功实践。但是万维网不怎么使用PUT和DELETE。有很多的理由来使用PUT和DELETE,但Web的经验并不是其中之一。

当前的Web最为重要的元素是强烈的区分开安全操作(GET)和非安全操作(POST),并且使用状态码来标示是否产生了错误。

# Level 3-Hypermedia Controls
最后一个层次引入了大家耳熟能详的拥有丑陋缩写的HATEOAS(Hypertext As The Engine Of Application State)。它将问题从如何获取“空闲时段列表”转换成了要做什么才能预定到一个时段(抽象啊)。
Figure 5 : Level 3 增加了超链接控制
我们从最开始的GET请求入手,和在Level 2中发送的请求一样。

1
2
GET /doctors/mjones/slots?date=20150616&status=open HTTP/1.1
Host: royalhope.nhs.uk

但是响应中有一个新的元素:
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
[various other headers]

<openSlotList>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50">
<link rel = "/linkrels/slot/book" uri = "/slots/1234" />
</slot>
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50">
<link rel = "/linkrels/slot/book" uri = "/slots/1234" />
</slot>
<openSlotList/>

每个slot中包含了一个link元素(包含个URI),用以表明可以怎样去预定一次预约。
超链接控制的关键点在于告诉我们下一步要做什么,以及下一步操作所需资源的URI。相对于要我们自己去了解要向哪里发送预约请求,超链接控制则是告诉我们怎么去做。

这个POST请求根Level 2中的一样。

1
2
3
4
5
6
POST /slots/1234 HTTP/1.1
[various other headers]

<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

而响应中则包含了一系列的超链接控制来表征下一步可以做的多个行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 201 Created
Location: slots/1234/appointment
[various headers]

<appointment>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
<link rel = "/linkrels/appointment/cancel" uri = "/slots/1234/appointment" />
<link rel = "/linkrels/appointment/addTest" uri = "/slots/1234/appointment/tests" />
<link rel = "self" uri = "/slots/1234/appointment" />
<link rel = "/linkrels/appointment/changeTime" uri = "/doctors/mjones/slots?date=20150616&status=open" />
<link rel = "/linkrels/appointment/updateContactInfo" uri = "/patients/jsmith/contactInfo" />
<link rel = "/linkrels/help" uri = "/help/appointment" />
</appointment>

超链接控制的一个显著好处是它允许服务端改变他自己的URI配置的同时不会对客户端引发影响。一旦用户寻找“addTest”的URI时,服务端可以隐藏掉所有其他的初始接入点。
一个附加的好处是允许客户端开发者了解整个协议。这些链接会给客户端开发者一个提示,“接下来你可能需要做什么”。他并不提供所有的信息,例如“latest”和“cancel”都指向相同的URI,他们需要明确一个是GET而另一个是DELETE。但是呐,这样至少给客户端开发人员一个出发点,让他们知道要去文档中找什么。
同样的,超链接控制允许服务端开发人员通过讲新的link加入响应的方式增加新功能。

到目前为止们还没有完整的标准来指导大家应该如何表征超链接控制。我当前所做的则是按照《REST in Practise》中推荐的那样(而他是遵从ATOM,RFC 4287的)。我使用元素,其中包含uri属性(其标示了目标URI)以及rel属性(其标示出了关系的类型)。一个通用的关系(例如self用来指向资源本身)是十分明显的,任何其他特定于该服务端的link都是一个合格的URI。ATOM表明熟知的linkrels是Registry Of Link Relations。我写的这些受到ATOM限制的,ATOM本身是在第三层restfulness的领导者。

# 分层的意义
我应该强调的是,RMM,尽管它是一个思考REST相关元素的好方式,但并不是REST层次的定义本身。[Roy Fielding说明了3层RMM是REST的一个前置条件](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven]。像软件中的许多定义一样,REST有很多的定义,但是由于Roy Fielding发现了这一概念,他的定义应该比其他的更具权威性。
我发现,RMM提供了一个逐步递进的方式来帮助我们理解restful 这种想法背后的基本概念。因此,我把RMM视为一种帮助我们理解概念的工具而不是某种评价验证体系。我觉得使用Restful来集成系统的实际案例还不足以证实它是最好的。但是我确实认为它是一个吸引人的方案,并且在大多数情况下我会推荐这种方案。
与lan Robinson讨论,它强调RMM真正吸引人的地方在于它引入了普通的程序设计理念。

* Level 1,通过讲一个大的服务接入点打碎成为许多的资源,来降低问题的复杂性。此处运用了分治的思想。
* Level 2,引入了一些列的标准谓词,所以我们可以用同样的方式来处理类似的问题。此处消除了不必要的变量。
* Level 3,引入了发现能力,提供了一种方式让协议可以自解释。此处引入了自解释能力。

本文译自Martin Fowler博客,原文链接

作为Java程序员,很容易喜爱垃圾回收(garbage collection),无需自己释放内存。然而GC行为的基本内存模型是什么?其中,又有很多的概念,像young generation, old generation以及permanent generation,并且她们之间又回产生怎样的交互呢?

本文试图解决上述问题。下面将以最为浅显直接的图示来说明这几部分之间的关系。

# JVM内存组成

JVM内存由两部分组成: heap(堆空间)+ non-heap(非堆空间)

# Heap堆空间的组成

Heap堆空间分为young区和old区,其具体的组成结构关系如下图所示。
Figure1: Heap堆空间的结构

从上图中,我们可以看出,Heap空间主要分为young区和old区,而其中young区由Eden Generation,Survivor Generation(多个)组成; 其中old区则由tenured generation组成。

并且,对象在这三个代中的流动时机也在图中有所表示。首先,当Eden Generation达到阈值,则会触发minor gc,如果对象依然有引用,则会被迁移到其中一个Survivor Generation中;如果对象在Survivor Generation中生存的时间足够长,则会被迁移到tenured generation中。

Figure2: minor gc, major gc, full gc

未完待续…