
restTemplate这个类基本是Spring项目里绕不开的HTTP客户端工具。我接手过不少老项目Controller里调第三方接口的代码十有八九是new RestTemplate然后getForObject看起来一切正常真跑到线上就开始出幺蛾子。这篇东西我想一次性把restTemplate的常规用法、文件上传、异常处理这些事讲透尤其把multipart上传这种容易翻车的点单独拎出来说。适合平时要写接口调用的后端开发也适合刚把Spring Boot项目跑起来、准备对接别人系统的新手。我觉得很多人对restTemplate都有个误解觉得它就是一个能用就行的简单工具压根没去关注底层连接、错误处理、超时配置这些东西。但实际踩过坑之后你会发现所有线上事故几乎都集中在几个固定的位置上。下面我就按自己平时写接口调用的顺序把restTemplate从初始化到文件上传再到性能调优一次性讲清楚。1. 先捋清楚RestTemplate在项目里的真实定位1.1 它和HttpClient、OkHttp、Feign到底啥关系RestTemplate是Spring框架提供的一个同步HTTP客户端它的核心特点是把HttpURLConnection、Apache HttpClient、OkHttp这些底层客户端包装成了一层更友好的API。也就是说RestTemplate本身并不发网络请求真正干活的是底层那些客户端RestTemplate做的是把URL、请求头、请求体、响应体这些Java对象和HTTP协议之间的转换工作。我在项目里遇到过不少人直接说我用RestTemplate不用HttpClient其实这种说法不太准确。RestTemplate默认走的是JDK自带的HttpURLConnection但你完全可以给它换成Apache HttpClient或者OkHttp作为底层实现换完之后API还是同一套这就是Spring封装的价值。换个说法RestTemplate好比是前台接待HttpClient这些才是真正干活的人你跟前台说话前台帮你转达。再对比一下Feign。Feign是声明式HTTP客户端你只需要定义一个接口配上注解Spring Cloud会帮你生成代理实现。RestTemplate则更偏向于命令式你手动拼URL、手动设置请求头。在很多Spring Boot项目里如果只是简单调用一两个第三方接口很多人懒得把整个Feign体系引进来直接注入一个RestTemplate完事这也是restTemplate到今天依然活得好好的原因。1.2 什么时候该用RestTemplate、什么时候该换我自己的判断标准很简单同步、低频、业务逻辑简单就用RestTemplate异步、高并发、需要连接池精细化管控就考虑WebClient或者自己封装HttpClient。举个实际例子我之前做过一个对接短信网关的服务每天就发几万条短信每条短信调用一次网关接口这完全在RestTemplate的能力范围内。但如果你的服务是每秒几百上千次的调用而且对时延敏感那RestTemplate默认的HttpURLConnection就不太行了这时候应该用带连接池的HttpComponentsClientHttpRequestFactory或者干脆考虑WebClient走异步。还有个场景也值得提那就是老项目的技术债。很多老项目里躺着几百处RestTemplate调用代码风格五花八门这时候不是让你全部推倒重来而是至少统一超时配置、统一错误处理、统一日志把这些基础设施做扎实RestTemplate本身完全够用。2. 从零搭建实例化方式与底层连接工厂的选择2.1 直接new和Spring Boot自动装配的区别最简单的用法是这样RestTemplate restTemplate new RestTemplate();这行代码十秒钟就能写完但它有两个问题。第一每次new都会重新创建一个RestTemplate实例如果是在工具类里每次调用都new一次那连接资源完全没法复用第二所有配置都是默认值连超时时间都没设置一旦下游接口卡住你的线程就可能一直挂着。Spring Boot项目里我更推荐用RestTemplateBuilder来构建Configuration public class RestTemplateConfig { Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder .setConnectTimeout(Duration.ofSeconds(3)) .setReadTimeout(Duration.ofSeconds(10)) .build(); } }RestTemplateBuilder是Spring Boot自动配置里的好东西它会根据你在application.yml里配置的spring.rest-template相关属性自动设置超时也允许你在代码里链式追加设置。而且这样构建出来的RestTemplate是单例Bean可以注入到任何地方使用。这里有个特别重要的认知RestTemplate本身是线程安全的只要你不往里面塞有状态的拦截器或HttpMessageConverter完全可以全局单例使用。我经常看到有人因为不知道这点在Service里每次调用方法都new一个这种习惯在高并发场景下很容易把连接资源打满。2.2 三个ClientHttpRequestFactory的取舍之前说了RestTemplate底层可以换靠的就是ClientHttpRequestFactory这个接口。实际项目中常见的三个实现我整理了一个对比表格工厂实现底层客户端连接池适用场景SimpleClientHttpRequestFactoryJDK HttpURLConnection无复用较弱开发调试、极低并发HttpComponentsClientHttpRequestFactoryApache HttpClient有可精细配置生产环境、高并发OkHttp3ClientHttpRequestFactoryOkHttp有Android过来的团队常用代码上来看如果选HttpComponents要先引入依赖dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId /dependency然后这样配置Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { HttpClient httpClient HttpClientBuilder.create() .setMaxConnTotal(200) .setMaxConnPerRoute(50) .setConnectionTimeToLive(30, TimeUnit.SECONDS) .build(); HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(httpClient); factory.setConnectTimeout(3000); factory.setReadTimeout(10000); return new RestTemplate(factory); }如果用了HttpComponents那默认情况下的连接管理就交给Apache HttpClient了MaxConnPerRoute是每个路由的最大连接数MaxConnTotal是整个客户端的总连接数。这个数量没有标准答案要根据下游接口的并发量来评估我一般先给一个保守值再通过监控看连接池的实际使用率再调整。有一点要提醒大家SimpleClientHttpRequestFactory虽然简单但它对PATCH请求的支持不友好默认方式下想要发PATCH会报错。如果你的接口里恰好要调用PATCH方法直接用Apache HttpClient那一套最省心。3. 最常用方法拆解getForObject、postForObject、exchange到底怎么选3.1 URI传参的三种写法和坑RestTemplate请求方法非常多但高频的无非就是getForObject、postForObject、getForEntity、postForEntity、exchange这几个。我们先从最基础的方法签名说起。我见过最多的问题是URL参数拼接错误。三种写法对比一下// 写法一直接拼字符串不推荐容易出编码问题 String url1 http://localhost:8080/user?id userId; // 写法二占位符推荐 String url2 http://localhost:8080/user?id{id}; User user restTemplate.getForObject(url2, User.class, userId); // 写法三Map传参适合参数多的情况 MapString, Object params new HashMap(); params.put(id, userId); params.put(type, 1); User user restTemplate.getForObject(http://localhost:8080/user?id{id}type{type}, User.class, params);用占位符的好处是Spring会帮你做URL编码。有一次我在对接一个搜索接口关键词里带了个中文逗号用字符串拼接直接发出去服务端那边收到的就是乱码排查了很久才发现是URL没有编码。换成占位符写法之后就没再出过这个问题。另外要注意占位符可以传多个参数restTemplate.getForObject(url, clazz, id, type)这种按顺序匹配没问题但如果你传的是一个List或者数组一定要确保数量和占位符数量一致。少了会直接抛异常多了会静默忽略后者更隐蔽。3.2 想拿响应头怎么办终于轮到exchange上场getForObject和getForEntity的区别一句话就能说清getForObject只关心响应体getForEntity把响应体和响应头、状态码一起包在ResponseEntity里。但如果你既要自定义请求头又要读响应头那最好还是用exchange。我当年对接一个文件下载接口时就遇到这种情况接口把文件的元信息放在响应头里Content-Disposition里有文件名响应体是文件流。用getForObject拿不到头信息用exchange就非常方便HttpHeaders headers new HttpHeaders(); headers.set(Authorization, Bearer token); RequestEntityVoid requestEntity RequestEntity .get(http://localhost:8080/download) .headers(headers) .build(); ResponseEntitybyte[] response restTemplate.exchange(requestEntity, byte[].class); String contentDisposition response.getHeaders().getFirst(Content-Disposition); byte[] fileBytes response.getBody();exchange的通用性很强它既能带上自定义请求头也能拿到完整的响应元数据所以很多人在项目里干脆只封装exchange其他方法都不用了。这种思路也没什么问题只是代码会稍微啰嗦一点。3.3 execute的底层到底在干嘛再往底层挖一层是execute方法。getForObject、postForObject、exchange这些方法最后都会调用execute。execute允许你传入一个RequestCallback来加工请求再用ResponseExtractor来解析响应。我平时不直接写execute但理解它的存在对排查问题很有帮助。有一次线上有个接口偶发超时我在日志里看到restTemplate抛了ResourceAccessException第一反应就是看底层连接工厂是什么、超时时间配了多少顺着execute这条调用链很快就能定位到是哪一层出了问题。这也是我建议多了解一点底层机制的原因遇到报错时能快速判断是连接问题、超时问题还是序列化问题。4. multipart文件上传的完整姿势与翻车记录4.1 为什么大部分人的第一次上传都失败文件上传这块是我见过踩坑最多的场景。很多人的第一反应是把File直接塞到Map里像这样// 错误写法别学 MultiValueMapString, Object body new LinkedMultiValueMap(); body.add(file, new File(/tmp/test.txt));这样写的问题在于File不是Spring能识别为文件资源的类型它会被当成form-data里的普通字符串字段处理最终请求没有multipart结构服务端用RequestParam(file)自然接不到东西。正确的做法是用Spring提供的Resource实现body.add(file, new FileSystemResource(new File(/tmp/test.txt)));或者读成字节后用ByteArrayResourcebody.add(file, new ByteArrayResource(bytes));之所以要用Resource是因为FormHttpMessageConverter在转换请求体时会检查value的类型遇到Resource就会把它编码成文件part其他类型都按普通字段处理。4.2 一个能跑通的multipart上传模板我直接把一个验证过能用的模板贴出来public String uploadFile(String url, File file, String remark) { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); LinkedMultiValueMapString, Object body new LinkedMultiValueMap(); body.add(file, new FileSystemResource(file)); body.add(remark, remark); HttpEntityLinkedMultiValueMapString, Object requestEntity new HttpEntity(body, headers); RestTemplate restTemplate new RestTemplate(); return restTemplate.postForObject(url, requestEntity, String.class); }映射到服务端接收方式大概是这样的PostMapping(/upload) public String upload(RequestPart(file) MultipartFile file, RequestParam(remark) String remark) { // 业务逻辑 }body里的key必须和服务端的参数名一致这个看起来不起眼的点我见人踩过不止一次。你这里add(file)服务端用RequestPart(uploadFile)两边对不上接口直接报Required request part is missing。还有一点要注意头里的Content-Type虽然设了MULTIPART_FORM_DATA但你千万不要手动去设置boundary。很多人为了让Content-Type看起来更完整手动加了一句boundaryxxx结果Spring在编码时又要自己生成boundary两边不一致服务端直接解析失败。正确做法就是只设置MediaType.MULTIPART_FORM_DATA剩下交给FormHttpMessageConverter处理。4.3 中文文件名、二次转码、超时这些暗坑文件上传还有几个暗坑我单独拿出来说。第一个是中文文件名。如果直接new FileSystemResource资源名就是文件路径Spring在组装Content-Disposition头时会拿getFilename()当文件名中文经常会被清理掉或者转成乱码服务端收下来的文件名惨不忍睹。我的解决办法是自定义一个Resource子类public class Utf8FileNameResource extends ByteArrayResource { private final String fileName; public Utf8FileNameResource(byte[] byteArray, String fileName) { super(byteArray); this.fileName fileName; } Override public String getFilename() { return fileName; } }这样可以稳定控制文件名。不过如果对接的是比较老的服务端对Content-Disposition里的UTF-8编码解析支持不好可能还是会出现乱码。这种情况下最稳妥的办法是和服务端约定文件名统一用ASCII字符或者由调用方进行一次编解码处理。第二个坑是异常导致的连接残留。文件上传属于耗时操作如果服务端处理慢响应时间很容易超过readTimeout。我建议给上传接口单独设置一个较大的超时时间尤其是图片、视频这类资源。我的做法是给文件上传单独定义一个RestTemplate实例专门配一个长一点的readTimeout不带业务调用一个默认的断开。第三个坑是内存占用。如果你用ByteArrayResource上传大文件整个文件都会加载到内存几百MB的文件直接可能OOM。大文件一般直接用FileSystemResource避免一次性读进内存。5. 拦截器、异常处理与消息转换器让RestTemplate不再裸奔5.1 用拦截器统一打日志和塞TokenRestTemplate支持拦截器每次请求发出前、接收到响应后都会经过它。最常见的场景是两个统一添加请求头、统一打印日志。实现ClientHttpRequestInterceptor接口public class LoggingInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { // 请求前打印日志 System.out.println(Request URI: request.getURI()); System.out.println(Request Method: request.getMethod()); System.out.println(Request Body: new String(body, StandardCharsets.UTF_8)); // 执行实际请求 ClientHttpResponse response execution.execute(request, body); // 响应后打印状态码 System.out.println(Response Status: response.getStatusCode()); return response; } }注册也简单RestTemplate restTemplate new RestTemplate(); restTemplate.getInterceptors().add(new LoggingInterceptor());用RestTemplateBuilder时可以在构建的时候传入多个拦截器builder.interceptors(loggingInterceptor, tokenInterceptor)。要注意的是拦截器里尽量不要做耗时操作比如往远程日志服务发日志因为在拦截器里做一次网络请求会让你的主线程多等一次RTT整个接口耗时直接翻倍。我自己吃过这个亏后来改成了异步日志。塞Token的拦截器也很常见public class TokenInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().set(Authorization, Bearer getToken()); return execution.execute(request, body); } }有了这样一个拦截器你就不用在每个请求前都手动加Header了对代码整洁度帮助很大。5.2 别让默认异常处理吞掉你的排错信息RestTemplate默认的错误处理是DefaultResponseErrorHandler它对4xx和5xx响应会抛出HttpClientErrorException或HttpServerErrorException。这个默认行为看起来没什么但实际排查问题的时候你会发现异常里的响应体经常是一堆HTML或者过长的错误信息真正对排查问题的关键字段反而被淹没了。我一般会自定义一个ResponseErrorHandler把错误体的内容读出来在异常里附上接口地址和状态码public class CustomResponseErrorHandler implements ResponseErrorHandler { Override public boolean hasError(ClientHttpResponse response) throws IOException { return response.getStatusCode().is4xxClientError() || response.getStatusCode().is5xxServerError(); } Override public void handleError(ClientHttpResponse response) throws IOException { String body new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8); throw new CustomApiException( HTTP response.getStatusCode().value() , body: body ); } }这样在catch异常的时候错误信息里直接能看到第三方接口返回的具体内容不用再去翻日志和抓包。还有一个细节handleError里读完body之后这个响应流就被消费了如果你想在异常处理器之外再读一次是读不到内容的。所以要么在handler里把所有信息都取出来完整地放进异常要么就不要在handler外面继续依赖响应体了。5.3 中文乱码的根源StringHttpMessageConverter中文乱码问题很多人会归咎于编码不对但RestTemplate里真正导致乱码的往往是StringHttpMessageConverter默认使用ISO-8859-1来解码响应体。如果你的服务端返回的是UTF-8而客户端用ISO-8859-1去解中文自然就成了问号或乱码。解决方案很简单把StringHttpMessageConverter的默认编码改成UTF-8RestTemplate restTemplate new RestTemplate(); // 移除默认的StringHttpMessageConverter换成UTF-8版本的 ListHttpMessageConverter? converters restTemplate.getMessageConverters(); converters.removeIf(converter - converter instanceof StringHttpMessageConverter); converters.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8)); restTemplate.setMessageConverters(converters);这个坑在对接一些老接口时特别常见服务端明明返回的是规范的UTF-8 JSON客户端却用ISO-8859-1去解码导致JSON里所有中文全部乱码。排查的时候不要光盯着响应头里的Content-Type也要看看项目里有没有人动过messageConverters。6. 连接池与会话保持性能和状态这两件事踩过才算懂6.1 HttpURLConnection为什么不适合高并发默认的RestTemplate底层是JDK自带HttpURLConnection。这个实现最大的问题在于连接复用能力弱每个连接用完就关频繁创建销毁TCP连接在高并发场景下就是灾难。我见过一个项目线上接口平均响应时间是800ms但调用方通过RestTemplate请求时p99却到了5秒。后来一看默认的SimpleClientHttpRequestFactory每次请求都在建TCP连接而目标服务端在带宽和句柄上都出现了瓶颈。换成了HttpComponents之后连接池把连接复用起来p99直接降到了1秒以内。所以我在前面就强调过生产环境尽量用HttpComponentsClientHttpRequestFactory不要图省事用默认工厂。连接池配置方面有两个参数必须心里有数参数含义建议setMaxConnTotal整个客户端的最大连接数根据下游接口并发量评估setMaxConnPerRoute单个目标路由的最大连接数一般小于等于MaxConnTotal我的经验是刚开始可以按照目标接口预期QPS的1/10来配置比如预期每秒100次调用那每个路由可以先给10到20个连接再配合超时和监控慢慢调优。6.2 Cookie和Session怎么处理很多内部系统之间调用时接口依赖Session要求客户端先登录拿Cookie再带着Cookie访问业务接口。RestTemplate本身不会帮你管理Cookie默认的HttpURLConnection虽然有一点Cookie存储机制但一旦你切换到底层客户端行为就不一样了。我的做法是手动管理Cookie在登录接口拿到Set-Cookie之后手动塞到后续请求的Header里HttpHeaders headers new HttpHeaders(); headers.set(HttpHeaders.COOKIE, JSESSIONID sessionId); HttpEntityVoid requestEntity new HttpEntity(headers); ResponseEntityString response restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);如果用的是Apache HttpClient那一套也可以配置一个BasicCookieStore让HttpClient自动管理Cookie。但我个人不太推荐为了Cookie引入复杂的会话状态管理因为会降低接口调用的无状态性更容易隐藏Bug。除非对接的是那种没法改的老系统否则还是尽量让服务端支持基于Token的认证方式。6.3 性能排查与超时参数的合理设置超时配置是整个RestTemplate使用中最重要的一件事没有之一。我的建议是connectTimeout和readTimeout一定要显式设置绝不能用默认值无限等。默认情况下如果服务端一直不返回你的线程就可能一直挂在那里资源被占着不放最终拖垮整个应用。connectTimeout是指建立连接的超时时间一般2到3秒就够了readTimeout是指建立连接后等待响应数据的时间这个要根据业务接口的耗时来定一般内部接口给5秒外部接口给10秒文件上传给30秒甚至更长。我曾经遇到一个情况一个接口偶尔会触发服务端一个很慢的统计任务3000ms的readTimeout经常被触发导致线上偶发超时告警。后来把readTimeout调到10秒配合重试机制问题就消失了。这里要再提一句重试要根据接口的幂等性谨慎开启。GET请求重试一般没问题POST请求如果下游接口没做幂等重试可能造成重复下单或重复扣款我踩过的这个坑比超时本身更惨。还有个性能排查的技巧如果怀疑RestTemplate请求慢先确认慢在哪个环节。用一个简单的办法在拦截器里分别记录执行前、执行后的时间戳打印到日志里就能看出建连耗时、请求耗时、解析耗时分别是多少。实际上一次执行中时间主要花在等待响应上如果建连那一步耗费了上百毫秒多半是连接池不足或没有连接池优先去优化连接复用而不是盲目调大readTimeout。最后再分享一个小习惯我会在项目里把RestTemplate封装成一个Client类统一放超时配置、拦截器、错误处理、消息转换器业务代码只负责传URL和参数。这样做的好处是线上出问题时你只需要在一个地方改配置和逻辑而不是去几百处调用点逐个排查。RestTemplate这个工具本身不算复杂复杂的是你围绕它做的工程化设计是否到位。