浅谈java中的编码

前言

之前一直对java中的编码一知半解,这一次在一次工作中,遇上了一个让我疑惑的问题,最终发现是由编码导致的,借此机会对编码有了进一步的理解

问题

本次的问题是由一次对字符串的MD5的功能引起的,代码如下

public static String MD5encryption(String plain) {
        String re_md5 = new String();
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(plain.getBytes());
            byte b[] = md.digest();
            int i;
            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < b.length; offset++) {
                i = b[offset];
                if (i < 0)
                    i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            re_md5 = buf.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return re_md5;
    }

问题是这样的,在和别人交互时双方需要对数据进行校验,对方会传递给我一个md5加密后的字符串,而我要根据对方加密的规则把数据加密后生成md5和对方的md5进行对比,一致则说明权限认证通过,加密的过程中会掺杂一个密钥,只要密钥不泄露,那么还是相对安全的

但是在我加密的过程中发现生成的md5始终与对方的不一致,但是我获取了对方加密的字符串和我自己要加密的字符串,他们是一致的

那么有哪些可能呢?
首先我以为是不是字符串转译的过程导致的,虽然看起来一致但是实际上不一致,但是其实字符串也是一致的,后来我测试了自己使用main方法和启动项目后使用postman调用接口的方式对字符串进行md5加密,发现他们的结果居然也不一致,那么我就产生了疑问

仔细看了下对方提供给我的md5加密方法,最终发现plain.getBytes()是没有传递编码格式的,那么很可能是这个问题导致的了

那么为什么main方法和启动项目调用的结果不一致呢,看一下源码会发现,默认会调用Charset.defaultCharset().name()来进行获取默认的编码,然后查了一下这个方法受哪些影响,即可能和操作系统有关,也可能和jvm有关,也可以通过启动参数设置等都有可能存在关联,而问题就出在这里,因为对方是通过UTF-8转换为字节数组的,而我启动项目后获得的值是gb2312,这就导致了最终转换为的字节数组不一样,导致md5值不一样

static byte[] encode(char[] var0, int var1, int var2) {
        String var3 = Charset.defaultCharset().name();

        try {
            return encode(var3, var0, var1, var2);
        } catch (UnsupportedEncodingException var6) {
            warnUnsupportedCharset(var3);

            try {
                return encode("ISO-8859-1", var0, var1, var2);
            } catch (UnsupportedEncodingException var5) {
                MessageUtils.err("ISO-8859-1 charset not available: " + var5.toString());
                System.exit(1);
                return null;
            }
        }
    }

理解

String s = new String(byte[] bt, String code);

之前我的理解是String字符串是有编码生成的,但是这个认知似乎是错误的,我的理解是这样的,String应该是一种展现形式,本身没有编码,而调用上面的方法传递的code应该理解为解码更为合适,应该理解为byte[]是以一种编码格式进行编码的,然后通过创建字符串进行解码转换为字符串显示

http请求过程

以一次rest风格的http请求为例

1、首先我通过postman根据发送一次post请求,传递了一个json格式的body体
2、然后我设置header头中的Content-Type=application/json;charset=UTF-8;
3、然后我在服务端进行接受参数,使用Spring的注解@RequestBody Map<String, Object> map进行接收,注意这里我的Spring版本比较高,低版本可能不一样,spring-webmvc的版本是5.2.12.RELEASE

@PostMapping("/demo")
public String demo(@RequestBody Map<String, Object> map) {
    System.out.println(JSON.toJSONString(map));
    return null;
}

此时我可以获取到正常的数据,但是当我设置Content-Type=application/json;charset=GBK;后,发现中文乱码了,那么这是为什么呢

首先要理解的是Content-Type=application/json;charset=GBK;是传递给服务端,告诉服务端我使用的编码是什么(并不是一定是我真正使用的编码格式),因为网络传输只能传递字节,所以字符串肯定会先被编码为字节,此时因为我使用的是postman,我没有找到哪里可以设置编码,但是服务端如果使用UTF-8进行解码,那么最终是正常的,可得我本地的postman使用了UTF-8编码

然后服务端收到我根据UTF-8编码的字节数组后,又收到了Content-Type中的GBK编码,所以服务端使用了new String(byte[],“GBK”),此时byte数组的编码格式是UTF-8,但是转为String时的解码编码为GBK,那么中文肯定就乱码了

看@RequestBody注解的核心源码发现以下代码

