18.springcloud_openfeign之扩展组件二

news2024/12/28 5:29:00

文章目录

  • 一、前言
  • 二、子容器默认组件
    • FeignClientsConfiguration
      • Decoder的注入
      • Contract约定
    • 对注解的支持
      • 对类上注解的支持
      • 对方法上注解的支持
      • 对参数上注解的支持
        • @MatrixVariable
        • @PathVariable
        • @RequestParam
        • @RequestHeader
        • @SpringQueryMap
        • @RequestPart
        • @CookieValue
      • FormattingConversionService
      • Retryer
      • FeignLoggerFactory
      • 属性文件开关
    • FeignAutoConfiguration
      • okHttp
  • 三、总结

一、前言

通过前面的学习, 我们知道了

  1. springcloud_openfeign的@EnableFeignClients注解, 使用@Import注解引入了FeignClientsRegistrar对象, FeignClientsRegistrar是个ImportBeanDefinitionRegistrar类型的对象

  2. 在registerBeanDefinitions方法中会将EnableFeignClients#defaultConfigurationFeignClient#configuration封装成FeignClientSpecification注入到容器中

  3. 自动装配引入了FeignClientsConfiguration类, 它将注入到容器中的FeignClientSpecification注入到了创建的FeignClientFactory对象中, 而FeignClientFactory是springcloud的父子容器工厂, 它会将注入的对象按照容器名称添加到不容的子容器中(**dafult.**开头的会注册到所有子容器中), 并且会将FeignClientsConfiguration最为defaultConfigType注入到所有子容器中

那么这个FeignClientsConfiguration都包含哪些内容呢, 这将是本章我们即将讨论的重点。

二、子容器默认组件

FeignClientsConfiguration

入口

/**
 * 实例化feign子容器工厂对象
 */
@Bean
public FeignClientFactory feignContext() {
    FeignClientFactory context = new FeignClientFactory();
    // 设置子容器实例对象
    context.setConfigurations(this.configurations);
    return context;
}

public class FeignClientFactory extends NamedContextFactory<FeignClientSpecification> {

	public FeignClientFactory() {
		this(new HashMap<>());
	}

	public FeignClientFactory(
        Map<String, ApplicationContextInitializer<GenericApplicationContext>> applicationContextInitializers) {
        // 配置文件类
    super(FeignClientsConfiguration.class, "spring.cloud.openfeign", "spring.cloud.openfeign.client.name",
            applicationContextInitializers);
	}
}

注意这里FeignClientFactory的构造器中super(FeignClientsConfiguration.class..., 这里就是给子容器注入FeignClientsConfiguration配置文件

Decoder

@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
    
    /**
	 * springboot的消息转换器
	 */
	@Autowired
	private ObjectFactory<HttpMessageConverters> messageConverters;
    
    /**
	 * 注入解码器
	 */
	@Bean
	@ConditionalOnMissingBean
	public Decoder feignDecoder(ObjectProvider<HttpMessageConverterCustomizer> customizers) {
		// 支持返回值类型 Optional<T> HttpEntity<> HttpEntity, 普通json
		return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters, customizers)));
	}
}

