鲁迅的名篇《孔乙己》中,有这样一个桥段,孔乙己问作者是否知道茴香豆的“茴”字,有四种写法,用于讽刺孔乙己这类落魄文人的迂腐。我们在coding时,为实现某一逻辑,也会存在各种各样的方案,而每种方案的背后,除了有各自的特点之外,也或多或少与给出这些方案的人的技能、经历、思考方式有关,本文试图以亲身经历的一个小故事,来探究编程领域内茴字N种写法背后的故事。

起因

一次代码Review的过程中,看到了类似下面的代码

1
2
3
4
5
6
7
8
9
public OrderApiResponse getOrder(String orderId) {
var headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

var request = new HttpEntity<>(Collections.singletonMap("orderId", orderId), headers);
var response = restTemplate.exchange(orderApiUri, HttpMethod.POST, request, String.class);
return JSON.parseObject(response.getBody(), OrderApiResponse.class);
}

放眼望去,感觉没什么不对;仔细端详,又感觉哪里不对….为什么要用String作为response的参数化类型呢?另外这个JSON.parseObject也不是Spring Framework内置的Jackson啊,仔细一看原来是我天朝上国的fastjson,为什么要放弃内置的Json方案而再引入一个第三方的呢?

赶紧找来开发人员小G请教原委,

Order API Response的属性名是以大写字母开头的,Jackson默认不能处理,fastjson默认可以处理,所以引入了fastjson并以String类型接收响应。

小G的回答

Hmmm,貌似很有道理的样子,但仔细推敲起来,至少还有其他问题,于是我回复道,

  1. 假设Order API返回10000个属性,我们只用10个,使用String全部接收响应,岂不是很浪费?性能是不是也不高?
  2. 假设使用fastjson不可避免,那么是否有更好的解决方案呢?
我的回复

开发人员G于是进入了冥思苦想状,而我的思绪也逐渐模糊,从fastjson转向了RestTemplate

第二种写法

无论使用Spring MVC来做为Server端,还是使用RestTemplate来做为Clinet端,Spring Framework都是在使用HttpMessageConverter来处理http消息。

Spring Framework内置了很多Http Message Converters,而MappingJackson2HttpMessageConverter就是内置使用Jackson来处理Json的Converter,假设我们要使用fastjson来替换Jackson,最好的办法是直接在替换掉MappingJackson2HttpMessageConverter,这样在使用RestTemplate来请求API时,不需要手动处理response的反序列化,本来,序列化方式也应该对客户端代码是透明的。那么问题来了,如何替换掉MappingJackson2HttpMessageConverter呢?来,动手吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OrderApiClient {

private final RestTemplate restTemplate;

public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
var converter = new FastJsonHttpMessageConverter();
this.restTemplate = restTemplateBuilder.build();
var messageConverters = this.restTemplate.getMessageConverters();
var converters = messageConverters.stream()
.filter(item -> !(item instanceof MappingJackson2HttpMessageConverter))
.collect(Collectors.toList());
converters.add(converter);
this.restTemplate.setMessageConverters(converters);
}

public OrderApiResponse getOrder(String orderId) {
//...
}
}

Seems better now. 开发人员G对我的这个方案很满意,且慢,我还有问题呢!

  1. 既然Spring已经内置了Jackson来处理Json,我们为什么要画蛇添足地引入另外一种Json处理包呢?
  2. 既然fastjson能做到,凭什么Jackson做不到呢?
我的新问题

路漫漫其修远兮,吾将上下而求索。

第三种写法

在我苦苦求索不得要领之时,小R兴冲冲跑过来说道,

  1. Jackson有对应的MapperFeature来支持兼容大写字母开头的属性名;
  2. 我们使用的是Spring Boot,可以直接使用properties来设置该属性,不需要额外写代码;
小R的回复

综合小R的回复,我们只需要在application.properties文件里,加上这么一句就好啦,

1
spring.jackson.mapper.accept_case_insensitive_properties=true

So Easy, 难道不是吗?可是敏而好学,不耻下问的我,又有新问题了,

  1. 如果兼容非Java Bean规范的属性名,想必要付出额外的性能代价吧?
  2. 在我们的应用中,有很多外部API调用的场景都使用到了RestTemplate,但并不是所有的这些场景都需要兼容首字母大写的属性名吧?
  3. 配置在application.properties中的mapper_feature应该是全局设置,对所有的RestTemplate都起效吧,那岂不是说不需要兼容首字母大写的API调用在使用RestTemplate时也需要承担性能下降的代价?
