微服务系列三:微服务核心——网关路由

news2024/11/26 8:17:50

目录

前言

一、登录存在的问题归纳

二、*微服务网关整体方案

三、认识微服务网关

四、网关鉴权实现 

五、OpenFeign微服务间用户标识信息传递实现

六、微服务网关知识追问巩固


前言

本篇文章具体讲解微服务中网关的实现逻辑、用于解决什么样的问题。其中标题中标注* 涉及到非常巧妙的设计或者核心配置。需要仔细阅读并理解,相信对你理解微服务、学习微服务会有很大的帮助!

通过这篇文章学习后,希望你能回答第六节中所有的问题。

当前微服务项目大致是被我们拆分出来了,分别是以下几个模块:

用户服务、商品服务、购物车服务、交易服务、支付服务

但是拆分模块后,还是会出现不少问题,主要的问题归纳如下:

一、登录存在的问题归纳

1. 请求不同数据时要访问不同的入口,需要维护多个入口地址。

原先单体项目的时候,前端只需要将请求转发到8080端口,就可以访问单体项目中所有的方法了。但是现在多入口模式下,前端要如何维护请求的转发呢?

2. 前端无法调用nacos,无法实时更新服务列表

前端nginx无法直接和nacos交互,怎么知道这个入口地址还可用,如果宕机了还怎么发送?

3.每个微服务都需要编写登录校验、用户信息获取的功能

最简单的做法是每一个需要校验的微服务模块都配置一个JWT校验。但是这样明显不太优雅,本节是为了提供一种统一校验的方法。利用网关解决这个问题。

4. 微服务模块如何获取用户标识信息?

只有通过JWT校验才会有用户标识信息。也就是说通过网关校验后,微服务模块又要如何获取到这些用户标识信息呢?

5. 微服务内如何使用用户标识信息?

可以使用TreadLocal保存用户信息供本模块方法使用。

6. 微服务间进行调用,OpenFeign如何发送用户标识信息?

我们需要想办法将用户标识信息写到请求头中,随着OpenFeign请求一同发送。

 想要解决上述问题,我们需要引入“网关”的概念。

二、*微服务网关整体方案

提前将整体方案总结,如果能看得明白,接下来的具体实现想必也不是问题:

2.1 后端黑盒

解决问题一。我们在前端和后端微服务项目之中添加了一层网关微服务。并且设置网关微服务的端口为8080。这样一来,前端代码无需修改,只需将所有请求发送到8080,接着由我们的网关微服务进行请求转发和负载均衡。实现了黑盒效果


2.2 网关模块的作用

1. 实现前端请求的统一处理转发、微服务列表维护、负载均衡

网关直接和Nacos注册中心进行交互,获取实时的服务列表。自动完成服务的转发和负载均衡。

2. 网关过滤器实现鉴权统一认证(JWT令牌)

既然所有的前端请求都会访问到网关模块,那我们完全可以在网关模块添加一个过滤器用于JWT令牌的统一校验。只有通过了JWT令牌校验的请求才会被转发。否则网关会返回401权限不足的异常。


2.3 微服务拦截器作用

实现用户标识信息的保存

具体的。当网关通过了JWT令牌校验后,我们需要通过拦截器将网关传递的用户标识信息保存到ThreadLocal中,供当前微服务模块使用。

然而,如果在每一个微服务都编写一个拦截器还是太不优雅了。对此,我们可以将MVC的拦截器编写到公共模块 common-service中,其他模块引用即可。


2.4 OpenFeign拦截器作用

实现微服务间调用的用户标识信息传递

前面微服务拦截器解决了如何保存用户标识信息的问题。但是这种拦截器能够保存用户标识信息的前提是——请求必须通过网关转发而来。

然而微服务间的调用是通过OpenFeign工具完成的。因此无法使用MVC拦截器获取到用户标识信息。这就要求我们在微服务请求发送前,要想办法给请求添加上用户标识信息。

如何呢?一样是利用拦截器将OpenFeign请求先拦截下来,将用户标识信息添加到请求头

三、认识微服务网关

3.1 网关模式的演示

前端:我到底该发给谁?

 网关:你发给我就行了,接下来的事你不用管!

网关:我要怎么知道我管理的微服务的列表呢?

注册中心:别担心,你找我就行了!


3.2 *编写网关模块

实现底层

在SpringCloud中网关的实现包括两种:

使用GateWay更好喔!

使用步骤

  • 创建网关模块
  • 引入网关依赖——SpringCloudGateway、NacosDiscovery
  • 编写网关启动类
  • 配置网关转发规则——路由规则

1. 创建网关模块

2. 引入网关依赖

        <dependencies>
            <!--common-->
            <dependency>
                <groupId>com.heima</groupId>
                <artifactId>hm-common</artifactId>
                <version>1.0.0</version>
            </dependency>
            <!--网关-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <!--nacos discovery-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--负载均衡-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
        </dependencies>
        <build>
            <finalName>${project.artifactId}</finalName>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>

3. 编写启动类

package com.hmall.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GateWayApplication {


    public static void main(String[] args) {
        SpringApplication.run(GateWayApplication.class, args);
    }
}

4. 编写路由规则

server:
  port: 8080 # 网关端口 前端请求统一处理

spring:
  application:
    name: hm-gateway # 微服务名称

  cloud:
    nacos:
      server-addr: 192.168.186.140:8848 # nacos地址

    gateway:
      routes:
        - id: cart # 路由id,自定义
          uri: lb://cart-service # 目标服务地址 lb://服务名称  lb表示负载均衡
          predicates:
            - Path=/carts/** # 断言,路径匹配  路径以/api/cart/开头都会被路由到cart-service
        - id: item
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/** # 多路径匹配以逗号分割
        - id: pay
          uri: lb://pay-service
          predicates:
            - Path=/pay-orders/**
        - id: trade
          uri: lb://trade-service
          predicates:
            - Path=/orders/**
        - id: user
          uri: lb://user-service
          predicates:
            - Path=/users/**,/addresses/**

3.3 测试网关转发功能

3.4 *网关路由属性

前面我们初步配置了网关路由规则。实际上,Gateway路由还有很多功能,我们窥探一下源码了解一下吧!

网关路由对应的Java类型是RouteDefinition,其中常见的属性有:

  • id:路由唯一标示
  • uri:路由目标地址
  • predicates:路由断言,判断请求是否符合当前路由。
  • filters:路由过滤器,对请求或响应做特殊处理。

【路由断言】

在Java中以工厂模式实现:Spring提供了12种基本的RoutePredicateFactory实现:

Spring Cloud Gatewayicon-default.png?t=O83Ahttps://docs.spring.io/spring-cloud-gateway/docs/3.1.7/reference/html/#gateway-request-predicates-factories

名称

说明

示例

After

是某个时间点后的请求

- After=2037-01-20T17:42:47.789-07:00[America/Denver]

Before

是某个时间点之前的请求

- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]

Between

是某两个时间点之前的请求

- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]

Cookie

请求必须包含某些cookie

- Cookie=chocolate, ch.p

Header

请求必须包含某些header

- Header=X-Request-Id, \d+

Host

请求必须是访问某个host(域名)

- Host=**.somehost.org,**.anotherhost.org

Method

请求方式必须是指定方式

- Method=GET,POST

Path

请求路径必须符合指定规则

- Path=/red/{segment},/blue/**

Query

请求参数必须包含指定参数

- Query=name, Jack或者- Query=name

RemoteAddr

请求者的ip必须是指定范围

- RemoteAddr=192.168.1.1/24

weight

权重处理

- Weight=group1, 2

XForwarded Remote Addr基于请求的来源IP做判断- XForwardedRemoteAddr=192.168.1.1/24

过滤器

网关中提供了33种路由过滤器,每种过滤器都有独特的作用Spring Cloud Gatewayicon-default.png?t=O83Ahttps://docs.spring.io/spring-cloud-gateway/docs/3.1.7/reference/html/#gateway-request-predicates-factories

四、网关鉴权实现 

我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:

  • 每个微服务都需要知道JWT的秘钥,不安全

  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦

所以我们把JWT校验提前到网关模块来做,成功则请求到对应的模块,失败则返回401报错信息。

4.1 网关请求处理流程分析

我们采取的网关gateway底层是如何进行处理的呢?

  • 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  • WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。

  • 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。

  • 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。

  • 微服务返回结果后,再倒序执行Filterpost逻辑。

  • 最终把响应结果返回。

这里我们主要关注过滤器。有以下特点:

  • 第一:过滤器的PRE部分在发送请求到微服务之前执行,因此时候在这部分实现JWT校验
  • 第二:过滤器有先后顺序之分,默认Netty路由过滤器是优先级最低的。用于请求转发。
  • 第三:我们编写的JWT鉴权过滤器要确保优先级比Netty路由器高。

4.2 自定义过滤器实践 

自定义过滤器分为两种:

  • GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效。
  • GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。

其中全局过滤器的实现更加简单,两者的过滤方法同名同参数

/**
 * 处理请求并将其传递给下一个过滤器
 * @param exchange 当前请求的上下文,其中包含request、response等各种数据
 * @param chain 过滤器链,基于它向下传递请求
 * @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
 */
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

4.2.1 GlobalFilter实现

1. 创建过滤器,继承GlobalFilter 和 Ordered类,编写filter方法

2. 测试执行情况


4.2.2 GatewayFilter实现

 【过滤器模板】、【简单无参过滤器

装饰模式】、【配置过滤器优先级

过滤器模板】、【有参过滤器的实现

生效范围配置

4.3 *实现网关登录校验

使用简单的GlodalFilter实现登录校验过滤器,基本步骤如下:

  • 配置JWT校验工具
  • 编写登录校验过滤器
  • 测试网关拦截效果

 1. 导入JWT相关工具类

  • AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问

  • JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置

  • SecurityConfig:工具的自动装配

  • JwtTool:JWT工具,其中包含了校验和解析token的功能

  • hmall.jks:秘钥文件

2. 编写登录校验过滤器

  1. 使用构造器注入配置
  2. 获取请求头
  3. 判断请求路径是否在放行白名单里
  4. 获取请求头中的token
  5. 解析并校验token
    1. 如果token无效,只需拦截,设置响应状态码401
  6. TODO: token有效,传递用户标识信息
  7. 放行
package com.hmall.gateway.fliters;


import cn.hutool.core.collection.CollUtil;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@Component // 将过滤器交给Spring管理
@RequiredArgsConstructor // 采用构造器注入(只会注入 final)
@EnableConfigurationProperties(AuthProperties.class) // 开启属性配置
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    // 注入Jwt工具类
    private final JwtTool jwtTool;

    // 注入配置文件中的属性
    private final AuthProperties authProperties;

    // **(重要) 路径匹配器,用于匹配路径是否在白名单中
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 登录校验开始
        //1. 获取request
        ServerHttpRequest request = exchange.getRequest();
        //2. 判断路径是否在白名单中
        if(isExclude(request.getURI().getPath())){
            // 无需拦截 直接放行
            return chain.filter(exchange);
        }
        //3. 获取请求头中的token
        String token = null;
        List<String> headers = request.getHeaders().get(HttpHeaders.AUTHORIZATION);
        if(!CollUtil.isEmpty(headers)){ // 请求头中存在token
            token = headers.get(0); // 获取token
        }
        //4. 解析并校验token
        Long userId = null;
        try{
            userId = jwtTool.parseToken(token);
        }catch (UnauthorizedException e){
            // 如果token无效,拦截返回401即可
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete();  // 直接返回响应,不会再往下走了
        }
        //TODO 5. 如果有效,传递用户标识信息
        System.out.println("userId = " + userId);
        //6. 放行
        return chain.filter(exchange);
    }

    /**
     * 判断路径是否在白名单中
     * @param path
     * @return
     */
    private boolean isExclude(String path) {
        for(String pathPattern : authProperties.getExcludePaths()){ // 遍历白名单
            if(antPathMatcher.match(pathPattern,path)) { // 如果路径匹配上了,则返回true
                return true;
            }
        }
        return false;
    }

    // 过滤器优先级,值越小优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

