当前位置: 代码网 > it编程>编程语言>Java > SpringBoot手写RestTemplate拦截器链,掌控HTTP请求实践

SpringBoot手写RestTemplate拦截器链,掌控HTTP请求实践

2025年12月08日 Java 我要评论
01 前言在项目开发中,resttemplate作为spring提供的http客户端工具,经常用于访问内部或三方服务。但实际开发时,我们常常需要对请求做统一处理,比如添加认证token、记录请求日志、

01 前言

在项目开发中,resttemplate作为spring提供的http客户端工具,经常用于访问内部或三方服务。

但实际开发时,我们常常需要对请求做统一处理,比如添加认证token、记录请求日志、设置超时时间、实现失败重试等。

要是每个接口调用都手动处理这些逻辑,不仅代码冗余,还容易出错。

02 为什么需要拦截器链?

先看一个场景:假设支付服务需要调用第三方支付接口,每次请求都得做一系列操作。

如果没有拦截器,代码会写成这样:

public string createorder(string orderid) {
    // 创建resttemplate实例
    resttemplate resttemplate = new resttemplate();
    
    // 设置请求超时时间
    simpleclienthttprequestfactory factory = new simpleclienthttprequestfactory();
    factory.setconnecttimeout(3000); // 连接超时3秒
    factory.setreadtimeout(3000); // 读取超时3秒
    resttemplate.setrequestfactory(factory);
    
    // 设置请求头(添加认证信息)
    httpheaders headers = new httpheaders();
    headers.set("authorization", "bearer " + gettoken()); // 拼接认证token
    httpentity<string> entity = new httpentity<>(headers);
    
    // 记录请求日志
    log.info("请求支付接口: {}", orderid);
    try {
        // 发送请求
        string result = resttemplate.postforobject(pay_url, entity, string.class);
        log.info("请求成功: {}", result);
        return result;
    } catch (exception e) {
        log.error("请求失败", e);
        
        // 失败重试逻辑
        for (int i = 0; i < 2; i++) {
            try {
                return resttemplate.postforobject(pay_url, entity, string.class);
            } catch (exception ex) {  }
        }
        throw e;
    }
}

这段代码的问题很明显:重复逻辑散落在各个接口调用中,维护成本极高。

如果有10个接口需要调用第三方服务,就要写10遍相同的代码。

而拦截器链能把这些通用逻辑抽离成独立组件,按顺序串联执行,实现"一次定义,处处复用"。

核心调用代码可以简化为:

return resttemplate.postforobject(pay_url, entity, string.class);

03 resttemplate拦截器基础

spring提供了clienthttprequestinterceptor接口,所有自定义拦截器都要实现它。

public interface clienthttprequestinterceptor {
    // 拦截方法:处理请求后通过execution继续执行调用链
    clienthttpresponse intercept(httprequest request, byte[] body, clienthttprequestexecution execution) throws ioexception;
}

拦截器工作流程

我们先实现一个打印请求响应日志的拦截器,这是项目中最常用的功能:

import lombok.extern.slf4j.slf4j;
import org.springframework.http.httprequest;
import org.springframework.http.client.clienthttprequestexecution;
import org.springframework.http.client.clienthttprequestinterceptor;
import org.springframework.http.client.clienthttpresponse;
import org.springframework.util.streamutils;

import java.io.ioexception;
import java.nio.charset.standardcharsets;

@slf4j // 简化日志输出
public class logginginterceptor implements clienthttprequestinterceptor {

    @override
    public clienthttpresponse intercept(httprequest request, byte[] body, clienthttprequestexecution execution) throws ioexception {
        // 记录请求开始时间
        long start = system.currenttimemillis();
        
        // 打印请求信息
        log.info("===== 请求开始 =====");
        log.info("url: {}", request.geturi()); // 请求地址
        log.info("method: {}", request.getmethod()); // 请求方法(get/post等)
        log.info("headers: {}", request.getheaders()); // 请求头
        log.info("body: {}", new string(body, standardcharsets.utf_8)); // 请求体
        
        // 执行后续拦截器或实际请求
        clienthttpresponse response = execution.execute(request, body);
        
        // 读取响应体(注意:默认流只能读一次)
        string responsebody = streamutils.copytostring(response.getbody(), standardcharsets.utf_8);
        
        // 打印响应信息
        log.info("===== 请求结束 =====");
        log.info("status: {}", response.getstatuscode()); // 响应状态码
        log.info("headers: {}", response.getheaders()); // 响应头
        log.info("body: {}", responsebody); // 响应体
        log.info("耗时: {}ms", system.currenttimemillis() - start); // 计算请求耗时
        
        return response;
    }
}

