Brady Haden
Brady Haden

Reputation: 225

Spring Boot @Async use With @PostConstruct

Edit for Solution

This ended up being my @Service class with multi-threading working. It populates each table in the DB at the same time. No longer needed the @Async annotations or task executor with this solution.

@Service
public class AsyncDBLoad extends Thread {

    final CovidDataServices service;

    Thread.UncaughtExceptionHandler exceptionHandler = new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread th, Throwable ex) {
            System.out.println("Uncaught exception: " + ex.getMessage()
                    + "\nIn Thread: " + th.getName());
        }
    };

    public AsyncDBLoad(CovidDataServices service) {
        this.service = service;
    }

    @PostConstruct
    public void initializeAsyncDB() throws IOException, InterruptedException {
        // Thread to populate state DB
        Thread stateThread = new Thread() {
            @Override
            public void run() {
                try {
                    service.populateDbWithStateData();
                } catch (InterruptedException | IOException e) {
                    System.out.println("State thread error");
                }
            }
        };
        // Thread to populate country DB
        Thread countryThread = new Thread() {
            @Override
            public void run() {
                try {
                    service.populateDBWithCountryData();
                } catch (InterruptedException | IOException e) {
                    System.out.println("Country thread error");
                }
            }
        };

        // set error handlers
        stateThread.setUncaughtExceptionHandler(exceptionHandler);
        countryThread.setUncaughtExceptionHandler(exceptionHandler);
        // set names
        stateThread.setName("State Thread");
        countryThread.setName("Country Thread");
        // start the threads
        stateThread.start();
        countryThread.start();
    }
}

Original Question

I am trying to get 2 @PostConstruct methods in a @Service to work with @Async.

Specifically on startup I am populating DB tables and these are separate from each other and could be loaded concurrently.

I have tried to follow along here and got the first @PostConstruct to run asynchronously but the second method still waits for the first one to be completed before starting.

This is currently what I have for code.

Application start:

@SpringBootApplication
@EnableAsync
public class Covid19Trackerv2Application {

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

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

@Service to call @Async methods:

@Service
public class AsyncDBLoad {

    final CovidDataServices service;

    public AsyncDBLoad(CovidDataServices service) {
        this.service = service;
    }

    @PostConstruct
    public void initializeAsyncDB() throws IOException, InterruptedException {
        service.populateDBWithCountryData();
    }

    @PostConstruct
    public void initializeAsyncDB2() throws IOException, InterruptedException {
        service.populateDbWithStateData();
    }
}

Relevant parts of the CovidDataServices @Service that holds the @Async methods:

    @Async
    public void populateDbWithStateData() throws IOException, InterruptedException {
        System.out.println("Start state DB population");
        // part of the code that populates the DB and takes around 15 seconds if DB is empty
        System.out.println("exit state DB population");
    }

    @Async
    public void populateDBWithCountryData() throws IOException, InterruptedException {
        System.out.println("Start country DB population");
        // part of the code that populates the DB and takes around 15 seconds if DB is empty
        System.out.println("exit country DB population");
    }

Log output of startup:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.3.RELEASE)

