thrift/swift:对swift2thrift-generator-cli IDL生成工具的改进

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类型处理了。

比如一个服务方法:

public test(Integer arg);

在生成thrift client代码时,对应的接口方法变成了

@ThriftMethod(value = "test")
public test(@ThriftField(value=1, name="arg", requiredness=Requiredness.NONE) final int arg);

一个类型:

@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代码时,对应的类变成了:

@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服务方法可以改为

public test( @ThriftField(value=1, name="arg", 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.

问题分析

ThriftFieldMetadata

通过分析swift的源码发现,不论是类的字段还是服务方法的参数,都是一个field,用com.facebook.swift.codec.metadata.ThriftFieldMetadata这个类来描述的。

Requiredness

在thrift IDL规范中每个field都可以指定必要性(requiredness),可以为optional(可选的),required(必须的),default(默认)。
在IDL文件中一个field如果是基本类型(Base Types,such as i32,i64,bool),且被定义为optional,那么生成的java代码中对应的类型就是该基本类型对应对象封装类型(Integer,Long,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方法重载):

/**
 * {@link ThriftFieldMetadata}的代理类,
 * 重载{@link #getRequiredness()}方法,根据参数类型对返回值进行修改
 * @author guyadong
 *
 */
@Immutable
public class DecoratorThriftFieldMetadata extends ThriftFieldMetadata {
	private static final Logger logger = Logger.getLogger(DecoratorThriftFieldMetadata.class.getName());
    private static Boolean primitiveOptional = null;
    /** 
     * {@link DecoratorThriftFieldMetadata}缓存对象,
     * 保存每个{@link ThriftFieldMetadata}对应的{@link DecoratorThriftFieldMetadata}实例 
     */
    private static final LoadingCache<ThriftFieldMetadata,DecoratorThriftFieldMetadata> 
    	FIELDS_CACHE = 
    		CacheBuilder.newBuilder().build(
    				new CacheLoader<ThriftFieldMetadata,DecoratorThriftFieldMetadata>(){
						@Override
						public DecoratorThriftFieldMetadata load(ThriftFieldMetadata key) throws Exception {
							return new DecoratorThriftFieldMetadata(key);
						}});
    /**  将{@link ThriftFieldMetadata}转换为 {@link DecoratorThriftFieldMetadata}对象 */
	public static  final Function<ThriftFieldMetadata,ThriftFieldMetadata> 
		FIELD_TRANSFORMER = 
			new Function<ThriftFieldMetadata,ThriftFieldMetadata>(){
				@Nullable
				@Override
				public ThriftFieldMetadata apply(@Nullable ThriftFieldMetadata input) {
				    return null == input || input instanceof DecoratorThriftFieldMetadata  
				    		? input 
				    		: FIELDS_CACHE.getUnchecked(input);
				}};
	private final Type javaType;
	private DecoratorThriftFieldMetadata(ThriftFieldMetadata input){
        super(
                input.getId(),
                input.getRequiredness(),
                input.getThriftType(),
                input.getName(),
                input.getType(),
                input.getInjections(),
                input.getConstructorInjection(),
                input.getMethodInjection(),
                input.getExtraction(),
                input.getCoercion());
		// 获取field的类型
		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;
			// 对于不支持的数据类型无法获取field类型,输出警告
			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.getRequiredness();
		checkState(Requiredness.UNSPECIFIED != requiredness);
		// 当为primitive类型时,Requiredness 为REQUIRED
		// 当为primitive类型的Object封装类型时(Long,Integer,Boolean),Requiredness为OPTIONAL
		if( !Boolean.FALSE.equals(primitiveOptional)
				&& javaType instanceof Class<?>
				&& requiredness == Requiredness.NONE){
			Class<?> parameterClass = (Class<?>)javaType;
			if(parameterClass.isPrimitive()){
				requiredness = Requiredness.REQUIRED;
				// logger.info(String.format("%s %s", parameterClass.getSimpleName(),requiredness));
			}else if(Primitives.isWrapperType(parameterClass)){
				requiredness = Requiredness.OPTIONAL;
				// logger.info(String.format("%s %s", parameterClass.getSimpleName(),requiredness));
			}
		}
		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 == DecoratorThriftFieldMetadata.primitiveOptional,"primitiveOptional is initialized already.");
		DecoratorThriftFieldMetadata.primitiveOptional = optional;
	}
}

偷天换日

有了上面的decorator,要让它发挥做用,还要做进一步的工作,需要用将原本对ThriftFieldMetadata的访问请求转向这个新的对象,以服务方法为例 ,我们同样需要写一个ThriftMethodMetadata的代理类。重载getParameters()方法,在这里完成对象转换(请求重定向)。
代码如下:

/**
 * 重载{@link #getParameters()}方法,用{@link DecoratorThriftFieldMetadata}替换{@link ThriftFieldMetadata}
 * @author guyadong
 *
 */
@Immutable
public class ThriftMethodMetadataCustom extends ThriftMethodMetadata
{
    private final List<ThriftFieldMetadata> parameters;
    public ThriftMethodMetadataCustom(String serviceName, Method method, ThriftCatalog catalog)
    {
        super(serviceName, method, catalog);
        parameters = Lists.transform(super.getParameters(), DecoratorThriftFieldMetadata.requirednessTransformer);
        // 这里用到了定义在DecoratorThriftFieldMetadata 中的 Function常量
    }

    @Override
    public List<ThriftFieldMetadata> getParameters()
    {
        // 返回转换成DecoratorThriftFieldMetadata类型的参数对象表
        return parameters;
    }
}

对于ThriftStruct对象(也就是我们在项目中自定义的java bean)。同样也要做上面类型的替换,需要对ThriftStructMetadata类的所有涉及访问其中的ThriftFieldMetadata对象的getField系列方法进行重载:

/**
 * {@link ThriftStructMetadata}的代理类<br>
 * 重载所有{@link ThriftFieldMetadata}相关方法
 * @author guyadong
 *
 */
@Immutable
public class DecoratorThriftStructMetadata extends ThriftStructMetadata {
    /** {@link DecoratorThriftStructMetadata}缓存对象,
     * 保存每个{@link ThriftStructMetadata}对应的{@link DecoratorThriftStructMetadata}实例 
     */
    private static final LoadingCache<ThriftStructMetadata,DecoratorThriftStructMetadata> 
    	STRUCTS_CACHE = 
    		CacheBuilder.newBuilder().build(
    				new CacheLoader<ThriftStructMetadata,DecoratorThriftStructMetadata>(){
						@Override
						public DecoratorThriftStructMetadata load(ThriftStructMetadata key) throws Exception {
							return new DecoratorThriftStructMetadata(key);
						}});
    /**  将{@link ThriftStructMetadata}转换为 {@link DecoratorThriftStructMetadata}对象 */
    public static final Function<ThriftStructMetadata,ThriftStructMetadata> 
    	STRUCT_TRANSFORMER = new Function<ThriftStructMetadata,ThriftStructMetadata>(){
    		@Nullable
			@Override
			public ThriftStructMetadata apply(@Nullable ThriftStructMetadata input) {
				return null == input || input instanceof DecoratorThriftStructMetadata
						? input
						: STRUCTS_CACHE.getUnchecked(input);
			}};
	private DecoratorThriftStructMetadata(ThriftStructMetadata input){
		super(input.getStructName(), 
				input.getStructType(), 
				input.getBuilderType(), 
				input.getMetadataType(), 
				input.getBuilderMethod(), 
				input.getDocumentation(), 
				ImmutableList.copyOf(input.getFields()),
				input.getConstructorInjection(), 
				input.getMethodInjections());
	}
	@Override
	public ThriftFieldMetadata getField(int id) {
		return DecoratorThriftFieldMetadata.FIELD_TRANSFORMER.apply(super.getField(id));
	}

	@Override
	public Collection<ThriftFieldMetadata> getFields() {
		return Collections2.transform(super.getFields(), DecoratorThriftFieldMetadata.FIELD_TRANSFORMER);
	}

	@Override
	public Collection<ThriftFieldMetadata> getFields(FieldKind type) {
		return Collections2.transform(super.getFields(type), DecoratorThriftFieldMetadata.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中央仓库,
命令行工具

<dependency>
    <groupId>com.gitee.l0km</groupId>
    <artifactId>swift2thrift-maven-plugin</artifactId>
    <version>1.7</version>
</dependency>

maven 插件

<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代码分析》
知道了原因,你就明白了:服务端也需要改造,改造的思路参照本文的思路就很容易想明白。有时间的话再我再就这个问题写个博文

<think>嗯,用户想了解Excel中的VLOOKUP函数的用法和参数含义。首先,我需要回忆一下VLOOKUP的基本结构。VLOOKUP是垂直查找函数,用于在表格的首列查找指定的值,然后返回该行中指定列的数据。它的四个参数分别是查找值、表格范围、列索引号和匹配模式。 接下来,我得确认每个参数的具体作用。第一个参数是lookup_value,也就是用户要查找的值,比如产品ID或者姓名。第二个参数是table_array,即查找的范围,这里要注意的是,查找值必须位于这个范围的第一列,否则函数无法正确工作。第三个参数是col_index_num,也就是用户想要返回的数据在表格范围中的第几列,这个列数是从表格范围的第一列开始算起的,不是整个工作表的列。第四个参数是range_lookup,决定是精确匹配还是近似匹配,通常用FALSE或0来指定精确匹配,避免错误。 然后,用户可能需要一个具体的例子来理解如何使用。比如,假设有一个产品价格表,用户想根据产品ID查找对应的价格。这时候,VLOOKUP就可以在ID列查找,然后返回价格列的数据。同时,要提醒用户注意表格范围最好使用绝对引用,比如$A$2:$C$100,这样在拖动公式时范围不会变化。 可能遇到的问题也需要考虑。比如,如果查找值不在表格的第一列,或者列索引号超过了表格的列数,函数会返回错误。此外,数据格式不一致也可能导致问题,比如查找值是数字,而表格中是文本格式的数字,这时候需要统一格式。还有,当出现#N/A错误时,可能是查找值不存在,或者匹配模式设置不正确,需要检查参数是否正确。 用户可能还会问到如何避免常见的错误,比如部分数据匹配出错的情况。这时候需要检查是否有多余的空格,或者数据格式不一致,或者表格范围没有锁定导致拖动时偏移。此外,XLOOKUP作为VLOOKUP的升级版,可能在某些情况下更方便,比如可以向左查找或者返回多列数据,这也是可以提及的内容。 最后,整理步骤,从基础语法到参数说明,再到具体案例,逐步引导用户理解如何使用VLOOKUP函数。同时,提供常见错误和解决方法,帮助用户在实际应用中避免问题。如果有必要,还可以比较VLOOKUP和其他查找函数的区别,比如HLOOKUP或XLOOKUP,但根据用户的问题,主要集中在VLOOKUP本身即可。</think>### Excel VLOOKUP函数详解 #### 一、基础语法与参数说明 VLOOKUP函数的语法为: $$=VLOOKUP(lookup\_value, table\_array, col\_index\_num, [range\_lookup])$$ 包含4个参数: 1. **lookup_value**(必填):要查找的值(如单元格引用或具体值) 2. **table_array**(必填):包含数据的表格范围(必须包含查找列和返回列) 3. **col_index_num**(必填):返回值所在列的序号(从table_array第一列开始计数) 4. **range_lookup**(可选):匹配类型 - `TRUE`/`1`:近似匹配(默认值,需数据升序排列) - `FALSE`/`0`:精确匹配(常用选项) [^1][^2] #### 二、使用步骤演示(工资表查询案例) 假设需要根据员工编号查询工资: 1. 建立查询单元格(如`B12`) 2. 输入公式: ```excel =VLOOKUP(A12, $A$2:$D$100, 4, 0) ``` - `A12`:待查询的员工编号 - `$A$2:$D$100`:锁定数据区域(绝对引用) - `4`:返回第4列(工资列) - `0`:精确匹配 [^2][^3] #### 三、常见错误与解决方法 | 错误现象 | 原因 | 解决方案 | |---------|------|---------| | #N/A | 查找值不存在 | 检查数据源或改用`IFERROR`容错 | | #REF! | 列序号超出范围 | 确认col_index_num ≤ 表格列数 | | 部分匹配失败 | 数据格式不一致 | 统一数值/文本格式 | | 结果错位 | 表格未锁定 | 使用`$`符号固定区域引用 | [^3][^4] #### 四、进阶技巧 1. **多条件查询**: 使用辅助列合并多个条件字段 ```excel =VLOOKUP(A2&B2, $D$2:$F$100, 3, 0) ``` 2. **通配符匹配**: `"*"`匹配任意字符,`"?"`匹配单个字符 ```excel =VLOOKUP("张*", $A$2:$C$100, 3, 0) ``` 3. **跨表查询**: 引用其他工作表数据 ```excel =VLOOKUP(A2, Sheet2!$A$2:$D$100, 4, 0) ``` [^1][^4]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10km

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值