从 Zuul 迁移到 Spring Cloud Gateway:一步步实现服务网关的升级

从 Zuul 迁移到 Spring Cloud Gateway:一步步实现服务网关的升级

    • 迁移前的准备工作
    • 迁移步骤详解
      • 第一步:查看源码
      • 第二步:启动类迁移
      • 第三步:引入 Gateway 依赖
      • 第四步 编写bootstrap.yaml
      • 第五步:替换路由配置
      • 第六步:迁移过滤器逻辑
      • 第七步:测试与调优
    • 迁移过程中常见问题及解决方案
    • 真实问题:
      • **注意事项:Nginx 转发配置的调整**
        • **问题背景**
        • **解决方法**
    • 总结

公司的项目之前使用的是Zuul,然后使用的是以前传下来的jar包,JDK1.8,spring1.*,都是比较老了,然后因为这些原因,要把Zuul替换成Gateway。
本文将详细介绍如何从 Zuul 迁移到 Gateway。

迁移前的准备工作

在开始迁移之前,需要做好以下准备:

  1. 确认现有的 Zuul 配置
    收集 Zuul 的路由配置、过滤器逻辑和插件依赖。

  2. 学习 Gateway 的基本概念
    熟悉 Gateway 的核心概念,例如:

    • Route(路由)
    • Predicate(断言)
    • Filter(过滤器)
  3. 确保系统支持响应式编程模型
    检查项目中的依赖库和代码是否与 Spring WebFlux 的非阻塞模型兼容。

  4. 升级到支持 Gateway 的 Spring Boot 版本
    确保 Spring Boot 版本 >= 2.1。


迁移步骤详解

第一步:查看源码

由于项目使用的是预先打包好的 Jar 文件,源码不可直接查看,因此需要通过反编译工具提取代码。我使用的是 jd-gui 工具,界面如图所示:

反编译工具界面

从反编译的结果可以看到,代码量相对简单,主要包含两个部分:启动类和核心过滤器。相对比较容易。

第二步:启动类迁移

原 Zuul 启动类:

@EnableZuulProxy
@SpringBootApplication
public class ZuulServerApplication {public static void main(String[] args) {(new SpringApplicationBuilder(ZuulServerApplication.class)).web(true).run(args);}@Beanpublic PathRewriteHeaderFilter customAddHeaderFilter(RouteLocator routeLocator) {return new PathRewriteHeaderFilter(routeLocator);}
}

迁移后的 Gateway 启动类:

@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.aspire.gateway.gatewayservice"})
public class GatewayServiceApplication {public static void main(String[] args) {SpringApplication.run(GatewayServiceApplication.class, args);}
}
  • Spring Boot 2.x 后,@EnableZuulProxy 不再需要,Gateway 默认支持路由功能。
  • 由于项目的特殊需求,需要添加 @ComponentScan 手动指定 Bean 扫描路径,确保组件能够被正确加载。
  • 因为spring2之后的版本不需要再显示指定Gateway了,其实理论上只需要一个SpringBootApplication就够了,其他其实都不用。但是我这里不知道为啥,扫描不到我的bean,所以我就写了扫描当前启动类。@ComponentScan(basePackages = {"com.aspire.gateway.gatewayservice"})这里你可以换成自己的扫描包路径。

第三步:引入 Gateway 依赖

pom.xml 中移除 Zuul 相关依赖,替换为 Gateway 依赖:

以下是我使用的版本控制,就是这些版本之间是兼容的,我使用的也是这些版本。

<properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-cloud.version>2024.0.0</spring-cloud.version> <!-- Spring Cloud 2024.x --><spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version> <!-- Spring Cloud Alibaba 对应版本 --><keycloak.version>22.0.4</keycloak.version>   <!-- 非必须,我的项目需要,你不用就删掉 --></properties>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

这一步主要就是导入你的依赖嘛。

第四步 编写bootstrap.yaml

这一块里面其实主要就是你的nacos的配置文件,反正我用的是nacos,因为Zuul是网关嘛,Gateway也是网关,然后你实际的服务和网关都是要在同一个服务发现下面的,我之前是eureka,现在是nacos,所以要在这里说明的。

