dubbo-springfox 實現Dubbo接口文檔動態生成與圖形化調用

https://github.com/zhaojigang/dubbo-springfox
  • 一、dubbo-springfox特性簡介
  • 二、dubbo-springfox入門案例
  • 三、dubbo-springfox基本原理
  • 附1、dubbo-springfox注解附表
  • 附2、Q&A

一、dubbo-springfox特性簡介

dubbo-springfox的兩個主要特點:
(1)動態文檔
(2)dubbo接口的圖形化調用

1.1 動態文檔

dubbo-springfox基于SpringFox進行開發,將文檔以注解的形式優雅的集成到代碼中;而且,當接口參數發生變化時,也會在UI界面上動態的進行同步。

1.2 dubbo接口的圖形化調用

dubbo-springfox在服務啟動的時候將<dubbo:service>接口掃描出來,并且動態生成對應的Http接口,并且以漂亮的格式展示在一個UI界面上(包括文檔說明),開發人員與測試人員可以非常方便的進行接口測試。

說明:由于dubbo-springfox是基于SpringFox開發的,所以也天然的支持SpringMVC接口文檔的動態生成與圖形化調用。

二、dubbo-springfox入門案例

項目以maven多模塊形式進行組織:

  • dubbo-springfox-core是核心代碼,提供jar包,供使用;
  • dubbo-springfox-demo-springboot提供了在使用springboot的情況下怎樣使用dubbo-springfox;
  • dubbo-springfox-demo-springmvc提供了在使用springmvc的情況下怎樣使用dubbo-springfox。

以dubbo-springfox-demo-springboot為例來看dubbo-springfox的使用姿勢。

2.1 構建增強版的springfox到nexus或者本地maven倉庫

dubbo-springfox基于增強版的springfox:https://github.com/zhaojigang/springfox。所以需要首先編譯發布增強版的springfox(注意:需要將版本改為6.6.6)
增強版的springfox,相較于原本的springfox:

  • 添加了自定義注解@RequestModel(用來打破@RequestBody單體限制)
  • 讓springfox內核去支持@RequestModel注解
  • 獲取swagger-ui的位置改成了:https://github.com/zhaojigang/swagger-ui,增強版的swagger-ui支持@RequestModel注解的參數解析和構造,支持axios進行ajax調用

可以從 這里 直接獲取的springfox的相關jar包,之后上傳到nexus或者本地maven倉庫即可。

2.2 引入dubbo-springfox jar包

        <dependency>
            <groupId>io.hulk</groupId>
            <artifactId>dubbo-springfox-core</artifactId>
            <version>1.0.0</version>
        </dependency>

2.3 在啟動類中添加啟動注解與掃描范圍

@SpringBootApplication
@EnableSwagger2
@ImportResource({ "classpath:*.xml" })
@ComponentScan({ "io.hulk.dubbo.springfox.demo.springboot", "io.hulk.dubbo.springfox.core" })
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

