This commit is contained in:
wyl
2023-05-16 23:07:03 +08:00
parent ec353b09e5
commit 52eb967507
16 changed files with 1524 additions and 6 deletions
@@ -3,7 +3,6 @@ package com.aivfo.el.starter.base;
import com.aivfo.el.starter.base.utils.StringPool;
import com.aivfo.el.starter.base.utils.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -41,7 +40,6 @@ import java.io.Serializable;
@ToString
@NoArgsConstructor
@SuppressWarnings("all")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public abstract class Result<T> implements Serializable {
/**
@@ -11,9 +11,33 @@
<artifactId>aivfo-doc-spring-boot-autoconfigure</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.aivfo</groupId>
<artifactId>aivfo-doc-spring-boot-core</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,147 @@
package com.aivfo.doc.autoconfigure.knife4j;
import com.aivfo.doc.core.common.schema.AivfoOperationModelsProviderPlugin;
import com.aivfo.doc.core.common.schema.AivfoOperationResponseMessagePlugin;
import com.aivfo.doc.core.knife4j.EnableKnife4j;
import com.github.xiaoymin.knife4j.core.extend.OpenApiExtendSetting;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import com.github.xiaoymin.knife4j.spring.filter.ProductionSecurityFilter;
import com.github.xiaoymin.knife4j.spring.filter.SecurityBasicAuthFilter;
import com.github.xiaoymin.knife4j.spring.model.MarkdownFiles;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.DispatcherServlet;
import springfox.documentation.swagger2.web.Swagger2ControllerWebMvc;
import javax.servlet.Servlet;
/**
* @author: wangyl
* @date: 2023/5/16
* @description: Knife4j 基础自动配置类
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(value = {
EnableKnife4j.class,
Servlet.class,
DispatcherServlet.class,
Swagger2ControllerWebMvc.class
})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableConfigurationProperties(value = {Knife4jProperties.class})
@ComponentScan(basePackages = "com.github.xiaoymin.knife4j.spring.plugin")
@Slf4j
public class Knife4jAutoConfiguration {
/**
* 配置Cors
*
* @return cors filter
* @since 2.0.4
*/
@Bean
@ConditionalOnMissingBean(CorsFilter.class)
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setMaxAge(10000L);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
/**
* Markdown resolver
*
* @param knife4jProperties knife 4 j properties
* @return the open api extension resolver
* @since 1.4.0
*/
@Bean(initMethod = "start")
@ConditionalOnMissingBean(OpenApiExtensionResolver.class)
@ConditionalOnProperty(name = "knife4j.enable", havingValue = "true")
public OpenApiExtensionResolver markdownResolver(Knife4jProperties knife4jProperties) {
OpenApiExtendSetting setting = knife4jProperties.getSetting();
if (setting == null) {
setting = new OpenApiExtendSetting();
}
return new OpenApiExtensionResolver(setting, knife4jProperties.getDocuments());
}
/**
* 初始化自定义Markdown特性
*
* @param knife4jProperties 配置文件
* @return markdownFiles markdown files
* @since 1.4.0
*/
@Bean(initMethod = "init")
public MarkdownFiles markdownFiles(@NotNull Knife4jProperties knife4jProperties) {
return new MarkdownFiles(knife4jProperties.getMarkdowns() == null ? "" : knife4jProperties.getMarkdowns());
}
/**
* Security basic auth filter
*
* @param knife4jProperties knife 4 j properties
* @return the security basic auth filter
* @since 1.4.0
*/
@Bean
public SecurityBasicAuthFilter securityBasicAuthFilter(@NotNull Knife4jProperties knife4jProperties) {
return new SecurityBasicAuthFilter(knife4jProperties.getBasic().isEnable(),
knife4jProperties.getBasic().getUsername(),
knife4jProperties.getBasic().getPassword());
}
/**
* Production security filter
*
* @return the production security filter
* @since 1.4.0
*/
@Bean
public ProductionSecurityFilter productionSecurityFilter() {
return new ProductionSecurityFilter(false);
}
/**
* Mvc operation response message plugin
*
* @return the mvc operation response message plugin
* @since 1.7.0
*/
@Bean
@ConditionalOnMissingBean
public AivfoOperationResponseMessagePlugin mvcOperationResponseMessagePlugin() {
return new AivfoOperationResponseMessagePlugin(null);
}
/**
* Mvc operation models provider plugin
*
* @return the mvc operation models provider plugin
* @since 1.7.0
*/
@Bean
@ConditionalOnMissingBean
public AivfoOperationModelsProviderPlugin mvcOperationModelsProviderPlugin() {
return new AivfoOperationModelsProviderPlugin(null);
}
}
@@ -0,0 +1,62 @@
package com.aivfo.doc.autoconfigure.knife4j;
import com.github.xiaoymin.knife4j.core.extend.OpenApiExtendSetting;
import com.github.xiaoymin.knife4j.core.model.MarkdownProperty;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@Data
@ConfigurationProperties(prefix = Knife4jProperties.PREFIX)
public class Knife4jProperties {
/**
* PREFIX
*/
public static final String PREFIX = "aivfo.doc.knife4j";
/**
* 是否开启BasicHttp验证
*/
private Knife4jHttpBasic basic = new Knife4jHttpBasic();
/**
* markdown 路径
*/
private String markdowns;
/**
* 是否开启Knife4j增强模式
*/
private boolean enable = false;
/**
* 是否开启默认跨域
*/
private boolean cors = false;
/**
* 是否生产环境
*/
private boolean production = false;
/**
* 个性化配置
*/
private OpenApiExtendSetting setting;
/**
* 分组文档集合
*/
private List<MarkdownProperty> documents;
@Data
public static class Knife4jHttpBasic {
/**
* basic 是否开启,默认为false
*/
private boolean enable = false;
/**
* basic 用户名
*/
private String username = "aivfo";
/**
* basic 密码
*/
private String password = "aivfo2017";
}
}
@@ -0,0 +1,443 @@
package com.aivfo.doc.autoconfigure.swagger;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.DispatcherServlet;
import springfox.documentation.builders.*;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.ApiSelectorBuilder;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.ApiKeyVehicle;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
import springfox.documentation.swagger2.configuration.Swagger2DocumentationWebMvcConfiguration;
import springfox.documentation.swagger2.web.Swagger2ControllerWebMvc;
import javax.servlet.Servlet;
import java.util.*;
import java.util.stream.Collectors;
import static com.google.common.collect.Lists.newArrayList;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(value = {Servlet.class, DispatcherServlet.class, Swagger2ControllerWebMvc.class})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@Import(value = {Swagger2DocumentationWebMvcConfiguration.class})
@ConditionalOnProperty(name = SwaggerDocProperties.PREFIX + ".enabled", matchIfMissing = true)
@EnableConfigurationProperties(SwaggerDocProperties.class)
public class SwaggerDocAutoConfiguration implements BeanFactoryAware {
/**
* Swagger properties
*/
private final SwaggerDocProperties swaggerProperties;
/**
* Basic auth
*/
private static final String BASIC_AUTH = "BasicAuth";
/**
* None
*/
private static final String NONE = "None";
/**
* Bean factory
*/
private BeanFactory beanFactory;
/**
* Instantiates a new Swagger auto configuration.
*
* @param swaggerProperties the swagger properties
* @since 1.4.0
*/
@Contract(pure = true)
public SwaggerDocAutoConfiguration(@NotNull SwaggerDocProperties swaggerProperties) {
this.swaggerProperties = swaggerProperties;
// 默认排除 /actuator 接口文档
swaggerProperties.getExcludePath().add("/actuator/**");
swaggerProperties.getExcludePath().add("/error");
}
/**
* Ui configuration ui configuration.
*
* @param swaggerProperties the swagger properties
* @return the ui configuration
* @since 1.4.0
*/
@Bean
public UiConfiguration uiConfiguration(@NotNull SwaggerDocProperties swaggerProperties) {
return UiConfigurationBuilder.builder()
.deepLinking(swaggerProperties.getUiConfig().getDeepLinking())
.defaultModelExpandDepth(swaggerProperties.getUiConfig().getDefaultModelExpandDepth())
.defaultModelRendering(swaggerProperties.getUiConfig().getDefaultModelRendering())
.defaultModelsExpandDepth(swaggerProperties.getUiConfig().getDefaultModelsExpandDepth())
.displayOperationId(swaggerProperties.getUiConfig().getDisplayOperationId())
.displayRequestDuration(swaggerProperties.getUiConfig().getDisplayRequestDuration())
.docExpansion(swaggerProperties.getUiConfig().getDocExpansion())
.maxDisplayedTags(swaggerProperties.getUiConfig().getMaxDisplayedTags())
.operationsSorter(swaggerProperties.getUiConfig().getOperationsSorter())
.showExtensions(swaggerProperties.getUiConfig().getShowExtensions())
.tagsSorter(swaggerProperties.getUiConfig().getTagsSorter())
.validatorUrl(swaggerProperties.getUiConfig().getValidatorUrl())
.build();
}
/**
* Create rest api list.
*
* @param swaggerProperties the swagger properties
* @return the list
* @since 1.4.0
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(UiConfiguration.class)
@ConditionalOnProperty(name = SwaggerDocProperties.PREFIX + ".enabled", matchIfMissing = true)
public List<Docket> createRestApi(@NotNull SwaggerDocProperties swaggerProperties) {
ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) this.beanFactory;
List<Docket> docketList = new LinkedList<>();
// 没有分组
if (swaggerProperties.getDocket().size() == 0) {
SwaggerDocProperties.DocketInfo docketInfo = new SwaggerDocProperties.DocketInfo()
.setTitle(swaggerProperties.getTitle())
.setDescription(swaggerProperties.getDescription())
.setVersion(StringUtils.isEmpty(swaggerProperties.getVersion())
? "1.0"
: swaggerProperties.getVersion())
.setLicense(swaggerProperties.getLicense())
.setLicenseUrl(swaggerProperties.getLicenseUrl())
.setBasePackage(swaggerProperties.getBasePackage())
.setContact(new SwaggerDocProperties.Contact(swaggerProperties.getContact().getName(),
swaggerProperties.getContact().getUrl(),
swaggerProperties.getContact().getEmail()))
.setTermsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl());
swaggerProperties.getDocket().put("wyl", docketInfo);
}
// 分组创建
return this.buildDockets(swaggerProperties, configurableBeanFactory, docketList);
}
/**
* Build dockets list
*
* @param swaggerProperties swagger properties
* @param configurableBeanFactory configurable bean factory
* @param docketList docket list
* @return the list
* @since 1.4.0
*/
@Contract("_, _, _ -> param3")
private List<Docket> buildDockets(@NotNull SwaggerDocProperties swaggerProperties,
ConfigurableBeanFactory configurableBeanFactory,
List<Docket> docketList) {
for (String groupName : swaggerProperties.getDocket().keySet()) {
SwaggerDocProperties.DocketInfo docketInfo = swaggerProperties.getDocket().get(groupName);
ApiInfo apiInfo = new ApiInfoBuilder()
.title(docketInfo.getTitle().isEmpty() ? swaggerProperties.getTitle() : docketInfo.getTitle())
.description(docketInfo.getDescription().isEmpty() ? swaggerProperties.getDescription() : docketInfo.getDescription())
.version(docketInfo.getVersion().isEmpty() ? StringUtils.isEmpty(swaggerProperties.getVersion())
? "1.0"
: swaggerProperties.getVersion()
: docketInfo.getVersion())
.license(docketInfo.getLicense().isEmpty() ? swaggerProperties.getLicense() : docketInfo.getLicense())
.licenseUrl(docketInfo.getLicenseUrl().isEmpty() ? swaggerProperties.getLicenseUrl() : docketInfo.getLicenseUrl())
.contact(
new Contact(
docketInfo.getContact().getName().isEmpty()
? swaggerProperties.getContact().getName()
: docketInfo.getContact().getName(),
docketInfo.getContact().getUrl().isEmpty()
? swaggerProperties.getContact().getUrl()
: docketInfo.getContact().getUrl(),
docketInfo.getContact().getEmail().isEmpty()
? swaggerProperties.getContact().getEmail()
: docketInfo.getContact().getEmail()
)
)
.termsOfServiceUrl(docketInfo.getTermsOfServiceUrl().isEmpty()
? swaggerProperties.getTermsOfServiceUrl()
: docketInfo.getTermsOfServiceUrl())
.build();
Docket docketForBuilder = new Docket(DocumentationType.SWAGGER_2)
.host(swaggerProperties.getHost())
.apiInfo(apiInfo)
.securityContexts(Collections.singletonList(this.securityContext()))
.globalOperationParameters(this.assemblyGlobalOperationParameters(
swaggerProperties.getGlobalOperationParameters(),
docketInfo.getGlobalOperationParameters()));
this.processAuthAndResponse(swaggerProperties, docketForBuilder);
Docket docket = this.processPath(
docketForBuilder
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(docketInfo.getBasePackage())))
.build();
// ignoredParameterTypes
Class<?>[] array = new Class[docketInfo.getIgnoredParameterTypes().size()];
Class<?>[] ignoredParameterTypes = docketInfo.getIgnoredParameterTypes().toArray(array);
docket.ignoredParameterTypes(ignoredParameterTypes);
// 如果是 dubbo 应用, 会导致存在相同的 bean name, 因此这里添加一个前缀
configurableBeanFactory.registerSingleton("SwaggerDoc@" + groupName, docket);
docketList.add(docket);
}
return docketList;
}
/**
* Process path *
*
* @param apiSelectorBuilder api selector builder
* @return the api selector builder
* @since 1.4.0
*/
private ApiSelectorBuilder processPath(ApiSelectorBuilder apiSelectorBuilder) {
// base-path 处理 当没有配置任何 path 的时候,解析 /**
if (this.swaggerProperties.getBasePath().isEmpty()) {
this.swaggerProperties.getBasePath().add("/**");
}
for (String base : this.swaggerProperties.getBasePath()) {
apiSelectorBuilder.paths(PathSelectors.ant(base));
}
for (String exclude : this.swaggerProperties.getExcludePath()) {
apiSelectorBuilder.paths(PathSelectors.ant(exclude).negate());
}
return apiSelectorBuilder;
}
/**
* 配置默认的全局鉴权策略的开关,以及通过正则表达式进行匹配; 默认 ^.*$ 匹配所有 URL
* 其中 securityReferences 为配置启用的鉴权策略
*
* @return security context
* @since 1.4.0
*/
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(this.defaultAuth())
.forPaths(PathSelectors.regex(this.swaggerProperties.getAuthorization().getAuthRegex()))
.build();
}
/**
* Build global operation parameters from swagger properties list
*
* @param globalOperationParameters global operation parameters
* @return the list
* @since 1.4.0
*/
@NotNull
private List<Parameter> buildGlobalOperationParametersFromSwaggerProperties(
List<SwaggerDocProperties.GlobalOperationParameter> globalOperationParameters) {
List<Parameter> parameters = new ArrayList<>();
if (Objects.isNull(globalOperationParameters)) {
return parameters;
}
for (SwaggerDocProperties.GlobalOperationParameter globalOperationParameter : globalOperationParameters) {
parameters.add(new ParameterBuilder()
.name(globalOperationParameter.getName())
.description(globalOperationParameter.getDescription())
.modelRef(new ModelRef(globalOperationParameter.getModelRef()))
.parameterType(globalOperationParameter.getParameterType())
.required(Boolean.parseBoolean(globalOperationParameter.getRequired()))
.build());
}
return parameters;
}
/**
* Process auth and response *
*
* @param swaggerProperties swagger properties
* @param docketForBuilder docket for builder
* @since 1.4.0
*/
private void processAuthAndResponse(@NotNull SwaggerDocProperties swaggerProperties, Docket docketForBuilder) {
if (BASIC_AUTH.equalsIgnoreCase(swaggerProperties.getAuthorization().getType())) {
docketForBuilder.securitySchemes(Collections.singletonList(this.basicAuth()));
} else if (!NONE.equalsIgnoreCase(swaggerProperties.getAuthorization().getType())) {
docketForBuilder.securitySchemes(Collections.singletonList(this.apiKey()));
}
// 全局响应消息
if (!swaggerProperties.isApplyDefaultResponseMessages()) {
this.buildGlobalResponseMessage(swaggerProperties, docketForBuilder);
}
}
/**
* 局部参数按照 name 覆盖局部参数
*
* @param globalOperationParameters global operation parameters
* @param docketOperationParameters docket operation parameters
* @return list list
* @since 1.4.0
*/
@NotNull
private List<Parameter> assemblyGlobalOperationParameters(
List<SwaggerDocProperties.GlobalOperationParameter> globalOperationParameters,
List<SwaggerDocProperties.GlobalOperationParameter> docketOperationParameters) {
if (Objects.isNull(docketOperationParameters) || docketOperationParameters.isEmpty()) {
return this.buildGlobalOperationParametersFromSwaggerProperties(globalOperationParameters);
}
Set<String> docketNames = docketOperationParameters.stream()
.map(SwaggerDocProperties.GlobalOperationParameter::getName)
.collect(Collectors.toSet());
List<SwaggerDocProperties.GlobalOperationParameter> resultOperationParameters = newArrayList();
if (Objects.nonNull(globalOperationParameters)) {
for (SwaggerDocProperties.GlobalOperationParameter parameter : globalOperationParameters) {
if (!docketNames.contains(parameter.getName())) {
resultOperationParameters.add(parameter);
}
}
}
resultOperationParameters.addAll(docketOperationParameters);
return this.buildGlobalOperationParametersFromSwaggerProperties(resultOperationParameters);
}
/**
* 配置默认的全局鉴权策略; 其中返回的 SecurityReference 中,reference 即为 ApiKey 对象里面的 name,保持一致才能开启全局鉴权
*
* @return list list
* @since 1.4.0
*/
@NotNull
private @Unmodifiable List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Collections.singletonList(SecurityReference.builder()
.reference(this.swaggerProperties.getAuthorization().getName())
.scopes(authorizationScopes).build());
}
/**
* 配置基于 BasicAuth 的鉴权对象
*
* @return basic auth
* @since 1.4.0
*/
@NotNull
@Contract(" -> new")
private BasicAuth basicAuth() {
return new BasicAuth(this.swaggerProperties.getAuthorization().getName());
}
/**
* 配置基于 ApiKey 的鉴权对象
*
* @return api key
* @since 1.4.0
*/
@NotNull
@Contract(" -> new")
private ApiKey apiKey() {
return new ApiKey(this.swaggerProperties.getAuthorization().getName(),
this.swaggerProperties.getAuthorization().getKeyName(),
ApiKeyVehicle.HEADER.getValue());
}
/**
* 设置全局响应消息
*
* @param swaggerProperties swaggerProperties 支持 POST,GET,PUT,PATCH,DELETE,HEAD,OPTIONS,TRACE
* @param docketForBuilder swagger docket builder
* @since 1.4.0
*/
private void buildGlobalResponseMessage(@NotNull SwaggerDocProperties swaggerProperties, @NotNull Docket docketForBuilder) {
SwaggerDocProperties.GlobalResponseMessage globalResponseMessages = swaggerProperties.getGlobalResponseMessage();
/* POST,GET,PUT,PATCH,DELETE,HEAD,OPTIONS,TRACE 响应消息体 **/
List<ResponseMessage> postResponseMessages = this.getResponseMessageList(globalResponseMessages.getPost());
List<ResponseMessage> getResponseMessages = this.getResponseMessageList(globalResponseMessages.getGet());
List<ResponseMessage> putResponseMessages = this.getResponseMessageList(globalResponseMessages.getPut());
List<ResponseMessage> patchResponseMessages = this.getResponseMessageList(globalResponseMessages.getPatch());
List<ResponseMessage> deleteResponseMessages = this.getResponseMessageList(globalResponseMessages.getDelete());
List<ResponseMessage> headResponseMessages = this.getResponseMessageList(globalResponseMessages.getHead());
List<ResponseMessage> optionsResponseMessages = this.getResponseMessageList(globalResponseMessages.getOptions());
List<ResponseMessage> trackResponseMessages = this.getResponseMessageList(globalResponseMessages.getTrace());
docketForBuilder.useDefaultResponseMessages(swaggerProperties.isApplyDefaultResponseMessages())
.globalResponseMessage(RequestMethod.POST, postResponseMessages)
.globalResponseMessage(RequestMethod.GET, getResponseMessages)
.globalResponseMessage(RequestMethod.PUT, putResponseMessages)
.globalResponseMessage(RequestMethod.PATCH, patchResponseMessages)
.globalResponseMessage(RequestMethod.DELETE, deleteResponseMessages)
.globalResponseMessage(RequestMethod.HEAD, headResponseMessages)
.globalResponseMessage(RequestMethod.OPTIONS, optionsResponseMessages)
.globalResponseMessage(RequestMethod.TRACE, trackResponseMessages);
}
/**
* 获取返回消息体列表
*
* @param globalResponseMessageBodyList 全局 Code 消息返回集合
* @return response message list
* @since 1.4.0
*/
@NotNull
private List<ResponseMessage> getResponseMessageList(
@NotNull List<SwaggerDocProperties.GlobalResponseMessageBody> globalResponseMessageBodyList) {
List<ResponseMessage> responseMessages = new ArrayList<>();
for (SwaggerDocProperties.GlobalResponseMessageBody globalResponseMessageBody : globalResponseMessageBodyList) {
ResponseMessageBuilder responseMessageBuilder = new ResponseMessageBuilder();
responseMessageBuilder.code(globalResponseMessageBody.getCode()).message(globalResponseMessageBody.getMessage());
if (!StringUtils.isEmpty(globalResponseMessageBody.getModelRef())) {
responseMessageBuilder.responseModel(new ModelRef(globalResponseMessageBody.getModelRef()));
}
responseMessages.add(responseMessageBuilder.build());
}
return responseMessages;
}
/**
* Sets bean factory *
*
* @param beanFactory bean factory
* @throws BeansException beans exception
* @since 1.4.0
*/
@Override
public void setBeanFactory(@NotNull BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
@@ -0,0 +1,383 @@
package com.aivfo.doc.autoconfigure.swagger;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import springfox.documentation.swagger.web.DocExpansion;
import springfox.documentation.swagger.web.ModelRendering;
import springfox.documentation.swagger.web.OperationsSorter;
import springfox.documentation.swagger.web.TagsSorter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* <p>Company: 成都返空汇网络技术有限公司</p>
* <p>Description: </p>
*
* @author dong4j
* @version 1.2.3
* @email "mailto:dongshijie@fkhwl.com"
* @date 2020.01.27 14:54
* @since 1.0.0
*/
@Data
@ConfigurationProperties(prefix = SwaggerDocProperties.PREFIX)
public class SwaggerDocProperties {
/**
* PREFIX
*/
public static final String PREFIX = "aivfo.doc.swagger";
/**
* 是否开启 swagger
*/
private boolean enabled;
/**
* 标题
*/
private String title = "";
/**
* 描述
*/
private String description = "";
/**
* 版本
*/
private String version = "";
/**
* 许可证
*/
private String license = "";
/**
* 许可证 URL
*/
private String licenseUrl = "";
/**
* 服务条款 URL
*/
private String termsOfServiceUrl = "";
/**
* 忽略的参数类型
*/
private List<Class<?>> ignoredParameterTypes = new ArrayList<>();
/**
* swagger 会解析的包路径
*/
private String basePackage = "com.aivfo.doc";
/**
* swagger 会解析的 url 规则
*/
private List<String> basePath = new ArrayList<>();
/**
* 在 basePath 基础上需要排除的 url 规则
*/
private List<String> excludePath = new ArrayList<>();
/**
* 分组文档
*/
private Map<String, DocketInfo> docket = new LinkedHashMap<>();
/**
* host 信息
*/
private String host = "";
/**
* 全局参数配置
*/
private List<GlobalOperationParameter> globalOperationParameters;
/**
* Contact
*/
private Contact contact = new Contact();
/**
* 页面功能配置
*/
private UiConfig uiConfig = new UiConfig();
/**
* 是否使用默认预定义的响应消息 ,默认 true
*/
private boolean applyDefaultResponseMessages = Boolean.TRUE;
/**
* 全局响应消息
*/
private GlobalResponseMessage globalResponseMessage;
/**
* 全局统一鉴权配置
*/
private Authorization authorization = new Authorization();
@Data
@NoArgsConstructor
public static class GlobalOperationParameter {
/**
* 参数名
*/
private String name;
/**
* 描述信息
*/
private String description;
/**
* 指定参数类型
*/
private String modelRef;
/**
* 参数放在哪个地方:header,query,path,body.form
*/
private String parameterType;
/**
* 参数是否必须传
*/
private String required;
}
@Data
@NoArgsConstructor
@Accessors(chain = true)
@AllArgsConstructor
public static class DocketInfo {
/**
* 标题
*/
private String title = "";
/**
* 描述
*/
private String description = "";
/**
* 版本
*/
private String version = "";
/**
* 许可证
*/
private String license = "";
/**
* 许可证 URL
*/
private String licenseUrl = "";
/**
* 服务条款 URL
*/
private String termsOfServiceUrl = "";
/**
* Contact
*/
private Contact contact = new Contact();
/**
* swagger 会解析的包路径
*/
private String basePackage = "";
/**
* swagger 会解析的 url 规则
*/
private List<String> basePath = new ArrayList<>();
/**
* 在 basePath 基础上需要排除的 url 规则
*/
private List<String> excludePath = new ArrayList<>();
/**
* Global operation parameters
*/
private List<GlobalOperationParameter> globalOperationParameters;
/**
* 忽略的参数类型
*/
private List<Class<?>> ignoredParameterTypes = new ArrayList<>();
}
/**
* The type Contact.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Contact {
/**
* 联系人
*/
private String name = "";
/**
* 联系人 url
*/
private String url = "";
/**
* 联系人 email
*/
private String email = "";
}
/**
* The type Global response message.
*/
@Data
@NoArgsConstructor
public static class GlobalResponseMessage {
/**
* POST 响应消息体
*/
List<GlobalResponseMessageBody> post = new ArrayList<>();
/**
* GET 响应消息体
*/
List<GlobalResponseMessageBody> get = new ArrayList<>();
/**
* PUT 响应消息体
*/
List<GlobalResponseMessageBody> put = new ArrayList<>();
/**
* PATCH 响应消息体
*/
List<GlobalResponseMessageBody> patch = new ArrayList<>();
/**
* DELETE 响应消息体
*/
List<GlobalResponseMessageBody> delete = new ArrayList<>();
/**
* HEAD 响应消息体
*/
List<GlobalResponseMessageBody> head = new ArrayList<>();
/**
* OPTIONS 响应消息体
*/
List<GlobalResponseMessageBody> options = new ArrayList<>();
/**
* TRACE 响应消息体
*/
List<GlobalResponseMessageBody> trace = new ArrayList<>();
}
/**
* The type Global response message body.
*/
@Data
@NoArgsConstructor
public static class GlobalResponseMessageBody {
/**
* 响应码
*/
private int code;
/**
* 响应消息
*/
private String message;
/**
* 响应体
*/
private String modelRef;
}
/**
* The type Ui config.
*/
@Data
@NoArgsConstructor
public static class UiConfig {
/**
* Api sorter
*/
private String apiSorter = "alpha";
/**
* 是否启用 json 编辑器
*/
private Boolean jsonEditor = false;
/**
* 是否显示请求头信息
*/
private Boolean showRequestHeaders = true;
/**
* 支持页面提交的请求类型
*/
private String submitMethods = "get,post,put,delete,patch";
/**
* 请求超时时间
*/
private Long requestTimeout = 10000L;
/**
* Deep linking
*/
private Boolean deepLinking;
/**
* Display operation id
*/
private Boolean displayOperationId;
/**
* Default models expand depth
*/
private Integer defaultModelsExpandDepth;
/**
* Default model expand depth
*/
private Integer defaultModelExpandDepth;
/**
* Default model rendering
*/
private ModelRendering defaultModelRendering;
/**
* 是否显示请求耗时,默认 false
*/
private Boolean displayRequestDuration = true;
/**
* 可选 none | list
*/
private DocExpansion docExpansion;
/**
* Boolean=false OR String
*/
private Object filter;
/**
* Max displayed tags
*/
private Integer maxDisplayedTags;
/**
* Operations sorter
*/
private OperationsSorter operationsSorter;
/**
* Show extensions
*/
private Boolean showExtensions;
/**
* Tags sorter
*/
private TagsSorter tagsSorter;
/**
* Network
*/
private String validatorUrl;
}
/**
* securitySchemes 支持方式之一 ApiKey
*/
@Data
@NoArgsConstructor
public static class Authorization {
/**
* 鉴权策略 ID,对应 SecurityReferences ID
*/
private String name = "Authorization";
/**
* 鉴权策略,可选 ApiKey | BasicAuth | None,默认 ApiKey
*/
private String type = "BasicAuth";
/**
* 鉴权传递的 Header 参数
*/
private String keyName = "X-Client-Token";
/**
* 需要开启鉴权 URL 的正则
*/
private String authRegex = "^.*$";
}
}
@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.aivfo.doc.autoconfigure.knife4j.Knife4jAutoConfiguration,\
com.aivfo.doc.autoconfigure.swagger.SwaggerDocAutoConfiguration
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>aivfo-doc-spring-boot</artifactId>
<groupId>com.aivfo</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>aivfo-doc-spring-boot-core</artifactId>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,114 @@
package com.aivfo.doc.core.common.schema;
import com.aivfo.el.starter.base.Result;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import springfox.documentation.schema.Types;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.OperationModelsProviderPlugin;
import springfox.documentation.spi.service.contexts.RequestMappingContext;
import springfox.documentation.swagger.common.SwaggerPluginSupport;
import javax.annotation.Resource;
import java.lang.annotation.Annotation;
/**
* 包装返回值为 Result
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class AivfoOperationModelsProviderPlugin implements OperationModelsProviderPlugin {
/**
* Annotation
*/
private final Class<? extends Annotation> filterAnnotationClass;
/**
* Type resolver
*/
@Resource
private TypeResolver typeResolver;
/**
* Fkh operation models provider plugin
*
* @param filterAnnotationClass filterAnnotationClass
* @since 1.7.0
*/
public AivfoOperationModelsProviderPlugin(Class<? extends Annotation> filterAnnotationClass) {
this.filterAnnotationClass = filterAnnotationClass;
}
/**
* Apply
*
* @param context context
* @since 1.7.0
*/
@Override
public void apply(RequestMappingContext context) {
if (this.findAnnotationIfNotNull(context)) {
this.addInputParams(context);
this.addReturnType(context);
}
}
/**
* Find annotation if not null
*
* @param context context
* @return the boolean
* @since 1.7.0
*/
public boolean findAnnotationIfNotNull(RequestMappingContext context) {
if (null == this.filterAnnotationClass) {
return true;
}
return context.findAnnotation(filterAnnotationClass).isPresent();
}
/**
* Add return type
*
* @param context context
* @since 1.7.0
*/
private void addReturnType(RequestMappingContext context) {
ResolvedType returnType = context.getReturnType();
// 兼容,如果方法返回值本身写的 Result 就不包装了
if (Result.class.equals(returnType.getErasedType())) {
return;
}
if (Types.isVoid(returnType)) {
returnType = this.typeResolver.resolve(Void.class);
}
ResolvedType packageType = context.alternateFor(this.typeResolver.resolve(Result.class, returnType));
context.operationModelsBuilder().addReturn(packageType);
}
/**
* Add input params
*
* @param context context
* @since 1.7.0
*/
private void addInputParams(RequestMappingContext context) {
for (ResolvedMethodParameter parameter : context.getParameters()) {
ResolvedType modelType = context.alternateFor(parameter.getParameterType());
context.operationModelsBuilder().addInputParam(modelType);
}
}
/**
* Supports
*
* @param delimiter delimiter
* @return the boolean
* @since 1.7.0
*/
@Override
public boolean supports(DocumentationType delimiter) {
return SwaggerPluginSupport.pluginDoesApply(delimiter);
}
}
@@ -0,0 +1,119 @@
package com.aivfo.doc.core.common.schema;
import com.aivfo.el.starter.base.Result;
import com.aivfo.el.starter.base.utils.StringPool;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.collect.Sets;
import org.springframework.core.annotation.Order;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.*;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.contexts.ModelContext;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spring.web.readers.operation.ResponseMessagesReader;
import springfox.documentation.swagger.common.SwaggerPluginSupport;
import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.util.Optional;
/**
* <p>Description: 最后一位执行,修改200状态码显示的 schema </p>
*/
@Order
public class AivfoOperationResponseMessagePlugin implements OperationBuilderPlugin {
/**
* Annotation
*/
private final Class<? extends Annotation> filterAnnotationClass;
/**
* Name extractor
*/
@Resource
private TypeNameExtractor nameExtractor;
/**
* Type resolver
*/
@Resource
private TypeResolver typeResolver;
/**
* Fkh operation models provider plugin
*
* @param filterAnnotationClass filterAnnotationClass
* @since 1.7.0
*/
public AivfoOperationResponseMessagePlugin(Class<? extends Annotation> filterAnnotationClass) {
this.filterAnnotationClass = filterAnnotationClass;
}
/**
* Apply
*
* @param operationContext operation context
* @since 1.7.0
*/
@Override
public void apply(OperationContext operationContext) {
if (this.findAnnotationIfNotNull(operationContext)) {
ResolvedType returnType = operationContext.getReturnType();
// 兼容,如果方法返回值本身写的 Result 就不包装了
if (Result.class.equals(returnType.getErasedType())) {
return;
}
if (Types.isVoid(returnType)) {
returnType = this.typeResolver.resolve(Void.class);
}
ResolvedType packageType = operationContext.alternateFor(this.typeResolver.resolve(Result.class, returnType));
ModelContext modelContext = ModelContext.returnValue(StringPool.EMPTY,
operationContext.getGroupName(),
packageType,
Optional.empty(),
operationContext.getDocumentationType(),
operationContext.getAlternateTypeProvider(),
operationContext.getGenericsNamingStrategy(),
operationContext.getIgnorableParameterTypes());
int httpStatusCode = ResponseMessagesReader.httpStatusCode(operationContext);
String message = ResponseMessagesReader.message(operationContext);
ModelReference modelRef =
ResolvedTypes.modelRefFactory(modelContext, new JacksonEnumTypeDeterminer(), this.nameExtractor).apply(packageType);
ResponseMessage built = new ResponseMessageBuilder()
.code(httpStatusCode)
.message(message)
.responseModel(modelRef)
.build();
operationContext.operationBuilder().responseMessages(Sets.newHashSet(built));
}
}
/**
* Find annotation if not null
*
* @param context context
* @return the boolean
* @since 1.7.0
*/
public boolean findAnnotationIfNotNull(OperationContext context) {
if (null == this.filterAnnotationClass) {
return true;
}
return context.findAnnotation(this.filterAnnotationClass).isPresent();
}
/**
* Supports
*
* @param delimiter delimiter
* @return the boolean
* @since 1.7.0
*/
@Override
public boolean supports(DocumentationType delimiter) {
return SwaggerPluginSupport.pluginDoesApply(delimiter);
}
}
@@ -0,0 +1,18 @@
package com.aivfo.doc.core.knife4j;
import java.lang.annotation.*;
/***
* Enable Knife4j enhanced annotation and use @EnableSwagger2 annotation together.
*
* inlude:
* <ul>
* <li>Interface sorting </li>
* <li>Interface document download (word)</li>
* </ul>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface EnableKnife4j {
}
@@ -15,5 +15,20 @@
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.aivfo</groupId>
<artifactId>aivfo-doc-spring-boot-autoconfigure</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,23 @@
package com.aivfo.doc;
import com.aivfo.doc.core.knife4j.EnableKnife4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* <p>Company: 成都返空汇网络技术有限公司</p>
* <p>Description: </p>
*
* @author dong4j
* @version 1.4.0
* @email "mailto:dongshijie@fkhwl.com"
* @date 2020.05.08 16:16
* @since 1.4.0
*/
@EnableKnife4j
@SpringBootApplication
public class SampleKnife4jApplication {
public static void main(String[] args) {
SpringApplication.run(SampleKnife4jApplication.class, args);
}
}
@@ -0,0 +1,134 @@
package com.aivfo.doc;
import com.aivfo.el.start.core.api.R;
import com.aivfo.el.start.core.exception.BaseException;
import com.aivfo.el.starter.base.Result;
import io.swagger.annotations.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.Date;
/**
* <p>Company: 成都返空汇网络技术有限公司</p>
* <p>Description: </p>
*
* @author dong4j
* @version 1.0.0
* @email "mailto:dongshijie@fkhwl.com"
* @date 2020.01.27 18:19
* @since 1.4.0
*/
@Api(tags = "swagger test api")
@RestController
public class TestController {
/**
* Find by id result.
*
* @param id the id
* @return the result
* @since 1.4.0
*/
@GetMapping("/user/{id}")
@ApiOperation(value = "获取用户详情", notes = "xxx", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses( {
@ApiResponse(code = 2000, message = "操作成功", response = R.class),
@ApiResponse(code = 6000, message = "参数校验失败", response = R.class),
@ApiResponse(code = 4000, message = "用户不存在", response = R.class)
})
public Result<Void> findById(@Valid @NotNull @Size(min = 1) @PathVariable("id") Long id) {
return R.succeed();
}
/**
* Save result.
*
* @param user the user
* @return the result
* @since 1.4.0
*/
@PostMapping("/user")
@ApiOperation(value = "创建新用户", notes = "传入一个 User 实体", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<Void> save(@Valid @RequestBody User user) {
return R.succeed();
}
/**
* Update result.
*
* @param user the user
* @return the result
* @since 1.4.0
*/
@PutMapping("/user")
@ApiOperation(value = "修改用户信息", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<String> update(@Valid @RequestBody User user) {
return R.succeed();
}
/**
* Delete by id result.
*
* @param id the id
* @return the result
* @since 1.4.0
*/
@DeleteMapping("/user/{id}")
@ApiOperation(value = "删除用户", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<String> deleteById(@Valid @NotNull @Size(min = 1) @PathVariable("id") Long id) {
if(id>10L)
{
throw new BaseException("123");
}
return R.succeed("delete user : " + id);
}
/**
* List.
*
* @param pageIndex the page index
* @param pageSize the page size
* @return the list
* @since 1.4.0
*/
@GetMapping("/user")
@ApiOperation(value = "用户列表", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<Void> list(@ApiParam("查看第几页") @RequestParam(value = "pageIndex", required = false) Integer pageIndex,
@ApiParam("每页多少条") @RequestParam(value = "pageSize", required = false) Integer pageSize) {
return R.succeed();
}
/**
* <p>Company: 成都返空汇网络技术有限公司 </p>
* <p>Description: </p>
*
* @author dong4j
* @version 1.4.0
* @email "mailto:dongshijie@fkhwl.com"
* @date 2020.05.08 17:27
* @since 1.4.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
private static class User {
/** Username */
@NotBlank(message = "用户名不能为空")
private String username;
/** Version */
private String version;
/** Date */
private Date date;
/** Age */
@Max(150)
@Min(1)
private Integer age;
}
}
@@ -0,0 +1,9 @@
aivfo:
doc:
swagger:
title: swagger-plus test
description: 描述信息
version: 2022.1.1-SNAPSHOT
license: xxxx
license-url: xxxxx
base-package: com.aivfo.doc
+1
View File
@@ -14,5 +14,6 @@
<modules>
<module>aivfo-doc-spring-boot-autoconfigure</module>
<module>aivfo-doc-spring-boot-start</module>
<module>aivfo-doc-spring-boot-core</module>
</modules>
</project>