使用Swagger生成Feign桩
在Eureka中,默认通过RestController进行RPC调用,虽然使用比较简单,但是纯手写还是比较麻烦的。目前一般有如下调用方法
RPC Stub | PROS | CONS |
---|---|---|
HttpClient/OkHttp | hight customized | maintains urls, serialization |
Feign/Retrofit | Dynamic Proxy, Interceptors | maintains source code |
Swagger Codegen | Auto generated maven jar | maintains templates |
其中Swagger Codegen方法中可以通过手动更新到VSC与通过Jenkins部署到MVN上实现,在实际项目中,你可以首先以源码形式进行管理,后面在切到全部自动化管理
graph LR
a2--mvn deploy-->b1
subgraph Swagger
a1(API)--swagger codegen maven plugin-->a2(POM Source)
end
subgraph Maven nexus
b1(Jar)
end
虽然网上讲Swagger代码生成很多 ,但是中文似乎研究的人并不多,而且官方的生成器模版有一堆定制问题,因此本文进行一下介绍
简易生成代码桩
这个仅用于没有负担的入门项目
- 没有鉴权(no auth)
- 没有多环境配置(no env profiles)
代码如下
# download the stable release jar
wget http://central.maven.org/maven2/io/swagger/swagger-codegen-cli/2.3.1/swagger-codegen-cli-2.3.1.jar -O swagger-codegen-cli.jar
# run codegen with petstore
java -jar swagger-codegen-cli.jar generate -i http://petstore.swagger.io/v2/swagger.yaml -o gen -l spring --library spring-cloud
Do not use codegen3, it not works now.
高度定制生成代码桩
虽然Swagger看似一键生成了代码,但是生成的内容效果都不是很好,因此建议自己维护一份模板,原因如下
- 在业务中一般会定制Encoder与鉴权插件,此部分仅仅修改模版是不够的
- 由于SpringCloud更新速度很快的原因,如果你使用SpringBoot2.0,那么需要自己定制模版中的import。
具体教程如下
配置Maven plugin
首先在Maven中配置插件,其中插件参数如下
<profiles>
<profile>
<id>swagger-gen</id>
<!-- eg: passed by mvn -Dkey=value -->
<properties>
<url>http://petstore.swagger.io/v2/swagger.yaml</url>
<package>com.github.miao1007</package>
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyyMMdd</maven.build.timestamp.format>
</properties>
<build>
<plugins>
<plugin>
<groupId>io.swagger</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>2.3.1</version>
<executions>
<execution>
<!-- see https://stackoverflow.com/a/3169340/4016014 -->
<id>default-cli</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${url}</inputSpec>
<language>spring</language>
<apiPackage>${package}.api</apiPackage>
<modelPackage>${package}.model</modelPackage>
<groupId>${package}</groupId>
<artifactId>sdk</artifactId>
<artifactVersion>${timestamp}-SNAPSHOTS</artifactVersion>
<invokerPackage>${package}.invoker</invokerPackage>
<templateDirectory>template</templateDirectory>
<configOptions>
<library>spring-cloud</library>
<dateLibrary>java8</dateLibrary>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
注意这里专门开了一个profile,避免与其他打包产生影响。
生成源码(Generate source code)
执行如下
# please replace the url to your own's
mvn -P swagger-gen -Durl=http://petstore.swagger.io/v2/swagger.yaml -f pom.xml
部署到Maven(Deploy to maven)
执行如下
# it won't work now because we havn't configure the template file in the pom
mvn clean deploy -f target/generated-sources/swagger/pom.xml
此处deploy需要一个Nexus私服,这个自己搭
使用桩
在其他需要此项目的pom文件中加入如下
<dependencies>
<dependency>
<groupId>com.github.miao1007</groupId>
<artifactId>sdk</artifactId>
<name>sdk</name>
<!-- you can customize your own timestamp -->
<version>20180724-SNAPSHOTS</version>
</dependency>
</dependencies>
通过上述流程,你的代码生成器应该就可以用了,但是默认模版还有以下问题
- 不支持Eureka的value属性(Do not support Eureka's dynamic naming service),而是hard coding url
- pom过于简单,不支持上传源码(maven-source-plugin)
定制模版
首先在根目录下创建文件夹template
然后,你需要覆盖文件的形式定制,从这里下载需要定制的文件,并放到刚刚的template
目录
# 注意不需要文件夹层次
https://github.com/swagger-api/swagger-codegen/tree/master/modules/swagger-codegen/src/main/resources/JavaSpring
定制apiClient动态模版
举个例子,需要支持基于yaml获取Eureka的name,那么需要进行如下定制,此处path相当于tomcat的contextPath,原版的模版中并不支持
File: template/apiClient.mustache
package {{package}};
import org.springframework.cloud.netflix.feign.FeignClient;
import {{configPackage}}.ClientConfiguration;
{{=<% %>=}}
@FeignClient(name="${<%groupId%>.name}", path="${<%groupId%>.path}")
<%={{ }}=%>
public interface {{classname}}Client extends {{classname}} {
}
然后生成的代码如下
//generated file by mustache
@FeignClient(name="${com.github.miao1007.name}", path="${com.github.miao1007.path}")
public interface PetApiClient extends PetApi {
}
接着我们在客户机的application.yaml
中配置即可
# eureka client config example
io:
github:
miao1007:
sdk:
# eureka's name
name: EUREKA-ORDER-PROD
# tomcat's context path
path: /context
然后就可以像往常一样注入服务即可
@Autowired
private PetApiClient client;
//use
client.queryBy...
如果你想明白底层原理的话,可以看这里
定制POM源码模版
同理,由于默认模版中只上传了jar,导致用户使用时参数可能是var1, var2,这里可以通过配置源码插件实现
File: template/pom.mustache
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>2.1.1</version>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>package</phase>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
以及nexus上传定制
+ <distributionManagement>
+ <repository>
+ <id>releases</id>
+ <!-- your nexus url -->
+ <url>http://127.0.0.1:8081/nexus/content/repositories/releases</url>
+ </repository>
+ <snapshotRepository>
+ <id>snapshots</id>
+ <url>http://127.0.0.1:8081/nexus/content/repositories/snapshots</url>
+ </snapshotRepository>
+ </distributionManagement>
这个不属于本文范畴,可以自行学习
Feign File upload
这个地方简直天坑了
- 首先Swagger生成的代码有问题,没有
@ParamPart
,导致上传无法使用,详见解决办法#8419 - Feign代码写的水平远不如Retrofit/OkHttp优雅,它不支持免配置上传二进制文件,我目前的解决如下
依赖如下
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.3.0</version>
</dependency>
全局配置如下,需要被扫描
// Feign client config
@Configuration
class FeignConfig {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Encoder feignEncoder() {
Encoder dft = new SpringEncoder(this.messageConverters);
Encoder form = new SpringFormEncoder();
return new Encoder(){
public void encode(Object object, Type bodyType, RequestTemplate template) {
if (bodyType == MultipartFile.class) {
form.encode(object, bodyType, template);
} else {
dft.encode(object, bodyType, template);
}
}
};
}
}
为什么不单独在接口中独立写Encoder呢?这样写的问题是,Feign内部的FeignContext使用name作为key,configuration作为value,因此如果你这里定制了不同的configuration,那么相同name下的configuration将被覆盖,详见FactoryBean中实现。
总结与建议
Feign与Swagger的结合可以说是一堆问题,当然网上并没有像Dubbo那么完善的方案,因此需要注意
- 如果使用Controller作为RPC的实现,那么在写Controller时一定不要用Map作为入参出参,这样RPC序列化时将无法使用。我在项目中发现了很多外包这样写,导致后期维护成本较高。再次感慨招人与静态检测的重要性。
- 如果需要鉴权,那么不用桩中支持,而是直接外部全局配置拦截器即可,在服务端的业务代码中也不要加入鉴权相关的
@ApiParams
- 如果使用基于编码TCP的形式进行RPC,那么需要自己定制模版,但是SpringCloud的调用链,日志均等生态就无法使用了