12 января 2018 г.

Переезд на новый сайт devmark.ru

Дорогие друзья!

Рад сообщить, что я открываю новый сайт для программистов devmark.ru (удобное сокращение от developer-remarks). Все новые статьи я буду публиковать только там. Некоторые особо популярные статьи с этого блога также переедут туда в течение ближайшего времени. Дополнительные материалы в виде исходников будут и далее публиковаться на github.

Помимо статей новый сайт также включает в себя различные онлайн-инструменты в помощь программистам (вычисление md5, преобразование в различные текстовые форматы, генератор uuid и т.п.)

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

Любые вопросы, предложения и пожелания можете писать в комментариях ниже.

11 ноября 2017 г.

Простой способ создания коллекций в Java 9

Актуальная версия статьи на моём сайте devmark.ru

Java 9 предоставляет новый способ создания read-only коллекций при помощи стандартных универсальных методов List.of(), Set.of() и Map.of().

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

        List<String> days = List.of(
                "понедельник",
                "вторник",
                "среда",
                "четверг",
                "пятница",
                "суббота",
                "воскресенье");

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

Теперь рассмотрим работы с мапой. Метод Map.of() принимает чётное количество элементов, где каждый нечётный - это ключ коллекции, а каждый чётный - значение. Для предыдущего примера вариант с мапой будет выглядеть так:

        Map<Integer, String> days = Map.of(
                1, "понедельник",
                2, "вторник",
                3, "среда",
                4, "четверг",
                5, "пятница",
                6, "суббота",
                7, "воскресенье");

Данная коллекция опять-таки является неизменяемой. Но есть и ещё один нюанс. Здесь вы можете объявить не более 10 элементов мапы. Если вы захотите объявить 11 и более элементов, компилятор выдаст ошибку.

Предположим, вы хотите создать коллекцию из названий месяцев вместе с их порядковыми номерами. Тут нам на помощь придёт метод Map.ofEntries(). И каждый элемент придётся обернуть в другой статический метод Map.entry().

        Map<Integer, String> months = Map.ofEntries(
                Map.entry(1, "январь"),
                Map.entry(2, "февраль"),
                Map.entry(3, "март"),
                Map.entry(4, "апрель"),
                Map.entry(5, "май"),
                Map.entry(6, "июнь"),
                Map.entry(7, "июль"),
                Map.entry(8, "август"),
                Map.entry(9, "сентябрь"),
                Map.entry(10, "октябрь"),
                Map.entry(11, "ноябрь"),
                Map.entry(12, "декабрь")
        );

В случае с методоми Set.of() и List.of() такой проблемы нет, так как там нет ключей.

Кстати, метод Map.entry() также является неплохой альтернативой для создания любой пары значений, ведь в Java нет стандартного класса Pair.

5 ноября 2017 г.

Передача null в перегруженный метод

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

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

interface MyInterface {
    void doWork();
}

class MyBaseObject implements MyInterface {
    @Override
    public void doWork() {
        System.out.println("Base");
    }
}

class MyChildObject extends MyBaseObject {
    @Override
    public void doWork() {
        System.out.println("Child");
    }
}

А также имеется набор из трёх перегруженных методов. Один принимает этот интерфейс, другой - реализацию, а третий - наследника.

void over(MyInterface face) {
    System.out.println("Interface");
}

void over(MyBaseObject base) {
    System.out.println("Base");
}

void over(MyChildObject child) {
    System.out.println("Child");
}

Вопрос: какой из этих методов будет использован, если мы захотим вызвать метод over() и передадим в него null?

Ответ: в таком случае на экране мы увидим слово "Child".

Поэтому можно выделить для себя такое мнемоническое правило, что при передаче null в перегруженный метод всегда будет использоваться наиболее частная реализация (наследник).

P.S. Пример несколько синтетический. На практике я бы избегал ситуации, когда в метод нужно передавать null. Вполне вероятно, что если внутри него нет специальной проверки, то будет NullPointerException.

Также я бы избегал перегрузки методов. Это может и выглядит красиво, но в Enterprise-приложениях, когда у вас over 9000 классов, это вносит дополнительную сложность в понимании.

Отсюда мораль для работодателей: не задавайте на собеседованиях глупых вопросов! А если кто-то любит писать такой код, то лучше его сразу уволить)

