-Поиск по дневнику

Поиск сообщений в rss_rss_hh_full

 -Подписка по e-mail

 

 -Постоянные читатели

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 1

Habrahabr








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

Intel научит дроны самостоятельности

Воскресенье, 10 Сентября 2017 г. 21:08 + в цитатник
1cloud сегодня в 21:08 Разработка

Intel научит дроны самостоятельности

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

    28 августа компания Intel представила визуальный процессор (VPU) Myriad X, который разработан специально для автономных интеллектуальных устройств. Главная особенность чипа — встроенный аппаратный ускоритель для нейросетей (Neural Compute Engine). Он отвечает за обучение искусственного интеллекта на периферийных устройствах, имея при этом низкое энергопотребление. Среди предложенных Intel сфер применения чипа числятся дроны, системы безопасности и наблюдения, VR/AR и другие устройства.


    / Flickr / Bidgee / CC

    По словам разработчиков, Myriad X — первая в мире однокристальная система, специально «заточенная» под ускоренное глубокое обучение. Neural Compute Engine помогает устройствам видеть, интерпретировать и реагировать на окружающую среду в реальном времени. С ускорителем производительность Myriad X достигает триллиона операций в секунду (TOPS).

    «Мы близки к тому, чтобы машинное зрение и глубокое обучение вошли в число стандартных требований к миллиардам ежедневно окружающих нас устройств», — сказал Реми Эль-Уаззани (Remi El-Ouazzane), вице-президент и генеральный менеджер Movidius, компании-разработчика чипов семейства Myriad.

    Архитектура VPU-чипа Movidius Myriad X VPU состоит из нескольких специализированных вычислительных устройств. Это несколько процессоров общего назначения, наряду с 16 процессорами SHAVE (Streaming Hybrid Architecture Vector Engine). Последние работают совместно с нейронным вычислительным движком.

    Процессоры SHAVE состоят из шестнадцати программируемых 128-битных векторных процессоров VLIW, конфигурируемых шин MIPI, аппаратных ускорителей и централизованной архитектуры встроенной памяти.

    В своем личном обращении Эль-Уаззани рассказывает, как устройства, наделенные искусственным интеллектом, сделают «наш мир безопаснее и продуктивнее». Автономные дроны-курьеры, камеры безопасности с полным пониманием происходящего и роботы с расширенным спектром реакций — в теории это возможно с Myriad X, поскольку глубокое обучение выполняется на уровне устройства без связи с облаком.

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

    Применение VPU


    Однако наиболее вероятной сферой применения нового чипа в Intel видят беспилотники. Дроны становятся умнее. Например, Spark — мини-дрон от DJI — оборудован набором из камер и датчиков и процессором Vision Movivius Myriad 2, который помогает интеллектуальной системе бортового зрения обнаруживать и избегать столкновения с объектами, создавать 3D-карты, распознавать лица и жесты.

    И достигнутый уровень развития — не предел. По словам Кормака Брика (Cormac Brick), руководителя по бортовым системам искусственно интеллекта в Movidius, дроны учатся думать и реагировать.

    «В будущем вы сможете достать дрон из кармана, запустить его в воздух и оставить летать вокруг двора, пока вы жарите барбекю, — рассказывает Брик, — а через час он отправит на ваш телефон 45-секундный видеоклип или 10 лучших снимков, чтобы вы могли поделиться ими в социальных сетях».

    Movidius видят применение своим чипам и в сфере безопасности. В этом году их процессор Myriad 2 уже был интегрирован в камеры видеонаблюдения Dahua. Эти камеры далеко шагнули от традиционных технологий мониторинга и записи. Они обладают расширенными функциями анализа видео: мониторят плотность толпы, распознают лица, имеют стереоскопическое зрение, могут обнаруживать неправильно припаркованные автомобили.

    Есть и еще одно применение VPU нового поколения. В июле Intel представили мобильный модуль Movidius Neural Compute Stick с разъемом USB 3.0. Его задача — обучение искусственного интеллекта на различных устройствах. Характеристики и особенности те же: низкое энергопотребление при высокой производительности, компактность, офлайн-режим работы, нацеленность на приложения машинного зрения.

    Акцент на AI


    По данным Accenture, рынок AI в 12 развитых странах удвоит темпы роста к 2035 году. Например, только в Китае к 2020 году собираются установить 450 млн новых камер видеонаблюдения с системами распознавания лиц. К слову, компания Dahua Technology, которая уже экспериментировала с процессором Myriad 2, занимает второе место на рынке камер наблюдения и базируется в Ханчжоу.

    На AI-рынке у Intel до выхода Myriad X было несколько серьезных противников: Qualcomm Neural Processing Engine, процессор Google TPU 2.0, предназначенный для обучения нейронных сетей, и NVIDIA Tesla V100. По мнению аналитиков Seeking Alpha, самую серьезную конкуренцию Intel на рынке машинного обучения, который достигнет $5 млрд к 2021 году, составляет именно NVIDIA.

    Однако вскоре могут появиться и другие сильные конкуренты. В августе компания Microsoft представила проект Brainwave. Это платформа для глубокого обучения AI в реальном времени с низкой задержкой в обработке запросов. Архитектура Brainwave поддерживает выполнение более 130 тыс. вычислений за цикл. Работа в этом направлении вскоре позволит применять нейронные сети для выполнения бытовых задач.

    О компании Movidius

    Компания Movidius разрабатывает визуальные процессоры для интернета вещей. Штаб-квартира располагается в Сан-Матео, Калифорния. В сентябре 2016 года была приобретена компанией Intel.

    P.S. А вот еще несколько статей из нашего блога:

    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337570/


    Метки:  

    [Перевод] Spring: ваш следующий Java микрофреймворк

    Воскресенье, 10 Сентября 2017 г. 21:06 + в цитатник
    alek_sys сегодня в 21:06 Разработка

    Spring: ваш следующий Java микрофреймворк

    • Перевод

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


    Вы, возможно, удивлены видеть Spring и микрофреймворк в одном предложении. Но все верно, Spring вполне может стать вашим следующим Java микрофреймворком. Чтобы избежать недоразумений, давайте определим, что им имеем в виду под микро:


    • Лаконичный — минимум бойлерплейта, минимум настройки
    • Простой — без магии
    • Простой в деплойменте — один артефакт для деплоймента
    • Простой в запуске — без дополнительных зависимостей
    • Легковесный — минимальное использование памяти / CPU
    • Неблокирующий — для написания конкуррентных неблокирующих приложений

    Несмотря на то, что некоторые из этих пунктов актуальны при использовании Spring Boot, он сам по себе добавляет дополнительную магию поверх самого Spring Framework. Даже такие базовые аннотации, как @Controller не совсем прямолинейны, что уж говорить про авто-конфигурации и сканирование компонентов. В общем-то, для крупномасштабных приложений, просто незаменимо то, что Spring берет на себя заботу о DI, роутинге, конфигурации и т.п. Однако, в мире микросервисов, где приложения это просто шестеренки в одной больной машине, вся мощь Spring Boot может быть немного лишней.


    Для решения этой проблемы, команда Spring представила новую фичу, которая называется функциональный веб-фреймворк — и именно о ней мы и будем говорить. В целом, это часть большего под-проекта Spring WebFlux, который раньше назывался Spring Reactive Web.


    Для начала, давайте вернемся к основам и посмотрим, что такое веб-приложение и какие компоненты мы ожидаем иметь в нем. Несомненно, есть базовая вещь — веб-сервер. Чтобы избежать ручной обработки запросов и вызова методов приложения, нам пригодится роутер. И, наконец, нам нужен обработчик — кусок кода, который принимает запрос и отдает ответ. По сути, это все, что нужно! И именно эти компоненты предоставляет функциональный веб-фреймворк Spring, убирая всю магию и фокусируясь на фундаментальном минимуме. Отмечу, что это вовсе не значит, что Spring резко меняет направление и уходит от Spring MVC, функциональный веб просто дает еще одну возможность создавать приложения на Spring.


    Обработчик


    Давайте рассмотрим пример. Для начала, пойдем на Spring Initializr и создадим новый проект используя Spring Boot 2.0 и Reactive Web как единственную зависимость. Теперь мы можем написать наш первый обработчик — функцию которая принимает запрос и отдает ответ.


    HandlerFunction hello = new HandlerFunction() {
        @Override
        public Mono handle(ServerRequest request) {
            return ServerResponse.ok().body(fromObject("Hello"));
        }
    };

    Итак, наш обработчик это просто реализация интерфейса HandlerFunction который принимает параметр request (типа ServerRequest) и возвращает объект типа ServerResponse с текстом "Hello". Spring так же предоставляет удобные билдеры чтобы создать ответ от сервера. В нашем случае, мы используем ok() которые автоматически возвращают HTTP код ответа 200. Чтобы вернуть ответ, нам потребуется еще один хелпер — fromObject, чтобы сформировать ответ из предоставленного объекта.


    Мы так же можем сделать код немного более лаконичным и использовать лямбды из Java 8 и т.к. HandlerFunction это интерфейс одного метода (single abstract method interface, SAM), мы можем записать нашу функцию как:


    HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));

    Роутер


    Теперь, когда у нас есть хендлер, пора определить роутер. Например, мы хотим вызвать наш обработчик когда URL "/" был вызван с помощью HTTP метода GET. Чтобы этого добиться, определим объект типа RouterFunction который мапит функцию-обработчик, на маршрут:


    RouterFunction router = route(GET("/"), hello);

    route и GET это статические методы из классов RequestPredicates и RouterFunctions, они позволяют создать так называемую RouterFunction. Такая функция принимает запрос, проверяет, соответствует ли он все предикатам (URL, метод, content type, etc) и вызывает нужную функцию-обработчик. В данном случае, предикат это http метод GET и URL '/', а функция обработчик это hello, которая определена выше.


    Веб-сервер


    А сейчас пришло время собрать все вместе в единое приложение. Мы используем легковесный и простой сервер Reactive Netty. Чтобы интегрировать наш роутер с веб-сервером, необходимо превратить его в HttpHandler. После этого можно запустить сервер:


    HttpServer
        .create("localhost", 8080)
        .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
        .block();

    ReactorHttpHandlerAdapter это класс предоставленный Netty, который принимает HttpHandler, остальной код, думаю, не требует пояснений. Мы создаем новые веб-сервер привязанный к хосту localhost и на порту 8080 и предоставляем httpHandler созданный из нашего роутера.


    И это все, приложение готово! И его полный код:


    public static void main(String[] args)
              throws IOException, LifecycleException, InterruptedException {
    
        HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));
    
        RouterFunction router = route(GET("/"), hello);
    
        HttpHandler httpHandler = RouterFunctions.toHttpHandler(router);
    
        HttpServer
                .create("localhost", 8080)
                .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
                .block();
    
        Thread.currentThread().join();
    }

    Последняя строчка нужна только чтобы держать JVM процесс живым, т.к. сам HttpServer его не блокирует. Вы возможно сразу обратите внимание, что приложение стартует мгновенно — там нет ни сканирования компонентов, ни авто-конфигурации.


    Мы так же может запустить это приложение как обычное Java приложение, не требуется никаких контейнеров приложений и прочего.


    Чтобы запаковать приложение для деплоймента, мы можем воспользоваться преимуществами Maven плагина Spring и просто вызвать


    ./mvnw package


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


    java -jar target/functional-web-0.0.1-SNAPSHOT.jar


    Так же, если мы проверим использование памяти приложением, то увидим, что оно держится примерно в районе 32 Мб — 22 Мб использовано на metaspace (классы) и около 10 Мб занято непосредственно в куче. Разумеется, наше приложение ничего и не делает — но тем не менее, это просто показатель, что фреймворк и рантайм сами по себе требуют минимум системных ресурсов.


    Поддержка JSON


    В нашем примере, мы возвращали строку, но вернуть JSON ответ так же просто. Давайте расширим наше приложение новым endpoint-ом, который вернет JSON. Наша модель будет очень простой — всего одно строковое поле под названием name. Чтобы избежать ненужного boilerplate кода, мы воспользуемся фичей из проекта Lombok, аннотацией @Data. Наличие этой аннотации автоматически создаст геттеры, сеттеры, методы equals и hashCode, так что нам не придется релизовывать их вручную.


    @Data
    class Hello {
        private final String name;
    }

    Теперь, нам нужно расширить наш роутер чтобы вернуть JSON ответ при обращении к URL /json. Это можно сделать вызвав andRoute(...) метод на существующем роуте. Также, давайте вынесем код роутер в отдельную функцию, чтобы отделить его от кода приложения и позволить использовать позже в тестах.


    static RouterFunction getRouter() {
        HandlerFunction hello = request -> ok().body(fromObject("Hello"));
    
        return
            route(
                GET("/"), hello)
            .andRoute(
                GET("/json"), req ->
                    ok()
                    .contentType(APPLICATION_JSON)
                    .body(fromObject(new Hello("world")));
    }

    После перезапуска, приложение вернет { "name": "world" } при обращении к URL /json при запросе контента с типом application/json.


    Контекст приложения


    Вы, возможно, заметили, что мы не определили контекст приложения — он нам просто не нужен! Несмотря на то, что мы можем объявить RouterFunction как бин (bean) в контексте Spring WebFlux приложения, и он точно так же будет обрабатывать запросы на определенные URL, роутер можно запустить просто поверх Netty Server чтобы создавать простые и легковесные JSON сервисы.


    Тестирование


    Для тестирования реактивных приложений, Spring предоставляет новый клиент под названием WebTestClient (подобно MockMvc). Его можно создать для существующего контекста приложения, но так же можно определить его и для RouterFunction.


    public class FunctionalWebApplicationTests {
    
        private final WebTestClient webTestClient =
                WebTestClient
                    .bindToRouterFunction(
                        FunctionalWebApplication.getRouter())
                    .build();
    
        @Test
        public void indexPage_WhenRequested_SaysHello() {
            webTestClient.get().uri("/").exchange()
                    .expectStatus().is2xxSuccessful()
                    .expectBody(String.class)
                    .isEqualTo("Hello");
        }
    
        @Test
        public void jsonPage_WhenRequested_SaysHello() {
            webTestClient.get().uri("/json").exchange()
                    .expectStatus().is2xxSuccessful()
                    .expectHeader().contentType(APPLICATION_JSON)
                    .expectBody(Hello.class)
                    .isEqualTo(new Hello("world"));
        }
    }

    WebTestClient включает ряд assert-ов, которые можно применить к полученному ответу, чтобы провалидировать HTTP код, содержимое ответа, тип ответа и т.п.


    В заключение


    Spring 5 представляет новую парадигму для разработки маленьких и легковесных microservice-style веб-приложений. Такие приложения могут работать без контекста приложений, автоконфигурации и в целом использовать подход микрофреймворков, когда роутер и функции-обработчики и веб-сервер опеределены явно в теле приложения.


    Код


    Доступен на GitHub


    Ссылки



    От переводчика


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

    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337604/


    Метки:  

    [Перевод] Spring: ваш следующий Java микрофреймворк

    Воскресенье, 10 Сентября 2017 г. 21:06 + в цитатник
    alek_sys сегодня в 21:06 Разработка

    Spring: ваш следующий Java микрофреймворк

    • Перевод

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


    Вы, возможно, удивлены видеть Spring и микрофреймворк в одном предложении. Но все верно, Spring вполне может стать вашим следующим Java микрофреймворком. Чтобы избежать недоразумений, давайте определим, что им имеем в виду под микро:


    • Лаконичный — минимум бойлерплейта, минимум настройки
    • Простой — без магии
    • Простой в деплойменте — один артефакт для деплоймента
    • Простой в запуске — без дополнительных зависимостей
    • Легковесный — минимальное использование памяти / CPU
    • Неблокирующий — для написания конкуррентных неблокирующих приложений

    Несмотря на то, что некоторые из этих пунктов актуальны при использовании Spring Boot, он сам по себе добавляет дополнительную магию поверх самого Spring Framework. Даже такие базовые аннотации, как @Controller не совсем прямолинейны, что уж говорить про авто-конфигурации и сканирование компонентов. В общем-то, для крупномасштабных приложений, просто незаменимо то, что Spring берет на себя заботу о DI, роутинге, конфигурации и т.п. Однако, в мире микросервисов, где приложения это просто шестеренки в одной больной машине, вся мощь Spring Boot может быть немного лишней.


    Для решения этой проблемы, команда Spring представила новую фичу, которая называется функциональный веб-фреймворк — и именно о ней мы и будем говорить. В целом, это часть большего под-проекта Spring WebFlux, который раньше назывался Spring Reactive Web.


    Для начала, давайте вернемся к основам и посмотрим, что такое веб-приложение и какие компоненты мы ожидаем иметь в нем. Несомненно, есть базовая вещь — веб-сервер. Чтобы избежать ручной обработки запросов и вызова методов приложения, нам пригодится роутер. И, наконец, нам нужен обработчик — кусок кода, который принимает запрос и отдает ответ. По сути, это все, что нужно! И именно эти компоненты предоставляет функциональный веб-фреймворк Spring, убирая всю магию и фокусируясь на фундаментальном минимуме. Отмечу, что это вовсе не значит, что Spring резко меняет направление и уходит от Spring MVC, функциональный веб просто дает еще одну возможность создавать приложения на Spring.


    Обработчик


    Давайте рассмотрим пример. Для начала, пойдем на Spring Initializr и создадим новый проект используя Spring Boot 2.0 и Reactive Web как единственную зависимость. Теперь мы можем написать наш первый обработчик — функцию которая принимает запрос и отдает ответ.


    HandlerFunction hello = new HandlerFunction() {
        @Override
        public Mono handle(ServerRequest request) {
            return ServerResponse.ok().body(fromObject("Hello"));
        }
    };

    Итак, наш обработчик это просто реализация интерфейса HandlerFunction который принимает параметр request (типа ServerRequest) и возвращает объект типа ServerResponse с текстом "Hello". Spring так же предоставляет удобные билдеры чтобы создать ответ от сервера. В нашем случае, мы используем ok() которые автоматически возвращают HTTP код ответа 200. Чтобы вернуть ответ, нам потребуется еще один хелпер — fromObject, чтобы сформировать ответ из предоставленного объекта.


    Мы так же можем сделать код немного более лаконичным и использовать лямбды из Java 8 и т.к. HandlerFunction это интерфейс одного метода (single abstract method interface, SAM), мы можем записать нашу функцию как:


    HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));

    Роутер


    Теперь, когда у нас есть хендлер, пора определить роутер. Например, мы хотим вызвать наш обработчик когда URL "/" был вызван с помощью HTTP метода GET. Чтобы этого добиться, определим объект типа RouterFunction который мапит функцию-обработчик, на маршрут:


    RouterFunction router = route(GET("/"), hello);

    route и GET это статические методы из классов RequestPredicates и RouterFunctions, они позволяют создать так называемую RouterFunction. Такая функция принимает запрос, проверяет, соответствует ли он все предикатам (URL, метод, content type, etc) и вызывает нужную функцию-обработчик. В данном случае, предикат это http метод GET и URL '/', а функция обработчик это hello, которая определена выше.


    Веб-сервер


    А сейчас пришло время собрать все вместе в единое приложение. Мы используем легковесный и простой сервер Reactive Netty. Чтобы интегрировать наш роутер с веб-сервером, необходимо превратить его в HttpHandler. После этого можно запустить сервер:


    HttpServer
        .create("localhost", 8080)
        .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
        .block();

    ReactorHttpHandlerAdapter это класс предоставленный Netty, который принимает HttpHandler, остальной код, думаю, не требует пояснений. Мы создаем новые веб-сервер привязанный к хосту localhost и на порту 8080 и предоставляем httpHandler созданный из нашего роутера.


    И это все, приложение готово! И его полный код:


    public static void main(String[] args)
              throws IOException, LifecycleException, InterruptedException {
    
        HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));
    
        RouterFunction router = route(GET("/"), hello);
    
        HttpHandler httpHandler = RouterFunctions.toHttpHandler(router);
    
        HttpServer
                .create("localhost", 8080)
                .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
                .block();
    
        Thread.currentThread().join();
    }

    Последняя строчка нужна только чтобы держать JVM процесс живым, т.к. сам HttpServer его не блокирует. Вы возможно сразу обратите внимание, что приложение стартует мгновенно — там нет ни сканирования компонентов, ни авто-конфигурации.


    Мы так же может запустить это приложение как обычное Java приложение, не требуется никаких контейнеров приложений и прочего.


    Чтобы запаковать приложение для деплоймента, мы можем воспользоваться преимуществами Maven плагина Spring и просто вызвать


    ./mvnw package


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


    java -jar target/functional-web-0.0.1-SNAPSHOT.jar


    Так же, если мы проверим использование памяти приложением, то увидим, что оно держится примерно в районе 32 Мб — 22 Мб использовано на metaspace (классы) и около 10 Мб занято непосредственно в куче. Разумеется, наше приложение ничего и не делает — но тем не менее, это просто показатель, что фреймворк и рантайм сами по себе требуют минимум системных ресурсов.


    Поддержка JSON


    В нашем примере, мы возвращали строку, но вернуть JSON ответ так же просто. Давайте расширим наше приложение новым endpoint-ом, который вернет JSON. Наша модель будет очень простой — всего одно строковое поле под названием name. Чтобы избежать ненужного boilerplate кода, мы воспользуемся фичей из проекта Lombok, аннотацией @Data. Наличие этой аннотации автоматически создаст геттеры, сеттеры, методы equals и hashCode, так что нам не придется релизовывать их вручную.


    @Data
    class Hello {
        private final String name;
    }

    Теперь, нам нужно расширить наш роутер чтобы вернуть JSON ответ при обращении к URL /json. Это можно сделать вызвав andRoute(...) метод на существующем роуте. Также, давайте вынесем код роутер в отдельную функцию, чтобы отделить его от кода приложения и позволить использовать позже в тестах.


    static RouterFunction getRouter() {
        HandlerFunction hello = request -> ok().body(fromObject("Hello"));
    
        return
            route(
                GET("/"), hello)
            .andRoute(
                GET("/json"), req ->
                    ok()
                    .contentType(APPLICATION_JSON)
                    .body(fromObject(new Hello("world")));
    }

    После перезапуска, приложение вернет { "name": "world" } при обращении к URL /json при запросе контента с типом application/json.


    Контекст приложения


    Вы, возможно, заметили, что мы не определили контекст приложения — он нам просто не нужен! Несмотря на то, что мы можем объявить RouterFunction как бин (bean) в контексте Spring WebFlux приложения, и он точно так же будет обрабатывать запросы на определенные URL, роутер можно запустить просто поверх Netty Server чтобы создавать простые и легковесные JSON сервисы.


    Тестирование


    Для тестирования реактивных приложений, Spring предоставляет новый клиент под названием WebTestClient (подобно MockMvc). Его можно создать для существующего контекста приложения, но так же можно определить его и для RouterFunction.


    public class FunctionalWebApplicationTests {
    
        private final WebTestClient webTestClient =
                WebTestClient
                    .bindToRouterFunction(
                        FunctionalWebApplication.getRouter())
                    .build();
    
        @Test
        public void indexPage_WhenRequested_SaysHello() {
            webTestClient.get().uri("/").exchange()
                    .expectStatus().is2xxSuccessful()
                    .expectBody(String.class)
                    .isEqualTo("Hello");
        }
    
        @Test
        public void jsonPage_WhenRequested_SaysHello() {
            webTestClient.get().uri("/json").exchange()
                    .expectStatus().is2xxSuccessful()
                    .expectHeader().contentType(APPLICATION_JSON)
                    .expectBody(Hello.class)
                    .isEqualTo(new Hello("world"));
        }
    }

    WebTestClient включает ряд assert-ов, которые можно применить к полученному ответу, чтобы провалидировать HTTP код, содержимое ответа, тип ответа и т.п.


    В заключение


    Spring 5 представляет новую парадигму для разработки маленьких и легковесных microservice-style веб-приложений. Такие приложения могут работать без контекста приложений, автоконфигурации и в целом использовать подход микрофреймворков, когда роутер и функции-обработчики и веб-сервер опеределены явно в теле приложения.


    Код


    Доступен на GitHub


    Ссылки



    От переводчика


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

    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337604/


    Метки:  

    [Из песочницы] Stateless аутентификация при помощи Spring Security и JWT

    Воскресенье, 10 Сентября 2017 г. 20:07 + в цитатник
    AnarSultanov сегодня в 20:07 Разработка

    Stateless аутентификация при помощи Spring Security и JWT

    Недавно передо мной встала задача отказаться от statefull аутентификации с помощью сессий, в пользу stateless аутентификации и JWT. Так как это было мое первое знакомство с JSON Web Token, в первую очередь я начал искать полезную информацию на просторах интернета, но чем больше информации я находил, тем больше вопросов у меня появлялось.

    Я не буду рассказывать, что это за «волшебные» токены, как они работают и зачем они нужны. Я хочу сосредоточиться на вопросах, которые встают перед многими, но не имеют однозначного правильного ответа. Мое решение не претендует на лучшее и эта статья не является пошаговым руководством, я просто хочу поделиться своим опытом и постараться обьяснить, почему я сделал именно так и никак иначе.

    Для работы с токенами я выбрал библиотеку JJWT, хотя в принципе все реализации библиотек довольно похожи по своей функциональности. Также было решено не создавать сервер авторизации, а выдавать/обновлять/проверять токены непосредственно из приложения. Кстати немного о приложении, это одностраничное приложение (AngularJS) с RESTful Web-сервисом (Spring) в back-end.

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

    Для обновления access токена можно использовать refresh токен с более длительным временем до истечения, который получает новые данные пользователя по его идентификатору из базы данных и создает новый access токен на их основе. Но как быть с refresh токеном, если он также выдан на не очень долгое время и истечет во время сеанса пользователя? Повторный запрос авторизационных данных во время сеанса не очень хорошее решение в плане UX, а обновление истекшего токена плохо скажется на безопастности. Нужно было найти другое решение, но к этому вопросу я вернусь немного позже. Для начала нужно определиться с временем «жизни» для токенов и я остановился на 10 минутах для access токена и 60 минутах, с возможностью продления в перспективе, для refresh токена.

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

    В случае с локальным хранилищем, обычно способом передачи токенов между back-end и front-end является добавление их в header или в тело запроса/ответа. То есть, мне пришлось бы вносить изминения и во front-end, для получения токенов, сохранения их в локальное хранилище, добавления к запросам, обновления и т.д., тогда как в случае с cookies их можно добавлять/изменять/удалять непосредственно из back-end.

    Думаю, самое время перейти от теории к практике и показать, как это все реализовано у меня. Не хочу загружать статью лишним кодом, выкладывая все классы полностью, поэтому постараюсь выделить только самое важное. Если вас заинтересуют какие-либо подробности, которые остались «за кадром», спрашивайте, и я с радостью отвечу вам в комментариях или дополню статью.

    Естественно, взаимодействие с приложением начинается с авторизации. Пользователь авторизуется с помощью метода в @RestController, в котором вызывается метод из TokenAuthenticationService для создания токенов и добавления их в ответ сервера в виде cookies (access_token и refresh_token).

        @RequestMapping(value = "/login", produces = "application/json", method = RequestMethod.GET)
        @ResponseStatus(value = HttpStatus.NO_CONTENT)
        public void login(HttpServletResponse response) {
            SessionUser user = (SessionUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            tokenAuthenticationService.addAuthentication(response, user);
            SecurityContextHolder.getContext().setAuthentication(null);
        }

    SessionUser — реализация UserDetails*

    Так выглядит этот метод в TokenAuthenticationService:

        public void addAuthentication(HttpServletResponse response, SessionUser user) {
            Cookie access = new Cookie("access_token", tokenHandler.createAccessToken(user));
            access.setPath("/");
            access.setHttpOnly(true);
            response.addCookie(access);
    
            Cookie refresh = new Cookie("refresh_token", tokenHandler.createRefreshToken(user));
            refresh.setPath("/");
            refresh.setHttpOnly(true);
            response.addCookie(refresh);
        }

    TokenHandler содержит стандартные методы для работы c JWT с использованием Jwts.builder() и Jwts.parser() из указанной выше библиотеки, поэтому не вижу смысла занимать место их кодом, но для ясности напишу что делает каждый из них:

    public String createRefreshToken(SessionUser user) {
        //возвращает токен, в котором хранится только username
    }
    public SessionUser parseRefreshToken(String token) {
        //парсит username из токена и получает данные пользователя из реализации UserDetailsService
    }
    public String createAccessToken(SessionUser user) {
        //возвращает токен, в котором хранятся все данные для воссоздания SessionUser
    }
    public SessionUser parseAccessToken(String token) {
        //использует данные из токена для создания нового SessionUser
    }

    UserDetailsService*

    Теперь немного о том, как происходит обработка последующих запросов. В конфигурационном файле, наследованном от WebSecurityConfigurerAdapter, я добавил два известных вам bean-a:

        @Bean
        public TokenAuthenticationService tokenAuthenticationService() {
            return new TokenAuthenticationService();
        }
    
        @Bean
        public TokenHandler tokenHandler() {
            return new TokenHandler();
        }

    Запретил создание/использование сессий и добавил фильтр для аутентификации с помощью токенов:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            //Ресурсы доступные анонимным пользователям
            .antMatchers("/", "/login").permitAll()
            //Все остальные доступны только после аутентификации
            .anyRequest().authenticated()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService()), UsernamePasswordAuthenticationFilter.class)             
    }

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

    public class StatelessAuthenticationFilter extends GenericFilterBean {
    
        private final TokenAuthenticationService tokenAuthenticationService;
    
        public StatelessAuthenticationFilter(TokenAuthenticationService tokenAuthenticationService) {
            this.tokenAuthenticationService= tokenAuthenticationService;
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws IOException, ServletException {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            Authentication authentication = tokenAuthenticationService.getAuthentication(httpRequest, httpResponse);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            filterChain.doFilter(request, response);
            SecurityContextHolder.getContext().setAuthentication(null);
        }
    }

    Как происходит создание аутентификации:
    — Присутствует access токен?
    — Нет? Отклонить запрос.
    — Да?
    — — Действительный и не истек?
    — — Да? Разрешить запрос.
    — — Нет? Попробовать получить новый access токен при помощи refresh токена.
    — — — Получилось?
    — — — Да? Разрешить запрос, и добавить новый access токен к ответу.
    — — — Нет? Отклонить запрос.
    За это отвечает еще один метод из уже известного вам TokenAuthenticationService:

    public Authentication getAuthentication(HttpServletRequest request, HttpServletResponse response) {
            Cookie[] cookies = request.getCookies();
    
            String accessToken = null;
            String refreshToken = null;
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (("access_token").equals(cookie.getName())) {
                        accessToken = cookie.getValue();
                    }
                    if (("refresh_token").equals(cookie.getName())) {
                        refreshToken = cookie.getValue();
                    }
                }
            }
    
            if (accessToken != null && !accessToken.isEmpty()) {
                try {
                    SessionUser user = tokenHandler.parseAccessToken(accessToken);
                    return new UserAuthentication(user);
                } catch (ExpiredJwtException ex) {
                    if (refreshToken != null && !refreshToken.isEmpty()) {
                        try {
                            SessionUser user = tokenHandler.parseRefreshToken(refreshToken);
                            Cookie access = new Cookie("access_token", tokenHandler.createAccessToken(user));
                            access.setPath("/");
                            access.setHttpOnly(true);
                            response.addCookie(access);
                            return new UserAuthentication(user);
                        } catch (JwtException e) {
                            return null;
                        }
                    }
                    return null;
                } catch (JwtException ex) {
                    return null;
                }
            }
            return null;
        } 

    UserAuthentication — реализация Authentication*

    Вот и все, что требуется для совместной работы Spring Security и JWT.

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

    Первое, что пришло в голову, убрать из токена время истечения, а так как в моем случае cookies хранятся только до закрытия браузера, то токен бы просто удалялся и больше не использовался. Но здесь были свои «подводные камни», во первых в случае кражи токен мог бы использоваться неограниченное количество времени и не было бы возможности его отозвать, во вторых я хотел продлевать токен только для активного пользователя, а если пользователь просто оставит окно браузера открытым, то токен должен был истечь спустя некоторое время.

    Чтобы обезопасить токен от кражи, нужно было реализовать возможность отзыва токена, поэтому я решил хранить все refresh токены в базе данных и перед выдачей новых access токенов, проверять наличие refresh токена в ней. Тогда в случае отзыва токена, достаточно удалить его из базы данных. Но это не решало моей проблемы с продлением токенов. И тогда я подумал, а что если хранить время до истечения токена не в самом токене, а в базе данных? И отодвигать это время на 60 минут вперед от каждого использования токена. Мне это показалось довольно не плохой идеей и я хотел бы поделиться с вами ее реализацией.

    Я создал таблицу (refresh_token) в базе данных для хранения refresh токенов со следующими столбцами:
    1. id (BIGINT)
    2. username (VARCHAR)
    3. token (VARCHAR)
    4. expires (TIMESTAMP)
    И создал класс RefreshTokenDao с двумя методами использующими JdbcTemplate для общения с этой таблицей:

        public void insert(String username, String token, long expires) {
            String sql = "INSERT INTO refresh_token "
                    + "(username, token, expires) VALUES (?, ?, ?)";
            jdbcTemplate.update(sql, username, token, new Timestamp(expires));
        }
    
        public int updateIfNotExpired(String username, String token, long expiration) {
            String sql = "UPDATE refresh_token "
                    + "SET expires = ? "
                    + "WHERE username = ? "
                    + "AND token = ? "
                    + "AND expires > ?";
            Timestamp now = new Timestamp(System.currentTimeMillis());
            Timestamp newExpirationTime = new Timestamp(now.getTime() + expiration);
            return jdbcTemplate.update(sql, newExpirationTime, username, token, now);
        }

    Методы довольно простые. Первый используется для добавления токена в базу данных при его создании. Второй для обновления токена, если он не истек и возвращения количества обновленных полей, в моем случае это количество всегда будет 0 или 1.

    Используются они в классе TokenHandler. При создании токена (createRefreshToken()) я добавляю запись о нем с помощью метода insert(). А в случае когда мне нужно получить информацию из токена (parseRefreshToken()), я сначала пытаюсь вызвать updateIfNotExpired() и если в ответе получаю значение не равное 0, значит токен валиден и его дата обновилась, можно продолжать выполнение метода; а в случае, если возвращенное значение равно 0, из чего следует что токен не найден или истек, я просто выбрасываю исключение (new JwtException(«Token is expired or missing»)).

    Это все что касалось продления refresh токенов. На просторах интернета я нигде не встречал такого способа (может плохо искал) и надеюсь что он будет кому-то полезен, как и вся статья в целом.

    Спасибо за внимание!
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337600/


    Метки:  

    [Из песочницы] Stateless аутентификация при помощи Spring Security и JWT

    Воскресенье, 10 Сентября 2017 г. 20:07 + в цитатник
    AnarSultanov сегодня в 20:07 Разработка

    Stateless аутентификация при помощи Spring Security и JWT

    Недавно передо мной встала задача отказаться от statefull аутентификации с помощью сессий, в пользу stateless аутентификации и JWT. Так как это было мое первое знакомство с JSON Web Token, в первую очередь я начал искать полезную информацию на просторах интернета, но чем больше информации я находил, тем больше вопросов у меня появлялось.

    Я не буду рассказывать, что это за «волшебные» токены, как они работают и зачем они нужны. Я хочу сосредоточиться на вопросах, которые встают перед многими, но не имеют однозначного правильного ответа. Мое решение не претендует на лучшее и эта статья не является пошаговым руководством, я просто хочу поделиться своим опытом и постараться обьяснить, почему я сделал именно так и никак иначе.

    Для работы с токенами я выбрал библиотеку JJWT, хотя в принципе все реализации библиотек довольно похожи по своей функциональности. Также было решено не создавать сервер авторизации, а выдавать/обновлять/проверять токены непосредственно из приложения. Кстати немного о приложении, это одностраничное приложение (AngularJS) с RESTful Web-сервисом (Spring) в back-end.

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

    Для обновления access токена можно использовать refresh токен с более длительным временем до истечения, который получает новые данные пользователя по его идентификатору из базы данных и создает новый access токен на их основе. Но как быть с refresh токеном, если он также выдан на не очень долгое время и истечет во время сеанса пользователя? Повторный запрос авторизационных данных во время сеанса не очень хорошее решение в плане UX, а обновление истекшего токена плохо скажется на безопастности. Нужно было найти другое решение, но к этому вопросу я вернусь немного позже. Для начала нужно определиться с временем «жизни» для токенов и я остановился на 10 минутах для access токена и 60 минутах, с возможностью продления в перспективе, для refresh токена.

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

    В случае с локальным хранилищем, обычно способом передачи токенов между back-end и front-end является добавление их в header или в тело запроса/ответа. То есть, мне пришлось бы вносить изминения и во front-end, для получения токенов, сохранения их в локальное хранилище, добавления к запросам, обновления и т.д., тогда как в случае с cookies их можно добавлять/изменять/удалять непосредственно из back-end.

    Думаю, самое время перейти от теории к практике и показать, как это все реализовано у меня. Не хочу загружать статью лишним кодом, выкладывая все классы полностью, поэтому постараюсь выделить только самое важное. Если вас заинтересуют какие-либо подробности, которые остались «за кадром», спрашивайте, и я с радостью отвечу вам в комментариях или дополню статью.

    Естественно, взаимодействие с приложением начинается с авторизации. Пользователь авторизуется с помощью метода в @RestController, в котором вызывается метод из TokenAuthenticationService для создания токенов и добавления их в ответ сервера в виде cookies (access_token и refresh_token).

        @RequestMapping(value = "/login", produces = "application/json", method = RequestMethod.GET)
        @ResponseStatus(value = HttpStatus.NO_CONTENT)
        public void login(HttpServletResponse response) {
            SessionUser user = (SessionUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            tokenAuthenticationService.addAuthentication(response, user);
            SecurityContextHolder.getContext().setAuthentication(null);
        }

    SessionUser — реализация UserDetails*

    Так выглядит этот метод в TokenAuthenticationService:

        public void addAuthentication(HttpServletResponse response, SessionUser user) {
            Cookie access = new Cookie("access_token", tokenHandler.createAccessToken(user));
            access.setPath("/");
            access.setHttpOnly(true);
            response.addCookie(access);
    
            Cookie refresh = new Cookie("refresh_token", tokenHandler.createRefreshToken(user));
            refresh.setPath("/");
            refresh.setHttpOnly(true);
            response.addCookie(refresh);
        }

    TokenHandler содержит стандартные методы для работы c JWT с использованием Jwts.builder() и Jwts.parser() из указанной выше библиотеки, поэтому не вижу смысла занимать место их кодом, но для ясности напишу что делает каждый из них:

    public String createRefreshToken(SessionUser user) {
        //возвращает токен, в котором хранится только username
    }
    public SessionUser parseRefreshToken(String token) {
        //парсит username из токена и получает данные пользователя из реализации UserDetailsService
    }
    public String createAccessToken(SessionUser user) {
        //возвращает токен, в котором хранятся все данные для воссоздания SessionUser
    }
    public SessionUser parseAccessToken(String token) {
        //использует данные из токена для создания нового SessionUser
    }

    UserDetailsService*

    Теперь немного о том, как происходит обработка последующих запросов. В конфигурационном файле, наследованном от WebSecurityConfigurerAdapter, я добавил два известных вам bean-a:

        @Bean
        public TokenAuthenticationService tokenAuthenticationService() {
            return new TokenAuthenticationService();
        }
    
        @Bean
        public TokenHandler tokenHandler() {
            return new TokenHandler();
        }

    Запретил создание/использование сессий и добавил фильтр для аутентификации с помощью токенов:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            //Ресурсы доступные анонимным пользователям
            .antMatchers("/", "/login").permitAll()
            //Все остальные доступны только после аутентификации
            .anyRequest().authenticated()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService()), UsernamePasswordAuthenticationFilter.class)             
    }

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

    public class StatelessAuthenticationFilter extends GenericFilterBean {
    
        private final TokenAuthenticationService tokenAuthenticationService;
    
        public StatelessAuthenticationFilter(TokenAuthenticationService tokenAuthenticationService) {
            this.tokenAuthenticationService= tokenAuthenticationService;
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws IOException, ServletException {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            Authentication authentication = tokenAuthenticationService.getAuthentication(httpRequest, httpResponse);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            filterChain.doFilter(request, response);
            SecurityContextHolder.getContext().setAuthentication(null);
        }
    }

    Как происходит создание аутентификации:
    — Присутствует access токен?
    — Нет? Отклонить запрос.
    — Да?
    — — Действительный и не истек?
    — — Да? Разрешить запрос.
    — — Нет? Попробовать получить новый access токен при помощи refresh токена.
    — — — Получилось?
    — — — Да? Разрешить запрос, и добавить новый access токен к ответу.
    — — — Нет? Отклонить запрос.
    За это отвечает еще один метод из уже известного вам TokenAuthenticationService:

    public Authentication getAuthentication(HttpServletRequest request, HttpServletResponse response) {
            Cookie[] cookies = request.getCookies();
    
            String accessToken = null;
            String refreshToken = null;
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (("access_token").equals(cookie.getName())) {
                        accessToken = cookie.getValue();
                    }
                    if (("refresh_token").equals(cookie.getName())) {
                        refreshToken = cookie.getValue();
                    }
                }
            }
    
            if (accessToken != null && !accessToken.isEmpty()) {
                try {
                    SessionUser user = tokenHandler.parseAccessToken(accessToken);
                    return new UserAuthentication(user);
                } catch (ExpiredJwtException ex) {
                    if (refreshToken != null && !refreshToken.isEmpty()) {
                        try {
                            SessionUser user = tokenHandler.parseRefreshToken(refreshToken);
                            Cookie access = new Cookie("access_token", tokenHandler.createAccessToken(user));
                            access.setPath("/");
                            access.setHttpOnly(true);
                            response.addCookie(access);
                            return new UserAuthentication(user);
                        } catch (JwtException e) {
                            return null;
                        }
                    }
                    return null;
                } catch (JwtException ex) {
                    return null;
                }
            }
            return null;
        } 

    UserAuthentication — реализация Authentication*

    Вот и все, что требуется для совместной работы Spring Security и JWT.

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

    Первое, что пришло в голову, убрать из токена время истечения, а так как в моем случае cookies хранятся только до закрытия браузера, то токен бы просто удалялся и больше не использовался. Но здесь были свои «подводные камни», во первых в случае кражи токен мог бы использоваться неограниченное количество времени и не было бы возможности его отозвать, во вторых я хотел продлевать токен только для активного пользователя, а если пользователь просто оставит окно браузера открытым, то токен должен был истечь спустя некоторое время.

    Чтобы обезопасить токен от кражи, нужно было реализовать возможность отзыва токена, поэтому я решил хранить все refresh токены в базе данных и перед выдачей новых access токенов, проверять наличие refresh токена в ней. Тогда в случае отзыва токена, достаточно удалить его из базы данных. Но это не решало моей проблемы с продлением токенов. И тогда я подумал, а что если хранить время до истечения токена не в самом токене, а в базе данных? И отодвигать это время на 60 минут вперед от каждого использования токена. Мне это показалось довольно не плохой идеей и я хотел бы поделиться с вами ее реализацией.

    Я создал таблицу (refresh_token) в базе данных для хранения refresh токенов со следующими столбцами:
    1. id (BIGINT)
    2. username (VARCHAR)
    3. token (VARCHAR)
    4. expires (TIMESTAMP)
    И создал класс RefreshTokenDao с двумя методами использующими JdbcTemplate для общения с этой таблицей:

        public void insert(String username, String token, long expires) {
            String sql = "INSERT INTO refresh_token "
                    + "(username, token, expires) VALUES (?, ?, ?)";
            jdbcTemplate.update(sql, username, token, new Timestamp(expires));
        }
    
        public int updateIfNotExpired(String username, String token, long expiration) {
            String sql = "UPDATE refresh_token "
                    + "SET expires = ? "
                    + "WHERE username = ? "
                    + "AND token = ? "
                    + "AND expires > ?";
            Timestamp now = new Timestamp(System.currentTimeMillis());
            Timestamp newExpirationTime = new Timestamp(now.getTime() + expiration);
            return jdbcTemplate.update(sql, newExpirationTime, username, token, now);
        }

    Методы довольно простые. Первый используется для добавления токена в базу данных при его создании. Второй для обновления токена, если он не истек и возвращения количества обновленных полей, в моем случае это количество всегда будет 0 или 1.

    Используются они в классе TokenHandler. При создании токена (createRefreshToken()) я добавляю запись о нем с помощью метода insert(). А в случае когда мне нужно получить информацию из токена (parseRefreshToken()), я сначала пытаюсь вызвать updateIfNotExpired() и если в ответе получаю значение не равное 0, значит токен валиден и его дата обновилась, можно продолжать выполнение метода; а в случае, если возвращенное значение равно 0, из чего следует что токен не найден или истек, я просто выбрасываю исключение (new JwtException(«Token is expired or missing»)).

    Это все что касалось продления refresh токенов. На просторах интернета я нигде не встречал такого способа (может плохо искал) и надеюсь что он будет кому-то полезен, как и вся статья в целом.

    Спасибо за внимание!
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337600/


    Метки:  

    [Перевод] ggplot2: как легко совместить несколько графиков в одном, часть 2

    Воскресенье, 10 Сентября 2017 г. 19:44 + в цитатник
    qc-enior сегодня в 19:44 Разработка

    ggplot2: как легко совместить несколько графиков в одном, часть 2

    • Перевод
    • Tutorial
    Эта статья шаг за шагом покажет, как совместить несколько ggplot-графиков на одной или нескольких иллюстрациях, с помощью вспомогательных функций, доступных в пакетах R ggpubr, cowplot и gridExtra. Также опишем, как экспортировать полученные графики в файл.

    Изменение расположения по строкам или столбцам


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


    Воспользуемся вложенными функциями ggarrange() для изменения расположения графиков по строкам или столбцам.

    Например, код ниже делает следующее:

    • диаграмма разброса (sp) будет находиться в первой строке и занимать две колонки
    • диаграмма рассеивания (bxp) и точечная диаграмма (dp) будут занимать вторую строку и две разных колонки

    ggarrange(sp,  # Первая строка с диаграммой разброса
              ggarrange(bxp, dp, ncol = 2, labels = c("B", "C")), # Вторая строка с диаграммой рассеивания и точечной диаграммой
              nrow = 2, 
              labels = "A"   # Метки диаграммы разброса
              ) 



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


    Следующую комбинацию функций [из пакета cowplot] можно использовать, чтобы расположить графики в определенных местах заданного размера: ggdraw() + draw_plot() + draw_plot_label().

    ggdraw(). Инициализируем пустое полотно:


    ggdraw()

    Обратите внимание, что по умолчанию координаты изменяются от 0 до 1, и точка (0, 0) находится в нижнем левом углу полотна (см. иллюстрацию ниже).


    draw_plot(). Располагает график где-то на полотне:


    draw_plot(plot, x = 0, y = 0, width = 1, height = 1)

    • plot: график для размещения (ggplot2 или gtable)
    • x, y: координаты x/y левого нижнего угла графика
    • width, height: ширина и высота графика

    draw_plot_label(). Добавляет метку в правом верхнем углу графика


    Может работать с векторами меток с ассоциированными координатами.

    draw_plot_label(label, x = 0, y = 1, size = 16, ...)

    • label: вектор меток
    • x, y: вектор с х/у координатами каждой метки соответственно
    • size: размер шрифта метки

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

    library("cowplot")
    ggdraw() +
      draw_plot(bxp, x = 0, y = .5, width = .5, height = .5) +
      draw_plot(dp, x = .5, y = .5, width = .5, height = .5) +
      draw_plot(bp, x = 0, y = 0, width = 1, height = 0.5) +
      draw_plot_label(label = c("A", "B", "C"), size = 15,
                      x = c(0, 0.5, 0), y = c(1, 1, 0.5))



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


    Функция arrangeGrop()gridExtra] помогает изменить расположение графиков по строкам или столбцам.

    Например, код ниже делает следующее:

    • диаграмма разброса (sp) будет находиться в первой строке и занимать две колонки
    • диаграмма рассеивания (bxp) и точечная диаграмма (dp) будут занимать вторую строку и две разных колонки

    library("gridExtra")
    grid.arrange(sp,                            # Первая строка с одним графиком на две колонки
                 arrangeGrob(bxp, dp, ncol = 2),# Вторая строка с двумя графиками в двух колонках
                 nrow = 2)                      # Количество строк



    В функции grid.arrange() можно также использовать аргумент layout_matrix для создания сложного взаимного расположения графиков.

    В коде ниже layout_matrix — матрица 2х2 (2 строки и 2 столбца). Первая строка — все единицы, там, где первый график, занимающий две колонки; вторая строка содержит графики 2 и 3, каждый из которых занимает свою колонку.

    grid.arrange(bp,                                # столбчатая диаграмма на две колонки
                 bxp, sp,                               # диаграммы рассеивания и разброса
                 ncol = 2, nrow = 2, 
                 layout_matrix = rbind(c(1,1), c(2,3)))



    Также можно добавить аннотацию к выводу функции grid.arrange(), используя вспомогательную функцию draw_plot_label()cowplot].

    Для того, чтобы легко добавлять аннотации к выводам функций grid.arrange() или arrangeGrob() (тип gtable), сначала нужно преобразовать их в тип ggplot с помощью функции as_ggplot()ggpubr]. После можно применять к ним фунцию draw_plot_label()cowplot].

    library("gridExtra")
    library("cowplot")
    # Упорядочиваем графики с arrangeGrob
    # возвращает тип gtable (gt)
    gt <- arrangeGrob(bp,                               # столбчатая диаграмма на две колонки
                 bxp, sp,                               # диаграммы рассеивания и разброса
                 ncol = 2, nrow = 2, 
                 layout_matrix = rbind(c(1,1), c(2,3)))
    # Добавляем метки к упорядоченным графикам
    p <- as_ggplot(gt) +                                # преобразуем в ggplot
      draw_plot_label(label = c("A", "B", "C"), size = 15,
                      x = c(0, 0, 0.5), y = c(1, 0.5, 0.5)) # Добавляем метки
    p



    В коде выше мы использовали arrangeGrob() вместо grid.arrange(). Основное отличие этих двух функций состоит в том, что grid.arrange() автоматически выводит упорядоченные графики. Поскольку мы хотели добавить аннотацию к графикам до того, как их нарисовать, предпочтительно в таком случае использовать функцию arrangeGrob().

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


    Пакет grid позволяет задать сложное взаимное расположение графиков с помощью функции grid.layout(). Он также предоставляет вспомогательную функцию viewport()для задания региона, или области видимости. Функция print() применяется для размещения графиков в заданном регионе.

    Шаги можно описать так:

    1. Создать графики: p1, p2, p3, ….
    2. Перейти на новую страницу с помощью функции grid.newpage()
    3. Создать расположение 2X2 — количество столбцов = 2; количество строк = 2
    4. Задать область видимости: прямоугольная область на графическом устройстве
    5. Напечатать график в области видимости

    library(grid)
    # Перейти на новую страницу
    grid.newpage()
    # Создать расположение: nrow = 3, ncol = 2
    pushViewport(viewport(layout = grid.layout(nrow = 3, ncol = 2)))
    # Вспомогательная функция для задания области в расположении
    define_region <- function(row, col){
      viewport(layout.pos.row = row, layout.pos.col = col)
    } 
    # Упорядочить графики
    print(sp, vp = define_region(row = 1, col = 1:2))   # Расположить в двух колонках
    print(bxp, vp = define_region(row = 2, col = 1))
    print(dp, vp = define_region(row = 2, col = 2))
    print(bp + rremove("x.text"), vp = define_region(row = 3, col = 1:2))



    Использование общей легенды для совмещенных ggplot-графиков


    Чтобы задать общую легенду для нескольких упорядоченных графиков, можно использовать функцию ggarrange()ggpubr] с такими аргументами:

    • common.legend = TRUE: сделать общую легенду
    • legend: задать положение легенды. Разрешенное значение — одно из c(“top”, “bottom”, “left”, “right”)

    ggarrange(bxp, dp, labels = c("A", "B"),
              common.legend = TRUE, legend = "bottom")



    Диаграмма разброса с графиками плотности безусловного распределения


    # Диаграмма разброса, цвет по группе ("Species")
    sp <- ggscatter(iris, x = "Sepal.Length", y = "Sepal.Width",
                    color = "Species", palette = "jco",
                    size = 3, alpha = 0.6)+
      border()                                         
    # График плотности безусловного распределения по x (панель сверху) и по y (панель справа)
    xplot <- ggdensity(iris, "Sepal.Length", fill = "Species",
                       palette = "jco")
    yplot <- ggdensity(iris, "Sepal.Width", fill = "Species", 
                       palette = "jco")+
      rotate()
    # Почистить графики
    yplot <- yplot + clean_theme() 
    xplot <- xplot + clean_theme()
    # Упорядочить графики
    ggarrange(xplot, NULL, sp, yplot, 
              ncol = 2, nrow = 2,  align = "hv", 
              widths = c(2, 1), heights = c(1, 2),
              common.legend = TRUE)



    В следующей части:

    • смешиваем таблицу, текст и ggplot2-графики
    • добавляем графический элемент в ggplot (таблицу, диаграмму рассеивания, фоновое изображение)
    • располагаем графики на нескольких страницах
    • вложенное взаиморасположение
    • экспорт графиков
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337598/


    Метки:  

    [Перевод] ggplot2: как легко совместить несколько графиков в одном, часть 2

    Воскресенье, 10 Сентября 2017 г. 19:44 + в цитатник
    qc-enior сегодня в 19:44 Разработка

    ggplot2: как легко совместить несколько графиков в одном, часть 2

    • Перевод
    • Tutorial
    Эта статья шаг за шагом покажет, как совместить несколько ggplot-графиков на одной или нескольких иллюстрациях, с помощью вспомогательных функций, доступных в пакетах R ggpubr, cowplot и gridExtra. Также опишем, как экспортировать полученные графики в файл.

    Изменение расположения по строкам или столбцам


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


    Воспользуемся вложенными функциями ggarrange() для изменения расположения графиков по строкам или столбцам.

    Например, код ниже делает следующее:

    • диаграмма разброса (sp) будет находиться в первой строке и занимать две колонки
    • диаграмма рассеивания (bxp) и точечная диаграмма (dp) будут занимать вторую строку и две разных колонки

    ggarrange(sp,  # Первая строка с диаграммой разброса
              ggarrange(bxp, dp, ncol = 2, labels = c("B", "C")), # Вторая строка с диаграммой рассеивания и точечной диаграммой
              nrow = 2, 
              labels = "A"   # Метки диаграммы разброса
              ) 



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


    Следующую комбинацию функций [из пакета cowplot] можно использовать, чтобы расположить графики в определенных местах заданного размера: ggdraw() + draw_plot() + draw_plot_label().

    ggdraw(). Инициализируем пустое полотно:


    ggdraw()

    Обратите внимание, что по умолчанию координаты изменяются от 0 до 1, и точка (0, 0) находится в нижнем левом углу полотна (см. иллюстрацию ниже).


    draw_plot(). Располагает график где-то на полотне:


    draw_plot(plot, x = 0, y = 0, width = 1, height = 1)

    • plot: график для размещения (ggplot2 или gtable)
    • x, y: координаты x/y левого нижнего угла графика
    • width, height: ширина и высота графика

    draw_plot_label(). Добавляет метку в правом верхнем углу графика


    Может работать с векторами меток с ассоциированными координатами.

    draw_plot_label(label, x = 0, y = 1, size = 16, ...)

    • label: вектор меток
    • x, y: вектор с х/у координатами каждой метки соответственно
    • size: размер шрифта метки

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

    library("cowplot")
    ggdraw() +
      draw_plot(bxp, x = 0, y = .5, width = .5, height = .5) +
      draw_plot(dp, x = .5, y = .5, width = .5, height = .5) +
      draw_plot(bp, x = 0, y = 0, width = 1, height = 0.5) +
      draw_plot_label(label = c("A", "B", "C"), size = 15,
                      x = c(0, 0.5, 0), y = c(1, 1, 0.5))



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


    Функция arrangeGrop()gridExtra] помогает изменить расположение графиков по строкам или столбцам.

    Например, код ниже делает следующее:

    • диаграмма разброса (sp) будет находиться в первой строке и занимать две колонки
    • диаграмма рассеивания (bxp) и точечная диаграмма (dp) будут занимать вторую строку и две разных колонки

    library("gridExtra")
    grid.arrange(sp,                            # Первая строка с одним графиком на две колонки
                 arrangeGrob(bxp, dp, ncol = 2),# Вторая строка с двумя графиками в двух колонках
                 nrow = 2)                      # Количество строк



    В функции grid.arrange() можно также использовать аргумент layout_matrix для создания сложного взаимного расположения графиков.

    В коде ниже layout_matrix — матрица 2х2 (2 строки и 2 столбца). Первая строка — все единицы, там, где первый график, занимающий две колонки; вторая строка содержит графики 2 и 3, каждый из которых занимает свою колонку.

    grid.arrange(bp,                                # столбчатая диаграмма на две колонки
                 bxp, sp,                               # диаграммы рассеивания и разброса
                 ncol = 2, nrow = 2, 
                 layout_matrix = rbind(c(1,1), c(2,3)))



    Также можно добавить аннотацию к выводу функции grid.arrange(), используя вспомогательную функцию draw_plot_label()cowplot].

    Для того, чтобы легко добавлять аннотации к выводам функций grid.arrange() или arrangeGrob() (тип gtable), сначала нужно преобразовать их в тип ggplot с помощью функции as_ggplot()ggpubr]. После можно применять к ним фунцию draw_plot_label()cowplot].

    library("gridExtra")
    library("cowplot")
    # Упорядочиваем графики с arrangeGrob
    # возвращает тип gtable (gt)
    gt <- arrangeGrob(bp,                               # столбчатая диаграмма на две колонки
                 bxp, sp,                               # диаграммы рассеивания и разброса
                 ncol = 2, nrow = 2, 
                 layout_matrix = rbind(c(1,1), c(2,3)))
    # Добавляем метки к упорядоченным графикам
    p <- as_ggplot(gt) +                                # преобразуем в ggplot
      draw_plot_label(label = c("A", "B", "C"), size = 15,
                      x = c(0, 0, 0.5), y = c(1, 0.5, 0.5)) # Добавляем метки
    p



    В коде выше мы использовали arrangeGrob() вместо grid.arrange(). Основное отличие этих двух функций состоит в том, что grid.arrange() автоматически выводит упорядоченные графики. Поскольку мы хотели добавить аннотацию к графикам до того, как их нарисовать, предпочтительно в таком случае использовать функцию arrangeGrob().

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


    Пакет grid позволяет задать сложное взаимное расположение графиков с помощью функции grid.layout(). Он также предоставляет вспомогательную функцию viewport()для задания региона, или области видимости. Функция print() применяется для размещения графиков в заданном регионе.

    Шаги можно описать так:

    1. Создать графики: p1, p2, p3, ….
    2. Перейти на новую страницу с помощью функции grid.newpage()
    3. Создать расположение 2X2 — количество столбцов = 2; количество строк = 2
    4. Задать область видимости: прямоугольная область на графическом устройстве
    5. Напечатать график в области видимости

    library(grid)
    # Перейти на новую страницу
    grid.newpage()
    # Создать расположение: nrow = 3, ncol = 2
    pushViewport(viewport(layout = grid.layout(nrow = 3, ncol = 2)))
    # Вспомогательная функция для задания области в расположении
    define_region <- function(row, col){
      viewport(layout.pos.row = row, layout.pos.col = col)
    } 
    # Упорядочить графики
    print(sp, vp = define_region(row = 1, col = 1:2))   # Расположить в двух колонках
    print(bxp, vp = define_region(row = 2, col = 1))
    print(dp, vp = define_region(row = 2, col = 2))
    print(bp + rremove("x.text"), vp = define_region(row = 3, col = 1:2))



    Использование общей легенды для совмещенных ggplot-графиков


    Чтобы задать общую легенду для нескольких упорядоченных графиков, можно использовать функцию ggarrange()ggpubr] с такими аргументами:

    • common.legend = TRUE: сделать общую легенду
    • legend: задать положение легенды. Разрешенное значение — одно из c(“top”, “bottom”, “left”, “right”)

    ggarrange(bxp, dp, labels = c("A", "B"),
              common.legend = TRUE, legend = "bottom")



    Диаграмма разброса с графиками плотности безусловного распределения


    # Диаграмма разброса, цвет по группе ("Species")
    sp <- ggscatter(iris, x = "Sepal.Length", y = "Sepal.Width",
                    color = "Species", palette = "jco",
                    size = 3, alpha = 0.6)+
      border()                                         
    # График плотности безусловного распределения по x (панель сверху) и по y (панель справа)
    xplot <- ggdensity(iris, "Sepal.Length", fill = "Species",
                       palette = "jco")
    yplot <- ggdensity(iris, "Sepal.Width", fill = "Species", 
                       palette = "jco")+
      rotate()
    # Почистить графики
    yplot <- yplot + clean_theme() 
    xplot <- xplot + clean_theme()
    # Упорядочить графики
    ggarrange(xplot, NULL, sp, yplot, 
              ncol = 2, nrow = 2,  align = "hv", 
              widths = c(2, 1), heights = c(1, 2),
              common.legend = TRUE)



    В следующей части:

    • смешиваем таблицу, текст и ggplot2-графики
    • добавляем графический элемент в ggplot (таблицу, диаграмму рассеивания, фоновое изображение)
    • располагаем графики на нескольких страницах
    • вложенное взаиморасположение
    • экспорт графиков
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337598/


    Метки:  

    Открытые проблемы в области распознавания речи. Лекция в Яндексе

    Воскресенье, 10 Сентября 2017 г. 18:57 + в цитатник
    Leono сегодня в 18:57 Разработка

    Открытые проблемы в области распознавания речи. Лекция в Яндексе

      Работа большинства специалистов по речевым технологиям состоит не в том, чтобы придумывать концептуально новые алгоритмы. Компании в основном фокусируются на существующих подходах. Машинный интеллект уже умеет распознавать и синтезировать голос, но не всегда в реальном времени, не всегда локально и не всегда «избирательно» — когда нужно реагировать только на ключевые фразы, робот может ошибаться. Подобными проблемами как раз и заняты разработчики. Муаммар Аль-Шедиват @Laytlas рассказывает об этих и других вопросах, которые пока не удаётся решить даже большим компаниям.




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

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

      Общая схема распознавания речи. Изначально на вход нам поступает звуковая волна.

      Ее мы дробим на маленькие кусочки, фреймы. Длина фрейма — обычно 25 мс, шаг — 10 мс. Они идут с некоторым захлестом.

      После этого из фреймов мы извлекаем наиболее важные признаки. Допустим, нам не важен тембр голоса или пол человека. Мы хотим распознавать речь вне зависимости от этих факторов, так что мы извлекаем самые важные признаки.

      Затем нейронная сеть натравливается на все это и выдает на каждом фрейме предсказание, распределение вероятностей по фонемам. Нейронка старается угадать, какая именно фонема была сказана на том или ином фрейме.

      Под конец все это запихивается в граф-декодирование, которое получает распределение вероятностей и учитывает языковую модель. Допустим, «Мама мыла раму» — более популярная фраза в русском языке, чем «Мама мыла Рому». Также учитывается произношение слов и выдаются итоговые гипотезы.

      В целом, именно так и происходит распознавание речи.

      Естественно, о метрике нужно пару слов сказать. Все используют метрику WER в распознавании речи. Она переводится как World Error Rate. Это просто расстояние по Левенштейну от того, что мы распознали, до того, что реально было сказано в фразе, поделить на количество слов, реально сказанных во фразе.

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



      Как мы будем это улучшать? Я выделил четыре основных подхода, которые пересекаются друг с другом, но на это не стоит обращать внимания. Основные подходы следующие: улучшим архитектуру нейронных сетей, попробуем изменить Loss-функцию, почему бы не использовать подходы End to end, модные в последнее время. И в заключение расскажу про другие задачи, для которых, допустим, не нужно декодирование.

      Когда люди придумали использовать нейронные сети, естественным решением было использовать самое простое: нейронные сетки feed forward. Берем фрейм, контекст, сколько-то фреймов слева, сколько-то справа, и предсказываем, какая фонема была сказана на данном фрейме. После чего можно посмотреть на все это как на картинку и применить всю артиллерию, уже использованную для обработки изображений, всевозможные сверточные нейронные сети.

      Вообще многие статьи state of the art получены именно с помощью сверточных нейронных сетей, но сегодня я расскажу больше о рекуррентных нейронных сетях.

      Рекуррентные нейронные сети. Все знают, как они работают. Но возникает большая проблема: обычно фреймов намного больше, чем фонем. На одну фонему приходится 10, а то и 20 фреймов. С этим нужно как-то бороться. Обычно это зашивается в граф-декодирование, где мы остаемся в одном состоянии много шагов. В принципе, с этим можно как-то бороться, есть парадигма encoder-decoder. Давайте сделаем две рекуррентных нейронных сетки: одна будет кодировать всю информацию и выдавать скрытое состояние, а декодер будет брать это состояние и выдавать последовательность фонем, букв или, может быть, слов — это как вы натренируете нейронную сеть.

      Обычно в распознавании речи мы работаем с очень большими последовательностями. Там спокойно бывает 1000 фреймов, которые нужно закодировать одним скрытым состоянием. Это нереально, ни одна нейронная сеть с этим не справится. Давайте использовать другие методы.

      Димой Богдановым, выпускником ШАД, был придуман метод Attention. Давайте encoder будет выдавать скрытые состояния, и мы их не будем выкидывать, а оставим только последнее. Возьмем взвешенную сумму на каждом шаге. Декодер будет брать взвешенную сумму скрытых состояний. Таким образом, мы будем сохранять контекст, то, на что мы в конкретном случае смотрим.

      Подход прекрасный, работает хорошо, на некоторых датасетах дает результаты state of the art, но есть один большой минус. Мы хотим распознавать речь в онлайне: человек сказал 10-секундную фразу, и мы сразу ему выдали результат. Но Attention требует знать фразу целиком, в этом его большая проблема. Человек скажет 10-секундную фразу, 10 секунд мы ее будем распознавать. За это время он удалит приложение и никогда больше не установит. Нужно с этим бороться. Совсем недавно с этим поборолись в одной из статей. Я назвал это online attention.

      Давайте поделим входную последовательность на блоки какой-то небольшой фиксированной длины, внутри каждого блока устроим Attention, затем будет декодер, который выдает на каждом блоке соответствующие символы, после чего в какой-то момент выдает символ end of block, перемещается к следующему блоку, поскольку мы тут исчерпали всю информацию.

      Тут можно серию лекций прочитать, я постараюсь просто сформулировать идею.

      Когда начали тренировать нейронные сети для распознавания речи, старались угадывать фонему. Для этого использовали обычную кросс-энтропийную функцию потерь. Проблема в том, что даже если мы соптимизируем кросс-энтропию, это еще не будет значить, что мы хорошо соптимизировали WER, потому что у этих метрик корреляция не 100%.

      Чтобы с этим побороться, были придуманы функции Sequence Based Loss: давайте саккумулируем всю информацию на всех фреймах, посчитаем один общий Loss и пропустим градиент обратно. Не буду вдаваться в детали, можете прочитать про CTC или SNBR Loss, это очень специфичная тема для распознавания речи.

      В подходах End to end два пути. Первый — делать более «сырые» фичи. У нас был момент, когда мы извлекали из фреймов фичи, и обычно они извлекаются, стараясь эмулировать ухо человека. А зачем эмулировать ухо человека? Пусть нейронка сама научится и поймет, какие фичи ей полезны, а какие бесполезны. Давайте в нейронку подавать все более сырые фичи.

      Второй подход. Мы пользователям выдаем слова, буквенное представление. Так зачем нам предсказывать фонемы? Хотя их предсказывать очень естественно, человек говорит в фонемах, а не буквах, — но итоговый результат мы должны выдать именно в буквах. Поэтому давайте предсказывать буквы, слоги или пары символов.

      Какие еще есть задачи? Допустим, задача фреймспоттинга. Есть какой-нибудь кусок звука, откуда надо извлечь информацию о том, была ли сказана фраза «Слушай, Яндекс» или не была. Для этого можно фразу распознать и грепнуть «Слушай, Яндекс», но это очень брутфорсный подход, причем распознавание обычно работает на серверах, модели очень большие. Обычно звук отсылается на сервер, распознается, и распознанная форма высылается обратно. Грузить 100 тыс. юзеров каждую секунду, слать звук на сервер — ни одни сервера не выдержат.

      Надо придумать решение, которое будет маленьким, сможет работать на телефоне и не будет жрать батарейку. И будет обладать хорошим качеством.

      Для этого давайте всё запихнем в нейронную сеть. Она просто будет предсказывать, к примеру, не фонемы и не буквы, а целые слова. И сделаем просто три класса. Сеть будет предсказывать слова «слушай» и «Яндекс», а все остальные слова замапим в филлер.

      Таким образом, если в какой-то момент сначала шли большие вероятности для «слушай», потом большие вероятности для «Яндекс», то с большой вероятностью тут была ключевая фраза «Слушай, Яндекс».

      Задача, которая не сильно исследуется в статьях. Обычно, когда пишутся статьи, берется какой-то датасет, на нем получаются хорошие результаты, бьется state of the art — ура, печатаем статью. Проблема этого подхода в том, что многие датасеты не меняются в течение 10, а то и 20 лет. И они не сталкиваются с проблемами, с которыми сталкиваемся мы.

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

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

      Какие задачи остались? Там state of the art побили, тут задачи решили… Приведу график WER за последние несколько лет.

      Как видите, Яндекс улучшился за последние несколько лет, и тут приводится график для лучшей тематики — геопоиска. Вы можете понять, что мы стараемся и улучшаемся, но есть тот маленький разрыв, который нужно побить. И даже если мы сделаем распознавание речи — а мы его сделаем, — которое сравнится со способностями человека, то возникнет другая задача: это сделалось на сервере, но давайте перенесем это на устройство. Речь идет об отдельной, сложной и интересной задаче.

      У нас есть и множество других задач, о которых меня можно спросить. Спасибо за внимание.
      Original source: habrahabr.ru (comments, light).

      https://habrahabr.ru/post/337572/


      Открытые проблемы в области распознавания речи. Лекция в Яндексе

      Воскресенье, 10 Сентября 2017 г. 18:57 + в цитатник
      Leono сегодня в 18:57 Разработка

      Открытые проблемы в области распознавания речи. Лекция в Яндексе

        Работа большинства специалистов по речевым технологиям состоит не в том, чтобы придумывать концептуально новые алгоритмы. Компании в основном фокусируются на существующих подходах. Машинный интеллект уже умеет распознавать и синтезировать голос, но не всегда в реальном времени, не всегда локально и не всегда «избирательно» — когда нужно реагировать только на ключевые фразы, робот может ошибаться. Подобными проблемами как раз и заняты разработчики. Муаммар Аль-Шедиват @Laytlas рассказывает об этих и других вопросах, которые пока не удаётся решить даже большим компаниям.




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

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

        Общая схема распознавания речи. Изначально на вход нам поступает звуковая волна.

        Ее мы дробим на маленькие кусочки, фреймы. Длина фрейма — обычно 25 мс, шаг — 10 мс. Они идут с некоторым захлестом.

        После этого из фреймов мы извлекаем наиболее важные признаки. Допустим, нам не важен тембр голоса или пол человека. Мы хотим распознавать речь вне зависимости от этих факторов, так что мы извлекаем самые важные признаки.

        Затем нейронная сеть натравливается на все это и выдает на каждом фрейме предсказание, распределение вероятностей по фонемам. Нейронка старается угадать, какая именно фонема была сказана на том или ином фрейме.

        Под конец все это запихивается в граф-декодирование, которое получает распределение вероятностей и учитывает языковую модель. Допустим, «Мама мыла раму» — более популярная фраза в русском языке, чем «Мама мыла Рому». Также учитывается произношение слов и выдаются итоговые гипотезы.

        В целом, именно так и происходит распознавание речи.

        Естественно, о метрике нужно пару слов сказать. Все используют метрику WER в распознавании речи. Она переводится как World Error Rate. Это просто расстояние по Левенштейну от того, что мы распознали, до того, что реально было сказано в фразе, поделить на количество слов, реально сказанных во фразе.

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



        Как мы будем это улучшать? Я выделил четыре основных подхода, которые пересекаются друг с другом, но на это не стоит обращать внимания. Основные подходы следующие: улучшим архитектуру нейронных сетей, попробуем изменить Loss-функцию, почему бы не использовать подходы End to end, модные в последнее время. И в заключение расскажу про другие задачи, для которых, допустим, не нужно декодирование.

        Когда люди придумали использовать нейронные сети, естественным решением было использовать самое простое: нейронные сетки feed forward. Берем фрейм, контекст, сколько-то фреймов слева, сколько-то справа, и предсказываем, какая фонема была сказана на данном фрейме. После чего можно посмотреть на все это как на картинку и применить всю артиллерию, уже использованную для обработки изображений, всевозможные сверточные нейронные сети.

        Вообще многие статьи state of the art получены именно с помощью сверточных нейронных сетей, но сегодня я расскажу больше о рекуррентных нейронных сетях.

        Рекуррентные нейронные сети. Все знают, как они работают. Но возникает большая проблема: обычно фреймов намного больше, чем фонем. На одну фонему приходится 10, а то и 20 фреймов. С этим нужно как-то бороться. Обычно это зашивается в граф-декодирование, где мы остаемся в одном состоянии много шагов. В принципе, с этим можно как-то бороться, есть парадигма encoder-decoder. Давайте сделаем две рекуррентных нейронных сетки: одна будет кодировать всю информацию и выдавать скрытое состояние, а декодер будет брать это состояние и выдавать последовательность фонем, букв или, может быть, слов — это как вы натренируете нейронную сеть.

        Обычно в распознавании речи мы работаем с очень большими последовательностями. Там спокойно бывает 1000 фреймов, которые нужно закодировать одним скрытым состоянием. Это нереально, ни одна нейронная сеть с этим не справится. Давайте использовать другие методы.

        Димой Богдановым, выпускником ШАД, был придуман метод Attention. Давайте encoder будет выдавать скрытые состояния, и мы их не будем выкидывать, а оставим только последнее. Возьмем взвешенную сумму на каждом шаге. Декодер будет брать взвешенную сумму скрытых состояний. Таким образом, мы будем сохранять контекст, то, на что мы в конкретном случае смотрим.

        Подход прекрасный, работает хорошо, на некоторых датасетах дает результаты state of the art, но есть один большой минус. Мы хотим распознавать речь в онлайне: человек сказал 10-секундную фразу, и мы сразу ему выдали результат. Но Attention требует знать фразу целиком, в этом его большая проблема. Человек скажет 10-секундную фразу, 10 секунд мы ее будем распознавать. За это время он удалит приложение и никогда больше не установит. Нужно с этим бороться. Совсем недавно с этим поборолись в одной из статей. Я назвал это online attention.

        Давайте поделим входную последовательность на блоки какой-то небольшой фиксированной длины, внутри каждого блока устроим Attention, затем будет декодер, который выдает на каждом блоке соответствующие символы, после чего в какой-то момент выдает символ end of block, перемещается к следующему блоку, поскольку мы тут исчерпали всю информацию.

        Тут можно серию лекций прочитать, я постараюсь просто сформулировать идею.

        Когда начали тренировать нейронные сети для распознавания речи, старались угадывать фонему. Для этого использовали обычную кросс-энтропийную функцию потерь. Проблема в том, что даже если мы соптимизируем кросс-энтропию, это еще не будет значить, что мы хорошо соптимизировали WER, потому что у этих метрик корреляция не 100%.

        Чтобы с этим побороться, были придуманы функции Sequence Based Loss: давайте саккумулируем всю информацию на всех фреймах, посчитаем один общий Loss и пропустим градиент обратно. Не буду вдаваться в детали, можете прочитать про CTC или SNBR Loss, это очень специфичная тема для распознавания речи.

        В подходах End to end два пути. Первый — делать более «сырые» фичи. У нас был момент, когда мы извлекали из фреймов фичи, и обычно они извлекаются, стараясь эмулировать ухо человека. А зачем эмулировать ухо человека? Пусть нейронка сама научится и поймет, какие фичи ей полезны, а какие бесполезны. Давайте в нейронку подавать все более сырые фичи.

        Второй подход. Мы пользователям выдаем слова, буквенное представление. Так зачем нам предсказывать фонемы? Хотя их предсказывать очень естественно, человек говорит в фонемах, а не буквах, — но итоговый результат мы должны выдать именно в буквах. Поэтому давайте предсказывать буквы, слоги или пары символов.

        Какие еще есть задачи? Допустим, задача фреймспоттинга. Есть какой-нибудь кусок звука, откуда надо извлечь информацию о том, была ли сказана фраза «Слушай, Яндекс» или не была. Для этого можно фразу распознать и грепнуть «Слушай, Яндекс», но это очень брутфорсный подход, причем распознавание обычно работает на серверах, модели очень большие. Обычно звук отсылается на сервер, распознается, и распознанная форма высылается обратно. Грузить 100 тыс. юзеров каждую секунду, слать звук на сервер — ни одни сервера не выдержат.

        Надо придумать решение, которое будет маленьким, сможет работать на телефоне и не будет жрать батарейку. И будет обладать хорошим качеством.

        Для этого давайте всё запихнем в нейронную сеть. Она просто будет предсказывать, к примеру, не фонемы и не буквы, а целые слова. И сделаем просто три класса. Сеть будет предсказывать слова «слушай» и «Яндекс», а все остальные слова замапим в филлер.

        Таким образом, если в какой-то момент сначала шли большие вероятности для «слушай», потом большие вероятности для «Яндекс», то с большой вероятностью тут была ключевая фраза «Слушай, Яндекс».

        Задача, которая не сильно исследуется в статьях. Обычно, когда пишутся статьи, берется какой-то датасет, на нем получаются хорошие результаты, бьется state of the art — ура, печатаем статью. Проблема этого подхода в том, что многие датасеты не меняются в течение 10, а то и 20 лет. И они не сталкиваются с проблемами, с которыми сталкиваемся мы.

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

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

        Какие задачи остались? Там state of the art побили, тут задачи решили… Приведу график WER за последние несколько лет.

        Как видите, Яндекс улучшился за последние несколько лет, и тут приводится график для лучшей тематики — геопоиска. Вы можете понять, что мы стараемся и улучшаемся, но есть тот маленький разрыв, который нужно побить. И даже если мы сделаем распознавание речи — а мы его сделаем, — которое сравнится со способностями человека, то возникнет другая задача: это сделалось на сервере, но давайте перенесем это на устройство. Речь идет об отдельной, сложной и интересной задаче.

        У нас есть и множество других задач, о которых меня можно спросить. Спасибо за внимание.
        Original source: habrahabr.ru (comments, light).

        https://habrahabr.ru/post/337572/


        Управление сертификатами с помощью протокола ACME

        Воскресенье, 10 Сентября 2017 г. 16:45 + в цитатник
        dernasherbrezon сегодня в 16:45 Разработка

        Управление сертификатами с помощью протокола ACME

          Возникла передо мной такая задача: автоматический выпуск сертификатов для Web приложения. И требования:


          • CA должны доверять все браузеры т.е. самоподписанные сертификаты не подходят;
          • желательно бесплатно;
          • Выпуск надо делать программно с помощью Java Embedded compact1 profile. Это всё по следам Java и без 16Gb памяти?.

          Наверное многие уже слышали про бесплатные сертификаты от LetsEncrypt и certbot. А можно ли certbot заменить Java?



          ACME


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


          Основные особенности протокола:


          • Описывает взаимодействие клиента и REST сервера;
          • Есть поддержка как платных сертификатов, так и бесплатных;
          • Несколько способов авторизации владения доменом;
          • Внесен на принятие в IETF. Сейчас находится в состоянии draft;
          • Все сообщения передаются в формате JSON Web Token.

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



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


          На официальном сайте LetsEncrypt есть множество клиентов работающих по протоколу ACME. Я взял acme4j. Эта библиотека достаточно компактная и работает в compact1 profile!


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


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


          Единственная проблема, которая у меня возникла — это подкладывание сертификата в nginx. Поясню на примере:


          • приложение стартует в первый раз;
          • nginx стартует. Поскольку приложение стартует в первый раз, то сертификата ещё нет, и nginx слушает на 80 порту;
          • пользователь заходит в приложение, соглашается с правилами использования сертификатов LetsEncrypt и нажимает кнопку "выдать сертификат";
          • сертификат скачивается.

          И вот тут проблема: для того чтобы включить 443 порт с новым сертификатом, nginx должен перезачитать конфигурацию. Но чтобы это сделать нужен root. Запускать приложение из под root — плохая идея. Запускать nginx из под пользователя тоже — нельзя будет слушать 80/443 порты.


          Я добавил правило для пользователя в sudoers, чтобы можно было делать sudo nginx -s reload. Но это выглядит как костыль. Может кто-нибудь знает как это сделать красивее?


          Итого


          Получение сертификата в автоматическом режиме оказалось достаточно простой процедурой. А если использовать ACME сервер boulder, то можно даже развернуть такую схему у себя в большой организации! Если у вас есть собственный CA для внутренних сервисов, то ACME должен сильно упростить работу с сертификатами.

          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/337592/


          Метки:  

          Управление сертификатами с помощью протокола ACME

          Воскресенье, 10 Сентября 2017 г. 16:45 + в цитатник
          dernasherbrezon сегодня в 16:45 Разработка

          Управление сертификатами с помощью протокола ACME

            Возникла передо мной такая задача: автоматический выпуск сертификатов для Web приложения. И требования:


            • CA должны доверять все браузеры т.е. самоподписанные сертификаты не подходят;
            • желательно бесплатно;
            • Выпуск надо делать программно с помощью Java Embedded compact1 profile. Это всё по следам Java и без 16Gb памяти?.

            Наверное многие уже слышали про бесплатные сертификаты от LetsEncrypt и certbot. А можно ли certbot заменить Java?



            ACME


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


            Основные особенности протокола:


            • Описывает взаимодействие клиента и REST сервера;
            • Есть поддержка как платных сертификатов, так и бесплатных;
            • Несколько способов авторизации владения доменом;
            • Внесен на принятие в IETF. Сейчас находится в состоянии draft;
            • Все сообщения передаются в формате JSON Web Token.

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



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


            На официальном сайте LetsEncrypt есть множество клиентов работающих по протоколу ACME. Я взял acme4j. Эта библиотека достаточно компактная и работает в compact1 profile!


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


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


            Единственная проблема, которая у меня возникла — это подкладывание сертификата в nginx. Поясню на примере:


            • приложение стартует в первый раз;
            • nginx стартует. Поскольку приложение стартует в первый раз, то сертификата ещё нет, и nginx слушает на 80 порту;
            • пользователь заходит в приложение, соглашается с правилами использования сертификатов LetsEncrypt и нажимает кнопку "выдать сертификат";
            • сертификат скачивается.

            И вот тут проблема: для того чтобы включить 443 порт с новым сертификатом, nginx должен перезачитать конфигурацию. Но чтобы это сделать нужен root. Запускать приложение из под root — плохая идея. Запускать nginx из под пользователя тоже — нельзя будет слушать 80/443 порты.


            Я добавил правило для пользователя в sudoers, чтобы можно было делать sudo nginx -s reload. Но это выглядит как костыль. Может кто-нибудь знает как это сделать красивее?


            Итого


            Получение сертификата в автоматическом режиме оказалось достаточно простой процедурой. А если использовать ACME сервер boulder, то можно даже развернуть такую схему у себя в большой организации! Если у вас есть собственный CA для внутренних сервисов, то ACME должен сильно упростить работу с сертификатами.

            Original source: habrahabr.ru (comments, light).

            https://habrahabr.ru/post/337592/


            Метки:  

            Дайджест интересных материалов для мобильного разработчика #220 (4 сентября — 10 сентября)

            Воскресенье, 10 Сентября 2017 г. 15:31 + в цитатник
            EverydayTools сегодня в 15:31 Разработка

            Дайджест интересных материалов для мобильного разработчика #220 (4 сентября — 10 сентября)

              Пока мы готовимся к премьере новых iPhone, давайте посмотрим на последний отсчет Google, дизайн ВКонтакте, использование CoreML и преимущества ARCore, посчитаем математику кликеров и иллюзию выбора. Все это и многое другое в новом дайджесте!



              Последний отсчёт — Гугл развлекается

              Некоторое время назад я писал о смешном методе-проверке «А не козёл ли ты, пользователь?». Сегодня обнаружил ещё один забавный метод для новенькой Android 8.0.

              VK by design

              ВКонтакте — это множество интерфейсов для разных платформ. Сегодня Вы узнаете о том, как мы работаем над этими интерфейсами, создавая дизайн своих продуктов.

              Цвет в дизайне интерфейсов: инструкция по применению

              Будучи по большому счёту дизайнером-самоучкой, мне всегда было интересно, почему так много статей и сайтов говорят о теории цвета и цветовых палитрах. По моему опыту, вероятность сделать красивый дизайн, использую «сплит-комплиментарную палитру», стремится к нулю.

              Дайджест доступен и в виде рассылки. Подписаться вы можете тут.

              iOS


              Android


              Разработка


              Аналитика, маркетинг и монетизация


              Устройства, IoT, AI



              <- Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку — пришлите, пожалуйста, в почту.
              Original source: habrahabr.ru (comments, light).

              https://habrahabr.ru/post/337586/


              Дайджест интересных материалов для мобильного разработчика #220 (4 сентября — 10 сентября)

              Воскресенье, 10 Сентября 2017 г. 15:31 + в цитатник
              EverydayTools сегодня в 15:31 Разработка

              Дайджест интересных материалов для мобильного разработчика #220 (4 сентября — 10 сентября)

                Пока мы готовимся к премьере новых iPhone, давайте посмотрим на последний отсчет Google, дизайн ВКонтакте, использование CoreML и преимущества ARCore, посчитаем математику кликеров и иллюзию выбора. Все это и многое другое в новом дайджесте!



                Последний отсчёт — Гугл развлекается

                Некоторое время назад я писал о смешном методе-проверке «А не козёл ли ты, пользователь?». Сегодня обнаружил ещё один забавный метод для новенькой Android 8.0.

                VK by design

                ВКонтакте — это множество интерфейсов для разных платформ. Сегодня Вы узнаете о том, как мы работаем над этими интерфейсами, создавая дизайн своих продуктов.

                Цвет в дизайне интерфейсов: инструкция по применению

                Будучи по большому счёту дизайнером-самоучкой, мне всегда было интересно, почему так много статей и сайтов говорят о теории цвета и цветовых палитрах. По моему опыту, вероятность сделать красивый дизайн, использую «сплит-комплиментарную палитру», стремится к нулю.

                Дайджест доступен и в виде рассылки. Подписаться вы можете тут.

                iOS


                Android


                Разработка


                Аналитика, маркетинг и монетизация


                Устройства, IoT, AI



                <- Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку — пришлите, пожалуйста, в почту.
                Original source: habrahabr.ru (comments, light).

                https://habrahabr.ru/post/337586/


                Валидация React компонентов с помощью Livr.js

                Воскресенье, 10 Сентября 2017 г. 14:10 + в цитатник
                one_more сегодня в 14:10 Разработка

                Валидация React компонентов с помощью Livr.js

                  Пару лет назад я увидел на хабре статью про LIVR и с тех пор использую библиотеку на всех проектах. С переходом на React я адаптировал для валидации ее же, т.к. существующие решения не предлагали гибкости которой мне хотелось. Свое решение я уже использую на двух проектах и решил выложить в npm — может кому-то еще оно покажетсяя удобным.
                  Пакет называется react-livr-validation.

                  Пример базового использвания
                  import React from 'react';
                  import Validation, {DisabledOnErrors, ValidationInput} from 'react-livr-validation';
                  
                  const schema = {
                      login: ['required', 'not_empty'],
                      password: ['required', 'not_empty']
                  };
                  
                  const data = {
                      login: '',
                      password: ''
                  };
                  
                  export default function() {
                      return (
                          
                              
                  ); }

                  Компонент принимает валидационную схему и первоначальные данные(если данные не валидны, кнопка submit сразу будет неактивна), так же можно передать custom rules и aliased rules
                  const customRules = {
                      alpha_chars: function() {
                          return function(value) {
                              if (typeof value === 'string') {
                                  if (!/[a-z,A-Z]+/.test(value)) {
                                      return 'WRONG_FORMAT';
                                  }
                              }
                          };
                      }
                  };
                  const aliasedRules = [
                      {
                          name: 'strong_password',
                          rules: { min_length: 6 },
                          error: 'TOO_SHORT'
                      }
                  ];
                  
                        // ... form
                  
                  

                  Обертка ValidationInput добавляет в инпут свои обработчики событий, на которые будет происходить валидация. По умолчанию это события change, blur, keyup.
                  
                          
                  
                  

                  Есть возможность реализовать свою обертку — пакет экпортит HOC, который прокидывает в пропсы api
                  // @flow
                  
                  import React, {Component} from 'react'
                  import {ValidationComponent} from 'react-livr-validation'
                  import get from 'lodash/get'
                  import noop from 'lodash/noop'
                  import compose from 'ramda/src/compose'
                  import styled from 'styled-components'
                  
                  type DataChunk = {
                      name: string,
                      value: any
                  }
                  
                  type State = {
                      touched: boolean
                  }
                  
                  type Props = {
                      // will be passed by HOC
                      setData: (data: DataChunk) => void,
                      getError: (name: string) => ?string,
                      getErrors: () => Object,
                      className: string, // for the error block
                      style: Object // for the error block
                      errorCodes: Object,
                      
                      name: string,
                      field: string
                  }
                  
                  class NestedError extends Component {
                      props: Props;
                      
                      isTouched() {
                          const {children} = this.props;
                          return get(children, 'props.value')
                      }
                      
                      state: State = {
                          touched: this.isTouched()
                      }
                      
                      setTouched() {
                          this.setState({
                              touched: true
                          })
                      }
                      
                      cloneElement() {
                          const {children} = this.props;
                          const onBlur = get(children, 'props.onBlur', noop);
                          return React.cloneElement(
                              children,
                              {
                                  onBlur: compose(this.setTouched, onBlur)
                              }
                          )
                      }
                      
                      render() {
                          const {touched} = this.state;
                          const {
                              children, 
                              field, 
                              name, 
                              getError,
                              errorCodes,
                              style,
                              className
                          } = this.props;
                          const errors = getErrors();
                          const error = get(errors, `${field}`.${name});
                          return (
                              
                  {touched ? children : this.cloneElement()} {error && {errorCodes[error] || error} }
                  ); } } const Error = styled.div` color: red; `; export default ValidationComponent(NestedError)
                  Original source: habrahabr.ru (comments, light).

                  https://habrahabr.ru/post/337582/


                  Метки:  

                  Валидация React компонентов с помощью Livr.js

                  Воскресенье, 10 Сентября 2017 г. 14:10 + в цитатник
                  one_more сегодня в 14:10 Разработка

                  Валидация React компонентов с помощью Livr.js

                    Пару лет назад я увидел на хабре статью про LIVR и с тех пор использую библиотеку на всех проектах. С переходом на React я адаптировал для валидации ее же, т.к. существующие решения не предлагали гибкости которой мне хотелось. Свое решение я уже использую на двух проектах и решил выложить в npm — может кому-то еще оно покажетсяя удобным.
                    Пакет называется react-livr-validation.

                    Пример базового использвания
                    import React from 'react';
                    import Validation, {DisabledOnErrors, ValidationInput} from 'react-livr-validation';
                    
                    const schema = {
                        login: ['required', 'not_empty'],
                        password: ['required', 'not_empty']
                    };
                    
                    const data = {
                        login: '',
                        password: ''
                    };
                    
                    export default function() {
                        return (
                            
                                
                    ); }

                    Компонент принимает валидационную схему и первоначальные данные(если данные не валидны, кнопка submit сразу будет неактивна), так же можно передать custom rules и aliased rules
                    const customRules = {
                        alpha_chars: function() {
                            return function(value) {
                                if (typeof value === 'string') {
                                    if (!/[a-z,A-Z]+/.test(value)) {
                                        return 'WRONG_FORMAT';
                                    }
                                }
                            };
                        }
                    };
                    const aliasedRules = [
                        {
                            name: 'strong_password',
                            rules: { min_length: 6 },
                            error: 'TOO_SHORT'
                        }
                    ];
                    
                          // ... form
                    
                    

                    Обертка ValidationInput добавляет в инпут свои обработчики событий, на которые будет происходить валидация. По умолчанию это события change, blur, keyup.
                    
                            
                    
                    

                    Есть возможность реализовать свою обертку — пакет экпортит HOC, который прокидывает в пропсы api
                    // @flow
                    
                    import React, {Component} from 'react'
                    import {ValidationComponent} from 'react-livr-validation'
                    import get from 'lodash/get'
                    import noop from 'lodash/noop'
                    import compose from 'ramda/src/compose'
                    import styled from 'styled-components'
                    
                    type DataChunk = {
                        name: string,
                        value: any
                    }
                    
                    type State = {
                        touched: boolean
                    }
                    
                    type Props = {
                        // will be passed by HOC
                        setData: (data: DataChunk) => void,
                        getError: (name: string) => ?string,
                        getErrors: () => Object,
                        className: string, // for the error block
                        style: Object // for the error block
                        errorCodes: Object,
                        
                        name: string,
                        field: string
                    }
                    
                    class NestedError extends Component {
                        props: Props;
                        
                        isTouched() {
                            const {children} = this.props;
                            return get(children, 'props.value')
                        }
                        
                        state: State = {
                            touched: this.isTouched()
                        }
                        
                        setTouched() {
                            this.setState({
                                touched: true
                            })
                        }
                        
                        cloneElement() {
                            const {children} = this.props;
                            const onBlur = get(children, 'props.onBlur', noop);
                            return React.cloneElement(
                                children,
                                {
                                    onBlur: compose(this.setTouched, onBlur)
                                }
                            )
                        }
                        
                        render() {
                            const {touched} = this.state;
                            const {
                                children, 
                                field, 
                                name, 
                                getError,
                                errorCodes,
                                style,
                                className
                            } = this.props;
                            const errors = getErrors();
                            const error = get(errors, `${field}`.${name});
                            return (
                                
                    {touched ? children : this.cloneElement()} {error && {errorCodes[error] || error} }
                    ); } } const Error = styled.div` color: red; `; export default ValidationComponent(NestedError)
                    Original source: habrahabr.ru (comments, light).

                    https://habrahabr.ru/post/337582/


                    Метки:  

                    Задача о премировании: почувствуй себя менеджером

                    Воскресенье, 10 Сентября 2017 г. 13:30 + в цитатник
                    1. Менеджмент некой компании уделяет большое внимание мотивации сотрудников.
                    2. Для поощрения высоких результатов было решено выдавать премии командам по результатам соблюдения сроков и бюджетов проектов.
                    3. Решение было доведено до сотрудников.
                    4. Одна из команд занималась доработками зрелого и стабильного проекта, успешно выполнила все условия, получила премию.
                    5. Другой команде достался новый сложный проект, люди работали над ним с неподдельным энтузиазмом.
                    6. К сожалению, и запланированные сроки, и бюджет оказались превышены в разы.
                    7. Получившийся в результате продукт дал компании рекордную прибыль.

                    Если вы менеджер в этой компании, то станете ли премировать вторую команду и почему?

                    https://habrahabr.ru/post/337580/


                    Метки:  

                    Задача о премировании: почувствуй себя менеджером

                    Воскресенье, 10 Сентября 2017 г. 13:30 + в цитатник
                    1. Менеджмент некой компании уделяет большое внимание мотивации сотрудников.
                    2. Для поощрения высоких результатов было решено выдавать премии командам по результатам соблюдения сроков и бюджетов проектов.
                    3. Решение было доведено до сотрудников.
                    4. Одна из команд занималась доработками зрелого и стабильного проекта, успешно выполнила все условия, получила премию.
                    5. Другой команде достался новый сложный проект, люди работали над ним с неподдельным энтузиазмом.
                    6. К сожалению, и запланированные сроки, и бюджет оказались превышены в разы.
                    7. Получившийся в результате продукт дал компании рекордную прибыль.

                    Если вы менеджер в этой компании, то станете ли премировать вторую команду и почему?

                    https://habrahabr.ru/post/337580/


                    Метки:  

                    Как воскресить Ягуара за тысячу часов?

                    Воскресенье, 10 Сентября 2017 г. 13:25 + в цитатник
                    imageБывает меня спрашивают — как я пишу эмуляторы? Попробую ответить на примере одной провалившейся консоли.

                    Эмуляция — почти бесконечное занятие, всегда остаются неточности, и если меня спросят сколько я потратил на эмуляцию 3DO, то я лишь пожму плечами, но одно знаю точно — с эмуляцией 3DO все очень хорошо. Поэтому пришло время найти новую жертву и ей оказался Atari Jaguar. 1000 часов — примерно столько я потратил на разработку ядра эмуляции данной консоли в проекте «Феникс», и вероятно еще столько же понадобится, чтобы поднять совместимость с текущих 95% до 99%, а оставшийся 1% потребует еще, возможно не одну тысячу часов, но это уже отдельные скучные истории про отладку едва уловимых глюков.
                    Читать дальше ->

                    https://habrahabr.ru/post/337566/


                    Метки:  

                    Как воскресить Ягуара за тысячу часов?

                    Воскресенье, 10 Сентября 2017 г. 13:25 + в цитатник
                    imageБывает меня спрашивают — как я пишу эмуляторы? Попробую ответить на примере одной провалившейся консоли.

                    Эмуляция — почти бесконечное занятие, всегда остаются неточности, и если меня спросят сколько я потратил на эмуляцию 3DO, то я лишь пожму плечами, но одно знаю точно — с эмуляцией 3DO все очень хорошо. Поэтому пришло время найти новую жертву и ей оказался Atari Jaguar. 1000 часов — примерно столько я потратил на разработку ядра эмуляции данной консоли в проекте «Феникс», и вероятно еще столько же понадобится, чтобы поднять совместимость с текущих 95% до 99%, а оставшийся 1% потребует еще, возможно не одну тысячу часов, но это уже отдельные скучные истории про отладку едва уловимых глюков.
                    Читать дальше ->

                    https://habrahabr.ru/post/337566/


                    Метки:  

                    [Из песочницы] Как я перестал любить Angular

                    Воскресенье, 10 Сентября 2017 г. 13:17 + в цитатник
                    MooooM сегодня в 13:17 Разработка

                    Как я перестал любить Angular

                    Вступление


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


                    На дворе 2017ый год и для каждого нового продукта/проекта встает вопрос выбора фреймворка для разработки. Долгое время я был уверен, что новый Angular 2/4 (далее просто Angular) станет главным трендом enterprise разработки еще на несколько лет вперед и даже не сомневался что буду работать только с ним.


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


                    Дисклеймер: данная статья строго субъективна, но таков мой личный взгляд на происходящее и касается разработки enterprise-level приложений.


                    AngularJS



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


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


                    Angular



                    И вот наконец Angular был переписан с нуля дабы стать основной для многих будущих веб-приложений.


                    Конечно путь к этому был долог и полон Breaking Changes,
                    но на сегодняшний день Angular 4 стабилен и позиционируется как полностью production-ready.


                    Одна из наиболее крутых вещей, которую дал нам новый Angular — популяризация TypeScript.
                    Лично я был с ним знаком и работал еще до того, как он стал основным для моего любимого фреймворка,
                    но многие узнали о нем именно благодаря Angular.


                    TypeScript



                    Не буду подробно останавливаться на TypeScript, т.к. это тема для отдельной статьи,
                    да и написано о нем уже больше чем нужно. Но для enterprise разработки TypeScript дает огромное количество преимуществ. Начиная с самой статической типизации и областями видимости и заканчивая поддержкой ES7/8 даже для IE9.


                    Главное преимущество работы с TypeScript — богатый инструментарий и прекрасная поддержка IDE. По нашему опыту, юнит тестов с TS приходится писать существенно меньше.


                    Vue



                    Если вы читаете данную статью, то с вероятностью 95% вы уже знаете что это такое.


                    Но для тех 5% кто еще не знает — Vue.js это крайне легковесный (но очень богатый по функционалу) фреймворк, вобравший в себя многое хорошее, как из AngularJS, так и из React.


                    Фактически больше он похож все же на React, но шаблоны практически идентичны AngularJS (HTML + Mustache).


                    В реальности от AngularJS, он отличается очень даже разительно, но в поверхносном смысле,
                    понять как он работает, если у вас уже был опыт работы с React или AngularJS, будет несложно.


                    Предыстория


                    Было — большой проект на AngularJS


                    Последний мой проект, который совсем недавно вышел в production, мы писали на AngularJS 1.5-1.6.


                    Вопреки наличию стабильной версии Angular уже долгое время, мы приняли решение не мигрировать на него по ряду причин (не столько технических, сколько политических).
                    Но одной из фич современного веба мы пользовались с самого начала — это TypeScript.


                    Вот пример нашего компонента из данного проекта:


                    import {Component} from "shared-front/app/decorators";
                    import FileService, {ContentType, IFile} from "../file.service";
                    import AlertService from "shared/alert.service";
                    
                    @Component({
                        template: require("./file.component.html"),
                        bindings: {
                            item: "<",
                        },
                    })
                    export default class FileComponent {
                        public static $inject = ["fileService"];
                        public item: IFile;
                    
                        constructor(private fileService: FileService, private alertService: AlertService) {
                        }
                    
                        public isVideo() {
                            return this.item.contentKeyType === ContentType.VIDEO;
                        }
                    
                        public downloadFile() {
                            this.fileService.download(this.getFileDownloadUrl()).then(() => {
                                this.alertService.success();
                            });
                        }
                    
                        private getFileDownloadUrl() {
                            return `url-for-download${this.item.text}`;
                        }
                    }

                    На мой взгляд выглядит очень даже приятно, не слишком многословно, даже если вы не фанат TS.
                    К тому же все это замечательно тестируется как Unit-тестами, так и Е2Е.


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


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


                    Стало — средний проект на Angular


                    Так мы и поступили, рационально выбрав Angular 2 (позже 4) для нашего нового проекта несколько месяцев назад.


                    Выбор казался довольно очевидным, учитывая, что вся наша команда имеет большой опыт работы с первой версией. К тому же, лично я ранее работал с alpha-RC версиями и тогда проблемы фреймворка списывались на 0.х номер версии.


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


                    А вот пример компонента из проекта на Angular:


                    import {Component} from '@angular/core';
                    
                    import FileService, {ContentType, IFile} from "../file.service";
                    import AlertService from "shared/alert.service";
                    
                    @Component({
                      selector: 'app-file',
                      templateUrl: './file.component.html',
                      styleUrls: ['./file.component.scss']
                    })
                    export class FileComponent {
                    
                        Input() item: IFile;
                    
                        constructor(private fileService: FileService, private alertService: AlertService) {
                        }
                    
                        public isVideo() {
                            return this.item.contentKeyType === ContentType.VIDEO;
                        }
                    
                        public downloadFile() {
                            this.fileService.download(this.getFileDownloadUrl()).subscribe(() => {
                                this.alertService.success();
                            });
                        }
                    
                        private getFileDownloadUrl() {
                            return `url-for-download${this.item.text}`;
                        }
                    }
                    

                    Возможно чуть чуть более многословно, но гораздо чище.


                    Плюсы


                    Angular CLI — единственное реальное преимущество перед AngularJS



                    Первое, что вы установите при разработке нового Angular 4 приложения это Angular CLI


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


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


                    Конечно CLI тоже имеет ряд недостатков в части настроек и конфигурации "под себя", но все же он на голову выше аналогичных утилит для React (create-react-app) или Vue (vue-cli). Хотя второй, благодаря своей гибкости, становится лучше с каждым днем.


                    Минусы или "За что я перестал любить Angular"


                    Изначально я не хотел писать очередную хейтерскую статью вроде
                    Angular 2 is terrible (нашелся даже перевод).


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


                    В целом не совсем разделяю взгяд автора на RxJS, т.к. библиотека невероятно мощная.


                    An Ajax request is singular, and running methods like Observable.prototype.map when there will only ever be one value in the pipe makes no semantic sense. Promises on the other hand represent a value that has yet to be fulfilled, which is exactly what a HTTP request gives you. I spent hours forcing Observables to behave before giving up using Observable.prototype.toPromise to transform the Observable back to a Promise and simply using Promise.all, which works much better than anything Rx.js offers.

                    В реальности, благодаря RxJS, со временем становится очень приятно воспринимать любые данные,
                    пусть даже не совсем подходящие для Observable, как единую шину.


                    Но суровая правда в том, что Object.observe нативно мы все же не увидим:


                    After much discussion with the parties involved, I plan to withdraw the Object.observe proposal from TC39 (where it currently sits at stage 2 in the ES spec process), and hope to remove support from V8 by the end of the year (the feature is used on 0.0169% of Chrome pageviews, according to chromestatus.com).

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


                    Также не соглашусь в целом по поводу TypeScript'а, так как это все же замечательный язык, но об этом ниже.


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


                    Однако все же изложу несколько собственных мыслей (к сожалению, все больше негативных), не упомянутых в статье выше.


                    TypeScript в Angular


                    Пожалуй самое болезненное разочарование для меня — это то, во что превратили работу с TypeScript'ом в Angular.


                    Ниже несколько примеров наиболее неудачных решений.


                    Ужасные API


                    Одной из основных проблем использования TypeScript в Angular я считаю крайне спорные API.
                    Сам по себе TypeScript идеально подходит для написания максимально строгого кода, без возможностей случайно сделать шаг не в ту сторону. Фактически он просто создан для того, чтобы писать публичный API, но команда Angular сделала все, чтобы данное преимущество превратилось в недостаток.


                    Примеры:


                    HttpParams

                    По какой-то причине команда Angular решила сделать класс HttpParams иммутабельным.
                    Иммутабельность это здорово, но если вы думаете, что большинство классов в Angular являются таковыми, то это вовсе не так.


                    Например код вида:


                    let params = new HttpParams();
                    params.set('param', param);
                    params.set('anotherParam', anotherParam);
                    ...
                    this.http.get('test', {params: params});

                    Не будет добавлять параметры к запросу. Казалось бы почему?
                    Ведь никаких ошибок, ни TypeScript ни сам Angular не отображает.


                    Только открыв сам класс в TypeScript можно найти комментарий


                    This class is immuatable — all mutation operations return a new instance.

                    Конечно, это совершенно неочевидно.


                    В вот и вся документация про них:


                    http
                      .post('/api/items/add', body, {
                        params: new HttpParams().set('id', '3'),
                      })
                      .subscribe();

                    RxJS operator import

                    Начнем с того, что документация по Angular вообще не имеет толкового разбора и описания Observable и того, как с ними работать.


                    Нет даже толковых ссылок на документацию по RxJS. И это при том, что Rx является ключевой частью фреймворка, а само создание Observable уже отличается:


                    // rx.js
                    Rx.Observable.create();
                    vs
                    // Angular
                    new Observable()

                    Ну да и черт с ним, здесь я хотел рассказать о Rx + TypeScript + Angular.


                    Допустим вы хотите использовать некий RxJS оператор, вроде do:


                    observable.do(event => {...})

                    В коде никакой ошибки не произойдет, это сработает и замечательно запустится.


                    Вот только, во время выполнения возникнет такая ошибка:


                    ERROR TypeError: observable.do is not a function

                    Потому что вы очевидно (потратили кучу времени на поиск проблемы) забыли заимпортировать сам оператор:


                    import 'rxjs/add/operator/do';

                    Почему это ломается в рантайме, если у нас есть TypeScript? Не знаю. Но это так.


                    Router API

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


                    Events

                    Теперь для работы с параметрами предлагается подписываться на события роутера.
                    Ок, пускай, но приходят всегда все события, независимо от того, какие нам нужны.
                    А проверять предлагается через instanceof (снова новый подход, отличный от большинства других мест):


                    this.router.events.subscribe(event => {
                      if(event instanceof NavigationStart) {
                        ...
                      }
                    }


                    В очередной раз странным решением было сделать всю работу с роутами командами — причем массивом из них. Простые и наиболее распространенные переходы будут выглядеть как-то так:


                    this.router.navigate(['/some']);
                    ...
                    this.router.navigate(['/other']);
                    

                    Почему это плохо?


                    Потому что команды в данном случае имеют сигнатуру any[].
                    Для незнакомых с TypeScript — это фактически отключение его фич.


                    Это при том, что роутинг — наиболее слабо связанная часть в Angular.


                    Например мы в нашем AngularJS приложении наоборот старались типизировать роуты,
                    чтобы вместо простых строк они были хотя бы enum.


                    Это позволяет находить те или иные места, где используется данный роут без глобального поиска по строке 'some'.


                    Но нет, в Angular это преимущество TypeScript не используется никак.


                    Lazy Load

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


                    {
                      path: 'admin',
                      loadChildren: 'app/admin/admin.module#AdminModule',
                    },

                    Forms API

                    Для начала — в Angular есть два типа форм: обычные
                    и реактивные.


                    Само собой, работать с ними нужно по-разному.


                    Однако лично меня раздражает именно API reactive forms:


                    // Зачем нужен первый пустой параметр?
                    // Почему name это массив c валидатором??
                    this.heroForm = this.fb.group({
                      name: ['', Validators.required ],
                    });

                    или из документации


                    // Почему пустое поле это имя??
                    this.heroForm = this.fb.group({
                      name: '', // <--- the FormControl called "name"
                    });

                    и так далее


                    this.complexForm = fb.group({   
                      // Почему понадобился compose ?
                      // Неужели нельзя без null ??
                      'lastName': [null, Validators.compose([Validators.required, Validators.minLength(5), Validators.maxLength(10)])],
                      'gender' : [null, Validators.required],
                    })

                    А еще — нельзя просто использовать атрибуты типа [disabled] с реактивными формами...


                    Это далеко не все примеры подобных откровенно спорных решений, но думаю, что для раздела про API этого хватит


                    __metadata


                    К сожалению использование горячо любимого мною TypeScript'а в Angular слишком сильно завязано на декораторы.


                    Декораторы — прекрасная вещь, но к сожалению в рантайме нет самой нужной их части, а именно __metadata.


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


                    Без метаданных декораторы тоже можно использовать — во время компиляции, но толку в таком случае от них не очень много.


                    Впрочем, мы в нашем AngularJS приложении использовали такие декораторы, например @Component:


                    export const Component = (options: ng.IComponentOptions = {}) => controller => angular.extend(options, {controller});

                    Он фактически просто оборачивает наши TypeScript классы в компоненты AngularJS и делает их контроллерами.


                    Но в Angular, несмотря на экспериментальность фичи, это стало частью ядра фреймворка,
                    что делает необходимым использование полифила reflect-metadata в совершенно любом случае. Очень спорное решение.


                    Абстракции


                    Обилие внутренних классов и абстракций, а также всякие хитрости завязанные на TypeScript,
                    также не идут на пользу его принятию комьюнити. Да еще и портят впечатление о TS в целом.


                    Самый яркий пример подобных проблем — это Dependency Injection в Angular.


                    Сама по себе концепция замечательная, особенно для unit тестирования.
                    Но практика показывает, что большой нужды делать из фронтенда нечто Java-подобное нет.
                    Да, в нашем AngularJS приложении мы очень активно это использовали, но поработав с тестированием Vue компонентов, я серьезно начал сомневаться в пользе DI.


                    В Angular для большинства обычных зависимостей, вроде сервисов, DI будет выглядеть очень просто, с получением через конструктор:


                    constructor(heroService: HeroService) {
                      this.heroes = heroService.getHeroes();
                    }

                    Но так работает только для TypeScript классов, и если вы хотите добавить константу, необходимо будет использовать @Inject:


                    constructor(@Inject(APP_CONFIG) config: AppConfig) {
                      this.title = config.title;
                    }

                    Ах да, сервисы которые вы будете инжектить должны быть проанотированы как @Injectable().


                    Но не все, а только те, у которых есть свои зависимости, если их нет — можно этот декоратор не указывать.


                    Consider adding @Injectable() to every service class, even those that don't have dependencies and, therefore, do not technically require it.
                    Here's why:

                    Future proofing: No need to remember @Injectable() when you add a dependency later.

                    Consistency: All services follow the same rules, and you don't have to wonder why a decorator is missing.

                    Почему бы не сделать это обязательным сразу, если потом это рекомендуется делать всегда?


                    Еще прекрасная цитата из официальной документации по поводу скобочек:


                    Always write @Injectable(), not just @Injectable. The application will fail mysteriously if you forget the parentheses.

                    Короче говоря, создается впечатление, что TypeScript в Angular явно используется не по назначению.


                    Хотя еще раз подчеркну, что сам по себе язык обычно очень помогает в разработке.


                    Синтаксис шаблонов


                    Синтаксис шаблонов — основная претензия к Angular. И по вполне объективным причинам.


                    Пример не только множества разных директив, но еще и разных вариантов их использования:


                    style using ngStyle
                    Hello Wordl!
                    CSS class using property syntax, this text is blue
                    object of classes
                    array of classes
                    string of classes

                    Изначально разработчики позиционировали новый синтакисис шаблонов, как спасение от множества директив.


                    Обещали, что будет достаточно только [] и ().


                    Binding Example
                    Properties
                    Events
                    Two-way

                    К сожалению в реальности директив едва ли не больше чем в AngularJS.



                    И да, простое правило запоминания синтаксиса two-way binding про банан в коробке
                    из официальной документации:


                    Visualize a banana in a box to remember that the parentheses go inside the brackets.

                    Документация


                    Вообще писать про документацию Angular даже нет смысла, она настолько неудачная,
                    что достаточно просто попытаться ее почитать, чтобы все стало понятно.


                    Контрпример — доки Vue.
                    Мало того, что написаны подробно и доходчиво, так еще и на 6 языках,
                    в т.ч. русском.


                    View encapsulation


                    Angular позволяет использовать так называемый View encapsulation.


                    Суть сводится к эмуляции Shadow DOM или использовании нативной его поддержки.


                    Сам по себе Shadow DOM — прекрасная вещь и действительно потенциально позволяет использовать даже разные CSS фреймворки для разных копмонентов без проблем.
                    Однако нативная поддержка на сегодняшний день совершенно печальна.


                    По умолчанию включена эмуляция Shadow DOM.


                    Вот пример простого CSS для компонента:


                    .first {
                      background-color: red;
                    }
                    .first .second {
                      background-color: green;
                    }
                    .first .second .third {
                      background-color: blue;
                    }

                    Angular преобразует это в:


                    .first[_ngcontent-c1] {
                      background-color: red;
                    }
                    .first[_ngcontent-c1]   .second[_ngcontent-c1] {
                      background-color: green;
                    }
                    .first[_ngcontent-c1]   .second[_ngcontent-c1]   .third[_ngcontent-c1] {
                      background-color: blue;
                    }

                    Совершенно не ясно зачем делать именно так.


                    Например Vue делает то же самое, но гораздо чище:


                    .first[data-v-50646cd8] {
                      background-color: red;
                    }
                    .first .second[data-v-50646cd8] {
                      background-color: green;
                    }
                    .first .second .third[data-v-50646cd8] {
                      background-color: blue;
                    }

                    Не говоря уже о том, что в Vue это не дефолтное поведение и включается добавлением простого scoped к стилю.


                    Так же хотелось бы отметить, что Vue (vue-cli webpack) подобным же образом позволяет указывать SASS/SCSS, тогда как для Angular CLI нужны команды типа ng set defaults.styleExt scss.
                    Не очень понятно зачем все это, если внутри такой же webpack.


                    Но это все ерунда, реальные проблемы у нас начались, когда нам потребовалось использовать сторонние компоненты.


                    В частности мы использовали один из наиболее популярных UI фреймворков — PrimeNG,
                    а он иногда использует подобные селекторы:


                    body .ui-tree .ui-treenode .ui-treenode-content .ui-tree-toggler {
                        font-size: 1.1em;
                    }

                    Они по определению имеют больший приоритет нежели стили компонента, который использует в себе тот или иной сторонний элемент.


                    Что в итоге приводит к необходимости писать нечто подобное:


                    body :host >>> .ui-tree .ui-treenode .ui-treenode-content .ui-tree-toggler {
                      font-size: 2em;
                    }

                    Иногда и вовсе приходилось вспомнить великий и ужасный !important.


                    Безусловно все это связано конкретно с PrimeNG и не является как таковой проблемой фреймворка, но это именно та проблема, которая скорее всего возникнет и у вас при реальной работе с Angular.


                    К слову о стабильности


                    В примере выше мы использовали >>> — как и /deep/ это алиас для так называемого shadow-piercing селектора.


                    Он позволяет как бы "игнорировать" Shadow DOM и для некоторых сторонних компонентов порой просто незаменим.


                    В одном из относительно свежих релизов Angular создатели фреймворка решили,
                    в соответствии со стандартом, задепрекейтить /deep/ и >>>.


                    Никаких ошибок или ворнингов их использование не принесло, они просто перестали работать.
                    Как выяснилось позже, теперь работает только ::ng-deep — аналог shadow-piercing селектора в Angular вселенной.


                    Обновление это было отнюдь не мажорной версии (4.2.6 -> 4.3.0), просто в один прекрасный момент наша верстка во многих местах поползла (спасибо и NPM за версии с шапочкой ^).


                    Конечно, не все наши разработчики ежедневно читают ChangeLog Angular 4, да и за трендами веб разработки не всегда можно уследить. Само собой сначала грешили на собственные стили — пришлось потратить немало времени и нервов для обнаружения столь неприятной особенности.


                    К тому же скоро и ::ng-deep перестанет работать.
                    Как в таком случае править стили кривых сторонних компонентов, вроде тех же PrimeNG, ума не приложу.


                    Наш личный вывод: дефолтная настройка — эмуляция Shadow DOM порождает больше проблем чем решает.


                    Свой HTML парсер


                    Это вполне потянет на отдельную статью, но вкратце суть в том, что команда Angular действительно написала свой HTML парсер. По большей части это было необходимо для обеспечения регистрозависимости.


                    Нет смысла холиварить на тему ухода Angular от стандартов, но по мнению многих это довольно странная идея, ведь в том же AngularJS обычного HTML (регистронезависимого) вполне хватало.


                    С AngularJS нередко бывало такое: добавили вы некий а тест не написали.
                    Прошло некоторое время и модуль который содержит логику данного компонента был удален/отрефакторен/итд.


                    Так или иначе — теперь ваш компонент не отображается.


                    Теперь он сразу определяет неизвестные теги и выдает ошибку если их не знает.


                    Правда теперь любой сторонний компонент требует либо полного отключения этой проверки,
                    либо включения CUSTOM_ELEMENTS_SCHEMA которая пропускает любые тэги с -


                    Сорцы можете оценить самостоятельно


                    ...
                    const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
                      'base': new HtmlTagDefinition({isVoid: true}),
                      'meta': new HtmlTagDefinition({isVoid: true}),
                      'area': new HtmlTagDefinition({isVoid: true}),
                      'embed': new HtmlTagDefinition({isVoid: true}),
                      'link': new HtmlTagDefinition({isVoid: true}),
                      'img': new HtmlTagDefinition({isVoid: true}),
                      'input': new HtmlTagDefinition({isVoid: true}),
                      'param': new HtmlTagDefinition({isVoid: true}),
                      'hr': new HtmlTagDefinition({isVoid: true}),
                      'br': new HtmlTagDefinition({isVoid: true}),
                      'source': new HtmlTagDefinition({isVoid: true}),
                      'track': new HtmlTagDefinition({isVoid: true}),
                      'wbr': new HtmlTagDefinition({isVoid: true}),
                      'p': new HtmlTagDefinition({
                        closedByChildren: [
                          'address', 'article', 'aside', 'blockquote', 'div', 'dl',      'fieldset', 'footer', 'form',
                          'h1',      'h2',      'h3',    'h4',         'h5',  'h6',      'header',   'hgroup', 'hr',
                          'main',    'nav',     'ol',    'p',          'pre', 'section', 'table',    'ul'
                        ],
                        closedByParent: true
                      }),
                    ...
                      'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
                      'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
                      'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}),
                      'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
                      'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
                      'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
                      'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
                      'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
                      'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc'
                      ...

                    А теперь ключевой момент — все эти ошибки происходят в консоли браузера в рантайме,
                    нет, ваш webpack билд не упадет, но и увидеть что либо кроме белого экрана не получится.
                    Потому что по умлочанию используется JIT компилятор.


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


                    Наличие двух различно работающих компиляторов само по себе довольно опасно звучит,
                    и на деле постоянно приводит к проблемам.


                    У нас с AOT было множество разнообразных ошибок, в том числе весьма неприятные и до сих пор открытые, например нельзя использовать экспорты по умолчанию


                    Обратите внимание на элегантные предлагаемые решения проблемы:


                    don't use default exports :)

                    Just place both export types and it works

                    Или нечто подобное описанному здесь (AOT не всегда разбирает замыкания)


                    Код подобного вида вызывает очень странные ошибки компилятора AOT:


                    @NgModule({
                      providers: [
                        {provide: SomeSymbol, useFactor: (i) => i.get('someSymbol'), deps: ['$injector']}
                      ]
                    })
                    export class MyModule {}

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


                    export factoryForSomeSymbol = (i) => i.get('someSymbol');
                    
                    @NgModule({
                      providers: [
                        {provide: SomeSymbol, useFactor: factoryForSomeSymbol, deps: ['$injector']}
                      ]
                    })
                    export class MyModule {}

                    Также хотелось бы отметить, что текст ошибок в шаблонах зачастую совершенно неинформативен.


                    Zone.js


                    Одна из концептуально очень крутых вещей которые появились в Angular это Zone.js.
                    Он позволяет отслеживать контекст выполнения для асинхронных задач, но новичков отпугивают огромной длины стактрейсы. Например:


                    core.es5.js:1020 ERROR Error: Uncaught (in promise): Error: No clusteredNodeId supplied to updateClusteredNode.
                    Error: No clusteredNodeId supplied to updateClusteredNode.
                        at ClusterEngine.updateClusteredNode (vis.js:47364)
                        at VisGraphDataService.webpackJsonp.../../../../../src/app/services/vis-graph-data.service.ts.VisGraphDataService.updateNetwork (vis-graph-data.service.ts:84)
                        at vis-graph-display.service.ts:63
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
                        at Object.onInvoke (core.es5.js:3890)
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
                        at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
                        at zone.js:818
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
                        at Object.onInvokeTask (core.es5.js:3881)
                        at ClusterEngine.updateClusteredNode (vis.js:47364)
                        at VisGraphDataService.webpackJsonp.../../../../../src/app/services/vis-graph-data.service.ts.VisGraphDataService.updateNetwork (vis-graph-data.service.ts:84)
                        at vis-graph-display.service.ts:63
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
                        at Object.onInvoke (core.es5.js:3890)
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
                        at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
                        at zone.js:818
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
                        at Object.onInvokeTask (core.es5.js:3881)
                        at resolvePromise (zone.js:770)
                        at zone.js:696
                        at zone.js:712
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
                        at Object.onInvoke (core.es5.js:3890)
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
                        at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
                        at zone.js:818
                        at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
                        at Object.onInvokeTask (core.es5.js:3881)

                    Некоторым Zone.js нравится и
                    наверняка в продакшене нам это еще не раз поможет, но в нашем проекте большой пользы мы пока не извлекли.


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


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


                    И сама суть проблемы остается где-то далеко наверху, если ошибка происходит слишком много раз.



                    UI frameworks


                    Еще один пункт, который формально не относится к самому фреймворку — это довольно скудное количество готовых наборов UI компонентов. Думаю, что написание большинства компонентов с нуля — непозволительная роскошь для многих веб-разработчиков. Зачастую проще, быстрее и разумнее взять готовый UI framework вместо написания собственных гридов и деревьев.


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


                    Вот список основных UI фреймворков для Angular: https://angular.io/resources (раздел UI components).


                    Рассмотрим наиболее популярные бесплатные варианты.


                    Angular Material 2



                    Безусловно, наибольшие надежды я возлагал на Angular Material 2 ввиду того, что разрабатывается он командой Angular и наверняка будет соответствовать всем гайдлайнам.


                    К сожалению, несмотря на его возраст, набор компонентов крайне мал.


                    На момент начала написания данной статьи, и уж тем более, когда мы выбирали фреймворк для проекта — гридов не было.


                    И вот совсем недавно он-таки появился. Но функционал все же пока довольно базовый.


                    Я считаю, что Angular Material 2 подойдет лишь небольшим или, в лучшем случае, средним проектам, т.к. до сих пор нет, например, деревьев. Часто очень нужны компоненты вроде multiple-select, коих тоже нет.


                    Отдельно стоит сказать про очень скупую документацию и малое количество примеров.


                    Перспективы развития тоже довольно печальные.


                    Feature Status
                    tree In-progress
                    stepper In-progress, planned Q3 2017
                    sticky-header In-progress, planned Q3 2017
                    virtual-repeat Not started, planned Q4 2017
                    fab speed-dial Not started, not planned
                    fab toolbar Not started, not planned
                    bottom-sheet Not started, not planned
                    bottom-nav Not started, not planned

                    Bootstrap



                    По тем же причинам, что и выше не буду останавливаться на Bootstrap фреймворках типа
                    ng2-bootstrap (получше) и ngx-bootstrap.
                    Они очень даже неплохи, но простейшие вещи можно сделать и обычным CSS, а сложных компонентов тут нет (хотя наверняка многим будет достаточно modal, datepicker и typeahead).


                    Prime Faces



                    Это на сегодняшний день наиболее популярый фреймворк содержащий множество сложных компонентов. В том числе гриды и деревья (и даже Tree Table!).


                    Изначально я вообще довольно скептически относился к PrimeFaces т.к. у меня был давний опыт работы с JSF, и много неприятных воспоминаний. Да и выглядят PrimeNG визуально так же (не очень современно). Но на момент начала нашего проекта достойных альтернатив не было, и все же хочется сказать спасибо разработчикам за то, что в короткие сроки был написан дейтсивтельно широчайший инструментарий.


                    Однако проблем с этим набором компонентов у нас возникало очень много.


                    Часто документация совершенно не помогает.


                    Забыли добавить некий модуль — что-то просто молча не работает и никаких ошибок не возникает.
                    Приходится дебажить и выясняется, что сорцы совершенно без комментариев.


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


                    Clarity



                    Луч света в темном царстве — это относительно молодая (меньше года от роду) библиотека Clarity от vmware.


                    Набор компонентов впечатляет,
                    документация крайне приятная, да и выглядят здорово.


                    Фреймворк не просто предоставляет набор UI компонентов, но и CSS гайдлайны.
                    Эдакий свой bootstrap. Благодаря этому достигается консистентный и крайне приятный/минималистичный вид компонентов.


                    Гриды очень функциональные и стабильные, а сорцы говорят сами за себя
                    (о боже, комментарии и юнит тесты, а так можно было?).


                    Однако пока что очень слабые формы,
                    нет datepicker'а и select2-подобного компонента.
                    Работа над ними идет в данный момент:
                    DatePicker,
                    Select 2.0
                    (как всегда дизайн на высоте, и хотя с разработкой не торопятся я могу быть уверен, что делают на совесть).


                    Пожалуй, "Clarity Design System" — единственная причина почему я еще верю в жизнь Angular
                    (и вообще единственный фреймворк который не стыдно использовать для enterprise разработки).
                    Как никак VMware серьезнейший мейнтейнер и есть надежда на светлое будущее.


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


                    Но он лишь один


                    Да, я считаю что для Angular на сегодняшний день есть лишь один достойный UI фреймворк.
                    О чем это говорит?


                    Полноценно разрабатывать такие фреймворки для Angular могут лишь серьезнейшие компании вроде той же VMware. Нужен ли вам такой суровый enterprise? Каждый решает сам.


                    А теперь давайте посмотрим, что происходит с одним из свежих конкурентов.


                    Vue UI frameworks



                    Для сравнения мощные уже существующие фреймворки для Vue.js с теми же гридами:


                    Element (~15k stars), Vue Material
                    (существенно младше Angular Material 2 но уже содержит в разы больше),
                    Vuetify (снова Material и снова множество компонентов),
                    Quasar,
                    также надо отметить популярные чисто китайские фреймворки типа
                    iView и Muse-UI
                    (iView выглядит очень приятно, но документация хромает).


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


                    Какой вывод мы сделали?


                    Благодаря Clarity есть надежда на то, что наш Angular проект в дальнейшем будет становится только лучше.


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


                    В реальности все это лишь увеличивает время разработки, не снижая стоимость поддержки и рефакторинга.


                    Поэтому для нашего нового проекта мы выбрали Vue.js.


                    Достаточно просто развернуть базовый webpack шаблон для vue-cli и оценить скорость работы библиотеки.


                    Несмотря на то, что лично я всегда был сторонником фреймворков all-in-one,
                    Vue без особых проблем делает почти все то же, что и Angular.


                    Ну и конечно, множество тех же UI framework'ов также играет свою роль.


                    Единственное чего немного не хватает — чуть более полноценной поддержки TypeScript,
                    уж очень много головной боли он нам сэкономил за эти годы.


                    Но ребята из Microsoft уже вот вот вмержает ее.
                    А потом она появится и в webpack шаблоне.


                    Почему мы не выбрали React? После AngularJS наша команда гораздо проще вошла в Vue,
                    ведь все эти v-if, v-model и v-for уже были очень знакомы.


                    Лично мне во многом, но не во всем, нравится Aurelia но уж больно она малопопулярна,
                    а в сравнении с взрывным ростом популярности Vue она кажется совсем неизвестной.


                    Надеюсь, что через год-два Angular под давлением community все-таки избавится от всего лишнего,
                    исправит основные проблемы и станет наконец тем enterprise framework'ом, которым должен был.
                    Но сегодня я рекомендую вам посмотреть в сторону других, более легковесных и элегантных решений.
                    И поверьте, спустя 4 года работы с Angular, вот так бросить его было очень нелегко.
                    Но достаточно один раз попробовать Vue...

                    Original source: habrahabr.ru (comments, light).

                    https://habrahabr.ru/post/337578/


                    Метки:  

                    Поиск сообщений в rss_rss_hh_full
                    Страницы: 1824 ... 1524 1523 [1522] 1521 1520 ..
                    .. 1 Календарь