說明:

  • 添加了@EnableSwagger2注解:用于啟動Swagger。
  • 在@ComponentScan注解中,添加"io.hulk.dubbo.springfox.core",掃描dubbo-springfox中的相關Bean。(在實際開發中,springboot的啟動類會放在基包下,基包通常會命名為groupId.artifactId,當然,其中的中劃線會去掉;此時,默認@ComponentScan會掃描基包下的所有類,所以不加入@ComponentScan也行,但是此處由于需要掃描dubbo-springfox中的相關Bean,所以這里的@ComponentScan需要掃描兩個包:基包 + "io.hulk.dubbo.springfox.core"

對于如果只是想進行在UI界面上調用dubbo和springmvc接口的需要來講,到這里就ok了!!!就是這么簡單。

使用方式總結:引入一個jar包 + 添加一個啟動注解 + 添加一個掃描范圍注解

看一下dubbo-springfox-demo-springboot類組織圖:


Alt pic

說明:

  • BookService:Dubbo服務接口;在該接口中,提供了在如下情景下的dubbo-springfox測試用例;
    • 測試基礎類型、對象類型、默認值設置、是否必須
    • 測試一層泛型
    • 測試嵌套泛型
    • 測試Map
    • 測試方法重載
    • 測試入參和返回值為空
    • 測試Long型
    • 測試接口拋異常
  • BookServiceImpl:BookService的實現類;
  • UserController:controller接口;
  • Book和User:兩個基本的model類。

此處為了簡便,僅以BookService中的一個接口為例來看Dubbo接口的圖像化調用,以UserController中的一個方法為例來看springmvc方法的圖形化調用。

BookService

public interface BookService {
    String testGenericField(List<Book> books);
}

BookServiceImpl

public class BookServiceImpl implements BookService {
    @Override
    public String testGenericField(List<Book> books) {
        if (!CollectionUtils.isEmpty(books)) {
            return books.get(0).getTitle();
        }
        return "書籍列表為空";
    }
}

Book

public class Book {
    private Long   id;
    private String title;
    private String content;
    ...省略setter和getter
}

UserController

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping(value = "/testCommonField2", method = RequestMethod.POST)
    public String testCommonField2(@RequestParam("name") String name,
                                   @RequestBody Map<String, Book> bookMap) {
        return name;
    }
}

啟動服務后,訪問:本機ip:port/swagger-ui.html。就可以看到下圖(這里只看BookService的,UserController的同理):

Alt pic

2.3 接口文檔

上述只是完成了接口的圖形化調用功能,下面來看一下接口文檔的生成。

BookService

@Api("Book相關的Dubbo服務")
public interface BookService {
    @ApiOperation("測試一層泛型")
    String testGenericField(@ApiParam List<Book> books);
}

說明:

  • 在接口上添加注解@Api描述該接口作用;
  • 在方法上添加注解@ApiOperation描述該接口作用;
  • 在參數上添加@ApiParam對參數進行描述;

BookServiceImpl

public class BookServiceImpl implements BookService {
    @Override
    public String testGenericField(List<Book> books) {
        if (!CollectionUtils.isEmpty(books)) {
            return books.get(0).getTitle();
        }
        return "書籍列表為空";
    }
}

Book

@ApiModel("書籍")
public class Book {
    @ApiModelProperty("ID")
    private Long   id;
    @ApiModelProperty("書標題")
    private String title;
    @ApiModelProperty("書的內容")
    private String content;
    ...省略setter和getter
}

說明:

  • 在模型上添加注解@ApiModel描述該模型作用;
  • 在屬性上添加注解@ApiModelProperty描述該屬性作用。

UserController

@Api(tags = "user相關api")
@RestController
@RequestMapping("/user")
public class UserController {
     @ApiOperation("普通2")
    @RequestMapping(value = "/testCommonField2", method = RequestMethod.POST)
    public String testCommonField2(@RequestParam("name") String name,
                                   @RequestBody Map<String, Book> bookMap) {
        return name;
    }
}

注意:

  • 以上的注解都不強制添加,根據需求自行添加。注解相互之間并無影響。
  • Dubbo服務的注解只能添加在接口上,不支持添加在實現上:因為對外的model通常會寫在xxx-api中,所以在接口上添加注解是必須支持的;其次接口上添加注解描述,也可以替代javadoc。
  • dubbo-springfox支持的注解附表見附錄

之后再啟動服務看一下UI圖:

Alt pic

其中Long型為什么展示為int64而非Long,這與js對Long型的是否支持有關系,有興趣的同學可以自行谷歌。

三、dubbo-springfox基本原理

dubbo-springfox基于SpringFox實現,SpringFox只實現了對springmvc方法的掃描;dubbo-springfox在此基礎上增加了對Dubbo接口的掃描。為了最小化的實現,只需要在服務啟動的時候動態的生成Dubbo接口對應的controller,之后由SpringFox的內核去統一掃描所有的controller即可。

關于SpringFox和Swagger,可自行去谷歌。

流程圖如下所示:

Alt pic

3.1 dubbo-springfox掃描接口的時機

編寫一個流程主控類:ApiDocumentBootstrap

@Configuration
public class ApiDocumentBootstrap implements ApplicationContextAware {
    private static final Logger LOGGER = LoggerFactory.getLogger(ApiDocumentBootstrap.class);

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        try {
            /** 1 獲取dubboXmlApi掃描器 */
            final ApiScanner<ApplicationContext, Map<String, ServiceBean>> dubboXmlApiScanner = new XmlDubboApiScanner();

            /** 2 使用dubboXmlApi掃描器從ApplicationContext中獲取ServiceBean(dubbo:service配置)*/
            final Map<String, ServiceBean> serviceBeanMap = dubboXmlApiScanner.scanFromSpringContext(context);

            /** 3 獲取JdkCompiler實例 */
            final JdkCompiler jdkCompiler = new JdkCompiler();

            String serviceGroup = "";

            for (ServiceBean sb : serviceBeanMap.values()) {

                /** 4 根據ServiceBean的相關信息生成dubbo:service對應的controller類字符串 */
                final String code = ClassCodeGenerator.generateClassCode(sb);

                /** 5 使用JdkCompiler實例編譯加載controller類字符串為Class對象 */
                final Class<?> dubboControllerClazz = jdkCompiler.compile(code,
                    ApiDocumentBootstrap.class.getClassLoader());

                /** 6 將生成的controller類Class對象注冊到BeanDefinitionRegistry中,接下來供mvc掃描器進行掃描 */
                SpringHelper.registerBeanDefinition(dubboControllerClazz, context);

                serviceGroup = serviceGroup == "" ? sb.getGroup() : serviceGroup;
            }

            /**
             * 7 設置系統屬性
             */
            System.setProperty(DubboSpringfoxContants.SERVICE_GROUP, serviceGroup == null ? "" : serviceGroup);
        } catch (Exception e) {
            LOGGER.error("dubbo-springfox generate api-document error, msg:", e);
        }
    }
}

該類實現了ApplicationContextAware的setApplicationContext(ApplicationContext context)方法,該方法實現流程圖中的所有步驟。setApplicationContext(ApplicationContext context)方法發生在Spring容器創建后 + springmvc容器掃描controller之前 + 可以獲取到ApplicationContext;

而以xml形式進行配置的Dubbo的每一個服務接口都會生成一個ServiceBean,最后放置在ApplicationContext中;這樣,我們就可以通過在setApplicationContext(ApplicationContext context)中獲取到的ApplicationContext中去獲取所有的ServiceBean;

3.2 獲取所有的ServiceBean

定義掃描器接口:方便后需擴展,掃描注解形式的Dubbo服務等。

public interface ApiScanner<T, R> {
    R scanFromSpringContext(T t);
}

Xml形式的掃描器:

public class XmlDubboApiScanner implements ApiScanner<ApplicationContext, Map<String, ServiceBean>> {
    @Override
    public Map<String, ServiceBean> scanFromSpringContext(ApplicationContext context) {
        if (context == null) {
            return new HashMap<>(0);
        }
        return context.getBeansOfType(ServiceBean.class);
    }
}

從ApplicationContext中獲取所有類型是ServiceBean的Bean。

3.3 創建JdkCompiler編譯器

JdkCompiler繼承于com.alibaba.dubbo.common.compiler.support.AbstractCompiler,Dubbo為該抽象類提供了兩個實現:com.alibaba.dubbo.common.compiler.support.JavassistCompiler和com.alibaba.dubbo.common.compiler.support.JdkCompiler。前者不具有通用性,與Dubbo中構造動態類的寫法強耦合;后者在編譯參數中沒有打印本地變量表,導致無法直接獲取方法參數名。

dubbo-springfox的JdkCompiler與com.alibaba.dubbo.common.compiler.support.JdkCompiler代碼幾乎相同,只是添加了編譯參數"-g",打印本地變量表,后續就可以獲取方法參數名。不使用Javassist的原因是因為Javassist要逐行進行構造代碼或者像com.alibaba.dubbo.common.compiler.support.JavassistCompiler那樣將構造好的完整代碼進行拆解構造,比較麻煩。

具體代碼見:

io.hulk.dubbo.springfox.core.compiler.JdkCompiler

3.4 使用ClassCodeGenerator生成DubboService的controller代碼

public class ClassCodeGenerator {

    public static String generateClassCode(ServiceBean sb) throws NoSuchMethodException, ClassNotFoundException {
        StringBuilder codeBuilder = new StringBuilder();

        /**
         * 1 獲取接口類和實現類
         */
        final Class<?> dubboInterface = sb.getInterfaceClass();
        final Class<?> rawDubboInterfaceImpl = sb.getRef().getClass();
        Class<?> dubboInterfaceImpl = rawDubboInterfaceImpl;

        String dubboInterfaceImplCanonicalName = rawDubboInterfaceImpl.getCanonicalName();
        // 保護性判斷:防止dubbo接口實現類被代理,這里轉換為真實類
        if (dubboInterfaceImplCanonicalName.contains("$$")) {
            dubboInterfaceImpl = ClassLoader.getSystemClassLoader()
                .loadClass(dubboInterfaceImplCanonicalName.substring(0, dubboInterfaceImplCanonicalName.indexOf("$$")));
        }

        /**
         * 2 獲取包名
         */
        final String packageName = dubboInterfaceImpl.getPackage().getName();
        // 保護性措施:包名尾部添加DubboApi,防止被意外AOP
        codeBuilder.append("package ").append(packageName).append("DubboApi;\n");

        /**
         * 4 獲取重組@Api注解
         */
        final Api api = dubboInterface.getAnnotation(Api.class);
        if (api != null) {
            String apiTag = api.value() != "" ? api.value() : api.tags()[0];
            codeBuilder.append("@io.swagger.annotations.Api(tags = \"" + apiTag + "\")").append("\n");
        }

        /**
         * 5 創建@RestController @RequestMapping注解
         */
        codeBuilder.append("@org.springframework.web.bind.annotation.RestController\n")
            .append("@org.springframework.web.bind.annotation.RequestMapping(\"/dubbo-api\")\n");

        /**
         * 6 創建類定義
         * public class BookServiceImplDubboApi
         */
        String classDefinition = dubboInterfaceImpl.toGenericString();
        codeBuilder.append(classDefinition.substring(0, classDefinition.lastIndexOf(" ") + 1))
            .append(dubboInterfaceImpl.getSimpleName()).append("DubboApi {\n");

        /**
         * 7 注入實現類
         */
        String dubboInterfaceImplName = dubboInterfaceImpl.getSimpleName().substring(0, 1).toLowerCase()
                                        + dubboInterfaceImpl.getSimpleName().substring(1);
        codeBuilder.append("@org.springframework.beans.factory.annotation.Autowired\n").append("private ")
            .append(dubboInterfaceImpl.getCanonicalName()).append(" ").append(dubboInterfaceImplName).append(";\n");

        /**
         * 8 獲取全部方法
         */
        for (Method method : dubboInterface.getMethods()) {

            /**
             * 8.1 獲取重組ApiOperation注解
             */
            final ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
            if (apiOperation != null) {
                codeBuilder.append("@io.swagger.annotations.ApiOperation(value = \"" + apiOperation.value() + "\")\n");
            }

            /**
             * 8.2 創建@RequestMapping注解
             * 支持多版本
             * 支持方法重寫
             */
            String serviceVersion = sb.getVersion() != null && sb.getVersion().length() > 0 ? sb.getVersion() : "0.0.0";
            codeBuilder.append("@org.springframework.web.bind.annotation.RequestMapping(value = \"/" + serviceVersion
                               + "/" + dubboInterfaceImpl.getCanonicalName() + "/" + method.getName());
            if (apiOperation != null && apiOperation.nickname().trim().length() > 0) {
                codeBuilder.append("/").append(apiOperation.nickname().trim());
            }
            codeBuilder.append("\", method = org.springframework.web.bind.annotation.RequestMethod.POST)\n");

            /**
             * 8.3 創建方法定義
             */
            codeBuilder.append("public ").append(method.getGenericReturnType().getTypeName()).append(" ")
                .append(method.getName()).append("(");

            /**
             * 8.4 組建方法參數
             */
            final LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
            final String[] parameterNames = parameterNameDiscoverer
                .getParameterNames(dubboInterfaceImpl.getMethod(method.getName(), method.getParameterTypes()));
            final Parameter[] parameters = method.getParameters();
            StringBuilder parameterBuilder = new StringBuilder();

            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                String parameterName = parameterNames[i];
                final ApiParam apiParam = parameter.getAnnotation(ApiParam.class);
                if (apiParam != null) {
                    parameterBuilder.append("@io.swagger.annotations.ApiParam(value = \"").append(apiParam.value())
                        .append("\"").append(", required = ").append(apiParam.required()).append(", defaultValue = \"")
                        .append(apiParam.defaultValue()).append("\") ");
                }

                if (PrimitiveTypeHelper.primitive(parameter.getType())) {
                    parameterBuilder.append("@org.springframework.web.bind.annotation.RequestParam(value = \"")
                        .append(parameterName).append("\"");
                    if (apiParam == null || apiParam.required() == false) {
                        parameterBuilder.append(", required = false");
                    } else {
                        parameterBuilder.append(", required = true");
                    }
                    parameterBuilder.append(")");
                } else {
                    parameterBuilder.append("@springfox.documentation.spi.annotations.RequestModel(value = \"")
                        .append(parameterName).append("\"");
                    if (apiParam == null || apiParam.required() == false) {
                        parameterBuilder.append(", required = false");
                    } else {
                        parameterBuilder.append(", required = true");
                    }
                    parameterBuilder.append(")");
                }

                parameterBuilder.append(parameter.getParameterizedType().getTypeName()).append(" ");
                parameterBuilder.append(parameterName).append(",");
            }

            if (parameterBuilder != null && parameterBuilder.length() > 0) {
                codeBuilder.append(parameterBuilder.substring(0, parameterBuilder.length() - 1));
            }

            String parameterNamesStr = "";
            if (parameterNames != null && parameterNames.length > 0) {
                parameterNamesStr = String.join(", ", parameterNames);
            }

            /**
             * 8.5 組建方法異常標識
             */
            codeBuilder.append(") ");
            final Class<?>[] exceptionTypes = method.getExceptionTypes();
            if (exceptionTypes != null && exceptionTypes.length > 0) {
                codeBuilder.append("throws ");
                for (int i = 0; i < exceptionTypes.length - 1; i++) {
                    codeBuilder.append(exceptionTypes[i].getCanonicalName()).append(",");
                }
                codeBuilder.append(exceptionTypes[exceptionTypes.length - 1].getCanonicalName());
            }

            /**
             * 8.6 組建方法內容
             */
            codeBuilder.append(" {");
            if (!method.getGenericReturnType().getTypeName().equalsIgnoreCase("void")) {
                codeBuilder.append("return ");
            }
            codeBuilder.append(dubboInterfaceImplName).append(".").append(method.getName()).append("(")
                .append(parameterNamesStr != "" ? parameterNamesStr : "").append(");");

            codeBuilder.append("}");
        }

