Triangle
Сайт разработчика
RSS
  • Home Page Home
  • Clover WorkSpace
  • Melon
  • Gefest Web Server
  • Donate :: Жертвовать
  • Contact

Тестовый почтовый сервер для php

Development, PHP, Uncategorized 0 Comment »

Для разработки веб приложений необходимо тестировать email сообщения генерируемые на сайте.

Для этого можно использовать любой почтовый сервер.

Exim, sendmail для примера или тот же James и многие другие.

Но есть маленький недостаток, это то что необходимо создавать сначала почтовые ящики или настраивать пересылку и т.д.

Если вам необходимо работать с большим количеством тестовых
ящиков то эта статья для вас.

  1. Качаем Melon Mail Server http://3le.org/ или https: / / sourceforge.net / projects / melonserv /Для тестирования своих приложений я использую Melon Mail Server 1.0
    это простой эмулятор SMTP, POP3 который принимает все сообщения.
    Главный недостаток сервера это то, что он не пересылает почту на реальные почтовые ящики, но для тестирования это то что надо.
  2. Распаковываем архив
  3. Открываем конфигурационный файл melon.conf# Melon 0.1melon.smtp.port = 25 # порт для SMTP сервера
    melon.pop3.port = 110 # порт для POP3 сервера
    melon.pop3.host = 127.0.0.1
    melon.smtp.host = 127.0.0.1
    melon.data.dir =. Melon # папка в которой будет храниться база данных сообщений.

    Настройки по умолчанию означают, что будут использоваться стандартные порты, но они могут быть другие например, если другой сервер уже использует 25 или 110 порт.

  4. Запускаем ServiceInstall.bat скрипт который устанавливаєт сервер как Windows сервис.

    По умолчанию сервис инсталлируется запуск вручную (не запускается автоматически) для этого необходимо открыть “Control Panel \ System and Security \ Administrative Tools” потом необходимо открыть Services найти сервис Melon

    Выбираем автоматическую загрузку и сохраняем

  5. Настройка php
    открываем ваш php.ini и находим секцию [mail function]там есть 2 строчки которые надо поменять, но может у вас уже все настроено
    SMTP = localhost
    smtp_port = 25

    Если вы использовали другие порты здесь нужно указать.

    Останавливаем и запускаем ваш веб сервер для того, чтобы настройки php вошли в силу.

  6. Настраиваем pop3 почтовый клиент
    Я использую The BatСоздаем почтовый ящик
    сервер входящих сообщений: 127.0.0.1
    сервер исходящих сообщений: 127.0.0.1

    Имя пользователя здесь не важно в данном случае я использую
    test – имя пользователя
    test – пароль

Для тестирования своих приложений я использую Melon Mail Server 1.0
это простой эмулятор SMTP, POP3 который принимает все сообщения.
Главный недостаток сервера это то, что он не пересылает почту на реальные почтовые ящики, но для тестирования это то что надо.

February 2nd, 2011  
Tags: Mail Server, Mail server Emulator, Mail server Emulator for PHP, POP3, SMTP, Почтовий симулятор, Почтовий симулятор для PHP



Реализация Flash на языке JavaScript и Web-сервис для преобразования FLV в Ogg

Development 0 Comment »

Фонд свободного ПО анонсировал введение в строй сайта tinyogg.com, позволяющего преобразовывать видеоролики в формате FLV в Ogg представление, для просмотра которого не требуется установка Flash-плагина и можно обойтись штатной поддержкой HTML5 тега video в браузере Firefox. После преобразования ролика в Ogg-формат он остаётся доступным для повторной загрузки в течение 48 часов. Для преобразования используются свободные программы ffmpeg2theora, URip и WatchVideo.

Кроме того, опубликованы исходные тексты проекта Gordon, представляющего собой реализацию технологии Flash, выполненную целиком на языке JavaScript. Принцип действия программы основан на прозрачном преобразовании SWF файлов в SVG-формат, с последующим его выполнением через штатный механизм поддержки SVG в браузере. В настоящий момент транслятор поддерживает только функции, свойственные спецификации SWF 1.0, что ограничивает его применение простейшими Flash роликами.

В текущем состоянии Gordon поддерживает работу в браузерах Firefox, Safari и Chrome. Исходные тексты разработки доступны под свободной лицензией MIT. Демонстрацию работы скрипта можно посмотреть здесь.

Дополнительно можно отметить, что на ресурсе по сбору идей по развитию сайта YouTube на первом месте числится пожелание о реализации поддержки представления видео через HTML5 тег “video” в формате Ogg Theora.

site:http://www.opennet.ru/opennews/art.shtml?num=25013


October 8th, 2010  
Tags: ffmpeg, flv, ogg



Организация вещания потокового Flash-видео средствами ffserver и ffmpeg

Development 2 Comments »

ffserver идёт в комплекте с ffmpeg и выполняет роль медиа-сервера - получает
видеопоток от ffmpeg, который может быть запущен на другой машине, и раздаёт
его пользователям.

Каждый получаемый поток называется Feed-ом (далее будет просто фид). Таких
потоков может быть несколько, так же как и отдаваемых (выходных). FFmpeg у нас
будет захватывать видео с веб-камеры или читать из файла.

Сначала настроим ffserver, правим /etc/ffserver.conf:

   Port 8090
   BindAddress 0.0.0.0
   MaxClients 100
   MaxBandwidth 20000
   NoDaemon

   # Фид, надо запомнить feed.ffm, он нам потребуется при запуске ffmpeg
   <Feed feed.ffm>
      File /tmp/feed.ffm
      FileMaxSize 3M
   </Feed>
   <Stream test.flv>
      # Ранее объявленный фид
      Feed feed.ffm
      Format flv
      VideoCodec flv
      VideoFrameRate 30
      VideoBufferSize 80000
      VideoBitRate 200
      # Качество видео от 1 до 31, 1 == отлично 31 == фи!
      VideoQMin 1
      VideoQMax 5
      # Разрешение, везде должно быть одинаково!
      VideoSize 352x288
      PreRoll 1
      # Если у вас есть микрофон, или вы транслируете видео со звуком, закомментируйте эту строчку
      Noaudio
   </Stream>

   # Флешка test.swf для просмотра трансляции
   <Stream test.swf>
      Feed feed.ffm
      # Будет запускаться в Adobe Flash Player
      Format swf
      VideoCodec flv
      VideoFrameRate 30
      VideoBufferSize 50000
      VideoBitRate 100
      VideoQMin 1
      VideoQMax 5
      # Разрешение, везде должно быть одинаково!
      VideoSize 352x288
      PreRoll
      # Если у вас есть микрофон, или вы транслируете видео со звуком, закомментируйте эту строчку
      46. Noaudio
   </Stream>

Запускаем ffserver:

   $ ffserver

Запускаем ffmpeg:

   $ ffmpeg -s 352x288 -r 30 -f video4linux2 -i /dev/video0 http://localhost:8090/feed.ffm

-s разрешение, указанное в конфурации ffserver, 
-r количество кадров/секунду, 
-f формат, 
-i путь к камере (или к видео-файлу), 
http://localhost:PORT/FEED - это адрес, где ffserver будет ждать наш поток

PS: если выдаст ошибку, то можно попробовать формат v4l (-f video4linux)

Открываем в браузере http://localhost:8090/test.swf 

Для отправки видео поменяем параметр -i на путь к файлу, уберём -f (ffmpeg сам
определит формат файла):

   $ ffmpeg -s 352x288 -r 30 -i ~/big_buck_bunny.ogg http://localhost:8090/feed.ffm

Ну а что бы вставить нашу флешку в html-страницу используем следующий код:

   <embed src="http://localhost:8090/test.swf" width="550" height="400"></embed>

October 8th, 2010  
Tags: ffmpeg, ffserver, flv, video



Создаем свой YouTube – Как обработать видео средствами веб-сервера (ffmpeg video stream flv mpeg 3gp)

Development 3 Comments »

Материал предоставлен редакцией журнала Системный администратор.
   Опубликовано в журнале "Системный администратор" N 12 2009 

   Каждый веб-разработчик должен иметь представление о том, как можно
   обработать видео средствами веб-сервера и автоматизировать
   конвертирование из одного формата в другой

С чего начать?

   Существует несколько инструментов для работы с видео, среди которых
   наибольшее распространение получили Mencoder, Transcoder и FFmpeg.

   Мы рассмотрим работу с FFmpeg, так как он наиболее удобен, является
   кроссплатформенным и обладает всем необходимым функционалом для работы
   с видео в WEB, а главное, у него есть API для удобной работы из языка
   программирования PHP, поставляемый в виде расширения. Мощь данному
   инструменту придает библиотека libavcodec. Родословные почти всех
   открытых программ для работы с медиа восходят к этим двум компонентам.

   Mencoder - часть медиаплеера Mplayer и DVD-риппера AcidRip. Transcode
   используется в таком известном приложении, как dvd::rip. И Mencoder и
   Transcode тоже основаны на использовании библиотеки libavcodec.

Немного слов о FFmpeg

   FFmpeg - это набор библиотек с открытым исходным кодом, которые
   позволяют работать (записывать, конвертировать и транслировать) аудио и
   видео в различных форматах. Сам набор включает в себя следующие
   компоненты:

   *  ffmpeg - утилита командной строки для конвертирования формата
   видеофайла. С её помощью можно также захватывать видео в реальном
   времени с TV-карты;


   *  ffserver - HTTP (RTSP в настоящее время разрабатывается) потоковый
   сервер для видео/радиовещания;


   *  ffplay - простой медиаплеер, основанный на SDL и библиотеках FFmpeg;


   *  libavcodec - библиотека, в которой содержатся все аудио/видеокодеки.
   Большинство кодеков были разработаны "с нуля" для обеспечения
   наилучшей производительности;


   *  libavformat - библиотека мультиплексирования и демультиплексирования
   в медиаконтейнер;


   *  libavutil - вспомогательная библиотека, содержащая стандартные,
   общие подпрограммы для различных компонентов ffmpeg. Включает в себя:
   adler32, crc, md5, sha1, lzo-декомпрессор, Base64-кодер/декодер,
   des-шифратор/дешифратор rc4-шифратор/дешифратор и
   aes-шифратор/дешифратор;


   *  libpostproc - библиотека, содержащая стандартные подпрограммы
   обработки видео;


   *  libswscale - библиотека, предназначенная для масштабирования видео.


   *  libavfilter - является заменой vhook, которая позволяет изменять
   видеопоток между энкодером и декодером на лету.


   Название самого проекта FFmpeg происходит от названия экспертной группы
   "MPEG" и "FF", означающего "fast forward". Данный проект основал
   Фабрис Беллар (известный под псевдонимом "Gerard Lantau"). Фабрис
   Беллар (фр. Fabrice Bellard) - французский программист, автор ряда
   известных проектов в сфере свободного программного обеспечения,
   к примеру, таких как: QEMU (свободная программа для эмуляции
   аппаратного обеспечения различных платформ с открытым исходным кодом),
   Tiny C Compiler (компактный компилятор C). В настоящее время проект
   поддерживает Митчел Недермайер (Michael Niedermayer). Многие
   разработчики FFmpeg работают над проектом Mplayer. Кстати, сам FFmpeg
   располагается на сервере Mplayer.

   FFmpeg разработан под операционные системы на основе GNU/Linux, однако
   может быть портирован и под многие другие ОС, в том числе и Windows.
   Разработчики не выпускают релизов и рекомендуют использовать последний
   снимок из Subversion. Распространяется FFmpeg под открытыми лицензиями
   GNU LGPL или GNU GPL.

   FFmpeg работает со следующими кодеками:

   *  ATRAC3.
   *  H.261, H.263 и H.264.
   *  Intel Indeo 2 и 3.
   *  QDesign Music Codec 2, используемый в QuickTime до QuickTime 7.
   *  Sorenson 3 Codec используемый в QuickTime.
   *  Theora (вместе с Vorbis используется в контейнере Ogg).
   *  Truespeech.
   *  TXD.
   *  VP5 и VP6.
   *  Vorbis.
   *  Windows Media Audio.
   *  Некоторые Windows Media Video-кодеки, включая WMV1, WMV2 и WMV3.



   Форматы, которые поддерживает FFmpeg:

   *  ASF, и через него оригинальную версию DivX.
   *  AVI.
   *  FLV.
   *  Matroska.
   *  MPEG transport stream.
   *  TXD.


   Для веб-разработчиков на PHP к FFmpeg было написано расширение
   ffmpeg-php. Данное расширение добавляет удобный
   объектно-ориентированный программный интерфейс для работы с видео и
   аудио посредством аfmpeg.

Первые шаги

   Прежде чем приступить к работе с FFmpeg, для начала его надо
   установить. В зависимости от вашей ОС, установка, естественно, будет
   отличаться. Если у вас операционная система семейства GNU/Linux, то,
   скорее всего, вы будете устанавливать его из репозитория, через
   встроенный менеджер пакетов. Это самый лучший вариант, так как FFmpeg
   имеет очень много зависимостей, которые легко удовлетворяются
   менеджером пакетов в автоматическом режиме. Если у вас, к примеру,
   openSUSE, то вы воспользуетесь YaST'ом, если Ubuntu или Debian-подобный
   дистрибутив, то вы, скорее всего, будете ставить его через apt. Если у
   вас FreeBSD, то лучше воспользоваться системой портеджей. Во FreeBSD
   FFmpeg находится в /usr/ports/multimedia/ffmpeg. Далее выполняете
   стандартные команды конфигурации и сборки "make config && make
   configure && make && make install && make clean", после чего у вас
   будет собран и установлен FFmpeg из репозитория.

   Надо заметить, что не все так просто. Многие кодеки имеют не полностью
   свободные лицензии. В силу этого, как правило, FFmpeg приходится
   скачивать со сторонних (не дистрибутивных) репозиториев. Например, для
   того же openSUSE с Packman. Там же и для Fedora, RedHut, CentOS. А это
   значит, что надо настраивать дополнительно yast/apt/yum/zypper в
   зависимости от вашей операционной системы.

   Если все сделано правильно, то, набрав в консоли команду:

           ffmpeg -v


   вы должны получить сведения о версии программы и опциях сборки. Это
   может выглядеть примерно так:

           FFmpeg version SVN-r20024, Copyright (c) 2000-2009
           Fabrice Bellard, et al.
             configuration: --shlibdir=/usr/lib64 --prefix=/usr
           --mandir=/usr/share/man --libdir=/usr/lib64 --enable-shared
           --enable-libmp3lame --enable-libvorbis --enable-libtheora
           --enable-libspeex --enable-libfaad --enable-libfaac
           --enable-nonfree --enable-libxvid --enable-postproc
           --enable-gpl --enable-x11grab --enable-libschroedinger
           --enable-libdirac --enable-libgsm --enable-version3
           --enable-libopencore-amrnb --enable-libopencore-amrwb
           --enable-libx264 --enable-libdc1394 --enable-pthreads

             libavutil     50. 3. 0 / 50. 3. 0
             libavcodec    52.35. 0 / 52.35. 0
             libavformat   52.38. 0 / 52.38. 0
             libavdevice   52. 2. 0 / 52. 2. 0
             libswscale     0. 7. 1 /  0. 7. 1
             libpostproc   51. 2. 0 / 51. 2. 0
             built on Sep 27 2009 15:47:36, gcc: 4.3.2
             [gcc-4_3-branch revision 141291]


   Как уже было сказано выше, для PHP-программистов существует удобный API
   для работы с FFmpeg, но это не значит, что работать с ним можно только
   через данную библиотеку. В жизни бывает так, что на целевом сервере у
   клиента нет возможности доставить нужные расширения для PHP и что-то
   переконфигурировать. Но там может стоять FFmpeg как системная утилита,
   доступная из консоли. Не стоит забывать, что ffmpeg-php - это всего
   лишь удобная обертка над консольной версией программы, поэтому нам
   ничто не мешает в наших программах обращаться напрямую к FFmpeg через
   системные вызовы. Работать в консоли с ffmpeg достаточно несложно,
   нужно просто знать заветные ключики и параметры. Итак, приступим.

   Для начала определимся, что мы хотим получить и ради чего будем всем
   этим заниматься. Допустим, у нас есть большое желание сделать сервис,
   аналогичный YouTube и ему подобным. Ориентируясь на современные
   потребности и возможности технологий, наш сервис будет доступен не
   только для обычных персональных компьютеров, но и для мобильных
   телефонов. В "большом вебе" мы будем отдавать видео для просмотра в
   формате FLV, что позволит пользователю смотреть видео прямо в браузере.
   Для других устройств, в частности, мобильных телефонов, мы будем
   отдавать видео в формате 3GP. Так же у нас будет задача делать для всех
   загружаемых видеороликов превью и с последующим брендированием как
   самих превью файлов, так и загруженного видео. Собственно, с целями
   определились, теперь приступим к реализации нашего технического
   задания.

   Начнем с полезных команд. Чтобы получить информацию о файле, достаточно
   его просто передать на вход программы. Входной файл задается ключом -i
   (от слова input):

           ffmpeg -i 1.3gp


   Вывод данной команды будет примерно таким (здесь опущена информация о
   версии FFmpeg):

           Seems stream 0 codec frame rate differs from container
           frame rate: 30060.00 (30060/1) -> 29.98 (45000/1501)
           Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '1.3gp':
             Duration: 00:01:00.00, start: 0.000000, bitrate: 137 kb/s
               Stream #0.0(eng): Video: mpeg4, yuv420p, 176x144
               [PAR 1:1 DAR 11:9], 29.98 tbr, 90k tbn, 30060 tbc
               Stream #0.1(eng): Audio: libopencore_amrnb, 8000 Hz,
               1 channels, s16
                 Metadata
               muxer           : avc2.0.11.1110
               muxer-jpn       : avc2.0.11.1110
           At least one output file must be specified


   Как правило, самый распространенный формат для загрузки на сервер - это
   AVI. Нам же надо будет еще иметь, соответственно, FLV, MP4 и 3GP, к
   примеру.

FLV-конвертирование

   Формат FLV предназначен для потокового видео, однако существует
   возможность использовать его и для локального хранения и
   воспроизведения видео. FLV используется в Adobe Flash Player, который
   распространяется в качестве плагина для различных браузеров и
   операционных систем. Формат поддерживается многими
   мультимедиапроигрывателями, такими как FFmpeg и Mplayer. Так как FLV -
   это медиа-контейнер, а не формат, некоторые проигрыватели могут
   некорректно воспроизводить видео или звуковой поток при отсутствии
   кодеков, использованных при создании файла. FLV-формат частично
   проприетарный. Кодеки закрыты и защищены патентами, а сам контейнер
   открытый. FLV-файл представляет собой битовый поток, являющийся
   вариантом видеостандарта H.263. Это стандарт сжатия видео,
   предназначенный для передачи по каналам с низкой пропускной
   способностью (128 кбит/с и ниже).

   Звук в FLV, как правило, закодирован в MP3. Но иногда могут
   использоваться Nellymoser codec, несжатое аудио или ADPCM-аудиоформат.
   В версии Flash Player 9 Update 3, в соответствии с внедрением Adobe
   формата ISO Base (MPEG-4 Part 12), добавлена поддержка AAC-аудио
   (профили AAC-LC, Main Profile и HE-AAC).

   В общих чертах организация собственного видеохостинга происходит
   следующим образом: пользователь закачивает на сервер исходный файл, на
   сервере происходит его перекодирование в нужный формат, создается
   превью, которое в дальнейшем используется как заставка и
   минимизированная картинка. Исходный файл по желанию сохраняется или
   удаляется ради экономии дискового пространства.

   Теперь переходим плавно от теории к практике. Чтобы сконвертировать
   AVI-файл в FLV-формат, достаточно вызвать FFmpeg таким образом:

           ffmpeg -i uploaded_file.avi output_file.flv


   В результате наших действий мы получим FLV-версию файла. Если мы хотим
   задать битрейт, то надо указать параметр "-b bitrate" - битрейт,
   параметр, определяющий качество, по умолчанию 200 кбит/с. К примеру:
   "-b 512k" означает, что задано качество в 512 кбит/с.

   Также может быть полезной опция "-t duration" - продолжительность
   проигрывания и "-ss start_pos" - смещение от начала исходного
   видеофрагмента (можно задать как количество секунд, так и время в
   формате ЧЧ:ММ:СС.Д).

   FFmpeg умеет по расширению имени выходного файла (output_file)
   определять параметры кодирования для этого файла (аудио/видео кодеки и
   прочее), так что обычно не приходится указывать их вручную при помощи
   соответствующих опций. Допустим, мы хотим для мобильной версии сайта
   оптимизировать файлы и убирать первые несколько секунд ролика, которые,
   как правило, не несут смысловой нагрузки.

           ffmpeg -i uploaded_file.avi -ss 00:00:16.0 -b 512k output_file.flv


   Этой командой мы отрезаем первые 16 секунд ролика и задаем битрейт в
   512 кбит/с.

3GP-конвертирование

   3GP (сокращение от англ. 3rd generation (mobile) phone) - это
   видеофайлы для мобильных телефонов третьего поколения. Некоторые
   современные мобильные телефоны (не обязательно 3G) имеют функции записи
   и просмотра аудио и видео в формате 3GP. Этот формат - упрощённая
   версия ISO 14496-1 Media Format, который похож на MOV, используемый
   QuickTime. 3GP сохраняет видео как MPEG-4 или H.263. Аудио сохраняется
   в форматах AMR-NB или AAC-LC. Готовые видеоролики в формате имеют малый
   размер по сравнению с другими форматами видео, но, к сожалению, за счет
   жертвы качеством.

   Чтобы произвести конвертацию файла в формат 3GP, нужно проделать почти
   то же самое, что и с конвертацией файла в FLV-формат, но только задать
   соответствующее расширение выходного файла и дополнительные настройки
   кодеков. Наша команда в консоли будет выглядеть так:

           ffmpeg -i inputfile.avi -s qcif -vcodec h263 -acodec aac -ac 1 -ar 8000 \
                  -r 25 -ab 32 -y outputfile.3gp


   Вы можете не хранить оригинальный AVI-файл, но, если вдруг вам
   понадобится этот формат, вот пример обратного преобразования:

           ffmpeg -i input_clip.3gp -f avi -vcodec xvid -acodec mp3 -ar 22050 output_file.avi


   Правда, надо заметить, что сжатие было с потерей качества изначально,
   поэтому мы получим файл в формате AVI, но качество будет таким как у
   3GP.

