Spring @Async אישור לעיבוד אסינכרוני

אנה סימון @Async ב- Spring מאפשר לנו ליצור שיטות אסינכרוניות ב- Spring. בואו נחקור את @Async במדריך זה על הספרייה של Spring. בקצרה, כאשר אנו סימנו שיטה של ביין ב- @Async, Spring יפעיל אותה בפועל בתהליך שונה thread והקורא של השיטה לא יחכה עד שהשיטה תושלם. נגדיר שירות משלנו ונשתמש ב- Spring Boot 2 בדוגמה זו. בואו נתחיל!

Spring @Async דוגמה

נשתמש ב- Maven כדי ליצור פרויקט דוגמה להדגמה. כדי ליצור את הפרויקט, בצע את הפקודה הבאה בתיקייה שתשתמש כמקום עבודה:

mvn archetype:generate -DgroupId=com.journaldev.asynchmethods -DartifactId=JD-SpringBoot-AsyncMethods -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

אם אתה מריץ את Maven לפעם הראשונה, זה ייקח מספר שניות עד שהפקודה generate תושלם מאחר ו־Maven צריך להוריד את כל התוספות והארטיפקטים הדרושים כדי לבצע את משימת היצירה. הנה כיצד נראית יצירת הפרויקט: יצירת פרויקט עם Maven לאחר שיצרת את הפרויקט, תרגיש חופשי לפתוח אותו בסביבת הפיתוח האהובה עליך. השלב הבא הוא להוסיף תלותים מתאימות של Maven לפרויקט. הנה קובץ pom.xml עם התלותים המתאימות:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

לבסוף, כדי להבין את כל ה־JARs שנוספים לפרויקט כאשר אנו מוסיפים את התלות הזו, אנו יכולים להפעיל פקודת Maven פשוטה שמאפשרת לנו לראות עץ תלות מלא עבור פרויקט כאשר אנו מוסיפים תלותים מסוימים אליו. הנה פקודה שאנו יכולים להשתמש בה:

mvn dependency:tree

כאשר אנו מפעילים את הפקודה הזו, היא תציג לנו את העץ הבא של תלותים:

אפשרות הפעלה אסינכרונית

הפעלת תמיכה ב-Async היא גם כן, עניין של אנוטציה יחידה. מלבד הפעלת ביצוע Async, אנו גם נעשה שימוש ב-Executor שמאפשר לנו להגדיר מגבלות של תהליכים גם. נדבר על זה יותר פרטית כאשר נכתוב את הקוד:

package com.journaldev.asynchexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@SpringBootApplication
@EnableAsync
public class AsyncApp {
    ...
}

כאן עשינו שימוש ב-@EnableAsync אנוטציה שמאפשרת ל-Spring להריץ שיטות אסינכרוניות בבקשה בפועל. לאחר מכן, אנו מוסיפים את ה-Executor המוזכר:

@Bean
public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(2);
    executor.setQueueCapacity(500);
    executor.setThreadNamePrefix("JDAsync-");
    executor.initialize();
    return executor;
}

כאן, אנו מגדירים שמקסימום של 2 תהליכים ירוצו באופן סקרני וגודל התור נקבע כ-500. הנה הקוד המלא של המחלקה עם הצהרות import:

package com.journaldev.asynchexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@SpringBootApplication
@EnableAsync
public class AsyncApp {

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

    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("JDAsync-");
        executor.initialize();
        return executor;
    }
}

בשלב הבא ניצור שירות שמבצע באמת ביצועי Thread.

יצירת מודל

נעשה שימוש ב-API הסרטים הציבורי שמחזיר פשוט נתוני סרט. אנו נגדיר את המודל שלנו עבור אותו דבר:

package com.journaldev.asynchexample;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class MovieModel {

    private String title;
    private String producer;

    // getters ו-setters סטנדרטיים

    @Override
    public String toString() {
        return String.format("MovieModel{title='%s', producer='%s'}", title, producer);
    }
}

השתמשנו ב-@JsonIgnoreProperties כדי שאם יש יותר מאפיונים בתגובה, יהיה אפשר להתעלם מהם בבטחה על ידי Spring.

יצירת השירות

זה הזמן שנגדיר את השירות שלנו אשר יקרא ל- API של הסרטים המוזכר. נשתמש ב- RestTemplate פשוט כדי להכות ב- API GET ולקבל תוצאות אסינכרוניות. בואו נסתכל על הקוד לדוגמה שאנו משתמשים בו:

package com.journaldev.asynchexample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.CompletableFuture;

@Service
public class MovieService {

    private static final Logger LOG = LoggerFactory.getLogger(MovieService.class);

    private final RestTemplate restTemplate;

    public MovieService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @Async
    public CompletableFuture lookForMovie(String movieId) throws InterruptedException {
        LOG.info("Looking up Movie ID: {}", movieId);
        String url = String.format("https://ghibliapi.herokuapp.com/films/%s", movieId);
        MovieModel results = restTemplate.getForObject(url, MovieModel.class);
        // השהייה מלאכותית של שנייה אחת לצורך הדגמה
        Thread.sleep(1000L);
        return CompletableFuture.completedFuture(results);
    }
}

כיתה זו היא @Service שמכשילה אותה לסורק רכיבי Spring. סוג ההחזר של שיטת lookForMovie הוא CompletableFuture שהוא דרישה עבור כל שירות אסינכרוני. מכיוון שהזמן עבור ה- API עשוי להשתנות, הוספנו השהייה של 2 שניות לצורך הדגמה.

יצירת CommandLine Runner

נפעיל את האפליקציה שלנו באמצעות CommandLineRunner שהוא הדרך הקלה ביותר לבדוק את האפליקציה שלנו. CommandLineRunner רץ מייד לאחר שכל ה- beans של האפליקציה כבר הופעלו. בואו נראה את הקוד ל- CommandLineRunner:

package com.journaldev.asynchexample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;

@Component
public class ApplicationRunner implements CommandLineRunner {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationRunner.class);

    private final MovieService movieService;

    public ApplicationRunner(MovieService movieService) {
        this.movieService = movieService;
    }


    @Override
    public void run(String... args) throws Exception {
        // התחל את השעון
        long start = System.currentTimeMillis();

        // התחלת מרץ מרובה, חיפושי אסינכרוניים
        CompletableFuture page1 = movieService.lookForMovie("58611129-2dbc-4a81-a72f-77ddfc1b1b49");
        CompletableFuture page2 = movieService.lookForMovie("2baf70d1-42bb-4437-b551-e5fed5a87abe");
        CompletableFuture page3 = movieService.lookForMovie("4e236f34-b981-41c3-8c65-f8c9000b94e7");

        // שולב את כל התהליכים כך שנוכל לחכות עד שכולם יסיימו
        CompletableFuture.allOf(page1, page2, page3).join();

        // הדפס תוצאות, כולל זמן עבר
        LOG.info("Elapsed time: " + (System.currentTimeMillis() - start));
        LOG.info("--> " + page1.get());
        LOG.info("--> " + page2.get());
        LOG.info("--> " + page3.get());
    }
}

פשוט השתמשנו ב- RestTemplate כדי להכות ב- API של הדוגמה שהשתמשנו בו עם מספרי זיהוי של סרטים שנבחרו באופן אקראי. נפעיל את האפליקציה שלנו כדי לראות אילו פלטים היא מציגה.

הפעלת היישום

כאשר אנו מפעילים את היישום, נראה את הפלט הבא:

2018-04-13  INFO 17868 --- [JDAsync-1] c.j.a.MovieService  : Looking up Movie ID: 58611129-2dbc-4a81-a72f-77ddfc1b1b49
2018-04-13 08:00:09.518  INFO 17868 --- [JDAsync-2] c.j.a.MovieService  : Looking up Movie ID: 2baf70d1-42bb-4437-b551-e5fed5a87abe
2018-04-13 08:00:12.254  INFO 17868 --- [JDAsync-1] c.j.a.MovieService  : Looking up Movie ID: 4e236f34-b981-41c3-8c65-f8c9000b94e7
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : Elapsed time: 4056
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='My Neighbor Totoro', producer='Hayao Miyazaki'}
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='Castle in the Sky', producer='Isao Takahata'}
2018-04-13 08:00:13.566  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='Only Yesterday', producer='Toshio Suzuki'}

אם נשים לב היטב, רק שני תהליכים נוצרו להרצת היישום, כלומר JDAsync-1 ו־JDAsync-2.

מסקנה

בשיעור זה, למדנו כיצד ניתן להשתמש ביכולות האסינכרוניות של Spring עם Spring Boot 2. לקריאת פוסטים נוספים בנושא Spring ניתן ללחוץ כאן.

להורדת קוד המקור

להורדת פרויקט דוגמה של Spring Boot Async

Source:
https://www.digitalocean.com/community/tutorials/spring-async-annotation