关键点:

  • execution.execute(…)是调用链的核心,必须调用才能继续执行后续拦截器或发送实际请求。
  • 响应流response.getbody()默认只能读取一次,如需多次处理需缓存(后面会讲解决方案)。

04 拦截器链实战:从单拦截器到多拦截器协同

springboot中通过resttemplate.setinterceptors()注册多个拦截器,形成调用链。

import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.http.client.bufferingclienthttprequestfactory;
import org.springframework.http.client.clienthttprequestinterceptor;
import org.springframework.http.client.simpleclienthttprequestfactory;
import org.springframework.web.client.resttemplate;

import java.util.arraylist;
import java.util.list;

@configuration // 标记为配置类,让spring扫描加载
public class resttemplateconfig {

    @bean // 将resttemplate实例注入spring容器
    public resttemplate customresttemplate() {
        // 创建拦截器列表
        list<clienthttprequestinterceptor> interceptors = new arraylist<>();
        interceptors.add(new authinterceptor()); // 1. 认证拦截器
        interceptors.add(new logginginterceptor()); // 2. 日志拦截器
        interceptors.add(new retryinterceptor(2)); // 3. 重试拦截器(最多重试2次)
        
        // 创建resttemplate并设置拦截器
        resttemplate resttemplate = new resttemplate();
        resttemplate.setinterceptors(interceptors);
        
        // 配置请求工厂(支持响应流缓存)
        simpleclienthttprequestfactory factory = new simpleclienthttprequestfactory();
        factory.setconnecttimeout(3000); // 连接超时3秒
        factory.setreadtimeout(3000); // 读取超时3秒
        // 用bufferingclienthttprequestfactory包装,解决响应流只能读一次的问题
        resttemplate.setrequestfactory(new bufferingclienthttprequestfactory(factory));
        
        return resttemplate;
    }
}

拦截器执行顺序

按添加顺序执行(像排队一样),上述示例的执行流程是:

authinterceptor → logginginterceptor → retryinterceptor → 实际请求 → retryinterceptor → logginginterceptor → authinterceptor

认证拦截器示例

import org.springframework.http.httpheaders;
import org.springframework.http.httprequest;
import org.springframework.http.client.clienthttprequestexecution;
import org.springframework.http.client.clienthttprequestinterceptor;
import org.springframework.http.client.clienthttpresponse;

import java.io.ioexception;

public class authinterceptor implements clienthttprequestinterceptor {

    @override
    public clienthttpresponse intercept(httprequest request, byte[] body, clienthttprequestexecution execution) throws ioexception {
        // 获取认证token(实际项目中可能从缓存或配置中心获取)
        string token = gettoken();
        
        // 往请求头添加认证信息
        httpheaders headers = request.getheaders();
        headers.set("authorization", "bearer " + token); // 标准bearer认证格式
        
        // 继续执行后续拦截器
        return execution.execute(request, body);
    }

    // 模拟获取token的方法
    private string gettoken() {
        return "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9..."; // 实际为真实token字符串
    }
}

重试拦截器示例

package com.example.rest;

import lombok.extern.slf4j.slf4j;
import org.springframework.http.httprequest;
import org.springframework.http.client.clienthttprequestexecution;
import org.springframework.http.client.clienthttprequestinterceptor;
import org.springframework.http.client.clienthttpresponse;
import org.springframework.web.client.httpstatuscodeexception;

import java.io.ioexception;

@slf4j
public class retryinterceptor implements clienthttprequestinterceptor {
    private final int maxretries; // 最大重试次数

    // 构造方法指定最大重试次数
    public retryinterceptor(int maxretries) {
        this.maxretries = maxretries;
    }

