一、简介
??OpenAPI规范(OAS)为RESTful API定义了一个与语言无关的标准接口,使人类和计算机都可以发现和理解服务的功能,而无需访问源代码,文档或通过网络流量检查。Swagger CodeGen是一个REST 客户端生成工具,它可以从OpenAPI的规范定义文件中生成对应的REST Client代码。Spring Boot中使用Swagger CodeGen生成REST Client,然后打包发布后,即可供其他服务进行调用了。程序中处理的大致流程如下:
- 获取指定服务的swagger文档定义(即得到api.json)
- 通过swagger-codegen-cli的generate命令生成接口调用
- 根据插件org.apache.maven.plugins:maven-dependency-plugin获取依赖关系
- 发布到私服,比如nexus
- 引用依赖并发起调用
二、Swagger Codegen
2.1、swagger-codegen-cli
??swagger-codegen-cli使用语法及说明如下:
java -jar SwaggerJarPath generate
--library LIBRARY
-l LANGUAGE
-i INPUTSPEC
--output OUTPUT-DIR
--api-package API-PACkAGE
--model-package MODEL-PACKAGE
--group-id GROUP-ID
--artifact-id ARTIFACT-ID
--artifact-version ARTIFACT-VERSION
-t TEMPLATE-DIR
--import-mappings org.threeten.bp.OffsetDateTime=java.util.Date
--type-mappings OffsetDateTime=Date
- –library:指定了实际的实现框架,比如resttemplate
- -l :指定生成代码的变成语言,比如java
- -i:指定了open api 定义文件的地址
- –output:指定生成文件的目录
- –api-package:指定api接口生成的目录,比如:cn.alian.stock.api
- –model-package:指定model生成的目录,比如:cn.alian.stock.model
- –group-id:指定生成的maven项目的group-id,比如:cn.alian.mall
- –artifact-id:指定生成的maven项目的artifact-id,比如:stock
- –artifact-version:指定生成的maven项目的version,比如:1.0.0-SNAPSHOT
- -t:指定生成文件使用到的模板文件
- –import-mappings:指定类型使用指定的方式进行处理
- –type-mappings:指定swagger中规范类型和生成代码类型中的映射
??更多内容可以通过命令查看:
java -jar swagger-codegen-cli.jar help generate
2.2、获取依赖关系
mvn org.apache.maven.plugins:maven-dependency-plugin:3.2.0:get
-Dartifact=ARCHETYPE-GROUP-ID:ARCHETYPE-ARTIFACT-ID:ARCHETYPE-VERSION:POM
-DremoteRepositories=REMOTE-REPOSITORIES
上面的语法有四变量如下:
- ARCHETYPE-GROUP-ID
- ARCHETYPE-ARTIFACT-ID
- ARCHETYPE-VERSION
- REMOTE-REPOSITORIES
??使用示例如下:
mvn org.apache.maven.plugins:maven-dependency-plugin:RELEASE:get
-Dartifact=cn.alian.microservice:alian-archetype-project:1.0.0-SNAPSHOT
-DremoteRepositories=http://192.168.0.210:8081/nexus/content/groups/public
2.3、发布到私服
mvn -f OUTPUTAPIDIR clean source:jar deploy -Dmaven.test.skip=true"
??其中OUTPUTAPIDIR 就是要打包发布的目录
三、项目整合
??其实有上述三个命令,我们就可以通过程序来实现,当然你手动调用或者通过bat也是没有问题的。
??继续改进我们的文档中心服务,增加生成API调用的接口,数据库配置如下:
3.1、属性配置
AppProperties.java
package cn.alian.microservice.doc.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
@Data
@RefreshScope
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String mvn;
private String swaggerTemplatePath;
private String swaggerJarPath;
private String configUri;
private String remoteRepositories;
private String tmpDir;
private Gav domain;
private Gav project;
@Data
public static class Gav{
private String groupId;
private String artifactId;
private String version;
private String packaging;
}
}
??具体的配置值,在我上面的截图里都有。不要后面用到的时候,这个参数是什么,这些都是在配置中心配置的。
3.2、控制层
GenerateApiController.java
@Slf4j
@RequestMapping("api/v1/client")
@Api(description = "生成调用客户端")
@RestController
public class GenerateApiController {
@Autowired
private GenerateApiService generateApiService;
@ApiOperation(value = "生成web调用客户端", notes = "生成web调用客户端")
@GetMapping(value = "/generateWebApi")
public synchronized Object generateWebApi(
@ApiParam(value = "应用接口的json文档地址", example = "http://10.130.3.222:9999/doc-service/service/stock-10.130.3.88-8001") @RequestParam() @NotBlank String appDocUrl,
@ApiParam(value = "生成api客户端类型,objc、android、typescript-fetch", example = "typescript-fetch") @RequestParam() @NotBlank String apiType,
@ApiParam(value = "调用类型,1:直连服务调用,2:经网关调用,在映射上添加应用名") @RequestParam @NotNull @Pattern(regexp = "1 | 2") String invokeType) throws Exception {
if (StringUtils.isBlank(appDocUrl)) {
return ApiResponseDto.fail("应用接口的json文档地址不能为空");
}
if (!ArrayUtils.contains(new String[]{"objc", "android", "typescript-fetch"}, apiType)) {
return ApiResponseDto.fail("暂只支持objc、android、typescript-fetch三种方式");
}
return generateApiService.generateWebApi(appDocUrl, apiType, invokeType);
}
@ApiOperation(value = "生成java调用客户端", notes = "生成java调用客户端")
@GetMapping(value = "/generateJavaApi")
public synchronized Object generateJavaApi(
@ApiParam(value = "应用接口的json文档地址", example = "http://10.130.3.222:9999/doc-service/service/stock-10.130.3.88-8001") @RequestParam @NotBlank String appDocUrl,
@ApiParam(value = "生成api客户端类型,spring-cloud、resttemplate,默认spring-cloud", example = "spring-cloud") @RequestParam @NotBlank String apiType) throws Exception {
if (StringUtils.isBlank(appDocUrl)) {
return ApiResponseDto.fail("应用接口的json文档地址不能为空");
}
if (!ArrayUtils.contains(new String[]{"spring-cloud", "resttemplate"}, apiType)) {
return ApiResponseDto.fail("暂只支持spring-cloud、resttemplate两种方式");
}
Pair<Boolean, String> pair = generateApiService.generateJavaApi(appDocUrl, apiType);
if (pair.getLeft()) {
return ApiResponseDto.success(pair.getRight());
}
return ApiResponseDto.fail(pair.getRight());
}
}
??这里提供了两个接口,一个生成web端调用的接口,一个生成java端调用的接口。这两个接口差不多,但是为了区分,我就没有合并为一个了,这样大家更加容易看懂,主要参数有:
- swagger应用接口的json文档地址
- 需要生成api客户端类型,比如spring-cloud、resttemplate或者其他
- 调用的方式,直接调用,还是经网关调用
3.3、生成api的服务层
GenerateApiService.java
@Slf4j
@Service
public class GenerateApiService extends BaseService {
@Autowired
private AppProperties appProperties;
public Object generateWebApi(String appDocUrl, String apiType, String invokeType) {
try {
String json = getSwaggerByAppDocUrl(appDocUrl);
Swagger swagger = JSON.parseObject(json, Swagger.class);
if (swagger == null) {
return ApiResponseDto.fail("未获取到文档定义内容");
}
Swagger.Info info = swagger.getInfo();
if ("01".equals(invokeType)) {
JSONObject jsonObject = JSON.parseObject(json, Feature.DisableSpecialKeyDetect);
JSONObject paths = (JSONObject) jsonObject.remove("paths");
Map<String, Object> newPaths = paths.entrySet().stream().map(e -> Pair.of("/" + info.getAppName() + e.getKey(), e.getValue())).collect(Collectors.toMap(Pair::getLeft, Pair::getValue));
JSONObject pathJson = new JSONObject(newPaths);
jsonObject.put("paths", pathJson);
json = JSON.toJSONString(jsonObject);
log.info("更新后的 json is {}", json);
}
String outputDir = appProperties.getTmpDir() + File.separator + "webApi" + System.currentTimeMillis();
log.info("webApi接口生成目录:{}", outputDir);
String outputJson = outputDir + File.separator + "api.json";
log.info("api.json生成目录:{}", outputJson);
FileUtils.writeStringToFile(new File(outputJson), json, StandardCharsets.UTF_8);
String templateDir = this.appProperties.getSwaggerTemplatePath() + File.separator + apiType;
log.info("swagger模板目录:{}", templateDir);
String outputApiDir = outputDir + File.separator + info.getArtifactId() + "-api";
log.info("压缩包生成的目录:{}", outputApiDir);
String cmd = CmdUtil.getSwaggerCodegenCliCmd(appProperties.getSwaggerJarPath(),false, apiType, outputApiDir, outputJson, templateDir, info);
Process process = Runtime.getRuntime().exec(cmd);
boolean generateApi = printProcess(process);
if (!generateApi) {
return ApiResponseDto.fail("生成webapi失败");
}
return generateZipReponse(outputApiDir + ".zip", info.getArtifactId() + "-api-" + info.getVersion(), outputApiDir);
} catch (Exception e) {
e.printStackTrace();
return ApiResponseDto.fail("生成webapi异常");
}
}
public Pair<Boolean, String> generateJavaApi(String appDocUrl, String apiType) {
try {
String json = getSwaggerByAppDocUrl(appDocUrl);
Swagger swagger = JSON.parseObject(json, Swagger.class);
if (swagger == null) {
return Pair.of(false, "未获取到文档定义内容");
}
Swagger.Info info = swagger.getInfo();
String outputDir = appProperties.getTmpDir() + File.separator + "javaApi" + System.currentTimeMillis();
log.info("javaApi接口生成目录:{}", outputDir);
String outputJson = outputDir + File.separator + "api.json";
log.info("api.json生成目录:{}", outputJson);
FileUtils.writeStringToFile(new File(outputJson), json, StandardCharsets.UTF_8);
String templateDir = this.appProperties.getSwaggerTemplatePath() + File.separator + apiType;
log.info("swagger模板目录:{}", templateDir);
String outputApiDir = outputDir + File.separator + info.getArtifactId() + "-" + GlobalConstant.immutableMap.get(apiType) + "-api";
log.info("webApi压缩包生成的目录:{}", outputApiDir);
String cmd = CmdUtil.getSwaggerCodegenCliCmd(appProperties.getSwaggerJarPath(),true, apiType, outputApiDir, outputJson, templateDir, info);
Process process = Runtime.getRuntime().exec(cmd);
boolean generateApi = printProcess(process);
if (!generateApi) {
return Pair.of(false, "生成webapi失败");
}
String pack = info.getPack();
if ("spring-cloud".equals(apiType)) {
String mainDir = outputApiDir + File.separator + "src" + File.separator + "main";
log.info("spring-cloud主目录:{}", mainDir);
FileUtils.deleteDirectory(new File(mainDir + File.separator + "java" + File.separator + "io"));
List<Swagger.Tag> tags = swagger.getTags();
String finalPack = pack;
String clients = tags.stream().map(t -> finalPack + ".api." + CaseUtils.toCamelCase(t.getName(), true, new char[]{'-'}) + "ApiClient").collect(Collectors.joining(","));
FileUtils.writeStringToFile(new File(mainDir + File.separator + "resources" + File.separator + "META-INF" + File.separator + "spring.factories"), "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + clients, StandardCharsets.UTF_8);
} else if ("resttemplate".equals(apiType)) {
String mainDir = outputApiDir + File.separator + "src" + File.separator + "main";
log.info("resttemplate主目录:{}", mainDir);
FileUtils.writeStringToFile(new File(mainDir + File.separator + "java" + File.separator + pack.replace(".", File.separator) + File.separator + "Config.java"), "package " + pack + ";\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.client.RestTemplate;@Configuration\n@ComponentScan(\"" + pack + "\")\npublic class Config {\n @Bean\n @ConditionalOnMissingBean\n public RestTemplate restTemplate(){\n return new RestTemplate();\n }}\n", StandardCharsets.UTF_8);
FileUtils.writeStringToFile(new File(mainDir + File.separator + "resources" + File.separator + "META-INF" + File.separator + "spring.factories"), "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + pack + ".Config", StandardCharsets.UTF_8);
}
AppProperties.Gav parentGav = new AppProperties.Gav();
parentGav.setGroupId("cn.alian.microservice");
parentGav.setArtifactId("parent");
parentGav.setVersion("1.0.0-SNAPSHOT");
parentGav.setPackaging("pom");
String mvnDependencyCmd = CmdUtil.getMvnDependencyCmd(appProperties.getMvn(), appProperties.getRemoteRepositories(), parentGav);
Process dependencyProcess = Runtime.getRuntime().exec(mvnDependencyCmd);
printProcess(dependencyProcess);
String mvnCmd = this.appProperties.getMvn() + " -f " + outputApiDir + " clean source:jar deploy -Dmaven.test.skip=true";
log.info("发布的命令是:{}", mvnCmd);
Process generateProcess = Runtime.getRuntime().exec(mvnCmd);
boolean deploy = printProcess(generateProcess);
if (!deploy) {
return Pair.of(false, "生成javaApi客户端失败,请检查swagger定义");
}
String dependency = "<dependency><groupId>" + info.getGroupId() + "</groupId><artifactId>" + info.getArtifactId() + "-api</artifactId><version>" + info.getVersion() + "</version></dependency>";
return Pair.of(true, "生成javaApi客户端的依赖是 :" + dependency);
} catch (Exception e) {
return Pair.of(false, "生成javaApi客户端异常");
}
}
}
3.4、公共服务类
BaseService.java
@Slf4j
@Service
public class BaseService {
@Autowired
private RestTemplate restTemplate;
public String getSwaggerByAppDocUrl(String appDocUrl) {
ResponseEntity<String> responseEntity = this.restTemplate.getForEntity(appDocUrl, String.class, new Object[0]);
String json = responseEntity.getBody();
log.info("json is {}", json);
return json;
}
public boolean printProcess(Process process) throws Exception {
log.info("打印进程信息");
PrintProcessInfoThread errorStreamThread = new PrintProcessInfoThread(process.getErrorStream(), "ERRORSTREAM");
PrintProcessInfoThread outputStreamThread = new PrintProcessInfoThread(process.getInputStream(), "OUTPUTSTREAM");
errorStreamThread.start();
outputStreamThread.start();
int exitVal = process.waitFor();
log.info("执行的结果: " + exitVal);
return errorStreamThread.getBuild() && outputStreamThread.getBuild();
}
public ResponseEntity generateZipReponse(String outputZip, String fileName, String... dirsAddToZip) throws Exception {
log.info("zip");
ZipFile zipFile = new ZipFile(outputZip);
ZipParameters parameters = new ZipParameters();
parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);
for (String dir : dirsAddToZip) {
zipFile.addFolder(dir, parameters);
}
File file = zipFile.getFile();
HttpHeaders header = new HttpHeaders();
header.set("Content-Disposition", "attachment; filename=" + fileName + ".zip");
header.setContentLength(file.length());
header.setContentType(MediaType.parseMediaType("application/octet-stream"));
InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
return new ResponseEntity(isr, header, HttpStatus.OK);
}
}
??这里都是公共的方法,通过appDocUrl获取swagger文档定义,打印或者返回结果。特别需要注意的是打印的方法,如果不读取流信息,那么执行的命令就会一直堵住,也就是卡主了,真正只有读取错误流输出才会完整的输出。我这里采用了线程输出的方式,应该算是比较好的一个方式了。
3.5、进程信息打印线程
PrintProcessInfoThread.java
package cn.alian.microservice.doc.thread;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
@Slf4j
public class PrintProcessInfoThread extends Thread {
private InputStream inputStream;
private String streamType;
private boolean build = true;
public PrintProcessInfoThread(InputStream inputStream, String streamType) {
this.inputStream = inputStream;
this.streamType = streamType;
}
public void run() {
try {
StringBuilder sb = new StringBuilder();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
log.info("{}>>>{}", streamType, line);
sb.append(line).append(File.separatorChar);
}
String message = sb.toString();
if (StringUtils.isNotBlank(message)) {
build = !message.contains("BUILD FAILURE") || !message.contains("[ERROR]");
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
public boolean getBuild(){
return this.build;
}
}
3.6、工具类
GlobalConstant.java
public class GlobalConstant {
public static ImmutableMap<Object, Object> langMap = ImmutableMap.builder()
.put("resttemplate", "java")
.put("spring-cloud", "spring")
.build();
public static ImmutableMap<Object, Object> immutableMap = ImmutableMap.builder()
.put("resttemplate", "rt")
.put("spring-cloud", "sc")
.build();
}
CmdUtil.java
@Slf4j
public class CmdUtil {
public static String getMvnDependencyCmd(String mvnPath, String remoteRepositories, AppProperties.Gav gav) {
String cmd = "MVNCMD org.apache.maven.plugins:maven-dependency-plugin:3.2.0:get -Dartifact=ARCHETYPE-GROUP-ID:ARCHETYPE-ARTIFACT-ID:ARCHETYPE-VERSION:POM -DremoteRepositories=REMOTE-REPOSITORIES";
cmd = cmd.replace("MVNCMD", mvnPath)
.replace("ARCHETYPE-GROUP-ID", gav.getGroupId())
.replace("ARCHETYPE-ARTIFACT-ID", gav.getArtifactId())
.replace("ARCHETYPE-VERSION", gav.getVersion())
.replace(":POM", StringUtils.isNotBlank(gav.getPackaging()) ? (":" + gav.getPackaging()) : "")
.replace("REMOTE-REPOSITORIES", remoteRepositories);
log.info("getMvnDependency cmd is: {}", cmd);
return cmd;
}
public static String getSwaggerCodegenCliCmd(String swaggerJarPath, boolean javaApi, String apiType, String outputApiDir, String outputJson, String templateDir, Swagger.Info info) {
String cmd = "java -jar SwaggerJarPath generate --library -l LANGUAGE -i INPUTSPEC --output OUTPUT-DIR" +
" --api-package API-PACkAGE --model-package MODEL-PACKAGE --group-id GROUP-ID --artifact-id ARTIFACT-ID --artifact-version ARTIFACT-VERSION " +
" -t TEMPLATE-DIR --import-mappings org.threeten.bp.OffsetDateTime=java.util.Date --type-mappings OffsetDateTime=Date";
cmd = cmd.replace("SwaggerJarPath", swaggerJarPath)
.replace("--library", (javaApi ? ("--library " + apiType) : ""))
.replace("LANGUAGE", (javaApi ? "" + GlobalConstant.langMap.get(apiType) : apiType))
.replace("INPUTSPEC", outputJson)
.replace("OUTPUT-DIR", outputApiDir)
.replace("API-PACkAGE", info.getPack() + ".api")
.replace("MODEL-PACKAGE", info.getPack() + ".api.dto")
.replace("GROUP-ID", info.getGroupId())
.replace("ARTIFACT-ID", (javaApi ? (info.getArtifactId() + "-" + GlobalConstant.immutableMap.get(apiType) + "-api") : info.getArtifactId()))
.replace("ARTIFACT-VERSION", info.getVersion())
.replace("TEMPLATE-DIR", templateDir);
log.info("getSwaggerCodegenCliCmd is: {}", cmd);
return cmd;
}
public static String getGenerateProjectCmd(String mvnPath, String groupId, String artifactId, String version, AppProperties.Gav gav, String outputDir, String configUri) {
log.info("generate groupId = [" + groupId + "], artifactId = [" + artifactId + "], version = [" + version + "], gav = [" + gav + "]");
String command = "MVNCMD archetype:generate -DarchetypeGroupId=ARCHETYPE-GROUP-ID -DarchetypeArtifactId=ARCHETYPE-ARTIFACT-ID -DarchetypeVersion=ARCHETYPE-VERSION -DinteractiveMode=false -DarchetypeCatalog=local -DgroupId=PROJECT-GROUP-ID -DartifactId=PROJECT-ARTIFACT-ID -Dversion=PROJECT-VERSION -Dpackage=PROJECT-PACKAGE -Dapp=PROJECT-APP -DconfigUri=CONFIG-URI -DoutputDirectory=OUTPUTDIRECTORY";
command = command.replace("MVNCMD", mvnPath)
.replace("ARCHETYPE-GROUP-ID", gav.getGroupId())
.replace("ARCHETYPE-ARTIFACT-ID", gav.getArtifactId())
.replace("ARCHETYPE-VERSION", gav.getVersion())
.replace("PROJECT-GROUP-ID", groupId)
.replace("PROJECT-ARTIFACT-ID", artifactId)
.replace("PROJECT-VERSION", version)
.replace("PROJECT-PACKAGE", groupId + "." + artifactId)
.replace("PROJECT-APP", StringUtils.capitalize(artifactId))
.replace("CONFIG-URI", configUri)
.replace("OUTPUTDIRECTORY", outputDir);
log.info("getGenerateProjectCmd is {}", command);
return command;
}
}
??把本章第二大点看懂了,转化为程序就容易懂了,现在开始重新启动我们的文档服务,准备验证。
四、验证
4.1、文档服务中心
我们需要通过网关进入文档中心:http://10.130.3.222:8888/gateway/doc-service/swagger-ui.html
可以看到库存系统appDocUrl为:http://10.130.3.222:8888/gateway/doc-service/service/stock-10.130.3.88-8001
4.2、生成web客户端调用
输入参数:
- apiType: spring-cloud
- appDocUrl: http://10.130.3.222:8888/gateway/doc-service/service/stock-10.130.3.88-8001
- invokeTye: 2
??执行后结果如下: ??前端只需要把生成的typescript文件拷贝及可以实现调用,是不是很方便?
4.3、生成java客户端调用
输入参数:
- apiType: spring-cloud
- appDocUrl: http://10.130.3.222:8888/gateway/doc-service/service/stock-10.130.3.88-8001
??我们生成了接口调用并且自动发布到了私服,直接引用依赖即可
<dependency>
<groupId>cn.alian.mall</groupId>
<artifactId>stock-sc-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
??如果你这里希望使用resttemplate方式调用,也很简单,就只要把参数apiType: spring-cloud改成apiType: resttemplate,就会生成新的接口调用,并且发布到私服,你只需要引用如下依赖即可
<dependency>
<groupId>cn.alian.mall</groupId>
<artifactId>stock-rs-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
??至于使用哪种方式,你可以根据你的需求或者网络环境去决定,一个是直接调用,一个是经过网关调用。既然接口调用都生成了,我们去订单服务去调用我们的库存服务试试吧。
|