суббота, 24 февраля 2018 г.

Взаимодействие SpringBoot с RabbitMq

RabbitMQ - одно из популярных решений для отправки сообщений, предоставляется клиентские библиотеки для таких языков как Java, Scala, .NET, Go, Python, PHP  и другие. В данном турториале мы рассмотрим отправку и обработку сообщений, а так же как отправлять сообщение с JSON содержимым и работать с ошибками при помощи Dead Letter Queues (DLQ)

Для начала установим сервер RabbitMQ на вашей локальной машине, как указано здесь или запустим с помощью docker-compose:

version: '3'
services:
  rabbitmq:
    container_name: rabbitmq
    image: 'rabbitmq:management'
    ports:
      - "5672:5672"
      - "15672:15672"
 
Теперь можно запустить RabbitMQ используя docker-compose up и открыть UI администратора по адресу http://localhost:15672/.

Если вы знакомы с другими брокерскими службами обмена сообщениями, такими как ActiveMQ,  в них обычно используются очереди(Queues) и топики (Topics) для отправки индивидуальной и общедоступной модели связи. В RabbitMQ используется Exchange и в зависимости от ключа маршрутизации, сообщение будет отправлено в очередь (очереди) (Queues). Вы можете больше узнать о концепциях RabbitMQ здесь.

Приложение SpringBoot c RabbitMQ

Создадим SpringBoot приложение, описав следующие стартеры:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.sivalabs</groupId>
    <artifactId>springboot-rabbitmq-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RC1</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
      
</project>
Начнем с конфигурации RabbitMQ, создадим RabbitConfig и опишем следующие бины: Queue, Exchange и Binding:
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
  
@Configuration
public class RabbitConfig 
{
    public static final String QUEUE_ORDERS = "orders-queue";
    public static final String EXCHANGE_ORDERS = "orders-exchange";

    @Bean
    Queue ordersQueue() {
        return QueueBuilder.durable(QUEUE_ORDERS).build();
    }

    @Bean
    Queue deadLetterQueue() {
        return QueueBuilder.durable(QUEUE_DEAD_ORDERS).build();
    }
    @Bean
    Exchange ordersExchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_ORDERS).build();
    }

    @Bean
    Binding binding(Queue ordersQueue, TopicExchange ordersExchange) {
        return BindingBuilder.bind(ordersQueue).to(ordersExchange).with(QUEUE_ORDERS);
    }
}
Мы объявляем Queue имен orders-queue и Exchange с именем orders-exchange.
Также определили привязку между orders-queue и orders-exchange, чтобы любое сообщение, отправленное на orders-exchange с ключом направления как orders-queue, отправлялось в orders-queue.

Конфигурация RabbitMQ представлена в application.properties:

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
Создадим бин OrderMessageSender для отправки сообщений в orders-exchange. Spring Boot автоматически конфигурирует инфраструктурные бины, необходимые для отправки/обработки сообщений в/от брокера сообщений RabbitMQ. Нам необходимо просто проинжектить RabbitTemplate и отправить сообщение вызвав метод  rabbitTemplate.convertAndSend(“routingKey”, Object).

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Order implements Serializable{
    private String orderNumber;
    private String productId;
    private double amount;
}

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderMessageSender {
    private final RabbitTemplate rabbitTemplate;
    @Autowired
    public OrderMessageSender(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    public void sendOrder(Order order) {
        this.rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_ORDERS, order);

    }
}
По умолчанию в SpringBoot используется org.springframework.amqp.support.converter. SimpleMessageConverterand, который сериализует объект в byte[].
После отправки сообщения вы можете просмотреть сообщение из интерфейса администратора, выполнив вход с учетными данными guest/guest.

Теперь создадим слушателя, используя аннотацию RabbitListener.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import ru.config.Config;
import ru.model.Order;

@Component
public class OrderMessageListener {
    static final Logger logger = LoggerFactory.getLogger(OrderMessageListener.class);

    @RabbitListener(queues = Config.QUEUE_ORDERS)
    public void process(Order order){
        logger.info("Received order");
        logger.info(order.toString());
    }

}

Вот и все! Просто добавив @RabbitListener и определив, какую очередь слушать, мы можем создать слушателя.

Теперь, если вы отправляете сообщение в очередь, то с помощью метода OrderMessageListener.process(), вы обработаете данное сообщение и в логах приложения будет написано Received order и само содержимое объекта.

Отправка и прием сообщений c JSON содержимым.

Как мы видели, механизм сериализации по умолчанию преобразует объект сообщения в byte [] с помощью SimpleMessageConverter и на принимающей стороне, он будет десериализован из byte [] в тип объекта (в нашем случае Order) с помощью GenericMessageConverter.

Чтобы изменить это поведение, нам нужно настроить автоконфигурацию бинов Spring Boot RabbitMQ.

Отправить сообщение как JSON

Одним быстрым способом отправки сообщения с JSON является использование ObjectMapper, мы можем конвертировать Orderobject в JSON и отправлять его.
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.config.Config;
import ru.model.Order;

@Service
public class OrderMessageService {
    private final RabbitTemplate rabbitTemplate;
    private ObjectMapper objectMapper;

    @Autowired
    public OrderMessageService(RabbitTemplate rabbitTemplate, ObjectMapper objectMapper) {
        this.rabbitTemplate = rabbitTemplate;
        this.objectMapper = objectMapper;
    }

    public void sendOrder(Order order) throws Exception{
        String orderJson = objectMapper.writeValueAsString(order);
        Message message = MessageBuilder
                .withBody(orderJson.getBytes())
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .build();

        rabbitTemplate.convertAndSend(Config.QUEUE_ORDERS, order);
    }
}
Но преобразование объектов в JSON, как это, является своего рода избыточностью. Вместо этого мы можем сделать изменить конфигурацию, настроив компонент org.springframework.amqp.support.converter.Jackson2JsonMessageConverter, который будет использоваться в RabbitTemplate, чтобы сообщение было сериализовано как JSON вместо byte[].
@Configuration
public class RabbitConfig{
    ...
    ...

    @Bean
    public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
        final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
        return rabbitTemplate;
    }

    @Bean
    public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
Теперь сообщения отправляются в виде JSON. Так же необходимо настроить прием JSON сообщения. Для этого необходимо имплиментировать для нашего класса конфигурций интерфейс RabbitListenerConfigurer.
 @Configuration
public class RabbitConfig implements RabbitListenerConfigurer {
    ...
    ...
    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
        registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
    }
    @Bean
    MessageHandlerMethodFactory messageHandlerMethodFactory() {
        DefaultMessageHandlerMethodFactory messageHandlerMethodFactory = new DefaultMessageHandlerMethodFactory();
        messageHandlerMethodFactory.setMessageConverter(consumerJackson2MessageConverter());
        return messageHandlerMethodFactory;
    }
  
    @Bean
    public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
        return new MappingJackson2MessageConverter();
    }
}

Обработка ошибок и невалидных сообщений, используя DeadLetterQueues(DLQ)

Мы можем отправить невалидные сообщения в отдельную очередь, чтобы мы могли их проверять и обрабатывать позже. Для этого можно использовать концепцию DLQ для автоматизации, вместо написания кода и обработки такого события.
Мы можем объявить dead-letter-exchange, dead-letter-routing-key для очереди, тогда объявление бина будет следующим:

@Configuration
public class RabbitConfig implements RabbitListenerConfigurer {

    public static final String QUEUE_ORDERS = "orders-queue";
    public static final String EXCHANGE_ORDERS = "orders-exchange";
    public static final String QUEUE_DEAD_ORDERS = "dead-orders-queue";

    @Bean
    Queue ordersQueue() {
        return QueueBuilder.durable(QUEUE_ORDERS)
                .withArgument("x-dead-letter-exchange", "")
                .withArgument("x-dead-letter-routing-key", QUEUE_DEAD_ORDERS)
                .withArgument("x-message-ttl", 15000) //Если сообщение не бработано за 15 сек - оно отправляется в DLQ
                .build();

    }
 
    @Bean
    Queue deadLetterQueue() {
        return QueueBuilder.durable(QUEUE_DEAD_ORDERS).build();
    }