Генерация строки на основе шаблона

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

Преположим, вам нужно генерить текст по определённому шаблону. У вас есть заранее заготовленный текст, куда вы передаёте параметры для подстановки. Например, текст sms для клиента.

Самое первое, что приходит на ум - это воспользоваться методом String.format(). Как известно, он чувствителен к порядку следования элементов. Но что, если сам шаблон лежит у вас где-нибудь в базе данных и может измениться в любое время, а порядок параметров "зашит" в коде самого приложения. Согласитесь, было бы удобнее, чтобы каждый параметр подставлялся по имени, а не по порядку.

И тут первое, что приходит на ум - это метод String.replace(). Но чтобы не писать очередной велосипед, лучше воспользоваться классом org.apache.commons.lang3.text.StrSubstitutor из стандартной библиотеки Apache Common.

Подключим её через maven pom.xml:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.1</version>
</dependency>

А теперь напишем код, который будет подставлять в шаблон необходимые параметры:

import org.apache.commons.lang3.text.StrSubstitutor;

import java.util.HashMap;
import java.util.Map;

public class StringTemplateExample {

    private static final String SMS_TEMPLATE =
            "Уважаемый ${user.name}! Ваш заказ будет доставлен по адресу ${user.address}.";

    public static void main(String[] args) {
        Map<String, String> valuesMap = new HashMap<>();
        valuesMap.put("user.name", "Николай Петрович");
        valuesMap.put("user.ddress", "Москва, Ленинский проспект, дом 1");
        StrSubstitutor sub = new StrSubstitutor(valuesMap);
        String resolvedString = sub.replace(SMS_TEMPLATE);
        System.out.println(resolvedString);
    }
}


Здесь в шаблоне мы указываем места для подстановки параметров в формате ${имя_параметра}. Имя параметра может быть произвольным. Далее в целевом методе создаём мапу с информацией об имени пользователя и его адресе. И затем передаём эту мапу в класс StrSubstitutor, где через метод replace() получаем уже конечную строку, которую отправим пользователю.

Уважаемый Николай Петрович! Ваш заказ будет доставлен по адресу Москва, Ленинский проспект, дом 1.

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

Генерация N одинаковых элементов через Stream API

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

Предположим, вы хотите сгенерировать N одинаковых элементов. Это очень легко сделать в Java при помощи Stream API. Рассмотрим следующий метод:

private List<String> repeatedValueGenerator(String text, int count) {
    return Stream.generate(() -> text)
            .limit(count)
            .collect(Collectors.toList());
}

Сначала стрим при помощи метода generate() создаёт бесконечный стрим (Stream), а потом мы его ограничиваем при помощи метода limit().

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

System.out.println(repeatedValueGenerator("test", 3));

В результате получим:

[test, test, test]

Генерация случайных чисел при помощи Stream API

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

В Java 8 у класса java.util.Random появился набор удобных методов для генерации стримов (Stream) случайных чисел для всех основных числовых типов. Например:

private List<Integer> randomIntsGenerator() {
    return new Random()
            .ints(25, 1, 6)
            .boxed()
            .collect(Collectors.toList());
}

В данном примеры мы генерим 25 случайных чисел из диапазона от 1 до 5 (указываем 6, т.к. верхняя граница не входит). Затем для удобства производим упаковку примитивов в список (каждый элемент из int преобразуется в Integer). В результате получаем случайные целые числа:

[4, 4, 1, 4, 3, 1, 5, 1, 5, 4, 3, 4, 3, 2, 2, 1, 1, 5, 5, 1, 5, 4, 1, 1, 2]

Класс Random позволяет генерить не только целые числа типа int, но и числа типа long при помощи метода longs() и double через метод doubles().

P.S. Если вам крайне важно равномерное распределение случайных чисел, рекомендуется использовать класс java.security.SecureRandom.

3 февраля 2017 г.

Работа с базой данных в Spring Boot на примере postgresql

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

Меня уже неоднократно просили написать продолжение статьи Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.

Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id. Сам профиль содержит кроме id также имя и фамилию. Поэтому создадим таблицу profiles в postgresql.

CREATE TABLE profiles
(
  id serial NOT NULL,
  first_name character varying(50) NOT NULL,
  last_name character varying(50) NOT NULL,
  CONSTRAINT profile_id_pk PRIMARY KEY (id)
);

