译见|构建用户管理微服务(四):实现 REST 控制器

512 阅读7分钟
原文链接: mp.weixin.qq.com

在上期的「译见」系列文章《译见|构建用户管理微服务(三):实现和测试存储库》中,我们了解数据访问层和存储库实现的方法,而在此之前,领域模型无需依赖于任何框架特定的类或其他要素,今天就让我们将 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



☟ 点击查看 上期文章