用Swagger生成接口,pom中少了一个library参数,排查了几个小时

前言:

        我们一般都会使用swagger生成restful接口,这样可以省不少时间,将更多的精力专注于写业务上。但接口也不是经常写,所以,swagger用的也不熟练。尤其是我喜欢拿之前的接口copy一份,然后在此基础上进行修改,一般情况下,都是能跑通的。不巧,这次并没有成功,原因在于我把服务端的接口拿来改成客户端的接口,当我沉浸于各种copy,delete的操作时,也给自己埋了一个大坑。导致我白白浪费了几个小时去找原因,最后发现是pom.xml文件中少了这一句:

 jersey2

为了了解library的流程,我还去大致浏览了swagger-codegen的源码。因此,不想让自己的时间白白地浪费了,特意写下这篇文章,算是当作笔记。

一、错误重现

如果不加library这个属性,编译接口时,会报如下错误:

[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[16,27] package com.squareup.okhttp does not exist
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[17,27] package com.squareup.okhttp does not exist
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[21,12] package okio does not exist
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[22,12] package okio does not exist
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[23,12] package okio does not exist
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[24,12] package okio does not exist
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[25,12] package okio does not exist
[ERROR] /C:/workspace/common/dhcp/api/client/target/generated-sources/swagger/src/gen/java/com/test/dhcp/ProgressRequestBody.java:[27,42] cannot find symbol
  symbol: class RequestBody
...

错误较多,只截取了前面一部分,通过这点错误信息,也足够说明问题了。

上面的错误提示找不到xxx的package,查看生成的源码ProgressRequestBody.java的内容:

package com.test.dhcp;

import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.RequestBody;

import java.io.IOException;

import okio.Buffer;
import okio.BufferedSink;
import okio.ForwardingSink;
import okio.Okio;
import okio.Sink;

public class ProgressRequestBody extends RequestBody {

    public interface ProgressRequestListener {
        void onRequestProgress(long bytesWritten, long contentLength, boolean done);
    }

    private final RequestBody requestBody;

    private final ProgressRequestListener progressListener;

    public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) {
        this.requestBody = requestBody;
        this.progressListener = progressListener;
    }

    @Override
    public MediaType contentType() {
        return requestBody.contentType();
    }

    @Override
    public long contentLength() throws IOException {
        return requestBody.contentLength();
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink bufferedSink = Okio.buffer(sink(sink));
        requestBody.writeTo(bufferedSink);
        bufferedSink.flush();
    }

    private Sink sink(Sink sink) {
        return new ForwardingSink(sink) {

            long bytesWritten = 0L;
            long contentLength = 0L;

            @Override
            public void write(Buffer source, long byteCount) throws IOException {
                super.write(source, byteCount);
                if (contentLength == 0) {
                    contentLength = contentLength();
                }

                bytesWritten += byteCount;
                progressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
            }
        };
    }
}

从源码中可看到,ProgressRequestBody.java确实引入了com.squareup.okhttp和okio,pom.xml里面的依赖包如下:



        
            ${project.groupId}
            dhcp-api-model
        

        
            io.swagger
            swagger-annotations
        

        
        
            org.glassfish.jersey.core
            jersey-client
        
        
            org.glassfish.jersey.media
            jersey-media-json-jackson
        
        
            org.glassfish.jersey.media
            jersey-media-multipart
        

        
        
            com.fasterxml.jackson.jaxrs
            jackson-jaxrs-base
        
        
            com.fasterxml.jackson.core
            jackson-core
        
        
            com.fasterxml.jackson.core
            jackson-annotations
        
        
            com.fasterxml.jackson.core
            jackson-databind
        
        
            com.fasterxml.jackson.jaxrs
            jackson-jaxrs-json-provider
        

        
            com.github.joschi.jackson
            jackson-datatype-threetenbp
        

        
            com.brsanthu
            migbase64
        

    