    ...
    ...
}

Теперь можно отправить сообщение с JSON отличным от объекта Order и оно попадет в очередь dead-orders-queue. Необходимо заметить, что если очередь orders-queue была создана - ее необходимо удалить, чтобы приложение запустилось.

PS это мой перевод данной статьи

среда, 22 ноября 2017 г.

Mongo Java driver

Рассмотрим простой пример приложения, которое обращается к Mongo. Содержимое оставим из предыдущих частей.
В зависимостях будет лишь драйвер для Mongo:
    <dependencies>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongo-java-driver</artifactId>
            <version>3.5.0</version>
        </dependency>
    </dependencies>
Необходимо заметить, что я использую драйвер 3.х.х. В долшинстве примеров, что я нашел, использовался драйвер версии 2.х.х, в 3 версии большинство используемых в примерах объектов отмечены ка устаревшие (Deprecated).

Для начала посчитаем количество записей в коллекции и выведем ее:

MongoClient mongo = new MongoClient( "localhost" , 27017 );

MongoDatabase database = mongo.getDatabase("test");
FindIterable<Document> unicorns = database.getCollection("employees").find();
long employeesCount = database.getCollection("employees").count();

System.out.println("count = " + employeesCount);
printD(id);

Функция для вывода коллекции:
private static void printD(Iterable<Document> objects){
 objects.forEach(document -> document.forEach((key, value) -> System.out.println(key + " = " + value)));
}
Создадим  новую запись:
        Document document = new Document();
        document.put("name", "Martin");
        document.put("hired", new Date());
        document.put("skills", "[smart, responsible]");
        document.put("age", 22);
        document.put("gender", "m");
        document.put("salary", 2468);
        database.getCollection("employees").insertOne(document);

        employeesCount = database.getCollection("employees").count();
        System.out.println("count = " + employeesCount);
Количество записей увеличилось на 1.  Попробуем обновить, созданную запись:
 Document find = new Document();
        find.put("name", "Martin");
        FindIterable<Document> beforeUpdate = database.getCollection("employees").find(find);
        printD(beforeUpdate);

        System.out.println("update:");

        Document update = new Document();
        Document set = new Document();
        update.put("salary", 2479);
        set.put("$set", update);
        database.getCollection("employees").updateMany(find, set);
        FindIterable<Document> updated = database.getCollection("employees").find(find);
        printD(updated);
Поиск записи по ИД. Внимание, я использую свой ИД, необходимо использовать именно ваш, сегенерированный ид:
 System.out.println("find by id");
        Document findById = new Document();
        findById.put("_id", new ObjectId("5a1525ea0f6ce550de6fc392"));
        FindIterable<Document> id = database.getCollection("employees").find(findById);
        printD(id);
Удалим созданный объект:
        database.getCollection("employees").deleteMany(find);
        employeesCount = database.getCollection("employees").count();
        System.out.println("count = " + employeesCount);

Количество записей уменьшилось на 1.

Полностью код приведен ниже:
package com;

import com.mongodb.MongoClient;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.bson.types.ObjectId;

import java.util.Date;
public class Application {

    public static void main(String[] args) {

        MongoClient mongo = new MongoClient( "localhost" , 27017 );

        MongoDatabase database = mongo.getDatabase("test");
        FindIterable<Document> unicorns = database.getCollection("employees").find();
        long employeesCount = database.getCollection("employees").count();
        System.out.println("count = " + employeesCount);
        printD(unicorns);

        Document document = new Document();
        document.put("name", "Martin");
        document.put("hired", new Date());
        document.put("skills", "[smart, responsible]");
        document.put("age", 22);
        document.put("gender", "m");
        document.put("salary", 2468);
        database.getCollection("employees").insertOne(document);

        employeesCount = database.getCollection("employees").count();
        System.out.println("count = " + employeesCount);


        Document find = new Document();
        find.put("name", "Martin");
        FindIterable<Document> beforeUpdate = database.getCollection("employees").find(find);
        printD(beforeUpdate);

        System.out.println("update:");

        Document update = new Document();
        Document set = new Document();
        update.put("salary", 2479);
        set.put("$set", update);
        database.getCollection("employees").updateMany(find, set);
        FindIterable<Document> updated = database.getCollection("employees").find(find);
        printD(updated);


        System.out.println("find by id");
        Document findById = new Document();
        findById.put("_id", new ObjectId("5a1525ea0f6ce550de6fc392"));
        FindIterable<Document> id = database.getCollection("employees").find(findById);
        printD(id);

        database.getCollection("employees").deleteMany(find);
        employeesCount = database.getCollection("employees").count();
        System.out.println("count = " + employeesCount);
      
    }

    private static void printD(Iterable<Document> objects){
        objects.forEach(document -> document.forEach((key, value) -> System.out.println(key + " = " + value)));
    }
}

среда, 8 ноября 2017 г.

MongoDb основные операции. Часть 3

Выбор полей

Команда find(), так же может принимать второй необязательный парметр, в котором перечисляются поля, которые будут в выборке. Так же необходимо заметить, что поле _id выбирается по умолчанию, если мы хотим его исключить - нужно это указать. Попробуем выбрать все имена:

db.employees.find({},{name:1, _id:0});
db.employees.find({},{name:1});
Второй вариант вернется так же поле _id.

Сортировка

Чтобы отсортировать результат к курсору find() добавляется sort, с очень похожими входными параметрами, необходимо указать поле и направление сортировки 1 - по возрастанию, -1 - по убыванию:

db.employees.find({},{name:1, _id:0}).sort({name:1});
Необходимо упомянуть, что если попытатьс отсортировать большой объем данных без индекса - мы получим ошибку.

Разбиение на страницы

Разбиение на страницы выполняется с помощью limit и skip, так же их можно использовать, чтобы избежать проблем при сортировке по полям, не содержащим индексы. Получим 4 и 5 имена:
db.employees.find({}, {name:1, _id:0}).sort({name:1}).limit(2).skip(3);

Подсчет записей

Консоль mongoDb позволяет выполнять count над коллекцией, некоторые драйвера не позволяют делать этого, в таком случае, count необходимо выполнить после find():

