In the previous part the data access layer along with the repositories were implemented, before that the domain model without having to rely on any framework specific class or feature and now time has come to add REST controllers on the top of that.
In most of the projects I had worked on, there was a need to configure Spring MVC correctly for the controller layer. As Single Page Applications had become wide-spread in recent years, it was less and less frequent that I had to configure and develop a view layer (with JSPs or a with a template engine) within a Spring MVC app.
Nowadays, it’s rather typical to create fully REST back-ends consuming and producing JSON, which are then used by SPAs or mobile apps directly. All that said, I gathered the commons requirements of Spring MVC configuration that makes it possible to develop such back-ends.
The code is now on GitHub and there’s a new module springuni-commons-rest which has all those common utilities one might need for implementing REST controllers. Specialized RestConfiguration can be extended by modules and they can further refine the default configuration.
Normal web applications present an easily consumable error page to end users. For a pure JSON-based REST back-end however this is not a requirement, as its clients are SPA or mobile apps.
For that reason, it’s desirable to respond to errors with a well-defined JSON structure (RestErrorResponse) front-ends can easily understand.
@Data
public class RestErrorResponse {
private final int statusCode;
private final String reasonPhrase;
private final String detailMessage;
protected RestErrorResponse(HttpStatus status, String detailMessage) {
statusCode = status.value();
reasonPhrase = status.getReasonPhrase();
this.detailMessage = detailMessage;
}
public static RestErrorResponse of(HttpStatus status) {
return of(status, null);
}
public static RestErrorResponse of(HttpStatus status, Exception ex) {
return new RestErrorResponse(status, ex.getMessage());
}
}
This one returns the HTTP error code, the textual representation of the HTTP error and a detail message to clients and RestErrorHandler takes care of generation the right response for application specific exceptions.
@RestControllerAdvice
public class RestErrorHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ApplicationException.class)
public ResponseEntity<Object> handleApplicationException(final ApplicationException ex) {
return handleExceptionInternal(ex, BAD_REQUEST);
}
@ExceptionHandler(EntityAlreadyExistsException.class)
public ResponseEntity<Object> handleEntityExistsException(final EntityAlreadyExistsException ex) {
return handleExceptionInternal(ex, BAD_REQUEST);
}
@ExceptionHandler(EntityConflictsException.class)
public ResponseEntity<Object> handleEntityConflictsException(final EntityConflictsException ex) {
return handleExceptionInternal(ex, CONFLICT);
}
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Object> handleEntityNotFoundException(final EntityNotFoundException ex) {
return handleExceptionInternal(ex, NOT_FOUND);
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Object> handleRuntimeException(final RuntimeException ex) {
return handleExceptionInternal(ex, INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UnsupportedOperationException.class)
public ResponseEntity<Object> handleUnsupportedOperationException(
final UnsupportedOperationException ex) {
return handleExceptionInternal(ex, NOT_IMPLEMENTED);
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
RestErrorResponse restErrorResponse = RestErrorResponse.of(status, ex);
return super.handleExceptionInternal(ex, restErrorResponse, headers, status, request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception ex, HttpStatus status) {
return handleExceptionInternal(ex, null, null, status, null);
}
}
In order to be able to handle unmapped requests, first we need to define a default handler and then set it up with RequestMappingHandlerMapping.
@Controller
public class DefaultController {
@RequestMapping
public ResponseEntity<RestErrorResponse> handleUnmappedRequest(final HttpServletRequest request) {
return ResponseEntity.status(NOT_FOUND).body(RestErrorResponse.of(NOT_FOUND));
}
}
RestConfiguration extends WebMvcConfigurationSupport, which provides customization hooks for fine tuning the MVC infrastructure.
@EnableWebMvc
@Configuration
public class RestConfiguration extends WebMvcConfigurationSupport {
...
protected Object createDefaultHandler() {
return new DefaultController();
}
...
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = super.createRequestMappingHandlerMapping();
Object defaultHandler = createDefaultHandler();
handlerMapping.setDefaultHandler(defaultHandler);
return handlerMapping;
}
}
All we have to do after that is to override createRequestMappingHandlerMapping() and use setDefaultHandler() on the pre-created RequestMappingHandlerMapping.
In part 1, I defined a bunch of restful endpoints for interacting with this user management service. In essence there’s nothing really special about creating RESTful endpoints with Spring MVC.
There are however some tiny details I’ve just recently realized.
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
private final ModelMapper modelMapper;
public UserController(ModelMapper modelMapper, UserService userService) {
this.modelMapper = modelMapper;
this.userService = userService;
}
@GetMapping("/{userId}")
public UserDto getUser(@PathVariable long userId) throws ApplicationException {
User user = userService.getUser(userId);
return modelMapper.map(user, UserDto.class);
}
...
@PostMapping
public void createUser(@RequestBody @Validated UserDto userDto) throws ApplicationException {
User user = modelMapper.map(userDto, User.class);
userService.signup(user, userDto.getPassword());
}
...
}
Although ModelMapper is fairly automatic at finding matching properties, there are situations when it need some manual tweaking. One example for that is the user’s password. This is something we definitely don’t want to expose.
By defining custom property maps, this can be avoided easily.
import org.modelmapper.PropertyMap;
public class UserMap extends PropertyMap<User, UserDto> {
@Override
protected void configure() {
skip().setPassword(null);
}
}
When the ModelMapper instance gets created at that time, custom property mapping, converters, destination value providers and a couple of other things can be customized.
@Configuration
@EnableWebMvc
public class AuthRestConfiguration extends RestConfiguration {
...
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
customizeModelMapper(modelMapper);
modelMapper.validate();
return modelMapper;
}
@Override
protected void customizeModelMapper(ModelMapper modelMapper) {
modelMapper.addMappings(new UserMap());
modelMapper.addMappings(new UserDtoMap());
}
...
}
Testing REST controllers with Spring MVC has been made easy since MockMvc was introduced in Spring 3.2.
@RunWith(SpringJUnit4ClassRunner)
@ContextConfiguration(classes = [AuthRestTestConfiguration])
@WebAppConfiguration
class UserControllerTest {
@Autowired
WebApplicationContext context
@Autowired
UserService userService
MockMvc mockMvc
@Before
void before() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build()
reset(userService)
when(userService.getUser(0L)).thenThrow(NoSuchUserException)
when(userService.getUser(1L))
.thenReturn(new User(1L, "test", "test@craftingjava.com"))
}
@Test
void testGetUser() {
mockMvc.perform(get("/users/1").contentType(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("id", is(1)))
.andExpect(jsonPath("screenName", is("test")))
.andExpect(jsonPath("contactData.email", is("test@craftingjava.com")))
.andDo(print())
verify(userService).getUser(1L)
verifyNoMoreInteractions(userService)
}
...
}
There are two ways how MockMvc can be built with
MockMvcBuilders.
One is to do it through a web app context (as in this example) and the other way is to supply concrete controller
instances to standaloneSetup().
I went for the former, because that’s a better fit for testing controllers once Spring Security gets integrated in
the next part.
Building a user management microservice (Part 5): Authentication with JWT tokens and Spring Security
If you like Java and Spring as much as I do, sign up for my newsletter.