Spring Projects Best Practices (Part 1)
In this series on best practices in Spring, we start with ways to write less boilerplate code, tips on REST API error handling, and annotation-based validation.
Join the DZone community and get the full member experience.
Join For FreeSpring is one of the most popular Java frameworks. In this article, I will be covering some of my learnings in Spring, specifically oriented towards web applications and Spring Boot.
Spring Framework makes developers' lives easy when building production-ready code. Adopting these best practices will help us write less and produce manageable code with low maintenance and fewer bugs.
Writing Less Boilerplate Code
Modern frameworks adopt the principle of avoiding boilerplate code. To that end, we can use Lombok to decorate Java POJOs. Lombok is a small library and is used as a boilerplate code generator for getters, setters, equals, hash codes, etc., and it also creates builder classes for constructing large objects.
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString(exclude = "id")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NonNull
private String line1;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String line2;
private String city;
private String state;
private String zip;
}
// usage
Address.builder().line1("123 Main Street").city("Mountain View")
.state("CA")
.zip("94043")
.build();
The Eclipse editor is compatible with Lombok. Simply install the Lombok plugin and we are good to go.
Error Handling for REST APIs
Spring provides a standard way to handle exceptions and errors with a standard response. Errors are very important in REST APIs. They inform clients that something went wrong, after all. Fortunately, handling errors is fairly easy in Spring — and it brings transparency to APIs.
Status Code Error Mapping
- 2xx: success – everything worked fine.
- 4xx: client error – the client did something wrong (e.g. the client sends an invalid request or he is not authorized).
- 5xx: server error – the server did something wrong (e.g. error while trying to process the request).
Consider the HTTP status codes on Wikipedia. However, be aware that using all of them could be confusing for the users of your API. Keep the set of used HTTP status codes small. It’s common to use the following:
Send meaningful error messages along with the code. Spring provides a handy POJO for this: The Vnd Error Class (application/vnd.error+json / application/vnd.error+xml):
package org.springframework.hateoas;
@XmlRootElement(name = "errors")
public class VndErrors implements Iterable < VndErrors.VndError > {
@XmlElement(name = "error") //
private final List < VndError > vndErrors;
...
...
@XmlType
public static class VndError extends ResourceSupport {
@XmlAttribute @JsonProperty private final String logref;
@XmlElement @JsonProperty private final String message;
...
...
}
}
application / vnd.error + json
{
"message": "Validation failed",
"logref": 42,
"_links": {
"describes": {
"href": "http://path.to/describes",
"title": "Error Description"
},
"help": {
"href": "http://path.to/help",
"title": "Error Information"
}
}
}
Furthermore, override ResponseEntityExceptionHandler to handle different sorts of errors. For example: Bad Request for inputting invalid input.
@ControllerAdvice
@RequestMapping(produces = "application/vnd.error+json")
@ResponseBody
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity < VndErrors > handleConstraintViolation(ConstraintViolationException ex) {
List < String > errors = ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());
return new ResponseEntity < > (error(ex, errors), HttpStatus.BAD_REQUEST);
}
private < E extends Exception > VndErrors error(Exception e, List < String > errors) {
String msg = e.getClass().getSimpleName();
StringBuilder sb = new StringBuilder();
if (errors != null && errors.size() > 0) {
for (String error: errors) {
sb.append(error).append(" ");
}
}
return new VndErrors(sb.toString(), msg);
}
}
Here's a quick reference for REST standards: Error Codes and Responses — Twitter Developers.
Annotation-Based Validation (JSR 303)
This also promotes both the "write less" and "separation of concerns" philosophies. Simply annotating properties will enable basic validations.
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Length;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Customer {
public static enum Gender {
MALE,
FEMALE;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Size(min = 1, max = 64)
private String firstName;
@Length(min = 10, max = 10)
private String lastName;
@Pattern(regexp = "(^[1-9]{1}[0-9]{9}$)|(^$)", message = "Invalid Phone Number format")
private String phoneNumber;
private Gender gender;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
private Address address;
}
import javax.validation.Valid;
import javax.validation.constraints.Size;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Validated
public class CustomerController {
@RequestMapping(value = "/",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE,
method = RequestMethod.POST)
public HttpEntity < String > create(@Valid @NotNull @RequestBody Customer customer) {
...
}
@RequestMapping(value = "/someurl",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE,
method = RequestMethod.POST)
public HttpEntity < String > someMethod(@Valid @Size(max = 100, message = "path cannot be greater than 100 chars") @RequestParam String path) {
...
}
}
We can even write our custom annotation (either new or combining multiples) by implementing ConstraintValidator, enhancing the separation of concerns:
@Constraint(validatedBy = FooTypeConstraintValidator.class)
@Target({
ElementType.TYPE
})
@Retention(RetentionPolicy.RUNTIME)
public @interface FooType {
String message() default "some default message";
Class << ? > [] groups() default {};
Class << ? extends Payload > [] payload() default {};
}
@Component
public class FooTypeConstraintValidator
implements ConstraintValidator < FooType, Foo > {
@Override
public void initialize(FooType constraintAnnotation) {}
@Override
public boolean isValid(Foo foo, ConstraintValidatorContext context) {
// custom logic
}
}
Spring provides a more declarative way of writing custom validators. Use as needed.
public class FooValidator implements org.springframework.validation.Validator {
@Override
public boolean supports(Class << ? > arg0) {
return Foo.class.equals(arg0);
}
@Override
public void validate(Object arg0, Errors arg1) {
Foo fooObject = (Foo) arg0;
// some custom logic..
}
}
// usage
autowire FooValidator
and then in your method: -
fooValidator.validate(fooObject, bindingResult);
if (bindingResult.hasErrors()) {
// take action
}
In Part 2, I will cover Spring Data REST, managing beans using conditional annotations, making use of actuator beans (health, metrics, etc.) to verify the integrity of a distributed system, and some cool features about dev tools.
Opinions expressed by DZone contributors are their own.
Comments