从利用Arthas排查线上Fastjson问题到Java动态字节码技术(上)

没被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

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信息,同时也并没有打印任何帮助信息;那么就需要对源码进行在线编辑,打印类信息,同时热部署;

  • 热部署需要先找到load当前这个controller的classloader
[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;
}
  • 再使用同一个classloader在线编译
mc -c 18b4aac2 /tmp/DriverController.java -d /tmp
  • 最后redefine直接热部署新的controller
redefine /tmp/com/rexsoft/demo/controller/DriverController.class

此时,当我们再次调用get方法,就可以在输出中看到有用的信息了

cached object class is: class com.rexsoft.demo.vo.Driver

所以 由重构变换class类信息导致fastjson无法反序列在线旧数据导致问题 的原因就基本石锤了…



Arthas是十分优秀的工具,下一篇再深入Arthas源码看看Java动态字节码技术。

你可能感兴趣的:(架构,编码,设计模式,源码,Arthas,fastjson,动态字节码,源码,重构)