Spring boot(六)之Spring HATEOAS构建超媒体驱动的 RESTful Web 服务

涂溪叠
2023-12-01

Springboot(六)之Spring HATEOAS构建超媒体驱动的 RESTful Web 服务

前言
超媒体是 REST 的一个重要方面。它让你构建服务,在很大程度上解耦客户端和服务器,让它们独立发展。为 REST 资源返回的表示不仅包含数据,还包含指向相关资源的链接。因此,表示的设计对于整体服务的设计至关重要。服务处理GET请求时,Json响应如下

{
  "_embedded": {
    "ingredients": [
      {
        "name": "面皮",
        "type": "necessities",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/1"
          }
        }]
      },
    "_links": {
    "recent": {
      "href": "http://localhost:8082/test/recent"
    }
  }
}

响应已经表明您可以使用name查询字符串中的可选参数自定义问候语,
这种风格的HATEOAS被称为HAL,是一种在JSON响应中嵌套了超链接的简单的格式,"_links"属性为用户提供导航API的超链接。

1.添加超链接
spring HATEOAS项目为Spring 提供了超链接的支持。添加相关依赖,

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

Spring HATEOAS提供了两个主要的类型来表示超链接资源:EntityModel和CollectionModel。EntityModel代表一个资源,CollectionModel代表资源的集合。
首先第一步为资源添加超链接

@GetMapping("/recent")
    public CollectionModel<IngredientsResource> halIngredients(){
    	//在前几章中写过findAll(),查询数据库中所有的Ingredients,以列表形式返回
        List<Ingredients> ingredientsList = ingredientsRepository.findAll();
        CollectionModel<EntityModel<Ingredients>> collectionModel = CollectionModel.wrap(ingredientsList);
       collectionModel.add(
                new Link("http://localhost:8082/query/recent","recent"));
        return collectionModel;
    }

使用CollectionModel.wrap()将IngredientsList包装成 CollectionModel<EntityModel>,并且在资源的集合添加了名为recent的关联关系,这样请求API时,JSON就会包含以下片段

  "_links": {
    "recent": {
      "href": "http://localhost:8082/test/recent"
    }
  }

但只是为资源的集合添加了超链接,并没有将每一个元素添加相关的超链接。
所以要进行第二步:
2.创建资源装配器
1.创建资源类型

//携带链接的资源类型
public class IngredientsResource extends RepresentationModel<IngredientsResource> {

    @Getter
    private final String name;

    @Getter
    private final String type;

    public IngredientsResource(Ingredients ingredients) {
        this.name = ingredients.getName();
        this.type = ingredients.getType();
    }
}

这个资源类型和ingredients领域类型没有太多区别,就是没有包含ingredients的id属性。这是因为必要在API中暴露数据库相关的API,对于客户端来说,资源的self链接会作为该资源的标识符。有一个简单的构造器,可以将ingredients对象转换成IngredientsResource 对象。
为了将ingredients对象转换成IngredientsResource 对象,2.需要创建一个资源装配器

public class IngredientsResourceAssembler extends
        RepresentationModelAssemblerSupport<Ingredients,IngredientsResource> {

    public IngredientsResourceAssembler(Class<?> controllerClass, Class<IngredientsResource> resourceType) {
        super(controllerClass, resourceType);
    }

    @Override
    protected IngredientsResource instantiateModel(Ingredients ingredients) {
        return new IngredientsResource(ingredients);//只是为了实列化	EntityModel对象
    }


    @Override
    public IngredientsResource toModel(Ingredients entity) {
        return createModelWithId(entity.getId(),entity);//不仅创建EntityModel对象,还为其填充链接。
		//设置一个self链接,这个链接的URL是根据对象的id来衍生出来的
    }
}

3.最后可以调整一下halIngredients(),让它使用IngredientsResourceAssembler 。

@GetMapping("/recent")
    public CollectionModel<IngredientsResource> halIngredients(){
        List<Ingredients> ingredientsList = ingredientsRepository.findAll();
        CollectionModel<IngredientsResource> collectionModel =
                new IngredientsResourceAssembler(HateoasController.class, IngredientsResource.class).toCollectionModel(ingredientsList);
        collectionModel.add(
                new Link("http://localhost:8082/query/recent","recent"));

        return collectionModel;
    }

最后JSON结果如下:

{
  "_embedded": {
    "ingredients": [
      {
        "name": "面皮",
        "type": "necessities",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/1"
          }
        }
      },
      {
        "name": "鸡肉",
        "type": "meat",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/2"
          }
        }
      },
      {
        "name": "玉米",
        "type": "vegetables",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/3"
          }
        }
      },
      {
        "name": "醋",
        "type": "seasoning",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/4"
          }
        }
      },
      {
        "name": "虾蟹",
        "type": "meat",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/5"
          }
        }
      },
      {
        "name": "酸汤",
        "type": "seasoning",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/6"
          }
        }
      },
      {
        "name": "油",
        "type": "seasoning",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/7"
          }
        }
      },
      {
        "name": "猪肉",
        "type": "meat",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/8"
          }
        }
      },
      {
        "name": "白菜",
        "type": "vegetables",
        "_links": {
          "self": {
            "href": "http://localhost:8082/query/9"
          }
        }
      }
    ]
  },
  "_links": {
    "recent": {
      "href": "http://localhost:8082/test/recent"
    }
  }
}

相关资料:Spring 官方文档
将Spring实战第5版中HATEOAS部分代码迁移到Spring HATEOAS 1.0

 类似资料: