Во введении мы уже использовали Gateway, в этой статье будем использовать адаптеры. Мы напишем приложение, которое будет получать данные Animal из почтового ящика через Imap Inbound Adapter и добавлять их в базу данных через Jpa Outbound Adapter.
Подготовка
Данные
Класс Animal выглядит так:
@Entity
public class Animal {
@Id
@GeneratedValue
private long id;
private String name;
//constructors/getters/setters
}
Мы сделали его JPA-сущностью с помощью аннотации @Entity, так как наш пример демонстрирует именно Jpa Outbound Adapter (а есть еще Jdbc Outbound Adapter).
Каждое письмо будет содержать в теле ровно одно Animal, у которого имя сгенерировано случайно с помощью Math.random() с префиксом cat:
cat0.8561380482057016
Заголовок и отправитель письма для работы программы не важен, так что можно накидать письма вручную. Но в моем приложении есть вспомогательный поток с автогенерицией писем.
Maven-зависимости
Помимо spring-integration, не забудьте включить дополнительные библиотеки для работы с почтой и JPA:
<!--для Jpa Outbound Adapter-->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-jpa</artifactId>
</dependency>
<!--для Imap Inbound Adapter
и Smtp Outbound Adapter-->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mail</artifactId>
</dependency>
<!--используется Imap Inbound Adapter
и Smtp Outbound Adapter для работы с почтой-->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<!--аналогично основная зависимость
для работы с базой через JPA-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- в приложении используется реальная база postgresql-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.8</version>
</dependency>
Настройки почтового сервера
В приложении используется почтовый сервер mail.ru. Чтобы не устанавливать сертификат, в настройках при отправке письма (во вспомогательном потоке) будем задавать свойства:
p.put("mail.smtp.ssl.trust", "*");
p.put("mail.smtp.starttls.enable", "true");
а при получении:
p.put("mail.imaps.ssl.trust", "*");
Лучше завести новый пустой ящик, поскольку в нашем потоке нет фильтрации писем, и при получении посторонних писем возникнет исключение.
Основной поток
Создадим стандартный поток, который начинается с Inbound Adapter, а заканчивается Outbound Adapter. Конкретно наш будет выглядеть так:
Imap Inbound Adapter — Service Activator — Service Activator — Filter — Jpa Outbound Adapter
@Bean
public IntegrationFlow mailListener() {
/* внимание, надо закодировать имя и пароль с URLEncoder.encode(),
чтоб не было лишних @
поэтому email@mail.ru превращается в email%40mail.ru*/
return IntegrationFlows.from(Mail.imapInboundAdapter(
"imaps://"
+ URLEncoder.encode(this.email, Charset.defaultCharset())
+ ":"
+ URLEncoder.encode(this.password, Charset.defaultCharset())
+ "@imap.mail.ru:993/inbox"
).javaMailProperties(p -> {
p.put("mail.debug", "false");
p.put("mail.imaps.ssl.trust", "*");
}),
e -> e.poller(Pollers.fixedDelay(1000).maxMessagesPerPoll(1)))
.<javax.mail.Message>handle((payload, headers) -> (payload))
.handle(converter, "animalFromEmail")
.<Animal>filter(animal -> !animal.getName().startsWith("cat0.1"))
.handle(Jpa.outboundAdapter(this.entityManagerFactory)
.entityClass(Animal.class)
.persistMode(PersistMode.PERSIST),
e -> e.transactional(true)).get();
}
После входного адаптера мы берем из сообщения типа org.springframework.messaging.Message полезную нагрузку payload — получается письмо типа javax.mail.Message. Делает это первый Service Activator в методе handle().
Второй Service Activator (второй метод handle()) получает из письма Animal путем вызова метода конвертера:
@Service
public class AnimalFromToEmailConverter {
@Value("${mail.email}")
private String email;
// ....
public Animal animalFromEmail(Message message){
String animalName="";
try {
animalName = message.getContent().toString();
} catch (IOException | MessagingException e) {
e.printStackTrace();
}
return new Animal(animalName);
}
}
Далее идет Filter — просто для примера. Мы фильтруем сообщения и пропускаем только те, которые не начинается с «cat0.1»
Далее Animal идет в выходной адаптер в базу данных.
Рассмотрим подробнее адаптеры.
Imap Inbound Adapter
Вообще в SI есть два вида Inbound Channel Adapter:
- MessageProducers
- MessageSources
Первый тип (MessageProducer) сам генерирует сообщения, Poller (тот, кто регулярно проверяет) не требуется. К первому типу относятся относится JMS message-driven adapter, TCP inbound channel adapter, IMAP Idle adapter.
У нас Imap adapter, но не Idle. То есть у нас второй тип MessageSource. Нам Poller требуется, мы сами проверяем сообщения. Сообщения вытягиваются из почты раз в секунду по одному письму:
Pollers.fixedDelay(1000).maxMessagesPerPoll(1)
Jpa Outbound Adapter
В базу мы отправляем сущность Animal по одной штуке.
На этом круг закончен. Сообщения попадают из почты во входной адаптер, проходят череду конечных точек и идут через выходной адаптер в базу.
Вспомогательный поток с Smtp Outbound Adapter
Но, как уже говорилось, для заполнения почты сделан вспомогательный поток, отправляющий письма в наш ящик:
//вспомогательный поток - сгенерируем данные, отправим Animals в почтовый ящик.
//но можно вручную отправить письма с текстом вида cat0.9542215123310624
@Bean
public IntegrationFlow sendMailFlow() {
return IntegrationFlows.from("sendMailChannel")
.handle(Mail.outboundAdapter("smtp.mail.ru")
.port(25)
.credentials(this.email, this.password)
.javaMailProperties(p -> {
p.put("mail.debug", "true");
p.put("mail.smtp.ssl.trust", "*");
p.put("mail.smtp.starttls.enable", "true");
}),
e -> e.id("sendMailEndpoint"))
.get();
}
sendMailChannel — Smtp Outbound Adapter
Чтобы поток начал отправлять письма, надо отправить сообщения в канал sendMailChannel, именно из него поток выше берет сообщения.
Для отправки сообщений в канал сделан специальный сервис:
@Service
public class EmailEmitterService {
private final MessageChannel sendMailChannel;
private final AnimalFromToEmailConverter converter;
public EmailEmitterService(@Qualifier("sendMailChannel") MessageChannel sendMailChannel,
AnimalFromToEmailConverter converter){
this.sendMailChannel=sendMailChannel;
this.converter=converter;
}
public void sendEmails(int emailCount) {
for (int i = 0; i < emailCount; i++) {
Message message = MessageBuilder.withPayload(converter.createRandomAnimalEmail()).build();
sendMailChannel.send(message);
}
}
}
Сервис использует тот самый конвертер выше, только другой его метод — который генерирует почтовые сообщения типа SimpleMailMessage:
@Service
public class AnimalFromToEmailConverter {
@Value("${mail.email}")
private String email;
public SimpleMailMessage createRandomAnimalEmail() {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setSubject("New animal");
//генеририруем случайные строки вида cat0.9542215123310624, cat0.2964173089983424
mailMessage.setText("cat" + Math.random());
//сами себе высылаем, чтобы не заводить два ящика
mailMessage.setTo(this.email);
mailMessage.setFrom(this.email);
return mailMessage;
}
//...
}
И вызывается сервис из метода main() нашего приложения:
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringIntegrationApplication.class, args);
EmailEmitterService emitterService = (EmailEmitterService) context.getBean(EmailEmitterService.class);
emitterService.sendEmails(2);
}
Тут отправляется всего два письма, поскольку mail.ru довольно быстро начинает выдавать ошибку — превышение суточного лимита отправленных писем, и надо быть осторожнее.
Итоги
Исходный код примера доступен на GitHub.
а базейку для этого примера где можно подтянуть?
заменить на локальную
в applocation.yml
spring:
h2:
console:
enabled: true
datasource:
url: jdbc:h2:file:~/test
username: sa
password:
driverClassName: org.h2.Driver
и в pom.xml
com.h2database
h2
1.4.200