天天看點

利用Spring擴充點模拟Feign實作遠端調用(幹貨滿滿)

作者:java易
利用Spring擴充點模拟Feign實作遠端調用(幹貨滿滿)

一、思路與編碼分析

一般,我們會這麼使用Feign:

利用Spring擴充點模拟Feign實作遠端調用(幹貨滿滿)

可以看到這裡是一個interface(接口),也就是沒有具體的實作類。

那麼知道對于接口是沒有辦法直接new的,那麼Spring也就無法擷取到具體的實作類,然後進行注入,放到IOC容器中。

思考1:接口沒有實作類?

這裡我們就要思考,隻有接口,沒有實作類的話,這種情況下,一般要怎麼搞?

一種思路就是動态代理,對于動态代理都可以把一些功能通用化。大部分的架構也是這麼實作的,比如Feign,還有MyBatis的注解程式設計。

思考2:接口怎麼掃描以及如何注入?

當使用注解編寫了接口之後,那麼這個接口是如何被掃描以及如何被注入的呢?

對于掃描的話,一般有兩種方式:

(1)其一就是直接在接口上有一個注解,比如Mybatis就是使用了注解@Mapper。

(2)其二全局注解接口包掃描,比如Mybatis使用了@MapperScan

那麼這些類是如何被注入呢?

這裡很明顯就需要使用到Spring的一些擴充點了,比如:導入bean定義以及工廠bean:

(1)ImportBeanDefinitionRegistrar:使用import的方式注冊bean定義,在這裡會擷取到所有注解了@FeignClient注解的接口,擷取建構bean定義資訊,具體的實作類呢?使用動态代理進行實作。

(2)FactoryBean:具體的實作類實作了接口FactoryBean,在這裡會傳回構造的對象,這個對象當然是代理對象。

編碼分析

在編碼分析之前,我們看下最後的一個代碼結構:

利用Spring擴充點模拟Feign實作遠端調用(幹貨滿滿)

(1)注解EnableFeignClients:啟用FeignClient以及指定要掃描的@FeignClient的包路徑。在這個注解上有一個重要的注解@Imort導入一個類FeignClientsRegistrar。

(2)FeignClientsRegistrar:實作接口ImportBeanDefinitionRegistrar,主要是擷取注解EnableFeignClients的包資訊,然後掃描該包下注解了@FeiClient的接口資訊,并且添加bean定義,以及指定具體的實作類FeignFactoryBean。

(3)FeignFactoryBean:@FeignClient注解的接口具體的實作類。

(4)注解FeignMethod:用于注解請求的接口資訊。

二、模拟FeignClient的實作

接下來,模拟FeignClient來看看,但這裡還是有一些差別,這裡的關鍵是我們要學習一下,如何使用Spring的擴充點,在實際的項目中進行使用。很多開源的架構都是利用了Spring的擴充點進行和Spring內建的。

2.1 建立項目

使用idea建立一個Spring Boot項目,取名為spring-boot-myfeign-example,引入依賴starter-web。

這些在每次的項目講解的時候,我都很詳細的說明了,這次就展開了,不懂的看之前的文章。

2.2 @EnableFeignClients

使用注解@EnableFeignClients進行啟用,具體的代碼如下所示:

package com.kfit.config;


import org.springframework.context.annotation.Import;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 通過 @EnableFeignClients 引入了FeignClientsRegistrar用戶端注冊類
 *
 * @author java易「頭條号SpringBoot」
 * @date 2023-02-23
 * @slogan 大道至簡 悟在天成
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(FeignClientsRegistrar.class) //FeignClientsRegistrar
public @interface EnableFeignClients {
    String basePackages();
}

           

說明:

(1)注解@EnableFeignClients就是啟用FeignClient的一個注解,主要用于指定要掃描的包路徑以及引入一個bean定義的注冊類。

(2)@Import:引入Bean定義的注冊類FeignClientsRegistrar,這個類的具體實作看後面。

(3)其它注解,比如@Target、@Retention是元注解(注解的注解稱為元注解)。

2.3 @FeignClient

注解FeiClient主要是用于接口,用于掃描過濾使用的:

package com.kfit.config;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 類注解,用于掃描過濾
 *
 * @author java易「頭條号SpringBoot」
 * @date 2022-02-23
 * @slogan 大道至簡 悟在天成
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FeiClient {
    String name();
}

           

2.4 @FeignMethod

注解@FeignMethod是指定請求的接口以及方法:

package com.kfit.config;


import org.springframework.http.HttpMethod;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 方法注解
 *
 * @author java易「頭條号SpringBoot」
 * @date 2022-02-23
 * @slogan 大道至簡 悟在天成
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignMethod {
    String path();
    HttpMethod method() default HttpMethod.POST;
}

           

2.5 FeignClientsRegistrar

FeignClientsRegistrar實作了接口ImportBeanDefinitionRegistrar,實作有點複雜,具體代碼如下示例:

package com.kfit.config;


import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;


import java.io.IOException;
import java.util.Map;
import java.util.Set;


/**
 * 借助Spring的ImportBeanDefinitionRegistrar利器,在Spring初始化時注入相關BeanDefinition,
 * 必須為FactoryBean,以便于生成動态代理完成遠端調用。
 *
 * @author java易「頭條号SpringBoot」
 * @date 2022-02-23
 * @slogan 大道至簡 悟在天成
 */
public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar {


    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //擷取到注解上的屬性 : {basePackages=com.kfit.demo.feign}
        Map<String, Object>  attrs = importingClassMetadata.getAnnotationAttributes(EnableFeignClients.class.getName());
        if(attrs == null && attrs.size() == 0){
            return;
        }
        //擷取配置要掃描的包路徑:com.kfit.demo.feign
        String basePackages = String.valueOf(attrs.get("basePackages"));
        System.out.println("basePackages:"+basePackages);
        //掃描包路徑下的類.
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false){
            /**
             * 判斷資源是否為候選的元件,這裡直接傳回true。
             *
             * 如果是底層的實作的話,會通過excludeFilters和includeFilters進行判斷。
             * @return
             */
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return true;
            }
        };
        //掃描的類上的注解為:@RMIClient
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeiClient.class));
        //查找獲選的元件.
        Set<BeanDefinition> beanDefinitionSet =  scanner.findCandidateComponents(basePackages);
        System.out.println("BeanDefinition:"+beanDefinitionSet.size());
        for(BeanDefinition beanDefinition:beanDefinitionSet){
            System.out.println(beanDefinition);
            // 向構造方法中添加參數,值為目标bean的全路徑名,Spring會自動轉換成Class對象
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            beanDefinition.setBeanClassName(FeignFactoryBean.class.getName());
            registry.registerBeanDefinition(beanDefinition.getBeanClassName(),beanDefinition);
        }
    }
}

           

說明:這個類對于沒有使用Spring提供的方法進行編碼的,會有點複雜,為了大家能夠看懂,我已經在每行核心代碼都添加了注釋。

(1)首先需要擷取到@EnableFeignClients的注解屬性資訊,擷取到設定的包路徑,比如:com.kfit.*.feign。

(2)使用Spring提供的ClassPathScanning…進行掃描到BeanDefinition,主要是通過設定的包路徑和注解@FeiClient進行過濾。

(3)對擷取到的BeanDefinition資訊,進行屬性指派,最核心的一句代碼就是指定bean class為FeignFactoryBean。對于FeignFactoryBean的實作看下面代碼。

2.6 FeignFactoryBean

最後看下bean定義的具體實作類,具體代碼如下所示:

package com.kfit.config;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.http.HttpMethod;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;


/**
 *
 *
 * @author java易「頭條号SpringBoot」
 * @date 2022-02-23
 * @slogan 大道至簡 悟在天成
 */
public class FeignFactoryBean<T> implements FactoryBean<T>, InvocationHandler {
    Map<String,String> hosts = new HashMap<>();


    // 對象存儲目标類型
    private Class<T> targetClass;


    // 構造方法傳入目标類型
    public FeignFactoryBean(Class<T> targetClass) {
        hosts.put("user-service","http://127.0.0.1:8080");
        this.targetClass = targetClass;
    }


    @Override
    public T getObject() throws Exception {
        return (T) Proxy.newProxyInstance(targetClass.getClassLoader(), new Class[]{targetClass}, this);
    }


    @Override
    public Class<?> getObjectType() {
        return targetClass;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String result = null;
        //擷取方法上的注解資訊.
        FeiClient rmiClient = targetClass.getAnnotation(FeiClient.class);
        FeignMethod rmiMethod = method.getAnnotation(FeignMethod.class);
        if(rmiMethod != null && rmiClient != null){
            String path = rmiMethod.path();
            HttpMethod httpMethod = rmiMethod.method();
            System.out.println("擷取到的注解上的資訊:" + rmiClient.name() + " -- " + rmiMethod.path());
            String urlPath = hosts.get(rmiClient.name()) + path;


            System.out.println("轉換之後的請求路徑:" +urlPath);


            //有了位址就可以使用HttpURLConnection、okHttp等發起網絡請求.
            // 這裡使用HttpURLConnection模拟一下.
            result = post(urlPath);
            System.out.println("網絡請求執行的結果:" + result);
        }
        return result;
    }




    private String post(String urlPath){
        String result = "";
        try {
            URL url = new URL(urlPath);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            // 設定請求方式
            connection.setRequestMethod("POST");
            // 設定是否向HttpURLConnection輸出
            connection.setDoOutput(true);
            // 設定是否從httpUrlConnection讀入
            connection.setDoInput(true);
            // 設定是否使用緩存
            connection.setUseCaches(false);
            //設定參數類型是json格式
            connection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
            connection.connect();
            int responseCode = connection.getResponseCode();
            if(responseCode == HttpURLConnection.HTTP_OK) {
                //定義 BufferedReader 輸入流來讀取URL的響應
                BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                String line;
                while ((line = in.readLine()) != null) {
                    result += line;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }


}

           

說明:

(1)FeignFactoryBean實作了兩個接口FactoryBean以及InvocationHandler。

(2)實作FactoryBean主要是為了能夠傳回一個通過動态代理實作的對象。

(3)實作接口InvocationHandler這個就是JDK的動态代理了。

(4)getObject():使用jdk的Proxy構造了一個代理對象。注意這個方法在Spring啟動的過程中就執行了,并不是每次通路的時候才執行,是以隻執行一次。

(5)invoke():InvocationHandler的回調方法,此方法核心就是擷取注解上的資訊,構造url,然後使用相應的網絡請求工具發起請求。

(6)網絡請求方式可以是:HttpURLConnection(java原生)、OkHttp、Apache HttpClient、RestTemplate、Ribbon等發起網絡請求。

(7)這裡對于服務名稱的解析,使用了最簡單的Map存儲方式,實際架構中會比複雜很多。

在前面我們對于RestTemplate和Ribbon有了一個基本的認知,那麼這裡有一個疑問?就是Ribbon可以不依賴RestTemplate單獨使用嗎?大家自行在評論區進行讨論。

三、模拟FeignClient的使用

我們已經通過Spring的擴充點FactoryBean和ImportBeanDefinitionRegistrar模拟了FeignClient的簡單實作,那麼是否可用呢,那麼就需要寫個例子來驗證下。

3.1添加注解@EnableFeignClients

在啟動類上添加注解@EnableFeignClients:

@SpringBootApplication
@EnableFeignClients(basePackages = "com.kfit.*.feign")           

說明:指定報名為com.kfit.*.feign。

3.2添加接口

使用@FeignClient添加接口:

package com.kfit.demo.feign;


import com.kfit.config.FeiClient;
import com.kfit.config.FeignMethod;


/**
 * 請求執行個體.
 *
 * @author java易「頭條号SpringBoot」
 * @date 2022-02-23
 * @slogan 大道至簡 悟在天成
 */
@FeiClient(name = "user-service")
public interface DemoFeignClient {
    @FeignMethod(path= "/api")
    Object test();
}           

3.3使用DemoFeignClient

由于DemoFeignClient上添加了注解@FeiClient,是以就被注入使用動态代理的方式進行實作,那麼就可以直接使用這個接口了(實際上也是類):

package com.kfit.demo;


import com.kfit.demo.feign.DemoFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 *
 *
 * @author java易「頭條号SpringBoot」
 * @date 2022-02-23
 * @slogan 大道至簡 悟在天成
 */
@RestController
public class DemoController {


    @Autowired
    private DemoFeignClient demoFeignClient;


    @RequestMapping("/api")
    public Object api(){
        return "I love Angelababy!";
    }


    @RequestMapping("/test")
    public Object test(){
        return demoFeignClient.test();
    }
}

           

說明:

(1)/api:用于FeignClent進行調用。

(2)/test:用于項目使用demoFeignClient調用通路。

(3)請求說明:通過浏覽器通路/test,然後調用demoFeignClient的test()方法;然後調用代理類FeignClientsRegistrar的invoke()方法,在invoke方法中會擷取到接口DemoFeignClient上的注解資訊,建構出請求位址,通路/api;最後就是相應的網絡請求發起網絡請求,傳回接口。

3.4測試

啟動Spring Boot應用,通路如下位址進行測試:

http://127.0.0.1:8080/test

看下控制台的一些資訊列印:

利用Spring擴充點模拟Feign實作遠端調用(幹貨滿滿)

傳回資訊:I love Angelababy,你也喜歡嗎?

總結

這一小結用到的知識點特别多,大家可以先收藏起來,然後多看幾遍。利用這套思路,可以在實際項目中玩出很多花樣。為了大家更好的了解,最後在對一些要點總結一下:

(1)整個程式的入口是@EnableFeignClients,Spring Boot在啟動的時候會執行這個注解。

(2)對于@EnableFeignClients最重要的一個點就是通過@Import導入了另外一個類FeignClientsRegistrar。對于package的指定到不是最重要的,如果沒有指定可以使用Spring Boot預設掃描的包路徑,這個要怎麼擷取?你知道嗎?

(4)對于FeignClientsRegistrar是整個@FeignClient注解的接口可以執行的關鍵。在該類中主要是掃描指定包下并且注解了@FeignClient的BeanDefinition,說明接口資訊是可以被Spring掃描成為bean定義的,預設情況下Spring不掃描的,因為掃描進來了之後Spring不知道接口具體要如何實作。在FeignClientsRegistrar中添加了BeanDefinition,其中最重要的就是要設定具體的實作類Bean Class,這這裡指定為FeignFactoryBean。

(5)對于FeignFactoryBean實作了接口FactoryBean、InvocationHandler。對于接口FactoryBean主要是實作方法getObject,通過JDK的代理類Proxy傳回代理對象;對于InvocationHandler就是動态代理具體的處理方法了,在這裡會擷取到注解了@FeignClient接口上的注解資訊和方法資訊,構造出請求的URL位址,進而發起網絡請求。

最後的最後,看沒看懂不重要,先收藏起來,在慢慢品嘗,這一篇值得你好好學習一下,你面試的話,就可以裝逼了~

利用Spring擴充點模拟Feign實作遠端調用(幹貨滿滿)

原文連結;https://mp.weixin.qq.com/s/Y6sjVA7vahDGSAudx50uQA