记SpringCloud 1.X 升级到2.x

前言

  • 前后花了两周多个时间完成了 Spring Boot 1.5.6.RELEASE & Spring Cloud Dalston.SR4 升级到 Spring Boot 2.0.6.RELEASE & Spring Cloud Finchley.SR2 & spring-cloud-netflix 2.0.2.RELEASE 的工作。
  • 总结一下遇到的一些问题

一些问题的总结

  • 首先 1.x和2.x的所有的服务注册,服务发现,灰度调用,服务调用,zuul网关等等组件核心都是兼容的。so大胆的升级吧。
  • 其次maven pom变化较大,主要是netifilx的artifactId变化比较多,其余的变化都不是太大,这都可以通过spring-cloud-netflix-dependenciespom中找到。
  • 然后是feign的变化比较大,整个包名发生了变化。
  • NotBlank,NotEmpty 现在已经纳入了JSR303了,不需要在使用hibernate提供的注解了。

一些建议

  • 建议统一抽象出一个业务服务使用pom依赖项目,并打包发布维护起来,比如我们这里就叫my-server-dependencies的这么一个pom项目。这样做有几个好处:
  • 第一个是通过将版本统一管理起来了,方便对所有的基础服务jar包进行升级,而且能够完成版本的基础依赖管理。
  • 第二个是能够避免掉由于未付项目过多之后导致的依赖版本混乱。
  • 第三个是能够将maven插件统一的配置,比如说docker打包插件,fatjar插件,compiler插件 ,能统一的对他们进行控制和配置,并通过properties暴露出集体的调优指标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

<!--比如说我我在 dependencies 中定义了很多 props,这些是通用的服务配置 -->

<properties>
<prod.jvm.Xms>1G</prod.jvm.Xms>
<prod.jvm.Xmx>1G</prod.jvm.Xmx>
<prod.jvm.g1.newp>5</prod.jvm.g1.newp>
<prod.jvm.g1.maxp>60</prod.jvm.g1.maxp>
</properties>

<!--
比如说我们通过可视化监控发现,某个业务服务访问比较多对象生成的比较快,由于默认配置的堆太小,导致GC触发的比较频繁
由于默认的G1MaxNewSizePercent为60%,我们通过可视化监控发现odl区分配的40%堆空间的利用率不到10%。
那么这时候可以通过调节G1的G1MaxNewSizePercent和增大一些JVM的最大内存,以此来减少GC触发频率和更高的资源利用率
那么根据以上结论我们就需要修改一下相关配置。由于我们把这些关键配置都通过props暴露出来了,业务项目只需要如下几个props修改,就完成了我们想要的。
-->

<properties>
<prod.jvm.Xms>4G</prod.jvm.Xms>
<prod.jvm.Xmx>4G</prod.jvm.Xmx>
<prod.jvm.g1.newp>40</prod.jvm.g1.newp>
<prod.jvm.g1.maxp>80</prod.jvm.g1.maxp>
</properties>
  • 第四个是能够将一些必带的包默认激活,比如说 lombok spring-boot-starter-actuator micrometer-registry-prometheus 等。
  • 第五个建议是将profiles 统一定义在parent中,这样方便gitlab-ci.yml 文件的统一处理。

POM Change

  • parent 目前最新版是 2.0.6.RELEASE 点击这里获取最新版
  • 这里建议使用2.0.5+的spring boot 不然会有个DataSource的bug导致无法启动
    1
    2
    3
    4
    5
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.6.RELEASE</version>
    </parent>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 spring:
multipart:
max-file-size: 10Mb # 旧
servlet:
multipart:
max-request-size: 10MB
max-file-size: 10MB # 新配置

# 开启新的指标
management:
endpoints:
web:
exposure:
include: "*"

JSR303

  1. org.hibernate.validator.constraints.NotBlank ==> javax.validation.constraints.NotBlank
    2.org.hibernate.validator.constraints.NotEmpty ==> javax.validation.constraints.NotEmpty

ErrorController

  • org.springframework.boot.autoconfigure.web.ErrorController ==> org.springframework.boot.web.servlet.error.ErrorController
  • pom 更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
      <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>Finchley.SR2</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <dependency>
    <!-- Import dependency management from Spring Boot -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.0.6.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-dependencies</artifactId>
    <version>2.0.2.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    Eureka Server

  • eureka server 需要将artifactId更新
  • 旧的artifactId 是 spring-cloud-starter-eureka-server
  • 新的artifactId spring-cloud-starter-netflix-eureka-server
  • 更新完成之后的完成eurake-server配置如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    <exclusions>
    <exclusion>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    <!-- 为方便prometheus拉取数据的一个eurake到consul的适配器-->
    <dependency>
    <groupId>at.twinformatics</groupId>
    <artifactId>eureka-consul-adapter</artifactId>
    <version>${eureka-consul-adapter.version}</version>
    </dependency>
    </dependencies>

