Skip to content

Простое spring приложение (собрано shell скриптом)

Синопсис

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

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

Для этого примера на понадобится:десятков уровнез этой внешней среды будут и

  1. JDK
  2. Библиотеки spring версии 4.0.5: spring-aop-4.0.5.RELEASE.jar, spring-beans-4.0.5.RELEASE.jar, spring-context-4.0.5.RELEASE.jar, spring-core-4.0.5.RELEASE.jar, spring-expression-4.0.5.RELEASE.jar для компиляции spring-приложения. Ссылка чуть ниже
  3. Библиотека логирования commons-logging-1.1.3.jar для выполнения spring-приложения. Ссылка чуть ниже
  4. Командная строка для запуска shell скрипта, который мы напишем сами
  5. Элементарные знания из курса геометрии 😉

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

А какой конкретно метод для вычисления площади вызовется будет решается во время выполнения приложения и зависит от типа чайлда на который указывает сслыка. То есть если придет объект типа Figure по ссылке на Circle тогда дернется метод Circle.square(), если придет ссылка на Rectangle типа Figure, дернется метод Rectangle.square().

А теперь рассмотрим два подхода проектирования этого приложения, правильный и неправильный и начнем с неправильного.

Неправильный подход

Структура проекта

Создадим каталог simple-bad-app и в нем создадим такую структуру приложения:

simple-bad-app
    ├──src
    │   └──com
    │       └──devblogs
    │           ├──component
    │           |   ├──figure
    │           |   |   ├─Figure.java     
    │           |   |   ├─Circle.java
    │           |   |   └─Rectangle.java
    │           |   └──Print.java
    │           └──execute
    │               └──Execute.java
    └──build.sh

Потом, когда мы добавим все необходимые java файлы и запустим build.sh скрипт, в текущем каталоге появится каталог build:

build
  └──com
      └──devblogs
          ├──component
          |   ├──figure
          |   |   ├──Figure.class
          |   |   ├──Circle.class
          |   |   └──Rectangle.class
          |   └──Print.class
          └──execute
              └──Execute.class

Java code

Создадим абстрактный класс Figure и два конкретных класса, которые наследуются от Figure: Circle и Rectangle.

Figure.java

package com.devblogs.component.figure;
 
public abstract class Figure {
    private String name;
 
    public Figure(String name) {
        this.name = name;
    }
 
    public String getName() {
        return this.name;
    }
 
    public abstract double square();
}

Circle.java

package com.devblogs.component.figure;
 
public class Circle extends Figure {
    private int radius;
    public static double PI = 3.1415;
     
    public Circle(String name, int radius) {
        super(name);
        this.radius = radius;
    }
 
    @Override
    public double square() {
        return PI*this.radius*this.radius;
    }
}

Rectangle.java

package com.devblogs.component.figure;
 
public class Rectangle extends Figure {
    private int width;
    private int height;
 
    public Rectangle(String name, int width, int height) {
        super(name);
        this.width = width;
        this.height = height;
    }
 
    @Override
    public double square() {
        return this.width*this.height;
    }
}

На данный момент есть три класса: Circle и Rectangle — это два независимых компонента. Почему два независимых, потому что они существуют отдельно друг от друга и каждый со своей логикой; у Circle логика вычисления окружности это пи эр квадрат, у Rectangle логика вычисления прямоугольника это произведение длины на ширину.
И есть класс Figure который их обобщает (или абстрагирует). Класс Figure нужен для того, чтобы вынести общую логику для классов Circle и Rectangle наверх, а в классах Circle и Rectangle оставить только ту, которая касается непосредственно окружности или прямоугольника. Кроме того, через объект класса Figure будет предоставляться доступ к одному из объектов Circle или Rectangle (в зависимости от того, на какой из них будет установлена ссылка времени выполнения) через обобщенные методы. Таким образом вся работа с объектами Circle и Rectangle будет вестись только через объект Figure, мы никогда не будем обращаться к Circle и Rectangle напрямую.

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

Print.java

package com.devblogs.component;
 
import com.devblogs.component.figure.Figure;
 
public class Print {
    private Figure figure;

    public void setFigure(Figure figure) {
        this.figure = figure;
    }    
 
    public void showSquare() {
        System.out.println("Square of " + this.figure.getName() + " is " + this.figure.square());
    }
}

