Swagger扩展 - 同一个接口生成多份Swagger API文档

news2024/11/19 15:20:20

为同一个@ApiOperation生成多份不同Swagger API文档。

0. 目录

      • 1. 背景
      • 2. 效果展示
      • 3. 实现
        • 3.1 关键逻辑 - 让接口自解释
        • 3.2 关键逻辑 - 如何生成相应的ApiDescription
        • 3.3 关键逻辑 - 如何为生成的ApiDescription 赋值
        • 3.4 关键逻辑 - 如何动态生成Docket
      • 4. 继续优化
      • 5. 参考

1. 背景

正规的软件开发流程里,要求都是先做设计,再进行开发;先写文档,再写实现。只是面对现实的时候,往往都是代码先行,文档则是之后有需要再说。

这个背景下,Swagger这类根据代码生成文档的工具也就有了连绵不绝的生命力。

而同样是在实际场景里,我们往往会因为某些原因,例如"遵循业内的既有标准",亦或是"降低接口使用者的使用门槛,减少心智负担"等等,让同一个接口实现多种功能。举个具体的例子就是,对于一些发布服务,发起流程类的接口,往往都是提供同一个URL入口地址,然后通过参数的不同来进行发起不同服务或流程的操作。

但世界上没有完美的事情,Swagger默认是一个@ApiOperation生成一个对应的API文档说明,但上面所举例的"发布服务,发起流程"中又是需要通过不同的传参来进行区分,这样矛盾就产生了 —— Swagger的默认实现里无法在不进行额外说明的情况下,让使用者自行分辨出不同的服务/流程需要传递哪些参数,以及哪些参数的基本校验规则。(当然你可以强行将所有的参数放在一个参数实体类里面,然后为不同的服务/流程类型创建不同的类型作为前面参数实体类的字段,组成这样的层级结构确实也可以减缓上面这个矛盾,但相信这个实现之下,Swagger前端展示时,浏览器的滚动条一定相当深)。

以上矛盾之下,这导致对于这类接口的使用时,使用者需要消耗大量的成本在反复的沟通确认上。人员更迭或时间长久之后,同样程度的沟通又得从头开始再来一次。使用者和提供者对此都是心力憔悴。

过往我们尝试通过编写文档来缓解上面的问题,但代码和文档分离的结果就是缺乏及时性,也缺乏快速验证的途径。

本文中我们尝试大幅缓解这个问题。借助Swagger扩展,实现同一个接口下,不同的服务/流程能够生成不同的swagger文档,以期提升接口文档的及时性和可验证性,从而大幅降低沟通成本。

2. 效果展示

以下为同一个接口所生成的两个Swagger API文档(可以看到它们是同一个url地址, 不同的请求参数):

  1. 流程服务1
    流程服务1
  2. 流程服务2
    流程服务2

3. 实现

首先让我们列举下实现这个需求过程中可能遇到的一些难点,然后针对性地进行解决方案介绍。

  1. 如何让目标接口拥有自解释"自己这个接口需要被生成多个API文档"的能力。
  2. 在第一步基础上,如何生成相应的API文档所对应的Swagger内部数据结构,参与到Swagger生命周期中,最小化我们的工作量。
  3. 如何为第二步生成的Swagger内部数据结构赋值。注意这里的赋值分为了入参和出参的解析赋值,以及其它诸如http method等的赋值。这一部分我们放在下面专门的小节里进行介绍。
  4. 如何让最终生成并且填充完毕的Swagger内部数据结构在前端页面正常展示。在Swagger的 restful接口返回值(准确说是openapi返回值规范)中,接口的唯一性是由uri所实现的,但本文我们的需求里恰恰就是要求生成的多个接口文档拥有相同的uri,这个矛盾应该如何缓解?

针对以上几个问题,有以下让我们分别进行解决。

3.1 关键逻辑 - 让接口自解释

这里我们采用的是自定义注解的方式。然后通过在某个接口上标注相应个数的注解,来声明该接口需要生成相应个数的API文档,同时通过声明相应的注解属性来兼顾个性化需求和通用性处理之间的平衡。

相关代码如下:

// ============================================================ 自定义注解
// 使用Java8的"重复性注解"特性, 优化使用体验.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface FlowTypes {
	FlowType[] value();
}

// 代表一个被支持的流程类型
@Repeatable(FlowTypes.class)
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FlowType {
	// 流程类型的名称
	String name();
	// 发起该流程时候, 需要传递的参数类型. 很明显, 我们建议使用自定义类型封装所有的请求参数
	Class<?> paramType();
}

// ============================================================
// 为了后面解析的方便, 这里我们还是使用了java8以前的使用方式.
// 以下就是代表该接口支持发送两类流程服务
@FlowTypes({ //
		@FlowType(name = "流程服务1", paramType = FlowServiceParams1.class), //
		@FlowType(name = "流程服务2", paramType = FlowServiceParams2.class) //
})
@ApiOperation(value="createFlowService")
@ResponseBody
@PostMapping("/createFlowService")
public Result createFlowService() {
	return new Result();
}

3.2 关键逻辑 - 如何生成相应的ApiDescription

这里我们采取的是实现Swagger提供的对外扩展接口ApiListingScannerPlugin

直接上代码。

@Component
public class SwaggerAddApiDescriptionPlugin implements ApiListingScannerPlugin {

	private final TypeResolver typeResolver;

  /**
   * 参考 ApiDescriptionLookup.java
   */
	private Map<RequestHandler, GeneratedApis> needDealed = new HashMap<>();
	
	public Map<RequestHandler, GeneratedApis> getNeedDealed() {
		return Collections.unmodifiableMap(needDealed);
	}	

	@Autowired
	public SwaggerAddApiDescriptionPlugin(TypeResolver typeResolver) {
		this.typeResolver = typeResolver;
	}

	@Override
	public List<ApiDescription> apply(DocumentationContext documentationContext) {
		return generateApiDesc(documentationContext);
	}

	private List<ApiDescription> generateApiDesc(final DocumentationContext documentationContext) {
		List<RequestHandler> requestHandlers = documentationContext.getRequestHandlers();
		List<ApiDescription> newArrayList = new ArrayList<>();
		requestHandlers.stream().filter(s -> s.findAnnotation(FlowTypes.class).isPresent())
				.forEach(handler -> {
					List<ApiDescription> apiDescriptions = addApiDescriptions(documentationContext, handler);
					newArrayList.addAll(apiDescriptions);
					if (!apiDescriptions.isEmpty()) {
						needDealed.put(handler, GeneratedApis.builder().ads(apiDescriptions).build());
					}					
				});
		
		return newArrayList;
	}

	private List<ApiDescription> addApiDescriptions(DocumentationContext documentationContext,
			RequestHandler handler) {
		Optional<FlowTypes> annotation = handler.findAnnotation(FlowTypes.class);
		List<ApiDescription> apiDescriptionList = new ArrayList<>();
		if (annotation.isPresent()) {
			FlowTypes FlowTypes = annotation.get();
			String tagName = FlowTypes.name();
			// 确保归类在不同的group下, 以实现相同path的共存
			Arrays.stream(FlowTypes.value()).filter(FlowType -> FlowType.name()
					.equalsIgnoreCase(documentationContext.getGroupName()))
					.forEach(FlowType -> apiDescriptionList
							.addAll(addApiDescription(handler, documentationContext, FlowType, tagName)));
		}
		return apiDescriptionList;
	}

	private List<ApiDescription> addApiDescription(RequestHandler handler,
			DocumentationContext documentationContext,
			FlowType FlowType, String tagName) {
		RequestHandlerKey requestHandlerKey = handler.key();
		final String value = FlowType.value();
		OperationBuilder operationBuilder = new OperationBuilder(new CachingOperationNameGenerator())
				.summary(value)
				.notes(value)
				.tags(CollUtil.newHashSet(tagName + "-" + value));

		final ApiDescriptionBuilder builder = new ApiDescriptionBuilder(
				documentationContext.operationOrdering());
		builder.description(value)
				.groupName(documentationContext.getGroupName())
				.hidden(false);
		List<ApiDescription> apiDescriptionList = new ArrayList<>();
		Iterator<RequestMethod> methodIterator = requestHandlerKey.getSupportedMethods().iterator();
		Iterator<String> pathIterator = requestHandlerKey.getPathMappings().iterator();
		while (methodIterator.hasNext()) {
			List<Parameter> parameters = createParameter(FlowType,
					requestHandlerKey.getSupportedMediaTypes(), operationBuilder.build().getMethod());
			// 设置参数
			operationBuilder.parameters(parameters);
      operationBuilder.uniqueId(value + IdUtil.fastUUID());
			while (pathIterator.hasNext()) {
				// 设置请求路径
				builder.path(pathIterator.next());
				List<Operation> operations = Arrays.asList(operationBuilder.build());
				apiDescriptionList.add(builder.operations(operations).build());
			}
		}
		return apiDescriptionList;
	}


	/**
	 * 解析参数
	 * @param FlowType
	 * @param consumes
	 * @param method
	 * @return
	 */
	private List<Parameter> createParameter(FlowType FlowType,
			Set<? extends MediaType> consumes, HttpMethod method) {
		final Class<?> paramType = FlowType.dataTypeClass();
		final Map<String, Field> fieldMap = ReflectUtil.getFieldMap(paramType);
		return fieldMap.entrySet().stream().map(kv -> {
			Field field = kv.getValue();
			ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class);
			ParameterBuilder parameterBuilder = new ParameterBuilder();
			ResolvedType resolve = typeResolver.resolve(field.getType());
			return parameterBuilder.description(annotation.value())
					//参数数据类型
					.type(resolve)
					//参数名称
					.name(field.getName())
					//参数默认值
					.defaultValue(annotation.name())
					//参数类型 query、form、formdata
					.parameterType(findParameterType(resolve, consumes, method))
					.parameterAccess(annotation.access())
					//是否必填
					.required(annotation.required())
					//参数数据类型
					.modelRef(modelReference(resolve)).build();
		}).collect(Collectors.toList());
	}

	/**
	 * 设置返回值model
	 * @param type
	 * @return
	 */
	private ModelReference modelReference(ResolvedType type) {
		if (Void.class.equals(type.getErasedType()) || Void.TYPE.equals(type.getErasedType())) {
			return new ModelRef("void");
		}
		if (MultipartFile.class.isAssignableFrom(type.getErasedType())|| isListOfFiles(type)) {
			return new ModelRef("__file");
		}
		return new ModelRef(
				type.getTypeName(),
				type.getBriefDescription(),
				null,
				allowableValues(type),
				type.getBriefDescription());
	}

	private static String findParameterType(ResolvedType resolvedType,
			Set<? extends MediaType> consumes, HttpMethod method) {
		//Multi-part file trumps any other annotations
		if (isFileType(resolvedType) || isListOfFiles(resolvedType)) {
			return "form";
		} else {
			return determineScalarParameterType(consumes, method);
		}
	}

	private static String determineScalarParameterType(Set<? extends MediaType> consumes,
			HttpMethod method) {
		String parameterType = "query";
		if (consumes.contains(MediaType.APPLICATION_FORM_URLENCODED)
				&& method == HttpMethod.POST) {
			parameterType = "form";
		} else if (consumes.contains(MediaType.MULTIPART_FORM_DATA)
				&& method == HttpMethod.POST) {
			parameterType = "formData";
		}
		return parameterType;
	}

	private static boolean isListOfFiles(ResolvedType parameterType) {
		return isContainerType(parameterType) && isFileType(collectionElementType(parameterType));
	}

	private static boolean isFileType(ResolvedType parameterType) {
		return MultipartFile.class.isAssignableFrom(parameterType.getErasedType());
	}
	
	@Override
	public boolean supports(DocumentationType documentationType) {
		return DocumentationType.SWAGGER_2.equals(documentationType);
	}
	
	@Builder(toBuilder = true)
	@Data
	public static class GeneratedApis {
		List<ApiDescription> ads;
		// 来源于哪个group
		//String groupNameOfSource;
	}	

