SpringBoot注解機(jī)制實(shí)現(xiàn)API多版本共存和灰度發(fā)布的方案
一、引言
在快速迭代的產(chǎn)品開(kāi)發(fā)中,API接口的更新迭代是常態(tài)。然而,舊版本接口往往已經(jīng)有大量用戶(hù)依賴(lài),無(wú)法立即停用。
那么如何在不影響現(xiàn)有用戶(hù)的前提下平滑引入新版本API,并逐步實(shí)現(xiàn)版本遷移 ?
本文將介紹如何通過(guò)Spring Boot注解機(jī)制實(shí)現(xiàn)API多版本共存,并基于此實(shí)現(xiàn)灰度發(fā)布,達(dá)到更優(yōu)雅地處理API版本管理問(wèn)題的目的。
二、背景與問(wèn)題
2.1 API版本管理的挑戰(zhàn)
在實(shí)際開(kāi)發(fā)過(guò)程中,我們常常面臨以下API版本管理的挑戰(zhàn):
兼容性問(wèn)題:新版API可能引入不兼容的變更,直接替換會(huì)導(dǎo)致客戶(hù)端崩潰
用戶(hù)體驗(yàn):強(qiáng)制用戶(hù)立即升級(jí)會(huì)帶來(lái)負(fù)面用戶(hù)體驗(yàn)
風(fēng)險(xiǎn)管控:新版API可能存在未知問(wèn)題,需要逐步推廣以控制風(fēng)險(xiǎn)
平滑過(guò)渡:需要提供平滑的過(guò)渡期,讓用戶(hù)有足夠時(shí)間適應(yīng)新版本
2.2 傳統(tǒng)API版本管理方案及不足
傳統(tǒng)的API版本管理方案主要有以下幾種:
1. URL路徑版本:如/v1/users、/v2/users
- 優(yōu)點(diǎn):簡(jiǎn)單直觀(guān),客戶(hù)端易于理解
- 缺點(diǎn):不便于根據(jù)規(guī)則在后端進(jìn)行動(dòng)態(tài)控制
2. 請(qǐng)求參數(shù)版本:如/users?version=1
- 優(yōu)點(diǎn):不改變資源標(biāo)識(shí)
- 缺點(diǎn):可能與業(yè)務(wù)參數(shù)混淆
3. HTTP頭版本:如Accept: application/vnd.company.app-v1+json
- 優(yōu)點(diǎn):符合HTTP規(guī)范,不污染URL
- 缺點(diǎn):對(duì)客戶(hù)端不友好,調(diào)試不便
三、基于注解的API版本路由方案
3.1 設(shè)計(jì)思路
我們的核心設(shè)計(jì)思路是:通過(guò)自定義注解標(biāo)記不同版本的API實(shí)現(xiàn),結(jié)合SpringMVC的RequestMappingHandlerMapping擴(kuò)展機(jī)制與條件選擇機(jī)制,根據(jù)請(qǐng)求中的版本信息動(dòng)態(tài)路由到對(duì)應(yīng)版本的處理方法。
同時(shí),我們引入用戶(hù)分組和灰度規(guī)則,使系統(tǒng)能夠根據(jù)用戶(hù)特征智能地選擇合適的API版本,實(shí)現(xiàn)精細(xì)化的灰度發(fā)布。
3.2 核心組件
@ApiVersion注解:標(biāo)記API方法的版本信息
@GrayRelease注解:定義灰度發(fā)布規(guī)則
ApiVersionRequestMappingHandlerMapping:擴(kuò)展Spring的請(qǐng)求映射處理器,支持版本路由
ApiVersionRequestCondition:版本路由條件選擇器
四、方案實(shí)現(xiàn)
4.1 版本注解定義
首先,定義API版本注解:
package com.example.version;
import java.lang.annotation.*;
/**
* API版本注解,用于標(biāo)記接口的版本
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiVersion {
/**
* 版本號(hào),默認(rèn)為1.0
*/
String value() default "1.0";
/**
* 版本描述
*/
String description() default "";
/**
* 是否廢棄
*/
boolean deprecated() default false;
/**
* 廢棄說(shuō)明,建議使用的新版本等信息
*/
String deprecatedDesc() default "";
}
灰度發(fā)布注解:
package com.example.version;
import java.lang.annotation.*;
/**
* 灰度發(fā)布注解,用于定義灰度發(fā)布規(guī)則
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GrayRelease {
/**
* 開(kāi)始時(shí)間,格式:yyyy-MM-dd HH:mm:ss
*/
String startTime() default "";
/**
* 結(jié)束時(shí)間,格式:yyyy-MM-dd HH:mm:ss
*/
String endTime() default "";
/**
* 用戶(hù)ID白名單,多個(gè)ID用逗號(hào)分隔
*/
String userIds() default "";
/**
* 用戶(hù)比例,0-100之間的整數(shù),表示百分比
*/
int percentage() default 0;
/**
* 指定的用戶(hù)組
*/
String[] userGroups() default {};
/**
* 地區(qū)限制,支持國(guó)家、省份、城市,如:CN,US,Beijing
*/
String[] regions() default {};
}
4.2 版本請(qǐng)求映射處理器
擴(kuò)展Spring的RequestMappingHandlerMapping,支持版本路由:
package com.example.version;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion annotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return (annotation != null) ?
new ApiVersionRequestCondition(annotation.value(), (HandlerMethod) null) : null;
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion annotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
if (annotation != null) {
// 需要獲取實(shí)際的HandlerMethod
return new ApiVersionRequestCondition(annotation.value(),
new HandlerMethod(new Object(), method)); // 需要實(shí)際handler實(shí)例
}
return null;
}
}
4.3 實(shí)現(xiàn)版本匹配條件
package com.example.version;
import cn.hutool.core.date.DateUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
/**
* API版本請(qǐng)求條件
*/
public class ApiVersionRequestCondition implements RequestCondition<ApiVersionRequestCondition> {
private final String apiVersion;
private final HandlerMethod handlerMethod;
private static final Random RANDOM = new SecureRandom();
public ApiVersionRequestCondition(String apiVersion, HandlerMethod handlerMethod) {
this.apiVersion = apiVersion;
this.handlerMethod = handlerMethod;
}
@Override
public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) {
// 采用方法上的版本號(hào)優(yōu)先于類(lèi)上的版本號(hào)
return new ApiVersionRequestCondition(other.getApiVersion(),null);
}
@Override
public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) {
//String requestVersion = VersionContextHolder.getVersion();
String requestVersion = getVersion(request);
// 版本比較邏輯,這里簡(jiǎn)化處理,只做字符串比較
// 實(shí)際應(yīng)用中可能需要更復(fù)雜的版本比較算法
if (requestVersion.equals(this.apiVersion)) {
return this;
}
return null;
}
@Override
public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) {
// 版本號(hào)越大優(yōu)先級(jí)越高
return other.getApiVersion().compareTo(this.apiVersion);
}
public String getApiVersion() {
return apiVersion;
}
private String getVersion(HttpServletRequest request){
// 獲取客戶(hù)端請(qǐng)求的版本
String clientVersion = request.getHeader("Api-Version");
if (clientVersion == null || clientVersion.isEmpty()) {
// 如果客戶(hù)端未指定版本,也可以從請(qǐng)求參數(shù)中獲取
clientVersion = request.getParameter("version");
}
// 如果客戶(hù)端仍未指定版本,則應(yīng)用灰度規(guī)則
if (clientVersion == null || clientVersion.isEmpty()) {
// 從請(qǐng)求中提取用戶(hù)信息
UserInfo userInfo = extractUserInfo(request);
// 獲取方法或類(lèi)上的灰度發(fā)布注解
GrayRelease grayRelease = handlerMethod.getMethodAnnotation(GrayRelease.class);
if (grayRelease == null) {
grayRelease = handlerMethod.getBeanType().getAnnotation(GrayRelease.class);
}
if (grayRelease != null) {
// 應(yīng)用灰度規(guī)則決定使用哪個(gè)版本
clientVersion = applyGrayReleaseRules(grayRelease, userInfo);
} else {
// 默認(rèn)使用最新版本
ApiVersion apiVersion = handlerMethod.getMethodAnnotation(ApiVersion.class);
if (apiVersion == null) {
apiVersion = handlerMethod.getBeanType().getAnnotation(ApiVersion.class);
}
clientVersion = apiVersion != null ? apiVersion.value() : "1.0";
}
}
return clientVersion;
}
private UserInfo extractUserInfo(HttpServletRequest request) {
UserInfo userInfo = new UserInfo();
// 實(shí)際應(yīng)用中這里可能從請(qǐng)求頭、Cookie或JWT Token中提取用戶(hù)信息
// 這里僅作示例
String userId = request.getHeader("User-Id");
userInfo.setUserId(userId);
String groups = request.getHeader("User-Groups");
if (groups != null && !groups.isEmpty()) {
userInfo.setGroups(groups.split(","));
}
String region = request.getHeader("User-Region");
userInfo.setRegion(region);
return userInfo;
}
/**
* 應(yīng)用灰度規(guī)則
*/
private String applyGrayReleaseRules(GrayRelease grayRelease, UserInfo userInfo) {
// 檢查時(shí)間范圍
if (!grayRelease.startTime().isEmpty() && !grayRelease.endTime().isEmpty()) {
try {
Date now = new Date();
Date startTime = DateUtil.parse(grayRelease.startTime());
Date endTime = DateUtil.parse(grayRelease.endTime());
if (now.before(startTime) || now.after(endTime)) {
return "1.0"; // 不在灰度時(shí)間范圍內(nèi),使用舊版本
}
} catch (Exception e) {
// 解析日期出錯(cuò),忽略時(shí)間規(guī)則
}
}
// 檢查用戶(hù)ID白名單
if (!grayRelease.userIds().isEmpty() && userInfo.getUserId() != null) {
String[] whitelistIds = grayRelease.userIds().split(",");
if (Arrays.asList(whitelistIds).contains(userInfo.getUserId())) {
return "2.0"; // 用戶(hù)在白名單中,使用新版本
}
}
// 檢查用戶(hù)組
if (grayRelease.userGroups().length > 0 && userInfo.getGroups() != null) {
for (String requiredGroup : grayRelease.userGroups()) {
for (String userGroup : userInfo.getGroups()) {
if (requiredGroup.equals(userGroup)) {
return "2.0"; // 用戶(hù)在指定組中,使用新版本
}
}
}
}
// 檢查地區(qū)
if (grayRelease.regions().length > 0 && userInfo.getRegion() != null) {
for (String region : grayRelease.regions()) {
if (region.equals(userInfo.getRegion())) {
return "2.0"; // 用戶(hù)在指定地區(qū),使用新版本
}
}
}
// 應(yīng)用百分比規(guī)則
if (grayRelease.percentage() > 0) {
int randomValue = RANDOM.nextInt(100) + 1; // 1-100的隨機(jī)數(shù)
if (randomValue <= grayRelease.percentage()) {
return "2.0"; // 隨機(jī)命中百分比,使用新版本
}
}
// 默認(rèn)使用舊版本
return "1.0";
}
}
4.5 配置類(lèi)
將上述組件注冊(cè)到Spring容器中:
package com.example.config;
import com.example.version.ApiVersionRequestMappingHandlerMapping;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer, WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}
五、實(shí)際應(yīng)用示例
下面是一個(gè)用戶(hù)服務(wù)API的多版本實(shí)現(xiàn)示例:
package com.example.controller;
import com.example.model.User;
import com.example.version.ApiVersion;
import com.example.version.GrayRelease;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/users")
public class UserController {
// 模擬數(shù)據(jù)庫(kù)
private final List<User> userDatabase = new ArrayList<>();
public UserController() {
// 初始化一些測(cè)試數(shù)據(jù)
userDatabase.add(new User(
1L, "john_doe", "John Doe", "john@example.com",
LocalDateTime.now().minusDays(100), LocalDateTime.now().minusDays(10),
true,"1234567890","https://example.com/avatars/john.jpg")
);
userDatabase.add(new User(
2L, "john_doe2", "John Doe2", "john2@example.com",
LocalDateTime.now().minusDays(102), LocalDateTime.now().minusDays(12),
true,"9876543210","https://example.com/avatars/john.jpg")
);
}
/**
* 獲取用戶(hù)列表 - 版本1.0
* 只返回基本用戶(hù)信息
*/
@GetMapping
@ApiVersion("1.0")
public List<User> getUsersV1() {
return userDatabase.stream()
.map(user -> {
User simpleUser = new User();
simpleUser.setId(user.getId());
simpleUser.setUsername(user.getUsername());
simpleUser.setName(user.getName());
simpleUser.setEmail(user.getEmail());
simpleUser.setActive(user.getActive());
return simpleUser;
})
.collect(Collectors.toList());
}
/**
* 獲取用戶(hù)列表 - 版本2.0(包含更多用戶(hù)信息)
* 返回完整的用戶(hù)信息
*/
@GetMapping
@ApiVersion("2.0")
@GrayRelease(
startTime = "2023-01-01 00:00:00",
//endTime = "2023-12-31 23:59:59",
endTime = "2025-12-31 23:59:59",
userGroups = {"vip", "beta-tester"},
percentage = 20
)
public List<User> getUsersV2() {
return userDatabase;
}
/**
* 獲取單個(gè)用戶(hù) - 版本1.0
* 只返回基本用戶(hù)信息
*/
@GetMapping("/{id}")
@ApiVersion("1.0")
public User getUserV1(@PathVariable Long id) {
User user = findUserById(id);
if (user == null) {
return null;
}
User simpleUser = new User();
simpleUser.setId(user.getId());
simpleUser.setUsername(user.getUsername());
simpleUser.setName(user.getName());
simpleUser.setEmail(user.getEmail());
simpleUser.setActive(user.getActive());
return simpleUser;
}
/**
* 獲取單個(gè)用戶(hù) - 版本2.0(包含更多用戶(hù)信息)
* 返回完整的用戶(hù)信息
*/
@GetMapping("/{id}")
@ApiVersion("2.0")
@GrayRelease(
userIds = "100,101,102",
regions = {"CN-BJ", "CN-SH"}
)
public User getUserV2(@PathVariable Long id) {
return findUserById(id);
}
/**
* 創(chuàng)建用戶(hù) - 版本1.0
* 只需要基本用戶(hù)信息
*/
@PostMapping
@ApiVersion("1.0")
public User createUserV1(@RequestBody User user) {
// 設(shè)置ID和時(shí)間戳
user.setId((long) (userDatabase.size() + 1));
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
user.setActive(true);
// 存儲(chǔ)用戶(hù)(實(shí)際項(xiàng)目中會(huì)保存到數(shù)據(jù)庫(kù))
userDatabase.add(user);
// 返回簡(jiǎn)化版本的用戶(hù)信息
User simpleUser = new User();
simpleUser.setId(user.getId());
simpleUser.setUsername(user.getUsername());
simpleUser.setName(user.getName());
simpleUser.setEmail(user.getEmail());
simpleUser.setPhone(user.getPhone());
simpleUser.setActive(user.getActive());
return simpleUser;
}
/**
* 創(chuàng)建用戶(hù) - 版本2.0(增加了參數(shù)驗(yàn)證和更豐富的返回信息)
*/
@PostMapping
@ApiVersion("2.0")
@GrayRelease(percentage = 50)
public User createUserV2(@RequestBody User user) {
// 參數(shù)驗(yàn)證
if (user.getName() == null || user.getName().isEmpty()) {
throw new IllegalArgumentException("User name cannot be empty");
}
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
// 設(shè)置ID和時(shí)間戳
user.setId((long) (userDatabase.size() + 1));
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
user.setActive(true);
// 存儲(chǔ)用戶(hù)
userDatabase.add(user);
// 返回完整的用戶(hù)信息
return user;
}
/**
* 更新用戶(hù) - 版本1.0
*/
@PutMapping("/{id}")
@ApiVersion("1.0")
public User updateUserV1(@PathVariable Long id, @RequestBody User userUpdate) {
User existingUser = findUserById(id);
if (existingUser == null) {
return null;
}
// 更新基本字段
if (userUpdate.getUsername() != null) existingUser.setUsername(userUpdate.getUsername());
if (userUpdate.getName() != null) existingUser.setName(userUpdate.getName());
if (userUpdate.getEmail() != null) existingUser.setEmail(userUpdate.getEmail());
if (userUpdate.getActive() != null) existingUser.setActive(userUpdate.getActive());
existingUser.setUpdatedAt(LocalDateTime.now());
// 返回簡(jiǎn)化版本
User simpleUser = new User();
simpleUser.setId(existingUser.getId());
simpleUser.setUsername(existingUser.getUsername());
simpleUser.setName(existingUser.getName());
simpleUser.setEmail(existingUser.getEmail());
simpleUser.setPhone(existingUser.getPhone());
simpleUser.setActive(existingUser.getActive());
return simpleUser;
}
/**
* 更新用戶(hù) - 版本2.0(支持更新更多字段)
*/
@PutMapping("/{id}")
@ApiVersion("2.0")
@GrayRelease(
userGroups = {"vip", "admin"},
percentage = 30
)
public User updateUserV2(@PathVariable Long id, @RequestBody User userUpdate) {
User existingUser = findUserById(id);
if (existingUser == null) {
return null;
}
// 更新基本字段
if (userUpdate.getUsername() != null) existingUser.setUsername(userUpdate.getUsername());
if (userUpdate.getName() != null) existingUser.setName(userUpdate.getName());
if (userUpdate.getEmail() != null) existingUser.setEmail(userUpdate.getEmail());
if (userUpdate.getActive() != null) existingUser.setActive(userUpdate.getActive());
// 更新擴(kuò)展字段
if (userUpdate.getPhone() != null) existingUser.setPhone(userUpdate.getPhone());
if (userUpdate.getAvatar() != null) existingUser.setAvatar(userUpdate.getAvatar());
existingUser.setUpdatedAt(LocalDateTime.now());
// 返回完整的用戶(hù)信息
return existingUser;
}
/**
* 刪除用戶(hù) - 版本1.0
*/
@DeleteMapping("/{id}")
@ApiVersion("1.0")
public void deleteUserV1(@PathVariable Long id) {
userDatabase.removeIf(user -> user.getId().equals(id));
}
/**
* 刪除用戶(hù) - 版本2.0(帶有軟刪除功能)
*/
@DeleteMapping("/{id}")
@ApiVersion("2.0")
@GrayRelease(
userGroups = {"admin"},
percentage = 10
)
public User deleteUserV2(@PathVariable Long id) {
User existingUser = findUserById(id);
if (existingUser == null) {
return null;
}
// 軟刪除,而不是物理刪除
existingUser.setActive(false);
existingUser.setUpdatedAt(LocalDateTime.now());
return existingUser;
}
/**
* 查找用戶(hù)的輔助方法
*/
private User findUserById(Long id) {
return userDatabase.stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.orElse(null);
}
}
六、最佳實(shí)踐和注意事項(xiàng)
6.1 API版本設(shè)計(jì)原則
向后兼容為先:盡量設(shè)計(jì)向后兼容的API,減少版本增加的頻率
明確變更范圍:只有不兼容的變更才需要新版本,小型改進(jìn)可以在現(xiàn)有版本中實(shí)現(xiàn)
妥善處理默認(rèn)版本:為未指定版本的請(qǐng)求提供合理的默認(rèn)版本
版本過(guò)渡期:為老版本設(shè)置合理的過(guò)渡期,并提供明確的廢棄通知
6.2 注意事項(xiàng)
性能考量:版本路由邏輯會(huì)帶來(lái)一定的性能開(kāi)銷(xiāo)
緩存策略:不同版本的API可能需要不同的緩存策略
測(cè)試覆蓋:確保所有版本的API都有完善的測(cè)試覆蓋
七、總結(jié)
本文介紹了如何通過(guò)注解路由機(jī)制實(shí)現(xiàn)API多版本共存和灰度發(fā)布。
通過(guò)自定義的@ApiVersion和@GrayRelease注解,結(jié)合Spring MVC的擴(kuò)展點(diǎn),我們實(shí)現(xiàn)了一個(gè)靈活、可配置的API版本管理方案。
以上就是SpringBoot注解機(jī)制實(shí)現(xiàn)API多版本共存和灰度發(fā)布的方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot API多版本共存和灰度發(fā)布的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
利用java反射機(jī)制調(diào)用類(lèi)的私有方法(推薦)
下面小編就為大家?guī)?lái)一篇利用java反射機(jī)制調(diào)用類(lèi)的私有方法(推薦)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08
對(duì)spring的@Cacheable緩存的使用理解
這篇文章主要介紹了對(duì)spring的@Cacheable緩存的使用理解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-10-10
java中volatile和synchronized的區(qū)別與聯(lián)系
這篇文章主要介紹了java中volatile和synchronized的區(qū)別與聯(lián)系的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家理解這部分內(nèi)容,需要的朋友可以參考下2017-10-10
Java實(shí)現(xiàn)簡(jiǎn)單推箱子游戲
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)推箱子游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06
深入解析Java的設(shè)計(jì)模式編程中建造者模式的運(yùn)用
這篇文章主要介紹了深入解析Java的設(shè)計(jì)模式編程中建造者模式的運(yùn)用,同時(shí)文中也介紹了建造者模式與工廠(chǎng)模式的區(qū)別,需要的朋友可以參考下2016-02-02
SpringBoot接收J(rèn)SON類(lèi)型的參數(shù)方式
這篇文章主要介紹了SpringBoot接收J(rèn)SON類(lèi)型的參數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
Java實(shí)現(xiàn)Dbhelper支持大數(shù)據(jù)增刪改
這篇文章主要介紹了Java實(shí)現(xiàn)Dbhelper支持大數(shù)據(jù)增刪改功能的實(shí)現(xiàn)過(guò)程,感興趣的小伙伴們可以參考一下2016-01-01
Ubuntu16.04安裝部署solr7的圖文詳細(xì)教程
這篇文章主要為大家詳細(xì)介紹了Ubuntu16.04安裝部署solr7的圖文詳細(xì)教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07