messageConverters的默认实现如下图
在这里插入图片描述

  1. ByteArrayHttpMessageConverter: web模块

  2. StringHttpMessageConverter: web模块 用来处理`ISO_8859_1字符编码

  3. StringHttpMessageConverter: web模块 用来处理UTF-8字符编码

  4. ResourceHttpMessageConverter: web模块, 用来处理请求内容编码; 例如applicatoin/json; 用于将 HTTP 响应直接转换为一个完整的 Resource 对象(例如文件、URL 资源等),或者将 Resource 对象写入 HTTP 响应。

  5. ResourceRegionHttpMessageConverter: web模块, 用于处理 ResourceRegion 对象,将资源的特定片段(区域)写入 HTTP 响应。它主要用于支持分块传输(如 HTTP 范围请求 Range),在视频流、文件分段下载等场景下很有用

  6. AllEncompassingFormHttpMessageConverter: web模块, 是 Spring 框架中用于处理表单数据(application/x-www-form-urlencoded)和文件上传(multipart/form-data)的核心类。它是一个多功能的 HttpMessageConverter,支持以下两种常见的表单提交方式:

  • application/x-www-form-urlencoded:普通表单提交
  • multipart/form-data:文件上传表单提交。
  1. MappingJackson2HttpMessageConverter: web模块, 用于将 Java 对象和 JSON 数据之间相互转换。它基于 Jackson 库实现,是 Spring MVC 和 Spring Boot 中处理 JSON 数据的核心组件。

同时也支持我们自定义HttpMessageConverterCustomizer, 注意ObjectProvider的使用方法, 它是一个ObjectFactory, 允许注入的对象是为空, 使用getObject或者getIfAvailable方法可以获取到实例对象。和@Autowired(required = false)的区别是ObjectProvider属于懒加载模式。

Decoder的注入

SpringDecoder

public class SpringDecoder implements Decoder {
    /**
	 * 解码
	 */
	@Override
	public Object decode(final Response response, Type type) throws IOException, FeignException {
		// 返回值类型是 1.原始类型  2.泛型参数类型  3.通配符类型
		if (type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType) {
            
			List<HttpMessageConverter<?>> converters = messageConverters.getObject().getConverters();
			customizers.forEach(customizer -> customizer.accept(converters));
            
			@SuppressWarnings({ "unchecked", "rawtypes" })
            // http数据转换器
			HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, converters);
			// 将 HTTP 响应解析为指定的对象; 这里是Object
			return extractor.extractData(new FeignResponseAdapter(response));
		}
		throw new DecodeException(response.status(), "type is not an instance of Class or ParameterizedType: " + type,
				response.request());
	}
}

它提供了对多种不同类型返回值的转换, 例如json, 文件传输等

  1. 它只支持返回类型为
  • 原始类型(Class), 例如Person
  • 泛型参数类型(ParameterizedType), 例如 List
  • 通配符类型(WildcardType), 例如 List<?>
  1. 使用转换器将返回数据Response转换成指定的Type类型

这里@SuppressWarnings({ “unchecked”, “rawtypes” })的作用

  1. unchecked: 用于抑制未进行泛型类型检查的警告。例如,当对一个未经检查的转换进行操作时(如从 Object 转为 List)
  2. rawtypes: 用于抑制"原始类型"相关的警告。即,当使用未指定泛型参数的集合类(例如 List、Map 等)

这里关于HttpMessageConverter的组装方法, 使用的是访问者模式, 是23中设计模式中不常用的一种

ResponseEntityDecoder

public class ResponseEntityDecoder implements Decoder {
    
    @Override
	public Object decode(final Response response, Type type) throws IOException, FeignException {

		// 如果返回类型为HttpEntity<XXX>的参数泛型类型
		if (isParameterizeHttpEntity(type)) {
			// 获取参数泛型类型
			type = ((ParameterizedType) type).getActualTypeArguments()[0];
			// 用decoder解码
			Object decodedObject = this.decoder.decode(response, type);
			// 构建ResponseEntity对象
			return createResponse(decodedObject, response);
		}
		// 返回类型是HttpEntity原始类型, 即不带参数泛型
		else if (isHttpEntity(type)) {
			// 直接丢弃数据, 即不支持返回类型为HttpEntity的情况
			return createResponse(null, response);
		}
		else {
			// 其它类型直接用decoder解码
			return this.decoder.decode(response, type);
		}
	}
}

提供了返回值为HttpEntity类型的支持

  1. 如果返回值类型为HttpEntity<XXX>的参数泛型类型, 那么将返回值解码成具体的泛型类型, 并封装成ResponseEntity返回
  2. 如果返回类型是不带泛型的HttpEntity对象, 只返回响应状态和响应头, 返回的具体数据就直接丢弃了
  3. 其它类型的话当前Decoder类不处理, 直接执行包装的目标对象(即不处理)

OptionalDecoder

public final class OptionalDecoder implements Decoder {
    @Override
    public Object decode(Response response, Type type) throws IOException {
    // 返回值不是Optional类型,直接执行包装的目标对象(即不处理)
    if (!isOptional(type)) {
      return delegate.decode(response, type);
    }
    // 404(找不到目标内容)和204(返回内容为空)状态码,直接返回Optional.empty()
    if (response.status() == 404 || response.status() == 204) {
      return Optional.empty();
    }
    // 获取Optional类型中泛型变量的上界类型; 例如 Optional<? extends Person>,返回Person
    Type enclosedType = Util.resolveLastTypeParameter(type, Optional.class);
    // 将返回值解码为Optional类型
    return Optional.ofNullable(delegate.decode(response, enclosedType));
    }
}

提供了对返回值为Optional的支持, 将返回值解析成Optional中泛型参数的类型, 然后封装成Optional返回

Decoder小结

  1. SpringDecoder提供了对常用返回类型的转换, 例如json, multipart/form-data内容格式
  2. ResponseEntityDecoder提供了对返回值为HttpEntity类型数据的支持
  3. OptionalDecoder提供了对返回值为Optional类型数据的支持

Contract约定

@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
    
    /**
	 * 自定义参数解析器
	 */
    @Autowired(required = false)
	private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

    /**
     * springcloud_openfign的默认注解约定解析器
     * @param feignConversionService	内容转换器
     */
    @Bean
    @ConditionalOnMissingBean
    public Contract feignContract(ConversionService feignConversionService) {
        // url分隔符是否解码, 为true时将斜杆转义符转换为/
        boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
        return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash);
    }
}

springcloud_openfeign默认提供了一个SpringMvcContract覆盖默认的Contract.Default

SpringMvcContract

public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
    
    public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
			ConversionService conversionService, boolean decodeSlash) {
		// 参数注解解析器不能为null; 这里是判null而不是empty
		Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
		// 消息转换器不能为空
		Assert.notNull(conversionService, "ConversionService can not be null.");

		// 获取默认的注解解析转换器
		List<AnnotatedParameterProcessor> processors = getDefaultAnnotatedArgumentsProcessors();
		processors.addAll(annotatedParameterProcessors);
		// 将添加到map中{注解, 解析器}
		annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
		// 消息转换器
		this.conversionService = conversionService;
        // 创建Param.Expander的工厂
		convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
		// 是否将斜杆转义符转换为/
		this.decodeSlash = decodeSlash;
	}
}

这是它最大的一个构造器, 初始化了一些依赖项, 其中注解处理器processors和消息转换器conversionService比较重要

getDefaultAnnotatedArgumentsProcessors

private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {

    List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
    // 对@MatrixVariable注解的支持
    annotatedArgumentResolvers.add(new MatrixVariableParameterProcessor());
    // 对@PathVariable注解的支持; restful风格
    annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
    // 对@RequestParam注解的支持; form表达参数的支持
    annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
    // 对@RequestHeader注解的支持; 请求头参数的支持
    annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
    // 对@SpringQueryMap注解的支持; 请求参数的集合支持; 对应feign原来的@QueryMap注解
    annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
    // 对@RequestPart注解的支持; 允许body参数平铺
    annotatedArgumentResolvers.add(new RequestPartParameterProcessor());
    // 对@CookieValue注解的支持; cookie参数的支持
    annotatedArgumentResolvers.add(new CookieValueParameterProcessor());

    return annotatedArgumentResolvers;
}

这里添加了7个默认的参数注解处理器

对注解的支持

对类上注解的支持

@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
    // 获取类上的RequestMapping注解
    RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class);
    if (classAnnotation != null) {
        LOG.error("Cannot process class: " + clz.getName()
                + ". @RequestMapping annotation is not allowed on @FeignClient interfaces.");
        throw new IllegalArgumentException("@RequestMapping annotation not allowed on @FeignClient interfaces");
    }
    // 类上的CollectionFormat注解
    CollectionFormat collectionFormat = findMergedAnnotation(clz, CollectionFormat.class);
    if (collectionFormat != null) {
        // 设置get请求的集合数据分割符
        data.template().collectionFormat(collectionFormat.value());
    }
}
  1. feign接口类上不支持@RequestMapping注解
  2. 仅支持@CollectionFormat注解, 用来设置当前接口中所有方法的集合参数添加到url上作为参数时的分隔符(默认是&)

不支持类级别的请求头了…

对方法上注解的支持

@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
    // 方法上的CollectionFormat注解
    if (methodAnnotation instanceof CollectionFormat) {
        CollectionFormat collectionFormat = findMergedAnnotation(method, CollectionFormat.class);
        // 设置get请求的集合数据分割符
        data.template().collectionFormat(collectionFormat.value());
    }
    // 判断注解是否是RequestMapping注解, 方法上的非RequestMapping注解直接不处理
    if (!(methodAnnotation instanceof RequestMapping)
            && !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
        return;
    }

    RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
    // HTTP Method
    RequestMethod[] methods = methodMapping.method();
    // 默认使用GET请求
    if (methods.length == 0) {
        methods = new RequestMethod[] { RequestMethod.GET };
    }
    // 只能定义一个请求方式
    checkOne(method, methods, "method");
    data.template().method(Request.HttpMethod.valueOf(methods[0].name()));

    // path
    // @RequestMapping(value = "") 就是@RequestLine中的路径部分
    checkAtMostOne(method, methodMapping.value(), "value");
    if (methodMapping.value().length > 0) {
        // 只取第一个路径参数
        String pathValue = emptyToNull(methodMapping.value()[0]);
        if (pathValue != null) {
            // 从环境变量中替换path中的占位符
            pathValue = resolve(pathValue);
            // Append path from @RequestMapping if value is present on method
            // 添加前缀/
            if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
                pathValue = "/" + pathValue;
            }
            // 追加uri的path部分
            data.template().uri(pathValue, true);
            // 是否将斜杆转义符转换为/
            if (data.template().decodeSlash() != decodeSlash) {
                data.template().decodeSlash(decodeSlash);
            }
        }
    }

    // produces
    // 设置客户端支持的返回数据类型
    parseProduces(data, method, methodMapping);

    // consumes
    // 设置当前方法支持的请求数据类型
    parseConsumes(data, method, methodMapping);

    // headers
    // 设置请求头
    parseHeaders(data, method, methodMapping);

    // params
    // 设置请求参数;RequestMapping注解上的params属性, 追加到url请求参数上
    parseParams(data, method, methodMapping);

    // 参数扩展为空
    data.indexToExpander(new LinkedHashMap<>());
}

方法小结

  1. 方法上支持@CollectionFormat注解, 用来设置当前方法的集合参数添加到url上作为参数时的分隔符(默认是&)
  2. 方法上支持@RequestMapping注解, 并且请求方式(GET/POST/PUT…)只能有一个, @RequestMapping(value = “”) 就是@RequestLine中的路径部分, 也支持使用占位符, 可以从环境上下文中获取值去替换该占位符
  3. @RequestMapping#produces属性实质就是添加的Accept请求头, 用于告诉服务端当前请求需要返回的数据类型, 例如application/json
  4. @RequestMapping#consumes属性实质是添加Content-Type请求头, 用于高速服务端当前请求的参数类型, 例如application/json
  5. @RequestMapping#headers设置当前方法级别的请求头
  6. @RequestMapping#params设置添加到url上的参数, 该参数可以使用占位符, 从环境上下文中获取值

对参数上注解的支持

@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
    boolean isHttpAnnotation = false;

    try {
        // 分页参数
        if (Pageable.class.isAssignableFrom(data.method().getParameterTypes()[paramIndex])) {
            // do not set a Pageable as QueryMap if there's an actual QueryMap param
            // 如果方法的某个参数上有@RequestParam,@SpringQueryMap,@QueryMap注解, 返回true, 否则返回false
            if (!queryMapParamPresent(data)) {
                // 设置当前参数为queryMap参数, 放在url上
                data.queryMapIndex(paramIndex);
                return false;
            }
        }
    }
    catch (NoClassDefFoundError ignored) {
        // Do nothing; added to avoid exceptions if optional dependency not present
    }

    AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(data,
            paramIndex);
    Method method = processedMethods.get(data.configKey());
    // 遍历方法的参数注解
    for (Annotation parameterAnnotation : annotations) {
        // 获取合适的参数注解处理器
        AnnotatedParameterProcessor processor = annotatedArgumentProcessors
                .get(parameterAnnotation.annotationType());
        if (processor != null) {
            Annotation processParameterAnnotation;
            // 创建新的注解 并 支持别名
            processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(parameterAnnotation,
                    method, paramIndex);
            // 参数注解处理器; 这里 |= 等价于 isHttpAnnotation = isHttpAnnotation || processor.processArgument(context,)
            isHttpAnnotation |= processor.processArgument(context, processParameterAnnotation, method);
        }
    }
    // 1.非multipart/form-data类型 2.http注解 3.当前参数没有增强器
    if (!isMultipartFormData(data) && isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) {
        // 获取参数类型描述符;
        // 如果是数组,则返回数组元素类型描述符;
        // 如果是集合,则返回集合元素类型描述符;
        // 如果是Stream,则返回Stream元素类型描述符;
        // 如果是iterable,则返回iterable元素类型描述符;
        // 其它类型返回该类型的类型描述符
        TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex);
        // 判断是否能转换成String类型
        if (conversionService.canConvert(typeDescriptor, STRING_TYPE_DESCRIPTOR)) {
            // 获取该类型的扩展器
            Param.Expander expander = convertingExpanderFactory.getExpander(typeDescriptor);
            if (expander != null) {
                // 设置当前参数的扩展器
                data.indexToExpander().put(paramIndex, expander);
            }
        }
    }
    return isHttpAnnotation;
}

方法小结

这里不介绍feign接口有关分页的部分

  1. 依次用注解处理器对参数注解进行处理, 只要有一个返回true(isHttpAnnotation为true), 那么它将不会被当做body字段被解析(这里说的是直接把参数变量当body参数,而非form参数当body)
  2. 满足一下条件,会给参数添加处理器,该处理器会将参数转成字符串
  • 请求头Content-Type是multipart/form-data
  • 参数处理器返回true(isHttpAnnotation为true), 表示它是一个http注解
  • 该参数上没有参数处理器

返回isHttpAnnotation为true的注解有: @MatrixVariable,@PathVariable,RequestParam,RequestHeader,SpringQueryMap,RequestPart,CookieValue; 下面分别介绍它们

@MatrixVariable

MatrixVariableParameterProcessor

用来处理参数上的MatrixVariable注解

public class MatrixVariableParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<MatrixVariable> ANNOTATION = MatrixVariable.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		int parameterIndex = context.getParameterIndex();
		// 参数类型
		Class<?> parameterType = method.getParameterTypes()[parameterIndex];
		MethodMetadata data = context.getMethodMetadata();
		// 注解value值
		String name = ANNOTATION.cast(annotation).value();

		checkState(emptyToNull(name) != null, "MatrixVariable annotation was empty on param %s.",
				context.getParameterIndex());

		context.setParameterName(name);

		// 参数是map
		if (Map.class.isAssignableFrom(parameterType)) {
			// 给当前位置的参数添加处理器, 该处理器将map转为如k1=v1;k2=v2的字符串
			data.indexToExpander().put(parameterIndex, this::expandMap);
		}
		else {
			// 给当前位置的参数添加处理器, 该处理器将参数转成字符串 格式: ;{MatrixVariable.value}=object.toString
			data.indexToExpander().put(parameterIndex, object -> ";" + name + "=" + object.toString());
		}

        // 注意这里返回的是true, 表示当前是http注解, 不会被当做body参数处理
		return true;
	}

	/**
	 * 将map转为字符串; 格式为 ;k1=v1;k2=v2
	 */
	@SuppressWarnings("unchecked")
	private String expandMap(Object object) {
		Map<String, Object> paramMap = (Map) object;

		return paramMap.keySet().stream().filter(key -> paramMap.get(key) != null)
				.map(key -> ";" + key + "=" + paramMap.get(key).toString()).collect(Collectors.joining());
	}
}

它处理了@MatrixVariable注解, 支持矩阵参数, 例如 ;name=小杜;age=18

  1. 如果参数是map, 那么将该map参数平铺转为;k1=v1;k2=v2形式的字符串
  2. 其它类型的参数直接转为字符串类型
  3. 这里处理完成之后返回了一个true, 表示当前参数的注解是http注解, 它将不会被当做body处理, 并且该参数将会被忽略,仅用来替换占位符

MatrixVariableParameterProcessor给标有@MatrixVariable注解的参数添加了参数处理器

需要注意的是, feign默认对占位符的值进行了u8编码, 而springmvc的@MatrixVariable不支持编码的特殊符号,例如;=, 需要先处理

@PathVariable

PathVariableParameterProcessor

用来处理参数上的@PathVariable注解

public class PathVariableParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<PathVariable> ANNOTATION = PathVariable.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		String name = ANNOTATION.cast(annotation).value();
		checkState(emptyToNull(name) != null, "PathVariable annotation was empty on param %s.",
				context.getParameterIndex());
		context.setParameterName(name);

		MethodMetadata data = context.getMethodMetadata();
		String varName = '{' + name + '}';
		// [^}]: 匹配任意不是右花括号 } 的字符
		// 例如: abc{username:admin}xyz
		String varNameRegex = ".*\\{" + name + "(:[^}]+)?\\}.*";
		// 1.url中不包含占位符路径,也就不需要替换 2.参数不包含变量中的占位符,也就替换不了 3.参数不包含请求头上的占位符, 也就是替换不了请求头上的内容
		if (!data.template().url().matches(varNameRegex) && !containsMapValues(data.template().queries(), varName)
				&& !containsMapValues(data.template().headers(), varName)) {
			// 添加为form参数; 不能用来处理url中的参数、header中的参数、url上的参数变量(例如?a={a}) 只能当为form参数
			data.formParams().add(name);
		}
        // 注意这里返回的是true
		return true;
	}

	private <K, V> boolean containsMapValues(Map<K, Collection<V>> map, V search) {
		Collection<Collection<V>> values = map.values();
		if (values == null) {
			return false;
		}
		for (Collection<V> entry : values) {
			if (entry.contains(search)) {
				return true;
			}
		}
		return false;
	}

}

如果@PathVariable标识的参数不能用来替换url变量、参数变量、请求头上的参数, 那么它将作为form参数, 当做body参数

@RequestParam

RequestParamParameterProcessor

用来处理参数上的@RequestParam注解

public class RequestParamParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<RequestParam> ANNOTATION = RequestParam.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		int parameterIndex = context.getParameterIndex();
		Class<?> parameterType = method.getParameterTypes()[parameterIndex];
		MethodMetadata data = context.getMethodMetadata();

		// 参数为map
		if (Map.class.isAssignableFrom(parameterType)) {
			// 只能有一个map 参数
			checkState(data.queryMapIndex() == null, "Query map can only be present once.");
			// 设置queryMap参数的索引
			data.queryMapIndex(parameterIndex);

			return true;
		}

		RequestParam requestParam = ANNOTATION.cast(annotation);
		String name = requestParam.value();
		// @RequestParam的value属性不能为空
		checkState(emptyToNull(name) != null, "RequestParam.value() was empty on parameter %s of method %s",
				parameterIndex, method.getName());
		context.setParameterName(name);

		// 给name变量对应的值添加"{name}"占位符
		Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
		data.template().query(name, query);
        // 这里返回的true, 它不会被当做body参数来处理
		return true;
	}

}

  1. RequestParamParameterProcessor会将@RequestParam标识的参数用占位符的形式添加到请求url上, 例如@RequestParam("name") String name, 此时请求url上会有?name={name}的参数, 然后将实际的参数经过编码后替换这个占位符。
  2. 如果@RequestParam注解标识的参数是个map, 那么它将会把参数都添加都url上; 此时与feign原生注解@QueryMap以及springcloud_openfeign的@SpringQueryMap作用一样
  3. 它用来给url添加单个参数。
  4. 解析该注解返回的isHttpAnnotation为true, 并且没有加入到form参数中, 所以它不会被解析成body参数
@RequestHeader

RequestHeaderParameterProcessor

用来处理@RequestHeader注解

public class RequestHeaderParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<RequestHeader> ANNOTATION = RequestHeader.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		int parameterIndex = context.getParameterIndex();
		Class<?> parameterType = method.getParameterTypes()[parameterIndex];
		MethodMetadata data = context.getMethodMetadata();

		// @RequestHeader Map 这种格式
		if (Map.class.isAssignableFrom(parameterType)) {
			// @RequestHeader Map参数只能有一个
			checkState(data.headerMapIndex() == null, "Header map can only be present once.");
			data.headerMapIndex(parameterIndex);

			return true;
		}

		String name = ANNOTATION.cast(annotation).value();
		checkState(emptyToNull(name) != null, "RequestHeader.value() was empty on parameter %s", parameterIndex);
		context.setParameterName(name);

		// 添加到请求头 "{name}"到请求头集合中
		Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name));
		data.template().header(name, header);
        
        // 这里返回的true, 它不会被当做body参数来处理
		return true;
	}

}

处理逻辑与@RequestParam一样

  1. 如果@RequestHeader标识的参数是map, 那么它与feign原生的@HeaderMap注解一样, 将map中的参数都添加到请求头上
  2. 如果是单个请求头, 那么会给请求头添加一个占位符的值对象, 然后用该值经过u8编码后替换它
  3. 解析该注解返回的isHttpAnnotation为true, 并且没有加入到form参数中, 所以它不会被解析成body参数
@SpringQueryMap

QueryMapParameterProcessor

用来处理@SpringQueryMap注解

public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<SpringQueryMap> ANNOTATION = SpringQueryMap.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		int paramIndex = context.getParameterIndex();
		MethodMetadata metadata = context.getMethodMetadata();
		if (metadata.queryMapIndex() == null) {
			metadata.queryMapIndex(paramIndex);
		}
		return true;
	}

}

这个注解就比较简单了, 完全是用来替代feign的@QueryMap注解的, 用来将map参数添加到请求url上;

需要注意controller中get请求的参数接受方式, 可以用实体对象批量接收, 也可以用@RequestParam注解单个接收

@RequestPart

RequestPartParameterProcessor

用来处理@RequestPart注解

public class RequestPartParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<RequestPart> ANNOTATION = RequestPart.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		int parameterIndex = context.getParameterIndex();
		MethodMetadata data = context.getMethodMetadata();

		String name = ANNOTATION.cast(annotation).value();
		// @RequestPart注解的value()不能为空
		checkState(emptyToNull(name) != null, "RequestPart.value() was empty on parameter %s", parameterIndex);
		context.setParameterName(name);

		// 添加到formParams
		data.formParams().add(name);
		// 添加一个 {name}到集合中
		Collection<String> names = context.setTemplateParameter(name, data.indexToName().get(parameterIndex));
		// 索引对参数名的映射
		data.indexToName().put(parameterIndex, names);
		return true;
	}

}
  1. @RequestPart注解直接将参数添加到了form中, 那么它将被当做body参数来传递
  2. 内容会经过u8编码传递
@CookieValue

CookieValueParameterProcessor

用来处理@CookieValue注解

public class CookieValueParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<CookieValue> ANNOTATION = CookieValue.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		int parameterIndex = context.getParameterIndex();
		MethodMetadata data = context.getMethodMetadata();
		CookieValue cookie = ANNOTATION.cast(annotation);
		String name = cookie.value().trim();
		// @CookieValue注解的value()不能为空
		checkState(emptyToNull(name) != null, "Cookie.name() was empty on parameter %s", parameterIndex);
		// 索引和名称的映射
		context.setParameterName(name);
		// 请求头上的的Cookie
		String cookieExpression = data.template().headers()
				.getOrDefault(HttpHeaders.COOKIE, Collections.singletonList("")).stream().findFirst().orElse("");
		// 请求头上没有Cookie; 添加占位符的cookie name={name}
		if (cookieExpression.length() == 0) {
			cookieExpression = String.format("%s={%s}", name, name);
		}
		else {
			// 追加Cookie 例如 session=abc; name={name}
			cookieExpression += String.format("; %s={%s}", name, name);
		}
		// 替换请求头上的Cookie
		data.template().removeHeader(HttpHeaders.COOKIE);
		data.template().header(HttpHeaders.COOKIE, cookieExpression);
		return true;
	}
}
  1. 添加cookie到请求头上
  2. 它将会被u8编码
  3. @CookieValue指定的cookie会覆盖请求头上的cookie

FormattingConversionService

/**
 * 默认格式转换器
 */
@Bean
public FormattingConversionService feignConversionService() {
    // 默认格式转换器; 支持了number,datetime,date
    FormattingConversionService conversionService = new DefaultFormattingConversionService();
    for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
        feignFormatterRegistrar.registerFormatters(conversionService);
    }
    return conversionService;
}

它支持格式化与参数转换; 下面是几个案例

public class ConversionTest {

    @Test
    void conversionTest() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

        // 基础类型转换
        Integer number = conversionService.convert("123", Integer.class);
        System.out.println(number); // 输出:123

        // 日期类型转换
        LocalDate date = conversionService.convert("2024-12-25", LocalDate.class);
        System.out.println(date); // 输出:2024-12-25

        // map中的value转为整数
        Map<String, String> sourceMap = new HashMap<>();
        sourceMap.put("key1", "1");
        sourceMap.put("key2", "2");
        TypeDescriptor sourceMapType = TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(String.class));
        TypeDescriptor targetMapType = TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class));
        Object convertedMap = conversionService.convert(sourceMap, sourceMapType, targetMapType);
        System.out.println("Converted Map: " + convertedMap); // 输出: {key1=1, key2=2}

        Method method = ClassUtils.getMethod(ConversionTest.class, "bb", Integer.class);
        Parameter parameter = method.getParameters()[0];
        MethodParameter methodParameter = MethodParameter.forParameter(parameter);
        TypeDescriptor typeDescriptor = new TypeDescriptor(methodParameter);
        Object person = conversionService.convert(20, typeDescriptor, TypeDescriptor.valueOf(String.class));
        System.out.println("convert methodParam:" + person);
    }

    public void bb(Integer age) {

    }
}

格式化

@Test
void formatTest1() {
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
    // 注册数字格式化器
    NumberStyleFormatter numberFormatter = new NumberStyleFormatter();
    numberFormatter.setPattern("#,###.##");
    conversionService.addFormatter(numberFormatter);

    // 转换数字字符串为数字
    String numberStr = "123,456.78";
    Locale locale = Locale.US; // 使用美国区域
    // 设置全局区域
    Locale.setDefault(locale);
    Object number = conversionService.convert(numberStr, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Number.class));
    System.out.println("解析后的数字: " + number); // 输出:123456.78
}

自定义格式化器

/**
 * 自定义格式化器
 */
@Test
void formatTest2() {
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

    // 自定义格式化器:将数字格式化为货币
    conversionService.addFormatter(new Formatter<Double>() {
        @Override
        public Double parse(String text, Locale locale) throws ParseException {
            return Double.parseDouble(text.replace("$", "").replace(",", ""));
        }

        @Override
        public String print(Double object, Locale locale) {
            return String.format(locale, "$%,.2f", object);
        }
    });

    // 测试格式化器
    String formatted = conversionService.convert(12345.678, String.class);
    System.out.println("Formatted value: " + formatted); // 输出:$12,345.68

    Double parsed = conversionService.convert("$12,345.68", Double.class);
    System.out.println("Parsed value: " + parsed); // 输出:12345.68
}

Retryer

springcloud_openfeign默认不允许重试, 可以自定义重试机制

@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
    return Retryer.NEVER_RETRY;
}

  Retryer NEVER_RETRY = new Retryer() {

    @Override
    public void continueOrPropagate(RetryableException e) {
      throw e;
    }

    @Override
    public Retryer clone() {
      return this;
    }
  };

FeignLoggerFactory

@Autowired(required = false)
private Logger logger;

/**
 * 日志工厂
 */
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
    // 日志工厂, 默认是构建Slf4jLogger
    return new DefaultFeignLoggerFactory(logger);
}

public class DefaultFeignLoggerFactory implements FeignLoggerFactory {

	private final Logger logger;

	public DefaultFeignLoggerFactory(Logger logger) {
		this.logger = logger;
	}

	@Override
	public Logger create(Class<?> type) {
		// 默认使用Slf4jLogger
		return this.logger != null ? this.logger : new Slf4jLogger(type);
	}

}

可以看出, springcloud_openfeign默认使用的slf4j作为日志框架, 我们在使用的时候配置logback.xml文件即可

属性文件开关

/**
 * 是否启用全局属性文件配置(即spring.cloud.openfeign.client.config), 默认是true
 */
@Bean
@ConditionalOnMissingBean(FeignClientConfigurer.class)
public FeignClientConfigurer feignClientConfigurer() {
    return new FeignClientConfigurer() {
    };
}

可以使用spring.cloud.openfeign.client.config=true/false来禁用或者启用yaml/yml/properties 配置文件中springcloud_openfeign的相关配置项(用作给feign接口定制参数)

FeignAutoConfiguration

okHttp

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
@ConditionalOnProperty("spring.cloud.openfeign.okhttp.enabled")
protected static class OkHttpFeignConfiguration {
    private okhttp3.OkHttpClient okHttpClient;

    @Bean
    @ConditionalOnMissingBean
    public okhttp3.OkHttpClient.Builder okHttpClientBuilder() {
        return new okhttp3.OkHttpClient.Builder();
    }
    
    /**
     * 连接池配置;
     */
    @Bean
    @ConditionalOnMissingBean(ConnectionPool.class)
    public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties) {
        // 最大连接数
        int maxTotalConnections = httpClientProperties.getMaxConnections();
        // 连接保活时间
        long timeToLive = httpClientProperties.getTimeToLive();
        // 连接保活时间单位
        TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
        return new ConnectionPool(maxTotalConnections, timeToLive, ttlUnit);
    }
    
    @Bean
    public okhttp3.OkHttpClient okHttpClient(okhttp3.OkHttpClient.Builder builder, ConnectionPool connectionPool,
            FeignHttpClientProperties httpClientProperties) {
        // 是否随服务端重定向, 默认是true
        boolean followRedirects = httpClientProperties.isFollowRedirects();
        // 连接超时时长
        int connectTimeout = httpClientProperties.getConnectionTimeout();
        // 默认是false
        boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
        // 读取超时
        Duration readTimeout = httpClientProperties.getOkHttp().getReadTimeout();
        // 协议
        List<Protocol> protocols = httpClientProperties.getOkHttp().getProtocols().stream().map(Protocol::valueOf)
                .collect(Collectors.toList());
        // 禁用ssl
        if (disableSslValidation) {
            disableSsl(builder);
        }
        this.okHttpClient = builder.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                .followRedirects(followRedirects).readTimeout(readTimeout).connectionPool(connectionPool)
                .protocols(protocols).build();
        return this.okHttpClient;
    }
}

使用spring.cloud.openfeign.okhttp.enabled=true/false开启或者禁用okhttp作为请求客户端, 使用spring.cloud.openfeign.httpclient配置相关属性

三、总结

  1. FeignClientsConfiguration中添加了子容器工厂FeignClientFactory,并添加了子容器默认的组件FeignClientsConfiguration
  2. springcloud_openfeign对于支持的参数注解的对象, 可以转为字符串的,都通过ConversionService转成字符串
  3. 对返回值为原始类型,Optional,HttpEntity,HttpEntity的支持; 使用HttpMessageConverter对返回值进行转换
  4. 仅支持feign接口上的@CollectionFormat注解, 特别地,如果接口上有@RequestMapping注解将会报错
  5. 方法上支持@CollectionFormat和@RequestMapping注解
  • @RequestMapping#produces属性实质就是添加的Accept请求头, 用于告诉服务端当前请求需要返回的数据类型, 例如application/json
  • @RequestMapping#consumes属性实质是添加Content-Type请求头, 用于高速服务端当前请求的参数类型, 例如application/json
  • @RequestMapping#headers设置当前方法级别的请求头
  • @RequestMapping#params设置添加到url上的参数, 该参数可以使用占位符, 从环境上下文中获取值
  1. 方法参数上支持
  • @MatrixVariable: 矩阵参数; 例如 ;name=小杜;age=18
  • @PathVariable: path路径参数
  • @RequestParam: url参数(一次一个)
  • @RequestHeader: 请求头
  • @SpringQueryMap: url参数(一次多个)
  • @RequestPart: 当做form参数, 以body传递
  • @CookieValue: cookie参数

别着急,下篇有完整demo

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

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

相关文章

7-8 N皇后问题

目录 题目描述 输入格式: 输出格式: 输入样例: 输出样例: 解题思路&#xff1a; 详细代码&#xff08;dfs&#xff09;&#xff1a; 简单代码&#xff08;打表&#xff09;&#xff1a; 题目描述 在NN格的国际象棋盘上摆放N个皇后&#xff0c;使其不能互相攻击&#xff0c;即任…

现代网络负载均衡与代理导论

大家觉得有有参考意义和帮助记得及时关注和点赞&#xff01;&#xff01;&#xff01; Service mesh 是近两年网络、容器编排和微服务领域最火热的话题之一。Envoy 是目前 service mesh 数据平面的首选组件。Matt Klein 是 Envoy 的设计者和核心开发。 文章循序渐进&#xff0…

Kubernetes Gateway API-2-跨命名空间路由

1 跨命名空间路由 Gateway API 具有跨命名空间路由的核心支持。当多个用户或团队共享底层网络基础设施时,这很有用,但必须对控制和配置进行分段,以尽量减少访问和容错域。 Gateway 和 Route(HTTPRoute,TCPRoute,GRPCRoute) 可以部署到不同的命名空间中,路由可以跨命名空间…

Wend看源码-Java-集合学习(List)

摘要 本篇文章深入探讨了基于JDK 21版本的Java.util包中提供的多样化集合类型。在Java中集合共分类为三种数据结构&#xff1a;List、Set和Queue。本文将详细阐述这些数据类型的各自实现&#xff0c;并按照线程安全性进行分类&#xff0c;分别介绍非线程安全与线程安全的实现方…

集成方案 | Docusign + 蓝凌 EKP,打造一站式合同管理平台,实现无缝协作!

本文将详细介绍 Docusign 与蓝凌 EKP 的集成步骤及其效果&#xff0c;并通过实际应用场景来展示 Docusign 的强大集成能力&#xff0c;以证明 Docusign 集成功能的高效性和实用性。 在当今数字化办公环境中&#xff0c;企业对于提高工作效率和提升用户体验的需求日益迫切。蓝凌…

突围边缘:OpenAI开源实时嵌入式API,AI触角延伸至微观世界

当OpenAI宣布开源其名为openai-realtime-embedded-sdk的实时嵌入式API时&#xff0c;整个科技界都为之震惊。这一举动意味着&#xff0c;曾经遥不可及的强大AI能力&#xff0c;如今可以被嵌入到像ESP32这样的微型控制器中&#xff0c;真正地将AI的触角延伸到了物联网和边缘计算…

webrtc-internals调试工具

Google 的 Chrome&#xff08;87 或更高版本&#xff09;WebRTC 内部工具是一套内置于 Chrome 浏览器中的调试工具; webrtc-internals 能够查看有关视频和音频轨道、使用的编解码器以及流的一般质量的详细信息。这些知识对于解决音频和视频质量差的问题非常有帮助。 webrtc-int…

使用Webpack构建微前端应用

英文社区对 Webpack Module Federation 的响应非常热烈&#xff0c;甚至被誉为“A game-changer in JavaScript architecture”&#xff0c;相对而言国内对此热度并不高&#xff0c;这一方面是因为 MF 强依赖于 Webpack5&#xff0c;升级成本有点高&#xff1b;另一方面是国内已…

[bug]java导出csv用Microsoft Office Excel打开乱码解决

[bug]java导出csv用Microsoft Office Excel打开乱码 ‍ 现象 首先这个csv文件用macbook自带的 "Numbers表格" 软件打开是不乱码的, 但是使用者是Windows系统,他的电脑没有"Numbers表格"工具, ​​ 他用Microsoft Office Excel打开之后出现乱码,如下图…

关于分布式数据库需要了解的相关知识!!!

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///计算机爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于关于分布式数据库方面的相关内容&a…

tortoisegit推送失败

tortoisegit推送失败 git.exe push --progress -- "origin" testLidar:testLidar /usr/bin/bash: gitgithub.com: No such file or directory fatal: Could not read from remote repository. Please make sure you have the correct access rights and the reposit…

moviepy将图片序列制作成视频并加载字幕 - python 实现

DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 需要更多数据资源和技术解决方案&#xff0c;知识星球&#xff1a; “DataBall - X 数据球(free)” -------------------------------------------------------------…

清空DNS 缓存

如果遇到修改了host文件&#xff0c;但是IP和域名的映射有问题的情况&#xff0c;可以尝试刷新DNS缓存。 ipconfig/flushdns win建加R建&#xff0c;然后输入cmd&#xff0c;然后回车 然后回车&#xff0c;或者点击确定按钮。 出现如下所示标识清空DNS 缓存成功。

2024最新鸿蒙开发面试题合集(二)-HarmonyOS NEXT Release(API 12 Release)

上一篇面试题链接&#xff1a;https://mp.csdn.net/mp_blog/creation/editor/144685078 1. 鸿蒙简单介绍和发展历程 HarmonyOS 是新一代的智能终端操作系统&#xff0c;为不同设备的智能化、互联与协同提供了统一的语言。带来简洁&#xff0c;流畅&#xff0c;连续&#xff0…

Yocto 项目 - 共享状态缓存 (Shared State Cache) 机制

引言 在嵌入式开发中&#xff0c;构建效率直接影响项目的开发进度和质量。Yocto 项目通过其核心工具 BitBake 提供了灵活而强大的构建能力。然而&#xff0c;OpenEmbedded 构建系统的传统设计是从头开始构建所有内容&#xff08;Build from Scratch&#xff09;&#xff0c;这…

idea 8年使用整理

文章目录 前言idea 8年使用整理1. 覆盖application配置2. 启动的时候设置编辑空间大小&#xff0c;并忽略最大空间3. 查询类的关系4. 查看这个方法的引用关系5. 查看方法的调用关系5.1. 查看被调用关系5.2. 查看调用关系 6. 方法分隔线7. 选择快捷键类型8. 代码预览插件9. JReb…

04软件测试需求分析案例-用户登录

通读文档&#xff0c;提取信息&#xff0c;提出问题&#xff0c;整理为需求。 从需求规格说明、设计说明、配置说明等文档获取原始需求&#xff0c;通读原始需求&#xff0c;分析有哪些功能&#xff0c;每种功能要完成什么业务&#xff0c;业务该如何实现&#xff0c;业务逻辑…

【MySQL】踩坑笔记——保存带有换行符等特殊字符的数据,需要进行转义保存

问题描述 从DBeaver中导出了部分业务数据的 insert sql&#xff0c;明明在开发、测试环境都可以一把执行通过&#xff0c;却在预发环境执行前的语法检查失败了&#xff0c;提示有SQL语法错误。 这条SQL长这样&#xff0c;default_sql是要在odps上执行的sql语句&#xff0c;提…

windos挂载目录到linux

验证环境麒麟V10 1: 在windows任意目录设置共享文件夹 2&#xff1a;记住网络路径\LAPTOP-86JV6NT1\gantie13_sdk 在linux中替换为本机ip级相对路径 比如本级ip是192.168.23.23&#xff0c;linux环境需要ping通本地地址 3&#xff1a; sudo apt-get install cifs-utils sud…

springboot494基于java的综合小区管理系统(论文+源码)_kaic

摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统综合小区管理系统信息管理难度大&#xff0c;容错率低&am…