Hello, Spring!

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

Центральная концепция фреймворка — IoC контейнер, управляющий объектами, и конфигурационный контекст (context), описывающий приложение и дополнительную функциональность.

Подготовка

Вначале создадим проект с помощью maven:

[INFO] Scanning for projects...
[INFO] BUILD SUCCESSFUL
[INFO] Total time: 28 seconds

И добавим к нему Spring и JUnit:

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <javaee.version>7.0</javaee.version>
        <org.springframework.version>4.1.7.RELEASE</org.springframework.version>
        <junit.version>4.12</junit.version>
        <hamcrest.version>1.3</hamcrest.version>
    </properties>




    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
        </dependency>


        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${org.springframework.version}</version>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
            <version>${hamcrest.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Помимо зависимостей, понадобятся ещё два плагина, один для сборки jar файла с зависимостями, другой для запуска интеграционных тестов:

   <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>ru.easyjava.spring.App</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.18.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Приложение

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

Начнём с монетки:

/**
* Coin, that could be tossed.
*/
public interface Coin {
    /**
     * Here we toss the coin.
     * @return unpredicted true of false.
     */
    boolean toss();
}
/**
* A simple implementation of Coin,
* based on Random class.
*/
@Service
public class CoinImpl implements Coin {
    /**
     * Random data source.
     */
    private Random random;


    /**
     * Simple constructor.
     * @param newRandom Supplied random generator.
     */
    @Inject
    public CoinImpl(final Random newRandom) {
        this.random = newRandom;
    }


    /**
     * Here we toss the coin.
     * @return unpredicted true of false.
     */
    @Override
    public final boolean toss() {
        return random.nextBoolean();
    }
}

Монетку надо покрыть юнит-тестами, но протестировать её не так просто, нам придётся написать свою собственую реализацию, дублёра Random, поведением которой мы сможем задавать. Такая реализация называется stub:

public class StubRandom extends Random {
    private boolean constantResult;


    public final void setConstantResult(final boolean newResult) {
        this.constantResult = newResult;
    }


    @Override
    public boolean nextBoolean() {
        return constantResult;
    }
}
public class CoinTest {
    @Test
    public void testToss() throws Exception {
        /** Prepare the mock */
        StubRandom random = new StubRandom();


        /** Prepare the object */
        Coin testedObject = new CoinImpl(random);


        /** Test it! */
        random.setConstantResult(true);
        assertTrue(testedObject.toss());


        random.setConstantResult(false);
        assertFalse(testedObject.toss());
    }
}

На основе броска монетки выберем, кого приветствовать:

/**
* Here we determine, who we are greeting today.
*/
public interface GreeterTarget {
    /**
     * Selects greeting target tossing a coin.
     * @return "World" or "Spring".
     */
    String get();
}
/**
* Simple implementation with
* hardcoded targets.
*/
@Service
public class GreeterTargetImpl implements GreeterTarget {
    /**
     * Coin, we toss to define greeting target.
     */
    private Coin coin;


    /**
     * Simple constructor.
     * @param newCoin Coin, that we will be tossing.
     */
    @Inject
    public GreeterTargetImpl(final Coin newCoin) {
        this.coin = newCoin;
    }


    /**
     * Selects greeting target tossing a coin.
     * @return "World" or "Spring".
     */
    @Override
    public final String get() {
        if (coin.toss()) {
            return "World";
        }
        return "Spring";
    }
}

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

public class StubCoin implements Coin {
    private boolean constantResult;


    public final void setConstantResult(final boolean newResult) {
        this.constantResult = newResult;
    }


    @Override
    public boolean toss() {
        return constantResult;
    }
}
public class GreeterTargetTest {
    @Test
    public void testGet() throws Exception {
        /* Prepare the mock */
        StubCoin coin = new StubCoin();


        /* Prepare the Object */
        GreeterTarget testedObject = new GreeterTargetImpl(coin);


        /* Test it! */
        coin.setConstantResult(true);
        assertEquals("World", testedObject.get());


        coin.setConstantResult(false);
        assertEquals("Spring", testedObject.get());
    }
}

Наконец, напишем сам класс приветствия:

/**
* Greeting service.
*/
@Service
public class Greeter {
    /**
     * Here we ask, who we are greeting.
     */
    private  GreeterTarget target;


    /**
     * Simple constructor.
     * @param newTarget Greeter target selector to use.
     */
    @Inject
    public Greeter(final GreeterTarget newTarget) {
        this.target = newTarget;
    }


    /**
     * Generates greeting.
     * @return "Hello-style" string.
     */
    public final String greet() {
        return "Hello " + target.get();
    }
}

Для теста сервиса приветствия придётся написать тестовую реализацию GreeterTarget:

public class StubGreeterTarget implements GreeterTarget {
    @Override
    public String get() {
        return "TEST";
    }
}
public class GreeterTest {
    @Test
    public void testGreet() throws Exception {
        /* Prepare the mock */
        GreeterTarget target = new StubGreeterTarget();


        /* Prepare the Object */
        Greeter testedObject = new Greeter(target);


        /* Test it! */
        assertEquals("Hello TEST", testedObject.greet());
    }
}

Проверим что все тесты успешны и перейдём непосредственно к Spring:

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running ru.easyjava.spring.CoinTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec - in ru.easyjava.spring.CoinTest
Running ru.easyjava.spring.GreeterTargetTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in ru.easyjava.spring.GreeterTargetTest
Running ru.easyjava.spring.GreeterTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in ru.easyjava.spring.GreeterTest


Results :


Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

Spring

Я сразу, при написании кода, пометил каждый класс аннотацией @Service, объявляющей класс Spring bean. С этой аннотацией жизненным циклом этих классов управляет Spring и нам не надо беспокоиться о их создании. Но чтобы Spring смог создать объекты этих классов, а в классах нет конструкторов по умолчанию, каждый конструктор имеет аннотацию @Inject.

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

Однако у внимательного читателя возникает вопрос: Greeter зависит от GreeterTarget, который создаст Spring. GreeterTarget зависит от Coin, которую тоже создась Spring. Coin зависит от Random, а кто создаст объект Random? Очевидно, что это должен быть Spring, но как? Это библиотечный класс и к нему нельзя добавить аннотацию @Service. Зато его можно создать вручную, в специальном конфигурационном классе:

/**
* Spring context configuration descriptor.
*/
@Configuration
@ComponentScan("ru.easyjava.spring")
public class ContextConfiguration {
    /**
     * "Random" service bean.
     * @return Java's built-in random generator.
     */
    @Bean
    public Random random() {
        return new Random();
    }
}

Конфигурационный класс, помеченный аннотацией @Configuration, настраивает контекст исполнения Spring, в том числе и добавляя к нему дополнительные Spring beans.

Аннотация  @ComponentScan(«ru.easyjava.spring»)  говорит Spring’у, что необходимо просканировать классы в пакете ru.easyjava.spring на наличие Spring аннотаций, таких как @Service, и обработать их.

Интеграционный тест

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

Spring включает в себя небольшой инструментарий для упрощения тестирования и, в частности, поддержку загрузки контекста в JUnit тестах. Используя специальный runner  SpringJUnit4ClassRunner, мы инициализируем Spring контест автоматически при запуске теста, а аннотация  @ContextConfiguration указывает, как именно мы хотим сконфигурировать контекст.

@ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes = ru.easyjava.spring.ContextConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class AppIT {


    @Inject
    private ApplicationContext context;


    @Test
    public void testSpring() {
        Greeter greeter = context.getBean(Greeter.class);


        assertTrue(greeter.greet().startsWith("Hello"));
    }
}

Обратите внимание, что класс интеграционного теста имеет суффикс *IT, а не *Test. По соглашению все классы имеющие суффикс *IT признаются maven’ом интеграционными тестами и запускаются отдельно от юнит-тестов:

mvn integration-test
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.323 sec - in ru.easyjava.spring.AppIT


Results :


Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Сразу видно, что даже такой простой тест исполняется на два порядке медленнее, чем модульный тест. Поэтому их и запускают отдельно.

Приложение

Тесты проходят, давайте запускать приложение:

/**
* Application main class.
*/
public final class App {
    /**
     * Do not construct me.
     */
    private App() { };


    /**
     * Application entry point.
     * @param args Array of command line arguments.
     */
    public static void main(final String[] args) {
        ApplicationContext context =
          new AnnotationConfigApplicationContext(ContextConfiguration.class);
        Greeter greeter = context.getBean(Greeter.class);


        System.out.println(greeter.greet());
    }
}

Разберём класс приложения детально:

        ApplicationContext context =
          new AnnotationConfigApplicationContext(ContextConfiguration.class);

Создаёт Spring context используя аннотации и Spring beans из  ContextConfiguration.

    
        Greeter greeter = context.getBean(Greeter.class);

Запрашивает из контекста bean типа Greeter. Стоит отметить, что класс к этому времени уже сконструирован, классы, от которых он зависит, тоже уже сконструированы и getBean только возвращает ссылку на существующий экземпляр.

В последней строке мы используем bean Greeter по назначению:

    
        System.out.println(greeter.greet());

Проверим результат:

>mvn verify
[INFO] ----------------------
[INFO] BUILD SUCCESS
[INFO] ----------------------
[INFO] Total time: 14.483 s
>java -jar target/hellospring-1-jar-with-dependencies.jar
9:21:14 PM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@7530d0a: startup date [Thu Jul 16 21:21:14 EEST 2015]; root of context hierarchy
9:21:14 PM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor <init>
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
Hello World


>java -jar target/hellospring-1-jar-with-dependencies.jar
9:21:18 PM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@7530d0a: startup date [Thu Jul 16 21:21:18 EEST 2015]; root of context hierarchy
9:21:18 PM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor <init>
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
Hello Spring

Мы видим как запускается IoC контейнер Spring и потом отрабатывает сервис приветствия, выдавая разные приветствия с каждым запуском. Всё получилось ????

Скачать код примера

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *