一、问题
微服务化的时代,我们整个项目工程下面都会有很多的子系统,对于每个应用都有暴露 Api 接口文档需要,这个时候我们就会想到 Swagger 这个优秀 jar 包。但是我们会遇到这样的问题,假如说我们有5个应用,难道说我们每个模块下面都要去引入这个 jar 包吗?我作为一个比较懒的程序感觉这样好麻烦,于是乎我思考了一种我认为比较好的方式,如果大家觉得有什么不太好的地方希望指正,谢谢!
二、基础
开始之前大家首先要了解一些基础,主要有以下几个方面:
- 单应用下 Swagger 的集成与使用
- 条件装配 @Conditional 介绍
- 配置文件参数获取 @ConfigurationProperties
单体应用下 Swagger 集成与使用
关于这部分从3方面讲起分别是:什么是、为什么、如何用
什么是 Swagger ?
Swagger 是一个规范且完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
为什么使用 Swagger ?
主要的优点:
- 支持 API 自动生成同步的在线文档:使用 Swagger 后可以直接通过代码生成文档,不再需要自己手动编写接口文档了,对程序员来说非常方便,可以节约写文档的时间去学习新技术。
- 提供 Web 页面在线测试 API:光有文档还不够,Swagger 生成的文档还支持在线测试。参数和格式都定好了,直接在界面上输入参数对应的值即可在线测试接口。
缺点的话就是但凡引入一个 jar 需要去了解下原理和使用,对于这个缺点我感觉相比于优点就是大巫见小巫,我简单看了一下源码,其实不算太难。
如何使用 Swagger
关于 Swagger 的使用其实也就是3板斧,大家一定很熟悉的;
第一板斧就是引入 jar 包,这里我使用的是2.9.2版本
<!-- swagger 相关 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger2.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger2.version}</version> </dependency>
第二板斧就是SpringBoot自动扫描配置类
/** * SwaggerConfig * * @author wangtongzhou * @since 2020-06-09 09:41 */ @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) //生产环境的时候关闭 Swagger 比较安全 .apiInfo(apiInfo()) .select() //Api扫描目录 .apis(RequestHandlerSelectors.basePackage("com.springboot2.learning")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("learn") .description("learn") .version("1.0") .build(); } }
第三板斧使用 Swagger 注解
/** * 用户相关接口 * * @author wangtongzhou * @since 2020-06-12 07:35 */ @RestController @RequestMapping("/user") @Api(value = "用户相关接口") public class UserController { @PostMapping("/") @ApiOperation("添加用户的接口") @ApiImplicitParams({ @ApiImplicitParam(name = "userName", value = "用户名", defaultValue = "wtz"), @ApiImplicitParam(name = "age", value = "年龄", defaultValue = "20") }) public User addUser(String userName, Integer age) { User user = new User(); user.setAge(age); user.setUserName(userName); return user; } @GetMapping("/{userId}") @ApiOperation("根据用户id查询用户信息") @ApiImplicitParam(name = "userId", value = "用户id", defaultValue = "20") public User queryUserByUserId(@PathVariable Long userId) { User user = new User(); user.setUserId(userId); return user; } } /** * 用户实体 * * @author wangtongzhou * @since 2020-06-12 07:45 */ @ApiModel public class User { @ApiModelProperty(value = "用户名称") private String userName; @ApiModelProperty(value = "年龄") private Integer age; @ApiModelProperty(value = "用户id") private Long userId; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } }
效果如下:
实体注解
接口描述
接口参数
执行接口
返回地址
条件装配 @Conditional 介绍
@Conditional 是Spring4.0提供的注解,位于 org.springframework.context.annotation 包内,它可以根据代码中设置的条件装载不同的bean。比如说当一个接口有两个实现类时,我们要把这个接口交给Spring管理时通常会只选择实现其中一个实现类,这个时候我们总不能使用if-else吧,所以这个@Conditional的注解就出现了。
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Conditional { Class<? extends Condition>[] value(); }
使用方法
这里介绍一个MySQL和Oracle选择方式,开始之前首先在properties文件中增加sql.name=mysql的配置,接下来步骤如下
- 实现Conditional接口, 实现matches方法
/** * mysql条件装配 * * @author wangtongzhou * @since 2020-06-13 08:01 */ public class MysqlConditional implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String sqlName = context.getEnvironment().getProperty("sql.name"); if ("mysql".equals(sqlName)){ return true; } return false; } } /** * oracle条件装配 * * @author wangtongzhou * @since 2020-06-13 08:02 */ public class OracleConditional implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String sqlName=context.getEnvironment().getProperty("sql.name"); if ("oracle".equals(sqlName)){ return true; } return false; } }
- 在需要判断条件的bean上,加上@Conditional(***.class)即可在满足条件的时候加载对应的类
/** * conditional * * @author wangtongzhou * @since 2020-06-13 08:01 */ @Configuration public class ConditionalConfig { @Bean @Conditional(MysqlConditional.class) public Mysql mysql() { return new Mysql(); } @Bean @Conditional(OracleConditional.class) public Oracle oracle() { return new Oracle(); } }
- 调用测试
@RunWith(SpringRunner.class) @SpringBootTest public class ConditionalTests { @Autowired private ApplicationContext applicationContext; @Test public void test_conditional() { Mysql mysql = (Mysql) applicationContext.getBean("mysql"); Assert.assertNotNull(mysql); Assert.assertTrue("mysql".equals(mysql.getSqlName())); } }
其他扩展注解
- @@ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
当存在Docket和ApiInfoBuilder类的时候才加载Bean; - @ConditionalOnMissingClass不存在某个类的时候才会实例化Bean;
- @ConditionalOnProperty(prefix = "swagger", value = "enable", matchIfMissing = true)当存在swagger为前缀的属性,才会实例化Bean;
- @ConditionalOnMissingBean当不存在某个Bean的时候才会实例化;
这里就介绍这几个常用,org.springframework.boot.autoconfigure.condition这个下面包含全部的关于@Conditional相关的所有注解
@Conditional扩展
注解介绍
配置文件参数获取 @ConfigurationProperties
@ConfigurationProperties是SpringBoot加入的注解,主要用于配置文件中的指定键值对映射到一个Java实体类上。关于这个的使用就在下面的方式引出。
三、比较好的方式
关于开篇中引入的问题,解题流程主要是以下3步:
- 抽象一个公共 Swagger jar;
- 如何定制化 Swagger jar;
- 使用定制化完成以后的 Swagger jar;
抽象一个公共 Swagger jar
主要是就是将 Swagger jar 和一些其他需要的 jar 进行引入,使其成为一个公共的模块,软件工程中把这个叫做单一原则;
Maven包的引入
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> </dependencies>
如何定制化 Swagger jar;
定制化就是将 Swagger 相关的属性进行配置化的处理,这里也可以分为两步;
- 将公共的属性抽象成配置化的类,这里就是关于@ConfigurationProperties的使用,将配置文件中的 swagger 开头的属性映射到配置类的属性当中;
/** * Swagger基本属性 * * @author wangtongzhou * @since 2020-05-24 16:58 */ @ConfigurationProperties("swagger") public class SwaggerProperties { /** * 子系统 */ private String title; /** * 描述 */ private String description; /** * 版本号 */ private String version; /** * api包路径 */ private String basePackage; public String getTitle() { return title; } public SwaggerProperties setTitle(String title) { this.title = title; return this; } public String getDescription() { return description; } public SwaggerProperties setDescription(String description) { this.description = description; return this; } public String getVersion() { return version; } public SwaggerProperties setVersion(String version) { this.version = version; return this; } public String getBasePackage() { return basePackage; } public SwaggerProperties setBasePackage(String basePackage) { this.basePackage = basePackage; return this; } }
- 公共属性赋值配置到 Swagger 的配置类中,该配置类中进行一些类条件的判断和插件Bean是否已经注入过,然后就是将配置类中的属性,赋值到 Swagger 的初始化工程中;
/** * Swagger自动配置类 * * @author wangtongzhou * @since 2020-05-24 16:35 */ @Configuration @EnableSwagger2 @ConditionalOnClass({Docket.class, ApiInfoBuilder.class}) @ConditionalOnProperty(prefix = "swagger", value = "enable", matchIfMissing = true) @EnableConfigurationProperties(SwaggerProperties.class) public class SwaggerConfig { @Bean @ConditionalOnMissingBean public SwaggerProperties swaggerProperties() { return new SwaggerProperties(); } @Bean public Docket createRestApi() { SwaggerProperties properties = swaggerProperties(); return new Docket(DocumentationType.SWAGGER_2) //生产环境的时候关闭 Swagger 比较安全 .apiInfo(apiInfo(properties)) .select() //Api扫描目录 .apis(RequestHandlerSelectors.basePackage(properties.getBasePackage())) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo(SwaggerProperties properties) { return new ApiInfoBuilder() .title(properties.getTitle()) .description(properties.getDescription()) .version(properties.getVersion()) .build(); } }
完成以上两步,就完成了 Swagger 模块的定制化开发,接下来还要做一件事情,作为一个公共的模块,我们要让他自己进行自动化装配,解放我们的双手,我们在 resources 目录下增加一个 spring.factories 配置文件,内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.springcloud.study.swagger.config.SwaggerConfig
到此我们完成所有的开发;
使用定制化完成以后的 Swagger jar;
关于使用也分为两步,
- 引入 jar;
<dependency> <groupId>com.springcloud.study</groupId> <artifactId>common-swagger</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- 定制化的属性配置;
# Swagger 配置项 swagger: title: 用户模块 description: 用户子系统 version: 1.0.0 base-package: com.springcloud.study.user.controller
完成这两步就可以开启正常的使用了;
四、后续的规划
- 注解整理
后续会将 Spring 注解进行一个统一的整理,包含一些使用说明或者原理等等,希望到时候能帮助到大家吧,目前计划两周一个吧; - 开源项目
Spring Cloud 的学习过于碎片化,希望通过自己搞一个开源项目,提升对各个组件的掌握能力,同时也能产出一套通用化权限管理系统,具备很高的灵活性、扩展性和高可用性,并且简单易用,这块是和未来做企业数字化转型相关的事是重合的,慢慢的会做一些企业级通用化的的功能开发;前端部分的话希望是采用Vue,但是这块有一个学习成本,还没有进行研究,目前还没排上日程。整体的里程碑是希望在6.22离职之前完成整套后端的开发,7月中旬完成第一次Commit。