SprringCloud Gateway动态添加路由不重启
文章目录
- 前言:
- 一、动态路由必要性
- 二、SpringCloud Gateway路由加载过程
- RouteDefinitionLocator接口
- PropertiesRouteDefinitionLocator类
- DiscoveryClientRouteDefinitionLocator
- InMemoryRouteDefinitionRepository
- CompositeRouteDefinitionLocator类
- CachingRouteDefinitionLocator类
- RouteLocator接口
- Route类
- RouteDefinitionRouteLocator类
- CachingRouteLocator类
- RouteRefreshListener类
- 三、Nacos实现动态路由
- 四、通过 Spring Boot Actuator实现动态路由
- 五、通过事件刷新机制自定义实现动态路由
- GatewayRouteEventPublisherAware类
- NacosRouteRefreshListener类
- NacosGatewayConfig类
- NacosRouteListener类
- isrm-gateway配置文件
- 初始化流程
- 监听流程
前言:
在微服务项目中,SpringCloud Gateway扮演着极为重要的角色,主要提供了路由、负载均衡、认证鉴权等功能。本文主要讲解如何实现网关的自定义动态路由配置,无需重启网关模块即可生效。
一、动态路由必要性
在微服务架构中,随着功能的迭代和上线,经常需要在网关添加路由配置。传统的做法是通过修改配置文件并重启网关服务来实现,但这种方式会导致服务中断,给用户带来不便。
例如如下配置:
spring: cloud: gateway: routes: - id: system predicates: - Path=/api/system/** filters: - StripPrefix=2 uri: lb://isrm-system-provider- id: basic predicates: - Path=/api/basic/** filters: - StripPrefix=2 uri: lb://isrm-basic-provider
该配置写在jar包同级的 isrm-gateway.yml文件中,假如现在网关模块是运行的,并且路由配置只有system模块。此时系统增加了basic模块,需要在配置文件中进行路由配置,但是配置完成之后,路由并未生效,只能重启网关模块去读取最新的配置来加载路由信息,重启过程中,整个网关模块都是用不了的,所有经过网关的请求都会失败,影响用户的体验。因此,动态添加路由而不重启服务成为了一个实际需求。
二、SpringCloud Gateway路由加载过程
SpringCloud Gateway路由加载过程
在看完上面的文章大概知道了路由相关类和接口的相关作用
- RoutePredicateFactory,断言工厂,用于创建具体的断言。
- GatewayFilterFactory,过滤器工厂,用于创建具体的过滤器。
- Predicate,断言接口。
- GatewayFilter,过滤器接口。
- RouteDefinition,路由定义对象,在yml里配置的路由规则其实就是配置它,包含一组断言工厂和过滤器工厂。
- Route, 路由对象,包含了一组断言规则列表和过滤器列表。
- RouteDefinitionLocator,用于获取一组RouteDefinition,最常见的就是从yml里配置,或者基于服务发现的默认路由配置。
- RouteLocator,用于把RouteDefinition转换成真正的Route对象。
RouteDefinitionLocator接口
主要有以下的实现类,该接口主要用来获取路由定义信息,比如上面yml配置文件的路由信息
PropertiesRouteDefinitionLocator类
public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {private final GatewayProperties properties;public PropertiesRouteDefinitionLocator(GatewayProperties properties) {this.properties = properties;}@Overridepublic Flux<RouteDefinition> getRouteDefinitions() {return Flux.fromIterable(this.properties.getRoutes());}}
主要获取配置文件的路由信息,其中GatewayProperties类标注着@ConfigurationProperties(“spring.cloud.gateway”)注解,可以获取配置文件中spring.cloud.gateway前缀的配置内容
DiscoveryClientRouteDefinitionLocator
spring.cloud.gateway.discovery.locator.enabled=true为ture时会装配该Bean
主要是从注册中心获取所有的服务列表,然后挨个加入Path断言以及去掉第一段路径的过滤器;比如某个服务名称为isrm-basic-provider,那么它会被该类发现,并加入Path断言’/isrm-basic-provider/**‘和过滤器’‘/isrm-basic-provider/(?. *)’,当访问/isrm-basic-provider/user/get时会被拦截,有过滤器重写路径为/user/get,最终访问lb://isrm-basic-provider/user/get。
如图,我并没有在配置文件配置相关路由信息,也可以经过网关访问basic服务的接口,因此可以通过DiscoveryClientRouteDefinitionLocator类发现注册的服务,然后添加默认的路由定义信息
InMemoryRouteDefinitionRepository
主要提供了对路由定义信息的增加、删除、查询方法,由一个LinkedHashMap变量存储,并包装成线程安全的SynchronizedMap
public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository {private final Map<String, RouteDefinition> routes = synchronizedMap(new LinkedHashMap<String, RouteDefinition>());@Overridepublic Mono<Void> save(Mono<RouteDefinition> route) {return route.flatMap(r -> {if (StringUtils.isEmpty(r.getId())) {return Mono.error(new IllegalArgumentException("id may not be empty"));}routes.put(r.getId(), r);return Mono.empty();});}@Overridepublic Mono<Void> delete(Mono<String> routeId) {return routeId.flatMap(id -> {if (routes.containsKey(id)) {routes.remove(id);return Mono.empty();}return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)));});}@Overridepublic Flux<RouteDefinition> getRouteDefinitions() {return Flux.fromIterable(routes.values());}}
CompositeRouteDefinitionLocator类
把其它的RouteDefinitionLocator
组合在一起,也是Spring Cloud Gateway默认装配的RouteDefinitionLocator
bean。加了@Primary
注解会优先注入。
// GatewayAutoConfiguration类部分代码,通过入参List<RouteDefinitionLocator> routeDefinitionLocators会把除了CachingRouteDefinitionLocator类
// 的所有RouteDefinitionLocator的实现类注入进来
@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(List<RouteDefinitionLocator> routeDefinitionLocators) {return new CompositeRouteDefinitionLocator(Flux.fromIterable(routeDefinitionLocators));
}
CompositeRouteDefinitionLocator代码
public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {private static final Log log = LogFactory.getLog(CompositeRouteDefinitionLocator.class);private final Flux<RouteDefinitionLocator> delegates;private final IdGenerator idGenerator;public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates) {this(delegates, new AlternativeJdkIdGenerator());}public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates,IdGenerator idGenerator) {this.delegates = delegates;this.idGenerator = idGenerator;}/** * 主要逻辑就是遍历所有注入的RouteDefinitionLocator的实现类,执行它们的getRouteDefinitions方法,合并它们返回的路由定义信息,如果路由定义信息没有* id,则默认生成一个随机id* PropertiesRouteDefinitionLocator:获取配置文件的路由定义信息* DiscoveryClientRouteDefinitionLocator:按服务名称生成默认的路由定义信息* InMemoryRouteDefinitionRepository:获取维护的路由定义信息,后续SpringBoot Actuator实现动态路由以及事件刷新机制实现动态路由都是基于该类的方法 * 实现的*/@Overridepublic Flux<RouteDefinition> getRouteDefinitions() {return this.delegates.flatMap(RouteDefinitionLocator::getRouteDefinitions).flatMap(routeDefinition -> {if (routeDefinition.getId() == null) {return randomId().map(id -> {routeDefinition.setId(id);if (log.isDebugEnabled()) {log.debug("Id set on route definition: " + routeDefinition);}return routeDefinition;});}return Mono.just(routeDefinition);});}protected Mono<String> randomId() {return Mono.fromSupplier(idGenerator::toString).publishOn(Schedulers.boundedElastic());}}
CachingRouteDefinitionLocator类
该类实现了ApplicationListener接口。主要逻辑是调用CompositeRouteDefinitionLocator类的getRouteDefinitions方法,因为CompositeRouteDefinitionLocator持有了一个RouteDefinitionLocator接口的实现类列表,所以调用getRouteDefinitions方法时,会依次调用它们的getRouteDefinitions方法,并将结果合并之后,缓存到CachingRouteDefinitionLocator类的routeDefinitions和cache中,这样就不需要每次都去调用fetch方法获取路由定义信息,只有监听到RefreshRoutesEvent事件时,才会重新调用fetch方法获取最新的路由定义信息。
注意:调试代码查看流程的时候,好像该类并没有被注入进来,而RefreshRoutesEvent事件会被下面介绍的CachingRouteLocator类处理
public class CachingRouteDefinitionLocator implements RouteDefinitionLocator, ApplicationListener<RefreshRoutesEvent> {private static final String CACHE_KEY = "routeDefs";private final RouteDefinitionLocator delegate;private final Flux<RouteDefinition> routeDefinitions;private final Map<String, List> cache = new ConcurrentHashMap<>();// 构造方法会注入RouteDefinitionLocator接口的实现类,因为CompositeRouteDefinitionLocator加了@Primary注解,所以会注入该类。public CachingRouteDefinitionLocator(RouteDefinitionLocator delegate) {this.delegate = delegate;// 执行fetch方法,获取所有路由定义信息,缓存起来routeDefinitions = CacheFlux.lookup(cache, CACHE_KEY, RouteDefinition.class).onCacheMissResume(this::fetch);}// 执行CompositeRouteDefinitionLocator类的getRouteDefinitions方法private Flux<RouteDefinition> fetch() {return this.delegate.getRouteDefinitions();}// 获取所有缓存的路由信息@Overridepublic Flux<RouteDefinition> getRouteDefinitions() {return this.routeDefinitions;}/*** Clears the cache of routeDefinitions.* @return routeDefinitions flux*/public Flux<RouteDefinition> refresh() {this.cache.clear();return this.routeDefinitions;}// 监听RefreshRoutesEvent事件,调用fetch方法,获取最新的路由定义信息@Overridepublic void onApplicationEvent(RefreshRoutesEvent event) {fetch().materialize().collect(Collectors.toList()).doOnNext(routes -> cache.put(CACHE_KEY, routes)).subscribe();}@Deprecated/* for testing */ void handleRefresh() {refresh();}}
RouteLocator接口
主要有以下实现类,该接口主要用于把RouteDefinition
路由定义信息对象转换成真实的Route
路由对象。
Route类
部分代码
public class Route implements Ordered {private final String id;private final URI uri;private final int order;private final AsyncPredicate<ServerWebExchange> predicate;private final List<GatewayFilter> gatewayFilters;private final Map<String, Object> metadata;
}
作用:
- 基本构建块:Route是Gateway网关的基本构建块,它负责定义和处理进入网关的网络请求。
- 组成元素:
- ID:每个Route都有一个唯一的ID,用于标识和区分不同的路由规则。
- 目标URI:目标URI指定了当路由匹配成功后,请求应该被转发到的目标地址或服务。
- 断言(Predicate)集合:断言是路由处理的第一个环节,它是一个集合,可以包含多个断言规则。这些断言规则用于匹配HTTP请求的不同属性,只有当所有断言都匹配成功时,才认为该请求匹配了当前路由。
- 过滤器(Filter)集合:如果请求通过了断言匹配,那么它将被发送到过滤器集合进行处理。过滤器可以对请求进行一系列的操作,如权限验证、参数修改等。过滤器可以在请求被转发之前或之后执行,提供了对请求和响应的精细化控制。
- 路由匹配:当客户端向Gateway发出请求时,Gateway会根据定义的Route对象进行路由匹配。如果请求与某个Route的断言集合匹配成功,那么该请求将被转发到该Route指定的目标URI,并经过该Route的过滤器集合处理。
- 服务发现和负载均衡:如果目标URI是基于服务注册名的方式(如Eureka中注册的服务名称),那么Gateway会借助服务发现机制(如Ribbon)来实现负载均衡,将请求分发到合适的服务实例上执行。
Route对象在Gateway中起到了定义路由规则、匹配网络请求、处理请求和响应的重要作用。通过配置合适的Route对象,可以实现复杂的路由逻辑和精细化的控制策略,提高系统的可扩展性和可维护性。
RouteDefinitionRouteLocator类
主要将RouteDefinition
路由定义信息对象转换成真实的Route
路由对象
GatewayAutoConfiguration部分代码
// 在创建RouteDefinitionRouteLocator的Bean时,会注入相关过滤器工厂、断言工厂、配置类、CompositeRouteDefinitionLocator对象
@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,List<GatewayFilterFactory> gatewayFilters,List<RoutePredicateFactory> predicates,RouteDefinitionLocator routeDefinitionLocator,ConfigurationService configurationService) {return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates,gatewayFilters, properties, configurationService);
}
RouteDefinitionRouteLocator部分代码
public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware {private final RouteDefinitionLocator routeDefinitionLocator;private final ConfigurationService configurationService;private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap<>();private final GatewayProperties gatewayProperties;/*** 将容器中的断言工厂,过滤器工厂放到Map中,key为工厂名称前缀* 如PathRoutePredicateFactory, 则key=Path* 这些断言工厂和过滤器工厂基本都在GatewayAutoConfiguration自动配置类注册到Spring容器中的*/public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,List<RoutePredicateFactory> predicates,List<GatewayFilterFactory> gatewayFilterFactories,GatewayProperties gatewayProperties,ConfigurationService configurationService) {this.routeDefinitionLocator = routeDefinitionLocator;this.configurationService = configurationService;initFactories(predicates);gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));this.gatewayProperties = gatewayProperties;}/*** 调用CompositeRouteDefinitionLocator对象的getRouteDefinitions方法,获取所有路由定义信息,然后遍历路由定义信息列表,调用断言工厂以及过滤器工厂 * 的相关方法将断言定义信息和过滤器定义信息转成断言和过滤器,接着生成一个路由对象,添加到routes中,最后返回所有路由对象*/@Overridepublic Flux<Route> getRoutes() {Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);if (!gatewayProperties.isFailOnRouteDefinitionError()) {// instead of letting error bubble up, continueroutes = routes.onErrorContinue((error, obj) -> {if (logger.isWarnEnabled()) {logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId()+ " will be ignored. Definition has invalid configs, "+ error.getMessage());}});}return routes.map(route -> {if (logger.isDebugEnabled()) {logger.debug("RouteDefinition matched: " + route.getId());}return route;});}
}
CachingRouteLocator类
主要作用是缓存路由对象信息,不然每次请求都会生成新的路由对象信息
GatewayAutoConfiguration部分代码
/**在创建CachingRouteLocator类的Bean时,会创建CompositeRouteLocator对象,而CompositeRouteLocator对象又会持有参数中注入的* RouteDefinitionRouteLocator的Bean*/
@Bean
@Primary
@ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
// TODO: property to disable composite?
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}
CompositeRouteLocator代码
public class CompositeRouteLocator implements RouteLocator {private final Flux<RouteLocator> delegates;public CompositeRouteLocator(Flux<RouteLocator> delegates) {this.delegates = delegates;}// 调用RouteDefinitionRouteLocator的getRoutes方法获取路由对象信息@Overridepublic Flux<Route> getRoutes() {return this.delegates.flatMap(RouteLocator::getRoutes);}}
CachingRouteLocator代码
public class CachingRouteLocator implements Ordered, RouteLocator, ApplicationListener<RefreshRoutesEvent> {private static final String CACHE_KEY = "routes";private final RouteLocator delegate;private final Flux<Route> routes;// 缓存路由信息private final Map<String, List> cache = new ConcurrentHashMap<>();public CachingRouteLocator(RouteLocator delegate) {this.delegate = delegate;routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class).onCacheMissResume(this::fetch);}// 调用CompositeRouteLocator的方法获取路由对象信息,每次调用都会生成新的路由信息private Flux<Route> fetch() {return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);}// 获取路由信息@Overridepublic Flux<Route> getRoutes() {return this.routes;}/*** Clears the routes cache.* @return routes flux*/public Flux<Route> refresh() {this.cache.clear();return this.routes;}// 监听RefreshRoutesEvent事件,调用fetch方法生成新的路由信息,同时放入缓存中@Overridepublic void onApplicationEvent(RefreshRoutesEvent event) {fetch().materialize().collect(Collectors.toList()).doOnNext(routes -> cache.put(CACHE_KEY, routes)).subscribe();}@Deprecated/* for testing */ void handleRefresh() {refresh();}@Overridepublic int getOrder() {return 0;}}
因此,我们想要获取最新的路由信息,只需要发布一个RefreshRoutesEvent事件即可
RouteRefreshListener类
除了发布RefreshRoutesEvent事件可以获取最新路由信息之外,当Nacos配置中心发布新配置时,也会去重新获取路由信息
public class RouteRefreshListener implements ApplicationListener<ApplicationEvent> {private final ApplicationEventPublisher publisher;private HeartbeatMonitor monitor = new HeartbeatMonitor();public RouteRefreshListener(ApplicationEventPublisher publisher) {Assert.notNull(publisher, "publisher may not be null");this.publisher = publisher;}@Overridepublic void onApplicationEvent(ApplicationEvent event) {/*** ContextRefreshedEvent:Spring容器初始化完成之后会发布该事件,初始化路由信息* RefreshScopeRefreshedEvent:配置中心发生变化后@RefreshScope或@ConfigurationProperties标注的bean刷新完之后会发布该事件, * 然后PropertiesRouteDefinitionLocator会获取配置文件新的定义信息* InstanceRegisteredEvent:服务注册会发布该事件,DiscoveryClientRouteDefinitionLocator会处理服务名称,获取默认路由定义信息*/if (event instanceof ContextRefreshedEvent|| event instanceof RefreshScopeRefreshedEvent|| event instanceof InstanceRegisteredEvent) {reset();}else if (event instanceof ParentHeartbeatEvent) {ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;resetIfNeeded(e.getValue());}else if (event instanceof HeartbeatEvent) {HeartbeatEvent e = (HeartbeatEvent) event;resetIfNeeded(e.getValue());}}private void resetIfNeeded(Object value) {if (this.monitor.update(value)) {reset();}}// 发布RefreshRoutesEvent,获取新的路由信息private void reset() {this.publisher.publishEvent(new RefreshRoutesEvent(this));}}
接下来说一下我所知道的三种动态路由实现方式
三、Nacos实现动态路由
前面讲了RouteRefreshListener这个监听器会监听RefreshScopeRefreshedEvent事件,当在Nacos修改了路由配置,点击发布按钮就会发布RefreshScopeRefreshedEvent事件,然后监听器监听到了这个事件,就会重新获取新的路由定义信息,然后再将这些路由定义信息转换成真正的路由对象保存在内存中。
例如我Nacos中的配置文件如下:
spring:cloud:gateway:routes:- id: suppredicates: - Path=/api/sup/**filters:- StripPrefix=2uri: lb://isrm-sup-provider- id: authpredicates: - Path=/api/auth/**filters:- StripPrefix=2uri: lb://isrm-auth-provider- id: basicpredicates: - Path=/api/basic/**filters:- StripPrefix=2uri: lb://isrm-basic-provider- id: systempredicates: - Path=/api/system/**filters:- StripPrefix=2uri: lb://isrm-system-provider
因为我上面没有配置合同模块的路由定义信息,所以我在本地访问合同模块的查询接口时,会报下面的异常信息,找不到对应的路由
'Failed to handle request [POST http://localhost:8081/api/contract/tContract/query]: 404 NOT_FOUND "No matching handler
在网关的配置文件加入合同模块的路由定义信息
此时点击发布按钮,配置中心的配置发生了变化,会发布一个RefreshScopeRefreshedEvent事件,RouteRefreshListener监听到这个事件会发布一个RefreshRoutesEvent事件
然后CachingRouteLocator类会监听RefreshRoutesEvent事件,接着调用CompositeRouteLocator类的方法
CompositeRouteLocator类接着调用RouteDefinitionRouteLocator类的方法
RouteDefinitionRouteLocator里面会调用CompositeRouteDefinitionLocator方法获取所有路由定义信息,并转换成真实的Route对象
CompositeRouteDefinitionLocator依次会调用其他RouteDefinitionLocator实现类的方法获取路由定义信息
PropertiesRouteDefinitionLocator类主要是获取配置文件定义的路由信息的,因为GatewayProperties被@ConfigurationProperties(“spring.cloud.gateway”)注解标注,所以它能获取最新的配置
刚刚加入的合同模块路由配置已经被读取到了,如下图,拿到这些信息就可以动态地去更新网关服务的路由信息了,不需要重启服务
此时我们再次访问合同模块的查询接口,可以发现我们已经可以成功访问到合同模块的接口了
四、通过 Spring Boot Actuator实现动态路由
-
利用
GatewayControllerEndpoint
端点暴露路由的 CRUD 操作接口。-
引入pom文件
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
在 yml配置文件中暴露所有端点
management:endpoints:web:exposure: include: "*"
-
GatewayControllerEndpoint类
@RestControllerEndpoint(id = "gateway" ) public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint {public GatewayControllerEndpoint(List<GlobalFilter> globalFilters, List<GatewayFilterFactory> gatewayFilters, List<RoutePredicateFactory> routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {super((RouteDefinitionLocator)null, globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, routeLocator);}// 获取全部路由信息@GetMapping({"/routes"})public Flux<Map<String, Object>> routes() {return this.routeLocator.getRoutes().map(this::serialize);}Map<String, Object> serialize(Route route) {HashMap<String, Object> r = new HashMap();r.put("route_id", route.getId());r.put("uri", route.getUri().toString());r.put("order", route.getOrder());r.put("predicate", route.getPredicate().toString());if (!CollectionUtils.isEmpty(route.getMetadata())) {r.put("metadata", route.getMetadata());}ArrayList<String> filters = new ArrayList();for(int i = 0; i < route.getFilters().size(); ++i) {GatewayFilter gatewayFilter = (GatewayFilter)route.getFilters().get(i);filters.add(gatewayFilter.toString());}r.put("filters", filters);return r;}// 获取单个路由信息@GetMapping({"/routes/{id}"})public Mono<ResponseEntity<Map<String, Object>>> route(@PathVariable String id) {return this.routeLocator.getRoutes().filter((route) -> {return route.getId().equals(id);}).singleOrEmpty().map(this::serialize).map(ResponseEntity::ok).switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));} }
@RestControllerEndpoint
注解作用:在Spring Cloud Gateway中,
@RestControllerEndpoint
注解通常与Actuator端点一起使用,用于暴露管理或监控端点。然而@RestControllerEndpoint
本身并不是Spring Cloud Gateway特有的,而是Spring Boot Actuator提供的一个注解。@RestControllerEndpoint
是@Endpoint
和@RestController
的组合,它允许你定义一个RESTful的Actuator端点。与@Endpoint
(它通常用于WebFlux或MVC的响应式或非响应式端点)不同,@RestControllerEndpoint
始终创建一个RESTful端点。id
属性用于定义端点的唯一标识符,该标识符将用于URL路径(例如,/actuator/{id}
)。 -
-
通过 HTTP 请求(如使用 Postman)向这些接口发送请求,实现路由的添加、删除、查询等操作。
-
添加路由:
actuator/gateway/routes/{id}
-
删除路由:
actuator/gateway/routes/{id}
-
查询单条路由:
actuator/gateway/routes/{id}
-
查询所有路由:
actuator/gateway/routes
-
增删改接口主要在其父类AbstractGatewayControllerEndpoint上
public class AbstractGatewayControllerEndpoint implements ApplicationEventPublisherAware {private static final Log log = LogFactory.getLog(GatewayControllerEndpoint.class);protected RouteDefinitionLocator routeDefinitionLocator;protected List<GlobalFilter> globalFilters;protected List<GatewayFilterFactory> GatewayFilters;protected List<RoutePredicateFactory> routePredicates;protected RouteDefinitionWriter routeDefinitionWriter;protected RouteLocator routeLocator;protected ApplicationEventPublisher publisher;public AbstractGatewayControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, List<GlobalFilter> globalFilters, List<GatewayFilterFactory> gatewayFilters, List<RoutePredicateFactory> routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {this.routeDefinitionLocator = routeDefinitionLocator;this.globalFilters = globalFilters;this.GatewayFilters = gatewayFilters;this.routePredicates = routePredicates;this.routeDefinitionWriter = routeDefinitionWriter;this.routeLocator = routeLocator;}public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {this.publisher = publisher;}// 刷新路由配置接口@PostMapping({"/refresh"})public Mono<Void> refresh() {// 发布RefreshRoutesEvent事件this.publisher.publishEvent(new RefreshRoutesEvent(this));return Mono.empty();}@GetMapping({"/globalfilters"})public Mono<HashMap<String, Object>> globalfilters() {return this.getNamesToOrders(this.globalFilters);}@GetMapping({"/routefilters"})public Mono<HashMap<String, Object>> routefilers() {return this.getNamesToOrders(this.GatewayFilters);}@GetMapping({"/routepredicates"})public Mono<HashMap<String, Object>> routepredicates() {return this.getNamesToOrders(this.routePredicates);}private <T> Mono<HashMap<String, Object>> getNamesToOrders(List<T> list) {return Flux.fromIterable(list).reduce(new HashMap(), this::putItem);}private HashMap<String, Object> putItem(HashMap<String, Object> map, Object o) {Integer order = null;if (o instanceof Ordered) {order = ((Ordered)o).getOrder();}map.put(o.toString(), order);return map;}// 新增接口@PostMapping({"/routes/{id}"})public Mono<ResponseEntity<Object>> save(@PathVariable String id, @RequestBody RouteDefinition route) {// 新增路由定义信息return Mono.just(route).filter(this::validateRouteDefinition).flatMap((routeDefinition) -> {return this.routeDefinitionWriter.save(Mono.just(routeDefinition).map((r) -> {r.setId(id);log.debug("Saving route: " + route);return r;})).then(Mono.defer(() -> {return Mono.just(ResponseEntity.created(URI.create("/routes/" + id)).build());}));}).switchIfEmpty(Mono.defer(() -> {return Mono.just(ResponseEntity.badRequest().build());}));}private boolean validateRouteDefinition(RouteDefinition routeDefinition) {boolean hasValidFilterDefinitions = routeDefinition.getFilters().stream().allMatch((filterDefinition) -> {return this.GatewayFilters.stream().anyMatch((gatewayFilterFactory) -> {return filterDefinition.getName().equals(gatewayFilterFactory.name());});});boolean hasValidPredicateDefinitions = routeDefinition.getPredicates().stream().allMatch((predicateDefinition) -> {return this.routePredicates.stream().anyMatch((routePredicate) -> {return predicateDefinition.getName().equals(routePredicate.name());});});log.debug("FilterDefinitions valid: " + hasValidFilterDefinitions);log.debug("PredicateDefinitions valid: " + hasValidPredicateDefinitions);return hasValidFilterDefinitions && hasValidPredicateDefinitions;}// 删除接口@DeleteMapping({"/routes/{id}"})public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {// 根据id删除路由定义信息return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> {return Mono.just(ResponseEntity.ok().build());})).onErrorResume((t) -> {return t instanceof NotFoundException;}, (t) -> {return Mono.just(ResponseEntity.notFound().build());});}@GetMapping({"/routes/{id}/combinedfilters"})public Mono<HashMap<String, Object>> combinedfilters(@PathVariable String id) {return this.routeLocator.getRoutes().filter((route) -> {return route.getId().equals(id);}).reduce(new HashMap(), this::putItem);} }
-
-
需要注意的是,这种方式没有可视化界面,维护起来可能比较繁琐,因为需要手动调用接口来更新路由信息;如果网关有多个,那么每个网关都要手动调用接口来更新路由信息,非常繁琐;并且这些路由信息是保存在内存中的,一旦重启,这些路由信息就会失效。
-
当然,你也可以重写这些接口,对这些接口实现可视化管理界面,并将这些路由信息保存在数据库中,这样这些路由信息即使重启还会保存下来,不会丢失;对于多个网关都要重复调用接口,我觉得可以集成消息队列进来,这样只要发布更新路由的消息到消息队列中,再由消息队列广播到所有网关中,每个网关再根据消息进行处理即可。
没在yml文件配置暴露所有端点访问获取所有路由信息节点会报错
配置了就不会报错了
五、通过事件刷新机制自定义实现动态路由
在第三点介绍的Nacos基于yml文件的配置就已经可以实现动态路由了,但是我想要将路由配置和该文件隔离,自定义实现动态路由,这样不仅可以集中化配置管理路由信息,也意味着你可以进行更多的自定义扩展操作,这取决于你的动态路由实现逻辑,比如可以实现根据特定条件动态加载或卸载路由规则。
本文提供的例子,仅进行了路由信息的添加操作和动态刷新功能,可根据自己的需求,自定义实现其他扩展逻辑,代码如下:
GatewayRouteEventPublisherAware类
提供动态路由的基础方法,可通过获取bean操作该类的方法,该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
package com.itl.isrm.gateway.context;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;@Slf4j
@Service
public class GatewayRouteEventPublisherAware implements ApplicationEventPublisherAware {/**注入RouteDefinitionWriter实现类InMemoryRouteDefinitionRepository,该类在上面已经介绍过:主要提供了对路由定义信息的增加、删除、查询方法,由一 * 个LinkedHashMap变量存储*/@Autowiredprivate RouteDefinitionWriter routeDefinitionWriter;// 注入事件发布器@Autowiredprivate ApplicationEventPublisher publisher;/*** 增加路由定义信息** @param definition 路由定义* @return*/public String add(RouteDefinition definition) {log.info("新增路由:" + definition);routeDefinitionWriter.save(Mono.just(definition)).subscribe();// 添加完成之后需要发布RefreshRoutesEvent事件,通知CachingRouteLocator类处理RefreshRoutesEvent事件获取最新的路由配置this.publisher.publishEvent(new RefreshRoutesEvent(this));return "success";}/*** 更新路由定义信息** @param definition 路由定义* @return*/public String update(RouteDefinition definition) {log.info("更新路由:" + definition);try {// 先根据id删除路由定义信息this.routeDefinitionWriter.delete(Mono.just(definition.getId()));} catch (Exception e) {return "update fail,not find route routeId: " + definition.getId();}try {routeDefinitionWriter.save(Mono.just(definition)).subscribe();// 添加完成之后需要发布RefreshRoutesEvent事件,通知CachingRouteLocator类处理RefreshRoutesEvent事件获取最新的路由配置this.publisher.publishEvent(new RefreshRoutesEvent(this));return "success";} catch (Exception e) {return "update route fail";}}/*** 删除路由定义信息** @param id 路由ID* @return*/public String delete(String id) {try {// 删除路由定义信息this.routeDefinitionWriter.delete(Mono.just(id));// 发布事件this.publisher.publishEvent(new RefreshRoutesEvent(this));return "delete success";} catch (Exception e) {log.error(e.getMessage(), e);return "delete fail";}}@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.publisher = applicationEventPublisher;}
}
NacosRouteRefreshListener类
主要作用是监听Nacos中的路由文件配置,当该配置文件的配置发生变化时会通知该类进行路由更新
package com.itl.isrm.gateway.listener;import com.alibaba.nacos.api.config.listener.Listener;
import com.itl.isrm.common.util.JsonUtils;
import com.itl.isrm.gateway.context.GatewayRouteEventPublisherAware;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.concurrent.Executor;/*** 动态实时刷新路由配置*/
@Slf4j
@Component
public class NacosRouteRefreshListener implements Listener {@Autowiredprivate GatewayRouteEventPublisherAware gatewayRouteEventPublisherAware;public NacosRouteRefreshListener() {System.out.println("--->>> Init NacosRouteRefreshListener.");}@Overridepublic Executor getExecutor() {return null;}/*** 获取最新的路由定义信息,然后由GatewayRouteEventPublisherAware对路由定义信息进行更新* @param configInfo*/@Overridepublic void receiveConfigInfo(String configInfo) {List<RouteDefinition> list = JsonUtils.toList(configInfo, RouteDefinition.class);list.forEach(definition -> {gatewayRouteEventPublisherAware.update(definition);});}
}
NacosGatewayConfig类
主要作用是配置隔离的路由配置文件地址
package com.itl.isrm.gateway.config;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;/*** 自定义属性绑定值,可通过配置文件配置属性*/
@Configuration
@ConfigurationProperties(prefix = "nacos", ignoreUnknownFields = true)
public class NacosGatewayConfig {private String address;private String dataId;private String groupId;private Long timeout;private String nameSpace;public String getNameSpace() {return nameSpace;}public void setNameSpace(String nameSpace) {this.nameSpace = nameSpace;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public String getDataId() {return dataId;}public void setDataId(String dataId) {this.dataId = dataId;}public String getGroupId() {return groupId;}public void setGroupId(String groupId) {this.groupId = groupId;}public Long getTimeout() {return timeout;}public void setTimeout(Long timeout) {this.timeout = timeout;}}
需在本地配置文件中配置
spring:application:name: isrm-gateway
nacos:address: ${NACOS_HOST:ip:8848}data-id: ${spring.application.name}group-id: isrmtimeout: 6000namespace: ${NAME_SPACE:dev}
NacosRouteListener类
package com.itl.isrm.gateway.listener;import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import com.itl.isrm.common.util.JsonUtils;
import com.itl.isrm.gateway.config.NacosGatewayConfig;
import com.itl.isrm.gateway.context.GatewayRouteEventPublisherAware;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Properties;/*** 服务启动时初始化路由配置信息*/
@Slf4j
@Component
public class NacosRouteListener {// 注入路由文件配置变化监听器@Autowiredprivate NacosRouteRefreshListener nacosRouteRefreshListener;// 注入配置类@Autowiredprivate NacosGatewayConfig nacosGatewayConfig;// 注入路由定义信息接口操作类@Autowiredprivate GatewayRouteEventPublisherAware gatewayRouteEventPublisherAware;/*** 当Bean初始化时执行,初始化路由配置*/@PostConstructpublic void loadRouteByNacosListener() {try {log.info("---->>> init nacos router data.");Properties nacosPro = new Properties();nacosPro.put("serverAddr", nacosGatewayConfig.getAddress());nacosPro.put("namespace", nacosGatewayConfig.getNameSpace());//添加命名空间ConfigService configService = NacosFactory.createConfigService(nacosPro);// 获取Nacos中命名空间为dev的isrm-gateway配置文件的路由定义信息String configInfo = configService.getConfig(nacosGatewayConfig.getDataId(), nacosGatewayConfig.getGroupId(), nacosGatewayConfig.getTimeout());// 新增路由addRoute(configInfo);// 添加srm-gateway配置文件发生变化时的监听器,监听Nacos Server下发的动态路由配置configService.addListener(nacosGatewayConfig.getDataId(), nacosGatewayConfig.getGroupId(), nacosRouteRefreshListener);} catch (NacosException e) {log.error(e.getMessage(), e);}}/*** 添加路由* @param configInfo*/private void addRoute(String configInfo) {if (StringUtils.isBlank(configInfo)) {throw new NullPointerException("route info is null");}// 将字符串转成RouteDefinition对象列表List<RouteDefinition> list = JsonUtils.toList(configInfo, RouteDefinition.class);// 遍历添加路由list.forEach(definition -> {gatewayRouteEventPublisherAware.update(definition);});}}
isrm-gateway配置文件
比如现在配置如下:
[{"id": "auth","order": 0,"predicates": [{"args": {"pattern": "/api/auth/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-auth-provider"},{"id": "system","order": 0,"predicates": [{"args": {"pattern": "/api/system/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-system-provider"},{"id": "basic","order": 0,"predicates": [{"args": {"pattern": "/api/basic/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-basic-provider"},{"id": "sup","order": 0,"predicates": [{"args": {"pattern": "/api/sup/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-sup-provider"}
]
初始化流程
启动网关服务,NacosRouteListener初始化时获取路由定义信息
遍历路由定义信息列表,调用GatewayRouteEventPublisherAware新增路由定义信息到InMemoryRouteDefinitionRepository中,同时发布RefreshRoutesEvent事件
CachingRouteLocator监听RefreshRoutesEvent事件,调用CompositeRouteLocator的getRoutes方法,然后由RouteDefinitionRouteLocator再去调用CompositeRouteDefinitionLocator的getRouteDefinitions方法获取所有的定义信息,然后转换成真实的路由对象
因为CompositeRouteDefinitionLocator持有了InMemoryRouteDefinitionRepository的引用,所以它能获取我们自定义维护的路由定义信息
到此,我们初始化路由配置完成
监听流程
因为上面的配置文件中没有合同模块的路由配置,所以调用合同模块的查询接口会报下面的错误
当我们在原有的配置文件基础上,新增合同模块的路由配置,然后点击发布按钮
[{"id": "auth","order": 0,"predicates": [{"args": {"pattern": "/api/auth/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-auth-provider"},{"id": "system","order": 0,"predicates": [{"args": {"pattern": "/api/system/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-system-provider"},{"id": "basic","order": 0,"predicates": [{"args": {"pattern": "/api/basic/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-basic-provider"},{"id": "sup","order": 0,"predicates": [{"args": {"pattern": "/api/sup/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-sup-provider"},{"id": "contract","order": 0,"predicates": [{"args": {"pattern": "/api/contract/**"},"name": "Path"}],"filters": [{"args": {"parts": "2"},"name": "StripPrefix"}],"uri": "lb://isrm-contract-provider"}
]
此时NacosRouteRefreshListener就能监听到配置文件的配置变化,重新调用GatewayRouteEventPublisherAware类的方法,重新加载新的路由,流程和初始化流程差不多就不讲了
重新调用合同模块的查询接口,发现数据出来了,接口没有报错,到此动态路由功能实现了,无需重启网关服务
相关文章:

SprringCloud Gateway动态添加路由不重启
文章目录 前言:一、动态路由必要性二、SpringCloud Gateway路由加载过程RouteDefinitionLocator接口PropertiesRouteDefinitionLocator类DiscoveryClientRouteDefinitionLocatorInMemoryRouteDefinitionRepositoryCompositeRouteDefinitionLocator类CachingRouteDef…...

Windows安装mysql
首先去官网下载社区版本的mysql(如果连不上,挂梯子) https://www.mysql.com/downloads/ 2. 去配置环境变量path 3. 在cmd里面初始化数据库(在搜索框输入cmd,或者在资源管理器下搜索烂输入cmd回车就行) my…...

chatgpt: linux 下用纯c 编写ui
在Linux下用纯C语言编写用户界面(UI),通常会使用GTK或Xlib。GTK是一个更高级的库,提供了丰富的控件和功能,而Xlib则是一个更底层的库,提供了直接操作X Window系统的功能。 下面是一个使用GTK在Linux上创建…...
Java十六进制Dump打印数据
代码 package test;import java.io.IOException;import sun.misc.HexDumpEncoder;@SuppressWarnings("restriction")...

某棋牌渗透测试
前言 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。 一、信息收集 这里通过fofa进行收集,语法为:body某棋牌 && titlexxx 图1-1 fofa资产收集 …...

JAVA面试(六)
缓存 MemcachedredisRedis常见数据类型和使用Redis缓存持久化RDB-快照AOF-追加文件 Redis数据过期机制惰性删除定期删除Redis缓存淘汰策略(8种)算法LRU (Least Recently Used):最近最少使用LFU(Least Frequ…...

【C语言】手写学生管理系统丨附源码+教程
最近感觉大家好多在忙C语言课设~ 我来贡献一下,如果对你有帮助的话谢谢大家的点赞收藏喔! 1. 项目分析 小白的神级项目,99%的程序员,都做过这个项目! 掌握这个项目,就基本掌握 C 语言了! 跳…...

流媒体传输协议HTTP-FLV、WebSocket-FLV、HTTP-TS 和 WebSocket-TS的详细介绍、应用场景及对比
一、前言 HTTP-FLV、WS-FLV、HTTP-TS 和 WS-TS 是针对 FLV 和 TS 格式视频流的不同传输方式。它们通过不同的协议实现视频流的传输,以满足不同的应用场景和需求。接下来我们对这些流媒体传输协议进行剖析。 二、传输协议 1、HTTP-FLV 介绍:基于 HTTP…...

【机器学习】线性回归:从基础到实践的深度解析
🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 💫个人格言: "如无必要,勿增实体" 文章目录 线性回归:从基础到实践的深度解析引言一、线性回归基础1.1 定义与目…...

短视频开源项目MoneyPrinterTurbo:AI副业搞起来,视频制作更轻松!
目录 引言一、MoneyPrinterTurbo简介二、MoneyPrinterTurbo的核心功能三、MoneyPrinterTurbo的未来发展四、MoneyPrinterTurbo与AI副业五、部署实践1、克隆代码2、创建虚拟环境3、安装依赖4、安装好 ImageMagick5、端口映射6、启动Web界面7、模型配置8、填写主题9、视频生成10、…...

【JAVA】SpringBoot + skywalking 将接口的入参、出参、异常等信息上报到skywalking 链路追踪服务器上
【JAVA】SpringBoot skywalking 将接口的入参、出参、异常等信息上报到skywalking 链路追踪服务器上 1.下载SkyWalking APM https://skywalking.apache.org/downloads/ jdk8 不支持 SkyWalking APM 9.3.0以上版本,所以这里我们下载 9.3.0版本 2.下载 Java Agent …...
[xmake]构建静态库和动态库
xmake 静态库和动态库 在xmake中创建静态库和动态库的方法非常相似。以下是创建静态库和动态库的基本步骤: 创建xmake工程文件(xmake.lua)。 配置工程属性,包括工程名、版本等。 添加源代码文件到工程中。 设置是创建静态库还…...

功能测试 之 单模块测试----轮播图、登录、注册
单功能怎么测? 需求分析 拆解测试点 编写用例 1.轮播图 (1)需求分析 位置:后台--页面--广告管理---广告列表(搜索index页面增加广告位2) 操作完成后需要点击admin---更新缓存,前台页面刷新生效 (2)拆解…...
MyBatis-PageHelper 源码解说
归档 GitHub: MyBatis-PageHelper-源码解说 总说明 源码仓库: https://github.com/pagehelper/Mybatis-PageHelper克隆:git clone https://github.com/pagehelper/Mybatis-PageHelper.git切分支(tag):git checkout m…...

基于uni-app和图鸟UI的智慧校园圈子小程序开发实践
摘要: 随着教育信息化和“互联网教育”的快速发展,智慧校园建设已成为推动校园管理现代化、提高教育教学质量的重要手段。本文介绍了基于uni-app和图鸟UI开发的智慧校园圈子小程序,旨在通过一站式服务、个性化定制、数据互通和安全可靠等特点…...

STM32 keil工程移植到Visual Studio Code环境中编译
1、GCC Vscode 搭建 STM32 开发环境 GCC Vscode 搭建 STM32 开发环境(一)- 环境部署 - 知乎 (zhihu.com) 2、在原有keil工程下找到原本CUBEMX生成的.ioc工程文件 3、将.ioc文件复制一个新的文件夹下双击打开工程,将IDE选为Makefile&…...
细说CountDownLatch
CountDownLatch是Java中提供的一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。在面试中,面试官经常会询问候选人是否在实际项目中使用过CountDownLatch,以评估其对多线程编程和并发控制的理解和经验。本文将详细介绍CountDownLat…...
java-克隆应用
5.2 创建复杂对象 对于某些复杂对象,通过克隆来创建其副本比通过构造函数创建新实例更加高效。例如,当对象包含大量字段或需要进行复杂初始化时,克隆可以显著提高性能。 java 复制代码 class ComplexObject implements Cloneable { private …...

RPC协议
3.8 既然有 HTTP 协议,为什么还要有 RPC 假设我们需要在 A 电脑的进程发一段数据到 B 电脑的进程,我们一般会在代码里使用 Socket 进行编程。 这时候,我们可选项一般也就 TCP 和 UDP 二选一。TCP 可靠,UDP 不可靠。 类似下面这…...

医疗器械3D全景展会在线漫游创造数字化时代的展览新篇章
在数字化浪潮的引领下,VR虚拟网上展会正逐渐成为企业展示品牌实力、吸引潜在客户的首选平台。我们与广交会携手走过三年多的时光,凭借优质的服务和丰富的经验,赢得了客户的广泛赞誉。 面对传统展会活动繁多、企业运营繁忙的挑战,许…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...

Yolov8 目标检测蒸馏学习记录
yolov8系列模型蒸馏基本流程,代码下载:这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中,**知识蒸馏(Knowledge Distillation)**被广泛应用,作为提升模型…...

【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

论文阅读:LLM4Drive: A Survey of Large Language Models for Autonomous Driving
地址:LLM4Drive: A Survey of Large Language Models for Autonomous Driving 摘要翻译 自动驾驶技术作为推动交通和城市出行变革的催化剂,正从基于规则的系统向数据驱动策略转变。传统的模块化系统受限于级联模块间的累积误差和缺乏灵活性的预设规则。…...

门静脉高压——表现
一、门静脉高压表现 00:01 1. 门静脉构成 00:13 组成结构:由肠系膜上静脉和脾静脉汇合构成,是肝脏血液供应的主要来源。淤血后果:门静脉淤血会同时导致脾静脉和肠系膜上静脉淤血,引发后续系列症状。 2. 脾大和脾功能亢进 00:46 …...
32位寻址与64位寻址
32位寻址与64位寻址 32位寻址是什么? 32位寻址是指计算机的CPU、内存或总线系统使用32位二进制数来标识和访问内存中的存储单元(地址),其核心含义与能力如下: 1. 核心定义 地址位宽:CPU或内存控制器用32位…...

2025-05-08-deepseek本地化部署
title: 2025-05-08-deepseek 本地化部署 tags: 深度学习 程序开发 2025-05-08-deepseek 本地化部署 参考博客 本地部署 DeepSeek:小白也能轻松搞定! 如何给本地部署的 DeepSeek 投喂数据,让他更懂你 [实验目的]:理解系统架构与原…...