insert into profiles (first_name, last_name) values ('Иван', 'Петров');

Для поля id можно использовать тип serial. Он представляет собой целое число, которое инкрементируется автоматически при вставке новой записи в таблицу.

При работе с БД нужно использовать пул подключений к БД, чтобы не создавать их заново при каждом новом sql-запросе, иначе выполнение запроса будет занимать продолжительное время. В качестве пула предлагаю использовать широко используемый в настоящий момент HikariCP. Также нам потребуется поддержка работы с БД со стороны Spring Boot и драйвер для работы с СУБД postgresql. Добавим все эти зависимости в наш проект.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>2.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.4.1209.jre7</version>
        </dependency>

При инициализации пула требуется указать параметры подключения к БД, такие как логин, пароль и т.п. Поскольку данные параметры являются изменяемыми и доступ к ним должен быть ограничен, вынесем их в отдельный текстовый файл и назовём его application.config. Пример содержимого такого файла:

mainPool.jdbcDriver=org.postgresql.Driver
mainPool.jdbcString=jdbc:postgresql://localhost:5432/database_name
mainPool.jdbcUser=username
mainPool.jdbcPassword=verySecretPassword

Чтобы Spring Boot увидел данные настройки, абсолютный путь к файлу следует указывать через параметр командной строки --spring.config.location=/путь/до/файла/application.config. Если запускаете проект при помощи Idea, указывайте данный параметр в строке Program Arguments.

Для удобства работы с этими настройками создадим новый класс ConnectionSettings, в который Spring автоматически подставит все настройки с префиксом "mainPool" в соответствующие поля, благодаря аннотации @ConfigurationProperties. Вообще это очень хорошая практика - группировать связанные настройки через префикс.

@Component
@ConfigurationProperties(prefix = "mainPool")
public class ConnectionSettings {

    private static int DEFAULT_MAX_POOL_SIZE = 5;

    private String jdbcDriver;
    private String jdbcString;
    private String jdbcUser;
    private String jdbcPassword;
    private int jdbcMaxPoolSize = DEFAULT_MAX_POOL_SIZE;
}

Для каждого из этих полей нужно создать геттер и сеттер, но я для краткости не стал их здесь приводить.

Наш пул подключений максимум может хранить до 5 объектов, однако это значение может быть переопределено через файл настроек.

Теперь создадим ещё один компонент, в котором будем инициализировать сам пул.

@Configuration
public class DatabaseConfig {

    @Autowired
    private ConnectionSettings connectionSettings;

    @Bean
    public DataSource dataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(connectionSettings.getJdbcDriver());
        hikariConfig.setJdbcUrl(connectionSettings.getJdbcString());
        hikariConfig.setUsername(connectionSettings.getJdbcUser());
        hikariConfig.setPassword(connectionSettings.getJdbcPassword());
        hikariConfig.setMaximumPoolSize(connectionSettings.getJdbcMaxPoolSize());
        hikariConfig.setPoolName("main");
        return new HikariDataSource(hikariConfig);
    }
}

Аннотация @Bean позволяет нам вручную создавать бины, которые Spring потом сможет подставлять в другие компоненты.

Для работы с БД принято выделять отдельной слой DAO (data access object - объект доступа к данным). Интерфейс нашего слоя работы с профилями пользователей будет иметь следующий интерфейс:

public interface ProfileDao {

    Profile getProfileById(int id);
}

Его реализация будет выглядеть так:

@Repository
public class ProfileDaoImpl implements ProfileDao {

    private static final String SQL_GET_PROFILE_BY_ID =
            "select id, first_name, last_name from profiles where id = :id";

    private final ProfileMapper profileMapper;
    private final NamedParameterJdbcTemplate jdbcTemplate;

