Introduction
Microservices are being developed all around us nowadays. Many of these services are short-lived. Scheduled tasks, data synchronization, data aggregation, report generation and similar services are short-lived. They are typically expected to start, run to completion and end.
Many external applications and schedulers have been built to meet this purpose, however, sometimes you need a custom task that requires deep integration with your organization's application.
The Spring Boot platform makes this functionality available to developers via the Spring Cloud Task API.
What is the Spring Cloud Task?
Normally, it is expected that services are long-running. An average Spring Boot service includes an embedded web server like Tomcat, Jetty or Undertow. A service is ended because it is stopped on purpose or a runtime error like an OOM (OutOfMemoryError
) occurred.
Spring Boot was built this way, but as paradigms shifted and the microservice architecture became popular - many services became short-lived. This was overkill as a short-lived notification service doesn't need to have an embedded server and could be a lot more lightweight.
Spring Cloud Task is Spring’s answer to the problem of short-lived microservices in Spring Boot.
With Spring Cloud Task, you get an on-demand JVM process that performs a task and immediately ends.
In this article, we'll be linking a lot to the official Spring Cloud Task project available on GitHub.
Spring Cloud Task Technical Architecture
Spring Cloud Task uses a few annotations to set up the system and a database (at least for production) to record the result of each invocation.
To make your Spring Boot application a cloud task, you need to annotate one of your application's configuration classes with @EnableTask
.
This annotation imports the TaskLifecycleConfiguration into your project. The TaskLifecycleConfiguration
class is the configuration class for the TaskLifecycleListener
, TaskRepository
and other useful classes needed to bring all the functionalities of Spring Cloud Task to life.
Implementation
Spring Initializr
A good way to bootstrap your skeleton Spring Boot project is to use Spring Initializr. Select your preferred database dependency, the Spring Cloud Task dependency and the Spring Data JPA dependency:
If you've already got a project running, using Maven, add the adequate dependencies:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-task</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${version}</version>
</dependency>
The spring-cloud-starter-task
dependency includes spring-boot-starter
, spring-cloud-task-core
, spring-cloud-task-batch
and spring-cloud-task-stream
.
The spring-cloud-task-core
dependency is the main one we'll be using - you can import the spring-boot-starter
and the former dependency separately.
Alternatively, if you're using Gradle:
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-task', version: '2.2.3.RELEASE'
Configuring the Application
To register an app as a Spring Cloud Task, you have to annotate one of the configuration classes with @EnableTask
:
@EnableTask
@SpringBootApplication
public class SampleSpringCloudTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SampleSpringCloudTaskApplication.class, args);
}
}
Since the @SpringBootApplication
annotation is a combination of @EnableAutoConfiguration
, @Configuration
and @ComponentScan
, it's perfectly fine to annotate the main class with the @EnableTask
annotation.
TaskRepository
A task can be created without a database. In that case, it uses an in-memory instance of H2 to manage the task repository events. This is fine for development, but not production. For production, a data source is recommended. It preserves all records of your task executions and errors.
All task events are persisted using the TaskRepository
.
In a single-data-source project, Spring Cloud Task creates the task tables in the specified database.
However, in a multi-data-source project, you have to choose the data source to use with Spring Cloud Task. An example of a multi-data-source project can be found in the Spring Cloud Task sample projects.
In the example, 2 data sources are specified in the DataSoureConfiguration
class:
@Configuration
public class DataSourceConfiguration {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.build();
}
@Bean
public DataSource secondDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
}
In order to specify the data source to be used by Spring Cloud Task, a CustomTaskConfigurer
component is created. It extends DefaultTaskConfigurer
, passing the qualified data source in the constructor:
@Component
public class CustomTaskConfigurer extends DefaultTaskConfigurer {
@Autowired
public CustomTaskConfigurer(@Qualifier("secondDataSource") DataSource dataSource) {
super(dataSource);
}
}
Unless a custom TaskRepository
is specified by extending the SimpleTaskRepository
, the SimpleTaskRepository
is the default TaskRepository
used by Spring Cloud Task.
TaskConfigurer
The TaskConfigurer
is used for customizing Spring Cloud Task configurations. DefaultTaskConfigurer
is the default configurer used if no custom task configurer that implements the TaskConfigurer
interface is provided.
DefaultTaskConfigurer
provides map-based components if no data source is available and a JDBC-components if a data source is provided.
TaskExplorer
The TaskExplorer
, as its name suggests, is an explorer for task executions. It is useful for gathering actual task information from the task repository.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
By default, Spring Cloud Tasks uses the SimpleTaskExplorer
.
From the TaskExplorer
, you can query a lot of useful information about the task executions like total count of TaskExecutions
, currently running TaskExecutions
, finding all TaskExecutions
, etc.
TaskExecution
TaskExecution
is the state of the Task for each Execution. All the information stored in the TaskRepository
is modeled in this class. It is the basic unit for every task.
Some of the information stored is
executionId
- Unique id associated with the task execution.exitcode
- Recorded exit code for the task.taskName
- User-defined name for the task.startTime
- Task start time.endTime
- Timestamp of task completion.
Running a Task
To run our task, we have to implement the Runner
interface and provide it as a bean in our configuration class.
Usually, CommandLineRunner
or ApplicationRunner
is implemented:
@Component
public class SampleCommandLineRunner implements CommandLineRunner {
@Override
public void run(String...args) throws Exception {
System.out.println("Specified Task");
}
}
@Configuration
public class TaskConfiguration {
@Bean
public SampleCommandLineRunner sampleCommandLineRunner() {
return new SampleCommandLineRunner();
}
}
And with that, from a main
method, we can call the SampleCommandLineRunner
:
@SpringBootApplication
public class SomeApplication {
public static void main(String[] args) {
SpringApplication.run(SampleCommandLineRunner.class, args);
}
}
TaskExecutionListener
All TaskExecution
s have a life cycle. Spring Cloud Task records these events. At the beginning of a task, before any Runner
interface implementations have been run, an entry in the TaskRepository
that records the start event is created.
When a task is completed or failed, another event is emitted. TaskExecutionListener
allows you to register listeners to listen for this event throughout the lifecycle.
You can specify as many listeners as you want to the same event.
Spring provides two approaches for doing this - using the TaskExecutionListener
interface or the method bean annotation approach.
For the former, you provide a component that implements the TaskExecutionListener
interface and its three methods:
void onTaskStartup(TaskExecution taskExecution);
void onTaskEnd(TaskExecution taskExecution);
void onTaskFailed(TaskExecution taskExecution, Throwable throwable);
-
onTaskStartup()
- Invoked after theTaskExecution
has been stored in theTaskRepository
. -
onTaskEnd()
- Invoked after theTaskExecution
has been updated in theTaskRepository
, upon task end. -
onTaskFailed()
- Invoked if an uncaught exception occurs during task execution.
On the other hand, using the method bean annotation approach, you create a component and supply it as a bean in your Spring configuration:
@Configuration
public class AppConfigurtion {
@Bean
public AppTaskListener appTaskListener() {
return new AppTaskListener();
}
}
@Component
class AppTaskListener {
@BeforeTask
public void beforeTaskInvocation(TaskExecution taskExecution) {
System.out.println("Before task");
}
@AfterTask
public void afterTaskInvocation(TaskExecution taskExecution) {
System.out.println("After task");
}
@FailedTask
public void afterFailedTaskInvocation(TaskExecution taskExecution, Throwable throwable) {
System.out.println("Failed task");
}
}
@BeforeTask
is analogous toonTaskStartup()
.@AfterTask
is analogous toonTaskEnd()
.@FailedTask
is analogous toonTaskFailed()
.
Task Exit Messages
Although you can specify any number of listeners to a particular event, if an exception is thrown by a TaskExecutionListener
event handler, all listener processing for that event handler stops.
For example, if three onTaskStartup()
listeners have started and the first onTaskStartup()
event handler throws an exception, the other two onTaskStartup
methods are not called.
However, the other event handlers (onTaskEnd()
and onTaskFailed()
) for the TaskExecutionListeners
are called.
When exceptions occur, the TaskExecutionListener
returns an exit code and an exit message. Annotating a method with @AfterTask
will allow us to set the exit message:
@AfterTask
public void afterError(TaskExecution taskExecution) {
taskExecution.setExitMessage("Custom Exit Message");
}
An Exit Message can be set at any of the listener events, though only the relevant messages will be set. If a task runs successfully, the onTaskFailed()
event will not fire. When the task ends, the message from the onTaskEnd()
event is set.
Customizing Cloud Task
A lot of properties can be overridden by the TaskConfigurer
by specifying custom values in the applications.properties
or applications.yaml
file.
Spring cloud task properties are prefixed with spring.cloud.task
in the applications.properties
file.
Some of the properties that can be overridden are:
spring.cloud.task.tablePrefix
- This is the table prefix for the task tables forTaskRepository
. The default table prefix is "TASK_"
.spring.cloud.task.initialize-enabled=false
- This is used to enable or disable the task tables creation at task startup. It defaults to true.spring.cloud.task.executionid=yourtaskId
- This is used to configure Spring Cloud Task to use your custom task id. By default, spring generates a task execution Id for each task execution.
To see more customizable properties, check out askProperties
.
Logging Cloud Task Events
It is usually useful during development to see your application debug logs. In order to change the logging level for a Spring Cloud Task, add this to your applications.properties
file:
logging.level.org.springframework.cloud.task=DEBUG
Conclusion
In this article, we introduced Spring Cloud Task, what it is and the problems it solves. We also covered examples of how to set it up with a data source and run a task with the Runner
interfaces.
Also, we explained the Spring Cloud Task architecture and all the domain models like TaskExecution
, TaskExecutionListener
, etc. used to achieve all the functionalities.