Рубрики
Без рубрики

Простая обработка длительных заданий с помощью Job Run

В этом уроке мы будем работать на вымышленную компанию Acme Corp, и нам нужно создать… Помеченный как java, распределенные системы.

Легко обрабатывать длительные задания с помощью Job Run В этом руководстве мы будем работать в вымышленной компании Acme Corp, и нам нужно сгенерировать квитанции о заработной плате для всех сотрудников Acme Corp.

Легко обрабатывать длительные задания с помощью Job Run В этом руководстве мы будем работать в вымышленной компании Acme Corp, и нам нужно сгенерировать квитанции о заработной плате для всех сотрудников Acme Corp. TLDR; вы можете найти полный проект в нашем репозитории Github: https://github.com/jobrunr/example-salary-slip

Легко обрабатывать длительные задания с помощью Job Run В этом руководстве мы будем работать в вымышленной компании Acme Corp, и нам нужно сгенерировать квитанции о заработной плате для всех сотрудников Acme Corp. TLDR; вы можете найти полный проект в нашем репозитории Github: || https://github .Технический директор для этого мы будем использовать 3 компонента с открытым исходным кодом: om/jobrunr/example-salary-slip

Во время этого урока мы сгенерируем еженедельные квитанции о заработной плате всех сотрудников Acme Corp и отправим их им по электронной почте. Как? Что ж, около

  • создание повторяющегося задания с помощью Job Run, которое будет выполняться каждую неделю – все сотрудники Acme Corp будут использовать Spring Data Jpa, и для каждого из этих сотрудников запланируйте новое фоновое задание для создания квитанция о заработной плате
  • каждое из этих фоновых заданий приведет к получению сотрудника и
    • потреблять Служба временных часов , которая показывает количество часов, отработанных сотрудником за данную неделю.
    • создайте документ квитанции о заработной плате с помощью Службы генерации документов , который будет содержать создайте документ квитанции о заработной плате с помощью
    • Службы генерации документов , который будет содержать отправьте электронное письмо сотруднику с его квитанцией о заработной плате, используя

В этом руководстве мы опускаем весь импорт Java для краткости – чтобы найти их, просто посетите пример проекта на https://github.com/jobrunr/example-salary-slip

Для создания этой службы расчета заработной платы мы используем gradle и наш файл build.gradle

plugins {
    id 'java'
    id 'idea'
    id 'org.springframework.boot' version '2.2.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
}

group 'org.paycheck'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
    jcenter()
    mavenLocal()
}

configurations.all {
    exclude group: 'org.slf4j', module: 'slf4j-log4j12'
}

dependencies {
    implementation 'org.jobrunr:jobrunr:0.9.2'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-mail'
    implementation 'com.fasterxml.jackson.core:jackson-databind'
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
    implementation 'org.wickedsource.docx-stamper:docx-stamper:1.4.0'
    implementation 'org.docx4j:docx4j-core:8.1.6'
    implementation 'org.docx4j:docx4j-JAXB-ReferenceImpl:11.1.3'
    implementation 'org.docx4j:docx4j-export-fo:8.1.6'


    implementation 'com.github.javafaker:javafaker:1.0.2'
    implementation 'com.h2database:h2'

    testImplementation 'org.junit.jupiter:junit-jupiter:5.6.1'
    testImplementation 'org.awaitility:awaitility:4.0.2'
}

test {
    useJUnitPlatform()
}

Организация-сотрудник

Поскольку нам нужно создать квитанции о заработной плате для всех сотрудников, давайте начнем с класса Employee – это простая сущность с некоторыми полями, такими как Имя , Фамилия и электронная почта .

package org.jobrunr.example.employee;

@Entity
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String firstName;
    private String lastName;
    private String email;

    protected Employee() {
    }

    public Employee(String firstName, String lastName, String email) {
        this(null, firstName, lastName, email);
    }

    public Employee(Long id, String firstName, String lastName, String email) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    @Override
    public String toString() {
        return String.format(
                "Employee[id=%d, firstName='%s', lastName='%s']",
                id, firstName, lastName);
    }

    public Long getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public String getEmail() {
        return email;
    }
}

Должность сотрудника

EmployeeRepository расширяет данные Spring CrudRepository и добавляет дополнительный метод для извлечения всех i двойки сотрудников