3.3 关键逻辑 - 如何为生成的ApiDescription 赋值

关于这一步,细分之下,其实有三个维度:

  1. 诸如请求方式,请求数据类型等等。
  2. 请求入参。 这一步在上面的步骤二中已经完成了,不够完美,但先凑活用。
  3. 请求返回值。

继续上代码。解决上面三个维度中的第一和第三两个。

/**
 * <p> 搭配 {@code SwaggerAddApiDescriptionPlugin } 实现新增的 ApiDescription属性填充
 * <p> 需要确保执行时机低于 {@code DocumentationPluginsBootstrapper}
 * <p> 但{@code DocumentationPluginsBootstrapper} 这个玩意的执行时机为最低
 * <p> 所以我们转而实现 ApplicationListener<ContextRefreshedEvent>
 * @author fulizhe
 *
 */
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class SwaggerAddAddtionApiDescriptionWithDeferPushValue implements ApplicationListener<ContextRefreshedEvent> {

	private AtomicBoolean initialized = new AtomicBoolean(false);

	private final ApiDescriptionLookup lookup;

	private final SwaggerAddApiDescriptionPlugin swaggerAddApiDescriptionPlugin;

	@Autowired
	private DocumentationCache cocumentationCache;

	public SwaggerAddAddtionApiDescriptionWithDeferPushValue(ApiDescriptionLookup lookup,
			SwaggerAddApiDescriptionPlugin swaggerAddApiDescriptionPlugin) {
		super();
		this.lookup = lookup;
		this.swaggerAddApiDescriptionPlugin = swaggerAddApiDescriptionPlugin;
	}

	void start() {
		if (initialized.compareAndSet(false, true)) {
			if (swaggerAddApiDescriptionPlugin.getNeedDealed().isEmpty()) {
				initialized.compareAndSet(true, false);
				return;
			}
			swaggerAddApiDescriptionPlugin.getNeedDealed().forEach((k, v) -> {
				if (v.ads.isEmpty()) {
					return;
				}

				ApiDescription sourceDescription = lookup.description(k);
				if (!Objects.isNull(sourceDescription)) { // 如果将 OneInterfaceMultiApiDescriptionController.createFlowService() 设置为hidden, 则这里判断失败
					List<ApiDescription> ads = v.ads;
					ApiDescription first = ads.get(0);
					
					// 这里所复制的就是请求方式,请求数据类型等等这些信息
					copyProperties(sourceDescription.getOperations().get(0), first.getOperations().get(0));

					// ============================== 设置返回值
					// 这里的思路是这样的:
					// 1. swagger中对于自定义类型的返回值显示采取的是 ref 引用的方式. (这一点可以随便找个swagger文档F12看一下), 同时将ref所引用的model定义放在整个接口最外层的definitions字段中
					// 2. 在上面的copyProperties(...)中我们已经复制response相关信息, 接下来我们就只需要将definitions相关信息拷贝到当前document之下就大功告成了
					Documentation matchedSourceDocumentationByGroup = matchedSourceDocumentationByGroup(
							sourceDescription);

					Documentation targetDocumentationByGroup = cocumentationCache
							.documentationByGroup(first.getGroupName().get());

					Map<String, List<ApiListing>> tartgetApiListings = targetDocumentationByGroup.getApiListings();

					String srouceGroupName = sourceDescription.getGroupName().get();
					List<ApiListing> list = matchedSourceDocumentationByGroup.getApiListings().get(srouceGroupName);

					// 确保返回值正常显示
					list.forEach(xv -> {
						tartgetApiListings.forEach((yk, yv) -> {
							yv.forEach(m -> ReflectUtil.setFieldValue(m, "models", xv.getModels()));
						});
					});
				}

			});
		}

	}

	private Documentation matchedSourceDocumentationByGroup(ApiDescription sourceDescription) {
		String srouceGroupName = sourceDescription.getGroupName().get();

		Optional<Documentation> findFirst = cocumentationCache.all().values().stream()
				.filter(v -> v.getApiListings().keySet().contains(srouceGroupName)).findFirst();

		return findFirst.get();
	}

	private void copyProperties(Operation src, Operation dest) {
		final HttpMethod method = src.getMethod();
		ReflectUtil.setFieldValue(dest, "method", method);

		final ModelReference responseModelOfSource = src.getResponseModel();
		ReflectUtil.setFieldValue(dest, "responseModel", responseModelOfSource);

		final int position = src.getPosition();
		ReflectUtil.setFieldValue(dest, "position", position);

		final Set<String> produces = src.getProduces();
		ReflectUtil.setFieldValue(dest, "produces", produces);

		final Set<String> consumes = src.getConsumes();
		ReflectUtil.setFieldValue(dest, "consumes", consumes);

		final Set<String> protocol = src.getProtocol();
		ReflectUtil.setFieldValue(dest, "protocol", protocol);

		ReflectUtil.setFieldValue(dest, "isHidden", src.isHidden());

		ReflectUtil.setFieldValue(dest, "securityReferences", src.getSecurityReferences());

		ReflectUtil.setFieldValue(dest, "responseMessages", src.getResponseMessages());

		ReflectUtil.setFieldValue(dest, "deprecated", src.getDeprecated());

		ReflectUtil.setFieldValue(dest, "vendorExtensions", src.getVendorExtensions());

		// 不拷貝以下屬性
		//	summary, notes, uniqueId, tags, parameters

		// 無效, 这个拷贝需要目标属性有setXX方法
		//	BeanUtil.copyProperties(src, dest, "parameters", "uniqueId", "summary", "notes", "tags");
	}

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		start();
	}
}