        /**
         * 9 定義類結尾
         */
        codeBuilder.append("}");
        return codeBuilder.toString();
    }
}

代碼較為簡單,就是使用反射獲取各種類信息、方法信息、注解信息等,最后進行拼接。

假設Dubbo服務代碼如下:

package io.hulk.dubbo.springfox.demo.springboot.api;

...import省略

@Api("Book相關的Dubbo服務")
public interface BookService {
    @ApiOperation("測試一層泛型")
    String testGenericField(@ApiParam List<Book> books);
}

其實現如下:

package io.hulk.dubbo.springfox.demo.springboot.apiimpl;

...import省略

public class BookServiceImpl implements BookService {
    @Override
    public String testGenericField(List<Book> books) {
        if (!CollectionUtils.isEmpty(books)) {
            return books.get(0).getTitle();
        }
        return "書籍列表為空";
    }
}

最后生成的Controller代碼:

package io.hulk.dubbo.springfox.demo.springboot.apiimplDubboApi;

@io.swagger.annotations.Api(tags = "Book相關的Dubbo服務")
@org.springframework.web.bind.annotation.RestController
@org.springframework.web.bind.annotation.RequestMapping("/dubbo-api")
public class BookServiceImplDubboApi {
    @org.springframework.beans.factory.annotation.Autowired
    private io.hulk.dubbo.springfox.demo.springboot.apiimpl.BookServiceImpl bookServiceImpl;

    @io.swagger.annotations.ApiOperation(value = "測試一層泛型")
    @org.springframework.web.bind.annotation.RequestMapping(value = "/0.0.0/io.hulk.dubbo.springfox.demo.springboot.apiimpl.BookServiceImpl/testGenericField", method = org.springframework.web.bind.annotation.RequestMethod.POST)
    public java.lang.String testGenericField(@io.swagger.annotations.ApiParam(value = "", required = false, defaultValue = "") @springfox.documentation.spi.annotations.RequestModel(value = "books", required = false) java.util.List<io.hulk.dubbo.springfox.demo.springboot.model.Book> books) {
        return bookServiceImpl.testGenericField(books);
    }
}

說明:

  • 包名:Dubbo服務實現類的包名+"DubboApi"后綴;
  • 類注解:添加Dubbo接口上配置的@Api注解 + @RestController + 類上下文匹配路徑("/dubbo-api");
  • 類:Dubbo服務實現類的類名+"DubboApi"后綴;
  • 注入Dubbo服務實現類,之后的實現都通過該實現類進行調用;
  • 方法注解:添加Dubbo接口上配置的@ApiOperation注解 + 方法匹配路徑("/{Dubbo服務版本號}/{Dubbo服務實現類的全類名}/{方法名}");
    • 對于重載的方法,方法注解需要添加nickname tag進行區分,具體見io.hulk.dubbo.springfox.demo.springboot.api.BookService。
  • 方法:與Dubbo服務方法相同;
  • 方法參數:添加@ApiParam注解 + 相關的參數接收注解;
    • 參數接收注解:對于普通類型的參數,使用@RequestParam;對于對象類型的參數,使用@RequestModel,該注解是自定義的一個注解,用于打破@RequestBody的單體限制,關于單體限制與自定義mvc參數注解的方式見作者的另一篇文章《自定義spring參數注解 - 打破@RequestBody單體限制
  • 方法體:調用注入的Dubbo服務實現類調用同名方法。

3.5 使用JdkCompiler編譯生成的controller并使用classLoader加載其到JVM中

使用JdkCompiler將4.4小節中生成的code進行javac編譯,之后使用ApiDocumentBootstrap的類加載器加載編譯好的class文件,返回Class對象;

Class<?> dubboControllerClazz = jdkCompiler.compile(code, ApiDocumentBootstrap.class.getClassLoader());

3.6 注冊Class對象到BeanDefinitionRegistry中

然后創建Class對象的BeanDefinition對象,最后注冊到BeanDefinitionRegistry中。

    /**
     * 獲取clazz的BeanDefinition并注冊到BeanDefinitionRegistry中
     *
     * @param clazz
     * @param context
     */
    public static void registerBeanDefinition(Class<?> clazz, ApplicationContext context) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) context;
        BeanDefinitionRegistry beanDefinitonRegistry = (BeanDefinitionRegistry) configurableApplicationContext.getBeanFactory();
        beanDefinitonRegistry.registerBeanDefinition(clazz.getCanonicalName(), beanDefinitionBuilder.getBeanDefinition());
    }