spring:main:allow-circular-references: trueallow-bean-definition-overriding: trueapplication:name: rbac-gatewaycloud:nacos:username: ${ENV_CONFIG_USERNAME:nacos}password: ${ENV_CONFIG_PASSWORD:}server-addr: ${ENV_CONFIG_IP:10.*.*.*}:${ENV_CONFIG_PORT:*}# Nacos 服务发现配置discovery:enabled: true  # 启用服务发现service: ${spring.application.name}  # 使用应用名作为服务名server-addr: ${ENV_CONFIG_IP:*}:${ENV_CONFIG_PORT:*}namespace: ${NAMESPACE:*}#group: ${spring.cloud.nacos.discovery.group:*}group: *metadata:version: v1env: prod# Nacos 配置中心配置config:enabled: trueserver-addr: ${ENV_CONFIG_IP:*}:${ENV_CONFIG_PORT:*}
#        group: ${spring.cloud.nacos.discovery.group:*}group: *namespace: ${NAMESPACE:*}file-extension: ymlshared-configs:- data-id: ${CONFIG_DATA_ID:ms-gateway.yml}group: *refresh: truetimeout: 600000config-long-poll-timeout: 5000config-retry-time: 2000max-retry: 3refresh-enabled: true

第五步:替换路由配置

将 Zuul 的 application.yml 配置迁移为 Gateway 的路由配置。这一块实际上就比较复杂了,因为他们之间的切换还是很麻烦的,所以我这里是直接使用AI帮我替换的,你也可以这样。

反正差不多样子就是如下吧。直接让AI帮你替换,然后你看一眼就行了。我反正是这么搞的,然后也没啥问题。

Zuul 配置:

zuul:#semaphore:max-semaphores: 1000servlet-path: /host:connect-timeout-millis: 60000socket-timeout-millis: 60000#routes:smartdata-check:path: /smartCheckservice-id: rbacstrip-prefix: falsesmartdata-token-init:path: /v1/smartdata/tokenservice-id: rbacstrip-prefix: falsecomposite-roles:path: /v1/roles/**service-id: rbacstrip-prefix: false

Gateway 配置:

spring:cloud:gateway:routes:- id: ssouri: lb://rbacpredicates:- Path=/v1/alerts/sso/**- id: smartdata-checkuri: lb://rbacpredicates:- Path=/smartCheck- id: smartdata-token-inituri: lb://rbacpredicates:- Path=/v1/smartdata/token- id: composite-rolesuri: lb://rbacpredicates:- Path=/v1/roles/**

其实没有全局过滤器,已经可以用了,就是网关服务已经是可以用了。到这里其实就已经结束了。服务能用。不看后面也行,我为什么要替换呢,因为我想完美迁移。

第六步:迁移过滤器逻辑

Zuul 使用过滤器机制来处理请求,而 Gateway 则使用过滤器工厂。这一块就比较复杂了,也是我花的最多时间的一步了。

原本的Zuul 过滤器:


