Содержание


Пять секретов... повседневных Java-инструментов

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

Comments

Много лет назад, когда я учился в школе и мечтал о карьере писателя, я выписывал журнал Writer's Digest. Помню, там была статья о «строчках, которые слишком малы, чтобы их хранить». Автор сравнивал их с наполняющими кухонный ящик принадлежностями, которые оказались там просто потому, что их больше некуда девать. Мне запомнилась эта метафора, и она кажется точным описанием темы моей последней колонки этого цикла (по крайней мере, на сегодняшний день).

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

1. StAX

Развить навыки по этой теме

Этот материал — часть knowledge path для развития ваших навыков. Смотри Стать Java-программистом

Когда XML впервые появился на радаре большинства Java-разработчиков, еще на рубеже нового тысячелетия, существовало два основных подхода к синтаксическому анализу XML-файлов. SAX-парсер, который, по существу, представляет собой гигантский конечный автомат, палящий по разработчику сериями методов обратного вызова. И DOM-парсер, который забирает весь XML-документ в память и шинкует его на серии дискретных объектов, связанные друг с другом в форме дерева. Это дерево описывает все представление документа XML Infoset. Оба парсера имеют свои недостатки: SAX слишком низкоуровневый для практического использования, а DOM ― слишком расточительный, особенно для больших файлов XML — дерево занимает слишком много места.

К счастью, Java-разработчики нашли третий способ синтаксического разбора XML-файлов ― путем моделирования документа в виде серии «узлов», которые можно вытаскивать из потока документа один за другим, рассматривать и либо обрабатывать, либо отбрасывать. Этот поток узлов предложил золотую середину между SAX и DOM и получил название Streaming API for XML, или StAX (чтобы отличать новый API от оригинального SAX-парсера). Позже синтаксический анализатор StAX вошел в JDK и находится в пакете javax.xml.stream.

Работать с парсером StAX довольно просто: создайте экземпляр XMLEventReader, направьте его на правильно сформированный XML-файл, а затем «вытягивайте» и рассматривайте узлы по одному (обычно это делается в цикле while). Например, можно перечислить все цели в сценарии сборки Ant, как показано в листинге 1.

Листинг 1. Простая наводка StAX на цель
import java.io.*;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
import javax.xml.stream.events.*;
import javax.xml.stream.util.*;

public class Targets
{
    public static void main(String[] args)
        throws Exception
    {
        for (String arg : args)
        {
            XMLEventReader xsr = 
                XMLInputFactory.newInstance()
                    .createXMLEventReader(new FileReader(arg));
            while (xsr.hasNext())
            {
                XMLEvent evt = xsr.nextEvent();
                switch (evt.getEventType())
                {
                    case XMLEvent.START_ELEMENT:
                    {
                        StartElement se = evt.asStartElement();
                        if (se.getName().getLocalPart().equals("target"))
                        {
                            Attribute targetName = 
                                se.getAttributeByName(new QName("name"));
                            // Цель найдена!
                            System.out.println(targetName.getValue());
                        }
                        break;
                    }
                    // Игнорировать все остальное
                }
            }
        }
    }
}

Синтаксический анализатор StAX не заменяет всего кода SAX и DOM в мире, но определенно может упростить некоторые задачи. Он особенно удобен для решения задач, где вся структура дерева XML-документа не нужна.

Обратите внимание, что на тот случай, если объекты событий все же кажутся слишком высокоуровневыми, чтобы работать с ними, у StAX в классе XMLStreamReader есть низкоуровневый API. И есть класс XMLEventWriter и соответствующий ему XMLStreamWriter для вывода XML, хотя они, возможно, не так полезны, как считыватели.

2. ServiceLoader

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

Класс ServiceLoader из java.util может прочесть файл конфигурации, спрятанный в JAR-файле, и найти реализации интерфейса, а затем превратить их в список объектов, из которого можно выбирать. Например, с помощью кода, приведенного в листинге 2, можно создать компонент, который послужит личным секретарем для решения ваших задач.

Листинг 2. IPersonalServant
public interface IPersonalServant
{
    // Обработка файла с командами для секретаря
    public void process(java.io.File f)
        throws java.io.IOException;
    public boolean can(String command);
}

