Работа с Java в командной строке

Опубликовано:

Сейчас уже никто не создает программы в консоли. Используя любимую IDE, разработчик чувствует себя неуютно за чужим компьютером, где её нет.
Решив разобраться в работе Ant и Maven, я поймал себя на том, что не смогу собрать приложение без них в консоли.
В данной статье я постарался уместить все этапы проектирования демонстрационного приложения, чтобы не искать справку по каждой команде на просторах Интернета.

От простого к ...

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

Один файл

Можно сделать и без лишних папок.
Берем сам файл.

publicclassHelloWorld
{
    publicstaticvoidmain(String[] args)
    {
        System.out.println("Hello World!");
        Calculator calc=newCalculator();
        System.out.println("2+2="+calc.sum(2,2));
    }
}

Переходим в каталог, где лежит данный файл, и выполняем команды.

    javac HelloWorld.java

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

    java -classpath . HelloWorld

 

Отделяем бинарные файлы от исходников


Теперь сделаем тоже самое, но с каталогами. Создадим каталог HelloWorld и в нем две папки src и bin.
Компилируем

    javac -d bin src/HelloWorld.java

Здесь мы указали, что бинарные файлы будут сохраняться в отдельную папку bin и не путаться с исходниками.

Запускаем

    java -classpath ./bin HelloWorld

 

Используем пакеты


А то, вдруг, программа перестанет быть просто HelloWorld-ом. Пакетам лучше давать понятное и уникальное имя. Это позволит добавить данную программу в другой проект без конфликта имен. Прочитав некоторые статьи, можно подумать, что для имени пакета обязательно нужен домен. Это не так. Домены — это удобный способ добиться уникальности. Если своего домена нет, воспользуйтесь аккаунтом на сайте (например, ru.habrahabr.mylogin). Он будет уникальным. Учтите, что имена пакетов должны быть в нижнем регистре. И избегайте использования спецсимволов. Проблемы возникают из-за разных платформ и файловых систем.

Поместим наш класс в пакет с именем com.qwertovsky.helloworld. Для этого добавим в начало файла строчку

    packagecom.qwertovsky.helloworld;

В каталоге src создадим дополнительные каталоги, чтобы путь к файлу выглядел так: src/com/qwertovsky/helloworld/HelloWorld.java.
Компилируем

    javac -d bin src/com/qwertovsky/helloworld/HelloWorld.java

В каталоге bin автоматически создастся структура каталогов как и в src.

    HelloWorld
    '---bin
    '   '---com
    '       '---qwertovsky
    '          '---helloworld
    '             '---HelloWorld.class
    '---src
        '---com
            '---qwertovsky
                '---helloworld
                    '---HelloWorld.java

Запускаем

    java -classpath ./bin com.qwertovsky.helloworld.HelloWorld

 

Если в программе несколько файлов


Изменим программу. Не обращайте внимание на логику. Её нет.

HelloWorld.java
packagecom.qwertovsky.helloworld;
publicclassHelloWorld
{
    publicstaticvoidmain(String[] args)
    {
        inta=2;
        intb=3;
        Calculator calc=newCalculator();
        System.out.println("Hello World!");
        System.out.println(a+"+"+b+"="+calc.sum(a,b));
    }
}
Calculator.java
packagecom.qwertovsky.helloworld;

importcom.qwertovsky.helloworld.operation.Adder;
publicclassCalculator
{
    publicintsum(int... a)
    {
        Adder adder=newAdder();
        for(inti:a)
        {
            adder.add(i);
        }
        returnadder.getSum();
    }
}
Adder.java
packagecom.qwertovsky.helloworld.operation;

publicclassAdder
{
    privateintsum;

    publicAdder()
    {
        sum=0;
    }

    publicAdder(inta)
    {
        this.sum=a;
    }

    publicvoidadd(intb)
    {
        sum+=b;
    }
    publicintgetSum()
    {
        returnsum;
    }
}

Компилируем

    javac  -d bin src/com/qwertovsky/helloworld/HelloWorld.java
    srccomqwertovskyhelloworldHelloWorld.java:9: cannot find symbol
    symbol  : class Calculator
    location: class com.qwertovsky.helloworld.HelloWorld
                Calculator calc=new Calculator();
                ^
    srccomqwertovskyhelloworldHelloWorld.java:9: cannot find symbol
    symbol  : class Calculator
    location: class com.qwertovsky.helloworld.HelloWorld
                Calculator calc=new Calculator();
                                    ^
    2 errors

