31 октября 2012 г.

Основные операции в ant-скрипте

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

Итак, ant-скрипт представляет собой xml-файл, обычно называемый build.xml. Этот файл обычно лежит в корневой директории java-проекта. Но все эти параметры можно переопределить в командной строке при запуске ant.
<project name="ant.tutorial" default="compile">
<property file="build.properties" />
Даём проекту имя и указываем, что по умолчанию для него следует вызывать цель compile, которая будет определена ниже. Далее подгружаем параметры из файла свойств build.properties. Вот его примерное содержимое:
lib.dir=lib
project.dir=.
project.src.dir=${project.dir}/src
build.dir=build
build.jar.name=simple-proj.jar
build.jar.mainclass=org.test.SimpleMain
dist.name=simple-proj
dist.dir=distribution
dist.lib.dir=lib
host=10.1.2.31
username=root
password=123456
archive.name=${dist.name}-distr.zip
Все эти параметры будут использоваться в нашем ant-скрипте. Обратите внимание, что в качестве значения параметра может использоваться другой параметр. Обращение к значению параметра как в файле свойств, так и в ant-скрипте производится при помощи конструкции вида ${имя_параметра}.
<target name="clean">
<delete dir="${build.dir}" />
<delete dir="${dist.dir}" />
</target>
Определяем цель по имени clean для очистки директорий, в которые мы будем складывать бинарные файлы.
<target name="make.build.dirs" depends="clean">
<mkdir dir="${build.dir}" />
<mkdir dir="${build.dir}/class" />
</target>
Определяем цель, в которой создаётся директории для бинарников (если они ещё не были созданы). Здесь имеется зависимость от предыдущей цели clean. Если необходимо создать несколько директорий, вложенных одна в другую, это можно сделать одной командой.
<target name="compile" description="compile java" depends="make.build.dirs">
<path id="classpath">
<fileset dir="${lib.dir}" includes="**/*.jar"
excludes="**/*javadoc*.jar,**/*sources*.jar" />
</path>
Определяем цель для компиляции проекта из исходников. Затем задаём объект classpath. В этом объекте содержится набор путей до каждой из библиотек, которые нужны для сборки проекта. Обратите внимание на параметры includes (включаем в classpath пути до всех jar-файлов) и excludes (исключаем из classpath jar-файлы, которые содержат в своём имени строки "javadoc" и "sources").
<pathconvert property="compile.classpath" refid="classpath" />
<echo>CLASSPATH: ${compile.classpath}</echo>
Создаём тектовое представление объекта classpath в виде свойства и выводим значение этого свойства в консоль при помощи уже знакомой нам команды echo.
<javac destdir="${build.dir}/class" includeantruntime="false"
source="1.6" target="1.6">
<src path="${project.src.dir}" />
<classpath refid="classpath" />
</javac>
</target>
Вызываем компилятор бинарный файлов javac и задаём для него необходимые параметры. Source и target определяют, какая версия jdk использовалась при написании кода и для какой jre следует компилировать бинарные файлы соответственно (хотя это и не обязательно указывать). Также указываем путь до исходников и пути до библиотек. На этой цель compile завершается.
<target name="make.dist.dirs" description="Make dirs for distribution">
<mkdir dir="${dist.dir}/${dist.name}" />
<mkdir dir="${dist.dir}/${dist.name}/${dist.lib.dir}" />
</target>
Определяем цель для создания директорий, в которых будет располагаться дистрибутив.
<target name="dist.lib.copy" depends="make.dist.dirs">
<copy todir="${dist.dir}/${dist.name}/${dist.lib.dir}" flatten="true">
<path refid="classpath" />
</copy>
</target>
Копируем необходимые библиотеки в папку дистрибутива. Пути берутся из classpath.
<target name="make.jar" description="Make jar file" depends="compile,dist.lib.copy">
<pathconvert property="dist.classpath" pathsep=" " refid="classpath">
<mapper>
<chainedmapper>
<flattenmapper />
<globmapper from="*" to="${dist.lib.dir}/*" />
</chainedmapper>
</mapper>
</pathconvert>
Создаём цель, которая формирует конечный jar-архив для нашего проекта. Здесь указывается зависимость от двух других целей. Конвертируем classpath в формат Class-Path, который используется в файле MANIFEST.MF внутри jar-архива. Также конвертируем пути до библиотек в пути относительно каталогов дистрибутива. Необходимость этих манипуляций Вам должна быть понятна, т.к. ранее мы уже рассматривали сборку проекта вручную.
<echo>Class-Path: ${dist.classpath}</echo>
<jar destfile="${dist.dir}/${dist.name}/${build.jar.name}">
<manifest>
<attribute name="Main-Class" value="${build.jar.mainclass}" />
<attribute name="Class-Path" value="${dist.classpath}" />
</manifest>
<fileset dir="${build.dir}/class">
<include name="**/*.class" />
</fileset>
<fileset dir="${project.src.dir}">
<include name="log4j.xml" />
</fileset>
</jar>
</target>
Выводим на экран полученный Class-Path для файла MANIFEST.MF. Затем формируем jar-файл, в который включаем созданный тут же MANIFEST.MF и бинарные class-файлы. Также добавляем дополнительные конфигурационные файлы (если необходимо). На этом цель завершается. Написав такую цель один раз, это избавляет вас от рутинных операций по сборке jar-файлов, которые вам приходилось бы делать всякий раз при ручной сборке.
<target name="javadoc">
<javadoc sourcepath="${project.src.dir}" defaultexcludes="yes"
destdir="${dist.dir}/${dist.name}/api" author="true" version="true"
use="true" windowtitle="Simple Project API" charset="UTF-8">
<doctitle><![CDATA[<h1>Simple Project API</h1>]]></doctitle>
<bottom><![CDATA[<i>Copyright &#169; 2012. All Rights Reserved.</i>]]></bottom>
</javadoc>
</target>
Дополнительная цель - создание javadoc файлов на основе специально оформленных комментариев в исходном коде. Одна цель - а на выходе вы получаете целую документацию по вашему проекту в виде html-файлов!
<target name="dist" description="Creating a distribution"
depends="make.jar, dist.lib.copy, javadoc">
<delete file="${dist.dir}/${archive.name}" />
<zip destfile="${dist.dir}/${archive.name}" basedir="${dist.dir}" />
</target>
Упаковываем дистрибутив в zip-архив. Это очень удобно при копировании на удалённый сервер. В архив войдут jar-файл, все необходимые библиотека, а также документация.
<target name="remote.copy" description="Copy to a remote server"
depends="dist">
<scp file="${dist.dir}/${archive.name}" todir="${username}@${host}:/root"
password="${password}" />
<sshexec host="${host}" username="${username}" password="${password}"
command="unzip -o ${archive.name}; rm -rf ${archive.name}" />
</target>
</project>
Ну и наконец пример цели, в которой происходит копирование по ssh на удалённый сервер - эту рутинную операцию вы тоже можете автоматизировать! Затем на удалённом сервере выполняется команда по распаковке архива дистрибутива. Для корректной работы этой цели вам потребуется библиотека для работы с ssh под названием jsch. Скачать можно здесь. Скопируйте её в папку ".ant/lib" в вашей домашней директории.

На этом наш ant-скрипт заканчивается. Он построен таким образом, что при вызове цели remote.copy будут выполнены все остальные цели в этом проекте. Минимально необходимая цель для получения дистрибутива - make.jar. Если вам нужен полный дистрибутив без копирования на удалённый сервер - вызывайте цель dist.

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

28 октября 2012 г.

Принципы написания ant-скриптов

Сборщик проектов Ant выполняет только те действия, которые описаны в файле build.xml (обычно лежит в корне проекта, который необходимо собрать). Этот файл иногда также называют ant-скриптом, хотя его содержимое представляет собой xml.

В скриптах ant имеется такое понятие, как цель (target). Это именованный набор команд, который выполняется ant-ом. В рамках одного проекта может быть несколько целей. Одна из этих целей может быть указана как цель по умолчанию. Одна цель может зависеть от другой. Таким образом, при выполнении этой цели, сначала будет выполнена та, от которой зависит текущая. Также может быть указана зависимость от нескольких целей сразу. Все эти цепочки зависимостей ant успешно обрабатывает.

Для примера рассмотрим следующий скрипт:
<?xml version="1.0" encoding="UTF-8"?>
<project name="ant.test" default="clean">
<target name="clean">
<echo>CLEAN target invoked</echo>
</target>
<target name="compile" depends="clean">
<echo>COMPILE target invoked</echo>
</target>
</project>
Он не делает ничего полезного, зато позволяет быстро разобраться что к чему. Сохраните его в файле build.xml, затем перейдите в ту директорию, где вы сохранили этот файл.

Проект называется ant.test, для него указана цель по умолчанию под названием "clean". Следовательно, для выполнения этой цели достаточно ввести команду "ant", и эта цель будет выполнена автоматически. Элемент <echo> просто выводит своё содержимое в консоль. Результат работы:
clean:
     [echo] CLEAN target invoked
BUILD SUCCESSFUL
Если мы хотим выполнить другую цель, то нужно при вызове ant задать её явно:
ant compile
В приведенном выше скрипте при помощи атрибута depends указана зависимость цели compile от clean, следовательно, сначала будет выполнена clean, а затем compile. Результатом работы будет вывод уже двух строк:
clean:
     [echo] CLEAN target invoked
compile:
     [echo] COMPILE target invoked
BUILD SUCCESSFUL
Таким образом, вызывается целая цепочка команд.

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

27 октября 2012 г.

Компиляция java-проекта вручную

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

Вкратце процесс сборки состоит из следующих шагов:

  1. Компиляция байт-кода в виде *.class-файлов из исходников.
  2. Упаковка полученных файлов с байт-кодом, а также необходимых конфигурационных файлов в jar-архив.
  3. Добавление в файл MANIFEST.MF внутри архива относительных путей до необходимых библиотек.

Компиляция из исходников в байт-код производится при помощи программы javac, которая входит в JDK. Перейдите в директорию вашего java-проекта и выполните следующую команду, подставляя, где необходимо, ваши значения:
javac -d директория_для_байт_кода -classpath пути_до_необходимых_библиотек -sourcepath директория_с_исходниками  путь_до_класса_с_main

  • Опция -d указывает, куда нужно положить class-файлы.
  • Опция -classpath задаёт набор путей до необходимых в данном проекте файлов сторонних библиотек. Если библиотек несколько, пути разделяются точкой с запятой ";".
  • Опция -sourcepath указывает путь до директории, в которой лежат исходники. У меня она называется "src".
  • В конце следует указать путь до исходника с классом, который содержит метод main.

В результате выполнения этой команды у вас появятся файлы байт-кода. Теперь их нужно упаковать в jar-архив. Перейдите в ту директорию, которую вы указали в опции -d для компиляции проекта. Скопируйте в неё необходимые конфигурационные файлы (например, log4j.xml). Выполните команду:
jar cvf имя_файла_архива .
Опция с - создать архив
Опция v - отображать служебную информацию в процессе создания архива
Опция f - производить вывод не в консоль, а в файловую систему
В качестве имени файла архива можно указать любое имя. Архиву, как правило, даётся расширение jar.

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

В результате вы получите jar-архив, в который буду запакованы файлы байт-кода, дополнительные конфигурационные файлы, а также будет создан стандартный файл /META-INF/MANIFEST.MF - нам его необходимо модифицировать.

Откройте архиватором созданный архив и модифицируйте файл MANIFEST.MF, добавив в него две строки:

Class-Path: пути_до_файлов_библиотек
Main-Class: полное_имя_класса_с_main


В Class-Path следует перечислить все необходимые файлы библиотек с относительными путями до них. Если библиотек несколько, то пути разделяются пробелами. Также рекомендуется положить все эти библиотеки в одну директорию.
В Main-Class нужно указать полное имя класса с методом main (т.е. с указанием пакета, например org.test.SimpleMain).

Внимание! Проследите за тем, чтобы в конце файла MANIFEST.MF была пустая строка (ещё один перевод строки) - это требование спецификации. В противном случае возможна некорректная обработка этого файла.

Сохраните изменения в архиве. Теперь вы можете запустить полученный архив при помощи команды java -jar имя_архива и ваше приложение начнёт свою работу.

26 октября 2012 г.

Простое логирование при помощи Log4j

Log4j - одна из первых библиотек, созданных для упрощения процедуры логирования. Эта библиотека прошла испытание временем, поэтому до сих широко используется в java-приложениях. В этой заметке я продемонстрирую минимальную настройку log4j для быстрого старта.

В большинстве информационных систем среди уровней логирования выделяют следующие (от более низкого к более высокому):
  1. TRACE - сообщения самого низкого уровня, наиболее подробные
  2. DEBUG - отладочные сообщения, менее подробные, чем TRACE
  3. INFO - стандартные информационные сообщения
  4. WARN - некритичные ошибки, не препятствующие работе приложения
  5. ERROR - ошибки, которые могут привести к неверному результату
  6. FATAL - ошибки, препятствующие дальнейшей работе приложения
При каждом последующем уровне количество выводимой информации уменьшается, но её критичность при этом должна увеличиваться.

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

  1. File - New Project, там выберите тип проекта Java, убедитесь, что не отмечено ни одного пункта и жмите Next.
  2. Здесь тоже ничего не надо отмечать, просто жмите Next.
  3. Укажите какое-нибудь название для проекта. Рекомендую использовать только строчные буквы, а пробелы заменять дефисом. Жмите Finish.

Здесь среда разработки создаст самую простую структуру проекта. Прежде чем приступить к написанию кода нам нужно подключить библиотеку log4j в виде jar-файла. Вы можете либо скачать его вручную, либо загрузить из maven-репозитория при помощи Idea (если у вас настроен maven).

Как подключить стороннюю библиотеку к проекту. Открывайте File - Project Structure, затем в левой панели выберите Libraries, затем жмите на зелёный плюс. И тут у вас есть выбор: либо добавить jar-файл к проекту, указав его расположение в файловой системе, либо загрузив его из maven-репозитория. Чтобы загрузить библиотеку из репозитория, просто укажите её название и версию как указано на картинке ниже:


Обязательно отметьте пункт Download to, не меняя его значения по умолчанию - тогда библиотека будет загружена прямо в ваш проект.

Создайте в папке src вашего проекта какой-нибудь пакет (например, ru.nordmine), а в нём создайте класс SimpleMain с таким содержимым:

package ru.nordmine;

import org.apache.log4j.Logger;

public class SimpleMain {

private static final Logger logger = Logger.getLogger(SimpleMain.class);

public static void main(String[] args) {
for(int i = 0; i < 3; i++) {
try {
logger.info("result: " + divide(i));
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}

public static int divide(int x) {
logger.debug("divide method invoked; 2 / " + x);
if(x == 0) {
logger.warn("x = 0; exception may occur");
}
return 2 / x;
}
}

В классе SimpleMain логгер определён в виде приватного статического и неизменяемого поля. В качестве параметра метода, который его создаёт, передаём текущий класс. В самом методе main в цикле вызывается метод divide, который выполняет какую-то работу. Неважно какую, в качестве примера он делит 2 на то число, которое ему передали. Обратите внимание на уровни логирования.

В методе divide при старте выводится сообщение уровня debug о том, что метод запущен и с каким параметром. Предположим, что мы используем уровень логирования INFO, поэтому данное сообщение будет выводится не будет. И зачем, ведь нам не интересно знать о каждом выводе метода и с каким параметром.

Далее, поскольку метод делит 2 на то, что к нему пришло, есть некая вероятность деления на нуль (что и прозойдёт в нашем примере). Поэтому лучше проверять входные аргументы и выводить предупреждение, если что-то будет не так. Уровень WARN выше, чем INFO, поэтому при текущем уровне логирования предупреждение будет выведено.

В методе main в цикле мы вызываем метод divide, передавая ему последовательно аргументы от 0 до 2. Зная, что в вызываемом методе потенциально может возникнуть ошибка, обернём его вызов в конструкцию try-catch. Обратите внимание, что она внутри цикла, а не снаружи. То есть ошибочный аргумент метода не остановит весь цикл, а только одну итерацию, что логично. Если вызов пройдёт успешно, будет показан результат с уровнем INFO, если произойдёт исключение, то будет выведен текст ошибки с уровнем ERROR и полный stack trace, ведь мы передаём логгеру не только текст ошибки, но и объект исключения. И это правильно, т.к. среди однотипных записей лога гораздо быстрее обнаружить stack trace, чем неприметную запись ERROR.

Теперь осталось сконфигурировать логгер. Его конфигурация может быть в двух форматах: обычном текстовом property-файле (log4j.properties) и в формате xml (log4j.xml). Начнём с более простого текстового. Положите файл log4j.properties со следующим содержимым в корень вашего проекта:

# Root logger option
log4j.rootLogger=INFO, stdout

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

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

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

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration PUBLIC
        "-//APACHE//DTD LOG4J 1.2//EN"
        "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
<log4j:configuration debug="false">
    <appender name="ConsoleAppender" class="org.apache.log4j.ConsoleAppender">
        <param name="Encoding" value="utf-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
        </layout>
    </appender>
    <appender name="FileAppender" class="org.apache.log4j.DailyRollingFileAppender">
        <param name="File" value="messages"/>
        <param name="DatePattern" value="'-'yyyy-MM-dd"/>
        <param name="Encoding" value="utf-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
        </layout>
    </appender>
    <root>
        <priority value="INFO"/>
        <appender-ref ref="ConsoleAppender"/>
        <appender-ref ref="FileAppender"/>
    </root>
</log4j:configuration>

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

Если с консолью всё понятно, то почему был выбран именно DailyRollingFileAppender? Дело в том, что этот тип аппендера позволяет не просто записывать в файл, а ещё и автоматически архивировать эти файлы. Например, создавать каждые сутки новый файл для лога. Имя файла, а заодно и правила ротации, задаёт параметр DatePattern. В нашем примере файлы будут создаваться каждые сутки. Имя файла будет иметь формат "messages-ГГГГ-ММ-ДД". Файл, в который в настоящий момент ведётся запись, имеет имя "messages" (параметр File) без метки времени. Можно настроить ротацию также каждый час и даже каждую минуту. В зависимости от того, как много записей добавляется в лог.

В конце указываем минимальный корневой уровень логирования (INFO) для обоих аппендеров. Записи ниже этого уровня добавляться не будут.

Теперь, если запустить наше приложение, мы увидим только одну строку с уровнем INFO (в файл была добавлена точно такая же строка):

2014-06-02 20:40:36 WARN  SimpleMain:22 - x = 0; exception may occur
2014-06-02 20:40:36 ERROR SimpleMain:14 - / by zero
java.lang.ArithmeticException: / by zero
at ru.nordmine.SimpleMain.divide(SimpleMain.java:24)
at ru.nordmine.SimpleMain.main(SimpleMain.java:12)
2014-06-02 20:40:36 INFO  SimpleMain:12 - result: 2
2014-06-02 20:40:36 INFO  SimpleMain:12 - result: 1

Здесь мы видим, что сначала было предупреждение, а затем произошла ошибка, поэтому понижаем уровень логирования до DEBUG, чтобы получить больше отладочной информации, и тогда мы увидим, какие методы вызывались и с какими параметрами:

2014-06-02 20:41:33 DEBUG SimpleMain:20 - divide method invoked; 2 / 0
2014-06-02 20:41:33 WARN  SimpleMain:22 - x = 0; exception may occur
2014-06-02 20:41:33 ERROR SimpleMain:14 - / by zero
java.lang.ArithmeticException: / by zero
at ru.nordmine.SimpleMain.divide(SimpleMain.java:24)
at ru.nordmine.SimpleMain.main(SimpleMain.java:12)
2014-06-02 20:41:33 DEBUG SimpleMain:20 - divide method invoked; 2 / 1
2014-06-02 20:41:33 INFO  SimpleMain:12 - result: 2
2014-06-02 20:41:33 DEBUG SimpleMain:20 - divide method invoked; 2 / 2
2014-06-02 20:41:33 INFO  SimpleMain:12 - result: 1

25 октября 2012 г.

Хранение настроек в property-файлах

В любом приложении всегда есть неизменяемые значения: путь до какой-либо директории, максимальное время установления соединения, адрес базы данных, количество попыток переподключения и т.п. Такие значения рекомендуется делать хотя бы статичными полями класса (три модификатора private static final). Это вам пригодится на тот случай, если казавшиеся ранее неизменяемыми параметры внезапно изменятся. Например, в БД на продакшене таблицы имеют другой префикс. И если имена таблиц вынесены как статичные поля в начале класса, то не нужно искать все вхождения имени таблицы. Достаточно просто отредактировать значения этих полей, запустить сборку - и приложение снова работает!

Давайте пойдём дальше и сделаем так, чтобы можно было изменять параметры без редактирования исходного кода и последующей его компиляции. В Java нам на помощь придёт стандартный класс Properties. У него есть методы, позволяющие загрузить значения параметров из файла конфигурации. Обычно этот файл имеет расширение .properties и хранится внутри jar-архива. Хотя это и не обязательно. Формат этого файла может быть как простой текстовый ("ключ=значение"), так и xml.

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

Если значение содержит текстовую строку с пробелами, то эта строка заключается в кавычки. Пример файла свойств в простом текстовом формате:
page.file.name = index.html
page.title = "Page Second Name"
Пример файла свойств в формате xml (обратите внимание, что тут уже не нужно заключать значение с пробелами в кавычки):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="page.file.name">index.html</entry>
<entry key="page.title">Page Second Name</entry>
</properties>
Загрузка свойств может быть произведена в любое время при помощи следующего кода:
Properties properties = new Properties();
properties.load(this.getClass().getResourceAsStream("/META-INF/deploy.properties"));
datasourceLookup = properties.getProperty("datasource.lookup");
Здесь путь к файлу со свойствами указан от корневой директории внутри jar-файла. Значение свойства получаем по его имени в файле (возвращается тип String) и присваиваем статичному полю класса.

Если мы используем файл свойств в формате xml, нужно использовать метод loadFromXml().

Чтобы вывести все свойства, загруженные в объект properties, нам пригодится метод properties.entrySet(), вызванный в цикле for. Однако порядок следования параметров не обязательно будет таким же, как и в файле. Поэтому сортировку нужно будет сделать дополнительно.

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

23 октября 2012 г.

Запуск jar-файла из командной строки

Сейчас я покажу, как можно запустить простенькую программу на java безо всякой среды вроде сервера приложений. Достаточно одного класса, в котором будет определён метод main.

package com.blogspot;
public class DeveloperRemarks {
public static void main(String[] args) {
if (args.length == 0) {
System.out
.println("Передайте в качестве параметра командной строки текст, который хотите увидеть.");
} else {
System.out.println("Вы ввели: " + args[0]);
}
System.out.println("Исходники доступны на developer-remarks.blogspot.com");
}
}
В командной строке при запуске программы необходимо передать текст, который будет отображён на экране. В противном случае будет выведено соответствующее сообщение. Все параметры, переданные программе, будут содержаться в массиве строк args, который принимает метод main. Если не передано ни одного параметра, длина массива будет равна нулю.

Теперь настроим в Eclipse возможность запуска jar-файла из терминала. Для этого сначала необходимо создать конфигурацию запуска. Выберите в верхнем меню Run - Run Configurations. Откроется следующее окно:


В списке слева выберите тип приложения: Java Application. Там создайте новую конфигурацию как показано на скриншоте. Для этого выберите целевой проект и класс, в котором есть метод main.

Теперь мы готовы к тому, чтобы экспортировать наш проект в запускаемый jar-файл. Для этого кликните на проекте правой кнопкой мыши, выберите "Export...", откроется следующее окно:


В древовидном списке выберите Runnable Jar file. Поиск можно значительно упростить, если отфильтровать тип проекта по названию. Нажмите Finish. После этого откроется окно для настроек экспорта:


В нём выберите созданную ранее конфигурацию запуска. Затем укажите полное имя jar-файла. В разделе "Library handling" выберите "Extract required libraries into generated JAR" (распаковать необходимые библиотеки внутрь создаваемого архива). Нажмите Finish и исполняемый архив будет создан.

Поскольку в данном примере нет зависимостей от других библиотек, то мы выбираем первый пункт. Но если бы у нас было несколько десятков, а то и сотен зависимостей, правильнее было бы выбрать третий пункт "Copy required libraries into a sub-folder next to the generated JAR" (копировать необходимые библиотека в поддиректорию, часть имени которой совпадает с создаваемым архивом) во избежание "раздувания" исполняемого файла. Если бы мы выбрали третий пункт, необходимые библиотеки были бы скопированы в отдельную папку.

Теперь откройте терминал и перейдите в ту директорию, где был создан исполняемый jar-файл. Введите следующую команду:
java -jar developer-remarks.jar привет,\ мир\!
Таким образом вы указываете интерпретатору java запустить наш архив. Обратите внимание, что пробел и восклицательный знак в командной строке необходимо экранировать обратным слэшем.

Продемонстрированный подход может быть полезен в образовательных и тестовых целях. Например, для создания клиента, который вызовет EJB и выведет результат его работы в консоль. Достоинством данного подхода является запуск jar-архива без какого бы то ни было окружения. Достаточно иметь JDK/JRE. Исходники доступны здесь.

22 октября 2012 г.

Основные запросы LINQ to Objects

LINQ (Language INtegrated Query, язык интегрированных запросов) - это довольно мощная и удобная технология, впервые представленная в .NET Framework 3.5. Основная его идея заключается в том, чтобы можно было извлекать информацию из любых объектов написанием запросов, похожих на запросы к базе данных. Ключевое слово здесь "любых". Именно поэтому выделяются такие понятия:
  • LINQ to Objects
  • LINQ to XML
  • LINQ to DataSet
  • LINQ to SQL
  • LINQ to Entities
  • Parallel LINQ
В этой своей заметке я хотел бы рассмотреть самую простую разновидность LINQ - LINQ to Objects. Тем не менее, изучив основные методы и конструкции LINQ to Objects можно будет легко использовать и другие его разновидности, т.к. интерфейс их почти одинаков.

Первое, с чем надо разобраться, заключается в том, что в LINQ можно использовать два синтаксиса. Самые базовые конструкции LINQ можно записывать в виде выражений запросов. Те же базовые конструкции + большая часть остальных доступна в виде обычной "точечной нотации" (т.е. методов, которые в качестве параметров получают лямбда-выражения). Базовые конструкции я использую только в виде выражений запросов, т.к. считаю этот синтаксис более наглядным. Для остальных конструкций приходится использовать "точечную нотацию".

Вторая особенность LINQ состоит в том, что все запросы делятся на отложенные и не отложенные. Отложенные запросы легко определить по типу возвращаемого значения. Для LINQ to Objects это тип IEnumerable<T>. Отложенные запросы на самом деле возвращают не данные, а лишь информацию о том, как их следует извлекать. Это сделано для того, чтобы можно было составлять цепочки ограничений на выборку без потери в производительности. В противовес этому, не отложенные запросы возвращают, как правило, конкретные объекты или коллекции этих объектов. Примеры методов, которые приводят к немедленному выполнению запросов:
  • Преобразование к одному из типов коллекций: ToList(), ToDictionary(), ToArray() и т.п.
  • Получение единственного элемента: Single(), First(), Last() и т.п.
  • Функции агрегации: Count(), Sum(), Max(), Min(), Average() и т.п.
За более полным списком обращайтесь к официальной документации MSDN.

Вполне логично предположить, что не отложенные операции должны заканчивать ваши запросы LINQ, т.к. в конечном итоге вам нужно получить конкретные данные. Но всегда нужно смотреть в зависимости от ситуации.

Теперь перейдём к примерам. У нас имеется тестовое приложение, состоящее из нескольких классов. Привожу их код.

Класс-сущность User.
using System;
namespace Developer.Remarks.Blogspot.Com
{
    class User
    {
        public int ID { get; set; }
        public String Name { get; set; }
        public String Surname { get; set; }
        public User(int id, String name, String surname)
        {
            this.ID = id;
            this.Name = name;
            this.Surname = surname;
        }
        public override string ToString()
        {
            return string.Format("ID={0}: {1} {2}", ID, Name, Surname);
        }
    }
}
Класс-сущность Record, зависящая от User.
using System;
namespace Developer.Remarks.Blogspot.Com
{
    class Record
    {
        public User Author { get; set; }
        public String Message { get; set; }
        public Record(User author, String message)
        {
            this.Author = author;
            this.Message = message;
        }
    }
}
Класс бизнес-логики по обработке данных. Именно в нём находятся LINQ-запросы.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Developer.Remarks.Blogspot.Com
{
    class BusinessLogic
    {
        private List<User> users = new List<User>();
        private List<Record> records = new List<Record>();
        public BusinessLogic()
        {
              // наполнение обеих коллекций тестовыми данными
        }
Теперь будем рассматривать каждый запрос по отдельности.
public List<User> GetUsersBySurname(String surname)
{
return (from u in users
where u.Surname == surname
select u).ToList();
}
Фильтрация коллекции пользователей по имени. В отличие от SQL-запросов, в LINQ сначала идёт ключевое слово from. Это сделано для сужения контекста автоподстановки в Visual Studio. После from идёт локальная переменная u (может быть любой, это как алиас в SQL-запросах), которой мы обозначаем текущий элемент в коллекции users в рамках данного запроса. Затем задаём ограничения на выборку в конструкции where. Здесь указывается любое булевое выражение. Каждый элемент коллекции будет добавлен в выборку, если для него будет удовлетворяться это условие. И в конце указываем при помощи select, какие именно данные нам нужны. Можно указать как весь элемент целиком, так и отдельные его поля. Результатом этого запроса будет отложенная операция. После всю эту выборку мы преобразуем в список при помощи не отложенной операции ToList().
public User GetUserByID(int id)
{
return (from u in users
where u.ID == id
select u).Single();
}
Получение одного конкретного пользователя по его id. В конце вызываем метод Single(), которые возвращает элемент, если он единственный. В противном случае, метод генерирует исключение, если найдено несколько элементов.
public List<User> GetUsersBySubstring(String substring)
{
return (from u in users
where u.Name.Contains(substring) || u.Surname.Contains(substring)
select u).ToList();
}
Выборка пользователей по подстроке. Ищем эту подстроку в имени или в фамилии при помощи метода Contains().
public List<String> GetAllUniqueNames()
{
return (from u in users
select u.Name).Distinct().ToList();
}
Выбираем только уникальные имена при помощи метода Distinct().
public List<User> GetAllAuthors()
{
return (from u in users
join r in records on u equals r.Author
select u).ToList();
}
Выбираем всех авторов, т.е. пользователей, у которых есть сообщения. Здесь демонстрируется объединение двух коллекций, аналогичное операции join в SQL-запросе. Связывание коллекций идёт при помощи ссылки на объект пользователя, указанного в сущности Record.
public Dictionary<int, User> GetUsersDictionary()
{
return (from u in users
select u).ToDictionary(i => i.ID, i => i);
}
Преобразуем коллекцию пользователей в словарь при помощи двух лямбда-выражений, которые принимает метод ToDictionary(). Первое выражение указывает, какое поле сделать ключом, второе - что выбрать в качестве значения. В данном случае в качестве значения выбираем саму сущность пользователя.
public int GetMaxID()
{
return (from u in users
select u.ID).Max();
}
Выбираем максимальное значение поля ID сущности пользователя в коллекции.
public List<User> GetOrderedUsers()
{
return (from u in users
orderby u.ID
select u).ToList();
}
Сортируем пользователей по их идентификатору при помощи конструкции orderby.
public List<User> GetDescendingOrderedUsers()
{
return (from u in users
orderby u.ID descending
select u).ToList();
}
Обратная сортировка пользователей. После orderby добавлено ключевое слово descending.
public List<User> GetReversedUsers()
{
return (from u in users
orderby u.ID
select u).Reverse().ToList();
}
Здесь метод Reverse() возвращает итератор, который проходит по коллекции от конца в начало. Результат работы данного метода идентичен предыдущему результату (обратная сортировка).
public List<User> GetUsersPage(int pageSize, int pageIndex)
{
return (from u in users
select u).Skip(pageIndex * pageSize).Take(pageSize).ToList();
}
И наконец, пример реализации пейджинга, т.е. постраничного вывода элементов коллекции. Метод Skip() пропускает указанное количество элементов от начала, а Take() включает в выборку то количество элементов с текущей позиции, которое максимально возможно на одной странице.

Обратите внимание, что не отложенные операции я поместил в самом конце выражений. Все операции перед ними являются отложенными, т.е. не выполняются сразу. Их выполнение инициирует не отложенная операция.

В этой заметке я постарался рассмотреть самые распространённые конструкции LINQ to Objects. Теперь вы сможете довольно быстро разобраться и в других разновидностях LINQ.

Загрузить архив с примерами можно здесь.

P.S. Если возникнут вопросы как писать запросы с другими методами LINQ, пишите здесь в комментариях, постараюсь помочь оперативно.

21 октября 2012 г.

Как вывести список на экран одной строкой в C#

Абсолютно в каждом приложении рано или поздно может потребоваться отобразить содержимое некой коллекции (чаще всего списка) на экран. Стандартный способ, доступный в большинстве объектно-ориентированных языков, таких как C# или Java, является использование цикла for или его более частного случая foreach:
foreach (SomeClass s in someCollection)
{
Console.WriteLine(s.ToString());
}
Но это довольно громоздкая запись для столь простого действия. Писать целых пять строк, чтобы вывести одну! Поэтому я предлагаю использовать более компактное решение.

Итак, пусть SomeClass - это наш класс, в котором переопределён метод ToString (если вы его ещё не переопределили, то самое время это сделать). someCollection имеет тип List<SomeClass>. Тогда указанный выше код можно свести к одной строке:

someCollection.ForEach(Console.WriteLine);
Этой строки достаточно для того, чтобы каждый элемент был выведен в консоль в отдельной строке. Здесь используется делегат (delegate), он же "указатель на функцию" Console.WriteLine.

Более подробная запись этой же команды при помощи лямбда-выражения:
ForEach(i => { Console.WriteLine(i); })
То, что передаётся методу ForEach в качестве параметра - это анонимный метод, записанный кратко в виде лямбда-выражения. Этот метод принимает единственный параметр i, а затем возвращает то, что записано в фигурных скобках. Вместо вызова метода Console.WriteLine вы можете подставить то, что требуется именно вам. Например, добавление записи в лог или БД.

7 октября 2012 г.

Разбиение слова на слоги

Несколько лет назад я написал небольшую утилиту на C#, которой на вход подавалось слово, состоящее из русских букв, а на выходе она выдавала коллекцию слогов этого слова. Решил опубликовать код здесь - возможно, кому-нибудь пригодится. Сразу оговорюсь, что алгоритм не оптимальный, поскольку в то время я был гораздо менее опытным программистом) Если будет время - постараюсь его оптимизировать.

Основная идея алгоритма заключается в том, что граница между слогами находится посередине между согласными буквами, которые ограничены гласными. Рассмотрим слово "арбуз". В нём содержатся две гласные "а" и "у". Они ограничивают две согласные "рб". Согласно приведённому выше правилу, граница слогов находится между этими согласными. Поэтому получаем два слога: "ар-буз". Если согласных между гласными нечётное количество, прибавляем "лишнюю" согласную к первому слогу.

Далее приведу сам листинг. Комментарии приведены прямо в коде.

/// <summary>
/// Предоставляет статические методы для работы со слогами в слове
/// </summary>
public class SyllableParser
{
// строка с гласными
private static string vowel = "аеёиоуыэюя";
// строка с согласными
private static string consonant = "бвгджзйклмнпрстфхцчшщъь";

/// <summary>
/// Определяет тип буквы: Vowel (гласная) или Consonant (согласная);
/// в остальных случаях возвращается тип Unknown
/// </summary>
/// <param name="letter">исходная буква</param>
/// <returns></returns>
public static LetterType GetLetterType(char letter)
{
// переводим букву в нижний регистр для удобства
letter = Char.ToLower(letter);

// проверка на гласную букву
if (vowel.Contains(letter))
return LetterType.Vowel;
// проверка на согласную букву
if (consonant.Contains(letter))
return LetterType.Consonant;

// иначе неопределенная буква
return LetterType.Unknown;
}

/// <summary>
/// Разделяет строку, состоящую только из согласных, на две подстроки
/// </summary>
/// <param name="word">строка, состоящая только из согласных</param>
/// <param name="consonants">массив из двух элементов</param>
/// <returns>Массив строк из двух элементов</returns>
private static bool ExplodeConsonant(string word, out string[] consonants)
{
// создаем массив для будущих подстрок
consonants = new string[2];
string buf = "";

// проходим по слову до конца и пока буквы являются согласными
for (int i = 0; i < word.Length && GetLetterType(word[i]) == LetterType.Consonant; i++)
{
// добавляем очередную букву
buf += word[i];
}

// если длина буфера получилась меньше двух, то ОШИБКА
if (buf.Length < 2)
{
return false;
}

// если длина буфера является четной, то разделяем его на две части
// и помещаем их в массив
if (buf.Length % 2 == 0)
{
consonants[0] = buf.Substring(0, buf.Length / 2);
consonants[1] = buf.Substring(buf.Length / 2, buf.Length / 2);
}
// а если длина буфера нечетна, то первая часть будет на один символ больше,
// чем вторая
else
{
consonants[0] = buf.Substring(0, (buf.Length - 1) / 2 + 1);
consonants[1] = buf.Substring((buf.Length - 1) / 2 + 1, (buf.Length - 1) / 2);
}
return true; // процедура выполнена УСПЕШНО
}

/// <summary>
/// Разделяет слово по слогам
/// </summary>
/// <param name="word">исходное слово</param>
/// <returns>массив слогов или NULL в случае ошибки</returns>
public static string[] Parse(string word)
{
// проверка: строка должна быть длиной не менее двух символов
if (word == null || word.Length < 2)
{
return null;
}
// проверка: в строке могут быть только гласные или согласные буквы
foreach (char c in word)
{
if (GetLetterType(c) == LetterType.Unknown)
return null;
}

LetterType Old = GetLetterType(word[0]), Cur = LetterType.Unknown;
string preformat = word[0].ToString();
// посимвольно проходим по исходному слову и формируем его маску
for (int i = 1; i < word.Length; i++)
{
// тип текущей буквы
Cur = GetLetterType(word[i]);
// если предыдущая буква была гласная и текущая тоже гласная,
// то добавляем пробел в маску
if (Old == LetterType.Vowel && Cur == LetterType.Vowel)
{
preformat += " ";
}
// если предыдущая буква и текущая разные по типу, то также добавляем пробел
if (Old != Cur)
{
preformat += " ";
}
// предыдущий тип равен текущему
Old = Cur;
// прибавляем в преформат текущую букву
preformat += word[i];
}

// преобразуем преформат в массив, пробел является разделителем
string[] PreformatArray = preformat.Split(' ');

// результирующий массив никак не больше преформатного массива
string[] ResultArray = new string[PreformatArray.Length];
//
int k = 0;
// проходимся по каждому элементу преформатного массива
for (int i = 0; i < PreformatArray.Length; i++)
{
// если первая буква в текущем элементе - гласная...
if (GetLetterType(PreformatArray[i][0]) == LetterType.Vowel)
{
// ... и если в текущем элементе результирующего массива уже есть
// и гласная, и согласная...
if (HasConsonant(ResultArray[k]) && HasVowel(ResultArray[k]))
// ... то переходим к следующему элементу массива
k++;
// прибавляем к текущему элементу результирующего массива
// элемент преформатного массива
ResultArray[k] += PreformatArray[i];
}
// если первая буква в текущем элементе - согласная...
if (GetLetterType(PreformatArray[i][0]) == LetterType.Consonant)
{

string[] buf = null;

// ... пытаемся разделить текущий элемент, состоящий из согласных
// на два новых элемента массива
if (ExplodeConsonant(PreformatArray[i], out buf))
{
// в случае успеха прибавляем первый элемент массива к текущему слогу
ResultArray[k] += buf[0];
// если в текущем слоге есть и гласная и согласная, то
if (HasConsonant(ResultArray[k]) && HasVowel(ResultArray[k]))
// переходим к новому слогу
k++;
// и добавляем второй элемент к новому слогу
ResultArray[k] += buf[1];
}
else
{
// если разделение на два элемента произошло с ошибкой, то
// проверяем текущий слог на "полноценность" и, в случае истины,
// переходим к новому слогу
if (HasConsonant(ResultArray[k]) && HasVowel(ResultArray[k]))
k++;
ResultArray[k] += PreformatArray[i];
}
}
}

// далее идет процедура, которая проверяет, не является ли последний элемент
// результирующего массива длиной в 1 символ; если да, то склеиваем предпоследний
// слог с последним
int LastIndex = 0;

// проходим результирующий массив до тех пор, пока не встретим пустой элемент
for (int i = 0; i < ResultArray.Length; i++)
{
if (ResultArray[i] == null)
break;
else
LastIndex++;
}

// корректируем индекс последнего элемента
LastIndex--;

// если длина последнего значащего элемента массива равна 1 и последний индекс
// больше 1, то прибавляем последний элемент к предпоследнему
if (ResultArray[LastIndex].Length == 1 && LastIndex >= 1)
{
ResultArray[LastIndex - 1] += ResultArray[LastIndex];
ResultArray[LastIndex] = null;
}

return ResultArray;
}

/// <summary>
/// Определяет, есть ли в строке гласная буква
/// </summary>
/// <param name="word">исходное слово</param>
/// <returns>ИСТИНА, если имеет гласную, в противном случае ЛОЖЬ</returns>
public static bool HasVowel(string word)
{
if (word == null)
return false;
for (int i = 0; i < word.Length; i++)
{
if (GetLetterType(word[i]) == LetterType.Vowel)
return true;
}
return false;
}

/// <summary>
/// Определяет, есть ли в строке согласная буква
/// </summary>
/// <param name="word">исходное слово</param>
/// <returns>ИСТИНА, если в строке есть согласная, в противном случае ЛОЖЬ</returns>
public static bool HasConsonant(string word)
{
if (word == null)
return false;
for (int i = 0; i < word.Length; i++)
{
if (GetLetterType(word[i]) == LetterType.Consonant)
return true;
}
return false;
}
}

Перечисление с типами букв:

/// <summary>
/// Тип буквы (гласная или согласная)
/// </summary>
public enum LetterType
{
/// <summary>
/// Не гласная и не согласная
/// </summary>
Unknown,
/// <summary>
/// Гласная
/// </summary>
Vowel,
/// <summary>
/// Согласная
/// </summary>
Consonant
}