微服务开发系列 第十篇:Gateway

news2024/11/18 3:23:12

总概

A、技术栈

  • 开发语言:Java 1.8
  • 数据库:MySQL、Redis、MongoDB、Elasticsearch
  • 微服务框架:Spring Cloud Alibaba
  • 微服务网关:Spring Cloud Gateway
  • 服务注册和配置中心:Nacos
  • 分布式事务:Seata
  • 链路追踪框架:Sleuth
  • 服务降级与熔断:Sentinel
  • ORM框架:MyBatis-Plus
  • 分布式任务调度平台:XXL-JOB
  • 消息中间件:RocketMQ
  • 分布式锁:Redisson
  • 权限:OAuth2
  • DevOps:Jenkins、Docker、K8S

B、本节实现目标

  • 新建mall-gateway服务,所有请求通过Gateway转发
  • Gateway鉴权token
  • Gateway配置白名单
  • 所有服务swagger通过gateway访问,并提供下列列表选择服务
  • @RestControllerAdvice拦截Controller返回统一格式数据
  • @ControllerAdvice拦截返回统一格式Exception

一、API Gateway

API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性。

  • 存在跨域请求,在一定场景下处理相对复杂。

  • 认证复杂,每个服务都需要独立认证。

  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。

  • 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:

API 网关

二、Spring Cloud Gateway简介

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

由于Spring 5.0支持 Netty,Http2,而Spring Boot 2.0支持Spring 5.0,因此Spring Cloud Gateway支持 Netty和Http2。

补充:
1、Zuul(1.x) 基于 Servlet,使用阻塞 API,它不支持任何长连接 ,如 WebSockets。
2、Zuul(2.x) 基于Netty。
3、Spring Cloud GateWay天⽣就是异步⾮阻塞的,基于Reactor模型,支持 WebSockets,支持限流等新特性。
4、Spring Cloud 已经不再集成 Zuul 2.x 。

三、架构说明

认证服务(mall-auth)负责认证授权,网关服务(mall-gateway)负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。

具体服务:

  • [mall-auth]:认证服务,负责对登录用户进行认证授权颁发token。
  • [mall-gateway]:网关服务,负责请求转发和校验认证和鉴权。
  • [mall-member]:受保护的API服务,用户鉴权通过后可以访问该服务,该类服务还有[mall-product]、[mall-search]等等。

四、代码实现

4.1 新建mall-gateway服务

新建mall-gateway服务用户token鉴权、API请求转发

4.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>mall-pom</artifactId>
        <groupId>com.ac</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <groupId>com.ac</groupId>
    <artifactId>mall-gateway</artifactId>
    <version>${mall.version}</version>
    <name>mall-gateway</name>
    <description>网关服务</description>

    <dependencies>
        <dependency>
            <groupId>com.ac</groupId>
            <artifactId>mall-core</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.ac</groupId>
            <artifactId>mall-oauth2-module</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- Spring Cloud Gateway 是使用 netty+webflux 实现因此不需要再引入 web 模块 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>4.0.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.3 配置路由Route(路由)、白名单

bootstrap-dev.yml

server:
  port: 6001

spring:
  application:
    name: mall-gateway

  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: dev_id
        file-extension: yml
        shared-configs:
          - data-id: common.yml
            group: DEFAULT_GROUP
            refresh: true
      discovery:
        namespace: dev_id

    gateway:
      routes:
        - id: mall-member-route            # 当前路由的标识, 要求唯一
          uri: lb://mall-member            # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
          predicates:
            - Path=/mall-member/**         # 当请求路径满足Path指定的规则时,才进行路由转发
          filters:
            - StripPrefix=1                # 转发之前去掉1层路径

        - id: mall-search-route
          uri: lb://mall-search
          predicates:
            - Path=/mall-search/**
          filters:
            - StripPrefix=1

        - id: mall-product-route
          uri: lb://mall-product
          predicates:
            - Path=/mall-product/**
          filters:
            - StripPrefix=1

        - id: mall-order-route
          uri: lb://mall-order
          predicates:
            - Path=/mall-order/**
          filters:
            - StripPrefix=1
 
#gateway swagger开关
swagger:
  enable: true

#配置白名单路径
mall:
  security:
    ignore:
      urls:
        - "/**/member/list"
        - "/**/redis/**"

重点说明一下配置,- StripPrefix=1 转发之前去掉1层路径,如:127.0.0.1:6001/mall-member/member/264260572479489,去掉第一层路径mall-member,就变成了127.0.0.1:6001/member/264260572479489,会被转发到mall-member服务。

4.4 Application配置@ComponentScan

mall-core服务config包里的WebMvcConfigurer配置类,和mall-gateway服务里排除的spring-webmvc有冲突,因此排除该目录下的配置类

@ComponentScan(
        value = "com.ac.*",
        excludeFilters = {@ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.ac.core.config.*")})
@SpringBootApplication
public class GatewayApplication {

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

4.5 swagger配置

4.5.1 配置类

package com.ac.gateway.config;

import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;

import java.util.ArrayList;
import java.util.List;

@Configuration
@Primary
@ConditionalOnProperty(name = "swagger.enable", havingValue = "true")
public class GateWaySwaggerConfig implements SwaggerResourcesProvider {
    public static final String API_URI = "/v2/api-docs";
    private final RouteLocator routeLocator;
    private final GatewayProperties gatewayProperties;

    public GateWaySwaggerConfig(RouteLocator routeLocator, GatewayProperties gatewayProperties) {
        this.routeLocator = routeLocator;
        this.gatewayProperties = gatewayProperties;
    }

    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> resources = new ArrayList<>();
        List<String> routes = new ArrayList<>();
        //取出gateway的route
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
        //结合配置的route-路径(Path),和route过滤,只获取有效的route节点
        gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
                .forEach(routeDefinition -> routeDefinition.getPredicates().stream()
                        .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                        .forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(),
                                predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
                                        .replace("/**", API_URI)))));

        return resources;
    }

    private SwaggerResource swaggerResource(String name, String location) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion("1.0");
        return swaggerResource;
    }
}

4.5.2 controller类

package com.ac.gateway.controller;

import com.ac.gateway.config.GateWaySwaggerConfig;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;

import javax.annotation.Resource;

/**
 * @author Alan Chen
 * @description 在浏览器中打开gateway的swagger地址时,会将请求自动打到下面API
 * http://127.0.0.1:6001/swagger-ui.html
 * @date 2023/02/22
 */
@ConditionalOnProperty(name = "swagger.enable", havingValue = "true")
@RestController
public class SwaggerController {

    @Resource
    private GateWaySwaggerConfig gateWaySwaggerConfig;

    @GetMapping("/swagger-resources/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
        return Mono.just(new ResponseEntity<>(SecurityConfigurationBuilder.builder().build(), HttpStatus.OK));
    }

    @GetMapping("/swagger-resources/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
        return Mono.just(new ResponseEntity<>(UiConfigurationBuilder.builder().build(), HttpStatus.OK));
    }

    @GetMapping("/swagger-resources")
    public Mono<ResponseEntity> swaggerResources() {
        return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
    }

    @GetMapping("/")
    public Mono<ResponseEntity> swaggerResourcesN() {
        return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
    }

    @GetMapping("/csrf")
    public Mono<ResponseEntity> swaggerResourcesCsrf() {
        return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
    }
}

在GateWaySwaggerConfig、SwaggerController类上都加上了@ConditionalOnProperty(name = "swagger.enable", havingValue = "true") 注解,该注解表示当swagger.enable配置值为true时,则将当前类初始化为bean。该开关用户关闭生产环境swagger,保证服务安全性。

下拉选择服务

4.6 @RestControllerAdvice拦截Controller返回统一格式数据

该配置类放在mall-core模块

package com.ac.core.config;

import com.ac.core.response.RepResult;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Alan Chen
 * @description Controller返回参数全局包装成ResponseResult对象
 * 使用是一般需要指定basePackages,@RestControllerAdvice(basePackages = {"com.netx.web.controller"})
 * 只拦截controller包下的类;否则swagger也会拦截影响swagger正常使用
 * @date 2023/04/15
 */
@EnableWebMvc
@Configuration
@RestControllerAdvice
public class GlobalReturnConfig implements ResponseBodyAdvice<Object>, WebMvcConfigurer {

    /**
     * 支持返回 text/plan 格式  字符串不会带双引号
     *
     * @return
     */
    public boolean supportTextPlan() {
        return false;
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        //排除swagger的请求 springfox.documentation.swagger2.web.Swagger2Controller
        if (methodParameter.getDeclaringClass().getName().contains("swagger")) {
            return false;
        }
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object returnObj, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

        // 返回值为void
        if (returnObj == null) {
            return RepResult.success();
        }

        //全局异常会拦截统一封装成ResponseResult对象,因此不需要再包装了
        if (returnObj instanceof RepResult) {
            return returnObj;
        }

        return RepResult.success(returnObj);

    }

