Магазин портативной техники "Портатив"
:: на главную
Java > Русские буквы и не только...

Содержание:

Файлы данных, потоки, БД.

Итак, как все, надеюсь, знают, в языке Java для представления символов используется Unicode, т.е. по два байта на один символ (тип char размером в 16 бит). В набор символов входят всевозможные буквы со всякими чёрточками и припендюльками, греческие, математические и символы псевдографики. В том числе и так любимые нами символы кириллицы (диапазон значений 0x0400-0x04ff). Так что с этой стороны никакой дискриминации нет.

Если Вам интересны конкретные кода символов, для их просмотра удобно использовать программу "Таблица символов" из WinNT. Вот, например, диапазон кириллицы:

С другой стороны большинство файлов данных основано на 8-битовом представлении символов. Сюда входят также текстовые файлы и большинство баз данных (окромя наиболее продвинутых). Кроме того, что самое паршивое, одни и те же байты могут представлять разные символы (в зависимости от кодовой страницы). Налицо конфликт - как преобразовать одно в другое и наоборот, причём с наименьшими потерями для данных. Для этого был придуман довольно удобный механизм использования кодовых страниц. Для каждой кодовой страницы было создано по 2 класса перекодировки (ByteToChar и CharToByte). Классы эти лежат в пакете sun.io. Если, при перекодировке из char в byte не было найдено соответствующего символа, он заменяется на символ ?.

Кстати, эти файлы кодовых страниц в некоторых ранних версиях JDK 1.1 содержат ошибки, вызывающие ошибки перекодировок, а то и вообще исключения при выполнении. Например, это касается кодировки KOI8_R. Лучшее, что можно при этом сделать - сменить версию на более позднюю. Судя по Sun-овскому описанию, большинство этих проблем было решено в версии JDK 1.1.6.

Когда и как надлежит пользоваться этой перекодировкой? Когда пользоваться, в принципе, понятно - при любом преобразовании из byte в char и наоборот. В классе String в тех местах, где есть преобразование можно указать дополнительный параметр (String enc), задающий имя кодовой страницы. Это конструктор по массиву байтов и метод getBytes(). Однако, в реальной программе, явно указывать кодовую страницу не всегда удобно. Для этого была введена кодировка по умолчанию. По умолчанию она зависит от системы (для русских виндов принята кодировка Cp1251), и в старых JDK её можно изменить установкой системного свойства file.encoding. Вообще-то, как утверждают в Sun, это свойство отражает системную кодировку, и она не должна изменяться в командной строке (см., например, комментарии к BugID 4256423) Эта кодировка используется тогда, когда явно не указанно название страницы. Т.к. эта настройка одна на все преобразования, иногда можно наткнуться на неприятности. Например, эта же настройка используется для вывода на консольный экран, что, в случае виндов, как правило, неприемлемо - там нужно использовать страницу Cp866. Было бы здорово, если бы эти кодировки указывались независимо - например, console.encoding и т.п., но, думаю, Sun-овцам пока не до таких высоких материй.

