不推荐使用Json-lib的n个理由

Json-lib, 包名以net.sf.json开头,是一个Java中常用的用于将JSON[JSON]和Java中的对象进行相互转换的库。出于以下几点原因,我不推荐使用这个库,排名不分先后。

Json-lib已经过时了

Json-lib的上一个版本是在2010年[Json-lib::Changelog]发布的,距今已经有8年之久。有的人可能会觉得这样的库一定会久经考验,非常稳定,不会出错。事实恰恰相反。在下文中我会阐述这个库目前我已经发现的bug。此外旧版本的库,或是软件会比较稳定而不易出错一直是一个错误的想法。新版本的库不仅能修复旧版本中已知的bug,还能引入新的功能,解决旧版本库所不能解决的问题。在选择开源库的时候,尽可能选择在持续维护和更新的库,并使用它的最新版本。

使用Json-lib,你的程序可能意外中断

1
2
3
4
5
6
7
8
9
public class App {
public static void main(String[] args) {
String s = "{";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
jsonObject = JSONObject.fromObject("{\"a\":\"1\"}");
System.out.println(jsonObject);
}
}

如上程序试图将两个JSON转换为JSONObject对象。由于“{”不是一个合法的JSON对象,转换自然而然就失败了。因此在JSONObject jsonObject = JSONObject.fromObject(s);这一行,程序会抛出一个异常,信息如下:

1
2
3
4
5
6
7
Exception in thread "main" net.sf.json.JSONException: Found starting '{' but missing '}' at the end. at character 0 of null
at net.sf.json.util.JSONTokener.syntaxError(JSONTokener.java:499)
at net.sf.json.util.JSONTokener.<init>(JSONTokener.java:85)
at net.sf.json.JSONObject._fromString(JSONObject.java:1201)
at net.sf.json.JSONObject.fromObject(JSONObject.java:165)
at net.sf.json.JSONObject.fromObject(JSONObject.java:134)
at me.deeloves.App.main(App.java:9)

如果我们查看源代码的话,会发现net.sf.json.JSONException继承于org.apache.commons.lang.exception.NestableRuntimeException,而org.apache.commons.lang.exception.NestableRuntimeException又继承于java.lang.RuntimeException。RuntimeException不需要被捕获,因此当它被抛出而又没有捕获的时候,程序就会中断。这就导致了我们的第二个字符串,一个合法的JSON没办法被正常转换,除非我们修复程序,让它重新运行。在实际的工作中,我们常常会忽视了传入JSONObject.fromObject(Object)方法的对象是否为合法的参数,因此无意识地传入了不合法的参数,并导致了中断的发生。

如果我们仔细阅读了Json-lib API,就会发现JSONObject.fromObject(Object)方法抛出了一个非检查型异常。IDE不会帮助我们捕获这个异常,但是我们手动操作一下就可以修复这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class App {
public static void main(String[] args) {
try {
String s = "{";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
try {
JSONObject jsonObject = JSONObject.fromObject("{\"a\":\"1\"}");
System.out.println(jsonObject);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

修复后的程序输出如下:

1
2
Found starting '{' but missing '}' at the end. at character 0 of null
{"a":"1"}

虽说在方法传入参数违背要求时可以抛出运行时异常,比如NullPointerException会在要求参数为非空但传入值为null时抛出。但是在Json-lib中,有些方法,如JSONObject.element(String, Map, JsonConfig)也会抛出JSONException,且文档[JSONObject.element(String, Map, JsonConfig)]中即没有告诉我们对参数的要求是什么,也没有告诉我们什么时候会抛出这个异常。这样的异常定义方式是令人困惑的。

Json-lib没有使用泛型

正如前文所提到的,Json-lib发布于2010年,而且最后一个版本的Json-lib是可以兼容JDK 1.5的。JDK 1.5是具有跨时代意义的一个Java版本。在这个版本中,Java语言第一次引入了泛型[New Features and Enhancements J2SE 5.0]。泛型增强了程序中的类型安全,使得许多过往在运行时类型不匹配而产生的异常在编译时就可以被发现并被修复。[Lesson: Generics (Updated)]遗憾的是,即便Json-lib的最后一个版本是在JDK 1.5发布了6年之后发布的,它依然没有使用泛型。

1
2
3
4
5
6
7
8
9
10
11
12
public class App {
public static void main(String[] args) {
try {
String s = "{\"a\":\"123\", \"b\":456}";
JSONObject jsonObject = JSONObject.fromObject(s);
char four = ((String) jsonObject.get("b")).charAt(0);
System.out.println(four);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

你可能以为这段程序会输出4。然而它运行结果如下:

1
2
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at me.deeloves.App.main(App.java:14)

很显然我们能看出问题在哪了:程序试图在运行时将Integer类型的对象转换成String对象,于是发生了这个错误。[JLS 5.1.6 Narrowing Reference Conversion] 在进行转换的时候,Json-lib会将JSON中的String类型转换成Java中的String类型。对于JSON中Number类型,Json-lib会将它转换成Integer类型,或是别的可以表示数字的类型。对于JSONObject,一个合适的泛型可能会是JSONObject<String, Object>。但很遗憾Json-lib没有这么做,于是Bug就很容易产生。它也不允许客户端指定要转换的JSONObject当中value的类型。如果你希望把所有的value都当成String来处理,一个比较可行的解决方案如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class App {
public static void main(String[] args) {
try {
String s = "{\"a\":\"123\", \"b\":456}";
JSONObject jsonObject = JSONObject.fromObject(s);
Map<String, String> theTrueConvertedMap = new HashMap<>();
for (Object e : jsonObject.entrySet()) {
if (e instanceof Map.Entry) {
Map.Entry entry = (Map.Entry) e;
theTrueConvertedMap.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
}
}
char four = theTrueConvertedMap.get("b").charAt(0);
System.out.println(four);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

这段程序将JSON转换成了类型安全的Map,确保了后续的操作都是类型安全的。但如果我们期望转换后得到的Map的泛型是别的类型的,恐怕就有些复杂了。

Json-lib对浮点数进行转换的时候会丢失精度[Json-lib Bug #116]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class App {
public static void main(String[] args) {
try {
String s = "{\"pi\":3.141592653589793}";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject.get("pi"));
System.out.println(jsonObject.get("pi").getClass());
Double pi = Math.PI;
System.out.println(pi);
System.out.println(pi.equals(jsonObject.get("pi")));
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

你可能以为这段程序的输出是:

1
2
3
4
3.141592653589793
class java.lang.Double
3.141592653589793
true

但实际上它输出的是:

1
2
3
4
3.1415927
class java.lang.Double
3.141592653589793
false

Json-lib缺少更加详细定义的异常

Json-lib只定义了一个异常,而且很可悲地是一个运行时异常JSONException。所有可能会抛出异常的地方,抛出的异常都是JSONException。假定我们具备了一定的对错误输入的处理能力,并写下了如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class App {
public static void main(String[] args) {
print("{");
print("}");
}

private static void print(String s) {
try {
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
} catch (JSONException e) {
if (e.getMessage().startsWith("Found starting '{' but missing '}' at the end.")) {
s = s + "}";
try {
System.out.println(JSONObject.fromObject(s));
} catch (JSONException e1) {
System.out.println("oh we lost");
}
} else if (e.getMessage().startsWith("A JSONObject text must begin with '{'")) {
s = "{" + s;
try {
System.out.println(JSONObject.fromObject(s));
} catch (JSONException e1) {
System.out.println("oh we lost");
}
} else {
System.out.println(e.getMessage());
}
}
}
}

运行后的输出是:

1
2
{}
{}

但如果我们有能力对所有的不同异常情况进行处理呢?我们就需要写一长串的if-else语句,来判断异常信息的内容是什么,然后根据e.getMessage()得到的返回值的内容来进行各种各样的处理。如此长的if-else语句会使代码变得不可读和难以维护。Json-lib应当给不同类型的异常定义不同类型的,名字上带有具体含义的异常。比如在这里所处理的两种不同情况,所对应的异常可以是MissingFirstBraceExceptionMissingLastBraceException。很遗憾,Json-lib没有这么做。

不仅如此,在一些不必使用JSONException的地方,Json-lib还是不恰当地抛出了JSONException。比如JSONObject.element(String, boolean)[JSONObject.element(String, boolean)]会在第一个参数为null时抛出JSONException,但显然在这里NullPointerException要比JSONException更适合被抛出。

Json-lib不符合JSON的格式规范[ECMA-404]

使用Json-lib的初衷是为了能在JSON和Java对象之间进行转换,但如果你操作的根本就不是标准的JSON呢?

1
2
3
4
5
6
7
8
9
10
public class App {
public static void main(String[] args) {
try {
JSONObject jsonObject = JSONObject.fromObject("{'a':1}");
System.out.println(jsonObject);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

这段程序能正常运行,但是{'a':1}真的是合乎规范的JSON Text吗?

由于版权的问题,我无法将规范内的图片截取到我这篇文章来。但上面这个程序的问题是显而易见的:规范中规定了JSON当中的字符串必须是双引号所包括的,内部是字符。而Json-lib会把单引号所括起来的,我不知道称之为什么的东西也当成JSON中合法的字符串。显然违背了JSON的格式规范。

更糟糕的是,JSON规范中规定了Object中的name必须为字符串,而Json-lib里却可以不是字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class App {
public static void main(String[] args) {
try {
String s = "{a:2}";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
s = "{'a':2}";
System.out.println(JSONObject.fromObject(s));
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

上面这段程序的输出结果如下:

1
2
{"a":2}
{"a":2}

这是两个一模一样的字符串。从第一行我们可以看到,当name不为字符串的时候,Json-lib会把整个“串”都当成字符串处理。根据这样的推断,第二行的输出应当是{"'a'":2}。而实际结果却不是这样。Json-lib自身对”JSON”格式的定义是怎么样的呢?我们无从得知,于是使用Json-lib所写出来的程序也就自然令人费解。

如果是前面所讲到的问题是可以被克服的(实际上我也尝试给出了解决的方案代码),那么这一小节所讲的是根本性问题,是我不推荐使用Json-lib的根本性原因。

总结

Json-lib的bug还有不少,如果你在搜索引擎里搜索一下,就能找到很多。[json-lib bug at DuckDuckGo]时间关系不再一一阐述。一句话总结:不要使用Json-lib。

飞行记 | PVG-BOS-LGA-BOS-PVG 海南航空与JetBlue联运

今年春节我没有回家。

十月的时候我就计划好了这个春节去纽约和女朋友过,大约十月底的时候我就买了机票。5250从途牛网出的票,浦东出发,波士顿中转到纽约拉瓜迪亚。返程是原路返回。这个行程是我从skyscanner上找到的,而且如果要从国内的网站出这个票的话就只能在途牛出,所以最后选了途牛,5250是实际付款的价格。其实之前还有别的选择。更早的时间我可以选择东航的直飞JFK,价格大约也在5000出头。如果中转的话也可以选择加航,价格在四千多,不过中转地点有可能是蒙特利尔。当时我对加拿大的中转政策并不怎么了解也就没有轻举妄动。(其实是因为口袋里没钱)。在买完这次出行的票后好像我还是看到了更便宜的,不过无所谓了。这次的行程还是比较保险的,因为出发和到达时间都是大白天,不用太担心晚上出什么意外。而且我想着海航算是Skytrax五星级航空,就当多掏钱买服务吧。

说到出票的问题。我之前尝试过在虹桥机场的海航售票点打行程单。结果售票点的小姐姐说用外币结算的他们打不出来,要我去找代理。后来我就索性没打。所以说我这个票肯定不是境内出的。如果要尝试购买一些中转地点不在国内的机票,或者一些行程比较特别的机票我觉得却是可以试着搜索一下途牛。

去程

我的住处位于浦东腹地,距离机场是比较近的,不过没有特别方便的交通工具去浦东机场。所以我还是选择了打车。实际上打车从我的住处只需要半小时就可以到浦东机场了。在车上司机和我聊起此行的目的地。司机问我:“去美国要多少钱啊?”。当我告知我的机票只要五千出头,司机觉得还是挺便宜的。

到机场的第一件事还是check in。

值机

排队的人还是不少的,不过十分钟左右手续还是都办完了。

登机牌

两程的登机牌都在这里打了出来。而且在托运的时候柜台也帮我给行李贴上了两段的托运条子。接下来就是出境和过安检了。

出境要比我想象的快很多。因为我用的是新版护照,出境可以走自助通道。自助通道人很少,不需要排队所以速度非常快。处于众所周知的原因这个环节没有照片。出境后就是安检。最后等我到登机口的时候还有约一个半小时时间。

之前参加(薅)了万事达卡的活动(羊毛)拿到了一点的龙腾点数,因为有效期和适用范围的限制,如果我这回不用可就没有机会用了,趁着有时间赶紧去一波休息室。这次(其实是唯一一次)我选择的是东航七十七号环亚贵宾室,据说是浦东T2国际出发坠吼滴休息室。

休息室的橙汁

讲真我并没有吃早餐,于是就在这里饱餐了一顿。能在机场里面免费吃吃喝喝真的是极好的。不过休息室并没有我想象中那么好。热餐也就米线,面还有什么煎蛋饼而以。剩下的就是一些小蛋糕什么的,反正吃个早饭还是足够的。饮品方面是有酒的,不过我并没有看到有人拿,而且我也不感兴趣。此外汽水也是任拿的。我觉得把汽水从冰箱里拿出来放包里带走也没什么问题,只是我没有这么做。

鱼蛋米线

鱼蛋米线还是很好吃的,我自己加了一点蒜末。

吃得差不多后我还是想稍微逛逛航站楼,于是就出来了。这个休息室楼下就是日上免税店。买东西的人很多,不过我就大概转了一圈就出来了,并没有买什么东西。我女朋友也没什么需求。接下来就是闲逛。

国泰港龙

拍到的国泰港龙航空。女朋友说那是壁虎。

登机需要坐摆渡车。如果不是像白云那样在摆渡车上坐15分钟周游全场,连GAMECO都要逛的话我是不抗拒摆渡车的。

是日座驾

从摆渡车上拍的是日座驾B-2730,一架Boeing 787-8(来跟我读:七八七减八)。海航的787-9是都有wifi的,但是787-8就大部分都没有了,这驾也不例外,真是悲剧。

Virgin Atlantic

朕登基了。

PTV

每个座位都配了毛毯和洗漱包。

洗漱包

此外还有本次航班的菜单,但是我好像忘了拍了……

flying

起飞后不久就开始了第一餐。整个航班一共有2+1餐,2分别是起飞后和降落前。1就看你中途是不是还醒着了。

我先要了个白葡萄酒,忘了酒标了。

白葡萄酒

说真的白葡萄酒放在一次性杯子里真的是变得平平无奇。

因为没有wifi,我在飞机上就是靠打饥荒度日……

一转眼就来到了北美的上空。这是我第一次见到大西洋。

大西洋

麻省的上空

波士顿的机场离市区似乎挺近的,而且就在海边。

远眺波士顿市区

海航降落在波士顿的Terminal E。降落后就是办理入境手续。在入境处已经有会中文的工作人员等候多时。不过似乎在我们后面的Fly Emirate就并没有配备阿拉伯语的工作人员。每当入境时有同胞遇到沟通上的困难的时候就可以召唤工作人员上前去帮忙。然后入境的美国官员就会说“Hainan”然后招呼海航的工作人员。不过还是有些人入境不太顺利。在降落前我和邻座一对年过半百的夫妇聊了起来。他们是来波士顿探望已经在这里工作的儿子。然而就是在入境时他们好像就被带到了“小黑屋”。入境对于我来说倒是没什么困难,我如实地说了来由,然后就过了。唯一尴尬的地方是当我要走向出口的时候差点走错走到了另一个出口通道,也就是往回走了。Terminal E似乎是专门的国际航站楼,一出出口我就能看到售卖电话卡的摊位。不过我之前已经买好了,并不需要。所谓的行李联运并不是完完全全一站到底的。我在入境后还是需要去提取托运行李,并走出出口后去转机柜台交运。在拿着行李往外走的时候我还是被拦了一下。不过检查人员也只是问了我是来美国干嘛的,然后就放行了,并没有查行李。总之,入境远比我想像得要顺利得多,我大概只花了15分钟就走完整个流程了。

走出出口后往左拐就到了中转柜台。这还是我第一回走中转。一开始我把行李交给了JetBlue的工作人员。然而JetBlue的工作人员说我要把行李给海航,海航会再把行李给JetBlue的。所以我把行李交回给了海航。接下来的事情就是去Terminal C然后换乘JetBlue了。在波士顿的Terminal E和Terminal C的二楼之间有个长廊,从这里可以再次过安检并前往Terminal C。而且这里的安检人数少很多。过完安检的第一件事就是去Hudson买东西,目的是为了凑出2.75美元的硬币用于在纽约坐公交,这是后话。

在Terminal C等候的间隙我吃了个汉堡,顺带试了试刷卡。第一次在美国吃东西我还是略紧张,随便点了个汉堡结果是素食的。没想到味道还不错。(无图)。美国的机场里的餐馆水平似乎都还不错。而且相当多的餐馆有精酿啤酒提供,不愧是精酿运动的发源地。我选择的这家餐馆Boston Beer Works有提供多款IPA,Ale和Lager,不过我不太想喝酒就没有尝试。美国的小费是可以写在信用卡付款的单子上然后用信用卡扣的,很方便。第一次我给的比较多,12刀的汉堡我给了2刀的小费。

随着登机时间的临近。登机口前的人越来越多。登机的时候,大家是按照登机牌上的分组先后登机的。只有在被叫到组的时候才可以登机。似乎在美国坐飞机都是这么个规矩。不过我的登机牌是在上海打的,登机牌上并没有写明我的登机组别。当我向登机口柜台的小姐姐反映这个问题的时候,她迅速地给我换了一个新的登机牌。我乘坐的是一架有相当历史的E190,一共有25排,每排4座单通道,全经济舱。不过这个经济舱的前后间距非常大,我感觉要比海南航空的大得多。除此以外还有全舱PTV。不过PTV的屏幕特别小而且不是触屏的。上面大概有三十多个频道,如果需要换台则需要操作座椅把手上的按钮。我并没有找到耳机的插孔也没有请求乘务人员的帮助。令人点赞的是,尽管这驾飞机的机龄有点老,但还是配备了wifi,JetBlue称之为Fly-Fi。

在滑行道上滑行

这次行程一下子刷新我的人生两大记录:最长飞行距离和最短飞行距离。在大约40分钟后我就降落在了古老的纽约拉瓜迪亚机场Marine Air Terminal,也就是Terminal A。

夜色中的纽约

Marine Air Terminal是非常具有历史感的一个航站楼。整个航站楼几乎都被JetBlue所承包了。它的出口处只有一个行李传送带。当我来到时它还是静止的。等了一会儿后,它动了起来,吐出了三个拉杆箱后就又停下来。如果单独购买JetBlue机票,额外托运是要收费的,加之行程较短,我估计也没什么人会选择托运行李吧。

从出口出来后,过了马路就到M60 SBS的车站。SBS是Select Bus Service的简称。M的开头表示这趟车会开往曼哈顿。坐车需要付费2.75刀,而且需要在候车亭旁边的自动售票机处购买,而且现金支付只能用0.25刀的硬币。这也是为什么我要凑硬币。M60SBS有点像巡回线路。从百老汇106街开出后,在机场转一圈就又会开回百老汇106街。Terminal A是它在机场的最后一站,因此上车后我就不需要跟着它周游航站楼。一个小时候我就出现在了百老汇116街。

回程

在纽约度过愉快而难忘的一周多之后,我不舍地踏上了归途。女朋友把我送到106街的起点站,直到看着我的车离开才走。其实我也很不舍的。希望以后能有多些机会发这样的博文,能有多些时间和机会陪她。

M60SBS这回带我把LGA的所有Terminal都转了个遍。除了Termnial A以外到处都是大工地,整个车程花了我整整一个半小时。

因为坐bus的时间太长了,这次我没什么心情参观,先check in再说。一开始我先走错了,去了阿拉斯加航空的柜台,然后被引导去了JetBlue。JetBlue的两位小姐姐在自助值机机器前似乎已经恭候多时。她们拿了我的护照帮我在机器上操作。结果机器却现实什么什么鬼超时。吓得我赶紧去人工柜台。这次依旧是把两段的登机牌都打了出来。接下来就是过安检了。过安检时有个大妈盘问。大妈祝我新年快乐。这是我第一次接受到美国人的春节祝福,也祝她春节快乐。

Terminal A一共只有六个还是五个登机口,而且看起来登机口和航班的对应关系非常固定。我去的时候是上午,但是看到每个登机口都把该口全天的信息都列了出来。其中有一个登机口是阿拉斯加航空专用的,剩下的则都归属于JetBlue。

西棕榈滩

此外还有一个小餐馆和一个小商店。商店里有卖纽约的一些纪念品但是我看不上眼。我觉得远不如波士顿的纪念品好。当然了,大部分的商品,仔细看都会发现是made in china,着实让人缺少购买欲望。小餐馆同样是有酒供应,而且还有几台电视在直播着各路体育赛事。冬奥会那自然也是要有的。在美国的时间里我还是能处处感受到美国人的体育热情的。不过因为在机场里,电视都不方便开声音。所有的直播节目都是有个人工智能语音识别的字幕打出。我暂时还不知道是谁提供的这个服务。我的航班有一些延误,所以我还有一些时间吃掉女朋友给我的爱心草莓。

登机后的第一件事还是连wifi。

Fly-Fi

Fly-Fi的速度相当不错,刷朋友圈也毫无压力。JetBlue的所有机上内容都是英西双语的。

目的地

我的航班的起飞时间大约延误了半小时,但是JetBlue依然致歉并且在登机口放了许多袋小饼干,可以随便拿。这是我在去程时所没有的。此外这一程似乎还可以免费点饮品。乘务人员在巡航时会逐个问要不要喝什么。然而我什么也没点,因为我怕收费。按照降落时间算的话,其实最后还是准点到达了。

即将起飞

在云端

到达

在BOS,老同学朱明黑已经等候多时。他在Brandeis深造,上午刚从芝加哥回到波士顿。我借着这个转机的机会与他在机场小聚。为此我出了安检区域。在美国,国内航班的出发和到达口区分并没有那么仔细。出发口往往也可以作为到达口使用。因此到达了其实就是进入了出发时的安检区内。一般来说安检区内的商铺和食肆要比安检区外多很多。然而他回程选择的是美联航,不是JetBlue,于是我们就不能在安检区内相聚了。最后我们在Terminal E的一家餐馆吃了午饭。感谢明黑请客。

炸圈圈

这道菜是明黑点的。实际上里面什么都有:洋葱,鱿鱼甚至酸黄瓜。一开始他并不知道,后来他专门挑出酸黄瓜出来不吃。于是酸黄瓜都被我吃了,虽然我也不爱吃。

龙虾卷

在波士顿当然要尝一下龙虾卷了!不过这个做得一般,因为用了很多钳子上的肉。那里的肉没有尾巴的好吃。此外我还点了IPA,服务员说是本地啤酒,味道不错。

吃完后我就和明黑分别了。我独自一人过了安检。这次排队的人不少。值得一提的是似乎没有“出境”这个环节。因此我的行李也是真正的直达,不需要我在BOS领取和重新交运。

过了安检后我就听到广播在找我,吓死我了。我以为已经催促登基了,真是皇帝不急太监急啊。原来是所有中转的乘客,海航都需要确认一下并且换成海航的登机牌。

换好登机牌后,距离登机还有一些时间。我又四处转了转。BOS有一家明黑口中的“网红餐厅”Legal Sea Food,在每个航站楼都有。于是我也去看了看他们的菜单。

菜单

如果不是已经饱了我真的想试试Crab Cake。

此外我还逛了逛免税店。免税店恐怕是整个机场唯一一家具备完全中文服务且可以使用支付宝的店,可见中国人的购买力是多么强大。售货员十分热情然而我只是想逛逛。

我在Hudson买了个红袜队的棒球,14.95刀,作为给钟老板迟到的生日礼物。

航站楼

回程依旧是没有Wifi的787-8。而且我的PTV的触摸屏和手柄都是坏掉的,以至于完全不能操作。我也懒得和乘务人员反映了,就这样吧。我估计很长一段时间里我都不会再选择海航了。

下厨记 | 煎厚切牛排

今天讲讲怎么煎牛排。

学做这个一开始的原因很简单,就是单位发了节日礼物,是一张食品兑换券。我选了有牛排的,于是就学了煎牛排。

话不多说,进入正题。首先我们要准备原材料,包括牛排和一些调味料。

牛排我选的是来自澳大利亚的一块安格斯西冷(sirloin)。我从盒马生鲜买的,当时的价格好像是75左右两片。这个价格大概只能买到草饲的牛肉,不过已经很不错了。如果还想要便宜的,我建议买嫩肩(flat iron)。一般来说买西冷或者是肋眼(rib eye)都不会错。

牛排

背面

这块牛排是天谱乐食(tender plus)特供给盒马的,质量还不错。在包装背面上你可以看到天谱乐食的logo。国内进口牛排的厂商里,天谱乐食和春秋禾牧的比较好。别的厂商的肉其实也不错,但是厚度并不太够。一般来说,真正原切的西冷或者肋眼牛排的价格大约至少要30元200g,根据饲养方式和牛品种的不同,价格会各有不同,最高应该不超过200元。不要买任何手工静微腌牛排。

买了牛排后,通常是处于冷冻状态。如果是冷鲜的,那很棒,你省去了我接下来所说的前两个步骤,可以直接从腌制开始了。

所以第一步自然是解冻啦!解冻后用厨房纸巾将肉表面的血水吸干,如果不这么做的话,做出来的牛排会比较腥。如果你不太能接受它的腥味,这一步是必须要做的。如果你买的是冷鲜肉,可以考虑忽略这一步。经过处理后的肉大概像图上这样子。

牛排

从侧面来看的话大概会是这个厚度

侧面

接下来看看我们需要的调味料。这一环节好像应该更早一些展现?罐子里的是盐和黑胡椒粒。上面的那块是黄油。迷迭香和百里香也可以用新鲜的,这两样在京东或者麦德龙可以买到。我在这里用了芥花油,实际上别的食用油也可以,但是我不太建议使用味道比较重比较突出的食用油。

调味料

除此以外我们还需要一口铸铁平底锅,我用的是Lodge的。在此感谢我的学弟大沈帮我把它从美国背回来。我们还需要一个烤箱,我用的是美的的T3-L324D。

锅

然后我们给牛排的表面都抹上黑胡椒和盐,并腌制一会儿,不要超过半小时。

腌制

然后让牛排进烤箱,温度120摄氏度,烤25-35分钟,这取决于你的牛排的厚度。这一步对于厚切牛排是很关键的,如果不烤一下而是直接下锅煎的话,很容易会出现外面焦里面生的现象。

烤箱

在这段时间里我们可以准备一下别的调味料。切一点黄油,拍两瓣大蒜。

大蒜和黄油

然后小火把油烧热,热到冒烟的程度。不知道你们能不能在图里看到冒烟呢?

冒烟

将烤过的牛排放入锅中,30秒后翻面。翻面后在旁边放入黄油大蒜迷迭香百里香。迷迭香和百里香的量取决于你想腥一些还是不腥一些。放得多有助于除腥但是也可能盖住原有的味道。然后等待黄油融化,倾斜你的锅。此时肉汁,油都会往下流。用一个小勺子舀起汁水,反复往牛排上浇,大约十几秒。然后你可以夹起牛排,把侧面也煎下。此时你可以关火并且将牛排移到砧板上或者盘子里冷却。这几步需要非常快地操作所以我就没办法拍什么照片了。

锅里的牛排

接近成品

冷却

冷却大约5-10分钟后就可以切片了,锅里的汁可以浇到肉上。不过通常我都是一个人吃,所以就直接用刀叉在锅里切着吃了。这一次我做得并不太好,火太大了,所以就放上之前比较成功的一次的图。

成功

切一块

参考视频