Показаны сообщения с ярлыком java. Показать все сообщения
Показаны сообщения с ярлыком java. Показать все сообщения

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.

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

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() добавит символы соответственно справа от исходной строки.

7 июня 2016 г.

Stream API в Java 8

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

В Java 8 появилось довольно важное нововведение под названием Stream. И здесь имеются в виду не потоки ввода/вывода. Stream - это абстракция, позволяющая с любыми объектами работать как с потоками. Порой это чем-то похоже на выполнение запросов к БД. Рассмотрим несколько типовых задач, с которыми часто сталкивается каждый разработчик.

Объединение нескольких строк в одну

Наверняка вам приходилось генерить одну строку из нескольких других, разделённых запятыми. При этом после последнего элемента запятой быть не должно. Знакомо? В java 8 это делается так:

Stream.of("Linux", "Windows", "Mac")
    .collect(Collectors.joining(", ")));

Мы создаём новый поток из простых строк, а затем собираем их в одну при помощи метода collect. В результате получим следующую строку:

Linux, Windows, Mac

Сгенерить N одинаковых элементов

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

Stream.generate(() -> "?")                
        .limit(3)                         
        .collect(Collectors.joining(", "))

Получим:

?, ?, ?

Работа с произвольными объектами

Потоки могут работать с любыми типами объектов. Давайте создадим простой бин, представляющий имя и возраст человека. Для наглядности также переопределим метод toString() у этого бина:

public class Person {

    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }


    public String getName() {
        return name;
    }


    public int getAge() {
        return age;
    }


    @Override
    public String toString() {
        return String.format("%s (%s)", name, age);
    }
}

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

List<Person> persons = new ArrayList<>();
persons.add(new Person("Боб", 25));
persons.add(new Person("Алиса", 20));
persons.add(new Person("Николай Петрович", 60));

Фильтрация элементов

Выберем всех пользователей не старше 30 лет и выведем каждого из них на экран:

persons.stream()                       
        .filter(p -> p.getAge() <= 30) 
        .forEach(System.out::println); 

Обратите внимание, что Stream можно получить из любой коллекции при помощи метода stream(). Затем в методе filter используем лямбда-выражение (вместо p можно использовать любое другое имя). В качестве выражения фильтрации можно использовать абсолютно любое выражение, возвращающее тип boolean. И наконец, для каждого из оставшихся элементов мы вызываем статическую функцию println. Обратите внимание на её особую запись. Эту запись можно использовать для любого метода, принимающего один параметр.

В результате на экране увидим Боба и Алису, а вот Николаю Петровичу не повезло:

Боб (25)
Алиса (20)

Среднее значение

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

persons.stream()                                          
        .collect(Collectors.averagingInt(Person::getAge)) 

Для метода Person.getAge опять используем сокращённую запись. В результате получим, что средний возраст составляет 35,0 лет.

Преобразование элементов


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

persons.stream()                            
        .map(p -> p.getName().toUpperCase())
        .collect(Collectors.joining(", "))  

Результат:

БОБ, АЛИСА, НИКОЛАЙ ПЕТРОВИЧ

Чтение файла в строку

Прочитать все строки текстового файла и объединить их в одну строку можно так:

Path path = Paths.get(путь_до_файла);                        
Files.lines(path, StandardCharsets.UTF_8)                    
        .collect(Collectors.joining(System.lineSeparator()));

Обратите внимание, что для объединения строк здесь используется платформенно-независимый метод System.lineSeparator(), возращающий один или два символа перевода строки в зависимости от вашей ОС.

Запись коллекции строк в файл

Похожим образом производится и запись строк в файл:

Path path = Paths.get("путь_до_файла");
Files.write(path, Arrays.asList("first line", "second line"), StandardCharsets.UTF_8);

На выходе получим файл, состоящий из двух строк.

6 июня 2016 г.

Spring Boot Restful Service

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

Что мы получим в результате

Простой сервис, который при выполнении get-запроса будет возращать профиль пользователя в формате json в зависимости от id, который передаётся в запросе. При возникновении исключительных ситуаций (например, профиль не найден), пользователь получит соответствующий ответ и это будет зафиксировано в логе.

Требования

Java 8, Spring Boot 1.3.5, Maven 3

Исходники

https://github.com/nordmine/spring-boot-restful-service

Реализуем обработку get-запроса


