Pull to refresh

Spring Boot. Фоновые задачи и не только

Reading time 10 min
Views 36K

Введение


В данном туториале я хочу привести пример приложения для отправки email-ов юзерам, основываясь на дате их рождения(например с поздравлениями), используя аннотацию Scheduled. Я решил привести данный пример, т к по моему мнению он включает в себя довольно многие вещи, такие как работа с базой данных(в нашем случает это PostgreSQL), Spring Data JPA, новый java 8 time api, email-сервис, создание фоновых задач и небольшую бизнес-логику при этом оставаясь компактным. Сегодня интернет пестрит огромным множеством туториалов которые обычно сводятся к тому как наследоваться от CrudRepository, JpaRepository и тд. Туториал расчитан на то, что вы уже смотрели хотя бы некоторые из них и имеете представление о том, что такое Spring Boot. Я же постараюсь показать пример приложения, которое более обширно показывает его возможности и как с ним работать.

Создание проекта


Идем на Spring Initializr.

Добавляем зависимости:

1. PosgreSQL — в качестве базы данных
2. JPA — доступ к базе
3. Lombok — для удобства и избавления от бойлерплейт кода(не придётся писать геттеры, сеттеры и тд самим), подробнее тут
4. Mail — собственно для работы и отправки email-ов, оф. документация

Указываем группу и артефакт, к примеру com.application и task. Скачиваем и распаковываем проект, затем открываем его в среде разработки, у меня это Intellij IDEA.

База данных


Теперь устанавливаем себе PostgreSQL. Далее создаём базу данных с юзером и паролем. Можно сделать это прямо из IDEA, во вкладке database, можно с помощью командной строки если у вас линукс, следующими командами:

sudo -u postgres createuser <username>
sudo -u postgres createdb <dbname>
$ sudo -u postgres psql
psql=# alter user <username> with encrypted password '<password>';
psql=# grant all privileges on database <dbname> to <username> ;

Также на windows это можно сделать с помощью pgAdmin или его альтернатив.

Начало


Открываем наш проект и можем приступать к написанию кода.

Сейчас у нас в проекте есть только один java-файл. Он выглядит примерно так:

@SpringBootApplication
public class TaskApplication {

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

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

Данный класс это точка запуска приложения. Аннотация @SpringBootApplication означает, что это Spring Boot приложение и эквивалентна использованию @Configuration, @EnableAutoConfiguration и @ComponentScan.

Создание модели


Первым делом разделим каталог в котором лежит наш класс для запуска всего приложения и разделим его на три директории: model, repository, service.

Далее в папке model создаем класс User:

@Getter
@Setter
@ToString
@NoArgsConstructor
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @Column(name = "name", nullable = false)
    private String name;

    @Email
    private String email;

    private LocalDate birthday;
}

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

Пройдёмся по аннотациям: Первые 4 над классом это аннотации lombok, которые генерируют геттеры, сеттеры, метод toString, и конструктор без аргументов.

Entity — указывает Hibernate, что данный класс является сущностью.
Table — название соответствует таблице в бд.
Id — указывает на первичный ключ данного класса.
@GeneratedValue — используется вместе с Id и определяет паметры strategy и generator.
@Column — указывает на имя колонки, которая отображается в свойство сущности, также с помощью nullable = false указываем на то, что поле обязательно.
Email — строка должна быть валидным адресом электронной почты(Используется пакет javax.validation.constraints, а не org.hibernate.validator.constraints, т к в последнем данная аннотация является устаревшей).

Репозиторий


Далее в папке repository создаём интерфейс UserRepository:

public interface UserRepository extends JpaRepository<User, Integer> {}

Наследование от JpaRepository даёт нам возможность использовать его методы для работы с бд такие как delete, save, findAll и многие другие. Кроме этого при желании мы можем создавать свои методы, по принципу «пишем то что нужно». Т е если нам нужно найти всех юзеров с одинаковым именем, то наш метод будет выглядеть так:

List<User> findAllByName(String name);

Данный метод в итоге создаст SQL запрос подобный этому:

SELECT * FROM users WHERE name = ?;

Или например:

List<User> findByBirthdayAfter(LocalDate date);

Позволит выбрать всех юзеров родившихся после определенной даты.

Вообще это довольно обширная тема, на которую довольно много статей и видео. Как например вот это.
Также есть возможность писать свои sql запросы используя JPA-аннотацию Query прямо над телом метода.
Есть возможность использовать два типа синтаксиса: JPQL(язык запросов JPA, подобный SQL использующий вместо таблиц и колонок — сущности, атрибуты и тд) либо собственно используемый нами SQL(тогда добавляется свойство nativeQuery = true). Пример с JPQL:

    
@Query(value = "SELECT u from User u where u.name = ?1")
List<User> findAllByName(String name);