package org.jobrunr.example.employee;

public interface EmployeeRepository extends CrudRepository {

    @Query("select e.id from Employee e")
    Stream getAllEmployeeIds();

}

Класс домена рабочей недели

Поскольку квитанция о заработной плате генерируется один раз в неделю, нам нужен класс, представляющий количество времени, которое сотрудник проработал на этой неделе, – Рабочая неделя класс. В нем есть несколько дополнительных полей, таких как выходные и дата с и по, которые мы будем использовать для нашего сгенерированного документа о заработной плате.

package org.jobrunr.example.timeclock;

public class WorkWeek {

    private final int weekNbr;
    private final BigDecimal workHoursMonday;
    private final BigDecimal workHoursTuesday;
    private final BigDecimal workHoursWednesday;
    private final BigDecimal workHoursThursday;
    private final BigDecimal workHoursFriday;
    private final LocalDate from;
    private final LocalDate to;

    public WorkWeek(BigDecimal workHoursMonday, BigDecimal workHoursTuesday, BigDecimal workHoursWednesday, BigDecimal workHoursThursday, BigDecimal workHoursFriday) {
        this.workHoursMonday = workHoursMonday;
        this.workHoursTuesday = workHoursTuesday;
        this.workHoursWednesday = workHoursWednesday;
        this.workHoursThursday = workHoursThursday;
        this.workHoursFriday = workHoursFriday;
        WeekFields weekFields = WeekFields.of(Locale.getDefault());
        weekNbr = now().get(weekFields.weekOfWeekBasedYear());
        this.from = now().with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
        this.to = now().with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
    }

    public BigDecimal getWorkHoursMonday() {
        return workHoursMonday;
    }

    public BigDecimal getWorkHoursTuesday() {
        return workHoursTuesday;
    }

    public BigDecimal getWorkHoursWednesday() {
        return workHoursWednesday;
    }

    public BigDecimal getWorkHoursThursday() {
        return workHoursThursday;
    }

    public BigDecimal getWorkHoursFriday() {
        return workHoursFriday;
    }

    public int getWeekNbr() {
        return weekNbr;
    }

    public LocalDate getFrom() {
        return from;
    }

    public LocalDate getTo() {
        return to;
    }

    public BigDecimal getTotal() {
        return workHoursMonday
                .add(workHoursTuesday)
                .add(workHoursWednesday)
                .add(workHoursThursday)
                .add(workHoursFriday);
    }
}

Служба часов Времени

Чтобы получить Рабочую неделю класс для определенного сотрудника, мы создаем Служба временных часов , которая является компонентом Spring. Служба временных часов

package org.jobrunr.example.timeclock;

@Component
public class TimeClockService {

    public WorkWeek getWorkWeekForEmployee(Long employeeId) {
        try {
            //simulate a long-during call
            Thread.sleep(ThreadLocalRandom.current().nextInt(3, 5 + 1) * 1000);
            return new WorkWeek(
                    BigDecimal.valueOf(getRandomHours()),
                    BigDecimal.valueOf(getRandomHours()),
                    BigDecimal.valueOf(getRandomHours()),
                    BigDecimal.valueOf(getRandomHours()),
                    BigDecimal.valueOf(getRandomHours())
            );
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }

    private int getRandomHours() {
        Random r = new Random();
        return r.nextInt((8 - 5) + 1) + 5;
    }
}

Квитанция о зарплате

Теперь у нас есть все необходимые данные для создания вашей квитанции о заработной плате – за исключением самого класса Квитанция о заработной плате :

package org.jobrunr.example.paycheck;

public class SalarySlip {

    private final Employee employee;
    private final WorkWeek workWeek;

    public SalarySlip(Employee employee, WorkWeek workWeek) {
        this.employee = employee;
        this.workWeek = workWeek;
    }

    public Employee getEmployee() {
        return employee;
    }

    public WorkWeek getWorkWeek() {
        return workWeek;
    }

    public BigDecimal getTotal() {
        BigDecimal totalPerHour = getTotalPerHour();
        BigDecimal amountOfWorkedHours = getAmountOfWorkedHours();
        return totalPerHour.multiply(amountOfWorkedHours).setScale(2, RoundingMode.HALF_UP);
    }

    private BigDecimal getAmountOfWorkedHours() {
        return workWeek.getTotal();
    }

