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之间无法直接通信。此外,从代码风格的层面, 还有如下一些变更。
- 空字符串和null的传输:protoBuffer 协议对空字符串和null都序列化为空,反序列化的结果都是null。 应用程序的业务逻辑不应该使用空字符串和null表达不同语义。尽管对于REST,程序底层支持这样的区分, 仍然不建议将业务逻辑构筑在这个假设之上,避免后期升级和兼容陷阱。
- 如果请求参数或者返回值是一个POJO,假设Person,REST可以返回null,但是HIGHWAY始终会创建一个 Person 对象返回。业务逻辑尽可能不要依赖于null对象提供语义。
- 在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数据类型映射的细节,避免一些陷阱。 - HIGHWAY 的数据类型定义不支持一些特殊字符串,比如
H.264
和MPEG-2
在OPEN API里面是合法的 参数名称,但是在 protoBuffer 里面是不合法的参数名称。带有"."和"-"的参数名字不能用于 HIGHWAY。 如果存在,第一次访问这个接口的时候,会报告:io.protostuff.compiler.parser.ParserException: Could not parse syntax
异常。 - 对于 ENUM 有一个需要特别注意的地方,protoBuffer 采用int来序列化 ENUM ,int的默认值(即0)不会序列化,那么反 序列化的时候,ENUM 的缺省值必须为第一个值。应用程序需要将null和第一个缺省值当成一样的语义对待。比 较好的做法是在程序里面显示的给 ENUM 字段赋缺省值。
- HIGHWAY 不支持数组参数存在 null 值的情况。如果一个接口的参数是String[],那么里面的元素不能 为 null,否则序列化和反序列化会失败。
- 对于 primitive 类型,并且接口声明为 @Required,由于 HIGHWAY 并不会序列化缺省值,比如 0 等, 在Consumer端传递的参数值为 0 的时候,Provider 端并不能区分这个值是传递了0,还是没有传递。因 此 HIGHWAY 会忽略 @Required 声明,使用缺省值0。
-
如果一个属性的名称采用一个小写字母开头,并且只有一个小写字母,即使生成的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; } } ```
-
返回多种类型或者通过 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
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 的名称。
支持 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 修复。