首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >简洁的HAL+JSON和JSON端点实现?

简洁的HAL+JSON和JSON端点实现?
EN

Stack Overflow用户
提问于 2022-05-10 19:20:07
回答 1查看 246关注 0票数 1

能否在Spring 2中简洁地实现单个HAL& JSON端点?目标是:

代码语言:javascript
复制
curl -v http://localhost:8091/books

返回此application/hal+json结果:

代码语言:javascript
复制
{
  "_embedded" : {
    "bookList" : [ {
      "title" : "The As",
      "author" : "ab",
      "isbn" : "A"
    }, {
      "title" : "The Bs",
      "author" : "ab",
      "isbn" : "B"
    }, {
      "title" : "The Cs",
      "author" : "cd",
      "isbn" : "C"
    } ]
  }

为此(和/或HTTP头部,因为这是一个REST ):

代码语言:javascript
复制
curl -v http://localhost:8091/books?format=application/json

要返回普通application/json结果,请执行以下操作:

代码语言:javascript
复制
[ {
  "title" : "The As",
  "author" : "ab",
  "isbn" : "A"
}, {
  "title" : "The Bs",
  "author" : "ab",
  "isbn" : "B"
}, {
  "title" : "The Cs",
  "author" : "cd",
  "isbn" : "C"
} ]

有最小的控制器代码。这些端点如预期的那样工作:

代码语言:javascript
复制
@GetMapping("/asJson")
public Collection<Book> booksAsJson() {
    return _books();
}

@GetMapping("/asHalJson")
public CollectionModel<Book> booksAsHalJson() {
    return _halJson(_books());
}

@GetMapping
public ResponseEntity<?> booksWithParam(
    @RequestParam(name="format", defaultValue="application/hal+json") 
    String format) {

    return _selectedMediaType(_books(), format);
}

@GetMapping("/asDesired")
public ResponseEntity<?> booksAsDesired() {
    return _selectedMediaType(_books(), _format());
}

与下列帮手合作:

代码语言:javascript
复制
private String _format() {
    // TODO: something clever here...perhaps Spring's content-negotiation?
    return MediaTypes.HAL_JSON_VALUE;
}

private <T> static CollectionModel<T> _halJson(Collection<T> items) {
    return CollectionModel.of(items);
}

private <T> static ResponseEntity<?> _selectedMediaType(
    Collection<T> items, String format) {

    return ResponseEntity.ok(switch(format.toLowerCase()) {
        case MediaTypes.HAL_JSON_VALUE        -> _halJson(items);
        case MediaType.APPLICATION_JSON_VALUE -> items;
        default                               -> throw _unknownFormat(format);
    });
}

但是booksWithParam实现太凌乱,无法对每个端点进行复制。是否有一种方法可以达到或接近类似于booksAsDesired实现或类似简洁的东西?

EN

回答 1

Stack Overflow用户

发布于 2022-05-11 19:12:00

告诉Spring想要支持普通JSON的一种方法是为此类媒体类型添加一个自定义转换器。这可以通过覆盖WebMvcConfigurerWebMvcConfigurer方法并在那里添加自定义转换器来完成,如下面的示例所示:

代码语言:javascript
复制
import ...PlainJsonHttpMessageConverter;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.web.config.EnableSpringDataWebSuport;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servelt.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

import javax.annotation.Nonnull;

@Configuration
@EnableSpringeDataWebSupport
public class WebMvcConfiguration implements WebMvcConfigurer {

  @Override
  public void extendMessageConverters(@Nonnull final List<HttpMessageConverter<?>> converters) {
    converters.add(new PlainJsonHttpMessageConverter());
  }
}

消息转换器本身也不是火箭科学,正如下面的PlainJsonHttpMessageConverter示例所看到的那样:

代码语言:javascript
复制
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsr310.JavaTimeModule;

import org.springframework.hateoas.RepresentationModel;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;

@Component
public class PlainJsonHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
  public PlainJsonHttpMessageConverter() {
    super(new ObjectMapper(), MediaType.APPLICATION_JSON);
    // add support for date and time format conversion to ISO 8601 and others
    this.defaultObjectMapper.registerModule(new JavaTimeModule());
    // return JSON payload in pretty format
    this.defaultObjectMapper.enable(SerializationFeature.INDENT_OUTPUT);
  }

  @Override
  protected boolean supports(@Nonnull final Class<?> clazz) {
    return RepresentationModel.class.isAssignableFrom(clazz);
  }
}

