จะค้นหา Charset / Encoding เริ่มต้นใน Java ได้อย่างไร?


92

คำตอบที่ชัดเจนคือการใช้Charset.defaultCharset()แต่เมื่อเร็ว ๆ นี้เราพบว่านี่อาจไม่ใช่คำตอบที่ถูกต้อง ฉันได้รับแจ้งว่าผลลัพธ์แตกต่างจากชุดอักขระเริ่มต้นจริงที่ใช้โดยคลาส java.io ในหลาย ๆ ครั้ง ดูเหมือนว่า Java จะเก็บชุดอักขระเริ่มต้นไว้ 2 ชุด ใครมีข้อมูลเชิงลึกเกี่ยวกับปัญหานี้หรือไม่?

เราสามารถทำซ้ำกรณีล้มเหลวหนึ่งกรณี เป็นข้อผิดพลาดของผู้ใช้ แต่ยังอาจเปิดเผยสาเหตุของปัญหาอื่น ๆ ทั้งหมด นี่คือรหัส

public class CharSetTest {

    public static void main(String[] args) {
        System.out.println("Default Charset=" + Charset.defaultCharset());
        System.setProperty("file.encoding", "Latin-1");
        System.out.println("file.encoding=" + System.getProperty("file.encoding"));
        System.out.println("Default Charset=" + Charset.defaultCharset());
        System.out.println("Default Charset in Use=" + getDefaultCharSet());
    }

    private static String getDefaultCharSet() {
        OutputStreamWriter writer = new OutputStreamWriter(new ByteArrayOutputStream());
        String enc = writer.getEncoding();
        return enc;
    }
}

เซิร์ฟเวอร์ของเราต้องการชุดอักขระเริ่มต้นในภาษาละติน -1 เพื่อจัดการกับการเข้ารหัสแบบผสม (ANSI / Latin-1 / UTF-8) ในโปรโตคอลเดิม ดังนั้นเซิร์ฟเวอร์ทั้งหมดของเราจึงทำงานด้วยพารามิเตอร์ JVM นี้

-Dfile.encoding=ISO-8859-1

นี่คือผลลัพธ์บน Java 5

Default Charset=ISO-8859-1
file.encoding=Latin-1
Default Charset=UTF-8
Default Charset in Use=ISO8859_1

มีคนพยายามเปลี่ยนรันไทม์การเข้ารหัสโดยตั้งค่า file.encoding ในโค้ด เราทุกคนรู้ว่าไม่ได้ผล อย่างไรก็ตามสิ่งนี้เห็นได้ชัดว่าเป็นการปิด defaultCharset () แต่จะไม่มีผลกับชุดอักขระเริ่มต้นจริงที่ใช้โดย OutputStreamWriter

นี่คือบั๊กหรือฟีเจอร์?

แก้ไข: คำตอบที่ยอมรับจะแสดงสาเหตุที่แท้จริงของปัญหา โดยทั่วไปคุณไม่สามารถเชื่อถือ defaultCharset () ใน Java 5 ซึ่งไม่ใช่การเข้ารหัสเริ่มต้นที่ใช้โดยคลาส I / O ดูเหมือนว่า Java 6 จะแก้ไขปัญหานี้


นั่นเป็นเรื่องแปลกเนื่องจาก defaultCharset ใช้ตัวแปรแบบคงที่ซึ่งตั้งค่าไว้เพียงครั้งเดียว (ยึดติดกับเอกสาร - เมื่อเริ่มต้น VM) คุณใช้ VM Vendor อะไร
Bozho

ฉันสามารถสร้างสิ่งนี้บน Java 5 ได้ทั้งบน Sun / Linux และ Apple / OS X
ZZ Coder

นั่นอธิบายว่าทำไม defaultCharset () ถึงไม่แคชผลลัพธ์ ฉันยังคงต้องค้นหาว่าชุดอักขระเริ่มต้นที่แท้จริงที่คลาส IO ใช้คืออะไร ต้องมีชุดอักขระเริ่มต้นอื่นที่แคชไว้ที่อื่น
ZZ Coder