确实没有引入报错的依赖包OkHttp,所以必然报错。但之前的接口文件,也是这些依赖,为什么他们就能编译通过,也不依赖这个包呢,就此,我开始了爬坑之路。

二、爬坑过程

        既然之前的接口文件编译都没有问题,那么,肯定是我修改的配置文件有问题了,就从这里开始入手,一步步排查。中间做了很多无用功,就不再描述了,总之,一直忽略了细节,找错了方向。最后拿之前的客户端接口文件和当前修改的文件进行一一对比,折腾了半天才发现是少了library那一行,真是一个粗心,在小小的配置文件里,挖了一个大大的坑,白白在坑里耗了那么长时间。为了一探究竟,加深印象,所以就浏览了下swagger-codegen的源码。

三、阅读源码

        根据pom.xml中配置的plugin,找到swagger-codegen的子模块:swagger-codegen-maven-plugin,该模块只有一个源码,如下图所示:

用Swagger生成接口,pom中少了一个library参数,排查了几个小时_第1张图片

 CodeGenMojo.java的执行入口是execute()方法,我们就从该方法进入,以下是源码,不相关的代码都被删掉了:

@Override
    public void execute() throws MojoExecutionException {
        // Using the naive approach for achieving thread safety
        synchronized (CodeGenMojo.class) {
            execute_();
        }
    }

    protected void execute_() throws MojoExecutionException {

        if (skip) {
            getLog().info("Code generation is skipped.");
            // Even when no new sources are generated, the existing ones should
            // still be compiled if needed.
            addCompileSourceRootIfConfigured();
            return;
        }

        // attempt to read from config file
        CodegenConfigurator configurator = CodegenConfigurator.fromFile(configurationFile);

        // if a config file wasn't specified or we were unable to read it
        if (configurator == null) {
            configurator = new CodegenConfigurator();
        }

        ......

        if (isNotEmpty(library)) {
            configurator.setLibrary(library);
        }

        ......

        final ClientOptInput input = configurator.toClientOptInput();
        final CodegenConfig config = input.getConfig();


        try {
            new DefaultGenerator().opts(input).generate();
        } catch (Exception e) {
            // Maven logs exceptions thrown by plugins only if invoked with -e
            // I find it annoying to jump through hoops to get basic diagnostic information,
            // so let's log it in any case:
            getLog().error(e);
            throw new MojoExecutionException(
                    "Code generation failed. See above for the full exception.");
        }

        addCompileSourceRootIfConfigured();
    }

从上面的源码可知,如果library属性不为空,则将其赋值给configurator对象:

configurator.setLibrary(library);

configurator对象初始化过程:

        // attempt to read from config file
        CodegenConfigurator configurator = CodegenConfigurator.fromFile(configurationFile);

        // if a config file wasn't specified or we were unable to read it
        if (configurator == null) {
            configurator = new CodegenConfigurator();
        }

因为并未指定configurationFile,所以library的值被赋值给CodegenConfigurator创建的configurator对象。

继续往下执行代码:

        final ClientOptInput input = configurator.toClientOptInput();
        final CodegenConfig config = input.getConfig();

这两行代码,是将CodegenConfigurator创建的configurator对象中的配置参数都赋值给CodegenConfig对象config。

继续往下执行,这行代码就是生成源码的入口了:

new DefaultGenerator().opts(input).generate();

我们先看下DefaultGenerator.opts()方法:

    public Generator opts(ClientOptInput opts) {
        this.opts = opts;
        this.swagger = opts.getSwagger();
        this.config = opts.getConfig();
        this.config.additionalProperties().putAll(opts.getOpts().getProperties());

        String ignoreFileLocation = this.config.getIgnoreFilePathOverride();
        if (ignoreFileLocation != null) {
            final File ignoreFile = new File(ignoreFileLocation);
            if (ignoreFile.exists() && ignoreFile.canRead()) {
                this.ignoreProcessor = new CodegenIgnoreProcessor(ignoreFile);
            } else {
                LOGGER.warn("Ignore file specified at {} is not valid. This will fall back to an existing ignore file if present in the output directory.", ignoreFileLocation);
            }
        }

        if (this.ignoreProcessor == null) {
            this.ignoreProcessor = new CodegenIgnoreProcessor(this.config.getOutputDir());
        }

        return this;
    }

