بهار دسته 5: یک مورد استفاده ساده با تست ها

پیش نیازها: دانش اساسی در مورد جاوا ، دسته بهار ، بهار JPA.
می توانید کد مورد استفاده در این وبلاگ را در این مخزن GitHub پیدا کنید.
بهار دسته چارچوبی در اکوسیستم بهار است که توسعه برنامه های پردازش دسته ای را ساده می کند. پردازش دسته ای شامل رسیدگی به داده های زیادی به طور همزمان ، اغلب به صورت برنامه ریزی شده است.
در اینجا نکات اصلی به زبان ساده تر وجود دارد:
- آسانتر کردن کارها: این به توسعه دهندگان کمک می کند تا با ارائه ابزارها و مؤلفه های آماده استفاده ، با پیچیدگی پردازش دسته ای مقابله کنند.
- نگه داشتن چیزها: مدیریت معاملات ، اطمینان از اینکه داده ها سازگار و قابل اعتماد هستند ، حتی اگر چیزی در طول پردازش اشتباه باشد.
- رسیدگی به کارهای بزرگ: با اجازه دادن به انجام کارها به صورت موازی ، شکستن آنها به قطعات کوچکتر و پردازش یک تکه در یک زمان ، می تواند مقادیر زیادی از داده ها را به طور کارآمد انجام دهد.
- کار سازمان یافته: پردازش دسته ای به مشاغل تقسیم می شود که از مراحل تشکیل شده است. یک کار مانند کار کلی است و مراحل بخش های کوچکتر آن کار است.
- بخوانید ، کاری انجام دهید ، بنویسید: یک رویکرد ساده خواندن داده ها از یک منبع ، انجام برخی کارها روی آن و سپس نوشتن آن به یک مقصد. اینگونه است که داده ها را پردازش می کند.
- برخورد با مشکلات: راه هایی برای مقابله با موضوعاتی مانند اقدامات آزمایشی برای عملیات شکست خورده و پرش از سوابق که نمی توانند به درستی پردازش شوند ، فراهم می کند.
- اقدامات سفارشی: به توسعه دهندگان این امکان را می دهد تا کد خود را در نقاط مختلف پردازش دسته ای مانند قبل یا بعد از یک مرحله یا کار ، سفارشی و اضافه کنند.
از نظر عملی ، دسته بندی بهار معمولاً هنگامی استفاده می شود که داده های زیادی برای پردازش منظم داشته باشید ، مانند هنگام جابجایی داده ها بین سیستم ها ، تبدیل آن یا انجام کارهای دوره ای مانند تولید گزارش. این باعث می شود کل فرآیند قابل کنترل تر و مستعد خطا باشد.
چالش برانگیزترین بخش در دسته بهار نحوه نوشتن تست های واحد و ادغام است. به طور ذاتی از پیچیدگی پیکربندی استفاده می شود.
بیایید نمونه ای از دسته ای را انجام دهیم که موارد زیر را انجام می دهد:
پوشه ای را برای دریافت پرونده های جدید تماشا کنید
داده ها را از یک فایل مسطح هنگام اضافه شدن به پوشه بخوانید
پردازش داده ها
داده ها را در یک پایگاه داده ذخیره کنید
برای این مثال ما از Course Batch 5 ، Spring Boot 3 ، Java 17 ، Mapstruct برای نقشه برداری شیء و Maven به عنوان یک ابزار ساخت استفاده خواهیم کرد.
در اینجا چگونه pom.xml ما به نظر می رسد:
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.2.2
dev.sabri
FolderMonitor
0.0.1-SNAPSHOT
FolderMonitor
FolderMonitor
17
1.5.5.Final
org.springframework.boot
spring-boot-starter-batch
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
com.h2database
h2
runtime
org.projectlombok
lombok
true
org.mapstruct
mapstruct
${map.struct.version}
org.mapstruct
mapstruct-processor
${map.struct.version}
org.springframework.boot
spring-boot-starter-test
test
org.springframework.batch
spring-batch-test
test
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
حال بیایید شی موجودیت خود را ایجاد کنیم:
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Visitors {
@Id
private Long id;
private String firstName;
private String lastName;
private String emailAddress;
private String phoneNumber;
private String address;
private String visitDate;
}
ما به یک شی DTO احتیاج داریم که بعداً به موجودیت ما نقشه برداری می شود. ما از یک رکورد استفاده خواهیم کرد:
@Builder
public record VisitorsDto(
Long id,
String firstName,
String lastName,
String emailAddress,
String phoneNumber,
String address,
String visitDate) {
}
رابط MapStruct به این شکل خواهد بود:
@Mapper(componentModel = "spring")
public interface VisitorsMapper {
Visitors toVisitors(VisitorsDto visitorsDto);
}
مرحله بعدی ایجاد یک jParePository برای دسترسی به بانک اطلاعاتی خواهد بود ، این یک jparepository ساده و بدون پرس و جوهای سفارشی است:
public interface VisitorsRepository extends JpaRepository {
}
یک کار دسته ای بهار استاندارد ترکیبی از یک یا چند مرحله است.
هر مرحله به خودی خود می تواند حاوی یک وظیفه یا یک مورد Reader ، AttemProcessor و یک آیتم نویسنده باشد.
بیایید با تعریف AtmiteReader خود شروع کنیم ، هدف اصلی در اینجا خواندن داده ها از پرونده مسطح ما است:
@Bean
@StepScope
public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['inputFile']}") final String visitorsFile) throws IOException {
val flatFileItemReader = new FlatFileItemReader();
flatFileItemReader.setName("VISITORS_READER");
flatFileItemReader.setLinesToSkip(1);
flatFileItemReader.setLineMapper(linMapper());
flatFileItemReader.setStrict(false);
flatFileItemReader.setResource(new FileSystemResource(visitorsFile));
return flatFileItemReader;
}
در صورت نقشه برداری از داده ها و زمینه های پرونده ما به شیء جاوا ، بخش مهمی در فرآیند خواندن داده ها در صورت نقشه برداری داده ها.
در این حالت ما از یک FieldsetMapper سفارشی استفاده خواهیم کرد:
public class VisitorsFieldSetMapper implements FieldSetMapper {
@Override
public VisitorsDto mapFieldSet(final FieldSet fieldSet) throws BindException {
return VisitorsDto.builder()
.id(fieldSet.readLong("id"))
.firstName(fieldSet.readString("firstName"))
.lastName(fieldSet.readString("lastName"))
.emailAddress(fieldSet.readString("emailAddress"))
.phoneNumber(fieldSet.readString("phoneNumber"))
.address(fieldSet.readString("address"))
.visitDate(fieldSet.readString("visitDate"))
.build();
}
}
پس از خواندن داده های پرونده ، بسته به نیاز ما باید پردازش شود. در مورد استفاده من ، پردازنده یک شیء بازدید کننده را به بازدید کنندگان نهاد ما نقشه می کند:
@Component
public class VisitorsItemProcessor implements ItemProcessor {
private final VisitorsMapper visitorsMapper;
public VisitorsItemProcessor(VisitorsMapper visitorsMapper) {
this.visitorsMapper = visitorsMapper;
}
@Override
public Visitors process(final VisitorsDto visitorsDto) throws Exception {
return visitorsMapper.toVisitors(visitorsDto);
}
}
پس از نقشه برداری از شیء ما ، اکنون وقت آن رسیده است که داده ها را در پایگاه داده H2 ما با استفاده از موارد نویسنده به شرح زیر وارد کنیم:
@Component
public class VisitorsItemWriter implements ItemWriter {
private final VisitorsRepository visitorsRepository;
public VisitorsItemWriter(VisitorsRepository visitorsRepository) {
this.visitorsRepository = visitorsRepository;
}
@Override
public void write(Chunk extends Visitors> chunk) throws Exception {
visitorsRepository.saveAll(chunk.getItems());
}
}
در اینجا نحوه پیکربندی کامل کار ما به نظر می رسد:
@Configuration
public class VisitorsBatchConfig {
@Bean
public Job importVistorsJob(final JobRepository jobRepository, final PlatformTransactionManager transactionManager, final VisitorsRepository visitorsRepository, final VisitorsMapper visitorsMapper) throws IOException {
return new JobBuilder("importVisitorsJob", jobRepository)
.start(importVisitorsStep(jobRepository, transactionManager, visitorsRepository, visitorsMapper))
.build();
}
@Bean
public Step importVisitorsStep(final JobRepository jobRepository, final PlatformTransactionManager transactionManager, final VisitorsRepository visitorsRepository, final VisitorsMapper visitorsMapper) throws IOException {
return new StepBuilder("importVisitorsStep", jobRepository)
.chunk(100, transactionManager)
.reader(flatFileItemReader(null))
.processor(itemProcessor(visitorsMapper))
.writer(visitorsItemWriter(visitorsRepository))
.build();
}
@Bean
public ItemProcessor itemProcessor(final VisitorsMapper visitorsMapper) {
return new VisitorsItemProcessor(visitorsMapper);
}
@Bean
public VisitorsItemWriter visitorsItemWriter(final VisitorsRepository visitorsRepository) {
return new VisitorsItemWriter(visitorsRepository);
}
@Bean
@StepScope
public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['inputFile']}") final String visitorsFile) throws IOException {
val flatFileItemReader = new FlatFileItemReader();
flatFileItemReader.setName("VISITORS_READER");
flatFileItemReader.setLinesToSkip(1);
flatFileItemReader.setLineMapper(linMapper());
flatFileItemReader.setStrict(false);
flatFileItemReader.setResource(new FileSystemResource(visitorsFile));
return flatFileItemReader;
}
@Bean
public LineMapper linMapper() {
val defaultLineMapper = new DefaultLineMapper();
val lineTokenizer = new DelimitedLineTokenizer();
lineTokenizer.setDelimiter(",");
lineTokenizer.setNames("id", "firstName", "lastName", "emailAddress", "phoneNumber", "address", "visitDate");
lineTokenizer.setStrict(false); // Set strict property to false
defaultLineMapper.setLineTokenizer(lineTokenizer);
defaultLineMapper.setFieldSetMapper(new VisitorsFieldSetMapper());
return defaultLineMapper;
}
}
برای بسته بندی دسته خود ، ما باید یک تماشاگر سرویس را تنظیم کنیم تا به دنبال هر پرونده جدیدی که وارد پوشه ما می شود ، جستجو کنیم.
این مرحله در پروژه اصلی بوت بهار ما پیکربندی شده است:
@SpringBootApplication
@EnableBatchProcessing
@EnableScheduling
@Slf4j
public class FolderMonitorApplication {
private final JobLauncher jobLauncher;
private final Job job;
public FolderMonitorApplication(JobLauncher jobLauncher, Job job) {
this.jobLauncher = jobLauncher;
this.job = job;
}
public static void main(String[] args) {
new SpringApplicationBuilder(FolderMonitorApplication.class)
.web(WebApplicationType.SERVLET)
.run(args)
.registerShutdownHook();
}
public void run(final String inputFile) throws Exception {
val jobParameters = new JobParametersBuilder()
.addDate("timestamp", Calendar.getInstance().getTime())
.addString("inputFile", inputFile)
.toJobParameters();
val jobExecution = jobLauncher.run(job, jobParameters);
while (jobExecution.isRunning()) {
log.info("..................");
}
}
@Scheduled(fixedRate = 2000)
public void runJob() {
val path = Paths.get("/home/sabri/Work");
WatchKey key;
WatchService watchService = null;
try {
watchService = FileSystems.getDefault().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
while ((key = watchService.take()) != null) {
for (WatchEvent> event : key.pollEvents()) {
log.info(
"Event kind:" + event.kind()
+ ". File affected: " + event.context() + ".");
if (event.kind().name().equals("ENTRY_CREATE")) {
run(path + "https://dev.to/" + event.context().toString());
}
}
key.reset();
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
اکنون که دسته ما آماده است بیایید آن را شروع کنیم و پرونده ای را درون پوشه خود قرار دهیم
/خانه/سابری/کار ، در اینجا نحوه ورود سیاههها آمده است:
2024-02-05T22: 58: 34.100+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر تکمیل شد: [{‘inputFile’:'{value=/home/sabri/Work/visitors.csv, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 22:58:34 CET 2024, type=class java.util.Date, identifying=true}’}] و وضعیت زیر: [COMPLETED] در 75ms
2024-02-05T22: 58: 53.023+01: 00 اطلاعات 6931 — [ scheduling-1] dsffoldermonitorapplication: نوع رویداد: enter_create. پرونده تحت تأثیر:. ~ lock.visitors.csv#.
2024-02-05T22: 58: 53.029+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر راه اندازی شد: [{‘inputFile’:'{value=/home/sabri/Work/.~lock.visitors.csv#, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 22:58:53 CET 2024, type=class java.util.Date, identifying=true}’}]2024-02-05T22: 58: 53.031+01: 00 اطلاعات 6931 — [ scheduling-1] osbatch.core.job.simplestephandler: مرحله اجرا: [importVisitorsStep]2024-02-05T22: 58: 53.034+01: 00 اطلاعات 6931 — [ scheduling-1] OSBATCH.CORE.STEP.ABSTRACTERSTEP: مرحله: [importVisitorsStep] در 2MS اجرا شد
2024-02-05T22: 58: 53.035+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر تکمیل شد: [{‘inputFile’:'{value=/home/sabri/Work/.~lock.visitors.csv#, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 22:58:53 CET 2024, type=class java.util.Date, identifying=true}’}] و وضعیت زیر: [COMPLETED] در 5MS
2024-02-05T23: 00: 32.457+01: 00 اطلاعات 6931 — [ scheduling-1] dsffoldermonitorapplication: نوع رویداد: enter_create. پرونده تحت تأثیر: بازدید کنندگان 2.csv.
2024-02-05T23: 00: 32.464+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر راه اندازی شد: [{‘inputFile’:'{value=/home/sabri/Work/Visitors2.csv, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 23:00:32 CET 2024, type=class java.util.Date, identifying=true}’}]2024-02-05T23: 00: 32.468+01: 00 اطلاعات 6931 — [ scheduling-1] osbatch.core.job.simplestephandler: مرحله اجرا: [importVisitorsStep]بازدید کننده[id=1, firstName=John, lastName=Doe, emailAddress=john.doe@example.com, phoneNumber=(555) 123-4567, address=123 Main St, visitDate=2024-02-05]بازدید کننده[id=2, firstName=Jane, lastName=Smith, emailAddress=jane.smith@example.com, phoneNumber=(555) 987-6543, address=456 Oak St, visitDate=2024-02-06]بازدید کننده[id=3, firstName=Michael, lastName=Johnson, emailAddress=michael.j@example.com, phoneNumber=(555) 555-5555, address=789 Pine St, visitDate=2024-02-07]بازدید کننده[id=4, firstName=Alice, lastName=Williams, emailAddress=alice.w@example.com, phoneNumber=(555) 456-7890, address=101 Elm St, visitDate=2024-02-08]بازدید کننده[id=5, firstName=David, lastName=Miller, emailAddress=david.m@example.com, phoneNumber=(555) 111-2222, address=202 Birch St, visitDate=2024-02-09]
اکنون که کار ما خوب است ، قسمت اول انجام می شود ، بیایید حرکت کنیم و یک تست برای این دسته بنویسیم.
همانطور که قبل از این که بیشترین قسمت تنظیم شده است ، پیکربندی است ، اینگونه به نظر می رسد:
@Configuration
@EnableBatchProcessing
@EnableJpaRepositories(basePackages = {"dev.sabri.foldermonitor.repositories"})
@EntityScan(basePackages = {"dev.sabri.foldermonitor.domain"})
@ComponentScan(basePackages = {"dev.sabri.foldermonitor.mapper"})
public class VisitorsBatchTestConfig {
@Bean
@Primary
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("/org/springframework/batch/core/schema-h2.sql")
.build();
}
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean("entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
val localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
localContainerEntityManagerFactoryBean.setDataSource(dataSource());
localContainerEntityManagerFactoryBean.setPackagesToScan("dev.sabri.foldermonitor.domain");
localContainerEntityManagerFactoryBean.setPersistenceUnitName("visitors");
val hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
localContainerEntityManagerFactoryBean.setJpaVendorAdapter(hibernateJpaVendorAdapter);
return localContainerEntityManagerFactoryBean;
}
@Bean
public JobRepository jobRepository() throws Exception {
val jobrepositoryFactoryBean = new JobRepositoryFactoryBean();
jobrepositoryFactoryBean.setDataSource(dataSource());
jobrepositoryFactoryBean.setTransactionManager(transactionManager());
jobrepositoryFactoryBean.afterPropertiesSet();
return jobrepositoryFactoryBean.getObject();
}
}
EnableJparePositories برای گفتن دسته من از کجا می توانم همه مخازن من را پیدا کنم
EntityScan برای شناسایی نهادهای من استفاده می شود
componentscan عمدتاً در اینجا برای نقشه برداری من استفاده می شود تا لوبیا با آزمایش من در زمان اجرا تشخیص داده شود
حال بیایید یک کلاس آزمون ساده بنویسیم:
@SpringBatchTest
@SpringJUnitConfig(classes = {VisitorsBatchConfig.class, VisitorsBatchTestConfig.class})
class VisitorsBatchIntegrationTests {
public static final String INPUt_FILE = "visitors.csv";
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
@Autowired
private JobRepositoryTestUtils jobRepositoryTestUtils;
@AfterEach
public void cleanUp() {
jobRepositoryTestUtils.removeJobExecutions();
}
private JobParameters defaultJobParameters() {
val paramsBuilder = new JobParametersBuilder();
paramsBuilder.addString("inputFile", INPUt_FILE);
paramsBuilder.addDate("timestamp", Calendar.getInstance().getTime());
return paramsBuilder.toJobParameters();
}
@Test
void givenVisitorsFlatFile_whenJobExecuted_thenSuccess(@Autowired Job job) throws Exception {
// given
this.jobLauncherTestUtils.setJob(job);
// when
val jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
val actualJobInstance = jobExecution.getJobInstance();
val actualJobExitStatus = jobExecution.getExitStatus();
// then
assertThat(actualJobInstance.getJobName()).isEqualTo("importVisitorsJob");
assertThat(actualJobExitStatus.getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode());
}
}
همانطور که در صفحه بعدی خواهید دید ، تست خوب است.
نتایج آزمون
در اینجا ساده از داده های مورد استفاده در این نمونه بهار دسته ای آورده شده است:
VISITOR_ID ، FIRST_NAME ، LAST_NAME ، email_address ، phone_number ، آدرس ، بازدید_دیت
1 ، جان ، doe ، john.doe@sexal.com ، (555) 123-4567،123 خیابان اصلی ، 2024-02-05
2 ، جین ، اسمیت ، jane.smith@به عنوان مثال ..com ، (555) 987-6543،456 خیابان بلوط ، 2024-02-06
3 ، مایکل ، جانسون ، michael.j@مثال ..com ، (555) 555-5555،789 خیابان کاج ، 2024-02-07
4 ، آلیس ، ویلیامز ، alice.w@مثال ..com ، (555) 456-7890،101 خیابان Elm ، 2024-02-08
5 ، دیوید ، میلر ، david.m@مثال ..com ، (555) 111-2222،202 خیابان بیرچ ، 2024-02-09
6 ، سوزان ، جونز ، Susan.J@به عنوان مثال ..com ، (555) 333-4444،303 خیابان سرو ، 2024-02-10
7 ، رابرت ، اسمیت ، Robert.s@به عنوان مثال ..com ، (555) 555-1234،505 خیابان Maple ، 2024-02-11
8 ، امیلی ، دیویس ، emily.d@مثال ..com ، (555) 876-5432،707 خیابان بلوط ، 2024-02-12
9 ، کریستوفر ، کلارک ، Chris.c@به عنوان مثال ..com ، (555) 234-5678،909 خیابان کاج ، 2024-02-13
10 ، Emma ، Johnson ، Emma.j@Exactine.com ، (555) 432-1098،111 Walnut St ، 2024-02-14
11 ، ویلیام ، مارتین ، William.m@به عنوان مثال ..com ، (555) 567-8901،222 خیابان بیرچ ، 2024-02-15
12 ، الیویا ، اندرسون ، olivia.a@مثال ..com ، (555) 789-0123،333 Elm ST ، 2024-02-16
13 ، میسون ، وایت ، mason.w@به عنوان مثال ..com ، (555) 321-0987،444 خیابان بلوط ، 2024-02-17
14 ، سوفیا ، تیلور ، sophia.t@مثال ..com ، (555) 654-3210،555 خیابان افرا ، 2024-02-18
15 ، Aiden ، Brown ، Aiden.b@مثال ..com ، (555) 876-5432،666 Pine ST ، 2024-02-19
و سرانجام ، این کار انجام شده است.
ما یک دسته بهاری عملیاتی کامل با تست هایی داریم که پیکربندی شده و خوب اجرا می شوند.
وقت خود را برای گذراندن مقاله من بگذرانید. نظرات و نظرات شما برای من ارزشمند است ، زیرا آنها به تقویت کار من کمک می کنند ، و من واقعاً برای هر یک از آنها ارزش قائل هستم.