import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.web.util.UrlPathHelper;public class PathRewriteHeaderFilter extends ZuulFilter {private static final Logger log = LoggerFactory.getLogger(com.migu.tsg.microservice.zuul.PathRewriteHeaderFilter.class);private RouteLocator routeLocator;private final UrlPathHelper urlPathHelper = new UrlPathHelper();private static final String EMPLOYEE_TYPE = "employeeType";private static final String ORG_ACCOUNT = "head_orgAccount";private static final String IS_ADMIN = "head_isAdmin";private static final String IS_SUPERUSER = "head_isSuperUser";private static final String USER_NAME = "head_userName";private static final String FALSE = "false";private static final String TRUE = "true";private static final String ADMIN = "admin";private static final String ROOT = "root";private static final Integer SIX = Integer.valueOf(6);private static final String COLON = ":";public PathRewriteHeaderFilter() {}public PathRewriteHeaderFilter(RouteLocator routeLocator) {this.routeLocator = routeLocator;}public int filterOrder() {return SIX.intValue();}public String filterType() {return "pre";}public boolean shouldFilter() {return true;}public Object run() {RequestContext requestContext = RequestContext.getCurrentContext();String requestURI = this.urlPathHelper.getPathWithinApplication(requestContext.getRequest());Route route = this.routeLocator.getMatchingRoute(requestURI);try {if (route != null) {String location = route.getLocation();log.info("location: {}", location);if (location != null) {HttpServletRequest request = requestContext.getRequest();KeycloakSecurityContext securityContext = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());handleRewriteHeader(securityContext, requestContext);if (location.startsWith("http:") || location.startsWith("https:"))log.info("forward url is : " + location); } } } catch (Exception e) {requestContext.set("error.status_code", Integer.valueOf(500));requestContext.set("error.message", e.getCause());requestContext.set("error.exception", e);} return null;}private void handleRewriteHeader(KeycloakSecurityContext securityContext, RequestContext requestContext) {log.info("keycloak securityContext = {}", securityContext);if (securityContext == null)return; AccessToken token = securityContext.getToken();Map<String, Object> otherClaims = token.getOtherClaims();log.info("keycloak token = {}, otherClaims = {}", token, otherClaims);String employeeType = (String)otherClaims.get("employeeType");String userName = (String)otherClaims.get("userName");String orgAccount = "";String isSupperUser = "false";String isAdmin = "false";if (employeeType.equals("root")) {isSupperUser = "true";orgAccount = userName;} if (employeeType.equals("admin")) {isAdmin = "true";orgAccount = userName;} if (!employeeType.equals("root") && !employeeType.equals("admin")) {int index = employeeType.indexOf(".") + 1;orgAccount = employeeType.substring(index, employeeType.length());} log.info("employeeType: {}, head_userName: {}, head_isSuperUser: {}, head_orgAccount: {}, head_isAdmin: {}", new Object[] { employeeType, userName, isSupperUser, orgAccount, isAdmin });requestContext.addZuulRequestHeader("head_userName", userName);requestContext.addZuulRequestHeader("head_isSuperUser", isSupperUser);requestContext.addZuulRequestHeader("head_orgAccount", orgAccount);requestContext.addZuulRequestHeader("head_isAdmin", isAdmin);}
}

但是我是想完美的等量替换,所以这里就把原本的过滤器也给拿过来了。可以看到是少了一些东西了,因为原本的方法有很多东西是用不到的,我就把那些东西给删掉了。只保留了用到的东西

反正我测下来,是没啥问题,反正就是实现起来差别真的很大,首先是extends ZuulFilter不用了,改成了GlobalFilter ,然后里面的实现也从public Object run() 变成了public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) ,然后具体的实现逻辑也变了,这一块呢,就是自己琢磨着改吧。每一个过滤器都不一样,反正大体逻辑就是实现的方法不一样了,然后重写的方法不一样了。这两个是最主要的。

  1. 实现的接口不一样
  2. 重写的方法不一样

其实主要把握这两个就行,里面就是具体的代码逻辑了。

替换后的Gateway 全局过滤器:


@Component
@Order(6)  // 这里使用 @Order 注解来设置过滤器顺序
public class PathRewriteHeaderFilter implements GlobalFilter {private static final Logger log = LoggerFactory.getLogger(PathRewriteHeaderFilter.class);@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {try {String requestURI = exchange.getRequest().getURI().getPath();log.info("Request URI: {}", requestURI);// 在此处你可以获取并处理 Keycloak 的 securityContext 和 tokenKeycloakSecurityContext securityContext = exchange.getAttribute(KeycloakSecurityContext.class.getName());if (securityContext != null) {AccessToken token = securityContext.getToken();Map<String, Object> otherClaims = token.getOtherClaims();log.info("keycloak token = {}, otherClaims = {}", token, otherClaims);String employeeType = (String) otherClaims.get("employeeType");String userName = (String) otherClaims.get("userName");String orgAccount = "";String isSuperUser = "false";String isAdmin = "false";if ("root".equals(employeeType)) {isSuperUser = "true";orgAccount = userName;} else if ("admin".equals(employeeType)) {isAdmin = "true";orgAccount = userName;} else {int index = employeeType.indexOf(".") + 1;orgAccount = employeeType.substring(index);}log.info("employeeType: {}, head_userName: {}, head_isSuperUser: {}, head_orgAccount: {}, head_isAdmin: {}",employeeType, userName, isSuperUser, orgAccount, isAdmin);exchange.getRequest().mutate().header("head_userName", userName).header("head_isSuperUser", isSuperUser).header("head_orgAccount", orgAccount).header("head_isAdmin", isAdmin).build();}} catch (Exception e) {log.error("Error processing request", e);}return chain.filter(exchange);}
}

第七步:测试与调优