Сразу оговорюсь, что здесь рассмотрю только создание самого веб-сервиса. Чаще всего, он будет обращаться к базе для получения профиля пользователя. Мы же этого здесь делать не будем, а только сымитируем загрузку профиля по id. Но всё, что касается взаимодействия по http, будет работать как положено.

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

Давайте создадим maven-проект, в котором в качестве parent укажем spring-boot-starter-parent. Также нам потребуется добавить одну зависимость spring-boot-starter-web. Этого вполне достаточно для нашего проекта.

<parent>                                                
    <groupId>org.springframework.boot</groupId>         
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>1.3.5.RELEASE</version>                    
</parent>                                               
                                                        
<dependencies>                                          
    <dependency>                                        
        <groupId>org.springframework.boot</groupId>     
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>                                       
</dependencies>                                         

Для упрощения процедуры развёртывания добавим spring-boot-maven-plugin. Он создаст нам один jar-файл со всеми необходимыми зависимостями внутри, а также определит точку запуска для нашего приложения.

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

Теперь добавим класс developer.remarks.app.RestfulApplication, который будет содержать единственный статический метод main. Он и будет отправной точкой при старте нашего приложения.

@SpringBootApplication(scanBasePackages = "developer.remarks")
public class RestfulApplication {

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

Обратите внимание на аннотацию @SpringBootApplication. По сути это замена трёх стандартных для спринга аннотаций @Configuration (программная конфигурация бинов), @EnableAutoConfiguration (автоматически создавать необходимые бины), @ComponentScan (где искать бины). Также важно указать параметр scanBasePackages. Он содержит базовый пакет, в котором Spring Boot будет искать бины. Если этого не сделать или указать пакет неправильно, у вас ничего работать не будет. Вместо пакета можно также явно указать конкретный класс.

А если перенести класс RestfulApplication в пакет developer.remarks, то и scanBasePackages указывать не обязательно, т.к. все бины в пределах этого пакета (включая вложенные) будут подтягиваться автоматически. Удобно, однако производя глобальные рефакторинги в больших проектах об этом можно легко забыть и внезапно вы обнаружите неработающее приложение. Причём ваша среда разработки такую ошибку скорее всего не обнаружит. Поэтому я за явные параметры в аннотациях.

Теперь давайте создадим класс профиля пользователя. Именно в нём содержатся все поля, которые будет возвращать наш сервис (уникальный id пользователя, его имя и фамилия). Это простой бин, которому даже не требуется специальных аннотаций. Однако будьте внимательны: Spring многое делает автоматически, но он не увидит те поля, для которых явно не сгенерены getter'ы.

public class Profile {

    private final int id;
    private final String firstName;
    private final String lastName;