Кодирование в MPEG-4

   MPEG-4 - это международный стандарт, используемый преимущественно для
   сжатия цифрового аудио и видео. Он появился в 1998 году и включает в
   себя группу стандартов сжатия аудио и видео и смежные технологии,
   одобренные ISO - Международной организацией по стандартизации/IEC
   Moving Picture Experts Group (MPEG). Стандарт MPEG-4 в основном
   используется для вещания (потоковое видео), записи фильмов на
   компакт-диски, видеотелефонии (видеотелефон) и широковещания, в которых
   активно используется сжатие цифровых видео и звука.

   MPEG-4 включает в себя многие функции MPEG-1, MPEG-2 и других подобных
   стандартов, добавляя такие функции, как поддержка языка виртуальной
   разметки VRML для показа 3D-объектов, объектно-ориентированные файлы,
   поддержка управления правами и разные типы интерактивного медиа. AAC
   (Advanced Audio Codec - или улучшенный аудиокодек) был стандартизован
   как дополнение к MPEG-2 (часть 7), был также расширен и включен в
   MPEG-4.

   MPEG-4 всё ещё находится на стадии разработки и делится на несколько
   частей. Ключевыми частями стандарта MPEG-4 являются часть 2 (MPEG-4
   part 2, включая Advanced Simple Profile, используемый такими кодеками
   как DivX, Xvid, Nero Digital и 3ivx, а также Quicktime 6) и часть 10
   (MPEG-4 part 10/MPEG-4 AVC/H.264 или Advanced Video Coding,
   используемый такими кодеками как x264, Nero Digital AVC, Quicktime 7, а
   также используемый в форматах DVD следующего поколения, таких как HD
   DVD и Blu-ray Disc). MPEG-4 Part 10 имеет обозначение H.264. Данный
   формат предназначен для достижения высокой степени сжатия видеопотока
   при сохранении высокого качества. Соответственно, кодирование в H.264
   будет осуществляться следующим образом:

           ffmpeg -i input_file.avi -vcodec h264 -threads 0 -r 25 -g 50 -b 500k \
                  -bt 500k -acodec mp3 -ar 44100 -ab 64k out_file.mp4


Вытаскиваем саундтрек из фильма

   Возможно, вы захотите позволить пользователям своего сервиса скачивать
   саундтреки из фильмов или просто аудиодорожки. Как это сделать? С
   FFmpeg проще простого. Для начала надо получить сведения о файле и
   определить параметры звуковой дорожки. Допустим, у нас есть видеофайл.
   Мы узнаем с помощью FFmpeg формат кодирования звука в требуемом файле.
   К примеру, звук в нашем файле идентифицируется как:

           Stream #0.1:Audio: mp3, 44100Hz, stereo, s16, 80kb/s


   Теперь у нас достаточно информации для извлечения звука из видеоролика.
   Мы просто пишем в консоли:

           ffmpeg -i input_file.flv -vn -acodec copy soundtrack.mp3


   Флаг -vn указывает на то, что мы не хотим работать с видео и оно нам не
   нужно. Команда copy указывает нашему FFmpeg на то, что следует
   кодировать выходной файл тем же самым кодеком, которым он
   раскодируется. По желанию вы можете изменить частоту дискретизации и
   битрейт.

   FFmpeg также позволяет извлекать аудио из AVI с высоким качеством
   звуковой дорожки для записи, к примеру, на аудио-CD-носитель. Пример
   извлечения такой дорожки:

           ffmpeg -i input_file.avi -vn -acodec pcm_s16le -ar 44100 -ac 2 output_file.wav


   Таким образом будет получена несжатая двухканальная аудиозапись с
   частотой дискретизации 44100 Гц и 16-битным качеством.

Контроль над содержимым

   Допустим, у вас сервис получил свое развитие и вы стали популярны. К
   вам "льют" видеоролики в больших количествах, но при этом ваш сервис
   ориентирован на детскую аудиторию, к примеру. Вы сами решите, как
   применять полученные знания на практике. Итак, у нас кто-то залил
   мультфильм про Винни Пуха, но со своей версией озвучивания, содержащей
   нецензурные выражения и прочие недетские высказывания.

   Вы, при модерации можете вырезать звук из данного ролика, оставив тем
   самым его без аудиодорожки, сообщив автору контента, что на вашем сайте
   запрещены ролики с ненормативной лексикой, и вы просите его перезалить
   с нормальным содержанием, либо ролик так и останется без
   аудиосодержимого. Чтобы проделать такую операцию, достаточно выполнить
   команду вида:

           ffmpeg -i input_censored_file.flv -an -vcodec copy output_censored_file.flv


   Параметр -an аналогичен параметру -vn, только указывает на то, что мы
   не хотим обрабатывать звук.

Делаем превью для видео

   Как мы уже говорили, на сайте нужно еще показать превью - маленькое
   изображение кадра, чтобы заинтересовать посетителя и дать
   предварительную информацию о содержимом. Все это также умеет делать
   FFmpeg. Достаточно указать ему, что мы хотим вытащить кадр и указать
   формат, в котором мы хотим сохранить его.

           ffmpeg -i video_input_file.flv -an -ss 30 -vframes 1 -s 340.180 -y -f mjpeg screenshot.jpg


   Таким образом, мы создали графический файл в формате JPEG, взяв кадр на
   30-й секунде.

   Если нам надо сделать анимированное превью в формате GIF, то достаточно
   указать количество кадров, количество цветов и формат выходного файла:

           ffmpeg -i input_file.avi -an -pix_fmt rgb24 -ss 40 -vframes 64 -s 128.128 \
                  -loop_output 0 -f gif -y screenshot.gif


   Обратите внимание на -loop_output - это количество повторов. Если его
   не указать, то он будет по дефолту равен единице. Если указать ноль, то
   анимация будет бесконечной.

   Если вы хотите узнать больше информации о ключах и параметрах FFmpeg,
   то вам достаточно набрать в консоли команду:

           ffmpeg -h


   либо пройтись по ссылке http://itbroadcastanddigitalcinema.com/ffmpeg_howto.html , где дана
   подробная расшифровка всего справочного материала.

   Собираем ffmpeg-php под *nix

   Сначала загружаем само расширение для PHP с сайта:
   http://sourceforge.net/projects/ffmpeg-php/files/ffmpeg-php

   Установка в принципе не составит особого труда. Распаковываем архив с
   исходными файлами командой:

           tar -xjf ffmpeg-php.tbz2


   Далее в директории с расширением запускаем утилиту phpize (входит в
   дистрибутив PHP) для сборки конфигурационного файла. Если у вас нет
   утилиты phpize, то вы можете установить пакет php-devel через ваш
   менеджер пакетов либо скачать и установить PHP с официального сайта.

           $ cd ffmpeg-php/
           $ phpize


   Далее конфигурируем и собираем расширение:

           $ ./configure && make
           $ sudo make install


   После сборки получаем библиотеку ffmpeg.so (для ОС Windows это будет
   ffmpeg.dll). Ее нужно вписать в php.ini с помощью директивы extension.

   Проверить, подключен ли ffmpeg-php, можно несколькими способами.
   Например, посмотреть в phpinfo информацию о расширении. Если оно
   подключено, то вы увидите примерно следующее (см. рис. 2).

   Если вы пишете консольную версию программы, то проверить информацию о
   ffmpeg-php можно и в консоли:

           $ php -i | grep ffmpeg


Возможные проблемы

   Не всегда сборка программного обеспечения проходит гладко. Бывают и
   проблемы. Я опишу два наиболее распространенных случая, которые
   случались в моей практике, и пути их решения. Один из вариантов ошибок
   приведен ниже:

           /home/0xfa/ffmpeg-php/ffmpeg-php.c -fPIC -DPIC -o .libs/ffmpeg-php.o
           In file included from /home/0xfa/ffmpeg-php-0.6.0/ffmpeg-php.c:42:

           /usr/local/include/ffmpeg/avcodec.h:30:30: error: libavutil/avutil.h:
           No such file or directory

           /usr/local/include/ffmpeg/avcodec.h:262:5: error: missing binary
           operator before token "("

           /usr/local/include/ffmpeg/avcodec.h:323:5: error: missing binary
           operator before token "("

           /usr/local/include/ffmpeg/avcodec.h:436:5: error: missing binary
           operator before token "("

           /usr/local/include/ffmpeg/avcodec.h:442:5: error: missing binary
           operator before token "("

           In file included from /home/0xfa/ffmpeg-php-0.6.0/ffmpeg-php.c:42:

           /usr/local/include/ffmpeg/avcodec.h:817: error: expected ':', ',', ';',
           '}' or '__attribute__' before '*' token


   У меня данная проблема встречалась на FreeBSD. При сборке FFmpeg не мог
   найти нужных заголовочных файлов. Данную проблему я решил тем, что
   скопировал /usr/local/include/libavutil в
   /usr/local/include/ffmpeg/libavutil.

   Еще один распространенный случай:

           Exit with error ... /ffmpeg-php/ffmpeg_frame.c:498: error:
           `PIX_FMT_RGBA32` undeclared (first use in this function) ...


   Лечится это правкой исходных файлов. Достаточно открыть файл
   ffmpeg_frame.c и сделать в нем исправления, а именно - заменить
   PIX_FMT_RGBA32 на PIX_FMT_RGB32. Сохраняем и пересобираем. Все должно
   пройти гладко.

Установка ffmpeg-php под ОС Windows

   Если у вас операционная система Windows, то сборка будет немного
   сложнее, как это ни странно. Скорее всего я просто так привык к
   UNIX-подобным системам и отвык от Windows, что мне кажется это немного
   сложнее. Итак, у меня стоит операционная система Windows XP SP2. Чтобы
   собрать библиотеку, понадобится Microsoft Visual Studio 2005 Express
   Edition, которую можно взять отсюда http://www.microsoft.com/express/2005/download/default.aspx

   Также нужно установить Microsoft Platform SDK, который забираем отсюда
   http://www.microsoft.com/downloads/details.aspx?familyid=0baf2b35-c656-4969-ace8-e4c0c0716adb.

   Еще потребуются LGPL Shared-библиотеки и заголовочные файлы FFmpeg для
   Windows. Их можно взять по этой ссылке http://ffmpeg.arrozcru.org/builds

   Далее нам потребуются заголовочные файлы inttype.h и stdint.h, которые
   можно взять здесь:

   * http://msinttypes.googlecode.com/svn/trunk/inttypes.h.
   * http://msinttypes.googlecode.com/svn/trunk/stdint.h.


   Создайте директорию, в которой будут размещаться все необходимые файлы.
   У меня это C:\dev\php\ffmpeg. Распакуйте туда исходные коды PHP и
   ffmpeg-php, все библиотеки и заголовочные файлы.

   Создайте новый проект в студии, задав тип проекта - Win32, шаблон -
   Win32 Project. Также скопируйте файлы из папки
   ffmpeg-lgpl-lshared-win32\dll и файл pthreadGC2.dll в папку system32. В
   качестве места расположения укажите созданную ранее директорию. В окне
   Application Wizard перейдите на вкладку Application Settings и
   установите тип приложения DLL, а Additional Options выставте в Empty
   Project. Жмите Finish, чтобы завершить создание проекта.

   В окне Solution Explorer добавьте в раздел Header Files заголовочные
   файлы ffmpeg-php:

           ffmpeg_animated_gif.h;
           ffmpeg_frame.h;
           ffmpeg_movie.h;
           gd.h;
           gd_io.h;
           php_ffmpeg.h.


   В раздел Source Files добавьте файлы исходных кодов ffmpeg-php:

           ffmpeg_animated_gif.c;
           ffmpeg_errorhandler.c;
           ffmpeg_frame.c;
           ffmpeg_movie.c;
           ffmpeg_php.c.


   Приступим к конфигурированию проекта. Откройте меню Project и выберите
   пункт Properties. Перед вами появится окно настройки проекта. Откройте
   вкладку Configuration Properties. Выберите конфигурацию - Release.
   Перейдите на вкладку General, вкладкиC/C++. В поле Additional Include
   Directories добавьте следующие пути:

           C:\dev\php\ffmpeg;
           C:\dev\php\ffmpeg\php5;
           C:\dev\php\ffmpeg\php5\TSRM;
           C:\dev\php\ffmpeg\php5\Zend;
           C:\dev\php\ffmpeg\php5\main;
           C:\dev\php\ffmpeg\ffmpeg-include\include\ffmpeg;
           C:\Program Files\Microsoft Platform SDK\Include.


   Перейдите на вкладку C/C++, раздел Preprocessor, и в поле Preprocessor
   Definitions добавьте следующие строки:

           PHP_WIN32
           ZEND_WIN32
           ZTS=1
           ZEND_DEBUG=0
           HAVE_LIBGD20=1
           COMPILE_DL_FFMPEG


   Затем перейдите на вкладку Linker, далее в General. В Additional
   Library Directories добавьте два пути:

           С:\dev\php\ffmpeg\php5-Win32\dev;
           C:\dev\php\ffmpeg\ffmpeg-lgpl-lshared-win32\lib.

   Далее перейдите на вкладку Linker, в Input и введите в поле Additional
   Dependencies:

           php5ts.lib;
           avcodec-51.lib;
           avformat-51.lib;
           avutil-49.lib.


   Затем на вкладке Linker, в Command Line и в поле Additional options
   добавьте:

           /FORCE:MULTIPLE


   Перейдите на вкладку Linker, в General. В поле Output File введите:

           $(OutDir)\ffmpeg.dll


   Прежде чем собирать проект, нужно сделать еще пару исправлений, а
   именно - в файле ffmpeg-php\ffmpeg_frame.c найдите следующую строку:

           #include "config.h"


   и замените её на следующую запись:

           #ifdef HAVE_CONFIG_H
           #include "config.h"
           #endif


   Попробуйте собрать все это, нажав клавишу <F7>. Если в консоли появится
   сообщение об ошибке:

           error C2466: cannot allocate an array of constant size 0


   то откройте файл php\main\config.w32.h и закомментируйте строку:

           #define _USE_32BIT_TIME_T 1


   Попробуйте собрать расширение. На выходе должен получиться наш файл
   библиотеки ffmpeg\release\ffmpeg.dll. Этот файл нужно скопировать в
   папку с расширениями PHP и добавить в php.ini строку:

           extension=php_ffmpeg.dll


   На этом процесс сборки и установки расширения можно считать
   законченным.

Работаем с FFmpeg в PHP

   Теперь приступим к программированию. Опираясь на полученные знания,
   весь ваш скрипт для работы с видео может уложиться в одну строку, а
   именно:

           <?php
               system($cmd);
           ?>


   Где $cmd - это одна из консольных команд, что мы рассматривали выше.
   Так можно описать все нужные вам вызовы. Но это бывает не очень удобно
   для некоторых задач. Удобнее всего работать с оберткой, которая
   выполнена в виде расширения PHP и позволяет избавиться от вызова
   системных команд через консоль.

   Для правильной работы ffmpeg-php необходима библиотека GD, которая
   позволяет работать с графическими файлами различных форматов. GD - это
   библиотека для динамической работы с изображениями. Она позволяет
   создавать изображения в форматах GIF, JPEG, PNG и WBMP. Название GD
   изначально обозначало "GIF Draw". Однако после аннулирования лицензии
   Unisys, аббревиатура расшифровывается как "Graphics Draw". GD
   позволяет создавать изображения, состоящие из линий, дуг, текста
   (включая программный выбор шрифтов) и других изображений, а также
   использовать различные цвета. C версии 2.0 добавлена поддержка
   32-битных (truecolor) изображений, альфа-каналов, дискретизация
   изображений (для плавного изменения размера 32-битных изображений) и
   многое другое. GD поддерживает множество языков программирования, в том
   числе и PHP, где библиотека значительно расширена. Начиная с версии PHP
   4.3, входит в стандартную поставку интерпретатора. До этой версии могла
   подключаться как отдельная библиотека.

   Ffmpeg-php оперирует с тремя типами объектов, а именно:

          ffmpeg_movie;
          ffmpeg_frame;
          ffmpeg_animated_gif.


   Рассмотрим подробнее работу с ffmpeg_vovie. Чтобы создать объект
   данного типа, достаточно инициализировать его следующим образом:

           $FfmpegMovie = new ffmpeg_movie(String path_to_media, boolean persistent);


   Данный код открывает аудио или видеофайл и возвращает объект. В
   качестве аргументов конструктора передаются: path_to_media - путь к
   аудио- или видеофайлу и persistent - открыть как постоянный ресурс. О
   постоянных ресурсах читайте в документации по PHP. После того, как
   будет создан объект, вам будут доступны методы для работы, которые
   описаны ниже.

   Методы объекта ffmpeg_movie

   Продолжительность аудио или видео:

           $FfmpegMovie->getDuration();


   Возвратит продолжительность файла в секундах.

   Количество кадров:

           $FfmpegMovie->getFrameCount();


   Возвратит количество фреймов аудио- или видеофайла.

   Частота кадров:

           $FfmpegMovie->getFrameRate();


   Возвратит частоту кадров видео в кадрах в секунду (fps - frame per
   second)

   Путь к файлу:

           $FfmpegMovie->getFilename();


   Возвратит путь к видео- или аудиофайлу.

   Поле комментария:

           $FfmpegMovie->getComment();


   Возвратит поле комментария из аудио- или видеофайла.

   Поле заголовка:

           $FfmpegMovie->getTitle();


   Возвратит поле заголовка из аудио- или видеофайла.

   Высота видео:

           $FfmpegMovie->getFrameHeight();


   Возвратит высоту видео в пикселях.

   Ширина видео:

           $FfmpegMovie->getFrameWidth();


   Возвратит ширину видео в пикселях.

   Скорость потока аудио:

           $FfmpegMovie->getAudioBitRate();


   Возвратит битрейт аудио у видео- или аудиофайла в битах в секунду.

   Номер кадра:

           $FfmpegMovie->getFrameNumber();


   Возвратит текущий номер кадра.

   Название видеокодека:

           $FfmpegMovie->getVideoCodec();


   Возвратит название видеокодека, который использован в видеофайле, как
   строку.

   Название аудиокодека:

           $FfmpegMovie->getAudioCodec();


   Возвратит название аудиокодека, который использован видеофайл как
   строку.

   Количество аудиоканалов:

           $FfmpegMovie->getAudioChannels();


   Возвратит количество аудиоканалов как целое число. Если 1 - это моно,
   если 2 - стерео, и т.д.

   Кадр из видео:

           $FfmpegMovie->getFrame([Integer framenumber]);


   Возвратит кадр из видео как ffmpeg_frame объект. Возвратит логическую
   ложь (false), если кадра нет, а framenumber - это номер кадра, который
   надо возвратить. Если framenumber не определен, то возвращается
   следующий кадр из видео.

   Методы объекта ffmpeg_frame

   Теперь рассмотрим работу с ffmpeg_frame. Чтобы создать объект данного
   типа, достаточно инициализировать его следующим образом:

           $Frame = new ffmpeg_frame(Resource gd_image);


   Данный объект создается из ресурса GD либо возвращается некоторыми
   методами объекта ffmpeg_movie.

           $Frame->getWidth();


   Возвратит ширину кадра.

           $Frame->getHeight();


   Возвратит высоту кадра.

           $Frame->resize(Integer width, Integer height [, Integer crop_top [,
           Integer crop_bottom [, Integercrop_left [, Integer crop_right ]]]]);


   Изменяет размер и обрезает кадр. Параметры:

    width - новая ширина кадра (должно быть натуральным числом);
    height - новая высота кадра (должно быть натуральным числом);
    croptop - удалить указанное количество рядов пикселов сверху кадра;
    cropbottom - удалить указанное количество рядов пикселов снизу кадра;
    cropleft - удалить указанное количество рядов пикселов слева от кадра;
    cropright - удалить указанное количество рядов пикселов справа от кадра.

   Обрезание всегда происходит до изменения размера кадра. Значения
   параметров должны быть натуральными числами!

           $Frame->crop(Integer crop_top [, Integer crop_bottom [, Integer
           crop_left [, Integer crop_right ]]]);


   Обрезать кадр. Параметры:

    croptop - удалить указанное количество рядов пикселов сверху кадра;
    cropbottom - удалить указанное количество рядов пикселов снизу кадра;
    cropleft - удалить указанное количество рядов пикселов слева от кадра;
    cropright - удалить указанное количество рядов пикселов справа от кадра.

   Замечание: параметры должны быть натуральными числами.

           $Frame->toGDImage();


   Возвращает truecolor GD картинку (ресурс) кадра. Функция недоступна,
   если нет библиотеки GD.

           $Frame->addFrame(ffmpeg_frame frame_to_add);


   Добавляет кадр в конец анимированного GIF. Параметры:

   frame_to_add - ffmpeg_frame-объект для добавления в конец анимированного GIF.

           $Frame->getPresentationTimestamp();


   Возвращает время создания кадра.

   Осталось рассмотреть последний тип объекта, это ffmpeg_animated_gif.

   Методы объекта ffmpeg_animated_gif

   Чтобы создать объект, не потребуется много усилий:

           $GifFile = new ffmpeg_animated_gif(String output_file_path, Integer
           width, Integer height, Integer frame_rate, [Integer loop_count])


   Данный код создает новый ffmpeg_animated_gif-объект. Параметры, которые
   нужно передать:

   output_file_path - путь в файловой системе, куда будет записан анимированный GIF;
   width - ширина анимированного GIF;
   height - высота анимированного GIF;
   frame_rate - частота кадров анимированного GIF в кадрах в секунду;
   loop_count - количество повторений анимации. Укажите 0 для
   бесконечного повторения или пропустите параметр для отключения
   повторений.

           $frame->addFrame(ffmpeg_frame frame_to_add);


   Добавляет кадр в конец анимированного файла.

   frame_to_add - ffmpeg_frame-объект для добавления в конец
   анимированного файла.

   Теперь можно рассмотреть небольшой пример создания превью с помощью
   ffmpeg-php:

           <?php
               if ( $Ffmpeg = new ffmpeg_movie( $input_file ) )
               {
                   if ( $Frame = $ffmpeg->getFrame($number_of_frame ) )
                   {
                      imagejpeg
                      (
                          $Frame->toGDImage(),
                          $path_to_image_file,
                          $quality
                      );
                   }
               }
           ?>


   В данном примере мы создаем объект ffmpeg_movie, затем получаем кадр с
   помощью метода getFrame(), и если он существует, то сохраняем его с
   помощью GD в JPG-формат, передав кадр в функцию imagejpeg()
   предварительно превратив его в ресурс GD через метод toGDImage().

