仿@FeignClient实现使用Http请求外部服务

end-of-a-journey / 2023-08-16 / 原文

因为某些原因,原本注册在同一个nacos里的部分微服务需要拆分出去,而拆分出去的那部分服务调用方式需要修改。所以为了简单省事,加个了@HttpClient注解用来替换@FeignClient。

三步走:

  1、@HttpClient注解

  2、扫描被@HttpClient注解的接口

  3、为扫描到的接口创建代理类

@HttpClient注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface HttpClient {

    HttpUrl value();
}

url枚举类

@AllArgsConstructor
@Getter
public enum HttpUrl {
    BAI_DU("https://www.baidu.com/", "百度"),

    private String url;
    private String desc;
}

扫描被@HttpClient注解的接口

@Component
public class HttpClientRegistryProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware, ResourceLoaderAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

    private final RestTemplate restTemplate;

    public HttpClientRegistryProcessor() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(5000);//ms
        factory.setConnectTimeout(15000);//ms
        restTemplate = new RestTemplate(factory);
    }

    @Override
    public void postProcessBeanDefinitionRegistry(@NotNull BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(HttpClient.class));
        Set<BeanDefinition> beanDefinitionHolders = scanner.findCandidateComponents("org.jeecg");
        for (BeanDefinition holder : beanDefinitionHolders) {
            registerHttpClientBean(holder, registry);
        }
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void registerHttpClientBean(BeanDefinition definition, BeanDefinitionRegistry registry) {
        AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) definition;
        String beanClassName = beanDefinition.getBeanClassName();
        try {
            Class<?> targetClass = Class.forName(beanClassName);
            if (targetClass.isInterface()) {
                Object proxy = Proxy.newProxyInstance(
                        targetClass.getClassLoader(),
                        new Class[]{targetClass},
                        new HttpClientInvocationHandler(targetClass, restTemplate));
                BeanDefinitionBuilder proxyBeanBuilder = BeanDefinitionBuilder.genericBeanDefinition(targetClass, (Supplier) () -> proxy);
                AbstractBeanDefinition proxyBeanDefinition = proxyBeanBuilder.getRawBeanDefinition();
                String beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry);
                registry.registerBeanDefinition(beanName, proxyBeanDefinition);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void postProcessBeanFactory(@NotNull ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // Do nothing
    }

    protected ClassPathScanningCandidateComponentProvider getScanner() {
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
            @Override
            protected boolean isCandidateComponent(@NotNull AnnotatedBeanDefinition beanDefinition) {
                boolean isCandidate = false;
                if (beanDefinition.getMetadata().isIndependent()) {
                    if (!beanDefinition.getMetadata().isAnnotation()) {
                        isCandidate = true;
                    }
                }
                return isCandidate;
            }
        };
    }

    @Override
    public void setEnvironment(@NotNull Environment environment) {
        this.environment = environment;
    }

    @Override
    public void setResourceLoader(@NotNull ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
}

 

为扫描到的接口创建代理类

public class HttpClientInvocationHandler implements InvocationHandler {

    private final RestTemplate restTemplate;
    private final String baseUrl;

    private static final List<MediaType> DEFAULT_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN);

    public static final Method HASH_CODE;
    public static final Method EQUALS;
    public static final Method TO_STRING;

    static {
        Class<Object> object = Object.class;
        try {
            HASH_CODE = object.getDeclaredMethod("hashCode");
            EQUALS = object.getDeclaredMethod("equals", object);
            TO_STRING = object.getDeclaredMethod("toString");
        } catch (NoSuchMethodException e) {
            // Never happens.
            throw new Error(e);
        }
    }

    private static final CopyOptions copyOptions = new CopyOptions(null, true);

    static {
        Editor<String> fieldNameEditor = s -> {
            if ("msg".equals(s)) {
                return "message";
            }
            return s;
        };
        copyOptions.setFieldNameEditor(fieldNameEditor);
    }


    public HttpClientInvocationHandler(Class<?> targetClass, RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
        HttpClient httpClientAnnotation = targetClass.getAnnotation(HttpClient.class);
        if (httpClientAnnotation != null) {
            this.baseUrl = httpClientAnnotation.value().getUrl();
        } else {
            this.baseUrl = null;
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.equals(HASH_CODE)) {
            return objectHashCode(proxy);
        }
        if (method.equals(EQUALS)) {
            return objectEquals(proxy, args[0]);
        }
        if (method.equals(TO_STRING)) {
            return objectToString(proxy);
        }
        // 对接口默认方法的支持
        if (method.isDefault()) {
            Class<?> declaringClass = method.getDeclaringClass();
            Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
            constructor.setAccessible(true);
            return constructor.
                    newInstance(declaringClass, MethodHandles.Lookup.PRIVATE).
                    unreflectSpecial(method, declaringClass).
                    bindTo(proxy).
                    invokeWithArguments(args);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(DEFAULT_MEDIA_TYPES);
        String url = resolveUrl(baseUrl, method);
        Object body = null;
        HttpMethod httpMethod = null;
        if (method.isAnnotationPresent(GetMapping.class)) {
            httpMethod = HttpMethod.GET;
        } else if (method.isAnnotationPresent(PostMapping.class)) {
            httpMethod = HttpMethod.POST;
        } else if (method.isAnnotationPresent(PutMapping.class)) {
            httpMethod = HttpMethod.PUT;
        } else if (method.isAnnotationPresent(DeleteMapping.class)) {
            httpMethod = HttpMethod.DELETE;
        }
        if (httpMethod == null) {
            throw new RuntimeException("@HttpClient不支持的请求方式");
        }
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(url);
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            RequestParam requestParamAnnotation = AnnotationUtils.findAnnotation(parameter, RequestParam.class);
            if (requestParamAnnotation != null) {
                String name = requestParamAnnotation.value();
                name = !"".equals(name) ? name : parameter.getName();
                uriComponentsBuilder.queryParam(name, args[i]);
                continue; // 应该不可能同一个参数同时标注多个吧。
            }
            RequestBody requestBodyAnnotation = AnnotationUtils.findAnnotation(parameter, RequestBody.class);
            if (requestBodyAnnotation != null) {
                body = args[i]; // 应该没人会传两个body吧
                continue;
            }
            RequestHeader requestHeaderAnnotation = AnnotationUtils.findAnnotation(parameter, RequestHeader.class);
            if (requestHeaderAnnotation != null) {
                String name = requestHeaderAnnotation.value();
                name = !"".equals(name) ? name : parameter.getName();
                headers.set(name, String.valueOf(args[i]));
                continue;
            }
        }
        HttpEntity<?> httpEntity = new HttpEntity<>(body, headers);
        ResponseEntity<String> exchange = restTemplate.exchange(uriComponentsBuilder.toUriString(), httpMethod, httpEntity, String.class);
        String res = exchange.getBody();
        JSONObject jsonObject = JSON.parseObject(res);
        Map<String, Object> innerMap = jsonObject.getInnerMap();
        Class<?> returnType = method.getReturnType();
        return BeanUtil.mapToBean(innerMap, returnType, false, copyOptions);
    }

    private String resolveUrl(String baseUrl, Method method) {
        GetMapping getMapping = method.getAnnotation(GetMapping.class);
        if (getMapping != null) {
            String path = getMapping.value()[0];
            return baseUrl + path;
        }
        PostMapping postMapping = method.getAnnotation(PostMapping.class);
        if (postMapping != null) {
            String path = postMapping.value()[0];
            return baseUrl + path;
        }
        PutMapping putMapping = method.getAnnotation(PutMapping.class);
        if (putMapping != null) {
            String path = putMapping.value()[0];
            return baseUrl + path;
        }
        DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
        if (deleteMapping != null) {
            String path = deleteMapping.value()[0];
            return baseUrl + path;
        }
        throw new RuntimeException("@HttpClient不支持的请求方式");
    }

    public String objectClassName(Object obj) {
        return obj.getClass().getName();
    }

    public int objectHashCode(Object obj) {
        return System.identityHashCode(obj);
    }

    public boolean objectEquals(Object obj, Object other) {
        return obj == other;
    }

    public String objectToString(Object obj) {
        return objectClassName(obj) + '@' + Integer.toHexString(objectHashCode(obj));
    }
}

 

使用方式

直接替换掉@FeignClient

//@FeignClient(name = "xxx-service")
@HttpClient(HttpUrl.BAI_DU)