    public Profile(int id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

Теперь мы можем добавить сервис, содержащий уровень бизнес-логики. Как я уже говорил, там может быть обращение к БД, но мы ограничимся лишь имитацией: если id = 2, то возвращаем некий профиль, иначе говорим, что профиль с указанным номером не существует.

@Component
public class ProfileService {

    public Profile getProfile(int personId) {
        // имитируем обращение к БД
        if (personId == 2) {
            return new Profile(personId, "Иван", "Петров");
        } else {
            throw new ProfileNotFoundException(personId);
        }
    }
}

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

ProfileNotFoundException наследуется от RuntimeException и не требует явного указания в сигнатуре метода, т.е. это исключение непроверяемое (unchecked). Это исключение отличается от стандартного тем, что содержит id профиля пользователя, который привёл к ошибке, а также переопределяет метод getMessage(), чтобы клиент, выполняющий запрос, получил более понятное описание ошибки.

Теперь мы готовы добавить в наш проект rest-контроллер. Его мы снабдим соответствующей аннотацией @RestController:

@RestController
@RequestMapping(value = "/profile", produces = MediaType.APPLICATION_JSON_VALUE)
public class ProfileController {

    private final ProfileService profileService;

    @Autowired
    public ProfileController(ProfileService profileService) {
        this.profileService = profileService;
    }

    @RequestMapping(value = "/{personId:\\d+}", method = RequestMethod.GET)
    @ResponseBody
    public Profile getProfile(@PathVariable String personId) {
        return profileService.getProfile(Integer.valueOf(personId));
    }
}

Обратите внимание на аннотацию @RequestMapping. С её помощью мы указываем, что данный контроллер обрабатывает все http-запросы, выполняемые по адресу адрес_сервера:8080/profile/*. Если вы запускаете сервис на локальной машине, то адресом сервера будет, разумеется, localhost. Также через параметр produces мы указываем, что сервис возвращает ответ в формате json.

Далее целевой метод аналогичным образом мапится уже на get-запрос адрес_сервера:8080/profile/ид_пользователя. Причём номер пользователя должен содержать только цифры. Spring автоматически поместит значение из адреса в переменную personId благодаря аннотации @PathVariable.

В самом методе мы просто конвертируем полученный номер пользователя из строки в целое число (можем это делать благодаря регулярке в @RequestMapping) и дёргаем соответствующий метод у нашего сервиса, который инжектим в данный контроллер через конструктор при помощи аннотации @Autowired. Можно было бы инжектить и напрямую в поле, но мировая общественность выступает за то, что удобнее это делать через конструктор. В частности, это облегчает написание юнит-тестов.

Очень важно здесь отметить, что мы инкапсулируем всю бизнес-логику в ProfileService и изолируем её от непосредственного rest-взаимодействия. Таким образом, если завтра нам помимо json потребуется добавить ещё и xml-контроллер, мы легко это сделаем без копипаста, задействовав тот же ProfileService.

В принципе, можете собирать приложение мавеном (mvn clean package) и запускать его через java -jar.

java -jar target/spring-boot-restful-service-1.0-SNAPSHOT.jar

Если выполнить get-запрос по адресу http://localhost:8080/profile/2, вы получите в ответ следующий json:

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

Добавляем обработку ошибок


Сервис вроде бы работает. Однако что получит пользователь, если профиль не найден? Желательно, чтобы он получал корректное описание ошибки также в формате json.

Spring Boot позволяет перехватывать все исключения, возникающие в каком-либо из контроллеров, при помощи аннотации @ControllerAdvice.

@ControllerAdvice
public class ErrorController {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorInfo processException(Exception e) {
        return new ErrorInfo(e.getMessage());
    }
}

В @ExceptionHandler указывается одно или несколько перехватываемых исключений. Если требуется указать более одного, их нужно взять в фигурные скобки. ErrorInfo - также обычный бин, который содержит текстовое описание ошибки. Здесь тоже нельзя забывать про добавление getter'ов. В идеале, сюда бы ещё добавить код ошибки для упрощения отладки и поиска багов.

Теперь, если выполнить запрос http://localhost:8080/profile/1, вы получите ответ:

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

Обратите внимание, что текст сообщения определяется в методе getMessage() перехваченного исключения. То есть различные исключения у нас обрабатываются единообразно.

Добавляем логирование

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

Spring Boot по умолчанию использует библиотеку логирования logback и ищет конфигурационный файл logback-spring.xml в проекте. Полное содержимое этого файла вы можете посмотреть в git-репозитории, а я здесь приложу только значимые части.

Сначала определяем appender. В нём указывается, как будет называться файл с логами, а также политика его ротации. В данном случае, если размер лога превысит 10 МБ, будет создан новый файл, а старый будет переименован и получит индекс.

    <appender name="error-controller" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_PATH}/exceptions.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>${LOG_PATH}/exceptions.log.%i</fileNamePattern>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

Затем определяем сам logger, ссылаясь на этот аппендер:

<logger name="developer.remarks.controller.ErrorController" level="INFO">
    <appender-ref ref="error-controller" />                              
</logger>

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

Модифицируем наш ErrorController для ведения логов:

@ControllerAdvice
public class ErrorController {

    private Logger logger = LoggerFactory.getLogger(ErrorController.class);

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorInfo processException(Exception e) {
        logger.error("Unexpected error", e);
        return new ErrorInfo(e.getMessage());
    }
}

При запуске также рекомендую явно указывать путь к директории с логами при помощи параметра logging.path. Итого, полная строка запуска нашего restful-сервиса:

java -jar target/spring-boot-restful-service-1.0-SNAPSHOT.jar -Dlogging.path=путь_до_директории_с_логами


Вот и всё. Если после прочтения статьи возникли вопросы, пишите их в комментариях. Напоминаю, что более подробно работающую версию вы можете посмотреть здесь: https://github.com/nordmine/spring-boot-restful-service.