# SpringBootжҺҘеҸЈ - еҰӮдҪ•жҸҗдҫӣеӨҡдёӘзүҲжң¬жҺҘеҸЈ
# SpringBootжҺҘеҸЈ - еҰӮдҪ•жҸҗдҫӣеӨҡдёӘзүҲжң¬жҺҘеҸЈ
еңЁд»ҘSpringBootејҖеҸ‘RestfulжҺҘеҸЈж—¶пјҢз”ұдәҺжЁЎеқ—пјҢзі»з»ҹзӯүдёҡеҠЎзҡ„еҸҳеҢ–пјҢйңҖиҰҒеҜ№еҗҢдёҖжҺҘеҸЈжҸҗдҫӣдёҚеҗҢзүҲжң¬зҡ„еҸӮж•°е®һзҺ°пјҲиҖҒзҡ„жҺҘеҸЈиҝҳжңүжЁЎеқ—жҲ–иҖ…зі»з»ҹеңЁз”ЁпјҢдёҚиғҪзӣҙжҺҘж”№пјҢжүҖд»ҘйңҖиҰҒдёҚеҗҢзүҲжң¬пјүгҖӮеҰӮдҪ•жӣҙеҠ дјҳйӣ…зҡ„е®һзҺ°еӨҡзүҲжң¬жҺҘеҸЈе‘ўпјҹ@pdai
# дёәд»Җд№ҲжҺҘеҸЈдјҡеҮәзҺ°еӨҡдёӘзүҲжң¬пјҹ
дёәд»Җд№ҲжҺҘеҸЈдјҡеҮәзҺ°еӨҡдёӘзүҲжң¬пјҹ
дёҖиҲ¬жқҘиҜҙпјҢRestful APIжҺҘеҸЈжҳҜжҸҗдҫӣз»ҷе…¶е®ғжЁЎеқ—пјҢзі»з»ҹжҲ–жҳҜе…¶д»–е…¬еҸёдҪҝз”ЁпјҢдёҚиғҪйҡҸж„Ҹйў‘з№Ғзҡ„еҸҳжӣҙгҖӮз„¶иҖҢпјҢйңҖжұӮе’ҢдёҡеҠЎдёҚж–ӯеҸҳеҢ–пјҢжҺҘеҸЈе’ҢеҸӮж•°д№ҹдјҡеҸ‘з”ҹзӣёеә”зҡ„еҸҳеҢ–гҖӮеҰӮжһңзӣҙжҺҘеҜ№еҺҹжқҘзҡ„жҺҘеҸЈиҝӣиЎҢдҝ®ж”№пјҢеҠҝеҝ…дјҡеҪұе“Қзәҝе…¶д»–зі»з»ҹзҡ„жӯЈеёёиҝҗиЎҢгҖӮиҝҷе°ұеҝ…йЎ»еҜ№api жҺҘеҸЈиҝӣиЎҢжңүж•Ҳзҡ„зүҲжң¬жҺ§еҲ¶гҖӮ
# жңүе“ӘдәӣжҺ§еҲ¶жҺҘеҸЈеӨҡзүҲжң¬зҡ„ж–№ејҸпјҹ
зӣёеҗҢURLпјҢз”Ё**дёҚеҗҢзҡ„зүҲжң¬еҸӮж•°**еҢәеҲҶ
api.pdai.tech/user?version=v1
иЎЁзӨә v1зүҲжң¬зҡ„жҺҘеҸЈ, дҝқжҢҒеҺҹжңүжҺҘеҸЈдёҚеҠЁapi.pdai.tech/user?version=v2
иЎЁзӨә v2зүҲжң¬зҡ„жҺҘеҸЈпјҢжӣҙж–°ж–°зҡ„жҺҘеҸЈ
еҢәеҲҶдёҚеҗҢзҡ„жҺҘеҸЈеҹҹеҗҚпјҢдёҚеҗҢзҡ„зүҲжң¬жңүдёҚеҗҢзҡ„еӯҗеҹҹеҗҚ, и·Ҝз”ұеҲ°дёҚеҗҢзҡ„е®һдҫӢ:
v1.api.pdai.tech/user
иЎЁзӨә v1зүҲжң¬зҡ„жҺҘеҸЈ, дҝқжҢҒеҺҹжңүжҺҘеҸЈдёҚеҠЁ, и·Ҝз”ұеҲ°instance1v2.api.pdai.tech/user
иЎЁзӨә v2зүҲжң¬зҡ„жҺҘеҸЈпјҢжӣҙж–°ж–°зҡ„жҺҘеҸЈ, и·Ҝз”ұеҲ°instance2
зҪ‘е…іи·Ҝз”ұдёҚеҗҢеӯҗзӣ®еҪ•еҲ°дёҚеҗҢзҡ„е®һдҫӢпјҲдёҚеҗҢpackageд№ҹеҸҜд»Ҙпјү
api.pdai.tech/v1/user
иЎЁзӨә v1зүҲжң¬зҡ„жҺҘеҸЈ, дҝқжҢҒеҺҹжңүжҺҘеҸЈдёҚеҠЁ, и·Ҝз”ұеҲ°instance1api.pdai.tech/v2/user
иЎЁзӨә v2зүҲжң¬зҡ„жҺҘеҸЈпјҢжӣҙж–°ж–°зҡ„жҺҘеҸЈ, и·Ҝз”ұеҲ°instance2
еҗҢдёҖе®һдҫӢпјҢз”ЁжіЁи§Јйҡ”зҰ»дёҚеҗҢзүҲжң¬жҺ§еҲ¶
api.pdai.tech/v1/user
иЎЁзӨә v1зүҲжң¬зҡ„жҺҘеҸЈ, дҝқжҢҒеҺҹжңүжҺҘеҸЈдёҚеҠЁпјҢеҢ№й…Қ@ApiVersion("1")зҡ„handlerMappingapi.pdai.tech/v2/user
иЎЁзӨә v2зүҲжң¬зҡ„жҺҘеҸЈпјҢжӣҙж–°ж–°зҡ„жҺҘеҸЈпјҢеҢ№й…Қ@ApiVersion("2")зҡ„handlerMapping
иҝҷйҮҢдё»иҰҒеұ•зӨә第еӣӣз§ҚеҚ•дёҖе®һдҫӢдёӯеҰӮдҪ•дјҳйӣ…зҡ„жҺ§еҲ¶жҺҘеҸЈзҡ„зүҲжң¬гҖӮ
# е®һзҺ°жЎҲдҫӢ
иҝҷдёӘдҫӢеӯҗеҹәдәҺSpringBootе°ҒиЈ…дәҶ@ApiVersionжіЁи§Јж–№ејҸжҺ§еҲ¶жҺҘеҸЈзүҲжң¬гҖӮ
# иҮӘе®ҡд№ү@ApiVersionжіЁи§Ј
package tech.pdai.springboot.api.version.config.version;
import org.springframework.web.bind.annotation.Mapping;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
String value();
}
# е®ҡд№үзүҲжң¬еҢ№й…ҚRequestCondition
зүҲжң¬еҢ№й…Қж”ҜжҢҒдёүеұӮзүҲжң¬
- v1.1.1 пјҲеӨ§зүҲжң¬.е°ҸзүҲжң¬.иЎҘдёҒзүҲжң¬пјү
- v1.1 (зӯүеҗҢдәҺv1.1.0)
- v1 пјҲзӯүеҗҢдәҺv1.0.0)
package tech.pdai.springboot.api.version.config.version;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
/**
* support v1.1.1, v1.1, v1; three levels .
*/
private static final Pattern VERSION_PREFIX_PATTERN_1 = Pattern.compile("/v\\d\\.\\d\\.\\d/");
private static final Pattern VERSION_PREFIX_PATTERN_2 = Pattern.compile("/v\\d\\.\\d/");
private static final Pattern VERSION_PREFIX_PATTERN_3 = Pattern.compile("/v\\d/");
private static final List<Pattern> VERSION_LIST = Collections.unmodifiableList(
Arrays.asList(VERSION_PREFIX_PATTERN_1, VERSION_PREFIX_PATTERN_2, VERSION_PREFIX_PATTERN_3)
);
@Getter
private final String apiVersion;
public ApiVersionCondition(String apiVersion) {
this.apiVersion = apiVersion;
}
/**
* method priority is higher then class.
*
* @param other other
* @return ApiVersionCondition
*/
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
return new ApiVersionCondition(other.apiVersion);
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
for (int vIndex = 0; vIndex < VERSION_LIST.size(); vIndex++) {
Matcher m = VERSION_LIST.get(vIndex).matcher(request.getRequestURI());
if (m.find()) {
String version = m.group(0).replace("/v", "").replace("/", "");
if (vIndex == 1) {
version = version + ".0";
} else if (vIndex == 2) {
version = version + ".0.0";
}
if (compareVersion(version, this.apiVersion) >= 0) {
log.info("version={}, apiVersion={}", version, this.apiVersion);
return this;
}
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return compareVersion(other.getApiVersion(), this.apiVersion);
}
private int compareVersion(String version1, String version2) {
if (version1 == null || version2 == null) {
throw new RuntimeException("compareVersion error:illegal params.");
}
String[] versionArray1 = version1.split("\\.");
String[] versionArray2 = version2.split("\\.");
int idx = 0;
int minLength = Math.min(versionArray1.length, versionArray2.length);
int diff = 0;
while (idx < minLength
&& (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
&& (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
++idx;
}
diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
return diff;
}
}
# е®ҡд№үHandlerMapping
package tech.pdai.springboot.api.version.config.version;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;
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 {
/**
* add @ApiVersion to controller class.
*
* @param handlerType handlerType
* @return RequestCondition
*/
@Override
protected RequestCondition<?> getCustomTypeCondition(@NonNull Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return null == apiVersion ? super.getCustomTypeCondition(handlerType) : new ApiVersionCondition(apiVersion.value());
}
/**
* add @ApiVersion to controller method.
*
* @param method method
* @return RequestCondition
*/
@Override
protected RequestCondition<?> getCustomMethodCondition(@NonNull Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return null == apiVersion ? super.getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion.value());
}
}
# й…ҚзҪ®жіЁеҶҢHandlerMapping
package tech.pdai.springboot.api.version.config.version;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {
@Override
public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}
жҲ–иҖ…е®һзҺ°WebMvcRegistrationsзҡ„жҺҘеҸЈ
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
//...
@Override
@NonNull
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}
# жөӢиҜ•иҝҗиЎҢ
controller
package tech.pdai.springboot.api.version.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.pdai.springboot.api.version.config.version.ApiVersion;
import tech.pdai.springboot.api.version.entity.User;
/**
* @author pdai
*/
@RestController
@RequestMapping("api/{v}/user")
public class UserController {
@RequestMapping("get")
public User getUser() {
return User.builder().age(18).name("pdai, default").build();
}
@ApiVersion("1.0.0")
@RequestMapping("get")
public User getUserV1() {
return User.builder().age(18).name("pdai, v1.0.0").build();
}
@ApiVersion("1.1.0")
@RequestMapping("get")
public User getUserV11() {
return User.builder().age(19).name("pdai, v1.1.0").build();
}
@ApiVersion("1.1.2")
@RequestMapping("get")
public User getUserV112() {
return User.builder().age(19).name("pdai2, v1.1.2").build();
}
}
иҫ“еҮә
http://localhost:8080/api/v1/user/get
// {"name":"pdai, v1.0.0","age":18}
http://localhost:8080/api/v1.1/user/get
// {"name":"pdai, v1.1.0","age":19}
http://localhost:8080/api/v1.1.1/user/get
// {"name":"pdai, v1.1.0","age":19} еҢ№й…ҚжҜ”1.1.1е°Ҹзҡ„дёӯжңҖеӨ§зҡ„дёҖдёӘзүҲжң¬еҸ·
http://localhost:8080/api/v1.1.2/user/get
// {"name":"pdai2, v1.1.2","age":19}
http://localhost:8080/api/v1.2/user/get
// {"name":"pdai2, v1.1.2","age":19} еҢ№й…ҚжңҖеӨ§зҡ„зүҲжң¬еҸ·пјҢv1.1.2
иҝҷж ·пјҢеҰӮжһңжҲ‘们еҗ‘еҸҰеӨ–дёҖдёӘжЁЎеқ—жҸҗдҫӣv1зүҲжң¬зҡ„жҺҘеҸЈпјҢж–°зҡ„йңҖжұӮдёӯеҸӘеҸҳеҠЁдәҶдёҖдёӘжҺҘеҸЈж–№жі•пјҢиҝҷж—¶еҖҷжҲ‘们еҸӘйңҖиҰҒеўһеҠ дёҖдёӘжҺҘеҸЈж·»еҠ зүҲжң¬еҸ·v1.1еҚіеҸҜз”Ёv1.1зүҲжң¬и®ҝй—®жүҖжңүжҺҘеҸЈгҖӮ
жӯӨеӨ–пјҢиҝҷз§Қж–№ејҸеҸҜиғҪдјҡеҜјиҮҙv3зүҲжң¬жҺҘеҸЈжІЎжңүеҸ‘еёғпјҢдҪҶжҳҜжҳҜеҸҜд»ҘйҖҡиҝҮv3и®ҝй—®жҺҘеҸЈзҡ„пјӣиҝҷз§Қжғ…еҶөдёӢеҸҜд»Ҙж·»еҠ дёҖдәӣйҷҗеҲ¶зүҲжң¬зҡ„йҖ»иҫ‘пјҢжҜ”еҰӮжңҖеӨ§зүҲжң¬пјҢзүҲжң¬йӣҶеҗҲзӯүгҖӮ
# зӨәдҫӢжәҗз Ғ
https://github.com/realpdai/tech-pdai-spring-demos