專案地址和示例程式碼: https://github.com/lvyahui8/spring-boot-data-aggregator
背景
介面開發是後端開發中最常見的場景, 可能是RESTFul介面, 也可能是RPC介面. 介面開發往往是從各處撈出資料, 然後組裝成結果, 特別是那些偏業務的介面.
例如, 我現在需要實現一個介面, 拉取使用者基礎資訊
+使用者的部落格列表
+使用者的粉絲資料
的整合資料, 假設已經有如下三個介面可以使用, 分別用來獲取 使用者基礎資訊
,使用者部落格列表
, 使用者的粉絲資料
.
使用者基礎資訊
@Service
public class UserServiceImpl implements UserService {
@Override
public User get(Long id) {
try {Thread.sleep(1000L);} catch (InterruptedException e) {}
/* mock a user*/
User user = new User();
user.setId(id);
user.setEmail("lvyahui8@gmail.com");
user.setUsername("lvyahui8");
return user;
}
}
使用者部落格列表
@Service
public class PostServiceImpl implements PostService {
@Override
public List<Post> getPosts(Long userId) {
try { Thread.sleep(1000L); } catch (InterruptedException e) {}
Post post = new Post();
post.setTitle("spring data aggregate example");
post.setContent("No active profile set, falling back to default profiles");
return Collections.singletonList(post);
}
}
使用者的粉絲資料
@Service
public class FollowServiceImpl implements FollowService {
@Override
public List<User> getFollowers(Long userId) {
try { Thread.sleep(1000L); } catch (InterruptedException e) {}
int size = 10;
List<User> users = new ArrayList<>(size);
for(int i = 0 ; i < size; i++) {
User user = new User();
user.setUsername("name"+i);
user.setEmail("email"+i+"@fox.com");
user.setId((long) i);
users.add(user);
};
return users;
}
}
注意, 每一個方法都sleep了1s以模擬業務耗時.
我們需要再封裝一個介面, 來拼裝以上三個介面的資料.
PS: 這樣的場景實際在工作中很常見, 而且往往我們需要拼湊的資料, 是要走網路請求調到第三方去的. 另外可能有人會想, 為何不分成3個請求? 實際為了客戶端網路效能考慮, 往往會在一次網路請求中, 儘可能多的傳輸資料, 當然前提是這個資料不能太大, 否則傳輸的耗時會影響渲染. 許多APP的首頁, 看著複雜, 實際也只有一個介面, 一次性拉下所有資料, 客戶端開發也簡單.
序列實現
編寫效能優良的介面不僅是每一位後端程式設計師的技術追求, 也是業務的基本訴求. 一般情況下, 為了保證更好的效能, 往往需要編寫更復雜的程式碼實現.
但凡人皆有惰性, 因此, 往往我們會像下面這樣編寫序列呼叫的程式碼
@Component
public class UserQueryFacade {
@Autowired
private FollowService followService;
@Autowired
private PostService postService;
@Autowired
private UserService userService;
public User getUserData(Long userId) {
User user = userService.get(userId);
user.setPosts(postService.getPosts(userId));
user.setFollowers(followService.getFollowers(userId));
return user;
}
}
很明顯, 上面的程式碼, 效率低下, 起碼要3s才能拿到結果, 且一旦用到某個介面的資料, 便需要注入相應的service, 複用麻煩.
並行實現
有追求的程式設計師可能立馬會考慮到, 這幾項資料之間並無強依賴性, 完全可以並行獲取嘛, 通過非同步執行緒+CountDownLatch+Future實現, 就像下面這樣.
@Component
public class UserQueryFacade {
@Autowired
private FollowService followService;
@Autowired
private PostService postService;
@Autowired
private UserService userService;
public User getUserDataByParallel(Long userId) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
CountDownLatch countDownLatch = new CountDownLatch(3);
Future<User> userFuture = executorService.submit(() -> {
try{
return userService.get(userId);
}finally {
countDownLatch.countDown();
}
});
Future<List<Post>> postsFuture = executorService.submit(() -> {
try{
return postService.getPosts(userId);
}finally {
countDownLatch.countDown();
}
});
Future<List<User>> followersFuture = executorService.submit(() -> {
try{
return followService.getFollowers(userId);
}finally {
countDownLatch.countDown();
}
});
countDownLatch.await();
User user = userFuture.get();
user.setFollowers(followersFuture.get());
user.setPosts(postsFuture.get());
return user;
}
}
上面的程式碼, 將序列呼叫改為並行呼叫, 在有限併發級別下, 能極大提高效能. 但很明顯, 它過於複雜, 如果每個介面都為了並行執行都寫這樣一段程式碼, 簡直是噩夢.
優雅的註解實現
熟悉java的都知道, java有一種非常便利的特性 ~~ 註解. 簡直是黑魔法. 往往只需要給類或者方法上新增一些註解, 便可以實現非常複雜的功能.
有了註解, 再結合Spring依賴自動注入的思想, 那麼我們可不可以通過註解的方式, 自動注入依賴, 自動並行呼叫介面呢? 答案是肯定的.
首先, 我們先定義一個聚合介面
@Component
public class UserAggregate {
@DataProvider(id="userFullData")
public User userFullData(@DataConsumer(id = "user") User user,
@DataConsumer(id = "posts") List<Post> posts,
@DataConsumer(id = "followers") List<User> followers) {
user.setFollowers(followers);
user.setPosts(posts);
return user;
}
}
其中
@DataProvider
表示這個方法是一個資料提供者, 資料Id為userFullData
@DataConsumer
表示這個方法的引數, 需要消費資料, 資料Id為user
,posts
,followers
.
當然, 原來的3個原子服務 使用者基礎資訊
,使用者部落格列表
, 使用者的粉絲資料
, 也分別需要新增一些註解
@Service
public class UserServiceImpl implements UserService {
@DataProvider(id = "user")
@Override
public User get(@InvokeParameter("userId") Long id) {
@Service
public class PostServiceImpl implements PostService {
@DataProvider(id = "posts")
@Override
public List<Post> getPosts(@InvokeParameter("userId") Long userId) {
@Service
public class FollowServiceImpl implements FollowService {
@DataProvider(id = "followers")
@Override
public List<User> getFollowers(@InvokeParameter("userId") Long userId) {
其中
@DataProvider
與前面的含義相同, 表示這個方法是一個資料提供者@InvokeParameter
表示方法執行時, 需要手動傳入的引數
這裡注意 @InvokeParameter
和 @DataConsumer
的區別, 前者需要使用者在最上層呼叫時手動傳參; 而後者, 是由框架自動分析依賴, 並非同步呼叫取得結果之後注入的.
最後, 僅僅只需要呼叫一個統一的門面(Facade)介面, 傳遞資料Id, Invoke Parameters,以及返回值型別. 剩下的並行處理, 依賴分析和注入, 完全由框架自動處理.
@Component
public class UserQueryFacade {
@Autowired
private DataBeanAggregateQueryFacade dataBeanAggregateQueryFacade;
public User getUserFinal(Long userId) throws InterruptedException,
IllegalAccessException, InvocationTargetException {
return dataBeanAggregateQueryFacade.get("userFullData",
Collections.singletonMap("userId", userId), User.class);
}
}
如何用在你的專案中
上面的功能, 筆者已經封裝為一個spring boot starter, 併發布到maven中央倉庫.
只需在你的專案引入依賴.
<dependency>
<groupId>io.github.lvyahui8</groupId>
<artifactId>spring-boot-data-aggregator-starter</artifactId>
<version>1.0.1</version>
</dependency>
並在 application.properties
檔案中宣告註解的掃描路徑.
# 替換成你需要掃描註解的包
io.github.lvyahui8.spring.base-packages=io.github.lvyahui8.spring.example
之後, 就可以使用如下註解和 Spring Bean 實現聚合查詢
@DataProvider
@DataConsumer
@InvokeParameter
- Spring Bean
DataBeanAggregateQueryFacade
注意, @DataConsumer
和 @InvokeParameter
可以混合使用, 可以用在同一個方法的不同引數上. 且方法的所有引數必須有其中一個註解, 不能有沒有註解的引數.
專案地址和上述示例程式碼: https://github.com/lvyahui8/spring-boot-data-aggregator
後期計劃
後續筆者將繼續完善異常處理, 超時邏輯, 解決命名衝突的問題, 並進一步提高外掛的易用性, 高可用性, 擴充套件性