我的新问题

显然,还得继续求索啊。

第四种写法

为了不使其他使用RestTemplate的API Client收到全局配置的影响,那么就不能在全局配置,配置应该发生在个体之上。在这个思路的只因下,小R在OrderApiClient自己的RestTemplate上下手了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OrderApiClient {

private final RestTemplate restTemplate;

public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {

this.restTemplate = restTemplateBuilder.build();

var messageConverter =
this.restTemplate.getMessageConverters()
.stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(MappingJackson2HttpMessageConverter.class::cast)
.findFirst()
.orElseThrow(() -> new RuntimeException("MappingJackson2HttpMessageConverter not found"));

messageConverter.getObjectMapper().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);

}

public OrderApiResponse getOrder(String orderId) {
//...
}
}

很好,所有的问题都解决了,需求实现了,性能还不错,更没有污染全局,小R,V5!
然而,既然已经走到了这里,还能更进一步吗?

更进一步

正如上面所说,该做到的其实都已经做到了,无以复加了。可是患有强迫症的我,仍然觉得代码在结构和写法上仍然还有可调整的空间,

  1. 我们真的需要那个RuntimeException吗?
  2. 基于目前的代码,所有的Converters里真的会没有找到MappingJackson2HttpMessageConverter
  3. 如果以上两个问题的答案是No,那么代码是否能写的更优雅呢?
我的新问题

这次不劳烦小G和小R动手了,我来亲手把它写出来吧,更进一步之后大概是这样子的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class OrderApiClient {

private final RestTemplate restTemplate;

public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {

this.restTemplate = restTemplateBuilder.build();

this.restTemplate.getMessageConverters()
.stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(MappingJackson2HttpMessageConverter.class::cast)
.findFirst()
.map(MappingJackson2HttpMessageConverter::getObjectMapper)
.ifPresent(objectMapper ->
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES,true));

}

public OrderApiResponse getOrder(String orderId) {
//...
}
}

好了,终于没有然后了。

退而思之

让我们复盘一下整个故事的发展过程,

  1. 引入fastjson并单独使用JSON.parseObject来处理非标准变量名;
  2. 使用fastjsonFastJsonHttpMessageConverter来替换自带的MappingJackson2HttpMessageConverter
  3. 干掉fastjson,在application.properties中配置全局的mapper_feature来适应非标准变量名;
  4. 直接修改OrderApiClient自己的RestTemplatemapper_feature来适应非标准变量名,并避免污染全局;
  5. 让代码变得更简洁、优雅。

从以上故事的发展过程,我们又可以总结出代码实现的不同等级,

  1. 实现了功能,但性能一般;
  2. 实现了功能,性能尚可,但引入了不必要的依赖;
  3. 实现了功能,性能尚可,但副作用较大,污染了全局;
  4. 实现了功能,性能不错,无污染,无副作用,代码略显冗余,或可读性差;
  5. 实现了功能,性能不错,无污染,无副作用,代码简洁优雅、易读易懂。

我们在做代码实现时,相信我们无一例外,都是奔着最高标准的第五点去的,没人从主观上就想写烂代码。但由于每个人的技能、经历、思考方式不同,写出的代码也自然不同,其质量也会大概率地分布在这五个等级内。我不能要求每个人写出的每行代码都是最高等级的,但我希望每行代码都能更趋近于最高标准。

为你们总结一下吧,小G,小R,新时代的我们,应该如何去写高质量的代码

  1. 首先要技术过硬,知其然亦要知其所以然;
  2. 要有全局观念和大局意识,从小处着手但从大处着眼;
  3. 多写亦要多想,学而不思则罔,思而不学则殆;
  4. 最后一点,也是最为重要的一点,对代码,一定要保留一份敬畏之心,一行代码写下去,即便不关乎个人生死,也会关乎项目存亡,不可不慎,不可不察。
我的总结

参考资料

  1. springboot2集成RestTemplate并使用fastjson序列化对象
  2. springboot之jackson的两种配置方式
  3. Make JSON payload fields case insensitive when mapping to Java Object in REST API developed using SpringBoot
  4. How can we configure the internal Jackson mapper when using RestTemplate?