1、概览 {#1概览}
本文将带你了解 Spring RestTemplate
抛出 IllegalArgumentException: Not enough variables available to expand
异常的原因以及解决办法。
2、原因 {#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、示例应用 {#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、示例应用说明 {#4示例应用说明}
Handler 方法 get()
根据指定的 Criterion
检索 Product
对象。
Criterion
可以表示为一个 JSON 字符串,其中有两个 Key:prop
和 value
。
prop
指的是 Product
属性,因此可以是 id
、name
称或 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、解决办法 {#5解决办法}
根据经验,应始终使用 POST
请求来发送 JSON 数据。
虽然不推荐使用 GET,但一个可行的解决方案是定义一个包含 criterion
的 String
对象,并在 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、总结 {#6总结}
本文介绍了 Spring RestTemplate
抛出 IllegalArgumentException: Not enough variables available to expand
异常的原因以及解决办法。
Ref:https://www.baeldung.com/spring-not-enough-variables-available