spring-mail-integration-demo example gas been added

This commit is contained in:
stvort
2019-10-11 22:19:47 +04:00
parent e849814b46
commit 304bcb24d3
25 changed files with 610 additions and 0 deletions
+1
View File
@@ -8,3 +8,4 @@
* *mongo-db-demo* - демонстрация подходов к хранению вложенных сущностенй в MongoDB, работы с MongoEventListener, агрегациями и инструментом миграций Mongock
* *docker-test-containers* - пример настройки TestContainers для монги
* *spring-cloud-demo-stvort* - пример работы двух микросевисов с использованием Config server, Eureka, Zuul, Feign client
* *spring-mail-integration-demo* - пример работы с SpringMail через SpringIntegration
@@ -0,0 +1,31 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
@@ -0,0 +1,17 @@
## Пример работы с SpringMail через SpringIntegration
В примере демонстрируется:
* *использование `@Scheduled` таймера для имитации пользовтельской активности*
* *обработка доменных сущностей средствами SpringIntegration (`@MessagingGateway`, `IntegrationFlow`) с использованием `route` и `subFlowMapping`*
* *создание и отправка email сообщений средствами SpringMail*
* *аггрегация данных с помощью SpringData/JPQL*
* *создание кастомного endpoint-а actuator-а для вывода статистики*
Описание примера:
* *по таймеру `UserActivityEmitterService` достает из БД случайный тип активности и пользователя*
* *после чего формирует из них объект активности (`UserActivity`) и отправляет в поток обработки с помощью `UserActivityGateway`. Активности,*
* *активности, у которых в названии типа есть вхождение "Вредн" помечаются, как важные соответствующим заголовком отправляемого сообщения*
* *в потоке обработки (`appUserActivityFlow`) объект активности сохраняется в БД, а важные сообщения преобразуются в письма и отправляются на почту администратору*
* *по таймеру `ActivityStatCalculationEmitterSerivce` инициирует подсчет статистики с помощью отправки сообщения в `activityStatFlow` через `ActivityStatGateway`*
* *в данном потоке происходит удаление старых статистических данных, а так же подсчет и сохранение в БД новых*
* *за вывод статистических данных отвечает кастомный endpoint actuator-а `ActivityStatEndpoint`*
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ru.otus.example</groupId>
<artifactId>spring-mail-integration-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-integration-mail-demo</name>
<description>Spring mail integration demo project</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,15 @@
package ru.otus.example.springmail_integration_demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class SpringMailIntegrationDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringMailIntegrationDemoApplication.class, args);
}
}
@@ -0,0 +1,23 @@
package ru.otus.example.springmail_integration_demo.actuator;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import ru.otus.example.springmail_integration_demo.models.ActivityStatElem;
import ru.otus.example.springmail_integration_demo.repositories.ActivityStatRepository;
import java.util.List;
@RequiredArgsConstructor
@Component
@Endpoint(id = "activity-stat")
public class ActivityStatEndpoint {
private final ActivityStatRepository activityStatRepository;
@ReadOperation
public List<ActivityStatElem> getAppUsersActivityStat() {
return activityStatRepository.findAll();
}
}
@@ -0,0 +1,13 @@
package ru.otus.example.springmail_integration_demo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties("app")
public class AppProps {
private String serverEmail;
private String adminEmail;
}
@@ -0,0 +1,12 @@
package ru.otus.example.springmail_integration_demo.integration;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.messaging.handler.annotation.Payload;
@MessagingGateway
public interface ActivityStatGateway {
@Gateway(requestChannel = "activityStatInChanel")
void calcActivityStat(@Payload String extInfo);
}
@@ -0,0 +1,92 @@
package ru.otus.example.springmail_integration_demo.integration;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlowDefinition;
import org.springframework.integration.dsl.MessageChannels;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.scheduling.PollerMetadata;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.messaging.Message;
import org.springframework.messaging.PollableChannel;
import ru.otus.example.springmail_integration_demo.repositories.ActivityRepository;
import ru.otus.example.springmail_integration_demo.repositories.ActivityStatRepository;
import ru.otus.example.springmail_integration_demo.services.UserActivityToEmailTransformer;
@Configuration
@IntegrationComponentScan
public class IntegrationConfig {
private static final int DEFAULT_QUEUE_CAPACITY = 100;
private static final int DEFAULT_POLLER_PERIOD = 1000;
private static final String IS_IMPORTANT_MESSAGE = "isImportant";
private static final String SAVE_METHOD_NAME = "save";
private static final String TRANSFORM_METHOD_NAME = "transform";
private static final String CALC_ACTIVITY_STAT_METHOD_NAME = "calcActivityStat";
private static final String SAVE_ALL_METHOD_NAME = "saveAll";
private static final String DELETE_ALL_METHOD_NAME = "deleteAll";
@Autowired
private ActivityRepository activityRepository;
@Autowired
private ActivityStatRepository activityStatRepository;
@Autowired
private UserActivityToEmailTransformer messageTransformer;
@Autowired
private JavaMailSender mailSender;
@Bean
public PollableChannel appUserActivityInChanel() {
return MessageChannels.queue("appUserActivityInChanel", DEFAULT_QUEUE_CAPACITY).get();
}
@Bean
public PollableChannel activityStatInChanel() {
return MessageChannels.queue("activityStatInChanel", DEFAULT_QUEUE_CAPACITY).get();
}
@Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata poller() {
return Pollers.fixedRate(DEFAULT_POLLER_PERIOD).get();
}
@Bean
public IntegrationFlow appUserActivityFlow() {
return f -> f.channel(appUserActivityInChanel())
.handle(activityRepository, SAVE_METHOD_NAME)
.route(Message.class, m -> m.getHeaders().get(IS_IMPORTANT_MESSAGE, Boolean.class)
, mapping -> mapping.subFlowMapping(true, sub -> sub
.transform(messageTransformer, TRANSFORM_METHOD_NAME)
.handle(m -> {
val isImportant = m.getHeaders().get(IS_IMPORTANT_MESSAGE, Boolean.class);
if (isImportant != null && isImportant) {
mailSender.send((SimpleMailMessage) m.getPayload());
}
})
)
.subFlowMapping(false, IntegrationFlowDefinition::nullChannel)
);
}
@Bean
public IntegrationFlow activityStatFlow() {
return f -> f.channel(activityStatInChanel())
.handle((m, h) -> {
activityStatRepository.deleteAll();
return true;
})
.handle(activityStatRepository, CALC_ACTIVITY_STAT_METHOD_NAME)
.handle(activityStatRepository, SAVE_ALL_METHOD_NAME)
.log();
}
}
@@ -0,0 +1,14 @@
package ru.otus.example.springmail_integration_demo.integration;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import ru.otus.example.springmail_integration_demo.models.UserActivity;
@MessagingGateway
public interface UserActivityGateway {
@Gateway(requestChannel = "appUserActivityInChanel")
void processActivity(@Payload UserActivity activity, @Header(name = "isImportant") boolean isImportant);
}
@@ -0,0 +1,38 @@
package ru.otus.example.springmail_integration_demo.models;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@NoArgsConstructor
@Entity
@Table(name = "activity_stat")
public class ActivityStatElem {
@JsonIgnore
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@JsonProperty("Имя пользователя")
@Column(name = "app_user_name")
private String appUserName;
@JsonProperty("Тип активности")
@Column(name = "activity_type")
private String activityType;
@JsonProperty("Количество")
@Column(name = "activities_count")
private long activitiesCount;
public ActivityStatElem(String appUserName, String activityType, long activitiesCount) {
this.appUserName = appUserName;
this.activityType = activityType;
this.activitiesCount = activitiesCount;
}
}
@@ -0,0 +1,22 @@
package ru.otus.example.springmail_integration_demo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "activity_types")
public class ActivityType {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "name")
private String name;
}
@@ -0,0 +1,25 @@
package ru.otus.example.springmail_integration_demo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "app_users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "email")
private String email;
@Column(name = "name")
private String name;
}
@@ -0,0 +1,38 @@
package ru.otus.example.springmail_integration_demo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "app_users_activity")
public class UserActivity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "activity_time")
private LocalDateTime activityTime;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "activity_type_id")
private ActivityType type;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "app_user_id")
private AppUser appUser;
public UserActivity(ActivityType type, AppUser appUser) {
this.id = id;
this.type = type;
this.appUser = appUser;
this.activityTime = LocalDateTime.now();
}
}
@@ -0,0 +1,9 @@
package ru.otus.example.springmail_integration_demo.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.springmail_integration_demo.models.UserActivity;
@Transactional
public interface ActivityRepository extends JpaRepository<UserActivity, Long> {
}
@@ -0,0 +1,20 @@
package ru.otus.example.springmail_integration_demo.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.springmail_integration_demo.models.ActivityStatElem;
import java.util.List;
@Transactional
public interface ActivityStatRepository extends JpaRepository<ActivityStatElem, Long> {
@Transactional(readOnly = true)
@Query("select new ru.otus.example.springmail_integration_demo.models.ActivityStatElem(u.name, t.name, count(a)) " +
"from UserActivity a left join a.appUser u left join a.type t " +
"group by u.name, t.name " +
"order by count(a) desc, u.name, t.name")
List<ActivityStatElem> calcActivityStat();
}
@@ -0,0 +1,7 @@
package ru.otus.example.springmail_integration_demo.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.otus.example.springmail_integration_demo.models.ActivityType;
public interface ActivityTypeRepository extends JpaRepository<ActivityType, Long> {
}
@@ -0,0 +1,9 @@
package ru.otus.example.springmail_integration_demo.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.springmail_integration_demo.models.AppUser;
@Transactional
public interface AppUserRepository extends JpaRepository<AppUser, Long> {
}
@@ -0,0 +1,23 @@
package ru.otus.example.springmail_integration_demo.services;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import ru.otus.example.springmail_integration_demo.integration.ActivityStatGateway;
import ru.otus.example.springmail_integration_demo.repositories.ActivityTypeRepository;
import ru.otus.example.springmail_integration_demo.repositories.AppUserRepository;
@RequiredArgsConstructor
@Service
public class ActivityStatCalculationEmitterSerivce {
private static final String EMPTY_EXT_INFO = "none";
private final ActivityTypeRepository activityTypeRepository;
private final AppUserRepository appUserRepository;
private final ActivityStatGateway activityStatGateway;
@Scheduled(initialDelay = 3000, fixedRate = 10000)
public void emitAppUserActivityStatCalculation(){
activityStatGateway.calcActivityStat(EMPTY_EXT_INFO);
}
}
@@ -0,0 +1,32 @@
package ru.otus.example.springmail_integration_demo.services;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import ru.otus.example.springmail_integration_demo.integration.UserActivityGateway;
import ru.otus.example.springmail_integration_demo.models.UserActivity;
import ru.otus.example.springmail_integration_demo.repositories.ActivityTypeRepository;
import ru.otus.example.springmail_integration_demo.repositories.AppUserRepository;
import java.util.Random;
@RequiredArgsConstructor
@Service
public class UserActivityEmitterService {
private final ActivityTypeRepository activityTypeRepository;
private final AppUserRepository appUserRepository;
private final UserActivityGateway userActivityGateway;
@Scheduled(initialDelay = 2000, fixedRate = 3000)
public void emitAppUserActivity(){
val random = new Random();
val activityTypes = activityTypeRepository.findAll();
val appUsers = appUserRepository.findAll();
val activityType = activityTypes.get(random.nextInt(activityTypes.size()));
val appUser = appUsers.get(random.nextInt(appUsers.size()));
val appUserActivity = new UserActivity(activityType, appUser);
userActivityGateway.processActivity(appUserActivity, activityType.getName().contains("Вредн"));
}
}
@@ -0,0 +1,8 @@
package ru.otus.example.springmail_integration_demo.services;
import org.springframework.mail.SimpleMailMessage;
import ru.otus.example.springmail_integration_demo.models.UserActivity;
public interface UserActivityToEmailTransformer {
SimpleMailMessage transform(UserActivity activity);
}
@@ -0,0 +1,25 @@
package ru.otus.example.springmail_integration_demo.services;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Service;
import ru.otus.example.springmail_integration_demo.config.AppProps;
import ru.otus.example.springmail_integration_demo.models.UserActivity;
@RequiredArgsConstructor
@Service
public class UserActivityToEmailTransformerImpl implements UserActivityToEmailTransformer {
private final AppProps appProps;
@Override
public SimpleMailMessage transform(UserActivity activity) {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(appProps.getAdminEmail());
mailMessage.setFrom(appProps.getServerEmail());
mailMessage.setSubject("Обнаружена вредная активность");
mailMessage.setText(String.format("Внимание!!! Обнаружена вредная активность! Время: %s, пользователь: %s, тип активности: %s",
activity.getActivityTime(), activity.getAppUser().getName(), activity.getType().getName()));
return mailMessage;
}
}
@@ -0,0 +1,34 @@
app:
admin-email: ${admin.email}
server-email: ${admin.email}
spring:
datasource:
initialization-mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
mail:
host: smtp.mail.ru
port: 465
username: ${email.server.user}
password: ${email.server.password}
protocol: smtps
properties:
mail:
smtp:
auth: true
starttls.enable: true
management:
endpoints:
web:
exposure:
include: health, info, activity-stat
endpoint:
health:
show-details: always
@@ -0,0 +1,16 @@
insert into app_users(name, email) values ('Рафаель Губерманович Тыгыдым', 'test@mail.ru')
insert into app_users(name, email) values ('Артем Демосфенович Шмяк', 'test@mail.ru')
insert into app_users(name, email) values ('Ифигения Бореславовна Фуфелшмерц', 'test@mail.ru')
insert into activity_types(name) values ('Очень полезное дело №4')
insert into activity_types(name) values ('Очень полезное дело №13')
insert into activity_types(name) values ('Очень полезное дело №34')
insert into activity_types(name) values ('Очень полезное дело №48')
insert into activity_types(name) values ('Очень полезное дело №53')
insert into activity_types(name) values ('Вредное дело №11')
insert into activity_types(name) values ('Вредное дело №12')
insert into activity_types(name) values ('Вредное дело №13')
insert into activity_types(name) values ('Вредное дело №14')
insert into activity_types(name) values ('Вредное дело №15')
insert into activity_types(name) values ('Вредное дело №16')
@@ -0,0 +1,8 @@
create table activity_types (id bigint auto_increment, name varchar(255), primary key (id))
create table app_users (id bigint auto_increment, email varchar(255), name varchar(255), primary key (id))
create table app_users_activity (id bigint auto_increment, activity_time timestamp, app_user_id bigint, activity_type_id bigint, primary key (id))
alter table app_users_activity add constraint app_users_activity_user_id_fk foreign key (app_user_id) references app_users
alter table app_users_activity add constraint app_users_activity_activity_type_id_fk foreign key (activity_type_id) references activity_types
create table activity_stat (id bigint auto_increment, app_user_name varchar(255), activity_type varchar(255), activities_count bigint, primary key (id))