Broadleaf Microservices

Error Common

This error module provides a few utilities:

  1. A general API error response JSON representation that can be returned from endpoints on general errors

  2. A validation response JSON representation for endpoint and other validation failures (Spring Validation and JSR-380 Java validation)

  3. Out of the box exception handlers for validation

Error Library Javadocs

API Errors

Take a look at the general API response:

@Getter
@ToString
@With
@AllArgsConstructor(onConstructor_ = {@JsonCreator})
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class ApiError {

    /**
     * A machine-readable representation of the type of error being returned
     */
    private final String type;

    /**
     * A human-readable representation of the type of error being returned
     */
    private final String title;

    /**
     * Optional exception class name
     */
    @Setter
    private String exception;

    /**
     * When the error occurred, defaults to {@link LocalDateTime#now()}
     */
    @Setter
    private OffsetDateTime timestamp = Instant.now().atOffset(ZoneOffset.UTC);

    /**
     * Should always match exactly the status in the response header. Makes it easier to parse out
     * and log
     */
    private final HttpStatus status;

    /**
     * Numerical representation of {@link #status}
     */
    private final int statusCode;

    /**
     * Optionally gives additional information to the client about what was wrong with their request
     *
     * @see #addError(GlobalError)
     */
    @JsonInclude(value = JsonInclude.Include.NON_EMPTY)
    private List<GlobalError> globalErrors = new ArrayList<>();

    /**
     * Initializes a new error
     *
     * @param type a machine-readable representation of this error
     * @param title a human-readable representation of the error
     * @param status HTTP status
     */
    public ApiError(String type, String title, @NonNull HttpStatus status) {
        this.type = type;
        this.title = title;
        this.status = status;
        this.statusCode = status.value();
    }

    public ResponseEntity<ApiError> toResponseEntity() {
        return new ResponseEntity<>(this, HttpStatus.valueOf(statusCode));
    }

    /**
     * Gives more information to a client about what went wrong in their request
     *
     * @param error the error to add to this response
     */
    public void addError(GlobalError error) {
        globalErrors.add(error);
    }

}

This error object can be as the result of a general Spring @ExceptionHandler, either in a global @ControllerAdvice or specifically in a @Controller. Here is an example of catching a very general exception and turning that into an APIError:

@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<Object> error(Exception ex) {
  return new ResponseEntity(new ApiError('general-error', "Looks like something went wrong on our end"), HttpStatus.INTERNAL_SERVER_ERROR)
}

Default MVC Exception Handler

Broadleaf automatically initializes an @ExceptionHandler as @RestControllerAdvice that handles common Spring MVC exceptions. This advice extends from the ResponseEntityExceptionHandler to return these common exceptions from the API in this consistent format.

Note
The ApiMvcExceptionAdvisor will only apply to controllers annotated with @ResponseBody (like @RestController and @FrameworkRestController)

Customizing

The ApiMvcExceptionAdvisor is default ordered to the lowest priority. To add your own exception handling, simply add an @ControllerAdvice with any set priority:

@RestControllerAdvice(annotations = ResponseBody.class)
@Order(0)
public class PrioritizedExceptionHandler {

  @ExceptionHandler(MyServerException.class)
  public ResponseEntity<Object> error(MyServerException ex) {
    return new ResponseEntity("Something went wrong", HttpStatus.INTERNAL_SERVER_ERROR);
  }
}

You can also override the bean definition:

@Configuration
public class WebConfiguration {

  @Bean
  public ApiMvcExceptionAdvisor customExceptionHandler(Environment env) {
    return new CustomizedAdvisor(env);
  }

  public static class CustomizedAdvisor extends ApiMvcExceptionAdvisor {
    public CustomizedAdvisor(Environment env) {
      return super(env);
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
          Exception ex,
          @Nullable Object body,
          HttpHeaders headers,
          HttpStatus status,
          WebRequest request) {
      // custom logic here
    }
}

Default Runtime Exception Handler

Broadleaf automatically initializes an @ExceptionHandler as @RestControllerAdvice that handles arbitrary runtime exceptions. This handler is given the lowest priority and is intended to catch any uncaught runtime exceptions, log them, and return a consistent error response.

Extension

You can override the bean definition of this handler:

@Configuration
public class WebConfiguration {

  @Bean
  public RuntimeExceptionAdvisor runtimeExceptionHandler() {
    return new MyRuntimeExceptionAdvisor();
  }

  public static class MyRuntimeExceptionAdvisor extends RuntimeExceptionAdvisor {

    @Override
    public ResponseEntity<ApiError> handleRuntimeException(RuntimeException ex,
            WebRequest request) {
        // handle exception
    }
  }
}

Spring Boot Error Attributes

Broadleaf provides an extension of Spring Boot’s DefaultErrorAttributes that provides consistent responses with a generic ApiError for any exceptions that do not have a corresponding @ExceptionHandler.

Important
Broadleaf will back off of its custom ErrorAttributes provided you provide your own. However, Broadleaf includes a number of default @ExceptionHandler implementations that take precedence over any Spring Boot error configuration with ErrorAttributes or ErrorController.

Customizing Error Attributes

Customizing the ErrorAttributes (or a custom ErrorController) is documented in Spring Boot’s reference docs. The only difference is to extend BroadleafErrorAttributes instead of DefaultErrorAttributes.

Validation

Exception Handlers

A default exception handler (ApiValidationWebExceptionAdvisor) is configured to respond to the following exceptions:

ValidationException.class,
BindException.class,
MethodArgumentNotValidException.class,
ConstraintViolationException.class

These exceptions will all return an ApiValidationError response:

@Getter
public class ApiValidationError extends ApiError {