    @override
    public clienthttpresponse intercept(httprequest request, byte[] body, clienthttprequestexecution execution) throws ioexception {
        int retrycount = 0; // 当前重试次数
        while (true) {
            try {
                // 执行请求,若成功直接返回响应
                return execution.execute(request, body);
            } catch (httpstatuscodeexception e) {
                // 处理http状态码异常(如5xx服务器错误)
                if (e.getstatuscode().is5xxservererror() && retrycount < maxretries) {
                    retrycount++;
                    log.warn("服务器错误,开始第{}次重试,状态码:{}", retrycount, e.getstatuscode());
                    continue; // 继续重试
                }
                // 不满足重试条件,抛出异常
                throw e;
            } catch (ioexception e) {
                // 处理网络异常(如连接超时)
                if (retrycount < maxretries) {
                    retrycount++;
                    log.warn("网络异常,开始第{}次重试", retrycount, e);
                    continue; // 继续重试
                }
                // 重试次数耗尽,抛出异常
                throw e;
            }
        }
    }
}

05 实战踩坑指南

响应流读取问题

现象:日志拦截器读取响应体后,后续拦截器再读会读取到空数据。

原因:响应流默认是一次性的,读完就关闭了。

解决方案:用bufferingclienthttprequestfactory包装请求工厂,缓存响应流:

// 创建基础请求工厂
simpleclienthttprequestfactory factory = new simpleclienthttprequestfactory();
// 包装成支持响应缓存的工厂
bufferingclienthttprequestfactory bufferingfactory = new bufferingclienthttprequestfactory(factory);
// 设置到resttemplate
resttemplate.setrequestfactory(bufferingfactory);
(上述代码已在04节的配置类中体现)

拦截器顺序问题

反例:把logginginterceptor放在authinterceptor前面,日志中会看不到authorization头。

因为日志拦截器先执行时,认证拦截器还没添加认证头。

正确顺序(按职责划分):

  1. 前置处理拦截器:认证、加密、参数修改
  2. 日志拦截器:记录完整请求(包含前置处理的结果)
  3. 重试/降级拦截器:处理异常情况(放在最后,确保重试时能重新执行所有前置逻辑)

线程安全问题

resttemplate是线程安全的,但拦截器若有成员变量,需确保线程安全!

错误示例:

public class badinterceptor implements clienthttprequestinterceptor {
    private int count = 0; // 非线程安全的成员变量

    @override
    public clienthttpresponse intercept(...) {
        count++; // 多线程环境下会出现计数错误
    }
}

解决方案:

  • 拦截器避免定义可变成员变量
  • 必须使用时,用threadlocal隔离线程状态(每个线程独立维护变量副本)

06 测试示例

package com.example.rest;

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.restcontroller;
import org.springframework.web.client.resttemplate;

@restcontroller
public class testcontroller {

    // 注入自定义的resttemplate(已包含拦截器链)
    @autowired
    private resttemplate customresttemplate;

    // 测试接口:调用第三方服务
    @getmapping("/call-third")
    public string callthirdapi() {
        // 直接调用,拦截器会自动处理认证、日志、重试等逻辑
        return customresttemplate.getforobject("http://localhost:8080/mock-third-api", string.class);
    }

    // 模拟第三方接口
    @getmapping("/mock-third-api")
    public string mockthirdapi() {
        return "hello from third party";
    }
}

所需依赖(只需基础的spring boot starter web):

<dependencies>
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-web</artifactid>
    </dependency>
</dependencies>

07 总结与扩展

通过自定义resttemplate的拦截器链,我们可以将请求处理的通用逻辑(认证、日志、重试等)抽离成独立组件,实现代码复用和统一维护。

  1. 拦截器链顺序:按"前置处理→日志→重试"顺序注册,确保功能正确。
  2. 响应流处理:使用bufferingclienthttprequestfactory解决流只能读取一次问题。
  3. 线程安全:拦截器避免定义可变成员变量,必要时使用threadlocal。
  4. 异常处理:重试拦截器需明确重试条件(如只对5xx错误重试,避免对4xx客户端错误重试)。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com