3.4 关键逻辑 - 如何动态生成Docket

在Swagger内部数据类型实例填充完毕之后,就只剩下最后的一个问题:如何让最终生成并且填充完毕的Swagger内部数据结构在前端页面正常展示?

在Swagger的 restful接口返回值(准确说是openapi返回值规范)中,接口的唯一性是由uri所实现的,但本文我们的需求里恰恰就是要求生成的多个接口文档拥有相同的uri。

当下我们采取的让不同的流程服务出现在不同的group之下。
在这里插入图片描述
相关代码如下:

@Configuration
@EnableKnife4j
@EnableSwagger2WebMvc
public class SwaggerConfig {

  private DefaultListableBeanFactory context;
  private RequestMappingHandlerMapping handlerMapping;

  public SwaggerConfig(DefaultListableBeanFactory context,
      RequestMappingHandlerMapping handlerMapping) {
    this.context = context;
    this.handlerMapping = handlerMapping;
    dynamicCreate();
  }

  private void dynamicCreate() {
    // 分组
    Set<String> groupNames = getGroupName();
    // 根据分好的组,循环创建配置类并添加到容器中
    groupNames.forEach(item -> {
      Docket docket = new Docket(DocumentationType.SWAGGER_2)
          .groupName(item)
          .select()
          .apis(RequestHandlerSelectors.basePackage("cn.com.kanq.dynamic")) // 确保生成的Docket扫不到任何可以生成API文档的注解
          .paths(PathSelectors.any())
          .build();
      // 手动将配置类注入到spring bean容器中
      context.registerSingleton("dynamicDocket" + item, docket);
    });
  }

  private Set<String> getGroupName() {
    HashSet<String> set = new HashSet<>();
    Map<RequestMappingInfo, HandlerMethod> mappingHandlerMethods = handlerMapping
        .getHandlerMethods();
    for (Map.Entry<RequestMappingInfo, HandlerMethod> map : mappingHandlerMethods.entrySet()) {
      HandlerMethod method = map.getValue();
      GisServiceTypes gisServiceTypes = method.getMethod().getAnnotation(GisServiceTypes.class);
      if (null != gisServiceTypes) {
        GisServiceType[] value = gisServiceTypes.value();
        for (GisServiceType gisServiceType : value) {
          set.add(gisServiceType.name());
        }
      }
    }
    return set;
  }
}

4. 继续优化

  1. 关于请求参数的解析,最好复用swagger的解析。
  2. 让不同的服务出现在同一个页面之下。