db.employees.find({}, {name:1, _id:0}.count();

воскресенье, 5 ноября 2017 г.

MongoDb основные операции. Часть 2

Обновление данных

Чтобы обновить данные воспользуемся, командой update, например добавил з/п для Бориса:
db.employees.update({name:'Boris'}, {salary:7000});
После обновления попробуем получить запись:
db.employees.find({name:'Boris'});
Запись не найдена, т.к. обычный update полностью заменяет документ. Чтобы обновлять только нужные поля необходимо воспользоваться модификатором $set:

db.employees.update({salary:7000}, {$set:{name: 'Boris', hired: new Date(2002,2,13,7,47), skills: ['smart','happy'], age: 60, gender: 'm'}});
Кроме модификатора $set есть другие, например, $inc увеличивает на заданную величину число или $push добавлет элемент в массив:

db.employees.update({name:'Boris'}, {$inc:{salary:200}, $push:{skills:'responsible'}});

Вставка/Обновление

Чтобы выполнить обновление или вствку записи при ее отсутствии необходимо передать третий параметр-флаг, true, например, на склад завезли канцелярские товары, а именно ручки:
db.storage.update({stationery:'pen'}, {$inc:{count: 200}},true)
При первом выполнении произойдет создание документы, а при втором - увеличение счетчика на 200 единиц.

Множественное обновление

Команда update обновляет только первую найденную запись, чтобы обновить все записи, удовлетворяющие заданному условию, необходимо установить четвертый параметр в true. Так же необходимо заметить, что множественное обновление работает только с модификатором, чтобы не произошло замены документов, только обновление полей:

db.employees.update({}, {$set:{isIll:false}}, false, true);

пятница, 3 ноября 2017 г.

MongoDb основные операции

1. Скачивание и запуск

Прежде чем рассматривать основные операции в MongoDb - скачаем ее с офф сайта. Разархивируем скачанный файл и перейдем в папку bin. Больше всего нас интересуют файлы: mongod - сервер и mondo - клиентская консоль. В папке bin, создадим конфигурационный файл mongo.config. В него добавим: dbpath=путь_куда_создать_базу_данных. Теперь, запустим mongod, в консоли выполним: mongod.exe --config mongo.config. Для проверки запустим  файл mongo и выполним db.version(). В результате получим версию своей MongoDb.

2. Выбор БД и ее наполнение

Запустим консоль -  mongo.  Она работает на JavaScript. Существует несколько глобальных команд, например exit или help. Большинство команд выполняются применительно к каому-либо объекту, например, выполним db.stats(), получим информацию о текущей базе данных:
Текущая база - test, сменим ее, выполнив: use learn:
При создании первой коллекции произойдет и создание БД. Создим несколько коллекций:
db.employees.insert({name: 'Boris', hired: new Date(2002,2,13,7,47), skills: ['smart','happy'], age: 60, gender: 'm', salary: 6300}); 
db.employees.insert({name: 'Aurora', hired: new Date(2011, 0, 24, 13, 0), skills: ['smart', 'responsible'], age: 45, gender: 'f', salary: 4300}); 
db.employees.insert({name: 'Tom', hired: new Date(2003, 1, 9, 22, 10), skills: ['energetic', 'enthusiast'], age: 34, gender: 'm', salary: 18200}); 
db.employees.insert({name: 'Rod', hired: new Date(2009, 7, 18, 18, 44), skills: ['serious'], age: 57, gender: 'm', salary: 9900}); 
db.employees.insert({name: 'Solnara', hired: new Date(2005, 6, 4, 2, 1), skills:['serious', 'smart', 'serious'], age:55, gender:'f', salary:8000}); 
db.employees.insert({name:'Kenny', hired: new Date(2007, 6, 1, 10, 42), skills: ['responsible', 'independent'], age: 39, gender: 'm', salary: 3900}); 
db.employees.insert({name: 'Ralf', hired: new Date(2005, 4, 3, 0, 57), skills: ['serious', 'independent'], age: 21, gender: 'm', salary: 200}); 
db.employees.insert({name: 'Leia', hired: new Date(2001, 9, 8, 14, 53), skills: ['serious', 'easily trained'], age: 40, gender: 'f', salary: 3300}); 
db.employees.insert({name: 'Bill', hired: new Date(2007, 2, 1, 5, 3), skills: ['serious', 'easily trained'], age: 35, gender: 'm', salary: 5400}); 
db.employees.insert({name: 'Nimue', hired: new Date(2009, 11, 20, 16, 15), skills: ['responsible', 'smart'], age: 54, gender: 'f'}); 
db.employees.insert({name: 'Max', hired: new Date(2006, 6, 18, 18, 18), skills: ['responsible', 'easily trained'], age: 44, gender: 'm', salary: 16500}); 
 Каждая команда вставляет в коллекцию employees перевадаваемый аргумент, который представляет из себя обычный JSON. Выполним команду поиска: db.employees.find(). В результате получим все введенные данные. У них повилось поле _id. У каждой записи должно быть уникальное поле, можно его генерировать самому или позволить это делать MongoDb. Поле _id, по умолчанию индексируется.
Когда у нас есть данные, можно начать осваивать выборки. Поиск происходит, так же как и вставка, ипользуя связку: {поле:значение}, так де есть операторы: $lt, $lte, $gt, $gte и $ne, обозначающие "меньше", "меньше или равно", "больше", "больше или равно", и "не равно". Попробуем выбрать всех людей, кто младше 30:
db.employees.find({"age":{$lte:30}})
Теперь попробуем выбрать тех, что младже 30 или у кого з/п выше 10000:
db.employees.find({$or:[{"age":{$lte:30}},{"salary":{$gte:10000}}]})
Если хотим выбрать только серьезных работников:
db.employees.find({"skills":'serious'})

вторник, 31 октября 2017 г.

MongoDb NoSql Общие сведения

1. Введение

MongoDb - документоорентированная NoSql база данных, используемая для хранения больших объемов информации, предоставляющая высокую производительность, высокую доступность и автоматическое масштабирование.

2. Что такое MongoDb?

  • MongoDb - документная база данных, каждая база данных содержит коллекции, которые в свою очередь содержат документы. Каждый документа может сосостоять из различного числа полей. Размер и содержимое каждого документа может быть различен и хранится в JSON подобном формате.
  • Строкам (или документам, если называть в стиле MongoDb), не нужно заранее созданной схемы. Вместо этого, поля можно создавать на лету.
  • Модель данных, доступная в MongoDb, позволяет разработчикам легче представлять иерархические отношения, хранить массивы и другие более сложные структуры

3. Что такое NoSql

БД NoSql обозначают не-SQL БД или без реляционный подход к БД.
  • Предоставляет механизм хранения и извлечения данных, отличных от табличной реляционной модели, используемой в реляционныз БД
  • Предоставляет большую гибкость, т.к. все записи не ограничены именами столбцов и типами, определенными во всей таблице.
  • Не использует стандартный язык SQL для запроса данных и не предусматривает строгую схему.
  • С NoSql, ACID ( Атомарность,Консистенция, Изоляция, Долговечность) свойства транзакций БД не гарантируются.
  • Предлагает масштабируемость и высокую производительность для обработки огромных объемов данных.

4. Почему использовать MongoDb

Ниже приведены несколько причин, почему, необходимо начать использовать MongoDb
  • Документо-ориентированность: т.к. MongoDb - NoSql БД, то вместо того, чтобы хранить данные в релационном формате - они хранятся в виде документа. Это делает MongoDb более гибкой.
  • Специальные запросы: MongoDb поддерживает поиск по полю,  диапазону и регулярным выражениям. Так же есть возможность вернуть поле документа.
  • Индексация: Индексы создаются, чтобы увеличить производительность поиска.
  • Автоматическая балансировка нагрузки: MongoDb использует концепцию Шардирования для горизонтального масштабирования , разбивая данные на нескольких экземплярах MongoDb.
  • Репликация: MongoDb может обеспечить высокую доступность набора реплик.

5. Различия с РСУБД

Ниже рассмотрены основные различия между NoSql и РСУБД.

  • В РСУБД используется термин Таблица, в MongoDb - Коллекция.  В РСУБД  таблица состоит из строк и столбцов, которые используются для хранения данныйх, в MongoDb, коллекции состоят документов, которые состоят из полей, которые состоят пары ключ-значение.
  • Строка  в РСУБД, Документ в MongoDb.
  • Столбец в РСУБД, поле в MongoDb.
  • Join в РСУБД, встроенные документы в MongoDb. В РСУБД часто данные хранятся в различных таблицах, чтобы показать полный набор данных - необходимо произвести Join таблиц. В MongoDb данные обычно хранятся в одной коллекции, но разделены встроенными документами, т.о Join, как концепция - отсутствует.
  • В РСУБД - Первичный ключ, в MongoDb - _id, который создается автоматически.

четверг, 26 октября 2017 г.

Java 9: Фабричный метод для создания неизменяемых коллекций

В данной статье рассмотрим новую фичу JDK 9 - создание неизменяемых коллекций. До Java9, если мы хотели получить неизменяемые коллекции - необходимо было вызвать unmodifiableXXX();  методы у java.util.Collections. Например, чтобы создать неизменяемый список, необходимо выполнить:

List<Strong> immutablelist = Collections.unmodifiableList(new ArrayList<String>(){{
            add("Smart");
            add("Techie");
}});

immutablelist.add("Smart_1");
При попытки добавлении новой записи "Smart_1" - получаем ошибку:

java.lang.UnsupportedOperationException thrown:
        at Collections$UnmodifiableCollection.add (Collections.java:1056)

Код выше - слегка многословен, чтобы создать немодифицированный список. Т.к. Java адоптирует функциональный стиль в Java 9 разработаны удобные, более компактные фабричные методы для создания немодифицируемых коллекций, как указано в JEP 269. Рассмотрим как это работает:

//Пустой список
List immutableList = List.of();
// Не пустой список
immutableList = List.of("Smart","Techie");
// Не пустая Map
Map immutableMap = Map.of(1,"Smart",2,"Techie");

 Если вы посмотрите на вышеупомянутый фабричный метод Java 9, код представляет собой простой однострочный набор для создания неизменяемых коллекций

PS это мой перевод данной стать.

среда, 11 октября 2017 г.

Инициализация базы данных в Docker

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

ENV INIT_SQL init.sql
ADD init.sql /docker-entrypoint-initdb.d/

Они копируют init.sql в докер контейнер. Выполнение данного скрипта произойдет только один раз, при мервом запуске.

воскресенье, 24 сентября 2017 г.

Запуск Postgres в Docker

Конфигурация docker файла:
FROM library/postgres
ENV POSTGRES_USER postgres
ENV POSTGRES_PASSWORD postgres
ENV POSTGRES_DB postgres

Для того, чтобы собрать контейнер, в консоли выполняем
sudo docker build  -t postgres .

В результате в консоль будет выводится процесс сборки контейнера, а в конце будет указан его идентификатор:
Этим идентификатором и воспользуемся для запуска:
sudo docker run 1f66d03cb1e7 & 
& используется для запуска в бэкграунде.  Выполнив:
sudo docker ps -a

Мы увидим, что наш контейнер запущен:

Чтобы понять, какой адрес у запущенной базы данных воспользуемся командой:
sudo docker inspect 0ba3681d3a3d
В консоль будет выведена вся информация о контейнере, в ней будет указан ip адрес, по нему и можно обращаться к базе:

понедельник, 18 сентября 2017 г.

Установка Docker на Ubuntu

Рассмотрим процесс установки docker, в Ubuntu, я использовал версию 17.04.
Первым делом обновим базу пакетов:
sudo apt-get update

Далее добавим ключ официального репозитория Docker в систему:
sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D

Добавим репозиторий Docker в список источников, для утилиты apt:
sudo apt-add-repository 'deb https://apt.dockerproject.org/repo ubuntu-xenial main'

Обновим информацию о пакетах из добавленного репозитория:
sudo apt-get update

Убедимся, что собираемся установить Docker из репозитория Docker, а не из репозитория по умолчанию Ubuntu:
apt-cache policy docker-engine

Результат должен быть похож на этот:
Установим Docker:
sudo apt-get install -y docker-engine

Проверим, запущен ли, демон:
sudo systemctl status docker

В результате получим:
Ну и запустил hello world:
sudo docker run hello-world

Т.к. это первый наш запуск, в начале произойдет загрузка имейджа:

суббота, 12 августа 2017 г.

Синглтон: глубокое погружение

Сингтон - паттерн проектирования, который используется, чтобы контролировать число создаваемых объектов, слодовательно он относится семейству паттернов Создатели.
Была тенденция создавать максимум один объект, но в некоторых ситуациях необходимо создать фиксированное число объектов; и именно в этом нам поможет данный паттерн. Обычно, конструктор создается приватным, чтобы убедиться, что внешний мир не может создать объекты и предоставляем один статический метод, который возвращает объект. Создание объекта происходит только если он не был создан до этого.
Со временем люди осознали несколькоп роблем с обычной реализацией синглтона и он был улучшен, чтобы решить эти проблемы. Запомните, что синглтон не хороший или плохой, он просто может подходить для решения вышей проблемы.
В этой статье мы рассмотрим различные реализации синглтонов.
Взглянем на диаграмму класса:

Применение синглтона

Есть несколько ситуаций, когда целесобразно использовать паттерн синглтон. На пример: логирование, кэширование, балансировка нагрузки, конфигурирование, коммуникации и пулл-коннекшинов в БД. Пример использования синглтона в Java API - класс Runtime.

Реализация синглтона

Приведем пример простой реализации синглтона. Инициализация произойдет при запуске приожения, так же данный способ является потокобезопасный.

public class MySingleton {
     private static final MySingleton mySingleton = new MySingleton();
     private MySingleton(){}     
     public static MySingleton getInstance(){
        return mySingleton;
     }
}
А вот реализация использующая инициализацию при первом обращении. В многопоточном приложении данная реализация будет плогих решением:

class MySingleton {
     private static MySingleton mySingleton;
     private MySingleton(){}
     public static MySingleton getInstance(){
           if(null == mySingleton) {
                 mySingleton = new MySingleton();
           }
           return mySingleton;
     }
}
Решение для многопоточной среды должо избежать гонки состояний  и убедиться, что не будет нарушения философии синглтона. Но в примере ниже, делать весь метод synchronized - не лучший подход, т.к нам необходимо добавить ограничение только на создание объекта.

class MySingleton {
     private static MySingleton mySingleton;
     private MySingleton(){}
     public synchronized static MySingleton getInstance(){      
        if(null == mySingleton) {
           mySingleton = new MySingleton();
        }
        return mySingleton;
     }
}
Пример ниже делает все то же, но так же пользуется двойной проверкой, используя блокировку по объекту. Это реализация гарантирует потокобезопасность. Но дополнительный объект, который нужен только для блокировки - не лушчая практика.

class MySingleton {
  private static MySingleton mySingleton;
  private static final Object lock = new Object();
  private MySingleton(){}
  public static MySingleton getInstance(){
     if(null == mySingleton) {
         synchronized(lock) {
            if(null == mySingleton) {
               mySingleton = new MySingleton();
            }
         }   
     }
     return mySingleton;
  }
}
Другая многопоточная реализация,  пользуется двойной проверкой, но с блокировкой на уровне класса. Отметив объект MySingletone, как volatile, мы уверены, что изменения , сделанные в одном потоке, будут видны другим потокам. Данная реализация гарантирует потокобезопасность.

class MySingleton {
    private volatile static MySingleton mySingleton;
    private MySingleton() {}
    public static MySingleton getInstance() {
        if (null == mySingleton) {
            synchronized(MySingleton.class) {
                if (null == mySingleton) {
                    mySingleton = new MySingleton();
                }
            }
        }
        return mySingleton;
    }
}
Данная реализация предоставляет конструктор, который помешает нарушить контракт синглтона, используя рефлексию.

class MySingleton {
    private volatile static MySingleton mySingleton;
    //Reflection может получить доступ к приватному конструктору
    private MySingleton() throws Exception {
        if (null == mySingleton) {
            mySingleton = new MySingleton();
        } else {
            throw new Exception("Это singleton; не ожидается создание более одного экземпляра");
        }
    }
    public static MySingleton getInstance() throws Exception {
        if (null == mySingleton) {
            synchronized(MySingleton.class) {
                if (null == mySingleton) {
                    mySingleton = new MySingleton();
                }
            }
        }
        return mySingleton;
    }
}
Очень популярная реализация, с использованием статического класса, которая предоставляет ленивую инициализацию (при первом обращении) и потокобезопасность.

public class MySingleton {
    private MySingleton() {}
    private static class SingletonUisngInner {
        private static MySingleton mySingleton = new MySingleton();
    }
    public static MySingleton getInstance() {
        return SingletonUisngInner.mySingleton;
    }
}
В некоторых обстоятельствах, если ваш синглтон наследуется от Cloneable - вашему классу необходимы дополнительные проверки, чтобы избежать нарушения контракта синглтона. Вам необходимо переопределить методо clone  и явно выкинуть ошибку CloneNotSupportedException.

class ClonedClass implements Cloneable {
    //Некоторая логика
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class MySingleton extends ClonedClass {
    private MySingleton() {}
    private static class SingletonUisngInner {
        private static MySingleton mySingleton = new MySingleton();
    }
    public static MySingleton getInstance() {
        return SingletonUisngInner.mySingleton;
    }
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
}
Следующий и наш финальный, очень популярный и умный способ реализации синглтона - использовать Enum, который возьмет все заботы на себя.

public enum  EnumSingleton{
    INSTANCE;
}
Иногда, люди говорят о синглтонах в нескольких JVM - разберемся с этим. Синглтон означет, что есть только один объект, и мы хоршо знаем, что жизненным циклом объекта управляет JVM, так что один разделяемый объект, среди нескольких JVM невозможен.
Но если вам это необходимо, вы можете создать объект в одной JVM и передавать его через сериализацию, в другие JVM (но помните, что вы десириализуете его, так что, все что отмечено static или transient не передается, и где то контракт синглтона может быть нарушен). Вы так же можете использовать RMI объекты, как синглтоны, чтобы удовлетворить вашим потребностям.
Веселого обучения!
PS это мой перевод данной статьи

пятница, 11 августа 2017 г.

Что такого плохого в Синглтонах

Все мы сталкивались со словом 'Синглтон', и большинство из нас даже знает его значение. Со слов Википедии: Синглтоны - классы, у которых есть ограничение: иметь только один экземпляр. Возможно несколько счастливчиков пользовались им, но поверьте мне, даже если вы использовали синглтон, вы можете не знать насколько большое это зло.

Чтобы было более понятно, этот паттерн содержит класс, который отвечает за создание объекта, а так же удостоверяется, что создается только один объект.

class Singleton {
    private static Singleton INSTANCE = null;
    private String criticalData;
    private Singleton() {
        criticalData = "Это должно быть уникальным, а состояние универсальным";
    }
    synchronized static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    public String getString() {
        return this.criticalData;
    }
    public void setCriticalData(String value) {
        criticalData = value;
    }
}
Теперь мы знаем, что из мебя представляе класс синглтон и как он  работает. Итак, в чем проблема? Подсказка: самая очевидная проблема находится в 7 строке.
Т.к. блок synchronized может быть доступен только одному потоку, это может создать узкое место при получении объекта. Но есть и другие менее известные проблемы.

Синглтоны и состояние

Сингтоны - враги unit-тестирования. Одно из основных требований для unit-тестирования, что каждый тест должен быть независим от других. Это может привезти к тому, что тест проходит, хотя это произошло, только потому, что они вызывались в определенном порядке.

Создается один объект

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

Сингтоны предоставляют глобальное состояние

Именно поэтому у нас есть синглтоны. Да. Но какой ценой? (Глобальные переменные были плохими! Помните?) Это обеспечивает возможность для некоторого сервиса в вашем приложении, так что нам не нужно передавать ссылку на этот сервис. Желание создать что-то глобальное, чтобы избежать его распространение, - это запашок от вашего дизайна.

PS это мой перевод данной статьи

пятница, 28 июля 2017 г.

Код без NPE и без проверок на NULL

Первый: Ты действительно уверен, что эта переменная никогда не будет null
Второй: конечно!
NullPointerException 

Ведь так начинается Java код-ревью? NPE всегда является кошмаром для Java разработчиков. Рассмотрим не самый простой пример кода:

public interface Service {  
    public boolean switchOn(int timmer);
    public boolean switchOff(int timmer);
    //другие элементы
}
public class RefrigeratorService implements Service {
// ...
}
public class HomeServices {
    private static final int NOW = 0;
    private static HomeServices service;
    public static HomeServices get() {
        //проверка на Null #1
        if(service == null) {
            service = new HomeServices();
        }
        return service;
    }
    public Service getRefrigertorControl() {
        return new RefrigeratorService();
    }
    public static void main(String[] args) {
        /* получаем Home Services */
        HomeServices homeServices = HomeServices.get();
        //проверка на Null #2
        if(homeServices != null) {
            Service refrigertorControl = homeServices.getRefrigertorControl();
            //проверка на Null #3
            if (refrigertorControl != null) {
                refrigertorControl.switchOn(NOW);
            }
        }
    }
}
Как видно вышел, в коде несколько проверок на Null. Конечно все это сделано, чтобы код был более надежным во всех ситуация. Это необходимо, или ...

Есть путь лучше?

В общем - да! В Java 8 представили java.util.Optional<T>. Это контейнер, который может содержить или не содержить null значения. Java 8 предоставляет нам более безопасный способ  взаимодействовать с объектами, которые  в некоторых случаях могут быть null. Данное решение вхоновлено идеями Haskell  и Scala.
Если коротко - класс Optional включает методы, для корректной работы в двух случаях, когда значение существует и когда отсутствует. Преимущество, в сравнении с null проверкой, заключается в том, что класс Optional, заставляет обдумывать случаи, когда значение отсутствует. Т.о.  получится избежать непредвиденных NPE.
В примере выше у нас есть фабрика HomeService, которая обрабатывает несколько устройств, доступных в доме. Но эти устройства функционально могут быть досутпны или не доступны. Т.е. может случиться NPE. Вместо того, чтобы добавлять проверку на null, обернем объект в Optional.

Оборачивание в Optional<T>

Рассмотрим получение объекта из фабрики. Вместо возвращение инстанса элемента, обернем его в Optional. Это позволит пользователю API, понимать, что возвращаемый сервис и может быть функционально доступным или нет. Используем безопасно:

public Optional<Service> getRefrigertorControl() {
    Service s = new  RefrigeratorService();
    //...
    return Optional.ofNullable(s);
}
Как вы видите, Optional.ofNullable предоставляет простой путь  получить обернутый объект. Есть другие пути, чтобы получить оберный объект в Optional: Optiona.empty() или Optional.of(). Первый возвращает пустой объект, вместо null,  а второй - оборачивает не null значения объектов.

Как это помогает избежать null проверок?

Обернув объект в Optional единожды, получаем множество полезных методов для вызова функционала обернутого объекта, без NPE.

Optional ref = homeServices.getRefrigertorControl(); 
ref.ifPresent(HomeServices::switchItOn);
Optiona.ifPresent вызывает потребителя (Consumer) по ссылке, если значение не null, иначе ничего не делает. Это очень красиво и легко для понимания. В примере выше, HomeService.switchOn(Service) вызывается, только если Optional содержить не null значение.
Мы часто используем тернарный оператор, чтобы проверить на null и значение по умолчанию, если null. Optional предоставляет то же самое без проверок. Optional.orElse(defaultObj) возвращает defaultObj, если в Optional значение null. Используем это в нашем коде:

public static Optional<HomeServices> get() {
    service = Optional.of(service.orElse(new HomeServices()));
    return service;
}
Сейчас HomeServices.get() делает то же самое, но лучшим способов. Он проверяет, проинициализирован ли сервис. Optional<T>.orElse(T) помогает вернуть значение по умолчанию.
В итоге вот наш код без NPE и null проверок:

import java.util.Optional;
public class HomeServices {
    private static final int NOW = 0;
    private static Optional<HomeServices> service;
    public static Optional<HomeServices> get() {
        service = Optional.of(service.orElse(new HomeServices()));
        return service;
    }
    public Optional<Service> getRefrigertorControl() {
  if(ServiceDiscovery.isAvaiable("refrigetor")) {
   Service s = RefrigeratorService.get();
   return Optional.ofNullable(s);
  }
  return Optional.empty();
 }
    public static void main(String[] args) {
        Optional<HomeServices> homeServices = HomeServices.get();
        if(homeServices.isPresent()) {
            Optional<Service> refrigertorControl = homeServices.get().getRefrigertorControl();
            refrigertorControl.ifPresent(HomeServices::switchItOn);
        }
    }
    public static void switchItOn(Service s){
        //...
    }
}

PS: это мой перевод данной статьи

суббота, 1 июля 2017 г.

Spring boot. Spring Security

Рассмотрим возможности Spring Security. В качестве примера, воспользуемся веб-проектом из предыдущих статей. Первым делом, необходимо добавить зависимость Spring Security в наш pom-файл:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
Собственно, этого остаточно, для самого простого примера. Если запустить наш пример и посмотреть на консоль вывода, можно заметить следующее сообщение:

Данный пароль можно использовать, если обратиться к localhost:8080:
Попробуем немного усложнить пример и создадим application.properties файл с таким содержимым:
security.user.name = user
security.user.password = password
Теперь, запустив наш проект, мы не увидим в консоли вывод пароль, в данном случае нам подойдут данные, которые мы указали в проперти файле.

Далее, рассмотрим In-memory security. Для этого необходимо создать следующий класс:
package com.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableGlobalAuthentication
public class InMemorySecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").
                roles("USER")
                .and().withUser("admin").password("password").
                roles("USER", "ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/journal/**").authenticated()
                .and()
                .httpBasic();
    }
}