3. 测试网关拦截效果 

4.4 通过网关传递用户标识信息

上一节中,我们还有一步没有完成:

网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?

由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。


4.4.1 *改造网关过滤器

在获取用户信息后保存到请求头,转发到下游微服务

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 登录校验开始
        //1. 获取request
        ServerHttpRequest request = exchange.getRequest();
        //2. 判断路径是否在白名单中
        if(isExclude(request.getURI().getPath())){
            // 无需拦截 直接放行
            return chain.filter(exchange);
        }
        //3. 获取请求头中的token
        String token = null;
        List<String> headers = request.getHeaders().get(HttpHeaders.AUTHORIZATION);
        if(!CollUtil.isEmpty(headers)){ // 请求头中存在token
            token = headers.get(0); // 获取token
        }
        //4. 解析并校验token
        Long userId = null;
        try{
            userId = jwtTool.parseToken(token);
        }catch (UnauthorizedException e){
            // 如果token无效,拦截返回401即可
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete();  // 直接返回响应,不会再往下走了
        }
        //5. 如果有效,将userId放到请求头中,继续往下走
        String userInfo = userId.toString();
        ServerWebExchange ex = exchange.mutate()
                .request(b -> b.header("user-info",userInfo))
                .build();
        //6. 放行
        return chain.filter(ex);
    }

4.4.2 *编写微服务拦截器

拦截请求获取用户信息,保存到ThreadLocal后放行。

为了提高代码复用,我们SpringMVC的拦截器最好写在通用模块中:hm-common。

在hm-common中已经有一个用于保存登录用户的ThreadLocal工具——UserContext,我们只需要编写拦截器将用户信息保存到UserContext即可。

基本步骤

  • 定义SpringMVC拦截器
  • 编写拦截器代码
  • 注册拦截器

    注意啦!!!

    拦截器中记得一定要加上这么两个注解:

    @Configuration

    @ConditionalOnClass(DispatcherServlet.class)

    其中第二个注解不能少,少了马上报错,原因如下:

    第二个配置表示:

    当DispatcherServlet类存在时这个配置类才会生效
    
    * 为什么要这么做呢? 因为网关模块也依赖了common模块,而网关模块底层gateway是没有走spring mvc的,
    * 所以这个配置类在网关模块中是不需要的,所以需要加上这个注解
    * 不加这个注解的话,在网关模块启动的时候会报错,因为配置不生效
  • 配置自动装配,使配置类生效

    这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

    基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件让其自动装配

编写拦截器代码,只做判断不拦截

package com.hmall.common.interceptor;

