Bagaimana Cara Menemukan Charset / Encoding Default di Java?

92

Jawaban yang jelas adalah dengan menggunakan Charset.defaultCharset()tetapi kami baru-baru ini menemukan bahwa ini mungkin bukan jawaban yang tepat. Saya diberitahu bahwa hasilnya berbeda dari charset default nyata yang digunakan oleh kelas java.io dalam beberapa kesempatan. Sepertinya Java menyimpan 2 set rangkaian karakter default. Apakah ada yang punya wawasan tentang masalah ini?

Kami dapat mereproduksi satu kasus gagal. Ini semacam kesalahan pengguna tetapi mungkin masih mengekspos akar penyebab semua masalah lainnya. Ini kodenya,

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;
    }
}

Server kami memerlukan charset default dalam Latin-1 untuk menangani beberapa encoding campuran (ANSI / Latin-1 / UTF-8) dalam protokol lama. Jadi semua server kami berjalan dengan parameter JVM ini,

-Dfile.encoding=ISO-8859-1

Inilah hasilnya di Java 5,

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

Seseorang mencoba mengubah runtime pengkodean dengan mengatur file.encoding dalam kode. Kita semua tahu itu tidak berhasil. Namun, ini tampaknya membuang defaultCharset () tetapi tidak mempengaruhi charset default nyata yang digunakan oleh OutputStreamWriter.

Apakah ini bug atau fitur?

EDIT: Jawaban yang diterima menunjukkan akar penyebab masalah. Pada dasarnya, Anda tidak dapat mempercayai defaultCharset () di Java 5, yang bukan merupakan pengkodean default yang digunakan oleh kelas I / O. Sepertinya Java 6 memperbaiki masalah ini.

ZZ Coder
sumber
Aneh, karena defaultCharset menggunakan variabel statis yang disetel hanya sekali (menurut dokumen - saat permulaan VM). Vendor VM apa yang Anda gunakan?
Bozho
Saya dapat mereproduksi ini di Java 5, baik di Sun / Linux dan Apple / OS X.
ZZ Coder
Itu menjelaskan mengapa defaultCharset () tidak menyimpan hasil dalam cache. Saya masih perlu mencari tahu apa charset default sebenarnya yang digunakan oleh kelas IO. Harus ada charset default lain yang di-cache di tempat lain.
ZZ Coder
@ZZ Coder, saya masih meneliti tentang itu. Satu-satunya pemikiran yang saya tahu adalah bahwa Charset.defaulyCharset () tidak dipanggil dari sun.nio.cs.StreamEncoder di JVM 1.5. Dalam JVM 1.6 metode Charset.defaulyCharset () dipanggil untuk memberikan hasil yang diharapkan. Implementasi JVM 1.5 dari StreamEncoder meng-cache encoding sebelumnya, entah bagaimana.
bruno conde

Jawaban:

62

Ini benar-benar aneh ... Setelah disetel, Charset default di-cache dan tidak diubah saat kelas ada di memori. Mengatur "file.encoding"properti dengan System.setProperty("file.encoding", "Latin-1");tidak melakukan apa pun. Setiap kali Charset.defaultCharset()dipanggil, ia mengembalikan charset yang di-cache.

Ini hasil saya:

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

Saya menggunakan JVM 1.6.

(memperbarui)

Baik. Saya mereproduksi bug Anda dengan JVM 1.5.

Melihat kode sumber 1.5, kumpulan karakter default yang di-cache tidak disetel. Saya tidak tahu apakah ini bug atau bukan tetapi 1.6 mengubah implementasi ini dan menggunakan charset yang di-cache:

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;
}

Saat Anda menyetel pengkodean file ke file.encoding=Latin-1saat Anda memanggil berikutnya Charset.defaultCharset(), yang terjadi adalah, karena charset default yang di-cache tidak disetel, ia akan mencoba menemukan charset yang sesuai untuk namanya Latin-1. Nama ini tidak ditemukan, karena salah, dan mengembalikan defaultUTF-8 .

Adapun mengapa kelas IO seperti OutputStreamWritermengembalikan hasil yang tidak diharapkan,
implementasi sun.nio.cs.StreamEncoder(witch digunakan oleh kelas IO ini) berbeda juga untuk JVM 1.5 dan JVM 1.6. Implementasi JVM 1.6 didasarkan pada Charset.defaultCharset()metode untuk mendapatkan pengkodean default, jika tidak disediakan untuk kelas IO. Implementasi JVM 1.5 menggunakan metode berbeda Converters.getDefaultEncodingName();untuk mendapatkan charset default. Metode ini menggunakan cache-nya sendiri dari charset default yang disetel pada inisialisasi 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);
}