這樣之后,springmvc在掃描controller的時候,就會將生成的一并掃描了,SpringFox也會統一掃描所有的controller,生成相關的文檔和圖形化調用界面。

3.7 設置系統屬性

最后,設置系統屬性。

System.setProperty(DubboSpringfoxContants.SERVICE_GROUP, serviceGroup);

將當前Dubbo服務的group讀取出來并存儲到系統屬性中。在構建springfox.documentation.spring.web.plugins.Docket實例的時候使用。通過Docket實例可以配置一系列與SpringFox相關的信息,其中最重要的就是指定圖形化UI界面在何時可以顯示接口信息。

    @Bean
    public Docket docket() {
        final String serviceGroup = System.getProperty(DubboSpringfoxContants.SERVICE_GROUP, "dev");
        if (serviceGroup.equals(DubboSpringfoxContants.SERVICE_GROUP_PRODUCT)) {
            return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.none()).build();
        }
        return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any())
            //                .paths(PathSelectors.regex("^(/error/)"))
            .build().useDefaultResponseMessages(false)
            .apiInfo(new ApiInfo("", "", "", "", new Contact("", "", ""), "", "", new ArrayList<VendorExtension>()));
    }

這里讀取剛剛存放到系統屬性中的Dubbo服務的group信息,然后判斷如果是"product"則不在UI上顯示接口信息,其他的group或者沒有配置group都會顯示。

假設配置為這樣:

<dubbo:service interface="io.hulk.dubbo.springfox.demo.springboot.api.BookService" ref="bookService" group="product"/>

UI界面為:

Alt pic

查看返回結果:http://localhost:8081/v2/api-docs

{
    swagger: "2.0",
    info: {
        description: "Api Documentation",
        version: "1.0",
        title: "Api Documentation",
        termsOfService: "urn:tos",
        contact:{},
        license: {
            name: "Apache 2.0",
            url: "http://www.apache.org/licenses/LICENSE-2.0"
        }
    },
    host: "localhost:8081",
    basePath: "/"
}

也沒有相關的接口信息,這樣就保證了在生產環境的安全性。

附1、dubbo-springfox注解附表

對于springmvc controller接口來講,支持全部的Swagger注解;對于Dubbo服務接口來講,僅支持如下注解:

@Api("xxx")

用于類上,注解內屬性僅支持value和tags。默認不寫屬性名就是value。

@ApiOperation(value="xxx", nickname ="xxx")

用于方法上,注解內屬性僅支持value和nickname。nickname用于區分重載的方法。默認不寫屬性名就是value。

@ApiParam(value="xxx", required="xxx", defaultValue="xxx")

用于參數上,注解內屬性僅支持value、required和defaultValue。默認不寫屬性名就是value。

附2、Q&A

Q1:dubbo-springfox是否支持注解形式配置的Dubbo服務?

不支持。

Q2:方法重載時是否必須填寫nickname?

方法重載時必須添加@ApiOperation(nickname="xxx")注解和注解屬性,否則會報出springmvc匹配路徑重復的錯誤。

Q3:為什么沒有看到前臺代碼,就可以顯示出UI界面?

SpringFox-UI采用webjar技術,將swagger-ui打包成jar包,是的后端可以以jar包的形式來引入前端代碼。關于webjar技術,參考:https://www.webjars.org/

Q4:其他關于SpringFox的疑問?

http://springfox.github.io/springfox/docs/current/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容

  • 「夢斷代碼」中對軟件工程所面臨的種種困難與艱難的描述,即便再過5年讀也許都不過時。因為正如原作者所說,書中描寫的是...
    Binaryify閱讀 362評論 1 2
  • 前面提到 Spring Boot 推崇的是 0 配置,不進行配置項目也能夠啟動起來,當然我們也可以對一些默認配置進...
    起司貓_0e99閱讀 286評論 0 0
  • 這幾天的情緒不怎么好。孩子生病自己著急,看劉先生咋看咋不順眼。所以沒有好臉色,沒有好聲音??粗R中的自己,...
    立春暖陽閱讀 222評論 0 3