目录导读

  • Java二进制及中文转码和校验
    • 1.Java基本数据类型
      • 1.1 基本数据类型占用的存储空间
      • 1.2 Java二进制流
      • 1.3 二进制转换说明
        • 1.3.1 二进制与Base64互转
        • 1.3.2 二进制与十六进制互转
    • 2.Java魔数应用
      • 2.1 文件魔数
      • 2.2 协议魔数
    • 3.Java Unicode编码应用
      • 3.1 Java中文字符转换
      • 3.2 Java中文字符校验
    • 4.参考资料

Java二进制及中文转码和校验

  • 自大学那会学习了Java的8大基础数据类型(boolean/byte/short/char/int/long/float/double)后,毕业后的很多年几乎都不涉及基础数据类型了,尽管选的是Java方向;
  • 工作中的前若干年还尽量避免定义基础类型,比如int数组改成List等,心里一直在告诫自己,Java是面向对象的语言,应该尽量使用面向对象的API和语法,少使用原始的基础类型;
  • 又过了一些年,现在对基础类型有了全新的、自洽的认识,不吐不快,还请诸君斧正;
  • 本文重点关注char和byte类型的编码使用、转换,以及实际业务场景的应用;

1.Java基本数据类型

  • 8大基本数据类型列表

    基本类型 大小(字节) 默认值 取值范围 后缀 示例
    boolean 1 false {false,true} - boolean sign=true;
    byte 1 0 [-128,127] - byte b=125;
    short 2 0 [-32768~32767] - short num=10;
    char 2 ‘\u0000’ [0,65535] - char ch1=‘k’;
    char ch2=‘\u77be’;
    char ch3=‘瞾’;
    int 4 0 [-2^ 31 , 2^31-1] - int count=100;
    long 8 0 [-2^63 , 2 ^63-1] L/l long a=100L;
    float 4 0.0f [-2^ 31 , 2^31-1] F/f float a=1.2f
    double 8 0.0d [-2^ 63,2^63-1] D/d double a=1.2d
    1. char表示一个字符,是相对于utf-8编码而言,但是对unicode却并不是100%适用,因为有部分汉字需要4个字节(2个unicode)来表示,也就是说一个汉字字符实际上是2个char,即上表的示例char ch3='瞾'就不够严谨;
    2. 我们来验证下char ch2='\u77be';,ch2表示1个字符,同时也可以用’\u77be’来定义,而77be就是4个16进制位,1个16进制位表示4个二进制位(4b),则77be表示16个二进制位(16b),即2个字节(2Byte=16bit);

1.1 基本数据类型占用的存储空间

  • 一个byte数字占用1个byte字节应该很好理解;
  • 我们刚才已经说明了1个常规字符(char类型),占用2个字节(2byte);