    @Autowired
    public ProfileDaoImpl(
            ProfileMapper profileMapper, 
            NamedParameterJdbcTemplate jdbcTemplate
    ) {
        this.profileMapper = profileMapper;
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public Profile getProfileById(int id) {
        MapSqlParameterSource params = new MapSqlParameterSource();
        params.addValue("id", id);
        List<Profile> profiles = jdbcTemplate.query(
                SQL_GET_PROFILE_BY_ID, 
                params, 
                profileMapper
        );
        if (profiles.isEmpty()) {
            throw new ProfileNotFoundException(id);
        }
        return profiles.get(0);
    }
}

Обратите внимание, что ВСЕ dao-компоненты снабжаются аннотацией @Repository, которая является аналогом @Component. Также она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.

Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Обратите внимание, что для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет сделать нам запрос более безопасным с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон этого запроса.

NamedParameterJdbcTemplate - стандартный компонент, предоставляющий  методы для взаимодействия с БД. ProfileMapper преобразует данные, полученные из БД в объект Profile. То есть он хранит в себе логику маппинга полей таблицы на поля класса. Более подробно мы рассмотрим его чуть ниже.

Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод query, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем типизированный список объектов Profile. Исходя из того, что id является первичным ключом в таблице и его значение уникально, наш список будет содержать единственный элемент с искомым профилем. Если же по данному id ничего не найдено - кидаем исключение, которое потом будет перехвачено в ErrorController (см. Spring Boot Restful Service).

Сам ProfileMapper не хранит внутреннего состояния и всего лишь реализует интерфейс RowMapper, типизированный нашим объектом Profile.

@Component
public class ProfileMapper implements RowMapper<Profile> {

    @Override
    public Profile mapRow(ResultSet rs, int rowNum) throws SQLException {
        Profile profile = new Profile();
        profile.setId(rs.getInt("id"));
        profile.setFirstName(rs.getString("first_name"));
        profile.setLastName(rs.getString("last_name"));
        return profile;
    }
}

На вход он получает ResultSet, представляющий собой одну строку из выборки. Из этого ResultSet мы извлекаем значения полей благодаря методам getInt() и getString(), передавая им имя колонки в таблице.

Теперь осталось только внедрить наш ProfileDao в сервисный слой.

@Component
public class ProfileService {

    private final ProfileDao profileDao;

    @Autowired
    public ProfileService(ProfileDao profileDao) {
        this.profileDao = profileDao;
    }

    public Profile getProfile(int personId) {
        return profileDao.getProfileById(personId);
    }
}

ProfileService, в свою очередь, вызывается из контроллера. То есть вырисовывается типичная трёхслойная архитектура, которой следует придерживаться при создании подобных приложений: контроллер (с аннотацией @Controller) -> сервис (@Service) -> dao (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а dao работает непосредственно с БД.

Теперь если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profile/1, то получите профиль с id = 1:

{
  "id": 1,
  "firstName": "Иван",
  "lastName": "Петров"
}

Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:

{
  "message": "Profile with id = 111 not found"
}

Исходники проекта вы можете посмотреть здесь: https://github.com/nordmine/spring-boot-restful-service.

Если вам помог данный материал, порекомендуйте его другим пользователям и поставьте +1. Если у вас появились вопросы - пишите их в комментариях, постараюсь ответить.

1 февраля 2017 г.

Простой пример dependency injection при помощи Spring

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

Рассмотрим базовые возможности внедрения зависимостей (dependency injection), которые открывает нам Spring.

Создадим обычный maven-проект, где в pom.xml добавим сам Spring и стандартную секцию <build>.

<dependencies>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.5.RELEASE</version>
</dependency>
</dependencies>

<build>
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <source>1.8</source>
            <target>1.8</target>
        </configuration>
    </plugin>
</plugins>
</build>

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

public class TestApp {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app-config.xml");
        MainService service = context.getBean(MainService.class);
        service.doWork();
    }
}

Здесь мы создаём контекст на основе конфигурационного файла Spring, который после сборки maven'ом окажется в том же jar-файле. Затем из контекста получаем главный бин MainService и вызываем у него целевой метод.

Поскольку все бины мы будем конфигурировать прямо в коде при помощи аннотаций (секция <annotation-config>), содержимое app-config.xml будет минимальным. Мы лишь указываем базовый пакет (секция <component-scan>), внутри которого Spring автоматически будет искать все бины.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config />
    <context:component-scan base-package="ru.devmark.test"/>
</beans>

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

public interface MainService {

    void doWork();
}

Его реализация MainServiceImpl, помеченная аннотацией @Service.

@Service
public class MainServiceImpl implements MainService {

    @Autowired
    private Handler handler;

