swift2thrift-generator-cli是thrift/swift提供的一个IDL文件命令行生成工具,它可以根据一个java服务接口类(interface,class)生成对应的IDL文件。
对于基于java做thrift框架的开发项目来说,这可是个神器,如果你的服务端是java开发的,就不需要手工写IDL文件(反正打死我也是不会手写的,太多了),使用这个命令行工具,可以一秒钟生成IDL,再用另一个工具swift-generator-cli就可以将根据生成的IDL生成java client/service调用代码了。这个过程我在之前的一篇博文有详细介绍,参见《thrift:swift 命令行生成 IDL文件及Client java代码过程》。
IDL是thrift的接口定义语言,有了IDL格式的接口定义脚本,就可以生成不同开发语言的thrift代码,官网说明参见 《Thrift interface description language》
问题描述
但是后续的开发过程中发现使用swift2thrift-generator-cli生成IDL有一个问题:
对于primitive的对象封装类型(Integer,Long,Boolean),不论是做为字段还是做为服务方法的参数,swift2thrift-generator-cli都把它当做primitive类型处理了。
比如一个服务方法:
@H_
502_14@
public test(Integer arg);
在生成thrift client代码时,对应的接口方法变成了
@H_
502_14@
@ThriftMethod(value =
"test")
public test(@
ThriftField(value=
1,name=
"arg",
requiredness=
requiredness.NONE)
final int arg);
一个类型:
@H_
502_14@
@ThriftStruct
public final TestBean{
private Integer id;
@ThriftField(
1)
public Integer
getId(){
return id;
}
@ThriftField
public void setId(Integer id){
this.id = id;
}
}
在生成thrift client代码时,对应的类变成了:
@H_
502_14@
@ThriftStruct(
"TestBean")
public final TestBean{
private int id;
@ThriftField(value=
1,name=
"id",
requiredness=
requiredness.NONE)
public int getId(){
return id;
}
@ThriftField
public void setId(
int id){
this.id = id;
}
}
仔细想想这是个大问题:比如我想传一个null参数,在这种情况下这就不可能了,
在很多情况下null并非完全没有意义,如果传一个0当做null,需要client/service双方约定好才行,而且很多情况下0有可能是个有意义的值。
换个别的值?还是有歧义的可能,所以无论如何应该在thrift这一层解决这个问题而不是让应用项目来解决。
有没有解决办法?
当然有,地球人都知道的,手工解决办法很简单在服务方法或类定义时加上requiredness.OPTIONAL
定义,告诉swift2thrift-generator-cli这个字段是可选的。
比如上面的test服务方法可以改为
@H_
502_14@
public test( @
ThriftField(value=
1,
requiredness=
requiredness.OPTIONAL)Integer arg);
这样在生成的thrift 接口代码中arg参数的类型就是希望的Integer。
如果你的服务接口很简单只有很少的方法,涉及的类也不多,那么这个办法,可以解决你的问题。
但是如果服务接口如果非常庞大,涉及的类也很多,手工维护这些属性标记就是个灾难。
很不幸,我遇到的就是这种情况,服务接口中有超过100个方法,还在增加中,涉及的类有十几个,加起来有上百个字段。。。有int,也有Integer(有的必须给值,有的可以为null)。手工去加这些属性太麻烦了,还非常容易出错。
怎么办呢?
从IDL生成工具swift2thrift-generator-cli入手改造它!
这就是本文的中心任务。
改造目标
从swift2thrift-generator-cli源码入门,在此基础上修改swift2thrift-generator-cli生成IDL的逻辑,对于一个字段或参数,如果它是primitive类型,就指定为required
,如果它是primitive对应的对象封装类型(wraptype),就指定为optional
.
问题分析
通过分析swift的源码发现,不论是类的字段还是服务方法的参数,都是一个field,用com.facebook.swift.codec.Metadata.ThriftFieldMetadata
这个类来描述的。
在thrift IDL规范中每个field都可以指定必要性(requiredness
),可以为optional
(可选的),required
(必须的),default
(默认)。
在IDL文件中一个field如果是基本类型(Base Types,such as i32,i64,bool),且被定义为optional
,那么生成的java代码中对应的类型就是该基本类型对应对象封装类型(Integer,Boolean),如果没有指定,那么它就会被生成基本类型对应的primitive类型(int,long,boolean)。
ThriftFieldMetadata
中有一个枚举型(com.facebook.swift.codec.ThriftField.requiredness
)字段requiredness就是指定该字段的必要性。
基本思路
了解了上面这个关键点,我的解决方案基本思路成形了:
为ThriftFieldMetadata
类写一个装饰类(decorator)或叫代理类,只需要重载getrequiredness()
方法,在这个方法中实现前面改造目标中描述的逻辑,根据该field的java type返回我们需要的requiredness
。然后将所有对ThriftFieldMetadata
的访问(读取,ThriftFieldMetadata
是不可变对象)都重定义到这个代理类。这样,在生成IDL过程中对每个field获取的requiredness
就是我们希望的值。
decorator
decorator的实现并不复杂,全部代码如下(代码中用到了google guava提供的cache技术用于减少重复对象创建提高性能,真正核心关键的地方就是getrequiredness
方法重载):
@H_
502_14@
/** * {@link ThriftFieldMetadata}的代理类, * 重载{@link #getrequiredness()}方法,根据参数类型对返回值进行修改 * @author guyadong * */
@Immutable
public class DecoratorThriftFieldMetadata extends ThriftFieldMetadata {
private static final Logger logger = Logger.getLogger(DecoratorThriftField
Metadata.class.getName());
private static Boolean primitiveOptional =
null;
/** * {@link DecoratorThriftFieldMetadata}缓存对象,* 保存每个{@link ThriftFieldMetadata}对应的{@link DecoratorThriftFieldMetadata}实例 */
private static final LoadingCache<ThriftField
Metadata,DecoratorThriftField
Metadata>
FIELDS_CACHE =
CacheBuilder.newBuilder().build(
new CacheLoader<ThriftField
Metadata,DecoratorThriftField
Metadata>(){
@Override
public DecoratorThriftField
Metadata
load(ThriftField
Metadata key)
throws Exception {
return new DecoratorThriftField
Metadata(key);
}});
/** 将{@link ThriftFieldMetadata}转换为 {@link DecoratorThriftFieldMetadata}对象 */
public static final Function<ThriftField
Metadata,ThriftField
Metadata>
FIELD_TRANSFORMER =
new Function<ThriftField
Metadata,ThriftField
Metadata>(){
@Nullable
@Override
public ThriftField
Metadata
apply(@Nullable ThriftField
Metadata input) {
return null == input || input
instanceof DecoratorThriftField
Metadata
? input
: FIELDS_CACHE.getUnchecked(input);
}};
private final Type javaType;
private DecoratorThriftFieldMetadata(ThriftField
Metadata input){
super(
input.getId(),input.get
requiredness(),input.getThriftType(),input.getName(),input.getType(),input.getInjections(),input.getConstructorInjection(),input.getMethodInjection(),input.getExtraction(),input.getCoercion());
List<ThriftInjection> injections = getInjections();
checkState(injections.size()>
0,
"invalid size of injections");
ThriftInjection injection = injections.get(
0);
if(injection
instanceof ThriftParameterInjection){
javaType = ((ThriftParameterInjection)injection).getJavaType();
}
else if(injection
instanceof ThriftFieldInjection){
javaType = ((ThriftFieldInjection)injection).getField().getType();
}
else{
javaType =
null;
logger.warning(
String.format(
"UNSUPPORED TYPE %s,can't get Java Type. "
+
"(不识别的ThriftInjection实例类型,无法实现requiredness转义)",
null == injection?
null : injection.getClass().getName()));
}
}
/** 重载方法,实现 requiredness 转义 */
@Override
public requiredness
getrequiredness() {
requiredness
requiredness =
super.get
requiredness();
checkState(
requiredness.UNSPECIFIED !=
requiredness);
if( !Boolean.FALSE.equals(primitiveOptional)
&& javaType
instanceof Class<?>
&&
requiredness ==
requiredness.NONE){
Class<?> parameterClass = (Class<?>)javaType;
if(parameterClass.isPrimitive()){
requiredness =
requiredness.
required;
}
else if(Primitives.isWrapperType(parameterClass)){
requiredness =
requiredness.OPTIONAL;
}
}
return requiredness;
}
/** * 设置optional标记<br> * 指定{@link #getrequiredness}方法调用时是否对primitive类型及其封装类型(Integer,Long)参数的返回值进行替换<br> * 默认值:{@code true}<br> * 该方法只能被调用一次 * @param optional * @see #getrequiredness() * @throws IllegalStateException 方法已经被调用 */
public static synchronized void setPrimitiveOptional(
boolean optional) {
checkState(
null == DecoratorThriftField
Metadata.primitiveOptional,
"primitiveOptional is initialized already.");
DecoratorThriftField
Metadata.primitiveOptional = optional;
}
}
偷天换日
有了上面的decorator,要让它发挥做用,还要做进一步的工作,需要用将原本对ThriftFieldMetadata
的访问请求转向这个新的对象,以服务方法为例 ,我们同样需要写一个ThriftMethodMetadata
的代理类。重载getParameters()
方法,在这里完成对象转换(请求重定向)。
代码如下:
@H_
502_14@
/** * 重载{@link #getParameters()}方法,用{@link DecoratorThriftFieldMetadata}替换{@link ThriftFieldMetadata} * @author guyadong * */
@Immutable
public class ThriftMethodMetadataCustom extends ThriftMethodMetadata {
private final List<ThriftField
Metadata> parameters;
public ThriftMethodMetadataCustom(String serviceName,Method method,ThriftCatalog catalog)
{
super(serviceName,method,catalog);
parameters = Lists.transform(
super.getParameters(),DecoratorThriftField
Metadata.
requirednessTransformer);
}
@Override
public List<ThriftField
Metadata>
getParameters()
{
return parameters;
}
}
对于ThriftStruct
对象(也就是我们在项目中自定义的java bean)。同样也要做上面类型的替换,需要对ThriftStructMetadata
类的所有涉及访问其中的ThriftFieldMetadata
对象的getField
系列方法进行重载:
@H_
502_14@
/** * {@link ThriftStructMetadata}的代理类<br> * 重载所有{@link ThriftFieldMetadata}相关方法 * @author guyadong * */
@Immutable
public class DecoratorThriftStructMetadata extends ThriftStructMetadata {
/** {@link DecoratorThriftStructMetadata}缓存对象,* 保存每个{@link ThriftStructMetadata}对应的{@link DecoratorThriftStructMetadata}实例 */
private static final LoadingCache<ThriftStruct
Metadata,DecoratorThriftStruct
Metadata>
STRUCTS_CACHE =
CacheBuilder.newBuilder().build(
new CacheLoader<ThriftStruct
Metadata,DecoratorThriftStruct
Metadata>(){
@Override
public DecoratorThriftStruct
Metadata
load(ThriftStruct
Metadata key)
throws Exception {
return new DecoratorThriftStruct
Metadata(key);
}});
/** 将{@link ThriftStructMetadata}转换为 {@link DecoratorThriftStructMetadata}对象 */
public static final Function<ThriftStruct
Metadata,ThriftStruct
Metadata>
STRUCT_TRANSFORMER =
new Function<ThriftStruct
Metadata,ThriftStruct
Metadata>(){
@Nullable
@Override
public ThriftStruct
Metadata
apply(@Nullable ThriftStruct
Metadata input) {
return null == input || input
instanceof DecoratorThriftStruct
Metadata
? input
: STRUCTS_CACHE.getUnchecked(input);
}};
private DecoratorThriftStructMetadata(ThriftStruct
Metadata input){
super(input.getStructName(),input.getStructType(),input.getBuilderType(),input.get
MetadataType(),input.getBuilderMethod(),input.getDocumentation(),ImmutableList.copyOf(input.getFields()),input.getMethodInjections());
}
@Override
public ThriftField
Metadata
getField(
int id) {
return DecoratorThriftField
Metadata.FIELD_TRANSFORMER.apply(
super.getField(id));
}
@Override
public Collection<ThriftField
Metadata>
getFields() {
return Collections2.transform(
super.getFields(),DecoratorThriftField
Metadata.FIELD_TRANSFORMER);
}
@Override
public Collection<ThriftField
Metadata>
getFields(FieldKind type) {
return Collections2.transform(
super.getFields(type),DecoratorThriftField
Metadata.FIELD_TRANSFORMER);
}
}
按照 上面的思路,以此类推要换掉在IDL生成过程中涉及ThriftFieldMetadata访问所有环节。就可以了。
限于篇幅,这里不再贴更多代码,需要完整的代码可以访问码云上的Git仓库:
https://gitee.com/l0km/idl-generator
需要用maven编译,下载代码后执行mvn package
就可以生成一个uber-jar.
执行下面的命令就可以看到用法说明。
java -jar idl-generator-cli-1.7-standalone.jar
同时上面的gitee项目地址中还包含对应的maven插件,更多详细信息参见README.md
项目的二进制文件jar包已经上传到maven中央仓库,
命令行工具
@H_
502_14@
<dependency>
<groupId>com.gitee.l0km
</groupId>
<artifactId>swift2thrift-maven-plugin
</artifactId>
<version>1.7
</version>
</dependency>
maven 插件
@H_
502_14@
<dependency>
<groupId>com.gitee.l0km
</groupId>
<artifactId>swift2thrift-maven-plugin
</artifactId>
<version>1.7
</version>
</dependency>
如果你只想直接下载jar包运行,可以从maven中央仓库下载:
http://central.maven.org/maven2/com/gitee/l0km/idl-generator-cli/1.7/idl-generator-cli-1.7-standalone.jar
后记
那现在可以传递一个类型为Integer的null值到服务端了么?
说实话,还是不行…
啊?!!!那不是白干了?那你废半天劲写这一大堆文字干嘛?说说为什么不行啊?
关于为什么,可以参见我的上篇博文《thrift/swift:ThriftMethodProcessor代码分析》。 知道了原因,你就明白了:服务端也需要改造,改造的思路参照本文的思路就很容易想明白。有时间的话再我再就这个问题写个博文