Для указывания имени параметра запроса можно использовать аннотацию JPA @Param:


@Query(value = "SELECT u from User u where u.name = :name")
List<User> findAllByName(@Param("name") String name);

Если же мы хотим использовать чисто SQL то:

    
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
List<User> findAllByName(@Param("name") String name);

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


@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    @Query(value = "SELECT * FROM users " +
            "WHERE email IS NOT NULL " +
            "AND extract(MONTH FROM birthday) = :m " +
            "AND extract(DAY FROM birthday) = :d",
            nativeQuery = true)
    List<User> findByMatchMonthAndMatchDay(@Param("m") int month, @Param("d") int day);
}

Пара особенностей репозитория

Первый параметр дженерика должен быть сущностью с которой мы будем работать, а второй соответствовать типу его первичного ключа.
Также типы методов должны соответствовать первому параметру(если не использовать собственный мэппинг).
Если вдруг у вас возник вопрос, почему данный каталог называется repository, а не dao, то это правило хорошего тона в Spring Boot, вы не обязаны делать так-же, просто так принято.

Сервисы


Первым делом создадим в каталоге service интерфейс UserRepositoryService:


public interface UserRepositoryService {
    List<User> getAll(int month, int day);
}

Далее здесь же создаем ещё один каталог impl и в нём класс-имплементацию для нашего сервиса:


@Service
public class UserRepositoryServiceImpl implements UserRepositoryService {

    private final UserRepository repository;

    @Autowired
    public UserRepositoryServiceImpl(UserRepository repository) {
        this.repository = repository;
    }

    @Override
    public List<User> getAll(int month, int day) {
        return repository.findByMatchMonthAndMatchDay(int month, int day);
    }
}

Теперь разберём наш класс:
Аннотация Service показывает спрингу, что это сервис.
Далее объявляем переменную типа UserRepository и инициализируем её в конструкторе, предварительно пометив его аннотаций @Autowired.
(Можно поставить аннотации прямо над полем repository, но предпочтительнее создать конструктор или сеттер)
@Autowired — спринг находит нужный бин и подставляет его значение в свойство помеченное аннотацией.
Есть возможность создания autowired конструктора с помощью аннотации ломбока над классом:

@RequiredArgsConstructor(onConstructor = @__(@Autowired))

После конструктора реализуем метод нашего интерфейса и в нём возвращаем метод из репозитория.
Идём дальше: в каталоге service создаём EmailService:

public interface EmailService {
    void send(String to, String title, String body);
}

И его имплементацию EmailServiceImpl в impl:

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class EmailServiceImpl implements EmailService {

    private final JavaMailSender emailSender;

    @Override
    public void send(String to, String subject, String text) {
        MimeMessage message = this.emailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message);
        try {
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(text);
            this.emailSender.send(message);
        } catch (MessagingException messageException) {
            throw new RuntimeException(messageException);
        }
    }
}

Не буду углубляться в описание, вот ОД.

Теперь в service создадим наш последний и основной класс с шедулером и бизнес-логикой, назовём его к примеру SchedulerService.

Сразу определим в нём следующие поля:


@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SchedulerService {

    private final UserRepositoryService userService; 

    private final EmailService emailService;

}

Итак мы инициализировали логгер (также аннотаций ломбока @Slf4j), user и email сервисы в конструкторе(@RequiredArgsConstructor(onConstructor = @__(@Autowired))).

Далее создадим void метод sendMailToUsers а над ним укажем аннотацию:

@Scheduled(cron = "*/10 * * * * *")

Данная аннотация позволяет указывать то, когда наш метод будет работать. Мы используем параметр cron, позволяющий указывать расписание по конкретным часам и датам. Также есть такие параметры как fixedRate(определяет интервал между вызовами метода), fixedDelay(определяет интервал с момента окончания работы последнего вызова метода и началом работы следующего), initialDelay(количество миллисекунд для задержки перед первым выполнением fixedRate или fixedDelay) и ещё парочка.

Каждая звездочка в строке cron означает секунды, минуты, часы, дни, месяцы, и дни недели. Вот более подробно. Сейчас значение означает, что проверка будет проходить каждые 10 секунд, это сделано для примера, в дальнейшем мы это поменяем.

Значение cron для удобства можно вынести в константу:

private static final String CRON = "*/10 * * * * *";

В методе создадим переменную с текущей датой(java date and time api), переменные для месяца и дня, которые берутся из даты, лист юзеров, который инициализируется методом из нашего сервиса и проверку на то, не будет ли он пустым:


    @Scheduled(cron = CRON)
    public void sendMailToUsers() {
        LocalDate date = LocalDate.now();
        int month = date.getMonthValue();
        int day = date.getDayOfMonth();
        List<User> list = userService.getUsersByBirthday(month, day);
        if (!list.isEmpty()) {
            
        }
    }