Метод can() позволяет решить, отвечает ли реализация личного секретаря вашим требованиям. ServiceLoader в листинге 3, по существу, представляет собой список реализаций IPersonalServant, которые отвечают требованиям.

Листинг 3. Подходит ли этот IPersonalServant?
import java.io.*;
import java.util.*;

public class Servant
{
    public static void main(String[] args)
        throws IOException
    {
        ServiceLoader<IPersonalServant> servantLoader = 
            ServiceLoader.load(IPersonalServant.class);

        IPersonalServant i = null;
        for (IPersonalServant ii : servantLoader)
            if (ii.can("fetch tea"))
                i = ii;

        if (i == null)
            throw new IllegalArgumentException("No suitable servant found");

        for (String arg : args)
        {
            i.process(new File(arg));
        }
    }
}

Предполагая, что реализация этого интерфейса существует, получаем листинг 4.

Листинг 4. Jeeves реализует IPersonalServant
import java.io.*;

public class Jeeves
    implements IPersonalServant
{
    public void process(File f)
    {
        System.out.println("Very good, sir.");
    }
    public boolean can(String cmd)
    {
        if (cmd.equals("fetch tea"))
            return true;
        else
            return false;
    }
}

Осталось только сделать JAR-файл, содержащий эти реализации, узнаваемым для ServiceLoader— что может оказаться не так просто. JDK требует, чтобы в JAR-файле был каталог META-INF/services с текстовым файлом, имя которого соответствует полному имени класса интерфейса — в данном случае это META-INF/services/IPersonalServant. Это имя класса интерфейса содержит имена реализаций, по одному в строке, как показано в листинге 5.

Листинг 5. META-INF/services/IPersonalServant
Jeeves   # comments are OK

К счастью, система сборки Ant (начиная с версии 1.7.0) включает в себя служебную метку для задачи jar, которая делает это сравнительно безболезненным, как показано в листинге 6.

Листинг 6. Построение IPersonalServant в Ant
    <target name="serviceloader" depends="build">
        <jar destfile="misc.jar" basedir="./classes">
            <service type="IPersonalServant">
                <provider classname="Jeeves" />
            </service>
        </jar>
    </target>

Теперь достаточно вызвать IPersonalServant и попросить его выполнить команды. Однако синтаксический анализ и выполнение этих команд могут оказаться затруднительными. Это подводит меня к следующей «строчке».

3. Scanner

Многочисленные утилиты Java помогают в построении синтаксического анализатора, а функциональные языки добились некоторых успехов в построении библиотеки функций синтаксического анализатора (комбинаторов). Но что делать, если нужно разобрать всего лишь файл со значениями, разделенными запятыми, или серию текстовых файлов с пробелами в качестве разделителей? Для большинства утилит это слишком мелкая цель, а для String.split() ― слишком крупная. (Что касается регулярных выражений, то помните, что гласит старая поговорка: «У вас была проблема, и вы попытались решить ее с помощью регулярных выражений? Теперь у вас две проблемы».)

В таких случаях лучшим выбором может оказаться класс Scanner платформы Java. Предназначенный для использования в качестве легкого парсера текста, Scanner обеспечивает сравнительно простой API для разбивки структурированного текста на строго типизированные части. Допустим, что есть серия DSL-подобных команд (из романа Терри Пратчетта "Плоский мир"), организованных в текстовой файл, как в листинге 7.

Листинг 7. Задачи для Игоря
fetch 1 head
fetch 3 eye
fetch 1 foot
attach foot to head
attach eye to head
admire

Вам или в данном случае вашему личному секретарю по имени Igor будет нетрудно разобрать эту серию команд с помощью Scanner, как показано в листинге 8.

Листинг 8. Задачи для Игоря, решенные Scanner'ом
import java.io.*;
import java.util.*;