1.2 Java二进制流

  • Java抽象的Stream流(InputStream/OutputStream)本质上都是都是byte数组的传输;

    • InputStream官方的注释为This abstract class is the superclass of all classes representing an input stream of bytes.
    • OutputStream官方的注释为This abstract class is the superclass of all classes representing an output stream of bytes.
  • 在此基础之上,实现类ArrayByteInputStream/ArrayByteOutputStream/FileInputStream/FileOutputStream等各种数据流,都是不同应用场景上的实现封装,其二进制数据的本质是一样的;
  • 项目中读取二进制流的工具类IoUtil :
    public final class IoUtil
    {/*** 读取文件流* <pre>* 1.文件支持从class外部读取(class调试模式)和jar内部读取(jar包使用模式)* 2.以此class是否在jar包为依据,当不在时,优先从外部读取(证明是class调试模式,根本就没有jar包)* 3.如果上述方式读不到文件,则遵从spring的读取规则(使用spring的API读取)** @param path 文件路径* @return 文件流*/public static InputStream readInputStream(String path){try{String clazzPath = IoUtil.class.getResource(FILE_SPLIT).getPath();boolean clazzInJar = clazzPath.startsWith(FILE_TYPE);boolean inClazzWithoutJar = clazzPath.startsWith(FILE_SPLIT) && !clazzInJar;boolean inJar = path.toLowerCase(Locale.US).startsWith(CLASSPATH) || clazzInJar;String realPath = path;//非jar包模式时,拼接全路径读取if (inClazzWithoutJar && !inJar){if (!realPath.contains(FILE_FULL_PATH_TYPE) && !realPath.startsWith(FILE_SPLIT)){realPath = clazzPath + realPath;}return new FileInputStream(realPath);}Resource resource = new PathMatchingResourcePatternResolver().getResource(path);boolean existFile = resource.exists();//jar包模式运行时,通过spring的api去读取if (existFile){return resource.getInputStream();}}catch (Exception e){throw new EncryptionException("read stream error.", e);}return null;}/*** 私有化构造方法*/private IoUtil(){}/*** 非jar形式的文件全路径标记*/public static final String FILE_FULL_PATH_TYPE = ":";/*** 文件类型*/public static final String FILE_TYPE = "file:";/*** 文件分割类型*/public static final String FILE_SPLIT = "/";/*** 配置类型*/private static final String CLASSPATH = "classpath:";
    }
    
  • 项目中读写文件二进制数据的工具类FileUtil 代码如下:
    public final class FileUtil
    {/*** 读取文件的二进制内容** @param path 文件路径* @return 文件二进制内容*/public static byte[] read(String path){return read(IoUtil.readInputStream(path));}/*** 读取输入流报文** @param in 文件输入流* @return 二进制报文*/public static byte[] read(InputStream in){return read(in, Integer.MAX_VALUE);}/*** 读取限定文件大小的二进制流** @param in   二进制流* @param size 二进制流的大小上限* @return 文件二进制报文*/public static byte[] read(InputStream in, int size){try{if (size <= 0){size = Integer.MAX_VALUE;}byte[] data = IOUtils.toByteArray(in);if (null != data && data.length > size){LOGGER.error("failed to read limit data size:{}.", data.length);return null;}FileType fileType = FileType.getType(data);LOGGER.info("current read stream file type:{}", fileType);return data;}catch (IOException e){LOGGER.error("failed to read input stream.", e);}finally{IOUtils.closeQuietly(in);}return null;}/*** 读取指定编码的文件内容(未指定编码格式时,则默认读取UTF-8编码)** @param path    文件路径* @param charset 文件编码格式* @return 文件内容*/public static String read(String path, Charset charset){byte[] data = read(path);if (null == data){LOGGER.error("failed to read input stream:{}.", path);return null;}if (null == charset){charset = StandardCharsets.UTF_8;}return new String(data, charset);}/*** 把内容写入文件** @param data 二进制文件内容* @param path 目标文件路径*/public static void write(byte[] data, String path){OutputStream out = null;try{File file = new File(path).getCanonicalFile();if (!file.isFile() || !file.exists()){FileUtils.forceMkdirParent(file);LOGGER.info("current file path[{}] force created.", file.getParentFile().getCanonicalPath());}FileType fileType = FileType.getType(data);LOGGER.info("current write stream file type:{}", fileType);out = new FileOutputStream(file);IOUtils.write(data, out);}catch (IOException e){LOGGER.error("failed to write file.", e);}finally{IOUtils.closeQuietly(out);}}private FileUtil(){}/*** 日志句柄*/private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class);
    }
    

1.3 二进制转换说明

  • 在了解了Java世界的二进制本质之后,还需要了解下二进制在不同业务场景下的具体转换应用。比如:图片转换、数据加解密等;

1.3.1 二进制与Base64互转

  • Base64本质上是二进制数据的封装,用于显示表示二进制的场景含义;
  • 图片二进制转成Base64源码ImageUtil 如下:
    public final class ImageUtil
    {/*** 从bas464中转换出图片二进制数据** @param base64 图片base64* @return 图片二进制数据*/public static byte[] toBytes(String base64){if (StringUtils.isEmpty(base64)){LOGGER.error("invalid base64.");return null;}try{if (base64.contains(Const.SPLIT)){//去掉base64中的文件头前缀(如:'data:image/png;base64,')base64 = base64.substring(base64.lastIndexOf(Const.SPLIT));}byte[] data = Base64.getMimeDecoder().decode(base64);LOGGER.info("current image file type:{}", FileType.getType(data));return data;}catch (Exception e){LOGGER.error("failed to get byte[] from base64.", e);}return null;}/*** 二进制图片数据转换成base64** @param data 图片二进制数据* @return 图片base64*/public static String toBase64(byte[] data){if (null == data){LOGGER.error("invalid image file data.");return null;}try{LOGGER.info("current image file type:{}", FileType.getType(data));String base64 = Base64.getEncoder().encodeToString(data);if (!StringUtils.isEmpty(base64) && base64.contains(Const.SPLIT)){//去掉base64中的文件头前缀(如:'data:image/png;base64,')base64 = base64.substring(base64.lastIndexOf(Const.SPLIT));}return base64;}catch (Exception e){LOGGER.error("failed to get base64 by byte[].", e);}return null;}/*** 图片文件转base64** @param path 文件路径* @return 文件的base64*/public static String toBase64(String path){return toBase64(FileUtil.read(path));}
    }
    

    在图片场景下,Base64表示的图片字符串完全等同于图片文件;

  • 秘钥二进制转成对应的Base64秘钥字符串,如:RSA/PGP生成的cer /pem /asc 等秘钥文件,形如效果:
    -----BEGIN RSA PUBLIC KEY-----
    MIIBCgKCAQEAjsWd3QKjDCVU+H9jkkMlOAxAKpG/nT7N+0LOQ75/SxjNaVdmLOhj
    oLAtzFOnY72HoJvd62fFNllU0AkpQuotp2Ajt7W9bHfvtxS8N2EXyShSdBqr1eLo
    zNwgeRqeU/uZmGVi5ehdgBTliQKeUs80mbnBwGzP6e/FUSVlXRXxyn4Pl3hclAD6
    ZM3vP6vSCXIhM4LoI4c...
    -----END RSA PUBLIC KEY-----
    

1.3.2 二进制与十六进制互转

  • 在Java世界中,经常要用到二进制数组(byte[]),但是在接口传输、存储时,又基本上没有,二进制去哪儿了?
  • 除了上面说的Base64外,还经常用到把二进制数组转换成十六进制(Hex),比如说SHA512/SHA256/MD5等摘要,也需要把加密密文由二进制转成十六进制,解密时把十六进制反转成二进制,示例代码SecurityFacade 如下:
    public class SecurityFacade extends BaseEncryptorFacade
    {/*** 本地不可逆加密或者hash** @param data 原始数据* @return 摘要数据*/@Overridepublic String hash(String data){byte[] encBytes = this.getEncryptSecurity().hash(data.getBytes(StandardCharsets.UTF_8));return Hex.toHexString(encBytes);}/*** 本地可逆加密** @param data 原始报文* @return 加密后的报文*/@Overridepublic String encrypt(String data){byte[] encBytes = this.getEncryptSecurity().encrypt(data.getBytes(StandardCharsets.UTF_8));return Hex.toHexString(encBytes);}/*** 本地解密* <p>* 与上面加密对应** @param data 加密后的数据* @return 解密后的数据*/@Overridepublic String decrypt(String data){byte[] decBytes = this.getEncryptSecurity().decrypt(Hex.decode(data));return new String(decBytes, StandardCharsets.UTF_8);}
    }
    

2.Java魔数应用

  • 在了解Java底层基本上都是二进制数据之后,还有一个概念和二进制息息相关:魔数魔数是用于识别文件格式或者协议类型的一段常量或者字符串。
  • 此处讲的魔数仅限于上述的命名规范,不涉及Java语言魔法数字的检验规则。

2.1 文件魔数

  • 文件魔数就是文件二进制/十六进制数据的特定起始标识位,此处封装常用文件检验的FileType 代码如下:

    public enum FileType
    {/*** JEPG.*/JPEG("FFD8FF"),/*** PNG.*/PNG("89504E47"),/*** GIF.*/GIF("47494638"),/*** TIFF.*/TIFF("49492A00"),/*** Windows Bitmap.*/BMP("424D"),/*** CAD.*/DWG("41433130"),/*** Adobe Photoshop.*/PSD("38425053"),/*** Rich Text Format.*/RTF("7B5C727466"),/*** XML.*/XML("3C3F786D6C"),/*** HTML.*/HTML("68746D6C3E"),/*** Email [thorough only].*/EML("44656C69766572792D646174653A"),/*** Outlook Express.*/DBX("CFAD12FEC5FD746F"),/*** Outlook (pst).*/PST("2142444E"),/*** MS Word/Excel(兼容格式).*/XLS_DOC("D0CF11E0"),/*** XLSX(excel新版本)*/XLSX("504B030414000600080000002100"),/*** DOCX(word新版本)*/DOCX("504B03041400060008000000210077"),/*** MS Access.*/MDB("5374616E64617264204A"),/*** WordPerfect.*/WPD("FF575043"),/*** Postscript.*/EPS("252150532D41646F6265"),/*** Adobe Acrobat.*/PDF("255044462D312E"),/*** Quicken.*/QDF("AC9EBD8F"),/*** Windows Password.*/PWL("E3828596"),/*** ZIP Archive.*/ZIP("504B0304"),/*** RAR Archive.*/RAR("52617221"),/*** Wave.*/WAV("57415645"),/*** AVI.*/AVI("41564920"),/*** Real Audio.*/RAM("2E7261FD"),/*** Real Media.*/RM("2E524D46"),/*** MPEG (mpg).*/MPG("000001BA"),/*** Quicktime.*/MOV("6D6F6F76"),/*** Windows Media.*/ASF("3026B2758E66CF11"),/*** MIDI.*/MID("4D546864");/*** 获取最长的二进制文件格式对应的内容** @return 文件格式对应的最长的二进制长度*/public static int getMaxBytes(){return MAX_LEN.get();}/*** 获取文件类型** @param data 文件二进制* @return 文件类型对象*/public static FileType getType(byte[] data){if (null == data){LOGGER.warn("unknown file type by stream.");return null;}//1.截取最长的文件格式的二进制位数int maxLen = Math.min(data.length, MAX_LEN.get());byte[] suffixBytes = Arrays.copyOf(data, maxLen);//2.把最长的长度的二进制转成十六进制String suffixTypes = Hex.toHexString(suffixBytes);for (FileType fileType : SORTED_TYPES){if (suffixTypes.toUpperCase(Locale.US).contains(fileType.type.toUpperCase(Locale.US))){LOGGER.info("current file type by stream:{}.", fileType.name());return fileType;}}LOGGER.warn("unknown file type by stream.");return null;}/*** 获取到十六进制的类型** @return 十六进制的类型*/public String getType(){return this.type;}@Overridepublic String toString(){return this.name().toLowerCase(Locale.US);}/*** 构造方法** @param type 文件类型*/FileType(String type){this.type = type;}/*** 最大长度的文件二进制数位*/private static final AtomicInteger MAX_LEN = new AtomicInteger();/*** 排序后的文件类型集合*/private static final List<FileType> SORTED_TYPES = Lists.newArrayList();/*** 日志句柄*/private static final Logger LOGGER = LoggerFactory.getLogger(FileType.class);/*** 文件类型*/private String type;//初始化static{//1.获取最长的文件格式的二进制长度int maxHexLen = 0;int maxBytesLen = 0;for (FileType fileType : values()){int len = fileType.type.length();if (len > maxHexLen){maxBytesLen = Hex.decode(fileType.type.getBytes(StandardCharsets.UTF_8)).length;}maxHexLen = Math.max(maxHexLen, len);}//2.把遍历的最长的二进制前缀长度保存下来MAX_LEN.set(maxBytesLen);//3.把所有格式枚举保存起来SORTED_TYPES.addAll(Lists.newArrayList(values()));//4.把所有的格式枚举重新排序(从长到短,为了避免文件格式存在包含而取错的情况,比如:docx格式应该包含了doc)Collections.sort(SORTED_TYPES, new Comparator<FileType>(){@Overridepublic int compare(FileType o1, FileType o2){return o2.type.length() - o1.type.length();}});}
    }
    

    注意:

    • 文件魔数可能会存在长的16进制编码包含了短的16进制编码的情况,本逻辑判断文件格式时,会优先匹配长的16进制编码,这样就不会误判;
    • 为了避免大文件解析,其实并不需要解析出整个文件的二进制/十六进制内容,仅需解析大概前200个byte即可判断文件格式。

2.2 协议魔数

  • 在实际项目中,编写SM2协议实现时,就存在基于协议魔数来判断秘钥是否压缩的情况,Sm2Encryption 代码如下:

    public class Sm2Encryption extends BaseSingleSignature
    {@Overridepublic PublicKey toPubKey(byte[] pubKey){try{String hexKey = Hex.toHexString(pubKey);KeyFactory kf = KeyFactory.getInstance(ALGORITHM, this.getProvider());if (hexKey.startsWith(STANDARD_HEX_KEY_PREFIX)){return kf.generatePublic(new X509EncodedKeySpec(pubKey));}else{// 获取SM2相关参数X9ECParameters ecParam = GMNamedCurves.getByName(SM2_VERSION);// 将公钥HEX字符串转换为椭圆曲线对应的点ECCurve ecCurve = ecParam.getCurve();ECPoint ecPoint = ecCurve.decodePoint(pubKey);// 椭圆曲线参数规格ECParameterSpec ecSpec = new ECParameterSpec(ecCurve, ecParam.getG(), ecParam.getN(), ecParam.getH());// 将椭圆曲线点转为公钥KEY对象return kf.generatePublic(new ECPublicKeySpec(ecPoint, ecSpec));}}catch (Exception e){throw new EncryptionException("failed to get sm2 pub key.", e);}}/*** 标准的秘钥hex前缀*/private static final String STANDARD_HEX_KEY_PREFIX = "30";
    }
    

3.Java Unicode编码应用

  • unicode与char的关系

    • Unicode,全称为Unicode标准(The Unicode Standard),官方机构使用的中文名称为统一码;
    • unicode是个编码方案,Java语言默认的UTF-8字符集就是unicode编码方案的一种实现,后面所有unicode编码均指UTF-8的unicode实现;
    • UTF-8的unicode是变长的,可以由2个或者4个字节来表示1个字符;
    • 1个字节需要2个16进制字符来表示;

    结论: unicode是变长的,可以是2个字节,也可以是4个字节来表示一个字符,unicode也可以说是4/8个16进制位来表示;

  • GB18030-2022字符规范 正式生效在即,也有必要和大家好好分析下中文字符在UTF-8编码的Unicode规范应用;

  • UTF-8 unicode中文字符集 如下:

    字符集 字数 Unicode 编码
    基本汉字 20902字 4E00-9FA5
    基本汉字补充 90字 9FA6-9FFF
    扩展A 6592字 3400-4DBF
    扩展B 42720字 20000-2A6DF
    扩展C 4154字 2A700-2B739
    扩展D 222字 2B740-2B81D
    扩展E 5762字 2B820-2CEA1
    扩展F 7473字 2CEB0-2EBE0
    扩展G 4939字 30000-3134A
    扩展H 4192字 31350-323AF
    康熙部首 214字 2F00-2FD5
    部首扩展 115字 2E80-2EF3
    兼容汉字 472字 F900-FAD9
    兼容扩展 542字 2F800-2FA1D
    汉字笔画 36字 31C0-31E3
    汉字结构 12字 2FF0-2FFB
    汉语注音 43字 3105-312F
    注音扩展 32字 31A0-31BF
    1字 3007

3.1 Java中文字符转换