2020-10-03 10:29:50.222  INFO 9636 --- [           main] c.c.Covid19Trackerv2Application          : Starting Covid19Trackerv2Application on DESKTOP-AE0J8AT with PID 9636 (C:\Users\bhade\Documents\covid-19-trackerv2\build\classes\java\main started by bhade in C:\Users\bhade\Documents\covid-19-trackerv2)
2020-10-03 10:29:50.224  INFO 9636 --- [           main] c.c.Covid19Trackerv2Application          : No active profile set, falling back to default profiles: default
2020-10-03 10:29:50.636  INFO 9636 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data MongoDB repositories in DEFAULT mode.
2020-10-03 10:29:50.677  INFO 9636 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 37ms. Found 2 MongoDB repository interfaces.
2020-10-03 10:29:51.352  INFO 9636 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-10-03 10:29:51.361  INFO 9636 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-10-03 10:29:51.361  INFO 9636 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.37]
2020-10-03 10:29:51.429  INFO 9636 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-10-03 10:29:51.429  INFO 9636 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1164 ms
2020-10-03 10:29:51.561  INFO 9636 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[localhost:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms'}
2020-10-03 10:29:51.609  INFO 9636 --- [localhost:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:1, serverValue:78}] to localhost:27017
2020-10-03 10:29:51.614  INFO 9636 --- [localhost:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=localhost:27017, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=8, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=2658300}
2020-10-03 10:29:51.843  INFO 9636 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'
2020-10-03 10:29:51.847  INFO 9636 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService
2020-10-03 10:29:51.847  INFO 9636 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'taskExecutor'
Start country DB population
2020-10-03 10:29:51.894  INFO 9636 --- [   scheduling-1] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:79}] to localhost:27017
2020-10-03 10:29:52.086  INFO 9636 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-10-03 10:29:52.095  INFO 9636 --- [           main] c.c.Covid19Trackerv2Application          : Started Covid19Trackerv2Application in 2.234 seconds (JVM running for 2.525)
exit country DB population
Start state DB population
exit state DB population

The log shows that the first @PostConstruct gets called, but the application continues to load. The second @PostConstruct does not get called until the first one is finished. I have also tried putting the method calls in the same @PostConstruct like below which gave the same results.

    @PostConstruct
    public void initializeAsyncDB() throws IOException, InterruptedException {
        service.populateDBWithCountryData();
        service.populateDbWithStateData();
    }

Is there a way to do this?

Any help is much appreciated!

Upvotes: 4

Views: 4950

Answers (2)

Julien Kronegg
Julien Kronegg

Reputation: 5251

While there is already an accepted solution based on threads, it's worth giving an answer which answer to the original question on how to combine @Async with @PostConstruct.

Quoting Spring documentation:

You can not use @Async in conjunction with lifecycle callbacks such as @PostConstruct. To asynchronously initialize Spring beans, you currently have to use a separate initializing Spring bean that then invokes the @Async annotated method on the target

public class SampleBeanImpl implements SampleBean {

    @Async
    void doSomething() {
        // ...
    }

}

public class SampleBeanInitializer {

    private final SampleBean bean;

    public SampleBeanInitializer(SampleBean bean) {
        this.bean = bean;
    }

    @PostConstruct
    public void initialize() {
        bean.doSomething();
    }

}

It requires the Spring application configuration to be annotated with @EnableAsync.

Upvotes: 0

gtiwari333
gtiwari333

Reputation: 25156

How about starting both populate methods in separate Threads?

        @PostConstruct
        void xx() {
            new Thread(service::a).start();
            new Thread(service::b).start();
        }
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@SpringBootApplication
public class PostConstructApp {
    public static void main(String[] args) {      SpringApplication.run(PostConstructApp.class, args); }
    @Component
    @RequiredArgsConstructor
    class C {
        final MyService service;
        @PostConstruct
        void xx() {
            new Thread(service::a).start();
            new Thread(service::b).start();
        }
    }

    @Component
    class MyService {
        @SneakyThrows
        void a() {
            System.out.println("A");
            Thread.sleep(10000);
            System.out.println("DONE A");
        }
        @SneakyThrows
        void b() {
            System.out.println("B");
            Thread.sleep(5000);
            System.out.println("DONE B");
        }
    }
}

Logs: A and B are printed together and DONE B and DONE A after a whlie:

2020-10-03 14:07:13.640  INFO 15974 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.38]
2020-10-03 14:07:13.687  INFO 15974 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-10-03 14:07:13.687  INFO 15974 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 544 ms
A
B
2020-10-03 14:07:14.018  INFO 15974 --- [           main] s.psotcons.PostConstructApp              : Started PostConstructApp in 1.046 seconds (JVM running for 1.341)
DONE B
DONE A

Upvotes: 2

Related Questions