以上代码主要是赋值和初始化,接着跟踪DefaultGenerator.generate()方法:

    public List generate() {

        if (swagger == null || config == null) {
            throw new RuntimeException("missing swagger input or config!");
        }
        configureGeneratorProperties();
        configureSwaggerInfo();

        // resolve inline models
        InlineModelResolver inlineModelResolver = new InlineModelResolver();
        inlineModelResolver.flatten(swagger);

        List files = new ArrayList();
        // models
        List allModels = new ArrayList();
        generateModels(files, allModels);
        // apis
        List allOperations = new ArrayList();
        generateApis(files, allOperations, allModels);

        // supporting files
        Map bundle = buildSupportFileBundle(allOperations, allModels);
        generateSupportingFiles(files, bundle);
        config.processSwagger(swagger);
        return files;
    } 
  

上面的代码,就是生成model和apis的地方,且各自单独用一个方法实现。这里我们就不跟踪models的生成方法了,仅跟踪apis的生成方法generateApis():

protected void generateApis(List files, List allOperations, List allModels) {
        if (!isGenerateApis) {
            return;
        }
        Map> paths = processPaths(swagger.getPaths());
        Set apisToGenerate = null;
        String apiNames = System.getProperty("apis");
        if (apiNames != null && !apiNames.isEmpty()) {
            apisToGenerate = new HashSet(Arrays.asList(apiNames.split(",")));
        }
        if (apisToGenerate != null && !apisToGenerate.isEmpty()) {
            Map> updatedPaths = new TreeMap>();
            for (String m : paths.keySet()) {
                if (apisToGenerate.contains(m)) {
                    updatedPaths.put(m, paths.get(m));
                }
            }
            paths = updatedPaths;
        }
        for (String tag : paths.keySet()) {
            try {
                List ops = paths.get(tag);
                Collections.sort(ops, new Comparator() {
                    @Override
                    public int compare(CodegenOperation one, CodegenOperation another) {
                        return ObjectUtils.compare(one.operationId, another.operationId);
                    }
                });
                Map operation = processOperations(config, tag, ops, allModels);

                operation.put("hostWithoutBasePath", getHostWithoutBasePath());
                operation.put("basePath", basePath);
                operation.put("basePathWithoutHost", basePathWithoutHost);
                operation.put("contextPath", contextPath);
                operation.put("baseName", tag);
                operation.put("apiPackage", config.apiPackage());
                operation.put("modelPackage", config.modelPackage());
                operation.putAll(config.additionalProperties());
                operation.put("classname", config.toApiName(tag));
                operation.put("classVarName", config.toApiVarName(tag));
                operation.put("importPath", config.toApiImport(tag));
                operation.put("classFilename", config.toApiFilename(tag));

                if (!config.vendorExtensions().isEmpty()) {
                    operation.put("vendorExtensions", config.vendorExtensions());
                }

                // Pass sortParamsByRequiredFlag through to the Mustache template...
                boolean sortParamsByRequiredFlag = true;
                if (this.config.additionalProperties().containsKey(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG)) {
                    sortParamsByRequiredFlag = Boolean.valueOf(this.config.additionalProperties().get(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG).toString());
                }
                operation.put("sortParamsByRequiredFlag", sortParamsByRequiredFlag);

                processMimeTypes(swagger.getConsumes(), operation, "consumes");
                processMimeTypes(swagger.getProduces(), operation, "produces");

                allOperations.add(new HashMap(operation));
                for (int i = 0; i < allOperations.size(); i++) {
                    Map oo = (Map) allOperations.get(i);
                    if (i < (allOperations.size() - 1)) {
                        oo.put("hasMore", "true");
                    }
                }

                for (String templateName : config.apiTemplateFiles().keySet()) {
                    String filename = config.apiFilename(templateName, tag);
                    if (!config.shouldOverwrite(filename) && new File(filename).exists()) {
                        LOGGER.info("Skipped overwriting " + filename);
                        continue;
                    }

                    File written = processTemplateToFile(operation, templateName, filename);
                    if (written != null) {
                        files.add(written);
                    }
                }

                if(isGenerateApiTests) {
                    // to generate api test files
                    for (String templateName : config.apiTestTemplateFiles().keySet()) {
                        String filename = config.apiTestFilename(templateName, tag);
                        // do not overwrite test file that already exists
                        if (new File(filename).exists()) {
                            LOGGER.info("File exists. Skipped overwriting " + filename);
                            continue;
                        }

                        File written = processTemplateToFile(operation, templateName, filename);
                        if (written != null) {
                            files.add(written);
                        }
                    }
                }


                if(isGenerateApiDocumentation) {
                    // to generate api documentation files
                    for (String templateName : config.apiDocTemplateFiles().keySet()) {
                        String filename = config.apiDocFilename(templateName, tag);
                        if (!config.shouldOverwrite(filename) && new File(filename).exists()) {
                            LOGGER.info("Skipped overwriting " + filename);
                            continue;
                        }

                        File written = processTemplateToFile(operation, templateName, filename);
                        if (written != null) {
                            files.add(written);
                        }
                    }
                }

            } catch (Exception e) {
                throw new RuntimeException("Could not generate api file for '" + tag + "'", e);
            }
        }
        if (System.getProperty("debugOperations") != null) {
            LOGGER.info("############ Operation info ############");
            Json.prettyPrint(allOperations);
        }

    } 
  