@ZZ Coder ฉันยังคงค้นคว้าอยู่ สิ่งเดียวที่ฉันรู้คือ Charset.defaulyCharset () ไม่ได้ถูกเรียกจาก sun.nio.cs.StreamEncoder ใน JVM 1.5 ใน JVM 1.6 วิธี Charset.defaulyCharset () เรียกว่าให้ผลลัพธ์ที่คาดหวัง การใช้งาน JVM 1.5 ของ StreamEncoder กำลังแคชการเข้ารหัสก่อนหน้านี้อย่างใด
bruno conde

คำตอบ:


62

นี่แปลกจริงๆ ... เมื่อตั้งค่าแล้ว Charset เริ่มต้นจะถูกแคชและจะไม่มีการเปลี่ยนแปลงในขณะที่คลาสอยู่ในหน่วยความจำ การตั้งค่า"file.encoding"คุณสมบัติโดยSystem.setProperty("file.encoding", "Latin-1");ไม่ทำอะไรเลย ทุกครั้งที่Charset.defaultCharset()เรียกว่าจะส่งคืนชุดอักขระที่แคชไว้

นี่คือผลลัพธ์ของฉัน:

Default Charset=ISO-8859-1
file.encoding=Latin-1
Default Charset=ISO-8859-1
Default Charset in Use=ISO8859_1

ฉันใช้ JVM 1.6 อยู่

(ปรับปรุง)

ตกลง. ฉันสร้างจุดบกพร่องของคุณด้วย JVM 1.5

ดูที่ซอร์สโค้ดของ 1.5 ไม่ได้ตั้งค่าชุดอักขระเริ่มต้นที่แคชไว้ ฉันไม่รู้ว่านี่เป็นจุดบกพร่องหรือไม่ แต่ 1.6 เปลี่ยนแปลงการใช้งานนี้และใช้ชุดอักขระที่แคชไว้:

JVM 1.5:

public static Charset defaultCharset() {
    synchronized (Charset.class) {
        if (defaultCharset == null) {
            java.security.PrivilegedAction pa =
                    new GetPropertyAction("file.encoding");
            String csn = (String) AccessController.doPrivileged(pa);
            Charset cs = lookup(csn);
            if (cs != null)
                return cs;
            return forName("UTF-8");
        }
        return defaultCharset;
    }
}

JVM 1.6:

public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            java.security.PrivilegedAction pa =
                    new GetPropertyAction("file.encoding");
            String csn = (String) AccessController.doPrivileged(pa);
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

เมื่อคุณตั้งค่าการเข้ารหัสไฟล์เพื่อfile.encoding=Latin-1ครั้งต่อไปที่คุณเรียกCharset.defaultCharset()สิ่งที่เกิดขึ้นเป็นเพราะ charset เริ่มต้นที่แคชไม่ได้ตั้งค่าก็จะพยายามที่จะหาสิ่งที่เหมาะสมสำหรับ charset Latin-1ชื่อ UTF-8ชื่อนี้จะไม่พบเพราะมันเป็นเรื่องที่ไม่ถูกต้องและส่งกลับเริ่มต้น

สำหรับสาเหตุที่คลาส IO เช่นOutputStreamWriterส่งคืนผลลัพธ์ที่ไม่คาดคิด
การใช้งานsun.nio.cs.StreamEncoder(แม่มดถูกใช้โดยคลาส IO เหล่านี้) จะแตกต่างกันเช่นกันสำหรับ JVM 1.5 และ JVM 1.6 การใช้งาน JVM 1.6 เป็นไปตามCharset.defaultCharset()วิธีการรับการเข้ารหัสเริ่มต้นหากไม่มีการจัดเตรียมให้กับคลาส IO การใช้งาน JVM 1.5 ใช้วิธีการอื่นConverters.getDefaultEncodingName();เพื่อรับชุดอักขระเริ่มต้น วิธีนี้ใช้แคชของตัวเองของชุดอักขระเริ่มต้นที่ตั้งค่าตามการเริ่มต้น JVM:

JVM 1.6:

public static StreamEncoder forOutputStreamWriter(OutputStream out,
        Object lock,
        String charsetName)
        throws UnsupportedEncodingException
{
    String csn = charsetName;
    if (csn == null)
        csn = Charset.defaultCharset().name();
    try {
        if (Charset.isSupported(csn))
            return new StreamEncoder(out, lock, Charset.forName(csn));
    } catch (IllegalCharsetNameException x) { }
    throw new UnsupportedEncodingException (csn);
}

JVM 1.5:

public static StreamEncoder forOutputStreamWriter(OutputStream out,
        Object lock,
        String charsetName)
        throws UnsupportedEncodingException
{
    String csn = charsetName;
    if (csn == null)
        csn = Converters.getDefaultEncodingName();
    if (!Converters.isCached(Converters.CHAR_TO_BYTE, csn)) {
        try {
            if (Charset.isSupported(csn))
                return new CharsetSE(out, lock, Charset.forName(csn));
        } catch (IllegalCharsetNameException x) { }
    }
    return new ConverterSE(out, lock, csn);
}

แต่ฉันเห็นด้วยกับความคิดเห็น คุณไม่ควรพึ่งพาคุณสมบัตินี้ เป็นรายละเอียดการใช้งาน


ในการสร้างข้อผิดพลาดนี้คุณต้องอยู่บน Java 5 และการเข้ารหัสเริ่มต้น JRE ของคุณต้องเป็น UTF-8
ZZ Coder

2
นี่คือการเขียนถึงการนำไปใช้ไม่ใช่สิ่งที่เป็นนามธรรม หากคุณพึ่งพาสิ่งที่ไม่มีเอกสารอย่าแปลกใจถ้าโค้ดของคุณพังเมื่อคุณอัปเกรดเป็นแพลตฟอร์มเวอร์ชันใหม่กว่า
McDowell

24

นี่คือบั๊กหรือฟีเจอร์?

ดูเหมือนพฤติกรรมที่ไม่ได้กำหนด ฉันรู้ว่าในทางปฏิบัติคุณสามารถเปลี่ยนการเข้ารหัสเริ่มต้นโดยใช้คุณสมบัติบรรทัดคำสั่ง แต่ฉันไม่คิดว่าจะเกิดอะไรขึ้นเมื่อคุณทำสิ่งนี้ถูกกำหนดไว้

รหัสข้อบกพร่อง: 4153515เกี่ยวกับปัญหาในการตั้งค่าคุณสมบัตินี้:

นี่ไม่ใช่บั๊ก คุณสมบัติ "file.encoding" ไม่จำเป็นต้องใช้โดยข้อกำหนดแพลตฟอร์ม J2SE เป็นรายละเอียดภายในของการใช้งานของ Sun และไม่ควรตรวจสอบหรือแก้ไขด้วยรหัสผู้ใช้ นอกจากนี้ยังมีวัตถุประสงค์เพื่ออ่านอย่างเดียว ในทางเทคนิคเป็นไปไม่ได้ที่จะสนับสนุนการตั้งค่าคุณสมบัตินี้เป็นค่าที่กำหนดเองบนบรรทัดคำสั่งหรือในเวลาอื่น ๆ ระหว่างการเรียกใช้โปรแกรม

วิธีที่แนะนำในการเปลี่ยนการเข้ารหัสดีฟอลต์ที่ VM และระบบรันไทม์ใช้คือการเปลี่ยนโลแคลของแพลตฟอร์มพื้นฐานก่อนเริ่มโปรแกรม Java ของคุณ

ฉันประจบประแจงเมื่อเห็นคนตั้งค่าการเข้ารหัสในบรรทัดคำสั่ง - คุณไม่รู้ว่าจะมีผลกับรหัสอะไร

