-
分布式全局唯一ID简介分布式全局唯一ID(Distributed Globally Unique Identifier, DGUID)是在分布式系统中用于唯一标识数据、消息、HTTP请求等的标识符。由于分布式系统可能涉及多个节点、多个服务、甚至跨地域的部署,传统的数据库自增主键或单机系统的唯一ID生成方式已无法满足需求。因此,需要一种能够在全局范围内保证唯一性的ID生成机制。分布式全局唯一ID的特点全局唯一性:生成的ID在整个分布式系统中必须是唯一的,不能出现重复。高可用性:ID生成系统作为基础系统,必须具有高可用性,避免因单点故障导致整个系统不可用。趋势递增:在某些场景下,需要ID具有一定的趋势递增性,以便于数据库索引和排序。单调递增:在某些特殊需求下,需要保证生成的ID是单调递增的。信息安全:ID的生成应避免暴露系统内部信息,如MAC地址等,以防止信息泄露。Spring Boot中的解决方案在Spring Boot中,实现分布式全局唯一ID的生成有多种方案,以下是一些常见的解决方案:UUID简介:UUID(Universally Unique Identifier)是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符。UUID是16字节128位长的数字,通常以36字节的字符串表示。优点:性能好,效率高,无需网络请求,直接本地生成。缺点:无序,无法保证趋势递增;字符串存储,查询效率慢,存储空间大;信息不安全,基于MAC地址生成UUID的算法可能会造成MAC地址泄露。数据库自增主键+分布式策略简介:利用数据库的自增主键特性,结合分布式策略(如设置不同的起始值和步长)来生成全局唯一的ID。优点:实现简单,数字ID排序、搜索、分页等操作有利。缺点:单点故障风险,高并发时性能瓶颈;分库分表时自增ID会重复,需要复杂的策略来保证唯一性。Redis生成ID简介:利用Redis的原子操作(如INCR和INCRBY)来实现全局唯一ID的生成。Redis是单线程的,因此其操作本身是线程安全的。优点:性能高,不依赖于数据库;灵活方便,可以根据需要自定义ID的生成规则。缺点:占用带宽,每次生成ID都需要向Redis发送请求;Redis重启时数据可能会丢失(虽然可以通过其他机制来保证数据的持久性)。雪花算法(Snowflake)简介:雪花算法是Twitter开源的一种生成唯一ID的算法。它通过将64位的长整型数字分为多个部分,分别表示时间戳、数据中心ID、机器ID和序列号等,以此来保证ID的唯一性和趋势递增性。优点:生成的ID全局唯一,趋势递增;性能高,适合分布式系统。缺点:实现相对复杂;需要合理规划数据中心ID和机器ID的分配。自定义算法根据具体业务需求和系统架构,可以自定义全局唯一ID的生成算法。例如,结合时间戳、随机数、业务标识等元素来生成ID。优点:灵活性高,可以根据具体需求进行优化。缺点:需要投入更多的开发成本和维护成本。在Spring Boot项目中,可以根据项目的具体需求和环境选择合适的方案来实现分布式全局唯一ID的生成。例如,如果系统对ID的唯一性和性能要求较高,可以考虑使用雪花算法或Redis生成ID的方案;如果系统规模较小且对ID的唯一性要求不是特别严格,可以使用UUID或数据库自增主键+分布式策略的方案。
-
在Spring Boot中,如果某个业务执行得很慢,可以通过一系列步骤来逐步分析问题所在。以下是一个详细的分析流程,包括可能的原因和对应的排查方法:1. 初步观察和日志分析步骤:查看日志:首先检查应用程序的日志文件,特别是与业务执行相关的部分。注意任何异常、错误或警告信息,这些信息可能是导致性能问题的直接原因。观察响应时间:通过日志或监控工具观察请求的响应时间,确定是哪个环节或组件导致了延迟。例子:日志中可能显示数据库查询耗时过长,或者某个特定的服务调用响应慢。2. 监控和性能指标步骤:使用Spring Boot Actuator:如果项目中已经集成了Spring Boot Actuator,可以利用其提供的端点(如/metrics)来查看系统的性能指标,如CPU使用率、内存占用、HTTP请求响应时间等。集成外部监控工具:如Prometheus、Grafana等,这些工具可以提供更详细和可视化的监控数据。例子:通过Actuator的/metrics端点,发现数据库查询的响应时间普遍较高。3. 深入代码分析步骤:审查代码:对执行慢的业务逻辑进行代码审查,查看是否有不必要的复杂操作、循环嵌套、大量计算或IO操作等。性能剖析:使用性能剖析工具(如JProfiler、YourKit Java Profiler等)对应用程序进行剖析,以获取更详细的性能数据,如方法执行时间、对象创建情况等。例子:通过性能剖析,发现某个服务方法内部有多个复杂的数据库查询,且这些查询之间存在不必要的重复计算。4. 数据库性能调优步骤:优化SQL查询:对性能瓶颈的SQL查询进行优化,如使用索引、优化查询逻辑、减少返回的数据量等。检查数据库连接:确保数据库连接池配置合理,没有过多的连接等待或超时。监控数据库性能:使用数据库监控工具(如MySQL的Performance Schema)来监控查询性能,查找慢查询并进行优化。例子:通过分析数据库查询日志,发现某个查询因为没有使用索引而导致了全表扫描,通过添加合适的索引后性能显著提升。5. 外部服务依赖步骤:检查外部服务响应:如果业务逻辑依赖于外部服务(如REST API、消息队列等),检查这些服务的响应时间和稳定性。优化服务调用:考虑使用异步调用、缓存结果或优化服务端的性能。例子:发现业务逻辑中频繁调用一个外部API,但该API的响应时间较长。通过增加本地缓存或改用更快速的替代服务,减少了等待时间。6. 并发和资源限制步骤:检查线程池和并发设置:确保Spring Boot的线程池和数据库连接池配置能够满足当前的业务需求。优化JVM参数:根据应用程序的内存和CPU使用情况,调整JVM的启动参数,如堆大小、垃圾回收策略等。例子:通过调整Spring Boot的线程池大小,增加了同时处理的请求数,从而减少了请求的平均响应时间。7. 总结和优化步骤:总结问题:根据以上步骤的分析结果,总结导致业务执行慢的根本原因。实施优化:根据问题原因制定相应的优化方案,并在测试环境中验证其效果。持续监控:在优化后持续监控系统的性能指标,确保问题得到根本解决,并准备应对可能出现的新问题。通过以上步骤,可以系统地分析和解决Spring Boot中业务执行慢的问题。
-
在Spring Boot中,常用的性能监视工具多种多样,这些工具可以帮助开发者监控应用程序的性能指标,如请求速度、响应时间、错误率等,从而及时发现并解决性能问题。以下是一些常用的性能监视工具:1. Spring Boot Actuator描述:Spring Boot Actuator是Spring Boot的一个模块,提供了一组用于监控和管理Spring Boot应用程序的端点。这些端点可以用于查看应用程序的性能指标、日志、配置等信息。优势:官方支持,与Spring Boot集成紧密。提供丰富的端点,如健康检查、度量信息、环境属性等。可与其他监控工具(如Prometheus、Grafana)集成。2. Micrometer描述:Micrometer是一个用于构建和监控微服务应用程序的度量标准库。它提供了一组用于收集和报告应用程序性能指标的工具,如计数器、桶计数器、计时器、分布器等。优势:支持多种度量标准,满足不同的监控需求。可与Spring Boot Actuator集成,方便在Spring Boot应用程序中使用。支持多种监控后端,如Prometheus、InfluxDB等。3. Prometheus描述:Prometheus是一个开源的监控系统,专注于收集和查询时间序列数据。它支持多种数据源,如JMX、HTTP、gRPC等,并提供了强大的查询语言和丰富的可视化工具(如Grafana)。优势:强大的数据收集和查询能力。可与Spring Boot Actuator集成,实现Spring Boot应用程序的监控。提供丰富的可视化工具,如Grafana,便于数据的展示和分析。4. Grafana描述:Grafana是一个开源的度量分析和可视化套件。它允许你查询、可视化、警报和了解你的指标,无论它们存储在哪里。Grafana支持多种数据源,如Prometheus、InfluxDB、Elasticsearch等。优势:强大的数据可视化能力,支持多种图表类型。可与Prometheus等监控工具集成,实现数据的可视化展示。提供丰富的插件和社区支持,满足不同场景下的需求。5. SkyWalking描述:SkyWalking是一个现代化的应用程序性能监控系统,专为云原生和基于容器的分布式系统设计。它提供了对微服务、云原生应用的全方面监控和分析功能,包括链路追踪、性能指标监控、服务网格拓扑等。优势:无侵入集成,可以在不修改源代码的情况下集成到应用程序中。丰富的功能,满足不同场景下的监控需求。强大的扩展性,支持多种插件和中间件集成。6. 其他工具除了上述工具外,还有一些其他的性能监视工具也常用于Spring Boot应用程序中,如:JVisualVM:JDK自带的图形化监控和性能分析工具,可以查看线程、堆内存、类加载、垃圾回收等信息。YourKit Java Profiler:一款强大的Java性能分析工具,提供实时的内存和CPU使用情况、堆转储分析等功能。JProfiler:一款全面的Java性能分析工具,提供实时的性能数据、内存分析、线程分析等功能。总结在选择性能监视工具时,应根据项目的实际需求和团队的技术栈来选择合适的工具。通常,结合使用多个工具可以提供更全面的性能分析。同时,还需要注意在生产环境之前测试和验证性能监控工具的集成,以确保其稳定性和可靠性。
-
网上有很多关于Spring Cloud写得不好的说法,这些观点可能源于多个方面。以下是一些可能的原因:1. 学习曲线陡峭复杂性高:Spring Cloud包含了众多的组件和功能,如服务发现、配置管理、消息总线、负载均衡等,这使得初学者需要投入较多的时间和精力来学习和理解其各个组件的原理和用法。文档和资料不足:尽管Spring Cloud的官方文档相对完善,但对于某些高级特性和复杂问题的解决方案,可能仍然缺乏足够的文档和示例,导致开发者在遇到问题时难以找到有效的解决途径。2. 维护成本高分布式系统复杂性:Spring Cloud主要用于构建分布式系统,而分布式系统本身就具有复杂性,如服务间的依赖关系、网络通信问题、数据一致性问题等。这些都需要开发者具备较高的技术水平和经验来应对。多服务管理:在微服务架构下,每个服务都是独立的模块,因此需要针对每个服务进行部署、监控和维护。这增加了整体系统的复杂性和维护成本。3. 版本兼容性问题组件更新快:随着Spring Cloud的不断发展,各个组件的版本可能会频繁更新,导致版本兼容性问题。在升级Spring Cloud的组件时,开发者需要仔细评估新版本与旧版本的兼容性,以避免出现不必要的问题。依赖冲突:由于Spring Cloud集成了多个第三方库和框架,不同版本的组件之间可能会存在依赖冲突,这增加了系统的复杂性和维护难度。4. 社区支持问题开源社区成熟度:尽管Spring Cloud的社区相对活跃,但相对于其他成熟的开源项目来说,其社区可能还不够成熟。这意味着在遇到问题时,可能找不到足够的解决方案或技术支持。问题定位困难:在分布式系统中,问题可能跨越多个服务和组件,这使得问题定位变得困难。此外,由于Spring Cloud的组件和功能众多,也增加了问题定位的难度。5. 适用场景限制不适合所有项目:Spring Cloud适用于需要构建复杂分布式系统的项目,但对于一些规模较小、需求简单的项目来说,使用Spring Cloud可能会增加不必要的复杂性和成本。学习成本与投资回报:对于一些公司来说,投入大量资源来学习和使用Spring Cloud可能并不划算,特别是当这些资源可以用于其他更直接、更高效的解决方案时。综上所述,网上关于Spring Cloud写得不好的说法可能源于其学习曲线陡峭、维护成本高、版本兼容性问题、社区支持问题以及适用场景限制等多个方面。然而,这并不意味着Spring Cloud不是一个优秀的框架。对于需要构建复杂分布式系统的项目来说,Spring Cloud仍然是一个非常有价值的选择。开发者在使用Spring Cloud时,应根据项目的实际需求和自身的技术水平来做出合理的选择。
-
Spring Boot作为一个流行的Java框架,其知识点涵盖了多个方面,以下是对Spring Boot主要知识点的归纳:1. Spring Boot基础核心注解:@SpringBootApplication是Spring Boot的核心注解,它包含了@Configuration、@EnableAutoConfiguration、@ComponentScan三个注解的功能,用于开启自动配置、组件扫描等。启动流程:Spring Boot项目启动时,会寻找带有@SpringBootApplication注解的类作为应用程序的入口点,并通过SpringApplication.run(Class<?> primarySource, String... args)方法启动应用。这个方法会完成一系列初始化工作,包括加载配置文件、创建ApplicationContext实例、注册并实例化Bean等。2. 自动配置自动配置原理:Spring Boot的自动配置特性利用Spring的条件化配置支持,通过@Conditional注解的派生注解(如@ConditionalOnClass、@ConditionalOnMissingBean等)来合理推测应用所需的Bean并自动配置它们。自定义自动配置:开发者可以通过编写自己的自动配置类,并使用@Configuration、@Conditional等注解来定义自己的自动配置逻辑。3. 配置文件全局配置文件:Spring Boot支持多种格式的全局配置文件,包括application.properties和application.yml(或application.yaml)。这些文件用于定义应用的配置信息,如数据库连接信息、服务器端口号等。配置文件优先级:Spring Boot会根据不同的位置加载配置文件,并按照一定的优先级进行覆盖。通常,application.yml的优先级高于application.properties(在Spring Boot 2.4.0及以上版本中)。4. 外部配置与属性注入外部配置:Spring Boot支持从多种外部源读取配置信息,包括命令行参数、环境变量、JNDI属性等。属性注入:开发者可以通过@Value注解或@ConfigurationProperties注解将配置文件中的属性注入到Bean中。5. 日志管理日志系统:Spring Boot默认使用SLF4J作为日志门面,并提供了对Logback、Log4j2等日志框架的支持。配置日志级别:开发者可以在配置文件中设置不同日志级别的输出,以便更好地控制日志信息。自定义日志配置:开发者还可以通过编写自定义的日志配置文件来覆盖默认的日志配置。6. 监控与调试Spring Boot Actuator:提供了一套生产环境监控和管理功能,通过一组端点暴露应用的运行状态、健康状况和各种指标。自定义健康检查:开发者可以通过实现HealthIndicator接口来自定义健康检查逻辑。7. 安全管理Spring Security集成:Spring Boot与Spring Security无缝集成,提供了强大的认证和授权功能。自定义用户认证:开发者可以通过实现UserDetailsService接口来自定义用户认证逻辑。8. 高级特性异步处理:通过@EnableAsync注解和@Async注解实现异步方法的调用,提高系统的响应速度和吞吐量。缓存:Spring Boot支持多种缓存解决方案,如Redis、EhCache等,通过@Cacheable、@CachePut、@CacheEvict等注解实现缓存的自动管理。多环境部署:Spring Boot支持多种环境(如开发、测试、生产)的部署,通过配置文件和profiles机制实现不同环境下的配置隔离。9. 部署与运行内嵌服务器:Spring Boot内置了Tomcat、Jetty、Undertow等Servlet容器,使得应用可以打包成jar包或war包直接运行,无需部署到外部服务器。热部署:通过引入spring-boot-devtools插件,可以实现不重启服务器情况下的即时编译和热部署。综上所述,Spring Boot的知识点涵盖了基础、自动配置、配置文件、外部配置与属性注入、日志管理、监控与调试、安全管理、高级特性以及部署与运行等多个方面。掌握这些知识点将有助于开发者更好地使用Spring Boot进行项目开发。
-
引言在现代的互联网应用中,数据安全和隐私保护变得越来越重要。尤其是在接口返回数据时,如何有效地对敏感数据进行脱敏处理,是每个开发者都需要关注的问题。本文将通过一个简单的Spring Boot项目,介绍如何实现接口数据脱敏。一、接口数据脱敏概述1.1 接口数据脱敏的定义接口数据脱敏是指在接口返回数据时,对其中的敏感信息进行处理,使其无法直接被识别和利用。例如,将用户的身份证号、手机号等信息进行部分隐藏。1.2 接口数据脱敏的重要性保护用户隐私:防止用户的敏感信息被泄露。合规要求:满足相关法律法规对数据保护的要求。减少风险:降低数据被恶意利用的风险。1.3 接口数据脱敏的实现方式常见的脱敏方式包括:字符替换:用特定字符替换敏感信息的一部分。字符隐藏:隐藏敏感信息的一部分字符。自定义规则:根据业务需求自定义脱敏规则。二、开发环境IDE:IntelliJ IDEAJDK:1.8+Spring Boot:2.5.4三、实现接口返回数据脱敏3.1 添加依赖首先,在pom.xml中添加必要的依赖:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>3.2 创建自定义注解创建一个自定义注解@Sensitive,用于标识需要脱敏的字段:import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Sensitive { SensitiveType type(); }3.3 定义脱敏枚举类定义一个枚举类SensitiveType,用于指定脱敏的类型:public enum SensitiveType { MOBILE, ID_CARD, EMAIL }3.4 创建自定义序列化类创建一个自定义序列化类SensitiveSerializer,实现脱敏逻辑:import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; public class SensitiveSerializer extends JsonSerializer<String> { private final SensitiveType type; public SensitiveSerializer(SensitiveType type) { this.type = type; } @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { switch (type) { case MOBILE: gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")); break; case ID_CARD: gen.writeString(value.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1******$2")); break; case EMAIL: gen.writeString(value.replaceAll("(^[^@]{3})[^@]*(@.*$)", "$1****$2")); break; default: gen.writeString(value); } } }四、测试4.1 编写测试代码创建一个简单的用户类,并在字段上使用@Sensitive注解:import com.fasterxml.jackson.databind.annotation.JsonSerialize; public class User { private String name; @Sensitive(type = SensitiveType.MOBILE) @JsonSerialize(using = SensitiveSerializer.class) private String mobile; @Sensitive(type = SensitiveType.ID_CARD) @JsonSerialize(using = SensitiveSerializer.class) private String idCard; @Sensitive(type = SensitiveType.EMAIL) @JsonSerialize(using = SensitiveSerializer.class) private String email; // getters and setters }创建一个控制器,返回用户信息:import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @GetMapping("/user") public User getUser() { User user = new User(); user.setName("张三"); user.setMobile("13812345678"); user.setIdCard("123456789012345678"); user.setEmail("example@example.com"); return user; } }4.2 测试启动Spring Boot应用,访问/user接口,查看返回结果:{ "name": "张三", "mobile": "138****5678", "idCard": "1234******5678", "email": "exa****@example.com" }五、总结通过本文的示例,我们了解了如何在Spring Boot中实现接口数据脱敏。通过自定义注解和序列化类,可以灵活地对不同类型的敏感数据进行处理,保护用户的隐私信息。转载自https://www.cnblogs.com/zhizu/p/18303081
-
原理PageHelper是一个用于MyBatis的分页插件,pagehelper-spring-boot-starter是其在Spring Boot中的集成组件。下面简要介绍PageHelper的分页原理:PageHelper的分页原理拦截器机制:PageHelper通过MyBatis的拦截器机制实现分页功能。它会在SQL执行前拦截并修改SQL语句,添加分页相关的信息。ThreadLocal存储分页参数:在调用分页查询之前,会将分页参数(如页码、每页数量)存储在当前线程的ThreadLocal中,确保每次查询都能获取到正确的分页信息。自动构建分页SQL:根据存储在ThreadLocal中的分页参数,在拦截器中自动构建带有分页逻辑的SQL语句,例如使用LIMIT和OFFSET来限制返回结果集。执行分页查询:当执行带有分页参数的查询时,PageHelper会拦截该查询并根据分页参数重新构建SQL语句,然后执行查询操作。封装分页结果:在查询完成后,PageHelper会将查询结果封装成包含分页信息的对象,方便在业务逻辑中使用。PageHelper-Spring-Boot-Starter的集成原理自动配置:pagehelper-spring-boot-starter提供了自动配置类,可以根据配置文件中的属性自动配置PageHelper的相关参数,简化了在Spring Boot项目中集成PageHelper的步骤。注入拦截器:在自动配置过程中,会向MyBatis的SqlSessionFactory中注入PageInterceptor,这个拦截器负责拦截SQL并处理分页逻辑。配置参数:通过在application.properties或application.yml中配置pagehelper相关属性,可以定制化地设置分页参数,如页码参数名、每页数量等。总的来说,PageHelper通过拦截器机制、ThreadLocal存储分页参数以及自动构建分页SQL来实现对MyBatis的分页支持,而pagehelper-spring-boot-starter则在Spring Boot中简化了PageHelper的集成和配置过程。实战下面是一个简单的示例,演示如何在Spring Boot项目中使用pagehelper-spring-boot-starter来实现分页查询:1. 添加依赖首先,在pom.xml文件中添加pagehelper-spring-boot-starter的依赖:<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>{version}</version> </dependency>2. 配置属性在application.properties或application.yml中配置pagehelper相关属性,例如:# PageHelper配置 pagehelper.helperDialect=mysql pagehelper.reasonable=true pagehelper.supportMethodsArguments=true pagehelper.params=count=countSql3. 编写Mapper接口和XML文件编写MyBatis的Mapper接口和对应的XML文件,定义分页查询的方法。4. 在Service中调用分页查询在Service类中调用分页查询方法,示例代码如下:@Service public class UserService { @Autowired private UserMapper userMapper; public PageInfo<User> findUsers(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<User> userList = userMapper.selectUsers(); return new PageInfo<>(userList); } }5. Controller层调用Service进行分页查询在Controller层调用Service中的方法进行分页查询,并返回分页结果给前端。示例代码如下:@RestController public class UserController { @Autowired private UserService userService; @GetMapping("/users") public PageInfo<User> getUsers(@RequestParam("pageNum") int pageNum, @RequestParam("pageSize") int pageSize) { return userService.findUsers(pageNum, pageSize); } }通过以上步骤,你就可以在Spring Boot项目中使用pagehelper-spring-boot-starter来实现分页查询功能。记得根据具体的业务需求和数据库类型配置相应的pagehelper属性转载自https://www.cnblogs.com/lori/p/18311038
-
介绍Spring Boot 提供了事件驱动的编程模型,允许你在应用程序中定义和监听自定义事件。这种机制使得组件之间可以进行松耦合的通信。应用使用场景解耦业务逻辑:通过事件机制,可以让不同模块或组件之间的业务逻辑解耦。异步处理:某些操作可以通过事件机制异步处理,提升系统响应速度。扩展性:可以很容易地添加新的事件处理器,而不会影响现有代码。原理解释Spring 的事件机制基于 ApplicationEvent 和 ApplicationListener。ApplicationEvent 是事件的基类,所有自定义事件都需要继承它,而 ApplicationListener 是事件监听器接口,用于接收事件通知。算法原理流程图+-----------------+| Application || Context |+--------+--------+ | v+--------+--------+| Raise Event || (ApplicationEvent)|+--------+--------+ | v+--------+--------+| Event Listener || (ApplicationListener) |+--------+--------+ | v+-----------------+| Handle Event |+-----------------+算法原理解释启动上下文:Spring 容器启动并初始化 ApplicationContext。触发事件:当某个事件发生时,调用 ApplicationEventPublisher#publishEvent() 方法发布事件。监听事件:实现 ApplicationListener 接口的 bean 会自动注册为事件监听器,当事件被触发时,监听器会收到事件通知并执行对应的业务逻辑。应用场景代码示例实现1. 定义自定义事件public class CustomEvent extends ApplicationEvent { private String message; public CustomEvent(Object source, String message) { super(source); this.message = message; } public String getMessage() { return message; }}2. 定义事件监听器import org.springframework.context.event.EventListener;import org.springframework.stereotype.Component;@Componentpublic class CustomEventListener { @EventListener public void handleCustomEvent(CustomEvent event) { System.out.println("Received custom event - " + event.getMessage()); }}3. 发布事件import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationEventPublisher;import org.springframework.stereotype.Service;@Servicepublic class CustomEventPublisher { @Autowired private ApplicationEventPublisher applicationEventPublisher; public void publishEvent(String message) { CustomEvent customEvent = new CustomEvent(this, message); applicationEventPublisher.publishEvent(customEvent); }}4. 启动应用程序并测试创建一个简单的 REST Controller 来触发事件:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class EventController { @Autowired private CustomEventPublisher customEventPublisher; @GetMapping("/trigger-event") public String triggerEvent(@RequestParam String message) { customEventPublisher.publishEvent(message); return "Event triggered!"; }}部署测试场景启动应用程序:使用 mvn spring-boot:run 启动 Spring Boot 应用。触发事件:访问 http://localhost:8080/trigger-event?message=HelloWorld,检查控制台输出。材料链接Spring 官方文档:事件Spring Boot 官方文档总结Spring 的事件机制提供了一种优雅的方式来解耦组件间的依赖关系,同时也能方便地实现异步处理。通过 ApplicationEvent 和 ApplicationListener,我们可以方便地定义、发布和监听事件,从而构建灵活且可扩展的系统架构。未来展望随着微服务架构的普及,事件驱动架构将会越来越重要。未来,我们可能会看到更多基于消息队列(如 Kafka 或 RabbitMQ)的事件驱动解决方案与 Spring 集成,以更好地支持分布式系统中的事件处理。此外,结合云计算平台(如 AWS Lambda)和无服务器架构,也将进一步拓展事件驱动编程的应用范围。
-
Spring Boot中的参数逐级传递:优雅的实现与选择策略在Spring Boot应用程序中,经常需要将数据或参数从一个组件或服务传递到另一个。当业务逻辑涉及到多层调用时,如何高效地传递这些参数就显得尤为重要。下面,我们将探讨几种不同的方法来实现参数的逐级传递,并分析何时使用参数传递,何时考虑全局变量。1. 方法参数传递最直接的方式是通过方法参数进行传递。这种方式在层与层之间的调用中非常常见。public class ServiceA { private ServiceB serviceB; public void performAction(String param) { // ... some logic ... serviceB.anotherAction(param); } } public class ServiceB { public void anotherAction(String param) { // ... use the param ... } }优点:明确性:参数传递清晰表明了哪些数据是必需的,以及数据的流向。封装性:每一层都只处理它需要的数据,不暴露不必要的信息。缺点:当层级较多时,参数需要在多个方法间传递,可能导致代码冗余。如果多个方法都需要相同的参数集,可能会导致重复的参数列表。2. 使用上下文对象或DTO(Data Transfer Object)当需要在多个层级间传递多个参数时,可以考虑将这些参数封装到一个上下文对象或DTO中。public class ActionContext { private String param1; private int param2; // getters, setters, etc. } public class ServiceA { private ServiceB serviceB; public void performAction(ActionContext context) { // ... some logic ... serviceB.anotherAction(context); } } public class ServiceB { public void anotherAction(ActionContext context) { // ... use the context ... } }优点:减少方法签名的复杂性,特别是当需要传递多个参数时。提供了更好的封装性,可以隐藏实现细节。缺点:可能引入不必要的复杂性,特别是当上下文对象变得庞大时。需要谨慎管理上下文对象的生命周期和可见性。3. 使用全局变量或线程局部变量在某些情况下,可以考虑使用全局变量或线程局部变量来传递信息。Spring Boot中可以使用@RequestScope或ThreadLocal来实现。优点:避免在多个层级间显式传递参数。在某些场景下(如跨多个服务或组件共享状态),可能更为方便。缺点:可能导致代码难以理解和维护,因为状态是在全局范围内共享的。需要谨慎管理线程安全和生命周期问题。可能导致测试困难,因为全局状态可能在测试之间意外地持续存在。何时使用参数,何时使用全局变量?使用参数的场景:当数据只在少数几个方法间传递时。当需要明确指示数据的来源和去向时。当希望保持代码的清晰性和可维护性时。使用全局变量的场景:当需要在多个不同层级和服务间共享状态时。当显式参数传递变得过于复杂或冗余时。需要注意,过度使用全局变量可能导致代码难以理解和维护,因此应谨慎使用。总的来说,在Spring Boot应用中,参数逐级传递的策略应根据具体的应用场景和需求来选择。在大多数情况下,通过方法参数或上下文对象进行传递是更清晰、更可维护的选择。全局变量或线程局部变量应在必要时谨慎使用,以避免引入不必要的复杂性和潜在的问题。
-
在Spring Boot中,校验参数的合法性通常涉及两个方面:一是定义参数校验的规则,二是确保在请求处理之前执行这些校验。下面我们先从理论层面介绍如何优雅地进行参数校验,然后给出一个实战的例子。理论部分使用校验注解: Spring Boot支持JSR 303/380校验规范,它提供了一系列注解,如@NotNull、@Min、@Max、@Email等,用于定义参数的校验规则。这些注解可以直接加在请求方法的参数上或者DTO(Data Transfer Object)类的字段上。使用DTO: 对于复杂的请求参数,建议使用DTO来封装参数。这样做的好处是:可以将校验规则与请求方法解耦,使代码更清晰。可以方便地复用DTO,例如在不同的服务层或控制器中。全局异常处理: 当参数校验失败时,Spring Boot会抛出一个MethodArgumentNotValidException异常。为了给用户一个友好的提示,我们需要全局捕获这个异常,并返回自定义的错误信息。分组校验: 对于同一个DTO,在不同的业务场景下可能需要不同的校验规则。这时可以使用JSR 303的分组校验功能。自定义校验注解: 如果JSR 303提供的注解无法满足需求,可以自定义校验注解。实战例子假设我们有一个用户注册接口,需要校验用户名、密码和邮箱的合法性。定义DTOimport javax.validation.constraints.*; public class UserDTO { @NotNull(message = "用户名不能为空") @Size(min = 4, max = 20, message = "用户名长度必须在4到20个字符之间") private String username; @NotNull(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度必须在6到20个字符之间") private String password; @NotNull(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; // getters and setters }在控制器中使用DTOimport org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController @Validated public class UserController { @PostMapping("/register") public String register(@RequestBody @Validated UserDTO userDTO) { // 处理注册逻辑 return "注册成功"; } }注意:在@RequestBody和@Validated注解之间没有任何参数,这是因为@Validated默认会校验其后面的对象(即UserDTO)。全局异常处理import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.List; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException ex) { List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors(); String errorMessage = fieldErrors.stream() .map(FieldError::getDefaultMessage) .reduce((s1, s2) -> s1 + "; " + s2) .orElse("Validation failed"); return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); } }这样,当参数校验失败时,用户会收到一个包含具体错误信息的HTTP 400响应。
-
在Spring Boot中,过滤器(Filter)和拦截器(Interceptor)都用于在请求处理过程中执行特定逻辑,但它们之间存在一些关键区别。以下是关于两者区别的详细解释以及使用场景的建议:过滤器和拦截器的区别触发时机:过滤器(Filter)是在请求进入容器后,但请求进入Servlet之前进行预处理的。在响应返回时,它也是在Servlet处理完后,返回给前端之前进行后处理的。拦截器(Interceptor)的触发时机稍微晚一些,它位于DispatcherServlet内部,即请求已经到达Spring MVC的控制器之前。响应时,它也是在控制器方法执行完毕之后,但在视图渲染之前。依赖与管理:过滤器(Filter)是Java EE标准的一部分,它依赖于Servlet API,不需要依赖Spring。它的实现基于回调函数,并由Servlet容器管理其生命周期。拦截器(Interceptor)则是Spring MVC提供的功能,基于Java的反射机制。它是Spring管理的一个组件,可以获取和使用IoC容器中的bean,包括service层等。功能与应用范围:过滤器(Filter)可以应用于几乎所有的请求,包括静态资源请求。它主要用于对请求和响应进行预处理和后处理,如编码设置、CORS设置等。拦截器(Interceptor)只能对Spring MVC的控制器请求起作用,不能用于静态资源请求。它主要用于处理与请求相关的业务逻辑,如日志记录、权限验证、请求参数处理等。访问权限:过滤器(Filter)无法直接访问到请求的具体控制器和方法信息,因为它工作在Servlet容器级别。拦截器(Interceptor)可以访问到请求的具体控制器和方法信息,因为它工作在Spring MVC的控制器级别。使用场景建议使用过滤器(Filter)的场景:需要对请求和响应进行预处理和后处理,如设置请求编码、添加响应头、处理跨域请求等。需要对静态资源请求进行过滤处理。使用拦截器(Interceptor)的场景:需要处理与请求相关的业务逻辑,如日志记录、权限验证、请求参数处理等。需要访问到请求的具体控制器和方法信息。总结过滤器和拦截器在Spring Boot中各有其用途和优势。选择使用哪一个取决于你的具体需求和应用场景。如果你需要对请求和响应进行底层处理,或者处理静态资源请求,那么应该选择使用过滤器。如果你需要处理与请求相关的业务逻辑,或者需要访问到请求的具体控制器和方法信息,那么应该选择使用拦截器。
-
在Spring Boot中,除了拦截器(Interceptor)之外,还可以使用过滤器(Filter)来在请求处理之前和之后执行一些逻辑。过滤器是Servlet API的一部分,与Spring MVC的拦截器不同,但它们在处理HTTP请求时都扮演着重要的角色。过滤器在Servlet容器级别工作,因此它们不仅可以应用于Spring MVC控制器,还可以应用于任何其他Servlet。它们通常用于日志记录、编码设置、请求和响应的包装等任务。以下是在Spring Boot中使用过滤器的步骤:创建过滤器实现javax.servlet.Filter接口,并重写doFilter方法。这个方法会在每次请求-响应交换时调用。import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component public class MyFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; // 在请求处理之前执行一些逻辑 System.out.println("MyFilter - Before request processing"); // 继续处理请求 filterChain.doFilter(servletRequest, servletResponse); // 在请求处理之后执行一些逻辑 System.out.println("MyFilter - After request processing"); } @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化逻辑(如果需要) } @Override public void destroy() { // 销毁逻辑(如果需要) } }注意:在Spring Boot中,即使你使用@Component注解,过滤器也不会自动注册。你需要手动注册它。注册过滤器你可以通过几种方式注册过滤器:使用@WebFilter注解(但通常与Spring Boot的自动配置不兼容,因此不推荐):@WebFilter(urlPatterns = "/*") @Component public class MyFilter implements Filter { // ... }在FilterRegistrationBean中注册:import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<MyFilter> myFilterRegistration() { FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new MyFilter()); registration.addUrlPatterns("/*"); return registration; } }使用@ServletComponentScan注解(如果过滤器上有@WebFilter):在启动类上添加@ServletComponentScan注解,Spring Boot将扫描带有@WebFilter、@WebListener和@WebServlet注解的类,并自动注册它们。测试过滤器启动Spring Boot应用并发送请求以测试过滤器是否按预期工作。你可以通过查看日志输出或使用调试工具来验证过滤器的行为。过滤器在Spring Boot应用中非常有用,特别是当你需要在Servlet容器级别执行一些通用逻辑时。与拦截器相比,过滤器通常更底层,并且可以在整个Web应用中使用,而不仅仅是Spring MVC控制器。
-
前言在Spring Boot中,拦截器(Interceptor)是一种用于在请求被处理之前和之后执行特定逻辑的强大工具。它们通常用于执行诸如日志记录、身份验证、授权、性能监控等任务。在Spring Boot应用中,拦截器可以通过实现HandlerInterceptor接口或扩展HandlerInterceptorAdapter类来创建。以下是使用Spring Boot拦截器的基本步骤:1. 创建拦截器通过实现HandlerInterceptor接口或扩展HandlerInterceptorAdapter类,定义你的拦截器。你需要至少覆盖以下三个方法中的一个或多个:preHandle(HttpServletRequest request, HttpServletResponse response, Object handler): 在请求处理之前调用。如果返回false,则请求将被拒绝,不会继续执行后续的拦截器和控制器方法。postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView): 在请求处理之后,但在视图渲染之前调用。afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): 在整个请求处理完成后调用,即视图渲染之后。示例:@Component public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 在这里执行你的逻辑,例如记录日志或检查权限 // 如果返回false,请求将被拒绝 return true; } // 可以选择性地覆盖postHandle和afterCompletion方法 }2. 注册拦截器在Spring Boot中,你需要将拦截器注册到WebMvcConfigurer的addInterceptors方法中。你可以通过实现WebMvcConfigurer接口或扩展WebMvcConfigurerAdapter(如果你使用的是较旧的Spring Boot版本)来完成这一步骤。示例:@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private MyInterceptor myInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/error", "/static/**", "/css/**", "/js/**", "/images/**"); // 排除某些路径 } }在上面的示例中,addPathPatterns方法指定了哪些路径应该被拦截,而excludePathPatterns方法则指定了哪些路径应该被排除在拦截之外。3. 测试拦截器启动你的Spring Boot应用,并发送请求以测试拦截器是否按预期工作。你可以通过查看日志输出、检查响应内容或使用调试工具来验证拦截器的行为。4. postHandle 和 afterCompletion 的区别在Spring Boot中,拦截器(Interceptor)是Spring MVC框架中的一个重要组件,它用于在请求处理过程中执行特定的逻辑。拦截器通常实现HandlerInterceptor接口,该接口定义了三个方法:preHandle、postHandle和afterCompletion。下面我将详细解释postHandle和afterCompletion方法的触发时机:postHandle方法:函数声明:void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception;说明:此方法在控制器处理请求方法调用之后,在解析视图之前执行。可以通过此方法对请求域中的模型和视图做进一步修改。如果preHandle方法返回false,则此方法不会被执行。参数:HttpServletRequest request:当前的HTTP请求。HttpServletResponse response:当前的HTTP响应。Object handler:被调用的处理器(通常是HandlerMethod或HandlerAdapter的实例)。@Nullable ModelAndView modelAndView:控制器返回的ModelAndView对象(如果有的话),可以为null。触发时机:在控制器方法成功执行后,但在视图渲染之前。也就是说,当请求被控制器处理完毕并返回ModelAndView对象(或者相应的响应)时,postHandle方法会被触发。作用:这个方法可以对控制器方法返回的ModelAndView对象进行后处理,例如添加额外的模型属性或修改响应内容。细节:如果preHandle方法返回false,则postHandle方法将不会被调用,因为请求已经被拦截器阻止,没有进入控制器方法。afterCompletion方法:函数声明:void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception;说明:此方法在视图渲染完成之后执行,通常用于资源清理、记录日志信息等工作。此方法只有在preHandle方法被成功执行并返回true时才会被执行。参数:HttpServletRequest request:当前的HTTP请求。HttpServletResponse response:当前的HTTP响应。Object handler:被调用的处理器(与postHandle相同)。@Nullable Exception ex:在请求处理过程中出现的异常(如果有的话),可以为null。触发时机:在整个请求处理完成之后,包括控制器方法的执行和视图的渲染。具体来说,当DispatcherServlet处理完请求并返回给客户端后,afterCompletion方法会被触发。作用:这个方法通常用于清理资源、记录日志或执行一些需要在请求结束后执行的逻辑。细节:即使preHandle方法返回false导致请求没有进入控制器方法,afterCompletion方法仍然会被调用(前提是该拦截器在preHandle中返回了true或者根本没有preHandle方法)。但需要注意的是,如果在preHandle中返回了false,那么postHandle方法将不会被调用,即使afterCompletion会。总结:postHandle在控制器方法执行之后、视图渲染之前被触发,用于对控制器方法的返回值进行后处理。afterCompletion在整个请求处理完成之后被触发,包括控制器方法和视图渲染,用于执行一些需要在请求结束后执行的逻辑。这两个方法提供了在请求处理过程中的不同阶段执行额外逻辑的能力,使得开发者能够更加灵活地控制请求的处理流程。
-
使用Spring Boot开发API的时候,读取请求参数是服务端编码中最基本的一项操作,Spring Boot中也提供了多种机制来满足不同的API设计要求。接下来,就通过本文,为大家总结6种常用的请求参数读取方式。如果你发现自己知道的不到6种,那么赶紧来查漏补缺一下。如果你知道的不止6种,那么告诉大家,一起互相学习一下吧~@RequestParam这是最最最最最最常用的一个了吧,用来加载URL中?之后的参数。比如:这个请求/user?name=didispace 就可以如下面这样,使用@RequestParam来加载URL中的name参数@GetMapping("/user") @ResponseBody() public User findUserByName(@RequestParam("name") String name){ return userRepo.findByName(name); }@PathVariable这是RESTful风格API中常用的注解,用来加载URL路径中的参数比如:这个请求/user/1 就可以如下面这样,使用@PathVariable来加载URL中的id参数@GetMapping("/user/{id}") @ResponseBody() public User findUserById(@PathVariable("id") String id){ return userRepo.findById(id); }@MatrixVariable这个我们用的并不是很多,但一些国外系统有提供这类API参数,这种API的参数通过;分割。比如:这个请求/books/reviews;isbn=1234;topN=5; 就可以如下面这样,使用@MatrixVariable来加载URL中用;分割的参数@GetMapping("/books/reviews") @ResponseBody() public List<BookReview> getBookReviews( @MatrixVariable String isbn, @MatrixVariable Integer topN) { return bookReviewsLogic.getTopNReviewsByIsbn(isbn, topN); }@RequestBody这也是最常用的一个注解,用来加载POST/PUT请求的复杂请求体(也叫:payload)。比如,客户端需要提交一个复杂数据的时候,就要将这些数据放到请求体中,然后服务端用@RequestBody来加载请求体中的数据@PostMapping("/add") public boolean addAccounts(@RequestBody List<Account> accounts) throws SQLException { accounts.stream().forEach(a -> { a.setCreatedOn(Timestamp.from(Instant.now())); a.setLastLogin(Timestamp.from(Instant.now())); }); return notificationLogic.addAccounts(accounts); }@RequestHeader@RequestHeader注解用来加载请求头中的数据,一般在业务系统中不太使用,但在基础设施的建设中会比较常用,比如传递分布式系统的TraceID等。用法也很简单,比如,假设我们将鉴权数据存在http请求头中,那么就可以像下面这样用@RequestHeader来加载请求头中的Authorization参数@GetMapping("/user") @ResponseBody() public List<User> getUserList(@RequestHeader("Authorization") String authToken) { return userRepo.findAll(); }@CookieValue当我们需要与客户端保持有状态的交互时,就需要用到Cookie。此时,服务端读取Cookie数据的时候,就可以像下面这样用@CookieValue来读取Cookie中的SessionId数据 @GetMapping("/user") @ResponseBody() public List<User> getUserList(@CookieValue(name = "SessionId") String sessionId) { return userRepo.findAll(); }
-
@DateTimeFormat 和 @JsonFormat 是 Spring 和 Jackson 中用于处理日期时间格式的注解,它们有不同的作用:@DateTimeFormat@DateTimeFormat 是 Spring 框架提供的注解,用于指定字符串如何转换为日期时间类型,以及如何格式化日期时间类型成字符串。通常用于 Spring MVC 控制器方法的参数或对象属性上。示例用法:public class MyRequest { @DateTimeFormat(pattern = "yyyy-MM-dd") private Date date; // getters and setters }在这个示例中,@DateTimeFormat 注解指定了日期字符串的格式,以便将其转换为 Date 类型。@JsonFormat@JsonFormat 是 Jackson 库提供的注解,用于指定 JSON 序列化和反序列化时日期时间类型的格式。通常用于 POJO 类的属性上,以影响 JSON 格式的输出。示例用法:public class MyResponse { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private Date dateTime; // getters and setters }在这个示例中,@JsonFormat 注解指定了日期时间的格式,以便 JSON 序列化和反序列化时使用。区别@DateTimeFormat 是 Spring 框架提供的,用于处理字符串到日期时间类型的转换和格式化。@JsonFormat 是 Jackson 库提供的,用于处理 JSON 格式到日期时间类型的转换和格式化。虽然它们的功能有些重叠,但它们的使用场景不同:@DateTimeFormat 用于处理 HTTP 请求参数的转换,而 @JsonFormat 用于处理 JSON 数据的序列化和反序列化。实际应用场景在Controller中使用Java对象接收前端传来的查询参数,这个时候需要使用@DateTimeFormat来格式化前端传来的日期格式,如果这个对象只是作为查询参数,那么只需要加@DateTimeFormat 这一个注解就够了,如果同时作为返回VO,那么就得加上 @JsonFormat用于 JSON 数据的序列化和反序列化。还有一个场景,就是对象虽然没有作为VO使用,但是作为Feign接口的查询对象,这个时候也涉及了 JSON 数据的序列化和反序列化,所以也得加上@JsonFormat。另外,Feign不支持使用GET请求但是使用对象作为参数,如果要使用对象作为参数,必须适应POST方法。
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签