Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 1335|回复: 0

[默认分类] Java中文乱码&特殊字符解决方案

[复制链接]
  • TA的每日心情
    开心
    2021-12-13 21:45
  • 签到天数: 15 天

    [LV.4]偶尔看看III

    发表于 2020-8-6 10:22:33 | 显示全部楼层 |阅读模式
    java中文乱码&特殊字符解决方案
    相信很多朋友遇到过Java的乱码问题,最近我也在解决一个“使用文本生成图片过程中中文以及特殊字符乱码”的问题;花了我大量时间,Debug了sun.font、sun.awt下面的各种源码,终于搞懂了其机制,解决了目前次问题;现在把问题解决过程给写下来,做个记录,以免以后再次遇到。
    遇到的问题
    下面是我想要执行的代码(经过极度简化,但是意思没变):

    1. 1 public static void main(String[] args) throws IOException {
    2. 2    File file = new File("test.png");
    3. 3    Font font = new Font("宋体", Font.PLAIN, 10);
    4. 4    BufferedImage bi = new BufferedImage(400, 200, BufferedImage.TYPE_INT_ARGB);
    5. 5    Graphics2D g2 = (Graphics2D) bi.getGraphics();
    6. 6    g2.setBackground(Color.WHITE);
    7. 7    g2.clearRect(0, 0, 400, 200);
    8. 8    g2.setFont(font);
    9. 9    g2.setColor(Color.BLACK);
    10. 10    g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    11. 11    g2.drawString("为什么没有(ꐚꌒꑿꆺ)(ꐚꌒꑿꆺ)这名字特殊不?@¥¥¥ 为什么没有(ꐚꌒꑿꆺ)(ꐚꌒꑿꆺ)这名字特 ", 0, 10);
    12. 12    g2.dispose();
    13. 13    ImageIO.write(bi, PNG, file);
    14. 14 }
    复制代码


    目标当然是想在打开test.png的时候看到如下场景:

      
    在本地调试没问题之后,就放到了测试机(Linux)上面去执行了,执行结果简直扑街:

      
    jdk1.8的sun源码下载
    奉行程序员一贯作风:既然有问题,那就Debug!
    坑爹的是现在的源码包已经不包含sun包的代码了!
    幸好java官方确认OpenJDK的代码基本和JVM源码一致,可以直接从OpenJDK8u进行下载:jdk8u
    至于如何使用源码debug,这个就不写了··· 这都不会基本也就别看这文章了
    定位问题
    直接下载好源码,远程断点,服务器执行,在debug中先发现了第一个产生本地和测试服务器不一致的代码:

      
    原来JVM创建Font的时候会使用FontManagerFactory获取FontManager,而不同的系统使用的FontManager是不同的!Mac用的是CFontManager,而Linux用的是X11FontManager!
    那么这两个FontManager的不同会导致什么不同呢?

      CFontManager会创建CFont作为Font2D,这个CFont是JVM专门为mac创建的类,看类和方法的注释可以知道在mac环境下有时候物理字体会被CFont包装,而这是在native代码中完成的:
       
      X11FontManager创建的Font2D是包含了逻辑字体和物理字体的集合。X11FontManager继承了FcFontManager,FcFontManager继承了SunFontManager;我们看一下X11FontManager的loadFonts()方法,直接使用了SunFontManager的loadFonts(),SunFontManager的loadFonts()方法加载了物理字体,SunFontManager实现了FontManager的preferLocaleFonts()方法,加载了逻辑字体:
       

    逻辑字体与物理字体
    代码debug到这边基本已经确认了是不同环境的字体加载问题,那么在debug linux环境的时候发现的逻辑字体和物理字体是什么东西呢?
    物理字体
    物理字体是实际的字体库,包含字形数据和表,这些数据和表使用字体技术(如 TrueType 或 PostScript Type 1)将字符序列映射到字形序列。Java Platform 的所有实现都支持 TrueType 字体;对其他字体技术的支持是与实现相关的。物理字体可以使用字体名称,如 Helvetica、Palatino、HonMincho 或任意数量的其他字体名称。通常,每种物理字体只支持有限的书写系统集合,例如,只支持拉丁文字符,或者只支持日文和基本拉丁文。可用的物理字体集合随配置的不同而有所不同。要求特定字体的应用程序可以使用 createFont 方法来捆绑这些字体,并对其进行实例化。
    逻辑字体
    逻辑字体是由必须受所有 Java 运行时环境支持的 Java 平台所定义的五种字体系列:Serif、SansSerif、Monospaced、Dialog 和 DialogInput。这些逻辑字体不是实际的字体库。此外,由 Java 运行时环境将逻辑字体名称映射到物理字体。映射关系与实现和通常语言环境相关,因此它们提供的外观和规格各不相同。通常,为了覆盖庞大的字符范围,每种逻辑字体名称都映射到几种物理字体。
    问题解决
    debug的源码很多,但是此次问题的关键点就在这里了,其它debug内容就不贴了。
    既然已经确认了本地(mac环境)是native的代码帮我们做了物理字体的封装,转换成了CFont进行渲染,而Linux环境的X11FontManager只是帮我们加载了物理字体和逻辑字体,但是却需要我们自己进行选择,那么解决问题的第一步就显而易见了:将Font的创建从物理字体改为逻辑字体

    1. 1 //  Serif、SansSerif、Monospaced、Dialog 和 DialogInput 随意选择
    2. 2 Font font = new Font("Serif", Font.PLAIN, 10);
    复制代码


    改完以后执行代码,仍然是乱码!继续Debug,发现是Linux上逻辑字体Serif映射的物理字体没有中文字体和对应的特殊符号字体,这就很简单了,直接在Linux上安装中文字体(simsun.ttf),再安装特殊符号“ꐚꌒꑿꆺ”可显示的字体(mysi.ttf),将这两个字体也放到了jdk的fonts目录(JAVA_HOME/jre/lib/fonts)下。文章后面有Linux字体安装方法。
    完成上面的改动之后,重启服务,再次执行成功显示!热烈庆祝~~~~
    JVM逻辑字体映射配置
    以上的改动已经可以解决中文和特殊字符乱码问题,但是我在Debug过程中发现在逻辑字体加载过程中,JVM会参考一个配置文件,代码在sun.awt.FontConfiguration中,这个配置类完成了逻辑字体和物理字体的映射,也指导了SunFontManager创建逻辑字体,而这个FontConfiguration读取的配置文件就是fontconfig.properties,这个配置文件目录是JAVA_HOME/jre/lib
    查阅了一下资料,JVM字体配置文件的加载顺序如下:
    JAVA_HOME/jre/lib/fontconfig.OS.Version.properties
    JAVA_HOME/jre/lib/fontconfig.OS.Version.bfc
    JAVA_HOME/jre/lib/fontconfig.OS.properties
    JAVA_HOME/jre/lib/fontconfig.OS.bfc
    JAVA_HOME/jre/lib/fontconfig.Version.properties
    JAVA_HOME/jre/lib/fontconfig.Version.bfc
    JAVA_HOME/jre/lib/fontconfig.properties
    JAVA_HOME/jre/lib/fontconfig.bfc
    OS是系统,例如:Linux、CentOs、RedHat等;Version是版本号
    在这个配置文件中可以修改逻辑字体与物理字体的对应关系,也就是说可以手动的修改Serif、SansSerif、Monospaced、Dialog 和 DialogInput这五个逻辑字体在不同场景下所使用的真正物理字体。
    举个栗子,下面的配置将serif.plain逻辑字体的中文使用simsun.ttf,拉丁文使用java自带字体:

    1. 1 # @(#)linux.fontconfig.SuSE.properties 1.2 03/10/17
    2. 2 #
    3. 3 # Copyright 2003 Sun Microsystems, Inc. All rights reserved.
    4. 4 #
    5. 5
    6. 6 # Version
    7. 7 version=1
    8. 8
    9. 9 # Component Font Mappings
    10. 10 serif.plain.chinese=-misc-simsun-medium-r-normal--*-%d-*-*-c-*-iso10646-1
    11. 11 serif.plain.latin-1=-b&h-lucidabright-medium-r-normal--*-%d-*-*-p-*-iso8859-1
    12. 12
    13. 13 # Search Sequences
    14. 14 sequence.allfonts=latin-1,chinese
    15. 15
    16. 16 # Exclusion Ranges
    17. 17
    18. 18 # Font File Names
    19. 19 filename.-misc-simsun-medium-r-normal--*-%d-*-*-c-*-iso10646-1=/usr/share/fonts/myfonts/simsun.ttf
    复制代码


    Linux安装字体

    Linux字体目录:/usr/share/fonts
    在fonts下面新建一个目录,例如:mkdir myfonts
    将需要安装的字体放到新建目录下面,例如:cp ~/test/simsun.ttf /usr/share/fonts/myfonts
    进入到myfonts目录:cd /usr/share/fonts/myfonts
    执行如下命令:
      
       mkfontscale
       mkfontdir
       fc-cache -fv
       
    查看是否已经安装对应的字体:fc-list
    fc-cache -fv 命令用来刷新linux的字体缓存,使其立刻生效

    PS:以上所有操作基本都需要root权限




    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-1 04:31 , Processed in 0.325880 second(s), 38 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表