    /**
     * 解决不能返回单个字符的问题
     *
     * @param converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

        if (supportTextPlan()) {
            converters.add(stringHttpMessageConverter());
        }

        //创建fastJson消息转换器
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();

        // 解决Content-Type cannot contain wildcard type '*'问题
        List<MediaType> supportedMediaTypes = new ArrayList<>();
        supportedMediaTypes.add(MediaType.APPLICATION_JSON);
        supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
        supportedMediaTypes.add(MediaType.APPLICATION_PDF);
        supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XML);
        supportedMediaTypes.add(MediaType.IMAGE_GIF);
        supportedMediaTypes.add(MediaType.IMAGE_JPEG);
        supportedMediaTypes.add(MediaType.IMAGE_PNG);
        supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
        supportedMediaTypes.add(MediaType.TEXT_HTML);
        supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
        supportedMediaTypes.add(MediaType.TEXT_PLAIN);
        supportedMediaTypes.add(MediaType.TEXT_XML);
        converter.setSupportedMediaTypes(supportedMediaTypes);

        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        // 字段为null时依然返回到前端,而不是省略该字段
        fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
        converter.setFastJsonConfig(fastJsonConfig);

        converters.add(converter);
    }

    @Bean
    public StringHttpMessageConverter stringHttpMessageConverter() {
        return new StringHttpMessageConverter();
    }
}

查询用户接口

虽然查询用户接口,返回的是一个用户对象,但返回到前端时,统一返回的是RepResult格式,将用户数据放在了data里。

统一返回RepResult格式

4.7 @ControllerAdvice拦截返回统一格式Exception

该配置放在mall-core里

package com.ac.core.exception;

import com.ac.core.i18n.I18nResource;
import com.ac.core.response.RepResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author Alan Chen
 * @description 全局异常处理
 * @date 2023/4/27
 */
@Slf4j
@ControllerAdvice
@Component
public class GlobalExceptionHandler {

    private I18nResource validationI18nSource;

    private I18nResource responseMessageI18nSource;

    /**
     * 是否开启Validator国际化功能
     * @return
     */
    protected boolean enableValidationI18n(){
        return false;
    }

    /**
     * 国际化文件地址
     * @return
     */
    protected String validationI18nSourcePath(){
        return "i18n/validation";
    }

    /**
     * 是否开启消息国际化
     * @return
     */
    protected boolean enableResponseMessageI18n(){
        return false;
    }

    /**
     * 消息国际化文件地址
     * @return
     */
    protected String responseMessageI18nSourcePath(){
        return "i18n/messages";
    }


