Spring RestTemplate 异常:“Not enough variables available to expand”

1、概览

本文将带你了解 Spring RestTemplate 抛出 IllegalArgumentException: Not enough variables available to expand 异常的原因以及解决办法。

2、原因

简而言之,当试图在 GET 请求参数中发送 JSON 数据时,通常会导致这个异常。

RestTemplate 提供了 getForObject 方法,通过在指定的 URL 上发出 GET 请求来获取表示对象。

出现异常的主要原因是 RestTemplate 将大括号中封装的 JSON 数据视为 URI 变量的占位符。

由于没有为预期的 URI 变量提供任何值,getForObject 方法就会抛出异常。

例如,尝试发送 {"name": "HP EliteBook"} 作为查询参数:

String url = "http://products.api.com/get?key=a123456789z&criterion={\"name\":\"HP EliteBook\"}";
Product product = restTemplate.getForObject(url, Product.class);

将导致 RestTemplate 抛出异常:

java.lang.IllegalArgumentException: Not enough variable values available to expand 'name'

3、示例应用

创建一个只有一个 GET 端点的基本 REST API 示例,来复现 RestTemplate 抛出 IllegalArgumentException 异常的情况。

首先,创建 Product Model 类:

public class Product {

    private int id;
    private String name;
    private double price;

    // 构造函数、get、set 方法省略 
}

接着,定义一个 Spring Controller 来封装 REST API 的逻辑:

@RestController
@RequestMapping("/api")
public class ProductApi {

    private List<Product> productList = new ArrayList<>(Arrays.asList(
      new Product(1, "Acer Aspire 5", 437), 
      new Product(2, "ASUS VivoBook", 650), 
      new Product(3, "Lenovo Legion", 990)
    ));

    @GetMapping("/get")
    public Product get(@RequestParam String criterion) throws JsonMappingException, JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        Criterion crt = objectMapper.readValue(criterion, Criterion.class);
        if (crt.getProp().equals("name")) {
            return findByName(crt.getValue());
        }

        // 按其他属性(id、price)检索

        return null;
    }

    private Product findByName(String name) {
        for (Product product : this.productList) {
            if (product.getName().equals(name)) {
                return product;
            }
        }
        return null;
    }

    // 其他方法
}

4、示例应用说明

Handler 方法 get() 根据指定的 Criterion 检索 Product 对象。

Criterion 可以表示为一个 JSON 字符串,其中有两个 Key:propvalue

prop 指的是 Product 属性,因此可以是 idname 称或 price

如上所示,Criterion 作为字符串参数传递给 Handler 方法。然后,使用 ObjectMapper 类将 JSON 字符串转换为 Criterion 对象。

Criterion 类如下:

public class Criterion {

    private String prop;
    private String value;

    // 构造函数、get、set 方法省略
}

最后,尝试向映射到 Handler 方法 get() 的 URL 发送 GET 请求。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { RestTemplate.class, RestTemplateExceptionApplication.class })
public class RestTemplateExceptionLiveTest {

    @Autowired
    RestTemplate restTemplate;

    @Test(expected = IllegalArgumentException.class)
    public void givenGetUrl_whenJsonIsPassed_thenThrowException() {
        String url = "http://localhost:8080/spring-rest/api/get?criterion={\"prop\":\"name\",\"value\":\"ASUS VivoBook\"}";
        Product product = restTemplate.getForObject(url, Product.class);
    }
}

单元测试会抛出 IllegalArgumentException 异常,因为我们试图传递 {"prop": "name", "value": "ASUS VivoBook"} 作为 URL 的一部分。

5、解决办法

根据经验,应始终使用 POST 请求来发送 JSON 数据。

虽然不推荐使用 GET,但一个可行的解决方案是定义一个包含 criterionString 对象,并在 URL 中提供一个真正的 URI 变量。

@Test
public void givenGetUrl_whenJsonIsPassed_thenGetProduct() {
    String criterion = "{\"prop\":\"name\",\"value\":\"ASUS VivoBook\"}";
    String url = "http://localhost:8080/spring-rest/api/get?criterion={criterion}";
    Product product = restTemplate.getForObject(url, Product.class, criterion);

    assertEquals(product.getPrice(), 650, 0);
}

接着,来看看使用 UriComponentsBuilder 类的另一种解决方案:

@Test
public void givenGetUrl_whenJsonIsPassed_thenReturnProduct() {
    String criterion = "{\"prop\":\"name\",\"value\":\"Acer Aspire 5\"}";
    String url = "http://localhost:8080/spring-rest/api/get";

    UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url).queryParam("criterion", criterion);
    Product product = restTemplate.getForObject(builder.build().toUri(), Product.class);

    assertEquals(product.getId(), 1, 0);
}

如你所见,在将 URI 传递给 getForObject 方法之前,使用 UriComponentsBuilder 类来构建带有查询参数 criterion 的 URI。

6、总结

本文介绍了 Spring RestTemplate 抛出 IllegalArgumentException: Not enough variables available to expand 异常的原因以及解决办法。


Ref:https://www.baeldung.com/spring-not-enough-variables-available