    private BigDecimal getTotalPerHour() {
        return BigDecimal.valueOf(21.50);
    }
}

Класс Квитанция о заработной плате содержит все данные, необходимые для создания квитанции о заработной плате, расчета общей суммы и будет использоваться DocumentGenerationService для создания квитанций о заработной плате в виде документов PDF.

Служба Создания документов

Служба Генерации документов также является компонентом Spring и отвечает за создание фактических документов о заработной плате на основе шаблона word. Шаблон word содержит много заполнителей, таких как ${employee.Имя}, ${сотрудник.Фамилия} и ${Рабочая неделя.часы работы в понедельник.setScale(2)}, который будет заменен на DocxStamper с использованием данного объекта контекста – в нашем случае объекта SalarySlip. Наконец, документ Word со всеми заполненными полями преобразуется в

package org.jobrunr.example.paycheck;

@Component
public class DocumentGenerationService {

    public void generateDocument(Path wordTemplatePath, Path wordOutputPath, Object context) throws IOException, Docx4JException {
        Files.createDirectories(wordOutputPath.getParent().toAbsolutePath());

        try(InputStream template = Files.newInputStream(wordTemplatePath); OutputStream out = Files.newOutputStream(wordOutputPath)) {
            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            final DocxStamper stamper = new DocxStamperConfiguration().setFailOnUnresolvedExpression(true).build();
            stamper.stamp(template, context, byteArrayOutputStream);

            Docx4J.toPDF(WordprocessingMLPackage.load(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())), out);
        }
    }
}

Наконец, документ Word со всеми заполненными полями преобразуется в

Служба электронной почты снова является компонентом Spring и несет ответственность за отправку по электронной почте окончательного документа word о заработной плате сотруднику - она использует электронную почту Spring Boot Starter и использует MimeMessage , созданный JavaMailSender . В нем есть метод с именем отправить квитанцию о заработной плате с двумя аргументами - классом Employee и путем к PDF-документу квитанции о заработной плате для этого сотрудника. Используя эти аргументы, мы можем отправить как персонализированное текстовое электронное письмо, так и прикрепить фактическую квитанцию о заработной плате в качестве вложения. JavaMailSender - это класс, предоставляемый Spring Boot Starter Mail и настроенный с помощью файла свойств. Он настраивается с помощью файла свойств, который вы можете найти здесь

package org.jobrunr.example.email;

@Component
public class EmailService {

    public JavaMailSender emailSender;

    public EmailService(JavaMailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void sendSalarySlip(Employee employee, Path salarySlipPath) throws MessagingException {
        MimeMessage message = emailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setTo(employee.getEmail());
        helper.setSubject("Your weekly salary slip");
        helper.setText(String.format("Dear %s,\n\nhere you can find your weekly salary slip. \n \nThanks again for your hard work,\nAcme corp", employee.getFirstName()));

        FileSystemResource file = new FileSystemResource(salarySlipPath);
        helper.addAttachment("Salary Slip", file);
        emailSender.send(message);
    }
}

И, наконец,, служба квитанций о заработной плате

Служба квитанций о заработной плате – это последний шаг головоломки, который связывает все воедино:

  • Служба квитанций о заработной плате – это последний шаг головоломки, который связывает все воедино:
  • он использует другие компоненты, которые мы уже создали:
    • EmployeeRepository для получения всех сотрудников из базы данных
    • служба Time Clock служба Time Clock сотрудник работал
    • служба Создания документов для создания квитанции о заработной плате из заданного шаблона Word
    • служба электронной почты для отправки персонального электронного письма с квитанцией о заработной плате в качестве приложения сотруднику

Это также два важных общедоступных метода:

Генерировать и отправлять повторяющийся слайд

Генерировать и отправлять повторяющийся слайд генерировать И отправлять квитанции о заработной плате использует идентификатор сотрудника для получения фактических данных о сотруднике, генерирует Это будет фоновое задание для выполнения задания, и оно вызывается из метода генерировать и отправлять Квитанции о заработной плате Всем Сотрудникам . Это будет фоновое задание для выполнения задания, и оно вызывается из метода генерировать и отправлять квитанции о заработной плате Всем сотрудникам. Мы аннотируем его аннотацией задания (это необязательно), чтобы иметь значимые имена в панели инструментов JobRunr.

    @Job(name = "Generate and send salary slip to employee %0")
    public void generateAndSendSalarySlip(Long employeeId) throws Exception {
        final Employee employee = getEmployee(employeeId);
        Path salarySlipPath = generateSalarySlip(employee);
        emailService.sendSalarySlip(employee, salarySlipPath);
    }

Это будет фоновое задание для выполнения задания, и оно вызывается из метода || генерировать и отправлять квитанции о заработной плате Всем сотрудникам. Мы аннотируем его аннотацией задания (это необязательно), чтобы иметь значимые имена в панели инструментов JobRunr. Генерировать и отправлять вознаграждение всем сотрудникам

Это будет фоновое задание для выполнения задания, и оно вызывается из метода генерировать и отправлять квитанции о заработной плате Всем сотрудникам. Мы аннотируем его аннотацией задания (это необязательно), чтобы иметь значимые имена в панели инструментов JobRunr. generateAndSendSalarySlipToAllEmployees Наш основной метод, который будет запланирован каждую неделю - он получает поток идентификаторов сотрудников и, используя BackgroundJob.enqueue метод JobRunr, мы создаем фоновое задание Это будет фоновое задание для выполнения задания, и оно вызывается из метода генерировать и отправлять квитанции о заработной плате Всем сотрудникам. Мы аннотируем его аннотацией задания (это необязательно), чтобы иметь значимые имена в панели инструментов JobRunr. Generateandsendsalaryslip для всех сотрудников Наш основной метод, который будет планироваться каждую неделю – он получает поток идентификаторов сотрудников и использует метод BackgroundJob.generateAndSendSalarySlip для каждого сотрудника. ставим в очередь

    @Transactional(readOnly = true)
    @Job(name = "Generate and send salary slip to all employees")
    public void generateAndSendSalarySlipToAllEmployees() {
        final Stream allEmployees = employeeRepository.getAllEmployeeIds();
        BackgroundJob.enqueue(allEmployees, (salarySlipService, employeeId) -> salarySlipService.generateAndSendSalarySlip(employeeId));
    }

Полное Обслуживание квитанций о заработной плате выглядит следующим образом:

package org.jobrunr.example.paycheck;

@Component
public class SalarySlipService {

    private static final Path salarySlipTemplatePath = Path.of("src/main/resources/templates/salary-slip-template.docx");

    private final EmployeeRepository employeeRepository;
    private final TimeClockService timeClockService;
    private final DocumentGenerationService documentGenerationService;
    private final EmailService emailService;

    public SalarySlipService(EmployeeRepository employeeRepository, TimeClockService timeClockService, DocumentGenerationService documentGenerationService, EmailService emailService) {
        this.employeeRepository = employeeRepository;
        this.timeClockService = timeClockService;
        this.documentGenerationService = documentGenerationService;
        this.emailService = emailService;
    }

    @Transactional(readOnly = true)
    @Job(name = "Generate and send salary slip to all employees")
    public void generateAndSendSalarySlipToAllEmployees() {
        final Stream allEmployees = employeeRepository.getAllEmployeeIds();
        BackgroundJob.enqueue(allEmployees, (salarySlipService, employeeId) -> salarySlipService.generateAndSendSalarySlip(employeeId));
    }

    @Job(name = "Generate and send salary slip to employee %0")
    public void generateAndSendSalarySlip(Long employeeId) throws Exception {
        final Employee employee = getEmployee(employeeId);
        Path salarySlipPath = generateSalarySlip(employee);
        emailService.sendSalarySlip(employee, salarySlipPath);
    }

    private Path generateSalarySlip(Employee employee) throws Exception {
        final WorkWeek workWeek = getWorkWeekForEmployee(employee.getId());
        final SalarySlip salarySlip = new SalarySlip(employee, workWeek);
        return generateSalarySlipDocumentUsingTemplate(salarySlip);
    }

    private Path generateSalarySlipDocumentUsingTemplate(SalarySlip salarySlip) throws Exception {
        Path salarySlipPath = Paths.get(System.getProperty("java.io.tmpdir"), String.valueOf(now().getYear()), format("workweek-%d", salarySlip.getWorkWeek().getWeekNbr()), format("salary-slip-employee-%d.docx", salarySlip.getEmployee().getId()));
        documentGenerationService.generateDocument(salarySlipTemplatePath, salarySlipPath, salarySlip);
        return salarySlipPath;
    }

