前言

  • 前后花了两周多个时间完成了 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;
//                }
            }

        }

    }
}