5. 参考

  1. Gitee - easyopen。本文介绍的方法是基于swagger扩展实现,主要目的是站在巨人的肩膀上,最大化复用前辈们的成果。你说我从零开始写一个不就不需要这么多细节扣来扣去了,那么你可以参考下这个库。
  2. SWAGGER除了注解方式之外自定义添加接口,额外定义接口

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/388449.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Spark分布式内存计算框架——Structured Streaming】3. Structured Streaming —— 入门案例:WordCount

1.3 入门案例&#xff1a;WordCount 入门案例与SparkStreaming的入门案例基本一致&#xff1a;实时从TCP Socket读取数据&#xff08;采用nc&#xff09;实时进行词频统计WordCount&#xff0c;并将结果输出到控制台Console。 文档&#xff1a;http://spark.apache.org/docs/2…

一个Bug让人类科技倒退几十年?

大家好&#xff0c;我是良许。 前几天在直播的时候&#xff0c;问了直播间的小伙伴有没人知道「千年虫」这种神奇的「生物」的&#xff0c;居然没有一人能够答得上来的。 所以&#xff0c;今天就跟大家科普一下这个人类历史上最大的 Bug 。 1. 全世界的恐慌 一个Bug会让人类…

Java中的自动类型提升与强制类型转换

一、自动类型提升 自动类型提升是指在程序运行时因为某种情况需要&#xff0c;JVM将较小的数据类型自动转换为较大的数据类型&#xff0c;以保证精度和正确性。在Java中&#xff0c;需要进行类型提升的情况有以下几种&#xff1a; 1. byte、short和char提升为int类型 当运算…

spark sql(五)sparksql支持查询哪些数据源,查询hive与查询mysql的区别

1、数据源介绍 sparksql默认查询的数据源是hive数据库&#xff0c;除此之外&#xff0c;它还支持其它类型的数据源查询&#xff0c;具体的到源码中看一下&#xff1a; 可以看到sparksql支持查询的数据源有CSV、parquet、json、orc、txt、jdbc。这些数据源中前面五个我还能理解&…

【Python】RPA批量生成word文件/重命名及批量删除

批量生成word文件 场景&#xff1a;需要新建多个类似文件名 比如&#xff1a;今天的事例是新建12个文件名为&#xff1a; ​ 保安员考试试卷1及答案.docx ​ 保安员考试试卷2及答案.docx ​ … ​ 保安员考试试卷12及答案.docx 痛点&#xff1a; ​ 手动操作重复性高&a…

目标检测中回归损失函数(L1Loss,L2Loss,Smooth L1Loss,IOU,GIOU,DIOU,CIOU,EIOU,αIOU ,SIOU)

文章目录L-norm Loss 系列L1 LossL2 LossSmooth L1 LossIOU系列IOU &#xff08;2016&#xff09;GIOU &#xff08;2019&#xff09;DIOU &#xff08;2020&#xff09;CIOU &#xff08;2020&#xff09;EIOU &#xff08;2022&#xff09;αIOU (2021)SIOU &#xff08;2022…

【SpringCloud】SpringCloud详解之Eureka实战

目录前言SpringCloud Eureka 注册中心一.服务提供者和服务消费者二.需求三.搭建Eureka-Server四.搭建Eureka-Client(在服务提供者配置:用户订单)前言 微服务中多个服务&#xff0c;想要调用&#xff0c;怎么找到对应的服务呢&#xff1f; 这里有组件的讲解 → SpringCloud组件…

深圳大学《计算机论题》作业:大数据与人工智能技术对人类生活的影响

说明 本作业为小组作业&#xff0c;要求基于一场报告完成&#xff08;即观后感&#xff09;。共分4个小题&#xff0c;讨论人工智能时代的伦理思考。由于版权原因&#xff0c;不提供报告的具体内容&#xff0c;只展示答题内容。 第一题 &#xff08;1&#xff09; 你如何看待…

winform控件PropertyGrid的应用(使运行中的程序能像vistual studio那样设置控件属性)

上周在看别人写的上位机demo代码时&#xff0c;发现创建的项目模板是"Windows 窗体控件库"(如下图) 生成的项目结构像自定义控件库&#xff0c;没有程序入口方法Main&#xff0c;但却很神奇能调试&#xff0c;最后发现原来Vistual Studio启动了一个外挂程序UserContr…