В данном классе:

  • Aннотация EnableGlobalAuthentication - говорит о том, что класс с данной аннотацией используется для конфигурирования AuthenticationManagerBuilder.
  • AuthenticationManagerBuilder инжектится благодаря аннотации Autowired
  • WebSecurityConfigurationAdapter - абстрактный класс, предоставляющий удобную базу для конфигурации инстанса WebSecurityConfigurer
  • configure - переопределенный метод, для настройки HttpSecurity. В данном случае, достп по адресу "/" предоставляется свободный, а для /journal/** - необходима авторизация



суббота, 24 июня 2017 г.

Spring boot. JDBC

Рассмотрим, как с помощью Spring Boot, можно обращаться к БД, используя шаблоны JDBC. Рассмотрим pom файл:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>spring-boot-jdbc-template</groupId>
    <artifactId>jdbc-template</artifactId>
    <version>1.0</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
Сущность для манипуляции:

package com.entity;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Journal {
    private Long id;
    private String title;
    private Date created;
    private String summary;
    private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");
    public Journal(Long id, String title, String summary, Date date){
        this.id = id;
        this.title = title;
        this.summary = summary;
        this.created = date;
    }
    Journal(){}

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public Date getCreated() {
        return created;
    }
    public void setCreated(Date created) {
        this.created = created;
    }
    public String getSummary() {
        return summary;
    }
    public void setSummary(String summary) {
        this.summary = summary;
    }
    public String getCreatedAsShort(){
        return format.format(created);
    }
    public String toString(){
        StringBuilder value = new StringBuilder("* JournalEntry(");
        value.append("Id: ");
        value.append(id);
        value.append(",Title: ");
        value.append(title);
        value.append(",Summary: ");
        value.append(summary);
        value.append(",Created: ");
        value.append(getCreatedAsShort());
        value.append(")");
        return value.toString();
    }
}

Далее создадим сервис. У него будет две функции: добавление новой записи в БД и получение всех записей.

package com.service;

import com.entity.Journal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Service
public class JournalService {
    private static final Logger log = LoggerFactory.getLogger(JournalService.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void insertData(){
        log.info("> Table creation");
        jdbcTemplate.execute("DROP TABLE JOURNAL IF EXISTS");
        jdbcTemplate.execute("CREATE TABLE JOURNAL(id SERIAL, title VARCHAR(255), summary VARCHAR(255), created TIMESTAMP)");
        log.info("> Inserting data...");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Get to know Spring Boot','Today I will learn Spring Boot'," +
                        "'2016-01-01 00:00:00.00')");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Simple Spring Boot Project','I will do my first Spring Boot" +
                                "project','2016-01-02 00:00:00.00')");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Spring Boot Reading','Read more about Spring Boot'," +
                        "'2016-02-01 00:00:00.00')");
        jdbcTemplate.execute("INSERT INTO JOURNAL(title,summary,created) VALUES('Spring Boot in the Cloud','Learn Spring Boot using Cloud" +
                                "Foundry','2016-01-01 00:00:00.00')");
        log.info("> Done.");
    }

    public List<Journal> findAll(){
        List<Journal> entries = new ArrayList<>();
        jdbcTemplate.query("SELECT * FROM JOURNAL",
                new Object[]{},
                (rs,row) -> new Journal(rs.getLong("id"),
                        rs.getString("title"), rs.getString("summary"),
                        new Date(rs.getTimestamp("created").getTime())))
                        .forEach(entries::add);
        return entries;
    }

}
  • JdbcTemplate - автосвязанный класс, который отвечает за выполнение запросов для БД.
  • insertDate - первым делом, метод пытается удалить таблицу JOURNAL, если она есть, далее создает таблицу занова и добавляет в нее записи
  • findAll - получает,с помощью RowMapper, все записи из таблицы JOURNAL.
И класс запуска приложения:
package com;

import com.service.JournalService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {
    private static final Logger log = LoggerFactory.getLogger(Application.class);
    @Autowired
    private JournalService service;

    public static void main(String[] arg){
        SpringApplication.run(Application.class, arg);
    }

    @Override
    public void run(String... strings) throws Exception {
        log.info("@@ Inserting Data....");
        service.insertData();
        log.info("@@ findAll() call...");
        service.findAll().forEach(entry -> log.info(entry.toString()));
    }
}

Данный класс имплементирует интерфейс CommandLineRunner, для этого необходимо переопределить метод run, он выполнится после старта приложения.

воскресенье, 11 июня 2017 г.

Spring boot. Тестирование web приложения

Рассмотрим процесс тестировани веб приложения. Для начала создадим тестовое веб приложение, очень похожее на пример из 1 части, только без БД.
Сущность:
package com.entity;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class JournalEntry {
    private String title;
    private Date created;
    private String summary;
    private final SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");
    public JournalEntry(String title, String summary, String date) throws
            ParseException{
        this.title = title;
        this.summary = summary;
        this.created = format.parse(date);
    }
    JournalEntry(){}
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public Date getCreated() {
        return created;
    }
    public void setCreated(String date) throws ParseException {
        Long _date = null;
        try{
            _date = Long.parseLong(date);
            this.created = new Date(_date);
            return;
        }catch(Exception ignored){}
        this.created = format.parse(date);
    }
    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
    public String toString(){
        return "* JournalEntry(" + "Title: " +
                title +
                ",Summary: " +
                summary +
                ",Created: " +
                format.format(created) +
                ")";
    }
}
Контроллер:
package com.controller;

import com.entity.JournalEntry;
import org.springframework.web.bind.annotation.*;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@RestController
public class JournalController {
    private static List<JournalEntry> entries = new ArrayList<JournalEntry>();
    static {
        try {
            entries.add(new JournalEntry("Get to know Spring Boot", "Today I will learn Spring Boot", "01/01/2016"));
            entries.add(new JournalEntry("Simple Spring Boot Project", "I will do my first Spring Boot Project", "01/02/2016"));
            entries.add(new JournalEntry("Spring Boot Reading", "Read more about Spring Boot", "02/01/2016"));
            entries.add(new JournalEntry("Spring Boot in the Cloud", "Spring Boot using Cloud Foundry", "03/01/2016"));
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    @RequestMapping("/journal/all")
    public List<JournalEntry> getAll() throws ParseException{
        return entries;
    }

    @RequestMapping("/journal/findBy/title/{title}")
    public List<JournalEntry> findByTitleContains(@PathVariable String title) throws
            ParseException{
        return entries
                .stream()
                .filter(entry -> entry.getTitle().toLowerCase().contains(title.toLowerCase()))
                .collect(Collectors.toList());
    }

    @RequestMapping(value="/journal",method = RequestMethod.POST )
    public JournalEntry add(@RequestBody JournalEntry entry){
        entries.add(entry);
        return entry;
    }
}
Рассмотрим класс для тестирования:
package com;

import com.entity.JournalEntry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;

import javax.annotation.Resource;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@WebAppConfiguration
public class SpringBootApplicationTest {
    private final String SPRING_BOOT_MATCH = "Spring Boot";
    private final String CLOUD_MATCH = "Cloud";
    private HttpMessageConverter mappingJackson2HttpMessageConverter;
    private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(),
            Charset.forName("utf8"));
    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webApplicationContext;
    @Autowired
    void setConverters(HttpMessageConverter<?>[] converters) {
        this.mappingJackson2HttpMessageConverter = Arrays.stream(converters).
                filter(
                        converter -> converter instanceof MappingJackson2HttpMessageConverter).
                findAny().get();
    }

    @Before
    public void setup() throws Exception {
        this.mockMvc = webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getAll() throws Exception {
        mockMvc.perform(post("/journal")
                .content(this.toJsonString(new JournalEntry("Spring Boot Testing","Create Spring Boot Tests","05/09/2016")))
                .contentType(contentType)).andExpect(status().isOk());
        mockMvc.perform(get("/journal/all"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$",iterableWithSize(5)))
                .andExpect(jsonPath("$[0]['title']",containsString(SPRING_BOOT_MATCH)));
    }

    @Test
    public void findByTitle() throws Exception {
        mockMvc.perform(get("/journal/findBy/title/" + CLOUD_MATCH))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$",iterableWithSize(1)))
                .andExpect(jsonPath("$[0]['title']",containsString(CLOUD_MATCH)));
    }

    @Test
    public void add() throws Exception {
        mockMvc.perform(post("/journal")
                .content(this.toJsonString(new JournalEntry("Spring Boot Testing","Create Spring Boot Tests","05/09/2016")))
                        .contentType(contentType)).andExpect(status().isOk());
    }

    @SuppressWarnings("unchecked")
    protected String toJsonString(Object obj) throws IOException {
        MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
        this.mappingJackson2HttpMessageConverter.write(obj, MediaType.APPLICATION_JSON,
                mockHttpOutputMessage);
        return mockHttpOutputMessage.getBodyAsString();
    }
}
После этого можно запускать тесты отдельно или все вместе, запустив сам класс SpringBootApplicationTest. Если запускать весь класс - нет возможности влиять на порядок запускаемых тестов, для этого придумана аннотация FixMethodOrde, передав туда MethodSorters.DEFAULT - методты будут запускать по порядку расположения в классе, MethodSorters.NAME_ASCENDING - по имени

Spring boot. Конфигурация XML

Предположим, у нас есть приложение написанное с помощью Spring Boot, а так же есть конфигурация на xml, рассмотрим, как использовать их в одном приложении:
Есть 2 сущности, кошки и пользователи:

package com.entity;

public class User {
    private String name;
    private String surname;

    public User() {
    }

    public User(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }
}
package com.entity;

public class Cat {
    private String name;
    private int age;

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Контроллер для пользователей и для котов, обратите внимание, они находятся в разных пакета:
package com.controller;

import com.entity.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class UserController {

    private static List users = new ArrayList(){{
       add(new User("name", "surname"));
       add(new User("name2", "surname2"));
       add(new User("name3", "surname3"));
       add(new User("name4", "surname4"));
       add(new User("name5", "surname5"));
    }};

    @RequestMapping("/api/users/all")
    public List getAllUser(){
        return users;
    }
}

package ru.controller.web;

import com.entity.Cat;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class CatController {
    private static List cats = new ArrayList(){{
        add(new Cat("name",1));
        add(new Cat("name2",2));
        add(new Cat("name3",3));
        add(new Cat("name4",4));
    }};

    @RequestMapping("/api/cats/all")
    public List getCats(){
        return cats;
    }
}

Конфиг 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"

       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       ">
    <context:component-scan base-package="ru.controller.*"/>


</beans>
И инициализатор для Spring Boot:
package com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@SpringBootApplication(scanBasePackages = "com.*")
@ImportResource("classpath*:context.xml")
public class Application {
    public static void main(String[] arg){
        SpringApplication.run(Application.class, arg);
    }
}

Необходимо обратить внимание, на то, что сканирование просиходит из пакета com.*, т.е. контроллер для кошек - игнорирутся. Подключение xml конфигурации происходит с помощью аннотации ImportResources, в него передается массив с путями до xml конфигураций. Запустив приложение, видим, что оба пути /api/cats/all и /api/users/all - работают.

воскресенье, 28 мая 2017 г.

Spring boot. Часть 2 Настройки

Рассмотрим некоторые возможности конфигурации приложения.

Исключение определенных конфигураций.

Чтобы не использовать какую то автоконфигурацию достаточно просто исключить ее в аннотации SpringBootApplication:

@SpringBootApplication( exclude={ActiveMQAutoConfiguration.class} )
И данный класс не учавствует в настройках, достаточно удобно.

Настройка для SpringApplication

Для того, чтобы иметь доступ к дополнительным конфигурациям необходимо создать инстанс класса:

SpringApplication app = new SpringApplication(Application.class);
        app.run(arg);
Например, можно заменить баннер спринга, который печатется при запуске приложения:

SpringApplication app = new SpringApplication(Application.class);
        app.setBanner((environment, aClass, printStream) -> printStream.println("\n\n\nThis is my banner!\n\n"));
        app.run(arg);
Или вообще его отключить:
SpringApplication app = new SpringApplication(SpringBootSimpleApplication.class);
app.setBannerMode(Mode.OFF);
app.run(args);

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

new SpringApplicationBuilder(Application.class)
                .listeners((ApplicationListener<ApplicationEvent>) event ->
                        log.info("#### > " + event.getClass().getCanonicalName())).profiles("prod","cloud")
                .banner((environment, aClass, printStream) -> {
                    printStream.println("\n\n\nMy new Banner!\n\n");
                })
                .run(arg);

Настройка через properties

Создадим файл application.properties, в нем добавим строку:
data.server=remoteserver:3030
К ней можно легко обратиться, рассмотрим пример из прошлого проекта. Контроллер  JournalController:
 @Value("${data.server}")
    private String server;
@RequestMapping("/server")
    public @ResponseBody String getServer(){
        return server;
    }


суббота, 27 мая 2017 г.

Spring boot Часть 1

Создадим первое приложение на Spring boot, пусть оно будеть чуть сложнее, чем HelloWorld! Начнем рассмотрение с pom файла:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
          http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>spring-boot-journal</groupId>
    <artifactId>spring-boot-journal</artifactId>
    <version>1.0</version>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Далее создадим сущность:

package ru.domain;

import javax.persistence.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

@Entity
public class Journal {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private Date create;
    private String summary;
    @Transient
    private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");

    public Journal(){
    }

    public Journal(String title, String summary,String create) throws ParseException{
        this.title = title;
        this.create = format.parse(create);
        this.summary = summary;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Date getCreate() {
        return create;
    }

    public void setCreate(Date create) {
        this.create = create;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }

    public String getCreatedAsShort(){
        return format.format(create);
    }

    public String toString(){
        return "JournalEntry(" + "Id: " +
                id +
                ",Title: " +
                title +
                ",Summary: " +
                summary +
                ",Created: " +
                getCreatedAsShort() +
                ")";
    }
}

Здесь ничего необычного, т.к. используем JPA - значит нам нужный аннотации Entity, ID и GeneratedValue. Используем 2 коструктора, с параметрами для нашего удобства и без парамтеров - для JPA. Аннотация Transient, говорит о том, что это поле не будет мапиться на таблицу.
Далее создадим репозиторий, используя SpringData

package ru.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import ru.domain.Journal;

public interface JournalRepository extends JpaRepository<Journal, Long> {
}

Данный интерфейс расширяет JpaRepository, что позволяет спрингу, с помощью proxy создавать не только просты CRUD действия, но и более сложные используя конвенцию имен, например, findById, findByTitle, findByTitleLike, findByTitleLikeIgnoreCase.
Теперь веб контроллер:

package ru.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import ru.domain.Journal;
import ru.repository.JournalRepository;

import javax.annotation.Resource;
import java.util.List;

@Controller
public class JournalController {
    @Resource
    private JournalRepository repository;

    @RequestMapping("/journal")
    public @ResponseBody List<Journal>getJournals(){
        return repository.findAll();
    }

    @RequestMapping("/")
    public String index(Model model){
        model.addAttribute("journal", repository.findAll());
        return "index";
    }
}
Ну и сама веб страничка:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="Content-Type" content="text/html"/>
    <title>Spring Boot Journal</title>
    <link rel="stylesheet" type="text/css" media="all" href="css/bootstrap.min.css"/>
    <link rel="stylesheet" type="text/css" media="all" href="css/bootstrap-glyphicons.css"/>
    <link rel="stylesheet" type="text/css" media="all" href="css/styles.css"/>
</head>
<body>
<div class="container">
    <h1>Spring Boot Journal</h1>
    <ul class="timeline">
        <div  th:each="entry,status : ${journal}" >
            <li  th:attr="class=${status.odd}?'timeline-inverted':''" >
                <div class="tl-circ"></div>
                <div class="timeline-panel">
                    <div>
                        <h4> <span th:text="${entry.title}">TITLE</span>
                        </h4>
                        <p><small class="text-muted">
                            <i class="glyphicon glyphicon-time"></i>
                            <span th:text="${entry.createdAsShort}">CREATED</span>
                        </small>
                        </p>
                    </div>

                    <div class="tl-body">
                        <p> <span th:text="${entry.summary}">SUMMARY</span> </p>
                    </div>
                </div>
            </li>
        </div>
    </ul>
</div>
</body>
</html>
Ну и сам запуск приложения:

package ru;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration;
import org.springframework.context.annotation.Bean;
import ru.domain.Journal;
import ru.repository.JournalRepository;

@SpringBootApplication( exclude={ActiveMQAutoConfiguration.class} )
public class Application {
    public static void main(String[] arg){
        SpringApplication.run(Application.class, arg);
    }

    @Bean
    InitializingBean saveData(JournalRepository repository){
        return () -> {
            repository.save(new Journal("Get to know Spring Boot","Today I will learn Spring Boot","01/01/2016"));
            repository.save(new Journal("Simple Spring Boot Project","I will do my first Spring Boot Project","01/02/2016"));
            repository.save(new Journal("Spring Boot Reading","Read more about Spring Boot","02/01/2016"));
            repository.save(new Journal("Spring Boot in the Cloud","Spring Boot using Cloud Foundry","03/01/2016"));
        };
    }
}

Запустив приложение, увидим:
Отлично, все работает! Но за счет чего? Ведь нет ни web.xml, ни xml или java конфигов для сприга. Если открыть аннотацию SpringBootApplication, то мы увидим следующее:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class,
        attribute = "exclude"
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class,
        attribute = "excludeName"
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};
}

И после этого становится все понятнее. Основная аннтоция - EnableAutoConfiguration. С ее помощью спринг использует автоконфигуратор, опираясь на classpath, аннотация, которые используются в коде. Например, спринг заметит, что у нас используется зависимость spring-boot-starter-web, и поймет, что у нас веб приложение и будет конфигурировать наше приложение, как веб. Так же к приложению будет вшит Tomcat для запуска и после сборки проекта, через mvn package мы сможем запустить наше приложение не используя дополнительный сервер приложений.

пятница, 21 апреля 2017 г.

Функциональное программирование в Java 8 (Часть 4): Splitter

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

Пример: Разделение stream, используя специальный фильтр

Вы могли сталкиваться с такой проблемой раньше: у вас есть коллекция объектов, и вы хотите разделить ее с помощью фильтра. После вы хотите выполнить действия с элементами, которые подошли условию фильтра и другое действие с элементами, которые не подошли.

Обычныи (и медленный) подход

public <T> void splitAndPerform(Collection<T> items, Predicate<T> splitBy, Consumer<T> passed, Consumer<T> notPassed) {
    items.stream()
        .filter(splitBy)
        .forEach(passed);
    items.stream()
        .filter(splitBy.negate())
        .forEach(notPassed);
}
Решение работает, но достаточно медленно. Сложность  выполнения O(2n), т.к. мы проходим по коллекции два раза:  один проход, для тех элементов, которые удовлетворяют условиям, второй, для тех, которые не удовлетворяют.
Но что, если мы сами созданим разделитель. Он сортирует элементы коллекции, в зависимости от того, проходят ли они условия или нет, разделит их на 2 списка. Это позволит снизить сложность выполнения до О(n), т.к. проход по списку происходит один раз. Давайте сделаем это.

Более быстрый подход.

1. Разделение коллекции.
Как вы уже догадались, сначала нужно создать разделение.
В нашей функции splitBy(), мы хотим получить Predicate<T> в качестве парамета и вернуть новый объект Splitter, каоторый представляет из себя объект из двух списков. Один список содержит объекты, которые подошли фильру, второй - которые не подошли.
public class Splitter<T> {
    private List<T> passed;
    private List<T> notPassed;
    private Splitter(List<T> passed, List<T> notPassed) {
        this.passed = passed;
        this.notPassed = notPassed;
    }
    public static <T> Splitter<T> splitBy(Collection<T> items,Predicate<T> test) {
        List<T> passed = new LinkedList<T>();
        List<T> notPassed = new LinkedList<T>();
        items.stream()
                .forEach(item -> {
                    if(test.test(item)){
                        passed.add(item);
                        return;
                    }
                    notPassed.add(item);
                });
        return new Splitter<T>(passed, notPassed);
    }
}
Как вы заметили, мы использовали паттер Фабрика для создания Splitter. Теперь надо создать сам объект и наделить его функционалом.
2. Работа с разделенными списками.
Мы хотим работать со списками таким же способом, как со Stream. Но мы не хотим пересоздавать весь функционал, котоый есть у Stream, для наших списков. Здесь нам пригодится паттерн, я узнал о нем из этого видео и это очень клевый способ использовать лямбды. Фактически, мы создаем две функции, workOnPassedItems и workOnNotPassedItems. Они принимают Consumer<Stream<T>>. Следовательно, мы можем создавать лямды  и работать внутри нее с нормальным потоком. Этот метод будет применен к обоим спискам.

public class Splitter<T> {
    //...
    public Splitter<T> workWithPassed(Consumer<Stream<T>> func) {
        func.accept(passed.stream());
        return this;
    }
    public Splitter<T> workWithNotPassed(Consumer<Stream<T>> func) {
        func.accept(notPassed.stream());
        return this;
    }
}
Мы использовали шаблон Каскад, чтобы сделать использование различных методов более приятным. Вы увидите это в примере ниже.
В общем-то этои есть наш разделитель (Splitter). Рассмотрим несколько примеров использования.

Пример 1. Вывод числе и возведение в квадрат всех нечетных

В нашем первом примере мы хотим оперировать со списком чисел. Мы хотим печатать простые числа и возвести в квадрат все нечетные перед тем как их напечатать. Сначала мы раделяем числа не четные и нечетным. Далее работаем с каждым списком как описывалось.

public void workOnNumbers() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Splitter.splitBy(numbers, num -> num%2 == 0)
            .workWithPassed(passed ->
                    passed.forEach(even -> System.out.println("" + even + " -> " + even)))
            .workWithNotPassed(notPassed ->
                        notPassed.forEach(odd -> System.out.println(odd + " -> " + (odd * odd))
                    ));
}

Пример 2. Отправка всем победителям уведомление и проигравшим другое

У нас есть список кандидатов. У все есть метода hasWon(), который возвращает boolean. Мы хотим разделить список на победителей и проигравших. Далее мы хотим разослать всем уведомления о победе или поражении. 

public void sendEmails(List<Candidates> candidates) {
    Splitter.splitBy(candidates, Candidates::hasWon)
            .workWithPassed(winners ->
                    winners.forEach(winner -> Email.send(winner.getEmail(), "You won!"))
    )
    .workWithNotPassed(losers ->
            losers.forEach(loser -> Email.send(loser.getEmail(), "You lost, sorry!"))
    );
}

об использовании partitioningBy

Получая обратную связь на данную статью, выявилось, что  Stream.collect(Collectors.partitioningBy(Predicate<T> test)) подходит нашему случаю и я с этим полностью согласен.
Происходит разделение Stream, в зависимости от test. Т.о. для нас map будет выглядеть как то так {true: passed, false: notPassed}. Далее мы получаем два списка из map и продолжаем. Новый метод будет выглядеть так:

public static <T> Splitter<T> splitBy(Collection<T> items,Predicate<T> test) {
        Map<Boolean, List<T>> map = items.stream()
                .collect(Collectors.partitioningBy(test));
        return new Splitter<T>(map.get(true), map.get(false));
}
И я должен признать, что данный метод выглядит лучше.

Какие права на существования у Splitter

Цель Splitter - продемонстировать как вы можете работать в функцией, как с объектом. Его цель не в том, чтобы заменить методы в JDK. Это класс для изучения и экспериментов. Если вы хотите эксперементировать - делайте это. Пожалуйста, оставьте комментарий к тому, что вы узнали или где вы оптимизировали класс, чтобы другие могли учиться на этом. Так же мы рассмотрели несколько паттернов. Они помогают сделать синтекс более приятным.

Вывод

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

PS это мой перевод данной статьи