使用 Spring Data REST 实现简单的超媒体服务

认识 HAL

HAL

  • Hypertext Application Language(全称)
  • HAL 是一种简单的格式,为 API 中的资源提供简单一致的链接

是一种基于json的扩展
HAL 模型

  • 链接的资源
  • 内嵌资源
  • 资源的状态

Spring Data REST

Spring Boot 依赖

  • spring-boot-starter-data-rest

常用注解与类

  • @RepositoryRestResource
    对于URI做相关的定制
  • Resource<T>
    T类型的资源
  • PagedResource<T>
    有分页的资源

例子

目录结构如下:
在这里插入图片描述

<!-- 增加Jackson的Hibernate类型支持 -->
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-hibernate5</artifactId>
			<version>2.9.8</version>
		</dependency>
		<!-- 增加Jackson XML支持 -->
		<dependency>
			<groupId>com.fasterxml.jackson.dataformat</groupId>
			<artifactId>jackson-dataformat-xml</artifactId>
			<version>2.9.0</version>
		</dependency>

@SpringBootApplication
@EnableCaching
public class WaiterServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(WaiterServiceApplication.class, args);
	}
	@Bean
	public Hibernate5Module hibernate5Module() {
		return new Hibernate5Module();//做一个Hibernate5的支持
	}

	@Bean
	public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
		return builder -> {
			builder.indentOutput(true);//输出的json是有缩进的
			builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));//设置时区
		};
	}
}

@RepositoryRestResource(path = "/coffee")//定制URI
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
    List<Coffee> findByNameInOrderById(List<String> list);
    Coffee findByName(String name);
}

结果

首先,我们访问http://localhost:8080/在这里插入图片描述
由上图我们可以看出,它生成一个类似于目录的页面,代码没有实现,这个是Spring Data REST
为我们提供的。

进入 http://localhost:8080/coffee/
在这里插入图片描述

我们可以看到 每个coffee 都有具体的链接信息(在link中) 还有 分页的信息 各种对于咖啡的操作在这里插入图片描述

使用http://localhost:8080/coffee?page=0&size=3&sort=id,desc 做一个降序的分页查询 一页为3个 当前页为第0页
在这里插入图片描述

使用http://localhost:8080/coffee/search 列出 所有的查询列表
在这里插入图片描述
这两个就是当时 在 CoffeeRepository 中定义的两个方法 让我们来使用一下

使用http://localhost:8080/coffee/search/findByName?name=mocha,查询mocha
在这里插入图片描述
使用http://localhost:8080/coffee/search/findByNameInOrderById?list=mocha,latte,进行多项查询
在这里插入图片描述

使用客户端如何访问 HATEOAS 服务

配置 Jackson JSON

  • 注册 HAL 支持

操作超链接

  • 让客户端通过目录找到需要的 Link
  • 通过webclient或者Repository访问超链接

客户端例子

目录结构如下:
在这里插入图片描述

CustomerServiceApplication代码:

@SpringBootApplication
@Slf4j
public class CustomerServiceApplication {

	public static void main(String[] args) {
		new SpringApplicationBuilder()
				.sources(CustomerServiceApplication.class)
				.bannerMode(Banner.Mode.OFF)
				.web(WebApplicationType.NONE)
				.run(args);
	}

	@Bean
	public Jackson2HalModule jackson2HalModule() { //注册Hal
		return new Jackson2HalModule();
	}

	@Bean
	public HttpComponentsClientHttpRequestFactory requestFactory() {
		PoolingHttpClientConnectionManager connectionManager =
				new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);//构造一个连接池的连接管理器 生命周期30s
		connectionManager.setMaxTotal(200);//最大保持连接数
		connectionManager.setDefaultMaxPerRoute(20);//设置每个路由的最大连接

		CloseableHttpClient httpClient = HttpClients.custom()//定制HttpClients
				.setConnectionManager(connectionManager) //设置连接管理器
				.evictIdleConnections(30, TimeUnit.SECONDS)//空闲连接退出时间
				.disableAutomaticRetries()//关闭自动重试  重试:请求处理的时候,系统进行了处理,但是返回响应的时候,没有把响应返回,客户端,会认为该操作没有成功,会再次尝试。这对于打款之类的敏感操作是有问题的
				// 有 Keep-Alive 认里面的值,没有的话永久有效
				//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)  也可以使用官方提供的请求策略
				// 换成自定义的
				.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())//设置请求策略
				.build();

		HttpComponentsClientHttpRequestFactory requestFactory =
				new HttpComponentsClientHttpRequestFactory(httpClient);//使用httpClient 构造请求工厂

		return requestFactory;


	}

	@Bean
	public RestTemplate restTemplate(RestTemplateBuilder builder) {
		return builder
				.setConnectTimeout(Duration.ofMillis(100))
				.setReadTimeout(Duration.ofMillis(500))
				.requestFactory(this::requestFactory)
				.build();
	}
}