LSM(日志结构合并树)_笔记

WAL&#xff1a;Write Ahead Log 写前日志&#xff0c;顺序日志文件 1 LSM tree的定义 LSM tree&#xff1a; Log-Structured-Merge-Tree&#xff0c;日志结构合并树。 Log-Structured Merge-tree (LSM-tree) is a disk-based data structure designed to provide low-cost …

Linux操作系统学习(了解文件系统动静态库)

文章目录浅谈文件系统了解EXT系列文件系统目录与inode的关系软硬链接动静态库浅谈文件系统 当我们创建一个文件时由两部分组成&#xff1a;文件内容文件属性&#xff0c;即使是空文件也有文件属性 一个文件没有被打开是存储在磁盘中的&#xff0c;而磁盘是计算机中的一个机械…

你想赚的钱不一定属于你

昨天一个同行跟我说&#xff0c;最近有个五十多万的订单&#xff0c;客户是拿着别人家的设计来找的他&#xff0c;跟了也有大半个月了&#xff0c;自己明明报的价格比原设计的公司要低&#xff0c;客户一直说会尽快下的&#xff0c;他原本想着能够从这个订单里赚到几万块&#…

王道计算机组成原理课代表 - 考研计算机 第六章 总线 究极精华总结笔记

本篇博客是考研期间学习王道课程 传送门 的笔记&#xff0c;以及一整年里对 计算机组成 知识点的理解的总结。希望对新一届的计算机考研人提供帮助&#xff01;&#xff01;&#xff01; 关于对 “总线” 章节知识点总结的十分全面&#xff0c;涵括了《计算机组成原理》课程里的…

软件测试用例(3)

按照测试对象划分: 一)界面测试: 1)软件只是一种工具&#xff0c;软件和人的信息交流是通过界面来进行的&#xff0c;界面是软件和用户交流的最直接的一层&#xff0c;界面的设计决定了用户对于我们设计软件的第一映像&#xff0c;界面如同人的面孔&#xff0c;具有最吸引用户的…

Java中String详解(从原理理解经典面试题)

本篇文章我先通过经典面试题&#xff0c;筛选需要观看本篇文章的朋友&#xff0c;然后咱们介绍String的基本特性&#xff0c;通过基本特性就可以找到面试题的答案。最后咱们再深入每个面试题&#xff0c;通过字节码、编译原理、基本特性深入剖析所有的面试题&#xff0c;让大家…

jsp试卷分析管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP试卷分析管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…

三、JavaScript

目录 一、JavaScript和html代码的结合方式 二、javascript和java的区别 1、变量 2、运算 3、数组&#xff08;重点&#xff09; 4、函数 5、重载 6、隐形参数arguments 7、js中的自定义对象 三、js中的事件 四、DOM模型 五、正则表达式 一、JavaScript和html代码的结合方…

代码执行漏洞 | iwebsec

文章目录00-代码执行漏洞原理环境01-eval函数示例命令执行写入webshellbash反弹shell02-assert函数示例webshell03-call_user_func函数示例04-call_user_func_array函数示例总结05-create_function函数示例06-array_map函数示例总结08-preg_replace漏洞函数示例07-preg_replace…

Centos 部署Oracle 11g

Centos 部署Oracle 11g部署Oracle 11g准备工作服务器信息oracle安装包服务器准备oracle环境安装Oracle静默方式配置监听以静默方式建立新库及实例部署Oracle 11g 在SpringMVC模式下开发web项目&#xff0c;必然会使用到关系型数据库来存储数据&#xff0c;目前使用比较多的关系…

18、多维图形绘制

目录 一、三维图形绘制 &#xff08;一&#xff09;曲线图绘制plot3() &#xff08;二&#xff09;网格图绘制 mesh() &#xff08;三&#xff09;曲面图绘制 surf() &#xff08;四&#xff09;光照模型 surfl() &#xff08;五&#xff09;等值线图(等高线图)绘制 cont…