【SpringCloud技術專題】「原生態Fegin」開啟Fegin之RPC技術的開端,你會使用原生態的Fegin嗎?(上)

李浩宇Alex發表於2021-08-09

前提介紹

  • Feign是SpringCloud中服務消費端的呼叫框架,通常與ribbon,hystrix等組合使用。

  • 由於遺留原因,某些專案中,整個系統並不是SpringCloud專案,甚至不是Spring專案,而使用者關注的重點僅僅是簡化http呼叫程式碼的編寫。

  • 如果採用httpclient或者okhttp這樣相對較重的框架,對初學者來說編碼量與學習曲線都會是一個挑戰,而使用spring中RestTemplate,又沒有配置化的解決方案,由此想到是否可以脫離Spring cloud,獨立使用Feign。

內容簡介

Feign使得 Java HTTP 客戶端編寫更方便。Feign 靈感來源於Retrofit、JAXRS-2.0和WebSocket。Feign最初是為了降低統一繫結Denominator到HTTP API的複雜度,不區分是否支援Restful。Feign旨在通過最少的資源和程式碼來實現和HTTP API的連線。通過可定製的解碼器和錯誤處理,可以編寫任意的HTTP API。

maven依賴

  <dependency>
            <groupId>com.netflix.feign</groupId>
            <artifactId>feign-core</artifactId>
            <version>8.18.0</version>
        </dependency>
        <dependency>
            <groupId>com.netflix.feign</groupId>
            <artifactId>feign-jackson</artifactId>
            <version>8.18.0</version>
        </dependency>
        <dependency>
            <groupId>io.github.lukehutch</groupId>
            <artifactId>fast-classpath-scanner</artifactId>
            <version>2.18.1</version>
	   </dependency>
	   <dependency>
    	<groupId>com.netflix.feign</groupId>
    	<artifactId>feign-jackson</artifactId>
        <version>8.18.0</version>
    </dependency>

定義配置類

RemoteService service = Feign.builder()
            .options(new Options(1000, 3500))
            .retryer(new Retryer.Default(5000, 5000, 3))
			.encoder(new JacksonEncoder())
            .decoder(new JacksonDecoder())
            .target(RemoteService.class, "http://127.0.0.1:8085");
  • options方法指定連線超時時長及響應超時時長
  • retryer方法指定重試策略
  • target方法繫結介面與服務端地址。
  • 返回型別為繫結的介面型別。

自定義介面

隨機定義一個遠端呼叫的服務介面,並且宣告相關的介面引數和請求地址。

通過@RequestLine指定HTTP協議及URL地址


public class User{
   String userName;
}

public interface RemoteService {
    @Headers({"Content-Type: application/json","Accept: application/json"})
    @RequestLine("POST /users/list")
    User getOwner(User user);
	@RequestLine("POST /users/list2")
    @Headers({
        "Content-Type: application/json",
        "Accept: application/json",
        "request-token: {requestToken}",
        "UserId: {userId}",
        "UserName: {userName}"
    })
    public User getOwner(@RequestBody User user,
        @Param("requestToken") String requestToken,
        @Param("userId") Long userId,
        @Param("userName") String userName);
}

服務提供者

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping(value="users")
public class UserController {
    @RequestMapping(value="/list",method={RequestMethod.GET,RequestMethod.POST,RequestMethod.PUT})
    @ResponseBody
    public User list(@RequestBody User user) throws InterruptedException{
        System.out.println(user.getUsername());
        user.setId(100L);
        user.setUsername(user.getUsername().toUpperCase());
        return user;
    }
}

呼叫

與呼叫本地方法相同的方式呼叫feign包裝的介面,直接獲取遠端服務提供的返回值。

String result = service.getOwner(new User("scott"));

原生Feign的兩個問題

  1. 原生Feign只能一次解析一個介面,生成對應的請求代理物件,如果一個包裡有多個呼叫介面就要多次解析非常麻煩。

  2. Feign生成的呼叫代理只是一個普通物件,該如何註冊到Spring中,以便於我們可以使用@Autowired隨時注入。

解決方案:

  1. 針對多次解析的問題,可以通過指定掃描包路徑,然後對包中的類依次解析。

  2. 實現BeanFactoryPostProcessor介面,擴充套件Spring容器功能。

定義一個註解類

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignApi {
/**
* 呼叫的服務地址
* @return
*/
String serviceUrl();
}

生成Feign代理並註冊到Spring實現類:

import feign.Feign;
import feign.Request;
import feign.Retryer;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class FeignClientRegister implements BeanFactoryPostProcessor{

	//掃描的介面路徑
    private String  scanPath="com.xxx.api";

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        List<String> classes = scan(scanPath);
        if(classes==null){
            return ;
        }
        System.out.println(classes);
        Feign.Builder builder = getFeignBuilder();
        if(classes.size()>0){
            for (String claz : classes) {
                Class<?> targetClass = null;
                try {
                    targetClass = Class.forName(claz);
                    String url=targetClass.getAnnotation(FeignApi.class).serviceUrl();
                    if(url.indexOf("http://")!=0){
                        url="http://"+url;
                    }
                    Object target = builder.target(targetClass, url);
                    beanFactory.registerSingleton(targetClass.getName(), target);
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }
            }
        }
    }

    public Feign.Builder getFeignBuilder(){
        Feign.Builder builder = Feign.builder()
                .encoder(new JacksonEncoder())
                .decoder(new JacksonDecoder())
                .options(new Request.Options(1000, 3500))
                .retryer(new Retryer.Default(5000, 5000, 3));
        return builder;
    }

    public List<String> scan(String path){
        ScanResult result = new FastClasspathScanner(path).matchClassesWithAnnotation(FeignApi.class, (Class<?> aClass) -> {
        }).scan();
        if(result!=null){
            return result.getNamesOfAllInterfaceClasses();
        }
        return  null;
    }
}
呼叫介面編寫示例:
import com.xiaokong.core.base.Result;
import com.xiaokong.domain.DO.DeptRoom;
import feign.Headers;
import feign.Param;
import feign.RequestLine;
import com.xiaokong.register.FeignApi;

import java.util.List;

@FeignApi(serviceUrl = "http://localhost:8085")
public interface RoomApi {
    @Headers({"Content-Type: application/json","Accept: application/json"})
    @RequestLine("GET /room/selectById?id={id}")
    Result<DeptRoom> selectById(@Param(value="id") String id);
    @Headers({"Content-Type: application/json","Accept: application/json"})
    @RequestLine("GET /room/test")
    Result<List<DeptRoom>> selectList();
}
介面使用示例:
@Service
public class ServiceImpl{
    //將介面注入要使用的bean中直接呼叫即可
    @Autowired
    private RoomApi roomApi;
    @Test
    public void demo(){
        Result<DeptRoom> result = roomApi.selectById("1");
        System.out.println(result);
    }
}

注意事項:

  1. 如果介面返回的是一個複雜的巢狀物件,那麼一定要明確的指定泛型,因為Feign在解析複雜物件的時候,需要通過反射獲取介面返回物件內部的泛型型別才能正確使用Jackson解析。如果不明確的指明型別,Jackson會將json物件轉換成一個LinkedHashMap型別。

  2. 如果你使用的是的Spring,又需要通過http呼叫別人的介面,都可以使用這個工具來簡化呼叫與解析的操作。

相關文章