    @Override
    public void doWork() {
        System.out.println(handler.getString());
    }
}

Все spring-бины помечаются одной из четырёх аннотаций:
  1. Controller - бины, содержащие маппинг входящих http-запросов
  2. Service - бины, реализующие бизнес-логику приложения
  3. Repository - бины, работающие с БД
  4. Component - все остальные бины
Component - наиболее обобщённая аннотация и три другие на самом деле от неё наследуются. По большому счёту, разницы между ними нет и почти любой бин будет работать с любой аннотацией, но лучше придерживаться такого деления.

Также обратите внимание на аннотацию @Autowired. Она указывает, что данное  поле класса должно быть автоматически инициализировано бином, имеющим такой же интерфейс, как и это поле класса. В нашем примере это некий интерфейс Handler (от англ. "обработчик").

Сам интерфейс Handler предельно прост. В нём только один метод, который возвращает строку.

public interface Handler {

    String getString();
}

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

@Component
public class DateHandler implements Handler {

    @Override
    public String getString() {
        return LocalDateTime.now().toString();
    }
}

Обратите внимание, что данный бин DateHandler помечен аннотацией @Component.

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

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

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

public class NameHandler implements Handler {

    private final String name;

    public NameHandler(String name) {
        this.name = name;
    }

    @Override
    public String getString() {
        return name;
    }
}

Теперь нам потребуется специальный конфигурационный класс с аннотацией @Configuration, который позволяет более гибко инициализировать бины.

@Configuration
public class AppConfiguration {

    @Bean
    public Handler firstNameHandler() {
        return new NameHandler("Alice");
    }

    @Bean
    public Handler secondNameHandler() {
        return new NameHandler("Bob");
    }
}

Здесь всё просто: один метод с аннотацией @Bean - один бин. В конструктор мы передаём ему константу. В итоге Spring создаст два бина, один из который всё время будет возвращать "Alice", а другой - "Bob".

Теперь для вызова новых обработчиков нам нужно внедрить их в наш сервис MainServiceImpl. Можно конечно прописать ещё два поля, но и тут Spring облегчает нам задачу. Достаточно создать одно поле, представляющее из себя коллекцию List из интерфейсов Handler, в которую Spring автоматически добавит ВСЕ бины, имеющие этот интерфейс. Вот ещё одна причина, по которой удобно использовать интерфейсы.

Наш сервис примет такой вид:

@Service
public class MainServiceImpl implements MainService {

    @Autowired
    private List<Handler> handlers;