import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserInfoInterceptor implements HandlerInterceptor {

    /**
     * 在请求处理之前进行调用(Controller方法调用之前)
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 获取请求头中的用户信息
        String userInfo = request.getHeader("userInfo");
        //2. 判断用户信息是否为空
        if(StrUtil.isNotBlank(userInfo)){
            // 不为空,将用户信息存入到ThreadLocal中
            UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3. 放行
        return true;
    }

    /**
     * 在请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清除ThreadLocal中的用户信息
        UserContext.removeUser();
    }
}

注册拦截器

package com.hmall.common.config;

import com.hmall.common.interceptor.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.text.DecimalFormat;


@Configuration // 配置类

/**
 * mvc配置类 当DispatcherServlet类存在时这个配置类才会生效

 * 为什么要这么做呢? 因为网关模块也依赖了common模块,而网关模块底层gateway是没有走spring mvc的,
 * 所以这个配置类在网关模块中是不需要的,所以需要加上这个注解
 
 * 不加这个注解的话,在网关模块启动的时候会报错,因为配置不生效
 */
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {

    /**
     * 添加拦截器
     * @param registry
     */
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

配置自动装配,使配置类生效

测试网关作用

五、OpenFeign微服务间用户标识信息传递实现

5.1 网关请求 和 OpenFeign请求的区别

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

但是这也是一种局限。只有通过网关转发的请求才有用户信息的传递。而在微服务项目中,有很多稍微复杂点的业务需要多个微服务模块协调,此时请求是通过OpenFeign发送的,而不是网关转发的。自然而然地无法传递用户标识信息。

但是,如果不传递用户标识信息,业务就无法正常进行。例如购物车微服务在下单结算后需要调用商品微服务清除购物车信息,但是如果不传递当前购物车用户的ID信息,如何正确清除商品信息呢?

因此,我们本节就是为了解决微服务间传递用户标识信息的。

5.2 如何使OpenFeign请求携带用户信息?

将UserContext中的用户表示信息在发送OpenFeign请求前,存入请求头一并发出给目标微服务。

5.3 如何使所有OpenFeign请求的请求头携带用户信息?

使用Feign中提供的一个拦截器接口feign.RequestInterceptor,确保请求在发出之前都能携带上用户标识信息。

5.4 *编写OpenGeign拦截器

  • 创建拦截器
    • 该写在哪呢?为了让所有微服务调用OpenFeign请求都能添加上请求用户信息,我们需要将拦截器写在hm-api 这个ClientAPI模块中
  • 编写拦截器代码
    • 这个拦截器需要像前一节写的拦截器一样先注册再使用么?不需要的哦!因为这个拦截器的底层并不是走SpringMvc那套的,直接使用即可!!
  • 测试

/**
     * 拦截器: 将登录用户信息放入请求头中,传递给下游微服务
     * @return
     */
    @Bean
    public RequestInterceptor userInfoRequestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                // 获取登录用户
                Long userId = UserContext.getUser();
                if(userId == null) {
                    // 如果为空则直接跳过
                    return;
                }
                // 如果不为空则放入请求头中,传递给下游微服务
                template.header("user-info", userId.toString());
            }
        };
    }

测试,清除选中的购物车信息:

好了,现在微服务之间通过OpenFeign调用时也会传递登录用户信息了。

六、微服务网关知识追问巩固

1. 拆分微服务模块后,前端如何知道要请求到哪个模块?如何解决这个问题?

2. 请你概述一下网关模块的基本方案是什么?

3. 微服务为什么添加网关层?谈谈网关层的作用是什么?

4. 微服务网关方案动用了“一个过滤器、两个拦截器”,它们分别的用途是什么?底层的实现有有什么不同?

5. 微服务中是如何更好地解决登录校验问题的?应该在哪里实现登录校验功能?

6. 谈谈gateway网关过滤器有几种实现形式?实现的步骤分别是什么?

6. 微服务模块如何获取用户标识信息?

7. 微服务中是怎么解决跨微服务请求中用户标识信息传递问题的?应该在哪里实现该功能?

8. 请你谈谈一下SpringMvc拦截器的使用步骤?

9. 假设配置的拦截器不在包扫描范围下,你要如何配置从而确保拦截器生效呢?

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

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

相关文章

开源一个开发的聊天应用与AI开发框架,集成 ChatGPT,支持私有部署的源码

大家好&#xff0c;我是一颗甜苞谷&#xff0c;今天分享一个开发的聊天应用与AI开发框架&#xff0c;集成 ChatGPT&#xff0c;支持私有部署的源码。 介绍 当前系统集成了ChatGPT的聊天应用&#xff0c;不仅提供了基本的即时通讯功能&#xff0c;还引入了先进的AI技术&#x…

