概述
在很多场景下,需要进行分析字节数据,但是我们存起来的字节数据一般都是二进制的,这时候就需要我们将其转成16进制的方式方便分析。比如在做音视频的时候,需要看下我们传输的视频h264数据中是否有对应的I帧或者B帧等数据,做ASM插桩的时候,可以使用输出类结构的16进制辅助分析了解问题。测试投屏的时候尤其有用,比如说投屏到电视上后,发现没有画面,或者是画面很卡顿,这时候就需要对我们传输的视频数据做分析,所以我们将视频的数据转成16进制的形式,并且以一定的格式输出,可以很方便的帮助我们定位问题。本文主要介绍如何使用Java将字节数组格式化成16进制的格式并输出。
输出效果展示
上图是以一个class字节码文件的16进制的格式输出,下面就介绍如何将我们的字节数组输出成16进制的格式
代码实现
首先我们定义一个类,用于生成一个class文件,作为我们格式化的对象。读者使用的时候可以是其他数据,只要是字节数组的方式提供就行了,这里仅仅作为演示
public class ASMDemoEntity {private int intNum = 10;private static final String staticString = "hello world";public void fun() {System.out.println("I am fun");}public int add(int a, int b) {return a + b;}public static void main(String[] args) {System.out.println("a+b = " + new ASMDemoEntity().add(1, 2));new ASMDemoEntity().fun();}
}
然后定义一个枚举类,定义我们格式化后的16进制数据的输出样式以及分隔符,如下所示:
public enum HexFormat {// 无分隔符分别展示0,8,16,32列FORMAT_HEX_0("", 0),FORMAT_HEX_8("", 8),FORMAT_HEX_16("", 16),FORMAT_HEX_32("", 32),// 带空格分隔符分别展示0,8,16,32列FORMAT_HEX_SPACE__0(" ", 0),FORMAT_HEX_SPACE_8(" ", 8),FORMAT_HEX_SPACE_16(" ", 16),FORMAT_HEX_SPACE_32(" ", 32);public final String separator; // 分隔符public final int column; // 展示几列HexFormat(String separator, int column) {this.separator = separator;this.column = column;}
}
如上所示:FORMAT_HEX_0就表示展示0列,无分隔符,用一行展示完所有的16进制数据,而FORMAT_HEX_SPACE_32 表示以一个空格做分隔符,展示32列,就如我们本文展示的效果图一样。
接着我们使用一个FileUtil类去读我们生成的.class文件:
public class FileUtil {public static String getFilePath(String relativePath){URL resource = FileUtil.class.getResource("/");String dir = resource == null? "" : resource.getPath();return dir + relativePath;}public static byte[] readBytes(String filePath){File file = new File(filePath);if(!file.exists()){throw new IllegalArgumentException(filePath + "not exist");}InputStream in = null;try {in = Files.newInputStream(file.toPath());in = new BufferedInputStream(in);ByteArrayOutputStream bao = new ByteArrayOutputStream();IOUtil.copy(in,bao);return bao.toByteArray();} catch (IOException e) {e.printStackTrace();}finally {IOUtil.closeIO(in);}return null;}
}
使用一个IOUtil类做复制字节数组和关闭IO流
public class IOUtil {private static final int EOF = -1;private static final int BUFFER_SIZE = 1024 * 4;public static long copy(final InputStream input,final OutputStream output) throws IOException {long count = 0;int n;byte[] buffer = new byte[BUFFER_SIZE];while (EOF != (n = input.read(buffer))) {output.write(buffer, 0, n);count += n;}return count;}public static void closeIO(final Closeable closeable) {if(closeable != null){try {closeable.close();} catch (IOException ignored) {}}}
}
最后使用格式化工具类将字节数组格式化成16进制的样式并按照指定的格式输出
public class HexUtil {public static String hexFormat(byte[] bytes,HexFormat format){String separator = format.separator;int column = format.column;return hexFormat(bytes,separator,column);}private static String hexFormat(byte[] bytes, String separator, int column) {if(bytes == null || bytes.length < 1) {return "";}StringBuilder sb = new StringBuilder();Formatter fm = new Formatter(sb);int length = bytes.length;for (int i = 0; i < length; i++) {int val = bytes[i] & 0xFF;fm.format("%02X",val);if(column > 0 && (i+1) % column == 0){fm.format("%n");}else{fm.format("%s",separator);}}return sb.toString();}
}
在上面代码中的代码是Formatter.format()方法,它的作用是格式化我们的字节数组,我们传入的格式中带有%…X…时表示输出16进制数据,具体的定义如下:
%X: 正常输出16进制数
%NX: 十六进制数,输出N位,如果本身大于N位,正常输出,比如format("%2X",val);
表示输出2位16进制数,若本身大于2位,正常输出
%NBX: 十六进制数,输出N位,不足N位就补B,若本身大于N位,就正常输出,比如format("%02X",val);
代表输出2位的16进制数,如果不足2位就补0,如果本身大于2位,就正常输出
演示将一个class文件的二进制数据转成16进制数据并格式化后输出:
public class HexFormatMain {public static void main(String[] args) {String relativePath = "org/example/entity/ASMDemoEntity.class";String filePath = FileUtil.getFilePath(relativePath);System.out.println("file path: " + filePath);byte[] bytes = FileUtil.readBytes(filePath);String hex = HexUtil.hexFormat(bytes, HexFormat.FORMAT_HEX_SPACE_32);System.out.println("class文件的16进制: ");System.out.println(hex);}
}