使用Swagger生成Feign桩

在Eureka中,默认通过RestController进行RPC调用,虽然使用比较简单,但是纯手写还是比较麻烦的。目前一般有如下调用方法

RPC StubPROSCONS
HttpClient/OkHttphight customizedmaintains urls, serialization
Feign/RetrofitDynamic Proxy, Interceptorsmaintains source code
Swagger CodegenAuto generated maven jarmaintains 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的调用链,日志均等生态就无法使用了