背景需求
springmvc 可以直接通过拦截器Interceptor和过滤器filter拦截请求头header,从而获取必要的验证信息作为我们业务逻辑服务。比如权限验证,多租户的权限范围等等。
但是在springcloud中微服务的调用其实最终也是远程调用了http,那么能不能在客户端调用的时候发给服务端的header中添加自定义的信息呢,比如业务线ID或者auhtor信息等等进而和springmvc的业务逻辑部分统一避免额外的代码实现。
Feign设置header目前有两种方式
1.实现拦截器RequestInterceptor 统一处理
2.手工创建Feignclient配置拦截器
3.RequestInterceptor +ThreadLocal实现动态传参
拦截器统一处理
我们可以直接在接口的interface的公共包中加入如下配置,这样同样的调用都会经过这个拦截器处理,进而实现统一的header处理。
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@Slf4j
public class HeaderInterceptor implements RequestInterceptor {
@Value("${feign.contract-op.businessId:sys}")
private String businessId;
@Override
public void apply(RequestTemplate requestTemplate) {
log.info("requestTemplate.header get value feign.contract-op.businessId: {}= {} ",ContractRequestHeaders.BUSINESS_ID,businessId);
if(businessId!=null && !"sys".equals(businessId)){
requestTemplate.header("businessId", businessId);
}
}
}
接口类这里直接配置为configuration,这样每次调用时它都执行一次HeaderInterceptor 拦截器
@FeignClient(
url = "${feign.contract-op.url:https://192.168.1.1:50003/contract-op-service}/billInfo",
name = "contractOpClient",
contextId = "billInfoClient",configuration = ContractFeignConfiguration.class
)
public interface BillInfoClient {
@ApiOperation(value = "分页")
@GetMapping
PageInfo<BillInfoListResponse> page(@SpringQueryMap @Validated BillInfoListRequest searchRequest);
}
如果我们不在这个configuration 配置,而是HeaderInterceptor 加上@Configuration注解,那么它会对所有全局的feignclien都会加上这个参数处理。
手工创建
也有文章提供了Feign.builder()手工创建的方式
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
public DeviceSeriesClient deviceSeriesClient(String businessId) {
DeviceSeriesClient deviceSeriesClient = Feign.builder().client(client)
.encoder(new SpringEncoder(this.messageConverters))
.decoder(new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters))))
.contract(new SpringMvcContract())
.requestInterceptor(template -> template.header(HEADER_KEY, businessId))
.target(DeviceSeriesClient.class, "https://192.168.1.1:50001/user-service/series");
return deviceSeriesClient;
}
这里encoder,decoder它改成了feign默认使用的是SpringEncoder,但是我这里Feign调用服务报错:Load balancer does not have available server for client:xxx
可能是我这边使用nacos作为服务发现和注册中心,获取到的url是注册中信的地址无法进行调用。
RequestInterceptor +ThreadLocal实现动态传参
由于提供方的代码是搭好版本的,只能调用方自行实现,因此在以上都不
我想到的另一个办法,
1.配置全局移除已经存在的header中的key值,防止该key多值数组
2.利用filter拦截前端请求过来的业务ID存入ThreadLocal,然后放入到对应feignclient的requestTemplate中
这样不管是前端还是feign客户端都可以统一在这个地方进行业务参数的加入,同时也支持不同业务参数根据前端需要动态的传递不同值的问题。
import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.deepblueai.cloud.robotics.webapi.exception.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class RequestParameterFilter extends GenericFilterBean implements RequestInterceptor {
static ThreadLocal<String> localVar = new ThreadLocal<>();
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.removeHeader("businessId");
String businessId =localVar.get();
if(businessId!=null){
log.info("feign RequestInterceptor businessId = {}",businessId);
requestTemplate.header("businessId", businessId);
}
localVar.remove();
log.info("feign RequestInterceptor apply end");
}
// 放行swagger-ui相应的请求路径
public static final String[] ALLOW_URL = {"/**/swagger**","/**/webjars/**",
"/**/swagger-resources/**","/**/v2/api-docs**","/**/csrf","/**/error"};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String uri = request.getRequestURI();
log.info("request uri :" + uri);
if (isAllow(uri)) {
filterChain.doFilter(servletRequest, response);
return;
}
String businessId = request.getHeader(ContractRequestHeaders.BUSINESS_ID);
log.info("Mvc header uri={},businessId={}",request.getRequestURI(),businessId);
if (businessId == null) {
log.error("businessId is null");
throw new ApiException(ExceptionEnum.business_ID_NULL_ERROR.getCode(), "businessId不能为空");
}else {
/*Map<String, Object> map = new HashMap<String, Object>(16);
map.put("businessId", businessId);
RequestParameterWrapper requestParameterWrapper = new RequestParameterWrapper(request, map);*/
log.info("Mvc ParameterFilter uri={},businessId={}",request.getRequestURI(),request.getParameter("businessId"));
//feign header set localval
localVar.set(businessId);
//filterChain.doFilter(requestParameterWrapper, response);
}
filterChain.doFilter(servletRequest, response);
}
}
public static boolean isAllow(String url) {
boolean flag = false;
for (String pattern : ALLOW_URL) {
AntPathMatcher matcher = new AntPathMatcher();
if (matcher.match(pattern, url)) {
flag = true;
break;
}
}
return flag;
}
@Override
public void destroy() {
//执行两次,非feign业务也执行一次
localVar.remove();
}
}
因为spring的http是使用的线程池,因此使用ThreadLocal时每次使用完要清理下,避免重复使用线程会互相残留问题。