006-spring cloud gateway-GatewayAutoConfiguration核心配置-GatewayProperties初始化加载、Route初始化加载...
一、GatewayProperties
1.1、在GatewayAutoConfiguration中加载
在Spring-Cloud-Gateway初始化时,同时GatewayAutoConfiguration核心配置类会被初始化加载如下 :
NettyConfiguration 底层通信netty配置 GlobalFilter (AdaptCachedBodyGlobalFilter,RouteToRequestUrlFilter,ForwardRoutingFilter,ForwardPathFilter,WebsocketRoutingFilter,WeightCalculatorWebFilter等) FilteringWebHandler GatewayProperties PrefixPathGatewayFilterFactory RoutePredicateFactory RouteDefinitionLocator RouteLocator RoutePredicateHandlerMapping 查找匹配到 Route并进行处理 GatewayWebfluxEndpoint 管理网关的 HTTP API
其中在GatewayAutoConfiguration配置加载中含初始化加载GatewayProperties实例的配置:
查看GatewayAutoConfiguration源码:
@Beanpublic GatewayProperties gatewayProperties() {return new GatewayProperties();}
1.2、再次查看GatewayProperties源码:
@ConfigurationProperties("spring.cloud.gateway") @Validated public class GatewayProperties {@NotNull@Validprivate List<RouteDefinition> routes = new ArrayList();private List<FilterDefinition> defaultFilters = new ArrayList();private List<MediaType> streamingMediaTypes;public GatewayProperties() {this.streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_STREAM_JSON);}public List<RouteDefinition> getRoutes() {return this.routes;}public void setRoutes(List<RouteDefinition> routes) {this.routes = routes;}public List<FilterDefinition> getDefaultFilters() {return this.defaultFilters;}public void setDefaultFilters(List<FilterDefinition> defaultFilters) {this.defaultFilters = defaultFilters;}public List<MediaType> getStreamingMediaTypes() {return this.streamingMediaTypes;}public void setStreamingMediaTypes(List<MediaType> streamingMediaTypes) {this.streamingMediaTypes = streamingMediaTypes;}public String toString() {return "GatewayProperties{routes=" + this.routes + ", defaultFilters=" + this.defaultFilters + ", streamingMediaTypes=" + this.streamingMediaTypes + '}';} }
以上会被默认加载并且读取配置信息,如下配置信息:
- spring.cloud.gateway.routes:网关路由定义配置,列表形式
- spring.cloud.gateway.default-filters: 网关默认过滤器定义配置,列表形式
- spring.cloud.gateway.streamingMediaTypes:网关网络媒体类型,列表形式
其中routes是RouteDefinition集合,defaultFilters是FilterDefinition集合,参看具体的配置字段。实际配置文件可如下:
spring:cloud:gateway:default-filters:- PrefixPath=/httpbin- AddResponseHeader=X-Response-Default-Foo, Default-Barroutes:- id: websocket_testuri: ws://localhost:9000order: 9000predicates:- Path=/echo- id: default_path_to_httpbinuri: ${test.uri}order: 10000predicates:- Path=/**
注意:default-filters的配置PrefixPath=/httpbin字符串,可以查看FilterDefinition的构造函数,它其中构造函数包含接收一个text字符串解析字符传并创建实例信息。predicates的配置也是如此。
字符传格式:name=param1,param2,param3
public FilterDefinition(String text) {int eqIdx = text.indexOf("=");if (eqIdx <= 0) {this.setName(text);} else {this.setName(text.substring(0, eqIdx));String[] args = StringUtils.tokenizeToStringArray(text.substring(eqIdx + 1), ",");for(int i = 0; i < args.length; ++i) {this.args.put(NameUtils.generateName(i), args[i]);}}}
二、Route初始化加载
2.1、GatewayAutoConfiguration加载RouteLocator
Spring-Cloud-Gateway路由信息是通过路由定位器RouteLocator加载以及初始化。
/*** 创建一个根据RouteDefinition转换的路由定位器*/@Beanpublic RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,List<GatewayFilterFactory> GatewayFilters,List<RoutePredicateFactory> predicates,RouteDefinitionLocator routeDefinitionLocator) {return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, GatewayFilters, properties);}/*** 创建一个缓存路由的路由定位器* @param routeLocators* @return*/@Bean@Primary//在相同的bean中,优先使用用@Primary注解的bean.public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {//1.创建组合路由定位器,根据(容器)已有的路由定位器集合//2.创建缓存功能的路由定位器return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));}
路由定位器的创建流程:
1、RouteDefinitionRouteLocator 2、CompositeRouteLocator 3、CachingRouteLocator 其中 RouteDefinitionRouteLocator 是获取路由的主要地方,CompositeRouteLocator,CachingRouteLocator对路由定位器做了附加功能的包装,最终使用的是CachingRouteLocator对外提供服务
2.2、查看RouteLocator源码:
/*** 路由定位器,服务获取路由信息* 可以通过 RouteDefinitionRouteLocator 获取 RouteDefinition ,并转换成 Route*/ public interface RouteLocator {/*** 获取路由*/Flux<Route> getRoutes(); }
查看RouteLocator实现类
2.3、缓存功能实现→CachingRouteLocator
// 路由定位器的包装类,实现了路由的本地缓存功能 public class CachingRouteLocator implements RouteLocator {//目标路由定位器private final RouteLocator delegate;/*** 路由信息* Flux 相当于一个 RxJava Observable,* 能够发出 0~N 个数据项,然后(可选地)completing 或 erroring。处理多个数据项作为stream*/private final Flux<Route> routes;// 本地缓存,用于缓存路由定位器获取的路由集合private final Map<String, List> cache = new HashMap<>();public CachingRouteLocator(RouteLocator delegate) {this.delegate = delegate;routes = CacheFlux.lookup(cache, "routes", Route.class).onCacheMissResume(() -> this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE));}@Overridepublic Flux<Route> getRoutes() {return this.routes;}// 刷新缓存public Flux<Route> refresh() {this.cache.clear();return this.routes;}@EventListener(RefreshRoutesEvent.class)void handleRefresh() {refresh();} }
1、路由信息的本地缓存,通过Map<String, List> cache 缓存路由到内存中;
2、此类通过@EventListener(RefreshRoutesEvent.class)监听RefreshRoutesEvent事件实现了对缓存的动态刷新;
注:路由动态刷新,使用GatewayControllerEndpoint发布刷新事件
@RestControllerEndpoint(id = "gateway") public class GatewayControllerEndpoint implements ApplicationEventPublisherAware{// 调用url= /gateway/refresh 刷新缓存中的路由信息@PostMapping("/refresh")public Mono<Void> refresh() {this.publisher.publishEvent(new RefreshRoutesEvent(this));return Mono.empty();} }
2.4、组合功能实现→CompositeRouteLocator
//组合多个 RRouteLocator 的实现,为Route提供统一获取入口 public class CompositeRouteLocator implements RouteLocator {/*** 能够发出 0~N 个数据项(RouteLocator),然后(可选地)completing 或 erroring。处理多个数据项作为stream*/private final Flux<RouteLocator> delegates;public CompositeRouteLocator(Flux<RouteLocator> delegates) {this.delegates = delegates;}@Overridepublic Flux<Route> getRoutes() {//this.delegates.flatMap((routeLocator)-> routeLocator.getRoutes());return this.delegates.flatMap(RouteLocator::getRoutes);} }
此类将遍历传入的目录路由定位器集合,组合每个路由定位器获取到的路由信息
2.5、通过路由定义转换路由实现→RouteDefinitionRouteLocator
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.cloud.gateway.route;import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.config.GatewayProperties; import org.springframework.cloud.gateway.event.FilterArgsEvent; import org.springframework.cloud.gateway.event.PredicateArgsEvent; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.handler.AsyncPredicate; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory; import org.springframework.cloud.gateway.route.Route.AsyncBuilder; import org.springframework.cloud.gateway.support.ConfigurationUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.validation.Validator; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux;public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware {protected final Log logger = LogFactory.getLog(this.getClass());private final RouteDefinitionLocator routeDefinitionLocator;private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap();private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap();private final GatewayProperties gatewayProperties;private final SpelExpressionParser parser = new SpelExpressionParser();private BeanFactory beanFactory;private ApplicationEventPublisher publisher;@Autowiredprivate Validator validator;public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator, List<RoutePredicateFactory> predicates, List<GatewayFilterFactory> gatewayFilterFactories, GatewayProperties gatewayProperties) {this.routeDefinitionLocator = routeDefinitionLocator;this.initFactories(predicates);gatewayFilterFactories.forEach((factory) -> {GatewayFilterFactory var10000 = (GatewayFilterFactory)this.gatewayFilterFactories.put(factory.name(), factory);});this.gatewayProperties = gatewayProperties;}public void setBeanFactory(BeanFactory beanFactory) throws BeansException {this.beanFactory = beanFactory;}public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {this.publisher = publisher;}private void initFactories(List<RoutePredicateFactory> predicates) {predicates.forEach((factory) -> {String key = factory.name();if (this.predicates.containsKey(key)) {this.logger.warn("A RoutePredicateFactory named " + key + " already exists, class: " + this.predicates.get(key) + ". It will be overwritten.");}this.predicates.put(key, factory);if (this.logger.isInfoEnabled()) {this.logger.info("Loaded RoutePredicateFactory [" + key + "]");}});}public Flux<Route> getRoutes() {return this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute).map((route) -> {if (this.logger.isDebugEnabled()) {this.logger.debug("RouteDefinition matched: " + route.getId());}return route;});}private Route convertToRoute(RouteDefinition routeDefinition) {AsyncPredicate<ServerWebExchange> predicate = this.combinePredicates(routeDefinition);List<GatewayFilter> gatewayFilters = this.getFilters(routeDefinition);return ((AsyncBuilder)Route.async(routeDefinition).asyncPredicate(predicate).replaceFilters(gatewayFilters)).build();}private List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {List<GatewayFilter> filters = (List)filterDefinitions.stream().map((definition) -> {GatewayFilterFactory factory = (GatewayFilterFactory)this.gatewayFilterFactories.get(definition.getName());if (factory == null) {throw new IllegalArgumentException("Unable to find GatewayFilterFactory with name " + definition.getName());} else {Map<String, String> args = definition.getArgs();if (this.logger.isDebugEnabled()) {this.logger.debug("RouteDefinition " + id + " applying filter " + args + " to " + definition.getName());}Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);Object configuration = factory.newConfig();ConfigurationUtils.bind(configuration, properties, factory.shortcutFieldPrefix(), definition.getName(), this.validator);GatewayFilter gatewayFilter = factory.apply(configuration);if (this.publisher != null) {this.publisher.publishEvent(new FilterArgsEvent(this, id, properties));}return gatewayFilter;}}).collect(Collectors.toList());ArrayList<GatewayFilter> ordered = new ArrayList(filters.size());for(int i = 0; i < filters.size(); ++i) {GatewayFilter gatewayFilter = (GatewayFilter)filters.get(i);if (gatewayFilter instanceof Ordered) {ordered.add(gatewayFilter);} else {ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));}}return ordered;}private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {List<GatewayFilter> filters = new ArrayList();if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {filters.addAll(this.loadGatewayFilters("defaultFilters", this.gatewayProperties.getDefaultFilters()));}if (!routeDefinition.getFilters().isEmpty()) {filters.addAll(this.loadGatewayFilters(routeDefinition.getId(), routeDefinition.getFilters()));}AnnotationAwareOrderComparator.sort(filters);return filters;}private AsyncPredicate<ServerWebExchange> combinePredicates(RouteDefinition routeDefinition) {List<PredicateDefinition> predicates = routeDefinition.getPredicates();AsyncPredicate<ServerWebExchange> predicate = this.lookup(routeDefinition, (PredicateDefinition)predicates.get(0));AsyncPredicate found;for(Iterator var4 = predicates.subList(1, predicates.size()).iterator(); var4.hasNext(); predicate = predicate.and(found)) {PredicateDefinition andPredicate = (PredicateDefinition)var4.next();found = this.lookup(routeDefinition, andPredicate);}return predicate;}private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route, PredicateDefinition predicate) {RoutePredicateFactory<Object> factory = (RoutePredicateFactory)this.predicates.get(predicate.getName());if (factory == null) {throw new IllegalArgumentException("Unable to find RoutePredicateFactory with name " + predicate.getName());} else {Map<String, String> args = predicate.getArgs();if (this.logger.isDebugEnabled()) {this.logger.debug("RouteDefinition " + route.getId() + " applying " + args + " to " + predicate.getName());}Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);Object config = factory.newConfig();ConfigurationUtils.bind(config, properties, factory.shortcutFieldPrefix(), predicate.getName(), this.validator);if (this.publisher != null) {this.publisher.publishEvent(new PredicateArgsEvent(this, route.getId(), properties));}return factory.applyAsync(config);}} }
View Code
此类的核心方法getRoutes通过传入的routeDefinitionLocator获取路由定位,并循环遍历路由定位依次转换成路由返回,
代码中可以看到getRoutes通过convertToRoute方法将路由定位转换成路由的
2.5.1、RouteDefinition转换:convertToRoute
// RouteDefinition 转换为对应的Routeprivate Route convertToRoute(RouteDefinition routeDefinition) {//获取routeDefinition中的Predicate信息Predicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);//获取routeDefinition中的GatewayFilter信息List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);//构建路由信息return Route.builder(routeDefinition).predicate(predicate).replaceFilters(gatewayFilters).build();}
convertToRoute方法功能作用
获取routeDefinition中的Predicate信息 (通过combinePredicates方法)
获取routeDefinition中的GatewayFilter信息(通过gatewayFilters方法)
构建路由信息
1、convertToRoute中combinePredicates获取routeDefinition中的Predicate信息如下:
// 返回组合的谓词private Predicate<ServerWebExchange> combinePredicates(RouteDefinition routeDefinition) {//获取RouteDefinition中的PredicateDefinition集合List<PredicateDefinition> predicates = routeDefinition.getPredicates();Predicate<ServerWebExchange> predicate = lookup(routeDefinition, predicates.get(0));for (PredicateDefinition andPredicate : predicates.subList(1, predicates.size())) {Predicate<ServerWebExchange> found = lookup(routeDefinition, andPredicate);//流程4//返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑ANDpredicate = predicate.and(found);}return predicate;}/*** 获取一个谓语定义(PredicateDefinition)转换的谓语* @param route* @param predicate* @return*/@SuppressWarnings("unchecked")private Predicate<ServerWebExchange> lookup(RouteDefinition route, PredicateDefinition predicate) {//流程1//流程1==获取谓语创建工厂RoutePredicateFactory<Object> factory = this.predicates.get(predicate.getName());if (factory == null) {throw new IllegalArgumentException("Unable to find RoutePredicateFactory with name " + predicate.getName());}//流程2//获取参数Map<String, String> args = predicate.getArgs();if (logger.isDebugEnabled()) {logger.debug("RouteDefinition " + route.getId() + " applying "+ args + " to " + predicate.getName());}//组装参数Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);//构建创建谓语的配置信息Object config = factory.newConfig();ConfigurationUtils.bind(config, properties,factory.shortcutFieldPrefix(), predicate.getName(), validator);if (this.publisher != null) {this.publisher.publishEvent(new PredicateArgsEvent(this, route.getId(), properties));}//流程3//通过谓语工厂构建谓语return factory.apply(config);}
获取Predicate流程:
- 根据PredicateDefinition name 获取 RoutePredicateFactory
- 根据PredicateDefinition args 组装 config信息
- 通过RoutePredicateFactory 根据config信息创建Predicate信息
- 多个Predicate 以短路逻辑AND组合
2、convertToRoute中 getFilters获取routeDefinition中的GatewayFilter信息
private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {List<GatewayFilter> filters = new ArrayList<>();//校验gatewayProperties是否含义默认的过滤器集合if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {//加载全局配置的默认过滤器集合filters.addAll(loadGatewayFilters("defaultFilters",this.gatewayProperties.getDefaultFilters()));}if (!routeDefinition.getFilters().isEmpty()) {//加载路由定义中的过滤器集合 filters.addAll(loadGatewayFilters(routeDefinition.getId(), routeDefinition.getFilters()));}//排序 AnnotationAwareOrderComparator.sort(filters);return filters;}/*** 加载过滤器,根据过滤器的定义加载* @param id* @param filterDefinitions* @return*/@SuppressWarnings("unchecked")private List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {//遍历过滤器定义,将过滤器定义转换成对应的过滤器List<GatewayFilter> filters = filterDefinitions.stream().map(definition -> {//流程1 //通过过滤器定义名称获取过滤器创建工厂GatewayFilterFactory factory = this.gatewayFilterFactories.get(definition.getName());if (factory == null) {throw new IllegalArgumentException("Unable to find GatewayFilterFactory with name " + definition.getName());}//流程2//获取参数Map<String, String> args = definition.getArgs();if (logger.isDebugEnabled()) {logger.debug("RouteDefinition " + id + " applying filter " + args + " to " + definition.getName());}//根据args组装配置信息Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);//构建过滤器创建配置信息Object configuration = factory.newConfig();ConfigurationUtils.bind(configuration, properties,factory.shortcutFieldPrefix(), definition.getName(), validator);//流程3//通过过滤器工厂创建GatewayFilterGatewayFilter gatewayFilter = factory.apply(configuration);if (this.publisher != null) {//发布事件this.publisher.publishEvent(new FilterArgsEvent(this, id, properties));}return gatewayFilter;}).collect(Collectors.toList());ArrayList<GatewayFilter> ordered = new ArrayList<>(filters.size());//包装过滤器使其所有过滤器继承Ordered属性,可进行排序for (int i = 0; i < filters.size(); i++) {GatewayFilter gatewayFilter = filters.get(i);if (gatewayFilter instanceof Ordered) {ordered.add(gatewayFilter);}else {ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));}}return ordered;}
- getFilters 方法 同时加载 全局配置 gatewayProperties与routeDefinition配置下的所有过滤器定义filterDefinitions
- loadGatewayFilters 负责将filterDefinition转化成对应的GatewayFilter
转化流程如下
- 根据filterDefinition name 获取 GatewayFilterFactory
- 根据filterDefinition args 组装 config信息
- 通过GatewayFilterFactory 根据config信息创建PGatewayFilter信息
006-spring cloud gateway-GatewayAutoConfiguration核心配置-GatewayProperties初始化加载、Route初始化加载...相关推荐
- spring cloud gateway的stripPrefix配置
序 本文主要研究下spring cloud gateway的stripPrefix配置 使用zuul的配置 zuul:routes:demo:sensitiveHeaders: Access-Cont ...
- Nacos + Spring Cloud Gateway动态路由配置
前言 Nacos最近项目一直在使用,其简单灵活,支持更细粒度的命令空间,分组等为麻烦复杂的环境切换提供了方便:同时也很好支持动态路由的配置,只需要简单的几步即可.在国产的注册中心.配置中心中比较突出, ...
- (十八)Alian 的 Spring Cloud Gateway 集群配置
目录 一.简介 二.配置 三.配置文件 3.1.application.properties 四.主类 五.部署及配置 5.1.部署 5.2.Nginx配置 5.3.Spring Cloud Gate ...
- Spring Cloud Gateway 源码解析(1) —— 基础
目录 Gateway初始化 启用Gateway GatewayClassPathWarningAutoConfiguration GatewayLoadBalancerClientAutoConfig ...
- Spring Cloud GateWay系列(三):路由规则动态刷新
Spring Cloud Gateway旨在提供一种简单而有效的方式来路由API,并为它们提供横切关注点,例如:安全性.监控/指标和弹性.Route(路由)是网关的基本单元,由唯一标识符ID.目标地址 ...
- Spring Cloud Gateway 源码解析(2) —— 路由
目录 基本组件 路由定位器(RouteDefinitionLocator ) 路由定义(RouteDefinition) PredicateDefinition FilterDefinition Co ...
- 有什么办法动态更改yml的值吗_基于Redis实现Spring Cloud Gateway的动态管理
转载本文需注明出处:微信公众号EAWorld,违者必究. 引言: Spring Cloud Gateway是当前使用非常广泛的一种API网关.它本身能力并不能完全满足企业对网关的期望,人们希望它可以提 ...
- 网关Spring Cloud Gateway科普
点击上方"朱小厮的博客",选择"设为星标" 后台回复"加群"获取公众号专属群聊入口 欢迎跳转到本文的原文链接:https://honeypp ...
- Spring Cloud Gateway一次请求调用源码解析
简介: 最近通过深入学习Spring Cloud Gateway发现这个框架的架构设计非常简单.有效,很多组件的设计都非常值得学习,本文就Spring Cloud Gateway做一个简单的介绍,以及 ...
最新文章
- Windows phone 8 学习笔记(5) 图块与通知
- 标签页使用及bug解决
- java new url 带密码_获取密码重置URL
- OpenCASCADE:拓扑 API之历史支持
- spring 同时配置hibernate and jdbc 事务
- DevC++最新汉化版(支持C++11)
- list容器java_【Java容器】List容器使用方法及源码分析
- 安卓手电筒_将价值10美元的手电筒砍入超高亮高级灯中
- Android学习问题:关于AlertDialog中自定义布局带有的EditText无法弹出键盘
- 毕业这几年的嵌入式开发之路
- leetcode链表题
- linux mysql 临时文件_linux下mysql自动备份数据库与自动删除临时文件
- 高效开发 Android App 的 10 个建议
- Android 系统(200)---Android build.prop参数详解
- 【Elasticsearch】es CPU热点线程 HotThreads 源码解析
- turtlebot3 模型没有显示_Turtlebot3新手教程:Open-Manipulator机械臂
- 严防ARP病毒的六个步骤
- 立志做个有激情的coder
- 利用imageio将多张.jpg转.gif图片(Python3)
- Unity材质:玻璃