Последний класс который мы добавим будет Execute. Этот класс объединяет все компоненты вместе, он же будет энтерпоинтом. Как я писал выше, все классы, которые учавствуют в вычислении и возвращении площади фигуры являются компонентами. Что это значит? Это значит то, что мы разложили логику приложения по полочкам, где в каждой полочке лежит отдельный компонент с конкретной логикой, а объект Execute будет брать эти компоненты с полочек и связывать их вместе. Другими словами каждый компонент лежащий в своей полочке отвечает за конкретное действие, которое не пересекается с действиями других компонентов, то есть они не наступают друг другу на пятки, и работают вместе как тим. В нашем примере всего три таких компонента, это Circle, Rectangle и Print. У всех трех есть строго свое предназначение.
Перечислим коротко о предназначениях каждого компонента.

  1. У компонента Circle предназначение возвращать свое имя и площадь окружности по формуле пи эр квадрат.
  2. У компонента Rectangle предназначение возвращать свое имя и площадь прямоугольника по формуле произведение длины на ширину.
  3. У компонента Print предназначение запрашивать у фигуры площадь и название и выводить запрошенную информацию в поток.

А предназначение класса Execute (это уже не компонент) оперирывать этими компонентами. То есть если эти компоненты существуют сами по себе, то они безполезны, компонент Print ничего не может вывести из поля Figure если там ничего нет (там null), а компоненты Circle и Rectangle сами по себе ничего не выводят (мы пока отгородимся от всяких там toString()), они лишь только могут вычислять свою площадь, и возвращать результат, но пока они ни с каким компонентом не связаны, вычислять площадь попросту не для кого. Реальная польза от них только тогда когда они работают вместе, например когда компонент Print запрашивает площадь у компонента Circle, иначе говоря когда объект Execute кладет компонент Circle в компонент Print через сетер, чтобы компонент Print смог запросить у компонента Circle площадь, и вывести ее в поток.

Приведем класс Execute.java который связывает компонеты вместе:

Execute.java

package com.devblogs.execute;

import com.devblogs.component.Print;
import com.devblogs.component.figure.Circle;
import com.devblogs.component.figure.Rectangle;

public class Execute {
    public static void main(String [] args) {
        Print print = new Print();
        print.setFigure(new Circle("circle", 5));
        print.showSquare();
    }
}

Компиляция и запуск

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

build.sh

PATH_TO_BUILD=`pwd`
 
# Удаляем каталог с предыдущим билдом
if test -d $PATH_TO_BUILD/build;
then
    rm -rf $PATH_TO_BUILD/build
fi
 
# Создаем новый каталог, куда будем помещать билд
mkdir $PATH_TO_BUILD/build
 