public class Igor
    implements IPersonalServant
{
    public boolean can(String cmd)
    {
        if (cmd.equals("fetch body parts"))
            return true;
        if (cmd.equals("attach body parts"))
            return true;
        else
            return false;
    }
    public void process(File commandFile)
        throws FileNotFoundException
    {
        Scanner scanner = new Scanner(commandFile);
        // Команды поступают в форме имен числительных/существительных или глаголов
        while (scanner.hasNext())
        {
            String verb = scanner.next();
            if (verb.equals("fetch"))
            {
                int num = scanner.nextInt();
                String type = scanner.next();
                fetch (num, type);
            }
            else if (verb.equals("attach"))
            {
                String item = scanner.next();
                String to = scanner.next();
                String target = scanner.next();
                attach(item, target);
            }
            else if (verb.equals("admire"))
            {
                admire();
            }
            else
            {
                System.out.println("I don't know how to " 
                    + verb + ", marthter.");
            }
        }
    }

    public void fetch(int number, String type)
    {
        if (parts.get(type) == null)
        {
            System.out.println("Fetching " + number + " " 
                + type + (number > 1 ? "s" : "") + ", marthter!");
            parts.put(type, number);
        }
        else
        {
            System.out.println("Fetching " + number + " more " 
                + type + (number > 1 ? "s" : "") + ", marthter!");
            Integer currentTotal = parts.get(type);
            parts.put(type, currentTotal + number);
        }
        System.out.println("We now have " + parts.toString());
    }

    public void attach(String item, String target)
    {
        System.out.println("Attaching the " + item + " to the " +
            target + ", marthter!");
    }

    public void admire()
    {
        System.out.println("It'th quite the creathion, marthter");
    }

    private Map<String, Integer> parts = new HashMap<String, Integer>();
}

В предположении, что Igor зарегистрирован в ServantLoader, достаточно заменить вызов can() на что-то более подходящее и повторить предыдущий код Servant, как показано в листинге 9.

Листинг 9. Что делает Игорь
import java.io.*;
import java.util.*;

public class Servant
{
    public static void main(String[] args)
        throws IOException
    {
        ServiceLoader<IPersonalServant> servantLoader = 
            ServiceLoader.load(IPersonalServant.class);

        IPersonalServant i = null;
        for (IPersonalServant ii : servantLoader)
            if (ii.can("fetch body parts"))
                i = ii;

        if (i == null)
            throw new IllegalArgumentException("No suitable servant found");

        for (String arg : args)
        {
            i.process(new File(arg));
        }
    }
}

Конечно, реальная реализация DSL будет не просто печатать в стандартный выходной поток. Подробности отслеживания того, какие части соединяются с какими, я предоставлю вам (и, конечно, верному Igor).

4. Timer

Классы java.util.Timer и TimerTask предоставляют удобный и относительно простой способ решения задач на основе периодической или одноразовой задержки (листинг 10).

Листинг 10. Выполнить позже
import java.util.*;

public class Later
{
    public static void main(String[] args)
    {
        Timer t = new Timer("TimerThread");
        t.schedule(new TimerTask() {
            public void run() {
                System.out.println("This is later");
                System.exit(0);
            }
        }, 1 * 1000);
        System.out.println("Exiting main()");
    }
}

Timer выполняет ряд перезагрузок schedule(), указывающих, является ли та или иная задача одноразовой или циклической, и принимает на выполнение экземпляр TimerTask. TimerTask, по существу, представляет собой Runnable (фактически, реализует его), но также содержит два дополнительных метода: cancel(), который удаляет задачу, и scheduledExecutionTime(), который возвращает приблизительное время запуска задачи.

Обратите внимание, однако, что для запуска задачи в фоновом режиме Timer создает не-daemon поток, так что в листинге 10 нужно уничтожить VM с помощью вызова System.exit(). В "долгоиграющей" программе Timer, вероятно, лучше создать как daemon-поток (используя конструкторы с применением логического параметра для указания состояния демона), который не будет сохранять VM.

В этом классе нет ничего сверъестественного, но он позволяет четче указать намерения при запуске фоновых задач. К тому же он может сэкономить несколько строк кода Thread и послужит легким вариантом ScheduledExecutorService (для тех, кто не готов погрузиться в полный пакет java.util.concurrent).

5. JavaSound