    private WorkWeek getWorkWeekForEmployee(Long employeeId) {
        return timeClockService.getWorkWeekForEmployee(employeeId);
    }

    private Employee getEmployee(Long employeeId) {
        return employeeRepository.findById(employeeId).orElseThrow(() -> new IllegalArgumentException(format("Employee with id '%d' does not exist", employeeId)));
    }
}

И последнее, но не менее важное – наше приложение Spring Boot

Приложение Spring Boot загружает наше приложение и содержит один важный фрагмент кода:

    BackgroundJob.scheduleRecurringly(
        "generate-and-send-salary-slip",
        SalarySlipService::generateAndSendSalarySlipToAllEmployees,
        Cron.weekly(DayOfWeek.SUNDAY, 22)
);

Этот вызов метода гарантирует, что метод генерировать и отправлять квитанции о заработной плате Всем сотрудникам вашего SalarySlipService будет запускаться каждое воскресенье в 10 вечера. В этом приложении SpringBootApplication мы создаем несколько поддельных сотрудников, определяем Источник данных (в нашем случае простая база данных H2) и инициализируйте JobRunr, используя его fluent-api.

package org.jobrunr.example;

@SpringBootApplication
public class SalarySlipMicroService {

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

    @Bean
    public CommandLineRunner demo(EmployeeRepository repository) {
        final Faker faker = new Faker();
        return (args) -> {
            for(int i = 0; i < 1000; i++) {
                repository.save(new Employee(faker.name().firstName(), faker.name().lastName(), faker.internet().emailAddress()));
            }

            BackgroundJob.scheduleRecurringly(
                    "generate-and-send-salary-slip",
                    SalarySlipService::generateAndSendSalarySlipToAllEmployees,
                    Cron.weekly(DayOfWeek.SUNDAY, 22)
            );

            Thread.currentThread().join();
        };
    }

    @Bean
    public DataSource dataSource() {
        final JdbcDataSource ds = new JdbcDataSource();
        ds.setURL("jdbc:h2:" + Paths.get(System.getProperty("java.io.tmpdir"), "paycheck"));
        ds.setUser("sa");
        ds.setPassword("sa");
        return ds;
    }

    @Bean
    public JobScheduler initJobRunr(ApplicationContext applicationContext) {
        return JobRunr.configure()
                .useStorageProvider(SqlStorageProviderFactory
                        .using(applicationContext.getBean(DataSource.class)))
                .useJobActivator(applicationContext::getBean)
                .useDefaultBackgroundJobServer()
                .useDashboard()
                .initialize();
    }

}

Как только вы запустите микросервисное приложение Salary Slip, вы можете открыть свой браузер, используя URL-адрес http://localhost:8000/dashboard/ и перейдите на вкладку Повторяющиеся задания.

Чтобы протестировать его, мы запускаем его сейчас вручную. Задание обрабатывается и назначается новое задание для создания квитанции о заработной плате для каждого сотрудника. В течение 15 секунд начнется обработка этих заданий, и мы увидим сгенерированные PDF-документы в вашей папке tmp.

Мы можем проверить задание и увидеть, что оно выполнено успешно – если по какой-то причине оно завершится неудачей, оно будет автоматически повторено.

Мы можем проверить задание и увидеть, что оно выполнено успешно – если по какой-то причине оно завершится неудачей, оно будет автоматически повторено.

  • Job run и Spring Data очень хорошо интегрируются, и оба они очень просты в использовании. Возможность планировать Java 8 lambda и запускать их в фоновом режиме – действительно приятная особенность Jobrunner.
  • Чтобы преобразовать документ Word в PDF, в шаблоне word есть некоторые неприятные вещи (например, белый текст), чтобы иметь ОК-макет. Docx-Stamper – отличная библиотека и зависит от Docx4J. Docx4J позволяет конвертировать документы Word в PDF, но это все еще требует некоторой работы, так как для правильного макета было сделано несколько взломов. Преобразование из Word в PDF также занимает много времени, что идеально подходит для Jobrunner, поскольку это длительная фоновая работа.

Оригинал: “https://dev.to/rdehuyss/easily-process-long-running-jobs-with-jobrunr-5gkh”