Ошибка возникла из-за того, что для компиляции нужны файлы с исходными кодами классов, которые используются (класс Calculator). Надо указать компилятору каталог с файлами с помощью ключа -sourcepath.
Компилируем

    javac -sourcepath ./src -d bin src/com/qwertovsky/helloworld/HelloWorld.java

Запускаем

    java -classpath ./bin com.qwertovsky.helloworld.HelloWorld
    Hello Word
    2+3=5

 

Если удивляет результат


Есть возможность запустить отладчик. Для этого существует jdb.
Сначала компилируем с ключом -g, чтобы у отладчика была информация.

    javac -g -sourcepath ./src -d bin src/com/qwertovsky/helloworld/HelloWorld.java

Запускаем отладчик

    jdb -classpath bin -sourcepath src com.qwertovsky.helloworld.HelloWorld
    Initializing jdb ...
    >

Отладчик запускает свой внутренний терминал для ввода команд. Справку по последним можно вывести с помощью команды help.
Указываем точку прерывания на 9 строке в классе Calculator

    > stop at com.qwertovsky.helloworld.Calculator:9
    Deferring breakpoint com.qwertovsky.helloworld.Calculator:9.
    It will be set after the class is loaded.

Запускаем на выполнение.

    > run
    run com.qwertovsky.helloworld.HelloWorld
    Set uncaught java.lang.Throwable
    Set deferred uncaught java.lang.Throwable
    >
    VM Started: Set deferred breakpoint com.qwertovsky.helloworld.Calculator:9
    Hello World!
    Breakpoint hit: "thread=main", com.qwertovsky.helloworld.Calculator.sum(), line=9 bci=0
    9               Adder adder=new Adder();