  1. 功能测试
    验证迁移后的路由和过滤器逻辑是否正常工作。
    在这里插入图片描述
    我这里是正常的,没有问题的。

  2. 性能测试
    测试 Gateway 的吞吐量和延迟,确保性能满足要求。

  3. 监控与日志
    配置 Gateway 的监控和日志,及时捕获异常和瓶颈。


迁移过程中常见问题及解决方案

  1. 问题:某些依赖库与 WebFlux 不兼容
    解决方案: 更新相关依赖或寻找替代方案,确保与 WebFlux 模型兼容。

  2. 问题:路由配置规则变更导致服务无法访问
    解决方案: 仔细对比 Zuul 和 Gateway 的配置方式,确保路径匹配规则正确。

  3. 问题:过滤器执行顺序混乱
    解决方案: 合理设置过滤器的 Order 值,并明确其执行逻辑。

真实问题:

注意事项:Nginx 转发配置的调整

在迁移过程中,有一个细节需要特别注意,那就是 Nginx 的转发规则。以下是我遇到的问题和解决方法,希望能对你有所帮助。

问题背景

在原有的 Zuul 部署环境中,我使用的是 IP+端口 的形式进行服务转发。由于迁移初期 IP 和端口并未发生改变,所以 Nginx 的配置无需修改,服务能够正常使用。然而,当将 Gateway 部署到云原生环境(如 Kubernetes)后,问题随之出现。

云原生环境中,服务之间的通信通常使用 服务名:端口 的形式,而不是 IP 地址。因此,原本在 Nginx 中配置的 qams-zuul-server 服务名需要进行修改,否则转发规则无法正确匹配,导致请求失败。


解决方法
  1. 检查原有的 Nginx 配置
    原有配置通常类似以下形式:
location ^~/v1/ {proxy_pass   http://zuulServer;}location ^~/v2/ {#proxy_pass   http://10.24.88.160:5566;proxy_pass   http://10.12.7.115:5566;}location ^~/zuul/ {#proxy_pass   http://10.24.88.160:5566;proxy_pass   http://10.12.7.115:5566;}location ^~/download/ {proxy_pass   http://10.24.88.160:2222;}

这种配置基于固定的 IP 和端口,在云原生环境下无法适用。

  1. 修改为基于服务名的配置
    在云原生环境中,需要将 10.12.7.115 的地址替换为服务名,示例如下:
  location ^~/v1/ {                                                                                         proxy_pass   http://qams-gateway-server:5566;}location ^~/v2/ {                                                                                       proxy_pass   http://qams-gateway-server:5566;                                                                 } location ^~/gateway/ {                                                                                       proxy_pass   http://qams-gateway-server:5566;                                                                 }location ^~/download/ {                                                                                       proxy_pass   http://qams-gateway-server:2222;                                                                 }

注意:

  • 服务名 qams-gateway-service 必须与云原生环境中定义的服务名称一致。
  • 确保 Nginx 能够解析服务名。通常情况下,Nginx 部署在同一 Kubernetes 集群内,DNS 解析应当是自动支持的。
  1. 重启 Nginx 并测试
    完成修改后,重启 Nginx 并通过实际访问测试转发是否正常。

迁移的时候注意nginx的转发,我之前呢,是因为我使用的是IP+端口的形式,然后我的IP和端口实际上并没有发生改变,所以我的Nginx没改然后服务依旧能正常使用。
但是当我把Gateway迁移到云原生环境下的时候,就不太行了,因为云原生环境使用的是服务名:端口的格式,所以他原本的服务名称为:qams-zuul-server,要换成下面的格式,就是nginx也要需要,这个不要忘记了。
在这里插入图片描述


总结

从 Zuul 迁移到 Spring Cloud Gateway 是一次提升系统性能和功能的好机会。通过合理规划和逐步迁移,可以平稳完成网关的升级,并充分利用 Gateway 的新特性来优化系统架构。

希望这篇文章能为你的迁移过程提供有价值的参考!如果你在迁移过程中遇到问题,欢迎留言讨论。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【金融贷后】贷后运营精细化管理

文章目录 一、贷后专业术语讲解① 什么是贷后&#xff0c;贷后部是干什么的&#xff1f;② 贷后部门常见组织架构&#xff1f;③ 贷后专业术语有哪些&#xff1f; 二、贷后常用作业手段介绍① 贷后产品形态介绍&#xff1f;② 催收常用的方法&#xff1f; 三、贷后策略岗位介绍…

利用cnocr库完成中文扫描pdf文件的文字识别

很多pdf文件文字识别软件都会收费&#xff0c;免费的网页版可能会带来信息泄露&#xff0c;还有一些类似于腾讯AI和百度AI的接口都有调用次数限制&#xff0c;因此&#xff0c;利用识别正确率极高且免费的cnocr库来自己动手做个pdf文件文字识别程序就是一个很不错的选择。以下程…

论文阅读 -- IDENTIFYING THE RISKS OF LM AGENTS WITHAN LM-EMULATED SANDBOX, ICLR2024

论文链接&#xff1a;https://arxiv.org/pdf/2309.15817 目录 ABSTRACT 1 INTRODUCTION 2 BACKGROUND & PROBLEM STATEMENT 3 CONSTRUCTING TOOLEMU 3.1 EMULATING TOOL EXECUTIONS WITH LANGUAGE MODELS 3.2 DESIGNING AUTOMATIC EVALUATIONS WITH LANGUAGE MODEL…

期末复习-计算机网络篇SCAU

第一章&#xff1a;概述 1.计算机网络的特点&#xff0c;互联网发展的三个阶段 特点&#xff1a;连通性、资源共享 三个阶段&#xff1a; 1969-1990&#xff1a;从单个网络ARPANET向互联网发展 1985-1993&#xff1a;建成了三级结构的互联网 1993-现在&#xff1a;全球范…

TesseractOCR-GUI:基于WPF/C#构建TesseractOCR简单易用的用户界面

前言 前篇文章使用Tesseract进行图片文字识别介绍了如何安装TesseractOCR与TesseractOCR的命令行使用。但在日常使用过程中&#xff0c;命令行使用还是不太方便的&#xff0c;因此今天介绍一下如何使用WPF/C#构建TesseractOCR简单易用的用户界面。 普通用户使用 参照上一篇教…

openGauss开源数据库实战二十一

文章目录 任务二十一 使用JDBC访问openGauss数据库任务目标实施步骤一、准备工作 二、下载并安装JavaSE81 下载JavaSE8安装Java8SE并配置环境变量 三、下载并安装eclipse四、下载并安装openGauss的JDBC驱动包五、使用IDEA编写JDBC测试程序1 使用IDEA的SSH连接虚拟机2 创建项目并…

算法——前缀和

如果我们想要得到数组中一段区间的和最朴素的想法肯定是我们从区间的开始下标遍历到结束下标并累加&#xff0c;但是这显然存在一个问题&#xff0c;时间开销是O&#xff08;n&#xff09;的级别&#xff0c;并且有着大量的重复计算&#xff0c;求[n, m]的和后继续求[n…m…p]区…

可视化建模以及UML期末复习篇----UML图

这是一篇相对较长的文章&#xff0c;如你们所见&#xff0c;比较详细&#xff0c;全长两万字。我不建议你们一次性看完&#xff0c;直接跳目录找你需要的知识点即可。 --------欢迎各位来到我UML国&#xff01; 一、UML图 总共有如下几种&#xff1a; 用例图&#xff08;Use Ca…

Tableau数据可视化与仪表盘搭建

1.Tableau介绍 可视化功能 数据赋能 数据赋能就是将我们的数据看板发布到我们的线上去 这里的IP地址是业务部门可以通过账号密码登入的 我们也可以根据需要下载&#xff0c;选中并点击下载即可 下载下来之后&#xff0c;自己就能根据数据进行自定义的分析 也可以下载图片 还有…

NanoLog起步笔记-7-log解压过程初探

nonolog起步笔记-6-log解压过程初探 再看解压过程建立调试工程修改makefile添加新的launch项 注&#xff1a;重新学习nanolog的README.mdPost-Execution Log Decompressor 下面我们尝试了解&#xff0c;解压的过程&#xff0c;是如何得到文件头部的meta信息的。 再看解压过程 …

设计模式-装饰器模式(结构型)与责任链模式(行为型)对比,以及链式设计

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1.装饰器模式1.1概念1.2作用1.3应用场景1.4特点1.5类与对象关系1.6实现 2责任链模式2.1概念2.2作用2.3应用场景2.4特点2.5类与对象关系2.6实现 3.对比总结 前言…

llama-factory实战: 基于qwen2.5-7b 手把手实战 自定义数据集清洗 微调

基于qwen2.5 手把手实战 自定义数据集 微调&#xff08;llama-factory&#xff09; 准备工作1.数据集准备&#xff08;例:民法典.txt&#xff09;2.服务器准备&#xff08;阿里云 DSW 白嫖&#xff09;3.环境配置pip 升级模型下载微调助手 4.数据集处理脚本文件4.1文本分割(ber…

day08 接口测试(4)知识点完结!!

【没有所谓的运气&#x1f36c;&#xff0c;只有绝对的努力✊】 目录 1、postman读取外部数据文件&#xff08;参数化&#xff09; 1.1 数据文件简介 1.2 导入外部数据文件 1.2.1 csv文件 1.2.2 导入 json文件 1.3 读取数据文件数据 1.4 案例 1.5 生成测试报告 2、小…

基于Springboot的实验室管理系统【附源码】

基于Springboot的实验室管理系统 效果如下&#xff1a; 系统登录页面 实验室信息页面 维修记录页面 轮播图管理页面 公告信息管理页面 知识库页面 实验课程页面 实验室预约页面 研究背景 在科研、教育等领域&#xff0c;实验室是进行实验教学和科学研究的重要场所。随着实验…

selenium学习:等待方式

隐式等待 1.针对查找元素设置最大的超时时间 2.可以全局性的设置 3.不满足时&#xff0c;提示no such element driver.implicitly_wait(5) #对查找元素最大的超时时间&#xff0c;如果超过最大等待时间后&#xff0c;没有找到元素&#xff0c;则会报错&#xff1a;no such #e…

计算生成报价单小程序系统开发方案

计算生成报价单小程序报价系统&#xff0c;是根据商品品牌、类型、型号、规格、芯数、特性、颜色、分类进行选择不同的参数进行生成报价单&#xff0c;要求报价单支持生成图片、pdf、excel表格。 计算生成报价单小程序系统的主要功能模块有&#xff1a; 1、在线生成报价单&…

constexpr、const和 #define 的比较

constexpr、const 和 #define 的比较 一、定义常量 constexpr 定义&#xff1a;constexpr用于定义在编译期可求值的常量表达式。示例&#xff1a;constexpr int x 5;这里&#xff0c;x的值在编译期就确定为5。 const 定义&#xff1a;const表示变量在运行期间不能被修改&…

Spring Boot 整合 Druid 并开启监控

文章目录 1. 引言2. 添加依赖3. 配置数据源4. 开启监控功能5. 自定义 Druid 配置&#xff08;可选&#xff09;6. 访问监控页面7. 注意事项8. 总结 Druid 是一个由阿里巴巴开源的高性能数据库连接池&#xff0c;它不仅提供了高效的连接管理功能&#xff0c;还自带了强大的监控和…

Abaqus断层扫描三维重建插件CT2Model 3D V1.1版本更新

更新说明 Abaqus AbyssFish CT2Model3D V1.1版本更新新增对TIF、TIFF图像文件格式的支持。本插件用户可免费获取升级服务。 插件介绍 插件说明&#xff1a; Abaqus基于CT断层扫描的三维重建插件CT2Model 3D 应用案例&#xff1a; ABAQUS基于CT断层扫描的细观混凝土三维重建…

word poi-tl 表格功能增强,实现表格功能垂直合并

目录 问题解决问题poi-tl介绍 功能实现引入依赖模版代码效果图 附加&#xff08;插件实现&#xff09;MergeColumnData 对象MergeGroupData 类ServerMergeTableData 数据信息ServerMergeTablePolicy 合并插件 问题 由于在开发功能需求中&#xff0c;word文档需要垂直合并表格&…