前言
之前一直对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,对字节数组进行解码,最终就能拿到和我传递的字符串一样的数据了
总结
以上是我自己的一些理解,不确定是否存在误解,但是可以明确的是在网络中进行交互时,要先确定自己传递的数据使用的编码集,然后告诉对方我使用的编码集,那么最终才能正常的进行交互,有时候如果使用默认的编码集,那么在双方默认编码集不一致时,就可能存在问题了,所以应该牢记,在任何时候都不要使用默认的编码集,而是应该自己指定