一、背景与需求

在微服务架构中,经常需要在服务间通信时传递某些上下文信息,比如操作员信息、追踪ID等。这些信息通常通过HTTP请求头来传递。Spring Cloud OpenFeign是一个声明式HTTP客户端,它简化了编写HTTP客户端的过程。但是,默认情况下它并不会自动传递当前请求的上下文信息。本文将介绍如何通过实现 RequestInterceptor 接口来自定义请求拦截器,以解决这一问题。

在基于Spring Boot的微服务应用中,使用OpenFeign与其他服务交互时,需要在指定请求中包含操作员的信息(例如账号名和姓名),以便于审计和日志记录。

二、解决方案:请求拦截器

请求拦截器是Feign的一个特性,它在请求发送给服务端之前对其进行修改。这可以用来添加认证信息、日志跟踪ID或者其他任何你想在请求头中携带的信息。

我们可以通过实现 RequestInterceptor 接口来创建一个自定义的请求拦截器,在发起远程调用前将必要的请求头添加到请求中。此外,还需要配置FeignClient来使用这个拦截器。

在这里插入图片描述

三、核心代码

1. 请求拦截器

首先,我们需要创建一个实现了 RequestInterceptor 接口的类。在这个类中,我们将从当前的Servlet请求上下文中获取操作员信息,并将其添加到即将发送的请求模板中。

package com.example.hello_feign_client.feign.config;

import com.example.hello_common.model.header.Operator;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Slf4j
public class OperatorRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return;
        }

        HttpServletRequest request = attributes.getRequest();
        String operator = request.getHeader(Operator.HEADER_NAME);
        log.info("operator: {}", operator);
        requestTemplate.header(Operator.HEADER_NAME, operator);
    }

}

2. 配置FeignClient

定义FeignClient时,指定使用上面创建的请求拦截器。

package com.example.hello_feign_client.feign.client;

import com.example.hello_common.model.param.RoleParam;
import com.example.hello_feign_client.feign.config.OperatorRequestInterceptor;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(contextId = "roleFeignClient",
        name = "hello-feign-server",
        url = "http://localhost:9001",
        path = "/roles",
        configuration = OperatorRequestInterceptor.class
)
public interface RoleFeignClient {

    @PostMapping
    void addRole(@RequestBody RoleParam param);

}

通过上述步骤,我们可以保证指定Feign客户端发出请求时,都能自动地将当前操作员的信息作为请求头的一部分传递给目标服务。这对于实现统一的日志记录和安全审计是非常有帮助的。

四、数据模型

Operator

package com.example.hello_common.model.header;

import lombok.Data;

/**
 * 操作人员
 */
@Data
public class Operator {

    public static final String HEADER_NAME = "X-Operator";

    private String account;

    private String name;

}

RoleParam

package com.example.hello_common.model.param;

import lombok.Data;

@Data
public class RoleParam {

    /**
     * 角色名称:一个易于理解的名字,用来描述角色的功能或职责。
     */
    private String name;

    /**
     * 描述:关于角色目的和功能的详细说明。
     */
    private String description;

}

五、目标服务

目标服务,接收请求头,解析成 Operator 对象。

OperatorHandler

package com.example.hello_feign_server.core.header;

import com.example.hello_common.exception.BusinessException;
import com.example.hello_common.model.header.Operator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

@Slf4j
@Component
@RequiredArgsConstructor
public class OperatorHandler {

    private final HttpServletRequest request;

    private final ObjectMapper objectMapper;

    public Operator getOperator() {
        String operator = request.getHeader(Operator.HEADER_NAME);
        log.info("operator: {}", operator);
        if (!StringUtils.hasText(operator)) {
            return new Operator();
        }

        String decodedOperator = URLDecoder.decode(operator, StandardCharsets.UTF_8);
        log.info("decodedOperator: {}", decodedOperator);
        try {
            return objectMapper.readValue(decodedOperator, Operator.class);
        } catch (JsonProcessingException e) {
            throw new BusinessException(e.toString());
        }
    }

}

RoleController

package com.example.hello_feign_server.web.role.controller;

import com.example.hello_common.model.param.RoleParam;
import com.example.hello_feign_server.core.header.OperatorHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/roles")
@RequiredArgsConstructor
public class RoleController {

    private final OperatorHandler operatorHandler;

    @PostMapping
    public void addRole(@RequestBody RoleParam param) {
        log.info("新增角色。param={}", param);
        log.info("操作人员:{}", operatorHandler.getOperator());
    }

}

六、测试

客户端服务 RoleController ,调用 FeignClient 的 新增角色 接口。

FeignClient 从当前的请求中解析出 操作人员 信息,然后传递给下游服务。

目标服务 的 RoleController 中,通过 OperatorHandler 获取到传递过来的 操作人员 信息,并打印日志。

调用客户端服务接口

在这里插入图片描述

发送请求:

curl --location --request POST 'http://localhost:9002/roles' \
--header 'X-Operator: %7B%22account%22%3A%22zhangsan%22%2C%22name%22%3A%22%E5%BC%A0%E4%B8%89%22%7D' \
--header 'User-Agent: Apifox/1.0.0 (https://www.apifox.cn)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: localhost:9002' \
--header 'Connection: keep-alive' \
--data-raw '{
    "name": "客服",
    "description": "负责处理客户的咨询和投诉,提供技术支持和服务。他们需要访问客户历史记录、服务请求和产品信息。"
}'

客户端 RoleController

package com.example.hello_feign_client.web.role.controller;

import com.example.hello_common.model.param.RoleParam;
import com.example.hello_feign_client.feign.client.RoleFeignClient;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/roles")
@RequiredArgsConstructor
public class RoleController {

    private final RoleFeignClient roleFeignClient;

    @PostMapping
    public void addRole(@RequestBody RoleParam param) {
        roleFeignClient.addRole(param);
    }

}

客户端服务日志

在这里插入图片描述

2024-09-02T23:26:32.577+08:00  INFO 12808 --- [hello-feign-client] [nio-9002-exec-5] c.e.h.f.c.OperatorRequestInterceptor     : operator: %7B%22account%22%3A%22zhangsan%22%2C%22name%22%3A%22%E5%BC%A0%E4%B8%89%22%7D

目标服务日志

在这里插入图片描述

2024-09-02T23:26:32.590+08:00  INFO 17700 --- [hello-feign-server] [nio-9001-exec-7] c.e.h.w.role.controller.RoleController   : 新增角色。param=RoleParam(name=客服, description=负责处理客户的咨询和投诉,提供技术支持和服务。他们需要访问客户历史记录、服务请求和产品信息。)
2024-09-02T23:26:32.591+08:00  INFO 17700 --- [hello-feign-server] [nio-9001-exec-7] c.e.h.core.header.OperatorHandler        : operator: %7B%22account%22%3A%22zhangsan%22%2C%22name%22%3A%22%E5%BC%A0%E4%B8%89%22%7D
2024-09-02T23:26:32.592+08:00  INFO 17700 --- [hello-feign-server] [nio-9001-exec-7] c.e.h.core.header.OperatorHandler        : decodedOperator: {"account":"zhangsan","name":"张三"}
2024-09-02T23:26:32.592+08:00  INFO 17700 --- [hello-feign-server] [nio-9001-exec-7] c.e.h.w.role.controller.RoleController   : 操作人员:Operator(account=zhangsan, name=张三)
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