Теперь пройдёмся по нему и для каждого юзера создаём переменную для сообщения и вызываем метод send из EmailService, и передаём в него email юзера, заголовок и наше сообщение. В конце оборачиваем всё в try/catch во избежание исключений. Всё, наш метод готов.

Смотрим на весь класс:


@Service
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SchedulerService {

    private static final String CRON = "*/10 * * * * *";

    private final UserRepositoryService userService;

    private final EmailService emailService;

    @Scheduled(cron = CRON)
    public void sendMailToUsers() {
        LocalDate date = LocalDate.now();
        int month = date.getMonthValue();
        int day = date.getDayOfMonth();
        List<User> list = userService.getUsersByBirthday(month, day);
        if (!list.isEmpty()) {
            list.forEach(user -> {
                try {
                    String message = "Happy Birthday dear " + user.getName() + "!";
                    emailService.send(user.getEmail(), "Happy Birthday!", message);
                    log.info("Email have been sent. User id: {}, Date: {}", user.getId(), date);
                } catch (Exception e) {
                    log.error("Email can't be sent.User's id: {}, Error: {}", user.getId(), e.getMessage());
                    log.error("Email can't be sent", e);
                }
            });
        }
    }

}

Теперь, чтобы иметь возможность запускать фоновые задачи добавим в наш TaskApplication аннотацию @EnableScheduling прямо над @SpringBootApplication, чтобы он в итоге выглядел вот так:

@EnableScheduling
@SpringBootApplication
public class TaskApplication {

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

На этом работа с java кодом закончена, нам осталось только в файле application.properties в каталоге resources указать конфиги.

Конфигурация


# Локальный порт сервера. Может быть любым, главное чтобы не был занят
server.port=7373
# База данных
spring.jpa.database=POSTGRESQL
spring.jpa.show-sql=true
spring.datasource.platform=postgres
spring.jpa.generate-ddl=true       
spring.jpa.hibernate.ddl-auto=update

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/your_database?stringtype=unspecified
spring.datasource.username=your_database_username
spring.datasource.password=your_database_password
# Логирование
logging.level.org.hibernate=info
logging.level.org.springframework.security=debug
# Предотвращает возможные ошибки связанные с jpa и postgreSQL
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
#Настройки email-a
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=your_email@gmail.com
spring.mail.password=your_password
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

Пара объяснений:
spring.jpa.generate-ddl=true       
spring.jpa.hibernate.ddl-auto=update 

Исользуются для автоматического создания/обновления таблицы в бд, используя нашу сущность.(В продакшне значения лучше менять на false и none)

spring.datasource.url=jdbc:postgresql://localhost:5432/your_database?stringtype=unspecified
spring.datasource.username=your_database_username
spring.datasource.password=your_database_password

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

spring.mail.username=your_email@gmail.com
spring.mail.password=your_password

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

Запуск


Идём в наш TaskApplication и запускаем приложение. Если всё сделано правильно, то у вас должны будут идти подобные логи каждые 10 секунд:

Hibernate: select users0_.id as id1_0_, users0_.birthday as birthday2_0_, users0_.email as email3_0_, users0_.name as name4_0_ from users users0_ where (users0_.birthday is not null) and (users0_.email is not null)

Означающие, что наш метод как минимум берёт лист юзеров из бд. Теперь если мы откроем нашу базу(я это делаю прямо в IDEA. Во вкладке database, обычно в правом верхнем углу, есть возможность подключиться к нужной нам бд), то увидим, что там появилась таблица users с соответствующими полями. Создадим новую запись и в качестве дня рождения впишем текущую дату, а в качестве email-a свой собственный. После коммита изменений, каждые 10 секунд должен появляться наш лог сообщающий о том, что email-успешно послан. Проверяем email и если всё сделано корректно, то там нас должны ждать одно или несколько поздравлений с днём рождения(В зависимости от того сколько раз отработал метод). Останавливаем наше приложение и меняем значение CRON на «0 0 10 * * *» означающее, что теперь проверка будет проходить не каждые 10 секунд, а ежедневно в 10 утра, что гарантирует нам отправку только одного поздравления.

Заключение


На основе данного примера можно создавать и решать разнообразные задачи, связанные в частности с фоновыми процессами, главное не бояться экспериментировать. Надеюсь сегодня я смог помочь кому-нибудь лучше понять как работать со Spring Boot, базами данных и java. Если кому-то будет интересно, то я могу написать вторую часть статьи, с добавлением контроллера(чтобы например при желании можно было отключать рассылку email-ов) тестирование и безопасность.

Конструктивная критика и замечания по теме приветствуются.
Отдельное спасибо за комментарии: StanislavL, elegorod, APXEOLOG, Singaporian

Ссылки


Исходный код на github
Официальная документация Spring Boot
Tags:
Hubs:
+18
Comments 68
Comments Comments 68

Articles