上面是完整的代码,我们需要重点关注下面这一段:



                for (String templateName : config.apiTemplateFiles().keySet()) {
                    String filename = config.apiFilename(templateName, tag);
                    if (!config.shouldOverwrite(filename) && new File(filename).exists()) {
                        LOGGER.info("Skipped overwriting " + filename);
                        continue;
                    }

                    File written = processTemplateToFile(operation, templateName, filename);
                    if (written != null) {
                        files.add(written);
                    }
                }

注意看,for循环里面就是根据名字去获取模板路径,然后将路径传递给processTemplateToFile()方法,跟踪代码:

    protected File processTemplateToFile(Map templateData, String templateName, String outputFilename) throws IOException {
        String adjustedOutputFilename = outputFilename.replaceAll("//", "/").replace('/', File.separatorChar);
        if (ignoreProcessor.allowsFile(new File(adjustedOutputFilename))) {
            String templateFile = getFullTemplateFile(config, templateName);
            String template = readTemplate(templateFile);
            Mustache.Compiler compiler = Mustache.compiler();
            compiler = config.processCompiler(compiler);
            Template tmpl = compiler
                    .withLoader(new Mustache.TemplateLoader() {
                        @Override
                        public Reader getTemplate(String name) {
                            return getTemplateReader(getFullTemplateFile(config, name + ".mustache"));
                        }
                    })
                    .defaultValue("")
                    .compile(template);

            writeToFile(adjustedOutputFilename, tmpl.execute(templateData));
            return new File(adjustedOutputFilename);
        }

        LOGGER.info("Skipped generation of " + adjustedOutputFilename + " due to rule in .swagger-codegen-ignore");
        return null;
    }

继续跟踪上述方法中的getFullTemplateFile()方法:

    /**
     * Get the template file path with template dir prepended, and use the
     * library template if exists.
     *
     * @param config Codegen config
     * @param templateFile Template file
     * @return String Full template file path
     */
    public String getFullTemplateFile(CodegenConfig config, String templateFile) {
        //1st the code will check if there's a