หากคุณไม่ต้องการใช้การเข้ารหัสเริ่มต้นให้ตั้งค่าการเข้ารหัสที่คุณต้องการอย่างชัดเจนผ่านวิธีการ / ตัวสร้างที่เหมาะสม


4

อันดับแรก Latin-1 เหมือนกับ ISO-8859-1 ดังนั้นค่าเริ่มต้นก็ใช้ได้สำหรับคุณแล้ว ขวา?

คุณตั้งค่าการเข้ารหัสเป็น ISO-8859-1 สำเร็จด้วยพารามิเตอร์บรรทัดคำสั่งของคุณ นอกจากนี้คุณยังตั้งค่าโดยใช้โปรแกรมเป็น "Latin-1" แต่นั่นไม่ใช่ค่าที่ยอมรับได้ของการเข้ารหัสไฟล์สำหรับ Java ดูhttp://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html

เมื่อคุณทำเช่นนั้นดูเหมือนว่า Charset จะรีเซ็ตเป็น UTF-8 จากการดูต้นทาง อย่างน้อยที่สุดก็อธิบายพฤติกรรมส่วนใหญ่ได้

ฉันไม่รู้ว่าทำไม OutputStreamWriter ถึงแสดง ISO8859_1 มันมอบสิทธิ์ให้กับคลาส sun.misc. * แบบปิด ฉันเดาว่ามันไม่ค่อยเกี่ยวข้องกับการเข้ารหัสผ่านกลไกเดียวกันซึ่งเป็นเรื่องแปลก

แต่แน่นอนคุณควรระบุเสมอว่าคุณหมายถึงการเข้ารหัสใดในรหัสนี้ ฉันไม่เคยพึ่งพาค่าเริ่มต้นของแพลตฟอร์ม


4

พฤติกรรมไม่แปลกเลยจริงๆ เมื่อพิจารณาถึงการนำคลาสไปใช้มันเกิดจาก:

  • Charset.defaultCharset() ไม่ได้แคชชุดอักขระที่กำหนดใน Java 5
  • การตั้งค่าคุณสมบัติระบบ "file.encoding" และการเรียกใช้Charset.defaultCharset()อีกครั้งทำให้เกิดการประเมินคุณสมบัติระบบครั้งที่สองไม่พบชุดอักขระที่มีชื่อ "Latin-1" ดังนั้นจึงมีCharset.defaultCharset()ค่าเริ่มต้นเป็น "UTF-8"
  • OutputStreamWriterเป็นอย่างไรแคชชุดอักขระเริ่มต้นและอาจจะใช้อยู่แล้วในช่วงเริ่มต้น VM เพื่อให้มันโอนชุดอักขระเริ่มต้นจากCharset.defaultCharset()ถ้าคุณสมบัติของระบบ "file.encoding" ได้รับการเปลี่ยนแปลงที่รันไทม์

ดังที่ได้กล่าวไปแล้วไม่มีการบันทึกว่า VM ต้องทำงานอย่างไรในสถานการณ์เช่นนี้ Charset.defaultCharset()เอกสาร API ไม่แม่นยำมากเกี่ยวกับวิธีการตั้งค่าตัวอักษรเริ่มต้นจะถูกกำหนดเพียงการกล่าวขวัญว่ามันมักจะทำในการเริ่มต้น VM ขึ้นอยู่กับปัจจัยต่างๆเช่นชุด OS ตัวอักษรเริ่มต้นหรือตำแหน่งที่ตั้งเริ่มต้น


3

ฉันได้ตั้งค่าอาร์กิวเมนต์ vm ในเซิร์ฟเวอร์ WAS เป็น -Dfile.encoding = UTF-8 เพื่อเปลี่ยนชุดอักขระเริ่มต้นของเซิร์ฟเวอร์


1

ตรวจสอบ

System.getProperty("sun.jnu.encoding")

ดูเหมือนว่าจะเป็นการเข้ารหัสแบบเดียวกับที่ใช้ในบรรทัดคำสั่งของระบบของคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.