自定义OpenFeign的FeignConfig,实现请求头传递参数

2,166 阅读5分钟

问题背景

本系统使用的是SprinCloud框架,各个微服务之间通过OpenFeign进行调用。本问题主要涉及三个模块:web模块、microService模块、webService模块。

用户通过web服务登录之后,需要查询webService接口(第三方提供),获取这个用户有权限的卡口列表信息,该卡口列表需要传递到microService模块中,在查询ES时作为默认检索条件。

解决思路

  1. 修改web服务中的FeignClient,统一添加一个请求参数。再统一修改microService模块中的接口,统一添加一个参数。
  2. 通过自定义FeignConfig,将参数通过请求头传递到microService模块中,在microService模块中使用拦截器获取即可。

OpenFeign实现参数的传递.jpg

因为使用第一种方案,对代码的改动量太大,所以使用第二种方案解决这个问题。

需要解决的问题

  1. 用户登录成功后,在哪个步骤去调用webSerivce接口查询权限信息?
  2. webService接口怎样调用?怎样解析返回的数据?
  3. 查询出来的权限数据过大,超过请求头的大小限制怎么办?
  4. 在microService的拦截器中,怎样保证拿到的数据的线程安全性?

解决方案

问题一

用户登录成功后,在哪个步骤去调用webSerivce接口查询权限信息?

在用户的登录接口中,添加代码,待用户登录成功后去调用webService接口,查询权限列表。为了不影响用户的登录时长,在这里使用RabbitMQ进行异步的处理。

问题二

webService接口怎样调用?怎样解析返回的数据?

调用webService接口的代码

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.axis.client.Call;
import org.apache.axis.encoding.XMLType;
import org.dom4j.Attribute;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;

import javax.xml.rpc.ParameterMode;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;

@Service
public class WebService {
    String webServiceUrl = "http://*********";

    public String getDateByUserId(String userId) {
        try {
            Call call = getCall();
            if (call != null) {
                call.setOperationName("你要调用的webService接口名");
                // 调用接口的参数名、参数类型、入参
                call.addParameter("调用接口的参数名", XMLType.XSD_STRING, ParameterMode.IN);
                call.addParameter(RETURNTYPE, XMLType.XSD_STRING, ParameterMode.IN);
                call.setReturnType(org.apache.axis.encoding.XMLType.XSD_STRING);
                String xml = (String) call.invoke(new Object[]{userId, "XML"});
                JSONObject json = documentToJSONObject(xml);
                // 根据自己的实际情况,修改第二个参数
                return extractValueFromJson(json, "equitment");
            }
        } catch (Exception ex) {
            loger.error("获取数据出错:", ex);
        }
        return null;
    }


    /**
     * 从json中获取想要的属性值 如果有多个用,拼接
     *
     * @param json  json字符串信息
     * @param param 传入的属性参数
     * @return xml属性值
     */
    private String extractValueFromJson(JSONObject json, String param) {
        StringBuilder sb = new StringBuilder();
        if (json.get(param) != null && !"".equals(json.get(param))) {
            Map<String, String> equipment = (Map<String, String>) ((List) json.get(param)).get(0);
            int count = 1;
            for (String value : equipment.values()){
                sb.append(value);
                if (count != equipment.values().size()){
                    sb.append(",");
                }
                count++;
            }
        }

        return sb.toString();
    }

    /**
     * org.dom4j.Document 转 com.alibaba.fastjson.JSONObject
     *
     * @param xml Xml结构体
     * @return JsonObject
     * @throws DocumentException
     */
    private JSONObject documentToJSONObject(String xml) throws DocumentException {
        return elementToJSONObject(DocumentHelper.parseText(xml).getRootElement());
    }


    /**
     * org.dom4j.Element 转 com.alibaba.fastjson.JSONObject
     *
     * @param node rootNode
     * @return jsonObject
     */
    private JSONObject elementToJSONObject(Element node) {
        JSONObject result = new JSONObject();
       // 当前节点的所有属性的list
        List<Attribute> listAttr = node.attributes();
        for (Attribute attr : listAttr) {
            result.put(attr.getName(), attr.getValue());
        }
        // 递归遍历当前节点所有的子节点
        List<Element> listElement = node.elements();// 所有一级子节点的list
        if (!listElement.isEmpty()) {
            int count = 0;
            // 遍历所有一级子节点
            for (Element e : listElement) {
                // 判断一级节点是否有属性和子节点
                if (e.attributes().isEmpty() && e.elements().isEmpty()) 
                {
                    if (result.containsKey(e.getName())) {
                        result.put(e.getName() + "-" + count, e.getTextTrim());
                        count++;
                    } else {
                    // 沒有则将当前节点作为上级节点的属性对待
                        result.put(e.getName(), e.getTextTrim());
                    }
                } else {
                    // 判断父节点是否存在该一级节点名称的属性
                    if (!result.containsKey(e.getName())) 
                        // 没有则创建
                        result.put(e.getName(), new JSONArray());
                     // 将该一级节点放入该节点名称的属性对应的值中
                    ((JSONArray) result.get(e.getName())).add(elementToJSONObject(e));
                }
            }
        }
        return result;
    }


    @Nullable
    private Call getCall() {
        try {
            org.apache.axis.client.Service service = new org.apache.axis.client.Service();
            Call call = (Call) service.createCall();
            call.setTargetEndpointAddress(getUserAuthUrl);
            return call;
        } catch (Exception ex) {
            loger.error("webservice创建异常:", ex);
        }
        return null;
    }


}

问题三

查询出来的权限数据过大,超过请求头的大小限制怎么办?

当查询出来的用户权限数据很大,我们可以将权限数据存放在Redis当中,使用userId作为key,将userId通过自定义的FeignConfig传递到microService中即可。

自定义FeignConfig

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpSession;

@Configuration
public class FeignConfig implements RequestInterceptor {
    /**
     * 从Session中获取用户登录信息,每次通过OpenFeign调用microService都会获取一次UserId
     *
     * @param requestTemplate
     */
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attrs != null){
            HttpSession session = attrs.getRequest().getSession();
            // 这一步可以获取数据是因为在用户登录时,将用户信息存放在Session当中
            UserInfo userInfo=(UserInfo)session.getAttribute(SessionConstants.USER_LOGIN_INFO);
            if (userInfo != null){
                requestTemplate.header("userId",userInfo.getUserId());
            }
        }
    }
}

使用自定义的FeignConfig

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

@FeignClient(value = "***-service",configuration = FeignConfig.class)
public interface ***Client {

    @PostMapping(value = "/***/***")
    Result<List<Object>> getdate(@RequestParam String ***);
}

问题四

在microService的拦截器中,怎样保证拿到的数据的线程安全性?

在拦截器中我们从请求头可以获取userId,因为我们后续的业务逻辑当中需要用到它,所以需要将userId声明为static的,让全局都可以访问它。

有A、B两个用户,A用户登录之后,查询数据时,B用户登录,也查询数据,此时系统当中只维护了一个userId,A、B用户肯定有一个查询会发生错误,这就是线程不安全性造成的。

我们使用ThreadLocal可以避免线程不安全性。用户A、用户B登录系统后,进行查询,是两个线不同的线程进行处理的,我们通过ThreadLocal将userId放在每个Thread的threadlocals当中,在使用userId时,从线程内获取即可。

总结

在处理这个问题的时候,首先采用的是第一种方式去解决,后来因为太麻烦弃用。
通过第二种方式,学习到的东西:

  1. webservice接口的调用以及响应信息的解析
  2. 自定义FeignConfig,通过请求头传参
  3. 使用ThreadLocal来避免线程不安全