Звук, хотя он и не часто появляется в серверной части приложений, может служить полезным «пассивным» сигналом для администраторов, — и хорошим материалом для шуток. Поздно появившись на платформе Java, API JavaSound проник в ядро библиотеки времени выполнения, спрятанной в пакетах javax.sound *— один пакет для MIDI-файлов и один для файлов сэмплированных звуков (таких как вездесущие файлы формата .WAV).

В листинге 11 приведен код «Hello world» инструмента JavaSound ― проигрыватель клипов.

Листинг 11. Играй, Сэм, играй
public static void playClip(String audioFile)
{
    try
    {
        AudioInputStream audioInputStream =
            AudioSystem.getAudioInputStream(
                this.getClass().getResourceAsStream(audioFile));
        DataLine.Info info = 
            new DataLine.Info( Clip.class, audioInputStream.getFormat() );
        Clip clip = (Clip) AudioSystem.getLine(info);
        clip.addLineListener(new LineListener() {
                public void update(LineEvent e) {
                    if (e.getType() == LineEvent.Type.STOP) {
                        synchronized(clip) {
                            clip.notify();
                        }
                    }
                }
            });
        clip.open(audioInputStream);        

        clip.setFramePosition(0);

        clip.start();
        synchronized (clip) {
            clip.wait();
        }
        clip.drain();
        clip.close();
    }
    catch (Exception ex)
    {
        ex.printStackTrace();
    }
}

Большая часть этого кода довольно проста (или, по крайней мере, так же проста, как извлечение JavaSound). Первый шаг заключается в создании AudioInputStream вокруг воспроизводимого файла. Чтобы сохранить этот метод как можно более контекстно-независимым, мы извлекаем этот файл как InputStream из ClassLoader, который загрузил класс. (AudioSystem также принимает File или String, если точный путь к звуковому файлу известен заранее.) Затем объект DataLine.Info передается в AudioSystem, чтобы получить Clip ― простейший способ воспроизведения аудиоклипа. (Другие подходы — такие как получение SourceDataLine— обеспечивают больший контроль над клипом, но они слишком громоздки для простого "проигрывателя".)

Теперь достаточно вызвать open() с AudioInputStream внутри (если не возникнет ошибка, обсуждаемая в следующем разделе.) Вызовите start(), чтобы начать воспроизведение, drain(), чтобы дождаться его окончания и close(), чтобы освободить аудиоканал. Воспроизведение происходит в отдельном потоке, поэтому вызов stop() остановит его, а последующий вызов start() запустит с того места, где воспроизведение было приостановлено; для сброса к началу используйте setFramePosition(0).

Нет звука?

В JDK, начиная с версии 5, есть неприятная ошибка: на некоторых платформах при коротких аудиоклипах код вроде бы работает нормально, но... звука нет. По-видимому, на этих коротких клипах событие медиаплеера STOP происходит раньше, чем нужно. (См. ссылку на страницу этой ошибки в разделе Ресурсы.)

Эта ошибка помечена как «неисправимая», но обойти ее очень легко: зарегистрируйте LineListener, чтобы прослушивать события STOP, и когда это произойдет, вызовите notifyAll() для объекта клипа. Затем в вызывающем коде дождитесь окончания клипа (и вызова notifyAll()), вызвав wait(). На платформах, где ошибка отсутствует, эти шаги будут лишними, но на Windows® и некоторых дистрибутивах Linux® они позволят разработчику сохранить лицо.

Заключение

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

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


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: 5 things you didn't know about ... everyday Java tools.
  • Пять секретов...: цикл статей с полезными советами по Java-программированию.
  • Scheduling recurring tasks in Java applications (Tom White, developerWorks, ноябрь 2003 г.): среда планирования, которая представляет собой обобщение Timer и TimerTask, обеспечивая более гибкое планирование.
  • javax.sound «нет звука» ― это известная ошибка, которая значится в базе данных ошибок Sun/Oracle.
  • StAX, потоковый API-интерфейс для XML, стандартный API обработки XML, который позволяет направлять поток XML-данных в приложение и из него.

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=843133
ArticleTitle=Пять секретов... повседневных Java-инструментов
publish-date=10292012