CustomerRunner代码;

@Component
@Slf4j
public class CustomerRunner implements ApplicationRunner {
    private static final URI ROOT_URI = URI.create("http://localhost:8080/");
    @Autowired
    private RestTemplate restTemplate;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Link coffeeLink = getLink(ROOT_URI,"coffees"); //从ROOT_URI根URI 获取有关coffee的link
        readCoffeeMenu(coffeeLink);//获取所有coffee信息
        Resource<Coffee> americano = addCoffee(coffeeLink);//添加一杯美式咖啡

        Link orderLink = getLink(ROOT_URI, "coffeeOrders");
        addOrder(orderLink, americano);//添加订单
        queryOrders(orderLink);//取出订单
    }

    private Link getLink(URI uri, String rel) {
        ResponseEntity<Resources<Link>> rootResp =
                restTemplate.exchange(uri, HttpMethod.GET, null,
                        new ParameterizedTypeReference<Resources<Link>>() {});
        Link link = rootResp.getBody().getLink(rel);
        log.info("Link: {}", link);
        return link;
    }

    private void readCoffeeMenu(Link coffeeLink) {
        ResponseEntity<PagedResources<Resource<Coffee>>> coffeeResp =  //超媒体分页Spring HATEOAS有一个PagedResources类, 他丰富了Page实体以及一些让用户更容易导航到资源的请求方式。Page转换到PagedResources是由一个实现了Spring HATEOASResourceAssembler接口的实现类:PagedResourcesAssembler提供转换的  使用Resource 做一个资源的缓存 转换  ResponseEntity可以定义返回的HttpStatus(状态码)和HttpHeaders(消息头:请求头和响应头)
                restTemplate.exchange(coffeeLink.getTemplate().expand(),//取出这个http头
                        HttpMethod.GET, null,         //get操作 无实体
                        new ParameterizedTypeReference<PagedResources<Resource<Coffee>>>() {});//new ParameterizedTypeReference() {} 就是通过定义一个匿名内部类的方式来获得泛型信息,从而进行反序列化的工作。   取出返回的所有咖啡放入coffeeResp中
        log.info("Menu Response: {}", coffeeResp.getBody());
    }

    private Resource<Coffee> addCoffee(Link link) {
        Coffee americano = Coffee.builder()
                .name("americano")
                .price(Money.of(CurrencyUnit.of("CNY"), 25.0))
                .build();
        RequestEntity<Coffee> req =
                RequestEntity.post(link.getTemplate().expand()).body(americano); //将新coffee绑定到这个URI上
        ResponseEntity<Resource<Coffee>> resp =
                restTemplate.exchange(req,
                        new ParameterizedTypeReference<Resource<Coffee>>() {});
        log.info("add Coffee Response: {}", resp);
        return resp.getBody();
    }

    private void addOrder(Link link, Resource<Coffee> coffee) {
        CoffeeOrder newOrder = CoffeeOrder.builder()
                .customer("Li Lei")
                .state(OrderState.INIT)
                .build();
        RequestEntity<?> req =
                RequestEntity.post(link.getTemplate().expand()).body(newOrder); //post操作
        ResponseEntity<Resource<CoffeeOrder>> resp =
                restTemplate.exchange(req,
                        new ParameterizedTypeReference<Resource<CoffeeOrder>>() {});
        log.info("add Order Response: {}", resp);

        Resource<CoffeeOrder> order = resp.getBody();
        Link items = order.getLink("items");
        req = RequestEntity.post(items.getTemplate().expand()).body(Collections.singletonMap("_links", coffee.getLink("self")));   // 可以使用http链接 去代替加入的咖啡

        ResponseEntity<String> itemResp = restTemplate.exchange(req, String.class);
        log.info("add Order Items Response: {}", itemResp);
    }

    private void queryOrders(Link link) {
        ResponseEntity<String> resp = restTemplate.getForEntity(link.getTemplate().expand(), String.class);
        log.info("query Order Response: {}", resp);
    }
}

结果

分析

从ROOT_URI根URI 获取有关coffee的link时,rel的填写,只能填写服务端 方法的url名加个s 换成其他的(服务器修改为aab 客户端改为aabs) 都会报错 故 此处 写法固定。此处 问题 因为 rel在服务器就写了
所以 我们需要使用服务端的rel来

 req = RequestEntity.post(items.getTemplate().expand()).body(Collections.singletonMap("_links", coffee.getLink("self")));   // 可以使用http链接 去代替加入的咖啡

的用法 用postman展示
在这里插入图片描述
在放入咖啡的时候,可以用此咖啡的地址 代替

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值