    @Override
    public void doWork() {
        handlers.forEach(h -> System.out.println(h.getString()));
    }
}

В итоге в нашей коллекции обработчиков будет ровно три бина (DateHandler и два экземпляра NameHandler).

Для отображения значения каждого из них воспользуемся Stream API из Java 8, что равносильно обычному циклу foreach и вызову метода getString() у каждого из элементов.

Исходники проекта: https://github.com/nordmine/spring-simple-example

Если вам помог данный материал, поставьте +1.

15 сентября 2016 г.

Выравнивание строки на примере числа

Актуальная версия статьи доступна на моём новом сайте devmark.ru. 

Если у вас есть некое число, например, номер документа docId, количество цифр в котором вам заранее неизвестно. Но вам необходимо, чтобы число всегда составляло 5 знаков, то есть добавить необходимое количество нулей слева. Для этих целей идеально подходит стандартная библиотека от apache:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>


Нам нужен статический метод StringUtils.leftPad(). Пример кода:

int docId = 23;
System.out.println(StringUtils.leftPad(Integer.toString(docId), 5, '0'));

Например, для числа 23 получим результат "00023".

Другой метод rightPad() добавит символы соответственно справа от исходной строки.

12 июня 2016 г.

Сервер и клиент на Thrift

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

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

План

Мы опишем интерфейс нашего сервиса на специальном языке Thrift, реализуем этот интерфейс на сервере и будем вызывать его с клиента. Для удобства реализуем всё это в виде maven-проекта с тремя модулями (клиент, сервер и сгенерированные общие классы). Для примера, наш сервис будет возвращать профиль пользователя по его id.

Требования

Java 8, Apache Thrift 0.9, Maven 3.

Исходники


https://github.com/nordmine/examples

Maven-проект с модулями

Поскольку классы, которые генерит Thrift нам нужны как на сервере, так и на клиенте, мы выделим их в отдельный maven-модуль и будем подтягивать его через dependency в модуле сервера и модуле клиента. Все эти три модуля связаны между собой, поэтому я поместил их в один maven-проект с модулями, хотя это и не обязательно. Вы можете сделать два или три отдельных проекта.

<groupId>developer-remarks</groupId>
<artifactId>examples</artifactId>
<packaging>pom</packaging>
<version>1.0</version>

<modules>
    <module>thrift-entities</module>
    <module>thrift-server</module>
    <module>thrift-client</module>
</modules>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.thrift</groupId>
            <artifactId>libthrift</artifactId>
            <version>0.9.3</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

В корневом pom-файле определяются все зависимости и версии, которые будут доступны в модулях. Здесь нам потребуется только библиотека libthrift. Также в parent-pom определяются плагины для сборки.

Модуль с классами Thrift


Добавим в наш maven-проект новый модуль thrift-entities. В parent-pom у вас должна появится запись:

<modules>
    <module>thrift-entities</module>
</modules>

А в pom-файле нового модуля будет ссылка на parent-pom:

<parent>
    <artifactId>examples</artifactId>
    <groupId>developer-remarks</groupId>
    <version>1.0</version>
</parent>

<artifactId>thrift-entities</artifactId>

<dependencies>
    <dependency>
        <groupId>org.apache.thrift</groupId>
        <artifactId>libthrift</artifactId>
    </dependency>
</dependencies>

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

Теперь мы можем перейти к описанию нашего сервиса на языке Thrift. Поскольку в качестве примера мы выбрали сервис, возвращающий профиль пользотеля, пусть у каждого пользователя может быть некий статус. Статус имеет конечное множество значений, по сути, это enum. Предположим, профиль пользователя может быть не подтверждённым, подтверждённым (проверили почтовый ящик) и золотым (какие-то платные услуги подключены, например).

На языке Thrift перечисление будет выглядеть следующим образом (cначала указывается java-пакет). Для перечисления используется ключевое слово enum:

namespace java developer.remarks.thrift.v1.enums

enum PersonState {
    UNVERIFIED
    VERIFIED
    GOLD
}

Поместим его в файл thrift-entities/src/main/resources/Enums.thrift.

Затем в thrift-entities/src/main/resources/ создадим файл Models.thrift и поместим в него описание сущности самого профиля пользователя.

namespace java developer.remarks.thrift.v1.models

include "Enums.thrift"

struct Profile {
    1: i64 prsId
    2: string firstName
    3: string lastName
    4: i64 birthDate
    5: Enums.PersonState state
}


К текущему файлу могут быть подключены другие thrift-файлы при помощи ключевого слова include. Классы-сущности описываются при помощи ключевого слова struct.

Обратите внимание, что каждое поле структуры снабжается обязательным индексом - такова особенности Thrift. Строка обозначается ключевым словом string, 32-битные целые обозначаются как i32, 64-битные целые - как i64. Если при описании нужно сослаться на сущность из другого thrift-файла, к имени этой сущности прибавляют имя файла с точкой (Enums.PersonState).

Наш профиль содержит id пользователя, его имя и фамилию, статус, а также дату рождения. В Thrift нет встроенных типов работы с датами. Можно было бы конечно определить отдельную структуру с полями год, месяц и день. Но что, если нужно знать дату до секунды? Уже потребуется шесть полей. А шесть полей уже не очень удобно инициализировать. Кроме того, кто будет контролировать, что в году 12 месяцев, а в месяце не более 31 дня? Поэтому я предлагаю любые даты отправлять в виде 64-битного целого числа, которое представляет собой количество миллисекунд, прошедших с 1 января 1970 года (Unix timestamp). А на клиенте мы всегда сможем создать новый объект даты на основе этого значения.

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

namespace java developer.remarks.thrift.v1.services

include "Exceptions.thrift"
include "Models.thrift"

service ProfileService {

    Models.Profile getById(1: i64 prsId) throws (1: Exceptions.ProfileException e)
}

Интерфейсы сервисов определяются при помощи ключевого слова service. Параметры метода и все возможные исключения также снабжаются порядковыми номерами.

Хорошей практикой является создание отдельного, специального исключения, связанного с ошибками бизнес-логики нашего сервиса.

namespace java developer.remarks.thrift.v1.exceptions

exception ProfileException {
    1: i32 errorCode
    2: string description
}

В данном случае это исключение (ключевое слово exception) с двумя пронумерованными полями: код ошибки и текстовое описание.

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

Генерим java-классы на основе thrift-файлов


Теперь мы определили все сущности нашего сервиса и готовы сгенерить java-классы на основании наших файлов. Делается это при помощи утилиты thrift в командной строке:

thrift -gen java -r -out thrift-entities/src/main/java  thrift-entities/src/main/resources/Services.thrift