Конфигурирование сервера для нашего веб-хостинга

   Видеофайлы достаточно объемные, поэтому, чтобы обеспечить возможность
   загрузки больших файлов на сервер, нужно дополнительно сконфигурировать
   PHP. В глобальном файле php.ini необходимо указать следующие параметры:

   Post_max_size - максимально допустимый размер POST-данных (в
   мегабайтах). Допустим, мы разрешаем загружать файлы размером 256 Мб,
   для этого мы пишем:

           post_max_size=256M


   Но данного параметра недостаточно. Также надо указать
   upload_max_filesize - максимальный размер закачиваемого файла. Данный
   параметр должен быть равен post_max_size.

   Так как загрузка файла и обработка файлов будут занимать достаточное
   количество времени, следует дополнительно в скриптах через ini_set
   указать max_execution_time - максимальное разрешенное время выполнения
   скрипта (в секундах). Делается это так:

           ini_set('max_execution_time', 9000);


   Также надо выставить max_input_time - максимально разрешенное время (в
   секундах), в течение которого скрипту разрешается анализировать входные
   данные. Я выставил в 9000 секунд данный параметр.

   Все эти параметры можно также указать через файл .htaccess для каждого
   корня документов или даже директории вместо изменения глобального
   php.ini. Делается это следующим образом:

           php_value post_max_size 256M
           php_value upload_max_filesize 256M
           php_value max_execution_time 9000
           php_value max_input_time 9000


   В следующей статье. Мы рассмотрим с вами, как делать брендирование
   файлов несколькими способами, изучим способы оптимизации и снижения
   нагрузки.

   1. http://ffmpeg-php.sourceforge.net
   2. http://ffmpeg-php.sourceforge.net/doc/api
   3. http://itbroadcastanddigitalcinema.com/ffmpeg_howto.html
   4. http://ffmpeg.mplayerhq.hu/ffmpeg-doc.html

site: http://www.opennet.ru/base/dev/ffmpeg_stream_video.txt.html

October 8th, 2010  
Tags: 3gp, ffmpeg, flv, mpeg, stream, video



Организация видеотрансляции на сайте

Development 4 Comments »

В одном из моих прошлых постов для передачи потокового видео я использовал связку Motion+ffmpeg+ffserver. В принципе, это рабочий вариант, но по какой-то причине ffserver довольно «криво» работал в качестве HTTP-сервера для отдачи видео.

В этот раз мы попробуем пойти другим путем и организовать трансляцию IP-камеры на сайте с помощью VLC Media Player – кроссплатформенного видеоплеера, а также, что наиболее важно в нашем случае, сервера потокового вещания. Сразу оговорюсь, что в качестве формата трансляции был выбран Flash Video (flv). Причин несколько, но самая главная – наличие Flash-плеера в браузере у 99% процентов пользователей Интернет. Конечно, можно выбрать и другой контейнер, например ASF.

Поскольку IP-камера (AXIS), с которой предполагалось вещание выдавала видео в формате MPEG ES, то предварительно его надо было преобразовать в MPEG TS, чтобы потом его можно было транскодировать в FLV.

Общая схема трансляции показана на рисунке ниже.

Общая схема

В качестве TS Muxer, FLV Encoder и FLV Streamer и будет выступать VLC.

Технически же это будет реализовано таким образом, что TS Muxer будет посылать MPEG TS поток по HTTP на порт 8080, откуда его можно будет «забирать», транскодировать в FLV и слать по HTTP на порт 8081. Чтобы было более понятно, проиллюстрирую это примером. Предположим, что все будет происходить на одном сервере (хотя это абсолютно не обязательно). Тогда техническая реализация работы будет следующей:

Реализация

Приступим к реализации. Как обычно, детали установки приведу для Debian Etch 4.0. VLC будем собирать из исходников.

1. Подготовка:

# Устанавливаем необходимые для сборки пакеты
# !!! НЕ ЗАБУДЬТЕ ДОБАВИТЬ РЕПОЗИТОРИЙ DEBIAN-MULTIMEDIA В APT !!!
apt-get install ffmpeg libavformatcvs51 libavcodeccvs51 libavcodeccvs51-dev libavformatcvs51-dev \
libavutilcvs49-dev libavutilcvs49 libavahi-client3 libavahi-common-dev libpostproccvs51-dev  \
libswscalecvs0-dev libswscalecvs0 libxvidcore4-dev libxvidcore4 libx264-dev libx264-54 automake1.9 \
autoconf g++ gcc liba52-0.7.4-dev libdvbpsi3-dev libdvbpsi3 libfaad-dev libfaac-dev libfribidi-dev \
libgcrypt11-dev liblame-dev liblua5.1-0-dev libmad0-dev libmpeg2-4-dev libogg-dev libvorbis-dev \
zlib1g-dev libvcdinfo-dev libiso9660-dev libcddb2-dev libflac-dev

# Скачиваем и собираем LIVE555 Streaming Media
# !!! ВНИМАНИЕ !!! На 4.11.2009 vlc с последней версией live от 28.10.2009 не работает. Проверено.
# http://forum.videolan.org/viewtopic.php?f=13&t=66303
# У меня сохранилась более старая версия, поэтому загружаем отсюда:
wget http://flance.onego.ru/files/live/live.tar.gz
tar xvfz live.tar.gz
cd live
./genMakefiles linux
make

2. VLC:

# На время написания статьи самая свежая стабильная версия – 0.9.8a
wget http://download.videolan.org/pub/videolan/vlc/0.9.8a/vlc-0.9.8a.tar.bz2
bzip2 -d vlc-0.9.8a.tar.bz2
tar xvf vlc-0.9.8a.tar

3. Добавляем live555 в дерево исходных текстов VLC:

cd vlc-0.9.8a
cp -r $YOUR_LIVE555_BUILD_DIR extras

4. Собираем VLC:

# Поскольку мне не нужен был VLC с графическим интерфейсом на сервере, то использовались ключи, которые его не собирали
./configure –enable-release –enable-faad –disable-remoteosd –disable-qt4 –disable-skins2 \
–disable-activex –disable-v4l2 –disable-libv4l2 –disable-x11 –disable-xvideo –disable-glx \
–disable-opengl –disable-visual –enable-realrtsp –enable-flac –with-live555-tree=extras/live \
–disable-dbus –disable-hal > /tmp/1 2> /tmp/2
# В файле /tmp/2 можно будет смотреть ошибки и предупреждения процесса конфигурирования
# Если будет какая-нибудь ошибка, то необходимо установить соответствующий пакет Debian и
# запустить configure еще раз
make

5. Запускаем VLC TS Muxer:

./vlc -vv –no-rtsp-tcp rtsp://<URL потока> –rtsp-caching=10000 –no-sout-audio –sout \
‘#std{access=http,dst=127.0.0.1:8080,mux=ts}’

6. Запускаем FLV encoder+streamer:

./vlc -vv http://127.0.0.1:8080 –loop –http-caching=10000 –sout \
‘#transcode{vcodec=FLV1,vb=1024}:std{access=http{mime=video/x-flv},dst=:8081/stream.flv,mux=ffmpeg{mux=flv}}’

6. Интеграция в HTML:

# Скачиваем flash-плеер, я рекомендую JW FLV Media Player, но можно попробовать и другой
wget http://www.longtailvideo.com/jw/upload/mediaplayer.zip
# Распаковываем и кладем файлы  player.swf и swfobject.js в один каталог с предполагаемой
# HTML-страницей, где будем показывать видео

Добавляем следующий HTML-код в страницу:

<script type=”text/javascript” src=”swfobject.js”></script>
<div id=”mplayer”>this will be replaced by the SWF.</div>
<script type=”text/javascript”>
var so = new SWFObject(‘player.swf’,'player’,’459′,’375′,’9′);
so.addParam(‘allowfullscreen’,'true’);
so.addParam(‘flashvars’,'start=1&amp;repeat=always&amp;file=http://<адрес потока>&amp;bufferlength=0&amp;autostart=true&amp;displayclick=none&amp;mute=true’);
so.write(‘mplayer’);
</script>

Внимание! Параметры start (спасибо Кириллу) и repeat важны.
Все, можно проверять. Хочу еще отметить, что на данный момент VLC с текущей версией FFMPEG для в плане FLV кодирования не работает, поэтому я использовал пакеты Debian. Если кодек FLV1 не нужен, то можно собрать VLC и со свежими FFMPEG и X264.

04.11.2009 ОБНОВЛЕНИЕ:
Важная информация:
1. Для сборки на Debian Lenny 5.0 необходимо добавить в опции конфигурирования VLC на 4 шаге опции –disable-nls и –disable-mozilla, иначе вылезет ошибка о Buggy glibc version.
2. VLC с последней версией liveMedia не стартует. Смотрите комментарии в шаге 1.

Ссылки:

  1. http://www.videolan.org
  2. http://www.ffmpeg.org
  3. http://www.live555.com/liveMedia
  4. http://www.longtailvideo.com/players/jw-flv-player

site: http://flance.onego.ru/2009/02/09/51


October 8th, 2010  
Tags: AXIS, Flash Video, flv



Приклад використання Callable для повернання результата

Java 0 Comment »

Я завжди використовував Runnable, але недавно довелось перейти на Callable

Наведу простий приклад використання. Є офіційний приклад Using Callable to Return Results From Runnables

Я наводжу свій приклад і результат виконання.

Class Main – клас який запускає на виконання пул потоків а також відображає результат виконання.

Class Task – Клас який виконує завдання в пулі потоків


package callabletest;

/**
*
* @author ivi
*/

import java.util.*;
import java.util.concurrent.*;
import callabletest.Task;
public class Main {

public static void main(String[] args) throws Exception
{
ExecutorService pool = Executors.newFixedThreadPool(3);

Set<Future<Object>> set = new HashSet<Future<Object>>();

for(int i =0; i < 10; i ++)
{
Callable<Object> callable = callabletest.Task.getTask();
Future<Object> future = pool.submit(callable);
set.add(future);
}

for (Future<Object> future : set)
{
Object obj  = future.get();
Task task  =  (Task) obj;
System.out.println("Test Return Values: "+task.getCurrent());
}
pool.shutdown();

}

}

package callabletest;
/**
*
* @author ivi
*/
import java.util.concurrent.*;

public class Task implements Callable
{
private static int counter  = 0;

/**
* @return the counter
*/
public static int getCounter() {
return counter;
}
private int current  = 0;

private Task()
{
counter ++;
this.current =  counter;
}

public synchronized static Task getTask()
{
return new Task();
}

Результат

run:
Task #1
Task #2
Task #3
Task #4
Task #5
Test Return Value: 1
Task #6
Task #7
Task #8
Task #9
Test Return Value: 6
Task #10
Test Return Value: 10
Test Return Value: 5
Test Return Value: 4
Test Return Value: 3
Test Return Value: 9
Test Return Value: 8
Test Return Value: 7
Test Return Value: 2
BUILD SUCCESSFUL (total time: 4 seconds)

August 18th, 2010  
Tags: Callable, Executors, ExecutorService, Future, Java, java.util.concurrent



Настройка кластера MySQL

Databases 0 Comment »

Введение

Кластерное решение на базе MySQL является отказоустойчивым, избыточным и масштабируемым решением для баз данных, основанным на открытых исходных текстах. Использование такой схемы позволяет достигнуть надежности в 99.999 %. В этой статье мы опишем процесс установки, настройки и тестирования кластера MySQL, состоящего из трех узлов.

Схема подключения

MySQL

Аппаратное обеспечение

Мы использовали четыре сервера Sun Ultra Enterprise, но процесс установки кластера на другой UNIX- или Linux-подобной системе будет отличаться очень незначительно.

Наши четыре машины относятся к одной из трех ролей:

  1. Хранилище (mysql-ndb-1 и mysql-ndb-2)
  2. API (mysql-api-1)
  3. Сервер управления и консоль управления (mgmt)

Обратите внимание, что узлы хранилища также явзяются API нодами, но API нода не является хранилищем. Узел API – полноправный член кластера, но он не хранит никаких данных кластера и его состояние (работает/не работает) не затрагивает целостность или доступность данных. Об этой ноде можно думать как о “клиенте” кластера. Приложения, такие как Web-сервер, установлены на ноде API и общаются с процессом MySQL, запущенным локально, именно этот процесс запрашивает данные от хранилищ. На хранилищах также могут быть установлены приложения, поскольку они совмещают в себе API ноды, но для промышленного применения такое совмещение нежелательно.

Программное обеспечение

Мы используем Sun Solaris 8 и mysql-max-4.1.9.

Мы использовали прекомпилированный пакет MySQL для Sun SPARC Solaris 8, вы должны использовать программное обеспечение в зависимости от используемой вами архитектуры, но в любом случае, необходимо использовать вариант “max”.

Порядок действий

Шаг 1: После загрузки нод mysql-ndb-1 (192.168.0.33) и mysql-ndb-2 (192.168.0.34) устанавливаем и настраиваем MySQL:

    
    mysql-ndb-1# groupadd mysql
    mysql-ndb-1# useradd -g mysql mysql
    mysql-ndb-1# cd /usr/local
    mysql-ndb-1# wget http://dev.mysql.com/get/Downloads/MySQL-4.1/ 
    mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz/from/http://mysql.he.net/
    mysql-ndb-1# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mysql-ndb-1# ln -s mysql-max-4.1.9-sun-solaris2.8-sparc mysql
    mysql-ndb-1# cd mysql
    mysql-ndb-1# scripts/mysql_install_db --user=mysql
    mysql-ndb-1# chown -R root  .
    mysql-ndb-1# chown -R mysql data
    mysql-ndb-1# chgrp -R mysql .
    mysql-ndb-1# cp support-files/mysql.server /etc/init.d/mysql.server
    
    mysql-ndb-2# groupadd mysql
    mysql-ndb-2# useradd -g mysql mysql
    mysql-ndb-2# cd /usr/local
    mysql-ndb-2# wget http://dev.mysql.com/get/Downloads/MySQL-4.1/
    mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz/from/http://mysql.he.net/
    mysql-ndb-2# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mysql-ndb-2# ln -s mysql-max-4.1.9-sun-solaris2.8-sparc mysql
    mysql-ndb-2# cd mysql
    mysql-ndb-2# scripts/mysql_install_db --user=mysql
    mysql-ndb-2# chown -R root  .
    mysql-ndb-2# chown -R mysql data
    mysql-ndb-2# chgrp -R mysql .
    mysql-ndb-2# cp support-files/mysql.server /etc/init.d/mysql.server
    

Не запускайте сервис!

Шаг 2: Установим сервер и консоль управления на mgmt (192.168.0.32):

    
    
    mgmt# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mgmt# cp mysql-max-4.1.9-sun-solaris2.8-sparc/bin/ndb_mgm /usr/bin
    mgmt# cp mysql-max-4.1.9-sun-solaris2.8-sparc/bin/ndb_mgmd /usr/bin
    mgmt# rm -r mysql-max-4.1.9-sun-solaris2.8-sparc
    mgmt# mkdir /var/lib/mysql-cluster
    mgmt# cd /var/lib/mysql-cluster
    mgmt# vi config.ini
    

Файл config.ini содержит необходимую информацию для кластера:

    
    [NDBD DEFAULT]
    NoOfReplicas=2
    [MYSQLD DEFAULT]
    [NDB_MGMD DEFAULT]
    [TCP DEFAULT]
    # Management Server
    [NDB_MGMD]
    HostName=192.168.0.32           # IP address of this server
    # Storage Nodes
    [NDBD]
    HostName=192.168.0.33           # IP address of storage-node-1
    DataDir= /var/lib/mysql-cluster
    [NDBD]
    HostName=192.168.0.34           # IP address of storage-node-2
    DataDir=/var/lib/mysql-cluster
    # Setup node IDs for mySQL API-servers (clients of the cluster)
    [MYSQLD]
    [MYSQLD]
    [MYSQLD]
    [MYSQLD]
    

Запускаем сервер управления и проверяем его работу:

    
    mgmt# ndb_mgmd
    mgmt# ps -ef | grep [n]db
    

Шаг 3: Конфигурируем MySQL на нодах mysql-ndb-1 (192.168.0.33) и mysql-ndb-2 (192.168.0.34):

    
    mysql-ndb-1# vi /etc/my.cnf
    mysql-ndb-2# vi /etc/my.cnf
    

В данном случае файл конфигурации выглядит следующим образом:

    
    [mysqld]
    ndbcluster
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    [mysql_cluster]
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    

Запускаем сервисы и проверяем их работу:

    
    mysql-ndb-1# mkdir /var/lib/mysql-cluster
    mysql-ndb-1# cd /var/lib/mysql-cluster
    mysql-ndb-1# /usr/local/mysql/bin/ndbd --initial
    mysql-ndb-1# /etc/init.d/mysql.server start
    mysql-ndb-1# ps -ef | grep [n]dbd
    mysql-ndb-1# ps -ef | grep [m]ysqld
    
    mysql-ndb-2# mkdir /var/lib/mysql-cluster
    mysql-ndb-2# cd /var/lib/mysql-cluster
    mysql-ndb-2# /usr/local/mysql/bin/ndbd --initial
    mysql-ndb-2# /etc/init.d/mysql.server start
    mysql-ndb-2# ps -ef | grep [n]dbd
    mysql-ndb-2# ps -ef | grep [m]ysqld
    

Если сервис не запустился, то просмотрите файл /usr/local/mysql/data/${HOSTNAME}.err и устраните проблему.

Шаг 4: Запускаем сервер и консоль управления, проверяем состояние кластера:

    
    mgmt# ndb_mgm
    -- NDB Cluster -- Management Client --
    ndb_mgm> show
    Connected to Management Server at: localhost:1186
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, starting, Nodegroup: 0, Master)
    id=3    @192.168.0.34  (Version: 4.1.9, starting, Nodegroup: 0)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4 (not connected, accepting connect from any host)
    id=5 (not connected, accepting connect from any host)
    id=6 (not connected, accepting connect from any host)
    id=7 (not connected, accepting connect from any host)
    

Шаг 5: Создаем тестовую базу данных и проверяем корректность операций:

Создаем на хранилищах mysql-ndb-1 и mysql-ndb-2 тестовую базу:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 1 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> create database foo;
    Query OK, 1 row affected (0.09 sec)
    
    
    
    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 6 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> create database foo;
    Query OK, 1 row affected (0.13 sec)
    

Вернитесь на хранилище mysql-ndb-1, и создайте простейшую таблицу с некоторыми значениями:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 1 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo; 
    Database changed
    mysql> create table test1 (i int) engine=ndbcluster;
    Query OK, 0 rows affected (0.94 sec)
    
    mysql> insert into test1 () values (1);
    Query OK, 1 row affected (0.02 sec)
    
    mysql> select * from test1;
    +------+
    | i    |
    +------+
    |    1 |
    +------+
    1 row in set (0.01 sec)
    

Перейдите на ноду mysql-ndb-2 и проверьте доступность данных:

    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 7 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test1;
    +------+
    | i    |
    +------+
    |    1 |
    +------+
    1 row in set (0.00 sec)
    

Если у вас все получилось, то это хороший признак, хотя стоит учесть то, что на самом то деле данные могут и не скопироваться. В очередной раз напомню, что хранилище (mysql-ndb-2) также является и API-нодой и этот тест просто показывает, что данные в кластере можно восстановить. Для более наглядной демонстрации мы воспользуемся следующим тестом.

Убейте процесс NDB (ndbd) на хранилище (mysql-ndb-2) для того, чтобы имитировать отказ одной из нод.

    
    mysql-ndb-2# ps -ef | grep [n]db
        root  3035  3034  0 17:28:41 ?   0:23 /usr/local/mysql/bin/ndbd --initial
        root  3034     1  0 17:28:41 ?   0:00 /usr/local/mysql/bin/ndbd --initial
    mysql-ndb-2# kill -TERM 3034 3035
    mysql-ndb-2# ps -ef | grep [n]db
    

Сервер управления должен обнаружить отказ хранилища mysql-ndb-2 (192.168.0.34), но связь с API должна быть.

    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0)
    id=3 (not connected, accepting connect from 192.168.0.34)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4    @192.168.0.33  (Version: 4.1.9)
    id=5    @192.168.0.34  (Version: 4.1.9)
    id=6 (not connected, accepting connect from any host)
    id=7 (not connected, accepting connect from any host)
    

В хранилище mysql-ndb-1 создайте еще одну таблицу:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 4 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> create table test2 (i int) engine=ndbcluster;
    Query OK, 0 rows affected (1.00 sec)
    
    mysql> insert into test2 () values (2);
    Query OK, 1 row affected (0.01 sec)
    
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

Перейдем на ноду mysql-ndb-2 и выполним следущую команду:

    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 9 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

Хранилище и сервер API являются независимыми приложениями, поэтому как только сервис хранилища ndbd будет запущен, данные будут среплицированы, что и будет продемонстрировано в следующем тесте.

Сперва перезапустите хранилище mysql-ndb-2:

    
    mysql-ndb-2# /usr/local/mysql/bin/ndbd
    

Затем, останавливаем хранилище на mysql-ndb-1, используя консоль управления или команду kill:

    
    mgmt# ndb_mgm
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0, Master)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4    @192.168.0.33  (Version: 4.1.9)
    id=5    @192.168.0.34  (Version: 4.1.9)
    id=6 (not connected, accepting connect from any host)
    id=7 (not connected, accepting connect from any host)
    
    ndb_mgm> 2 stop
    Node 2 has shutdown.
    

После того, как хранилище на mysql-ndb-2 было перезапущено, необходимо убедиться в репликации данных:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 5 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    
    
    
    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 10 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

Тем самым мы убедились в репликации данных между хранилищами. Запускаем хранилище mysql-ndb-1:

    
    mysql-ndb-1# /usr/local/mysql/bin/ndbd
    

Шаг 6: Теперь мы добавим в кластер ноду API. Она является полноценным членом кластера, за исключением того, что на ней не запущен движок хранилища NDB. Данные на эту ноду не реплицируются и она выполняет только “клиентские” функции. Как правило, на такие ноды устанавливаются приложения, требующие для своей работы MySQL. Приложения обращается к серверу MySQL на localhost, а он, в свою очередь, обращается за данными к кластеру.

Сперва установим сервер MySQL для API ноды mysql-api-1 (192.168.0.35):

    
    mysql-api-1# groupadd mysql
    mysql-api-1# useradd -g mysql mysql
    mysql-api-1# cd /usr/local
    mysql-api-1# wget http://dev.mysql.com/get/Downloads/MySQL-4.1/
    mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz/from/http://mysql.he.net/
    mysql-api-1# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mysql-api-1# ln -s mysql-max-4.1.9-sun-solaris2.8-sparc mysql
    mysql-api-1# cd mysql
    mysql-api-1# scripts/mysql_install_db --user=mysql
    mysql-api-1# chown -R root  .
    mysql-api-1# chown -R mysql data
    mysql-api-1# chgrp -R mysql .
    mysql-api-1# cp support-files/mysql.server /etc/init.d/mysql.server
    

Устанавливаем простой файл конфигурации /etc/my.cnf:

    
    [mysqld]
    ndbcluster
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    [mysql_cluster]
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    

Запускаем сервер MySQL:

    
    mysql-api-1# /etc/init.d/mysql.server start
    

Выполним несколько запросов к таблицам, которые мы создали ранее:

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 1 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> create database foo;
    Query OK, 1 row affected (0.11 sec)
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test1;
    +------+
    | i    |
    +------+
    |    1 |
    +------+
    1 row in set (0.01 sec)
    
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

С помощью консоли управления убедимся, что API нода теперь доступна:

    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0, Master)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4   (Version: 4.1.9)
    id=5   (Version: 4.1.9)
    id=6    @192.168.0.35  (Version: 4.1.9)
    id=7 (not connected, accepting connect from any host)
    

