Thrift IDL 快速入門

Rekent發表於2019-03-02

I.背景

眾所周知,Thrift是一個RPC的框架,其可用於不同語言之間的服務相互呼叫。比如最近接觸到的一個運用環境:
*前端使用Node.Js重構了部分我們的老舊程式碼(前後端未分離的SpringBoot專案),我們後端使用zookeeper+Thrift為新的Node.Js前端專案提供基本的DAO層服務支援*
所以基於這個專案,我大概瞭解了一下Thrift,該文章則均以Java為基礎語言。


II.如何入門

因為後端已經有一套服務註冊和暴露機制,所以服務已經是RPC的形式,所以我們僅需要使用Thrift IDL來重寫一遍我們需要暴露的方法即可,Thrift IDL有兩個比較好的參考選擇:

  • Thrift 型別說明可以參考官方文件: Thrift Type,簡單來說,thrift基本支援所有的Java基本型別以及引用型別:bool(boolean)、byte(byte)、i16(short)、i32(int)、i64(long)、double(double)、string(String)、binary(byte[])以及一些常用容器和自建型別(Struct);
  • Thrift IDL的書寫規範則可以參考Thrift: The Missing Guide,這個文件相較於官方文件有更多的例子可以參考。

III.TIPS

  • 很多參考和學習文件,都將servicesstruct放在一個.thrift檔案中,這種方式將service和其所需的struct繫結在一起,會導致個別struct重複出現在多個.thrift檔案中,導致大量的程式碼重複。所以,應該與Java的編碼風格保持一致,採用POJO類(struct)+介面(services)的方案,利用include關鍵字,將struct引進services中使用
  • Thrift支援基本所有的Java基本型別,但是注意是基本型別,類似於Java中的Integer、Boolean、Long、Double等基本型別包裝類是不支援的,或許你可以使用struct實現一個類似的包裝類結構進行資料承載。
  • Thrift支援enum列舉型別,但是如果沒有description的列舉型別也可以直接使用string來承接。
  • Thrift是通過序列化和反序列化來獲取對應struct結構體的資料的,所以struct中的資料順序一定要和java檔案中的一致,否則可能會出現資料對應關係錯亂或者資料丟失。
  • 在編寫struct體時,要注意java物件父級,如果父級中含有變數,不要忘記其變數的書寫,且順序一定在前面。
  • 如非必要,java物件中的常量可以不出現在thrift idlstruct結構體中。
  • 時間Date我們固定使用timestamp時間戳的形式傳遞,即long型。

IV. 例子

我們現在有一個Account物件,需要將其變為thrift檔案,Account的結構如下:

    public class Account extends BaseEntity implements SecurityUser{

		private static final long serialVersionUID = 1L;
		public static final String PASSWORD_TIME = "passwordTime";
	
		private String password;							// 密碼
		private Date createTime;							// 建立時間
		private Date lastLoginTime;							// 最後登入時間
		private int loginCount = 0;							// 登入次數
		private boolean enabled = true;
		private Date passwordTime;							//密碼修改時間
		private boolean freeze;								//賬戶是否被凍結
	
		//該賬戶的繫結資訊,非持久化欄位
		@Transient
		private Set<Binding> bindings = new HashSet();
	
		@Transient
		private String name;//儲存登入時用的使用者名稱,非持久化欄位

		//省略getter和setter
  }
複製程式碼

根據上述結構我們可以書寫如下的Account.thrift:

/**
  * 賬戶資訊
  */
struct Account{
	1: string password,	   //密碼
	2: i64 createTime,     //建立時間
	3: i64 lastLoginTime,  //最後登入時間
	4: i32 loginCount,     //登陸次數
	5: bool enabled=true,
	6: i64 passwordTime,   //密碼修改時間
	7: bool freeze,        //賬戶是否被凍結
}  
複製程式碼

但是經過測試前端呼叫介面獲取到的Account資訊,要麼資料錯位,要麼資料丟失,問題出在哪裡呢?這時,我們發現Account物件繼承了BaseEntity,實現了SecurityUser,我們去檢視一下繼承的BaseEntity物件:

public abstract class BaseEntity extends IdEntity implements Cacheable, TypeAliases{
	
	private static final long serialVersionUID = 1L;
	
	private static final String CACHE_NAMESPACE = "entity" ;

	public BaseEntity() {
		super();
	}

	public BaseEntity(Long id) {
		super(id);
	}
	
	//省略下方程式碼
}
複製程式碼

我們發現其中並沒有非常量變數,但是BaseEntity又繼承了IdEntity,所以我們得再去看一看IdEntity:

public abstract class IdEntity implements Serializable{

	private static final long serialVersionUID = -1L;

	/**
	 * Hibernate JPA環境中使用@GenericGenerator註解生成主鍵
	 */
	@Id
	@GeneratedValue(generator = "longIdGenerator")
	@GenericGenerator(name = "longIdGenerator", strategy = "net.qiyuesuo.framework.id.LongIdGenerator")
	protected Long id;
	
	public IdEntity() {
		super();
	}
	
	public IdEntity(Long id) {
		super();
		this.id = id;
	}

	//省略getter和setter
}
複製程式碼

這時我們發現IdEntity中含有一個Id的變數,所以我們需要重構一下剛剛書寫的Account.thrift

/**
  * 賬戶資訊
  */
struct Account{
	1: i64 id,			//賬戶Id
	2: string password,	 //密碼
	3: i64 createTime,  //建立時間
	4: i64 lastLoginTime, //最後登入時間
	5: i32 loginCount, //登陸次數
	6: bool enabled,
	7: i64 passwordTime,  //密碼修改時間
	8: bool freeze, //賬戶是否被凍結
}  
複製程式碼

書寫完Account.thrift後,我們需要寫其相應的介面即service,檢視Interface AccountService:

public interface AccountService {
	
	/**
	 * 建立賬戶
	 * @param account
	 * @return 返回建立後的Account物件
	 */
	Account create(Account account);
	
	/**
	 * 更新賬戶資訊
	 * @param account
	 * @return
	 */
	boolean update(Account account);
	
	/**
	 * 修改開放平臺密碼  傳入的 密碼是未加密的
	 * @param username
	 * @param newPassword
	 * @return
	 */
	boolean updatePasswd(String username, String newPassword);
	
	/**
	 * 重置密碼
	 * @param username 使用者名稱
	 * @param newPassword 新密碼
	 */
	void resetPasswd(String username, String newPassword);

	/**
	 * 驗證使用者名稱和密碼是否匹配
	 * @param username
 	* @param password
	 */
	boolean matches(String username,String password);
}
複製程式碼

有很多方法,但是如果前端只需要用到校驗使用者名稱和密碼的方法,那麼我就只需要暴露建立賬戶的方法,即:

include "Account.thrift"

service AccountService{
	/*
	 * 校驗使用者名稱和密碼
	 */
	bool matches(1: string username,2: string password),
}
複製程式碼

這樣我們就完成了一個關於使用者名稱和密碼的校驗方法的thrift idl文件的書寫,前端需要執行thrift的一條語句對檔案進行編譯,以node.js為例(具體可參考:Apache Thrift Tutorial

thrift --gen <language> <Thrift filename>

thrift -r --gen js:node Account.thrift
thrift -r --gen js:node AccountService.thrift複製程式碼