    private Map<String, List<FieldValidationError>> fieldErrors = new HashMap<>();

    public ApiValidationError() {
        super("VALIDATION", "Validation Error", HttpStatus.BAD_REQUEST);
    }

    public void addFieldError(@NonNull FieldValidationError error) {
        Assert.notNull(error, "Cannot initialize a null field error");
        fieldErrors.computeIfAbsent(error.getPath(), k -> new ArrayList<FieldValidationError>())
                .add(error);
    }

This is a customization of the ApiError to also include field-level validations from a Spring Errors object (or JSR-380 `ConstraintViolation`s).

Generally, Broadleaf services will throw instances of ValidationException that includes a BingingResult of what went wrong. In conjunction with the BroadleafValidationWebExceptionHandler, this allows you to throw ValidationException(errors) from anywhere in a code flow to communicate back a 400, formatted response to clients.

JSR 380 Annotations

In a Spring MVC controller, you can mark an object as @Valid. Example:

@PostMapping("/receive")
@ResponseBody
// throws MethodArgumentNotValidException if invalid
public String receiveBodyInput(@Valid @RequestBody Product p) {
  return "Validation success!"
}

// throws BindException if invalid
public String receiveParameterInput(@Valid @RequestParam Product p) {
  return "Validation success!"
}

You can annotate these objects (like Product) with JSR-380 annotations (e.g. @NotNull for required fields and @Valid to cascade the validation check into embedded objects and collections) and those will get validated automatically. For more customized logic you can also add your own validators with an @InitBinder in a controller:

@RestController
public class ProductEndpoint {

  @InitBinder
  public void binder(WebDataBinder binder) {
    binder.addValidators(new CustomValidator());
  }

  static class CustomValidator implements Validator {
    boolean supports(Class<?> clazz) {
      return Product.class.isAssignableFrom(clazz);
    }

	  void validate(Object target, Errors errors) {
      ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "Name is required")
    }
  }

  @PostMapping
  public String receive(@Valid @RequestBody Product p) {
    return "Success!";
  }
}

Or in an @ControllerAdvice to do this globally (don’t forget to add your bean to a config file):

@ControllerAdvice
public class GlobalAdvice {

  @InitBinder
  public void binder(WebDataBinder binder) {
    binder.addValidators(new CustomValidator());
  }

  static class CustomValidator implements Validator {
    boolean supports(Class<?> clazz) {
      return Product.class.isAssignableFrom(clazz);
    }

    void validate(Object target, Errors errors) {
      ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "Name is required")
    }
  }
}
Note
Init binders by default will try to apply to all @Valid objects. If you have multiple Validators for different classes being validated in the same scope, you will get the error java.lang.IllegalStateException: Invalid target for Validator …​. You will need to provide type information for the init binders, like: @InitBinder("user"). See detailed example [here](https://stackoverflow.com/questions/14533488/adding-multiple-validators-using-initbinder/15796537#15796537).

You can also mark other Spring beans with @Validated to validate those method parameters:

@Service
@Validated
// throws ConstraintViolationException if invalid
public void ValidatedProductService(@Valid Product p) {
}

Entity Validations

Since JSR-380 annotations are not extensible, Broadleaf provides service-level validation through implementations of EntityValidator. Here is an example implementation:

public class ProductValidator implements EntityValidator {

  @Override
  public boolean supports(Class<?> clazz, ContextInfo contextInfo) {
      return Product.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(@NonNull Object businessInstance,
          @NonNull Errors errors,
          ContextInfo context) {
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "url", "URL is required");
  }
}
Tip
Tying it all together

If validation fails using any of the configured EntityValidators, the CrudEntityHelper throws a ValidationException with the validated BindingResult, which is consumed by the ApiValidationWebExceptionAdvisor

Since validate() takes in a ContextInfo, this allows you to pass that on to additional services for checks. For instance, you might want to ensure that the URL is unique:

public class ProductValidator implements EntityValidator {

    ProductService<Product> productService;

    @Autowired
    @Lazy (1)
    public void setProductService(ProductService<Product> productService) {
        this.productService = productService;
    }

    @Override
    public boolean supports(Class<?> clazz, ContextInfo contextInfo) {
        return Product.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(@NonNull Object businessInstance,
            @NonNull Errors errors,
            ContextInfo context) {
        Optional<Product> product = productService.readByUri(serviceInstance.getUri(), context);
        if (product.isPresent() && !Objects.equals(product.get().getId(), serviceInstance.getId())) {
            errors.rejectValue("uri", "uriUnique", "URIs must be unique");
        }
    }
}
  1. We lazily inject the ProductService dependency to avoid circular bean dependencies

Important
Be careful when injecting service components. Mark the injection point as @Lazy if there are circular bean definitions
Note
All of the EntityValidator beans are collected into the EntityValidatorManager and then injected into CRUDEntityHelper. Therefore to add your own custom validator, just add a bean that implements the EntityValidator interface

Each service has its own restrictions and validations on its business domain, consult each microservice docs for information on overriding and extending individual validators.