SpringBoot實(shí)現(xiàn)本地與遠(yuǎn)程方法調(diào)用的無縫切換
一、引言
公司業(yè)務(wù)發(fā)展過程中,前期一般需要快速實(shí)現(xiàn)產(chǎn)品的MVP版本,用于市場驗(yàn)證。
此時(shí)選擇單體架構(gòu)有助于快速完成MVP版本的功能開發(fā),降低前期的投入成本。
但往往我們又需要考慮未來如果業(yè)務(wù)發(fā)展起來,用戶數(shù)、訪問量等快速增長的情況下,如何能讓單體架構(gòu)又快速切換到微服務(wù)架構(gòu)以很好的支撐產(chǎn)品的正常運(yùn)行。
在將單體架構(gòu)切換為微服務(wù)架構(gòu)的過程中,存在一個(gè)關(guān)鍵問題就是將原來的本地調(diào)用需要切換為遠(yuǎn)程調(diào)用。如果前期實(shí)現(xiàn)未預(yù)留相關(guān)切換機(jī)制,這個(gè)過程勢必會(huì)存在大量的代碼修改甚至重寫。
那么如何能在不修改大量業(yè)務(wù)代碼的情況下,實(shí)現(xiàn)從本地方法調(diào)用到遠(yuǎn)程方法調(diào)用(RPC)的平滑切換?
本文將分享一種實(shí)現(xiàn)方案,能夠以統(tǒng)一的方式編寫代碼,而無需關(guān)心底層是本地方法調(diào)用還是遠(yuǎn)程服務(wù)調(diào)用,通過配置即可實(shí)現(xiàn)方法調(diào)用的平滑切換。
二、技術(shù)背景
在深入討論實(shí)現(xiàn)方案前,我們先了解一下相關(guān)的技術(shù)概念:
本地調(diào)用:在同一個(gè)JVM內(nèi),直接通過方法調(diào)用實(shí)現(xiàn),性能高,無網(wǎng)絡(luò)開銷
遠(yuǎn)程調(diào)用:跨JVM、跨網(wǎng)絡(luò)的服務(wù)調(diào)用,涉及序列化/反序列化、網(wǎng)絡(luò)傳輸?shù)?,有性能損耗
三、設(shè)計(jì)思路
實(shí)現(xiàn)本地遠(yuǎn)程方法調(diào)用的無縫切換,核心思路是設(shè)計(jì)一個(gè)統(tǒng)一的抽象層,該層能夠:
- 1. 提供統(tǒng)一的API接口
- 2. 根據(jù)配置或運(yùn)行環(huán)境決定使用本地實(shí)現(xiàn)還是遠(yuǎn)程調(diào)用
- 3. 對(duì)上層業(yè)務(wù)代碼透明,無需修改業(yè)務(wù)邏輯
基于這一思路,我們可以設(shè)計(jì)如下幾個(gè)關(guān)鍵組件
- 統(tǒng)一服務(wù)接口:定義業(yè)務(wù)服務(wù)的API
- 本地實(shí)現(xiàn)類:接口的本地具體實(shí)現(xiàn)
- 遠(yuǎn)程代理類:通過Feign等技術(shù)實(shí)現(xiàn)的遠(yuǎn)程調(diào)用代理
- 自動(dòng)配置:基于SpringBoot的自動(dòng)配置機(jī)制實(shí)現(xiàn)動(dòng)態(tài)切換
四、實(shí)現(xiàn)方案
4.1 定義統(tǒng)一服務(wù)接口
首先,我們需要定義服務(wù)接口。這些接口將同時(shí)作為本地實(shí)現(xiàn)和遠(yuǎn)程調(diào)用的契約。
// 用戶服務(wù)接口
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)建本地實(shí)現(xiàn)
為服務(wù)接口提供本地實(shí)現(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 定義遠(yuǎn)程接口
使用OpenFeign定義遠(yuǎn)程調(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)建遠(yuǎn)程實(shí)現(xiàn)代理
將Feign客戶端封裝為符合服務(wù)接口的實(shí)現(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)建自動(dòng)配置類,根據(jù)配置選擇合適的實(shí)現(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 實(shí)現(xiàn)動(dòng)態(tài)切換
通過配置屬性實(shí)現(xiàn)動(dòng)態(tài)切換:
# application.yml service: mode: local # 可選值: local, remote
業(yè)務(wù)代碼中的使用方式保持一致:
@Service
public class UserBusinessService {
@Autowired
private UserService userService; // 自動(dòng)注入適合的實(shí)現(xiàn)
public void processUser(Long userId) {
User user = userService.getUserById(userId);
// 處理用戶數(shù)據(jù)...
}
}
五、進(jìn)階實(shí)現(xiàn)
5.1 混合模式
在某些場景下,我們可能希望部分服務(wù)使用本地調(diào)用,部分服務(wù)使用遠(yuǎn)程調(diào)用。這可以通過更細(xì)粒度的配置實(shí)現(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實(shí)現(xiàn)智能路由
我們可以使用AOP實(shí)現(xiàn)更智能的路由策略,例如根據(jù)負(fù)載、性能等因素動(dòng)態(tài)決定是使用本地調(diào)用還是遠(yuǎn)程調(diào)用:
/**
* 標(biāo)記支持智能路由的方法,會(huì)根據(jù)負(fù)載情況自動(dòng)選擇本地或遠(yuǎn)程執(zhí)行
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SmartRouting {
/**
* 是否啟用智能路由,默認(rèn)為true
*/
boolean enabled() default true;
/**
* 指定遠(yuǎn)程服務(wù)名,如果為空則自動(dòng)推斷
*/
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 {
// 遠(yuǎn)程執(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();
}
// 從方法所在類名推斷服務(wù)名
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 {
// 查找對(duì)應(yīng)服務(wù)的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);
}
}
}
}
/**
* 智能負(fù)載均衡服務(wù)實(shí)現(xiàn)
* 基于方法調(diào)用的性能指標(biāo)和系統(tǒng)負(fù)載狀態(tài)做出路由決策
*/
@Service
public class SmartLoadBalancingService {
private static final Logger logger = LoggerFactory.getLogger(SmartLoadBalancingService.class);
// 方法執(zhí)行統(tǒng)計(jì)數(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í)行時(shí)間
double getLocalAvgTimeMs() {
int count = localCallCount.get();
return count > 0 ? (double) localTotalTimeMs.get() / count : 0;
}
// 獲取遠(yuǎn)程平均執(zhí)行時(shí)間
double getRemoteAvgTimeMs() {
int count = remoteCallCount.get();
return count > 0 ? (double) remoteTotalTimeMs.get() / count : 0;
}
// 獲取本地錯(cuò)誤率
double getLocalErrorRate() {
int count = localCallCount.get();
return count > 0 ? (double) localErrorCount.get() / count : 0;
}
// 獲取遠(yuǎn)程錯(cuò)誤率
double getRemoteErrorRate() {
int count = remoteCallCount.get();
return count > 0 ? (double) remoteErrorCount.get() / count : 0;
}
}
// 每個(gè)方法的統(tǒng)計(jì)數(shù)據(jù)
private final Map<String, MethodStats> methodStatsMap = new ConcurrentHashMap<>();
// 系統(tǒng)負(fù)載指標(biāo)
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; // 如果遠(yuǎn)程服務(wù)被禁用,總是本地執(zhí)行
}
// 檢查遠(yuǎn)程服務(wù)是否可用
if (!isRemoteServiceAvailable(method)) {
return true; // 遠(yuǎn)程服務(wù)不可用,使用本地執(zhí)行
}
String methodKey = getMethodKey(method);
MethodStats stats = methodStatsMap.computeIfAbsent(methodKey, k -> new MethodStats());
// 如果沒有足夠的統(tǒng)計(jì)數(shù)據(jù),交替使用本地和遠(yuǎn)程
if (stats.localCallCount.get() < 10 || stats.remoteCallCount.get() < 10) {
return stats.localCallCount.get() <= stats.remoteCallCount.get();
}
// 計(jì)算決策得分,得分越高越傾向于本地執(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();
}
}
// 記錄詳細(xì)統(tǒng)計(jì)數(shù)據(jù)(可用于監(jiān)控)
logger.debug("Execution recorded: method={}, local={}, time={}ms, success={}",
methodKey, isLocal, executionTimeMs, success);
}
/**
* 計(jì)算路由決策得分
*
* @param stats 方法統(tǒng)計(jì)數(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ù)不足時(shí)取中間值
} else {
// 歸一化處理,確保分?jǐn)?shù)在0-1之間
performanceScore = remoteAvgTime / (localAvgTime + remoteAvgTime);
}
// 錯(cuò)誤率因素(本地錯(cuò)誤率低得分更高)
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)負(fù)載因素(負(fù)載高時(shí)傾向于遠(yuǎn)程執(zhí)行)
double loadScore = 1.0 - Math.min(1.0, systemLoadAverage / availableProcessors);
// 綜合得分(加權(quán)平均)
return performanceScore * performanceWeight +
errorScore * errorWeight +
loadScore * loadWeight;
}
/**
* 定期更新系統(tǒng)負(fù)載信息
*/
@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);
}
}
/**
* 檢查遠(yuǎn)程服務(wù)是否可用
*/
private boolean isRemoteServiceAvailable(Method method) {
if (discoveryClient == null) {
return true; // 沒有服務(wù)發(fā)現(xiàn)客戶端,假設(shè)服務(wù)可用
}
// 從方法所在類或包名推斷服務(wù)名
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;
}
/**
* 從方法推斷服務(wù)名
*/
private String inferServiceName(Method method) {
// 簡單示例:從類名推斷服務(wù)名
// 實(shí)際實(shí)現(xiàn)可能需要更復(fù)雜的邏輯或配置
String className = method.getDeclaringClass().getSimpleName();
// 去掉"Service"后綴并轉(zhuǎn)換為小寫短橫線格式
return className.replaceAll("Service$", "")
.replaceAll("([a-z])([A-Z])", "$1-$2")
.toLowerCase();
}
/**
* 獲取方法的唯一標(biāo)識(shí)
*/
private String getMethodKey(Method method) {
return method.getDeclaringClass().getName() + "#" + method.getName();
}
/**
* 提供監(jiān)控API用的統(tǒng)計(jì)數(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 服務(wù)降級(jí)
在遠(yuǎn)程調(diào)用模式下,為了提高系統(tǒng)的可靠性,我們可以實(shí)現(xiàn)服務(wù)降級(jí):
public class UserServiceFallback implements UserServiceFeignClient {
@Override
public User getUserById(Long id) {
// 返回一個(gè)默認(rèn)用戶或從緩存獲取
return new User(id, "Default User", "default@example.com");
}
@Override
public List<User> getAllUsers() {
// 返回空列表或緩存數(shù)據(jù)
return Collections.emptyList();
}
// 其他方法實(shí)現(xiàn)...
}
六、方案優(yōu)缺點(diǎn)分析
6.1 優(yōu)點(diǎn)
代碼統(tǒng)一:業(yè)務(wù)代碼不需要關(guān)心底層實(shí)現(xiàn)方式,保持一致的調(diào)用方式
靈活部署:可以根據(jù)需要靈活切換部署模式,支持單體和微服務(wù)架構(gòu)
平滑遷移:支持系統(tǒng)架構(gòu)的漸進(jìn)式演進(jìn),無需一次性重構(gòu)
便于測試:可以在測試環(huán)境使用本地實(shí)現(xiàn),降低測試復(fù)雜度
運(yùn)維便利:通過配置變更實(shí)現(xiàn)部署調(diào)整,無需修改代碼
6.2 缺點(diǎn)
額外復(fù)雜性:增加了系統(tǒng)設(shè)計(jì)的復(fù)雜度
性能差異:本地調(diào)用和遠(yuǎn)程調(diào)用的性能特性不同,可能需要針對(duì)性優(yōu)化
一致性考量:需要確保本地實(shí)現(xiàn)和遠(yuǎn)程實(shí)現(xiàn)的行為一致
異常處理:遠(yuǎn)程調(diào)用涉及網(wǎng)絡(luò)異常等情況,異常處理策略需要統(tǒng)一
七、實(shí)際應(yīng)用場景
7.1 單體應(yīng)用逐步拆分為微服務(wù)
當(dāng)需要將單體應(yīng)用逐步拆分為微服務(wù)時(shí),可以首先將業(yè)務(wù)功能模塊化,定義清晰的服務(wù)接口,實(shí)現(xiàn)本地調(diào)用。
然后逐步將部分服務(wù)遷移到獨(dú)立部署的微服務(wù),并將調(diào)用模式從本地切換為遠(yuǎn)程,業(yè)務(wù)代碼無需修改。
7.2 微服務(wù)合并簡化架構(gòu)
當(dāng)發(fā)現(xiàn)某些微服務(wù)之間耦合度高、頻繁交互時(shí),可以考慮將它們合并部署。
使用本文提出的方案,只需修改配置將調(diào)用模式從遠(yuǎn)程切換為本地,無需修改業(yè)務(wù)代碼。
7.3 多環(huán)境部署策略
在不同環(huán)境中可以采用不同的部署策略
開發(fā)環(huán)境: 全部使用本地模式,簡化開發(fā)和調(diào)試
測試環(huán)境: 模擬生產(chǎn)的遠(yuǎn)程模式,驗(yàn)證服務(wù)間通信
生產(chǎn)環(huán)境: 根據(jù)實(shí)際需求選擇最優(yōu)部署方式
八、總結(jié)
在實(shí)際應(yīng)用中,可以根據(jù)自身業(yè)務(wù)特點(diǎn)和技術(shù)棧,對(duì)本文提出的方案進(jìn)行適當(dāng)?shù)恼{(diào)整和擴(kuò)展,以滿足特定場景的需求。
以上就是SpringBoot實(shí)現(xiàn)本地與遠(yuǎn)程方法調(diào)用的無縫切換的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot本地與遠(yuǎn)程調(diào)用切換的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java利用Phantomjs實(shí)現(xiàn)生成圖片的功能
這篇文章主要介紹了Java利用Phantomjs實(shí)現(xiàn)生成圖片的功能,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-08-08
將Java項(xiàng)目打包為可執(zhí)行JAR文件的完整流程
在Java開發(fā)中,我們通常會(huì)將我們的項(xiàng)目打包成可執(zhí)行的Jar包,以便于在其他環(huán)境中部署和運(yùn)行,本文將介紹如何使用IDEA集成開發(fā)環(huán)境將Java項(xiàng)目打包成可執(zhí)行的Jar包,需要的朋友可以參考下2025-08-08
Java數(shù)據(jù)結(jié)構(gòu)之插入排序與希爾排序
在本篇文章,我們將為小伙伴們進(jìn)行排序概念的基本講解并具體講解其中的兩種基礎(chǔ)排序:插入排序和希爾排序,希望小伙伴們能夠從中有所收獲2023-04-04
解決java攔截器獲取POST入?yún)?dǎo)致@RequestBody參數(shù)丟失問題
文章講述了在Java開發(fā)中使用攔截器獲取POST請(qǐng)求入?yún)r(shí),由于流關(guān)閉導(dǎo)致`@RequestBody`參數(shù)丟失的問題,并提出了一種解決方案,解決方案包括自定義方法、防止流丟失、過濾器和攔截器的合理組織和使用,最終確保了參數(shù)的正確傳遞2024-11-11
springboot如何使用@ConfigurationProperties封裝配置文件
springboot如何使用@ConfigurationProperties封裝配置文件的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
Spring中的@CrossOrigin注解的使用詳細(xì)解讀
這篇文章主要介紹了Spring中的@CrossOrigin注解的使用詳細(xì)解讀,跨源資源共享(CORS),是由大多數(shù)瀏覽器實(shí)現(xiàn)的W3C規(guī)范,允許對(duì)跨域請(qǐng)求進(jìn)行靈活授權(quán),用來代替IFRAME或JSONP等非正規(guī)實(shí)現(xiàn)方式,需要的朋友可以參考下2023-11-11

