源碼地址: https://github.com/charlesvhe/spring-cloud-practice/tree/refactor
spring cloud 實(shí)踐
項(xiàng)目結(jié)構(gòu)
config 配置中心
端口:8888,方便起見(jiàn)直接讀取配置文件碌宴,生產(chǎn)環(huán)境可以讀取git个曙。application-dev.properties為全局配置察郁。先啟動(dòng)配置中心伙菜,所有服務(wù)的配置(包括注冊(cè)中心的地址)均從配置中心讀取轩缤。
eureka 注冊(cè)中心
端口:8761,/metadata端點(diǎn)實(shí)現(xiàn)metadata信息配置贩绕。
zuul 網(wǎng)關(guān)
端口:8080典奉,演示解析token獲得label并放入header往后傳遞
core 框架核心包
核心jar包,所有微服務(wù)均引用該包丧叽,使用AutoConfig實(shí)現(xiàn)免配置,模擬生產(chǎn)環(huán)境下spring-cloud的使用公你。
starter spring cloud以及core框架依賴簡(jiǎn)化starter
provider 服務(wù)提供者
api 服務(wù)提供者SDK
強(qiáng)制服務(wù)消費(fèi)方只能通過(guò)服務(wù)提供者提供的SDK包(api項(xiàng)目)進(jìn)行調(diào)用踊淳,以便更加方便控制服務(wù)消費(fèi)方的行為:
提供可能的優(yōu)化,SDK直接讀取緩存(SDK只讀緩存陕靠,寫(xiě)緩存放還是在微服務(wù))迂尝。
統(tǒng)計(jì)有哪些服務(wù)消費(fèi)者,info端點(diǎn)顯示包依賴剪芥,通過(guò)注冊(cè)中心遍歷所有服務(wù)垄开。
方便dubbo平滑遷移,接口加feign注解税肪。
service 服務(wù)提供者微服務(wù)
端口:18090溉躲,服務(wù)提供者,無(wú)特殊邏輯益兄。
consumer 服務(wù)消費(fèi)者
端口:18090锻梳,調(diào)用服務(wù)提供者。
Restful API 設(shè)計(jì)規(guī)范
/版本/訪問(wèn)控制/域?qū)ο?br> /版本/訪問(wèn)控制/域?qū)ο?action/動(dòng)作
版本
版本為微服務(wù)級(jí)別净捅,也就是說(shuō)不存在一個(gè)API是v3版疑枯,其他API還只是v1版的問(wèn)題,要升級(jí)所有API版本一起升級(jí)蛔六,但是需要保證之前版本v1-v3還可以使用荆永。
原則上要兼容上一個(gè)版本 如果當(dāng)前是 /v3 則 /v2 要求可以正常使用 /v1 不做要求
如果無(wú)法兼容 需要通知所有服務(wù)消費(fèi)者 并約定版本火車(chē) 一起上線時(shí)間
小技巧
下面swagger注解就可以實(shí)現(xiàn)上述要求废亭,路徑中的{version}沒(méi)有限定,其實(shí)可以是任意內(nèi)容具钥,通過(guò)swagger文檔來(lái)進(jìn)行約定
// v1版api 即將廢棄
@ApiOperation("分頁(yè)查詢")
@RequestMapping(value = "/v1/pb/product", method = RequestMethod.GET)
@Deprecated
List<Product> selectAll(@RequestParam("offset") Integer offset, @RequestParam("limit") Integer limit);
//ProviderApiAutoConfig.CURRENT_VERSION="v2" 用來(lái)替換上面的v1版本
@ApiOperation("帶過(guò)濾條件和排序的分頁(yè)查詢")
@RequestMapping(value = "/{version}/pb/product", method = RequestMethod.GET)
@ApiImplicitParam(name = "version", paramType = "path", allowableValues = ProviderApiAutoConfig.CURRENT_VERSION, required = true)
Response<PageData<Product, Product>> selectAllGet(Page page);
// ProviderApiAutoConfig.COMPATIBLE_VERSION="v2,v1" swagger-ui上會(huì)顯示一個(gè)version的下拉框, 默認(rèn)v2
@ApiOperation(value = "帶過(guò)濾條件和排序的復(fù)雜分頁(yè)查詢")
@ApiImplicitParam(name = "version", paramType = "path", allowableValues = ProviderApiAutoConfig.COMPATIBLE_VERSION, required = true)
@RequestMapping(value = "/{version}/pb/product/action/search", method = RequestMethod.POST)
Response<PageData<Product, Product>> selectAll(@RequestBody Page page);
訪問(wèn)控制
用于在網(wǎng)關(guān)統(tǒng)一進(jìn)行訪問(wèn)控制, 訪問(wèn)控制分為
pb - public 所有請(qǐng)求均可訪問(wèn)
pt - protected 需要進(jìn)行token認(rèn)證通過(guò)后方可訪問(wèn)
df - default 網(wǎng)關(guān)進(jìn)行token認(rèn)證 并且請(qǐng)求參數(shù)和返回結(jié)果進(jìn)行加解密
pv - private 無(wú)法通過(guò)網(wǎng)關(guān)訪問(wèn) 只能微服務(wù)內(nèi)部調(diào)用
其他 同pv
action
無(wú)法用名詞+請(qǐng)求方法表述的可以擴(kuò)展為 /域?qū)ο?action/動(dòng)詞 必須為POST方法
例如 POST /users/action/login
問(wèn)題描述
主要由于使用了API(SDK)為了偷懶豆村,以及Restful API路徑中的版本帶來(lái)的一系列問(wèn)題。
spring MVC 不支持繼承接口中方法參數(shù)上的注解(支持繼承類(lèi)氓拼、方法上的注解)
API中為了方便你画,使用feign替代RestTemplate手動(dòng)調(diào)用。帶來(lái)的問(wèn)題:springMVC注解想偷懶桃漾,只在feign接口寫(xiě)一遍坏匪,然后實(shí)現(xiàn)類(lèi)繼承此接口即可。
例如:
feign接口定義如下
@FeignClient(ProviderApiAutoConfig.PLACE_HOLD_SERVICE_NAME)
public interface ProductService {
// 為了讓spring mvc能夠正確綁定變量
public class Page extends PageRequest<Product> {
}
@RequestMapping(value = "/{version}/pt/product", method = RequestMethod.POST)
Response<Product> insert(@RequestBody Product product);
}
service實(shí)現(xiàn)類(lèi)方法參數(shù)必須再寫(xiě)一次@RequestBody注解撬统,方法上的@RequestMapping注解可以省略
@RestController
public class ProductServiceImpl implements ProductService {
@Override
public Response<Product> insert(@RequestBody Product product) {
product.setId(1L);
return new Response(product);
}
}
解決辦法适滓,@Configuration配置類(lèi)添加如下代碼,擴(kuò)展spring默認(rèn)的ArgumentResolvers
public static MethodParameter interfaceMethodParameter(MethodParameter parameter, Class annotationType) {
if (!parameter.hasParameterAnnotation(annotationType)) {
for (Class<?> itf : parameter.getDeclaringClass().getInterfaces()) {
try {
Method method = itf.getMethod(parameter.getMethod().getName(), parameter.getMethod().getParameterTypes());
MethodParameter itfParameter = new MethodParameter(method, parameter.getParameterIndex());
if (itfParameter.hasParameterAnnotation(annotationType)) {
return itfParameter;
}
} catch (NoSuchMethodException e) {
continue;
}
}
}
return parameter;
}
@PostConstruct
public void modifyArgumentResolvers() {
List<HandlerMethodArgumentResolver> list = new ArrayList<>(adapter.getArgumentResolvers());
list.add(0, new PathVariableMethodArgumentResolver() { // PathVariable 支持接口注解
@Override
public boolean supportsParameter(MethodParameter parameter) {
return super.supportsParameter(interfaceMethodParameter(parameter, PathVariable.class));
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
return super.createNamedValueInfo(interfaceMethodParameter(parameter, PathVariable.class));
}
});
list.add(0, new RequestHeaderMethodArgumentResolver(beanFactory) { // RequestHeader 支持接口注解
@Override
public boolean supportsParameter(MethodParameter parameter) {
return super.supportsParameter(interfaceMethodParameter(parameter, RequestHeader.class));
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
return super.createNamedValueInfo(interfaceMethodParameter(parameter, RequestHeader.class));
}
});
list.add(0, new ServletCookieValueMethodArgumentResolver(beanFactory) { // CookieValue 支持接口注解
@Override
public boolean supportsParameter(MethodParameter parameter) {
return super.supportsParameter(interfaceMethodParameter(parameter, CookieValue.class));
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
return super.createNamedValueInfo(interfaceMethodParameter(parameter, CookieValue.class));
}
});
list.add(0, new RequestResponseBodyMethodProcessor(adapter.getMessageConverters()) { // RequestBody 支持接口注解
@Override
public boolean supportsParameter(MethodParameter parameter) {
return super.supportsParameter(interfaceMethodParameter(parameter, RequestBody.class));
}
@Override
protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) { // 支持@Valid驗(yàn)證
super.validateIfApplicable(binder, interfaceMethodParameter(methodParam, Valid.class));
}
});
// 修改ArgumentResolvers, 支持接口注解
adapter.setArgumentResolvers(list);
}
swagger不支持繼承接口中方法參數(shù)上的注解(支持繼承類(lèi)恋追、方法上的注解)
沒(méi)有找到swagger自帶擴(kuò)展點(diǎn)能夠優(yōu)雅擴(kuò)展的方法凭迹,只好修改源碼了,下載springfox-spring-web 2.8.0 release源碼包苦囱。
添加pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.springfox</groupId>
<artifactId>springfox-spring-web</artifactId>
<version>2.8.0-charles</version>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
<resource.delimiter>@</resource.delimiter>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<!--<version>2.0.0.RELEASE</version>-->
<version>1.5.10.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.11</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
<exclusions>
<exclusion>
<groupId>io.springfox</groupId>
<artifactId>springfox-spring-web</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
添加ResolvedMethodParameterInterface繼承ResolvedMethodParameter
public class ResolvedMethodParameterInterface extends ResolvedMethodParameter {
public ResolvedMethodParameterInterface(String paramName, MethodParameter methodParameter, ResolvedType parameterType) {
this(methodParameter.getParameterIndex(),
paramName,
interfaceAnnotations(methodParameter),
parameterType);
}
public ResolvedMethodParameterInterface(int parameterIndex, String defaultName, List<Annotation> annotations, ResolvedType parameterType) {
super(parameterIndex, defaultName, annotations, parameterType);
}
public static List<Annotation> interfaceAnnotations(MethodParameter methodParameter) {
List<Annotation> annotationList = new ArrayList<>();
annotationList.addAll(Arrays.asList(methodParameter.getParameterAnnotations()));
if (CollectionUtils.isEmpty(annotationList)) {
for (Class<?> itf : methodParameter.getDeclaringClass().getInterfaces()) {
try {
Method method = itf.getMethod(methodParameter.getMethod().getName(), methodParameter.getMethod().getParameterTypes());
MethodParameter itfParameter = new MethodParameter(method, methodParameter.getParameterIndex());
annotationList.addAll(Arrays.asList(itfParameter.getParameterAnnotations()));
} catch (NoSuchMethodException e) {
continue;
}
}
}
return annotationList;
}
}
修改HandlerMethodResolver類(lèi)line 181嗅绸,將ResolvedMethodParameter替換為ResolvedMethodParameterInterface,重新打包deploy撕彤,并在swagger相關(guān)依賴中強(qiáng)制指定修改后的版本鱼鸠。
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--擴(kuò)展swagger支持從接口獲得方法參數(shù)注解-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-spring-web</artifactId>
<version>2.8.0-charles</version>
</dependency>
這樣就能夠順利生產(chǎn)swagger文檔啦。
feign不支持GET方法傳遞POJO
由于springMVC是支持GET方法直接綁定POJO的羹铅,只是feign實(shí)現(xiàn)并未覆蓋所有springMVC特效蚀狰,網(wǎng)上的很多變通方法都不是很好,要么是吧POJO拆散成一個(gè)一個(gè)單獨(dú)的屬性放在方法參數(shù)里职员,要么是把方法參數(shù)變成Map麻蹋,要么就是要違反HTTP協(xié)議,GET傳遞@RequestBody:
http://www.reibang.com/p/7ce46c0ebe9d
https://github.com/spring-cloud/spring-cloud-netflix/issues/1253
解決辦法焊切,使用feign攔截器:
public class CharlesRequestInterceptor implements RequestInterceptor {
@Autowired
private ObjectMapper objectMapper;
@Override
public void apply(RequestTemplate template) {
// feign 不支持 GET 方法傳 POJO, json body轉(zhuǎn)query
if (template.method().equals("GET") && template.body() != null) {
try {
JsonNode jsonNode = objectMapper.readTree(template.body());
template.body(null);
Map<String, Collection<String>> queries = new HashMap<>();
buildQuery(jsonNode, "", queries);
template.queries(queries);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
if (!jsonNode.isContainerNode()) { // 葉子節(jié)點(diǎn)
if (jsonNode.isNull()) {
return;
}
Collection<String> values = queries.get(path);
if (null == values) {
values = new ArrayList<>();
queries.put(path, values);
}
values.add(jsonNode.asText());
return;
}
if (jsonNode.isArray()) { // 數(shù)組節(jié)點(diǎn)
Iterator<JsonNode> it = jsonNode.elements();
while (it.hasNext()) {
buildQuery(it.next(), path, queries);
}
} else {
Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
if (StringUtils.hasText(path)) {
buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
} else { // 根節(jié)點(diǎn)
buildQuery(entry.getValue(), entry.getKey(), queries);
}
}
}
}
}
feign不支持路徑中的{version}
對(duì)于一個(gè)典型的Restful API定義如下:
@ApiOperation("帶過(guò)濾條件和排序的分頁(yè)查詢")
@RequestMapping(value = "/{version}/pb/product", method = RequestMethod.GET)
// 當(dāng)前版本新開(kāi)發(fā)api 隨微服務(wù)整體升級(jí) pt=protected 受保護(hù)的網(wǎng)關(guān)token驗(yàn)證合法可調(diào)用
@ApiImplicitParam(name = "version", paramType = "path", allowableValues = ProviderApiAutoConfig.CURRENT_VERSION, required = true)
Response<PageData<Product, Product>> selectAllGet(Page page);
我們并不關(guān)心路徑中的{version}扮授,因此方法參數(shù)中也沒(méi)有@PathVariable("version"),這個(gè)時(shí)候feign就傻了专肪,不知道路徑中的{version}應(yīng)該被替換成什么值糙箍。
解決辦法 使用自己的Contract替換SpringMvcContract,先將SpringMvcContract代碼復(fù)制過(guò)來(lái)牵祟,修改其中processAnnotationOnMethod方法的代碼深夯,從swagger注解中獲得{version}的值:
public class CharlesSpringMvcContract extends Contract.BaseContract
implements ResourceLoaderAware {
@Override
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation methodAnnotation, Method method) {
if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation
.annotationType().isAnnotationPresent(RequestMapping.class)) {
return;
}
RequestMapping methodMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
// HTTP Method
RequestMethod[] methods = methodMapping.method();
if (methods.length == 0) {
methods = new RequestMethod[]{RequestMethod.GET};
}
checkOne(method, methods, "method");
data.template().method(methods[0].name());
// path
checkAtMostOne(method, methodMapping.value(), "value");
if (methodMapping.value().length > 0) {
String pathValue = Util.emptyToNull(methodMapping.value()[0]);
if (pathValue != null) {
pathValue = resolve(pathValue);
// Append path from @RequestMapping if value is present on method
if (!pathValue.startsWith("/")
&& !data.template().toString().endsWith("/")) {
pathValue = "/" + pathValue;
}
// 處理version
if (pathValue.contains("/{version}/")) {
Set<ApiImplicitParam> apiImplicitParams = AnnotatedElementUtils.findAllMergedAnnotations(method, ApiImplicitParam.class);
for (ApiImplicitParam apiImplicitParam : apiImplicitParams) {
if ("version".equals(apiImplicitParam.name())) {
String version = apiImplicitParam.allowableValues().split(",")[0].trim();
pathValue = pathValue.replaceFirst("\\{version\\}", version);
}
}
}
data.template().append(pathValue);
}
}
// produces
parseProduces(data, method, methodMapping);
// consumes
parseConsumes(data, method, methodMapping);
// headers
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
}
}
然后在自己的AutoConfig中聲明成spring的bean
@Configuration
@ConditionalOnClass(Feign.class)
public class FeignAutoConfig {
@Bean
public Contract charlesSpringMvcContract(ConversionService conversionService) {
return new CharlesSpringMvcContract(Collections.emptyList(), conversionService);
}
@Bean
public CharlesRequestInterceptor charlesRequestInterceptor(){
return new CharlesRequestInterceptor();
}
}
spring cloud以及feign小技巧
要求源碼對(duì)dev環(huán)境負(fù)責(zé)(application.properties里的配置均為dev環(huán)境地址)
配置中心dev環(huán)境全局配置添加:
# dev環(huán)境允許本地配置override配置中心配置
spring.cloud.config.overrideNone=true
在開(kāi)發(fā)過(guò)程中,dev環(huán)境會(huì)部署一套微服務(wù)到服務(wù)器上,本機(jī)開(kāi)發(fā)時(shí)為了不影響dev服務(wù)器咕晋,spring.application.name添加自己姓名為前綴雹拄,注冊(cè)到dev注冊(cè)中心也不影響dev服務(wù)器,配置中心指定名字掌呜,不再隨spring.application.name變化滓玖。
啟動(dòng)參數(shù)添加:
--spring.application.name=charles-framework-provider
bootstrap.properties配置模板
# 配置中心預(yù)留
spring.cloud.config.uri=https://config-mo.xxxx.com/config
# config.name 可以不指定 默認(rèn)為 spring.application.name
# 這里指定名字是為了兼容本機(jī)注冊(cè)到dev環(huán)境時(shí)通過(guò)修改spring.application.name=charles-framework-provider來(lái)區(qū)分微服務(wù)以免造成干擾
spring.cloud.config.name=framework-provider
spring.cloud.config.profile=${ENV:dev}
# 連不上配置中心不啟動(dòng)
spring.cloud.config.fail-fast=true
這樣本機(jī)開(kāi)發(fā)可以調(diào)用服務(wù)器上的微服務(wù),但是服務(wù)器上的微服務(wù)不會(huì)調(diào)用本機(jī)(服務(wù)名修改了)质蕉。
那么如果兩名研發(fā)需要互相調(diào)用聯(lián)調(diào)該如何處理呢势篡?
@FeignClient注解不要寫(xiě)死服務(wù)名,使用place holder模暗,類(lèi)似如下代碼:
@FeignClient(ProviderApiAutoConfig.PLACE_HOLD_SERVICE_NAME)
public interface ProductService {
}
ProviderApiAutoConfig的代碼如下:
@Configuration
// 自動(dòng)配置feign掃描包 使用方零配置 微服務(wù)本身不加載自己的API中的feign
@ConditionalOnExpression("#{!environment['spring.application.name'].endsWith('" + ProviderApiAutoConfig.SERVICE_NAME + "')}")
@EnableFeignClients(basePackages = "com.github.charlesvhe.springcloud.practice.provider")
public class ProviderApiAutoConfig {
public static final String SERVICE_NAME = "provider";
// FeignClient 用placeholder可以方便的進(jìn)行內(nèi)部調(diào)用 配置key為charles.service.服務(wù)名
// 配置charles.service.framework-provider=charles-framework-provider來(lái)調(diào)用charles-framework-provider服務(wù)
public static final String PLACE_HOLD_SERVICE_NAME = "${charles.service." + SERVICE_NAME + ":" + SERVICE_NAME + "}";
public static final String CURRENT_VERSION = "v2";
public static final String COMPATIBLE_VERSION = "v2,v1";
}
這樣在啟動(dòng)參數(shù)中指定哪個(gè)服務(wù)想調(diào)哪個(gè)人的實(shí)例都可以禁悠。同時(shí)處理了service依賴api時(shí),不會(huì)注入feign的api實(shí)例兑宇。