1.3.0 升级 2.0.0指导

2.0.0 版本实现了 弱类型契约。 总体而言,对于 REST 通信模式, 弱类型契约不仅增强了写代码的灵活性, 还完整保留了强类型契约的写代码方式,几乎不 存在用户需要感知的变更。 对于 HIGHWAY 通信模式, 由于底层采用 ProtoBuffer 编码, 而 ProtoBuffer 天然就是一种 强类型契约的编解码过程, java-chassis 为了支持弱类型契约, 做了大量努力, 在一些边界条件处理上与弱类型契约存在 变更,两个版本的编解码是不兼容的,需要同时升级提供者和消费者。 在编码方式上,差异主要体现在对于缺省值的处理,对于 null 的处理等问题上。

Highway通信协议的变更

Highway通信协议是2.0.0最大的变更点。如果业务采用了Highway通信协议,需要确保所有相关的consumer 和provider都必须升级,低版本的consumer和高版本的provider之间无法直接通信。此外,从代码风格的层面, 还有如下一些变更。

  1. 空字符串和null的传输:protoBuffer 协议对空字符串和null都序列化为空,反序列化的结果都是null。 应用程序的业务逻辑不应该使用空字符串和null表达不同语义。尽管对于REST,程序底层支持这样的区分, 仍然不建议将业务逻辑构筑在这个假设之上,避免后期升级和兼容陷阱。
  2. 如果请求参数或者返回值是一个POJO,假设Person,REST可以返回null,但是HIGHWAY始终会创建一个 Person 对象返回。业务逻辑尽可能不要依赖于null对象提供语义。
  3. 在contrast first编程模式情况下,用户先写契约,然后通过契约生成代码,并将契约文件放 到 microservices/{microservice name}/{schema id}.yaml 文件中,这种情况下不会通过代码生成 契约。通常通过工具生成代码,工具生成的数据类型和契约的数据类型匹配,但是如果这种场景的代码经过 修改,并且yaml中的数据类型是number,而代码中的类型是Integer,在HIGHWAY通信模式下会报告错误。 HIGHWAY 会将 number 类型解析为 protoBuffer 的 double 类型, 对应的 JAVA 类型为 double。 protoBuffer 不支持将Integer类型采用Double的方式序列化。在contract first编程模式下,建议通过 生成工具生成代码,这样用户就不用了解swagger数据类型和JAVA数据类型映射的细节,避免一些陷阱。
  4. HIGHWAY 的数据类型定义不支持一些特殊字符串,比如 H.264MPEG-2 在OPEN API里面是合法的 参数名称,但是在 protoBuffer 里面是不合法的参数名称。带有"."和"-"的参数名字不能用于 HIGHWAY。 如果存在,第一次访问这个接口的时候,会报告: io.protostuff.compiler.parser.ParserException: Could not parse syntax 异常。
  5. 对于 ENUM 有一个需要特别注意的地方,protoBuffer 采用int来序列化 ENUM ,int的默认值(即0)不会序列化,那么反 序列化的时候,ENUM 的缺省值必须为第一个值。应用程序需要将null和第一个缺省值当成一样的语义对待。比 较好的做法是在程序里面显示的给 ENUM 字段赋缺省值。
  6. HIGHWAY 不支持数组参数存在 null 值的情况。如果一个接口的参数是String[],那么里面的元素不能 为 null,否则序列化和反序列化会失败。
  7. 对于 primitive 类型,并且接口声明为 @Required,由于 HIGHWAY 并不会序列化缺省值,比如 0 等, 在Consumer端传递的参数值为 0 的时候,Provider 端并不能区分这个值是传递了0,还是没有传递。因 此 HIGHWAY 会忽略 @Required 声明,使用缺省值0。
  8. 如果一个属性的名称采用一个小写字母开头,并且只有一个小写字母,即使生成的getter/setter是合法 的, swagger 生成的属性名称还是会出现错误。应该尽可能避免使用这样的属性名称。必须使用的场景,需要 显示的使用 @JsonProperty 声明。

     ```java
        public class SpecialNameModel {
          // names starts with only one lower case , although getter/setter generated by IDE is correct,
          // will cause jackson generate incorrect swagger names.
          // @JsonProperty must be used to make json work in a predictable way.
          @JsonProperty("aIntName")
          private int aIntName;
    
          public int getaIntName() {
            return aIntName;
          }
    
          public void setaIntName(int aIntName) {
            this.aIntName = aIntName;
          }
        }
     ```
    
  9. 返回多种类型或者通过 ContextUtils 设置状态码 HIGHWAY 不再支持。 尽管 1.3.0 之前用户也不会在 HIGHWAY 模式下 使用类似下面的代码, 但是在 1.3.0 之前的版本, 这些代码部分确实能够工作。

    ```java
          @PUT
          public String sayHi(@PathParam("name") String name) {
              ContextUtils.getInvocationContext().setStatus(202);
              return name + " sayhi";
          }
     ```
    

优秀实践:

如果项目中需要同时使用 REST 和 HIGHWAY 对外提供服务, 并且会使用到一些 HIGHWAY 协议有差异的用法, 可以将这些接口定义到不同的类中, 使用不一样的 Schema ID 进行区分,比如 MySchemaHighwayOnly , MySchemaRestOnly , MySchema

使用 Edge Service 场景下 Model 的缺省值

假设业务应用采用 Edge Service 转发请求。 并且定义了一个接口, 有如下 Model 作为参数:

public class Person {
  private Integer age = 30;
  private List<String> items;
}

用户从浏览器调用这个接口, JSON 内容为 {} , 不传递任何内容, 1.3.0 版本得到的 age = null, items = null 。 2.0.0 版本 age = 30, items 为空表, 不为 null 。 这个行为是由于 1.3.0 版本 Edge Service 会自动生成一个 Person 类, 这个类没有缺省值, Edge Service 重新序列化, 造成服务端取到了 null。 弱类型契约没有中间类型, 序列化的结果 和用户从浏览器传递过来的值一样。

RestTemplate的使用

对于下面的 consumer 和 provider 代码:

// provider
@PostMapping(path = "/object")
public Object testObject(@RequestBody Object input) 

// consumer
Object result = restTemplate.postForObject(prefix + "/object", 
  new EmptyObject(), EmptyObject.class);

1.3.0 版本返回的 result 类型为 Map。 2.0.0 版本返回的类型和 postForObject 指定的类型一致,上面的示例 中,result 类型为 EmptyObject。

下面的代码,1.3.0 和 2.0.0 版本运行的结果是一样的:

List<GenericObjectParam<List<RecursiveObjectParam>>> response = consumers.getSCBRestTemplate()
   postForObject("/testListObjectParam", request, List.class);

前提条件是 GenericObjectParam 和 RecursiveObjectParam 在 consumer 的 classpath 中存在对应的 类,并且 package 和服务端定义的类一样。如果不一样, 则 response 类型为 List,上面的代码会 抛出类型转换异常。2.0.0 没有相同 package 的约束,并且在保持兼容的情况下,支持下面的用法:

HttpEntity<SpringmvcBasicRequestModel> requestEntity = new HttpEntity<>(requestModel, null);
List<SpringmvcBasicResponseModel> responseModelList = 
    template.exchange("/postListObject", HttpMethod.POST, requestEntity,
        new ParameterizedTypeReference<List<SpringmvcBasicResponseModel>>() {
        }).getBody();

这种方式的语义根据清晰,在使用泛型的时候,建议采用这种用法。

在1.3.0版本支持如下用法:

template.postForEntity(url, 
    "{\"time\":3073113710456,\"date\":3073113710456,\"holder\":\"test\"}",
    DateTimeModel.class)

即通过 JSON String 的方式传递参数给后台的 DateTimeModel 对象。 由于这种用法在解析的时候存在二义性, 2.0.0 不再支持这种用法。 可以使用 Map 或者 JsonObject 来传递。

AsyncRestTemplate的使用

1.3.0 和 2.0.0 中 AsyncRestTemplate 的使用方式一样,没有变化。但是由于 2.0.0 只支持spring 5版 本,而 spring 5 将 AsyncRestTemplate 标记为废弃状态,开发者在后续开发过程中尽可能不要使 用 AsyncRestTemplate 。可以使用 CompletableFuture 来替代,可以参考这 个例子

Spring Boot 集成的变化

2.0.0 不再支持 spring 4 和 spring boot 1, 缺省使用 spring 5 和 spring boot 2, 并修改了相 关 starters 的名称。 可以通过阅 读 在Spring Boot中使用java chassis 了解 相关变化。

支持 JDK 11

2.0.0 版本可以在 JDK 11 下运行,并进行了简单的集成测试。 2.0.0 支持的核心 JDK 版本仍然是 8, 并没有采用 JDK 11 编译。 JDK 11 的一个主要变化是后续可能不再支持通过反射改变类的封装性。 这个特性目前有很多地方使用, 2.0.0 版本为了适配 JDK 11, 某些特性的使用会发生变化。 具体有如下几个特性 :

  • 使用 EventManager 注册事件

在 1.3.0 版本, 允许采用 private 类 或者 内部匿名类作为事件监听对象, 比如:

public void myMethod() {
    Object receiveEvent = new Object() {
      @Subscribe    
      public void onEvent(AlarmEvent circutBreakerEvent) {  
        taskList.add(circutBreakerEvent);   
      } 
    };  
    EventManager.getEventBus().register(receiveEvent);
}

在 2.0.0 版本不允许,启动的时候会报告异常。 2.0.0 版本注册的事件监听器,必须保证对于 EventManager 类具有可访问性。 通常 定义的类和 EventManager 不属于同一个 package , 因此这个类必须是 public 的, 事件处理方法也必须是 public 的。

  • 定义接口的 Model 使用匿名内部类

在 1.3.0 版本, 使用匿名内部类作为 REST 接口的 Model 是允许的, 但是 2.0.0 版本不允许。 如果采用这样的类型作为接口参数, 启动的时候会报告异常。

  • 其他方面的影响

