SpringBoot實現(xiàn)本地與遠程方法調(diào)用的無縫切換
一、引言
公司業(yè)務發(fā)展過程中,前期一般需要快速實現(xiàn)產(chǎn)品的MVP版本,用于市場驗證。
此時選擇單體架構(gòu)有助于快速完成MVP版本的功能開發(fā),降低前期的投入成本。
但往往我們又需要考慮未來如果業(yè)務發(fā)展起來,用戶數(shù)、訪問量等快速增長的情況下,如何能讓單體架構(gòu)又快速切換到微服務架構(gòu)以很好的支撐產(chǎn)品的正常運行。
在將單體架構(gòu)切換為微服務架構(gòu)的過程中,存在一個關鍵問題就是將原來的本地調(diào)用需要切換為遠程調(diào)用。如果前期實現(xiàn)未預留相關切換機制,這個過程勢必會存在大量的代碼修改甚至重寫。
那么如何能在不修改大量業(yè)務代碼的情況下,實現(xiàn)從本地方法調(diào)用到遠程方法調(diào)用(RPC)的平滑切換?
本文將分享一種實現(xiàn)方案,能夠以統(tǒng)一的方式編寫代碼,而無需關心底層是本地方法調(diào)用還是遠程服務調(diào)用,通過配置即可實現(xiàn)方法調(diào)用的平滑切換。
二、技術背景
在深入討論實現(xiàn)方案前,我們先了解一下相關的技術概念:
本地調(diào)用:在同一個JVM內(nèi),直接通過方法調(diào)用實現(xiàn),性能高,無網(wǎng)絡開銷
遠程調(diào)用:跨JVM、跨網(wǎng)絡的服務調(diào)用,涉及序列化/反序列化、網(wǎng)絡傳輸?shù)?,有性能損耗
三、設計思路
實現(xiàn)本地遠程方法調(diào)用的無縫切換,核心思路是設計一個統(tǒng)一的抽象層,該層能夠:
- 1. 提供統(tǒng)一的API接口
- 2. 根據(jù)配置或運行環(huán)境決定使用本地實現(xiàn)還是遠程調(diào)用
- 3. 對上層業(yè)務代碼透明,無需修改業(yè)務邏輯
基于這一思路,我們可以設計如下幾個關鍵組件
- 統(tǒng)一服務接口:定義業(yè)務服務的API
- 本地實現(xiàn)類:接口的本地具體實現(xiàn)
- 遠程代理類:通過Feign等技術實現(xiàn)的遠程調(diào)用代理
- 自動配置:基于SpringBoot的自動配置機制實現(xiàn)動態(tài)切換
四、實現(xiàn)方案
4.1 定義統(tǒng)一服務接口
首先,我們需要定義服務接口。這些接口將同時作為本地實現(xiàn)和遠程調(diào)用的契約。
// 用戶服務接口
public interface UserService {
User getUserById(Long id);
List<User> getAllUsers();
User createUser(User user);
void updateUser(User user);
void deleteUser(Long id);
}
4.2 創(chuàng)建本地實現(xiàn)
為服務接口提供本地實現(xiàn):
@Service
@ConditionalOnProperty(name = "service.mode", havingValue = "local", matchIfMissing = true)
public class UserServiceLocalImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Override
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Override
public User createUser(User user) {
return userRepository.save(user);
}
@Override
public void updateUser(User user) {
userRepository.save(user);
}
@Override
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
4.3 定義遠程接口
使用OpenFeign定義遠程調(diào)用接口:
@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceFeignClient {
@GetMapping("/api/users/{id}")
User getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<User> getAllUsers();
@PostMapping("/api/users")
User createUser(@RequestBody User user);
@PutMapping("/api/users")
void updateUser(@RequestBody User user);
@DeleteMapping("/api/users/{id}")
void deleteUser(@PathVariable("id") Long id);
}
4.4 創(chuàng)建遠程實現(xiàn)代理
將Feign客戶端封裝為符合服務接口的實現(xiàn):
@Service
@ConditionalOnProperty(name = "service.mode", havingValue = "remote")
public class UserServiceRemoteImpl implements UserService {
@Autowired
private UserServiceFeignClient feignClient;
@Override
public User getUserById(Long id) {
return feignClient.getUserById(id);
}
@Override
public List<User> getAllUsers() {
return feignClient.getAllUsers();
}
@Override
public User createUser(User user) {
return feignClient.createUser(user);
}
@Override
public void updateUser(User user) {
feignClient.updateUser(user);
}
@Override
public void deleteUser(Long id) {
feignClient.deleteUser(id);
}
}
4.5 配置類
創(chuàng)建自動配置類,根據(jù)配置選擇合適的實現(xiàn):
@Configuration
@EnableFeignClients(basePackages = "com.example.service.feign")
public class ServiceConfiguration {
@Bean
@ConditionalOnProperty(name = "service.mode", havingValue = "remote")
public UserService userServiceRemote(UserServiceFeignClient feignClient) {
return new UserServiceRemoteImpl(feignClient);
}
@Bean
@ConditionalOnProperty(name = "service.mode", havingValue = "local", matchIfMissing = true)
public UserService userServiceLocal(UserRepository userRepository) {
return new UserServiceLocalImpl(userRepository);
}
}
4.6 實現(xiàn)動態(tài)切換
通過配置屬性實現(xiàn)動態(tài)切換:
# application.yml service: mode: local # 可選值: local, remote
業(yè)務代碼中的使用方式保持一致:
@Service
public class UserBusinessService {
@Autowired
private UserService userService; // 自動注入適合的實現(xiàn)
public void processUser(Long userId) {
User user = userService.getUserById(userId);
// 處理用戶數(shù)據(jù)...
}
}
五、進階實現(xiàn)
5.1 混合模式
在某些場景下,我們可能希望部分服務使用本地調(diào)用,部分服務使用遠程調(diào)用。這可以通過更細粒度的配置實現(xiàn):
service: user: local order: remote product: local
然后調(diào)整條件配置:
@ConditionalOnProperty(name = "service.user", havingValue = "local", matchIfMissing = true)
public class UserServiceLocalImpl implements UserService {
// ...
}
@ConditionalOnProperty(name = "service.user", havingValue = "remote")
public class UserServiceRemoteImpl implements UserService {
// ...
}
5.2 利用AOP實現(xiàn)智能路由
我們可以使用AOP實現(xiàn)更智能的路由策略,例如根據(jù)負載、性能等因素動態(tài)決定是使用本地調(diào)用還是遠程調(diào)用:
/**
* 標記支持智能路由的方法,會根據(jù)負載情況自動選擇本地或遠程執(zhí)行
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SmartRouting {
/**
* 是否啟用智能路由,默認為true
*/
boolean enabled() default true;
/**
* 指定遠程服務名,如果為空則自動推斷
*/
String serviceName() default "";
}
@Aspect
@Component
public class SmartRoutingAspect {
private static final Logger logger = LoggerFactory.getLogger(SmartRoutingAspect.class);
@Autowired
private SmartLoadBalancingService loadBalancingService;
@Autowired
private ApplicationContext applicationContext;
@Around("@annotation(com.example.annotation.SmartRouting) || @within(com.example.annotation.SmartRouting)")
public Object routeService(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 獲取方法或類上的注解
SmartRouting annotation = method.getAnnotation(SmartRouting.class);
if (annotation == null) {
annotation = method.getDeclaringClass().getAnnotation(SmartRouting.class);
}
// 如果注解被禁用,直接本地執(zhí)行
if (annotation != null && !annotation.enabled()) {
return joinPoint.proceed();
}
long startTime = System.currentTimeMillis();
boolean isLocal = loadBalancingService.shouldRouteLocally(method);
boolean success = true;
try {
Object result;
if (isLocal) {
// 本地執(zhí)行
logger.debug("Executing locally: {}", method.getName());
result = joinPoint.proceed();
} else {
// 遠程執(zhí)行
logger.debug("Routing to remote service: {}", method.getName());
String serviceName = getServiceName(method, annotation);
result = invokeRemoteService(method, joinPoint.getArgs(), serviceName);
}
return result;
} catch (Throwable t) {
success = false;
throw t;
} finally {
long executionTime = System.currentTimeMillis() - startTime;
loadBalancingService.recordExecution(method, isLocal, executionTime, success);
}
}
private String getServiceName(Method method, SmartRouting annotation) {
if (annotation != null && !annotation.serviceName().isEmpty()) {
return annotation.serviceName();
}
// 從方法所在類名推斷服務名
String className = method.getDeclaringClass().getSimpleName();
return className.replaceAll("Service$", "")
.replaceAll("([a-z])([A-Z])", "$1-$2")
.toLowerCase();
}
private Object invokeRemoteService(Method method, Object[] args, String serviceName) throws Throwable {
// 查找對應服務的Feign客戶端
Class<?> interfaceClass = method.getDeclaringClass();
String feignClientName = interfaceClass.getName() + "FeignClient";
try {
// 嘗試直接按名稱查找
Object feignClient = applicationContext.getBean(feignClientName);
return method.invoke(feignClient, args);
} catch (Exception e) {
// 如果按名稱找不到,嘗試按類型查找
try {
Class<?> feignClientClass = Class.forName(feignClientName);
Object feignClient = applicationContext.getBean(feignClientClass);
return method.invoke(feignClient, args);
} catch (Exception ex) {
logger.error("Failed to find or invoke remote service: {}", serviceName, ex);
throw new RuntimeException("Remote service invocation failed", ex);
}
}
}
}
/**
* 智能負載均衡服務實現(xiàn)
* 基于方法調(diào)用的性能指標和系統(tǒng)負載狀態(tài)做出路由決策
*/
@Service
public class SmartLoadBalancingService {
private static final Logger logger = LoggerFactory.getLogger(SmartLoadBalancingService.class);
// 方法執(zhí)行統(tǒng)計數(shù)據(jù)
private static class MethodStats {
final AtomicInteger localCallCount = new AtomicInteger(0);
final AtomicInteger remoteCallCount = new AtomicInteger(0);
final AtomicLong localTotalTimeMs = new AtomicLong(0);
final AtomicLong remoteTotalTimeMs = new AtomicLong(0);
final AtomicInteger localErrorCount = new AtomicInteger(0);
final AtomicInteger remoteErrorCount = new AtomicInteger(0);
// 獲取本地平均執(zhí)行時間
double getLocalAvgTimeMs() {
int count = localCallCount.get();
return count > 0 ? (double) localTotalTimeMs.get() / count : 0;
}
// 獲取遠程平均執(zhí)行時間
double getRemoteAvgTimeMs() {
int count = remoteCallCount.get();
return count > 0 ? (double) remoteTotalTimeMs.get() / count : 0;
}
// 獲取本地錯誤率
double getLocalErrorRate() {
int count = localCallCount.get();
return count > 0 ? (double) localErrorCount.get() / count : 0;
}
// 獲取遠程錯誤率
double getRemoteErrorRate() {
int count = remoteCallCount.get();
return count > 0 ? (double) remoteErrorCount.get() / count : 0;
}
}
// 每個方法的統(tǒng)計數(shù)據(jù)
private final Map<String, MethodStats> methodStatsMap = new ConcurrentHashMap<>();
// 系統(tǒng)負載指標
private volatile double systemLoadAverage = 0.0;
private volatile int availableProcessors = Runtime.getRuntime().availableProcessors();
@Autowired(required = false)
private DiscoveryClient discoveryClient;
// 配置參數(shù)
@Value("${loadbalancing.local-threshold:0.7}")
private double localThreshold;
@Value("${loadbalancing.performance-weight:0.6}")
private double performanceWeight;
@Value("${loadbalancing.error-weight:0.3}")
private double errorWeight;
@Value("${loadbalancing.load-weight:0.1}")
private double loadWeight;
@Value("${loadbalancing.remote-services-enabled:true}")
private boolean remoteServicesEnabled;
@PostConstruct
public void init() {
logger.info("Smart load balancing service initialized with local-threshold={}, " +
"performance-weight={}, error-weight={}, load-weight={}",
localThreshold, performanceWeight, errorWeight, loadWeight);
}
@Override
public boolean shouldRouteLocally(Method method) {
if (!remoteServicesEnabled) {
return true; // 如果遠程服務被禁用,總是本地執(zhí)行
}
// 檢查遠程服務是否可用
if (!isRemoteServiceAvailable(method)) {
return true; // 遠程服務不可用,使用本地執(zhí)行
}
String methodKey = getMethodKey(method);
MethodStats stats = methodStatsMap.computeIfAbsent(methodKey, k -> new MethodStats());
// 如果沒有足夠的統(tǒng)計數(shù)據(jù),交替使用本地和遠程
if (stats.localCallCount.get() < 10 || stats.remoteCallCount.get() < 10) {
return stats.localCallCount.get() <= stats.remoteCallCount.get();
}
// 計算決策得分,得分越高越傾向于本地執(zhí)行
double score = calculateRoutingScore(stats);
// 記錄決策過程
logger.debug("Routing decision for {}: score={}, threshold={}, route={}",
methodKey, score, localThreshold, score >= localThreshold ? "local" : "remote");
return score >= localThreshold;
}
@Override
public void recordExecution(Method method, boolean isLocal, long executionTimeMs, boolean success) {
String methodKey = getMethodKey(method);
MethodStats stats = methodStatsMap.computeIfAbsent(methodKey, k -> new MethodStats());
if (isLocal) {
stats.localCallCount.incrementAndGet();
stats.localTotalTimeMs.addAndGet(executionTimeMs);
if (!success) {
stats.localErrorCount.incrementAndGet();
}
} else {
stats.remoteCallCount.incrementAndGet();
stats.remoteTotalTimeMs.addAndGet(executionTimeMs);
if (!success) {
stats.remoteErrorCount.incrementAndGet();
}
}
// 記錄詳細統(tǒng)計數(shù)據(jù)(可用于監(jiān)控)
logger.debug("Execution recorded: method={}, local={}, time={}ms, success={}",
methodKey, isLocal, executionTimeMs, success);
}
/**
* 計算路由決策得分
*
* @param stats 方法統(tǒng)計數(shù)據(jù)
* @return 得分,范圍0-1,越高越傾向于本地執(zhí)行
*/
private double calculateRoutingScore(MethodStats stats) {
// 性能因素(本地更快得分更高)
double localAvgTime = stats.getLocalAvgTimeMs();
double remoteAvgTime = stats.getRemoteAvgTimeMs();
double performanceScore;
if (localAvgTime <= 0 || remoteAvgTime <= 0) {
performanceScore = 0.5; // 數(shù)據(jù)不足時取中間值
} else {
// 歸一化處理,確保分數(shù)在0-1之間
performanceScore = remoteAvgTime / (localAvgTime + remoteAvgTime);
}
// 錯誤率因素(本地錯誤率低得分更高)
double localErrorRate = stats.getLocalErrorRate();
double remoteErrorRate = stats.getRemoteErrorRate();
double errorScore = 0.5;
if (localErrorRate > 0 || remoteErrorRate > 0) {
double totalErrorRate = localErrorRate + remoteErrorRate;
if (totalErrorRate > 0) {
errorScore = 1 - (localErrorRate / totalErrorRate);
}
}
// 系統(tǒng)負載因素(負載高時傾向于遠程執(zhí)行)
double loadScore = 1.0 - Math.min(1.0, systemLoadAverage / availableProcessors);
// 綜合得分(加權平均)
return performanceScore * performanceWeight +
errorScore * errorWeight +
loadScore * loadWeight;
}
/**
* 定期更新系統(tǒng)負載信息
*/
@Scheduled(fixedRate = 10000) // 每10秒更新一次
public void updateSystemLoad() {
try {
java.lang.management.OperatingSystemMXBean osBean =
java.lang.management.ManagementFactory.getOperatingSystemMXBean();
if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
com.sun.management.OperatingSystemMXBean sunOsBean =
(com.sun.management.OperatingSystemMXBean) osBean;
systemLoadAverage = sunOsBean.getSystemLoadAverage();
availableProcessors = osBean.getAvailableProcessors();
logger.debug("System load updated: load={}, processors={}",
systemLoadAverage, availableProcessors);
}
} catch (Exception e) {
logger.warn("Failed to update system load", e);
}
}
/**
* 檢查遠程服務是否可用
*/
private boolean isRemoteServiceAvailable(Method method) {
if (discoveryClient == null) {
return true; // 沒有服務發(fā)現(xiàn)客戶端,假設服務可用
}
// 從方法所在類或包名推斷服務名
String serviceName = inferServiceName(method);
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
boolean available = !instances.isEmpty();
if (!available) {
logger.warn("Remote service {} is not available", serviceName);
}
return available;
}
/**
* 從方法推斷服務名
*/
private String inferServiceName(Method method) {
// 簡單示例:從類名推斷服務名
// 實際實現(xiàn)可能需要更復雜的邏輯或配置
String className = method.getDeclaringClass().getSimpleName();
// 去掉"Service"后綴并轉(zhuǎn)換為小寫短橫線格式
return className.replaceAll("Service$", "")
.replaceAll("([a-z])([A-Z])", "$1-$2")
.toLowerCase();
}
/**
* 獲取方法的唯一標識
*/
private String getMethodKey(Method method) {
return method.getDeclaringClass().getName() + "#" + method.getName();
}
/**
* 提供監(jiān)控API用的統(tǒng)計數(shù)據(jù)
*/
public Map<String, Object> getStatistics() {
Map<String, Object> stats = new ConcurrentHashMap<>();
methodStatsMap.forEach((methodKey, methodStats) -> {
Map<String, Object> methodData = new ConcurrentHashMap<>();
methodData.put("localCalls", methodStats.localCallCount.get());
methodData.put("remoteCalls", methodStats.remoteCallCount.get());
methodData.put("localAvgTimeMs", methodStats.getLocalAvgTimeMs());
methodData.put("remoteAvgTimeMs", methodStats.getRemoteAvgTimeMs());
methodData.put("localErrorRate", methodStats.getLocalErrorRate());
methodData.put("remoteErrorRate", methodStats.getRemoteErrorRate());
stats.put(methodKey, methodData);
});
stats.put("systemLoad", systemLoadAverage);
stats.put("availableProcessors", availableProcessors);
return stats;
}
}
5.3 服務降級
在遠程調(diào)用模式下,為了提高系統(tǒng)的可靠性,我們可以實現(xiàn)服務降級:
public class UserServiceFallback implements UserServiceFeignClient {
@Override
public User getUserById(Long id) {
// 返回一個默認用戶或從緩存獲取
return new User(id, "Default User", "default@example.com");
}
@Override
public List<User> getAllUsers() {
// 返回空列表或緩存數(shù)據(jù)
return Collections.emptyList();
}
// 其他方法實現(xiàn)...
}
六、方案優(yōu)缺點分析
6.1 優(yōu)點
代碼統(tǒng)一:業(yè)務代碼不需要關心底層實現(xiàn)方式,保持一致的調(diào)用方式
靈活部署:可以根據(jù)需要靈活切換部署模式,支持單體和微服務架構(gòu)
平滑遷移:支持系統(tǒng)架構(gòu)的漸進式演進,無需一次性重構(gòu)
便于測試:可以在測試環(huán)境使用本地實現(xiàn),降低測試復雜度
運維便利:通過配置變更實現(xiàn)部署調(diào)整,無需修改代碼
6.2 缺點
額外復雜性:增加了系統(tǒng)設計的復雜度
性能差異:本地調(diào)用和遠程調(diào)用的性能特性不同,可能需要針對性優(yōu)化
一致性考量:需要確保本地實現(xiàn)和遠程實現(xiàn)的行為一致
異常處理:遠程調(diào)用涉及網(wǎng)絡異常等情況,異常處理策略需要統(tǒng)一
七、實際應用場景
7.1 單體應用逐步拆分為微服務
當需要將單體應用逐步拆分為微服務時,可以首先將業(yè)務功能模塊化,定義清晰的服務接口,實現(xiàn)本地調(diào)用。
然后逐步將部分服務遷移到獨立部署的微服務,并將調(diào)用模式從本地切換為遠程,業(yè)務代碼無需修改。
7.2 微服務合并簡化架構(gòu)
當發(fā)現(xiàn)某些微服務之間耦合度高、頻繁交互時,可以考慮將它們合并部署。
使用本文提出的方案,只需修改配置將調(diào)用模式從遠程切換為本地,無需修改業(yè)務代碼。
7.3 多環(huán)境部署策略
在不同環(huán)境中可以采用不同的部署策略
開發(fā)環(huán)境: 全部使用本地模式,簡化開發(fā)和調(diào)試
測試環(huán)境: 模擬生產(chǎn)的遠程模式,驗證服務間通信
生產(chǎn)環(huán)境: 根據(jù)實際需求選擇最優(yōu)部署方式
八、總結(jié)
在實際應用中,可以根據(jù)自身業(yè)務特點和技術棧,對本文提出的方案進行適當?shù)恼{(diào)整和擴展,以滿足特定場景的需求。
以上就是SpringBoot實現(xiàn)本地與遠程方法調(diào)用的無縫切換的詳細內(nèi)容,更多關于SpringBoot本地與遠程調(diào)用切換的資料請關注腳本之家其它相關文章!
相關文章
把Java程序轉(zhuǎn)換成exe,可直接運行的實現(xiàn)
這篇文章主要介紹了把Java程序轉(zhuǎn)換成exe,可直接運行的實現(xiàn),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09
Java中使用Levenshtein距離實現(xiàn)字符串相似度匹配方式
這篇文章主要介紹了Java中使用Levenshtein距離實現(xiàn)字符串相似度匹配方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-06-06
SpringBoot配置shiro安全框架的實現(xiàn)
這篇文章主要介紹了SpringBoot配置shiro安全框架的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04
Springboot整合Shiro實現(xiàn)登錄與權限校驗詳細解讀
本文給大家介紹Springboot整合Shiro的基本使用,Apache?Shiro是Java的一個安全框架,Shiro本身無法知道所持有令牌的用戶是否合法,我們將整合Shiro實現(xiàn)登錄與權限的驗證2022-04-04
Java高級之HashMap中的entrySet()方法使用
這篇文章主要介紹了Java高級之HashMap中的entrySet()方法使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
java List出現(xiàn)All elements are null問題及解決
這篇文章主要介紹了java List出現(xiàn)All elements are null問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-11-11