Eureka Client

  • 旧pom
1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
  • 更新的pom如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<!--这里排除掉jersey的依赖 spring cloud 会默认构建一个resttpml替代-->
<exclusions>
<exclusion>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-apache-client4</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>

Config client

1
2
3
4
5
6
7
8
9
10
11
<!--旧的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

<!--更新之后的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>

Feign

Feign的POM更新

  • 上面提到说feign的变化是最大的,如下是具体变化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      <!-- 旧的 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
    <!-- 新的 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

Feign 中的注解类

  • feign 注解变化
    1. org.springframework.cloud.netflix.feign.FeignClient ==> org.springframework.cloud.openfeign.FeignClient
    2. org.springframework.cloud.netflix.feign.EnableFeignClients ==> org.springframework.cloud.openfeign.EnableFeignClients

feign 默认的jackson配置会导致服务直接调用的抛出如下异常

1
Can not deserialize value of type java.util.Date from String "2018-11-02T04:14:56.761+0000": not a valid representation (error: Failed to parse Date value '2018-11-02T04:14:56.761+0000': Unparseable date: "2018-11-02T04:14:56.761+0000")
  • 原因是因为默认的jackson日期格式无法解析成yyyy-MM-dd HH:mm:ss
  • 默认的jackson配置也会把null值的属性序列化,这样会导致无用的字符串开销。所以这里我们需要配置一下fegin默认的Encoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public feign.codec.Encoder feignEncoder() {
return new SpringEncoder(() -> httpMessageConverters());
}

private HttpMessageConverters httpMessageConverters() {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
// 由于我们的feign只用于对内的请求,所以这里我们只需要使用Jackson的converter,所以new的时候第一个参数填false,排除掉默认的
return new HttpMessageConverters(false,Arrays.asList(new MappingJackson2HttpMessageConverter(mapper)));
}

#ZUUL

The default HTTP client used by Zuul is now backed by the Apache HTTP Client instead of the deprecated Ribbon RestClient. To use RestClient or okhttp3.OkHttpClient, set ribbon.restclient.enabled=true or ribbon.okhttp.enabled=true, respectively. If you would like to customize the Apache HTTP client or the OK HTTP client, provide a bean of type ClosableHttpClient or OkHttpClient.

Ribbon 灰度调用

  • 之前有提到过通过扩展ribbon支持灰度,由于2.0的spring boot 默认在eurake的matedata重携带了一些稀奇古怪的东西,导致我们之前的代码不能用了,修改后的结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class MetadataAwareRule extends ZoneAvoidanceRule {

@Override
public Server choose(Object key) {
final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
ILoadBalancer lb = getLoadBalancer();
final List<Server> allServers = lb.getAllServers();

// 存放已打标签但不满足标签的server
final List<Server> metaServers = new ArrayList<>();

// 存放未标签的server
final List<Server> noMetaServers = new ArrayList<>();

// 匹配成功的server
final List<Server> matchedMetaServers = new ArrayList<>();


final Map<String, String> attributes = context.getAttributes();
// 取得接口端传入的参数
final String inputDeveloper = attributes.get("developer");

for (Server server : allServers) {
if (server instanceof DiscoveryEnabledServer) {
final DiscoveryEnabledServer discoveryEnabledServer = (DiscoveryEnabledServer) server;
final Map<String, String> metadata = discoveryEnabledServer.getInstanceInfo().getMetadata();
final String developer = metadata.get("developer");
// 如果没有meta数据 表示是测试服务上的地址
if (developer == null || developer.equals("")) {
// 存放并没有打标签的server
noMetaServers.add(server);
} else {
// 如果匹配成功开发者直接调用
if (inputDeveloper != null && (!"".equals(inputDeveloper)) && developer.equals(inputDeveloper)) {
matchedMetaServers.add(server);
} else {
// 存入server有标签但是不匹配的server
metaServers.add(server);
}
}


}
}

//优先走自定义路由。即满足灰度要求的server
if (!matchedMetaServers.isEmpty()) {
com.google.common.base.Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(matchedMetaServers, key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
// 如果没有匹配成功的则走
else {
if (!noMetaServers.isEmpty()) {
com.google.common.base.Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(noMetaServers, key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
} else {
// 似情况打开
return null;
// com.google.common.base.Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(metaServers, key);
// if (server.isPresent()) {
// return server.get();
// } else {
// return null;
// }
}

}

}
}