由于 JDK 11 不允许通过反射破坏封装, 早期通过反射修改 private 字段的值,来规避一些三方软件的 bug, 以及做一些额外 定制变得不可行, 使用这些特性 JDK 11 暂时只是打印警告, 在 JDK 13 等更高版本会彻底禁止。 因此业务开发的时候, 尽可能 不要使用破坏封装的特性。

常见问题

  • java-chassis运行时依赖于接口定义里面的名字

为了更好的基于swagger对服务进行治理,以及提高客户端代码书写的灵活性,java-chassis要求书写的接口定义代码在编译的时候,带上参数名称信息,否则会报告如下错误:

Caused by: java.lang.IllegalStateException: parameter name is not present, method=org.apache.servicecomb.samples.porter.file.api.InternalAccessEndpoint:localAccess
solution:
  change pom.xml, add compiler argument: -parameters, for example:
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <compilerArgument>-parameters</compilerArgument>
      </configuration>
    </plugin>

解决该问题可以通过配置maven compiler plugin, 加上-parameters参数。如果在IDE下面运行,需要设置 build -> java compilers 在编译参数里面增加-parameters。

  • spring 5变更

cse.bean.xml文件如果采用了classpath查找定义文件

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans classpath:org/springframework/beans/factory/xml/spring-beans-3.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

会报告下面的错误:

[main][WARN][org.springframework.beans.factory.xml.XmlBeanDefinitionReader:48] Ignored XML validation warning
org.xml.sax.SAXParseException: schema_reference.4: 无法读取方案文档 'classpath:org/springframework/beans/factory/xml/spring-beans-3.0.xsd', 原因为 1) 无法找到文档; 2) 无法读取文档; 3) 文档的根元素不是 <xsd:schema>。
    at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:203) ~[?:1.8.0_131]
    at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.warning(ErrorHandlerWrapper.java:99) [?:1.8.0_131]
    at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:392) [?:1.8.0_131]

修改为:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
  • SCBEngine使用时机

代码

@RestSchema(schemaId = "inspector")
@Path("/inspector")
public class InspectorEndpoint {
  private InspectorConfig inspectorConfig;

  public InspectorEndpoint() {
    this.inspectorConfig = SCBEngine.getInstance().getPriorityPropertyManager().createConfigObject(InspectorConfig.class);
  }
Caused by: java.lang.NullPointerException
    at org.apache.servicecomb.core.SCBEngine.<init>(SCBEngine.java:126)
    at org.apache.servicecomb.core.SCBEngine.getInstance(SCBEngine.java:159)
    at org.apache.servicecomb.samples.porter.file.api.InspectorEndpoint.<init>(InspectorEndpoint.java:82)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)

不能够在bean的初始化里面使用SCBEngine的实例。这个实例业务需要在事件AFTER_REGISTRY等处理函数中使用。

编译错误

2.0.0 相对于 1.0.0 升级了大量三方件,包括netty, vert.x, spring, spring boot等,直接引用这些组件的代码可能编译失败。同时还对代码进行了一定重构,有些代码使用了java-chassis未公开接口,使用这些接口可能编译失败。下面是一些常见的问题。下面一些PR的修改可以参考:

下面是一些场景的问题:

  • router.routeWithRegex(regex).handler(CookieHandler.create()) 提示 CookieHandler deprecated,删除这行代码 即可,新版本的vert.x已经默认提供了cookie处理。
  • io.vertx.ext.web中的 io.vertx.ext.web.Cookie已过时 , 修改 为 io.vertx.core.http.Cookie

2.0.0 一些可能被外部使用的内部接口调整:

  • Invocation 类删除了 getArgs 接口, 替换为 getSwaggerArguments , 同时新增了 getInvocationArguments, 关于这个变更的说明,请参考新特性介绍文章弱类型契约
  • 删除 DynamicSchemaLoader , 这个类早期版本提供出来是方便注册契约, 最新版本客户端契约发现可以通过服务中心 完成,不再需要这样的功能。
  • CseContext.getInstance().getTransportManager().findTransport(Const.RESTFUL) 修改 为 SCBEngine.getInstance().getTransportManager().findTransport(Const.RESTFUL)
  • 测试代码可能使用 CseContext.getInstance().getConsumerProviderManager().setTransport(microserviceName, transport) , 修改 为 ArchaiusUtils.setProperty("servicecomb.references.transport." + microserviceName, transport);

已知缺陷

  • 2.0.0 版本将 servicecomb.service.registry.registerUrlPrefix 弄丢了。 这个配置项是为一个特殊场景服务的的, 默认值为 false。 在使用 WEB 容器(比如 tomcat) java-chassis的场景下, 如果设置了 context-path, 使用 java-chassis 提供的 RestTemplate 访问服务的时候, 不需要指定 context-path,以保证用户不用关注微服务的部署方式,提供了很大的便利。 但是有些用户的代码是历史遗留代码改造过来的,期望 URL 包含完整的 context-path。 使用 2.0.0 版本如果存在这个特殊要 求, 程序会运行错误,访问接口提示 NOT FOUND。这个问题在 2.0.1 修复。