  • флаг -gen указывает, на каком языке следует сгенерить сущности
  • флаг -r говорит, что нужно сгенерить не только указанную сущность, но и пройтись по всем include.
  • флаг -out указывает директорию, куда следует сложить сгенерированные файлы.
  • в конце следует путь до целевого thrift-файла, как правило, это файл с сервисом.

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

В итоге у вас должна получиться такая структура в модуле thrift-entities:


Запускаем сервер


Модуль с сущностями нам понадобится на сервере. Добавим новый maven-модуль thrift-server. Его pom-файл делаем аналогично предыдущему, только туда добавляется ещё одна зависимость:

<dependencies>
    <dependency>
        <groupId>org.apache.thrift</groupId>
        <artifactId>libthrift</artifactId>
    </dependency>
    <dependency>
        <groupId>developer-remarks</groupId>
        <artifactId>thrift-entities</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

Теперь создадим обработчик для нашего сервиса. Для этого достаточно реализовать его интерфейс:

public class ProfileHandler implements ProfileService.Iface {

    @Override
    public Profile getById(long prsId) throws ProfileException, TException {
        if (prsId == 2) {
            long birthTimestamp = LocalDateTime.of(1985, 8, 31, 0, 0, 0)
                    .toEpochSecond(ZoneOffset.UTC);
            return new Profile(prsId, "Александр", "Кузнецов", 
                    birthTimestamp, PersonState.GOLD);
        } else {
            throw new ProfileException(-1, "Profile with id = " + prsId + " not found");
        }
    }
}


В идеале здесь могла бы быть работа с базой, но мы лишь имитируем получение профиля. Если id персоны равно 2, то возвращаем некоторые данные. Иначе имитируем отсутствие данных. Обратите внимание, что тут мы используем два класса (Profile и ProfileException), которые изначально определяли на thrift.

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

public class ProfileServer {

    private static final int PORT = 9090;

    public static ProfileHandler handler;
    public static ProfileService.Processor processor;

    public static void main(String[] args) {
        try {
            handler = new ProfileHandler();
            processor = new ProfileService.Processor(handler);

            Runnable simple = () -> perform(processor);

            new Thread(simple).start();
        } catch (Exception x) {
            x.printStackTrace();
        }
    }

    public static void perform(ProfileService.Processor processor) {
        try {
            TServerTransport serverTransport = new TServerSocket(PORT);
            TServer server = new TSimpleServer(
                    new TServer.Args(serverTransport).processor(processor)
            );

            System.out.println("Starting the simple server on port " + PORT + "...");
            server.serve();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


Добавляем клиент

Создадим новый модуль thrift-client. Его pom-файл будет аналогичен серверному. В этом модуле мы также используем thrift-сущности.

Код клиента предельно прост. Указываем хост и порт сервера, а также задаём взаимодействие через сокеты (TSocket) по бинарному протоколу (TBinaryProtocol):


public class ThriftClient {

    public static void main(String[] args) {
        try {
            TTransport transport = new TSocket("localhost", 9090);
            transport.open();

            TProtocol protocol = new TBinaryProtocol(transport);
            ProfileService.Client client = new ProfileService.Client(protocol);

            perform(client);

            transport.close();
        } catch (TException x) {
            x.printStackTrace();
        }
    }

    private static void perform(ProfileService.Client client) throws TException {
        Profile profile = client.getById(2L);

        Instant instant = Instant.ofEpochSecond(profile.getBirthDate());
        LocalDateTime birthDate = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());

        System.out.println(
                String.format("%s родился %s. Статус: %s.",
                        profile.getFirstName(),
                        DateTimeFormatter.ISO_DATE.format(birthDate),
                        profile.getState())
        );
    }
} 

Теперь можно запустить сервер и выполнить запрос с клиента для prsId = 2. В ответ вы получите профиль пользователя. Для любого другого значения prsId вы получите исключение.

Заключение


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