心觉:人每日60000念头,如何让你的时间精力只专注于核心目标?

Hi&#xff0c;我是心觉&#xff0c;带你用潜意识化解各种焦虑、内耗&#xff0c;建立无敌自信&#xff1b;教你财富精准显化的实操方法&#xff1b;关注我,伴你一路成长&#xff01; 每日一省写作220/1000天 据说一个人每天会产生60000个念头 有些我们的意识能察觉到&#x…

万宇科技闪耀创新舞台 荣膺潜在独角兽企业殊荣

2024年10月24日&#xff0c;在“2024东北亚(沈阳)人才交流大会暨中国潜在独角兽企业发展大会”上&#xff0c;长城战略咨询重磅发布《GEI中国潜在独角兽企业研究报告2024》&#xff0c;揭示了中国潜在独角兽企业群体的最新发展态势。其中&#xff0c;安徽万宇机械设备科技有限公…

论文阅读:Computational Long Exposure Mobile Photography (一)

这篇文章是谷歌发表在 2023 ACM transaction on Graphic 上的一篇文章&#xff0c;介绍如何在手机摄影中实现长曝光的一些拍摄效果。 Abstract 长曝光摄影能拍出令人惊叹的影像&#xff0c;用运动模糊来呈现场景中的移动元素。它通常有两种模式&#xff0c;分别产生前景模糊或…

数据结构与算法 - 基础

本文首发于 个人博客 程序 数据结构 算法 其实很多同学知道数据结构与算法很重要&#xff0c;但是却不明觉厉。 这里我们看一个简单的题&#xff1a; 对自然数从1到100的求和 最简单的设计无非是&#xff1a; void addNum () { int total 0; for (int i 1; i < 1…

算法简介:动态规划

动态规划 1. 动态规划2. 案例2.1 旅游行程最优化 1. 动态规划 背包问题&#xff1a;背包可以容纳的重量是4磅&#xff0c;吉他为1磅&#xff0c;价值1500元&#xff1b;音响为4磅&#xff0c;价值3000元&#xff1b;笔记本电脑为3磅&#xff0c;价值为2000元。如何在背包中放入…

GPU 学习笔记四:GPU多卡通信(基于nccl和hccl)

文章目录 一、前沿1.1 背景回顾1.2 XCCL在AI通信架构中的位置和作用 二、英伟达GPU通信 nccl2.1 NCCL简介2.2 通信模式2.2.1 通信模式分类2.2.2 通信模式分析2.2.3 通信nccl编程实例 2.3 NCCL通信算法2.3.1 环形算法 ring algorithm2.3.2 树形算法 tree algorithm 防止遗忘和后…

最新PHP校园源码系统开发(多客社区校园系统源码全套APP源码附搭建教程)

最新PHP校园源码系统开发、多客社区校园系统源码以及全套APP源码附搭建教程的需求 一、最新PHP校园源码系统开发 技术栈选择&#xff1a; 后端&#xff1a;PHP&#xff08;建议使用PHP 7.2或更高版本&#xff09;数据库&#xff1a;MySQL&#xff08;建议使用MySQL 5.6或更高版…

Netty 组件介绍 - ByteBuf

直接内存&堆内存 ByteBuf buffer ByteBufAllocator.DEFAULT.heapBuffer(10);ByteBuf byteBuf ByteBufAllocator.DEFAULT.directBuffer(10); 组成 ByteBuf维护了两个不同的索引&#xff0c;一个用于读取&#xff0c;一个用于写入。 写入 内存回收 堆内存使用的是JVM内…