Теперь наша конфигурация похожа на диаграмму, представленую в верхней части статьи.

Шаг 7: Теперь мы готовы проверить отказоустойчивость кластера, обслуживая запросы с API ноды:

С помошью сервера API ноды создадим тестовую базу данных и наполним ее неким содержимым:

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 258519 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> create table test3 (i int) engine=ndbcluster;
    Query OK, 0 rows affected (0.81 sec)
    
    mysql> quit
    Bye
    
    

Вставим случайные данные в таблицу, руками или используя этот короткий сценарий:

    
    #!/bin/sh
    for i in 1 2 3 4 5 6 7 8 9 10
    do
            random=`perl -e "print int(rand(100));"`
            echo "use foo; insert into test3 () values ($random);" | mysql -u root
    done
    

Выполняем запросы:

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 258551 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test3;
    +------+
    | i    |
    +------+
    |   92 |
    |   20 |
    |   18 |
    |   84 |
    |   49 |
    |   22 |
    |   54 |
    |   91 |
    |   79 |
    |   52 |
    +------+
    10 rows in set (0.02 sec)
    

Круто, работает. Теперь отключим серевой кабель от первого хранилища, чтобы вызвать аварию в кластере. Через несколько секунд консоль управления доложит об исчезновении ноды:

    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2 (not connected, accepting connect from 192.168.0.33)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4 (not connected, accepting connect from any host)
    id=5   (Version: 4.1.9)
    id=6    @192.168.0.35  (Version: 4.1.9)
    id=7 (not connected, accepting connect from any host)
    

Для API ноды данные кластера все еще доступны?

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 258552 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test3;
    +------+
    | i    |
    +------+
    |   54 |
    |   91 |
    |   79 |
    |   52 |
    |   92 |
    |   20 |
    |   18 |
    |   84 |
    |   49 |
    |   22 |
    +------+
    10 rows in set (0.02 sec)
    

Подключим кабель обратно. Хранилище попытается подключиться обратно в кластер но, вероятно, будет отключен сервером управления, с появлением подобной ошибки (/var/lib/mysql-cluster/mdb_2_error.log):

    
    Date/Time: Saturday 12 February 2005 - 12:46:21
    Type of error: error
    Message: Arbitrator shutdown
    Fault ID: 2305
    Problem data: Arbitrator decided to shutdown this node
    Object of reference: QMGR (Line: 3796) 0x0000000a
    ProgramName: /usr/local/mysql/bin/ndbd
    ProcessID: 1185
    TraceFile: /var/lib/mysql-cluster/ndb_2_trace.log.3
    ***EOM***
    

