[Из песочницы] Spring Boot. Фоновые задачи и не только
Введение
В данном туториале я хочу привести пример приложения для отправки 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. Далее создаём базу данных с юзером и паролем. Если у вас линукс, то можете сделать это следующими командами:
sudo -u postgres createuser
sudo -u postgres createdb
$ sudo -u postgres psql
psql=# alter user with encrypted password '';
psql=# grant all privileges on database to ;
На 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)
protected Integer id;
private String name;
private String email;
private LocalDate birthday;
}
Итак мы создали класс с минимальным количеством полей, которые нам необходимы: id юзера, его имя, email и дата рождения.
Пройдёмся по аннотациям: Первые 4 над классом это аннотации lombok, которые генерируют геттеры, сеттеры, метод toString, и конструктор без аргументов.
Entity — указывает Hibernate, что данный класс является сущностью.
Table — название соответствует таблице в бд.
Id — указывает на первичный ключ данного класса.
@GeneratedValue — используется вместе с Id и определяет паметры strategy и generator.
Репозиторий
Далее в папке repository создаём интерфейс UserRepository:
public interface UserRepository extends JpaRepository {}
И это собственно практически весь наш DAO. Наследование от JpaRepository даёт нам возможность использовать его методы для работы с бд такие как delete, save, findAll и многие другие. Кроме этого при желании мы можем создавать свои методы, по принципу «пишем то что нужно». Т е если нам нужно найти всех юзеров с одинаковым именем, то наш метод будет выглядеть так:
List findAllByName(String name);
Данный метод в итоге создаст SQL запрос подобный этому:
SELECT * FROM users WHERE name = ?;
Или например:
List findByBirthdayAfter(LocalDate date);
Позволит выбрать всех юзеров родившихся после определенной даты.
Вообще это довольно обширная тема, на которую довольно много статей и видео. Как например вот это.
Мы же создадим метод, который будет брать из базы всех юзеров, у которых дата рождения и email не null, и теперь наш репозиторий будет выглядеть следующим образом:
@Repository
public interface UserRepository extends JpaRepository {
List findAllByBirthdayIsNotNullAndEmailIsNotNull();
}
Пара особенностей репозитория
Первый параметр дженерика должен быть сущностью с которой мы будем работать, а второй соответствовать типу его первичного ключа.
Также типы методов должны соответствовать первому параметру.
Если вдруг у вас возник вопрос, почему данный каталог называется repository, а не dao, то это правило хорошего тона в Spring Boot, вы не обязаны делать так-же, просто так принято.
Сервисы
Первым делом создадим в каталоге service интерфейс UserRepositoryService:
public interface UserRepositoryService {
List getAll();
}
Далее здесь же создаем ещё один каталог impl и в нём класс-имплементацию для нашего сервиса:
@Service
public class UserRepositoryServiceImpl implements UserRepositoryService {
private final UserRepository repository;
@Autowired
public UserRepositoryServiceImpl(UserRepository repository) {
this.repository = repository;
}
@Override
public List getAll() {
return repository.findAllByBirthdayIsNotNullAndEmailIsNotNull();
}
}
Теперь разберём наш класс:
Аннотация Service показывает спрингу, что это сервис.
Далее объявляем переменную типа UserRepository и инициализируем её в конструкторе, предварительно пометив его аннотаций @Autowired.
(Можно поставить аннотации прямо над полем repository, но предпочтительнее создать конструктор или сеттер)
@Autowired — спринг находит нужный бин и подставляет его значение в свойство помеченное аннотацией.
После конструктора реализуем метод нашего интерфейса и в нём возвращаем метод из репозитория.
Идём дальше: в каталоге service создаём EmailService:
public interface EmailService {
void send(String to, String title, String body);
}
И его имплементацию EmailServiceImpl в impl:
@Service
public class EmailServiceImpl implements EmailService {
private final JavaMailSender emailSender;
@Autowired
public EmailServiceImpl(JavaMailSender javaMailSender) {
this.emailSender = javaMailSender;
}
@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.
Сразу определим в нём следующие поля:
@Service
public class SchedulerService {
private static final Logger LOG = LoggerFactory.getLogger(SchedulerService.class);
private LocalDate DATE = LocalDate.now();
private final UserRepositoryService userService;
private final EmailService emailService;
@Autowired
public SchedulerService(EmailService emailService, UserRepositoryService userService) {
this.emailService = emailService;
this.userService = userService;
}
}
Итак мы инициализировали логгер, переменную с текущей датой (Java 8 time api), user и email сервисы в конструкторе.
Далее создадим void метод sendMailToUsers, а над ним укажем аннотацию:
@Scheduled(cron = "*/10 * * * * *")
Данная аннотация позволяет указывать то, когда наш метод будет работать. Мы используем параметр cron, позволяющий указывать расписание по конкретным часам и датам. Также есть такие параметры как fixedRate (определяет интервал между вызовами метода), fixedDelay (определяет интервал с момента окончания работы последнего вызова метода и началом работы следующего), initialDelay (количество миллисекунд для задержки перед первым выполнением fixedRate или fixedDelay) и ещё парочка.
Каждая звездочка в строке cron означает секунды, минуты, часы, дни, месяцы, и дни недели. Вот более подробно. Сейчас значение означает, что проверка будет проходить каждые 10 секунд, это сделано для примера, в дальнейшем мы это поменяем.
Значение cron для удобства можно вынести в константу:
private static final String CRON = "*/10 * * * * *";
В методе создадим проверку на то, не вернёт ли нам метод getAll null или пустой список и лист юзеров, который будет содержать всех пользователей из нашей таблицы в бд:
if (userService.getAll() != null && userService.getAll().size() != 0) {
List list = userService.getAll();
}
Теперь пройдёмся по нему и сделаем проверку на то, чтобы месяц и день рождения юзера совпадал с текущими, используя методы time api.
list.forEach(user -> {
if (DATE.getMonth() == user.getBirthday().getMonth() && DATE.getDayOfMonth() == user.getBirthday().getDayOfMonth()) {
}
});
Если проверки удовлетворены, то создаём переменную для сообщения типа StringBuffer (т к он справляется с конкатенациями быстрее стринга и потокобезопасен в отличие от StringBuilder-a) и с помощью метода append добавляем в него нужные строки с поздравлениями юзеру. После чего вызываем метод send из EmailService, и передаём в него email юзера, заголовок и наше сообщение. В конце оборачиваем всё в try/catch во избежание исключений. Всё, наш метод готов.
Смотрим на весь класс:
@Service
public class SchedulerService {
private static final Logger LOG = LoggerFactory.getLogger(SchedulerService.class);
private static final String CRON = "*/10 * * * * *";
private LocalDate DATE = LocalDate.now();
private final UserRepositoryService userService;
private final EmailService emailService;
@Autowired
public SchedulerService(EmailService emailService, UserRepositoryService userService) {
this.emailService = emailService;
this.userService = userService;
}
@Scheduled(cron = CRON)
public void sendMailToUsers() {
if (userService.getAll() != null && userService.getAll().size() != 0) {
List list = userService.getAll();
list.forEach(user -> {
if (user.getBirthday() != null) {
if (DATE.getMonth() == user.getBirthday().getMonth() && DATE.getDayOfMonth() == user.getBirthday().getDayOfMonth()) {
try {
StringBuffer message = new StringBuffer();
message.append("Happy Birthday dear ")
.append(user.getName())
.append("!");
emailService.send(user.getEmail(), "Happy Birthday!", message.toString());
LOG.info("Email have been sent. User's id: {}, Date: {}", user.getId(), DATE);
} catch (Exception e) {
LOG.error("Email can't be sent.User's id: {}, Error: {}", user.getId(), e.getMessage());
}
}
}
});
}
}
}
Теперь, чтобы иметь возможность запускать фоновые задачи добавим в наш 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-ов) тестирование и безопасность.
Конструктивная критика и замечания по теме приветствуются.
Ссылки
Исходный код на github
Официальная документация Spring Boot