# Компилируем проект в каталог build
javac -d build $(find src/* | grep .java)
 
# Выполняем приложение
java -cp .:build com.devblogs.execute.Execute

Делаем его экзекьютэбл:

sudo chmod +x build.sh

Раним из командной строки:

./build.sh

И получаем предсказуемый вывод площади окружности:

Square of circle is 78.53750000000001

Проблемы связанные с неправильным подходом

Выше я уже писал, что мы должны взять объект типа Figure и не задумываясь о том, какова его природа, должны просто воспользоваться преимуществом полиморфизма, а именно вызвать метод square() и получить ответ из которого будет следовать, а что собственно за фигуру мы взяли, или другими словами, мы должны узнать, что это был за тип объекта уже после, то есть прочитать в выводе приложения. В принципе мы все так и делаем, однако не задуматься о типе объекта не получается, так как нам приходится указывать какой конкретно будет тип еще на этапе написания джава-плайнтекст класса, а значит мы знаем заранее какой будет вызываться метод (поэтому вывод предсказуемый).
Обратим внимание на десятую строчку в файле Execute.java. В ней мы задаем фигуру, площадь которой мы хотим получить. Чтобы узнать площадь не окружности, а прямоугольника, мы должны в этой строчке передать сетеру класса Print объект типа Rectangle то есть написать new Rectangle(«rectangle», 5, 5) вместо new Circle(«circle», 5) и что самое обидное, сделать мы это должны во время написания кода в классе Execute, с последующей перекомпиляцией, то есть создание экземпляра нужной фигуры захардкожено в классе Execute, а это не совсем то, что мы хотим.
Напомню, мы хотим не задумываться площадь чего сейчас метод showSquare() объекта Print покажет, так как задача приложения запрашивать логику вычисления площади какой-то фигуры, но приложение не должно иметь никакого понятия, где эту логику взять, а вот где эту логику взять это задача для спринга.

Готовый проект неправильного проектирования можно скачать по ссылке:

или взять с gitHub
Распакуйте архив и запустите скрипт build.sh.

Правильный подход

Spring

Еще раз посмотрим на девятую и десятую строчки класса Execute. Помимо функции энтерпоинта и запроса вывести результат площади фигуры в поток, этот класс еще выполняет функцию менеджера компонентов. То есть в этом классе создаются компоненты Print и Circle которые в нем же связываются.

Что в этом подходе не так? Не так только то, что объекты Print и Circle создаются вручную операцией new и то что мы связываем объекты Circle и Print прямо в коде это тоже как бы не правильно. Но это только одна из проблем, которую мы сейчас разберем. Так а в чём тут проблема? Дело в том, что создание объектов операцией new подходят только для несложных простых программ, которые состоят из небольших объектов, не содержащих в себе другие объекты.
На практике программы состоят из множества объектов, которые в свою очередь включают в себя другие объекты, а поля тех, других объектов, в свою очередь могут включать еще другие объекты и такая глубина вложенности может состоять из нескольких уровней. Возьмем такой пример. Предположим у нас есть класс A который состоит из вложенных классов B, которые в свою очередь состоят из вложенных классов C etc. В коде это выглядит примерно так:

// Создание и инициализация объектов
A a = new A();
B1 b1 = new B1();
B2 b2 = new B2();
...
Bn bn = new Bn();
C1 c1 = new C1();
...
Cn cn = new Cn();
// Связующий код
b1.setC1(c1);
...
bn.setCn(cn);
a.setB1(b1);
a.setB2(b2);
...
a.setBn(bn);

Недостаток такого подхода виден сразу, нам приходится писать огромный связующий код.
Так вот, в чем идея спринга, чтобы не пришлось проходить по всем уровням рекурсивно и для каждого уровня создавать объекты операцией new, для этого нам нужен Spring. То есть со спрингом мы просто пишем классы объектов как обычно, а затем в конфигурационном файле спринга указываем какой объект куда вложить. С помощью спринга мы отделяем конфигурацию приложения от кода этого приложения или другими словами всю инициализацию объектов программы мы перекладываем на spring путем поднятия контекста спринга, а спринг уже выполнит всю инициализацию за нас. Таким образом создание инстанса одного из компонентов Circle или Rectangle и привязывание его к компоненту Print выносится в spring. Упрощенно это выглядит так: spring читает конфигурационный файл beans.xml (может называться по разному), находит там бины, которые и являются компонентами, поднимает их и кладет в объект Print посредством DI (dependency injection), это называется связыванием. В этом случае мы перекладываем функцию менеджера компонентов с класса Execute на контейнер Spring это называется инверсией контроля или Inversion of control — IoC, то есть другими словами выносим управление по созданию инстансов объектов из класса Execute во внешнюю среду, а объект Execute получает бины из этой внешней среды, то есть бины в него инжектятся, это называется dependency injection — DI. В этом случае класс Execute теперь выполняет только функцию энтерепоинта, поднимает контекст спринга и запрашивает вывод результата площади фигуры в поток у объекта Print. А объект Print теперь больше не парится к какому объекту методы getName() и square() фактически принадлежат, то ли к Circle то ли к Rectangle так как объект Print так же получает эти объекты от спринга и работает с этими объектами через интерфейс Figure (в данном случае Figure это абстрактный класс, но сейчас это не важно), а объект Execute больше не создает ни Circle ни Rectangle ни Print оператором new, возложив эту задачу на спринг.

Все вышесказанное наводит на мысль, что спринг в первом рассмотрении для нас является какбы гигантским фэктори-паттерном, который создаёт объекты, если надо связывает их, и выдаёт готовые объекты запрашиваемому классу.

Info
Чтобы проще было понять, что такое в спринге DI и IoC возьмем такой пример.
Допустим есть класс A который зависит от класса B, то есть класс A вызывает класс B.
Согласно подходу IoC создание инстанса класса B и управление им, осуществляется не самим классом A внутри его, а делегируется внешнему процессу, отсюда название Inversion of Control — IoC или инверсия контроля, то есть контроль над объектом B передается изнутри наружу (из класса A внешнему процессу то есть спрингу). Этот внешний процесс создает нистанс класса B и управляет им, то есть предоставляет его классу A это называется внедрение зависимости, то есть Dependency Injection — DI.

Структура проекта

Теперь рассмотрим структуру измененного проекта. В каталоге src все тоже самое что и в предыдущем проекте, кроме каталога resources, и lib. В каталоге resources добавился файл beans.xml который называется файлом конфигурации, в нем описаны бины, которые спринг будет поднимать. В каталоге lib лежат библиотеки спринга. Теперь создадим каталог simple-spring-app и добавим в него вот такую структуру проекта:

simple-spring-app
    ├──src
    |   └──com
    |       └──devblogs
    |           ├──component
    |           |   ├──figure
    |           |   |   ├──Figure.java
    |           |   |   ├──Circle.java
    |           |   |   └──Rectangle.java
    |           |   └─Print.java
    |           └──execute
    |              └─Execute.java
    ├──resources
    │   └─beans.xml
    ├──lib
    |   ├─commons-logging-1.1.3.jar
    |   ├─spring-aop-4.0.5.RELEASE.jar
    |   ├─spring-beans-4.0.5.RELEASE.jar
    |   ├─spring-context-4.0.5.RELEASE.jar
    |   ├─spring-core-4.0.5.RELEASE.jar
    |   └─spring-expression-4.0.5.RELEASE.jar
    └──build.sh

Потом, когда мы добавим все необходимые файлы в приложение и запустим build.sh скрипт, в текущем каталоге появится каталог build, со структурой собранного приложения:

build
  ├──com
  |   └──devblogs
  |       ├──component
  |       |   ├──figure
  |       |   |   ├──Figure.class
  |       |   |   ├──Circle.class
  |       |   |   └──Rectangle.class
  |       |   └──Print.class
  |       └──execute
  |           └──Execute.class
  └──beans.xml

Java code

Классы Figure, Circle, Rectangle и Print ни чем не отличаются от классов в предыдущем проекте:

Figure.java

package com.devblogs.component.figure;
 
public abstract class Figure {
    private String name;
     
    public Figure(String name) {
        this.name = name;
        System.out.println("Bean " + name + " has been created");
    }
 
    public String getName() {
        return this.name;
    }
 
    public abstract double square();
}

Circle.java

package com.devblogs.component.figure;
 
public class Circle extends Figure {
    private int radius;
    public static double PI = 3.1415;
    private String name;
      
    public Circle(String name, int radius) {
        super(name);
        this.radius = radius;
    }
  
    public double square() {
        return PI*this.radius*this.radius;
    }
}

Rectangle.java

package com.devblogs.component.figure;
 
public class Rectangle extends Figure {
    private int length;
    private int width;
    private String name;
      
    public Rectangle(String name, int length, int width) {
        super(name);
        this.length = length;
        this.width = width;
    }
 
    public double square() {
        return this.length*this.width;
    }
}

Print.java

package com.devblogs.component;
 
import com.devblogs.component.figure.Figure;
 
public class Print {
    private Figure figure;
 
    public Print() {
        System.out.println("Bean print is being created");
    }
 
    public void setFigure(Figure figure) {
        this.figure = figure;
    }
 
    public void showSquare() {
        System.out.println("Square of " + this.figure.getName() + " is " + this.figure.square());
    }
}

Связываем все компоненты вместе с помощью Spring

А вот в классе Execute кое-что изменилось. Теперь вместо создания компонентов Print и Circle в 10 строчке поднимается контекст спринга и из этого контекста, в строчке 11, достается бин по его айдишнику. В нашем случае из контекста достается бин Print. Контекст спринга создается из файла beans.xml который ищется по класспасу:

Execute.java

package com.devblogs.execute;
    
import com.devblogs.component.Print;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
    
public class Execute {
    public static void main(String [] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Print print = (Print) context.getBean("print");
        print.showSquare();
    }
}

Тут стоит обратить внимание на одну особенность ради чего мы взяли спринг — все что мы делаем, это достаем из контекста спринга бин Print и дергаем из него метод showSquare() который нам покажет площадь фигуры. И сразу появляется вопрос, покажет площадь какой фигуры? Ответ, той, которая подвязана к бину Print в данный момент, и это не обязательно может быть Circle. Дело в том, что когда спринг читает файл конфигурации beans.xml он создает в памяти инстансы всех перечисленных там бинов и связывает их так, как описано в файле контекста. Например, если в файле контекста перечисляются два бина circle и print, где бин circle вложен в бин print, то спринг создаст в памяти объект класса Print и положит в него объект класса Circle через сетер:

    <bean id="circle" class="com.devblogs.component.figure.Circle">
        <constructor-arg type="java.lang.String" value="circle"/>
        <constructor-arg type="int" value="5"/>
    </bean>
    <bean id="print" class="com.devblogs.component.Print">
        <property name="figure" ref="circle" />
    </bean>

В пропертях через атрибут name указывается сетер в классе Print, а в атрибуте ref указывается айдишник того бина, который нужно передать в сетер. Полностью файл конфигурации beans.xml выглядит так:

beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
 
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
 
    <bean id="circle" class="com.devblogs.component.figure.Circle">
        <constructor-arg type="java.lang.String" value="circle"/>
        <constructor-arg type="int" value="5"/>
    </bean>
 
    <bean id="rectangle" class="com.devblogs.component.figure.Rectangle">
        <constructor-arg type="java.lang.String" value="rectangle"/>
        <constructor-arg type="int" value="5"/>
        <constructor-arg type="int" value="5"/>
    </bean>
 
    <bean id="print" class="com.devblogs.component.Print">
        <property name="figure" ref="circle" />
    </bean>
   
</beans>

Положите файл конфигурации beans.xml в каталог resources, но это не значит, что там его место. Я напоминаю, его место в каталоге, на который указывает класспас в нашем случае это каталог build или текущий каталог скрипта (потому что мы указали ключу -cp значение .:build). Когда мы запустим скрипт он перенесет его из resources в build.

Сборка проекта

Чтобы собрать проект кроме всего прочего нужно еще в каталог lib подложить шесть библиотек:

  • spring-context-4.0.5.RELEASE.jar
  • spring-aop-4.0.5.RELEASE.jar
  • spring-core-4.0.5.RELEASE.jar
  • spring-beans-4.0.5.RELEASE.jar
  • spring-expression-4.0.5.RELEASE.jar
  • commons-logging-1.1.3.jar

  • которые можно взять здесь:

    Добавляем в корень проекта build.sh скрипт, код которого приводится ниже, назначьте его экзекьютэбл командой:

    sudo chmod +x build.sh
    

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

    build.sh

    PATH_TO_PROJECT=`pwd`
       
    # Удаляем каталог с предыдущим билдом
    if test -d $PATH_TO_PROJECT/build;
    then
        rm -rf $PATH_TO_PROJECT/build
    fi
        
    # Создаем новый каталог, куда будем помещать билд
    mkdir $PATH_TO_PROJECT/build
        
    # Ложим туда файл конфигурации beans.xml
    cp resources/beans.xml $PATH_TO_PROJECT/build
        
    # Компилируем проект в каталог build
    # Для компиляции достаточно библиотеки spring-2.5.5.jar
    javac -d build -cp .:lib/* $(find src/* | grep .java)
        
    # Выполняем приложение
    # Для выполнения приложения к библиотеке spring-2.5.5.jar
    # добавим еще библиотеку commons-logging-1.1.3.jar для логирования
    java -cp .:build:lib/* com.devblogs.execute.Execute
    

    В 22 строке в класспасе вместо -cp .:build:lib/* можно написать пути к библиотекам с которыми непосредственно работает приложение во время выполнения, то есть -cp .:build:lib/spring-2.0.6.jar:lib/commons-logging-1.1.3.jar.
    После того как скрипт добавлен и назначен выполняемым, осталось заранить его командой:

    ./build.sh
    

    Если после запуска скрипта вы увидите что-то похожее на это:

    июн 30, 2014 11:56:29 AM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
    INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@32d16dc8: startup date [Mon Jun 30 11:56:29 EEST 2014]; root of context hierarchy
    июн 30, 2014 11:56:29 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
    INFO: Loading XML bean definitions from class path resource [beans.xml]
    Bean circle has been created
    Bean rectangle has been created
    Bean print is being created
    Square of circle is 78.53750000000001
    

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

    Заключение

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

    Готовый проект простого спринг приложения можно скачать по ссылке:

    или взять с gitHub.

    Линки

    Хороший вводный материал:
    http://www.spring-source.ru/docs_simple.php
    Ресурсы для поиска артефактов:
    search.maven.org/
    mvnrepository.com

    Поделиться в социальных сетях

    Опубликовать в Google Plus
    Опубликовать в LiveJournal
    Опубликовать в Мой Мир
    Опубликовать в Одноклассники
    Опубликовать в Яндекс