本期的「译见」,让作者带领您将 REST 控制器添加到领域控制模型的顶端。
在上期的「译见」系列文章《译见|构建用户管理微服务(三):实现和测试存储库》中,我们了解数据访问层和存储库实现的方法,而在此之前,领域模型无需依赖于任何框架特定的类或其他要素,今天就让我们将 REST 控制器添加到领域控制模型的顶端。
有关 REST | 船长导语
REST, 全称是 Resource Representational State Transfer(Resource 被省略掉了)。通俗来讲就是:资源在网络中以某种表现形式进行状态转移。在 web 平台上,REST 就是选择通过使用 http 协议和 uri,利用 client/server model 对资源进行 CRUD (Create/Read/Update/Delete) 增删改查操作。
使用 REST 结构风格是因为,随着时代的发展,传统前后端融为一体的网页模式无法满足需求,而 RESTful 可以通过一套统一的接口为 Web,iOS 和 Android 提供服务。另外对于广大平台来说,比如 Facebook platform,微博开放平台,微信公共平台等,他们需要一套提供服务的接口,于是 RESTful 更是它们最好的选择。
REST 端点的支撑模块
我经手的大多数项目,都需要对控制器层面正确地进行 Spring MVC 的配置。随着近几年单页应用程序的广泛应用,越来越不需要在 Spring mvc 应用程序中配置和开发视图层 (使用 jsp 或模板引擎)。
现在,创建完整的 REST 后端的消耗并生成了 JSON 是相当典型的, 然后通过 SPA 或移动应用程序直接使用。基于以上所讲, 我收集了 Spring MVC 常见配置,这能实现对后端的开发。
- Jackson 用于生成和消解 JSON
- application/json 是默认的内容类型
- ObjectMapper 知道如何处理 Joda 和 JSR-310 日期/时间 api, 它在 iso 格式中对日期进行序列化, 并且不将缺省的值序列化 (NON_ABSENT)
- ModelMapper 用于转换为 DTO 和模型类
- 存在一个自定义异常处理程序, 用于处理 EntityNotFoundException 和其他常见应用程序级别的异常
- 捕获未映射的请求并使用以前定义的错误响应来处理它们
能被重新使用的常见 REST 配置项目
该代码在 github, 有一个新的模块 springuni-commons-rest , 它包含实现 REST 控制器所需的所有常用的实用程序。 专有的 RestConfiguration 可以通过模块进行扩展, 它们可以进一步细化默认配置。
错误处理
正常的 web 应用程序向最终用户提供易于使用的错误页。但是,对于一个纯粹的 JSON-based REST 后端, 这不是一个需求, 因为它的客户是 SPA 或移动应用。
因此, 最好的方法是用一个明确定义的 JSON 结构 (RestErrorResponse) 前端可以很容易地响应错误, 这是非常可取的。
@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()); } }
以上代码将返回 HTTP 错误代码,包括 HTTP 错误的文本表示和对客户端的详细信息,RestErrorHandler 负责生成针对应用程序特定异常的正确响应。
@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); } }
处理未响应请求
为了处理未映射的请求, 首先我们需要定义一个默认处理程序, 然后用 RequestMappingHandlerMapping 来设置它。
@Controller public class DefaultController { @RequestMapping public ResponseEntity<RestErrorResponse> handleUnmappedRequest(final HttpServletRequest request) { return ResponseEntity.status(NOT_FOUND).body(RestErrorResponse.of(NOT_FOUND)); } }
经过这样的设置,RestConfiguration 在一定程度上扩展了 WebMvcConfigurationSupport, 这提供了用于调用 MVC 基础结构的自定义钩子。
@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; } }
我们在这之后所要做的,就是重写 createRequestMappingHandlerMapping () 并在预先创建的 RequestMappingHandlerMapping 上使用 setDefaultHandler ()。
用于管理用户的 REST 端点
在第一部分中,我定义了一堆用于和用户管理服务进行交互的 REST 风格的端点。而实际上, 他们与用 Spring MVC 创建 REST 风格的端点相比,并没有什么特别的。但是,我有一些最近意识到的小细节想要补充。
- 正如 Spring 4.3 有一堆用于定义请求处理程序的速记注释,@GetMapping 是一个组合的注释, 它为 @RequestMapping (method = RequestMethod. GET) 作为其对应的 @PostMapping、@PutMapping 等的快捷方式。
- 我找到了一个用于处理从/到模型类转换的 DTO 的模块映射库 。在此之前,我用的是 Apache Commons Beanutils。
- 手动注册控制器来加快应用程序初始化的速度。正如我在第三部分中提到的, 这个应用程序将托管在谷歌应用引擎标准环境中,而开启一个新的实例是至关重要的。
@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()); } ... }
将 DTO 映射到模型类
虽然 ModelMapper 在查找匹配属性时是相当自动的, 但在某些情况下需要进行手动调整。比如说,用户的密码。这是我们绝对不想暴露的内容。
通过定义自定义属性的映射, 可以很容易地避免这一点。
import org.modelmapper.PropertyMap; public class UserMap extends PropertyMap<User, UserDto> { @Override protected void configure() { skip().setPassword(null); } }
当 ModelMapper 的实例被创建时, 我们可以自定义属性映射、转换器、目标值提供程序和一些其他的内容。
@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()); } ... }
测试 REST 控制器
自 MockMvc 在 Spring 3.2 上推出以来, 使用 Spring mvc 测试 REST 控制器变得非常容易。
@RunWith(SpringJUnit4ClassRunner) @ContextConfiguration(classes = [AuthRestTestConfiguration]) @WebAppConfigurationclass 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@springuni.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@springuni.com"))) .andDo(print()) verify(userService).getUser(1L) verifyNoMoreInteractions(userService) } ... }
有两种方式能让 MockMvc 与 MockMvcBuilders 一起被搭建。 一个是通过 web 应用程序上下文 (如本例中) 来完成, 另一种方法是向 standaloneSetup () 提供具体的控制器实例。我使用的是前者,当 Spring Security得到配置的时候,测试控制器显得更为合适。
下期预告:构建用户管理微服务(五):使用 JWT 令牌和 Spring Security 来实现身份验证
原文链接:https://www.springuni.com/user-management-microservice-part-4
未经允许不得转载:DaoCloud道客博客 » 译见|构建用户管理微服务(四):实现 REST 控制器