月度归档: 2024年4月

一个开发中用到的报文压缩实现

之前开发中用到的一个报文压缩的实现方案,简单在这里单独提出来写一下,分为Java和JS两版代码。

Java版代码是服务端使用,用来在各服务端之间发送接收报文使用。JS版是在前端页面上用来查看或者调试接口报文使用的。

Java代码,压缩和解压缩方法


/**
 * 
 * 功能描述:字符串压缩 <br>
 * 将字符串压缩
 *
 * @param str 待压缩的字符串
 * @return 压缩后的字符串
 */
@SuppressWarnings("restriction")
public static String gzip(String str) {
    // 创建字符流
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    GZIPOutputStream gzip = null;
    try {
        // 对字符串进行压缩并写入压缩流
        gzip = new GZIPOutputStream(out);
        gzip.write(str.getBytes());
    } catch (IOException e) {
        String errMsg = e.getMessage();
        logger.error(errMsg);
    } finally {
        if (gzip != null) {
            try {
                gzip.close();
            } catch (IOException e) {
                String errMsg = e.getMessage();
                logger.error(errMsg);
            }
        }
    }
    // 返回压缩后的字符串
    return new sun.misc.BASE64Encoder().encode(out.toByteArray());
}

/**
 * 
 * 功能描述: 字符串解压<br>
 * 将字符串解压
 *
 * @param str 待解压的字符串
 * @return 解压后的字符串
 * @throws Exception
 */
@SuppressWarnings("restriction")
public static String gunzip(String str) {
    // 校验压缩数据
    if (str == null) {
        return null;
    }

    // 创建读取流
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ByteArrayInputStream in = null;
    // 解压缩流
    GZIPInputStream ginzip = null;
    byte[] compressed = null;
    // 原始字符串
    String decompressed = null;

    try {
        // 进行解压缩操作
        compressed = new sun.misc.BASE64Decoder().decodeBuffer(str);
        in = new ByteArrayInputStream(compressed);
        ginzip = new GZIPInputStream(in);

        byte[] buffer = new byte[BYTE_SIZE];
        int offset = -1;
        while ((offset = ginzip.read(buffer)) != -1) {
            out.write(buffer, 0, offset);
        }
        decompressed = out.toString();
    } catch (IOException e) {
        String errMsg = e.getMessage();
        logger.error(errMsg);
        logger.error("解析压缩字符串异常", e);
    } finally {
        if (ginzip != null) {
            try {
                ginzip.close();
            } catch (IOException e) {
                String errMsg = e.getMessage();
                logger.error(errMsg);
            }
        }
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                String errMsg = e.getMessage();
                logger.error(errMsg);
            }
        }
        try {
            out.close();
        } catch (IOException e) {
            String errMsg = e.getMessage();
            logger.error(errMsg);
        }
    }
    // 返回原始字符串
    return decompressed;
}

JavaScript代码,js的压缩解压缩需要调用到pako包的内容,可以在https://www.bootcdn.cn/pako/ 上找到需要的版本引用,或者简单点直接把需要min版本代码复制到你需要的页面里, https://cdn.bootcdn.net/ajax/libs/pako/2.0.4/pako.min.js,另外也用到浏览器自带的base64加密解密的方法btoa和atob

Continue reading

一次序列化与反序列化引发的BUG排查

BUG现场

这是一份若干年前的历史代码,当时的同学写这份代码的时候设计思路是这样的,我复现这份BUG现场的代码如下

要点1 有一个用来传输数据的DTO,大致如下

@Data
public class CategoryBrandBu implements Serializable {

    protected static final Map<String,String> BRAND_RELATION_MAP = new HashMap<>();
    public Map<String,String> getRelationMap(){
        return BRAND_RELATION_MAP;
    }
    private Long id;
    private String brandCode;
    private String brandName;

    public String getBrandName() {
        if (StringUtils.isBlank(brandCode)){
            return StringUtils.EMPTY;
        }
        return BRAND_RELATION_MAP.getOrDefault(brandCode,StringUtils.EMPTY);
    }
}

DTO中声明了一个静态常量BRAND_RELATION_MAP的HashMap,用来保存某个不常更新的表中的映射信息,表中的数据基本只有几十条,所以放在这用来获取品牌名称

要点2. 再另外起一个定时job,定时来刷新这个HashMap中的映射关系数据,代码大意如下

@Configuration
public class ScheduledConfig {

    @Scheduled(fixedDelay = 5000)
    public void updateMap(){
        Random random = new Random();
        int i = random.nextInt(1000);
        System.out.println("updated: "+i);
        CategoryBrandBu dto = new CategoryBrandBu();
        dto.getRelationMap().put("ABC", String.valueOf(i));
    }
}

其中代码先new CategoryBrandBu()getRelationMap(),再put的操作虽然看起来比较挫,不过嗯、至少确实还是有用的(原来的代码就是这样),这个不是重点,另外fixedDelay = 5000是我特地调整的5秒刷新一次。

要点3. 接下来一个至关重要的东西,当前Application中提供了对外服务RPC(某自研RPC框架)接口用于查询相关数据,其中有一个接口的返回值中就用到了当前涉及的对象CategoryBrandBu。

RPC基础服务类中将请求参数序列化之后,发送到目标Application中。目标Application接收到请求之后,将请求信息解析之后调用对应bean实例的对应方法,且同时反序列化对应的请求参数为对应方法的java参数对象。这里这序列化和反序列化中用的fastjson(能用,不过也不是很高明的样子)。

BUG表现情况

我们的@Scheduled定时任务无论怎样刷新CategoryBrandBu类中静态成员变量Map的信息,外部应用实例(B)通过RPC访问过来的之后得到的结果永远都是第一次初始化之后得到Map中的值(其实也不是第一次初始化的值,后面我再说到)

举个栗子来说明下,首先当我们的Application(A)启动之后,当前静态成员变量上Map被初始化成了如下

(A)->AAA
(B)->BBB
(C)->CCC

此时外部的应用实例(B)通过RPC访问到当前Application(A)的对应接口之后,得到了这个DTO序列化的结果,其中包含已经被序列化了的BRAND_RELATION_MAP,而我们对应外部应用实例(B)的系统也恰巧用了同样的DTO的java类文件,于是在外部应用实例(B)接收到序列化的返回结果同时,也接收到了序列化的BRAND_RELATION_MAP,外部应用实例(B)对数据进行反序列化,同时也对外部应用实例(B)中的CategoryBrandBu类中的静态成员变量BRAND_RELATION_MAP进行赋值。

Continue reading