Java项目实战II基于Java+Spring Boot+MySQL的高校办公室行政事务管理系统(源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 在高等教育…

word mathml 创建粗体字母快捷键

在 mathml 中达到latex中 \mathbf{A} 的效果 由于word本身不支持这个命令&#xff0c;所以打算用快捷键实现 快捷键的功能是加粗光标前一个字目 1. Alt F8 打开宏&#xff0c;如果打不开可以尝试 Alt Fn F8 2. 输入 BoldPreviousCharacter 新建宏&#xff1a; Sub Bold…

redis实现分布式锁,go实现完整code

Redis分布式锁 Redis 分布式锁是一种使用 Redis 数据库实现分布式锁的方式&#xff0c;可以保证在分布式环境中同一时间只有一个实例可以访问共享资源。 实现机制 以下是实现其加锁步骤&#xff1a; 获取锁 在 Redis 中&#xff0c;一个相同的key代表一把锁。是否拥有这把锁&…

flink 自定义kudu connector中使用Metrics计数平均吞吐量,并推送到自定义kafkaReporter

文章目录 前言1. Registering metrics2. Metrics 的类型2.1 counter2.2 Gauge2.3 Histogram2.4 meter 3. 指标划分3.1 指标所属的范围3.2 默认所属 4. 自定义kudu connector中使用Metrics4.1 sink算子继承RichFunction4.2 注册指标4.3 计数逻辑4.4 自定义Reporter&#xff0c;推…

柯桥日语培训|N1常考语法:~(よ)うが/(よ)うと——“无论……都……”

&#xff5e;&#xff08;よ&#xff09;うが&#xff0f;&#xff08;よ&#xff09;うと 接续&#xff1a;動意向形&#xff0f;イ形→かろう&#xff0f;名、ナ形→だろう・であろう&#xff0b;が&#xff0f;と 说明&#xff1a;表示假定条件的逆接&#xff0c;无论前项如…

一个基于Zookeeper+Dubbo3+SpringBoot3的完整微服务调用程序示例代码

一、关于 Dubbo3 的一些优化改进介绍 Dubbo3 的官方文档地址&#xff1a; https://cn.dubbo.apache.org/zh-cn/overview/what/overview/ 其针对一些问题进行了优化和改变。个人整理3个小的方面&#xff1a; 1. 在服务注册方面使用 DubboService 注解&#xff0c;不再使用 Servi…

电能表预付费系统-标准传输规范(STS)(33)

6.5.4.4 Key rotation process 按键旋转过程 The entire key is rotated one bit position to the left as illustrated in Figure 15.整个密钥向左旋转一个位&#xff0c;如图15所示。 6.5.4.5 Worked example to generate TokenData for a TransferCredit token using the S…

时序数据库是什么:概念、特点与分类简析

时序数据与时序数据库的“保姆级”科普&#xff01; 作为将数据价值转化为产能能效的“核心大脑”&#xff0c;数据库的发展依然处于加速期&#xff0c;面向不同数据类型的数据库类型也在不断增加。 在众多细分领域数据库类型中&#xff0c;伴随制造业数字化转型的行业趋势和多…

【创建型】单例模式

单例模式使用的场景&#xff1a;需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即&#xff1a;重量级对象)&#xff0c;但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等) 1. 饿汉式&#xff08;静态常量&#xf…

6.0、静态路由

路由器最主要的功能就是转发数据包。路由器转发数据包时需要查找路由表&#xff08;你可以理解为地图&#xff09;&#xff0c;管理员可以直接手动配置路由表&#xff0c;这就是静态路由。 1.什么是路由&#xff1f; 在网络世界中&#xff0c;路由是指数据包在网络中的传输路…

工业通信网关的各项功能解析-天拓四方

在工业自动化和智能制造的浪潮中&#xff0c;工业通信网关作为连接工业现场与互联网的重要桥梁&#xff0c;发挥着至关重要的作用。它不仅实现了不同网络协议之间的转换&#xff0c;还在数据采集、设备控制、网络管理等方面展现出强大的功能。 一、协议转换功能 工业通信网关…