这将启用除了HAL之外的简单JSON支持,而无需在域逻辑或服务代码中进行任何进一步的分支或自定义媒体类型的特定转换。

例如,让我们以一个简单的task为例。在TaskController中,您可能有这样的代码

代码语言:javascript
复制
@GetMapping(path = "/{taskId:.+}", produces = {
  MediaTypes.HAL_JSON_VALUE,
  MediaType.APPLICATION_JSON_VALUE,
  MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE
})
public ResponseEntity<?> task(@PathVariable("taskId") String taskId,
                              @RequestParam(required = false) Map<String, String> queryParams,
                              HttpServletRequest request) {
  if (queryParams == null) {
    queryParams = new HashMap<>();
  }
  Pageable pageable = RequestUtils.getPageableForInput(queryParams);
  final String caseId = queryParams.get("caseId");
  ...

  final Query query = buildSearchCriteria(taskId, caseId, ...);
  query.with(pageable);
  List<Task> matches = mongoTemplate.find(query, Task.class);

  if (!matches.isEmpty()) {
    final Task task = matches.get(0);
    
    return ResponseEntity.ok()
        .eTag(Long.toString(task.getVersion())
        .body(TASK_ASSEMBLER.toModel(task));
  } else {
    if (request.getHeader("Accept").contains(MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE)) {
      return ResponseEntity.status(HttpStatus.NOT_FOUND)
          .contentType(MediaTypes.HTTP_PROBLEM_DETAILS_JSON)
          .body(generateNotFoundProblem(request, taskId));
    } else {
      final String msg = "No task with ID " + taskId + " found";
      throw new ResponseStatusException(HttpStatus.NOT_FOUND, msg);
    }
  }
}

它只是通过其唯一标识符检索任意任务,并根据Accept header中指定的表示返回该任务的表示形式。这里的TASK_ASSEMBLER只是一个定制的Spring RepresentationModelAssembler<Task, TaskResource>类,它通过添加某些相关内容的链接将任务对象转换为任务资源。

现在可以通过Spring测试轻松地进行测试,例如

代码语言:javascript
复制
@Test
public void halJson() throws Exception {
  given(mongoTemplate.find(any(Query.class), eq(Task.class)))
    .willReturn(setupSingleTaskList());
  final ResultActions result = mockMvc.perform(
      get("/api/tasks/taskId")
          .accept(MediaTypes.HAL_JSON_VALUE)
  );
  result.andExpect(status().isOk())
      .andExpect(content().contentType(MediaTypes.HAL_JSON_VALUE));
  // see raw payload received by commenting out below line
  // System.err.println(result.andReturn().getResponse().getContentAsString());
  verifyHalJson(result);
}

@Test
public void plainJson() throws Exception {
  given(mongoTemplate.find(any(Query.class), eq(Task.class)))
    .willReturn(setupSingleTaskList());
  final ResultActions result = mockMvc.perform(
      get("/api/tasks/taskId")
          .accept(MediaType.APPLICATION_JSON_VALUE)
  );
  result.andExpect(status().isOk())
      .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE));
  // see raw payload received by commenting out below line
  // System.err.println(result.andReturn().getResponse().getContentAsString());
  verifyPlainJson(result);
}

...

private void verifyHalJson(final ResultActions action) throws Exception {
  action.andExpect(jsonPath("taskId", is("taskId")))
      .andExpect(jsonPath("caseId", is("caseId")))
      ...
      .andExpect(jsonPath("_links.self.href", is(BASE_URI + "/tasks/taskId")))
      .andExpect(jsonPath("_links.up.href", is(BASE_URI + "/tasks")));
}

rivate void verifyPlainJson(final ResultActions action) throws Exception {
  action.andExpect(jsonPath("taskId", is("taskId")))
      .andExpect(jsonPath("caseId", is("caseId")))
      ...
      .andExpect(jsonPath("links[0].rel", is("self")))
      .andExpect(jsonPath("links[0].href", is(BASE_URI + "/tasks/taskId")))
      .andExpect(jsonPath("links[1].rel", is("up")))
      .andExpect(jsonPath("links[1].href", is(BASE_URI + "/tasks")));
}

注意这里的链接是如何以不同的方式呈现的,这取决于您所选择的媒体类型。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/72191943

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档