    /**
     * 全局异常捕捉处理
     * @param ex
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public RepResult errorHandler(Exception ex) {
        ex.printStackTrace();
        log.error("Exception:"+ex.getMessage());
        return RepResult.fail(ex.getMessage());
    }

    /**
     * validator校验失败信息处理
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = BindException.class)
    public RepResult bindExceptionHandler(BindException exception) {
        exception.printStackTrace();
        return doValidationException(exception.getBindingResult());
    }

    /**
     * validator校验失败信息处理
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public RepResult validationHandler(MethodArgumentNotValidException exception) {
        exception.printStackTrace();
        log.error("MethodArgumentNotValidException:"+exception.getMessage());
        return doValidationException(exception.getBindingResult());
    }

    /**
     * 拦截捕捉业务异常 ServiceException.class
     * @param ex
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = ServerException.class)
    public RepResult commonExceptionHandler(ServerException ex) {

        ex.printStackTrace();
        log.error("ServiceException:"+ex.getMessage());

        if(enableResponseMessageI18n()){
            if(responseMessageI18nSource == null){
                responseMessageI18nSource = new I18nResource(responseMessageI18nSourcePath());
            }
            String messageKey = ex.getMessage();
            try{
                String message = responseMessageI18nSource.getValue(messageKey);
                String[] placeholder = ex.getPlaceholder();
                if(placeholder!=null && placeholder.length>0){
                   for(int i =0;i<placeholder.length;i++){
                       message = message.replace("#{"+(i+1)+"}",placeholder[i]);
                   }
                }
                return RepResult.info(message);
            }catch (Exception e){
                return RepResult.info(ex.getMessage());
            }
        }

        return RepResult.info(ex.getMessage());
    }


    private RepResult doValidationException(BindingResult bindingResult){
        StringBuffer stringBuffer = new StringBuffer();

        if(enableValidationI18n()){
            if(validationI18nSource == null){
                validationI18nSource = new I18nResource(validationI18nSourcePath());
            }

            for (FieldError error : bindingResult.getFieldErrors()) {
                String messageKey = error.getDefaultMessage();
                try{
                    String message = validationI18nSource.getValue(messageKey);
                    stringBuffer.append(message).append(";");
                }catch (Exception e){
                    stringBuffer.append(messageKey).append(";");
                }
            }
        }else{
            for (FieldError error : bindingResult.getFieldErrors()) {
                stringBuffer.append(error.getDefaultMessage()).append(";");
            }
        }

        log.error("BindException:"+stringBuffer.toString());
        return RepResult.info(stringBuffer.toString());
    }

}

统一异常格式

五、token鉴权测试

5.1 鉴权拦截成功

请求gateway访问mall-member服务接口,不携带token,请求被拦截

鉴权拦截成功

5.2 鉴权成功转发请求

请求gateway访问mall-member服务接口,携带合法token,请求被正确转发

鉴权成功转发请求

5.3 白名单

在bootstrap-dev.yml里配置了白名单:

#配置白名单路径
mall:
  security:
    ignore:
      urls:
        - "/**/member/list"
        - "/**/redis/**"

请求gateway访问mall-member服务白名单接口,不携带token,请求被正确转发

访问白名单接口

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

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

相关文章

2023年餐饮连锁行业研究报告

第一章 行业概况 餐饮连锁行业是全球经济的重要组成部分&#xff0c;它的主要运营模式是通过连锁经营形式提供食品和饮料服务。 连锁经营主要分为三种运营模式&#xff1a;直营连锁、特许经营和自由连锁经营。三种模式通过专业分工、集中管理实现规模效益。 直营连锁&#x…

华为又开始放大招了?CV新架构:VanillaNet: the Power of Minimalism in Deep Learning 论文阅读笔记

华为又开始放大招了&#xff1f;CV新架构&#xff1a;VanillaNet: the Power of Minimalism in Deep Learning 论文阅读笔记 一、Abstract二、引言三、单个 Vanilla 的神经结构四、训练 VanillaNet4.1 深度训练策略4.2 Series Informed Activation Function 五、实验5.1 消融实…

SSD202D-logo显示调试

参考网址:cBOOT LOGO以及显示参数使用参考 - SigmaStarDocsSigmaStar Developer Documentationhttp://wx.comake.online/doc/doc/TAKOYAKI_ZH/customer/development/software/BOOTLOGO.html1.mipii点亮过程中需要加点复位 2.编译生成新屏参 3.修改屏参选择名字 4.

Cortex-M3 的 双堆栈MSP和PSP

什么是栈&#xff1f; 在谈M3堆栈之前我们先回忆一下数据结构中的栈。栈是一种先进后出的数据结构(类似于枪支的弹夹&#xff0c;先放入的子弹最后打出&#xff0c;后放入的子弹先打出)。M3内核的堆栈也不例外&#xff0c;也是先进后出的。栈的作用&#xff1f; …

性能测试基础知识(一)性能测试的分类

性能测试的分类 一、什么是性能测试&#xff1f;二、性能测试的目的三、性能测试的分类1、基准测试2、并发测试3、负载测试4、压力测试5、其他测试 一、什么是性能测试&#xff1f; 性能测试是在一定的负载1条件下&#xff0c;系统的响应时间等特性是否满足特定的性能需求。需…

Appium 并发多进程基于 Pytest框架详解

目录 前言&#xff1a; 改造思路&#xff1a; 实现&#xff1a; 最后&#xff1a; 总结&#xff1a; 前言&#xff1a; 之前通过重写unittest的初始化方法加入设备参数进行并发&#xff0c;实现了基于unittest的appium多设备并发&#xff0c;但是考虑到unittest的框架实在…

幼儿园门禁如何应用人脸识别技术?3大优势你知道几个

随着社会的发展和科技的进步&#xff0c;人脸识别技术逐渐渗透到各个领域&#xff0c;为我们的生活带来了许多便利和安全。在幼儿园这个特殊的场所&#xff0c;保证幼儿的安全和管理是至关重要的。 通过人脸识别技术&#xff0c;幼儿园可以准确、快速地辨识幼儿、家长和教职工的…

yolov8 目标检测与跟踪

参考&#xff1a; 参考&#xff1a; https://github.com/ultralytics/ultralytics https://github.com/TommyZihao/Train_Custom_Dataset/blob/main/%E7%9B%AE%E6%A0%87%E8%BF%BD%E8%B8%AA/%E5%85%AC%E5%BC%80%E8%AF%BE/ https://www.rstk.cn/news/42041.html?actiononClick …

Docker部署gitlab-runner

gitlab-runner 1.部署 Linux使用二进制的方式Docker中使用容器的方式启动gitlab-runnerHelm包的方式安装gitlab-runner Docker中使用容器的方式启动gitlab-runner 1.安装gitlab runner docker run -d --name gitlab-runner --restart always \ -v /srv/gitlab-runner/conf…

基于U-Net网络实现图像分割

目录 1、作者介绍2、U-Net网络及数据集介绍2.1 U-Net网络2.2 数据集介绍2.2.1 VOC_2012数据集2.2.2 眼球毛细血管数据集2.2.3 医学图像数据集 3、U-Net实现图像分割3.1 U-Net实现图像分割实验&#xff08;简易版本&#xff09;3.1.1 环境配置3.1.2 数据集准备3.1.3 代码实现3.1…

《项目实战》使用JDBC手写分库

文章目录 1、概要2、整体架构流程3、技术名词解释4、技术细节4.1、指定分库规则4.2、安装Mysql数据库以及建库建表4.3、创建Java项目4.3.1、使用 Idea创建Maven项目4.3.1.1、修改pom.xml配置 4.3.2、编写分库/路由规则 DbRouter4.3.3、编写数据库交互工具 DaoUtil4.3.4、编写数…

MyBits的创建与使用

文章目录 前言MyBits的优点这里简单回忆下用JDBC的流程 MyBits的调用流程MyBits的配置传递参数之# 与 $ 的区别 当mysql与程序属性映射不一致时的解决方案 前言 上篇博客讲述了 Spring后端与前端进行交互的过程, 而这篇博客将讲述Spring与数据库的交互 , 众所周知 后端与数据库…

1.1数据结构绪论

一、数据结构 学习如何使用程序代码把现实世界的问题信息化 二、数据的基本概 1、数据&#xff1a;信息的载体&#xff0c;是描述客观世界属性的数、字符及被计算机程序识别和处理的集合。 早期计算机处理的数据——纯数值类型&#xff1b;现代计算机处理数据——非数据类型 …

融合创新:AI虚拟数字人与3D VR全景引领未来旅游潮流

导语&#xff1a; 随着科技不断发展&#xff0c;AI虚拟数字人和3D VR全景技术的融合正引领着创新的潮流。这种融合不仅仅是对传统导览的升级&#xff0c;更为各个领域带来了全新的创新应用。让我们一起探索AI虚拟数字人与3D VR全景融合的创新应用&#xff0c;看看它们如何在多…

快速解决Github无法访问的问题

Github访问慢&#xff0c;是困扰很多人的问题&#xff0c;今天就出一个解决方案&#xff0c;按照下面思路&#xff0c;可以实现快速访问Github&#xff0c;来查看我们需要的资源。 目录 一、获取DNS 二、修改hosts文件内容 2.1 修改hosts权限 2.2 修改hosts内容 三、轻…

以指标驱动,企业数智化迈向新阶段

近年来&#xff0c;我国数字经济蓬勃发展&#xff0c;数据成为推动经济社会发展的新要素。国家十四五规划指出&#xff0c;要激活数据要素潜能&#xff0c;加快建设数字经济&#xff0c;需要重点实施“上云用数赋智”行动&#xff0c;推动数据赋能全产业链协同转型。为进一步迈…

保姆级教你用Python制作超级玛丽游戏“爷青回~”(文末赠书)

名字&#xff1a;阿玥的小东东 学习&#xff1a;Python、C/C 主页链接&#xff1a;阿玥的小东东的博客_CSDN博客-python&&c高级知识,过年必备,C/C知识讲解领域博主 目录 贪吃蛇游戏 弹珠游戏 超级玛丽&#xff08;爷青回~&#xff09; 完整代码如下&#xff1a; 总…

SpringBoot 实现 PDF 添加水印

SpringBoot 实现 PDF 添加水印 使用场景方式一&#xff1a;使用 Apache PDFBox 库方式二&#xff1a;使用 iText 库方式三&#xff1a;Free Spire.PDF for JavaDemo 使用场景 PDF&#xff08;Portable Document Format&#xff0c;便携式文档格式&#xff09;是一种流行的文件…

LIME论文阅读笔记

这是暗图增强领域一篇经典的传统方法论文&#xff0c;发表在TIP这个顶刊 文章基于的是这样一个公式&#xff1a; L R ⋅ T LR\cdot T LR⋅T 其中&#xff0c; L L L是暗图&#xff0c; R R R是反射分量&#xff0c; T T T是illumination map&#xff0c;并且对于彩色图像来说…

OpenCV reshape函数

reshape函数 在opencv中&#xff0c;reshape函数比较有意思&#xff0c;它既可以改变矩阵的通道数&#xff0c;又可以对矩阵元素进行序列化&#xff0c;非常有用的一个函数。 函数原型&#xff1a; C: Mat Mat::reshape(int cn, int rows0) const参数比较少&#xff0c;但设…