AbstractJackson2HttpMessageConverter的readJavaType方法

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
		
        MediaType contentType = inputMessage.getHeaders().getContentType();
        // 核心1
        Charset charset = this.getCharset(contentType);
        boolean isUnicode = ENCODINGS.containsKey(charset.name());

        try {
            if (inputMessage instanceof MappingJacksonInputMessage) {
                Class<?> deserializationView = ((MappingJacksonInputMessage)inputMessage).getDeserializationView();
                if (deserializationView != null) {
                    ObjectReader objectReader = this.objectMapper.readerWithView(deserializationView).forType(javaType);
                    if (isUnicode) {
                        return objectReader.readValue(inputMessage.getBody());
                    }

                    Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
                    return objectReader.readValue(reader);
                }
            }

            if (isUnicode) {
                return this.objectMapper.readValue(inputMessage.getBody(), javaType);
            } else {
            	// 核心2
                Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
                return this.objectMapper.readValue(reader, javaType);
            }
        } catch (InvalidDefinitionException var9) {
            throw new HttpMessageConversionException("Type definition error: " + var9.getType(), var9);
        } catch (JsonProcessingException var10) {
            throw new HttpMessageNotReadableException("JSON parse error: " + var10.getOriginalMessage(), var10, inputMessage);
        }
    }

核心1处获取到了Content-type中的编码,然后核心2这里isUnicode是判断了是否是UTF-8,UTF-16等编码,如果不是则生成一个新的InputStreamReader并且使用我们指定编码格式

另外通过直接获取流的方式来获取参数测试一下

@PostMapping("/getByte")
    public String getByte(HttpServletRequest request) {
        int len = request.getContentLength();
        try {
            ServletInputStream inputStream = request.getInputStream();
            byte[] buffer = new byte[len];
            inputStream.read(buffer, 0, len);
            String gb2312Body = new String(buffer, "gb2312");
            String utf8Body = new String(buffer, "utf-8");
            String gbkBody = new String(buffer, "gbk");

            System.out.println(Arrays.toString(buffer));
            System.out.println("gb2312Body:" + gb2312Body);
            System.out.println("utf8Body:" + utf8Body);
            System.out.println("gbkBody:" + gbkBody);

        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

发现只有使用utf-8才能拿到非乱码的中文,这也说明了我本地的postman传递时使用的是UTF-8编码,而Content-type传递的编码只是告诉服务端而已,服务端要如何处理取决于服务端

HttpClient案例

再看一下httpClient发起http请求时字符串转换为字节数组时是否也使用了编码呢,以下面的代码为例

public static String postRaw(String url, String data) {
        String result = "";
        try {
            HttpClient httpClient = new DefaultHttpClient();
            HttpPost httpPost = new HttpPost(url);
            // 核心1
            StringEntity postingString = new StringEntity(data, ContentType.APPLICATION_JSON);
            httpPost.setEntity(postingString);
            // 核心2
            httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
            HttpResponse httpResponse = httpClient.execute(httpPost);
            result = EntityUtils.toString(httpResponse.getEntity());
        } catch (IOException e) {
            e.printStackTrace();
            return result;
        }
        return result;
    }

核心1处创建StringEntity有多个方法,也可以只传一个data

public StringEntity(String string) throws UnsupportedEncodingException {
        this(string, ContentType.DEFAULT_TEXT);
    }

在这里插入图片描述
可以看到默认是使用的text/plain,并且是iso_8859_1编码

而我传递参数的contentType 是这样的,可以看到是application/json并且是UTF-8编码
在这里插入图片描述
构造方法

public StringEntity(String string, ContentType contentType) throws UnsupportedCharsetException {
        Args.notNull(string, "Source string");
        Charset charset = contentType != null ? contentType.getCharset() : null;
        if (charset == null) {
            charset = HTTP.DEF_CONTENT_CHARSET;
        }
		// 核心1
        this.content = string.getBytes(charset);
        if (contentType != null) {
            this.setContentType(contentType.toString());
        }

    }

可以看到string.getBytes(charset),使用了我传进来的UTF-8编码进行编码,转换为字节数组进行传递

并且我设置了header头中的content-type=application/json; charset=utf-8,告诉了服务端我使用的编码格式

然后服务端收到我传递的字节数组和content-type后,根据获得的编码utf-8,对字节数组进行解码,最终就能拿到和我传递的字符串一样的数据了

总结

以上是我自己的一些理解,不确定是否存在误解,但是可以明确的是在网络中进行交互时,要先确定自己传递的数据使用的编码集,然后告诉对方我使用的编码集,那么最终才能正常的进行交互,有时候如果使用默认的编码集,那么在双方默认编码集不一致时,就可能存在问题了,所以应该牢记,在任何时候都不要使用默认的编码集,而是应该自己指定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值