Кстати, о выводе на консоль. Есть два пути решения вышеуказанной проблемы:

  1. Использовать вместо System.out.println свой класс, а уже в нём делать преобразование. Например:
 public class Msg
 {
  static String cp = System.getProperty("console.encoding","Cp866");

  public static void message(String msg)
  {
   msg += "\n";
   byte[] b;

   try { b = msg.getBytes(cp); }
   catch( UnsupportedEncodingException e )
	 {
	  b = msg.getBytes();	// В случае отсутствия нужной кодировки,
				// делаем преобразование по умолчанию
	 }

   System.out.write(b);
  }
 }

 ...
 Msg.message("Сообщение");
  1. Написать свою версию PrintStream, поддерживающую нужную кодировку, и подставить его через System.setOut() и System.setErr(). Вот, например, обычное начало в моих программах:
 ...
 public static void main(String[] args)
 {
  // Установка вывода консольных сообщений в нужной кодировке
  try
	{
	 System.setOut(new CodepagePrintStream(System.out,
		System.getProperty("console.encoding",
"Cp866")) ); } catch(UnsupportedEncodingException e) { System.out.println("Unable to setup console codepage: " + e); } ...

Куда же податься бедному российскому программисту? Не отчаивайтесь, есть множество путей прочитать и сохранить данные из файлов в нужной Вам кодировке.

  1. Читать и записывать массивы байтов (byte[]), а для перекодировки использовать упомянутые методы класса String. Этот способ особенно удобен, когда в потоке могут присутствовать данные в разных кодировках.
  2. Использовать классы InputStreamReader и OutputStreamWriter из пакета java.io, специально предназначенные для этих целей.
  3. Сделать преобразование в нужную кодировку. Если вы всё сделаете корректно, то данные не потеряются, хотя, конечно, пользоваться этим желательно только в крайнем случае. Пример:
 // Чтение русских букв в кодировке Cp866 через объект,
 // поддерживающий только Cp437
 String str = o.readString();

 str = new String(str.getBytes("Cp437"),"Cp866");

 // Сохранение русских букв в кодировке Cp866 через объект,
 // поддерживающий только Cp437
 str = new String(str.getBytes("Cp866"),"Cp437");

 o.writeString(str);

Более подробно о этом методе, и о возможных проблемах с ним, расписано ниже, в разделе О методе перекодировки символов.

  1. Настроить драйвер БД на нужную кодировку. Как именно - это зависит от конкретного драйвера. К сожалению, многие драйвера понятия не имеют о каких-то там кодировках. Иногда их можно пропатчить на этот счёт, но чаще всего приходится действовать обходными путями.

    Например, один из самых часто используемых драйверов - мост JDBC-ODBC. В версиях JDK 1.1, этот мост просто игнорировал кодировки символов, из-за чего нужно было предпринять дополнительные ухищрения, типа описанных в предыдущем пункте (это также касается и последней ихней версии, 1.1.8).

    Мост из комплекта Sun Java 2 теперь можно настроить на нужную кодировку. Это делается добавлением дополнительного свойства charSet в набор параметров, передаваемых для открытия соединения с базой. По умолчанию используется file.encoding. Делается это примерно так:

  // Параметры соединения с базой
  Properties connInfo = new Properties();

  connInfo.put("user", username);
  connInfo.put("password", password);
  connInfo.put("charSet", "Cp1251");

  // Устанавливаем соединение
  Connection db = DriverManager.getConnection(dataurl, connInfo);

Другой пример - драйвер JDBC-OCI (не pure Java - тот называется thin) от Oracle 8.0.5 под Linux. При получении данных из БД, драйвер определяет "свою" кодировку при помощи переменной окружения NLS_LANG. Если эта переменная не найдена, то он считает что кодировка - ISO88591. Весь фокус в том, что NLS_LANG должна быть именно переменной окружения, а properties (типа file.encoding) здесь "не катят". В случае использования драйвера внутри servlet engine Apache+Jserv, переменную окружения можно задать в файле jserv.properties:

  wrapper.env=NLS_LANG=American_America.CL8KOI8R

Информацию об этом прислал Сергей Безруков, за что ему отдельное спасибо.

Если же Вы свободны в формировании формата - тогда всё проще. Используйте формат Unicode или UTF8 - и проблем не будет.

В случае с БД, можно, конечно, использовать и какой-нибудь 16-ричный формат, но это не всегда приемлемо, т.к. Вы получите 2-х - 4-х кратный рост места на диске и потеряете возможность использовать стандартные программы работы с БД, например генераторы отчётов.


Русские буквы в исходниках Java-программ.

Как уже упоминалось, при выполнении программы используется Unicode. Исходные же файлы пишутся в обычных редакторах. Я пользуюсь Far-ом, у Вас, наверняка есть свой любимый редактор. Эти редакторы сохраняют файлы в 8-битовом формате, а значит, что к этим файлам также применимы рассуждения, аналогичные приведённым выше. Разные версии компиляторов немного по разному выполняют преобразование символов. В ранних версиях JDK 1.1.x используется настройка file.encoding, которую можно поменять при помощи нестандартной опции -J. В более новых (как сообщил Денис Кокарев - начиная с 1.1.4) был введён дополнительный параметр -encoding, при помощи которого можно указать используемую кодировку. В скомпилированных классах строки представлены в виде Unicode (точнее в модифицированном варианте формата UTF8), так что самое интересное происходит при компиляции. Поэтому, самое главное - выяснить, в какой кодировке у Вас исходники и указать правильное значение при компиляции. По умолчанию будет использован всё тот же пресловутый file.encoding. Пример вызова компилятора:

javac -encoding=KOI8_R ...

Кроме использования этой настройки есть ещё один метод - указывать буквы в формате "\uxxxx", где указывается код символа. Этот метод работает со всеми версиями, а для получения этих кодов можно использовать стандартную утилиту native2ascii.


Русские буквы в файлах properties.

Для чтения файлов properties используются методы загрузки ресурсов, которые работают специфичным образом. Собственно для чтения используется метод Properties.load, который не использует file.encoding (там кодировка жёстко указана), поэтому единственный способ указать русские буквы - использовать формат "\uxxxx" и утилиту native2ascii.

Метод Properties.save работает по разному в версиях JDK 1.1 и 1.2. В версиях 1.1 он просто отбрасывал старший байт, поэтому правильно работал только с англицкими буквами. В 1.2 делается обратное преобразование в "\uxxxx", так что он работает зеркально к методу load.


Русские буквы в Servlet-ах.

Ну, для чего эти самые Servlet-ы нужны, я думаю, Вы в курсе. Если нет - то лучше сначала прочитать документацию. Здесь же рассказывается только об особенностях работы с русскими буквами.

Так в чём же особенности? Когда Servlet посылает ответ клиенту, есть два способа послать этот ответ - через OutputStream (getOutputStream()) или через PrintWriter (getWriter()). В первом случае Вы записываете массивы байтов, поэтому применимы вышеописанные методы записи в файл. В случае же PrintWriter, он использует установленную кодировку. В любом случае необходимо правильно указать используемую кодировку при вызове метода setContentType(), для того, чтобы было правильное преобразование символов на стороне сервера. Это указание должно быть сделано перед вызовом getWriter() или перед первой записью в OutputStream. Пример:


 public void doPost(HttpServletRequest request,HttpServletResponse response)
	throws ServletException, IOException
 {
  response.setContentType("text/html; charset=windows-1251"));

  PrintWriter out = response.getWriter();

  // Отладочный вывод названия кодировки для проверки
  out.println( "Encoding: " + response.getCharacterEncoding() );
  ...
  out.close();
Это по поводу отдачи ответов клиенту. Со входными параметрами, к сожалению не так просто. Входные параметры кодируются броузером побайтно в соответствии с MIME-типом "application/x-www-form-urlencoded". Как рассказал Алексей Менделев русские буквы броузеры кодируют, используя текущую установленную кодировку. Ну и, разумеется, ничего о ней не сообщают. Соотвественно, например, в JSDK 2.0 и 2.1 это никак не проверяется. Собственно для раскодирования используются методы HttpUtils.parsePostData() и HttpUtils.parseQueryString(), которые просто обнуляют старший байт. Это зарегистрированная ошибка в JSDK (4154966). К сожалению, эту ошибку закрыли как "Will not be fixed", с тем оправданием, что, дескать, раз в RFC на эту тему ничего не сказанно, то и делать мы ничего не будем. Однако, после переписки наших разработчиков в майл-листе SERVLET-INTEREST дело, похоже, сдвинулось с мёртвой точки. По крайней мере на словах было обещано включить метод установки кодировки в спецификацию JSDK 2.3.

Пока же приходится обходиться своими средствами. Оригинальный способ работы с кодировками предлагает Russian Apache - здесь расписано, как именно. Судя по отзывам, не имеет проблем с русскими и система Resin.

Своё решение проблемы так же предложил Вячеслав Педак.

Ну а самый простейший вариант извлечь таки символы - передавать в комплекте параметров имя кодировки (или, если вы уверены в текущей кодировке броузера, использовать предопределённую кодировку) и использовать метод перекодировки символов:


 public void doPost(HttpServletRequest request,HttpServletResponse response)
	throws ServletException, IOException
 {
  // Кодировка сообщений
  // В связке MSIE 4.01 SP1 -> JSDK 2.0 servletrunner.exe всегда выдаёт "ISO-8859-1"
  String requestEnc = request.getCharacterEncoding();

  // Некоторые servlet engine, не мудрствуя лукаво, возвращают null
  if( requestEnc==null ) requestEnc="ISO-8859-1";

  String clientEnc = request.getParameter("charset");

  if( clientEnc==null ) clientEnc="Cp1251";

  String value = new String(request.getParameter("value")
				.getBytes(requestEnc),clientEnc);
Изврат, конечно, но зато работает. :-)

В общем, опыт в написании Servlet-ов у меня небольшой, так что Ваши замечания будут приветствоваться.


CORBA

В стандарте CORBA предусмотрен тип, соответствующий Java-овскому типу String. Это тип wstring. Всё бы хорошо, но некоторые CORBA-сервера не поддерживают его в полной мере. Типичное исключение, возникающие при спотыкании на русских буквах: org.omg.CORBA.MARSHAL: minor code 5 completed No. Лучше всего, конечно, заменить CORBA-сервер. К сожалению у меня нет статистики, поэтому я не могу сказать, с какими проблем не будет. Если сменить систему не представляется возможным, можно вместо типа wstring использовать тип string в паре с нашим любимым преобразованием:

// Серверная часть
a = new Answer(new String( src.getBytes("Cp1251"),"ISO-8859-1" ));

...

// Клиентская часть
Answer answer=serverRef.getAnswer();
res = new String( answer.msg.getBytes("ISO-8859-1"),"Cp1251" );
Тип wstring при этом лучше не использовать, потому как тем самым Вы кривость сервера будете компенсировать кривостью своих компонентов, а это практически всегда чревато разнообразными проблемами в будущем.

Вместо Cp1251 можно использовать любую кодировку русских букв, по желанию. Это будет кодировка, в которой будут передаваться строки в компоненты на других языках. Также, аналогичный код может потребоваться, если необходимо организовать связь с готовыми не-Java компонентами, которые уже использовали тип string.

Честно говоря, не лежит у меня душа к таким решениям, ну да что поделаешь, иногда оно единственное.

<< НАЗАД