Error Handling in a REST Service with Quartz
Java, Maven, Liquibase, Quartz, REST, Spring Boot
Spring MVC provides several complimentary approaches to exception handling.
Introduction
I welcome you all to this third post in the series of integrating Quartz with Spring. Througout the course of this tutorial on Quartz, you will notice we have not done any error handling. In this post I will show you how to configure a Spring REST application to handle errors.
This post builds upon the previous post and you can get the source/base for this post as zip|tar.gz. Extract the contents of the archive and let us begin.
Directory structure
The contents of the archive should be similar to the directory structure below:
.
|__src/
| |__main/
| | |__java/
| | | |__com/
| | | | |__juliuskrah/
| | | | | |__quartz/
| | | | | | |__Application.java
| | | | | | |__autoconfigure/
| | | | | | | |__QuartzProperties.java
| | | | | | |__job/
| | | | | | | |__EmailJob.java
| | | | | | |__mail/
| | | | | | | |__javamail/
| | | | | | | | |__AsyncMailSender.java
| | | | | | |__model/
| | | | | | | |__JobDescriptor.java
| | | | | | | |__TriggerDescriptor.java
| | | | | | |__service/
| | | | | | | |__EmailService.java
| | | | | | |__web/
| | | | | | | |__rest/
| | | | | | | | |__EmailResource.java
| | | | | | | | |__errors/
| | | | | | | | | |__ErrorVO.java
| | | | | | | | | |__ExceptionTranslator.java
| | | | | | | | | |__FieldErrorVO.java
| | | |__org/
| | | | |__springframework/
| | | | | |__boot/
| | | | | | |__autoconfigure/
| | | | | | | |__quartz/
| | | | | | | | |__AutowireCapableBeanJobFactory.java
| | | |__io/
| | | | |__github/
| | | | | |__jhipster/
| | | | | | |__async/
| | | | | | | |__ExceptionHandlingAsyncTaskExecutor.java
| | |__resources/
| | | |__db/
| | | | |__changelog/
| | | | | |__db.changelog-master.yaml
| | | |__application.yaml
| | | |__quartz.properties
|__pom.xml
Prerequisites
To follow along with this guide, you should have the following set up on your development machine:
Optional
Adding Validation
I will start with basic validation by annotating the *Descriptor
classes with Hibernate Validator annotations.
Hibernate Validator is the Reference Implementation of Bean Validation [JSR 349] (for those of you who like to know):
file:
src/main/java/com/juliuskrah/quartz/model/TriggerDescriptor.java
public class TriggerDescriptor {
@NotBlank
private String name;
@NotBlank
private String group;
@Valid
private List<TriggerDescriptor> triggerDescriptors = new ArrayList<>();
...
}
The trigger name
and group
may not be empty or null.
file:
src/main/java/com/juliuskrah/quartz/model/JobDescriptor.java
public class JobDescriptor {
@NotBlank
private String name;
private String group;
@NotEmpty
private String subject;
@NotEmpty
private String messageBody;
@NotEmpty
private List<String> to;
...
}
As you can see from the above, there is no validation on the group
. This is because the API consumers are not
required to specify a group name in the request payload but rather in the URL as a PathVariable
.
To activate the validation, annotate the JobDescriptor
parameter with @Valid
:
file:
src/main/java/com/juliuskrah/quartz/web/rest/EmailResource.java
public class EmailResource {
@PostMapping(path = "/groups/{group}/jobs")
public ResponseEntity<JobDescriptor> createJob(@PathVariable String group,
@Valid @RequestBody JobDescriptor descriptor) {
//
}
@PutMapping(path = "/groups/{group}/jobs/{name}")
public ResponseEntity<Void> updateJob(@PathVariable String group,
@PathVariable String name,
@Valid @RequestBody JobDescriptor descriptor) {
//
}
}
You can test this out by making a POST
or PUT
request and leave the name
field blank.
Adding Exceptions
Let us start with throwing Unchecked
exceptions for a few known exception prone areas. When an API
consumer creates a job and specifies its trigger(s) and fails to add either cron
or fireTime
in the
trigger(s) we will throw an IllegalStateException
:
file:
src/main/java/com/juliuskrah/quartz/model/TriggerDescriptor.java
public class TriggerDescriptor {
...
public Trigger buildTrigger() {
if (!isEmpty(cron)) {
//
} else if (!isEmpty(fireTime)) {
//
}
throw new IllegalStateException("unsupported trigger descriptor " + this);
}
}
We can also validate the given cron
expression and throw IllegalArgumentException
if expression is invalid:
file:
src/main/java/com/juliuskrah/quartz/model/TriggerDescriptor.java
public class TriggerDescriptor {
...
public Trigger buildTrigger() {
if (!isEmpty(cron)) {
if (!isValidExpression(cron))
throw new IllegalArgumentException("Provided expression '" + cron + "' is not a valid cron expression");
//
}
}
}
We need to ensure that consumers of our API do not register jobs with an existing name
and group
in the
scheduler:
file:
src/main/java/com/juliuskrah/quartz/service/EmailService.java
public class EmailService extends AbstractJobService {
...
public JobDescriptor createJob(String group, JobDescriptor descriptor) {
String name = ...;
try {
if (scheduler.checkExists(jobKey(name, group)))
throw new DataIntegrityViolationException("Job with Key '" + group + "." + name +"' already exists");
...
} catch (SchedulerException e) {
//
}
return descriptor;
}
}
I will show you how to translate these exceptions into meaningful 4xx
status codes to the API consumer in the
next section.
Adding Exception Translators
We will create two immutable value objects (FieldErrorVO
and ErrorVO
) to transfer the errors to the API consumers.
To ease this creation we will use Immutables:
file:
pom.xml
...
<dependency>
<groupId>org.immutables</groupId>
<artifactId>value</artifactId>
<version>2.5.6</version>
<scope>provided</scope>
</dependency>
Immutables is an annotation processor and if you are using an IDE you can activate it by following the instructions laid out in this link.
We will create a value object to map field validation errors:
file:
src/main/java/com/juliuskrah/quartz/web/rest/errors/FieldErrorVO.java
@Value.Immutable
@JsonSerialize(as = ImmutableFieldErrorVO.class)
@JsonDeserialize(as = ImmutableFieldErrorVO.class)
public interface FieldErrorVO {
String objectName();
String field();
String message();
}
While
ImmutableFieldErrorVO
may not be generated yet, the above will compile properly
And another value object to map all other errors:
file:
src/main/java/com/juliuskrah/quartz/web/rest/errors/ErrorVO.java
@Value.Immutable
@JsonSerialize(as = ImmutableErrorVO.class)
@JsonDeserialize(as = ImmutableErrorVO.class)
public interface ErrorVO {
String message();
String description();
List<FieldErrorVO> fieldErrors();
}
While
ImmutableErrorVO
may not be generated yet, the above will compile properly
Now we create the ExceptionTranslator
class:
file:
src/main/java/com/juliuskrah/quartz/web/rest/errors/ExceptionTranslator.java
@RestControllerAdvice
public class ExceptionTranslator {
@ExceptionHandler(IllegalStateException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorVO processUnsupportedTriggerError(IllegalStateException ex) {
ErrorVO dto = ImmutableErrorVO.builder()
.message("400: Bad Request")
.description(ex.getMessage())
.build();
return dto;
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorVO processInvalidCronExpressionError(IllegalArgumentException ex) {
//
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorVO processValidationError(MethodArgumentNotValidException ex) {
...
List<FieldError> fieldErrors = ...;
ImmutableErrorVO.Builder builder = ...;
for (FieldError fieldError : fieldErrors) {
builder.addFieldErrors(ImmutableFieldErrorVO.builder()
.objectName(fieldError.getObjectName())
.field(fieldError.getField())
.message(fieldError.getCode())
.build());
}
return builder.build();
}
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(CONFLICT)
public ErrorVO processDataIntegrityViolationError(DataIntegrityViolationException ex) {
//
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorVO> processException(Exception ex) throws Exception {
//
}
}
That’s all folks
Conclusion
In this post we learned how to validate and handle errors from the client and server. You can extend this to create powerful and robust exception handling for your APIs.
You can find the source to this guide in the github repository. Until the next post, keep doing cool things .