Tapi saya setuju dengan komentarnya. Anda tidak boleh mengandalkan properti ini . Ini adalah detail implementasi.

bruno conde
sumber
Untuk mereproduksi kesalahan ini, Anda harus menggunakan Java 5 dan encoding default JRE Anda harus UTF-8.
ZZ Coder
2
Ini menulis untuk implementasi, bukan abstraksi. Jika Anda mengandalkan barang-barang yang tidak terdokumentasi, jangan kaget jika kode Anda rusak saat Anda meningkatkan ke versi platform yang lebih baru.
McDowell
24

Apakah ini bug atau fitur?

Sepertinya perilaku tidak terdefinisi. Saya tahu bahwa, dalam praktiknya, Anda dapat mengubah pengkodean default menggunakan properti baris perintah, tetapi saya tidak berpikir apa yang terjadi ketika Anda melakukan ini ditentukan.

Bug ID: 4153515 tentang masalah pengaturan properti ini:

Ini bukan bug. Properti "file.encoding" tidak diperlukan oleh spesifikasi platform J2SE; ini adalah detail internal implementasi Sun dan tidak boleh diperiksa atau dimodifikasi oleh kode pengguna. Ini juga dimaksudkan untuk menjadi hanya-baca; secara teknis tidak mungkin untuk mendukung pengaturan properti ini ke nilai arbitrer pada baris perintah atau di waktu lain selama eksekusi program.

Cara yang lebih disukai untuk mengubah pengkodean default yang digunakan oleh VM dan sistem runtime adalah dengan mengubah lokal platform yang mendasarinya sebelum memulai program Java Anda.

Saya merasa ngeri ketika melihat orang-orang mengatur pengkodean pada baris perintah - Anda tidak tahu kode apa yang akan mempengaruhi.

Jika Anda tidak ingin menggunakan encoding default, setel encoding yang Anda inginkan secara eksplisit melalui metode / konstruktor yang sesuai .

McDowell
sumber
4

Pertama, Latin-1 sama dengan ISO-8859-1, jadi defaultnya sudah OK untuk Anda. Baik?

Anda berhasil menyetel encoding ke ISO-8859-1 dengan parameter baris perintah Anda. Anda juga menyetelnya secara terprogram ke "Latin-1", tapi, itu bukan nilai yang dikenali dari pengkodean file untuk Java. Lihat http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html

Saat Anda melakukannya, sepertinya Charset disetel ulang ke UTF-8, dari melihat sumbernya. Setidaknya itu menjelaskan sebagian besar perilaku.

Saya tidak tahu mengapa OutputStreamWriter menampilkan ISO8859_1. Ini mendelegasikan ke kelas sun.misc. * Sumber tertutup. Saya menduga itu tidak cukup berurusan dengan pengkodean melalui mekanisme yang sama, yang aneh.

Tetapi tentu saja Anda harus selalu menentukan pengkodean apa yang Anda maksud dalam kode ini. Saya tidak pernah mengandalkan default platform.

Sean Owen
sumber
4

Perilakunya tidak terlalu aneh. Melihat ke dalam implementasi kelas, ini disebabkan oleh:

  • Charset.defaultCharset() tidak sedang meng-cache set karakter yang ditentukan di Java 5.
  • Menyetel properti sistem "file.encoding" dan memanggil Charset.defaultCharset()lagi menyebabkan evaluasi kedua terhadap properti sistem, tidak ada kumpulan karakter dengan nama "Latin-1" yang ditemukan, jadi Charset.defaultCharset()defaultnya adalah "UTF-8".
  • The OutputStreamWriteradalah namun caching karakter default set dan mungkin digunakan sudah selama VM inisialisasi, sehingga karakter default set pengalihan nya dari Charset.defaultCharset()jika sistem properti "file.encoding" telah berubah pada saat runtime.

Seperti yang telah ditunjukkan, tidak didokumentasikan bagaimana VM harus berperilaku dalam situasi seperti itu. The Charset.defaultCharset()dokumentasi API tidak sangat tepat tentang bagaimana set karakter default ditentukan, hanya menyebutkan bahwa itu biasanya dilakukan pada VM startup, berdasarkan faktor-faktor seperti set karakter default OS atau lokal default.

jarnbjo
sumber
3

Saya telah menetapkan argumen vm di server WS sebagai -Dfile.encoding = UTF-8 untuk mengubah set karakter default server.

Davy Jones
sumber
1

memeriksa

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

tampaknya pengkodean yang sama dengan yang digunakan di baris perintah sistem Anda.

neoedmund
sumber