没被Fastjson搞过的程序员不是合格的程序员 ---- 手动狗头
开个玩笑,福报厂的同学们不要喷,Fastjson是非常优秀的工具!
先简短复盘下之前遇到的一个线上问题:随着业务发展项目A日渐臃肿,已经成为人人都头疼的big ball of mud 大泥球,遂决定对其进行重构,细节包括服务拆分与部分逻辑重构。虽然我不是这块业务的技术owner,但这类重构任务自然还是我来负责,同时在业务需求排队与原owner看戏心态的情况下,留给我从头熟悉与重构的时间并不多… 重构过程就不在这赘述了,虽然发现和解决了很多问题,但还算顺利,这中间花费时间最多的就是测试,读过老马的书或真正参与过重构的同学都知道,任何重构都建立在测试的基础上,没有单元测试、集成测试、回归测试,都是在自寻死路… 所以当我拿到项目A源码看到那空空的test文件夹时,我陷入了沉思… 但还是要硬着头皮上的,做了各种我认为有必要的测试… 时间来到了上线那一天,问题还是出现了… 问题是由新项目无法使用fastjson反序列化线上redis缓存数据导致的… 百密一疏,功亏一篑,在本地与测试环境做了N种测试,但并没考虑到线上redis缓存中还有部分老数据…
大厂的同学们都很喜欢搞脚手架,喜欢在开源工具上包装,原项目A中缓存部分就有这么一个Redis缓存脚手架工具;脚手架如果设计的好那么可以大幅提高开发效率,但如果设计的不好那对于使用者来说就是黑盒的!
这个Redis工具在序列化的时候使用json格式来做存储,使用的是Fastjson;而Fastjson在序列化Redis对象的时候,会记录class信息,所以一旦class信息对不上,那么序列化反序列化就会失败…
public class GenericFastJsonRedisSerializer implements RedisSerializer<Object> {
/** 略 */
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
return new byte[0];
}
try {
return JSON.toJSONBytes(object, SerializerFeature.WriteClassName);
} catch (Exception ex) {
throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);
}
}
/** 略 */
}
虽然缓存功能也包含在测试范围内了,但不清楚这个脚手架的细节让我还是掉坑里了…
Anyway,吃一堑长一智,这就是成长啊 ---- 狗头
做个最简单的demo来重新这个问题:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
//想象一下这个东西被包装了无数层,让你看不到它的细节忘记了它本来是干啥的..
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory, StringRedisSerializer stringRedisSerializer, GenericFastJsonRedisSerializer genericFastJsonRedisSerializer){
RedisTemplate<String,Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(genericFastJsonRedisSerializer);
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
@Bean
public StringRedisSerializer stringRedisSerializer(){
return new StringRedisSerializer();
}
@Bean
public GenericFastJsonRedisSerializer genericFastJsonRedisSerializer(){
return new GenericFastJsonRedisSerializer();
}
}
操作也很简单,一个set一个get:
@RestController("/")
public class DriverController {
public static final String DRIVER = "driver_";
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "/set", method = RequestMethod.POST)
public void setDriver(@RequestBody Driver driver){
redisTemplate.opsForValue().set(DRIVER+driver.getDriverId(), driver);
}
@RequestMapping(value = "/get", method = RequestMethod.GET)
public Driver getDriverById(@RequestParam(value = "id") long id){
return (Driver) redisTemplate.opsForValue().get(DRIVER+id);
}
}
所以当Redis中存储的是由原项目A使用com.didichuxing.pkg1.Driver
set的Driver数据,但新项目使用 com.didichuxing.pkg2.Driver
来get Driver的时候,就会出现反序列化问题
在排查问题的时候就引入了两个疑问:如何确定当前线上的这个controller使用的是哪个Driver类?在没有打印任何帮助信息的情况下,如何对controller进行编辑同时进行热部署?
这就用到了阿里开源的Java在线诊断工具Arthas
Arthas是一个建立在Java动态字节码技术之上的一个诊断工具,不仅功能强大,使用也极其简单,基础功能诸如attach后查看JVM信息与监控方法调用的输入输出,到复杂的redefine源码热部署…
这里先解决上面抛出的那两个问题,如何在线查看源码与在线编辑热部署
jad
来在线反编译这个controller查看具体使用的是哪个Driver类[arthas@89334]$ jad com.rexsoft.demo.controller.DriverController
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@51132775
package com.rexsoft.demo.controller;
import com.rexsoft.demo.entity.Driver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController(value="/")
public class DriverController {
public static final String DRIVER = "driver_";
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value={"/get"}, method={RequestMethod.GET})
public Driver getDriverById(@RequestParam(value="id") long id) {
/*25*/ return (Driver)this.redisTemplate.opsForValue().get(DRIVER + id);
}
/** 略 */
这就确定了再反序列化的时候,新项目尝试使用com.rexsoft.demo.entity.Driver
来进行反序列化;但此时我们并不清楚redis缓存中存储的class信息,同时也并没有打印任何帮助信息;那么就需要对源码进行在线编辑,打印类信息,同时热部署;
[arthas@89334]$ sc -d com.rexsoft.demo.controller.DriverController | grep classLoaderHash
classLoaderHash 18b4aac2
rex@192 /tmp % cat DriverController.java
/** 略 */
@ReqestMapping(value={"/get"}, method={RequestMethod.GET})
public Driver getDriverById(@RequestParam(value="id") long id) {
Object object = this.redisTemplate.opsForValue().get(DRIVER + id);
System.out.println("cached object class is: "+object.getClass());
Driver rst = (Driver)object;
return rst;
}
mc -c 18b4aac2 /tmp/DriverController.java -d /tmp
redefine
直接热部署新的controllerredefine /tmp/com/rexsoft/demo/controller/DriverController.class
此时,当我们再次调用get方法,就可以在输出中看到有用的信息了
cached object class is: class com.rexsoft.demo.vo.Driver
所以 由重构变换class类信息导致fastjson无法反序列在线旧数据导致问题 的原因就基本石锤了…