SpringCloud-RestTemplate发送Http请求讲解

SpringCloud-RestTemplate发送Http请求讲解

一、RestTemplate 讲解

1. 没有 RestTemplate 时,Java 怎么发 HTTP 请求?

1.1 用 JDK 原生的 HttpURLConnection

// 发一个 GET 请求,获取用户信息 public String getUser(Long id) { try { URL url = new URL("http://api.example.com/users/" + id); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); // 读取响应 BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream()) ); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); return response.toString(); // 返回的是 JSON 字符串 } catch (IOException e) { throw new RuntimeException(e); } }

弊端:

  • 每发一个请求要写 20 行样板代码

  • 没有自动的 JSON 转换(返回的是字符串,你需要自己用ObjectMapper转成对象)

  • 错误处理繁琐(HTTP 404、500 需要手动判断conn.getResponseCode()

  • 没有连接复用(每次openConnection()都新建 TCP 连接)

  • 没有请求模板化(每个接口都要重复写超时、请求头设置)


1.2 所以 Spring 设计了 RestTemplate

RestTemplate的核心设计目标:

"像调用本地方法一样,发起 HTTP 请求"


2. RestTemplate 是什么?

它是Spring 提供的同步 HTTP 客户端,封装了 JDK 的HttpURLConnection(或 Apache HttpClient、OkHttp),让你用更简洁的方式发送 HTTP 请求

它在 Spring 生态中的定位:

你的 Service 层 ↓ RestTemplate (发 HTTP 请求) ↓ HttpMessageConverter (对象 ↔ JSON/XML 转换) ↓ ClientHttpRequestFactory (实际网络层:HttpURLConnection / HttpClient / OkHttp) ↓ 目标服务器

3. 基本用法:对比原生代码

3.1 GET 请求

原生写法(对比):

// 返回 JSON 字符串,自己解析 String json = getUser(1L); User user = objectMapper.readValue(json, User.class);

RestTemplate 写法:

@Autowired private RestTemplate restTemplate; public User getUser(Long id) { // 直接返回 User 对象,JSON 转换自动完成 User user = restTemplate.getForObject( "http://api.example.com/users/{id}", // URL,{id} 是占位符 User.class, // 返回类型 id // 占位符的值 ); return user; }

为什么能直接返回User对象?

因为RestTemplate内部注册了MappingJackson2HttpMessageConverter,它自动把响应的 JSON 字符串反序列化成User对象。


3.2 POST 请求(带请求体)

public User createUser(UserDTO dto) { // 发送 POST 请求,dto 自动序列化为 JSON User user = restTemplate.postForObject( "http://api.example.com/users", dto, // 请求体,自动转 JSON User.class // 返回类型 ); return user; }

3.3 带请求头(如 Token 鉴权)

public User getUserWithAuth(Long id, String token) { // 构建请求头 HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); headers.setContentType(MediaType.APPLICATION_JSON); // 构建请求实体(头 + 体) HttpEntity<Void> requestEntity = new HttpEntity<>(headers); // 发送请求 ResponseEntity<User> response = restTemplate.exchange( "http://api.example.com/users/{id}", HttpMethod.GET, requestEntity, User.class, id ); // 获取状态码、响应头、响应体 HttpStatus status = response.getStatusCode(); User user = response.getBody(); return user; }

exchange是最通用的方法,可以处理所有 HTTP 方法,并且能拿到完整的响应信息(状态码、Header、Body)。


4. 核心设计:为什么 RestTemplate 要这样设计?

4.1 为什么有getForObjectgetForEntity两个方法?

// 方法 1:只关心返回的数据 User user = restTemplate.getForObject(url, User.class, id); // 方法 2:关心完整响应(状态码 + Header + Body) ResponseEntity<User> response = restTemplate.getForEntity(url, User.class, id); HttpStatus status = response.getStatusCode(); HttpHeaders headers = response.getHeaders(); User user = response.getBody();

设计原因:

  • 80% 的场景你只需要数据,所以getForObject简化了调用

  • 20% 的场景你需要判断 HTTP 状态码或读取响应头,所以提供getForEntity

4.2 为什么 URL 支持{id}占位符?

restTemplate.getForObject( "http://api.example.com/users/{id}/orders/{orderId}", Order.class, 1L, // 对应 {id} 100L // 对应 {orderId} );

设计原因:

  • 避免字符串拼接url + "/" + id,容易出错且可读性差

  • 内部自动做 URL 编码(比如参数里有空格、中文会自动转义)


4.3 HttpMessageConverter:自动转换的秘密

RestTemplate能自动处理 JSON,是因为它内部维护了一个转换器列表:

RestTemplate ├── MappingJackson2HttpMessageConverter ← 处理 application/json ├── StringHttpMessageConverter ← 处理 text/plain ├── FormHttpMessageConverter ← 处理 application/x-www-form-urlencoded ├── ByteArrayHttpMessageConverter ← 处理二进制 └── ...

执行流程:

发请求时: UserDTO 对象 ↓ 遍历 converters,找到能处理 application/json 的 ↓ MappingJackson2HttpMessageConverter.write() ↓ JSON 字符串 → 发送 收响应时: 响应 Content-Type: application/json ↓ 遍历 converters,找到能处理 JSON 的 ↓ MappingJackson2HttpMessageConverter.read() ↓ User 对象

5. 企业级用法:RestTemplate 的配置

5.1 为什么需要手动创建 RestTemplate?

Spring Boot 不会自动注入RestTemplate(不像RedisTemplate),你需要自己配置。

原因:HTTP 客户端的配置差异很大(超时时间、连接池、SSL、代理),框架无法预设一个适合所有场景的默认值。


5.2 企业级配置类

@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { // 使用 Apache HttpClient 作为底层(比 JDK 的 HttpURLConnection 性能更好) HttpClient httpClient = HttpClientBuilder.create() .setMaxConnTotal(200) // 最大连接数 .setMaxConnPerRoute(50) // 每个路由的最大连接(如 api.example.com) .setConnectionTimeToLive(30, TimeUnit.SECONDS) // 连接存活时间 .build(); // 创建工厂 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); factory.setConnectTimeout(5000); // 连接超时 5 秒 factory.setReadTimeout(10000); // 读取超时 10 秒 RestTemplate restTemplate = new RestTemplate(factory); // 添加拦截器(如统一加请求头、记录日志) restTemplate.getInterceptors().add(new LoggingInterceptor()); return restTemplate; } }

5.3 为什么要用连接池?

没有连接池的问题:

  • 每次请求都新建 TCP 连接(三次握手)

  • 高并发时,TCP 连接数暴涨,导致TIME_WAIT状态过多

  • 响应延迟高

使用 Apache HttpClient 连接池后:

  • 连接复用,避免重复握手

  • 控制最大连接数,防止资源耗尽

  • 支持 HTTP/1.1 的 Keep-Alive


6. RestTemplate 的局限:为什么 Spring 官方不推荐在新项目使用?

6.1 它是同步阻塞的

User user = restTemplate.getForObject(url, User.class); // 这行代码会阻塞当前线程,直到 HTTP 响应返回 // 在高并发下,线程池很快被耗尽

弊端:

  • 线程阻塞等待网络 IO,CPU 空转

  • 不适合高并发场景(如网关、聚合服务需要同时调多个下游接口)

6.2 替代方案:WebClient(Spring 5 引入)

@Autowired private WebClient webClient; public Mono<User> getUser(Long id) { return webClient.get() .uri("http://api.example.com/users/{id}", id) .retrieve() .bodyToMono(User.class); // 非阻塞,返回 Mono(响应式) }

WebClient的优势:

  • 基于 Netty,异步非阻塞

  • 一个线程可以并发处理多个请求

  • 适合微服务架构下的高并发调用

6.3 但 RestTemplate 仍然广泛使用

因为:

  • 代码简单直观,适合大多数 CRUD 场景

  • 团队熟悉度高,维护成本低

  • 很多老项目仍在使用


7. 总结

问题RestTemplate 的解决方案
发 HTTP 请求代码繁琐封装为getForObjectpostForObject等一行调用
JSON 转换麻烦HttpMessageConverter自动序列化/反序列化
URL 拼接易出错占位符{id}自动替换和编码
需要处理响应状态码ResponseEntity封装完整响应
连接管理低效可配置ClientHttpRequestFactory使用连接池
同步阻塞性能差新项目推荐WebClient

一句话:RestTemplate是 Spring 对 HTTP 客户端的封装,让你用调用本地方法的方式发起远程 HTTP 请求,屏蔽了网络通信的底层细节。


二、ObjectMapper 讲解

1. 没有 ObjectMapper 时,Java 怎么处理 JSON?

1.1 手动拼接 JSON 字符串

假设你有一个User对象,要返回给前端:

public class User { private Long id; private String name; private Integer age; private LocalDateTime createTime; // getter / setter }

没有 ObjectMapper,你只能手动拼字符串:

public String userToJson(User user) { return "{" + "\"id\":" + user.getId() + "," + "\"name\":\"" + user.getName() + "\"," + "\"age\":" + user.getAge() + "," + "\"createTime\":\"" + user.getCreateTime() + "\"" + "}"; }

弊端:

  • 字段多了根本没法维护

  • 字符串拼接容易漏逗号、引号,语法错误

  • 如果name里有特殊字符(如"或换行),直接破坏 JSON 格式

  • 日期格式不统一(有的接口返回2024-01-01,有的返回毫秒时间戳)

  • 嵌套对象(User里有Address)时,拼接复杂度爆炸

1.2 手动解析 JSON 更痛苦

前端传过来一个 JSON 字符串,你要转成 Java 对象:

{"id":1,"name":"张三","age":25,"createTime":"2024-01-15 10:30:00"}

没有 ObjectMapper,你只能手动解析:

public User jsonToUser(String json) { // 用字符串截取?正则?根本不可行 // 只能用 org.json 或 fastjson 的底层 API,一行行 get JSONObject obj = new JSONObject(json); User user = new User(); user.setId(obj.getLong("id")); user.setName(obj.getString("name")); user.setAge(obj.getInt("age")); // ... 每个字段都要手动映射 return user; }

弊端:

  • 字段多了写死人

  • 字段名拼写错误在运行时才暴露

  • 类型转换容易出错(比如 JSON 里age传了字符串"25",手动getInt可能抛异常)

  • 嵌套对象需要递归解析

1.3 所以 Jackson 库被创建出来

Jackson 是 Java 世界最流行的 JSON 处理库,而ObjectMapper是它的核心入口类


2. ObjectMapper 是什么?

ObjectMapper是 Jackson 库提供的"对象映射器",负责:

方向方法俗称
Java 对象 → JSON 字符串writeValueAsString()序列化(Serialization)
JSON 字符串 → Java 对象readValue()反序列化(Deserialization)

3. 核心用法演示

3.1 序列化:对象 → JSON

User user = new User(); user.setId(1L); user.setName("张三"); user.setAge(25); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(user); System.out.println(json); // 输出:{"id":1,"name":"张三","age":25}

对比手动拼接:

  • 一行代码搞定

  • 自动处理引号、转义、特殊字符

  • 字段再多也不怕

3.2 反序列化:JSON → 对象

String json = "{\"id\":1,\"name\":\"张三\",\"age\":25}"; ObjectMapper mapper = new ObjectMapper(); User user = mapper.readValue(json, User.class); System.out.println(user.getName()); // 输出:张三

对比手动解析:

  • 一行代码,自动映射所有字段

  • 类型安全(id自动转Longage自动转Integer

  • 字段名对应不上时报错明确

3.3 处理集合和嵌套对象

// List 序列化 List<User> list = Arrays.asList(user1, user2); String json = mapper.writeValueAsString(list); // 输出:[{"id":1,"name":"张三"},{"id":2,"name":"李四"}] // List 反序列化 List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});

TypeReference是什么?

因为 Java 泛型擦除,mapper.readValue(json, List.class)无法知道 List 里是什么类型。TypeReference是 Jackson 提供的技巧,利用匿名内部类保留泛型信息。


4. Spring Boot 中 ObjectMapper 的自动配置

4.1 你几乎不需要手动 new ObjectMapper()

在 Spring Boot 项目中,当你引入spring-boot-starter-web,Jackson 作为默认 JSON 库被引入,Spring Boot 自动配置了一个ObjectMapperBean:

// Spring Boot 自动配置的源码(简化) @Bean @Primary public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 自动应用 application.yml 中的配置 // 自动注册 classpath 下的模块 return mapper; }

这意味着:

  • @RestController返回对象时,Spring 自动用这个ObjectMapper转 JSON

  • @RequestBody接收 JSON 时,Spring 自动用这个ObjectMapper转对象

  • 你可以直接@Autowired注入使用

4.2 实际使用方式

方式一:让 Spring 自动处理(最常用)

@RestController public class UserController { @GetMapping("/user/{id}") public User getUser(@PathVariable Long id) { User user = userService.getById(id); return user; // Spring 自动用 ObjectMapper 转成 JSON 返回 } @PostMapping("/user") public User createUser(@RequestBody UserDTO dto) { // Spring 自动用 ObjectMapper 把请求体的 JSON 转成 UserDTO return userService.save(dto); } }

方式二:手动注入使用(特殊场景)

@Service public class UserService { @Autowired private ObjectMapper objectMapper; public void cacheUser(User user) { // 手动转成 JSON 字符串,存入 Redis String json = objectMapper.writeValueAsString(user); redisTemplate.opsForValue().set("user:" + user.getId(), json); } }

5. ObjectMapper 的常用配置

5.1 为什么需要配置?

默认的ObjectMapper行为不一定符合企业需求,比如:

  • 日期字段默认序列化为时间戳(如1705312200000),前端看不懂

  • 空对象可能抛异常

  • 字段命名风格可能不匹配(Java 用驼峰userName,前端要下划线user_name

5.2 通过 application.yml 配置(推荐)

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss # 日期格式 time-zone: GMT+8 # 时区 default-property-inclusion: non_null # 忽略 null 字段,不输出 property-naming-strategy: SNAKE_CASE # 驼峰转下划线

效果对比:

public class User { private Long id; private String name; private String email; // 假设为 null private LocalDateTime createTime; } // 默认配置输出: // {"id":1,"name":"张三","email":null,"createTime":1705312200000} // 加上面配置后输出: // {"id":1,"name":"张三","create_time":"2024-01-15 10:30:00"} // 注意:email 为 null 被忽略了,createTime 变成了下划线 + 可读日期

5.3 通过代码配置(更灵活)

@Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 1. 日期格式:转字符串,不用时间戳 mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 2. 忽略 null 字段 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 3. 驼峰转下划线 mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); // 4. 允许单引号(前端有时传单引号 JSON) mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); // 5. 未知字段不报错(前端传了后端没有的字段,忽略而不是抛异常) mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); return mapper; }

6. 核心配置项详解(企业级常用)

配置作用场景
WRITE_DATES_AS_TIMESTAMPS控制日期序列化格式禁用后输出2024-01-15,启用后输出1705312200000
NON_NULL忽略值为 null 的字段减少 JSON 体积,前端不用判空
SNAKE_CASE驼峰命名转下划线Java 用userName,JSON 用user_name
FAIL_ON_UNKNOWN_PROPERTIES遇到未知字段是否报错禁用后,前端多传字段不会导致 400 错误
ALLOW_SINGLE_QUOTES允许 JSON 用单引号包裹兼容不规范的前端 JSON

7. 注意事项

7.1 ObjectMapper 是线程安全的

// 正确:全局复用一个实例 @Service public class UserService { @Autowired private ObjectMapper mapper; // Spring 注入的单例,线程安全 } // 错误:每次方法调用都 new 一个(浪费资源) public void bad() { ObjectMapper mapper = new ObjectMapper(); // 不要这样写 }

设计原因:ObjectMapper内部有大量缓存(如类结构反射信息),创建成本高,应该复用。

7.2 不要混用不同的 ObjectMapper 实例

如果你手动new ObjectMapper(),它的配置和 Spring 自动配置的那个可能不一样,导致:

  • Controller 返回的 JSON 格式是一套规则

  • 你手动序列化存 Redis 的 JSON 是另一套规则

  • 两边不一致,反序列化时可能出问题

企业级做法:统一使用 Spring 注入的ObjectMapper,或者确保手动创建的实例配置完全一致。


8. 总结

问题ObjectMapper 的解决方案
手动拼 JSON 容易出错writeValueAsString()自动序列化
手动解析 JSON 繁琐readValue()自动反序列化
日期格式不统一配置date-format
字段命名风格不一致配置SNAKE_CASE
null 字段污染 JSON配置NON_NULL
前端多传字段导致报错配置FAIL_ON_UNKNOWN_PROPERTIES

一句话:

ObjectMapper是 Java 世界处理 JSON 的"翻译官",负责 Java 对象和 JSON 字符串之间的双向转换。

在 Spring Boot 中,它已经被自动配置好,你只需要通过application.yml或代码调整它的行为,就能统一控制整个应用的 JSON 格式。