Перезапускаем процесс ndb и видим, как нода присоединяется к кластеру:

    
    mysql-ndb-1# /usr/local/mysql/bin/ndbd
    
    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0, Master)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4   (Version: 4.1.9)
    id=5   (Version: 4.1.9)
    id=6    @192.168.0.35  (Version: 4.1.9)
    id=7 (not connected, accepting connect from any host
    

Разное

  • Помните, что данные SQL, сохраненные (реплицированные) в кластере, таблицы баз данных должны быть созданы, используя engine=NDBCLUSTER (как показано в примерах выше). Возможно использовать этот механизм, чтобы определить различные типы хранения для таблиц в пределах одной базы данных, в зависимости от индивидуальной работы и требований надежности. Некритичные таблицы баз данных могут не храниться в кластере.
  • Можно сделать тип хранения NDBCLUSTER используемым по умолчанию с помощью записи в /etc/my.cnf:
      
      [mysqld]
      default-table-type=NDBCLUSTER
      
  • Иногда, после аварийного завершения узла кластера (например, аварийный отказ или отключение питания) мы видим “зависшие” подключения, и после рестарта отказавший узел неспособен присоединиться к кластеру. В этом случае, сеанс должен быть вручную удален с консоли управления, используя команду “purge stale sessions”:
      
      ndb_mgm> purge stale sessions
      Purged sessions with node id's: 3
      ndb_mgm>
      

Перевод: Михаил Сгибнев (www.dreamcatcher.ru)


August 12th, 2010  
Tags: Cluster, MySQL, NDB



Некоторые недокументированные функции Java

Java 0 Comment »

Даниил Алиевский

Изучение недокументированных функций в применении к языку Java может показаться несколько странным. Java – грамотный, современный, высоконадежный объектно-ориентированный язык программирования, поставляемый фирмой Sun совместно с обширнейшими библиотеками готовых классов. Неужели в среде Java существуют задачи, которые не решаются с помощью стандартных библиотек и для решения которых имеет смысл прибегать к недокументированным функциям? И что вообще такое «недокументированная функция» в рамках Java?

Слова «недокументированная функция» в случае Java имеют самый прямой смысл. В стандартный комплект поставки языка Java (мы рассматриваем версии 1.4 и выше – Sun Java SDK) входит, помимо документированных пакетов типа java.lang.*, java.util.*, javax.swing.* и т. д. также целый ряд недокументированных пакетов, прежде всего подпакеты sun.* и com.sun.*. Фирма Sun совершенно справедливо рекомендует не пользоваться классами из этих пакетов. Фирма Sun оставляет за собой право в любой момент поменять поведение и даже сам набор этих классов, так что программа, пользующаяся ими, рискует оказаться несовместимой с будущими версиями Java.

На самом деле в подавляющем большинстве ситуаций недокументированные функции, точнее, недокументированные классы из подпакетов sun.* и com.sun.* действительно не нужны. Эти классы в основном обеспечивают низкоуровневую реализацию универсальных классов и интерфейсов, предназначенных для использования в прикладных программах, и почти ничего не добавляют к тем возможностям, которые и так предоставляются стандартными документированными прикладными пакетами.

Из этого правила иногда встречаются исключения.

Автор этой статьи участвовал в разработке сложной системы на языке Java, содержащей более 100 тысяч строк исходного кода. При этом нам лишь несколько раз потребовались недокументированные функции Java. Именно о таких исключениях мне хотелось бы рассказать в данной статье.

Встроенный компилятор JAVA

Наверное, наиболее популярная «недокументированная функция» Java – это компиляция исходных текстов Java в .class-файлы.

Стандартный компилятор javac, входящий в комплект поставки Sun Java SDK, вполне последовательно реализован фирмой Sun на том же самом языке Java в виде сложной иерархии Java-классов. Утилита javac не более чем тривиальная программа, реализованная в машинном коде для всех операционных систем, предназначенная для запуска виртуальной машины Java и обращения к Java-классу, выполняющему собственно компиляцию.

Если ваша программа нуждается в компиляции исходных текстов на языке Java, то наиболее изящное, хотя и недокументированное решение, – воспользоваться стандартным Java-классом фирмы Sun, реализующим такую компиляцию.

На самом деле тут существуют даже два решения – классы sun.tools.javac.Main и com.sun.tools.javac.Main. Оба этих класса находятся в JAR-файле tools.jar, входящем в комплект поставки Sun Java SDK. Этот класс по умолчанию не входит в так называемый Java Runtime Environment (JRE) – набор классов и подкаталогов, который разрешается свободно распространять совместно с вашим Java-приложением для обеспечения его корректного исполнения. Тем не менее лицензионное соглашение фирмы Sun специально разрешает распространять tools.jar в дополнение к JRE совместно с вашими приложениями.

Оба класса – sun.tools.javac.Main и com.sun.tools.ja-vac.Main – хотя и не документированы, но довольно активно обсуждаются на форумах сайта http://java.sun.com.

Первый класс, sun.tools.javac.Main, имеет конструктор:

public Main(

OutputStream p0,

String p1)

В качестве параметров конструктор принимает некоторый поток p0, в который будут выдаваться все сообщения компилятора, и некоторую строку неизвестного (автору статьи) назначения; известные мне примеры использования данного класса передавали в качестве p1 строку «javac».

Собственно компиляцию выполняет метод того же класса:

public synchronized boolean compile(

String[] p0)

Этому методу нужно передать в качестве аргумента массив строк-параметров, которые обычно передаются утилите javac, например:

new String[] {“-d”,”/путь_к_подкаталогу”,”myfile.java”}

О результатах компиляции можно узнать из результата метода compile() – в случае успеха он должен вернуть true – или с помощью отдельного метода:

public int getExitStatus()

который в случае успеха должен вернуть 0.

Самая ценная особенность класса sun.tools.javac.Main – возможность указать в конструкторе выходной поток, который будет использоваться для выдачи сообщений об ошибках компилятора. Это дает возможность легко преобразовать этот поток, скажем, в переменную типа String, с тем чтобы самостоятельно ее проанализировать и показать пользователю.

Начиная с версии Sun Java SDK 1.4, объявлен устаревшим класс sun.tools.javac.Main. Попытка явно обратиться к этому классу в программе выдает предупреждение «deprecation warning», а попытка им воспользоваться для компиляции любого класса выдает аналогичное предупреждение в поток ошибок (параметр конструктора p0), если только явно не подавить эти предупреждения ключом «-nowarn» среди параметров метода compile.

Для такого предупреждения есть основания. По крайней мере один из сложных классов, разработанных автором на Java версии 1.4 и прекрасно компилирующихся обычными компиляторами, оказалось невозможным скомпилировать с помощью класса sun.tools.ja-vac.Main. Компилятор вполне явно «сошел с ума» и стал «ругаться» на законные языковые конструкции.

Второй класс, предназначенный для компиляции исходных текстов на Java – единственный «неустаревший» в версии Sun Java SDK 1.4 – это com.sun.tools.ja-vac.Main. Функционально он эквивалентен классу sun.tools.ja-vac.Main, но несколько менее «многословен» в своем наборе методов. Конструктор этого класса не имеет параметров. Методов compile здесь два, причем несинхронизированных, в отличие от sun.tools.javac.Main:

public static int compile(String[] p0)

public static int compile(String[] p0,

PrintWriter p1)

Второй из этих методов позволяет указать поток вывода, который будет использоваться для вывода всех сообщений компилятора, например:

new PrintWriter(writer=new CharArrayWriter(),true)

Как и в случае sun.tools.javac.Main, второй метод compile позволяет перенаправить поток сообщений компилятора в собственный буфер, с тем чтобы впоследствии превратить его, скажем, в строку типа String для анализа и визуализации.

Класс com.sun.tools.javac.Main в версии Sun Java SDK 1.4 работает существенно быстрее предыдущего класса sun.tools.javac.Main и «справляется» со всеми корректными исходными текстами. Похоже, именно этот класс вызывается изнутри стандартной утилиты javac.

Файлы-ссылки – *.lnk в Microsoft Windows

Второй известный автору случай использования недокументированных функций – распознавание файлов-«ярлыков» (shortcuts) Microsoft Windows, обычно имеющих расширение «.lnk».

Стандартные средства работы с файлами из пакета java.io.* вообще не слишком хорошо «справляются» с файловой системой современных версий Microsoft Windows. Так, стандартный класс java.io.File «понятия не имеет» о том, что корнем файловой иерархии Windows следует считать «Рабочий стол» («Desktop»), у которого есть такие дочерние узлы, как «Мои документы» («My documents») или «Мой компьютер» («My computer»). Стандартный java.io.File по старинке считает корнем иерархии корневой каталог любого дискового устройства.

Чтобы скомпенсировать этот недостаток, не нарушая совместимости с классом File, фирма Sun разработала новый, более современный класс javax.swing.file-chooser.FileSystemView. Он активнейшим образом используется стандартным диалогом выбора файла javax.swing.JFileChooser, что отчасти объясняет несколько странный выбор пакета для FileSystemView.

К сожалению, даже класс FileSystemView не решает всех проблем, по крайней мере, в имеющейся у меня последней версии Sun Java SDK 1.4.1. Современные версии Windows, в частности Windows XP, предлагают пользователю интерфейс, существенно опирающийся на механизм файлов-«ссылок», или «ярлыков». Такими ссылками являются специальные файлы или даже подкаталоги (обычно, но не обязательно с расширением «.lnk»). Щелчок по ним в стандартном Windows Explorer приводит к перемещению в некоторый другой каталог или открытию некоторого другого файла. Именно так в Windows XP организована работа в локальной сети – компьютеры пользователей-«соседей» представлены маленькими виртуальными подкаталогами-«ссылками» в локальной файловой системе текущего пользователя.

Класс FileSystemView не содержит никаких средств для распознавания и обработки подобных ссылок. В результате стандартный диалог выбора файла javax.swing.JFileChooser при использовании в Windows XP производит довольно жалкое впечатление – попытка перейти к компьютерам локальной сети заканчивается позорной неудачей.

В действительности фирма Sun уже реализовала механизм обработки файлов-«ссылок» Microsoft Windows. К сожалению, он пока недокументирован. Это класс sun.awt.shell.ShellFolder. Среди прочих методов, имеющих документированные эквиваленты в классе FileSystemView, класс ShellFolder содержит следующие два метода:

public abstract boolean isLink();

public abstract ShellFolder getLinkLocation()

throws FileNotFoundException

Вот как можно ими пользоваться:

public static boolean isLink(File f) {

try {

return sun.awt.shell.ShellFolder

.getShellFolder(f).isLink();

} catch (FileNotFoundException e) {

return false;

}

}

public static File getLinkLocation(File f)

throws FileNotFoundException

{

File result= sun.awt.shell.ShellFolder

.getShellFolder(f).getLinkLocation();

if (result==null ||

result.getPath().trim().length()==0)

throw new FileNotFoundException(

“Incorrect link – it is empty”);

return result;

}

Применяя эти методы, при желании можно «исправить» поведение стандартного диалога выбора файла javax.swing.JFileChooser, «научив» его правильно работать с современными локальными сетями Microsoft Windows.

Конечно, будет гораздо лучше, если фирма Sun в очередной версии Java SDK включит в FileSystemView документированные эквиваленты этих методов и исправит javax.swing.JFileChooser. Существование подобных методов в sun.awt.shell.ShellFolder позволяет на это надеяться. Пока же приходится пользоваться недокументированными методами.

Стек вызовов метода

Предположим, нужно получить «трассу стека» – узнать, какой метод исполняется в данный момент, какой метод вызвал этот метод, и т. д. Трудно представить, зачем это может понадобиться, за исключением отладки программы. Однако в реальной практике автору это однажды понадобилось – чтобы выяснить, какие загрузчики классов использовались для загрузки текущего исполняемого кода и всех классов «трассы стека», вызвавших этот код.

Даже для этой столь экзотической ситуации фирма Sun предусмотрела документированную технику. Достаточно возбудить фиктивное исключение, тут же «поймать» его и воспользоваться методом объекта-исключения getStackTrace().

Проблема может заключаться в том, что метод getStackTrace() возвращает исключительно «описательную» информацию о классах и методах «трассы стека» – попросту строковые имена классов и методов. Чтобы получить собственно классы, задействованные в данный момент в стеке (объекты типа Class), необходимо вызвать метод Class.forName или эквивалентный. Но что, если текущий загрузчик классов не в состоянии загрузить такие классы просто по имени? Что, если текущий исполняемый код загружен самым обычным традиционным загрузчиком классов, а класс, который его вызвал – это интернетовский class-файл, загруженный и исполняемый совершенно другим специальным загрузчиком? Тогда метод getStackTrace() никак не поможет «добраться» до этого класса (объекта Class) хотя бы для того, чтобы получить ссылку на загрузивший его загрузчик классов.

В подобной ситуации автору пришлось прибегнуть к недокументированному классу sun.reflect.Reflection, точнее, к его методу.

public static native Class getCallerClass(int p0)

Вот как выглядит использование этого метода:

public static Class[] getCurrentStackTraceClasses() {

List result= new ArrayList();

for (int count=1; count<10000

/* страховка на всякий случай */;

count++)

{

Class clazz= sun.reflect.Reflection

.getCallerClass(count);

if (clazz==null) break;

result.add(clazz);

}

return (Class[])result.toArray(

new Class[0]);

}

Провайдеры сервисов

Сервис-провайдеры (Service provider) – довольно распространенная и неплохо документированная техника среди стандартных библиотек Java. Тем более удивительно, что фирма Sun оставила недокументированным механизм перечисления сервис-провайдеров – класс sun.misc.Service.

Идея сервис-провайдеров вполне очевидна. Допустим, есть некоторый сервис (интерфейс или абстракный класс), предназначенный для использования прикладными программами. Есть также произвольное количество провайдеров – классов, реализующих этот интерфейс или абстрактный класс. Предполагается, что набор провайдеров не фиксирован и может меняться в зависимости от поставки программы. Не исключено, что пользователь может самостоятельно приобрести и инсталлировать в систему дополнительный набор провайдеров какого-либо стандартного сервиса.

Типичный пример применения сервис-провайдеров – разнообразные кодеки, позволяющие читать и писать изображения, аудио или видеозаписи различных форматов. Общая процедура чтения или записи объекта – это сервис, а конкретный кодек, рассчитанный на определенный формат – это провайдер.

Согласно документации, для поддержки сервис-провайдеров нужно положить в подкаталог META-INF/Services некоторого JAR-файла, присутствующего в путях поиска классов Java, специальный текстовый файл. Имя текстового файла должно совпадать с полным именем некоторого сервиса – интерфейса или абстрактного класса, например, «com.pupkin.vasya.My-Service». Этот файл должен содержать (в отдельных строках) список некоторых классов – провайдеров этого сервиса, присутствующих в данном JAR-файле. Тогда система Java сумеет стандартным образом получить этот список в виде набора объектов – экземпляров соответствующих провайдеров. Стандартные библиотеки фирмы Sun примерно так и поступают, когда нужно прочитать изображение или аудиозапись. Почему-то остался недокументированным лишь тот самый стандартный способ, которым извлекается список провайдеров некоторого сервиса.

Этот способ следующий:

for (Iterator iterator=

sun.misc.Service.providers(

класс_сервиса.class);

iterator.hasNext(); )

{

класс_сервиса o= (класс_сервиса)

iterator.next();

используем o – экземпляр

очередного провайдера сервиса;

}

На самом деле для использования такой техники не обязательно создавать JAR-файл – вполне достаточно разместить правильный подкаталог META-INF в одном из каталогов поиска class-файлов.

http://www.samag.ru/cgi-bin/go.pl?q=articles;n=03.2003;a=08


July 27th, 2010  
Tags: Java



Java: магия отражений

Java 0 Comment »

Часть III. Компиляция Java средствами Java

Даниил Алиевский

В предыдущих частях статьи мы познакомились с технологией отражений (Java Reflection). Это мощнейший механизм Java, позволяющий делать с .class-файлами практически все что угодно – загружать из произвольных файлов, анализировать набор членов класса, обращаться к этим членам, при необходимости обходя стандартную защиту «private»/«protected». При желании можно даже подменить стандартный механизм загрузки Java-классов и взять этот процесс под полный контроль, например, разрешить перезагружать изменившиеся версии .class-файлов без полной перезагрузки Java-машины (эта техника подробно рассматривалась в части II, см. №1(2) журнала «Системный администратор»).

В этой части статьи мы научимся компилировать Java-код в .class-файлы. Совместно с технологией отражений это позволит в процессе исполнения программы «на лету» создавать новые классы в виде исходного текста, компилировать их, загружать и использовать. Столь мощные возможности обычно присущи лишь чисто интерпретируемым, сравнительно медленным языкам типа JavaScript или Perl или Ассемблеру (точнее, машинному языку).

Всюду далее, если не оговорено обратное, мы будем подразумевать последнюю (на момент написания статьи) версию Java фирмы Sun: Sun Java SDK 1.4.

Как скомпилировать Java-файл с исходным текстом

Решение, вообще говоря, совершенно банально – вызвать стандартный компилятор javac!

На первый взгляд подобное решение кажется неизящным, сопряженным с массой проблем. Например, компилятор придется поставлять всем вашим пользователям. Другой пример – интеграция компилятора с вашей системой: сообщения компилятора должны выдаваться не на системную консоль, а «внутрь» вызывающей программы для обработки и визуализации в рамках вашего пользовательского интерфейса.

Разработчики Java позаботились о том, чтобы эти проблемы легко решались.

Прежде всего, стандартный компилятор javac входит в комплект поставки Sun Java SDK, распространяемого совершенно бесплатно. Правда, здесь есть одна тонкость.

При формировании дистрибутива Java-приложения обычно принято включать в этот дистрибутив некий фрагмент Java SDK, так называемый JRE (Java2 TM Runtime Environment) – набор файлов, достаточный для запуска Java-приложения. В комплекте Sun Java SDK этот набор оформлен в виде подкаталога jre/. По умолчанию JRE не содержит компилятора javac (и ряда других полезных утилит из Java SDK). Включать в дистрибутив полный пакет Java SDK запрещено лицензионным соглашением фирмы Sun.

Однако в том же лицензионном соглашении специально оговорено, что компилятор байт-кода javac вместе с необходимым вспомогательным JAR-файлом tools.jar можно включать в дистрибутив в дополнение к стандартному JRE, точнее, распространять совместно с Java-приложением. См. файл jre/license в комплекте поставки Sun Java SDK 1.4.1, раздел «JavaTM 2 runtime environment (j2re), standard edition, version 1.4.1_x supplemental license terms», пункт 3 и файл jre/README.txt, раздел «Redistribution of Java 2 SDK Files».

Но самое приятное заключается в том, что в действительности компилятор фирмы Sun реализован на том же языке Java – в виде класса com.sun.tools.javac.Main и пакета вспомогательных классов, размещенных в архиве tools.jar. Утилита javac является всего-навсего «оболочкой», стартующей виртуальную машину Java и запускающей указанный класс. Архив tools.jar, как и саму утилиту javac, разрешается свободно распространять (в дополнение к стандартному JRE) совместно с Java-приложением.

Это означает, что для компиляции Java-класса из Java-приложения нет необходимости обращаться к внешней утилите javac средствами операционной системы (методами Runtime.getRuntime().exec(…)). Можно напрямую воспользоваться классом com.sun.tools.javac.Main.

Использование класса com.sun.tools.javac.Main предельно просто. Вот полный интерфейс этого класса (конструктор и public-методы):

// Constructors

public Main()

// Methods

public static void main(String[] p0)

public static int compile(String[] p0)

public static int compile(String[] p0, PrintWriter p1)

Для вызова компилятора нужно обратиться к к одному из двух его static-методов compile.

В качестве аргумента p0 нужно передать массив строк-параметров, которые обычно передаются утилите javac, например:

new String[] {

“-d”,

“/путь_к_подкаталогу”,

“myfile.java”

}

В качестве аргумента p1 во второй из этих методов нужно передать поток вывода, который будет использоваться для вывода всех сообщений компилятора. Таким образом можно «перехватить» все сообщения компилятора и перенаправить их в свой собственный буфер, который впоследствии можно проанализировать или показать пользователю.

В качестве результата оба метода возвращают стандартный код возврата утилиты javac (errorlevel в терминах MS-DOS). 0 сигнализирует об успешном завершении, другие значения – о каких-либо ошибках.

Приведем пример вызова компилятора путем обращения к классу com.sun.tools.javac.Main:

String[] args= параметры утилиты javac;

CharArrayWriter writer= new CharArrayWriter();

int result= com.sun.tools.javac.Main.compile(

args,

new PrintWriter(writer,true));

if (result!=0) {

/* – произошла какая-то ошибка */

анализируем и, возможно, показываем пользователю строку сообщений компилятора writer.toString()

}

Описанное решение очень просто, удобно и эффективно. Но у него есть серьезный недостаток.

Класс com.sun.tools.javac.Main, как и все классы из пакетов com.sun.* и sun.*, является недокументированным. Приведенное выше описание использования класса опирается на здравый смысл и эксперименты, а не на официальную документацию фирмы Sun. Фирма Sun имеет полное право в очередной версии Java SDK изменить поведение или интерфейс этого класса или даже вообще исключить его.

Указанная проблема не надумана.

Действительно, все сказанное выше справедливо лишь для версии Sun Java SDK 1.4. В предыдущей версии, Sun Java SDK 1.3, тот же самый класс com.sun.tools.javac.Main имел совершенно другой интерфейс:

// Constructors

public Main()

// Methods

public static void main(String[] p0)

public int compile(String[] p0)

Метод compile не был статическим, т.е. нуждался в создании экземпляра класса. Также отсутствовала версия метода, позволяющая указать собственный поток для сообщений компилятора.

В версии Java SDK 1.3 для компиляции исходных текстов Java обычно использовался другой класс, sun.tools.javac.Main (расположенный все в том же архиве tools.jar). Этот класс также позволяет указать свой поток для сообщений компилятора. Причем этот класс полностью сохранил свой интерфейс при переходе от версии SDK 1.3 к SDK 1.4.

Класс sun.tools.javac.Main имеет нестатический синхронизованный метод compile:

public synchronized boolean compile(String[] p0)

Поток для сообщений компилятора указывается в качестве первого параметра конструктора:

public Main(OutputStream p0, String p1)

(не PrintWriter, а более «архаичный» OutputStream). Смысл второго параметра конструктора я так и не выяснил, но найденные мной в Интернете примеры использования данного класса передавали в качестве p1 строку «javac».

Код возврата утилиты javac возвращается отдельным методом:

public int getExitStatus()

Факт успешности компиляции можно также узнать по boolean-результату метода compile (false означает неудачу).

Но и этот класс, несмотря на сохранение интерфейса, в действительности изменил свое поведение при переходе от версии SDK 1.3 к SDK 1.4.

Во-первых, он был объявлен как устаревший («deprecated»). Конечно, предупреждение компилятора, появляющееся при попытке использовать данный класс («warning: sun.tools.javac.Main in sun.tools.javac has been deprecated»), можно и проигнорировать. Но фирма Sun почему-то решила добавлять аналогичное предупреждение в любое сообщение, выдаваемое самим компилятором sun.tools.javac.Main! Если попытаться использовать sun.tools.javac.Main для компиляции любого, даже совершенно корректного Java-файла, в любом случае будет выдано предупреждение «sun.tools.javac.Main has been deprecated». Чтобы избавиться от него, sun.tools.javac.Main придется использовать с ключом «-nowarn», но тогда вообще теряется возможность получать и анализировать предупреждения компилятора.

Во-вторых, в версии Sun Java SDK 1.4 компилятор sun.tools.javac.Main попросту не всегда адекватно работает. На простых тестах это трудно обнаружить. Но когда я попытался скомпилировать с помощью этого компилятора все исходные тексты большого Java-проекта, обнаружилось, что некоторые сложные, но вполне корректные классы, прекрасно компилируемые «штатными» компиляторами и классом com.sun.tools.javac.Main, не компилируются с помощью sun.tools.javac.Main. В частности, компилятор sun.tools.javac.Main «сломался» на некоторых нетривиальных случаях перегрузки методов с аргументами примитивных типов, а также при попытке объявить метод toString() у некоторого вложенного класса. Это очень похоже на внутреннюю ошибку компилятора, которую фирма Sun не сочла нужным исправлять в устаревшем наборе классов.

Есть также мелкие отличия в самом синтаксисе языка Java, понимаемом компиляторами sun.tools.javac.Main и стандартным com.sun.tools.javac.Main. (Ошибка, о которой ранее шла речь, не связана с этими мелкими отличиями – там действительно имела место явная ошибка компилятора.) Например, sun.tools.javac.Main разрешает импортировать (предложением import) конкретные классы, расположенные в корневом пакете – т.е. непосредственно в корне одного из каталогов, перечисленных в путях поиска CLASSPATH. Стандартный компилятор в современных версиях Java не допускает такого экзотического импорта – все импортируемые классы должны лежать внутри какого-либо пакета.

Также можно заметить, что в версии Sun Java SDK 1.4 компилятор sun.tools.javac.Main работает примерно вдвое медленнее, чем com.sun.tools.javac.Main.

Все сказанное означает, что использование для компиляции Java-файлов конкретных классов типа com.sun.tools.javac.Main или sun.tools.javac.Main – рискованное занятие. Соответствующий код придется заново тестировать при выпуске каждой новой версии Java SDK и, возможно, в какой-то момент его придется радикально переписывать.

В случае com.sun.tools.javac.Main лично мне риск не кажется слишком большим. Похоже, что в этом классе фирма Sun наконец «довела до ума» решения, существовавшие в предыдущих версиях Java в этом же классе и в sun.tools.javac.Main. Трудно представить, чтобы возможности класса com.sun.tools.javac.Main в какой-то версии исчезли или радикально поменялись. Скорее всего, этот класс либо сохранится, либо превратится в легальный документированный класс, например, в пакете java.*, тогда необходимые изменения будут минимальны.

Если необходимо надежное документированное решение, то на сегодня единственный доступный вариант – вызвать внешнюю утилиту javac одним из методов Runtime.getRuntime().exec(…).

Для этого, правда, эту утилиту нужно еще найти в файловой системе. Если данная утилита входит в состав дистрибутива и инсталлируется вместе с Java-приложением, то инсталлятор может сам позаботиться о том, чтобы полный путь к утилите javac передавался в Java-программу. Если же приложение дожно работать под управлением «чужого» Java SDK, инсталлируемого пользователем независимо от приложения, то можно поискать файлы:

n  bin/javac.exe (случай Microsoft Windows);

n  bin/javac (случай Unix/Linux);

n  bin/sparcv9/javac (случай Solaris SPARC)

в каталоге System.getProperty(«java.home») и содержащем его каталоге. (Чаще всего System.getProperty(«java.home») соответствует подкаталогу jre/ в главном каталоге Sun Java SDK. Соответственно, утилита javac расположена в подкаталоге bin/ содержащего его каталога.)

Для получения сообщений компилятора в данном случае можно использовать стандартную технику – чтение из потоков, возвращаемых методами getInputStream() и getErrorStream() объекта Process, полученного в результате обращения к методу Runtime.getRuntime().exec(…).

Вот как примерно это выглядит:

Process p= Runtime.getRuntime().exec(массив_аргументов);

/* первый элемент в массиве должен содержать полное имя файла утилиты javac, остальные элементы – параметры этой утилиты */

final InputStreamReader is=new InputStreamReader(p.getInputStream());

final InputStreamReader es=new InputStreamReader(p.getErrorStream());

final StringBuffer out= new StringBuffer();

final StringBuffer err= new StringBuffer();

new Thread() {

public void run() {

try {

char[] buf= new char[32768];

int len;

while ((len=is.read(buf,0,buf.length))>=0) {

out.append(buf,0,len);

}

} catch (Exception e) {

e.printStackTrace();

}

}

}.start();

new Thread() {

public void run() {

… (аналогичный цикл для es и err)

}

}.start();

int result= p.waitFor();

/* дожидаемся завершения утилиты javac и получаем ее код завершения */

if (result!=0) {

/* – произошла какая-то ошибка */

анализируем и, возможно, показываем пользователю сообщения компилятора out и err

}

Обратите внимание: чтение потоков вывода и ошибок в отдельных параллельных потоках совершенно необходимо. Если этого не сделать, то вызов метода waitFor() может привести к зависанию в случае, когда внешняя программа выводит хоть что-нибудь в свои потоки вывода и ошибок.

Как скомпилировать исходный текст Java, заданный в виде строки

Итак, мы научились вызывать компилятор Java. Таким образом можно скомпилировать любой Java-файл – достаточно следовать инструкциям по использованию компилятора javac. Но что делать, если у нас нет готового Java-файла, размещенного где-то в файловой системе? Допустим, мы располагаем просто исходным текстом Java-программы в виде строки типа String – загруженным, скажем, из базы данных, или сгенерированным автоматически. Как скомпилировать такой текст?

Очевидно, нужно создать некоторый временный файл с расширением .java, записать туда исходный текст, после чего вызвать компилятор. Эти действия далеко не так просты, как кажется на первый взгляд. Рассмотрим это подробнее.

В стандартных библиотеках Java есть средства для создания временных файлов – это 2 статических метода createTempFile класса File, создающие файл с уникальным «случайным» именем. К сожалению, воспользоваться ими в данном случае невозможно. Новый Java-файл, создаваемый для сохранения заданного исходного Java-кода, не может иметь произвольное имя – его имя обязательно должно совпадать с именем public-класса, объявленного в этом исходном коде (если, конечно, таковой имеется). Кроме того, нужен какой-то каталог, куда будут записаны .class-файлы, полученные в результате компиляции.

Все это означает, что нужна специальная функция, создающая временный подкаталог – так же, как File.createTempFile создает временный файл. Внутри этого подкаталога можно создать серию вложенных каталогов, соответствующую пакету, в котором должен располагаться компилируемый класс. Затем в самый внутренний каталог нужно записать Java-файл с исходным текстом, присвоив этому файлу имя, соответствующее имени компилируемого класса. Эту же структуру каталогов можно использовать для размещения результирующих .class-файлов, передав соответствующие инструкции компилятору javac.

Написать функцию создания временного каталога не очень сложно. Достаточно использовать в качестве образца реализацию File.createTempFile в исходном тексте класса java.io.File. Основная идея – циклически генерировать более или менее случайные имена подкаталогов внутри каталога временных файлов операционной системы, для каждого подкаталога пытаться его создать (методом File.mkdir) и выйти из цикла, как только очередная попытка будет удачной (mkdir вернет true). Цикл генерации имен и создания подкаталога нужно синхронизовать относительно какого-либо глобального объекта точно так же, как это сделано в методе File.createTempFile. Чтобы найти умолчательный каталог временных файлов операционной системы, можно обратиться к переменной среды:

System.getProperty(“java.io.tmpdir”)

В версии Sun Java SDK 1.4.1 эта переменная является документированной (в отличие от Sun Java SDK 1.3).

Возникает также проблема автоматического удаления созданного временного подкаталога при выходе из Java-программы. В случае файла эта задача решалась бы методом deleteOnExit класса File. Но непустые подкаталоги этот метод удалять не умеет. Самое простое решение – написать функцию, рекурсивно удаляющую созданный временный каталог вместе со всеми файлами и подкаталогами, и вызвать ее в потоке, зарегистрированном методом Runtime.getRuntime().addShutdownHook(…).

Следующий вопрос, требующий некоторого внимания – как правильно записать Java-файл с исходным текстом. Исходный текст Java (представленный по условию в виде строки String) может содержать произвольные символы Unicode, а компилятор javac обычно используется с ASCII-файлами.

Здесь есть два решения. Во-первых, начиная с версии Sun Java SDK 1.4, компилятор javac «понимает» дополнительный параметр «-encoding». Можно, например, сохранить файл в кодировке «UTF-8» и указать такую же кодировку в качестве параметра «-encoding». Чтобы сохранить текст в файле с заданной кодировкой, используется объект java.io.Writer, создаваемый вызовом:

OutputStreamWriter writer= new OutputStreamWriter(new FileOutputStream(file),encoding)

Во-вторых, компилятор javac – и в SDK 1.4, и в предыдущих версиях – поддерживает специальный способ кодирования Unicode-символов. А именно, цепочка символов вида \uNNNN, где N – шестнадцатеричные цифры, в любом месте Java-файла воспринимается компилятором как Unicode-символ с кодом NNNN. Можно перед записью Java-файла заменить все символы с кодами і128 такими цепочками и записать полученный «чистый» ASCII-файл.

После того как исходный текст скомпилирован, если компилятор сообщил об отсутствии ошибок (нулевой код возврата) и если параметры компилятора были заданы правильно (точнее, параметр «-d»), можно ожидать, что в структуре каталогов появятся все необходимые .class-файлы. Сколько их появится, заранее сказать невозможно (без полного синтаксического анализа исходного кода). Так, каждый анонимный класс породит отдельный .class-файл. Но, по крайней мере, должен появиться .class-файл, соответствующий главному public-классу, объявленному в исходном тексте (если, конечно, исходный текст не состоял из описаний одного или нескольких не-public-классов). В качестве последней «страховки» имеет смысл проверить существование этого .class-файла – его отсутствие говорит о неверных настройках компилятора.

Затем остается только загрузить сгенерированный .class-файл. Для этого нужен собственный загрузчик классов – наследник ClassLoader, умеющий загружать классы из нестандартного каталога (в нашем случае из временного каталога, созданного внутри каталога временных файлов операционной системы). Эта задача подробно рассматривалась во второй части статьи: «ClassLoader – скрытые возможности». Здесь мы не будем на ней останавливаться.

В завершение стоит добавить, что скомпилированные и загруженные классы имеет смысл кэшировать в оперативной памяти с помощью таблицы HashMap, ставящей каждому исходному тексту в соответствие результирующий объект Class – загруженный Java-класс. Если нужно скомпилировать исходный текст, идентичный тексту, который когда-то уже компилировался, то в большинстве случаев нет смысла компилировать его повторно – достаточно извлечь готовый класс из кэша.

Eval на Java: интерпретатор формул

Теперь мы располагаем чрезвычайно мощным инструментом. Мы умеем компилировать произвольные исходные тексты Java, заданные в виде строковой переменной, и загружать получаемые при этом классы. Обычно такую технику называют самопрограммированием. Эта возможность традиционно присутствует в медленных интерпретируемых скриптовых языках типа JavaScript или Perl, но отсутствует в высокоэффективных компилируемых языках (к которым относится и Java), исключая разве что Ассемблер. Фактически, мы снабдили язык Java самопрограммированием.

Но пока что пользоваться этой техникой не очень удобно.

В скриптовых языках типа JavaScript или Perl самопрограммирование выглядит крайне просто. Например, в JavaScript можно написать так:

var a= 23;

var b= 45;

var formula= “a+b”;

var result= eval(formula);

Оператор eval исполняет переданный ему фрагмент JavaScript-кода так, как если бы он непосредственно был вставлен в общий JavaScript-код вместо самого оператора eval.

Мы попробуем реализовать аналогичную технику в рамках Java.

Java – не JavaScript, и в точно таком же виде реализовать функцию eval здесь вряд ли возможно. В качестве ближайшего Java-аналога eval, мы поставим следующую задачу.

Требуется реализовать статический метод некоторого класса-библиотеки (допустим, Evaluator), имеющий следующий интерфейс:

public Object eval(String javaExpression, Object context) throws Exception

В качестве javaExpression передается произвольное выражение Java, например то же «a+b». В качестве context передается объект, предоставляющий «пространство имен». Это значит, что в выражении javaExpression должна быть возможность без дополнительных уточнений обращаться ко всем public- и, может быть, protected-членам объекта context. Так, в случае формулы «a+b» объект context должен содержать числовые (или строчные) поля a и b. В своем результате eval возвращает объект Java, получаемый в результате интерпретации выражения javaExpression.

Приведенный выше пример на JavaScript для Java выглядел бы примерно так:

public static class Context {

public int a,b;

}

…

public void НекоторыйМетод() {

…

Context c= new Context();

c.a= 23;

c.b= 45;

String formula= “new Integer(a+b)”;

Integer result= (Integer)Evaluator.eval(formula,c);

…

}

Язык Java по обыкновению создает затруднения при попытке работать с примитивными типами на общих основаниях – их приходится заменять соответствующими классами-оболочками. Специально для упрощения работы с примитивными типами имеет смысл дополнить основной метод eval версиями evalInt, evalLong, evalFloat, evaDouble, evalBoolean, возвращающими результат соответствующего примитивного типа. Тогда последние 2 строки примера выглядели бы проще:

…

String formula= “a+b”;

int result= Evaluator.evalInt(formula,c);

…

Наконец, если считать, что наш пример является частью реализации нестатического метода некоторого класса, то пример можно было бы упростить еще больше:

public class НекоторыйКласс {

…

public int a,b;

public void НекоторыйМетод() {

…

a= 23;

b= 45;

String formula= “a+b”;

int result= Evaluator.evalInt(formula,this);

…

}

}

Это практически так же удобно, как и eval в скриптовых языках.

Как решить поставленную задачу – реализовать описанные методы eval, evalInt и прочие?

Существует достаточно элементарное частичное решение.

Потребуем, чтобы все обращения к членам объекта context в формуле javaExpression производились не напрямую, а через некоторую дополнительную переменную – ссылку на объект context. Формула в этом случае приобретает примерно такой вид: «c.a+c.b» (имя ссылки «c» могло бы быть дополнительным аргументом метода eval).

В этом варианте задачу решить легко. Конструируем «на лету» текст Java-класса:

import java.io.*;

import java.util.*;

какие-нибудь еще полезные import, которые могут пригодиться внутри формулы

public class ___ExpressionNNN {

public static int ___performEval(

имя_класса_context c)

{

int returnValue= текст_формулы;

return returnValue;

}

}

Вместо NNN подставляется некоторый уникальный индекс – свой для каждого текста формулы. (Если разные формулы будут интерпретироваться с помощью разных классов, то эти классы можно будет кэшировать и не компилировать повторно – смотри о кэшировании в конце предыдущего раздела.) Вместо «имя_класса_context» подставляется context.getClass().getName(), вместо «текст_формулы» – значение javaExpression.

Тип результата int метода ___performEval и тип переменной returnValue соответствуют варианту метода evalInt. Другие варианты – eval, evalDouble и прочие – должны использовать другой тип (соответственно Object, double, и т. д.).

Имя «c» аргумента ___performEval – это имя, под которым объект context будет доступен внутри формулы. Оно может быть дополнительным аргументом методов eval, evalInt, …

Компилируем полученный текст класса и загружаем скомпилированный класс, как описано в предыдущих разделах. Затем средствами отражений вызываем его статический метод ___performEval, передавая ему в качестве аргумента наш объект context, и возвращаем полученный результат. Задача решена.

Дополнительная переменная returnValue позволяет передавать в качестве формулы javaExpression фрагменты кода, состоящие более чем из одного оператора Java. В этом случае по умолчанию результатом вызова eval окажется результат первого оператора, но всегда можно будет в последнем операторе написать что-нибудь вроде

returnValue= результат_наших_вычислений;

и тем самым вернуть другой результат.

Приведенное решение, разумеется, неизящно. В формуле приходится ссылаться на члены класса-контекста через громоздкую запись типа «c.a». Попробуем избавиться от явной ссылки «c.». Язык Java разрешает ссылаться непосредственно, без дополнительных уточнений, на члены текущего класса, его предков, члены класса, по отношению к которому текущий является вложенным, и члены предков этого класса. Если класс, которому принадлежит метод ___performEval, унаследовать от класса context.getClass().getName() или вложить в другой класс, унаследованный от context.getClass().getName(), то к членам этого предка можно будет обращаться из нашей формулы непосредственно. Метод ___performEval, разумеется, нужно будет сделать нестатическим.

Таким способом формулу типа «a+b» скомпилировать удастся – синтаксически все будет соблюдено. Но как добиться, чтобы a и b ссылались именно на члены данного экземпляра context, переданного в качестве аргумента в метод eval (или evalInt, evalDouble, …)? Все, что можно сделать «легально» для исполнения формулы – создать новый экземпляр для нашего нового класса (или по экземпляру для серии вложенных классов) и указать именно этот новый экземпляр при вызове метода ___performEval через отражения. Виртуальная машина Java не позволит «подменить» экземпляр нового временного класса объектом context, так же context является предком нашего нового объекта, и его нельзя использовать там, где декларировано использование объекта-потомка.

Приходит в голову банальное решение – перед вызовом ___performEval скопировать все поля объекта context в заново созданный экземпляр наследника context.getClass().getName(), а в конце скопировать все поля обратно (на случай, если формула их изменяла). Все это в принципе осуществимо средствами отражений, но вряд ли такое решение можно назвать качественным. Если полей много, такое копирование может занять много времени. Кроме того, в сложных случаях такое поведение попросту может оказаться ошибочным. Представьте себе, что некоторый внешний поток постоянно наблюдает за состоянием экземпляра context, и каждое изменение его состояния, в том числе внутри формулы (в результате вызова методов context), должно быть немедленно обнаружено. Очевидно, при описанном подходе изменения вообще обнаружены не будут. Все изменения будут произведены с другим объектом – копией context, а в момент присваивания новых полей полям экземпляра context наш метод eval «не будет знать», как сообщить об этих изменениях.

Хотелось бы попытаться все-таки получить доступ непосредственно к экземпляру context.

Решение существует. Для этого нам придется «обмануть» компилятор – создать .class-файл, который не может быть создан «законным» компилятором javac.

Рассмотрим следующий код Java:

import java.io.*;

import java.util.*;

какие-нибудь еще полезные import, которые могут пригодиться внутри формулы

public class ___ExpressionNNN

extends имя_класса_context

{

public class ___Performer {

public int ___performEval() {

int returnValue= текст_формулы;

return returnValue;

}

}

}

Все, как в прошлый раз, но теперь добавился вложенный класс ___Performer. Метод ___performEval теперь нестатический и не обладает аргументом, а внешний класс ___ExpressionNNN унаследован от context.getClass().getName().

Во что компилируется такой исходный код?

В результате компиляции получается 2 .class-файла – ___ExpressionNNN.class и ___ExpressionNNN$___Per-former.class. Рассмотрим их внимательно с помощью какого-нибудь дизассемблера, например утилиты javap (с ключами -private и -c).

Класс ___ExpressionNNN здесь – «пустышка». Он не содержит ни одного члена, кроме пустого (автоматически добавленного) конструктора. Этот класс – наследник context.getClass().getName().

Класс ___Performer имеет следующий вид:

public class ___ExpressionNNN$___Performer {

// Fields

private final ___ExpressionNNN this$0;

// Constructors

public ___ExpressionNNN$___Performer(

___ExpressionNNN p0)

{

реализация: копирует p0 в this$0

}

// Methods

public int ___performEval()

{

реализация, содержащая скомпилированный текст нашей формулы: для обращений к a,b и другим членам context используется ссылка this$0

}

}

Обратите внимание на поле this$0. Это «скрытый механизм» языка Java, позволяющий добираться из вложенных нестатических классов до текущих экземпляров содержащих их внешних классов. Заметьте: хотя идентификатор this$0 является корректным с точки зрения синтаксиса Java, компилятор javac не позволит объявить поле с таким именем в обычном классе – идентификатор «зарезервирован для внутреннего использования».

Вызов ___performEval через отражения выглядит следующим образом:

n  Вначале нужно создать экземпляр класса ___ExpressionNNN (обычным вызовом Class.newInstance).

n  Затем нужно отыскать (единственный) конструктор класса ___ExpressionNNN$___Performer и вызвать его (через java.lang.reflect.Constructor), передав в качестве аргумента ссылку на созданный экземпляр ___ExpressionNNN. Будет создан экземпляр ___ExpressionNNN$___Performer.

n  Затем нужно обычным образом (через java.lang.reflect.Method) отыскать и вызвать метод ___performEval, передав методу invoke в качестве параметра ссылку на только что созданный экземпляр ___ExpressionNNN$___Performer.

Если бы на шаге 2 удалось вместо нового экземпляра ___ExpressionNNN «подсунуть» конструктору наш экземпляр context, задача была бы решена. Метод ___performEval работал бы (через ссылку this$0) с нашим экземпляром, т.е. выполнил бы нашу формулу в требуемом контексте.

Как уже говорилось выше, виртуальная машина Java не допускает передачи в качестве аргумента типа ___ExpressionNNN экземпляра его предка context.getClass().getName().

Но можно слегка изменить класс ___Performer. Действительно, если в классе ___Performer везде заменить имя класса ___ExpressionNNN на context.getClass().getName() (прежде всего тип поля this$0 и тип аргумента конструктора), то этот класс останется с точки зрения виртуальной машины вполне корректным. Класс ___ExpressionNNN не добавляет к своему предку ни одного нового члена, к которому метод ___performEval мог бы попытаться обратиться через this$0. При работе с таким скорректированным классом через отражения на шаге 2 требования к аргументу конструктора были бы слабее: можно было бы передать ссылку на экземпляр класса context.getClass().getName(), в частности на наш экземпляр context.

Такую коррекцию невозможно сделать «легальным» путем, изменяя исходный текст и вызывая компилятор javac. Стандартный компилятор при генерации вложенного класса непременно придаст ссылке this$0 точный тип того класса, в который вложен данный. Но можно скорректировать уже скомпилированный .class-файл ___ExpressionNNN$___Performer.class.

Это не так сложно, как кажется. Формат .class-файлов спроектирован очень грамотно и удобен для наших целей.

В начале файла идет так называемый «пул констант», в котором собраны все символьные идентификаторы (в кодировке UTF-8), в том числе имена всех упоминаемых классов. Весь .class-файл, в частности пул констант, организован в виде последовательности секций примерно такого вида:

n  код_типа_секции

n  содержимое_фиксированной_длины

или

n  код_типа_секции

n  длина_секции

n  содержимое_переменной_длины

Если в произвольной секции изменить содержимое и соответствующим образом подправить длину секции (размер файла при этом тоже поменяется), то файл сохранит свою корректность. В .class-файле не встречаются прямые смещения от начала файла или длины в байтах каких-то фрагментов файла, за исключением длин элементарных секций.

Имя класса ___ExpressionNNN может встречаться внутри файла ___ExpressionNNN$___Performer.class в виде строковых констант трех видов:

n  непосредственно строка «___ExpressionNNN» (используется не всеми компиляторами);

n  строка «L___ExpressionNNN;» – внутреннее имя типа ___ExpressionNNN: именно таким образом виртуальная машина «именует» классы «внутри себя» (для примитивных типов и массивов используются другие обозначения);

n  строка «(L___ExpressionNNN;)V» – сигнатура конструктора или любого другого метода с единственным аргументом типа ___ExpressionNNN.

Если бы наш класс ___ExpressionNNN был вложен внутрь какого-либо некорневого пакета (в наших примерах он размещается в корневом пакете), то в его полном имени нужно было бы заменить точки символами “/”.

Строковые константы, в частности перечисленные выше, представлены в пуле констант в виде следующих цепочек байтов:

n  1 байт: 1 (код строкового типа);

n  2 байта: длина length строковой константы в кодировке UTF-8 (сначала старший байт, потом младший);

n  length байтов: содержимое строковой константы в кодировке UTF-8.

Все, что нам нужно сделать – загрузить файл ___ExpressionNNN$___Performer.class в виде массива байтов, отыскать в нем все такие цепочки байтов для строк «___ExpressionNNN», «L___ExpressionNNN;», «(L___ExpressionNNN;)V» и заменить их соответствующими цепочками для строк «XXXX», «LXXXX;», (LXXXX;)V». Здесь XXXX – полное имя класса context, в котором точки (разделители имени пакета) заменены знаком «/»:

context.getClass().getName().replace(‘.’,'/’)

Полученный новый массив байтов (другой длины) нужно записать обратно в файл ___ExpressionNNN$___Performer.class.

Если имя ___ExpressionNNN не используется в Java-приложении ни для каких других целей – для максимальной уверенности можно заменить его чем-нибудь вроде ___Expression_Asj5Sjl3_NNN, – то полученный .class-файл будет вполне корректным. Остается загрузить его, создать экземпляр, передав конструктору в качестве аргумента наш объект context, и выполнить метод ___performEval(). Задача решена полностью.

Возможно, в формулах имеет смысл открыть непосредственный доступ (без уточняющего имени класса) к какому-либо набору стандартных функций или констант. Например, в математических формулах естественнее смотрелась бы запись «sin(a+PI/4)», а не «Math.sin(a+Math.PI/4)». Для этого достаточно автоматически добавить желаемый набор функций и констант во вложенный класс ___Performer.

Некоторое неудобство приведенного решения связано с тем, что класс объекта context обязан быть public, а в случае локального класса – public static. В частности, недопустимо использовать анонимные классы или локальные классы, объявленные внутри методов. В противном случае нам попросту не удастся унаследовать от него класс ___ExpressionNNN, расположенный, вообще говоря, в совершенно другом пакете (в наших примерах – в корневом пакете).

Очевидно также, что для достижения хорошей эффективности все скомпилированные формулы необходимо кэшировать, чтобы одна и та же формула не компилировалась повторно. Имеет смысл для каждой пары «формулы, экземпляр context» сохранить (в таблице HashMap) готовый объект java.lang.reflect.Method для метода ___performEval и экземпляр вложенного класса ___Performer, чтобы при повторном обращении к eval осталось просто вызвать метод invoke. Также есть смысл проверить, что одна и та же формула с одним и тем же context вызывается повторно, и в этом случае перейти на особо быструю ветку – не обращающуюся к таблице HashMap. Подобная оптимизация позволяет достичь чрезвычайно высокого быстродействия, недостижимого для интерпретируемых скриптовых языков: накладные расходы будут укладываться в доли микросекунды (на компьютерах класса Pentium-III 800).

Идея описанного выше изящного решения принадлежит моему коллеге, Алексею Вылегжанину (anv@siams.com).

Разумеется, это решение (в отличие от первого варианта, требующего использования в формулах ссылки «с.»), не является стопроцентно переносимым. Оно зависит от особенностей компилятора javac, которые в принципе могут измениться в следующей версии Java. Например, компилятору ничто не мешает вставить в класс ___Performer явную проверку, что поле this$0 принадлежит нужному типу, причем сформировать имя типа динамически путем сложения строк «___Expres» и «sionNNN». Тогда скорректированный нами класс работать не будет. Кажется маловероятным, что подобное произойдет. Но на всякий случай описанное решение желательно заново тестировать с каждой новой версией Sun Java SDK.

Технология интерпретации формул на Java может оказаться чрезвычайно полезной в сложных приложениях, гибко настраиваемых пользователем. Скажем, процедура построения графика или статистического анализа может принимать на вход не только массив чисел, но и некоторую набранную пользователем аналитическую формулу. При заполнении сложной формы с множеством параметров можно разрешить в некоторых полях указывать формулы, оперирующие другими полями формы (например, «предыдущее_поле + 1»). Можно снабдить пользователя простейшим формульным калькулятором, который всегда под рукой и обладает функциями, специфичными для данного приложения.

Единственной серьезной проблемой может оказаться безопасность. Если разрешить пользователю Java-приложения самостоятельно писать формулы, исполняемые системой, то пользователь сможет, вообще говоря, выполнить любой законный оператор языка Java – скажем, добраться до содержимого диска. Если это недопустимо, то придется разрабатывать специальный менеджер безопасности («песочницу») для исполнения формул или жестко контролировать текст формулы.

Заключение

Нашу экскурсию по миру отражений можно считать законченной. Мы рассмотрели основные задачи по управлению классами: доступ к произвольным классам и их членам, загрузку классов из произвольного источника и, наконец, компиляцию новых классов из Java-файлов либо «на лету». Эти технологии позволяют профессионально решать по-настоящему сложные задачи, а также могут быть чрезвычайно удобны и в небольших утилитах.

Я сознательно не стал приводить Java-реализацию большинства из описанных выше технологий. Кроме того что это было бы чрезвычайно громоздко, мне кажется, что подобные задачи лучше разработчику решать самостоятельно, если только готовое решение не существует в виде стандартизованных и общеизвестных библиотек фирмы Sun или другой фирмы, заслуживающей не меньшего доверия. Разумеется, я располагаю законченной библиотекой, воплощающей все приведенные идеи, в том числе динамическую загрузку классов, описанную во второй части статьи. Но я никоим образом не берусь утверждать, что моя библиотека достаточно грамотно реализована и хорошо протестирована – настолько же, насколько это можно сказать о библиотеках фирмы Sun. Если вы реализуете необходимые функции самостоятельно, то, по крайней мере, вы сможете достаточно свободно отлаживать их и совершенствовать, что было бы гораздо труднее сделать с чужим кодом.

При написании статьи были использованы официальные материалы фирмы Sun, приведенные на сайте http://java.sun.com, а также следующие книги:

n  Арнолд К., Гослинг Дж., Холмс Д. Язык программирования Java. Издательский дом «Вильямс», Москва – С.-Петербург – Киев, 2001.

n  Вебер Дж. Технология Java(TM) в подлиннике. «BHV – Санкт-Петербург», С.-Петербург, 1997.

Все приведенные в статье фрагменты кода я старался тщательно тестировать. Если вы обнаружите в моих текстах ошибки или неточности, а также в случае любых вопросов буду рад вашим письмам.

http://www.samag.ru/cgi-bin/go.pl?q=articles;n=02.2003;a=15


July 27th, 2010  
Tags: Java, Java Reflection



Java: Магия отражений

Java 0 Comment »

Часть II. ClassLoader – скрытые возможности

Даниил Алиевский

Самый интригующий класс мира отражений

В этой статье речь пойдет о классе java.lang.ClassLoader. Во многих отношениях он заслуживает эпитета «самый». Это самый фундаментальный класс в механизме отражений Java и одновременно самый яркий пример этой технологии. Это, пожалуй, самый необычный модуль не только в мире Java, но и среди большинства компилируемых языков. Это самый низкоуровневый, «глубинный» механизм Java, позволяющий вмешиваться практически в ядро Java-машины, причем оставаясь в рамках программирования на Java. Наконец, это один из самых трудных в понимании и использовании прикладных классов.

Как следует из названия, класс ClassLoader обеспечивает загрузку классов Java. Точнее, обеспечивают его наследники, конкретные загрузчики классов – сам ClassLoader абстрактен. Каждый раз, когда загружается какой-либо .class-файл, например, вследствие обращения к конструктору или статическому методу соответствующего класса – на самом деле это действие выполняет один из наследников класса ClassLoader.

Существует стандартный вариант реализации ClassLoader – так называемый системный загрузчик классов. Этот загрузчик используется по умолчанию при запуске приложений Java командой:

java Имя_главного_класса

Системный загрузчик классов реализует стандартный алгоритм загрузки из каталогов и JAR-файлов, перечисленных в переменной CLASSPATH (переменной среды либо параметре «-cp» утилиты «java»), а также из JAR-файлов, содержащих стандартные системные классы вроде java.lang.String и входящих в любой комплект поставки Java.

Одна из замечательных особенностей языка Java заключается в том, что вы можете реализовать свой собственный загрузчик классов – наследник ClassLoader – и использовать его вместо системного.

Наиболее популярный пример применения этой техники – Java-апплеты. Классы Java-апплетов, а также все классы, которыми они пользуются, автоматически загружаются с веб-сервера благодаря специальному загрузчику классов, реализованному «внутри» броузера.

Реализуя наследников ClassLoader, вы можете полностью контролировать процесс загрузки абсолютно всех Java-классов. Вы можете загружать их из любого источника, скажем, из собственной системы каталогов, не отраженной в CLASSPATH, из базы данных или из Internet. Вы можете предоставить загрузку стандартных библиотечных классов системному загрузчику, но при этом протоколировать факт обращения к ним. При желании вы даже можете сконструировать байт-код класса в памяти и после этого работать с ним, как с нормальным классом, загруженным из «добропорядочного» .class-файла. Среди компилируемых языков подобные возможности встречаются разве что в ассемблере.

Единственное, что вы не можете сделать – создать новый класс, не располагая его байт-кодом. Каким-либо образом: загрузив c диска, из Internet, из базы данных или создав как-то иначе, – ваш наследник ClassLoader обязан получить корректный байт-код класса (образ в памяти обычного .class-файла) в виде массива byte[]. Затем его нужно передать специальному стандартному методу ClassLoader.defineClass, который «превратит» его в готовый класс – объект типа Class.

Ниже мы подробно рассмотрим весь этот механизм и решим с его помощью практическую задачу – динамическую подгрузку изменившихся версий .class-файлов без перезапуска главной Java-программы. В процессе решения этой задачи мы увидим, как поразительным образом изменится поведение, казалось бы, четко стандартизованных конструкций языка Java. С помощью довольно простого Java-кода мы изменим сам язык Java, адаптируя его к новым возможностям, которые обеспечиваются нашим вариантом класса ClassLoader.

Как Java загружает классы

Основной способ работы с классом ClassLoader – это реализация наследников от него. Прежде чем переходить к рассмотрению этой техники, мы немного поговорим о том, как Java использует загрузчики классов.

Как уже было сказано, в системе всегда существует по крайней мере один готовый наследник ClassLoader – системный загрузчик. Его всегда можно получить с помощью вызова ClassLoader.getSystemClassLoader() – статического метода класса ClassLoader, объявленного следующим образом:

public static ClassLoader getSystemClassLoader()

Когда вы запускаете приложение Java с помощью стандартной команды:

java Имя_главного_класса

виртуальная машина Java первым делом создает системный загрузчик, загружает с его помощью .class-файла вашего главного класса и вызывает статический метод вашего класса, соответствующий объявлению:

public static void main(String[] argv)

(или же сообщает об ошибке, не обнаружив такого метода).

Java – язык с отложенной загрузкой кода. Первоначально загружается только один класс – тот, который передан в качестве параметра утилите «java»[1]. Как только код этого класса обращается к какому-то другому классу (любым способом: вызовом конструктора, обращением к статическому методу или полю), загружается другой класс. По мере выполнения кода, загружаются всё новые и новые классы. Ни один класс не загружается до тех пор, пока в нем не возникнет реальная потребность. (Такое поведение заложено в стандартный системный загрузчик.)

Главный класс приложения всегда загружается системным загрузчиком. А какие загрузчики будут использоваться для загрузки всех прочих классов?

В Java поддерживается понятие «текущего» загрузчика классов. Текущий загрузчик – это тот загрузчик классов (экземпляр некоторого наследника ClassLoader), который загрузил класс, код которого исполняется в данный момент. Каждый класс «помнит» загрузивший его загрузчик. Загрузчик, загрузивший некоторый класс, всегда можно узнать, вызвав метод getClassLoader:

public ClassLoader getClassLoader()

у объекта типа Class, соответствующего данному классу. Например, если мы находимся внутри некоторого метода класса MyClass, то вызов MyClass.class.getClassLoader() вернет ссылку на загрузчик, загрузивший этот класс, т.е. загрузивший тот самый байт-код, который выполняет вызов «MyClass.class.getClassLoader()».

Когда возникает необходимость загрузить другой класс вследствие обращения к его конструктору, статическому методу или полю, виртуальная Java-машина автоматически обращается к текущему загрузчику классов, о котором «помнит» текущий исполняемый класс. При этом другой класс также «запоминает» этот загрузчик в качестве текущего. Иначе говоря, текущий загрузчик, загрузивший данный класс, по умолчанию наследуется всеми классами, прямо или косвенно вызываемыми из данного.

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

На самом деле наследование текущего загрузчика – лишь поведение по умолчанию. Загрузчик классов можно написать и так, что он не будет наследоваться для некоторых классов. В тот момент, когда к загрузчику приходит запрос «выдать класс по заданному имени», он может передать этот запрос какому-нибудь другому загрузчику. Тогда данный класс и другие классы, вызываемые из него, будут загружаться новым загрузчиком. Например, специальный загрузчик, реализуемый броузером для загрузки классов апплетов, вполне может «передать свои полномочия» системному загрузчику, когда дело касается стандартного системного класса вроде java.lang.String.

Стандартный способ загрузить некоторый класс загрузчиком, отличным от текущего – специальная версия статического метода Class.forName:

public static Class forName(String name,boolean initialize,ClassLoader loader)

В качестве name передается полное имя класса (с указанием пакета), в качестве loader – требуемый загрузчик. Не столь очевидный (и не столь важный) параметр initialize управляет инициализацией класса, т.е. установкой значений всех static-полей класса и исполнением кода в секциях:

static {

…

}

Если initialize содержит true, то инициализация происходит немедленно, в противном случае – откладывается до первого обращения к любому конструктору, статическому методу или полю этого класса.

Более простая форма метода Class.forName, о которой шла речь в первой части статьи («Основы», журнал «Системный администратор» №1, октябрь 2002г.)

public static Class forName(String className)

всегда использует текущий загрузчик классов. На самом деле, вызов

Class.forName(name)

эквивалентен вызову

Class.forName(name,true,Текущий_класс.class.getClassLoader())

где Текущий_класс – имя класса, внутри которого содержится данный вызов.

Загрузив класс, можно создать его экземпляр или вызвать статический метод средствами отражений. (Техника работы с классом через отражения была подробно описана в первой части статьи.) Дальше этот класс может обычными средствами языка Java обращаться к другим классам – для них будет вызван тот же самый загрузчик loader (либо какие-то другие загрузчики, если реализация loader в какой-то момент «решит» передать управление другому загрузчику). Простейший пример:

Class clazz= Class.forName(“Имя_класса”,true,Мой_нестандартный_загрузчик);

clazz.newInstance(); // создаем экземпляр класса

Конструктор без параметров класса Имя_класса может стартовать поток (Thread), решающий некоторую сложную задачу. Все классы, необходимые этой задаче, будут загружены указанным загрузчиком.

Обзор класса ClassLoader

Перед тем как переходить к нашей основной задаче – реализации наследников класса ClassLoader – давайте посмотрим, какие методы предоставляет этот класс. Как обычно, мы рассмотрим только наиболее важные из них. Полный список можно найти в документации фирмы Sun.

Один из методов ClassLoader мы уже рассматривали. Это статический метод, возвращающий ссылку на стандартный системный загрузчик.

public static ClassLoader getSystemClassLoader()

Среди прочих методов самый «бросающийся в глаза»:

public Class loadClass(String name)

Этот метод загружает класс с заданным именем. На самом деле его реализация сводится к вызову другого protected-метода:

protected synchronized Class loadClass(String name,boolean resolve)

Как можно догадаться, переопределение этого protected-метода – один из основных способов реализовать собственный загрузчик классов.

Я не знаю, почему метод loadClass(String name) объявлен как public. Ведь уже существует стандартный способ загрузки класса по имени, с помощью произвольного загрузчика – вызов

Class.forName(“Имя_класса”,true,loader)

(Классы Class и ClassLoader расположены в общем пакете – так что метод loadClass(String name) вполне мог бы быть protected. Это не помешало бы методу Class.forName его вызвать.)

Может быть, раз уж loadClass – public-метод, то вместо Class.forName(«Имя_класса»,true,loader) можно пользоваться прямым обращением loader.loadClass(“Имя_класса”) ?

Судя по всему, следует все же всегда использовать вызов Class.forName. Хотя это совершенно неочевидно из документации. Несколько позже мы увидим, что метод Class.forName выполняет с классом некоторые дополнительные действия, в частности, кэширует его, обеспечивая, в отличие от прямого вызова loadClass, стабильную работу даже при недостаточно аккуратной реализации загрузчика loader.

Есть также группа методов, предназначенных для загрузки ресурсов:

public URL getResource(String name)

public InputStream getResourceAsStream(String name)

public final Enumeration getResources(String name)

public static URL getSystemResource(String name)

public static InputStream getSystemResourceAsStream(String name)

public static Enumeration getSystemResources(String name)

Это более полный аналог методов getResource и getResourceAsStream класса Class, рассмотренных в первой части статьи. На самом деле методы Class.getResource и Class.getResourceAsStream как раз обращаются к соответствующим методам текущего загрузчика, загрузившего данный класс. Главное отличие методов работы с ресурсами класса ClassLoader – абсолютные пути. Путь к ресурсу отсчитывается не от каталога, содержащего данный class-файл (как в случае Class.getResource и Class.getResourceAsStream), а от одного из каталогов, указанных в переменной CLASSPATH.

Обратите внимание: названия методов getSystemResource, getSystemResourceAsStream, getSystemResources вовсе не означают, что загружаются какие-то особые «системные» ресурсы. Слово «System» в этих названиях говорит о том, что для загрузки ресурсов будет в любом случае использоваться стандартный системный загрузчик.

В сущности, это практически все. Любая реализация ClassLoader должна обеспечивать работоспособность перечисленных методов.

Ставим задачу: перезагрузка классов «на лету»

Мы приступаем к самой интересной части – реализации своего наследника абстрактного класса ClassLoader.

Обычно в книгах по языку Java реализацию ClassLoader рассматривают на примере загрузки .class-файлов из какого-либо нестандартного источника, например, каталога, не указанного в переменной CLASSPATH. Такой пример достаточно прост, но, на мой взгляд, не очень интересен. В большинстве ситуаций поиск .class-файлов в путях, перечисленных в CLASSPATH, – вполне нормальное решение. Загрузка же из принципиально иных источников типа Internet вряд ли будет полезна вне контекста куда более сложной задачи, включающей такие вопросы, как политика безопасности или кэширование загруженных файлов на локальном диске.

Мы попробуем решить другую задачу.

Предположим, разрабатывается большая программа на Java. По каким-либо причинам эту программу нежелательно часто перезагружать: останавливать и запускать снова. Например, это может быть сложная серверная программа, каждую секунду обслуживающая многих клиентов, для которой даже сравнительно кратковременная неработоспособность является критичной. Или просто программа настолько «тяжелая», что полный ее перезапуск требует несколько минут, и часто перезапускать ее крайне неудобно.

В то же время программа постоянно развивается. Создаются новые независимые блоки, переписываются и отлаживаются старые. Возможно, программа «умеет» исполнять сторонние классы, разработанные пользователями (как делать такие вещи с помощью отражений, рассказывалось в первой части статьи). В таких условиях возникает естественное желание, чтобы подключение новых классов или новых версий старых классов не требовало полной остановки и перезапуска программы.

Что касается действительно новых классов – тут проблем нет. Вы вольны в любой момент скомпилировать новый класс с новым уникальным именем – даже когда программа уже запущена. Если ваша программа после этого выполнит вызов Class.forName(name) с этим именем (например, в результате автоматического сканирования каталогов поиска CLASSPATH в поисках новых .class-файлов), то этот класс будет успешно подключен, и программа сможет им пользоваться.

Что касается версий .class-файлов – тут все значительно хуже. Однажды обратившись к некоторому классу, стандартный системный загрузчик запомнит его в своем внутреннем кэше и будет всегда использовать именно его. Никакие последующие перекомпиляции .class-файла и даже физическое удаление этого файла не отразятся на работе этого класса. Насколько я знаю, никакими силами, кроме полного перезапуска программы (т.е. всей виртуальной машины Java), нельзя заставить системный загрузчик «забыть» тот байт-код класса, который он однажды загрузил.

Если вы когда-нибудь разрабатывали и отлаживали апплеты, возможно вы сталкивались с неприятной особенностью броузеров: не учитывать изменения в перекомпилированном классе апплета до тех пор, пока программа-броузер не будет закрыта и запущена заново. Судя по всему, происхождение этой проблемы кроется как раз в описанном поведении системного загрузчика (вместе с нежеланием разработчиков броузера рестартовать по кнопке «Refresh»/»Reload» виртуальную Java-машину).

На самом деле у такого поведения системного загрузчика есть веские причины. Это станет вполне очевидно позже, когда мы – как вы, вероятно, уже догадались – реализуем собственный загрузчик классов, лишенный описанного «недостатка». На самом деле возможность «на лету» менять реализацию классов, не перезапуская программу, может оказаться очень полезной в описанной выше ситуации. Но при этом, как мы увидим, в разработке Java-программ появляются некоторые экзотические нюансы, которые были бы совершенно неуместны в большинстве обычных программ – поэтому разработчики Java и заблокировали подобную возможность в стандартном системном загрузчике.

Итак, наша задача – написать загрузчик классов, аналогичный стандартному системному, который, в отличие от него, умел бы «забывать» загруженные ранее версии классов и загружать .class-файлы заново.

Заодно мы решим и более простую традиционную задачу – загрузку .class-файлов из собственного списка каталогов поиска. Это нам ничего не будет стоить. Какая разница – загружать файлы непременно из каталогов CLASSPATH или из каких-либо других. Мы будем учитывать, что некоторые (или все) каталоги CLASSPATH могут попасть в наш список.

Для простоты мы не будем поддерживать в этом загрузчике работу с JAR-архивами. Все-таки JAR предназначен для упаковки достаточно стабильных версий программных модулей, которые вряд ли стоит обновлять настолько часто, что ради этого нежелательно выполнять перезапуск основной Java-программы. В частности, загрузку стандартных библиотечных классов (пакеты java.lang и подобные), которые обычно находятся в JAR-файле, мы возложим на системный загрузчик.

Назовем наш новый загрузчик DynamicClass-Overloader – «динамический перезагрузчик классов».

Первые тесты и первые успехи

Итак, мы собираемся создать наследника абстрактного класса ClassLoader, который умел бы загружать классы из некоторого заданного набора каталогов поиска так же, как это делает системный загрузчик для каталогов, перечисленных в переменной CLASSPATH. В отличие от системного загрузчика наш вариант ClassLoader должен уметь «забывать» о загруженных ранее классах.

Мы должны реализовать собственные версии следующих методов ClassLoader:

protected synchronized Class loadClass(String name,boolean resolve)

throws ClassNotFoundException

protected Class findClass(String name)

throws ClassNotFoundException

protected java.net.URL findResource(String name)

protected java.util.Enumeration findResources(String name)

throws IOException

Абстрактный класс ClassLoader в действительности предоставляет реализацию для первого метода, loadClass, основанную на двух других protected-методах – findLoadedClass и findClass. (Метод findLoadedClass объявлен как final – его переопределять не нужно.) Эта реализация проверяет, не был ли загружен класс раньше (вызовом «findLoadedClass(name)»); если нет – делает попытку загрузить класс стандартным образом, и если эта попытка терпит неудачу – обращается к методу findClass.

Для решения более традиционной задачи – обеспечения загрузки классов из нестандартного источника – такая реализация вполне подходит. В этом случае было бы достаточно реализовать метод findClass. Но мы хотим загружать классы из вполне стандартного источника (хотя и нестандартным образом): из набора каталогов, который может соответствовать стандартной переменной CLASSPATH. Значит мы не должны первым делом вызывать стандартный загрузчик, полагаясь на то, что для наших имен классов он потерпит неудачу, и loadClass обратится к нашему методу findClass. Нам нужно реализовать свою версию loadClass, действующую наоборот: вначале пытающуюся загрузить .class-файл самостоятельно и лишь в случае неудачи (скажем, для классов из пакета java.lang, которые обычно упакованы в JAR-архив) обращающуюся к системному загрузчику.

Методы findResource и findResources, подобно findClass, обеспечивают работоспособность public-методов загрузки ресурсов getResource, getResourceAsStream и getResources. Для ресурсов задачу динамической перезагрузки без рестарта программы решать не нужно – они и так всегда загружаются динамически. Поэтому, в отличие от ситуации с findClass, нам вполне достаточно переопределить методы findResource и findResources. Сделать это необходимо, так как, возможно, мы будем использовать наш загрузчик с каталогами, неизвестными системному загрузчику, т.е. отличными от каталогов CLASSPATH.

Мы не будем отвлекаться на реализацию метода findResources. Его использование представляется чересчур экзотичным. Мы реализуем только findResource – это почти ничего не стоит, и на этом методе основаны все типичные приемы работы с ресурсами (через методы Class.getResource и Class.getResourceAsStream), описанные в первой части статьи.

Как нужно реализовывать эти методы?

Согласно документации loadClass должен просто найти или загрузить указанный класс с помощью findClass, после чего, если параметр resolve содержит true, вызвать для полученного класса protected-метод resolveClass. Что этот метод в точности делает – для нас в данный момент неважно.

Метод findClass должен загрузить байт-код указанного класса (в нашем случае это просто чтение файла), после чего выполнить для полученного массива байтов специальный «магический» метод defineClass:

protected final Class defineClass(String name,byte[] b, int off, int len)

throws ClassFormatError

Это как раз то самое место, где цепочка байтов – образ .class-файла (фрагмент массива b длины len по смещению off) – «чудесным образом» превращается в готовый к использованию класс. Метод defineClass, как и следовало ожидать, реализован в native-коде. Именно он помещает байт-код класса в недра виртуальной машины, где он приобретает вид, пригодный для непосредственного исполнения на конкретной аппаратной платформе, в частности, компилируется в машинный код процессора для более быстрого исполнения (так называемая технология Just-In-Time, сокращенно JIT-компиляция).

Наконец, метод findResource должен просто найти файл, соответствующий данному ресурсу – по тем же правилам, по которым отыскивается файл класса в методе findClass – и вернуть ссылку на него в виде URL.

Системный загрузчик классов не просто загружает файлы классов с диска, но еще и запоминает их во внутреннем кэше – так что последующие обращения к loadClass для того же имени класса просто выдают готовый объект Class из кэша. Кэширование, вообще говоря, представляется разумной идеей: зачем каждый раз заново обращаться к диску? Мы будем хранить кэш в нестатическом private-поле типа java.util.HashMap нашего класса DynamicClassOverloader. Таким образом, каждый новый экземпляр нашего загрузчика будет создаваться с новым кэшем, и для «забывания» загруженных ранее классов будет достаточно просто создать новый экземпляр загрузчика.

Итак, реализация: версия первая.

import java.io.*;

public class DynamicClassOverloader extends ClassLoader {

private java.util.Map classesHash= new java.util.HashMap();

public final String[] classPath;

public DynamicClassOverloader(String[] classPath) {

// Набор путей поиска – аналог переменной CLASSPATH

this.classPath= classPath;

}

protected synchronized Class loadClass(String name,boolean resolve)

throws ClassNotFoundException

{

Class result= findClass(name);

if (resolve) resolveClass(result);

return result;

}

protected Class findClass(String name)

throws ClassNotFoundException

{

Class result= (Class)classesHash.get(name);

if (result!=null) {

/*

System.out.println(“% Class “+name+” found in cache”);

/*

return result;

}

File f= findFile(name.replace(‘.’,'/’),”.class”);

// Класс mypackage.MyClass следует искать файле mypackage/MyClass.class

/*

System.out.println(“% Class “+name+(f==null?”":” found in “+f));

/*

if (f==null) {

return findSystemClass(name);

// Обращаемся к системному загрузчику в случае неудачи. findSystemClass – это метод абстрактного класса

// ClassLoader с объявлением protected final Class findSystemClass(String name) (т.е. предназначенный

// для использования в наследниках и не подлежащий переопределению). Он выполняет поиск и загрузку класса

// по алгоритму системного загрузчика. Без вызова findSystemClass(name) нам пришлось бы самостоятельно

// позаботиться о загрузке всех стандартных библиотечных классов типа java.lang.String, что потребовало бы

// реализовать работу с JAR-архивами (стандартные библиотеки почти всегда упакованы в JAR)

}

try {

byte[] classBytes= loadFileAsBytes(f);

result= defineClass(name,classBytes,0,classBytes.length);

} catch (IOException e) {

throw new ClassNotFoundException(“Cannot load class “+name+”: “+e);

} catch (ClassFormatError e) {

throw new ClassNotFoundException(“Format of class file incorrect for class “+name+”: “+e);

}

classesHash.put(name,result);

return result;

}

protected java.net.URL findResource(String name) {

File f= findFile(name,”");

if (f==null) return null;

try {

return f.toURL();

} catch(java.net.MalformedURLException e) {

return null;

}

}

private File findFile(String name, String extension) {

// Поиск файла с именем name и, возможно, расширением extension в каталогах поиска, заданных параметром

// конструктора classPath. Имена подкаталогов в name разделяются символом ‘/’ – даже если в операционной

// системе принят другой разделитель для подкаталогов. (Именно в таком виде получает свой параметр метод

// findResource.)

for (int k=0; k<classPath.length; k++) {

File f= new File((new File(classPath[k])).getPath()+File.separatorChar+name.replace(‘/’,File.separatorChar)+extension);

if (f.exists()) return f;

}

return null;

}

public static byte[] loadFileAsBytes(File file)

throws IOException

{

byte[] result= new byte[(int)file.length()];

FileInputStream f= new FileInputStream(file);

try {

f.read(result,0,result.length);

} finally {

try {

f.close();

} catch (Exception e) {

// Игнорируем исключения, возникшие при вызове close. Они крайне маловероятны и не очень

// важны – файл уже прочитан. Но если они все же возникнут, то они не должны замаскировать

// действительно важные ошибки, возникшие при вызове read.

};

}

return result;

}

}

Проверяем. Пишем тестовый класс TestModule.java, который собираемся загружать нашим загрузчиком:

public class TestModule {

public String toString() {

return “TestModule, version 1!”;

}

}

Пишем тест Test.java, который будет загружать этот класс:

import java.io.*;

public class Test {

public static void main(String[] argv) throws Exception {

for (;;) {

ClassLoader loader= new DynamicClassOverloader(new String[] {“.”});

// текущий каталог “.” будет единственным каталогом поиска

Class clazz= Class.forName(“TestModule”,true,loader);

Object object= clazz.newInstance();

System.out.println(object);

new BufferedReader(new InputStreamReader(System.in)).readLine();

}

}

}

Кладем все эти файлы в один каталог, компилируем и запускаем Test:

java Test

В каждой итерации бесконечного цикла создается экземпляр нашего загрузчика loader, с его помощью загружается класс TestModule, создается его экземпляр и распечатывается, при этом, как обычно, неявно используется метод toString(), реализованный в TestModule. Затем ожидается нажатие на клавиатуре клавиши ENTER (либо Ctrl-C для выхода из программы).

Пока наш тест ждет нажатия ENTER, перейдем в другое окно (ОС Windows) или консоль (ОС Unix), чуть-чуть исправим класс TestModule: изменим результат toString() на «TestModule, version 2!» и перекомпилируем его. После чего вернемся в наш тест и нажмем ENTER.

Ура! В следующей итерации цикла мы видим результат работы свежей версии класса TestModule.class – будет напечатана новая строка «TestModule, version 2!».

Мы добились успеха, не выходя из программы, модифицировали class-файл, и новая версия класса была успешно загружена.

Класс TestModule можно заменить на любой другой сложный класс, конструктор которого инициирует сколь угодно сложную цепочку действий. Все классы, которые в процессе этого будут задействованы, будут точно так же динамически перезагружаться.

Первые проблемы

Наш тест работает, но возникает законный вопрос – ну и что? Да, мы можем в определенный момент запустить некий класс, заданный своим именем, после чего он будет выполнять какие-то действия и, в конце концов, вернет строку «object.toString()». Но это в общем-то ничем принципиально не отличается от запуска новой java-программы стандартным вызовом

java Имя_класса

Вспомним постановку задачи. У нас есть большая, очень большая Java-программа, полный перезапуск которой занимает длительное время и крайне нежелателен. Мы хотим иметь возможность в какой-то момент быстро перезагрузить некоторые ФРАГМЕНТЫ программы, чтобы все классы, относящиеся к этим фрагментам, после этого момента заново считывались с диска. Вероятнее всего, эти классы должны активно взаимодействовать друг с другом и со стационарной, неперезагружаемой частью программы. Например, они могут реализовывать различные интерфейсы, которыми пользуется стационарная часть программы, их экземпляры могут сохраняться в различных переменных в стационарной части и т. д.

В терминах нашего загрузчика классов это означает, что мы должны уметь взаимодействовать с классами, загруженными вызовом

Class.forName(“Имя_класса”,true,loader)

Мы должны работать с их экземплярами, вызывать методы, обращаться к полям, перехватывать исключения, причем по возможности стандартными способами языка Java. Аналогично классы, загруженные разными экземплярами загрузчика, например, отвечающие за слабо связанные фрагменты большой программы, которые нужно перезагружать в разные моменты – должны уметь взаимодействовать друг с другом.

Казалось бы, с этим нет никаких проблем. В тесте мы получили вызовом newInstance() переменную типа Object. Но если мы знаем, что ее тип – TestModule, мы можем спокойно привести ее к этому типу и работать дальше обычным образом:

…

Class clazz= Class.forName(“TestModule”,true,loader);

TestModule testModule= (TestModule)clazz.newInstance();

работаем с полями testModule, вызываем методы и т.д.

Если бы здесь был обычный вызов Class.forName(«TestModule»), все было бы нормально. Это был бы простейший вариант классической схемы построения расширяемых систем. В качестве аргумента forName мог бы выступать любой наследник TestModule (или класс, реализующий интерфейс TestModule), реализация которого неизвестна и не обязательно доступна в момент компиляции системы. Об этом способе работы с отражениями рассказывалось в первой части статьи.

Но с нашим необычным загрузчиком классов нас ждет неприятная неожиданность. При попытке приведения типа будет возбуждено исключение ClassCastException – ошибка приведения типа!

Новые свойства языка – «динамические» и «истинно-статические» классы

Когда я впервые столкнулся с описанной ситуацией, то долго не мог понять, в чем тут дело. Дальнейшие тесты только усугубляли недоумение. Вроде бы все сделано правильно, при простейшем использовании работает, но постоянно обнаруживаются какие-то странности. В некоторых случаях, на вид совершенно невинных, вообще возникало низкоуровневое исключение LinkageError.

На самом деле мы только что столкнулись с проявлением достаточно общей проблемы. Чтобы понять ее природу, нужно заново внимательно рассмотреть понятие класса в языке Java.

В мире объектно-ориентированного программирования, в частности в Java, мы привыкли к тому, что существует два уровня иерархии сущностей – класс и экземпляр. Класс с заданным именем в системе всегда один – он однозначно идентифицируется своим полным именем. Экземпляров же у класса может быть много. Так, в Java поля с квалификатором static принадлежат целому классу, каждое такое поле существует в системе в единственном экземпляре. В отличие от этого для обычного (нестатического) поля отдельная его версия присутствует в каждом экземпляре класса.

Создав наш загрузчик DynamicClassOverloader, всегда загружающий свежие версии class-файлов, мы принципиально изменили ситуацию. Теперь есть три уровня иерархии: сам класс, версия класса – тот байт-код, который был загружен конкретной версией DynamicClassOverloader (возможно, меняющийся в процессе работы программы) и экземпляры конкретной версии класса.

На самом деле виртуальная машина Java «считает» классом как раз то, что для нас является версией. Хотя это и неочевидно из документации, но класс в Java однозначно идентифицируется не только полным именем, но еще и экземпляром загрузчика, загрузившего этот класс. Каждый экземпляр загрузчика классов порождает собственное пространство имен, внутри которого классы действительно однозначно идентифицируются полными именами, но классы в разных пространствах имен, загруженные разными загрузчиками, вполне могут иметь идентичные имена.

На самом деле, с точки зрения Java, класс TestModule, возникающий в результате прямого вызова «TestModule testModule= …» в тексте файла Test.java, и класс TestModule, полученный вызовом:

Class.forName(“TestModule”,true,loader)

– это два совершенно разных класса. Первый был загружен системным загрузчиком (вместе с самым главным классом Test), а второй – одним из экземпляров нашего загрузчика DynamicClassOverloader. По классическим законам объектно-ориентированного программирования приведение типов между ними невозможно.

Более того, каждая итерация нашего цикла, оказывается, порождала новую версию класса TestModule, не связанную с предыдущими. В этом можно убедиться следующим образом. Модифицируем класс TestModule:

public class TestModule {

private static int counter= 0;

public String toString() {

return “TestModule, version 1! “+(counter++);

}

}

В нормальных условиях каждое новое обращение к методу toString() такого класса привело бы к получению строки с новым, увеличенным на 1 значением счетчика counter. Если бы в тесте Test.java был обычный вызов Class.forName(«TestModule»), мы бы это и увидели: на каждой итерации цикла распечатывались бы разные значения счетчика. А с нашим загрузчиком мы все время будем видеть нулевое значение. Каждое пересоздание экземпляра загрузчика DynamicClassOverloader приводит к появлению нового пространства имен, в котором инициализируется совершенно новая версия класса TestModule, ничего не знающая о предыдущих версиях и о содержащихся в них статических переменных.

Фактически, мы придали языку Java новое свойство – «динамичность» классов. Теперь, обращаясь прямо или косвенно к любому классу, придется думать о том, какая именно версия этого класса будет использована и можно ли ее использовать совместно с тем классом, который к ней обращается.

Свойство, что и говорить, крайне неудобное. Как же теперь работать с «динамическими» классами? Ведь все версии классов, которыми пользуется наш первый класс, загруженный DynamicClassOverloader, и которые тем самым тоже загружены нашим загрузчиком, – все эти версии неизвестны в пространстве имен стационарной части – в нашем случае в главном классе Test.

Решение этой проблемы – заблокировать свойство «динамичности» для некоторых классов, т.е. потребовать, чтобы такие классы наш загрузчик загружал стандартным способом – через вызов «findSystemClass(name)». Назовем такие классы «истинно-статическими» – «true-static». Такие «true-static»-классы можно будет свободно совместно использовать в стационарной части программы и всех версиях «динамических» классов. Для «true-static»-классов всегда будет существовать только одна версия, как и предполагается в обычном языке Java, и не будет проблем несоответствия типа. Можно, например, сделать «true-static» все ключевые интерфейсы, основные типы данных, которые должны получать и возвращать «динамические» классы, базовые типы исключений, подлежащие перехвату и единообразной обработке, и т. п.

В сущности, уже в реализованной нами версии загрузчика существовали «true-static»-классы – это библиотечные классы из пакетов типа java.lang, которые мы не пытались грузить самостоятельно. Скажем, такими были стандартные типы Object и String. Именно поэтому в первоначальном варианте теста мы смогли получить от созданного экземпляра динамического класса TestModule строку String – результат метода toString().

Можно придумать много соглашений, по которым загрузчик должен опознавать «true-static»-классы. Например, можно проверить существование некоторого ключевого static-поля или проверить, не реализован ли в классе некоторый специальный пустой интерфейс[2]. Мы ограничимся наиболее простым (хотя и не всегда удобным) вариантом: будем проверять, не содержит ли имя класса name цепочки символов «truestatic» без учета регистра символов.

Итак, начинаем модифицировать наш загрузчик DynamicClassOverloader: добавляем в методе findClass сразу перед вызовом:

File f= findFile(name.replace(‘.’,'/’),”.class”);

дополнительную проверку имени name. Вот начало исходного текста нового метода findClass:

protected Class findClass(String name)

throws ClassNotFoundException

{

Class result= (Class)classesHash.get(name);

if (result!=null) {

/*

System.out.println(“% Class “+name+” found in cache”);

/*

return result;

}

if (name.toLowerCase().indexOf(“truestatic”)!=-1)

return findSystemClass(name);

File f= findFile(name.replace(‘.’,'/’),”.class”);

…

Попробуем этим воспользоваться. Создаем «true-static»-класс TrueStaticModule.java:

public class TrueStaticModule {

protected static int counter= 0;

public int getCounter() {

return counter;

}

}

В нем есть public-метод getCounter(), которым мы собираемся пользоваться в стационарной части программы.

Наследуем от него «динамический» класс DynamicModule.java:

public class DynamicModule extends TrueStaticModule {

public String toString() {

return “DynamicModule, version 1! “+(counter++);

}

}

Наконец, переписываем тест Test.java – «стационарную часть» программы:

import java.io.*;

public class Test {

public static void main(String[] argv) throws Exception {

for (;;) {

ClassLoader loader= new DynamicClassOverloader(new String[] {“.”});

// текущий каталог “.” будет единственным каталогом поиска

Class clazz= Class.forName(“DynamicModule”,true,loader);

TrueStaticModule trueStaticModule=(TrueStaticModule) clazz.newInstance();

System.out.println(trueStaticModule.getCounter());

System.out.println(trueStaticModule);

new BufferedReader(new InputStreamReader (System.in)).readLine();

}

}

}

Компилируем все эти файлы и запускаем:

java Test

Все работает нормально. Как и раньше мы можем прямо в процессе работы теста модифицировать и перекомпилировать класс DynamicModule, и это изменение будет учтено. Но для «общения» со стационарной частью программы и для хранения счетчика обращений к методу toString() теперь используется «true-static»-класс TrueStaticModule. Поэтому мы не получаем исключения ClassCastException, а счетчик counter корректно увеличивается на протяжении всей работы теста.

Поставленную задачу можно считать в принципе решенной.

Чтобы использование нашего загрузчика стало действительно удобным, стоило бы еще реализовать специальный сервисный «true-static»-класс с методом forName, аналогичным стандартному forName. Только в отличие от стандартного, наш forName обращался бы к нашему загрузчику, экземпляр которого, внутреннее private-поле, создавался бы при первом обращении к forName. Параметры конструктора для нашего загрузчика можно было бы настраивать с помощью специальных полей сервисного класса. Кроме того, в нашем сервисном классе был бы специальный метод invalidate, обнуляющий private-поле с нашим загрузчиком и вынуждающий метод forName при следующем вызове заново создать загрузчик. Метод invalidate можно было бы вызывать в Java-программе всякий раз, когда требуется перезагрузить с диска новые версии всех «динамических» классов. Написание подобного сервисного класса – достаточно понятная задача, и мы не будем на ней останавливаться.

Правила работы с «динамическими» классами

При практическом использовании описанного выше загрузчика классов программирование в языке Java заметно усложняется. Нужно помнить о возможной «динамичности» классов: каждый экземпляр загрузчика порождает отдельную версию каждого такого класса в собственном пространстве имен. Нужно заботиться о том, чтобы некоторые классы были «true-static». Все это требует четкого понимания описанных выше механизмов и достаточной аккуратности.

Я сформулирую ниже несколько правил, которыми следует руководствоваться при программировании в таком, изменившем свое поведение, языке Java.

Будем называть динамической частью Java-программы ту систему «динамических» классов, которая загружается некоторым экземпляром нашего загрузчика DynamicClassOverloader, и стационарной частью – основную систему классов, которая загружает динамическую часть, используя экземпляры DynamicClassOverloader. В программе может быть и несколько динамических частей, никак не связанных друг с другом, одновременно загруженных несколькими экземплярами DynamicClass-Overloader. В стационарную часть входят, в частности, все «true-static»-классы.

ПРАВИЛО A. Стационарную часть программы – в частности, все «true-static»-классы – следует разрабатывать таким образом, чтобы никак не использовать информацию о структуре «динамических» классов: имена классов, имена их членов, типы параметров у методов. Иными словами, нельзя прямо ссылаться на конкретные имена «динамических» классов и обращаться к ним средствами языка Java. Единственным способом взаимодействия стационарной части программы с «динамическими» классами должна быть их загрузка вызовом

Class.forName(“TestModule”,true,loader)

и использование системы «true-static»-классов (или интерфейсов), известных и стационарной, и динамической частям. Например, можно обращаться (через приведение типа) к «true-static»-интерфейсам, которые реализуют «динамические» классы, получать результаты методов этих интерфейсов в виде «true-static»-классов, перехватывать «true-static»-исключения и т. д.

Сформулированное правило вполне логично и «выдержано в духе» объектно-ориентированного программирования. «Динамические» классы для того и сделаны «динамическими», чтобы их можно было разрабатывать и компилировать уже после того, как стационарная часть программы скомпилирована и запущена. Поэтому стационарная часть и не должна ничего «знать» об этих классах кроме того, что они, возможно, реализуют какие-то заранее известные «true-static»-интерфейсы или, скажем, как-то работают со static-полями заранее известных «true-static»-классов.

В первом нашем тесте, вызвавшем ошибку приведения типа, мы нарушили это правило. При приведении типа мы непосредственно сослались из стационарной части программы на имя «динамического» класса TestModule. В правильном решении, которое мы привели позже, мы преобразовывали полученный объект неизвестного нам «динамического» типа к типу «true-static»-класса TrueStaticModule – предка «динамического» класса.

Заметим, сформулированное правило не является категорическим. Его вполне можно нарушать, т.е. прямо ссылаться из стационарной части на «динамические» классы. Просто следует помнить, что классы, которые будут при этом задействованы, и классы с теми же именами, к которым будет обращаться динамическая часть программы, – это совершенно разные классы, лежащие в разных пространствах имен.

Например, и стационарная, и динамическая части программы могут активно пользоваться некоторыми сервисными библиотеками, и эти библиотеки нет никакой необходимости делать «true-static». Они вполне могут быть «динамическими». Если мы изменим реализацию некоторого метода в таком классе-библиотеке и перекомпилируем этот класс, то стационарная часть этого «не заметит» и будет продолжать работать со своей старой версией библиотеки, а динамическая часть, после очередного пересоздания экземпляра загрузчика, воспользуется новой версией.

Из общего правила A можно выделить несколько более простых частных правил.

ПРАВИЛО A1. Не следует ссылаться из стационарной части программы (в частности, «true-static»-класса) на какие-либо поля, конструкторы или методы «динамического» класса. Точнее, следует иметь в виду, что такая ссылка означает обращение к версии «динамического» класса, загруженной системным загрузчиком по умолчанию, а никак не к версии, загруженной нашим загрузчиком DynamicClassOverloader.

ПРАВИЛО A2. Все аргументы и результат любого метода в «true-static»-классе, используемого для связи между стационарной и динамической частями программы, например, метода «true-static»-интерфейса, который реализуют конкретные «динамические» классы, – должны иметь либо примитивный тип, либо тип «true-static»-класс. То же самое относится к public-полям, используемым с аналогичными целями. (К «true-static»-классам мы относим также все стандартные системные классы, вроде java.lang.String или java.io.File, которые наш загрузчик не пытается грузить самостоятельно.)

ПРАВИЛО A3. Если «динамические» классы возбуждают какие-либо исключения, которые, возможно, потребуется перехватить оператором

catch (Тип_исключения e)

в стационарной части программы, то перехватываемый класс Тип_исключения должен быть «true-static».

Следующее общее правило:

ПРАВИЛО B. Нужно учитывать, что каждый экземпляр загрузчика порождает независимое пространство имен. Любая ссылка на «динамический» класс: на его конструкторы, методы, статические или обычные поля действует только в пределах текущего пространства имен и не относится к версиям того же класса, загруженным другими экземплярами загрузчика.

Вот частные следствия из этого правила:

ПРАВИЛО B1. Не следует думать, что статические поля «динамического» класса существуют в системе в единственном экземпляре. В каждом пространстве имен существует отдельная версия класса со своим набором статических полей.

Например, есть такая практика управления поведением Java-класса. Объявляется статическое public-поле, скажем,

public static boolean debugMode= false;

влияющее на работу некоторых методов класса. Обычно используется значение этого поля по умолчанию. Но при желании главный класс Java-приложения на этапе общей инициализации системы и загрузки конфигурационных файлов может записать в это поле другое значение.

С «динамическими» классами такой прием «не проходит». Главный класс в стационарной части может повлиять на значение static-поля только для одной версии «динамического» класса, загруженной системным загрузчиком. Все последующие версии, загруженные экземплярами DynamicClassOverloader, получат умолчательное, заново инициализированное значение этого static-поля.

Если «динамический» класс действительно нуждается в наборе истинно глобальных полей, разделяемых всеми своими версиями, то самое естественное решение – определить внутри этого класса локальный «true-static»-класс. Например:

public class Мой_динамический_класс {

public static class TrueStaticSection {

public static boolean debugMode= false;

другие глобальные переменные

}

…

}

ПРАВИЛО B2. Если «динамический» класс нуждается в доступе к некоторым полям, методам, конструкторам или локальным классам некоторого «true-static»-класса, то все эти члены «true-static»-класса обязаны быть public, даже если «динамический» и «true-static»-класс лежат в одном пакете или внутри одного java-файла. Исключение: если «динамический» класс наследует «true-static», то он, как обычно, имеет доступ к protected-членам «true-static»-класса.

Например, если в «динамический» класс вложен локальный «true-static»-класс, то «динамический» класс, вопреки обычной практике, не может пользоваться private-полями или полями с дружественным доступом вложенного класса.

Дело в том, что с точки зрения виртуальной машины Java-классы, загруженные разными загрузчиками и поэтому лежащие в разных пространствах имен, всегда имеют друг с другом столь же слабую связь, как и классы, лежащие в разных пакетах. «Дружественный» доступ или доступ к private-членам вложенного класса между разными пространствами имен «не работает».

Будьте внимательны: подобная ошибка (разумеется) не отслеживается компилятором Java и обнаруживается в виде системного исключения уже при выполнении программы.

Есть еще одно специфическое правило, не вытекающее из описанных выше общих принципов.

ПРАВИЛО C. Если некоторые методы класса являются native – эти методы реализованы в отдельном машинно-зависимом модуле, загружаемом методом System.loadLibrary в секции статической инициализации класса, то такой класс обязан быть «true-static».

Это – внутреннее свойство современной версии Java-машины, по крайней мере, для платформы Windows. Java-машина не допускает повторной загрузки внешнего модуля вызовом loadLibrary с тем же самым именем – такая попытка вызывает исключение. Для «динамических» классов инициализация происходит многократно в каждой версии класса. Если в «динамическом» классе попытаться в соответствии с документацией обратиться к System.loadLibrary в секции статической инициализации:

static {

System.loadLibrary(“Имя_внешнего_модуля”);

}

то уже вторая загрузка такого класса нашим загрузчиком возбудит внутреннее исключение с сообщением: «данный внешний модуль уже загружен другим загрузчиком классов».

Не обесценивают ли описанные сложности нового удобства – возможности «на лету» перекомпилировать и перезагружать классы? Я считаю, что нет – при условии грамотного проектирования системы в целом. На самом деле, описанные выше правила касаются только разработки сравнительно небольшого блока: стандартизованного интерфейса между стационарной и динамической частью. При разработке остальных блоков стационарной части, не работающих с «динамическими» классами, все описанные нюансы несущественны: стационарная часть загружается системным загрузчиком по обычным правилам. При разработке динамических частей программы, расширяющих ее функциональность, возможно, сторонними разработчиками, все эти правила, за исключением очень простого правила C, обычно также можно спокойно игнорировать. Все правила, за исключением C, относятся к доступу к «динамическим» классам из стационарной части и к работе с различными пространствами имен, но никак не затрагивают разработку системы «динамических» классов в пределах одного пространства имен. Поэтому все новые классы, разрабатываемые для расширения динамической части программы при уже выработанном протоколе общения со стационарной частью, почти всегда можно объявлять «динамическими» и спокойно разрабатывать по обычным правилам языка Java.

Странности кэширования: различие loadClass и forName

Здесь мне хотелось бы ненадолго вернуться назад к нашей реализации DynamicClassOverloader. В приведенной выше реализации есть две закомментированные строки с вызовом System.out.println, позволяющим увидеть момент загрузки класса и извлечение его из кэша.

Если их раскомментировать, можно увидеть удивительную вещь. Когда я воспользовался таким загрузчиком для исполнения большого проекта на Java, включающим тысячи различных классов и реализующим интенсивные вычисления, я обнаружил, что проверка наличия класса в кэше

Class result= (Class)classesHash.get(name);

if (result!=null) {

…

не срабатывает никогда!

Я пробовал повторно обращаться к загруженным классам всеми возможными способами: прямым обращением к классу, через Class.forName с различными наборами параметров, путем наследования, перехвата исключений и т. д. Во всех случаях, кроме прямого обращения к методу нашего загрузчика loadClass, до исполнения наших методов loadClass и findClass дело просто не доходило. Очевидно, Java-машина реализует дополнительное кэширование загруженных классов. Пока мы явно не подаем указание использовать другой экземпляр загрузчика с помощью соответствующего аргумента метода Class.forName. Все классы, уже загруженные один раз нашим загрузчиком, автоматически извлекаются из какого-то внутреннего кэша. Фактически наше кэширование, реализованное в DynamicClassOverloader, оказалось ненужным – классы и так прекрасно кэшируются.

Я не знаю, насколько можно полагаться на эту особенность Java-машины. Документация к классу ClassLoader рекомендует использовать вызов protected-метода findLoadedClass для выяснения, был ли уже загружен данный класс. Но в моих тестах мне не удавалось этим воспользоваться – этот метод всегда возвращал null.

Я все же считаю реализацию собственного кэша «на всякий случай» целесообразным, тем более что это требует всего нескольких строк кода. Кроме того, собственный кэш необходим, если при использовании нашего загрузчика классов предполагается пользоваться вместо классического вызова

Class.forName(“Имя_класса”,true,loader)

альтернативным вариантом:

loader.loadClass(“Имя_класса”)

Здесь метод loadClass вызывается напрямую – и здесь уже никто кроме нас не позаботится о кэшировании однажды загруженных классов. (В отличие от этого вызов Class.forName всегда обращается к внутреннему кэшу, и, если для данного загрузчика loader класс name уже однажды загружался, он будет найден и возвращен в результате forName без обращения к loadClass и findClass.)

Хуже того, если мы не будем реализовывать кэширование в загрузчике, т.е. будем в любом случае читать класс с диска и вызывать для него метод defineClass. В этом случае два последовательных вызова

loader.loadClass(“Имя_класса”)

loader.loadClass(“Имя_класса”)

для одинакового класса приведут к низкоуровневому исключению LinkageError! Виртуальная машина не разрешает в рамках одного и того же экземпляра загрузчика дважды определять один и тот же класс, т.е. обращаться к методу defineClass. С вызовом

Class.forName(“Имя_класса”,true,loader)

такую проблему пронаблюдать не удастся, даже если мы не будем реализовывать кэш.

Описанное поведение, как мне кажется, является веским аргументом, чтобы по возможности никогда, за исключением тестов, не использовать прямой вызов

loader.loadClass(“Имя_класса”)

отдавая предпочтение вызову

Class.forName(“Имя_класса”,true,loader)

или

Class.forName(“Имя_класса”,false,loader)

У внимательного читателя при изучении нашего загрузчика DynamicClassOverloader мог возникнуть вопрос. Если наша цель – научить загрузчик «забывать» старые версии классов, почему мы попросту не реализовали в классе DynamicClassOverloader метод invalidate:

public void invalidate() {

classesHash.clear();

}

очищающий наш кэш classesHash? Почему вместо этого мы пошли по более сложному пути: созданию новых экземпляров загрузчика?

Теперь мы можем ответить на этот вопрос. При использовании вызова

Class.forName(“Имя_класса”,true,loader)

метод invalidate, очищающий classesHash, просто не дал бы никакого эффекта. Виртуальная машина все равно извлекала бы класс из внутреннего кэша, пока мы не сменили бы экземпляр loader используемого загрузчика. А при использовании прямого вызова

loader.loadClass(“Имя_класса”)

метод invalidate, вместо ожидаемого «забывания» старых классов, привел бы к низкоуровневому исключению LinkageError.

Применения DynamicClassOverloader

Приведем несколько примеров ситуаций, где можно применить созданный нами загрузчик DynamicClassOverloader помимо решения поставленной нами главной задачи – загрузки измененных версий классов «на лету» без остановки основной программы.

Прежде всего напрашивается простейшее применение: профайлинг загрузки классов. С помощью параметра «-verbose:class» стандартной утилиты «java» можно проследить, какие классы загружаются в процессе работы. Но собственный загрузчик может сделать намного больше. Очень легко дополнить приведенный выше исходный код DynamicClassOverloader сбором любой статистики о загружаемых файлах: сколько классов различных пакетов загружается, сколько времени расходуется на загрузку классов, какие классы загружаются обычно в первую очередь и т. д.

DynamicClassOverloader можно использовать для загрузки классов из нестандартного каталога, не указанного в переменной CLASSPATH. Это требуется не так уж часто, но в некоторых ситуациях без этого сложно обойтись. Например, предположим, что вы пишете систему обработки документов, к которым могут прилагаться специальные Java-классы: как апплеты в веб-страницах или скрипты в документах Microsoft Word. Пользователь может работать с любым числом таких документов, расположенных где угодно в пределах своей файловой системы. Очевидно, для загрузки и исполнения таких классов, сопутствующих документу, стандартный набор путей из CLASSPATH окажется недостаточным.

Наконец, отдельные пространства имен, порождаемые экземплярами нашего загрузчика, из неудобного недостатка могут превратиться в ключевое достоинство. Обычно разработчики Java-классов избегают конфликтов имен своих классов благодаря стандартной системе именования пакетов, когда в имя пакета включается серия вложенных доменов Internet, соответствующих уникальному веб-сайту разработчика. Но если Java применяется в упрощенном виде – скажем, для реализации небольших скриптов конечными пользователями продукта – такая схема именования может оказаться чересчур обременительной. (Те же апплеты редко размещают в пакете. Часто пакет вообще не указывается, т.е. используется корневой package.) Наш загрузчик позволяет надежно изолировать друг от друга подобные простые классы, не нуждающиеся во взаимодействии друг с другом и разработанные, возможно, разными разработчиками. Если два класса загружены разными экземплярами DynamicClassOverloader, они могут спокойно иметь идентичное имя. Они «живут» в разных пространствах имен и не могут ничего «знать» друг о друге.

Заключение

Реализовав собственный загрузчик классов, мы действительно всерьез познакомились с миром отражений Java и много узнали о «внутренней кухне» виртуальной Java-машины. Конечно, это далеко не все.

Я не пытался написать исчерпывающее руководство. Я постарался предложить краткую экскурсию, позволяющую познакомиться с основными понятиями, технологиями и проблемами мира отражений Java и, я надеюсь, позволяющую почувствовать изящество и мощь этого чудесного языка. С помощью всего лишь около сотни строк исходного текста Java мы создали инструмент, позволивший существенно «подправить» поведение Java-машины и изменивший свойства самого языка Java.

В следующих номерах журнала я расскажу, как внутри вашей программы компилировать исходный Java-код в .class-файлы. Вместе с собственным загрузчиком классов, описанным в этой статье, это сможет послужить основой для создания систем, позволяющих конечному пользователю программировать на Java вплоть до создания настоящих систем Java-разработки.


[1] Это утверждение не совсем точное. На самом деле, вначале загружается целый ряд системных классов, в частности, системный загрузчик и все используемые ими классы. Процесс загрузки классов легко проследить, запустив утилиту «java» с ключом «-verbose:class».

[2] Что касается проверки интерфейса – это традиционное решение, но в данной ситуации реализовать его труднее чем обычно. Не так просто, построив класс – объект Class – вызовом

result= defineClass(name,classBytes,0,classBytes.length)

выяснить, не является ли он наследником данного интерфейса, скажем, TrueStatic. Простая проверка вроде

TrueStatic.class.isAssignableFrom(result)

не сработает – ведь интерфейс TrueStatic и класс result, скорее всего, загружены разными загрузчиками и лежат в разных пространствах имен. Одно из допустимых решений – получить список интерфейсов, реализуемых result, вызовом «result.getInterfaces()» и затем проверить их имена.

http://www.samag.ru/cgi-bin/go.pl?q=articles;n=01.2003;a=15


July 27th, 2010  
Tags: Java, Java Reflection, java.lang.ClassLoader



Previous Entries
  • Categories

    • Development
      • C++
      • Databases
      • Java
      • PHP
    • GZM Web
    • Help
      • Web Server
    • iPhone
    • Linux
    • Netowrking
      • DNS
    • News
    • Security
    • Uncategorized
  • Downloads

    • Clover WorkSpace
    • Gefest Web Server
  • Archives

    • February 2011
    • October 2010
    • August 2010
    • July 2010
    • June 2010
    • May 2010
    • April 2010
    • March 2010
    • February 2010
  • Recent Posts

    • Тестовый почтовый сервер для php
    • Реализация Flash на языке JavaScript и Web-сервис для преобразования FLV в Ogg
    • Организация вещания потокового Flash-видео средствами ffserver и ffmpeg
    • Создаем свой YouTube – Как обработать видео средствами веб-сервера (ffmpeg video stream flv mpeg 3gp)
    • Организация видеотрансляции на сайте
    • Приклад використання Callable для повернання результата
    • Настройка кластера MySQL
    • Некоторые недокументированные функции Java
    • Java: магия отражений
    • Java: Магия отражений

Support This Project




Copyright © GZMweb.com All rights reserved. Privacy policy | Terms of use
XHTML CSS Log in