Чтобы соориентироваться можно вывести кусок исходного кода, где в данный момент находится курссор.

    main[1] list
    5    public class Calculator
    6    {
    7       public int sum(int... a)
    8       {
    9 =>            Adder adder=new Adder();
    10              for(int i:a)
    11              {
    12                      adder.add(i);
    13              }
    14              return adder.getSum();

Узнаем, что из себя представляет переменная а.

    main[1] print a
     a = instance of int[2] (id=340)
    main[1] dump a
     a = {
    2, 3
    }
    main[1] stop at com.qwertovsky.helloworld.operation.Adder:19
    Deferring breakpoint com.qwertovsky.helloworld.operation.Adder:19.
    It will be set after the class is loaded.

Продолжим исполнение.

    main[1] cont
    > Set deferred breakpoint com.qwertovsky.helloworld.operation.Adder:19

    Breakpoint hit: "thread=main", com.qwertovsky.helloworld.operation.Adder.add(), line=19 bci=0
    19              sum+=b;
    main[1] list
    15      }
    16
    17      public void add(int b)
    18      {
    19 =>           sum+=b;
    20      }
    21
    22      public int getSum()
    23      {
    24              return sum;
    main[1] print sum
     sum = 0
    main[1] print b
     b = 2

Выполним код в текущей строке и увидим, что sum стала равняться 2.

    main[1] step
    >
    Step completed: "thread=main", com.qwertovsky.helloworld.operation.Adder.add(), line=20 bci=10
    20      }
    main[1] print sum
     sum = 2

Поднимемся из класса Adder в вызвавший его класс Calculator.

    main[1] step up
    >
    Step completed: "thread=main", com.qwertovsky.helloworld.Calculator.sum(), line=10 bci=36
    10              for(int i:a)

Удаляем точку прерывания

    main[1] clear com.qwertovsky.helloworld.operation.Adder:19
    Removed: breakpoint com.qwertovsky.helloworld.operation.Adder:19
    main[1] step
    >
    Step completed: "thread=main", com.qwertovsky.helloworld.Calculator.sum(), line=12 bci=30
    12                      adder.add(i);

Можно избежать захода в методы, используя команду next.

    main[1] next
    >
    Step completed: "thread=main", com.qwertovsky.helloworld.Calculator.sum(), line=10 bci=36
    10              for(int i:a)
    main[1] next
    >
    Step completed: "thread=main", com.qwertovsky.helloworld.Calculator.sum(), line=14 bci=42
    14              return adder.getSum();

Проверяем значение выражения и завершаем выполнение.

    main[1] eval adder.getSum()
     adder.getSum() = 5
    main[1] cont
    > 2+3=5
    The application exited

 

Хорошо бы протестировать

Используем JUnit.

packagecom.qwertovsky.helloworld;

importstaticorg.junit.Assert.*;

importjava.util.Arrays;
importjava.util.Collection;

importorg.junit.Test;
importorg.junit.runner.RunWith;
importorg.junit.runners.Parameterized.Parameters;

@RunWith(value=org.junit.runners.Parameterized.class)
publicclassTestCalculator
{
    intexpected;
    int[] arg;

    @Parameters
    publicstaticCollection<int[][]> parameters()
    {

        returnArrays.asList(newint[][][]{
                {{4}, {2, 2}}
                ,{{-1},{4, -5}}
                ,{{0},{0,0,0}}
                ,{{0},{}}
                });
    }

    publicTestCalculator(int[] expected, int[] arg)
    {
        this.expected=expected[0];
        this.arg=arg;
    }
    @Test
    publicvoidtestSum()
    {
        Calculator c=newCalculator();
        assertEquals(expected,c.sum(arg));
    }
}

Компилируем

    mkdir test_bin
    javac  -classpath lib/path/junit-4.8.2.jar  -sourcepath ./src -d test_bin test/com/qwertovsky/helloworld/TestCalculator.java

Запускаем. В качестве разделителя нескольких путей в classpath в Windows используется ';', в Linux — ':'. В консоли Cygwin не работают оба разделителя. Возможно, должен работать ';', но он воспринимается как разделитель команд.

    java  -classpath lib/path/junit-4.8.2.jar:./test_bin  org.junit.runner.JUnitCore com.qwertovsky.helloworld.TestCalculator
    JUnit version 4.8.2
    ....
    Time: 0,031
    OK (4 tests)

 

Создадим библиотеку

Класс Calculator оказался полезным и может быть использован во многих проектах. Перенесем всё, что касается класса Calculator в отдельный проект.

    HelloWorld
    '---bin
    '---src
        '---com
            '---qwertovsky
                '---helloworld
                    '---HelloWorld.java
    Сalculator
    '---bin
    '---src
    '   '---com
    '       '---qwertovsky
    '           '---calculator
    '               '---Calculator.java
    '               '---operation
    '                   '---Adder.java
    '---test
        '---com
            '---qwertovsky
                '---calculator
                    '---TestCalculator.java
    

Измените также назавания пакетов в исходных текстах. В HelloWorld.java нужно будет добавить строку

    importcom.qwertovsky.calculator.Calculator;

Компилируем.

    cd Calculator
    javac -sourcepath src -d bin src/com/qwertovsky/calculator/Calculator.java

Делаем архив jar

    jar cvf calculator.jar -C bin .
    added manifest
    adding: com/(in = 0) (out= 0)(stored 0%)
    adding: com/qwertovsky/(in = 0) (out= 0)(stored 0%)
    adding: com/qwertovsky/calculator/(in = 0) (out= 0)(stored 0%)
    adding: com/qwertovsky/calculator/Calculator.class(in = 497) (out= 373)(deflated 24%)
    adding: com/qwertovsky/calculator/operation/(in = 0) (out= 0)(stored 0%)
    adding: com/qwertovsky/calculator/operation/Adder.class(in = 441) (out= 299)(deflated 32%)

С помощью ключа -C мы запустили программу в каталоге bin.

Надо узнать, что у библиотеки внутри


Можно распаковать архив zip-распаковщиком и посмотреть, какие классы есть в библиотеке.
Информацию о любом классе можно получить с помощью дизассемблера javap.

    javap -c -classpath calculator.jar com.qwertovsky.calculator.Calculator
    Compiled from "Calculator.java"
    public class com.qwertovsky.calculator.Calculator extends java.lang.Object{
    public com.qwertovsky.calculator.Calculator();
      Code:
       0:	aload_0
       1:	invokespecial	#1; //Method java/lang/Object."<init>":()V
       4:	return

    public int sum(int[]);
      Code:
       0:	new	#2; //class com/qwertovsky/calculator/operation/Adder
       3:	dup
       4:	invokespecial	#3; //Method com/qwertovsky/calculator/operation/Adder."<init>":()V
       7:	astore_2
       8:	aload_1
       9:	astore_3
       10:	aload_3
       11:	arraylength
       12:	istore	4
       14:	iconst_0
       15:	istore	5
       17:	iload	5
       19:	iload	4
       21:	if_icmpge	42
       24:	aload_3
       25:	iload	5
       27:	iaload
       28:	istore	6
       30:	aload_2
       31:	iload	6
       33:	invokevirtual	#4; //Method com/qwertovsky/calculator/operation/Adder.add:(I)V
       36:	iinc	5, 1
       39:	goto	17
       42:	aload_2
       43:	invokevirtual	#5; //Method com/qwertovsky/calculator/operation/Adder.getSum:()I
       46:	ireturn
    }

Из результата видно, что класс содержит кроме пустого конструктора, ещё один метод sum, внутри которого в цикле вызывается метод add класса Adder. По завершении метода sum, вызывается Adder.getSum ().
Без ключа -c программа выдаст только список переменных и методов (если использовать -private, то всех).

    javap -private  -classpath calculator.jar com.qwertovsky.calculator.operation.Adder
    Compiled from "Adder.java"
    public class com.qwertovsky.calculator.operation.Adder extends java.lang.Object{
        private int sum;
        public com.qwertovsky.calculator.operation.Adder();
        public com.qwertovsky.calculator.operation.Adder(int);
        public void add(int);
        public int getSum();
    }

 

Лучше снабдить библиотеку документацией

Изменим для этого класс калькулятора.

packagecom.qwertovsky.calculator;

importcom.qwertovsky.calculator.operation.Adder;

/**
 * Калькулятор, который умеет складывать
 * @authorQwertovsky
 *
 */
publicclassCalculator
{
    /**
     * Определение суммы слагаемых
     * @parama массив слагаемых
     * @returnсумма
     */
    publicintsum(int... a)
    {
        Adder adder=newAdder();
        for(inti:a)
        {
            adder.add(i);
        }
        returnadder.getSum();
    }
}

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

    mkdir doc
    javadoc -d doc -charset utf-8  -sourcepath src -author -subpackages com.qwertovsky.calculator

В результате получиться следующее
Calculator_388_292

Можно подписать jar-архив

Если требуется подписать свою библиотеку цифровой подписью, на помощь придут keytool и jarsigner.
Генерируем подпись.

    keytool -genkey -keyalg rsa -keysize 2048 -alias qwertokey -keystore path/to/qwerto.keystore
    Enter keystore password:
    Re-enter new password:
    What is your first and last name?
      [Unknown]:  Valery Qwertovsky
    What is the name of your organizational unit?
      [Unknown]:  Qwertovsky
    What is the name of your organization?
      [Unknown]:  Qwertovsky
    What is the name of your City or Locality?
      [Unknown]:  Tver
    What is the name of your State or Province?
      [Unknown]:  Tverskaya obl.
    What is the two-letter country code for this unit?
      [Unknown]:  RU
    Is CN=Valery Qwertovsky, OU=Qwertovsky, O=Qwertovsky, L=Tver, ST=Tverskaya	obl., C=RU correct?
      [no]:  y
    Enter key password for <qwertokey>
            (RETURN if same as keystore password):
    Re-enter new password:


Генерируем Certificate Signing Request (CSR)

    keytool -certreq -file path/to/qwertokey.crt -alias qwertokey -keystore path/to/qwerto.keystore

Содержимое полученного файла отправляем в центр сертификации. От центра сертификации получаем сертификат. Сохраняем его в файле (например, qwertokey.cer) и импортируем в хранилище

    keytool -import -trustcacerts -keystore path/to/qwert.keystore -alias qwertokey -file path/to/qwertokey.cer

Подписываем jar-архив

    jarsigner -keystore path/to/qwerto.keystore calculator.jar qwertokey

Файл qwertokey.cer отправляем всем, кто хочет проверить архив. Проверяется он так

    jarsigner -verify -verbose -certs -keystore path/to/qwerto.keystore calculator.jar

 

Использование библиотеки


Есть программа HelloWorld, которая использует библиотечный класс Calculator. Чтобы скомпилировать и запустить программу, нужно присоединить библиотеку.
Компилируем

    cd HelloWorld
    javac -sourcepath src -d bin -classpath path/to/calculator.jar src/com/qwertovsky/helloworld/HelloWorld.java

Запускаем

    java -classpath bin:path/to/calculator.jar com.qwertovsky.helloworld.HelloWorld

 

Собираем программу

Это можно сделать по-разному.

Первый способ

 

    cd HelloWorld
    echo main-class: com.qwertovsky.helloworld.HelloWorld>manifest.mf
    echo class-path: lib/calculator.jar >>manifest.mf
    mkdir lib
    cp path/to/calculator.jar lib/calculator.jar
    jar -cmf manifest.mf helloworld.jar  -C bin .

Здесь есть тонкости.
В строке

    main-class: com.qwertovsky.helloworld.HelloWorld

не должно быть пробелов в конце.
Вторая тонкость описана в [3]: в этой же строке должен стоять перенос на следующую строку. Это если манифест помещается в архив сторонним архиватором.
Программа jar не включит в манифест последнюю строку из манифеста, если в конце не стоит перенос строки.
Ещё момент: в манифесте не должно быть пустых строк между строками. Будет выдана ошибка «java.io.IOException: invalid manifest format».

При использовании команды echo надо следить только за пробелом в конце строки с main-class.

Второй способ

 

    cd HelloWorld
    echo class-path: lib/calculator.jar >manifest.mf
    mkdir lib
    cp path/to/calculator.jar lib/calculator.jar
    jar -cmef manifest.mf com.qwertovsky.helloworld.HelloWorld  helloworld.jar  -C bin .

В данном способе избегаем ошибки с пробелом в main-class.

Третий способ

 

    cd HelloWorld
    mkdir lib
    cd lib
    jar -xvf path/to/calculator.jar com/
      created: com/
      created: com/qwertovsky/
      created: com/qwertovsky/calculator/
     inflated: com/qwertovsky/calculator/Calculator.class
      created: com/qwertovsky/calculator/operation/
     inflated: com/qwertovsky/calculator/operation/Adder.class
    cd ..
    cp  -r bin/* lib/
    jar -cef com.qwertovsky.helloworld.HelloWorld  helloworld.jar  -C lib .
    rm -r lib

Включили код нужной библиотеки в исполняемый файл.

Запуск исполняемого jar-файла

Файл calculator.jar исполняемым не является. А вот helloworld.jar можно запустить.
Если архив был создан первыми двумя способами, то рядом с ним в одном каталоге должна находится папка lib с файлом calculator.jar. Такие ограничения из-за того, что в манифесте в class-path указан путь относительно исполняемого файла.

    cd Calculator
    ls ../HelloWorld/lib
    calculator.jar
    java -jar ../HelloWorld/helloworld.jar

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

    java -jar ../HelloWorld/helloworld.jar

 

Как быть с приложениями JavaEE

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

     javac -classpath path/to/jboss/common/lib/jboss-servlet*.jar  -d ./classes src/com/qwertovsky/app/servlets/MenuSt.java


Структура архива JavaEE-приложения должна соответствовать определенному формату. Например

    my.ear
    `---META-INF
    |   `---manifest.mf
    `---lib
    |   `---mylib.jar
    `---my.war
    |   `---META-INF
    |   |   `---manifest.mf
    |   `---WEB-INF
    |   |   `---lib
    |   |   |   `---myweblib.jar
    |   |   `---classes
    |   |   |   `---com
    |   |   |       `---...
    |   |   `---web.xml
    |   `---index.html
    |   `---<остальноевеб-содержимое (страницы, изображения)>
    `---myejb.jar

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

Надеюсь, данная статья станет для кого-нибудь шпаргалкой для работы с Java в командной строке. Данные навыки помогут понять содержание и смысл Ant-скриптов и ответить на собеседовании на более каверзные вопросы, чем «Какая IDE Вам больше нравится?».

Понравилась статья, расскажи о ней друзьям, нажми кнопку!